sh3-core 0.19.6 → 0.20.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 (123) hide show
  1. package/dist/app/admin/AuthSettingsView.svelte +3 -9
  2. package/dist/app/admin/MountsView.svelte +276 -0
  3. package/dist/app/admin/MountsView.svelte.d.ts +3 -0
  4. package/dist/app/admin/SystemView.svelte +6 -6
  5. package/dist/app/admin/UsersView.svelte +103 -7
  6. package/dist/app/admin/adminApp.js +1 -0
  7. package/dist/app/admin/adminShard.svelte.js +10 -0
  8. package/dist/apps/lifecycle.js +1 -0
  9. package/dist/apps/types.d.ts +7 -0
  10. package/dist/assets/iconIds.generated.d.ts +1 -1
  11. package/dist/assets/iconIds.generated.js +1 -0
  12. package/dist/assets/icons.svg +5 -0
  13. package/dist/auth/admin-users.svelte.js +2 -1
  14. package/dist/auth/auth.svelte.d.ts +4 -5
  15. package/dist/auth/auth.svelte.js +5 -6
  16. package/dist/auth/types.d.ts +0 -2
  17. package/dist/chrome/CompactChrome.svelte +25 -6
  18. package/dist/chrome/FloatsSheet.svelte +7 -32
  19. package/dist/chrome/FloatsSheet.svelte.d.ts +1 -2
  20. package/dist/chrome/FloatsSheet.svelte.test.js +8 -14
  21. package/dist/chrome/MenuSheet.svelte +154 -148
  22. package/dist/chrome/MenuSheet.svelte.d.ts +1 -2
  23. package/dist/chrome/MenuSheet.svelte.test.js +24 -12
  24. package/dist/createShell.js +32 -21
  25. package/dist/createShell.remoteAuth.test.js +9 -3
  26. package/dist/documents/backends.d.ts +12 -0
  27. package/dist/documents/backends.js +230 -3
  28. package/dist/documents/backends.test.js +147 -1
  29. package/dist/documents/browse.d.ts +18 -1
  30. package/dist/documents/browse.js +40 -7
  31. package/dist/documents/browse.test.js +35 -35
  32. package/dist/documents/config.d.ts +6 -0
  33. package/dist/documents/config.js +18 -1
  34. package/dist/documents/handle.js +65 -17
  35. package/dist/documents/handle.test.js +88 -1
  36. package/dist/documents/http-backend.d.ts +6 -0
  37. package/dist/documents/http-backend.js +71 -2
  38. package/dist/documents/http-backend.test.js +51 -1
  39. package/dist/documents/index.d.ts +2 -2
  40. package/dist/documents/index.js +1 -1
  41. package/dist/documents/picker-api.d.ts +4 -2
  42. package/dist/documents/picker-api.test.d.ts +1 -1
  43. package/dist/documents/picker-api.test.js +89 -59
  44. package/dist/documents/picker-primitive.d.ts +4 -0
  45. package/dist/documents/picker-primitive.js +27 -29
  46. package/dist/documents/types.d.ts +93 -19
  47. package/dist/documents/types.js +6 -0
  48. package/dist/layout/presets.test.js +4 -4
  49. package/dist/layout/types.d.ts +1 -1
  50. package/dist/layouts-shard/LayoutsSection.svelte +3 -16
  51. package/dist/primitives/widgets/DocumentFilePicker.d.ts +6 -2
  52. package/dist/primitives/widgets/DocumentFilePicker.js +12 -5
  53. package/dist/primitives/widgets/DocumentFilePicker.svelte +27 -5
  54. package/dist/primitives/widgets/DocumentFilePicker.svelte.d.ts +14 -0
  55. package/dist/primitives/widgets/DocumentFilePicker.test.d.ts +1 -0
  56. package/dist/primitives/widgets/DocumentFilePicker.test.js +33 -0
  57. package/dist/primitives/widgets/DocumentOpener.svelte +20 -0
  58. package/dist/primitives/widgets/DocumentOpener.svelte.d.ts +14 -0
  59. package/dist/primitives/widgets/DocumentSaver.svelte +17 -0
  60. package/dist/primitives/widgets/DocumentSaver.svelte.d.ts +13 -0
  61. package/dist/primitives/widgets/PickerList.svelte +1 -0
  62. package/dist/primitives/widgets/_DocumentBrowser.svelte +419 -35
  63. package/dist/primitives/widgets/_DocumentBrowser.svelte.d.ts +12 -0
  64. package/dist/primitives/widgets/_DocumentBrowser.svelte.test.d.ts +1 -0
  65. package/dist/primitives/widgets/_DocumentBrowser.svelte.test.js +277 -0
  66. package/dist/primitives/widgets/_FolderConfirmDelete.svelte +57 -0
  67. package/dist/primitives/widgets/_FolderConfirmDelete.svelte.d.ts +12 -0
  68. package/dist/projects-shard/DeleteProjectDialog.svelte +32 -1
  69. package/dist/projects-shard/ProjectManage.svelte +197 -28
  70. package/dist/projects-shard/ProjectManage.svelte.test.d.ts +1 -0
  71. package/dist/projects-shard/ProjectManage.svelte.test.js +320 -0
  72. package/dist/projects-shard/ProjectsSection.svelte +3 -16
  73. package/dist/projects-shard/projectsApi.js +2 -1
  74. package/dist/registry/permission-descriptions.js +4 -0
  75. package/dist/server-shard/types.d.ts +21 -0
  76. package/dist/sh3Api/headless.js +10 -0
  77. package/dist/sh3core-shard/HomeSection.svelte +107 -0
  78. package/dist/sh3core-shard/HomeSection.svelte.d.ts +10 -0
  79. package/dist/sh3core-shard/Sh3Home.svelte +9 -23
  80. package/dist/shards/activate.svelte.d.ts +4 -0
  81. package/dist/shards/activate.svelte.js +11 -3
  82. package/dist/shards/types.d.ts +7 -0
  83. package/dist/shell-shard/Terminal.svelte +4 -1
  84. package/dist/shell-shard/Terminal.svelte.d.ts +2 -0
  85. package/dist/shell-shard/dispatch.d.ts +2 -0
  86. package/dist/shell-shard/dispatch.js +2 -0
  87. package/dist/shell-shard/manifest.js +7 -1
  88. package/dist/shell-shard/shellShard.svelte.js +1 -1
  89. package/dist/shell-shard/tenant-fs-client.js +2 -1
  90. package/dist/shell-shard/verbs/cat.d.ts +2 -0
  91. package/dist/shell-shard/verbs/cat.js +35 -0
  92. package/dist/shell-shard/verbs/cat.test.d.ts +1 -0
  93. package/dist/shell-shard/verbs/cat.test.js +49 -0
  94. package/dist/shell-shard/verbs/index.js +12 -0
  95. package/dist/shell-shard/verbs/ls.d.ts +2 -0
  96. package/dist/shell-shard/verbs/ls.js +48 -0
  97. package/dist/shell-shard/verbs/ls.test.d.ts +1 -0
  98. package/dist/shell-shard/verbs/ls.test.js +64 -0
  99. package/dist/shell-shard/verbs/mkdir.d.ts +2 -0
  100. package/dist/shell-shard/verbs/mkdir.js +30 -0
  101. package/dist/shell-shard/verbs/mkdir.test.d.ts +1 -0
  102. package/dist/shell-shard/verbs/mkdir.test.js +48 -0
  103. package/dist/shell-shard/verbs/mv.d.ts +2 -0
  104. package/dist/shell-shard/verbs/mv.js +33 -0
  105. package/dist/shell-shard/verbs/mv.test.d.ts +1 -0
  106. package/dist/shell-shard/verbs/mv.test.js +55 -0
  107. package/dist/shell-shard/verbs/rm.d.ts +2 -0
  108. package/dist/shell-shard/verbs/rm.js +28 -0
  109. package/dist/shell-shard/verbs/rm.test.d.ts +1 -0
  110. package/dist/shell-shard/verbs/rm.test.js +47 -0
  111. package/dist/shell-shard/verbs/scope-parse.d.ts +7 -0
  112. package/dist/shell-shard/verbs/scope-parse.js +33 -0
  113. package/dist/shell-shard/verbs/scope-parse.test.d.ts +1 -0
  114. package/dist/shell-shard/verbs/scope-parse.test.js +76 -0
  115. package/dist/shell-shard/verbs/xfer.d.ts +2 -0
  116. package/dist/shell-shard/verbs/xfer.js +101 -0
  117. package/dist/shell-shard/verbs/xfer.test.d.ts +1 -0
  118. package/dist/shell-shard/verbs/xfer.test.js +96 -0
  119. package/dist/transport/apiFetch.js +12 -5
  120. package/dist/verbs/types.d.ts +18 -0
  121. package/dist/version.d.ts +1 -1
  122. package/dist/version.js +1 -1
  123. package/package.json +1 -1
