pennyrouter 0.3.21 → 0.3.22
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/package.json +1 -1
- package/src/cli.js +57 -25
- package/src/cli.test.js +10 -1
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -209,7 +209,7 @@ async function profiles(flags) {
|
|
|
209
209
|
console.log("Switch with: pennyrouter profiles <name> (or penny claude --penny-profile <name>)");
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
function parseArgs(argv) {
|
|
212
|
+
export function parseArgs(argv) {
|
|
213
213
|
const command = argv[0] && !argv[0].startsWith("-") ? argv[0] : "install";
|
|
214
214
|
const rest = command === argv[0] ? argv.slice(1) : argv;
|
|
215
215
|
const flags = {};
|
|
@@ -260,6 +260,8 @@ function parseArgs(argv) {
|
|
|
260
260
|
// clobbering the cloud credentials that `penny claude` uses by default.
|
|
261
261
|
else if (arg === "--profile") flags.profile = rest[++i] || "";
|
|
262
262
|
else if (arg.startsWith("--profile=")) flags.profile = arg.slice("--profile=".length);
|
|
263
|
+
else if (arg === "--private") flags.profile = "private";
|
|
264
|
+
else if (arg === "--cloud") flags.profile = "cloud";
|
|
263
265
|
else if (arg === "--harness") flags.harness = rest[++i] || "";
|
|
264
266
|
else if (arg.startsWith("--harness=")) flags.harness = arg.slice("--harness=".length);
|
|
265
267
|
// --private-gateway [url]: install against a self-hosted gateway. Unlike
|
|
@@ -320,9 +322,12 @@ function parseArgs(argv) {
|
|
|
320
322
|
|
|
321
323
|
if (flags.configPath) applyConfigFile(flags);
|
|
322
324
|
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
|
|
325
|
+
// Install chooses a destination profile when none was named: hosted installs go to cloud,
|
|
326
|
+
// while self-hosted installs go to private. Operational commands such as update and auth
|
|
327
|
+
// should instead leave the profile unset so loadPennyConfig() resolves active_profile.
|
|
328
|
+
if (!flags.profile && command === "install") {
|
|
329
|
+
flags.profile = flags.privateGateway ? "private" : DEFAULT_PROFILE;
|
|
330
|
+
}
|
|
326
331
|
|
|
327
332
|
return { command, flags };
|
|
328
333
|
}
|
|
@@ -925,10 +930,11 @@ export async function install(flags = {}) {
|
|
|
925
930
|
record.launch_available = true;
|
|
926
931
|
const launchConfigPath = harness.id === "claude-code"
|
|
927
932
|
? claudeLaunchSettingsPath()
|
|
928
|
-
: harness.id === "codex" ? codexLaunchProfilePath() : null;
|
|
933
|
+
: harness.id === "codex" ? codexLaunchProfilePath(flags.profile) : null;
|
|
929
934
|
record.launch_config_path = launchConfigPath ? compactHome(launchConfigPath) : null;
|
|
930
935
|
record.default_routing = Boolean(makeDefault);
|
|
931
936
|
}
|
|
937
|
+
record.profile = flags.profile;
|
|
932
938
|
if (harness.id === "claude-code" && claudeSubscriptionAuth) {
|
|
933
939
|
record.anthropic_oauth = true;
|
|
934
940
|
record.anthropic_token_stored = true;
|
|
@@ -1256,6 +1262,12 @@ async function update(flags) {
|
|
|
1256
1262
|
if (record.disabled) return false;
|
|
1257
1263
|
if (flags.all || requested.length === 0) return true;
|
|
1258
1264
|
return requested.includes(record.harness);
|
|
1265
|
+
}).filter((record) => {
|
|
1266
|
+
if (!flags.profile) return true;
|
|
1267
|
+
const recordProfile = record.profile || profileFromManifestRecord(record);
|
|
1268
|
+
// Records written before profile ownership was added have no reliable Claude profile
|
|
1269
|
+
// marker, so leave them eligible for an explicit profile as a compatibility fallback.
|
|
1270
|
+
return !recordProfile || recordProfile === flags.profile;
|
|
1259
1271
|
});
|
|
1260
1272
|
|
|
1261
1273
|
if (records.length === 0) {
|
|
@@ -1266,25 +1278,32 @@ async function update(flags) {
|
|
|
1266
1278
|
return;
|
|
1267
1279
|
}
|
|
1268
1280
|
|
|
1269
|
-
const
|
|
1270
|
-
const
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|| globalAuth.
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1281
|
+
const updateContexts = new Map();
|
|
1282
|
+
for (const record of targets) {
|
|
1283
|
+
const profile = record.profile || profileFromManifestRecord(record) || flags.profile;
|
|
1284
|
+
const launchConfig = await loadPennyConfig({ profile });
|
|
1285
|
+
const globalAuth = await readClaudeCodeGatewayAuth();
|
|
1286
|
+
const apiKey = flags.pennyKey || launchConfig.api_key || globalAuth.apiKey;
|
|
1287
|
+
const gatewayBaseUrl = flags.gatewayBaseUrl
|
|
1288
|
+
|| launchConfig.gateway_base_url
|
|
1289
|
+
|| globalAuth.gatewayBaseUrl
|
|
1290
|
+
|| process.env.PENNYROUTER_GATEWAY_BASE_URL
|
|
1291
|
+
|| DEFAULT_GATEWAY_BASE_URL;
|
|
1292
|
+
if (!apiKey) {
|
|
1293
|
+
const suffix = profile ? ` for profile "${profile}"` : "";
|
|
1294
|
+
throw new Error(
|
|
1295
|
+
`Could not find your PennyRouter key in the local config${suffix}. `
|
|
1296
|
+
+ "Pass `--penny-key pr-...`, or run `npx pennyrouter install` to re-authorize.",
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
updateContexts.set(record, { profile, apiKey, gatewayBaseUrl });
|
|
1283
1300
|
}
|
|
1284
1301
|
|
|
1285
1302
|
console.log("PennyRouter update");
|
|
1286
1303
|
console.log("");
|
|
1287
|
-
|
|
1304
|
+
for (const { profile, apiKey, gatewayBaseUrl } of updateContexts.values()) {
|
|
1305
|
+
console.log(`${profile ? `Profile ${profile} · ` : ""}Key ${maskKey(apiKey)} · gateway ${gatewayBaseUrl}`);
|
|
1306
|
+
}
|
|
1288
1307
|
console.log("Subscription and API keys are left untouched.");
|
|
1289
1308
|
console.log("");
|
|
1290
1309
|
|
|
@@ -1307,6 +1326,7 @@ async function update(flags) {
|
|
|
1307
1326
|
for (const record of targets) {
|
|
1308
1327
|
const harness = HARNESS_BY_ID[record.harness];
|
|
1309
1328
|
if (!harness) continue;
|
|
1329
|
+
const { profile, apiKey, gatewayBaseUrl } = updateContexts.get(record);
|
|
1310
1330
|
|
|
1311
1331
|
await runHarnessJob(failures, record.harness, "update", async () => {
|
|
1312
1332
|
const supportsPennyCommand = LAUNCHABLE_HARNESS_IDS.includes(record.harness);
|
|
@@ -1333,7 +1353,7 @@ async function update(flags) {
|
|
|
1333
1353
|
? claudeSubscriptionAuth
|
|
1334
1354
|
: codexSubscriptionAuth,
|
|
1335
1355
|
models: record.harness === "claude-code" ? CLAUDE_CODE_MODELS : null,
|
|
1336
|
-
profile
|
|
1356
|
+
profile,
|
|
1337
1357
|
});
|
|
1338
1358
|
}
|
|
1339
1359
|
|
|
@@ -1368,9 +1388,13 @@ async function update(flags) {
|
|
|
1368
1388
|
// and `pennyrouter mcp uninstall` removes it for good.
|
|
1369
1389
|
if (!flags.dryRun && !flags.noMcp) {
|
|
1370
1390
|
const wanted = targets.filter((record) => MCP_HARNESS_IDS.includes(record.harness));
|
|
1371
|
-
const results =
|
|
1372
|
-
|
|
1373
|
-
|
|
1391
|
+
const results = [];
|
|
1392
|
+
for (const record of wanted) {
|
|
1393
|
+
const context = updateContexts.get(record);
|
|
1394
|
+
results.push(...await registerMcp([record.harness], {
|
|
1395
|
+
gatewayBaseUrl: customGatewayBaseUrl(context.gatewayBaseUrl),
|
|
1396
|
+
}));
|
|
1397
|
+
}
|
|
1374
1398
|
const added = results.filter((r) => r.changed).map((r) => HARNESS_BY_ID[r.harness]?.name);
|
|
1375
1399
|
if (added.length) console.log(`Registered the Threads MCP server on ${added.join(" and ")}.`);
|
|
1376
1400
|
for (const result of results.filter((r) => r.error)) {
|
|
@@ -2277,6 +2301,14 @@ function upsertManifestRecord(manifest, record) {
|
|
|
2277
2301
|
manifest.installations.push(record);
|
|
2278
2302
|
}
|
|
2279
2303
|
|
|
2304
|
+
// Older manifests predate profile ownership. Codex's per-profile launch filename gives us a
|
|
2305
|
+
// reliable migration signal; Claude's launch settings are shared, so it follows active_profile.
|
|
2306
|
+
function profileFromManifestRecord(record) {
|
|
2307
|
+
const launchPath = String(record.launch_config_path || "");
|
|
2308
|
+
const match = launchPath.match(/pennyrouter-([^/\\.]+)\.config\.toml$/);
|
|
2309
|
+
return match ? match[1] : null;
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2280
2312
|
function pruneUnsupportedManifestRecords(manifest) {
|
|
2281
2313
|
manifest.installations ||= [];
|
|
2282
2314
|
const kept = manifest.installations.filter((record) => HARNESS_BY_ID[record.harness]);
|
|
@@ -2442,7 +2474,7 @@ function printHelp() {
|
|
|
2442
2474
|
|
|
2443
2475
|
Usage:
|
|
2444
2476
|
pennyrouter install [--harness ${SUPPORTED_HARNESS_IDS}] [--all] [--yes] [--dry-run] [--make-default|--penny-only] [--no-path-update] [--no-mcp] [--anthropic-auth|--no-anthropic-auth] [--anthropic-api-key KEY] [--openai-api-key KEY] [--openai-proxy-base-url URL --openai-proxy-api-key KEY] [--openrouter-api-key KEY] [--anthropic-proxy-base-url URL --anthropic-proxy-api-key KEY] [--proxy-base-url URL --proxy-api-key KEY] [--private-gateway|--private-server[=URL] [--email you@example.com]] [--gateway-base-url URL] [--private-key pr-...] [--local[=URL]|--prod] [--config PATH]
|
|
2445
|
-
pennyrouter update [--harness ${SUPPORTED_HARNESS_IDS}] [--all] [--dry-run] [--penny-key pr-...] [--gateway-base-url URL|--local[=URL]|--prod] [--no-mcp]
|
|
2477
|
+
pennyrouter update [--harness ${SUPPORTED_HARNESS_IDS}] [--all] [--dry-run] [--penny-key pr-...] [--profile NAME|--private|--cloud] [--gateway-base-url URL|--local[=URL]|--prod] [--no-mcp]
|
|
2446
2478
|
pennyrouter auth anthropic [--penny-key pr-...] [--token oauth-token] [--token-command CMD] [--gateway-base-url URL] [--forget]
|
|
2447
2479
|
pennyrouter disable [--harness ${SUPPORTED_HARNESS_IDS}] [--all] [--yes]
|
|
2448
2480
|
pennyrouter enable [--harness ${SUPPORTED_HARNESS_IDS}] [--all] [--yes]
|
package/src/cli.test.js
CHANGED
|
@@ -4,7 +4,8 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import test from "node:test";
|
|
6
6
|
import {
|
|
7
|
-
applyConfigFile, harnessFundingChanged, INSTALL_MANIFEST, main,
|
|
7
|
+
applyConfigFile, harnessFundingChanged, INSTALL_MANIFEST, main, parseArgs,
|
|
8
|
+
SUPPORTED_HARNESS_IDS,
|
|
8
9
|
} from "./cli.js";
|
|
9
10
|
|
|
10
11
|
// --config lets a GUI installer (the macOS DMG) answer install questions up front instead of
|
|
@@ -29,6 +30,14 @@ function captureLogs(run) {
|
|
|
29
30
|
return run().finally(() => { console.log = original; }).then(() => lines);
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
test("update uses the active profile unless one is explicitly selected", () => {
|
|
34
|
+
assert.equal(parseArgs(["update"]).flags.profile, undefined);
|
|
35
|
+
assert.equal(parseArgs(["update", "--profile", "private"]).flags.profile, "private");
|
|
36
|
+
assert.equal(parseArgs(["update", "--cloud"]).flags.profile, "cloud");
|
|
37
|
+
assert.equal(parseArgs(["install"]).flags.profile, "cloud");
|
|
38
|
+
assert.equal(parseArgs(["install", "--private-gateway"]).flags.profile, "private");
|
|
39
|
+
});
|
|
40
|
+
|
|
32
41
|
test("--config selects harnesses without prompting", async () => {
|
|
33
42
|
await withConfigFile({ harnesses: ["claude-code"] }, async (path) => {
|
|
34
43
|
const lines = await captureLogs(() => main(["install", "--config", path, "--dry-run"]));
|