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
|
@@ -11,6 +11,67 @@
|
|
|
11
11
|
import { makeShellModeOutput } from './output';
|
|
12
12
|
export function makeDispatch(deps) {
|
|
13
13
|
let activeController = null;
|
|
14
|
+
/**
|
|
15
|
+
* Programmatic cross-mode dispatch. Validates target id, role gate, and
|
|
16
|
+
* self-invoke; routes to sh3 (full local resolution), bash (WS forward
|
|
17
|
+
* with lazy connect), or a custom descriptor (recursive desc.dispatch).
|
|
18
|
+
*
|
|
19
|
+
* sh3 path bypasses mode gating: a verb's globalVerb flag is irrelevant
|
|
20
|
+
* because the caller is a trusted mode shard, not user input from a
|
|
21
|
+
* different mode.
|
|
22
|
+
*/
|
|
23
|
+
let bashConnected = false;
|
|
24
|
+
async function invoke(modeId, line) {
|
|
25
|
+
var _a, _b, _c, _d;
|
|
26
|
+
const current = deps.mode();
|
|
27
|
+
if (modeId === current.id) {
|
|
28
|
+
throw new Error(`invoke: cannot invoke own mode '${modeId}'`);
|
|
29
|
+
}
|
|
30
|
+
if (modeId === 'sh3') {
|
|
31
|
+
const resolution = deps.resolver.resolve(line, { globalOnly: false });
|
|
32
|
+
if (resolution.kind === 'forward') {
|
|
33
|
+
const head = (_a = line.trim().split(/\s+/)[0]) !== null && _a !== void 0 ? _a : '';
|
|
34
|
+
throw new Error(`invoke: unknown sh3 verb '${head}'`);
|
|
35
|
+
}
|
|
36
|
+
await resolution.verb.run({
|
|
37
|
+
shell: deps.shell,
|
|
38
|
+
scrollback: deps.scrollback,
|
|
39
|
+
session: deps.session,
|
|
40
|
+
cwd: deps.cwd(),
|
|
41
|
+
dispatch,
|
|
42
|
+
fs: deps.fs,
|
|
43
|
+
}, resolution.args);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (modeId === 'bash') {
|
|
47
|
+
if (deps.role() !== 'admin') {
|
|
48
|
+
throw new Error("invoke: 'bash' requires admin role");
|
|
49
|
+
}
|
|
50
|
+
if (!bashConnected) {
|
|
51
|
+
bashConnected = true;
|
|
52
|
+
deps.session.connect();
|
|
53
|
+
}
|
|
54
|
+
deps.session.send({ t: 'submit', line });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// Custom mode
|
|
58
|
+
const desc = (_c = (_b = deps.customMode) === null || _b === void 0 ? void 0 : _b.call(deps, modeId)) !== null && _c !== void 0 ? _c : null;
|
|
59
|
+
if (!desc) {
|
|
60
|
+
throw new Error(`invoke: unknown mode '${modeId}'`);
|
|
61
|
+
}
|
|
62
|
+
if (desc.requiresRole && desc.requiresRole !== deps.role()) {
|
|
63
|
+
throw new Error(`invoke: mode '${modeId}' requires ${desc.requiresRole} role`);
|
|
64
|
+
}
|
|
65
|
+
if (desc.runsOn === 'server') {
|
|
66
|
+
throw new Error('invoke: server-side modes are not yet supported');
|
|
67
|
+
}
|
|
68
|
+
const subOutput = makeShellModeOutput({
|
|
69
|
+
scrollback: deps.scrollback,
|
|
70
|
+
busy: deps.busy,
|
|
71
|
+
invoke,
|
|
72
|
+
});
|
|
73
|
+
await desc.dispatch({ line, cwd: deps.cwd(), signal: (_d = activeController === null || activeController === void 0 ? void 0 : activeController.signal) !== null && _d !== void 0 ? _d : new AbortController().signal }, subOutput);
|
|
74
|
+
}
|
|
14
75
|
async function dispatch(line) {
|
|
15
76
|
var _a, _b, _c, _d;
|
|
16
77
|
// Abort any in-flight custom dispatch when a new line is submitted.
|
|
@@ -25,7 +86,9 @@ export function makeDispatch(deps) {
|
|
|
25
86
|
deps.scrollback.push({ kind: 'status', text: 'shell: server shell not available in user mode', level: 'error', ts: Date.now() });
|
|
26
87
|
return;
|
|
27
88
|
}
|
|
28
|
-
const resolution = deps.resolver.resolve(line
|
|
89
|
+
const resolution = deps.resolver.resolve(line, {
|
|
90
|
+
globalOnly: mode.id !== 'sh3',
|
|
91
|
+
});
|
|
29
92
|
if (resolution.kind === 'local') {
|
|
30
93
|
// Log locally-dispatched verbs for shared history (ws only)
|
|
31
94
|
if (mode.transport === 'ws') {
|
|
@@ -78,7 +141,12 @@ export function makeDispatch(deps) {
|
|
|
78
141
|
});
|
|
79
142
|
return;
|
|
80
143
|
}
|
|
81
|
-
const output = makeShellModeOutput(
|
|
144
|
+
const output = makeShellModeOutput({
|
|
145
|
+
scrollback: deps.scrollback,
|
|
146
|
+
busy: deps.busy,
|
|
147
|
+
invoke,
|
|
148
|
+
});
|
|
149
|
+
const clearBusy = deps.busy();
|
|
82
150
|
try {
|
|
83
151
|
await desc.dispatch({ line: resolution.line, cwd: deps.cwd(), signal: controller.signal }, output);
|
|
84
152
|
}
|
|
@@ -95,6 +163,9 @@ export function makeDispatch(deps) {
|
|
|
95
163
|
});
|
|
96
164
|
}
|
|
97
165
|
}
|
|
166
|
+
finally {
|
|
167
|
+
clearBusy();
|
|
168
|
+
}
|
|
98
169
|
return;
|
|
99
170
|
}
|
|
100
171
|
// 'none' transport, unknown verb: print error
|
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
import type { Scrollback } from './scrollback.svelte';
|
|
2
2
|
import type { ShellModeOutput } from './contract';
|
|
3
|
-
export
|
|
3
|
+
export interface ShellModeOutputDeps {
|
|
4
|
+
scrollback: Scrollback;
|
|
5
|
+
/** Acquire a busy indicator; returns a clear handle. */
|
|
6
|
+
busy: (label?: string) => () => void;
|
|
7
|
+
/** Programmatically dispatch a line through another mode's resolution path. */
|
|
8
|
+
invoke: (modeId: string, line: string) => Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
export declare function makeShellModeOutput(deps: ShellModeOutputDeps): ShellModeOutput;
|
|
@@ -24,7 +24,8 @@ function lastRichId(sb) {
|
|
|
24
24
|
const last = sb.entries[sb.entries.length - 1];
|
|
25
25
|
return last.id;
|
|
26
26
|
}
|
|
27
|
-
export function makeShellModeOutput(
|
|
27
|
+
export function makeShellModeOutput(deps) {
|
|
28
|
+
const sb = deps.scrollback;
|
|
28
29
|
return {
|
|
29
30
|
text(stream, chunk) {
|
|
30
31
|
sb.push({ kind: 'text', stream, chunks: [chunk], ts: Date.now() });
|
|
@@ -71,5 +72,20 @@ export function makeShellModeOutput(sb) {
|
|
|
71
72
|
},
|
|
72
73
|
};
|
|
73
74
|
},
|
|
75
|
+
busy(label) {
|
|
76
|
+
const clearController = deps.busy(label);
|
|
77
|
+
let cleared = false;
|
|
78
|
+
return {
|
|
79
|
+
clear() {
|
|
80
|
+
if (cleared)
|
|
81
|
+
return;
|
|
82
|
+
cleared = true;
|
|
83
|
+
clearController();
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
invoke(modeId, line) {
|
|
88
|
+
return deps.invoke(modeId, line);
|
|
89
|
+
},
|
|
74
90
|
};
|
|
75
91
|
}
|
|
@@ -2,10 +2,12 @@ import { describe, it, expect } from 'vitest';
|
|
|
2
2
|
import { Scrollback } from './scrollback.svelte';
|
|
3
3
|
import { makeShellModeOutput } from './output';
|
|
4
4
|
const FakeComponent = (() => { });
|
|
5
|
+
const stubBusy = () => () => { };
|
|
6
|
+
const stubInvoke = async () => { };
|
|
5
7
|
describe('makeShellModeOutput', () => {
|
|
6
8
|
it('text() pushes coalescing text entries', () => {
|
|
7
9
|
const sb = new Scrollback();
|
|
8
|
-
const out = makeShellModeOutput(sb);
|
|
10
|
+
const out = makeShellModeOutput({ scrollback: sb, busy: stubBusy, invoke: stubInvoke });
|
|
9
11
|
out.text('stdout', 'hello ');
|
|
10
12
|
out.text('stdout', 'world');
|
|
11
13
|
const text = sb.entries.filter((e) => e.kind === 'text');
|
|
@@ -14,7 +16,7 @@ describe('makeShellModeOutput', () => {
|
|
|
14
16
|
});
|
|
15
17
|
it('status() pushes a status entry with the right level', () => {
|
|
16
18
|
const sb = new Scrollback();
|
|
17
|
-
const out = makeShellModeOutput(sb);
|
|
19
|
+
const out = makeShellModeOutput({ scrollback: sb, busy: stubBusy, invoke: stubInvoke });
|
|
18
20
|
out.status('warn', 'careful');
|
|
19
21
|
const s = sb.entries.find((e) => e.kind === 'status');
|
|
20
22
|
expect(s).toBeDefined();
|
|
@@ -23,7 +25,7 @@ describe('makeShellModeOutput', () => {
|
|
|
23
25
|
});
|
|
24
26
|
it('rich().update() mutates the live entry props', () => {
|
|
25
27
|
const sb = new Scrollback();
|
|
26
|
-
const out = makeShellModeOutput(sb);
|
|
28
|
+
const out = makeShellModeOutput({ scrollback: sb, busy: stubBusy, invoke: stubInvoke });
|
|
27
29
|
const handle = out.rich(FakeComponent, { tokens: 'a' });
|
|
28
30
|
handle.update({ tokens: 'ab' });
|
|
29
31
|
const entry = sb.entries.find((e) => e.kind === 'rich');
|
|
@@ -31,7 +33,7 @@ describe('makeShellModeOutput', () => {
|
|
|
31
33
|
});
|
|
32
34
|
it('stream() marks the entry mid-stream until complete()', () => {
|
|
33
35
|
const sb = new Scrollback();
|
|
34
|
-
const out = makeShellModeOutput(sb);
|
|
36
|
+
const out = makeShellModeOutput({ scrollback: sb, busy: stubBusy, invoke: stubInvoke });
|
|
35
37
|
const h = out.stream(FakeComponent, { tokens: '' });
|
|
36
38
|
let entry = sb.entries.find((e) => e.kind === 'rich');
|
|
37
39
|
expect(entry.props.__streamState).toBe('streaming');
|
|
@@ -42,7 +44,7 @@ describe('makeShellModeOutput', () => {
|
|
|
42
44
|
});
|
|
43
45
|
it('stream().error() marks the entry errored and pushes a status', () => {
|
|
44
46
|
const sb = new Scrollback();
|
|
45
|
-
const out = makeShellModeOutput(sb);
|
|
47
|
+
const out = makeShellModeOutput({ scrollback: sb, busy: stubBusy, invoke: stubInvoke });
|
|
46
48
|
const h = out.stream(FakeComponent, { tokens: '' });
|
|
47
49
|
h.error(new Error('boom'));
|
|
48
50
|
const entry = sb.entries.find((e) => e.kind === 'rich');
|
|
@@ -52,3 +54,20 @@ describe('makeShellModeOutput', () => {
|
|
|
52
54
|
expect(s.text).toMatch(/boom/);
|
|
53
55
|
});
|
|
54
56
|
});
|
|
57
|
+
describe('makeShellModeOutput — busy', () => {
|
|
58
|
+
it('forwards busy() to the controller and clear() is idempotent', () => {
|
|
59
|
+
const calls = [];
|
|
60
|
+
let cleared = 0;
|
|
61
|
+
const sb = new Scrollback();
|
|
62
|
+
const busy = (label) => {
|
|
63
|
+
calls.push(`acquire:${label !== null && label !== void 0 ? label : ''}`);
|
|
64
|
+
return () => { cleared++; };
|
|
65
|
+
};
|
|
66
|
+
const out = makeShellModeOutput({ scrollback: sb, busy, invoke: stubInvoke });
|
|
67
|
+
const h = out.busy('thinking');
|
|
68
|
+
expect(calls).toEqual(['acquire:thinking']);
|
|
69
|
+
h.clear();
|
|
70
|
+
h.clear();
|
|
71
|
+
expect(cleared).toBe(1);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { VerbRegistry } from './registry';
|
|
3
|
+
import { registerVerb, __resetViewRegistryForTest } from '../shards/registry';
|
|
4
|
+
const sh3Verb = { name: 'apps', summary: '', async run() { } };
|
|
5
|
+
const globalVerb = { name: 'clear', summary: '', globalVerb: true, async run() { } };
|
|
6
|
+
describe('VerbRegistry.resolve — globalOnly option', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
__resetViewRegistryForTest();
|
|
9
|
+
registerVerb('apps', sh3Verb, 'shell');
|
|
10
|
+
registerVerb('clear', globalVerb, 'shell');
|
|
11
|
+
});
|
|
12
|
+
it('without globalOnly resolves any registered verb', () => {
|
|
13
|
+
const r = new VerbRegistry();
|
|
14
|
+
expect(r.resolve('apps').kind).toBe('local');
|
|
15
|
+
expect(r.resolve('clear').kind).toBe('local');
|
|
16
|
+
});
|
|
17
|
+
it('with globalOnly only resolves globalVerb=true entries', () => {
|
|
18
|
+
const r = new VerbRegistry();
|
|
19
|
+
expect(r.resolve('apps', { globalOnly: true }).kind).toBe('forward');
|
|
20
|
+
expect(r.resolve('clear', { globalOnly: true }).kind).toBe('local');
|
|
21
|
+
});
|
|
22
|
+
it('the $ escape always forwards regardless of globalOnly', () => {
|
|
23
|
+
const r = new VerbRegistry();
|
|
24
|
+
expect(r.resolve('$ ls', { globalOnly: true }).kind).toBe('forward');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -3,5 +3,16 @@ export type { Verb, VerbContext, Resolution, ShellApi };
|
|
|
3
3
|
export declare class VerbRegistry {
|
|
4
4
|
list(): Verb[];
|
|
5
5
|
get(name: string): Verb | undefined;
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Resolve a submitted line to a local verb or a forward instruction.
|
|
8
|
+
*
|
|
9
|
+
* @param line The user-submitted line.
|
|
10
|
+
* @param opts.globalOnly When true, only verbs declared with `globalVerb:
|
|
11
|
+
* true` resolve locally; everything else forwards. Used by the dispatch
|
|
12
|
+
* path to gate sh3-domain verbs to sh3 mode while keeping framework
|
|
13
|
+
* controls (clear, mode) reachable from every mode.
|
|
14
|
+
*/
|
|
15
|
+
resolve(line: string, opts?: {
|
|
16
|
+
globalOnly?: boolean;
|
|
17
|
+
}): Resolution;
|
|
7
18
|
}
|
|
@@ -14,7 +14,16 @@ export class VerbRegistry {
|
|
|
14
14
|
get(name) {
|
|
15
15
|
return getVerb(name);
|
|
16
16
|
}
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a submitted line to a local verb or a forward instruction.
|
|
19
|
+
*
|
|
20
|
+
* @param line The user-submitted line.
|
|
21
|
+
* @param opts.globalOnly When true, only verbs declared with `globalVerb:
|
|
22
|
+
* true` resolve locally; everything else forwards. Used by the dispatch
|
|
23
|
+
* path to gate sh3-domain verbs to sh3 mode while keeping framework
|
|
24
|
+
* controls (clear, mode) reachable from every mode.
|
|
25
|
+
*/
|
|
26
|
+
resolve(line, opts = {}) {
|
|
18
27
|
const trimmed = line.trim();
|
|
19
28
|
if (!trimmed)
|
|
20
29
|
return { kind: 'forward', line };
|
|
@@ -32,6 +41,8 @@ export class VerbRegistry {
|
|
|
32
41
|
const verb = getVerb(head);
|
|
33
42
|
if (!verb)
|
|
34
43
|
return { kind: 'forward', line };
|
|
44
|
+
if (opts.globalOnly && !verb.globalVerb)
|
|
45
|
+
return { kind: 'forward', line };
|
|
35
46
|
// Simple space-split for args — verbs can re-tokenize if they need quoting
|
|
36
47
|
const args = rest.length ? rest.split(/\s+/) : [];
|
|
37
48
|
return { kind: 'local', verb, args, line };
|
|
@@ -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
|
|
@@ -15,18 +15,25 @@ function scaffold(modeId) {
|
|
|
15
15
|
const fs = {};
|
|
16
16
|
const shell = {};
|
|
17
17
|
const resolver = {
|
|
18
|
-
resolve: (line) =>
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
resolve: (line, opts = {}) => {
|
|
19
|
+
// The test only exercises 'pwd' (sh3-domain) and unknown lines.
|
|
20
|
+
// Under globalOnly, 'pwd' should forward.
|
|
21
|
+
if (line.startsWith('pwd') && !opts.globalOnly) {
|
|
22
|
+
return { kind: 'local', verb: { name: 'pwd', run: async () => { } }, args: [], line };
|
|
23
|
+
}
|
|
24
|
+
return { kind: 'forward', line };
|
|
25
|
+
},
|
|
21
26
|
};
|
|
22
27
|
const { dispatch } = makeDispatch({
|
|
23
28
|
mode: () => mode,
|
|
29
|
+
role: () => (modeId === 'bash' ? 'admin' : 'user'),
|
|
24
30
|
resolver,
|
|
25
31
|
scrollback,
|
|
26
32
|
session,
|
|
27
33
|
shell,
|
|
28
34
|
fs,
|
|
29
35
|
cwd: () => '/',
|
|
36
|
+
busy: () => () => { },
|
|
30
37
|
});
|
|
31
38
|
return { dispatch, sent, pushed };
|
|
32
39
|
}
|