@timurproko/a1 0.2.7-dev.1255 → 0.2.7-dev.1264

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 (78) hide show
  1. package/dist/app/session-shell/prompt-submission.d.ts +1 -1
  2. package/dist/app/session-shell/prompt-submission.js +9 -9
  3. package/dist/app/session-shell/resource-labels.d.ts +2 -0
  4. package/dist/app/session-shell/resource-labels.js +158 -0
  5. package/dist/app/session-shell/session-prompt-history.d.ts +29 -0
  6. package/dist/app/session-shell/session-prompt-history.js +80 -0
  7. package/dist/app/session-shell/session-selectors.d.ts +46 -0
  8. package/dist/app/session-shell/session-selectors.js +269 -0
  9. package/dist/app/session-shell/session-shell-root.d.ts +6 -28
  10. package/dist/app/session-shell/session-shell-root.js +26 -220
  11. package/dist/app/session-shell/session-shell.d.ts +3 -23
  12. package/dist/app/session-shell/session-shell.js +54 -365
  13. package/dist/app/session-shell/session-viewport-controller.d.ts +2 -10
  14. package/dist/app/session-shell/session-viewport-controller.js +40 -318
  15. package/dist/app/session-shell/settings-bindings.d.ts +1 -1
  16. package/dist/app/session-shell/settings-bindings.js +2 -2
  17. package/dist/app/session-shell/viewport-pointer.d.ts +83 -0
  18. package/dist/app/session-shell/viewport-pointer.js +361 -0
  19. package/dist/composition/owned-ui.js +63 -59
  20. package/dist/contracts/owned-ui/index.d.ts +1 -1
  21. package/dist/contracts/owned-ui/index.js +1 -1
  22. package/dist/contracts/owned-ui/validation.d.ts +1 -11
  23. package/dist/contracts/owned-ui/validation.js +8 -104
  24. package/dist/contracts/presentation/index.d.ts +1 -1
  25. package/dist/contracts/presentation/terminal.d.ts +0 -6
  26. package/dist/features/owned-ui/settings-app.js +13 -208
  27. package/dist/features/owned-ui/settings-model-dialog.d.ts +41 -0
  28. package/dist/features/owned-ui/settings-model-dialog.js +194 -0
  29. package/dist/foundation/release/automatic-update.js +1 -8
  30. package/dist/foundation/release/dependency-layer.js +3 -3
  31. package/dist/foundation/release/latest-release.js +1 -1
  32. package/dist/foundation/release/managed-path.d.ts +10 -0
  33. package/dist/foundation/release/managed-path.js +26 -0
  34. package/dist/foundation/release/release-gc.js +6 -8
  35. package/dist/foundation/release/restart-certification.js +3 -4
  36. package/dist/foundation/release/update-activation.js +1 -1
  37. package/dist/foundation/release/update-execution-lease.js +1 -7
  38. package/dist/foundation/release/update-installer.d.ts +45 -0
  39. package/dist/foundation/release/update-installer.js +57 -0
  40. package/dist/foundation/release/update-recovery.js +2 -9
  41. package/dist/foundation/release/update.d.ts +4 -23
  42. package/dist/foundation/release/update.js +160 -179
  43. package/dist/integrations/pi/components/bracketed-paste.d.ts +33 -0
  44. package/dist/integrations/pi/components/bracketed-paste.js +123 -0
  45. package/dist/integrations/pi/components/owned-editor-ux.js +46 -140
  46. package/dist/integrations/pi/engine/adapter.d.ts +1 -8
  47. package/dist/integrations/pi/engine/adapter.js +6 -49
  48. package/dist/integrations/pi/engine/host.js +1 -1
  49. package/dist/integrations/pi/engine/repository-metadata.d.ts +49 -0
  50. package/dist/integrations/pi/engine/repository-metadata.js +189 -0
  51. package/dist/integrations/pi/engine/session-runtime.d.ts +6 -12
  52. package/dist/integrations/pi/engine/session-runtime.js +21 -178
  53. package/dist/integrations/pi/engine/view-state.d.ts +21 -2
  54. package/dist/integrations/pi/engine/view-state.js +129 -1
  55. package/dist/integrations/pi/engine/workflow-runner.d.ts +0 -1
  56. package/dist/integrations/pi/engine/workflow-runner.js +254 -365
  57. package/dist/integrations/pi/engine/workflow-share.d.ts +9 -0
  58. package/dist/integrations/pi/engine/workflow-share.js +168 -0
  59. package/dist/integrations/pi/engine/workflow-support.d.ts +0 -14
  60. package/dist/integrations/pi/engine/workflow-support.js +0 -47
  61. package/dist/integrations/pi/tui-runtime/adapter.d.ts +9 -19
  62. package/dist/integrations/pi/tui-runtime/adapter.js +35 -144
  63. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +4 -4
  64. package/dist/integrations/pi/tui-runtime/index.d.ts +0 -1
  65. package/dist/integrations/pi/tui-runtime/overlay-geometry.d.ts +2 -2
  66. package/dist/integrations/pi/tui-runtime/presentation-adapter.d.ts +2 -2
  67. package/dist/ui/components/transcript-viewport-frame.d.ts +120 -0
  68. package/dist/ui/components/transcript-viewport-frame.js +314 -0
  69. package/dist/ui/components/transcript-viewport-types.d.ts +147 -0
  70. package/dist/ui/components/transcript-viewport.d.ts +2 -147
  71. package/dist/ui/components/transcript-viewport.js +87 -315
  72. package/package.json +3 -3
  73. package/dist/integrations/pi/engine/diagnostics-state.d.ts +0 -9
  74. package/dist/integrations/pi/engine/diagnostics-state.js +0 -34
  75. package/dist/integrations/pi/engine/usage-view.d.ts +0 -4
  76. package/dist/integrations/pi/engine/usage-view.js +0 -56
  77. package/dist/integrations/pi/tui-runtime/contracts.d.ts +0 -22
  78. /package/dist/{integrations/pi/tui-runtime/contracts.js → ui/components/transcript-viewport-types.js} +0 -0
