pinsay-cli 0.7.3 → 0.8.1

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 +956 -379
  3. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -15,13 +15,13 @@ 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.1" : "0.0.0-dev";
19
19
  }
20
20
  });
21
21
 
22
22
  // src/config.ts
23
23
  import { promises as fs } from "node:fs";
24
- import { join, dirname, isAbsolute, sep } from "node:path";
24
+ import { join, dirname, resolve, sep } from "node:path";
25
25
  function isMultiProject(config) {
26
26
  return !!config.projects && Object.keys(config.projects).length > 0;
27
27
  }
@@ -70,7 +70,7 @@ function resolveProject(config, cwd2, root, flag) {
70
70
  return { ok: false, reason: "ambiguous", keys: projects.map((p) => p.key) };
71
71
  }
72
72
  function resolveAbs(p) {
73
- return isAbsolute(p) ? normalizeTrailingSlash(p) : normalizeTrailingSlash(join(process.cwd(), p));
73
+ return normalizeTrailingSlash(resolve(p));
74
74
  }
75
75
  function normalizeTrailingSlash(p) {
76
76
  return p.endsWith(sep) && p.length > 1 ? p.slice(0, -1) : p;
@@ -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 });
@@ -147,48 +145,7 @@ async function removeLegacyRepoFiles(cwd2) {
147
145
  }
148
146
  return removed;
149
147
  }
