sh3-core 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +3 -1
- package/dist/api.js +4 -0
- package/dist/contributions/index.d.ts +1 -1
- package/dist/contributions/index.js +1 -1
- package/dist/contributions/registry.d.ts +7 -0
- package/dist/contributions/registry.js +24 -4
- package/dist/contributions/registry.test.js +56 -1
- package/dist/contributions/types.d.ts +9 -0
- package/dist/layout/LayoutRenderer.svelte +1 -1
- package/dist/layout/tree-walk.js +6 -1
- package/dist/layout/types.d.ts +7 -0
- package/dist/overlays/FloatFrame.svelte +8 -2
- package/dist/overlays/float.js +6 -3
- package/dist/overlays/float.test.js +71 -0
- package/dist/primitives/widgets/IconToggleGroup.svelte +4 -1
- package/dist/primitives/widgets/Segmented.svelte +4 -1
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/runVerb.d.ts +10 -0
- package/dist/runtime/runVerb.js +97 -0
- package/dist/runtime/runVerb.test.d.ts +1 -0
- package/dist/runtime/runVerb.test.js +132 -0
- package/dist/sh3core-shard/AppInfoView.svelte +154 -0
- package/dist/sh3core-shard/AppInfoView.svelte.d.ts +11 -0
- package/dist/sh3core-shard/appActions.js +23 -5
- package/dist/shards/activate-contributions.test.js +31 -0
- package/dist/shards/activate-runtime.test.d.ts +1 -0
- package/dist/shards/activate-runtime.test.js +179 -0
- package/dist/shards/activate.svelte.js +20 -3
- package/dist/shards/registry.d.ts +11 -1
- package/dist/shards/registry.js +16 -4
- package/dist/shards/registry.test.js +24 -16
- package/dist/shards/types.d.ts +38 -1
- package/dist/shell-shard/ScrollbackView.svelte +40 -19
- package/dist/shell-shard/Terminal.svelte +55 -4
- package/dist/shell-shard/contract.d.ts +34 -0
- package/dist/shell-shard/dispatch-custom.test.js +48 -0
- package/dist/shell-shard/dispatch-gating.test.d.ts +1 -0
- package/dist/shell-shard/dispatch-gating.test.js +63 -0
- package/dist/shell-shard/dispatch-invoke.test.d.ts +1 -0
- package/dist/shell-shard/dispatch-invoke.test.js +214 -0
- package/dist/shell-shard/dispatch.d.ts +9 -1
- package/dist/shell-shard/dispatch.js +73 -2
- package/dist/shell-shard/output.d.ts +8 -1
- package/dist/shell-shard/output.js +17 -1
- package/dist/shell-shard/output.test.js +24 -5
- package/dist/shell-shard/registry-resolve.test.d.ts +1 -0
- package/dist/shell-shard/registry-resolve.test.js +26 -0
- package/dist/shell-shard/registry.d.ts +12 -1
- package/dist/shell-shard/registry.js +12 -1
- package/dist/shell-shard/shellApi.d.ts +3 -0
- package/dist/shell-shard/shellApi.js +142 -0
- package/dist/shell-shard/shellShard.svelte.d.ts +1 -7
- package/dist/shell-shard/shellShard.svelte.js +8 -163
- package/dist/shell-shard/terminal-dispatch.test.js +10 -3
- package/dist/shell-shard/toolbar/slots/BusySlot.svelte +35 -0
- package/dist/shell-shard/toolbar/slots/BusySlot.svelte.d.ts +7 -0
- package/dist/shell-shard/verbs/clear.js +1 -0
- package/dist/shell-shard/verbs/mode.js +1 -0
- package/dist/verbs/types.d.ts +68 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/*
|
|
3
|
+
* Read-only details modal for an app, opened by the home-card `app.info`
|
|
4
|
+
* action. Pulls the manifest from listRegisteredApps() and — when the app
|
|
5
|
+
* was installed from a registry — augments it with the catalog entry
|
|
6
|
+
* (description, author) and the InstalledPackage record (source registry,
|
|
7
|
+
* install date, contract version). Built-in apps show only manifest fields.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { AppManifest } from '../apps/types';
|
|
11
|
+
import type { InstalledPackage, PackageEntry } from '../registry/types';
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
manifest: AppManifest;
|
|
15
|
+
installed: InstalledPackage | null;
|
|
16
|
+
catalogEntry: PackageEntry | null;
|
|
17
|
+
close: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let { manifest, installed, catalogEntry, close }: Props = $props();
|
|
21
|
+
|
|
22
|
+
let installedDate = $derived.by(() => {
|
|
23
|
+
if (!installed) return null;
|
|
24
|
+
const d = new Date(installed.installedAt);
|
|
25
|
+
return isNaN(d.getTime()) ? installed.installedAt : d.toLocaleString();
|
|
26
|
+
});
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<div class="app-info-modal">
|
|
30
|
+
<header>
|
|
31
|
+
<h2>{manifest.label}</h2>
|
|
32
|
+
<code class="id">{manifest.id}</code>
|
|
33
|
+
</header>
|
|
34
|
+
|
|
35
|
+
<p class="version">
|
|
36
|
+
<span class="label">Version</span>
|
|
37
|
+
<code>v{manifest.version}</code>
|
|
38
|
+
{#if !installed}<span class="badge built-in">built-in</span>{/if}
|
|
39
|
+
</p>
|
|
40
|
+
|
|
41
|
+
{#if catalogEntry?.description}
|
|
42
|
+
<section class="description">
|
|
43
|
+
<span class="label">Description</span>
|
|
44
|
+
<p>{catalogEntry.description}</p>
|
|
45
|
+
</section>
|
|
46
|
+
{/if}
|
|
47
|
+
|
|
48
|
+
{#if catalogEntry?.author?.name}
|
|
49
|
+
<p class="row">
|
|
50
|
+
<span class="label">Author</span>
|
|
51
|
+
<span>{catalogEntry.author.name}</span>
|
|
52
|
+
</p>
|
|
53
|
+
{/if}
|
|
54
|
+
|
|
55
|
+
{#if manifest.requiredShards.length > 0}
|
|
56
|
+
<section class="row">
|
|
57
|
+
<span class="label">Required shards</span>
|
|
58
|
+
<ul class="chips">
|
|
59
|
+
{#each manifest.requiredShards as shard}
|
|
60
|
+
<li><code>{shard}</code></li>
|
|
61
|
+
{/each}
|
|
62
|
+
</ul>
|
|
63
|
+
</section>
|
|
64
|
+
{/if}
|
|
65
|
+
|
|
66
|
+
{#if manifest.permissions && manifest.permissions.length > 0}
|
|
67
|
+
<section class="row">
|
|
68
|
+
<span class="label">Permissions</span>
|
|
69
|
+
<ul class="chips">
|
|
70
|
+
{#each manifest.permissions as perm}
|
|
71
|
+
<li><code>{perm}</code></li>
|
|
72
|
+
{/each}
|
|
73
|
+
</ul>
|
|
74
|
+
</section>
|
|
75
|
+
{/if}
|
|
76
|
+
|
|
77
|
+
{#if installed}
|
|
78
|
+
<section class="installed">
|
|
79
|
+
<p class="row"><span class="label">Source registry</span><code>{installed.sourceRegistry}</code></p>
|
|
80
|
+
{#if installedDate}
|
|
81
|
+
<p class="row"><span class="label">Installed</span><span>{installedDate}</span></p>
|
|
82
|
+
{/if}
|
|
83
|
+
<p class="row"><span class="label">Contract</span><code>v{installed.contractVersion}</code></p>
|
|
84
|
+
</section>
|
|
85
|
+
{/if}
|
|
86
|
+
|
|
87
|
+
<div class="actions">
|
|
88
|
+
<button type="button" onclick={close}>Close</button>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<style>
|
|
93
|
+
.app-info-modal {
|
|
94
|
+
padding: 16px 20px;
|
|
95
|
+
min-width: 360px;
|
|
96
|
+
max-width: 520px;
|
|
97
|
+
color: var(--shell-fg);
|
|
98
|
+
background: var(--shell-bg);
|
|
99
|
+
font: inherit;
|
|
100
|
+
}
|
|
101
|
+
header { display: flex; align-items: baseline; gap: 10px; margin-bottom: 10px; }
|
|
102
|
+
header h2 { margin: 0; font-size: 16px; }
|
|
103
|
+
header .id {
|
|
104
|
+
font-size: 12px;
|
|
105
|
+
color: var(--shell-fg-muted);
|
|
106
|
+
}
|
|
107
|
+
.version { margin: 4px 0 12px; font-size: 13px; display: flex; align-items: center; gap: 8px; }
|
|
108
|
+
.description { margin: 8px 0 12px; }
|
|
109
|
+
.description p {
|
|
110
|
+
margin: 4px 0 0;
|
|
111
|
+
font-size: 13px;
|
|
112
|
+
line-height: 1.5;
|
|
113
|
+
white-space: pre-wrap;
|
|
114
|
+
}
|
|
115
|
+
.row { margin: 6px 0; font-size: 13px; display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
|
|
116
|
+
.label {
|
|
117
|
+
color: var(--shell-fg-muted);
|
|
118
|
+
font-size: 11px;
|
|
119
|
+
text-transform: uppercase;
|
|
120
|
+
letter-spacing: 0.04em;
|
|
121
|
+
flex: 0 0 auto;
|
|
122
|
+
min-width: 90px;
|
|
123
|
+
}
|
|
124
|
+
.chips { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 4px; }
|
|
125
|
+
.chips code {
|
|
126
|
+
padding: 1px 6px;
|
|
127
|
+
border: 1px solid var(--shell-border);
|
|
128
|
+
border-radius: var(--shell-radius-sm, 3px);
|
|
129
|
+
}
|
|
130
|
+
.badge {
|
|
131
|
+
font-size: 11px;
|
|
132
|
+
padding: 1px 6px;
|
|
133
|
+
border-radius: var(--shell-radius-sm, 3px);
|
|
134
|
+
background: var(--shell-bg-elevated);
|
|
135
|
+
color: var(--shell-fg-muted);
|
|
136
|
+
border: 1px solid var(--shell-border);
|
|
137
|
+
}
|
|
138
|
+
.installed { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--shell-border); }
|
|
139
|
+
code {
|
|
140
|
+
font-family: var(--shell-font-mono, monospace);
|
|
141
|
+
background: var(--shell-bg-elevated);
|
|
142
|
+
padding: 0 4px;
|
|
143
|
+
border-radius: var(--shell-radius-sm, 3px);
|
|
144
|
+
}
|
|
145
|
+
.actions { display: flex; gap: 8px; margin-top: 16px; justify-content: flex-end; }
|
|
146
|
+
.actions button {
|
|
147
|
+
background: var(--shell-bg-elevated);
|
|
148
|
+
color: var(--shell-fg);
|
|
149
|
+
border: 1px solid var(--shell-border);
|
|
150
|
+
border-radius: var(--shell-radius-sm, 3px);
|
|
151
|
+
padding: 6px 14px; font: inherit; cursor: pointer;
|
|
152
|
+
}
|
|
153
|
+
.actions button:hover { border-color: var(--shell-accent); }
|
|
154
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AppManifest } from '../apps/types';
|
|
2
|
+
import type { InstalledPackage, PackageEntry } from '../registry/types';
|
|
3
|
+
interface Props {
|
|
4
|
+
manifest: AppManifest;
|
|
5
|
+
installed: InstalledPackage | null;
|
|
6
|
+
catalogEntry: PackageEntry | null;
|
|
7
|
+
close: () => void;
|
|
8
|
+
}
|
|
9
|
+
declare const AppInfoView: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type AppInfoView = ReturnType<typeof AppInfoView>;
|
|
11
|
+
export default AppInfoView;
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* 'app' (see ShellHome.svelte).
|
|
6
6
|
*
|
|
7
7
|
* Three actions are registered here:
|
|
8
|
-
* - app.info :
|
|
8
|
+
* - app.info : open the AppInfoView modal with manifest + (when
|
|
9
|
+
* installed) catalog/install metadata. Always enabled.
|
|
9
10
|
* - app.checkUpdate : refresh catalog, then prompt update or toast up-to-date.
|
|
10
11
|
* - app.uninstall : open uninstall confirm dialog.
|
|
11
12
|
*
|
|
@@ -25,6 +26,7 @@ import { modalManager } from '../overlays/modal';
|
|
|
25
26
|
import { toastManager } from '../overlays/toast';
|
|
26
27
|
import AppUpdateAvailableModal from '../app/store/AppUpdateAvailableModal.svelte';
|
|
27
28
|
import UninstallAppDialog from '../app/store/UninstallAppDialog.svelte';
|
|
29
|
+
import AppInfoView from './AppInfoView.svelte';
|
|
28
30
|
export function computeAppActionDisabled(g) {
|
|
29
31
|
return !g.admin || g.builtin;
|
|
30
32
|
}
|
|
@@ -50,6 +52,22 @@ function isBuiltin(appId) {
|
|
|
50
52
|
function gateFor(appId) {
|
|
51
53
|
return { admin: isAdmin(), builtin: isBuiltin(appId) };
|
|
52
54
|
}
|
|
55
|
+
function runShowInfo(_ctx) {
|
|
56
|
+
var _a, _b, _c;
|
|
57
|
+
const ref = readSelection();
|
|
58
|
+
if (!ref)
|
|
59
|
+
return;
|
|
60
|
+
const manifest = findApp(ref.appId);
|
|
61
|
+
if (!manifest)
|
|
62
|
+
return;
|
|
63
|
+
const installed = (_a = storeContext.state.ephemeral.installed.find((p) => p.id === ref.appId)) !== null && _a !== void 0 ? _a : null;
|
|
64
|
+
// Catalog entry only exists for apps fetched from a registry — built-in
|
|
65
|
+
// apps will fall through to null and the modal will hide registry-only
|
|
66
|
+
// fields (description, author).
|
|
67
|
+
const catalogEntry = (_c = (_b = storeContext.state.ephemeral.catalog.find((p) => p.entry.id === ref.appId)) === null || _b === void 0 ? void 0 : _b.entry) !== null && _c !== void 0 ? _c : null;
|
|
68
|
+
const props = { manifest, installed, catalogEntry };
|
|
69
|
+
modalManager.open(AppInfoView, props);
|
|
70
|
+
}
|
|
53
71
|
async function runCheckUpdate(_ctx) {
|
|
54
72
|
var _a, _b;
|
|
55
73
|
const ref = readSelection();
|
|
@@ -124,11 +142,11 @@ export function registerAppActions(ctx) {
|
|
|
124
142
|
const infoLabel = () => {
|
|
125
143
|
const ref = readSelection();
|
|
126
144
|
if (!ref)
|
|
127
|
-
return '';
|
|
145
|
+
return 'Info…';
|
|
128
146
|
const m = findApp(ref.appId);
|
|
129
147
|
if (!m)
|
|
130
|
-
return ref.appId
|
|
131
|
-
return
|
|
148
|
+
return `Info — ${ref.appId}`;
|
|
149
|
+
return `Info — ${m.label} v${m.version}`;
|
|
132
150
|
};
|
|
133
151
|
const updateLabel = () => {
|
|
134
152
|
const ref = readSelection();
|
|
@@ -155,7 +173,7 @@ export function registerAppActions(ctx) {
|
|
|
155
173
|
scope: { element: 'app' },
|
|
156
174
|
contextItem: true,
|
|
157
175
|
group: 'info',
|
|
158
|
-
|
|
176
|
+
run: runShowInfo,
|
|
159
177
|
},
|
|
160
178
|
{
|
|
161
179
|
id: 'app.checkUpdate',
|
|
@@ -107,4 +107,35 @@ describe('ctx.contributions', () => {
|
|
|
107
107
|
// Deactivate must not throw when the entry is already gone.
|
|
108
108
|
expect(() => deactivateShard('p')).not.toThrow();
|
|
109
109
|
});
|
|
110
|
+
it('onAnyChange fires for register/unregister at any point and auto-unsubscribes on deactivate', async () => {
|
|
111
|
+
const cb = vi.fn();
|
|
112
|
+
registerShard({
|
|
113
|
+
manifest: { id: 'watcher', label: 'W', version: '0.0.0', views: [] },
|
|
114
|
+
activate(ctx) {
|
|
115
|
+
ctx.contributions.onAnyChange(cb);
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
await activateShard('watcher');
|
|
119
|
+
registerShard({
|
|
120
|
+
manifest: { id: 'p1', label: 'P1', version: '0.0.0', views: [] },
|
|
121
|
+
activate(ctx) {
|
|
122
|
+
ctx.contributions.register('point.a', { id: 'x' });
|
|
123
|
+
ctx.contributions.register('point.b', { id: 'y' });
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
await activateShard('p1');
|
|
127
|
+
expect(cb).toHaveBeenCalledTimes(2);
|
|
128
|
+
expect(cb).toHaveBeenCalledWith('point.a');
|
|
129
|
+
expect(cb).toHaveBeenCalledWith('point.b');
|
|
130
|
+
deactivateShard('watcher');
|
|
131
|
+
cb.mockClear();
|
|
132
|
+
registerShard({
|
|
133
|
+
manifest: { id: 'p2', label: 'P2', version: '0.0.0', views: [] },
|
|
134
|
+
activate(ctx) {
|
|
135
|
+
ctx.contributions.register('point.c', { id: 'z' });
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
await activateShard('p2');
|
|
139
|
+
expect(cb).not.toHaveBeenCalled();
|
|
140
|
+
});
|
|
110
141
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { MemoryDocumentBackend } from '../documents/backends';
|
|
3
|
+
import { __setDocumentBackend, __setTenantId } from '../documents/config';
|
|
4
|
+
import { registerShard, activateShard, __resetShardRegistryForTest, } from './activate.svelte';
|
|
5
|
+
import { __resetViewRegistryForTest } from './registry';
|
|
6
|
+
function programmaticVerb(name, summary, body) {
|
|
7
|
+
return {
|
|
8
|
+
name,
|
|
9
|
+
summary,
|
|
10
|
+
programmatic: true,
|
|
11
|
+
async run(vctx, args) {
|
|
12
|
+
if (body)
|
|
13
|
+
await body(vctx, args);
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function plainVerb(name, summary) {
|
|
18
|
+
return { name, summary, async run() { } };
|
|
19
|
+
}
|
|
20
|
+
describe('ctx.listVerbs / ctx.runVerb (integration)', () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
__resetShardRegistryForTest();
|
|
23
|
+
__resetViewRegistryForTest();
|
|
24
|
+
__setDocumentBackend(new MemoryDocumentBackend());
|
|
25
|
+
__setTenantId('tenant-test');
|
|
26
|
+
});
|
|
27
|
+
it('listVerbs returns every verb across active shards with shardId', async () => {
|
|
28
|
+
registerShard({
|
|
29
|
+
manifest: { id: 'host', label: 'Host', version: '0.0.0', views: [] },
|
|
30
|
+
activate(ctx) {
|
|
31
|
+
ctx.registerVerb(programmaticVerb('a', 'first verb'));
|
|
32
|
+
ctx.registerVerb(plainVerb('b', 'second verb'));
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
let consumerCtx = null;
|
|
36
|
+
registerShard({
|
|
37
|
+
manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
|
|
38
|
+
activate(ctx) {
|
|
39
|
+
consumerCtx = ctx;
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
await activateShard('host');
|
|
43
|
+
await activateShard('consumer');
|
|
44
|
+
const list = consumerCtx.listVerbs();
|
|
45
|
+
const a = list.find((v) => v.name === 'host:a');
|
|
46
|
+
const b = list.find((v) => v.name === 'host:b');
|
|
47
|
+
expect(a).toEqual({ shardId: 'host', name: 'host:a', summary: 'first verb', schema: undefined });
|
|
48
|
+
expect(b).toEqual({ shardId: 'host', name: 'host:b', summary: 'second verb', schema: undefined });
|
|
49
|
+
});
|
|
50
|
+
it('runVerb dispatches a programmatic verb and returns captured scrollback', async () => {
|
|
51
|
+
registerShard({
|
|
52
|
+
manifest: { id: 'host', label: 'Host', version: '0.0.0', views: [] },
|
|
53
|
+
activate(ctx) {
|
|
54
|
+
ctx.registerVerb(programmaticVerb('echo', 'echoes args', async (vctx, args) => {
|
|
55
|
+
vctx.scrollback.push({ kind: 'status', text: `echo: ${args.join(' ')}`, level: 'info', ts: 0 });
|
|
56
|
+
}));
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
let consumerCtx = null;
|
|
60
|
+
registerShard({
|
|
61
|
+
manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
|
|
62
|
+
activate(ctx) {
|
|
63
|
+
consumerCtx = ctx;
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
await activateShard('host');
|
|
67
|
+
await activateShard('consumer');
|
|
68
|
+
const out = await consumerCtx.runVerb('host', 'host:echo', ['hello']);
|
|
69
|
+
expect(out.scrollback).toHaveLength(1);
|
|
70
|
+
expect(out.scrollback[0]).toMatchObject({ kind: 'status', text: 'echo: hello' });
|
|
71
|
+
});
|
|
72
|
+
it('runVerb rejects when the target verb is not programmatic', async () => {
|
|
73
|
+
registerShard({
|
|
74
|
+
manifest: { id: 'host', label: 'Host', version: '0.0.0', views: [] },
|
|
75
|
+
activate(ctx) {
|
|
76
|
+
ctx.registerVerb(plainVerb('plain', 'no opt-in'));
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
let consumerCtx = null;
|
|
80
|
+
registerShard({
|
|
81
|
+
manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
|
|
82
|
+
activate(ctx) {
|
|
83
|
+
consumerCtx = ctx;
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
await activateShard('host');
|
|
87
|
+
await activateShard('consumer');
|
|
88
|
+
await expect(consumerCtx.runVerb('host', 'host:plain', [])).rejects.toThrow('verb "host:plain" is not programmatic');
|
|
89
|
+
});
|
|
90
|
+
it('runVerb populates ctx.structuredArgs when opts.structured is set', async () => {
|
|
91
|
+
let observed = undefined;
|
|
92
|
+
registerShard({
|
|
93
|
+
manifest: { id: 'host', label: 'Host', version: '0.0.0', views: [] },
|
|
94
|
+
activate(ctx) {
|
|
95
|
+
ctx.registerVerb(programmaticVerb('schemaCheck', 'reads structuredArgs', async (vctx) => {
|
|
96
|
+
observed = vctx.structuredArgs;
|
|
97
|
+
}));
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
let consumerCtx = null;
|
|
101
|
+
registerShard({
|
|
102
|
+
manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
|
|
103
|
+
activate(ctx) {
|
|
104
|
+
consumerCtx = ctx;
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
await activateShard('host');
|
|
108
|
+
await activateShard('consumer');
|
|
109
|
+
await consumerCtx.runVerb('host', 'host:schemaCheck', [], { structured: { foo: 'bar' } });
|
|
110
|
+
expect(observed).toEqual({ foo: 'bar' });
|
|
111
|
+
});
|
|
112
|
+
it('runVerb rejects on unknown shardId', async () => {
|
|
113
|
+
let consumerCtx = null;
|
|
114
|
+
registerShard({
|
|
115
|
+
manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
|
|
116
|
+
activate(ctx) {
|
|
117
|
+
consumerCtx = ctx;
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
await activateShard('consumer');
|
|
121
|
+
await expect(consumerCtx.runVerb('missing', 'x', [])).rejects.toThrow('unknown shard: missing');
|
|
122
|
+
});
|
|
123
|
+
it('runVerb rejects on unknown verb', async () => {
|
|
124
|
+
registerShard({
|
|
125
|
+
manifest: { id: 'host', label: 'Host', version: '0.0.0', views: [] },
|
|
126
|
+
activate(ctx) {
|
|
127
|
+
ctx.registerVerb(programmaticVerb('present', 'exists'));
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
let consumerCtx = null;
|
|
131
|
+
registerShard({
|
|
132
|
+
manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
|
|
133
|
+
activate(ctx) {
|
|
134
|
+
consumerCtx = ctx;
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
await activateShard('host');
|
|
138
|
+
await activateShard('consumer');
|
|
139
|
+
await expect(consumerCtx.runVerb('host', 'host:absent', [])).rejects.toThrow('unknown verb: host:absent');
|
|
140
|
+
});
|
|
141
|
+
it('verbs declaring schema.input expose it via listVerbs', async () => {
|
|
142
|
+
registerShard({
|
|
143
|
+
manifest: { id: 'host', label: 'Host', version: '0.0.0', views: [] },
|
|
144
|
+
activate(ctx) {
|
|
145
|
+
ctx.registerVerb({
|
|
146
|
+
name: 'typed',
|
|
147
|
+
summary: 'has schema',
|
|
148
|
+
programmatic: true,
|
|
149
|
+
schema: {
|
|
150
|
+
input: {
|
|
151
|
+
type: 'object',
|
|
152
|
+
properties: { msg: { type: 'string', description: 'message' } },
|
|
153
|
+
required: ['msg'],
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
async run() { },
|
|
157
|
+
});
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
let consumerCtx = null;
|
|
161
|
+
registerShard({
|
|
162
|
+
manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
|
|
163
|
+
activate(ctx) {
|
|
164
|
+
consumerCtx = ctx;
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
await activateShard('host');
|
|
168
|
+
await activateShard('consumer');
|
|
169
|
+
const list = consumerCtx.listVerbs();
|
|
170
|
+
const typed = list.find((v) => v.name === 'host:typed');
|
|
171
|
+
expect(typed === null || typed === void 0 ? void 0 : typed.schema).toEqual({
|
|
172
|
+
input: {
|
|
173
|
+
type: 'object',
|
|
174
|
+
properties: { msg: { type: 'string', description: 'message' } },
|
|
175
|
+
required: ['msg'],
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
* stays in `registeredShards` — it's still known, just not running.
|
|
18
18
|
*/
|
|
19
19
|
import { shell } from '../shellRuntime.svelte';
|
|
20
|
-
import { registerView, unregisterView, registerVerb as fwRegisterVerb, unregisterVerb as fwUnregisterVerb } from './registry';
|
|
20
|
+
import { registerView, unregisterView, registerVerb as fwRegisterVerb, unregisterVerb as fwUnregisterVerb, listVerbsWithShard } from './registry';
|
|
21
|
+
import { runVerbProgrammatic } from '../runtime/runVerb';
|
|
21
22
|
import { createDocumentHandle, getTenantId, getDocumentBackend } from '../documents';
|
|
22
23
|
import { fetchEnvState, putEnvState } from '../env/client';
|
|
23
24
|
import { isAdmin as checkIsAdmin } from '../auth/index';
|
|
@@ -28,7 +29,7 @@ import { createBrowseCapability } from '../documents/browse';
|
|
|
28
29
|
import { createShardKeysApi } from '../keys/client';
|
|
29
30
|
import { PERMISSION_KEYS_MINT } from '../keys/types';
|
|
30
31
|
import { subscribe } from '../keys/revocation-bus.svelte';
|
|
31
|
-
import { register as contributionsRegister, list as contributionsList, listPoints as contributionsListPoints, onChange as contributionsOnChange, } from '../contributions';
|
|
32
|
+
import { register as contributionsRegister, list as contributionsList, listPoints as contributionsListPoints, onChange as contributionsOnChange, onAnyChange as contributionsOnAnyChange, } from '../contributions';
|
|
32
33
|
import { registerAction } from '../actions/registry';
|
|
33
34
|
import { makeSelectionApi, clearSelectionForShard } from '../actions/selection.svelte';
|
|
34
35
|
import { openContextMenu as shellOpenContextMenu, openPalette as shellOpenPalette } from '../actions/listeners';
|
|
@@ -122,6 +123,11 @@ export async function activateShard(id, opts) {
|
|
|
122
123
|
entry.cleanupFns.push(async () => off());
|
|
123
124
|
return off;
|
|
124
125
|
},
|
|
126
|
+
onAnyChange(cb) {
|
|
127
|
+
const off = contributionsOnAnyChange(cb);
|
|
128
|
+
entry.cleanupFns.push(async () => off());
|
|
129
|
+
return off;
|
|
130
|
+
},
|
|
125
131
|
};
|
|
126
132
|
const ctx = {
|
|
127
133
|
state: (schema) => shell.state(id, schema),
|
|
@@ -131,7 +137,7 @@ export async function activateShard(id, opts) {
|
|
|
131
137
|
},
|
|
132
138
|
registerVerb: (verb) => {
|
|
133
139
|
const prefixed = id === 'shell' ? verb.name : `${id}:${verb.name}`;
|
|
134
|
-
fwRegisterVerb(prefixed, Object.assign(Object.assign({}, verb), { name: prefixed }));
|
|
140
|
+
fwRegisterVerb(prefixed, Object.assign(Object.assign({}, verb), { name: prefixed }), id);
|
|
135
141
|
entry.verbNames.add(prefixed);
|
|
136
142
|
},
|
|
137
143
|
documents: (options) => {
|
|
@@ -195,6 +201,17 @@ export async function activateShard(id, opts) {
|
|
|
195
201
|
openContextMenu(opts) { shellOpenContextMenu(opts); },
|
|
196
202
|
openPalette(opts) { shellOpenPalette(opts); },
|
|
197
203
|
},
|
|
204
|
+
listVerbs() {
|
|
205
|
+
return listVerbsWithShard().map(({ verb, shardId }) => ({
|
|
206
|
+
shardId,
|
|
207
|
+
name: verb.name,
|
|
208
|
+
summary: verb.summary,
|
|
209
|
+
schema: verb.schema,
|
|
210
|
+
}));
|
|
211
|
+
},
|
|
212
|
+
runVerb(shardId, name, args, opts) {
|
|
213
|
+
return runVerbProgrammatic(shardId, name, args, opts);
|
|
214
|
+
},
|
|
198
215
|
};
|
|
199
216
|
entry.ctx = ctx;
|
|
200
217
|
// Wire onKeyRevoked hook: subscribe to the revocation bus for this shard.
|
|
@@ -5,9 +5,19 @@ export declare function registerView(viewId: string, factory: ViewFactory): void
|
|
|
5
5
|
export declare function getView(viewId: string): ViewFactory | undefined;
|
|
6
6
|
export declare function unregisterView(viewId: string): void;
|
|
7
7
|
import type { Verb } from '../verbs/types';
|
|
8
|
-
export declare function registerVerb(name: string, verb: Verb): void;
|
|
8
|
+
export declare function registerVerb(name: string, verb: Verb, shardId: string): void;
|
|
9
9
|
export declare function getVerb(name: string): Verb | undefined;
|
|
10
10
|
export declare function unregisterVerb(name: string): void;
|
|
11
11
|
export declare function listVerbs(): Verb[];
|
|
12
|
+
/**
|
|
13
|
+
* Like `listVerbs`, but also exposes the `shardId` each verb belongs to.
|
|
14
|
+
* Used by `ctx.listVerbs()` so callers don't have to parse the
|
|
15
|
+
* shardId-prefixed verb name. Order is undefined — callers that want
|
|
16
|
+
* sorted output sort themselves.
|
|
17
|
+
*/
|
|
18
|
+
export declare function listVerbsWithShard(): Array<{
|
|
19
|
+
verb: Verb;
|
|
20
|
+
shardId: string;
|
|
21
|
+
}>;
|
|
12
22
|
/** Test-only reset: clear the view and verb registries. */
|
|
13
23
|
export declare function __resetViewRegistryForTest(): void;
|
package/dist/shards/registry.js
CHANGED
|
@@ -38,20 +38,32 @@ export function unregisterView(viewId) {
|
|
|
38
38
|
views.delete(viewId);
|
|
39
39
|
}
|
|
40
40
|
const verbs = new Map();
|
|
41
|
-
export function registerVerb(name, verb) {
|
|
41
|
+
export function registerVerb(name, verb, shardId) {
|
|
42
42
|
if (verbs.has(name)) {
|
|
43
43
|
throw new Error(`Verb "${name}" is already registered`);
|
|
44
44
|
}
|
|
45
|
-
verbs.set(name, verb);
|
|
45
|
+
verbs.set(name, { verb, shardId });
|
|
46
46
|
}
|
|
47
47
|
export function getVerb(name) {
|
|
48
|
-
|
|
48
|
+
var _a;
|
|
49
|
+
return (_a = verbs.get(name)) === null || _a === void 0 ? void 0 : _a.verb;
|
|
49
50
|
}
|
|
50
51
|
export function unregisterVerb(name) {
|
|
51
52
|
verbs.delete(name);
|
|
52
53
|
}
|
|
53
54
|
export function listVerbs() {
|
|
54
|
-
return Array.from(verbs.values())
|
|
55
|
+
return Array.from(verbs.values())
|
|
56
|
+
.map((entry) => entry.verb)
|
|
57
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Like `listVerbs`, but also exposes the `shardId` each verb belongs to.
|
|
61
|
+
* Used by `ctx.listVerbs()` so callers don't have to parse the
|
|
62
|
+
* shardId-prefixed verb name. Order is undefined — callers that want
|
|
63
|
+
* sorted output sort themselves.
|
|
64
|
+
*/
|
|
65
|
+
export function listVerbsWithShard() {
|
|
66
|
+
return Array.from(verbs.values()).map((entry) => (Object.assign({}, entry)));
|
|
55
67
|
}
|
|
56
68
|
/** Test-only reset: clear the view and verb registries. */
|
|
57
69
|
export function __resetViewRegistryForTest() {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { registerVerb, getVerb, unregisterVerb, listVerbs, } from './registry';
|
|
2
|
+
import { registerVerb, getVerb, unregisterVerb, listVerbs, listVerbsWithShard, } from './registry';
|
|
3
3
|
function makeStubVerb(name) {
|
|
4
4
|
return { name, summary: `stub ${name}`, run: async () => { } };
|
|
5
5
|
}
|
|
@@ -10,53 +10,61 @@ describe('verb registry', () => {
|
|
|
10
10
|
unregisterVerb(name);
|
|
11
11
|
registered.length = 0;
|
|
12
12
|
});
|
|
13
|
-
function trackVerb(name, verb) {
|
|
14
|
-
registerVerb(name, verb);
|
|
13
|
+
function trackVerb(name, verb, shardId) {
|
|
14
|
+
registerVerb(name, verb, shardId);
|
|
15
15
|
registered.push(name);
|
|
16
16
|
}
|
|
17
17
|
it('registers and retrieves a verb by name', () => {
|
|
18
18
|
const verb = makeStubVerb('foo');
|
|
19
|
-
trackVerb('foo', verb);
|
|
19
|
+
trackVerb('foo', verb, 'shell');
|
|
20
20
|
expect(getVerb('foo')).toBe(verb);
|
|
21
21
|
});
|
|
22
22
|
it('returns undefined for unknown verb', () => {
|
|
23
23
|
expect(getVerb('nope')).toBeUndefined();
|
|
24
24
|
});
|
|
25
25
|
it('throws on duplicate verb name', () => {
|
|
26
|
-
trackVerb('dup', makeStubVerb('dup'));
|
|
27
|
-
expect(() => trackVerb('dup', makeStubVerb('dup'))).toThrowError('Verb "dup" is already registered');
|
|
26
|
+
trackVerb('dup', makeStubVerb('dup'), 'shell');
|
|
27
|
+
expect(() => trackVerb('dup', makeStubVerb('dup'), 'shell')).toThrowError('Verb "dup" is already registered');
|
|
28
28
|
});
|
|
29
29
|
it('unregisters a verb', () => {
|
|
30
|
-
trackVerb('gone', makeStubVerb('gone'));
|
|
30
|
+
trackVerb('gone', makeStubVerb('gone'), 'shell');
|
|
31
31
|
unregisterVerb('gone');
|
|
32
32
|
registered.pop();
|
|
33
33
|
expect(getVerb('gone')).toBeUndefined();
|
|
34
34
|
});
|
|
35
|
-
it('lists verbs sorted by name', () => {
|
|
36
|
-
trackVerb('zeta', makeStubVerb('zeta'));
|
|
37
|
-
trackVerb('alpha', makeStubVerb('alpha'));
|
|
38
|
-
trackVerb('mid', makeStubVerb('mid'));
|
|
35
|
+
it('lists verbs sorted by name (legacy shape)', () => {
|
|
36
|
+
trackVerb('zeta', makeStubVerb('zeta'), 'shell');
|
|
37
|
+
trackVerb('alpha', makeStubVerb('alpha'), 'shell');
|
|
38
|
+
trackVerb('mid', makeStubVerb('mid'), 'shell');
|
|
39
39
|
const names = listVerbs().map((v) => v.name);
|
|
40
40
|
expect(names).toEqual(['alpha', 'mid', 'zeta']);
|
|
41
41
|
});
|
|
42
|
+
it('listVerbsWithShard returns shardId per entry', () => {
|
|
43
|
+
trackVerb('apps', makeStubVerb('apps'), 'shell');
|
|
44
|
+
trackVerb('sh3-store:install', makeStubVerb('sh3-store:install'), 'sh3-store');
|
|
45
|
+
const result = listVerbsWithShard();
|
|
46
|
+
const apps = result.find((e) => e.verb.name === 'apps');
|
|
47
|
+
const install = result.find((e) => e.verb.name === 'sh3-store:install');
|
|
48
|
+
expect(apps === null || apps === void 0 ? void 0 : apps.shardId).toBe('shell');
|
|
49
|
+
expect(install === null || install === void 0 ? void 0 : install.shardId).toBe('sh3-store');
|
|
50
|
+
});
|
|
42
51
|
it('stores prefixed name inside verb object (mirrors activate auto-prefix)', () => {
|
|
43
|
-
// activate.svelte.ts does: { ...verb, name: prefixed }
|
|
44
52
|
const original = makeStubVerb('install');
|
|
45
53
|
const prefixed = Object.assign(Object.assign({}, original), { name: 'registry:install' });
|
|
46
|
-
trackVerb('registry:install', prefixed);
|
|
54
|
+
trackVerb('registry:install', prefixed, 'registry');
|
|
47
55
|
const found = getVerb('registry:install');
|
|
48
56
|
expect(found === null || found === void 0 ? void 0 : found.name).toBe('registry:install');
|
|
49
57
|
expect(found === null || found === void 0 ? void 0 : found.summary).toBe('stub install');
|
|
50
58
|
});
|
|
51
59
|
it('bulk unregister simulates deactivate cleanup', () => {
|
|
52
|
-
// activate.svelte.ts tracks verbNames and unregisters on deactivate
|
|
53
60
|
const names = ['registry:install', 'registry:search', 'registry:info'];
|
|
54
61
|
for (const name of names)
|
|
55
|
-
trackVerb(name, makeStubVerb(name));
|
|
62
|
+
trackVerb(name, makeStubVerb(name), 'registry');
|
|
56
63
|
expect(listVerbs()).toHaveLength(3);
|
|
57
64
|
for (const name of names)
|
|
58
65
|
unregisterVerb(name);
|
|
59
|
-
registered.length = 0;
|
|
66
|
+
registered.length = 0;
|
|
60
67
|
expect(listVerbs()).toHaveLength(0);
|
|
68
|
+
expect(listVerbsWithShard()).toHaveLength(0);
|
|
61
69
|
});
|
|
62
70
|
});
|