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

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 (55) hide show
  1. package/README.md +34 -1
  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/17-export-linked-set/expected.json +17 -0
  9. package/dist/conformance/host/17-export-linked-set/files/second.mk.md +3 -0
  10. package/dist/conformance/host/17-export-linked-set/note.mk.md +4 -0
  11. package/dist/conformance/host/17-export-linked-set/scenario.json +6 -0
  12. package/dist/conformance/host/README.md +17 -12
  13. package/dist/diagnostics/render-diagnostics.d.ts +1 -1
  14. package/dist/export/note-export.d.ts +1 -1
  15. package/dist/host/adapter.d.ts +13 -3
  16. package/dist/host/cascade-export-behavior.d.ts +24 -8
  17. package/dist/host/cascade-export-behavior.js +61 -1
  18. package/dist/host/conformance-runner.d.ts +3 -4
  19. package/dist/host/conformance-runner.js +58 -8
  20. package/dist/host/create-host.d.ts +65 -8
  21. package/dist/host/create-host.js +54 -23
  22. package/dist/host/export-behavior.d.ts +2 -1
  23. package/dist/host/export-behavior.js +1 -1
  24. package/dist/host/labels.d.ts +5 -14
  25. package/dist/host/labels.js +8 -22
  26. package/dist/host/pack-archive.d.ts +4 -10
  27. package/dist/host/pack-archive.js +11 -14
  28. package/dist/host/pack-install.d.ts +10 -21
  29. package/dist/host/pack-install.js +22 -16
  30. package/dist/host/pack-trust-behavior.js +1 -1
  31. package/dist/host/run-behavior.d.ts +4 -0
  32. package/dist/host/run-behavior.js +9 -1
  33. package/dist/host/terminal-packs.d.ts +1 -1
  34. package/dist/html-engine.js +4 -4
  35. package/dist/index.d.ts +19 -81
  36. package/dist/index.js +19 -62
  37. package/dist/internal.d.ts +28 -0
  38. package/dist/internal.js +31 -0
  39. package/dist/node-fs.d.ts +0 -3
  40. package/dist/node-fs.js +1 -21
  41. package/dist/node-worker.d.ts +8 -0
  42. package/dist/node-worker.js +36 -15
  43. package/dist/node.d.ts +1 -2
  44. package/dist/node.js +4 -3
  45. package/dist/packs/discover.d.ts +2 -0
  46. package/dist/packs/discover.js +1 -0
  47. package/dist/run/bundle-run.d.ts +1 -1
  48. package/dist/run/bundle-run.js +1 -1
  49. package/dist/run/net-bridge.js +7 -5
  50. package/dist/run/net-pinning.d.ts +2 -0
  51. package/dist/run/net-pinning.js +17 -3
  52. package/dist/run/net-provider.js +23 -12
  53. package/dist/run/net-refusal.d.ts +4 -0
  54. package/dist/run/net-refusal.js +24 -0
  55. package/package.json +28 -12
@@ -18,9 +18,11 @@ 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
+ import type { CascadeLinkResolver } from '../export/cascade.js';
25
+ import { type CascadeExportOutcome } from './cascade-export-behavior.js';
24
26
  import type { DiscoveredPack, SkippedPackFolder } from '../packs/discover.js';
25
27
  import { type TerminalPackScript } from './terminal-packs.js';
26
28
  import { type RemovePackOutcome } from './pack-remove.js';
@@ -28,7 +30,7 @@ import type { DisablePackOutcome, EnablePackOutcome, InstalledPacksListing } fro
28
30
  /** The shared decline shape every behavior returns when the adapter lacks the capability it needs. */
29
31
  export interface Unsupported {
30
32
  readonly kind: 'unsupported';
31
- readonly capability: 'isolate' | 'editor' | 'packs' | 'pack-trust' | 'export-format';
33
+ readonly capability: 'isolate' | 'editor' | 'packs' | 'pack-trust' | 'export-format' | 'linked-set-export';
32
34
  readonly detail?: string;
33
35
  }
