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.
- package/dist/app/admin/AuthSettingsView.svelte +3 -9
- package/dist/app/admin/MountsView.svelte +276 -0
- package/dist/app/admin/MountsView.svelte.d.ts +3 -0
- package/dist/app/admin/SystemView.svelte +6 -6
- package/dist/app/admin/UsersView.svelte +103 -7
- package/dist/app/admin/adminApp.js +1 -0
- package/dist/app/admin/adminShard.svelte.js +10 -0
- package/dist/apps/lifecycle.js +1 -0
- package/dist/apps/types.d.ts +7 -0
- package/dist/assets/iconIds.generated.d.ts +1 -1
- package/dist/assets/iconIds.generated.js +1 -0
- package/dist/assets/icons.svg +5 -0
- package/dist/auth/admin-users.svelte.js +2 -1
- package/dist/auth/auth.svelte.d.ts +4 -5
- package/dist/auth/auth.svelte.js +5 -6
- package/dist/auth/types.d.ts +0 -2
- package/dist/chrome/CompactChrome.svelte +25 -6
- package/dist/chrome/FloatsSheet.svelte +7 -32
- package/dist/chrome/FloatsSheet.svelte.d.ts +1 -2
- package/dist/chrome/FloatsSheet.svelte.test.js +8 -14
- package/dist/chrome/MenuSheet.svelte +154 -148
- package/dist/chrome/MenuSheet.svelte.d.ts +1 -2
- package/dist/chrome/MenuSheet.svelte.test.js +24 -12
- package/dist/createShell.js +32 -21
- package/dist/createShell.remoteAuth.test.js +9 -3
- 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 +18 -1
- package/dist/documents/browse.js +40 -7
- package/dist/documents/browse.test.js +35 -35
- package/dist/documents/config.d.ts +6 -0
- package/dist/documents/config.js +18 -1
- package/dist/documents/handle.js +65 -17
- package/dist/documents/handle.test.js +88 -1
- package/dist/documents/http-backend.d.ts +6 -0
- package/dist/documents/http-backend.js +71 -2
- package/dist/documents/http-backend.test.js +51 -1
- package/dist/documents/index.d.ts +2 -2
- package/dist/documents/index.js +1 -1
- package/dist/documents/picker-api.d.ts +4 -2
- package/dist/documents/picker-api.test.d.ts +1 -1
- package/dist/documents/picker-api.test.js +89 -59
- package/dist/documents/picker-primitive.d.ts +4 -0
- package/dist/documents/picker-primitive.js +27 -29
- package/dist/documents/types.d.ts +93 -19
- package/dist/documents/types.js +6 -0
- package/dist/layout/presets.test.js +4 -4
- package/dist/layout/types.d.ts +1 -1
- package/dist/layouts-shard/LayoutsSection.svelte +3 -16
- package/dist/primitives/widgets/DocumentFilePicker.d.ts +6 -2
- package/dist/primitives/widgets/DocumentFilePicker.js +12 -5
- package/dist/primitives/widgets/DocumentFilePicker.svelte +27 -5
- 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/PickerList.svelte +1 -0
- package/dist/primitives/widgets/_DocumentBrowser.svelte +419 -35
- 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-shard/DeleteProjectDialog.svelte +32 -1
- package/dist/projects-shard/ProjectManage.svelte +197 -28
- package/dist/projects-shard/ProjectManage.svelte.test.d.ts +1 -0
- package/dist/projects-shard/ProjectManage.svelte.test.js +320 -0
- package/dist/projects-shard/ProjectsSection.svelte +3 -16
- package/dist/projects-shard/projectsApi.js +2 -1
- package/dist/registry/permission-descriptions.js +4 -0
- package/dist/server-shard/types.d.ts +21 -0
- package/dist/sh3Api/headless.js +10 -0
- package/dist/sh3core-shard/HomeSection.svelte +107 -0
- package/dist/sh3core-shard/HomeSection.svelte.d.ts +10 -0
- package/dist/sh3core-shard/Sh3Home.svelte +9 -23
- package/dist/shards/activate.svelte.d.ts +4 -0
- package/dist/shards/activate.svelte.js +11 -3
- package/dist/shards/types.d.ts +7 -0
- 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/tenant-fs-client.js +2 -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 +101 -0
- package/dist/shell-shard/verbs/xfer.test.d.ts +1 -0
- package/dist/shell-shard/verbs/xfer.test.js +96 -0
- package/dist/transport/apiFetch.js +12 -5
- 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
|
@@ -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(
|
|
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,
|
package/dist/shards/types.d.ts
CHANGED
|
@@ -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: [
|
|
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
|
|
33
|
+
const res = await apiFetch(url);
|
|
33
34
|
if (!res.ok) {
|
|
34
35
|
let msg = `HTTP ${res.status}`;
|
|
35
36
|
try {
|
|
@@ -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
|
+
});
|
|
@@ -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 {};
|