150
- async function upsertGitignore(cwd2, productName = "Feedback tool", skillsDir) {
151
- const file = join(cwd2, ".gitignore");
152
- let content = await fs.readFile(file, "utf8").catch(() => "");
153
- const before = content;
154
- const skillsDirPaths = skillsDir ? [`${skillsDir.replace(/\/+$/, "")}/pinsay-init/`, `${skillsDir.replace(/\/+$/, "")}/pinsay-feedback/`] : [];
155
- const allSkillPaths = [...IGNORED_SKILL_DIRS, ...skillsDirPaths];
156
- const entry = [
157
- "",
158
- `# ${productName}`,
159
- // `.pinsay/*`, not `.pinsay/`. Git does not descend into an excluded DIRECTORY, so the
160
- // directory form makes every `!` line below inert and the files this block exists to keep
161
- // committable are silently ignored instead.
162
- ".pinsay/*",
163
- "!.pinsay/stack.json",
164
- "!.pinsay/config.json",
165
- // Re-includes the directory itself (not a wildcard for its contents) — with nothing further
166
- // ignoring paths inside it, git descends and every `projects/<key>.stack.json` stays
167
- // committable, exactly like stack.json/config.json above.
168
- "!.pinsay/projects/",
169
- ...allSkillPaths,
170
- ""
171
- ].join("\n");
172
- content = content.replace(/^\.pinsay\/$/m, ".pinsay/*");
173
- content = content.replace(/\n?# [^\n]*\n\.pinsay\/credentials\.env\n/, "");
174
- content = content.replace(/^!\.pinsay\/pinsay\.sh\n/m, "");
175
- content = content.replace(/^!\.pinsay\/credentials\.env\.example\n/m, "");
176
- if (!/^!\.pinsay\/stack\.json$/m.test(content)) {
177
- content += entry;
178
- }
179
- const missingSkillDirs = allSkillPaths.filter((d) => !content.includes(d));
180
- if (missingSkillDirs.length > 0) {
181
- content += missingSkillDirs.map((d) => `${d}
182
- `).join("");
183
- }
184
- if (!content.includes("!.pinsay/projects/")) {
185
- content += "!.pinsay/projects/\n";
186
- }
187
- if (content !== before) {
188
- await fs.writeFile(file, content, "utf8");
189
- }
190
- }
191
- var CONFIG_FILE, CREDENTIALS_FILE, LEGACY_SERVER, CANONICAL_SERVER, IGNORED_SKILL_DIRS, LEGACY_REPO_FILES;
148
+ var CONFIG_FILE, CREDENTIALS_FILE, LEGACY_SERVER, CANONICAL_SERVER, LEGACY_REPO_FILES;
192
149
  var init_config = __esm({
193
150
  "src/config.ts"() {
194
151
  "use strict";
@@ -196,22 +153,6 @@ var init_config = __esm({
196
153
  CREDENTIALS_FILE = ".pinsay/credentials.env";
197
154
  LEGACY_SERVER = "https://api.pinsay.dev";
198
155
  CANONICAL_SERVER = "https://app.pinsay.dev";
199
- IGNORED_SKILL_DIRS = [
200
- ".claude/skills/pinsay-init/",
201
- ".claude/skills/pinsay-feedback/",
202
- // Current Agent Skills layout (2026-09-16+) — used by `other`/`antigravity`, and symlinked into
203
- // from claude-code/cursor/windsurf (see `writeOrLink` in skills.ts).
204
- ".agents/skills/pinsay-init/",
205
- ".agents/skills/pinsay-feedback/",
206
- // Legacy pre-2026-09-16 layout. `installSkills` removes these on every install, but a repo that
207
- // has not run init/update since still has them on disk until it does — keep ignoring them too.
208
- ".agents/pinsay-init/",
209
- ".agents/pinsay-feedback/",
210
- ".cursor/rules/pinsay-init.md",
211
- ".cursor/rules/pinsay-feedback.md",
212
- ".windsurf/rules/pinsay-init.md",
213
- ".windsurf/rules/pinsay-feedback.md"
214
- ];
215
156
  LEGACY_REPO_FILES = [".pinsay/credentials.env.example", ".pinsay/.token_cache"];
216
157
  }
217
158
  });
@@ -258,10 +199,10 @@ var init_api = __esm({
258
199
  });
259
200
 
260
201
  // src/credentials.ts
261
- import { promises as fs10 } from "node:fs";
202
+ import { promises as fs12 } from "node:fs";
262
203
  import { createHash } from "node:crypto";
263
204
  import { homedir } from "node:os";
264
- import { join as join10, dirname as dirname4 } from "node:path";
205
+ import { join as join11, dirname as dirname5 } from "node:path";
265
206
  function sourceLabel(source) {
266
207
  if (source === "env")
267
208
  return "env var";
@@ -278,43 +219,88 @@ function normalizeServerOrigin(server) {
278
219
  return server.replace(/\/+$/, "");
279
220
  }
280
221
  }
222
+ function configBase() {
223
+ if (process.platform === "win32") {
224
+ return process.env.APPDATA || join11(homedir(), "AppData", "Roaming");
225
+ }
226
+ return process.env.XDG_CONFIG_HOME || join11(homedir(), ".config");
227
+ }
281
228
  function globalConfigDir() {
282
229
  if (process.env.PINSAY_CONFIG_DIR)
283
230
  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");
231
+ return join11(configBase(), DIR_NAME);
288
232
  }
289
233
  function globalCredentialsPath() {
290
- return join10(globalConfigDir(), "credentials.json");
234
+ return join11(globalConfigDir(), "credentials.json");
235
+ }
236
+ function legacyGlobalCredentialsPath() {
237
+ if (process.env.PINSAY_CONFIG_DIR)
238
+ return void 0;
239
+ return join11(configBase(), LEGACY_DIR_NAME, "credentials.json");
291
240
  }
292
241
  function globalCacheDir() {
293
242
  if (process.env.PINSAY_CONFIG_DIR)
294
- return join10(process.env.PINSAY_CONFIG_DIR, "cache");
243
+ return join11(process.env.PINSAY_CONFIG_DIR, "cache");
295
244
  if (process.platform === "win32") {
296
- return join10(process.env.LOCALAPPDATA || join10(homedir(), "AppData", "Local"), "pointer", "cache");
245
+ return join11(process.env.LOCALAPPDATA || join11(homedir(), "AppData", "Local"), DIR_NAME, "cache");
297
246
  }
298
- return join10(process.env.XDG_CACHE_HOME || join10(homedir(), ".cache"), "pointer");
247
+ return join11(process.env.XDG_CACHE_HOME || join11(homedir(), ".cache"), DIR_NAME);
299
248
  }
300
249
  function tokenCacheFile(server, apiKey) {
301
250
  const hash = createHash("sha256").update(`${normalizeServerOrigin(server)}:${apiKey}`).digest("hex").slice(0, 32);
302
- return join10(globalCacheDir(), `${hash}.json`);
251
+ return join11(globalCacheDir(), `${hash}.json`);
252
+ }
253
+ function isCredentialStore(value) {
254
+ if (!value || typeof value !== "object" || Array.isArray(value))
255
+ return false;
256
+ return Object.values(value).every(
257
+ (entry) => !!entry && typeof entry === "object" && typeof entry.apiKey === "string"
258
+ );
303
259
  }
304
260
  async function readGlobalStore() {
261
+ let raw;
262
+ try {
263
+ raw = await fs12.readFile(globalCredentialsPath(), "utf8");
264
+ } catch (err) {
265
+ if (err?.code === "ENOENT")
266
+ return migrateLegacyStore();
267
+ return {};
268
+ }
305
269
  try {
306
- const raw = await fs10.readFile(globalCredentialsPath(), "utf8");
307
270
  const parsed = JSON.parse(raw);
308
271
  return parsed && typeof parsed === "object" ? parsed : {};
309
272
  } catch {
310
273
  return {};
311
274
  }
312
275
  }
276
+ async function migrateLegacyStore() {
277
+ const legacyFile = legacyGlobalCredentialsPath();
278
+ if (!legacyFile)
279
+ return {};
280
+ let legacy;
281
+ try {
282
+ legacy = JSON.parse(await fs12.readFile(legacyFile, "utf8"));
283
+ } catch {
284
+ return {};
285
+ }
286
+ if (!isCredentialStore(legacy))
287
+ return {};
288
+ try {
289
+ await writeGlobalStore(legacy);
290
+ } catch {
291
+ return legacy;
292
+ }
293
+ await fs12.rm(legacyFile, { force: true }).catch(() => {
294
+ });
295
+ await fs12.rmdir(dirname5(legacyFile)).catch(() => {
296
+ });
297
+ return legacy;
298
+ }
313
299
  async function writeGlobalStore(store) {
314
300
  const file = globalCredentialsPath();
315
- await fs10.mkdir(dirname4(file), { recursive: true });
316
- await fs10.writeFile(file, JSON.stringify(store, null, 2) + "\n", { encoding: "utf8", mode: 384 });
317
- await fs10.chmod(file, 384).catch(() => {
301
+ await fs12.mkdir(dirname5(file), { recursive: true });
302
+ await fs12.writeFile(file, JSON.stringify(store, null, 2) + "\n", { encoding: "utf8", mode: 384 });
303
+ await fs12.chmod(file, 384).catch(() => {
318
304
  });
319
305
  }
320
306
  async function getGlobalCredential(server) {
@@ -332,15 +318,18 @@ async function saveGlobalCredential(server, entry) {
332
318
  async function removeGlobalCredential(server) {
333
319
  const store = await readGlobalStore();
334
320
  const origin = normalizeServerOrigin(server);
335
- if (!(origin in store))
321
+ const origins = origin === "https://app.pinsay.dev" ? [origin, "https://api.pinsay.dev"] : [origin];
322
+ const present = origins.filter((o) => o in store);
323
+ if (present.length === 0)
336
324
  return false;
337
- delete store[origin];
325
+ for (const o of present)
326
+ delete store[o];
338
327
  await writeGlobalStore(store);
339
328
  return true;
340
329
  }
341
330
  async function readRepoApiKey(root) {
342
331
  try {
343
- const raw = await fs10.readFile(join10(root, ".pinsay", "credentials.env"), "utf8");
332
+ const raw = await fs12.readFile(join11(root, ".pinsay", "credentials.env"), "utf8");
344
333
  return raw.match(/^PINSAY_API_KEY=(.*)$/m)?.[1]?.trim() || void 0;
345
334
  } catch {
346
335
  return void 0;
@@ -361,12 +350,42 @@ async function resolveApiKey(root, server) {
361
350
  return { key: void 0, source: null };
362
351
  }
363
352
  async function removeStaleRepoTokenCache(root) {
364
- await fs10.rm(join10(root, ".pinsay", ".token_cache"), { force: true }).catch(() => {
353
+ await fs12.rm(join11(root, ".pinsay", ".token_cache"), { force: true }).catch(() => {
365
354
  });
366
355
  }
356
+ var DIR_NAME, LEGACY_DIR_NAME;
367
357
  var init_credentials = __esm({
368
358
  "src/credentials.ts"() {
369
359
  "use strict";
360
+ DIR_NAME = "pinsay";
361
+ LEGACY_DIR_NAME = "pointer";
362
+ }
363
+ });
364
+
365
+ // src/server.ts
366
+ function resolveServer() {
367
+ const override = process.env.PINSAY_SERVER?.trim();
368
+ return canonicalServer(override || BUILD_DEFAULT_SERVER).replace(/\/+$/, "");
369
+ }
370
+ async function serverSettingError(cwd2, parsed) {
371
+ const server = resolveServer();
372
+ if (parsed["server"] !== void 0) {
373
+ return `--server was removed: this CLI talks only to ${server}.`;
374
+ }
375
+ const root = await findRepoRoot(cwd2);
376
+ const config = await readConfig(root).catch(() => ({}));
377
+ if (config.server && normalizeServerOrigin(config.server) !== normalizeServerOrigin(server)) {
378
+ return `.pinsay/config.json sets "server": "${config.server}", but this CLI talks only to ${server}.
379
+ Remove the "server" line from .pinsay/config.json, then run the command again.`;
380
+ }
381
+ return null;
382
+ }
383
+ var init_server = __esm({
384
+ "src/server.ts"() {
385
+ "use strict";
386
+ init_build_constants();
387
+ init_config();
388
+ init_credentials();
370
389
  }
371
390
  });
372
391
 
@@ -562,8 +581,8 @@ __export(map_exports, {
562
581
  isSameManifest: () => isSameManifest,
563
582
  mapCommand: () => mapCommand
564
583
  });
565
- import { promises as fs14 } from "node:fs";
566
- import { join as join14, relative as relative4, resolve as resolve3 } from "node:path";
584
+ import { promises as fs16 } from "node:fs";
585
+ import { join as join15, relative as relative6, resolve as resolve7 } from "node:path";
567
586
  import { execFileSync } from "node:child_process";
568
587
  function gitRoot(cwd2) {
569
588
  try {
@@ -575,7 +594,7 @@ function gitRoot(cwd2) {
575
594
  async function walk(dir, out = []) {
576
595
  let entries;
577
596
  try {
578
- entries = await fs14.readdir(dir, { withFileTypes: true });
597
+ entries = await fs16.readdir(dir, { withFileTypes: true });
579
598
  } catch {
580
599
  return out;
581
600
  }
@@ -584,7 +603,7 @@ async function walk(dir, out = []) {
584
603
  if (SKIP_DIRS.has(entry.name))
585
604
  continue;
586
605
  }
587
- const full = join14(dir, entry.name);
606
+ const full = join15(dir, entry.name);
588
607
  if (entry.isDirectory()) {
589
608
  if (SKIP_DIRS.has(entry.name))
590
609
  continue;
@@ -636,10 +655,10 @@ async function buildManifest(cwd2, opts = {}) {
636
655
  let scanned = 0;
637
656
  let failed = 0;
638
657
  for (const file of files) {
639
- const relPath = toPosix(relative4(root, file));
658
+ const relPath = toPosix(relative6(root, file));
640
659
  let code;
641
660
  try {
642
- code = await fs14.readFile(file, "utf8");
661
+ code = await fs16.readFile(file, "utf8");
643
662
  } catch {
644
663
  continue;
645
664
  }
@@ -655,25 +674,25 @@ async function buildManifest(cwd2, opts = {}) {
655
674
  console.error(` skipped ${relPath}: ${err?.message ?? err}`);
656
675
  }
657
676
  }
658
- const target = resolve3(root, ".pinsay/manifest.json");
659
- await fs14.mkdir(join14(root, ".pinsay"), { recursive: true });
677
+ const target = resolve7(root, ".pinsay/manifest.json");
678
+ await fs16.mkdir(join15(root, ".pinsay"), { recursive: true });
660
679
  const entries = Object.fromEntries(
661
680
  Object.entries(manifest).sort(([a], [b]) => a.localeCompare(b)).map(([hash, entry]) => [hash, { path: entry.path, component: entry.export }])
662
681
  );
663
682
  const nextContent = JSON.stringify({ version: 1, entries }, null, 2) + "\n";
664
683
  let currentContent = null;
665
684
  try {
666
- currentContent = await fs14.readFile(target, "utf8");
685
+ currentContent = await fs16.readFile(target, "utf8");
667
686
  } catch {
668
687
  currentContent = null;
669
688
  }
670
689
  const prev = target.replace(/\.json$/, ".prev.json");
671
690
  if (currentContent !== null && !isSameManifest(currentContent, nextContent)) {
672
- await fs14.copyFile(target, prev);
691
+ await fs16.copyFile(target, prev);
673
692
  }
674
693
  const tmp = `${target}.tmp`;
675
- await fs14.writeFile(tmp, nextContent, "utf8");
676
- await fs14.rename(tmp, target);
694
+ await fs16.writeFile(tmp, nextContent, "utf8");
695
+ await fs16.rename(tmp, target);
677
696
  const count = Object.keys(entries).length;
678
697
  if (!opts.quiet) {
679
698
  console.log(
@@ -705,7 +724,7 @@ var init_map = __esm({
705
724
  });
706
725
 
707
726
  // src/auth.ts
708
- import { promises as fs18 } from "node:fs";
727
+ import { promises as fs20 } from "node:fs";
709
728
  import { dirname as dirname8 } from "node:path";
710
729
  async function readApiKey(cwd2, server) {
711
730
  const { key } = await resolveApiKey(cwd2, server);
@@ -730,7 +749,7 @@ async function resolveToken(server, cwd2, explicitApiKey) {
730
749
  return void 0;
731
750
  const cacheFile = tokenCacheFile(server, apiKey);
732
751
  try {
733
- const cached = JSON.parse(await fs18.readFile(cacheFile, "utf8"));
752
+ const cached = JSON.parse(await fs20.readFile(cacheFile, "utf8"));
734
753
  const token = typeof cached?.token === "string" ? cached.token.trim() : "";
735
754
  if (token && !isJwtExpired(token))
736
755
  return token;
@@ -747,8 +766,8 @@ async function resolveToken(server, cwd2, explicitApiKey) {
747
766
  );
748
767
  if (login?.token) {
749
768
  try {
750
- await fs18.mkdir(dirname8(cacheFile), { recursive: true });
751
- await fs18.writeFile(cacheFile, JSON.stringify({ token: login.token }), "utf8");
769
+ await fs20.mkdir(dirname8(cacheFile), { recursive: true });
770
+ await fs20.writeFile(cacheFile, JSON.stringify({ token: login.token }), "utf8");
752
771
  } catch {
753
772
  }
754
773
  return login.token;
@@ -931,11 +950,7 @@ function mapEnvironmentToString(env) {
931
950
  async function getClient(cwd2, parsed, opts = {}) {
932
951
  const root = await findRepoRoot(cwd2);
933
952
  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
- }
953
+ const server = resolveServer();
939
954
  const explicitKey = typeof parsed["key"] === "string" ? parsed["key"] : void 0;
940
955
  const token = await resolveToken(server, root, explicitKey);
941
956
  const apiKey = explicitKey || await readApiKey(root, server);
@@ -1145,8 +1160,8 @@ var init_comments = __esm({
1145
1160
  init_resolve();
1146
1161
  init_api();
1147
1162
  init_auth();
1148
- init_build_constants();
1149
1163
  init_projection();
1164
+ init_server();
1150
1165
  }
1151
1166
  });
1152
1167
 
@@ -1261,7 +1276,7 @@ function assertInteractive() {
1261
1276
  if (process.stdin.isTTY)
1262
1277
  return;
1263
1278
  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."
1279
+ "\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
1280
  );
1266
1281
  process.exit(2);
1267
1282
  }
@@ -1354,7 +1369,7 @@ async function menu(question, items, cursorStart, opts) {
1354
1369
  process.stdin.resume();
1355
1370
  render(true);
1356
1371
  try {
1357
- return await new Promise((resolve5) => {
1372
+ return await new Promise((resolve9) => {
1358
1373
  const onKey = (_str, key) => {
1359
1374
  if (key.ctrl && key.name === "c") {
1360
1375
  cleanup();
@@ -1381,7 +1396,7 @@ async function menu(question, items, cursorStart, opts) {
1381
1396
  selected.add(cursor);
1382
1397
  }
1383
1398
  cleanup();
1384
- resolve5(opts.multi ? [...selected].sort((a, b) => a - b) : [cursor]);
1399
+ resolve9(opts.multi ? [...selected].sort((a, b) => a - b) : [cursor]);
1385
1400
  }
1386
1401
  };
1387
1402
  const cleanup = () => {
@@ -1863,10 +1878,10 @@ ${indent}${call},` + rest.replace(/^\s*\n/, "\n") : next.slice(0, at) + `${call}
1863
1878
  const envPath = join6(cwd2, envName);
1864
1879
  const envBefore = await fs6.readFile(envPath, "utf8").catch(() => "");
1865
1880
  if (!/^VITE_PINSAY_SOURCE=/m.test(envBefore)) {
1866
- const sep3 = envBefore && !envBefore.endsWith("\n") ? "\n" : "";
1881
+ const sep6 = envBefore && !envBefore.endsWith("\n") ? "\n" : "";
1867
1882
  await fs6.writeFile(
1868
1883
  envPath,
1869
- `${envBefore}${sep3}# Stamps component source hashes for PinSay. Development only.
1884
+ `${envBefore}${sep6}# Stamps component source hashes for PinSay. Development only.
1870
1885
  VITE_PINSAY_SOURCE=true
1871
1886
  `,
1872
1887
  "utf8"
@@ -1887,34 +1902,317 @@ async function firstExisting2(cwd2, names) {
1887
1902
  }
1888
1903
 
1889
1904
  // src/skills.ts
1905
+ import { promises as fs8 } from "node:fs";
1906
+ import { join as join8, posix, relative as relative3, resolve as resolve3, sep as sep3, isAbsolute as isAbsolute2 } from "node:path";
1907
+
1908
+ // src/lib/repo-paths.ts
1890
1909
  import { promises as fs7 } from "node:fs";
1891
- import { join as join7, dirname as dirname2 } from "node:path";
1892
- async function fetchText(url) {
1893
- const res = await fetch(url);
1894
- if (!res.ok)
1895
- throw new Error(`Failed to fetch ${url}: ${res.status}`);
1896
- return res.text();
1910
+ import { execFile } from "node:child_process";
1911
+ import { promisify } from "node:util";
1912
+ import { dirname as dirname2, isAbsolute, join as join7, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
1913
+ var execFileAsync = promisify(execFile);
1914
+ var RepoPathError = class extends Error {
1915
+ kind;
1916
+ /** Repo-relative path of what is in the way, always with `/`. */
1917
+ blocker;
1918
+ /** What the user should do, as commands that work in PowerShell, cmd and POSIX shells. */
1919
+ hint;
1920
+ constructor(kind, blocker, message, hint) {
1921
+ super(message);
1922
+ this.name = "RepoPathError";
1923
+ this.kind = kind;
1924
+ this.blocker = blocker;
1925
+ this.hint = hint;
1926
+ }
1927
+ };
1928
+ var MAX_HOPS = 8;
1929
+ var STUB_MAX_BYTES = 1024;
1930
+ var THEN_UPDATE = 'Then run "npx pinsay-cli update".';
1931
+ function toSlash(p) {
1932
+ return p.split(sep2).join("/");
1933
+ }
1934
+ function segmentsBelow(base, p) {
1935
+ const rel = relative2(base, p);
1936
+ if (rel === "")
1937
+ return [];
1938
+ if (rel === ".." || rel.startsWith(`..${sep2}`) || isAbsolute(rel))
1939
+ return null;
1940
+ return rel.split(sep2).filter(Boolean);
1941
+ }
1942
+ async function rootForms(root) {
1943
+ const abs = resolve2(root);
1944
+ const real = await fs7.realpath(abs).catch(() => abs);
1945
+ return real === abs ? [abs] : [abs, real];
1897
1946
  }
1898
- async function download(url, dest, chmod = false) {
1899
- const txt = await fetchText(url);
1900
- await fs7.mkdir(dirname2(dest), { recursive: true });
1901
- await fs7.writeFile(dest, txt, "utf8");
1902
- if (chmod) {
1903
- await fs7.chmod(dest, 493).catch(() => {
1947
+ function segmentsInRepo(roots, target) {
1948
+ for (const r of roots) {
1949
+ const s = segmentsBelow(r, target);
1950
+ if (s !== null)
1951
+ return s;
1952
+ }
1953
+ return null;
1954
+ }
1955
+ function samePath(a, b) {
1956
+ const x = resolve2(a);
1957
+ const y = resolve2(b);
1958
+ return process.platform === "win32" ? x.toLowerCase() === y.toLowerCase() : x === y;
1959
+ }
1960
+ async function realOr(p) {
1961
+ return fs7.realpath(p).catch(() => resolve2(p));
1962
+ }
1963
+ async function isGitSymlink(rootAbs, abs) {
1964
+ try {
1965
+ const { stdout } = await execFileAsync("git", ["ls-files", "-s", "--", toSlash(relative2(rootAbs, abs))], {
1966
+ cwd: rootAbs,
1967
+ windowsHide: true
1904
1968
  });
1969
+ return stdout.startsWith("120000 ");
1970
+ } catch {
1971
+ return false;
1972
+ }
1973
+ }
1974
+ async function linkStubTarget(rootAbs, abs) {
1975
+ let st;
1976
+ try {
1977
+ st = await fs7.lstat(abs);
1978
+ } catch {
1979
+ return null;
1980
+ }
1981
+ if (!st.isFile() || st.size === 0 || st.size > STUB_MAX_BYTES)
1982
+ return null;
1983
+ const text = (await fs7.readFile(abs, "utf8")).replace(/\r?\n$/, "");
1984
+ if (text === "" || /[\r\n\0]/.test(text))
1985
+ return null;
1986
+ const target = resolve2(dirname2(abs), text.replace(/\\/g, "/"));
1987
+ if (await isGitSymlink(rootAbs, abs))
1988
+ return target;
1989
+ try {
1990
+ if ((await fs7.stat(target)).isDirectory())
1991
+ return target;
1992
+ } catch {
1993
+ }
1994
+ return null;
1995
+ }
1996
+ function fileInTheWay(blocker, what) {
1997
+ return new RepoPathError(
1998
+ "file-in-the-way",
1999
+ blocker,
2000
+ `${blocker} ${what}, not a folder, so the skills can't go inside it. PinSay left it unchanged.`,
2001
+ `If ${blocker} is a Git symlink checked out as a plain file (Windows without symlink support): turn on Developer Mode, then run "git config core.symlinks true" and "git checkout -- ${blocker}". Otherwise move it away. ${THEN_UPDATE}`
2002
+ );
2003
+ }
2004
+ function outsideRepo(blocker, target) {
2005
+ return new RepoPathError(
2006
+ "link-outside-repo",
2007
+ blocker,
2008
+ `${blocker} points to ${target}, outside this repository. PinSay only writes inside the repository and left it unchanged.`,
2009
+ `Fix or remove the link, or choose another folder with "npx pinsay-cli init --skills-dir <folder>". ${THEN_UPDATE}`
2010
+ );
2011
+ }
2012
+ function linkLoop(blocker) {
2013
+ return new RepoPathError(
2014
+ "link-loop",
2015
+ blocker,
2016
+ `${blocker} starts a chain of links PinSay could not follow (more than ${MAX_HOPS} links, or a loop). PinSay left it unchanged.`,
2017
+ `Fix the links. ${THEN_UPDATE}`
2018
+ );
2019
+ }
2020
+ function dirInTheWay(blocker) {
2021
+ return new RepoPathError(
2022
+ "directory-in-the-way",
2023
+ blocker,
2024
+ `${blocker} is a folder where PinSay writes a file. PinSay left it unchanged.`,
2025
+ `Move the folder away. ${THEN_UPDATE}`
2026
+ );
2027
+ }
2028
+ function ioError(blocker, err) {
2029
+ const msg = err instanceof Error ? err.message : String(err);
2030
+ return new RepoPathError(
2031
+ "io-error",
2032
+ blocker,
2033
+ `could not write ${blocker} (${msg}).`,
2034
+ `Check the folder's permissions, or close programs that lock it. ${THEN_UPDATE}`
2035
+ );
2036
+ }
2037
+ async function resolveRepoPath(root, rel, opts) {
2038
+ const roots = await rootForms(root);
2039
+ const rootAbs = roots[0];
2040
+ const naive = resolve2(rootAbs, rel.replace(/\\/g, "/"));
2041
+ const below = segmentsBelow(rootAbs, naive);
2042
+ if (below === null || below.length === 0) {
2043
+ if (opts.create)
2044
+ await fs7.mkdir(dirname2(naive), { recursive: true });
2045
+ return { abs: naive };
2046
+ }
2047
+ let segments = below;
2048
+ const fileName = segments.pop();
2049
+ const shown = (p) => toSlash(relative2(rootAbs, p)) || ".";
2050
+ let cur = rootAbs;
2051
+ let i = 0;
2052
+ let hops = 0;
2053
+ let redirectedVia;
2054
+ while (i < segments.length) {
2055
+ const next = join7(cur, segments[i]);
2056
+ let st;
2057
+ try {
2058
+ st = await fs7.lstat(next);
2059
+ } catch (err) {
2060
+ if (err?.code !== "ENOENT")
2061
+ throw ioError(shown(next), err);
2062
+ if (!opts.create)
2063
+ return { abs: join7(next, ...segments.slice(i + 1), fileName), redirectedVia };
2064
+ try {
2065
+ await fs7.mkdir(next);
2066
+ } catch (e) {
2067
+ if (e?.code !== "EEXIST")
2068
+ throw ioError(shown(next), e);
2069
+ }
2070
+ continue;
2071
+ }
2072
+ if (st.isDirectory()) {
2073
+ cur = next;
2074
+ i++;
2075
+ continue;
2076
+ }
2077
+ let target;
2078
+ if (st.isSymbolicLink()) {
2079
+ const live = await fs7.stat(next).catch(() => null);
2080
+ if (live?.isDirectory()) {
2081
+ cur = next;
2082
+ i++;
2083
+ continue;
2084
+ }
2085
+ if (live)
2086
+ throw fileInTheWay(shown(next), "points to a file");
2087
+ let raw;
2088
+ try {
2089
+ raw = await fs7.readlink(next);
2090
+ } catch (e) {
2091
+ throw ioError(shown(next), e);
2092
+ }
2093
+ target = resolve2(dirname2(next), raw);
2094
+ } else if (st.isFile()) {
2095
+ const stub = await linkStubTarget(rootAbs, next);
2096
+ if (stub === null)
2097
+ throw fileInTheWay(shown(next), "is a file");
2098
+ target = stub;
2099
+ } else {
2100
+ throw fileInTheWay(shown(next), "is not a folder");
2101
+ }
2102
+ const targetSegments = segmentsInRepo(roots, target);
2103
+ if (targetSegments === null)
2104
+ throw outsideRepo(shown(next), target);
2105
+ hops++;
2106
+ if (hops > MAX_HOPS)
2107
+ throw linkLoop(redirectedVia ?? shown(next));
2108
+ redirectedVia ??= shown(next);
2109
+ segments = [...targetSegments, ...segments.slice(i + 1)];
2110
+ cur = rootAbs;
2111
+ i = 0;
2112
+ }
2113
+ return { abs: join7(cur, fileName), redirectedVia };
2114
+ }
2115
+ async function writeRepoFile(root, rel, content) {
2116
+ const r = await resolveRepoPath(root, rel, { create: true });
2117
+ const shown = toSlash(relative2(resolve2(root), r.abs));
2118
+ const st = await fs7.lstat(r.abs).catch(() => null);
2119
+ if (st?.isDirectory())
2120
+ throw dirInTheWay(shown);
2121
+ try {
2122
+ if (st?.isSymbolicLink() && !await fs7.stat(r.abs).catch(() => null))
2123
+ await fs7.unlink(r.abs);
2124
+ await fs7.writeFile(r.abs, content, "utf8");
2125
+ } catch (err) {
2126
+ throw ioError(shown, err);
1905
2127
  }
2128
+ return r;
1906
2129
  }
1907
- async function makeSymlink(target, path) {
1908
- await fs7.mkdir(dirname2(path), { recursive: true });
2130
+ async function linkOrCopy(root, sourceAbs, destRel) {
2131
+ const dest = await resolveRepoPath(root, destRel, { create: true });
2132
+ const shown = toSlash(relative2(resolve2(root), dest.abs));
2133
+ const sourceReal = await realOr(sourceAbs);
2134
+ const st = await fs7.lstat(dest.abs).catch(() => null);
2135
+ if (st && samePath(await realOr(dest.abs), sourceReal) && !st.isSymbolicLink()) {
2136
+ return { abs: dest.abs, mode: "same" };
2137
+ }
2138
+ if (st?.isDirectory())
2139
+ throw dirInTheWay(shown);
1909
2140
  try {
1910
- await fs7.symlink(target, path);
2141
+ if (st?.isSymbolicLink()) {
2142
+ const real = await fs7.realpath(dest.abs).catch(() => null);
2143
+ if (real !== null && samePath(real, sourceReal))
2144
+ return { abs: dest.abs, mode: "link" };
2145
+ await fs7.unlink(dest.abs);
2146
+ } else if (st) {
2147
+ await fs7.unlink(dest.abs);
2148
+ }
1911
2149
  } catch (err) {
1912
- if (err.code === "EPERM" && process.platform === "win32") {
1913
- await fs7.copyFile(target, path);
1914
- } else if (err.code !== "EEXIST") {
1915
- throw err;
2150
+ throw ioError(shown, err);
2151
+ }
2152
+ try {
2153
+ await fs7.symlink(relative2(dirname2(dest.abs), sourceAbs), dest.abs, "file");
2154
+ return { abs: dest.abs, mode: "link" };
2155
+ } catch {
2156
+ try {
2157
+ await fs7.copyFile(sourceAbs, dest.abs);
2158
+ } catch (err) {
2159
+ throw ioError(shown, err);
1916
2160
  }
2161
+ return { abs: dest.abs, mode: "copy" };
2162
+ }
2163
+ }
2164
+
2165
+ // src/skills.ts
2166
+ var DownloadError = class extends Error {
2167
+ reason;
2168
+ constructor(url, reason) {
2169
+ super(`Failed to fetch ${url}: ${reason}`);
2170
+ this.name = "DownloadError";
2171
+ this.reason = reason;
2172
+ }
2173
+ };
2174
+ async function fetchText(url) {
2175
+ let res;
2176
+ try {
2177
+ res = await fetch(url);
2178
+ } catch (err) {
2179
+ throw new DownloadError(url, err?.message ?? String(err));
2180
+ }
2181
+ if (!res.ok)
2182
+ throw new DownloadError(url, `HTTP ${res.status}`);
2183
+ return res.text();
2184
+ }
2185
+ function toWarning(tool, path, server, err) {
2186
+ if (err instanceof RepoPathError)
2187
+ return { tool, path, message: err.message, hint: err.hint };
2188
+ if (err instanceof DownloadError) {
2189
+ return {
2190
+ tool,
2191
+ path,
2192
+ message: `could not download the skill files from ${server} (${err.reason}).`,
2193
+ hint: 'Check your connection. Then run "npx pinsay-cli update".'
2194
+ };
1917
2195
  }
2196
+ const msg = err instanceof Error ? err.message : String(err);
2197
+ return { tool, path, message: `could not install it (${msg}).`, hint: 'Run "npx pinsay-cli update" to try again.' };
2198
+ }
2199
+ function formatSkillWarnings(warnings) {
2200
+ const groups = /* @__PURE__ */ new Map();
2201
+ for (const w of warnings) {
2202
+ const key = `${w.tool}\0${w.message}\0${w.hint}`;
2203
+ const g = groups.get(key);
2204
+ if (g)
2205
+ g.paths.push(w.path);
2206
+ else
2207
+ groups.set(key, { w, paths: [w.path] });
2208
+ }
2209
+ const lines = [];
2210
+ for (const { w, paths } of groups.values()) {
2211
+ lines.push(`\u26A0 Skills for ${w.tool}: ${w.message}`);
2212
+ lines.push(` Not installed: ${paths.join(", ")}`);
2213
+ lines.push(` Fix: ${w.hint}`);
2214
+ }
2215
+ return lines;
1918
2216
  }
1919
2217
  var SUB_SKILLS = ["apply", "translate", "advanced"];
1920
2218
  var SUB_SKILL_TITLES = {
@@ -1963,57 +2261,231 @@ ${text.trim()}`
1963
2261
  }
1964
2262
  async function removeLegacyAgentsLayout(cwd2) {
1965
2263
  for (const name of ["pinsay-init", "pinsay-feedback"]) {
1966
- const filePath = join7(cwd2, ".agents", name, "SKILL.md");
1967
- const dirPath = join7(cwd2, ".agents", name);
2264
+ const filePath = join8(cwd2, ".agents", name, "SKILL.md");
2265
+ const dirPath = join8(cwd2, ".agents", name);
1968
2266
  try {
1969
- await fs7.rm(filePath, { force: true });
2267
+ await fs8.rm(filePath, { force: true });
1970
2268
  } catch {
1971
2269
  }
1972
2270
  try {
1973
- const remaining = await fs7.readdir(dirPath);
2271
+ const remaining = await fs8.readdir(dirPath);
1974
2272
  if (remaining.length === 0)
1975
- await fs7.rmdir(dirPath);
2273
+ await fs8.rmdir(dirPath);
1976
2274
  } catch {
1977
2275
  }
1978
2276
  }
1979
2277
  }
1980
2278
  async function installSkills(server, aiTool, cwd2, overrideDir) {
1981
2279
  const files = [];
2280
+ const warnings = [];
2281
+ const hide = [];
1982
2282
  server = server.replace(/\/$/, "");
1983
- await removeLegacyAgentsLayout(cwd2);
1984
- const pinsayShPath = join7(cwd2, ".pinsay", "pinsay.sh");
1985
- await download(`${server}/pinsay.sh`, pinsayShPath, true);
1986
- files.push(".pinsay/pinsay.sh");
2283
+ const landed = (abs, folder) => {
2284
+ const rel = relative3(resolve3(cwd2), abs).split(sep3).join("/");
2285
+ if (rel === "" || rel.startsWith("../") || isAbsolute2(rel))
2286
+ return;
2287
+ hide.push(folder ? `${posix.dirname(rel)}/` : rel);
2288
+ };
2289
+ const step = async (rel, work) => {
2290
+ try {
2291
+ await work();
2292
+ } catch (err) {
2293
+ warnings.push(toWarning(aiTool, rel, server, err));
2294
+ }
2295
+ };
2296
+ await removeLegacyAgentsLayout(cwd2).catch(() => {
2297
+ });
2298
+ await step(".pinsay/pinsay.sh", async () => {
2299
+ const { abs } = await writeRepoFile(cwd2, ".pinsay/pinsay.sh", await fetchText(`${server}/pinsay.sh`));
2300
+ await fs8.chmod(abs, 493).catch(() => {
2301
+ });
2302
+ files.push(".pinsay/pinsay.sh");
2303
+ });
1987
2304
  const isFlatFileTool = !overrideDir && (aiTool === "cursor" || aiTool === "windsurf");
2305
+ const overrideSlash = overrideDir ? overrideDir.replace(/\\/g, "/") : void 0;
1988
2306
  async function writeOrLink(primaryPath, skillName) {
1989
2307
  const url = skillName === "pinsay-init" ? `${server}/pinsay-init.md` : `${server}/skill.md`;
1990
- const finalPath = overrideDir ? join7(cwd2, overrideDir, skillName, "SKILL.md") : join7(cwd2, primaryPath);
1991
- if (skillName === "pinsay-feedback" && isFlatFileTool) {
1992
- const combined = await buildFlatPinSayFeedback(server);
1993
- await fs7.mkdir(dirname2(finalPath), { recursive: true });
1994
- await fs7.writeFile(finalPath, combined, "utf8");
1995
- } else {
1996
- await download(url, finalPath);
1997
- }
1998
- files.push(overrideDir ? join7(overrideDir, skillName, "SKILL.md") : primaryPath);
2308
+ const rel = overrideSlash ? posix.join(overrideSlash, skillName, "SKILL.md") : primaryPath;
2309
+ let primaryAbs = null;
2310
+ await step(rel, async () => {
2311
+ const body = skillName === "pinsay-feedback" && isFlatFileTool ? await buildFlatPinSayFeedback(server) : await fetchText(url);
2312
+ primaryAbs = (await writeRepoFile(cwd2, rel, body)).abs;
2313
+ landed(primaryAbs, !isFlatFileTool);
2314
+ files.push(rel);
2315
+ });
2316
+ if (primaryAbs === null)
2317
+ return;
2318
+ const source = primaryAbs;
1999
2319
  if (!overrideDir && (aiTool === "claude-code" || aiTool === "cursor" || aiTool === "windsurf")) {
2000
- const symDest = join7(cwd2, ".agents", "skills", skillName, "SKILL.md");
2001
- await makeSymlink(join7("..", "..", "..", primaryPath), symDest);
2002
- files.push(`.agents/skills/${skillName}/SKILL.md`);
2320
+ const mirrorRel = `.agents/skills/${skillName}/SKILL.md`;
2321
+ await step(mirrorRel, async () => {
2322
+ const m = await linkOrCopy(cwd2, source, mirrorRel);
2323
+ landed(m.abs, true);
2324
+ files.push(mirrorRel);
2325
+ });
2003
2326
  }
2004
2327
  if (skillName === "pinsay-feedback" && !isFlatFileTool) {
2005
- const siblingDir = dirname2(finalPath);
2006
- const relDir = overrideDir ? join7(overrideDir, skillName) : dirname2(primaryPath);
2328
+ const relDir = posix.dirname(rel);
2007
2329
  for (const name of SUB_SKILLS) {
2008
- await download(`${server}/skills/${name}.md`, join7(siblingDir, `${name}.md`));
2009
- files.push(join7(relDir, `${name}.md`));
2330
+ const subRel = posix.join(relDir, `${name}.md`);
2331
+ await step(subRel, async () => {
2332
+ await writeRepoFile(cwd2, subRel, await fetchText(`${server}/skills/${name}.md`));
2333
+ files.push(subRel);
2334
+ });
2010
2335
  }
2011
2336
  }
2012
2337
  }
2013
2338
  const layout = SKILL_FILES[aiTool] ?? SKILL_FILES.other;
2014
2339
  await writeOrLink(layout[0], "pinsay-init");
2015
2340
  await writeOrLink(layout[1], "pinsay-feedback");
2016
- return files;
2341
+ return { files, warnings, hide: [...new Set(hide)] };
2342
+ }
2343
+
2344
+ // src/lib/git-exclude.ts
2345
+ import { promises as fs9 } from "node:fs";
2346
+ import { execFile as execFile2 } from "node:child_process";
2347
+ import { promisify as promisify2 } from "node:util";
2348
+ import { dirname as dirname3, join as join9, resolve as resolve4 } from "node:path";
2349
+ var execFileAsync2 = promisify2(execFile2);
2350
+ var EXCLUDE_BEGIN = "# pinsay-cli: begin (managed by pinsay-cli; rewritten by init/update)";
2351
+ var EXCLUDE_END = "# pinsay-cli: end";
2352
+ var TEAM_FILES = [];
2353
+ var TEAM_FILE_PATHS = [".pinsay/config.json", ".pinsay/stack.json", ".pinsay/projects/"];
2354
+ var PINSAY_SKILL_PATHS = [
2355
+ ".claude/skills/pinsay-init/",
2356
+ ".claude/skills/pinsay-feedback/",
2357
+ ".agents/skills/pinsay-init/",
2358
+ ".agents/skills/pinsay-feedback/",
2359
+ // Legacy pre-2026-09-16 layout; installSkills removes it, older clones may still have it.
2360
+ ".agents/pinsay-init/",
2361
+ ".agents/pinsay-feedback/",
2362
+ ".cursor/rules/pinsay-init.md",
2363
+ ".cursor/rules/pinsay-feedback.md",
2364
+ ".windsurf/rules/pinsay-init.md",
2365
+ ".windsurf/rules/pinsay-feedback.md"
2366
+ ];
2367
+ function literal(p) {
2368
+ return p.replace(/([\\*?[\]!#])/g, "\\$1");
2369
+ }
2370
+ async function gitInfo(cwd2) {
2371
+ try {
2372
+ const { stdout } = await execFileAsync2("git", ["rev-parse", "--git-path", "info/exclude", "--show-prefix"], {
2373
+ cwd: cwd2,
2374
+ windowsHide: true
2375
+ });
2376
+ const [path, prefix = ""] = stdout.split(/\r?\n/);
2377
+ if (!path)
2378
+ return null;
2379
+ return { excludeFile: resolve4(cwd2, path.trim()), prefix: prefix.trim() };
2380
+ } catch {
2381
+ return null;
2382
+ }
2383
+ }
2384
+ function cleanExtra(extra) {
2385
+ return extra.map((e) => e.replace(/\\/g, "/").replace(/^\.\/+/, "")).filter((e) => e !== "" && !e.startsWith("../") && e !== ".." && !/^[A-Za-z]:\//.test(e) && !e.startsWith("/"));
2386
+ }
2387
+ function skillsDirExtra(dir) {
2388
+ if (!dir)
2389
+ return [];
2390
+ const d = dir.replace(/\\/g, "/").replace(/\/+$/, "");
2391
+ return [`${d}/pinsay-init/`, `${d}/pinsay-feedback/`];
2392
+ }
2393
+ function excludeLines(prefix, extra = []) {
2394
+ const p = `/${literal(prefix)}`;
2395
+ const pinsay = TEAM_FILES.length === 0 ? [`${p}.pinsay/`] : [`${p}.pinsay/*`, ...TEAM_FILES.map((f) => `!${p}.pinsay/${f}`)];
2396
+ const lines = [
2397
+ ...pinsay,
2398
+ ...PINSAY_SKILL_PATHS.map((s) => `${p}${s}`),
2399
+ ...cleanExtra(extra).map((e) => `${p}${literal(e)}`)
2400
+ ];
2401
+ return [...new Set(lines)];
2402
+ }
2403
+ function pinsayDirGitignore() {
2404
+ const body = TEAM_FILES.length === 0 ? ["*"] : ["/*", ...TEAM_FILES.map((f) => `!/${f}`)];
2405
+ return [
2406
+ "# Written by pinsay-cli. Keeps PinSay's per-machine files (credentials, scripts, caches) out of git,",
2407
+ "# even before this folder is in a git repository. It ignores itself too.",
2408
+ ...body,
2409
+ ""
2410
+ ].join("\n");
2411
+ }
2412
+ async function writePinsayDirGitignore(cwd2) {
2413
+ const file = join9(cwd2, ".pinsay", ".gitignore");
2414
+ const want = pinsayDirGitignore();
2415
+ const have = await fs9.readFile(file, "utf8").catch(() => null);
2416
+ if (have === want)
2417
+ return;
2418
+ await fs9.mkdir(dirname3(file), { recursive: true });
2419
+ await fs9.writeFile(file, want, "utf8");
2420
+ }
2421
+ async function trackedPinsayFiles(cwd2, extra = []) {
2422
+ const specs = [".pinsay", ...PINSAY_SKILL_PATHS, ...cleanExtra(extra)].map((s) => s.replace(/\/$/, ""));
2423
+ try {
2424
+ const { stdout } = await execFileAsync2("git", ["ls-files", "-z", "--", ...specs], { cwd: cwd2, windowsHide: true });
2425
+ return stdout.split("\0").filter(Boolean).filter((f) => !TEAM_FILE_PATHS.some((t) => t.endsWith("/") ? f.startsWith(t) : f === t)).sort();
2426
+ } catch {
2427
+ return [];
2428
+ }
2429
+ }
2430
+ async function hidePinsayFiles(cwd2, extra = []) {
2431
+ try {
2432
+ await writePinsayDirGitignore(cwd2);
2433
+ } catch {
2434
+ }
2435
+ const info = await gitInfo(cwd2);
2436
+ if (!info)
2437
+ return { status: "not-a-repo", tracked: [] };
2438
+ let status = "unchanged";
2439
+ let error;
2440
+ try {
2441
+ const content = await fs9.readFile(info.excludeFile, "utf8").catch(() => "");
2442
+ const eol = content.includes("\r\n") ? "\r\n" : "\n";
2443
+ const lines = content === "" ? [] : content.split(/\r?\n/);
2444
+ const b = lines.findIndex((l) => l.startsWith("# pinsay-cli: begin"));
2445
+ const e = b >= 0 ? lines.indexOf(EXCLUDE_END, b + 1) : -1;
2446
+ const kept = b >= 0 && e > b ? lines.slice(b + 1, e).filter((l) => /\/pinsay-(init|feedback)(\/|\.md)$/.test(l) && !l.startsWith("!")) : [];
2447
+ const block = [EXCLUDE_BEGIN, .../* @__PURE__ */ new Set([...excludeLines(info.prefix, extra), ...kept]), EXCLUDE_END];
2448
+ let next;
2449
+ if (b >= 0 && e > b) {
2450
+ next = [...lines.slice(0, b), ...block, ...lines.slice(e + 1)];
2451
+ } else {
2452
+ const base = [...lines];
2453
+ while (base.length > 0 && base[base.length - 1] === "")
2454
+ base.pop();
2455
+ next = [...base, ...base.length > 0 ? [""] : [], ...block];
2456
+ }
2457
+ while (next.length > 0 && next[next.length - 1] === "")
2458
+ next.pop();
2459
+ const out = next.join(eol) + eol;
2460
+ if (out !== content) {
2461
+ await fs9.mkdir(dirname3(info.excludeFile), { recursive: true });
2462
+ await fs9.writeFile(info.excludeFile, out, "utf8");
2463
+ status = "written";
2464
+ }
2465
+ } catch (err) {
2466
+ status = "failed";
2467
+ error = err instanceof Error ? err.message : String(err);
2468
+ }
2469
+ return { status, file: info.excludeFile, tracked: await trackedPinsayFiles(cwd2, extra), error };
2470
+ }
2471
+ function formatHideWarnings(r) {
2472
+ const lines = [];
2473
+ if (r.status === "failed") {
2474
+ lines.push(`\u26A0 Could not update ${r.file ?? ".git/info/exclude"} (${r.error}), so PinSay's files may show in git status.`);
2475
+ lines.push(` Fix: check that file's permissions. Then run "npx pinsay-cli update".`);
2476
+ }
2477
+ if (r.tracked.length > 0) {
2478
+ lines.push(`\u26A0 Git already tracks these PinSay files, so they can't be hidden: ${r.tracked.join(", ")}`);
2479
+ lines.push(` To stop tracking them (the files stay on disk), run: git rm -r --cached -- ${r.tracked.join(" ")}`);
2480
+ }
2481
+ return lines;
2482
+ }
2483
+ async function excludeBlockStatus(cwd2) {
2484
+ const info = await gitInfo(cwd2);
2485
+ if (!info)
2486
+ return "not-a-repo";
2487
+ const content = await fs9.readFile(info.excludeFile, "utf8").catch(() => "");
2488
+ return content.includes(EXCLUDE_END) && /^# pinsay-cli: begin/m.test(content) ? "ok" : "missing";
2017
2489
  }
2018
2490
 
2019
2491
  // src/branding.ts
@@ -2054,17 +2526,17 @@ async function postEvent(server, token, payload) {
2054
2526
  // src/checks.ts
2055
2527
  init_config();
2056
2528
  init_api();
2057
- import { promises as fs11 } from "node:fs";
2058
- import { join as join11 } from "node:path";
2059
- import { execFile } from "node:child_process";
2060
- import { promisify } from "node:util";
2529
+ import { promises as fs13 } from "node:fs";
2530
+ import { join as join12 } from "node:path";
2531
+ import { execFile as execFile3 } from "node:child_process";
2532
+ import { promisify as promisify3 } from "node:util";
2061
2533
 
2062
2534
  // src/lib/skill-stamp.ts
2063
- import { promises as fs8 } from "node:fs";
2535
+ import { promises as fs10 } from "node:fs";
2064
2536
  async function readStamp(path) {
2065
2537
  let content;
2066
2538
  try {
2067
- content = await fs8.readFile(path, "utf8");
2539
+ content = await fs10.readFile(path, "utf8");
2068
2540
  } catch {
2069
2541
  return null;
2070
2542
  }
@@ -2124,20 +2596,20 @@ async function readStamp(path) {
2124
2596
  }
2125
2597
 
2126
2598
  // src/lib/skill-paths.ts
2127
- import { join as join8 } from "node:path";
2128
2599
  function skillFilesFor(config) {
2129
2600
  const layout = SKILL_FILES[config.aiTool ?? ""] ?? SKILL_FILES.other;
2130
- const skillPaths = config.skillsDir ? [
2131
- join8(config.skillsDir, "pinsay-init", "SKILL.md"),
2132
- join8(config.skillsDir, "pinsay-feedback", "SKILL.md"),
2133
- ...SUB_SKILLS.map((name) => join8(config.skillsDir, "pinsay-feedback", `${name}.md`))
2601
+ const dir = config.skillsDir?.replace(/\\/g, "/").replace(/\/+$/, "");
2602
+ const skillPaths = dir ? [
2603
+ `${dir}/pinsay-init/SKILL.md`,
2604
+ `${dir}/pinsay-feedback/SKILL.md`,
2605
+ ...SUB_SKILLS.map((name) => `${dir}/pinsay-feedback/${name}.md`)
2134
2606
  ] : [...layout];
2135
2607
  return [...skillPaths, ".pinsay/pinsay.sh"];
2136
2608
  }
2137
2609
 
2138
2610
  // src/stack/stackfile.ts
2139
- import { promises as fs9 } from "node:fs";
2140
- import { join as join9, dirname as dirname3 } from "node:path";
2611
+ import { promises as fs11 } from "node:fs";
2612
+ import { join as join10, dirname as dirname4 } from "node:path";
2141
2613
  function buildRequestBody(stack) {
2142
2614
  const body = {};
2143
2615
  if (stack.frontend !== void 0)
@@ -2151,11 +2623,11 @@ function buildRequestBody(stack) {
2151
2623
  return body;
2152
2624
  }
2153
2625
  function stackFileRelPath(projectKey) {
2154
- return projectKey ? join9(".pinsay", "projects", `${projectKey}.stack.json`) : join9(".pinsay", "stack.json");
2626
+ return projectKey ? join10(".pinsay", "projects", `${projectKey}.stack.json`) : join10(".pinsay", "stack.json");
2155
2627
  }
2156
2628
  async function readStackFile(cwd2, projectKey) {
2157
2629
  try {
2158
- const raw = await fs9.readFile(join9(cwd2, stackFileRelPath(projectKey)), "utf8");
2630
+ const raw = await fs11.readFile(join10(cwd2, stackFileRelPath(projectKey)), "utf8");
2159
2631
  return JSON.parse(raw);
2160
2632
  } catch {
2161
2633
  return null;
@@ -2260,18 +2732,19 @@ function formatStackJson(stack) {
2260
2732
  }
2261
2733
  async function writeStackFile(cwd2, stack, projectKey) {
2262
2734
  const relPath = stackFileRelPath(projectKey);
2263
- const dir = join9(cwd2, dirname3(relPath));
2264
- await fs9.mkdir(dir, { recursive: true });
2265
- const targetPath = join9(cwd2, relPath);
2266
- const tempPath = join9(dir, `${projectKey ?? "stack"}.json.tmp.${Date.now()}.${Math.random().toString(36).slice(2)}`);
2735
+ const dir = join10(cwd2, dirname4(relPath));
2736
+ await fs11.mkdir(dir, { recursive: true });
2737
+ const targetPath = join10(cwd2, relPath);
2738
+ const tempPath = join10(dir, `${projectKey ?? "stack"}.json.tmp.${Date.now()}.${Math.random().toString(36).slice(2)}`);
2267
2739
  const content = formatStackJson(stack);
2268
- await fs9.writeFile(tempPath, content, "utf8");
2269
- await fs9.rename(tempPath, targetPath);
2740
+ await fs11.writeFile(tempPath, content, "utf8");
2741
+ await fs11.rename(tempPath, targetPath);
2270
2742
  }
2271
2743
 
2272
2744
  // src/checks.ts
2273
2745
  init_credentials();
2274
- var execFileAsync = promisify(execFile);
2746
+ init_server();
2747
+ var execFileAsync3 = promisify3(execFile3);
2275
2748
  function tooOldMessage(cliVersion, min) {
2276
2749
  return `CLI ${cliVersion} is older than the server requires (${min}). Upgrade with: npx -y pinsay@latest`;
2277
2750
  }
@@ -2308,7 +2781,7 @@ async function fetchWithTimeout(url, ms, init) {
2308
2781
  async function runInitChecks(cwd2, overrides = {}, cliVersion = "0.0.0") {
2309
2782
  const checks = [];
2310
2783
  const config = await readConfig(cwd2);
2311
- const server = (overrides.server || config.server || "").replace(/\/$/, "");
2784
+ const server = (overrides.server || resolveServer()).replace(/\/$/, "");
2312
2785
  const multiProject = isMultiProject(config);
2313
2786
  const allProjects = listProjects(config);
2314
2787
  const projectTargets = multiProject && overrides.project ? allProjects.filter((p) => p.key === overrides.project) : allProjects;
@@ -2416,7 +2889,7 @@ async function runInitChecks(cwd2, overrides = {}, cliVersion = "0.0.0") {
2416
2889
  }
2417
2890
  for (const target of multiProject ? projectTargets : [{ key: project, path: ".", environment, delivery: config.delivery }]) {
2418
2891
  const prefix = multiProject ? `[${target.key}] ` : "";
2419
- const appCwd = multiProject ? join11(cwd2, target.path) : cwd2;
2892
+ const appCwd = multiProject ? join12(cwd2, target.path) : cwd2;
2420
2893
  if (token) {
2421
2894
  try {
2422
2895
  const projects = await api(server, "/api/admin/projects", { token });
@@ -2456,7 +2929,7 @@ async function runInitChecks(cwd2, overrides = {}, cliVersion = "0.0.0") {
2456
2929
  }
2457
2930
  }
2458
2931
  try {
2459
- await fs11.access(join11(cwd2, stackFileRelPath(multiProject ? target.key : void 0)));
2932
+ await fs13.access(join12(cwd2, stackFileRelPath(multiProject ? target.key : void 0)));
2460
2933
  checks.push({ id: "stack", status: "ok", message: `${prefix}Stack registered` });
2461
2934
  } catch {
2462
2935
  checks.push({ id: "stack", status: "warn", message: `${prefix}Stack not registered`, fixable: true });
@@ -2478,9 +2951,9 @@ async function runInitChecks(cwd2, overrides = {}, cliVersion = "0.0.0") {
2478
2951
  if (meta?.skillVersion) {
2479
2952
  const stale = [];
2480
2953
  for (const rel of skillFilesFor(config)) {
2481
- const abs = join11(cwd2, rel);
2954
+ const abs = join12(cwd2, rel);
2482
2955
  try {
2483
- await fs11.access(abs);
2956
+ await fs13.access(abs);
2484
2957
  } catch {
2485
2958
  continue;
2486
2959
  }
@@ -2502,7 +2975,7 @@ async function runInitChecks(cwd2, overrides = {}, cliVersion = "0.0.0") {
2502
2975
  async function sourceMapCheck(cwd2, prefix = "") {
2503
2976
  const configured = await (async () => {
2504
2977
  for (const name of ["vite.config.ts", "vite.config.js", "vite.config.mjs", "vite.config.mts"]) {
2505
- const body = await fs11.readFile(join11(cwd2, name), "utf8").catch(() => "");
2978
+ const body = await fs13.readFile(join12(cwd2, name), "utf8").catch(() => "");
2506
2979
  if (body.includes("pinsay/vite"))
2507
2980
  return true;
2508
2981
  }
@@ -2516,7 +2989,7 @@ async function sourceMapCheck(cwd2, prefix = "") {
2516
2989
  };
2517
2990
  }
2518
2991
  try {
2519
- const raw = await fs11.readFile(join11(cwd2, ".pinsay/manifest.json"), "utf8");
2992
+ const raw = await fs13.readFile(join12(cwd2, ".pinsay/manifest.json"), "utf8");
2520
2993
  const count = Object.keys(JSON.parse(raw)?.entries ?? {}).length;
2521
2994
  return count > 0 ? { id: "source-map", status: "ok", message: `${prefix}Source manifest present (${count} components)` } : { id: "source-map", status: "warn", message: `${prefix}Source manifest is empty`, fixable: true };
2522
2995
  } catch {
@@ -2539,16 +3012,16 @@ async function widgetCheck(cwd2, config = {}, prefix = "") {
2539
3012
  const detection = await detectStack(cwd2).catch(() => null);
2540
3013
  const candidates = [config.htmlPath, detection?.htmlPath, "index.html", "public/index.html", "src/index.html"].filter(Boolean);
2541
3014
  try {
2542
- for (const entry of await fs11.readdir(cwd2, { withFileTypes: true })) {
3015
+ for (const entry of await fs13.readdir(cwd2, { withFileTypes: true })) {
2543
3016
  if (entry.isDirectory() && !entry.name.startsWith(".")) {
2544
- candidates.push(join11(entry.name, "index.html"));
3017
+ candidates.push(join12(entry.name, "index.html"));
2545
3018
  }
2546
3019
  }
2547
3020
  } catch {
2548
3021
  }
2549
3022
  for (const rel of candidates) {
2550
3023
  try {
2551
- const html = await fs11.readFile(join11(cwd2, rel), "utf8");
3024
+ const html = await fs13.readFile(join12(cwd2, rel), "utf8");
2552
3025
  if (html.includes("<!-- pinsay-feedback:start -->") || html.includes("<pinsay-feedback")) {
2553
3026
  return { id: "widget", status: "ok", message: `${prefix}Widget found in ${rel}` };
2554
3027
  }
@@ -2557,7 +3030,7 @@ async function widgetCheck(cwd2, config = {}, prefix = "") {
2557
3030
  }
2558
3031
  for (const envFile of [".env", ".env.local", ".env.development"]) {
2559
3032
  try {
2560
- const env = await fs11.readFile(join11(cwd2, envFile), "utf8");
3033
+ const env = await fs13.readFile(join12(cwd2, envFile), "utf8");
2561
3034
  if (/^VITE_PINSAY_PROJECT=/m.test(env)) {
2562
3035
  return { id: "widget", status: "ok", message: `${prefix}Widget env configured in ${envFile}` };
2563
3036
  }
@@ -2575,20 +3048,14 @@ async function skillsCheck(cwd2, config) {
2575
3048
  const tool = config.aiTool;
2576
3049
  if (!tool)
2577
3050
  return { id: "skills", status: "warn", message: "No AI tool configured" };
2578
- const expected = SKILL_FILES[tool] ?? SKILL_FILES.other;
2579
3051
  const missing = [];
2580
- for (const rel of expected) {
3052
+ for (const rel of skillFilesFor(config)) {
2581
3053
  try {
2582
- await fs11.access(join11(cwd2, config.skillsDir ?? "", rel));
3054
+ await fs13.access((await resolveRepoPath(cwd2, rel, { create: false })).abs);
2583
3055
  } catch {
2584
3056
  missing.push(rel);
2585
3057
  }
2586
3058
  }
2587
- try {
2588
- await fs11.access(join11(cwd2, ".pinsay/pinsay.sh"));
2589
- } catch {
2590
- missing.push(".pinsay/pinsay.sh");
2591
- }
2592
3059
  return missing.length === 0 ? { id: "skills", status: "ok", message: `Skills installed for ${tool}` } : {
2593
3060
  id: "skills",
2594
3061
  status: "warn",
@@ -2601,7 +3068,7 @@ async function gitignoreChecks(cwd2) {
2601
3068
  const results = [];
2602
3069
  let tracked = false;
2603
3070
  try {
2604
- await execFileAsync("git", ["ls-files", "--error-unmatch", ".pinsay/credentials.env"], { cwd: cwd2 });
3071
+ await execFileAsync3("git", ["ls-files", "--error-unmatch", ".pinsay/credentials.env"], { cwd: cwd2 });
2605
3072
  tracked = true;
2606
3073
  } catch {
2607
3074
  tracked = false;
@@ -2615,24 +3082,33 @@ async function gitignoreChecks(cwd2) {
2615
3082
  });
2616
3083
  return results;
2617
3084
  }
2618
- try {
2619
- const ignore = await fs11.readFile(join11(cwd2, ".gitignore"), "utf8");
2620
- results.push(
2621
- ignore.includes(".pinsay/") ? { id: "gitignore", status: "ok", message: "Credentials ignored by git" } : { id: "gitignore", status: "warn", message: ".gitignore is missing the .pinsay/ entries", fixable: true }
2622
- );
2623
- } catch {
2624
- results.push({ id: "gitignore", status: "warn", message: "No .gitignore found", fixable: true });
3085
+ const block = await excludeBlockStatus(cwd2);
3086
+ if (block === "not-a-repo") {
3087
+ results.push({ id: "gitignore", status: "ok", message: "Not a git repository \u2014 nothing to hide" });
3088
+ return results;
3089
+ }
3090
+ const others = (await trackedPinsayFiles(cwd2)).filter((f) => f !== ".pinsay/credentials.env");
3091
+ if (others.length > 0) {
3092
+ results.push({
3093
+ id: "gitignore",
3094
+ status: "warn",
3095
+ message: `Git tracks PinSay files that should be per-machine: ${others.join(", ")}`,
3096
+ hint: `Run: git rm -r --cached -- ${others.join(" ")}`
3097
+ });
2625
3098
  }
3099
+ results.push(
3100
+ block === "ok" ? { id: "gitignore", status: "ok", message: "PinSay files hidden from git (.git/info/exclude)" } : { id: "gitignore", status: "warn", message: "PinSay's files are not hidden from git yet", hint: "Run `npx pinsay-cli doctor --fix`", fixable: true }
3101
+ );
2626
3102
  return results;
2627
3103
  }
2628
3104
 
2629
3105
  // src/commands/init.ts
2630
- import { promises as fs13, existsSync as existsSync2 } from "node:fs";
2631
- import { join as join13, dirname as dirname6, relative as relative3, resolve as resolve2, isAbsolute as isAbsolute2, sep as sep2 } from "node:path";
3106
+ import { promises as fs15, existsSync as existsSync2 } from "node:fs";
3107
+ import { join as join14, dirname as dirname7, relative as relative5, resolve as resolve6, isAbsolute as isAbsolute3, sep as sep5 } from "node:path";
2632
3108
 
2633
3109
  // src/stack/design.ts
2634
- import { promises as fs12, statSync } from "node:fs";
2635
- import { dirname as dirname5, join as join12, relative as relative2, resolve } from "node:path";
3110
+ import { promises as fs14, statSync } from "node:fs";
3111
+ import { dirname as dirname6, join as join13, relative as relative4, resolve as resolve5, sep as sep4 } from "node:path";
2636
3112
  var IGNORED_DIRS = /* @__PURE__ */ new Set([
2637
3113
  "node_modules",
2638
3114
  "dist",
@@ -2658,7 +3134,7 @@ async function collectFiles(cwd2, options) {
2658
3134
  return;
2659
3135
  let entries;
2660
3136
  try {
2661
- entries = await fs12.readdir(dir);
3137
+ entries = await fs14.readdir(dir);
2662
3138
  } catch {
2663
3139
  return;
2664
3140
  }
@@ -2668,17 +3144,17 @@ async function collectFiles(cwd2, options) {
2668
3144
  return;
2669
3145
  if (IGNORED_DIRS.has(entry))
2670
3146
  continue;
2671
- const fullPath = join12(dir, entry);
3147
+ const fullPath = join13(dir, entry);
2672
3148
  let stat;
2673
3149
  try {
2674
- stat = await fs12.stat(fullPath);
3150
+ stat = await fs14.stat(fullPath);
2675
3151
  } catch {
2676
3152
  continue;
2677
3153
  }
2678
3154
  if (stat.isDirectory()) {
2679
3155
  subdirs.push(fullPath);
2680
3156
  } else if (stat.isFile()) {
2681
- collected.push(relative2(cwd2, fullPath));
3157
+ collected.push(relative4(cwd2, fullPath).split(sep4).join("/"));
2682
3158
  }
2683
3159
  }
2684
3160
  for (const sub of subdirs) {
@@ -2777,15 +3253,15 @@ function parseArrayOrObjectKeys(content, matchIndex) {
2777
3253
  return [];
2778
3254
  }
2779
3255
  async function collectDependencyChain(cwd2, root, readFile) {
2780
- const cwdAbs = resolve(cwd2);
2781
- const rootAbs = resolve(root);
3256
+ const cwdAbs = resolve5(cwd2);
3257
+ const rootAbs = resolve5(root);
2782
3258
  const dirs = [];
2783
3259
  let dir = cwdAbs;
2784
3260
  while (true) {
2785
3261
  dirs.push(dir);
2786
3262
  if (dir === rootAbs)
2787
3263
  break;
2788
- const parent = dirname5(dir);
3264
+ const parent = dirname6(dir);
2789
3265
  if (parent === dir)
2790
3266
  break;
2791
3267
  dir = parent;
@@ -2794,7 +3270,7 @@ async function collectDependencyChain(cwd2, root, readFile) {
2794
3270
  const merged = {};
2795
3271
  for (const d of dirs) {
2796
3272
  try {
2797
- const pkg = JSON.parse(await readFile(join12(d, "package.json")));
3273
+ const pkg = JSON.parse(await readFile(join13(d, "package.json")));
2798
3274
  Object.assign(merged, pkg.dependencies, pkg.devDependencies);
2799
3275
  } catch {
2800
3276
  }
@@ -2821,7 +3297,7 @@ async function detectTailwind(cwd2, files, readFile, root = cwd2) {
2821
3297
  let exists = files.includes(name);
2822
3298
  if (!exists) {
2823
3299
  try {
2824
- await readFile(join12(cwd2, name));
3300
+ await readFile(join13(cwd2, name));
2825
3301
  exists = true;
2826
3302
  } catch {
2827
3303
  exists = false;
@@ -2829,16 +3305,16 @@ async function detectTailwind(cwd2, files, readFile, root = cwd2) {
2829
3305
  }
2830
3306
  if (exists) {
2831
3307
  foundConfigFile = name;
2832
- configAbsPath = join12(cwd2, name);
3308
+ configAbsPath = join13(cwd2, name);
2833
3309
  break;
2834
3310
  }
2835
3311
  }
2836
- if (!foundConfigFile && resolve(root) !== resolve(cwd2)) {
3312
+ if (!foundConfigFile && resolve5(root) !== resolve5(cwd2)) {
2837
3313
  for (const name of configNames) {
2838
- const candidate = join12(root, name);
3314
+ const candidate = join13(root, name);
2839
3315
  try {
2840
3316
  await readFile(candidate);
2841
- foundConfigFile = relative2(cwd2, candidate);
3317
+ foundConfigFile = relative4(cwd2, candidate);
2842
3318
  configAbsPath = candidate;
2843
3319
  break;
2844
3320
  } catch {
@@ -2909,7 +3385,7 @@ async function detectTailwind(cwd2, files, readFile, root = cwd2) {
2909
3385
  for (const file of cssFiles) {
2910
3386
  let content = "";
2911
3387
  try {
2912
- content = await readFile(join12(cwd2, file));
3388
+ content = await readFile(join13(cwd2, file));
2913
3389
  } catch {
2914
3390
  continue;
2915
3391
  }
@@ -2955,7 +3431,7 @@ async function scanCssVars(baseDir, candidatePaths, readFile) {
2955
3431
  const candidateFiles = [];
2956
3432
  for (const f of candidatePaths) {
2957
3433
  try {
2958
- const fullPath = join12(baseDir, f);
3434
+ const fullPath = join13(baseDir, f);
2959
3435
  const stat = statSync(fullPath);
2960
3436
  if (stat.size <= 204800) {
2961
3437
  candidateFiles.push({ path: f, size: stat.size });
@@ -2975,7 +3451,7 @@ async function scanCssVars(baseDir, candidatePaths, readFile) {
2975
3451
  for (const item of selectedFiles) {
2976
3452
  let content = "";
2977
3453
  try {
2978
- content = await readFile(join12(baseDir, item.path));
3454
+ content = await readFile(join13(baseDir, item.path));
2979
3455
  } catch {
2980
3456
  continue;
2981
3457
  }
@@ -3018,7 +3494,7 @@ async function scanScss(baseDir, candidatePaths, readFile) {
3018
3494
  for (const f of candidatePaths) {
3019
3495
  let content = "";
3020
3496
  try {
3021
- content = await readFile(join12(baseDir, f));
3497
+ content = await readFile(join13(baseDir, f));
3022
3498
  } catch {
3023
3499
  continue;
3024
3500
  }
@@ -3084,7 +3560,7 @@ async function detectTheme(cwd2, files, readFile) {
3084
3560
  for (const f of themeFiles) {
3085
3561
  let content = "";
3086
3562
  try {
3087
- content = await readFile(join12(cwd2, f));
3563
+ content = await readFile(join13(cwd2, f));
3088
3564
  } catch {
3089
3565
  continue;
3090
3566
  }
@@ -3114,7 +3590,7 @@ async function detectAngularMaterial(cwd2, files, readFile) {
3114
3590
  for (const f of scssFiles) {
3115
3591
  let content = "";
3116
3592
  try {
3117
- content = await readFile(join12(cwd2, f));
3593
+ content = await readFile(join13(cwd2, f));
3118
3594
  } catch {
3119
3595
  continue;
3120
3596
  }
@@ -3147,7 +3623,7 @@ async function detectLibraries(cwd2, files, readFile, root = cwd2) {
3147
3623
  "primeng",
3148
3624
  "primevue"
3149
3625
  ];
3150
- if (resolve(root) !== resolve(cwd2)) {
3626
+ if (resolve5(root) !== resolve5(cwd2)) {
3151
3627
  tracked.push("tailwindcss");
3152
3628
  }
3153
3629
  for (const [depName, depVer] of Object.entries(deps)) {
@@ -3205,7 +3681,7 @@ function buildDesignBlock(libraries, tokens) {
3205
3681
  };
3206
3682
  }
3207
3683
  async function detectDesignTokens(cwd2, options) {
3208
- const readFile = options?.readFile ?? ((p) => fs12.readFile(p, "utf8"));
3684
+ const readFile = options?.readFile ?? ((p) => fs14.readFile(p, "utf8"));
3209
3685
  const root = options?.root ?? cwd2;
3210
3686
  const files = await collectFiles(cwd2, options);
3211
3687
  const libraries = await detectLibraries(cwd2, files, readFile, root);
@@ -3214,7 +3690,7 @@ async function detectDesignTokens(cwd2, options) {
3214
3690
  let scss = await detectScss(cwd2, files, readFile);
3215
3691
  const theme = await detectTheme(cwd2, files, readFile);
3216
3692
  const angularMaterial = await detectAngularMaterial(cwd2, files, readFile);
3217
- if ((!cssVars || !scss) && resolve(root) !== resolve(cwd2)) {
3693
+ if ((!cssVars || !scss) && resolve5(root) !== resolve5(cwd2)) {
3218
3694
  const rootFiles = await collectFiles(root, options);
3219
3695
  const sharedStyleFiles = rootFiles.filter(isRootSharedStylePath);
3220
3696
  if (!cssVars) {
@@ -3294,7 +3770,7 @@ function openBrowser(url) {
3294
3770
  }
3295
3771
  }
3296
3772
  function sleep(ms) {
3297
- return new Promise((resolve5) => setTimeout(resolve5, ms));
3773
+ return new Promise((resolve9) => setTimeout(resolve9, ms));
3298
3774
  }
3299
3775
  async function runDeviceLogin(server, options = {}) {
3300
3776
  const clientName = options.clientName ?? `pinsay CLI on ${hostname()}`;
@@ -3355,7 +3831,35 @@ async function runDeviceLogin(server, options = {}) {
3355
3831
  return { ok: false, reason: "expired" };
3356
3832
  }
3357
3833
 
3834
+ // src/key-scope.ts
3835
+ var KEY_SCOPE_QUESTION = "Where should this API key be stored?";
3836
+ var KEY_SCOPE_GLOBAL = "Global \u2014 this machine, every repo (~/.config/pinsay/credentials.json)";
3837
+ var KEY_SCOPE_REPO = "Repo \u2014 .pinsay/credentials.env in this repo only (hidden from git)";
3838
+ function scopeFromFlags(options) {
3839
+ const raw = options["scope"];
3840
+ if (typeof raw === "string") {
3841
+ const value = raw.toLowerCase();
3842
+ if (value !== "global" && value !== "repo") {
3843
+ return { error: `Invalid --scope "${raw}". Valid values: global, repo.` };
3844
+ }
3845
+ return { scope: value };
3846
+ }
3847
+ if (options["local-credentials"] === true)
3848
+ return { scope: "repo" };
3849
+ return {};
3850
+ }
3851
+ function decideKeyScope(flagScope, interactive) {
3852
+ if (flagScope)
3853
+ return flagScope;
3854
+ return interactive ? "ask" : "global";
3855
+ }
3856
+ async function askKeyScope() {
3857
+ const choice = await select(KEY_SCOPE_QUESTION, [KEY_SCOPE_GLOBAL, KEY_SCOPE_REPO]);
3858
+ return choice === KEY_SCOPE_REPO ? "repo" : "global";
3859
+ }
3860
+
3358
3861
  // src/commands/init.ts
3862
+ init_server();
3359
3863
  async function initCommand(cwd2, options = {}) {
3360
3864
  const isYes = options["yes"] || options["json"];
3361
3865
  const isJson = options["json"];
@@ -3373,12 +3877,9 @@ async function initCommand(cwd2, options = {}) {
3373
3877
  const pathFlag = typeof options["path"] === "string" ? String(options["path"]).replace(/^\.\/+/, "").replace(/\/+$/, "") : void 0;
3374
3878
  const isAddProject = Boolean(pathFlag);
3375
3879
  const configIsMulti = isMultiProject(config);
3376
- const isJoin = !isAddProject && Boolean(config.server) && (Boolean(config.project) || configIsMulti);
3880
+ const isJoin = !isAddProject && (Boolean(config.project) || configIsMulti);
3377
3881
  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
- }
3882
+ const server = resolveServer();
3382
3883
  const scopeFlag = typeof options["scope"] === "string" ? String(options["scope"]).toLowerCase() : void 0;
3383
3884
  if (scopeFlag !== void 0 && scopeFlag !== "global" && scopeFlag !== "repo") {
3384
3885
  console.error(`Invalid --scope "${options["scope"]}". Valid values: global, repo.`);
@@ -3521,11 +4022,7 @@ async function initCommand(cwd2, options = {}) {
3521
4022
  if (freshlyAuthenticated) {
3522
4023
  let saveGlobally = !localCredentialsFlag;
3523
4024
  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");
4025
+ saveGlobally = await askKeyScope() === "global";
3529
4026
  }
3530
4027
  if (saveGlobally) {
3531
4028
  await saveGlobalCredential(server, { apiKey: key, email: me?.email, displayName: me?.displayName });
@@ -3534,9 +4031,9 @@ async function initCommand(cwd2, options = {}) {
3534
4031
  }
3535
4032
  }
3536
4033
  if (writeLocalCreds) {
3537
- await writeCredentials(cwd2, key, { server });
4034
+ await writeCredentials(cwd2, key);
3538
4035
  }
3539
- await upsertGitignore(cwd2, product, options["skills-dir"] || config.skillsDir);
4036
+ await hidePinsayFiles(cwd2, skillsDirExtra(options["skills-dir"] || config.skillsDir));
3540
4037
  if (isJoin && configIsMulti) {
3541
4038
  await handleMultiJoin(cwd2, config, server, product, Boolean(isJson), options, token, me);
3542
4039
  return;
@@ -3734,6 +4231,7 @@ and the pinsay-init skill uses them to mount the widget for you afterwards.
3734
4231
  let routedToSkill = false;
3735
4232
  let filesMod = [];
3736
4233
  let skillFiles = [];
4234
+ let skillWarnings = [];
3737
4235
  let pin = null;
3738
4236
  if (!isJoin && options["pin"] === true) {
3739
4237
  try {
@@ -3812,23 +4310,43 @@ and the pinsay-init skill uses them to mount the widget for you afterwards.
3812
4310
  console.error(`\u26A0 ${sourceMapNote}`);
3813
4311
  }
3814
4312
  }
4313
+ const skillHide = [];
3815
4314
  if (!options["no-skills"]) {
3816
4315
  if (!isJson)
3817
4316
  console.log(`Installing AI skills for ${tools.join(", ")}`);
3818
4317
  const skillsDir = options["skills-dir"];
3819
4318
  const installed = [];
3820
4319
  for (const t of tools) {
3821
- installed.push(...await installSkills(server, t, cwd2, t === tool ? skillsDir : void 0));
4320
+ try {
4321
+ const r = await installSkills(server, t, cwd2, t === tool ? skillsDir : void 0);
4322
+ installed.push(...r.files);
4323
+ skillWarnings.push(...r.warnings);
4324
+ skillHide.push(...r.hide);
4325
+ } catch (err) {
4326
+ skillWarnings.push({
4327
+ tool: t,
4328
+ path: "(all skill files)",
4329
+ message: `could not install the skills (${err?.message ?? err}).`,
4330
+ hint: 'Run "npx pinsay-cli update" to try again.'
4331
+ });
4332
+ }
3822
4333
  }
4334
+ if (!isJson)
4335
+ for (const line of formatSkillWarnings(skillWarnings))
4336
+ console.error(line);
3823
4337
  filesMod.push(...installed);
3824
4338
  skillFiles = [...new Set(installed.filter((f) => f.includes("SKILL.md") || f.endsWith(".md")))];
3825
4339
  }
3826
- const pkgStr = await fs13.readFile(join13(cwd2, "package.json"), "utf8").catch(() => "{}");
4340
+ const hidden = await hidePinsayFiles(cwd2, [...skillsDirExtra(options["skills-dir"]), ...skillHide]);
4341
+ if (!isJson)
4342
+ for (const line of formatHideWarnings(hidden))
4343
+ console.error(line);
4344
+ const pkgStr = await fs15.readFile(join14(cwd2, "package.json"), "utf8").catch(() => "{}");
3827
4345
  const tokens = extractTokens(JSON.parse(pkgStr));
3828
4346
  const stackMeta = { frontend: tokens.frontend, backend: tokens.backend, aiTool: tool };
3829
4347
  let stackFileExists = false;
3830
4348
  try {
3831
- await fs13.access(join13(cwd2, ".pinsay/stack.json"));
4349
+ await fs15.access(join14(cwd2, ".pinsay/stack.json"));
3832
4350
  stackFileExists = true;
3833
4351
  } catch {
3834
4352
  stackFileExists = false;
@@ -3856,7 +4374,6 @@ and the pinsay-init skill uses them to mount the widget for you afterwards.
3856
4374
  }
3857
4375
  const injectedHtml = injected ? filesMod.find((f) => f.toLowerCase().endsWith(".html"))?.replace(`${cwd2}/`, "") : void 0;
3858
4376
  const configPatch = {
3859
- server,
3860
4377
  project: finalProjectKey,
3861
4378
  aiTool: tool,
3862
4379
  skillsDir: options["skills-dir"],
@@ -3868,10 +4385,9 @@ and the pinsay-init skill uses them to mount the widget for you afterwards.
3868
4385
  await writeConfig(cwd2, configPatch);
3869
4386
  filesMod.push(".pinsay/config.json");
3870
4387
  if (writeLocalCreds) {
3871
- await writeCredentials(cwd2, key, { server, project: finalProjectKey });
4388
+ await writeCredentials(cwd2, key, { project: finalProjectKey });
3872
4389
  filesMod.push(".pinsay/credentials.env");
3873
4390
  }
3874
- filesMod.push(".gitignore");
3875
4391
  if (!isJson)
3876
4392
  console.log("Verifying...");
3877
4393
  const checks = await runInitChecks(cwd2, { server, project: finalProjectKey }, BUILD_CLI_VERSION);
@@ -3901,6 +4417,9 @@ and the pinsay-init skill uses them to mount the widget for you afterwards.
3901
4417
  injected,
3902
4418
  routedToSkill,
3903
4419
  files: filesMod,
4420
+ skillWarnings,
4421
+ hiddenFromGit: hidden.status,
4422
+ trackedPinsayFiles: hidden.tracked,
3904
4423
  checks,
3905
4424
  cliVersion: BUILD_CLI_VERSION
3906
4425
  }));
@@ -3912,7 +4431,7 @@ and the pinsay-init skill uses them to mount the widget for you afterwards.
3912
4431
  const cyan = (s) => `\x1B[36m${s}\x1B[0m`;
3913
4432
  const rule = dim("\u2500".repeat(60));
3914
4433
  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)")}`;
4434
+ const keyLine = keyLivesGlobally ? `this machine's global store ${dim("(~/.config/pinsay/credentials.json)")}` : `.pinsay/credentials.env ${dim("(gitignored)")}`;
3916
4435
  if (isJoin) {
3917
4436
  console.log(`
3918
4437
  ${rule}
@@ -3921,7 +4440,7 @@ ${envLabel !== null ? `
3921
4440
  ${dim("Environment(s)")} ${envLabel}` : ""}
3922
4441
  ${dim("Server")} ${server}
3923
4442
  ${dim("Key")} ${keyLine}
3924
- ${dim("Skills")} ${skillFiles.length ? skillFiles.join("\n ") : "installed"}
4443
+ ${dim("Skills")} ${skillFiles.length ? skillFiles.join("\n ") : skillWarnings.length ? "not installed \u2014 see the warning above" : "installed"}
3925
4444
  ${rule}`);
3926
4445
  } else {
3927
4446
  console.log(`
@@ -3932,7 +4451,7 @@ ${green("\u2714")} ${bold(`${product} is set up`)}
3932
4451
  ${envLabel !== null ? ` ${dim("Environments")} ${envLabel}
3933
4452
  ` : ""} ${dim("Server")} ${server}
3934
4453
  ${dim("Key")} ${keyLine}
3935
- ${dim("Skills")} ${skillFiles.length ? skillFiles.join("\n ") : "installed"}
4454
+ ${dim("Skills")} ${skillFiles.length ? skillFiles.join("\n ") : skillWarnings.length ? "not installed \u2014 see the warning above" : "installed"}
3936
4455
  ${rule}`);
3937
4456
  }
3938
4457
  if (delivery === "extension") {
@@ -3944,7 +4463,7 @@ ${rule}`);
3944
4463
  ${bold("Next")} Reviewers open the widget through the ${product} Chrome extension \u2014 nothing was injected.
3945
4464
 
3946
4465
  ${installLine}
3947
- 2. Extension \u2192 Options \u2192 set server ${server}; sign in with your ${product} account.
4466
+ 2. Open the extension and sign in with your ${product} account.
3948
4467
  3. Open your app, click the extension icon, choose project ${dim(finalProjectKey)}, Activate.
3949
4468
  4. Then ${bold("npx pinsay-cli list")} / ${bold("apply")} as usual.
3950
4469
  ${dim(`Dashboard: ${branding.urls?.app || server}`)}`);
@@ -3999,19 +4518,19 @@ function projectQuestion(label) {
3999
4518
  return label ? `Which PinSay project is ${label}?` : "Which project is this app?";
4000
4519
  }
4001
4520
  function toRootRelative(root, p) {
4002
- const abs = isAbsolute2(p) ? p : resolve2(p);
4003
- return relative3(root, abs).split(sep2).join("/");
4521
+ const abs = isAbsolute3(p) ? p : resolve6(p);
4522
+ return relative5(root, abs).split(sep5).join("/");
4004
4523
  }
4005
4524
  async function hasViteConfig(appDir) {
4006
4525
  for (const ext of ["ts", "js", "mjs", "mts"]) {
4007
- if (existsSync2(join13(appDir, `vite.config.${ext}`)))
4526
+ if (existsSync2(join14(appDir, `vite.config.${ext}`)))
4008
4527
  return true;
4009
4528
  }
4010
4529
  return false;
4011
4530
  }
4012
4531
  async function readJsonSafe(p) {
4013
4532
  try {
4014
- return JSON.parse(await fs13.readFile(p, "utf8"));
4533
+ return JSON.parse(await fs15.readFile(p, "utf8"));
4015
4534
  } catch {
4016
4535
  return null;
4017
4536
  }
@@ -4019,13 +4538,13 @@ async function readJsonSafe(p) {
4019
4538
  async function resolveHtmlCandidate(root, appDir, explicitHtml) {
4020
4539
  if (explicitHtml)
4021
4540
  return explicitHtml;
4022
- const targetCwd = join13(root, appDir);
4023
- const candidates = [join13(targetCwd, "index.html"), join13(targetCwd, "src", "index.html")];
4024
- const projectJson = await readJsonSafe(join13(targetCwd, "project.json"));
4541
+ const targetCwd = join14(root, appDir);
4542
+ const candidates = [join14(targetCwd, "index.html"), join14(targetCwd, "src", "index.html")];
4543
+ const projectJson = await readJsonSafe(join14(targetCwd, "project.json"));
4025
4544
  if (projectJson?.sourceRoot) {
4026
- candidates.push(join13(root, projectJson.sourceRoot, "index.html"));
4545
+ candidates.push(join14(root, projectJson.sourceRoot, "index.html"));
4027
4546
  }
4028
- candidates.push(join13(targetCwd, "public", "index.html"));
4547
+ candidates.push(join14(targetCwd, "public", "index.html"));
4029
4548
  for (const c of candidates) {
4030
4549
  if (existsSync2(c))
4031
4550
  return c;
@@ -4035,7 +4554,7 @@ async function resolveHtmlCandidate(root, appDir, explicitHtml) {
4035
4554
  function deriveAppDirFromHtmlPath(htmlPath) {
4036
4555
  if (!htmlPath)
4037
4556
  return ".";
4038
- let dir = dirname6(htmlPath).replace(/\/src$/, "");
4557
+ let dir = dirname7(htmlPath).replace(/\/src$/, "");
4039
4558
  return dir === "" || dir === "." ? "." : dir;
4040
4559
  }
4041
4560
  async function selectOrCreateProject(server, token, isYes, presetKey, presetCreate, label) {
@@ -4160,7 +4679,7 @@ async function resolvePin(server, options) {
4160
4679
  }
4161
4680
  async function setupOneProject(ctx) {
4162
4681
  const { cwd: cwd2, appDir, server, token } = ctx;
4163
- const targetCwd = join13(cwd2, appDir);
4682
+ const targetCwd = join14(cwd2, appDir);
4164
4683
  const label = appLabel(appDir);
4165
4684
  const { key, name, created } = await selectOrCreateProject(server, token, ctx.isYes, ctx.presetKey, ctx.presetCreate, label);
4166
4685
  const ALL_ENVS = ["local", "staging", "production"];
@@ -4236,9 +4755,9 @@ async function setupOneProject(ctx) {
4236
4755
  noHtmlFound = true;
4237
4756
  }
4238
4757
  }
4239
- let pkgStr = await fs13.readFile(join13(targetCwd, "package.json"), "utf8").catch(() => "");
4758
+ let pkgStr = await fs15.readFile(join14(targetCwd, "package.json"), "utf8").catch(() => "");
4240
4759
  if (!pkgStr)
4241
- pkgStr = await fs13.readFile(join13(cwd2, "package.json"), "utf8").catch(() => "{}");
4760
+ pkgStr = await fs15.readFile(join14(cwd2, "package.json"), "utf8").catch(() => "{}");
4242
4761
  const tokens = extractTokens(JSON.parse(pkgStr || "{}"));
4243
4762
  const stackMeta = { frontend: tokens.frontend, backend: tokens.backend, aiTool: ctx.aiTool };
4244
4763
  let serverStackResponse = null;
@@ -4260,25 +4779,39 @@ async function setupOneProject(ctx) {
4260
4779
  async function handleMultiJoin(cwd2, config, server, product, isJson, options, token, me) {
4261
4780
  closePrompts();
4262
4781
  const tool = options["tool"] || config.aiTool || "other";
4782
+ const skillWarnings = [];
4783
+ const hide = [];
4263
4784
  if (!options["no-skills"]) {
4264
- await installSkills(server, tool, cwd2, options["skills-dir"]).catch(() => {
4265
- });
4785
+ try {
4786
+ const r = await installSkills(server, tool, cwd2, options["skills-dir"]);
4787
+ skillWarnings.push(...r.warnings);
4788
+ hide.push(...r.hide);
4789
+ } catch (err) {
4790
+ skillWarnings.push({ tool, path: "(all skill files)", message: `could not install the skills (${err?.message ?? err}).`, hint: 'Run "npx pinsay-cli update" to try again.' });
4791
+ }
4792
+ if (!isJson)
4793
+ for (const line of formatSkillWarnings(skillWarnings))
4794
+ console.error(line);
4266
4795
  }
4796
+ const hidden = await hidePinsayFiles(cwd2, [...skillsDirExtra(options["skills-dir"]), ...hide]);
4797
+ if (!isJson)
4798
+ for (const line of formatHideWarnings(hidden))
4799
+ console.error(line);
4267
4800
  const projects = listProjects(config);
4268
4801
  for (const p of projects) {
4269
4802
  const relStack = stackFileRelPath(p.key);
4270
4803
  let exists = true;
4271
4804
  try {
4272
- await fs13.access(join13(cwd2, relStack));
4805
+ await fs15.access(join14(cwd2, relStack));
4273
4806
  } catch {
4274
4807
  exists = false;
4275
4808
  }
4276
4809
  if (exists)
4277
4810
  continue;
4278
- const appCwd = join13(cwd2, p.path);
4279
- let pkgStr = await fs13.readFile(join13(appCwd, "package.json"), "utf8").catch(() => "");
4811
+ const appCwd = join14(cwd2, p.path);
4812
+ let pkgStr = await fs15.readFile(join14(appCwd, "package.json"), "utf8").catch(() => "");
4280
4813
  if (!pkgStr)
4281
- pkgStr = await fs13.readFile(join13(cwd2, "package.json"), "utf8").catch(() => "{}");
4814
+ pkgStr = await fs15.readFile(join14(cwd2, "package.json"), "utf8").catch(() => "{}");
4282
4815
  const tokens = extractTokens(JSON.parse(pkgStr || "{}"));
4283
4816
  const stackMeta = { frontend: tokens.frontend, backend: tokens.backend, aiTool: config.aiTool };
4284
4817
  let serverStackResponse = null;
@@ -4310,6 +4843,9 @@ async function handleMultiJoin(cwd2, config, server, product, isJson, options, t
4310
4843
  htmlPath: p.htmlPath,
4311
4844
  delivery: p.delivery ?? config.delivery ?? "embed"
4312
4845
  })),
4846
+ skillWarnings,
4847
+ hiddenFromGit: hidden.status,
4848
+ trackedPinsayFiles: hidden.tracked,
4313
4849
  cliVersion: BUILD_CLI_VERSION
4314
4850
  }));
4315
4851
  } else {
@@ -4397,12 +4933,26 @@ async function handleMultiProjectSetup(args) {
4397
4933
  }
4398
4934
  }
4399
4935
  closePrompts();
4936
+ const skillWarnings = [];
4937
+ const hide = [];
4400
4938
  if (!options["no-skills"]) {
4401
4939
  for (const t of tools) {
4402
- await installSkills(server, t, cwd2, t === tool ? options["skills-dir"] : void 0).catch(() => {
4403
- });
4940
+ try {
4941
+ const r = await installSkills(server, t, cwd2, t === tool ? options["skills-dir"] : void 0);
4942
+ skillWarnings.push(...r.warnings);
4943
+ hide.push(...r.hide);
4944
+ } catch (err) {
4945
+ skillWarnings.push({ tool: t, path: "(all skill files)", message: `could not install the skills (${err?.message ?? err}).`, hint: 'Run "npx pinsay-cli update" to try again.' });
4946
+ }
4404
4947
  }
4948
+ if (!isJson)
4949
+ for (const line of formatSkillWarnings(skillWarnings))
4950
+ console.error(line);
4405
4951
  }
4952
+ const hidden = await hidePinsayFiles(cwd2, [...skillsDirExtra(options["skills-dir"]), ...hide]);
4953
+ if (!isJson)
4954
+ for (const line of formatHideWarnings(hidden))
4955
+ console.error(line);
4406
4956
  const projectsMap = { ...config.projects ?? {} };
4407
4957
  let migrationNote = null;
4408
4958
  let migrationOk = null;
@@ -4426,7 +4976,6 @@ async function handleMultiProjectSetup(args) {
4426
4976
  for (const r of results)
4427
4977
  projectsMap[r.key] = r.entry;
4428
4978
  await writeConfigFull(cwd2, {
4429
- server,
4430
4979
  aiTool: tool,
4431
4980
  skillsDir: (options["skills-dir"] || config.skillsDir) ?? void 0,
4432
4981
  cliVersion: BUILD_CLI_VERSION,
@@ -4434,7 +4983,7 @@ async function handleMultiProjectSetup(args) {
4434
4983
  projects: projectsMap
4435
4984
  });
4436
4985
  if (writeLocalCreds) {
4437
- await writeCredentials(cwd2, key, { server });
4986
+ await writeCredentials(cwd2, key);
4438
4987
  }
4439
4988
  await postEvent(server, token, {
4440
4989
  type: "installed",
@@ -4451,6 +5000,9 @@ async function handleMultiProjectSetup(args) {
4451
5000
  const r = results.find((res) => res.key === k);
4452
5001
  return { key: k, path: p.path, injected: r ? r.injected : false, htmlPath: p.htmlPath, delivery: p.delivery ?? repoDefaultDelivery };
4453
5002
  }),
5003
+ skillWarnings,
5004
+ hiddenFromGit: hidden.status,
5005
+ trackedPinsayFiles: hidden.tracked,
4454
5006
  cliVersion: BUILD_CLI_VERSION
4455
5007
  }));
4456
5008
  } else {
@@ -4467,10 +5019,11 @@ ${migrationLine} Config: .pinsay/config.json (projects map)
4467
5019
 
4468
5020
  // src/commands/doctor.ts
4469
5021
  init_config();
4470
- import { promises as fs15 } from "node:fs";
4471
- import { join as join15 } from "node:path";
5022
+ import { promises as fs17 } from "node:fs";
5023
+ import { join as join16 } from "node:path";
4472
5024
  init_api();
4473
5025
  init_credentials();
5026
+ init_server();
4474
5027
  var ICON = { ok: "\u2714", warn: "\u26A0", error: "\u2718" };
4475
5028
  function exitCodeFor(checks) {
4476
5029
  const failed = checks.filter((c) => c.status === "error");
@@ -4485,7 +5038,7 @@ async function doctorCommand(cwd2, options, cliVersion) {
4485
5038
  const config = await readConfig(cwd2);
4486
5039
  const multi = isMultiProject(config);
4487
5040
  const projectKey = multi ? options.project || listProjects(config)[0]?.key : void 0;
4488
- const appCwd = multi && projectKey ? join15(cwd2, config.projects?.[projectKey]?.path ?? ".") : cwd2;
5041
+ const appCwd = multi && projectKey ? join16(cwd2, config.projects?.[projectKey]?.path ?? ".") : cwd2;
4489
5042
  const start = Date.now();
4490
5043
  const designBlock = await detectDesignTokens(appCwd, { root: cwd2 });
4491
5044
  const detectMs = Date.now() - start;
@@ -4500,11 +5053,12 @@ async function doctorCommand(cwd2, options, cliVersion) {
4500
5053
  }
4501
5054
  return 0;
4502
5055
  }
4503
- let checks = await runInitChecks(cwd2, { server: options.server, project: options.project }, cliVersion);
5056
+ const server = (options.server || resolveServer()).replace(/\/$/, "");
5057
+ let checks = await runInitChecks(cwd2, { server, project: options.project }, cliVersion);
4504
5058
  if (options.fix) {
4505
- const repaired = await applyFixes(cwd2, checks);
5059
+ const repaired = await applyFixes(cwd2, checks, server);
4506
5060
  if (repaired.length > 0) {
4507
- checks = await runInitChecks(cwd2, { server: options.server, project: options.project }, cliVersion);
5061
+ checks = await runInitChecks(cwd2, { server, project: options.project }, cliVersion);
4508
5062
  }
4509
5063
  }
4510
5064
  const code = exitCodeFor(checks);
@@ -4534,7 +5088,7 @@ async function reportRun(cwd2, options, checks, ok) {
4534
5088
  return;
4535
5089
  try {
4536
5090
  const config = await readConfig(cwd2);
4537
- const server = (options.server || config.server || "").replace(/\/$/, "");
5091
+ const server = (options.server || resolveServer()).replace(/\/$/, "");
4538
5092
  if (!server)
4539
5093
  return;
4540
5094
  const { key: apiKey } = await resolveApiKey(cwd2, server);
@@ -4551,10 +5105,9 @@ async function reportRun(cwd2, options, checks, ok) {
4551
5105
  } catch {
4552
5106
  }
4553
5107
  }
4554
- async function applyFixes(cwd2, checks) {
5108
+ async function applyFixes(cwd2, checks, server) {
4555
5109
  const repaired = [];
4556
5110
  const config = await readConfig(cwd2);
4557
- const server = (config.server || "").replace(/\/$/, "");
4558
5111
  let token;
4559
5112
  const tokenFor = async () => {
4560
5113
  if (token)
@@ -4576,11 +5129,10 @@ async function applyFixes(cwd2, checks) {
4576
5129
  continue;
4577
5130
  try {
4578
5131
  if (check.id === "gitignore") {
4579
- const path = join15(cwd2, ".gitignore");
4580
- const before = await fs15.readFile(path, "utf8").catch(() => "");
4581
- await upsertGitignore(cwd2, "Feedback tool", config.skillsDir);
4582
- const after = await fs15.readFile(path, "utf8").catch(() => "");
4583
- if (after !== before)
5132
+ const r = await hidePinsayFiles(cwd2, skillsDirExtra(config.skillsDir));
5133
+ for (const line of formatHideWarnings(r))
5134
+ console.error(line);
5135
+ if (r.status === "written" || r.status === "unchanged")
4584
5136
  repaired.push(check.id);
4585
5137
  } else if (check.id === "source-map") {
4586
5138
  const { buildManifest: buildManifest2 } = await Promise.resolve().then(() => (init_map(), map_exports));
@@ -4588,8 +5140,12 @@ async function applyFixes(cwd2, checks) {
4588
5140
  if (built.ok)
4589
5141
  repaired.push(check.id);
4590
5142
  } else if (check.id === "skills" && server && config.aiTool) {
4591
- await installSkills(server, config.aiTool, cwd2, config.skillsDir);
4592
- repaired.push(check.id);
5143
+ const r = await installSkills(server, config.aiTool, cwd2, config.skillsDir);
5144
+ for (const line of formatSkillWarnings(r.warnings))
5145
+ console.error(line);
5146
+ await hidePinsayFiles(cwd2, [...skillsDirExtra(config.skillsDir), ...r.hide]);
5147
+ if (r.warnings.length === 0)
5148
+ repaired.push(check.id);
4593
5149
  }
4594
5150
  } catch {
4595
5151
  }
@@ -4600,7 +5156,7 @@ async function applyFixes(cwd2, checks) {
4600
5156
  let any = false;
4601
5157
  for (const t of targets) {
4602
5158
  try {
4603
- const appCwd = multi ? join15(cwd2, t.path) : cwd2;
5159
+ const appCwd = multi ? join16(cwd2, t.path) : cwd2;
4604
5160
  const detection = await detectStack(appCwd);
4605
5161
  const stackToken = await tokenFor();
4606
5162
  const stack = stackToken ? await api(server, `/api/projects/${t.key}/stack`, {
@@ -4609,7 +5165,7 @@ async function applyFixes(cwd2, checks) {
4609
5165
  body: { kind: detection.kind, evidence: detection.evidence }
4610
5166
  }).catch(() => null) : null;
4611
5167
  if (stack) {
4612
- await fs15.writeFile(join15(cwd2, stackFileRelPath(multi ? t.key : void 0)), JSON.stringify(stack, null, 2) + "\n", "utf8");
5168
+ await fs17.writeFile(join16(cwd2, stackFileRelPath(multi ? t.key : void 0)), JSON.stringify(stack, null, 2) + "\n", "utf8");
4613
5169
  any = true;
4614
5170
  }
4615
5171
  } catch {
@@ -4624,8 +5180,8 @@ async function applyFixes(cwd2, checks) {
4624
5180
  // src/commands/update.ts
4625
5181
  init_config();
4626
5182
  init_api();
4627
- import { promises as fs16 } from "node:fs";
4628
- import { dirname as dirname7, join as join16 } from "node:path";
5183
+ import { promises as fs18 } from "node:fs";
5184
+ init_server();
4629
5185
  function sourceFor(path) {
4630
5186
  if (path.endsWith("pinsay.sh"))
4631
5187
  return "/pinsay.sh";
@@ -4649,9 +5205,9 @@ async function updateCommand(cwd2, options) {
4649
5205
  for (const f of removedLegacyFiles)
4650
5206
  console.log(`\x1B[2mremoved legacy ${f}\x1B[0m`);
4651
5207
  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.");
5208
+ const server = (options.server || resolveServer()).replace(/\/$/, "");
5209
+ if (!config.project && !isMultiProject(config)) {
5210
+ console.error("No .pinsay/config.json here \u2014 run `npx -y pinsay-cli init` first.");
4655
5211
  return 1;
4656
5212
  }
4657
5213
  let served = null;
@@ -4659,16 +5215,17 @@ async function updateCommand(cwd2, options) {
4659
5215
  const meta = await api(server, "/api/meta");
4660
5216
  served = meta?.skillVersion ?? null;
4661
5217
  } catch {
4662
- console.error(`Could not reach ${server} \u2014 check the URL.`);
5218
+ console.error(`Could not reach ${server}.`);
4663
5219
  return 1;
4664
5220
  }
4665
5221
  const files = skillFilesFor(config);
4666
5222
  const missing = [];
4667
5223
  const stale = [];
4668
5224
  for (const rel of files) {
4669
- const abs = join16(cwd2, rel);
5225
+ let abs;
4670
5226
  try {
4671
- await fs16.access(abs);
5227
+ abs = (await resolveRepoPath(cwd2, rel, { create: false })).abs;
5228
+ await fs18.access(abs);
4672
5229
  } catch {
4673
5230
  missing.push(rel);
4674
5231
  continue;
@@ -4679,6 +5236,10 @@ async function updateCommand(cwd2, options) {
4679
5236
  }
4680
5237
  if (missing.length === 0 && stale.length === 0) {
4681
5238
  console.log(`Up to date (skill version ${served ?? "unknown"}).`);
5239
+ if (!options.check) {
5240
+ for (const line of formatHideWarnings(await hidePinsayFiles(cwd2, skillsDirExtra(config.skillsDir))))
5241
+ console.error(line);
5242
+ }
4682
5243
  return 0;
4683
5244
  }
4684
5245
  if (options.check) {
@@ -4695,17 +5256,31 @@ async function updateCommand(cwd2, options) {
4695
5256
  return 0;
4696
5257
  }
4697
5258
  let installedCount = 0;
5259
+ let installHide = [];
4698
5260
  if (missing.length > 0) {
4699
5261
  if (!config.aiTool) {
4700
5262
  console.error("No AI tool configured \u2014 run `npx -y pinsay-cli init` to record one, then `update` again.");
4701
5263
  } else {
4702
5264
  try {
4703
- await installSkills(server, config.aiTool, cwd2, config.skillsDir);
4704
- installedCount = missing.length;
4705
- console.log(`installed ${installedCount} file${installedCount === 1 ? "" : "s"}: ${missing.join(", ")}`);
5265
+ const r = await installSkills(server, config.aiTool, cwd2, config.skillsDir);
5266
+ for (const line of formatSkillWarnings(r.warnings))
5267
+ console.error(line);
5268
+ installHide = r.hide;
4706
5269
  } catch (err) {
4707
5270
  console.error(` failed to install missing skills: ${err?.message ?? err}`);
4708
5271
  }
5272
+ const nowPresent = [];
5273
+ for (const rel of missing) {
5274
+ try {
5275
+ await fs18.access((await resolveRepoPath(cwd2, rel, { create: false })).abs);
5276
+ nowPresent.push(rel);
5277
+ } catch {
5278
+ }
5279
+ }
5280
+ installedCount = nowPresent.length;
5281
+ if (installedCount > 0) {
5282
+ console.log(`installed ${installedCount} file${installedCount === 1 ? "" : "s"}: ${nowPresent.join(", ")}`);
5283
+ }
4709
5284
  }
4710
5285
  }
4711
5286
  let updated = 0;
@@ -4725,11 +5300,10 @@ async function updateCommand(cwd2, options) {
4725
5300
  throw new Error(`HTTP ${res.status}`);
4726
5301
  body = await res.text();
4727
5302
  }
4728
- const abs = join16(cwd2, f.path);
4729
- await fs16.mkdir(dirname7(abs), { recursive: true });
4730
- await fs16.writeFile(abs, body, "utf8");
5303
+ const abs = (await resolveRepoPath(cwd2, f.path, { create: true })).abs;
5304
+ await fs18.writeFile(abs, body, "utf8");
4731
5305
  if (abs.endsWith(".sh"))
4732
- await fs16.chmod(abs, 493).catch(() => {
5306
+ await fs18.chmod(abs, 493).catch(() => {
4733
5307
  });
4734
5308
  updated++;
4735
5309
  } catch (err) {
@@ -4739,6 +5313,9 @@ async function updateCommand(cwd2, options) {
4739
5313
  if (stale.length > 0) {
4740
5314
  console.log(`updated ${updated} file${updated === 1 ? "" : "s"} (skill version ${from} \u2192 ${served ?? "unknown"})`);
4741
5315
  }
5316
+ const hidden = await hidePinsayFiles(cwd2, [...skillsDirExtra(config.skillsDir), ...installHide]);
5317
+ for (const line of formatHideWarnings(hidden))
5318
+ console.error(line);
4742
5319
  const installedOk = missing.length === 0 || installedCount === missing.length;
4743
5320
  const updatedOk = updated === stale.length;
4744
5321
  return installedOk && updatedOk ? 0 : 1;
@@ -4749,7 +5326,7 @@ init_config();
4749
5326
  init_api();
4750
5327
 
4751
5328
  // src/lib/notify-update.ts
4752
- import { promises as fs17 } from "node:fs";
5329
+ import { promises as fs19 } from "node:fs";
4753
5330
  import { join as join17 } from "node:path";
4754
5331
  init_build_constants();
4755
5332
  async function checkAndNotifyUpdates(options) {
@@ -4775,7 +5352,7 @@ async function checkAndNotifyUpdates(options) {
4775
5352
  for (const rel of files) {
4776
5353
  const abs = join17(options.cwd, rel);
4777
5354
  try {
4778
- await fs17.access(abs);
5355
+ await fs19.access(abs);
4779
5356
  const installed = await readStamp(abs);
4780
5357
  if (installed && installed !== meta.skillVersion) {
4781
5358
  hasStale = true;
@@ -4805,7 +5382,7 @@ init_build_constants();
4805
5382
  // src/apply/run.ts
4806
5383
  init_resolve();
4807
5384
  init_api();
4808
- import { promises as fs20 } from "node:fs";
5385
+ import { promises as fs22 } from "node:fs";
4809
5386
  import { join as join20, dirname as dirname9 } from "node:path";
4810
5387
  import { spawnSync } from "node:child_process";
4811
5388
 
@@ -4964,7 +5541,7 @@ async function fetchQueue(ctx, filter) {
4964
5541
 
4965
5542
  // src/apply/context.ts
4966
5543
  init_api();
4967
- import { promises as fs19 } from "node:fs";
5544
+ import { promises as fs21 } from "node:fs";
4968
5545
  import { join as join19 } from "node:path";
4969
5546
  init_config();
4970
5547
  async function loadProjectContext(ctx) {
@@ -4976,7 +5553,7 @@ async function loadProjectContext(ctx) {
4976
5553
  if (config.delegation === "off")
4977
5554
  delegation = "off";
4978
5555
  const relPath = isMultiProject(config) ? stackFileRelPath(ctx.project) : stackFileRelPath();
4979
- const stackRaw = await fs19.readFile(join19(ctx.cwd, relPath), "utf8");
5556
+ const stackRaw = await fs21.readFile(join19(ctx.cwd, relPath), "utf8");
4980
5557
  stack = JSON.parse(stackRaw);
4981
5558
  } catch {
4982
5559
  }
@@ -5351,7 +5928,7 @@ async function ensureToolRegistered(ctx, tool) {
5351
5928
  const stackPath = join20(ctx.cwd, relStackPath);
5352
5929
  let stackData = {};
5353
5930
  try {
5354
- const raw = await fs20.readFile(stackPath, "utf8");
5931
+ const raw = await fs22.readFile(stackPath, "utf8");
5355
5932
  stackData = JSON.parse(raw);
5356
5933
  } catch {
5357
5934
  }
@@ -5370,8 +5947,8 @@ async function ensureToolRegistered(ctx, tool) {
5370
5947
  }
5371
5948
  );
5372
5949
  if (res) {
5373
- await fs20.mkdir(dirname9(stackPath), { recursive: true });
5374
- await fs20.writeFile(stackPath, JSON.stringify(res, null, 2) + "\n", "utf8");
5950
+ await fs22.mkdir(dirname9(stackPath), { recursive: true });
5951
+ await fs22.writeFile(stackPath, JSON.stringify(res, null, 2) + "\n", "utf8");
5375
5952
  return;
5376
5953
  }
5377
5954
  } catch {
@@ -5380,8 +5957,8 @@ async function ensureToolRegistered(ctx, tool) {
5380
5957
  aiTools.push(tool);
5381
5958
  stackData.aiTools = aiTools;
5382
5959
  try {
5383
- await fs20.mkdir(dirname9(stackPath), { recursive: true });
5384
- await fs20.writeFile(stackPath, JSON.stringify(stackData, null, 2) + "\n", "utf8");
5960
+ await fs22.mkdir(dirname9(stackPath), { recursive: true });
5961
+ await fs22.writeFile(stackPath, JSON.stringify(stackData, null, 2) + "\n", "utf8");
5385
5962
  } catch {
5386
5963
  }
5387
5964
  }
@@ -5456,8 +6033,8 @@ async function runApply(options, ctx) {
5456
6033
  }
5457
6034
  } else if (tool === "cursor") {
5458
6035
  const promptFile = join20(ctx.cwd, ".pinsay/apply-prompt.md");
5459
- await fs20.mkdir(join20(ctx.cwd, ".pinsay"), { recursive: true });
5460
- await fs20.writeFile(promptFile, prompt, "utf8");
6036
+ await fs22.mkdir(join20(ctx.cwd, ".pinsay"), { recursive: true });
6037
+ await fs22.writeFile(promptFile, prompt, "utf8");
5461
6038
  console.log(`Saved apply prompt to ${promptFile}`);
5462
6039
  } else if (tool === "clipboard") {
5463
6040
  const copied = copyToClipboard(prompt);
@@ -5823,6 +6400,7 @@ function describeFilter(filter) {
5823
6400
 
5824
6401
  // src/commands/apply.ts
5825
6402
  init_projection();
6403
+ init_server();
5826
6404
  async function exitIfWorkspaceFrozen(server, token) {
5827
6405
  if (!token)
5828
6406
  return;
@@ -5871,11 +6449,7 @@ function parseModelFlags(parsed) {
5871
6449
  async function applyCommand(cwd2, parsed, positionals = []) {
5872
6450
  const root = await findRepoRoot(cwd2);
5873
6451
  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
- }
6452
+ const server = resolveServer();
5879
6453
  const projectFlag = typeof parsed["project"] === "string" ? parsed["project"] : void 0;
5880
6454
  const resolved = resolveProject(config, cwd2, root, projectFlag);
5881
6455
  const markVal = parsed["mark"];
@@ -11002,7 +11576,7 @@ var Protocol = class {
11002
11576
  * Do not use this method to emit notifications! Use notification() instead.
11003
11577
  */
11004
11578
  request(request, resultSchema, options) {
11005
- return new Promise((resolve5, reject) => {
11579
+ return new Promise((resolve9, reject) => {
11006
11580
  var _a, _b, _c, _d;
11007
11581
  if (!this._transport) {
11008
11582
  reject(new Error("Not connected"));
@@ -11039,7 +11613,7 @@ var Protocol = class {
11039
11613
  }
11040
11614
  try {
11041
11615
  const result = resultSchema.parse(response.result);
11042
- resolve5(result);
11616
+ resolve9(result);
11043
11617
  } catch (error) {
11044
11618
  reject(error);
11045
11619
  }
@@ -11392,12 +11966,12 @@ var StdioServerTransport = class {
11392
11966
  (_a = this.onclose) === null || _a === void 0 ? void 0 : _a.call(this);
11393
11967
  }
11394
11968
  send(message) {
11395
- return new Promise((resolve5) => {
11969
+ return new Promise((resolve9) => {
11396
11970
  const json = serializeMessage(message);
11397
11971
  if (this._stdout.write(json)) {
11398
- resolve5();
11972
+ resolve9();
11399
11973
  } else {
11400
- this._stdout.once("drain", resolve5);
11974
+ this._stdout.once("drain", resolve9);
11401
11975
  }
11402
11976
  });
11403
11977
  }
@@ -11662,7 +12236,7 @@ var ALL_TOOLS = [
11662
12236
  // src/mcp/tools.ts
11663
12237
  init_api();
11664
12238
  import { existsSync as existsSync4, readFileSync as readFileSync2 } from "node:fs";
11665
- import { isAbsolute as isAbsolute3, join as join21, relative as relative5, resolve as resolve4 } from "node:path";
12239
+ import { isAbsolute as isAbsolute4, join as join21, relative as relative7, resolve as resolve8 } from "node:path";
11666
12240
  import { spawnSync as spawnSync3 } from "node:child_process";
11667
12241
  init_build_constants();
11668
12242
  init_projection();
@@ -11976,12 +12550,12 @@ async function handleCommitAndMark(args, ctx) {
11976
12550
  if (Array.isArray(files) && files.length > 0) {
11977
12551
  for (const entry of files) {
11978
12552
  const cleanPath = entry.includes(":") ? entry.split(":").slice(1).join(":") : entry;
11979
- if (isAbsolute3(cleanPath)) {
12553
+ if (isAbsolute4(cleanPath)) {
11980
12554
  throw mcpError("git", `Path must be relative to repo root: ${cleanPath}`);
11981
12555
  }
11982
- const resolved = resolve4(ctx.cwd, cleanPath);
11983
- const rel = relative5(ctx.cwd, resolved);
11984
- if (rel.startsWith("..") || isAbsolute3(rel)) {
12556
+ const resolved = resolve8(ctx.cwd, cleanPath);
12557
+ const rel = relative7(ctx.cwd, resolved);
12558
+ if (rel.startsWith("..") || isAbsolute4(rel)) {
11985
12559
  throw mcpError("git", `Path escapes repository root: ${cleanPath}`);
11986
12560
  }
11987
12561
  if (!existsSync4(resolved)) {
@@ -12452,8 +13026,8 @@ function createMcpServer(ctx) {
12452
13026
  async function runMcpServer(ctx, logFile) {
12453
13027
  if (logFile) {
12454
13028
  try {
12455
- const fs21 = await import("node:fs");
12456
- const logStream = fs21.createWriteStream(logFile, { flags: "a" });
13029
+ const fs23 = await import("node:fs");
13030
+ const logStream = fs23.createWriteStream(logFile, { flags: "a" });
12457
13031
  const origWrite = process.stderr.write;
12458
13032
  process.stderr.write = function(chunk, encoding, cb) {
12459
13033
  logStream.write(chunk);
@@ -12468,10 +13042,11 @@ async function runMcpServer(ctx, logFile) {
12468
13042
  }
12469
13043
 
12470
13044
  // src/commands/mcp.ts
13045
+ init_server();
12471
13046
  async function mcpCommand(cwd2, parsed = {}) {
12472
13047
  const root = await findRepoRoot(cwd2);
12473
13048
  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(/\/$/, "");
13049
+ const server = resolveServer();
12475
13050
  const projectFlag = typeof parsed["project"] === "string" ? parsed["project"] : void 0;
12476
13051
  const project = projectFlag || config.project || process.env.PINSAY_PROJECT || "";
12477
13052
  const explicitKey = typeof parsed["key"] === "string" ? parsed["key"] : void 0;
@@ -12508,11 +13083,15 @@ async function mcpCommand(cwd2, parsed = {}) {
12508
13083
  init_config();
12509
13084
  init_api();
12510
13085
  init_credentials();
12511
- init_build_constants();
13086
+ init_server();
12512
13087
  async function loginCommand(cwd2, options = {}) {
13088
+ const flags = scopeFromFlags(options);
13089
+ if (flags.error) {
13090
+ console.error(flags.error);
13091
+ process.exit(2);
13092
+ }
12513
13093
  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(/\/$/, "");
13094
+ const server = resolveServer();
12516
13095
  const branding = await getBranding(server);
12517
13096
  const product = branding.productName;
12518
13097
  const flagKey = typeof options["key"] === "string" ? options["key"] : void 0;
@@ -12547,14 +13126,15 @@ async function loginCommand(cwd2, options = {}) {
12547
13126
  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
13127
  process.exit(2);
12549
13128
  }
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
- }
13129
+ const interactive = Boolean(process.stdin.isTTY) && options["yes"] !== true;
13130
+ const decided = decideKeyScope(flags.scope, interactive);
13131
+ const scope = decided === "ask" ? await askKeyScope() : decided;
13132
+ closePrompts();
12555
13133
  const who = me?.displayName ? `${me.displayName}${me?.email ? ` (${me.email})` : ""}` : me?.email ?? "you";
12556
13134
  if (scope === "repo") {
12557
- await writeCredentials(root, key, { server });
13135
+ await writeCredentials(root, key);
13136
+ for (const line of formatHideWarnings(await hidePinsayFiles(root)))
13137
+ console.error(line);
12558
13138
  console.log(`\u2714 Signed in to ${server} as ${who} \u2014 saved to .pinsay/credentials.env (this repo only; overrides the global store here)`);
12559
13139
  process.exit(0);
12560
13140
  }
@@ -12564,13 +13144,10 @@ async function loginCommand(cwd2, options = {}) {
12564
13144
  }
12565
13145
 
12566
13146
  // src/commands/logout.ts
12567
- init_config();
12568
13147
  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(/\/$/, "");
13148
+ init_server();
13149
+ async function logoutCommand(_cwd, options = {}) {
13150
+ const server = resolveServer();
12574
13151
  const origin = normalizeServerOrigin(server);
12575
13152
  const removed = await removeGlobalCredential(server);
12576
13153
  if (options["json"] === true) {
@@ -12587,11 +13164,10 @@ async function logoutCommand(cwd2, options = {}) {
12587
13164
  init_config();
12588
13165
  init_api();
12589
13166
  init_credentials();
12590
- init_build_constants();
13167
+ init_server();
12591
13168
  async function whoamiCommand(cwd2, options = {}) {
12592
13169
  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(/\/$/, "");
13170
+ const server = resolveServer();
12595
13171
  const isJson = options["json"] === true;
12596
13172
  const { key, source } = await resolveApiKey(root, server);
12597
13173
  if (!key) {
@@ -12631,6 +13207,7 @@ async function whoamiCommand(cwd2, options = {}) {
12631
13207
  // src/cli.ts
12632
13208
  init_build_constants();
12633
13209
  init_api();
13210
+ init_server();
12634
13211
  import { argv, cwd } from "node:process";
12635
13212
  function parseArgs(args) {
12636
13213
  const parsed = {};
@@ -12687,8 +13264,8 @@ Usage: npx pinsay-cli <command> [options]
12687
13264
 
12688
13265
  Commands:
12689
13266
  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
13267
+ login Sign in and save your API key (this machine, or this repo only)
13268
+ logout Remove this machine's saved key
12692
13269
  whoami Show the signed-in account and where the API key came from
12693
13270
  doctor Diagnose an install and report what is wrong
12694
13271
  update Refresh the served skills to the server's current version
@@ -12719,13 +13296,19 @@ async function main() {
12719
13296
  process.exit(0);
12720
13297
  }
12721
13298
  }
13299
+ if (command !== "map" && !parsed["help"]) {
13300
+ const serverError = await serverSettingError(cwd(), parsed);
13301
+ if (serverError) {
13302
+ console.error(serverError);
13303
+ process.exit(2);
13304
+ }
13305
+ }
12722
13306
  if (command === "init") {
12723
13307
  if (parsed["help"]) {
12724
13308
  console.log(`
12725
13309
  Usage: npx pinsay-cli init [options]
12726
13310
 
12727
13311
  Options:
12728
- --server <url> Feedback server URL
12729
13312
  --key <key> API key
12730
13313
  --project <key> Project key
12731
13314
  --create <name> Create project with name
@@ -12744,9 +13327,10 @@ Options:
12744
13327
  --no-skills Skip skills installation
12745
13328
  --no-design Skip design token detection
12746
13329
  --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
13330
+ --scope <global|repo> Where the API key is stored, without asking: global (this machine, all
13331
+ repos, ~/.config/pinsay/credentials.json; the default with --yes) or
13332
+ repo (.pinsay/credentials.env, hidden from git, this repo only).
13333
+ --local-credentials is an alias for --scope repo
12750
13334
  --no-browser When signing in in the browser (first run, no key resolved yet), print
12751
13335
  the link/code but don't try to open a browser
12752
13336
  -y, --yes Non-interactive
@@ -12761,21 +13345,21 @@ Options:
12761
13345
  console.log(`
12762
13346
  Usage: npx pinsay-cli login [options]
12763
13347
 
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.
13348
+ Sign in and save your API key. On a terminal it asks where to save it:
13349
+ Global this machine, every repo (~/.config/pinsay/credentials.json, honours $XDG_CONFIG_HOME)
13350
+ Repo .pinsay/credentials.env in this repo only (hidden from git)
13351
+ With no terminal (CI, a pipe) or with --yes it saves globally, unless --scope repo is given.
12767
13352
 
12768
13353
  With no --key on a real terminal, opens your browser to sign in (mirrors \`gh auth login\`): prints
12769
13354
  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.
13355
+ key it hands back. Pass --key to skip the browser and validate a pasted key instead.
12772
13356
 
12773
13357
  Options:
12774
- --server <url> Server URL (default: this repo's .pinsay/config.json, then $PINSAY_SERVER)
12775
13358
  --key <key> API key \u2014 skips the browser flow entirely
12776
13359
  --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
13360
+ --scope <global|repo> Where to save the key, without asking
13361
+ --local-credentials Alias for --scope repo
13362
+ -y, --yes Don't ask; save globally unless --scope repo is given
12779
13363
  -h, --help Show this help
12780
13364
  `);
12781
13365
  process.exit(0);
@@ -12786,10 +13370,9 @@ Options:
12786
13370
  console.log(`
12787
13371
  Usage: npx pinsay-cli logout [options]
12788
13372
 
12789
- Removes this machine's saved global key for a server.
13373
+ Removes this machine's saved global key.
12790
13374
 
12791
13375
  Options:
12792
- --server <url> Server URL (default: this repo's .pinsay/config.json, then $PINSAY_SERVER)
12793
13376
  --json Emit { ok, server, removed } as JSON
12794
13377
  -h, --help Show this help
12795
13378
  `);
@@ -12805,7 +13388,6 @@ Prints the server, the signed-in account, and which of env/repo/global answered
12805
13388
  never the key itself.
12806
13389
 
12807
13390
  Options:
12808
- --server <url> Server URL (default: this repo's .pinsay/config.json, then $PINSAY_SERVER)
12809
13391
  --json Emit { ok, server, displayName, email, source } as JSON
12810
13392
  -h, --help Show this help
12811
13393
  `);
@@ -12820,7 +13402,6 @@ Usage: npx pinsay-cli doctor [options]
12820
13402
  Checks an existing install and prints one line per check.
12821
13403
 
12822
13404
  Options:
12823
- --server <url> Override the server from .pinsay/config.json
12824
13405
  --project <key> Override the project key
12825
13406
  --json Emit { ok, checks } as JSON
12826
13407
  --fix Apply the idempotent repairs (gitignore, skills, stack)
@@ -12836,7 +13417,6 @@ Exit codes:
12836
13417
  process.exit(0);
12837
13418
  }
12838
13419
  const code = await doctorCommand(cwd(), {
12839
- server: typeof parsed["server"] === "string" ? parsed["server"] : void 0,
12840
13420
  project: typeof parsed["project"] === "string" ? parsed["project"] : void 0,
12841
13421
  json: parsed["json"] === true,
12842
13422
  fix: parsed["fix"] === true,
@@ -12848,17 +13428,15 @@ Exit codes:
12848
13428
  console.log(`
12849
13429
  Usage: npx pinsay-cli update [options]
12850
13430
 
12851
- Refreshes the AI skills and pinsay.sh from the configured server.
13431
+ Refreshes the AI skills and pinsay.sh from the server.
12852
13432
 
12853
13433
  Options:
12854
- --server <url> Override the server from .pinsay/config.json
12855
13434
  --check Report what is out of date without writing anything
12856
13435
  -h, --help Show this help
12857
13436
  `);
12858
13437
  process.exit(0);
12859
13438
  }
12860
13439
  const code = await updateCommand(cwd(), {
12861
- server: typeof parsed["server"] === "string" ? parsed["server"] : void 0,
12862
13440
  check: parsed["check"] === true
12863
13441
  });
12864
13442
  process.exit(code);
@@ -12979,7 +13557,6 @@ Start the PinSay stdio MCP server for AI tools.
12979
13557
 
12980
13558
  Options:
12981
13559
  --log <file> Log MCP server traffic to file
12982
- --server <url> Feedback server URL
12983
13560
  --project <key> Project key
12984
13561
  --key <key> API key
12985
13562
  -h, --help Show this help