codebyplan 1.4.1 → 1.4.3
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 +10 -7
- package/dist/cli.js +790 -289
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ var VERSION, PACKAGE_NAME;
|
|
|
14
14
|
var init_version = __esm({
|
|
15
15
|
"src/lib/version.ts"() {
|
|
16
16
|
"use strict";
|
|
17
|
-
VERSION = "1.4.
|
|
17
|
+
VERSION = "1.4.3";
|
|
18
18
|
PACKAGE_NAME = "codebyplan";
|
|
19
19
|
}
|
|
20
20
|
});
|
|
@@ -52,7 +52,7 @@ function isRetryable(err) {
|
|
|
52
52
|
return false;
|
|
53
53
|
}
|
|
54
54
|
function delay(ms) {
|
|
55
|
-
return new Promise((
|
|
55
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
56
56
|
}
|
|
57
57
|
async function request(method, path, options) {
|
|
58
58
|
const url = buildUrl(path, options?.params);
|
|
@@ -126,7 +126,7 @@ var init_api = __esm({
|
|
|
126
126
|
"use strict";
|
|
127
127
|
init_version();
|
|
128
128
|
API_KEY = process.env.CODEBYPLAN_API_KEY ?? "";
|
|
129
|
-
BASE_URL = (process.env.CODEBYPLAN_API_URL ?? "https://codebyplan.com").replace(/\/$/, "");
|
|
129
|
+
BASE_URL = (process.env.CODEBYPLAN_API_URL ?? "https://www.codebyplan.com").replace(/\/$/, "");
|
|
130
130
|
REQUEST_TIMEOUT_MS = 12e4;
|
|
131
131
|
MAX_RETRIES = 3;
|
|
132
132
|
BASE_DELAY_MS = 1e3;
|
|
@@ -143,6 +143,68 @@ var init_api = __esm({
|
|
|
143
143
|
}
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
+
// src/lib/resolve-worktree.ts
|
|
147
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
148
|
+
import { join } from "node:path";
|
|
149
|
+
async function resolveAndCacheWorktreeId(repoId, projectPath, options) {
|
|
150
|
+
let worktreeId;
|
|
151
|
+
try {
|
|
152
|
+
const worktreesRes = await apiGet(`/worktrees?repo_id=${repoId}`);
|
|
153
|
+
const match = worktreesRes.data.find((wt) => {
|
|
154
|
+
const wtPath = wt.path.endsWith("/") ? wt.path.slice(0, -1) : wt.path;
|
|
155
|
+
return projectPath === wtPath || projectPath.startsWith(wtPath + "/");
|
|
156
|
+
});
|
|
157
|
+
if (match) worktreeId = match.id;
|
|
158
|
+
} catch (err) {
|
|
159
|
+
console.error(
|
|
160
|
+
`Worktree lookup failed: ${err instanceof Error ? err.message : String(err)}`
|
|
161
|
+
);
|
|
162
|
+
return void 0;
|
|
163
|
+
}
|
|
164
|
+
if (!worktreeId) {
|
|
165
|
+
return void 0;
|
|
166
|
+
}
|
|
167
|
+
if (options?.skipWrite) {
|
|
168
|
+
return worktreeId;
|
|
169
|
+
}
|
|
170
|
+
const codebyplanPath = join(projectPath, ".codebyplan.json");
|
|
171
|
+
let currentConfig = {};
|
|
172
|
+
try {
|
|
173
|
+
const raw = await readFile(codebyplanPath, "utf-8");
|
|
174
|
+
const parsed = JSON.parse(raw);
|
|
175
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
176
|
+
currentConfig = parsed;
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
}
|
|
180
|
+
if (currentConfig.worktree_id === worktreeId) {
|
|
181
|
+
return worktreeId;
|
|
182
|
+
}
|
|
183
|
+
const merged = {
|
|
184
|
+
...currentConfig,
|
|
185
|
+
worktree_id: worktreeId
|
|
186
|
+
};
|
|
187
|
+
try {
|
|
188
|
+
await writeFile(
|
|
189
|
+
codebyplanPath,
|
|
190
|
+
JSON.stringify(merged, null, 2) + "\n",
|
|
191
|
+
"utf-8"
|
|
192
|
+
);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
console.error(
|
|
195
|
+
`Failed to cache worktree_id in ${codebyplanPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
196
|
+
);
|
|
197
|
+
throw err;
|
|
198
|
+
}
|
|
199
|
+
return worktreeId;
|
|
200
|
+
}
|
|
201
|
+
var init_resolve_worktree = __esm({
|
|
202
|
+
"src/lib/resolve-worktree.ts"() {
|
|
203
|
+
"use strict";
|
|
204
|
+
init_api();
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
146
208
|
// src/lib/settings-merge.ts
|
|
147
209
|
function mergeSettings(template, local) {
|
|
148
210
|
const merged = { ...local };
|
|
@@ -208,14 +270,17 @@ var init_settings_merge = __esm({
|
|
|
208
270
|
});
|
|
209
271
|
|
|
210
272
|
// src/lib/hook-registry.ts
|
|
211
|
-
import { readdir, readFile } from "node:fs/promises";
|
|
212
|
-
import { join } from "node:path";
|
|
273
|
+
import { readdir, readFile as readFile2 } from "node:fs/promises";
|
|
274
|
+
import { join as join2 } from "node:path";
|
|
213
275
|
function parseHookMeta(content) {
|
|
214
|
-
const
|
|
215
|
-
if (!
|
|
276
|
+
const lineMatch = content.match(/^#\s*@hook:(.*)$/m);
|
|
277
|
+
if (!lineMatch) return null;
|
|
278
|
+
const parts = lineMatch[1].trim().split(/\s+/);
|
|
279
|
+
const event = parts[0];
|
|
280
|
+
if (!event) return null;
|
|
216
281
|
return {
|
|
217
|
-
event
|
|
218
|
-
matcher:
|
|
282
|
+
event,
|
|
283
|
+
matcher: parts.slice(1).join(" ")
|
|
219
284
|
};
|
|
220
285
|
}
|
|
221
286
|
async function discoverHooks(hooksDir) {
|
|
@@ -228,7 +293,7 @@ async function discoverHooks(hooksDir) {
|
|
|
228
293
|
return discovered;
|
|
229
294
|
}
|
|
230
295
|
for (const filename of filenames) {
|
|
231
|
-
const content = await
|
|
296
|
+
const content = await readFile2(join2(hooksDir, filename), "utf-8");
|
|
232
297
|
const meta = parseHookMeta(content);
|
|
233
298
|
if (meta) {
|
|
234
299
|
discovered.set(filename.replace(/\.sh$/, ""), meta);
|
|
@@ -375,45 +440,45 @@ __export(sync_engine_exports, {
|
|
|
375
440
|
});
|
|
376
441
|
import {
|
|
377
442
|
readdir as readdir2,
|
|
378
|
-
readFile as
|
|
379
|
-
writeFile,
|
|
443
|
+
readFile as readFile3,
|
|
444
|
+
writeFile as writeFile2,
|
|
380
445
|
unlink,
|
|
381
446
|
mkdir,
|
|
382
447
|
rmdir,
|
|
383
448
|
chmod,
|
|
384
449
|
stat
|
|
385
450
|
} from "node:fs/promises";
|
|
386
|
-
import { join as
|
|
451
|
+
import { join as join3, dirname } from "node:path";
|
|
387
452
|
function getTypeDir(claudeDir, dir) {
|
|
388
|
-
if (dir === "commands") return
|
|
389
|
-
return
|
|
453
|
+
if (dir === "commands") return join3(claudeDir, dir, "cbp");
|
|
454
|
+
return join3(claudeDir, dir);
|
|
390
455
|
}
|
|
391
456
|
function getFilePath(claudeDir, typeName, file) {
|
|
392
457
|
const cfg = typeConfig[typeName];
|
|
393
458
|
const typeDir = getTypeDir(claudeDir, cfg.dir);
|
|
394
459
|
if (cfg.subfolder) {
|
|
395
|
-
return
|
|
460
|
+
return join3(typeDir, file.name, `${cfg.subfolder}${cfg.ext}`);
|
|
396
461
|
}
|
|
397
462
|
if (typeName === "command" && file.category) {
|
|
398
|
-
return
|
|
463
|
+
return join3(typeDir, file.category, `${file.name}${cfg.ext}`);
|
|
399
464
|
}
|
|
400
465
|
if (typeName === "template") {
|
|
401
|
-
return
|
|
466
|
+
return join3(typeDir, file.name);
|
|
402
467
|
}
|
|
403
|
-
return
|
|
468
|
+
return join3(typeDir, `${file.name}${cfg.ext}`);
|
|
404
469
|
}
|
|
405
470
|
async function readDirRecursive(dir, base = dir) {
|
|
406
471
|
const result = /* @__PURE__ */ new Map();
|
|
407
472
|
try {
|
|
408
473
|
const entries = await readdir2(dir, { withFileTypes: true });
|
|
409
474
|
for (const entry of entries) {
|
|
410
|
-
const fullPath =
|
|
475
|
+
const fullPath = join3(dir, entry.name);
|
|
411
476
|
if (entry.isDirectory()) {
|
|
412
477
|
const sub = await readDirRecursive(fullPath, base);
|
|
413
478
|
for (const [k, v] of sub) result.set(k, v);
|
|
414
479
|
} else {
|
|
415
480
|
const relPath = fullPath.slice(base.length + 1);
|
|
416
|
-
const fileContent = await
|
|
481
|
+
const fileContent = await readFile3(fullPath, "utf-8");
|
|
417
482
|
result.set(relPath, fileContent);
|
|
418
483
|
}
|
|
419
484
|
}
|
|
@@ -423,7 +488,7 @@ async function readDirRecursive(dir, base = dir) {
|
|
|
423
488
|
}
|
|
424
489
|
async function isGitWorktree(projectPath) {
|
|
425
490
|
try {
|
|
426
|
-
const gitPath =
|
|
491
|
+
const gitPath = join3(projectPath, ".git");
|
|
427
492
|
const info = await stat(gitPath);
|
|
428
493
|
return info.isFile();
|
|
429
494
|
} catch {
|
|
@@ -450,7 +515,7 @@ async function executeSyncToLocal(options) {
|
|
|
450
515
|
const syncData = syncRes.data;
|
|
451
516
|
const repoData = repoRes.data;
|
|
452
517
|
syncData.claude_md = [];
|
|
453
|
-
const claudeDir =
|
|
518
|
+
const claudeDir = join3(projectPath, ".claude");
|
|
454
519
|
const worktree = await isGitWorktree(projectPath);
|
|
455
520
|
const byType = {};
|
|
456
521
|
const totals = { created: 0, updated: 0, deleted: 0, unchanged: 0 };
|
|
@@ -486,7 +551,7 @@ async function executeSyncToLocal(options) {
|
|
|
486
551
|
remotePathMap.set(relPath, { content: substituted, name: remote.name });
|
|
487
552
|
}
|
|
488
553
|
for (const [relPath, { content, name }] of remotePathMap) {
|
|
489
|
-
const fullPath =
|
|
554
|
+
const fullPath = join3(targetDir, relPath);
|
|
490
555
|
const localContent = localFiles.get(relPath);
|
|
491
556
|
if (localContent === void 0) {
|
|
492
557
|
const remoteFile = remoteFiles.find((f) => f.name === name);
|
|
@@ -498,14 +563,14 @@ async function executeSyncToLocal(options) {
|
|
|
498
563
|
});
|
|
499
564
|
if (!dryRun) {
|
|
500
565
|
await mkdir(dirname(fullPath), { recursive: true });
|
|
501
|
-
await
|
|
566
|
+
await writeFile2(fullPath, content, "utf-8");
|
|
502
567
|
if (typeName === "hook") await chmod(fullPath, 493);
|
|
503
568
|
}
|
|
504
569
|
result.created.push(name);
|
|
505
570
|
totals.created++;
|
|
506
571
|
} else if (localContent !== content) {
|
|
507
572
|
if (!dryRun) {
|
|
508
|
-
await
|
|
573
|
+
await writeFile2(fullPath, content, "utf-8");
|
|
509
574
|
if (typeName === "hook") await chmod(fullPath, 493);
|
|
510
575
|
}
|
|
511
576
|
result.updated.push(name);
|
|
@@ -517,7 +582,7 @@ async function executeSyncToLocal(options) {
|
|
|
517
582
|
}
|
|
518
583
|
for (const [relPath] of localFiles) {
|
|
519
584
|
if (!remotePathMap.has(relPath)) {
|
|
520
|
-
const fullPath =
|
|
585
|
+
const fullPath = join3(targetDir, relPath);
|
|
521
586
|
if (!dryRun) {
|
|
522
587
|
await unlink(fullPath);
|
|
523
588
|
await removeEmptyParents(fullPath, targetDir);
|
|
@@ -532,7 +597,7 @@ async function executeSyncToLocal(options) {
|
|
|
532
597
|
{
|
|
533
598
|
const typeName = "docs_stack";
|
|
534
599
|
const syncKey = "docs_stack";
|
|
535
|
-
const targetDir =
|
|
600
|
+
const targetDir = join3(projectPath, "docs", "stack");
|
|
536
601
|
const remoteFiles = syncData[syncKey] ?? [];
|
|
537
602
|
const result = {
|
|
538
603
|
created: [],
|
|
@@ -546,26 +611,26 @@ async function executeSyncToLocal(options) {
|
|
|
546
611
|
const localFiles = await readDirRecursive(targetDir);
|
|
547
612
|
const remotePathMap = /* @__PURE__ */ new Map();
|
|
548
613
|
for (const remote of remoteFiles) {
|
|
549
|
-
const relPath = remote.category ?
|
|
614
|
+
const relPath = remote.category ? join3(remote.category, remote.name) : remote.name;
|
|
550
615
|
const substituted = substituteVariables(remote.content, repoData);
|
|
551
616
|
remotePathMap.set(relPath, {
|
|
552
617
|
content: substituted,
|
|
553
|
-
name: `${remote.category
|
|
618
|
+
name: remote.category ? `${remote.category}/${remote.name}` : remote.name
|
|
554
619
|
});
|
|
555
620
|
}
|
|
556
621
|
for (const [relPath, { content, name }] of remotePathMap) {
|
|
557
|
-
const fullPath =
|
|
622
|
+
const fullPath = join3(targetDir, relPath);
|
|
558
623
|
const localContent = localFiles.get(relPath);
|
|
559
624
|
if (localContent === void 0) {
|
|
560
625
|
if (!dryRun) {
|
|
561
626
|
await mkdir(dirname(fullPath), { recursive: true });
|
|
562
|
-
await
|
|
627
|
+
await writeFile2(fullPath, content, "utf-8");
|
|
563
628
|
}
|
|
564
629
|
result.created.push(name);
|
|
565
630
|
totals.created++;
|
|
566
631
|
} else if (localContent !== content) {
|
|
567
632
|
if (!dryRun) {
|
|
568
|
-
await
|
|
633
|
+
await writeFile2(fullPath, content, "utf-8");
|
|
569
634
|
}
|
|
570
635
|
result.updated.push(name);
|
|
571
636
|
totals.updated++;
|
|
@@ -576,7 +641,7 @@ async function executeSyncToLocal(options) {
|
|
|
576
641
|
}
|
|
577
642
|
for (const [relPath] of localFiles) {
|
|
578
643
|
if (!remotePathMap.has(relPath)) {
|
|
579
|
-
const fullPath =
|
|
644
|
+
const fullPath = join3(targetDir, relPath);
|
|
580
645
|
if (!dryRun) {
|
|
581
646
|
await unlink(fullPath);
|
|
582
647
|
await removeEmptyParents(fullPath, targetDir);
|
|
@@ -596,8 +661,8 @@ async function executeSyncToLocal(options) {
|
|
|
596
661
|
globalSettings = { ...globalSettings, ...parsed };
|
|
597
662
|
}
|
|
598
663
|
const specialTypes = {
|
|
599
|
-
claude_md: () =>
|
|
600
|
-
settings: () =>
|
|
664
|
+
claude_md: () => join3(projectPath, "CLAUDE.md"),
|
|
665
|
+
settings: () => join3(projectPath, ".claude", "settings.json")
|
|
601
666
|
};
|
|
602
667
|
for (const [typeName, getPath] of Object.entries(specialTypes)) {
|
|
603
668
|
const remoteFiles = syncData[typeName] ?? [];
|
|
@@ -612,7 +677,7 @@ async function executeSyncToLocal(options) {
|
|
|
612
677
|
const remoteContent = substituteVariables(remote.content, repoData);
|
|
613
678
|
let localContent;
|
|
614
679
|
try {
|
|
615
|
-
localContent = await
|
|
680
|
+
localContent = await readFile3(targetPath, "utf-8");
|
|
616
681
|
} catch {
|
|
617
682
|
}
|
|
618
683
|
if (typeName === "settings") {
|
|
@@ -621,7 +686,7 @@ async function executeSyncToLocal(options) {
|
|
|
621
686
|
globalSettings,
|
|
622
687
|
repoSettings
|
|
623
688
|
);
|
|
624
|
-
const hooksDir =
|
|
689
|
+
const hooksDir = join3(projectPath, ".claude", "hooks");
|
|
625
690
|
const discovered = await discoverHooks(hooksDir);
|
|
626
691
|
if (localContent === void 0) {
|
|
627
692
|
const finalSettings = stripPermissionsAllow(combinedTemplate);
|
|
@@ -633,7 +698,7 @@ async function executeSyncToLocal(options) {
|
|
|
633
698
|
}
|
|
634
699
|
if (!dryRun) {
|
|
635
700
|
await mkdir(dirname(targetPath), { recursive: true });
|
|
636
|
-
await
|
|
701
|
+
await writeFile2(
|
|
637
702
|
targetPath,
|
|
638
703
|
JSON.stringify(finalSettings, null, 2) + "\n",
|
|
639
704
|
"utf-8"
|
|
@@ -654,7 +719,7 @@ async function executeSyncToLocal(options) {
|
|
|
654
719
|
const mergedContent = JSON.stringify(merged, null, 2) + "\n";
|
|
655
720
|
if (localContent !== mergedContent) {
|
|
656
721
|
if (!dryRun) {
|
|
657
|
-
await
|
|
722
|
+
await writeFile2(targetPath, mergedContent, "utf-8");
|
|
658
723
|
}
|
|
659
724
|
result.updated.push(remote.name);
|
|
660
725
|
totals.updated++;
|
|
@@ -667,13 +732,13 @@ async function executeSyncToLocal(options) {
|
|
|
667
732
|
if (localContent === void 0) {
|
|
668
733
|
if (!dryRun) {
|
|
669
734
|
await mkdir(dirname(targetPath), { recursive: true });
|
|
670
|
-
await
|
|
735
|
+
await writeFile2(targetPath, remoteContent, "utf-8");
|
|
671
736
|
}
|
|
672
737
|
result.created.push(remote.name);
|
|
673
738
|
totals.created++;
|
|
674
739
|
} else if (localContent !== remoteContent) {
|
|
675
740
|
if (!dryRun) {
|
|
676
|
-
await
|
|
741
|
+
await writeFile2(targetPath, remoteContent, "utf-8");
|
|
677
742
|
}
|
|
678
743
|
result.updated.push(remote.name);
|
|
679
744
|
totals.updated++;
|
|
@@ -774,15 +839,15 @@ __export(setup_exports, {
|
|
|
774
839
|
});
|
|
775
840
|
import { createInterface } from "node:readline/promises";
|
|
776
841
|
import { stdin, stdout } from "node:process";
|
|
777
|
-
import { readFile as
|
|
842
|
+
import { readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
|
|
778
843
|
import { homedir } from "node:os";
|
|
779
|
-
import { join as
|
|
844
|
+
import { join as join4 } from "node:path";
|
|
780
845
|
function getConfigPath(scope) {
|
|
781
|
-
return scope === "user" ?
|
|
846
|
+
return scope === "user" ? join4(homedir(), ".claude.json") : join4(process.cwd(), ".mcp.json");
|
|
782
847
|
}
|
|
783
848
|
async function readConfig(path) {
|
|
784
849
|
try {
|
|
785
|
-
const raw = await
|
|
850
|
+
const raw = await readFile4(path, "utf-8");
|
|
786
851
|
const parsed = JSON.parse(raw);
|
|
787
852
|
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
788
853
|
return parsed;
|
|
@@ -793,7 +858,7 @@ async function readConfig(path) {
|
|
|
793
858
|
}
|
|
794
859
|
}
|
|
795
860
|
function buildMcpEntry(apiKey) {
|
|
796
|
-
const baseUrl = process.env.CODEBYPLAN_API_URL ?? "https://codebyplan.com";
|
|
861
|
+
const baseUrl = process.env.CODEBYPLAN_API_URL ?? "https://www.codebyplan.com";
|
|
797
862
|
return {
|
|
798
863
|
url: `${baseUrl}/mcp`,
|
|
799
864
|
headers: { "x-api-key": apiKey }
|
|
@@ -806,7 +871,7 @@ async function writeMcpConfig(scope, apiKey) {
|
|
|
806
871
|
config.mcpServers = {};
|
|
807
872
|
}
|
|
808
873
|
config.mcpServers.codebyplan = buildMcpEntry(apiKey);
|
|
809
|
-
await
|
|
874
|
+
await writeFile3(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
810
875
|
return configPath;
|
|
811
876
|
}
|
|
812
877
|
async function verifyMcpConfig(scope, apiKey) {
|
|
@@ -835,7 +900,7 @@ async function runSetup() {
|
|
|
835
900
|
return;
|
|
836
901
|
}
|
|
837
902
|
console.log("\n Validating API key...");
|
|
838
|
-
const baseUrl = process.env.CODEBYPLAN_API_URL ?? "https://codebyplan.com";
|
|
903
|
+
const baseUrl = process.env.CODEBYPLAN_API_URL ?? "https://www.codebyplan.com";
|
|
839
904
|
const res = await fetch(`${baseUrl}/api/repos`, {
|
|
840
905
|
headers: { "x-api-key": apiKey },
|
|
841
906
|
signal: AbortSignal.timeout(1e4)
|
|
@@ -887,7 +952,7 @@ async function runSetup() {
|
|
|
887
952
|
`
|
|
888
953
|
);
|
|
889
954
|
console.log(
|
|
890
|
-
` { "url": "https://codebyplan.com/mcp", "headers": { "x-api-key": "${apiKey}" } }
|
|
955
|
+
` { "url": "https://www.codebyplan.com/mcp", "headers": { "x-api-key": "${apiKey}" } }
|
|
891
956
|
`
|
|
892
957
|
);
|
|
893
958
|
}
|
|
@@ -910,27 +975,28 @@ async function runSetup() {
|
|
|
910
975
|
console.log(`
|
|
911
976
|
Selected: ${selectedRepo.name}
|
|
912
977
|
`);
|
|
913
|
-
let worktreeId;
|
|
914
978
|
const projectPath = process.cwd();
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
} catch {
|
|
922
|
-
}
|
|
923
|
-
const codebyplanPath = join3(projectPath, ".codebyplan.json");
|
|
979
|
+
const worktreeId = await resolveAndCacheWorktreeId(
|
|
980
|
+
selectedRepo.id,
|
|
981
|
+
projectPath,
|
|
982
|
+
{ skipWrite: true }
|
|
983
|
+
);
|
|
984
|
+
const codebyplanPath = join4(projectPath, ".codebyplan.json");
|
|
924
985
|
const codebyplanConfig = {
|
|
925
986
|
repo_id: selectedRepo.id
|
|
926
987
|
};
|
|
927
988
|
if (worktreeId) codebyplanConfig.worktree_id = worktreeId;
|
|
928
|
-
await
|
|
989
|
+
await writeFile3(
|
|
929
990
|
codebyplanPath,
|
|
930
991
|
JSON.stringify(codebyplanConfig, null, 2) + "\n",
|
|
931
992
|
"utf-8"
|
|
932
993
|
);
|
|
933
994
|
console.log(` Created ${codebyplanPath}`);
|
|
995
|
+
if (worktreeId) {
|
|
996
|
+
console.log(
|
|
997
|
+
` Worktree id set (${worktreeId}) \u2014 this worktree is now identified for hard-lock enforcement.`
|
|
998
|
+
);
|
|
999
|
+
}
|
|
934
1000
|
console.log("\n Running initial sync...\n");
|
|
935
1001
|
try {
|
|
936
1002
|
const { executeSyncToLocal: executeSyncToLocal2 } = await Promise.resolve().then(() => (init_sync_engine(), sync_engine_exports));
|
|
@@ -965,20 +1031,36 @@ async function runSetup() {
|
|
|
965
1031
|
var init_setup = __esm({
|
|
966
1032
|
"src/cli/setup.ts"() {
|
|
967
1033
|
"use strict";
|
|
968
|
-
|
|
1034
|
+
init_resolve_worktree();
|
|
969
1035
|
}
|
|
970
1036
|
});
|
|
971
1037
|
|
|
972
1038
|
// src/cli/config.ts
|
|
973
|
-
import { readFile as
|
|
974
|
-
import { join as
|
|
1039
|
+
import { readFile as readFile5 } from "node:fs/promises";
|
|
1040
|
+
import { join as join5, resolve } from "node:path";
|
|
1041
|
+
async function findCodebyplanConfig(startDir, maxDepth = 20) {
|
|
1042
|
+
let cursor = resolve(startDir);
|
|
1043
|
+
for (let depth = 0; depth < maxDepth; depth++) {
|
|
1044
|
+
const configPath = join5(cursor, ".codebyplan.json");
|
|
1045
|
+
try {
|
|
1046
|
+
const raw = await readFile5(configPath, "utf-8");
|
|
1047
|
+
const parsed = JSON.parse(raw);
|
|
1048
|
+
return { path: configPath, contents: parsed };
|
|
1049
|
+
} catch {
|
|
1050
|
+
}
|
|
1051
|
+
const parent = resolve(cursor, "..");
|
|
1052
|
+
if (parent === cursor) return null;
|
|
1053
|
+
cursor = parent;
|
|
1054
|
+
}
|
|
1055
|
+
return null;
|
|
1056
|
+
}
|
|
975
1057
|
function parseFlags(startIndex) {
|
|
976
1058
|
const flags = {};
|
|
977
1059
|
const args = process.argv.slice(startIndex);
|
|
978
1060
|
for (let i = 0; i < args.length; i++) {
|
|
979
|
-
const
|
|
980
|
-
if (
|
|
981
|
-
const key =
|
|
1061
|
+
const arg = args[i];
|
|
1062
|
+
if (arg.startsWith("--") && i + 1 < args.length) {
|
|
1063
|
+
const key = arg.slice(2);
|
|
982
1064
|
flags[key] = args[++i];
|
|
983
1065
|
}
|
|
984
1066
|
}
|
|
@@ -992,13 +1074,10 @@ async function resolveConfig(flags) {
|
|
|
992
1074
|
let repoId = flags["repo-id"] ?? process.env.CODEBYPLAN_REPO_ID;
|
|
993
1075
|
let worktreeId = flags["worktree-id"] ?? process.env.CODEBYPLAN_WORKTREE_ID;
|
|
994
1076
|
if (!repoId || !worktreeId) {
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
if (!repoId) repoId = config.repo_id;
|
|
1000
|
-
if (!worktreeId) worktreeId = config.worktree_id;
|
|
1001
|
-
} catch {
|
|
1077
|
+
const found = await findCodebyplanConfig(projectPath);
|
|
1078
|
+
if (found) {
|
|
1079
|
+
if (!repoId) repoId = found.contents.repo_id;
|
|
1080
|
+
if (!worktreeId) worktreeId = found.contents.worktree_id;
|
|
1002
1081
|
}
|
|
1003
1082
|
}
|
|
1004
1083
|
if (!repoId) {
|
|
@@ -1015,8 +1094,8 @@ var init_config = __esm({
|
|
|
1015
1094
|
});
|
|
1016
1095
|
|
|
1017
1096
|
// src/cli/fileMapper.ts
|
|
1018
|
-
import { readdir as readdir3, readFile as
|
|
1019
|
-
import { join as
|
|
1097
|
+
import { readdir as readdir3, readFile as readFile6 } from "node:fs/promises";
|
|
1098
|
+
import { join as join6, extname } from "node:path";
|
|
1020
1099
|
function extractScope(content, type) {
|
|
1021
1100
|
if (type === "hook") {
|
|
1022
1101
|
const match = content.match(/^#\s*@scope:\s*(\S+)/m);
|
|
@@ -1046,29 +1125,29 @@ function compositeKey(type, name, category) {
|
|
|
1046
1125
|
}
|
|
1047
1126
|
async function scanLocalFiles(claudeDir, projectPath) {
|
|
1048
1127
|
const result = /* @__PURE__ */ new Map();
|
|
1049
|
-
await scanCommands(
|
|
1128
|
+
await scanCommands(join6(claudeDir, "commands", "cbp"), result);
|
|
1050
1129
|
await scanSubfolderType(
|
|
1051
|
-
|
|
1130
|
+
join6(claudeDir, "agents"),
|
|
1052
1131
|
"agent",
|
|
1053
1132
|
"AGENT.md",
|
|
1054
1133
|
result
|
|
1055
1134
|
);
|
|
1056
1135
|
await scanSubfolderType(
|
|
1057
|
-
|
|
1136
|
+
join6(claudeDir, "skills"),
|
|
1058
1137
|
"skill",
|
|
1059
1138
|
"SKILL.md",
|
|
1060
1139
|
result
|
|
1061
1140
|
);
|
|
1062
|
-
await scanFlatType(
|
|
1063
|
-
await scanFlatType(
|
|
1064
|
-
await scanTemplates(
|
|
1141
|
+
await scanFlatType(join6(claudeDir, "rules"), "rule", ".md", result);
|
|
1142
|
+
await scanFlatType(join6(claudeDir, "hooks"), "hook", ".sh", result);
|
|
1143
|
+
await scanTemplates(join6(claudeDir, "templates"), result);
|
|
1065
1144
|
await scanCategorizedType(
|
|
1066
|
-
|
|
1145
|
+
join6(claudeDir, "context"),
|
|
1067
1146
|
"context",
|
|
1068
1147
|
".md",
|
|
1069
1148
|
result
|
|
1070
1149
|
);
|
|
1071
|
-
await scanDocsRecursive(
|
|
1150
|
+
await scanDocsRecursive(join6(claudeDir, "docs"), result);
|
|
1072
1151
|
await scanSettings(claudeDir, projectPath, result);
|
|
1073
1152
|
return result;
|
|
1074
1153
|
}
|
|
@@ -1086,12 +1165,12 @@ async function scanCommandsRecursive(baseDir, currentDir, result) {
|
|
|
1086
1165
|
if (entry.isDirectory()) {
|
|
1087
1166
|
await scanCommandsRecursive(
|
|
1088
1167
|
baseDir,
|
|
1089
|
-
|
|
1168
|
+
join6(currentDir, entry.name),
|
|
1090
1169
|
result
|
|
1091
1170
|
);
|
|
1092
1171
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1093
1172
|
const name = entry.name.slice(0, -3);
|
|
1094
|
-
const content = await
|
|
1173
|
+
const content = await readFile6(join6(currentDir, entry.name), "utf-8");
|
|
1095
1174
|
const relDir = currentDir.slice(baseDir.length + 1);
|
|
1096
1175
|
const category = relDir || null;
|
|
1097
1176
|
const scope = extractScope(content, "command");
|
|
@@ -1109,9 +1188,9 @@ async function scanSubfolderType(dir, type, fileName, result) {
|
|
|
1109
1188
|
}
|
|
1110
1189
|
for (const entry of entries) {
|
|
1111
1190
|
if (entry.isDirectory()) {
|
|
1112
|
-
const filePath =
|
|
1191
|
+
const filePath = join6(dir, entry.name, fileName);
|
|
1113
1192
|
try {
|
|
1114
|
-
const content = await
|
|
1193
|
+
const content = await readFile6(filePath, "utf-8");
|
|
1115
1194
|
const scope = extractScope(content, type);
|
|
1116
1195
|
const key = compositeKey(type, entry.name, null);
|
|
1117
1196
|
result.set(key, {
|
|
@@ -1136,7 +1215,7 @@ async function scanFlatType(dir, type, ext, result) {
|
|
|
1136
1215
|
for (const entry of entries) {
|
|
1137
1216
|
if (entry.isFile() && entry.name.endsWith(ext)) {
|
|
1138
1217
|
const name = entry.name.slice(0, -ext.length);
|
|
1139
|
-
const content = await
|
|
1218
|
+
const content = await readFile6(join6(dir, entry.name), "utf-8");
|
|
1140
1219
|
const scope = extractScope(content, type);
|
|
1141
1220
|
const key = compositeKey(type, name, null);
|
|
1142
1221
|
result.set(key, { type, name, category: null, content, scope });
|
|
@@ -1155,7 +1234,7 @@ async function scanCategorizedType(dir, type, ext, result) {
|
|
|
1155
1234
|
const category = entry.name;
|
|
1156
1235
|
let subEntries;
|
|
1157
1236
|
try {
|
|
1158
|
-
subEntries = await readdir3(
|
|
1237
|
+
subEntries = await readdir3(join6(dir, category), {
|
|
1159
1238
|
withFileTypes: true
|
|
1160
1239
|
});
|
|
1161
1240
|
} catch {
|
|
@@ -1164,8 +1243,8 @@ async function scanCategorizedType(dir, type, ext, result) {
|
|
|
1164
1243
|
for (const sub of subEntries) {
|
|
1165
1244
|
if (sub.isFile() && sub.name.endsWith(ext)) {
|
|
1166
1245
|
const name = sub.name.slice(0, -ext.length);
|
|
1167
|
-
const content = await
|
|
1168
|
-
|
|
1246
|
+
const content = await readFile6(
|
|
1247
|
+
join6(dir, category, sub.name),
|
|
1169
1248
|
"utf-8"
|
|
1170
1249
|
);
|
|
1171
1250
|
const scope = extractScope(content, type);
|
|
@@ -1175,7 +1254,7 @@ async function scanCategorizedType(dir, type, ext, result) {
|
|
|
1175
1254
|
}
|
|
1176
1255
|
} else if (entry.isFile() && entry.name.endsWith(ext)) {
|
|
1177
1256
|
const name = entry.name.slice(0, -ext.length);
|
|
1178
|
-
const content = await
|
|
1257
|
+
const content = await readFile6(join6(dir, entry.name), "utf-8");
|
|
1179
1258
|
const scope = extractScope(content, type);
|
|
1180
1259
|
const key = compositeKey(type, name, null);
|
|
1181
1260
|
result.set(key, { type, name, category: null, content, scope });
|
|
@@ -1194,10 +1273,10 @@ async function scanDocsDir(baseDir, currentDir, result) {
|
|
|
1194
1273
|
}
|
|
1195
1274
|
for (const entry of entries) {
|
|
1196
1275
|
if (entry.isDirectory()) {
|
|
1197
|
-
await scanDocsDir(baseDir,
|
|
1276
|
+
await scanDocsDir(baseDir, join6(currentDir, entry.name), result);
|
|
1198
1277
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1199
1278
|
const name = entry.name.slice(0, -3);
|
|
1200
|
-
const content = await
|
|
1279
|
+
const content = await readFile6(join6(currentDir, entry.name), "utf-8");
|
|
1201
1280
|
const scope = extractScope(content, "docs");
|
|
1202
1281
|
const relDir = currentDir.slice(baseDir.length + 1);
|
|
1203
1282
|
const category = relDir || null;
|
|
@@ -1215,7 +1294,7 @@ async function scanTemplates(dir, result) {
|
|
|
1215
1294
|
}
|
|
1216
1295
|
for (const entry of entries) {
|
|
1217
1296
|
if (entry.isFile() && extname(entry.name)) {
|
|
1218
|
-
const content = await
|
|
1297
|
+
const content = await readFile6(join6(dir, entry.name), "utf-8");
|
|
1219
1298
|
const scope = extractScope(content, "template");
|
|
1220
1299
|
const key = compositeKey("template", entry.name, null);
|
|
1221
1300
|
result.set(key, {
|
|
@@ -1229,10 +1308,10 @@ async function scanTemplates(dir, result) {
|
|
|
1229
1308
|
}
|
|
1230
1309
|
}
|
|
1231
1310
|
async function scanSettings(claudeDir, projectPath, result) {
|
|
1232
|
-
const settingsPath =
|
|
1311
|
+
const settingsPath = join6(claudeDir, "settings.json");
|
|
1233
1312
|
let raw;
|
|
1234
1313
|
try {
|
|
1235
|
-
raw = await
|
|
1314
|
+
raw = await readFile6(settingsPath, "utf-8");
|
|
1236
1315
|
} catch {
|
|
1237
1316
|
return;
|
|
1238
1317
|
}
|
|
@@ -1244,7 +1323,7 @@ async function scanSettings(claudeDir, projectPath, result) {
|
|
|
1244
1323
|
}
|
|
1245
1324
|
parsed = stripPermissionsAllow(parsed);
|
|
1246
1325
|
if (parsed.hooks && typeof parsed.hooks === "object") {
|
|
1247
|
-
const hooksDir = projectPath ?
|
|
1326
|
+
const hooksDir = projectPath ? join6(projectPath, ".claude", "hooks") : join6(claudeDir, "hooks");
|
|
1248
1327
|
const discovered = await discoverHooks(hooksDir);
|
|
1249
1328
|
if (discovered.size > 0) {
|
|
1250
1329
|
parsed.hooks = stripDiscoveredHooks(
|
|
@@ -1576,8 +1655,8 @@ var init_confirm = __esm({
|
|
|
1576
1655
|
});
|
|
1577
1656
|
|
|
1578
1657
|
// src/lib/tech-detect.ts
|
|
1579
|
-
import { readFile as
|
|
1580
|
-
import { join as
|
|
1658
|
+
import { readFile as readFile7, access, readdir as readdir4 } from "node:fs/promises";
|
|
1659
|
+
import { join as join7, relative } from "node:path";
|
|
1581
1660
|
async function fileExists(filePath) {
|
|
1582
1661
|
try {
|
|
1583
1662
|
await access(filePath);
|
|
@@ -1590,8 +1669,8 @@ async function discoverMonorepoApps(projectPath) {
|
|
|
1590
1669
|
const apps = [];
|
|
1591
1670
|
const patterns = [];
|
|
1592
1671
|
try {
|
|
1593
|
-
const raw = await
|
|
1594
|
-
|
|
1672
|
+
const raw = await readFile7(
|
|
1673
|
+
join7(projectPath, "pnpm-workspace.yaml"),
|
|
1595
1674
|
"utf-8"
|
|
1596
1675
|
);
|
|
1597
1676
|
const matches = raw.match(/^\s*-\s*['"]?([^'"#\n]+)['"]?/gm);
|
|
@@ -1605,7 +1684,7 @@ async function discoverMonorepoApps(projectPath) {
|
|
|
1605
1684
|
}
|
|
1606
1685
|
if (patterns.length === 0) {
|
|
1607
1686
|
try {
|
|
1608
|
-
const raw = await
|
|
1687
|
+
const raw = await readFile7(join7(projectPath, "package.json"), "utf-8");
|
|
1609
1688
|
const pkg = JSON.parse(raw);
|
|
1610
1689
|
const ws = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages;
|
|
1611
1690
|
if (ws) patterns.push(...ws);
|
|
@@ -1615,14 +1694,14 @@ async function discoverMonorepoApps(projectPath) {
|
|
|
1615
1694
|
for (const pattern of patterns) {
|
|
1616
1695
|
if (pattern.endsWith("/*")) {
|
|
1617
1696
|
const dir = pattern.slice(0, -2);
|
|
1618
|
-
const absDir =
|
|
1697
|
+
const absDir = join7(projectPath, dir);
|
|
1619
1698
|
try {
|
|
1620
1699
|
const entries = await readdir4(absDir, { withFileTypes: true });
|
|
1621
1700
|
for (const entry of entries) {
|
|
1622
1701
|
if (entry.isDirectory()) {
|
|
1623
|
-
const relPath =
|
|
1624
|
-
const absPath =
|
|
1625
|
-
if (await fileExists(
|
|
1702
|
+
const relPath = join7(dir, entry.name);
|
|
1703
|
+
const absPath = join7(absDir, entry.name);
|
|
1704
|
+
if (await fileExists(join7(absPath, "package.json"))) {
|
|
1626
1705
|
apps.push({ name: entry.name, path: relPath, absPath });
|
|
1627
1706
|
}
|
|
1628
1707
|
}
|
|
@@ -1633,12 +1712,77 @@ async function discoverMonorepoApps(projectPath) {
|
|
|
1633
1712
|
}
|
|
1634
1713
|
return apps;
|
|
1635
1714
|
}
|
|
1715
|
+
async function hasJsxFile(dir, depth = 0) {
|
|
1716
|
+
if (depth > 6) return false;
|
|
1717
|
+
try {
|
|
1718
|
+
const entries = await readdir4(dir, { withFileTypes: true });
|
|
1719
|
+
for (const entry of entries) {
|
|
1720
|
+
const name = entry.name;
|
|
1721
|
+
if (entry.isDirectory()) {
|
|
1722
|
+
if (SKIP_DIRS.has(name) || JSX_SKIP_DIRS.has(name)) continue;
|
|
1723
|
+
if (await hasJsxFile(join7(dir, name), depth + 1)) return true;
|
|
1724
|
+
} else if (entry.isFile()) {
|
|
1725
|
+
if (JSX_TEST_PATTERN.test(name)) continue;
|
|
1726
|
+
if (name.endsWith(".tsx") || name.endsWith(".jsx")) return true;
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
} catch (err) {
|
|
1730
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
1731
|
+
return false;
|
|
1732
|
+
}
|
|
1733
|
+
console.error(
|
|
1734
|
+
`detectCapabilities: readdir failed for ${dir}: ${err instanceof Error ? err.message : String(err)}`
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
return false;
|
|
1738
|
+
}
|
|
1739
|
+
async function detectCapabilities(dirPath, pkgJson) {
|
|
1740
|
+
const caps = /* @__PURE__ */ new Set();
|
|
1741
|
+
for (const sub of JSX_SCAN_DIRS) {
|
|
1742
|
+
if (await hasJsxFile(join7(dirPath, sub))) {
|
|
1743
|
+
caps.add("jsx");
|
|
1744
|
+
break;
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
if (pkgJson) {
|
|
1748
|
+
const allDeps = {
|
|
1749
|
+
...pkgJson.dependencies ?? {},
|
|
1750
|
+
...pkgJson.devDependencies ?? {}
|
|
1751
|
+
};
|
|
1752
|
+
for (const dep of Object.keys(allDeps)) {
|
|
1753
|
+
if (SERVER_FRAMEWORK_DEPS.has(dep)) {
|
|
1754
|
+
caps.add("node-server");
|
|
1755
|
+
break;
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
if (!caps.has("node-server") && typeof pkgJson.main === "string") {
|
|
1759
|
+
if (SERVER_MAIN_ENTRIES.has(pkgJson.main.trim())) {
|
|
1760
|
+
caps.add("node-server");
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
if (!caps.has("node-server") && await fileExists(join7(dirPath, "src", "main.ts"))) {
|
|
1765
|
+
caps.add("node-server");
|
|
1766
|
+
}
|
|
1767
|
+
if (pkgJson && pkgJson.bin) {
|
|
1768
|
+
if (typeof pkgJson.bin === "string" && pkgJson.bin.trim().length > 0) {
|
|
1769
|
+
caps.add("cli-bin");
|
|
1770
|
+
} else if (typeof pkgJson.bin === "object" && pkgJson.bin !== null && Object.keys(pkgJson.bin).length > 0) {
|
|
1771
|
+
caps.add("cli-bin");
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
return caps;
|
|
1775
|
+
}
|
|
1636
1776
|
async function detectFromDirectory(dirPath) {
|
|
1637
1777
|
const seen = /* @__PURE__ */ new Map();
|
|
1778
|
+
let pkgJson = null;
|
|
1638
1779
|
try {
|
|
1639
|
-
const raw = await
|
|
1640
|
-
|
|
1641
|
-
const allDeps = {
|
|
1780
|
+
const raw = await readFile7(join7(dirPath, "package.json"), "utf-8");
|
|
1781
|
+
pkgJson = JSON.parse(raw);
|
|
1782
|
+
const allDeps = {
|
|
1783
|
+
...pkgJson.dependencies ?? {},
|
|
1784
|
+
...pkgJson.devDependencies ?? {}
|
|
1785
|
+
};
|
|
1642
1786
|
for (const depName of Object.keys(allDeps)) {
|
|
1643
1787
|
const rule = PACKAGE_MAP[depName];
|
|
1644
1788
|
if (rule) {
|
|
@@ -1665,15 +1809,44 @@ async function detectFromDirectory(dirPath) {
|
|
|
1665
1809
|
}
|
|
1666
1810
|
for (const { file, rule } of CONFIG_FILE_MAP) {
|
|
1667
1811
|
const key = rule.name.toLowerCase();
|
|
1668
|
-
if (!seen.has(key) && await fileExists(
|
|
1812
|
+
if (!seen.has(key) && await fileExists(join7(dirPath, file))) {
|
|
1669
1813
|
seen.set(key, { name: rule.name, category: rule.category });
|
|
1670
1814
|
}
|
|
1671
1815
|
}
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1816
|
+
const capabilities = await detectCapabilities(dirPath, pkgJson);
|
|
1817
|
+
const capsArray = Array.from(capabilities).sort();
|
|
1818
|
+
const entries = Array.from(seen.values()).map((entry) => {
|
|
1819
|
+
const isBearer = CAPABILITY_BEARER_NAMES.has(entry.name.toLowerCase());
|
|
1820
|
+
return {
|
|
1821
|
+
...entry,
|
|
1822
|
+
capabilities: isBearer ? capsArray : []
|
|
1823
|
+
};
|
|
1676
1824
|
});
|
|
1825
|
+
if (capsArray.length > 0) {
|
|
1826
|
+
const hasBearerWithCaps = entries.some(
|
|
1827
|
+
(e) => CAPABILITY_BEARER_NAMES.has(e.name.toLowerCase()) && (e.capabilities?.some((c) => c.length > 0) ?? false)
|
|
1828
|
+
);
|
|
1829
|
+
if (!hasBearerWithCaps) {
|
|
1830
|
+
entries.push({
|
|
1831
|
+
name: SYNTHETIC_CARRIER_NAME,
|
|
1832
|
+
category: "tool",
|
|
1833
|
+
capabilities: capsArray
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
return entries.sort(compareByCategoryThenName);
|
|
1838
|
+
}
|
|
1839
|
+
function collectCapabilities(entries) {
|
|
1840
|
+
const set = /* @__PURE__ */ new Set();
|
|
1841
|
+
for (const entry of entries) {
|
|
1842
|
+
if (!entry.capabilities) continue;
|
|
1843
|
+
for (const cap of entry.capabilities) {
|
|
1844
|
+
if (typeof cap === "string" && cap.length > 0) {
|
|
1845
|
+
set.add(cap.toLowerCase());
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
return Array.from(set).sort();
|
|
1677
1850
|
}
|
|
1678
1851
|
async function detectTechStack(projectPath) {
|
|
1679
1852
|
const repo = await detectFromDirectory(projectPath);
|
|
@@ -1692,40 +1865,80 @@ async function detectTechStack(projectPath) {
|
|
|
1692
1865
|
for (const app of apps) {
|
|
1693
1866
|
for (const entry of app.stack) {
|
|
1694
1867
|
const key = entry.name.toLowerCase();
|
|
1695
|
-
|
|
1868
|
+
const existing = flatMap.get(key);
|
|
1869
|
+
if (!existing) {
|
|
1696
1870
|
flatMap.set(key, entry);
|
|
1871
|
+
} else if (entry.capabilities?.length) {
|
|
1872
|
+
const merged = Array.from(
|
|
1873
|
+
/* @__PURE__ */ new Set([...existing.capabilities ?? [], ...entry.capabilities])
|
|
1874
|
+
).sort();
|
|
1875
|
+
flatMap.set(key, { ...existing, capabilities: merged });
|
|
1697
1876
|
}
|
|
1698
1877
|
}
|
|
1699
1878
|
}
|
|
1700
|
-
const
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1879
|
+
const repoCleaned = stripSyntheticIfCovered(repo).sort(
|
|
1880
|
+
compareByCategoryThenName
|
|
1881
|
+
);
|
|
1882
|
+
const appsCleaned = apps.map((a) => ({
|
|
1883
|
+
...a,
|
|
1884
|
+
stack: stripSyntheticIfCovered(a.stack).sort(compareByCategoryThenName)
|
|
1885
|
+
}));
|
|
1886
|
+
const flat = stripSyntheticIfCovered(Array.from(flatMap.values())).sort(
|
|
1887
|
+
compareByCategoryThenName
|
|
1888
|
+
);
|
|
1889
|
+
return { repo: repoCleaned, apps: appsCleaned, flat };
|
|
1890
|
+
}
|
|
1891
|
+
function stripSyntheticIfCovered(entries) {
|
|
1892
|
+
const synth = entries.find((e) => e.name === SYNTHETIC_CARRIER_NAME);
|
|
1893
|
+
if (!synth?.capabilities?.length) return entries;
|
|
1894
|
+
const realBearerCaps = /* @__PURE__ */ new Set();
|
|
1895
|
+
for (const e of entries) {
|
|
1896
|
+
if (e.name === SYNTHETIC_CARRIER_NAME) continue;
|
|
1897
|
+
for (const c of e.capabilities ?? []) realBearerCaps.add(c);
|
|
1898
|
+
}
|
|
1899
|
+
if (synth.capabilities.every((c) => realBearerCaps.has(c))) {
|
|
1900
|
+
return entries.filter((e) => e.name !== SYNTHETIC_CARRIER_NAME);
|
|
1901
|
+
}
|
|
1902
|
+
return entries;
|
|
1706
1903
|
}
|
|
1707
1904
|
function mergeTechStack(remote, detected) {
|
|
1708
|
-
const
|
|
1905
|
+
const stripCarrier = (e) => e.name !== SYNTHETIC_CARRIER_NAME;
|
|
1906
|
+
const cleanDetected = {
|
|
1907
|
+
repo: detected.repo.filter(stripCarrier),
|
|
1908
|
+
apps: detected.apps.map((a) => ({
|
|
1909
|
+
...a,
|
|
1910
|
+
stack: a.stack.filter(stripCarrier)
|
|
1911
|
+
})),
|
|
1912
|
+
flat: detected.flat.filter(stripCarrier)
|
|
1913
|
+
};
|
|
1914
|
+
const remoteResult = Array.isArray(remote) ? {
|
|
1915
|
+
repo: remote.filter(stripCarrier),
|
|
1916
|
+
apps: [],
|
|
1917
|
+
flat: remote.filter(stripCarrier)
|
|
1918
|
+
} : {
|
|
1919
|
+
repo: remote.repo.filter(stripCarrier),
|
|
1920
|
+
apps: remote.apps.map((a) => ({
|
|
1921
|
+
...a,
|
|
1922
|
+
stack: a.stack.filter(stripCarrier)
|
|
1923
|
+
})),
|
|
1924
|
+
flat: remote.flat.filter(stripCarrier)
|
|
1925
|
+
};
|
|
1709
1926
|
const seen = /* @__PURE__ */ new Map();
|
|
1710
1927
|
for (const entry of remoteResult.flat) {
|
|
1711
1928
|
seen.set(entry.name.toLowerCase(), entry);
|
|
1712
1929
|
}
|
|
1713
1930
|
const added = [];
|
|
1714
|
-
for (const entry of
|
|
1931
|
+
for (const entry of cleanDetected.flat) {
|
|
1715
1932
|
const key = entry.name.toLowerCase();
|
|
1716
1933
|
if (!seen.has(key)) {
|
|
1717
1934
|
seen.set(key, entry);
|
|
1718
1935
|
added.push(entry);
|
|
1719
1936
|
}
|
|
1720
1937
|
}
|
|
1721
|
-
const flat = Array.from(seen.values()).sort(
|
|
1722
|
-
const catCmp = a.category.localeCompare(b.category);
|
|
1723
|
-
if (catCmp !== 0) return catCmp;
|
|
1724
|
-
return a.name.localeCompare(b.name);
|
|
1725
|
-
});
|
|
1938
|
+
const flat = Array.from(seen.values()).sort(compareByCategoryThenName);
|
|
1726
1939
|
const merged = {
|
|
1727
|
-
repo:
|
|
1728
|
-
apps:
|
|
1940
|
+
repo: cleanDetected.repo,
|
|
1941
|
+
apps: cleanDetected.apps,
|
|
1729
1942
|
flat
|
|
1730
1943
|
};
|
|
1731
1944
|
return { merged, added };
|
|
@@ -1774,7 +1987,7 @@ function categorizeDependency(depName) {
|
|
|
1774
1987
|
async function findPackageJsonFiles(dir, projectPath, depth = 0) {
|
|
1775
1988
|
if (depth > 4) return [];
|
|
1776
1989
|
const results = [];
|
|
1777
|
-
const pkgPath =
|
|
1990
|
+
const pkgPath = join7(dir, "package.json");
|
|
1778
1991
|
if (await fileExists(pkgPath)) {
|
|
1779
1992
|
results.push(pkgPath);
|
|
1780
1993
|
}
|
|
@@ -1783,7 +1996,7 @@ async function findPackageJsonFiles(dir, projectPath, depth = 0) {
|
|
|
1783
1996
|
for (const entry of entries) {
|
|
1784
1997
|
if (!entry.isDirectory() || SKIP_DIRS.has(entry.name)) continue;
|
|
1785
1998
|
const subResults = await findPackageJsonFiles(
|
|
1786
|
-
|
|
1999
|
+
join7(dir, entry.name),
|
|
1787
2000
|
projectPath,
|
|
1788
2001
|
depth + 1
|
|
1789
2002
|
);
|
|
@@ -1798,7 +2011,7 @@ async function scanAllDependencies(projectPath) {
|
|
|
1798
2011
|
const dependencies = [];
|
|
1799
2012
|
for (const pkgPath of packageJsonPaths) {
|
|
1800
2013
|
try {
|
|
1801
|
-
const raw = await
|
|
2014
|
+
const raw = await readFile7(pkgPath, "utf-8");
|
|
1802
2015
|
const pkg = JSON.parse(raw);
|
|
1803
2016
|
const sourcePath = relative(projectPath, pkgPath);
|
|
1804
2017
|
const depSections = [
|
|
@@ -1825,7 +2038,7 @@ async function scanAllDependencies(projectPath) {
|
|
|
1825
2038
|
}
|
|
1826
2039
|
return { dependencies };
|
|
1827
2040
|
}
|
|
1828
|
-
var PACKAGE_MAP, PACKAGE_PREFIX_MAP, CONFIG_FILE_MAP, SKIP_DIRS;
|
|
2041
|
+
var PACKAGE_MAP, PACKAGE_PREFIX_MAP, CONFIG_FILE_MAP, SYNTHETIC_CARRIER_NAME, CAPABILITY_BEARER_NAMES, JSX_TEST_PATTERN, JSX_SKIP_DIRS, JSX_SCAN_DIRS, SERVER_MAIN_ENTRIES, SERVER_FRAMEWORK_DEPS, compareByCategoryThenName, SKIP_DIRS;
|
|
1829
2042
|
var init_tech_detect = __esm({
|
|
1830
2043
|
"src/lib/tech-detect.ts"() {
|
|
1831
2044
|
"use strict";
|
|
@@ -1841,6 +2054,7 @@ var init_tech_detect = __esm({
|
|
|
1841
2054
|
svelte: { name: "Svelte", category: "framework" },
|
|
1842
2055
|
astro: { name: "Astro", category: "framework" },
|
|
1843
2056
|
"@angular/core": { name: "Angular", category: "framework" },
|
|
2057
|
+
"@nestjs/core": { name: "NestJS", category: "framework" },
|
|
1844
2058
|
// Libraries (UI)
|
|
1845
2059
|
react: { name: "React", category: "framework" },
|
|
1846
2060
|
vue: { name: "Vue", category: "framework" },
|
|
@@ -1955,6 +2169,47 @@ var init_tech_detect = __esm({
|
|
|
1955
2169
|
{ file: "nx.json", rule: { name: "Nx", category: "build" } },
|
|
1956
2170
|
{ file: "lerna.json", rule: { name: "Lerna", category: "build" } }
|
|
1957
2171
|
];
|
|
2172
|
+
SYNTHETIC_CARRIER_NAME = "__capabilities__";
|
|
2173
|
+
CAPABILITY_BEARER_NAMES = /* @__PURE__ */ new Set([
|
|
2174
|
+
"react",
|
|
2175
|
+
"next.js",
|
|
2176
|
+
"vue",
|
|
2177
|
+
"svelte",
|
|
2178
|
+
"solid",
|
|
2179
|
+
"preact",
|
|
2180
|
+
"remix",
|
|
2181
|
+
"astro",
|
|
2182
|
+
"angular",
|
|
2183
|
+
"nestjs",
|
|
2184
|
+
"nuxt",
|
|
2185
|
+
"gatsby",
|
|
2186
|
+
"express",
|
|
2187
|
+
"fastify",
|
|
2188
|
+
"hono",
|
|
2189
|
+
"react native",
|
|
2190
|
+
"expo"
|
|
2191
|
+
]);
|
|
2192
|
+
JSX_TEST_PATTERN = /\.(test|spec)\.(tsx|jsx)$/;
|
|
2193
|
+
JSX_SKIP_DIRS = /* @__PURE__ */ new Set(["__tests__", "test", "tests"]);
|
|
2194
|
+
JSX_SCAN_DIRS = ["src", "app", "pages"];
|
|
2195
|
+
SERVER_MAIN_ENTRIES = /* @__PURE__ */ new Set([
|
|
2196
|
+
"dist/main.js",
|
|
2197
|
+
"dist/server.js",
|
|
2198
|
+
"dist/index.js",
|
|
2199
|
+
"main.js",
|
|
2200
|
+
"server.js"
|
|
2201
|
+
]);
|
|
2202
|
+
SERVER_FRAMEWORK_DEPS = /* @__PURE__ */ new Set([
|
|
2203
|
+
"@nestjs/core",
|
|
2204
|
+
"express",
|
|
2205
|
+
"fastify",
|
|
2206
|
+
"hono"
|
|
2207
|
+
]);
|
|
2208
|
+
compareByCategoryThenName = (a, b) => {
|
|
2209
|
+
const catCmp = a.category.localeCompare(b.category);
|
|
2210
|
+
if (catCmp !== 0) return catCmp;
|
|
2211
|
+
return a.name.localeCompare(b.name);
|
|
2212
|
+
};
|
|
1958
2213
|
SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
1959
2214
|
"node_modules",
|
|
1960
2215
|
".next",
|
|
@@ -2005,14 +2260,14 @@ var init_server_detect = __esm({
|
|
|
2005
2260
|
});
|
|
2006
2261
|
|
|
2007
2262
|
// src/lib/port-verify.ts
|
|
2008
|
-
import { readFile as
|
|
2263
|
+
import { readFile as readFile8 } from "node:fs/promises";
|
|
2009
2264
|
async function verifyPorts(projectPath, portAllocations) {
|
|
2010
2265
|
const mismatches = [];
|
|
2011
2266
|
const allocatedPorts = new Set(portAllocations.map((a) => a.port));
|
|
2012
2267
|
const packageJsonPaths = await findPackageJsonFiles(projectPath, projectPath);
|
|
2013
2268
|
for (const pkgPath of packageJsonPaths) {
|
|
2014
2269
|
try {
|
|
2015
|
-
const raw = await
|
|
2270
|
+
const raw = await readFile8(pkgPath, "utf-8");
|
|
2016
2271
|
const pkg = JSON.parse(raw);
|
|
2017
2272
|
const scriptPort = detectPortFromScripts(pkg);
|
|
2018
2273
|
if (scriptPort !== null && !allocatedPorts.has(scriptPort)) {
|
|
@@ -2032,30 +2287,65 @@ async function verifyPorts(projectPath, portAllocations) {
|
|
|
2032
2287
|
}
|
|
2033
2288
|
return mismatches;
|
|
2034
2289
|
}
|
|
2290
|
+
function isDevServerScript(pkg) {
|
|
2291
|
+
const scripts = pkg.scripts;
|
|
2292
|
+
const raw = scripts?.dev;
|
|
2293
|
+
if (!raw || typeof raw !== "string") return false;
|
|
2294
|
+
const script = raw.trim().toLowerCase();
|
|
2295
|
+
if (!script) return false;
|
|
2296
|
+
for (const pattern of DEV_SERVER_BIN_PATTERNS) {
|
|
2297
|
+
if (pattern.test(script)) return true;
|
|
2298
|
+
}
|
|
2299
|
+
const tokens = script.split(/\s+/);
|
|
2300
|
+
for (const token of tokens) {
|
|
2301
|
+
if (token === "--port" || token === "-p") return true;
|
|
2302
|
+
if (token.startsWith("--port=")) return true;
|
|
2303
|
+
}
|
|
2304
|
+
return false;
|
|
2305
|
+
}
|
|
2306
|
+
function labelMatchesAppName(label, appName) {
|
|
2307
|
+
if (!label || !appName) return false;
|
|
2308
|
+
const normalize = (s) => s.toLowerCase().replace(/-/g, " ").replace(/[()]/g, " ").replace(/\s+/g, " ").trim();
|
|
2309
|
+
const labelTokens = normalize(label).split(" ").filter(Boolean);
|
|
2310
|
+
const appToken = normalize(appName);
|
|
2311
|
+
if (!appToken) return false;
|
|
2312
|
+
const appTokens = appToken.split(" ").filter(Boolean);
|
|
2313
|
+
if (appTokens.length === 1) {
|
|
2314
|
+
return labelTokens.includes(appTokens[0]);
|
|
2315
|
+
}
|
|
2316
|
+
for (let i = 0; i <= labelTokens.length - appTokens.length; i++) {
|
|
2317
|
+
if (appTokens.every((t, j) => labelTokens[i + j] === t)) return true;
|
|
2318
|
+
}
|
|
2319
|
+
return false;
|
|
2320
|
+
}
|
|
2035
2321
|
async function findUnallocatedApps(projectPath, portAllocations) {
|
|
2036
2322
|
const apps = await discoverMonorepoApps(projectPath);
|
|
2037
2323
|
if (apps.length === 0) {
|
|
2038
2324
|
return [];
|
|
2039
2325
|
}
|
|
2040
|
-
const allocatedLabels = new Set(portAllocations.map((a) => a.label));
|
|
2041
2326
|
const unallocated = [];
|
|
2042
2327
|
for (const app of apps) {
|
|
2043
|
-
if (
|
|
2328
|
+
if (portAllocations.some((a) => labelMatchesAppName(a.label ?? "", app.name))) {
|
|
2329
|
+
continue;
|
|
2330
|
+
}
|
|
2331
|
+
let pkg;
|
|
2044
2332
|
try {
|
|
2045
|
-
const raw = await
|
|
2046
|
-
|
|
2047
|
-
const framework = detectFramework(pkg);
|
|
2048
|
-
const detectedPort = detectPortFromScripts(pkg);
|
|
2049
|
-
const command = `pnpm --filter ${app.name} dev`;
|
|
2050
|
-
unallocated.push({
|
|
2051
|
-
name: app.name,
|
|
2052
|
-
path: app.path,
|
|
2053
|
-
framework,
|
|
2054
|
-
detectedPort,
|
|
2055
|
-
command
|
|
2056
|
-
});
|
|
2333
|
+
const raw = await readFile8(`${app.absPath}/package.json`, "utf-8");
|
|
2334
|
+
pkg = JSON.parse(raw);
|
|
2057
2335
|
} catch {
|
|
2336
|
+
continue;
|
|
2058
2337
|
}
|
|
2338
|
+
if (!isDevServerScript(pkg)) continue;
|
|
2339
|
+
const framework = detectFramework(pkg);
|
|
2340
|
+
const detectedPort = detectPortFromScripts(pkg);
|
|
2341
|
+
const command = `pnpm --filter ${app.name} dev`;
|
|
2342
|
+
unallocated.push({
|
|
2343
|
+
name: app.name,
|
|
2344
|
+
path: app.path,
|
|
2345
|
+
framework,
|
|
2346
|
+
detectedPort,
|
|
2347
|
+
command
|
|
2348
|
+
});
|
|
2059
2349
|
}
|
|
2060
2350
|
return unallocated;
|
|
2061
2351
|
}
|
|
@@ -2066,16 +2356,46 @@ function getAppLabel(relativePath) {
|
|
|
2066
2356
|
}
|
|
2067
2357
|
return "root";
|
|
2068
2358
|
}
|
|
2359
|
+
var DEV_SERVER_BIN_PATTERNS;
|
|
2069
2360
|
var init_port_verify = __esm({
|
|
2070
2361
|
"src/lib/port-verify.ts"() {
|
|
2071
2362
|
"use strict";
|
|
2072
2363
|
init_tech_detect();
|
|
2073
2364
|
init_server_detect();
|
|
2365
|
+
DEV_SERVER_BIN_PATTERNS = [
|
|
2366
|
+
/\bnext\s+dev\b/,
|
|
2367
|
+
/\bnest\s+start\b/,
|
|
2368
|
+
/\bvite\s+(?:dev|serve)\b/,
|
|
2369
|
+
/\bvite\s+preview\b/,
|
|
2370
|
+
/\bnuxt\s+dev\b/,
|
|
2371
|
+
/\b(?:svelte-kit|sveltekit)\s+dev\b/,
|
|
2372
|
+
/\bexpo\s+start\b/
|
|
2373
|
+
];
|
|
2074
2374
|
}
|
|
2075
2375
|
});
|
|
2076
2376
|
|
|
2077
2377
|
// src/lib/eslint-generator.ts
|
|
2078
2378
|
import { createHash } from "node:crypto";
|
|
2379
|
+
function importedIdentifiers(importLines) {
|
|
2380
|
+
const names = /* @__PURE__ */ new Set();
|
|
2381
|
+
for (const line of importLines) {
|
|
2382
|
+
let m = line.match(/^import\s+([A-Za-z_$][\w$]*)\s+from/);
|
|
2383
|
+
if (m) names.add(m[1]);
|
|
2384
|
+
m = line.match(/^import\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from/);
|
|
2385
|
+
if (m) names.add(m[1]);
|
|
2386
|
+
m = line.match(/^import\s*\{([^}]*)\}\s*from/);
|
|
2387
|
+
if (m) {
|
|
2388
|
+
for (const entry of m[1].split(",")) {
|
|
2389
|
+
const parts = entry.trim().split(/\s+as\s+/);
|
|
2390
|
+
const n = (parts[1] ?? parts[0]).trim();
|
|
2391
|
+
if (n) names.add(n);
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
m = line.match(/^const\s+([A-Za-z_$][\w$]*)\s*=\s*require/);
|
|
2395
|
+
if (m) names.add(m[1]);
|
|
2396
|
+
}
|
|
2397
|
+
return names;
|
|
2398
|
+
}
|
|
2079
2399
|
function parseFragment(fragment) {
|
|
2080
2400
|
if (!fragment) return { imports: [], configComments: [] };
|
|
2081
2401
|
const lines = fragment.split("\n");
|
|
@@ -2125,7 +2445,7 @@ function collectDependencies(presets) {
|
|
|
2125
2445
|
function hashConfig(content) {
|
|
2126
2446
|
return createHash("sha256").update(content).digest("hex");
|
|
2127
2447
|
}
|
|
2128
|
-
function buildRules(presets
|
|
2448
|
+
function buildRules(presets) {
|
|
2129
2449
|
const merged = {};
|
|
2130
2450
|
for (const preset of presets) {
|
|
2131
2451
|
const rules = preset.rules;
|
|
@@ -2133,11 +2453,30 @@ function buildRules(presets, userOverrides) {
|
|
|
2133
2453
|
Object.assign(merged, rules);
|
|
2134
2454
|
}
|
|
2135
2455
|
}
|
|
2136
|
-
if (userOverrides) {
|
|
2137
|
-
Object.assign(merged, userOverrides);
|
|
2138
|
-
}
|
|
2139
2456
|
return merged;
|
|
2140
2457
|
}
|
|
2458
|
+
function splitRulesByPlugin(rules, hasNextJs) {
|
|
2459
|
+
const reactHooks = {};
|
|
2460
|
+
const reactOrA11y = {};
|
|
2461
|
+
const importPlugin = {};
|
|
2462
|
+
const generic = {};
|
|
2463
|
+
for (const [key, value] of Object.entries(rules)) {
|
|
2464
|
+
if (key.startsWith("react-hooks/") || key.startsWith("react-compiler/")) {
|
|
2465
|
+
reactHooks[key] = value;
|
|
2466
|
+
} else if (key.startsWith("react/") || key.startsWith("jsx-a11y/")) {
|
|
2467
|
+
reactOrA11y[key] = value;
|
|
2468
|
+
} else if (key.startsWith("import/")) {
|
|
2469
|
+
if (hasNextJs) {
|
|
2470
|
+
importPlugin[key] = value;
|
|
2471
|
+
} else {
|
|
2472
|
+
generic[key] = value;
|
|
2473
|
+
}
|
|
2474
|
+
} else {
|
|
2475
|
+
generic[key] = value;
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
return { reactHooks, reactOrA11y, importPlugin, generic };
|
|
2479
|
+
}
|
|
2141
2480
|
function formatRules(rules, indent) {
|
|
2142
2481
|
const entries = Object.entries(rules);
|
|
2143
2482
|
if (entries.length === 0) return "{}";
|
|
@@ -2151,6 +2490,7 @@ ${indent}}`;
|
|
|
2151
2490
|
}
|
|
2152
2491
|
function generateEslintConfig(input) {
|
|
2153
2492
|
const { presets, ruleOverrides, tsconfigRootDir, ignorePatterns } = input;
|
|
2493
|
+
const hasNextJs = presets.some((p) => p.is_system && p.name === "nextjs");
|
|
2154
2494
|
const allImports = [];
|
|
2155
2495
|
const allConfigComments = [];
|
|
2156
2496
|
for (const preset of presets) {
|
|
@@ -2163,7 +2503,14 @@ function generateEslintConfig(input) {
|
|
|
2163
2503
|
const dedupedImports = deduplicateImports(allImports);
|
|
2164
2504
|
const tsCheck = dedupedImports.find((i) => i === "// @ts-check");
|
|
2165
2505
|
const importLines = dedupedImports.filter((i) => i !== "// @ts-check");
|
|
2166
|
-
const
|
|
2506
|
+
const presetRules = buildRules(presets);
|
|
2507
|
+
const userOverrides = ruleOverrides ?? {};
|
|
2508
|
+
const presetRulesClean = Object.fromEntries(
|
|
2509
|
+
Object.entries(presetRules).filter(
|
|
2510
|
+
([k]) => !Object.hasOwn(userOverrides, k)
|
|
2511
|
+
)
|
|
2512
|
+
);
|
|
2513
|
+
const splitRules = splitRulesByPlugin(presetRulesClean, hasNextJs);
|
|
2167
2514
|
const defaultIgnores = [
|
|
2168
2515
|
"eslint.config.mjs",
|
|
2169
2516
|
"node_modules/**",
|
|
@@ -2174,7 +2521,24 @@ function generateEslintConfig(input) {
|
|
|
2174
2521
|
const ignores = [.../* @__PURE__ */ new Set([...defaultIgnores, ...ignorePatterns ?? []])];
|
|
2175
2522
|
const rootDir = tsconfigRootDir ?? "import.meta.dirname";
|
|
2176
2523
|
const rootDirValue = rootDir === "import.meta.dirname" ? "import.meta.dirname" : `"${rootDir}"`;
|
|
2177
|
-
const
|
|
2524
|
+
const KNOWN_SYSTEM_PRESETS = [
|
|
2525
|
+
"base",
|
|
2526
|
+
"react",
|
|
2527
|
+
"nextjs",
|
|
2528
|
+
"node",
|
|
2529
|
+
"cli",
|
|
2530
|
+
"testing",
|
|
2531
|
+
"testing-react",
|
|
2532
|
+
"testing-e2e"
|
|
2533
|
+
];
|
|
2534
|
+
for (const preset of presets) {
|
|
2535
|
+
if (preset.is_system && !KNOWN_SYSTEM_PRESETS.includes(preset.name) && !warnedPresetNames.has(preset.name)) {
|
|
2536
|
+
console.warn(
|
|
2537
|
+
`Unknown system preset: ${preset.name} \u2014 skipping structural emission`
|
|
2538
|
+
);
|
|
2539
|
+
warnedPresetNames.add(preset.name);
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2178
2542
|
const hasReact = presets.some((p) => p.is_system && p.name === "react");
|
|
2179
2543
|
const hasNode = presets.some((p) => p.is_system && p.name === "node");
|
|
2180
2544
|
const hasTesting = presets.some((p) => p.is_system && p.name === "testing");
|
|
@@ -2204,8 +2568,9 @@ function generateEslintConfig(input) {
|
|
|
2204
2568
|
if ((hasNode || hasReact || hasNextJs) && !hasGlobalsImport) {
|
|
2205
2569
|
sections.push('import globals from "globals";');
|
|
2206
2570
|
}
|
|
2571
|
+
sections.push('import { defineConfig } from "eslint/config";');
|
|
2207
2572
|
sections.push("");
|
|
2208
|
-
sections.push("export default [");
|
|
2573
|
+
sections.push("export default defineConfig([");
|
|
2209
2574
|
sections.push(` { ignores: ${JSON.stringify(ignores)} },`);
|
|
2210
2575
|
sections.push("");
|
|
2211
2576
|
const hasBase = presets.some((p) => p.is_system && p.name === "base");
|
|
@@ -2226,11 +2591,43 @@ function generateEslintConfig(input) {
|
|
|
2226
2591
|
sections.push("");
|
|
2227
2592
|
}
|
|
2228
2593
|
if (hasNextJs) {
|
|
2594
|
+
const bindings = importedIdentifiers(importLines);
|
|
2595
|
+
const nextFusedRules = {
|
|
2596
|
+
...splitRules.reactHooks,
|
|
2597
|
+
...splitRules.importPlugin
|
|
2598
|
+
};
|
|
2229
2599
|
sections.push(" // Next.js: Core Web Vitals + TypeScript");
|
|
2230
2600
|
sections.push(" ...nextCoreWebVitals,");
|
|
2231
2601
|
sections.push(" ...nextTypescript,");
|
|
2232
|
-
|
|
2233
|
-
|
|
2602
|
+
if (bindings.has("jsxA11y")) {
|
|
2603
|
+
sections.push(" { rules: jsxA11y.flatConfigs.strict.rules },");
|
|
2604
|
+
} else {
|
|
2605
|
+
console.warn(
|
|
2606
|
+
"eslint-generator: skipping `jsxA11y.flatConfigs.strict.rules` emission \u2014 `jsxA11y` binding missing from nextjs preset config_fragment. See CHK-087 TASK-4/TASK-5."
|
|
2607
|
+
);
|
|
2608
|
+
}
|
|
2609
|
+
if (Object.keys(nextFusedRules).length > 0) {
|
|
2610
|
+
sections.push(" {");
|
|
2611
|
+
sections.push(' plugins: { "react-compiler": reactCompiler },');
|
|
2612
|
+
sections.push(` rules: ${formatRules(nextFusedRules, " ")},`);
|
|
2613
|
+
sections.push(" },");
|
|
2614
|
+
} else {
|
|
2615
|
+
sections.push(' { plugins: { "react-compiler": reactCompiler } },');
|
|
2616
|
+
}
|
|
2617
|
+
if (Object.keys(splitRules.reactOrA11y).length > 0) {
|
|
2618
|
+
if (bindings.has("react") && bindings.has("jsxA11y")) {
|
|
2619
|
+
sections.push(" {");
|
|
2620
|
+
sections.push(' plugins: { react, "jsx-a11y": jsxA11y },');
|
|
2621
|
+
sections.push(
|
|
2622
|
+
` rules: ${formatRules(splitRules.reactOrA11y, " ")},`
|
|
2623
|
+
);
|
|
2624
|
+
sections.push(" },");
|
|
2625
|
+
} else {
|
|
2626
|
+
console.warn(
|
|
2627
|
+
"eslint-generator: skipping defensive reactOrA11y block \u2014 requires both `react` and `jsxA11y` bindings from nextjs preset config_fragment. See CHK-087 TASK-4/TASK-5."
|
|
2628
|
+
);
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2234
2631
|
sections.push("");
|
|
2235
2632
|
}
|
|
2236
2633
|
if (hasReact && !hasNextJs) {
|
|
@@ -2250,6 +2647,12 @@ function generateEslintConfig(input) {
|
|
|
2250
2647
|
sections.push(" rules: {");
|
|
2251
2648
|
sections.push(" ...react.configs.flat.recommended.rules,");
|
|
2252
2649
|
sections.push(' ...react.configs.flat["jsx-runtime"].rules,');
|
|
2650
|
+
for (const [key, value] of Object.entries(splitRules.reactHooks)) {
|
|
2651
|
+
sections.push(` "${key}": ${JSON.stringify(value)},`);
|
|
2652
|
+
}
|
|
2653
|
+
for (const [key, value] of Object.entries(splitRules.reactOrA11y)) {
|
|
2654
|
+
sections.push(` "${key}": ${JSON.stringify(value)},`);
|
|
2655
|
+
}
|
|
2253
2656
|
sections.push(" },");
|
|
2254
2657
|
sections.push(" },");
|
|
2255
2658
|
sections.push(" jsxA11y.flatConfigs.strict,");
|
|
@@ -2270,10 +2673,14 @@ function generateEslintConfig(input) {
|
|
|
2270
2673
|
sections.push(" prettier,");
|
|
2271
2674
|
sections.push("");
|
|
2272
2675
|
}
|
|
2273
|
-
|
|
2676
|
+
const overridesCombined = {
|
|
2677
|
+
...splitRules.generic,
|
|
2678
|
+
...userOverrides
|
|
2679
|
+
};
|
|
2680
|
+
if (Object.keys(overridesCombined).length > 0) {
|
|
2274
2681
|
sections.push(" // Rule overrides");
|
|
2275
2682
|
sections.push(" {");
|
|
2276
|
-
sections.push(` rules: ${formatRules(
|
|
2683
|
+
sections.push(` rules: ${formatRules(overridesCombined, " ")},`);
|
|
2277
2684
|
sections.push(" },");
|
|
2278
2685
|
sections.push("");
|
|
2279
2686
|
}
|
|
@@ -2335,26 +2742,29 @@ function generateEslintConfig(input) {
|
|
|
2335
2742
|
sections.push(" },");
|
|
2336
2743
|
sections.push("");
|
|
2337
2744
|
}
|
|
2338
|
-
sections.push("];");
|
|
2745
|
+
sections.push("]);");
|
|
2339
2746
|
sections.push("");
|
|
2340
2747
|
return sections.join("\n");
|
|
2341
2748
|
}
|
|
2749
|
+
var warnedPresetNames;
|
|
2342
2750
|
var init_eslint_generator = __esm({
|
|
2343
2751
|
"src/lib/eslint-generator.ts"() {
|
|
2344
2752
|
"use strict";
|
|
2753
|
+
warnedPresetNames = /* @__PURE__ */ new Set();
|
|
2345
2754
|
}
|
|
2346
2755
|
});
|
|
2347
2756
|
|
|
2348
2757
|
// src/cli/eslint.ts
|
|
2349
2758
|
var eslint_exports = {};
|
|
2350
2759
|
__export(eslint_exports, {
|
|
2760
|
+
autoDetectIgnorePatterns: () => autoDetectIgnorePatterns,
|
|
2351
2761
|
checkEslintDrift: () => checkEslintDrift,
|
|
2352
2762
|
eslintInit: () => eslintInit,
|
|
2353
2763
|
eslintSync: () => eslintSync,
|
|
2354
2764
|
runEslint: () => runEslint
|
|
2355
2765
|
});
|
|
2356
|
-
import { readFile as
|
|
2357
|
-
import { join as
|
|
2766
|
+
import { readFile as readFile9, writeFile as writeFile4, access as access2, readdir as readdir5 } from "node:fs/promises";
|
|
2767
|
+
import { join as join8, relative as relative2 } from "node:path";
|
|
2358
2768
|
async function fileExists2(filePath) {
|
|
2359
2769
|
try {
|
|
2360
2770
|
await access2(filePath);
|
|
@@ -2363,16 +2773,56 @@ async function fileExists2(filePath) {
|
|
|
2363
2773
|
return false;
|
|
2364
2774
|
}
|
|
2365
2775
|
}
|
|
2776
|
+
async function autoDetectIgnorePatterns(absPath) {
|
|
2777
|
+
const patterns = [];
|
|
2778
|
+
if (await fileExists2(join8(absPath, "esbuild.js"))) {
|
|
2779
|
+
patterns.push("esbuild.js");
|
|
2780
|
+
}
|
|
2781
|
+
let entries = [];
|
|
2782
|
+
try {
|
|
2783
|
+
entries = await readdir5(absPath);
|
|
2784
|
+
} catch (err) {
|
|
2785
|
+
console.error(
|
|
2786
|
+
` autoDetectIgnorePatterns: failed to read ${absPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
2787
|
+
);
|
|
2788
|
+
entries = [];
|
|
2789
|
+
}
|
|
2790
|
+
const esbuildVariant = /^esbuild\.[^.]+\.(mjs|cjs|js)$/;
|
|
2791
|
+
for (const name of entries) {
|
|
2792
|
+
if (esbuildVariant.test(name)) {
|
|
2793
|
+
patterns.push(name);
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
for (const ext of ["ts", "mts", "js", "mjs"]) {
|
|
2797
|
+
const candidate = `vitest.config.${ext}`;
|
|
2798
|
+
if (await fileExists2(join8(absPath, candidate))) {
|
|
2799
|
+
patterns.push(candidate);
|
|
2800
|
+
break;
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
for (const ext of ["ts", "mts", "js", "mjs"]) {
|
|
2804
|
+
const candidate = `vite.config.${ext}`;
|
|
2805
|
+
if (await fileExists2(join8(absPath, candidate))) {
|
|
2806
|
+
patterns.push(candidate);
|
|
2807
|
+
break;
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
if (await fileExists2(join8(absPath, "tauri.conf.json"))) {
|
|
2811
|
+
patterns.push("src-tauri/**");
|
|
2812
|
+
patterns.push("**/*.d.ts");
|
|
2813
|
+
}
|
|
2814
|
+
return patterns;
|
|
2815
|
+
}
|
|
2366
2816
|
function detectPackageManager(projectPath) {
|
|
2367
2817
|
return (async () => {
|
|
2368
|
-
if (await fileExists2(
|
|
2369
|
-
if (await fileExists2(
|
|
2818
|
+
if (await fileExists2(join8(projectPath, "pnpm-lock.yaml"))) return "pnpm";
|
|
2819
|
+
if (await fileExists2(join8(projectPath, "yarn.lock"))) return "yarn";
|
|
2370
2820
|
return "npm";
|
|
2371
2821
|
})();
|
|
2372
2822
|
}
|
|
2373
2823
|
async function getInstalledDeps(pkgJsonPath) {
|
|
2374
2824
|
try {
|
|
2375
|
-
const raw = await
|
|
2825
|
+
const raw = await readFile9(pkgJsonPath, "utf-8");
|
|
2376
2826
|
const pkg = JSON.parse(raw);
|
|
2377
2827
|
const all = /* @__PURE__ */ new Set();
|
|
2378
2828
|
for (const name of Object.keys(pkg.dependencies ?? {})) all.add(name);
|
|
@@ -2393,11 +2843,13 @@ function buildInstallCommand(pm, packages, workspaceRoot) {
|
|
|
2393
2843
|
return `npm install -D ${pkgStr}`;
|
|
2394
2844
|
}
|
|
2395
2845
|
}
|
|
2396
|
-
async function resolvePresetsForTechStack(techNames) {
|
|
2846
|
+
async function resolvePresetsForTechStack(techNames, capabilities = []) {
|
|
2397
2847
|
const techParam = techNames.join(",");
|
|
2398
|
-
const
|
|
2399
|
-
|
|
2400
|
-
|
|
2848
|
+
const query = { tech_stack: techParam };
|
|
2849
|
+
if (capabilities.length > 0) {
|
|
2850
|
+
query.capabilities = capabilities.join(",");
|
|
2851
|
+
}
|
|
2852
|
+
const res = await apiGet("/eslint-presets", query);
|
|
2401
2853
|
return res.data ?? [];
|
|
2402
2854
|
}
|
|
2403
2855
|
async function eslintInit(repoId, projectPath) {
|
|
@@ -2432,19 +2884,27 @@ async function eslintInit(repoId, projectPath) {
|
|
|
2432
2884
|
const allRequiredDeps = /* @__PURE__ */ new Map();
|
|
2433
2885
|
const configsToWrite = [];
|
|
2434
2886
|
for (const target of targets) {
|
|
2435
|
-
const techNames = target.techStack.map((t) => t.name);
|
|
2887
|
+
const techNames = target.techStack.map((t) => t.name).filter((n) => n !== SYNTHETIC_CARRIER_NAME);
|
|
2888
|
+
const capabilities = collectCapabilities(target.techStack);
|
|
2436
2889
|
console.log(
|
|
2437
2890
|
` ${target.name}: ${techNames.length > 0 ? techNames.join(", ") : "(no tech detected)"}`
|
|
2438
2891
|
);
|
|
2439
|
-
if (
|
|
2440
|
-
console.log(`
|
|
2441
|
-
|
|
2892
|
+
if (capabilities.length > 0) {
|
|
2893
|
+
console.log(` Capabilities: ${capabilities.join(", ")}`);
|
|
2894
|
+
}
|
|
2895
|
+
if (techNames.length === 0 && capabilities.length === 0) {
|
|
2896
|
+
console.log(
|
|
2897
|
+
` Skipping ${target.name}: no tech or capabilities detected
|
|
2898
|
+
`
|
|
2899
|
+
);
|
|
2442
2900
|
continue;
|
|
2443
2901
|
}
|
|
2444
|
-
const presets = await resolvePresetsForTechStack(techNames);
|
|
2902
|
+
const presets = await resolvePresetsForTechStack(techNames, capabilities);
|
|
2445
2903
|
if (presets.length === 0) {
|
|
2446
|
-
console.log(
|
|
2447
|
-
`)
|
|
2904
|
+
console.log(
|
|
2905
|
+
` No preset matches ${target.name}; skipping (override via --force-preset if needed)
|
|
2906
|
+
`
|
|
2907
|
+
);
|
|
2448
2908
|
continue;
|
|
2449
2909
|
}
|
|
2450
2910
|
console.log(` Presets: ${presets.map((p) => p.name).join(", ")}`);
|
|
@@ -2465,12 +2925,17 @@ async function eslintInit(repoId, projectPath) {
|
|
|
2465
2925
|
}
|
|
2466
2926
|
} catch {
|
|
2467
2927
|
}
|
|
2928
|
+
const detectedIgnores = await autoDetectIgnorePatterns(target.absPath);
|
|
2929
|
+
if (detectedIgnores.length > 0) {
|
|
2930
|
+
console.log(` Auto-ignore: ${detectedIgnores.join(", ")}`);
|
|
2931
|
+
}
|
|
2468
2932
|
const content = generateEslintConfig({
|
|
2469
2933
|
presets,
|
|
2470
|
-
ruleOverrides: userOverrides
|
|
2934
|
+
ruleOverrides: userOverrides,
|
|
2935
|
+
ignorePatterns: detectedIgnores
|
|
2471
2936
|
});
|
|
2472
2937
|
const hash = hashConfig(content);
|
|
2473
|
-
const configPath =
|
|
2938
|
+
const configPath = join8(target.absPath, "eslint.config.mjs");
|
|
2474
2939
|
configsToWrite.push({
|
|
2475
2940
|
target,
|
|
2476
2941
|
presets,
|
|
@@ -2492,11 +2957,11 @@ async function eslintInit(repoId, projectPath) {
|
|
|
2492
2957
|
return;
|
|
2493
2958
|
}
|
|
2494
2959
|
const pm = await detectPackageManager(projectPath);
|
|
2495
|
-
const rootPkgJsonPath =
|
|
2960
|
+
const rootPkgJsonPath = join8(projectPath, "package.json");
|
|
2496
2961
|
const installed = await getInstalledDeps(rootPkgJsonPath);
|
|
2497
2962
|
if (isMonorepo) {
|
|
2498
2963
|
for (const { target } of configsToWrite) {
|
|
2499
|
-
const appPkgJson =
|
|
2964
|
+
const appPkgJson = join8(target.absPath, "package.json");
|
|
2500
2965
|
const appDeps = await getInstalledDeps(appPkgJson);
|
|
2501
2966
|
for (const dep of appDeps) {
|
|
2502
2967
|
installed.add(dep);
|
|
@@ -2548,7 +3013,7 @@ async function eslintInit(repoId, projectPath) {
|
|
|
2548
3013
|
} of configsToWrite) {
|
|
2549
3014
|
if (await fileExists2(configPath)) {
|
|
2550
3015
|
try {
|
|
2551
|
-
const existing = await
|
|
3016
|
+
const existing = await readFile9(configPath, "utf-8");
|
|
2552
3017
|
const existingHash = hashConfig(existing);
|
|
2553
3018
|
if (existingHash === hash) {
|
|
2554
3019
|
console.log(
|
|
@@ -2568,7 +3033,7 @@ async function eslintInit(repoId, projectPath) {
|
|
|
2568
3033
|
}
|
|
2569
3034
|
}
|
|
2570
3035
|
try {
|
|
2571
|
-
await
|
|
3036
|
+
await writeFile4(configPath, content, "utf-8");
|
|
2572
3037
|
} catch (err) {
|
|
2573
3038
|
console.error(
|
|
2574
3039
|
` ${target.name}: Failed to write config: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -2618,18 +3083,22 @@ async function eslintSync(repoId, projectPath) {
|
|
|
2618
3083
|
let skippedCount = 0;
|
|
2619
3084
|
let driftCount = 0;
|
|
2620
3085
|
for (const config of configs) {
|
|
2621
|
-
const absPath = config.source_path === "." ? projectPath :
|
|
2622
|
-
const configPath =
|
|
3086
|
+
const absPath = config.source_path === "." ? projectPath : join8(projectPath, config.source_path);
|
|
3087
|
+
const configPath = join8(absPath, "eslint.config.mjs");
|
|
2623
3088
|
const detected = await detectTechStack(absPath);
|
|
2624
|
-
const techNames = detected.flat.map((t) => t.name);
|
|
2625
|
-
const
|
|
3089
|
+
const techNames = detected.flat.map((t) => t.name).filter((n) => n !== SYNTHETIC_CARRIER_NAME);
|
|
3090
|
+
const capabilities = collectCapabilities(detected.flat);
|
|
3091
|
+
const currentPresets = await resolvePresetsForTechStack(
|
|
3092
|
+
techNames,
|
|
3093
|
+
capabilities
|
|
3094
|
+
);
|
|
2626
3095
|
const currentPresetIds = currentPresets.map((p) => p.id).sort();
|
|
2627
3096
|
const savedPresetIds = [...config.active_preset_ids ?? []].sort();
|
|
2628
3097
|
const presetsChanged = currentPresetIds.length !== savedPresetIds.length || currentPresetIds.some((id) => !savedPresetIds.includes(id));
|
|
2629
3098
|
if (!presetsChanged) {
|
|
2630
3099
|
if (await fileExists2(configPath)) {
|
|
2631
3100
|
try {
|
|
2632
|
-
const currentContent = await
|
|
3101
|
+
const currentContent = await readFile9(configPath, "utf-8");
|
|
2633
3102
|
const currentHash = hashConfig(currentContent);
|
|
2634
3103
|
if (config.generated_hash && currentHash !== config.generated_hash) {
|
|
2635
3104
|
console.log(
|
|
@@ -2651,14 +3120,18 @@ async function eslintSync(repoId, projectPath) {
|
|
|
2651
3120
|
);
|
|
2652
3121
|
}
|
|
2653
3122
|
}
|
|
2654
|
-
|
|
3123
|
+
if (presetsChanged) {
|
|
3124
|
+
console.log(` ${config.source_path}: presets changed, regenerating...`);
|
|
3125
|
+
}
|
|
2655
3126
|
const userOverrides = config.rule_overrides;
|
|
3127
|
+
const detectedIgnores = await autoDetectIgnorePatterns(absPath);
|
|
2656
3128
|
const content = generateEslintConfig({
|
|
2657
3129
|
presets: currentPresets,
|
|
2658
|
-
ruleOverrides: userOverrides && Object.keys(userOverrides).length > 0 ? userOverrides : void 0
|
|
3130
|
+
ruleOverrides: userOverrides && Object.keys(userOverrides).length > 0 ? userOverrides : void 0,
|
|
3131
|
+
ignorePatterns: detectedIgnores
|
|
2659
3132
|
});
|
|
2660
3133
|
try {
|
|
2661
|
-
await
|
|
3134
|
+
await writeFile4(configPath, content, "utf-8");
|
|
2662
3135
|
} catch (err) {
|
|
2663
3136
|
console.error(
|
|
2664
3137
|
` ${config.source_path}: Failed to write config: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -2694,11 +3167,11 @@ async function checkEslintDrift(repoId, projectPath) {
|
|
|
2694
3167
|
const configs = res.data ?? [];
|
|
2695
3168
|
for (const config of configs) {
|
|
2696
3169
|
if (!config.generated_hash) continue;
|
|
2697
|
-
const absPath = config.source_path === "." ? projectPath :
|
|
2698
|
-
const configPath =
|
|
3170
|
+
const absPath = config.source_path === "." ? projectPath : join8(projectPath, config.source_path);
|
|
3171
|
+
const configPath = join8(absPath, "eslint.config.mjs");
|
|
2699
3172
|
if (!await fileExists2(configPath)) continue;
|
|
2700
3173
|
try {
|
|
2701
|
-
const content = await
|
|
3174
|
+
const content = await readFile9(configPath, "utf-8");
|
|
2702
3175
|
const currentHash = hashConfig(content);
|
|
2703
3176
|
if (currentHash !== config.generated_hash) {
|
|
2704
3177
|
return true;
|
|
@@ -2750,8 +3223,8 @@ __export(sync_exports, {
|
|
|
2750
3223
|
runSync: () => runSync
|
|
2751
3224
|
});
|
|
2752
3225
|
import { createHash as createHash2 } from "node:crypto";
|
|
2753
|
-
import { readFile as
|
|
2754
|
-
import { join as
|
|
3226
|
+
import { readFile as readFile10, writeFile as writeFile5, mkdir as mkdir2, chmod as chmod2, unlink as unlink2 } from "node:fs/promises";
|
|
3227
|
+
import { join as join9, dirname as dirname2 } from "node:path";
|
|
2755
3228
|
function contentHash(content) {
|
|
2756
3229
|
return createHash2("sha256").update(content).digest("hex");
|
|
2757
3230
|
}
|
|
@@ -2819,11 +3292,14 @@ async function runSync() {
|
|
|
2819
3292
|
}
|
|
2820
3293
|
async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
|
|
2821
3294
|
console.log(" Reading local and remote state...");
|
|
2822
|
-
const claudeDir =
|
|
3295
|
+
const claudeDir = join9(projectPath, ".claude");
|
|
2823
3296
|
let localFiles = /* @__PURE__ */ new Map();
|
|
2824
3297
|
try {
|
|
2825
3298
|
localFiles = await scanLocalFiles(claudeDir, projectPath);
|
|
2826
|
-
} catch {
|
|
3299
|
+
} catch (err) {
|
|
3300
|
+
console.warn(
|
|
3301
|
+
` Local file scan incomplete: ${err instanceof Error ? err.message : String(err)}`
|
|
3302
|
+
);
|
|
2827
3303
|
}
|
|
2828
3304
|
const [defaultsRes, repoSyncRes, repoRes, , fileReposRes] = await Promise.all(
|
|
2829
3305
|
[
|
|
@@ -3051,7 +3527,7 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
|
|
|
3051
3527
|
for (const p of toPull) {
|
|
3052
3528
|
if (p.filePath && p.remoteContent !== null) {
|
|
3053
3529
|
await mkdir2(dirname2(p.filePath), { recursive: true });
|
|
3054
|
-
await
|
|
3530
|
+
await writeFile5(p.filePath, p.remoteContent, "utf-8");
|
|
3055
3531
|
if (p.isHook) await chmod2(p.filePath, 493);
|
|
3056
3532
|
}
|
|
3057
3533
|
}
|
|
@@ -3142,7 +3618,10 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
|
|
|
3142
3618
|
repo_id: repoId,
|
|
3143
3619
|
file_repos: fileRepoUpdates
|
|
3144
3620
|
});
|
|
3145
|
-
} catch {
|
|
3621
|
+
} catch (err) {
|
|
3622
|
+
console.warn(
|
|
3623
|
+
` Warning: failed to update file-repo tracking for ${fileRepoUpdates.length} files: ${err instanceof Error ? err.message : String(err)}`
|
|
3624
|
+
);
|
|
3146
3625
|
}
|
|
3147
3626
|
}
|
|
3148
3627
|
console.log(
|
|
@@ -3202,7 +3681,7 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
|
|
|
3202
3681
|
console.log("\n Sync complete.\n");
|
|
3203
3682
|
}
|
|
3204
3683
|
async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun) {
|
|
3205
|
-
const settingsPath =
|
|
3684
|
+
const settingsPath = join9(claudeDir, "settings.json");
|
|
3206
3685
|
const globalSettingsFiles = syncData.global_settings ?? [];
|
|
3207
3686
|
let globalSettings = {};
|
|
3208
3687
|
for (const gf of globalSettingsFiles) {
|
|
@@ -3222,11 +3701,11 @@ async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun)
|
|
|
3222
3701
|
globalSettings,
|
|
3223
3702
|
repoSettings
|
|
3224
3703
|
);
|
|
3225
|
-
const hooksDir =
|
|
3704
|
+
const hooksDir = join9(projectPath, ".claude", "hooks");
|
|
3226
3705
|
const discovered = await discoverHooks(hooksDir);
|
|
3227
3706
|
let localSettings = {};
|
|
3228
3707
|
try {
|
|
3229
|
-
const raw = await
|
|
3708
|
+
const raw = await readFile10(settingsPath, "utf-8");
|
|
3230
3709
|
localSettings = JSON.parse(raw);
|
|
3231
3710
|
} catch {
|
|
3232
3711
|
}
|
|
@@ -3241,7 +3720,7 @@ async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun)
|
|
|
3241
3720
|
const mergedContent = JSON.stringify(merged, null, 2) + "\n";
|
|
3242
3721
|
let currentContent = "";
|
|
3243
3722
|
try {
|
|
3244
|
-
currentContent = await
|
|
3723
|
+
currentContent = await readFile10(settingsPath, "utf-8");
|
|
3245
3724
|
} catch {
|
|
3246
3725
|
}
|
|
3247
3726
|
if (currentContent === mergedContent) {
|
|
@@ -3253,18 +3732,30 @@ async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun)
|
|
|
3253
3732
|
return;
|
|
3254
3733
|
}
|
|
3255
3734
|
await mkdir2(dirname2(settingsPath), { recursive: true });
|
|
3256
|
-
await
|
|
3735
|
+
await writeFile5(settingsPath, mergedContent, "utf-8");
|
|
3257
3736
|
console.log(" Updated settings.json");
|
|
3258
3737
|
}
|
|
3259
3738
|
async function syncConfig(repoId, projectPath, dryRun) {
|
|
3260
|
-
const configPath =
|
|
3739
|
+
const configPath = join9(projectPath, ".codebyplan.json");
|
|
3261
3740
|
let currentConfig = {};
|
|
3262
3741
|
try {
|
|
3263
|
-
const raw = await
|
|
3742
|
+
const raw = await readFile10(configPath, "utf-8");
|
|
3264
3743
|
currentConfig = JSON.parse(raw);
|
|
3265
3744
|
} catch {
|
|
3266
3745
|
currentConfig = { repo_id: repoId };
|
|
3267
3746
|
}
|
|
3747
|
+
let resolvedWorktreeId;
|
|
3748
|
+
try {
|
|
3749
|
+
resolvedWorktreeId = await resolveAndCacheWorktreeId(repoId, projectPath);
|
|
3750
|
+
} catch (err) {
|
|
3751
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3752
|
+
console.warn(
|
|
3753
|
+
` Warning: failed to cache worktree_id (self-heal skipped): ${msg}`
|
|
3754
|
+
);
|
|
3755
|
+
}
|
|
3756
|
+
if (resolvedWorktreeId && currentConfig.worktree_id !== resolvedWorktreeId) {
|
|
3757
|
+
currentConfig = { ...currentConfig, worktree_id: resolvedWorktreeId };
|
|
3758
|
+
}
|
|
3268
3759
|
const repoRes = await apiGet(`/repos/${repoId}`);
|
|
3269
3760
|
const repo = repoRes.data;
|
|
3270
3761
|
let portAllocations = [];
|
|
@@ -3298,7 +3789,10 @@ async function syncConfig(repoId, projectPath, dryRun) {
|
|
|
3298
3789
|
}
|
|
3299
3790
|
return clean;
|
|
3300
3791
|
});
|
|
3301
|
-
} catch {
|
|
3792
|
+
} catch (err) {
|
|
3793
|
+
console.warn(
|
|
3794
|
+
` Warning: failed to fetch port allocations: ${err instanceof Error ? err.message : String(err)}`
|
|
3795
|
+
);
|
|
3302
3796
|
}
|
|
3303
3797
|
const worktreeId = currentConfig.worktree_id;
|
|
3304
3798
|
const matchingAlloc = portAllocations[0];
|
|
@@ -3329,7 +3823,7 @@ async function syncConfig(repoId, projectPath, dryRun) {
|
|
|
3329
3823
|
console.log(" Config would be updated (dry-run).");
|
|
3330
3824
|
return;
|
|
3331
3825
|
}
|
|
3332
|
-
await
|
|
3826
|
+
await writeFile5(configPath, newJson + "\n", "utf-8");
|
|
3333
3827
|
console.log(" Updated .codebyplan.json");
|
|
3334
3828
|
}
|
|
3335
3829
|
async function syncTechStack(repoId, projectPath, dryRun) {
|
|
@@ -3363,8 +3857,10 @@ async function syncTechStack(repoId, projectPath, dryRun) {
|
|
|
3363
3857
|
}
|
|
3364
3858
|
}
|
|
3365
3859
|
}
|
|
3366
|
-
} catch {
|
|
3367
|
-
console.
|
|
3860
|
+
} catch (err) {
|
|
3861
|
+
console.warn(
|
|
3862
|
+
` Tech stack detection skipped: ${err instanceof Error ? err.message : String(err)}`
|
|
3863
|
+
);
|
|
3368
3864
|
}
|
|
3369
3865
|
}
|
|
3370
3866
|
async function syncEslintDriftCheck(repoId, projectPath) {
|
|
@@ -3442,8 +3938,10 @@ async function syncPortVerification(repoId, projectPath, dryRun, fix) {
|
|
|
3442
3938
|
if (mismatches.length === 0 && unallocated.length === 0) {
|
|
3443
3939
|
console.log(" Ports verified.");
|
|
3444
3940
|
}
|
|
3445
|
-
} catch {
|
|
3446
|
-
console.
|
|
3941
|
+
} catch (err) {
|
|
3942
|
+
console.warn(
|
|
3943
|
+
` Port verification skipped: ${err instanceof Error ? err.message : String(err)}`
|
|
3944
|
+
);
|
|
3447
3945
|
}
|
|
3448
3946
|
}
|
|
3449
3947
|
function groupByType(items) {
|
|
@@ -3477,28 +3975,28 @@ function getLocalFilePath(claudeDir, projectPath, remote) {
|
|
|
3477
3975
|
hook: { dir: "hooks", ext: ".sh" },
|
|
3478
3976
|
template: { dir: "templates", ext: "" },
|
|
3479
3977
|
context: { dir: "context", ext: ".md" },
|
|
3480
|
-
docs_stack: { dir:
|
|
3978
|
+
docs_stack: { dir: join9("docs", "stack"), ext: ".md" },
|
|
3481
3979
|
docs: { dir: "docs", ext: ".md" },
|
|
3482
3980
|
claude_md: { dir: "", ext: "" },
|
|
3483
3981
|
settings: { dir: "", ext: "" }
|
|
3484
3982
|
};
|
|
3485
|
-
if (remote.type === "claude_md") return
|
|
3486
|
-
if (remote.type === "settings") return
|
|
3983
|
+
if (remote.type === "claude_md") return join9(projectPath, "CLAUDE.md");
|
|
3984
|
+
if (remote.type === "settings") return join9(claudeDir, "settings.json");
|
|
3487
3985
|
const cfg = typeConfig2[remote.type];
|
|
3488
|
-
if (!cfg) return
|
|
3489
|
-
const typeDir = remote.type === "command" ?
|
|
3986
|
+
if (!cfg) return join9(claudeDir, remote.name);
|
|
3987
|
+
const typeDir = remote.type === "command" ? join9(claudeDir, cfg.dir, "cbp") : join9(claudeDir, cfg.dir);
|
|
3490
3988
|
if (cfg.subfolder)
|
|
3491
|
-
return
|
|
3989
|
+
return join9(typeDir, remote.name, `${cfg.subfolder}${cfg.ext}`);
|
|
3492
3990
|
if (remote.type === "command" && remote.category)
|
|
3493
|
-
return
|
|
3494
|
-
if (remote.type === "template") return
|
|
3991
|
+
return join9(typeDir, remote.category, `${remote.name}${cfg.ext}`);
|
|
3992
|
+
if (remote.type === "template") return join9(typeDir, remote.name);
|
|
3495
3993
|
if (remote.category && (remote.type === "context" || remote.type === "docs_stack" || remote.type === "docs"))
|
|
3496
|
-
return
|
|
3497
|
-
return
|
|
3994
|
+
return join9(typeDir, remote.category, `${remote.name}${cfg.ext}`);
|
|
3995
|
+
return join9(typeDir, `${remote.name}${cfg.ext}`);
|
|
3498
3996
|
}
|
|
3499
3997
|
function getSyncVersion() {
|
|
3500
3998
|
try {
|
|
3501
|
-
return "1.4.
|
|
3999
|
+
return "1.4.3";
|
|
3502
4000
|
} catch {
|
|
3503
4001
|
return "unknown";
|
|
3504
4002
|
}
|
|
@@ -3547,6 +4045,7 @@ var init_sync = __esm({
|
|
|
3547
4045
|
init_settings_merge();
|
|
3548
4046
|
init_hook_registry();
|
|
3549
4047
|
init_port_verify();
|
|
4048
|
+
init_resolve_worktree();
|
|
3550
4049
|
init_eslint();
|
|
3551
4050
|
}
|
|
3552
4051
|
});
|
|
@@ -3554,68 +4053,69 @@ var init_sync = __esm({
|
|
|
3554
4053
|
// src/index.ts
|
|
3555
4054
|
init_version();
|
|
3556
4055
|
import { readFileSync } from "node:fs";
|
|
3557
|
-
import { resolve } from "node:path";
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
const
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
4056
|
+
import { resolve as resolve2 } from "node:path";
|
|
4057
|
+
void (async () => {
|
|
4058
|
+
if (!process.env.CODEBYPLAN_API_KEY) {
|
|
4059
|
+
try {
|
|
4060
|
+
const envPath = resolve2(process.cwd(), ".env.local");
|
|
4061
|
+
const content = readFileSync(envPath, "utf-8");
|
|
4062
|
+
for (const line of content.split("\n")) {
|
|
4063
|
+
const trimmed = line.trim();
|
|
4064
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
4065
|
+
const eq = trimmed.indexOf("=");
|
|
4066
|
+
if (eq === -1) continue;
|
|
4067
|
+
const key = trimmed.slice(0, eq).trim();
|
|
4068
|
+
const val = trimmed.slice(eq + 1).trim().replace(/^["']|["']$/g, "");
|
|
4069
|
+
if (!process.env[key]) process.env[key] = val;
|
|
4070
|
+
}
|
|
4071
|
+
} catch {
|
|
3570
4072
|
}
|
|
3571
|
-
} catch {
|
|
3572
4073
|
}
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
4074
|
+
if (process.env.CODEBYPLAN_API_KEY?.startsWith("CODEBYPLAN_API_KEY=")) {
|
|
4075
|
+
process.env.CODEBYPLAN_API_KEY = process.env.CODEBYPLAN_API_KEY.slice(
|
|
4076
|
+
"CODEBYPLAN_API_KEY=".length
|
|
4077
|
+
);
|
|
4078
|
+
}
|
|
4079
|
+
const arg = process.argv[2];
|
|
4080
|
+
if (arg === "--version" || arg === "-v") {
|
|
4081
|
+
console.log(VERSION);
|
|
4082
|
+
process.exit(0);
|
|
4083
|
+
}
|
|
4084
|
+
if (arg === "setup") {
|
|
4085
|
+
const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
|
|
4086
|
+
await runSetup2();
|
|
4087
|
+
process.exit(0);
|
|
4088
|
+
}
|
|
4089
|
+
if (arg === "sync") {
|
|
4090
|
+
const { runSync: runSync2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
4091
|
+
const { SyncCancelledError: SyncCancelledError2 } = await Promise.resolve().then(() => (init_confirm(), confirm_exports));
|
|
4092
|
+
try {
|
|
4093
|
+
await runSync2();
|
|
4094
|
+
} catch (err) {
|
|
4095
|
+
if (err instanceof SyncCancelledError2) {
|
|
4096
|
+
console.log("\n Sync cancelled.\n");
|
|
4097
|
+
process.exit(0);
|
|
4098
|
+
}
|
|
4099
|
+
throw err;
|
|
3598
4100
|
}
|
|
3599
|
-
|
|
4101
|
+
process.exit(0);
|
|
3600
4102
|
}
|
|
3601
|
-
|
|
3602
|
-
}
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
4103
|
+
if (arg === "eslint") {
|
|
4104
|
+
const { runEslint: runEslint2 } = await Promise.resolve().then(() => (init_eslint(), eslint_exports));
|
|
4105
|
+
const { SyncCancelledError: SyncCancelledError2 } = await Promise.resolve().then(() => (init_confirm(), confirm_exports));
|
|
4106
|
+
try {
|
|
4107
|
+
await runEslint2();
|
|
4108
|
+
} catch (err) {
|
|
4109
|
+
if (err instanceof SyncCancelledError2) {
|
|
4110
|
+
console.log("\n Cancelled.\n");
|
|
4111
|
+
process.exit(0);
|
|
4112
|
+
}
|
|
4113
|
+
throw err;
|
|
3612
4114
|
}
|
|
3613
|
-
|
|
4115
|
+
process.exit(0);
|
|
3614
4116
|
}
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
if (arg === "help" || arg === "--help" || arg === "-h" || arg === void 0) {
|
|
3618
|
-
console.log(`
|
|
4117
|
+
if (arg === "help" || arg === "--help" || arg === "-h" || arg === void 0) {
|
|
4118
|
+
console.log(`
|
|
3619
4119
|
CodeByPlan CLI v${VERSION}
|
|
3620
4120
|
|
|
3621
4121
|
Usage:
|
|
@@ -3638,13 +4138,14 @@ if (arg === "help" || arg === "--help" || arg === "-h" || arg === void 0) {
|
|
|
3638
4138
|
|
|
3639
4139
|
MCP Server:
|
|
3640
4140
|
Claude Code connects to CodeByPlan via remote MCP:
|
|
3641
|
-
URL: https://codebyplan.com/mcp
|
|
4141
|
+
URL: https://www.codebyplan.com/mcp
|
|
3642
4142
|
Auth: x-api-key header (configured during setup)
|
|
3643
4143
|
|
|
3644
4144
|
Learn more: https://codebyplan.com
|
|
3645
4145
|
`);
|
|
3646
|
-
|
|
3647
|
-
}
|
|
3648
|
-
console.error(`Unknown command: ${arg}`);
|
|
3649
|
-
console.error("Run 'codebyplan help' for usage.");
|
|
3650
|
-
process.exit(1);
|
|
4146
|
+
process.exit(0);
|
|
4147
|
+
}
|
|
4148
|
+
console.error(`Unknown command: ${arg}`);
|
|
4149
|
+
console.error("Run 'codebyplan help' for usage.");
|
|
4150
|
+
process.exit(1);
|
|
4151
|
+
})();
|