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.
- package/dist/BrandSlot.svelte +2 -2
- package/dist/actions/ctx-actions.svelte.test.js +2 -2
- package/dist/artifact.d.ts +2 -0
- package/dist/boot/satellitePayload.d.ts +2 -0
- package/dist/boot/satellitePayload.test.js +19 -0
- package/dist/build.d.ts +7 -1
- package/dist/build.js +22 -3
- package/dist/build.test.js +27 -1
- package/dist/createShell.js +34 -9
- package/dist/documents/backends.d.ts +12 -0
- package/dist/documents/backends.js +230 -3
- package/dist/documents/backends.test.js +147 -1
- package/dist/documents/browse.d.ts +20 -0
- package/dist/documents/browse.js +35 -0
- package/dist/documents/browse.test.js +125 -0
- package/dist/documents/config.d.ts +2 -4
- package/dist/documents/config.js +3 -7
- package/dist/documents/handle.js +40 -0
- package/dist/documents/handle.test.js +88 -1
- package/dist/documents/http-backend.d.ts +11 -0
- package/dist/documents/http-backend.js +86 -0
- package/dist/documents/http-backend.test.js +117 -1
- package/dist/documents/index.d.ts +1 -1
- package/dist/documents/index.js +1 -1
- package/dist/documents/picker-api.test.js +2 -2
- package/dist/documents/types.d.ts +87 -14
- package/dist/documents/types.js +4 -0
- package/dist/host-entry.d.ts +1 -1
- package/dist/host-entry.js +1 -1
- package/dist/host.d.ts +1 -1
- package/dist/host.js +1 -1
- package/dist/layout/slotHostPool.svelte.js +2 -2
- package/dist/overlays/FloatFrame.svelte +1 -0
- package/dist/primitives/widgets/DocumentFilePicker.d.ts +6 -2
- package/dist/primitives/widgets/DocumentFilePicker.js +12 -5
- package/dist/primitives/widgets/DocumentFilePicker.svelte +23 -1
- package/dist/primitives/widgets/DocumentFilePicker.svelte.d.ts +14 -0
- package/dist/primitives/widgets/DocumentFilePicker.test.d.ts +1 -0
- package/dist/primitives/widgets/DocumentFilePicker.test.js +33 -0
- package/dist/primitives/widgets/DocumentOpener.svelte +20 -0
- package/dist/primitives/widgets/DocumentOpener.svelte.d.ts +14 -0
- package/dist/primitives/widgets/DocumentSaver.svelte +17 -0
- package/dist/primitives/widgets/DocumentSaver.svelte.d.ts +13 -0
- package/dist/primitives/widgets/_DocumentBrowser.svelte +414 -27
- package/dist/primitives/widgets/_DocumentBrowser.svelte.d.ts +12 -0
- package/dist/primitives/widgets/_DocumentBrowser.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/_DocumentBrowser.svelte.test.js +277 -0
- package/dist/primitives/widgets/_FolderConfirmDelete.svelte +57 -0
- package/dist/primitives/widgets/_FolderConfirmDelete.svelte.d.ts +12 -0
- package/dist/projects/session-state.svelte.d.ts +3 -0
- package/dist/projects/session-state.svelte.js +25 -0
- package/dist/projects/session-state.test.js +43 -2
- package/dist/projects-shard/ProjectsSection.svelte +14 -18
- package/dist/runtime/runVerb-shell.test.js +2 -2
- package/dist/runtime/runVerb.test.js +2 -2
- package/dist/sh3Api/headless.js +10 -0
- package/dist/sh3core-shard/appActions.js +5 -2
- package/dist/shards/activate-browse.test.js +2 -2
- package/dist/shards/activate-contributions.test.js +2 -2
- package/dist/shards/activate-error-isolation.test.js +3 -3
- package/dist/shards/activate-on-key-revoked.test.js +2 -2
- package/dist/shards/activate-runtime.test.js +2 -2
- package/dist/shards/activate.svelte.js +5 -5
- package/dist/shards/ctx-fetch.test.js +4 -4
- package/dist/shell-shard/Terminal.svelte +4 -1
- package/dist/shell-shard/Terminal.svelte.d.ts +2 -0
- package/dist/shell-shard/dispatch.d.ts +2 -0
- package/dist/shell-shard/dispatch.js +2 -0
- package/dist/shell-shard/manifest.js +7 -1
- package/dist/shell-shard/shellShard.svelte.js +1 -1
- package/dist/shell-shard/verbs/cat.d.ts +2 -0
- package/dist/shell-shard/verbs/cat.js +35 -0
- package/dist/shell-shard/verbs/cat.test.d.ts +1 -0
- package/dist/shell-shard/verbs/cat.test.js +49 -0
- package/dist/shell-shard/verbs/index.js +12 -0
- package/dist/shell-shard/verbs/ls.d.ts +2 -0
- package/dist/shell-shard/verbs/ls.js +48 -0
- package/dist/shell-shard/verbs/ls.test.d.ts +1 -0
- package/dist/shell-shard/verbs/ls.test.js +64 -0
- package/dist/shell-shard/verbs/mkdir.d.ts +2 -0
- package/dist/shell-shard/verbs/mkdir.js +30 -0
- package/dist/shell-shard/verbs/mkdir.test.d.ts +1 -0
- package/dist/shell-shard/verbs/mkdir.test.js +48 -0
- package/dist/shell-shard/verbs/mv.d.ts +2 -0
- package/dist/shell-shard/verbs/mv.js +33 -0
- package/dist/shell-shard/verbs/mv.test.d.ts +1 -0
- package/dist/shell-shard/verbs/mv.test.js +55 -0
- package/dist/shell-shard/verbs/rm.d.ts +2 -0
- package/dist/shell-shard/verbs/rm.js +28 -0
- package/dist/shell-shard/verbs/rm.test.d.ts +1 -0
- package/dist/shell-shard/verbs/rm.test.js +47 -0
- package/dist/shell-shard/verbs/scope-parse.d.ts +7 -0
- package/dist/shell-shard/verbs/scope-parse.js +33 -0
- package/dist/shell-shard/verbs/scope-parse.test.d.ts +1 -0
- package/dist/shell-shard/verbs/scope-parse.test.js +76 -0
- package/dist/shell-shard/verbs/xfer.d.ts +2 -0
- package/dist/shell-shard/verbs/xfer.js +87 -0
- package/dist/shell-shard/verbs/xfer.test.d.ts +1 -0
- package/dist/shell-shard/verbs/xfer.test.js +107 -0
- package/dist/verbs/types.d.ts +18 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: [
|
|
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,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,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,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,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
|
+
});
|