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