gentle-pi 4.1.0 → 4.3.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 +352 -128
- 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/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 +9 -9
- package/bin/engram-guard +1 -1
- package/bin/gentle-shell.mjs +188 -35
- 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/auto-permission-answers.md +25 -0
- package/docs/auto-permission-evidence.md +39 -0
- package/docs/bundled-install.md +40 -8
- package/docs/delegated-verification.md +10 -8
- package/docs/gentle-agents-activity.md +11 -5
- package/docs/gentle-shell-uninstall.md +8 -1
- package/docs/gentle-shell.md +20 -12
- package/docs/install-wizard.md +17 -10
- package/docs/permission-modes.md +51 -6
- package/docs/readme-reference.md +155 -31
- package/docs/session-profile-format.md +39 -0
- package/docs/yolo-mode.md +21 -7
- package/extensions/ask-user-choice.ts +4 -0
- package/extensions/ask-user-question.ts +6 -1
- package/extensions/codegraph-tools.ts +68 -6
- package/extensions/gentle-agents.ts +146 -28
- package/extensions/gentle-ai.ts +380 -68
- package/extensions/gentle-herdr-activity.ts +2 -3
- package/extensions/gentle-shell.ts +53 -43
- package/extensions/gentle-todo.ts +62 -18
- package/extensions/quiet-tools.ts +1 -1
- package/extensions/session-scratch.ts +365 -0
- package/extensions/startup-banner.ts +2 -3
- package/lib/agent-home.ts +8 -3
- package/lib/agents-protocol.ts +3 -0
- package/lib/agents-runner.ts +55 -6
- package/lib/agents-view.ts +3 -1
- package/lib/auto-permission-answers.ts +133 -0
- package/lib/auto-permission-classifier.ts +215 -36
- package/lib/auto-permission-evidence.ts +88 -0
- package/lib/auto-permission-memory.ts +69 -0
- package/lib/auto-permission-runtime.ts +251 -35
- package/lib/auto-permission-tool-context.ts +61 -0
- package/lib/bounded-writer-admission.ts +51 -0
- package/lib/command-palette-catalog.ts +0 -4
- package/lib/commit-slice-signal.ts +100 -0
- package/lib/context-backstop.ts +84 -0
- package/lib/delivered-test-lint.ts +80 -0
- package/lib/destructive-command-guard.ts +215 -28
- package/lib/gauge-theme.ts +146 -0
- package/lib/gentle-ai-binary.ts +28 -7
- package/lib/gentle-shell-bundled.ts +62 -8
- package/lib/gentle-shell-import-pi.ts +233 -0
- package/lib/gentle-shell-launcher.ts +146 -12
- package/lib/gentle-shell-uninstall.ts +18 -1
- package/lib/herdr-activity.ts +24 -0
- package/lib/model-routing-authority.ts +6 -1
- package/lib/native-review-cli.ts +9 -1
- package/lib/orchestrator-discovery.ts +5 -1
- package/lib/orchestrator-presence.ts +9 -0
- package/lib/orchestrator-scope.ts +21 -3
- 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-risk-assessment.ts +43 -38
- package/lib/review-session-standing-permission.ts +3 -1
- package/lib/selection-engine.ts +4 -1
- package/lib/session-profile-freeze.ts +40 -16
- package/lib/session-profile-persistence.ts +187 -0
- package/lib/session-scratch.ts +474 -0
- package/lib/shell-bar.ts +25 -25
- package/lib/shell-changes-view.ts +80 -13
- package/lib/shell-changes.ts +2 -0
- package/lib/shell-gauge.ts +49 -6
- package/lib/shell-prompt.ts +6 -4
- package/lib/shell-todo.ts +39 -8
- package/lib/shell-usage-view.ts +5 -1
- package/lib/shell-usage.ts +7 -7
- package/lib/verifier-admission.ts +29 -0
- package/lib/vim-editor-adapter.ts +22 -4
- package/lib/yolo-temp-scratch.ts +69 -0
- package/package.json +5 -6
- package/runtime/gentle-ai-binary.mjs +28 -7
- package/runtime/gentle-shell-bundled.mjs +61 -7
- package/runtime/gentle-shell-import-pi.mjs +234 -0
- package/runtime/gentle-shell-launcher.mjs +144 -10
- package/runtime/gentle-shell-uninstall.mjs +18 -1
- package/runtime/native-review-cli.mjs +8 -0
- package/runtime/review-risk-assessment.mjs +43 -38
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/bundled-engram-migration.mjs +125 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/install-wizard-preview.mjs +2 -2
- package/scripts/installer-downloads.mjs +7 -7
- package/scripts/installer-preflight.mjs +1 -1
- package/scripts/installer-server.mjs +14 -4
- package/scripts/main-channel.mjs +3 -2
- package/scripts/pi-update.mjs +223 -0
- package/scripts/run-test-suite.mjs +5 -0
- package/scripts/verify-package-files.mjs +19 -13
- package/skills/gentle-ai/SKILL.md +3 -3
- package/skills/work-unit-commits/SKILL.md +5 -3
- package/tests/agent-risk-escalation.test.ts +37 -10
- package/tests/agents-runner.test.ts +260 -3
- package/tests/append-system-prompt-route.test.ts +18 -1
- package/tests/auto-mode-human-answers.test.ts +68 -0
- package/tests/auto-mode-invariants.test.ts +23 -5
- package/tests/auto-mode-sdk.test.ts +29 -2
- package/tests/auto-permission-answers.test.ts +170 -0
- package/tests/auto-permission-classifier.test.ts +353 -2
- package/tests/auto-permission-compound.test.ts +24 -0
- package/tests/auto-permission-evidence.test.ts +88 -0
- package/tests/auto-permission-memory.test.ts +46 -0
- package/tests/auto-permission-public-regressions.test.ts +111 -0
- package/tests/auto-permission-runtime.test.ts +270 -12
- package/tests/autonomous-guard.test.ts +208 -16
- 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/codegraph-tools.test.ts +123 -2
- package/tests/command-palette.test.ts +14 -4
- package/tests/commit-slice-signal.test.ts +162 -0
- package/tests/context-backstop.test.ts +159 -0
- package/tests/delegate-for-reason-contract.test.ts +91 -24
- package/tests/delivered-test-lint.test.ts +105 -0
- package/tests/destructive-command-guard.test.ts +85 -1
- package/tests/engram-safety.test.ts +35 -7
- 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 +443 -8
- 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-shell-bin.test.ts +180 -17
- package/tests/gentle-shell-bundled.test.ts +235 -38
- package/tests/gentle-shell-import-pi.test.ts +262 -0
- package/tests/gentle-shell-launcher.test.ts +97 -13
- package/tests/gentle-shell-uninstall.test.ts +30 -0
- package/tests/gentle-shell.test.ts +113 -14
- package/tests/gentle-todo.test.ts +258 -27
- package/tests/headless-delivery-question.test.ts +58 -0
- package/tests/herdr-activity.test.ts +34 -1
- package/tests/install-wizard.test.ts +2 -2
- package/tests/installer-posix-bootstrap.test.ts +8 -8
- package/tests/installer-preflight.test.ts +3 -3
- package/tests/installer-probes.test.ts +8 -8
- package/tests/installer-runner.test.ts +14 -13
- package/tests/installer-server.test.ts +13 -5
- package/tests/installer-windows-bootstrap.test.ts +33 -4
- package/tests/main-channel-upgrade.test.ts +7 -1
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-parity.test.ts +22 -1
- package/tests/no-self-pause-contract.test.ts +5 -2
- package/tests/odd-integration.test.ts +5 -1
- package/tests/odd-routing-canonical-ratchet.test.ts +127 -162
- package/tests/odd-routing-contract.test.ts +115 -30
- package/tests/odd-runtime-delegation-gate.test.ts +4 -1
- package/tests/orchestrator-budget.test.ts +19 -8
- package/tests/orchestrator-discovery.test.ts +29 -0
- package/tests/orchestrator-rdd-ownership.test.ts +3 -1
- package/tests/orchestrator-scope.test.ts +66 -0
- package/tests/orchestrator-status-publication-contract.test.ts +12 -8
- package/tests/package-manifest.test.ts +28 -24
- package/tests/pi-update-launcher.test.ts +106 -0
- package/tests/pi-update.test.ts +186 -0
- 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 +131 -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 +59 -2
- package/tests/review-loadout-rdd-off-sdk.test.ts +237 -0
- package/tests/review-owner-identity.test.ts +132 -0
- 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/selection-engine.test.ts +35 -5
- package/tests/session-changes-shell.test.ts +1 -1
- package/tests/session-profile-freeze.test.ts +37 -1
- package/tests/session-profile-persistence.test.ts +334 -0
- package/tests/session-scratch.test.ts +945 -0
- package/tests/session-subject-routing-contract.test.ts +2 -1
- package/tests/shell-bar.test.ts +7 -0
- package/tests/shell-changes-view.test.ts +137 -1
- package/tests/shell-changes.test.ts +18 -0
- package/tests/shell-gauge.test.ts +42 -0
- package/tests/shell-todo.test.ts +15 -12
- 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 +69 -9
- 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 +137 -0
- package/tests/verify-agent-contract.test.ts +8 -8
- package/tests/vim-editor-adapter.test.ts +41 -15
- package/tests/windows-hidden-processes.test.ts +5 -0
- package/tests/writer-edit-surface-scope.test.ts +3 -0
- package/tests/writer-feature-document-admission.test.ts +156 -0
- package/tests/yolo-mode.test.ts +24 -1
- 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
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import { formatImportPiReport, importPi, nodeImportPiFs } from "../lib/gentle-shell-import-pi.ts";
|
|
8
|
+
|
|
9
|
+
// `gentle-shell home import-pi` core: real temp directories, never the real
|
|
10
|
+
// ~/.pi or ~/.gentle-shell. Credential contents never appear in assertions.
|
|
11
|
+
|
|
12
|
+
const AUTH = JSON.stringify({ anthropic: { type: "oauth", refresh: "dummy-refresh", access: "dummy-access" } });
|
|
13
|
+
const MCP = JSON.stringify({ mcpServers: { demo: { command: "demo" } } }, null, 2);
|
|
14
|
+
|
|
15
|
+
function fixture(t: test.TestContext) {
|
|
16
|
+
const root = mkdtempSync(join(tmpdir(), "gentle-shell-import-pi-"));
|
|
17
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
18
|
+
const source = join(root, "pi-agent");
|
|
19
|
+
const target = join(root, "gentle-agent");
|
|
20
|
+
mkdirSync(source, { recursive: true });
|
|
21
|
+
mkdirSync(target, { recursive: true });
|
|
22
|
+
return { root, source, target };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function seedSource(source: string) {
|
|
26
|
+
writeFileSync(join(source, "auth.json"), AUTH, { mode: 0o644 });
|
|
27
|
+
chmodSync(join(source, "auth.json"), 0o644);
|
|
28
|
+
writeFileSync(join(source, "mcp.json"), MCP);
|
|
29
|
+
chmodSync(join(source, "mcp.json"), 0o640);
|
|
30
|
+
writeFileSync(join(source, "settings.json"), `${JSON.stringify({
|
|
31
|
+
defaultProvider: "anthropic",
|
|
32
|
+
defaultModel: "claude-opus-5-5",
|
|
33
|
+
defaultThinkingLevel: "high",
|
|
34
|
+
packages: ["npm:foreign"],
|
|
35
|
+
extensions: ["./x.ts"],
|
|
36
|
+
theme: "dark",
|
|
37
|
+
npmCommand: ["npm"],
|
|
38
|
+
}, null, 2)}\n`);
|
|
39
|
+
writeFileSync(join(source, "mcp-cache.json"), "{}");
|
|
40
|
+
writeFileSync(join(source, "mcp-npx-cache.json"), "{}");
|
|
41
|
+
writeFileSync(join(source, "AGENTS.md"), "# agents\n");
|
|
42
|
+
mkdirSync(join(source, "sessions"));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function snapshot(dir: string) {
|
|
46
|
+
return Object.fromEntries(readdirSync(dir).sort().map((name) => {
|
|
47
|
+
const stat = statSync(join(dir, name));
|
|
48
|
+
const hash = stat.isFile() ? createHash("sha256").update(readFileSync(join(dir, name))).digest("hex") : "dir";
|
|
49
|
+
return [name, { hash, mode: stat.mode, mtimeMs: stat.mtimeMs }];
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function statusOf(result: ReturnType<typeof importPi>, item: string) {
|
|
54
|
+
return result.items.find((entry) => entry.item === item)?.status;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const modeOf = (path: string) => statSync(path).mode & 0o777;
|
|
58
|
+
|
|
59
|
+
test("copies auth.json (0600) and mcp.json (source mode) only when absent in the target", (t) => {
|
|
60
|
+
const f = fixture(t);
|
|
61
|
+
seedSource(f.source);
|
|
62
|
+
const result = importPi({ source: f.source, target: f.target, dryRun: false });
|
|
63
|
+
assert.equal(result.failed, false);
|
|
64
|
+
assert.equal(statusOf(result, "auth.json"), "copied");
|
|
65
|
+
assert.equal(statusOf(result, "mcp.json"), "copied");
|
|
66
|
+
assert.ok(readFileSync(join(f.target, "auth.json"), "utf8") === AUTH, "auth.json bytes copied verbatim");
|
|
67
|
+
assert.ok(readFileSync(join(f.target, "mcp.json"), "utf8") === MCP);
|
|
68
|
+
if (process.platform !== "win32") {
|
|
69
|
+
assert.equal(modeOf(join(f.target, "auth.json")), 0o600);
|
|
70
|
+
assert.equal(modeOf(join(f.target, "mcp.json")), 0o640);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("never overwrites a target file that already exists and is idempotent", (t) => {
|
|
75
|
+
const f = fixture(t);
|
|
76
|
+
seedSource(f.source);
|
|
77
|
+
writeFileSync(join(f.target, "auth.json"), "{\"mine\":true}");
|
|
78
|
+
const first = importPi({ source: f.source, target: f.target, dryRun: false });
|
|
79
|
+
assert.equal(statusOf(first, "auth.json"), "present");
|
|
80
|
+
assert.ok(readFileSync(join(f.target, "auth.json"), "utf8") === "{\"mine\":true}", "existing target auth.json kept");
|
|
81
|
+
const before = snapshot(f.target);
|
|
82
|
+
const second = importPi({ source: f.source, target: f.target, dryRun: false });
|
|
83
|
+
assert.equal(second.failed, false);
|
|
84
|
+
assert.ok(second.items.every((entry) => entry.status !== "copied"), "a second run copies nothing");
|
|
85
|
+
assert.deepEqual(snapshot(f.target), before);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("never modifies the source: content, mode and mtime stay the same", (t) => {
|
|
89
|
+
const f = fixture(t);
|
|
90
|
+
seedSource(f.source);
|
|
91
|
+
const before = snapshot(f.source);
|
|
92
|
+
importPi({ source: f.source, target: f.target, dryRun: false });
|
|
93
|
+
assert.deepEqual(snapshot(f.source), before);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("never copies caches, AGENTS.md, sessions or any other file", (t) => {
|
|
97
|
+
const f = fixture(t);
|
|
98
|
+
seedSource(f.source);
|
|
99
|
+
importPi({ source: f.source, target: f.target, dryRun: false });
|
|
100
|
+
assert.deepEqual(readdirSync(f.target).sort(), ["auth.json", "mcp.json", "settings.json"]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("missing source files are reported as not found, not errors", (t) => {
|
|
104
|
+
const f = fixture(t);
|
|
105
|
+
const result = importPi({ source: f.source, target: f.target, dryRun: false });
|
|
106
|
+
assert.equal(result.failed, false);
|
|
107
|
+
assert.equal(statusOf(result, "auth.json"), "not found");
|
|
108
|
+
assert.equal(statusOf(result, "mcp.json"), "not found");
|
|
109
|
+
assert.equal(statusOf(result, "settings.json"), "not found");
|
|
110
|
+
assert.deepEqual(readdirSync(f.target), []);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("skips a symlinked or non-regular source file", { skip: process.platform === "win32" }, (t) => {
|
|
114
|
+
const f = fixture(t);
|
|
115
|
+
writeFileSync(join(f.root, "elsewhere.json"), AUTH);
|
|
116
|
+
symlinkSync(join(f.root, "elsewhere.json"), join(f.source, "auth.json"));
|
|
117
|
+
mkdirSync(join(f.source, "mcp.json"));
|
|
118
|
+
const result = importPi({ source: f.source, target: f.target, dryRun: false });
|
|
119
|
+
assert.equal(statusOf(result, "auth.json"), "skipped");
|
|
120
|
+
assert.equal(statusOf(result, "mcp.json"), "skipped");
|
|
121
|
+
assert.equal(existsSync(join(f.target, "auth.json")), false);
|
|
122
|
+
assert.equal(existsSync(join(f.target, "mcp.json")), false);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("settings: copies only the default provider/model/thinking keys absent in the target, preserving every target key and order", (t) => {
|
|
126
|
+
const f = fixture(t);
|
|
127
|
+
seedSource(f.source);
|
|
128
|
+
writeFileSync(join(f.target, "settings.json"), `${JSON.stringify({ tuiMode: "fullscreen", theme: "Gentleman-Cute", defaultModel: "kept-model", packages: ["npm:own"] }, null, 2)}\n`);
|
|
129
|
+
const result = importPi({ source: f.source, target: f.target, dryRun: false });
|
|
130
|
+
assert.equal(result.failed, false);
|
|
131
|
+
assert.equal(statusOf(result, "settings.json defaultProvider"), "copied");
|
|
132
|
+
assert.equal(statusOf(result, "settings.json defaultModel"), "present");
|
|
133
|
+
assert.equal(statusOf(result, "settings.json defaultThinkingLevel"), "copied");
|
|
134
|
+
const text = readFileSync(join(f.target, "settings.json"), "utf8");
|
|
135
|
+
assert.equal(text, `${JSON.stringify({
|
|
136
|
+
tuiMode: "fullscreen", theme: "Gentleman-Cute", defaultModel: "kept-model", packages: ["npm:own"],
|
|
137
|
+
defaultProvider: "anthropic", defaultThinkingLevel: "high",
|
|
138
|
+
}, null, 2)}\n`);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("settings: the target keeps its own indentation and trailing-newline style", (t) => {
|
|
142
|
+
const f = fixture(t);
|
|
143
|
+
seedSource(f.source);
|
|
144
|
+
writeFileSync(join(f.target, "settings.json"), `{\n\t"tuiMode": "fullscreen",\n\t"zeta": 1\n}`);
|
|
145
|
+
importPi({ source: f.source, target: f.target, dryRun: false });
|
|
146
|
+
assert.equal(readFileSync(join(f.target, "settings.json"), "utf8"), JSON.stringify({
|
|
147
|
+
tuiMode: "fullscreen", zeta: 1, defaultProvider: "anthropic", defaultModel: "claude-opus-5-5", defaultThinkingLevel: "high",
|
|
148
|
+
}, null, "\t"));
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("settings: an unreadable settings.json is reported per item while the other items still run", (t) => {
|
|
152
|
+
if (process.platform === "win32" || process.getuid?.() === 0) return t.skip("needs POSIX permissions as a non-root user");
|
|
153
|
+
const f = fixture(t);
|
|
154
|
+
seedSource(f.source);
|
|
155
|
+
const settings = join(f.target, "settings.json");
|
|
156
|
+
writeFileSync(settings, "{}\n");
|
|
157
|
+
chmodSync(settings, 0o000);
|
|
158
|
+
try {
|
|
159
|
+
const result = importPi({ source: f.source, target: f.target, dryRun: false });
|
|
160
|
+
assert.equal(statusOf(result, "auth.json"), "copied");
|
|
161
|
+
assert.equal(statusOf(result, "settings.json"), "failed");
|
|
162
|
+
assert.equal(result.failed, true);
|
|
163
|
+
assert.match(formatImportPiReport(result, { source: f.source, target: f.target, dryRun: false }).join("\n"), /settings\.json: failed/);
|
|
164
|
+
} finally {
|
|
165
|
+
chmodSync(settings, 0o600);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("a destination that cannot be classified is one failed item while the other items still run", (t) => {
|
|
170
|
+
const f = fixture(t);
|
|
171
|
+
seedSource(f.source);
|
|
172
|
+
const blocked = join(f.target, "auth.json");
|
|
173
|
+
const fs = { ...nodeImportPiFs, kind: (path: string) => {
|
|
174
|
+
if (path === blocked) throw Object.assign(new Error(`ENOTDIR: not a directory, lstat '${path}'`), { code: "ENOTDIR" });
|
|
175
|
+
return nodeImportPiFs.kind(path);
|
|
176
|
+
} };
|
|
177
|
+
for (const dryRun of [true, false]) {
|
|
178
|
+
const result = importPi({ source: f.source, target: f.target, dryRun, fs });
|
|
179
|
+
assert.equal(statusOf(result, "auth.json"), "failed", `dryRun=${dryRun}`);
|
|
180
|
+
assert.equal(statusOf(result, "mcp.json"), dryRun ? "would copy" : "copied");
|
|
181
|
+
assert.equal(result.failed, true);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("settings: a missing target settings.json gets only the allowlisted keys", (t) => {
|
|
186
|
+
const f = fixture(t);
|
|
187
|
+
seedSource(f.source);
|
|
188
|
+
importPi({ source: f.source, target: f.target, dryRun: false });
|
|
189
|
+
assert.deepEqual(JSON.parse(readFileSync(join(f.target, "settings.json"), "utf8")), {
|
|
190
|
+
defaultProvider: "anthropic", defaultModel: "claude-opus-5-5", defaultThinkingLevel: "high",
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("settings: nothing to add leaves the target settings.json untouched", (t) => {
|
|
195
|
+
const f = fixture(t);
|
|
196
|
+
writeFileSync(join(f.source, "settings.json"), JSON.stringify({ packages: ["npm:x"] }));
|
|
197
|
+
writeFileSync(join(f.target, "settings.json"), "{\"tuiMode\":\"fullscreen\"}");
|
|
198
|
+
const before = snapshot(f.target);
|
|
199
|
+
importPi({ source: f.source, target: f.target, dryRun: false });
|
|
200
|
+
assert.deepEqual(snapshot(f.target), before);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("settings: malformed source or target JSON is reported and nothing is written", (t) => {
|
|
204
|
+
const f = fixture(t);
|
|
205
|
+
writeFileSync(join(f.source, "settings.json"), "{ not json");
|
|
206
|
+
const badSource = importPi({ source: f.source, target: f.target, dryRun: false });
|
|
207
|
+
assert.equal(statusOf(badSource, "settings.json"), "skipped");
|
|
208
|
+
assert.equal(badSource.failed, false);
|
|
209
|
+
assert.equal(existsSync(join(f.target, "settings.json")), false);
|
|
210
|
+
|
|
211
|
+
writeFileSync(join(f.source, "settings.json"), JSON.stringify({ defaultProvider: "anthropic" }));
|
|
212
|
+
writeFileSync(join(f.target, "settings.json"), "[1, 2]");
|
|
213
|
+
const badTarget = importPi({ source: f.source, target: f.target, dryRun: false });
|
|
214
|
+
assert.equal(statusOf(badTarget, "settings.json"), "skipped");
|
|
215
|
+
assert.equal(readFileSync(join(f.target, "settings.json"), "utf8"), "[1, 2]");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("dry run reports the same plan and writes nothing, not even the target directory", (t) => {
|
|
219
|
+
const f = fixture(t);
|
|
220
|
+
seedSource(f.source);
|
|
221
|
+
const target = join(f.root, "not-created", "agent");
|
|
222
|
+
const result = importPi({ source: f.source, target, dryRun: true });
|
|
223
|
+
assert.equal(result.failed, false);
|
|
224
|
+
assert.equal(statusOf(result, "auth.json"), "would copy");
|
|
225
|
+
assert.equal(statusOf(result, "settings.json defaultProvider"), "would copy");
|
|
226
|
+
assert.equal(existsSync(join(f.root, "not-created")), false);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("a write failure is reported per item and keeps what was already copied", { skip: process.platform === "win32" }, (t) => {
|
|
230
|
+
const f = fixture(t);
|
|
231
|
+
seedSource(f.source);
|
|
232
|
+
// A read-only target makes every write fail; nothing is half-written.
|
|
233
|
+
writeFileSync(join(f.target, "settings.json"), "{}");
|
|
234
|
+
chmodSync(f.target, 0o500);
|
|
235
|
+
let result;
|
|
236
|
+
try {
|
|
237
|
+
result = importPi({ source: f.source, target: f.target, dryRun: false });
|
|
238
|
+
} finally {
|
|
239
|
+
chmodSync(f.target, 0o700); // before the fixture's own cleanup
|
|
240
|
+
}
|
|
241
|
+
assert.equal(result.failed, true);
|
|
242
|
+
assert.equal(statusOf(result, "auth.json"), "failed");
|
|
243
|
+
assert.equal(statusOf(result, "settings.json defaultProvider"), "failed");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("the report has one line per item and a summary, and never prints credential contents", (t) => {
|
|
247
|
+
const f = fixture(t);
|
|
248
|
+
seedSource(f.source);
|
|
249
|
+
const result = importPi({ source: f.source, target: f.target, dryRun: false });
|
|
250
|
+
const lines = formatImportPiReport(result, { source: f.source, target: f.target, dryRun: false });
|
|
251
|
+
const text = lines.join("\n");
|
|
252
|
+
assert.match(text, /auth\.json: copied \(0600\)/);
|
|
253
|
+
assert.match(text, /mcp\.json: copied/);
|
|
254
|
+
assert.match(text, /settings\.json defaultModel: copied/);
|
|
255
|
+
assert.match(text, /Imported 5 item\(s\)/);
|
|
256
|
+
assert.doesNotMatch(text, /dummy-refresh|dummy-access/);
|
|
257
|
+
|
|
258
|
+
const again = formatImportPiReport(importPi({ source: f.source, target: f.target, dryRun: true }), { source: f.source, target: f.target, dryRun: true });
|
|
259
|
+
assert.match(again.join("\n"), /auth\.json: already present, kept/);
|
|
260
|
+
assert.match(again.join("\n"), /Dry run/);
|
|
261
|
+
assert.match(again.join("\n"), /Nothing to import/);
|
|
262
|
+
});
|
|
@@ -5,7 +5,9 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import test from "node:test";
|
|
6
6
|
import { CHILD_PACKAGE_INJECTION_ENV, encodeChildPackageInjection, parseChildPackageInjection } from "../lib/child-package-injection.ts";
|
|
7
7
|
import {
|
|
8
|
+
GENTLE_AI_PI_EXCLUDE_PACKAGES_ENV,
|
|
8
9
|
MIN_PI_VERSION,
|
|
10
|
+
withLauncherPackagesExcluded,
|
|
9
11
|
MIN_SETUP_GENTLE_AI_VERSION,
|
|
10
12
|
PI_SUBCOMMANDS,
|
|
11
13
|
buildPiInvocation,
|
|
@@ -37,6 +39,7 @@ import {
|
|
|
37
39
|
userPiHome,
|
|
38
40
|
resolvePiRuntime,
|
|
39
41
|
restoreJsonField,
|
|
42
|
+
separateHomeHint,
|
|
40
43
|
settingsDeclareGentlePi,
|
|
41
44
|
shellQuote,
|
|
42
45
|
type PackageJsonPeerShape,
|
|
@@ -66,6 +69,20 @@ test("parseLauncherArgs returns all-false defaults for an empty argv", () => {
|
|
|
66
69
|
});
|
|
67
70
|
});
|
|
68
71
|
|
|
72
|
+
test("parseLauncherArgs reserves explicit memory recovery without consuming prior commands or escaped prompts", () => {
|
|
73
|
+
const memory = parseLauncherArgs(["memory", "migrate", "--confirm-quiescent"]);
|
|
74
|
+
assert.equal(memory.command, "memory");
|
|
75
|
+
assert.deepEqual(memory.commandArgs, ["migrate", "--confirm-quiescent"]);
|
|
76
|
+
assert.deepEqual(memory.passthrough, []);
|
|
77
|
+
for (const command of ["home", "upgrade", "self-uninstall"] as const) {
|
|
78
|
+
const parsed = parseLauncherArgs([command, "--help"]);
|
|
79
|
+
assert.equal(parsed.command, command);
|
|
80
|
+
assert.deepEqual(parsed.commandArgs, ["--help"]);
|
|
81
|
+
}
|
|
82
|
+
assert.deepEqual(parseLauncherArgs(["--", "memory"]).passthrough, ["memory"]);
|
|
83
|
+
assert.match(helpText(), /gentle-shell memory migrate --confirm-quiescent/);
|
|
84
|
+
});
|
|
85
|
+
|
|
69
86
|
test("parseLauncherArgs captures a --package-root value from the next argument", () => {
|
|
70
87
|
assert.equal(parseLauncherArgs(["--package-root", "/custom/root"]).packageRoot, "/custom/root");
|
|
71
88
|
});
|
|
@@ -205,6 +222,40 @@ test("parseLauncherArgs treats home as a plain passthrough token when it is not
|
|
|
205
222
|
assert.deepEqual(parsed.passthrough, ["home"]);
|
|
206
223
|
});
|
|
207
224
|
|
|
225
|
+
test("parseLauncherArgs accepts home selectors ahead of home import-pi only", () => {
|
|
226
|
+
const link = parseLauncherArgs(["--link", "home", "import-pi"]);
|
|
227
|
+
assert.equal(link.link, true);
|
|
228
|
+
assert.equal(link.command, "home");
|
|
229
|
+
assert.deepEqual(link.commandArgs, ["import-pi"]);
|
|
230
|
+
assert.deepEqual(link.passthrough, []);
|
|
231
|
+
const path = parseLauncherArgs(["--home", "/x", "home", "import-pi", "--dry-run"]);
|
|
232
|
+
assert.equal(path.home, "/x");
|
|
233
|
+
assert.equal(path.command, "home");
|
|
234
|
+
assert.deepEqual(path.commandArgs, ["import-pi", "--dry-run"]);
|
|
235
|
+
const first = parseLauncherArgs(["home", "import-pi", "--dry-run"]);
|
|
236
|
+
assert.equal(first.command, "home");
|
|
237
|
+
assert.deepEqual(first.commandArgs, ["import-pi", "--dry-run"]);
|
|
238
|
+
// Anything else after a selector stays pi's, as before.
|
|
239
|
+
assert.deepEqual(parseLauncherArgs(["--link", "home", "link"]).passthrough, ["home", "link"]);
|
|
240
|
+
assert.deepEqual(parseLauncherArgs(["-p", "home", "import-pi"]).passthrough, ["-p", "home", "import-pi"]);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("help text documents gentle-shell home import-pi", () => {
|
|
244
|
+
const text = helpText();
|
|
245
|
+
assert.ok(text.includes(" gentle-shell [home selectors] home import-pi [--dry-run]"));
|
|
246
|
+
assert.match(text, /import-pi\s+Copy your pi sign-ins \(auth\.json\) and MCP servers \(mcp\.json\)/);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("separateHomeHint mentions home import-pi only when the user's pi home has sign-ins", () => {
|
|
250
|
+
const isolated: ResolvedHome = { mode: "isolated", dir: "/h/.gentle-shell/agent", source: "default" };
|
|
251
|
+
const without = separateHomeHint(isolated, { piSignIns: false });
|
|
252
|
+
assert.equal(without, "gentle-shell: using a separate home at /h/.gentle-shell/agent. Run 'gentle-shell --link' to reuse your pi sign-ins and chats.");
|
|
253
|
+
const withSignIns = separateHomeHint(isolated, { piSignIns: true });
|
|
254
|
+
assert.match(withSignIns, /Run 'gentle-shell --link' to reuse your pi sign-ins and chats, or 'gentle-shell home import-pi' to copy your sign-ins and MCP servers into this home\.$/);
|
|
255
|
+
const path = separateHomeHint({ mode: "path", dir: "/custom", source: "flag" }, { piSignIns: true });
|
|
256
|
+
assert.match(path, /'gentle-shell --home \/custom home import-pi'/);
|
|
257
|
+
});
|
|
258
|
+
|
|
208
259
|
// --- setup subcommand ----------------------------------------------------
|
|
209
260
|
//
|
|
210
261
|
// Unlike `home`, `setup` is not restricted to argv[0]: it accepts the home
|
|
@@ -293,6 +344,19 @@ test("parseLauncherArgs recognises a pi subcommand as the first token after --",
|
|
|
293
344
|
assert.deepEqual(parsed.passthrough, ["install", "npm:x"]);
|
|
294
345
|
});
|
|
295
346
|
|
|
347
|
+
test("parseLauncherArgs keeps a separator that follows a pi subcommand (#2140)", () => {
|
|
348
|
+
const parsed = parseLauncherArgs(["mcp", "add", "demo", "--", "npx", "demo-server"]);
|
|
349
|
+
assert.equal(parsed.piSubcommand, "mcp");
|
|
350
|
+
assert.deepEqual(parsed.passthrough, ["mcp", "add", "demo", "--", "npx", "demo-server"]);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test("parseLauncherArgs still consumes a later separator when no pi subcommand started", () => {
|
|
354
|
+
const parsed = parseLauncherArgs(["hello", "--", "--link"]);
|
|
355
|
+
assert.equal(parsed.piSubcommand, undefined);
|
|
356
|
+
assert.equal(parsed.link, false);
|
|
357
|
+
assert.deepEqual(parsed.passthrough, ["hello", "--link"]);
|
|
358
|
+
});
|
|
359
|
+
|
|
296
360
|
test("parseLauncherArgs errors when --link is combined with --isolated", () => {
|
|
297
361
|
const parsed = parseLauncherArgs(["--link", "--isolated"]);
|
|
298
362
|
assert.match(parsed.error ?? "", /--link/);
|
|
@@ -683,9 +747,10 @@ test("checkPeerVersionPin passes for a matching pin", () => {
|
|
|
683
747
|
assert.deepEqual(result, { ok: true, pinned: ">=0.85.1" });
|
|
684
748
|
});
|
|
685
749
|
|
|
686
|
-
test("Pi
|
|
750
|
+
test("Pi 1.2.0 baseline and host peers follow the open development range policy", () => {
|
|
687
751
|
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
688
|
-
assert.equal(MIN_PI_VERSION, "
|
|
752
|
+
assert.equal(MIN_PI_VERSION, "1.2.0");
|
|
753
|
+
assert.equal(pkg.peerDependencies["@earendil-works/pi-coding-agent"], ">=1.2.0");
|
|
689
754
|
assert.equal(pkg.engines.node, ">=22.19.0");
|
|
690
755
|
for (const name of ["@earendil-works/pi-ai", "@earendil-works/pi-tui"]) {
|
|
691
756
|
assert.equal(pkg.peerDependencies[name], "*");
|
|
@@ -695,34 +760,38 @@ test("Pi 0.99.1 baseline and host peers follow the open development range policy
|
|
|
695
760
|
// Development ranges are policy specifiers; their floor never drops below
|
|
696
761
|
// the runtime minimum the launcher enforces.
|
|
697
762
|
for (const name of ["@earendil-works/pi-coding-agent", "@earendil-works/pi-ai", "@earendil-works/pi-tui"]) {
|
|
698
|
-
assert.equal(pkg.devDependencies[name], ">=1.
|
|
763
|
+
assert.equal(pkg.devDependencies[name], ">=1.2.0", name);
|
|
699
764
|
assert.equal(checkPiVersion(pkg.devDependencies[name].slice(2)).ok, true, name);
|
|
700
765
|
}
|
|
701
766
|
});
|
|
702
767
|
|
|
703
768
|
test("checkPiVersion accepts a version equal to the minimum", () => {
|
|
704
|
-
assert.deepEqual(checkPiVersion("
|
|
769
|
+
assert.deepEqual(checkPiVersion("1.2.0"), { ok: true, version: "1.2.0" });
|
|
705
770
|
});
|
|
706
771
|
|
|
707
772
|
test("checkPiVersion accepts a version above the minimum", () => {
|
|
708
|
-
|
|
773
|
+
for (const version of ["1.2.1", "1.3.0", "2.0.0"]) {
|
|
774
|
+
assert.deepEqual(checkPiVersion(version), { ok: true, version });
|
|
775
|
+
}
|
|
709
776
|
});
|
|
710
777
|
|
|
711
778
|
test("checkPiVersion accepts a v-prefixed version", () => {
|
|
712
|
-
assert.deepEqual(checkPiVersion("
|
|
779
|
+
assert.deepEqual(checkPiVersion("v1.2.0"), { ok: true, version: "1.2.0" });
|
|
713
780
|
});
|
|
714
781
|
|
|
715
782
|
test("checkPiVersion accepts a prerelease suffix at the minimum", () => {
|
|
716
|
-
assert.deepEqual(checkPiVersion("pi version
|
|
783
|
+
assert.deepEqual(checkPiVersion("pi version 1.2.0-rc.2"), { ok: true, version: "1.2.0" });
|
|
717
784
|
});
|
|
718
785
|
|
|
719
786
|
test("checkPiVersion rejects a version below the minimum and names both versions", () => {
|
|
720
|
-
const result = checkPiVersion("
|
|
787
|
+
const result = checkPiVersion("1.1.0");
|
|
721
788
|
assert.equal(result.ok, false);
|
|
722
789
|
if (result.ok) throw new Error("expected a failing result");
|
|
723
|
-
assert.equal(result.version, "
|
|
724
|
-
assert.match(result.message, /
|
|
725
|
-
assert.match(result.message, /
|
|
790
|
+
assert.equal(result.version, "1.1.0");
|
|
791
|
+
assert.match(result.message, /1\.1\.0/);
|
|
792
|
+
assert.match(result.message, /1\.2\.0/);
|
|
793
|
+
assert.equal(checkPiVersion("1.0.0").ok, false);
|
|
794
|
+
assert.equal(checkPiVersion("0.99.1").ok, false);
|
|
726
795
|
});
|
|
727
796
|
|
|
728
797
|
test("checkPiVersion rejects a prerelease below the minimum", () => {
|
|
@@ -736,7 +805,7 @@ test("checkPiVersion reports unparsable output with the raw text and the minimum
|
|
|
736
805
|
if (result.ok) throw new Error("expected a failing result");
|
|
737
806
|
assert.equal(result.version, undefined);
|
|
738
807
|
assert.match(result.message, /not a version/);
|
|
739
|
-
assert.match(result.message, /
|
|
808
|
+
assert.match(result.message, /1\.2\.0/);
|
|
740
809
|
});
|
|
741
810
|
|
|
742
811
|
test("checkPiVersion accepts a custom minimum", () => {
|
|
@@ -2015,7 +2084,7 @@ test("the isolated-home bootstrap creates ~/.gentle-shell as 0700 under umask 00
|
|
|
2015
2084
|
const root = mkdtempSync(join(tmpdir(), "gentle-shell-umask-"));
|
|
2016
2085
|
try {
|
|
2017
2086
|
const piScript = join(root, "fake-pi.mjs");
|
|
2018
|
-
writeFileSync(piScript, "#!/usr/bin/env node\nif (process.argv.includes('--version')) console.log('
|
|
2087
|
+
writeFileSync(piScript, "#!/usr/bin/env node\nif (process.argv.includes('--version')) console.log('1.2.0');\n");
|
|
2019
2088
|
chmodSync(piScript, 0o755);
|
|
2020
2089
|
const binPath = fileURLToPath(new URL("../bin/gentle-shell.mjs", import.meta.url));
|
|
2021
2090
|
const env = { HOME: root, USERPROFILE: root, PATH: process.env.PATH ?? "/usr/bin:/bin", GENTLE_SHELL_PI: piScript, GENTLE_SHELL_NO_AUTO_SETUP: "1" };
|
|
@@ -2027,3 +2096,18 @@ test("the isolated-home bootstrap creates ~/.gentle-shell as 0700 under umask 00
|
|
|
2027
2096
|
rmSync(root, { recursive: true, force: true });
|
|
2028
2097
|
}
|
|
2029
2098
|
});
|
|
2099
|
+
|
|
2100
|
+
// #2143: Gentle AI installs npm:gentle-pi into the home only for the launcher
|
|
2101
|
+
// to remove it again; a Gentle AI with package exclusion (gentle-ai#5503)
|
|
2102
|
+
// skips it, and an older one ignores the variable.
|
|
2103
|
+
test("withLauncherPackagesExcluded asks Gentle AI to skip npm:gentle-pi, keeping a caller's own exclusions", () => {
|
|
2104
|
+
assert.equal(GENTLE_AI_PI_EXCLUDE_PACKAGES_ENV, "GENTLE_AI_PI_EXCLUDE_PACKAGES");
|
|
2105
|
+
assert.equal(withLauncherPackagesExcluded({ PATH: "/bin" }).GENTLE_AI_PI_EXCLUDE_PACKAGES, "npm:gentle-pi");
|
|
2106
|
+
assert.equal(withLauncherPackagesExcluded({ GENTLE_AI_PI_EXCLUDE_PACKAGES: "" }).GENTLE_AI_PI_EXCLUDE_PACKAGES, "npm:gentle-pi");
|
|
2107
|
+
assert.equal(withLauncherPackagesExcluded({ GENTLE_AI_PI_EXCLUDE_PACKAGES: "npm:pi-btw" }).GENTLE_AI_PI_EXCLUDE_PACKAGES, "npm:pi-btw,npm:gentle-pi");
|
|
2108
|
+
assert.equal(withLauncherPackagesExcluded({ GENTLE_AI_PI_EXCLUDE_PACKAGES: "npm:gentle-pi, npm:pi-btw" }).GENTLE_AI_PI_EXCLUDE_PACKAGES, "npm:gentle-pi,npm:pi-btw");
|
|
2109
|
+
const input = { PATH: "/bin" };
|
|
2110
|
+
assert.equal(withLauncherPackagesExcluded(input).PATH, "/bin");
|
|
2111
|
+
assert.deepEqual(input, { PATH: "/bin" }, "the caller's env is not mutated");
|
|
2112
|
+
});
|
|
2113
|
+
|
|
@@ -976,3 +976,33 @@ test("bundled on Windows: the user Path entry goes first, the running node.exe,
|
|
|
976
976
|
assert.equal(existsSync(join(w.shared.agent, "auth.json")), true);
|
|
977
977
|
} finally { w.cleanup(); }
|
|
978
978
|
});
|
|
979
|
+
|
|
980
|
+
// #2201: the standalone isolated home keeps its config in ~/.gentle-shell/gentle-ai.
|
|
981
|
+
test("Gentle Shell's own config folder next to the isolated home is removed; the shared config home keeps its rule", async () => {
|
|
982
|
+
const w = world();
|
|
983
|
+
try {
|
|
984
|
+
const own = join(w.shell, "gentle-ai");
|
|
985
|
+
write(join(own, "profiles.json"));
|
|
986
|
+
write(join(own, "persona.json"));
|
|
987
|
+
assert.equal(await w.uninstall(["--dry-run"]), 0);
|
|
988
|
+
assert.ok(section(w.out, "Remove Gentle Shell data:").some((line) => line.includes(own)), w.out.join("\n"));
|
|
989
|
+
assert.equal(await w.uninstall(["--yes"]), 0);
|
|
990
|
+
assert.equal(existsSync(own), false);
|
|
991
|
+
assert.equal(existsSync(w.shell), false);
|
|
992
|
+
for (const name of ["profiles.json", "persona.json"]) assert.equal(existsSync(join(w.config, name)), true, name);
|
|
993
|
+
} finally { w.cleanup(); }
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
test("the gentle-ai folder next to a custom GENTLE_SHELL_HOME is kept: it may not be Gentle Shell's", async () => {
|
|
997
|
+
const w = world();
|
|
998
|
+
try {
|
|
999
|
+
const custom = join(w.root, "elsewhere", "agent");
|
|
1000
|
+
const sibling = join(w.root, "elsewhere", "gentle-ai");
|
|
1001
|
+
write(join(custom, ".gentle-shell-home"));
|
|
1002
|
+
write(join(sibling, "notes.json"));
|
|
1003
|
+
assert.equal(await w.uninstall(["--yes"], { env: { GENTLE_SHELL_HOME: custom } }), 0);
|
|
1004
|
+
assert.equal(existsSync(custom), false);
|
|
1005
|
+
assert.equal(existsSync(join(sibling, "notes.json")), true);
|
|
1006
|
+
assert.ok(section(w.out, "Kept:").some((line) => line.includes(sibling)), w.out.join("\n"));
|
|
1007
|
+
} finally { w.cleanup(); }
|
|
1008
|
+
});
|