34
36
  export interface OpenRequest {
@@ -61,11 +63,31 @@ export interface ExportRequest {
61
63
  readonly unavailableComponents?: readonly UnavailableComponent[];
62
64
  /** Receives the static engine's render diagnostics, such as the `no-engine-view` event. */
63
65
  readonly onDiagnostic?: OnDiagnostic;
66
+ /** 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. */
67
+ readonly renderBody?: ExportBodyRenderer;
68
+ /** Why the static engine renders when `renderBody` is omitted. Defaults to `no-packs`. */
69
+ readonly staticReason?: StaticExportReason;
70
+ /** The loaded packs' stylesheets, embedded only when `renderBody` ran. */
71
+ readonly packStylesheets?: readonly ExportPackStylesheet[];
72
+ /** How many loaded packs contributed to the render. Diagnostics only. */
73
+ readonly packCount?: number;
74
+ /** Reads one of the note's local images so the export embeds it as a `data:` URI. Omitted leaves every image source as written. */
75
+ readonly embedImages?: ExportImageReader;
76
+ /** Hides the collapsed script marker in the exported file. Defaults to `false`. */
77
+ readonly hideScriptBlocks?: boolean;
64
78
  }
65
79
  export type ExportOutcome = {
66
80
  readonly kind: 'exported';
67
81
  readonly path: string;
68
82
  readonly bytes: number;
83
+ /** How many last-run values were baked into the file. */
84
+ readonly valueCount: number;
85
+ /** `true` when the note contains a script fence. */
86
+ readonly hasScripts?: boolean;
87
+ /** Which engine rendered the body, and why when it was the static one. */
88
+ readonly render: ExportRenderInfo;
89
+ /** What image embedding did. */
90
+ readonly images: EmbeddedImageReport;
69
91
  } | {
70
92
  readonly kind: 'failed';
71
93
  readonly reason: string;
@@ -74,6 +96,41 @@ export type ExportOutcome = {
74
96
  | {
75
97
  readonly kind: 'cancelled';
76
98
  } | Unsupported;
99
+ export interface ExportLinkedSetRequest {
100
+ /** The root note's path, in whatever form `adapter.readFile` and `resolveLink` understand. */
101
+ readonly rootPath: string;
102
+ /** Resolves one link in a note to the path of a note this host can export, or `undefined` to leave the link as written. */
103
+ readonly resolveLink: CascadeLinkResolver;
104
+ /** Reads one note's persisted last-run values, keyed by that note's own path. An empty object exports that note's standard empty states. */
105
+ readonly readValues: (notePath: string) => Record<string, StoredValue>;
106
+ /** The host's own render of each note's body. Omitted renders every note with the static engine, and `staticReason` says why. */
107
+ readonly renderBody?: ExportBodyRenderer;
108
+ /** Why the static engine renders when `renderBody` is omitted. Defaults to `no-packs`. */
109
+ readonly staticReason?: StaticExportReason;
110
+ /** The loaded packs' stylesheets, embedded only when `renderBody` ran. */
111
+ readonly packStylesheets?: readonly ExportPackStylesheet[];
112
+ /** How many loaded packs contributed to the render. Diagnostics only. */
113
+ readonly packCount?: number;
114
+ /** Installed pack components the static engine cannot draw. */
115
+ readonly unavailableComponents?: readonly UnavailableComponent[];
116
+ /** Receives the static engine's render diagnostics for every note. */
117
+ readonly onDiagnostic?: OnDiagnostic;
118
+ /** Builds the image reader for one note, bound to that note's own folder. Omitted embeds no images. */
119
+ readonly embedImagesFor?: (notePath: string) => ExportImageReader;
120
+ /** Hides the collapsed script marker in every exported note. Defaults to `false`. */
121
+ readonly hideScriptBlocks?: boolean;
122
+ }
123
+ export type ExportLinkedSetOutcome = Extract<CascadeExportOutcome, {
124
+ kind: 'exported';
125
+ }> | {
126
+ readonly kind: 'failed';
127
+ readonly path?: string;
128
+ readonly reason: string;
129
+ }
130
+ /** The host's picker was cancelled, so nothing was written. */
131
+ | {
132
+ readonly kind: 'cancelled';
133
+ } | Unsupported;
77
134
  export interface InstallPackRequest {
78
135
  readonly archiveBytes: Uint8Array;
79
136
  /** The archive's own path or display name, used only in wording. Defaults to a generic phrase when omitted. */
@@ -82,6 +139,8 @@ export interface InstallPackRequest {
82
139
  export type InstallPackOutcome = {
83
140
  readonly kind: 'installed';
84
141
  readonly namespace: string;
142
+ /** The folder the pack now lives in, under the adapter's install root. */
143
+ readonly installedDir: string;
85
144
  readonly replaced: boolean;
86
145
  } | {
87
146
  readonly kind: 'declined';
@@ -196,6 +255,8 @@ export interface MarkiiHost {
196
255
  open(request: OpenRequest): Promise<OpenOutcome>;
197
256
  run(request: RunRequest): Promise<RunOutcome>;
198
257
  exportNote(request: ExportRequest): Promise<ExportOutcome>;
258
+ /** Exports a note and the notes it links to as one zip archive of HTML files. */
259
+ exportLinkedSet(request: ExportLinkedSetRequest): Promise<ExportLinkedSetOutcome>;
199
260
  installPack(request: InstallPackRequest): Promise<InstallPackOutcome>;
200
261
  loadPacks(): Promise<LoadPacksOutcome>;
201
262
  /**
@@ -237,15 +298,11 @@ export interface MarkiiHost {
237
298
  */
238
299
  export interface MarkiiHostOptions {
239
300
  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`. */
301
+ /** 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
302
  readonly htmlEngine?: HtmlExportEngine;
244
303
  /** Pack discovery's file reads. Defaults to reads through the adapter's own `readFile` and `listFolder`. */
245
304
  readonly packDiscoveryIo?: PackDiscoveryIo;
246
305
  /** `@markii/ansi`'s renderer, injected because this package never depends on `@markii/ansi` (a platform renderer, never a `@markii/host` dependency). A host declaring `'ansi'` in `adapter.exports.formats` without supplying this gets `Unsupported` at export time. */
247
306
  readonly renderAnsi?: (text: string) => Promise<string>;
248
- /** Composes a pack-aware render registry (webview registration, in-process evaluation, ...). Reserved for a future slice that renders through `exportNote`'s `html` path with packs; unused today, since `buildExportDocument`'s default (no `renderBody`) already produces a correct, pack-free static export. */
249
- readonly buildPackRegistration?: unknown;
250
307
  }
251
308
  export declare function createMarkiiHost(adapter: HostAdapter, options?: MarkiiHostOptions): MarkiiHost;
@@ -1,6 +1,8 @@
1
1
  import { runViaAdapter, } from './run-behavior.js';
2
2
  import { installPackDiagnosticLines, installPackFromArchive, } from './pack-install.js';
3
3
  import { loadPacksViaAdapter, packDiscoveryIoFromAdapter, } from './pack-load.js';
4
+ import { exportBaseName } from '../export/note-export.js';
5
+ import { exportLinkedSetViaAdapter, } from './cascade-export-behavior.js';
4
6
  import { buildComponentCatalog } from '../insert/component-catalog.js';
5
7
  import { formatPackDiagnosticLines, skippedPackFolderLine, } from '../packs/pack-diagnostics.js';
6
8
  import { readTerminalPackScripts, } from './terminal-packs.js';
@@ -95,6 +97,24 @@ export function createMarkiiHost(adapter, options = {}) {
95
97
  ...(request.onDiagnostic !== undefined
96
98
  ? { onDiagnostic: request.onDiagnostic }
97
99
  : {}),
100
+ ...(request.renderBody !== undefined
101
+ ? { renderBody: request.renderBody }
102
+ : {}),
103
+ ...(request.staticReason !== undefined
104
+ ? { staticReason: request.staticReason }
105
+ : {}),
106
+ ...(request.packStylesheets !== undefined
107
+ ? { packStylesheets: request.packStylesheets }
108
+ : {}),
109
+ ...(request.packCount !== undefined
110
+ ? { packCount: request.packCount }
111
+ : {}),
112
+ ...(request.embedImages !== undefined
113
+ ? { embedImages: request.embedImages }
114
+ : {}),
115
+ ...(request.hideScriptBlocks !== undefined
116
+ ? { hideScriptBlocks: request.hideScriptBlocks }
117
+ : {}),
98
118
  }, options.renderAnsi);
99
119
  if (outcome === undefined) {
100
120
  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 +126,58 @@ export function createMarkiiHost(adapter, options = {}) {
106
126
  if (outcome.kind === 'failed') {
107
127
  return { kind: 'failed', reason: outcome.reason };
108
128
  }
109
- return { kind: 'exported', path: outcome.path, bytes: outcome.bytes };
129
+ return {
130
+ kind: 'exported',
131
+ path: outcome.path,
132
+ bytes: outcome.bytes,
133
+ valueCount: outcome.valueCount,
134
+ ...(outcome.hasScripts !== undefined
135
+ ? { hasScripts: outcome.hasScripts }
136
+ : {}),
137
+ render: outcome.render,
138
+ images: outcome.images,
139
+ };
140
+ },
141
+ async exportLinkedSet(request) {
142
+ if (adapter.exports?.resolveLinkedSetTarget === undefined) {
143
+ return unsupported(adapter, 'linked-set-export', `the linked-set export is not available on ${adapter.id}.`);
144
+ }
145
+ if (options.htmlEngine === undefined) {
146
+ return unsupported(adapter, 'linked-set-export', 'no HTML export engine was supplied, so the linked-set export cannot render.');
147
+ }
148
+ const outcome = await exportLinkedSetViaAdapter(adapter, {
149
+ ...request,
150
+ engine: options.htmlEngine,
151
+ suggestedFileName: `${exportBaseName(request.rootPath)}.zip`,
152
+ });
153
+ if (outcome === undefined) {
154
+ adapter.diagnostics(`linked-set export declined: no archive target was chosen on ${adapter.id}.`);
155
+ return { kind: 'cancelled' };
156
+ }
157
+ return outcome;
110
158
  },
111
159
  async installPack(request) {
112
160
  if (!adapter.packs?.installRoot) {
113
161
  return unsupported(adapter, 'packs', 'this host has no pack-install destination configured.');
114
162
  }
115
163
  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.');
164
+ const removeFolder = adapter.packs.removeFolder;
165
+ if (removeFolder === undefined) {
166
+ return unsupported(adapter, 'packs', 'this host has no way to remove an installed pack folder.');
119
167
  }
120
168
  const outcome = await installPackFromArchive(adapter, {
121
169
  archiveBytes: request.archiveBytes,
122
170
  archivePath,
123
171
  installRoot: adapter.packs.installRoot(),
124
172
  exists: (path) => adapter.exists(path),
125
- extractFs,
173
+ removeFolder: (path) => removeFolder.call(adapter.packs, path),
126
174
  reservedNamespaces: adapter.packs.reservedNamespaces(),
127
175
  });
128
176
  catalogCache.invalidate();
129
177
  for (const line of installPackDiagnosticLines(outcome, archivePath)) {
130
178
  adapter.diagnostics(line);
131
179
  }
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 };
180
+ return outcome;
150
181
  },
151
182
  async loadPacks() {
152
183
  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[];
@@ -39,7 +39,7 @@ function describeThrown(err) {
39
39
  * assume yes.
40
40
  */
41
41
  export async function installPackFromArchive(adapter, options) {
42
- const { archiveBytes, archivePath, installRoot, exists, extractFs, reservedNamespaces, } = options;
42
+ const { archiveBytes, archivePath, installRoot, exists, removeFolder, reservedNamespaces, } = options;
43
43
  const opened = await openPackArchive(archiveBytes);
44
44
  if (!opened.ok) {
45
45
  return {
@@ -49,7 +49,7 @@ export async function installPackFromArchive(adapter, options) {
49
49
  }
50
50
  const packName = opened.archive.manifest.name;
51
51
  if (reservedNamespaces.has(packName)) {
52
- return { kind: 'reserved', packName };
52
+ return { kind: 'reserved', namespace: packName };
53
53
  }
54
54
  let consented;
55
55
  try {
@@ -65,7 +65,7 @@ export async function installPackFromArchive(adapter, options) {
65
65
  return { kind: 'rejected', reason: describeThrown(err) };
66
66
  }
67
67
  if (!consented) {
68
- return { kind: 'declined', step: 'consent', packName };
68
+ return { kind: 'declined', step: 'consent', namespace: packName };
69
69
  }
70
70
  const targetDir = joinPath(installRoot, packName);
71
71
  let alreadyInstalled;
@@ -91,13 +91,14 @@ export async function installPackFromArchive(adapter, options) {
91
91
  return { kind: 'rejected', reason: describeThrown(err) };
92
92
  }
93
93
  if (!proceed) {
94
- return { kind: 'declined', step: 'replace', packName };
94
+ return { kind: 'declined', step: 'replace', namespace: packName };
95
95
  }
96
96
  replaced = true;
97
97
  }
98
98
  try {
99
- await extractFs.removeDirectory(targetDir);
100
- await writeArchiveContents(opened.archive, targetDir, extractFs);
99
+ if (alreadyInstalled)
100
+ await removeFolder(targetDir);
101
+ await writeArchiveContents(opened.archive, targetDir, (path, bytes) => adapter.writeFile(path, bytes));
101
102
  }
102
103
  catch (err) {
103
104
  return {
@@ -106,7 +107,12 @@ export async function installPackFromArchive(adapter, options) {
106
107
  };
107
108
  }
108
109
  await trustInstalledPack(adapter, packName, targetDir);
109
- return { kind: 'installed', packName, installedDir: targetDir, replaced };
110
+ return {
111
+ kind: 'installed',
112
+ namespace: packName,
113
+ installedDir: targetDir,
114
+ replaced,
115
+ };
110
116
  }
111
117
  /**
112
118
  * The consent prompt's wording (AGENTS.md's product principles: "clean is
@@ -147,21 +153,21 @@ export function installReplaceConfirmMessage(packName) {
147
153
  * `false`.
148
154
  */
149
155
  export function installPackMessage(outcome, archivePath, labels, reloadsAutomatically = false) {
150
- if (outcome.kind === 'rejected') {
156
+ if (outcome.kind === 'rejected' || outcome.kind === 'unsupported') {
151
157
  return `Markii: could not install a pack from "${archivePath}". Open ${labels.diagnosticsSurface} for details.`;
152
158
  }
153
159
  if (outcome.kind === 'reserved') {
154
- return `Markii: "${outcome.packName}" is built in and cannot be installed. Nothing was installed.`;
160
+ return `Markii: "${outcome.namespace}" is built in and cannot be installed. Nothing was installed.`;
155
161
  }
156
162
  if (outcome.kind === 'declined') {
157
163
  return outcome.step === 'consent'
158
- ? `Markii: install of "${outcome.packName}" cancelled. Nothing was installed.`
159
- : `Markii: install of "${outcome.packName}" cancelled. The existing pack was kept.`;
164
+ ? `Markii: install of "${outcome.namespace}" cancelled. Nothing was installed.`
165
+ : `Markii: install of "${outcome.namespace}" cancelled. The existing pack was kept.`;
160
166
  }
161
167
  const reloadSuffix = reloadsAutomatically ? ' Markii packs reloaded.' : '';
162
168
  return outcome.replaced
163
- ? `Markii: reinstalled pack "${outcome.packName}".${reloadSuffix}`
164
- : `Markii: installed pack "${outcome.packName}".${reloadSuffix}`;
169
+ ? `Markii: reinstalled pack "${outcome.namespace}".${reloadSuffix}`
170
+ : `Markii: installed pack "${outcome.namespace}".${reloadSuffix}`;
165
171
  }
166
172
  /** The full diagnostics-surface detail for one install attempt (AGENTS.md's "clean is not silent": the other of a failure's two homes). */
167
173
  export function installPackDiagnosticLines(outcome, archivePath) {
@@ -172,15 +178,15 @@ export function installPackDiagnosticLines(outcome, archivePath) {
172
178
  }
173
179
  if (outcome.kind === 'reserved') {
174
180
  return [
175
- `Install pack from file refused "${archivePath}": pack "${outcome.packName}" is a reserved namespace and cannot be installed.`,
181
+ `Install pack from file refused "${archivePath}": pack "${outcome.namespace}" is a reserved namespace and cannot be installed.`,
176
182
  ];
177
183
  }
178
184
  if (outcome.kind === 'declined') {
179
185
  return [
180
- `Install pack from file cancelled for "${archivePath}" (pack "${outcome.packName}") at the ${outcome.step} step; nothing was installed.`,
186
+ `Install pack from file cancelled for "${archivePath}" (pack "${outcome.namespace}") at the ${outcome.step} step; nothing was installed.`,
181
187
  ];
182
188
  }
183
189
  return [
184
- `Install pack from file installed pack "${outcome.packName}" from "${archivePath}" to ${outcome.installedDir}${outcome.replaced ? ' (replacing the previous install)' : ''}.`,
190
+ `Install pack from file installed pack "${outcome.namespace}" from "${archivePath}" to ${outcome.installedDir}${outcome.replaced ? ' (replacing the previous install)' : ''}.`,
185
191
  ];
186
192
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The shared pack-trust behavior (docs/packs.md, docs/integration.md's host
2
+ * The shared pack-trust behavior (docs/packs.md, docs/hosts.md's host
3
3
  * checklist). A host that declares `HostPackSource.trust: 'device-list'`
4
4
  * loads an installed pack only when this device's trust list names its
5
5
  * namespace. A folder that sits in the install root without being named
@@ -74,6 +74,10 @@ export interface RunViaAdapterRequest {
74
74
  /** A run's result plus the `RunTrace` it just recorded, so a host can show its in-note marker from the same value the shared path persisted. */
75
75
  export type RunViaAdapterResult = RunOnceResult & {
76
76
  readonly trace: RunTrace;
77
+ /** How many of this run's values came back with the value store's `fresh` status. */
78
+ readonly freshCount: number;
79
+ /** How many of this run's values came back with the value store's `error` status. */
80
+ readonly errorCount: number;
77
81
  };
78
82
  /**
79
83
  * Runs one manual, auto, or scheduled pass of `request.text`'s scripts,
@@ -185,5 +185,13 @@ export async function runViaAdapter(adapter, request, overrideSpawnRun) {
185
185
  // must never turn a successful (or already-reported) run into a thrown
186
186
  // error.
187
187
  await writeLastRunTrace(adapter.memento, request.documentKey, trace).then(undefined, () => { });
188
- return { ...result, trace };
188
+ let freshCount = 0;
189
+ let errorCount = 0;
190
+ for (const value of Object.values(result.values)) {
191
+ if (value.status === 'fresh')
192
+ freshCount += 1;
193
+ else if (value.status === 'error')
194
+ errorCount += 1;
195
+ }
196
+ return { ...result, trace, freshCount, errorCount };
189
197
  }