@@ -0,0 +1,194 @@
1
+ import { DIALOG_BACK_SHORTCUT_HINT, DIALOG_CLOSE_SHORTCUT_HINT } from "../../contracts/presentation/index.js";
2
+ import { LineInput, ShortcutRegistry, assertNoShortcutConflicts, configureDialogChoiceCapacity, configureDialogTextRows, handleLineInputKey, humanizeLabel, renderSteppedDialogPanel, } from "../../ui/components/index.js";
3
+ import { changeFailure, structuredValue } from "./settings-edits.js";
4
+ import { SETTINGS_APP_ID } from "./settings-route.js";
5
+ const MODEL_SCOPE = `${SETTINGS_APP_ID}-models`;
6
+ const MODEL_LEVEL_SCOPE = `${SETTINGS_APP_ID}-model-levels`;
7
+ const MODEL_SHORTCUTS = new ShortcutRegistry();
8
+ MODEL_SHORTCUTS.declare({ key: "edit", scope: MODEL_SCOPE, description: "Search models", section: "Choose", hint: { keys: "Type", does: "search" } }, "edit-filter");
9
+ MODEL_SHORTCUTS.declare({ key: "up", scope: MODEL_SCOPE, description: "Previous model", section: "Choose" }, "previous");
10
+ MODEL_SHORTCUTS.declare({ key: "down", scope: MODEL_SCOPE, description: "Next model", section: "Choose" }, "next");
11
+ MODEL_SHORTCUTS.declare({ key: "enter", scope: MODEL_SCOPE, description: "Select model", section: "Choose", hint: { keys: "Enter", does: "select" } }, "select");
12
+ MODEL_SHORTCUTS.declare({ key: "escape", scope: MODEL_SCOPE, description: "Close selector", section: "Choose", hint: { keys: DIALOG_CLOSE_SHORTCUT_HINT.key, does: DIALOG_CLOSE_SHORTCUT_HINT.action } }, "close");
13
+ MODEL_SHORTCUTS.declare({ key: "up", scope: MODEL_LEVEL_SCOPE, description: "Previous level", section: "Choose", hint: { keys: "↑↓", does: "navigate" } }, "previous");
14
+ MODEL_SHORTCUTS.declare({ key: "down", scope: MODEL_LEVEL_SCOPE, description: "Next level", section: "Choose", hint: { keys: "↑↓", does: "navigate" } }, "next");
15
+ MODEL_SHORTCUTS.declare({ key: "enter", scope: MODEL_LEVEL_SCOPE, description: "Select level", section: "Choose", hint: { keys: "Enter", does: "select" } }, "select");
16
+ MODEL_SHORTCUTS.declare({ key: "escape", scope: MODEL_LEVEL_SCOPE, description: "Return to models", section: "Choose", hint: { keys: DIALOG_BACK_SHORTCUT_HINT.key, does: DIALOG_BACK_SHORTCUT_HINT.action } }, "back");
17
+ assertNoShortcutConflicts(MODEL_SHORTCUTS.assemble());
18
+ /**
19
+ * One key in the default-model selector or the two-step thinking-level dialog. `key` is the
20
+ * named key for `data`; typed text that names no shortcut edits the search.
21
+ */
22
+ export function modelDialogKey(open, data, key, actions) {
23
+ if (open.modelDialog === "default" || open.step === "model") {
24
+ const flags = filteredModelFlags(open);
25
+ const action = MODEL_SHORTCUTS.resolve(key, MODEL_SCOPE) ?? "edit-filter";
26
+ if (action === "close") {
27
+ actions.close();
28
+ return { consumed: true };
29
+ }
30
+ if (action === "previous" || action === "next") {
31
+ open.index = Math.min(Math.max(0, flags.length - 1), Math.max(0, open.index + (action === "next" ? 1 : -1)));
32
+ return { consumed: true };
33
+ }
34
+ if (action === "select") {
35
+ const selected = flags[open.index];
36
+ if (selected === undefined)
37
+ return { consumed: true, render: false };
38
+ if (open.modelDialog === "default")
39
+ selectDefaultModel(open, selected.key, actions);
40
+ else {
41
+ open.modelIndex = open.index;
42
+ open.modelKey = selected.key;
43
+ open.step = "level";
44
+ const options = modelLevelOptions(open, selected);
45
+ const current = structuredValue(open.entry, open.record)[selected.key];
46
+ open.index = Math.max(0, options.findIndex(option => option.value === current));
47
+ }
48
+ return { consumed: true };
49
+ }
50
+ if (open.filter === null)
51
+ return { consumed: true, render: false };
52
+ handleLineInputKey(open.filter, data);
53
+ open.index = 0;
54
+ return { consumed: true };
55
+ }
56
+ const selected = open.entry.flags.find(flag => flag.key === open.modelKey);
57
+ const choices = selected === undefined ? [] : modelLevelOptions(open, selected);
58
+ const action = MODEL_SHORTCUTS.resolve(key, MODEL_LEVEL_SCOPE);
59
+ if (action === "back") {
60
+ returnToModels(open);
61
+ return { consumed: true };
62
+ }
63
+ if (action === "previous" || action === "next") {
64
+ open.index = Math.min(Math.max(0, choices.length - 1), Math.max(0, open.index + (action === "next" ? 1 : -1)));
65
+ return { consumed: true };
66
+ }
67
+ if (action === "select") {
68
+ const choice = choices[open.index];
69
+ if (selected === undefined || choice === undefined)
70
+ return { consumed: true, render: false };
71
+ const previous = { ...open.record };
72
+ open.record[selected.key] = choice.value;
73
+ const forward = { ...open.record };
74
+ actions.save(open, previous, forward);
75
+ returnToModels(open);
76
+ return { consumed: true };
77
+ }
78
+ return { consumed: true, render: false };
79
+ }
80
+ export function modelDialogLines(open, width, height, theme) {
81
+ if (open.modelDialog === "default") {
82
+ const flags = filteredModelFlags(open);
83
+ const current = typeof open.entry.rawValue === "string" ? open.entry.rawValue : null;
84
+ const rows = flags.length === 0
85
+ ? [{ label: "No models found", value: "" }]
86
+ : flags.map(flag => ({
87
+ ...modelLabelParts(flag.label ?? humanizeLabel(flag.key)),
88
+ ...(flag.key === current ? { successSuffix: "✓" } : {}),
89
+ value: "",
90
+ }));
91
+ const description = open.error ?? "Select the model used for new sessions";
92
+ const visible = dialogWindow(rows, open.index, configureDialogChoiceCapacity(height, true, configureDialogTextRows(description, width)));
93
+ return renderSteppedDialogPanel({
94
+ title: "Default Model", description, input: open.filter ?? new LineInput(""),
95
+ rows: visible.rows, index: visible.index, hint: MODEL_SHORTCUTS.hintEntries(MODEL_SCOPE),
96
+ }, width, theme);
97
+ }
98
+ if (open.step === "model") {
99
+ const flags = filteredModelFlags(open);
100
+ const stored = structuredValue(open.entry, open.record);
101
+ const allRows = flags.length === 0
102
+ ? [{ label: "No models found", value: "" }]
103
+ : flags.map(flag => ({
104
+ ...modelLabelParts(flag.label ?? humanizeLabel(flag.key)),
105
+ value: stored[flag.key] === undefined ? "" : String(stored[flag.key]),
106
+ }));
107
+ const description = "Select a model to configure";
108
+ const visible = dialogWindow(allRows, open.index, configureDialogChoiceCapacity(height, true, configureDialogTextRows(description, width)));
109
+ return renderSteppedDialogPanel({
110
+ title: "Thinking Level",
111
+ step: 1,
112
+ steps: 2,
113
+ description,
114
+ ...(open.filter === null ? {} : { input: open.filter }),
115
+ rows: visible.rows,
116
+ index: visible.index,
117
+ hint: MODEL_SHORTCUTS.hintEntries(MODEL_SCOPE),
118
+ }, width, theme);
119
+ }
120
+ const selected = open.entry.flags.find(flag => flag.key === open.modelKey);
121
+ const current = selected === undefined ? undefined : structuredValue(open.entry, open.record)[selected.key];
122
+ const allRows = (selected === undefined ? [] : modelLevelOptions(open, selected)).map(choice => ({
123
+ label: choice.value === current ? `✓ ${choice.label}` : choice.label,
124
+ value: choice.description,
125
+ }));
126
+ const description = `Select default thinking level for ${selected?.label ?? open.modelKey ?? "model"}`;
127
+ const visible = dialogWindow(allRows, open.index, configureDialogChoiceCapacity(height, false, configureDialogTextRows(description, width)));
128
+ return renderSteppedDialogPanel({
129
+ title: "Thinking Level",
130
+ step: 2,
131
+ steps: 2,
132
+ description,
133
+ rows: visible.rows,
134
+ index: visible.index,
135
+ hint: MODEL_SHORTCUTS.hintEntries(MODEL_LEVEL_SCOPE),
136
+ }, width, theme);
137
+ }
138
+ /** The rows a dialog of `maximum` rows shows, centred on `index` where the list allows. */
139
+ export function dialogWindow(rows, index, maximum) {
140
+ if (rows.length <= maximum)
141
+ return { rows, index: Math.min(Math.max(0, rows.length - 1), Math.max(0, index)) };
142
+ const start = Math.min(rows.length - maximum, Math.max(0, index - Math.floor(maximum / 2)));
143
+ return { rows: rows.slice(start, start + maximum), index: index - start };
144
+ }
145
+ // Concurrency: the dialog closes at once; a refused save reopens it with the reason.
146
+ function selectDefaultModel(open, model, actions) {
147
+ open.error = null;
148
+ actions.close();
149
+ void actions.session.change(open.entry.backend, open.entry.id, model).then(outcome => {
150
+ const failure = changeFailure(outcome);
151
+ if (failure !== null) {
152
+ open.error = `Could not save: ${failure}`;
153
+ actions.reopen(open);
154
+ }
155
+ });
156
+ }
157
+ function returnToModels(open) {
158
+ open.step = "model";
159
+ open.modelKey = null;
160
+ open.index = open.modelIndex;
161
+ }
162
+ function filteredModelFlags(open) {
163
+ const needle = open.filter?.value.trim().toLowerCase() ?? "";
164
+ return open.entry.flags.filter(flag => needle.length === 0
165
+ || (flag.label ?? humanizeLabel(flag.key)).toLowerCase().includes(needle)
166
+ || flag.description.toLowerCase().includes(needle)
167
+ || flag.key.toLowerCase().includes(needle));
168
+ }
169
+ function modelLevelOptions(open, selected) {
170
+ const fallback = String(selected.fallback);
171
+ const supported = (selected.choices ?? [])
172
+ .filter(choice => choice !== fallback)
173
+ .map(choice => ({ value: choice, label: choice, description: thinkingLevelDescription(choice) }));
174
+ return structuredValue(open.entry, open.record)[selected.key] === undefined
175
+ ? supported
176
+ : [...supported, { value: fallback, label: "(clear override)", description: "Revert to global default" }];
177
+ }
178
+ function modelLabelParts(label) {
179
+ const match = /^(.*?)(\s+\[[^\]]+\])$/u.exec(label);
180
+ return match === null ? { label } : { label: match[1] ?? label, labelSuffix: match[2] ?? "" };
181
+ }
182
+ function thinkingLevelDescription(level) {
183
+ switch (level) {
184
+ case "default": return "global default";
185
+ case "off": return "No reasoning";
186
+ case "minimal": return "Very brief reasoning (~1k tokens)";
187
+ case "low": return "Light reasoning (~2k tokens)";
188
+ case "medium": return "Moderate reasoning (~8k tokens)";
189
+ case "high": return "Deep reasoning (~16k tokens)";
190
+ case "xhigh": return "Extra-high reasoning (~32k tokens)";
191
+ case "max": return "Maximum reasoning";
192
+ default: return "";
193
+ }
194
+ }
@@ -13,6 +13,7 @@ import { readCertifiedReleaseManifest, resolveReleaseEntryPoint } from "./releas
13
13
  import { acquireUpdateExecutionLease, readUpdateExecutionOwner } from "./update-execution-lease.js";
