Skip to content

Commit b7a0be7

Browse files
fix(app-shell): Installed Apps shows the engine's reason for a register-refused app (#31662)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbGpSrzUPSqJztUEqtbhr58C)_ Fixes #31651 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:控制台的 `LocalInstallNotLoaded` 类型改成和服务端一样的两种形态(协议不兼容 `{ code, requiredRange }`、注册被拒 `{ code, message }`);新增一个共用的原因组件 `MarketplaceNotLoadedReason`,Setup → Packages 的行和应用详情页头部都用它。注册被拒的应用现在在“未加载(代码)”后面显示引擎给的原句。 - 为什么改:#31566(PR #31650)之后服务端会给被引擎拒绝注册的安装记录带上原因原句,但控制台只认协议那一种,只显示代码,管理员看不出该卸载还是修包。 - 风险与回滚:只动控制台三个文件;协议不兼容那种的显示逐字不变(DOM 也不变),没有原句或原句为空时和以前一样只显示代码;不加语言包键。回滚即撤销本 PR。 - 先看:`MarketplaceRowActions.tsx` 里新增的 `MarketplaceNotLoadedReason`(原句以文本节点渲染);再看 `marketplaceApi.ts` 的联合类型。 - 两分钟验证:在 `ui/` 下跑 `pnpm exec vitest run packages/app-shell/src/console/marketplace/__tests__/MarketplaceRowActions.installedAppsPins.test.tsx -t 31566`,预期新增用例全绿。 - 需要你做的:无,席位落地。 ## What changes - **`marketplaceApi.ts`**: `LocalInstallNotLoaded` is now the server's closed union (`NotLoadedMarker` in `@objectstack/cloud-connection`, PR #31650): `LocalInstallNotLoadedProtocol` `{ code: 'OS_PROTOCOL_INCOMPATIBLE', requiredRange }` or `LocalInstallNotLoadedRefused` `{ code, message }`. Mirrored member by member, never widened to a record. - **`MarketplaceRowActions.tsx`**: one reader, `MarketplaceNotLoadedReason`, replaces the two copies of the reason ternary. - The protocol arm is told by its code, as before, and draws `marketplace.notLoaded.protocolIncompatible` in the same single span. - Any other code draws `marketplace.notLoaded.otherReason` with the code. When the marker carries a non-blank `message`, the engine's sentence follows it in its own block span, as a React text node, never as markup. - A marker with no `message`, an empty one or a blank one reads with the code alone, exactly as today. So does any other unknown shape. - **`MarketplacePackagePage.tsx`**: the header's `notLoadedReason` closure is gone; both header placements draw `MarketplaceNotLoadedReason`. - **Locale call (step 0): no new key.** The register-refusal arm reuses `otherReason` (which already names the code) and appends the engine's sentence as separate text, so `Clause-②: no` holds. No language pack changes. None of these names is exported from `@object-ui/app-shell`'s `.` entry, so its public surface is byte-unchanged. - Changeset: `.changeset/31651-not-loaded-engine-reason.md` (`@object-ui/app-shell` patch). ## Tests New pins go into the two existing harness files as top-level `describe`s: - `MarketplacePackagePage.notLoadedHeader-11760.test.tsx` (package page header, en and zh, catalog view and offline local view): a `{ code, message }` marker shows the badge, the code's sentence and the engine's sentence, and not "Installed · vX". A markup-shaped sentence is drawn as literal text with no element. A marker with no, empty or blank `message` reads `otherReason` alone, with no engine-sentence node. - `MarketplaceRowActions.installedAppsPins.test.tsx` (Setup → Packages row, en and zh-Hans): the same message, markup and fallback cases on the row, plus a CONTROL: a protocol marker beside a register-refused one reads the protocol sentence exactly, with no engine sentence. - CONTROL: the existing 11760 and 11645 pins are unedited and green, including the unknown-code case (`OS_SOME_LATER_REFUSAL` with a stray `requiredRange`), which still reads `otherReason` and never shows the range. ## Acceptance notes - The engine's sentence is English server text and is drawn after the localized sentence in every language. Translating it would need a sentence per refusal code, which the server does not serve. - Not changed, per the server PR's own notes: the sample-data doors (reseed, purge) refuse only a protocol-incompatible entry. On the console side neither door is offered for any `notLoaded` entry, both arms included. ## Verification Head `d91784fe`: the fix `a5de5906` and changeset `c0121833`, with origin/main merged (`8c9260f7`; the three incoming commits touch `plugin-auth`, `metadata` and `metadata-protocol`, none of this diff's files, so the ui readings below carry over). Branch cut from `532230f32`. <details><summary>Tests (ui vitest, under the verify lock)</summary> `pnpm exec vitest run` from `ui/` on the four `notLoaded` files (`MarketplacePackagePage.notLoadedHeader-11760`, `MarketplaceRowActions.installedAppsPins`, `MarketplacePage.notLoadedBadge-11760`, `MarketplacePackagePage.notLoadedMenu-11645`): `Test Files 4 passed (4)`, `Tests 164 passed (164)`. `pnpm exec turbo run type-check --filter=@object-ui/app-shell --concurrency=2`: `Tasks: 69 successful, 69 total` (app-shell's own type-check covers its test files). `eslint` on the five touched files: 0 errors (53 warnings, all `no-explicit-any` in untouched lines). </details> <details><summary>Ablation (scripts/ablation-replace.mjs, anchor x1 -> x0, restored blob == HEAD 3e32477 and git diff HEAD empty)</summary> The message arm removed: `if (!engineMessage) return ...` replaced by an unconditional return of the code's sentence. The two harness files: `Tests 8 failed | 142 passed (150)`. The 8 are every new message pin: the header case in en/zh x two views, the row case in en/zh-Hans, and both markup cases. The fallback cases and the protocol CONTROL stay green, as they should. The component is imported from `src` by a relative path (no dist leg). </details> <details><summary>Gates</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 37 derived commands, each run at `c0121833` with exit 0 (`check:type-check-debt` under the verify lock); `--ran` reconciliation: `37 derived, 37 run, 0 NOT-MEASURED, 0 UNRUN`. `--tier`: no path-derived mandate. Examples census (`git grep` of each touched source path in `ui/examples/**/test/**` and `ui/e2e/**`): no hits. Changed lines 235 (+209 / -26). The body gates (`check-changeset-no-major`, `check-adr-0087-registration`, `check-closing-keyword-parity`) and `check-sdui-manifest` were re-run on `d91784fe` with this body. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01QsR2jTQrsZdgRYLfQjYo6u)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent df91a35 commit b7a0be7

6 files changed

Lines changed: 209 additions & 26 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
fix(app-shell): Installed Apps shows the engine's reason for an app whose register it refused at boot
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
Since `@objectstack/cloud-connection` lists a ledgered install whose register the engine refuses at boot as `notLoaded: { code, message }`, the install-local marker is a closed union of two arms: the protocol arm `{ code: 'OS_PROTOCOL_INCOMPATIBLE', requiredRange }` and the register-refusal arm `{ code, message }`, where `message` is the engine's own sentence. The console typed the marker as the protocol arm only and never read `message`, so a register-refused app read "Not loaded" with its code and no reason. `LocalInstallNotLoaded` now mirrors the two arms, and one reader, `MarketplaceNotLoadedReason`, draws the reason on both the Setup → Packages row and the package page header. The protocol arm reads exactly as before. The register-refusal arm reads the existing "did not load this package (CODE)" sentence followed by the engine's sentence, so the operator can tell whether to uninstall the app or fix the package. The engine's sentence is drawn as text, never as markup. A marker with no sentence, or an empty one, reads with the code alone, as before. No language pack changes.

‎ui/packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx‎

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ import { PluginDisclosure } from './PluginDisclosure.js';
6464
import { MarketplaceAccessDenied } from './MarketplaceAccessDenied.js';
6565
import { MarketplaceResolving } from './MarketplaceResolving.js';
6666
import { MarketplaceDisabled } from './MarketplaceDisabled.js';
67+
import { MarketplaceNotLoadedReason } from './MarketplaceRowActions.js';
6768
import { localizePackage } from './usePackageL10n.js';
6869
import { isNewerVersion } from './versionPrecedence.js';
6970
import {
@@ -86,7 +87,6 @@ import {
8687
type MarketplaceDetailResponse,
8788
type CloudEnvironment,
8889
type LocalInstallEntry,
89-
type LocalInstallNotLoaded,
9090
type LocalInstallMode,
9191
type LocalInstallStrippedLock,
9292
type CloudInstallationInfo,
@@ -1035,16 +1035,12 @@ function MarketplacePackagePageBody() {
10351035
{t('marketplace.detail.installedV', { version: localInstall.version })}
10361036
</Badge>
10371037
));
1038-
// The reason in plain words. A code the console has no sentence for is still
1039-
// read as not loaded, naming the code — Installed Apps' reading of the marker.
1040-
const notLoadedReason = (marker: LocalInstallNotLoaded): string =>
1041-
marker.code === 'OS_PROTOCOL_INCOMPATIBLE'
1042-
? t('marketplace.notLoaded.protocolIncompatible', { requiredRange: marker.requiredRange })
1043-
: t('marketplace.notLoaded.otherReason', { code: marker.code });
1038+
// The reason in plain words, through Installed Apps' one reader of the
1039+
// marker: the protocol range, or the code with the engine's sentence.
10441040
const localNotLoadedReason = localInstall?.notLoaded && (
10451041
<p className="text-xs text-destructive mt-2 flex items-start gap-1.5">
10461042
<AlertCircle className="h-3.5 w-3.5 mt-px shrink-0" aria-hidden="true" />
1047-
<span>{notLoadedReason(localInstall.notLoaded)}</span>
1043+
<MarketplaceNotLoadedReason marker={localInstall.notLoaded} />
10481044
</p>
10491045
);
10501046
const localMenu = localInstall && (

‎ui/packages/app-shell/src/console/marketplace/MarketplaceRowActions.tsx‎

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,25 +37,59 @@ import {
3737
} from '@object-ui/components';
3838
import { AlertCircle, ExternalLink, Trash2 } from 'lucide-react';
3939
import { useObjectTranslation } from '@object-ui/i18n';
40-
import { uninstallLocal, type LocalInstallEntry, type LocalInstallNotLoaded } from './marketplaceApi.js';
40+
import {
41+
uninstallLocal,
42+
type LocalInstallEntry,
43+
type LocalInstallNotLoaded,
44+
type LocalInstallNotLoadedProtocol,
45+
} from './marketplaceApi.js';
46+
47+
/** The protocol arm, told by its code alone, as the console has always read it. */
48+
const isProtocolMarker = (marker: LocalInstallNotLoaded): marker is LocalInstallNotLoadedProtocol =>
49+
marker.code === 'OS_PROTOCOL_INCOMPATIBLE';
50+
51+
/** The engine's sentence on a register-refusal marker, or '' when it carries none. */
52+
const engineMessageOf = (marker: LocalInstallNotLoaded): string =>
53+
'message' in marker && typeof marker.message === 'string' ? marker.message.trim() : '';
4154

4255
/**
43-
* The reason a `notLoaded` entry was refused, in plain words (objectui#11645).
44-
* A code the console has no sentence for still reads as not loaded, naming
45-
* the code, rather than hiding the row the operator needs to uninstall.
56+
* The reason a `notLoaded` entry was refused, in plain words: the ONE reader of
57+
* the marker, drawn by Installed Apps' row ({@link MarketplaceNotLoaded}) and
58+
* the package page's header (objectui#11645, objectui#11760).
59+
*
60+
* - The protocol arm names the range the package targets.
61+
* - Any other code reads as not loaded, naming the code, rather than hiding the
62+
* row the operator needs to uninstall. On the register-refusal arm the
63+
* engine's sentence follows it (objectstack#31566), so the operator can tell
64+
* whether to uninstall the app or fix the package. It is server text and is
65+
* drawn as a text node, never as markup. A marker with no sentence, or an
66+
* empty one, reads with the code alone.
4667
*/
68+
export function MarketplaceNotLoadedReason({ marker }: { marker: LocalInstallNotLoaded }) {
69+
const { t } = useObjectTranslation();
70+
if (isProtocolMarker(marker)) {
71+
return <span>{t('marketplace.notLoaded.protocolIncompatible', { requiredRange: marker.requiredRange })}</span>;
72+
}
73+
const sentence = t('marketplace.notLoaded.otherReason', { code: marker.code });
74+
const engineMessage = engineMessageOf(marker);
75+
if (!engineMessage) return <span>{sentence}</span>;
76+
return (
77+
<span className="min-w-0">
78+
<span>{sentence}</span>{' '}
79+
<span className="block break-words" data-testid="marketplace-not-loaded-engine-message">{engineMessage}</span>
80+
</span>
81+
);
82+
}
83+
84+
/** A `notLoaded` entry's badge and its reason (objectui#11645). */
4785
export function MarketplaceNotLoaded({ marker }: { marker: LocalInstallNotLoaded }) {
4886
const { t } = useObjectTranslation();
49-
const reason =
50-
marker.code === 'OS_PROTOCOL_INCOMPATIBLE'
51-
? t('marketplace.notLoaded.protocolIncompatible', { requiredRange: marker.requiredRange })
52-
: t('marketplace.notLoaded.otherReason', { code: marker.code });
5387
return (
5488
<div className="flex flex-col gap-1">
5589
<Badge variant="destructive" className="self-start">{t('marketplace.notLoaded.badge')}</Badge>
5690
<p className="text-xs text-destructive flex items-start gap-1.5">
5791
<AlertCircle className="h-3.5 w-3.5 mt-px shrink-0" aria-hidden="true" />
58-
<span>{reason}</span>
92+
<MarketplaceNotLoadedReason marker={marker} />
5993
</p>
6094
</div>
6195
);

‎ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.notLoadedHeader-11760.test.tsx‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,3 +256,62 @@ describe('Details — a refusal code the console has no sentence for (objectui#1
256256
expect(screen.queryByText(fill(en.marketplace.detail.installedV, { version: '3.2.0' }))).toBeNull();
257257
});
258258
});
259+
260+
/**
261+
* objectstack#31566 (PR objectstack#31650) adds the marker's second arm,
262+
* `{ code, message }`: the engine refused the package's register at boot, and
263+
* `message` is its own sentence. The header reads it through Installed Apps'
264+
* one reader (`MarketplaceNotLoadedReason`): the code's sentence, then the
265+
* engine's, as text. A marker with no sentence, or an empty one, reads with the
266+
* code alone. The protocol arm is pinned unchanged by the cases above.
267+
*/
268+
const ENGINE_MESSAGE =
269+
'Package "com.example.crm" declares the default page "crm_home", which is not a record page: '
270+
+ 'a default page must be a record page of its object.';
271+
const REGISTER_REFUSED = { ...REFUSED, notLoaded: { code: 'INVALID_METADATA', message: ENGINE_MESSAGE } };
272+
273+
describe.each([
274+
['en', EN, en as Pack],
275+
['zh', ZH, zh as unknown as Pack],
276+
])('Details — a register-refused local install shows the engine\'s reason (objectstack#31566) [%s]', (_lang, config, pack) => {
277+
const sentence = fill(pack.marketplace.notLoaded.otherReason, { code: 'INVALID_METADATA' });
278+
279+
describe.each([
280+
['the catalog view (marketplace on)', true],
281+
['the offline local view (marketplace off, objectui#11627)', false],
282+
])('%s', (_view, marketplace) => {
283+
it('the header carries the not-loaded badge, the code\'s sentence and the engine\'s sentence', async () => {
284+
await boot(marketplace, REGISTER_REFUSED);
285+
renderPage(config);
286+
287+
expect(await screen.findByText(pack.marketplace.notLoaded.badge)).toBeInTheDocument();
288+
expect(screen.getByText(sentence)).toBeInTheDocument();
289+
expect(screen.getByText(ENGINE_MESSAGE)).toBeInTheDocument();
290+
expect(screen.queryByText(fill(pack.marketplace.detail.installedV, { version: '3.2.0' }))).toBeNull();
291+
});
292+
});
293+
});
294+
295+
describe('Details — the engine\'s sentence is server text (objectstack#31566)', () => {
296+
it('a sentence shaped like markup is drawn as its literal text, never as elements', async () => {
297+
const markup = 'Object <b>crm_lead</b> is owned by package "com.example.base".';
298+
await boot(true, { ...REFUSED, notLoaded: { code: 'OBJECT_OWNERSHIP_CONFLICT', message: markup } });
299+
renderPage(EN);
300+
301+
expect(await screen.findByText(markup)).toBeInTheDocument();
302+
expect(document.body.querySelector('b')).toBeNull();
303+
});
304+
305+
it.each([
306+
['no `message` member', { code: 'INVALID_METADATA' }],
307+
['an empty `message`', { code: 'INVALID_METADATA', message: '' }],
308+
['a blank `message`', { code: 'INVALID_METADATA', message: ' ' }],
309+
])('a marker with %s reads with the code alone', async (_case, notLoaded) => {
310+
await boot(true, { ...REFUSED, notLoaded });
311+
renderPage(EN);
312+
313+
expect(await screen.findByText(en.marketplace.notLoaded.badge)).toBeInTheDocument();
314+
expect(screen.getByText(fill(en.marketplace.notLoaded.otherReason, { code: 'INVALID_METADATA' }))).toBeInTheDocument();
315+
expect(screen.queryByTestId('marketplace-not-loaded-engine-message')).toBeNull();
316+
});
317+
});

‎ui/packages/app-shell/src/console/marketplace/__tests__/MarketplaceRowActions.installedAppsPins.test.tsx‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,3 +433,69 @@ describe('no pack\'s uninstall texts say the app stays until a restart (objectui
433433
expect(value).not.toBe(enByKey.get(key));
434434
});
435435
});
436+
437+
/**
438+
* objectstack#31566 (PR objectstack#31650) adds the marker's second arm,
439+
* `{ code, message }`: the engine refused the package's register at boot, and
440+
* `message` is its own sentence. The row reads it through the one reader
441+
* (`MarketplaceNotLoadedReason`): the code's sentence, then the engine's, as
442+
* text, so the operator can tell whether to uninstall or fix the package. A
443+
* marker with no sentence, or an empty one, reads with the code alone. The
444+
* protocol arm is pinned unchanged by the cases above.
445+
*/
446+
const ENGINE_MESSAGE =
447+
'Package "com.example.crm" declares the default page "crm_home", which is not a record page: '
448+
+ 'a default page must be a record page of its object.';
449+
const REGISTER_REFUSED = { ...REFUSED, notLoaded: { code: 'INVALID_METADATA', message: ENGINE_MESSAGE } };
450+
451+
describe.each([
452+
['en', EN, en as Pack],
453+
['zh-Hans', ZH, zh as unknown as Pack],
454+
])('Packages — a register-refused marketplace install shows the engine\'s reason (objectstack#31566) [%s]', (_lang, config, pack) => {
455+
it('the row carries the badge, the code\'s sentence and the engine\'s sentence, and Uninstall', async () => {
456+
await serve([REGISTER_REFUSED, LOADED]);
457+
renderPackages(config);
458+
459+
const r = await row('com.example.crm');
460+
expect(within(r).getByText(pack.marketplace.notLoaded.badge)).toBeInTheDocument();
461+
expect(within(r).getByText(fill(pack.marketplace.notLoaded.otherReason, { code: 'INVALID_METADATA' }))).toBeInTheDocument();
462+
expect(within(r).getByText(ENGINE_MESSAGE)).toBeInTheDocument();
463+
expect(within(r).getByRole('button', { name: pack.marketplace.action.uninstall })).toBeEnabled();
464+
});
465+
466+
it('control: the protocol marker beside it reads exactly as before, with no engine sentence', async () => {
467+
await serve([REGISTER_REFUSED, { ...REFUSED, packageId: 'com.example.old', manifestId: 'com.example.old' }]);
468+
renderPackages(config);
469+
470+
const r = await row('com.example.old');
471+
expect(within(r).getByText(fill(pack.marketplace.notLoaded.protocolIncompatible, { requiredRange: '^16' }))).toBeInTheDocument();
472+
expect(within(r).queryByTestId('marketplace-not-loaded-engine-message')).toBeNull();
473+
expect(r.textContent).not.toContain(ENGINE_MESSAGE);
474+
});
475+
});
476+
477+
describe('Packages — the engine\'s sentence on a row is server text (objectstack#31566)', () => {
478+
it('a sentence shaped like markup is drawn as its literal text, never as elements', async () => {
479+
const markup = 'Object <b>crm_lead</b> is owned by package "com.example.base".';
480+
await serve([{ ...REFUSED, notLoaded: { code: 'OBJECT_OWNERSHIP_CONFLICT', message: markup } }]);
481+
renderPackages(EN);
482+
483+
const r = await row('com.example.crm');
484+
expect(within(r).getByText(markup)).toBeInTheDocument();
485+
expect(r.querySelector('b')).toBeNull();
486+
});
487+
488+
it.each([
489+
['no `message` member', { code: 'INVALID_METADATA' }],
490+
['an empty `message`', { code: 'INVALID_METADATA', message: '' }],
491+
['a blank `message`', { code: 'INVALID_METADATA', message: ' ' }],
492+
])('a marker with %s reads with the code alone', async (_case, notLoaded) => {
493+
await serve([{ ...REFUSED, notLoaded }]);
494+
renderPackages(EN);
495+
496+
const r = await row('com.example.crm');
497+
expect(within(r).getByText(en.marketplace.notLoaded.badge)).toBeInTheDocument();
498+
expect(within(r).getByText(fill(en.marketplace.notLoaded.otherReason, { code: 'INVALID_METADATA' }))).toBeInTheDocument();
499+
expect(within(r).queryByTestId('marketplace-not-loaded-engine-message')).toBeNull();
500+
});
501+
});

