pinsay-cli 0.8.0 → 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 (2) hide show
  1. package/dist/cli.js +784 -300
  2. package/package.json +1 -1
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.8.0" : "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;
@@ -145,48 +145,7 @@ async function removeLegacyRepoFiles(cwd2) {
145
145
  }
146
146
  return removed;
147
147
  }
148
- async function upsertGitignore(cwd2, productName = "Feedback tool", skillsDir) {
149
- const file = join(cwd2, ".gitignore");
150
- let content = await fs.readFile(file, "utf8").catch(() => "");
151
- const before = content;
152
- const skillsDirPaths = skillsDir ? [`${skillsDir.replace(/\/+$/, "")}/pinsay-init/`, `${skillsDir.replace(/\/+$/, "")}/pinsay-feedback/`] : [];
153
- const allSkillPaths = [...IGNORED_SKILL_DIRS, ...skillsDirPaths];
154
- const entry = [
155
- "",
156
- `# ${productName}`,
157
- // `.pinsay/*`, not `.pinsay/`. Git does not descend into an excluded DIRECTORY, so the
158
- // directory form makes every `!` line below inert and the files this block exists to keep
159
- // committable are silently ignored instead.
160
- ".pinsay/*",
161
- "!.pinsay/stack.json",
162
- "!.pinsay/config.json",
163
- // Re-includes the directory itself (not a wildcard for its contents) — with nothing further
164
- // ignoring paths inside it, git descends and every `projects/<key>.stack.json` stays
165
- // committable, exactly like stack.json/config.json above.
166
- "!.pinsay/projects/",
167
- ...allSkillPaths,
168
- ""
169
- ].join("\n");
170
- content = content.replace(/^\.pinsay\/$/m, ".pinsay/*");
171
- content = content.replace(/\n?# [^\n]*\n\.pinsay\/credentials\.env\n/, "");
172
- content = content.replace(/^!\.pinsay\/pinsay\.sh\n/m, "");
173
- content = content.replace(/^!\.pinsay\/credentials\.env\.example\n/m, "");
174
- if (!/^!\.pinsay\/stack\.json$/m.test(content)) {
175
- content += entry;
176
- }
177
- const missingSkillDirs = allSkillPaths.filter((d) => !content.includes(d));
178
- if (missingSkillDirs.length > 0) {
179
- content += missingSkillDirs.map((d) => `${d}
180
- `).join("");
181
- }
182
- if (!content.includes("!.pinsay/projects/")) {
183
- content += "!.pinsay/projects/\n";
184
- }
185
- if (content !== before) {
186
- await fs.writeFile(file, content, "utf8");
187
- }
188
- }
189
- 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;
190
149
  var init_config = __esm({
191
150
  "src/config.ts"() {
192
151
  "use strict";
@@ -194,22 +153,6 @@ var init_config = __esm({
194
153
  CREDENTIALS_FILE = ".pinsay/credentials.env";
195
154
  LEGACY_SERVER = "https://api.pinsay.dev";
196
155
  CANONICAL_SERVER = "https://app.pinsay.dev";
197
- IGNORED_SKILL_DIRS = [
198
- ".claude/skills/pinsay-init/",
199
- ".claude/skills/pinsay-feedback/",
200
- // Current Agent Skills layout (2026-09-16+) — used by `other`/`antigravity`, and symlinked into
201
- // from claude-code/cursor/windsurf (see `writeOrLink` in skills.ts).
202
- ".agents/skills/pinsay-init/",
203
- ".agents/skills/pinsay-feedback/",
204
- // Legacy pre-2026-09-16 layout. `installSkills` removes these on every install, but a repo that
205
- // has not run init/update since still has them on disk until it does — keep ignoring them too.
206
- ".agents/pinsay-init/",
207
- ".agents/pinsay-feedback/",
208
- ".cursor/rules/pinsay-init.md",
209
- ".cursor/rules/pinsay-feedback.md",
210
- ".windsurf/rules/pinsay-init.md",
211
- ".windsurf/rules/pinsay-feedback.md"
212
- ];
213
156
  LEGACY_REPO_FILES = [".pinsay/credentials.env.example", ".pinsay/.token_cache"];
214
157
  }
215
158
  });
@@ -256,10 +199,10 @@ var init_api = __esm({
256
199
  });
257
200
 
258
201
  // src/credentials.ts
259
- import { promises as fs10 } from "node:fs";
202
+ import { promises as fs12 } from "node:fs";
260
203
  import { createHash } from "node:crypto";
261
204
  import { homedir } from "node:os";
262
- import { join as join10, dirname as dirname4 } from "node:path";
205
+ import { join as join11, dirname as dirname5 } from "node:path";
263
206
  function sourceLabel(source) {
264
207
  if (source === "env")
265
208
  return "env var";
@@ -278,34 +221,34 @@ function normalizeServerOrigin(server) {
278
221
  }
279
222
  function configBase() {
280
223
  if (process.platform === "win32") {
281
- return process.env.APPDATA || join10(homedir(), "AppData", "Roaming");
224
+ return process.env.APPDATA || join11(homedir(), "AppData", "Roaming");
282
225
  }
283
- return process.env.XDG_CONFIG_HOME || join10(homedir(), ".config");
226
+ return process.env.XDG_CONFIG_HOME || join11(homedir(), ".config");
284
227
  }
285
228
  function globalConfigDir() {
286
229
  if (process.env.PINSAY_CONFIG_DIR)
287
230
  return process.env.PINSAY_CONFIG_DIR;
288
- return join10(configBase(), DIR_NAME);
231
+ return join11(configBase(), DIR_NAME);
289
232
  }
290
233
  function globalCredentialsPath() {
291
- return join10(globalConfigDir(), "credentials.json");
234
+ return join11(globalConfigDir(), "credentials.json");
292
235
  }
293
236
  function legacyGlobalCredentialsPath() {
294
237
  if (process.env.PINSAY_CONFIG_DIR)
295
238
  return void 0;
296
- return join10(configBase(), LEGACY_DIR_NAME, "credentials.json");
239
+ return join11(configBase(), LEGACY_DIR_NAME, "credentials.json");
297
240
  }
298
241
  function globalCacheDir() {
299
242
  if (process.env.PINSAY_CONFIG_DIR)
300
- return join10(process.env.PINSAY_CONFIG_DIR, "cache");
243
+ return join11(process.env.PINSAY_CONFIG_DIR, "cache");
301
244
  if (process.platform === "win32") {
302
- return join10(process.env.LOCALAPPDATA || join10(homedir(), "AppData", "Local"), DIR_NAME, "cache");
245
+ return join11(process.env.LOCALAPPDATA || join11(homedir(), "AppData", "Local"), DIR_NAME, "cache");
303
246
  }
304
- return join10(process.env.XDG_CACHE_HOME || join10(homedir(), ".cache"), DIR_NAME);
247
+ return join11(process.env.XDG_CACHE_HOME || join11(homedir(), ".cache"), DIR_NAME);
305
248
  }
306
249
  function tokenCacheFile(server, apiKey) {
307
250
  const hash = createHash("sha256").update(`${normalizeServerOrigin(server)}:${apiKey}`).digest("hex").slice(0, 32);
308
- return join10(globalCacheDir(), `${hash}.json`);
251
+ return join11(globalCacheDir(), `${hash}.json`);
309
252
  }
310
253
  function isCredentialStore(value) {
311
254
  if (!value || typeof value !== "object" || Array.isArray(value))
@@ -317,7 +260,7 @@ function isCredentialStore(value) {
317
260
  async function readGlobalStore() {
318
261
  let raw;
319
262
  try {
320
- raw = await fs10.readFile(globalCredentialsPath(), "utf8");
263
+ raw = await fs12.readFile(globalCredentialsPath(), "utf8");
321
264
  } catch (err) {
322
265
  if (err?.code === "ENOENT")
323
266
  return migrateLegacyStore();
@@ -336,7 +279,7 @@ async function migrateLegacyStore() {
336
279
  return {};
337
280
  let legacy;
338
281
  try {
339
- legacy = JSON.parse(await fs10.readFile(legacyFile, "utf8"));
282
+ legacy = JSON.parse(await fs12.readFile(legacyFile, "utf8"));
340
283
  } catch {
341
284
  return {};
342
285
  }
@@ -347,17 +290,17 @@ async function migrateLegacyStore() {
347
290
  } catch {
348
291
  return legacy;
349
292
  }
350
- await fs10.rm(legacyFile, { force: true }).catch(() => {
293
+ await fs12.rm(legacyFile, { force: true }).catch(() => {
351
294
  });
352
- await fs10.rmdir(dirname4(legacyFile)).catch(() => {
295
+ await fs12.rmdir(dirname5(legacyFile)).catch(() => {
353
296
  });
354
297
  return legacy;
355
298
  }
356
299
  async function writeGlobalStore(store) {
357
300
  const file = globalCredentialsPath();
358
- await fs10.mkdir(dirname4(file), { recursive: true });
359
- await fs10.writeFile(file, JSON.stringify(store, null, 2) + "\n", { encoding: "utf8", mode: 384 });
360
- 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(() => {
361
304
  });
362
305
  }
363
306
  async function getGlobalCredential(server) {
@@ -386,7 +329,7 @@ async function removeGlobalCredential(server) {
386
329
  }
387
330
  async function readRepoApiKey(root) {
388
331
  try {
389
- const raw = await fs10.readFile(join10(root, ".pinsay", "credentials.env"), "utf8");
332
+ const raw = await fs12.readFile(join11(root, ".pinsay", "credentials.env"), "utf8");
390
333
  return raw.match(/^PINSAY_API_KEY=(.*)$/m)?.[1]?.trim() || void 0;
391
334
  } catch {
392
335
  return void 0;
@@ -407,7 +350,7 @@ async function resolveApiKey(root, server) {
407
350
  return { key: void 0, source: null };
408
351
  }
409
352
  async function removeStaleRepoTokenCache(root) {
410
- await fs10.rm(join10(root, ".pinsay", ".token_cache"), { force: true }).catch(() => {
353
+ await fs12.rm(join11(root, ".pinsay", ".token_cache"), { force: true }).catch(() => {
411
354
  });
412
355
  }
413
356
  var DIR_NAME, LEGACY_DIR_NAME;
@@ -638,8 +581,8 @@ __export(map_exports, {
638
581
  isSameManifest: () => isSameManifest,
639
582
  mapCommand: () => mapCommand
640
583
  });
641
- import { promises as fs14 } from "node:fs";
642
- 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";
643
586
  import { execFileSync } from "node:child_process";
644
587
  function gitRoot(cwd2) {
645
588
  try {
@@ -651,7 +594,7 @@ function gitRoot(cwd2) {
651
594
  async function walk(dir, out = []) {
652
595
  let entries;
653
596
  try {
654
- entries = await fs14.readdir(dir, { withFileTypes: true });
597
+ entries = await fs16.readdir(dir, { withFileTypes: true });
655
598
  } catch {
656
599
  return out;
657
600
  }
@@ -660,7 +603,7 @@ async function walk(dir, out = []) {
660
603
  if (SKIP_DIRS.has(entry.name))
661
604
  continue;
662
605
  }
663
- const full = join14(dir, entry.name);
606
+ const full = join15(dir, entry.name);
664
607
  if (entry.isDirectory()) {
665
608
  if (SKIP_DIRS.has(entry.name))
666
609
  continue;
@@ -712,10 +655,10 @@ async function buildManifest(cwd2, opts = {}) {
712
655
  let scanned = 0;
713
656
  let failed = 0;
714
657
  for (const file of files) {
715
- const relPath = toPosix(relative4(root, file));
658
+ const relPath = toPosix(relative6(root, file));
716
659
  let code;
717
660
  try {
718
- code = await fs14.readFile(file, "utf8");
661
+ code = await fs16.readFile(file, "utf8");
719
662
  } catch {
720
663
  continue;
721
664
  }
@@ -731,25 +674,25 @@ async function buildManifest(cwd2, opts = {}) {
731
674
  console.error(` skipped ${relPath}: ${err?.message ?? err}`);
732
675
  }
733
676
  }
734
- const target = resolve3(root, ".pinsay/manifest.json");
735
- 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 });
736
679
  const entries = Object.fromEntries(
737
680
  Object.entries(manifest).sort(([a], [b]) => a.localeCompare(b)).map(([hash, entry]) => [hash, { path: entry.path, component: entry.export }])
738
681
  );
739
682
  const nextContent = JSON.stringify({ version: 1, entries }, null, 2) + "\n";
740
683
  let currentContent = null;
741
684
  try {
742
- currentContent = await fs14.readFile(target, "utf8");
685
+ currentContent = await fs16.readFile(target, "utf8");
743
686
  } catch {
744
687
  currentContent = null;
745
688
  }
746
689
  const prev = target.replace(/\.json$/, ".prev.json");
747
690
  if (currentContent !== null && !isSameManifest(currentContent, nextContent)) {
748
- await fs14.copyFile(target, prev);
691
+ await fs16.copyFile(target, prev);
749
692
  }
750
693
  const tmp = `${target}.tmp`;
751
- await fs14.writeFile(tmp, nextContent, "utf8");
752
- await fs14.rename(tmp, target);
694
+ await fs16.writeFile(tmp, nextContent, "utf8");
695
+ await fs16.rename(tmp, target);
753
696
  const count = Object.keys(entries).length;
754
697
  if (!opts.quiet) {
755
698
  console.log(
@@ -781,7 +724,7 @@ var init_map = __esm({
781
724
  });
782
725
 
783
726
  // src/auth.ts
784
- import { promises as fs18 } from "node:fs";
727
+ import { promises as fs20 } from "node:fs";
785
728
  import { dirname as dirname8 } from "node:path";
786
729
  async function readApiKey(cwd2, server) {
787
730
  const { key } = await resolveApiKey(cwd2, server);
@@ -806,7 +749,7 @@ async function resolveToken(server, cwd2, explicitApiKey) {
806
749
  return void 0;
807
750
  const cacheFile = tokenCacheFile(server, apiKey);
808
751
  try {
809
- const cached = JSON.parse(await fs18.readFile(cacheFile, "utf8"));
752
+ const cached = JSON.parse(await fs20.readFile(cacheFile, "utf8"));
810
753
  const token = typeof cached?.token === "string" ? cached.token.trim() : "";
811
754
  if (token && !isJwtExpired(token))
812
755
  return token;
@@ -823,8 +766,8 @@ async function resolveToken(server, cwd2, explicitApiKey) {
823
766
  );
824
767
  if (login?.token) {
825
768
  try {
826
- await fs18.mkdir(dirname8(cacheFile), { recursive: true });
827
- 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");
828
771
  } catch {
829
772
  }
830
773
  return login.token;
@@ -1426,7 +1369,7 @@ async function menu(question, items, cursorStart, opts) {
1426
1369
  process.stdin.resume();
1427
1370
  render(true);
1428
1371
  try {
1429
- return await new Promise((resolve5) => {
1372
+ return await new Promise((resolve9) => {
1430
1373
  const onKey = (_str, key) => {
1431
1374
  if (key.ctrl && key.name === "c") {
1432
1375
  cleanup();
@@ -1453,7 +1396,7 @@ async function menu(question, items, cursorStart, opts) {
1453
1396
  selected.add(cursor);
1454
1397
  }
1455
1398
  cleanup();
1456
- resolve5(opts.multi ? [...selected].sort((a, b) => a - b) : [cursor]);
1399
+ resolve9(opts.multi ? [...selected].sort((a, b) => a - b) : [cursor]);
1457
1400
  }
1458
1401
  };
1459
1402
  const cleanup = () => {
@@ -1935,10 +1878,10 @@ ${indent}${call},` + rest.replace(/^\s*\n/, "\n") : next.slice(0, at) + `${call}
1935
1878
  const envPath = join6(cwd2, envName);
1936
1879
  const envBefore = await fs6.readFile(envPath, "utf8").catch(() => "");
1937
1880
  if (!/^VITE_PINSAY_SOURCE=/m.test(envBefore)) {
1938
- const sep3 = envBefore && !envBefore.endsWith("\n") ? "\n" : "";
1881
+ const sep6 = envBefore && !envBefore.endsWith("\n") ? "\n" : "";
1939
1882
  await fs6.writeFile(
1940
1883
  envPath,
1941
- `${envBefore}${sep3}# Stamps component source hashes for PinSay. Development only.
1884
+ `${envBefore}${sep6}# Stamps component source hashes for PinSay. Development only.
1942
1885
  VITE_PINSAY_SOURCE=true
1943
1886
  `,
1944
1887
  "utf8"
@@ -1959,35 +1902,318 @@ async function firstExisting2(cwd2, names) {
1959
1902
  }
1960
1903
 
1961
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
1962
1909
  import { promises as fs7 } from "node:fs";
1963
- import { join as join7, dirname as dirname2 } from "node:path";
1964
- async function fetchText(url) {
1965
- const res = await fetch(url);
1966
- if (!res.ok)
1967
- throw new Error(`Failed to fetch ${url}: ${res.status}`);
1968
- 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];
1969
1946
  }
1970
- async function download(url, dest, chmod = false) {
1971
- const txt = await fetchText(url);
1972
- await fs7.mkdir(dirname2(dest), { recursive: true });
1973
- await fs7.writeFile(dest, txt, "utf8");
1974
- if (chmod) {
1975
- 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
1976
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);
1977
2127
  }
2128
+ return r;
1978
2129
  }
1979
- async function makeSymlink(target, path) {
1980
- 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);
1981
2140
  try {
1982
- 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
+ }
1983
2149
  } catch (err) {
1984
- if (err.code === "EPERM" && process.platform === "win32") {
1985
- await fs7.copyFile(target, path);
1986
- } else if (err.code !== "EEXIST") {
1987
- 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);
1988
2160
  }
2161
+ return { abs: dest.abs, mode: "copy" };
1989
2162
  }
1990
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
+ };
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;
2216
+ }
1991
2217
  var SUB_SKILLS = ["apply", "translate", "advanced"];
1992
2218
  var SUB_SKILL_TITLES = {
1993
2219
  apply: "Apply workflow",
@@ -2035,57 +2261,231 @@ ${text.trim()}`
2035
2261
  }
2036
2262
  async function removeLegacyAgentsLayout(cwd2) {
2037
2263
  for (const name of ["pinsay-init", "pinsay-feedback"]) {
2038
- const filePath = join7(cwd2, ".agents", name, "SKILL.md");
2039
- const dirPath = join7(cwd2, ".agents", name);
2264
+ const filePath = join8(cwd2, ".agents", name, "SKILL.md");
2265
+ const dirPath = join8(cwd2, ".agents", name);
2040
2266
  try {
2041
- await fs7.rm(filePath, { force: true });
2267
+ await fs8.rm(filePath, { force: true });
2042
2268
  } catch {
2043
2269
  }
2044
2270
  try {
2045
- const remaining = await fs7.readdir(dirPath);
2271
+ const remaining = await fs8.readdir(dirPath);
2046
2272
  if (remaining.length === 0)
2047
- await fs7.rmdir(dirPath);
2273
+ await fs8.rmdir(dirPath);
2048
2274
  } catch {
2049
2275
  }
2050
2276
  }
2051
2277
  }
2052
2278
  async function installSkills(server, aiTool, cwd2, overrideDir) {
2053
2279
  const files = [];
2280
+ const warnings = [];
2281
+ const hide = [];
2054
2282
  server = server.replace(/\/$/, "");
2055
- await removeLegacyAgentsLayout(cwd2);
2056
- const pinsayShPath = join7(cwd2, ".pinsay", "pinsay.sh");
2057
- await download(`${server}/pinsay.sh`, pinsayShPath, true);
2058
- 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
+ });
2059
2304
  const isFlatFileTool = !overrideDir && (aiTool === "cursor" || aiTool === "windsurf");
2305
+ const overrideSlash = overrideDir ? overrideDir.replace(/\\/g, "/") : void 0;
2060
2306
  async function writeOrLink(primaryPath, skillName) {
2061
2307
  const url = skillName === "pinsay-init" ? `${server}/pinsay-init.md` : `${server}/skill.md`;
2062
- const finalPath = overrideDir ? join7(cwd2, overrideDir, skillName, "SKILL.md") : join7(cwd2, primaryPath);
2063
- if (skillName === "pinsay-feedback" && isFlatFileTool) {
2064
- const combined = await buildFlatPinSayFeedback(server);
2065
- await fs7.mkdir(dirname2(finalPath), { recursive: true });
2066
- await fs7.writeFile(finalPath, combined, "utf8");
2067
- } else {
2068
- await download(url, finalPath);
2069
- }
2070
- 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;
2071
2319
  if (!overrideDir && (aiTool === "claude-code" || aiTool === "cursor" || aiTool === "windsurf")) {
2072
- const symDest = join7(cwd2, ".agents", "skills", skillName, "SKILL.md");
2073
- await makeSymlink(join7("..", "..", "..", primaryPath), symDest);
2074
- 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
+ });
2075
2326
  }
2076
2327
  if (skillName === "pinsay-feedback" && !isFlatFileTool) {
2077
- const siblingDir = dirname2(finalPath);
2078
- const relDir = overrideDir ? join7(overrideDir, skillName) : dirname2(primaryPath);
2328
+ const relDir = posix.dirname(rel);
2079
2329
  for (const name of SUB_SKILLS) {
2080
- await download(`${server}/skills/${name}.md`, join7(siblingDir, `${name}.md`));
2081
- 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
+ });
2082
2335
  }
2083
2336
  }
2084
2337
  }
2085
2338
  const layout = SKILL_FILES[aiTool] ?? SKILL_FILES.other;
2086
2339
  await writeOrLink(layout[0], "pinsay-init");
2087
2340
  await writeOrLink(layout[1], "pinsay-feedback");
2088
- 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";
2089
2489
  }
2090
2490
 
2091
2491
  // src/branding.ts
@@ -2126,17 +2526,17 @@ async function postEvent(server, token, payload) {
2126
2526
  // src/checks.ts
2127
2527
  init_config();
2128
2528
  init_api();
2129
- import { promises as fs11 } from "node:fs";
2130
- import { join as join11 } from "node:path";
2131
- import { execFile } from "node:child_process";
2132
- 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";
2133
2533
 
2134
2534
  // src/lib/skill-stamp.ts
2135
- import { promises as fs8 } from "node:fs";
2535
+ import { promises as fs10 } from "node:fs";
2136
2536
  async function readStamp(path) {
2137
2537
  let content;
2138
2538
  try {
2139
- content = await fs8.readFile(path, "utf8");
2539
+ content = await fs10.readFile(path, "utf8");
2140
2540
  } catch {
2141
2541
  return null;
2142
2542
  }
@@ -2196,20 +2596,20 @@ async function readStamp(path) {
2196
2596
  }
2197
2597
 
2198
2598
  // src/lib/skill-paths.ts
2199
- import { join as join8 } from "node:path";
2200
2599
  function skillFilesFor(config) {
2201
2600
  const layout = SKILL_FILES[config.aiTool ?? ""] ?? SKILL_FILES.other;
2202
- const skillPaths = config.skillsDir ? [
2203
- join8(config.skillsDir, "pinsay-init", "SKILL.md"),
2204
- join8(config.skillsDir, "pinsay-feedback", "SKILL.md"),
2205
- ...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`)
2206
2606
  ] : [...layout];
2207
2607
  return [...skillPaths, ".pinsay/pinsay.sh"];
2208
2608
  }
2209
2609
 
2210
2610
  // src/stack/stackfile.ts
2211
- import { promises as fs9 } from "node:fs";
2212
- 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";
2213
2613
  function buildRequestBody(stack) {
2214
2614
  const body = {};
2215
2615
  if (stack.frontend !== void 0)
@@ -2223,11 +2623,11 @@ function buildRequestBody(stack) {
2223
2623
  return body;
2224
2624
  }
2225
2625
  function stackFileRelPath(projectKey) {
2226
- 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");
2227
2627
  }
2228
2628
  async function readStackFile(cwd2, projectKey) {
2229
2629
  try {
2230
- const raw = await fs9.readFile(join9(cwd2, stackFileRelPath(projectKey)), "utf8");
2630
+ const raw = await fs11.readFile(join10(cwd2, stackFileRelPath(projectKey)), "utf8");
2231
2631
  return JSON.parse(raw);
2232
2632
  } catch {
2233
2633
  return null;
@@ -2332,19 +2732,19 @@ function formatStackJson(stack) {
2332
2732
  }
2333
2733
  async function writeStackFile(cwd2, stack, projectKey) {
2334
2734
  const relPath = stackFileRelPath(projectKey);
2335
- const dir = join9(cwd2, dirname3(relPath));
2336
- await fs9.mkdir(dir, { recursive: true });
2337
- const targetPath = join9(cwd2, relPath);
2338
- 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)}`);
2339
2739
  const content = formatStackJson(stack);
2340
- await fs9.writeFile(tempPath, content, "utf8");
2341
- await fs9.rename(tempPath, targetPath);
2740
+ await fs11.writeFile(tempPath, content, "utf8");
2741
+ await fs11.rename(tempPath, targetPath);
2342
2742
  }
2343
2743
 
2344
2744
  // src/checks.ts
2345
2745
  init_credentials();
2346
2746
  init_server();
2347
- var execFileAsync = promisify(execFile);
2747
+ var execFileAsync3 = promisify3(execFile3);
2348
2748
  function tooOldMessage(cliVersion, min) {
2349
2749
  return `CLI ${cliVersion} is older than the server requires (${min}). Upgrade with: npx -y pinsay@latest`;
2350
2750
  }
@@ -2489,7 +2889,7 @@ async function runInitChecks(cwd2, overrides = {}, cliVersion = "0.0.0") {
2489
2889
  }
2490
2890
  for (const target of multiProject ? projectTargets : [{ key: project, path: ".", environment, delivery: config.delivery }]) {
2491
2891
  const prefix = multiProject ? `[${target.key}] ` : "";
2492
- const appCwd = multiProject ? join11(cwd2, target.path) : cwd2;
2892
+ const appCwd = multiProject ? join12(cwd2, target.path) : cwd2;
2493
2893
  if (token) {
2494
2894
  try {
2495
2895
  const projects = await api(server, "/api/admin/projects", { token });
@@ -2529,7 +2929,7 @@ async function runInitChecks(cwd2, overrides = {}, cliVersion = "0.0.0") {
2529
2929
  }
2530
2930
  }
2531
2931
  try {
2532
- await fs11.access(join11(cwd2, stackFileRelPath(multiProject ? target.key : void 0)));
2932
+ await fs13.access(join12(cwd2, stackFileRelPath(multiProject ? target.key : void 0)));
2533
2933
  checks.push({ id: "stack", status: "ok", message: `${prefix}Stack registered` });
2534
2934
  } catch {
2535
2935
  checks.push({ id: "stack", status: "warn", message: `${prefix}Stack not registered`, fixable: true });
@@ -2551,9 +2951,9 @@ async function runInitChecks(cwd2, overrides = {}, cliVersion = "0.0.0") {
2551
2951
  if (meta?.skillVersion) {
2552
2952
  const stale = [];
2553
2953
  for (const rel of skillFilesFor(config)) {
2554
- const abs = join11(cwd2, rel);
2954
+ const abs = join12(cwd2, rel);
2555
2955
  try {
2556
- await fs11.access(abs);
2956
+ await fs13.access(abs);
2557
2957
  } catch {
2558
2958
  continue;
2559
2959
  }
@@ -2575,7 +2975,7 @@ async function runInitChecks(cwd2, overrides = {}, cliVersion = "0.0.0") {
2575
2975
  async function sourceMapCheck(cwd2, prefix = "") {
2576
2976
  const configured = await (async () => {
2577
2977
  for (const name of ["vite.config.ts", "vite.config.js", "vite.config.mjs", "vite.config.mts"]) {
2578
- const body = await fs11.readFile(join11(cwd2, name), "utf8").catch(() => "");
2978
+ const body = await fs13.readFile(join12(cwd2, name), "utf8").catch(() => "");
2579
2979
  if (body.includes("pinsay/vite"))
2580
2980
  return true;
2581
2981
  }
@@ -2589,7 +2989,7 @@ async function sourceMapCheck(cwd2, prefix = "") {
2589
2989
  };
2590
2990
  }
2591
2991
  try {
2592
- const raw = await fs11.readFile(join11(cwd2, ".pinsay/manifest.json"), "utf8");
2992
+ const raw = await fs13.readFile(join12(cwd2, ".pinsay/manifest.json"), "utf8");
2593
2993
  const count = Object.keys(JSON.parse(raw)?.entries ?? {}).length;
2594
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 };
2595
2995
  } catch {
@@ -2612,16 +3012,16 @@ async function widgetCheck(cwd2, config = {}, prefix = "") {
2612
3012
  const detection = await detectStack(cwd2).catch(() => null);
2613
3013
  const candidates = [config.htmlPath, detection?.htmlPath, "index.html", "public/index.html", "src/index.html"].filter(Boolean);
2614
3014
  try {
2615
- for (const entry of await fs11.readdir(cwd2, { withFileTypes: true })) {
3015
+ for (const entry of await fs13.readdir(cwd2, { withFileTypes: true })) {
2616
3016
  if (entry.isDirectory() && !entry.name.startsWith(".")) {
2617
- candidates.push(join11(entry.name, "index.html"));
3017
+ candidates.push(join12(entry.name, "index.html"));
2618
3018
  }
2619
3019
  }
2620
3020
  } catch {
2621
3021
  }
2622
3022
  for (const rel of candidates) {
2623
3023
  try {
2624
- const html = await fs11.readFile(join11(cwd2, rel), "utf8");
3024
+ const html = await fs13.readFile(join12(cwd2, rel), "utf8");
2625
3025
  if (html.includes("<!-- pinsay-feedback:start -->") || html.includes("<pinsay-feedback")) {
2626
3026
  return { id: "widget", status: "ok", message: `${prefix}Widget found in ${rel}` };
2627
3027
  }
@@ -2630,7 +3030,7 @@ async function widgetCheck(cwd2, config = {}, prefix = "") {
2630
3030
  }
2631
3031
  for (const envFile of [".env", ".env.local", ".env.development"]) {
2632
3032
  try {
2633
- const env = await fs11.readFile(join11(cwd2, envFile), "utf8");
3033
+ const env = await fs13.readFile(join12(cwd2, envFile), "utf8");
2634
3034
  if (/^VITE_PINSAY_PROJECT=/m.test(env)) {
2635
3035
  return { id: "widget", status: "ok", message: `${prefix}Widget env configured in ${envFile}` };
2636
3036
  }
@@ -2648,20 +3048,14 @@ async function skillsCheck(cwd2, config) {
2648
3048
  const tool = config.aiTool;
2649
3049
  if (!tool)
2650
3050
  return { id: "skills", status: "warn", message: "No AI tool configured" };
2651
- const expected = SKILL_FILES[tool] ?? SKILL_FILES.other;
2652
3051
  const missing = [];
2653
- for (const rel of expected) {
3052
+ for (const rel of skillFilesFor(config)) {
2654
3053
  try {
2655
- await fs11.access(join11(cwd2, config.skillsDir ?? "", rel));
3054
+ await fs13.access((await resolveRepoPath(cwd2, rel, { create: false })).abs);
2656
3055
  } catch {
2657
3056
  missing.push(rel);
2658
3057
  }
2659
3058
  }
2660
- try {
2661
- await fs11.access(join11(cwd2, ".pinsay/pinsay.sh"));
2662
- } catch {
2663
- missing.push(".pinsay/pinsay.sh");
2664
- }
2665
3059
  return missing.length === 0 ? { id: "skills", status: "ok", message: `Skills installed for ${tool}` } : {
2666
3060
  id: "skills",
2667
3061
  status: "warn",
@@ -2674,7 +3068,7 @@ async function gitignoreChecks(cwd2) {
2674
3068
  const results = [];
2675
3069
  let tracked = false;
2676
3070
  try {
2677
- 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 });
2678
3072
  tracked = true;
2679
3073
  } catch {
2680
3074
  tracked = false;
@@ -2688,24 +3082,33 @@ async function gitignoreChecks(cwd2) {
2688
3082
  });
2689
3083
  return results;
2690
3084
  }
2691
- try {
2692
- const ignore = await fs11.readFile(join11(cwd2, ".gitignore"), "utf8");
2693
- results.push(
2694
- 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 }
2695
- );
2696
- } catch {
2697
- 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;
2698
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
+ });
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
+ );
2699
3102
  return results;
2700
3103
  }
2701
3104
 
2702
3105
  // src/commands/init.ts
2703
- import { promises as fs13, existsSync as existsSync2 } from "node:fs";
2704
- 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";
2705
3108
 
2706
3109
  // src/stack/design.ts
2707
- import { promises as fs12, statSync } from "node:fs";
2708
- 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";
2709
3112
  var IGNORED_DIRS = /* @__PURE__ */ new Set([
2710
3113
  "node_modules",
2711
3114
  "dist",
@@ -2731,7 +3134,7 @@ async function collectFiles(cwd2, options) {
2731
3134
  return;
2732
3135
  let entries;
2733
3136
  try {
2734
- entries = await fs12.readdir(dir);
3137
+ entries = await fs14.readdir(dir);
2735
3138
  } catch {
2736
3139
  return;
2737
3140
  }
@@ -2741,17 +3144,17 @@ async function collectFiles(cwd2, options) {
2741
3144
  return;
2742
3145
  if (IGNORED_DIRS.has(entry))
2743
3146
  continue;
2744
- const fullPath = join12(dir, entry);
3147
+ const fullPath = join13(dir, entry);
2745
3148
  let stat;
2746
3149
  try {
2747
- stat = await fs12.stat(fullPath);
3150
+ stat = await fs14.stat(fullPath);
2748
3151
  } catch {
2749
3152
  continue;
2750
3153
  }
2751
3154
  if (stat.isDirectory()) {
2752
3155
  subdirs.push(fullPath);
2753
3156
  } else if (stat.isFile()) {
2754
- collected.push(relative2(cwd2, fullPath));
3157
+ collected.push(relative4(cwd2, fullPath).split(sep4).join("/"));
2755
3158
  }
2756
3159
  }
2757
3160
  for (const sub of subdirs) {
@@ -2850,15 +3253,15 @@ function parseArrayOrObjectKeys(content, matchIndex) {
2850
3253
  return [];
2851
3254
  }
2852
3255
  async function collectDependencyChain(cwd2, root, readFile) {
2853
- const cwdAbs = resolve(cwd2);
2854
- const rootAbs = resolve(root);
3256
+ const cwdAbs = resolve5(cwd2);
3257
+ const rootAbs = resolve5(root);
2855
3258
  const dirs = [];
2856
3259
  let dir = cwdAbs;
2857
3260
  while (true) {
2858
3261
  dirs.push(dir);
2859
3262
  if (dir === rootAbs)
2860
3263
  break;
2861
- const parent = dirname5(dir);
3264
+ const parent = dirname6(dir);
2862
3265
  if (parent === dir)
2863
3266
  break;
2864
3267
  dir = parent;
@@ -2867,7 +3270,7 @@ async function collectDependencyChain(cwd2, root, readFile) {
2867
3270
  const merged = {};
2868
3271
  for (const d of dirs) {
2869
3272
  try {
2870
- const pkg = JSON.parse(await readFile(join12(d, "package.json")));
3273
+ const pkg = JSON.parse(await readFile(join13(d, "package.json")));
2871
3274
  Object.assign(merged, pkg.dependencies, pkg.devDependencies);
2872
3275
  } catch {
2873
3276
  }
@@ -2894,7 +3297,7 @@ async function detectTailwind(cwd2, files, readFile, root = cwd2) {
2894
3297
  let exists = files.includes(name);
2895
3298
  if (!exists) {
2896
3299
  try {
2897
- await readFile(join12(cwd2, name));
3300
+ await readFile(join13(cwd2, name));
2898
3301
  exists = true;
2899
3302
  } catch {
2900
3303
  exists = false;
@@ -2902,16 +3305,16 @@ async function detectTailwind(cwd2, files, readFile, root = cwd2) {
2902
3305
  }
2903
3306
  if (exists) {
2904
3307
  foundConfigFile = name;
2905
- configAbsPath = join12(cwd2, name);
3308
+ configAbsPath = join13(cwd2, name);
2906
3309
  break;
2907
3310
  }
2908
3311
  }
2909
- if (!foundConfigFile && resolve(root) !== resolve(cwd2)) {
3312
+ if (!foundConfigFile && resolve5(root) !== resolve5(cwd2)) {
2910
3313
  for (const name of configNames) {
2911
- const candidate = join12(root, name);
3314
+ const candidate = join13(root, name);
2912
3315
  try {
2913
3316
  await readFile(candidate);
2914
- foundConfigFile = relative2(cwd2, candidate);
3317
+ foundConfigFile = relative4(cwd2, candidate);
2915
3318
  configAbsPath = candidate;
2916
3319
  break;
2917
3320
  } catch {
@@ -2982,7 +3385,7 @@ async function detectTailwind(cwd2, files, readFile, root = cwd2) {
2982
3385
  for (const file of cssFiles) {
2983
3386
  let content = "";
2984
3387
  try {
2985
- content = await readFile(join12(cwd2, file));
3388
+ content = await readFile(join13(cwd2, file));
2986
3389
  } catch {
2987
3390
  continue;
2988
3391
  }
@@ -3028,7 +3431,7 @@ async function scanCssVars(baseDir, candidatePaths, readFile) {
3028
3431
  const candidateFiles = [];
3029
3432
  for (const f of candidatePaths) {
3030
3433
  try {
3031
- const fullPath = join12(baseDir, f);
3434
+ const fullPath = join13(baseDir, f);
3032
3435
  const stat = statSync(fullPath);
3033
3436
  if (stat.size <= 204800) {
3034
3437
  candidateFiles.push({ path: f, size: stat.size });
@@ -3048,7 +3451,7 @@ async function scanCssVars(baseDir, candidatePaths, readFile) {
3048
3451
  for (const item of selectedFiles) {
3049
3452
  let content = "";
3050
3453
  try {
3051
- content = await readFile(join12(baseDir, item.path));
3454
+ content = await readFile(join13(baseDir, item.path));
3052
3455
  } catch {
3053
3456
  continue;
3054
3457
  }
@@ -3091,7 +3494,7 @@ async function scanScss(baseDir, candidatePaths, readFile) {
3091
3494
  for (const f of candidatePaths) {
3092
3495
  let content = "";
3093
3496
  try {
3094
- content = await readFile(join12(baseDir, f));
3497
+ content = await readFile(join13(baseDir, f));
3095
3498
  } catch {
3096
3499
  continue;
3097
3500
  }
@@ -3157,7 +3560,7 @@ async function detectTheme(cwd2, files, readFile) {
3157
3560
  for (const f of themeFiles) {
3158
3561
  let content = "";
3159
3562
  try {
3160
- content = await readFile(join12(cwd2, f));
3563
+ content = await readFile(join13(cwd2, f));
3161
3564
  } catch {
3162
3565
  continue;
3163
3566
  }
@@ -3187,7 +3590,7 @@ async function detectAngularMaterial(cwd2, files, readFile) {
3187
3590
  for (const f of scssFiles) {
3188
3591
  let content = "";
3189
3592
  try {
3190
- content = await readFile(join12(cwd2, f));
3593
+ content = await readFile(join13(cwd2, f));
3191
3594
  } catch {
3192
3595
  continue;
3193
3596
  }
@@ -3220,7 +3623,7 @@ async function detectLibraries(cwd2, files, readFile, root = cwd2) {
3220
3623
  "primeng",
3221
3624
  "primevue"
3222
3625
  ];
3223
- if (resolve(root) !== resolve(cwd2)) {
3626
+ if (resolve5(root) !== resolve5(cwd2)) {
3224
3627
  tracked.push("tailwindcss");
3225
3628
  }
3226
3629
  for (const [depName, depVer] of Object.entries(deps)) {
@@ -3278,7 +3681,7 @@ function buildDesignBlock(libraries, tokens) {
3278
3681
  };
3279
3682
  }
3280
3683
  async function detectDesignTokens(cwd2, options) {
3281
- const readFile = options?.readFile ?? ((p) => fs12.readFile(p, "utf8"));
3684
+ const readFile = options?.readFile ?? ((p) => fs14.readFile(p, "utf8"));
3282
3685
  const root = options?.root ?? cwd2;
3283
3686
  const files = await collectFiles(cwd2, options);
3284
3687
  const libraries = await detectLibraries(cwd2, files, readFile, root);
@@ -3287,7 +3690,7 @@ async function detectDesignTokens(cwd2, options) {
3287
3690
  let scss = await detectScss(cwd2, files, readFile);
3288
3691
  const theme = await detectTheme(cwd2, files, readFile);
3289
3692
  const angularMaterial = await detectAngularMaterial(cwd2, files, readFile);
3290
- if ((!cssVars || !scss) && resolve(root) !== resolve(cwd2)) {
3693
+ if ((!cssVars || !scss) && resolve5(root) !== resolve5(cwd2)) {
3291
3694
  const rootFiles = await collectFiles(root, options);
3292
3695
  const sharedStyleFiles = rootFiles.filter(isRootSharedStylePath);
3293
3696
  if (!cssVars) {
@@ -3367,7 +3770,7 @@ function openBrowser(url) {
3367
3770
  }
3368
3771
  }
3369
3772
  function sleep(ms) {
3370
- return new Promise((resolve5) => setTimeout(resolve5, ms));
3773
+ return new Promise((resolve9) => setTimeout(resolve9, ms));
3371
3774
  }
3372
3775
  async function runDeviceLogin(server, options = {}) {
3373
3776
  const clientName = options.clientName ?? `pinsay CLI on ${hostname()}`;
@@ -3431,7 +3834,7 @@ async function runDeviceLogin(server, options = {}) {
3431
3834
  // src/key-scope.ts
3432
3835
  var KEY_SCOPE_QUESTION = "Where should this API key be stored?";
3433
3836
  var KEY_SCOPE_GLOBAL = "Global \u2014 this machine, every repo (~/.config/pinsay/credentials.json)";
3434
- var KEY_SCOPE_REPO = "Repo \u2014 .pinsay/credentials.env in this repo only (gitignored)";
3837
+ var KEY_SCOPE_REPO = "Repo \u2014 .pinsay/credentials.env in this repo only (hidden from git)";
3435
3838
  function scopeFromFlags(options) {
3436
3839
  const raw = options["scope"];
3437
3840
  if (typeof raw === "string") {
@@ -3630,7 +4033,7 @@ async function initCommand(cwd2, options = {}) {
3630
4033
  if (writeLocalCreds) {
3631
4034
  await writeCredentials(cwd2, key);
3632
4035
  }
3633
- await upsertGitignore(cwd2, product, options["skills-dir"] || config.skillsDir);
4036
+ await hidePinsayFiles(cwd2, skillsDirExtra(options["skills-dir"] || config.skillsDir));
3634
4037
  if (isJoin && configIsMulti) {
3635
4038
  await handleMultiJoin(cwd2, config, server, product, Boolean(isJson), options, token, me);
3636
4039
  return;
@@ -3828,6 +4231,7 @@ and the pinsay-init skill uses them to mount the widget for you afterwards.
3828
4231
  let routedToSkill = false;
3829
4232
  let filesMod = [];
3830
4233
  let skillFiles = [];
4234
+ let skillWarnings = [];
3831
4235
  let pin = null;
3832
4236
  if (!isJoin && options["pin"] === true) {
3833
4237
  try {
@@ -3906,23 +4310,43 @@ and the pinsay-init skill uses them to mount the widget for you afterwards.
3906
4310
  console.error(`\u26A0 ${sourceMapNote}`);
3907
4311
  }
3908
4312
  }
4313
+ const skillHide = [];
3909
4314
  if (!options["no-skills"]) {
3910
4315
  if (!isJson)
3911
4316
  console.log(`Installing AI skills for ${tools.join(", ")}`);
3912
4317
  const skillsDir = options["skills-dir"];
3913
4318
  const installed = [];
3914
4319
  for (const t of tools) {
3915
- 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
+ }
3916
4333
  }
4334
+ if (!isJson)
4335
+ for (const line of formatSkillWarnings(skillWarnings))
4336
+ console.error(line);
3917
4337
  filesMod.push(...installed);
3918
4338
  skillFiles = [...new Set(installed.filter((f) => f.includes("SKILL.md") || f.endsWith(".md")))];
3919
4339
  }
3920
- 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(() => "{}");
3921
4345
  const tokens = extractTokens(JSON.parse(pkgStr));
3922
4346
  const stackMeta = { frontend: tokens.frontend, backend: tokens.backend, aiTool: tool };
3923
4347
  let stackFileExists = false;
3924
4348
  try {
3925
- await fs13.access(join13(cwd2, ".pinsay/stack.json"));
4349
+ await fs15.access(join14(cwd2, ".pinsay/stack.json"));
3926
4350
  stackFileExists = true;
3927
4351
  } catch {
3928
4352
  stackFileExists = false;
@@ -3964,7 +4388,6 @@ and the pinsay-init skill uses them to mount the widget for you afterwards.
3964
4388
  await writeCredentials(cwd2, key, { project: finalProjectKey });
3965
4389
  filesMod.push(".pinsay/credentials.env");
3966
4390
  }
3967
- filesMod.push(".gitignore");
3968
4391
  if (!isJson)
3969
4392
  console.log("Verifying...");
3970
4393
  const checks = await runInitChecks(cwd2, { server, project: finalProjectKey }, BUILD_CLI_VERSION);
@@ -3994,6 +4417,9 @@ and the pinsay-init skill uses them to mount the widget for you afterwards.
3994
4417
  injected,
3995
4418
  routedToSkill,
3996
4419
  files: filesMod,
4420
+ skillWarnings,
4421
+ hiddenFromGit: hidden.status,
4422
+ trackedPinsayFiles: hidden.tracked,
3997
4423
  checks,
3998
4424
  cliVersion: BUILD_CLI_VERSION
3999
4425
  }));
@@ -4014,7 +4440,7 @@ ${envLabel !== null ? `
4014
4440
  ${dim("Environment(s)")} ${envLabel}` : ""}
4015
4441
  ${dim("Server")} ${server}
4016
4442
  ${dim("Key")} ${keyLine}
4017
- ${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"}
4018
4444
  ${rule}`);
4019
4445
  } else {
4020
4446
  console.log(`
@@ -4025,7 +4451,7 @@ ${green("\u2714")} ${bold(`${product} is set up`)}
4025
4451
  ${envLabel !== null ? ` ${dim("Environments")} ${envLabel}
4026
4452
  ` : ""} ${dim("Server")} ${server}
4027
4453
  ${dim("Key")} ${keyLine}
4028
- ${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"}
4029
4455
  ${rule}`);
4030
4456
  }
4031
4457
  if (delivery === "extension") {
@@ -4092,19 +4518,19 @@ function projectQuestion(label) {
4092
4518
  return label ? `Which PinSay project is ${label}?` : "Which project is this app?";
4093
4519
  }
4094
4520
  function toRootRelative(root, p) {
4095
- const abs = isAbsolute2(p) ? p : resolve2(p);
4096
- return relative3(root, abs).split(sep2).join("/");
4521
+ const abs = isAbsolute3(p) ? p : resolve6(p);
4522
+ return relative5(root, abs).split(sep5).join("/");
4097
4523
  }
4098
4524
  async function hasViteConfig(appDir) {
4099
4525
  for (const ext of ["ts", "js", "mjs", "mts"]) {
4100
- if (existsSync2(join13(appDir, `vite.config.${ext}`)))
4526
+ if (existsSync2(join14(appDir, `vite.config.${ext}`)))
4101
4527
  return true;
4102
4528
  }
4103
4529
  return false;
4104
4530
  }
4105
4531
  async function readJsonSafe(p) {
4106
4532
  try {
4107
- return JSON.parse(await fs13.readFile(p, "utf8"));
4533
+ return JSON.parse(await fs15.readFile(p, "utf8"));
4108
4534
  } catch {
4109
4535
  return null;
4110
4536
  }
@@ -4112,13 +4538,13 @@ async function readJsonSafe(p) {
4112
4538
  async function resolveHtmlCandidate(root, appDir, explicitHtml) {
4113
4539
  if (explicitHtml)
4114
4540
  return explicitHtml;
4115
- const targetCwd = join13(root, appDir);
4116
- const candidates = [join13(targetCwd, "index.html"), join13(targetCwd, "src", "index.html")];
4117
- 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"));
4118
4544
  if (projectJson?.sourceRoot) {
4119
- candidates.push(join13(root, projectJson.sourceRoot, "index.html"));
4545
+ candidates.push(join14(root, projectJson.sourceRoot, "index.html"));
4120
4546
  }
4121
- candidates.push(join13(targetCwd, "public", "index.html"));
4547
+ candidates.push(join14(targetCwd, "public", "index.html"));
4122
4548
  for (const c of candidates) {
4123
4549
  if (existsSync2(c))
4124
4550
  return c;
@@ -4128,7 +4554,7 @@ async function resolveHtmlCandidate(root, appDir, explicitHtml) {
4128
4554
  function deriveAppDirFromHtmlPath(htmlPath) {
4129
4555
  if (!htmlPath)
4130
4556
  return ".";
4131
- let dir = dirname6(htmlPath).replace(/\/src$/, "");
4557
+ let dir = dirname7(htmlPath).replace(/\/src$/, "");
4132
4558
  return dir === "" || dir === "." ? "." : dir;
4133
4559
  }
4134
4560
  async function selectOrCreateProject(server, token, isYes, presetKey, presetCreate, label) {
@@ -4253,7 +4679,7 @@ async function resolvePin(server, options) {
4253
4679
  }
4254
4680
  async function setupOneProject(ctx) {
4255
4681
  const { cwd: cwd2, appDir, server, token } = ctx;
4256
- const targetCwd = join13(cwd2, appDir);
4682
+ const targetCwd = join14(cwd2, appDir);
4257
4683
  const label = appLabel(appDir);
4258
4684
  const { key, name, created } = await selectOrCreateProject(server, token, ctx.isYes, ctx.presetKey, ctx.presetCreate, label);
4259
4685
  const ALL_ENVS = ["local", "staging", "production"];
@@ -4329,9 +4755,9 @@ async function setupOneProject(ctx) {
4329
4755
  noHtmlFound = true;
4330
4756
  }
4331
4757
  }
4332
- let pkgStr = await fs13.readFile(join13(targetCwd, "package.json"), "utf8").catch(() => "");
4758
+ let pkgStr = await fs15.readFile(join14(targetCwd, "package.json"), "utf8").catch(() => "");
4333
4759
  if (!pkgStr)
4334
- pkgStr = await fs13.readFile(join13(cwd2, "package.json"), "utf8").catch(() => "{}");
4760
+ pkgStr = await fs15.readFile(join14(cwd2, "package.json"), "utf8").catch(() => "{}");
4335
4761
  const tokens = extractTokens(JSON.parse(pkgStr || "{}"));
4336
4762
  const stackMeta = { frontend: tokens.frontend, backend: tokens.backend, aiTool: ctx.aiTool };
4337
4763
  let serverStackResponse = null;
@@ -4353,25 +4779,39 @@ async function setupOneProject(ctx) {
4353
4779
  async function handleMultiJoin(cwd2, config, server, product, isJson, options, token, me) {
4354
4780
  closePrompts();
4355
4781
  const tool = options["tool"] || config.aiTool || "other";
4782
+ const skillWarnings = [];
4783
+ const hide = [];
4356
4784
  if (!options["no-skills"]) {
4357
- await installSkills(server, tool, cwd2, options["skills-dir"]).catch(() => {
4358
- });
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);
4359
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);
4360
4800
  const projects = listProjects(config);
4361
4801
  for (const p of projects) {
4362
4802
  const relStack = stackFileRelPath(p.key);
4363
4803
  let exists = true;
4364
4804
  try {
4365
- await fs13.access(join13(cwd2, relStack));
4805
+ await fs15.access(join14(cwd2, relStack));
4366
4806
  } catch {
4367
4807
  exists = false;
4368
4808
  }
4369
4809
  if (exists)
4370
4810
  continue;
4371
- const appCwd = join13(cwd2, p.path);
4372
- 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(() => "");
4373
4813
  if (!pkgStr)
4374
- pkgStr = await fs13.readFile(join13(cwd2, "package.json"), "utf8").catch(() => "{}");
4814
+ pkgStr = await fs15.readFile(join14(cwd2, "package.json"), "utf8").catch(() => "{}");
4375
4815
  const tokens = extractTokens(JSON.parse(pkgStr || "{}"));
4376
4816
  const stackMeta = { frontend: tokens.frontend, backend: tokens.backend, aiTool: config.aiTool };
4377
4817
  let serverStackResponse = null;
@@ -4403,6 +4843,9 @@ async function handleMultiJoin(cwd2, config, server, product, isJson, options, t
4403
4843
  htmlPath: p.htmlPath,
4404
4844
  delivery: p.delivery ?? config.delivery ?? "embed"
4405
4845
  })),
4846
+ skillWarnings,
4847
+ hiddenFromGit: hidden.status,
4848
+ trackedPinsayFiles: hidden.tracked,
4406
4849
  cliVersion: BUILD_CLI_VERSION
4407
4850
  }));
4408
4851
  } else {
@@ -4490,12 +4933,26 @@ async function handleMultiProjectSetup(args) {
4490
4933
  }
4491
4934
  }
4492
4935
  closePrompts();
4936
+ const skillWarnings = [];
4937
+ const hide = [];
4493
4938
  if (!options["no-skills"]) {
4494
4939
  for (const t of tools) {
4495
- await installSkills(server, t, cwd2, t === tool ? options["skills-dir"] : void 0).catch(() => {
4496
- });
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
+ }
4497
4947
  }
4948
+ if (!isJson)
4949
+ for (const line of formatSkillWarnings(skillWarnings))
4950
+ console.error(line);
4498
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);
4499
4956
  const projectsMap = { ...config.projects ?? {} };
4500
4957
  let migrationNote = null;
4501
4958
  let migrationOk = null;
@@ -4543,6 +5000,9 @@ async function handleMultiProjectSetup(args) {
4543
5000
  const r = results.find((res) => res.key === k);
4544
5001
  return { key: k, path: p.path, injected: r ? r.injected : false, htmlPath: p.htmlPath, delivery: p.delivery ?? repoDefaultDelivery };
4545
5002
  }),
5003
+ skillWarnings,
5004
+ hiddenFromGit: hidden.status,
5005
+ trackedPinsayFiles: hidden.tracked,
4546
5006
  cliVersion: BUILD_CLI_VERSION
4547
5007
  }));
4548
5008
  } else {
@@ -4559,8 +5019,8 @@ ${migrationLine} Config: .pinsay/config.json (projects map)
4559
5019
 
4560
5020
  // src/commands/doctor.ts
4561
5021
  init_config();
4562
- import { promises as fs15 } from "node:fs";
4563
- import { join as join15 } from "node:path";
5022
+ import { promises as fs17 } from "node:fs";
5023
+ import { join as join16 } from "node:path";
4564
5024
  init_api();
4565
5025
  init_credentials();
4566
5026
  init_server();
@@ -4578,7 +5038,7 @@ async function doctorCommand(cwd2, options, cliVersion) {
4578
5038
  const config = await readConfig(cwd2);
4579
5039
  const multi = isMultiProject(config);
4580
5040
  const projectKey = multi ? options.project || listProjects(config)[0]?.key : void 0;
4581
- const appCwd = multi && projectKey ? join15(cwd2, config.projects?.[projectKey]?.path ?? ".") : cwd2;
5041
+ const appCwd = multi && projectKey ? join16(cwd2, config.projects?.[projectKey]?.path ?? ".") : cwd2;
4582
5042
  const start = Date.now();
4583
5043
  const designBlock = await detectDesignTokens(appCwd, { root: cwd2 });
4584
5044
  const detectMs = Date.now() - start;
@@ -4669,11 +5129,10 @@ async function applyFixes(cwd2, checks, server) {
4669
5129
  continue;
4670
5130
  try {
4671
5131
  if (check.id === "gitignore") {
4672
- const path = join15(cwd2, ".gitignore");
4673
- const before = await fs15.readFile(path, "utf8").catch(() => "");
4674
- await upsertGitignore(cwd2, "Feedback tool", config.skillsDir);
4675
- const after = await fs15.readFile(path, "utf8").catch(() => "");
4676
- 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")
4677
5136
  repaired.push(check.id);
4678
5137
  } else if (check.id === "source-map") {
4679
5138
  const { buildManifest: buildManifest2 } = await Promise.resolve().then(() => (init_map(), map_exports));
@@ -4681,8 +5140,12 @@ async function applyFixes(cwd2, checks, server) {
4681
5140
  if (built.ok)
4682
5141
  repaired.push(check.id);
4683
5142
  } else if (check.id === "skills" && server && config.aiTool) {
4684
- await installSkills(server, config.aiTool, cwd2, config.skillsDir);
4685
- 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);
4686
5149
  }
4687
5150
  } catch {
4688
5151
  }
@@ -4693,7 +5156,7 @@ async function applyFixes(cwd2, checks, server) {
4693
5156
  let any = false;
4694
5157
  for (const t of targets) {
4695
5158
  try {
4696
- const appCwd = multi ? join15(cwd2, t.path) : cwd2;
5159
+ const appCwd = multi ? join16(cwd2, t.path) : cwd2;
4697
5160
  const detection = await detectStack(appCwd);
4698
5161
  const stackToken = await tokenFor();
4699
5162
  const stack = stackToken ? await api(server, `/api/projects/${t.key}/stack`, {
@@ -4702,7 +5165,7 @@ async function applyFixes(cwd2, checks, server) {
4702
5165
  body: { kind: detection.kind, evidence: detection.evidence }
4703
5166
  }).catch(() => null) : null;
4704
5167
  if (stack) {
4705
- 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");
4706
5169
  any = true;
4707
5170
  }
4708
5171
  } catch {
@@ -4717,8 +5180,7 @@ async function applyFixes(cwd2, checks, server) {
4717
5180
  // src/commands/update.ts
4718
5181
  init_config();
4719
5182
  init_api();
4720
- import { promises as fs16 } from "node:fs";
4721
- import { dirname as dirname7, join as join16 } from "node:path";
5183
+ import { promises as fs18 } from "node:fs";
4722
5184
  init_server();
4723
5185
  function sourceFor(path) {
4724
5186
  if (path.endsWith("pinsay.sh"))
@@ -4760,9 +5222,10 @@ async function updateCommand(cwd2, options) {
4760
5222
  const missing = [];
4761
5223
  const stale = [];
4762
5224
  for (const rel of files) {
4763
- const abs = join16(cwd2, rel);
5225
+ let abs;
4764
5226
  try {
4765
- await fs16.access(abs);
5227
+ abs = (await resolveRepoPath(cwd2, rel, { create: false })).abs;
5228
+ await fs18.access(abs);
4766
5229
  } catch {
4767
5230
  missing.push(rel);
4768
5231
  continue;
@@ -4773,6 +5236,10 @@ async function updateCommand(cwd2, options) {
4773
5236
  }
4774
5237
  if (missing.length === 0 && stale.length === 0) {
4775
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
+ }
4776
5243
  return 0;
4777
5244
  }
4778
5245
  if (options.check) {
@@ -4789,17 +5256,31 @@ async function updateCommand(cwd2, options) {
4789
5256
  return 0;
4790
5257
  }
4791
5258
  let installedCount = 0;
5259
+ let installHide = [];
4792
5260
  if (missing.length > 0) {
4793
5261
  if (!config.aiTool) {
4794
5262
  console.error("No AI tool configured \u2014 run `npx -y pinsay-cli init` to record one, then `update` again.");
4795
5263
  } else {
4796
5264
  try {
4797
- await installSkills(server, config.aiTool, cwd2, config.skillsDir);
4798
- installedCount = missing.length;
4799
- 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;
4800
5269
  } catch (err) {
4801
5270
  console.error(` failed to install missing skills: ${err?.message ?? err}`);
4802
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
+ }
4803
5284
  }
4804
5285
  }
4805
5286
  let updated = 0;
@@ -4819,11 +5300,10 @@ async function updateCommand(cwd2, options) {
4819
5300
  throw new Error(`HTTP ${res.status}`);
4820
5301
  body = await res.text();
4821
5302
  }
4822
- const abs = join16(cwd2, f.path);
4823
- await fs16.mkdir(dirname7(abs), { recursive: true });
4824
- await fs16.writeFile(abs, body, "utf8");
5303
+ const abs = (await resolveRepoPath(cwd2, f.path, { create: true })).abs;
5304
+ await fs18.writeFile(abs, body, "utf8");
4825
5305
  if (abs.endsWith(".sh"))
4826
- await fs16.chmod(abs, 493).catch(() => {
5306
+ await fs18.chmod(abs, 493).catch(() => {
4827
5307
  });
4828
5308
  updated++;
4829
5309
  } catch (err) {
@@ -4833,6 +5313,9 @@ async function updateCommand(cwd2, options) {
4833
5313
  if (stale.length > 0) {
4834
5314
  console.log(`updated ${updated} file${updated === 1 ? "" : "s"} (skill version ${from} \u2192 ${served ?? "unknown"})`);
4835
5315
  }
5316
+ const hidden = await hidePinsayFiles(cwd2, [...skillsDirExtra(config.skillsDir), ...installHide]);
5317
+ for (const line of formatHideWarnings(hidden))
5318
+ console.error(line);
4836
5319
  const installedOk = missing.length === 0 || installedCount === missing.length;
4837
5320
  const updatedOk = updated === stale.length;
4838
5321
  return installedOk && updatedOk ? 0 : 1;
@@ -4843,7 +5326,7 @@ init_config();
4843
5326
  init_api();
4844
5327
 
4845
5328
  // src/lib/notify-update.ts
4846
- import { promises as fs17 } from "node:fs";
5329
+ import { promises as fs19 } from "node:fs";
4847
5330
  import { join as join17 } from "node:path";
4848
5331
  init_build_constants();
4849
5332
  async function checkAndNotifyUpdates(options) {
@@ -4869,7 +5352,7 @@ async function checkAndNotifyUpdates(options) {
4869
5352
  for (const rel of files) {
4870
5353
  const abs = join17(options.cwd, rel);
4871
5354
  try {
4872
- await fs17.access(abs);
5355
+ await fs19.access(abs);
4873
5356
  const installed = await readStamp(abs);
4874
5357
  if (installed && installed !== meta.skillVersion) {
4875
5358
  hasStale = true;
@@ -4899,7 +5382,7 @@ init_build_constants();
4899
5382
  // src/apply/run.ts
4900
5383
  init_resolve();
4901
5384
  init_api();
4902
- import { promises as fs20 } from "node:fs";
5385
+ import { promises as fs22 } from "node:fs";
4903
5386
  import { join as join20, dirname as dirname9 } from "node:path";
4904
5387
  import { spawnSync } from "node:child_process";
4905
5388
 
@@ -5058,7 +5541,7 @@ async function fetchQueue(ctx, filter) {
5058
5541
 
5059
5542
  // src/apply/context.ts
5060
5543
  init_api();
5061
- import { promises as fs19 } from "node:fs";
5544
+ import { promises as fs21 } from "node:fs";
5062
5545
  import { join as join19 } from "node:path";
5063
5546
  init_config();
5064
5547
  async function loadProjectContext(ctx) {
@@ -5070,7 +5553,7 @@ async function loadProjectContext(ctx) {
5070
5553
  if (config.delegation === "off")
5071
5554
  delegation = "off";
5072
5555
  const relPath = isMultiProject(config) ? stackFileRelPath(ctx.project) : stackFileRelPath();
5073
- const stackRaw = await fs19.readFile(join19(ctx.cwd, relPath), "utf8");
5556
+ const stackRaw = await fs21.readFile(join19(ctx.cwd, relPath), "utf8");
5074
5557
  stack = JSON.parse(stackRaw);
5075
5558
  } catch {
5076
5559
  }
@@ -5445,7 +5928,7 @@ async function ensureToolRegistered(ctx, tool) {
5445
5928
  const stackPath = join20(ctx.cwd, relStackPath);
5446
5929
  let stackData = {};
5447
5930
  try {
5448
- const raw = await fs20.readFile(stackPath, "utf8");
5931
+ const raw = await fs22.readFile(stackPath, "utf8");
5449
5932
  stackData = JSON.parse(raw);
5450
5933
  } catch {
5451
5934
  }
@@ -5464,8 +5947,8 @@ async function ensureToolRegistered(ctx, tool) {
5464
5947
  }
5465
5948
  );
5466
5949
  if (res) {
5467
- await fs20.mkdir(dirname9(stackPath), { recursive: true });
5468
- 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");
5469
5952
  return;
5470
5953
  }
5471
5954
  } catch {
@@ -5474,8 +5957,8 @@ async function ensureToolRegistered(ctx, tool) {
5474
5957
  aiTools.push(tool);
5475
5958
  stackData.aiTools = aiTools;
5476
5959
  try {
5477
- await fs20.mkdir(dirname9(stackPath), { recursive: true });
5478
- 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");
5479
5962
  } catch {
5480
5963
  }
5481
5964
  }
@@ -5550,8 +6033,8 @@ async function runApply(options, ctx) {
5550
6033
  }
5551
6034
  } else if (tool === "cursor") {
5552
6035
  const promptFile = join20(ctx.cwd, ".pinsay/apply-prompt.md");
5553
- await fs20.mkdir(join20(ctx.cwd, ".pinsay"), { recursive: true });
5554
- await fs20.writeFile(promptFile, prompt, "utf8");
6036
+ await fs22.mkdir(join20(ctx.cwd, ".pinsay"), { recursive: true });
6037
+ await fs22.writeFile(promptFile, prompt, "utf8");
5555
6038
  console.log(`Saved apply prompt to ${promptFile}`);
5556
6039
  } else if (tool === "clipboard") {
5557
6040
  const copied = copyToClipboard(prompt);
@@ -11093,7 +11576,7 @@ var Protocol = class {
11093
11576
  * Do not use this method to emit notifications! Use notification() instead.
11094
11577
  */
11095
11578
  request(request, resultSchema, options) {
11096
- return new Promise((resolve5, reject) => {
11579
+ return new Promise((resolve9, reject) => {
11097
11580
  var _a, _b, _c, _d;
11098
11581
  if (!this._transport) {
11099
11582
  reject(new Error("Not connected"));
@@ -11130,7 +11613,7 @@ var Protocol = class {
11130
11613
  }
11131
11614
  try {
11132
11615
  const result = resultSchema.parse(response.result);
11133
- resolve5(result);
11616
+ resolve9(result);
11134
11617
  } catch (error) {
11135
11618
  reject(error);
11136
11619
  }
@@ -11483,12 +11966,12 @@ var StdioServerTransport = class {
11483
11966
  (_a = this.onclose) === null || _a === void 0 ? void 0 : _a.call(this);
11484
11967
  }
11485
11968
  send(message) {
11486
- return new Promise((resolve5) => {
11969
+ return new Promise((resolve9) => {
11487
11970
  const json = serializeMessage(message);
11488
11971
  if (this._stdout.write(json)) {
11489
- resolve5();
11972
+ resolve9();
11490
11973
  } else {
11491
- this._stdout.once("drain", resolve5);
11974
+ this._stdout.once("drain", resolve9);
11492
11975
  }
11493
11976
  });
11494
11977
  }
@@ -11753,7 +12236,7 @@ var ALL_TOOLS = [
11753
12236
  // src/mcp/tools.ts
11754
12237
  init_api();
11755
12238
  import { existsSync as existsSync4, readFileSync as readFileSync2 } from "node:fs";
11756
- 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";
11757
12240
  import { spawnSync as spawnSync3 } from "node:child_process";
11758
12241
  init_build_constants();
11759
12242
  init_projection();
@@ -12067,12 +12550,12 @@ async function handleCommitAndMark(args, ctx) {
12067
12550
  if (Array.isArray(files) && files.length > 0) {
12068
12551
  for (const entry of files) {
12069
12552
  const cleanPath = entry.includes(":") ? entry.split(":").slice(1).join(":") : entry;
12070
- if (isAbsolute3(cleanPath)) {
12553
+ if (isAbsolute4(cleanPath)) {
12071
12554
  throw mcpError("git", `Path must be relative to repo root: ${cleanPath}`);
12072
12555
  }
12073
- const resolved = resolve4(ctx.cwd, cleanPath);
12074
- const rel = relative5(ctx.cwd, resolved);
12075
- 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)) {
12076
12559
  throw mcpError("git", `Path escapes repository root: ${cleanPath}`);
12077
12560
  }
12078
12561
  if (!existsSync4(resolved)) {
@@ -12543,8 +13026,8 @@ function createMcpServer(ctx) {
12543
13026
  async function runMcpServer(ctx, logFile) {
12544
13027
  if (logFile) {
12545
13028
  try {
12546
- const fs21 = await import("node:fs");
12547
- const logStream = fs21.createWriteStream(logFile, { flags: "a" });
13029
+ const fs23 = await import("node:fs");
13030
+ const logStream = fs23.createWriteStream(logFile, { flags: "a" });
12548
13031
  const origWrite = process.stderr.write;
12549
13032
  process.stderr.write = function(chunk, encoding, cb) {
12550
13033
  logStream.write(chunk);
@@ -12650,7 +13133,8 @@ async function loginCommand(cwd2, options = {}) {
12650
13133
  const who = me?.displayName ? `${me.displayName}${me?.email ? ` (${me.email})` : ""}` : me?.email ?? "you";
12651
13134
  if (scope === "repo") {
12652
13135
  await writeCredentials(root, key);
12653
- await upsertGitignore(root, product);
13136
+ for (const line of formatHideWarnings(await hidePinsayFiles(root)))
13137
+ console.error(line);
12654
13138
  console.log(`\u2714 Signed in to ${server} as ${who} \u2014 saved to .pinsay/credentials.env (this repo only; overrides the global store here)`);
12655
13139
  process.exit(0);
12656
13140
  }
@@ -12845,7 +13329,7 @@ Options:
12845
13329
  --source-map Wire in the Vite plugin that stamps component source hashes
12846
13330
  --scope <global|repo> Where the API key is stored, without asking: global (this machine, all
12847
13331
  repos, ~/.config/pinsay/credentials.json; the default with --yes) or
12848
- repo (.pinsay/credentials.env, gitignored, this repo only).
13332
+ repo (.pinsay/credentials.env, hidden from git, this repo only).
12849
13333
  --local-credentials is an alias for --scope repo
12850
13334
  --no-browser When signing in in the browser (first run, no key resolved yet), print
12851
13335
  the link/code but don't try to open a browser
@@ -12863,7 +13347,7 @@ Usage: npx pinsay-cli login [options]
12863
13347
 
12864
13348
  Sign in and save your API key. On a terminal it asks where to save it:
12865
13349
  Global this machine, every repo (~/.config/pinsay/credentials.json, honours $XDG_CONFIG_HOME)
12866
- Repo .pinsay/credentials.env in this repo only (gitignored)
13350
+ Repo .pinsay/credentials.env in this repo only (hidden from git)
12867
13351
  With no terminal (CI, a pipe) or with --yes it saves globally, unless --scope repo is given.
12868
13352
 
12869
13353
  With no --key on a real terminal, opens your browser to sign in (mirrors \`gh auth login\`): prints