gentle-pi 4.3.0 → 4.5.0
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/README.md +10 -7
- package/assets/agents/gentle-ai-worker.md +1 -1
- package/assets/design-canvas/canvas.css +648 -0
- package/assets/design-canvas/canvas.js +1497 -0
- package/assets/design-canvas/index.html +88 -0
- package/assets/design-canvas/inspector.js +290 -0
- package/assets/orchestrator-delegation-reference.md +2 -2
- package/assets/orchestrator-verification.md +1 -1
- package/assets/orchestrator.md +2 -2
- package/bin/engram-guard +1 -1
- package/bin/gentle-shell.mjs +138 -155
- package/docs/bundled-install.md +47 -42
- package/docs/delegated-verification.md +6 -0
- package/docs/gentle-agents-activity.md +5 -1
- package/docs/gentle-music.md +82 -0
- package/docs/gentle-shell.md +80 -10
- package/docs/install-wizard.md +30 -6
- package/docs/permission-modes.md +7 -2
- package/docs/readme-reference.md +72 -55
- package/docs/session-profile-format.md +26 -0
- package/docs/voice.md +259 -0
- package/docs/yolo-mode.md +17 -2
- package/extensions/ask-user-choice.ts +4 -1
- package/extensions/gentle-agents.ts +187 -40
- package/extensions/gentle-ai.ts +1765 -1185
- package/extensions/gentle-design.ts +158 -0
- package/extensions/gentle-music.ts +354 -0
- package/extensions/gentle-shell.ts +418 -79
- package/extensions/gentle-voice.ts +11 -0
- package/extensions/session-scratch.ts +41 -22
- package/extensions/skill-registry.ts +63 -8
- package/lib/agent-profiles.ts +26 -0
- package/lib/agents-config.ts +35 -0
- package/lib/agents-protocol.ts +57 -1
- package/lib/agents-runner.ts +52 -3
- package/lib/agents-session-transport.ts +101 -6
- package/lib/agents-widget.ts +14 -1
- package/lib/auto-permission-memory.ts +48 -18
- package/lib/auto-permission-runtime.ts +21 -6
- package/lib/clipboard-image.ts +147 -0
- package/lib/command-palette-catalog.ts +3 -2
- package/lib/commit-slice-signal.ts +1 -1
- package/lib/context-backstop.ts +36 -0
- package/lib/design-canvas-server.ts +947 -0
- package/lib/design-comments.ts +437 -0
- package/lib/design-handoffs.ts +313 -0
- package/lib/design-open-browser.ts +58 -0
- package/lib/design-sources.ts +120 -0
- package/lib/external-editor.ts +31 -0
- package/lib/gentle-design.ts +513 -0
- package/lib/gentle-shell-bundled.ts +141 -57
- package/lib/gentle-shell-launcher.ts +11 -4
- package/lib/gentle-shell-startup-update.ts +61 -9
- package/lib/gentle-shell-uninstall.ts +33 -1
- package/lib/image-attachments.ts +150 -0
- package/lib/inprocess-reviewer.ts +33 -2
- package/lib/jobs-sidebar-state.ts +22 -3
- package/lib/jobs-widget.ts +112 -0
- package/lib/nan-provider.ts +3 -0
- package/lib/native-review-cli.ts +11 -4
- package/lib/notification-audio-native.ts +149 -13
- package/lib/notification-audio.ts +4 -0
- package/lib/notification-pulse-worker.ts +82 -18
- package/lib/notification-service.ts +5 -1
- package/lib/orchestrator-presence.ts +78 -8
- package/lib/orchestrator-scope.ts +10 -0
- package/lib/orchestrator-state.ts +11 -3
- package/lib/permission-mode-policy.ts +2 -2
- package/lib/review-host-relay.ts +11 -2
- package/lib/review-reminder-receipt.ts +24 -1
- package/lib/session-profile-append-controller.ts +713 -0
- package/lib/session-profile-disk-reader.ts +182 -0
- package/lib/session-profile-freeze.ts +2 -2
- package/lib/session-profile-persistence.ts +15 -7
- package/lib/session-scratch.ts +52 -3
- package/lib/session-worktree-registry.ts +7 -0
- package/lib/shell-bar.ts +6 -7
- package/lib/shell-changes.ts +81 -5
- package/lib/shell-prompt.ts +4 -4
- package/lib/shell-sidebar-layout.ts +27 -7
- package/lib/shell-todo.ts +1 -1
- package/lib/verifier-admission.ts +5 -4
- package/lib/vim-editor-adapter.ts +14 -10
- package/lib/voice-artifacts.ts +188 -0
- package/lib/voice-command.ts +576 -0
- package/lib/voice-install.ts +455 -0
- package/lib/voice-platform.ts +99 -0
- package/lib/voice-push-to-talk.ts +262 -0
- package/lib/voice-session.ts +460 -0
- package/lib/voice-settings.ts +50 -0
- package/lib/voice-ui.ts +158 -0
- package/lib/yolo-session-policy.ts +58 -22
- package/package.json +1 -1
- package/runtime/gentle-shell-bundled.mjs +140 -56
- package/runtime/gentle-shell-launcher.mjs +11 -4
- package/runtime/gentle-shell-startup-update.mjs +61 -9
- package/runtime/gentle-shell-uninstall.mjs +33 -1
- package/runtime/native-review-cli.mjs +11 -4
- package/scripts/bundled-engram-migration.mjs +0 -6
- package/scripts/bundled-install.mjs +209 -13
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/installer-downloads.mjs +31 -7
- package/scripts/installer-preflight.mjs +6 -4
- package/scripts/installer-windows.mjs +37 -7
- package/scripts/main-channel.mjs +91 -17
- package/scripts/pi-update.mjs +37 -7
- package/scripts/verify-package-files.mjs +26 -2
- package/scripts/voice-worker.cjs +476 -0
- package/skills/design/SKILL.md +110 -0
- package/tests/agent-profiles.test.ts +24 -0
- package/tests/agents-config.test.ts +61 -0
- package/tests/agents-fake-child.ts +8 -1
- package/tests/agents-protocol.test.ts +32 -0
- package/tests/agents-runner.test.ts +226 -5
- package/tests/agents-session-transport.test.ts +53 -3
- package/tests/ask-user-choice.test.ts +30 -0
- package/tests/auto-mode.test.ts +17 -0
- package/tests/auto-permission-memory-diagnostics.test.ts +56 -0
- package/tests/auto-permission-runtime.test.ts +85 -1
- package/tests/autonomous-guard.test.ts +20 -0
- package/tests/bundled-install.test.ts +276 -5
- package/tests/clipboard-image.test.ts +114 -0
- package/tests/command-palette.test.ts +4 -4
- package/tests/context-backstop.test.ts +70 -4
- package/tests/delegate-for-reason-contract.test.ts +2 -1
- package/tests/design-canvas-assets.test.ts +710 -0
- package/tests/design-canvas-server.test.ts +1096 -0
- package/tests/design-comments.test.ts +201 -0
- package/tests/design-handoffs.test.ts +282 -0
- package/tests/design-open-browser.test.ts +70 -0
- package/tests/design-sources.test.ts +132 -0
- package/tests/engram-safety.test.ts +132 -57
- package/tests/external-editor.test.ts +84 -0
- package/tests/fixtures/primary-session-prompt/gentleman.golden.md +3 -3
- package/tests/gentle-agents.test.ts +369 -24
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +51 -51
- package/tests/gentle-ai.test.ts +928 -448
- package/tests/gentle-design.test.ts +1313 -0
- package/tests/gentle-music.test.ts +452 -0
- package/tests/gentle-shell-bundled.test.ts +321 -248
- package/tests/gentle-shell-launcher.test.ts +29 -2
- package/tests/gentle-shell-startup-update.test.ts +77 -1
- package/tests/gentle-shell-uninstall.test.ts +59 -0
- package/tests/gentle-shell.test.ts +227 -27
- package/tests/gentle-todo.test.ts +11 -0
- package/tests/image-attachments.test.ts +119 -0
- package/tests/image-paste-chips.test.ts +236 -0
- package/tests/inprocess-reviewer.test.ts +55 -2
- package/tests/installer-bundles.test.ts +12 -6
- package/tests/installer-downloads.test.ts +32 -0
- package/tests/installer-posix-bootstrap.test.ts +8 -8
- package/tests/installer-preflight.test.ts +16 -0
- package/tests/installer-server.test.ts +2 -2
- package/tests/installer-windows-bootstrap.test.ts +61 -3
- package/tests/installer-windows-diagnostics.test.ts +111 -0
- package/tests/jobs-widget.test.ts +138 -0
- package/tests/main-channel-upgrade.test.ts +232 -2
- package/tests/main-channel.test.ts +140 -21
- package/tests/model-profiles-preservation.test.ts +2 -2
- package/tests/nan-provider.test.ts +36 -0
- package/tests/native-review-capability-contract.test.ts +11 -4
- package/tests/native-review-parity.test.ts +1 -1
- package/tests/notification-audio-native.test.ts +128 -9
- package/tests/notification-customize.test.ts +17 -5
- package/tests/odd-routing-canonical-ratchet.test.ts +2 -1
- package/tests/orchestrator-budget.test.ts +5 -2
- package/tests/orchestrator-presence.test.ts +100 -2
- package/tests/orchestrator-rdd-ownership.test.ts +2 -2
- package/tests/orchestrator-scope.test.ts +14 -0
- package/tests/orchestrator-state.test.ts +29 -0
- package/tests/package-files.test.ts +34 -0
- package/tests/package-manifest.test.ts +6 -6
- package/tests/permission-mode-policy.test.ts +10 -1
- package/tests/pi-update.test.ts +104 -4
- package/tests/rdd-aware-verification-contract.test.ts +1 -1
- package/tests/review-host-relay-routing.test.ts +88 -1
- package/tests/review-host-relay.test.ts +9 -2
- package/tests/review-json-arguments.test.ts +20 -3
- package/tests/review-loadout-rdd-off-sdk.test.ts +23 -2
- package/tests/review-relay-transport-agent.test.ts +26 -0
- package/tests/review-reminder-receipt.test.ts +31 -1
- package/tests/runtime-harness.mjs +188 -211
- package/tests/session-changes.test.ts +2 -1
- package/tests/session-profile-append-controller-fork.test.ts +458 -0
- package/tests/session-profile-append-controller-pi.test.ts +290 -0
- package/tests/session-profile-append-controller.test.ts +731 -0
- package/tests/session-profile-disk-reader-pi.test.ts +183 -0
- package/tests/session-profile-disk-reader.test.ts +311 -0
- package/tests/session-profile-freeze.test.ts +6 -4
- package/tests/session-scratch.test.ts +68 -2
- package/tests/session-worktree-registry.test.ts +13 -1
- package/tests/shell-bar.test.ts +18 -6
- package/tests/shell-changes.test.ts +148 -1
- package/tests/shell-prompt.test.ts +3 -3
- package/tests/shell-render-cache.test.ts +134 -1
- package/tests/shell-sidebar-layout.test.ts +67 -0
- package/tests/short-check-routing-contract.test.ts +1 -1
- package/tests/skill-collision-prefixes.test.ts +3 -1
- package/tests/skill-registry-watcher.test.ts +17 -5
- package/tests/skill-registry.test.ts +47 -3
- package/tests/task-size-routing-contract.test.ts +1 -1
- package/tests/test-strength-contract.test.ts +1 -1
- package/tests/verifier-admission.test.ts +18 -1
- package/tests/vim-editor-adapter.test.ts +2 -2
- package/tests/voice-command.test.ts +773 -0
- package/tests/voice-install.test.ts +323 -0
- package/tests/voice-platform.test.ts +176 -0
- package/tests/voice-push-to-talk.test.ts +444 -0
- package/tests/voice-session.test.ts +434 -0
- package/tests/voice-settings.test.ts +49 -0
- package/tests/voice-ui.test.ts +97 -0
- package/tests/voice-worker.test.ts +445 -0
- package/tests/weak-model-economy-prompt.test.ts +32 -0
- package/tests/windows-hidden-processes.test.ts +25 -2
- package/tests/yolo-customize.test.ts +5 -1
- package/tests/yolo-mode.test.ts +1 -1
- package/tests/yolo-session-policy-capture.test.ts +104 -0
|
@@ -21,6 +21,30 @@
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
/** The launcher config records the last live lookup so the everyday launch stays off the network. */
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
/** Reads the config's `startupReleaseCheck` key; anything malformed is no cache. */
|
|
39
|
+
export function parseStartupReleaseCache(value ) {
|
|
40
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
41
|
+
const { checkedAt, latest } = value ;
|
|
42
|
+
const ms = typeof checkedAt === "number" ? checkedAt : typeof checkedAt === "string" ? Date.parse(checkedAt) : Number.NaN;
|
|
43
|
+
if (!Number.isFinite(ms) || typeof latest !== "string" || latest === "") return undefined;
|
|
44
|
+
return { checkedAt: ms, latest };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const STARTUP_RELEASE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
24
48
|
|
|
25
49
|
export function shouldCheckStartupUpdate({ args, env, bundled, inputTTY, outputTTY } ) {
|
|
26
50
|
if (!bundled || !inputTTY || !outputTTY || args.command || args.piSubcommand || args.error || args.help || args.version || args.packageRoot !== undefined) {
|
|
@@ -62,9 +86,30 @@ function isNewer(latest , current ) {
|
|
|
62
86
|
return false;
|
|
63
87
|
}
|
|
64
88
|
|
|
65
|
-
|
|
89
|
+
function offerFor(current , latest , skipped ) {
|
|
66
90
|
const currentParts = stableParts(current);
|
|
67
91
|
if (!currentParts) return undefined;
|
|
92
|
+
if (latest === undefined || latest === skipped) return undefined;
|
|
93
|
+
const latestParts = stableParts(latest);
|
|
94
|
+
if (!latestParts || !isNewer(latestParts, currentParts)) return undefined;
|
|
95
|
+
return {
|
|
96
|
+
current,
|
|
97
|
+
latest,
|
|
98
|
+
notesUrl: `https://github.com/Gentleman-Programming/gentle-shell/releases/tag/v${latest}`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// A malformed cached latest is never trusted — it must not suppress the
|
|
103
|
+
// network check for a whole TTL window — so it counts as no cache at all.
|
|
104
|
+
function usableCache(cache , ttlMs , nowMs ) {
|
|
105
|
+
if (cache === undefined) return undefined;
|
|
106
|
+
if (!Number.isFinite(cache.checkedAt) || typeof cache.latest !== "string" || cache.latest === "" || stableParts(cache.latest) === undefined) return undefined;
|
|
107
|
+
return nowMs - cache.checkedAt < ttlMs ? cache : undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function findStartupUpdate({ current, skipped, lookup, timeoutMs = 1500, cache, nowMs = Date.now(), ttlMs = STARTUP_RELEASE_TTL_MS, cacheWrite } ) {
|
|
111
|
+
const cached = usableCache(cache, ttlMs, nowMs);
|
|
112
|
+
if (cached !== undefined) return offerFor(current, cached.latest, skipped);
|
|
68
113
|
const controller = new AbortController();
|
|
69
114
|
const deadline = Number.isFinite(timeoutMs) && timeoutMs >= 0 && timeoutMs <= 2_147_483_647 ? timeoutMs : 1500;
|
|
70
115
|
let timer ;
|
|
@@ -77,17 +122,24 @@ export async function findStartupUpdate({ current, skipped, lookup, timeoutMs =
|
|
|
77
122
|
});
|
|
78
123
|
// The race bounds startup even if the injected lookup ignores cancellation.
|
|
79
124
|
const latest = await Promise.race([lookup(controller.signal), timeout]);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
return
|
|
84
|
-
current,
|
|
85
|
-
latest,
|
|
86
|
-
notesUrl: `https://github.com/Gentleman-Programming/gentle-shell/releases/tag/v${latest}`,
|
|
87
|
-
};
|
|
125
|
+
// Only a real answer is worth a day's trust: a timeout or rejection must
|
|
126
|
+
// not suppress tomorrow's check.
|
|
127
|
+
if (typeof latest === "string") cacheWrite?.(latest);
|
|
128
|
+
return offerFor(current, latest, skipped);
|
|
88
129
|
} catch {
|
|
89
130
|
return undefined;
|
|
90
131
|
} finally {
|
|
91
132
|
clearTimeout(timer);
|
|
92
133
|
}
|
|
93
134
|
}
|
|
135
|
+
|
|
136
|
+
// Mirrors omitStartupVersion: preserves every other config key, fails loudly on
|
|
137
|
+
// an invalid config, and records the lookup's answer with an injected clock.
|
|
138
|
+
export function writeStartupReleaseCache(read , write , version , nowMs = Date.now()) {
|
|
139
|
+
const text = read();
|
|
140
|
+
const config = text === undefined ? {} : JSON.parse(text);
|
|
141
|
+
if (config === null || typeof config !== "object" || Array.isArray(config)) throw new Error("Invalid launcher config");
|
|
142
|
+
const cache = { checkedAt: nowMs, latest: version };
|
|
143
|
+
const serialized = JSON.stringify({ ...config, startupReleaseCheck: { checkedAt: new Date(cache.checkedAt).toISOString(), latest: cache.latest } }, null, 2);
|
|
144
|
+
write(`${serialized}\n`);
|
|
145
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// Generated by scripts/build-runtime-modules.mjs. Do not edit.
|
|
2
|
-
import { lstatSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmdirSync, rmSync } from "node:fs";
|
|
2
|
+
import { lstatSync, readdirSync, readFileSync, readlinkSync, realpathSync, renameSync, rmdirSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
3
4
|
import { createInterface } from "node:readline";
|
|
4
5
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
6
|
+
import { withoutBundledNpmCommand } from "./gentle-shell-bundled.mjs";
|
|
5
7
|
import { isolatedDir, launcherConfigPath, parseRawLauncherConfig, provisionedEntry, userPiHome } from "./gentle-shell-launcher.mjs";
|
|
6
8
|
|
|
7
9
|
// `gentle-shell self-uninstall`: removes what Gentle Shell itself created, then
|
|
@@ -779,6 +781,18 @@ function printBundledPlan(plan , input , o
|
|
|
779
781
|
}
|
|
780
782
|
}
|
|
781
783
|
|
|
784
|
+
/** Replaces `path`'s text through a same-directory temporary file, so a crash never leaves it half-written. */
|
|
785
|
+
function writeTextAtomically(path , text ) {
|
|
786
|
+
const temporary = join(dirname(path), `.${basename(path)}.${randomUUID()}.tmp`);
|
|
787
|
+
try {
|
|
788
|
+
writeFileSync(temporary, text);
|
|
789
|
+
renameSync(temporary, path);
|
|
790
|
+
} catch (error) {
|
|
791
|
+
try { unlinkSync(temporary); } catch { /* The temporary file is best effort. */ }
|
|
792
|
+
throw error;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
782
796
|
/**
|
|
783
797
|
* Runs `gentle-shell self-uninstall` for the bundled install at `root`. Exit
|
|
784
798
|
* codes as runSelfUninstall: 0 done or dry run, 1 refused, cancelled or failed,
|
|
@@ -838,6 +852,24 @@ export async function runBundledSelfUninstall(input )
|
|
|
838
852
|
} catch {
|
|
839
853
|
// A leftover empty folder is harmless.
|
|
840
854
|
}
|
|
855
|
+
// A home that stays must not keep Pi pointed at the npm this uninstall removed
|
|
856
|
+
// (#2469): an `npmCommand` naming the prefix fails every later `pi install`
|
|
857
|
+
// with ENOENT naming the deleted node.exe.
|
|
858
|
+
if (!confirmation.includeShared) {
|
|
859
|
+
const settingsPath = join(plan.shellHome, "agent", "settings.json");
|
|
860
|
+
let settingsText ;
|
|
861
|
+
try { settingsText = readFileSync(settingsPath, "utf8"); } catch { settingsText = undefined; }
|
|
862
|
+
const removal = withoutBundledNpmCommand(settingsText, (path) => within(root, path, platform));
|
|
863
|
+
if (removal.status === "removed") {
|
|
864
|
+
try {
|
|
865
|
+
writeTextAtomically(settingsPath, removal.text);
|
|
866
|
+
out(`Removed npmCommand from ${settingsPath}: it named the removed install.`);
|
|
867
|
+
} catch (error) {
|
|
868
|
+
err(`gentle-shell self-uninstall: could not remove npmCommand from ${settingsPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
869
|
+
err(`Pi package installs keep failing until the npmCommand naming ${root} is deleted from it.`);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
841
873
|
if (plan.held.length > 0) {
|
|
842
874
|
try {
|
|
843
875
|
if (!input.deferRemoval) throw new Error("no deferred removal on this system");
|
|
@@ -203,10 +203,10 @@ export const NATIVE_REVIEW_MODE_SCOPE = {
|
|
|
203
203
|
// stream while still succeeding. That last qualifier is what keeps this list
|
|
204
204
|
// honest: v2.4.0 deleted reviewConsentSkippedDefaultProvenance ("Reviews are
|
|
205
205
|
// on by default; this was never explicitly chosen. ..."), which used to ride
|
|
206
|
-
// with the skip notice whenever the resolved mode source was `default`.
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
206
|
+
// with the skip notice whenever the resolved mode source was `default`. Reviews
|
|
207
|
+
// are on by default again since gentle-ai v4.0.0, but the pinned binary no
|
|
208
|
+
// longer has that function and cannot emit the line, so it is removed here
|
|
209
|
+
// rather than left as dead tolerance. Multi-
|
|
210
210
|
// line stderr is still expected in principle — these are separate Fprintln
|
|
211
211
|
// calls, never one joined string — which is why membership is per line.
|
|
212
212
|
export const REVIEW_CONSENT_NOTICES = Object.freeze([
|
|
@@ -1091,6 +1091,13 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
|
|
|
1091
1091
|
// 9d2927a84ba9a596f69029e3f854b16f86ca4c8b32cc5d63683764ccf2fb394b.
|
|
1092
1092
|
// No new negotiated capability identity is asserted.
|
|
1093
1093
|
"4.2.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
1094
|
+
// v4.3.1 repeats 4.2.0: the published binary still advertises
|
|
1095
|
+
// capabilities/v2.6 under review-integration/v2, and the provider contract
|
|
1096
|
+
// stays at 1.4.0, with published tarball SHA-256
|
|
1097
|
+
// 58450471280aeb357443d6a28c819132d8c8eb78dd25b12bac64296b2f8c27c2.
|
|
1098
|
+
// The v4.3.0 tag has no release and is never pinned.
|
|
1099
|
+
// No new negotiated capability identity is asserted.
|
|
1100
|
+
"4.3.1": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
1094
1101
|
});
|
|
1095
1102
|
|
|
1096
1103
|
|
|
@@ -43,12 +43,6 @@ function coreHelp(core, env) {
|
|
|
43
43
|
function supportsMigration(core, env) {
|
|
44
44
|
return coreHelp(core, env).includes("migrate-copy --data-dir <absolute-source> --output-dir <new-absolute-destination> --confirm-quiescent --json");
|
|
45
45
|
}
|
|
46
|
-
export function supportsBundledSessionOwned(core, env) {
|
|
47
|
-
if (!core) return false;
|
|
48
|
-
const clean = Object.fromEntries(Object.entries(env).filter(([key]) => !key.startsWith("ENGRAM_") && key !== "GH_TOKEN" && key !== "GITHUB_TOKEN"));
|
|
49
|
-
try { return coreHelp(core, { ...clean, ENGRAM_NO_UPDATE_CHECK: "1" }).some((line) => /^serve --session-owned(?:\s|$)/.test(line)); }
|
|
50
|
-
catch { return false; }
|
|
51
|
-
}
|
|
52
46
|
|
|
53
47
|
export function migrateBundledEngram({ prefix, core, home, env }) {
|
|
54
48
|
let markerOwned = false;
|
|
@@ -11,7 +11,7 @@ import { posix, win32 } from "node:path";
|
|
|
11
11
|
import { spawn } from "node:child_process";
|
|
12
12
|
import { pathToFileURL } from "node:url";
|
|
13
13
|
import { crc32, gunzipSync, inflateRawSync } from "node:zlib";
|
|
14
|
-
import { acquireGo, artifactFor, engramPinVersion, verifiedDownload } from "./installer-downloads.mjs";
|
|
14
|
+
import { acquireGo, artifactFor, engramPinVersion, uvPinVersion, verifiedDownload } from "./installer-downloads.mjs";
|
|
15
15
|
import { ensureWindowsPrivateFolder, readWindowsPnpmArchive, verifyWindowsStorage } from "./installer-windows.mjs";
|
|
16
16
|
|
|
17
17
|
// The acquisition pins already verified for the installer; never a second copy.
|
|
@@ -23,9 +23,30 @@ const LAUNCHER_MARK = "gentle-shell bundled launcher (generated; do not edit)";
|
|
|
23
23
|
const LAUNCHER_MISSING = "gentle-shell: no active Gentle Shell version; run the Gentle Shell installer again.";
|
|
24
24
|
const PROFILE_MARK = "# gentle-shell bundled install";
|
|
25
25
|
const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
26
|
+
const STABLE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
27
|
+
const PI_PACKAGE = "@earendil-works/pi-coding-agent";
|
|
26
28
|
const VERSION_ID = /^[0-9A-Za-z][0-9A-Za-z.-]*$/;
|
|
27
29
|
const MAX_EXPANDED = 512 * 1024 * 1024;
|
|
28
30
|
const INSTALL_TIMEOUT = 15 * 60 * 1000;
|
|
31
|
+
// The gentle-stream commit the /gentle:music offer installs, as a GitHub
|
|
32
|
+
// archive (no git needed); `samples` adds pyfluidsynth for the sample voices.
|
|
33
|
+
export const MUSIC_PLAYER_ARCHIVE = "https://github.com/Gentleman-Programming/gentle-stream/archive/381290451544ad2ca3348a68a81b3056bdac73b7.tar.gz";
|
|
34
|
+
const MUSIC_PYTHON = "3.13";
|
|
35
|
+
const MUSIC_MARKER = ".gentle-shell-music";
|
|
36
|
+
// uv links its tool executables on POSIX (bin/gentle-stream -> tools/…): the
|
|
37
|
+
// player counts when it resolves to a regular file inside <prefix>/music.
|
|
38
|
+
// The /gentle:music extension asks the same question before running it.
|
|
39
|
+
export function installedMusicPlayer(layout) {
|
|
40
|
+
try {
|
|
41
|
+
const real = realpathSync(layout.gentleStream);
|
|
42
|
+
const path = pathFor(process.platform);
|
|
43
|
+
const inside = path.relative(realpathSync(layout.music), real);
|
|
44
|
+
return !inside.startsWith("..") && !path.isAbsolute(inside) && lstatSync(real).isFile();
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const musicSpec = (samples) => `gentle-stream${samples ? "[samples]" : ""} @ ${MUSIC_PLAYER_ARCHIVE}`;
|
|
29
50
|
|
|
30
51
|
const pathFor = (platform) => (platform === "win32" ? win32 : posix);
|
|
31
52
|
function stat(path) {
|
|
@@ -77,6 +98,12 @@ export function prefixLayout({ platform, env = {}, home, root }) {
|
|
|
77
98
|
// The pinned Engram (ensureEngram), given to Gentle AI and gentle-engram (S9).
|
|
78
99
|
engramDir: path.join(runtime, `engram-${engramPinVersion}`),
|
|
79
100
|
engram: path.join(runtime, `engram-${engramPinVersion}`, windows ? "engram.exe" : "engram"),
|
|
101
|
+
// Only when /gentle:music's offer is accepted (installMusicPlayer): the
|
|
102
|
+
// pinned uv, and the Python and gentle-stream it installs under music/.
|
|
103
|
+
uvDir: path.join(runtime, `uv-${uvPinVersion}`),
|
|
104
|
+
uv: path.join(runtime, `uv-${uvPinVersion}`, windows ? "uv.exe" : "uv"),
|
|
105
|
+
music: path.join(base, "music"),
|
|
106
|
+
gentleStream: path.join(base, "music", "bin", windows ? "gentle-stream.exe" : "gentle-stream"),
|
|
80
107
|
pnpmHome, store: path.join(pnpmHome, "store"), cache: path.join(pnpmHome, "cache"), state: path.join(pnpmHome, "state"),
|
|
81
108
|
config: path.join(pnpmHome, "config"), npmrc: path.join(pnpmHome, "config", "npmrc"), tmp: path.join(base, "tmp"),
|
|
82
109
|
versions: path.join(base, "versions"), history: path.join(base, "versions.history"),
|
|
@@ -412,6 +439,94 @@ export async function ensureEngram({ layout, platform, arch, env = {}, adapters
|
|
|
412
439
|
});
|
|
413
440
|
}
|
|
414
441
|
|
|
442
|
+
/** The one executable a verified uv release archive must contain: `uv` in the
|
|
443
|
+
* archive's own top folder (`uv-<target>/uv`) of the POSIX `.tar.gz`, or the
|
|
444
|
+
* top-level `uv.exe` of the Windows `.zip`. uvx, uvw and anything else are
|
|
445
|
+
* never written.
|
|
446
|
+
*/
|
|
447
|
+
export function uvExecutable(bytes, descriptor) {
|
|
448
|
+
const message = "uv archive rejected";
|
|
449
|
+
const url = String(descriptor?.url ?? "");
|
|
450
|
+
if (url.endsWith(".zip")) return zipMember(bytes, "uv.exe", message);
|
|
451
|
+
const stem = /\/(uv-[A-Za-z0-9_.-]+)\.tar\.gz$/.exec(url)?.[1];
|
|
452
|
+
if (!stem) throw new Error(message);
|
|
453
|
+
return tarMember(bytes, `${stem}/uv`, message);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** The pinned uv in runtime/uv-<v>/, published like Engram: downloaded through
|
|
457
|
+
* verifiedDownload, only its executable written, `uv --version` run from the
|
|
458
|
+
* staging folder, marked, then published. Never part of the default install:
|
|
459
|
+
* only installMusicPlayer asks for it. Returns whether it was acquired.
|
|
460
|
+
*/
|
|
461
|
+
export async function ensureUv({ layout, platform, arch, env = {}, adapters = {} }) {
|
|
462
|
+
const run = adapters.run ?? runCommand;
|
|
463
|
+
const path = pathFor(process.platform);
|
|
464
|
+
const childEnv = pnpmEnvironment(layout, { platform: layout.platform, env });
|
|
465
|
+
return publishRuntime(layout, "uv", layout.uvDir, layout.uv, { platform, arch, adapters }, async (stage, bytes, descriptor) => {
|
|
466
|
+
const binary = path.join(stage, path.relative(layout.uvDir, layout.uv));
|
|
467
|
+
writeFileSync(binary, uvExecutable(bytes, descriptor), { flag: "wx", mode: 0o700 });
|
|
468
|
+
const result = await run(binary, ["--version"], { cwd: layout.runtime, env: childEnv });
|
|
469
|
+
if (result.status !== 0 || !String(result.stdout).trim().startsWith(`uv ${descriptor.version} `)) throw new Error("Bundled uv verification failed");
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** The environment the music player's uv runs in: its tools, their executables,
|
|
474
|
+
* the uv-managed Python 3.13 and uv's cache all under <prefix>/music; no system
|
|
475
|
+
* Python and no uv.toml are consulted. Inherited UV_*, PYTHON*, PIP_*,
|
|
476
|
+
* VIRTUAL_ENV and CONDA_PREFIX are dropped; the rest (HOME, PATH, proxies, CA
|
|
477
|
+
* settings) is kept.
|
|
478
|
+
*/
|
|
479
|
+
export function musicEnvironment(layout, { env = {} } = {}) {
|
|
480
|
+
const path = pathFor(layout.platform);
|
|
481
|
+
const kept = Object.fromEntries(Object.entries(env).filter(([key]) => !/^(?:uv_.*|python.*|pip_.*|virtual_env|conda_prefix)$/i.test(key)));
|
|
482
|
+
return {
|
|
483
|
+
...kept,
|
|
484
|
+
UV_TOOL_DIR: path.join(layout.music, "tools"), UV_TOOL_BIN_DIR: path.dirname(layout.gentleStream),
|
|
485
|
+
UV_PYTHON_INSTALL_DIR: path.join(layout.music, "python"), UV_PYTHON_BIN_DIR: path.join(layout.music, "python", "bin"),
|
|
486
|
+
UV_CACHE_DIR: path.join(layout.music, "cache"),
|
|
487
|
+
UV_PYTHON: MUSIC_PYTHON, UV_PYTHON_PREFERENCE: "only-managed", UV_PYTHON_DOWNLOADS: "automatic", UV_NO_CONFIG: "1",
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** S5: the /gentle:music offer, after the user accepted it. Publishes the
|
|
492
|
+
* pinned uv (ensureUv), then `uv tool install`s the pinned gentle-stream
|
|
493
|
+
* archive with its `samples` extra into <prefix>/music (musicEnvironment);
|
|
494
|
+
* when that fails (pyfluidsynth, which needs the system FluidSynth library),
|
|
495
|
+
* once more without the extra. A marker naming the installed spec makes a
|
|
496
|
+
* second call a no-op. `out` gets one line per step. Returns the executable,
|
|
497
|
+
* whether the extra is in, and whether anything was installed.
|
|
498
|
+
*/
|
|
499
|
+
export async function installMusicPlayer({ layout, platform = layout.platform, arch = process.arch, env = {}, adapters = {}, out = () => {} }) {
|
|
500
|
+
const run = adapters.run ?? runCommand;
|
|
501
|
+
const path = pathFor(process.platform);
|
|
502
|
+
const marker = path.join(layout.music, MUSIC_MARKER);
|
|
503
|
+
const done = read(marker);
|
|
504
|
+
if (installedMusicPlayer(layout) && (done === `${musicSpec(true)}\n` || done === `${musicSpec(false)}\n`)) {
|
|
505
|
+
return { gentleStream: layout.gentleStream, samples: done === `${musicSpec(true)}\n`, installed: false };
|
|
506
|
+
}
|
|
507
|
+
out(`Downloading uv ${uvPinVersion} into Gentle Shell…`);
|
|
508
|
+
await ensureUv({ layout, platform, arch, env, adapters });
|
|
509
|
+
ensureDirectory(layout.music, layout.platform);
|
|
510
|
+
const childEnv = musicEnvironment(layout, { env });
|
|
511
|
+
const install = (samples) => run(layout.uv, ["tool", "install", "--python", MUSIC_PYTHON, musicSpec(samples)], { cwd: layout.music, env: childEnv, timeout: INSTALL_TIMEOUT });
|
|
512
|
+
out(`Installing Python ${MUSIC_PYTHON} and gentle-stream; this can take a few minutes…`);
|
|
513
|
+
let samples = true;
|
|
514
|
+
let result = await install(true);
|
|
515
|
+
if (result.status !== 0) {
|
|
516
|
+
out("The samples extra could not be installed (it needs the system FluidSynth library); retrying without the samples extra…");
|
|
517
|
+
samples = false;
|
|
518
|
+
result = await install(false);
|
|
519
|
+
}
|
|
520
|
+
if (result.status !== 0 || !installedMusicPlayer(layout)) {
|
|
521
|
+
const reason = String(result.stderr ?? "").trim().split("\n").filter(Boolean).at(-1);
|
|
522
|
+
throw new Error(`uv could not install gentle-stream${reason ? `: ${reason}` : ""}`);
|
|
523
|
+
}
|
|
524
|
+
replaceAtomically(marker, (temporary) => writeFileSync(temporary, `${musicSpec(samples)}\n`, { flag: "wx", mode: 0o600 }));
|
|
525
|
+
// The downloaded wheels and archives are not needed once installed.
|
|
526
|
+
await run(layout.uv, ["cache", "clean"], { cwd: layout.music, env: childEnv, timeout: INSTALL_TIMEOUT });
|
|
527
|
+
return { gentleStream: layout.gentleStream, samples, installed: true };
|
|
528
|
+
}
|
|
529
|
+
|
|
415
530
|
/** Our pinned Node with its bundled npm, and pnpm, in runtime/ (and the pinned
|
|
416
531
|
* Go only when `go`, the pinned Engram only when `engram`), each downloaded
|
|
417
532
|
* through verifiedDownload, checked by running it from its staging folder,
|
|
@@ -670,11 +785,16 @@ function pinnedGo(layout, go) {
|
|
|
670
785
|
* not allowed. An installed version is reused; a folder left by an interrupted
|
|
671
786
|
* install of this id (its process gone) is removed and installed again; any
|
|
672
787
|
* other folder is never replaced. On Windows the pinned Go is required, so
|
|
673
|
-
* gentle-pi's postinstall never builds with the user's go.exe.
|
|
674
|
-
*
|
|
788
|
+
* gentle-pi's postinstall never builds with the user's go.exe. `unverified`
|
|
789
|
+
* (`gentle-shell update --latest` only) installs without a release lockfile:
|
|
790
|
+
* none is written beforehand and pnpm resolves the exact versions itself
|
|
791
|
+
* (`--no-frozen-lockfile`, so a CI environment cannot turn it frozen; the
|
|
792
|
+
* pnpm-lock.yaml pnpm leaves behind is never read again); the installed
|
|
793
|
+
* versions are still checked exactly. Adapters (run, and
|
|
794
|
+
* distribution for local fixture packages in place of the registry) are
|
|
675
795
|
* trusted local test code only.
|
|
676
796
|
*/
|
|
677
|
-
export async function installVersion({ layout, manifest, lockfile, platform = layout.platform, env = {}, go = null, adapters = {} }) {
|
|
797
|
+
export async function installVersion({ layout, manifest, lockfile, unverified = false, platform = layout.platform, env = {}, go = null, adapters = {} }) {
|
|
678
798
|
const files = (adapters.distribution ?? distributionFiles)(manifest);
|
|
679
799
|
const { shell, pi, id } = versionsOf(manifest);
|
|
680
800
|
const destination = versionDirectory(layout, id);
|
|
@@ -682,7 +802,7 @@ export async function installVersion({ layout, manifest, lockfile, platform = la
|
|
|
682
802
|
const marker = path.join(destination, VERSION_MARKER);
|
|
683
803
|
if (installedVersion(layout, id)) return { id, path: destination, installed: false };
|
|
684
804
|
if (platform === "win32" && !pinnedGo(layout, go)) throw new Error("A Windows version install needs the pinned Go from ensureRuntime({ go: true })");
|
|
685
|
-
if (typeof lockfile !== "string" || lockfile.length === 0) throw new Error("Release lockfile missing");
|
|
805
|
+
if (!unverified && (typeof lockfile !== "string" || lockfile.length === 0)) throw new Error("Release lockfile missing");
|
|
686
806
|
const existing = stat(destination);
|
|
687
807
|
if (existing) {
|
|
688
808
|
const pending = existing.isDirectory() ? /^installing (\S+) ([1-9]\d*)\n$/.exec(read(marker) ?? "") : null;
|
|
@@ -695,8 +815,8 @@ export async function installVersion({ layout, manifest, lockfile, platform = la
|
|
|
695
815
|
mkdirSync(destination, { mode: 0o700 });
|
|
696
816
|
try {
|
|
697
817
|
writeFileSync(marker, `installing ${id} ${process.pid}\n`, { flag: "wx", mode: 0o600 });
|
|
698
|
-
for (const [name, text] of Object.entries({ ...files, "pnpm-lock.yaml": lockfile })) writeFileSync(path.join(destination, name), text, { flag: "wx", mode: 0o600 });
|
|
699
|
-
const result = await (adapters.run ?? runCommand)(layout.node, [layout.pnpm, "install", "--frozen-lockfile"],
|
|
818
|
+
for (const [name, text] of Object.entries(unverified ? files : { ...files, "pnpm-lock.yaml": lockfile })) writeFileSync(path.join(destination, name), text, { flag: "wx", mode: 0o600 });
|
|
819
|
+
const result = await (adapters.run ?? runCommand)(layout.node, [layout.pnpm, "install", unverified ? "--no-frozen-lockfile" : "--frozen-lockfile"],
|
|
700
820
|
{ cwd: destination, env: pnpmEnvironment(layout, { platform, env, go }), timeout: INSTALL_TIMEOUT });
|
|
701
821
|
// pnpm prints install and postinstall failures on stdout: keep both tails.
|
|
702
822
|
if (result.status !== 0) {
|
|
@@ -704,7 +824,7 @@ export async function installVersion({ layout, manifest, lockfile, platform = la
|
|
|
704
824
|
{ stderr: [String(result.stdout ?? "").slice(-6000), String(result.stderr ?? "").slice(-4000)].filter(Boolean).join("\n") });
|
|
705
825
|
}
|
|
706
826
|
const shellDirectory = installedPackage(destination, "gentle-pi", shell, "gentle-pi");
|
|
707
|
-
installedPackage(destination,
|
|
827
|
+
installedPackage(destination, PI_PACKAGE, pi, "Pi");
|
|
708
828
|
if (!regular(path.join(shellDirectory, "bin", "gentle-shell.mjs"))) throw new Error("Installed gentle-pi has no launcher entry");
|
|
709
829
|
replaceAtomically(marker, (temp) => writeFileSync(temp, `${id}\n`, { flag: "wx", mode: 0o600 }));
|
|
710
830
|
return { id, path: destination, installed: true };
|
|
@@ -882,11 +1002,12 @@ function activateWithLauncher(layout, id, active, { module, next }) {
|
|
|
882
1002
|
* print exactly the release's gentle-pi and Pi before `current` switches; the
|
|
883
1003
|
* release's launcher is then written (a failure puts `current` back) and two
|
|
884
1004
|
* versions are kept (a pruning failure is only a warning). An already current
|
|
885
|
-
* release changes nothing
|
|
886
|
-
*
|
|
1005
|
+
* release changes nothing, and so does a release of the active Gentle Shell
|
|
1006
|
+
* whose Pi is older than the one `gentle-shell update --latest` installed (no
|
|
1007
|
+
* downgrade); any failure leaves `current` as it was and removes a version this
|
|
1008
|
+
* call installed. Returns { id, changed, previous?, removed?, warning? }.
|
|
887
1009
|
*/
|
|
888
1010
|
export async function upgradeBundled({ layout, home, platform = layout.platform, arch = process.arch, env = {}, adapters, out = () => {} }) {
|
|
889
|
-
const path = pathFor(process.platform);
|
|
890
1011
|
const latest = await adapters.latest();
|
|
891
1012
|
const release = await adapters.distribution(latest);
|
|
892
1013
|
if (release?.status !== "published") throw new Error((UNPUBLISHED[release?.status] ?? UNPUBLISHED.unavailable)(latest));
|
|
@@ -897,9 +1018,40 @@ export async function upgradeBundled({ layout, home, platform = layout.platform,
|
|
|
897
1018
|
out(`Gentle Shell ${shell} with Pi ${pi} is already current.`);
|
|
898
1019
|
return { id, changed: false };
|
|
899
1020
|
}
|
|
1021
|
+
const activePi = piOfVersion(active, shell);
|
|
1022
|
+
if (activePi && newerVersion(activePi, pi)) {
|
|
1023
|
+
out(`Gentle Shell ${shell} is current; Pi ${activePi} (installed with 'gentle-shell update --latest') is newer than this release's verified Pi ${pi}.`);
|
|
1024
|
+
return { id: active, changed: false };
|
|
1025
|
+
}
|
|
1026
|
+
return installAndActivate({ layout, home, platform, arch, env, adapters, out, manifest, lockfile, active,
|
|
1027
|
+
success: `Upgraded to Gentle Shell ${shell} with Pi ${pi} (from ${active ?? "no active version"}). Run 'gentle-shell upgrade --rollback' to switch back.` });
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/** The Pi of version id `<shell>-<pi>` for exactly this `shell`, or null. */
|
|
1031
|
+
function piOfVersion(id, shell) {
|
|
1032
|
+
const pi = typeof id === "string" && id.startsWith(`${shell}-`) ? id.slice(shell.length + 1) : "";
|
|
1033
|
+
return SEMVER.test(pi) ? pi : null;
|
|
1034
|
+
}
|
|
1035
|
+
/** Whether version `a` is newer than `b` by major, minor and patch. */
|
|
1036
|
+
function newerVersion(a, b) {
|
|
1037
|
+
const [left, right] = [a, b].map((version) => SEMVER.exec(version).slice(1, 4).map(Number));
|
|
1038
|
+
const index = left.findIndex((part, at) => part !== right[at]);
|
|
1039
|
+
return index !== -1 && left[index] > right[index];
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/** Installs `manifest` beside the active version (with the release lockfile,
|
|
1043
|
+
* or `unverified` without one) and switches to it the way upgradeBundled
|
|
1044
|
+
* always has: our runtime first, then the new version's own module and pinned
|
|
1045
|
+
* runtime, its launcher checked for exactly `shell` and `pi`, `current`
|
|
1046
|
+
* switched with that launcher, `success` printed and two versions kept (a
|
|
1047
|
+
* pruning failure is only a warning). Any failure leaves `current` as it was
|
|
1048
|
+
* and removes a version this call installed.
|
|
1049
|
+
*/
|
|
1050
|
+
async function installAndActivate({ layout, home, platform, arch, env, adapters, out, manifest, lockfile, unverified = false, active, success }) {
|
|
1051
|
+
const { shell, pi, id } = versionsOf(manifest);
|
|
900
1052
|
const go = platform === "win32";
|
|
901
1053
|
const runtime = await (adapters.ensureRuntime ?? ensureRuntime)({ layout, platform, arch, go, env });
|
|
902
|
-
const version = await installVersion({ layout, manifest, lockfile, platform, env, go: runtime.go, adapters: adapters.install ?? {} });
|
|
1054
|
+
const version = await installVersion({ layout, manifest, lockfile, unverified, platform, env, go: runtime.go, adapters: adapters.install ?? {} });
|
|
903
1055
|
try {
|
|
904
1056
|
const loaded = await loadRelease({ layout, id, home, platform, env, adapters, active });
|
|
905
1057
|
// The release's own pins, its Engram included (an older module ignores `engram`).
|
|
@@ -914,7 +1066,7 @@ export async function upgradeBundled({ layout, home, platform = layout.platform,
|
|
|
914
1066
|
if (version.installed && activeVersion(layout) !== id) rmSync(version.path, { recursive: true, force: true });
|
|
915
1067
|
throw error;
|
|
916
1068
|
}
|
|
917
|
-
out(
|
|
1069
|
+
out(success);
|
|
918
1070
|
try {
|
|
919
1071
|
return { id, changed: true, previous: active, removed: (adapters.prune ?? pruneVersions)(layout, 2) };
|
|
920
1072
|
} catch (error) {
|
|
@@ -924,6 +1076,50 @@ export async function upgradeBundled({ layout, home, platform = layout.platform,
|
|
|
924
1076
|
}
|
|
925
1077
|
}
|
|
926
1078
|
|
|
1079
|
+
/** The newest published stable Pi: our pnpm's `view` of Pi's `latest`
|
|
1080
|
+
* dist-tag, with the prefix npmrc (its registry and credentials). Only an
|
|
1081
|
+
* exact major.minor.patch is accepted.
|
|
1082
|
+
*/
|
|
1083
|
+
export async function publishedPi(layout, { platform = layout.platform, env = {}, run = runCommand } = {}) {
|
|
1084
|
+
const result = await (run ?? runCommand)(layout.node, [layout.pnpm, "view", `${PI_PACKAGE}@latest`, "version"],
|
|
1085
|
+
{ cwd: layout.root, env: pnpmEnvironment(layout, { platform, env }), timeout: 60000 });
|
|
1086
|
+
if (result.status !== 0) {
|
|
1087
|
+
throw Object.assign(new Error(`the latest published Pi could not be resolved (pnpm view exit ${result.status})`), { stderr: String(result.stderr ?? "").slice(-4000) });
|
|
1088
|
+
}
|
|
1089
|
+
const version = String(result.stdout ?? "").trim();
|
|
1090
|
+
if (!STABLE.test(version)) throw new Error(`the latest published Pi version ${JSON.stringify(version)} was rejected`);
|
|
1091
|
+
return version;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/** `gentle-shell update --latest` in a bundled install (S3): the newest
|
|
1095
|
+
* published stable Pi (adapters.latestPi) installed with this Gentle Shell
|
|
1096
|
+
* `shell` as version `<shell>-<pi>`, without the release's verified lockfile
|
|
1097
|
+
* (installVersion `unverified`), after a warning; then verified and activated
|
|
1098
|
+
* exactly like upgradeBundled (installAndActivate). An active version that
|
|
1099
|
+
* already has that Pi, or a newer one, changes nothing. Returns what upgradeBundled returns.
|
|
1100
|
+
*/
|
|
1101
|
+
export async function upgradePiLatest({ layout, home, shell, platform = layout.platform, arch = process.arch, env = {}, adapters, out = () => {} }) {
|
|
1102
|
+
const latest = await adapters.latestPi();
|
|
1103
|
+
if (!STABLE.test(String(latest))) throw new Error(`the latest published Pi version ${JSON.stringify(latest)} was rejected`);
|
|
1104
|
+
const manifest = { shell, pi: latest };
|
|
1105
|
+
const { pi, id } = versionsOf(manifest);
|
|
1106
|
+
const active = activeVersion(layout);
|
|
1107
|
+
if (active === id) {
|
|
1108
|
+
out(`Pi ${pi} is already the latest published Pi.`);
|
|
1109
|
+
return { id, changed: false };
|
|
1110
|
+
}
|
|
1111
|
+
const activePi = piOfVersion(active, shell);
|
|
1112
|
+
if (!activePi) throw new Error(`the active version ${active ?? "(none)"} is not Gentle Shell ${shell}; current stays as it is`);
|
|
1113
|
+
// A `latest` dist-tag moved back (or a release pinning a Pi newer than it) never downgrades Pi.
|
|
1114
|
+
if (!newerVersion(pi, activePi)) {
|
|
1115
|
+
out(`Pi ${activePi} is newer than the latest published Pi (${pi}); nothing changed.`);
|
|
1116
|
+
return { id: active, changed: false };
|
|
1117
|
+
}
|
|
1118
|
+
out(`warning: Pi ${pi} is newer than the Pi verified for this Gentle Shell release (${activePi}); it installs without the release's verified lockfile, and Gentle Shell features that rely on audited Pi internals (such as the Vim editor) may turn off until a Gentle Shell release verifies it.`);
|
|
1119
|
+
return installAndActivate({ layout, home, platform, arch, env, adapters, out, manifest, unverified: true, active,
|
|
1120
|
+
success: `Installed Pi ${pi} with Gentle Shell ${shell} (from ${active}). Run 'gentle-shell upgrade --rollback' to switch back.` });
|
|
1121
|
+
}
|
|
1122
|
+
|
|
927
1123
|
/** Whether version `id` reports exactly `gentle-shell <shell>` and `pi <pi>` to
|
|
928
1124
|
* `--version`: its entry run by our Node, or with `launcher` on POSIX the
|
|
929
1125
|
* bin/gentle-shell launcher itself (it runs the version `current` names).
|
|
@@ -37,7 +37,7 @@ const WINDOWS_SYSTEM_ROOT = "C:\\Windows";
|
|
|
37
37
|
// version check below) derives from this constant instead of repeating the
|
|
38
38
|
// literal, so a pin bump cannot leave a stale copy behind. See
|
|
39
39
|
// scripts/install-gentle-ai.mjs for the incident that motivated this.
|
|
40
|
-
export const INSTALLER_VERSION = "4.
|
|
40
|
+
export const INSTALLER_VERSION = "4.3.1";
|
|
41
41
|
export const RELEASE_BASE_URL = `https://github.com/Gentleman-Programming/gentle-ai/releases/download/v${INSTALLER_VERSION}/`;
|
|
42
42
|
export const GENTLE_AI_INSTALL_METHOD = Object.freeze({
|
|
43
43
|
SIGNED_RELEASE_ASSET: "signed-release-asset",
|
|
@@ -46,11 +46,11 @@ export const GENTLE_AI_INSTALL_METHOD = Object.freeze({
|
|
|
46
46
|
export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH = "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai";
|
|
47
47
|
export const GENTLE_AI_WINDOWS_SOURCE_MODULE = "github.com/gentleman-programming/gentle-ai/v4";
|
|
48
48
|
export const GENTLE_AI_WINDOWS_SOURCE_TAG = `v${INSTALLER_VERSION}`;
|
|
49
|
-
// `go mod download -json github.com/gentleman-programming/gentle-ai/v4@v4.
|
|
49
|
+
// `go mod download -json github.com/gentleman-programming/gentle-ai/v4@v4.3.1`
|
|
50
50
|
// with GOSUMDB=sum.golang.org reports this exact module SumDB checksum, and the
|
|
51
|
-
// tag resolves to commit
|
|
51
|
+
// tag resolves to commit 2dba7784114fff57864519c4c0d067e952a8c98b, the published v4.3.1 release head.
|
|
52
52
|
// v4.0.0 moved the Go module path to the /v4 major-version suffix.
|
|
53
|
-
export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:
|
|
53
|
+
export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:00p8dnjJrhYFq1Qikk2oY6K/UXIty9A/IBUUOUiX9XA=";
|
|
54
54
|
export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE = `${GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH}@${GENTLE_AI_WINDOWS_SOURCE_TAG}`;
|
|
55
55
|
export const GENTLE_AI_WINDOWS_MINIMUM_GO_VERSION = "1.25.10";
|
|
56
56
|
export const GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE_CODE = "GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE";
|
|
@@ -69,7 +69,7 @@ export class GentleAiInstallerError extends Error {
|
|
|
69
69
|
// Sentinel used while a re-pinned gentle-ai release is not yet published. A
|
|
70
70
|
// sentinel digest can never match a real SHA-256, so installation fails closed,
|
|
71
71
|
// and verify-package-files.mjs refuses to pack/publish while any digest below
|
|
72
|
-
// still holds it. The v4.
|
|
72
|
+
// still holds it. The v4.3.1 digests are pinned from the published release:
|
|
73
73
|
// archive sha256 values verified against the minisign-signed checksums.txt and
|
|
74
74
|
// freshly computed hashes; binary sha256 values computed from the extracted
|
|
75
75
|
// executables.
|
|
@@ -111,15 +111,15 @@ async function downloadPinnedGentleAiAsset(asset, destination, options) {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
// Windows is absent from signed release archives on purpose. gentle-ai stopped
|
|
114
|
-
// distributing unsigned Windows builds in c4b764d0, so v4.
|
|
114
|
+
// distributing unsigned Windows builds in c4b764d0, so v4.3.1 publishes signed
|
|
115
115
|
// Darwin/Linux archives only. Windows x64/arm64 uses the separately verified
|
|
116
116
|
// exact-tag Go SumDB source-build path below; restore archive rows only when
|
|
117
117
|
// upstream ships signed Windows assets.
|
|
118
118
|
export const GENTLE_AI_RELEASE_ASSETS = Object.freeze({
|
|
119
|
-
"darwin/amd64": asset("gentle-ai_4.
|
|
120
|
-
"darwin/arm64": asset("gentle-ai_4.
|
|
121
|
-
"linux/amd64": asset("gentle-ai_4.
|
|
122
|
-
"linux/arm64": asset("gentle-ai_4.
|
|
119
|
+
"darwin/amd64": asset("gentle-ai_4.3.1_darwin_amd64.tar.gz", "6cf57fee0574daef2abed07f54a355fec3b93ac8f01733dc6c4d3674f1746112", "ca44c3fce186b63a7e7ad455c338133ba1982f4a13485301ce4d66edb73d2928", "gentle-ai"),
|
|
120
|
+
"darwin/arm64": asset("gentle-ai_4.3.1_darwin_arm64.tar.gz", "13e8828447f9081465ee0c35a61b24fb49296d51a310c6999227e2aed178956b", "749d08062b0dec13681678a1a1c018613d223ddcca47b5f27681b1d8ab5d14c2", "gentle-ai"),
|
|
121
|
+
"linux/amd64": asset("gentle-ai_4.3.1_linux_amd64.tar.gz", "04ec685c56b42c29dff6db10fc7dc2c730217dc9c96736e53ad2acf3226dafb7", "86248792bde3fe5e9ca93b128c07e7caa6a5b31fac0f9fc84b5b3011b123cb90", "gentle-ai"),
|
|
122
|
+
"linux/arm64": asset("gentle-ai_4.3.1_linux_arm64.tar.gz", "99e7d313a2ff85eaf23a6415e9b3aae67bf74c2fa169b21b93070ada7613f6a0", "df3580506bb5ea63cc4ef5ccebe8b68cfedf66b72f3b51f84ff393183d8cea2b", "gentle-ai"),
|
|
123
123
|
});
|
|
124
124
|
|
|
125
125
|
// A pinned asset is either a signed archive or, for a prerelease pin only,
|
|
@@ -44,19 +44,36 @@ export const goPinVersion = go.version;
|
|
|
44
44
|
// checksums.txt and GitHub's asset digests; every archive was downloaded and
|
|
45
45
|
// re-hashed before pinning. GitHub serves the assets from its asset hosts.
|
|
46
46
|
const engram = Object.freeze({
|
|
47
|
-
version: "3.
|
|
47
|
+
version: "3.5.2",
|
|
48
48
|
redirectHosts: Object.freeze(["objects.githubusercontent.com", "release-assets.githubusercontent.com"]),
|
|
49
49
|
archives: Object.freeze({
|
|
50
|
-
"darwin-arm64": ["darwin_arm64.tar.gz", "
|
|
51
|
-
"darwin-x64": ["darwin_amd64.tar.gz", "
|
|
52
|
-
"linux-x64": ["linux_amd64.tar.gz", "
|
|
53
|
-
"linux-arm64": ["linux_arm64.tar.gz", "
|
|
54
|
-
"win32-x64": ["windows_amd64.zip", "
|
|
55
|
-
"win32-arm64": ["windows_arm64.zip", "
|
|
50
|
+
"darwin-arm64": ["darwin_arm64.tar.gz", "43d398b2fe3327a4a6436c26b2a17d3285ba80bf2f11a22d0796ea2a7fd2b78c", 7749520],
|
|
51
|
+
"darwin-x64": ["darwin_amd64.tar.gz", "aea28ebf461a4ea86c52b793c29311539d7123875354ed7411eae982fc82ec87", 8181300],
|
|
52
|
+
"linux-x64": ["linux_amd64.tar.gz", "106a43ca8f43a27c2c260a6787151c3be6d91c5ed3d5e454e6140527adf92e01", 8064598],
|
|
53
|
+
"linux-arm64": ["linux_arm64.tar.gz", "b0ba49cf5a7d5af436c9744a9a0d638b3f9ebb2a7321c8736d30e96305c2993c", 7472279],
|
|
54
|
+
"win32-x64": ["windows_amd64.zip", "ba56a36d37b2da85fcb1581bbdbed9ae1ee690e642aab8dbe1e18209bf264afe", 8272809],
|
|
55
|
+
"win32-arm64": ["windows_arm64.zip", "df5acc5dd6ee407d484074e570c77a79567c0b8dccf55e25ae25a644c1e7db5f", 7530282],
|
|
56
56
|
}),
|
|
57
57
|
});
|
|
58
58
|
/** The Engram version the bundled install publishes under runtime/. */
|
|
59
59
|
export const engramPinVersion = engram.version;
|
|
60
|
+
// uv for the /gentle:music offer (installMusicPlayer), never part of the default
|
|
61
|
+
// install: the astral-sh/uv release. Each sha256 and size comes from GitHub's
|
|
62
|
+
// asset digests; every archive was downloaded and re-hashed before pinning.
|
|
63
|
+
const uv = Object.freeze({
|
|
64
|
+
version: "0.13.6",
|
|
65
|
+
redirectHosts: engram.redirectHosts,
|
|
66
|
+
archives: Object.freeze({
|
|
67
|
+
"darwin-arm64": ["uv-aarch64-apple-darwin.tar.gz", "60fbd5c8a1ed681845eea28d9d622fcde03c51e91a43465b64a1e17db1ed3472", 14235670],
|
|
68
|
+
"darwin-x64": ["uv-x86_64-apple-darwin.tar.gz", "63ba17fdda55ad1a230d4b37fc6017ffa10c23688c83818bd3bfdf914c652a02", 17004934],
|
|
69
|
+
"linux-x64": ["uv-x86_64-unknown-linux-gnu.tar.gz", "f29c3e94ffabd7e6f3ba521dba169a9b7ec12100bf3257087f0f24f6123f3645", 16799024],
|
|
70
|
+
"linux-arm64": ["uv-aarch64-unknown-linux-gnu.tar.gz", "229e194255c7958d86d56e8790d969a6f74736592133f0dfd0076f754c642c1d", 15794886],
|
|
71
|
+
"win32-x64": ["uv-x86_64-pc-windows-msvc.zip", "a2a8820d5eea32f08ef467ef3b7f66883a9b076d5737a2fc2fbb5f458d1bb50f", 15239500],
|
|
72
|
+
"win32-arm64": ["uv-aarch64-pc-windows-msvc.zip", "c2426c0c8e28b53d8c440e47a18d96ad4db51e159866c0e81435b196d7f0081c", 15439263],
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
/** The uv version the music offer publishes under runtime/. */
|
|
76
|
+
export const uvPinVersion = uv.version;
|
|
60
77
|
|
|
61
78
|
/** Fixed allowlist; callers cannot supply download URLs, checksums or commands. */
|
|
62
79
|
export function artifactFor(name, platform, arch) {
|
|
@@ -77,6 +94,13 @@ export function artifactFor(name, platform, arch) {
|
|
|
77
94
|
url: `https://github.com/Gentleman-Programming/engram/releases/download/v${engram.version}/engram_${engram.version}_${suffix}`,
|
|
78
95
|
integrity: `sha256-${sha256}`, size, maxBytes: size, redirectHosts: engram.redirectHosts });
|
|
79
96
|
}
|
|
97
|
+
if (name === "uv") {
|
|
98
|
+
const pin = Object.hasOwn(uv.archives, `${platform}-${arch}`) ? uv.archives[`${platform}-${arch}`] : null;
|
|
99
|
+
if (!pin) throw new Error("Unsupported acquisition target");
|
|
100
|
+
const [asset, sha256, size] = pin;
|
|
101
|
+
return Object.freeze({ name, version: uv.version, platform, arch, url: `https://github.com/astral-sh/uv/releases/download/${uv.version}/${asset}`,
|
|
102
|
+
integrity: `sha256-${sha256}`, size, maxBytes: size, redirectHosts: uv.redirectHosts });
|
|
103
|
+
}
|
|
80
104
|
if (name === "go") {
|
|
81
105
|
const pin = Object.hasOwn(go.archives, `${platform}-${arch}`) ? go.archives[`${platform}-${arch}`] : null;
|
|
82
106
|
if (!pin) throw new Error("Unsupported acquisition target");
|