@markii/host 0.17.0-next.5 → 0.17.0-next.6

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 (39) hide show
  1. package/dist/browser-worker.classic.js +43 -43
  2. package/dist/browser-worker.js +43 -43
  3. package/dist/conformance/host/18-last-run-and-grants/expected.json +42 -0
  4. package/dist/conformance/host/18-last-run-and-grants/note.mk.md +5 -0
  5. package/dist/conformance/host/18-last-run-and-grants/scenario.json +27 -0
  6. package/dist/conformance/host/README.md +11 -4
  7. package/dist/conformance.d.ts +1 -1
  8. package/dist/conformance.js +1 -1
  9. package/dist/host/adapter.d.ts +21 -7
  10. package/dist/host/cascade-export-behavior.d.ts +3 -1
  11. package/dist/host/cascade-export-behavior.js +7 -4
  12. package/dist/host/conformance-runner.d.ts +8 -0
  13. package/dist/host/conformance-runner.js +40 -2
  14. package/dist/host/create-host.d.ts +231 -62
  15. package/dist/host/create-host.js +204 -98
  16. package/dist/host/export-behavior.d.ts +2 -0
  17. package/dist/host/export-behavior.js +10 -4
  18. package/dist/host/pack-archive.d.ts +7 -6
  19. package/dist/host/pack-archive.js +11 -7
  20. package/dist/host/pack-install.js +9 -7
  21. package/dist/host/pack-remove.d.ts +7 -18
  22. package/dist/host/pack-remove.js +2 -0
  23. package/dist/host/pack-trust-behavior.d.ts +13 -36
  24. package/dist/host/pack-trust-behavior.js +22 -1
  25. package/dist/host/run-behavior.d.ts +21 -34
  26. package/dist/host/run-behavior.js +23 -5
  27. package/dist/index.d.ts +5 -10
  28. package/dist/index.js +0 -1
  29. package/dist/internal.d.ts +9 -2
  30. package/dist/internal.js +3 -2
  31. package/dist/node-worker.js +3 -1
  32. package/dist/node.d.ts +1 -2
  33. package/dist/node.js +1 -1
  34. package/dist/run/node-isolate.d.ts +6 -2
  35. package/dist/run/node-isolate.js +2 -0
  36. package/dist/run/run-host.d.ts +2 -2
  37. package/dist/run/worker-entry-browser.d.ts +6 -1
  38. package/dist/run/worker-entry-browser.js +62 -5
  39. package/package.json +11 -11
@@ -1,4 +1,8 @@
1
- import { runViaAdapter, } from './run-behavior.js';
1
+ import { readPersistedValues } from '../run/run-flow.js';
2
+ import { clearGrantForDocument } from '../run/grant-flow.js';
3
+ import { readLastRunTrace } from '../run/run-trace.js';
4
+ import { staleValuesForRehydration } from '../run/stale-values.js';
5
+ import { runViaAdapter } from './run-behavior.js';
2
6
  import { installPackDiagnosticLines, installPackFromArchive, } from './pack-install.js';
3
7
  import { loadPacksViaAdapter, packDiscoveryIoFromAdapter, } from './pack-load.js';
4
8
  import { exportBaseName } from '../export/note-export.js';
@@ -15,7 +19,22 @@ function unsupported(adapter, capability, detail) {
15
19
  adapter.diagnostics(`declined (${capability} unsupported on ${adapter.id}): ${detail}`);
16
20
  return { kind: 'unsupported', capability, detail };
17
21
  }
