codebyplan 1.4.3 → 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 +671 -2240
- package/package.json +2 -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.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"() {
|
|
@@ -198,6 +195,25 @@ async function resolveAndCacheWorktreeId(repoId, projectPath, options) {
|
|
|
198
195
|
}
|
|
199
196
|
return worktreeId;
|
|
200
197
|
}
|
|
198
|
+
async function resolveWorktreeId({
|
|
199
|
+
repoId,
|
|
200
|
+
repoPath,
|
|
201
|
+
branch,
|
|
202
|
+
deviceId
|
|
203
|
+
}) {
|
|
204
|
+
try {
|
|
205
|
+
const res = await apiPost(
|
|
206
|
+
"/worktrees/resolve",
|
|
207
|
+
{ repo_id: repoId, device_id: deviceId, repo_path: repoPath, branch }
|
|
208
|
+
);
|
|
209
|
+
return res.worktree_id ?? null;
|
|
210
|
+
} catch (err) {
|
|
211
|
+
console.error(
|
|
212
|
+
`Tuple worktree resolve failed: ${err instanceof Error ? err.message : String(err)}`
|
|
213
|
+
);
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
201
217
|
var init_resolve_worktree = __esm({
|
|
202
218
|
"src/lib/resolve-worktree.ts"() {
|
|
203
219
|
"use strict";
|
|
@@ -205,630 +221,75 @@ var init_resolve_worktree = __esm({
|
|
|
205
221
|
}
|
|
206
222
|
});
|
|
207
223
|
|
|
208
|
-
// src/lib/
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
merged[key] = template[key];
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
if (template.permissions && typeof template.permissions === "object") {
|
|
217
|
-
const templatePerms = template.permissions;
|
|
218
|
-
const localPerms = local.permissions && typeof local.permissions === "object" ? local.permissions : {};
|
|
219
|
-
const mergedPerms = { ...localPerms };
|
|
220
|
-
for (const key of TEMPLATE_MANAGED_PERMISSION_KEYS) {
|
|
221
|
-
if (key in templatePerms) {
|
|
222
|
-
mergedPerms[key] = templatePerms[key];
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
merged.permissions = mergedPerms;
|
|
226
|
-
}
|
|
227
|
-
return merged;
|
|
228
|
-
}
|
|
229
|
-
function mergeGlobalAndRepoSettings(global, repo) {
|
|
230
|
-
const merged = { ...global, ...repo };
|
|
231
|
-
const globalPerms = global.permissions && typeof global.permissions === "object" ? global.permissions : {};
|
|
232
|
-
const repoPerms = repo.permissions && typeof repo.permissions === "object" ? repo.permissions : {};
|
|
233
|
-
if (Object.keys(globalPerms).length > 0 || Object.keys(repoPerms).length > 0) {
|
|
234
|
-
const mergedPerms = { ...globalPerms, ...repoPerms };
|
|
235
|
-
for (const key of ARRAY_PERMISSION_KEYS) {
|
|
236
|
-
const globalArr = Array.isArray(globalPerms[key]) ? globalPerms[key] : [];
|
|
237
|
-
const repoArr = Array.isArray(repoPerms[key]) ? repoPerms[key] : [];
|
|
238
|
-
if (globalArr.length > 0 || repoArr.length > 0) {
|
|
239
|
-
mergedPerms[key] = [.../* @__PURE__ */ new Set([...globalArr, ...repoArr])];
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
merged.permissions = mergedPerms;
|
|
243
|
-
}
|
|
244
|
-
return merged;
|
|
245
|
-
}
|
|
246
|
-
function stripPermissionsAllow(settings) {
|
|
247
|
-
if (!settings.permissions || typeof settings.permissions !== "object") {
|
|
248
|
-
return settings;
|
|
249
|
-
}
|
|
250
|
-
const perms = { ...settings.permissions };
|
|
251
|
-
delete perms.allow;
|
|
252
|
-
if (Object.keys(perms).length === 0) {
|
|
253
|
-
const { permissions: _, ...rest } = settings;
|
|
254
|
-
return rest;
|
|
255
|
-
}
|
|
256
|
-
return { ...settings, permissions: perms };
|
|
257
|
-
}
|
|
258
|
-
var TEMPLATE_MANAGED_KEYS, TEMPLATE_MANAGED_PERMISSION_KEYS, ARRAY_PERMISSION_KEYS;
|
|
259
|
-
var init_settings_merge = __esm({
|
|
260
|
-
"src/lib/settings-merge.ts"() {
|
|
261
|
-
"use strict";
|
|
262
|
-
TEMPLATE_MANAGED_KEYS = ["attribution", "hooks", "statusLine"];
|
|
263
|
-
TEMPLATE_MANAGED_PERMISSION_KEYS = [
|
|
264
|
-
"deny",
|
|
265
|
-
"ask",
|
|
266
|
-
"additionalDirectories"
|
|
267
|
-
];
|
|
268
|
-
ARRAY_PERMISSION_KEYS = ["deny", "ask"];
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
// src/lib/hook-registry.ts
|
|
273
|
-
import { readdir, readFile as readFile2 } from "node:fs/promises";
|
|
224
|
+
// src/lib/local-config.ts
|
|
225
|
+
import { execSync } from "node:child_process";
|
|
226
|
+
import { createHash } from "node:crypto";
|
|
227
|
+
import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
228
|
+
import { hostname } from "node:os";
|
|
274
229
|
import { join as join2 } from "node:path";
|
|
275
|
-
function
|
|
276
|
-
|
|
277
|
-
if (!lineMatch) return null;
|
|
278
|
-
const parts = lineMatch[1].trim().split(/\s+/);
|
|
279
|
-
const event = parts[0];
|
|
280
|
-
if (!event) return null;
|
|
281
|
-
return {
|
|
282
|
-
event,
|
|
283
|
-
matcher: parts.slice(1).join(" ")
|
|
284
|
-
};
|
|
230
|
+
function localConfigPath(projectPath) {
|
|
231
|
+
return join2(projectPath, ".codebyplan.local.json");
|
|
285
232
|
}
|
|
286
|
-
async function
|
|
287
|
-
const discovered = /* @__PURE__ */ new Map();
|
|
288
|
-
let filenames;
|
|
233
|
+
async function readLocalConfig(projectPath) {
|
|
289
234
|
try {
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
for (const filename of filenames) {
|
|
296
|
-
const content = await readFile2(join2(hooksDir, filename), "utf-8");
|
|
297
|
-
const meta = parseHookMeta(content);
|
|
298
|
-
if (meta) {
|
|
299
|
-
discovered.set(filename.replace(/\.sh$/, ""), meta);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
return discovered;
|
|
303
|
-
}
|
|
304
|
-
function mergeDiscoveredHooks(existing, discovered, hooksRelPath = ".claude/hooks") {
|
|
305
|
-
if (discovered.size === 0) return existing;
|
|
306
|
-
const merged = {};
|
|
307
|
-
for (const [event, matchers] of Object.entries(existing)) {
|
|
308
|
-
merged[event] = matchers.map((m) => ({
|
|
309
|
-
matcher: m.matcher,
|
|
310
|
-
hooks: [...m.hooks]
|
|
311
|
-
}));
|
|
312
|
-
}
|
|
313
|
-
for (const [filename, meta] of discovered) {
|
|
314
|
-
const command = `bash ${hooksRelPath}/${filename}.sh`;
|
|
315
|
-
if (!merged[meta.event]) {
|
|
316
|
-
merged[meta.event] = [];
|
|
235
|
+
const raw = await readFile2(localConfigPath(projectPath), "utf-8");
|
|
236
|
+
const parsed = JSON.parse(raw);
|
|
237
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) && typeof parsed.device_id === "string") {
|
|
238
|
+
return parsed;
|
|
317
239
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
240
|
+
console.error("Failed to read local config: invalid shape");
|
|
241
|
+
return null;
|
|
242
|
+
} catch (err) {
|
|
243
|
+
console.error(
|
|
244
|
+
`Failed to read local config: ${err instanceof Error ? err.message : String(err)}`
|
|
321
245
|
);
|
|
322
|
-
|
|
323
|
-
const matcherEntry = eventEntries.find((m) => m.matcher === meta.matcher);
|
|
324
|
-
if (matcherEntry) {
|
|
325
|
-
matcherEntry.hooks.push({ type: "command", command });
|
|
326
|
-
} else {
|
|
327
|
-
eventEntries.push({
|
|
328
|
-
matcher: meta.matcher,
|
|
329
|
-
hooks: [{ type: "command", command }]
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
return merged;
|
|
334
|
-
}
|
|
335
|
-
function stripDiscoveredHooks(config, hooksRelPath = ".claude/hooks") {
|
|
336
|
-
const prefix = `bash ${hooksRelPath}/`;
|
|
337
|
-
const stripped = {};
|
|
338
|
-
for (const [event, matchers] of Object.entries(config)) {
|
|
339
|
-
const filteredMatchers = [];
|
|
340
|
-
for (const matcher of matchers) {
|
|
341
|
-
const filteredHooks = matcher.hooks.filter(
|
|
342
|
-
(h) => !(h.command && h.command.startsWith(prefix) && h.command.endsWith(".sh"))
|
|
343
|
-
);
|
|
344
|
-
if (filteredHooks.length > 0) {
|
|
345
|
-
filteredMatchers.push({
|
|
346
|
-
matcher: matcher.matcher,
|
|
347
|
-
hooks: filteredHooks
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
if (filteredMatchers.length > 0) {
|
|
352
|
-
stripped[event] = filteredMatchers;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
return stripped;
|
|
356
|
-
}
|
|
357
|
-
var init_hook_registry = __esm({
|
|
358
|
-
"src/lib/hook-registry.ts"() {
|
|
359
|
-
"use strict";
|
|
360
|
-
}
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
// src/lib/variables.ts
|
|
364
|
-
function splitFrontmatter(content) {
|
|
365
|
-
const fmMatch = content.match(/^(---\s*\n[\s\S]*?\n---\n?)([\s\S]*)$/);
|
|
366
|
-
if (fmMatch) {
|
|
367
|
-
return { frontmatter: fmMatch[1], body: fmMatch[2] };
|
|
368
|
-
}
|
|
369
|
-
if (content.startsWith("#!/") || content.startsWith("# @")) {
|
|
370
|
-
const lines = content.split("\n");
|
|
371
|
-
let headerEnd = 0;
|
|
372
|
-
for (let i = 0; i < lines.length; i++) {
|
|
373
|
-
if (lines[i].startsWith("#") || lines[i].startsWith("#!/") || lines[i].trim() === "") {
|
|
374
|
-
headerEnd = i + 1;
|
|
375
|
-
} else {
|
|
376
|
-
break;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
return {
|
|
380
|
-
frontmatter: lines.slice(0, headerEnd).join("\n") + "\n",
|
|
381
|
-
body: lines.slice(headerEnd).join("\n")
|
|
382
|
-
};
|
|
246
|
+
return null;
|
|
383
247
|
}
|
|
384
|
-
return { frontmatter: "", body: content };
|
|
385
248
|
}
|
|
386
|
-
function
|
|
387
|
-
|
|
388
|
-
const { frontmatter, body } = splitFrontmatter(content);
|
|
389
|
-
let result = body;
|
|
390
|
-
for (const [name, resolver] of Object.entries(TEMPLATE_VARIABLES)) {
|
|
391
|
-
const placeholder = `{{${name}}}`;
|
|
392
|
-
if (result.includes(placeholder)) {
|
|
393
|
-
result = result.replaceAll(placeholder, resolver(repoData));
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
return frontmatter + result;
|
|
397
|
-
}
|
|
398
|
-
function escapeRegex(str) {
|
|
399
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
400
|
-
}
|
|
401
|
-
function reverseSubstituteVariables(content, repoData) {
|
|
402
|
-
const { frontmatter, body } = splitFrontmatter(content);
|
|
403
|
-
const entries = [];
|
|
404
|
-
for (const [name, resolver] of Object.entries(TEMPLATE_VARIABLES)) {
|
|
405
|
-
const value = resolver(repoData);
|
|
406
|
-
if (value.length === 0) continue;
|
|
407
|
-
entries.push([value, `{{${name}}}`]);
|
|
408
|
-
}
|
|
409
|
-
entries.sort((a, b) => b[0].length - a[0].length);
|
|
410
|
-
let result = body;
|
|
411
|
-
for (const [value, placeholder] of entries) {
|
|
412
|
-
if (value.length < 8) {
|
|
413
|
-
const pattern = new RegExp(`\\b${escapeRegex(value)}\\b`, "g");
|
|
414
|
-
result = result.replace(pattern, placeholder);
|
|
415
|
-
} else {
|
|
416
|
-
result = result.replaceAll(value, placeholder);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
return frontmatter + result;
|
|
420
|
-
}
|
|
421
|
-
var TEMPLATE_VARIABLES;
|
|
422
|
-
var init_variables = __esm({
|
|
423
|
-
"src/lib/variables.ts"() {
|
|
424
|
-
"use strict";
|
|
425
|
-
TEMPLATE_VARIABLES = {
|
|
426
|
-
REPO_ID: (repo) => repo.id,
|
|
427
|
-
REPO_NAME: (repo) => repo.name,
|
|
428
|
-
REPO_PATH: (repo) => repo.path ?? "",
|
|
429
|
-
GIT_BRANCH: (repo) => repo.git_branch ?? "development",
|
|
430
|
-
SERVER_PORT: (repo) => repo.server_port != null ? String(repo.server_port) : "",
|
|
431
|
-
SERVER_TYPE: (repo) => repo.server_type ?? "none"
|
|
432
|
-
};
|
|
433
|
-
}
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
// src/lib/sync-engine.ts
|
|
437
|
-
var sync_engine_exports = {};
|
|
438
|
-
__export(sync_engine_exports, {
|
|
439
|
-
executeSyncToLocal: () => executeSyncToLocal
|
|
440
|
-
});
|
|
441
|
-
import {
|
|
442
|
-
readdir as readdir2,
|
|
443
|
-
readFile as readFile3,
|
|
444
|
-
writeFile as writeFile2,
|
|
445
|
-
unlink,
|
|
446
|
-
mkdir,
|
|
447
|
-
rmdir,
|
|
448
|
-
chmod,
|
|
449
|
-
stat
|
|
450
|
-
} from "node:fs/promises";
|
|
451
|
-
import { join as join3, dirname } from "node:path";
|
|
452
|
-
function getTypeDir(claudeDir, dir) {
|
|
453
|
-
if (dir === "commands") return join3(claudeDir, dir, "cbp");
|
|
454
|
-
return join3(claudeDir, dir);
|
|
455
|
-
}
|
|
456
|
-
function getFilePath(claudeDir, typeName, file) {
|
|
457
|
-
const cfg = typeConfig[typeName];
|
|
458
|
-
const typeDir = getTypeDir(claudeDir, cfg.dir);
|
|
459
|
-
if (cfg.subfolder) {
|
|
460
|
-
return join3(typeDir, file.name, `${cfg.subfolder}${cfg.ext}`);
|
|
461
|
-
}
|
|
462
|
-
if (typeName === "command" && file.category) {
|
|
463
|
-
return join3(typeDir, file.category, `${file.name}${cfg.ext}`);
|
|
464
|
-
}
|
|
465
|
-
if (typeName === "template") {
|
|
466
|
-
return join3(typeDir, file.name);
|
|
467
|
-
}
|
|
468
|
-
return join3(typeDir, `${file.name}${cfg.ext}`);
|
|
469
|
-
}
|
|
470
|
-
async function readDirRecursive(dir, base = dir) {
|
|
471
|
-
const result = /* @__PURE__ */ new Map();
|
|
249
|
+
async function writeLocalConfig(projectPath, config) {
|
|
250
|
+
const content = { device_id: config.device_id };
|
|
472
251
|
try {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
} catch {
|
|
252
|
+
await writeFile2(
|
|
253
|
+
localConfigPath(projectPath),
|
|
254
|
+
JSON.stringify(content, null, 2) + "\n",
|
|
255
|
+
"utf-8"
|
|
256
|
+
);
|
|
257
|
+
} catch (err) {
|
|
258
|
+
console.error(
|
|
259
|
+
`Failed to write local config: ${err instanceof Error ? err.message : String(err)}`
|
|
260
|
+
);
|
|
261
|
+
throw err;
|
|
486
262
|
}
|
|
487
|
-
return result;
|
|
488
263
|
}
|
|
489
|
-
async function
|
|
264
|
+
async function resolveMachineSeed() {
|
|
490
265
|
try {
|
|
491
|
-
const
|
|
492
|
-
const
|
|
493
|
-
|
|
266
|
+
const raw = await readFile2("/etc/machine-id", "utf-8");
|
|
267
|
+
const trimmed = raw.trim();
|
|
268
|
+
if (trimmed) return trimmed;
|
|
494
269
|
} catch {
|
|
495
|
-
return false;
|
|
496
270
|
}
|
|
497
|
-
|
|
498
|
-
async function removeEmptyParents(filePath, stopAt) {
|
|
499
|
-
let dir = dirname(filePath);
|
|
500
|
-
while (dir.length > stopAt.length && dir.startsWith(stopAt)) {
|
|
271
|
+
if (process.platform === "darwin") {
|
|
501
272
|
try {
|
|
502
|
-
|
|
503
|
-
|
|
273
|
+
const out = execSync("sysctl -n kern.uuid", { encoding: "utf-8" }).trim();
|
|
274
|
+
if (out) return out;
|
|
504
275
|
} catch {
|
|
505
|
-
break;
|
|
506
276
|
}
|
|
507
277
|
}
|
|
278
|
+
return hostname();
|
|
508
279
|
}
|
|
509
|
-
async function
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
apiGet(`/repos/${repoId}`)
|
|
514
|
-
]);
|
|
515
|
-
const syncData = syncRes.data;
|
|
516
|
-
const repoData = repoRes.data;
|
|
517
|
-
syncData.claude_md = [];
|
|
518
|
-
const claudeDir = join3(projectPath, ".claude");
|
|
519
|
-
const worktree = await isGitWorktree(projectPath);
|
|
520
|
-
const byType = {};
|
|
521
|
-
const totals = { created: 0, updated: 0, deleted: 0, unchanged: 0 };
|
|
522
|
-
const dbOnlyFiles = [];
|
|
523
|
-
for (const [syncKey, typeName] of Object.entries(syncKeyToType)) {
|
|
524
|
-
if (worktree && typeName === "command") {
|
|
525
|
-
byType["commands"] = {
|
|
526
|
-
created: [],
|
|
527
|
-
updated: [],
|
|
528
|
-
deleted: [],
|
|
529
|
-
unchanged: []
|
|
530
|
-
};
|
|
531
|
-
continue;
|
|
532
|
-
}
|
|
533
|
-
const cfg = typeConfig[typeName];
|
|
534
|
-
const targetDir = getTypeDir(claudeDir, cfg.dir);
|
|
535
|
-
const remoteFiles = syncData[syncKey] ?? [];
|
|
536
|
-
const result = {
|
|
537
|
-
created: [],
|
|
538
|
-
updated: [],
|
|
539
|
-
deleted: [],
|
|
540
|
-
unchanged: []
|
|
541
|
-
};
|
|
542
|
-
if (!dryRun) {
|
|
543
|
-
await mkdir(targetDir, { recursive: true });
|
|
544
|
-
}
|
|
545
|
-
const localFiles = await readDirRecursive(targetDir);
|
|
546
|
-
const remotePathMap = /* @__PURE__ */ new Map();
|
|
547
|
-
for (const remote of remoteFiles) {
|
|
548
|
-
const fullPath = getFilePath(claudeDir, typeName, remote);
|
|
549
|
-
const relPath = fullPath.slice(targetDir.length + 1);
|
|
550
|
-
const substituted = substituteVariables(remote.content, repoData);
|
|
551
|
-
remotePathMap.set(relPath, { content: substituted, name: remote.name });
|
|
552
|
-
}
|
|
553
|
-
for (const [relPath, { content, name }] of remotePathMap) {
|
|
554
|
-
const fullPath = join3(targetDir, relPath);
|
|
555
|
-
const localContent = localFiles.get(relPath);
|
|
556
|
-
if (localContent === void 0) {
|
|
557
|
-
const remoteFile = remoteFiles.find((f) => f.name === name);
|
|
558
|
-
dbOnlyFiles.push({
|
|
559
|
-
type: typeName,
|
|
560
|
-
name,
|
|
561
|
-
category: remoteFile?.category ?? null,
|
|
562
|
-
localPath: fullPath
|
|
563
|
-
});
|
|
564
|
-
if (!dryRun) {
|
|
565
|
-
await mkdir(dirname(fullPath), { recursive: true });
|
|
566
|
-
await writeFile2(fullPath, content, "utf-8");
|
|
567
|
-
if (typeName === "hook") await chmod(fullPath, 493);
|
|
568
|
-
}
|
|
569
|
-
result.created.push(name);
|
|
570
|
-
totals.created++;
|
|
571
|
-
} else if (localContent !== content) {
|
|
572
|
-
if (!dryRun) {
|
|
573
|
-
await writeFile2(fullPath, content, "utf-8");
|
|
574
|
-
if (typeName === "hook") await chmod(fullPath, 493);
|
|
575
|
-
}
|
|
576
|
-
result.updated.push(name);
|
|
577
|
-
totals.updated++;
|
|
578
|
-
} else {
|
|
579
|
-
result.unchanged.push(name);
|
|
580
|
-
totals.unchanged++;
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
for (const [relPath] of localFiles) {
|
|
584
|
-
if (!remotePathMap.has(relPath)) {
|
|
585
|
-
const fullPath = join3(targetDir, relPath);
|
|
586
|
-
if (!dryRun) {
|
|
587
|
-
await unlink(fullPath);
|
|
588
|
-
await removeEmptyParents(fullPath, targetDir);
|
|
589
|
-
}
|
|
590
|
-
const pathName = relPath.replace(/\.(md|sh)$/, "").replace(/\/(AGENT|SKILL)$/, "");
|
|
591
|
-
result.deleted.push(pathName);
|
|
592
|
-
totals.deleted++;
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
byType[`${typeName}s`] = result;
|
|
596
|
-
}
|
|
597
|
-
{
|
|
598
|
-
const typeName = "docs_stack";
|
|
599
|
-
const syncKey = "docs_stack";
|
|
600
|
-
const targetDir = join3(projectPath, "docs", "stack");
|
|
601
|
-
const remoteFiles = syncData[syncKey] ?? [];
|
|
602
|
-
const result = {
|
|
603
|
-
created: [],
|
|
604
|
-
updated: [],
|
|
605
|
-
deleted: [],
|
|
606
|
-
unchanged: []
|
|
607
|
-
};
|
|
608
|
-
if (remoteFiles.length > 0 && !dryRun) {
|
|
609
|
-
await mkdir(targetDir, { recursive: true });
|
|
610
|
-
}
|
|
611
|
-
const localFiles = await readDirRecursive(targetDir);
|
|
612
|
-
const remotePathMap = /* @__PURE__ */ new Map();
|
|
613
|
-
for (const remote of remoteFiles) {
|
|
614
|
-
const relPath = remote.category ? join3(remote.category, remote.name) : remote.name;
|
|
615
|
-
const substituted = substituteVariables(remote.content, repoData);
|
|
616
|
-
remotePathMap.set(relPath, {
|
|
617
|
-
content: substituted,
|
|
618
|
-
name: remote.category ? `${remote.category}/${remote.name}` : remote.name
|
|
619
|
-
});
|
|
620
|
-
}
|
|
621
|
-
for (const [relPath, { content, name }] of remotePathMap) {
|
|
622
|
-
const fullPath = join3(targetDir, relPath);
|
|
623
|
-
const localContent = localFiles.get(relPath);
|
|
624
|
-
if (localContent === void 0) {
|
|
625
|
-
if (!dryRun) {
|
|
626
|
-
await mkdir(dirname(fullPath), { recursive: true });
|
|
627
|
-
await writeFile2(fullPath, content, "utf-8");
|
|
628
|
-
}
|
|
629
|
-
result.created.push(name);
|
|
630
|
-
totals.created++;
|
|
631
|
-
} else if (localContent !== content) {
|
|
632
|
-
if (!dryRun) {
|
|
633
|
-
await writeFile2(fullPath, content, "utf-8");
|
|
634
|
-
}
|
|
635
|
-
result.updated.push(name);
|
|
636
|
-
totals.updated++;
|
|
637
|
-
} else {
|
|
638
|
-
result.unchanged.push(name);
|
|
639
|
-
totals.unchanged++;
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
for (const [relPath] of localFiles) {
|
|
643
|
-
if (!remotePathMap.has(relPath)) {
|
|
644
|
-
const fullPath = join3(targetDir, relPath);
|
|
645
|
-
if (!dryRun) {
|
|
646
|
-
await unlink(fullPath);
|
|
647
|
-
await removeEmptyParents(fullPath, targetDir);
|
|
648
|
-
}
|
|
649
|
-
result.deleted.push(relPath);
|
|
650
|
-
totals.deleted++;
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
byType[typeName] = result;
|
|
654
|
-
}
|
|
655
|
-
const globalSettingsFiles = syncData.global_settings ?? [];
|
|
656
|
-
let globalSettings = {};
|
|
657
|
-
for (const gf of globalSettingsFiles) {
|
|
658
|
-
const parsed = JSON.parse(
|
|
659
|
-
substituteVariables(gf.content, repoData)
|
|
660
|
-
);
|
|
661
|
-
globalSettings = { ...globalSettings, ...parsed };
|
|
662
|
-
}
|
|
663
|
-
const specialTypes = {
|
|
664
|
-
claude_md: () => join3(projectPath, "CLAUDE.md"),
|
|
665
|
-
settings: () => join3(projectPath, ".claude", "settings.json")
|
|
666
|
-
};
|
|
667
|
-
for (const [typeName, getPath] of Object.entries(specialTypes)) {
|
|
668
|
-
const remoteFiles = syncData[typeName] ?? [];
|
|
669
|
-
const result = {
|
|
670
|
-
created: [],
|
|
671
|
-
updated: [],
|
|
672
|
-
deleted: [],
|
|
673
|
-
unchanged: []
|
|
674
|
-
};
|
|
675
|
-
for (const remote of remoteFiles) {
|
|
676
|
-
const targetPath = getPath(remote.name);
|
|
677
|
-
const remoteContent = substituteVariables(remote.content, repoData);
|
|
678
|
-
let localContent;
|
|
679
|
-
try {
|
|
680
|
-
localContent = await readFile3(targetPath, "utf-8");
|
|
681
|
-
} catch {
|
|
682
|
-
}
|
|
683
|
-
if (typeName === "settings") {
|
|
684
|
-
const repoSettings = JSON.parse(remoteContent);
|
|
685
|
-
const combinedTemplate = mergeGlobalAndRepoSettings(
|
|
686
|
-
globalSettings,
|
|
687
|
-
repoSettings
|
|
688
|
-
);
|
|
689
|
-
const hooksDir = join3(projectPath, ".claude", "hooks");
|
|
690
|
-
const discovered = await discoverHooks(hooksDir);
|
|
691
|
-
if (localContent === void 0) {
|
|
692
|
-
const finalSettings = stripPermissionsAllow(combinedTemplate);
|
|
693
|
-
if (discovered.size > 0) {
|
|
694
|
-
finalSettings.hooks = mergeDiscoveredHooks(
|
|
695
|
-
finalSettings.hooks ?? {},
|
|
696
|
-
discovered
|
|
697
|
-
);
|
|
698
|
-
}
|
|
699
|
-
if (!dryRun) {
|
|
700
|
-
await mkdir(dirname(targetPath), { recursive: true });
|
|
701
|
-
await writeFile2(
|
|
702
|
-
targetPath,
|
|
703
|
-
JSON.stringify(finalSettings, null, 2) + "\n",
|
|
704
|
-
"utf-8"
|
|
705
|
-
);
|
|
706
|
-
}
|
|
707
|
-
result.created.push(remote.name);
|
|
708
|
-
totals.created++;
|
|
709
|
-
} else {
|
|
710
|
-
const localSettings = JSON.parse(localContent);
|
|
711
|
-
let merged = mergeSettings(combinedTemplate, localSettings);
|
|
712
|
-
merged = stripPermissionsAllow(merged);
|
|
713
|
-
if (discovered.size > 0) {
|
|
714
|
-
merged.hooks = mergeDiscoveredHooks(
|
|
715
|
-
merged.hooks ?? {},
|
|
716
|
-
discovered
|
|
717
|
-
);
|
|
718
|
-
}
|
|
719
|
-
const mergedContent = JSON.stringify(merged, null, 2) + "\n";
|
|
720
|
-
if (localContent !== mergedContent) {
|
|
721
|
-
if (!dryRun) {
|
|
722
|
-
await writeFile2(targetPath, mergedContent, "utf-8");
|
|
723
|
-
}
|
|
724
|
-
result.updated.push(remote.name);
|
|
725
|
-
totals.updated++;
|
|
726
|
-
} else {
|
|
727
|
-
result.unchanged.push(remote.name);
|
|
728
|
-
totals.unchanged++;
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
} else {
|
|
732
|
-
if (localContent === void 0) {
|
|
733
|
-
if (!dryRun) {
|
|
734
|
-
await mkdir(dirname(targetPath), { recursive: true });
|
|
735
|
-
await writeFile2(targetPath, remoteContent, "utf-8");
|
|
736
|
-
}
|
|
737
|
-
result.created.push(remote.name);
|
|
738
|
-
totals.created++;
|
|
739
|
-
} else if (localContent !== remoteContent) {
|
|
740
|
-
if (!dryRun) {
|
|
741
|
-
await writeFile2(targetPath, remoteContent, "utf-8");
|
|
742
|
-
}
|
|
743
|
-
result.updated.push(remote.name);
|
|
744
|
-
totals.updated++;
|
|
745
|
-
} else {
|
|
746
|
-
result.unchanged.push(remote.name);
|
|
747
|
-
totals.unchanged++;
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
byType[typeName] = result;
|
|
752
|
-
}
|
|
753
|
-
if (!dryRun) {
|
|
754
|
-
await apiPost("/sync/state", {
|
|
755
|
-
repo_id: repoId,
|
|
756
|
-
last_synced_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
757
|
-
was_skipped: false,
|
|
758
|
-
files_synced_count: totals.created + totals.updated + totals.deleted + totals.unchanged,
|
|
759
|
-
files_pushed: 0,
|
|
760
|
-
files_pulled: totals.created + totals.updated,
|
|
761
|
-
files_deleted: totals.deleted,
|
|
762
|
-
files_skipped: 0
|
|
763
|
-
});
|
|
764
|
-
const fileRepoUpdates = [];
|
|
765
|
-
const syncTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
766
|
-
for (const [syncKey, typeName] of Object.entries(syncKeyToType)) {
|
|
767
|
-
const remoteFiles = syncData[syncKey] ?? [];
|
|
768
|
-
for (const file of remoteFiles) {
|
|
769
|
-
fileRepoUpdates.push({
|
|
770
|
-
claude_file_id: file.id ?? void 0,
|
|
771
|
-
file_type: typeName,
|
|
772
|
-
file_name: file.name,
|
|
773
|
-
file_category: file.category ?? null,
|
|
774
|
-
file_scope: file.scope ?? "shared",
|
|
775
|
-
last_synced_at: syncTimestamp,
|
|
776
|
-
sync_status: "synced"
|
|
777
|
-
});
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
for (const typeName of ["claude_md", "settings"]) {
|
|
781
|
-
const remoteFiles = syncData[typeName] ?? [];
|
|
782
|
-
for (const file of remoteFiles) {
|
|
783
|
-
fileRepoUpdates.push({
|
|
784
|
-
claude_file_id: file.id ?? void 0,
|
|
785
|
-
file_type: typeName,
|
|
786
|
-
file_name: file.name,
|
|
787
|
-
file_category: file.category ?? null,
|
|
788
|
-
file_scope: file.scope ?? `local:${repoId}`,
|
|
789
|
-
last_synced_at: syncTimestamp,
|
|
790
|
-
sync_status: "synced"
|
|
791
|
-
});
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
if (fileRepoUpdates.length > 0) {
|
|
795
|
-
try {
|
|
796
|
-
await apiPost("/sync/file-repos", {
|
|
797
|
-
repo_id: repoId,
|
|
798
|
-
file_repos: fileRepoUpdates
|
|
799
|
-
});
|
|
800
|
-
} catch {
|
|
801
|
-
}
|
|
802
|
-
}
|
|
280
|
+
async function getOrCreateDeviceId(projectPath) {
|
|
281
|
+
const existing = await readLocalConfig(projectPath);
|
|
282
|
+
if (existing?.device_id) {
|
|
283
|
+
return existing.device_id;
|
|
803
284
|
}
|
|
804
|
-
|
|
285
|
+
const seed = await resolveMachineSeed();
|
|
286
|
+
const deviceId = createHash("sha256").update(seed).digest("hex").slice(0, 16);
|
|
287
|
+
await writeLocalConfig(projectPath, { device_id: deviceId });
|
|
288
|
+
return deviceId;
|
|
805
289
|
}
|
|
806
|
-
var
|
|
807
|
-
|
|
808
|
-
"src/lib/sync-engine.ts"() {
|
|
290
|
+
var init_local_config = __esm({
|
|
291
|
+
"src/lib/local-config.ts"() {
|
|
809
292
|
"use strict";
|
|
810
|
-
init_api();
|
|
811
|
-
init_settings_merge();
|
|
812
|
-
init_hook_registry();
|
|
813
|
-
init_variables();
|
|
814
|
-
typeConfig = {
|
|
815
|
-
command: { dir: "commands", ext: ".md" },
|
|
816
|
-
agent: { dir: "agents", ext: ".md", subfolder: "AGENT" },
|
|
817
|
-
skill: { dir: "skills", ext: ".md", subfolder: "SKILL" },
|
|
818
|
-
rule: { dir: "rules", ext: ".md" },
|
|
819
|
-
hook: { dir: "hooks", ext: ".sh" },
|
|
820
|
-
template: { dir: "templates", ext: "" },
|
|
821
|
-
context: { dir: "context", ext: ".md" }
|
|
822
|
-
};
|
|
823
|
-
syncKeyToType = {
|
|
824
|
-
commands: "command",
|
|
825
|
-
agents: "agent",
|
|
826
|
-
skills: "skill",
|
|
827
|
-
rules: "rule",
|
|
828
|
-
hooks: "hook",
|
|
829
|
-
templates: "template",
|
|
830
|
-
contexts: "context"
|
|
831
|
-
};
|
|
832
293
|
}
|
|
833
294
|
});
|
|
834
295
|
|
|
@@ -839,15 +300,15 @@ __export(setup_exports, {
|
|
|
839
300
|
});
|
|
840
301
|
import { createInterface } from "node:readline/promises";
|
|
841
302
|
import { stdin, stdout } from "node:process";
|
|
842
|
-
import { readFile as
|
|
303
|
+
import { readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
843
304
|
import { homedir } from "node:os";
|
|
844
|
-
import { join as
|
|
305
|
+
import { join as join3 } from "node:path";
|
|
845
306
|
function getConfigPath(scope) {
|
|
846
|
-
return scope === "user" ?
|
|
307
|
+
return scope === "user" ? join3(homedir(), ".claude.json") : join3(process.cwd(), ".mcp.json");
|
|
847
308
|
}
|
|
848
309
|
async function readConfig(path) {
|
|
849
310
|
try {
|
|
850
|
-
const raw = await
|
|
311
|
+
const raw = await readFile3(path, "utf-8");
|
|
851
312
|
const parsed = JSON.parse(raw);
|
|
852
313
|
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
853
314
|
return parsed;
|
|
@@ -976,12 +437,29 @@ async function runSetup() {
|
|
|
976
437
|
Selected: ${selectedRepo.name}
|
|
977
438
|
`);
|
|
978
439
|
const projectPath = process.cwd();
|
|
979
|
-
const
|
|
440
|
+
const pathBasedId = await resolveAndCacheWorktreeId(
|
|
980
441
|
selectedRepo.id,
|
|
981
442
|
projectPath,
|
|
982
443
|
{ skipWrite: true }
|
|
983
444
|
);
|
|
984
|
-
const
|
|
445
|
+
const deviceId = await getOrCreateDeviceId(projectPath);
|
|
446
|
+
let branch = "main";
|
|
447
|
+
try {
|
|
448
|
+
const { execSync: execSync3 } = await import("node:child_process");
|
|
449
|
+
branch = execSync3("git symbolic-ref --short HEAD", {
|
|
450
|
+
cwd: projectPath,
|
|
451
|
+
encoding: "utf-8"
|
|
452
|
+
}).trim();
|
|
453
|
+
} catch {
|
|
454
|
+
}
|
|
455
|
+
const tupleId = await resolveWorktreeId({
|
|
456
|
+
repoId: selectedRepo.id,
|
|
457
|
+
repoPath: projectPath,
|
|
458
|
+
branch,
|
|
459
|
+
deviceId
|
|
460
|
+
});
|
|
461
|
+
const worktreeId = tupleId ?? pathBasedId;
|
|
462
|
+
const codebyplanPath = join3(projectPath, ".codebyplan.json");
|
|
985
463
|
const codebyplanConfig = {
|
|
986
464
|
repo_id: selectedRepo.id
|
|
987
465
|
};
|
|
@@ -997,27 +475,6 @@ async function runSetup() {
|
|
|
997
475
|
` Worktree id set (${worktreeId}) \u2014 this worktree is now identified for hard-lock enforcement.`
|
|
998
476
|
);
|
|
999
477
|
}
|
|
1000
|
-
console.log("\n Running initial sync...\n");
|
|
1001
|
-
try {
|
|
1002
|
-
const { executeSyncToLocal: executeSyncToLocal2 } = await Promise.resolve().then(() => (init_sync_engine(), sync_engine_exports));
|
|
1003
|
-
const syncResult = await executeSyncToLocal2({
|
|
1004
|
-
repoId: selectedRepo.id,
|
|
1005
|
-
projectPath
|
|
1006
|
-
});
|
|
1007
|
-
const totalChanges = syncResult.totals.created + syncResult.totals.updated + syncResult.totals.deleted;
|
|
1008
|
-
if (totalChanges > 0) {
|
|
1009
|
-
console.log(
|
|
1010
|
-
` Synced: ${syncResult.totals.created} created, ${syncResult.totals.updated} updated, ${syncResult.totals.deleted} deleted
|
|
1011
|
-
`
|
|
1012
|
-
);
|
|
1013
|
-
} else {
|
|
1014
|
-
console.log(" All files already up to date.\n");
|
|
1015
|
-
}
|
|
1016
|
-
} catch (err) {
|
|
1017
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1018
|
-
console.log(` Sync failed: ${msg}`);
|
|
1019
|
-
console.log(" Run 'codebyplan sync' later to sync files.\n");
|
|
1020
|
-
}
|
|
1021
478
|
}
|
|
1022
479
|
}
|
|
1023
480
|
}
|
|
@@ -1032,18 +489,19 @@ var init_setup = __esm({
|
|
|
1032
489
|
"src/cli/setup.ts"() {
|
|
1033
490
|
"use strict";
|
|
1034
491
|
init_resolve_worktree();
|
|
492
|
+
init_local_config();
|
|
1035
493
|
}
|
|
1036
494
|
});
|
|
1037
495
|
|
|
1038
|
-
// src/cli/
|
|
1039
|
-
import { readFile as
|
|
1040
|
-
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";
|
|
1041
499
|
async function findCodebyplanConfig(startDir, maxDepth = 20) {
|
|
1042
500
|
let cursor = resolve(startDir);
|
|
1043
501
|
for (let depth = 0; depth < maxDepth; depth++) {
|
|
1044
|
-
const configPath =
|
|
502
|
+
const configPath = join4(cursor, ".codebyplan.json");
|
|
1045
503
|
try {
|
|
1046
|
-
const raw = await
|
|
504
|
+
const raw = await readFile4(configPath, "utf-8");
|
|
1047
505
|
const parsed = JSON.parse(raw);
|
|
1048
506
|
return { path: configPath, contents: parsed };
|
|
1049
507
|
} catch {
|
|
@@ -1087,590 +545,72 @@ async function resolveConfig(flags) {
|
|
|
1087
545
|
}
|
|
1088
546
|
return { repoId, worktreeId, projectPath };
|
|
1089
547
|
}
|
|
1090
|
-
var
|
|
1091
|
-
"src/cli/
|
|
548
|
+
var init_flags = __esm({
|
|
549
|
+
"src/cli/flags.ts"() {
|
|
1092
550
|
"use strict";
|
|
1093
551
|
}
|
|
1094
552
|
});
|
|
1095
553
|
|
|
1096
|
-
// src/cli/
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
return "shared";
|
|
1107
|
-
}
|
|
1108
|
-
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
1109
|
-
if (fmMatch) {
|
|
1110
|
-
const scopeLine = fmMatch[1].match(/^scope:\s*(\S+)/m);
|
|
1111
|
-
if (scopeLine) {
|
|
1112
|
-
const raw = scopeLine[1];
|
|
1113
|
-
return raw === "shared" ? "shared" : `local:${raw}`;
|
|
1114
|
-
}
|
|
1115
|
-
if (/^scope\b/m.test(fmMatch[1])) {
|
|
1116
|
-
console.error(
|
|
1117
|
-
` Warning: frontmatter contains "scope" but could not parse it. Expected format: "scope: shared" or "scope: <repo-name>". Defaulting to "shared".`
|
|
1118
|
-
);
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
return "shared";
|
|
1122
|
-
}
|
|
1123
|
-
function compositeKey(type, name, category) {
|
|
1124
|
-
return category ? `${type}:${category}/${name}` : `${type}:${name}`;
|
|
1125
|
-
}
|
|
1126
|
-
async function scanLocalFiles(claudeDir, projectPath) {
|
|
1127
|
-
const result = /* @__PURE__ */ new Map();
|
|
1128
|
-
await scanCommands(join6(claudeDir, "commands", "cbp"), result);
|
|
1129
|
-
await scanSubfolderType(
|
|
1130
|
-
join6(claudeDir, "agents"),
|
|
1131
|
-
"agent",
|
|
1132
|
-
"AGENT.md",
|
|
1133
|
-
result
|
|
1134
|
-
);
|
|
1135
|
-
await scanSubfolderType(
|
|
1136
|
-
join6(claudeDir, "skills"),
|
|
1137
|
-
"skill",
|
|
1138
|
-
"SKILL.md",
|
|
1139
|
-
result
|
|
1140
|
-
);
|
|
1141
|
-
await scanFlatType(join6(claudeDir, "rules"), "rule", ".md", result);
|
|
1142
|
-
await scanFlatType(join6(claudeDir, "hooks"), "hook", ".sh", result);
|
|
1143
|
-
await scanTemplates(join6(claudeDir, "templates"), result);
|
|
1144
|
-
await scanCategorizedType(
|
|
1145
|
-
join6(claudeDir, "context"),
|
|
1146
|
-
"context",
|
|
1147
|
-
".md",
|
|
1148
|
-
result
|
|
1149
|
-
);
|
|
1150
|
-
await scanDocsRecursive(join6(claudeDir, "docs"), result);
|
|
1151
|
-
await scanSettings(claudeDir, projectPath, result);
|
|
1152
|
-
return result;
|
|
1153
|
-
}
|
|
1154
|
-
async function scanCommands(dir, result) {
|
|
1155
|
-
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";
|
|
1156
564
|
}
|
|
1157
|
-
async function
|
|
1158
|
-
|
|
565
|
+
async function confirmProceed(message) {
|
|
566
|
+
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
1159
567
|
try {
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
baseDir,
|
|
1168
|
-
join6(currentDir, entry.name),
|
|
1169
|
-
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.`
|
|
1170
575
|
);
|
|
1171
|
-
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1172
|
-
const name = entry.name.slice(0, -3);
|
|
1173
|
-
const content = await readFile6(join6(currentDir, entry.name), "utf-8");
|
|
1174
|
-
const relDir = currentDir.slice(baseDir.length + 1);
|
|
1175
|
-
const category = relDir || null;
|
|
1176
|
-
const scope = extractScope(content, "command");
|
|
1177
|
-
const key = compositeKey("command", name, category);
|
|
1178
|
-
result.set(key, { type: "command", name, category, content, scope });
|
|
1179
576
|
}
|
|
577
|
+
} catch (err) {
|
|
578
|
+
if (isAbortError(err)) throw new SyncCancelledError();
|
|
579
|
+
throw err;
|
|
580
|
+
} finally {
|
|
581
|
+
rl.close();
|
|
1180
582
|
}
|
|
1181
583
|
}
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
if (entry.isDirectory()) {
|
|
1191
|
-
const filePath = join6(dir, entry.name, fileName);
|
|
1192
|
-
try {
|
|
1193
|
-
const content = await readFile6(filePath, "utf-8");
|
|
1194
|
-
const scope = extractScope(content, type);
|
|
1195
|
-
const key = compositeKey(type, entry.name, null);
|
|
1196
|
-
result.set(key, {
|
|
1197
|
-
type,
|
|
1198
|
-
name: entry.name,
|
|
1199
|
-
category: null,
|
|
1200
|
-
content,
|
|
1201
|
-
scope
|
|
1202
|
-
});
|
|
1203
|
-
} 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";
|
|
1204
592
|
}
|
|
1205
|
-
}
|
|
593
|
+
};
|
|
1206
594
|
}
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
|
|
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) {
|
|
1210
601
|
try {
|
|
1211
|
-
|
|
602
|
+
await access(filePath);
|
|
603
|
+
return true;
|
|
1212
604
|
} catch {
|
|
1213
|
-
return;
|
|
1214
|
-
}
|
|
1215
|
-
for (const entry of entries) {
|
|
1216
|
-
if (entry.isFile() && entry.name.endsWith(ext)) {
|
|
1217
|
-
const name = entry.name.slice(0, -ext.length);
|
|
1218
|
-
const content = await readFile6(join6(dir, entry.name), "utf-8");
|
|
1219
|
-
const scope = extractScope(content, type);
|
|
1220
|
-
const key = compositeKey(type, name, null);
|
|
1221
|
-
result.set(key, { type, name, category: null, content, scope });
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
async function scanCategorizedType(dir, type, ext, result) {
|
|
1226
|
-
let entries;
|
|
1227
|
-
try {
|
|
1228
|
-
entries = await readdir3(dir, { withFileTypes: true });
|
|
1229
|
-
} catch {
|
|
1230
|
-
return;
|
|
1231
|
-
}
|
|
1232
|
-
for (const entry of entries) {
|
|
1233
|
-
if (entry.isDirectory()) {
|
|
1234
|
-
const category = entry.name;
|
|
1235
|
-
let subEntries;
|
|
1236
|
-
try {
|
|
1237
|
-
subEntries = await readdir3(join6(dir, category), {
|
|
1238
|
-
withFileTypes: true
|
|
1239
|
-
});
|
|
1240
|
-
} catch {
|
|
1241
|
-
continue;
|
|
1242
|
-
}
|
|
1243
|
-
for (const sub of subEntries) {
|
|
1244
|
-
if (sub.isFile() && sub.name.endsWith(ext)) {
|
|
1245
|
-
const name = sub.name.slice(0, -ext.length);
|
|
1246
|
-
const content = await readFile6(
|
|
1247
|
-
join6(dir, category, sub.name),
|
|
1248
|
-
"utf-8"
|
|
1249
|
-
);
|
|
1250
|
-
const scope = extractScope(content, type);
|
|
1251
|
-
const key = compositeKey(type, name, category);
|
|
1252
|
-
result.set(key, { type, name, category, content, scope });
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
} else if (entry.isFile() && entry.name.endsWith(ext)) {
|
|
1256
|
-
const name = entry.name.slice(0, -ext.length);
|
|
1257
|
-
const content = await readFile6(join6(dir, entry.name), "utf-8");
|
|
1258
|
-
const scope = extractScope(content, type);
|
|
1259
|
-
const key = compositeKey(type, name, null);
|
|
1260
|
-
result.set(key, { type, name, category: null, content, scope });
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
async function scanDocsRecursive(docsDir, result) {
|
|
1265
|
-
await scanDocsDir(docsDir, docsDir, result);
|
|
1266
|
-
}
|
|
1267
|
-
async function scanDocsDir(baseDir, currentDir, result) {
|
|
1268
|
-
let entries;
|
|
1269
|
-
try {
|
|
1270
|
-
entries = await readdir3(currentDir, { withFileTypes: true });
|
|
1271
|
-
} catch {
|
|
1272
|
-
return;
|
|
1273
|
-
}
|
|
1274
|
-
for (const entry of entries) {
|
|
1275
|
-
if (entry.isDirectory()) {
|
|
1276
|
-
await scanDocsDir(baseDir, join6(currentDir, entry.name), result);
|
|
1277
|
-
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1278
|
-
const name = entry.name.slice(0, -3);
|
|
1279
|
-
const content = await readFile6(join6(currentDir, entry.name), "utf-8");
|
|
1280
|
-
const scope = extractScope(content, "docs");
|
|
1281
|
-
const relDir = currentDir.slice(baseDir.length + 1);
|
|
1282
|
-
const category = relDir || null;
|
|
1283
|
-
const key = compositeKey("docs", name, category);
|
|
1284
|
-
result.set(key, { type: "docs", name, category, content, scope });
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
async function scanTemplates(dir, result) {
|
|
1289
|
-
let entries;
|
|
1290
|
-
try {
|
|
1291
|
-
entries = await readdir3(dir, { withFileTypes: true });
|
|
1292
|
-
} catch {
|
|
1293
|
-
return;
|
|
1294
|
-
}
|
|
1295
|
-
for (const entry of entries) {
|
|
1296
|
-
if (entry.isFile() && extname(entry.name)) {
|
|
1297
|
-
const content = await readFile6(join6(dir, entry.name), "utf-8");
|
|
1298
|
-
const scope = extractScope(content, "template");
|
|
1299
|
-
const key = compositeKey("template", entry.name, null);
|
|
1300
|
-
result.set(key, {
|
|
1301
|
-
type: "template",
|
|
1302
|
-
name: entry.name,
|
|
1303
|
-
category: null,
|
|
1304
|
-
content,
|
|
1305
|
-
scope
|
|
1306
|
-
});
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1310
|
-
async function scanSettings(claudeDir, projectPath, result) {
|
|
1311
|
-
const settingsPath = join6(claudeDir, "settings.json");
|
|
1312
|
-
let raw;
|
|
1313
|
-
try {
|
|
1314
|
-
raw = await readFile6(settingsPath, "utf-8");
|
|
1315
|
-
} catch {
|
|
1316
|
-
return;
|
|
1317
|
-
}
|
|
1318
|
-
let parsed;
|
|
1319
|
-
try {
|
|
1320
|
-
parsed = JSON.parse(raw);
|
|
1321
|
-
} catch {
|
|
1322
|
-
return;
|
|
1323
|
-
}
|
|
1324
|
-
parsed = stripPermissionsAllow(parsed);
|
|
1325
|
-
if (parsed.hooks && typeof parsed.hooks === "object") {
|
|
1326
|
-
const hooksDir = projectPath ? join6(projectPath, ".claude", "hooks") : join6(claudeDir, "hooks");
|
|
1327
|
-
const discovered = await discoverHooks(hooksDir);
|
|
1328
|
-
if (discovered.size > 0) {
|
|
1329
|
-
parsed.hooks = stripDiscoveredHooks(
|
|
1330
|
-
parsed.hooks,
|
|
1331
|
-
".claude/hooks"
|
|
1332
|
-
);
|
|
1333
|
-
if (Object.keys(parsed.hooks).length === 0) {
|
|
1334
|
-
delete parsed.hooks;
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
const content = JSON.stringify(parsed, null, 2) + "\n";
|
|
1339
|
-
const key = compositeKey("settings", "settings", null);
|
|
1340
|
-
result.set(key, {
|
|
1341
|
-
type: "settings",
|
|
1342
|
-
name: "settings",
|
|
1343
|
-
category: null,
|
|
1344
|
-
content,
|
|
1345
|
-
scope: "shared"
|
|
1346
|
-
});
|
|
1347
|
-
}
|
|
1348
|
-
var init_fileMapper = __esm({
|
|
1349
|
-
"src/cli/fileMapper.ts"() {
|
|
1350
|
-
"use strict";
|
|
1351
|
-
init_settings_merge();
|
|
1352
|
-
init_hook_registry();
|
|
1353
|
-
}
|
|
1354
|
-
});
|
|
1355
|
-
|
|
1356
|
-
// src/cli/confirm.ts
|
|
1357
|
-
var confirm_exports = {};
|
|
1358
|
-
__export(confirm_exports, {
|
|
1359
|
-
SyncCancelledError: () => SyncCancelledError,
|
|
1360
|
-
confirmEach: () => confirmEach,
|
|
1361
|
-
confirmProceed: () => confirmProceed,
|
|
1362
|
-
promptChoice: () => promptChoice,
|
|
1363
|
-
promptReviewMode: () => promptReviewMode,
|
|
1364
|
-
reviewFilesOneByOne: () => reviewFilesOneByOne,
|
|
1365
|
-
reviewFolder: () => reviewFolder
|
|
1366
|
-
});
|
|
1367
|
-
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
1368
|
-
import { stdin as stdin2, stdout as stdout2 } from "node:process";
|
|
1369
|
-
function isAbortError(err) {
|
|
1370
|
-
return err instanceof Error && err.code === "ABORT_ERR";
|
|
1371
|
-
}
|
|
1372
|
-
async function confirmProceed(message) {
|
|
1373
|
-
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
1374
|
-
try {
|
|
1375
|
-
while (true) {
|
|
1376
|
-
const answer = await rl.question(message ?? " Proceed? [Y/n] ");
|
|
1377
|
-
const a = answer.trim().toLowerCase();
|
|
1378
|
-
if (a === "" || a === "y" || a === "yes") return true;
|
|
1379
|
-
if (a === "n" || a === "no") return false;
|
|
1380
|
-
console.log(
|
|
1381
|
-
` Unknown option "${answer.trim()}". Valid: y/yes, n/no, or Enter for yes.`
|
|
1382
|
-
);
|
|
1383
|
-
}
|
|
1384
|
-
} catch (err) {
|
|
1385
|
-
if (isAbortError(err)) throw new SyncCancelledError();
|
|
1386
|
-
throw err;
|
|
1387
|
-
} finally {
|
|
1388
|
-
rl.close();
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
async function promptChoice(message, options) {
|
|
1392
|
-
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
1393
|
-
try {
|
|
1394
|
-
const answer = await rl.question(message);
|
|
1395
|
-
const a = answer.trim().toLowerCase();
|
|
1396
|
-
return options.includes(a) ? a : options[0];
|
|
1397
|
-
} catch (err) {
|
|
1398
|
-
if (isAbortError(err)) throw new SyncCancelledError();
|
|
1399
|
-
throw err;
|
|
1400
|
-
} finally {
|
|
1401
|
-
rl.close();
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
async function confirmEach(items, label) {
|
|
1405
|
-
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
1406
|
-
const accepted = [];
|
|
1407
|
-
try {
|
|
1408
|
-
for (const item of items) {
|
|
1409
|
-
const answer = await rl.question(` ${label(item)} \u2014 delete? [y/n/a] `);
|
|
1410
|
-
const a = answer.trim().toLowerCase();
|
|
1411
|
-
if (a === "a") {
|
|
1412
|
-
accepted.push(item, ...items.slice(items.indexOf(item) + 1));
|
|
1413
|
-
break;
|
|
1414
|
-
}
|
|
1415
|
-
if (a === "y" || a === "yes" || a === "") {
|
|
1416
|
-
accepted.push(item);
|
|
1417
|
-
}
|
|
1418
|
-
}
|
|
1419
|
-
} catch (err) {
|
|
1420
|
-
if (isAbortError(err)) throw new SyncCancelledError();
|
|
1421
|
-
throw err;
|
|
1422
|
-
} finally {
|
|
1423
|
-
rl.close();
|
|
1424
|
-
}
|
|
1425
|
-
return accepted;
|
|
1426
|
-
}
|
|
1427
|
-
function parseReviewAction(input) {
|
|
1428
|
-
const a = input.trim().toLowerCase();
|
|
1429
|
-
switch (a) {
|
|
1430
|
-
case "d":
|
|
1431
|
-
case "delete":
|
|
1432
|
-
return { action: "delete", all: false, special: null };
|
|
1433
|
-
case "p":
|
|
1434
|
-
case "pull":
|
|
1435
|
-
return { action: "pull", all: false, special: null };
|
|
1436
|
-
case "s":
|
|
1437
|
-
case "push":
|
|
1438
|
-
return { action: "push", all: false, special: null };
|
|
1439
|
-
case "k":
|
|
1440
|
-
case "skip":
|
|
1441
|
-
return { action: "skip", all: false, special: null };
|
|
1442
|
-
case "da":
|
|
1443
|
-
return { action: "delete", all: true, special: null };
|
|
1444
|
-
case "pa":
|
|
1445
|
-
return { action: "pull", all: true, special: null };
|
|
1446
|
-
case "sa":
|
|
1447
|
-
return { action: "push", all: true, special: null };
|
|
1448
|
-
case "ka":
|
|
1449
|
-
return { action: "skip", all: true, special: null };
|
|
1450
|
-
case "v":
|
|
1451
|
-
case "view":
|
|
1452
|
-
return { action: null, all: false, special: "view" };
|
|
1453
|
-
case "r":
|
|
1454
|
-
case "recommended":
|
|
1455
|
-
return { action: null, all: false, special: "recommended" };
|
|
1456
|
-
case "":
|
|
1457
|
-
return { action: null, all: false, special: "recommended" };
|
|
1458
|
-
// Enter = recommended
|
|
1459
|
-
default:
|
|
1460
|
-
return { action: null, all: false, special: null };
|
|
1461
|
-
}
|
|
1462
|
-
}
|
|
1463
|
-
function formatActionPrompt(recommended, includeView, includeRecommended) {
|
|
1464
|
-
const actions = [
|
|
1465
|
-
`[d]elete${recommended === "delete" ? "\u2605" : ""}`,
|
|
1466
|
-
`[p]ull${recommended === "pull" ? "\u2605" : ""}`,
|
|
1467
|
-
`pu[s]h${recommended === "push" ? "\u2605" : ""}`,
|
|
1468
|
-
`s[k]ip${recommended === "skip" ? "\u2605" : ""}`
|
|
1469
|
-
];
|
|
1470
|
-
if (includeView) actions.push("[v]iew");
|
|
1471
|
-
if (includeRecommended) actions.push("[r]ecommended");
|
|
1472
|
-
return actions.join(" ");
|
|
1473
|
-
}
|
|
1474
|
-
function showDiff(local, remote, displayPath) {
|
|
1475
|
-
console.log(`
|
|
1476
|
-
--- ${displayPath} (diff) ---`);
|
|
1477
|
-
if (local === null && remote !== null) {
|
|
1478
|
-
console.log(" (no local file \u2014 remote content below)");
|
|
1479
|
-
for (const line of remote.split("\n").slice(0, 30)) {
|
|
1480
|
-
console.log(` + ${line}`);
|
|
1481
|
-
}
|
|
1482
|
-
if (remote.split("\n").length > 30) console.log(" ... (truncated)");
|
|
1483
|
-
} else if (local !== null && remote === null) {
|
|
1484
|
-
console.log(" (no remote file \u2014 local content below)");
|
|
1485
|
-
for (const line of local.split("\n").slice(0, 30)) {
|
|
1486
|
-
console.log(` - ${line}`);
|
|
1487
|
-
}
|
|
1488
|
-
if (local.split("\n").length > 30) console.log(" ... (truncated)");
|
|
1489
|
-
} else if (local !== null && remote !== null) {
|
|
1490
|
-
const localLines = local.split("\n");
|
|
1491
|
-
const remoteLines = remote.split("\n");
|
|
1492
|
-
let shown = 0;
|
|
1493
|
-
const maxLines = 40;
|
|
1494
|
-
for (let i = 0; i < Math.max(localLines.length, remoteLines.length) && shown < maxLines; i++) {
|
|
1495
|
-
const l = localLines[i];
|
|
1496
|
-
const r = remoteLines[i];
|
|
1497
|
-
if (l === r) {
|
|
1498
|
-
console.log(` ${l ?? ""}`);
|
|
1499
|
-
} else {
|
|
1500
|
-
if (l !== void 0) console.log(` - ${l}`);
|
|
1501
|
-
if (r !== void 0) console.log(` + ${r}`);
|
|
1502
|
-
}
|
|
1503
|
-
shown++;
|
|
1504
|
-
}
|
|
1505
|
-
if (Math.max(localLines.length, remoteLines.length) > maxLines) {
|
|
1506
|
-
console.log(" ... (truncated)");
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
console.log();
|
|
1510
|
-
}
|
|
1511
|
-
async function promptReviewMode() {
|
|
1512
|
-
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
1513
|
-
try {
|
|
1514
|
-
while (true) {
|
|
1515
|
-
const answer = await rl.question(
|
|
1516
|
-
" Review [o]ne-by-one or [f]older-by-folder? "
|
|
1517
|
-
);
|
|
1518
|
-
const a = answer.trim().toLowerCase();
|
|
1519
|
-
if (a === "o" || a === "one-by-one" || a === "one" || a === "file")
|
|
1520
|
-
return "file";
|
|
1521
|
-
if (a === "f" || a === "folder") return "folder";
|
|
1522
|
-
console.log(
|
|
1523
|
-
` Unknown option "${answer.trim()}". Valid: o/one-by-one, f/folder`
|
|
1524
|
-
);
|
|
1525
|
-
}
|
|
1526
|
-
} catch (err) {
|
|
1527
|
-
if (isAbortError(err)) throw new SyncCancelledError();
|
|
1528
|
-
throw err;
|
|
1529
|
-
} finally {
|
|
1530
|
-
rl.close();
|
|
1531
|
-
}
|
|
1532
|
-
}
|
|
1533
|
-
async function reviewFilesOneByOne(items, label, plannedAction, recommendedAction, content) {
|
|
1534
|
-
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
1535
|
-
const results = [];
|
|
1536
|
-
try {
|
|
1537
|
-
let applyAll = null;
|
|
1538
|
-
for (const item of items) {
|
|
1539
|
-
if (applyAll) {
|
|
1540
|
-
results.push(applyAll);
|
|
1541
|
-
continue;
|
|
1542
|
-
}
|
|
1543
|
-
const planned = plannedAction(item);
|
|
1544
|
-
const rec = recommendedAction ? recommendedAction(item) : planned;
|
|
1545
|
-
const hasContent = content != null;
|
|
1546
|
-
const prompt = ` ${label(item)} (${planned}) \u2014 ${formatActionPrompt(rec, hasContent, false)}: `;
|
|
1547
|
-
while (true) {
|
|
1548
|
-
const answer = await rl.question(prompt);
|
|
1549
|
-
const result = parseReviewAction(answer);
|
|
1550
|
-
if (result.special === "view") {
|
|
1551
|
-
if (content) {
|
|
1552
|
-
showDiff(content.local(item), content.remote(item), label(item));
|
|
1553
|
-
} else {
|
|
1554
|
-
console.log(" No content available for diff.");
|
|
1555
|
-
}
|
|
1556
|
-
continue;
|
|
1557
|
-
}
|
|
1558
|
-
if (result.special === "recommended") {
|
|
1559
|
-
results.push(rec);
|
|
1560
|
-
break;
|
|
1561
|
-
}
|
|
1562
|
-
if (result.action === null) {
|
|
1563
|
-
console.log(
|
|
1564
|
-
` Unknown option "${answer.trim()}". Valid: ${formatActionPrompt(rec, hasContent, false)}`
|
|
1565
|
-
);
|
|
1566
|
-
continue;
|
|
1567
|
-
}
|
|
1568
|
-
results.push(result.action);
|
|
1569
|
-
if (result.all) applyAll = result.action;
|
|
1570
|
-
break;
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
} catch (err) {
|
|
1574
|
-
if (isAbortError(err)) throw new SyncCancelledError();
|
|
1575
|
-
throw err;
|
|
1576
|
-
} finally {
|
|
1577
|
-
rl.close();
|
|
1578
|
-
}
|
|
1579
|
-
return results;
|
|
1580
|
-
}
|
|
1581
|
-
async function reviewFolder(folderName, items, label, plannedAction, recommendedAction, content) {
|
|
1582
|
-
console.log(`
|
|
1583
|
-
${folderName} (${items.length} files):`);
|
|
1584
|
-
for (const item of items) {
|
|
1585
|
-
const rec = recommendedAction ? recommendedAction(item) : plannedAction(item);
|
|
1586
|
-
const actionLabel = plannedAction(item);
|
|
1587
|
-
const star = actionLabel === rec ? "\u2605" : "";
|
|
1588
|
-
console.log(` ${label(item)} (${actionLabel}${star})`);
|
|
1589
|
-
}
|
|
1590
|
-
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
1591
|
-
try {
|
|
1592
|
-
while (true) {
|
|
1593
|
-
const promptStr = ` Action for all: ${formatActionPrompt(
|
|
1594
|
-
recommendedAction ? recommendedAction(items[0]) : plannedAction(items[0]),
|
|
1595
|
-
false,
|
|
1596
|
-
true
|
|
1597
|
-
)} [o]ne-by-one: `;
|
|
1598
|
-
const answer = await rl.question(promptStr);
|
|
1599
|
-
const a = answer.trim().toLowerCase();
|
|
1600
|
-
if (a === "o" || a === "one-by-one") {
|
|
1601
|
-
rl.close();
|
|
1602
|
-
return reviewFilesOneByOne(
|
|
1603
|
-
items,
|
|
1604
|
-
label,
|
|
1605
|
-
plannedAction,
|
|
1606
|
-
recommendedAction,
|
|
1607
|
-
content
|
|
1608
|
-
);
|
|
1609
|
-
}
|
|
1610
|
-
if (a === "r" || a === "recommended") {
|
|
1611
|
-
return items.map(
|
|
1612
|
-
(item) => recommendedAction ? recommendedAction(item) : plannedAction(item)
|
|
1613
|
-
);
|
|
1614
|
-
}
|
|
1615
|
-
if (a === "v" || a === "view") {
|
|
1616
|
-
if (content) {
|
|
1617
|
-
for (const item of items) {
|
|
1618
|
-
showDiff(content.local(item), content.remote(item), label(item));
|
|
1619
|
-
}
|
|
1620
|
-
} else {
|
|
1621
|
-
console.log(" No content available for diff.");
|
|
1622
|
-
}
|
|
1623
|
-
continue;
|
|
1624
|
-
}
|
|
1625
|
-
const result = parseReviewAction(a);
|
|
1626
|
-
if (result.action !== null) {
|
|
1627
|
-
return items.map(() => result.action);
|
|
1628
|
-
}
|
|
1629
|
-
console.log(
|
|
1630
|
-
` Unknown option "${answer.trim()}". Valid: ${formatActionPrompt(
|
|
1631
|
-
recommendedAction ? recommendedAction(items[0]) : plannedAction(items[0]),
|
|
1632
|
-
false,
|
|
1633
|
-
true
|
|
1634
|
-
)} [o]ne-by-one`
|
|
1635
|
-
);
|
|
1636
|
-
}
|
|
1637
|
-
} catch (err) {
|
|
1638
|
-
if (isAbortError(err)) throw new SyncCancelledError();
|
|
1639
|
-
throw err;
|
|
1640
|
-
} finally {
|
|
1641
|
-
rl.close();
|
|
1642
|
-
}
|
|
1643
|
-
}
|
|
1644
|
-
var SyncCancelledError;
|
|
1645
|
-
var init_confirm = __esm({
|
|
1646
|
-
"src/cli/confirm.ts"() {
|
|
1647
|
-
"use strict";
|
|
1648
|
-
SyncCancelledError = class extends Error {
|
|
1649
|
-
constructor() {
|
|
1650
|
-
super("Sync cancelled");
|
|
1651
|
-
this.name = "SyncCancelledError";
|
|
1652
|
-
}
|
|
1653
|
-
};
|
|
1654
|
-
}
|
|
1655
|
-
});
|
|
1656
|
-
|
|
1657
|
-
// src/lib/tech-detect.ts
|
|
1658
|
-
import { readFile as readFile7, access, readdir as readdir4 } from "node:fs/promises";
|
|
1659
|
-
import { join as join7, relative } from "node:path";
|
|
1660
|
-
async function fileExists(filePath) {
|
|
1661
|
-
try {
|
|
1662
|
-
await access(filePath);
|
|
1663
|
-
return true;
|
|
1664
|
-
} catch {
|
|
1665
|
-
return false;
|
|
605
|
+
return false;
|
|
1666
606
|
}
|
|
1667
607
|
}
|
|
1668
608
|
async function discoverMonorepoApps(projectPath) {
|
|
1669
609
|
const apps = [];
|
|
1670
610
|
const patterns = [];
|
|
1671
611
|
try {
|
|
1672
|
-
const raw = await
|
|
1673
|
-
|
|
612
|
+
const raw = await readFile5(
|
|
613
|
+
join5(projectPath, "pnpm-workspace.yaml"),
|
|
1674
614
|
"utf-8"
|
|
1675
615
|
);
|
|
1676
616
|
const matches = raw.match(/^\s*-\s*['"]?([^'"#\n]+)['"]?/gm);
|
|
@@ -1684,7 +624,7 @@ async function discoverMonorepoApps(projectPath) {
|
|
|
1684
624
|
}
|
|
1685
625
|
if (patterns.length === 0) {
|
|
1686
626
|
try {
|
|
1687
|
-
const raw = await
|
|
627
|
+
const raw = await readFile5(join5(projectPath, "package.json"), "utf-8");
|
|
1688
628
|
const pkg = JSON.parse(raw);
|
|
1689
629
|
const ws = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages;
|
|
1690
630
|
if (ws) patterns.push(...ws);
|
|
@@ -1694,14 +634,14 @@ async function discoverMonorepoApps(projectPath) {
|
|
|
1694
634
|
for (const pattern of patterns) {
|
|
1695
635
|
if (pattern.endsWith("/*")) {
|
|
1696
636
|
const dir = pattern.slice(0, -2);
|
|
1697
|
-
const absDir =
|
|
637
|
+
const absDir = join5(projectPath, dir);
|
|
1698
638
|
try {
|
|
1699
|
-
const entries = await
|
|
639
|
+
const entries = await readdir(absDir, { withFileTypes: true });
|
|
1700
640
|
for (const entry of entries) {
|
|
1701
641
|
if (entry.isDirectory()) {
|
|
1702
|
-
const relPath =
|
|
1703
|
-
const absPath =
|
|
1704
|
-
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"))) {
|
|
1705
645
|
apps.push({ name: entry.name, path: relPath, absPath });
|
|
1706
646
|
}
|
|
1707
647
|
}
|
|
@@ -1715,12 +655,12 @@ async function discoverMonorepoApps(projectPath) {
|
|
|
1715
655
|
async function hasJsxFile(dir, depth = 0) {
|
|
1716
656
|
if (depth > 6) return false;
|
|
1717
657
|
try {
|
|
1718
|
-
const entries = await
|
|
658
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
1719
659
|
for (const entry of entries) {
|
|
1720
660
|
const name = entry.name;
|
|
1721
661
|
if (entry.isDirectory()) {
|
|
1722
662
|
if (SKIP_DIRS.has(name) || JSX_SKIP_DIRS.has(name)) continue;
|
|
1723
|
-
if (await hasJsxFile(
|
|
663
|
+
if (await hasJsxFile(join5(dir, name), depth + 1)) return true;
|
|
1724
664
|
} else if (entry.isFile()) {
|
|
1725
665
|
if (JSX_TEST_PATTERN.test(name)) continue;
|
|
1726
666
|
if (name.endsWith(".tsx") || name.endsWith(".jsx")) return true;
|
|
@@ -1739,7 +679,7 @@ async function hasJsxFile(dir, depth = 0) {
|
|
|
1739
679
|
async function detectCapabilities(dirPath, pkgJson) {
|
|
1740
680
|
const caps = /* @__PURE__ */ new Set();
|
|
1741
681
|
for (const sub of JSX_SCAN_DIRS) {
|
|
1742
|
-
if (await hasJsxFile(
|
|
682
|
+
if (await hasJsxFile(join5(dirPath, sub))) {
|
|
1743
683
|
caps.add("jsx");
|
|
1744
684
|
break;
|
|
1745
685
|
}
|
|
@@ -1761,7 +701,7 @@ async function detectCapabilities(dirPath, pkgJson) {
|
|
|
1761
701
|
}
|
|
1762
702
|
}
|
|
1763
703
|
}
|
|
1764
|
-
if (!caps.has("node-server") && await fileExists(
|
|
704
|
+
if (!caps.has("node-server") && await fileExists(join5(dirPath, "src", "main.ts"))) {
|
|
1765
705
|
caps.add("node-server");
|
|
1766
706
|
}
|
|
1767
707
|
if (pkgJson && pkgJson.bin) {
|
|
@@ -1777,7 +717,7 @@ async function detectFromDirectory(dirPath) {
|
|
|
1777
717
|
const seen = /* @__PURE__ */ new Map();
|
|
1778
718
|
let pkgJson = null;
|
|
1779
719
|
try {
|
|
1780
|
-
const raw = await
|
|
720
|
+
const raw = await readFile5(join5(dirPath, "package.json"), "utf-8");
|
|
1781
721
|
pkgJson = JSON.parse(raw);
|
|
1782
722
|
const allDeps = {
|
|
1783
723
|
...pkgJson.dependencies ?? {},
|
|
@@ -1809,7 +749,7 @@ async function detectFromDirectory(dirPath) {
|
|
|
1809
749
|
}
|
|
1810
750
|
for (const { file, rule } of CONFIG_FILE_MAP) {
|
|
1811
751
|
const key = rule.name.toLowerCase();
|
|
1812
|
-
if (!seen.has(key) && await fileExists(
|
|
752
|
+
if (!seen.has(key) && await fileExists(join5(dirPath, file))) {
|
|
1813
753
|
seen.set(key, { name: rule.name, category: rule.category });
|
|
1814
754
|
}
|
|
1815
755
|
}
|
|
@@ -1987,16 +927,16 @@ function categorizeDependency(depName) {
|
|
|
1987
927
|
async function findPackageJsonFiles(dir, projectPath, depth = 0) {
|
|
1988
928
|
if (depth > 4) return [];
|
|
1989
929
|
const results = [];
|
|
1990
|
-
const pkgPath =
|
|
930
|
+
const pkgPath = join5(dir, "package.json");
|
|
1991
931
|
if (await fileExists(pkgPath)) {
|
|
1992
932
|
results.push(pkgPath);
|
|
1993
933
|
}
|
|
1994
934
|
try {
|
|
1995
|
-
const entries = await
|
|
935
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
1996
936
|
for (const entry of entries) {
|
|
1997
937
|
if (!entry.isDirectory() || SKIP_DIRS.has(entry.name)) continue;
|
|
1998
938
|
const subResults = await findPackageJsonFiles(
|
|
1999
|
-
|
|
939
|
+
join5(dir, entry.name),
|
|
2000
940
|
projectPath,
|
|
2001
941
|
depth + 1
|
|
2002
942
|
);
|
|
@@ -2011,7 +951,7 @@ async function scanAllDependencies(projectPath) {
|
|
|
2011
951
|
const dependencies = [];
|
|
2012
952
|
for (const pkgPath of packageJsonPaths) {
|
|
2013
953
|
try {
|
|
2014
|
-
const raw = await
|
|
954
|
+
const raw = await readFile5(pkgPath, "utf-8");
|
|
2015
955
|
const pkg = JSON.parse(raw);
|
|
2016
956
|
const sourcePath = relative(projectPath, pkgPath);
|
|
2017
957
|
const depSections = [
|
|
@@ -2225,176 +1165,27 @@ var init_tech_detect = __esm({
|
|
|
2225
1165
|
}
|
|
2226
1166
|
});
|
|
2227
1167
|
|
|
2228
|
-
// src/lib/
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
const
|
|
2232
|
-
const
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
if (!scripts?.dev) return null;
|
|
2244
|
-
const parts = scripts.dev.split(/\s+/);
|
|
2245
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
2246
|
-
if (parts[i] === "--port" || parts[i] === "-p") {
|
|
2247
|
-
const next = parts[i + 1];
|
|
2248
|
-
if (next) {
|
|
2249
|
-
const port = parseInt(next, 10);
|
|
2250
|
-
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);
|
|
2251
1183
|
}
|
|
2252
1184
|
}
|
|
1185
|
+
m = line.match(/^const\s+([A-Za-z_$][\w$]*)\s*=\s*require/);
|
|
1186
|
+
if (m) names.add(m[1]);
|
|
2253
1187
|
}
|
|
2254
|
-
return
|
|
2255
|
-
}
|
|
2256
|
-
var init_server_detect = __esm({
|
|
2257
|
-
"src/lib/server-detect.ts"() {
|
|
2258
|
-
"use strict";
|
|
2259
|
-
}
|
|
2260
|
-
});
|
|
2261
|
-
|
|
2262
|
-
// src/lib/port-verify.ts
|
|
2263
|
-
import { readFile as readFile8 } from "node:fs/promises";
|
|
2264
|
-
async function verifyPorts(projectPath, portAllocations) {
|
|
2265
|
-
const mismatches = [];
|
|
2266
|
-
const allocatedPorts = new Set(portAllocations.map((a) => a.port));
|
|
2267
|
-
const packageJsonPaths = await findPackageJsonFiles(projectPath, projectPath);
|
|
2268
|
-
for (const pkgPath of packageJsonPaths) {
|
|
2269
|
-
try {
|
|
2270
|
-
const raw = await readFile8(pkgPath, "utf-8");
|
|
2271
|
-
const pkg = JSON.parse(raw);
|
|
2272
|
-
const scriptPort = detectPortFromScripts(pkg);
|
|
2273
|
-
if (scriptPort !== null && !allocatedPorts.has(scriptPort)) {
|
|
2274
|
-
const relativePath = pkgPath.replace(projectPath + "/", "");
|
|
2275
|
-
const matchingAlloc = portAllocations.find(
|
|
2276
|
-
(a) => a.label === getAppLabel(relativePath)
|
|
2277
|
-
);
|
|
2278
|
-
mismatches.push({
|
|
2279
|
-
packageJsonPath: relativePath,
|
|
2280
|
-
scriptPort,
|
|
2281
|
-
allocation: matchingAlloc ?? null,
|
|
2282
|
-
reason: matchingAlloc ? `Script uses port ${scriptPort} but allocation has port ${matchingAlloc.port}` : `Port ${scriptPort} in scripts is not in any allocation`
|
|
2283
|
-
});
|
|
2284
|
-
}
|
|
2285
|
-
} catch {
|
|
2286
|
-
}
|
|
2287
|
-
}
|
|
2288
|
-
return mismatches;
|
|
2289
|
-
}
|
|
2290
|
-
function isDevServerScript(pkg) {
|
|
2291
|
-
const scripts = pkg.scripts;
|
|
2292
|
-
const raw = scripts?.dev;
|
|
2293
|
-
if (!raw || typeof raw !== "string") return false;
|
|
2294
|
-
const script = raw.trim().toLowerCase();
|
|
2295
|
-
if (!script) return false;
|
|
2296
|
-
for (const pattern of DEV_SERVER_BIN_PATTERNS) {
|
|
2297
|
-
if (pattern.test(script)) return true;
|
|
2298
|
-
}
|
|
2299
|
-
const tokens = script.split(/\s+/);
|
|
2300
|
-
for (const token of tokens) {
|
|
2301
|
-
if (token === "--port" || token === "-p") return true;
|
|
2302
|
-
if (token.startsWith("--port=")) return true;
|
|
2303
|
-
}
|
|
2304
|
-
return false;
|
|
2305
|
-
}
|
|
2306
|
-
function labelMatchesAppName(label, appName) {
|
|
2307
|
-
if (!label || !appName) return false;
|
|
2308
|
-
const normalize = (s) => s.toLowerCase().replace(/-/g, " ").replace(/[()]/g, " ").replace(/\s+/g, " ").trim();
|
|
2309
|
-
const labelTokens = normalize(label).split(" ").filter(Boolean);
|
|
2310
|
-
const appToken = normalize(appName);
|
|
2311
|
-
if (!appToken) return false;
|
|
2312
|
-
const appTokens = appToken.split(" ").filter(Boolean);
|
|
2313
|
-
if (appTokens.length === 1) {
|
|
2314
|
-
return labelTokens.includes(appTokens[0]);
|
|
2315
|
-
}
|
|
2316
|
-
for (let i = 0; i <= labelTokens.length - appTokens.length; i++) {
|
|
2317
|
-
if (appTokens.every((t, j) => labelTokens[i + j] === t)) return true;
|
|
2318
|
-
}
|
|
2319
|
-
return false;
|
|
2320
|
-
}
|
|
2321
|
-
async function findUnallocatedApps(projectPath, portAllocations) {
|
|
2322
|
-
const apps = await discoverMonorepoApps(projectPath);
|
|
2323
|
-
if (apps.length === 0) {
|
|
2324
|
-
return [];
|
|
2325
|
-
}
|
|
2326
|
-
const unallocated = [];
|
|
2327
|
-
for (const app of apps) {
|
|
2328
|
-
if (portAllocations.some((a) => labelMatchesAppName(a.label ?? "", app.name))) {
|
|
2329
|
-
continue;
|
|
2330
|
-
}
|
|
2331
|
-
let pkg;
|
|
2332
|
-
try {
|
|
2333
|
-
const raw = await readFile8(`${app.absPath}/package.json`, "utf-8");
|
|
2334
|
-
pkg = JSON.parse(raw);
|
|
2335
|
-
} catch {
|
|
2336
|
-
continue;
|
|
2337
|
-
}
|
|
2338
|
-
if (!isDevServerScript(pkg)) continue;
|
|
2339
|
-
const framework = detectFramework(pkg);
|
|
2340
|
-
const detectedPort = detectPortFromScripts(pkg);
|
|
2341
|
-
const command = `pnpm --filter ${app.name} dev`;
|
|
2342
|
-
unallocated.push({
|
|
2343
|
-
name: app.name,
|
|
2344
|
-
path: app.path,
|
|
2345
|
-
framework,
|
|
2346
|
-
detectedPort,
|
|
2347
|
-
command
|
|
2348
|
-
});
|
|
2349
|
-
}
|
|
2350
|
-
return unallocated;
|
|
2351
|
-
}
|
|
2352
|
-
function getAppLabel(relativePath) {
|
|
2353
|
-
const parts = relativePath.split("/");
|
|
2354
|
-
if (parts.length >= 3 && parts[0] === "apps") {
|
|
2355
|
-
return parts[1];
|
|
2356
|
-
}
|
|
2357
|
-
return "root";
|
|
2358
|
-
}
|
|
2359
|
-
var DEV_SERVER_BIN_PATTERNS;
|
|
2360
|
-
var init_port_verify = __esm({
|
|
2361
|
-
"src/lib/port-verify.ts"() {
|
|
2362
|
-
"use strict";
|
|
2363
|
-
init_tech_detect();
|
|
2364
|
-
init_server_detect();
|
|
2365
|
-
DEV_SERVER_BIN_PATTERNS = [
|
|
2366
|
-
/\bnext\s+dev\b/,
|
|
2367
|
-
/\bnest\s+start\b/,
|
|
2368
|
-
/\bvite\s+(?:dev|serve)\b/,
|
|
2369
|
-
/\bvite\s+preview\b/,
|
|
2370
|
-
/\bnuxt\s+dev\b/,
|
|
2371
|
-
/\b(?:svelte-kit|sveltekit)\s+dev\b/,
|
|
2372
|
-
/\bexpo\s+start\b/
|
|
2373
|
-
];
|
|
2374
|
-
}
|
|
2375
|
-
});
|
|
2376
|
-
|
|
2377
|
-
// src/lib/eslint-generator.ts
|
|
2378
|
-
import { createHash } from "node:crypto";
|
|
2379
|
-
function importedIdentifiers(importLines) {
|
|
2380
|
-
const names = /* @__PURE__ */ new Set();
|
|
2381
|
-
for (const line of importLines) {
|
|
2382
|
-
let m = line.match(/^import\s+([A-Za-z_$][\w$]*)\s+from/);
|
|
2383
|
-
if (m) names.add(m[1]);
|
|
2384
|
-
m = line.match(/^import\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from/);
|
|
2385
|
-
if (m) names.add(m[1]);
|
|
2386
|
-
m = line.match(/^import\s*\{([^}]*)\}\s*from/);
|
|
2387
|
-
if (m) {
|
|
2388
|
-
for (const entry of m[1].split(",")) {
|
|
2389
|
-
const parts = entry.trim().split(/\s+as\s+/);
|
|
2390
|
-
const n = (parts[1] ?? parts[0]).trim();
|
|
2391
|
-
if (n) names.add(n);
|
|
2392
|
-
}
|
|
2393
|
-
}
|
|
2394
|
-
m = line.match(/^const\s+([A-Za-z_$][\w$]*)\s*=\s*require/);
|
|
2395
|
-
if (m) names.add(m[1]);
|
|
2396
|
-
}
|
|
2397
|
-
return names;
|
|
1188
|
+
return names;
|
|
2398
1189
|
}
|
|
2399
1190
|
function parseFragment(fragment) {
|
|
2400
1191
|
if (!fragment) return { imports: [], configComments: [] };
|
|
@@ -2443,7 +1234,7 @@ function collectDependencies(presets) {
|
|
|
2443
1234
|
return deps;
|
|
2444
1235
|
}
|
|
2445
1236
|
function hashConfig(content) {
|
|
2446
|
-
return
|
|
1237
|
+
return createHash2("sha256").update(content).digest("hex");
|
|
2447
1238
|
}
|
|
2448
1239
|
function buildRules(presets) {
|
|
2449
1240
|
const merged = {};
|
|
@@ -2556,8 +1347,7 @@ function generateEslintConfig(input) {
|
|
|
2556
1347
|
sections.push(
|
|
2557
1348
|
"/**",
|
|
2558
1349
|
" * ESLint flat config \u2014 generated by CodeByPlan CLI.",
|
|
2559
|
-
" * Edit rule overrides via the web UI, then run `codebyplan eslint
|
|
2560
|
-
" * Manual edits will be detected as drift.",
|
|
1350
|
+
" * Edit rule overrides via the web UI, then run `codebyplan eslint init` to regenerate.",
|
|
2561
1351
|
" */",
|
|
2562
1352
|
""
|
|
2563
1353
|
);
|
|
@@ -2758,13 +1548,11 @@ var init_eslint_generator = __esm({
|
|
|
2758
1548
|
var eslint_exports = {};
|
|
2759
1549
|
__export(eslint_exports, {
|
|
2760
1550
|
autoDetectIgnorePatterns: () => autoDetectIgnorePatterns,
|
|
2761
|
-
checkEslintDrift: () => checkEslintDrift,
|
|
2762
1551
|
eslintInit: () => eslintInit,
|
|
2763
|
-
eslintSync: () => eslintSync,
|
|
2764
1552
|
runEslint: () => runEslint
|
|
2765
1553
|
});
|
|
2766
|
-
import { readFile as
|
|
2767
|
-
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";
|
|
2768
1556
|
async function fileExists2(filePath) {
|
|
2769
1557
|
try {
|
|
2770
1558
|
await access2(filePath);
|
|
@@ -2775,12 +1563,12 @@ async function fileExists2(filePath) {
|
|
|
2775
1563
|
}
|
|
2776
1564
|
async function autoDetectIgnorePatterns(absPath) {
|
|
2777
1565
|
const patterns = [];
|
|
2778
|
-
if (await fileExists2(
|
|
1566
|
+
if (await fileExists2(join6(absPath, "esbuild.js"))) {
|
|
2779
1567
|
patterns.push("esbuild.js");
|
|
2780
1568
|
}
|
|
2781
1569
|
let entries = [];
|
|
2782
1570
|
try {
|
|
2783
|
-
entries = await
|
|
1571
|
+
entries = await readdir2(absPath);
|
|
2784
1572
|
} catch (err) {
|
|
2785
1573
|
console.error(
|
|
2786
1574
|
` autoDetectIgnorePatterns: failed to read ${absPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -2795,19 +1583,19 @@ async function autoDetectIgnorePatterns(absPath) {
|
|
|
2795
1583
|
}
|
|
2796
1584
|
for (const ext of ["ts", "mts", "js", "mjs"]) {
|
|
2797
1585
|
const candidate = `vitest.config.${ext}`;
|
|
2798
|
-
if (await fileExists2(
|
|
1586
|
+
if (await fileExists2(join6(absPath, candidate))) {
|
|
2799
1587
|
patterns.push(candidate);
|
|
2800
1588
|
break;
|
|
2801
1589
|
}
|
|
2802
1590
|
}
|
|
2803
1591
|
for (const ext of ["ts", "mts", "js", "mjs"]) {
|
|
2804
1592
|
const candidate = `vite.config.${ext}`;
|
|
2805
|
-
if (await fileExists2(
|
|
1593
|
+
if (await fileExists2(join6(absPath, candidate))) {
|
|
2806
1594
|
patterns.push(candidate);
|
|
2807
1595
|
break;
|
|
2808
1596
|
}
|
|
2809
1597
|
}
|
|
2810
|
-
if (await fileExists2(
|
|
1598
|
+
if (await fileExists2(join6(absPath, "tauri.conf.json"))) {
|
|
2811
1599
|
patterns.push("src-tauri/**");
|
|
2812
1600
|
patterns.push("**/*.d.ts");
|
|
2813
1601
|
}
|
|
@@ -2815,14 +1603,14 @@ async function autoDetectIgnorePatterns(absPath) {
|
|
|
2815
1603
|
}
|
|
2816
1604
|
function detectPackageManager(projectPath) {
|
|
2817
1605
|
return (async () => {
|
|
2818
|
-
if (await fileExists2(
|
|
2819
|
-
if (await fileExists2(
|
|
1606
|
+
if (await fileExists2(join6(projectPath, "pnpm-lock.yaml"))) return "pnpm";
|
|
1607
|
+
if (await fileExists2(join6(projectPath, "yarn.lock"))) return "yarn";
|
|
2820
1608
|
return "npm";
|
|
2821
1609
|
})();
|
|
2822
1610
|
}
|
|
2823
1611
|
async function getInstalledDeps(pkgJsonPath) {
|
|
2824
1612
|
try {
|
|
2825
|
-
const raw = await
|
|
1613
|
+
const raw = await readFile6(pkgJsonPath, "utf-8");
|
|
2826
1614
|
const pkg = JSON.parse(raw);
|
|
2827
1615
|
const all = /* @__PURE__ */ new Set();
|
|
2828
1616
|
for (const name of Object.keys(pkg.dependencies ?? {})) all.add(name);
|
|
@@ -2935,7 +1723,7 @@ async function eslintInit(repoId, projectPath) {
|
|
|
2935
1723
|
ignorePatterns: detectedIgnores
|
|
2936
1724
|
});
|
|
2937
1725
|
const hash = hashConfig(content);
|
|
2938
|
-
const configPath =
|
|
1726
|
+
const configPath = join6(target.absPath, "eslint.config.mjs");
|
|
2939
1727
|
configsToWrite.push({
|
|
2940
1728
|
target,
|
|
2941
1729
|
presets,
|
|
@@ -2957,11 +1745,11 @@ async function eslintInit(repoId, projectPath) {
|
|
|
2957
1745
|
return;
|
|
2958
1746
|
}
|
|
2959
1747
|
const pm = await detectPackageManager(projectPath);
|
|
2960
|
-
const rootPkgJsonPath =
|
|
1748
|
+
const rootPkgJsonPath = join6(projectPath, "package.json");
|
|
2961
1749
|
const installed = await getInstalledDeps(rootPkgJsonPath);
|
|
2962
1750
|
if (isMonorepo) {
|
|
2963
1751
|
for (const { target } of configsToWrite) {
|
|
2964
|
-
const appPkgJson =
|
|
1752
|
+
const appPkgJson = join6(target.absPath, "package.json");
|
|
2965
1753
|
const appDeps = await getInstalledDeps(appPkgJson);
|
|
2966
1754
|
for (const dep of appDeps) {
|
|
2967
1755
|
installed.add(dep);
|
|
@@ -2987,9 +1775,9 @@ async function eslintInit(repoId, projectPath) {
|
|
|
2987
1775
|
Install ${missingPkgs.length} missing packages? [Y/n] `
|
|
2988
1776
|
);
|
|
2989
1777
|
if (confirmed) {
|
|
2990
|
-
const { execSync } = await import("node:child_process");
|
|
1778
|
+
const { execSync: execSync3 } = await import("node:child_process");
|
|
2991
1779
|
try {
|
|
2992
|
-
|
|
1780
|
+
execSync3(installCmd, { cwd: projectPath, stdio: "inherit" });
|
|
2993
1781
|
console.log(" Packages installed.\n");
|
|
2994
1782
|
} catch (err) {
|
|
2995
1783
|
console.error(
|
|
@@ -3013,7 +1801,7 @@ async function eslintInit(repoId, projectPath) {
|
|
|
3013
1801
|
} of configsToWrite) {
|
|
3014
1802
|
if (await fileExists2(configPath)) {
|
|
3015
1803
|
try {
|
|
3016
|
-
const existing = await
|
|
1804
|
+
const existing = await readFile6(configPath, "utf-8");
|
|
3017
1805
|
const existingHash = hashConfig(existing);
|
|
3018
1806
|
if (existingHash === hash) {
|
|
3019
1807
|
console.log(
|
|
@@ -3056,134 +1844,6 @@ async function eslintInit(repoId, projectPath) {
|
|
|
3056
1844
|
}
|
|
3057
1845
|
console.log("\n ESLint init complete.\n");
|
|
3058
1846
|
}
|
|
3059
|
-
async function eslintSync(repoId, projectPath) {
|
|
3060
|
-
console.log("\n ESLint Sync");
|
|
3061
|
-
console.log(` Repo: ${repoId}`);
|
|
3062
|
-
console.log(` Path: ${projectPath}
|
|
3063
|
-
`);
|
|
3064
|
-
let configs;
|
|
3065
|
-
try {
|
|
3066
|
-
const res = await apiGet(
|
|
3067
|
-
`/repos/${repoId}/eslint-config`
|
|
3068
|
-
);
|
|
3069
|
-
configs = res.data ?? [];
|
|
3070
|
-
} catch {
|
|
3071
|
-
console.log(
|
|
3072
|
-
" No existing ESLint config found. Run `codebyplan eslint init` first.\n"
|
|
3073
|
-
);
|
|
3074
|
-
return;
|
|
3075
|
-
}
|
|
3076
|
-
if (configs.length === 0) {
|
|
3077
|
-
console.log(
|
|
3078
|
-
" No ESLint configs registered. Run `codebyplan eslint init` first.\n"
|
|
3079
|
-
);
|
|
3080
|
-
return;
|
|
3081
|
-
}
|
|
3082
|
-
let updatedCount = 0;
|
|
3083
|
-
let skippedCount = 0;
|
|
3084
|
-
let driftCount = 0;
|
|
3085
|
-
for (const config of configs) {
|
|
3086
|
-
const absPath = config.source_path === "." ? projectPath : join8(projectPath, config.source_path);
|
|
3087
|
-
const configPath = join8(absPath, "eslint.config.mjs");
|
|
3088
|
-
const detected = await detectTechStack(absPath);
|
|
3089
|
-
const techNames = detected.flat.map((t) => t.name).filter((n) => n !== SYNTHETIC_CARRIER_NAME);
|
|
3090
|
-
const capabilities = collectCapabilities(detected.flat);
|
|
3091
|
-
const currentPresets = await resolvePresetsForTechStack(
|
|
3092
|
-
techNames,
|
|
3093
|
-
capabilities
|
|
3094
|
-
);
|
|
3095
|
-
const currentPresetIds = currentPresets.map((p) => p.id).sort();
|
|
3096
|
-
const savedPresetIds = [...config.active_preset_ids ?? []].sort();
|
|
3097
|
-
const presetsChanged = currentPresetIds.length !== savedPresetIds.length || currentPresetIds.some((id) => !savedPresetIds.includes(id));
|
|
3098
|
-
if (!presetsChanged) {
|
|
3099
|
-
if (await fileExists2(configPath)) {
|
|
3100
|
-
try {
|
|
3101
|
-
const currentContent = await readFile9(configPath, "utf-8");
|
|
3102
|
-
const currentHash = hashConfig(currentContent);
|
|
3103
|
-
if (config.generated_hash && currentHash !== config.generated_hash) {
|
|
3104
|
-
console.log(
|
|
3105
|
-
` ${config.source_path}: drift detected (manually edited). Not overwriting.`
|
|
3106
|
-
);
|
|
3107
|
-
driftCount++;
|
|
3108
|
-
continue;
|
|
3109
|
-
}
|
|
3110
|
-
skippedCount++;
|
|
3111
|
-
continue;
|
|
3112
|
-
} catch {
|
|
3113
|
-
console.warn(
|
|
3114
|
-
` ${config.source_path}: config file unreadable, regenerating...`
|
|
3115
|
-
);
|
|
3116
|
-
}
|
|
3117
|
-
} else {
|
|
3118
|
-
console.log(
|
|
3119
|
-
` ${config.source_path}: config file missing, regenerating...`
|
|
3120
|
-
);
|
|
3121
|
-
}
|
|
3122
|
-
}
|
|
3123
|
-
if (presetsChanged) {
|
|
3124
|
-
console.log(` ${config.source_path}: presets changed, regenerating...`);
|
|
3125
|
-
}
|
|
3126
|
-
const userOverrides = config.rule_overrides;
|
|
3127
|
-
const detectedIgnores = await autoDetectIgnorePatterns(absPath);
|
|
3128
|
-
const content = generateEslintConfig({
|
|
3129
|
-
presets: currentPresets,
|
|
3130
|
-
ruleOverrides: userOverrides && Object.keys(userOverrides).length > 0 ? userOverrides : void 0,
|
|
3131
|
-
ignorePatterns: detectedIgnores
|
|
3132
|
-
});
|
|
3133
|
-
try {
|
|
3134
|
-
await writeFile4(configPath, content, "utf-8");
|
|
3135
|
-
} catch (err) {
|
|
3136
|
-
console.error(
|
|
3137
|
-
` ${config.source_path}: Failed to write config: ${err instanceof Error ? err.message : String(err)}`
|
|
3138
|
-
);
|
|
3139
|
-
continue;
|
|
3140
|
-
}
|
|
3141
|
-
const newHash = hashConfig(content);
|
|
3142
|
-
try {
|
|
3143
|
-
await apiPut(`/repos/${repoId}/eslint-config`, {
|
|
3144
|
-
source_path: config.source_path,
|
|
3145
|
-
preset_ids: currentPresetIds,
|
|
3146
|
-
rule_overrides: userOverrides ?? {},
|
|
3147
|
-
generated_hash: newHash
|
|
3148
|
-
});
|
|
3149
|
-
} catch (err) {
|
|
3150
|
-
console.error(
|
|
3151
|
-
` Warning: Failed to update server: ${err instanceof Error ? err.message : String(err)}`
|
|
3152
|
-
);
|
|
3153
|
-
}
|
|
3154
|
-
updatedCount++;
|
|
3155
|
-
}
|
|
3156
|
-
console.log(
|
|
3157
|
-
`
|
|
3158
|
-
Sync: ${updatedCount} updated, ${skippedCount} unchanged, ${driftCount} drift detected.
|
|
3159
|
-
`
|
|
3160
|
-
);
|
|
3161
|
-
}
|
|
3162
|
-
async function checkEslintDrift(repoId, projectPath) {
|
|
3163
|
-
try {
|
|
3164
|
-
const res = await apiGet(
|
|
3165
|
-
`/repos/${repoId}/eslint-config`
|
|
3166
|
-
);
|
|
3167
|
-
const configs = res.data ?? [];
|
|
3168
|
-
for (const config of configs) {
|
|
3169
|
-
if (!config.generated_hash) continue;
|
|
3170
|
-
const absPath = config.source_path === "." ? projectPath : join8(projectPath, config.source_path);
|
|
3171
|
-
const configPath = join8(absPath, "eslint.config.mjs");
|
|
3172
|
-
if (!await fileExists2(configPath)) continue;
|
|
3173
|
-
try {
|
|
3174
|
-
const content = await readFile9(configPath, "utf-8");
|
|
3175
|
-
const currentHash = hashConfig(content);
|
|
3176
|
-
if (currentHash !== config.generated_hash) {
|
|
3177
|
-
return true;
|
|
3178
|
-
}
|
|
3179
|
-
} catch {
|
|
3180
|
-
}
|
|
3181
|
-
}
|
|
3182
|
-
return false;
|
|
3183
|
-
} catch {
|
|
3184
|
-
return false;
|
|
3185
|
-
}
|
|
3186
|
-
}
|
|
3187
1847
|
async function runEslint() {
|
|
3188
1848
|
const subcommand = process.argv[3];
|
|
3189
1849
|
const flags = parseFlags(4);
|
|
@@ -3194,14 +1854,10 @@ async function runEslint() {
|
|
|
3194
1854
|
case "init":
|
|
3195
1855
|
await eslintInit(repoId, projectPath);
|
|
3196
1856
|
break;
|
|
3197
|
-
case "sync":
|
|
3198
|
-
await eslintSync(repoId, projectPath);
|
|
3199
|
-
break;
|
|
3200
1857
|
default:
|
|
3201
1858
|
console.log(`
|
|
3202
1859
|
Usage:
|
|
3203
1860
|
codebyplan eslint init Detect tech stack, resolve presets, generate eslint.config.mjs
|
|
3204
|
-
codebyplan eslint sync Regenerate if presets changed, detect drift
|
|
3205
1861
|
`);
|
|
3206
1862
|
break;
|
|
3207
1863
|
}
|
|
@@ -3209,7 +1865,7 @@ async function runEslint() {
|
|
|
3209
1865
|
var init_eslint = __esm({
|
|
3210
1866
|
"src/cli/eslint.ts"() {
|
|
3211
1867
|
"use strict";
|
|
3212
|
-
|
|
1868
|
+
init_flags();
|
|
3213
1869
|
init_confirm();
|
|
3214
1870
|
init_api();
|
|
3215
1871
|
init_tech_detect();
|
|
@@ -3217,556 +1873,128 @@ var init_eslint = __esm({
|
|
|
3217
1873
|
}
|
|
3218
1874
|
});
|
|
3219
1875
|
|
|
3220
|
-
// src/cli/
|
|
3221
|
-
var
|
|
3222
|
-
__export(
|
|
3223
|
-
|
|
1876
|
+
// src/cli/resolve-worktree.ts
|
|
1877
|
+
var resolve_worktree_exports = {};
|
|
1878
|
+
__export(resolve_worktree_exports, {
|
|
1879
|
+
runResolveWorktree: () => runResolveWorktree
|
|
3224
1880
|
});
|
|
3225
|
-
import {
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
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);
|
|
1888
|
+
}
|
|
1889
|
+
const repoId = found.contents.repo_id;
|
|
1890
|
+
const deviceId = await getOrCreateDeviceId(projectPath);
|
|
1891
|
+
let branch = "";
|
|
1892
|
+
try {
|
|
1893
|
+
branch = execSync2("git symbolic-ref --short HEAD", {
|
|
1894
|
+
cwd: projectPath,
|
|
1895
|
+
encoding: "utf-8"
|
|
1896
|
+
}).trim();
|
|
1897
|
+
} catch {
|
|
1898
|
+
}
|
|
1899
|
+
const worktreeId = await resolveWorktreeId({
|
|
1900
|
+
repoId,
|
|
1901
|
+
repoPath: projectPath,
|
|
1902
|
+
branch,
|
|
1903
|
+
deviceId
|
|
1904
|
+
});
|
|
1905
|
+
if (worktreeId) {
|
|
1906
|
+
process.stdout.write(worktreeId);
|
|
1907
|
+
}
|
|
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}
|
|
1913
|
+
`);
|
|
1914
|
+
}
|
|
1915
|
+
process.exit(0);
|
|
1916
|
+
}
|
|
3230
1917
|
}
|
|
3231
|
-
|
|
1918
|
+
var init_resolve_worktree2 = __esm({
|
|
1919
|
+
"src/cli/resolve-worktree.ts"() {
|
|
1920
|
+
"use strict";
|
|
1921
|
+
init_flags();
|
|
1922
|
+
init_local_config();
|
|
1923
|
+
init_resolve_worktree();
|
|
1924
|
+
}
|
|
1925
|
+
});
|
|
1926
|
+
|
|
1927
|
+
// src/cli/config.ts
|
|
1928
|
+
var config_exports = {};
|
|
1929
|
+
__export(config_exports, {
|
|
1930
|
+
runConfig: () => runConfig
|
|
1931
|
+
});
|
|
1932
|
+
import { readFile as readFile7, writeFile as writeFile5 } from "node:fs/promises";
|
|
1933
|
+
import { join as join7 } from "node:path";
|
|
1934
|
+
async function runConfig() {
|
|
3232
1935
|
const flags = parseFlags(3);
|
|
3233
1936
|
const dryRun = hasFlag("dry-run", 3);
|
|
3234
|
-
const force = hasFlag("force", 3);
|
|
3235
|
-
const fix = hasFlag("fix", 3);
|
|
3236
1937
|
validateApiKey();
|
|
3237
1938
|
const config = await resolveConfig(flags);
|
|
3238
1939
|
const { repoId, projectPath } = config;
|
|
3239
1940
|
console.log(`
|
|
3240
|
-
CodeByPlan
|
|
1941
|
+
CodeByPlan Config`);
|
|
3241
1942
|
console.log(` Repo: ${repoId}`);
|
|
3242
1943
|
console.log(` Path: ${projectPath}`);
|
|
3243
1944
|
if (dryRun) console.log(` Mode: dry-run`);
|
|
3244
|
-
if (force) console.log(` Mode: force`);
|
|
3245
1945
|
console.log();
|
|
3246
|
-
|
|
3247
|
-
|
|
1946
|
+
await syncConfigToFile(repoId, projectPath, dryRun);
|
|
1947
|
+
console.log("\n Config complete.\n");
|
|
1948
|
+
}
|
|
1949
|
+
async function syncConfigToFile(repoId, projectPath, dryRun) {
|
|
1950
|
+
const configPath = join7(projectPath, ".codebyplan.json");
|
|
1951
|
+
let currentConfig = {};
|
|
1952
|
+
try {
|
|
1953
|
+
const raw = await readFile7(configPath, "utf-8");
|
|
1954
|
+
currentConfig = JSON.parse(raw);
|
|
1955
|
+
} catch {
|
|
1956
|
+
currentConfig = { repo_id: repoId };
|
|
1957
|
+
}
|
|
1958
|
+
let resolvedWorktreeId;
|
|
1959
|
+
try {
|
|
1960
|
+
const deviceId = await getOrCreateDeviceId(projectPath);
|
|
1961
|
+
let branch = "main";
|
|
3248
1962
|
try {
|
|
3249
|
-
await
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
if (!force) return;
|
|
3267
|
-
await apiPost("/sync/lock", {
|
|
3268
|
-
repo_id: repoId,
|
|
3269
|
-
locked_by: `cli-sync`,
|
|
3270
|
-
reason: "Bidirectional sync (forced)",
|
|
3271
|
-
ttl_minutes: 10
|
|
3272
|
-
});
|
|
3273
|
-
console.log(" Lock acquired (forced).\n");
|
|
3274
|
-
} else {
|
|
3275
|
-
throw lockErr;
|
|
3276
|
-
}
|
|
1963
|
+
const { execSync: execSync3 } = await import("node:child_process");
|
|
1964
|
+
branch = execSync3("git symbolic-ref --short HEAD", {
|
|
1965
|
+
cwd: projectPath,
|
|
1966
|
+
encoding: "utf-8"
|
|
1967
|
+
}).trim();
|
|
1968
|
+
} catch {
|
|
1969
|
+
}
|
|
1970
|
+
const tupleId = await resolveWorktreeId({
|
|
1971
|
+
repoId,
|
|
1972
|
+
repoPath: projectPath,
|
|
1973
|
+
branch,
|
|
1974
|
+
deviceId
|
|
1975
|
+
});
|
|
1976
|
+
if (tupleId) {
|
|
1977
|
+
resolvedWorktreeId = tupleId;
|
|
1978
|
+
} else {
|
|
1979
|
+
resolvedWorktreeId = await resolveAndCacheWorktreeId(repoId, projectPath) ?? void 0;
|
|
3277
1980
|
}
|
|
1981
|
+
} catch (err) {
|
|
1982
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1983
|
+
console.warn(
|
|
1984
|
+
` Warning: failed to cache worktree_id (self-heal skipped): ${msg}`
|
|
1985
|
+
);
|
|
3278
1986
|
}
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
if (!dryRun) {
|
|
3283
|
-
try {
|
|
3284
|
-
await apiDelete("/sync/lock", { repo_id: repoId });
|
|
3285
|
-
} catch (err) {
|
|
3286
|
-
console.error(
|
|
3287
|
-
` Warning: failed to release sync lock: ${err instanceof Error ? err.message : String(err)}`
|
|
3288
|
-
);
|
|
3289
|
-
}
|
|
3290
|
-
}
|
|
3291
|
-
}
|
|
3292
|
-
}
|
|
3293
|
-
async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
|
|
3294
|
-
console.log(" Reading local and remote state...");
|
|
3295
|
-
const claudeDir = join9(projectPath, ".claude");
|
|
3296
|
-
let localFiles = /* @__PURE__ */ new Map();
|
|
3297
|
-
try {
|
|
3298
|
-
localFiles = await scanLocalFiles(claudeDir, projectPath);
|
|
3299
|
-
} catch (err) {
|
|
3300
|
-
console.warn(
|
|
3301
|
-
` Local file scan incomplete: ${err instanceof Error ? err.message : String(err)}`
|
|
3302
|
-
);
|
|
3303
|
-
}
|
|
3304
|
-
const [defaultsRes, repoSyncRes, repoRes, , fileReposRes] = await Promise.all(
|
|
3305
|
-
[
|
|
3306
|
-
apiGet("/sync/defaults"),
|
|
3307
|
-
apiGet("/sync/files", { repo_id: repoId }),
|
|
3308
|
-
apiGet(`/repos/${repoId}`),
|
|
3309
|
-
apiGet("/sync/state", {
|
|
3310
|
-
repo_id: repoId
|
|
3311
|
-
}),
|
|
3312
|
-
apiGet("/sync/file-repos", {
|
|
3313
|
-
repo_id: repoId
|
|
3314
|
-
})
|
|
3315
|
-
]
|
|
3316
|
-
);
|
|
3317
|
-
const syncStartTime = Date.now();
|
|
3318
|
-
const repoData = repoRes.data;
|
|
3319
|
-
const remoteDefaults = flattenSyncData(defaultsRes.data);
|
|
3320
|
-
const remoteRepoFiles = flattenSyncData(repoSyncRes.data);
|
|
3321
|
-
const fileRepoHashes = /* @__PURE__ */ new Map();
|
|
3322
|
-
const fileRepoByClaudeFileId = /* @__PURE__ */ new Map();
|
|
3323
|
-
for (const entry of fileReposRes.data ?? []) {
|
|
3324
|
-
const baseKey = compositeKey(
|
|
3325
|
-
entry.file_type,
|
|
3326
|
-
entry.file_name,
|
|
3327
|
-
entry.file_category
|
|
3328
|
-
);
|
|
3329
|
-
const scopedKey = `${baseKey}:${entry.file_scope}`;
|
|
3330
|
-
fileRepoHashes.set(scopedKey, entry.last_synced_content_hash);
|
|
3331
|
-
if (!fileRepoHashes.has(baseKey)) {
|
|
3332
|
-
fileRepoHashes.set(baseKey, entry.last_synced_content_hash);
|
|
3333
|
-
}
|
|
3334
|
-
if (entry.claude_file_id) {
|
|
3335
|
-
fileRepoByClaudeFileId.set(
|
|
3336
|
-
entry.claude_file_id,
|
|
3337
|
-
entry.last_synced_content_hash
|
|
3338
|
-
);
|
|
3339
|
-
}
|
|
3340
|
-
}
|
|
3341
|
-
const remoteFiles = new Map([...remoteDefaults, ...remoteRepoFiles]);
|
|
3342
|
-
console.log(
|
|
3343
|
-
` Local: ${localFiles.size} files, Remote: ${remoteFiles.size} files
|
|
3344
|
-
`
|
|
3345
|
-
);
|
|
3346
|
-
const plan = [];
|
|
3347
|
-
const allKeys = /* @__PURE__ */ new Set([...localFiles.keys(), ...remoteFiles.keys()]);
|
|
3348
|
-
for (const key of allKeys) {
|
|
3349
|
-
const local = localFiles.get(key);
|
|
3350
|
-
const remote = remoteFiles.get(key);
|
|
3351
|
-
if (local && !remote) {
|
|
3352
|
-
plan.push({
|
|
3353
|
-
key,
|
|
3354
|
-
displayPath: `${local.type}/${local.category ? local.category + "/" : ""}${local.name}`,
|
|
3355
|
-
action: "push",
|
|
3356
|
-
recommended: "push",
|
|
3357
|
-
localContent: local.content,
|
|
3358
|
-
remoteContent: null,
|
|
3359
|
-
pushContent: reverseSubstituteVariables(local.content, repoData),
|
|
3360
|
-
filePath: getLocalFilePath(claudeDir, projectPath, {
|
|
3361
|
-
type: local.type,
|
|
3362
|
-
name: local.name,
|
|
3363
|
-
category: local.category
|
|
3364
|
-
}),
|
|
3365
|
-
type: local.type,
|
|
3366
|
-
name: local.name,
|
|
3367
|
-
category: local.category,
|
|
3368
|
-
scope: local.scope,
|
|
3369
|
-
isHook: local.type === "hook",
|
|
3370
|
-
claudeFileId: null
|
|
3371
|
-
});
|
|
3372
|
-
} else if (!local && remote) {
|
|
3373
|
-
const remoteScope = remote.scope ?? "shared";
|
|
3374
|
-
if (remoteScope.startsWith("local:") && remoteScope !== `local:${repoData.name}`) {
|
|
3375
|
-
continue;
|
|
3376
|
-
}
|
|
3377
|
-
const resolvedContent = substituteVariables(remote.content, repoData);
|
|
3378
|
-
const hadSyncedThisFile = remote.id ? fileRepoByClaudeFileId.has(remote.id) : fileRepoHashes.has(key);
|
|
3379
|
-
const recommended = hadSyncedThisFile ? "delete" : "pull";
|
|
3380
|
-
plan.push({
|
|
3381
|
-
key,
|
|
3382
|
-
displayPath: `${remote.type}/${remote.category ? remote.category + "/" : ""}${remote.name}`,
|
|
3383
|
-
action: recommended,
|
|
3384
|
-
recommended,
|
|
3385
|
-
localContent: null,
|
|
3386
|
-
remoteContent: resolvedContent,
|
|
3387
|
-
pushContent: null,
|
|
3388
|
-
filePath: getLocalFilePath(claudeDir, projectPath, remote),
|
|
3389
|
-
type: remote.type,
|
|
3390
|
-
name: remote.name,
|
|
3391
|
-
category: remote.category ?? null,
|
|
3392
|
-
scope: remote.scope ?? "shared",
|
|
3393
|
-
isHook: remote.type === "hook",
|
|
3394
|
-
claudeFileId: remote.id ?? null
|
|
3395
|
-
});
|
|
3396
|
-
} else if (local && remote) {
|
|
3397
|
-
const remoteScope = remote.scope ?? "shared";
|
|
3398
|
-
if (remoteScope.startsWith("local:") && remoteScope !== `local:${repoData.name}`) {
|
|
3399
|
-
continue;
|
|
3400
|
-
}
|
|
3401
|
-
const resolvedRemote = substituteVariables(remote.content, repoData);
|
|
3402
|
-
if (local.content === resolvedRemote) {
|
|
3403
|
-
continue;
|
|
3404
|
-
}
|
|
3405
|
-
const localHash = contentHash(local.content);
|
|
3406
|
-
const scopedKey = `${key}:${local.scope}`;
|
|
3407
|
-
const lastSyncedHash = fileRepoHashes.get(scopedKey) ?? fileRepoHashes.get(key) ?? null;
|
|
3408
|
-
const localChanged = lastSyncedHash ? localHash !== lastSyncedHash : true;
|
|
3409
|
-
let action;
|
|
3410
|
-
if (force) {
|
|
3411
|
-
action = "pull";
|
|
3412
|
-
} else if (!localChanged) {
|
|
3413
|
-
action = "pull";
|
|
3414
|
-
} else if (lastSyncedHash === null) {
|
|
3415
|
-
action = "conflict";
|
|
3416
|
-
} else {
|
|
3417
|
-
const remoteResolvedHash = contentHash(resolvedRemote);
|
|
3418
|
-
const remoteChanged = remoteResolvedHash !== lastSyncedHash;
|
|
3419
|
-
if (remoteChanged) {
|
|
3420
|
-
action = "conflict";
|
|
3421
|
-
} else {
|
|
3422
|
-
action = "push";
|
|
3423
|
-
}
|
|
3424
|
-
}
|
|
3425
|
-
plan.push({
|
|
3426
|
-
key,
|
|
3427
|
-
displayPath: `${local.type}/${local.category ? local.category + "/" : ""}${local.name}`,
|
|
3428
|
-
action,
|
|
3429
|
-
recommended: action === "conflict" ? "pull" : action,
|
|
3430
|
-
localContent: local.content,
|
|
3431
|
-
remoteContent: resolvedRemote,
|
|
3432
|
-
pushContent: reverseSubstituteVariables(local.content, repoData),
|
|
3433
|
-
filePath: getLocalFilePath(claudeDir, projectPath, remote),
|
|
3434
|
-
type: local.type,
|
|
3435
|
-
name: local.name,
|
|
3436
|
-
category: local.category,
|
|
3437
|
-
scope: local.scope,
|
|
3438
|
-
isHook: local.type === "hook",
|
|
3439
|
-
claudeFileId: remote.id ?? null
|
|
3440
|
-
});
|
|
3441
|
-
}
|
|
3442
|
-
}
|
|
3443
|
-
const pulls = plan.filter((p) => p.action === "pull");
|
|
3444
|
-
const pushes = plan.filter((p) => p.action === "push");
|
|
3445
|
-
const conflicts = plan.filter((p) => p.action === "conflict");
|
|
3446
|
-
const contentPulls = pulls.filter((p) => p.localContent !== null);
|
|
3447
|
-
const dbOnlyPull = plan.filter(
|
|
3448
|
-
(p) => p.localContent === null && p.action === "pull"
|
|
3449
|
-
);
|
|
3450
|
-
const dbOnlyDelete = plan.filter(
|
|
3451
|
-
(p) => p.localContent === null && p.action === "delete"
|
|
3452
|
-
);
|
|
3453
|
-
if (contentPulls.length > 0) {
|
|
3454
|
-
console.log(` Pull (DB \u2192 local): ${contentPulls.length}`);
|
|
3455
|
-
for (const p of contentPulls) console.log(` \u2193 ${p.displayPath}`);
|
|
3456
|
-
}
|
|
3457
|
-
if (pushes.length > 0) {
|
|
3458
|
-
console.log(` Push (local \u2192 DB): ${pushes.length}`);
|
|
3459
|
-
for (const p of pushes) console.log(` \u2191 ${p.displayPath}`);
|
|
3460
|
-
}
|
|
3461
|
-
if (dbOnlyPull.length > 0) {
|
|
3462
|
-
console.log(`
|
|
3463
|
-
DB-only (new, will pull): ${dbOnlyPull.length}`);
|
|
3464
|
-
for (const p of dbOnlyPull) console.log(` \u2193 ${p.displayPath}`);
|
|
3465
|
-
}
|
|
3466
|
-
if (dbOnlyDelete.length > 0) {
|
|
3467
|
-
console.log(
|
|
3468
|
-
`
|
|
3469
|
-
DB-only (previously synced, will delete): ${dbOnlyDelete.length}`
|
|
3470
|
-
);
|
|
3471
|
-
for (const p of dbOnlyDelete) console.log(` \u2715 ${p.displayPath}`);
|
|
3472
|
-
}
|
|
3473
|
-
if (conflicts.length > 0) {
|
|
3474
|
-
console.log(`
|
|
3475
|
-
Conflicts (both sides changed): ${conflicts.length}`);
|
|
3476
|
-
for (const p of conflicts) console.log(` \u26A0 ${p.displayPath}`);
|
|
3477
|
-
}
|
|
3478
|
-
if (contentPulls.length === 0 && pushes.length === 0 && dbOnlyPull.length === 0 && dbOnlyDelete.length === 0 && conflicts.length === 0) {
|
|
3479
|
-
console.log(" All .claude/ files in sync.");
|
|
3480
|
-
}
|
|
3481
|
-
if (plan.length > 0 && !dryRun) {
|
|
3482
|
-
if (!force) {
|
|
3483
|
-
const agreed = await confirmProceed(`
|
|
3484
|
-
Agree with sync? [Y/n] `);
|
|
3485
|
-
if (!agreed) {
|
|
3486
|
-
const mode = await promptReviewMode();
|
|
3487
|
-
const contentProvider = {
|
|
3488
|
-
local: (p) => p.localContent,
|
|
3489
|
-
remote: (p) => p.remoteContent
|
|
3490
|
-
};
|
|
3491
|
-
if (mode === "file") {
|
|
3492
|
-
const actions = await reviewFilesOneByOne(
|
|
3493
|
-
plan,
|
|
3494
|
-
(p) => p.displayPath,
|
|
3495
|
-
(p) => p.action,
|
|
3496
|
-
(p) => p.recommended,
|
|
3497
|
-
contentProvider
|
|
3498
|
-
);
|
|
3499
|
-
for (let i = 0; i < plan.length; i++) {
|
|
3500
|
-
plan[i].action = actions[i];
|
|
3501
|
-
}
|
|
3502
|
-
} else {
|
|
3503
|
-
const groups = groupByType(plan);
|
|
3504
|
-
for (const [typeName, items] of groups) {
|
|
3505
|
-
const actions = await reviewFolder(
|
|
3506
|
-
typeName,
|
|
3507
|
-
items,
|
|
3508
|
-
(p) => p.displayPath,
|
|
3509
|
-
(p) => p.action,
|
|
3510
|
-
(p) => p.recommended,
|
|
3511
|
-
contentProvider
|
|
3512
|
-
);
|
|
3513
|
-
for (let i = 0; i < items.length; i++) {
|
|
3514
|
-
items[i].action = actions[i];
|
|
3515
|
-
}
|
|
3516
|
-
}
|
|
3517
|
-
}
|
|
3518
|
-
}
|
|
3519
|
-
}
|
|
3520
|
-
const toPull = plan.filter((p) => p.action === "pull");
|
|
3521
|
-
const toPush = plan.filter((p) => p.action === "push");
|
|
3522
|
-
const toDelete = plan.filter((p) => p.action === "delete");
|
|
3523
|
-
const skipped = plan.filter((p) => p.action === "skip");
|
|
3524
|
-
if (toPull.length + toPush.length + toDelete.length === 0) {
|
|
3525
|
-
console.log("\n All items skipped \u2014 no changes applied.");
|
|
3526
|
-
} else {
|
|
3527
|
-
for (const p of toPull) {
|
|
3528
|
-
if (p.filePath && p.remoteContent !== null) {
|
|
3529
|
-
await mkdir2(dirname2(p.filePath), { recursive: true });
|
|
3530
|
-
await writeFile5(p.filePath, p.remoteContent, "utf-8");
|
|
3531
|
-
if (p.isHook) await chmod2(p.filePath, 493);
|
|
3532
|
-
}
|
|
3533
|
-
}
|
|
3534
|
-
const toUpsert = toPush.filter((p) => p.pushContent !== null).map((p) => ({
|
|
3535
|
-
type: p.type,
|
|
3536
|
-
name: p.name,
|
|
3537
|
-
category: p.category,
|
|
3538
|
-
content: p.pushContent,
|
|
3539
|
-
scope: p.scope
|
|
3540
|
-
}));
|
|
3541
|
-
if (toUpsert.length > 0) {
|
|
3542
|
-
await apiPost("/sync/files", {
|
|
3543
|
-
repo_id: repoId,
|
|
3544
|
-
files: toUpsert,
|
|
3545
|
-
changed_by_repo_id: repoId
|
|
3546
|
-
});
|
|
3547
|
-
}
|
|
3548
|
-
if (toDelete.length > 0) {
|
|
3549
|
-
const deleteKeys = toDelete.map((p) => ({
|
|
3550
|
-
type: p.type,
|
|
3551
|
-
name: p.name,
|
|
3552
|
-
category: p.category
|
|
3553
|
-
}));
|
|
3554
|
-
await apiPost("/sync/files", {
|
|
3555
|
-
repo_id: repoId,
|
|
3556
|
-
delete_keys: deleteKeys
|
|
3557
|
-
});
|
|
3558
|
-
for (const p of toDelete) {
|
|
3559
|
-
if (p.filePath) {
|
|
3560
|
-
try {
|
|
3561
|
-
await unlink2(p.filePath);
|
|
3562
|
-
} catch (err) {
|
|
3563
|
-
if (err instanceof Error && "code" in err && err.code !== "ENOENT") {
|
|
3564
|
-
console.error(
|
|
3565
|
-
` Warning: failed to delete ${p.filePath}: ${err.message}`
|
|
3566
|
-
);
|
|
3567
|
-
}
|
|
3568
|
-
}
|
|
3569
|
-
}
|
|
3570
|
-
}
|
|
3571
|
-
}
|
|
3572
|
-
const syncDurationMs = Date.now() - syncStartTime;
|
|
3573
|
-
await apiPost("/sync/state", {
|
|
3574
|
-
repo_id: repoId,
|
|
3575
|
-
last_synced_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3576
|
-
was_skipped: skipped.length > 0,
|
|
3577
|
-
files_synced_count: toPull.length + toPush.length + toDelete.length,
|
|
3578
|
-
files_pushed: toPush.length,
|
|
3579
|
-
files_pulled: toPull.length,
|
|
3580
|
-
files_deleted: toDelete.length,
|
|
3581
|
-
files_skipped: skipped.length,
|
|
3582
|
-
sync_duration_ms: syncDurationMs,
|
|
3583
|
-
sync_version: getSyncVersion()
|
|
3584
|
-
});
|
|
3585
|
-
const syncTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3586
|
-
const fileRepoUpdates = [];
|
|
3587
|
-
for (const p of toPull) {
|
|
3588
|
-
if (p.remoteContent !== null) {
|
|
3589
|
-
fileRepoUpdates.push({
|
|
3590
|
-
claude_file_id: p.claudeFileId ?? void 0,
|
|
3591
|
-
file_type: p.type,
|
|
3592
|
-
file_name: p.name,
|
|
3593
|
-
file_category: p.category,
|
|
3594
|
-
file_scope: p.scope,
|
|
3595
|
-
last_synced_at: syncTimestamp,
|
|
3596
|
-
last_synced_content_hash: contentHash(p.remoteContent),
|
|
3597
|
-
sync_status: "synced"
|
|
3598
|
-
});
|
|
3599
|
-
}
|
|
3600
|
-
}
|
|
3601
|
-
for (const p of toPush) {
|
|
3602
|
-
if (p.localContent !== null) {
|
|
3603
|
-
fileRepoUpdates.push({
|
|
3604
|
-
claude_file_id: p.claudeFileId ?? void 0,
|
|
3605
|
-
file_type: p.type,
|
|
3606
|
-
file_name: p.name,
|
|
3607
|
-
file_category: p.category,
|
|
3608
|
-
file_scope: p.scope,
|
|
3609
|
-
last_synced_at: syncTimestamp,
|
|
3610
|
-
last_synced_content_hash: contentHash(p.localContent),
|
|
3611
|
-
sync_status: "synced"
|
|
3612
|
-
});
|
|
3613
|
-
}
|
|
3614
|
-
}
|
|
3615
|
-
if (fileRepoUpdates.length > 0) {
|
|
3616
|
-
try {
|
|
3617
|
-
await apiPost("/sync/file-repos", {
|
|
3618
|
-
repo_id: repoId,
|
|
3619
|
-
file_repos: fileRepoUpdates
|
|
3620
|
-
});
|
|
3621
|
-
} catch (err) {
|
|
3622
|
-
console.warn(
|
|
3623
|
-
` Warning: failed to update file-repo tracking for ${fileRepoUpdates.length} files: ${err instanceof Error ? err.message : String(err)}`
|
|
3624
|
-
);
|
|
3625
|
-
}
|
|
3626
|
-
}
|
|
3627
|
-
console.log(
|
|
3628
|
-
`
|
|
3629
|
-
Applied: ${toPull.length} pulled, ${toPush.length} pushed, ${toDelete.length} deleted` + (skipped.length > 0 ? `, ${skipped.length} skipped` : "")
|
|
3630
|
-
);
|
|
3631
|
-
}
|
|
3632
|
-
const unresolvedConflicts = plan.filter(
|
|
3633
|
-
(p) => p.action === "conflict" || p.action === "skip" && p.localContent !== null && p.remoteContent !== null
|
|
3634
|
-
);
|
|
3635
|
-
if (unresolvedConflicts.length > 0) {
|
|
3636
|
-
let stored = 0;
|
|
3637
|
-
for (const p of unresolvedConflicts) {
|
|
3638
|
-
try {
|
|
3639
|
-
await apiPost("/sync/conflicts", {
|
|
3640
|
-
repo_id: repoId,
|
|
3641
|
-
claude_file_id: p.claudeFileId ?? void 0,
|
|
3642
|
-
file_type: p.type,
|
|
3643
|
-
file_name: p.name,
|
|
3644
|
-
file_category: p.category,
|
|
3645
|
-
file_scope: p.scope,
|
|
3646
|
-
conflict_type: "both_modified",
|
|
3647
|
-
local_content: p.localContent,
|
|
3648
|
-
remote_content: p.remoteContent
|
|
3649
|
-
});
|
|
3650
|
-
stored++;
|
|
3651
|
-
} catch (err) {
|
|
3652
|
-
console.error(`Failed to store conflict for ${p.displayPath}:`, err);
|
|
3653
|
-
}
|
|
3654
|
-
}
|
|
3655
|
-
if (stored > 0) {
|
|
3656
|
-
console.log(
|
|
3657
|
-
`
|
|
3658
|
-
${stored} conflict(s) stored in DB for later resolution.`
|
|
3659
|
-
);
|
|
3660
|
-
}
|
|
3661
|
-
}
|
|
3662
|
-
} else if (dryRun) {
|
|
3663
|
-
console.log("\n (dry-run \u2014 no changes)");
|
|
3664
|
-
}
|
|
3665
|
-
console.log("\n Settings sync...");
|
|
3666
|
-
await syncSettings(
|
|
3667
|
-
claudeDir,
|
|
3668
|
-
projectPath,
|
|
3669
|
-
defaultsRes.data,
|
|
3670
|
-
repoData,
|
|
3671
|
-
dryRun
|
|
3672
|
-
);
|
|
3673
|
-
console.log(" Config sync...");
|
|
3674
|
-
await syncConfig(repoId, projectPath, dryRun);
|
|
3675
|
-
console.log(" Tech stack...");
|
|
3676
|
-
await syncTechStack(repoId, projectPath, dryRun);
|
|
3677
|
-
console.log(" ESLint config...");
|
|
3678
|
-
await syncEslintDriftCheck(repoId, projectPath);
|
|
3679
|
-
console.log(" Port verification...");
|
|
3680
|
-
await syncPortVerification(repoId, projectPath, dryRun, fix);
|
|
3681
|
-
console.log("\n Sync complete.\n");
|
|
3682
|
-
}
|
|
3683
|
-
async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun) {
|
|
3684
|
-
const settingsPath = join9(claudeDir, "settings.json");
|
|
3685
|
-
const globalSettingsFiles = syncData.global_settings ?? [];
|
|
3686
|
-
let globalSettings = {};
|
|
3687
|
-
for (const gf of globalSettingsFiles) {
|
|
3688
|
-
const parsed = JSON.parse(
|
|
3689
|
-
substituteVariables(gf.content, repoData)
|
|
3690
|
-
);
|
|
3691
|
-
globalSettings = { ...globalSettings, ...parsed };
|
|
3692
|
-
}
|
|
3693
|
-
const repoSettingsFiles = syncData.settings ?? [];
|
|
3694
|
-
let repoSettings = {};
|
|
3695
|
-
for (const rf of repoSettingsFiles) {
|
|
3696
|
-
repoSettings = JSON.parse(
|
|
3697
|
-
substituteVariables(rf.content, repoData)
|
|
3698
|
-
);
|
|
3699
|
-
}
|
|
3700
|
-
const combinedTemplate = mergeGlobalAndRepoSettings(
|
|
3701
|
-
globalSettings,
|
|
3702
|
-
repoSettings
|
|
3703
|
-
);
|
|
3704
|
-
const hooksDir = join9(projectPath, ".claude", "hooks");
|
|
3705
|
-
const discovered = await discoverHooks(hooksDir);
|
|
3706
|
-
let localSettings = {};
|
|
3707
|
-
try {
|
|
3708
|
-
const raw = await readFile10(settingsPath, "utf-8");
|
|
3709
|
-
localSettings = JSON.parse(raw);
|
|
3710
|
-
} catch {
|
|
3711
|
-
}
|
|
3712
|
-
let merged = Object.keys(localSettings).length > 0 ? mergeSettings(combinedTemplate, localSettings) : combinedTemplate;
|
|
3713
|
-
merged = stripPermissionsAllow(merged);
|
|
3714
|
-
if (discovered.size > 0) {
|
|
3715
|
-
merged.hooks = mergeDiscoveredHooks(
|
|
3716
|
-
merged.hooks ?? {},
|
|
3717
|
-
discovered
|
|
3718
|
-
);
|
|
3719
|
-
}
|
|
3720
|
-
const mergedContent = JSON.stringify(merged, null, 2) + "\n";
|
|
3721
|
-
let currentContent = "";
|
|
3722
|
-
try {
|
|
3723
|
-
currentContent = await readFile10(settingsPath, "utf-8");
|
|
3724
|
-
} catch {
|
|
3725
|
-
}
|
|
3726
|
-
if (currentContent === mergedContent) {
|
|
3727
|
-
console.log(" Settings up to date.");
|
|
3728
|
-
return;
|
|
3729
|
-
}
|
|
3730
|
-
if (dryRun) {
|
|
3731
|
-
console.log(" Settings would be updated (dry-run).");
|
|
3732
|
-
return;
|
|
3733
|
-
}
|
|
3734
|
-
await mkdir2(dirname2(settingsPath), { recursive: true });
|
|
3735
|
-
await writeFile5(settingsPath, mergedContent, "utf-8");
|
|
3736
|
-
console.log(" Updated settings.json");
|
|
3737
|
-
}
|
|
3738
|
-
async function syncConfig(repoId, projectPath, dryRun) {
|
|
3739
|
-
const configPath = join9(projectPath, ".codebyplan.json");
|
|
3740
|
-
let currentConfig = {};
|
|
3741
|
-
try {
|
|
3742
|
-
const raw = await readFile10(configPath, "utf-8");
|
|
3743
|
-
currentConfig = JSON.parse(raw);
|
|
3744
|
-
} catch {
|
|
3745
|
-
currentConfig = { repo_id: repoId };
|
|
3746
|
-
}
|
|
3747
|
-
let resolvedWorktreeId;
|
|
3748
|
-
try {
|
|
3749
|
-
resolvedWorktreeId = await resolveAndCacheWorktreeId(repoId, projectPath);
|
|
3750
|
-
} catch (err) {
|
|
3751
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
3752
|
-
console.warn(
|
|
3753
|
-
` Warning: failed to cache worktree_id (self-heal skipped): ${msg}`
|
|
3754
|
-
);
|
|
3755
|
-
}
|
|
3756
|
-
if (resolvedWorktreeId && currentConfig.worktree_id !== resolvedWorktreeId) {
|
|
3757
|
-
currentConfig = { ...currentConfig, worktree_id: resolvedWorktreeId };
|
|
3758
|
-
}
|
|
3759
|
-
const repoRes = await apiGet(`/repos/${repoId}`);
|
|
3760
|
-
const repo = repoRes.data;
|
|
3761
|
-
let portAllocations = [];
|
|
1987
|
+
const repoRes = await apiGet(`/repos/${repoId}`);
|
|
1988
|
+
const repo = repoRes.data;
|
|
1989
|
+
let portAllocations = [];
|
|
3762
1990
|
try {
|
|
3763
1991
|
const portsRes = await apiGet(
|
|
3764
1992
|
`/port-allocations`,
|
|
3765
1993
|
{ repo_id: repoId }
|
|
3766
1994
|
);
|
|
3767
1995
|
const allAllocations = portsRes.data ?? [];
|
|
3768
|
-
const
|
|
3769
|
-
const filtered =
|
|
1996
|
+
const filteredByWorktree = resolvedWorktreeId;
|
|
1997
|
+
const filtered = filteredByWorktree ? allAllocations.filter((a) => a.worktree_id === filteredByWorktree) : allAllocations.filter((a) => !a.worktree_id);
|
|
3770
1998
|
const ALLOWED_FIELDS = [
|
|
3771
1999
|
"id",
|
|
3772
2000
|
"repo_id",
|
|
@@ -3794,7 +2022,7 @@ async function syncConfig(repoId, projectPath, dryRun) {
|
|
|
3794
2022
|
` Warning: failed to fetch port allocations: ${err instanceof Error ? err.message : String(err)}`
|
|
3795
2023
|
);
|
|
3796
2024
|
}
|
|
3797
|
-
const worktreeId =
|
|
2025
|
+
const worktreeId = resolvedWorktreeId;
|
|
3798
2026
|
const matchingAlloc = portAllocations[0];
|
|
3799
2027
|
const defaultBranchConfig = {
|
|
3800
2028
|
protected: ["main", "development"],
|
|
@@ -3805,7 +2033,9 @@ async function syncConfig(repoId, projectPath, dryRun) {
|
|
|
3805
2033
|
const branchConfig = repo.branch_config ?? defaultBranchConfig;
|
|
3806
2034
|
const newConfig = {
|
|
3807
2035
|
repo_id: repoId,
|
|
3808
|
-
|
|
2036
|
+
// worktree_id is intentionally omitted — it is never persisted in
|
|
2037
|
+
// .codebyplan.json (CHK-108). The in-memory worktreeId is used only
|
|
2038
|
+
// for server_port / server_type resolution immediately below.
|
|
3809
2039
|
server_port: worktreeId && matchingAlloc ? matchingAlloc.port : repo.server_port,
|
|
3810
2040
|
server_type: worktreeId && matchingAlloc ? matchingAlloc.server_type : repo.server_type,
|
|
3811
2041
|
git_branch: repo.git_branch ?? "development",
|
|
@@ -3816,68 +2046,194 @@ async function syncConfig(repoId, projectPath, dryRun) {
|
|
|
3816
2046
|
const currentJson = JSON.stringify(currentConfig, null, 2);
|
|
3817
2047
|
const newJson = JSON.stringify(newConfig, null, 2);
|
|
3818
2048
|
if (currentJson === newJson) {
|
|
3819
|
-
console.log("
|
|
2049
|
+
console.log(" Config up to date.");
|
|
3820
2050
|
return;
|
|
3821
2051
|
}
|
|
3822
2052
|
if (dryRun) {
|
|
3823
|
-
console.log("
|
|
2053
|
+
console.log(" Config would be updated (dry-run).");
|
|
3824
2054
|
return;
|
|
3825
2055
|
}
|
|
3826
2056
|
await writeFile5(configPath, newJson + "\n", "utf-8");
|
|
3827
|
-
console.log("
|
|
2057
|
+
console.log(" Updated .codebyplan.json");
|
|
3828
2058
|
}
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
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;
|
|
3846
2092
|
}
|
|
3847
2093
|
}
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
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)
|
|
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
|
+
});
|
|
3858
2125
|
}
|
|
2126
|
+
} catch {
|
|
3859
2127
|
}
|
|
3860
|
-
} catch (err) {
|
|
3861
|
-
console.warn(
|
|
3862
|
-
` Tech stack detection skipped: ${err instanceof Error ? err.message : String(err)}`
|
|
3863
|
-
);
|
|
3864
2128
|
}
|
|
2129
|
+
return mismatches;
|
|
3865
2130
|
}
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
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;
|
|
3875
2171
|
}
|
|
3876
|
-
|
|
3877
|
-
|
|
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;
|
|
2178
|
+
}
|
|
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
|
+
});
|
|
3878
2190
|
}
|
|
2191
|
+
return unallocated;
|
|
3879
2192
|
}
|
|
3880
|
-
|
|
2193
|
+
function getAppLabel(relativePath) {
|
|
2194
|
+
const parts = relativePath.split("/");
|
|
2195
|
+
if (parts.length >= 3 && parts[0] === "apps") {
|
|
2196
|
+
return parts[1];
|
|
2197
|
+
}
|
|
2198
|
+
return "root";
|
|
2199
|
+
}
|
|
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();
|
|
3881
2237
|
try {
|
|
3882
2238
|
const portsRes = await apiGet(
|
|
3883
2239
|
`/port-allocations`,
|
|
@@ -3885,22 +2241,23 @@ async function syncPortVerification(repoId, projectPath, dryRun, fix) {
|
|
|
3885
2241
|
);
|
|
3886
2242
|
const allocations = portsRes.data ?? [];
|
|
3887
2243
|
if (allocations.length === 0) {
|
|
3888
|
-
console.log("
|
|
2244
|
+
console.log(" No port allocations found \u2014 skipping verification.");
|
|
2245
|
+
console.log("\n Ports complete.\n");
|
|
3889
2246
|
return;
|
|
3890
2247
|
}
|
|
3891
2248
|
const mismatches = await verifyPorts(projectPath, allocations);
|
|
3892
2249
|
if (mismatches.length > 0) {
|
|
3893
|
-
console.log(`
|
|
2250
|
+
console.log(` Port mismatches: ${mismatches.length}`);
|
|
3894
2251
|
for (const m of mismatches) {
|
|
3895
|
-
console.log(`
|
|
2252
|
+
console.log(` ! ${m.packageJsonPath}: ${m.reason}`);
|
|
3896
2253
|
}
|
|
3897
2254
|
}
|
|
3898
2255
|
const unallocated = await findUnallocatedApps(projectPath, allocations);
|
|
3899
2256
|
if (unallocated.length > 0) {
|
|
3900
|
-
console.log(`
|
|
2257
|
+
console.log(` Unallocated apps: ${unallocated.length}`);
|
|
3901
2258
|
for (const app of unallocated) {
|
|
3902
2259
|
console.log(
|
|
3903
|
-
`
|
|
2260
|
+
` + ${app.name} (${app.framework}${app.detectedPort ? `, port ${app.detectedPort}` : ""})`
|
|
3904
2261
|
);
|
|
3905
2262
|
}
|
|
3906
2263
|
if (fix && !dryRun) {
|
|
@@ -3918,11 +2275,11 @@ async function syncPortVerification(repoId, projectPath, dryRun, fix) {
|
|
|
3918
2275
|
command: app.command,
|
|
3919
2276
|
working_dir: app.path
|
|
3920
2277
|
});
|
|
3921
|
-
console.log(`
|
|
2278
|
+
console.log(` Created allocation: ${app.name} \u2192 port ${port}`);
|
|
3922
2279
|
} catch (err) {
|
|
3923
2280
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3924
2281
|
console.log(
|
|
3925
|
-
`
|
|
2282
|
+
` Failed to create allocation for ${app.name}: ${msg}`
|
|
3926
2283
|
);
|
|
3927
2284
|
}
|
|
3928
2285
|
if (app.detectedPort && app.detectedPort >= nextPort) {
|
|
@@ -3930,123 +2287,180 @@ async function syncPortVerification(repoId, projectPath, dryRun, fix) {
|
|
|
3930
2287
|
}
|
|
3931
2288
|
}
|
|
3932
2289
|
} else if (fix && dryRun) {
|
|
3933
|
-
console.log("
|
|
2290
|
+
console.log(" (dry-run \u2014 would create allocations with --fix)");
|
|
3934
2291
|
} else {
|
|
3935
|
-
console.log("
|
|
2292
|
+
console.log(" Run with --fix to auto-create allocations.");
|
|
3936
2293
|
}
|
|
3937
2294
|
}
|
|
3938
2295
|
if (mismatches.length === 0 && unallocated.length === 0) {
|
|
3939
|
-
console.log("
|
|
2296
|
+
console.log(" Ports verified.");
|
|
3940
2297
|
}
|
|
3941
2298
|
} catch (err) {
|
|
3942
2299
|
console.warn(
|
|
3943
|
-
`
|
|
2300
|
+
` Port verification skipped: ${err instanceof Error ? err.message : String(err)}`
|
|
3944
2301
|
);
|
|
3945
2302
|
}
|
|
2303
|
+
console.log("\n Ports complete.\n");
|
|
3946
2304
|
}
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
rule: "Rules",
|
|
3954
|
-
hook: "Hooks",
|
|
3955
|
-
template: "Templates",
|
|
3956
|
-
settings: "Settings",
|
|
3957
|
-
context: "Context",
|
|
3958
|
-
docs_stack: "Stack Docs",
|
|
3959
|
-
docs: "Docs"
|
|
3960
|
-
};
|
|
3961
|
-
for (const item of items) {
|
|
3962
|
-
const label = typeLabels[item.type] ?? item.type;
|
|
3963
|
-
const group = groups.get(label) ?? [];
|
|
3964
|
-
group.push(item);
|
|
3965
|
-
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();
|
|
3966
2311
|
}
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
rule: { dir: "rules", ext: ".md" },
|
|
3975
|
-
hook: { dir: "hooks", ext: ".sh" },
|
|
3976
|
-
template: { dir: "templates", ext: "" },
|
|
3977
|
-
context: { dir: "context", ext: ".md" },
|
|
3978
|
-
docs_stack: { dir: join9("docs", "stack"), ext: ".md" },
|
|
3979
|
-
docs: { dir: "docs", ext: ".md" },
|
|
3980
|
-
claude_md: { dir: "", ext: "" },
|
|
3981
|
-
settings: { dir: "", ext: "" }
|
|
3982
|
-
};
|
|
3983
|
-
if (remote.type === "claude_md") return join9(projectPath, "CLAUDE.md");
|
|
3984
|
-
if (remote.type === "settings") return join9(claudeDir, "settings.json");
|
|
3985
|
-
const cfg = typeConfig2[remote.type];
|
|
3986
|
-
if (!cfg) return join9(claudeDir, remote.name);
|
|
3987
|
-
const typeDir = remote.type === "command" ? join9(claudeDir, cfg.dir, "cbp") : join9(claudeDir, cfg.dir);
|
|
3988
|
-
if (cfg.subfolder)
|
|
3989
|
-
return join9(typeDir, remote.name, `${cfg.subfolder}${cfg.ext}`);
|
|
3990
|
-
if (remote.type === "command" && remote.category)
|
|
3991
|
-
return join9(typeDir, remote.category, `${remote.name}${cfg.ext}`);
|
|
3992
|
-
if (remote.type === "template") return join9(typeDir, remote.name);
|
|
3993
|
-
if (remote.category && (remote.type === "context" || remote.type === "docs_stack" || remote.type === "docs"))
|
|
3994
|
-
return join9(typeDir, remote.category, `${remote.name}${cfg.ext}`);
|
|
3995
|
-
return join9(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");
|
|
3996
2319
|
}
|
|
3997
|
-
function
|
|
2320
|
+
async function needsLocalMigration(projectPath) {
|
|
3998
2321
|
try {
|
|
3999
|
-
|
|
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;
|
|
4000
2336
|
} catch {
|
|
4001
|
-
return
|
|
2337
|
+
return false;
|
|
4002
2338
|
}
|
|
4003
2339
|
}
|
|
4004
|
-
function
|
|
4005
|
-
const
|
|
4006
|
-
const
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
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
|
+
);
|
|
2347
|
+
}
|
|
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
|
|
4017
2367
|
};
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
2368
|
+
}
|
|
2369
|
+
var init_migrate_local_config = __esm({
|
|
2370
|
+
"src/lib/migrate-local-config.ts"() {
|
|
2371
|
+
"use strict";
|
|
2372
|
+
init_local_config();
|
|
2373
|
+
}
|
|
2374
|
+
});
|
|
2375
|
+
|
|
2376
|
+
// src/cli/tech-stack.ts
|
|
2377
|
+
var tech_stack_exports = {};
|
|
2378
|
+
__export(tech_stack_exports, {
|
|
2379
|
+
runTechStack: () => runTechStack
|
|
2380
|
+
});
|
|
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) {
|
|
2394
|
+
try {
|
|
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
|
+
}
|
|
2400
|
+
} catch {
|
|
2401
|
+
}
|
|
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
|
+
);
|
|
2417
|
+
}
|
|
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;
|
|
2425
|
+
}
|
|
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
|
+
}
|
|
4032
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
|
+
);
|
|
4033
2454
|
}
|
|
4034
|
-
|
|
2455
|
+
console.log("\n Tech stack complete.\n");
|
|
4035
2456
|
}
|
|
4036
|
-
var
|
|
4037
|
-
"src/cli/
|
|
2457
|
+
var init_tech_stack = __esm({
|
|
2458
|
+
"src/cli/tech-stack.ts"() {
|
|
4038
2459
|
"use strict";
|
|
4039
|
-
|
|
4040
|
-
init_fileMapper();
|
|
4041
|
-
init_confirm();
|
|
2460
|
+
init_flags();
|
|
4042
2461
|
init_api();
|
|
4043
|
-
init_variables();
|
|
4044
2462
|
init_tech_detect();
|
|
4045
|
-
|
|
4046
|
-
init_hook_registry();
|
|
4047
|
-
init_port_verify();
|
|
4048
|
-
init_resolve_worktree();
|
|
4049
|
-
init_eslint();
|
|
2463
|
+
init_migrate_local_config();
|
|
4050
2464
|
}
|
|
4051
2465
|
});
|
|
4052
2466
|
|
|
@@ -4086,20 +2500,6 @@ void (async () => {
|
|
|
4086
2500
|
await runSetup2();
|
|
4087
2501
|
process.exit(0);
|
|
4088
2502
|
}
|
|
4089
|
-
if (arg === "sync") {
|
|
4090
|
-
const { runSync: runSync2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
4091
|
-
const { SyncCancelledError: SyncCancelledError2 } = await Promise.resolve().then(() => (init_confirm(), confirm_exports));
|
|
4092
|
-
try {
|
|
4093
|
-
await runSync2();
|
|
4094
|
-
} catch (err) {
|
|
4095
|
-
if (err instanceof SyncCancelledError2) {
|
|
4096
|
-
console.log("\n Sync cancelled.\n");
|
|
4097
|
-
process.exit(0);
|
|
4098
|
-
}
|
|
4099
|
-
throw err;
|
|
4100
|
-
}
|
|
4101
|
-
process.exit(0);
|
|
4102
|
-
}
|
|
4103
2503
|
if (arg === "eslint") {
|
|
4104
2504
|
const { runEslint: runEslint2 } = await Promise.resolve().then(() => (init_eslint(), eslint_exports));
|
|
4105
2505
|
const { SyncCancelledError: SyncCancelledError2 } = await Promise.resolve().then(() => (init_confirm(), confirm_exports));
|
|
@@ -4114,27 +2514,58 @@ void (async () => {
|
|
|
4114
2514
|
}
|
|
4115
2515
|
process.exit(0);
|
|
4116
2516
|
}
|
|
2517
|
+
if (arg === "resolve-worktree") {
|
|
2518
|
+
const { runResolveWorktree: runResolveWorktree2 } = await Promise.resolve().then(() => (init_resolve_worktree2(), resolve_worktree_exports));
|
|
2519
|
+
await runResolveWorktree2();
|
|
2520
|
+
process.exit(0);
|
|
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
|
+
}
|
|
4117
2537
|
if (arg === "help" || arg === "--help" || arg === "-h" || arg === void 0) {
|
|
4118
2538
|
console.log(`
|
|
4119
2539
|
CodeByPlan CLI v${VERSION}
|
|
4120
2540
|
|
|
4121
2541
|
Usage:
|
|
4122
|
-
codebyplan setup
|
|
4123
|
-
codebyplan
|
|
4124
|
-
codebyplan
|
|
4125
|
-
codebyplan
|
|
4126
|
-
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)
|
|
2547
|
+
codebyplan resolve-worktree Resolve active worktree UUID from device+path+branch tuple
|
|
2548
|
+
codebyplan help Show this help message
|
|
2549
|
+
codebyplan --version Print version
|
|
4127
2550
|
|
|
4128
|
-
|
|
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:
|
|
4129
2557
|
--path <dir> Project root directory (default: cwd)
|
|
4130
2558
|
--repo-id <uuid> Repository ID (or set via .codebyplan.json)
|
|
4131
2559
|
--dry-run Preview changes without writing
|
|
4132
|
-
--force Skip confirmation and conflict prompts
|
|
4133
2560
|
--fix Auto-create missing port allocations
|
|
4134
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
|
+
|
|
4135
2567
|
ESLint commands:
|
|
4136
2568
|
codebyplan eslint init Detect tech stack, resolve presets, generate configs
|
|
4137
|
-
codebyplan eslint sync Regenerate if presets changed, detect drift
|
|
4138
2569
|
|
|
4139
2570
|
MCP Server:
|
|
4140
2571
|
Claude Code connects to CodeByPlan via remote MCP:
|