sh3-core 0.22.0 → 0.22.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 (80) hide show
  1. package/dist/__test__/fixtures.js +1 -1
  2. package/dist/__test__/reset.js +1 -3
  3. package/dist/__test__/smoke.test.js +2 -2
  4. package/dist/actions/contextMenuModel.test.js +6 -3
  5. package/dist/actions/ctx-actions.svelte.test.js +9 -9
  6. package/dist/actions/dispatcher-v3.test.js +8 -0
  7. package/dist/actions/dispatcher.svelte.d.ts +1 -2
  8. package/dist/actions/dispatcher.svelte.js +6 -7
  9. package/dist/actions/dispatcher.test.js +9 -12
  10. package/dist/actions/listActionsFromEntries.test.js +1 -2
  11. package/dist/actions/listActive.test.js +2 -3
  12. package/dist/actions/menuBarModel.test.js +1 -7
  13. package/dist/actions/paletteModel.test.js +1 -3
  14. package/dist/actions/scope-helpers.test.js +4 -4
  15. package/dist/actions/shardContext.test.js +2 -2
  16. package/dist/actions/state.svelte.d.ts +12 -2
  17. package/dist/actions/state.svelte.js +15 -12
  18. package/dist/actions/state.test.js +4 -4
  19. package/dist/api.d.ts +4 -3
  20. package/dist/api.js +1 -1
  21. package/dist/app/admin/adminShard.svelte.js +1 -1
  22. package/dist/app/store/storeShard.svelte.js +10 -5
  23. package/dist/app-appearance/appearanceShard.svelte.js +1 -5
  24. package/dist/apps/lifecycle.js +49 -64
  25. package/dist/apps/lifecycle.test.js +30 -76
  26. package/dist/conflicts/adapter-documents.js +1 -2
  27. package/dist/createShell.js +1 -1
  28. package/dist/documents/handle.d.ts +9 -4
  29. package/dist/documents/handle.js +40 -29
  30. package/dist/documents/handle.test.js +60 -51
  31. package/dist/documents/index.d.ts +1 -1
  32. package/dist/documents/types.d.ts +16 -26
  33. package/dist/host.d.ts +1 -1
  34. package/dist/host.js +9 -56
  35. package/dist/host.svelte.test.js +31 -63
  36. package/dist/layouts-shard/LayoutsSection.svelte +1 -1
  37. package/dist/layouts-shard/layoutsShard.svelte.js +2 -5
  38. package/dist/layouts-shard/layoutsShard.svelte.test.js +2 -2
  39. package/dist/projects-shard/projectsShard.svelte.js +1 -5
  40. package/dist/registry/installer.js +1 -1
  41. package/dist/registry/loader.d.ts +1 -1
  42. package/dist/registry/loader.js +3 -3
  43. package/dist/registry/permission-descriptions.test.js +2 -2
  44. package/dist/registry/register.js +1 -1
  45. package/dist/registry/register.test.js +1 -1
  46. package/dist/runtime/runVerb-shell.test.js +1 -1
  47. package/dist/runtime/runVerb.js +2 -2
  48. package/dist/runtime/runVerb.test.js +9 -9
  49. package/dist/server-shard/types.d.ts +56 -0
  50. package/dist/sh3Api/headless.js +1 -1
  51. package/dist/sh3core-shard/sh3coreShard.svelte.js +1 -6
  52. package/dist/shards/ctx-fetch.test.js +9 -9
  53. package/dist/shards/lifecycle.svelte.d.ts +108 -0
  54. package/dist/shards/lifecycle.svelte.js +551 -0
  55. package/dist/shards/lifecycle.test.js +139 -0
  56. package/dist/shards/types.d.ts +30 -63
  57. package/dist/shell-shard/shellShard.svelte.js +1 -4
  58. package/dist/version.d.ts +1 -1
  59. package/dist/version.js +1 -1
  60. package/package.json +1 -1
  61. package/dist/shards/activate-browse.test.js +0 -120
  62. package/dist/shards/activate-contributions.test.js +0 -141
  63. package/dist/shards/activate-error-isolation.test.d.ts +0 -1
  64. package/dist/shards/activate-error-isolation.test.js +0 -98
  65. package/dist/shards/activate-fields.svelte.test.d.ts +0 -1
  66. package/dist/shards/activate-fields.svelte.test.js +0 -121
  67. package/dist/shards/activate-on-key-revoked.test.d.ts +0 -1
  68. package/dist/shards/activate-on-key-revoked.test.js +0 -60
  69. package/dist/shards/activate-runtime.test.d.ts +0 -1
  70. package/dist/shards/activate-runtime.test.js +0 -344
  71. package/dist/shards/activate-scopeid.test.d.ts +0 -1
  72. package/dist/shards/activate-scopeid.test.js +0 -21
  73. package/dist/shards/activate.svelte.d.ts +0 -102
  74. package/dist/shards/activate.svelte.js +0 -407
  75. package/dist/shards/app-binding.svelte.d.ts +0 -8
  76. package/dist/shards/app-binding.svelte.js +0 -30
  77. package/dist/shards/app-binding.test.d.ts +0 -1
  78. package/dist/shards/app-binding.test.js +0 -25
  79. /package/dist/{shards/activate-browse.test.d.ts → actions/dispatcher-v3.test.d.ts} +0 -0
  80. /package/dist/shards/{activate-contributions.test.d.ts → lifecycle.test.d.ts} +0 -0
