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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { rmVerb } from './rm';
|
|
3
|
+
function makeDocs(overrides = {}) {
|
|
4
|
+
return Object.assign({ listDocuments: vi.fn(async () => []), listShards: vi.fn(async () => []), watchDocuments: vi.fn(() => () => { }), deleteFrom: 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('rm verb', () => {
|
|
20
|
+
it('emits error when docs capability missing', async () => {
|
|
21
|
+
const { ctx, pushed } = makeCtx(undefined);
|
|
22
|
+
await rmVerb.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 rmVerb.run(ctx, []);
|
|
29
|
+
const err = pushed.find((e) => e.kind === 'status' && e.level === 'error');
|
|
30
|
+
expect(err.text).toMatch(/usage/i);
|
|
31
|
+
});
|
|
32
|
+
it('deletes a document', async () => {
|
|
33
|
+
const docs = makeDocs({ deleteFrom: vi.fn(async () => { }) });
|
|
34
|
+
const { ctx, pushed } = makeCtx(docs);
|
|
35
|
+
await rmVerb.run(ctx, ['notes/draft.md']);
|
|
36
|
+
expect(docs.deleteFrom).toHaveBeenCalledWith('notes', 'draft.md');
|
|
37
|
+
const ok = pushed.find((e) => e.kind === 'status' && e.level === 'info');
|
|
38
|
+
expect(ok).toBeDefined();
|
|
39
|
+
});
|
|
40
|
+
it('emits error when deleteFrom not available on capability', async () => {
|
|
41
|
+
const docs = makeDocs({ deleteFrom: undefined });
|
|
42
|
+
const { ctx, pushed } = makeCtx(docs);
|
|
43
|
+
await rmVerb.run(ctx, ['notes/draft.md']);
|
|
44
|
+
const err = pushed.find((e) => e.kind === 'status' && e.level === 'error');
|
|
45
|
+
expect(err).toBeDefined();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface ParsedScopePath {
|
|
2
|
+
scope: string | null;
|
|
3
|
+
shardId: string;
|
|
4
|
+
path: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function parseScopePath(raw: string): ParsedScopePath | null;
|
|
7
|
+
export declare function resolveScope(alias: string | null, activeId: string, personalId: string): string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function parseScopePath(raw) {
|
|
2
|
+
if (!raw)
|
|
3
|
+
return null;
|
|
4
|
+
let scope = null;
|
|
5
|
+
let rest = raw;
|
|
6
|
+
const colonIdx = raw.indexOf(':');
|
|
7
|
+
if (colonIdx > 0) {
|
|
8
|
+
const prefix = raw.slice(0, colonIdx);
|
|
9
|
+
if (prefix === '@me' || prefix.startsWith('@project-')) {
|
|
10
|
+
scope = prefix;
|
|
11
|
+
rest = raw.slice(colonIdx + 1);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
if (!rest)
|
|
15
|
+
return null;
|
|
16
|
+
const slashIdx = rest.indexOf('/');
|
|
17
|
+
if (slashIdx < 0) {
|
|
18
|
+
return { scope, shardId: rest, path: '' };
|
|
19
|
+
}
|
|
20
|
+
const shardId = rest.slice(0, slashIdx);
|
|
21
|
+
if (!shardId)
|
|
22
|
+
return null;
|
|
23
|
+
return { scope, shardId, path: rest.slice(slashIdx + 1) };
|
|
24
|
+
}
|
|
25
|
+
export function resolveScope(alias, activeId, personalId) {
|
|
26
|
+
if (alias === null)
|
|
27
|
+
return activeId;
|
|
28
|
+
if (alias === '@me')
|
|
29
|
+
return personalId;
|
|
30
|
+
if (alias.startsWith('@project-'))
|
|
31
|
+
return alias.slice('@project-'.length);
|
|
32
|
+
throw new Error(`Unknown scope alias: ${alias}`);
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseScopePath, resolveScope } from './scope-parse';
|
|
3
|
+
describe('parseScopePath', () => {
|
|
4
|
+
it('parses a bare shard/path', () => {
|
|
5
|
+
expect(parseScopePath('notes/foo/bar.md')).toEqual({
|
|
6
|
+
scope: null,
|
|
7
|
+
shardId: 'notes',
|
|
8
|
+
path: 'foo/bar.md',
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
it('parses a shard root (no slash)', () => {
|
|
12
|
+
expect(parseScopePath('notes')).toEqual({
|
|
13
|
+
scope: null,
|
|
14
|
+
shardId: 'notes',
|
|
15
|
+
path: '',
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
it('parses @me: prefix', () => {
|
|
19
|
+
expect(parseScopePath('@me:notes/draft.md')).toEqual({
|
|
20
|
+
scope: '@me',
|
|
21
|
+
shardId: 'notes',
|
|
22
|
+
path: 'draft.md',
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
it('parses @project-slug: prefix', () => {
|
|
26
|
+
expect(parseScopePath('@project-acme:design/spec.md')).toEqual({
|
|
27
|
+
scope: '@project-acme',
|
|
28
|
+
shardId: 'design',
|
|
29
|
+
path: 'spec.md',
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
it('parses @project-compound-slug: prefix', () => {
|
|
33
|
+
expect(parseScopePath('@project-notes-v2:shard/path.md')).toEqual({
|
|
34
|
+
scope: '@project-notes-v2',
|
|
35
|
+
shardId: 'shard',
|
|
36
|
+
path: 'path.md',
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
it('parses @me: with shard root only', () => {
|
|
40
|
+
expect(parseScopePath('@me:notes')).toEqual({
|
|
41
|
+
scope: '@me',
|
|
42
|
+
shardId: 'notes',
|
|
43
|
+
path: '',
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
it('returns null for empty string', () => {
|
|
47
|
+
expect(parseScopePath('')).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
it('returns null when scope prefix has no shard', () => {
|
|
50
|
+
expect(parseScopePath('@me:')).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
it('treats unknown @-prefix as part of shard name (no scope)', () => {
|
|
53
|
+
// @unknown is not @me or @project-*, so no scope stripping
|
|
54
|
+
const result = parseScopePath('@unknown:shard/path');
|
|
55
|
+
expect(result === null || result === void 0 ? void 0 : result.scope).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe('resolveScope', () => {
|
|
59
|
+
const active = 'proj-tenant';
|
|
60
|
+
const personal = 'user-tenant';
|
|
61
|
+
it('returns active scope for null alias', () => {
|
|
62
|
+
expect(resolveScope(null, active, personal)).toBe(active);
|
|
63
|
+
});
|
|
64
|
+
it('returns personal id for @me', () => {
|
|
65
|
+
expect(resolveScope('@me', active, personal)).toBe(personal);
|
|
66
|
+
});
|
|
67
|
+
it('strips @project- prefix to get project tenant id', () => {
|
|
68
|
+
expect(resolveScope('@project-acme', active, personal)).toBe('acme');
|
|
69
|
+
});
|
|
70
|
+
it('handles multi-segment project slug', () => {
|
|
71
|
+
expect(resolveScope('@project-notes-v2', active, personal)).toBe('notes-v2');
|
|
72
|
+
});
|
|
73
|
+
it('throws for unknown alias', () => {
|
|
74
|
+
expect(() => resolveScope('@unknown', active, personal)).toThrow();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { parseScopePath, resolveScope } from './scope-parse';
|
|
2
|
+
export const xferVerb = {
|
|
3
|
+
name: 'xfer',
|
|
4
|
+
summary: [
|
|
5
|
+
'Transfer docs across scopes. Usage: xfer [-R] [-C] <src> <dst>',
|
|
6
|
+
' Scopes: @me | @project-<slug> (e.g. @project-acme:notes/draft.md)',
|
|
7
|
+
' -R recursive (src is a folder prefix)',
|
|
8
|
+
' -C copy only, do not delete source',
|
|
9
|
+
].join('\n'),
|
|
10
|
+
programmatic: true,
|
|
11
|
+
async run(ctx, args) {
|
|
12
|
+
const ts = Date.now();
|
|
13
|
+
if (!ctx.docs) {
|
|
14
|
+
ctx.scrollback.push({ kind: 'status', text: 'xfer: document capability not available', level: 'error', ts });
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const scope = ctx.sh3.getActiveScope();
|
|
18
|
+
if (!scope.isProject) {
|
|
19
|
+
ctx.scrollback.push({ kind: 'status', text: 'xfer: only available when a project scope is active', level: 'error', ts });
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
// Parse flags
|
|
23
|
+
let recursive = false;
|
|
24
|
+
let copy = false;
|
|
25
|
+
const positional = [];
|
|
26
|
+
for (const a of args) {
|
|
27
|
+
if (a === '-R') {
|
|
28
|
+
recursive = true;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (a === '-C') {
|
|
32
|
+
copy = true;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
positional.push(a);
|
|
36
|
+
}
|
|
37
|
+
if (positional.length < 2) {
|
|
38
|
+
ctx.scrollback.push({ kind: 'status', text: 'usage: xfer [-R] [-C] <src> <dst>', level: 'error', ts });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (!ctx.docs.transferToScope) {
|
|
42
|
+
ctx.scrollback.push({ kind: 'status', text: 'xfer: write permission not granted', level: 'error', ts });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const srcParsed = parseScopePath(positional[0]);
|
|
46
|
+
const dstParsed = parseScopePath(positional[1]);
|
|
47
|
+
if (!srcParsed || !dstParsed) {
|
|
48
|
+
ctx.scrollback.push({ kind: 'status', text: 'xfer: invalid path', level: 'error', ts });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
let srcTenant;
|
|
52
|
+
let dstTenant;
|
|
53
|
+
try {
|
|
54
|
+
srcTenant = resolveScope(srcParsed.scope, scope.id, scope.personalId);
|
|
55
|
+
dstTenant = resolveScope(dstParsed.scope, scope.id, scope.personalId);
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
ctx.scrollback.push({ kind: 'status', text: `xfer: ${e.message}`, level: 'error', ts });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// transferToScope always reads from the active tenant; reject if src doesn't match.
|
|
62
|
+
if (srcTenant !== scope.id) {
|
|
63
|
+
ctx.scrollback.push({
|
|
64
|
+
kind: 'status',
|
|
65
|
+
text: 'xfer: source must be the active project scope in v1 — switch to the source scope first',
|
|
66
|
+
level: 'error',
|
|
67
|
+
ts,
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const opts = { delete: !copy, targetShardId: dstParsed.shardId };
|
|
72
|
+
if (!recursive) {
|
|
73
|
+
if (!srcParsed.path) {
|
|
74
|
+
ctx.scrollback.push({ kind: 'status', text: 'xfer: path required (use -R for folder recursion)', level: 'error', ts });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
await ctx.docs.transferToScope(srcParsed.shardId, srcParsed.path, dstTenant, opts);
|
|
78
|
+
const verb = copy ? 'copied' : 'moved';
|
|
79
|
+
ctx.scrollback.push({ kind: 'status', text: `xfer: ${verb} ${positional[0]} → ${positional[1]}`, level: 'info', ts });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// Recursive: list all docs in srcTenant matching the prefix
|
|
83
|
+
// transferToScope uses getTenantId() (active scope) — to read from srcTenant
|
|
84
|
+
// we rely on the src scope being the active tenant or the capability seeing it.
|
|
85
|
+
// For v1 we use listDocuments (active tenant) and filter by shard + prefix.
|
|
86
|
+
const prefix = srcParsed.path;
|
|
87
|
+
const allDocs = await ctx.docs.listDocuments();
|
|
88
|
+
const matching = allDocs.filter((d) => d.shardId === srcParsed.shardId && (!prefix || d.path.startsWith(prefix)));
|
|
89
|
+
if (matching.length === 0) {
|
|
90
|
+
ctx.scrollback.push({ kind: 'status', text: `xfer: no documents found under ${positional[0]}`, level: 'info', ts });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
let count = 0;
|
|
94
|
+
for (const doc of matching) {
|
|
95
|
+
await ctx.docs.transferToScope(doc.shardId, doc.path, dstTenant, opts);
|
|
96
|
+
count++;
|
|
97
|
+
}
|
|
98
|
+
const verb = copy ? 'copied' : 'moved';
|
|
99
|
+
ctx.scrollback.push({ kind: 'status', text: `xfer: ${verb} ${count} document${count !== 1 ? 's' : ''}`, level: 'info', ts });
|
|
100
|
+
},
|
|
101
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { xferVerb } from './xfer';
|
|
3
|
+
function makeDocs(overrides = {}) {
|
|
4
|
+
return Object.assign({ listDocuments: vi.fn(async () => []), listShards: vi.fn(async () => []), watchDocuments: vi.fn(() => () => { }), transferToScope: vi.fn(async () => { }) }, overrides);
|
|
5
|
+
}
|
|
6
|
+
function makeSh3(scope) {
|
|
7
|
+
return {
|
|
8
|
+
getActiveScope: () => scope,
|
|
9
|
+
listProjects: () => [],
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function makeCtx(docs, sh3) {
|
|
13
|
+
const pushed = [];
|
|
14
|
+
const ctx = {
|
|
15
|
+
sh3: sh3 !== null && sh3 !== void 0 ? sh3 : {},
|
|
16
|
+
scrollback: { push: (e) => pushed.push(e) },
|
|
17
|
+
session: {},
|
|
18
|
+
cwd: '/',
|
|
19
|
+
fs: {},
|
|
20
|
+
docs,
|
|
21
|
+
dispatch: async () => { },
|
|
22
|
+
};
|
|
23
|
+
return { ctx, pushed };
|
|
24
|
+
}
|
|
25
|
+
const projectScope = { id: 'proj-abc', isProject: true, personalId: 'user-me' };
|
|
26
|
+
const personalScope = { id: 'user-me', isProject: false, personalId: 'user-me' };
|
|
27
|
+
describe('xfer verb', () => {
|
|
28
|
+
it('emits error when docs capability missing', async () => {
|
|
29
|
+
const sh3 = makeSh3(projectScope);
|
|
30
|
+
const { ctx, pushed } = makeCtx(undefined, sh3);
|
|
31
|
+
await xferVerb.run(ctx, ['@project-proj-abc:notes/draft.md', '@me:notes/draft.md']);
|
|
32
|
+
const err = pushed.find((e) => e.kind === 'status' && e.level === 'error');
|
|
33
|
+
expect(err).toBeDefined();
|
|
34
|
+
});
|
|
35
|
+
it('emits error when active scope is not a project', async () => {
|
|
36
|
+
const sh3 = makeSh3(personalScope);
|
|
37
|
+
const { ctx, pushed } = makeCtx(makeDocs(), sh3);
|
|
38
|
+
await xferVerb.run(ctx, ['notes/draft.md', '@me:notes/draft.md']);
|
|
39
|
+
const err = pushed.find((e) => e.kind === 'status' && e.level === 'error');
|
|
40
|
+
expect(err).toBeDefined();
|
|
41
|
+
expect(err.text).toMatch(/project/i);
|
|
42
|
+
});
|
|
43
|
+
it('emits usage error when fewer than two args', async () => {
|
|
44
|
+
const sh3 = makeSh3(projectScope);
|
|
45
|
+
const { ctx, pushed } = makeCtx(makeDocs(), sh3);
|
|
46
|
+
await xferVerb.run(ctx, ['notes/draft.md']);
|
|
47
|
+
const err = pushed.find((e) => e.kind === 'status' && e.level === 'error');
|
|
48
|
+
expect(err.text).toMatch(/usage/i);
|
|
49
|
+
});
|
|
50
|
+
it('moves (default) a doc from project to personal scope', async () => {
|
|
51
|
+
const transferToScope = vi.fn(async () => { });
|
|
52
|
+
const docs = makeDocs({ transferToScope });
|
|
53
|
+
const sh3 = makeSh3(projectScope);
|
|
54
|
+
const { ctx, pushed } = makeCtx(docs, sh3);
|
|
55
|
+
await xferVerb.run(ctx, ['@project-proj-abc:notes/draft.md', '@me:notes/draft.md']);
|
|
56
|
+
expect(transferToScope).toHaveBeenCalledWith('notes', 'draft.md', 'user-me', expect.objectContaining({ delete: true }));
|
|
57
|
+
const ok = pushed.find((e) => e.kind === 'status' && e.level === 'info');
|
|
58
|
+
expect(ok).toBeDefined();
|
|
59
|
+
});
|
|
60
|
+
it('-C flag copies without deleting source', async () => {
|
|
61
|
+
const transferToScope = vi.fn(async () => { });
|
|
62
|
+
const docs = makeDocs({ transferToScope });
|
|
63
|
+
const sh3 = makeSh3(projectScope);
|
|
64
|
+
const { ctx } = makeCtx(docs, sh3);
|
|
65
|
+
await xferVerb.run(ctx, ['-C', '@project-proj-abc:notes/draft.md', '@me:notes/draft.md']);
|
|
66
|
+
expect(transferToScope).toHaveBeenCalledWith('notes', 'draft.md', 'user-me', expect.objectContaining({ delete: false }));
|
|
67
|
+
});
|
|
68
|
+
it('rejects when src scope is not the active project (v1 limitation)', async () => {
|
|
69
|
+
const transferToScope = vi.fn(async () => { });
|
|
70
|
+
const docs = makeDocs({ transferToScope });
|
|
71
|
+
const sh3 = makeSh3(projectScope);
|
|
72
|
+
const { ctx, pushed } = makeCtx(docs, sh3);
|
|
73
|
+
// @me src while project is active → src tenant differs from active
|
|
74
|
+
await xferVerb.run(ctx, ['@me:notes/draft.md', '@project-proj-abc:notes/draft.md']);
|
|
75
|
+
expect(transferToScope).not.toHaveBeenCalled();
|
|
76
|
+
const err = pushed.find((e) => e.kind === 'status' && e.level === 'error');
|
|
77
|
+
expect(err).toBeDefined();
|
|
78
|
+
});
|
|
79
|
+
it('-R flag recurses over all docs matching src prefix', async () => {
|
|
80
|
+
const transferToScope = vi.fn(async () => { });
|
|
81
|
+
const allDocs = [
|
|
82
|
+
{ shardId: 'notes', path: 'ideas/a.md', size: 1, lastModified: 0 },
|
|
83
|
+
{ shardId: 'notes', path: 'ideas/b.md', size: 1, lastModified: 0 },
|
|
84
|
+
{ shardId: 'notes', path: 'other.md', size: 1, lastModified: 0 },
|
|
85
|
+
];
|
|
86
|
+
const docs = makeDocs({
|
|
87
|
+
transferToScope,
|
|
88
|
+
listDocuments: vi.fn(async () => allDocs),
|
|
89
|
+
});
|
|
90
|
+
const sh3 = makeSh3(projectScope);
|
|
91
|
+
const { ctx } = makeCtx(docs, sh3);
|
|
92
|
+
await xferVerb.run(ctx, ['-R', '@project-proj-abc:notes/ideas', '@me:notes/ideas']);
|
|
93
|
+
// Only the two docs under ideas/ should be transferred
|
|
94
|
+
expect(transferToScope).toHaveBeenCalledTimes(2);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -43,7 +43,7 @@ async function getTauriFetch() {
|
|
|
43
43
|
}
|
|
44
44
|
return tauriFetch;
|
|
45
45
|
}
|
|
46
|
-
export
|
|
46
|
+
export function apiFetch(url, init) {
|
|
47
47
|
var _a;
|
|
48
48
|
// Inject Authorization: Bearer <session-token> if a session is active
|
|
49
49
|
// and the caller didn't already supply one. Cookies don't survive the
|
|
@@ -58,8 +58,15 @@ export async function apiFetch(url, init) {
|
|
|
58
58
|
finalInit = Object.assign(Object.assign({}, finalInit), { headers });
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
// Fast path: outside the Tauri webview, fire fetch synchronously so
|
|
62
|
+
// call-timing assertions in tests still hold and web builds skip the
|
|
63
|
+
// dynamic-import probe entirely.
|
|
64
|
+
if (!inTauriRuntime()) {
|
|
65
|
+
return fetch(url, Object.assign({ credentials: 'include' }, finalInit));
|
|
66
|
+
}
|
|
67
|
+
return getTauriFetch().then((tf) => {
|
|
68
|
+
if (tf)
|
|
69
|
+
return tf(url, finalInit);
|
|
70
|
+
return fetch(url, Object.assign({ credentials: 'include' }, finalInit));
|
|
71
|
+
});
|
|
65
72
|
}
|
package/dist/verbs/types.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { TreeRootRef } from '../layout/types';
|
|
|
5
5
|
import type { DispatchToTerminalResult } from '../shell-shard/dispatch-to-terminal';
|
|
6
6
|
import type { ActionDescriptor } from '../actions/types';
|
|
7
7
|
import type { FieldsApi } from '../fields/types';
|
|
8
|
+
import type { BrowseCapability } from '../documents/browse';
|
|
8
9
|
export interface Sh3Api {
|
|
9
10
|
listApps(): Array<{
|
|
10
11
|
id: string;
|
|
@@ -158,6 +159,21 @@ export interface Sh3Api {
|
|
|
158
159
|
* Controllable-field surface — see fields/types.ts:FieldsApi for shape.
|
|
159
160
|
*/
|
|
160
161
|
fields: FieldsApi;
|
|
162
|
+
/**
|
|
163
|
+
* Active scope identity. `id` is the current tenant (project or personal).
|
|
164
|
+
* `personalId` is always the user's base tenant regardless of active project.
|
|
165
|
+
* `isProject` is true when a project scope is currently active.
|
|
166
|
+
*/
|
|
167
|
+
getActiveScope(): {
|
|
168
|
+
id: string;
|
|
169
|
+
isProject: boolean;
|
|
170
|
+
personalId: string;
|
|
171
|
+
};
|
|
172
|
+
/** List projects visible to the current user. */
|
|
173
|
+
listProjects(): Array<{
|
|
174
|
+
id: string;
|
|
175
|
+
name: string;
|
|
176
|
+
}>;
|
|
161
177
|
}
|
|
162
178
|
export type { DispatchToTerminalResult } from '../shell-shard/dispatch-to-terminal';
|
|
163
179
|
export interface VerbContext {
|
|
@@ -166,6 +182,8 @@ export interface VerbContext {
|
|
|
166
182
|
session: SessionClient;
|
|
167
183
|
cwd: string;
|
|
168
184
|
fs: TenantFsClient;
|
|
185
|
+
/** Document zone browse capability. Present when shell-shard has documents:browse permission. */
|
|
186
|
+
docs?: BrowseCapability;
|
|
169
187
|
/** Invoke another registered verb programmatically (used by rich-entry clicks). */
|
|
170
188
|
dispatch(line: string): Promise<void>;
|
|
171
189
|
/**
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Auto-generated from package.json — do not edit manually. */
|
|
2
|
-
export declare const VERSION = "0.
|
|
2
|
+
export declare const VERSION = "0.20.2";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Auto-generated from package.json — do not edit manually. */
|
|
2
|
-
export const VERSION = '0.
|
|
2
|
+
export const VERSION = '0.20.2';
|