sh3-core 0.22.2 → 0.23.2
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 +1 -1
- package/dist/api.js +1 -1
- package/dist/app/admin/adminApp.js +2 -0
- package/dist/app/admin/adminShard.svelte.js +1 -0
- package/dist/app/store/storeApp.js +3 -1
- package/dist/app/store/storeShard.svelte.js +1 -0
- package/dist/app-appearance/appearanceShard.svelte.js +1 -0
- package/dist/apps/lifecycle.js +22 -10
- package/dist/apps/lifecycle.test.js +53 -1
- package/dist/apps/types.d.ts +13 -0
- package/dist/chrome/CompactChrome.svelte +11 -7
- package/dist/createShell.js +40 -0
- package/dist/documents/picker-api.test.js +40 -0
- package/dist/documents/picker-primitive.d.ts +39 -1
- package/dist/documents/picker-primitive.js +5 -4
- package/dist/host.js +30 -7
- package/dist/layout/slotHostPool.svelte.d.ts +11 -0
- package/dist/layout/slotHostPool.svelte.js +41 -17
- package/dist/layout/slotHostPool.test.js +45 -1
- package/dist/layouts-shard/layoutsShard.svelte.js +1 -0
- package/dist/overlays/OverlayRoots.svelte +15 -4
- package/dist/overlays/__test__/OverlayBindHarness.svelte +20 -0
- package/dist/overlays/__test__/OverlayBindHarness.svelte.d.ts +3 -0
- package/dist/overlays/float-compact-bind.svelte.test.d.ts +1 -0
- package/dist/overlays/float-compact-bind.svelte.test.js +51 -0
- package/dist/overlays/modal.js +3 -0
- package/dist/overlays/modal.test.js +45 -0
- package/dist/overlays/types.d.ts +9 -0
- package/dist/primitives/widgets/Field.svelte +5 -0
- package/dist/primitives/widgets/Field.svelte.d.ts +1 -0
- package/dist/primitives/widgets/Field.svelte.test.js +16 -0
- package/dist/primitives/widgets/NumberInput.svelte +21 -12
- package/dist/primitives/widgets/NumberInput.svelte.d.ts +2 -0
- package/dist/primitives/widgets/NumberInput.svelte.test.js +26 -0
- package/dist/primitives/widgets/ShardPicker.svelte +38 -0
- package/dist/primitives/widgets/ShardPicker.svelte.d.ts +9 -0
- package/dist/primitives/widgets/Textarea.svelte +5 -0
- package/dist/primitives/widgets/Textarea.svelte.d.ts +1 -0
- package/dist/primitives/widgets/Textarea.svelte.test.js +16 -0
- package/dist/primitives/widgets/_DocumentBrowser.svelte +11 -3
- package/dist/primitives/widgets/_DocumentBrowser.svelte.d.ts +2 -0
- package/dist/primitives/widgets/_selectOnFocus.d.ts +15 -0
- package/dist/primitives/widgets/_selectOnFocus.js +24 -0
- package/dist/projects/scope-gate.d.ts +4 -0
- package/dist/projects/scope-gate.js +51 -0
- package/dist/projects/scope-gate.test.d.ts +1 -0
- package/dist/projects/scope-gate.test.js +92 -0
- package/dist/projects-shard/ProjectManage.svelte +42 -2
- package/dist/projects-shard/ProjectManage.svelte.test.js +10 -9
- package/dist/projects-shard/projectsApi.d.ts +3 -2
- package/dist/projects-shard/projectsApi.test.js +1 -1
- package/dist/projects-shard/projectsShard.svelte.js +1 -0
- package/dist/runtime/runVerb.d.ts +9 -0
- package/dist/runtime/runVerb.js +4 -4
- package/dist/runtime/runVerb.test.js +29 -0
- package/dist/sh3Api/headless.d.ts +7 -0
- package/dist/sh3Api/headless.js +3 -1
- package/dist/sh3Api/headless.svelte.test.js +42 -0
- package/dist/sh3core-shard/Sh3Home.svelte +5 -4
- package/dist/sh3core-shard/sh3coreShard.svelte.js +1 -0
- package/dist/shards/lifecycle.svelte.d.ts +8 -2
- package/dist/shards/lifecycle.svelte.js +65 -7
- package/dist/shards/lifecycle.test.js +110 -1
- package/dist/shards/types.d.ts +13 -0
- package/dist/shell-shard/Terminal.svelte +1 -4
- package/dist/shell-shard/Terminal.svelte.d.ts +0 -2
- package/dist/shell-shard/dispatch.d.ts +0 -2
- package/dist/shell-shard/dispatch.js +0 -2
- package/dist/shell-shard/display-cwd.test.js +4 -4
- package/dist/shell-shard/manifest.js +1 -0
- package/dist/shell-shard/shellShard.svelte.d.ts +1 -1
- package/dist/shell-shard/shellShard.svelte.js +9 -4
- package/dist/shell-shard/verbs/cat.js +3 -3
- package/dist/shell-shard/verbs/cat.test.js +1 -2
- package/dist/shell-shard/verbs/ls.js +2 -2
- package/dist/shell-shard/verbs/ls.test.js +1 -2
- package/dist/shell-shard/verbs/mkdir.js +3 -3
- package/dist/shell-shard/verbs/mkdir.test.js +1 -2
- package/dist/shell-shard/verbs/mv.js +3 -3
- package/dist/shell-shard/verbs/mv.test.js +1 -2
- package/dist/shell-shard/verbs/rm.js +3 -3
- package/dist/shell-shard/verbs/rm.test.js +1 -2
- package/dist/shell-shard/verbs/xfer.js +5 -5
- package/dist/shell-shard/verbs/xfer.test.js +2 -2
- package/dist/verbs/types.d.ts +10 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -17,6 +17,7 @@ import { PERMISSION_STATE_MANAGE } from '../state/types';
|
|
|
17
17
|
import { PERMISSION_DOCUMENTS_BROWSE, PERMISSION_DOCUMENTS_READ, PERMISSION_DOCUMENTS_WRITE, } from '../documents/types';
|
|
18
18
|
import { createBrowseCapability } from '../documents/browse';
|
|
19
19
|
import { createDocumentPicker } from '../documents/picker-primitive';
|
|
20
|
+
import { documentChanges } from '../documents/notifications';
|
|
20
21
|
import { createShardKeysApi } from '../keys/client';
|
|
21
22
|
import { PERMISSION_KEYS_MINT } from '../keys/types';
|
|
22
23
|
import { makeSh3Api } from '../sh3Api/headless';
|
|
@@ -29,11 +30,65 @@ import { clearSelectionForShard } from '../actions/selection.svelte';
|
|
|
29
30
|
import { fetchEnvState } from '../env/client';
|
|
30
31
|
import { subscribe as subscribeKeyRevocation } from '../keys/revocation-bus.svelte';
|
|
31
32
|
const shardAppBindings = $state(new Map());
|
|
33
|
+
/**
|
|
34
|
+
* Build the picker-primitive options for a shard: backend-backed
|
|
35
|
+
* `listFolders` so empty folders surface in the modal, a mutation `handle`
|
|
36
|
+
* that emits the right documentChanges events, and a `readOnlyShard`
|
|
37
|
+
* predicate that blocks cross-shard mutations unless the caller holds
|
|
38
|
+
* `documents:write`.
|
|
39
|
+
*/
|
|
40
|
+
function makePickerOptions(callerShardId, permissions, lockToShard) {
|
|
41
|
+
const backend = getDocumentBackend();
|
|
42
|
+
const hasWrite = permissions.includes(PERMISSION_DOCUMENTS_WRITE);
|
|
43
|
+
return {
|
|
44
|
+
listFolders: (sid, prefix) => backend.listFolders(getActiveScopeId(), sid, prefix),
|
|
45
|
+
handle: {
|
|
46
|
+
mkdir: async (sid, path) => {
|
|
47
|
+
const tid = getActiveScopeId();
|
|
48
|
+
await backend.mkdir(tid, sid, path);
|
|
49
|
+
documentChanges.emit({ type: 'folder-create', path, tenantId: tid, shardId: sid });
|
|
50
|
+
},
|
|
51
|
+
rmdir: async (sid, path, opts) => {
|
|
52
|
+
const tid = getActiveScopeId();
|
|
53
|
+
await backend.rmdir(tid, sid, path, opts);
|
|
54
|
+
documentChanges.emit({ type: 'folder-delete', path, tenantId: tid, shardId: sid });
|
|
55
|
+
},
|
|
56
|
+
renameFolder: async (sid, oldPath, newPath) => {
|
|
57
|
+
const tid = getActiveScopeId();
|
|
58
|
+
await backend.renameFolder(tid, sid, oldPath, newPath);
|
|
59
|
+
documentChanges.emit({ type: 'folder-rename', path: newPath, oldPath, tenantId: tid, shardId: sid });
|
|
60
|
+
},
|
|
61
|
+
rename: async (sid, oldPath, newPath) => {
|
|
62
|
+
const tid = getActiveScopeId();
|
|
63
|
+
await backend.rename(tid, sid, oldPath, newPath);
|
|
64
|
+
documentChanges.emit({ type: 'rename', path: newPath, oldPath, tenantId: tid, shardId: sid });
|
|
65
|
+
},
|
|
66
|
+
delete: async (sid, path) => {
|
|
67
|
+
const tid = getActiveScopeId();
|
|
68
|
+
const existed = await backend.exists(tid, sid, path);
|
|
69
|
+
await backend.delete(tid, sid, path);
|
|
70
|
+
if (existed)
|
|
71
|
+
documentChanges.emit({ type: 'delete', path, tenantId: tid, shardId: sid });
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
readOnlyShard: (sid) => sid !== callerShardId && !hasWrite,
|
|
75
|
+
initialShardId: callerShardId,
|
|
76
|
+
lockToShard,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
32
79
|
/**
|
|
33
80
|
* Reactive registry of every shard known to the host. Keys are shard ids.
|
|
34
81
|
* Populated by `registerShard`.
|
|
35
82
|
*/
|
|
36
83
|
export const registeredShards = $state(new Map());
|
|
84
|
+
/**
|
|
85
|
+
* Reactive snapshot of every registered shard's manifest. Mirrors
|
|
86
|
+
* `listRegisteredApps()` — used by project manage UI / ShardPicker to
|
|
87
|
+
* enumerate service-kind shards.
|
|
88
|
+
*/
|
|
89
|
+
export function listRegisteredShards() {
|
|
90
|
+
return Array.from(registeredShards.values()).map((s) => s.manifest);
|
|
91
|
+
}
|
|
37
92
|
export const erroredShards = $state(new Map());
|
|
38
93
|
/** Read the app id currently bound to this shard, or null. */
|
|
39
94
|
export function getShardBinding(shardId) {
|
|
@@ -65,7 +120,7 @@ export function __setScopeResolver(resolver) {
|
|
|
65
120
|
* `entry.activeAppId`); otherwise they go to the boot bag.
|
|
66
121
|
*/
|
|
67
122
|
export function buildShardContext(shard, entry) {
|
|
68
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
123
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
69
124
|
const id = shard.manifest.id;
|
|
70
125
|
function trackDisposer(fn) {
|
|
71
126
|
var _a;
|
|
@@ -200,15 +255,15 @@ export function buildShardContext(shard, entry) {
|
|
|
200
255
|
: undefined,
|
|
201
256
|
browse: browseCap,
|
|
202
257
|
documentPicker: browseCap
|
|
203
|
-
? createDocumentPicker(() => browseCap.listDocuments())
|
|
258
|
+
? createDocumentPicker(() => browseCap.listDocuments(), makePickerOptions(id, (_g = shard.manifest.permissions) !== null && _g !== void 0 ? _g : [], false))
|
|
204
259
|
: createDocumentPicker(async () => {
|
|
205
260
|
const docs = await getDocumentBackend().list(getActiveScopeId(), id);
|
|
206
261
|
return docs.map(d => (Object.assign(Object.assign({}, d), { shardId: id })));
|
|
207
|
-
}),
|
|
208
|
-
keys: ((
|
|
262
|
+
}, makePickerOptions(id, (_h = shard.manifest.permissions) !== null && _h !== void 0 ? _h : [], true)),
|
|
263
|
+
keys: ((_j = shard.manifest.permissions) === null || _j === void 0 ? void 0 : _j.includes(PERMISSION_KEYS_MINT))
|
|
209
264
|
? createShardKeysApi({
|
|
210
265
|
shardId: id,
|
|
211
|
-
shardPermissions: (
|
|
266
|
+
shardPermissions: (_k = shard.manifest.permissions) !== null && _k !== void 0 ? _k : [],
|
|
212
267
|
})
|
|
213
268
|
: undefined,
|
|
214
269
|
contributions,
|
|
@@ -225,9 +280,10 @@ export function buildShardContext(shard, entry) {
|
|
|
225
280
|
sh3: makeSh3Api({
|
|
226
281
|
callerKind: 'shard',
|
|
227
282
|
callerShardId: id,
|
|
228
|
-
zones: ((
|
|
283
|
+
zones: ((_l = shard.manifest.permissions) === null || _l === void 0 ? void 0 : _l.includes(PERMISSION_STATE_MANAGE))
|
|
229
284
|
? createZoneManager()
|
|
230
285
|
: undefined,
|
|
286
|
+
docs: browseCap,
|
|
231
287
|
}),
|
|
232
288
|
};
|
|
233
289
|
// Stash env state on the ctx for registerAllShards' hydration step.
|
|
@@ -241,10 +297,12 @@ export const activeShards = $state(new Map());
|
|
|
241
297
|
* an already-entered shard is a no-op. Errors are recorded in
|
|
242
298
|
* `erroredShards` with phase 'register'; one failure does not block others.
|
|
243
299
|
*/
|
|
244
|
-
export async function registerAllShards() {
|
|
300
|
+
export async function registerAllShards(allowed = null) {
|
|
245
301
|
for (const [id, shard] of registeredShards) {
|
|
246
302
|
if (shardEntries.has(id))
|
|
247
303
|
continue;
|
|
304
|
+
if (allowed !== null && !allowed.has(id))
|
|
305
|
+
continue;
|
|
248
306
|
const entry = {
|
|
249
307
|
shard,
|
|
250
308
|
ctx: undefined,
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import { __setActiveScope, __setDocumentBackend } from '../documents/config';
|
|
3
|
+
import { MemoryDocumentBackend } from '../documents/backends';
|
|
4
|
+
import { getShardBinding, rotateShardDocumentNamespace, __resetLifecycleForTest, registerAllShards, runAppActivate, runAppDeactivate, rebuildShardEntry, shardEntries, listRegisteredShards, } from './lifecycle.svelte';
|
|
3
5
|
import { registerShard, __resetShardRegistryForTest, erroredShards } from './lifecycle.svelte';
|
|
6
|
+
function makeShard(id, registerFn) {
|
|
7
|
+
return {
|
|
8
|
+
manifest: { id, label: id, version: '0.0.0', views: [] },
|
|
9
|
+
register: registerFn,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
4
12
|
describe('shards/lifecycle — binding map', () => {
|
|
5
13
|
beforeEach(() => __resetLifecycleForTest());
|
|
6
14
|
it('returns null for an unbound shard', () => {
|
|
@@ -137,3 +145,104 @@ describe('hot-swap on re-register', () => {
|
|
|
137
145
|
expect((_c = shardEntries.get('hot-s')) === null || _c === void 0 ? void 0 : _c.shard.manifest.version).toBe('0.0.1');
|
|
138
146
|
});
|
|
139
147
|
});
|
|
148
|
+
describe('registerAllShards with allowed-set gate', () => {
|
|
149
|
+
beforeEach(() => {
|
|
150
|
+
__resetLifecycleForTest();
|
|
151
|
+
__resetShardRegistryForTest();
|
|
152
|
+
});
|
|
153
|
+
it('skips shards not in the allowed set when one is provided', async () => {
|
|
154
|
+
const calls = [];
|
|
155
|
+
const a = makeShard('shard-a', () => { calls.push('a'); });
|
|
156
|
+
const b = makeShard('shard-b', () => { calls.push('b'); });
|
|
157
|
+
const c = makeShard('shard-c', () => { calls.push('c'); });
|
|
158
|
+
registerShard(a);
|
|
159
|
+
registerShard(b);
|
|
160
|
+
registerShard(c);
|
|
161
|
+
await registerAllShards(new Set(['shard-a', 'shard-c']));
|
|
162
|
+
expect(calls).toEqual(['a', 'c']);
|
|
163
|
+
expect(shardEntries.has('shard-a')).toBe(true);
|
|
164
|
+
expect(shardEntries.has('shard-b')).toBe(false);
|
|
165
|
+
expect(shardEntries.has('shard-c')).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
it('registers everything when allowed-set is null', async () => {
|
|
168
|
+
const calls = [];
|
|
169
|
+
const a = makeShard('shard-a', () => { calls.push('a'); });
|
|
170
|
+
const b = makeShard('shard-b', () => { calls.push('b'); });
|
|
171
|
+
registerShard(a);
|
|
172
|
+
registerShard(b);
|
|
173
|
+
await registerAllShards(null);
|
|
174
|
+
expect(calls).toEqual(['a', 'b']);
|
|
175
|
+
});
|
|
176
|
+
it('registers everything when allowed-set arg is omitted (back-compat)', async () => {
|
|
177
|
+
const calls = [];
|
|
178
|
+
const a = makeShard('shard-a', () => { calls.push('a'); });
|
|
179
|
+
registerShard(a);
|
|
180
|
+
await registerAllShards();
|
|
181
|
+
expect(calls).toEqual(['a']);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
describe('listRegisteredShards', () => {
|
|
185
|
+
beforeEach(() => {
|
|
186
|
+
__resetLifecycleForTest();
|
|
187
|
+
__resetShardRegistryForTest();
|
|
188
|
+
});
|
|
189
|
+
it('returns the manifest of every registered shard', () => {
|
|
190
|
+
var _a;
|
|
191
|
+
const a = {
|
|
192
|
+
manifest: { id: 'a', label: 'A', version: '1.0.0', views: [] },
|
|
193
|
+
register: () => { },
|
|
194
|
+
};
|
|
195
|
+
const b = {
|
|
196
|
+
manifest: { id: 'b', label: 'B', version: '1.0.0', views: [], kind: 'service' },
|
|
197
|
+
register: () => { },
|
|
198
|
+
};
|
|
199
|
+
registerShard(a);
|
|
200
|
+
registerShard(b);
|
|
201
|
+
const manifests = listRegisteredShards();
|
|
202
|
+
expect(manifests.map((m) => m.id).sort()).toEqual(['a', 'b']);
|
|
203
|
+
expect((_a = manifests.find((m) => m.id === 'b')) === null || _a === void 0 ? void 0 : _a.kind).toBe('service');
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
describe('shards/lifecycle — ctx.sh3.docs wiring', () => {
|
|
207
|
+
beforeEach(() => {
|
|
208
|
+
__resetLifecycleForTest();
|
|
209
|
+
__resetShardRegistryForTest();
|
|
210
|
+
__setDocumentBackend(new MemoryDocumentBackend());
|
|
211
|
+
__setActiveScope('tenant-test');
|
|
212
|
+
});
|
|
213
|
+
it('mirrors ctx.browse onto ctx.sh3.docs for browse-permitted shards', async () => {
|
|
214
|
+
let captured = { sh3Docs: 'untouched', ctxBrowse: 'untouched' };
|
|
215
|
+
const shard = {
|
|
216
|
+
manifest: {
|
|
217
|
+
id: 'browse-test',
|
|
218
|
+
label: 'Browse Test',
|
|
219
|
+
version: '0.0.1',
|
|
220
|
+
views: [],
|
|
221
|
+
permissions: ['documents:browse', 'documents:read', 'documents:write'],
|
|
222
|
+
},
|
|
223
|
+
register(ctx) {
|
|
224
|
+
captured = { sh3Docs: ctx.sh3.docs, ctxBrowse: ctx.browse };
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
registerShard(shard);
|
|
228
|
+
await registerAllShards();
|
|
229
|
+
expect(captured.sh3Docs).toBeDefined();
|
|
230
|
+
expect(captured.sh3Docs).toBe(captured.ctxBrowse);
|
|
231
|
+
});
|
|
232
|
+
it('leaves ctx.sh3.docs undefined when shard lacks documents:browse', async () => {
|
|
233
|
+
let docsRef = 'untouched';
|
|
234
|
+
const shard = {
|
|
235
|
+
manifest: {
|
|
236
|
+
id: 'no-browse-test',
|
|
237
|
+
label: 'No Browse',
|
|
238
|
+
version: '0.0.1',
|
|
239
|
+
views: [],
|
|
240
|
+
permissions: [],
|
|
241
|
+
},
|
|
242
|
+
register(ctx) { docsRef = ctx.sh3.docs; },
|
|
243
|
+
};
|
|
244
|
+
registerShard(shard);
|
|
245
|
+
await registerAllShards();
|
|
246
|
+
expect(docsRef).toBeUndefined();
|
|
247
|
+
});
|
|
248
|
+
});
|
package/dist/shards/types.d.ts
CHANGED
|
@@ -176,6 +176,19 @@ export interface ShardManifest {
|
|
|
176
176
|
* by sh3-validate at build time.
|
|
177
177
|
*/
|
|
178
178
|
verbNamespace?: string;
|
|
179
|
+
/**
|
|
180
|
+
* Project-allowlist classification.
|
|
181
|
+
* - 'system' : always allowed in every project scope. The shard is part
|
|
182
|
+
* of the framework/OS surface (e.g. shell-shard). Surfaced
|
|
183
|
+
* read-only in the project manage view.
|
|
184
|
+
* - 'service' : opt-in per project. Appears in the Shards tab of the
|
|
185
|
+
* project manage view; admin ticks it to grant write access.
|
|
186
|
+
* - omitted : regular shard. Reached only via an app's
|
|
187
|
+
* requiredShards / bundledShards (existing behavior).
|
|
188
|
+
*
|
|
189
|
+
* Apps never set this — shard-only.
|
|
190
|
+
*/
|
|
191
|
+
kind?: 'system' | 'service';
|
|
179
192
|
}
|
|
180
193
|
/**
|
|
181
194
|
* Source-declared shape of a shard manifest — what external package authors
|
|
@@ -28,7 +28,6 @@
|
|
|
28
28
|
import BusySlot from './toolbar/slots/BusySlot.svelte';
|
|
29
29
|
import { registerTerminalView, mintTerminalId, type TerminalHandle } from './terminal-registry';
|
|
30
30
|
import { makeDispatchToTerminal } from './dispatch-to-terminal';
|
|
31
|
-
import type { BrowseCapability } from '../documents/browse';
|
|
32
31
|
|
|
33
32
|
interface Props {
|
|
34
33
|
shell: Sh3Api;
|
|
@@ -36,9 +35,8 @@
|
|
|
36
35
|
userId: string;
|
|
37
36
|
role: ShellRole;
|
|
38
37
|
contributions: ContributionsApi;
|
|
39
|
-
docs?: BrowseCapability;
|
|
40
38
|
}
|
|
41
|
-
let { shell, wsUrl, userId, role, contributions
|
|
39
|
+
let { shell, wsUrl, userId, role, contributions }: Props = $props();
|
|
42
40
|
|
|
43
41
|
// Per-mode buffer map. Each ModeBuffer bundles a Scrollback + history +
|
|
44
42
|
// locked flag and is materialized lazily on first switch into that mode.
|
|
@@ -252,7 +250,6 @@
|
|
|
252
250
|
session,
|
|
253
251
|
sh3: shellWithModes,
|
|
254
252
|
fs,
|
|
255
|
-
docs,
|
|
256
253
|
cwd: () => session.cwd,
|
|
257
254
|
busy: acquireBusy,
|
|
258
255
|
customMode: (id: string) => contributedModes.find((d) => d.id === id) ?? null,
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { type Sh3Api } from './registry';
|
|
2
2
|
import type { ShellRole } from './modes/types';
|
|
3
3
|
import type { ContributionsApi } from '../contributions/types';
|
|
4
|
-
import type { BrowseCapability } from '../documents/browse';
|
|
5
4
|
interface Props {
|
|
6
5
|
shell: Sh3Api;
|
|
7
6
|
wsUrl: string;
|
|
8
7
|
userId: string;
|
|
9
8
|
role: ShellRole;
|
|
10
9
|
contributions: ContributionsApi;
|
|
11
|
-
docs?: BrowseCapability;
|
|
12
10
|
}
|
|
13
11
|
declare const Terminal: import("svelte").Component<Props, {}, "">;
|
|
14
12
|
type Terminal = ReturnType<typeof Terminal>;
|
|
@@ -4,7 +4,6 @@ import type { TenantFsClient } from './tenant-fs-client';
|
|
|
4
4
|
import type { ModeBuffer } from './mode-buffer.svelte';
|
|
5
5
|
import type { ShellMode, ShellRole } from './modes/types';
|
|
6
6
|
import type { ShellModeDescriptor } from './contract';
|
|
7
|
-
import type { BrowseCapability } from '../documents/browse';
|
|
8
7
|
export interface DispatchDeps {
|
|
9
8
|
mode: () => ShellMode;
|
|
10
9
|
/** Current shell role — used by invoke() role-gating. */
|
|
@@ -19,7 +18,6 @@ export interface DispatchDeps {
|
|
|
19
18
|
session: SessionClient;
|
|
20
19
|
sh3: Sh3Api;
|
|
21
20
|
fs: TenantFsClient;
|
|
22
|
-
docs?: BrowseCapability;
|
|
23
21
|
cwd: () => string;
|
|
24
22
|
/**
|
|
25
23
|
* Acquire a busy indicator. Returns a clear handle. Calling clear()
|
|
@@ -39,7 +39,6 @@ export function makeDispatch(deps) {
|
|
|
39
39
|
cwd: deps.cwd(),
|
|
40
40
|
dispatch,
|
|
41
41
|
fs: deps.fs,
|
|
42
|
-
docs: deps.docs,
|
|
43
42
|
}, resolution.args);
|
|
44
43
|
return;
|
|
45
44
|
}
|
|
@@ -108,7 +107,6 @@ export function makeDispatch(deps) {
|
|
|
108
107
|
cwd: deps.cwd(),
|
|
109
108
|
dispatch,
|
|
110
109
|
fs: deps.fs,
|
|
111
|
-
docs: deps.docs,
|
|
112
110
|
}, resolution.args);
|
|
113
111
|
}
|
|
114
112
|
catch (err) {
|
|
@@ -2,19 +2,19 @@ import { describe, it, expect } from 'vitest';
|
|
|
2
2
|
import { shortenCwd } from './display-cwd';
|
|
3
3
|
describe('shortenCwd', () => {
|
|
4
4
|
it('returns ~ when cwd equals tenant root', () => {
|
|
5
|
-
expect(shortenCwd('/home/u/data/
|
|
5
|
+
expect(shortenCwd('/home/u/data/docs/x', '/home/u/data/docs/x')).toBe('~');
|
|
6
6
|
});
|
|
7
7
|
it('substitutes ~ for tenant-root prefix (POSIX)', () => {
|
|
8
|
-
expect(shortenCwd('/home/u/data/
|
|
8
|
+
expect(shortenCwd('/home/u/data/docs/x/notes', '/home/u/data/docs/x')).toBe('~/notes');
|
|
9
9
|
});
|
|
10
10
|
it('substitutes ~ for tenant-root prefix (Windows)', () => {
|
|
11
|
-
expect(shortenCwd('C:\\a\\b\\
|
|
11
|
+
expect(shortenCwd('C:\\a\\b\\docs\\x\\notes', 'C:\\a\\b\\docs\\x')).toBe('~/notes');
|
|
12
12
|
});
|
|
13
13
|
it('normalizes Windows backslashes to forward slashes after the tilde', () => {
|
|
14
14
|
expect(shortenCwd('C:\\root\\a\\b\\c', 'C:\\root')).toBe('~/a/b/c');
|
|
15
15
|
});
|
|
16
16
|
it('returns the absolute cwd when outside the tenant root', () => {
|
|
17
|
-
expect(shortenCwd('/tmp', '/home/u/data/
|
|
17
|
+
expect(shortenCwd('/tmp', '/home/u/data/docs/x')).toBe('/tmp');
|
|
18
18
|
});
|
|
19
19
|
it('does not match when cwd matches the tenant root prefix without a separator boundary', () => {
|
|
20
20
|
// tenant=/foo, cwd=/foobar → must NOT shorten to ~bar
|
|
@@ -5,6 +5,7 @@ export const manifest = {
|
|
|
5
5
|
id: 'shell',
|
|
6
6
|
label: 'Sh3',
|
|
7
7
|
version: VERSION,
|
|
8
|
+
kind: 'system',
|
|
8
9
|
views: [{ id: 'shell:terminal', label: 'Sh3', standalone: true }],
|
|
9
10
|
// serverBundle intentionally omitted — this shard is a framework built-in
|
|
10
11
|
// and is statically mounted at sh3-server boot. The existing contract in
|
|
@@ -20,18 +20,23 @@ import { mount, unmount } from 'svelte';
|
|
|
20
20
|
import { manifest } from './manifest';
|
|
21
21
|
import Terminal from './Terminal.svelte';
|
|
22
22
|
import { registerV1Verbs } from './verbs';
|
|
23
|
-
import {
|
|
23
|
+
import { makeSh3Api } from '../sh3Api/headless';
|
|
24
24
|
import { focusView } from '../layout/inspection';
|
|
25
25
|
import { floatManager } from '../overlays/float';
|
|
26
26
|
import { getUser, isAdmin } from '../auth/index';
|
|
27
27
|
import { __bindZone, __unbindZone } from './buffer-zone-state.svelte';
|
|
28
28
|
import { getAuthToken } from '../transport/authToken';
|
|
29
|
-
export {
|
|
29
|
+
export { makeSh3ApiForTest } from '../sh3Api/headless';
|
|
30
30
|
export const shellShard = {
|
|
31
31
|
manifest,
|
|
32
32
|
register(ctx) {
|
|
33
33
|
registerV1Verbs(ctx);
|
|
34
|
-
const shell =
|
|
34
|
+
const shell = makeSh3Api({
|
|
35
|
+
callerKind: 'shard',
|
|
36
|
+
callerShardId: 'shell',
|
|
37
|
+
zones: ctx.zones,
|
|
38
|
+
docs: ctx.browse,
|
|
39
|
+
});
|
|
35
40
|
// Bind the shell-shard's workspace zone — backs scrollback persistence
|
|
36
41
|
// (SH8). BufferStore reads/writes through this proxy.
|
|
37
42
|
const zone = ctx.state({
|
|
@@ -73,7 +78,7 @@ export const shellShard = {
|
|
|
73
78
|
const role = isAdmin() ? 'admin' : 'user';
|
|
74
79
|
const instance = mount(Terminal, {
|
|
75
80
|
target: container,
|
|
76
|
-
props: { shell, wsUrl, userId, role, contributions: ctx.contributions
|
|
81
|
+
props: { shell, wsUrl, userId, role, contributions: ctx.contributions },
|
|
77
82
|
});
|
|
78
83
|
return {
|
|
79
84
|
unmount() {
|
|
@@ -5,7 +5,7 @@ export const catVerb = {
|
|
|
5
5
|
programmatic: true,
|
|
6
6
|
async run(ctx, args) {
|
|
7
7
|
const ts = Date.now();
|
|
8
|
-
if (!ctx.docs) {
|
|
8
|
+
if (!ctx.sh3.docs) {
|
|
9
9
|
ctx.scrollback.push({ kind: 'status', text: 'cat: document capability not available', level: 'error', ts });
|
|
10
10
|
return;
|
|
11
11
|
}
|
|
@@ -18,11 +18,11 @@ export const catVerb = {
|
|
|
18
18
|
ctx.scrollback.push({ kind: 'status', text: `cat: invalid path '${args[0]}'`, level: 'error', ts });
|
|
19
19
|
return;
|
|
20
20
|
}
|
|
21
|
-
if (!ctx.docs.readFrom) {
|
|
21
|
+
if (!ctx.sh3.docs.readFrom) {
|
|
22
22
|
ctx.scrollback.push({ kind: 'status', text: 'cat: read permission not granted', level: 'error', ts });
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
|
-
const content = await ctx.docs.readFrom(parsed.shardId, parsed.path);
|
|
25
|
+
const content = await ctx.sh3.docs.readFrom(parsed.shardId, parsed.path);
|
|
26
26
|
if (content === null) {
|
|
27
27
|
ctx.scrollback.push({ kind: 'status', text: `cat: not found: ${args[0]}`, level: 'error', ts });
|
|
28
28
|
return;
|
|
@@ -6,12 +6,11 @@ function makeDocs(overrides = {}) {
|
|
|
6
6
|
function makeCtx(docs) {
|
|
7
7
|
const pushed = [];
|
|
8
8
|
const ctx = {
|
|
9
|
-
sh3: {},
|
|
9
|
+
sh3: { docs },
|
|
10
10
|
scrollback: { push: (e) => pushed.push(e) },
|
|
11
11
|
session: {},
|
|
12
12
|
cwd: '/',
|
|
13
13
|
fs: {},
|
|
14
|
-
docs,
|
|
15
14
|
dispatch: async () => { },
|
|
16
15
|
};
|
|
17
16
|
return { ctx, pushed };
|
|
@@ -6,11 +6,11 @@ export const lsVerb = {
|
|
|
6
6
|
async run(ctx, args) {
|
|
7
7
|
var _a, _b, _c;
|
|
8
8
|
const ts = Date.now();
|
|
9
|
-
if (!ctx.docs) {
|
|
9
|
+
if (!ctx.sh3.docs) {
|
|
10
10
|
ctx.scrollback.push({ kind: 'status', text: 'ls: document capability not available', level: 'error', ts });
|
|
11
11
|
return;
|
|
12
12
|
}
|
|
13
|
-
const all = await ctx.docs.listDocuments();
|
|
13
|
+
const all = await ctx.sh3.docs.listDocuments();
|
|
14
14
|
const arg = args[0];
|
|
15
15
|
if (!arg) {
|
|
16
16
|
// Group by shard, emit counts
|
|
@@ -6,12 +6,11 @@ function makeDocs(overrides = {}) {
|
|
|
6
6
|
function makeCtx(docs) {
|
|
7
7
|
const pushed = [];
|
|
8
8
|
const ctx = {
|
|
9
|
-
sh3: {},
|
|
9
|
+
sh3: { docs },
|
|
10
10
|
scrollback: { push: (e) => pushed.push(e) },
|
|
11
11
|
session: {},
|
|
12
12
|
cwd: '/',
|
|
13
13
|
fs: {},
|
|
14
|
-
docs,
|
|
15
14
|
dispatch: async () => { },
|
|
16
15
|
};
|
|
17
16
|
return { ctx, pushed };
|
|
@@ -5,7 +5,7 @@ export const mkdirVerb = {
|
|
|
5
5
|
programmatic: true,
|
|
6
6
|
async run(ctx, args) {
|
|
7
7
|
const ts = Date.now();
|
|
8
|
-
if (!ctx.docs) {
|
|
8
|
+
if (!ctx.sh3.docs) {
|
|
9
9
|
ctx.scrollback.push({ kind: 'status', text: 'mkdir: document capability not available', level: 'error', ts });
|
|
10
10
|
return;
|
|
11
11
|
}
|
|
@@ -13,7 +13,7 @@ export const mkdirVerb = {
|
|
|
13
13
|
ctx.scrollback.push({ kind: 'status', text: 'usage: mkdir <shardId>/<folder>', level: 'error', ts });
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
|
-
if (!ctx.docs.writeTo) {
|
|
16
|
+
if (!ctx.sh3.docs.writeTo) {
|
|
17
17
|
ctx.scrollback.push({ kind: 'status', text: 'mkdir: write permission not granted', level: 'error', ts });
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
@@ -24,7 +24,7 @@ export const mkdirVerb = {
|
|
|
24
24
|
}
|
|
25
25
|
// Materialise the folder with a sentinel file; backends treat paths as flat.
|
|
26
26
|
const keepPath = parsed.path.replace(/\/$/, '') + '/.keep';
|
|
27
|
-
await ctx.docs.writeTo(parsed.shardId, keepPath, '');
|
|
27
|
+
await ctx.sh3.docs.writeTo(parsed.shardId, keepPath, '');
|
|
28
28
|
ctx.scrollback.push({ kind: 'status', text: `mkdir: created ${args[0]}`, level: 'info', ts });
|
|
29
29
|
},
|
|
30
30
|
};
|
|
@@ -6,12 +6,11 @@ function makeDocs(overrides = {}) {
|
|
|
6
6
|
function makeCtx(docs) {
|
|
7
7
|
const pushed = [];
|
|
8
8
|
const ctx = {
|
|
9
|
-
sh3: {},
|
|
9
|
+
sh3: { docs },
|
|
10
10
|
scrollback: { push: (e) => pushed.push(e) },
|
|
11
11
|
session: {},
|
|
12
12
|
cwd: '/',
|
|
13
13
|
fs: {},
|
|
14
|
-
docs,
|
|
15
14
|
dispatch: async () => { },
|
|
16
15
|
};
|
|
17
16
|
return { ctx, pushed };
|
|
@@ -5,7 +5,7 @@ export const mvVerb = {
|
|
|
5
5
|
programmatic: true,
|
|
6
6
|
async run(ctx, args) {
|
|
7
7
|
const ts = Date.now();
|
|
8
|
-
if (!ctx.docs) {
|
|
8
|
+
if (!ctx.sh3.docs) {
|
|
9
9
|
ctx.scrollback.push({ kind: 'status', text: 'mv: document capability not available', level: 'error', ts });
|
|
10
10
|
return;
|
|
11
11
|
}
|
|
@@ -13,7 +13,7 @@ export const mvVerb = {
|
|
|
13
13
|
ctx.scrollback.push({ kind: 'status', text: 'usage: mv <shardId>/<old> <shardId>/<new>', level: 'error', ts });
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
|
-
if (!ctx.docs.renameFrom) {
|
|
16
|
+
if (!ctx.sh3.docs.renameFrom) {
|
|
17
17
|
ctx.scrollback.push({ kind: 'status', text: 'mv: write permission not granted', level: 'error', ts });
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
@@ -27,7 +27,7 @@ export const mvVerb = {
|
|
|
27
27
|
ctx.scrollback.push({ kind: 'status', text: 'mv: src and dst must be in the same shard (use xfer for cross-scope moves)', level: 'error', ts });
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
30
|
-
await ctx.docs.renameFrom(src.shardId, src.path, dst.path);
|
|
30
|
+
await ctx.sh3.docs.renameFrom(src.shardId, src.path, dst.path);
|
|
31
31
|
ctx.scrollback.push({ kind: 'status', text: `mv: renamed ${args[0]} → ${args[1]}`, level: 'info', ts });
|
|
32
32
|
},
|
|
33
33
|
};
|
|
@@ -6,12 +6,11 @@ function makeDocs(overrides = {}) {
|
|
|
6
6
|
function makeCtx(docs) {
|
|
7
7
|
const pushed = [];
|
|
8
8
|
const ctx = {
|
|
9
|
-
sh3: {},
|
|
9
|
+
sh3: { docs },
|
|
10
10
|
scrollback: { push: (e) => pushed.push(e) },
|
|
11
11
|
session: {},
|
|
12
12
|
cwd: '/',
|
|
13
13
|
fs: {},
|
|
14
|
-
docs,
|
|
15
14
|
dispatch: async () => { },
|
|
16
15
|
};
|
|
17
16
|
return { ctx, pushed };
|
|
@@ -5,7 +5,7 @@ export const rmVerb = {
|
|
|
5
5
|
programmatic: true,
|
|
6
6
|
async run(ctx, args) {
|
|
7
7
|
const ts = Date.now();
|
|
8
|
-
if (!ctx.docs) {
|
|
8
|
+
if (!ctx.sh3.docs) {
|
|
9
9
|
ctx.scrollback.push({ kind: 'status', text: 'rm: document capability not available', level: 'error', ts });
|
|
10
10
|
return;
|
|
11
11
|
}
|
|
@@ -13,7 +13,7 @@ export const rmVerb = {
|
|
|
13
13
|
ctx.scrollback.push({ kind: 'status', text: 'usage: rm <shardId>/<path>', level: 'error', ts });
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
|
-
if (!ctx.docs.deleteFrom) {
|
|
16
|
+
if (!ctx.sh3.docs.deleteFrom) {
|
|
17
17
|
ctx.scrollback.push({ kind: 'status', text: 'rm: write permission not granted', level: 'error', ts });
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
@@ -22,7 +22,7 @@ export const rmVerb = {
|
|
|
22
22
|
ctx.scrollback.push({ kind: 'status', text: `rm: invalid path '${args[0]}'`, level: 'error', ts });
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
|
-
await ctx.docs.deleteFrom(parsed.shardId, parsed.path);
|
|
25
|
+
await ctx.sh3.docs.deleteFrom(parsed.shardId, parsed.path);
|
|
26
26
|
ctx.scrollback.push({ kind: 'status', text: `rm: deleted ${args[0]}`, level: 'info', ts });
|
|
27
27
|
},
|
|
28
28
|
};
|
|
@@ -6,12 +6,11 @@ function makeDocs(overrides = {}) {
|
|
|
6
6
|
function makeCtx(docs) {
|
|
7
7
|
const pushed = [];
|
|
8
8
|
const ctx = {
|
|
9
|
-
sh3: {},
|
|
9
|
+
sh3: { docs },
|
|
10
10
|
scrollback: { push: (e) => pushed.push(e) },
|
|
11
11
|
session: {},
|
|
12
12
|
cwd: '/',
|
|
13
13
|
fs: {},
|
|
14
|
-
docs,
|
|
15
14
|
dispatch: async () => { },
|
|
16
15
|
};
|
|
17
16
|
return { ctx, pushed };
|
|
@@ -11,11 +11,11 @@ export const xferVerb = {
|
|
|
11
11
|
programmatic: true,
|
|
12
12
|
async run(ctx, args) {
|
|
13
13
|
const ts = Date.now();
|
|
14
|
-
if (!ctx.docs) {
|
|
14
|
+
if (!ctx.sh3.docs) {
|
|
15
15
|
ctx.scrollback.push({ kind: 'status', text: 'xfer: document capability not available', level: 'error', ts });
|
|
16
16
|
return;
|
|
17
17
|
}
|
|
18
|
-
if (!ctx.docs.transferBetweenScopes) {
|
|
18
|
+
if (!ctx.sh3.docs.transferBetweenScopes) {
|
|
19
19
|
ctx.scrollback.push({ kind: 'status', text: 'xfer: write permission not granted', level: 'error', ts });
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
@@ -64,13 +64,13 @@ export const xferVerb = {
|
|
|
64
64
|
ctx.scrollback.push({ kind: 'status', text: 'xfer: source and destination are the same', level: 'error', ts });
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
|
-
await ctx.docs.transferBetweenScopes(srcTenant, srcParsed.shardId, srcParsed.path, dstTenant, dstParsed.shardId, dstParsed.path, moveOpts);
|
|
67
|
+
await ctx.sh3.docs.transferBetweenScopes(srcTenant, srcParsed.shardId, srcParsed.path, dstTenant, dstParsed.shardId, dstParsed.path, moveOpts);
|
|
68
68
|
const verb = copy ? 'copied' : 'moved';
|
|
69
69
|
ctx.scrollback.push({ kind: 'status', text: `xfer: ${verb} ${positional[0]} → ${positional[1]}`, level: 'info', ts });
|
|
70
70
|
return;
|
|
71
71
|
}
|
|
72
72
|
const prefix = srcParsed.path;
|
|
73
|
-
const allDocs = await ctx.docs.listDocumentsIn(srcTenant);
|
|
73
|
+
const allDocs = await ctx.sh3.docs.listDocumentsIn(srcTenant);
|
|
74
74
|
const matching = allDocs.filter((d) => d.shardId === srcParsed.shardId && (!prefix || d.path.startsWith(prefix)));
|
|
75
75
|
if (matching.length === 0) {
|
|
76
76
|
ctx.scrollback.push({ kind: 'status', text: `xfer: no documents found under ${positional[0]}`, level: 'info', ts });
|
|
@@ -78,7 +78,7 @@ export const xferVerb = {
|
|
|
78
78
|
}
|
|
79
79
|
let count = 0;
|
|
80
80
|
for (const doc of matching) {
|
|
81
|
-
await ctx.docs.transferBetweenScopes(srcTenant, doc.shardId, doc.path, dstTenant, dstParsed.shardId, doc.path, moveOpts);
|
|
81
|
+
await ctx.sh3.docs.transferBetweenScopes(srcTenant, doc.shardId, doc.path, dstTenant, dstParsed.shardId, doc.path, moveOpts);
|
|
82
82
|
count++;
|
|
83
83
|
}
|
|
84
84
|
const verb = copy ? 'copied' : 'moved';
|