@@ -4,7 +4,7 @@ import { createDocumentHandle } from './handle';
4
4
  import { documentChanges } from './notifications';
5
5
  function harness() {
6
6
  const backend = new MemoryDocumentBackend();
7
- const handle = createDocumentHandle('tenant1', 'shard1', backend, { format: 'text' });
7
+ const handle = createDocumentHandle('tenant1', 'shard1', backend);
8
8
  return { backend, handle };
9
9
  }
10
10
  describe('DocumentHandle.status()', () => {
@@ -14,7 +14,7 @@ describe('DocumentHandle.status()', () => {
14
14
  });
15
15
  it('returns DocStatus after a write', async () => {
16
16
  const { handle } = harness();
17
- await handle.write('a.txt', 'hi');
17
+ await handle.writeText('a.txt', 'hi');
18
18
  const s = await handle.status('a.txt');
19
19
  expect(s).toMatchObject({ exists: true, version: 1, syncState: 'synced' });
20
20
  });
@@ -33,7 +33,7 @@ describe('DocumentHandle.status()', () => {
33
33
  async renameFolder() { },
34
34
  async listFolders() { return []; },
35
35
  };
36
- const handle = createDocumentHandle('t', 's', backend, { format: 'text' });
36
+ const handle = createDocumentHandle('t', 's', backend);
37
37
  await expect(handle.status('a.txt')).rejects.toThrow(/status/);
38
38
  });
39
39
  });
@@ -55,7 +55,7 @@ describe('DocumentHandle.resolveConflict()', () => {
55
55
  async renameFolder() { },
56
56
  async listFolders() { return []; },
57
57
  };
58
- const handle = createDocumentHandle('tenant1', 'shard1', backend, { format: 'text' });
58
+ const handle = createDocumentHandle('tenant1', 'shard1', backend);
59
59
  await handle.resolveConflict('a.txt', 'local');
60
60
  expect(resolved).toEqual([{ t: 'tenant1', s: 'shard1', p: 'a.txt', c: 'local' }]);
61
61
  });
@@ -82,7 +82,7 @@ describe('DocumentHandle.readBranch()', () => {
82
82
  async renameFolder() { },
83
83
  async listFolders() { return []; },
84
84
  };
85
- const handle = createDocumentHandle('tenant1', 'shard1', backend, { format: 'text' });
85
+ const handle = createDocumentHandle('tenant1', 'shard1', backend);
86
86
  const out = await handle.readBranch('a.txt', 'peer-1');
87
87
  expect(out).toBe('remote-content');
88
88
  expect(calls[0]).toEqual(['tenant1', 'shard1', 'a.txt', 'peer-1']);
@@ -103,7 +103,7 @@ describe('DocumentHandle.readBranch()', () => {
103
103
  async renameFolder() { },
104
104
  async listFolders() { return []; },
105
105
  };
106
- const handle = createDocumentHandle('t', 's', backend, { format: 'text' });
106
+ const handle = createDocumentHandle('t', 's', backend);
107
107
  expect(await handle.readBranch('a.txt', 'peer-1')).toBeNull();
108
108
  });
