sh3-core 0.20.1 → 0.20.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/dist/BrandSlot.svelte +2 -2
  2. package/dist/actions/ctx-actions.svelte.test.js +2 -2
  3. package/dist/artifact.d.ts +2 -0
  4. package/dist/boot/satellitePayload.d.ts +2 -0
  5. package/dist/boot/satellitePayload.test.js +19 -0
  6. package/dist/build.d.ts +7 -1
  7. package/dist/build.js +22 -3
  8. package/dist/build.test.js +27 -1
  9. package/dist/createShell.js +34 -9
  10. package/dist/documents/backends.d.ts +12 -0
  11. package/dist/documents/backends.js +230 -3
  12. package/dist/documents/backends.test.js +147 -1
  13. package/dist/documents/browse.d.ts +20 -0
  14. package/dist/documents/browse.js +35 -0
  15. package/dist/documents/browse.test.js +125 -0
  16. package/dist/documents/config.d.ts +2 -4
  17. package/dist/documents/config.js +3 -7
  18. package/dist/documents/handle.js +40 -0
  19. package/dist/documents/handle.test.js +88 -1
  20. package/dist/documents/http-backend.d.ts +11 -0
  21. package/dist/documents/http-backend.js +86 -0
  22. package/dist/documents/http-backend.test.js +117 -1
  23. package/dist/documents/index.d.ts +1 -1
  24. package/dist/documents/index.js +1 -1
  25. package/dist/documents/picker-api.test.js +2 -2
  26. package/dist/documents/types.d.ts +87 -14
  27. package/dist/documents/types.js +4 -0
  28. package/dist/host-entry.d.ts +1 -1
  29. package/dist/host-entry.js +1 -1
  30. package/dist/host.d.ts +1 -1
  31. package/dist/host.js +1 -1
  32. package/dist/layout/slotHostPool.svelte.js +2 -2
  33. package/dist/overlays/FloatFrame.svelte +1 -0
  34. package/dist/primitives/widgets/DocumentFilePicker.d.ts +6 -2
  35. package/dist/primitives/widgets/DocumentFilePicker.js +12 -5
  36. package/dist/primitives/widgets/DocumentFilePicker.svelte +23 -1
  37. package/dist/primitives/widgets/DocumentFilePicker.svelte.d.ts +14 -0
  38. package/dist/primitives/widgets/DocumentFilePicker.test.d.ts +1 -0
  39. package/dist/primitives/widgets/DocumentFilePicker.test.js +33 -0
  40. package/dist/primitives/widgets/DocumentOpener.svelte +20 -0
  41. package/dist/primitives/widgets/DocumentOpener.svelte.d.ts +14 -0
  42. package/dist/primitives/widgets/DocumentSaver.svelte +17 -0
  43. package/dist/primitives/widgets/DocumentSaver.svelte.d.ts +13 -0
  44. package/dist/primitives/widgets/_DocumentBrowser.svelte +414 -27
  45. package/dist/primitives/widgets/_DocumentBrowser.svelte.d.ts +12 -0
  46. package/dist/primitives/widgets/_DocumentBrowser.svelte.test.d.ts +1 -0
  47. package/dist/primitives/widgets/_DocumentBrowser.svelte.test.js +277 -0
  48. package/dist/primitives/widgets/_FolderConfirmDelete.svelte +57 -0
  49. package/dist/primitives/widgets/_FolderConfirmDelete.svelte.d.ts +12 -0
  50. package/dist/projects/session-state.svelte.d.ts +3 -0
  51. package/dist/projects/session-state.svelte.js +25 -0
  52. package/dist/projects/session-state.test.js +43 -2
  53. package/dist/projects-shard/ProjectsSection.svelte +14 -18
  54. package/dist/runtime/runVerb-shell.test.js +2 -2
  55. package/dist/runtime/runVerb.test.js +2 -2
  56. package/dist/sh3Api/headless.js +10 -0
  57. package/dist/sh3core-shard/appActions.js +5 -2
  58. package/dist/shards/activate-browse.test.js +2 -2
  59. package/dist/shards/activate-contributions.test.js +2 -2
  60. package/dist/shards/activate-error-isolation.test.js +3 -3
  61. package/dist/shards/activate-on-key-revoked.test.js +2 -2
  62. package/dist/shards/activate-runtime.test.js +2 -2
  63. package/dist/shards/activate.svelte.js +5 -5
  64. package/dist/shards/ctx-fetch.test.js +4 -4
  65. package/dist/shell-shard/Terminal.svelte +4 -1
  66. package/dist/shell-shard/Terminal.svelte.d.ts +2 -0
  67. package/dist/shell-shard/dispatch.d.ts +2 -0
  68. package/dist/shell-shard/dispatch.js +2 -0
  69. package/dist/shell-shard/manifest.js +7 -1
  70. package/dist/shell-shard/shellShard.svelte.js +1 -1
  71. package/dist/shell-shard/verbs/cat.d.ts +2 -0
  72. package/dist/shell-shard/verbs/cat.js +35 -0
  73. package/dist/shell-shard/verbs/cat.test.d.ts +1 -0
  74. package/dist/shell-shard/verbs/cat.test.js +49 -0
  75. package/dist/shell-shard/verbs/index.js +12 -0
  76. package/dist/shell-shard/verbs/ls.d.ts +2 -0
  77. package/dist/shell-shard/verbs/ls.js +48 -0
  78. package/dist/shell-shard/verbs/ls.test.d.ts +1 -0
  79. package/dist/shell-shard/verbs/ls.test.js +64 -0
  80. package/dist/shell-shard/verbs/mkdir.d.ts +2 -0
  81. package/dist/shell-shard/verbs/mkdir.js +30 -0
  82. package/dist/shell-shard/verbs/mkdir.test.d.ts +1 -0
  83. package/dist/shell-shard/verbs/mkdir.test.js +48 -0
  84. package/dist/shell-shard/verbs/mv.d.ts +2 -0
  85. package/dist/shell-shard/verbs/mv.js +33 -0
  86. package/dist/shell-shard/verbs/mv.test.d.ts +1 -0
  87. package/dist/shell-shard/verbs/mv.test.js +55 -0
  88. package/dist/shell-shard/verbs/rm.d.ts +2 -0
  89. package/dist/shell-shard/verbs/rm.js +28 -0
  90. package/dist/shell-shard/verbs/rm.test.d.ts +1 -0
  91. package/dist/shell-shard/verbs/rm.test.js +47 -0
  92. package/dist/shell-shard/verbs/scope-parse.d.ts +7 -0
  93. package/dist/shell-shard/verbs/scope-parse.js +33 -0
  94. package/dist/shell-shard/verbs/scope-parse.test.d.ts +1 -0
  95. package/dist/shell-shard/verbs/scope-parse.test.js +76 -0
  96. package/dist/shell-shard/verbs/xfer.d.ts +2 -0
  97. package/dist/shell-shard/verbs/xfer.js +87 -0
  98. package/dist/shell-shard/verbs/xfer.test.d.ts +1 -0
  99. package/dist/shell-shard/verbs/xfer.test.js +107 -0
  100. package/dist/verbs/types.d.ts +18 -0
  101. package/dist/version.d.ts +1 -1
  102. package/dist/version.js +1 -1
  103. package/package.json +1 -1
