@markii/host 0.17.0-next.3 → 0.17.0-next.4

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 (42) hide show
  1. package/README.md +6 -0
  2. package/dist/browser-worker.classic.js +640 -0
  3. package/dist/browser-worker.js +19 -19
  4. package/dist/browser.d.ts +1 -1
  5. package/dist/browser.js +10 -3
  6. package/dist/conformance/host/05-export-html-pack-directive/expected.json +2 -0
  7. package/dist/conformance/host/06-install-pack-then-render/expected.json +6 -1
  8. package/dist/conformance/host/README.md +4 -2
  9. package/dist/export/note-export.d.ts +1 -1
  10. package/dist/host/adapter.d.ts +4 -2
  11. package/dist/host/conformance-runner.d.ts +0 -3
  12. package/dist/host/conformance-runner.js +18 -6
  13. package/dist/host/create-host.d.ts +25 -5
  14. package/dist/host/create-host.js +34 -23
  15. package/dist/host/export-behavior.d.ts +2 -1
  16. package/dist/host/export-behavior.js +1 -1
  17. package/dist/host/labels.d.ts +5 -14
  18. package/dist/host/labels.js +8 -22
  19. package/dist/host/pack-archive.d.ts +4 -10
  20. package/dist/host/pack-archive.js +11 -14
  21. package/dist/host/pack-install.d.ts +10 -21
  22. package/dist/host/pack-install.js +22 -16
  23. package/dist/html-engine.js +4 -4
  24. package/dist/index.d.ts +15 -80
  25. package/dist/index.js +18 -62
  26. package/dist/internal.d.ts +30 -0
  27. package/dist/internal.js +31 -0
  28. package/dist/node-fs.d.ts +0 -3
  29. package/dist/node-fs.js +1 -21
  30. package/dist/node-worker.d.ts +8 -0
  31. package/dist/node-worker.js +36 -15
  32. package/dist/node.d.ts +1 -2
  33. package/dist/node.js +4 -3
  34. package/dist/packs/discover.d.ts +2 -0
  35. package/dist/packs/discover.js +1 -0
  36. package/dist/run/net-bridge.js +7 -5
  37. package/dist/run/net-pinning.d.ts +2 -0
  38. package/dist/run/net-pinning.js +17 -3
  39. package/dist/run/net-provider.js +23 -12
  40. package/dist/run/net-refusal.d.ts +4 -0
  41. package/dist/run/net-refusal.js +24 -0
  42. package/package.json +28 -12
