pennyrouter 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/package.json +1 -2
- package/src/cli.js +60 -28
- package/src/launch.js +54 -4
- package/src/launch.test.js +18 -5
- package/claude-plugin/.claude-plugin/plugin.json +0 -8
- package/claude-plugin/skills/penny/SKILL.md +0 -8
package/README.md
CHANGED
|
@@ -22,8 +22,9 @@ npx pennyrouter status
|
|
|
22
22
|
Installation opens a browser and requires login to the PennyRouter account that should own
|
|
23
23
|
the new machine key. CLI installs no longer create anonymous ghost accounts.
|
|
24
24
|
|
|
25
|
-
With no `--harness` flag, the interactive installer shows
|
|
26
|
-
|
|
25
|
+
With no `--harness` flag, the interactive installer shows Claude Code and Codex. Detected
|
|
26
|
+
tools start checked. The additional legacy integrations remain available through an explicit
|
|
27
|
+
`--harness <id>` selection or `--all`.
|
|
27
28
|
|
|
28
29
|
When Claude Code is selected, the installer also offers an optional Claude Pro/Max
|
|
29
30
|
subscription-auth step. Users can skip it during install and enable it later with
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pennyrouter",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Install and manage PennyRouter local coding-agent integrations.",
|
|
5
5
|
"homepage": "https://pennyrouter.com",
|
|
6
6
|
"bugs": {
|
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
"files": [
|
|
21
21
|
"bin",
|
|
22
22
|
"src",
|
|
23
|
-
"claude-plugin",
|
|
24
23
|
"README.md"
|
|
25
24
|
],
|
|
26
25
|
"scripts": {
|
package/src/cli.js
CHANGED
|
@@ -59,6 +59,7 @@ const HARNESS_BY_ID = {
|
|
|
59
59
|
};
|
|
60
60
|
|
|
61
61
|
const SUPPORTED_HARNESS_IDS = Object.keys(HARNESS_BY_ID).join(",");
|
|
62
|
+
const DEFAULT_INSTALL_HARNESS_IDS = ["claude-code", "codex"];
|
|
62
63
|
|
|
63
64
|
export async function main(argv = process.argv.slice(2)) {
|
|
64
65
|
const { command, flags } = parseArgs(argv);
|
|
@@ -375,9 +376,9 @@ async function install(flags) {
|
|
|
375
376
|
console.log("");
|
|
376
377
|
|
|
377
378
|
const manifest = await loadManifest();
|
|
378
|
-
const defaultRouting = await chooseDefaultRouting(flags, selected, manifest);
|
|
379
379
|
|
|
380
380
|
if (flags.dryRun) {
|
|
381
|
+
const defaultRouting = await chooseDefaultRouting(flags, selected, manifest);
|
|
381
382
|
for (const harness of selected) {
|
|
382
383
|
const explicit = ["claude-code", "codex"].includes(harness.id) && process.platform !== "win32";
|
|
383
384
|
const command = harness.id === "claude-code" ? "penny claude" : "penny codex";
|
|
@@ -411,6 +412,25 @@ async function install(flags) {
|
|
|
411
412
|
console.log("");
|
|
412
413
|
|
|
413
414
|
const claudeAnthropicAuth = await maybeConfigureClaudeAnthropicAuth(flags, selected);
|
|
415
|
+
const gatewayBaseUrl = flags.gatewayBaseUrl || claim.gateway_base_url;
|
|
416
|
+
|
|
417
|
+
if (selected.some((harness) => harness.id === "claude-code")) {
|
|
418
|
+
if (claudeAnthropicAuth) {
|
|
419
|
+
await storeAnthropicToken({
|
|
420
|
+
gatewayBaseUrl,
|
|
421
|
+
apiKey: claim.api_key,
|
|
422
|
+
token: claudeAnthropicAuth.token,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
console.log("Verifying modern Claude Code compatibility before changing local settings...");
|
|
426
|
+
await probeClaudeCodeGateway({ gatewayBaseUrl, apiKey: claim.api_key });
|
|
427
|
+
console.log("");
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Keep this as the final interactive choice: users first authorize their account,
|
|
431
|
+
// optionally connect their subscription, and pass compatibility checks. Then they
|
|
432
|
+
// choose the visible command behavior with all relevant context in front of them.
|
|
433
|
+
const defaultRouting = await chooseDefaultRouting(flags, selected, manifest);
|
|
414
434
|
|
|
415
435
|
const launchable = selected.filter((harness) => ["claude-code", "codex"].includes(harness.id));
|
|
416
436
|
let runtimeResult = null;
|
|
@@ -423,7 +443,6 @@ async function install(flags) {
|
|
|
423
443
|
for (const harness of selected) {
|
|
424
444
|
await runHarnessJob(failures, harness.id, "install", async () => {
|
|
425
445
|
const plan = await harness.planInstall();
|
|
426
|
-
const gatewayBaseUrl = flags.gatewayBaseUrl || claim.gateway_base_url;
|
|
427
446
|
const existingRecord = (manifest.installations || []).find(
|
|
428
447
|
(candidate) => candidate.harness === harness.id,
|
|
429
448
|
);
|
|
@@ -441,18 +460,6 @@ async function install(flags) {
|
|
|
441
460
|
const claudeSubscriptionAuth = harness.id === "claude-code"
|
|
442
461
|
&& Boolean(claudeAnthropicAuth || existingRecord?.anthropic_token_stored);
|
|
443
462
|
|
|
444
|
-
if (harness.id === "claude-code" && claudeAnthropicAuth) {
|
|
445
|
-
await storeAnthropicToken({
|
|
446
|
-
gatewayBaseUrl,
|
|
447
|
-
apiKey: claim.api_key,
|
|
448
|
-
token: claudeAnthropicAuth.token,
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
if (harness.id === "claude-code") {
|
|
452
|
-
console.log("Verifying modern Claude Code compatibility before changing local settings...");
|
|
453
|
-
await probeClaudeCodeGateway({ gatewayBaseUrl, apiKey: claim.api_key });
|
|
454
|
-
}
|
|
455
|
-
|
|
456
463
|
if (supportsPennyCommand) {
|
|
457
464
|
await configurePennyHarness(harness.id, {
|
|
458
465
|
apiKey: claim.api_key,
|
|
@@ -505,14 +512,22 @@ async function install(flags) {
|
|
|
505
512
|
|
|
506
513
|
await saveManifest(manifest);
|
|
507
514
|
console.log("");
|
|
508
|
-
console.log(
|
|
515
|
+
console.log(records.length > 0
|
|
516
|
+
? "PennyRouter installation complete."
|
|
517
|
+
: "PennyRouter was not installed on any selected tool.");
|
|
518
|
+
console.log(`Manifest: ${statePath("installations.json")}`);
|
|
509
519
|
for (const record of records) {
|
|
510
520
|
const harness = HARNESS_BY_ID[record.harness];
|
|
511
521
|
if (record.launch_available) {
|
|
512
522
|
const command = record.harness === "claude-code" ? "penny claude" : "penny codex";
|
|
513
523
|
const native = record.harness === "claude-code" ? "claude" : "codex";
|
|
514
|
-
|
|
515
|
-
|
|
524
|
+
const harnessName = record.harness === "claude-code" ? "Claude Code" : "Codex";
|
|
525
|
+
const commandWidth = Math.max(command.length, native.length) + 2;
|
|
526
|
+
console.log("");
|
|
527
|
+
console.log(color(`How to Run ${harnessName}`, "bold"));
|
|
528
|
+
console.log("");
|
|
529
|
+
console.log(` ${color(command.padEnd(commandWidth), "bold")}→ ${harnessName} with PennyRouter`);
|
|
530
|
+
console.log(` ${color(native.padEnd(commandWidth), "bold")}→ ${record.default_routing ? `${harnessName} with PennyRouter (default routing enabled)` : `${harnessName} native`}`);
|
|
516
531
|
} else {
|
|
517
532
|
console.log(`Installed on ${harness?.name || record.harness}${installNoteForRecord(record)}.`);
|
|
518
533
|
}
|
|
@@ -522,7 +537,7 @@ async function install(flags) {
|
|
|
522
537
|
} else if (runtimeResult?.changed) {
|
|
523
538
|
console.log("Open a new terminal (or reload your shell) before using the `penny` command.");
|
|
524
539
|
}
|
|
525
|
-
console.log(
|
|
540
|
+
console.log("");
|
|
526
541
|
reportFailures(failures);
|
|
527
542
|
}
|
|
528
543
|
|
|
@@ -552,15 +567,31 @@ async function chooseDefaultRouting(flags, selected, manifest) {
|
|
|
552
567
|
}
|
|
553
568
|
const native = harness.id === "claude-code" ? "claude" : "codex";
|
|
554
569
|
const change = await confirm(
|
|
555
|
-
`PennyRouter currently runs when you type
|
|
570
|
+
`PennyRouter currently runs when you type ${color(native, "bold")}. Change it to run only with ${color(`penny ${native}`, "bold")}?`,
|
|
556
571
|
);
|
|
557
572
|
out.set(harness.id, !change);
|
|
558
573
|
continue;
|
|
559
574
|
}
|
|
560
575
|
if (newInstallChoice === null) {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
576
|
+
if (flags.dryRun || flags.yes || !input.isTTY || !output.isTTY) {
|
|
577
|
+
newInstallChoice = false;
|
|
578
|
+
} else {
|
|
579
|
+
for (const item of launchable) {
|
|
580
|
+
const native = item.id === "claude-code" ? "claude" : "codex";
|
|
581
|
+
const harnessName = item.id === "claude-code" ? "Claude Code" : "Codex";
|
|
582
|
+
const command = `penny ${native}`;
|
|
583
|
+
const commandWidth = Math.max(command.length, native.length) + 2;
|
|
584
|
+
console.log("");
|
|
585
|
+
console.log(color(`How to Run ${harnessName}`, "bold"));
|
|
586
|
+
console.log("");
|
|
587
|
+
console.log(` ${color(command.padEnd(commandWidth), "bold")}→ ${harnessName} with PennyRouter`);
|
|
588
|
+
console.log(` ${color(native.padEnd(commandWidth), "bold")}→ ${harnessName} native (recommended)`);
|
|
589
|
+
}
|
|
590
|
+
console.log("");
|
|
591
|
+
const bare = launchable.map((item) => item.id === "claude-code" ? "claude" : "codex");
|
|
592
|
+
const label = bare.length === 1 ? bare[0] : `${bare.slice(0, -1).join(", ")} and ${bare.at(-1)}`;
|
|
593
|
+
newInstallChoice = await confirm(`Also route ${color(label, "bold")} through PennyRouter by default?`);
|
|
594
|
+
}
|
|
564
595
|
}
|
|
565
596
|
out.set(harness.id, newInstallChoice);
|
|
566
597
|
}
|
|
@@ -915,15 +946,16 @@ async function status() {
|
|
|
915
946
|
|
|
916
947
|
async function selectHarnesses(flags) {
|
|
917
948
|
const requested = flags.all ? Object.keys(HARNESS_BY_ID) : parseHarnessList(flags.harness);
|
|
918
|
-
const detected = [];
|
|
919
|
-
for (const harness of Object.values(HARNESS_BY_ID)) {
|
|
920
|
-
if (await safeDetect(harness)) detected.push(harness);
|
|
921
|
-
}
|
|
922
|
-
|
|
923
949
|
if (requested.length > 0) {
|
|
924
950
|
return requested.map((id) => HARNESS_BY_ID[id]).filter(Boolean);
|
|
925
951
|
}
|
|
926
952
|
|
|
953
|
+
const detected = [];
|
|
954
|
+
for (const id of DEFAULT_INSTALL_HARNESS_IDS) {
|
|
955
|
+
const harness = HARNESS_BY_ID[id];
|
|
956
|
+
if (await safeDetect(harness)) detected.push(harness);
|
|
957
|
+
}
|
|
958
|
+
|
|
927
959
|
if (flags.yes) return detected;
|
|
928
960
|
|
|
929
961
|
return promptHarnessCheckboxes(detected);
|
|
@@ -1049,7 +1081,7 @@ async function promptHarnessCheckboxesByText(harnesses, selectedIds) {
|
|
|
1049
1081
|
}
|
|
1050
1082
|
|
|
1051
1083
|
function orderedHarnessesForPrompt(detectedIds) {
|
|
1052
|
-
return
|
|
1084
|
+
return DEFAULT_INSTALL_HARNESS_IDS.map((id) => HARNESS_BY_ID[id]).sort((left, right) => {
|
|
1053
1085
|
const leftDetected = detectedIds.has(left.id);
|
|
1054
1086
|
const rightDetected = detectedIds.has(right.id);
|
|
1055
1087
|
if (leftDetected !== rightDetected) return leftDetected ? -1 : 1;
|
package/src/launch.js
CHANGED
|
@@ -19,6 +19,7 @@ const CONFIG_VERSION = 1;
|
|
|
19
19
|
const PATH_START = "# >>> pennyrouter PATH >>>";
|
|
20
20
|
const PATH_END = "# <<< pennyrouter PATH <<<";
|
|
21
21
|
const CODEX_PROFILE_MARKER = "# PennyRouter per-command profile (managed; no secrets)";
|
|
22
|
+
const CLAUDE_COMMAND_MARKER = "<!-- PennyRouter managed Claude command -->";
|
|
22
23
|
|
|
23
24
|
export function pennyConfigDir() {
|
|
24
25
|
return join(process.env.XDG_CONFIG_HOME || join(homeDir(), ".config"), "pennyrouter");
|
|
@@ -53,8 +54,12 @@ export function codexLaunchProfilePath() {
|
|
|
53
54
|
return join(codexHome, "pennyrouter.config.toml");
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
export function
|
|
57
|
-
return join(
|
|
57
|
+
export function claudeUserSkillPath() {
|
|
58
|
+
return join(homeDir(), ".claude", "skills", "penny", "SKILL.md");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function legacyClaudeUserCommandPath() {
|
|
62
|
+
return join(homeDir(), ".claude", "commands", "penny.md");
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
async function privateWrite(path, text) {
|
|
@@ -84,12 +89,19 @@ async function copyRuntime() {
|
|
|
84
89
|
const source = packageRoot();
|
|
85
90
|
const destination = pennyRuntimeDir();
|
|
86
91
|
await mkdir(destination, { recursive: true, mode: 0o700 });
|
|
87
|
-
for (const entry of ["bin", "src"
|
|
92
|
+
for (const entry of ["bin", "src"]) {
|
|
93
|
+
// Runtime upgrades must not merge with the previous package. In particular,
|
|
94
|
+
// 0.2.1 moved /penny from a skill to a command; leaving the old skill behind
|
|
95
|
+
// makes Claude discover two incompatible definitions.
|
|
96
|
+
await rm(join(destination, entry), { recursive: true, force: true });
|
|
88
97
|
await cp(join(source, entry), join(destination, entry), {
|
|
89
98
|
recursive: true,
|
|
90
99
|
force: true,
|
|
91
100
|
});
|
|
92
101
|
}
|
|
102
|
+
// Remove plugin assets used by 0.2.0/0.2.1. /penny is now a managed personal
|
|
103
|
+
// command so it can use the exact unnamespaced name in every Claude session.
|
|
104
|
+
await rm(join(destination, "claude-plugin"), { recursive: true, force: true });
|
|
93
105
|
await cp(join(source, "package.json"), join(destination, "package.json"), { force: true });
|
|
94
106
|
}
|
|
95
107
|
|
|
@@ -172,6 +184,36 @@ async function writeClaudeLaunchSettings() {
|
|
|
172
184
|
await privateWrite(claudeLaunchSettingsPath(), `${JSON.stringify(overlay, null, 2)}\n`);
|
|
173
185
|
}
|
|
174
186
|
|
|
187
|
+
async function writeClaudeUserSkill() {
|
|
188
|
+
const path = claudeUserSkillPath();
|
|
189
|
+
const existing = await readText(path, "");
|
|
190
|
+
if (existing.trim() && !existing.includes(CLAUDE_COMMAND_MARKER)) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`${path} already exists and is not managed by PennyRouter. Rename it before installing.`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
await mkdir(dirname(path), { recursive: true });
|
|
196
|
+
await atomicWrite(path, [
|
|
197
|
+
"---",
|
|
198
|
+
"name: penny",
|
|
199
|
+
"description: PennyRouter commands and help.",
|
|
200
|
+
"argument-hint: <command and arguments>",
|
|
201
|
+
"disable-model-invocation: true",
|
|
202
|
+
"---",
|
|
203
|
+
CLAUDE_COMMAND_MARKER,
|
|
204
|
+
"",
|
|
205
|
+
"@penny $ARGUMENTS",
|
|
206
|
+
"",
|
|
207
|
+
].join("\n"));
|
|
208
|
+
const legacyPath = legacyClaudeUserCommandPath();
|
|
209
|
+
const legacy = await readText(legacyPath, "");
|
|
210
|
+
if (legacy.includes(CLAUDE_COMMAND_MARKER)) {
|
|
211
|
+
await unlink(legacyPath).catch((error) => {
|
|
212
|
+
if (error.code !== "ENOENT") throw error;
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
175
217
|
async function writeCodexLaunchProfile({ gatewayBaseUrl, subscriptionAuth }) {
|
|
176
218
|
const path = codexLaunchProfilePath();
|
|
177
219
|
const existing = await readText(path, "");
|
|
@@ -194,6 +236,7 @@ export async function configurePennyHarness(harness, {
|
|
|
194
236
|
config.api_key = apiKey;
|
|
195
237
|
config.harnesses ||= {};
|
|
196
238
|
if (harness === "claude-code") {
|
|
239
|
+
await writeClaudeUserSkill();
|
|
197
240
|
await writeClaudeLaunchSettings();
|
|
198
241
|
config.harnesses.claude = {
|
|
199
242
|
enabled: true,
|
|
@@ -228,6 +271,13 @@ export async function removePennyHarness(harness) {
|
|
|
228
271
|
await unlink(claudeLaunchSettingsPath()).catch((error) => {
|
|
229
272
|
if (error.code !== "ENOENT") throw error;
|
|
230
273
|
});
|
|
274
|
+
for (const path of [claudeUserSkillPath(), legacyClaudeUserCommandPath()]) {
|
|
275
|
+
const command = await readText(path, "");
|
|
276
|
+
if (!command.includes(CLAUDE_COMMAND_MARKER)) continue;
|
|
277
|
+
await unlink(path).catch((error) => {
|
|
278
|
+
if (error.code !== "ENOENT") throw error;
|
|
279
|
+
});
|
|
280
|
+
}
|
|
231
281
|
} else if (harness === "codex") {
|
|
232
282
|
const existing = await readText(codexLaunchProfilePath(), "");
|
|
233
283
|
if (existing.startsWith(CODEX_PROFILE_MARKER)) {
|
|
@@ -296,7 +346,7 @@ export function buildPennyInvocation(harness, args, config) {
|
|
|
296
346
|
if (models.fable) env.ANTHROPIC_DEFAULT_FABLE_MODEL = models.fable;
|
|
297
347
|
return {
|
|
298
348
|
command: "claude",
|
|
299
|
-
args: ["--settings", claudeLaunchSettingsPath(),
|
|
349
|
+
args: ["--settings", claudeLaunchSettingsPath(), ...args],
|
|
300
350
|
env,
|
|
301
351
|
};
|
|
302
352
|
}
|
package/src/launch.test.js
CHANGED
|
@@ -6,7 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import {
|
|
7
7
|
buildPennyInvocation,
|
|
8
8
|
claudeLaunchSettingsPath,
|
|
9
|
-
|
|
9
|
+
claudeUserSkillPath,
|
|
10
10
|
codexLaunchProfilePath,
|
|
11
11
|
configurePennyHarness,
|
|
12
12
|
fileMode,
|
|
@@ -56,13 +56,16 @@ async function main() {
|
|
|
56
56
|
|
|
57
57
|
try {
|
|
58
58
|
const first = await installPennyRuntime();
|
|
59
|
+
const staleSkill = path.join(pennyRuntimeDir(), "claude-plugin", "skills", "penny", "SKILL.md");
|
|
60
|
+
fs.mkdirSync(path.dirname(staleSkill), { recursive: true });
|
|
61
|
+
fs.writeFileSync(staleSkill, "stale plugin file");
|
|
59
62
|
const second = await installPennyRuntime();
|
|
60
63
|
assert.equal(first.supported, true);
|
|
61
64
|
assert.equal(first.changed, true);
|
|
62
65
|
assert.equal(second.changed, false);
|
|
63
66
|
assert.equal(await fileMode(pennyShimPath()), 0o755);
|
|
64
67
|
assert.ok(fs.existsSync(path.join(pennyRuntimeDir(), "bin", "penny.js")));
|
|
65
|
-
assert.ok(fs.existsSync(
|
|
68
|
+
assert.ok(!fs.existsSync(staleSkill), "runtime upgrade removes stale plugin files");
|
|
66
69
|
const rc = fs.readFileSync(path.join(home, ".zshrc"), "utf8");
|
|
67
70
|
assert.equal((rc.match(/>>> pennyrouter PATH >>>/g) || []).length, 1);
|
|
68
71
|
|
|
@@ -94,7 +97,7 @@ async function main() {
|
|
|
94
97
|
const claude = buildPennyInvocation("claude", ["--resume", "abc"], config);
|
|
95
98
|
assert.equal(claude.command, "claude");
|
|
96
99
|
assert.deepEqual(claude.args.slice(-2), ["--resume", "abc"]);
|
|
97
|
-
assert.ok(claude.args.includes("--plugin-dir"));
|
|
100
|
+
assert.ok(!claude.args.includes("--plugin-dir"));
|
|
98
101
|
assert.equal(claude.env.ANTHROPIC_AUTH_TOKEN, "pr-super-secret");
|
|
99
102
|
assert.equal(claude.env.ANTHROPIC_BASE_URL, "https://gateway.example/");
|
|
100
103
|
assert.equal(claude.env.ANTHROPIC_DEFAULT_OPUS_MODEL, "Penny Opus");
|
|
@@ -105,10 +108,20 @@ async function main() {
|
|
|
105
108
|
assert.throws(() => buildPennyInvocation("claude", ["--settings=x"], config), /manages --settings/);
|
|
106
109
|
assert.throws(() => buildPennyInvocation("codex", ["-p", "other"], config), /manages --profile/);
|
|
107
110
|
|
|
108
|
-
const
|
|
109
|
-
assert.ok(
|
|
111
|
+
const command = fs.readFileSync(claudeUserSkillPath(), "utf8");
|
|
112
|
+
assert.ok(command.includes("@penny $ARGUMENTS"));
|
|
113
|
+
assert.ok(command.includes("name: penny"));
|
|
110
114
|
|
|
111
115
|
await removePennyHarness("claude-code");
|
|
116
|
+
assert.ok(!fs.existsSync(claudeUserSkillPath()));
|
|
117
|
+
fs.mkdirSync(path.dirname(claudeUserSkillPath()), { recursive: true });
|
|
118
|
+
fs.writeFileSync(claudeUserSkillPath(), "user-owned command\n");
|
|
119
|
+
await assert.rejects(configurePennyHarness("claude-code", {
|
|
120
|
+
apiKey: "pr-super-secret",
|
|
121
|
+
gatewayBaseUrl: "https://gateway.example/",
|
|
122
|
+
models: {},
|
|
123
|
+
}), /not managed by PennyRouter/);
|
|
124
|
+
assert.equal(fs.readFileSync(claudeUserSkillPath(), "utf8"), "user-owned command\n");
|
|
112
125
|
assert.ok(fs.existsSync(codexLaunchProfilePath()));
|
|
113
126
|
assert.ok(fs.existsSync(pennyShimPath()));
|
|
114
127
|
await removePennyHarness("codex");
|