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.
- package/dist/cli.js +784 -300
- 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.
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 ||
|
|
224
|
+
return process.env.APPDATA || join11(homedir(), "AppData", "Roaming");
|
|
282
225
|
}
|
|
283
|
-
return process.env.XDG_CONFIG_HOME ||
|
|
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
|
|
231
|
+
return join11(configBase(), DIR_NAME);
|
|
289
232
|
}
|
|
290
233
|
function globalCredentialsPath() {
|
|
291
|
-
return
|
|
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
|
|
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
|
|
243
|
+
return join11(process.env.PINSAY_CONFIG_DIR, "cache");
|
|
301
244
|
if (process.platform === "win32") {
|
|
302
|
-
return
|
|
245
|
+
return join11(process.env.LOCALAPPDATA || join11(homedir(), "AppData", "Local"), DIR_NAME, "cache");
|
|
303
246
|
}
|
|
304
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
293
|
+
await fs12.rm(legacyFile, { force: true }).catch(() => {
|
|
351
294
|
});
|
|
352
|
-
await
|
|
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
|
|
359
|
-
await
|
|
360
|
-
await
|
|
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
|
|
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
|
|
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
|
|
642
|
-
import { join as
|
|
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
|
|
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 =
|
|
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(
|
|
658
|
+
const relPath = toPosix(relative6(root, file));
|
|
716
659
|
let code;
|
|
717
660
|
try {
|
|
718
|
-
code = await
|
|
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 =
|
|
735
|
-
await
|
|
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
|
|
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
|
|
691
|
+
await fs16.copyFile(target, prev);
|
|
749
692
|
}
|
|
750
693
|
const tmp = `${target}.tmp`;
|
|
751
|
-
await
|
|
752
|
-
await
|
|
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
|
|
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
|
|
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
|
|
827
|
-
await
|
|
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((
|
|
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
|
-
|
|
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
|
|
1881
|
+
const sep6 = envBefore && !envBefore.endsWith("\n") ? "\n" : "";
|
|
1939
1882
|
await fs6.writeFile(
|
|
1940
1883
|
envPath,
|
|
1941
|
-
`${envBefore}${
|
|
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 {
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
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
|
-
|
|
1971
|
-
const
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
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
|
|
1980
|
-
await
|
|
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
|
-
|
|
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
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
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 =
|
|
2039
|
-
const dirPath =
|
|
2264
|
+
const filePath = join8(cwd2, ".agents", name, "SKILL.md");
|
|
2265
|
+
const dirPath = join8(cwd2, ".agents", name);
|
|
2040
2266
|
try {
|
|
2041
|
-
await
|
|
2267
|
+
await fs8.rm(filePath, { force: true });
|
|
2042
2268
|
} catch {
|
|
2043
2269
|
}
|
|
2044
2270
|
try {
|
|
2045
|
-
const remaining = await
|
|
2271
|
+
const remaining = await fs8.readdir(dirPath);
|
|
2046
2272
|
if (remaining.length === 0)
|
|
2047
|
-
await
|
|
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
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
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
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
await
|
|
2066
|
-
await
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
}
|
|
2070
|
-
|
|
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
|
|
2073
|
-
await
|
|
2074
|
-
|
|
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
|
|
2078
|
-
const relDir = overrideDir ? join7(overrideDir, skillName) : dirname2(primaryPath);
|
|
2328
|
+
const relDir = posix.dirname(rel);
|
|
2079
2329
|
for (const name of SUB_SKILLS) {
|
|
2080
|
-
|
|
2081
|
-
|
|
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
|
|
2130
|
-
import { join as
|
|
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
|
|
2535
|
+
import { promises as fs10 } from "node:fs";
|
|
2136
2536
|
async function readStamp(path) {
|
|
2137
2537
|
let content;
|
|
2138
2538
|
try {
|
|
2139
|
-
content = await
|
|
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
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
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
|
|
2212
|
-
import { join as
|
|
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 ?
|
|
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
|
|
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 =
|
|
2336
|
-
await
|
|
2337
|
-
const targetPath =
|
|
2338
|
-
const tempPath =
|
|
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
|
|
2341
|
-
await
|
|
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
|
|
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 ?
|
|
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
|
|
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 =
|
|
2954
|
+
const abs = join12(cwd2, rel);
|
|
2555
2955
|
try {
|
|
2556
|
-
await
|
|
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
|
|
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
|
|
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
|
|
3015
|
+
for (const entry of await fs13.readdir(cwd2, { withFileTypes: true })) {
|
|
2616
3016
|
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
2617
|
-
candidates.push(
|
|
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
|
|
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
|
|
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
|
|
3052
|
+
for (const rel of skillFilesFor(config)) {
|
|
2654
3053
|
try {
|
|
2655
|
-
await
|
|
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
|
|
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
|
-
|
|
2692
|
-
|
|
2693
|
-
results.push(
|
|
2694
|
-
|
|
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
|
|
2704
|
-
import { join as
|
|
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
|
|
2708
|
-
import { dirname as
|
|
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
|
|
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 =
|
|
3147
|
+
const fullPath = join13(dir, entry);
|
|
2745
3148
|
let stat;
|
|
2746
3149
|
try {
|
|
2747
|
-
stat = await
|
|
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(
|
|
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 =
|
|
2854
|
-
const rootAbs =
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
3308
|
+
configAbsPath = join13(cwd2, name);
|
|
2906
3309
|
break;
|
|
2907
3310
|
}
|
|
2908
3311
|
}
|
|
2909
|
-
if (!foundConfigFile &&
|
|
3312
|
+
if (!foundConfigFile && resolve5(root) !== resolve5(cwd2)) {
|
|
2910
3313
|
for (const name of configNames) {
|
|
2911
|
-
const candidate =
|
|
3314
|
+
const candidate = join13(root, name);
|
|
2912
3315
|
try {
|
|
2913
3316
|
await readFile(candidate);
|
|
2914
|
-
foundConfigFile =
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 (
|
|
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) =>
|
|
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) &&
|
|
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((
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
4096
|
-
return
|
|
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(
|
|
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
|
|
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 =
|
|
4116
|
-
const candidates = [
|
|
4117
|
-
const projectJson = await readJsonSafe(
|
|
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(
|
|
4545
|
+
candidates.push(join14(root, projectJson.sourceRoot, "index.html"));
|
|
4120
4546
|
}
|
|
4121
|
-
candidates.push(
|
|
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 =
|
|
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 =
|
|
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
|
|
4758
|
+
let pkgStr = await fs15.readFile(join14(targetCwd, "package.json"), "utf8").catch(() => "");
|
|
4333
4759
|
if (!pkgStr)
|
|
4334
|
-
pkgStr = await
|
|
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
|
-
|
|
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
|
|
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 =
|
|
4372
|
-
let pkgStr = await
|
|
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
|
|
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
|
-
|
|
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
|
|
4563
|
-
import { join as
|
|
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 ?
|
|
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
|
|
4673
|
-
const
|
|
4674
|
-
|
|
4675
|
-
|
|
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
|
-
|
|
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 ?
|
|
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
|
|
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
|
|
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
|
-
|
|
5225
|
+
let abs;
|
|
4764
5226
|
try {
|
|
4765
|
-
await
|
|
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
|
-
|
|
4799
|
-
|
|
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 =
|
|
4823
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5468
|
-
await
|
|
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
|
|
5478
|
-
await
|
|
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
|
|
5554
|
-
await
|
|
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((
|
|
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
|
-
|
|
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((
|
|
11969
|
+
return new Promise((resolve9) => {
|
|
11487
11970
|
const json = serializeMessage(message);
|
|
11488
11971
|
if (this._stdout.write(json)) {
|
|
11489
|
-
|
|
11972
|
+
resolve9();
|
|
11490
11973
|
} else {
|
|
11491
|
-
this._stdout.once("drain",
|
|
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
|
|
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 (
|
|
12553
|
+
if (isAbsolute4(cleanPath)) {
|
|
12071
12554
|
throw mcpError("git", `Path must be relative to repo root: ${cleanPath}`);
|
|
12072
12555
|
}
|
|
12073
|
-
const resolved =
|
|
12074
|
-
const rel =
|
|
12075
|
-
if (rel.startsWith("..") ||
|
|
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
|
|
12547
|
-
const logStream =
|
|
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
|
|
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,
|
|
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 (
|
|
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
|