sh3-core 0.22.2 → 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 +13 -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/Field.svelte +5 -0
- package/dist/primitives/widgets/Field.svelte.d.ts +1 -0
- package/dist/primitives/widgets/Field.svelte.test.js +16 -0
- package/dist/primitives/widgets/NumberInput.svelte +21 -12
- package/dist/primitives/widgets/NumberInput.svelte.d.ts +2 -0
- package/dist/primitives/widgets/NumberInput.svelte.test.js +26 -0
- package/dist/primitives/widgets/ShardPicker.svelte +38 -0
- package/dist/primitives/widgets/ShardPicker.svelte.d.ts +9 -0
- package/dist/primitives/widgets/Textarea.svelte +5 -0
- package/dist/primitives/widgets/Textarea.svelte.d.ts +1 -0
- package/dist/primitives/widgets/Textarea.svelte.test.js +16 -0
- package/dist/primitives/widgets/_DocumentBrowser.svelte +11 -3
- package/dist/primitives/widgets/_DocumentBrowser.svelte.d.ts +2 -0
- package/dist/primitives/widgets/_selectOnFocus.d.ts +15 -0
- package/dist/primitives/widgets/_selectOnFocus.js +24 -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 +5 -4
- 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
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Project-scope register() gate — pure resolution of the shard ids that
|
|
3
|
+
* may register at boot under a given project's allowlist. Mirrors the
|
|
4
|
+
* server-side `project-allowlist` middleware so client and server agree
|
|
5
|
+
* on the closure.
|
|
6
|
+
*
|
|
7
|
+
* Returns `null` (no gating) only when the project is null (personal scope)
|
|
8
|
+
* OR when both `appAllowlist` and `shardAllowlist` are empty. Otherwise the
|
|
9
|
+
* allowed set is the union of:
|
|
10
|
+
* - system-kind shards (always allowed)
|
|
11
|
+
* - the resolved `requiredShards`/`bundledShards` of each allowlisted app
|
|
12
|
+
* - service-kind shards listed in `project.shardAllowlist`
|
|
13
|
+
*
|
|
14
|
+
* The set of system-kind shards is derived at call time from the
|
|
15
|
+
* registered-shards map; mark framework shards with `kind: 'system'` in
|
|
16
|
+
* their manifest to keep them reachable inside any project.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveAllowedShardIds(project, apps, shardManifests) {
|
|
19
|
+
var _a;
|
|
20
|
+
if (!project)
|
|
21
|
+
return null;
|
|
22
|
+
if (project.appAllowlist.length === 0 && project.shardAllowlist.length === 0)
|
|
23
|
+
return null;
|
|
24
|
+
const allowed = new Set();
|
|
25
|
+
const systemShardIds = new Set();
|
|
26
|
+
const serviceShardIds = new Set();
|
|
27
|
+
for (const m of shardManifests) {
|
|
28
|
+
if (m.kind === 'system') {
|
|
29
|
+
allowed.add(m.id);
|
|
30
|
+
systemShardIds.add(m.id);
|
|
31
|
+
}
|
|
32
|
+
else if (m.kind === 'service') {
|
|
33
|
+
serviceShardIds.add(m.id);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
for (const appId of project.appAllowlist) {
|
|
37
|
+
const app = apps.get(appId);
|
|
38
|
+
if (!app)
|
|
39
|
+
continue;
|
|
40
|
+
allowed.add(appId);
|
|
41
|
+
for (const s of app.manifest.requiredShards)
|
|
42
|
+
allowed.add(s);
|
|
43
|
+
for (const s of (_a = app.manifest.bundledShards) !== null && _a !== void 0 ? _a : [])
|
|
44
|
+
allowed.add(s);
|
|
45
|
+
}
|
|
46
|
+
for (const shardId of project.shardAllowlist) {
|
|
47
|
+
if (serviceShardIds.has(shardId))
|
|
48
|
+
allowed.add(shardId);
|
|
49
|
+
}
|
|
50
|
+
return allowed;
|
|
51
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { resolveAllowedShardIds } from './scope-gate';
|
|
3
|
+
function makeApp(id, requiredShards = [], bundledShards) {
|
|
4
|
+
const manifest = Object.assign({ id, label: id, version: '0.0.0', requiredShards, layoutVersion: 1 }, (bundledShards ? { bundledShards } : {}));
|
|
5
|
+
return { manifest, initialLayout: { kind: 'leaf', viewId: 'x' } };
|
|
6
|
+
}
|
|
7
|
+
function makeProject(appAllowlist, shardAllowlist = []) {
|
|
8
|
+
return {
|
|
9
|
+
id: 'acme-1234',
|
|
10
|
+
name: 'Acme',
|
|
11
|
+
members: ['u1'],
|
|
12
|
+
appAllowlist,
|
|
13
|
+
shardAllowlist,
|
|
14
|
+
createdBy: 'u1',
|
|
15
|
+
createdAt: 0,
|
|
16
|
+
updatedAt: 0,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const SYSTEM_SHARDS = [
|
|
20
|
+
{ id: 'shell', label: 'Shell', version: '0', kind: 'system', views: [] },
|
|
21
|
+
{ id: '__sh3core__', label: 'SH3 Core', version: '0', kind: 'system', views: [] },
|
|
22
|
+
];
|
|
23
|
+
describe('resolveAllowedShardIds', () => {
|
|
24
|
+
it('returns null (no gating) when project is null', () => {
|
|
25
|
+
expect(resolveAllowedShardIds(null, new Map(), SYSTEM_SHARDS)).toBeNull();
|
|
26
|
+
});
|
|
27
|
+
it('returns null (no gating) when both allowlists are empty', () => {
|
|
28
|
+
const project = makeProject([], []);
|
|
29
|
+
expect(resolveAllowedShardIds(project, new Map(), SYSTEM_SHARDS)).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
it('returns system-kind shards only when allowlist contains no resolvable apps', () => {
|
|
32
|
+
const project = makeProject(['ghost-app']);
|
|
33
|
+
const apps = new Map();
|
|
34
|
+
const allowed = resolveAllowedShardIds(project, apps, SYSTEM_SHARDS);
|
|
35
|
+
expect(allowed.has('shell')).toBe(true);
|
|
36
|
+
expect(allowed.has('__sh3core__')).toBe(true);
|
|
37
|
+
expect(allowed.has('ghost-app')).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
it('includes app id + requiredShards for each resolved app, plus system shards', () => {
|
|
40
|
+
const project = makeProject(['notes']);
|
|
41
|
+
const apps = new Map([
|
|
42
|
+
['notes', makeApp('notes', ['notes-shard', 'editor'])],
|
|
43
|
+
]);
|
|
44
|
+
const allowed = resolveAllowedShardIds(project, apps, SYSTEM_SHARDS);
|
|
45
|
+
expect(allowed.has('shell')).toBe(true);
|
|
46
|
+
expect(allowed.has('__sh3core__')).toBe(true);
|
|
47
|
+
expect(allowed.has('notes')).toBe(true);
|
|
48
|
+
expect(allowed.has('notes-shard')).toBe(true);
|
|
49
|
+
expect(allowed.has('editor')).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
it('includes bundledShards when the app declares them', () => {
|
|
52
|
+
const project = makeProject(['guml-ide']);
|
|
53
|
+
const apps = new Map([
|
|
54
|
+
['guml-ide', makeApp('guml-ide', ['guml.core'], ['guml.preview'])],
|
|
55
|
+
]);
|
|
56
|
+
const allowed = resolveAllowedShardIds(project, apps, SYSTEM_SHARDS);
|
|
57
|
+
expect(allowed.has('guml.core')).toBe(true);
|
|
58
|
+
expect(allowed.has('guml.preview')).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
it('unions across multiple allowlisted apps', () => {
|
|
61
|
+
const project = makeProject(['notes', 'files']);
|
|
62
|
+
const apps = new Map([
|
|
63
|
+
['notes', makeApp('notes', ['notes-shard'])],
|
|
64
|
+
['files', makeApp('files', ['files-shard'])],
|
|
65
|
+
]);
|
|
66
|
+
const allowed = resolveAllowedShardIds(project, apps, SYSTEM_SHARDS);
|
|
67
|
+
expect(allowed.has('notes-shard')).toBe(true);
|
|
68
|
+
expect(allowed.has('files-shard')).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
it('includes service-kind shards listed in shardAllowlist', () => {
|
|
71
|
+
const project = makeProject([], ['svc-a']);
|
|
72
|
+
const manifests = [
|
|
73
|
+
...SYSTEM_SHARDS,
|
|
74
|
+
{ id: 'svc-a', label: 'Svc A', version: '0', kind: 'service', views: [] },
|
|
75
|
+
{ id: 'svc-b', label: 'Svc B', version: '0', kind: 'service', views: [] },
|
|
76
|
+
];
|
|
77
|
+
const allowed = resolveAllowedShardIds(project, new Map(), manifests);
|
|
78
|
+
expect(allowed.has('svc-a')).toBe(true);
|
|
79
|
+
expect(allowed.has('svc-b')).toBe(false);
|
|
80
|
+
expect(allowed.has('shell')).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
it('ignores shardAllowlist entries that are not service-kind', () => {
|
|
83
|
+
const project = makeProject([], ['not-a-service']);
|
|
84
|
+
const manifests = [
|
|
85
|
+
...SYSTEM_SHARDS,
|
|
86
|
+
{ id: 'not-a-service', label: 'X', version: '0', views: [] },
|
|
87
|
+
];
|
|
88
|
+
const allowed = resolveAllowedShardIds(project, new Map(), manifests);
|
|
89
|
+
expect(allowed.has('not-a-service')).toBe(false);
|
|
90
|
+
expect(allowed.has('shell')).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
import { refreshProjects } from './projectsShard.svelte';
|
|
14
14
|
import { getUser } from '../auth/auth.svelte';
|
|
15
15
|
import AppPicker from '../primitives/widgets/AppPicker.svelte';
|
|
16
|
+
import ShardPicker from '../primitives/widgets/ShardPicker.svelte';
|
|
16
17
|
import UserPicker from '../primitives/widgets/UserPicker.svelte';
|
|
18
|
+
import { listRegisteredShards } from '../shards/lifecycle.svelte';
|
|
17
19
|
import TabbedPanel from '../primitives/TabbedPanel.svelte';
|
|
18
20
|
import { modalManager } from '../overlays/modal';
|
|
19
21
|
import DeleteProjectDialog from './DeleteProjectDialog.svelte';
|
|
@@ -50,6 +52,9 @@
|
|
|
50
52
|
let appAllowlist = $state<string[]>(
|
|
51
53
|
untrack(() => (project ? [...project.appAllowlist] : [])),
|
|
52
54
|
);
|
|
55
|
+
let shardAllowlist = $state<string[]>(
|
|
56
|
+
untrack(() => (project ? [...(project.shardAllowlist ?? [])] : [])),
|
|
57
|
+
);
|
|
53
58
|
let saving = $state(false);
|
|
54
59
|
let error = $state<string | null>(null);
|
|
55
60
|
let activeTab = $state(0);
|
|
@@ -58,6 +63,9 @@
|
|
|
58
63
|
let mountsLoading = $state(true);
|
|
59
64
|
let mountsLoadError = $state<string | null>(null);
|
|
60
65
|
let baselineApps = $state<string[]>(untrack(() => (project ? [...project.appAllowlist] : [])));
|
|
66
|
+
let baselineShards = $state<string[]>(
|
|
67
|
+
untrack(() => (project ? [...(project.shardAllowlist ?? [])] : [])),
|
|
68
|
+
);
|
|
61
69
|
let baselineMembers = $state<string[]>(
|
|
62
70
|
untrack(() => {
|
|
63
71
|
if (project) return [...project.members];
|
|
@@ -80,9 +88,16 @@
|
|
|
80
88
|
}
|
|
81
89
|
|
|
82
90
|
const appsDirty = $derived(!arrayEq(appAllowlist, baselineApps));
|
|
91
|
+
const shardsDirty = $derived(!arrayEq(shardAllowlist, baselineShards));
|
|
83
92
|
const membersDirty = $derived(!arrayEq(members, baselineMembers));
|
|
84
93
|
const mountsDirty = $derived(!setEq(mountAttached, baselineMounts));
|
|
85
94
|
|
|
95
|
+
const systemShards = $derived(
|
|
96
|
+
listRegisteredShards()
|
|
97
|
+
.filter((m) => m.kind === 'system')
|
|
98
|
+
.sort((a, b) => a.label.localeCompare(b.label)),
|
|
99
|
+
);
|
|
100
|
+
|
|
86
101
|
async function save() {
|
|
87
102
|
if (!name.trim()) {
|
|
88
103
|
error = 'Name is required';
|
|
@@ -95,6 +110,7 @@
|
|
|
95
110
|
description: description.trim() || undefined,
|
|
96
111
|
members,
|
|
97
112
|
appAllowlist,
|
|
113
|
+
shardAllowlist,
|
|
98
114
|
};
|
|
99
115
|
try {
|
|
100
116
|
const saved = isEdit && project
|
|
@@ -234,10 +250,10 @@
|
|
|
234
250
|
|
|
235
251
|
<div class="tabs">
|
|
236
252
|
<TabbedPanel
|
|
237
|
-
labels={['Apps', 'Users', 'Mounts']}
|
|
253
|
+
labels={['Apps', 'Shards', 'Users', 'Mounts']}
|
|
238
254
|
{activeTab}
|
|
239
255
|
onActiveChange={(i) => (activeTab = i)}
|
|
240
|
-
dirty={[appsDirty, membersDirty, mountsDirty]}
|
|
256
|
+
dirty={[appsDirty, shardsDirty, membersDirty, mountsDirty]}
|
|
241
257
|
body={tabBody}
|
|
242
258
|
/>
|
|
243
259
|
</div>
|
|
@@ -265,6 +281,19 @@
|
|
|
265
281
|
<AppPicker bind:value={appAllowlist} disabled={saving} />
|
|
266
282
|
</label>
|
|
267
283
|
{:else if i === 1}
|
|
284
|
+
<label class="field">
|
|
285
|
+
<span>Services</span>
|
|
286
|
+
<ShardPicker bind:value={shardAllowlist} disabled={saving} />
|
|
287
|
+
</label>
|
|
288
|
+
<details class="system-block">
|
|
289
|
+
<summary>System (always allowed)</summary>
|
|
290
|
+
<ul class="system-list">
|
|
291
|
+
{#each systemShards as m (m.id)}
|
|
292
|
+
<li><span class="system-label">{m.label}</span><span class="system-id">{m.id}</span></li>
|
|
293
|
+
{/each}
|
|
294
|
+
</ul>
|
|
295
|
+
</details>
|
|
296
|
+
{:else if i === 2}
|
|
268
297
|
<label class="field">
|
|
269
298
|
<span>Members</span>
|
|
270
299
|
<UserPicker bind:value={members} disabled={saving} />
|
|
@@ -385,4 +414,15 @@
|
|
|
385
414
|
}
|
|
386
415
|
.actions button.danger { margin-left: auto; color: var(--sh3-error, #c33); }
|
|
387
416
|
.actions button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
417
|
+
.system-block { margin-top: 16px; font-size: 13px; }
|
|
418
|
+
.system-block summary {
|
|
419
|
+
cursor: pointer;
|
|
420
|
+
color: var(--sh3-fg-muted);
|
|
421
|
+
padding: 4px 0;
|
|
422
|
+
user-select: none;
|
|
423
|
+
}
|
|
424
|
+
.system-list { list-style: none; margin: 4px 0 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
|
|
425
|
+
.system-list li { display: flex; justify-content: space-between; gap: 8px; color: var(--sh3-fg-muted); padding: 4px 0; }
|
|
426
|
+
.system-id { font-size: 11px; font-family: var(--sh3-font-mono, monospace); }
|
|
427
|
+
.system-label { font-weight: 500; }
|
|
388
428
|
</style>
|
|
@@ -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
|
+
});
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { makeSelectionApi } from '../actions/selection.svelte';
|
|
19
19
|
import { getAppearance } from '../app-appearance';
|
|
20
20
|
import iconsUrl from '../assets/icons.svg';
|
|
21
|
+
import { manifest } from '../shell-shard/manifest';
|
|
21
22
|
|
|
22
23
|
const homeSelection = makeSelectionApi('__sh3core__');
|
|
23
24
|
|
|
@@ -92,8 +93,8 @@
|
|
|
92
93
|
<button
|
|
93
94
|
type="button"
|
|
94
95
|
class="sh3-home-card"
|
|
95
|
-
class:sh3-home-card--tinted={appearance?.color}
|
|
96
|
-
style:--card-color={appearance?.color ?? 'transparent'}
|
|
96
|
+
class:sh3-home-card--tinted={appearance?.color ?? manifest.color}
|
|
97
|
+
style:--card-color={appearance?.color ?? manifest.color ?? 'transparent'}
|
|
97
98
|
data-sh3-scope="element:app"
|
|
98
99
|
onclick={() => launchApp(manifest.id)}
|
|
99
100
|
oncontextmenu={(e) => openAppContextMenu(e, manifest.id)}
|
|
@@ -118,8 +119,8 @@
|
|
|
118
119
|
<button
|
|
119
120
|
type="button"
|
|
120
121
|
class="sh3-home-card"
|
|
121
|
-
class:sh3-home-card--tinted={appearance?.color}
|
|
122
|
-
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'}
|
|
123
124
|
data-sh3-scope="element:app"
|
|
124
125
|
onclick={() => launchApp(manifest.id)}
|
|
125
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
|
/**
|