@@ -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, getTenantId, 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';
@@ -52,6 +52,13 @@ export const registeredShards = $state(new Map());
52
52
  const active = new Map();
53
53
  export const activeShards = $state(new Map());
54
54
  export const erroredShards = $state(new Map());
55
+ let scopeResolver = null;
56
+ /** Host-only. Register a callback that resolves whether the current scope
57
+ * is a personal tenant or an active project. Wired by createShell after
58
+ * bootstrap. Avoids circular dependencies with session-state. */
59
+ export function __setScopeResolver(resolver) {
60
+ scopeResolver = resolver;
61
+ }
55
62
  /**
56
63
  * Register (or re-register) a shard with the framework so it can later be
57
64
  * activated. Records the shard in `registeredShards` but does not run
@@ -146,7 +153,7 @@ export async function activateShard(id, opts) {
146
153
  };
147
154
  const hasBrowse = (_a = shard.manifest.permissions) === null || _a === void 0 ? void 0 : _a.includes(PERMISSION_DOCUMENTS_BROWSE);
148
155
  const browseCap = hasBrowse
149
- ? createBrowseCapability(getTenantId(), getDocumentBackend(), {
156
+ ? createBrowseCapability(() => getTenantId(), getDocumentBackend(), {
150
157
  canRead: (_c = (_b = shard.manifest.permissions) === null || _b === void 0 ? void 0 : _b.includes(PERMISSION_DOCUMENTS_READ)) !== null && _c !== void 0 ? _c : false,
151
158
  canWrite: (_e = (_d = shard.manifest.permissions) === null || _d === void 0 ? void 0 : _d.includes(PERMISSION_DOCUMENTS_WRITE)) !== null && _e !== void 0 ? _e : false,
152
159
  })
@@ -172,7 +179,7 @@ export async function activateShard(id, opts) {
172
179
  }
173
180
  },
174
181
  documents: (options) => {
175
- const handle = createDocumentHandle(getTenantId(), id, getDocumentBackend(), options);
182
+ const handle = createDocumentHandle(getActiveScopeId(), id, getDocumentBackend(), options);
176
183
  entry.cleanupFns.push(() => handle.dispose());
177
184
  return handle;
178
185
  },
@@ -220,6 +227,7 @@ export async function activateShard(id, opts) {
220
227
  get tenantId() {
221
228
  return getTenantId();
222
229
  },
230
+ getScope: () => { var _a; return (_a = scopeResolver === null || scopeResolver === void 0 ? void 0 : scopeResolver()) !== null && _a !== void 0 ? _a : 'tenant'; },
223
231
  zones: ((_f = shard.manifest.permissions) === null || _f === void 0 ? void 0 : _f.includes(PERMISSION_STATE_MANAGE))
224
232
  ? createZoneManager()
225
233
  : undefined,
@@ -274,6 +274,13 @@ export interface ShardContext {
274
274
  * serialize into persistent storage.
275
275
  */