‎ui/packages/app-shell/src/console/marketplace/marketplaceApi.ts‎

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -783,20 +783,38 @@ export async function purgeSampleData(installationId: string): Promise<SampleDat
783783

784784
/**
785785
* The marker the install-local listing puts on an entry this runtime's startup
786-
* rehydrate refused to load (objectstack#21822, served since `@objectstack/*`
787-
* 17.7.0). CLOSED on the server: exactly these two members.
786+
* rehydrate refused to load. CLOSED on the server (`NotLoadedMarker` in
787+
* `@objectstack/cloud-connection`): exactly one of these two arms, each with
788+
* exactly these members.
788789
*
789-
* `code` is the refusal's error code. The one the server sends today is
790-
* `OS_PROTOCOL_INCOMPATIBLE`, and `requiredRange` is then the protocol range the
791-
* package declares (the same `requiredRange` the install door's 422 carries in
792-
* `error.details`). It is typed `string` rather than that literal because the
793-
* console names a code it has no sentence for instead of hiding the row.
790+
* - {@link LocalInstallNotLoadedProtocol}: the package targets a protocol range
791+
* this runtime does not support (objectstack#21822, served since
792+
* `@objectstack/*` 17.7.0).
793+
* - {@link LocalInstallNotLoadedRefused}: the engine refused the package's
794+
* register (objectstack#31566): `code` is the refusal's ADR-0112 code and
795+
* `message` the engine's own sentence, the one the boot logs.
796+
*
797+
* Read the reason through `notLoadedReason` (`MarketplaceRowActions.tsx`),
798+
* the one reader both Installed Apps and the package page draw.
794799
*/
795-
export interface LocalInstallNotLoaded {
796-
code: string;
800+
export type LocalInstallNotLoaded = LocalInstallNotLoadedProtocol | LocalInstallNotLoadedRefused;
801+
802+
/** The protocol arm: `requiredRange` is the range the package declares (the same `requiredRange` the install door's 422 carries in `error.details`). */
803+
export interface LocalInstallNotLoadedProtocol {
804+
code: 'OS_PROTOCOL_INCOMPATIBLE';
797805
requiredRange: string;
798806
}
799807

808+
/**
809+
* The register-refusal arm. `code` is typed `string` because the engine's
810+
* refusal codes are open to this console: it names a code it has no sentence
811+
* for instead of hiding the row. `message` is server text, drawn as text.
812+
*/
813+
export interface LocalInstallNotLoadedRefused {
814+
code: string;
815+
message: string;
816+
}
817+
800818
/**
801819
* One item of `GET /api/v1/marketplace/install-local` (`listLocalInstalls`), and
802820
* the shape `listInstalledPackages` maps the control plane's rows into.

0 commit comments

Comments
 (0)