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.
Files changed (88) hide show
  1. package/dist/api.d.ts +1 -1
  2. package/dist/api.js +1 -1
  3. package/dist/app/admin/adminApp.js +2 -0
  4. package/dist/app/admin/adminShard.svelte.js +1 -0
  5. package/dist/app/store/storeApp.js +3 -1
  6. package/dist/app/store/storeShard.svelte.js +1 -0
  7. package/dist/app-appearance/appearanceShard.svelte.js +1 -0
  8. package/dist/apps/lifecycle.js +22 -10
  9. package/dist/apps/lifecycle.test.js +53 -1
  10. package/dist/apps/types.d.ts +13 -0
  11. package/dist/chrome/CompactChrome.svelte +11 -7
  12. package/dist/createShell.js +40 -0
  13. package/dist/documents/picker-api.test.js +40 -0
  14. package/dist/documents/picker-primitive.d.ts +39 -1
  15. package/dist/documents/picker-primitive.js +5 -4
  16. package/dist/host.js +30 -7
  17. package/dist/layout/slotHostPool.svelte.d.ts +11 -0
  18. package/dist/layout/slotHostPool.svelte.js +41 -17
  19. package/dist/layout/slotHostPool.test.js +45 -1
  20. package/dist/layouts-shard/layoutsShard.svelte.js +1 -0
  21. package/dist/overlays/OverlayRoots.svelte +15 -4
  22. package/dist/overlays/__test__/OverlayBindHarness.svelte +20 -0
  23. package/dist/overlays/__test__/OverlayBindHarness.svelte.d.ts +3 -0
  24. package/dist/overlays/float-compact-bind.svelte.test.d.ts +1 -0
  25. package/dist/overlays/float-compact-bind.svelte.test.js +51 -0
  26. package/dist/overlays/modal.js +3 -0
  27. package/dist/overlays/modal.test.js +45 -0
  28. package/dist/overlays/types.d.ts +9 -0
  29. package/dist/primitives/widgets/Field.svelte +5 -0
  30. package/dist/primitives/widgets/Field.svelte.d.ts +1 -0
  31. package/dist/primitives/widgets/Field.svelte.test.js +16 -0
  32. package/dist/primitives/widgets/NumberInput.svelte +21 -12
  33. package/dist/primitives/widgets/NumberInput.svelte.d.ts +2 -0
  34. package/dist/primitives/widgets/NumberInput.svelte.test.js +26 -0
  35. package/dist/primitives/widgets/ShardPicker.svelte +38 -0
  36. package/dist/primitives/widgets/ShardPicker.svelte.d.ts +9 -0
  37. package/dist/primitives/widgets/Textarea.svelte +5 -0
  38. package/dist/primitives/widgets/Textarea.svelte.d.ts +1 -0
  39. package/dist/primitives/widgets/Textarea.svelte.test.js +16 -0
  40. package/dist/primitives/widgets/_DocumentBrowser.svelte +11 -3
  41. package/dist/primitives/widgets/_DocumentBrowser.svelte.d.ts +2 -0
  42. package/dist/primitives/widgets/_selectOnFocus.d.ts +15 -0
  43. package/dist/primitives/widgets/_selectOnFocus.js +24 -0
  44. package/dist/projects/scope-gate.d.ts +4 -0
  45. package/dist/projects/scope-gate.js +51 -0
  46. package/dist/projects/scope-gate.test.d.ts +1 -0
  47. package/dist/projects/scope-gate.test.js +92 -0
  48. package/dist/projects-shard/ProjectManage.svelte +42 -2
  49. package/dist/projects-shard/ProjectManage.svelte.test.js +10 -9
  50. package/dist/projects-shard/projectsApi.d.ts +3 -2
  51. package/dist/projects-shard/projectsApi.test.js +1 -1
  52. package/dist/projects-shard/projectsShard.svelte.js +1 -0
  53. package/dist/runtime/runVerb.d.ts +9 -0
  54. package/dist/runtime/runVerb.js +4 -4
  55. package/dist/runtime/runVerb.test.js +29 -0
  56. package/dist/sh3Api/headless.d.ts +7 -0
  57. package/dist/sh3Api/headless.js +3 -1
  58. package/dist/sh3Api/headless.svelte.test.js +42 -0
  59. package/dist/sh3core-shard/Sh3Home.svelte +5 -4
  60. package/dist/sh3core-shard/sh3coreShard.svelte.js +1 -0
  61. package/dist/shards/lifecycle.svelte.d.ts +8 -2
  62. package/dist/shards/lifecycle.svelte.js +65 -7
  63. package/dist/shards/lifecycle.test.js +110 -1
  64. package/dist/shards/types.d.ts +13 -0
  65. package/dist/shell-shard/Terminal.svelte +1 -4
  66. package/dist/shell-shard/Terminal.svelte.d.ts +0 -2
  67. package/dist/shell-shard/dispatch.d.ts +0 -2
  68. package/dist/shell-shard/dispatch.js +0 -2
  69. package/dist/shell-shard/display-cwd.test.js +4 -4
  70. package/dist/shell-shard/manifest.js +1 -0
  71. package/dist/shell-shard/shellShard.svelte.d.ts +1 -1
  72. package/dist/shell-shard/shellShard.svelte.js +9 -4
  73. package/dist/shell-shard/verbs/cat.js +3 -3
  74. package/dist/shell-shard/verbs/cat.test.js +1 -2
  75. package/dist/shell-shard/verbs/ls.js +2 -2
  76. package/dist/shell-shard/verbs/ls.test.js +1 -2
  77. package/dist/shell-shard/verbs/mkdir.js +3 -3
  78. package/dist/shell-shard/verbs/mkdir.test.js +1 -2
  79. package/dist/shell-shard/verbs/mv.js +3 -3
  80. package/dist/shell-shard/verbs/mv.test.js +1 -2
  81. package/dist/shell-shard/verbs/rm.js +3 -3
  82. package/dist/shell-shard/verbs/rm.test.js +1 -2
  83. package/dist/shell-shard/verbs/xfer.js +5 -5
  84. package/dist/shell-shard/verbs/xfer.test.js +2 -2
  85. package/dist/verbs/types.d.ts +10 -2
  86. package/dist/version.d.ts +1 -1
  87. package/dist/version.js +1 -1
  88. 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: ((_g = shard.manifest.permissions) === null || _g === void 0 ? void 0 : _g.includes(PERMISSION_KEYS_MINT))
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: (_h = shard.manifest.permissions) !== null && _h !== void 0 ? _h : [],
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: ((_j = shard.manifest.permissions) === null || _j === void 0 ? void 0 : _j.includes(PERMISSION_STATE_MANAGE))
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 { getShardBinding, rotateShardDocumentNamespace, __resetLifecycleForTest, registerAllShards, runAppActivate, runAppDeactivate, rebuildShardEntry, shardEntries, } from './lifecycle.svelte';
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
+ });
@@ -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, docs }: Props = $props();
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/users/x/documents/shell', '/home/u/data/users/x/documents/shell')).toBe('~');
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/users/x/documents/shell/notes', '/home/u/data/users/x/documents/shell')).toBe('~/notes');
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\\users\\x\\documents\\shell\\notes', 'C:\\a\\b\\users\\x\\documents\\shell')).toBe('~/notes');
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/users/x/documents/shell')).toBe('/tmp');
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
@@ -1,3 +1,3 @@
1
1
  import type { Shard } from '../api';
2
- export { makeSh3ApiHeadless, makeSh3ApiForTest } from '../sh3Api/headless';
2
+ export { makeSh3ApiForTest } from '../sh3Api/headless';
3
3
  export declare const shellShard: Shard;
@@ -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 { makeSh3ApiHeadless } from '../sh3Api/headless';
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 { makeSh3ApiHeadless, makeSh3ApiForTest } from '../sh3Api/headless';
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 = makeSh3ApiHeadless(ctx.zones);
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, docs: ctx.browse },
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';