276
276
  tenantId: string;
277
+ /**
278
+ * Whether this shard is running in a 'tenant' (personal) or 'project'
279
+ * scope. Shards that need to scope their behavior differently per context
280
+ * can read this at any time — it is reactive through the session state.
281
+ * Returns 'tenant' when no project is active, 'project' otherwise.
282
+ */
283
+ getScope(): 'tenant' | 'project';
277
284
  /**
278
285
  * Cross-shard zone management API. Only present when the shard's
279
286
  * manifest declares the `'state:manage'` permission. Check with
@@ -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() {
@@ -8,6 +8,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
8
8
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
9
9
  };
10
10
  var _TenantFsClient_instances, _TenantFsClient_get;
11
+ import { apiFetch } from '../transport/apiFetch';
11
12
  export class TenantFsClient {
12
13
  constructor(base = '') {
13
14
  _TenantFsClient_instances.add(this);
@@ -29,7 +30,7 @@ export class TenantFsClient {
29
30
  }
30
31
  _TenantFsClient_instances = new WeakSet(), _TenantFsClient_get = async function _TenantFsClient_get(route, path) {
31
32
  const url = `${this.base}${route}?path=${encodeURIComponent(path)}`;
32
- const res = await fetch(url, { credentials: 'include' });
33
+ const res = await apiFetch(url);
33
34
  if (!res.ok) {
34
35
  let msg = `HTTP ${res.status}`;
35
36
  try {
@@ -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;
@@ -0,0 +1,28 @@
1
+ import { parseScopePath } from './scope-parse';
2
+ export const rmVerb = {
3
+ name: 'rm',
4
+ summary: 'Delete a document. Usage: rm <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: 'rm: document capability not available', level: 'error', ts });
10
+ return;
11
+ }
12
+ if (!args[0]) {
13
+ ctx.scrollback.push({ kind: 'status', text: 'usage: rm <shardId>/<path>', level: 'error', ts });
14
+ return;
15
+ }
16
+ if (!ctx.docs.deleteFrom) {
17
+ ctx.scrollback.push({ kind: 'status', text: 'rm: 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: `rm: invalid path '${args[0]}'`, level: 'error', ts });
23
+ return;
24
+ }
25
+ await ctx.docs.deleteFrom(parsed.shardId, parsed.path);
26
+ ctx.scrollback.push({ kind: 'status', text: `rm: deleted ${args[0]}`, level: 'info', ts });
27
+ },
28
+ };
@@ -0,0 +1 @@
1
+ export {};