@@ -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 {};
@@ -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,2 @@
1
+ import type { Verb } from '../../verbs/types';
2
+ export declare const xferVerb: Verb;
@@ -0,0 +1,87 @@
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. @me:notes/draft.md, @project-acme:notes/draft.md)',
7
+ ' Either side may be @me or @project-<slug>; bare paths resolve to the active scope.',
8
+ ' -R recursive (src is a folder prefix)',
9
+ ' -C copy only, do not delete source',
10
+ ].join('\n'),
11
+ programmatic: true,
12
+ async run(ctx, args) {
13
+ const ts = Date.now();
14
+ if (!ctx.docs) {
15
+ ctx.scrollback.push({ kind: 'status', text: 'xfer: document capability not available', level: 'error', ts });
16
+ return;
17
+ }
18
+ if (!ctx.docs.transferBetweenScopes) {
19
+ ctx.scrollback.push({ kind: 'status', text: 'xfer: write permission not granted', level: 'error', ts });
20
+ return;
21
+ }
22
+ let recursive = false;
23
+ let copy = false;
24
+ const positional = [];
25
+ for (const a of args) {
26
+ if (a === '-R') {
27
+ recursive = true;
28
+ continue;
29
+ }
30
+ if (a === '-C') {
31
+ copy = true;
32
+ continue;
33
+ }
34
+ positional.push(a);
35
+ }
36
+ if (positional.length < 2) {
37
+ ctx.scrollback.push({ kind: 'status', text: 'usage: xfer [-R] [-C] <src> <dst>', level: 'error', ts });
38
+ return;
39
+ }
40
+ const srcParsed = parseScopePath(positional[0]);
41
+ const dstParsed = parseScopePath(positional[1]);
42
+ if (!srcParsed || !dstParsed) {
43
+ ctx.scrollback.push({ kind: 'status', text: 'xfer: invalid path', level: 'error', ts });
44
+ return;
45
+ }
46
+ const scope = ctx.sh3.getActiveScope();
47
+ let srcTenant;
48
+ let dstTenant;
49
+ try {
50
+ srcTenant = resolveScope(srcParsed.scope, scope.id, scope.personalId);
51
+ dstTenant = resolveScope(dstParsed.scope, scope.id, scope.personalId);
52
+ }
53
+ catch (e) {
54
+ ctx.scrollback.push({ kind: 'status', text: `xfer: ${e.message}`, level: 'error', ts });
55
+ return;
56
+ }
57
+ const moveOpts = { delete: !copy };
58
+ if (!recursive) {
59
+ if (!srcParsed.path) {
60
+ ctx.scrollback.push({ kind: 'status', text: 'xfer: path required (use -R for folder recursion)', level: 'error', ts });
61
+ return;
62
+ }
63
+ if (srcTenant === dstTenant && srcParsed.shardId === dstParsed.shardId && srcParsed.path === dstParsed.path) {
64
+ ctx.scrollback.push({ kind: 'status', text: 'xfer: source and destination are the same', level: 'error', ts });
65
+ return;
66
+ }
67
+ await ctx.docs.transferBetweenScopes(srcTenant, srcParsed.shardId, srcParsed.path, dstTenant, dstParsed.shardId, dstParsed.path, moveOpts);
68
+ const verb = copy ? 'copied' : 'moved';
69
+ ctx.scrollback.push({ kind: 'status', text: `xfer: ${verb} ${positional[0]} → ${positional[1]}`, level: 'info', ts });
70
+ return;
71
+ }
72
+ const prefix = srcParsed.path;
73
+ const allDocs = await ctx.docs.listDocumentsIn(srcTenant);
74
+ const matching = allDocs.filter((d) => d.shardId === srcParsed.shardId && (!prefix || d.path.startsWith(prefix)));
75
+ if (matching.length === 0) {
76
+ ctx.scrollback.push({ kind: 'status', text: `xfer: no documents found under ${positional[0]}`, level: 'info', ts });
77
+ return;
78
+ }
79
+ let count = 0;
80
+ for (const doc of matching) {
81
+ await ctx.docs.transferBetweenScopes(srcTenant, doc.shardId, doc.path, dstTenant, dstParsed.shardId, doc.path, moveOpts);
82
+ count++;
83
+ }
84
+ const verb = copy ? 'copied' : 'moved';
85
+ ctx.scrollback.push({ kind: 'status', text: `xfer: ${verb} ${count} document${count !== 1 ? 's' : ''}`, level: 'info', ts });
86
+ },
87
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,107 @@
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 () => { }), listDocumentsIn: vi.fn(async () => []), transferBetweenScopes: 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('works from personal (non-project) scope', async () => {
36
+ const transferBetweenScopes = vi.fn(async () => { });
37
+ const docs = makeDocs({ transferBetweenScopes });
38
+ const sh3 = makeSh3(personalScope);
39
+ const { ctx, pushed } = makeCtx(docs, sh3);
40
+ await xferVerb.run(ctx, ['@me:notes/draft.md', '@project-proj-abc:notes/draft.md']);
41
+ expect(transferBetweenScopes).toHaveBeenCalledWith('user-me', 'notes', 'draft.md', 'proj-abc', 'notes', 'draft.md', expect.objectContaining({ delete: true }));
42
+ const ok = pushed.find((e) => e.kind === 'status' && e.level === 'info');
43
+ expect(ok).toBeDefined();
44
+ });
45
+ it('emits usage error when fewer than two args', async () => {
46
+ const sh3 = makeSh3(projectScope);
47
+ const { ctx, pushed } = makeCtx(makeDocs(), sh3);
48
+ await xferVerb.run(ctx, ['notes/draft.md']);
49
+ const err = pushed.find((e) => e.kind === 'status' && e.level === 'error');
50
+ expect(err.text).toMatch(/usage/i);
51
+ });
52
+ it('moves a doc from project to personal scope', async () => {
53
+ const transferBetweenScopes = vi.fn(async () => { });
54
+ const docs = makeDocs({ transferBetweenScopes });
55
+ const sh3 = makeSh3(projectScope);
56
+ const { ctx, pushed } = makeCtx(docs, sh3);
57
+ await xferVerb.run(ctx, ['@project-proj-abc:notes/draft.md', '@me:notes/draft.md']);
58
+ expect(transferBetweenScopes).toHaveBeenCalledWith('proj-abc', 'notes', 'draft.md', 'user-me', 'notes', 'draft.md', expect.objectContaining({ delete: true }));
59
+ const ok = pushed.find((e) => e.kind === 'status' && e.level === 'info');
60
+ expect(ok).toBeDefined();
61
+ });
62
+ it('-C flag copies without deleting source', async () => {
63
+ const transferBetweenScopes = vi.fn(async () => { });
64
+ const docs = makeDocs({ transferBetweenScopes });
65
+ const sh3 = makeSh3(projectScope);
66
+ const { ctx } = makeCtx(docs, sh3);
67
+ await xferVerb.run(ctx, ['-C', '@project-proj-abc:notes/draft.md', '@me:notes/draft.md']);
68
+ expect(transferBetweenScopes).toHaveBeenCalledWith('proj-abc', 'notes', 'draft.md', 'user-me', 'notes', 'draft.md', expect.objectContaining({ delete: false }));
69
+ });
70
+ it('allows @me as source when project scope is active', async () => {
71
+ const transferBetweenScopes = vi.fn(async () => { });
72
+ const docs = makeDocs({ transferBetweenScopes });
73
+ const sh3 = makeSh3(projectScope);
74
+ const { ctx, pushed } = makeCtx(docs, sh3);
75
+ await xferVerb.run(ctx, ['@me:notes/draft.md', '@project-proj-abc:notes/draft.md']);
76
+ expect(transferBetweenScopes).toHaveBeenCalledWith('user-me', 'notes', 'draft.md', 'proj-abc', 'notes', 'draft.md', expect.objectContaining({ delete: true }));
77
+ const ok = pushed.find((e) => e.kind === 'status' && e.level === 'info');
78
+ expect(ok).toBeDefined();
79
+ });
80
+ it('emits error when source and destination are identical', async () => {
81
+ const transferBetweenScopes = vi.fn(async () => { });
82
+ const docs = makeDocs({ transferBetweenScopes });
83
+ const sh3 = makeSh3(projectScope);
84
+ const { ctx, pushed } = makeCtx(docs, sh3);
85
+ await xferVerb.run(ctx, ['@project-proj-abc:notes/draft.md', '@project-proj-abc:notes/draft.md']);
86
+ expect(transferBetweenScopes).not.toHaveBeenCalled();
87
+ const err = pushed.find((e) => e.kind === 'status' && e.level === 'error');
88
+ expect(err.text).toMatch(/same/i);
89
+ });
90
+ it('-R flag recurses over all docs matching src prefix', async () => {
91
+ const transferBetweenScopes = vi.fn(async () => { });
92
+ const allDocs = [
93
+ { shardId: 'notes', path: 'ideas/a.md', size: 1, lastModified: 0 },
94
+ { shardId: 'notes', path: 'ideas/b.md', size: 1, lastModified: 0 },
95
+ { shardId: 'notes', path: 'other.md', size: 1, lastModified: 0 },
96
+ ];
97
+ const docs = makeDocs({
98
+ transferBetweenScopes,
99
+ listDocumentsIn: vi.fn(async () => allDocs),
100
+ });
101
+ const sh3 = makeSh3(projectScope);
102
+ const { ctx } = makeCtx(docs, sh3);
103
+ await xferVerb.run(ctx, ['-R', '@project-proj-abc:notes/ideas', '@me:notes/ideas']);
104
+ expect(transferBetweenScopes).toHaveBeenCalledTimes(2);
105
+ expect(transferBetweenScopes).toHaveBeenCalledWith('proj-abc', 'notes', 'ideas/a.md', 'user-me', 'notes', 'ideas/a.md', expect.objectContaining({ delete: true }));
106
+ });
107
+ });
@@ -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.20.1";
2
+ export declare const VERSION = "0.20.3";
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.20.1';
2
+ export const VERSION = '0.20.3';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh3-core",
3
- "version": "0.20.1",
3
+ "version": "0.20.3",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"