109
109
  it('throws if the backend does not implement readBranch', async () => {
@@ -114,7 +114,7 @@ describe('DocumentHandle.readBranch()', () => {
114
114
  describe('DocumentHandle.rename', () => {
115
115
  it('moves the doc and emits one rename event', async () => {
116
116
  const { backend, handle } = harness();
117
- await handle.write('old.txt', 'hello');
117
+ await handle.writeText('old.txt', 'hello');
118
118
  const events = [];
119
119
  const unsub = (await import('./notifications')).documentChanges.subscribe((c) => events.push(c));
120
120
  await handle.rename('old.txt', 'new.txt');
@@ -131,7 +131,7 @@ describe('DocumentHandle.rename', () => {
131
131
  });
132
132
  it('throws when an autosave controller is active on oldPath', async () => {
133
133
  const { handle } = harness();
134
- await handle.write('old.txt', 'v1');
134
+ await handle.writeText('old.txt', 'v1');
135
135
  const ctrl = handle.autosave('old.txt', { debounceMs: 10000 });
136
136
  ctrl.update('v2');
137
137
  await expect(handle.rename('old.txt', 'new.txt'))
@@ -140,23 +140,15 @@ describe('DocumentHandle.rename', () => {
140
140
  });
141
141
  it('does not throw when an autosave controller exists for an unrelated path', async () => {
142
142
  const { handle } = harness();
143
- await handle.write('old.txt', 'src');
144
- await handle.write('other.txt', 'unrelated');
143
+ await handle.writeText('old.txt', 'src');
144
+ await handle.writeText('other.txt', 'unrelated');
145
145
  const ctrl = handle.autosave('other.txt', { debounceMs: 10000 });
146
146
  ctrl.update('still-unrelated');
147
147
  await expect(handle.rename('old.txt', 'new.txt')).resolves.toBeUndefined();
148
148
  await ctrl.dispose();
149
149
  });
150
- it('throws when newPath violates the handle extensions filter', async () => {
151
- const backend = new MemoryDocumentBackend();
152
- const handle = createDocumentHandle('t1', 's1', backend, {
153
- format: 'text',
154
- extensions: ['.txt'],
155
- });
156
- await handle.write('a.txt', 'hi');
157
- await expect(handle.rename('a.txt', 'a.md'))
158
- .rejects.toThrow(/extensions/);
159
- });
150
+ // v3: handle extensions filter is gone. Rename allows any path; per-format
151
+ // restrictions are now caller-side via the appropriate readX/writeX method.
160
152
  });
161
153
  describe('DocumentHandle.delete()', () => {
162
154
  it('emits a delete event when the path existed', async () => {
@@ -185,23 +177,23 @@ describe('DocumentHandle folder ops', () => {
185
177
  let handle;
186
178
  beforeEach(() => {
187
179
  backend = new MemoryDocumentBackend();
188
- handle = createDocumentHandle('t', 's', backend, { format: 'text' });
180
+ handle = createDocumentHandle('t', 's', backend);
189
181
  });
190
182
  it('mkdir forwards to backend with bound tenant/shard', async () => {
191
183
  await handle.mkdir('a');
192
184
  expect(await backend.listFolders('t', 's', '')).toEqual(['a']);
193
185
  });
194
186
  it('rmdir defaults recursive to false', async () => {
195
- await handle.write('a/x.md', 'x');
187
+ await handle.writeText('a/x.md', 'x');
196
188
  await expect(handle.rmdir('a')).rejects.toThrow();
197
189
  });
198
190
  it('rmdir({recursive:true}) cascades', async () => {
199
- await handle.write('a/x.md', 'x');
191
+ await handle.writeText('a/x.md', 'x');
200
192
  await handle.rmdir('a', { recursive: true });
201
193
  expect(await handle.list()).toEqual([]);
202
194
  });
203
195
  it('renameFolder rewrites descendant paths', async () => {
204
- await handle.write('old/x.md', 'x');
196
+ await handle.writeText('old/x.md', 'x');
205
197
  await handle.renameFolder('old', 'new');
206
198
  const docs = (await handle.list()).map((d) => d.path).sort();
207
199
  expect(docs).toEqual(['new/x.md']);
@@ -232,8 +224,8 @@ describe('DocumentHandle folder ops', () => {
232
224
  ]);
233
225
  });
234
226
  it('rmdir(recursive) emits a single folder-delete event', async () => {
235
- await handle.write('a/x.md', 'x');
236
- await handle.write('a/y.md', 'y');
227
+ await handle.writeText('a/x.md', 'x');
228
+ await handle.writeText('a/y.md', 'y');
237
229
  const events = [];
238
230
  handle.watch((c) => events.push(c));
239
231
  await handle.rmdir('a', { recursive: true });
@@ -242,7 +234,7 @@ describe('DocumentHandle folder ops', () => {
242
234
  ]);
243
235
  });
244
236
  it('renameFolder emits a single folder-rename event', async () => {
245
- await handle.write('old/x.md', 'x');
237
+ await handle.writeText('old/x.md', 'x');
246
238
  const events = [];
247
239
  handle.watch((c) => events.push(c));
248
240
  await handle.renameFolder('old', 'new');
@@ -251,20 +243,11 @@ describe('DocumentHandle folder ops', () => {
251
243
  ]);
252
244
  });
253
245
  });
254
- describe('createDocumentHandle — appId override', () => {
255
- it('uses appId as the namespace root when provided', async () => {
256
- const backend = new MemoryDocumentBackend();
257
- const handle = createDocumentHandle('tenant-1', 'sh3-editor', backend, { format: 'text', appId: 'guml-ide' });
258
- await handle.write('test.guml', 'content');
259
- const keysAppId = await backend.list('tenant-1', 'guml-ide');
260
- expect(keysAppId.some((k) => k.path === 'test.guml')).toBe(true);
261
- const keysShardId = await backend.list('tenant-1', 'sh3-editor');
262
- expect(keysShardId).toHaveLength(0);
263
- });
264
- it('falls back to shardId when appId is not provided', async () => {
246
+ describe('createDocumentHandle — namespace resolution', () => {
247
+ it('uses the shard/namespace string as the namespace root', async () => {
265
248
  const backend = new MemoryDocumentBackend();
266
- const handle = createDocumentHandle('tenant-1', 'sh3-editor', backend, { format: 'text' });
267
- await handle.write('test.guml', 'content');
249
+ const handle = createDocumentHandle('tenant-1', 'sh3-editor', backend);
250
+ await handle.writeText('test.guml', 'content');
268
251
  const keys = await backend.list('tenant-1', 'sh3-editor');
269
252
  expect(keys.some((k) => k.path === 'test.guml')).toBe(true);
270
253
  });
@@ -274,11 +257,11 @@ describe('createDocumentHandle — lazy resolvers', () => {
274
257
  const { MemoryDocumentBackend } = await import('./backends');
275
258
  const backend = new MemoryDocumentBackend();
276
259
  let ns = 'shard-A';
277
- const handle = createDocumentHandle('local', () => ns, backend, { format: 'text' });
278
- await handle.write('foo.txt', 'first');
260
+ const handle = createDocumentHandle('local', () => ns, backend);
261
+ await handle.writeText('foo.txt', 'first');
279
262
  expect((await backend.list('local', 'shard-A')).map(d => d.path)).toContain('foo.txt');
280
263
  ns = 'app-X';
281
- await handle.write('bar.txt', 'second');
264
+ await handle.writeText('bar.txt', 'second');
282
265
  expect((await backend.list('local', 'app-X')).map(d => d.path)).toContain('bar.txt');
283
266
  expect((await backend.list('local', 'shard-A')).map(d => d.path)).not.toContain('bar.txt');
284
267
  });
@@ -286,31 +269,57 @@ describe('createDocumentHandle — lazy resolvers', () => {
286
269
  const { MemoryDocumentBackend } = await import('./backends');
287
270
  const backend = new MemoryDocumentBackend();
288
271
  let tenant = 'alice';
289
- const handle = createDocumentHandle(() => tenant, () => 'shard-A', backend, { format: 'text' });
290
- await handle.write('p.txt', 'a');
272
+ const handle = createDocumentHandle(() => tenant, () => 'shard-A', backend);
273
+ await handle.writeText('p.txt', 'a');
291
274
  expect((await backend.list('alice', 'shard-A')).map(d => d.path)).toContain('p.txt');
292
275
  tenant = 'bob';
293
- await handle.write('q.txt', 'b');
276
+ await handle.writeText('q.txt', 'b');
294
277
  expect((await backend.list('bob', 'shard-A')).map(d => d.path)).toContain('q.txt');
295
278
  });
296
279
  it('watchers resolve namespace at emit time, not subscribe time', async () => {
297
280
  const { MemoryDocumentBackend } = await import('./backends');
298
281
  const backend = new MemoryDocumentBackend();
299
282
  let ns = 'shard-A';
300
- const handle = createDocumentHandle('local', () => ns, backend, { format: 'text' });
283
+ const handle = createDocumentHandle('local', () => ns, backend);
301
284
  const seen = [];
302
285
  handle.watch((c) => seen.push(`${c.type}:${c.path}`));
303
- await handle.write('a.txt', 'x');
286
+ await handle.writeText('a.txt', 'x');
304
287
  ns = 'app-X';
305
- await handle.write('b.txt', 'y');
288
+ await handle.writeText('b.txt', 'y');
306
289
  expect(seen).toContain('create:a.txt');
307
290
  expect(seen).toContain('create:b.txt');
308
291
  });
309
292
  it('still accepts string tenant + namespace for back-compat', async () => {
310
293
  const { MemoryDocumentBackend } = await import('./backends');
311
294
  const backend = new MemoryDocumentBackend();
312
- const handle = createDocumentHandle('local', 'shard-A', backend, { format: 'text' });
313
- await handle.write('foo.txt', 'hi');
295
+ const handle = createDocumentHandle('local', 'shard-A', backend);
296
+ await handle.writeText('foo.txt', 'hi');
314
297
  expect((await backend.list('local', 'shard-A')).map(d => d.path)).toContain('foo.txt');
315
298
  });
316
299
  });
300
+ describe('DocumentHandle — per-call format (v3)', () => {
301
+ let backend;
302
+ let handle;
303
+ beforeEach(() => {
304
+ backend = new MemoryDocumentBackend();
305
+ handle = createDocumentHandle(() => 'tenant-a', () => 'shard-a', backend);
306
+ });
307
+ it('readJson returns parsed object', async () => {
308
+ await handle.writeJson('config.json', { a: 1, b: 'two' });
309
+ expect(await handle.readJson('config.json')).toEqual({ a: 1, b: 'two' });
310
+ });
311
+ it('readJson returns null for missing doc', async () => {
312
+ expect(await handle.readJson('missing.json')).toBeNull();
313
+ });
314
+ it('readText round-trips', async () => {
315
+ await handle.writeText('note.md', 'hello');
316
+ expect(await handle.readText('note.md')).toBe('hello');
317
+ });
318
+ it('readBinary round-trips', async () => {
319
+ const data = new Uint8Array([1, 2, 3, 4]).buffer;
320
+ await handle.writeBinary('blob.bin', data);
321
+ const out = await handle.readBinary('blob.bin');
322
+ expect(out).not.toBeNull();
323
+ expect(new Uint8Array(out)).toEqual(new Uint8Array(data));
324
+ });
325
+ });
@@ -1,4 +1,4 @@
1
- export type { DocumentFormat, DocumentHandleOptions, DocumentMeta, DocumentChange, DocumentBackend, DocumentHandle, AutosaveController, ScopeOption, } from './types';
1
+ export type { DocumentMeta, DocumentChange, DocumentBackend, DocumentHandle, AutosaveController, ScopeOption, } from './types';
2
2
  export { MemoryDocumentBackend, IndexedDBDocumentBackend } from './backends';
3
3
  export { HttpDocumentBackend } from './http-backend';
4
4
  export { createDocumentHandle } from './handle';
@@ -36,27 +36,6 @@ export declare const PERMISSION_DOCUMENTS_READ = "documents:read";
36
36
  export declare const PERMISSION_DOCUMENTS_WRITE = "documents:write";
37
37
  /** Permission to manage document mount points (admin-only). */
38
38
  export declare const PERMISSION_DOCUMENTS_MOUNT = "documents:mount";
39
- /**
40
- * Format hint for document content. Determines whether reads return a string
41
- * (`text`) or an `ArrayBuffer` (`binary`).
42
- */
43
- export type DocumentFormat = 'text' | 'binary';
44
- /**
45
- * Options passed to `ctx.documents()` to scope the handle. The handle
46
- * only operates on files matching the declared extensions (if provided).
47
- * Omitting `extensions` means "all files."
48
- */
49
- export interface DocumentHandleOptions {
50
- format: DocumentFormat;
51
- /** File extensions this handle operates on, e.g. ['.guml', '.md']. */
52
- extensions?: string[];
53
- /**
54
- * When set, this handle's namespace root becomes `{scope}/docs/{appId}/`
55
- * instead of `{scope}/docs/{shardId}/`. Use in `onAppActivate` to get
56
- * a handle scoped to the app's shared document namespace.
57
- */
58
- appId?: string;
59
- }
60
39
  /** Metadata about a stored document. */
61
40
  export interface DocumentMeta {
62
41
  path: string;
@@ -225,12 +204,23 @@ export interface ScopeOption {
225
204
  scope?: string;
226
205
  }
227
206
  export interface DocumentHandle {
228
- /** List documents matching the handle's extensions filter. */
207
+ /** List documents stored under this handle's namespace. */
229
208
  list(opts?: ScopeOption): Promise<DocumentMeta[]>;
230
- /** Read a document by path. Returns null if not found. */
231
- read(path: string, opts?: ScopeOption): Promise<string | null>;
232
- /** Write (create or overwrite) a document. Explicit save. */
233
- write(path: string, content: string, opts?: ScopeOption): Promise<void>;
209
+ /** Read a document as text. Returns null if not found. */
210
+ readText(path: string, opts?: ScopeOption): Promise<string | null>;
211
+ /** Read a document as a binary ArrayBuffer. Returns null if not found. */
212
+ readBinary(path: string, opts?: ScopeOption): Promise<ArrayBuffer | null>;
213
+ /**
214
+ * Read a document and JSON-parse it. Returns null if not found.
215
+ * Throws if the stored content is not valid JSON.
216
+ */
217
+ readJson<T = unknown>(path: string, opts?: ScopeOption): Promise<T | null>;
218
+ /** Write a string. */
219
+ writeText(path: string, content: string, opts?: ScopeOption): Promise<void>;
220
+ /** Write a binary ArrayBuffer. */
221
+ writeBinary(path: string, content: ArrayBuffer, opts?: ScopeOption): Promise<void>;
222
+ /** JSON-stringify and write. */
223
+ writeJson(path: string, data: unknown, opts?: ScopeOption): Promise<void>;
234
224
  /** Delete a document. */
235
225
  delete(path: string, opts?: ScopeOption): Promise<void>;
236
226
  /**
package/dist/host.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { registerShard as registerShardInternal } from './shards/activate.svelte';
1
+ import { registerShard as registerShardInternal } from './shards/lifecycle.svelte';
2
2
  import { registerApp } from './apps/registry.svelte';
3
3
  import { __setBackend } from './state/zones.svelte';
4
4
  import { setLocalOwner } from './auth/index';
package/dist/host.js CHANGED
@@ -15,8 +15,7 @@
15
15
  * import-hygiene rule is: shards and apps import from `api.ts`, the host
16
16
  * imports from `host.ts`.
17
17
  */
18
- import { registerShard as registerShardInternal, activateShard, registeredShards, } from './shards/activate.svelte';
19
- import { addAutostartShard } from './actions/state.svelte';
18
+ import { registerShard as registerShardInternal, registerAllShards, } from './shards/lifecycle.svelte';
20
19
  import { registerApp, registeredApps } from './apps/registry.svelte';
21
20
  import { launchApp, readLastApp, clearLastApp } from './apps/lifecycle';
22
21
  import { sh3coreShard } from './sh3core-shard/sh3coreShard.svelte';
@@ -84,21 +83,9 @@ export async function bootstrap(config) {
84
83
  }
85
84
  // 3. Load any packages installed in a previous session from IndexedDB
86
85
  await loadInstalledPackages();
87
- // 4. Activate every self-starting shard. Track them in the dispatcher's
88
- // autostartShards set so the `'app'` action scope treats their actions as
89
- // ambient (active even inside apps that don't list them as required).
90
- for (const [id, shard] of registeredShards) {
91
- if (shard.autostart) {
92
- addAutostartShard(id);
93
- try {
94
- await activateShard(id, { phase: 'autostart' });
95
- }
96
- catch (_a) {
97
- // Already logged + recorded in erroredShards by activateShard.
98
- // One bad self-starting shard must not prevent the sh3 from booting.
99
- }
100
- }
101
- }
86
+ // 4. v3: run register(ctx) on every registered shard. Lifecycle module
87
+ // handles error isolation; one failing shard does not block boot.
88
+ await registerAllShards();
102
89
  // 5. Read the last-active app from the user zone. If auto-launch fails,
103
90
  // clear the slot so the next reload lands on home instead of looping
104
91
  // into the same failure. No toast — the user did not initiate this.
@@ -143,44 +130,10 @@ export async function bootstrapSatellite(config) {
143
130
  }
144
131
  // 3. Load any packages installed in a previous session from IndexedDB
145
132
  await loadInstalledPackages();
146
- // 4. Autostart sweep mirror host bootstrap. Every shard with an
147
- // `autostart` hook runs in satellites too: services (LLM providers),
148
- // ambient action hosts (`__sh3core__` provides the command palette
149
- // via `sh3.palette.open`), and any user-installed self-starter.
150
- // `addAutostartShard` keeps action scoping consistent with the host
151
- // so palette/global actions remain ambient inside `'app'` scope.
152
- const autostartActivated = new Set();
153
- for (const [id, shard] of registeredShards) {
154
- if (shard.autostart) {
155
- addAutostartShard(id);
156
- try {
157
- await activateShard(id, { phase: 'autostart' });
158
- autostartActivated.add(id);
159
- }
160
- catch (_a) {
161
- // Already logged + recorded in erroredShards by activateShard.
162
- // One bad self-starting shard must not prevent the satellite from booting.
163
- }
164
- }
165
- }
166
- // 5. Activate explicit satellite shards (those carried by activateShards
167
- // in the payload — typically view-providing shards walked from the
168
- // layout). Skip ids already activated by the autostart sweep so the
169
- // diagnostic phase stays correct and we do no redundant work.
170
- for (const id of config.activateShardIds) {
171
- if (autostartActivated.has(id))
172
- continue;
173
- if (registeredShards.has(id)) {
174
- try {
175
- await activateShard(id, { phase: 'satellite' });
176
- }
177
- catch (err) {
178
- console.error(`[sh3] satellite activation of "${id}" failed:`, err);
179
- }
180
- }
181
- else {
182
- console.warn(`[sh3] satellite requested shard "${id}" but it is not registered`);
183
- }
184
- }
133
+ // 4. v3: one-pass register sweep replaces the old autostart + satellite
134
+ // passes. `config.activateShardIds` is now a no-op (the field is
135
+ // retained on the interface for back-compat; Phase 6 deletes it).
136
+ void config;
137
+ await registerAllShards();
185
138
  }
186
139
  export { installPackage, listInstalledPackages } from './registry/installer';
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
2
  import { resetFramework } from './__test__/reset';
3
3
  import { makeShard, makeShardManifest } from './__test__/fixtures';
4
4
  import { bootstrapSatellite } from './host';
5
- import { registerShard, activeShards, erroredShards, } from './shards/activate.svelte';
5
+ import { registerShard, activeShards, erroredShards, } from './shards/lifecycle.svelte';
6
6
  import { getLiveDispatcherState } from './actions/state.svelte';
7
7
  import { listActions } from './actions/registry';
8
8
  // loadInstalledPackages reads IndexedDB; neutralize it so the satellite
@@ -11,82 +11,50 @@ vi.mock('./registry/installer', async (orig) => {
11
11
  const real = await orig();
12
12
  return Object.assign(Object.assign({}, real), { loadInstalledPackages: vi.fn(async () => { }) });
13
13
  });
14
- describe('bootstrapSatellite — autostart sweep', () => {
14
+ describe('bootstrapSatellite — v3 register sweep', () => {
15
15
  beforeEach(resetFramework);
16
- it('activates a registered autostart shard even when not in activateShardIds', async () => {
17
- const activate = vi.fn();
18
- const autostart = vi.fn();
19
- const svc = makeShard({
20
- manifest: makeShardManifest({ id: 'svc-llm' }),
21
- activate,
22
- autostart,
23
- });
24
- registerShard(svc);
16
+ it('runs register() on every registered shard regardless of activateShardIds', async () => {
17
+ const registerA = vi.fn();
18
+ const registerB = vi.fn();
19
+ registerShard(makeShard({
20
+ manifest: makeShardManifest({ id: 'svc-a' }),
21
+ register: registerA,
22
+ }));
23
+ registerShard(makeShard({
24
+ manifest: makeShardManifest({ id: 'svc-b' }),
25
+ register: registerB,
26
+ }));
25
27
  await bootstrapSatellite({ activateShardIds: [] });
26
- expect(activate).toHaveBeenCalledTimes(1);
27
- expect(autostart).toHaveBeenCalledTimes(1);
28
- expect(activeShards.has('svc-llm')).toBe(true);
28
+ expect(registerA).toHaveBeenCalledTimes(1);
29
+ expect(registerB).toHaveBeenCalledTimes(1);
30
+ expect(activeShards.has('svc-a')).toBe(true);
31
+ expect(activeShards.has('svc-b')).toBe(true);
29
32
  });
30
- it('adds every activated autostart shard to the dispatcher autostart set', async () => {
31
- const svc = makeShard({
32
- manifest: makeShardManifest({ id: 'svc-llm' }),
33
- autostart: () => { },
34
- });
35
- registerShard(svc);
36
- await bootstrapSatellite({ activateShardIds: [] });
37
- expect(getLiveDispatcherState().autostartShards.has('svc-llm')).toBe(true);
38
- });
39
- it('records a throwing autostart shard with phase "autostart" and continues activating the rest', async () => {
33
+ it('records a throwing register with phase "register" and continues activating the rest', async () => {
40
34
  var _a;
41
- const goodActivate = vi.fn();
42
- const bad = makeShard({
35
+ const goodRegister = vi.fn();
36
+ registerShard(makeShard({
43
37
  manifest: makeShardManifest({ id: 'svc-bad' }),
44
- activate: () => {
45
- throw new Error('boom');
46
- },
47
- autostart: () => { },
48
- });
49
- const good = makeShard({
38
+ register: () => { throw new Error('boom'); },
39
+ }));
40
+ registerShard(makeShard({
50
41
  manifest: makeShardManifest({ id: 'svc-good' }),
51
- activate: goodActivate,
52
- autostart: () => { },
53
- });
54
- registerShard(bad);
55
- registerShard(good);
42
+ register: goodRegister,
43
+ }));
56
44
  await bootstrapSatellite({ activateShardIds: [] });
57
- expect((_a = erroredShards.get('svc-bad')) === null || _a === void 0 ? void 0 : _a.phase).toBe('autostart');
45
+ expect((_a = erroredShards.get('svc-bad')) === null || _a === void 0 ? void 0 : _a.phase).toBe('register');
58
46
  expect(activeShards.has('svc-bad')).toBe(false);
59
- expect(goodActivate).toHaveBeenCalledTimes(1);
47
+ expect(goodRegister).toHaveBeenCalledTimes(1);
60
48
  expect(activeShards.has('svc-good')).toBe(true);
61
49
  });
62
- it('dedupes an autostart shard listed in activateShardIds activates exactly once', async () => {
63
- const activate = vi.fn();
64
- const svc = makeShard({
65
- manifest: makeShardManifest({ id: 'svc-llm' }),
66
- activate,
67
- autostart: () => { },
68
- });
69
- registerShard(svc);
70
- await bootstrapSatellite({ activateShardIds: ['svc-llm'] });
71
- expect(activate).toHaveBeenCalledTimes(1);
50
+ it('exposes getLiveDispatcherState without autostartShards', async () => {
51
+ await bootstrapSatellite({ activateShardIds: [] });
52
+ const state = getLiveDispatcherState();
53
+ expect('autostartShards' in state).toBe(false);
72
54
  });
73
55
  it('registers sh3.palette.open after bootstrapSatellite completes (palette reachable)', async () => {
74
56
  await bootstrapSatellite({ activateShardIds: [] });
75
57
  const ids = listActions().map((entry) => entry.action.id);
76
58
  expect(ids).toContain('sh3.palette.open');
77
59
  });
78
- it('still activates non-autostart shards passed in activateShardIds with phase "satellite"', async () => {
79
- var _a;
80
- const activate = vi.fn(() => {
81
- throw new Error('explicit-fail');
82
- });
83
- const view = makeShard({
84
- manifest: makeShardManifest({ id: 'view-only' }),
85
- activate,
86
- });
87
- registerShard(view);
88
- await bootstrapSatellite({ activateShardIds: ['view-only'] });
89
- expect(activate).toHaveBeenCalledTimes(1);
90
- expect((_a = erroredShards.get('view-only')) === null || _a === void 0 ? void 0 : _a.phase).toBe('satellite');
91
- });
92
60
  });
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { getLayouts } from './layoutsState.svelte';
9
9
  import { restoreToFloat } from './layoutsApi';
10
- import { listStandaloneViews } from '../shards/activate.svelte';
10
+ import { listStandaloneViews } from '../shards/lifecycle.svelte';
11
11
  import { toastManager } from '../overlays/toast';
12
12
  import { sh3 } from '../sh3Runtime.svelte';
13
13
  import { makeSelectionApi } from '../actions/selection.svelte';
@@ -8,7 +8,7 @@
8
8
  * submenu and the home-page card grid.
9
9
  */
10
10
  import { VERSION } from '../version';
11
- import { listStandaloneViews } from '../shards/activate.svelte';
11
+ import { listStandaloneViews } from '../shards/lifecycle.svelte';
12
12
  import { getSelection } from '../actions/selection.svelte';
13
13
  import { modalManager } from '../overlays/modal';
14
14
  import { toastManager } from '../overlays/toast';
@@ -137,7 +137,7 @@ export const layoutsShard = {
137
137
  version: VERSION,
138
138
  views: [],
139
139
  },
140
- activate(ctx) {
140
+ register(ctx) {
141
141
  const zone = ctx.state({
142
142
  user: { layouts: [] },
143
143
  });
@@ -222,9 +222,6 @@ export const layoutsShard = {
222
222
  });
223
223
  });
224
224
  },
225
- autostart() {
226
- /* self-start so the action is available before any app launches. */
227
- },
228
225
  deactivate() {
229
226
  __unbindZone();
230
227
  },
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
- import { registerShard, activateShard } from '../shards/activate.svelte';
2
+ import { registerShard, activateShard } from '../shards/lifecycle.svelte';
3
3
  import { addAutostartShard } from '../actions/state.svelte';
4
4
  import { floatManager } from '../overlays/float';
5
5
  import { layoutsShard } from './layoutsShard.svelte';
@@ -15,7 +15,7 @@ const stubShard = {
15
15
  version: '0.0.0',
16
16
  views: [{ id: 'shell:terminal', label: 'Sh3', standalone: true }],
17
17
  },
18
- activate(ctx) {
18
+ register(ctx) {
19
19
  ctx.registerView('shell:terminal', {
20
20
  mount: () => ({ unmount() { } }),
21
21
  });
@@ -56,7 +56,7 @@ export const projectsShard = {
56
56
  version: VERSION,
57
57
  views: [{ id: PROJECTS_MANAGE_VIEW, label: 'Project Manager' }],
58
58
  },
59
- activate(ctx) {
59
+ register(ctx) {
60
60
  void refreshProjects();
61
61
  if (typeof document !== 'undefined') {
62
62
  document.addEventListener('visibilitychange', () => {
@@ -141,8 +141,4 @@ export const projectsShard = {
141
141
  });
142
142
  });
143
143
  },
144
- autostart() {
145
- /* register on the self-starting path so the project list is available
146
- on the home screen without an app launch. */
147
- },
148
144
  };
@@ -16,7 +16,7 @@
16
16
  */
17
17
  import { loadBundleModule } from './loader';
18
18
  import { savePackage, loadBundle, listInstalled, removePackage } from './storage';
19
- import { deactivateShard } from '../shards/activate.svelte';
19
+ import { deactivateShard } from '../shards/lifecycle.svelte';
20
20
  import { unregisterApp } from '../apps/lifecycle';
21
21
  import { registerLoadedBundle } from './register';
22
22
  import { extractBundlePermissions } from './permission-descriptions';
@@ -37,7 +37,7 @@ export declare function loadBundleModule(bytes: ArrayBuffer): Promise<LoadedBund
37
37
  /**
38
38
  * Type guard: returns true if the loaded module is a shard.
39
39
  *
40
- * A shard has an `activate` function and `manifest.views` array. An app
40
+ * A shard has a `register` function and `manifest.views` array. An app
41
41
  * has neither — it has `initialLayout` and `manifest.requiredShards`.
42
42
  */
43
43
  export declare function isShard(mod: Shard | App): mod is Shard;
@@ -125,12 +125,12 @@ export async function loadBundleModule(bytes) {
125
125
  /**
126
126
  * Type guard: returns true if the loaded module is a shard.
127
127
  *
128
- * A shard has an `activate` function and `manifest.views` array. An app
128
+ * A shard has a `register` function and `manifest.views` array. An app
129
129
  * has neither — it has `initialLayout` and `manifest.requiredShards`.
130
130
  */
131
131
  export function isShard(mod) {
132
- return ('activate' in mod &&
133
- typeof mod.activate === 'function' &&
132
+ return ('register' in mod &&
133
+ typeof mod.register === 'function' &&
134
134
  Array.isArray(mod.manifest.views));
135
135
  }
136
136
  /**