pinsay-cli 0.7.3 → 0.8.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.
Files changed (3) hide show
  1. package/README.md +3 -3
  2. package/dist/cli.js +187 -94
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -17,8 +17,8 @@ npx pinsay-cli init
17
17
 
18
18
  | Command | What it does |
19
19
  |---|---|
20
- | `npx pinsay-cli init` | Configure a repo (server, project, widget snippet, skills) |
21
- | `npx pinsay-cli login` / `whoami` / `logout` | Sign in once per machine; check / clear credentials |
20
+ | `npx pinsay-cli init` | Configure a repo (project, widget snippet, skills) |
21
+ | `npx pinsay-cli login` / `whoami` / `logout` | Sign in (asks: this machine or this repo only); check / clear credentials |
22
22
  | `npx pinsay-cli list` / `get <id>` | List pending feedback; inspect one comment |
23
23
  | `npx pinsay-cli apply` | Generate the fix-it prompt for your AI tool, then record what changed |
24
24
  | `npx pinsay-cli apply --plan` | Preview proposed edits without touching code |
@@ -38,7 +38,7 @@ npx pinsay-cli init
38
38
 
39
39
  ## API keys
40
40
 
41
- Keys look like `pnsy_` + 40 hex characters. They resolve in this order: `PINSAY_API_KEY` env var → `.pinsay/credentials.env` → the per-machine store (`~/.config/pointer/credentials.json`, mode 0600, never committed).
41
+ Keys look like `pnsy_` + 40 hex characters. They resolve in this order: `PINSAY_API_KEY` env var → `.pinsay/credentials.env` → the per-machine store (`~/.config/pinsay/credentials.json`, mode 0600, never committed).
42
42
 
43
43
  ## MCP (Claude Code / Cursor)
44
44
 
package/dist/cli.js CHANGED
@@ -15,7 +15,7 @@ var init_build_constants = __esm({
15
15
  "src/build-constants.ts"() {
16
16
  "use strict";
17
17
  BUILD_DEFAULT_SERVER = true ? "https://app.pinsay.dev" : "https://app.pinsay.dev";
18
- BUILD_CLI_VERSION = true ? "0.7.3" : "0.0.0-dev";
18
+ BUILD_CLI_VERSION = true ? "0.8.0" : "0.0.0-dev";
19
19
  }
20
20
  });
21
21
 
@@ -124,8 +124,6 @@ async function writeCredentials(cwd2, token, extra = {}) {
124
124
  const file = join(cwd2, CREDENTIALS_FILE);
125
125
  await fs.mkdir(dirname(file), { recursive: true });
126
126
  const lines = [`PINSAY_API_KEY=${token}`];
127
- if (extra.server)
128
- lines.push(`PINSAY_SERVER=${extra.server}`);
129
127
  if (extra.project)
130
128
  lines.push(`PINSAY_PROJECT=${extra.project}`);
131
129
  await fs.writeFile(file, lines.join("\n") + "\n", { encoding: "utf8", mode: 384 });
@@ -278,38 +276,83 @@ function normalizeServerOrigin(server) {
278
276
  return server.replace(/\/+$/, "");
279
277
  }
280
278
  }
279
+ function configBase() {
280
+ if (process.platform === "win32") {
281
+ return process.env.APPDATA || join10(homedir(), "AppData", "Roaming");
282
+ }
283
+ return process.env.XDG_CONFIG_HOME || join10(homedir(), ".config");
284
+ }
281
285
  function globalConfigDir() {
282
286
  if (process.env.PINSAY_CONFIG_DIR)
283
287
  return process.env.PINSAY_CONFIG_DIR;
284
- if (process.platform === "win32") {
285
- return join10(process.env.APPDATA || join10(homedir(), "AppData", "Roaming"), "pointer");
286
- }
287
- return join10(process.env.XDG_CONFIG_HOME || join10(homedir(), ".config"), "pointer");
288
+ return join10(configBase(), DIR_NAME);
288
289
  }
289
290
  function globalCredentialsPath() {
290
291
  return join10(globalConfigDir(), "credentials.json");
291
292
  }
293
+ function legacyGlobalCredentialsPath() {
294
+ if (process.env.PINSAY_CONFIG_DIR)
295
+ return void 0;
296
+ return join10(configBase(), LEGACY_DIR_NAME, "credentials.json");
297
+ }
292
298
  function globalCacheDir() {
293
299
  if (process.env.PINSAY_CONFIG_DIR)
294
300
  return join10(process.env.PINSAY_CONFIG_DIR, "cache");
295
301
  if (process.platform === "win32") {
296
- return join10(process.env.LOCALAPPDATA || join10(homedir(), "AppData", "Local"), "pointer", "cache");
302
+ return join10(process.env.LOCALAPPDATA || join10(homedir(), "AppData", "Local"), DIR_NAME, "cache");
297
303
  }
298
- return join10(process.env.XDG_CACHE_HOME || join10(homedir(), ".cache"), "pointer");
304
+ return join10(process.env.XDG_CACHE_HOME || join10(homedir(), ".cache"), DIR_NAME);
299
305
  }