package/dist/browser.d.ts CHANGED
@@ -11,7 +11,7 @@ export interface FetchNetProviderOptions {
11
11
  * every request. Entries that do not parse as an http(s) URL are ignored.
12
12
  */
13
13
  allowedOrigins?: readonly string[];
14
- /** How a policy refusal is built. Defaults to a plain `Error`; the isolate bridge marks every refusal as a denial on the wire either way. */
14
+ /** How a policy refusal is built. Defaults to a plain `Error`. The provider marks every refusal it throws, so the isolate bridge sends it as a denial. A transport failure is never marked. */
15
15
  denial?: (message: string) => Error;
16
16
  /** The `fetch` to use. Defaults to the global one; injectable for tests. */
17
17
  fetch?: typeof fetch;
package/dist/browser.js CHANGED
@@ -14,6 +14,7 @@
14
14
  // `new URL(..., import.meta.url)` references below resolve in a consumer's
15
15
  // bundler without configuration where that bundler treats them as assets.
16
16
  import { createBrowserIsolate } from './run/browser-isolate.js';
17
+ import { netRefusal } from './run/net-refusal.js';
17
18
  function originOf(entry) {
18
19
  try {
19
20
  const url = new URL(entry);
@@ -82,7 +83,10 @@ export function createFetchNetProvider(options = {}) {
82
83
  const allowed = new Set((options.allowedOrigins ?? [])
83
84
  .map(originOf)
84
85
  .filter((origin) => origin !== undefined));
85
- const denial = options.denial ?? ((message) => new Error(message));
86
+ const build = options.denial ?? ((message) => new Error(message));
87
+ // Every policy refusal is marked so the net bridge can tell it from a
88
+ // transport failure (a plain `Error`).
89
+ const denial = (message) => netRefusal(build(message));
86
90
  return (netAllowlist, maxFetchBytes, _policy, signal) => {
87
91
  // The run's granted hosts, re-checked here on the page side: the worker
88
92
  // checks them too, but the worker is the isolate a hostile script runs
@@ -110,7 +114,7 @@ export function createFetchNetProvider(options = {}) {
110
114
  throw denial(`origin "${url.origin}" is not on the host's allowlist`);
111
115
  }
112
116
  if (signal?.aborted)
113
- throw denial('the run has ended');
117
+ throw new Error('the run has ended');
114
118
  const doFetch = options.fetch ?? fetch;
115
119
  let response;
116
120
  try {
@@ -126,7 +130,10 @@ export function createFetchNetProvider(options = {}) {
126
130
  });
127
131
  }
128
132
  catch (error) {
129
- throw denial(`request failed: ${error instanceof Error ? error.message : String(error)}`);
133
+ // A transport failure (network down, connection refused, CORS,
134
+ // abort): an ordinary runtime failure with the reason, not a policy
135
+ // refusal.
136
+ throw new Error(`request failed: ${error instanceof Error ? error.message : String(error)}`);
130
137
  }
131
138
  if (response.type === 'opaqueredirect') {
132
139
  throw denial('redirects are not followed in a browser');
@@ -2,6 +2,8 @@
2
2
  "outcomes": [
3
3
  {
4
4
  "kind": "exported",
5
+ "valueCount": 0,
6
+ "renderEngine": "static",
5
7
  "contains": ["mk-unknown", "ana_timeline"],
6
8
  "excludes": ["<script"]
7
9
  }
@@ -1,7 +1,12 @@
1
1
  {
2
2
  "outcomes": [
3
3
  { "kind": "opened" },
4
- { "kind": "installed", "namespace": "ana", "replaced": false },
4
+ {
5
+ "kind": "installed",
6
+ "namespace": "ana",
7
+ "replaced": false,
8
+ "installedDirName": "ana"
9
+ },
5
10
  { "kind": "loaded", "namespaces": ["ana"] },
6
11
  { "kind": "installed", "namespace": "ana", "replaced": true }
7
12
  ],
@@ -138,8 +138,10 @@ coarse, matching each `kind`:
138
138
  compares the recorded run trace's `ok` flag.
139
139
  - `exported`: `contains`/`excludes` are substrings checked against the
140
140
  written file's content, never the whole document — the render corpora
141
- already pin bytes.
142
- - `installed`: `namespace` and `replaced`.
141
+ already pin bytes. `valueCount` is how many last-run values were baked
142
+ in, and `renderEngine` (`static` or `react`) is which engine drew the body.
143
+ - `installed`: `namespace` and `replaced`, and `installedDirName`, the name
144
+ of the folder the pack was unzipped into.
143
145
  - `declined`: `step` (`'consent'`/`'replace'`, or absent for the
144
146
  scripts-disabled synthetic outcome) and `reasonContains`, a substring of
145
147
  the user-facing reason.
@@ -71,7 +71,7 @@ export declare const EXPORT_HIDE_SCRIPT_BLOCKS_CLASS = "mk-export--hide-scripts"
71
71
  export declare const EXPORT_PAGE_CSS = "\nbody {\n margin: 0;\n background: #fff;\n}\n.doc {\n box-sizing: border-box;\n max-width: 46rem;\n margin: 0 auto;\n padding: 2.5rem 1.5rem 4rem;\n}\n.doc.mk-export--hide-scripts .mk-script {\n display: none;\n}\n@page {\n margin: 2cm;\n}\n@media print {\n .doc {\n max-width: none;\n margin: 0;\n padding: 0;\n }\n .doc .mk-card,\n .doc .mk-callout,\n .doc .mk-stat,\n .doc .mk-figure,\n .doc .mk-unknown,\n .doc table {\n break-inside: avoid;\n }\n /* A collapsed `details` prints as its summary alone, which loses the\n content the author wrote. On paper there is nothing to expand, so\n every disclosure prints open. */\n .doc details > *:not(summary) {\n display: block;\n }\n .doc .mk-script,\n .doc .mk-preview__run-marker {\n display: none;\n }\n}\n";
72
72
  /**
73
73
  * The static HTML engine, injected so this module imports no renderer.
74
- * A host passes `createHtmlExportEngine()` from `@markii/host/node`, which
74
+ * A host passes `createHtmlExportEngine()` from `@markii/host/html-engine`, which
75
75
  * wraps `@markii/html`; a test may pass a stub.
76
76
  */
77
77
  export interface HtmlExportEngine {
@@ -150,7 +150,7 @@ export interface HostPackSource {
150
150
  * `authorizedFolders` names.
151
151
  */
152
152
  bundledPacks?(): readonly DiscoveredPack[];
153
- /** Removes an installed pack folder. Required when `installRoot` is present. */
153
+ /** Removes an installed pack folder and everything under it. Required when `installRoot` is present: a replacing install clears the old folder through it, and removing a pack uses it. */
154
154
  removeFolder?(path: string): Promise<void>;
155
155
  }
156
156
  /** Every export format any host offers today. */
@@ -194,10 +194,12 @@ export interface HostLabels {
194
194
  *.
195
195
  */
196
196
  export interface HostAdapter {
197
- readonly id: 'vscode' | 'obsidian' | 'cli' | (string & {});
197
+ /** A short name for the host, used in diagnostics wording. */
198
+ readonly id: string;
198
199
  readonly labels: HostLabels;
199
200
  readFile(path: string): Promise<Uint8Array>;
200
201
  exists(path: string): Promise<boolean>;
202
+ /** Writes `bytes` to `path` and creates any missing parent folder first. Exports and pack installs both write through it. */
201
203
  writeFile(path: string, bytes: Uint8Array): Promise<void>;
202
204
  /** A missing folder resolves to an empty list — never throws, matching the existing `PackDirectoryLister` contract. */
203
205
  listFolder(path: string): Promise<readonly HostDirEntry[]>;
@@ -1,4 +1,3 @@
1
- import type { ArchiveExtractFs } from './pack-archive.js';
2
1
  import type { RunTrigger } from '@markii/runtime';
3
2
  import type { HostAdapter } from './adapter.js';
4
3
  /**
@@ -135,8 +134,6 @@ export interface HostScenarioRunResult {
135
134
  */
136
135
  export declare function runHostScenario(adapter: HostAdapter, scenario: HostScenario, noteText: string, scenarioDir: string, options?: {
137
136
  readonly renderAnsi?: (text: string) => Promise<string>;
138
- /** Where an installed pack archive is extracted. Defaults to the Node file system, which is right for an adapter that reads and writes real files. An adapter over another store (memory, a virtual file system) passes the matching writer. */
139
- readonly archiveExtractFs?: ArchiveExtractFs;
140
137
  }): Promise<HostScenarioRunResult>;
141
138
  /** `expected[i]` compared against `actual[i]` for each index up to `expected`'s own length; a length mismatch is reported once rather than index-by-index noise. */
142
139
  export declare function containsInOrder(actual: readonly string[], expected: readonly string[]): string[];
@@ -40,7 +40,6 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs';
40
40
  import { dirname } from 'node:path';
41
41
  import { fileURLToPath } from 'node:url';
42
42
  import { createHtmlExportEngine } from '../html-engine.js';
43
- import { createNodeArchiveExtractFs } from '../node-fs.js';
44
43
  import { usesPackTrust } from './pack-trust-behavior.js';
45
44
  import { join, relative, sep } from 'node:path';
46
45
  import { zipSync } from 'fflate';
@@ -140,10 +139,7 @@ export function hostDeclaresCapability(adapter, action) {
140
139
  if (capability === 'packs') {
141
140
  if (adapter.packs === undefined)
142
141
  return false;
143
- if (action.type === 'installPack') {
144
- return adapter.packs.installRoot !== undefined;
145
- }
146
- if (action.type === 'removePack') {
142
+ if (action.type === 'installPack' || action.type === 'removePack') {
147
143
  return (adapter.packs.installRoot !== undefined &&
148
144
  adapter.packs.removeFolder !== undefined);
149
145
  }
@@ -262,7 +258,6 @@ export async function runHostScenario(adapter, scenario, noteText, scenarioDir,
262
258
  // The runner is Node-only test tooling, so it supplies the real Node
263
259
  // implementations of the seams the host layer takes by injection.
264
260
  htmlEngine: createHtmlExportEngine(),
265
- archiveExtractFs: options.archiveExtractFs ?? createNodeArchiveExtractFs(),
266
261
  spawnRun: (spawnOptions) => Promise.resolve(simulatedRunResult(currentSimulation, spawnOptions)),
267
262
  ...(options.renderAnsi ? { renderAnsi: options.renderAnsi } : {}),
268
263
  });
@@ -452,6 +447,16 @@ export function outcomeMismatches(actual, expected) {
452
447
  break;
453
448
  }
454
449
  case 'exported': {
450
+ if (expected['valueCount'] !== undefined &&
451
+ actualRecord['valueCount'] !== expected['valueCount']) {
452
+ mismatches.push(`valueCount: expected ${String(expected['valueCount'])}, got ${String(actualRecord['valueCount'])}`);
453
+ }
454
+ if (expected['renderEngine'] !== undefined) {
455
+ const render = actualRecord['render'];
456
+ if (render?.engine !== expected['renderEngine']) {
457
+ mismatches.push(`render engine: expected "${String(expected['renderEngine'])}", got "${String(render?.engine)}"`);
458
+ }
459
+ }
455
460
  const content = String(actualRecord['content'] ?? '');
456
461
  for (const needle of expected['contains'] ?? []) {
457
462
  if (!content.includes(needle)) {
@@ -466,6 +471,13 @@ export function outcomeMismatches(actual, expected) {
466
471
  break;
467
472
  }
468
473
  case 'installed': {
474
+ if (expected['installedDirName'] !== undefined) {
475
+ const dir = String(actualRecord['installedDir'] ?? '');
476
+ const name = dir.split(/[/\\]/).pop();
477
+ if (name !== expected['installedDirName']) {
478
+ mismatches.push(`installedDir: expected a folder named "${String(expected['installedDirName'])}", got "${dir}"`);
479
+ }
480
+ }
469
481
  if (expected['namespace'] !== undefined &&
470
482
  actualRecord['namespace'] !== expected['namespace']) {
471
483
  mismatches.push(`namespace: expected "${String(expected['namespace'])}", got "${String(actualRecord['namespace'])}"`);
@@ -18,9 +18,9 @@ import type { StoredValue } from '@markii/runtime';
18
18
  import type { OnDiagnostic, UnavailableComponent } from '@markii/stdlib';
19
19
  import type { CompletionItem, ComponentDocumentation, LineColumn } from '@markii/stdlib/editor';
20
20
  import { type RunViaAdapterRequest, type RunViaAdapterResult } from './run-behavior.js';
21
- import { type ArchiveExtractFs } from './pack-archive.js';
22
21
  import { type PackDiscoveryIo } from './pack-load.js';
23
- import type { HtmlExportEngine } from '../export/note-export.js';
22
+ import type { ExportBodyRenderer, ExportPackStylesheet, ExportRenderInfo, HtmlExportEngine, StaticExportReason } from '../export/note-export.js';
23
+ import type { EmbeddedImageReport, ExportImageReader } from '../export/image-embed.js';
24
24
  import type { DiscoveredPack, SkippedPackFolder } from '../packs/discover.js';
25
25
  import { type TerminalPackScript } from './terminal-packs.js';
26
26
  import { type RemovePackOutcome } from './pack-remove.js';
@@ -61,11 +61,31 @@ export interface ExportRequest {
61
61
  readonly unavailableComponents?: readonly UnavailableComponent[];
62
62
  /** Receives the static engine's render diagnostics, such as the `no-engine-view` event. */
63
63
  readonly onDiagnostic?: OnDiagnostic;
64
+ /** The host's own render of the body (a React pass bound to its loaded packs). Omitted means the static engine renders, and `staticReason` says why. Used by the `html` and `pdf` formats. */
65
+ readonly renderBody?: ExportBodyRenderer;
66
+ /** Why the static engine renders when `renderBody` is omitted. Defaults to `no-packs`. */
67
+ readonly staticReason?: StaticExportReason;
68
+ /** The loaded packs' stylesheets, embedded only when `renderBody` ran. */
69
+ readonly packStylesheets?: readonly ExportPackStylesheet[];
70
+ /** How many loaded packs contributed to the render. Diagnostics only. */
71
+ readonly packCount?: number;
72
+ /** Reads one of the note's local images so the export embeds it as a `data:` URI. Omitted leaves every image source as written. */
73
+ readonly embedImages?: ExportImageReader;
74
+ /** Hides the collapsed script marker in the exported file. Defaults to `false`. */
75
+ readonly hideScriptBlocks?: boolean;
64
76
  }
65
77
  export type ExportOutcome = {
66
78
  readonly kind: 'exported';
67
79
  readonly path: string;
68
80
  readonly bytes: number;
81
+ /** How many last-run values were baked into the file. */
82
+ readonly valueCount: number;
83
+ /** `true` when the note contains a script fence. */
84
+ readonly hasScripts?: boolean;
85
+ /** Which engine rendered the body, and why when it was the static one. */
86
+ readonly render: ExportRenderInfo;
87
+ /** What image embedding did. */
88
+ readonly images: EmbeddedImageReport;
69
89
  } | {
70
90
  readonly kind: 'failed';
71
91
  readonly reason: string;
@@ -82,6 +102,8 @@ export interface InstallPackRequest {
82
102
  export type InstallPackOutcome = {
83
103
  readonly kind: 'installed';
84
104
  readonly namespace: string;
105
+ /** The folder the pack now lives in, under the adapter's install root. */
106
+ readonly installedDir: string;
85
107
  readonly replaced: boolean;
86
108
  } | {
87
109
  readonly kind: 'declined';
@@ -237,9 +259,7 @@ export interface MarkiiHost {
237
259
  */
238
260
  export interface MarkiiHostOptions {
239
261
  readonly spawnRun?: (options: SpawnRunRequest) => Promise<RunResult>;
240
- /** The `.mkp` extraction filesystem `installPack` writes through. Required for a host that installs packs (`createNodeArchiveExtractFs()` from `@markii/host/node`); without it `installPack` is `Unsupported`. */
241
- readonly archiveExtractFs?: ArchiveExtractFs;
242
- /** The static HTML engine behind the `html` and `pdf` exports (`createHtmlExportEngine()` from `@markii/host/node`). Without it those formats decline, like `renderAnsi` for `ansi`. */
262
+ /** The static HTML engine behind the `html` and `pdf` exports (`createHtmlExportEngine()` from `@markii/host/html-engine`). Without it those formats decline, like `renderAnsi` for `ansi`. */
243
263
  readonly htmlEngine?: HtmlExportEngine;
244
264
  /** Pack discovery's file reads. Defaults to reads through the adapter's own `readFile` and `listFolder`. */
245
265
  readonly packDiscoveryIo?: PackDiscoveryIo;
@@ -95,6 +95,24 @@ export function createMarkiiHost(adapter, options = {}) {
95
95
  ...(request.onDiagnostic !== undefined
96
96
  ? { onDiagnostic: request.onDiagnostic }
97
97
  : {}),
98
+ ...(request.renderBody !== undefined
99
+ ? { renderBody: request.renderBody }
100
+ : {}),
101
+ ...(request.staticReason !== undefined
102
+ ? { staticReason: request.staticReason }
103
+ : {}),
104
+ ...(request.packStylesheets !== undefined
105
+ ? { packStylesheets: request.packStylesheets }
106
+ : {}),
107
+ ...(request.packCount !== undefined
108
+ ? { packCount: request.packCount }
109
+ : {}),
110
+ ...(request.embedImages !== undefined
111
+ ? { embedImages: request.embedImages }
112
+ : {}),
113
+ ...(request.hideScriptBlocks !== undefined
114
+ ? { hideScriptBlocks: request.hideScriptBlocks }
115
+ : {}),
98
116
  }, options.renderAnsi);
99
117
  if (outcome === undefined) {
100
118
  adapter.diagnostics(`export declined: format "${request.format}" has no working export path on ${adapter.id} (a cancelled picker, or a missing renderer/PDF seam).`);
@@ -106,47 +124,40 @@ export function createMarkiiHost(adapter, options = {}) {
106
124
  if (outcome.kind === 'failed') {
107
125
  return { kind: 'failed', reason: outcome.reason };
108
126
  }
109
- return { kind: 'exported', path: outcome.path, bytes: outcome.bytes };
127
+ return {
128
+ kind: 'exported',
129
+ path: outcome.path,
130
+ bytes: outcome.bytes,
131
+ valueCount: outcome.valueCount,
132
+ ...(outcome.hasScripts !== undefined
133
+ ? { hasScripts: outcome.hasScripts }
134
+ : {}),
135
+ render: outcome.render,
136
+ images: outcome.images,
137
+ };
110
138
  },
111
139
  async installPack(request) {
112
140
  if (!adapter.packs?.installRoot) {
113
141
  return unsupported(adapter, 'packs', 'this host has no pack-install destination configured.');
114
142
  }
115
143
  const archivePath = request.archivePath ?? 'the selected archive';
116
- const extractFs = options.archiveExtractFs;
117
- if (extractFs === undefined) {
118
- return unsupported(adapter, 'packs', 'this host was given no archive extraction filesystem.');
144
+ const removeFolder = adapter.packs.removeFolder;
145
+ if (removeFolder === undefined) {
146
+ return unsupported(adapter, 'packs', 'this host has no way to remove an installed pack folder.');
119
147
  }
120
148
  const outcome = await installPackFromArchive(adapter, {
121
149
  archiveBytes: request.archiveBytes,
122
150
  archivePath,
123
151
  installRoot: adapter.packs.installRoot(),
124
152
  exists: (path) => adapter.exists(path),
125
- extractFs,
153
+ removeFolder: (path) => removeFolder.call(adapter.packs, path),
126
154
  reservedNamespaces: adapter.packs.reservedNamespaces(),
127
155
  });
128
156
  catalogCache.invalidate();
129
157
  for (const line of installPackDiagnosticLines(outcome, archivePath)) {
130
158
  adapter.diagnostics(line);
131
159
  }
132
- if (outcome.kind === 'installed') {
133
- return {
134
- kind: 'installed',
135
- namespace: outcome.packName,
136
- replaced: outcome.replaced,
137
- };
138
- }
139
- if (outcome.kind === 'declined') {
140
- return {
141
- kind: 'declined',
142
- step: outcome.step,
143
- namespace: outcome.packName,
144
- };
145
- }
146
- if (outcome.kind === 'reserved') {
147
- return { kind: 'reserved', namespace: outcome.packName };
148
- }
149
- return { kind: 'rejected', reason: outcome.reason };
160
+ return outcome;
150
161
  },
151
162
  async loadPacks() {
152
163
  if (!adapter.packs) {
@@ -30,6 +30,7 @@ import type { OnDiagnostic, UnavailableComponent } from '@markii/stdlib';
30
30
  import type { HtmlExportEngine, ExportBodyRenderer, ExportPackStylesheet, ExportRenderInfo, StaticExportReason } from '../export/note-export.js';
31
31
  import type { EmbeddedImageReport, ExportImageReader } from '../export/image-embed.js';
32
32
  import type { HostAdapter, HostExportFormat, HostLabels } from './adapter.js';
33
+ import type { ExportOutcome } from './create-host.js';
33
34
  /** The file name an export lands under, by format: the note's own base name with the format's own extension, so the export lands beside the note unless the host's picker says otherwise. */
34
35
  export declare function exportDefaultFileName(notePath: string, format: HostExportFormat): string;
35
36
  /** What a `'html'` or `'pdf'` export attempt produced. Every shape except `failed` means the host got a file, and every shape but `failed` carries the same diagnostics-facing detail so the two hosts' wording can share it. */
@@ -58,7 +59,7 @@ export declare function fileNameOf(path: string): string;
58
59
  * host's own diagnostics surface (`labels.diagnosticsSurface`), never at
59
60
  * a stack trace — the verbatim reason goes there instead.
60
61
  */
61
- export declare function exportResultMessage(outcome: NoteFileExportOutcome, labels: HostLabels): string;
62
+ export declare function exportResultMessage(outcome: ExportOutcome, labels: HostLabels): string;
62
63
  /** One diagnostic line describing HOW an export's body was rendered: the pack-vs-static distinction, kept off the result message and put here instead. */
63
64
  export declare function renderEngineDiagnosticLine(render: ExportRenderInfo): string;
64
65
  /** `bytes`, formatted as a human-readable size (`"1.2 KB"`, `"512 bytes"`). */
@@ -23,7 +23,7 @@ export function fileNameOf(path) {
23
23
  * a stack trace — the verbatim reason goes there instead.
24
24
  */
25
25
  export function exportResultMessage(outcome, labels) {
26
- if (outcome.kind === 'failed') {
26
+ if (outcome.kind !== 'exported') {
27
27
  return `Markii: could not export this note. Open ${labels.diagnosticsSurface} for details.`;
28
28
  }
29
29
  const name = fileNameOf(outcome.path);
@@ -1,20 +1,11 @@
1
1
  /**
2
- * `HostLabels`: the wording nouns every shared behavior module
3
- * (`./script-execution.ts`, `./refresh-interval.ts`) interpolates, and the
4
- * one concrete record per app this repo ships. Each app's `HostAdapter`
5
- * imports its record rather than inventing its own strings, which is what
6
- * lets one template serve every host.
2
+ * The shared wording templates a host's shared behaviors interpolate
3
+ * `HostLabels` into live beside each behavior. This module holds the one
4
+ * label sentence that has no per-host noun. Each app owns its own
5
+ * `HostLabels` record and passes it as `HostAdapter.labels`.
7
6
  *
8
- * Pure and dependency-free: re-exported from `../browser.ts` as well as
9
- * `../index.ts` so VS Code's Node-free webview bundle can read it too.
7
+ * Pure and dependency-free.
10
8
  */
11
- import type { HostLabels } from './adapter.js';
12
- /** The VS Code extension's labels, taken from its current wording (`apps/vscode/src/script-execution.ts`, `apps/vscode/src/extension.ts`'s output channel). */
13
- export declare const VSCODE_LABELS: HostLabels;
14
- /** The Obsidian plugin's labels, taken from its current wording (`apps/obsidian/src/script-execution.ts`, docs/integration.md's host checklist). */
15
- export declare const OBSIDIAN_LABELS: HostLabels;
16
- /** The `markii` command line tool's labels: it has no persistent preview panel, and its diagnostics surface is stderr, gated behind `--verbose` for anything short of a failure (`apps/cli/src/main.ts`). */
17
- export declare const CLI_LABELS: HostLabels;
18
9
  /**
19
10
  * The quiet marker a rendered note shows when its `uses:` list names packs
20
11
  * this host has not installed (docs/packs.md). One sentence for every host;
@@ -1,25 +1,11 @@
1
- /** The VS Code extension's labels, taken from its current wording (`apps/vscode/src/script-execution.ts`, `apps/vscode/src/extension.ts`'s output channel). */
2
- export const VSCODE_LABELS = {
3
- appNoun: 'extension',
4
- previewNoun: 'the Markii preview',
5
- diagnosticsSurface: 'the Markii output channel',
6
- deviceNoun: 'this device',
7
- settingName: 'markii.scriptsDisabled',
8
- };
9
- /** The Obsidian plugin's labels, taken from its current wording (`apps/obsidian/src/script-execution.ts`, docs/integration.md's host checklist). */
10
- export const OBSIDIAN_LABELS = {
11
- appNoun: 'plugin',
12
- previewNoun: 'the Markii preview',
13
- diagnosticsSurface: 'the Markii diagnostics',
14
- deviceNoun: 'this device',
15
- };
16
- /** The `markii` command line tool's labels: it has no persistent preview panel, and its diagnostics surface is stderr, gated behind `--verbose` for anything short of a failure (`apps/cli/src/main.ts`). */
17
- export const CLI_LABELS = {
18
- appNoun: 'command line tool',
19
- previewNoun: 'the rendered note',
20
- diagnosticsSurface: 'stderr',
21
- deviceNoun: 'this device',
22
- };
1
+ /**
2
+ * The shared wording templates a host's shared behaviors interpolate
3
+ * `HostLabels` into live beside each behavior. This module holds the one
4
+ * label sentence that has no per-host noun. Each app owns its own
5
+ * `HostLabels` record and passes it as `HostAdapter.labels`.
6
+ *
7
+ * Pure and dependency-free.
8
+ */
23
9
  /**
24
10
  * The quiet marker a rendered note shows when its `uses:` list names packs
25
11
  * this host has not installed (docs/packs.md). One sentence for every host;
@@ -1,14 +1,8 @@
1
1
  import type { PackArchiveContents, PackArchiveError } from '@markii/pack';
2
2
  /** One line describing why `openPackArchive` rejected an archive, for a host's install-diagnostics wording. Passes `kind: 'zip'`'s and `kind: 'missing-entry'`'s message through verbatim (already specific); summarizes a manifest failure plainly. */
3
3
  export declare function describeArchiveError(error: PackArchiveError): string;
4
- /** The filesystem write seam `writeArchiveContents` needs (`./pack-install.ts`). Deliberately narrow: extracting an archive only ever creates files under a directory it first clears, it never reads back or deletes anything else. */
5
- export interface ArchiveExtractFs {
6
- /** Removes `absolutePath` and everything under it, or resolves quietly if it does not exist. Never rejects: a directory that fails to clear is treated the same as a fresh one, and the write that follows fails loudly on its own if the path is genuinely unusable. */
7
- readonly removeDirectory: (absolutePath: string) => Promise<void>;
8
- /** Creates a directory and any missing parents. May reject on a genuine I/O failure. */
9
- readonly makeDirectory: (absolutePath: string) => Promise<void>;
10
- readonly writeFile: (absolutePath: string, bytes: Uint8Array) => Promise<void>;
11
- }
4
+ /** Writes one file and creates its missing parent folders: `HostAdapter.writeFile`. */
5
+ export type ArchiveFileWriter = (absolutePath: string, bytes: Uint8Array) => Promise<void>;
12
6
  /**
13
7
  * Writes a validated archive's contents into `destinationDir`: `pack.json`
14
8
  * (the parsed manifest, re-serialized: `openPackArchive` keeps only the
@@ -17,7 +11,7 @@ export interface ArchiveExtractFs {
17
11
  * copy), `webview.js`, `webview.css` and `terminal.js` when the archive has them, and
18
12
  * `scripts/*` when it ships any. Does NOT clear `destinationDir` first;
19
13
  * a caller that needs a clean re-extract (a reinstall replacing an
20
- * existing pack) calls `fs.removeDirectory` itself before this.
14
+ * existing pack) removes the folder itself before this.
21
15
  *
22
16
  * PATH JAIL: `@markii/pack`'s `openPackArchive` has already rejected any
23
17
  * entry whose name escapes the archive (`../`, an absolute path) or
@@ -27,4 +21,4 @@ export interface ArchiveExtractFs {
27
21
  * `webview.css`, or a `scripts/`-relative Lua module name), never from
28
22
  * anything read back out of the zip's raw entry names.
29
23
  */
30
- export declare function writeArchiveContents(archive: PackArchiveContents, destinationDir: string, fs: ArchiveExtractFs): Promise<void>;
24
+ export declare function writeArchiveContents(archive: PackArchiveContents, destinationDir: string, writeFile: ArchiveFileWriter): Promise<void>;
@@ -1,18 +1,17 @@
1
1
  /**
2
2
  * `.mkp` pack archive handling, merged from `apps/vscode/src/packs/archive-packs.ts`
3
3
  * and `apps/obsidian/src/packs/archive-packs.ts`. It exports
4
- * `describeArchiveError`, `writeArchiveContents` and `ArchiveExtractFs`.
4
+ * `describeArchiveError`, `writeArchiveContents` and `ArchiveFileWriter`.
5
5
  *
6
6
  * This module bridges `@markii/pack`'s in-memory archive reader
7
7
  * (`openPackArchive`, never modified here) into the write a pack install
8
8
  * needs: extracting a validated archive's contents onto disk, under
9
9
  * whichever install root the calling host names (`HostPackSource.installRoot`).
10
10
  *
11
- * Environment-free (`ArchiveExtractFs` is an injected seam). The real
12
- * Node-backed one, `createNodeArchiveExtractFs`, lives in `../node-fs.ts`
13
- * and is exported from `@markii/host/node`.
11
+ * Environment-free: the write goes through the host adapter's `writeFile`,
12
+ * which creates any missing parent folder.
14
13
  */
15
- import { dirnamePath, joinPath } from '../path-util.js';
14
+ import { joinPath } from '../path-util.js';
16
15
  /** One line describing why `openPackArchive` rejected an archive, for a host's install-diagnostics wording. Passes `kind: 'zip'`'s and `kind: 'missing-entry'`'s message through verbatim (already specific); summarizes a manifest failure plainly. */
17
16
  export function describeArchiveError(error) {
18
17
  if (error.kind === 'zip')
@@ -29,7 +28,7 @@ export function describeArchiveError(error) {
29
28
  * copy), `webview.js`, `webview.css` and `terminal.js` when the archive has them, and
30
29
  * `scripts/*` when it ships any. Does NOT clear `destinationDir` first;
31
30
  * a caller that needs a clean re-extract (a reinstall replacing an
32
- * existing pack) calls `fs.removeDirectory` itself before this.
31
+ * existing pack) removes the folder itself before this.
33
32
  *
34
33
  * PATH JAIL: `@markii/pack`'s `openPackArchive` has already rejected any
35
34
  * entry whose name escapes the archive (`../`, an absolute path) or
@@ -39,24 +38,22 @@ export function describeArchiveError(error) {
39
38
  * `webview.css`, or a `scripts/`-relative Lua module name), never from
40
39
  * anything read back out of the zip's raw entry names.
41
40
  */
42
- export async function writeArchiveContents(archive, destinationDir, fs) {
43
- await fs.makeDirectory(destinationDir);
41
+ export async function writeArchiveContents(archive, destinationDir, writeFile) {
44
42
  const encoder = new TextEncoder();
45
- await fs.writeFile(joinPath(destinationDir, 'pack.json'), encoder.encode(JSON.stringify(archive.manifest, null, 2) + '\n'));
46
- await fs.writeFile(joinPath(destinationDir, 'webview.js'), archive.scriptBytes);
43
+ await writeFile(joinPath(destinationDir, 'pack.json'), encoder.encode(JSON.stringify(archive.manifest, null, 2) + '\n'));
44
+ await writeFile(joinPath(destinationDir, 'webview.js'), archive.scriptBytes);
47
45
  if (archive.stylesheetBytes !== undefined) {
48
- await fs.writeFile(joinPath(destinationDir, 'webview.css'), archive.stylesheetBytes);
46
+ await writeFile(joinPath(destinationDir, 'webview.css'), archive.stylesheetBytes);
49
47
  }
50
48
  if (archive.terminalScriptBytes !== undefined) {
51
- await fs.writeFile(joinPath(destinationDir, 'terminal.js'), archive.terminalScriptBytes);
49
+ await writeFile(joinPath(destinationDir, 'terminal.js'), archive.terminalScriptBytes);
52
50
  }
53
51
  const moduleNames = Object.keys(archive.scriptModules);
54
52
  if (moduleNames.length > 0) {
55
53
  const scriptsDir = joinPath(destinationDir, 'scripts');
56
54
  for (const name of moduleNames) {
57
55
  const target = joinPath(scriptsDir, name);
58
- await fs.makeDirectory(dirnamePath(target));
59
- await fs.writeFile(target, archive.scriptModules[name]);
56
+ await writeFile(target, archive.scriptModules[name]);
60
57
  }
61
58
  }
62
59
  }
@@ -1,5 +1,5 @@
1
- import type { ArchiveExtractFs } from './pack-archive.js';
2
1
  import type { HostAdapter, HostLabels } from './adapter.js';
2
+ import type { InstallPackOutcome as HostInstallPackOutcome } from './create-host.js';
3
3
  /** Whether a directory already exists under the install root, injected so this module needs no real disk to test. */
4
4
  export type PackDirectoryExists = (absolutePath: string) => Promise<boolean>;
5
5
  export interface InstallPackFromArchiveOptions {
@@ -9,7 +9,8 @@ export interface InstallPackFromArchiveOptions {
9
9
  /** This host's own pack-install directory (`HostPackSource.installRoot()`). Each installed pack gets a subdirectory named by its namespace, since two packs can never share one (docs/packs.md's collision rule), which makes "is this namespace already installed" a plain directory check. */
10
10
  readonly installRoot: string;
11
11
  readonly exists: PackDirectoryExists;
12
- readonly extractFs: ArchiveExtractFs;
12
+ /** Removes the installed folder before a replacing install (`HostPackSource.removeFolder`). Called only when the namespace is already installed. */
13
+ readonly removeFolder: (absolutePath: string) => Promise<void>;
13
14
  /**
14
15
  * Namespaces this install may never claim (`HostPackSource.reservedNamespaces()`).
15
16
  * Empty for a host with no reserved set (VS Code). Checked before the
@@ -17,22 +18,10 @@ export interface InstallPackFromArchiveOptions {
17
18
  */
18
19
  readonly reservedNamespaces: ReadonlySet<string>;
19
20
  }
20
- export type InstallPackOutcome = {
21
- readonly kind: 'installed';
22
- readonly packName: string;
23
- readonly installedDir: string;
24
- readonly replaced: boolean;
25
- } | {
26
- readonly kind: 'declined';
27
- readonly step: 'consent' | 'replace';
28
- readonly packName: string;
29
- } | {
30
- readonly kind: 'reserved';
31
- readonly packName: string;
32
- } | {
33
- readonly kind: 'rejected';
34
- readonly reason: string;
35
- };
21
+ /** What one install attempt produced, in the shape `createMarkiiHost(...).installPack` returns (minus `unsupported`, which only the host behavior can say). */
22
+ export type InstallAttemptOutcome = Exclude<HostInstallPackOutcome, {
23
+ readonly kind: 'unsupported';
24
+ }>;
36
25
  /**
37
26
  * Never throws: a validation failure, a declined prompt, or a write
38
27
  * failure all come back as a structured outcome. Order: validate the
@@ -47,7 +36,7 @@ export type InstallPackOutcome = {
47
36
  * a host that can only show a non-blocking surface must deny rather than
48
37
  * assume yes.
49
38
  */
50
- export declare function installPackFromArchive(adapter: HostAdapter, options: InstallPackFromArchiveOptions): Promise<InstallPackOutcome>;
39
+ export declare function installPackFromArchive(adapter: HostAdapter, options: InstallPackFromArchiveOptions): Promise<InstallAttemptOutcome>;
51
40
  /**
52
41
  * The consent prompt's wording (AGENTS.md's product principles: "clean is
53
42
  * not silent," and a consent step must say plainly what it authorizes).
@@ -82,6 +71,6 @@ export declare function installReplaceConfirmMessage(packName: string): string;
82
71
  * `markii.packs` before anything loads it), so it doesn't. Defaults to
83
72
  * `false`.
84
73
  */
85
- export declare function installPackMessage(outcome: InstallPackOutcome, archivePath: string, labels: HostLabels, reloadsAutomatically?: boolean): string;
74
+ export declare function installPackMessage(outcome: HostInstallPackOutcome, archivePath: string, labels: HostLabels, reloadsAutomatically?: boolean): string;
86
75
  /** The full diagnostics-surface detail for one install attempt (AGENTS.md's "clean is not silent": the other of a failure's two homes). */
87
- export declare function installPackDiagnosticLines(outcome: InstallPackOutcome, archivePath: string): string[];
76
+ export declare function installPackDiagnosticLines(outcome: InstallAttemptOutcome, archivePath: string): string[];