sh3-core 0.20.1 → 0.20.3

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 (103) hide show
  1. package/dist/BrandSlot.svelte +2 -2
  2. package/dist/actions/ctx-actions.svelte.test.js +2 -2
  3. package/dist/artifact.d.ts +2 -0
  4. package/dist/boot/satellitePayload.d.ts +2 -0
  5. package/dist/boot/satellitePayload.test.js +19 -0
  6. package/dist/build.d.ts +7 -1
  7. package/dist/build.js +22 -3
  8. package/dist/build.test.js +27 -1
  9. package/dist/createShell.js +34 -9
  10. package/dist/documents/backends.d.ts +12 -0
  11. package/dist/documents/backends.js +230 -3
  12. package/dist/documents/backends.test.js +147 -1
  13. package/dist/documents/browse.d.ts +20 -0
  14. package/dist/documents/browse.js +35 -0
  15. package/dist/documents/browse.test.js +125 -0
  16. package/dist/documents/config.d.ts +2 -4
  17. package/dist/documents/config.js +3 -7
  18. package/dist/documents/handle.js +40 -0
  19. package/dist/documents/handle.test.js +88 -1
  20. package/dist/documents/http-backend.d.ts +11 -0
  21. package/dist/documents/http-backend.js +86 -0
  22. package/dist/documents/http-backend.test.js +117 -1
  23. package/dist/documents/index.d.ts +1 -1
  24. package/dist/documents/index.js +1 -1
  25. package/dist/documents/picker-api.test.js +2 -2
  26. package/dist/documents/types.d.ts +87 -14
  27. package/dist/documents/types.js +4 -0
  28. package/dist/host-entry.d.ts +1 -1
  29. package/dist/host-entry.js +1 -1
  30. package/dist/host.d.ts +1 -1
  31. package/dist/host.js +1 -1
  32. package/dist/layout/slotHostPool.svelte.js +2 -2
  33. package/dist/overlays/FloatFrame.svelte +1 -0
  34. package/dist/primitives/widgets/DocumentFilePicker.d.ts +6 -2
  35. package/dist/primitives/widgets/DocumentFilePicker.js +12 -5
  36. package/dist/primitives/widgets/DocumentFilePicker.svelte +23 -1
  37. package/dist/primitives/widgets/DocumentFilePicker.svelte.d.ts +14 -0
  38. package/dist/primitives/widgets/DocumentFilePicker.test.d.ts +1 -0
  39. package/dist/primitives/widgets/DocumentFilePicker.test.js +33 -0
  40. package/dist/primitives/widgets/DocumentOpener.svelte +20 -0
  41. package/dist/primitives/widgets/DocumentOpener.svelte.d.ts +14 -0
  42. package/dist/primitives/widgets/DocumentSaver.svelte +17 -0
  43. package/dist/primitives/widgets/DocumentSaver.svelte.d.ts +13 -0
  44. package/dist/primitives/widgets/_DocumentBrowser.svelte +414 -27
  45. package/dist/primitives/widgets/_DocumentBrowser.svelte.d.ts +12 -0
  46. package/dist/primitives/widgets/_DocumentBrowser.svelte.test.d.ts +1 -0
  47. package/dist/primitives/widgets/_DocumentBrowser.svelte.test.js +277 -0
  48. package/dist/primitives/widgets/_FolderConfirmDelete.svelte +57 -0
  49. package/dist/primitives/widgets/_FolderConfirmDelete.svelte.d.ts +12 -0
  50. package/dist/projects/session-state.svelte.d.ts +3 -0
  51. package/dist/projects/session-state.svelte.js +25 -0
  52. package/dist/projects/session-state.test.js +43 -2
  53. package/dist/projects-shard/ProjectsSection.svelte +14 -18
  54. package/dist/runtime/runVerb-shell.test.js +2 -2
  55. package/dist/runtime/runVerb.test.js +2 -2
  56. package/dist/sh3Api/headless.js +10 -0
  57. package/dist/sh3core-shard/appActions.js +5 -2
  58. package/dist/shards/activate-browse.test.js +2 -2
  59. package/dist/shards/activate-contributions.test.js +2 -2
  60. package/dist/shards/activate-error-isolation.test.js +3 -3
  61. package/dist/shards/activate-on-key-revoked.test.js +2 -2
  62. package/dist/shards/activate-runtime.test.js +2 -2
  63. package/dist/shards/activate.svelte.js +5 -5
  64. package/dist/shards/ctx-fetch.test.js +4 -4
  65. package/dist/shell-shard/Terminal.svelte +4 -1
  66. package/dist/shell-shard/Terminal.svelte.d.ts +2 -0
  67. package/dist/shell-shard/dispatch.d.ts +2 -0
  68. package/dist/shell-shard/dispatch.js +2 -0
  69. package/dist/shell-shard/manifest.js +7 -1
  70. package/dist/shell-shard/shellShard.svelte.js +1 -1
  71. package/dist/shell-shard/verbs/cat.d.ts +2 -0
  72. package/dist/shell-shard/verbs/cat.js +35 -0
  73. package/dist/shell-shard/verbs/cat.test.d.ts +1 -0
  74. package/dist/shell-shard/verbs/cat.test.js +49 -0
  75. package/dist/shell-shard/verbs/index.js +12 -0
  76. package/dist/shell-shard/verbs/ls.d.ts +2 -0
  77. package/dist/shell-shard/verbs/ls.js +48 -0
  78. package/dist/shell-shard/verbs/ls.test.d.ts +1 -0
  79. package/dist/shell-shard/verbs/ls.test.js +64 -0
  80. package/dist/shell-shard/verbs/mkdir.d.ts +2 -0
  81. package/dist/shell-shard/verbs/mkdir.js +30 -0
  82. package/dist/shell-shard/verbs/mkdir.test.d.ts +1 -0
  83. package/dist/shell-shard/verbs/mkdir.test.js +48 -0
  84. package/dist/shell-shard/verbs/mv.d.ts +2 -0
  85. package/dist/shell-shard/verbs/mv.js +33 -0
  86. package/dist/shell-shard/verbs/mv.test.d.ts +1 -0
  87. package/dist/shell-shard/verbs/mv.test.js +55 -0
  88. package/dist/shell-shard/verbs/rm.d.ts +2 -0
  89. package/dist/shell-shard/verbs/rm.js +28 -0
  90. package/dist/shell-shard/verbs/rm.test.d.ts +1 -0
  91. package/dist/shell-shard/verbs/rm.test.js +47 -0
  92. package/dist/shell-shard/verbs/scope-parse.d.ts +7 -0
  93. package/dist/shell-shard/verbs/scope-parse.js +33 -0
  94. package/dist/shell-shard/verbs/scope-parse.test.d.ts +1 -0
  95. package/dist/shell-shard/verbs/scope-parse.test.js +76 -0
  96. package/dist/shell-shard/verbs/xfer.d.ts +2 -0
  97. package/dist/shell-shard/verbs/xfer.js +87 -0
  98. package/dist/shell-shard/verbs/xfer.test.d.ts +1 -0
  99. package/dist/shell-shard/verbs/xfer.test.js +107 -0
  100. package/dist/verbs/types.d.ts +18 -0
  101. package/dist/version.d.ts +1 -1
  102. package/dist/version.js +1 -1
  103. package/package.json +1 -1
