@markii/host 0.0.0-stage → 0.17.0-next.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +19 -2
- package/dist/browser-worker.js +639 -0
- package/dist/browser.d.ts +57 -0
- package/dist/browser.js +167 -0
- package/dist/conformance/host/01-plain-open/expected.json +5 -0
- package/dist/conformance/host/01-plain-open/note.mk.md +8 -0
- package/dist/conformance/host/01-plain-open/scenario.json +6 -0
- package/dist/conformance/host/02-run-manual-grant-accepted/expected.json +16 -0
- package/dist/conformance/host/02-run-manual-grant-accepted/note.mk.md +5 -0
- package/dist/conformance/host/02-run-manual-grant-accepted/scenario.json +14 -0
- package/dist/conformance/host/03-run-manual-grant-refused/expected.json +18 -0
- package/dist/conformance/host/03-run-manual-grant-refused/note.mk.md +5 -0
- package/dist/conformance/host/03-run-manual-grant-refused/scenario.json +14 -0
- package/dist/conformance/host/04-run-auto-write-denied/expected.json +16 -0
- package/dist/conformance/host/04-run-auto-write-denied/note.mk.md +10 -0
- package/dist/conformance/host/04-run-auto-write-denied/scenario.json +13 -0
- package/dist/conformance/host/05-export-html-pack-directive/expected.json +14 -0
- package/dist/conformance/host/05-export-html-pack-directive/note.mk.md +8 -0
- package/dist/conformance/host/05-export-html-pack-directive/scenario.json +6 -0
- package/dist/conformance/host/06-install-pack-then-render/expected.json +18 -0
- package/dist/conformance/host/06-install-pack-then-render/files/ana-1.0.0/pack.json +13 -0
- package/dist/conformance/host/06-install-pack-then-render/files/ana-1.0.0/webview.js +1 -0
- package/dist/conformance/host/06-install-pack-then-render/note.mk.md +5 -0
- package/dist/conformance/host/06-install-pack-then-render/scenario.json +15 -0
- package/dist/conformance/host/07-complete-in-directive/expected.json +14 -0
- package/dist/conformance/host/07-complete-in-directive/note.mk.md +3 -0
- package/dist/conformance/host/07-complete-in-directive/scenario.json +6 -0
- package/dist/conformance/host/08-hover-standard-component/expected.json +5 -0
- package/dist/conformance/host/08-hover-standard-component/note.mk.md +5 -0
- package/dist/conformance/host/08-hover-standard-component/scenario.json +6 -0
- package/dist/conformance/host/09-insert-skeleton/expected.json +5 -0
- package/dist/conformance/host/09-insert-skeleton/note.mk.md +4 -0
- package/dist/conformance/host/09-insert-skeleton/scenario.json +6 -0
- package/dist/conformance/host/10-scripts-disabled/expected.json +11 -0
- package/dist/conformance/host/10-scripts-disabled/note.mk.md +5 -0
- package/dist/conformance/host/10-scripts-disabled/scenario.json +9 -0
- package/dist/conformance/host/11-complete-closing-fence/expected.json +5 -0
- package/dist/conformance/host/11-complete-closing-fence/note.mk.md +5 -0
- package/dist/conformance/host/11-complete-closing-fence/scenario.json +12 -0
- package/dist/conformance/host/12-complete-nested-container/expected.json +5 -0
- package/dist/conformance/host/12-complete-nested-container/note.mk.md +5 -0
- package/dist/conformance/host/12-complete-nested-container/scenario.json +12 -0
- package/dist/conformance/host/13-install-pack-declined/expected.json +12 -0
- package/dist/conformance/host/13-install-pack-declined/files/ana-1.0.0/pack.json +13 -0
- package/dist/conformance/host/13-install-pack-declined/files/ana-1.0.0/webview.js +1 -0
- package/dist/conformance/host/13-install-pack-declined/note.mk.md +5 -0
- package/dist/conformance/host/13-install-pack-declined/scenario.json +10 -0
- package/dist/conformance/host/14-remove-pack/expected.json +14 -0
- package/dist/conformance/host/14-remove-pack/files/ana-1.0.0/pack.json +13 -0
- package/dist/conformance/host/14-remove-pack/files/ana-1.0.0/webview.js +1 -0
- package/dist/conformance/host/14-remove-pack/note.mk.md +5 -0
- package/dist/conformance/host/14-remove-pack/scenario.json +11 -0
- package/dist/conformance/host/15-load-packs-engine-views/expected.json +19 -0
- package/dist/conformance/host/15-load-packs-engine-views/files/duo-1.0.0/pack.json +9 -0
- package/dist/conformance/host/15-load-packs-engine-views/files/duo-1.0.0/webview.js +1 -0
- package/dist/conformance/host/15-load-packs-engine-views/note.mk.md +9 -0
- package/dist/conformance/host/15-load-packs-engine-views/scenario.json +10 -0
- package/dist/conformance/host/16-pack-trust/expected.json +37 -0
- package/dist/conformance/host/16-pack-trust/files/ana-1.0.0/pack.json +12 -0
- package/dist/conformance/host/16-pack-trust/files/ana-1.0.0/webview.js +1 -0
- package/dist/conformance/host/16-pack-trust/files/sneaky-1.0.0/pack.json +12 -0
- package/dist/conformance/host/16-pack-trust/files/sneaky-1.0.0/webview.js +1 -0
- package/dist/conformance/host/16-pack-trust/note.mk.md +5 -0
- package/dist/conformance/host/16-pack-trust/scenario.json +26 -0
- package/dist/conformance/host/README.md +221 -0
- package/dist/conformance.d.ts +2 -0
- package/dist/conformance.js +5 -0
- package/dist/diagnostics/render-diagnostics.d.ts +41 -0
- package/dist/diagnostics/render-diagnostics.js +46 -0
- package/dist/export/cascade.d.ts +98 -0
- package/dist/export/cascade.js +159 -0
- package/dist/export/export-zip.d.ts +19 -0
- package/dist/export/export-zip.js +38 -0
- package/dist/export/image-embed.d.ts +126 -0
- package/dist/export/image-embed.js +274 -0
- package/dist/export/note-export.d.ts +332 -0
- package/dist/export/note-export.js +366 -0
- package/dist/export/note-links.d.ts +83 -0
- package/dist/export/note-links.js +241 -0
- package/dist/glue.wasm +0 -0
- package/dist/host/adapter.d.ts +223 -0
- package/dist/host/adapter.js +9 -0
- package/dist/host/cascade-export-behavior.d.ts +120 -0
- package/dist/host/cascade-export-behavior.js +151 -0
- package/dist/host/conformance-runner.d.ts +156 -0
- package/dist/host/conformance-runner.js +634 -0
- package/dist/host/create-host.d.ts +253 -0
- package/dist/host/create-host.js +329 -0
- package/dist/host/editor-behavior.d.ts +157 -0
- package/dist/host/editor-behavior.js +227 -0
- package/dist/host/export/md-plain.d.ts +15 -0
- package/dist/host/export/md-plain.js +125 -0
- package/dist/host/export-behavior.d.ts +113 -0
- package/dist/host/export-behavior.js +257 -0
- package/dist/host/labels.d.ts +24 -0
- package/dist/host/labels.js +32 -0
- package/dist/host/pack-archive.d.ts +30 -0
- package/dist/host/pack-archive.js +68 -0
- package/dist/host/pack-install.d.ts +88 -0
- package/dist/host/pack-install.js +189 -0
- package/dist/host/pack-load.d.ts +72 -0
- package/dist/host/pack-load.js +57 -0
- package/dist/host/pack-remove.d.ts +21 -0
- package/dist/host/pack-remove.js +65 -0
- package/dist/host/pack-trust-behavior.d.ts +79 -0
- package/dist/host/pack-trust-behavior.js +262 -0
- package/dist/host/refresh-interval.d.ts +39 -0
- package/dist/host/refresh-interval.js +61 -0
- package/dist/host/run-behavior.d.ts +89 -0
- package/dist/host/run-behavior.js +168 -0
- package/dist/host/run-diagnostics.d.ts +26 -0
- package/dist/host/run-diagnostics.js +43 -0
- package/dist/host/script-execution.d.ts +72 -0
- package/dist/host/script-execution.js +68 -0
- package/dist/host/terminal-packs.d.ts +39 -0
- package/dist/host/terminal-packs.js +51 -0
- package/dist/html-engine.d.ts +2 -0
- package/dist/html-engine.js +29 -0
- package/dist/index.d.ts +95 -0
- package/dist/index.js +70 -0
- package/dist/insert/component-catalog.d.ts +23 -0
- package/dist/insert/component-catalog.js +96 -0
- package/dist/node-fs.d.ts +14 -0
- package/dist/node-fs.js +70 -0
- package/dist/node.d.ts +5 -0
- package/dist/node.js +13 -0
- package/dist/package.json +3 -0
- package/dist/packs/discover.d.ts +119 -0
- package/dist/packs/discover.js +266 -0
- package/dist/packs/pack-diagnostics.d.ts +81 -0
- package/dist/packs/pack-diagnostics.js +105 -0
- package/dist/packs/pack-paths.d.ts +53 -0
- package/dist/packs/pack-paths.js +106 -0
- package/dist/packs/pack-render-registry.d.ts +83 -0
- package/dist/packs/pack-render-registry.js +182 -0
- package/dist/packs/pack-scripts.d.ts +20 -0
- package/dist/packs/pack-scripts.js +96 -0
- package/dist/packs/pack-trust.d.ts +57 -0
- package/dist/packs/pack-trust.js +111 -0
- package/dist/packs/prebuilt.d.ts +49 -0
- package/dist/packs/prebuilt.js +97 -0
- package/dist/path-util.d.ts +24 -0
- package/dist/path-util.js +82 -0
- package/dist/run/browser-isolate.d.ts +45 -0
- package/dist/run/browser-isolate.js +81 -0
- package/dist/run/bundle-document.d.ts +29 -0
- package/dist/run/bundle-document.js +40 -0
- package/dist/run/bundle-run.d.ts +188 -0
- package/dist/run/bundle-run.js +232 -0
- package/dist/run/grant-flow.d.ts +232 -0
- package/dist/run/grant-flow.js +390 -0
- package/dist/run/ip-address.d.ts +41 -0
- package/dist/run/ip-address.js +255 -0
- package/dist/run/isolate.d.ts +57 -0
- package/dist/run/isolate.js +22 -0
- package/dist/run/lua-resolver.d.ts +21 -0
- package/dist/run/lua-resolver.js +63 -0
- package/dist/run/net-bridge-worker.d.ts +26 -0
- package/dist/run/net-bridge-worker.js +66 -0
- package/dist/run/net-bridge.d.ts +56 -0
- package/dist/run/net-bridge.js +59 -0
- package/dist/run/net-pinning.d.ts +82 -0
- package/dist/run/net-pinning.js +137 -0
- package/dist/run/net-provider.d.ts +70 -0
- package/dist/run/net-provider.js +289 -0
- package/dist/run/node-isolate.d.ts +30 -0
- package/dist/run/node-isolate.js +117 -0
- package/dist/run/run-flow.d.ts +187 -0
- package/dist/run/run-flow.js +323 -0
- package/dist/run/run-host.d.ts +107 -0
- package/dist/run/run-host.js +241 -0
- package/dist/run/run-job.d.ts +130 -0
- package/dist/run/run-job.js +279 -0
- package/dist/run/run-progress.d.ts +55 -0
- package/dist/run/run-progress.js +42 -0
- package/dist/run/run-trace.d.ts +44 -0
- package/dist/run/run-trace.js +36 -0
- package/dist/run/script-requirements.d.ts +98 -0
- package/dist/run/script-requirements.js +212 -0
- package/dist/run/snapshot-storage.d.ts +25 -0
- package/dist/run/snapshot-storage.js +31 -0
- package/dist/run/stale-values.d.ts +25 -0
- package/dist/run/stale-values.js +32 -0
- package/dist/run/worker-entry-browser.d.ts +42 -0
- package/dist/run/worker-entry-browser.js +77 -0
- package/dist/run/worker-entry.d.ts +1 -0
- package/dist/run/worker-entry.js +96 -0
- package/dist/values-failure.d.ts +17 -0
- package/dist/values-failure.js +1 -0
- package/dist/values-merge.d.ts +39 -0
- package/dist/values-merge.js +33 -0
- package/package.json +84 -4
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared pack-trust behavior (docs/packs.md, docs/integration.md's host
|
|
3
|
+
* checklist). A host that declares `HostPackSource.trust: 'device-list'`
|
|
4
|
+
* loads an installed pack only when this device's trust list names its
|
|
5
|
+
* namespace. A folder that sits in the install root without being named
|
|
6
|
+
* there is listed as present but not enabled, and nothing in it is read
|
|
7
|
+
* past its folder name: it is never discovered, never evaluated.
|
|
8
|
+
*
|
|
9
|
+
* The list lives in the adapter's device-local `memento` (the same state
|
|
10
|
+
* the grant model uses), never in anything that travels with a vault or a
|
|
11
|
+
* workspace file. Installing a pack trusts it (the install consent prompt
|
|
12
|
+
* already asked), enabling a present folder asks the same prompt, and
|
|
13
|
+
* disabling or removing withdraws the entry.
|
|
14
|
+
*
|
|
15
|
+
* A host that declares `trust: 'none'` (or omits the field) trusts every
|
|
16
|
+
* folder its own policy authorizes, and every function here is inert for
|
|
17
|
+
* it. Neutral on purpose: only the adapter's file and state primitives
|
|
18
|
+
* are used.
|
|
19
|
+
*/
|
|
20
|
+
import { parsePackManifest, validatePackName } from '@markii/pack';
|
|
21
|
+
import { joinPath } from '../path-util.js';
|
|
22
|
+
import { PACK_TRUST_STORAGE_KEY, isPackTrusted, normalizePackTrustList, trustPack, untrustPack, } from '../packs/pack-trust.js';
|
|
23
|
+
import { installConsentMessage } from './pack-install.js';
|
|
24
|
+
/** Whether this adapter's pack source gates installed packs on the device trust list and can say where they live. */
|
|
25
|
+
export function usesPackTrust(adapter) {
|
|
26
|
+
return (adapter.packs?.trust === 'device-list' &&
|
|
27
|
+
adapter.packs.installRoot !== undefined);
|
|
28
|
+
}
|
|
29
|
+
export function readPackTrust(adapter) {
|
|
30
|
+
return normalizePackTrustList(adapter.memento.get(PACK_TRUST_STORAGE_KEY));
|
|
31
|
+
}
|
|
32
|
+
async function writePackTrust(adapter, list) {
|
|
33
|
+
await adapter.memento.update(PACK_TRUST_STORAGE_KEY, list);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Pure: splits the namespaces found in the install root by the trust
|
|
37
|
+
* list. A trust entry naming a namespace no longer on disk contributes to
|
|
38
|
+
* neither side: there is nothing to load and nothing to report.
|
|
39
|
+
*/
|
|
40
|
+
export function selectEnabledPacks(installRoot, onDiskNamespaces, trust) {
|
|
41
|
+
const byNamespace = new Map(trust.entries.map((entry) => [entry.namespace, entry]));
|
|
42
|
+
const enabled = [];
|
|
43
|
+
const notEnabled = [];
|
|
44
|
+
for (const namespace of onDiskNamespaces) {
|
|
45
|
+
const entry = byNamespace.get(namespace);
|
|
46
|
+
if (entry) {
|
|
47
|
+
enabled.push({
|
|
48
|
+
namespace,
|
|
49
|
+
folder: joinPath(installRoot, namespace),
|
|
50
|
+
...(entry.version !== undefined ? { version: entry.version } : {}),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
notEnabled.push(namespace);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { enabled, notEnabled };
|
|
58
|
+
}
|
|
59
|
+
/** Lists the install root through the adapter and splits it by trust. A host without trust returns everything as neither: callers check `usesPackTrust` first. Never throws. */
|
|
60
|
+
export async function listInstalledPacks(adapter) {
|
|
61
|
+
const installRoot = adapter.packs?.installRoot?.();
|
|
62
|
+
if (installRoot === undefined)
|
|
63
|
+
return { enabled: [], notEnabled: [] };
|
|
64
|
+
let names;
|
|
65
|
+
try {
|
|
66
|
+
names = (await adapter.listFolder(installRoot))
|
|
67
|
+
.filter((entry) => entry.isDirectory)
|
|
68
|
+
.map((entry) => entry.name)
|
|
69
|
+
.sort();
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
names = [];
|
|
73
|
+
}
|
|
74
|
+
return selectEnabledPacks(installRoot, names, readPackTrust(adapter));
|
|
75
|
+
}
|
|
76
|
+
/** The folders the trust gate lets load, for `authorizedFolders` to be extended with. Empty for a host that does not use trust. */
|
|
77
|
+
export async function trustedPackFolders(adapter) {
|
|
78
|
+
if (!usesPackTrust(adapter))
|
|
79
|
+
return [];
|
|
80
|
+
return (await listInstalledPacks(adapter)).enabled.map((p) => p.folder);
|
|
81
|
+
}
|
|
82
|
+
/** Reads a pack's declared `version`, or `undefined` for a missing or malformed `pack.json`. Never throws. */
|
|
83
|
+
async function readPackVersion(adapter, folder) {
|
|
84
|
+
try {
|
|
85
|
+
const text = new TextDecoder().decode(await adapter.readFile(joinPath(folder, 'pack.json')));
|
|
86
|
+
const parsed = JSON.parse(text);
|
|
87
|
+
return typeof parsed.version === 'string' ? parsed.version : undefined;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** Why a folder's `pack.json` cannot be a pack, or `undefined` when it parses as a valid manifest. Never throws. */
|
|
94
|
+
async function manifestProblem(adapter, folder) {
|
|
95
|
+
let text;
|
|
96
|
+
try {
|
|
97
|
+
text = new TextDecoder().decode(await adapter.readFile(joinPath(folder, 'pack.json')));
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return 'the folder has no readable pack.json';
|
|
101
|
+
}
|
|
102
|
+
const parsed = parsePackManifest(text);
|
|
103
|
+
return parsed.ok ? undefined : parsed.errors.join('; ');
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Called by the install path after an archive was extracted: records the
|
|
107
|
+
* namespace as trusted on this device. A no-op for a host without trust.
|
|
108
|
+
* Never throws; a refused write is reported on the diagnostics surface.
|
|
109
|
+
*/
|
|
110
|
+
export async function trustInstalledPack(adapter, namespace, installedDir) {
|
|
111
|
+
if (!usesPackTrust(adapter))
|
|
112
|
+
return;
|
|
113
|
+
try {
|
|
114
|
+
const version = await readPackVersion(adapter, installedDir);
|
|
115
|
+
await writePackTrust(adapter, trustPack(readPackTrust(adapter), namespace, version));
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
adapter.diagnostics(`Could not record trust for pack "${namespace}": ${err instanceof Error ? err.message : String(err)}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function invalidReason(namespace) {
|
|
122
|
+
return `"${namespace}" is not a valid pack namespace`;
|
|
123
|
+
}
|
|
124
|
+
/** Never throws. The caller checks `usesPackTrust` first. */
|
|
125
|
+
export async function enablePackViaAdapter(adapter, namespace) {
|
|
126
|
+
if (!validatePackName(namespace).ok) {
|
|
127
|
+
return { kind: 'invalid', reason: invalidReason(namespace) };
|
|
128
|
+
}
|
|
129
|
+
const packs = adapter.packs;
|
|
130
|
+
if (packs === undefined || packs.installRoot === undefined) {
|
|
131
|
+
return { kind: 'failed', reason: 'no pack install folder is available' };
|
|
132
|
+
}
|
|
133
|
+
if (packs.reservedNamespaces().has(namespace)) {
|
|
134
|
+
return { kind: 'reserved', namespace };
|
|
135
|
+
}
|
|
136
|
+
const folder = joinPath(packs.installRoot(), namespace);
|
|
137
|
+
try {
|
|
138
|
+
if (!(await adapter.exists(folder))) {
|
|
139
|
+
return { kind: 'not-installed', namespace };
|
|
140
|
+
}
|
|
141
|
+
// A folder that is not a valid pack is refused before the consent
|
|
142
|
+
// prompt: trusting it would record an entry for a namespace that
|
|
143
|
+
// cannot load, and would trust whatever later appears in that folder.
|
|
144
|
+
const problem = await manifestProblem(adapter, folder);
|
|
145
|
+
if (problem !== undefined) {
|
|
146
|
+
return {
|
|
147
|
+
kind: 'failed',
|
|
148
|
+
reason: `could not enable pack "${namespace}": ${problem}`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const consented = await adapter.prompt({
|
|
152
|
+
kind: 'pack-install-consent',
|
|
153
|
+
message: installConsentMessage(namespace, adapter.labels),
|
|
154
|
+
allowLabel: 'Enable',
|
|
155
|
+
denyLabel: 'Cancel',
|
|
156
|
+
consequential: true,
|
|
157
|
+
});
|
|
158
|
+
if (!consented)
|
|
159
|
+
return { kind: 'declined', namespace };
|
|
160
|
+
const version = await readPackVersion(adapter, folder);
|
|
161
|
+
await writePackTrust(adapter, trustPack(readPackTrust(adapter), namespace, version));
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
return {
|
|
165
|
+
kind: 'failed',
|
|
166
|
+
reason: `could not enable pack "${namespace}": ${err instanceof Error ? err.message : String(err)}`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return { kind: 'enabled', namespace };
|
|
170
|
+
}
|
|
171
|
+
/** Never throws. The caller checks `usesPackTrust` first. The folder stays on disk. */
|
|
172
|
+
export async function disablePackViaAdapter(adapter, namespace) {
|
|
173
|
+
if (!validatePackName(namespace).ok) {
|
|
174
|
+
return { kind: 'invalid', reason: invalidReason(namespace) };
|
|
175
|
+
}
|
|
176
|
+
const list = readPackTrust(adapter);
|
|
177
|
+
if (!isPackTrusted(list, namespace)) {
|
|
178
|
+
return { kind: 'not-enabled', namespace };
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
await writePackTrust(adapter, untrustPack(list, namespace));
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
return {
|
|
185
|
+
kind: 'failed',
|
|
186
|
+
reason: `could not disable pack "${namespace}": ${err instanceof Error ? err.message : String(err)}`,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return { kind: 'disabled', namespace };
|
|
190
|
+
}
|
|
191
|
+
/** Withdraws a namespace's trust entry after a removal. A no-op for a host without trust. Never throws. */
|
|
192
|
+
export async function forgetPackTrust(adapter, namespace) {
|
|
193
|
+
if (!usesPackTrust(adapter))
|
|
194
|
+
return;
|
|
195
|
+
try {
|
|
196
|
+
await writePackTrust(adapter, untrustPack(readPackTrust(adapter), namespace));
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
adapter.diagnostics(`Could not withdraw trust for pack "${namespace}": ${err instanceof Error ? err.message : String(err)}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// --- wording ---------------------------------------------------------------
|
|
203
|
+
export function enablePackMessage(outcome) {
|
|
204
|
+
switch (outcome.kind) {
|
|
205
|
+
case 'enabled':
|
|
206
|
+
return `Markii: enabled the pack ${outcome.namespace}.`;
|
|
207
|
+
case 'declined':
|
|
208
|
+
return `Markii: the pack ${outcome.namespace} was not enabled.`;
|
|
209
|
+
case 'not-installed':
|
|
210
|
+
return `Markii: no pack named "${outcome.namespace}" is installed. Nothing was enabled.`;
|
|
211
|
+
case 'reserved':
|
|
212
|
+
return `Markii: "${outcome.namespace}" is built in and is always enabled.`;
|
|
213
|
+
case 'invalid':
|
|
214
|
+
return `Markii: ${outcome.reason}. Nothing was enabled.`;
|
|
215
|
+
case 'failed':
|
|
216
|
+
return `Markii: ${outcome.reason}.`;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
export function enablePackDiagnosticLine(outcome) {
|
|
220
|
+
switch (outcome.kind) {
|
|
221
|
+
case 'enabled':
|
|
222
|
+
return `Enable pack enabled "${outcome.namespace}".`;
|
|
223
|
+
case 'declined':
|
|
224
|
+
return `Enable pack declined for "${outcome.namespace}": the consent prompt was refused.`;
|
|
225
|
+
case 'not-installed':
|
|
226
|
+
return `Enable pack found no installed pack "${outcome.namespace}"; nothing was enabled.`;
|
|
227
|
+
case 'reserved':
|
|
228
|
+
return `Enable pack refused "${outcome.namespace}": a built-in pack is always enabled.`;
|
|
229
|
+
case 'invalid':
|
|
230
|
+
return `Enable pack refused: ${outcome.reason}.`;
|
|
231
|
+
case 'failed':
|
|
232
|
+
return `Enable pack failed: ${outcome.reason}`;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
export function disablePackMessage(outcome) {
|
|
236
|
+
switch (outcome.kind) {
|
|
237
|
+
case 'disabled':
|
|
238
|
+
return `Markii: disabled the pack ${outcome.namespace}. Its folder was left in place.`;
|
|
239
|
+
case 'not-enabled':
|
|
240
|
+
return `Markii: the pack "${outcome.namespace}" is not enabled. Nothing changed.`;
|
|
241
|
+
case 'invalid':
|
|
242
|
+
return `Markii: ${outcome.reason}. Nothing changed.`;
|
|
243
|
+
case 'failed':
|
|
244
|
+
return `Markii: ${outcome.reason}.`;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
export function disablePackDiagnosticLine(outcome) {
|
|
248
|
+
switch (outcome.kind) {
|
|
249
|
+
case 'disabled':
|
|
250
|
+
return `Disable pack disabled "${outcome.namespace}"; its folder was left in place.`;
|
|
251
|
+
case 'not-enabled':
|
|
252
|
+
return `Disable pack found "${outcome.namespace}" not enabled; nothing changed.`;
|
|
253
|
+
case 'invalid':
|
|
254
|
+
return `Disable pack refused: ${outcome.reason}.`;
|
|
255
|
+
case 'failed':
|
|
256
|
+
return `Disable pack failed: ${outcome.reason}`;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/** One line per folder that is present in the install root but not enabled on this device. Informational, not a failure. */
|
|
260
|
+
export function notEnabledPackDiagnosticLine(namespace, deviceNoun) {
|
|
261
|
+
return `Pack "${namespace}" is present but not enabled on ${deviceNoun}; it was not loaded.`;
|
|
262
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scheduled-refresh interval's parsing, validation, and clamp,
|
|
3
|
+
* merged from `apps/vscode/src/refresh-interval.ts` (the input-box
|
|
4
|
+
* parse/validate half) and `apps/obsidian/src/local-settings.ts`'s
|
|
5
|
+
* `refreshIntervalMsFromSeconds`/`MIN_REFRESH_INTERVAL_SECONDS` (the
|
|
6
|
+
* read-side clamp half) — survey finding A7: the constant was duplicated
|
|
7
|
+
* verbatim, with Obsidian's own comment noting it mirrors VS Code's, and
|
|
8
|
+
* the clamp-not-reject policy was duplicated as both prose and behavior.
|
|
9
|
+
*
|
|
10
|
+
* The policy: a positive value under `MIN_REFRESH_INTERVAL_SECONDS` is
|
|
11
|
+
* still ACCEPTED as typed (a host's own settings UI must not silently
|
|
12
|
+
* rewrite what the user entered), but is clamped UP to the minimum only
|
|
13
|
+
* once a view actually schedules a timer against it.
|
|
14
|
+
*
|
|
15
|
+
* Pure and dependency-free: re-exported from `../browser.ts` as well as
|
|
16
|
+
* `../index.ts`.
|
|
17
|
+
*/
|
|
18
|
+
/** Below this, a positive scheduled-refresh interval is clamped up rather than silently rejected. */
|
|
19
|
+
export declare const MIN_REFRESH_INTERVAL_SECONDS = 5;
|
|
20
|
+
/**
|
|
21
|
+
* Parses a user-typed interval into whole seconds, or `undefined` when the
|
|
22
|
+
* input is not a positive integer (empty, non-numeric, zero, negative, or
|
|
23
|
+
* fractional). Whitespace around the input is ignored.
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseRefreshIntervalSeconds(input: string): number | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* An error message for input `parseRefreshIntervalSeconds` would reject,
|
|
28
|
+
* or `undefined` when the input is valid. What a host's own interval
|
|
29
|
+
* prompt (an input box, a settings field) shows as its validation
|
|
30
|
+
* message.
|
|
31
|
+
*/
|
|
32
|
+
export declare function refreshIntervalValidationMessage(input: string): string | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* The scheduled-refresh interval in milliseconds a view should actually
|
|
35
|
+
* run its timer at, or `undefined` when refresh is off (`seconds` is `0`
|
|
36
|
+
* or any non-positive/invalid value). A positive value below
|
|
37
|
+
* `MIN_REFRESH_INTERVAL_SECONDS` is clamped up to it.
|
|
38
|
+
*/
|
|
39
|
+
export declare function refreshIntervalMsFromSeconds(seconds: number): number | undefined;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scheduled-refresh interval's parsing, validation, and clamp,
|
|
3
|
+
* merged from `apps/vscode/src/refresh-interval.ts` (the input-box
|
|
4
|
+
* parse/validate half) and `apps/obsidian/src/local-settings.ts`'s
|
|
5
|
+
* `refreshIntervalMsFromSeconds`/`MIN_REFRESH_INTERVAL_SECONDS` (the
|
|
6
|
+
* read-side clamp half) — survey finding A7: the constant was duplicated
|
|
7
|
+
* verbatim, with Obsidian's own comment noting it mirrors VS Code's, and
|
|
8
|
+
* the clamp-not-reject policy was duplicated as both prose and behavior.
|
|
9
|
+
*
|
|
10
|
+
* The policy: a positive value under `MIN_REFRESH_INTERVAL_SECONDS` is
|
|
11
|
+
* still ACCEPTED as typed (a host's own settings UI must not silently
|
|
12
|
+
* rewrite what the user entered), but is clamped UP to the minimum only
|
|
13
|
+
* once a view actually schedules a timer against it.
|
|
14
|
+
*
|
|
15
|
+
* Pure and dependency-free: re-exported from `../browser.ts` as well as
|
|
16
|
+
* `../index.ts`.
|
|
17
|
+
*/
|
|
18
|
+
/** Below this, a positive scheduled-refresh interval is clamped up rather than silently rejected. */
|
|
19
|
+
export const MIN_REFRESH_INTERVAL_SECONDS = 5;
|
|
20
|
+
/**
|
|
21
|
+
* Parses a user-typed interval into whole seconds, or `undefined` when the
|
|
22
|
+
* input is not a positive integer (empty, non-numeric, zero, negative, or
|
|
23
|
+
* fractional). Whitespace around the input is ignored.
|
|
24
|
+
*/
|
|
25
|
+
export function parseRefreshIntervalSeconds(input) {
|
|
26
|
+
const trimmed = input.trim();
|
|
27
|
+
if (trimmed === '')
|
|
28
|
+
return undefined;
|
|
29
|
+
if (!/^\d+$/.test(trimmed))
|
|
30
|
+
return undefined;
|
|
31
|
+
const seconds = Number(trimmed);
|
|
32
|
+
if (!Number.isFinite(seconds) || seconds <= 0)
|
|
33
|
+
return undefined;
|
|
34
|
+
return seconds;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* An error message for input `parseRefreshIntervalSeconds` would reject,
|
|
38
|
+
* or `undefined` when the input is valid. What a host's own interval
|
|
39
|
+
* prompt (an input box, a settings field) shows as its validation
|
|
40
|
+
* message.
|
|
41
|
+
*/
|
|
42
|
+
export function refreshIntervalValidationMessage(input) {
|
|
43
|
+
if (parseRefreshIntervalSeconds(input) === undefined) {
|
|
44
|
+
return 'Enter a whole number of seconds greater than 0.';
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The scheduled-refresh interval in milliseconds a view should actually
|
|
50
|
+
* run its timer at, or `undefined` when refresh is off (`seconds` is `0`
|
|
51
|
+
* or any non-positive/invalid value). A positive value below
|
|
52
|
+
* `MIN_REFRESH_INTERVAL_SECONDS` is clamped up to it.
|
|
53
|
+
*/
|
|
54
|
+
export function refreshIntervalMsFromSeconds(seconds) {
|
|
55
|
+
if (typeof seconds !== 'number' ||
|
|
56
|
+
!Number.isFinite(seconds) ||
|
|
57
|
+
seconds <= 0) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
return Math.max(seconds, MIN_REFRESH_INTERVAL_SECONDS) * 1000;
|
|
61
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `run` behavior's host-shaped seams, built from a `HostAdapter`:
|
|
3
|
+
* this is where survey findings A1 and A6 stop being duplicated three
|
|
4
|
+
* times over.
|
|
5
|
+
*
|
|
6
|
+
* A1: `apps/vscode/src/preview-panel.ts`'s `promptHostAdapter` /
|
|
7
|
+
* `promptUnknownHostsAdapter` / `promptManyHostsAdapter`,
|
|
8
|
+
* `apps/obsidian/src/run-modals.ts`'s `promptHostModal` /
|
|
9
|
+
* `promptUnknownHostsModal` / `promptManyHostsModal`, and
|
|
10
|
+
* `apps/cli/src/prompts.ts`'s three closures all did the same thing: call
|
|
11
|
+
* one of `@markii/host`'s exported message builders and turn the answer
|
|
12
|
+
* into a `boolean` through a host-specific UI primitive. `promptsFromAdapter`
|
|
13
|
+
* below collapses all nine into three calls to the ONE `adapter.prompt`
|
|
14
|
+
* method every `HostAdapter` already has.
|
|
15
|
+
*
|
|
16
|
+
* A6: `apps/vscode/src/preview-panel.ts:spawnRun` and
|
|
17
|
+
* `apps/cli/src/run-note.ts:spawnRun` were the same five-line function —
|
|
18
|
+
* the CLI's own doc comment said outright it was "mirroring
|
|
19
|
+
* apps/vscode/src/preview-panel.ts's identical function". `spawnRunViaAdapter`
|
|
20
|
+
* below is that function, written once, dispatching on `HostAdapter.isolate`'s
|
|
21
|
+
* discriminated union instead of assuming a Node worker path.
|
|
22
|
+
*
|
|
23
|
+
* `runViaAdapter` composes both of these around `../run/run-flow.ts`'s
|
|
24
|
+
* `runOnce` (the existing, already-shared grant-flow-plus-spawn
|
|
25
|
+
* orchestration) and writes the `RunTrace` through `adapter.now()` rather
|
|
26
|
+
* than `Date.now()`, so a fixed clock in a test fake makes the trace
|
|
27
|
+
* byte-stable (batch 11 Phase 3's requirement).
|
|
28
|
+
*/
|
|
29
|
+
import type { RunTrigger } from '@markii/runtime';
|
|
30
|
+
import { type PromptHost, type PromptManyHosts, type PromptUnknownHosts } from '../run/grant-flow.js';
|
|
31
|
+
import { type RunResult, type SpawnRunRequest } from '../run/run-host.js';
|
|
32
|
+
import { type RunOnceOptions, type RunOnceResult } from '../run/run-flow.js';
|
|
33
|
+
import { type RunTrace } from '../run/run-trace.js';
|
|
34
|
+
import type { HostAdapter, HostIsolate } from './adapter.js';
|
|
35
|
+
/**
|
|
36
|
+
* External wall-clock budget for one run, matching the figure both
|
|
37
|
+
* `apps/vscode` and `apps/cli` already used, so a note behaves the same
|
|
38
|
+
* way under any host's watchdog.
|
|
39
|
+
*/
|
|
40
|
+
export declare const RUN_TIMEOUT_MS = 15000;
|
|
41
|
+
/**
|
|
42
|
+
* Builds the three grant prompts (`PromptHost`, `PromptUnknownHosts`,
|
|
43
|
+
* `PromptManyHosts`) `../run/run-flow.ts`'s `runOnce` needs, each routed
|
|
44
|
+
* through `adapter.prompt` with the exact wording `@markii/host`'s
|
|
45
|
+
* message builders already produce. `consequential: true` on all three:
|
|
46
|
+
* every one of them authorizes network access, so a host that can only
|
|
47
|
+
* show a non-blocking surface must deny rather than assume yes — that
|
|
48
|
+
* policy is the adapter's own (see `HostPromptRequest`'s doc comment).
|
|
49
|
+
*/
|
|
50
|
+
export declare function promptsFromAdapter(adapter: HostAdapter): {
|
|
51
|
+
promptHost: PromptHost;
|
|
52
|
+
promptUnknownHosts: PromptUnknownHosts;
|
|
53
|
+
promptManyHosts: PromptManyHosts;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* The one `spawnRun` adapter every Node host (`apps/vscode`, `apps/cli`)
|
|
57
|
+
* used to duplicate (survey A6), plus the browser-isolate path Obsidian's
|
|
58
|
+
* own `spawnRun` wrapper needed. Dispatches on `HostIsolate`'s
|
|
59
|
+
* discriminated union: a `'node'` host supplies only a worker path; a
|
|
60
|
+
* `'browser'` host supplies an async, disposable `IsolateSpawner` factory,
|
|
61
|
+
* disposed once this run's spawner has been used.
|
|
62
|
+
*/
|
|
63
|
+
export declare function spawnRunViaAdapter(isolate: HostIsolate | undefined, options: SpawnRunRequest): Promise<RunResult>;
|
|
64
|
+
export interface RunViaAdapterRequest {
|
|
65
|
+
/** Stable identity for the note this run belongs to. */
|
|
66
|
+
readonly documentKey: string;
|
|
67
|
+
readonly text: string;
|
|
68
|
+
readonly trigger: RunTrigger;
|
|
69
|
+
readonly netPolicy?: RunOnceOptions['netPolicy'];
|
|
70
|
+
readonly bundle?: RunOnceOptions['bundle'];
|
|
71
|
+
readonly packModules?: RunOnceOptions['packModules'];
|
|
72
|
+
readonly onValue?: RunOnceOptions['onValue'];
|
|
73
|
+
readonly timeoutMs?: number;
|
|
74
|
+
}
|
|
75
|
+
/** 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. */
|
|
76
|
+
export type RunViaAdapterResult = RunOnceResult & {
|
|
77
|
+
readonly trace: RunTrace;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Runs one manual, auto, or scheduled pass of `request.text`'s scripts,
|
|
81
|
+
* exactly as `runOnce` already does (the grant flow prompts only on a
|
|
82
|
+
* miss, and only for `'manual'`), but with every host-shaped input —
|
|
83
|
+
* `spawnRun`, the three grant prompts, the memento, the last-run clock —
|
|
84
|
+
* supplied from `adapter` instead of assembled ad hoc per app. Writes the
|
|
85
|
+
* `RunTrace` through `writeLastRunTrace` using `adapter.now()`, never
|
|
86
|
+
* `Date.now()` directly, so a fixed clock in a test fake makes the trace
|
|
87
|
+
* byte-stable.
|
|
88
|
+
*/
|
|
89
|
+
export declare function runViaAdapter(adapter: HostAdapter, request: RunViaAdapterRequest, overrideSpawnRun?: (options: SpawnRunRequest) => Promise<RunResult>): Promise<RunViaAdapterResult>;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { ALLOW_LABEL, DONT_ALLOW_LABEL, UNKNOWN_HOSTS_PROMPT_MESSAGE, hostPromptMessage, manyHostsPromptMessage, } from '../run/grant-flow.js';
|
|
2
|
+
import { spawnRun as spawnRunHost, } from '../run/run-host.js';
|
|
3
|
+
import { runOnce, } from '../run/run-flow.js';
|
|
4
|
+
import { writeLastRunTrace } from '../run/run-trace.js';
|
|
5
|
+
import { runCrashDiagnosticLine, runDiagnosticLines, } from './run-diagnostics.js';
|
|
6
|
+
import { BROWSER_ISOLATE_ENTRY } from './adapter.js';
|
|
7
|
+
/**
|
|
8
|
+
* External wall-clock budget for one run, matching the figure both
|
|
9
|
+
* `apps/vscode` and `apps/cli` already used, so a note behaves the same
|
|
10
|
+
* way under any host's watchdog.
|
|
11
|
+
*/
|
|
12
|
+
export const RUN_TIMEOUT_MS = 15_000;
|
|
13
|
+
/**
|
|
14
|
+
* Builds the three grant prompts (`PromptHost`, `PromptUnknownHosts`,
|
|
15
|
+
* `PromptManyHosts`) `../run/run-flow.ts`'s `runOnce` needs, each routed
|
|
16
|
+
* through `adapter.prompt` with the exact wording `@markii/host`'s
|
|
17
|
+
* message builders already produce. `consequential: true` on all three:
|
|
18
|
+
* every one of them authorizes network access, so a host that can only
|
|
19
|
+
* show a non-blocking surface must deny rather than assume yes — that
|
|
20
|
+
* policy is the adapter's own (see `HostPromptRequest`'s doc comment).
|
|
21
|
+
*/
|
|
22
|
+
export function promptsFromAdapter(adapter) {
|
|
23
|
+
return {
|
|
24
|
+
promptHost: (host, declaredHosts) => adapter.prompt({
|
|
25
|
+
kind: 'grant-host',
|
|
26
|
+
message: hostPromptMessage(host, declaredHosts),
|
|
27
|
+
allowLabel: ALLOW_LABEL,
|
|
28
|
+
denyLabel: DONT_ALLOW_LABEL,
|
|
29
|
+
consequential: true,
|
|
30
|
+
}),
|
|
31
|
+
promptUnknownHosts: () => adapter.prompt({
|
|
32
|
+
kind: 'grant-unknown-hosts',
|
|
33
|
+
message: UNKNOWN_HOSTS_PROMPT_MESSAGE,
|
|
34
|
+
allowLabel: ALLOW_LABEL,
|
|
35
|
+
denyLabel: DONT_ALLOW_LABEL,
|
|
36
|
+
consequential: true,
|
|
37
|
+
}),
|
|
38
|
+
promptManyHosts: (hostCount) => adapter.prompt({
|
|
39
|
+
kind: 'grant-many-hosts',
|
|
40
|
+
message: manyHostsPromptMessage(hostCount),
|
|
41
|
+
allowLabel: ALLOW_LABEL,
|
|
42
|
+
denyLabel: DONT_ALLOW_LABEL,
|
|
43
|
+
consequential: true,
|
|
44
|
+
}),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A synthetic `RunResult` for a host with no `isolate` capability at all —
|
|
49
|
+
* `spawnRunViaAdapter`'s never-throws contract extends to this case too:
|
|
50
|
+
* a host that cannot run scripts reports it as an ordinary
|
|
51
|
+
* `'capability-denied'` failure, not a thrown error.
|
|
52
|
+
*/
|
|
53
|
+
function noIsolateResult(cacheSnapshot) {
|
|
54
|
+
return {
|
|
55
|
+
values: {},
|
|
56
|
+
failures: [
|
|
57
|
+
{
|
|
58
|
+
name: '<document>',
|
|
59
|
+
message: 'this host has no isolate configured, so scripts cannot run.',
|
|
60
|
+
kind: 'capability-denied',
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
cacheSnapshot,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The one `spawnRun` adapter every Node host (`apps/vscode`, `apps/cli`)
|
|
68
|
+
* used to duplicate (survey A6), plus the browser-isolate path Obsidian's
|
|
69
|
+
* own `spawnRun` wrapper needed. Dispatches on `HostIsolate`'s
|
|
70
|
+
* discriminated union: a `'node'` host supplies only a worker path; a
|
|
71
|
+
* `'browser'` host supplies an async, disposable `IsolateSpawner` factory,
|
|
72
|
+
* disposed once this run's spawner has been used.
|
|
73
|
+
*/
|
|
74
|
+
export async function spawnRunViaAdapter(isolate, options) {
|
|
75
|
+
if (!isolate)
|
|
76
|
+
return noIsolateResult(options.cacheSnapshot);
|
|
77
|
+
if (isolate.kind === 'node') {
|
|
78
|
+
return spawnRunHost({
|
|
79
|
+
...options,
|
|
80
|
+
workerPath: isolate.workerPath(),
|
|
81
|
+
spawnIsolate: isolate.spawnIsolate,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const spawner = await isolate.spawner();
|
|
85
|
+
try {
|
|
86
|
+
return await spawnRunHost({
|
|
87
|
+
...options,
|
|
88
|
+
spawnIsolate: spawner,
|
|
89
|
+
// A blob-URL Web Worker has no entry path, but `spawnRun` still
|
|
90
|
+
// labels the entry it handed out. Naming the label here keeps a
|
|
91
|
+
// meaningless path out of the browser branch.
|
|
92
|
+
workerPath: isolate.entryLabel ?? BROWSER_ISOLATE_ENTRY,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
isolate.dispose();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Runs one manual, auto, or scheduled pass of `request.text`'s scripts,
|
|
101
|
+
* exactly as `runOnce` already does (the grant flow prompts only on a
|
|
102
|
+
* miss, and only for `'manual'`), but with every host-shaped input —
|
|
103
|
+
* `spawnRun`, the three grant prompts, the memento, the last-run clock —
|
|
104
|
+
* supplied from `adapter` instead of assembled ad hoc per app. Writes the
|
|
105
|
+
* `RunTrace` through `writeLastRunTrace` using `adapter.now()`, never
|
|
106
|
+
* `Date.now()` directly, so a fixed clock in a test fake makes the trace
|
|
107
|
+
* byte-stable.
|
|
108
|
+
*/
|
|
109
|
+
export async function runViaAdapter(adapter, request, overrideSpawnRun) {
|
|
110
|
+
const prompts = promptsFromAdapter(adapter);
|
|
111
|
+
const spawn = overrideSpawnRun ??
|
|
112
|
+
((options) => spawnRunViaAdapter(adapter.isolate, options));
|
|
113
|
+
let result;
|
|
114
|
+
try {
|
|
115
|
+
result = await runOnce({
|
|
116
|
+
documentKey: request.documentKey,
|
|
117
|
+
text: request.text,
|
|
118
|
+
trigger: request.trigger,
|
|
119
|
+
memento: adapter.memento,
|
|
120
|
+
...prompts,
|
|
121
|
+
spawnRun: spawn,
|
|
122
|
+
timeoutMs: request.timeoutMs ?? RUN_TIMEOUT_MS,
|
|
123
|
+
...(request.netPolicy !== undefined
|
|
124
|
+
? { netPolicy: request.netPolicy }
|
|
125
|
+
: {}),
|
|
126
|
+
...(request.bundle !== undefined ? { bundle: request.bundle } : {}),
|
|
127
|
+
...(request.packModules !== undefined
|
|
128
|
+
? { packModules: request.packModules }
|
|
129
|
+
: {}),
|
|
130
|
+
...(request.onValue !== undefined ? { onValue: request.onValue } : {}),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
// A run that threw is still a failure with two homes: the diagnostics
|
|
135
|
+
// line and a failed trace for the in-note marker. The caller keeps its
|
|
136
|
+
// own visible surface (a notice), so the error is rethrown.
|
|
137
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
138
|
+
adapter.diagnostics(runCrashDiagnosticLine(request.trigger, detail));
|
|
139
|
+
await writeLastRunTrace(adapter.memento, request.documentKey, {
|
|
140
|
+
trigger: request.trigger,
|
|
141
|
+
ranAt: adapter.now(),
|
|
142
|
+
ok: false,
|
|
143
|
+
reason: detail,
|
|
144
|
+
}).then(undefined, () => { });
|
|
145
|
+
throw err;
|
|
146
|
+
}
|
|
147
|
+
// The failure's second home (AGENTS.md, "clean is not silent"): one line
|
|
148
|
+
// per failed script and per declaration mismatch, written here once so no
|
|
149
|
+
// host can forget it.
|
|
150
|
+
for (const line of runDiagnosticLines(request.trigger, result)) {
|
|
151
|
+
adapter.diagnostics(line);
|
|
152
|
+
}
|
|
153
|
+
const trace = {
|
|
154
|
+
trigger: request.trigger,
|
|
155
|
+
ranAt: adapter.now(),
|
|
156
|
+
ok: result.failures.length === 0,
|
|
157
|
+
...(result.failures.length > 0
|
|
158
|
+
? {
|
|
159
|
+
reason: `${result.failures.length} script${result.failures.length === 1 ? '' : 's'} failed`,
|
|
160
|
+
}
|
|
161
|
+
: {}),
|
|
162
|
+
};
|
|
163
|
+
// Best-effort, matching both apps' own posture: a trace-write failure
|
|
164
|
+
// must never turn a successful (or already-reported) run into a thrown
|
|
165
|
+
// error.
|
|
166
|
+
await writeLastRunTrace(adapter.memento, request.documentKey, trace).then(undefined, () => { });
|
|
167
|
+
return { ...result, trace };
|
|
168
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { RunTrigger } from '@markii/runtime';
|
|
2
|
+
import type { RunOnceResult } from '../run/run-flow.js';
|
|
3
|
+
/**
|
|
4
|
+
* The lines a host's diagnostics surface gets for one run's script
|
|
5
|
+
* failures (GitHub issue #37), moved here from
|
|
6
|
+
* `apps/vscode/src/run-diagnostics.ts` so Obsidian gains the same
|
|
7
|
+
* behavior instead of writing its own copy (it previously wrote each
|
|
8
|
+
* failure to its console ad hoc; VS Code showed them only as value-marker
|
|
9
|
+
* tooltips and the run marker, which a hidden or collapsed marker can
|
|
10
|
+
* bury). This is the "second home" AGENTS.md's "clean is not silent" rule
|
|
11
|
+
* requires: one line per failed script, with its name, its failure kind,
|
|
12
|
+
* and the same short reason the tooltip carries. No failures, no lines:
|
|
13
|
+
* the surface stays quiet on a clean run.
|
|
14
|
+
*/
|
|
15
|
+
export declare function formatRunFailureLines(failures: RunOnceResult['failureDetails']): string[];
|
|
16
|
+
/**
|
|
17
|
+
* Every diagnostics line one completed run owes the host's surface, in
|
|
18
|
+
* the order they are written: a header and one indented line per failed
|
|
19
|
+
* script (the "second home" of each failure, `formatRunFailureLines`),
|
|
20
|
+
* then a header and the lines of any net-declaration mismatch. A clean
|
|
21
|
+
* run with agreeing declarations returns no lines, so an empty surface
|
|
22
|
+
* still means nothing went wrong.
|
|
23
|
+
*/
|
|
24
|
+
export declare function runDiagnosticLines(trigger: RunTrigger, result: Pick<RunOnceResult, 'failureDetails' | 'netDeclarationDiagnostics'>): string[];
|
|
25
|
+
/** The one line a run that threw (rather than returning failures) owes the diagnostics surface. */
|
|
26
|
+
export declare function runCrashDiagnosticLine(trigger: RunTrigger, detail: string): string;
|