pennyrouter 0.3.12 → 0.3.14

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pennyrouter",
3
- "version": "0.3.12",
3
+ "version": "0.3.14",
4
4
  "description": "Install and manage PennyRouter local coding-agent integrations.",
5
5
  "homepage": "https://pennyrouter.com",
6
6
  "bugs": {
package/src/cli.js CHANGED
@@ -174,7 +174,7 @@ async function profiles(flags) {
174
174
  }
175
175
  if (requested) {
176
176
  // Resolve before saving so an unknown name fails without changing anything.
177
- const selected = resolvePennyProfile(config, requested);
177
+ const selected = resolvePennyProfile(config, requested, { requireExisting: true });
178
178
  await savePennyConfig({ ...selected, active_profile: requested });
179
179
  console.log(`Active profile is now "${requested}" (${selected.gateway_base_url}).`);
180
180
  return;
package/src/launch.js CHANGED
@@ -151,16 +151,19 @@ export function activePennyProfile(config) {
151
151
  * `gateway_base_url`. Every consumer reads those two fields, so resolving here keeps the
152
152
  * profile lookup in one place rather than threading a profile through each caller.
153
153
  *
154
- * An unconfigured machine resolves to a profile with no credentials rather than throwing:
155
- * `install` has to run against a config that does not exist yet.
154
+ * A name that does not exist yet resolves to a profile with no credentials rather than
155
+ * throwing, because `install --profile private` has to read the profile it is about to
156
+ * create. Only an explicit `requireExisting` lookup — selecting a profile to launch or
157
+ * switch to — treats an unknown name as an error.
156
158
  */
157
- export function resolvePennyProfile(config, name = null) {
159
+ export function resolvePennyProfile(config, name = null, { requireExisting = false } = {}) {
158
160
  const requested = name || activePennyProfile(config);
159
161
  const profiles = config.profiles || {};
160
162
  const entry = profiles[requested];
161
163
  if (!entry) {
162
- if (Object.keys(profiles).length === 0) return { ...config, profile: requested };
163
- const known = Object.keys(profiles).join(", ");
164
+ if (!requireExisting) return { ...config, profile: requested };
165
+ const names = Object.keys(profiles);
166
+ const known = names.length ? names.join(", ") : "none configured";
164
167
  throw new Error(`Unknown PennyRouter profile "${requested}". Available: ${known}.`);
165
168
  }
166
169
  return {
@@ -183,13 +186,13 @@ export function setPennyProfile(config, name, { apiKey, gatewayBaseUrl }) {
183
186
  };
184
187
  }
185
188
 
186
- export async function loadPennyConfig({ profile = null } = {}) {
189
+ export async function loadPennyConfig({ profile = null, requireExisting = false } = {}) {
187
190
  const config = (await readJsonFile(pennyConfigPath(), null)) || {
188
191
  version: CONFIG_VERSION,
189
192
  profiles: {},
190
193
  harnesses: {},
191
194
  };
192
- return resolvePennyProfile(config, profile);
195
+ return resolvePennyProfile(config, profile, { requireExisting });
193
196
  }
194
197
 
195
198
  /**
@@ -1039,7 +1042,10 @@ export async function runPenny(argv = process.argv.slice(2)) {
1039
1042
  }
1040
1043
  const selection = extractProfileArgs(argv.slice(1));
1041
1044
  argv = [harness, ...selection.args];
1042
- const config = await loadPennyConfig({ profile: selection.profile });
1045
+ const config = await loadPennyConfig({
1046
+ profile: selection.profile,
1047
+ requireExisting: Boolean(selection.profile),
1048
+ });
1043
1049
  if (harness === "claude" && argv[1]?.toLowerCase() === "desktop") {
1044
1050
  const option = argv[2]?.toLowerCase();
1045
1051
  if (option === "--restore" || option === "restore") {
@@ -284,6 +284,10 @@ async function main() {
284
284
  });
285
285
  assert.ok(paid.includes('env_key = "PENNYROUTER_API_KEY"'));
286
286
  assert.ok(!paid.includes("requires_openai_auth"));
287
+
288
+ // Inside the fixture: testProfiles persists a config, which must land in the
289
+ // temporary HOME and never in the developer's real ~/.config/pennyrouter.
290
+ await testProfiles();
287
291
  } finally {
288
292
  for (const key of ENV_KEYS) {
289
293
  if (previous[key] === undefined) delete process.env[key];
@@ -292,7 +296,6 @@ async function main() {
292
296
  fs.rmSync(root, { recursive: true, force: true });
293
297
  }
294
298
 
295
- await testProfiles();
296
299
  console.log("launch.test.js: all checks passed");
297
300
  }
298
301
 
@@ -333,7 +336,21 @@ async function testProfiles() {
333
336
  resolvePennyProfile(config, "cloud").gateway_base_url,
334
337
  "https://api.pennyrouter.com",
335
338
  );
336
- assert.throws(() => resolvePennyProfile(config, "nope"), /Unknown PennyRouter profile/);
339
+ // A not-yet-created profile resolves empty: `install --profile private` must be able to
340
+ // read the profile it is about to write, even when other profiles already exist.
341
+ const pending = resolvePennyProfile(config, "brand-new");
342
+ assert.equal(pending.profile, "brand-new");
343
+ assert.equal(pending.api_key, undefined);
344
+ // Selecting one to launch or switch to is the opposite: an unknown name must fail loudly
345
+ // rather than silently launching with no credentials.
346
+ assert.throws(
347
+ () => resolvePennyProfile(config, "nope", { requireExisting: true }),
348
+ /Unknown PennyRouter profile "nope"\. Available: cloud, private\./,
349
+ );
350
+ await assert.rejects(
351
+ loadPennyConfig({ profile: "nope", requireExisting: true }),
352
+ /Unknown PennyRouter profile/,
353
+ );
337
354
 
338
355
  // A resolved config must not persist one profile's credentials at the top level,
339
356
  // where they would shadow whichever profile is selected on the next load.