@@ -1,13 +1,13 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
2
  import { MemoryDocumentBackend } from '../documents/backends';
3
- import { __setDocumentBackend, __setTenantId } from '../documents/config';
3
+ import { __setDocumentBackend, __setActiveScope } from '../documents/config';
4
4
  import { registerShard, activateShard, deactivateShard, __resetShardRegistryForTest } from './activate.svelte';
5
5
  import { emit } from '../keys/revocation-bus.svelte';
6
6
  describe('onKeyRevoked hook wiring', () => {
7
7
  beforeEach(() => {
8
8
  __resetShardRegistryForTest();
9
9
  __setDocumentBackend(new MemoryDocumentBackend());
10
- __setTenantId('tenant-a');
10
+ __setActiveScope('tenant-a');
11
11
  });
12
12
  it('fires onKeyRevoked when the bus emits for the shard', async () => {
13
13
  const received = [];
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, beforeEach, vi } from 'vitest';
2
2
  import { MemoryDocumentBackend } from '../documents/backends';
3
- import { __setDocumentBackend, __setTenantId } from '../documents/config';
3
+ import { __setDocumentBackend, __setActiveScope } from '../documents/config';
4
4
  import { registerShard, activateShard, __resetShardRegistryForTest, } from './activate.svelte';
5
5
  import { __resetViewRegistryForTest } from './registry';
6
6
  function programmaticVerb(name, summary, body) {
@@ -22,7 +22,7 @@ describe('ctx.listVerbs / ctx.runVerb (integration)', () => {
22
22
  __resetShardRegistryForTest();
23
23
  __resetViewRegistryForTest();
24
24
  __setDocumentBackend(new MemoryDocumentBackend());
25
- __setTenantId('tenant-test');
25
+ __setActiveScope('tenant-test');
26
26
  });
27
27
  it('listVerbs returns every verb across active shards with shardId', async () => {
28
28
  registerShard({
@@ -19,7 +19,7 @@
19
19
  import { sh3 } from '../sh3Runtime.svelte';
20
20
  import { registerView, unregisterView, registerVerb as fwRegisterVerb, unregisterVerb as fwUnregisterVerb } from './registry';
21
21
  import { makeSh3Api } from '../sh3Api/headless';
22
- import { createDocumentHandle, getTenantId, getDocumentBackend } from '../documents';
22
+ import { createDocumentHandle, getDocumentBackend, getActiveScopeId } from '../documents';
23
23
  import { fetchEnvState, putEnvState } from '../env/client';
24
24
  import { getEnvServerUrl } from '../env/index';
25
25
  import { apiFetch } from '../transport/apiFetch';
@@ -153,7 +153,7 @@ export async function activateShard(id, opts) {
153
153
  };
154
154
  const hasBrowse = (_a = shard.manifest.permissions) === null || _a === void 0 ? void 0 : _a.includes(PERMISSION_DOCUMENTS_BROWSE);
155
155
  const browseCap = hasBrowse
156
- ? createBrowseCapability(() => getTenantId(), getDocumentBackend(), {
156
+ ? createBrowseCapability(getActiveScopeId, getDocumentBackend(), {
157
157
  canRead: (_c = (_b = shard.manifest.permissions) === null || _b === void 0 ? void 0 : _b.includes(PERMISSION_DOCUMENTS_READ)) !== null && _c !== void 0 ? _c : false,
158
158
  canWrite: (_e = (_d = shard.manifest.permissions) === null || _d === void 0 ? void 0 : _d.includes(PERMISSION_DOCUMENTS_WRITE)) !== null && _e !== void 0 ? _e : false,
159
159
  })
@@ -179,7 +179,7 @@ export async function activateShard(id, opts) {
179
179
  }
180
180
  },
181
181
  documents: (options) => {
182
- const handle = createDocumentHandle(getTenantId(), id, getDocumentBackend(), options);
182
+ const handle = createDocumentHandle(getActiveScopeId(), id, getDocumentBackend(), options);
183
183
  entry.cleanupFns.push(() => handle.dispose());
184
184
  return handle;
185
185
  },
@@ -225,7 +225,7 @@ export async function activateShard(id, opts) {
225
225
  return checkIsAdmin();
226
226
  },
227
227
  get tenantId() {
228
- return getTenantId();
228
+ return getActiveScopeId();
229
229
  },
230
230
  getScope: () => { var _a; return (_a = scopeResolver === null || scopeResolver === void 0 ? void 0 : scopeResolver()) !== null && _a !== void 0 ? _a : 'tenant'; },
231
231
  zones: ((_f = shard.manifest.permissions) === null || _f === void 0 ? void 0 : _f.includes(PERMISSION_STATE_MANAGE))
@@ -235,7 +235,7 @@ export async function activateShard(id, opts) {
235
235
  documentPicker: browseCap
236
236
  ? createDocumentPicker(() => browseCap.listDocuments())
237
237
  : createDocumentPicker(async () => {
238
- const docs = await getDocumentBackend().list(getTenantId(), id);
238
+ const docs = await getDocumentBackend().list(getActiveScopeId(), id);
239
239
  return docs.map(d => (Object.assign(Object.assign({}, d), { shardId: id })));
240
240
  }),
241
241
  keys: ((_g = shard.manifest.permissions) === null || _g === void 0 ? void 0 : _g.includes(PERMISSION_KEYS_MINT))
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
2
2
  import { MemoryDocumentBackend } from '../documents/backends';
3
- import { __setDocumentBackend, __setTenantId } from '../documents/config';
3
+ import { __setDocumentBackend, __setActiveScope } from '../documents/config';
4
4
  import { __setEnvServerUrl } from '../env/index';
5
5
  import { registerShard, activateShard, __resetShardRegistryForTest, } from './activate.svelte';
6
6
  import { __resetViewRegistryForTest } from './registry';
@@ -11,7 +11,7 @@ describe('ctx.fetch', () => {
11
11
  __resetShardRegistryForTest();
12
12
  __resetViewRegistryForTest();
13
13
  __setDocumentBackend(new MemoryDocumentBackend());
14
- __setTenantId('tenant-test');
14
+ __setActiveScope('tenant-test');
15
15
  __setEnvServerUrl('https://example.com');
16
16
  });
17
17
  afterEach(() => {
@@ -69,7 +69,7 @@ describe('ctx.serverUrl', () => {
69
69
  __resetShardRegistryForTest();
70
70
  __resetViewRegistryForTest();
71
71
  __setDocumentBackend(new MemoryDocumentBackend());
72
- __setTenantId('tenant-test');
72
+ __setActiveScope('tenant-test');
73
73
  __setEnvServerUrl('https://example.com');
74
74
  });
75
75
  afterEach(() => {
@@ -100,7 +100,7 @@ describe('ctx.resolveUrl', () => {
100
100
  __resetShardRegistryForTest();
101
101
  __resetViewRegistryForTest();
102
102
  __setDocumentBackend(new MemoryDocumentBackend());
103
- __setTenantId('tenant-test');
103
+ __setActiveScope('tenant-test');
104
104
  __setEnvServerUrl('https://example.com');
105
105
  });
106
106
  afterEach(() => {
@@ -28,6 +28,7 @@
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';
31
32
 
32
33
  interface Props {
33
34
  shell: Sh3Api;
@@ -35,8 +36,9 @@
35
36
  userId: string;
36
37
  role: ShellRole;
37
38
  contributions: ContributionsApi;
39
+ docs?: BrowseCapability;
38
40
  }
39
- let { shell, wsUrl, userId, role, contributions }: Props = $props();
41
+ let { shell, wsUrl, userId, role, contributions, docs }: Props = $props();
40
42
 
41
43
  // Per-mode buffer map. Each ModeBuffer bundles a Scrollback + history +
42
44
  // locked flag and is materialized lazily on first switch into that mode.
@@ -250,6 +252,7 @@
250
252
  session,
251
253
  sh3: shellWithModes,
252
254
  fs,
255
+ docs,
253
256
  cwd: () => session.cwd,
254
257
  busy: acquireBusy,
255
258
  customMode: (id: string) => contributedModes.find((d) => d.id === id) ?? null,
@@ -1,12 +1,14 @@
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';
4
5
  interface Props {
5
6
  shell: Sh3Api;
6
7
  wsUrl: string;
7
8
  userId: string;
8
9
  role: ShellRole;
9
10
  contributions: ContributionsApi;
11
+ docs?: BrowseCapability;
10
12
  }
11
13
  declare const Terminal: import("svelte").Component<Props, {}, "">;
12
14
  type Terminal = ReturnType<typeof Terminal>;
@@ -4,6 +4,7 @@ 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';
7
8
  export interface DispatchDeps {
8
9
  mode: () => ShellMode;
9
10
  /** Current shell role — used by invoke() role-gating. */
@@ -18,6 +19,7 @@ export interface DispatchDeps {
18
19
  session: SessionClient;
19
20
  sh3: Sh3Api;
20
21
  fs: TenantFsClient;
22
+ docs?: BrowseCapability;
21
23
  cwd: () => string;
22
24
  /**
23
25
  * Acquire a busy indicator. Returns a clear handle. Calling clear()
@@ -39,6 +39,7 @@ export function makeDispatch(deps) {
39
39
  cwd: deps.cwd(),
40
40
  dispatch,
41
41
  fs: deps.fs,
42
+ docs: deps.docs,
42
43
  }, resolution.args);
43
44
  return;
44
45
  }
@@ -107,6 +108,7 @@ export function makeDispatch(deps) {
107
108
  cwd: deps.cwd(),
108
109
  dispatch,
109
110
  fs: deps.fs,
111
+ docs: deps.docs,
110
112
  }, resolution.args);
111
113
  }
112
114
  catch (err) {
@@ -1,5 +1,6 @@
1
1
  import { VERSION } from '../version';
2
2
  import { PERMISSION_STATE_MANAGE } from '../state/types';
3
+ import { PERMISSION_DOCUMENTS_BROWSE, PERMISSION_DOCUMENTS_READ, PERMISSION_DOCUMENTS_WRITE, } from '../documents/types';
3
4
  export const manifest = {
4
5
  id: 'shell',
5
6
  label: 'Sh3',
@@ -9,5 +10,10 @@ export const manifest = {
9
10
  // and is statically mounted at sh3-server boot. The existing contract in
10
11
  // sh3-core/src/shards/types.ts documents that framework-shipped shards do
11
12
  // not use this field.
12
- permissions: [PERMISSION_STATE_MANAGE],
13
+ permissions: [
14
+ PERMISSION_STATE_MANAGE,
15
+ PERMISSION_DOCUMENTS_BROWSE,
16
+ PERMISSION_DOCUMENTS_READ,
17
+ PERMISSION_DOCUMENTS_WRITE,
18
+ ],
13
19
  };
@@ -64,7 +64,7 @@ export const shellShard = {
64
64
  const role = isAdmin() ? 'admin' : 'user';
65
65
  const instance = mount(Terminal, {
66
66
  target: container,
67
- props: { shell, wsUrl, userId, role, contributions: ctx.contributions },
67
+ props: { shell, wsUrl, userId, role, contributions: ctx.contributions, docs: ctx.browse },
68
68
  });
69
69
  return {
70
70
  unmount() {
@@ -0,0 +1,2 @@
1
+ import type { Verb } from '../../verbs/types';
2
+ export declare const catVerb: Verb;
@@ -0,0 +1,35 @@
1
+ import { parseScopePath } from './scope-parse';
2
+ export const catVerb = {
3
+ name: 'cat',
4
+ summary: 'Print document content. Usage: cat <shardId>/<path>',
5
+ programmatic: true,
6
+ async run(ctx, args) {
7
+ const ts = Date.now();
8
+ if (!ctx.docs) {
9
+ ctx.scrollback.push({ kind: 'status', text: 'cat: document capability not available', level: 'error', ts });
10
+ return;
11
+ }
12
+ if (!args[0]) {
13
+ ctx.scrollback.push({ kind: 'status', text: 'usage: cat <shardId>/<path>', level: 'error', ts });
14
+ return;
15
+ }
16
+ const parsed = parseScopePath(args[0]);
17
+ if (!parsed || !parsed.path) {
18
+ ctx.scrollback.push({ kind: 'status', text: `cat: invalid path '${args[0]}'`, level: 'error', ts });
19
+ return;
20
+ }
21
+ if (!ctx.docs.readFrom) {
22
+ ctx.scrollback.push({ kind: 'status', text: 'cat: read permission not granted', level: 'error', ts });
23
+ return;
24
+ }
25
+ const content = await ctx.docs.readFrom(parsed.shardId, parsed.path);
26
+ if (content === null) {
27
+ ctx.scrollback.push({ kind: 'status', text: `cat: not found: ${args[0]}`, level: 'error', ts });
28
+ return;
29
+ }
30
+ const text = content instanceof ArrayBuffer
31
+ ? `<binary ${content.byteLength}b>`
32
+ : content;
33
+ ctx.scrollback.push({ kind: 'text', stream: 'stdout', chunks: [text], ts });
34
+ },
35
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,49 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { catVerb } from './cat';
3
+ function makeDocs(overrides = {}) {
4
+ return Object.assign({ listDocuments: vi.fn(async () => []), listShards: vi.fn(async () => []), watchDocuments: vi.fn(() => () => { }), readFrom: vi.fn(async () => null) }, overrides);
5
+ }
6
+ function makeCtx(docs) {
7
+ const pushed = [];
8
+ const ctx = {
9
+ sh3: {},
10
+ scrollback: { push: (e) => pushed.push(e) },
11
+ session: {},
12
+ cwd: '/',
13
+ fs: {},
14
+ docs,
15
+ dispatch: async () => { },
16
+ };
17
+ return { ctx, pushed };
18
+ }
19
+ describe('cat verb', () => {
20
+ it('emits error when docs capability missing', async () => {
21
+ const { ctx, pushed } = makeCtx(undefined);
22
+ await catVerb.run(ctx, ['notes/draft.md']);
23
+ const err = pushed.find((e) => e.kind === 'status' && e.level === 'error');
24
+ expect(err).toBeDefined();
25
+ });
26
+ it('emits usage error when no args', async () => {
27
+ const { ctx, pushed } = makeCtx(makeDocs());
28
+ await catVerb.run(ctx, []);
29
+ const err = pushed.find((e) => e.kind === 'status' && e.level === 'error');
30
+ expect(err).toBeDefined();
31
+ expect(err.text).toMatch(/usage/i);
32
+ });
33
+ it('outputs document content', async () => {
34
+ const docs = makeDocs({ readFrom: vi.fn(async () => 'hello world') });
35
+ const { ctx, pushed } = makeCtx(docs);
36
+ await catVerb.run(ctx, ['notes/draft.md']);
37
+ expect(docs.readFrom).toHaveBeenCalledWith('notes', 'draft.md');
38
+ const text = pushed.find((e) => e.kind === 'text');
39
+ expect(text === null || text === void 0 ? void 0 : text.chunks.join('')).toContain('hello world');
40
+ });
41
+ it('emits error when document not found', async () => {
42
+ const docs = makeDocs({ readFrom: vi.fn(async () => null) });
43
+ const { ctx, pushed } = makeCtx(docs);
44
+ await catVerb.run(ctx, ['notes/missing.md']);
45
+ const err = pushed.find((e) => e.kind === 'status' && e.level === 'error');
46
+ expect(err).toBeDefined();
47
+ expect(err.text).toMatch(/not found/i);
48
+ });
49
+ });
@@ -12,6 +12,12 @@ import { viewsVerb, openVerb, closeVerb, popoutVerb, dockVerb } from './views';
12
12
  import { zonesVerb, zoneVerb } from './zones';
13
13
  import { envVerb } from './env';
14
14
  import { resetVerb } from './reset';
15
+ import { lsVerb } from './ls';
16
+ import { catVerb } from './cat';
17
+ import { rmVerb } from './rm';
18
+ import { mkdirVerb } from './mkdir';
19
+ import { mvVerb } from './mv';
20
+ import { xferVerb } from './xfer';
15
21
  export function registerV1Verbs(ctx) {
16
22
  ctx.registerVerb(makeHelpVerb());
17
23
  ctx.registerVerb(clearVerb);
@@ -29,4 +35,10 @@ export function registerV1Verbs(ctx) {
29
35
  ctx.registerVerb(zoneVerb);
30
36
  ctx.registerVerb(envVerb);
31
37
  ctx.registerVerb(resetVerb);
38
+ ctx.registerVerb(lsVerb);
39
+ ctx.registerVerb(catVerb);
40
+ ctx.registerVerb(rmVerb);
41
+ ctx.registerVerb(mkdirVerb);
42
+ ctx.registerVerb(mvVerb);
43
+ ctx.registerVerb(xferVerb);
32
44
  }
@@ -0,0 +1,2 @@
1
+ import type { Verb } from '../../verbs/types';
2
+ export declare const lsVerb: Verb;
@@ -0,0 +1,48 @@
1
+ import { parseScopePath } from './scope-parse';
2
+ export const lsVerb = {
3
+ name: 'ls',
4
+ summary: 'List documents. Usage: ls | ls <shardId> | ls <shardId>/<prefix>',
5
+ programmatic: true,
6
+ async run(ctx, args) {
7
+ var _a, _b, _c;
8
+ const ts = Date.now();
9
+ if (!ctx.docs) {
10
+ ctx.scrollback.push({ kind: 'status', text: 'ls: document capability not available', level: 'error', ts });
11
+ return;
12
+ }
13
+ const all = await ctx.docs.listDocuments();
14
+ const arg = args[0];
15
+ if (!arg) {
16
+ // Group by shard, emit counts
17
+ const counts = new Map();
18
+ for (const doc of all) {
19
+ counts.set(doc.shardId, ((_a = counts.get(doc.shardId)) !== null && _a !== void 0 ? _a : 0) + 1);
20
+ }
21
+ if (counts.size === 0) {
22
+ ctx.scrollback.push({ kind: 'status', text: 'ls: no documents in active scope', level: 'info', ts });
23
+ return;
24
+ }
25
+ const lines = [...counts.entries()]
26
+ .sort(([a], [b]) => a.localeCompare(b))
27
+ .map(([shard, count]) => ` ${shard.padEnd(24)} ${count} doc${count !== 1 ? 's' : ''}`);
28
+ ctx.scrollback.push({ kind: 'text', stream: 'stdout', chunks: [lines.join('\n') + '\n'], ts });
29
+ return;
30
+ }
31
+ const parsed = parseScopePath(arg);
32
+ const shardId = (_b = parsed === null || parsed === void 0 ? void 0 : parsed.shardId) !== null && _b !== void 0 ? _b : arg;
33
+ const prefix = (_c = parsed === null || parsed === void 0 ? void 0 : parsed.path) !== null && _c !== void 0 ? _c : '';
34
+ const docs = all.filter((d) => {
35
+ if (d.shardId !== shardId)
36
+ return false;
37
+ return prefix ? d.path.startsWith(prefix) : true;
38
+ });
39
+ if (docs.length === 0) {
40
+ ctx.scrollback.push({ kind: 'status', text: `ls: no documents in ${arg}`, level: 'info', ts });
41
+ return;
42
+ }
43
+ const lines = docs
44
+ .sort((a, b) => a.path.localeCompare(b.path))
45
+ .map((d) => ` ${d.path}`);
46
+ ctx.scrollback.push({ kind: 'text', stream: 'stdout', chunks: [lines.join('\n') + '\n'], ts });
47
+ },
48
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,64 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { lsVerb } from './ls';
3
+ function makeDocs(overrides = {}) {
4
+ return Object.assign({ listDocuments: vi.fn(async () => []), listShards: vi.fn(async () => []), watchDocuments: vi.fn(() => () => { }) }, overrides);
5
+ }
6
+ function makeCtx(docs) {
7
+ const pushed = [];
8
+ const ctx = {
9
+ sh3: {},
10
+ scrollback: { push: (e) => pushed.push(e) },
11
+ session: {},
12
+ cwd: '/',
13
+ fs: {},
14
+ docs,
15
+ dispatch: async () => { },
16
+ };
17
+ return { ctx, pushed };
18
+ }
19
+ const sampleDocs = [
20
+ { shardId: 'notes', path: 'draft.md', size: 100, lastModified: 0 },
21
+ { shardId: 'notes', path: 'ideas/spark.md', size: 50, lastModified: 0 },
22
+ { shardId: 'design', path: 'spec.guml', size: 200, lastModified: 0 },
23
+ ];
24
+ describe('ls verb', () => {
25
+ it('emits error when docs capability missing', async () => {
26
+ const { ctx, pushed } = makeCtx(undefined);
27
+ await lsVerb.run(ctx, []);
28
+ const err = pushed.find((e) => e.kind === 'status' && e.level === 'error');
29
+ expect(err).toBeDefined();
30
+ });
31
+ it('lists shards with doc counts when no args', async () => {
32
+ const docs = makeDocs({ listDocuments: vi.fn(async () => sampleDocs) });
33
+ const { ctx, pushed } = makeCtx(docs);
34
+ await lsVerb.run(ctx, []);
35
+ const text = pushed.find((e) => e.kind === 'text');
36
+ expect(text === null || text === void 0 ? void 0 : text.chunks.join('')).toMatch(/notes/);
37
+ expect(text === null || text === void 0 ? void 0 : text.chunks.join('')).toMatch(/design/);
38
+ expect(text === null || text === void 0 ? void 0 : text.chunks.join('')).toMatch(/2/); // notes has 2 docs
39
+ });
40
+ it('lists docs in a specific shard', async () => {
41
+ const docs = makeDocs({ listDocuments: vi.fn(async () => sampleDocs) });
42
+ const { ctx, pushed } = makeCtx(docs);
43
+ await lsVerb.run(ctx, ['notes']);
44
+ const text = pushed.find((e) => e.kind === 'text');
45
+ expect(text === null || text === void 0 ? void 0 : text.chunks.join('')).toMatch(/draft\.md/);
46
+ expect(text === null || text === void 0 ? void 0 : text.chunks.join('')).toMatch(/ideas\/spark\.md/);
47
+ expect(text === null || text === void 0 ? void 0 : text.chunks.join('')).not.toMatch(/spec\.guml/);
48
+ });
49
+ it('filters docs by prefix when path given', async () => {
50
+ const docs = makeDocs({ listDocuments: vi.fn(async () => sampleDocs) });
51
+ const { ctx, pushed } = makeCtx(docs);
52
+ await lsVerb.run(ctx, ['notes/ideas']);
53
+ const text = pushed.find((e) => e.kind === 'text');
54
+ expect(text === null || text === void 0 ? void 0 : text.chunks.join('')).toMatch(/spark\.md/);
55
+ expect(text === null || text === void 0 ? void 0 : text.chunks.join('')).not.toMatch(/draft\.md/);
56
+ });
57
+ it('emits a status when shard has no docs', async () => {
58
+ const docs = makeDocs({ listDocuments: vi.fn(async () => sampleDocs) });
59
+ const { ctx, pushed } = makeCtx(docs);
60
+ await lsVerb.run(ctx, ['empty-shard']);
61
+ const status = pushed.find((e) => e.kind === 'status' && e.level === 'info');
62
+ expect(status).toBeDefined();
63
+ });
64
+ });
@@ -0,0 +1,2 @@
1
+ import type { Verb } from '../../verbs/types';
2
+ export declare const mkdirVerb: Verb;
@@ -0,0 +1,30 @@
1
+ import { parseScopePath } from './scope-parse';
2
+ export const mkdirVerb = {
3
+ name: 'mkdir',
4
+ summary: 'Create a folder. Usage: mkdir <shardId>/<folder>',
5
+ programmatic: true,
6
+ async run(ctx, args) {
7
+ const ts = Date.now();
8
+ if (!ctx.docs) {
9
+ ctx.scrollback.push({ kind: 'status', text: 'mkdir: document capability not available', level: 'error', ts });
10
+ return;
11
+ }
12
+ if (!args[0]) {
13
+ ctx.scrollback.push({ kind: 'status', text: 'usage: mkdir <shardId>/<folder>', level: 'error', ts });
14
+ return;
15
+ }
16
+ if (!ctx.docs.writeTo) {
17
+ ctx.scrollback.push({ kind: 'status', text: 'mkdir: write permission not granted', level: 'error', ts });
18
+ return;
19
+ }
20
+ const parsed = parseScopePath(args[0]);
21
+ if (!parsed || !parsed.path) {
22
+ ctx.scrollback.push({ kind: 'status', text: `mkdir: invalid path '${args[0]}'`, level: 'error', ts });
23
+ return;
24
+ }
25
+ // Materialise the folder with a sentinel file; backends treat paths as flat.
26
+ const keepPath = parsed.path.replace(/\/$/, '') + '/.keep';
27
+ await ctx.docs.writeTo(parsed.shardId, keepPath, '');
28
+ ctx.scrollback.push({ kind: 'status', text: `mkdir: created ${args[0]}`, level: 'info', ts });
29
+ },
30
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,48 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { mkdirVerb } from './mkdir';
3
+ function makeDocs(overrides = {}) {
4
+ return Object.assign({ listDocuments: vi.fn(async () => []), listShards: vi.fn(async () => []), watchDocuments: vi.fn(() => () => { }), writeTo: vi.fn(async () => { }) }, overrides);
5
+ }
6
+ function makeCtx(docs) {
7
+ const pushed = [];
8
+ const ctx = {
9
+ sh3: {},
10
+ scrollback: { push: (e) => pushed.push(e) },
11
+ session: {},
12
+ cwd: '/',
13
+ fs: {},
14
+ docs,
15
+ dispatch: async () => { },
16
+ };
17
+ return { ctx, pushed };
18
+ }
19
+ describe('mkdir verb', () => {
20
+ it('emits error when docs capability missing', async () => {
21
+ const { ctx, pushed } = makeCtx(undefined);
22
+ await mkdirVerb.run(ctx, ['notes/ideas']);
23
+ const err = pushed.find((e) => e.kind === 'status' && e.level === 'error');
24
+ expect(err).toBeDefined();
25
+ });
26
+ it('emits usage error when no args', async () => {
27
+ const { ctx, pushed } = makeCtx(makeDocs());
28
+ await mkdirVerb.run(ctx, []);
29
+ const err = pushed.find((e) => e.kind === 'status' && e.level === 'error');
30
+ expect(err.text).toMatch(/usage/i);
31
+ });
32
+ it('creates a folder via writeTo', async () => {
33
+ const writeTo = vi.fn(async () => { });
34
+ const docs = makeDocs({ writeTo });
35
+ const { ctx, pushed } = makeCtx(docs);
36
+ await mkdirVerb.run(ctx, ['notes/ideas']);
37
+ expect(writeTo).toHaveBeenCalledWith('notes', 'ideas/.keep', '');
38
+ const ok = pushed.find((e) => e.kind === 'status' && e.level === 'info');
39
+ expect(ok).toBeDefined();
40
+ });
41
+ it('emits error when writeTo not available', async () => {
42
+ const docs = makeDocs({ writeTo: undefined });
43
+ const { ctx, pushed } = makeCtx(docs);
44
+ await mkdirVerb.run(ctx, ['notes/ideas']);
45
+ const err = pushed.find((e) => e.kind === 'status' && e.level === 'error');
46
+ expect(err).toBeDefined();
47
+ });
48
+ });
@@ -0,0 +1,2 @@
1
+ import type { Verb } from '../../verbs/types';
2
+ export declare const mvVerb: Verb;
@@ -0,0 +1,33 @@
1
+ import { parseScopePath } from './scope-parse';
2
+ export const mvVerb = {
3
+ name: 'mv',
4
+ summary: 'Rename a document within a shard. Usage: mv <shardId>/<old> <shardId>/<new>',
5
+ programmatic: true,
6
+ async run(ctx, args) {
7
+ const ts = Date.now();
8
+ if (!ctx.docs) {
9
+ ctx.scrollback.push({ kind: 'status', text: 'mv: document capability not available', level: 'error', ts });
10
+ return;
11
+ }
12
+ if (args.length < 2) {
13
+ ctx.scrollback.push({ kind: 'status', text: 'usage: mv <shardId>/<old> <shardId>/<new>', level: 'error', ts });
14
+ return;
15
+ }
16
+ if (!ctx.docs.renameFrom) {
17
+ ctx.scrollback.push({ kind: 'status', text: 'mv: write permission not granted', level: 'error', ts });
18
+ return;
19
+ }
20
+ const src = parseScopePath(args[0]);
21
+ const dst = parseScopePath(args[1]);
22
+ if (!(src === null || src === void 0 ? void 0 : src.path) || !(dst === null || dst === void 0 ? void 0 : dst.path)) {
23
+ ctx.scrollback.push({ kind: 'status', text: 'mv: invalid path', level: 'error', ts });
24
+ return;
25
+ }
26
+ if (src.shardId !== dst.shardId) {
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
+ return;
29
+ }
30
+ await ctx.docs.renameFrom(src.shardId, src.path, dst.path);
31
+ ctx.scrollback.push({ kind: 'status', text: `mv: renamed ${args[0]} → ${args[1]}`, level: 'info', ts });
32
+ },
33
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,55 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { mvVerb } from './mv';
3
+ function makeDocs(overrides = {}) {
4
+ return Object.assign({ listDocuments: vi.fn(async () => []), listShards: vi.fn(async () => []), watchDocuments: vi.fn(() => () => { }), renameFrom: vi.fn(async () => { }) }, overrides);
5
+ }
6
+ function makeCtx(docs) {
7
+ const pushed = [];
8
+ const ctx = {
9
+ sh3: {},
10
+ scrollback: { push: (e) => pushed.push(e) },
11
+ session: {},
12
+ cwd: '/',
13
+ fs: {},
14
+ docs,
15
+ dispatch: async () => { },
16
+ };
17
+ return { ctx, pushed };
18
+ }
19
+ describe('mv verb', () => {
20
+ it('emits error when docs capability missing', async () => {
21
+ const { ctx, pushed } = makeCtx(undefined);
22
+ await mvVerb.run(ctx, ['notes/old.md', 'notes/new.md']);
23
+ const err = pushed.find((e) => e.kind === 'status' && e.level === 'error');
24
+ expect(err).toBeDefined();
25
+ });
26
+ it('emits usage error when fewer than two args', async () => {
27
+ const { ctx, pushed } = makeCtx(makeDocs());
28
+ await mvVerb.run(ctx, ['notes/old.md']);
29
+ const err = pushed.find((e) => e.kind === 'status' && e.level === 'error');
30
+ expect(err.text).toMatch(/usage/i);
31
+ });
32
+ it('renames a document within the same shard', async () => {
33
+ const renameFrom = vi.fn(async () => { });
34
+ const docs = makeDocs({ renameFrom });
35
+ const { ctx, pushed } = makeCtx(docs);
36
+ await mvVerb.run(ctx, ['notes/old.md', 'notes/new.md']);
37
+ expect(renameFrom).toHaveBeenCalledWith('notes', 'old.md', 'new.md');
38
+ const ok = pushed.find((e) => e.kind === 'status' && e.level === 'info');
39
+ expect(ok).toBeDefined();
40
+ });
41
+ it('emits error when src and dst are in different shards', async () => {
42
+ const { ctx, pushed } = makeCtx(makeDocs());
43
+ await mvVerb.run(ctx, ['notes/old.md', 'design/new.md']);
44
+ const err = pushed.find((e) => e.kind === 'status' && e.level === 'error');
45
+ expect(err).toBeDefined();
46
+ expect(err.text).toMatch(/same shard/i);
47
+ });
48
+ it('emits error when renameFrom not available', async () => {
49
+ const docs = makeDocs({ renameFrom: undefined });
50
+ const { ctx, pushed } = makeCtx(docs);
51
+ await mvVerb.run(ctx, ['notes/old.md', 'notes/new.md']);
52
+ const err = pushed.find((e) => e.kind === 'status' && e.level === 'error');
53
+ expect(err).toBeDefined();
54
+ });
55
+ });
@@ -0,0 +1,2 @@
1
+ import type { Verb } from '../../verbs/types';
2
+ export declare const rmVerb: Verb;