codebyplan 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +313 -132
- package/package.json +14 -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.
|
|
17
|
+
VERSION = "1.1.0";
|
|
18
18
|
PACKAGE_NAME = "codebyplan";
|
|
19
19
|
}
|
|
20
20
|
});
|
|
@@ -127,7 +127,7 @@ var init_api = __esm({
|
|
|
127
127
|
init_version();
|
|
128
128
|
API_KEY = process.env.CODEBYPLAN_API_KEY ?? "";
|
|
129
129
|
BASE_URL = (process.env.CODEBYPLAN_API_URL ?? "https://codebyplan.com").replace(/\/$/, "");
|
|
130
|
-
REQUEST_TIMEOUT_MS =
|
|
130
|
+
REQUEST_TIMEOUT_MS = 12e4;
|
|
131
131
|
MAX_RETRIES = 3;
|
|
132
132
|
BASE_DELAY_MS = 1e3;
|
|
133
133
|
ApiError = class extends Error {
|
|
@@ -197,11 +197,7 @@ var TEMPLATE_MANAGED_KEYS, TEMPLATE_MANAGED_PERMISSION_KEYS, ARRAY_PERMISSION_KE
|
|
|
197
197
|
var init_settings_merge = __esm({
|
|
198
198
|
"src/lib/settings-merge.ts"() {
|
|
199
199
|
"use strict";
|
|
200
|
-
TEMPLATE_MANAGED_KEYS = [
|
|
201
|
-
"attribution",
|
|
202
|
-
"hooks",
|
|
203
|
-
"statusLine"
|
|
204
|
-
];
|
|
200
|
+
TEMPLATE_MANAGED_KEYS = ["attribution", "hooks", "statusLine"];
|
|
205
201
|
TEMPLATE_MANAGED_PERMISSION_KEYS = [
|
|
206
202
|
"deny",
|
|
207
203
|
"ask",
|
|
@@ -255,12 +251,13 @@ function mergeDiscoveredHooks(existing, discovered, hooksRelPath = ".claude/hook
|
|
|
255
251
|
merged[meta.event] = [];
|
|
256
252
|
}
|
|
257
253
|
const eventEntries = merged[meta.event];
|
|
254
|
+
const alreadyRegistered = eventEntries.some(
|
|
255
|
+
(m) => m.hooks.some((h) => h.command === command)
|
|
256
|
+
);
|
|
257
|
+
if (alreadyRegistered) continue;
|
|
258
258
|
const matcherEntry = eventEntries.find((m) => m.matcher === meta.matcher);
|
|
259
259
|
if (matcherEntry) {
|
|
260
|
-
|
|
261
|
-
if (!exists) {
|
|
262
|
-
matcherEntry.hooks.push({ type: "command", command });
|
|
263
|
-
}
|
|
260
|
+
matcherEntry.hooks.push({ type: "command", command });
|
|
264
261
|
} else {
|
|
265
262
|
eventEntries.push({
|
|
266
263
|
matcher: meta.matcher,
|
|
@@ -280,7 +277,10 @@ function stripDiscoveredHooks(config, hooksRelPath = ".claude/hooks") {
|
|
|
280
277
|
(h) => !(h.command && h.command.startsWith(prefix) && h.command.endsWith(".sh"))
|
|
281
278
|
);
|
|
282
279
|
if (filteredHooks.length > 0) {
|
|
283
|
-
filteredMatchers.push({
|
|
280
|
+
filteredMatchers.push({
|
|
281
|
+
matcher: matcher.matcher,
|
|
282
|
+
hooks: filteredHooks
|
|
283
|
+
});
|
|
284
284
|
}
|
|
285
285
|
}
|
|
286
286
|
if (filteredMatchers.length > 0) {
|
|
@@ -307,17 +307,25 @@ function substituteVariables(content, repoData) {
|
|
|
307
307
|
}
|
|
308
308
|
return result;
|
|
309
309
|
}
|
|
310
|
+
function escapeRegex(str) {
|
|
311
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
312
|
+
}
|
|
310
313
|
function reverseSubstituteVariables(content, repoData) {
|
|
311
314
|
const entries = [];
|
|
312
315
|
for (const [name, resolver] of Object.entries(TEMPLATE_VARIABLES)) {
|
|
313
316
|
const value = resolver(repoData);
|
|
314
|
-
if (value.length
|
|
317
|
+
if (value.length === 0) continue;
|
|
315
318
|
entries.push([value, `{{${name}}}`]);
|
|
316
319
|
}
|
|
317
320
|
entries.sort((a, b) => b[0].length - a[0].length);
|
|
318
321
|
let result = content;
|
|
319
322
|
for (const [value, placeholder] of entries) {
|
|
320
|
-
|
|
323
|
+
if (value.length < 8) {
|
|
324
|
+
const pattern = new RegExp(`\\b${escapeRegex(value)}\\b`, "g");
|
|
325
|
+
result = result.replace(pattern, placeholder);
|
|
326
|
+
} else {
|
|
327
|
+
result = result.replaceAll(value, placeholder);
|
|
328
|
+
}
|
|
321
329
|
}
|
|
322
330
|
return result;
|
|
323
331
|
}
|
|
@@ -341,7 +349,16 @@ var sync_engine_exports = {};
|
|
|
341
349
|
__export(sync_engine_exports, {
|
|
342
350
|
executeSyncToLocal: () => executeSyncToLocal
|
|
343
351
|
});
|
|
344
|
-
import {
|
|
352
|
+
import {
|
|
353
|
+
readdir as readdir2,
|
|
354
|
+
readFile as readFile2,
|
|
355
|
+
writeFile,
|
|
356
|
+
unlink,
|
|
357
|
+
mkdir,
|
|
358
|
+
rmdir,
|
|
359
|
+
chmod,
|
|
360
|
+
stat
|
|
361
|
+
} from "node:fs/promises";
|
|
345
362
|
import { join as join2, dirname } from "node:path";
|
|
346
363
|
function getTypeDir(claudeDir, dir) {
|
|
347
364
|
if (dir === "commands") return join2(claudeDir, dir, "cbp");
|
|
@@ -416,13 +433,23 @@ async function executeSyncToLocal(options) {
|
|
|
416
433
|
const dbOnlyFiles = [];
|
|
417
434
|
for (const [syncKey, typeName] of Object.entries(syncKeyToType)) {
|
|
418
435
|
if (worktree && typeName === "command") {
|
|
419
|
-
byType["commands"] = {
|
|
436
|
+
byType["commands"] = {
|
|
437
|
+
created: [],
|
|
438
|
+
updated: [],
|
|
439
|
+
deleted: [],
|
|
440
|
+
unchanged: []
|
|
441
|
+
};
|
|
420
442
|
continue;
|
|
421
443
|
}
|
|
422
444
|
const cfg = typeConfig[typeName];
|
|
423
445
|
const targetDir = getTypeDir(claudeDir, cfg.dir);
|
|
424
446
|
const remoteFiles = syncData[syncKey] ?? [];
|
|
425
|
-
const result = {
|
|
447
|
+
const result = {
|
|
448
|
+
created: [],
|
|
449
|
+
updated: [],
|
|
450
|
+
deleted: [],
|
|
451
|
+
unchanged: []
|
|
452
|
+
};
|
|
426
453
|
if (!dryRun) {
|
|
427
454
|
await mkdir(targetDir, { recursive: true });
|
|
428
455
|
}
|
|
@@ -483,7 +510,12 @@ async function executeSyncToLocal(options) {
|
|
|
483
510
|
const syncKey = "docs_stack";
|
|
484
511
|
const targetDir = join2(projectPath, "docs", "stack");
|
|
485
512
|
const remoteFiles = syncData[syncKey] ?? [];
|
|
486
|
-
const result = {
|
|
513
|
+
const result = {
|
|
514
|
+
created: [],
|
|
515
|
+
updated: [],
|
|
516
|
+
deleted: [],
|
|
517
|
+
unchanged: []
|
|
518
|
+
};
|
|
487
519
|
if (remoteFiles.length > 0 && !dryRun) {
|
|
488
520
|
await mkdir(targetDir, { recursive: true });
|
|
489
521
|
}
|
|
@@ -492,7 +524,10 @@ async function executeSyncToLocal(options) {
|
|
|
492
524
|
for (const remote of remoteFiles) {
|
|
493
525
|
const relPath = remote.category ? join2(remote.category, remote.name) : remote.name;
|
|
494
526
|
const substituted = substituteVariables(remote.content, repoData);
|
|
495
|
-
remotePathMap.set(relPath, {
|
|
527
|
+
remotePathMap.set(relPath, {
|
|
528
|
+
content: substituted,
|
|
529
|
+
name: `${remote.category ?? ""}/${remote.name}`
|
|
530
|
+
});
|
|
496
531
|
}
|
|
497
532
|
for (const [relPath, { content, name }] of remotePathMap) {
|
|
498
533
|
const fullPath = join2(targetDir, relPath);
|
|
@@ -531,7 +566,9 @@ async function executeSyncToLocal(options) {
|
|
|
531
566
|
const globalSettingsFiles = syncData.global_settings ?? [];
|
|
532
567
|
let globalSettings = {};
|
|
533
568
|
for (const gf of globalSettingsFiles) {
|
|
534
|
-
const parsed = JSON.parse(
|
|
569
|
+
const parsed = JSON.parse(
|
|
570
|
+
substituteVariables(gf.content, repoData)
|
|
571
|
+
);
|
|
535
572
|
globalSettings = { ...globalSettings, ...parsed };
|
|
536
573
|
}
|
|
537
574
|
const specialTypes = {
|
|
@@ -540,7 +577,12 @@ async function executeSyncToLocal(options) {
|
|
|
540
577
|
};
|
|
541
578
|
for (const [typeName, getPath] of Object.entries(specialTypes)) {
|
|
542
579
|
const remoteFiles = syncData[typeName] ?? [];
|
|
543
|
-
const result = {
|
|
580
|
+
const result = {
|
|
581
|
+
created: [],
|
|
582
|
+
updated: [],
|
|
583
|
+
deleted: [],
|
|
584
|
+
unchanged: []
|
|
585
|
+
};
|
|
544
586
|
for (const remote of remoteFiles) {
|
|
545
587
|
const targetPath = getPath(remote.name);
|
|
546
588
|
const remoteContent = substituteVariables(remote.content, repoData);
|
|
@@ -551,11 +593,14 @@ async function executeSyncToLocal(options) {
|
|
|
551
593
|
}
|
|
552
594
|
if (typeName === "settings") {
|
|
553
595
|
const repoSettings = JSON.parse(remoteContent);
|
|
554
|
-
const combinedTemplate = mergeGlobalAndRepoSettings(
|
|
596
|
+
const combinedTemplate = mergeGlobalAndRepoSettings(
|
|
597
|
+
globalSettings,
|
|
598
|
+
repoSettings
|
|
599
|
+
);
|
|
555
600
|
const hooksDir = join2(projectPath, ".claude", "hooks");
|
|
556
601
|
const discovered = await discoverHooks(hooksDir);
|
|
557
602
|
if (localContent === void 0) {
|
|
558
|
-
|
|
603
|
+
const finalSettings = stripPermissionsAllow(combinedTemplate);
|
|
559
604
|
if (discovered.size > 0) {
|
|
560
605
|
finalSettings.hooks = mergeDiscoveredHooks(
|
|
561
606
|
finalSettings.hooks ?? {},
|
|
@@ -564,7 +609,11 @@ async function executeSyncToLocal(options) {
|
|
|
564
609
|
}
|
|
565
610
|
if (!dryRun) {
|
|
566
611
|
await mkdir(dirname(targetPath), { recursive: true });
|
|
567
|
-
await writeFile(
|
|
612
|
+
await writeFile(
|
|
613
|
+
targetPath,
|
|
614
|
+
JSON.stringify(finalSettings, null, 2) + "\n",
|
|
615
|
+
"utf-8"
|
|
616
|
+
);
|
|
568
617
|
}
|
|
569
618
|
result.created.push(remote.name);
|
|
570
619
|
totals.created++;
|
|
@@ -625,28 +674,32 @@ async function executeSyncToLocal(options) {
|
|
|
625
674
|
});
|
|
626
675
|
const fileRepoUpdates = [];
|
|
627
676
|
const syncTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
628
|
-
for (const [syncKey] of Object.entries(syncKeyToType)) {
|
|
677
|
+
for (const [syncKey, typeName] of Object.entries(syncKeyToType)) {
|
|
629
678
|
const remoteFiles = syncData[syncKey] ?? [];
|
|
630
679
|
for (const file of remoteFiles) {
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
680
|
+
fileRepoUpdates.push({
|
|
681
|
+
claude_file_id: file.id ?? void 0,
|
|
682
|
+
file_type: typeName,
|
|
683
|
+
file_name: file.name,
|
|
684
|
+
file_category: file.category ?? null,
|
|
685
|
+
file_scope: file.scope ?? "shared",
|
|
686
|
+
last_synced_at: syncTimestamp,
|
|
687
|
+
sync_status: "synced"
|
|
688
|
+
});
|
|
638
689
|
}
|
|
639
690
|
}
|
|
640
691
|
for (const typeName of ["claude_md", "settings"]) {
|
|
641
692
|
const remoteFiles = syncData[typeName] ?? [];
|
|
642
693
|
for (const file of remoteFiles) {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
}
|
|
649
|
-
|
|
694
|
+
fileRepoUpdates.push({
|
|
695
|
+
claude_file_id: file.id ?? void 0,
|
|
696
|
+
file_type: typeName,
|
|
697
|
+
file_name: file.name,
|
|
698
|
+
file_category: file.category ?? null,
|
|
699
|
+
file_scope: file.scope ?? `local:${repoId}`,
|
|
700
|
+
last_synced_at: syncTimestamp,
|
|
701
|
+
sync_status: "synced"
|
|
702
|
+
});
|
|
650
703
|
}
|
|
651
704
|
}
|
|
652
705
|
if (fileRepoUpdates.length > 0) {
|
|
@@ -748,7 +801,9 @@ async function runSetup() {
|
|
|
748
801
|
console.log("\n CodeByPlan Setup\n");
|
|
749
802
|
console.log(" This will configure Claude Code to use CodeByPlan.\n");
|
|
750
803
|
console.log(" 1. Sign up at https://codebyplan.com");
|
|
751
|
-
console.log(
|
|
804
|
+
console.log(
|
|
805
|
+
" 2. Create an API key at https://codebyplan.com/settings/api-keys/\n"
|
|
806
|
+
);
|
|
752
807
|
try {
|
|
753
808
|
const apiKey = (await rl.question(" Enter your API key: ")).trim();
|
|
754
809
|
if (!apiKey) {
|
|
@@ -780,8 +835,10 @@ async function runSetup() {
|
|
|
780
835
|
console.log(" API key is valid!\n");
|
|
781
836
|
}
|
|
782
837
|
} else {
|
|
783
|
-
console.log(
|
|
784
|
-
`
|
|
838
|
+
console.log(
|
|
839
|
+
` Warning: API returned status ${res.status}, but continuing.
|
|
840
|
+
`
|
|
841
|
+
);
|
|
785
842
|
}
|
|
786
843
|
console.log(" Where should the MCP server be configured?\n");
|
|
787
844
|
console.log(" 1. Global \u2014 available in all projects (~/.claude.json)");
|
|
@@ -795,14 +852,20 @@ async function runSetup() {
|
|
|
795
852
|
console.log(` Done! Config written to ${configPath}
|
|
796
853
|
`);
|
|
797
854
|
if (scope === "project") {
|
|
798
|
-
console.log(
|
|
855
|
+
console.log(
|
|
856
|
+
" Note: .mcp.json contains your API key \u2014 add it to .gitignore.\n"
|
|
857
|
+
);
|
|
799
858
|
}
|
|
800
859
|
} else {
|
|
801
860
|
console.log(" Warning: Could not verify the saved configuration.\n");
|
|
802
|
-
console.log(
|
|
803
|
-
`
|
|
804
|
-
|
|
805
|
-
|
|
861
|
+
console.log(
|
|
862
|
+
` Manually add to ~/.claude.json under mcpServers.codebyplan:
|
|
863
|
+
`
|
|
864
|
+
);
|
|
865
|
+
console.log(
|
|
866
|
+
` { "url": "https://codebyplan.com/mcp", "headers": { "x-api-key": "${apiKey}" } }
|
|
867
|
+
`
|
|
868
|
+
);
|
|
806
869
|
}
|
|
807
870
|
if (repos.length > 0) {
|
|
808
871
|
console.log(" Initialize this project?\n");
|
|
@@ -827,14 +890,22 @@ async function runSetup() {
|
|
|
827
890
|
const projectPath = process.cwd();
|
|
828
891
|
try {
|
|
829
892
|
const worktreesRes = await apiGet(`/worktrees?repo_id=${selectedRepo.id}`);
|
|
830
|
-
const match = worktreesRes.data.find(
|
|
893
|
+
const match = worktreesRes.data.find(
|
|
894
|
+
(wt) => projectPath === wt.path || projectPath.startsWith(wt.path + "/")
|
|
895
|
+
);
|
|
831
896
|
if (match) worktreeId = match.id;
|
|
832
897
|
} catch {
|
|
833
898
|
}
|
|
834
899
|
const codebyplanPath = join3(projectPath, ".codebyplan.json");
|
|
835
|
-
const codebyplanConfig = {
|
|
900
|
+
const codebyplanConfig = {
|
|
901
|
+
repo_id: selectedRepo.id
|
|
902
|
+
};
|
|
836
903
|
if (worktreeId) codebyplanConfig.worktree_id = worktreeId;
|
|
837
|
-
await writeFile2(
|
|
904
|
+
await writeFile2(
|
|
905
|
+
codebyplanPath,
|
|
906
|
+
JSON.stringify(codebyplanConfig, null, 2) + "\n",
|
|
907
|
+
"utf-8"
|
|
908
|
+
);
|
|
838
909
|
console.log(` Created ${codebyplanPath}`);
|
|
839
910
|
console.log("\n Running initial sync...\n");
|
|
840
911
|
try {
|
|
@@ -845,8 +916,10 @@ async function runSetup() {
|
|
|
845
916
|
});
|
|
846
917
|
const totalChanges = syncResult.totals.created + syncResult.totals.updated + syncResult.totals.deleted;
|
|
847
918
|
if (totalChanges > 0) {
|
|
848
|
-
console.log(
|
|
849
|
-
`
|
|
919
|
+
console.log(
|
|
920
|
+
` Synced: ${syncResult.totals.created} created, ${syncResult.totals.updated} updated, ${syncResult.totals.deleted} deleted
|
|
921
|
+
`
|
|
922
|
+
);
|
|
850
923
|
} else {
|
|
851
924
|
console.log(" All files already up to date.\n");
|
|
852
925
|
}
|
|
@@ -858,7 +931,9 @@ async function runSetup() {
|
|
|
858
931
|
}
|
|
859
932
|
}
|
|
860
933
|
}
|
|
861
|
-
console.log(
|
|
934
|
+
console.log(
|
|
935
|
+
" Setup complete! Start a new Claude Code session to begin.\n"
|
|
936
|
+
);
|
|
862
937
|
} finally {
|
|
863
938
|
rl.close();
|
|
864
939
|
}
|
|
@@ -918,14 +993,48 @@ var init_config = __esm({
|
|
|
918
993
|
// src/cli/fileMapper.ts
|
|
919
994
|
import { readdir as readdir3, readFile as readFile5 } from "node:fs/promises";
|
|
920
995
|
import { join as join5, extname } from "node:path";
|
|
996
|
+
function extractScope(content, type) {
|
|
997
|
+
if (type === "hook") {
|
|
998
|
+
const match = content.match(/^#\s*@scope:\s*(\S+)/m);
|
|
999
|
+
if (match) {
|
|
1000
|
+
const raw = match[1];
|
|
1001
|
+
return raw === "shared" ? "shared" : `local:${raw}`;
|
|
1002
|
+
}
|
|
1003
|
+
return "shared";
|
|
1004
|
+
}
|
|
1005
|
+
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
1006
|
+
if (fmMatch) {
|
|
1007
|
+
const scopeLine = fmMatch[1].match(/^scope:\s*(\S+)/m);
|
|
1008
|
+
if (scopeLine) {
|
|
1009
|
+
const raw = scopeLine[1];
|
|
1010
|
+
return raw === "shared" ? "shared" : `local:${raw}`;
|
|
1011
|
+
}
|
|
1012
|
+
if (/^scope\b/m.test(fmMatch[1])) {
|
|
1013
|
+
console.error(
|
|
1014
|
+
` Warning: frontmatter contains "scope" but could not parse it. Expected format: "scope: shared" or "scope: <repo-name>". Defaulting to "shared".`
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
return "shared";
|
|
1019
|
+
}
|
|
921
1020
|
function compositeKey(type, name, category) {
|
|
922
1021
|
return category ? `${type}:${category}/${name}` : `${type}:${name}`;
|
|
923
1022
|
}
|
|
924
1023
|
async function scanLocalFiles(claudeDir, projectPath) {
|
|
925
1024
|
const result = /* @__PURE__ */ new Map();
|
|
926
1025
|
await scanCommands(join5(claudeDir, "commands", "cbp"), result);
|
|
927
|
-
await scanSubfolderType(
|
|
928
|
-
|
|
1026
|
+
await scanSubfolderType(
|
|
1027
|
+
join5(claudeDir, "agents"),
|
|
1028
|
+
"agent",
|
|
1029
|
+
"AGENT.md",
|
|
1030
|
+
result
|
|
1031
|
+
);
|
|
1032
|
+
await scanSubfolderType(
|
|
1033
|
+
join5(claudeDir, "skills"),
|
|
1034
|
+
"skill",
|
|
1035
|
+
"SKILL.md",
|
|
1036
|
+
result
|
|
1037
|
+
);
|
|
929
1038
|
await scanFlatType(join5(claudeDir, "rules"), "rule", ".md", result);
|
|
930
1039
|
await scanFlatType(join5(claudeDir, "hooks"), "hook", ".sh", result);
|
|
931
1040
|
await scanTemplates(join5(claudeDir, "templates"), result);
|
|
@@ -944,14 +1053,19 @@ async function scanCommandsRecursive(baseDir, currentDir, result) {
|
|
|
944
1053
|
}
|
|
945
1054
|
for (const entry of entries) {
|
|
946
1055
|
if (entry.isDirectory()) {
|
|
947
|
-
await scanCommandsRecursive(
|
|
1056
|
+
await scanCommandsRecursive(
|
|
1057
|
+
baseDir,
|
|
1058
|
+
join5(currentDir, entry.name),
|
|
1059
|
+
result
|
|
1060
|
+
);
|
|
948
1061
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
949
1062
|
const name = entry.name.slice(0, -3);
|
|
950
1063
|
const content = await readFile5(join5(currentDir, entry.name), "utf-8");
|
|
951
1064
|
const relDir = currentDir.slice(baseDir.length + 1);
|
|
952
1065
|
const category = relDir || null;
|
|
1066
|
+
const scope = extractScope(content, "command");
|
|
953
1067
|
const key = compositeKey("command", name, category);
|
|
954
|
-
result.set(key, { type: "command", name, category, content });
|
|
1068
|
+
result.set(key, { type: "command", name, category, content, scope });
|
|
955
1069
|
}
|
|
956
1070
|
}
|
|
957
1071
|
}
|
|
@@ -967,8 +1081,15 @@ async function scanSubfolderType(dir, type, fileName, result) {
|
|
|
967
1081
|
const filePath = join5(dir, entry.name, fileName);
|
|
968
1082
|
try {
|
|
969
1083
|
const content = await readFile5(filePath, "utf-8");
|
|
1084
|
+
const scope = extractScope(content, type);
|
|
970
1085
|
const key = compositeKey(type, entry.name, null);
|
|
971
|
-
result.set(key, {
|
|
1086
|
+
result.set(key, {
|
|
1087
|
+
type,
|
|
1088
|
+
name: entry.name,
|
|
1089
|
+
category: null,
|
|
1090
|
+
content,
|
|
1091
|
+
scope
|
|
1092
|
+
});
|
|
972
1093
|
} catch {
|
|
973
1094
|
}
|
|
974
1095
|
}
|
|
@@ -985,8 +1106,9 @@ async function scanFlatType(dir, type, ext, result) {
|
|
|
985
1106
|
if (entry.isFile() && entry.name.endsWith(ext)) {
|
|
986
1107
|
const name = entry.name.slice(0, -ext.length);
|
|
987
1108
|
const content = await readFile5(join5(dir, entry.name), "utf-8");
|
|
1109
|
+
const scope = extractScope(content, type);
|
|
988
1110
|
const key = compositeKey(type, name, null);
|
|
989
|
-
result.set(key, { type, name, category: null, content });
|
|
1111
|
+
result.set(key, { type, name, category: null, content, scope });
|
|
990
1112
|
}
|
|
991
1113
|
}
|
|
992
1114
|
}
|
|
@@ -1000,8 +1122,15 @@ async function scanTemplates(dir, result) {
|
|
|
1000
1122
|
for (const entry of entries) {
|
|
1001
1123
|
if (entry.isFile() && extname(entry.name)) {
|
|
1002
1124
|
const content = await readFile5(join5(dir, entry.name), "utf-8");
|
|
1125
|
+
const scope = extractScope(content, "template");
|
|
1003
1126
|
const key = compositeKey("template", entry.name, null);
|
|
1004
|
-
result.set(key, {
|
|
1127
|
+
result.set(key, {
|
|
1128
|
+
type: "template",
|
|
1129
|
+
name: entry.name,
|
|
1130
|
+
category: null,
|
|
1131
|
+
content,
|
|
1132
|
+
scope
|
|
1133
|
+
});
|
|
1005
1134
|
}
|
|
1006
1135
|
}
|
|
1007
1136
|
}
|
|
@@ -1035,7 +1164,13 @@ async function scanSettings(claudeDir, projectPath, result) {
|
|
|
1035
1164
|
}
|
|
1036
1165
|
const content = JSON.stringify(parsed, null, 2) + "\n";
|
|
1037
1166
|
const key = compositeKey("settings", "settings", null);
|
|
1038
|
-
result.set(key, {
|
|
1167
|
+
result.set(key, {
|
|
1168
|
+
type: "settings",
|
|
1169
|
+
name: "settings",
|
|
1170
|
+
category: null,
|
|
1171
|
+
content,
|
|
1172
|
+
scope: "shared"
|
|
1173
|
+
});
|
|
1039
1174
|
}
|
|
1040
1175
|
var init_fileMapper = __esm({
|
|
1041
1176
|
"src/cli/fileMapper.ts"() {
|
|
@@ -1069,7 +1204,9 @@ async function confirmProceed(message) {
|
|
|
1069
1204
|
const a = answer.trim().toLowerCase();
|
|
1070
1205
|
if (a === "" || a === "y" || a === "yes") return true;
|
|
1071
1206
|
if (a === "n" || a === "no") return false;
|
|
1072
|
-
console.log(
|
|
1207
|
+
console.log(
|
|
1208
|
+
` Unknown option "${answer.trim()}". Valid: y/yes, n/no, or Enter for yes.`
|
|
1209
|
+
);
|
|
1073
1210
|
}
|
|
1074
1211
|
} catch (err) {
|
|
1075
1212
|
if (isAbortError(err)) throw new SyncCancelledError();
|
|
@@ -1202,11 +1339,16 @@ async function promptReviewMode() {
|
|
|
1202
1339
|
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
1203
1340
|
try {
|
|
1204
1341
|
while (true) {
|
|
1205
|
-
const answer = await rl.question(
|
|
1342
|
+
const answer = await rl.question(
|
|
1343
|
+
" Review [o]ne-by-one or [f]older-by-folder? "
|
|
1344
|
+
);
|
|
1206
1345
|
const a = answer.trim().toLowerCase();
|
|
1207
|
-
if (a === "o" || a === "one-by-one" || a === "one" || a === "file")
|
|
1346
|
+
if (a === "o" || a === "one-by-one" || a === "one" || a === "file")
|
|
1347
|
+
return "file";
|
|
1208
1348
|
if (a === "f" || a === "folder") return "folder";
|
|
1209
|
-
console.log(
|
|
1349
|
+
console.log(
|
|
1350
|
+
` Unknown option "${answer.trim()}". Valid: o/one-by-one, f/folder`
|
|
1351
|
+
);
|
|
1210
1352
|
}
|
|
1211
1353
|
} catch (err) {
|
|
1212
1354
|
if (isAbortError(err)) throw new SyncCancelledError();
|
|
@@ -1245,7 +1387,9 @@ async function reviewFilesOneByOne(items, label, plannedAction, recommendedActio
|
|
|
1245
1387
|
break;
|
|
1246
1388
|
}
|
|
1247
1389
|
if (result.action === null) {
|
|
1248
|
-
console.log(
|
|
1390
|
+
console.log(
|
|
1391
|
+
` Unknown option "${answer.trim()}". Valid: ${formatActionPrompt(rec, hasContent, false)}`
|
|
1392
|
+
);
|
|
1249
1393
|
continue;
|
|
1250
1394
|
}
|
|
1251
1395
|
results.push(result.action);
|
|
@@ -1282,7 +1426,13 @@ async function reviewFolder(folderName, items, label, plannedAction, recommended
|
|
|
1282
1426
|
const a = answer.trim().toLowerCase();
|
|
1283
1427
|
if (a === "o" || a === "one-by-one") {
|
|
1284
1428
|
rl.close();
|
|
1285
|
-
return reviewFilesOneByOne(
|
|
1429
|
+
return reviewFilesOneByOne(
|
|
1430
|
+
items,
|
|
1431
|
+
label,
|
|
1432
|
+
plannedAction,
|
|
1433
|
+
recommendedAction,
|
|
1434
|
+
content
|
|
1435
|
+
);
|
|
1286
1436
|
}
|
|
1287
1437
|
if (a === "r" || a === "recommended") {
|
|
1288
1438
|
return items.map(
|
|
@@ -1303,11 +1453,13 @@ async function reviewFolder(folderName, items, label, plannedAction, recommended
|
|
|
1303
1453
|
if (result.action !== null) {
|
|
1304
1454
|
return items.map(() => result.action);
|
|
1305
1455
|
}
|
|
1306
|
-
console.log(
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1456
|
+
console.log(
|
|
1457
|
+
` Unknown option "${answer.trim()}". Valid: ${formatActionPrompt(
|
|
1458
|
+
recommendedAction ? recommendedAction(items[0]) : plannedAction(items[0]),
|
|
1459
|
+
false,
|
|
1460
|
+
true
|
|
1461
|
+
)} [o]ne-by-one`
|
|
1462
|
+
);
|
|
1311
1463
|
}
|
|
1312
1464
|
} catch (err) {
|
|
1313
1465
|
if (isAbortError(err)) throw new SyncCancelledError();
|
|
@@ -1893,7 +2045,10 @@ async function runSync() {
|
|
|
1893
2045
|
if (!dryRun) {
|
|
1894
2046
|
try {
|
|
1895
2047
|
await apiDelete("/sync/lock", { repo_id: repoId });
|
|
1896
|
-
} catch {
|
|
2048
|
+
} catch (err) {
|
|
2049
|
+
console.error(
|
|
2050
|
+
` Warning: failed to release sync lock: ${err instanceof Error ? err.message : String(err)}`
|
|
2051
|
+
);
|
|
1897
2052
|
}
|
|
1898
2053
|
}
|
|
1899
2054
|
}
|
|
@@ -1906,37 +2061,42 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
|
|
|
1906
2061
|
localFiles = await scanLocalFiles(claudeDir, projectPath);
|
|
1907
2062
|
} catch {
|
|
1908
2063
|
}
|
|
1909
|
-
const [defaultsRes, repoSyncRes, repoRes,
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
2064
|
+
const [defaultsRes, repoSyncRes, repoRes, , fileReposRes] = await Promise.all(
|
|
2065
|
+
[
|
|
2066
|
+
apiGet("/sync/defaults"),
|
|
2067
|
+
apiGet("/sync/files", { repo_id: repoId }),
|
|
2068
|
+
apiGet(`/repos/${repoId}`),
|
|
2069
|
+
apiGet("/sync/state", {
|
|
2070
|
+
repo_id: repoId
|
|
2071
|
+
}),
|
|
2072
|
+
apiGet("/sync/file-repos", {
|
|
2073
|
+
repo_id: repoId
|
|
2074
|
+
})
|
|
2075
|
+
]
|
|
2076
|
+
);
|
|
1920
2077
|
const syncStartTime = Date.now();
|
|
1921
2078
|
const repoData = repoRes.data;
|
|
1922
2079
|
const remoteDefaults = flattenSyncData(defaultsRes.data);
|
|
1923
2080
|
const remoteRepoFiles = flattenSyncData(repoSyncRes.data);
|
|
1924
|
-
const syncState = syncStateRes.data;
|
|
1925
2081
|
const fileRepoHashes = /* @__PURE__ */ new Map();
|
|
1926
2082
|
const fileRepoByClaudeFileId = /* @__PURE__ */ new Map();
|
|
1927
2083
|
for (const entry of fileReposRes.data ?? []) {
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
2084
|
+
const baseKey = compositeKey(
|
|
2085
|
+
entry.file_type,
|
|
2086
|
+
entry.file_name,
|
|
2087
|
+
entry.file_category
|
|
2088
|
+
);
|
|
2089
|
+
const scopedKey = `${baseKey}:${entry.file_scope}`;
|
|
2090
|
+
fileRepoHashes.set(scopedKey, entry.last_synced_content_hash);
|
|
2091
|
+
if (!fileRepoHashes.has(baseKey)) {
|
|
2092
|
+
fileRepoHashes.set(baseKey, entry.last_synced_content_hash);
|
|
2093
|
+
}
|
|
2094
|
+
if (entry.claude_file_id) {
|
|
2095
|
+
fileRepoByClaudeFileId.set(
|
|
2096
|
+
entry.claude_file_id,
|
|
2097
|
+
entry.last_synced_content_hash
|
|
1933
2098
|
);
|
|
1934
|
-
fileRepoHashes.set(key, entry.last_synced_content_hash);
|
|
1935
2099
|
}
|
|
1936
|
-
fileRepoByClaudeFileId.set(
|
|
1937
|
-
entry.claude_file_id,
|
|
1938
|
-
entry.last_synced_content_hash
|
|
1939
|
-
);
|
|
1940
2100
|
}
|
|
1941
2101
|
const remoteFiles = new Map([...remoteDefaults, ...remoteRepoFiles]);
|
|
1942
2102
|
console.log(
|
|
@@ -1965,6 +2125,7 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
|
|
|
1965
2125
|
type: local.type,
|
|
1966
2126
|
name: local.name,
|
|
1967
2127
|
category: local.category,
|
|
2128
|
+
scope: local.scope,
|
|
1968
2129
|
isHook: local.type === "hook",
|
|
1969
2130
|
claudeFileId: null
|
|
1970
2131
|
});
|
|
@@ -1984,6 +2145,7 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
|
|
|
1984
2145
|
type: remote.type,
|
|
1985
2146
|
name: remote.name,
|
|
1986
2147
|
category: remote.category ?? null,
|
|
2148
|
+
scope: remote.scope ?? "shared",
|
|
1987
2149
|
isHook: remote.type === "hook",
|
|
1988
2150
|
claudeFileId: remote.id ?? null
|
|
1989
2151
|
});
|
|
@@ -1993,7 +2155,8 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
|
|
|
1993
2155
|
continue;
|
|
1994
2156
|
}
|
|
1995
2157
|
const localHash = contentHash(local.content);
|
|
1996
|
-
const
|
|
2158
|
+
const scopedKey = `${key}:${local.scope}`;
|
|
2159
|
+
const lastSyncedHash = fileRepoHashes.get(scopedKey) ?? fileRepoHashes.get(key) ?? null;
|
|
1997
2160
|
const localChanged = lastSyncedHash ? localHash !== lastSyncedHash : true;
|
|
1998
2161
|
let action;
|
|
1999
2162
|
if (force) {
|
|
@@ -2003,8 +2166,8 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
|
|
|
2003
2166
|
} else if (lastSyncedHash === null) {
|
|
2004
2167
|
action = "conflict";
|
|
2005
2168
|
} else {
|
|
2006
|
-
const
|
|
2007
|
-
const remoteChanged =
|
|
2169
|
+
const remoteResolvedHash = contentHash(resolvedRemote);
|
|
2170
|
+
const remoteChanged = remoteResolvedHash !== lastSyncedHash;
|
|
2008
2171
|
if (remoteChanged) {
|
|
2009
2172
|
action = "conflict";
|
|
2010
2173
|
} else {
|
|
@@ -2023,6 +2186,7 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
|
|
|
2023
2186
|
type: local.type,
|
|
2024
2187
|
name: local.name,
|
|
2025
2188
|
category: local.category,
|
|
2189
|
+
scope: local.scope,
|
|
2026
2190
|
isHook: local.type === "hook",
|
|
2027
2191
|
claudeFileId: remote.id ?? null
|
|
2028
2192
|
});
|
|
@@ -2123,7 +2287,8 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
|
|
|
2123
2287
|
type: p.type,
|
|
2124
2288
|
name: p.name,
|
|
2125
2289
|
category: p.category,
|
|
2126
|
-
content: p.pushContent
|
|
2290
|
+
content: p.pushContent,
|
|
2291
|
+
scope: p.scope
|
|
2127
2292
|
}));
|
|
2128
2293
|
if (toUpsert.length > 0) {
|
|
2129
2294
|
await apiPost("/sync/files", {
|
|
@@ -2146,38 +2311,16 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
|
|
|
2146
2311
|
if (p.filePath) {
|
|
2147
2312
|
try {
|
|
2148
2313
|
await unlink2(p.filePath);
|
|
2149
|
-
} catch {
|
|
2314
|
+
} catch (err) {
|
|
2315
|
+
if (err instanceof Error && "code" in err && err.code !== "ENOENT") {
|
|
2316
|
+
console.error(
|
|
2317
|
+
` Warning: failed to delete ${p.filePath}: ${err.message}`
|
|
2318
|
+
);
|
|
2319
|
+
}
|
|
2150
2320
|
}
|
|
2151
2321
|
}
|
|
2152
2322
|
}
|
|
2153
2323
|
}
|
|
2154
|
-
const unresolvedConflicts = plan.filter(
|
|
2155
|
-
(p) => p.action === "conflict" || p.action === "skip" && p.localContent !== null && p.remoteContent !== null
|
|
2156
|
-
);
|
|
2157
|
-
if (unresolvedConflicts.length > 0) {
|
|
2158
|
-
let stored = 0;
|
|
2159
|
-
for (const p of unresolvedConflicts) {
|
|
2160
|
-
if (p.claudeFileId) {
|
|
2161
|
-
try {
|
|
2162
|
-
await apiPost("/sync/conflicts", {
|
|
2163
|
-
repo_id: repoId,
|
|
2164
|
-
claude_file_id: p.claudeFileId,
|
|
2165
|
-
conflict_type: "both_modified",
|
|
2166
|
-
local_content: p.localContent,
|
|
2167
|
-
remote_content: p.remoteContent
|
|
2168
|
-
});
|
|
2169
|
-
stored++;
|
|
2170
|
-
} catch {
|
|
2171
|
-
}
|
|
2172
|
-
}
|
|
2173
|
-
}
|
|
2174
|
-
if (stored > 0) {
|
|
2175
|
-
console.log(
|
|
2176
|
-
`
|
|
2177
|
-
${stored} conflict(s) stored in DB for later resolution.`
|
|
2178
|
-
);
|
|
2179
|
-
}
|
|
2180
|
-
}
|
|
2181
2324
|
const syncDurationMs = Date.now() - syncStartTime;
|
|
2182
2325
|
await apiPost("/sync/state", {
|
|
2183
2326
|
repo_id: repoId,
|
|
@@ -2194,9 +2337,13 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
|
|
|
2194
2337
|
const syncTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2195
2338
|
const fileRepoUpdates = [];
|
|
2196
2339
|
for (const p of toPull) {
|
|
2197
|
-
if (p.
|
|
2340
|
+
if (p.remoteContent !== null) {
|
|
2198
2341
|
fileRepoUpdates.push({
|
|
2199
|
-
claude_file_id: p.claudeFileId,
|
|
2342
|
+
claude_file_id: p.claudeFileId ?? void 0,
|
|
2343
|
+
file_type: p.type,
|
|
2344
|
+
file_name: p.name,
|
|
2345
|
+
file_category: p.category,
|
|
2346
|
+
file_scope: p.scope,
|
|
2200
2347
|
last_synced_at: syncTimestamp,
|
|
2201
2348
|
last_synced_content_hash: contentHash(p.remoteContent),
|
|
2202
2349
|
sync_status: "synced"
|
|
@@ -2204,9 +2351,13 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
|
|
|
2204
2351
|
}
|
|
2205
2352
|
}
|
|
2206
2353
|
for (const p of toPush) {
|
|
2207
|
-
if (p.
|
|
2354
|
+
if (p.localContent !== null) {
|
|
2208
2355
|
fileRepoUpdates.push({
|
|
2209
|
-
claude_file_id: p.claudeFileId,
|
|
2356
|
+
claude_file_id: p.claudeFileId ?? void 0,
|
|
2357
|
+
file_type: p.type,
|
|
2358
|
+
file_name: p.name,
|
|
2359
|
+
file_category: p.category,
|
|
2360
|
+
file_scope: p.scope,
|
|
2210
2361
|
last_synced_at: syncTimestamp,
|
|
2211
2362
|
last_synced_content_hash: contentHash(p.localContent),
|
|
2212
2363
|
sync_status: "synced"
|
|
@@ -2227,6 +2378,36 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
|
|
|
2227
2378
|
Applied: ${toPull.length} pulled, ${toPush.length} pushed, ${toDelete.length} deleted` + (skipped.length > 0 ? `, ${skipped.length} skipped` : "")
|
|
2228
2379
|
);
|
|
2229
2380
|
}
|
|
2381
|
+
const unresolvedConflicts = plan.filter(
|
|
2382
|
+
(p) => p.action === "conflict" || p.action === "skip" && p.localContent !== null && p.remoteContent !== null
|
|
2383
|
+
);
|
|
2384
|
+
if (unresolvedConflicts.length > 0) {
|
|
2385
|
+
let stored = 0;
|
|
2386
|
+
for (const p of unresolvedConflicts) {
|
|
2387
|
+
try {
|
|
2388
|
+
await apiPost("/sync/conflicts", {
|
|
2389
|
+
repo_id: repoId,
|
|
2390
|
+
claude_file_id: p.claudeFileId ?? void 0,
|
|
2391
|
+
file_type: p.type,
|
|
2392
|
+
file_name: p.name,
|
|
2393
|
+
file_category: p.category,
|
|
2394
|
+
file_scope: p.scope,
|
|
2395
|
+
conflict_type: "both_modified",
|
|
2396
|
+
local_content: p.localContent,
|
|
2397
|
+
remote_content: p.remoteContent
|
|
2398
|
+
});
|
|
2399
|
+
stored++;
|
|
2400
|
+
} catch (err) {
|
|
2401
|
+
console.error(`Failed to store conflict for ${p.displayPath}:`, err);
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
if (stored > 0) {
|
|
2405
|
+
console.log(
|
|
2406
|
+
`
|
|
2407
|
+
${stored} conflict(s) stored in DB for later resolution.`
|
|
2408
|
+
);
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2230
2411
|
} else if (dryRun) {
|
|
2231
2412
|
console.log("\n (dry-run \u2014 no changes)");
|
|
2232
2413
|
}
|
|
@@ -2521,7 +2702,7 @@ function getLocalFilePath(claudeDir, projectPath, remote) {
|
|
|
2521
2702
|
}
|
|
2522
2703
|
function getSyncVersion() {
|
|
2523
2704
|
try {
|
|
2524
|
-
return "1.
|
|
2705
|
+
return "1.1.0";
|
|
2525
2706
|
} catch {
|
|
2526
2707
|
return "unknown";
|
|
2527
2708
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codebyplan",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "CLI for CodeByPlan — AI-powered development planning and tracking",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
"build": "tsc",
|
|
15
15
|
"build:npm": "node esbuild.npm.mjs",
|
|
16
16
|
"prepublishOnly": "npm run build:npm",
|
|
17
|
+
"lint": "eslint",
|
|
18
|
+
"lint:fix": "eslint --fix",
|
|
19
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
20
|
+
"format:check": "prettier --check \"src/**/*.ts\"",
|
|
17
21
|
"test": "vitest run",
|
|
18
22
|
"test:watch": "vitest",
|
|
19
23
|
"test:coverage": "vitest run --coverage"
|
|
@@ -40,9 +44,18 @@
|
|
|
40
44
|
"node": ">=18"
|
|
41
45
|
},
|
|
42
46
|
"devDependencies": {
|
|
47
|
+
"@eslint/js": "^9.18.0",
|
|
43
48
|
"@types/node": "^20",
|
|
49
|
+
"@vitest/eslint-plugin": "^1.1.44",
|
|
44
50
|
"esbuild": "^0.25",
|
|
51
|
+
"eslint": "^9.18.0",
|
|
52
|
+
"eslint-config-prettier": "^10.0.1",
|
|
53
|
+
"eslint-plugin-no-secrets": "^2.2.1",
|
|
54
|
+
"eslint-plugin-prettier": "^5.2.2",
|
|
55
|
+
"eslint-plugin-security": "^3.0.1",
|
|
56
|
+
"globals": "^17.0.0",
|
|
45
57
|
"typescript": "^5",
|
|
58
|
+
"typescript-eslint": "^8.20.0",
|
|
46
59
|
"vitest": "^4.0.18"
|
|
47
60
|
}
|
|
48
61
|
}
|