sh3-core 0.14.3 → 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/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/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/registry-resolve.test.js +2 -2
- 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/verbs/types.d.ts +60 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -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
|
});
|
package/dist/shards/types.d.ts
CHANGED
|
@@ -3,7 +3,8 @@ import type { ZoneSchema, ZoneManager } from '../state/types';
|
|
|
3
3
|
import type { DocumentHandle, DocumentHandleOptions } from '../documents/types';
|
|
4
4
|
import type { BrowseCapability } from '../documents/browse';
|
|
5
5
|
import type { EnvState } from '../env/types';
|
|
6
|
-
import type { Verb } from '../verbs/types';
|
|
6
|
+
import type { Verb, VerbSchema } from '../verbs/types';
|
|
7
|
+
import type { ScrollbackEntry } from '../shell-shard/scrollback.svelte';
|
|
7
8
|
import type { ShardContextKeys } from '../keys/types';
|
|
8
9
|
import type { ContributionsApi } from '../contributions/types';
|
|
9
10
|
import type { ActionsApi } from '../actions/types';
|
|
@@ -251,6 +252,42 @@ export interface ShardContext {
|
|
|
251
252
|
* entries). Actions are auto-unregistered when the shard deactivates.
|
|
252
253
|
*/
|
|
253
254
|
actions: ActionsApi;
|
|
255
|
+
/**
|
|
256
|
+
* Read-only snapshot of every verb registered across every active shard.
|
|
257
|
+
* Returned entries include the contributing `shardId`, the prefixed
|
|
258
|
+
* `name`, the verb's `summary`, and (when present) its `schema`.
|
|
259
|
+
* Order is undefined.
|
|
260
|
+
*
|
|
261
|
+
* No permission gate — verb names + summaries are already visible via
|
|
262
|
+
* the `help` verb. Diagnostic and AI-class shards (sh3-ai, sh3-diagnostic)
|
|
263
|
+
* use this to enumerate the host's action surface.
|
|
264
|
+
*/
|
|
265
|
+
listVerbs(): Array<{
|
|
266
|
+
shardId: string;
|
|
267
|
+
name: string;
|
|
268
|
+
summary: string;
|
|
269
|
+
schema?: VerbSchema;
|
|
270
|
+
}>;
|
|
271
|
+
/**
|
|
272
|
+
* Programmatically dispatch a verb by `(shardId, name)`. Resolves with
|
|
273
|
+
* `{ result, scrollback }` where `scrollback` is the array of entries
|
|
274
|
+
* the verb pushed during invocation. Rejects on:
|
|
275
|
+
* - unknown shardId,
|
|
276
|
+
* - unknown verb,
|
|
277
|
+
* - target verb not opted in via `programmatic: true`,
|
|
278
|
+
* - any error thrown by the verb's `run`.
|
|
279
|
+
*
|
|
280
|
+
* Pass `opts.structured` to populate `ctx.structuredArgs` for verbs
|
|
281
|
+
* that declare `schema.input`. Pass `opts.signal` for cooperative
|
|
282
|
+
* cancellation (verbs must opt in to honor it).
|
|
283
|
+
*/
|
|
284
|
+
runVerb(shardId: string, name: string, args: string[], opts?: {
|
|
285
|
+
signal?: AbortSignal;
|
|
286
|
+
structured?: unknown;
|
|
287
|
+
}): Promise<{
|
|
288
|
+
result: unknown;
|
|
289
|
+
scrollback: ScrollbackEntry[];
|
|
290
|
+
}>;
|
|
254
291
|
}
|
|
255
292
|
/**
|
|
256
293
|
* A shard module. Shards are the fundamental unit of contribution in SH3.
|
|
@@ -6,8 +6,8 @@ const globalVerb = { name: 'clear', summary: '', globalVerb: true, async run() {
|
|
|
6
6
|
describe('VerbRegistry.resolve — globalOnly option', () => {
|
|
7
7
|
beforeEach(() => {
|
|
8
8
|
__resetViewRegistryForTest();
|
|
9
|
-
registerVerb('apps', sh3Verb);
|
|
10
|
-
registerVerb('clear', globalVerb);
|
|
9
|
+
registerVerb('apps', sh3Verb, 'shell');
|
|
10
|
+
registerVerb('clear', globalVerb, 'shell');
|
|
11
11
|
});
|
|
12
12
|
it('without globalOnly resolves any registered verb', () => {
|
|
13
13
|
const r = new VerbRegistry();
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Headless ShellApi factory.
|
|
3
|
+
*
|
|
4
|
+
* Built only from framework primitives (apps registry, layout inspection,
|
|
5
|
+
* auth, float manager). No Svelte component imports — safe to load from
|
|
6
|
+
* any test project, including the node-only project, and from
|
|
7
|
+
* runtime/runVerb.ts which builds a synthesized VerbContext.
|
|
8
|
+
*
|
|
9
|
+
* Mode-related methods (`setMode`, `listModes`) stub to false / [];
|
|
10
|
+
* Terminal.svelte wraps this with mode-aware closures.
|
|
11
|
+
*/
|
|
12
|
+
import { listRegisteredApps, getActiveApp } from '../apps/registry.svelte';
|
|
13
|
+
import { launchApp } from '../apps/lifecycle';
|
|
14
|
+
import { registeredShards, listStandaloneViews } from '../shards/activate.svelte';
|
|
15
|
+
import { inspectActiveLayout, focusView, closeTab, popoutView, dockFloat, dockIntoActiveLayout, locateSlot as locateSlotInActiveLayout, } from '../layout/inspection';
|
|
16
|
+
import { floatManager } from '../overlays/float';
|
|
17
|
+
import { getUser, isAdmin } from '../auth/index';
|
|
18
|
+
function collectTabEntries(node) {
|
|
19
|
+
if (node.type === 'tabs') {
|
|
20
|
+
return node.tabs.filter((t) => t.viewId !== null);
|
|
21
|
+
}
|
|
22
|
+
if (node.type === 'split') {
|
|
23
|
+
return node.children.flatMap(collectTabEntries);
|
|
24
|
+
}
|
|
25
|
+
if (node.viewId !== null) {
|
|
26
|
+
return [{ slotId: node.slotId, viewId: node.viewId, label: node.viewId }];
|
|
27
|
+
}
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
export function makeShellApiHeadless() {
|
|
31
|
+
return {
|
|
32
|
+
listApps() {
|
|
33
|
+
return listRegisteredApps().map((m) => ({ id: m.id, label: m.label }));
|
|
34
|
+
},
|
|
35
|
+
getActiveApp() {
|
|
36
|
+
const m = getActiveApp();
|
|
37
|
+
return m ? { id: m.id, label: m.label } : null;
|
|
38
|
+
},
|
|
39
|
+
launchApp(id) {
|
|
40
|
+
void launchApp(id);
|
|
41
|
+
},
|
|
42
|
+
listShards() {
|
|
43
|
+
return Array.from(registeredShards.values()).map((s) => ({
|
|
44
|
+
id: s.manifest.id,
|
|
45
|
+
label: s.manifest.label,
|
|
46
|
+
version: s.manifest.version,
|
|
47
|
+
}));
|
|
48
|
+
},
|
|
49
|
+
listViewsInCurrentLayout() {
|
|
50
|
+
try {
|
|
51
|
+
const { root } = inspectActiveLayout();
|
|
52
|
+
return collectTabEntries(root.docked).map((t) => {
|
|
53
|
+
var _a;
|
|
54
|
+
return ({
|
|
55
|
+
slotId: t.slotId,
|
|
56
|
+
viewId: (_a = t.viewId) !== null && _a !== void 0 ? _a : '',
|
|
57
|
+
label: t.label,
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch (_a) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
openViewInCurrentLayout(viewId) {
|
|
66
|
+
try {
|
|
67
|
+
if (focusView(viewId))
|
|
68
|
+
return { ok: true };
|
|
69
|
+
const standalone = listStandaloneViews().find((v) => v.viewId === viewId);
|
|
70
|
+
if (standalone) {
|
|
71
|
+
const slotId = `standalone:${viewId}:${Date.now()}`;
|
|
72
|
+
const ok = dockIntoActiveLayout({ slotId, viewId, label: standalone.label });
|
|
73
|
+
return ok ? { ok: true } : { ok: false, error: `could not dock "${viewId}" — no available slot` };
|
|
74
|
+
}
|
|
75
|
+
return { ok: false, error: `view "${viewId}" not found in current layout` };
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
listStandaloneViews() {
|
|
82
|
+
return listStandaloneViews();
|
|
83
|
+
},
|
|
84
|
+
popoutSlot(slotId) {
|
|
85
|
+
try {
|
|
86
|
+
const floatId = popoutView(slotId);
|
|
87
|
+
return floatId ? { ok: true, floatId } : { ok: false, error: `slot "${slotId}" not found in docked tree` };
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
dockFloat(floatId) {
|
|
94
|
+
try {
|
|
95
|
+
const ok = dockFloat(floatId);
|
|
96
|
+
return ok ? { ok: true } : { ok: false, error: `float "${floatId}" not found or has no dockable content` };
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
locateSlot(slotId) {
|
|
103
|
+
try {
|
|
104
|
+
return locateSlotInActiveLayout(slotId);
|
|
105
|
+
}
|
|
106
|
+
catch (_a) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
listFloats() {
|
|
111
|
+
return floatManager.list().map((f) => {
|
|
112
|
+
var _a, _b, _c, _d, _e;
|
|
113
|
+
const tabs = f.content.type === 'tabs' ? f.content : null;
|
|
114
|
+
const active = tabs ? (_b = tabs.tabs[(_a = tabs.activeTab) !== null && _a !== void 0 ? _a : 0]) !== null && _b !== void 0 ? _b : tabs.tabs[0] : null;
|
|
115
|
+
return {
|
|
116
|
+
floatId: f.id,
|
|
117
|
+
viewId: (_c = active === null || active === void 0 ? void 0 : active.viewId) !== null && _c !== void 0 ? _c : null,
|
|
118
|
+
label: (_e = (_d = f.title) !== null && _d !== void 0 ? _d : active === null || active === void 0 ? void 0 : active.label) !== null && _e !== void 0 ? _e : f.id,
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
closeSlot(slotId) {
|
|
123
|
+
void closeTab(slotId);
|
|
124
|
+
return { ok: true };
|
|
125
|
+
},
|
|
126
|
+
listZones(_shardId) { return []; },
|
|
127
|
+
readZone(_shardId, _zoneName) { return null; },
|
|
128
|
+
whoAmI() {
|
|
129
|
+
var _a;
|
|
130
|
+
const user = getUser();
|
|
131
|
+
return {
|
|
132
|
+
userId: (_a = user === null || user === void 0 ? void 0 : user.id) !== null && _a !== void 0 ? _a : 'guest',
|
|
133
|
+
admin: isAdmin(),
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
setMode(_id) { return false; },
|
|
137
|
+
listModes() { return []; },
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
export function makeShellApiForTest() {
|
|
141
|
+
return makeShellApiHeadless();
|
|
142
|
+
}
|
|
@@ -1,9 +1,3 @@
|
|
|
1
1
|
import type { Shard } from '../api';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Test-only ShellApi constructor. Bypasses the admin gate and uses a
|
|
5
|
-
* stub ShardContext. Only methods that do not consult `ctx` are
|
|
6
|
-
* guaranteed to work — `locateSlot`, `listFloats`, `listApps`, etc.
|
|
7
|
-
*/
|
|
8
|
-
export declare function makeShellApiForTest(): ShellApi;
|
|
2
|
+
export { makeShellApiHeadless, makeShellApiForTest } from './shellApi';
|
|
9
3
|
export declare const shellShard: Shard;
|
|
@@ -11,180 +11,25 @@
|
|
|
11
11
|
*
|
|
12
12
|
* autostart() is defined so the shard activates at boot without requiring
|
|
13
13
|
* a dedicated app to launch it first, matching the __sh3core__ pattern.
|
|
14
|
+
*
|
|
15
|
+
* The headless ShellApi factory lives in ./shellApi.ts so non-DOM callers
|
|
16
|
+
* (node-only test project, runtime/runVerb) can build a ShellApi without
|
|
17
|
+
* pulling in Terminal.svelte.
|
|
14
18
|
*/
|
|
15
19
|
import { mount, unmount } from 'svelte';
|
|
16
20
|
import { manifest } from './manifest';
|
|
17
21
|
import Terminal from './Terminal.svelte';
|
|
18
22
|
import { registerV1Verbs } from './verbs';
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import { registeredShards } from '../shards/activate.svelte';
|
|
22
|
-
import { inspectActiveLayout, focusView, closeTab, popoutView, dockFloat, dockIntoActiveLayout, locateSlot as locateSlotInActiveLayout } from '../layout/inspection';
|
|
23
|
+
import { makeShellApiHeadless } from './shellApi';
|
|
24
|
+
import { focusView } from '../layout/inspection';
|
|
23
25
|
import { floatManager } from '../overlays/float';
|
|
24
|
-
import { listStandaloneViews } from '../shards/activate.svelte';
|
|
25
26
|
import { getUser, isAdmin } from '../auth/index';
|
|
26
|
-
|
|
27
|
-
function collectTabEntries(node) {
|
|
28
|
-
if (node.type === 'tabs') {
|
|
29
|
-
return node.tabs.filter((t) => t.viewId !== null);
|
|
30
|
-
}
|
|
31
|
-
if (node.type === 'split') {
|
|
32
|
-
return node.children.flatMap(collectTabEntries);
|
|
33
|
-
}
|
|
34
|
-
// slot node: wrap as a synthetic tab entry if it has a viewId
|
|
35
|
-
if (node.viewId !== null) {
|
|
36
|
-
return [{ slotId: node.slotId, viewId: node.viewId, label: node.viewId }];
|
|
37
|
-
}
|
|
38
|
-
return [];
|
|
39
|
-
}
|
|
40
|
-
function makeShellApi(_ctx) {
|
|
41
|
-
return {
|
|
42
|
-
// → apps/registry.svelte: listRegisteredApps() returns AppManifest[]
|
|
43
|
-
listApps() {
|
|
44
|
-
return listRegisteredApps().map((m) => ({ id: m.id, label: m.label }));
|
|
45
|
-
},
|
|
46
|
-
// → apps/registry.svelte: getActiveApp() returns AppManifest | null
|
|
47
|
-
getActiveApp() {
|
|
48
|
-
const m = getActiveApp();
|
|
49
|
-
return m ? { id: m.id, label: m.label } : null;
|
|
50
|
-
},
|
|
51
|
-
// → apps/lifecycle: launchApp() is async; fire-and-forget to keep ShellApi sync.
|
|
52
|
-
// Verb handlers display feedback independently via scrollback.
|
|
53
|
-
launchApp(id) {
|
|
54
|
-
void launchApp(id);
|
|
55
|
-
},
|
|
56
|
-
// → shards/activate.svelte: registeredShards reactive map
|
|
57
|
-
listShards() {
|
|
58
|
-
return Array.from(registeredShards.values()).map((s) => ({
|
|
59
|
-
id: s.manifest.id,
|
|
60
|
-
label: s.manifest.label,
|
|
61
|
-
version: s.manifest.version,
|
|
62
|
-
}));
|
|
63
|
-
},
|
|
64
|
-
// → layout/inspection: inspectActiveLayout() + tree walk
|
|
65
|
-
listViewsInCurrentLayout() {
|
|
66
|
-
try {
|
|
67
|
-
const { root } = inspectActiveLayout();
|
|
68
|
-
return collectTabEntries(root.docked).map((t) => {
|
|
69
|
-
var _a;
|
|
70
|
-
return ({
|
|
71
|
-
slotId: t.slotId,
|
|
72
|
-
viewId: (_a = t.viewId) !== null && _a !== void 0 ? _a : '',
|
|
73
|
-
label: t.label,
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
catch (_a) {
|
|
78
|
-
return [];
|
|
79
|
-
}
|
|
80
|
-
},
|
|
81
|
-
// → layout/inspection: focusView(viewId). Falls back to dockIntoActiveLayout
|
|
82
|
-
// for standalone views that aren't mounted yet — this is the single
|
|
83
|
-
// "summon" entry point wired behind the `open` verb.
|
|
84
|
-
openViewInCurrentLayout(viewId) {
|
|
85
|
-
try {
|
|
86
|
-
if (focusView(viewId))
|
|
87
|
-
return { ok: true };
|
|
88
|
-
const standalone = listStandaloneViews().find((v) => v.viewId === viewId);
|
|
89
|
-
if (standalone) {
|
|
90
|
-
const slotId = `standalone:${viewId}:${Date.now()}`;
|
|
91
|
-
const ok = dockIntoActiveLayout({ slotId, viewId, label: standalone.label });
|
|
92
|
-
return ok ? { ok: true } : { ok: false, error: `could not dock "${viewId}" — no available slot` };
|
|
93
|
-
}
|
|
94
|
-
return { ok: false, error: `view "${viewId}" not found in current layout` };
|
|
95
|
-
}
|
|
96
|
-
catch (err) {
|
|
97
|
-
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
98
|
-
}
|
|
99
|
-
},
|
|
100
|
-
// → shards/activate.svelte: listStandaloneViews() walks activeShards
|
|
101
|
-
listStandaloneViews() {
|
|
102
|
-
return listStandaloneViews();
|
|
103
|
-
},
|
|
104
|
-
// → layout/inspection: popoutView(slotId) returns floatId | null
|
|
105
|
-
popoutSlot(slotId) {
|
|
106
|
-
try {
|
|
107
|
-
const floatId = popoutView(slotId);
|
|
108
|
-
return floatId ? { ok: true, floatId } : { ok: false, error: `slot "${slotId}" not found in docked tree` };
|
|
109
|
-
}
|
|
110
|
-
catch (err) {
|
|
111
|
-
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
112
|
-
}
|
|
113
|
-
},
|
|
114
|
-
// → layout/inspection: dockFloat(floatId) returns boolean
|
|
115
|
-
dockFloat(floatId) {
|
|
116
|
-
try {
|
|
117
|
-
const ok = dockFloat(floatId);
|
|
118
|
-
return ok ? { ok: true } : { ok: false, error: `float "${floatId}" not found or has no dockable content` };
|
|
119
|
-
}
|
|
120
|
-
catch (err) {
|
|
121
|
-
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
122
|
-
}
|
|
123
|
-
},
|
|
124
|
-
// → layout/inspection: locateSlot(slotId) returns TreeRootRef | null
|
|
125
|
-
locateSlot(slotId) {
|
|
126
|
-
try {
|
|
127
|
-
return locateSlotInActiveLayout(slotId);
|
|
128
|
-
}
|
|
129
|
-
catch (_a) {
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
132
|
-
},
|
|
133
|
-
// → overlays/float: floatManager.list() returns FloatEntry[]
|
|
134
|
-
listFloats() {
|
|
135
|
-
return floatManager.list().map((f) => {
|
|
136
|
-
var _a, _b, _c, _d, _e;
|
|
137
|
-
const tabs = f.content.type === 'tabs' ? f.content : null;
|
|
138
|
-
const active = tabs ? (_b = tabs.tabs[(_a = tabs.activeTab) !== null && _a !== void 0 ? _a : 0]) !== null && _b !== void 0 ? _b : tabs.tabs[0] : null;
|
|
139
|
-
return {
|
|
140
|
-
floatId: f.id,
|
|
141
|
-
viewId: (_c = active === null || active === void 0 ? void 0 : active.viewId) !== null && _c !== void 0 ? _c : null,
|
|
142
|
-
label: (_e = (_d = f.title) !== null && _d !== void 0 ? _d : active === null || active === void 0 ? void 0 : active.label) !== null && _e !== void 0 ? _e : f.id,
|
|
143
|
-
};
|
|
144
|
-
});
|
|
145
|
-
},
|
|
146
|
-
// → layout/inspection: closeTab(slotId) is async (guarded close).
|
|
147
|
-
// Fire-and-forget; the tab disappears asynchronously. ShellApi stays sync.
|
|
148
|
-
closeSlot(slotId) {
|
|
149
|
-
void closeTab(slotId);
|
|
150
|
-
return { ok: true };
|
|
151
|
-
},
|
|
152
|
-
// TODO Phase 10: wire to zone manager when state:manage permission is available.
|
|
153
|
-
// The shell manifest declares permissions: [] so ctx.zones is undefined.
|
|
154
|
-
// A future permission grant + ctx.zones.list() would power these.
|
|
155
|
-
listZones(_shardId) { return []; },
|
|
156
|
-
readZone(_shardId, _zoneName) { return null; },
|
|
157
|
-
// → auth/index: getUser() + isAdmin()
|
|
158
|
-
whoAmI() {
|
|
159
|
-
var _a;
|
|
160
|
-
const user = getUser();
|
|
161
|
-
return {
|
|
162
|
-
userId: (_a = user === null || user === void 0 ? void 0 : user.id) !== null && _a !== void 0 ? _a : 'guest',
|
|
163
|
-
admin: isAdmin(),
|
|
164
|
-
};
|
|
165
|
-
},
|
|
166
|
-
// Mode switching is per-view state owned by Terminal.svelte; the base
|
|
167
|
-
// ShellApi cannot reach it from the shard scope. Terminal.svelte wraps
|
|
168
|
-
// this object and overrides setMode/listModes with the live registry +
|
|
169
|
-
// setMode closure. Verbs called outside a terminal context fall through
|
|
170
|
-
// these stubs (no-op switch, empty list).
|
|
171
|
-
setMode(_id) { return false; },
|
|
172
|
-
listModes() { return []; },
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
/**
|
|
176
|
-
* Test-only ShellApi constructor. Bypasses the admin gate and uses a
|
|
177
|
-
* stub ShardContext. Only methods that do not consult `ctx` are
|
|
178
|
-
* guaranteed to work — `locateSlot`, `listFloats`, `listApps`, etc.
|
|
179
|
-
*/
|
|
180
|
-
export function makeShellApiForTest() {
|
|
181
|
-
return makeShellApi({});
|
|
182
|
-
}
|
|
27
|
+
export { makeShellApiHeadless, makeShellApiForTest } from './shellApi';
|
|
183
28
|
export const shellShard = {
|
|
184
29
|
manifest,
|
|
185
30
|
activate(ctx) {
|
|
186
31
|
registerV1Verbs(ctx);
|
|
187
|
-
const shell =
|
|
32
|
+
const shell = makeShellApiHeadless();
|
|
188
33
|
// The AZERTY `²` key (top-left on FR keyboards, below Escape) opens the
|
|
189
34
|
// terminal view — focusing it if already mounted, floating it otherwise.
|
|
190
35
|
// Migrated from Shell.svelte's inline keydown handler as proof-of-concept
|