22
+ /** Writes one diagnostics line for a rejected adapter primitive and returns the `failed` outcome. Never throws. */
23
+ function failedOutcome(adapter, what, err) {
24
+ const reason = err instanceof Error ? err.message : String(err);
25
+ try {
26
+ adapter.diagnostics(`${what} failed: ${reason}`);
27
+ }
28
+ catch {
29
+ // the diagnostics surface itself is what failed
30
+ }
31
+ return { kind: 'failed', reason };
32
+ }
18
33
  export function createMarkiiHost(adapter, options = {}) {
34
+ return createMarkiiHostWithSeams(adapter, options);
35
+ }
36
+ /** `createMarkiiHost` with the test seams. Exported from `@markii/host/internal` only. */
37
+ export function createMarkiiHostWithSeams(adapter, options = {}) {
19
38
  const packIo = options.packDiscoveryIo ?? packDiscoveryIoFromAdapter(adapter);
20
39
  /**
21
40
  * The pack source every load reads: under `trust: 'device-list'` the
@@ -35,12 +54,22 @@ export function createMarkiiHost(adapter, options = {}) {
35
54
  };
36
55
  }
37
56
  /** The pack-aware insert/completion catalog, built fresh from this host's currently authorized folders. Not cached here: `./editor-behavior.js`'s `createCatalogCache` is the caching seam, and a caller wiring a live editor (repeated completion/hover calls per keystroke) is expected to wrap this call in one. */
38
- async function currentCatalog() {
57
+ async function readCatalog() {
39
58
  if (!adapter.packs)
40
59
  return buildComponentCatalog([]);
41
60
  const { packs } = await loadPacksViaAdapter(packSource(adapter.packs), packIo);
42
61
  return buildComponentCatalog([...(adapter.packs.bundledPacks?.() ?? []), ...packs], adapter.packs.engine);
43
62
  }
63
+ /** The catalog for the cache. A rejected pack read writes one diagnostics line and leaves the standard set, so completion and hover still answer. */
64
+ async function currentCatalog() {
65
+ try {
66
+ return await readCatalog();
67
+ }
68
+ catch (err) {
69
+ failedOutcome(adapter, 'read the pack catalog', err);
70
+ return buildComponentCatalog([]);
71
+ }
72
+ }
44
73
  /** Completion and hover fire per keystroke, so they read a cached catalog. `insertComponent` stays uncached: it runs once per command. */
45
74
  const catalogCache = createCatalogCache(currentCatalog);
46
75
  function documentTextFor(editor, request) {
@@ -76,15 +105,47 @@ export function createMarkiiHost(adapter, options = {}) {
76
105
  if (!adapter.isolate) {
77
106
  return unsupported(adapter, 'isolate', 'no isolate is configured, so scripts cannot run.');
78
107
  }
79
- const result = await runViaAdapter(adapter, request, options.spawnRun);
80
- return { kind: 'ran', ...result };
108
+ try {
109
+ return await runViaAdapter(adapter, request, options.spawnRun);
110
+ }
111
+ catch (err) {
112
+ // `runViaAdapter` reports a throwing run as `failed` itself. This
113
+ // catches only what threw outside it, so `run` never rejects.
114
+ const reason = err instanceof Error ? err.message : String(err);
115
+ try {
116
+ adapter.diagnostics(`run failed: ${reason}`);
117
+ }
118
+ catch {
119
+ // the diagnostics surface itself is what failed
120
+ }
121
+ return { kind: 'failed', reason };
122
+ }
123
+ },
124
+ lastRun(documentKey) {
125
+ const persistedValues = readPersistedValues(adapter.memento, documentKey);
126
+ const trace = readLastRunTrace(adapter.memento, documentKey);
127
+ return {
128
+ values: staleValuesForRehydration(persistedValues),
129
+ persistedValues,
130
+ ...(trace !== undefined ? { trace } : {}),
131
+ };
132
+ },
133
+ forgetGrants(documentKey) {
134
+ return clearGrantForDocument(adapter.memento, documentKey);
81
135
  },
82
136
  async exportNote(request) {
83
137
  const formats = adapter.exports?.formats;
84
138
  if (formats === undefined || !formats.has(request.format)) {
85
139
  return unsupported(adapter, 'export-format', `export format "${request.format}" is not available on ${adapter.id}.`);
86
140
  }
141
+ if (request.targetPath === undefined &&
142
+ adapter.exports?.resolveTarget === undefined) {
143
+ return unsupported(adapter, 'export-format', `export format "${request.format}" has no destination on ${adapter.id}: the host declares no resolveTarget and the request has no targetPath.`);
144
+ }
87
145
  const outcome = await exportNoteFileViaAdapter(adapter, request.format, {
146
+ ...(request.targetPath !== undefined
147
+ ? { targetPath: request.targetPath }
148
+ : {}),
88
149
  ...(options.htmlEngine !== undefined
89
150
  ? { engine: options.htmlEngine }
90
151
  : {}),
@@ -118,7 +179,7 @@ export function createMarkiiHost(adapter, options = {}) {
118
179
  }, options.renderAnsi);
119
180
  if (outcome === undefined) {
120
181
  adapter.diagnostics(`export declined: format "${request.format}" has no working export path on ${adapter.id} (a cancelled picker, or a missing renderer/PDF seam).`);
121
- return { kind: 'cancelled' };
182
+ return { kind: 'declined' };
122
183
  }
123
184
  for (const line of exportDiagnosticLines(outcome)) {
124
185
  adapter.diagnostics(line);
@@ -139,7 +200,12 @@ export function createMarkiiHost(adapter, options = {}) {
139
200
  };
140
201
  },
141
202
  async exportLinkedSet(request) {
142
- if (adapter.exports?.resolveLinkedSetTarget === undefined) {
203
+ // A host offers the linked-set export when it declares `exports` and
204
+ // either names a `resolveLinkedSetTarget` or gets a `targetPath` with
205
+ // the request.
206
+ if (adapter.exports === undefined ||
207
+ (request.targetPath === undefined &&
208
+ adapter.exports.resolveLinkedSetTarget === undefined)) {
143
209
  return unsupported(adapter, 'linked-set-export', `the linked-set export is not available on ${adapter.id}.`);
144
210
  }
145
211
  if (options.htmlEngine === undefined) {
@@ -152,7 +218,7 @@ export function createMarkiiHost(adapter, options = {}) {
152
218
  });
153
219
  if (outcome === undefined) {
154
220
  adapter.diagnostics(`linked-set export declined: no archive target was chosen on ${adapter.id}.`);
155
- return { kind: 'cancelled' };
221
+ return { kind: 'declined' };
156
222
  }
157
223
  return outcome;
158
224
  },
@@ -184,29 +250,34 @@ export function createMarkiiHost(adapter, options = {}) {
184
250
  return unsupported(adapter, 'packs', 'this host has no pack folders configured.');
185
251
  }
186
252
  catalogCache.invalidate();
187
- const result = await loadPacksViaAdapter(packSource(adapter.packs), packIo);
188
- for (const line of formatPackDiagnosticLines({
189
- packs: result.packs,
190
- skipped: result.skipped,
191
- engine: adapter.packs.engine,
192
- cssWarnings: [],
193
- })) {
194
- adapter.diagnostics(line);
195
- }
196
- if (usesPackTrust(adapter)) {
197
- for (const namespace of (await listInstalledPacks(adapter))
198
- .notEnabled) {
199
- adapter.diagnostics(notEnabledPackDiagnosticLine(namespace, adapter.labels.deviceNoun));
253
+ try {
254
+ const result = await loadPacksViaAdapter(packSource(adapter.packs), packIo);
255
+ for (const line of formatPackDiagnosticLines({
256
+ packs: result.packs,
257
+ skipped: result.skipped,
258
+ engine: adapter.packs.engine,
259
+ cssWarnings: [],
260
+ })) {
261
+ adapter.diagnostics(line);
200
262
  }
263
+ if (usesPackTrust(adapter)) {
264
+ for (const namespace of (await listInstalledPacks(adapter))
265
+ .notEnabled) {
266
+ adapter.diagnostics(notEnabledPackDiagnosticLine(namespace, adapter.labels.deviceNoun));
267
+ }
268
+ }
269
+ return {
270
+ kind: 'loaded',
271
+ namespaces: result.namespaces,
272
+ packs: result.packs,
273
+ skipped: result.skipped,
274
+ components: result.components,
275
+ unavailable: result.unavailable,
276
+ };
277
+ }
278
+ catch (err) {
279
+ return failedOutcome(adapter, 'load packs', err);
201
280
  }
202
- return {
203
- kind: 'loaded',
204
- namespaces: result.namespaces,
205
- packs: result.packs,
206
- skipped: result.skipped,
207
- components: result.components,
208
- unavailable: result.unavailable,
209
- };
210
281
  },
211
282
  async loadTerminalPacks() {
212
283
  if (!adapter.packs) {
@@ -216,34 +287,39 @@ export function createMarkiiHost(adapter, options = {}) {
216
287
  return unsupported(adapter, 'packs', 'this host does not run the terminal engine, so it has no terminal packs.');
217
288
  }
218
289
  catalogCache.invalidate();
219
- const result = await loadPacksViaAdapter(packSource(adapter.packs), packIo);
220
- const read = await readTerminalPackScripts(adapter, result.packs);
221
- for (const line of formatPackDiagnosticLines({
222
- packs: result.packs,
223
- skipped: result.skipped,
224
- engine: 'ansi',
225
- cssWarnings: [],
226
- })) {
227
- adapter.diagnostics(line);
290
+ try {
291
+ const result = await loadPacksViaAdapter(packSource(adapter.packs), packIo);
292
+ const read = await readTerminalPackScripts(adapter, result.packs);
293
+ for (const line of formatPackDiagnosticLines({
294
+ packs: result.packs,
295
+ skipped: result.skipped,
296
+ engine: 'ansi',
297
+ cssWarnings: [],
298
+ })) {
299
+ adapter.diagnostics(line);
300
+ }
301
+ for (const line of read.problems)
302
+ adapter.diagnostics(line);
303
+ const failureLines = [
304
+ ...result.skipped
305
+ .filter((entry) => entry.engineUnsupported === undefined)
306
+ .map(skippedPackFolderLine),
307
+ ...read.problems,
308
+ ];
309
+ return {
310
+ kind: 'loaded',
311
+ namespaces: result.namespaces,
312
+ packs: result.packs,
313
+ skipped: result.skipped,
314
+ components: result.components,
315
+ unavailable: result.unavailable,
316
+ scripts: read.scripts,
317
+ failureLines,
318
+ };
319
+ }
320
+ catch (err) {
321
+ return failedOutcome(adapter, 'load terminal packs', err);
228
322
  }
229
- for (const line of read.problems)
230
- adapter.diagnostics(line);
231
- const failureLines = [
232
- ...result.skipped
233
- .filter((entry) => entry.engineUnsupported === undefined)
234
- .map(skippedPackFolderLine),
235
- ...read.problems,
236
- ];
237
- return {
238
- kind: 'loaded',
239
- namespaces: result.namespaces,
240
- packs: result.packs,
241
- skipped: result.skipped,
242
- components: result.components,
243
- unavailable: result.unavailable,
244
- scripts: read.scripts,
245
- failureLines,
246
- };
247
323
  },
248
324
  async removePack(namespace) {
249
325
  const packs = adapter.packs;
@@ -265,7 +341,12 @@ export function createMarkiiHost(adapter, options = {}) {
265
341
  if (!usesPackTrust(adapter)) {
266
342
  return unsupported(adapter, 'pack-trust', 'this host does not gate installed packs on a trust list.');
267
343
  }
268
- return { kind: 'listed', ...(await listInstalledPacks(adapter)) };
344
+ try {
345
+ return { kind: 'listed', ...(await listInstalledPacks(adapter)) };
346
+ }
347
+ catch (err) {
348
+ return failedOutcome(adapter, 'list installed packs', err);
349
+ }
269
350
  },
270
351
  async enablePack(namespace) {
271
352
  if (!usesPackTrust(adapter)) {
@@ -290,17 +371,31 @@ export function createMarkiiHost(adapter, options = {}) {
290
371
  if (!editor) {
291
372
  return unsupported(adapter, 'editor', 'completion is not available: this host has no live editor.');
292
373
  }
293
- return completeWith(await catalogCache.get(), request, editor);
374
+ try {
375
+ return completeWith(await catalogCache.get(), request, editor);
376
+ }
377
+ catch (err) {
378
+ return failedOutcome(adapter, 'completion', err);
379
+ }
294
380
  },
295
381
  completeAtSync(request) {
296
382
  const editor = adapter.editor;
297
383
  if (!editor) {
298
384
  return unsupported(adapter, 'editor', 'completion is not available: this host has no live editor.');
299
385
  }
300
- const warm = catalogCache.peek();
301
- if (warm === undefined)
302
- void catalogCache.get();
303
- return completeWith(warm ?? buildComponentCatalog([]), request, editor);
386
+ try {
387
+ const warm = catalogCache.peek();
388
+ if (warm === undefined) {
389
+ catalogCache.get().catch((err) => {
390
+ failedOutcome(adapter, 'completion catalog', err);
391
+ });
392
+ }
393
+ return completeWith(warm ?? buildComponentCatalog([]), request, editor);
394
+ }
395
+ catch (err) {
396
+ failedOutcome(adapter, 'completion', err);
397
+ return { kind: 'none' };
398
+ }
304
399
  },
305
400
  invalidateCatalog() {
306
401
  catalogCache.invalidate();
@@ -310,48 +405,59 @@ export function createMarkiiHost(adapter, options = {}) {
310
405
  if (!editor) {
311
406
  return unsupported(adapter, 'editor', 'hover is not available: this host has no live editor.');
312
407
  }
313
- const catalog = await catalogCache.get();
314
- const hover = hoverAtViaEditor({ documentText: () => documentTextFor(editor, request) }, request, catalog);
315
- if (hover === undefined)
316
- return { kind: 'none' };
317
- // The formatted text (summary, attributes, and the usage example),
318
- // not the bare summary sentence: a component's description alone
319
- // often never repeats its own directive name (see
320
- // `./editor-behavior.js`'s `hoverDocumentationText` doc comment),
321
- // while the usage example always does.
322
- return {
323
- kind: 'hover',
324
- text: formatComponentDocumentation(hover.documentation),
325
- documentation: hover.documentation,
326
- start: hover.start,
327
- end: hover.end,
328
- };
408
+ try {
409
+ const catalog = await catalogCache.get();
410
+ const hover = hoverAtViaEditor({ documentText: () => documentTextFor(editor, request) }, request, catalog);
411
+ if (hover === undefined)
412
+ return { kind: 'none' };
413
+ // The formatted text (summary, attributes, and the usage example),
414
+ // not the bare summary sentence: a component's description alone
415
+ // often never repeats its own directive name (see
416
+ // `./editor-behavior.js`'s `hoverDocumentationText` doc comment),
417
+ // while the usage example always does.
418
+ return {
419
+ kind: 'hover',
420
+ text: formatComponentDocumentation(hover.documentation),
421
+ documentation: hover.documentation,
422
+ start: hover.start,
423
+ end: hover.end,
424
+ };
425
+ }
426
+ catch (err) {
427
+ return failedOutcome(adapter, 'hover', err);
428
+ }
329
429
  },
330
430
  async insertComponent(request) {
331
431
  if (!adapter.editor) {
332
432
  return unsupported(adapter, 'editor', 'insert component is not available: this host has no live editor.');
333
433
  }
334
- const catalog = await currentCatalog();
335
- const component = catalog.find((entry) => entry.directiveName === request.name);
336
- if (component === undefined) {
337
- adapter.diagnostics(`insert component: no catalog entry named "${request.name}".`);
338
- return { kind: 'not-found', name: request.name };
339
- }
340
- const cursor = adapter.editor.cursor();
341
- const documentText = adapter.editor.documentText();
342
- const plan = insertComponentPlan(component, documentText, cursor.line, cursor.column);
343
- const edits = plan.edits.map((edit) => ({
344
- line: edit.line,
345
- startColumn: edit.startColumn,
346
- endColumn: edit.endColumn,
347
- text: edit.text,
348
- }));
349
- const applied = await adapter.editor.applyEdits(edits);
350
- if (!applied) {
351
- adapter.diagnostics(`insert component: the host declined to apply the edit for "${request.name}".`);
352
- return { kind: 'failed' };
353
- }
354
- return { kind: 'inserted', cursor: plan.cursor };
434
+ try {
435
+ const catalog = await readCatalog();
436
+ const component = catalog.find((entry) => entry.directiveName === request.name);
437
+ if (component === undefined) {
438
+ adapter.diagnostics(`insert component: no catalog entry named "${request.name}".`);
439
+ return { kind: 'not-found', name: request.name };
440
+ }
441
+ const cursor = adapter.editor.cursor();
442
+ const documentText = adapter.editor.documentText();
443
+ const plan = insertComponentPlan(component, documentText, cursor.line, cursor.column);
444
+ const edits = plan.edits.map((edit) => ({
445
+ line: edit.line,
446
+ startColumn: edit.startColumn,
447
+ endColumn: edit.endColumn,
448
+ text: edit.text,
449
+ }));
450
+ const applied = await adapter.editor.applyEdits(edits);
451
+ if (!applied) {
452
+ const reason = 'the host declined to apply the edit';
453
+ adapter.diagnostics(`insert component: the host declined to apply the edit for "${request.name}".`);
454
+ return { kind: 'failed', reason };
455
+ }
456
+ return { kind: 'inserted', cursor: plan.cursor };
457
+ }
458
+ catch (err) {
459
+ return failedOutcome(adapter, 'insert component', err);
460
+ }
355
461
  },
356
462
  diagnostics(line) {
357
463
  adapter.diagnostics(line);
@@ -70,6 +70,8 @@ export declare function imageEmbedDiagnosticLines(images: EmbeddedImageReport):
70
70
  export declare function exportDiagnosticLines(outcome: NoteFileExportOutcome): string[];
71
71
  /** What `exportHtmlDocumentViaAdapter` needs beyond the note's own text and path. */
72
72
  export interface ExportHtmlDocumentRequest {
73
+ /** Where to write the file. When present the adapter's `resolveTarget` is not asked. */
74
+ readonly targetPath?: string;
73
75
  /** The static HTML engine. Required for the `html` and `pdf` formats; `exportNoteFileViaAdapter` declines those without it. */
74
76
  readonly engine?: HtmlExportEngine;
75
77
  readonly notePath: string;
@@ -148,6 +148,12 @@ export async function buildExportDocument(request) {
148
148
  images: document.images,
149
149
  };
150
150
  }
151
+ /** The path an export writes to: the request's own `targetPath`, else whatever the adapter's `resolveTarget` picks, which resolves `undefined` on a cancel. */
152
+ async function exportTarget(adapter, request, format) {
153
+ if (request.targetPath !== undefined)
154
+ return request.targetPath;
155
+ return adapter.exports?.resolveTarget?.(exportDefaultFileName(request.notePath, format), format);
156
+ }
151
157
  function reasonOf(error) {
152
158
  return error instanceof Error ? error.message : String(error);
153
159
  }
@@ -176,7 +182,7 @@ export async function exportNoteFileViaAdapter(adapter, format, request, renderA
176
182
  const built = await buildExportDocument({ ...request, engine });
177
183
  if (format === 'html') {
178
184
  const bytes = new TextEncoder().encode(built.html).byteLength;
179
- const path = await adapter.exports?.resolveTarget(exportDefaultFileName(request.notePath, 'html'), 'html');
185
+ const path = await exportTarget(adapter, request, 'html');
180
186
  if (path === undefined)
181
187
  return undefined;
182
188
  await adapter.writeFile(path, new TextEncoder().encode(built.html));
@@ -194,7 +200,7 @@ export async function exportNoteFileViaAdapter(adapter, format, request, renderA
194
200
  const htmlToPdf = adapter.exports?.htmlToPdf;
195
201
  if (htmlToPdf === undefined)
196
202
  return undefined;
197
- const path = await adapter.exports?.resolveTarget(exportDefaultFileName(request.notePath, 'pdf'), 'pdf');
203
+ const path = await exportTarget(adapter, request, 'pdf');
198
204
  if (path === undefined)
199
205
  return undefined;
200
206
  const pdfBytes = await htmlToPdf(built.html);
@@ -216,7 +222,7 @@ export async function exportNoteFileViaAdapter(adapter, format, request, renderA
216
222
  if (format === 'md-plain') {
217
223
  try {
218
224
  const text = mdPlainFromSource(request.text);
219
- const path = await adapter.exports?.resolveTarget(exportDefaultFileName(request.notePath, 'md-plain'), 'md-plain');
225
+ const path = await exportTarget(adapter, request, 'md-plain');
220
226
  if (path === undefined)
221
227
  return undefined;
222
228
  await adapter.writeFile(path, new TextEncoder().encode(text));
@@ -238,7 +244,7 @@ export async function exportNoteFileViaAdapter(adapter, format, request, renderA
238
244
  return undefined;
239
245
  try {
240
246
  const text = await renderAnsi(request.text);
241
- const path = await adapter.exports?.resolveTarget(exportDefaultFileName(request.notePath, 'ansi'), 'ansi');
247
+ const path = await exportTarget(adapter, request, 'ansi');
242
248
  if (path === undefined)
243
249
  return undefined;
244
250
  await adapter.writeFile(path, new TextEncoder().encode(text));
@@ -4,12 +4,13 @@ export declare function describeArchiveError(error: PackArchiveError): string;
4
4
  /** Writes one file and creates its missing parent folders: `HostAdapter.writeFile`. */
5
5
  export type ArchiveFileWriter = (absolutePath: string, bytes: Uint8Array) => Promise<void>;
6
6
  /**
7
- * Writes a validated archive's contents into `destinationDir`: `pack.json`
8
- * (the parsed manifest, re-serialized: `openPackArchive` keeps only the
9
- * validated manifest fields, not the original bytes, so the written file
10
- * is a normalized round-trip of the same fields, not a byte-for-byte
11
- * copy), `webview.js`, `webview.css` and `terminal.js` when the archive has them, and
12
- * `scripts/*` when it ships any. Does NOT clear `destinationDir` first;
7
+ * Writes a validated archive's contents into `destinationDir`: `webview.js`,
8
+ * `webview.css` and `terminal.js` when the archive has them, `scripts/*`
9
+ * when it ships any, and then `pack.json` (the parsed manifest,
10
+ * re-serialized: `openPackArchive` keeps only the validated manifest
11
+ * fields, not the original bytes, so the written file is a normalized
12
+ * round-trip of the same fields, not a byte-for-byte copy). The manifest is
13
+ * written last, so a failure midway leaves no loadable pack. Does NOT clear `destinationDir` first;
13
14
  * a caller that needs a clean re-extract (a reinstall replacing an
14
15
  * existing pack) removes the folder itself before this.
15
16
  *
@@ -21,12 +21,13 @@ export function describeArchiveError(error) {
21
21
  return `invalid pack.json in archive (${error.errors.join('; ')})`;
22
22
  }
23
23
  /**
24
- * Writes a validated archive's contents into `destinationDir`: `pack.json`
25
- * (the parsed manifest, re-serialized: `openPackArchive` keeps only the
26
- * validated manifest fields, not the original bytes, so the written file
27
- * is a normalized round-trip of the same fields, not a byte-for-byte
28
- * copy), `webview.js`, `webview.css` and `terminal.js` when the archive has them, and
29
- * `scripts/*` when it ships any. Does NOT clear `destinationDir` first;
24
+ * Writes a validated archive's contents into `destinationDir`: `webview.js`,
25
+ * `webview.css` and `terminal.js` when the archive has them, `scripts/*`
26
+ * when it ships any, and then `pack.json` (the parsed manifest,
27
+ * re-serialized: `openPackArchive` keeps only the validated manifest
28
+ * fields, not the original bytes, so the written file is a normalized
29
+ * round-trip of the same fields, not a byte-for-byte copy). The manifest is
30
+ * written last, so a failure midway leaves no loadable pack. Does NOT clear `destinationDir` first;
30
31
  * a caller that needs a clean re-extract (a reinstall replacing an
31
32
  * existing pack) removes the folder itself before this.
32
33
  *
@@ -40,7 +41,6 @@ export function describeArchiveError(error) {
40
41
  */
41
42
  export async function writeArchiveContents(archive, destinationDir, writeFile) {
42
43
  const encoder = new TextEncoder();
43
- await writeFile(joinPath(destinationDir, 'pack.json'), encoder.encode(JSON.stringify(archive.manifest, null, 2) + '\n'));
44
44
  await writeFile(joinPath(destinationDir, 'webview.js'), archive.scriptBytes);
45
45
  if (archive.stylesheetBytes !== undefined) {
46
46
  await writeFile(joinPath(destinationDir, 'webview.css'), archive.stylesheetBytes);
@@ -56,4 +56,8 @@ export async function writeArchiveContents(archive, destinationDir, writeFile) {
56
56
  await writeFile(target, archive.scriptModules[name]);
57
57
  }
58
58
  }
59
+ // The manifest goes last. Discovery treats a folder with no `pack.json`
60
+ // as no pack, so an install that fails partway never leaves a manifest
61
+ // beside a missing script.
62
+ await writeFile(joinPath(destinationDir, 'pack.json'), encoder.encode(JSON.stringify(archive.manifest, null, 2) + '\n'));
59
63
  }
@@ -43,7 +43,7 @@ export async function installPackFromArchive(adapter, options) {
43
43
  const opened = await openPackArchive(archiveBytes);
44
44
  if (!opened.ok) {
45
45
  return {
46
- kind: 'rejected',
46
+ kind: 'invalid',
47
47
  reason: `"${archivePath}" is not a valid pack archive: ${describeArchiveError(opened.error)}`,
48
48
  };
49
49
  }
@@ -62,7 +62,7 @@ export async function installPackFromArchive(adapter, options) {
62
62
  });
63
63
  }
64
64
  catch (err) {
65
- return { kind: 'rejected', reason: describeThrown(err) };
65
+ return { kind: 'failed', reason: describeThrown(err) };
66
66
  }
67
67
  if (!consented) {
68
68
  return { kind: 'declined', step: 'consent', namespace: packName };
@@ -73,7 +73,7 @@ export async function installPackFromArchive(adapter, options) {
73
73
  alreadyInstalled = await exists(targetDir);
74
74
  }
75
75
  catch (err) {
76
- return { kind: 'rejected', reason: describeThrown(err) };
76
+ return { kind: 'failed', reason: describeThrown(err) };
77
77
  }
78
78
  let replaced = false;
79
79
  if (alreadyInstalled) {
@@ -88,7 +88,7 @@ export async function installPackFromArchive(adapter, options) {
88
88
  });
89
89
  }
90
90
  catch (err) {
91
- return { kind: 'rejected', reason: describeThrown(err) };
91
+ return { kind: 'failed', reason: describeThrown(err) };
92
92
  }
93
93
  if (!proceed) {
94
94
  return { kind: 'declined', step: 'replace', namespace: packName };
@@ -102,7 +102,7 @@ export async function installPackFromArchive(adapter, options) {
102
102
  }
103
103
  catch (err) {
104
104
  return {
105
- kind: 'rejected',
105
+ kind: 'failed',
106
106
  reason: `could not install pack "${packName}": ${describeThrown(err)}`,
107
107
  };
108
108
  }
@@ -153,7 +153,9 @@ export function installReplaceConfirmMessage(packName) {
153
153
  * `false`.
154
154
  */
155
155
  export function installPackMessage(outcome, archivePath, labels, reloadsAutomatically = false) {
156
- if (outcome.kind === 'rejected' || outcome.kind === 'unsupported') {
156
+ if (outcome.kind === 'invalid' ||
157
+ outcome.kind === 'failed' ||
158
+ outcome.kind === 'unsupported') {
157
159
  return `Markii: could not install a pack from "${archivePath}". Open ${labels.diagnosticsSurface} for details.`;
158
160
  }
159
161
  if (outcome.kind === 'reserved') {
@@ -171,7 +173,7 @@ export function installPackMessage(outcome, archivePath, labels, reloadsAutomati
171
173
  }
172
174
  /** The full diagnostics-surface detail for one install attempt (AGENTS.md's "clean is not silent": the other of a failure's two homes). */
173
175
  export function installPackDiagnosticLines(outcome, archivePath) {
174
- if (outcome.kind === 'rejected') {
176
+ if (outcome.kind === 'invalid' || outcome.kind === 'failed') {
175
177
  return [
176
178
  `Install pack from file rejected "${archivePath}": ${outcome.reason}`,
177
179
  ];
@@ -1,21 +1,10 @@
1
1
  import type { HostAdapter } from './adapter.js';
2
- export type RemovePackOutcome = {
3
- readonly kind: 'removed';
4
- readonly namespace: string;
5
- } | {
6
- readonly kind: 'not-installed';
7
- readonly namespace: string;
8
- } | {
9
- readonly kind: 'reserved';
10
- readonly namespace: string;
11
- } | {
12
- readonly kind: 'invalid';
13
- readonly reason: string;
14
- } | {
15
- readonly kind: 'failed';
16
- readonly reason: string;
17
- };
2
+ import type { RemovePackOutcome } from './create-host.js';
3
+ /** What one removal attempt produced, in the shape `createMarkiiHost(...).removePack` returns (minus `unsupported`, which only the host behavior can say). */
4
+ export type RemovePackAttempt = Exclude<RemovePackOutcome, {
5
+ readonly kind: 'unsupported';
6
+ }>;
18
7
  /** Never throws. The caller checks `adapter.packs.installRoot`/`removeFolder` exist first. */
19
- export declare function removePackViaAdapter(adapter: HostAdapter, namespace: string, installRoot: string, removeFolder: (folder: string) => Promise<void>, reservedNamespaces: ReadonlySet<string>): Promise<RemovePackOutcome>;
8
+ export declare function removePackViaAdapter(adapter: HostAdapter, namespace: string, installRoot: string, removeFolder: (folder: string) => Promise<void>, reservedNamespaces: ReadonlySet<string>): Promise<RemovePackAttempt>;
20
9
  export declare function removePackMessage(outcome: RemovePackOutcome): string;
21
- export declare function removePackDiagnosticLine(outcome: RemovePackOutcome): string;
10
+ export declare function removePackDiagnosticLine(outcome: RemovePackAttempt): string;
@@ -37,6 +37,8 @@ export async function removePackViaAdapter(adapter, namespace, installRoot, remo
37
37
  }
38
38
  export function removePackMessage(outcome) {
39
39
  switch (outcome.kind) {
40
+ case 'unsupported':
41
+ return 'Markii: removing packs is not available on this host. Nothing was removed.';
40
42
  case 'removed':
41
43
  return `Markii: removed pack "${outcome.namespace}".`;
42
44
  case 'not-installed':