codebyplan 1.5.0 → 1.5.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 +48 -5
- package/dist/cli.js +542 -2387
- package/package.json +1 -1
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.5.
|
|
17
|
+
VERSION = "1.5.1";
|
|
18
18
|
PACKAGE_NAME = "codebyplan";
|
|
19
19
|
}
|
|
20
20
|
});
|
|
@@ -117,9 +117,6 @@ async function apiPost(path, body) {
|
|
|
117
117
|
async function apiPut(path, body) {
|
|
118
118
|
return request("PUT", path, { body });
|
|
119
119
|
}
|
|
120
|
-
async function apiDelete(path, params) {
|
|
121
|
-
await request("DELETE", path, { params });
|
|
122
|
-
}
|
|
123
120
|
var API_KEY, BASE_URL, REQUEST_TIMEOUT_MS, MAX_RETRIES, BASE_DELAY_MS, ApiError;
|
|
124
121
|
var init_api = __esm({
|
|
125
122
|
"src/lib/api.ts"() {
|
|
@@ -296,633 +293,6 @@ var init_local_config = __esm({
|
|
|
296
293
|
}
|
|
297
294
|
});
|
|
298
295
|
|
|
299
|
-
// src/lib/settings-merge.ts
|
|
300
|
-
function mergeSettings(template, local) {
|
|
301
|
-
const merged = { ...local };
|
|
302
|
-
for (const key of TEMPLATE_MANAGED_KEYS) {
|
|
303
|
-
if (key in template) {
|
|
304
|
-
merged[key] = template[key];
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
if (template.permissions && typeof template.permissions === "object") {
|
|
308
|
-
const templatePerms = template.permissions;
|
|
309
|
-
const localPerms = local.permissions && typeof local.permissions === "object" ? local.permissions : {};
|
|
310
|
-
const mergedPerms = { ...localPerms };
|
|
311
|
-
for (const key of TEMPLATE_MANAGED_PERMISSION_KEYS) {
|
|
312
|
-
if (key in templatePerms) {
|
|
313
|
-
mergedPerms[key] = templatePerms[key];
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
merged.permissions = mergedPerms;
|
|
317
|
-
}
|
|
318
|
-
return merged;
|
|
319
|
-
}
|
|
320
|
-
function mergeGlobalAndRepoSettings(global, repo) {
|
|
321
|
-
const merged = { ...global, ...repo };
|
|
322
|
-
const globalPerms = global.permissions && typeof global.permissions === "object" ? global.permissions : {};
|
|
323
|
-
const repoPerms = repo.permissions && typeof repo.permissions === "object" ? repo.permissions : {};
|
|
324
|
-
if (Object.keys(globalPerms).length > 0 || Object.keys(repoPerms).length > 0) {
|
|
325
|
-
const mergedPerms = { ...globalPerms, ...repoPerms };
|
|
326
|
-
for (const key of ARRAY_PERMISSION_KEYS) {
|
|
327
|
-
const globalArr = Array.isArray(globalPerms[key]) ? globalPerms[key] : [];
|
|
328
|
-
const repoArr = Array.isArray(repoPerms[key]) ? repoPerms[key] : [];
|
|
329
|
-
if (globalArr.length > 0 || repoArr.length > 0) {
|
|
330
|
-
mergedPerms[key] = [.../* @__PURE__ */ new Set([...globalArr, ...repoArr])];
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
merged.permissions = mergedPerms;
|
|
334
|
-
}
|
|
335
|
-
return merged;
|
|
336
|
-
}
|
|
337
|
-
function stripPermissionsAllow(settings) {
|
|
338
|
-
if (!settings.permissions || typeof settings.permissions !== "object") {
|
|
339
|
-
return settings;
|
|
340
|
-
}
|
|
341
|
-
const perms = { ...settings.permissions };
|
|
342
|
-
delete perms.allow;
|
|
343
|
-
if (Object.keys(perms).length === 0) {
|
|
344
|
-
const { permissions: _, ...rest } = settings;
|
|
345
|
-
return rest;
|
|
346
|
-
}
|
|
347
|
-
return { ...settings, permissions: perms };
|
|
348
|
-
}
|
|
349
|
-
var TEMPLATE_MANAGED_KEYS, TEMPLATE_MANAGED_PERMISSION_KEYS, ARRAY_PERMISSION_KEYS;
|
|
350
|
-
var init_settings_merge = __esm({
|
|
351
|
-
"src/lib/settings-merge.ts"() {
|
|
352
|
-
"use strict";
|
|
353
|
-
TEMPLATE_MANAGED_KEYS = ["attribution", "hooks", "statusLine"];
|
|
354
|
-
TEMPLATE_MANAGED_PERMISSION_KEYS = [
|
|
355
|
-
"deny",
|
|
356
|
-
"ask",
|
|
357
|
-
"additionalDirectories"
|
|
358
|
-
];
|
|
359
|
-
ARRAY_PERMISSION_KEYS = ["deny", "ask"];
|
|
360
|
-
}
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
// src/lib/hook-registry.ts
|
|
364
|
-
import { readdir, readFile as readFile3 } from "node:fs/promises";
|
|
365
|
-
import { join as join3 } from "node:path";
|
|
366
|
-
function parseHookMeta(content) {
|
|
367
|
-
const lineMatch = content.match(/^#\s*@hook:(.*)$/m);
|
|
368
|
-
if (!lineMatch) return null;
|
|
369
|
-
const parts = lineMatch[1].trim().split(/\s+/);
|
|
370
|
-
const event = parts[0];
|
|
371
|
-
if (!event) return null;
|
|
372
|
-
return {
|
|
373
|
-
event,
|
|
374
|
-
matcher: parts.slice(1).join(" ")
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
async function discoverHooks(hooksDir) {
|
|
378
|
-
const discovered = /* @__PURE__ */ new Map();
|
|
379
|
-
let filenames;
|
|
380
|
-
try {
|
|
381
|
-
const entries = await readdir(hooksDir);
|
|
382
|
-
filenames = entries.filter((e) => e.endsWith(".sh"));
|
|
383
|
-
} catch {
|
|
384
|
-
return discovered;
|
|
385
|
-
}
|
|
386
|
-
for (const filename of filenames) {
|
|
387
|
-
const content = await readFile3(join3(hooksDir, filename), "utf-8");
|
|
388
|
-
const meta = parseHookMeta(content);
|
|
389
|
-
if (meta) {
|
|
390
|
-
discovered.set(filename.replace(/\.sh$/, ""), meta);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
return discovered;
|
|
394
|
-
}
|
|
395
|
-
function mergeDiscoveredHooks(existing, discovered, hooksRelPath = ".claude/hooks") {
|
|
396
|
-
if (discovered.size === 0) return existing;
|
|
397
|
-
const merged = {};
|
|
398
|
-
for (const [event, matchers] of Object.entries(existing)) {
|
|
399
|
-
merged[event] = matchers.map((m) => ({
|
|
400
|
-
matcher: m.matcher,
|
|
401
|
-
hooks: [...m.hooks]
|
|
402
|
-
}));
|
|
403
|
-
}
|
|
404
|
-
for (const [filename, meta] of discovered) {
|
|
405
|
-
const command = `bash ${hooksRelPath}/${filename}.sh`;
|
|
406
|
-
if (!merged[meta.event]) {
|
|
407
|
-
merged[meta.event] = [];
|
|
408
|
-
}
|
|
409
|
-
const eventEntries = merged[meta.event];
|
|
410
|
-
const alreadyRegistered = eventEntries.some(
|
|
411
|
-
(m) => m.hooks.some((h) => h.command === command)
|
|
412
|
-
);
|
|
413
|
-
if (alreadyRegistered) continue;
|
|
414
|
-
const matcherEntry = eventEntries.find((m) => m.matcher === meta.matcher);
|
|
415
|
-
if (matcherEntry) {
|
|
416
|
-
matcherEntry.hooks.push({ type: "command", command });
|
|
417
|
-
} else {
|
|
418
|
-
eventEntries.push({
|
|
419
|
-
matcher: meta.matcher,
|
|
420
|
-
hooks: [{ type: "command", command }]
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
return merged;
|
|
425
|
-
}
|
|
426
|
-
function stripDiscoveredHooks(config, hooksRelPath = ".claude/hooks") {
|
|
427
|
-
const prefix = `bash ${hooksRelPath}/`;
|
|
428
|
-
const stripped = {};
|
|
429
|
-
for (const [event, matchers] of Object.entries(config)) {
|
|
430
|
-
const filteredMatchers = [];
|
|
431
|
-
for (const matcher of matchers) {
|
|
432
|
-
const filteredHooks = matcher.hooks.filter(
|
|
433
|
-
(h) => !(h.command && h.command.startsWith(prefix) && h.command.endsWith(".sh"))
|
|
434
|
-
);
|
|
435
|
-
if (filteredHooks.length > 0) {
|
|
436
|
-
filteredMatchers.push({
|
|
437
|
-
matcher: matcher.matcher,
|
|
438
|
-
hooks: filteredHooks
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
if (filteredMatchers.length > 0) {
|
|
443
|
-
stripped[event] = filteredMatchers;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
return stripped;
|
|
447
|
-
}
|
|
448
|
-
var init_hook_registry = __esm({
|
|
449
|
-
"src/lib/hook-registry.ts"() {
|
|
450
|
-
"use strict";
|
|
451
|
-
}
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
// src/lib/variables.ts
|
|
455
|
-
function splitFrontmatter(content) {
|
|
456
|
-
const fmMatch = content.match(/^(---\s*\n[\s\S]*?\n---\n?)([\s\S]*)$/);
|
|
457
|
-
if (fmMatch) {
|
|
458
|
-
return { frontmatter: fmMatch[1], body: fmMatch[2] };
|
|
459
|
-
}
|
|
460
|
-
if (content.startsWith("#!/") || content.startsWith("# @")) {
|
|
461
|
-
const lines = content.split("\n");
|
|
462
|
-
let headerEnd = 0;
|
|
463
|
-
for (let i = 0; i < lines.length; i++) {
|
|
464
|
-
if (lines[i].startsWith("#") || lines[i].startsWith("#!/") || lines[i].trim() === "") {
|
|
465
|
-
headerEnd = i + 1;
|
|
466
|
-
} else {
|
|
467
|
-
break;
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
return {
|
|
471
|
-
frontmatter: lines.slice(0, headerEnd).join("\n") + "\n",
|
|
472
|
-
body: lines.slice(headerEnd).join("\n")
|
|
473
|
-
};
|
|
474
|
-
}
|
|
475
|
-
return { frontmatter: "", body: content };
|
|
476
|
-
}
|
|
477
|
-
function substituteVariables(content, repoData) {
|
|
478
|
-
if (!content.includes("{{")) return content;
|
|
479
|
-
const { frontmatter, body } = splitFrontmatter(content);
|
|
480
|
-
let result = body;
|
|
481
|
-
for (const [name, resolver] of Object.entries(TEMPLATE_VARIABLES)) {
|
|
482
|
-
const placeholder = `{{${name}}}`;
|
|
483
|
-
if (result.includes(placeholder)) {
|
|
484
|
-
result = result.replaceAll(placeholder, resolver(repoData));
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
return frontmatter + result;
|
|
488
|
-
}
|
|
489
|
-
function escapeRegex(str) {
|
|
490
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
491
|
-
}
|
|
492
|
-
function reverseSubstituteVariables(content, repoData) {
|
|
493
|
-
const { frontmatter, body } = splitFrontmatter(content);
|
|
494
|
-
const entries = [];
|
|
495
|
-
for (const [name, resolver] of Object.entries(TEMPLATE_VARIABLES)) {
|
|
496
|
-
const value = resolver(repoData);
|
|
497
|
-
if (value.length === 0) continue;
|
|
498
|
-
entries.push([value, `{{${name}}}`]);
|
|
499
|
-
}
|
|
500
|
-
entries.sort((a, b) => b[0].length - a[0].length);
|
|
501
|
-
let result = body;
|
|
502
|
-
for (const [value, placeholder] of entries) {
|
|
503
|
-
if (value.length < 8) {
|
|
504
|
-
const pattern = new RegExp(`\\b${escapeRegex(value)}\\b`, "g");
|
|
505
|
-
result = result.replace(pattern, placeholder);
|
|
506
|
-
} else {
|
|
507
|
-
result = result.replaceAll(value, placeholder);
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
return frontmatter + result;
|
|
511
|
-
}
|
|
512
|
-
var TEMPLATE_VARIABLES;
|
|
513
|
-
var init_variables = __esm({
|
|
514
|
-
"src/lib/variables.ts"() {
|
|
515
|
-
"use strict";
|
|
516
|
-
TEMPLATE_VARIABLES = {
|
|
517
|
-
REPO_ID: (repo) => repo.id,
|
|
518
|
-
REPO_NAME: (repo) => repo.name,
|
|
519
|
-
REPO_PATH: (repo) => repo.path ?? "",
|
|
520
|
-
GIT_BRANCH: (repo) => repo.git_branch ?? "development",
|
|
521
|
-
SERVER_PORT: (repo) => repo.server_port != null ? String(repo.server_port) : "",
|
|
522
|
-
SERVER_TYPE: (repo) => repo.server_type ?? "none"
|
|
523
|
-
};
|
|
524
|
-
}
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
// src/lib/sync-engine.ts
|
|
528
|
-
var sync_engine_exports = {};
|
|
529
|
-
__export(sync_engine_exports, {
|
|
530
|
-
executeSyncToLocal: () => executeSyncToLocal
|
|
531
|
-
});
|
|
532
|
-
import {
|
|
533
|
-
readdir as readdir2,
|
|
534
|
-
readFile as readFile4,
|
|
535
|
-
writeFile as writeFile3,
|
|
536
|
-
unlink,
|
|
537
|
-
mkdir,
|
|
538
|
-
rmdir,
|
|
539
|
-
chmod,
|
|
540
|
-
stat
|
|
541
|
-
} from "node:fs/promises";
|
|
542
|
-
import { join as join4, dirname } from "node:path";
|
|
543
|
-
function getTypeDir(claudeDir, dir) {
|
|
544
|
-
if (dir === "commands") return join4(claudeDir, dir, "cbp");
|
|
545
|
-
return join4(claudeDir, dir);
|
|
546
|
-
}
|
|
547
|
-
function getFilePath(claudeDir, typeName, file) {
|
|
548
|
-
const cfg = typeConfig[typeName];
|
|
549
|
-
const typeDir = getTypeDir(claudeDir, cfg.dir);
|
|
550
|
-
if (cfg.subfolder) {
|
|
551
|
-
return join4(typeDir, file.name, `${cfg.subfolder}${cfg.ext}`);
|
|
552
|
-
}
|
|
553
|
-
if (typeName === "command" && file.category) {
|
|
554
|
-
return join4(typeDir, file.category, `${file.name}${cfg.ext}`);
|
|
555
|
-
}
|
|
556
|
-
if (typeName === "template") {
|
|
557
|
-
return join4(typeDir, file.name);
|
|
558
|
-
}
|
|
559
|
-
return join4(typeDir, `${file.name}${cfg.ext}`);
|
|
560
|
-
}
|
|
561
|
-
async function readDirRecursive(dir, base = dir) {
|
|
562
|
-
const result = /* @__PURE__ */ new Map();
|
|
563
|
-
try {
|
|
564
|
-
const entries = await readdir2(dir, { withFileTypes: true });
|
|
565
|
-
for (const entry of entries) {
|
|
566
|
-
const fullPath = join4(dir, entry.name);
|
|
567
|
-
if (entry.isDirectory()) {
|
|
568
|
-
const sub = await readDirRecursive(fullPath, base);
|
|
569
|
-
for (const [k, v] of sub) result.set(k, v);
|
|
570
|
-
} else {
|
|
571
|
-
const relPath = fullPath.slice(base.length + 1);
|
|
572
|
-
const fileContent = await readFile4(fullPath, "utf-8");
|
|
573
|
-
result.set(relPath, fileContent);
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
} catch {
|
|
577
|
-
}
|
|
578
|
-
return result;
|
|
579
|
-
}
|
|
580
|
-
async function isGitWorktree(projectPath) {
|
|
581
|
-
try {
|
|
582
|
-
const gitPath = join4(projectPath, ".git");
|
|
583
|
-
const info = await stat(gitPath);
|
|
584
|
-
return info.isFile();
|
|
585
|
-
} catch {
|
|
586
|
-
return false;
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
async function removeEmptyParents(filePath, stopAt) {
|
|
590
|
-
let dir = dirname(filePath);
|
|
591
|
-
while (dir.length > stopAt.length && dir.startsWith(stopAt)) {
|
|
592
|
-
try {
|
|
593
|
-
await rmdir(dir);
|
|
594
|
-
dir = dirname(dir);
|
|
595
|
-
} catch {
|
|
596
|
-
break;
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
async function executeSyncToLocal(options) {
|
|
601
|
-
const { repoId, projectPath, dryRun = false } = options;
|
|
602
|
-
const [syncRes, repoRes] = await Promise.all([
|
|
603
|
-
apiGet("/sync/defaults"),
|
|
604
|
-
apiGet(`/repos/${repoId}`)
|
|
605
|
-
]);
|
|
606
|
-
const syncData = syncRes.data;
|
|
607
|
-
const repoData = repoRes.data;
|
|
608
|
-
syncData.claude_md = [];
|
|
609
|
-
const claudeDir = join4(projectPath, ".claude");
|
|
610
|
-
const worktree = await isGitWorktree(projectPath);
|
|
611
|
-
const byType = {};
|
|
612
|
-
const totals = { created: 0, updated: 0, deleted: 0, unchanged: 0 };
|
|
613
|
-
const dbOnlyFiles = [];
|
|
614
|
-
for (const [syncKey, typeName] of Object.entries(syncKeyToType)) {
|
|
615
|
-
if (worktree && typeName === "command") {
|
|
616
|
-
byType["commands"] = {
|
|
617
|
-
created: [],
|
|
618
|
-
updated: [],
|
|
619
|
-
deleted: [],
|
|
620
|
-
unchanged: []
|
|
621
|
-
};
|
|
622
|
-
continue;
|
|
623
|
-
}
|
|
624
|
-
const cfg = typeConfig[typeName];
|
|
625
|
-
const targetDir = getTypeDir(claudeDir, cfg.dir);
|
|
626
|
-
const remoteFiles = syncData[syncKey] ?? [];
|
|
627
|
-
const result = {
|
|
628
|
-
created: [],
|
|
629
|
-
updated: [],
|
|
630
|
-
deleted: [],
|
|
631
|
-
unchanged: []
|
|
632
|
-
};
|
|
633
|
-
if (!dryRun) {
|
|
634
|
-
await mkdir(targetDir, { recursive: true });
|
|
635
|
-
}
|
|
636
|
-
const localFiles = await readDirRecursive(targetDir);
|
|
637
|
-
const remotePathMap = /* @__PURE__ */ new Map();
|
|
638
|
-
for (const remote of remoteFiles) {
|
|
639
|
-
const fullPath = getFilePath(claudeDir, typeName, remote);
|
|
640
|
-
const relPath = fullPath.slice(targetDir.length + 1);
|
|
641
|
-
const substituted = substituteVariables(remote.content, repoData);
|
|
642
|
-
remotePathMap.set(relPath, { content: substituted, name: remote.name });
|
|
643
|
-
}
|
|
644
|
-
for (const [relPath, { content, name }] of remotePathMap) {
|
|
645
|
-
const fullPath = join4(targetDir, relPath);
|
|
646
|
-
const localContent = localFiles.get(relPath);
|
|
647
|
-
if (localContent === void 0) {
|
|
648
|
-
const remoteFile = remoteFiles.find((f) => f.name === name);
|
|
649
|
-
dbOnlyFiles.push({
|
|
650
|
-
type: typeName,
|
|
651
|
-
name,
|
|
652
|
-
category: remoteFile?.category ?? null,
|
|
653
|
-
localPath: fullPath
|
|
654
|
-
});
|
|
655
|
-
if (!dryRun) {
|
|
656
|
-
await mkdir(dirname(fullPath), { recursive: true });
|
|
657
|
-
await writeFile3(fullPath, content, "utf-8");
|
|
658
|
-
if (typeName === "hook") await chmod(fullPath, 493);
|
|
659
|
-
}
|
|
660
|
-
result.created.push(name);
|
|
661
|
-
totals.created++;
|
|
662
|
-
} else if (localContent !== content) {
|
|
663
|
-
if (!dryRun) {
|
|
664
|
-
await writeFile3(fullPath, content, "utf-8");
|
|
665
|
-
if (typeName === "hook") await chmod(fullPath, 493);
|
|
666
|
-
}
|
|
667
|
-
result.updated.push(name);
|
|
668
|
-
totals.updated++;
|
|
669
|
-
} else {
|
|
670
|
-
result.unchanged.push(name);
|
|
671
|
-
totals.unchanged++;
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
for (const [relPath] of localFiles) {
|
|
675
|
-
if (!remotePathMap.has(relPath)) {
|
|
676
|
-
const fullPath = join4(targetDir, relPath);
|
|
677
|
-
if (!dryRun) {
|
|
678
|
-
await unlink(fullPath);
|
|
679
|
-
await removeEmptyParents(fullPath, targetDir);
|
|
680
|
-
}
|
|
681
|
-
const pathName = relPath.replace(/\.(md|sh)$/, "").replace(/\/(AGENT|SKILL)$/, "");
|
|
682
|
-
result.deleted.push(pathName);
|
|
683
|
-
totals.deleted++;
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
byType[`${typeName}s`] = result;
|
|
687
|
-
}
|
|
688
|
-
{
|
|
689
|
-
const typeName = "docs_stack";
|
|
690
|
-
const syncKey = "docs_stack";
|
|
691
|
-
const targetDir = join4(projectPath, "docs", "stack");
|
|
692
|
-
const remoteFiles = syncData[syncKey] ?? [];
|
|
693
|
-
const result = {
|
|
694
|
-
created: [],
|
|
695
|
-
updated: [],
|
|
696
|
-
deleted: [],
|
|
697
|
-
unchanged: []
|
|
698
|
-
};
|
|
699
|
-
if (remoteFiles.length > 0 && !dryRun) {
|
|
700
|
-
await mkdir(targetDir, { recursive: true });
|
|
701
|
-
}
|
|
702
|
-
const localFiles = await readDirRecursive(targetDir);
|
|
703
|
-
const remotePathMap = /* @__PURE__ */ new Map();
|
|
704
|
-
for (const remote of remoteFiles) {
|
|
705
|
-
const relPath = remote.category ? join4(remote.category, remote.name) : remote.name;
|
|
706
|
-
const substituted = substituteVariables(remote.content, repoData);
|
|
707
|
-
remotePathMap.set(relPath, {
|
|
708
|
-
content: substituted,
|
|
709
|
-
name: remote.category ? `${remote.category}/${remote.name}` : remote.name
|
|
710
|
-
});
|
|
711
|
-
}
|
|
712
|
-
for (const [relPath, { content, name }] of remotePathMap) {
|
|
713
|
-
const fullPath = join4(targetDir, relPath);
|
|
714
|
-
const localContent = localFiles.get(relPath);
|
|
715
|
-
if (localContent === void 0) {
|
|
716
|
-
if (!dryRun) {
|
|
717
|
-
await mkdir(dirname(fullPath), { recursive: true });
|
|
718
|
-
await writeFile3(fullPath, content, "utf-8");
|
|
719
|
-
}
|
|
720
|
-
result.created.push(name);
|
|
721
|
-
totals.created++;
|
|
722
|
-
} else if (localContent !== content) {
|
|
723
|
-
if (!dryRun) {
|
|
724
|
-
await writeFile3(fullPath, content, "utf-8");
|
|
725
|
-
}
|
|
726
|
-
result.updated.push(name);
|
|
727
|
-
totals.updated++;
|
|
728
|
-
} else {
|
|
729
|
-
result.unchanged.push(name);
|
|
730
|
-
totals.unchanged++;
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
for (const [relPath] of localFiles) {
|
|
734
|
-
if (!remotePathMap.has(relPath)) {
|
|
735
|
-
const fullPath = join4(targetDir, relPath);
|
|
736
|
-
if (!dryRun) {
|
|
737
|
-
await unlink(fullPath);
|
|
738
|
-
await removeEmptyParents(fullPath, targetDir);
|
|
739
|
-
}
|
|
740
|
-
result.deleted.push(relPath);
|
|
741
|
-
totals.deleted++;
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
byType[typeName] = result;
|
|
745
|
-
}
|
|
746
|
-
const globalSettingsFiles = syncData.global_settings ?? [];
|
|
747
|
-
let globalSettings = {};
|
|
748
|
-
for (const gf of globalSettingsFiles) {
|
|
749
|
-
const parsed = JSON.parse(
|
|
750
|
-
substituteVariables(gf.content, repoData)
|
|
751
|
-
);
|
|
752
|
-
globalSettings = { ...globalSettings, ...parsed };
|
|
753
|
-
}
|
|
754
|
-
const specialTypes = {
|
|
755
|
-
claude_md: () => join4(projectPath, "CLAUDE.md"),
|
|
756
|
-
settings: () => join4(projectPath, ".claude", "settings.json")
|
|
757
|
-
};
|
|
758
|
-
for (const [typeName, getPath] of Object.entries(specialTypes)) {
|
|
759
|
-
const remoteFiles = syncData[typeName] ?? [];
|
|
760
|
-
const result = {
|
|
761
|
-
created: [],
|
|
762
|
-
updated: [],
|
|
763
|
-
deleted: [],
|
|
764
|
-
unchanged: []
|
|
765
|
-
};
|
|
766
|
-
for (const remote of remoteFiles) {
|
|
767
|
-
const targetPath = getPath(remote.name);
|
|
768
|
-
const remoteContent = substituteVariables(remote.content, repoData);
|
|
769
|
-
let localContent;
|
|
770
|
-
try {
|
|
771
|
-
localContent = await readFile4(targetPath, "utf-8");
|
|
772
|
-
} catch {
|
|
773
|
-
}
|
|
774
|
-
if (typeName === "settings") {
|
|
775
|
-
const repoSettings = JSON.parse(remoteContent);
|
|
776
|
-
const combinedTemplate = mergeGlobalAndRepoSettings(
|
|
777
|
-
globalSettings,
|
|
778
|
-
repoSettings
|
|
779
|
-
);
|
|
780
|
-
const hooksDir = join4(projectPath, ".claude", "hooks");
|
|
781
|
-
const discovered = await discoverHooks(hooksDir);
|
|
782
|
-
if (localContent === void 0) {
|
|
783
|
-
const finalSettings = stripPermissionsAllow(combinedTemplate);
|
|
784
|
-
if (discovered.size > 0) {
|
|
785
|
-
finalSettings.hooks = mergeDiscoveredHooks(
|
|
786
|
-
finalSettings.hooks ?? {},
|
|
787
|
-
discovered
|
|
788
|
-
);
|
|
789
|
-
}
|
|
790
|
-
if (!dryRun) {
|
|
791
|
-
await mkdir(dirname(targetPath), { recursive: true });
|
|
792
|
-
await writeFile3(
|
|
793
|
-
targetPath,
|
|
794
|
-
JSON.stringify(finalSettings, null, 2) + "\n",
|
|
795
|
-
"utf-8"
|
|
796
|
-
);
|
|
797
|
-
}
|
|
798
|
-
result.created.push(remote.name);
|
|
799
|
-
totals.created++;
|
|
800
|
-
} else {
|
|
801
|
-
const localSettings = JSON.parse(localContent);
|
|
802
|
-
let merged = mergeSettings(combinedTemplate, localSettings);
|
|
803
|
-
merged = stripPermissionsAllow(merged);
|
|
804
|
-
if (discovered.size > 0) {
|
|
805
|
-
merged.hooks = mergeDiscoveredHooks(
|
|
806
|
-
merged.hooks ?? {},
|
|
807
|
-
discovered
|
|
808
|
-
);
|
|
809
|
-
}
|
|
810
|
-
const mergedContent = JSON.stringify(merged, null, 2) + "\n";
|
|
811
|
-
if (localContent !== mergedContent) {
|
|
812
|
-
if (!dryRun) {
|
|
813
|
-
await writeFile3(targetPath, mergedContent, "utf-8");
|
|
814
|
-
}
|
|
815
|
-
result.updated.push(remote.name);
|
|
816
|
-
totals.updated++;
|
|
817
|
-
} else {
|
|
818
|
-
result.unchanged.push(remote.name);
|
|
819
|
-
totals.unchanged++;
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
} else {
|
|
823
|
-
if (localContent === void 0) {
|
|
824
|
-
if (!dryRun) {
|
|
825
|
-
await mkdir(dirname(targetPath), { recursive: true });
|
|
826
|
-
await writeFile3(targetPath, remoteContent, "utf-8");
|
|
827
|
-
}
|
|
828
|
-
result.created.push(remote.name);
|
|
829
|
-
totals.created++;
|
|
830
|
-
} else if (localContent !== remoteContent) {
|
|
831
|
-
if (!dryRun) {
|
|
832
|
-
await writeFile3(targetPath, remoteContent, "utf-8");
|
|
833
|
-
}
|
|
834
|
-
result.updated.push(remote.name);
|
|
835
|
-
totals.updated++;
|
|
836
|
-
} else {
|
|
837
|
-
result.unchanged.push(remote.name);
|
|
838
|
-
totals.unchanged++;
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
byType[typeName] = result;
|
|
843
|
-
}
|
|
844
|
-
if (!dryRun) {
|
|
845
|
-
await apiPost("/sync/state", {
|
|
846
|
-
repo_id: repoId,
|
|
847
|
-
last_synced_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
848
|
-
was_skipped: false,
|
|
849
|
-
files_synced_count: totals.created + totals.updated + totals.deleted + totals.unchanged,
|
|
850
|
-
files_pushed: 0,
|
|
851
|
-
files_pulled: totals.created + totals.updated,
|
|
852
|
-
files_deleted: totals.deleted,
|
|
853
|
-
files_skipped: 0
|
|
854
|
-
});
|
|
855
|
-
const fileRepoUpdates = [];
|
|
856
|
-
const syncTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
857
|
-
for (const [syncKey, typeName] of Object.entries(syncKeyToType)) {
|
|
858
|
-
const remoteFiles = syncData[syncKey] ?? [];
|
|
859
|
-
for (const file of remoteFiles) {
|
|
860
|
-
fileRepoUpdates.push({
|
|
861
|
-
claude_file_id: file.id ?? void 0,
|
|
862
|
-
file_type: typeName,
|
|
863
|
-
file_name: file.name,
|
|
864
|
-
file_category: file.category ?? null,
|
|
865
|
-
file_scope: file.scope ?? "shared",
|
|
866
|
-
last_synced_at: syncTimestamp,
|
|
867
|
-
sync_status: "synced"
|
|
868
|
-
});
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
for (const typeName of ["claude_md", "settings"]) {
|
|
872
|
-
const remoteFiles = syncData[typeName] ?? [];
|
|
873
|
-
for (const file of remoteFiles) {
|
|
874
|
-
fileRepoUpdates.push({
|
|
875
|
-
claude_file_id: file.id ?? void 0,
|
|
876
|
-
file_type: typeName,
|
|
877
|
-
file_name: file.name,
|
|
878
|
-
file_category: file.category ?? null,
|
|
879
|
-
file_scope: file.scope ?? `local:${repoId}`,
|
|
880
|
-
last_synced_at: syncTimestamp,
|
|
881
|
-
sync_status: "synced"
|
|
882
|
-
});
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
if (fileRepoUpdates.length > 0) {
|
|
886
|
-
try {
|
|
887
|
-
await apiPost("/sync/file-repos", {
|
|
888
|
-
repo_id: repoId,
|
|
889
|
-
file_repos: fileRepoUpdates
|
|
890
|
-
});
|
|
891
|
-
} catch {
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
return { byType, totals, dbOnlyFiles };
|
|
896
|
-
}
|
|
897
|
-
var typeConfig, syncKeyToType;
|
|
898
|
-
var init_sync_engine = __esm({
|
|
899
|
-
"src/lib/sync-engine.ts"() {
|
|
900
|
-
"use strict";
|
|
901
|
-
init_api();
|
|
902
|
-
init_settings_merge();
|
|
903
|
-
init_hook_registry();
|
|
904
|
-
init_variables();
|
|
905
|
-
typeConfig = {
|
|
906
|
-
command: { dir: "commands", ext: ".md" },
|
|
907
|
-
agent: { dir: "agents", ext: ".md", subfolder: "AGENT" },
|
|
908
|
-
skill: { dir: "skills", ext: ".md", subfolder: "SKILL" },
|
|
909
|
-
rule: { dir: "rules", ext: ".md" },
|
|
910
|
-
hook: { dir: "hooks", ext: ".sh" },
|
|
911
|
-
template: { dir: "templates", ext: "" },
|
|
912
|
-
context: { dir: "context", ext: ".md" }
|
|
913
|
-
};
|
|
914
|
-
syncKeyToType = {
|
|
915
|
-
commands: "command",
|
|
916
|
-
agents: "agent",
|
|
917
|
-
skills: "skill",
|
|
918
|
-
rules: "rule",
|
|
919
|
-
hooks: "hook",
|
|
920
|
-
templates: "template",
|
|
921
|
-
contexts: "context"
|
|
922
|
-
};
|
|
923
|
-
}
|
|
924
|
-
});
|
|
925
|
-
|
|
926
296
|
// src/cli/setup.ts
|
|
927
297
|
var setup_exports = {};
|
|
928
298
|
__export(setup_exports, {
|
|
@@ -930,15 +300,15 @@ __export(setup_exports, {
|
|
|
930
300
|
});
|
|
931
301
|
import { createInterface } from "node:readline/promises";
|
|
932
302
|
import { stdin, stdout } from "node:process";
|
|
933
|
-
import { readFile as
|
|
303
|
+
import { readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
934
304
|
import { homedir } from "node:os";
|
|
935
|
-
import { join as
|
|
305
|
+
import { join as join3 } from "node:path";
|
|
936
306
|
function getConfigPath(scope) {
|
|
937
|
-
return scope === "user" ?
|
|
307
|
+
return scope === "user" ? join3(homedir(), ".claude.json") : join3(process.cwd(), ".mcp.json");
|
|
938
308
|
}
|
|
939
309
|
async function readConfig(path) {
|
|
940
310
|
try {
|
|
941
|
-
const raw = await
|
|
311
|
+
const raw = await readFile3(path, "utf-8");
|
|
942
312
|
const parsed = JSON.parse(raw);
|
|
943
313
|
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
944
314
|
return parsed;
|
|
@@ -962,7 +332,7 @@ async function writeMcpConfig(scope, apiKey) {
|
|
|
962
332
|
config.mcpServers = {};
|
|
963
333
|
}
|
|
964
334
|
config.mcpServers.codebyplan = buildMcpEntry(apiKey);
|
|
965
|
-
await
|
|
335
|
+
await writeFile3(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
966
336
|
return configPath;
|
|
967
337
|
}
|
|
968
338
|
async function verifyMcpConfig(scope, apiKey) {
|
|
@@ -1089,12 +459,12 @@ async function runSetup() {
|
|
|
1089
459
|
deviceId
|
|
1090
460
|
});
|
|
1091
461
|
const worktreeId = tupleId ?? pathBasedId;
|
|
1092
|
-
const codebyplanPath =
|
|
462
|
+
const codebyplanPath = join3(projectPath, ".codebyplan.json");
|
|
1093
463
|
const codebyplanConfig = {
|
|
1094
464
|
repo_id: selectedRepo.id
|
|
1095
465
|
};
|
|
1096
466
|
if (worktreeId) codebyplanConfig.worktree_id = worktreeId;
|
|
1097
|
-
await
|
|
467
|
+
await writeFile3(
|
|
1098
468
|
codebyplanPath,
|
|
1099
469
|
JSON.stringify(codebyplanConfig, null, 2) + "\n",
|
|
1100
470
|
"utf-8"
|
|
@@ -1105,27 +475,6 @@ async function runSetup() {
|
|
|
1105
475
|
` Worktree id set (${worktreeId}) \u2014 this worktree is now identified for hard-lock enforcement.`
|
|
1106
476
|
);
|
|
1107
477
|
}
|
|
1108
|
-
console.log("\n Running initial sync...\n");
|
|
1109
|
-
try {
|
|
1110
|
-
const { executeSyncToLocal: executeSyncToLocal2 } = await Promise.resolve().then(() => (init_sync_engine(), sync_engine_exports));
|
|
1111
|
-
const syncResult = await executeSyncToLocal2({
|
|
1112
|
-
repoId: selectedRepo.id,
|
|
1113
|
-
projectPath
|
|
1114
|
-
});
|
|
1115
|
-
const totalChanges = syncResult.totals.created + syncResult.totals.updated + syncResult.totals.deleted;
|
|
1116
|
-
if (totalChanges > 0) {
|
|
1117
|
-
console.log(
|
|
1118
|
-
` Synced: ${syncResult.totals.created} created, ${syncResult.totals.updated} updated, ${syncResult.totals.deleted} deleted
|
|
1119
|
-
`
|
|
1120
|
-
);
|
|
1121
|
-
} else {
|
|
1122
|
-
console.log(" All files already up to date.\n");
|
|
1123
|
-
}
|
|
1124
|
-
} catch (err) {
|
|
1125
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1126
|
-
console.log(` Sync failed: ${msg}`);
|
|
1127
|
-
console.log(" Run 'codebyplan sync' later to sync files.\n");
|
|
1128
|
-
}
|
|
1129
478
|
}
|
|
1130
479
|
}
|
|
1131
480
|
}
|
|
@@ -1144,15 +493,15 @@ var init_setup = __esm({
|
|
|
1144
493
|
}
|
|
1145
494
|
});
|
|
1146
495
|
|
|
1147
|
-
// src/cli/
|
|
1148
|
-
import { readFile as
|
|
1149
|
-
import { join as
|
|
496
|
+
// src/cli/flags.ts
|
|
497
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
498
|
+
import { join as join4, resolve } from "node:path";
|
|
1150
499
|
async function findCodebyplanConfig(startDir, maxDepth = 20) {
|
|
1151
500
|
let cursor = resolve(startDir);
|
|
1152
501
|
for (let depth = 0; depth < maxDepth; depth++) {
|
|
1153
|
-
const configPath =
|
|
502
|
+
const configPath = join4(cursor, ".codebyplan.json");
|
|
1154
503
|
try {
|
|
1155
|
-
const raw = await
|
|
504
|
+
const raw = await readFile4(configPath, "utf-8");
|
|
1156
505
|
const parsed = JSON.parse(raw);
|
|
1157
506
|
return { path: configPath, contents: parsed };
|
|
1158
507
|
} catch {
|
|
@@ -1196,590 +545,72 @@ async function resolveConfig(flags) {
|
|
|
1196
545
|
}
|
|
1197
546
|
return { repoId, worktreeId, projectPath };
|
|
1198
547
|
}
|
|
1199
|
-
var
|
|
1200
|
-
"src/cli/
|
|
548
|
+
var init_flags = __esm({
|
|
549
|
+
"src/cli/flags.ts"() {
|
|
1201
550
|
"use strict";
|
|
1202
551
|
}
|
|
1203
552
|
});
|
|
1204
553
|
|
|
1205
|
-
// src/cli/
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
return "shared";
|
|
1216
|
-
}
|
|
1217
|
-
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
1218
|
-
if (fmMatch) {
|
|
1219
|
-
const scopeLine = fmMatch[1].match(/^scope:\s*(\S+)/m);
|
|
1220
|
-
if (scopeLine) {
|
|
1221
|
-
const raw = scopeLine[1];
|
|
1222
|
-
return raw === "shared" ? "shared" : `local:${raw}`;
|
|
1223
|
-
}
|
|
1224
|
-
if (/^scope\b/m.test(fmMatch[1])) {
|
|
1225
|
-
console.error(
|
|
1226
|
-
` Warning: frontmatter contains "scope" but could not parse it. Expected format: "scope: shared" or "scope: <repo-name>". Defaulting to "shared".`
|
|
1227
|
-
);
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
return "shared";
|
|
1231
|
-
}
|
|
1232
|
-
function compositeKey(type, name, category) {
|
|
1233
|
-
return category ? `${type}:${category}/${name}` : `${type}:${name}`;
|
|
1234
|
-
}
|
|
1235
|
-
async function scanLocalFiles(claudeDir, projectPath) {
|
|
1236
|
-
const result = /* @__PURE__ */ new Map();
|
|
1237
|
-
await scanCommands(join7(claudeDir, "commands", "cbp"), result);
|
|
1238
|
-
await scanSubfolderType(
|
|
1239
|
-
join7(claudeDir, "agents"),
|
|
1240
|
-
"agent",
|
|
1241
|
-
"AGENT.md",
|
|
1242
|
-
result
|
|
1243
|
-
);
|
|
1244
|
-
await scanSubfolderType(
|
|
1245
|
-
join7(claudeDir, "skills"),
|
|
1246
|
-
"skill",
|
|
1247
|
-
"SKILL.md",
|
|
1248
|
-
result
|
|
1249
|
-
);
|
|
1250
|
-
await scanFlatType(join7(claudeDir, "rules"), "rule", ".md", result);
|
|
1251
|
-
await scanFlatType(join7(claudeDir, "hooks"), "hook", ".sh", result);
|
|
1252
|
-
await scanTemplates(join7(claudeDir, "templates"), result);
|
|
1253
|
-
await scanCategorizedType(
|
|
1254
|
-
join7(claudeDir, "context"),
|
|
1255
|
-
"context",
|
|
1256
|
-
".md",
|
|
1257
|
-
result
|
|
1258
|
-
);
|
|
1259
|
-
await scanDocsRecursive(join7(claudeDir, "docs"), result);
|
|
1260
|
-
await scanSettings(claudeDir, projectPath, result);
|
|
1261
|
-
return result;
|
|
1262
|
-
}
|
|
1263
|
-
async function scanCommands(dir, result) {
|
|
1264
|
-
await scanCommandsRecursive(dir, dir, result);
|
|
554
|
+
// src/cli/confirm.ts
|
|
555
|
+
var confirm_exports = {};
|
|
556
|
+
__export(confirm_exports, {
|
|
557
|
+
SyncCancelledError: () => SyncCancelledError,
|
|
558
|
+
confirmProceed: () => confirmProceed
|
|
559
|
+
});
|
|
560
|
+
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
561
|
+
import { stdin as stdin2, stdout as stdout2 } from "node:process";
|
|
562
|
+
function isAbortError(err) {
|
|
563
|
+
return err instanceof Error && err.code === "ABORT_ERR";
|
|
1265
564
|
}
|
|
1266
|
-
async function
|
|
1267
|
-
|
|
565
|
+
async function confirmProceed(message) {
|
|
566
|
+
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
1268
567
|
try {
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
baseDir,
|
|
1277
|
-
join7(currentDir, entry.name),
|
|
1278
|
-
result
|
|
568
|
+
while (true) {
|
|
569
|
+
const answer = await rl.question(message ?? " Proceed? [Y/n] ");
|
|
570
|
+
const a = answer.trim().toLowerCase();
|
|
571
|
+
if (a === "" || a === "y" || a === "yes") return true;
|
|
572
|
+
if (a === "n" || a === "no") return false;
|
|
573
|
+
console.log(
|
|
574
|
+
` Unknown option "${answer.trim()}". Valid: y/yes, n/no, or Enter for yes.`
|
|
1279
575
|
);
|
|
1280
|
-
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1281
|
-
const name = entry.name.slice(0, -3);
|
|
1282
|
-
const content = await readFile7(join7(currentDir, entry.name), "utf-8");
|
|
1283
|
-
const relDir = currentDir.slice(baseDir.length + 1);
|
|
1284
|
-
const category = relDir || null;
|
|
1285
|
-
const scope = extractScope(content, "command");
|
|
1286
|
-
const key = compositeKey("command", name, category);
|
|
1287
|
-
result.set(key, { type: "command", name, category, content, scope });
|
|
1288
576
|
}
|
|
577
|
+
} catch (err) {
|
|
578
|
+
if (isAbortError(err)) throw new SyncCancelledError();
|
|
579
|
+
throw err;
|
|
580
|
+
} finally {
|
|
581
|
+
rl.close();
|
|
1289
582
|
}
|
|
1290
583
|
}
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
if (entry.isDirectory()) {
|
|
1300
|
-
const filePath = join7(dir, entry.name, fileName);
|
|
1301
|
-
try {
|
|
1302
|
-
const content = await readFile7(filePath, "utf-8");
|
|
1303
|
-
const scope = extractScope(content, type);
|
|
1304
|
-
const key = compositeKey(type, entry.name, null);
|
|
1305
|
-
result.set(key, {
|
|
1306
|
-
type,
|
|
1307
|
-
name: entry.name,
|
|
1308
|
-
category: null,
|
|
1309
|
-
content,
|
|
1310
|
-
scope
|
|
1311
|
-
});
|
|
1312
|
-
} catch {
|
|
584
|
+
var SyncCancelledError;
|
|
585
|
+
var init_confirm = __esm({
|
|
586
|
+
"src/cli/confirm.ts"() {
|
|
587
|
+
"use strict";
|
|
588
|
+
SyncCancelledError = class extends Error {
|
|
589
|
+
constructor() {
|
|
590
|
+
super("Sync cancelled");
|
|
591
|
+
this.name = "SyncCancelledError";
|
|
1313
592
|
}
|
|
1314
|
-
}
|
|
593
|
+
};
|
|
1315
594
|
}
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// src/lib/tech-detect.ts
|
|
598
|
+
import { readFile as readFile5, access, readdir } from "node:fs/promises";
|
|
599
|
+
import { join as join5, relative } from "node:path";
|
|
600
|
+
async function fileExists(filePath) {
|
|
1319
601
|
try {
|
|
1320
|
-
|
|
602
|
+
await access(filePath);
|
|
603
|
+
return true;
|
|
1321
604
|
} catch {
|
|
1322
|
-
return;
|
|
1323
|
-
}
|
|
1324
|
-
for (const entry of entries) {
|
|
1325
|
-
if (entry.isFile() && entry.name.endsWith(ext)) {
|
|
1326
|
-
const name = entry.name.slice(0, -ext.length);
|
|
1327
|
-
const content = await readFile7(join7(dir, entry.name), "utf-8");
|
|
1328
|
-
const scope = extractScope(content, type);
|
|
1329
|
-
const key = compositeKey(type, name, null);
|
|
1330
|
-
result.set(key, { type, name, category: null, content, scope });
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
async function scanCategorizedType(dir, type, ext, result) {
|
|
1335
|
-
let entries;
|
|
1336
|
-
try {
|
|
1337
|
-
entries = await readdir3(dir, { withFileTypes: true });
|
|
1338
|
-
} catch {
|
|
1339
|
-
return;
|
|
1340
|
-
}
|
|
1341
|
-
for (const entry of entries) {
|
|
1342
|
-
if (entry.isDirectory()) {
|
|
1343
|
-
const category = entry.name;
|
|
1344
|
-
let subEntries;
|
|
1345
|
-
try {
|
|
1346
|
-
subEntries = await readdir3(join7(dir, category), {
|
|
1347
|
-
withFileTypes: true
|
|
1348
|
-
});
|
|
1349
|
-
} catch {
|
|
1350
|
-
continue;
|
|
1351
|
-
}
|
|
1352
|
-
for (const sub of subEntries) {
|
|
1353
|
-
if (sub.isFile() && sub.name.endsWith(ext)) {
|
|
1354
|
-
const name = sub.name.slice(0, -ext.length);
|
|
1355
|
-
const content = await readFile7(
|
|
1356
|
-
join7(dir, category, sub.name),
|
|
1357
|
-
"utf-8"
|
|
1358
|
-
);
|
|
1359
|
-
const scope = extractScope(content, type);
|
|
1360
|
-
const key = compositeKey(type, name, category);
|
|
1361
|
-
result.set(key, { type, name, category, content, scope });
|
|
1362
|
-
}
|
|
1363
|
-
}
|
|
1364
|
-
} else if (entry.isFile() && entry.name.endsWith(ext)) {
|
|
1365
|
-
const name = entry.name.slice(0, -ext.length);
|
|
1366
|
-
const content = await readFile7(join7(dir, entry.name), "utf-8");
|
|
1367
|
-
const scope = extractScope(content, type);
|
|
1368
|
-
const key = compositeKey(type, name, null);
|
|
1369
|
-
result.set(key, { type, name, category: null, content, scope });
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
async function scanDocsRecursive(docsDir, result) {
|
|
1374
|
-
await scanDocsDir(docsDir, docsDir, result);
|
|
1375
|
-
}
|
|
1376
|
-
async function scanDocsDir(baseDir, currentDir, result) {
|
|
1377
|
-
let entries;
|
|
1378
|
-
try {
|
|
1379
|
-
entries = await readdir3(currentDir, { withFileTypes: true });
|
|
1380
|
-
} catch {
|
|
1381
|
-
return;
|
|
1382
|
-
}
|
|
1383
|
-
for (const entry of entries) {
|
|
1384
|
-
if (entry.isDirectory()) {
|
|
1385
|
-
await scanDocsDir(baseDir, join7(currentDir, entry.name), result);
|
|
1386
|
-
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1387
|
-
const name = entry.name.slice(0, -3);
|
|
1388
|
-
const content = await readFile7(join7(currentDir, entry.name), "utf-8");
|
|
1389
|
-
const scope = extractScope(content, "docs");
|
|
1390
|
-
const relDir = currentDir.slice(baseDir.length + 1);
|
|
1391
|
-
const category = relDir || null;
|
|
1392
|
-
const key = compositeKey("docs", name, category);
|
|
1393
|
-
result.set(key, { type: "docs", name, category, content, scope });
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
1397
|
-
async function scanTemplates(dir, result) {
|
|
1398
|
-
let entries;
|
|
1399
|
-
try {
|
|
1400
|
-
entries = await readdir3(dir, { withFileTypes: true });
|
|
1401
|
-
} catch {
|
|
1402
|
-
return;
|
|
1403
|
-
}
|
|
1404
|
-
for (const entry of entries) {
|
|
1405
|
-
if (entry.isFile() && extname(entry.name)) {
|
|
1406
|
-
const content = await readFile7(join7(dir, entry.name), "utf-8");
|
|
1407
|
-
const scope = extractScope(content, "template");
|
|
1408
|
-
const key = compositeKey("template", entry.name, null);
|
|
1409
|
-
result.set(key, {
|
|
1410
|
-
type: "template",
|
|
1411
|
-
name: entry.name,
|
|
1412
|
-
category: null,
|
|
1413
|
-
content,
|
|
1414
|
-
scope
|
|
1415
|
-
});
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
}
|
|
1419
|
-
async function scanSettings(claudeDir, projectPath, result) {
|
|
1420
|
-
const settingsPath = join7(claudeDir, "settings.json");
|
|
1421
|
-
let raw;
|
|
1422
|
-
try {
|
|
1423
|
-
raw = await readFile7(settingsPath, "utf-8");
|
|
1424
|
-
} catch {
|
|
1425
|
-
return;
|
|
1426
|
-
}
|
|
1427
|
-
let parsed;
|
|
1428
|
-
try {
|
|
1429
|
-
parsed = JSON.parse(raw);
|
|
1430
|
-
} catch {
|
|
1431
|
-
return;
|
|
1432
|
-
}
|
|
1433
|
-
parsed = stripPermissionsAllow(parsed);
|
|
1434
|
-
if (parsed.hooks && typeof parsed.hooks === "object") {
|
|
1435
|
-
const hooksDir = projectPath ? join7(projectPath, ".claude", "hooks") : join7(claudeDir, "hooks");
|
|
1436
|
-
const discovered = await discoverHooks(hooksDir);
|
|
1437
|
-
if (discovered.size > 0) {
|
|
1438
|
-
parsed.hooks = stripDiscoveredHooks(
|
|
1439
|
-
parsed.hooks,
|
|
1440
|
-
".claude/hooks"
|
|
1441
|
-
);
|
|
1442
|
-
if (Object.keys(parsed.hooks).length === 0) {
|
|
1443
|
-
delete parsed.hooks;
|
|
1444
|
-
}
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
const content = JSON.stringify(parsed, null, 2) + "\n";
|
|
1448
|
-
const key = compositeKey("settings", "settings", null);
|
|
1449
|
-
result.set(key, {
|
|
1450
|
-
type: "settings",
|
|
1451
|
-
name: "settings",
|
|
1452
|
-
category: null,
|
|
1453
|
-
content,
|
|
1454
|
-
scope: "shared"
|
|
1455
|
-
});
|
|
1456
|
-
}
|
|
1457
|
-
var init_fileMapper = __esm({
|
|
1458
|
-
"src/cli/fileMapper.ts"() {
|
|
1459
|
-
"use strict";
|
|
1460
|
-
init_settings_merge();
|
|
1461
|
-
init_hook_registry();
|
|
1462
|
-
}
|
|
1463
|
-
});
|
|
1464
|
-
|
|
1465
|
-
// src/cli/confirm.ts
|
|
1466
|
-
var confirm_exports = {};
|
|
1467
|
-
__export(confirm_exports, {
|
|
1468
|
-
SyncCancelledError: () => SyncCancelledError,
|
|
1469
|
-
confirmEach: () => confirmEach,
|
|
1470
|
-
confirmProceed: () => confirmProceed,
|
|
1471
|
-
promptChoice: () => promptChoice,
|
|
1472
|
-
promptReviewMode: () => promptReviewMode,
|
|
1473
|
-
reviewFilesOneByOne: () => reviewFilesOneByOne,
|
|
1474
|
-
reviewFolder: () => reviewFolder
|
|
1475
|
-
});
|
|
1476
|
-
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
1477
|
-
import { stdin as stdin2, stdout as stdout2 } from "node:process";
|
|
1478
|
-
function isAbortError(err) {
|
|
1479
|
-
return err instanceof Error && err.code === "ABORT_ERR";
|
|
1480
|
-
}
|
|
1481
|
-
async function confirmProceed(message) {
|
|
1482
|
-
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
1483
|
-
try {
|
|
1484
|
-
while (true) {
|
|
1485
|
-
const answer = await rl.question(message ?? " Proceed? [Y/n] ");
|
|
1486
|
-
const a = answer.trim().toLowerCase();
|
|
1487
|
-
if (a === "" || a === "y" || a === "yes") return true;
|
|
1488
|
-
if (a === "n" || a === "no") return false;
|
|
1489
|
-
console.log(
|
|
1490
|
-
` Unknown option "${answer.trim()}". Valid: y/yes, n/no, or Enter for yes.`
|
|
1491
|
-
);
|
|
1492
|
-
}
|
|
1493
|
-
} catch (err) {
|
|
1494
|
-
if (isAbortError(err)) throw new SyncCancelledError();
|
|
1495
|
-
throw err;
|
|
1496
|
-
} finally {
|
|
1497
|
-
rl.close();
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
|
-
async function promptChoice(message, options) {
|
|
1501
|
-
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
1502
|
-
try {
|
|
1503
|
-
const answer = await rl.question(message);
|
|
1504
|
-
const a = answer.trim().toLowerCase();
|
|
1505
|
-
return options.includes(a) ? a : options[0];
|
|
1506
|
-
} catch (err) {
|
|
1507
|
-
if (isAbortError(err)) throw new SyncCancelledError();
|
|
1508
|
-
throw err;
|
|
1509
|
-
} finally {
|
|
1510
|
-
rl.close();
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
async function confirmEach(items, label) {
|
|
1514
|
-
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
1515
|
-
const accepted = [];
|
|
1516
|
-
try {
|
|
1517
|
-
for (const item of items) {
|
|
1518
|
-
const answer = await rl.question(` ${label(item)} \u2014 delete? [y/n/a] `);
|
|
1519
|
-
const a = answer.trim().toLowerCase();
|
|
1520
|
-
if (a === "a") {
|
|
1521
|
-
accepted.push(item, ...items.slice(items.indexOf(item) + 1));
|
|
1522
|
-
break;
|
|
1523
|
-
}
|
|
1524
|
-
if (a === "y" || a === "yes" || a === "") {
|
|
1525
|
-
accepted.push(item);
|
|
1526
|
-
}
|
|
1527
|
-
}
|
|
1528
|
-
} catch (err) {
|
|
1529
|
-
if (isAbortError(err)) throw new SyncCancelledError();
|
|
1530
|
-
throw err;
|
|
1531
|
-
} finally {
|
|
1532
|
-
rl.close();
|
|
1533
|
-
}
|
|
1534
|
-
return accepted;
|
|
1535
|
-
}
|
|
1536
|
-
function parseReviewAction(input) {
|
|
1537
|
-
const a = input.trim().toLowerCase();
|
|
1538
|
-
switch (a) {
|
|
1539
|
-
case "d":
|
|
1540
|
-
case "delete":
|
|
1541
|
-
return { action: "delete", all: false, special: null };
|
|
1542
|
-
case "p":
|
|
1543
|
-
case "pull":
|
|
1544
|
-
return { action: "pull", all: false, special: null };
|
|
1545
|
-
case "s":
|
|
1546
|
-
case "push":
|
|
1547
|
-
return { action: "push", all: false, special: null };
|
|
1548
|
-
case "k":
|
|
1549
|
-
case "skip":
|
|
1550
|
-
return { action: "skip", all: false, special: null };
|
|
1551
|
-
case "da":
|
|
1552
|
-
return { action: "delete", all: true, special: null };
|
|
1553
|
-
case "pa":
|
|
1554
|
-
return { action: "pull", all: true, special: null };
|
|
1555
|
-
case "sa":
|
|
1556
|
-
return { action: "push", all: true, special: null };
|
|
1557
|
-
case "ka":
|
|
1558
|
-
return { action: "skip", all: true, special: null };
|
|
1559
|
-
case "v":
|
|
1560
|
-
case "view":
|
|
1561
|
-
return { action: null, all: false, special: "view" };
|
|
1562
|
-
case "r":
|
|
1563
|
-
case "recommended":
|
|
1564
|
-
return { action: null, all: false, special: "recommended" };
|
|
1565
|
-
case "":
|
|
1566
|
-
return { action: null, all: false, special: "recommended" };
|
|
1567
|
-
// Enter = recommended
|
|
1568
|
-
default:
|
|
1569
|
-
return { action: null, all: false, special: null };
|
|
1570
|
-
}
|
|
1571
|
-
}
|
|
1572
|
-
function formatActionPrompt(recommended, includeView, includeRecommended) {
|
|
1573
|
-
const actions = [
|
|
1574
|
-
`[d]elete${recommended === "delete" ? "\u2605" : ""}`,
|
|
1575
|
-
`[p]ull${recommended === "pull" ? "\u2605" : ""}`,
|
|
1576
|
-
`pu[s]h${recommended === "push" ? "\u2605" : ""}`,
|
|
1577
|
-
`s[k]ip${recommended === "skip" ? "\u2605" : ""}`
|
|
1578
|
-
];
|
|
1579
|
-
if (includeView) actions.push("[v]iew");
|
|
1580
|
-
if (includeRecommended) actions.push("[r]ecommended");
|
|
1581
|
-
return actions.join(" ");
|
|
1582
|
-
}
|
|
1583
|
-
function showDiff(local, remote, displayPath) {
|
|
1584
|
-
console.log(`
|
|
1585
|
-
--- ${displayPath} (diff) ---`);
|
|
1586
|
-
if (local === null && remote !== null) {
|
|
1587
|
-
console.log(" (no local file \u2014 remote content below)");
|
|
1588
|
-
for (const line of remote.split("\n").slice(0, 30)) {
|
|
1589
|
-
console.log(` + ${line}`);
|
|
1590
|
-
}
|
|
1591
|
-
if (remote.split("\n").length > 30) console.log(" ... (truncated)");
|
|
1592
|
-
} else if (local !== null && remote === null) {
|
|
1593
|
-
console.log(" (no remote file \u2014 local content below)");
|
|
1594
|
-
for (const line of local.split("\n").slice(0, 30)) {
|
|
1595
|
-
console.log(` - ${line}`);
|
|
1596
|
-
}
|
|
1597
|
-
if (local.split("\n").length > 30) console.log(" ... (truncated)");
|
|
1598
|
-
} else if (local !== null && remote !== null) {
|
|
1599
|
-
const localLines = local.split("\n");
|
|
1600
|
-
const remoteLines = remote.split("\n");
|
|
1601
|
-
let shown = 0;
|
|
1602
|
-
const maxLines = 40;
|
|
1603
|
-
for (let i = 0; i < Math.max(localLines.length, remoteLines.length) && shown < maxLines; i++) {
|
|
1604
|
-
const l = localLines[i];
|
|
1605
|
-
const r = remoteLines[i];
|
|
1606
|
-
if (l === r) {
|
|
1607
|
-
console.log(` ${l ?? ""}`);
|
|
1608
|
-
} else {
|
|
1609
|
-
if (l !== void 0) console.log(` - ${l}`);
|
|
1610
|
-
if (r !== void 0) console.log(` + ${r}`);
|
|
1611
|
-
}
|
|
1612
|
-
shown++;
|
|
1613
|
-
}
|
|
1614
|
-
if (Math.max(localLines.length, remoteLines.length) > maxLines) {
|
|
1615
|
-
console.log(" ... (truncated)");
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
console.log();
|
|
1619
|
-
}
|
|
1620
|
-
async function promptReviewMode() {
|
|
1621
|
-
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
1622
|
-
try {
|
|
1623
|
-
while (true) {
|
|
1624
|
-
const answer = await rl.question(
|
|
1625
|
-
" Review [o]ne-by-one or [f]older-by-folder? "
|
|
1626
|
-
);
|
|
1627
|
-
const a = answer.trim().toLowerCase();
|
|
1628
|
-
if (a === "o" || a === "one-by-one" || a === "one" || a === "file")
|
|
1629
|
-
return "file";
|
|
1630
|
-
if (a === "f" || a === "folder") return "folder";
|
|
1631
|
-
console.log(
|
|
1632
|
-
` Unknown option "${answer.trim()}". Valid: o/one-by-one, f/folder`
|
|
1633
|
-
);
|
|
1634
|
-
}
|
|
1635
|
-
} catch (err) {
|
|
1636
|
-
if (isAbortError(err)) throw new SyncCancelledError();
|
|
1637
|
-
throw err;
|
|
1638
|
-
} finally {
|
|
1639
|
-
rl.close();
|
|
1640
|
-
}
|
|
1641
|
-
}
|
|
1642
|
-
async function reviewFilesOneByOne(items, label, plannedAction, recommendedAction, content) {
|
|
1643
|
-
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
1644
|
-
const results = [];
|
|
1645
|
-
try {
|
|
1646
|
-
let applyAll = null;
|
|
1647
|
-
for (const item of items) {
|
|
1648
|
-
if (applyAll) {
|
|
1649
|
-
results.push(applyAll);
|
|
1650
|
-
continue;
|
|
1651
|
-
}
|
|
1652
|
-
const planned = plannedAction(item);
|
|
1653
|
-
const rec = recommendedAction ? recommendedAction(item) : planned;
|
|
1654
|
-
const hasContent = content != null;
|
|
1655
|
-
const prompt = ` ${label(item)} (${planned}) \u2014 ${formatActionPrompt(rec, hasContent, false)}: `;
|
|
1656
|
-
while (true) {
|
|
1657
|
-
const answer = await rl.question(prompt);
|
|
1658
|
-
const result = parseReviewAction(answer);
|
|
1659
|
-
if (result.special === "view") {
|
|
1660
|
-
if (content) {
|
|
1661
|
-
showDiff(content.local(item), content.remote(item), label(item));
|
|
1662
|
-
} else {
|
|
1663
|
-
console.log(" No content available for diff.");
|
|
1664
|
-
}
|
|
1665
|
-
continue;
|
|
1666
|
-
}
|
|
1667
|
-
if (result.special === "recommended") {
|
|
1668
|
-
results.push(rec);
|
|
1669
|
-
break;
|
|
1670
|
-
}
|
|
1671
|
-
if (result.action === null) {
|
|
1672
|
-
console.log(
|
|
1673
|
-
` Unknown option "${answer.trim()}". Valid: ${formatActionPrompt(rec, hasContent, false)}`
|
|
1674
|
-
);
|
|
1675
|
-
continue;
|
|
1676
|
-
}
|
|
1677
|
-
results.push(result.action);
|
|
1678
|
-
if (result.all) applyAll = result.action;
|
|
1679
|
-
break;
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
|
-
} catch (err) {
|
|
1683
|
-
if (isAbortError(err)) throw new SyncCancelledError();
|
|
1684
|
-
throw err;
|
|
1685
|
-
} finally {
|
|
1686
|
-
rl.close();
|
|
1687
|
-
}
|
|
1688
|
-
return results;
|
|
1689
|
-
}
|
|
1690
|
-
async function reviewFolder(folderName, items, label, plannedAction, recommendedAction, content) {
|
|
1691
|
-
console.log(`
|
|
1692
|
-
${folderName} (${items.length} files):`);
|
|
1693
|
-
for (const item of items) {
|
|
1694
|
-
const rec = recommendedAction ? recommendedAction(item) : plannedAction(item);
|
|
1695
|
-
const actionLabel = plannedAction(item);
|
|
1696
|
-
const star = actionLabel === rec ? "\u2605" : "";
|
|
1697
|
-
console.log(` ${label(item)} (${actionLabel}${star})`);
|
|
1698
|
-
}
|
|
1699
|
-
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
1700
|
-
try {
|
|
1701
|
-
while (true) {
|
|
1702
|
-
const promptStr = ` Action for all: ${formatActionPrompt(
|
|
1703
|
-
recommendedAction ? recommendedAction(items[0]) : plannedAction(items[0]),
|
|
1704
|
-
false,
|
|
1705
|
-
true
|
|
1706
|
-
)} [o]ne-by-one: `;
|
|
1707
|
-
const answer = await rl.question(promptStr);
|
|
1708
|
-
const a = answer.trim().toLowerCase();
|
|
1709
|
-
if (a === "o" || a === "one-by-one") {
|
|
1710
|
-
rl.close();
|
|
1711
|
-
return reviewFilesOneByOne(
|
|
1712
|
-
items,
|
|
1713
|
-
label,
|
|
1714
|
-
plannedAction,
|
|
1715
|
-
recommendedAction,
|
|
1716
|
-
content
|
|
1717
|
-
);
|
|
1718
|
-
}
|
|
1719
|
-
if (a === "r" || a === "recommended") {
|
|
1720
|
-
return items.map(
|
|
1721
|
-
(item) => recommendedAction ? recommendedAction(item) : plannedAction(item)
|
|
1722
|
-
);
|
|
1723
|
-
}
|
|
1724
|
-
if (a === "v" || a === "view") {
|
|
1725
|
-
if (content) {
|
|
1726
|
-
for (const item of items) {
|
|
1727
|
-
showDiff(content.local(item), content.remote(item), label(item));
|
|
1728
|
-
}
|
|
1729
|
-
} else {
|
|
1730
|
-
console.log(" No content available for diff.");
|
|
1731
|
-
}
|
|
1732
|
-
continue;
|
|
1733
|
-
}
|
|
1734
|
-
const result = parseReviewAction(a);
|
|
1735
|
-
if (result.action !== null) {
|
|
1736
|
-
return items.map(() => result.action);
|
|
1737
|
-
}
|
|
1738
|
-
console.log(
|
|
1739
|
-
` Unknown option "${answer.trim()}". Valid: ${formatActionPrompt(
|
|
1740
|
-
recommendedAction ? recommendedAction(items[0]) : plannedAction(items[0]),
|
|
1741
|
-
false,
|
|
1742
|
-
true
|
|
1743
|
-
)} [o]ne-by-one`
|
|
1744
|
-
);
|
|
1745
|
-
}
|
|
1746
|
-
} catch (err) {
|
|
1747
|
-
if (isAbortError(err)) throw new SyncCancelledError();
|
|
1748
|
-
throw err;
|
|
1749
|
-
} finally {
|
|
1750
|
-
rl.close();
|
|
1751
|
-
}
|
|
1752
|
-
}
|
|
1753
|
-
var SyncCancelledError;
|
|
1754
|
-
var init_confirm = __esm({
|
|
1755
|
-
"src/cli/confirm.ts"() {
|
|
1756
|
-
"use strict";
|
|
1757
|
-
SyncCancelledError = class extends Error {
|
|
1758
|
-
constructor() {
|
|
1759
|
-
super("Sync cancelled");
|
|
1760
|
-
this.name = "SyncCancelledError";
|
|
1761
|
-
}
|
|
1762
|
-
};
|
|
1763
|
-
}
|
|
1764
|
-
});
|
|
1765
|
-
|
|
1766
|
-
// src/lib/tech-detect.ts
|
|
1767
|
-
import { readFile as readFile8, access, readdir as readdir4 } from "node:fs/promises";
|
|
1768
|
-
import { join as join8, relative } from "node:path";
|
|
1769
|
-
async function fileExists(filePath) {
|
|
1770
|
-
try {
|
|
1771
|
-
await access(filePath);
|
|
1772
|
-
return true;
|
|
1773
|
-
} catch {
|
|
1774
|
-
return false;
|
|
605
|
+
return false;
|
|
1775
606
|
}
|
|
1776
607
|
}
|
|
1777
608
|
async function discoverMonorepoApps(projectPath) {
|
|
1778
609
|
const apps = [];
|
|
1779
610
|
const patterns = [];
|
|
1780
611
|
try {
|
|
1781
|
-
const raw = await
|
|
1782
|
-
|
|
612
|
+
const raw = await readFile5(
|
|
613
|
+
join5(projectPath, "pnpm-workspace.yaml"),
|
|
1783
614
|
"utf-8"
|
|
1784
615
|
);
|
|
1785
616
|
const matches = raw.match(/^\s*-\s*['"]?([^'"#\n]+)['"]?/gm);
|
|
@@ -1793,7 +624,7 @@ async function discoverMonorepoApps(projectPath) {
|
|
|
1793
624
|
}
|
|
1794
625
|
if (patterns.length === 0) {
|
|
1795
626
|
try {
|
|
1796
|
-
const raw = await
|
|
627
|
+
const raw = await readFile5(join5(projectPath, "package.json"), "utf-8");
|
|
1797
628
|
const pkg = JSON.parse(raw);
|
|
1798
629
|
const ws = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages;
|
|
1799
630
|
if (ws) patterns.push(...ws);
|
|
@@ -1803,14 +634,14 @@ async function discoverMonorepoApps(projectPath) {
|
|
|
1803
634
|
for (const pattern of patterns) {
|
|
1804
635
|
if (pattern.endsWith("/*")) {
|
|
1805
636
|
const dir = pattern.slice(0, -2);
|
|
1806
|
-
const absDir =
|
|
637
|
+
const absDir = join5(projectPath, dir);
|
|
1807
638
|
try {
|
|
1808
|
-
const entries = await
|
|
639
|
+
const entries = await readdir(absDir, { withFileTypes: true });
|
|
1809
640
|
for (const entry of entries) {
|
|
1810
641
|
if (entry.isDirectory()) {
|
|
1811
|
-
const relPath =
|
|
1812
|
-
const absPath =
|
|
1813
|
-
if (await fileExists(
|
|
642
|
+
const relPath = join5(dir, entry.name);
|
|
643
|
+
const absPath = join5(absDir, entry.name);
|
|
644
|
+
if (await fileExists(join5(absPath, "package.json"))) {
|
|
1814
645
|
apps.push({ name: entry.name, path: relPath, absPath });
|
|
1815
646
|
}
|
|
1816
647
|
}
|
|
@@ -1824,12 +655,12 @@ async function discoverMonorepoApps(projectPath) {
|
|
|
1824
655
|
async function hasJsxFile(dir, depth = 0) {
|
|
1825
656
|
if (depth > 6) return false;
|
|
1826
657
|
try {
|
|
1827
|
-
const entries = await
|
|
658
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
1828
659
|
for (const entry of entries) {
|
|
1829
660
|
const name = entry.name;
|
|
1830
661
|
if (entry.isDirectory()) {
|
|
1831
662
|
if (SKIP_DIRS.has(name) || JSX_SKIP_DIRS.has(name)) continue;
|
|
1832
|
-
if (await hasJsxFile(
|
|
663
|
+
if (await hasJsxFile(join5(dir, name), depth + 1)) return true;
|
|
1833
664
|
} else if (entry.isFile()) {
|
|
1834
665
|
if (JSX_TEST_PATTERN.test(name)) continue;
|
|
1835
666
|
if (name.endsWith(".tsx") || name.endsWith(".jsx")) return true;
|
|
@@ -1848,7 +679,7 @@ async function hasJsxFile(dir, depth = 0) {
|
|
|
1848
679
|
async function detectCapabilities(dirPath, pkgJson) {
|
|
1849
680
|
const caps = /* @__PURE__ */ new Set();
|
|
1850
681
|
for (const sub of JSX_SCAN_DIRS) {
|
|
1851
|
-
if (await hasJsxFile(
|
|
682
|
+
if (await hasJsxFile(join5(dirPath, sub))) {
|
|
1852
683
|
caps.add("jsx");
|
|
1853
684
|
break;
|
|
1854
685
|
}
|
|
@@ -1870,7 +701,7 @@ async function detectCapabilities(dirPath, pkgJson) {
|
|
|
1870
701
|
}
|
|
1871
702
|
}
|
|
1872
703
|
}
|
|
1873
|
-
if (!caps.has("node-server") && await fileExists(
|
|
704
|
+
if (!caps.has("node-server") && await fileExists(join5(dirPath, "src", "main.ts"))) {
|
|
1874
705
|
caps.add("node-server");
|
|
1875
706
|
}
|
|
1876
707
|
if (pkgJson && pkgJson.bin) {
|
|
@@ -1886,7 +717,7 @@ async function detectFromDirectory(dirPath) {
|
|
|
1886
717
|
const seen = /* @__PURE__ */ new Map();
|
|
1887
718
|
let pkgJson = null;
|
|
1888
719
|
try {
|
|
1889
|
-
const raw = await
|
|
720
|
+
const raw = await readFile5(join5(dirPath, "package.json"), "utf-8");
|
|
1890
721
|
pkgJson = JSON.parse(raw);
|
|
1891
722
|
const allDeps = {
|
|
1892
723
|
...pkgJson.dependencies ?? {},
|
|
@@ -1918,7 +749,7 @@ async function detectFromDirectory(dirPath) {
|
|
|
1918
749
|
}
|
|
1919
750
|
for (const { file, rule } of CONFIG_FILE_MAP) {
|
|
1920
751
|
const key = rule.name.toLowerCase();
|
|
1921
|
-
if (!seen.has(key) && await fileExists(
|
|
752
|
+
if (!seen.has(key) && await fileExists(join5(dirPath, file))) {
|
|
1922
753
|
seen.set(key, { name: rule.name, category: rule.category });
|
|
1923
754
|
}
|
|
1924
755
|
}
|
|
@@ -2096,16 +927,16 @@ function categorizeDependency(depName) {
|
|
|
2096
927
|
async function findPackageJsonFiles(dir, projectPath, depth = 0) {
|
|
2097
928
|
if (depth > 4) return [];
|
|
2098
929
|
const results = [];
|
|
2099
|
-
const pkgPath =
|
|
930
|
+
const pkgPath = join5(dir, "package.json");
|
|
2100
931
|
if (await fileExists(pkgPath)) {
|
|
2101
932
|
results.push(pkgPath);
|
|
2102
933
|
}
|
|
2103
934
|
try {
|
|
2104
|
-
const entries = await
|
|
935
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
2105
936
|
for (const entry of entries) {
|
|
2106
937
|
if (!entry.isDirectory() || SKIP_DIRS.has(entry.name)) continue;
|
|
2107
938
|
const subResults = await findPackageJsonFiles(
|
|
2108
|
-
|
|
939
|
+
join5(dir, entry.name),
|
|
2109
940
|
projectPath,
|
|
2110
941
|
depth + 1
|
|
2111
942
|
);
|
|
@@ -2120,7 +951,7 @@ async function scanAllDependencies(projectPath) {
|
|
|
2120
951
|
const dependencies = [];
|
|
2121
952
|
for (const pkgPath of packageJsonPaths) {
|
|
2122
953
|
try {
|
|
2123
|
-
const raw = await
|
|
954
|
+
const raw = await readFile5(pkgPath, "utf-8");
|
|
2124
955
|
const pkg = JSON.parse(raw);
|
|
2125
956
|
const sourcePath = relative(projectPath, pkgPath);
|
|
2126
957
|
const depSections = [
|
|
@@ -2334,238 +1165,27 @@ var init_tech_detect = __esm({
|
|
|
2334
1165
|
}
|
|
2335
1166
|
});
|
|
2336
1167
|
|
|
2337
|
-
// src/lib/
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
const
|
|
2341
|
-
const
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
if (!scripts?.dev) return null;
|
|
2353
|
-
const parts = scripts.dev.split(/\s+/);
|
|
2354
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
2355
|
-
if (parts[i] === "--port" || parts[i] === "-p") {
|
|
2356
|
-
const next = parts[i + 1];
|
|
2357
|
-
if (next) {
|
|
2358
|
-
const port = parseInt(next, 10);
|
|
2359
|
-
if (!isNaN(port)) return port;
|
|
1168
|
+
// src/lib/eslint-generator.ts
|
|
1169
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1170
|
+
function importedIdentifiers(importLines) {
|
|
1171
|
+
const names = /* @__PURE__ */ new Set();
|
|
1172
|
+
for (const line of importLines) {
|
|
1173
|
+
let m = line.match(/^import\s+([A-Za-z_$][\w$]*)\s+from/);
|
|
1174
|
+
if (m) names.add(m[1]);
|
|
1175
|
+
m = line.match(/^import\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from/);
|
|
1176
|
+
if (m) names.add(m[1]);
|
|
1177
|
+
m = line.match(/^import\s*\{([^}]*)\}\s*from/);
|
|
1178
|
+
if (m) {
|
|
1179
|
+
for (const entry of m[1].split(",")) {
|
|
1180
|
+
const parts = entry.trim().split(/\s+as\s+/);
|
|
1181
|
+
const n = (parts[1] ?? parts[0]).trim();
|
|
1182
|
+
if (n) names.add(n);
|
|
2360
1183
|
}
|
|
2361
1184
|
}
|
|
1185
|
+
m = line.match(/^const\s+([A-Za-z_$][\w$]*)\s*=\s*require/);
|
|
1186
|
+
if (m) names.add(m[1]);
|
|
2362
1187
|
}
|
|
2363
|
-
return
|
|
2364
|
-
}
|
|
2365
|
-
var init_server_detect = __esm({
|
|
2366
|
-
"src/lib/server-detect.ts"() {
|
|
2367
|
-
"use strict";
|
|
2368
|
-
}
|
|
2369
|
-
});
|
|
2370
|
-
|
|
2371
|
-
// src/lib/port-verify.ts
|
|
2372
|
-
import { readFile as readFile9 } from "node:fs/promises";
|
|
2373
|
-
async function verifyPorts(projectPath, portAllocations) {
|
|
2374
|
-
const mismatches = [];
|
|
2375
|
-
const allocatedPorts = new Set(portAllocations.map((a) => a.port));
|
|
2376
|
-
const packageJsonPaths = await findPackageJsonFiles(projectPath, projectPath);
|
|
2377
|
-
for (const pkgPath of packageJsonPaths) {
|
|
2378
|
-
try {
|
|
2379
|
-
const raw = await readFile9(pkgPath, "utf-8");
|
|
2380
|
-
const pkg = JSON.parse(raw);
|
|
2381
|
-
const scriptPort = detectPortFromScripts(pkg);
|
|
2382
|
-
if (scriptPort !== null && !allocatedPorts.has(scriptPort)) {
|
|
2383
|
-
const relativePath = pkgPath.replace(projectPath + "/", "");
|
|
2384
|
-
const matchingAlloc = portAllocations.find(
|
|
2385
|
-
(a) => a.label === getAppLabel(relativePath)
|
|
2386
|
-
);
|
|
2387
|
-
mismatches.push({
|
|
2388
|
-
packageJsonPath: relativePath,
|
|
2389
|
-
scriptPort,
|
|
2390
|
-
allocation: matchingAlloc ?? null,
|
|
2391
|
-
reason: matchingAlloc ? `Script uses port ${scriptPort} but allocation has port ${matchingAlloc.port}` : `Port ${scriptPort} in scripts is not in any allocation`
|
|
2392
|
-
});
|
|
2393
|
-
}
|
|
2394
|
-
} catch {
|
|
2395
|
-
}
|
|
2396
|
-
}
|
|
2397
|
-
return mismatches;
|
|
2398
|
-
}
|
|
2399
|
-
function isDevServerScript(pkg) {
|
|
2400
|
-
const scripts = pkg.scripts;
|
|
2401
|
-
const raw = scripts?.dev;
|
|
2402
|
-
if (!raw || typeof raw !== "string") return false;
|
|
2403
|
-
const script = raw.trim().toLowerCase();
|
|
2404
|
-
if (!script) return false;
|
|
2405
|
-
for (const pattern of DEV_SERVER_BIN_PATTERNS) {
|
|
2406
|
-
if (pattern.test(script)) return true;
|
|
2407
|
-
}
|
|
2408
|
-
const tokens = script.split(/\s+/);
|
|
2409
|
-
for (const token of tokens) {
|
|
2410
|
-
if (token === "--port" || token === "-p") return true;
|
|
2411
|
-
if (token.startsWith("--port=")) return true;
|
|
2412
|
-
}
|
|
2413
|
-
return false;
|
|
2414
|
-
}
|
|
2415
|
-
function labelMatchesAppName(label, appName) {
|
|
2416
|
-
if (!label || !appName) return false;
|
|
2417
|
-
const normalize = (s) => s.toLowerCase().replace(/-/g, " ").replace(/[()]/g, " ").replace(/\s+/g, " ").trim();
|
|
2418
|
-
const labelTokens = normalize(label).split(" ").filter(Boolean);
|
|
2419
|
-
const appToken = normalize(appName);
|
|
2420
|
-
if (!appToken) return false;
|
|
2421
|
-
const appTokens = appToken.split(" ").filter(Boolean);
|
|
2422
|
-
if (appTokens.length === 1) {
|
|
2423
|
-
return labelTokens.includes(appTokens[0]);
|
|
2424
|
-
}
|
|
2425
|
-
for (let i = 0; i <= labelTokens.length - appTokens.length; i++) {
|
|
2426
|
-
if (appTokens.every((t, j) => labelTokens[i + j] === t)) return true;
|
|
2427
|
-
}
|
|
2428
|
-
return false;
|
|
2429
|
-
}
|
|
2430
|
-
async function findUnallocatedApps(projectPath, portAllocations) {
|
|
2431
|
-
const apps = await discoverMonorepoApps(projectPath);
|
|
2432
|
-
if (apps.length === 0) {
|
|
2433
|
-
return [];
|
|
2434
|
-
}
|
|
2435
|
-
const unallocated = [];
|
|
2436
|
-
for (const app of apps) {
|
|
2437
|
-
if (portAllocations.some((a) => labelMatchesAppName(a.label ?? "", app.name))) {
|
|
2438
|
-
continue;
|
|
2439
|
-
}
|
|
2440
|
-
let pkg;
|
|
2441
|
-
try {
|
|
2442
|
-
const raw = await readFile9(`${app.absPath}/package.json`, "utf-8");
|
|
2443
|
-
pkg = JSON.parse(raw);
|
|
2444
|
-
} catch {
|
|
2445
|
-
continue;
|
|
2446
|
-
}
|
|
2447
|
-
if (!isDevServerScript(pkg)) continue;
|
|
2448
|
-
const framework = detectFramework(pkg);
|
|
2449
|
-
const detectedPort = detectPortFromScripts(pkg);
|
|
2450
|
-
const command = `pnpm --filter ${app.name} dev`;
|
|
2451
|
-
unallocated.push({
|
|
2452
|
-
name: app.name,
|
|
2453
|
-
path: app.path,
|
|
2454
|
-
framework,
|
|
2455
|
-
detectedPort,
|
|
2456
|
-
command
|
|
2457
|
-
});
|
|
2458
|
-
}
|
|
2459
|
-
return unallocated;
|
|
2460
|
-
}
|
|
2461
|
-
function getAppLabel(relativePath) {
|
|
2462
|
-
const parts = relativePath.split("/");
|
|
2463
|
-
if (parts.length >= 3 && parts[0] === "apps") {
|
|
2464
|
-
return parts[1];
|
|
2465
|
-
}
|
|
2466
|
-
return "root";
|
|
2467
|
-
}
|
|
2468
|
-
var DEV_SERVER_BIN_PATTERNS;
|
|
2469
|
-
var init_port_verify = __esm({
|
|
2470
|
-
"src/lib/port-verify.ts"() {
|
|
2471
|
-
"use strict";
|
|
2472
|
-
init_tech_detect();
|
|
2473
|
-
init_server_detect();
|
|
2474
|
-
DEV_SERVER_BIN_PATTERNS = [
|
|
2475
|
-
/\bnext\s+dev\b/,
|
|
2476
|
-
/\bnest\s+start\b/,
|
|
2477
|
-
/\bvite\s+(?:dev|serve)\b/,
|
|
2478
|
-
/\bvite\s+preview\b/,
|
|
2479
|
-
/\bnuxt\s+dev\b/,
|
|
2480
|
-
/\b(?:svelte-kit|sveltekit)\s+dev\b/,
|
|
2481
|
-
/\bexpo\s+start\b/
|
|
2482
|
-
];
|
|
2483
|
-
}
|
|
2484
|
-
});
|
|
2485
|
-
|
|
2486
|
-
// src/lib/migrate-local-config.ts
|
|
2487
|
-
import { readFile as readFile10, writeFile as writeFile5 } from "node:fs/promises";
|
|
2488
|
-
import { join as join9 } from "node:path";
|
|
2489
|
-
function sharedConfigPath(projectPath) {
|
|
2490
|
-
return join9(projectPath, ".codebyplan.json");
|
|
2491
|
-
}
|
|
2492
|
-
async function needsLocalMigration(projectPath) {
|
|
2493
|
-
try {
|
|
2494
|
-
const raw = await readFile10(sharedConfigPath(projectPath), "utf-8");
|
|
2495
|
-
const parsed = JSON.parse(raw);
|
|
2496
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
2497
|
-
return false;
|
|
2498
|
-
}
|
|
2499
|
-
const cfg = parsed;
|
|
2500
|
-
if (typeof cfg.worktree_id !== "string" || cfg.worktree_id === "") {
|
|
2501
|
-
return false;
|
|
2502
|
-
}
|
|
2503
|
-
const local = await readLocalConfig(projectPath);
|
|
2504
|
-
if (local?.device_id) {
|
|
2505
|
-
return false;
|
|
2506
|
-
}
|
|
2507
|
-
return true;
|
|
2508
|
-
} catch {
|
|
2509
|
-
return false;
|
|
2510
|
-
}
|
|
2511
|
-
}
|
|
2512
|
-
async function runLocalMigration(projectPath) {
|
|
2513
|
-
const raw = await readFile10(sharedConfigPath(projectPath), "utf-8");
|
|
2514
|
-
const parsed = JSON.parse(raw);
|
|
2515
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
2516
|
-
throw new Error(
|
|
2517
|
-
".codebyplan.json does not contain a JSON object \u2014 cannot migrate"
|
|
2518
|
-
);
|
|
2519
|
-
}
|
|
2520
|
-
const cfg = parsed;
|
|
2521
|
-
const hadWorktreeId = "worktree_id" in cfg;
|
|
2522
|
-
const localBefore = await readLocalConfig(projectPath);
|
|
2523
|
-
const localWillBeCreated = !localBefore?.device_id;
|
|
2524
|
-
const device_id = await getOrCreateDeviceId(projectPath);
|
|
2525
|
-
const cleaned = { ...cfg };
|
|
2526
|
-
delete cleaned.worktree_id;
|
|
2527
|
-
await writeFile5(
|
|
2528
|
-
sharedConfigPath(projectPath),
|
|
2529
|
-
JSON.stringify(cleaned, null, 2) + "\n",
|
|
2530
|
-
"utf-8"
|
|
2531
|
-
);
|
|
2532
|
-
const files_changed = [".codebyplan.json"];
|
|
2533
|
-
if (localWillBeCreated) files_changed.push(".codebyplan.local.json");
|
|
2534
|
-
return {
|
|
2535
|
-
migrated: true,
|
|
2536
|
-
was_dirty: hadWorktreeId || localWillBeCreated,
|
|
2537
|
-
files_changed,
|
|
2538
|
-
device_id
|
|
2539
|
-
};
|
|
2540
|
-
}
|
|
2541
|
-
var init_migrate_local_config = __esm({
|
|
2542
|
-
"src/lib/migrate-local-config.ts"() {
|
|
2543
|
-
"use strict";
|
|
2544
|
-
init_local_config();
|
|
2545
|
-
}
|
|
2546
|
-
});
|
|
2547
|
-
|
|
2548
|
-
// src/lib/eslint-generator.ts
|
|
2549
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
2550
|
-
function importedIdentifiers(importLines) {
|
|
2551
|
-
const names = /* @__PURE__ */ new Set();
|
|
2552
|
-
for (const line of importLines) {
|
|
2553
|
-
let m = line.match(/^import\s+([A-Za-z_$][\w$]*)\s+from/);
|
|
2554
|
-
if (m) names.add(m[1]);
|
|
2555
|
-
m = line.match(/^import\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from/);
|
|
2556
|
-
if (m) names.add(m[1]);
|
|
2557
|
-
m = line.match(/^import\s*\{([^}]*)\}\s*from/);
|
|
2558
|
-
if (m) {
|
|
2559
|
-
for (const entry of m[1].split(",")) {
|
|
2560
|
-
const parts = entry.trim().split(/\s+as\s+/);
|
|
2561
|
-
const n = (parts[1] ?? parts[0]).trim();
|
|
2562
|
-
if (n) names.add(n);
|
|
2563
|
-
}
|
|
2564
|
-
}
|
|
2565
|
-
m = line.match(/^const\s+([A-Za-z_$][\w$]*)\s*=\s*require/);
|
|
2566
|
-
if (m) names.add(m[1]);
|
|
2567
|
-
}
|
|
2568
|
-
return names;
|
|
1188
|
+
return names;
|
|
2569
1189
|
}
|
|
2570
1190
|
function parseFragment(fragment) {
|
|
2571
1191
|
if (!fragment) return { imports: [], configComments: [] };
|
|
@@ -2727,8 +1347,7 @@ function generateEslintConfig(input) {
|
|
|
2727
1347
|
sections.push(
|
|
2728
1348
|
"/**",
|
|
2729
1349
|
" * ESLint flat config \u2014 generated by CodeByPlan CLI.",
|
|
2730
|
-
" * Edit rule overrides via the web UI, then run `codebyplan eslint
|
|
2731
|
-
" * Manual edits will be detected as drift.",
|
|
1350
|
+
" * Edit rule overrides via the web UI, then run `codebyplan eslint init` to regenerate.",
|
|
2732
1351
|
" */",
|
|
2733
1352
|
""
|
|
2734
1353
|
);
|
|
@@ -2929,13 +1548,11 @@ var init_eslint_generator = __esm({
|
|
|
2929
1548
|
var eslint_exports = {};
|
|
2930
1549
|
__export(eslint_exports, {
|
|
2931
1550
|
autoDetectIgnorePatterns: () => autoDetectIgnorePatterns,
|
|
2932
|
-
checkEslintDrift: () => checkEslintDrift,
|
|
2933
1551
|
eslintInit: () => eslintInit,
|
|
2934
|
-
eslintSync: () => eslintSync,
|
|
2935
1552
|
runEslint: () => runEslint
|
|
2936
1553
|
});
|
|
2937
|
-
import { readFile as
|
|
2938
|
-
import { join as
|
|
1554
|
+
import { readFile as readFile6, writeFile as writeFile4, access as access2, readdir as readdir2 } from "node:fs/promises";
|
|
1555
|
+
import { join as join6, relative as relative2 } from "node:path";
|
|
2939
1556
|
async function fileExists2(filePath) {
|
|
2940
1557
|
try {
|
|
2941
1558
|
await access2(filePath);
|
|
@@ -2946,12 +1563,12 @@ async function fileExists2(filePath) {
|
|
|
2946
1563
|
}
|
|
2947
1564
|
async function autoDetectIgnorePatterns(absPath) {
|
|
2948
1565
|
const patterns = [];
|
|
2949
|
-
if (await fileExists2(
|
|
1566
|
+
if (await fileExists2(join6(absPath, "esbuild.js"))) {
|
|
2950
1567
|
patterns.push("esbuild.js");
|
|
2951
1568
|
}
|
|
2952
1569
|
let entries = [];
|
|
2953
1570
|
try {
|
|
2954
|
-
entries = await
|
|
1571
|
+
entries = await readdir2(absPath);
|
|
2955
1572
|
} catch (err) {
|
|
2956
1573
|
console.error(
|
|
2957
1574
|
` autoDetectIgnorePatterns: failed to read ${absPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -2966,19 +1583,19 @@ async function autoDetectIgnorePatterns(absPath) {
|
|
|
2966
1583
|
}
|
|
2967
1584
|
for (const ext of ["ts", "mts", "js", "mjs"]) {
|
|
2968
1585
|
const candidate = `vitest.config.${ext}`;
|
|
2969
|
-
if (await fileExists2(
|
|
1586
|
+
if (await fileExists2(join6(absPath, candidate))) {
|
|
2970
1587
|
patterns.push(candidate);
|
|
2971
1588
|
break;
|
|
2972
1589
|
}
|
|
2973
1590
|
}
|
|
2974
1591
|
for (const ext of ["ts", "mts", "js", "mjs"]) {
|
|
2975
1592
|
const candidate = `vite.config.${ext}`;
|
|
2976
|
-
if (await fileExists2(
|
|
1593
|
+
if (await fileExists2(join6(absPath, candidate))) {
|
|
2977
1594
|
patterns.push(candidate);
|
|
2978
1595
|
break;
|
|
2979
1596
|
}
|
|
2980
1597
|
}
|
|
2981
|
-
if (await fileExists2(
|
|
1598
|
+
if (await fileExists2(join6(absPath, "tauri.conf.json"))) {
|
|
2982
1599
|
patterns.push("src-tauri/**");
|
|
2983
1600
|
patterns.push("**/*.d.ts");
|
|
2984
1601
|
}
|
|
@@ -2986,14 +1603,14 @@ async function autoDetectIgnorePatterns(absPath) {
|
|
|
2986
1603
|
}
|
|
2987
1604
|
function detectPackageManager(projectPath) {
|
|
2988
1605
|
return (async () => {
|
|
2989
|
-
if (await fileExists2(
|
|
2990
|
-
if (await fileExists2(
|
|
1606
|
+
if (await fileExists2(join6(projectPath, "pnpm-lock.yaml"))) return "pnpm";
|
|
1607
|
+
if (await fileExists2(join6(projectPath, "yarn.lock"))) return "yarn";
|
|
2991
1608
|
return "npm";
|
|
2992
1609
|
})();
|
|
2993
1610
|
}
|
|
2994
1611
|
async function getInstalledDeps(pkgJsonPath) {
|
|
2995
1612
|
try {
|
|
2996
|
-
const raw = await
|
|
1613
|
+
const raw = await readFile6(pkgJsonPath, "utf-8");
|
|
2997
1614
|
const pkg = JSON.parse(raw);
|
|
2998
1615
|
const all = /* @__PURE__ */ new Set();
|
|
2999
1616
|
for (const name of Object.keys(pkg.dependencies ?? {})) all.add(name);
|
|
@@ -3106,7 +1723,7 @@ async function eslintInit(repoId, projectPath) {
|
|
|
3106
1723
|
ignorePatterns: detectedIgnores
|
|
3107
1724
|
});
|
|
3108
1725
|
const hash = hashConfig(content);
|
|
3109
|
-
const configPath =
|
|
1726
|
+
const configPath = join6(target.absPath, "eslint.config.mjs");
|
|
3110
1727
|
configsToWrite.push({
|
|
3111
1728
|
target,
|
|
3112
1729
|
presets,
|
|
@@ -3128,11 +1745,11 @@ async function eslintInit(repoId, projectPath) {
|
|
|
3128
1745
|
return;
|
|
3129
1746
|
}
|
|
3130
1747
|
const pm = await detectPackageManager(projectPath);
|
|
3131
|
-
const rootPkgJsonPath =
|
|
1748
|
+
const rootPkgJsonPath = join6(projectPath, "package.json");
|
|
3132
1749
|
const installed = await getInstalledDeps(rootPkgJsonPath);
|
|
3133
1750
|
if (isMonorepo) {
|
|
3134
1751
|
for (const { target } of configsToWrite) {
|
|
3135
|
-
const appPkgJson =
|
|
1752
|
+
const appPkgJson = join6(target.absPath, "package.json");
|
|
3136
1753
|
const appDeps = await getInstalledDeps(appPkgJson);
|
|
3137
1754
|
for (const dep of appDeps) {
|
|
3138
1755
|
installed.add(dep);
|
|
@@ -3184,7 +1801,7 @@ async function eslintInit(repoId, projectPath) {
|
|
|
3184
1801
|
} of configsToWrite) {
|
|
3185
1802
|
if (await fileExists2(configPath)) {
|
|
3186
1803
|
try {
|
|
3187
|
-
const existing = await
|
|
1804
|
+
const existing = await readFile6(configPath, "utf-8");
|
|
3188
1805
|
const existingHash = hashConfig(existing);
|
|
3189
1806
|
if (existingHash === hash) {
|
|
3190
1807
|
console.log(
|
|
@@ -3204,7 +1821,7 @@ async function eslintInit(repoId, projectPath) {
|
|
|
3204
1821
|
}
|
|
3205
1822
|
}
|
|
3206
1823
|
try {
|
|
3207
|
-
await
|
|
1824
|
+
await writeFile4(configPath, content, "utf-8");
|
|
3208
1825
|
} catch (err) {
|
|
3209
1826
|
console.error(
|
|
3210
1827
|
` ${target.name}: Failed to write config: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -3227,721 +1844,117 @@ async function eslintInit(repoId, projectPath) {
|
|
|
3227
1844
|
}
|
|
3228
1845
|
console.log("\n ESLint init complete.\n");
|
|
3229
1846
|
}
|
|
3230
|
-
async function
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
1847
|
+
async function runEslint() {
|
|
1848
|
+
const subcommand = process.argv[3];
|
|
1849
|
+
const flags = parseFlags(4);
|
|
1850
|
+
validateApiKey();
|
|
1851
|
+
const config = await resolveConfig(flags);
|
|
1852
|
+
const { repoId, projectPath } = config;
|
|
1853
|
+
switch (subcommand) {
|
|
1854
|
+
case "init":
|
|
1855
|
+
await eslintInit(repoId, projectPath);
|
|
1856
|
+
break;
|
|
1857
|
+
default:
|
|
1858
|
+
console.log(`
|
|
1859
|
+
Usage:
|
|
1860
|
+
codebyplan eslint init Detect tech stack, resolve presets, generate eslint.config.mjs
|
|
3234
1861
|
`);
|
|
3235
|
-
|
|
3236
|
-
try {
|
|
3237
|
-
const res = await apiGet(
|
|
3238
|
-
`/repos/${repoId}/eslint-config`
|
|
3239
|
-
);
|
|
3240
|
-
configs = res.data ?? [];
|
|
3241
|
-
} catch {
|
|
3242
|
-
console.log(
|
|
3243
|
-
" No existing ESLint config found. Run `codebyplan eslint init` first.\n"
|
|
3244
|
-
);
|
|
3245
|
-
return;
|
|
1862
|
+
break;
|
|
3246
1863
|
}
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
1864
|
+
}
|
|
1865
|
+
var init_eslint = __esm({
|
|
1866
|
+
"src/cli/eslint.ts"() {
|
|
1867
|
+
"use strict";
|
|
1868
|
+
init_flags();
|
|
1869
|
+
init_confirm();
|
|
1870
|
+
init_api();
|
|
1871
|
+
init_tech_detect();
|
|
1872
|
+
init_eslint_generator();
|
|
3252
1873
|
}
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
)
|
|
3266
|
-
|
|
3267
|
-
const savedPresetIds = [...config.active_preset_ids ?? []].sort();
|
|
3268
|
-
const presetsChanged = currentPresetIds.length !== savedPresetIds.length || currentPresetIds.some((id) => !savedPresetIds.includes(id));
|
|
3269
|
-
if (!presetsChanged) {
|
|
3270
|
-
if (await fileExists2(configPath)) {
|
|
3271
|
-
try {
|
|
3272
|
-
const currentContent = await readFile11(configPath, "utf-8");
|
|
3273
|
-
const currentHash = hashConfig(currentContent);
|
|
3274
|
-
if (config.generated_hash && currentHash !== config.generated_hash) {
|
|
3275
|
-
console.log(
|
|
3276
|
-
` ${config.source_path}: drift detected (manually edited). Not overwriting.`
|
|
3277
|
-
);
|
|
3278
|
-
driftCount++;
|
|
3279
|
-
continue;
|
|
3280
|
-
}
|
|
3281
|
-
skippedCount++;
|
|
3282
|
-
continue;
|
|
3283
|
-
} catch {
|
|
3284
|
-
console.warn(
|
|
3285
|
-
` ${config.source_path}: config file unreadable, regenerating...`
|
|
3286
|
-
);
|
|
3287
|
-
}
|
|
3288
|
-
} else {
|
|
3289
|
-
console.log(
|
|
3290
|
-
` ${config.source_path}: config file missing, regenerating...`
|
|
3291
|
-
);
|
|
3292
|
-
}
|
|
3293
|
-
}
|
|
3294
|
-
if (presetsChanged) {
|
|
3295
|
-
console.log(` ${config.source_path}: presets changed, regenerating...`);
|
|
1874
|
+
});
|
|
1875
|
+
|
|
1876
|
+
// src/cli/resolve-worktree.ts
|
|
1877
|
+
var resolve_worktree_exports = {};
|
|
1878
|
+
__export(resolve_worktree_exports, {
|
|
1879
|
+
runResolveWorktree: () => runResolveWorktree
|
|
1880
|
+
});
|
|
1881
|
+
import { execSync as execSync2 } from "node:child_process";
|
|
1882
|
+
async function runResolveWorktree() {
|
|
1883
|
+
try {
|
|
1884
|
+
const projectPath = process.cwd();
|
|
1885
|
+
const found = await findCodebyplanConfig(projectPath);
|
|
1886
|
+
if (!found?.contents.repo_id) {
|
|
1887
|
+
process.exit(0);
|
|
3296
1888
|
}
|
|
3297
|
-
const
|
|
3298
|
-
const
|
|
3299
|
-
|
|
3300
|
-
presets: currentPresets,
|
|
3301
|
-
ruleOverrides: userOverrides && Object.keys(userOverrides).length > 0 ? userOverrides : void 0,
|
|
3302
|
-
ignorePatterns: detectedIgnores
|
|
3303
|
-
});
|
|
3304
|
-
try {
|
|
3305
|
-
await writeFile6(configPath, content, "utf-8");
|
|
3306
|
-
} catch (err) {
|
|
3307
|
-
console.error(
|
|
3308
|
-
` ${config.source_path}: Failed to write config: ${err instanceof Error ? err.message : String(err)}`
|
|
3309
|
-
);
|
|
3310
|
-
continue;
|
|
3311
|
-
}
|
|
3312
|
-
const newHash = hashConfig(content);
|
|
1889
|
+
const repoId = found.contents.repo_id;
|
|
1890
|
+
const deviceId = await getOrCreateDeviceId(projectPath);
|
|
1891
|
+
let branch = "";
|
|
3313
1892
|
try {
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
});
|
|
3320
|
-
} catch (err) {
|
|
3321
|
-
console.error(
|
|
3322
|
-
` Warning: Failed to update server: ${err instanceof Error ? err.message : String(err)}`
|
|
3323
|
-
);
|
|
1893
|
+
branch = execSync2("git symbolic-ref --short HEAD", {
|
|
1894
|
+
cwd: projectPath,
|
|
1895
|
+
encoding: "utf-8"
|
|
1896
|
+
}).trim();
|
|
1897
|
+
} catch {
|
|
3324
1898
|
}
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
async function checkEslintDrift(repoId, projectPath) {
|
|
3334
|
-
try {
|
|
3335
|
-
const res = await apiGet(
|
|
3336
|
-
`/repos/${repoId}/eslint-config`
|
|
3337
|
-
);
|
|
3338
|
-
const configs = res.data ?? [];
|
|
3339
|
-
for (const config of configs) {
|
|
3340
|
-
if (!config.generated_hash) continue;
|
|
3341
|
-
const absPath = config.source_path === "." ? projectPath : join10(projectPath, config.source_path);
|
|
3342
|
-
const configPath = join10(absPath, "eslint.config.mjs");
|
|
3343
|
-
if (!await fileExists2(configPath)) continue;
|
|
3344
|
-
try {
|
|
3345
|
-
const content = await readFile11(configPath, "utf-8");
|
|
3346
|
-
const currentHash = hashConfig(content);
|
|
3347
|
-
if (currentHash !== config.generated_hash) {
|
|
3348
|
-
return true;
|
|
3349
|
-
}
|
|
3350
|
-
} catch {
|
|
3351
|
-
}
|
|
1899
|
+
const worktreeId = await resolveWorktreeId({
|
|
1900
|
+
repoId,
|
|
1901
|
+
repoPath: projectPath,
|
|
1902
|
+
branch,
|
|
1903
|
+
deviceId
|
|
1904
|
+
});
|
|
1905
|
+
if (worktreeId) {
|
|
1906
|
+
process.stdout.write(worktreeId);
|
|
3352
1907
|
}
|
|
3353
|
-
|
|
3354
|
-
} catch {
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
}
|
|
3358
|
-
async function runEslint() {
|
|
3359
|
-
const subcommand = process.argv[3];
|
|
3360
|
-
const flags = parseFlags(4);
|
|
3361
|
-
validateApiKey();
|
|
3362
|
-
const config = await resolveConfig(flags);
|
|
3363
|
-
const { repoId, projectPath } = config;
|
|
3364
|
-
switch (subcommand) {
|
|
3365
|
-
case "init":
|
|
3366
|
-
await eslintInit(repoId, projectPath);
|
|
3367
|
-
break;
|
|
3368
|
-
case "sync":
|
|
3369
|
-
await eslintSync(repoId, projectPath);
|
|
3370
|
-
break;
|
|
3371
|
-
default:
|
|
3372
|
-
console.log(`
|
|
3373
|
-
Usage:
|
|
3374
|
-
codebyplan eslint init Detect tech stack, resolve presets, generate eslint.config.mjs
|
|
3375
|
-
codebyplan eslint sync Regenerate if presets changed, detect drift
|
|
1908
|
+
process.exit(0);
|
|
1909
|
+
} catch (err) {
|
|
1910
|
+
if (process.env.CODEBYPLAN_DEBUG === "1") {
|
|
1911
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1912
|
+
process.stderr.write(`resolve-worktree: ${msg}
|
|
3376
1913
|
`);
|
|
3377
|
-
|
|
1914
|
+
}
|
|
1915
|
+
process.exit(0);
|
|
3378
1916
|
}
|
|
3379
1917
|
}
|
|
3380
|
-
var
|
|
3381
|
-
"src/cli/
|
|
1918
|
+
var init_resolve_worktree2 = __esm({
|
|
1919
|
+
"src/cli/resolve-worktree.ts"() {
|
|
3382
1920
|
"use strict";
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
init_tech_detect();
|
|
3387
|
-
init_eslint_generator();
|
|
1921
|
+
init_flags();
|
|
1922
|
+
init_local_config();
|
|
1923
|
+
init_resolve_worktree();
|
|
3388
1924
|
}
|
|
3389
1925
|
});
|
|
3390
1926
|
|
|
3391
|
-
// src/cli/
|
|
3392
|
-
var
|
|
3393
|
-
__export(
|
|
3394
|
-
|
|
1927
|
+
// src/cli/config.ts
|
|
1928
|
+
var config_exports = {};
|
|
1929
|
+
__export(config_exports, {
|
|
1930
|
+
runConfig: () => runConfig
|
|
3395
1931
|
});
|
|
3396
|
-
import {
|
|
3397
|
-
import {
|
|
3398
|
-
|
|
3399
|
-
function contentHash(content) {
|
|
3400
|
-
return createHash3("sha256").update(content).digest("hex");
|
|
3401
|
-
}
|
|
3402
|
-
async function runSync() {
|
|
1932
|
+
import { readFile as readFile7, writeFile as writeFile5 } from "node:fs/promises";
|
|
1933
|
+
import { join as join7 } from "node:path";
|
|
1934
|
+
async function runConfig() {
|
|
3403
1935
|
const flags = parseFlags(3);
|
|
3404
1936
|
const dryRun = hasFlag("dry-run", 3);
|
|
3405
|
-
const force = hasFlag("force", 3);
|
|
3406
|
-
const fix = hasFlag("fix", 3);
|
|
3407
1937
|
validateApiKey();
|
|
3408
1938
|
const config = await resolveConfig(flags);
|
|
3409
1939
|
const { repoId, projectPath } = config;
|
|
3410
1940
|
console.log(`
|
|
3411
|
-
CodeByPlan
|
|
1941
|
+
CodeByPlan Config`);
|
|
3412
1942
|
console.log(` Repo: ${repoId}`);
|
|
3413
1943
|
console.log(` Path: ${projectPath}`);
|
|
3414
1944
|
if (dryRun) console.log(` Mode: dry-run`);
|
|
3415
|
-
if (force) console.log(` Mode: force`);
|
|
3416
1945
|
console.log();
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
try {
|
|
3420
|
-
await apiPost("/sync/lock", {
|
|
3421
|
-
repo_id: repoId,
|
|
3422
|
-
locked_by: `cli-sync`,
|
|
3423
|
-
reason: "Bidirectional sync",
|
|
3424
|
-
ttl_minutes: 10
|
|
3425
|
-
});
|
|
3426
|
-
console.log(" Lock acquired.\n");
|
|
3427
|
-
} catch (lockErr) {
|
|
3428
|
-
const lockStatus = await apiGet("/sync/lock", { repo_id: repoId });
|
|
3429
|
-
if (lockStatus.data.locked && lockStatus.data.lock) {
|
|
3430
|
-
const lock = lockStatus.data.lock;
|
|
3431
|
-
console.log(
|
|
3432
|
-
` Sync locked by ${lock.locked_by} since ${lock.locked_at}.`
|
|
3433
|
-
);
|
|
3434
|
-
console.log(` Expires: ${lock.expires_at}`);
|
|
3435
|
-
console.log(` Use --force to override, or wait for lock to expire.
|
|
3436
|
-
`);
|
|
3437
|
-
if (!force) return;
|
|
3438
|
-
await apiPost("/sync/lock", {
|
|
3439
|
-
repo_id: repoId,
|
|
3440
|
-
locked_by: `cli-sync`,
|
|
3441
|
-
reason: "Bidirectional sync (forced)",
|
|
3442
|
-
ttl_minutes: 10
|
|
3443
|
-
});
|
|
3444
|
-
console.log(" Lock acquired (forced).\n");
|
|
3445
|
-
} else {
|
|
3446
|
-
throw lockErr;
|
|
3447
|
-
}
|
|
3448
|
-
}
|
|
3449
|
-
}
|
|
3450
|
-
try {
|
|
3451
|
-
await runSyncInner(repoId, projectPath, dryRun, force, fix);
|
|
3452
|
-
} finally {
|
|
3453
|
-
if (!dryRun) {
|
|
3454
|
-
try {
|
|
3455
|
-
await apiDelete("/sync/lock", { repo_id: repoId });
|
|
3456
|
-
} catch (err) {
|
|
3457
|
-
console.error(
|
|
3458
|
-
` Warning: failed to release sync lock: ${err instanceof Error ? err.message : String(err)}`
|
|
3459
|
-
);
|
|
3460
|
-
}
|
|
3461
|
-
}
|
|
3462
|
-
}
|
|
1946
|
+
await syncConfigToFile(repoId, projectPath, dryRun);
|
|
1947
|
+
console.log("\n Config complete.\n");
|
|
3463
1948
|
}
|
|
3464
|
-
async function
|
|
3465
|
-
|
|
3466
|
-
const claudeDir = join11(projectPath, ".claude");
|
|
3467
|
-
let localFiles = /* @__PURE__ */ new Map();
|
|
3468
|
-
try {
|
|
3469
|
-
localFiles = await scanLocalFiles(claudeDir, projectPath);
|
|
3470
|
-
} catch (err) {
|
|
3471
|
-
console.warn(
|
|
3472
|
-
` Local file scan incomplete: ${err instanceof Error ? err.message : String(err)}`
|
|
3473
|
-
);
|
|
3474
|
-
}
|
|
3475
|
-
const [defaultsRes, repoSyncRes, repoRes, , fileReposRes] = await Promise.all(
|
|
3476
|
-
[
|
|
3477
|
-
apiGet("/sync/defaults"),
|
|
3478
|
-
apiGet("/sync/files", { repo_id: repoId }),
|
|
3479
|
-
apiGet(`/repos/${repoId}`),
|
|
3480
|
-
apiGet("/sync/state", {
|
|
3481
|
-
repo_id: repoId
|
|
3482
|
-
}),
|
|
3483
|
-
apiGet("/sync/file-repos", {
|
|
3484
|
-
repo_id: repoId
|
|
3485
|
-
})
|
|
3486
|
-
]
|
|
3487
|
-
);
|
|
3488
|
-
const syncStartTime = Date.now();
|
|
3489
|
-
const repoData = repoRes.data;
|
|
3490
|
-
const remoteDefaults = flattenSyncData(defaultsRes.data);
|
|
3491
|
-
const remoteRepoFiles = flattenSyncData(repoSyncRes.data);
|
|
3492
|
-
const fileRepoHashes = /* @__PURE__ */ new Map();
|
|
3493
|
-
const fileRepoByClaudeFileId = /* @__PURE__ */ new Map();
|
|
3494
|
-
for (const entry of fileReposRes.data ?? []) {
|
|
3495
|
-
const baseKey = compositeKey(
|
|
3496
|
-
entry.file_type,
|
|
3497
|
-
entry.file_name,
|
|
3498
|
-
entry.file_category
|
|
3499
|
-
);
|
|
3500
|
-
const scopedKey = `${baseKey}:${entry.file_scope}`;
|
|
3501
|
-
fileRepoHashes.set(scopedKey, entry.last_synced_content_hash);
|
|
3502
|
-
if (!fileRepoHashes.has(baseKey)) {
|
|
3503
|
-
fileRepoHashes.set(baseKey, entry.last_synced_content_hash);
|
|
3504
|
-
}
|
|
3505
|
-
if (entry.claude_file_id) {
|
|
3506
|
-
fileRepoByClaudeFileId.set(
|
|
3507
|
-
entry.claude_file_id,
|
|
3508
|
-
entry.last_synced_content_hash
|
|
3509
|
-
);
|
|
3510
|
-
}
|
|
3511
|
-
}
|
|
3512
|
-
const remoteFiles = new Map([...remoteDefaults, ...remoteRepoFiles]);
|
|
3513
|
-
console.log(
|
|
3514
|
-
` Local: ${localFiles.size} files, Remote: ${remoteFiles.size} files
|
|
3515
|
-
`
|
|
3516
|
-
);
|
|
3517
|
-
const plan = [];
|
|
3518
|
-
const allKeys = /* @__PURE__ */ new Set([...localFiles.keys(), ...remoteFiles.keys()]);
|
|
3519
|
-
for (const key of allKeys) {
|
|
3520
|
-
const local = localFiles.get(key);
|
|
3521
|
-
const remote = remoteFiles.get(key);
|
|
3522
|
-
if (local && !remote) {
|
|
3523
|
-
plan.push({
|
|
3524
|
-
key,
|
|
3525
|
-
displayPath: `${local.type}/${local.category ? local.category + "/" : ""}${local.name}`,
|
|
3526
|
-
action: "push",
|
|
3527
|
-
recommended: "push",
|
|
3528
|
-
localContent: local.content,
|
|
3529
|
-
remoteContent: null,
|
|
3530
|
-
pushContent: reverseSubstituteVariables(local.content, repoData),
|
|
3531
|
-
filePath: getLocalFilePath(claudeDir, projectPath, {
|
|
3532
|
-
type: local.type,
|
|
3533
|
-
name: local.name,
|
|
3534
|
-
category: local.category
|
|
3535
|
-
}),
|
|
3536
|
-
type: local.type,
|
|
3537
|
-
name: local.name,
|
|
3538
|
-
category: local.category,
|
|
3539
|
-
scope: local.scope,
|
|
3540
|
-
isHook: local.type === "hook",
|
|
3541
|
-
claudeFileId: null
|
|
3542
|
-
});
|
|
3543
|
-
} else if (!local && remote) {
|
|
3544
|
-
const remoteScope = remote.scope ?? "shared";
|
|
3545
|
-
if (remoteScope.startsWith("local:") && remoteScope !== `local:${repoData.name}`) {
|
|
3546
|
-
continue;
|
|
3547
|
-
}
|
|
3548
|
-
const resolvedContent = substituteVariables(remote.content, repoData);
|
|
3549
|
-
const hadSyncedThisFile = remote.id ? fileRepoByClaudeFileId.has(remote.id) : fileRepoHashes.has(key);
|
|
3550
|
-
const recommended = hadSyncedThisFile ? "delete" : "pull";
|
|
3551
|
-
plan.push({
|
|
3552
|
-
key,
|
|
3553
|
-
displayPath: `${remote.type}/${remote.category ? remote.category + "/" : ""}${remote.name}`,
|
|
3554
|
-
action: recommended,
|
|
3555
|
-
recommended,
|
|
3556
|
-
localContent: null,
|
|
3557
|
-
remoteContent: resolvedContent,
|
|
3558
|
-
pushContent: null,
|
|
3559
|
-
filePath: getLocalFilePath(claudeDir, projectPath, remote),
|
|
3560
|
-
type: remote.type,
|
|
3561
|
-
name: remote.name,
|
|
3562
|
-
category: remote.category ?? null,
|
|
3563
|
-
scope: remote.scope ?? "shared",
|
|
3564
|
-
isHook: remote.type === "hook",
|
|
3565
|
-
claudeFileId: remote.id ?? null
|
|
3566
|
-
});
|
|
3567
|
-
} else if (local && remote) {
|
|
3568
|
-
const remoteScope = remote.scope ?? "shared";
|
|
3569
|
-
if (remoteScope.startsWith("local:") && remoteScope !== `local:${repoData.name}`) {
|
|
3570
|
-
continue;
|
|
3571
|
-
}
|
|
3572
|
-
const resolvedRemote = substituteVariables(remote.content, repoData);
|
|
3573
|
-
if (local.content === resolvedRemote) {
|
|
3574
|
-
continue;
|
|
3575
|
-
}
|
|
3576
|
-
const localHash = contentHash(local.content);
|
|
3577
|
-
const scopedKey = `${key}:${local.scope}`;
|
|
3578
|
-
const lastSyncedHash = fileRepoHashes.get(scopedKey) ?? fileRepoHashes.get(key) ?? null;
|
|
3579
|
-
const localChanged = lastSyncedHash ? localHash !== lastSyncedHash : true;
|
|
3580
|
-
let action;
|
|
3581
|
-
if (force) {
|
|
3582
|
-
action = "pull";
|
|
3583
|
-
} else if (!localChanged) {
|
|
3584
|
-
action = "pull";
|
|
3585
|
-
} else if (lastSyncedHash === null) {
|
|
3586
|
-
action = "conflict";
|
|
3587
|
-
} else {
|
|
3588
|
-
const remoteResolvedHash = contentHash(resolvedRemote);
|
|
3589
|
-
const remoteChanged = remoteResolvedHash !== lastSyncedHash;
|
|
3590
|
-
if (remoteChanged) {
|
|
3591
|
-
action = "conflict";
|
|
3592
|
-
} else {
|
|
3593
|
-
action = "push";
|
|
3594
|
-
}
|
|
3595
|
-
}
|
|
3596
|
-
plan.push({
|
|
3597
|
-
key,
|
|
3598
|
-
displayPath: `${local.type}/${local.category ? local.category + "/" : ""}${local.name}`,
|
|
3599
|
-
action,
|
|
3600
|
-
recommended: action === "conflict" ? "pull" : action,
|
|
3601
|
-
localContent: local.content,
|
|
3602
|
-
remoteContent: resolvedRemote,
|
|
3603
|
-
pushContent: reverseSubstituteVariables(local.content, repoData),
|
|
3604
|
-
filePath: getLocalFilePath(claudeDir, projectPath, remote),
|
|
3605
|
-
type: local.type,
|
|
3606
|
-
name: local.name,
|
|
3607
|
-
category: local.category,
|
|
3608
|
-
scope: local.scope,
|
|
3609
|
-
isHook: local.type === "hook",
|
|
3610
|
-
claudeFileId: remote.id ?? null
|
|
3611
|
-
});
|
|
3612
|
-
}
|
|
3613
|
-
}
|
|
3614
|
-
const pulls = plan.filter((p) => p.action === "pull");
|
|
3615
|
-
const pushes = plan.filter((p) => p.action === "push");
|
|
3616
|
-
const conflicts = plan.filter((p) => p.action === "conflict");
|
|
3617
|
-
const contentPulls = pulls.filter((p) => p.localContent !== null);
|
|
3618
|
-
const dbOnlyPull = plan.filter(
|
|
3619
|
-
(p) => p.localContent === null && p.action === "pull"
|
|
3620
|
-
);
|
|
3621
|
-
const dbOnlyDelete = plan.filter(
|
|
3622
|
-
(p) => p.localContent === null && p.action === "delete"
|
|
3623
|
-
);
|
|
3624
|
-
if (contentPulls.length > 0) {
|
|
3625
|
-
console.log(` Pull (DB \u2192 local): ${contentPulls.length}`);
|
|
3626
|
-
for (const p of contentPulls) console.log(` \u2193 ${p.displayPath}`);
|
|
3627
|
-
}
|
|
3628
|
-
if (pushes.length > 0) {
|
|
3629
|
-
console.log(` Push (local \u2192 DB): ${pushes.length}`);
|
|
3630
|
-
for (const p of pushes) console.log(` \u2191 ${p.displayPath}`);
|
|
3631
|
-
}
|
|
3632
|
-
if (dbOnlyPull.length > 0) {
|
|
3633
|
-
console.log(`
|
|
3634
|
-
DB-only (new, will pull): ${dbOnlyPull.length}`);
|
|
3635
|
-
for (const p of dbOnlyPull) console.log(` \u2193 ${p.displayPath}`);
|
|
3636
|
-
}
|
|
3637
|
-
if (dbOnlyDelete.length > 0) {
|
|
3638
|
-
console.log(
|
|
3639
|
-
`
|
|
3640
|
-
DB-only (previously synced, will delete): ${dbOnlyDelete.length}`
|
|
3641
|
-
);
|
|
3642
|
-
for (const p of dbOnlyDelete) console.log(` \u2715 ${p.displayPath}`);
|
|
3643
|
-
}
|
|
3644
|
-
if (conflicts.length > 0) {
|
|
3645
|
-
console.log(`
|
|
3646
|
-
Conflicts (both sides changed): ${conflicts.length}`);
|
|
3647
|
-
for (const p of conflicts) console.log(` \u26A0 ${p.displayPath}`);
|
|
3648
|
-
}
|
|
3649
|
-
if (contentPulls.length === 0 && pushes.length === 0 && dbOnlyPull.length === 0 && dbOnlyDelete.length === 0 && conflicts.length === 0) {
|
|
3650
|
-
console.log(" All .claude/ files in sync.");
|
|
3651
|
-
}
|
|
3652
|
-
if (plan.length > 0 && !dryRun) {
|
|
3653
|
-
if (!force) {
|
|
3654
|
-
const agreed = await confirmProceed(`
|
|
3655
|
-
Agree with sync? [Y/n] `);
|
|
3656
|
-
if (!agreed) {
|
|
3657
|
-
const mode = await promptReviewMode();
|
|
3658
|
-
const contentProvider = {
|
|
3659
|
-
local: (p) => p.localContent,
|
|
3660
|
-
remote: (p) => p.remoteContent
|
|
3661
|
-
};
|
|
3662
|
-
if (mode === "file") {
|
|
3663
|
-
const actions = await reviewFilesOneByOne(
|
|
3664
|
-
plan,
|
|
3665
|
-
(p) => p.displayPath,
|
|
3666
|
-
(p) => p.action,
|
|
3667
|
-
(p) => p.recommended,
|
|
3668
|
-
contentProvider
|
|
3669
|
-
);
|
|
3670
|
-
for (let i = 0; i < plan.length; i++) {
|
|
3671
|
-
plan[i].action = actions[i];
|
|
3672
|
-
}
|
|
3673
|
-
} else {
|
|
3674
|
-
const groups = groupByType(plan);
|
|
3675
|
-
for (const [typeName, items] of groups) {
|
|
3676
|
-
const actions = await reviewFolder(
|
|
3677
|
-
typeName,
|
|
3678
|
-
items,
|
|
3679
|
-
(p) => p.displayPath,
|
|
3680
|
-
(p) => p.action,
|
|
3681
|
-
(p) => p.recommended,
|
|
3682
|
-
contentProvider
|
|
3683
|
-
);
|
|
3684
|
-
for (let i = 0; i < items.length; i++) {
|
|
3685
|
-
items[i].action = actions[i];
|
|
3686
|
-
}
|
|
3687
|
-
}
|
|
3688
|
-
}
|
|
3689
|
-
}
|
|
3690
|
-
}
|
|
3691
|
-
const toPull = plan.filter((p) => p.action === "pull");
|
|
3692
|
-
const toPush = plan.filter((p) => p.action === "push");
|
|
3693
|
-
const toDelete = plan.filter((p) => p.action === "delete");
|
|
3694
|
-
const skipped = plan.filter((p) => p.action === "skip");
|
|
3695
|
-
if (toPull.length + toPush.length + toDelete.length === 0) {
|
|
3696
|
-
console.log("\n All items skipped \u2014 no changes applied.");
|
|
3697
|
-
} else {
|
|
3698
|
-
for (const p of toPull) {
|
|
3699
|
-
if (p.filePath && p.remoteContent !== null) {
|
|
3700
|
-
await mkdir2(dirname2(p.filePath), { recursive: true });
|
|
3701
|
-
await writeFile7(p.filePath, p.remoteContent, "utf-8");
|
|
3702
|
-
if (p.isHook) await chmod2(p.filePath, 493);
|
|
3703
|
-
}
|
|
3704
|
-
}
|
|
3705
|
-
const toUpsert = toPush.filter((p) => p.pushContent !== null).map((p) => ({
|
|
3706
|
-
type: p.type,
|
|
3707
|
-
name: p.name,
|
|
3708
|
-
category: p.category,
|
|
3709
|
-
content: p.pushContent,
|
|
3710
|
-
scope: p.scope
|
|
3711
|
-
}));
|
|
3712
|
-
if (toUpsert.length > 0) {
|
|
3713
|
-
await apiPost("/sync/files", {
|
|
3714
|
-
repo_id: repoId,
|
|
3715
|
-
files: toUpsert,
|
|
3716
|
-
changed_by_repo_id: repoId
|
|
3717
|
-
});
|
|
3718
|
-
}
|
|
3719
|
-
if (toDelete.length > 0) {
|
|
3720
|
-
const deleteKeys = toDelete.map((p) => ({
|
|
3721
|
-
type: p.type,
|
|
3722
|
-
name: p.name,
|
|
3723
|
-
category: p.category
|
|
3724
|
-
}));
|
|
3725
|
-
await apiPost("/sync/files", {
|
|
3726
|
-
repo_id: repoId,
|
|
3727
|
-
delete_keys: deleteKeys
|
|
3728
|
-
});
|
|
3729
|
-
for (const p of toDelete) {
|
|
3730
|
-
if (p.filePath) {
|
|
3731
|
-
try {
|
|
3732
|
-
await unlink2(p.filePath);
|
|
3733
|
-
} catch (err) {
|
|
3734
|
-
if (err instanceof Error && "code" in err && err.code !== "ENOENT") {
|
|
3735
|
-
console.error(
|
|
3736
|
-
` Warning: failed to delete ${p.filePath}: ${err.message}`
|
|
3737
|
-
);
|
|
3738
|
-
}
|
|
3739
|
-
}
|
|
3740
|
-
}
|
|
3741
|
-
}
|
|
3742
|
-
}
|
|
3743
|
-
const syncDurationMs = Date.now() - syncStartTime;
|
|
3744
|
-
await apiPost("/sync/state", {
|
|
3745
|
-
repo_id: repoId,
|
|
3746
|
-
last_synced_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3747
|
-
was_skipped: skipped.length > 0,
|
|
3748
|
-
files_synced_count: toPull.length + toPush.length + toDelete.length,
|
|
3749
|
-
files_pushed: toPush.length,
|
|
3750
|
-
files_pulled: toPull.length,
|
|
3751
|
-
files_deleted: toDelete.length,
|
|
3752
|
-
files_skipped: skipped.length,
|
|
3753
|
-
sync_duration_ms: syncDurationMs,
|
|
3754
|
-
sync_version: getSyncVersion()
|
|
3755
|
-
});
|
|
3756
|
-
const syncTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3757
|
-
const fileRepoUpdates = [];
|
|
3758
|
-
for (const p of toPull) {
|
|
3759
|
-
if (p.remoteContent !== null) {
|
|
3760
|
-
fileRepoUpdates.push({
|
|
3761
|
-
claude_file_id: p.claudeFileId ?? void 0,
|
|
3762
|
-
file_type: p.type,
|
|
3763
|
-
file_name: p.name,
|
|
3764
|
-
file_category: p.category,
|
|
3765
|
-
file_scope: p.scope,
|
|
3766
|
-
last_synced_at: syncTimestamp,
|
|
3767
|
-
last_synced_content_hash: contentHash(p.remoteContent),
|
|
3768
|
-
sync_status: "synced"
|
|
3769
|
-
});
|
|
3770
|
-
}
|
|
3771
|
-
}
|
|
3772
|
-
for (const p of toPush) {
|
|
3773
|
-
if (p.localContent !== null) {
|
|
3774
|
-
fileRepoUpdates.push({
|
|
3775
|
-
claude_file_id: p.claudeFileId ?? void 0,
|
|
3776
|
-
file_type: p.type,
|
|
3777
|
-
file_name: p.name,
|
|
3778
|
-
file_category: p.category,
|
|
3779
|
-
file_scope: p.scope,
|
|
3780
|
-
last_synced_at: syncTimestamp,
|
|
3781
|
-
last_synced_content_hash: contentHash(p.localContent),
|
|
3782
|
-
sync_status: "synced"
|
|
3783
|
-
});
|
|
3784
|
-
}
|
|
3785
|
-
}
|
|
3786
|
-
if (fileRepoUpdates.length > 0) {
|
|
3787
|
-
try {
|
|
3788
|
-
await apiPost("/sync/file-repos", {
|
|
3789
|
-
repo_id: repoId,
|
|
3790
|
-
file_repos: fileRepoUpdates
|
|
3791
|
-
});
|
|
3792
|
-
} catch (err) {
|
|
3793
|
-
console.warn(
|
|
3794
|
-
` Warning: failed to update file-repo tracking for ${fileRepoUpdates.length} files: ${err instanceof Error ? err.message : String(err)}`
|
|
3795
|
-
);
|
|
3796
|
-
}
|
|
3797
|
-
}
|
|
3798
|
-
console.log(
|
|
3799
|
-
`
|
|
3800
|
-
Applied: ${toPull.length} pulled, ${toPush.length} pushed, ${toDelete.length} deleted` + (skipped.length > 0 ? `, ${skipped.length} skipped` : "")
|
|
3801
|
-
);
|
|
3802
|
-
}
|
|
3803
|
-
const unresolvedConflicts = plan.filter(
|
|
3804
|
-
(p) => p.action === "conflict" || p.action === "skip" && p.localContent !== null && p.remoteContent !== null
|
|
3805
|
-
);
|
|
3806
|
-
if (unresolvedConflicts.length > 0) {
|
|
3807
|
-
let stored = 0;
|
|
3808
|
-
for (const p of unresolvedConflicts) {
|
|
3809
|
-
try {
|
|
3810
|
-
await apiPost("/sync/conflicts", {
|
|
3811
|
-
repo_id: repoId,
|
|
3812
|
-
claude_file_id: p.claudeFileId ?? void 0,
|
|
3813
|
-
file_type: p.type,
|
|
3814
|
-
file_name: p.name,
|
|
3815
|
-
file_category: p.category,
|
|
3816
|
-
file_scope: p.scope,
|
|
3817
|
-
conflict_type: "both_modified",
|
|
3818
|
-
local_content: p.localContent,
|
|
3819
|
-
remote_content: p.remoteContent
|
|
3820
|
-
});
|
|
3821
|
-
stored++;
|
|
3822
|
-
} catch (err) {
|
|
3823
|
-
console.error(`Failed to store conflict for ${p.displayPath}:`, err);
|
|
3824
|
-
}
|
|
3825
|
-
}
|
|
3826
|
-
if (stored > 0) {
|
|
3827
|
-
console.log(
|
|
3828
|
-
`
|
|
3829
|
-
${stored} conflict(s) stored in DB for later resolution.`
|
|
3830
|
-
);
|
|
3831
|
-
}
|
|
3832
|
-
}
|
|
3833
|
-
} else if (dryRun) {
|
|
3834
|
-
console.log("\n (dry-run \u2014 no changes)");
|
|
3835
|
-
}
|
|
3836
|
-
console.log("\n Settings sync...");
|
|
3837
|
-
await syncSettings(
|
|
3838
|
-
claudeDir,
|
|
3839
|
-
projectPath,
|
|
3840
|
-
defaultsRes.data,
|
|
3841
|
-
repoData,
|
|
3842
|
-
dryRun
|
|
3843
|
-
);
|
|
3844
|
-
console.log(" Config sync...");
|
|
3845
|
-
await syncConfig(repoId, projectPath, dryRun);
|
|
3846
|
-
console.log(" Tech stack...");
|
|
3847
|
-
await syncTechStack(repoId, projectPath, dryRun);
|
|
3848
|
-
console.log(" ESLint config...");
|
|
3849
|
-
await syncEslintDriftCheck(repoId, projectPath);
|
|
3850
|
-
console.log(" Port verification...");
|
|
3851
|
-
await syncPortVerification(repoId, projectPath, dryRun, fix);
|
|
3852
|
-
console.log("\n Sync complete.\n");
|
|
3853
|
-
}
|
|
3854
|
-
async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun) {
|
|
3855
|
-
const settingsPath = join11(claudeDir, "settings.json");
|
|
3856
|
-
const globalSettingsFiles = syncData.global_settings ?? [];
|
|
3857
|
-
let globalSettings = {};
|
|
3858
|
-
for (const gf of globalSettingsFiles) {
|
|
3859
|
-
const parsed = JSON.parse(
|
|
3860
|
-
substituteVariables(gf.content, repoData)
|
|
3861
|
-
);
|
|
3862
|
-
globalSettings = { ...globalSettings, ...parsed };
|
|
3863
|
-
}
|
|
3864
|
-
const repoSettingsFiles = syncData.settings ?? [];
|
|
3865
|
-
let repoSettings = {};
|
|
3866
|
-
for (const rf of repoSettingsFiles) {
|
|
3867
|
-
repoSettings = JSON.parse(
|
|
3868
|
-
substituteVariables(rf.content, repoData)
|
|
3869
|
-
);
|
|
3870
|
-
}
|
|
3871
|
-
const combinedTemplate = mergeGlobalAndRepoSettings(
|
|
3872
|
-
globalSettings,
|
|
3873
|
-
repoSettings
|
|
3874
|
-
);
|
|
3875
|
-
const hooksDir = join11(projectPath, ".claude", "hooks");
|
|
3876
|
-
const discovered = await discoverHooks(hooksDir);
|
|
3877
|
-
let localSettings = {};
|
|
3878
|
-
try {
|
|
3879
|
-
const raw = await readFile12(settingsPath, "utf-8");
|
|
3880
|
-
localSettings = JSON.parse(raw);
|
|
3881
|
-
} catch {
|
|
3882
|
-
}
|
|
3883
|
-
let merged = Object.keys(localSettings).length > 0 ? mergeSettings(combinedTemplate, localSettings) : combinedTemplate;
|
|
3884
|
-
merged = stripPermissionsAllow(merged);
|
|
3885
|
-
if (discovered.size > 0) {
|
|
3886
|
-
merged.hooks = mergeDiscoveredHooks(
|
|
3887
|
-
merged.hooks ?? {},
|
|
3888
|
-
discovered
|
|
3889
|
-
);
|
|
3890
|
-
}
|
|
3891
|
-
const mergedContent = JSON.stringify(merged, null, 2) + "\n";
|
|
3892
|
-
let currentContent = "";
|
|
3893
|
-
try {
|
|
3894
|
-
currentContent = await readFile12(settingsPath, "utf-8");
|
|
3895
|
-
} catch {
|
|
3896
|
-
}
|
|
3897
|
-
if (currentContent === mergedContent) {
|
|
3898
|
-
console.log(" Settings up to date.");
|
|
3899
|
-
return;
|
|
3900
|
-
}
|
|
3901
|
-
if (dryRun) {
|
|
3902
|
-
console.log(" Settings would be updated (dry-run).");
|
|
3903
|
-
return;
|
|
3904
|
-
}
|
|
3905
|
-
await mkdir2(dirname2(settingsPath), { recursive: true });
|
|
3906
|
-
await writeFile7(settingsPath, mergedContent, "utf-8");
|
|
3907
|
-
console.log(" Updated settings.json");
|
|
3908
|
-
}
|
|
3909
|
-
async function syncConfig(repoId, projectPath, dryRun) {
|
|
3910
|
-
const configPath = join11(projectPath, ".codebyplan.json");
|
|
1949
|
+
async function syncConfigToFile(repoId, projectPath, dryRun) {
|
|
1950
|
+
const configPath = join7(projectPath, ".codebyplan.json");
|
|
3911
1951
|
let currentConfig = {};
|
|
3912
1952
|
try {
|
|
3913
|
-
const raw = await
|
|
1953
|
+
const raw = await readFile7(configPath, "utf-8");
|
|
3914
1954
|
currentConfig = JSON.parse(raw);
|
|
3915
1955
|
} catch {
|
|
3916
1956
|
currentConfig = { repo_id: repoId };
|
|
3917
1957
|
}
|
|
3918
|
-
if (dryRun) {
|
|
3919
|
-
try {
|
|
3920
|
-
if (await needsLocalMigration(projectPath)) {
|
|
3921
|
-
console.log(
|
|
3922
|
-
` Would migrate .codebyplan.json -> worktree_id to .codebyplan.local.json (dry-run, skipping actual write).`
|
|
3923
|
-
);
|
|
3924
|
-
}
|
|
3925
|
-
} catch {
|
|
3926
|
-
}
|
|
3927
|
-
} else {
|
|
3928
|
-
try {
|
|
3929
|
-
if (await needsLocalMigration(projectPath)) {
|
|
3930
|
-
const result = await runLocalMigration(projectPath);
|
|
3931
|
-
delete currentConfig.worktree_id;
|
|
3932
|
-
console.log(
|
|
3933
|
-
` Migrated .codebyplan.json -> moved worktree_id to gitignored .codebyplan.local.json (device_id=${result.device_id.slice(0, 8)})`
|
|
3934
|
-
);
|
|
3935
|
-
console.log(
|
|
3936
|
-
` Suggest /cbp-git-commit to stage the cleaned shared file.`
|
|
3937
|
-
);
|
|
3938
|
-
}
|
|
3939
|
-
} catch (err) {
|
|
3940
|
-
console.warn(
|
|
3941
|
-
` Warning: local migration failed (continuing): ${err instanceof Error ? err.message : String(err)}`
|
|
3942
|
-
);
|
|
3943
|
-
}
|
|
3944
|
-
}
|
|
3945
1958
|
let resolvedWorktreeId;
|
|
3946
1959
|
try {
|
|
3947
1960
|
const deviceId = await getOrCreateDeviceId(projectPath);
|
|
@@ -4033,68 +2046,194 @@ async function syncConfig(repoId, projectPath, dryRun) {
|
|
|
4033
2046
|
const currentJson = JSON.stringify(currentConfig, null, 2);
|
|
4034
2047
|
const newJson = JSON.stringify(newConfig, null, 2);
|
|
4035
2048
|
if (currentJson === newJson) {
|
|
4036
|
-
console.log("
|
|
2049
|
+
console.log(" Config up to date.");
|
|
4037
2050
|
return;
|
|
4038
2051
|
}
|
|
4039
2052
|
if (dryRun) {
|
|
4040
|
-
console.log("
|
|
2053
|
+
console.log(" Config would be updated (dry-run).");
|
|
4041
2054
|
return;
|
|
4042
2055
|
}
|
|
4043
|
-
await
|
|
4044
|
-
console.log("
|
|
2056
|
+
await writeFile5(configPath, newJson + "\n", "utf-8");
|
|
2057
|
+
console.log(" Updated .codebyplan.json");
|
|
4045
2058
|
}
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
2059
|
+
var init_config = __esm({
|
|
2060
|
+
"src/cli/config.ts"() {
|
|
2061
|
+
"use strict";
|
|
2062
|
+
init_flags();
|
|
2063
|
+
init_api();
|
|
2064
|
+
init_resolve_worktree();
|
|
2065
|
+
init_local_config();
|
|
2066
|
+
}
|
|
2067
|
+
});
|
|
2068
|
+
|
|
2069
|
+
// src/lib/server-detect.ts
|
|
2070
|
+
function detectFramework(pkg) {
|
|
2071
|
+
const deps = pkg.dependencies ?? {};
|
|
2072
|
+
const devDeps = pkg.devDependencies ?? {};
|
|
2073
|
+
const hasDep = (name) => name in deps || name in devDeps;
|
|
2074
|
+
if (hasDep("next")) return "nextjs";
|
|
2075
|
+
if (hasDep("@tauri-apps/api") || hasDep("@tauri-apps/cli")) return "tauri";
|
|
2076
|
+
if (hasDep("expo")) return "expo";
|
|
2077
|
+
if (hasDep("vite")) return "vite";
|
|
2078
|
+
if (hasDep("express")) return "express";
|
|
2079
|
+
if (hasDep("@nestjs/core")) return "nestjs";
|
|
2080
|
+
return "custom";
|
|
2081
|
+
}
|
|
2082
|
+
function detectPortFromScripts(pkg) {
|
|
2083
|
+
const scripts = pkg.scripts;
|
|
2084
|
+
if (!scripts?.dev) return null;
|
|
2085
|
+
const parts = scripts.dev.split(/\s+/);
|
|
2086
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
2087
|
+
if (parts[i] === "--port" || parts[i] === "-p") {
|
|
2088
|
+
const next = parts[i + 1];
|
|
2089
|
+
if (next) {
|
|
2090
|
+
const port = parseInt(next, 10);
|
|
2091
|
+
if (!isNaN(port)) return port;
|
|
2092
|
+
}
|
|
4052
2093
|
}
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
2094
|
+
}
|
|
2095
|
+
return null;
|
|
2096
|
+
}
|
|
2097
|
+
var init_server_detect = __esm({
|
|
2098
|
+
"src/lib/server-detect.ts"() {
|
|
2099
|
+
"use strict";
|
|
2100
|
+
}
|
|
2101
|
+
});
|
|
2102
|
+
|
|
2103
|
+
// src/lib/port-verify.ts
|
|
2104
|
+
import { readFile as readFile8 } from "node:fs/promises";
|
|
2105
|
+
async function verifyPorts(projectPath, portAllocations) {
|
|
2106
|
+
const mismatches = [];
|
|
2107
|
+
const allocatedPorts = new Set(portAllocations.map((a) => a.port));
|
|
2108
|
+
const packageJsonPaths = await findPackageJsonFiles(projectPath, projectPath);
|
|
2109
|
+
for (const pkgPath of packageJsonPaths) {
|
|
2110
|
+
try {
|
|
2111
|
+
const raw = await readFile8(pkgPath, "utf-8");
|
|
2112
|
+
const pkg = JSON.parse(raw);
|
|
2113
|
+
const scriptPort = detectPortFromScripts(pkg);
|
|
2114
|
+
if (scriptPort !== null && !allocatedPorts.has(scriptPort)) {
|
|
2115
|
+
const relativePath = pkgPath.replace(projectPath + "/", "");
|
|
2116
|
+
const matchingAlloc = portAllocations.find(
|
|
2117
|
+
(a) => a.label === getAppLabel(relativePath)
|
|
4062
2118
|
);
|
|
2119
|
+
mismatches.push({
|
|
2120
|
+
packageJsonPath: relativePath,
|
|
2121
|
+
scriptPort,
|
|
2122
|
+
allocation: matchingAlloc ?? null,
|
|
2123
|
+
reason: matchingAlloc ? `Script uses port ${scriptPort} but allocation has port ${matchingAlloc.port}` : `Port ${scriptPort} in scripts is not in any allocation`
|
|
2124
|
+
});
|
|
4063
2125
|
}
|
|
2126
|
+
} catch {
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
return mismatches;
|
|
2130
|
+
}
|
|
2131
|
+
function isDevServerScript(pkg) {
|
|
2132
|
+
const scripts = pkg.scripts;
|
|
2133
|
+
const raw = scripts?.dev;
|
|
2134
|
+
if (!raw || typeof raw !== "string") return false;
|
|
2135
|
+
const script = raw.trim().toLowerCase();
|
|
2136
|
+
if (!script) return false;
|
|
2137
|
+
for (const pattern of DEV_SERVER_BIN_PATTERNS) {
|
|
2138
|
+
if (pattern.test(script)) return true;
|
|
2139
|
+
}
|
|
2140
|
+
const tokens = script.split(/\s+/);
|
|
2141
|
+
for (const token of tokens) {
|
|
2142
|
+
if (token === "--port" || token === "-p") return true;
|
|
2143
|
+
if (token.startsWith("--port=")) return true;
|
|
2144
|
+
}
|
|
2145
|
+
return false;
|
|
2146
|
+
}
|
|
2147
|
+
function labelMatchesAppName(label, appName) {
|
|
2148
|
+
if (!label || !appName) return false;
|
|
2149
|
+
const normalize = (s) => s.toLowerCase().replace(/-/g, " ").replace(/[()]/g, " ").replace(/\s+/g, " ").trim();
|
|
2150
|
+
const labelTokens = normalize(label).split(" ").filter(Boolean);
|
|
2151
|
+
const appToken = normalize(appName);
|
|
2152
|
+
if (!appToken) return false;
|
|
2153
|
+
const appTokens = appToken.split(" ").filter(Boolean);
|
|
2154
|
+
if (appTokens.length === 1) {
|
|
2155
|
+
return labelTokens.includes(appTokens[0]);
|
|
2156
|
+
}
|
|
2157
|
+
for (let i = 0; i <= labelTokens.length - appTokens.length; i++) {
|
|
2158
|
+
if (appTokens.every((t, j) => labelTokens[i + j] === t)) return true;
|
|
2159
|
+
}
|
|
2160
|
+
return false;
|
|
2161
|
+
}
|
|
2162
|
+
async function findUnallocatedApps(projectPath, portAllocations) {
|
|
2163
|
+
const apps = await discoverMonorepoApps(projectPath);
|
|
2164
|
+
if (apps.length === 0) {
|
|
2165
|
+
return [];
|
|
2166
|
+
}
|
|
2167
|
+
const unallocated = [];
|
|
2168
|
+
for (const app of apps) {
|
|
2169
|
+
if (portAllocations.some((a) => labelMatchesAppName(a.label ?? "", app.name))) {
|
|
2170
|
+
continue;
|
|
4064
2171
|
}
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
const
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
console.log(` ${added.length} new tech entries`);
|
|
4072
|
-
if (!dryRun) {
|
|
4073
|
-
await apiPut(`/repos/${repoId}`, { tech_stack: merged });
|
|
4074
|
-
}
|
|
4075
|
-
}
|
|
2172
|
+
let pkg;
|
|
2173
|
+
try {
|
|
2174
|
+
const raw = await readFile8(`${app.absPath}/package.json`, "utf-8");
|
|
2175
|
+
pkg = JSON.parse(raw);
|
|
2176
|
+
} catch {
|
|
2177
|
+
continue;
|
|
4076
2178
|
}
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
2179
|
+
if (!isDevServerScript(pkg)) continue;
|
|
2180
|
+
const framework = detectFramework(pkg);
|
|
2181
|
+
const detectedPort = detectPortFromScripts(pkg);
|
|
2182
|
+
const command = `pnpm --filter ${app.name} dev`;
|
|
2183
|
+
unallocated.push({
|
|
2184
|
+
name: app.name,
|
|
2185
|
+
path: app.path,
|
|
2186
|
+
framework,
|
|
2187
|
+
detectedPort,
|
|
2188
|
+
command
|
|
2189
|
+
});
|
|
4081
2190
|
}
|
|
2191
|
+
return unallocated;
|
|
4082
2192
|
}
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
console.log(
|
|
4088
|
-
" ESLint config drift detected. Run `codebyplan eslint sync` to update."
|
|
4089
|
-
);
|
|
4090
|
-
} else {
|
|
4091
|
-
console.log(" ESLint configs up to date.");
|
|
4092
|
-
}
|
|
4093
|
-
} catch (error) {
|
|
4094
|
-
console.warn(" ESLint drift check skipped:", error);
|
|
2193
|
+
function getAppLabel(relativePath) {
|
|
2194
|
+
const parts = relativePath.split("/");
|
|
2195
|
+
if (parts.length >= 3 && parts[0] === "apps") {
|
|
2196
|
+
return parts[1];
|
|
4095
2197
|
}
|
|
2198
|
+
return "root";
|
|
4096
2199
|
}
|
|
4097
|
-
|
|
2200
|
+
var DEV_SERVER_BIN_PATTERNS;
|
|
2201
|
+
var init_port_verify = __esm({
|
|
2202
|
+
"src/lib/port-verify.ts"() {
|
|
2203
|
+
"use strict";
|
|
2204
|
+
init_tech_detect();
|
|
2205
|
+
init_server_detect();
|
|
2206
|
+
DEV_SERVER_BIN_PATTERNS = [
|
|
2207
|
+
/\bnext\s+dev\b/,
|
|
2208
|
+
/\bnest\s+start\b/,
|
|
2209
|
+
/\bvite\s+(?:dev|serve)\b/,
|
|
2210
|
+
/\bvite\s+preview\b/,
|
|
2211
|
+
/\bnuxt\s+dev\b/,
|
|
2212
|
+
/\b(?:svelte-kit|sveltekit)\s+dev\b/,
|
|
2213
|
+
/\bexpo\s+start\b/
|
|
2214
|
+
];
|
|
2215
|
+
}
|
|
2216
|
+
});
|
|
2217
|
+
|
|
2218
|
+
// src/cli/ports.ts
|
|
2219
|
+
var ports_exports = {};
|
|
2220
|
+
__export(ports_exports, {
|
|
2221
|
+
runPorts: () => runPorts
|
|
2222
|
+
});
|
|
2223
|
+
async function runPorts() {
|
|
2224
|
+
const flags = parseFlags(3);
|
|
2225
|
+
const dryRun = hasFlag("dry-run", 3);
|
|
2226
|
+
const fix = hasFlag("fix", 3);
|
|
2227
|
+
validateApiKey();
|
|
2228
|
+
const config = await resolveConfig(flags);
|
|
2229
|
+
const { repoId, projectPath } = config;
|
|
2230
|
+
console.log(`
|
|
2231
|
+
CodeByPlan Ports`);
|
|
2232
|
+
console.log(` Repo: ${repoId}`);
|
|
2233
|
+
console.log(` Path: ${projectPath}`);
|
|
2234
|
+
if (dryRun) console.log(` Mode: dry-run`);
|
|
2235
|
+
if (fix) console.log(` Mode: fix`);
|
|
2236
|
+
console.log();
|
|
4098
2237
|
try {
|
|
4099
2238
|
const portsRes = await apiGet(
|
|
4100
2239
|
`/port-allocations`,
|
|
@@ -4102,22 +2241,23 @@ async function syncPortVerification(repoId, projectPath, dryRun, fix) {
|
|
|
4102
2241
|
);
|
|
4103
2242
|
const allocations = portsRes.data ?? [];
|
|
4104
2243
|
if (allocations.length === 0) {
|
|
4105
|
-
console.log("
|
|
2244
|
+
console.log(" No port allocations found \u2014 skipping verification.");
|
|
2245
|
+
console.log("\n Ports complete.\n");
|
|
4106
2246
|
return;
|
|
4107
2247
|
}
|
|
4108
2248
|
const mismatches = await verifyPorts(projectPath, allocations);
|
|
4109
2249
|
if (mismatches.length > 0) {
|
|
4110
|
-
console.log(`
|
|
2250
|
+
console.log(` Port mismatches: ${mismatches.length}`);
|
|
4111
2251
|
for (const m of mismatches) {
|
|
4112
|
-
console.log(`
|
|
2252
|
+
console.log(` ! ${m.packageJsonPath}: ${m.reason}`);
|
|
4113
2253
|
}
|
|
4114
2254
|
}
|
|
4115
2255
|
const unallocated = await findUnallocatedApps(projectPath, allocations);
|
|
4116
2256
|
if (unallocated.length > 0) {
|
|
4117
|
-
console.log(`
|
|
2257
|
+
console.log(` Unallocated apps: ${unallocated.length}`);
|
|
4118
2258
|
for (const app of unallocated) {
|
|
4119
2259
|
console.log(
|
|
4120
|
-
`
|
|
2260
|
+
` + ${app.name} (${app.framework}${app.detectedPort ? `, port ${app.detectedPort}` : ""})`
|
|
4121
2261
|
);
|
|
4122
2262
|
}
|
|
4123
2263
|
if (fix && !dryRun) {
|
|
@@ -4135,11 +2275,11 @@ async function syncPortVerification(repoId, projectPath, dryRun, fix) {
|
|
|
4135
2275
|
command: app.command,
|
|
4136
2276
|
working_dir: app.path
|
|
4137
2277
|
});
|
|
4138
|
-
console.log(`
|
|
2278
|
+
console.log(` Created allocation: ${app.name} \u2192 port ${port}`);
|
|
4139
2279
|
} catch (err) {
|
|
4140
2280
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4141
2281
|
console.log(
|
|
4142
|
-
`
|
|
2282
|
+
` Failed to create allocation for ${app.name}: ${msg}`
|
|
4143
2283
|
);
|
|
4144
2284
|
}
|
|
4145
2285
|
if (app.detectedPort && app.detectedPort >= nextPort) {
|
|
@@ -4147,176 +2287,180 @@ async function syncPortVerification(repoId, projectPath, dryRun, fix) {
|
|
|
4147
2287
|
}
|
|
4148
2288
|
}
|
|
4149
2289
|
} else if (fix && dryRun) {
|
|
4150
|
-
console.log("
|
|
2290
|
+
console.log(" (dry-run \u2014 would create allocations with --fix)");
|
|
4151
2291
|
} else {
|
|
4152
|
-
console.log("
|
|
2292
|
+
console.log(" Run with --fix to auto-create allocations.");
|
|
4153
2293
|
}
|
|
4154
2294
|
}
|
|
4155
2295
|
if (mismatches.length === 0 && unallocated.length === 0) {
|
|
4156
|
-
console.log("
|
|
2296
|
+
console.log(" Ports verified.");
|
|
4157
2297
|
}
|
|
4158
2298
|
} catch (err) {
|
|
4159
2299
|
console.warn(
|
|
4160
|
-
`
|
|
2300
|
+
` Port verification skipped: ${err instanceof Error ? err.message : String(err)}`
|
|
4161
2301
|
);
|
|
4162
2302
|
}
|
|
2303
|
+
console.log("\n Ports complete.\n");
|
|
4163
2304
|
}
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
rule: "Rules",
|
|
4171
|
-
hook: "Hooks",
|
|
4172
|
-
template: "Templates",
|
|
4173
|
-
settings: "Settings",
|
|
4174
|
-
context: "Context",
|
|
4175
|
-
docs_stack: "Stack Docs",
|
|
4176
|
-
docs: "Docs"
|
|
4177
|
-
};
|
|
4178
|
-
for (const item of items) {
|
|
4179
|
-
const label = typeLabels[item.type] ?? item.type;
|
|
4180
|
-
const group = groups.get(label) ?? [];
|
|
4181
|
-
group.push(item);
|
|
4182
|
-
groups.set(label, group);
|
|
2305
|
+
var init_ports = __esm({
|
|
2306
|
+
"src/cli/ports.ts"() {
|
|
2307
|
+
"use strict";
|
|
2308
|
+
init_flags();
|
|
2309
|
+
init_api();
|
|
2310
|
+
init_port_verify();
|
|
4183
2311
|
}
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
rule: { dir: "rules", ext: ".md" },
|
|
4192
|
-
hook: { dir: "hooks", ext: ".sh" },
|
|
4193
|
-
template: { dir: "templates", ext: "" },
|
|
4194
|
-
context: { dir: "context", ext: ".md" },
|
|
4195
|
-
docs_stack: { dir: join11("docs", "stack"), ext: ".md" },
|
|
4196
|
-
docs: { dir: "docs", ext: ".md" },
|
|
4197
|
-
claude_md: { dir: "", ext: "" },
|
|
4198
|
-
settings: { dir: "", ext: "" }
|
|
4199
|
-
};
|
|
4200
|
-
if (remote.type === "claude_md") return join11(projectPath, "CLAUDE.md");
|
|
4201
|
-
if (remote.type === "settings") return join11(claudeDir, "settings.json");
|
|
4202
|
-
const cfg = typeConfig2[remote.type];
|
|
4203
|
-
if (!cfg) return join11(claudeDir, remote.name);
|
|
4204
|
-
const typeDir = remote.type === "command" ? join11(claudeDir, cfg.dir, "cbp") : join11(claudeDir, cfg.dir);
|
|
4205
|
-
if (cfg.subfolder)
|
|
4206
|
-
return join11(typeDir, remote.name, `${cfg.subfolder}${cfg.ext}`);
|
|
4207
|
-
if (remote.type === "command" && remote.category)
|
|
4208
|
-
return join11(typeDir, remote.category, `${remote.name}${cfg.ext}`);
|
|
4209
|
-
if (remote.type === "template") return join11(typeDir, remote.name);
|
|
4210
|
-
if (remote.category && (remote.type === "context" || remote.type === "docs_stack" || remote.type === "docs"))
|
|
4211
|
-
return join11(typeDir, remote.category, `${remote.name}${cfg.ext}`);
|
|
4212
|
-
return join11(typeDir, `${remote.name}${cfg.ext}`);
|
|
2312
|
+
});
|
|
2313
|
+
|
|
2314
|
+
// src/lib/migrate-local-config.ts
|
|
2315
|
+
import { readFile as readFile9, writeFile as writeFile6 } from "node:fs/promises";
|
|
2316
|
+
import { join as join8 } from "node:path";
|
|
2317
|
+
function sharedConfigPath(projectPath) {
|
|
2318
|
+
return join8(projectPath, ".codebyplan.json");
|
|
4213
2319
|
}
|
|
4214
|
-
function
|
|
2320
|
+
async function needsLocalMigration(projectPath) {
|
|
4215
2321
|
try {
|
|
4216
|
-
|
|
2322
|
+
const raw = await readFile9(sharedConfigPath(projectPath), "utf-8");
|
|
2323
|
+
const parsed = JSON.parse(raw);
|
|
2324
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
2325
|
+
return false;
|
|
2326
|
+
}
|
|
2327
|
+
const cfg = parsed;
|
|
2328
|
+
if (typeof cfg.worktree_id !== "string" || cfg.worktree_id === "") {
|
|
2329
|
+
return false;
|
|
2330
|
+
}
|
|
2331
|
+
const local = await readLocalConfig(projectPath);
|
|
2332
|
+
if (local?.device_id) {
|
|
2333
|
+
return false;
|
|
2334
|
+
}
|
|
2335
|
+
return true;
|
|
4217
2336
|
} catch {
|
|
4218
|
-
return
|
|
2337
|
+
return false;
|
|
4219
2338
|
}
|
|
4220
2339
|
}
|
|
4221
|
-
function
|
|
4222
|
-
const
|
|
4223
|
-
const
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
hooks: "hook",
|
|
4229
|
-
templates: "template",
|
|
4230
|
-
settings: "settings",
|
|
4231
|
-
contexts: "context",
|
|
4232
|
-
docs_stack: "docs_stack",
|
|
4233
|
-
docs: "docs"
|
|
4234
|
-
};
|
|
4235
|
-
for (const [syncKey, typeName] of Object.entries(typeMap)) {
|
|
4236
|
-
const files = data[syncKey] ?? [];
|
|
4237
|
-
for (const file of files) {
|
|
4238
|
-
const key = compositeKey(typeName, file.name, file.category ?? null);
|
|
4239
|
-
result.set(key, {
|
|
4240
|
-
id: file.id,
|
|
4241
|
-
type: typeName,
|
|
4242
|
-
name: file.name,
|
|
4243
|
-
content: file.content,
|
|
4244
|
-
category: file.category,
|
|
4245
|
-
updated_at: file.updated_at,
|
|
4246
|
-
content_hash: file.content_hash,
|
|
4247
|
-
scope: file.scope
|
|
4248
|
-
});
|
|
4249
|
-
}
|
|
2340
|
+
async function runLocalMigration(projectPath) {
|
|
2341
|
+
const raw = await readFile9(sharedConfigPath(projectPath), "utf-8");
|
|
2342
|
+
const parsed = JSON.parse(raw);
|
|
2343
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
2344
|
+
throw new Error(
|
|
2345
|
+
".codebyplan.json does not contain a JSON object \u2014 cannot migrate"
|
|
2346
|
+
);
|
|
4250
2347
|
}
|
|
4251
|
-
|
|
2348
|
+
const cfg = parsed;
|
|
2349
|
+
const hadWorktreeId = "worktree_id" in cfg;
|
|
2350
|
+
const localBefore = await readLocalConfig(projectPath);
|
|
2351
|
+
const localWillBeCreated = !localBefore?.device_id;
|
|
2352
|
+
const device_id = await getOrCreateDeviceId(projectPath);
|
|
2353
|
+
const cleaned = { ...cfg };
|
|
2354
|
+
delete cleaned.worktree_id;
|
|
2355
|
+
await writeFile6(
|
|
2356
|
+
sharedConfigPath(projectPath),
|
|
2357
|
+
JSON.stringify(cleaned, null, 2) + "\n",
|
|
2358
|
+
"utf-8"
|
|
2359
|
+
);
|
|
2360
|
+
const files_changed = [".codebyplan.json"];
|
|
2361
|
+
if (localWillBeCreated) files_changed.push(".codebyplan.local.json");
|
|
2362
|
+
return {
|
|
2363
|
+
migrated: true,
|
|
2364
|
+
was_dirty: hadWorktreeId || localWillBeCreated,
|
|
2365
|
+
files_changed,
|
|
2366
|
+
device_id
|
|
2367
|
+
};
|
|
4252
2368
|
}
|
|
4253
|
-
var
|
|
4254
|
-
"src/
|
|
2369
|
+
var init_migrate_local_config = __esm({
|
|
2370
|
+
"src/lib/migrate-local-config.ts"() {
|
|
4255
2371
|
"use strict";
|
|
4256
|
-
init_config();
|
|
4257
|
-
init_fileMapper();
|
|
4258
|
-
init_confirm();
|
|
4259
|
-
init_api();
|
|
4260
|
-
init_variables();
|
|
4261
|
-
init_tech_detect();
|
|
4262
|
-
init_settings_merge();
|
|
4263
|
-
init_hook_registry();
|
|
4264
|
-
init_port_verify();
|
|
4265
|
-
init_resolve_worktree();
|
|
4266
2372
|
init_local_config();
|
|
4267
|
-
init_migrate_local_config();
|
|
4268
|
-
init_eslint();
|
|
4269
2373
|
}
|
|
4270
2374
|
});
|
|
4271
2375
|
|
|
4272
|
-
// src/cli/
|
|
4273
|
-
var
|
|
4274
|
-
__export(
|
|
4275
|
-
|
|
2376
|
+
// src/cli/tech-stack.ts
|
|
2377
|
+
var tech_stack_exports = {};
|
|
2378
|
+
__export(tech_stack_exports, {
|
|
2379
|
+
runTechStack: () => runTechStack
|
|
4276
2380
|
});
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
2381
|
+
async function runTechStack() {
|
|
2382
|
+
const flags = parseFlags(3);
|
|
2383
|
+
const dryRun = hasFlag("dry-run", 3);
|
|
2384
|
+
validateApiKey();
|
|
2385
|
+
const config = await resolveConfig(flags);
|
|
2386
|
+
const { repoId, projectPath } = config;
|
|
2387
|
+
console.log(`
|
|
2388
|
+
CodeByPlan Tech Stack`);
|
|
2389
|
+
console.log(` Repo: ${repoId}`);
|
|
2390
|
+
console.log(` Path: ${projectPath}`);
|
|
2391
|
+
if (dryRun) console.log(` Mode: dry-run`);
|
|
2392
|
+
console.log();
|
|
2393
|
+
if (dryRun) {
|
|
4288
2394
|
try {
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
2395
|
+
if (await needsLocalMigration(projectPath)) {
|
|
2396
|
+
console.log(
|
|
2397
|
+
` Would migrate .codebyplan.json -> worktree_id to .codebyplan.local.json (dry-run, skipping actual write).`
|
|
2398
|
+
);
|
|
2399
|
+
}
|
|
4293
2400
|
} catch {
|
|
4294
2401
|
}
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
2402
|
+
} else {
|
|
2403
|
+
try {
|
|
2404
|
+
if (await needsLocalMigration(projectPath)) {
|
|
2405
|
+
const result = await runLocalMigration(projectPath);
|
|
2406
|
+
console.log(
|
|
2407
|
+
` Migrated .codebyplan.json -> moved worktree_id to gitignored .codebyplan.local.json (device_id=${result.device_id.slice(0, 8)})`
|
|
2408
|
+
);
|
|
2409
|
+
console.log(
|
|
2410
|
+
` Suggest /cbp-git-commit to stage the cleaned shared file.`
|
|
2411
|
+
);
|
|
2412
|
+
}
|
|
2413
|
+
} catch (err) {
|
|
2414
|
+
console.warn(
|
|
2415
|
+
` Warning: local migration failed (continuing): ${err instanceof Error ? err.message : String(err)}`
|
|
2416
|
+
);
|
|
4303
2417
|
}
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
2418
|
+
}
|
|
2419
|
+
try {
|
|
2420
|
+
const { dependencies } = await scanAllDependencies(projectPath);
|
|
2421
|
+
if (dependencies.length === 0) {
|
|
2422
|
+
console.log(" No dependencies found.");
|
|
2423
|
+
console.log("\n Tech stack complete.\n");
|
|
2424
|
+
return;
|
|
4310
2425
|
}
|
|
4311
|
-
|
|
2426
|
+
const sourcePaths = new Set(dependencies.map((d) => d.source_path));
|
|
2427
|
+
console.log(
|
|
2428
|
+
` ${dependencies.length} dependencies from ${sourcePaths.size} package.json file${sourcePaths.size !== 1 ? "s" : ""}`
|
|
2429
|
+
);
|
|
2430
|
+
if (!dryRun) {
|
|
2431
|
+
const result = await apiPost(`/repos/${repoId}/tech-stack`, { dependencies });
|
|
2432
|
+
if (result.data.stale_removed > 0) {
|
|
2433
|
+
console.log(
|
|
2434
|
+
` ${result.data.stale_removed} stale dependencies removed`
|
|
2435
|
+
);
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
const detected = await detectTechStack(projectPath);
|
|
2439
|
+
if (detected.flat.length > 0) {
|
|
2440
|
+
const repoRes = await apiGet(`/repos/${repoId}`);
|
|
2441
|
+
const remote = parseTechStackResult(repoRes.data.tech_stack);
|
|
2442
|
+
const { merged, added } = mergeTechStack(remote, detected);
|
|
2443
|
+
if (added.length > 0) {
|
|
2444
|
+
console.log(` ${added.length} new tech entries`);
|
|
2445
|
+
if (!dryRun) {
|
|
2446
|
+
await apiPut(`/repos/${repoId}`, { tech_stack: merged });
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
} catch (err) {
|
|
2451
|
+
console.warn(
|
|
2452
|
+
` Tech stack detection skipped: ${err instanceof Error ? err.message : String(err)}`
|
|
2453
|
+
);
|
|
4312
2454
|
}
|
|
2455
|
+
console.log("\n Tech stack complete.\n");
|
|
4313
2456
|
}
|
|
4314
|
-
var
|
|
4315
|
-
"src/cli/
|
|
2457
|
+
var init_tech_stack = __esm({
|
|
2458
|
+
"src/cli/tech-stack.ts"() {
|
|
4316
2459
|
"use strict";
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
2460
|
+
init_flags();
|
|
2461
|
+
init_api();
|
|
2462
|
+
init_tech_detect();
|
|
2463
|
+
init_migrate_local_config();
|
|
4320
2464
|
}
|
|
4321
2465
|
});
|
|
4322
2466
|
|
|
@@ -4356,20 +2500,6 @@ void (async () => {
|
|
|
4356
2500
|
await runSetup2();
|
|
4357
2501
|
process.exit(0);
|
|
4358
2502
|
}
|
|
4359
|
-
if (arg === "sync") {
|
|
4360
|
-
const { runSync: runSync2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
4361
|
-
const { SyncCancelledError: SyncCancelledError2 } = await Promise.resolve().then(() => (init_confirm(), confirm_exports));
|
|
4362
|
-
try {
|
|
4363
|
-
await runSync2();
|
|
4364
|
-
} catch (err) {
|
|
4365
|
-
if (err instanceof SyncCancelledError2) {
|
|
4366
|
-
console.log("\n Sync cancelled.\n");
|
|
4367
|
-
process.exit(0);
|
|
4368
|
-
}
|
|
4369
|
-
throw err;
|
|
4370
|
-
}
|
|
4371
|
-
process.exit(0);
|
|
4372
|
-
}
|
|
4373
2503
|
if (arg === "eslint") {
|
|
4374
2504
|
const { runEslint: runEslint2 } = await Promise.resolve().then(() => (init_eslint(), eslint_exports));
|
|
4375
2505
|
const { SyncCancelledError: SyncCancelledError2 } = await Promise.resolve().then(() => (init_confirm(), confirm_exports));
|
|
@@ -4389,28 +2519,53 @@ void (async () => {
|
|
|
4389
2519
|
await runResolveWorktree2();
|
|
4390
2520
|
process.exit(0);
|
|
4391
2521
|
}
|
|
2522
|
+
if (arg === "config") {
|
|
2523
|
+
const { runConfig: runConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
2524
|
+
await runConfig2();
|
|
2525
|
+
process.exit(0);
|
|
2526
|
+
}
|
|
2527
|
+
if (arg === "ports") {
|
|
2528
|
+
const { runPorts: runPorts2 } = await Promise.resolve().then(() => (init_ports(), ports_exports));
|
|
2529
|
+
await runPorts2();
|
|
2530
|
+
process.exit(0);
|
|
2531
|
+
}
|
|
2532
|
+
if (arg === "tech-stack") {
|
|
2533
|
+
const { runTechStack: runTechStack2 } = await Promise.resolve().then(() => (init_tech_stack(), tech_stack_exports));
|
|
2534
|
+
await runTechStack2();
|
|
2535
|
+
process.exit(0);
|
|
2536
|
+
}
|
|
4392
2537
|
if (arg === "help" || arg === "--help" || arg === "-h" || arg === void 0) {
|
|
4393
2538
|
console.log(`
|
|
4394
2539
|
CodeByPlan CLI v${VERSION}
|
|
4395
2540
|
|
|
4396
2541
|
Usage:
|
|
4397
|
-
codebyplan setup Interactive setup (API key + project init
|
|
4398
|
-
codebyplan
|
|
4399
|
-
codebyplan
|
|
2542
|
+
codebyplan setup Interactive setup (API key + project init)
|
|
2543
|
+
codebyplan config Sync repo config from DB to .codebyplan.json
|
|
2544
|
+
codebyplan ports Verify port allocations against local package.json scripts
|
|
2545
|
+
codebyplan tech-stack Detect and sync tech stack dependencies
|
|
2546
|
+
codebyplan eslint ESLint config management (init)
|
|
4400
2547
|
codebyplan resolve-worktree Resolve active worktree UUID from device+path+branch tuple
|
|
4401
2548
|
codebyplan help Show this help message
|
|
4402
2549
|
codebyplan --version Print version
|
|
4403
2550
|
|
|
4404
|
-
|
|
2551
|
+
Config options:
|
|
2552
|
+
--path <dir> Project root directory (default: cwd)
|
|
2553
|
+
--repo-id <uuid> Repository ID (or set via .codebyplan.json)
|
|
2554
|
+
--dry-run Preview changes without writing
|
|
2555
|
+
|
|
2556
|
+
Ports options:
|
|
4405
2557
|
--path <dir> Project root directory (default: cwd)
|
|
4406
2558
|
--repo-id <uuid> Repository ID (or set via .codebyplan.json)
|
|
4407
2559
|
--dry-run Preview changes without writing
|
|
4408
|
-
--force Skip confirmation and conflict prompts
|
|
4409
2560
|
--fix Auto-create missing port allocations
|
|
4410
2561
|
|
|
2562
|
+
Tech stack options:
|
|
2563
|
+
--path <dir> Project root directory (default: cwd)
|
|
2564
|
+
--repo-id <uuid> Repository ID (or set via .codebyplan.json)
|
|
2565
|
+
--dry-run Preview changes without writing
|
|
2566
|
+
|
|
4411
2567
|
ESLint commands:
|
|
4412
2568
|
codebyplan eslint init Detect tech stack, resolve presets, generate configs
|
|
4413
|
-
codebyplan eslint sync Regenerate if presets changed, detect drift
|
|
4414
2569
|
|
|
4415
2570
|
MCP Server:
|
|
4416
2571
|
Claude Code connects to CodeByPlan via remote MCP:
|