codebyplan 1.0.0 → 1.2.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 +396 -134
- 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.2.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,17 +993,58 @@ 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);
|
|
1041
|
+
await scanCategorizedType(
|
|
1042
|
+
join5(claudeDir, "context"),
|
|
1043
|
+
"context",
|
|
1044
|
+
".md",
|
|
1045
|
+
result
|
|
1046
|
+
);
|
|
1047
|
+
await scanDocsRecursive(join5(claudeDir, "docs"), result);
|
|
932
1048
|
await scanSettings(claudeDir, projectPath, result);
|
|
933
1049
|
return result;
|
|
934
1050
|
}
|
|
@@ -944,14 +1060,19 @@ async function scanCommandsRecursive(baseDir, currentDir, result) {
|
|
|
944
1060
|
}
|
|
945
1061
|
for (const entry of entries) {
|
|
946
1062
|
if (entry.isDirectory()) {
|
|
947
|
-
await scanCommandsRecursive(
|
|
1063
|
+
await scanCommandsRecursive(
|
|
1064
|
+
baseDir,
|
|
1065
|
+
join5(currentDir, entry.name),
|
|
1066
|
+
result
|
|
1067
|
+
);
|
|
948
1068
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
949
1069
|
const name = entry.name.slice(0, -3);
|
|
950
1070
|
const content = await readFile5(join5(currentDir, entry.name), "utf-8");
|
|
951
1071
|
const relDir = currentDir.slice(baseDir.length + 1);
|
|
952
1072
|
const category = relDir || null;
|
|
1073
|
+
const scope = extractScope(content, "command");
|
|
953
1074
|
const key = compositeKey("command", name, category);
|
|
954
|
-
result.set(key, { type: "command", name, category, content });
|
|
1075
|
+
result.set(key, { type: "command", name, category, content, scope });
|
|
955
1076
|
}
|
|
956
1077
|
}
|
|
957
1078
|
}
|
|
@@ -967,8 +1088,15 @@ async function scanSubfolderType(dir, type, fileName, result) {
|
|
|
967
1088
|
const filePath = join5(dir, entry.name, fileName);
|
|
968
1089
|
try {
|
|
969
1090
|
const content = await readFile5(filePath, "utf-8");
|
|
1091
|
+
const scope = extractScope(content, type);
|
|
970
1092
|
const key = compositeKey(type, entry.name, null);
|
|
971
|
-
result.set(key, {
|
|
1093
|
+
result.set(key, {
|
|
1094
|
+
type,
|
|
1095
|
+
name: entry.name,
|
|
1096
|
+
category: null,
|
|
1097
|
+
content,
|
|
1098
|
+
scope
|
|
1099
|
+
});
|
|
972
1100
|
} catch {
|
|
973
1101
|
}
|
|
974
1102
|
}
|
|
@@ -985,8 +1113,72 @@ async function scanFlatType(dir, type, ext, result) {
|
|
|
985
1113
|
if (entry.isFile() && entry.name.endsWith(ext)) {
|
|
986
1114
|
const name = entry.name.slice(0, -ext.length);
|
|
987
1115
|
const content = await readFile5(join5(dir, entry.name), "utf-8");
|
|
1116
|
+
const scope = extractScope(content, type);
|
|
988
1117
|
const key = compositeKey(type, name, null);
|
|
989
|
-
result.set(key, { type, name, category: null, content });
|
|
1118
|
+
result.set(key, { type, name, category: null, content, scope });
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
async function scanCategorizedType(dir, type, ext, result) {
|
|
1123
|
+
let entries;
|
|
1124
|
+
try {
|
|
1125
|
+
entries = await readdir3(dir, { withFileTypes: true });
|
|
1126
|
+
} catch {
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
for (const entry of entries) {
|
|
1130
|
+
if (entry.isDirectory()) {
|
|
1131
|
+
const category = entry.name;
|
|
1132
|
+
let subEntries;
|
|
1133
|
+
try {
|
|
1134
|
+
subEntries = await readdir3(join5(dir, category), {
|
|
1135
|
+
withFileTypes: true
|
|
1136
|
+
});
|
|
1137
|
+
} catch {
|
|
1138
|
+
continue;
|
|
1139
|
+
}
|
|
1140
|
+
for (const sub of subEntries) {
|
|
1141
|
+
if (sub.isFile() && sub.name.endsWith(ext)) {
|
|
1142
|
+
const name = sub.name.slice(0, -ext.length);
|
|
1143
|
+
const content = await readFile5(
|
|
1144
|
+
join5(dir, category, sub.name),
|
|
1145
|
+
"utf-8"
|
|
1146
|
+
);
|
|
1147
|
+
const scope = extractScope(content, type);
|
|
1148
|
+
const key = compositeKey(type, name, category);
|
|
1149
|
+
result.set(key, { type, name, category, content, scope });
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
} else if (entry.isFile() && entry.name.endsWith(ext)) {
|
|
1153
|
+
const name = entry.name.slice(0, -ext.length);
|
|
1154
|
+
const content = await readFile5(join5(dir, entry.name), "utf-8");
|
|
1155
|
+
const scope = extractScope(content, type);
|
|
1156
|
+
const key = compositeKey(type, name, null);
|
|
1157
|
+
result.set(key, { type, name, category: null, content, scope });
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
async function scanDocsRecursive(docsDir, result) {
|
|
1162
|
+
await scanDocsDir(docsDir, docsDir, result);
|
|
1163
|
+
}
|
|
1164
|
+
async function scanDocsDir(baseDir, currentDir, result) {
|
|
1165
|
+
let entries;
|
|
1166
|
+
try {
|
|
1167
|
+
entries = await readdir3(currentDir, { withFileTypes: true });
|
|
1168
|
+
} catch {
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
for (const entry of entries) {
|
|
1172
|
+
if (entry.isDirectory()) {
|
|
1173
|
+
await scanDocsDir(baseDir, join5(currentDir, entry.name), result);
|
|
1174
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1175
|
+
const name = entry.name.slice(0, -3);
|
|
1176
|
+
const content = await readFile5(join5(currentDir, entry.name), "utf-8");
|
|
1177
|
+
const scope = extractScope(content, "docs");
|
|
1178
|
+
const relDir = currentDir.slice(baseDir.length + 1);
|
|
1179
|
+
const category = relDir || null;
|
|
1180
|
+
const key = compositeKey("docs", name, category);
|
|
1181
|
+
result.set(key, { type: "docs", name, category, content, scope });
|
|
990
1182
|
}
|
|
991
1183
|
}
|
|
992
1184
|
}
|
|
@@ -1000,8 +1192,15 @@ async function scanTemplates(dir, result) {
|
|
|
1000
1192
|
for (const entry of entries) {
|
|
1001
1193
|
if (entry.isFile() && extname(entry.name)) {
|
|
1002
1194
|
const content = await readFile5(join5(dir, entry.name), "utf-8");
|
|
1195
|
+
const scope = extractScope(content, "template");
|
|
1003
1196
|
const key = compositeKey("template", entry.name, null);
|
|
1004
|
-
result.set(key, {
|
|
1197
|
+
result.set(key, {
|
|
1198
|
+
type: "template",
|
|
1199
|
+
name: entry.name,
|
|
1200
|
+
category: null,
|
|
1201
|
+
content,
|
|
1202
|
+
scope
|
|
1203
|
+
});
|
|
1005
1204
|
}
|
|
1006
1205
|
}
|
|
1007
1206
|
}
|
|
@@ -1035,7 +1234,13 @@ async function scanSettings(claudeDir, projectPath, result) {
|
|
|
1035
1234
|
}
|
|
1036
1235
|
const content = JSON.stringify(parsed, null, 2) + "\n";
|
|
1037
1236
|
const key = compositeKey("settings", "settings", null);
|
|
1038
|
-
result.set(key, {
|
|
1237
|
+
result.set(key, {
|
|
1238
|
+
type: "settings",
|
|
1239
|
+
name: "settings",
|
|
1240
|
+
category: null,
|
|
1241
|
+
content,
|
|
1242
|
+
scope: "shared"
|
|
1243
|
+
});
|
|
1039
1244
|
}
|
|
1040
1245
|
var init_fileMapper = __esm({
|
|
1041
1246
|
"src/cli/fileMapper.ts"() {
|
|
@@ -1069,7 +1274,9 @@ async function confirmProceed(message) {
|
|
|
1069
1274
|
const a = answer.trim().toLowerCase();
|
|
1070
1275
|
if (a === "" || a === "y" || a === "yes") return true;
|
|
1071
1276
|
if (a === "n" || a === "no") return false;
|
|
1072
|
-
console.log(
|
|
1277
|
+
console.log(
|
|
1278
|
+
` Unknown option "${answer.trim()}". Valid: y/yes, n/no, or Enter for yes.`
|
|
1279
|
+
);
|
|
1073
1280
|
}
|
|
1074
1281
|
} catch (err) {
|
|
1075
1282
|
if (isAbortError(err)) throw new SyncCancelledError();
|
|
@@ -1202,11 +1409,16 @@ async function promptReviewMode() {
|
|
|
1202
1409
|
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
1203
1410
|
try {
|
|
1204
1411
|
while (true) {
|
|
1205
|
-
const answer = await rl.question(
|
|
1412
|
+
const answer = await rl.question(
|
|
1413
|
+
" Review [o]ne-by-one or [f]older-by-folder? "
|
|
1414
|
+
);
|
|
1206
1415
|
const a = answer.trim().toLowerCase();
|
|
1207
|
-
if (a === "o" || a === "one-by-one" || a === "one" || a === "file")
|
|
1416
|
+
if (a === "o" || a === "one-by-one" || a === "one" || a === "file")
|
|
1417
|
+
return "file";
|
|
1208
1418
|
if (a === "f" || a === "folder") return "folder";
|
|
1209
|
-
console.log(
|
|
1419
|
+
console.log(
|
|
1420
|
+
` Unknown option "${answer.trim()}". Valid: o/one-by-one, f/folder`
|
|
1421
|
+
);
|
|
1210
1422
|
}
|
|
1211
1423
|
} catch (err) {
|
|
1212
1424
|
if (isAbortError(err)) throw new SyncCancelledError();
|
|
@@ -1245,7 +1457,9 @@ async function reviewFilesOneByOne(items, label, plannedAction, recommendedActio
|
|
|
1245
1457
|
break;
|
|
1246
1458
|
}
|
|
1247
1459
|
if (result.action === null) {
|
|
1248
|
-
console.log(
|
|
1460
|
+
console.log(
|
|
1461
|
+
` Unknown option "${answer.trim()}". Valid: ${formatActionPrompt(rec, hasContent, false)}`
|
|
1462
|
+
);
|
|
1249
1463
|
continue;
|
|
1250
1464
|
}
|
|
1251
1465
|
results.push(result.action);
|
|
@@ -1282,7 +1496,13 @@ async function reviewFolder(folderName, items, label, plannedAction, recommended
|
|
|
1282
1496
|
const a = answer.trim().toLowerCase();
|
|
1283
1497
|
if (a === "o" || a === "one-by-one") {
|
|
1284
1498
|
rl.close();
|
|
1285
|
-
return reviewFilesOneByOne(
|
|
1499
|
+
return reviewFilesOneByOne(
|
|
1500
|
+
items,
|
|
1501
|
+
label,
|
|
1502
|
+
plannedAction,
|
|
1503
|
+
recommendedAction,
|
|
1504
|
+
content
|
|
1505
|
+
);
|
|
1286
1506
|
}
|
|
1287
1507
|
if (a === "r" || a === "recommended") {
|
|
1288
1508
|
return items.map(
|
|
@@ -1303,11 +1523,13 @@ async function reviewFolder(folderName, items, label, plannedAction, recommended
|
|
|
1303
1523
|
if (result.action !== null) {
|
|
1304
1524
|
return items.map(() => result.action);
|
|
1305
1525
|
}
|
|
1306
|
-
console.log(
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1526
|
+
console.log(
|
|
1527
|
+
` Unknown option "${answer.trim()}". Valid: ${formatActionPrompt(
|
|
1528
|
+
recommendedAction ? recommendedAction(items[0]) : plannedAction(items[0]),
|
|
1529
|
+
false,
|
|
1530
|
+
true
|
|
1531
|
+
)} [o]ne-by-one`
|
|
1532
|
+
);
|
|
1311
1533
|
}
|
|
1312
1534
|
} catch (err) {
|
|
1313
1535
|
if (isAbortError(err)) throw new SyncCancelledError();
|
|
@@ -1893,7 +2115,10 @@ async function runSync() {
|
|
|
1893
2115
|
if (!dryRun) {
|
|
1894
2116
|
try {
|
|
1895
2117
|
await apiDelete("/sync/lock", { repo_id: repoId });
|
|
1896
|
-
} catch {
|
|
2118
|
+
} catch (err) {
|
|
2119
|
+
console.error(
|
|
2120
|
+
` Warning: failed to release sync lock: ${err instanceof Error ? err.message : String(err)}`
|
|
2121
|
+
);
|
|
1897
2122
|
}
|
|
1898
2123
|
}
|
|
1899
2124
|
}
|
|
@@ -1906,37 +2131,42 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
|
|
|
1906
2131
|
localFiles = await scanLocalFiles(claudeDir, projectPath);
|
|
1907
2132
|
} catch {
|
|
1908
2133
|
}
|
|
1909
|
-
const [defaultsRes, repoSyncRes, repoRes,
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
2134
|
+
const [defaultsRes, repoSyncRes, repoRes, , fileReposRes] = await Promise.all(
|
|
2135
|
+
[
|
|
2136
|
+
apiGet("/sync/defaults"),
|
|
2137
|
+
apiGet("/sync/files", { repo_id: repoId }),
|
|
2138
|
+
apiGet(`/repos/${repoId}`),
|
|
2139
|
+
apiGet("/sync/state", {
|
|
2140
|
+
repo_id: repoId
|
|
2141
|
+
}),
|
|
2142
|
+
apiGet("/sync/file-repos", {
|
|
2143
|
+
repo_id: repoId
|
|
2144
|
+
})
|
|
2145
|
+
]
|
|
2146
|
+
);
|
|
1920
2147
|
const syncStartTime = Date.now();
|
|
1921
2148
|
const repoData = repoRes.data;
|
|
1922
2149
|
const remoteDefaults = flattenSyncData(defaultsRes.data);
|
|
1923
2150
|
const remoteRepoFiles = flattenSyncData(repoSyncRes.data);
|
|
1924
|
-
const syncState = syncStateRes.data;
|
|
1925
2151
|
const fileRepoHashes = /* @__PURE__ */ new Map();
|
|
1926
2152
|
const fileRepoByClaudeFileId = /* @__PURE__ */ new Map();
|
|
1927
2153
|
for (const entry of fileReposRes.data ?? []) {
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
2154
|
+
const baseKey = compositeKey(
|
|
2155
|
+
entry.file_type,
|
|
2156
|
+
entry.file_name,
|
|
2157
|
+
entry.file_category
|
|
2158
|
+
);
|
|
2159
|
+
const scopedKey = `${baseKey}:${entry.file_scope}`;
|
|
2160
|
+
fileRepoHashes.set(scopedKey, entry.last_synced_content_hash);
|
|
2161
|
+
if (!fileRepoHashes.has(baseKey)) {
|
|
2162
|
+
fileRepoHashes.set(baseKey, entry.last_synced_content_hash);
|
|
2163
|
+
}
|
|
2164
|
+
if (entry.claude_file_id) {
|
|
2165
|
+
fileRepoByClaudeFileId.set(
|
|
2166
|
+
entry.claude_file_id,
|
|
2167
|
+
entry.last_synced_content_hash
|
|
1933
2168
|
);
|
|
1934
|
-
fileRepoHashes.set(key, entry.last_synced_content_hash);
|
|
1935
2169
|
}
|
|
1936
|
-
fileRepoByClaudeFileId.set(
|
|
1937
|
-
entry.claude_file_id,
|
|
1938
|
-
entry.last_synced_content_hash
|
|
1939
|
-
);
|
|
1940
2170
|
}
|
|
1941
2171
|
const remoteFiles = new Map([...remoteDefaults, ...remoteRepoFiles]);
|
|
1942
2172
|
console.log(
|
|
@@ -1965,6 +2195,7 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
|
|
|
1965
2195
|
type: local.type,
|
|
1966
2196
|
name: local.name,
|
|
1967
2197
|
category: local.category,
|
|
2198
|
+
scope: local.scope,
|
|
1968
2199
|
isHook: local.type === "hook",
|
|
1969
2200
|
claudeFileId: null
|
|
1970
2201
|
});
|
|
@@ -1984,6 +2215,7 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
|
|
|
1984
2215
|
type: remote.type,
|
|
1985
2216
|
name: remote.name,
|
|
1986
2217
|
category: remote.category ?? null,
|
|
2218
|
+
scope: remote.scope ?? "shared",
|
|
1987
2219
|
isHook: remote.type === "hook",
|
|
1988
2220
|
claudeFileId: remote.id ?? null
|
|
1989
2221
|
});
|
|
@@ -1993,7 +2225,8 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
|
|
|
1993
2225
|
continue;
|
|
1994
2226
|
}
|
|
1995
2227
|
const localHash = contentHash(local.content);
|
|
1996
|
-
const
|
|
2228
|
+
const scopedKey = `${key}:${local.scope}`;
|
|
2229
|
+
const lastSyncedHash = fileRepoHashes.get(scopedKey) ?? fileRepoHashes.get(key) ?? null;
|
|
1997
2230
|
const localChanged = lastSyncedHash ? localHash !== lastSyncedHash : true;
|
|
1998
2231
|
let action;
|
|
1999
2232
|
if (force) {
|
|
@@ -2003,8 +2236,8 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
|
|
|
2003
2236
|
} else if (lastSyncedHash === null) {
|
|
2004
2237
|
action = "conflict";
|
|
2005
2238
|
} else {
|
|
2006
|
-
const
|
|
2007
|
-
const remoteChanged =
|
|
2239
|
+
const remoteResolvedHash = contentHash(resolvedRemote);
|
|
2240
|
+
const remoteChanged = remoteResolvedHash !== lastSyncedHash;
|
|
2008
2241
|
if (remoteChanged) {
|
|
2009
2242
|
action = "conflict";
|
|
2010
2243
|
} else {
|
|
@@ -2023,6 +2256,7 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
|
|
|
2023
2256
|
type: local.type,
|
|
2024
2257
|
name: local.name,
|
|
2025
2258
|
category: local.category,
|
|
2259
|
+
scope: local.scope,
|
|
2026
2260
|
isHook: local.type === "hook",
|
|
2027
2261
|
claudeFileId: remote.id ?? null
|
|
2028
2262
|
});
|
|
@@ -2123,7 +2357,8 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
|
|
|
2123
2357
|
type: p.type,
|
|
2124
2358
|
name: p.name,
|
|
2125
2359
|
category: p.category,
|
|
2126
|
-
content: p.pushContent
|
|
2360
|
+
content: p.pushContent,
|
|
2361
|
+
scope: p.scope
|
|
2127
2362
|
}));
|
|
2128
2363
|
if (toUpsert.length > 0) {
|
|
2129
2364
|
await apiPost("/sync/files", {
|
|
@@ -2146,38 +2381,16 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
|
|
|
2146
2381
|
if (p.filePath) {
|
|
2147
2382
|
try {
|
|
2148
2383
|
await unlink2(p.filePath);
|
|
2149
|
-
} catch {
|
|
2384
|
+
} catch (err) {
|
|
2385
|
+
if (err instanceof Error && "code" in err && err.code !== "ENOENT") {
|
|
2386
|
+
console.error(
|
|
2387
|
+
` Warning: failed to delete ${p.filePath}: ${err.message}`
|
|
2388
|
+
);
|
|
2389
|
+
}
|
|
2150
2390
|
}
|
|
2151
2391
|
}
|
|
2152
2392
|
}
|
|
2153
2393
|
}
|
|
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
2394
|
const syncDurationMs = Date.now() - syncStartTime;
|
|
2182
2395
|
await apiPost("/sync/state", {
|
|
2183
2396
|
repo_id: repoId,
|
|
@@ -2194,9 +2407,13 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
|
|
|
2194
2407
|
const syncTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2195
2408
|
const fileRepoUpdates = [];
|
|
2196
2409
|
for (const p of toPull) {
|
|
2197
|
-
if (p.
|
|
2410
|
+
if (p.remoteContent !== null) {
|
|
2198
2411
|
fileRepoUpdates.push({
|
|
2199
|
-
claude_file_id: p.claudeFileId,
|
|
2412
|
+
claude_file_id: p.claudeFileId ?? void 0,
|
|
2413
|
+
file_type: p.type,
|
|
2414
|
+
file_name: p.name,
|
|
2415
|
+
file_category: p.category,
|
|
2416
|
+
file_scope: p.scope,
|
|
2200
2417
|
last_synced_at: syncTimestamp,
|
|
2201
2418
|
last_synced_content_hash: contentHash(p.remoteContent),
|
|
2202
2419
|
sync_status: "synced"
|
|
@@ -2204,9 +2421,13 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
|
|
|
2204
2421
|
}
|
|
2205
2422
|
}
|
|
2206
2423
|
for (const p of toPush) {
|
|
2207
|
-
if (p.
|
|
2424
|
+
if (p.localContent !== null) {
|
|
2208
2425
|
fileRepoUpdates.push({
|
|
2209
|
-
claude_file_id: p.claudeFileId,
|
|
2426
|
+
claude_file_id: p.claudeFileId ?? void 0,
|
|
2427
|
+
file_type: p.type,
|
|
2428
|
+
file_name: p.name,
|
|
2429
|
+
file_category: p.category,
|
|
2430
|
+
file_scope: p.scope,
|
|
2210
2431
|
last_synced_at: syncTimestamp,
|
|
2211
2432
|
last_synced_content_hash: contentHash(p.localContent),
|
|
2212
2433
|
sync_status: "synced"
|
|
@@ -2227,6 +2448,36 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
|
|
|
2227
2448
|
Applied: ${toPull.length} pulled, ${toPush.length} pushed, ${toDelete.length} deleted` + (skipped.length > 0 ? `, ${skipped.length} skipped` : "")
|
|
2228
2449
|
);
|
|
2229
2450
|
}
|
|
2451
|
+
const unresolvedConflicts = plan.filter(
|
|
2452
|
+
(p) => p.action === "conflict" || p.action === "skip" && p.localContent !== null && p.remoteContent !== null
|
|
2453
|
+
);
|
|
2454
|
+
if (unresolvedConflicts.length > 0) {
|
|
2455
|
+
let stored = 0;
|
|
2456
|
+
for (const p of unresolvedConflicts) {
|
|
2457
|
+
try {
|
|
2458
|
+
await apiPost("/sync/conflicts", {
|
|
2459
|
+
repo_id: repoId,
|
|
2460
|
+
claude_file_id: p.claudeFileId ?? void 0,
|
|
2461
|
+
file_type: p.type,
|
|
2462
|
+
file_name: p.name,
|
|
2463
|
+
file_category: p.category,
|
|
2464
|
+
file_scope: p.scope,
|
|
2465
|
+
conflict_type: "both_modified",
|
|
2466
|
+
local_content: p.localContent,
|
|
2467
|
+
remote_content: p.remoteContent
|
|
2468
|
+
});
|
|
2469
|
+
stored++;
|
|
2470
|
+
} catch (err) {
|
|
2471
|
+
console.error(`Failed to store conflict for ${p.displayPath}:`, err);
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
if (stored > 0) {
|
|
2475
|
+
console.log(
|
|
2476
|
+
`
|
|
2477
|
+
${stored} conflict(s) stored in DB for later resolution.`
|
|
2478
|
+
);
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2230
2481
|
} else if (dryRun) {
|
|
2231
2482
|
console.log("\n (dry-run \u2014 no changes)");
|
|
2232
2483
|
}
|
|
@@ -2486,7 +2737,10 @@ function groupByType(items) {
|
|
|
2486
2737
|
rule: "Rules",
|
|
2487
2738
|
hook: "Hooks",
|
|
2488
2739
|
template: "Templates",
|
|
2489
|
-
settings: "Settings"
|
|
2740
|
+
settings: "Settings",
|
|
2741
|
+
context: "Context",
|
|
2742
|
+
docs_stack: "Stack Docs",
|
|
2743
|
+
docs: "Docs"
|
|
2490
2744
|
};
|
|
2491
2745
|
for (const item of items) {
|
|
2492
2746
|
const label = typeLabels[item.type] ?? item.type;
|
|
@@ -2504,6 +2758,9 @@ function getLocalFilePath(claudeDir, projectPath, remote) {
|
|
|
2504
2758
|
rule: { dir: "rules", ext: ".md" },
|
|
2505
2759
|
hook: { dir: "hooks", ext: ".sh" },
|
|
2506
2760
|
template: { dir: "templates", ext: "" },
|
|
2761
|
+
context: { dir: "context", ext: ".md" },
|
|
2762
|
+
docs_stack: { dir: join7("docs", "stack"), ext: ".md" },
|
|
2763
|
+
docs: { dir: "docs", ext: ".md" },
|
|
2507
2764
|
claude_md: { dir: "", ext: "" },
|
|
2508
2765
|
settings: { dir: "", ext: "" }
|
|
2509
2766
|
};
|
|
@@ -2517,11 +2774,13 @@ function getLocalFilePath(claudeDir, projectPath, remote) {
|
|
|
2517
2774
|
if (remote.type === "command" && remote.category)
|
|
2518
2775
|
return join7(typeDir, remote.category, `${remote.name}${cfg.ext}`);
|
|
2519
2776
|
if (remote.type === "template") return join7(typeDir, remote.name);
|
|
2777
|
+
if (remote.category && (remote.type === "context" || remote.type === "docs_stack" || remote.type === "docs"))
|
|
2778
|
+
return join7(typeDir, remote.category, `${remote.name}${cfg.ext}`);
|
|
2520
2779
|
return join7(typeDir, `${remote.name}${cfg.ext}`);
|
|
2521
2780
|
}
|
|
2522
2781
|
function getSyncVersion() {
|
|
2523
2782
|
try {
|
|
2524
|
-
return "1.
|
|
2783
|
+
return "1.2.0";
|
|
2525
2784
|
} catch {
|
|
2526
2785
|
return "unknown";
|
|
2527
2786
|
}
|
|
@@ -2535,7 +2794,10 @@ function flattenSyncData(data) {
|
|
|
2535
2794
|
rules: "rule",
|
|
2536
2795
|
hooks: "hook",
|
|
2537
2796
|
templates: "template",
|
|
2538
|
-
settings: "settings"
|
|
2797
|
+
settings: "settings",
|
|
2798
|
+
contexts: "context",
|
|
2799
|
+
docs_stack: "docs_stack",
|
|
2800
|
+
docs: "docs"
|
|
2539
2801
|
};
|
|
2540
2802
|
for (const [syncKey, typeName] of Object.entries(typeMap)) {
|
|
2541
2803
|
const files = data[syncKey] ?? [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codebyplan",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.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
|
}
|