gentle-pi 4.2.0 → 4.4.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 +340 -138
- package/assets/agents/gentle-ai-explore.md +1 -0
- package/assets/agents/gentle-ai-verify.md +6 -4
- package/assets/agents/gentle-ai-worker.md +7 -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 +163 -0
- package/assets/orchestrator-delegation.md +25 -176
- package/assets/orchestrator-tracking.md +1 -1
- package/assets/orchestrator-verification.md +14 -8
- package/assets/orchestrator-writer.md +5 -1
- package/assets/orchestrator.md +11 -11
- package/bin/engram-guard +1 -1
- package/bin/gentle-shell.mjs +237 -139
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
- package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/assets/diagrams/agent-orchestration.svg +54 -15
- package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
- package/docs/assets/diagrams/measured-routing.svg +49 -0
- package/docs/assets/diagrams/native-review.svg +52 -12
- package/docs/assets/diagrams/odd-workflow.svg +128 -66
- package/docs/assets/diagrams/permission-modes.svg +51 -0
- package/docs/assets/features/audio-notifications.png +0 -0
- package/docs/assets/features/command-palette.png +0 -0
- package/docs/bundled-install.md +65 -30
- package/docs/delegated-verification.md +16 -8
- package/docs/gentle-agents-activity.md +16 -6
- package/docs/gentle-music.md +61 -0
- package/docs/gentle-shell-uninstall.md +8 -1
- package/docs/gentle-shell.md +80 -9
- package/docs/install-wizard.md +37 -6
- package/docs/permission-modes.md +8 -3
- package/docs/readme-reference.md +120 -28
- package/docs/session-profile-format.md +26 -0
- package/docs/voice.md +259 -0
- package/docs/yolo-mode.md +38 -10
- package/extensions/ask-user-choice.ts +4 -1
- package/extensions/codegraph-tools.ts +68 -6
- package/extensions/gentle-agents.ts +270 -48
- package/extensions/gentle-ai.ts +490 -113
- package/extensions/gentle-design.ts +158 -0
- package/extensions/gentle-herdr-activity.ts +2 -3
- package/extensions/gentle-music.ts +289 -0
- package/extensions/gentle-shell.ts +359 -85
- package/extensions/gentle-todo.ts +43 -9
- package/extensions/gentle-voice.ts +11 -0
- package/extensions/quiet-tools.ts +1 -1
- package/extensions/session-scratch.ts +384 -0
- package/extensions/skill-registry.ts +63 -8
- package/extensions/startup-banner.ts +2 -3
- package/lib/agent-home.ts +8 -3
- package/lib/agent-profiles.ts +26 -0
- package/lib/agents-config.ts +35 -0
- package/lib/agents-protocol.ts +60 -1
- package/lib/agents-runner.ts +107 -9
- package/lib/agents-session-transport.ts +101 -6
- package/lib/agents-view.ts +3 -1
- 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/bounded-writer-admission.ts +51 -0
- package/lib/clipboard-image.ts +147 -0
- package/lib/command-palette-catalog.ts +2 -4
- package/lib/commit-slice-signal.ts +100 -0
- package/lib/context-backstop.ts +120 -0
- package/lib/delivered-test-lint.ts +80 -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/destructive-command-guard.ts +120 -9
- package/lib/external-editor.ts +31 -0
- package/lib/gauge-theme.ts +146 -0
- package/lib/gentle-ai-binary.ts +28 -7
- package/lib/gentle-design.ts +513 -0
- package/lib/gentle-shell-bundled.ts +146 -36
- package/lib/gentle-shell-import-pi.ts +233 -0
- package/lib/gentle-shell-launcher.ts +154 -15
- package/lib/gentle-shell-startup-update.ts +61 -9
- package/lib/gentle-shell-uninstall.ts +51 -2
- package/lib/herdr-activity.ts +24 -0
- package/lib/image-attachments.ts +150 -0
- package/lib/inprocess-reviewer.ts +33 -2
- package/lib/nan-provider.ts +3 -0
- package/lib/native-review-cli.ts +20 -5
- 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-discovery.ts +5 -1
- package/lib/orchestrator-presence.ts +87 -8
- package/lib/orchestrator-scope.ts +31 -3
- package/lib/orchestrator-state.ts +11 -3
- package/lib/permission-mode-policy.ts +2 -2
- package/lib/provider-contract-bundle.ts +99 -53
- package/lib/review-candidate-view-owner.ts +29 -19
- package/lib/review-candidate-view.ts +36 -2
- package/lib/review-host-relay.ts +11 -2
- package/lib/review-reminder-receipt.ts +24 -1
- package/lib/review-risk-assessment.ts +43 -38
- package/lib/review-session-standing-permission.ts +3 -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 +30 -1
- package/lib/session-profile-persistence.ts +15 -7
- package/lib/session-scratch.ts +523 -0
- package/lib/session-worktree-registry.ts +7 -0
- package/lib/shell-bar.ts +29 -28
- package/lib/shell-changes-view.ts +80 -13
- package/lib/shell-changes.ts +81 -5
- package/lib/shell-gauge.ts +49 -6
- package/lib/shell-prompt.ts +4 -4
- package/lib/shell-sidebar-layout.ts +26 -6
- package/lib/shell-todo.ts +24 -5
- package/lib/shell-usage-view.ts +5 -1
- package/lib/shell-usage.ts +7 -7
- package/lib/verifier-admission.ts +30 -0
- package/lib/vim-editor-adapter.ts +11 -8
- 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/lib/yolo-temp-scratch.ts +69 -0
- package/package.json +1 -2
- package/runtime/gentle-ai-binary.mjs +28 -7
- package/runtime/gentle-shell-bundled.mjs +145 -35
- package/runtime/gentle-shell-import-pi.mjs +234 -0
- package/runtime/gentle-shell-launcher.mjs +152 -13
- package/runtime/gentle-shell-startup-update.mjs +61 -9
- package/runtime/gentle-shell-uninstall.mjs +51 -2
- package/runtime/native-review-cli.mjs +19 -4
- package/runtime/review-risk-assessment.mjs +43 -38
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/bundled-engram-migration.mjs +119 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/installer-downloads.mjs +7 -7
- package/scripts/installer-preflight.mjs +5 -3
- package/scripts/installer-server.mjs +14 -4
- package/scripts/installer-windows.mjs +37 -7
- package/scripts/main-channel.mjs +94 -19
- package/scripts/pi-update.mjs +34 -6
- package/scripts/run-test-suite.mjs +5 -0
- package/scripts/verify-package-files.mjs +43 -13
- package/scripts/voice-worker.cjs +476 -0
- package/skills/design/SKILL.md +110 -0
- package/skills/gentle-ai/SKILL.md +3 -3
- package/skills/work-unit-commits/SKILL.md +5 -3
- package/tests/agent-profiles.test.ts +24 -0
- package/tests/agent-risk-escalation.test.ts +37 -10
- 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 +485 -7
- 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 +142 -0
- package/tests/bounded-writer-admission.test.ts +11 -3
- package/tests/bundled-install.test.ts +3 -3
- package/tests/card-content-policy.test.ts +19 -9
- package/tests/child-safety.test.ts +39 -0
- package/tests/clipboard-image.test.ts +114 -0
- package/tests/codegraph-tools.test.ts +123 -2
- package/tests/command-palette.test.ts +15 -5
- package/tests/commit-slice-signal.test.ts +162 -0
- package/tests/context-backstop.test.ts +225 -0
- package/tests/delegate-for-reason-contract.test.ts +92 -24
- package/tests/delivered-test-lint.test.ts +105 -0
- 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/destructive-command-guard.test.ts +85 -1
- 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 +143 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
- package/tests/gauge-theme.test.ts +120 -0
- package/tests/gentle-agents.test.ts +645 -21
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-child-guards.test.ts +61 -0
- package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
- package/tests/gentle-ai-installer.test.ts +51 -51
- package/tests/gentle-ai.test.ts +292 -13
- package/tests/gentle-design.test.ts +1313 -0
- package/tests/gentle-music.test.ts +335 -0
- package/tests/gentle-shell-bin.test.ts +163 -0
- package/tests/gentle-shell-bundled.test.ts +392 -176
- package/tests/gentle-shell-import-pi.test.ts +262 -0
- package/tests/gentle-shell-launcher.test.ts +107 -2
- package/tests/gentle-shell-startup-update.test.ts +77 -1
- package/tests/gentle-shell-uninstall.test.ts +89 -0
- package/tests/gentle-shell.test.ts +225 -17
- package/tests/gentle-todo.test.ts +190 -0
- package/tests/headless-delivery-question.test.ts +58 -0
- package/tests/herdr-activity.test.ts +34 -1
- 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-posix-bootstrap.test.ts +8 -8
- package/tests/installer-preflight.test.ts +16 -0
- package/tests/installer-server.test.ts +10 -2
- package/tests/installer-windows-bootstrap.test.ts +94 -7
- package/tests/installer-windows-diagnostics.test.ts +111 -0
- package/tests/main-channel-upgrade.test.ts +232 -2
- package/tests/main-channel.test.ts +140 -21
- package/tests/nan-provider.test.ts +36 -0
- package/tests/native-review-capability-contract.test.ts +17 -4
- package/tests/native-review-parity.test.ts +23 -2
- package/tests/no-self-pause-contract.test.ts +5 -2
- package/tests/notification-audio-native.test.ts +128 -9
- package/tests/notification-customize.test.ts +17 -5
- package/tests/odd-integration.test.ts +5 -1
- package/tests/odd-routing-canonical-ratchet.test.ts +128 -162
- package/tests/odd-routing-contract.test.ts +115 -30
- package/tests/orchestrator-budget.test.ts +22 -8
- package/tests/orchestrator-discovery.test.ts +29 -0
- package/tests/orchestrator-presence.test.ts +100 -2
- package/tests/orchestrator-rdd-ownership.test.ts +3 -1
- package/tests/orchestrator-scope.test.ts +80 -0
- package/tests/orchestrator-state.test.ts +29 -0
- package/tests/orchestrator-status-publication-contract.test.ts +12 -8
- package/tests/package-files.test.ts +34 -0
- package/tests/package-manifest.test.ts +24 -20
- package/tests/permission-mode-policy.test.ts +10 -1
- package/tests/pi-update-launcher.test.ts +21 -2
- package/tests/pi-update.test.ts +99 -4
- package/tests/primary-session-prompt.test.ts +101 -0
- package/tests/provider-contract-bundle.test.ts +118 -0
- package/tests/provider-contract-mirror.test.ts +24 -0
- package/tests/quiet-tool-rendering.test.ts +23 -0
- package/tests/rdd-aware-verification-contract.test.ts +29 -11
- package/tests/rdd-status-line.test.ts +79 -0
- package/tests/review-agent-end-preflight.test.ts +6 -0
- package/tests/review-candidate-view.test.ts +49 -0
- package/tests/review-contract-prompt.test.ts +5 -2
- package/tests/review-controller-native-routing.test.ts +14 -1
- package/tests/review-controller.test.ts +1 -1
- package/tests/review-host-relay-routing.test.ts +147 -3
- 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 +258 -0
- package/tests/review-owner-identity.test.ts +132 -0
- package/tests/review-relay-transport-agent.test.ts +26 -0
- package/tests/review-reminder-receipt.test.ts +31 -1
- package/tests/review-risk-assessment.test.ts +163 -76
- package/tests/review-session-standing-permission-controller.test.ts +14 -1
- package/tests/review-session-standing-permission.test.ts +27 -1
- package/tests/run-test-suite.test.ts +15 -0
- package/tests/runtime-harness.mjs +16 -6
- package/tests/session-changes-shell.test.ts +1 -1
- 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 +23 -0
- package/tests/session-scratch.test.ts +1011 -0
- package/tests/session-subject-routing-contract.test.ts +2 -1
- package/tests/session-worktree-registry.test.ts +13 -1
- package/tests/shell-bar.test.ts +21 -2
- package/tests/shell-changes-view.test.ts +137 -1
- package/tests/shell-changes.test.ts +148 -1
- package/tests/shell-gauge.test.ts +42 -0
- package/tests/shell-prompt.test.ts +3 -3
- package/tests/shell-render-cache.test.ts +134 -1
- package/tests/shell-sidebar-layout.test.ts +50 -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/small-task-inline-contract.test.ts +37 -0
- package/tests/standalone-config-home.test.ts +324 -0
- package/tests/startup-banner.test.ts +5 -4
- package/tests/startup-home-repo.test.ts +34 -0
- package/tests/support/orchestrator-modules.ts +1 -0
- package/tests/task-size-routing-contract.test.ts +70 -10
- package/tests/test-scratch-contract.test.ts +102 -0
- package/tests/test-strength-contract.test.ts +36 -1
- package/tests/tool-batching-contract.test.ts +64 -0
- package/tests/verification-proportionality.test.ts +46 -0
- package/tests/verifier-admission.test.ts +154 -0
- package/tests/verify-agent-contract.test.ts +8 -8
- 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 +30 -2
- package/tests/writer-edit-surface-scope.test.ts +3 -0
- package/tests/writer-feature-document-admission.test.ts +156 -0
- package/tests/yolo-customize.test.ts +5 -1
- package/tests/yolo-mode.test.ts +19 -1
- package/tests/yolo-session-policy-capture.test.ts +104 -0
- package/tests/yolo-temp-scratch.test.ts +71 -0
- package/scripts/mirror-odd-routing.mjs +0 -242
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
|
@@ -6,6 +6,11 @@ export interface DestructiveCommandMatch {
|
|
|
6
6
|
kind: DestructionKind;
|
|
7
7
|
triggerIndex: number;
|
|
8
8
|
hardDeny: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Literal targets of a direct, top-level, unwrapped and unquoted recursive
|
|
11
|
+
* `rm` that runs before every other segment of the command; absent otherwise.
|
|
12
|
+
*/
|
|
13
|
+
removalTargets?: readonly string[];
|
|
9
14
|
}
|
|
10
15
|
interface Token {
|
|
11
16
|
value: string;
|
|
@@ -14,6 +19,10 @@ interface Token {
|
|
|
14
19
|
index: number;
|
|
15
20
|
}
|
|
16
21
|
const separators = new Set([";", "&&", "||", "|", "&", "\n", "(", ")"]);
|
|
22
|
+
// Separators after which the next segment starts only once this one finished.
|
|
23
|
+
// Pipes, background jobs and subshell parentheses run segments concurrently or
|
|
24
|
+
// out of textual order.
|
|
25
|
+
const SEQUENTIAL_SEPARATORS = new Set(["", ";", "&&", "||", "\n"]);
|
|
17
26
|
|
|
18
27
|
/** Literal, named heredocs are data, not executable shell segments; offsets are preserved. */
|
|
19
28
|
function blankHeredocs(command: string): string {
|
|
@@ -49,10 +58,14 @@ const WRAPPER_VALUE_OPTIONS: Record<string, readonly string[]> = {
|
|
|
49
58
|
xargs: ["-n", "-I", "-P", "--max-args", "--replace", "--max-procs"],
|
|
50
59
|
};
|
|
51
60
|
const WRAPPERS = ["env", "sudo", "command", "exec", "nohup", "timeout", "xargs"];
|
|
61
|
+
// gentle-pi#2285: unquoted reserved words that open a compound body or a
|
|
62
|
+
// condition are followed by a command in command position.
|
|
63
|
+
const RESERVED_WORDS = new Set(["then", "else", "elif", "do", "{", "!", "if", "while", "until"]);
|
|
52
64
|
|
|
53
|
-
/** Index of the executable token after assignments and familiar wrappers. */
|
|
65
|
+
/** Index of the executable token after reserved words, assignments and familiar wrappers. */
|
|
54
66
|
function headCursor(segment: readonly Token[]): number {
|
|
55
67
|
let cursor = 0;
|
|
68
|
+
while (cursor < segment.length && segment[cursor].raw === segment[cursor].value && RESERVED_WORDS.has(segment[cursor].value)) cursor++;
|
|
56
69
|
while (cursor < segment.length) {
|
|
57
70
|
const value = segment[cursor].value;
|
|
58
71
|
if (/^[A-Za-z_][A-Za-z_0-9]*=/.test(value)) {
|
|
@@ -85,29 +98,63 @@ const DATA_COMMANDS = new Set([
|
|
|
85
98
|
const INERT_COMMANDS = new Set(["cd", "pwd", "true", "false", "test", "[", "mkdir", "touch"]);
|
|
86
99
|
const HEREDOC_RE = /<<-?\s*(['"]?)([A-Za-z_]\w*)\1[^\n]*\n[\s\S]*?\n\2(?=\n|$)/g;
|
|
87
100
|
|
|
101
|
+
type Ranges = Array<readonly [number, number]>;
|
|
102
|
+
// A simple `$(...)`: no nested substitution, quoting, expansion or escapes in its body.
|
|
103
|
+
const SIMPLE_SUBSTITUTION_RE = /\$\(([^()'"`$\\]*)\)/g;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Replace each simple `$(...)` whose body is itself only data and inert
|
|
107
|
+
* commands with a same-length placeholder word, so offsets are preserved.
|
|
108
|
+
* Such a substitution runs nothing guarded; its output is only more words.
|
|
109
|
+
* Returns undefined when any substitution is not provably data, or when a
|
|
110
|
+
* heredoc could expand one in its body.
|
|
111
|
+
*/
|
|
112
|
+
function maskDataSubstitutions(command: string): { masked: string; ranges: Ranges } | undefined {
|
|
113
|
+
if (!command.includes("$(")) return { masked: command, ranges: [] };
|
|
114
|
+
if (command.includes("<<")) return undefined;
|
|
115
|
+
const ranges: Ranges = [];
|
|
116
|
+
let opaque = false;
|
|
117
|
+
const masked = command.replace(SIMPLE_SUBSTITUTION_RE, (raw: string, body: string, offset: number) => {
|
|
118
|
+
const inner = dataRanges(body);
|
|
119
|
+
if (inner === undefined) opaque = true;
|
|
120
|
+
else ranges.push(...inner.map(([from, to]) => [offset + 2 + from, offset + 2 + to] as const));
|
|
121
|
+
return "_".repeat(raw.length);
|
|
122
|
+
});
|
|
123
|
+
return opaque || masked.includes("$(") ? undefined : { masked, ranges };
|
|
124
|
+
}
|
|
125
|
+
|
|
88
126
|
/**
|
|
89
127
|
* Byte ranges of the command that are plain arguments of a known data command
|
|
90
128
|
* (gentle-shell#2174). A guarded or denied pattern matching only inside these
|
|
91
129
|
* ranges is prose, not an operation. Returns no ranges, keeping every lexical
|
|
92
|
-
* match, when the command is opaque: command
|
|
130
|
+
* match, when the command is opaque: a command substitution that is not
|
|
131
|
+
* itself only data commands, process substitution or backticks, an
|
|
93
132
|
* interpreter or remote shell anywhere, or quoting the tokenizer cannot
|
|
94
133
|
* account for.
|
|
95
134
|
*/
|
|
96
|
-
export function dataArgumentRanges(command: string):
|
|
97
|
-
|
|
135
|
+
export function dataArgumentRanges(command: string): Ranges {
|
|
136
|
+
return dataRanges(command) ?? [];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Undefined means opaque; an empty list means only inert commands. */
|
|
140
|
+
function dataRanges(original: string): Ranges | undefined {
|
|
141
|
+
const substitutions = maskDataSubstitutions(original);
|
|
142
|
+
if (substitutions === undefined) return undefined;
|
|
143
|
+
const command = substitutions.masked;
|
|
144
|
+
if (/`|[<>]\(/.test(command)) return undefined;
|
|
98
145
|
// A heredoc counts only when its `<<` starts an unquoted token of the raw
|
|
99
146
|
// command; quoted text such as `echo '<<x'` must not blank following lines.
|
|
100
147
|
const rawStarts = new Set(tokenize(command, false).filter((token) => token.raw.startsWith("<<")).map((token) => token.index));
|
|
101
148
|
const heredocs = [...command.matchAll(HEREDOC_RE)];
|
|
102
|
-
if (heredocs.some((heredoc) => !rawStarts.has(heredoc.index))) return
|
|
149
|
+
if (heredocs.some((heredoc) => !rawStarts.has(heredoc.index))) return undefined;
|
|
103
150
|
const blanked = blankHeredocs(command);
|
|
104
151
|
const tokens = tokenize(command);
|
|
105
152
|
const covered = new Uint8Array(blanked.length);
|
|
106
153
|
for (const token of tokens) covered.fill(1, token.index, token.index + token.raw.length);
|
|
107
154
|
for (let index = 0; index < blanked.length; index++) {
|
|
108
|
-
if (!covered[index] && !/\s/.test(blanked[index])) return
|
|
155
|
+
if (!covered[index] && !/\s/.test(blanked[index])) return undefined;
|
|
109
156
|
}
|
|
110
|
-
const ranges:
|
|
157
|
+
const ranges: Ranges = [...substitutions.ranges];
|
|
111
158
|
let start = 0;
|
|
112
159
|
for (let end = 0; end <= tokens.length; end++) {
|
|
113
160
|
if (end < tokens.length && !tokens[end].operator) continue;
|
|
@@ -120,10 +167,12 @@ export function dataArgumentRanges(command: string): Array<readonly [number, num
|
|
|
120
167
|
if (segment[0].value === "env") cursor++;
|
|
121
168
|
while (cursor < segment.length && /^[A-Za-z_][A-Za-z_0-9]*=/.test(segment[cursor].value)) cursor++;
|
|
122
169
|
const head = segment[cursor];
|
|
123
|
-
|
|
170
|
+
// A segment of plain assignments only (`f=$(ls)`) runs no command.
|
|
171
|
+
if (head === undefined && cursor > 0 && segment[0].value !== "env") continue;
|
|
172
|
+
if (head === undefined || head.value !== head.raw) return undefined;
|
|
124
173
|
const name = head.value;
|
|
125
174
|
if (INERT_COMMANDS.has(name)) continue;
|
|
126
|
-
if (!DATA_COMMANDS.has(name)) return
|
|
175
|
+
if (!DATA_COMMANDS.has(name)) return undefined;
|
|
127
176
|
ranges.push([head.index + head.raw.length, tokens[end]?.index ?? command.length]);
|
|
128
177
|
}
|
|
129
178
|
// A heredoc body belongs to the segment of its `<<` header, which the
|
|
@@ -148,12 +197,58 @@ function sqlDestroys(sql: string): boolean {
|
|
|
148
197
|
});
|
|
149
198
|
}
|
|
150
199
|
|
|
200
|
+
/** Operands of a plain `rm` invocation, or undefined when any argument is quoted, escaped or disables root protection. */
|
|
201
|
+
function removalTargets(args: readonly Token[]): string[] | undefined {
|
|
202
|
+
const targets: string[] = [];
|
|
203
|
+
let options = true;
|
|
204
|
+
for (const arg of args) {
|
|
205
|
+
if (arg.raw !== arg.value || arg.value === "--no-preserve-root") return undefined;
|
|
206
|
+
if (options && arg.value === "--") options = false;
|
|
207
|
+
else if (options && arg.value.startsWith("-")) continue;
|
|
208
|
+
else targets.push(arg.value);
|
|
209
|
+
}
|
|
210
|
+
return targets.length > 0 ? targets : undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Bodies of backtick command substitutions outside single quotes, with the
|
|
215
|
+
* backslash escapes that backticks remove, and the offset of each opening
|
|
216
|
+
* backtick. An unterminated substitution is a shell syntax error and runs
|
|
217
|
+
* nothing, so it yields no body. Heredoc bodies stay data, as elsewhere.
|
|
218
|
+
*/
|
|
219
|
+
function backtickBodies(command: string): Array<{ body: string; index: number }> {
|
|
220
|
+
const bodies: Array<{ body: string; index: number }> = [];
|
|
221
|
+
const text = blankHeredocs(command);
|
|
222
|
+
let quote: "'" | '"' | undefined;
|
|
223
|
+
for (let index = 0; index < text.length; index++) {
|
|
224
|
+
const char = text[index];
|
|
225
|
+
if (quote === "'") {
|
|
226
|
+
if (char === "'") quote = undefined;
|
|
227
|
+
} else if (char === "\\") {
|
|
228
|
+
index++;
|
|
229
|
+
} else if (char === "'" && quote === undefined) {
|
|
230
|
+
quote = "'";
|
|
231
|
+
} else if (char === '"') {
|
|
232
|
+
quote = quote === '"' ? undefined : '"';
|
|
233
|
+
} else if (char === "`") {
|
|
234
|
+
let end = index + 1;
|
|
235
|
+
while (end < text.length && text[end] !== "`") end += text[end] === "\\" ? 2 : 1;
|
|
236
|
+
if (end >= text.length) break;
|
|
237
|
+
bodies.push({ body: text.slice(index + 1, end).replace(/\\([\\$`])/g, "$1"), index });
|
|
238
|
+
index = end;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return bodies;
|
|
242
|
+
}
|
|
243
|
+
|
|
151
244
|
export function recognizeDestructiveCommands(command: string, depth = 0): DestructiveCommandMatch[] {
|
|
152
245
|
if (depth > 4) return [];
|
|
153
246
|
const tokens = tokenize(command);
|
|
154
247
|
const matches: DestructiveCommandMatch[] = [];
|
|
155
248
|
let previous: Token[] = [];
|
|
156
249
|
let previousSeparator = "";
|
|
250
|
+
// True while every earlier segment was a leading literal recursive `rm`.
|
|
251
|
+
let leadingRemovals = depth === 0;
|
|
157
252
|
let start = 0;
|
|
158
253
|
for (let end = 0; end <= tokens.length; end++) {
|
|
159
254
|
if (end < tokens.length && !tokens[end].operator) continue;
|
|
@@ -172,6 +267,11 @@ export function recognizeDestructiveCommands(command: string, depth = 0): Destru
|
|
|
172
267
|
matches.push(...recognizeDestructiveCommands(args[flag + 1], depth + 1).map((match) => ({ ...match, triggerIndex: head.index })));
|
|
173
268
|
}
|
|
174
269
|
}
|
|
270
|
+
// `eval` runs its arguments joined by spaces, like a literal `sh -c` payload.
|
|
271
|
+
if (name === "eval") {
|
|
272
|
+
const payload = (args[0] === "--" ? args.slice(1) : args).join(" ");
|
|
273
|
+
matches.push(...recognizeDestructiveCommands(payload, depth + 1).map((match) => ({ ...match, triggerIndex: head.index })));
|
|
274
|
+
}
|
|
175
275
|
if (["psql", "mysql", "mariadb", "sqlite3"].includes(name)) {
|
|
176
276
|
const payloads = args.map((arg) => arg.replace(/^(?:--command|--execute)=/, ""));
|
|
177
277
|
const heredoc = command.slice(head.index).match(/^[^\n]*<<-?\s*(['"]?)([A-Za-z_]\w*)\1[^\n]*\n([\s\S]*?)\n\2(?=\n|$)/);
|
|
@@ -184,6 +284,13 @@ export function recognizeDestructiveCommands(command: string, depth = 0): Destru
|
|
|
184
284
|
if (name === "rm" && args.some((arg) => /^-[^-]*[rR]/.test(arg) || arg === "--recursive")) {
|
|
185
285
|
const rootTarget = args.some((arg) => /^(?:\/(?:\*+)?$|\.{1,2}\/?$|~(?:\/|$)|\$\{?HOME\}?(?:\/|$))/.test(arg));
|
|
186
286
|
add(DESTRUCTION_KIND.FILESYSTEM, rootTarget);
|
|
287
|
+
const sequential = SEQUENTIAL_SEPARATORS.has(tokens[end]?.raw ?? "");
|
|
288
|
+
const targets = leadingRemovals && sequential && cursor === 0 && head.raw === "rm" ? removalTargets(segment.slice(1)) : undefined;
|
|
289
|
+
if (targets !== undefined) matches[matches.length - 1].removalTargets = targets;
|
|
290
|
+
else leadingRemovals = false;
|
|
291
|
+
} else if (segment.length > 0 || tokens[end]?.raw !== "\n") {
|
|
292
|
+
// Only blank lines may sit between leading removals.
|
|
293
|
+
leadingRemovals = false;
|
|
187
294
|
}
|
|
188
295
|
if (name === "find") {
|
|
189
296
|
if (args.includes("-delete")) add(DESTRUCTION_KIND.FILESYSTEM);
|
|
@@ -216,5 +323,9 @@ export function recognizeDestructiveCommands(command: string, depth = 0): Destru
|
|
|
216
323
|
previousSeparator = tokens[end]?.value ?? "";
|
|
217
324
|
start = end + 1;
|
|
218
325
|
}
|
|
326
|
+
// Backtick substitutions run like `$(...)`, also inside double quotes.
|
|
327
|
+
for (const { body, index } of backtickBodies(command)) {
|
|
328
|
+
matches.push(...recognizeDestructiveCommands(body, depth + 1).map((match) => ({ ...match, triggerIndex: index })));
|
|
329
|
+
}
|
|
219
330
|
return matches;
|
|
220
331
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export interface ExternalEditorHost {
|
|
4
|
+
stop(): void;
|
|
5
|
+
start(): void;
|
|
6
|
+
requestRender(force?: boolean): void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function openInExternalEditor(host: ExternalEditorHost, path: string, env: NodeJS.ProcessEnv = process.env, spawn: typeof spawnSync = spawnSync, cwd?: string): boolean {
|
|
10
|
+
const command = env.VISUAL || env.EDITOR;
|
|
11
|
+
if (!command) return false;
|
|
12
|
+
const [editor, ...editorArgs] = command.split(" ");
|
|
13
|
+
// Repository-controlled paths must never become shell syntax. Batch
|
|
14
|
+
// editors require cmd.exe, so fail closed rather than quoting for a shell.
|
|
15
|
+
if (process.platform === "win32" && /\.(cmd|bat)$/i.test(editor)) {
|
|
16
|
+
throw new Error("Could not open editor: Windows batch editors are not supported. Set $VISUAL or $EDITOR to a direct executable (.exe).");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
host.stop();
|
|
20
|
+
try {
|
|
21
|
+
const result = spawn(editor, [...editorArgs, path], { cwd, stdio: "inherit", shell: false });
|
|
22
|
+
if (result.error) throw result.error;
|
|
23
|
+
if (result.status !== 0) throw new Error(result.signal ? `terminated by ${result.signal}` : `exit status ${result.status}`);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
throw new Error(`Could not open editor: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
26
|
+
} finally {
|
|
27
|
+
host.start();
|
|
28
|
+
host.requestRender(true);
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Gauge theme roles from the installed theme source (issue #2222).
|
|
2
|
+
//
|
|
3
|
+
// Pi's theme schema is closed at every level (colors, export and the theme
|
|
4
|
+
// object itself reject unknown properties) and `theme.fg` throws on unknown
|
|
5
|
+
// tokens, so gauge roles cannot be theme colors or a top-level block. The
|
|
6
|
+
// one open map is `vars` (a free-form record of color values), so themes
|
|
7
|
+
// declare the roles there under reserved `gauge.` keys. Color values resolve
|
|
8
|
+
// like any Pi theme value: an existing theme token name (painted through
|
|
9
|
+
// `theme.fg`), or a direct color (hex, okhsl, or a 256-color index, painted
|
|
10
|
+
// as raw ANSI so the gauges alone recolor). Glyph values are literal single
|
|
11
|
+
// characters. Everything is optional and falls back per role to the gauge's
|
|
12
|
+
// historical roles, so themes without the keys — and invalid values — render
|
|
13
|
+
// exactly as before.
|
|
14
|
+
|
|
15
|
+
import { closeSync, constants, fstatSync, openSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
16
|
+
import { colorToRgb, parseColor } from "@earendil-works/pi-tui";
|
|
17
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
18
|
+
import type { GaugePaint } from "./shell-gauge.ts";
|
|
19
|
+
|
|
20
|
+
const MAX_BYTES = 256_000;
|
|
21
|
+
const REFERENCE = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
|
|
22
|
+
const COLOR_KEYS = ["ok", "warning", "error", "empty"] as const;
|
|
23
|
+
const GLYPH_KEYS = ["filled", "emptyGlyph"] as const;
|
|
24
|
+
// Reserved `vars` names that carry the gauge customization, e.g. "vars": { "gauge.ok": "#22c55e" }.
|
|
25
|
+
const gaugeVar = (key: string): string => `gauge.${key}`;
|
|
26
|
+
|
|
27
|
+
interface CacheEntry {
|
|
28
|
+
mtimeMs: number;
|
|
29
|
+
size: number;
|
|
30
|
+
paint: GaugePaint | undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// The bar and panel render many times a second, so a render pays at most one
|
|
34
|
+
// statSync per call; the file is opened and parsed only when the stat changes,
|
|
35
|
+
// and a failed resolve is cached like any other result so a malformed theme
|
|
36
|
+
// costs no repeated reads on the render path.
|
|
37
|
+
const cache = new Map<string, CacheEntry>();
|
|
38
|
+
|
|
39
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
40
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** One visible terminal column, single code point, no control characters or escape sequences. */
|
|
44
|
+
function isGlyph(value: unknown): value is string {
|
|
45
|
+
return typeof value === "string" && [...value].length === 1 && visibleWidth(value) === 1 && !/[\u0000-\u001f\u007f\x1b]/.test(value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// A color entry resolves like a Pi theme value: variable references expand
|
|
49
|
+
// first, then either an existing theme token keeps its name (so the theme's
|
|
50
|
+
// own resolved ANSI applies) or the value delegates to pi-tui's `parseColor`,
|
|
51
|
+
// the same grammar themes use everywhere else (3 or 6 digit hex, oklch,
|
|
52
|
+
// case-insensitive okhsl), and becomes raw foreground ANSI. Direct colors are
|
|
53
|
+
// what let a theme recolor only the gauges, exactly like the issue's
|
|
54
|
+
// green/yellow/red statusline use case.
|
|
55
|
+
function resolveGaugeColor(value: unknown, colors: Record<string, unknown>, vars: Record<string, unknown>): string | undefined {
|
|
56
|
+
const seen = new Set<unknown>();
|
|
57
|
+
while (typeof value === "string" && REFERENCE.test(value) && Object.hasOwn(vars, value)) {
|
|
58
|
+
if (seen.has(value)) return undefined;
|
|
59
|
+
seen.add(value);
|
|
60
|
+
value = vars[value];
|
|
61
|
+
}
|
|
62
|
+
if (typeof value === "string" && Object.hasOwn(colors, value)) return value;
|
|
63
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 255) return `\x1b[38;5;${value}m`;
|
|
64
|
+
if (typeof value === "string") {
|
|
65
|
+
try {
|
|
66
|
+
const { r, g, b } = colorToRgb(parseColor(value));
|
|
67
|
+
return `\x1b[38;2;${r};${g};${b}m`;
|
|
68
|
+
} catch {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Read only the installed theme source supplied by Pi; never fall back to the active theme. */
|
|
76
|
+
function readThemeSource(sourcePath: string): unknown {
|
|
77
|
+
const resolved = realpathSync(sourcePath);
|
|
78
|
+
const fd = openSync(resolved, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
79
|
+
let text: string;
|
|
80
|
+
try {
|
|
81
|
+
const stat = fstatSync(fd);
|
|
82
|
+
if (!stat.isFile() || stat.size > MAX_BYTES) throw new Error("Theme source unavailable or too large.");
|
|
83
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(readFileSync(fd));
|
|
84
|
+
} finally {
|
|
85
|
+
closeSync(fd);
|
|
86
|
+
}
|
|
87
|
+
return JSON.parse(text);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Gauge roles and glyphs declared by the theme source, or undefined when the
|
|
92
|
+
* theme declares none. Invalid entries are dropped individually, never
|
|
93
|
+
* throwing: a bad `gaugeWarning` keeps `warning` rather than disabling the rest.
|
|
94
|
+
*/
|
|
95
|
+
export function resolveGaugePaint(sourcePath: string): GaugePaint | undefined {
|
|
96
|
+
const document: unknown = readThemeSource(sourcePath);
|
|
97
|
+
if (!record(document) || !record(document.vars)) return undefined;
|
|
98
|
+
const vars = document.vars;
|
|
99
|
+
const hasGaugeVars = COLOR_KEYS.some((key) => gaugeVar(key) in vars) || GLYPH_KEYS.some((key) => gaugeVar(key) in vars);
|
|
100
|
+
if (!hasGaugeVars) return undefined;
|
|
101
|
+
const colors = record(document.colors) ? document.colors : {};
|
|
102
|
+
const paint: GaugePaint = {};
|
|
103
|
+
for (const key of COLOR_KEYS) {
|
|
104
|
+
const declared = vars[gaugeVar(key)];
|
|
105
|
+
if (declared === undefined) continue;
|
|
106
|
+
const value = resolveGaugeColor(declared, colors, vars);
|
|
107
|
+
if (value !== undefined) paint[key] = value;
|
|
108
|
+
}
|
|
109
|
+
for (const key of GLYPH_KEYS) {
|
|
110
|
+
const value = vars[gaugeVar(key)];
|
|
111
|
+
if (isGlyph(value)) paint[key] = value;
|
|
112
|
+
}
|
|
113
|
+
return paint;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Memoized `resolveGaugePaint`; undefined whenever the theme source is unavailable or invalid. */
|
|
117
|
+
export function gaugePaintFor(sourcePath: string | undefined): GaugePaint | undefined {
|
|
118
|
+
if (!sourcePath) return undefined;
|
|
119
|
+
let stat;
|
|
120
|
+
try {
|
|
121
|
+
stat = statSync(sourcePath);
|
|
122
|
+
} catch {
|
|
123
|
+
// A missing source stays uncached: recreating it must take effect.
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
const cached = cache.get(sourcePath);
|
|
127
|
+
if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) return cached.paint;
|
|
128
|
+
// Validate the stat before any open: a path that is not a regular file (a
|
|
129
|
+
// FIFO, a device) must never reach openSync, and an oversized file must
|
|
130
|
+
// never reach a read. Failures resolve to the historical roles.
|
|
131
|
+
let paint: GaugePaint | undefined;
|
|
132
|
+
if (stat.isFile() && stat.size <= MAX_BYTES) {
|
|
133
|
+
try {
|
|
134
|
+
paint = resolveGaugePaint(sourcePath);
|
|
135
|
+
} catch {
|
|
136
|
+
paint = undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
cache.set(sourcePath, { mtimeMs: stat.mtimeMs, size: stat.size, paint });
|
|
140
|
+
return paint;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Test hook: drop every cached theme source entry. */
|
|
144
|
+
export function clearGaugePaintCache(): void {
|
|
145
|
+
cache.clear();
|
|
146
|
+
}
|
package/lib/gentle-ai-binary.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
resolveGentleAiReleaseAsset,
|
|
15
15
|
} from "../scripts/gentle-ai-installer.mjs";
|
|
16
16
|
import { engramPinVersion } from "../scripts/installer-downloads.mjs";
|
|
17
|
-
import {
|
|
17
|
+
import { bundledPiConfigDir, bundledPiConfigParent, GENTLE_SHELL_CONFIG_HOME_ENV, resolveGentlePiConfigHome, runningBundledPrefix } from "./gentle-shell-bundled.ts";
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
19
19
|
|
|
20
20
|
export const GENTLE_AI_BINARY_MISSING_CODE = "package-local-binary-missing";
|
|
@@ -86,9 +86,11 @@ export function bundledGentleAiPaths(prefix: string, platform: string = process.
|
|
|
86
86
|
* (only when the pinned Engram is published), GENTLE_AI_NO_SELF_UPDATE=1 and
|
|
87
87
|
* GENTLE_PI_CONFIG_HOME (the parent of GENTLE_AI_PI_CONFIG_DIR)
|
|
88
88
|
* when this gentle-pi runs from the valid bundled install GENTLE_SHELL_PREFIX
|
|
89
|
-
* names (runningBundledPrefix).
|
|
90
|
-
*
|
|
91
|
-
*
|
|
89
|
+
* names (runningBundledPrefix). In the standalone launcher's isolated home
|
|
90
|
+
* (#2201) only the config pair, for the folder its marker names. Otherwise
|
|
91
|
+
* `env` itself, unchanged: a gentle-pi installed elsewhere (the user's own,
|
|
92
|
+
* started from the agent's shell, which inherits the variable) keeps its own
|
|
93
|
+
* Gentle AI.
|
|
92
94
|
*/
|
|
93
95
|
export function gentleAiIsolationEnv(
|
|
94
96
|
env: NodeJS.ProcessEnv,
|
|
@@ -96,7 +98,7 @@ export function gentleAiIsolationEnv(
|
|
|
96
98
|
): NodeJS.ProcessEnv {
|
|
97
99
|
const platform = options.platform ?? process.platform;
|
|
98
100
|
const prefix = runningBundledPrefix(env, { packageRoot: options.packageRoot, platform });
|
|
99
|
-
if (prefix === undefined) return env;
|
|
101
|
+
if (prefix === undefined) return standaloneGentleAiEnv(env, platform);
|
|
100
102
|
const paths = bundledGentleAiPaths(prefix, platform);
|
|
101
103
|
let engram = false;
|
|
102
104
|
try {
|
|
@@ -125,6 +127,23 @@ export function gentleAiIsolationEnv(
|
|
|
125
127
|
};
|
|
126
128
|
}
|
|
127
129
|
|
|
130
|
+
// #2201: the same config pair as a bundled install, for the folder the
|
|
131
|
+
// standalone launcher's marker names, when it decides the config home
|
|
132
|
+
// (resolveGentlePiConfigHome; an explicit GENTLE_PI_CONFIG_HOME keeps its
|
|
133
|
+
// existing meaning for Gentle AI). Gentle AI's own state stays where it was.
|
|
134
|
+
// The parent is given only for a folder named gentle-ai, the one Gentle AI
|
|
135
|
+
// 4.0.0 appends to it.
|
|
136
|
+
function standaloneGentleAiEnv(env: NodeJS.ProcessEnv, platform: string): NodeJS.ProcessEnv {
|
|
137
|
+
const marker = env[GENTLE_SHELL_CONFIG_HOME_ENV];
|
|
138
|
+
if (marker === undefined || resolveGentlePiConfigHome(env, homedir(), { platform }) !== marker) return env;
|
|
139
|
+
const path = platform === "win32" ? win32 : posix;
|
|
140
|
+
return {
|
|
141
|
+
...env,
|
|
142
|
+
GENTLE_AI_PI_CONFIG_DIR: marker,
|
|
143
|
+
...(path.basename(marker) === "gentle-ai" ? { GENTLE_PI_CONFIG_HOME: path.dirname(marker) } : {}),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
128
147
|
function sha256(value: Buffer): string {
|
|
129
148
|
return createHash("sha256").update(value).digest("hex");
|
|
130
149
|
}
|
|
@@ -136,7 +155,9 @@ function sha256(value: Buffer): string {
|
|
|
136
155
|
// 1. GENTLE_PI_GENTLE_AI_DEV_BINARY (session override, absolute path), then
|
|
137
156
|
// 2. the persistent registration file at
|
|
138
157
|
// <config home>/dev-binary.json (a bundled prefix's gentle-pi, else
|
|
139
|
-
// GENTLE_PI_CONFIG_HOME, else ~/.pi/gentle-ai
|
|
158
|
+
// GENTLE_PI_CONFIG_HOME, else ~/.pi/gentle-ai, never the standalone
|
|
159
|
+
// launcher's marker: the main channel writes it outside any session,
|
|
160
|
+
// #2201) with the strict
|
|
140
161
|
// shape {"schema":"gentle-pi.dev-binary/v1","path":"<absolute path>"}.
|
|
141
162
|
//
|
|
142
163
|
// The registration deliberately pins no digest: it is the unpinned field-test
|
|
@@ -194,7 +215,7 @@ function ambientDevBinaryEnvironment(): GentleAiDevBinaryEnvironment {
|
|
|
194
215
|
}
|
|
195
216
|
|
|
196
217
|
export function gentleAiDevBinaryRegistrationPath(environment: GentleAiDevBinaryEnvironment = ambientDevBinaryEnvironment()): string {
|
|
197
|
-
const configHome =
|
|
218
|
+
const configHome = resolveGentlePiConfigHome(environment.env, environment.home, { launcherMarker: false });
|
|
198
219
|
return join(configHome, "dev-binary.json");
|
|
199
220
|
}
|
|
200
221
|
|