sh3-core 0.19.6 → 0.20.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/dist/app/admin/AuthSettingsView.svelte +3 -9
  2. package/dist/app/admin/MountsView.svelte +276 -0
  3. package/dist/app/admin/MountsView.svelte.d.ts +3 -0
  4. package/dist/app/admin/SystemView.svelte +6 -6
  5. package/dist/app/admin/UsersView.svelte +103 -7
  6. package/dist/app/admin/adminApp.js +1 -0
  7. package/dist/app/admin/adminShard.svelte.js +10 -0
  8. package/dist/apps/lifecycle.js +1 -0
  9. package/dist/apps/types.d.ts +7 -0
  10. package/dist/assets/iconIds.generated.d.ts +1 -1
  11. package/dist/assets/iconIds.generated.js +1 -0
  12. package/dist/assets/icons.svg +5 -0
  13. package/dist/auth/admin-users.svelte.js +2 -1
  14. package/dist/auth/auth.svelte.d.ts +4 -5
  15. package/dist/auth/auth.svelte.js +5 -6
  16. package/dist/auth/types.d.ts +0 -2
  17. package/dist/chrome/CompactChrome.svelte +25 -6
  18. package/dist/chrome/FloatsSheet.svelte +7 -32
  19. package/dist/chrome/FloatsSheet.svelte.d.ts +1 -2
  20. package/dist/chrome/FloatsSheet.svelte.test.js +8 -14
  21. package/dist/chrome/MenuSheet.svelte +154 -148
  22. package/dist/chrome/MenuSheet.svelte.d.ts +1 -2
  23. package/dist/chrome/MenuSheet.svelte.test.js +24 -12
  24. package/dist/createShell.js +32 -21
  25. package/dist/createShell.remoteAuth.test.js +9 -3
  26. package/dist/documents/backends.d.ts +12 -0
  27. package/dist/documents/backends.js +230 -3
  28. package/dist/documents/backends.test.js +147 -1
  29. package/dist/documents/browse.d.ts +18 -1
  30. package/dist/documents/browse.js +40 -7
  31. package/dist/documents/browse.test.js +35 -35
  32. package/dist/documents/config.d.ts +6 -0
  33. package/dist/documents/config.js +18 -1
  34. package/dist/documents/handle.js +65 -17
  35. package/dist/documents/handle.test.js +88 -1
  36. package/dist/documents/http-backend.d.ts +6 -0
  37. package/dist/documents/http-backend.js +71 -2
  38. package/dist/documents/http-backend.test.js +51 -1
  39. package/dist/documents/index.d.ts +2 -2
  40. package/dist/documents/index.js +1 -1
  41. package/dist/documents/picker-api.d.ts +4 -2
  42. package/dist/documents/picker-api.test.d.ts +1 -1
  43. package/dist/documents/picker-api.test.js +89 -59
  44. package/dist/documents/picker-primitive.d.ts +4 -0
  45. package/dist/documents/picker-primitive.js +27 -29
  46. package/dist/documents/types.d.ts +93 -19
  47. package/dist/documents/types.js +6 -0
  48. package/dist/layout/presets.test.js +4 -4
  49. package/dist/layout/types.d.ts +1 -1
  50. package/dist/layouts-shard/LayoutsSection.svelte +3 -16
  51. package/dist/primitives/widgets/DocumentFilePicker.d.ts +6 -2
  52. package/dist/primitives/widgets/DocumentFilePicker.js +12 -5
  53. package/dist/primitives/widgets/DocumentFilePicker.svelte +27 -5
  54. package/dist/primitives/widgets/DocumentFilePicker.svelte.d.ts +14 -0
  55. package/dist/primitives/widgets/DocumentFilePicker.test.d.ts +1 -0
  56. package/dist/primitives/widgets/DocumentFilePicker.test.js +33 -0
  57. package/dist/primitives/widgets/DocumentOpener.svelte +20 -0
  58. package/dist/primitives/widgets/DocumentOpener.svelte.d.ts +14 -0
  59. package/dist/primitives/widgets/DocumentSaver.svelte +17 -0
  60. package/dist/primitives/widgets/DocumentSaver.svelte.d.ts +13 -0
  61. package/dist/primitives/widgets/PickerList.svelte +1 -0
  62. package/dist/primitives/widgets/_DocumentBrowser.svelte +419 -35
  63. package/dist/primitives/widgets/_DocumentBrowser.svelte.d.ts +12 -0
  64. package/dist/primitives/widgets/_DocumentBrowser.svelte.test.d.ts +1 -0
  65. package/dist/primitives/widgets/_DocumentBrowser.svelte.test.js +277 -0
  66. package/dist/primitives/widgets/_FolderConfirmDelete.svelte +57 -0
  67. package/dist/primitives/widgets/_FolderConfirmDelete.svelte.d.ts +12 -0
  68. package/dist/projects-shard/DeleteProjectDialog.svelte +32 -1
  69. package/dist/projects-shard/ProjectManage.svelte +197 -28
  70. package/dist/projects-shard/ProjectManage.svelte.test.d.ts +1 -0
  71. package/dist/projects-shard/ProjectManage.svelte.test.js +320 -0
  72. package/dist/projects-shard/ProjectsSection.svelte +3 -16
  73. package/dist/projects-shard/projectsApi.js +2 -1
  74. package/dist/registry/permission-descriptions.js +4 -0
  75. package/dist/server-shard/types.d.ts +21 -0
  76. package/dist/sh3Api/headless.js +10 -0
  77. package/dist/sh3core-shard/HomeSection.svelte +107 -0
  78. package/dist/sh3core-shard/HomeSection.svelte.d.ts +10 -0
  79. package/dist/sh3core-shard/Sh3Home.svelte +9 -23
  80. package/dist/shards/activate.svelte.d.ts +4 -0
  81. package/dist/shards/activate.svelte.js +11 -3
  82. package/dist/shards/types.d.ts +7 -0
  83. package/dist/shell-shard/Terminal.svelte +4 -1
  84. package/dist/shell-shard/Terminal.svelte.d.ts +2 -0
  85. package/dist/shell-shard/dispatch.d.ts +2 -0
  86. package/dist/shell-shard/dispatch.js +2 -0
  87. package/dist/shell-shard/manifest.js +7 -1
  88. package/dist/shell-shard/shellShard.svelte.js +1 -1
  89. package/dist/shell-shard/tenant-fs-client.js +2 -1
  90. package/dist/shell-shard/verbs/cat.d.ts +2 -0
  91. package/dist/shell-shard/verbs/cat.js +35 -0
  92. package/dist/shell-shard/verbs/cat.test.d.ts +1 -0
  93. package/dist/shell-shard/verbs/cat.test.js +49 -0
  94. package/dist/shell-shard/verbs/index.js +12 -0
  95. package/dist/shell-shard/verbs/ls.d.ts +2 -0
  96. package/dist/shell-shard/verbs/ls.js +48 -0
  97. package/dist/shell-shard/verbs/ls.test.d.ts +1 -0
  98. package/dist/shell-shard/verbs/ls.test.js +64 -0
  99. package/dist/shell-shard/verbs/mkdir.d.ts +2 -0
  100. package/dist/shell-shard/verbs/mkdir.js +30 -0
  101. package/dist/shell-shard/verbs/mkdir.test.d.ts +1 -0
  102. package/dist/shell-shard/verbs/mkdir.test.js +48 -0
  103. package/dist/shell-shard/verbs/mv.d.ts +2 -0
  104. package/dist/shell-shard/verbs/mv.js +33 -0
  105. package/dist/shell-shard/verbs/mv.test.d.ts +1 -0
  106. package/dist/shell-shard/verbs/mv.test.js +55 -0
  107. package/dist/shell-shard/verbs/rm.d.ts +2 -0
  108. package/dist/shell-shard/verbs/rm.js +28 -0
  109. package/dist/shell-shard/verbs/rm.test.d.ts +1 -0
  110. package/dist/shell-shard/verbs/rm.test.js +47 -0
  111. package/dist/shell-shard/verbs/scope-parse.d.ts +7 -0
  112. package/dist/shell-shard/verbs/scope-parse.js +33 -0
  113. package/dist/shell-shard/verbs/scope-parse.test.d.ts +1 -0
  114. package/dist/shell-shard/verbs/scope-parse.test.js +76 -0
  115. package/dist/shell-shard/verbs/xfer.d.ts +2 -0
  116. package/dist/shell-shard/verbs/xfer.js +101 -0
  117. package/dist/shell-shard/verbs/xfer.test.d.ts +1 -0
  118. package/dist/shell-shard/verbs/xfer.test.js +96 -0
  119. package/dist/transport/apiFetch.js +12 -5
  120. package/dist/verbs/types.d.ts +18 -0
  121. package/dist/version.d.ts +1 -1
  122. package/dist/version.js +1 -1
  123. package/package.json +1 -1
@@ -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,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 async function apiFetch(url, init) {
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
- const tf = await getTauriFetch();
62
- if (tf)
63
- return tf(url, finalInit);
64
- return fetch(url, Object.assign({ credentials: 'include' }, finalInit));
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
  }
@@ -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.19.6";
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.19.6';
2
+ export const VERSION = '0.20.2';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh3-core",
3
- "version": "0.19.6",
3
+ "version": "0.20.2",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"