14
14
  import { UpdateTransactionStore } from "./update-transaction.js";
15
15
  import { runSelfUpdate, UPDATE_ALREADY_RUNNING_EXIT_CODE } from "./update.js";
16
+ import { errorMessage, samePath } from "./managed-path.js";
16
17
  const AUTOMATIC_UPDATE_STATE_SCHEMA = 1;
17
18
  const AUTOMATIC_UPDATE_RETRY_MS = 24 * 60 * 60_000;
18
19
  const AUTOMATIC_UPDATE_STATE_FILENAME = "automatic-update.json";
@@ -406,14 +407,6 @@ function validateState(value) {
406
407
  throw new Error("invalid automatic update state");
407
408
  }
408
409
  }
409
- function samePath(left, right) {
410
- const canonicalLeft = resolve(left);
411
- const canonicalRight = resolve(right);
412
- return process.platform === "win32"
413
- ? canonicalLeft.toLowerCase() === canonicalRight.toLowerCase()
414
- : canonicalLeft === canonicalRight;
415
- }
416
410
  function hasCode(error, code) {
417
411
  return error instanceof Error && "code" in error && error.code === code;
418
412
  }
419
- function errorMessage(error) { return error instanceof Error ? error.message : String(error); }
@@ -1,12 +1,13 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { chmod, lstat, mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
3
- import { dirname, posix, relative, resolve, sep } from "node:path";
3
+ import { dirname, posix, resolve, sep } from "node:path";
4
4
  import { mapWithConcurrency } from "./concurrency.js";
5
5
  import { assertImmutableFileMode } from "./immutable-platform.js";
6
6
  import { readDependencyCertification, writeDependencyCertification as writeLayerCertification } from "./dependency-certification.js";
7
7
  export { dependencyLayerCertificationPath, legacyDependencyLayerCertificationPath } from "./dependency-certification.js";
8
8
  import { digestManifestFiles, releaseFileIdentity, resolveWithin } from "./release.js";
9
9
  import { PRODUCT_IDENTITY } from "../../product-identity.js";
10
+ import { isDirectChild } from "./managed-path.js";
10
11
  const LAYER_IO_CONCURRENCY = 32;
11
12
  export const DEPENDENCY_LAYER_SCHEMA = PRODUCT_IDENTITY.evidence.dependencyLayerSchema;
12
13
  export const DEPENDENCY_LAYER_MANIFEST = "dependency-layer-manifest.json";
@@ -460,7 +461,6 @@ function isKnownRuntimePath(path) {
460
461
  }
461
462
  function isJavaScriptModule(path) { return /\.(?:js|mjs|cjs)$/.test(path); }
462
463
  function assertDirectChild(parent, child) {
463
- const path = relative(parent, child);
464
- if (path.length === 0 || path === ".." || path.startsWith(`..${sep}`) || path.includes(sep))
464
+ if (!isDirectChild(parent, child))
465
465
  throw new Error(`dependency layer escapes managed storage: ${child}`);
466
466
  }
@@ -4,6 +4,7 @@ import { join } from "node:path";
4
4
  import { gt, prerelease, valid as validSemver } from "semver";
5
5
  import lockfile from "proper-lockfile";
6
6
  import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
7
+ import { errorMessage } from "./managed-path.js";
7
8
  const DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
8
9
  export const UPDATE_CHECK_INTERVAL_MS = 15 * 60_000;
9
10
  const STARTUP_FETCH_TIMEOUT_MS = 3_000;
@@ -255,4 +256,3 @@ function parseVersion(value, source) {
255
256
  throw new Error(`${source} did not provide a valid semantic version`);
256
257
  return parsed;
257
258
  }
258
- function errorMessage(error) { return error instanceof Error ? error.message : String(error); }
@@ -0,0 +1,10 @@
1
+ /** Whether two paths name the same location; Windows paths compare without case. */
2
+ export declare function samePath(left: string, right: string): boolean;
3
+ /**
4
+ * Whether `child` is exactly one non-empty segment below `parent`.
5
+ * Invariant: release storage operations act only on such paths, never on the root itself, another drive, a nested descendant,
6
+ * or anything reached through `..`. `path.relative` already ignores case on Windows.
7
+ */
8
+ export declare function isDirectChild(parent: string, child: string): boolean;
9
+ /** The message of a thrown value, whatever was thrown. */
10
+ export declare function errorMessage(error: unknown): string;
@@ -0,0 +1,26 @@
1
+ import { isAbsolute, relative, resolve, sep } from "node:path";
2
+ /** Whether two paths name the same location; Windows paths compare without case. */
3
+ export function samePath(left, right) {
4
+ const canonicalLeft = resolve(left);
5
+ const canonicalRight = resolve(right);
6
+ return process.platform === "win32"
7
+ ? canonicalLeft.toLowerCase() === canonicalRight.toLowerCase()
8
+ : canonicalLeft === canonicalRight;
9
+ }
10
+ /**
11
+ * Whether `child` is exactly one non-empty segment below `parent`.
12
+ * Invariant: release storage operations act only on such paths, never on the root itself, another drive, a nested descendant,
13
+ * or anything reached through `..`. `path.relative` already ignores case on Windows.
14
+ */
15
+ export function isDirectChild(parent, child) {
16
+ const fromParent = relative(resolve(parent), resolve(child));
17
+ return fromParent.length > 0
18
+ && fromParent !== ".."
19
+ && !fromParent.startsWith(`..${sep}`)
20
+ && !isAbsolute(fromParent)
21
+ && !fromParent.includes(sep);
22
+ }
23
+ /** The message of a thrown value, whatever was thrown. */
24
+ export function errorMessage(error) {
25
+ return error instanceof Error ? error.message : String(error);
26
+ }
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { spawn } from "node:child_process";
3
3
  import { lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat } from "node:fs/promises";
4
- import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
4
+ import { dirname, resolve, sep } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { ReleaseStateStore, } from "./release-state.js";
7
7
  import { resolveProductPaths } from "../lifecycle/index.js";
@@ -15,6 +15,7 @@ import { PRODUCT_IDENTITY } from "../../product-identity.js";
15
15
  import { collectCompileCaches, startupCompileCachePath } from "../startup/index.js";
16
16
  import { readLaunchContext } from "../launch-context/index.js";
17
17
  import { processIsAlive } from "./process-liveness.js";
18
+ import { errorMessage, isDirectChild } from "./managed-path.js";
18
19
  const DEFAULT_CANDIDATE_AGE_MS = 60 * 60 * 1_000;
19
20
  const DEFAULT_LIMITS = { maxItems: 8, maxDurationMs: 2_000, concurrency: 1 };
20
21
  const DEFAULT_WORKER_DURATION_MS = 10 * 60 * 1_000;
@@ -638,12 +639,6 @@ async function protectedCompileCachePaths(dataDir, state) {
638
639
  }
639
640
  return paths;
640
641
  }
