sh3-core 0.22.5 → 0.23.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/api.d.ts +1 -1
- package/dist/api.js +1 -1
- package/dist/app/admin/adminApp.js +2 -0
- package/dist/app/admin/adminShard.svelte.js +1 -0
- package/dist/app/store/storeApp.js +3 -1
- package/dist/app/store/storeShard.svelte.js +1 -0
- package/dist/app-appearance/appearanceShard.svelte.js +1 -0
- package/dist/apps/lifecycle.js +22 -10
- package/dist/apps/lifecycle.test.js +53 -1
- package/dist/apps/types.d.ts +9 -0
- package/dist/chrome/CompactChrome.svelte +11 -7
- package/dist/createShell.js +40 -0
- package/dist/documents/picker-api.test.js +40 -0
- package/dist/documents/picker-primitive.d.ts +39 -1
- package/dist/documents/picker-primitive.js +5 -4
- package/dist/host.js +30 -7
- package/dist/layout/slotHostPool.svelte.d.ts +11 -0
- package/dist/layout/slotHostPool.svelte.js +41 -17
- package/dist/layout/slotHostPool.test.js +45 -1
- package/dist/layouts-shard/layoutsShard.svelte.js +1 -0
- package/dist/overlays/OverlayRoots.svelte +15 -4
- package/dist/overlays/__test__/OverlayBindHarness.svelte +20 -0
- package/dist/overlays/__test__/OverlayBindHarness.svelte.d.ts +3 -0
- package/dist/overlays/float-compact-bind.svelte.test.d.ts +1 -0
- package/dist/overlays/float-compact-bind.svelte.test.js +51 -0
- package/dist/overlays/modal.js +3 -0
- package/dist/overlays/modal.test.js +45 -0
- package/dist/overlays/types.d.ts +9 -0
- package/dist/primitives/widgets/ShardPicker.svelte +38 -0
- package/dist/primitives/widgets/ShardPicker.svelte.d.ts +9 -0
- package/dist/primitives/widgets/_DocumentBrowser.svelte +11 -3
- package/dist/primitives/widgets/_DocumentBrowser.svelte.d.ts +2 -0
- package/dist/projects/scope-gate.d.ts +4 -0
- package/dist/projects/scope-gate.js +51 -0
- package/dist/projects/scope-gate.test.d.ts +1 -0
- package/dist/projects/scope-gate.test.js +92 -0
- package/dist/projects-shard/ProjectManage.svelte +42 -2
- package/dist/projects-shard/ProjectManage.svelte.test.js +10 -9
- package/dist/projects-shard/projectsApi.d.ts +3 -2
- package/dist/projects-shard/projectsApi.test.js +1 -1
- package/dist/projects-shard/projectsShard.svelte.js +1 -0
- package/dist/runtime/runVerb.d.ts +9 -0
- package/dist/runtime/runVerb.js +4 -4
- package/dist/runtime/runVerb.test.js +29 -0
- package/dist/sh3Api/headless.d.ts +7 -0
- package/dist/sh3Api/headless.js +3 -1
- package/dist/sh3Api/headless.svelte.test.js +42 -0
- package/dist/sh3core-shard/Sh3Home.svelte +3 -3
- package/dist/sh3core-shard/sh3coreShard.svelte.js +1 -0
- package/dist/shards/lifecycle.svelte.d.ts +8 -2
- package/dist/shards/lifecycle.svelte.js +65 -7
- package/dist/shards/lifecycle.test.js +110 -1
- package/dist/shards/types.d.ts +13 -0
- package/dist/shell-shard/Terminal.svelte +1 -4
- package/dist/shell-shard/Terminal.svelte.d.ts +0 -2
- package/dist/shell-shard/dispatch.d.ts +0 -2
- package/dist/shell-shard/dispatch.js +0 -2
- package/dist/shell-shard/display-cwd.test.js +4 -4
- package/dist/shell-shard/manifest.js +1 -0
- package/dist/shell-shard/shellShard.svelte.d.ts +1 -1
- package/dist/shell-shard/shellShard.svelte.js +9 -4
- package/dist/shell-shard/verbs/cat.js +3 -3
- package/dist/shell-shard/verbs/cat.test.js +1 -2
- package/dist/shell-shard/verbs/ls.js +2 -2
- package/dist/shell-shard/verbs/ls.test.js +1 -2
- package/dist/shell-shard/verbs/mkdir.js +3 -3
- package/dist/shell-shard/verbs/mkdir.test.js +1 -2
- package/dist/shell-shard/verbs/mv.js +3 -3
- package/dist/shell-shard/verbs/mv.test.js +1 -2
- package/dist/shell-shard/verbs/rm.js +3 -3
- package/dist/shell-shard/verbs/rm.test.js +1 -2
- package/dist/shell-shard/verbs/xfer.js +5 -5
- package/dist/shell-shard/verbs/xfer.test.js +2 -2
- package/dist/verbs/types.d.ts +10 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ let host;
|
|
|
6
6
|
let cmp = null;
|
|
7
7
|
let originalFetch;
|
|
8
8
|
function makeProject(overrides = {}) {
|
|
9
|
-
return Object.assign({ id: 'acme-1234', name: 'Acme', description: '', members: ['user-1'], appAllowlist: [], createdBy: 'user-1', createdAt: 0, updatedAt: 0 }, overrides);
|
|
9
|
+
return Object.assign({ id: 'acme-1234', name: 'Acme', description: '', members: ['user-1'], appAllowlist: [], shardAllowlist: [], createdBy: 'user-1', createdAt: 0, updatedAt: 0 }, overrides);
|
|
10
10
|
}
|
|
11
11
|
beforeEach(() => {
|
|
12
12
|
__resetAdminUsersForTest();
|
|
@@ -33,14 +33,14 @@ afterEach(() => {
|
|
|
33
33
|
globalThis.fetch = originalFetch;
|
|
34
34
|
});
|
|
35
35
|
describe('ProjectManage tabs', () => {
|
|
36
|
-
it('renders
|
|
36
|
+
it('renders four tabs: Apps, Shards, Users, Mounts', async () => {
|
|
37
37
|
cmp = mount(ProjectManage, {
|
|
38
38
|
target: host,
|
|
39
39
|
props: { project: makeProject(), onClose: () => { } },
|
|
40
40
|
});
|
|
41
41
|
await tick();
|
|
42
42
|
const labels = Array.from(host.querySelectorAll('[role="tab"]')).map((el) => { var _a, _b; return (_b = (_a = el.textContent) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : ''; });
|
|
43
|
-
expect(labels).toEqual(['Apps', 'Users', 'Mounts']);
|
|
43
|
+
expect(labels).toEqual(['Apps', 'Shards', 'Users', 'Mounts']);
|
|
44
44
|
});
|
|
45
45
|
});
|
|
46
46
|
describe('ProjectManage mount fetch', () => {
|
|
@@ -119,7 +119,7 @@ describe('ProjectManage mount list', () => {
|
|
|
119
119
|
await new Promise((r) => setTimeout(r, 0));
|
|
120
120
|
await tick();
|
|
121
121
|
const tabs = host.querySelectorAll('[role="tab"]');
|
|
122
|
-
tabs[
|
|
122
|
+
tabs[3].click();
|
|
123
123
|
await tick();
|
|
124
124
|
const rows = host.querySelectorAll('[data-mount-row]');
|
|
125
125
|
expect(rows.length).toBe(2);
|
|
@@ -150,7 +150,7 @@ describe('ProjectManage mount list', () => {
|
|
|
150
150
|
await new Promise((r) => setTimeout(r, 0));
|
|
151
151
|
await tick();
|
|
152
152
|
const tabs = host.querySelectorAll('[role="tab"]');
|
|
153
|
-
tabs[
|
|
153
|
+
tabs[3].click();
|
|
154
154
|
await tick();
|
|
155
155
|
const cb = host.querySelector('[data-mount-row="assets"] input[type="checkbox"]');
|
|
156
156
|
expect(cb.checked).toBe(false);
|
|
@@ -183,15 +183,16 @@ describe('ProjectManage dirty indicators', () => {
|
|
|
183
183
|
await tick();
|
|
184
184
|
const tabs = host.querySelectorAll('[role="tab"]');
|
|
185
185
|
expect(host.querySelectorAll('.tab-dirty').length).toBe(0);
|
|
186
|
-
tabs[
|
|
186
|
+
tabs[3].click();
|
|
187
187
|
await tick();
|
|
188
188
|
const cb = host.querySelector('[data-mount-row="assets"] input[type="checkbox"]');
|
|
189
189
|
cb.click();
|
|
190
190
|
await tick();
|
|
191
|
-
const dirtyDot = tabs[
|
|
191
|
+
const dirtyDot = tabs[3].querySelector('.tab-dirty');
|
|
192
192
|
expect(dirtyDot).not.toBeNull();
|
|
193
193
|
expect(tabs[0].querySelector('.tab-dirty')).toBeNull();
|
|
194
194
|
expect(tabs[1].querySelector('.tab-dirty')).toBeNull();
|
|
195
|
+
expect(tabs[2].querySelector('.tab-dirty')).toBeNull();
|
|
195
196
|
});
|
|
196
197
|
});
|
|
197
198
|
describe('ProjectManage save mount diff', () => {
|
|
@@ -235,7 +236,7 @@ describe('ProjectManage save mount diff', () => {
|
|
|
235
236
|
await new Promise((r) => setTimeout(r, 0));
|
|
236
237
|
await tick();
|
|
237
238
|
const tabs = host.querySelectorAll('[role="tab"]');
|
|
238
|
-
tabs[
|
|
239
|
+
tabs[3].click();
|
|
239
240
|
await tick();
|
|
240
241
|
host.querySelector('[data-mount-row="assets"] input').click();
|
|
241
242
|
await tick();
|
|
@@ -313,7 +314,7 @@ describe('ProjectManage mount empty state', () => {
|
|
|
313
314
|
await new Promise((r) => setTimeout(r, 0));
|
|
314
315
|
await tick();
|
|
315
316
|
const tabs = host.querySelectorAll('[role="tab"]');
|
|
316
|
-
tabs[
|
|
317
|
+
tabs[3].click();
|
|
317
318
|
await tick();
|
|
318
319
|
expect(host.textContent).toContain('No mounts configured. Create mounts first.');
|
|
319
320
|
});
|
|
@@ -4,6 +4,7 @@ export interface ProjectRecord {
|
|
|
4
4
|
description?: string;
|
|
5
5
|
members: string[];
|
|
6
6
|
appAllowlist: string[];
|
|
7
|
+
shardAllowlist: string[];
|
|
7
8
|
createdBy: string;
|
|
8
9
|
createdAt: number;
|
|
9
10
|
updatedAt: number;
|
|
@@ -12,8 +13,8 @@ export declare const projectsApi: {
|
|
|
12
13
|
list(): Promise<ProjectRecord[]>;
|
|
13
14
|
listAll(): Promise<ProjectRecord[]>;
|
|
14
15
|
get(id: string): Promise<ProjectRecord>;
|
|
15
|
-
create(input: Pick<ProjectRecord, "name" | "description" | "members" | "appAllowlist">): Promise<ProjectRecord>;
|
|
16
|
-
update(id: string, patch: Partial<Pick<ProjectRecord, "name" | "description" | "members" | "appAllowlist">>): Promise<ProjectRecord>;
|
|
16
|
+
create(input: Pick<ProjectRecord, "name" | "description" | "members" | "appAllowlist" | "shardAllowlist">): Promise<ProjectRecord>;
|
|
17
|
+
update(id: string, patch: Partial<Pick<ProjectRecord, "name" | "description" | "members" | "appAllowlist" | "shardAllowlist">>): Promise<ProjectRecord>;
|
|
17
18
|
delete(id: string, opts?: {
|
|
18
19
|
wipeData?: boolean;
|
|
19
20
|
}): Promise<void>;
|
|
@@ -27,7 +27,7 @@ describe('projectsApi', () => {
|
|
|
27
27
|
ok: true,
|
|
28
28
|
json: async () => ({ id: 'x-1234', name: 'X', members: [], appAllowlist: [], createdBy: 'admin', createdAt: 0, updatedAt: 0 }),
|
|
29
29
|
});
|
|
30
|
-
const project = await projectsApi.create({ name: 'X', members: [], appAllowlist: [] });
|
|
30
|
+
const project = await projectsApi.create({ name: 'X', members: [], appAllowlist: [], shardAllowlist: [] });
|
|
31
31
|
expect(globalThis.fetch).toHaveBeenCalledWith('/api/projects', expect.objectContaining({ method: 'POST' }));
|
|
32
32
|
expect(project.id).toBe('x-1234');
|
|
33
33
|
});
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { type ScrollbackEntry } from '../shell-shard/scrollback.svelte';
|
|
2
|
+
import type { BrowseCapability } from '../documents/browse';
|
|
2
3
|
export interface RunVerbOpts {
|
|
3
4
|
signal?: AbortSignal;
|
|
4
5
|
structured?: unknown;
|
|
6
|
+
/**
|
|
7
|
+
* BrowseCapability to expose at `ctx.sh3.docs` inside the invoked verb.
|
|
8
|
+
* Callers (Sh3Api.runVerb closures) pass their own capability so the
|
|
9
|
+
* verb inherits the caller's documents:* gating. Undefined means no
|
|
10
|
+
* docs access — the document-touching verbs will print
|
|
11
|
+
* "document capability not available".
|
|
12
|
+
*/
|
|
13
|
+
docs?: BrowseCapability;
|
|
5
14
|
}
|
|
6
15
|
export interface RunVerbResult {
|
|
7
16
|
result: unknown;
|
package/dist/runtime/runVerb.js
CHANGED
|
@@ -51,9 +51,9 @@ export async function runVerbProgrammatic(shardId, name, args, opts) {
|
|
|
51
51
|
return { result, scrollback: captured };
|
|
52
52
|
}
|
|
53
53
|
async function buildProgrammaticContext(b) {
|
|
54
|
-
var _a, _b;
|
|
54
|
+
var _a, _b, _c;
|
|
55
55
|
const ctx = {
|
|
56
|
-
sh3: makeSh3Api({ callerKind: 'verb' }),
|
|
56
|
+
sh3: makeSh3Api({ callerKind: 'verb', docs: (_a = b.opts) === null || _a === void 0 ? void 0 : _a.docs }),
|
|
57
57
|
scrollback: b.sinkScrollback,
|
|
58
58
|
session: makeStubSession(),
|
|
59
59
|
cwd: '/',
|
|
@@ -73,8 +73,8 @@ async function buildProgrammaticContext(b) {
|
|
|
73
73
|
const innerCtx = Object.assign(Object.assign({}, ctx), { structuredArgs: undefined });
|
|
74
74
|
await resolved.verb.run(innerCtx, parts);
|
|
75
75
|
},
|
|
76
|
-
structuredArgs: (
|
|
77
|
-
signal: (
|
|
76
|
+
structuredArgs: (_b = b.opts) === null || _b === void 0 ? void 0 : _b.structured,
|
|
77
|
+
signal: (_c = b.opts) === null || _c === void 0 ? void 0 : _c.signal,
|
|
78
78
|
};
|
|
79
79
|
return ctx;
|
|
80
80
|
}
|
|
@@ -146,4 +146,33 @@ describe('runVerbProgrammatic', () => {
|
|
|
146
146
|
error: 'no-active-context',
|
|
147
147
|
});
|
|
148
148
|
});
|
|
149
|
+
it('propagates opts.docs into ctx.sh3.docs for the invoked verb', async () => {
|
|
150
|
+
let seenDocs = 'untouched';
|
|
151
|
+
registerShard({
|
|
152
|
+
manifest: { id: 'docs-probe', label: 'D', version: '0.0.0', views: [] },
|
|
153
|
+
register(ctx) {
|
|
154
|
+
ctx.registerVerb(makeVerb('peek', true, async (vctx) => {
|
|
155
|
+
seenDocs = vctx.sh3.docs;
|
|
156
|
+
}));
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
await activateShard('docs-probe');
|
|
160
|
+
const fakeBrowse = { tag: 'fake-browse' };
|
|
161
|
+
await runVerbProgrammatic('docs-probe', 'docs-probe:peek', [], { docs: fakeBrowse });
|
|
162
|
+
expect(seenDocs).toBe(fakeBrowse);
|
|
163
|
+
});
|
|
164
|
+
it('leaves ctx.sh3.docs undefined when opts.docs is not provided', async () => {
|
|
165
|
+
let seenDocs = 'untouched';
|
|
166
|
+
registerShard({
|
|
167
|
+
manifest: { id: 'docs-probe-2', label: 'D2', version: '0.0.0', views: [] },
|
|
168
|
+
register(ctx) {
|
|
169
|
+
ctx.registerVerb(makeVerb('peek', true, async (vctx) => {
|
|
170
|
+
seenDocs = vctx.sh3.docs;
|
|
171
|
+
}));
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
await activateShard('docs-probe-2');
|
|
175
|
+
await runVerbProgrammatic('docs-probe-2', 'docs-probe-2:peek', []);
|
|
176
|
+
expect(seenDocs).toBeUndefined();
|
|
177
|
+
});
|
|
149
178
|
});
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import type { Sh3Api } from '../shell-shard/registry';
|
|
2
2
|
import type { ZoneManager } from '../state/types';
|
|
3
|
+
import type { BrowseCapability } from '../documents/browse';
|
|
3
4
|
export interface MakeSh3ApiOpts {
|
|
4
5
|
callerKind: 'shard' | 'verb';
|
|
5
6
|
/** Present when callerKind === 'shard' (used by future permission gates). */
|
|
6
7
|
callerShardId?: string;
|
|
7
8
|
/** Cross-shard zone manager — passed in by ShardContext when permitted. */
|
|
8
9
|
zones?: ZoneManager;
|
|
10
|
+
/**
|
|
11
|
+
* Tenant-wide BrowseCapability for the calling shard, minted at the call
|
|
12
|
+
* site from `shard.manifest.permissions`. The factory does not gate; the
|
|
13
|
+
* caller decides whether to mint and pass one in.
|
|
14
|
+
*/
|
|
15
|
+
docs?: BrowseCapability;
|
|
9
16
|
}
|
|
10
17
|
export declare function makeSh3Api(opts?: MakeSh3ApiOpts): Sh3Api;
|
|
11
18
|
/** @deprecated Renamed to makeSh3Api(opts?). Kept for one minor cycle. */
|
package/dist/sh3Api/headless.js
CHANGED
|
@@ -52,6 +52,7 @@ export function makeSh3Api(opts) {
|
|
|
52
52
|
void (opts === null || opts === void 0 ? void 0 : opts.callerKind);
|
|
53
53
|
void (opts === null || opts === void 0 ? void 0 : opts.callerShardId);
|
|
54
54
|
const zones = opts === null || opts === void 0 ? void 0 : opts.zones;
|
|
55
|
+
const docs = opts === null || opts === void 0 ? void 0 : opts.docs;
|
|
55
56
|
function listViewsImpl() {
|
|
56
57
|
try {
|
|
57
58
|
const { root } = inspectActiveLayout();
|
|
@@ -298,7 +299,7 @@ export function makeSh3Api(opts) {
|
|
|
298
299
|
return programmaticOnly ? out.filter((v) => v.programmatic === true) : out;
|
|
299
300
|
},
|
|
300
301
|
async runVerb(shardId, name, args, runOpts) {
|
|
301
|
-
return runVerbProgrammatic(shardId, name, args, runOpts);
|
|
302
|
+
return runVerbProgrammatic(shardId, name, args, Object.assign(Object.assign({}, runOpts), { docs }));
|
|
302
303
|
},
|
|
303
304
|
listActions(actionOpts) {
|
|
304
305
|
const all = listActionsFromEntries(listActionEntriesFromRegistry(), getLiveDispatcherState());
|
|
@@ -327,6 +328,7 @@ export function makeSh3Api(opts) {
|
|
|
327
328
|
listProjects() {
|
|
328
329
|
return projectsState.projects.map((p) => ({ id: p.id, name: p.name }));
|
|
329
330
|
},
|
|
331
|
+
docs,
|
|
330
332
|
};
|
|
331
333
|
}
|
|
332
334
|
/** @deprecated Renamed to makeSh3Api(opts?). Kept for one minor cycle. */
|
|
@@ -101,3 +101,45 @@ describe('sh3Api listActions submenu filter', () => {
|
|
|
101
101
|
expect(ids).toEqual(['p:a']);
|
|
102
102
|
});
|
|
103
103
|
});
|
|
104
|
+
describe('sh3Api docs capability', () => {
|
|
105
|
+
it('exposes docs capability when passed via opts', () => {
|
|
106
|
+
const fakeBrowse = {
|
|
107
|
+
listDocuments: async () => [],
|
|
108
|
+
listShards: async () => [],
|
|
109
|
+
watchDocuments: () => () => { },
|
|
110
|
+
};
|
|
111
|
+
const api = makeSh3Api({ callerKind: 'shard', callerShardId: 'x', docs: fakeBrowse });
|
|
112
|
+
expect(api.docs).toBe(fakeBrowse);
|
|
113
|
+
});
|
|
114
|
+
it('leaves docs undefined when opts.docs is omitted', () => {
|
|
115
|
+
const api = makeSh3Api({ callerKind: 'verb' });
|
|
116
|
+
expect(api.docs).toBeUndefined();
|
|
117
|
+
});
|
|
118
|
+
it('forwards opts.docs through Sh3Api.runVerb into the invoked verb', async () => {
|
|
119
|
+
const { registerShard, activateShard, __resetShardRegistryForTest } = await import('../shards/lifecycle.svelte');
|
|
120
|
+
const { __resetViewRegistryForTest } = await import('../shards/registry');
|
|
121
|
+
const { MemoryDocumentBackend } = await import('../documents/backends');
|
|
122
|
+
const { __setDocumentBackend, __setActiveScope } = await import('../documents/config');
|
|
123
|
+
__resetShardRegistryForTest();
|
|
124
|
+
__resetViewRegistryForTest();
|
|
125
|
+
__setDocumentBackend(new MemoryDocumentBackend());
|
|
126
|
+
__setActiveScope('tenant-test');
|
|
127
|
+
let seenDocs = 'untouched';
|
|
128
|
+
registerShard({
|
|
129
|
+
manifest: { id: 'docs-via-api', label: 'D', version: '0.0.0', views: [] },
|
|
130
|
+
register(ctx) {
|
|
131
|
+
ctx.registerVerb({
|
|
132
|
+
name: 'peek',
|
|
133
|
+
summary: 'peek',
|
|
134
|
+
programmatic: true,
|
|
135
|
+
async run(vctx) { seenDocs = vctx.sh3.docs; },
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
await activateShard('docs-via-api');
|
|
140
|
+
const fakeBrowse = { tag: 'forwarded' };
|
|
141
|
+
const api = makeSh3Api({ callerKind: 'shard', callerShardId: 'docs-via-api', docs: fakeBrowse });
|
|
142
|
+
await api.runVerb('docs-via-api', 'docs-via-api:peek', []);
|
|
143
|
+
expect(seenDocs).toBe(fakeBrowse);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
<button
|
|
94
94
|
type="button"
|
|
95
95
|
class="sh3-home-card"
|
|
96
|
-
class:sh3-home-card--tinted={appearance?.color}
|
|
96
|
+
class:sh3-home-card--tinted={appearance?.color ?? manifest.color}
|
|
97
97
|
style:--card-color={appearance?.color ?? manifest.color ?? 'transparent'}
|
|
98
98
|
data-sh3-scope="element:app"
|
|
99
99
|
onclick={() => launchApp(manifest.id)}
|
|
@@ -119,8 +119,8 @@
|
|
|
119
119
|
<button
|
|
120
120
|
type="button"
|
|
121
121
|
class="sh3-home-card"
|
|
122
|
-
class:sh3-home-card--tinted={appearance?.color}
|
|
123
|
-
style:--card-color={appearance?.color ?? 'transparent'}
|
|
122
|
+
class:sh3-home-card--tinted={appearance?.color ?? manifest.color}
|
|
123
|
+
style:--card-color={appearance?.color ?? manifest.color ?? 'transparent'}
|
|
124
124
|
data-sh3-scope="element:app"
|
|
125
125
|
onclick={() => launchApp(manifest.id)}
|
|
126
126
|
oncontextmenu={(e) => openAppContextMenu(e, manifest.id)}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import type { Shard, ShardContext } from './types';
|
|
1
|
+
import type { Shard, ShardContext, ShardManifest } from './types';
|
|
2
2
|
/**
|
|
3
3
|
* Reactive registry of every shard known to the host. Keys are shard ids.
|
|
4
4
|
* Populated by `registerShard`.
|
|
5
5
|
*/
|
|
6
6
|
export declare const registeredShards: Map<string, Shard>;
|
|
7
|
+
/**
|
|
8
|
+
* Reactive snapshot of every registered shard's manifest. Mirrors
|
|
9
|
+
* `listRegisteredApps()` — used by project manage UI / ShardPicker to
|
|
10
|
+
* enumerate service-kind shards.
|
|
11
|
+
*/
|
|
12
|
+
export declare function listRegisteredShards(): ShardManifest[];
|
|
7
13
|
/**
|
|
8
14
|
* Reactive map of shard ids that failed during the lifecycle. Populated
|
|
9
15
|
* by registerAllShards and related operations.
|
|
@@ -53,7 +59,7 @@ export declare const activeShards: Map<string, Shard>;
|
|
|
53
59
|
* an already-entered shard is a no-op. Errors are recorded in
|
|
54
60
|
* `erroredShards` with phase 'register'; one failure does not block others.
|
|
55
61
|
*/
|
|
56
|
-
export declare function registerAllShards(): Promise<void>;
|
|
62
|
+
export declare function registerAllShards(allowed?: Set<string> | null): Promise<void>;
|
|
57
63
|
export declare function runAppActivate(shardId: string, appId: string): Promise<void>;
|
|
58
64
|
export declare function runAppDeactivate(shardId: string, appId: string): Promise<void>;
|
|
59
65
|
/**
|
|
@@ -17,6 +17,7 @@ import { PERMISSION_STATE_MANAGE } from '../state/types';
|
|
|
17
17
|
import { PERMISSION_DOCUMENTS_BROWSE, PERMISSION_DOCUMENTS_READ, PERMISSION_DOCUMENTS_WRITE, } from '../documents/types';
|
|
18
18
|
import { createBrowseCapability } from '../documents/browse';
|
|
19
19
|
import { createDocumentPicker } from '../documents/picker-primitive';
|
|
20
|
+
import { documentChanges } from '../documents/notifications';
|
|
20
21
|
import { createShardKeysApi } from '../keys/client';
|
|
21
22
|
import { PERMISSION_KEYS_MINT } from '../keys/types';
|
|
22
23
|
import { makeSh3Api } from '../sh3Api/headless';
|
|
@@ -29,11 +30,65 @@ import { clearSelectionForShard } from '../actions/selection.svelte';
|
|
|
29
30
|
import { fetchEnvState } from '../env/client';
|
|
30
31
|
import { subscribe as subscribeKeyRevocation } from '../keys/revocation-bus.svelte';
|
|
31
32
|
const shardAppBindings = $state(new Map());
|
|
33
|
+
/**
|
|
34
|
+
* Build the picker-primitive options for a shard: backend-backed
|
|
35
|
+
* `listFolders` so empty folders surface in the modal, a mutation `handle`
|
|
36
|
+
* that emits the right documentChanges events, and a `readOnlyShard`
|
|
37
|
+
* predicate that blocks cross-shard mutations unless the caller holds
|
|
38
|
+
* `documents:write`.
|
|
39
|
+
*/
|
|
40
|
+
function makePickerOptions(callerShardId, permissions, lockToShard) {
|
|
41
|
+
const backend = getDocumentBackend();
|
|
42
|
+
const hasWrite = permissions.includes(PERMISSION_DOCUMENTS_WRITE);
|
|
43
|
+
return {
|
|
44
|
+
listFolders: (sid, prefix) => backend.listFolders(getActiveScopeId(), sid, prefix),
|
|
45
|
+
handle: {
|
|
46
|
+
mkdir: async (sid, path) => {
|
|
47
|
+
const tid = getActiveScopeId();
|
|
48
|
+
await backend.mkdir(tid, sid, path);
|
|
49
|
+
documentChanges.emit({ type: 'folder-create', path, tenantId: tid, shardId: sid });
|
|
50
|
+
},
|
|
51
|
+
rmdir: async (sid, path, opts) => {
|
|
52
|
+
const tid = getActiveScopeId();
|
|
53
|
+
await backend.rmdir(tid, sid, path, opts);
|
|
54
|
+
documentChanges.emit({ type: 'folder-delete', path, tenantId: tid, shardId: sid });
|
|
55
|
+
},
|
|
56
|
+
renameFolder: async (sid, oldPath, newPath) => {
|
|
57
|
+
const tid = getActiveScopeId();
|
|
58
|
+
await backend.renameFolder(tid, sid, oldPath, newPath);
|
|
59
|
+
documentChanges.emit({ type: 'folder-rename', path: newPath, oldPath, tenantId: tid, shardId: sid });
|
|
60
|
+
},
|
|
61
|
+
rename: async (sid, oldPath, newPath) => {
|
|
62
|
+
const tid = getActiveScopeId();
|
|
63
|
+
await backend.rename(tid, sid, oldPath, newPath);
|
|
64
|
+
documentChanges.emit({ type: 'rename', path: newPath, oldPath, tenantId: tid, shardId: sid });
|
|
65
|
+
},
|
|
66
|
+
delete: async (sid, path) => {
|
|
67
|
+
const tid = getActiveScopeId();
|
|
68
|
+
const existed = await backend.exists(tid, sid, path);
|
|
69
|
+
await backend.delete(tid, sid, path);
|
|
70
|
+
if (existed)
|
|
71
|
+
documentChanges.emit({ type: 'delete', path, tenantId: tid, shardId: sid });
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
readOnlyShard: (sid) => sid !== callerShardId && !hasWrite,
|
|
75
|
+
initialShardId: callerShardId,
|
|
76
|
+
lockToShard,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
32
79
|
/**
|
|
33
80
|
* Reactive registry of every shard known to the host. Keys are shard ids.
|
|
34
81
|
* Populated by `registerShard`.
|
|
35
82
|
*/
|
|
36
83
|
export const registeredShards = $state(new Map());
|
|
84
|
+
/**
|
|
85
|
+
* Reactive snapshot of every registered shard's manifest. Mirrors
|
|
86
|
+
* `listRegisteredApps()` — used by project manage UI / ShardPicker to
|
|
87
|
+
* enumerate service-kind shards.
|
|
88
|
+
*/
|
|
89
|
+
export function listRegisteredShards() {
|
|
90
|
+
return Array.from(registeredShards.values()).map((s) => s.manifest);
|
|
91
|
+
}
|
|
37
92
|
export const erroredShards = $state(new Map());
|
|
38
93
|
/** Read the app id currently bound to this shard, or null. */
|
|
39
94
|
export function getShardBinding(shardId) {
|
|
@@ -65,7 +120,7 @@ export function __setScopeResolver(resolver) {
|
|
|
65
120
|
* `entry.activeAppId`); otherwise they go to the boot bag.
|
|
66
121
|
*/
|
|
67
122
|
export function buildShardContext(shard, entry) {
|
|
68
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
123
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
69
124
|
const id = shard.manifest.id;
|
|
70
125
|
function trackDisposer(fn) {
|
|
71
126
|
var _a;
|
|
@@ -200,15 +255,15 @@ export function buildShardContext(shard, entry) {
|
|
|
200
255
|
: undefined,
|
|
201
256
|
browse: browseCap,
|
|
202
257
|
documentPicker: browseCap
|
|
203
|
-
? createDocumentPicker(() => browseCap.listDocuments())
|
|
258
|
+
? createDocumentPicker(() => browseCap.listDocuments(), makePickerOptions(id, (_g = shard.manifest.permissions) !== null && _g !== void 0 ? _g : [], false))
|
|
204
259
|
: createDocumentPicker(async () => {
|
|
205
260
|
const docs = await getDocumentBackend().list(getActiveScopeId(), id);
|
|
206
261
|
return docs.map(d => (Object.assign(Object.assign({}, d), { shardId: id })));
|
|
207
|
-
}),
|
|
208
|
-
keys: ((
|
|
262
|
+
}, makePickerOptions(id, (_h = shard.manifest.permissions) !== null && _h !== void 0 ? _h : [], true)),
|
|
263
|
+
keys: ((_j = shard.manifest.permissions) === null || _j === void 0 ? void 0 : _j.includes(PERMISSION_KEYS_MINT))
|
|
209
264
|
? createShardKeysApi({
|
|
210
265
|
shardId: id,
|
|
211
|
-
shardPermissions: (
|
|
266
|
+
shardPermissions: (_k = shard.manifest.permissions) !== null && _k !== void 0 ? _k : [],
|
|
212
267
|
})
|
|
213
268
|
: undefined,
|
|
214
269
|
contributions,
|
|
@@ -225,9 +280,10 @@ export function buildShardContext(shard, entry) {
|
|
|
225
280
|
sh3: makeSh3Api({
|
|
226
281
|
callerKind: 'shard',
|
|
227
282
|
callerShardId: id,
|
|
228
|
-
zones: ((
|
|
283
|
+
zones: ((_l = shard.manifest.permissions) === null || _l === void 0 ? void 0 : _l.includes(PERMISSION_STATE_MANAGE))
|
|
229
284
|
? createZoneManager()
|
|
230
285
|
: undefined,
|
|
286
|
+
docs: browseCap,
|
|
231
287
|
}),
|
|
232
288
|
};
|
|
233
289
|
// Stash env state on the ctx for registerAllShards' hydration step.
|
|
@@ -241,10 +297,12 @@ export const activeShards = $state(new Map());
|
|
|
241
297
|
* an already-entered shard is a no-op. Errors are recorded in
|
|
242
298
|
* `erroredShards` with phase 'register'; one failure does not block others.
|
|
243
299
|
*/
|
|
244
|
-
export async function registerAllShards() {
|
|
300
|
+
export async function registerAllShards(allowed = null) {
|
|
245
301
|
for (const [id, shard] of registeredShards) {
|
|
246
302
|
if (shardEntries.has(id))
|
|
247
303
|
continue;
|
|
304
|
+
if (allowed !== null && !allowed.has(id))
|
|
305
|
+
continue;
|
|
248
306
|
const entry = {
|
|
249
307
|
shard,
|
|
250
308
|
ctx: undefined,
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import { __setActiveScope, __setDocumentBackend } from '../documents/config';
|
|
3
|
+
import { MemoryDocumentBackend } from '../documents/backends';
|
|
4
|
+
import { getShardBinding, rotateShardDocumentNamespace, __resetLifecycleForTest, registerAllShards, runAppActivate, runAppDeactivate, rebuildShardEntry, shardEntries, listRegisteredShards, } from './lifecycle.svelte';
|
|
3
5
|
import { registerShard, __resetShardRegistryForTest, erroredShards } from './lifecycle.svelte';
|
|
6
|
+
function makeShard(id, registerFn) {
|
|
7
|
+
return {
|
|
8
|
+
manifest: { id, label: id, version: '0.0.0', views: [] },
|
|
9
|
+
register: registerFn,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
4
12
|
describe('shards/lifecycle — binding map', () => {
|
|
5
13
|
beforeEach(() => __resetLifecycleForTest());
|
|
6
14
|
it('returns null for an unbound shard', () => {
|
|
@@ -137,3 +145,104 @@ describe('hot-swap on re-register', () => {
|
|
|
137
145
|
expect((_c = shardEntries.get('hot-s')) === null || _c === void 0 ? void 0 : _c.shard.manifest.version).toBe('0.0.1');
|
|
138
146
|
});
|
|
139
147
|
});
|
|
148
|
+
describe('registerAllShards with allowed-set gate', () => {
|
|
149
|
+
beforeEach(() => {
|
|
150
|
+
__resetLifecycleForTest();
|
|
151
|
+
__resetShardRegistryForTest();
|
|
152
|
+
});
|
|
153
|
+
it('skips shards not in the allowed set when one is provided', async () => {
|
|
154
|
+
const calls = [];
|
|
155
|
+
const a = makeShard('shard-a', () => { calls.push('a'); });
|
|
156
|
+
const b = makeShard('shard-b', () => { calls.push('b'); });
|
|
157
|
+
const c = makeShard('shard-c', () => { calls.push('c'); });
|
|
158
|
+
registerShard(a);
|
|
159
|
+
registerShard(b);
|
|
160
|
+
registerShard(c);
|
|
161
|
+
await registerAllShards(new Set(['shard-a', 'shard-c']));
|
|
162
|
+
expect(calls).toEqual(['a', 'c']);
|
|
163
|
+
expect(shardEntries.has('shard-a')).toBe(true);
|
|
164
|
+
expect(shardEntries.has('shard-b')).toBe(false);
|
|
165
|
+
expect(shardEntries.has('shard-c')).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
it('registers everything when allowed-set is null', async () => {
|
|
168
|
+
const calls = [];
|
|
169
|
+
const a = makeShard('shard-a', () => { calls.push('a'); });
|
|
170
|
+
const b = makeShard('shard-b', () => { calls.push('b'); });
|
|
171
|
+
registerShard(a);
|
|
172
|
+
registerShard(b);
|
|
173
|
+
await registerAllShards(null);
|
|
174
|
+
expect(calls).toEqual(['a', 'b']);
|
|
175
|
+
});
|
|
176
|
+
it('registers everything when allowed-set arg is omitted (back-compat)', async () => {
|
|
177
|
+
const calls = [];
|
|
178
|
+
const a = makeShard('shard-a', () => { calls.push('a'); });
|
|
179
|
+
registerShard(a);
|
|
180
|
+
await registerAllShards();
|
|
181
|
+
expect(calls).toEqual(['a']);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
describe('listRegisteredShards', () => {
|
|
185
|
+
beforeEach(() => {
|
|
186
|
+
__resetLifecycleForTest();
|
|
187
|
+
__resetShardRegistryForTest();
|
|
188
|
+
});
|
|
189
|
+
it('returns the manifest of every registered shard', () => {
|
|
190
|
+
var _a;
|
|
191
|
+
const a = {
|
|
192
|
+
manifest: { id: 'a', label: 'A', version: '1.0.0', views: [] },
|
|
193
|
+
register: () => { },
|
|
194
|
+
};
|
|
195
|
+
const b = {
|
|
196
|
+
manifest: { id: 'b', label: 'B', version: '1.0.0', views: [], kind: 'service' },
|
|
197
|
+
register: () => { },
|
|
198
|
+
};
|
|
199
|
+
registerShard(a);
|
|
200
|
+
registerShard(b);
|
|
201
|
+
const manifests = listRegisteredShards();
|
|
202
|
+
expect(manifests.map((m) => m.id).sort()).toEqual(['a', 'b']);
|
|
203
|
+
expect((_a = manifests.find((m) => m.id === 'b')) === null || _a === void 0 ? void 0 : _a.kind).toBe('service');
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
describe('shards/lifecycle — ctx.sh3.docs wiring', () => {
|
|
207
|
+
beforeEach(() => {
|
|
208
|
+
__resetLifecycleForTest();
|
|
209
|
+
__resetShardRegistryForTest();
|
|
210
|
+
__setDocumentBackend(new MemoryDocumentBackend());
|
|
211
|
+
__setActiveScope('tenant-test');
|
|
212
|
+
});
|
|
213
|
+
it('mirrors ctx.browse onto ctx.sh3.docs for browse-permitted shards', async () => {
|
|
214
|
+
let captured = { sh3Docs: 'untouched', ctxBrowse: 'untouched' };
|
|
215
|
+
const shard = {
|
|
216
|
+
manifest: {
|
|
217
|
+
id: 'browse-test',
|
|
218
|
+
label: 'Browse Test',
|
|
219
|
+
version: '0.0.1',
|
|
220
|
+
views: [],
|
|
221
|
+
permissions: ['documents:browse', 'documents:read', 'documents:write'],
|
|
222
|
+
},
|
|
223
|
+
register(ctx) {
|
|
224
|
+
captured = { sh3Docs: ctx.sh3.docs, ctxBrowse: ctx.browse };
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
registerShard(shard);
|
|
228
|
+
await registerAllShards();
|
|
229
|
+
expect(captured.sh3Docs).toBeDefined();
|
|
230
|
+
expect(captured.sh3Docs).toBe(captured.ctxBrowse);
|
|
231
|
+
});
|
|
232
|
+
it('leaves ctx.sh3.docs undefined when shard lacks documents:browse', async () => {
|
|
233
|
+
let docsRef = 'untouched';
|
|
234
|
+
const shard = {
|
|
235
|
+
manifest: {
|
|
236
|
+
id: 'no-browse-test',
|
|
237
|
+
label: 'No Browse',
|
|
238
|
+
version: '0.0.1',
|
|
239
|
+
views: [],
|
|
240
|
+
permissions: [],
|
|
241
|
+
},
|
|
242
|
+
register(ctx) { docsRef = ctx.sh3.docs; },
|
|
243
|
+
};
|
|
244
|
+
registerShard(shard);
|
|
245
|
+
await registerAllShards();
|
|
246
|
+
expect(docsRef).toBeUndefined();
|
|
247
|
+
});
|
|
248
|
+
});
|
package/dist/shards/types.d.ts
CHANGED
|
@@ -176,6 +176,19 @@ export interface ShardManifest {
|
|
|
176
176
|
* by sh3-validate at build time.
|
|
177
177
|
*/
|
|
178
178
|
verbNamespace?: string;
|
|
179
|
+
/**
|
|
180
|
+
* Project-allowlist classification.
|
|
181
|
+
* - 'system' : always allowed in every project scope. The shard is part
|
|
182
|
+
* of the framework/OS surface (e.g. shell-shard). Surfaced
|
|
183
|
+
* read-only in the project manage view.
|
|
184
|
+
* - 'service' : opt-in per project. Appears in the Shards tab of the
|
|
185
|
+
* project manage view; admin ticks it to grant write access.
|
|
186
|
+
* - omitted : regular shard. Reached only via an app's
|
|
187
|
+
* requiredShards / bundledShards (existing behavior).
|
|
188
|
+
*
|
|
189
|
+
* Apps never set this — shard-only.
|
|
190
|
+
*/
|
|
191
|
+
kind?: 'system' | 'service';
|
|
179
192
|
}
|
|
180
193
|
/**
|
|
181
194
|
* Source-declared shape of a shard manifest — what external package authors
|
|
@@ -28,7 +28,6 @@
|
|
|
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';
|
|
32
31
|
|
|
33
32
|
interface Props {
|
|
34
33
|
shell: Sh3Api;
|
|
@@ -36,9 +35,8 @@
|
|
|
36
35
|
userId: string;
|
|
37
36
|
role: ShellRole;
|
|
38
37
|
contributions: ContributionsApi;
|
|
39
|
-
docs?: BrowseCapability;
|
|
40
38
|
}
|
|
41
|
-
let { shell, wsUrl, userId, role, contributions
|
|
39
|
+
let { shell, wsUrl, userId, role, contributions }: Props = $props();
|
|
42
40
|
|
|
43
41
|
// Per-mode buffer map. Each ModeBuffer bundles a Scrollback + history +
|
|
44
42
|
// locked flag and is materialized lazily on first switch into that mode.
|
|
@@ -252,7 +250,6 @@
|
|
|
252
250
|
session,
|
|
253
251
|
sh3: shellWithModes,
|
|
254
252
|
fs,
|
|
255
|
-
docs,
|
|
256
253
|
cwd: () => session.cwd,
|
|
257
254
|
busy: acquireBusy,
|
|
258
255
|
customMode: (id: string) => contributedModes.find((d) => d.id === id) ?? null,
|
|
@@ -1,14 +1,12 @@
|
|
|
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';
|
|
5
4
|
interface Props {
|
|
6
5
|
shell: Sh3Api;
|
|
7
6
|
wsUrl: string;
|
|
8
7
|
userId: string;
|
|
9
8
|
role: ShellRole;
|
|
10
9
|
contributions: ContributionsApi;
|
|
11
|
-
docs?: BrowseCapability;
|
|
12
10
|
}
|
|
13
11
|
declare const Terminal: import("svelte").Component<Props, {}, "">;
|
|
14
12
|
type Terminal = ReturnType<typeof Terminal>;
|