@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.
- package/dist/browser-worker.classic.js +43 -43
- package/dist/browser-worker.js +43 -43
- package/dist/conformance/host/18-last-run-and-grants/expected.json +42 -0
- package/dist/conformance/host/18-last-run-and-grants/note.mk.md +5 -0
- package/dist/conformance/host/18-last-run-and-grants/scenario.json +27 -0
- package/dist/conformance/host/README.md +11 -4
- package/dist/conformance.d.ts +1 -1
- package/dist/conformance.js +1 -1
- package/dist/host/adapter.d.ts +21 -7
- package/dist/host/cascade-export-behavior.d.ts +3 -1
- package/dist/host/cascade-export-behavior.js +7 -4
- package/dist/host/conformance-runner.d.ts +8 -0
- package/dist/host/conformance-runner.js +40 -2
- package/dist/host/create-host.d.ts +231 -62
- package/dist/host/create-host.js +204 -98
- package/dist/host/export-behavior.d.ts +2 -0
- package/dist/host/export-behavior.js +10 -4
- package/dist/host/pack-archive.d.ts +7 -6
- package/dist/host/pack-archive.js +11 -7
- package/dist/host/pack-install.js +9 -7
- package/dist/host/pack-remove.d.ts +7 -18
- package/dist/host/pack-remove.js +2 -0
- package/dist/host/pack-trust-behavior.d.ts +13 -36
- package/dist/host/pack-trust-behavior.js +22 -1
- package/dist/host/run-behavior.d.ts +21 -34
- package/dist/host/run-behavior.js +23 -5
- package/dist/index.d.ts +5 -10
- package/dist/index.js +0 -1
- package/dist/internal.d.ts +9 -2
- package/dist/internal.js +3 -2
- package/dist/node-worker.js +3 -1
- package/dist/node.d.ts +1 -2
- package/dist/node.js +1 -1
- package/dist/run/node-isolate.d.ts +6 -2
- package/dist/run/node-isolate.js +2 -0
- package/dist/run/run-host.d.ts +2 -2
- package/dist/run/worker-entry-browser.d.ts +6 -1
- package/dist/run/worker-entry-browser.js +62 -5
- package/package.json +11 -11
package/dist/host/create-host.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
-
|
|
80
|
-
|
|
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: '
|
|
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
|
-
|
|
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: '
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`: `
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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`: `
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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 === '
|
|
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 === '
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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<
|
|
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:
|
|
10
|
+
export declare function removePackDiagnosticLine(outcome: RemovePackAttempt): string;
|
package/dist/host/pack-remove.js
CHANGED
|
@@ -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':
|