641
- function assertDirectChild(parent, child) {
642
- const fromParent = relative(parent, child);
643
- if (fromParent.length === 0 || fromParent === ".." || fromParent.startsWith(`..${sep}`) || isAbsolute(fromParent) || fromParent.includes(sep)) {
644
- throw new Error(`cleanup path is not a direct child of its managed root: ${child}`);
645
- }
646
- }
647
642
  function normalizeLimits(input) {
648
643
  const limits = { ...DEFAULT_LIMITS, ...input };
649
644
  for (const [name, value] of Object.entries(limits)) {
@@ -748,4 +743,7 @@ function artifactName(path) { return path.split(sep).at(-1) ?? "artifact"; }
748
743
  function certificationReleaseId(path) {
749
744
  return /^certification-(.+)\.json$/.exec(artifactName(path))?.[1] ?? null;
750
745
  }
751
- function errorMessage(error) { return error instanceof Error ? error.message : String(error); }
746
+ function assertDirectChild(parent, child) {
747
+ if (!isDirectChild(parent, child))
748
+ throw new Error(`cleanup path is not a direct child of its managed root: ${child}`);
749
+ }
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { lstat, readFile, realpath, stat } from "node:fs/promises";
3
- import { basename, isAbsolute, relative, resolve, sep } from "node:path";
3
+ import { basename, resolve } from "node:path";
4
4
  import { PRODUCT_IDENTITY } from "../../product-identity.js";
5
5
  import { PRIVATE_LAUNCH_CONTRACT, launchContractTarget } from "../launch-context/index.js";
6
6
  import { DEPENDENCY_LAYER_MANIFEST, dependencyLayerCertificationPath, legacyDependencyLayerCertificationPath, readCertifiedDependencyLayer } from "./dependency-layer.js";
@@ -9,6 +9,7 @@ import { immutablePlatformPolicy } from "./immutable-platform.js";
9
9
  import { PRODUCT_PACKAGE_NAME } from "./release.js";
10
10
  import { RELEASE_MANIFEST_FILENAME } from "./release-store.js";
11
11
  import { UpdateTransactionStore } from "./update-transaction.js";
12
+ import { isDirectChild } from "./managed-path.js";
12
13
  const RESTART_SEAL_VERSION = 1;
13
14
  const REQUIRED_LAUNCH_ENTRIES = ["bin/ui.js"];
14
15
  /** Build compact restart authority after complete content verification established read-only payload files. */
@@ -219,9 +220,7 @@ async function observedRealpath(path, onEvent) {
219
220
  return await realpath(path);
220
221
  }
221
222
  function assertDirectChild(parent, child, expectedName, kind) {
222
- const fromParent = relative(parent, child);
223
- if (isAbsolute(fromParent) || fromParent === ".." || fromParent.startsWith(`..${sep}`) || fromParent.includes(sep)
224
- || basename(child) !== expectedName) {
223
+ if (!isDirectChild(parent, child) || basename(child) !== expectedName) {
225
224
  throw new Error(`restart ${kind} is outside its managed store: ${child}`);
226
225
  }
227
226
  }
@@ -8,6 +8,7 @@ import { ReleaseStateStore } from "./release-state.js";
8
8
  import { resolveProductPaths } from "../lifecycle/index.js";
9
9
  import { materializeRelease } from "./release-store.js";
10
10
  import { warmMaterializedRelease } from "./warmup.js";
11
+ import { errorMessage } from "./managed-path.js";
11
12
  /**
12
13
  * The one activation contract an installed tree can offer the updater that installed it.
13
14
  * The updater knows nothing else about the tree: the manifest names the contracts the tree
@@ -211,4 +212,3 @@ function summarize(text) {
211
212
  const lines = text.split(/\r?\n/).map(line => line.trim()).filter(line => line.length > 0);
212
213
  return lines.length === 0 ? "" : `: ${lines.slice(0, 4).join(" | ")}`;
213
214
  }
214
- function errorMessage(error) { return error instanceof Error ? error.message : String(error); }
@@ -4,6 +4,7 @@ import { dirname, resolve } from "node:path";
4
4
  import lockfile from "proper-lockfile";
5
5
  import { processIsAlive } from "./process-liveness.js";
6
6
  import { UpdateTransactionStore } from "./update-transaction.js";
7
+ import { samePath } from "./managed-path.js";
7
8
  const UPDATE_EXECUTION_LEASE_FILENAME = "update-execution.json";
8
9
  const LEASE_SCHEMA = 1;
9
10
  const MAX_LEASE_BYTES = 8_192;
@@ -146,13 +147,6 @@ async function foreignActiveTransaction(dataDir, owner, requested) {
146
147
  && samePath(requested.packageRoot, owner.packageRoot);
147
148
  return matches ? null : transaction;
148
149
  }
149
- function samePath(left, right) {
150
- const canonicalLeft = resolve(left);
151
- const canonicalRight = resolve(right);
152
- return process.platform === "win32"
153
- ? canonicalLeft.toLowerCase() === canonicalRight.toLowerCase()
154
- : canonicalLeft === canonicalRight;
155
- }
156
150
  function currentProcessStartIdentity() {
157
151
  return `${process.pid}:${Math.floor(Date.now() - process.uptime() * 1_000)}`;
158
152
  }
@@ -0,0 +1,45 @@
1
+ import type { ProductPaths } from "../lifecycle/index.js";
2
+ import type { UpdateRecoveryState, UpdateTransaction } from "./update-transaction.js";
3
+ import { type ProtectedPackageReplacementResult } from "./update-recovery.js";
4
+ /** The approved release protected replacement restores when the new package cannot launch. */
5
+ export interface UpdatePriorRelease {
6
+ readonly releaseId: string;
7
+ readonly releaseRoot: string;
8
+ readonly contentDigest: string;
9
+ }
10
+ /** What an update knows about the release that was active when it began. */
11
+ export interface UpdatePriorReleaseProtection {
12
+ /** The active release to roll back to, or null when none was recorded. */
13
+ readonly priorActiveReleaseId: string | null;
14
+ /** The approved record for a release id, or undefined when it cannot protect replacement. */
15
+ approvedRelease(releaseId: string | null): UpdatePriorRelease | undefined;
16
+ }
17
+ export interface UpdatePackageReplacementInput {
18
+ readonly globalRoot: string;
19
+ readonly npmCliRoot: string;
20
+ readonly npmCli?: string;
21
+ readonly packageRoot: string;
22
+ readonly transaction: UpdateTransaction;
23
+ readonly priorRelease: UpdatePriorRelease;
24
+ readonly output: {
25
+ stderr(message: string): void;
26
+ };
27
+ readonly onRecoveryState: (state: UpdateRecoveryState) => Promise<void>;
28
+ }
29
+ /**
30
+ * The update steps that touch npm and release storage. Every update runs all of them in the same
31
+ * order; a test replaces a step's behaviour, never which steps run.
32
+ */
33
+ export interface UpdateInstaller {
34
+ /** Resolve npm's CLI entry before anything changes; a throw stops the update with nothing changed. */
35
+ preflight(npmCliRoot: string): Promise<string | undefined>;
36
+ /** Make sure an approved release can launch while npm replaces the package. */
37
+ protectPriorRelease(packageRoot: string): Promise<UpdatePriorReleaseProtection>;
38
+ replacePackage(input: UpdatePackageReplacementInput): Promise<ProtectedPackageReplacementResult>;
39
+ /** Return to the prior release after a failure, describing what happened. "rolled back" means it did. */
40
+ rollback(priorReleaseId: string | null): Promise<string>;
41
+ /** Release cleanup committed after activation and before success is reported. */
42
+ maintenance(): Promise<void>;
43
+ }
44
+ /** The production installer: npm preflight, recovery release, protected replacement, rollback and release cleanup. */
45
+ export declare function createProtectedUpdateInstaller(paths: ProductPaths, environment: NodeJS.ProcessEnv): UpdateInstaller;
@@ -0,0 +1,57 @@
1
+ import { certifyMaterializedRelease } from "./bootstrap.js";
2
+ import { scheduleReleaseCleanup } from "./release-gc.js";
3
+ import { ReleaseStateStore } from "./release-state.js";
4
+ import { materializeRelease, readMaterializedRelease } from "./release-store.js";
5
+ import { resolveNpmCli, runProtectedPackageReplacement } from "./update-recovery.js";
6
+ /** The production installer: npm preflight, recovery release, protected replacement, rollback and release cleanup. */
7
+ export function createProtectedUpdateInstaller(paths, environment) {
8
+ const dataDir = paths.dataDir;
9
+ return {
10
+ preflight: async (npmCliRoot) => await resolveNpmCli(npmCliRoot, environment),
11
+ protectPriorRelease: async (packageRoot) => await protectPriorRelease(dataDir, packageRoot),
12
+ replacePackage: async (input) => await runProtectedPackageReplacement({ ...input, dataDir, environment }),
13
+ rollback: async (priorReleaseId) => await rollbackPriorRelease(dataDir, priorReleaseId),
14
+ maintenance: async () => { await scheduleReleaseCleanup(dataDir, paths); },
15
+ };
16
+ }
17
+ async function protectPriorRelease(dataDir, packageRoot) {
18
+ const releaseStateStore = new ReleaseStateStore(dataDir);
19
+ let state = await releaseStateStore.read();
20
+ let priorActiveReleaseId = state.references.active;
21
+ if (priorActiveReleaseId === null || state.releases[priorActiveReleaseId]?.approval !== "approved") {
22
+ // Invariant: protected replacement needs an immutable launch authority that npm cannot rename.
23
+ const recoveryRelease = await materializeRelease(packageRoot, dataDir);
24
+ await releaseStateStore.recordCandidate(recoveryRelease);
25
+ const diagnostics = await certifyMaterializedRelease(recoveryRelease, dataDir);
26
+ await releaseStateStore.approve(recoveryRelease.releaseId, diagnostics);
27
+ await releaseStateStore.activate(recoveryRelease.releaseId);
28
+ state = await releaseStateStore.read();
29
+ priorActiveReleaseId = recoveryRelease.releaseId;
30
+ }
31
+ const releases = state.releases;
32
+ return {
33
+ priorActiveReleaseId,
34
+ approvedRelease: releaseId => {
35
+ const record = releaseId === null ? undefined : releases[releaseId];
36
+ return record?.approval === "approved" ? record : undefined;
37
+ },
38
+ };
39
+ }
40
+ async function rollbackPriorRelease(dataDir, priorReleaseId) {
41
+ if (!priorReleaseId)
42
+ return "no prior release was available for rollback";
43
+ const stateStore = new ReleaseStateStore(dataDir);
44
+ const state = await stateStore.read();
45
+ const prior = state.releases[priorReleaseId];
46
+ if (!prior || prior.approval !== "approved")
47
+ return "prior release is not a verified rollback candidate";
48
+ if (state.references.active !== priorReleaseId) {
49
+ if (state.references.rollback === priorReleaseId)
50
+ await stateStore.rollback();
51
+ else
52
+ throw new Error(`prior release ${priorReleaseId} is not the recorded rollback release`);
53
+ }
54
+ // Invariant: rollback selects only a prior release whose retained content still verifies.
55
+ await readMaterializedRelease(prior.releaseRoot);
56
+ return "rolled back";
57
+ }
@@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
6
6
  import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
7
7
  import { processIsAlive } from "./process-liveness.js";
8
8
  import { PRIVATE_LAUNCH_CONTRACT, assertCurrentLaunchContract } from "../launch-context/index.js";
9
+ import { isDirectChild, samePath } from "./managed-path.js";
9
10
  export const UPDATE_RECOVERY_SCHEMA = "a1-update-recovery-v1";
10
11
  const WORKER_TIMEOUT_MS = 15 * 60 * 1_000;
11
12
  const POLL_MS = 50;
@@ -429,9 +430,6 @@ async function writeDurableJson(path, value) {
429
430
  await rename(temporary, path);
430
431
  }
431
432
  async function readJson(path) { return JSON.parse(await readFile(path, "utf8")); }
432
- function samePath(left, right) {
433
- return process.platform === "win32" ? resolve(left).toLowerCase() === resolve(right).toLowerCase() : resolve(left) === resolve(right);
434
- }
435
433
  function sameSegment(left, right, platform) {
436
434
  return platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
437
435
  }
@@ -443,11 +441,6 @@ function containedBy(parent, child) {
443
441
  return fromParent.length > 0 && fromParent !== ".." && !fromParent.startsWith(`..${sep}`) && !isAbsolute(fromParent);
444
442
  }
445
443
  function assertDirectChild(parent, child) {
446
- const expectedParent = resolve(parent);
447
- const actualParent = dirname(resolve(child));
448
- const matches = process.platform === "win32"
449
- ? actualParent.toLowerCase() === expectedParent.toLowerCase()
450
- : actualParent === expectedParent;
451
- if (!matches)
444
+ if (!isDirectChild(parent, child))
452
445
  throw new Error(`update recovery path is outside its managed root: ${child}`);
453
446
  }
@@ -3,7 +3,8 @@ import { type RegistryFetcher } from "./latest-release.js";
3
3
  import { type UpdateActivationPhase, type UpdateMaterializationProgress } from "./update-activation.js";
4
4
  import { type UpdateRecoveryState, type UpdateTransaction, type UpdateTransactionPhase } from "./update-transaction.js";
5
5
  import { type UpdateExecutionOwner } from "./update-execution-lease.js";
6
- import { resolveNpmCli, type ProtectedPackageReplacementResult } from "./update-recovery.js";
6
+ import { type UpdateInstaller } from "./update-installer.js";
7
+ export type { UpdateInstaller, UpdatePackageReplacementInput, UpdatePriorRelease, UpdatePriorReleaseProtection, } from "./update-installer.js";
7
8
  export declare const PRODUCT_PACKAGE: string;
8
9
  export type { UpdateChannel } from "./types.js";
9
10
  export interface ProcessRequest {
@@ -76,12 +77,8 @@ export interface SelfUpdateOptions {
76
77
  fetcher?: RegistryFetcher;
77
78
  lifecycle?: UpdateLifecycleCoordinator;
78
79
  transactionStore?: UpdateTransactionJournal;
79
- /** Test seam for protected global package replacement. */
80
- packageReplacement?: (input: UpdatePackageReplacementInput) => Promise<ProtectedPackageReplacementResult>;
81
- /** Test seam for proving replacement-authority preflight ordering. */
82
- npmCliResolver?: typeof resolveNpmCli;
83
- /** Test or embedding seam for post-activation release maintenance. */
84
- maintenance?: () => Promise<void>;
80
+ /** The npm and release-storage steps; defaults to protected replacement of the managed package. */
81
+ installer?: UpdateInstaller;
85
82
  progress?: boolean;
86
83
  onPhaseTiming?: (event: UpdatePhaseTimingEvent) => void;
87
84
  now?: () => number;
@@ -107,22 +104,6 @@ export interface UpdateTransactionJournal {
107
104
  finish(status: "completed" | "rolled-back" | "failed", error?: string | null): Promise<UpdateTransaction>;
108
105
  clearCompleted(): Promise<void>;
109
106
  }
110
- export interface UpdatePackageReplacementInput {
111
- readonly dataDir: string;
112
- readonly globalRoot: string;
113
- readonly npmCliRoot: string;
114
- readonly npmCli?: string;
115
- readonly packageRoot: string;
116
- readonly transaction: UpdateTransaction;
117
- readonly priorRelease: {
118
- readonly releaseId: string;
119
- readonly releaseRoot: string;
120
- readonly contentDigest: string;
121
- };
122
- readonly output: UpdateOutput;
123
- readonly environment: NodeJS.ProcessEnv;
124
- readonly onRecoveryState: (state: UpdateRecoveryState) => Promise<void>;
125
- }
126
107
  /**
127
108
  * The package tree is checked for holders by renaming it and renaming it back, because on
128
109
  * Windows that is the one operation that fails while any file under it is open. A holder is