300
306
  function tokenCacheFile(server, apiKey) {
301
307
  const hash = createHash("sha256").update(`${normalizeServerOrigin(server)}:${apiKey}`).digest("hex").slice(0, 32);
302
308
  return join10(globalCacheDir(), `${hash}.json`);
303
309
  }
310
+ function isCredentialStore(value) {
311
+ if (!value || typeof value !== "object" || Array.isArray(value))
312
+ return false;
313
+ return Object.values(value).every(
314
+ (entry) => !!entry && typeof entry === "object" && typeof entry.apiKey === "string"
315
+ );
316
+ }
304
317
  async function readGlobalStore() {
318
+ let raw;
319
+ try {
320
+ raw = await fs10.readFile(globalCredentialsPath(), "utf8");
321
+ } catch (err) {
322
+ if (err?.code === "ENOENT")
323
+ return migrateLegacyStore();
324
+ return {};
325
+ }
305
326
  try {
306
- const raw = await fs10.readFile(globalCredentialsPath(), "utf8");
307
327
  const parsed = JSON.parse(raw);
308
328
  return parsed && typeof parsed === "object" ? parsed : {};
309
329
  } catch {
310
330
  return {};
311
331
  }
312
332
  }
333
+ async function migrateLegacyStore() {
334
+ const legacyFile = legacyGlobalCredentialsPath();
335
+ if (!legacyFile)
336
+ return {};
337
+ let legacy;
338
+ try {
339
+ legacy = JSON.parse(await fs10.readFile(legacyFile, "utf8"));
340
+ } catch {
341
+ return {};
342
+ }
343
+ if (!isCredentialStore(legacy))
344
+ return {};
345
+ try {
346
+ await writeGlobalStore(legacy);
347
+ } catch {
348
+ return legacy;
349
+ }
350
+ await fs10.rm(legacyFile, { force: true }).catch(() => {
351
+ });
352
+ await fs10.rmdir(dirname4(legacyFile)).catch(() => {
353
+ });
354
+ return legacy;
355
+ }
313
356
  async function writeGlobalStore(store) {
314
357
  const file = globalCredentialsPath();
315
358
  await fs10.mkdir(dirname4(file), { recursive: true });
@@ -332,9 +375,12 @@ async function saveGlobalCredential(server, entry) {
332
375
  async function removeGlobalCredential(server) {
333
376
  const store = await readGlobalStore();
334
377
  const origin = normalizeServerOrigin(server);
335
- if (!(origin in store))
378
+ const origins = origin === "https://app.pinsay.dev" ? [origin, "https://api.pinsay.dev"] : [origin];
379
+ const present = origins.filter((o) => o in store);
380
+ if (present.length === 0)
336
381
  return false;
337
- delete store[origin];
382
+ for (const o of present)
383
+ delete store[o];
338
384
  await writeGlobalStore(store);
339
385
  return true;
340
386
  }
@@ -364,9 +410,39 @@ async function removeStaleRepoTokenCache(root) {
364
410
  await fs10.rm(join10(root, ".pinsay", ".token_cache"), { force: true }).catch(() => {
365
411
  });
366
412
  }
413
+ var DIR_NAME, LEGACY_DIR_NAME;
367
414
  var init_credentials = __esm({
368
415
  "src/credentials.ts"() {
369
416
  "use strict";
417
+ DIR_NAME = "pinsay";
418
+ LEGACY_DIR_NAME = "pointer";
419
+ }
420
+ });
421
+
422
+ // src/server.ts
423
+ function resolveServer() {
424
+ const override = process.env.PINSAY_SERVER?.trim();
425
+ return canonicalServer(override || BUILD_DEFAULT_SERVER).replace(/\/+$/, "");
426
+ }
427
+ async function serverSettingError(cwd2, parsed) {
428
+ const server = resolveServer();
429
+ if (parsed["server"] !== void 0) {
430
+ return `--server was removed: this CLI talks only to ${server}.`;
431
+ }
432
+ const root = await findRepoRoot(cwd2);
433
+ const config = await readConfig(root).catch(() => ({}));
434
+ if (config.server && normalizeServerOrigin(config.server) !== normalizeServerOrigin(server)) {
435
+ return `.pinsay/config.json sets "server": "${config.server}", but this CLI talks only to ${server}.
436
+ Remove the "server" line from .pinsay/config.json, then run the command again.`;
437
+ }
438
+ return null;
439
+ }
440
+ var init_server = __esm({
441
+ "src/server.ts"() {
442
+ "use strict";
443
+ init_build_constants();
444
+ init_config();
445
+ init_credentials();
370
446
  }
371
447
  });
372
448
 
@@ -931,11 +1007,7 @@ function mapEnvironmentToString(env) {
931
1007
  async function getClient(cwd2, parsed, opts = {}) {
932
1008
  const root = await findRepoRoot(cwd2);
933
1009
  const config = await readConfig(root);
934
- const server = ((typeof parsed["server"] === "string" ? parsed["server"] : config.server) || BUILD_DEFAULT_SERVER).replace(/\/$/, "");
935
- if (!server) {
936
- console.error("No server configured.");
937
- process.exit(2);
938
- }
1010
+ const server = resolveServer();
939
1011
  const explicitKey = typeof parsed["key"] === "string" ? parsed["key"] : void 0;
940
1012
  const token = await resolveToken(server, root, explicitKey);
941
1013
  const apiKey = explicitKey || await readApiKey(root, server);
@@ -1145,8 +1217,8 @@ var init_comments = __esm({
1145
1217
  init_resolve();
1146
1218
  init_api();
1147
1219
  init_auth();
1148
- init_build_constants();
1149
1220
  init_projection();
1221
+ init_server();
1150
1222
  }
1151
1223
  });
1152
1224
 
@@ -1261,7 +1333,7 @@ function assertInteractive() {
1261
1333
  if (process.stdin.isTTY)
1262
1334
  return;
1263
1335
  console.error(
1264
- "\x1B[31mThis command is interactive, but stdin is not a terminal.\x1B[0m\nPiped input, CI, and some editor-embedded shells have no TTY, so there is no way to ask you anything.\n\nEither run it in a real terminal, or pass every answer as a flag:\n npx -y pinsay-cli init --server <url> --key ptr_... --project <key> --environment local --yes\n\nRun 'npx -y pinsay-cli init --help' for the full list of flags."
1336
+ "\x1B[31mThis command is interactive, but stdin is not a terminal.\x1B[0m\nPiped input, CI, and some editor-embedded shells have no TTY, so there is no way to ask you anything.\n\nEither run it in a real terminal, or pass every answer as a flag:\n npx -y pinsay-cli init --key ptr_... --project <key> --yes\n\nRun 'npx -y pinsay-cli init --help' for the full list of flags."
1265
1337
  );
1266
1338
  process.exit(2);
1267
1339
  }
@@ -2271,6 +2343,7 @@ async function writeStackFile(cwd2, stack, projectKey) {
2271
2343
 
2272
2344
  // src/checks.ts
2273
2345
  init_credentials();
2346
+ init_server();
2274
2347
  var execFileAsync = promisify(execFile);
2275
2348
  function tooOldMessage(cliVersion, min) {
2276
2349
  return `CLI ${cliVersion} is older than the server requires (${min}). Upgrade with: npx -y pinsay@latest`;
@@ -2308,7 +2381,7 @@ async function fetchWithTimeout(url, ms, init) {
2308
2381
  async function runInitChecks(cwd2, overrides = {}, cliVersion = "0.0.0") {
2309
2382
  const checks = [];
2310
2383
  const config = await readConfig(cwd2);
2311
- const server = (overrides.server || config.server || "").replace(/\/$/, "");
2384
+ const server = (overrides.server || resolveServer()).replace(/\/$/, "");
2312
2385
  const multiProject = isMultiProject(config);
2313
2386
  const allProjects = listProjects(config);
2314
2387
  const projectTargets = multiProject && overrides.project ? allProjects.filter((p) => p.key === overrides.project) : allProjects;
@@ -3355,7 +3428,35 @@ async function runDeviceLogin(server, options = {}) {
3355
3428
  return { ok: false, reason: "expired" };
3356
3429
  }
3357
3430
 
3431
+ // src/key-scope.ts
3432
+ var KEY_SCOPE_QUESTION = "Where should this API key be stored?";
3433
+ var KEY_SCOPE_GLOBAL = "Global \u2014 this machine, every repo (~/.config/pinsay/credentials.json)";
3434
+ var KEY_SCOPE_REPO = "Repo \u2014 .pinsay/credentials.env in this repo only (gitignored)";
3435
+ function scopeFromFlags(options) {
3436
+ const raw = options["scope"];
3437
+ if (typeof raw === "string") {
3438
+ const value = raw.toLowerCase();
3439
+ if (value !== "global" && value !== "repo") {
3440
+ return { error: `Invalid --scope "${raw}". Valid values: global, repo.` };
3441
+ }
3442
+ return { scope: value };
3443
+ }
3444
+ if (options["local-credentials"] === true)
3445
+ return { scope: "repo" };
3446
+ return {};
3447
+ }
3448
+ function decideKeyScope(flagScope, interactive) {
3449
+ if (flagScope)
3450
+ return flagScope;
3451
+ return interactive ? "ask" : "global";
3452
+ }
3453
+ async function askKeyScope() {
3454
+ const choice = await select(KEY_SCOPE_QUESTION, [KEY_SCOPE_GLOBAL, KEY_SCOPE_REPO]);
3455
+ return choice === KEY_SCOPE_REPO ? "repo" : "global";
3456
+ }
3457
+
3358
3458
  // src/commands/init.ts
3459
+ init_server();
3359
3460
  async function initCommand(cwd2, options = {}) {
3360
3461
  const isYes = options["yes"] || options["json"];
3361
3462
  const isJson = options["json"];
@@ -3373,12 +3474,9 @@ async function initCommand(cwd2, options = {}) {
3373
3474
  const pathFlag = typeof options["path"] === "string" ? String(options["path"]).replace(/^\.\/+/, "").replace(/\/+$/, "") : void 0;
3374
3475
  const isAddProject = Boolean(pathFlag);
3375
3476
  const configIsMulti = isMultiProject(config);
3376
- const isJoin = !isAddProject && Boolean(config.server) && (Boolean(config.project) || configIsMulti);
3477
+ const isJoin = !isAddProject && (Boolean(config.project) || configIsMulti);
3377
3478
  const mode = isJoin ? "join" : "install";
3378
- let server = options["server"] || config.server || canonicalServer(process.env.PINSAY_SERVER) || BUILD_DEFAULT_SERVER;
3379
- if (!isYes && !options["server"] && !config.server) {
3380
- server = await ask("Server URL", { default: server });
3381
- }
3479
+ const server = resolveServer();
3382
3480
  const scopeFlag = typeof options["scope"] === "string" ? String(options["scope"]).toLowerCase() : void 0;
3383
3481
  if (scopeFlag !== void 0 && scopeFlag !== "global" && scopeFlag !== "repo") {
3384
3482
  console.error(`Invalid --scope "${options["scope"]}". Valid values: global, repo.`);
@@ -3521,11 +3619,7 @@ async function initCommand(cwd2, options = {}) {
3521
3619
  if (freshlyAuthenticated) {
3522
3620
  let saveGlobally = !localCredentialsFlag;
3523
3621
  if (saveGlobally && !isYes && scopeFlag === void 0) {
3524
- const choice = await select("Where should this API key be stored?", [
3525
- "Global \u2014 this machine, every repo (~/.config/pointer/credentials.json)",
3526
- "Repo \u2014 .pinsay/credentials.env in this repo only (gitignored)"
3527
- ]);
3528
- saveGlobally = choice.startsWith("Global");
3622
+ saveGlobally = await askKeyScope() === "global";
3529
3623
  }
3530
3624
  if (saveGlobally) {
3531
3625
  await saveGlobalCredential(server, { apiKey: key, email: me?.email, displayName: me?.displayName });
@@ -3534,7 +3628,7 @@ async function initCommand(cwd2, options = {}) {
3534
3628
  }
3535
3629
  }
3536
3630
  if (writeLocalCreds) {
3537
- await writeCredentials(cwd2, key, { server });
3631
+ await writeCredentials(cwd2, key);
3538
3632
  }
3539
3633
  await upsertGitignore(cwd2, product, options["skills-dir"] || config.skillsDir);
3540
3634
  if (isJoin && configIsMulti) {
@@ -3856,7 +3950,6 @@ and the pinsay-init skill uses them to mount the widget for you afterwards.
3856
3950
  }
3857
3951
  const injectedHtml = injected ? filesMod.find((f) => f.toLowerCase().endsWith(".html"))?.replace(`${cwd2}/`, "") : void 0;
3858
3952
  const configPatch = {
3859
- server,
3860
3953
  project: finalProjectKey,
3861
3954
  aiTool: tool,
3862
3955
  skillsDir: options["skills-dir"],
@@ -3868,7 +3961,7 @@ and the pinsay-init skill uses them to mount the widget for you afterwards.
3868
3961
  await writeConfig(cwd2, configPatch);
3869
3962
  filesMod.push(".pinsay/config.json");
3870
3963
  if (writeLocalCreds) {
3871
- await writeCredentials(cwd2, key, { server, project: finalProjectKey });
3964
+ await writeCredentials(cwd2, key, { project: finalProjectKey });
3872
3965
  filesMod.push(".pinsay/credentials.env");
3873
3966
  }
3874
3967
  filesMod.push(".gitignore");
@@ -3912,7 +4005,7 @@ and the pinsay-init skill uses them to mount the widget for you afterwards.
3912
4005
  const cyan = (s) => `\x1B[36m${s}\x1B[0m`;
3913
4006
  const rule = dim("\u2500".repeat(60));
3914
4007
  const envLabel = environmentPinned ? envs.length > 1 ? envs.join(", ") : env : null;
3915
- const keyLine = keyLivesGlobally ? `this machine's global store ${dim("(~/.config/pointer/credentials.json)")}` : `.pinsay/credentials.env ${dim("(gitignored)")}`;
4008
+ const keyLine = keyLivesGlobally ? `this machine's global store ${dim("(~/.config/pinsay/credentials.json)")}` : `.pinsay/credentials.env ${dim("(gitignored)")}`;
3916
4009
  if (isJoin) {
3917
4010
  console.log(`
3918
4011
  ${rule}
@@ -3944,7 +4037,7 @@ ${rule}`);
3944
4037
  ${bold("Next")} Reviewers open the widget through the ${product} Chrome extension \u2014 nothing was injected.
3945
4038
 
3946
4039
  ${installLine}
3947
- 2. Extension \u2192 Options \u2192 set server ${server}; sign in with your ${product} account.
4040
+ 2. Open the extension and sign in with your ${product} account.
3948
4041
  3. Open your app, click the extension icon, choose project ${dim(finalProjectKey)}, Activate.
3949
4042
  4. Then ${bold("npx pinsay-cli list")} / ${bold("apply")} as usual.
3950
4043
  ${dim(`Dashboard: ${branding.urls?.app || server}`)}`);
@@ -4426,7 +4519,6 @@ async function handleMultiProjectSetup(args) {
4426
4519
  for (const r of results)
4427
4520
  projectsMap[r.key] = r.entry;
4428
4521
  await writeConfigFull(cwd2, {
4429
- server,
4430
4522
  aiTool: tool,
4431
4523
  skillsDir: (options["skills-dir"] || config.skillsDir) ?? void 0,
4432
4524
  cliVersion: BUILD_CLI_VERSION,
@@ -4434,7 +4526,7 @@ async function handleMultiProjectSetup(args) {
4434
4526
  projects: projectsMap
4435
4527
  });
4436
4528
  if (writeLocalCreds) {
4437
- await writeCredentials(cwd2, key, { server });
4529
+ await writeCredentials(cwd2, key);
4438
4530
  }
4439
4531
  await postEvent(server, token, {
4440
4532
  type: "installed",
@@ -4471,6 +4563,7 @@ import { promises as fs15 } from "node:fs";
4471
4563
  import { join as join15 } from "node:path";
4472
4564
  init_api();
4473
4565
  init_credentials();
4566
+ init_server();
4474
4567
  var ICON = { ok: "\u2714", warn: "\u26A0", error: "\u2718" };
4475
4568
  function exitCodeFor(checks) {
4476
4569
  const failed = checks.filter((c) => c.status === "error");
@@ -4500,11 +4593,12 @@ async function doctorCommand(cwd2, options, cliVersion) {
4500
4593
  }
4501
4594
  return 0;
4502
4595
  }
4503
- let checks = await runInitChecks(cwd2, { server: options.server, project: options.project }, cliVersion);
4596
+ const server = (options.server || resolveServer()).replace(/\/$/, "");
4597
+ let checks = await runInitChecks(cwd2, { server, project: options.project }, cliVersion);
4504
4598
  if (options.fix) {
4505
- const repaired = await applyFixes(cwd2, checks);
4599
+ const repaired = await applyFixes(cwd2, checks, server);
4506
4600
  if (repaired.length > 0) {
4507
- checks = await runInitChecks(cwd2, { server: options.server, project: options.project }, cliVersion);
4601
+ checks = await runInitChecks(cwd2, { server, project: options.project }, cliVersion);
4508
4602
  }
4509
4603
  }
4510
4604
  const code = exitCodeFor(checks);
@@ -4534,7 +4628,7 @@ async function reportRun(cwd2, options, checks, ok) {
4534
4628
  return;
4535
4629
  try {
4536
4630
  const config = await readConfig(cwd2);
4537
- const server = (options.server || config.server || "").replace(/\/$/, "");
4631
+ const server = (options.server || resolveServer()).replace(/\/$/, "");
4538
4632
  if (!server)
4539
4633
  return;
4540
4634
  const { key: apiKey } = await resolveApiKey(cwd2, server);
@@ -4551,10 +4645,9 @@ async function reportRun(cwd2, options, checks, ok) {
4551
4645
  } catch {
4552
4646
  }
4553
4647
  }
4554
- async function applyFixes(cwd2, checks) {
4648
+ async function applyFixes(cwd2, checks, server) {
4555
4649
  const repaired = [];
4556
4650
  const config = await readConfig(cwd2);
4557
- const server = (config.server || "").replace(/\/$/, "");
4558
4651
  let token;
4559
4652
  const tokenFor = async () => {
4560
4653
  if (token)
@@ -4626,6 +4719,7 @@ init_config();
4626
4719
  init_api();
4627
4720
  import { promises as fs16 } from "node:fs";
4628
4721
  import { dirname as dirname7, join as join16 } from "node:path";
4722
+ init_server();
4629
4723
  function sourceFor(path) {
4630
4724
  if (path.endsWith("pinsay.sh"))
4631
4725
  return "/pinsay.sh";
@@ -4649,9 +4743,9 @@ async function updateCommand(cwd2, options) {
4649
4743
  for (const f of removedLegacyFiles)
4650
4744
  console.log(`\x1B[2mremoved legacy ${f}\x1B[0m`);
4651
4745
  const config = await readConfig(cwd2);
4652
- const server = (options.server || config.server || "").replace(/\/$/, "");
4653
- if (!server) {
4654
- console.error("No server configured \u2014 run `npx -y pinsay-cli init` first.");
4746
+ const server = (options.server || resolveServer()).replace(/\/$/, "");
4747
+ if (!config.project && !isMultiProject(config)) {
4748
+ console.error("No .pinsay/config.json here \u2014 run `npx -y pinsay-cli init` first.");
4655
4749
  return 1;
4656
4750
  }
4657
4751
  let served = null;
@@ -4659,7 +4753,7 @@ async function updateCommand(cwd2, options) {
4659
4753
  const meta = await api(server, "/api/meta");
4660
4754
  served = meta?.skillVersion ?? null;
4661
4755
  } catch {
4662
- console.error(`Could not reach ${server} \u2014 check the URL.`);
4756
+ console.error(`Could not reach ${server}.`);
4663
4757
  return 1;
4664
4758
  }
4665
4759
  const files = skillFilesFor(config);
@@ -5823,6 +5917,7 @@ function describeFilter(filter) {
5823
5917
 
5824
5918
  // src/commands/apply.ts
5825
5919
  init_projection();
5920
+ init_server();
5826
5921
  async function exitIfWorkspaceFrozen(server, token) {
5827
5922
  if (!token)
5828
5923
  return;
@@ -5871,11 +5966,7 @@ function parseModelFlags(parsed) {
5871
5966
  async function applyCommand(cwd2, parsed, positionals = []) {
5872
5967
  const root = await findRepoRoot(cwd2);
5873
5968
  const config = await readConfig(root);
5874
- const server = ((typeof parsed["server"] === "string" ? parsed["server"] : config.server) || BUILD_DEFAULT_SERVER).replace(/\/$/, "");
5875
- if (!server) {
5876
- console.error("No server configured. Run `npx pinsay-cli init` or pass --server.");
5877
- process.exit(2);
5878
- }
5969
+ const server = resolveServer();
5879
5970
  const projectFlag = typeof parsed["project"] === "string" ? parsed["project"] : void 0;
5880
5971
  const resolved = resolveProject(config, cwd2, root, projectFlag);
5881
5972
  const markVal = parsed["mark"];
@@ -12468,10 +12559,11 @@ async function runMcpServer(ctx, logFile) {
12468
12559
  }
12469
12560
 
12470
12561
  // src/commands/mcp.ts
12562
+ init_server();
12471
12563
  async function mcpCommand(cwd2, parsed = {}) {
12472
12564
  const root = await findRepoRoot(cwd2);
12473
12565
  const config = await readConfig(root).catch(() => ({}));
12474
- const server = ((typeof parsed["server"] === "string" ? parsed["server"] : config.server) || canonicalServer(process.env.PINSAY_SERVER) || BUILD_DEFAULT_SERVER).replace(/\/$/, "");
12566
+ const server = resolveServer();
12475
12567
  const projectFlag = typeof parsed["project"] === "string" ? parsed["project"] : void 0;
12476
12568
  const project = projectFlag || config.project || process.env.PINSAY_PROJECT || "";
12477
12569
  const explicitKey = typeof parsed["key"] === "string" ? parsed["key"] : void 0;
@@ -12508,11 +12600,15 @@ async function mcpCommand(cwd2, parsed = {}) {
12508
12600
  init_config();
12509
12601
  init_api();
12510
12602
  init_credentials();
12511
- init_build_constants();
12603
+ init_server();
12512
12604
  async function loginCommand(cwd2, options = {}) {
12605
+ const flags = scopeFromFlags(options);
12606
+ if (flags.error) {
12607
+ console.error(flags.error);
12608
+ process.exit(2);
12609
+ }
12513
12610
  const root = await findRepoRoot(cwd2);
12514
- const config = await readConfig(root).catch(() => ({}));
12515
- const server = ((typeof options["server"] === "string" ? options["server"] : void 0) || config.server || canonicalServer(process.env.PINSAY_SERVER) || BUILD_DEFAULT_SERVER).replace(/\/$/, "");
12611
+ const server = resolveServer();
12516
12612
  const branding = await getBranding(server);
12517
12613
  const product = branding.productName;
12518
12614
  const flagKey = typeof options["key"] === "string" ? options["key"] : void 0;
@@ -12547,14 +12643,14 @@ async function loginCommand(cwd2, options = {}) {
12547
12643
  console.error("No key provided and no terminal to sign in from \u2014 run `npx pinsay-cli login --key <key>` or set PINSAY_API_KEY.");
12548
12644
  process.exit(2);
12549
12645
  }
12550
- const scope = typeof options["scope"] === "string" ? String(options["scope"]).toLowerCase() : "global";
12551
- if (scope !== "global" && scope !== "repo") {
12552
- console.error(`Invalid --scope "${options["scope"]}". Valid values: global, repo.`);
12553
- process.exit(2);
12554
- }
12646
+ const interactive = Boolean(process.stdin.isTTY) && options["yes"] !== true;
12647
+ const decided = decideKeyScope(flags.scope, interactive);
12648
+ const scope = decided === "ask" ? await askKeyScope() : decided;
12649
+ closePrompts();
12555
12650
  const who = me?.displayName ? `${me.displayName}${me?.email ? ` (${me.email})` : ""}` : me?.email ?? "you";
12556
12651
  if (scope === "repo") {
12557
- await writeCredentials(root, key, { server });
12652
+ await writeCredentials(root, key);
12653
+ await upsertGitignore(root, product);
12558
12654
  console.log(`\u2714 Signed in to ${server} as ${who} \u2014 saved to .pinsay/credentials.env (this repo only; overrides the global store here)`);
12559
12655
  process.exit(0);
12560
12656
  }
@@ -12564,13 +12660,10 @@ async function loginCommand(cwd2, options = {}) {
12564
12660
  }
12565
12661
 
12566
12662
  // src/commands/logout.ts
12567
- init_config();
12568
12663
  init_credentials();
12569
- init_build_constants();
12570
- async function logoutCommand(cwd2, options = {}) {
12571
- const root = await findRepoRoot(cwd2);
12572
- const config = await readConfig(root).catch(() => ({}));
12573
- const server = ((typeof options["server"] === "string" ? options["server"] : void 0) || config.server || canonicalServer(process.env.PINSAY_SERVER) || BUILD_DEFAULT_SERVER).replace(/\/$/, "");
12664
+ init_server();
12665
+ async function logoutCommand(_cwd, options = {}) {
12666
+ const server = resolveServer();
12574
12667
  const origin = normalizeServerOrigin(server);
12575
12668
  const removed = await removeGlobalCredential(server);
12576
12669
  if (options["json"] === true) {
@@ -12587,11 +12680,10 @@ async function logoutCommand(cwd2, options = {}) {
12587
12680
  init_config();
12588
12681
  init_api();
12589
12682
  init_credentials();
12590
- init_build_constants();
12683
+ init_server();
12591
12684
  async function whoamiCommand(cwd2, options = {}) {
12592
12685
  const root = await findRepoRoot(cwd2);
12593
- const config = await readConfig(root).catch(() => ({}));
12594
- const server = ((typeof options["server"] === "string" ? options["server"] : void 0) || config.server || canonicalServer(process.env.PINSAY_SERVER) || BUILD_DEFAULT_SERVER).replace(/\/$/, "");
12686
+ const server = resolveServer();
12595
12687
  const isJson = options["json"] === true;
12596
12688
  const { key, source } = await resolveApiKey(root, server);
12597
12689
  if (!key) {
@@ -12631,6 +12723,7 @@ async function whoamiCommand(cwd2, options = {}) {
12631
12723
  // src/cli.ts
12632
12724
  init_build_constants();
12633
12725
  init_api();
12726
+ init_server();
12634
12727
  import { argv, cwd } from "node:process";
12635
12728
  function parseArgs(args) {
12636
12729
  const parsed = {};
@@ -12687,8 +12780,8 @@ Usage: npx pinsay-cli <command> [options]
12687
12780
 
12688
12781
  Commands:
12689
12782
  init Set up the feedback widget in your project
12690
- login Authenticate once per machine (saves a key for every repo)
12691
- logout Remove this machine's saved key for a server
12783
+ login Sign in and save your API key (this machine, or this repo only)
12784
+ logout Remove this machine's saved key
12692
12785
  whoami Show the signed-in account and where the API key came from
12693
12786
  doctor Diagnose an install and report what is wrong
12694
12787
  update Refresh the served skills to the server's current version
@@ -12719,13 +12812,19 @@ async function main() {
12719
12812
  process.exit(0);
12720
12813
  }
12721
12814
  }
12815
+ if (command !== "map" && !parsed["help"]) {
12816
+ const serverError = await serverSettingError(cwd(), parsed);
12817
+ if (serverError) {
12818
+ console.error(serverError);
12819
+ process.exit(2);
12820
+ }
12821
+ }
12722
12822
  if (command === "init") {
12723
12823
  if (parsed["help"]) {
12724
12824
  console.log(`
12725
12825
  Usage: npx pinsay-cli init [options]
12726
12826
 
12727
12827
  Options:
12728
- --server <url> Feedback server URL
12729
12828
  --key <key> API key
12730
12829
  --project <key> Project key
12731
12830
  --create <name> Create project with name
@@ -12744,9 +12843,10 @@ Options:
12744
12843
  --no-skills Skip skills installation
12745
12844
  --no-design Skip design token detection
12746
12845
  --source-map Wire in the Vite plugin that stamps component source hashes
12747
- --scope <global|repo> Where the API key is stored: global (default \u2014 this machine, all repos,
12748
- ~/.config/pointer/credentials.json) or repo (.pinsay/credentials.env,
12749
- gitignored, this repo only). --local-credentials is an alias for --scope repo
12846
+ --scope <global|repo> Where the API key is stored, without asking: global (this machine, all
12847
+ repos, ~/.config/pinsay/credentials.json; the default with --yes) or
12848
+ repo (.pinsay/credentials.env, gitignored, this repo only).
12849
+ --local-credentials is an alias for --scope repo
12750
12850
  --no-browser When signing in in the browser (first run, no key resolved yet), print
12751
12851
  the link/code but don't try to open a browser
12752
12852
  -y, --yes Non-interactive
@@ -12761,21 +12861,21 @@ Options:
12761
12861
  console.log(`
12762
12862
  Usage: npx pinsay-cli login [options]
12763
12863
 
12764
- Authenticate once per machine and save the result to
12765
- ~/.config/pointer/credentials.json (honours $XDG_CONFIG_HOME / $PINSAY_CONFIG_DIR), keyed by
12766
- server. Every repo on this machine then resolves a key for that server without being asked again.
12864
+ Sign in and save your API key. On a terminal it asks where to save it:
12865
+ Global this machine, every repo (~/.config/pinsay/credentials.json, honours $XDG_CONFIG_HOME)
12866
+ Repo .pinsay/credentials.env in this repo only (gitignored)
12867
+ With no terminal (CI, a pipe) or with --yes it saves globally, unless --scope repo is given.
12767
12868
 
12768
12869
  With no --key on a real terminal, opens your browser to sign in (mirrors \`gh auth login\`): prints
12769
12870
  a link and a short code, waits for you to approve it in the dashboard, then saves the personal API
12770
- key it hands back. Pass --key to skip the browser and validate a pasted key instead, unchanged from
12771
- before.
12871
+ key it hands back. Pass --key to skip the browser and validate a pasted key instead.
12772
12872
 
12773
12873
  Options:
12774
- --server <url> Server URL (default: this repo's .pinsay/config.json, then $PINSAY_SERVER)
12775
12874
  --key <key> API key \u2014 skips the browser flow entirely
12776
12875
  --no-browser Print the sign-in link/code but don't try to open a browser
12777
- --scope <global|repo> global (default): save for every repo on this machine;
12778
- repo: write .pinsay/credentials.env in the current repo only
12876
+ --scope <global|repo> Where to save the key, without asking
12877
+ --local-credentials Alias for --scope repo
12878
+ -y, --yes Don't ask; save globally unless --scope repo is given
12779
12879
  -h, --help Show this help
12780
12880
  `);
12781
12881
  process.exit(0);
@@ -12786,10 +12886,9 @@ Options:
12786
12886
  console.log(`
12787
12887
  Usage: npx pinsay-cli logout [options]
12788
12888
 
12789
- Removes this machine's saved global key for a server.
12889
+ Removes this machine's saved global key.
12790
12890
 
12791
12891
  Options:
12792
- --server <url> Server URL (default: this repo's .pinsay/config.json, then $PINSAY_SERVER)
12793
12892
  --json Emit { ok, server, removed } as JSON
12794
12893
  -h, --help Show this help
12795
12894
  `);
@@ -12805,7 +12904,6 @@ Prints the server, the signed-in account, and which of env/repo/global answered
12805
12904
  never the key itself.
12806
12905
 
12807
12906
  Options:
12808
- --server <url> Server URL (default: this repo's .pinsay/config.json, then $PINSAY_SERVER)
12809
12907
  --json Emit { ok, server, displayName, email, source } as JSON
12810
12908
  -h, --help Show this help
12811
12909
  `);
@@ -12820,7 +12918,6 @@ Usage: npx pinsay-cli doctor [options]
12820
12918
  Checks an existing install and prints one line per check.
12821
12919
 
12822
12920
  Options:
12823
- --server <url> Override the server from .pinsay/config.json
12824
12921
  --project <key> Override the project key
12825
12922
  --json Emit { ok, checks } as JSON
12826
12923
  --fix Apply the idempotent repairs (gitignore, skills, stack)
@@ -12836,7 +12933,6 @@ Exit codes:
12836
12933
  process.exit(0);
12837
12934
  }
12838
12935
  const code = await doctorCommand(cwd(), {
12839
- server: typeof parsed["server"] === "string" ? parsed["server"] : void 0,
12840
12936
  project: typeof parsed["project"] === "string" ? parsed["project"] : void 0,
12841
12937
  json: parsed["json"] === true,
12842
12938
  fix: parsed["fix"] === true,
@@ -12848,17 +12944,15 @@ Exit codes:
12848
12944
  console.log(`
12849
12945
  Usage: npx pinsay-cli update [options]
12850
12946
 
12851
- Refreshes the AI skills and pinsay.sh from the configured server.
12947
+ Refreshes the AI skills and pinsay.sh from the server.
12852
12948
 
12853
12949
  Options:
12854
- --server <url> Override the server from .pinsay/config.json
12855
12950
  --check Report what is out of date without writing anything
12856
12951
  -h, --help Show this help
12857
12952
  `);
12858
12953
  process.exit(0);
12859
12954
  }
12860
12955
  const code = await updateCommand(cwd(), {
12861
- server: typeof parsed["server"] === "string" ? parsed["server"] : void 0,
12862
12956
  check: parsed["check"] === true
12863
12957
  });
12864
12958
  process.exit(code);
@@ -12979,7 +13073,6 @@ Start the PinSay stdio MCP server for AI tools.
12979
13073
 
12980
13074
  Options:
12981
13075
  --log <file> Log MCP server traffic to file
12982
- --server <url> Feedback server URL
12983
13076
  --project <key> Project key
12984
13077
  --key <key> API key
12985
13078
  -h, --help Show this help
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinsay-cli",
3
- "version": "0.7.3",
3
+ "version": "0.8.0",
4
4
  "description": "Click-to-comment feedback for your web app: install the widget, then turn pending comments into AI apply prompts.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -38,7 +38,7 @@
38
38
  "scripts": {
39
39
  "build": "node build.mjs",
40
40
  "typecheck": "tsc --noEmit",
41
- "test": "node --import tsx --test test/*.test.ts test/**/*.test.ts",
41
+ "test": "node --import tsx --import ./test/setup.ts --test test/*.test.ts test/**/*.test.ts",
42
42
  "prepublishOnly": "npm run typecheck && npm test && npm run build"
43
43
  },
44
44
  "devDependencies": {