codebyplan 0.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -11
- package/dist/cli.js +2830 -0
- package/package.json +42 -7
- package/index.js +0 -17
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2830 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/lib/version.ts
|
|
13
|
+
var VERSION, PACKAGE_NAME;
|
|
14
|
+
var init_version = __esm({
|
|
15
|
+
"src/lib/version.ts"() {
|
|
16
|
+
"use strict";
|
|
17
|
+
VERSION = "1.1.0";
|
|
18
|
+
PACKAGE_NAME = "codebyplan";
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// src/lib/api.ts
|
|
23
|
+
function validateApiKey() {
|
|
24
|
+
if (!API_KEY) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Missing CODEBYPLAN_API_KEY environment variable.
|
|
27
|
+
|
|
28
|
+
Quick setup:
|
|
29
|
+
npx ${PACKAGE_NAME} setup
|
|
30
|
+
|
|
31
|
+
Or manually:
|
|
32
|
+
1. Get your API key at https://codebyplan.com/settings/api-keys/
|
|
33
|
+
2. claude mcp add codebyplan -e CODEBYPLAN_API_KEY=<key> -- npx -y ${PACKAGE_NAME}`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function buildUrl(path, params) {
|
|
38
|
+
const url = new URL(`${BASE_URL}/api${path}`);
|
|
39
|
+
if (params) {
|
|
40
|
+
for (const [key, value] of Object.entries(params)) {
|
|
41
|
+
if (value !== void 0) {
|
|
42
|
+
url.searchParams.set(key, value);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return url.toString();
|
|
47
|
+
}
|
|
48
|
+
function isRetryable(err) {
|
|
49
|
+
if (err instanceof TypeError) return true;
|
|
50
|
+
if (err instanceof Error && err.name === "AbortError") return false;
|
|
51
|
+
if (err instanceof ApiError) return err.status >= 500;
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
function delay(ms) {
|
|
55
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
56
|
+
}
|
|
57
|
+
async function request(method, path, options) {
|
|
58
|
+
const url = buildUrl(path, options?.params);
|
|
59
|
+
let lastError;
|
|
60
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
61
|
+
if (attempt > 0) {
|
|
62
|
+
const jitter = Math.random() * 0.5 + 0.75;
|
|
63
|
+
const backoff = BASE_DELAY_MS * Math.pow(2, attempt - 1) * jitter;
|
|
64
|
+
await delay(backoff);
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const res = await fetch(url, {
|
|
68
|
+
method,
|
|
69
|
+
headers: {
|
|
70
|
+
"x-api-key": API_KEY,
|
|
71
|
+
...options?.body !== void 0 ? { "Content-Type": "application/json" } : {}
|
|
72
|
+
},
|
|
73
|
+
body: options?.body !== void 0 ? JSON.stringify(options.body) : void 0,
|
|
74
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
75
|
+
});
|
|
76
|
+
if (!res.ok) {
|
|
77
|
+
let message = `API ${method} ${path} failed with status ${res.status}`;
|
|
78
|
+
let code;
|
|
79
|
+
try {
|
|
80
|
+
const body = await res.json();
|
|
81
|
+
if (body.error && typeof body.error === "object") {
|
|
82
|
+
const err = body.error;
|
|
83
|
+
if (err.message) message = err.message;
|
|
84
|
+
if (err.code) code = err.code;
|
|
85
|
+
} else if (typeof body.error === "string") {
|
|
86
|
+
message = body.error;
|
|
87
|
+
}
|
|
88
|
+
if (body.code && typeof body.code === "string") code = body.code;
|
|
89
|
+
} catch {
|
|
90
|
+
}
|
|
91
|
+
const apiError = new ApiError(message, res.status, code);
|
|
92
|
+
if (isRetryable(apiError) && attempt < MAX_RETRIES) {
|
|
93
|
+
lastError = apiError;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
throw apiError;
|
|
97
|
+
}
|
|
98
|
+
if (res.status === 204) {
|
|
99
|
+
return void 0;
|
|
100
|
+
}
|
|
101
|
+
return res.json();
|
|
102
|
+
} catch (err) {
|
|
103
|
+
lastError = err;
|
|
104
|
+
if (!isRetryable(err) || attempt >= MAX_RETRIES) {
|
|
105
|
+
throw err;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
throw lastError;
|
|
110
|
+
}
|
|
111
|
+
async function apiGet(path, params) {
|
|
112
|
+
return request("GET", path, { params });
|
|
113
|
+
}
|
|
114
|
+
async function apiPost(path, body) {
|
|
115
|
+
return request("POST", path, { body });
|
|
116
|
+
}
|
|
117
|
+
async function apiPut(path, body) {
|
|
118
|
+
return request("PUT", path, { body });
|
|
119
|
+
}
|
|
120
|
+
async function apiDelete(path, params) {
|
|
121
|
+
await request("DELETE", path, { params });
|
|
122
|
+
}
|
|
123
|
+
var API_KEY, BASE_URL, REQUEST_TIMEOUT_MS, MAX_RETRIES, BASE_DELAY_MS, ApiError;
|
|
124
|
+
var init_api = __esm({
|
|
125
|
+
"src/lib/api.ts"() {
|
|
126
|
+
"use strict";
|
|
127
|
+
init_version();
|
|
128
|
+
API_KEY = process.env.CODEBYPLAN_API_KEY ?? "";
|
|
129
|
+
BASE_URL = (process.env.CODEBYPLAN_API_URL ?? "https://codebyplan.com").replace(/\/$/, "");
|
|
130
|
+
REQUEST_TIMEOUT_MS = 12e4;
|
|
131
|
+
MAX_RETRIES = 3;
|
|
132
|
+
BASE_DELAY_MS = 1e3;
|
|
133
|
+
ApiError = class extends Error {
|
|
134
|
+
status;
|
|
135
|
+
code;
|
|
136
|
+
constructor(message, status, code) {
|
|
137
|
+
super(message);
|
|
138
|
+
this.name = "ApiError";
|
|
139
|
+
this.status = status;
|
|
140
|
+
this.code = code;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// src/lib/settings-merge.ts
|
|
147
|
+
function mergeSettings(template, local) {
|
|
148
|
+
const merged = { ...local };
|
|
149
|
+
for (const key of TEMPLATE_MANAGED_KEYS) {
|
|
150
|
+
if (key in template) {
|
|
151
|
+
merged[key] = template[key];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (template.permissions && typeof template.permissions === "object") {
|
|
155
|
+
const templatePerms = template.permissions;
|
|
156
|
+
const localPerms = local.permissions && typeof local.permissions === "object" ? local.permissions : {};
|
|
157
|
+
const mergedPerms = { ...localPerms };
|
|
158
|
+
for (const key of TEMPLATE_MANAGED_PERMISSION_KEYS) {
|
|
159
|
+
if (key in templatePerms) {
|
|
160
|
+
mergedPerms[key] = templatePerms[key];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
merged.permissions = mergedPerms;
|
|
164
|
+
}
|
|
165
|
+
return merged;
|
|
166
|
+
}
|
|
167
|
+
function mergeGlobalAndRepoSettings(global, repo) {
|
|
168
|
+
const merged = { ...global, ...repo };
|
|
169
|
+
const globalPerms = global.permissions && typeof global.permissions === "object" ? global.permissions : {};
|
|
170
|
+
const repoPerms = repo.permissions && typeof repo.permissions === "object" ? repo.permissions : {};
|
|
171
|
+
if (Object.keys(globalPerms).length > 0 || Object.keys(repoPerms).length > 0) {
|
|
172
|
+
const mergedPerms = { ...globalPerms, ...repoPerms };
|
|
173
|
+
for (const key of ARRAY_PERMISSION_KEYS) {
|
|
174
|
+
const globalArr = Array.isArray(globalPerms[key]) ? globalPerms[key] : [];
|
|
175
|
+
const repoArr = Array.isArray(repoPerms[key]) ? repoPerms[key] : [];
|
|
176
|
+
if (globalArr.length > 0 || repoArr.length > 0) {
|
|
177
|
+
mergedPerms[key] = [.../* @__PURE__ */ new Set([...globalArr, ...repoArr])];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
merged.permissions = mergedPerms;
|
|
181
|
+
}
|
|
182
|
+
return merged;
|
|
183
|
+
}
|
|
184
|
+
function stripPermissionsAllow(settings) {
|
|
185
|
+
if (!settings.permissions || typeof settings.permissions !== "object") {
|
|
186
|
+
return settings;
|
|
187
|
+
}
|
|
188
|
+
const perms = { ...settings.permissions };
|
|
189
|
+
delete perms.allow;
|
|
190
|
+
if (Object.keys(perms).length === 0) {
|
|
191
|
+
const { permissions: _, ...rest } = settings;
|
|
192
|
+
return rest;
|
|
193
|
+
}
|
|
194
|
+
return { ...settings, permissions: perms };
|
|
195
|
+
}
|
|
196
|
+
var TEMPLATE_MANAGED_KEYS, TEMPLATE_MANAGED_PERMISSION_KEYS, ARRAY_PERMISSION_KEYS;
|
|
197
|
+
var init_settings_merge = __esm({
|
|
198
|
+
"src/lib/settings-merge.ts"() {
|
|
199
|
+
"use strict";
|
|
200
|
+
TEMPLATE_MANAGED_KEYS = ["attribution", "hooks", "statusLine"];
|
|
201
|
+
TEMPLATE_MANAGED_PERMISSION_KEYS = [
|
|
202
|
+
"deny",
|
|
203
|
+
"ask",
|
|
204
|
+
"additionalDirectories"
|
|
205
|
+
];
|
|
206
|
+
ARRAY_PERMISSION_KEYS = ["deny", "ask"];
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// src/lib/hook-registry.ts
|
|
211
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
212
|
+
import { join } from "node:path";
|
|
213
|
+
function parseHookMeta(content) {
|
|
214
|
+
const match = content.match(/^#\s*@hook:\s*(\S+)(?:\s+(.+))?$/m);
|
|
215
|
+
if (!match) return null;
|
|
216
|
+
return {
|
|
217
|
+
event: match[1],
|
|
218
|
+
matcher: match[2]?.trim() ?? ""
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
async function discoverHooks(hooksDir) {
|
|
222
|
+
const discovered = /* @__PURE__ */ new Map();
|
|
223
|
+
let filenames;
|
|
224
|
+
try {
|
|
225
|
+
const entries = await readdir(hooksDir);
|
|
226
|
+
filenames = entries.filter((e) => e.endsWith(".sh"));
|
|
227
|
+
} catch {
|
|
228
|
+
return discovered;
|
|
229
|
+
}
|
|
230
|
+
for (const filename of filenames) {
|
|
231
|
+
const content = await readFile(join(hooksDir, filename), "utf-8");
|
|
232
|
+
const meta = parseHookMeta(content);
|
|
233
|
+
if (meta) {
|
|
234
|
+
discovered.set(filename.replace(/\.sh$/, ""), meta);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return discovered;
|
|
238
|
+
}
|
|
239
|
+
function mergeDiscoveredHooks(existing, discovered, hooksRelPath = ".claude/hooks") {
|
|
240
|
+
if (discovered.size === 0) return existing;
|
|
241
|
+
const merged = {};
|
|
242
|
+
for (const [event, matchers] of Object.entries(existing)) {
|
|
243
|
+
merged[event] = matchers.map((m) => ({
|
|
244
|
+
matcher: m.matcher,
|
|
245
|
+
hooks: [...m.hooks]
|
|
246
|
+
}));
|
|
247
|
+
}
|
|
248
|
+
for (const [filename, meta] of discovered) {
|
|
249
|
+
const command = `bash ${hooksRelPath}/${filename}.sh`;
|
|
250
|
+
if (!merged[meta.event]) {
|
|
251
|
+
merged[meta.event] = [];
|
|
252
|
+
}
|
|
253
|
+
const eventEntries = merged[meta.event];
|
|
254
|
+
const alreadyRegistered = eventEntries.some(
|
|
255
|
+
(m) => m.hooks.some((h) => h.command === command)
|
|
256
|
+
);
|
|
257
|
+
if (alreadyRegistered) continue;
|
|
258
|
+
const matcherEntry = eventEntries.find((m) => m.matcher === meta.matcher);
|
|
259
|
+
if (matcherEntry) {
|
|
260
|
+
matcherEntry.hooks.push({ type: "command", command });
|
|
261
|
+
} else {
|
|
262
|
+
eventEntries.push({
|
|
263
|
+
matcher: meta.matcher,
|
|
264
|
+
hooks: [{ type: "command", command }]
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return merged;
|
|
269
|
+
}
|
|
270
|
+
function stripDiscoveredHooks(config, hooksRelPath = ".claude/hooks") {
|
|
271
|
+
const prefix = `bash ${hooksRelPath}/`;
|
|
272
|
+
const stripped = {};
|
|
273
|
+
for (const [event, matchers] of Object.entries(config)) {
|
|
274
|
+
const filteredMatchers = [];
|
|
275
|
+
for (const matcher of matchers) {
|
|
276
|
+
const filteredHooks = matcher.hooks.filter(
|
|
277
|
+
(h) => !(h.command && h.command.startsWith(prefix) && h.command.endsWith(".sh"))
|
|
278
|
+
);
|
|
279
|
+
if (filteredHooks.length > 0) {
|
|
280
|
+
filteredMatchers.push({
|
|
281
|
+
matcher: matcher.matcher,
|
|
282
|
+
hooks: filteredHooks
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (filteredMatchers.length > 0) {
|
|
287
|
+
stripped[event] = filteredMatchers;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return stripped;
|
|
291
|
+
}
|
|
292
|
+
var init_hook_registry = __esm({
|
|
293
|
+
"src/lib/hook-registry.ts"() {
|
|
294
|
+
"use strict";
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// src/lib/variables.ts
|
|
299
|
+
function substituteVariables(content, repoData) {
|
|
300
|
+
if (!content.includes("{{")) return content;
|
|
301
|
+
let result = content;
|
|
302
|
+
for (const [name, resolver] of Object.entries(TEMPLATE_VARIABLES)) {
|
|
303
|
+
const placeholder = `{{${name}}}`;
|
|
304
|
+
if (result.includes(placeholder)) {
|
|
305
|
+
result = result.replaceAll(placeholder, resolver(repoData));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return result;
|
|
309
|
+
}
|
|
310
|
+
function escapeRegex(str) {
|
|
311
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
312
|
+
}
|
|
313
|
+
function reverseSubstituteVariables(content, repoData) {
|
|
314
|
+
const entries = [];
|
|
315
|
+
for (const [name, resolver] of Object.entries(TEMPLATE_VARIABLES)) {
|
|
316
|
+
const value = resolver(repoData);
|
|
317
|
+
if (value.length === 0) continue;
|
|
318
|
+
entries.push([value, `{{${name}}}`]);
|
|
319
|
+
}
|
|
320
|
+
entries.sort((a, b) => b[0].length - a[0].length);
|
|
321
|
+
let result = content;
|
|
322
|
+
for (const [value, placeholder] of entries) {
|
|
323
|
+
if (value.length < 8) {
|
|
324
|
+
const pattern = new RegExp(`\\b${escapeRegex(value)}\\b`, "g");
|
|
325
|
+
result = result.replace(pattern, placeholder);
|
|
326
|
+
} else {
|
|
327
|
+
result = result.replaceAll(value, placeholder);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return result;
|
|
331
|
+
}
|
|
332
|
+
var TEMPLATE_VARIABLES;
|
|
333
|
+
var init_variables = __esm({
|
|
334
|
+
"src/lib/variables.ts"() {
|
|
335
|
+
"use strict";
|
|
336
|
+
TEMPLATE_VARIABLES = {
|
|
337
|
+
REPO_ID: (repo) => repo.id,
|
|
338
|
+
REPO_NAME: (repo) => repo.name,
|
|
339
|
+
REPO_PATH: (repo) => repo.path ?? "",
|
|
340
|
+
GIT_BRANCH: (repo) => repo.git_branch ?? "development",
|
|
341
|
+
SERVER_PORT: (repo) => repo.server_port != null ? String(repo.server_port) : "",
|
|
342
|
+
SERVER_TYPE: (repo) => repo.server_type ?? "none"
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// src/lib/sync-engine.ts
|
|
348
|
+
var sync_engine_exports = {};
|
|
349
|
+
__export(sync_engine_exports, {
|
|
350
|
+
executeSyncToLocal: () => executeSyncToLocal
|
|
351
|
+
});
|
|
352
|
+
import {
|
|
353
|
+
readdir as readdir2,
|
|
354
|
+
readFile as readFile2,
|
|
355
|
+
writeFile,
|
|
356
|
+
unlink,
|
|
357
|
+
mkdir,
|
|
358
|
+
rmdir,
|
|
359
|
+
chmod,
|
|
360
|
+
stat
|
|
361
|
+
} from "node:fs/promises";
|
|
362
|
+
import { join as join2, dirname } from "node:path";
|
|
363
|
+
function getTypeDir(claudeDir, dir) {
|
|
364
|
+
if (dir === "commands") return join2(claudeDir, dir, "cbp");
|
|
365
|
+
return join2(claudeDir, dir);
|
|
366
|
+
}
|
|
367
|
+
function getFilePath(claudeDir, typeName, file) {
|
|
368
|
+
const cfg = typeConfig[typeName];
|
|
369
|
+
const typeDir = getTypeDir(claudeDir, cfg.dir);
|
|
370
|
+
if (cfg.subfolder) {
|
|
371
|
+
return join2(typeDir, file.name, `${cfg.subfolder}${cfg.ext}`);
|
|
372
|
+
}
|
|
373
|
+
if (typeName === "command" && file.category) {
|
|
374
|
+
return join2(typeDir, file.category, `${file.name}${cfg.ext}`);
|
|
375
|
+
}
|
|
376
|
+
if (typeName === "template") {
|
|
377
|
+
return join2(typeDir, file.name);
|
|
378
|
+
}
|
|
379
|
+
return join2(typeDir, `${file.name}${cfg.ext}`);
|
|
380
|
+
}
|
|
381
|
+
async function readDirRecursive(dir, base = dir) {
|
|
382
|
+
const result = /* @__PURE__ */ new Map();
|
|
383
|
+
try {
|
|
384
|
+
const entries = await readdir2(dir, { withFileTypes: true });
|
|
385
|
+
for (const entry of entries) {
|
|
386
|
+
const fullPath = join2(dir, entry.name);
|
|
387
|
+
if (entry.isDirectory()) {
|
|
388
|
+
const sub = await readDirRecursive(fullPath, base);
|
|
389
|
+
for (const [k, v] of sub) result.set(k, v);
|
|
390
|
+
} else {
|
|
391
|
+
const relPath = fullPath.slice(base.length + 1);
|
|
392
|
+
const fileContent = await readFile2(fullPath, "utf-8");
|
|
393
|
+
result.set(relPath, fileContent);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
} catch {
|
|
397
|
+
}
|
|
398
|
+
return result;
|
|
399
|
+
}
|
|
400
|
+
async function isGitWorktree(projectPath) {
|
|
401
|
+
try {
|
|
402
|
+
const gitPath = join2(projectPath, ".git");
|
|
403
|
+
const info = await stat(gitPath);
|
|
404
|
+
return info.isFile();
|
|
405
|
+
} catch {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
async function removeEmptyParents(filePath, stopAt) {
|
|
410
|
+
let dir = dirname(filePath);
|
|
411
|
+
while (dir.length > stopAt.length && dir.startsWith(stopAt)) {
|
|
412
|
+
try {
|
|
413
|
+
await rmdir(dir);
|
|
414
|
+
dir = dirname(dir);
|
|
415
|
+
} catch {
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
async function executeSyncToLocal(options) {
|
|
421
|
+
const { repoId, projectPath, dryRun = false } = options;
|
|
422
|
+
const [syncRes, repoRes] = await Promise.all([
|
|
423
|
+
apiGet("/sync/defaults"),
|
|
424
|
+
apiGet(`/repos/${repoId}`)
|
|
425
|
+
]);
|
|
426
|
+
const syncData = syncRes.data;
|
|
427
|
+
const repoData = repoRes.data;
|
|
428
|
+
syncData.claude_md = [];
|
|
429
|
+
const claudeDir = join2(projectPath, ".claude");
|
|
430
|
+
const worktree = await isGitWorktree(projectPath);
|
|
431
|
+
const byType = {};
|
|
432
|
+
const totals = { created: 0, updated: 0, deleted: 0, unchanged: 0 };
|
|
433
|
+
const dbOnlyFiles = [];
|
|
434
|
+
for (const [syncKey, typeName] of Object.entries(syncKeyToType)) {
|
|
435
|
+
if (worktree && typeName === "command") {
|
|
436
|
+
byType["commands"] = {
|
|
437
|
+
created: [],
|
|
438
|
+
updated: [],
|
|
439
|
+
deleted: [],
|
|
440
|
+
unchanged: []
|
|
441
|
+
};
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
const cfg = typeConfig[typeName];
|
|
445
|
+
const targetDir = getTypeDir(claudeDir, cfg.dir);
|
|
446
|
+
const remoteFiles = syncData[syncKey] ?? [];
|
|
447
|
+
const result = {
|
|
448
|
+
created: [],
|
|
449
|
+
updated: [],
|
|
450
|
+
deleted: [],
|
|
451
|
+
unchanged: []
|
|
452
|
+
};
|
|
453
|
+
if (!dryRun) {
|
|
454
|
+
await mkdir(targetDir, { recursive: true });
|
|
455
|
+
}
|
|
456
|
+
const localFiles = await readDirRecursive(targetDir);
|
|
457
|
+
const remotePathMap = /* @__PURE__ */ new Map();
|
|
458
|
+
for (const remote of remoteFiles) {
|
|
459
|
+
const fullPath = getFilePath(claudeDir, typeName, remote);
|
|
460
|
+
const relPath = fullPath.slice(targetDir.length + 1);
|
|
461
|
+
const substituted = substituteVariables(remote.content, repoData);
|
|
462
|
+
remotePathMap.set(relPath, { content: substituted, name: remote.name });
|
|
463
|
+
}
|
|
464
|
+
for (const [relPath, { content, name }] of remotePathMap) {
|
|
465
|
+
const fullPath = join2(targetDir, relPath);
|
|
466
|
+
const localContent = localFiles.get(relPath);
|
|
467
|
+
if (localContent === void 0) {
|
|
468
|
+
const remoteFile = remoteFiles.find((f) => f.name === name);
|
|
469
|
+
dbOnlyFiles.push({
|
|
470
|
+
type: typeName,
|
|
471
|
+
name,
|
|
472
|
+
category: remoteFile?.category ?? null,
|
|
473
|
+
localPath: fullPath
|
|
474
|
+
});
|
|
475
|
+
if (!dryRun) {
|
|
476
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
477
|
+
await writeFile(fullPath, content, "utf-8");
|
|
478
|
+
if (typeName === "hook") await chmod(fullPath, 493);
|
|
479
|
+
}
|
|
480
|
+
result.created.push(name);
|
|
481
|
+
totals.created++;
|
|
482
|
+
} else if (localContent !== content) {
|
|
483
|
+
if (!dryRun) {
|
|
484
|
+
await writeFile(fullPath, content, "utf-8");
|
|
485
|
+
if (typeName === "hook") await chmod(fullPath, 493);
|
|
486
|
+
}
|
|
487
|
+
result.updated.push(name);
|
|
488
|
+
totals.updated++;
|
|
489
|
+
} else {
|
|
490
|
+
result.unchanged.push(name);
|
|
491
|
+
totals.unchanged++;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
for (const [relPath] of localFiles) {
|
|
495
|
+
if (!remotePathMap.has(relPath)) {
|
|
496
|
+
const fullPath = join2(targetDir, relPath);
|
|
497
|
+
if (!dryRun) {
|
|
498
|
+
await unlink(fullPath);
|
|
499
|
+
await removeEmptyParents(fullPath, targetDir);
|
|
500
|
+
}
|
|
501
|
+
const pathName = relPath.replace(/\.(md|sh)$/, "").replace(/\/(AGENT|SKILL)$/, "");
|
|
502
|
+
result.deleted.push(pathName);
|
|
503
|
+
totals.deleted++;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
byType[`${typeName}s`] = result;
|
|
507
|
+
}
|
|
508
|
+
{
|
|
509
|
+
const typeName = "docs_stack";
|
|
510
|
+
const syncKey = "docs_stack";
|
|
511
|
+
const targetDir = join2(projectPath, "docs", "stack");
|
|
512
|
+
const remoteFiles = syncData[syncKey] ?? [];
|
|
513
|
+
const result = {
|
|
514
|
+
created: [],
|
|
515
|
+
updated: [],
|
|
516
|
+
deleted: [],
|
|
517
|
+
unchanged: []
|
|
518
|
+
};
|
|
519
|
+
if (remoteFiles.length > 0 && !dryRun) {
|
|
520
|
+
await mkdir(targetDir, { recursive: true });
|
|
521
|
+
}
|
|
522
|
+
const localFiles = await readDirRecursive(targetDir);
|
|
523
|
+
const remotePathMap = /* @__PURE__ */ new Map();
|
|
524
|
+
for (const remote of remoteFiles) {
|
|
525
|
+
const relPath = remote.category ? join2(remote.category, remote.name) : remote.name;
|
|
526
|
+
const substituted = substituteVariables(remote.content, repoData);
|
|
527
|
+
remotePathMap.set(relPath, {
|
|
528
|
+
content: substituted,
|
|
529
|
+
name: `${remote.category ?? ""}/${remote.name}`
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
for (const [relPath, { content, name }] of remotePathMap) {
|
|
533
|
+
const fullPath = join2(targetDir, relPath);
|
|
534
|
+
const localContent = localFiles.get(relPath);
|
|
535
|
+
if (localContent === void 0) {
|
|
536
|
+
if (!dryRun) {
|
|
537
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
538
|
+
await writeFile(fullPath, content, "utf-8");
|
|
539
|
+
}
|
|
540
|
+
result.created.push(name);
|
|
541
|
+
totals.created++;
|
|
542
|
+
} else if (localContent !== content) {
|
|
543
|
+
if (!dryRun) {
|
|
544
|
+
await writeFile(fullPath, content, "utf-8");
|
|
545
|
+
}
|
|
546
|
+
result.updated.push(name);
|
|
547
|
+
totals.updated++;
|
|
548
|
+
} else {
|
|
549
|
+
result.unchanged.push(name);
|
|
550
|
+
totals.unchanged++;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
for (const [relPath] of localFiles) {
|
|
554
|
+
if (!remotePathMap.has(relPath)) {
|
|
555
|
+
const fullPath = join2(targetDir, relPath);
|
|
556
|
+
if (!dryRun) {
|
|
557
|
+
await unlink(fullPath);
|
|
558
|
+
await removeEmptyParents(fullPath, targetDir);
|
|
559
|
+
}
|
|
560
|
+
result.deleted.push(relPath);
|
|
561
|
+
totals.deleted++;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
byType[typeName] = result;
|
|
565
|
+
}
|
|
566
|
+
const globalSettingsFiles = syncData.global_settings ?? [];
|
|
567
|
+
let globalSettings = {};
|
|
568
|
+
for (const gf of globalSettingsFiles) {
|
|
569
|
+
const parsed = JSON.parse(
|
|
570
|
+
substituteVariables(gf.content, repoData)
|
|
571
|
+
);
|
|
572
|
+
globalSettings = { ...globalSettings, ...parsed };
|
|
573
|
+
}
|
|
574
|
+
const specialTypes = {
|
|
575
|
+
claude_md: () => join2(projectPath, "CLAUDE.md"),
|
|
576
|
+
settings: () => join2(projectPath, ".claude", "settings.json")
|
|
577
|
+
};
|
|
578
|
+
for (const [typeName, getPath] of Object.entries(specialTypes)) {
|
|
579
|
+
const remoteFiles = syncData[typeName] ?? [];
|
|
580
|
+
const result = {
|
|
581
|
+
created: [],
|
|
582
|
+
updated: [],
|
|
583
|
+
deleted: [],
|
|
584
|
+
unchanged: []
|
|
585
|
+
};
|
|
586
|
+
for (const remote of remoteFiles) {
|
|
587
|
+
const targetPath = getPath(remote.name);
|
|
588
|
+
const remoteContent = substituteVariables(remote.content, repoData);
|
|
589
|
+
let localContent;
|
|
590
|
+
try {
|
|
591
|
+
localContent = await readFile2(targetPath, "utf-8");
|
|
592
|
+
} catch {
|
|
593
|
+
}
|
|
594
|
+
if (typeName === "settings") {
|
|
595
|
+
const repoSettings = JSON.parse(remoteContent);
|
|
596
|
+
const combinedTemplate = mergeGlobalAndRepoSettings(
|
|
597
|
+
globalSettings,
|
|
598
|
+
repoSettings
|
|
599
|
+
);
|
|
600
|
+
const hooksDir = join2(projectPath, ".claude", "hooks");
|
|
601
|
+
const discovered = await discoverHooks(hooksDir);
|
|
602
|
+
if (localContent === void 0) {
|
|
603
|
+
const finalSettings = stripPermissionsAllow(combinedTemplate);
|
|
604
|
+
if (discovered.size > 0) {
|
|
605
|
+
finalSettings.hooks = mergeDiscoveredHooks(
|
|
606
|
+
finalSettings.hooks ?? {},
|
|
607
|
+
discovered
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
if (!dryRun) {
|
|
611
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
612
|
+
await writeFile(
|
|
613
|
+
targetPath,
|
|
614
|
+
JSON.stringify(finalSettings, null, 2) + "\n",
|
|
615
|
+
"utf-8"
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
result.created.push(remote.name);
|
|
619
|
+
totals.created++;
|
|
620
|
+
} else {
|
|
621
|
+
const localSettings = JSON.parse(localContent);
|
|
622
|
+
let merged = mergeSettings(combinedTemplate, localSettings);
|
|
623
|
+
merged = stripPermissionsAllow(merged);
|
|
624
|
+
if (discovered.size > 0) {
|
|
625
|
+
merged.hooks = mergeDiscoveredHooks(
|
|
626
|
+
merged.hooks ?? {},
|
|
627
|
+
discovered
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
const mergedContent = JSON.stringify(merged, null, 2) + "\n";
|
|
631
|
+
if (localContent !== mergedContent) {
|
|
632
|
+
if (!dryRun) {
|
|
633
|
+
await writeFile(targetPath, mergedContent, "utf-8");
|
|
634
|
+
}
|
|
635
|
+
result.updated.push(remote.name);
|
|
636
|
+
totals.updated++;
|
|
637
|
+
} else {
|
|
638
|
+
result.unchanged.push(remote.name);
|
|
639
|
+
totals.unchanged++;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
} else {
|
|
643
|
+
if (localContent === void 0) {
|
|
644
|
+
if (!dryRun) {
|
|
645
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
646
|
+
await writeFile(targetPath, remoteContent, "utf-8");
|
|
647
|
+
}
|
|
648
|
+
result.created.push(remote.name);
|
|
649
|
+
totals.created++;
|
|
650
|
+
} else if (localContent !== remoteContent) {
|
|
651
|
+
if (!dryRun) {
|
|
652
|
+
await writeFile(targetPath, remoteContent, "utf-8");
|
|
653
|
+
}
|
|
654
|
+
result.updated.push(remote.name);
|
|
655
|
+
totals.updated++;
|
|
656
|
+
} else {
|
|
657
|
+
result.unchanged.push(remote.name);
|
|
658
|
+
totals.unchanged++;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
byType[typeName] = result;
|
|
663
|
+
}
|
|
664
|
+
if (!dryRun) {
|
|
665
|
+
await apiPost("/sync/state", {
|
|
666
|
+
repo_id: repoId,
|
|
667
|
+
last_synced_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
668
|
+
was_skipped: false,
|
|
669
|
+
files_synced_count: totals.created + totals.updated + totals.deleted + totals.unchanged,
|
|
670
|
+
files_pushed: 0,
|
|
671
|
+
files_pulled: totals.created + totals.updated,
|
|
672
|
+
files_deleted: totals.deleted,
|
|
673
|
+
files_skipped: 0
|
|
674
|
+
});
|
|
675
|
+
const fileRepoUpdates = [];
|
|
676
|
+
const syncTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
677
|
+
for (const [syncKey, typeName] of Object.entries(syncKeyToType)) {
|
|
678
|
+
const remoteFiles = syncData[syncKey] ?? [];
|
|
679
|
+
for (const file of remoteFiles) {
|
|
680
|
+
fileRepoUpdates.push({
|
|
681
|
+
claude_file_id: file.id ?? void 0,
|
|
682
|
+
file_type: typeName,
|
|
683
|
+
file_name: file.name,
|
|
684
|
+
file_category: file.category ?? null,
|
|
685
|
+
file_scope: file.scope ?? "shared",
|
|
686
|
+
last_synced_at: syncTimestamp,
|
|
687
|
+
sync_status: "synced"
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
for (const typeName of ["claude_md", "settings"]) {
|
|
692
|
+
const remoteFiles = syncData[typeName] ?? [];
|
|
693
|
+
for (const file of remoteFiles) {
|
|
694
|
+
fileRepoUpdates.push({
|
|
695
|
+
claude_file_id: file.id ?? void 0,
|
|
696
|
+
file_type: typeName,
|
|
697
|
+
file_name: file.name,
|
|
698
|
+
file_category: file.category ?? null,
|
|
699
|
+
file_scope: file.scope ?? `local:${repoId}`,
|
|
700
|
+
last_synced_at: syncTimestamp,
|
|
701
|
+
sync_status: "synced"
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (fileRepoUpdates.length > 0) {
|
|
706
|
+
try {
|
|
707
|
+
await apiPost("/sync/file-repos", {
|
|
708
|
+
repo_id: repoId,
|
|
709
|
+
file_repos: fileRepoUpdates
|
|
710
|
+
});
|
|
711
|
+
} catch {
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return { byType, totals, dbOnlyFiles };
|
|
716
|
+
}
|
|
717
|
+
var typeConfig, syncKeyToType;
|
|
718
|
+
var init_sync_engine = __esm({
|
|
719
|
+
"src/lib/sync-engine.ts"() {
|
|
720
|
+
"use strict";
|
|
721
|
+
init_api();
|
|
722
|
+
init_settings_merge();
|
|
723
|
+
init_hook_registry();
|
|
724
|
+
init_variables();
|
|
725
|
+
typeConfig = {
|
|
726
|
+
command: { dir: "commands", ext: ".md" },
|
|
727
|
+
agent: { dir: "agents", ext: ".md", subfolder: "AGENT" },
|
|
728
|
+
skill: { dir: "skills", ext: ".md", subfolder: "SKILL" },
|
|
729
|
+
rule: { dir: "rules", ext: ".md" },
|
|
730
|
+
hook: { dir: "hooks", ext: ".sh" },
|
|
731
|
+
template: { dir: "templates", ext: "" },
|
|
732
|
+
context: { dir: "context", ext: ".md" }
|
|
733
|
+
};
|
|
734
|
+
syncKeyToType = {
|
|
735
|
+
commands: "command",
|
|
736
|
+
agents: "agent",
|
|
737
|
+
skills: "skill",
|
|
738
|
+
rules: "rule",
|
|
739
|
+
hooks: "hook",
|
|
740
|
+
templates: "template",
|
|
741
|
+
contexts: "context"
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
// src/cli/setup.ts
|
|
747
|
+
var setup_exports = {};
|
|
748
|
+
__export(setup_exports, {
|
|
749
|
+
runSetup: () => runSetup
|
|
750
|
+
});
|
|
751
|
+
import { createInterface } from "node:readline/promises";
|
|
752
|
+
import { stdin, stdout } from "node:process";
|
|
753
|
+
import { readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
|
|
754
|
+
import { homedir } from "node:os";
|
|
755
|
+
import { join as join3 } from "node:path";
|
|
756
|
+
function getConfigPath(scope) {
|
|
757
|
+
return scope === "user" ? join3(homedir(), ".claude.json") : join3(process.cwd(), ".mcp.json");
|
|
758
|
+
}
|
|
759
|
+
async function readConfig(path) {
|
|
760
|
+
try {
|
|
761
|
+
const raw = await readFile3(path, "utf-8");
|
|
762
|
+
const parsed = JSON.parse(raw);
|
|
763
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
764
|
+
return parsed;
|
|
765
|
+
}
|
|
766
|
+
return {};
|
|
767
|
+
} catch {
|
|
768
|
+
return {};
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
function buildMcpEntry(apiKey) {
|
|
772
|
+
const baseUrl = process.env.CODEBYPLAN_API_URL ?? "https://codebyplan.com";
|
|
773
|
+
return {
|
|
774
|
+
url: `${baseUrl}/mcp`,
|
|
775
|
+
headers: { "x-api-key": apiKey }
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
async function writeMcpConfig(scope, apiKey) {
|
|
779
|
+
const configPath = getConfigPath(scope);
|
|
780
|
+
const config = await readConfig(configPath);
|
|
781
|
+
if (typeof config.mcpServers !== "object" || config.mcpServers === null || Array.isArray(config.mcpServers)) {
|
|
782
|
+
config.mcpServers = {};
|
|
783
|
+
}
|
|
784
|
+
config.mcpServers.codebyplan = buildMcpEntry(apiKey);
|
|
785
|
+
await writeFile2(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
786
|
+
return configPath;
|
|
787
|
+
}
|
|
788
|
+
async function verifyMcpConfig(scope, apiKey) {
|
|
789
|
+
try {
|
|
790
|
+
const config = await readConfig(getConfigPath(scope));
|
|
791
|
+
const servers = config.mcpServers;
|
|
792
|
+
if (!servers) return false;
|
|
793
|
+
const entry = servers.codebyplan;
|
|
794
|
+
return entry?.url !== void 0 && entry?.headers?.["x-api-key"] === apiKey;
|
|
795
|
+
} catch {
|
|
796
|
+
return false;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
async function runSetup() {
|
|
800
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
801
|
+
console.log("\n CodeByPlan Setup\n");
|
|
802
|
+
console.log(" This will configure Claude Code to use CodeByPlan.\n");
|
|
803
|
+
console.log(" 1. Sign up at https://codebyplan.com");
|
|
804
|
+
console.log(
|
|
805
|
+
" 2. Create an API key at https://codebyplan.com/settings/api-keys/\n"
|
|
806
|
+
);
|
|
807
|
+
try {
|
|
808
|
+
const apiKey = (await rl.question(" Enter your API key: ")).trim();
|
|
809
|
+
if (!apiKey) {
|
|
810
|
+
console.log("\n No API key provided. Aborting setup.\n");
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
console.log("\n Validating API key...");
|
|
814
|
+
const baseUrl = process.env.CODEBYPLAN_API_URL ?? "https://codebyplan.com";
|
|
815
|
+
const res = await fetch(`${baseUrl}/api/repos`, {
|
|
816
|
+
headers: { "x-api-key": apiKey },
|
|
817
|
+
signal: AbortSignal.timeout(1e4)
|
|
818
|
+
});
|
|
819
|
+
if (res.status === 401) {
|
|
820
|
+
console.log(" Invalid API key. Please check and try again.\n");
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
let repos = [];
|
|
824
|
+
if (res.ok) {
|
|
825
|
+
try {
|
|
826
|
+
const body = await res.json();
|
|
827
|
+
repos = body.data ?? [];
|
|
828
|
+
if (repos.length === 0) {
|
|
829
|
+
console.log(" API key is valid but no repositories found.");
|
|
830
|
+
console.log(" Create one at https://codebyplan.com after setup.\n");
|
|
831
|
+
} else {
|
|
832
|
+
console.log(" API key is valid!\n");
|
|
833
|
+
}
|
|
834
|
+
} catch {
|
|
835
|
+
console.log(" API key is valid!\n");
|
|
836
|
+
}
|
|
837
|
+
} else {
|
|
838
|
+
console.log(
|
|
839
|
+
` Warning: API returned status ${res.status}, but continuing.
|
|
840
|
+
`
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
console.log(" Where should the MCP server be configured?\n");
|
|
844
|
+
console.log(" 1. Global \u2014 available in all projects (~/.claude.json)");
|
|
845
|
+
console.log(" 2. Project \u2014 only this project (.mcp.json)\n");
|
|
846
|
+
const scopeInput = (await rl.question(" Select (1/2, default: 1): ")).trim();
|
|
847
|
+
const scope = scopeInput === "2" ? "project" : "user";
|
|
848
|
+
console.log("\n Configuring MCP server...");
|
|
849
|
+
const configPath = await writeMcpConfig(scope, apiKey);
|
|
850
|
+
const verified = await verifyMcpConfig(scope, apiKey);
|
|
851
|
+
if (verified) {
|
|
852
|
+
console.log(` Done! Config written to ${configPath}
|
|
853
|
+
`);
|
|
854
|
+
if (scope === "project") {
|
|
855
|
+
console.log(
|
|
856
|
+
" Note: .mcp.json contains your API key \u2014 add it to .gitignore.\n"
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
} else {
|
|
860
|
+
console.log(" Warning: Could not verify the saved configuration.\n");
|
|
861
|
+
console.log(
|
|
862
|
+
` Manually add to ~/.claude.json under mcpServers.codebyplan:
|
|
863
|
+
`
|
|
864
|
+
);
|
|
865
|
+
console.log(
|
|
866
|
+
` { "url": "https://codebyplan.com/mcp", "headers": { "x-api-key": "${apiKey}" } }
|
|
867
|
+
`
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
if (repos.length > 0) {
|
|
871
|
+
console.log(" Initialize this project?\n");
|
|
872
|
+
const initAnswer = (await rl.question(" Link to a repository? (Y/n): ")).trim().toLowerCase();
|
|
873
|
+
if (initAnswer === "" || initAnswer === "y" || initAnswer === "yes") {
|
|
874
|
+
process.env.CODEBYPLAN_API_KEY = apiKey;
|
|
875
|
+
console.log("\n Your repositories:\n");
|
|
876
|
+
for (let i = 0; i < repos.length; i++) {
|
|
877
|
+
console.log(` ${i + 1}. ${repos[i].name}`);
|
|
878
|
+
}
|
|
879
|
+
console.log();
|
|
880
|
+
const choice = (await rl.question(" Select a repository (number): ")).trim();
|
|
881
|
+
const index = parseInt(choice, 10) - 1;
|
|
882
|
+
if (isNaN(index) || index < 0 || index >= repos.length) {
|
|
883
|
+
console.log(" Invalid selection. Skipping project init.\n");
|
|
884
|
+
} else {
|
|
885
|
+
const selectedRepo = repos[index];
|
|
886
|
+
console.log(`
|
|
887
|
+
Selected: ${selectedRepo.name}
|
|
888
|
+
`);
|
|
889
|
+
let worktreeId;
|
|
890
|
+
const projectPath = process.cwd();
|
|
891
|
+
try {
|
|
892
|
+
const worktreesRes = await apiGet(`/worktrees?repo_id=${selectedRepo.id}`);
|
|
893
|
+
const match = worktreesRes.data.find(
|
|
894
|
+
(wt) => projectPath === wt.path || projectPath.startsWith(wt.path + "/")
|
|
895
|
+
);
|
|
896
|
+
if (match) worktreeId = match.id;
|
|
897
|
+
} catch {
|
|
898
|
+
}
|
|
899
|
+
const codebyplanPath = join3(projectPath, ".codebyplan.json");
|
|
900
|
+
const codebyplanConfig = {
|
|
901
|
+
repo_id: selectedRepo.id
|
|
902
|
+
};
|
|
903
|
+
if (worktreeId) codebyplanConfig.worktree_id = worktreeId;
|
|
904
|
+
await writeFile2(
|
|
905
|
+
codebyplanPath,
|
|
906
|
+
JSON.stringify(codebyplanConfig, null, 2) + "\n",
|
|
907
|
+
"utf-8"
|
|
908
|
+
);
|
|
909
|
+
console.log(` Created ${codebyplanPath}`);
|
|
910
|
+
console.log("\n Running initial sync...\n");
|
|
911
|
+
try {
|
|
912
|
+
const { executeSyncToLocal: executeSyncToLocal2 } = await Promise.resolve().then(() => (init_sync_engine(), sync_engine_exports));
|
|
913
|
+
const syncResult = await executeSyncToLocal2({
|
|
914
|
+
repoId: selectedRepo.id,
|
|
915
|
+
projectPath
|
|
916
|
+
});
|
|
917
|
+
const totalChanges = syncResult.totals.created + syncResult.totals.updated + syncResult.totals.deleted;
|
|
918
|
+
if (totalChanges > 0) {
|
|
919
|
+
console.log(
|
|
920
|
+
` Synced: ${syncResult.totals.created} created, ${syncResult.totals.updated} updated, ${syncResult.totals.deleted} deleted
|
|
921
|
+
`
|
|
922
|
+
);
|
|
923
|
+
} else {
|
|
924
|
+
console.log(" All files already up to date.\n");
|
|
925
|
+
}
|
|
926
|
+
} catch (err) {
|
|
927
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
928
|
+
console.log(` Sync failed: ${msg}`);
|
|
929
|
+
console.log(" Run 'codebyplan sync' later to sync files.\n");
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
console.log(
|
|
935
|
+
" Setup complete! Start a new Claude Code session to begin.\n"
|
|
936
|
+
);
|
|
937
|
+
} finally {
|
|
938
|
+
rl.close();
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
var init_setup = __esm({
|
|
942
|
+
"src/cli/setup.ts"() {
|
|
943
|
+
"use strict";
|
|
944
|
+
init_api();
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
// src/cli/config.ts
|
|
949
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
950
|
+
import { join as join4 } from "node:path";
|
|
951
|
+
function parseFlags(startIndex) {
|
|
952
|
+
const flags = {};
|
|
953
|
+
const args = process.argv.slice(startIndex);
|
|
954
|
+
for (let i = 0; i < args.length; i++) {
|
|
955
|
+
const arg2 = args[i];
|
|
956
|
+
if (arg2.startsWith("--") && i + 1 < args.length) {
|
|
957
|
+
const key = arg2.slice(2);
|
|
958
|
+
flags[key] = args[++i];
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
return flags;
|
|
962
|
+
}
|
|
963
|
+
function hasFlag(name, startIndex) {
|
|
964
|
+
return process.argv.slice(startIndex).includes(`--${name}`);
|
|
965
|
+
}
|
|
966
|
+
async function resolveConfig(flags) {
|
|
967
|
+
const projectPath = flags["path"] ?? process.cwd();
|
|
968
|
+
let repoId = flags["repo-id"] ?? process.env.CODEBYPLAN_REPO_ID;
|
|
969
|
+
let worktreeId = flags["worktree-id"] ?? process.env.CODEBYPLAN_WORKTREE_ID;
|
|
970
|
+
if (!repoId || !worktreeId) {
|
|
971
|
+
try {
|
|
972
|
+
const configPath = join4(projectPath, ".codebyplan.json");
|
|
973
|
+
const raw = await readFile4(configPath, "utf-8");
|
|
974
|
+
const config = JSON.parse(raw);
|
|
975
|
+
if (!repoId) repoId = config.repo_id;
|
|
976
|
+
if (!worktreeId) worktreeId = config.worktree_id;
|
|
977
|
+
} catch {
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
if (!repoId) {
|
|
981
|
+
throw new Error(
|
|
982
|
+
'Could not determine repo_id.\n\nProvide it via one of:\n --repo-id <uuid> CLI flag\n CODEBYPLAN_REPO_ID=<uuid> environment variable\n .codebyplan.json { "repo_id": "<uuid>" } in project root'
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
return { repoId, worktreeId, projectPath };
|
|
986
|
+
}
|
|
987
|
+
var init_config = __esm({
|
|
988
|
+
"src/cli/config.ts"() {
|
|
989
|
+
"use strict";
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
// src/cli/fileMapper.ts
|
|
994
|
+
import { readdir as readdir3, readFile as readFile5 } from "node:fs/promises";
|
|
995
|
+
import { join as join5, extname } from "node:path";
|
|
996
|
+
function extractScope(content, type) {
|
|
997
|
+
if (type === "hook") {
|
|
998
|
+
const match = content.match(/^#\s*@scope:\s*(\S+)/m);
|
|
999
|
+
if (match) {
|
|
1000
|
+
const raw = match[1];
|
|
1001
|
+
return raw === "shared" ? "shared" : `local:${raw}`;
|
|
1002
|
+
}
|
|
1003
|
+
return "shared";
|
|
1004
|
+
}
|
|
1005
|
+
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
1006
|
+
if (fmMatch) {
|
|
1007
|
+
const scopeLine = fmMatch[1].match(/^scope:\s*(\S+)/m);
|
|
1008
|
+
if (scopeLine) {
|
|
1009
|
+
const raw = scopeLine[1];
|
|
1010
|
+
return raw === "shared" ? "shared" : `local:${raw}`;
|
|
1011
|
+
}
|
|
1012
|
+
if (/^scope\b/m.test(fmMatch[1])) {
|
|
1013
|
+
console.error(
|
|
1014
|
+
` Warning: frontmatter contains "scope" but could not parse it. Expected format: "scope: shared" or "scope: <repo-name>". Defaulting to "shared".`
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
return "shared";
|
|
1019
|
+
}
|
|
1020
|
+
function compositeKey(type, name, category) {
|
|
1021
|
+
return category ? `${type}:${category}/${name}` : `${type}:${name}`;
|
|
1022
|
+
}
|
|
1023
|
+
async function scanLocalFiles(claudeDir, projectPath) {
|
|
1024
|
+
const result = /* @__PURE__ */ new Map();
|
|
1025
|
+
await scanCommands(join5(claudeDir, "commands", "cbp"), result);
|
|
1026
|
+
await scanSubfolderType(
|
|
1027
|
+
join5(claudeDir, "agents"),
|
|
1028
|
+
"agent",
|
|
1029
|
+
"AGENT.md",
|
|
1030
|
+
result
|
|
1031
|
+
);
|
|
1032
|
+
await scanSubfolderType(
|
|
1033
|
+
join5(claudeDir, "skills"),
|
|
1034
|
+
"skill",
|
|
1035
|
+
"SKILL.md",
|
|
1036
|
+
result
|
|
1037
|
+
);
|
|
1038
|
+
await scanFlatType(join5(claudeDir, "rules"), "rule", ".md", result);
|
|
1039
|
+
await scanFlatType(join5(claudeDir, "hooks"), "hook", ".sh", result);
|
|
1040
|
+
await scanTemplates(join5(claudeDir, "templates"), result);
|
|
1041
|
+
await scanSettings(claudeDir, projectPath, result);
|
|
1042
|
+
return result;
|
|
1043
|
+
}
|
|
1044
|
+
async function scanCommands(dir, result) {
|
|
1045
|
+
await scanCommandsRecursive(dir, dir, result);
|
|
1046
|
+
}
|
|
1047
|
+
async function scanCommandsRecursive(baseDir, currentDir, result) {
|
|
1048
|
+
let entries;
|
|
1049
|
+
try {
|
|
1050
|
+
entries = await readdir3(currentDir, { withFileTypes: true });
|
|
1051
|
+
} catch {
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
for (const entry of entries) {
|
|
1055
|
+
if (entry.isDirectory()) {
|
|
1056
|
+
await scanCommandsRecursive(
|
|
1057
|
+
baseDir,
|
|
1058
|
+
join5(currentDir, entry.name),
|
|
1059
|
+
result
|
|
1060
|
+
);
|
|
1061
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1062
|
+
const name = entry.name.slice(0, -3);
|
|
1063
|
+
const content = await readFile5(join5(currentDir, entry.name), "utf-8");
|
|
1064
|
+
const relDir = currentDir.slice(baseDir.length + 1);
|
|
1065
|
+
const category = relDir || null;
|
|
1066
|
+
const scope = extractScope(content, "command");
|
|
1067
|
+
const key = compositeKey("command", name, category);
|
|
1068
|
+
result.set(key, { type: "command", name, category, content, scope });
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
async function scanSubfolderType(dir, type, fileName, result) {
|
|
1073
|
+
let entries;
|
|
1074
|
+
try {
|
|
1075
|
+
entries = await readdir3(dir, { withFileTypes: true });
|
|
1076
|
+
} catch {
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
for (const entry of entries) {
|
|
1080
|
+
if (entry.isDirectory()) {
|
|
1081
|
+
const filePath = join5(dir, entry.name, fileName);
|
|
1082
|
+
try {
|
|
1083
|
+
const content = await readFile5(filePath, "utf-8");
|
|
1084
|
+
const scope = extractScope(content, type);
|
|
1085
|
+
const key = compositeKey(type, entry.name, null);
|
|
1086
|
+
result.set(key, {
|
|
1087
|
+
type,
|
|
1088
|
+
name: entry.name,
|
|
1089
|
+
category: null,
|
|
1090
|
+
content,
|
|
1091
|
+
scope
|
|
1092
|
+
});
|
|
1093
|
+
} catch {
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
async function scanFlatType(dir, type, ext, result) {
|
|
1099
|
+
let entries;
|
|
1100
|
+
try {
|
|
1101
|
+
entries = await readdir3(dir, { withFileTypes: true });
|
|
1102
|
+
} catch {
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
for (const entry of entries) {
|
|
1106
|
+
if (entry.isFile() && entry.name.endsWith(ext)) {
|
|
1107
|
+
const name = entry.name.slice(0, -ext.length);
|
|
1108
|
+
const content = await readFile5(join5(dir, entry.name), "utf-8");
|
|
1109
|
+
const scope = extractScope(content, type);
|
|
1110
|
+
const key = compositeKey(type, name, null);
|
|
1111
|
+
result.set(key, { type, name, category: null, content, scope });
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
async function scanTemplates(dir, result) {
|
|
1116
|
+
let entries;
|
|
1117
|
+
try {
|
|
1118
|
+
entries = await readdir3(dir, { withFileTypes: true });
|
|
1119
|
+
} catch {
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
for (const entry of entries) {
|
|
1123
|
+
if (entry.isFile() && extname(entry.name)) {
|
|
1124
|
+
const content = await readFile5(join5(dir, entry.name), "utf-8");
|
|
1125
|
+
const scope = extractScope(content, "template");
|
|
1126
|
+
const key = compositeKey("template", entry.name, null);
|
|
1127
|
+
result.set(key, {
|
|
1128
|
+
type: "template",
|
|
1129
|
+
name: entry.name,
|
|
1130
|
+
category: null,
|
|
1131
|
+
content,
|
|
1132
|
+
scope
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
async function scanSettings(claudeDir, projectPath, result) {
|
|
1138
|
+
const settingsPath = join5(claudeDir, "settings.json");
|
|
1139
|
+
let raw;
|
|
1140
|
+
try {
|
|
1141
|
+
raw = await readFile5(settingsPath, "utf-8");
|
|
1142
|
+
} catch {
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
let parsed;
|
|
1146
|
+
try {
|
|
1147
|
+
parsed = JSON.parse(raw);
|
|
1148
|
+
} catch {
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
parsed = stripPermissionsAllow(parsed);
|
|
1152
|
+
if (parsed.hooks && typeof parsed.hooks === "object") {
|
|
1153
|
+
const hooksDir = projectPath ? join5(projectPath, ".claude", "hooks") : join5(claudeDir, "hooks");
|
|
1154
|
+
const discovered = await discoverHooks(hooksDir);
|
|
1155
|
+
if (discovered.size > 0) {
|
|
1156
|
+
parsed.hooks = stripDiscoveredHooks(
|
|
1157
|
+
parsed.hooks,
|
|
1158
|
+
".claude/hooks"
|
|
1159
|
+
);
|
|
1160
|
+
if (Object.keys(parsed.hooks).length === 0) {
|
|
1161
|
+
delete parsed.hooks;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
const content = JSON.stringify(parsed, null, 2) + "\n";
|
|
1166
|
+
const key = compositeKey("settings", "settings", null);
|
|
1167
|
+
result.set(key, {
|
|
1168
|
+
type: "settings",
|
|
1169
|
+
name: "settings",
|
|
1170
|
+
category: null,
|
|
1171
|
+
content,
|
|
1172
|
+
scope: "shared"
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
var init_fileMapper = __esm({
|
|
1176
|
+
"src/cli/fileMapper.ts"() {
|
|
1177
|
+
"use strict";
|
|
1178
|
+
init_settings_merge();
|
|
1179
|
+
init_hook_registry();
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
// src/cli/confirm.ts
|
|
1184
|
+
var confirm_exports = {};
|
|
1185
|
+
__export(confirm_exports, {
|
|
1186
|
+
SyncCancelledError: () => SyncCancelledError,
|
|
1187
|
+
confirmEach: () => confirmEach,
|
|
1188
|
+
confirmProceed: () => confirmProceed,
|
|
1189
|
+
promptChoice: () => promptChoice,
|
|
1190
|
+
promptReviewMode: () => promptReviewMode,
|
|
1191
|
+
reviewFilesOneByOne: () => reviewFilesOneByOne,
|
|
1192
|
+
reviewFolder: () => reviewFolder
|
|
1193
|
+
});
|
|
1194
|
+
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
1195
|
+
import { stdin as stdin2, stdout as stdout2 } from "node:process";
|
|
1196
|
+
function isAbortError(err) {
|
|
1197
|
+
return err instanceof Error && err.code === "ABORT_ERR";
|
|
1198
|
+
}
|
|
1199
|
+
async function confirmProceed(message) {
|
|
1200
|
+
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
1201
|
+
try {
|
|
1202
|
+
while (true) {
|
|
1203
|
+
const answer = await rl.question(message ?? " Proceed? [Y/n] ");
|
|
1204
|
+
const a = answer.trim().toLowerCase();
|
|
1205
|
+
if (a === "" || a === "y" || a === "yes") return true;
|
|
1206
|
+
if (a === "n" || a === "no") return false;
|
|
1207
|
+
console.log(
|
|
1208
|
+
` Unknown option "${answer.trim()}". Valid: y/yes, n/no, or Enter for yes.`
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
} catch (err) {
|
|
1212
|
+
if (isAbortError(err)) throw new SyncCancelledError();
|
|
1213
|
+
throw err;
|
|
1214
|
+
} finally {
|
|
1215
|
+
rl.close();
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
async function promptChoice(message, options) {
|
|
1219
|
+
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
1220
|
+
try {
|
|
1221
|
+
const answer = await rl.question(message);
|
|
1222
|
+
const a = answer.trim().toLowerCase();
|
|
1223
|
+
return options.includes(a) ? a : options[0];
|
|
1224
|
+
} catch (err) {
|
|
1225
|
+
if (isAbortError(err)) throw new SyncCancelledError();
|
|
1226
|
+
throw err;
|
|
1227
|
+
} finally {
|
|
1228
|
+
rl.close();
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
async function confirmEach(items, label) {
|
|
1232
|
+
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
1233
|
+
const accepted = [];
|
|
1234
|
+
try {
|
|
1235
|
+
for (const item of items) {
|
|
1236
|
+
const answer = await rl.question(` ${label(item)} \u2014 delete? [y/n/a] `);
|
|
1237
|
+
const a = answer.trim().toLowerCase();
|
|
1238
|
+
if (a === "a") {
|
|
1239
|
+
accepted.push(item, ...items.slice(items.indexOf(item) + 1));
|
|
1240
|
+
break;
|
|
1241
|
+
}
|
|
1242
|
+
if (a === "y" || a === "yes" || a === "") {
|
|
1243
|
+
accepted.push(item);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
} catch (err) {
|
|
1247
|
+
if (isAbortError(err)) throw new SyncCancelledError();
|
|
1248
|
+
throw err;
|
|
1249
|
+
} finally {
|
|
1250
|
+
rl.close();
|
|
1251
|
+
}
|
|
1252
|
+
return accepted;
|
|
1253
|
+
}
|
|
1254
|
+
function parseReviewAction(input) {
|
|
1255
|
+
const a = input.trim().toLowerCase();
|
|
1256
|
+
switch (a) {
|
|
1257
|
+
case "d":
|
|
1258
|
+
case "delete":
|
|
1259
|
+
return { action: "delete", all: false, special: null };
|
|
1260
|
+
case "p":
|
|
1261
|
+
case "pull":
|
|
1262
|
+
return { action: "pull", all: false, special: null };
|
|
1263
|
+
case "s":
|
|
1264
|
+
case "push":
|
|
1265
|
+
return { action: "push", all: false, special: null };
|
|
1266
|
+
case "k":
|
|
1267
|
+
case "skip":
|
|
1268
|
+
return { action: "skip", all: false, special: null };
|
|
1269
|
+
case "da":
|
|
1270
|
+
return { action: "delete", all: true, special: null };
|
|
1271
|
+
case "pa":
|
|
1272
|
+
return { action: "pull", all: true, special: null };
|
|
1273
|
+
case "sa":
|
|
1274
|
+
return { action: "push", all: true, special: null };
|
|
1275
|
+
case "ka":
|
|
1276
|
+
return { action: "skip", all: true, special: null };
|
|
1277
|
+
case "v":
|
|
1278
|
+
case "view":
|
|
1279
|
+
return { action: null, all: false, special: "view" };
|
|
1280
|
+
case "r":
|
|
1281
|
+
case "recommended":
|
|
1282
|
+
return { action: null, all: false, special: "recommended" };
|
|
1283
|
+
case "":
|
|
1284
|
+
return { action: null, all: false, special: "recommended" };
|
|
1285
|
+
// Enter = recommended
|
|
1286
|
+
default:
|
|
1287
|
+
return { action: null, all: false, special: null };
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
function formatActionPrompt(recommended, includeView, includeRecommended) {
|
|
1291
|
+
const actions = [
|
|
1292
|
+
`[d]elete${recommended === "delete" ? "\u2605" : ""}`,
|
|
1293
|
+
`[p]ull${recommended === "pull" ? "\u2605" : ""}`,
|
|
1294
|
+
`pu[s]h${recommended === "push" ? "\u2605" : ""}`,
|
|
1295
|
+
`s[k]ip${recommended === "skip" ? "\u2605" : ""}`
|
|
1296
|
+
];
|
|
1297
|
+
if (includeView) actions.push("[v]iew");
|
|
1298
|
+
if (includeRecommended) actions.push("[r]ecommended");
|
|
1299
|
+
return actions.join(" ");
|
|
1300
|
+
}
|
|
1301
|
+
function showDiff(local, remote, displayPath) {
|
|
1302
|
+
console.log(`
|
|
1303
|
+
--- ${displayPath} (diff) ---`);
|
|
1304
|
+
if (local === null && remote !== null) {
|
|
1305
|
+
console.log(" (no local file \u2014 remote content below)");
|
|
1306
|
+
for (const line of remote.split("\n").slice(0, 30)) {
|
|
1307
|
+
console.log(` + ${line}`);
|
|
1308
|
+
}
|
|
1309
|
+
if (remote.split("\n").length > 30) console.log(" ... (truncated)");
|
|
1310
|
+
} else if (local !== null && remote === null) {
|
|
1311
|
+
console.log(" (no remote file \u2014 local content below)");
|
|
1312
|
+
for (const line of local.split("\n").slice(0, 30)) {
|
|
1313
|
+
console.log(` - ${line}`);
|
|
1314
|
+
}
|
|
1315
|
+
if (local.split("\n").length > 30) console.log(" ... (truncated)");
|
|
1316
|
+
} else if (local !== null && remote !== null) {
|
|
1317
|
+
const localLines = local.split("\n");
|
|
1318
|
+
const remoteLines = remote.split("\n");
|
|
1319
|
+
let shown = 0;
|
|
1320
|
+
const maxLines = 40;
|
|
1321
|
+
for (let i = 0; i < Math.max(localLines.length, remoteLines.length) && shown < maxLines; i++) {
|
|
1322
|
+
const l = localLines[i];
|
|
1323
|
+
const r = remoteLines[i];
|
|
1324
|
+
if (l === r) {
|
|
1325
|
+
console.log(` ${l ?? ""}`);
|
|
1326
|
+
} else {
|
|
1327
|
+
if (l !== void 0) console.log(` - ${l}`);
|
|
1328
|
+
if (r !== void 0) console.log(` + ${r}`);
|
|
1329
|
+
}
|
|
1330
|
+
shown++;
|
|
1331
|
+
}
|
|
1332
|
+
if (Math.max(localLines.length, remoteLines.length) > maxLines) {
|
|
1333
|
+
console.log(" ... (truncated)");
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
console.log();
|
|
1337
|
+
}
|
|
1338
|
+
async function promptReviewMode() {
|
|
1339
|
+
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
1340
|
+
try {
|
|
1341
|
+
while (true) {
|
|
1342
|
+
const answer = await rl.question(
|
|
1343
|
+
" Review [o]ne-by-one or [f]older-by-folder? "
|
|
1344
|
+
);
|
|
1345
|
+
const a = answer.trim().toLowerCase();
|
|
1346
|
+
if (a === "o" || a === "one-by-one" || a === "one" || a === "file")
|
|
1347
|
+
return "file";
|
|
1348
|
+
if (a === "f" || a === "folder") return "folder";
|
|
1349
|
+
console.log(
|
|
1350
|
+
` Unknown option "${answer.trim()}". Valid: o/one-by-one, f/folder`
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
} catch (err) {
|
|
1354
|
+
if (isAbortError(err)) throw new SyncCancelledError();
|
|
1355
|
+
throw err;
|
|
1356
|
+
} finally {
|
|
1357
|
+
rl.close();
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
async function reviewFilesOneByOne(items, label, plannedAction, recommendedAction, content) {
|
|
1361
|
+
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
1362
|
+
const results = [];
|
|
1363
|
+
try {
|
|
1364
|
+
let applyAll = null;
|
|
1365
|
+
for (const item of items) {
|
|
1366
|
+
if (applyAll) {
|
|
1367
|
+
results.push(applyAll);
|
|
1368
|
+
continue;
|
|
1369
|
+
}
|
|
1370
|
+
const planned = plannedAction(item);
|
|
1371
|
+
const rec = recommendedAction ? recommendedAction(item) : planned;
|
|
1372
|
+
const hasContent = content != null;
|
|
1373
|
+
const prompt = ` ${label(item)} (${planned}) \u2014 ${formatActionPrompt(rec, hasContent, false)}: `;
|
|
1374
|
+
while (true) {
|
|
1375
|
+
const answer = await rl.question(prompt);
|
|
1376
|
+
const result = parseReviewAction(answer);
|
|
1377
|
+
if (result.special === "view") {
|
|
1378
|
+
if (content) {
|
|
1379
|
+
showDiff(content.local(item), content.remote(item), label(item));
|
|
1380
|
+
} else {
|
|
1381
|
+
console.log(" No content available for diff.");
|
|
1382
|
+
}
|
|
1383
|
+
continue;
|
|
1384
|
+
}
|
|
1385
|
+
if (result.special === "recommended") {
|
|
1386
|
+
results.push(rec);
|
|
1387
|
+
break;
|
|
1388
|
+
}
|
|
1389
|
+
if (result.action === null) {
|
|
1390
|
+
console.log(
|
|
1391
|
+
` Unknown option "${answer.trim()}". Valid: ${formatActionPrompt(rec, hasContent, false)}`
|
|
1392
|
+
);
|
|
1393
|
+
continue;
|
|
1394
|
+
}
|
|
1395
|
+
results.push(result.action);
|
|
1396
|
+
if (result.all) applyAll = result.action;
|
|
1397
|
+
break;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
} catch (err) {
|
|
1401
|
+
if (isAbortError(err)) throw new SyncCancelledError();
|
|
1402
|
+
throw err;
|
|
1403
|
+
} finally {
|
|
1404
|
+
rl.close();
|
|
1405
|
+
}
|
|
1406
|
+
return results;
|
|
1407
|
+
}
|
|
1408
|
+
async function reviewFolder(folderName, items, label, plannedAction, recommendedAction, content) {
|
|
1409
|
+
console.log(`
|
|
1410
|
+
${folderName} (${items.length} files):`);
|
|
1411
|
+
for (const item of items) {
|
|
1412
|
+
const rec = recommendedAction ? recommendedAction(item) : plannedAction(item);
|
|
1413
|
+
const actionLabel = plannedAction(item);
|
|
1414
|
+
const star = actionLabel === rec ? "\u2605" : "";
|
|
1415
|
+
console.log(` ${label(item)} (${actionLabel}${star})`);
|
|
1416
|
+
}
|
|
1417
|
+
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
1418
|
+
try {
|
|
1419
|
+
while (true) {
|
|
1420
|
+
const promptStr = ` Action for all: ${formatActionPrompt(
|
|
1421
|
+
recommendedAction ? recommendedAction(items[0]) : plannedAction(items[0]),
|
|
1422
|
+
false,
|
|
1423
|
+
true
|
|
1424
|
+
)} [o]ne-by-one: `;
|
|
1425
|
+
const answer = await rl.question(promptStr);
|
|
1426
|
+
const a = answer.trim().toLowerCase();
|
|
1427
|
+
if (a === "o" || a === "one-by-one") {
|
|
1428
|
+
rl.close();
|
|
1429
|
+
return reviewFilesOneByOne(
|
|
1430
|
+
items,
|
|
1431
|
+
label,
|
|
1432
|
+
plannedAction,
|
|
1433
|
+
recommendedAction,
|
|
1434
|
+
content
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
if (a === "r" || a === "recommended") {
|
|
1438
|
+
return items.map(
|
|
1439
|
+
(item) => recommendedAction ? recommendedAction(item) : plannedAction(item)
|
|
1440
|
+
);
|
|
1441
|
+
}
|
|
1442
|
+
if (a === "v" || a === "view") {
|
|
1443
|
+
if (content) {
|
|
1444
|
+
for (const item of items) {
|
|
1445
|
+
showDiff(content.local(item), content.remote(item), label(item));
|
|
1446
|
+
}
|
|
1447
|
+
} else {
|
|
1448
|
+
console.log(" No content available for diff.");
|
|
1449
|
+
}
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
const result = parseReviewAction(a);
|
|
1453
|
+
if (result.action !== null) {
|
|
1454
|
+
return items.map(() => result.action);
|
|
1455
|
+
}
|
|
1456
|
+
console.log(
|
|
1457
|
+
` Unknown option "${answer.trim()}". Valid: ${formatActionPrompt(
|
|
1458
|
+
recommendedAction ? recommendedAction(items[0]) : plannedAction(items[0]),
|
|
1459
|
+
false,
|
|
1460
|
+
true
|
|
1461
|
+
)} [o]ne-by-one`
|
|
1462
|
+
);
|
|
1463
|
+
}
|
|
1464
|
+
} catch (err) {
|
|
1465
|
+
if (isAbortError(err)) throw new SyncCancelledError();
|
|
1466
|
+
throw err;
|
|
1467
|
+
} finally {
|
|
1468
|
+
rl.close();
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
var SyncCancelledError;
|
|
1472
|
+
var init_confirm = __esm({
|
|
1473
|
+
"src/cli/confirm.ts"() {
|
|
1474
|
+
"use strict";
|
|
1475
|
+
SyncCancelledError = class extends Error {
|
|
1476
|
+
constructor() {
|
|
1477
|
+
super("Sync cancelled");
|
|
1478
|
+
this.name = "SyncCancelledError";
|
|
1479
|
+
}
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
// src/lib/tech-detect.ts
|
|
1485
|
+
import { readFile as readFile6, access, readdir as readdir4 } from "node:fs/promises";
|
|
1486
|
+
import { join as join6, relative } from "node:path";
|
|
1487
|
+
async function fileExists(filePath) {
|
|
1488
|
+
try {
|
|
1489
|
+
await access(filePath);
|
|
1490
|
+
return true;
|
|
1491
|
+
} catch {
|
|
1492
|
+
return false;
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
async function discoverMonorepoApps(projectPath) {
|
|
1496
|
+
const apps = [];
|
|
1497
|
+
const patterns = [];
|
|
1498
|
+
try {
|
|
1499
|
+
const raw = await readFile6(
|
|
1500
|
+
join6(projectPath, "pnpm-workspace.yaml"),
|
|
1501
|
+
"utf-8"
|
|
1502
|
+
);
|
|
1503
|
+
const matches = raw.match(/^\s*-\s*['"]?([^'"#\n]+)['"]?/gm);
|
|
1504
|
+
if (matches) {
|
|
1505
|
+
for (const m of matches) {
|
|
1506
|
+
const pattern = m.replace(/^\s*-\s*['"]?/, "").replace(/['"]?\s*$/, "").trim();
|
|
1507
|
+
if (pattern) patterns.push(pattern);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
} catch {
|
|
1511
|
+
}
|
|
1512
|
+
if (patterns.length === 0) {
|
|
1513
|
+
try {
|
|
1514
|
+
const raw = await readFile6(join6(projectPath, "package.json"), "utf-8");
|
|
1515
|
+
const pkg = JSON.parse(raw);
|
|
1516
|
+
const ws = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages;
|
|
1517
|
+
if (ws) patterns.push(...ws);
|
|
1518
|
+
} catch {
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
for (const pattern of patterns) {
|
|
1522
|
+
if (pattern.endsWith("/*")) {
|
|
1523
|
+
const dir = pattern.slice(0, -2);
|
|
1524
|
+
const absDir = join6(projectPath, dir);
|
|
1525
|
+
try {
|
|
1526
|
+
const entries = await readdir4(absDir, { withFileTypes: true });
|
|
1527
|
+
for (const entry of entries) {
|
|
1528
|
+
if (entry.isDirectory()) {
|
|
1529
|
+
const relPath = join6(dir, entry.name);
|
|
1530
|
+
const absPath = join6(absDir, entry.name);
|
|
1531
|
+
if (await fileExists(join6(absPath, "package.json"))) {
|
|
1532
|
+
apps.push({ name: entry.name, path: relPath, absPath });
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
} catch {
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
return apps;
|
|
1541
|
+
}
|
|
1542
|
+
async function detectFromDirectory(dirPath) {
|
|
1543
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1544
|
+
try {
|
|
1545
|
+
const raw = await readFile6(join6(dirPath, "package.json"), "utf-8");
|
|
1546
|
+
const pkg = JSON.parse(raw);
|
|
1547
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1548
|
+
for (const depName of Object.keys(allDeps)) {
|
|
1549
|
+
const rule = PACKAGE_MAP[depName];
|
|
1550
|
+
if (rule) {
|
|
1551
|
+
const key = rule.name.toLowerCase();
|
|
1552
|
+
if (!seen.has(key)) {
|
|
1553
|
+
seen.set(key, { name: rule.name, category: rule.category });
|
|
1554
|
+
}
|
|
1555
|
+
continue;
|
|
1556
|
+
}
|
|
1557
|
+
for (const { prefix, rule: prefixRule } of PACKAGE_PREFIX_MAP) {
|
|
1558
|
+
if (depName.startsWith(prefix)) {
|
|
1559
|
+
const key = prefixRule.name.toLowerCase();
|
|
1560
|
+
if (!seen.has(key)) {
|
|
1561
|
+
seen.set(key, {
|
|
1562
|
+
name: prefixRule.name,
|
|
1563
|
+
category: prefixRule.category
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
break;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
} catch {
|
|
1571
|
+
}
|
|
1572
|
+
for (const { file, rule } of CONFIG_FILE_MAP) {
|
|
1573
|
+
const key = rule.name.toLowerCase();
|
|
1574
|
+
if (!seen.has(key) && await fileExists(join6(dirPath, file))) {
|
|
1575
|
+
seen.set(key, { name: rule.name, category: rule.category });
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
return Array.from(seen.values()).sort((a, b) => {
|
|
1579
|
+
const catCmp = a.category.localeCompare(b.category);
|
|
1580
|
+
if (catCmp !== 0) return catCmp;
|
|
1581
|
+
return a.name.localeCompare(b.name);
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
async function detectTechStack(projectPath) {
|
|
1585
|
+
const repo = await detectFromDirectory(projectPath);
|
|
1586
|
+
const discoveredApps = await discoverMonorepoApps(projectPath);
|
|
1587
|
+
const apps = [];
|
|
1588
|
+
for (const app of discoveredApps) {
|
|
1589
|
+
const stack = await detectFromDirectory(app.absPath);
|
|
1590
|
+
if (stack.length > 0) {
|
|
1591
|
+
apps.push({ name: app.name, path: app.path, stack });
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
const flatMap = /* @__PURE__ */ new Map();
|
|
1595
|
+
for (const entry of repo) {
|
|
1596
|
+
flatMap.set(entry.name.toLowerCase(), entry);
|
|
1597
|
+
}
|
|
1598
|
+
for (const app of apps) {
|
|
1599
|
+
for (const entry of app.stack) {
|
|
1600
|
+
const key = entry.name.toLowerCase();
|
|
1601
|
+
if (!flatMap.has(key)) {
|
|
1602
|
+
flatMap.set(key, entry);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
const flat = Array.from(flatMap.values()).sort((a, b) => {
|
|
1607
|
+
const catCmp = a.category.localeCompare(b.category);
|
|
1608
|
+
if (catCmp !== 0) return catCmp;
|
|
1609
|
+
return a.name.localeCompare(b.name);
|
|
1610
|
+
});
|
|
1611
|
+
return { repo, apps, flat };
|
|
1612
|
+
}
|
|
1613
|
+
function mergeTechStack(remote, detected) {
|
|
1614
|
+
const remoteResult = Array.isArray(remote) ? { repo: remote, apps: [], flat: remote } : remote;
|
|
1615
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1616
|
+
for (const entry of remoteResult.flat) {
|
|
1617
|
+
seen.set(entry.name.toLowerCase(), entry);
|
|
1618
|
+
}
|
|
1619
|
+
const added = [];
|
|
1620
|
+
for (const entry of detected.flat) {
|
|
1621
|
+
const key = entry.name.toLowerCase();
|
|
1622
|
+
if (!seen.has(key)) {
|
|
1623
|
+
seen.set(key, entry);
|
|
1624
|
+
added.push(entry);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
const flat = Array.from(seen.values()).sort((a, b) => {
|
|
1628
|
+
const catCmp = a.category.localeCompare(b.category);
|
|
1629
|
+
if (catCmp !== 0) return catCmp;
|
|
1630
|
+
return a.name.localeCompare(b.name);
|
|
1631
|
+
});
|
|
1632
|
+
const merged = {
|
|
1633
|
+
repo: detected.repo,
|
|
1634
|
+
apps: detected.apps,
|
|
1635
|
+
flat
|
|
1636
|
+
};
|
|
1637
|
+
return { merged, added };
|
|
1638
|
+
}
|
|
1639
|
+
function parseTechStack(raw) {
|
|
1640
|
+
if (Array.isArray(raw)) {
|
|
1641
|
+
return raw.filter(
|
|
1642
|
+
(item) => typeof item === "object" && item !== null && typeof item.name === "string" && typeof item.category === "string"
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
if (typeof raw === "object" && raw !== null && "flat" in raw) {
|
|
1646
|
+
return parseTechStack(raw.flat);
|
|
1647
|
+
}
|
|
1648
|
+
return [];
|
|
1649
|
+
}
|
|
1650
|
+
function parseAppTechStacks(raw) {
|
|
1651
|
+
if (!Array.isArray(raw)) return [];
|
|
1652
|
+
return raw.filter(
|
|
1653
|
+
(item) => typeof item === "object" && item !== null && typeof item.name === "string" && typeof item.path === "string" && Array.isArray(item.stack)
|
|
1654
|
+
).map((item) => ({
|
|
1655
|
+
name: item.name,
|
|
1656
|
+
path: item.path,
|
|
1657
|
+
stack: parseTechStack(item.stack)
|
|
1658
|
+
}));
|
|
1659
|
+
}
|
|
1660
|
+
function parseTechStackResult(raw) {
|
|
1661
|
+
if (typeof raw === "object" && raw !== null && !Array.isArray(raw) && "flat" in raw) {
|
|
1662
|
+
const obj = raw;
|
|
1663
|
+
return {
|
|
1664
|
+
repo: parseTechStack(obj.repo),
|
|
1665
|
+
apps: parseAppTechStacks(obj.apps),
|
|
1666
|
+
flat: parseTechStack(obj.flat)
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
const flat = parseTechStack(raw);
|
|
1670
|
+
return { repo: flat, apps: [], flat };
|
|
1671
|
+
}
|
|
1672
|
+
function categorizeDependency(depName) {
|
|
1673
|
+
const rule = PACKAGE_MAP[depName];
|
|
1674
|
+
if (rule) return rule.category;
|
|
1675
|
+
for (const { prefix, rule: prefixRule } of PACKAGE_PREFIX_MAP) {
|
|
1676
|
+
if (depName.startsWith(prefix)) return prefixRule.category;
|
|
1677
|
+
}
|
|
1678
|
+
return "other";
|
|
1679
|
+
}
|
|
1680
|
+
async function findPackageJsonFiles(dir, projectPath, depth = 0) {
|
|
1681
|
+
if (depth > 4) return [];
|
|
1682
|
+
const results = [];
|
|
1683
|
+
const pkgPath = join6(dir, "package.json");
|
|
1684
|
+
if (await fileExists(pkgPath)) {
|
|
1685
|
+
results.push(pkgPath);
|
|
1686
|
+
}
|
|
1687
|
+
try {
|
|
1688
|
+
const entries = await readdir4(dir, { withFileTypes: true });
|
|
1689
|
+
for (const entry of entries) {
|
|
1690
|
+
if (!entry.isDirectory() || SKIP_DIRS.has(entry.name)) continue;
|
|
1691
|
+
const subResults = await findPackageJsonFiles(
|
|
1692
|
+
join6(dir, entry.name),
|
|
1693
|
+
projectPath,
|
|
1694
|
+
depth + 1
|
|
1695
|
+
);
|
|
1696
|
+
results.push(...subResults);
|
|
1697
|
+
}
|
|
1698
|
+
} catch {
|
|
1699
|
+
}
|
|
1700
|
+
return results;
|
|
1701
|
+
}
|
|
1702
|
+
async function scanAllDependencies(projectPath) {
|
|
1703
|
+
const packageJsonPaths = await findPackageJsonFiles(projectPath, projectPath);
|
|
1704
|
+
const dependencies = [];
|
|
1705
|
+
for (const pkgPath of packageJsonPaths) {
|
|
1706
|
+
try {
|
|
1707
|
+
const raw = await readFile6(pkgPath, "utf-8");
|
|
1708
|
+
const pkg = JSON.parse(raw);
|
|
1709
|
+
const sourcePath = relative(projectPath, pkgPath);
|
|
1710
|
+
const depSections = [
|
|
1711
|
+
{ deps: pkg.dependencies, depType: "production", isDev: false },
|
|
1712
|
+
{ deps: pkg.devDependencies, depType: "dev", isDev: true },
|
|
1713
|
+
{ deps: pkg.peerDependencies, depType: "peer", isDev: false },
|
|
1714
|
+
{ deps: pkg.optionalDependencies, depType: "optional", isDev: false }
|
|
1715
|
+
];
|
|
1716
|
+
for (const { deps, depType, isDev } of depSections) {
|
|
1717
|
+
if (!deps) continue;
|
|
1718
|
+
for (const [name, version] of Object.entries(deps)) {
|
|
1719
|
+
dependencies.push({
|
|
1720
|
+
name,
|
|
1721
|
+
version,
|
|
1722
|
+
category: categorizeDependency(name),
|
|
1723
|
+
source_path: sourcePath,
|
|
1724
|
+
is_dev: isDev,
|
|
1725
|
+
dep_type: depType
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
} catch {
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
return { dependencies };
|
|
1733
|
+
}
|
|
1734
|
+
var PACKAGE_MAP, PACKAGE_PREFIX_MAP, CONFIG_FILE_MAP, SKIP_DIRS;
|
|
1735
|
+
var init_tech_detect = __esm({
|
|
1736
|
+
"src/lib/tech-detect.ts"() {
|
|
1737
|
+
"use strict";
|
|
1738
|
+
PACKAGE_MAP = {
|
|
1739
|
+
// Frameworks
|
|
1740
|
+
next: { name: "Next.js", category: "framework" },
|
|
1741
|
+
nuxt: { name: "Nuxt", category: "framework" },
|
|
1742
|
+
gatsby: { name: "Gatsby", category: "framework" },
|
|
1743
|
+
express: { name: "Express", category: "framework" },
|
|
1744
|
+
fastify: { name: "Fastify", category: "framework" },
|
|
1745
|
+
hono: { name: "Hono", category: "framework" },
|
|
1746
|
+
"@remix-run/node": { name: "Remix", category: "framework" },
|
|
1747
|
+
svelte: { name: "Svelte", category: "framework" },
|
|
1748
|
+
astro: { name: "Astro", category: "framework" },
|
|
1749
|
+
"@angular/core": { name: "Angular", category: "framework" },
|
|
1750
|
+
// Libraries (UI)
|
|
1751
|
+
react: { name: "React", category: "framework" },
|
|
1752
|
+
vue: { name: "Vue", category: "framework" },
|
|
1753
|
+
"solid-js": { name: "Solid", category: "framework" },
|
|
1754
|
+
preact: { name: "Preact", category: "framework" },
|
|
1755
|
+
// Languages (detected via devDeps)
|
|
1756
|
+
typescript: { name: "TypeScript", category: "language" },
|
|
1757
|
+
// Styling
|
|
1758
|
+
tailwindcss: { name: "Tailwind CSS", category: "styling" },
|
|
1759
|
+
sass: { name: "SCSS", category: "styling" },
|
|
1760
|
+
"styled-components": { name: "styled-components", category: "styling" },
|
|
1761
|
+
"@emotion/react": { name: "Emotion", category: "styling" },
|
|
1762
|
+
// Database
|
|
1763
|
+
prisma: { name: "Prisma", category: "database" },
|
|
1764
|
+
"@prisma/client": { name: "Prisma", category: "database" },
|
|
1765
|
+
"drizzle-orm": { name: "Drizzle", category: "database" },
|
|
1766
|
+
"@supabase/supabase-js": { name: "Supabase", category: "database" },
|
|
1767
|
+
mongoose: { name: "MongoDB", category: "database" },
|
|
1768
|
+
typeorm: { name: "TypeORM", category: "database" },
|
|
1769
|
+
knex: { name: "Knex", category: "database" },
|
|
1770
|
+
// Testing
|
|
1771
|
+
jest: { name: "Jest", category: "testing" },
|
|
1772
|
+
vitest: { name: "Vitest", category: "testing" },
|
|
1773
|
+
mocha: { name: "Mocha", category: "testing" },
|
|
1774
|
+
playwright: { name: "Playwright", category: "testing" },
|
|
1775
|
+
"@playwright/test": { name: "Playwright", category: "testing" },
|
|
1776
|
+
cypress: { name: "Cypress", category: "testing" },
|
|
1777
|
+
supertest: { name: "Supertest", category: "testing" },
|
|
1778
|
+
// Build tools
|
|
1779
|
+
turbo: { name: "Turborepo", category: "build" },
|
|
1780
|
+
vite: { name: "Vite", category: "build" },
|
|
1781
|
+
webpack: { name: "Webpack", category: "build" },
|
|
1782
|
+
esbuild: { name: "esbuild", category: "build" },
|
|
1783
|
+
rollup: { name: "Rollup", category: "build" },
|
|
1784
|
+
nx: { name: "Nx", category: "build" },
|
|
1785
|
+
lerna: { name: "Lerna", category: "build" },
|
|
1786
|
+
tsup: { name: "tsup", category: "build" },
|
|
1787
|
+
"@swc/core": { name: "SWC", category: "build" },
|
|
1788
|
+
parcel: { name: "Parcel", category: "build" },
|
|
1789
|
+
// Tools
|
|
1790
|
+
eslint: { name: "ESLint", category: "tool" },
|
|
1791
|
+
prettier: { name: "Prettier", category: "tool" },
|
|
1792
|
+
"@biomejs/biome": { name: "Biome", category: "tool" },
|
|
1793
|
+
storybook: { name: "Storybook", category: "tool" },
|
|
1794
|
+
// Component libs
|
|
1795
|
+
"@mui/material": { name: "MUI", category: "component-lib" },
|
|
1796
|
+
"@chakra-ui/react": { name: "Chakra UI", category: "component-lib" },
|
|
1797
|
+
"@mantine/core": { name: "Mantine", category: "component-lib" },
|
|
1798
|
+
// GraphQL
|
|
1799
|
+
graphql: { name: "GraphQL", category: "graphql" },
|
|
1800
|
+
"@apollo/client": { name: "Apollo Client", category: "graphql" },
|
|
1801
|
+
urql: { name: "urql", category: "graphql" },
|
|
1802
|
+
"graphql-request": { name: "graphql-request", category: "graphql" },
|
|
1803
|
+
// Documentation
|
|
1804
|
+
typedoc: { name: "TypeDoc", category: "documentation" },
|
|
1805
|
+
"@docusaurus/core": { name: "Docusaurus", category: "documentation" },
|
|
1806
|
+
vitepress: { name: "VitePress", category: "documentation" },
|
|
1807
|
+
// Code quality
|
|
1808
|
+
husky: { name: "Husky", category: "quality" },
|
|
1809
|
+
"lint-staged": { name: "lint-staged", category: "quality" },
|
|
1810
|
+
commitlint: { name: "commitlint", category: "quality" },
|
|
1811
|
+
"@commitlint/cli": { name: "commitlint", category: "quality" },
|
|
1812
|
+
// Mobile
|
|
1813
|
+
"react-native": { name: "React Native", category: "mobile" },
|
|
1814
|
+
expo: { name: "Expo", category: "mobile" }
|
|
1815
|
+
};
|
|
1816
|
+
PACKAGE_PREFIX_MAP = [
|
|
1817
|
+
{
|
|
1818
|
+
prefix: "@radix-ui/",
|
|
1819
|
+
rule: { name: "Radix UI", category: "component-lib" }
|
|
1820
|
+
},
|
|
1821
|
+
{ prefix: "@storybook/", rule: { name: "Storybook", category: "tool" } },
|
|
1822
|
+
{
|
|
1823
|
+
prefix: "@testing-library/",
|
|
1824
|
+
rule: { name: "Testing Library", category: "testing" }
|
|
1825
|
+
}
|
|
1826
|
+
];
|
|
1827
|
+
CONFIG_FILE_MAP = [
|
|
1828
|
+
{ file: "tsconfig.json", rule: { name: "TypeScript", category: "language" } },
|
|
1829
|
+
{ file: "next.config.js", rule: { name: "Next.js", category: "framework" } },
|
|
1830
|
+
{ file: "next.config.mjs", rule: { name: "Next.js", category: "framework" } },
|
|
1831
|
+
{ file: "next.config.ts", rule: { name: "Next.js", category: "framework" } },
|
|
1832
|
+
{
|
|
1833
|
+
file: "tailwind.config.js",
|
|
1834
|
+
rule: { name: "Tailwind CSS", category: "styling" }
|
|
1835
|
+
},
|
|
1836
|
+
{
|
|
1837
|
+
file: "tailwind.config.ts",
|
|
1838
|
+
rule: { name: "Tailwind CSS", category: "styling" }
|
|
1839
|
+
},
|
|
1840
|
+
{ file: "turbo.json", rule: { name: "Turborepo", category: "build" } },
|
|
1841
|
+
{
|
|
1842
|
+
file: "docker-compose.yml",
|
|
1843
|
+
rule: { name: "Docker", category: "deployment" }
|
|
1844
|
+
},
|
|
1845
|
+
{
|
|
1846
|
+
file: "docker-compose.yaml",
|
|
1847
|
+
rule: { name: "Docker", category: "deployment" }
|
|
1848
|
+
},
|
|
1849
|
+
{ file: "Dockerfile", rule: { name: "Docker", category: "deployment" } },
|
|
1850
|
+
{ file: "vercel.json", rule: { name: "Vercel", category: "deployment" } },
|
|
1851
|
+
{ file: ".storybook/main.js", rule: { name: "Storybook", category: "tool" } },
|
|
1852
|
+
{ file: ".storybook/main.ts", rule: { name: "Storybook", category: "tool" } },
|
|
1853
|
+
{
|
|
1854
|
+
file: ".storybook/main.mjs",
|
|
1855
|
+
rule: { name: "Storybook", category: "tool" }
|
|
1856
|
+
},
|
|
1857
|
+
{
|
|
1858
|
+
file: "components.json",
|
|
1859
|
+
rule: { name: "shadcn/ui", category: "component-lib" }
|
|
1860
|
+
},
|
|
1861
|
+
{ file: "nx.json", rule: { name: "Nx", category: "build" } },
|
|
1862
|
+
{ file: "lerna.json", rule: { name: "Lerna", category: "build" } }
|
|
1863
|
+
];
|
|
1864
|
+
SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
1865
|
+
"node_modules",
|
|
1866
|
+
".next",
|
|
1867
|
+
"dist",
|
|
1868
|
+
".turbo",
|
|
1869
|
+
".git",
|
|
1870
|
+
"coverage",
|
|
1871
|
+
"build",
|
|
1872
|
+
"out",
|
|
1873
|
+
".vercel",
|
|
1874
|
+
".expo"
|
|
1875
|
+
]);
|
|
1876
|
+
}
|
|
1877
|
+
});
|
|
1878
|
+
|
|
1879
|
+
// src/lib/server-detect.ts
|
|
1880
|
+
function detectFramework(pkg) {
|
|
1881
|
+
const deps = pkg.dependencies ?? {};
|
|
1882
|
+
const devDeps = pkg.devDependencies ?? {};
|
|
1883
|
+
const hasDep = (name) => name in deps || name in devDeps;
|
|
1884
|
+
if (hasDep("next")) return "nextjs";
|
|
1885
|
+
if (hasDep("@tauri-apps/api") || hasDep("@tauri-apps/cli")) return "tauri";
|
|
1886
|
+
if (hasDep("expo")) return "expo";
|
|
1887
|
+
if (hasDep("vite")) return "vite";
|
|
1888
|
+
if (hasDep("express")) return "express";
|
|
1889
|
+
if (hasDep("@nestjs/core")) return "nestjs";
|
|
1890
|
+
return "custom";
|
|
1891
|
+
}
|
|
1892
|
+
function detectPortFromScripts(pkg) {
|
|
1893
|
+
const scripts = pkg.scripts;
|
|
1894
|
+
if (!scripts?.dev) return null;
|
|
1895
|
+
const parts = scripts.dev.split(/\s+/);
|
|
1896
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
1897
|
+
if (parts[i] === "--port" || parts[i] === "-p") {
|
|
1898
|
+
const next = parts[i + 1];
|
|
1899
|
+
if (next) {
|
|
1900
|
+
const port = parseInt(next, 10);
|
|
1901
|
+
if (!isNaN(port)) return port;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
return null;
|
|
1906
|
+
}
|
|
1907
|
+
var init_server_detect = __esm({
|
|
1908
|
+
"src/lib/server-detect.ts"() {
|
|
1909
|
+
"use strict";
|
|
1910
|
+
}
|
|
1911
|
+
});
|
|
1912
|
+
|
|
1913
|
+
// src/lib/port-verify.ts
|
|
1914
|
+
import { readFile as readFile7 } from "node:fs/promises";
|
|
1915
|
+
async function verifyPorts(projectPath, portAllocations) {
|
|
1916
|
+
const mismatches = [];
|
|
1917
|
+
const allocatedPorts = new Set(portAllocations.map((a) => a.port));
|
|
1918
|
+
const packageJsonPaths = await findPackageJsonFiles(projectPath, projectPath);
|
|
1919
|
+
for (const pkgPath of packageJsonPaths) {
|
|
1920
|
+
try {
|
|
1921
|
+
const raw = await readFile7(pkgPath, "utf-8");
|
|
1922
|
+
const pkg = JSON.parse(raw);
|
|
1923
|
+
const scriptPort = detectPortFromScripts(pkg);
|
|
1924
|
+
if (scriptPort !== null && !allocatedPorts.has(scriptPort)) {
|
|
1925
|
+
const relativePath = pkgPath.replace(projectPath + "/", "");
|
|
1926
|
+
const matchingAlloc = portAllocations.find(
|
|
1927
|
+
(a) => a.label === getAppLabel(relativePath)
|
|
1928
|
+
);
|
|
1929
|
+
mismatches.push({
|
|
1930
|
+
packageJsonPath: relativePath,
|
|
1931
|
+
scriptPort,
|
|
1932
|
+
allocation: matchingAlloc ?? null,
|
|
1933
|
+
reason: matchingAlloc ? `Script uses port ${scriptPort} but allocation has port ${matchingAlloc.port}` : `Port ${scriptPort} in scripts is not in any allocation`
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
} catch {
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
return mismatches;
|
|
1940
|
+
}
|
|
1941
|
+
async function findUnallocatedApps(projectPath, portAllocations) {
|
|
1942
|
+
const apps = await discoverMonorepoApps(projectPath);
|
|
1943
|
+
if (apps.length === 0) {
|
|
1944
|
+
return [];
|
|
1945
|
+
}
|
|
1946
|
+
const allocatedLabels = new Set(portAllocations.map((a) => a.label));
|
|
1947
|
+
const unallocated = [];
|
|
1948
|
+
for (const app of apps) {
|
|
1949
|
+
if (allocatedLabels.has(app.name)) continue;
|
|
1950
|
+
try {
|
|
1951
|
+
const raw = await readFile7(`${app.absPath}/package.json`, "utf-8");
|
|
1952
|
+
const pkg = JSON.parse(raw);
|
|
1953
|
+
const framework = detectFramework(pkg);
|
|
1954
|
+
const detectedPort = detectPortFromScripts(pkg);
|
|
1955
|
+
const command = `pnpm --filter ${app.name} dev`;
|
|
1956
|
+
unallocated.push({
|
|
1957
|
+
name: app.name,
|
|
1958
|
+
path: app.path,
|
|
1959
|
+
framework,
|
|
1960
|
+
detectedPort,
|
|
1961
|
+
command
|
|
1962
|
+
});
|
|
1963
|
+
} catch {
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
return unallocated;
|
|
1967
|
+
}
|
|
1968
|
+
function getAppLabel(relativePath) {
|
|
1969
|
+
const parts = relativePath.split("/");
|
|
1970
|
+
if (parts.length >= 3 && parts[0] === "apps") {
|
|
1971
|
+
return parts[1];
|
|
1972
|
+
}
|
|
1973
|
+
return "root";
|
|
1974
|
+
}
|
|
1975
|
+
var init_port_verify = __esm({
|
|
1976
|
+
"src/lib/port-verify.ts"() {
|
|
1977
|
+
"use strict";
|
|
1978
|
+
init_tech_detect();
|
|
1979
|
+
init_server_detect();
|
|
1980
|
+
}
|
|
1981
|
+
});
|
|
1982
|
+
|
|
1983
|
+
// src/cli/sync.ts
|
|
1984
|
+
var sync_exports = {};
|
|
1985
|
+
__export(sync_exports, {
|
|
1986
|
+
runSync: () => runSync
|
|
1987
|
+
});
|
|
1988
|
+
import { createHash } from "node:crypto";
|
|
1989
|
+
import { readFile as readFile8, writeFile as writeFile3, mkdir as mkdir2, chmod as chmod2, unlink as unlink2 } from "node:fs/promises";
|
|
1990
|
+
import { join as join7, dirname as dirname2 } from "node:path";
|
|
1991
|
+
function contentHash(content) {
|
|
1992
|
+
return createHash("sha256").update(content).digest("hex");
|
|
1993
|
+
}
|
|
1994
|
+
async function runSync() {
|
|
1995
|
+
const flags = parseFlags(3);
|
|
1996
|
+
const dryRun = hasFlag("dry-run", 3);
|
|
1997
|
+
const force = hasFlag("force", 3);
|
|
1998
|
+
const fix = hasFlag("fix", 3);
|
|
1999
|
+
validateApiKey();
|
|
2000
|
+
const config = await resolveConfig(flags);
|
|
2001
|
+
const { repoId, projectPath } = config;
|
|
2002
|
+
console.log(`
|
|
2003
|
+
CodeByPlan Sync`);
|
|
2004
|
+
console.log(` Repo: ${repoId}`);
|
|
2005
|
+
console.log(` Path: ${projectPath}`);
|
|
2006
|
+
if (dryRun) console.log(` Mode: dry-run`);
|
|
2007
|
+
if (force) console.log(` Mode: force`);
|
|
2008
|
+
console.log();
|
|
2009
|
+
if (!dryRun) {
|
|
2010
|
+
console.log(" Acquiring sync lock...");
|
|
2011
|
+
try {
|
|
2012
|
+
await apiPost("/sync/lock", {
|
|
2013
|
+
repo_id: repoId,
|
|
2014
|
+
locked_by: `cli-sync`,
|
|
2015
|
+
reason: "Bidirectional sync",
|
|
2016
|
+
ttl_minutes: 10
|
|
2017
|
+
});
|
|
2018
|
+
console.log(" Lock acquired.\n");
|
|
2019
|
+
} catch (lockErr) {
|
|
2020
|
+
const lockStatus = await apiGet("/sync/lock", { repo_id: repoId });
|
|
2021
|
+
if (lockStatus.data.locked && lockStatus.data.lock) {
|
|
2022
|
+
const lock = lockStatus.data.lock;
|
|
2023
|
+
console.log(
|
|
2024
|
+
` Sync locked by ${lock.locked_by} since ${lock.locked_at}.`
|
|
2025
|
+
);
|
|
2026
|
+
console.log(` Expires: ${lock.expires_at}`);
|
|
2027
|
+
console.log(` Use --force to override, or wait for lock to expire.
|
|
2028
|
+
`);
|
|
2029
|
+
if (!force) return;
|
|
2030
|
+
await apiPost("/sync/lock", {
|
|
2031
|
+
repo_id: repoId,
|
|
2032
|
+
locked_by: `cli-sync`,
|
|
2033
|
+
reason: "Bidirectional sync (forced)",
|
|
2034
|
+
ttl_minutes: 10
|
|
2035
|
+
});
|
|
2036
|
+
console.log(" Lock acquired (forced).\n");
|
|
2037
|
+
} else {
|
|
2038
|
+
throw lockErr;
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
try {
|
|
2043
|
+
await runSyncInner(repoId, projectPath, dryRun, force, fix);
|
|
2044
|
+
} finally {
|
|
2045
|
+
if (!dryRun) {
|
|
2046
|
+
try {
|
|
2047
|
+
await apiDelete("/sync/lock", { repo_id: repoId });
|
|
2048
|
+
} catch (err) {
|
|
2049
|
+
console.error(
|
|
2050
|
+
` Warning: failed to release sync lock: ${err instanceof Error ? err.message : String(err)}`
|
|
2051
|
+
);
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
|
|
2057
|
+
console.log(" Reading local and remote state...");
|
|
2058
|
+
const claudeDir = join7(projectPath, ".claude");
|
|
2059
|
+
let localFiles = /* @__PURE__ */ new Map();
|
|
2060
|
+
try {
|
|
2061
|
+
localFiles = await scanLocalFiles(claudeDir, projectPath);
|
|
2062
|
+
} catch {
|
|
2063
|
+
}
|
|
2064
|
+
const [defaultsRes, repoSyncRes, repoRes, , fileReposRes] = await Promise.all(
|
|
2065
|
+
[
|
|
2066
|
+
apiGet("/sync/defaults"),
|
|
2067
|
+
apiGet("/sync/files", { repo_id: repoId }),
|
|
2068
|
+
apiGet(`/repos/${repoId}`),
|
|
2069
|
+
apiGet("/sync/state", {
|
|
2070
|
+
repo_id: repoId
|
|
2071
|
+
}),
|
|
2072
|
+
apiGet("/sync/file-repos", {
|
|
2073
|
+
repo_id: repoId
|
|
2074
|
+
})
|
|
2075
|
+
]
|
|
2076
|
+
);
|
|
2077
|
+
const syncStartTime = Date.now();
|
|
2078
|
+
const repoData = repoRes.data;
|
|
2079
|
+
const remoteDefaults = flattenSyncData(defaultsRes.data);
|
|
2080
|
+
const remoteRepoFiles = flattenSyncData(repoSyncRes.data);
|
|
2081
|
+
const fileRepoHashes = /* @__PURE__ */ new Map();
|
|
2082
|
+
const fileRepoByClaudeFileId = /* @__PURE__ */ new Map();
|
|
2083
|
+
for (const entry of fileReposRes.data ?? []) {
|
|
2084
|
+
const baseKey = compositeKey(
|
|
2085
|
+
entry.file_type,
|
|
2086
|
+
entry.file_name,
|
|
2087
|
+
entry.file_category
|
|
2088
|
+
);
|
|
2089
|
+
const scopedKey = `${baseKey}:${entry.file_scope}`;
|
|
2090
|
+
fileRepoHashes.set(scopedKey, entry.last_synced_content_hash);
|
|
2091
|
+
if (!fileRepoHashes.has(baseKey)) {
|
|
2092
|
+
fileRepoHashes.set(baseKey, entry.last_synced_content_hash);
|
|
2093
|
+
}
|
|
2094
|
+
if (entry.claude_file_id) {
|
|
2095
|
+
fileRepoByClaudeFileId.set(
|
|
2096
|
+
entry.claude_file_id,
|
|
2097
|
+
entry.last_synced_content_hash
|
|
2098
|
+
);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
const remoteFiles = new Map([...remoteDefaults, ...remoteRepoFiles]);
|
|
2102
|
+
console.log(
|
|
2103
|
+
` Local: ${localFiles.size} files, Remote: ${remoteFiles.size} files
|
|
2104
|
+
`
|
|
2105
|
+
);
|
|
2106
|
+
const plan = [];
|
|
2107
|
+
const allKeys = /* @__PURE__ */ new Set([...localFiles.keys(), ...remoteFiles.keys()]);
|
|
2108
|
+
for (const key of allKeys) {
|
|
2109
|
+
const local = localFiles.get(key);
|
|
2110
|
+
const remote = remoteFiles.get(key);
|
|
2111
|
+
if (local && !remote) {
|
|
2112
|
+
plan.push({
|
|
2113
|
+
key,
|
|
2114
|
+
displayPath: `${local.type}/${local.category ? local.category + "/" : ""}${local.name}`,
|
|
2115
|
+
action: "push",
|
|
2116
|
+
recommended: "push",
|
|
2117
|
+
localContent: local.content,
|
|
2118
|
+
remoteContent: null,
|
|
2119
|
+
pushContent: reverseSubstituteVariables(local.content, repoData),
|
|
2120
|
+
filePath: getLocalFilePath(claudeDir, projectPath, {
|
|
2121
|
+
type: local.type,
|
|
2122
|
+
name: local.name,
|
|
2123
|
+
category: local.category
|
|
2124
|
+
}),
|
|
2125
|
+
type: local.type,
|
|
2126
|
+
name: local.name,
|
|
2127
|
+
category: local.category,
|
|
2128
|
+
scope: local.scope,
|
|
2129
|
+
isHook: local.type === "hook",
|
|
2130
|
+
claudeFileId: null
|
|
2131
|
+
});
|
|
2132
|
+
} else if (!local && remote) {
|
|
2133
|
+
const resolvedContent = substituteVariables(remote.content, repoData);
|
|
2134
|
+
const hadSyncedThisFile = remote.id ? fileRepoByClaudeFileId.has(remote.id) : fileRepoHashes.has(key);
|
|
2135
|
+
const recommended = hadSyncedThisFile ? "delete" : "pull";
|
|
2136
|
+
plan.push({
|
|
2137
|
+
key,
|
|
2138
|
+
displayPath: `${remote.type}/${remote.category ? remote.category + "/" : ""}${remote.name}`,
|
|
2139
|
+
action: recommended,
|
|
2140
|
+
recommended,
|
|
2141
|
+
localContent: null,
|
|
2142
|
+
remoteContent: resolvedContent,
|
|
2143
|
+
pushContent: null,
|
|
2144
|
+
filePath: getLocalFilePath(claudeDir, projectPath, remote),
|
|
2145
|
+
type: remote.type,
|
|
2146
|
+
name: remote.name,
|
|
2147
|
+
category: remote.category ?? null,
|
|
2148
|
+
scope: remote.scope ?? "shared",
|
|
2149
|
+
isHook: remote.type === "hook",
|
|
2150
|
+
claudeFileId: remote.id ?? null
|
|
2151
|
+
});
|
|
2152
|
+
} else if (local && remote) {
|
|
2153
|
+
const resolvedRemote = substituteVariables(remote.content, repoData);
|
|
2154
|
+
if (local.content === resolvedRemote) {
|
|
2155
|
+
continue;
|
|
2156
|
+
}
|
|
2157
|
+
const localHash = contentHash(local.content);
|
|
2158
|
+
const scopedKey = `${key}:${local.scope}`;
|
|
2159
|
+
const lastSyncedHash = fileRepoHashes.get(scopedKey) ?? fileRepoHashes.get(key) ?? null;
|
|
2160
|
+
const localChanged = lastSyncedHash ? localHash !== lastSyncedHash : true;
|
|
2161
|
+
let action;
|
|
2162
|
+
if (force) {
|
|
2163
|
+
action = "pull";
|
|
2164
|
+
} else if (!localChanged) {
|
|
2165
|
+
action = "pull";
|
|
2166
|
+
} else if (lastSyncedHash === null) {
|
|
2167
|
+
action = "conflict";
|
|
2168
|
+
} else {
|
|
2169
|
+
const remoteResolvedHash = contentHash(resolvedRemote);
|
|
2170
|
+
const remoteChanged = remoteResolvedHash !== lastSyncedHash;
|
|
2171
|
+
if (remoteChanged) {
|
|
2172
|
+
action = "conflict";
|
|
2173
|
+
} else {
|
|
2174
|
+
action = "push";
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
plan.push({
|
|
2178
|
+
key,
|
|
2179
|
+
displayPath: `${local.type}/${local.category ? local.category + "/" : ""}${local.name}`,
|
|
2180
|
+
action,
|
|
2181
|
+
recommended: action === "conflict" ? "pull" : action,
|
|
2182
|
+
localContent: local.content,
|
|
2183
|
+
remoteContent: resolvedRemote,
|
|
2184
|
+
pushContent: reverseSubstituteVariables(local.content, repoData),
|
|
2185
|
+
filePath: getLocalFilePath(claudeDir, projectPath, remote),
|
|
2186
|
+
type: local.type,
|
|
2187
|
+
name: local.name,
|
|
2188
|
+
category: local.category,
|
|
2189
|
+
scope: local.scope,
|
|
2190
|
+
isHook: local.type === "hook",
|
|
2191
|
+
claudeFileId: remote.id ?? null
|
|
2192
|
+
});
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
const pulls = plan.filter((p) => p.action === "pull");
|
|
2196
|
+
const pushes = plan.filter((p) => p.action === "push");
|
|
2197
|
+
const conflicts = plan.filter((p) => p.action === "conflict");
|
|
2198
|
+
const contentPulls = pulls.filter((p) => p.localContent !== null);
|
|
2199
|
+
const dbOnlyPull = plan.filter(
|
|
2200
|
+
(p) => p.localContent === null && p.action === "pull"
|
|
2201
|
+
);
|
|
2202
|
+
const dbOnlyDelete = plan.filter(
|
|
2203
|
+
(p) => p.localContent === null && p.action === "delete"
|
|
2204
|
+
);
|
|
2205
|
+
if (contentPulls.length > 0) {
|
|
2206
|
+
console.log(` Pull (DB \u2192 local): ${contentPulls.length}`);
|
|
2207
|
+
for (const p of contentPulls) console.log(` \u2193 ${p.displayPath}`);
|
|
2208
|
+
}
|
|
2209
|
+
if (pushes.length > 0) {
|
|
2210
|
+
console.log(` Push (local \u2192 DB): ${pushes.length}`);
|
|
2211
|
+
for (const p of pushes) console.log(` \u2191 ${p.displayPath}`);
|
|
2212
|
+
}
|
|
2213
|
+
if (dbOnlyPull.length > 0) {
|
|
2214
|
+
console.log(`
|
|
2215
|
+
DB-only (new, will pull): ${dbOnlyPull.length}`);
|
|
2216
|
+
for (const p of dbOnlyPull) console.log(` \u2193 ${p.displayPath}`);
|
|
2217
|
+
}
|
|
2218
|
+
if (dbOnlyDelete.length > 0) {
|
|
2219
|
+
console.log(
|
|
2220
|
+
`
|
|
2221
|
+
DB-only (previously synced, will delete): ${dbOnlyDelete.length}`
|
|
2222
|
+
);
|
|
2223
|
+
for (const p of dbOnlyDelete) console.log(` \u2715 ${p.displayPath}`);
|
|
2224
|
+
}
|
|
2225
|
+
if (conflicts.length > 0) {
|
|
2226
|
+
console.log(`
|
|
2227
|
+
Conflicts (both sides changed): ${conflicts.length}`);
|
|
2228
|
+
for (const p of conflicts) console.log(` \u26A0 ${p.displayPath}`);
|
|
2229
|
+
}
|
|
2230
|
+
if (contentPulls.length === 0 && pushes.length === 0 && dbOnlyPull.length === 0 && dbOnlyDelete.length === 0 && conflicts.length === 0) {
|
|
2231
|
+
console.log(" All .claude/ files in sync.");
|
|
2232
|
+
}
|
|
2233
|
+
if (plan.length > 0 && !dryRun) {
|
|
2234
|
+
if (!force) {
|
|
2235
|
+
const agreed = await confirmProceed(`
|
|
2236
|
+
Agree with sync? [Y/n] `);
|
|
2237
|
+
if (!agreed) {
|
|
2238
|
+
const mode = await promptReviewMode();
|
|
2239
|
+
const contentProvider = {
|
|
2240
|
+
local: (p) => p.localContent,
|
|
2241
|
+
remote: (p) => p.remoteContent
|
|
2242
|
+
};
|
|
2243
|
+
if (mode === "file") {
|
|
2244
|
+
const actions = await reviewFilesOneByOne(
|
|
2245
|
+
plan,
|
|
2246
|
+
(p) => p.displayPath,
|
|
2247
|
+
(p) => p.action,
|
|
2248
|
+
(p) => p.recommended,
|
|
2249
|
+
contentProvider
|
|
2250
|
+
);
|
|
2251
|
+
for (let i = 0; i < plan.length; i++) {
|
|
2252
|
+
plan[i].action = actions[i];
|
|
2253
|
+
}
|
|
2254
|
+
} else {
|
|
2255
|
+
const groups = groupByType(plan);
|
|
2256
|
+
for (const [typeName, items] of groups) {
|
|
2257
|
+
const actions = await reviewFolder(
|
|
2258
|
+
typeName,
|
|
2259
|
+
items,
|
|
2260
|
+
(p) => p.displayPath,
|
|
2261
|
+
(p) => p.action,
|
|
2262
|
+
(p) => p.recommended,
|
|
2263
|
+
contentProvider
|
|
2264
|
+
);
|
|
2265
|
+
for (let i = 0; i < items.length; i++) {
|
|
2266
|
+
items[i].action = actions[i];
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
const toPull = plan.filter((p) => p.action === "pull");
|
|
2273
|
+
const toPush = plan.filter((p) => p.action === "push");
|
|
2274
|
+
const toDelete = plan.filter((p) => p.action === "delete");
|
|
2275
|
+
const skipped = plan.filter((p) => p.action === "skip");
|
|
2276
|
+
if (toPull.length + toPush.length + toDelete.length === 0) {
|
|
2277
|
+
console.log("\n All items skipped \u2014 no changes applied.");
|
|
2278
|
+
} else {
|
|
2279
|
+
for (const p of toPull) {
|
|
2280
|
+
if (p.filePath && p.remoteContent !== null) {
|
|
2281
|
+
await mkdir2(dirname2(p.filePath), { recursive: true });
|
|
2282
|
+
await writeFile3(p.filePath, p.remoteContent, "utf-8");
|
|
2283
|
+
if (p.isHook) await chmod2(p.filePath, 493);
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
const toUpsert = toPush.filter((p) => p.pushContent !== null).map((p) => ({
|
|
2287
|
+
type: p.type,
|
|
2288
|
+
name: p.name,
|
|
2289
|
+
category: p.category,
|
|
2290
|
+
content: p.pushContent,
|
|
2291
|
+
scope: p.scope
|
|
2292
|
+
}));
|
|
2293
|
+
if (toUpsert.length > 0) {
|
|
2294
|
+
await apiPost("/sync/files", {
|
|
2295
|
+
repo_id: repoId,
|
|
2296
|
+
files: toUpsert,
|
|
2297
|
+
changed_by_repo_id: repoId
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
if (toDelete.length > 0) {
|
|
2301
|
+
const deleteKeys = toDelete.map((p) => ({
|
|
2302
|
+
type: p.type,
|
|
2303
|
+
name: p.name,
|
|
2304
|
+
category: p.category
|
|
2305
|
+
}));
|
|
2306
|
+
await apiPost("/sync/files", {
|
|
2307
|
+
repo_id: repoId,
|
|
2308
|
+
delete_keys: deleteKeys
|
|
2309
|
+
});
|
|
2310
|
+
for (const p of toDelete) {
|
|
2311
|
+
if (p.filePath) {
|
|
2312
|
+
try {
|
|
2313
|
+
await unlink2(p.filePath);
|
|
2314
|
+
} catch (err) {
|
|
2315
|
+
if (err instanceof Error && "code" in err && err.code !== "ENOENT") {
|
|
2316
|
+
console.error(
|
|
2317
|
+
` Warning: failed to delete ${p.filePath}: ${err.message}`
|
|
2318
|
+
);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
const syncDurationMs = Date.now() - syncStartTime;
|
|
2325
|
+
await apiPost("/sync/state", {
|
|
2326
|
+
repo_id: repoId,
|
|
2327
|
+
last_synced_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2328
|
+
was_skipped: skipped.length > 0,
|
|
2329
|
+
files_synced_count: toPull.length + toPush.length + toDelete.length,
|
|
2330
|
+
files_pushed: toPush.length,
|
|
2331
|
+
files_pulled: toPull.length,
|
|
2332
|
+
files_deleted: toDelete.length,
|
|
2333
|
+
files_skipped: skipped.length,
|
|
2334
|
+
sync_duration_ms: syncDurationMs,
|
|
2335
|
+
sync_version: getSyncVersion()
|
|
2336
|
+
});
|
|
2337
|
+
const syncTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2338
|
+
const fileRepoUpdates = [];
|
|
2339
|
+
for (const p of toPull) {
|
|
2340
|
+
if (p.remoteContent !== null) {
|
|
2341
|
+
fileRepoUpdates.push({
|
|
2342
|
+
claude_file_id: p.claudeFileId ?? void 0,
|
|
2343
|
+
file_type: p.type,
|
|
2344
|
+
file_name: p.name,
|
|
2345
|
+
file_category: p.category,
|
|
2346
|
+
file_scope: p.scope,
|
|
2347
|
+
last_synced_at: syncTimestamp,
|
|
2348
|
+
last_synced_content_hash: contentHash(p.remoteContent),
|
|
2349
|
+
sync_status: "synced"
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
for (const p of toPush) {
|
|
2354
|
+
if (p.localContent !== null) {
|
|
2355
|
+
fileRepoUpdates.push({
|
|
2356
|
+
claude_file_id: p.claudeFileId ?? void 0,
|
|
2357
|
+
file_type: p.type,
|
|
2358
|
+
file_name: p.name,
|
|
2359
|
+
file_category: p.category,
|
|
2360
|
+
file_scope: p.scope,
|
|
2361
|
+
last_synced_at: syncTimestamp,
|
|
2362
|
+
last_synced_content_hash: contentHash(p.localContent),
|
|
2363
|
+
sync_status: "synced"
|
|
2364
|
+
});
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
if (fileRepoUpdates.length > 0) {
|
|
2368
|
+
try {
|
|
2369
|
+
await apiPost("/sync/file-repos", {
|
|
2370
|
+
repo_id: repoId,
|
|
2371
|
+
file_repos: fileRepoUpdates
|
|
2372
|
+
});
|
|
2373
|
+
} catch {
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
console.log(
|
|
2377
|
+
`
|
|
2378
|
+
Applied: ${toPull.length} pulled, ${toPush.length} pushed, ${toDelete.length} deleted` + (skipped.length > 0 ? `, ${skipped.length} skipped` : "")
|
|
2379
|
+
);
|
|
2380
|
+
}
|
|
2381
|
+
const unresolvedConflicts = plan.filter(
|
|
2382
|
+
(p) => p.action === "conflict" || p.action === "skip" && p.localContent !== null && p.remoteContent !== null
|
|
2383
|
+
);
|
|
2384
|
+
if (unresolvedConflicts.length > 0) {
|
|
2385
|
+
let stored = 0;
|
|
2386
|
+
for (const p of unresolvedConflicts) {
|
|
2387
|
+
try {
|
|
2388
|
+
await apiPost("/sync/conflicts", {
|
|
2389
|
+
repo_id: repoId,
|
|
2390
|
+
claude_file_id: p.claudeFileId ?? void 0,
|
|
2391
|
+
file_type: p.type,
|
|
2392
|
+
file_name: p.name,
|
|
2393
|
+
file_category: p.category,
|
|
2394
|
+
file_scope: p.scope,
|
|
2395
|
+
conflict_type: "both_modified",
|
|
2396
|
+
local_content: p.localContent,
|
|
2397
|
+
remote_content: p.remoteContent
|
|
2398
|
+
});
|
|
2399
|
+
stored++;
|
|
2400
|
+
} catch (err) {
|
|
2401
|
+
console.error(`Failed to store conflict for ${p.displayPath}:`, err);
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
if (stored > 0) {
|
|
2405
|
+
console.log(
|
|
2406
|
+
`
|
|
2407
|
+
${stored} conflict(s) stored in DB for later resolution.`
|
|
2408
|
+
);
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
} else if (dryRun) {
|
|
2412
|
+
console.log("\n (dry-run \u2014 no changes)");
|
|
2413
|
+
}
|
|
2414
|
+
console.log("\n Settings sync...");
|
|
2415
|
+
await syncSettings(
|
|
2416
|
+
claudeDir,
|
|
2417
|
+
projectPath,
|
|
2418
|
+
defaultsRes.data,
|
|
2419
|
+
repoData,
|
|
2420
|
+
dryRun
|
|
2421
|
+
);
|
|
2422
|
+
console.log(" Config sync...");
|
|
2423
|
+
await syncConfig(repoId, projectPath, dryRun);
|
|
2424
|
+
console.log(" Tech stack...");
|
|
2425
|
+
await syncTechStack(repoId, projectPath, dryRun);
|
|
2426
|
+
console.log(" Port verification...");
|
|
2427
|
+
await syncPortVerification(repoId, projectPath, dryRun, fix);
|
|
2428
|
+
console.log("\n Sync complete.\n");
|
|
2429
|
+
}
|
|
2430
|
+
async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun) {
|
|
2431
|
+
const settingsPath = join7(claudeDir, "settings.json");
|
|
2432
|
+
const globalSettingsFiles = syncData.global_settings ?? [];
|
|
2433
|
+
let globalSettings = {};
|
|
2434
|
+
for (const gf of globalSettingsFiles) {
|
|
2435
|
+
const parsed = JSON.parse(
|
|
2436
|
+
substituteVariables(gf.content, repoData)
|
|
2437
|
+
);
|
|
2438
|
+
globalSettings = { ...globalSettings, ...parsed };
|
|
2439
|
+
}
|
|
2440
|
+
const repoSettingsFiles = syncData.settings ?? [];
|
|
2441
|
+
let repoSettings = {};
|
|
2442
|
+
for (const rf of repoSettingsFiles) {
|
|
2443
|
+
repoSettings = JSON.parse(
|
|
2444
|
+
substituteVariables(rf.content, repoData)
|
|
2445
|
+
);
|
|
2446
|
+
}
|
|
2447
|
+
const combinedTemplate = mergeGlobalAndRepoSettings(
|
|
2448
|
+
globalSettings,
|
|
2449
|
+
repoSettings
|
|
2450
|
+
);
|
|
2451
|
+
const hooksDir = join7(projectPath, ".claude", "hooks");
|
|
2452
|
+
const discovered = await discoverHooks(hooksDir);
|
|
2453
|
+
let localSettings = {};
|
|
2454
|
+
try {
|
|
2455
|
+
const raw = await readFile8(settingsPath, "utf-8");
|
|
2456
|
+
localSettings = JSON.parse(raw);
|
|
2457
|
+
} catch {
|
|
2458
|
+
}
|
|
2459
|
+
let merged = Object.keys(localSettings).length > 0 ? mergeSettings(combinedTemplate, localSettings) : combinedTemplate;
|
|
2460
|
+
merged = stripPermissionsAllow(merged);
|
|
2461
|
+
if (discovered.size > 0) {
|
|
2462
|
+
merged.hooks = mergeDiscoveredHooks(
|
|
2463
|
+
merged.hooks ?? {},
|
|
2464
|
+
discovered
|
|
2465
|
+
);
|
|
2466
|
+
}
|
|
2467
|
+
const mergedContent = JSON.stringify(merged, null, 2) + "\n";
|
|
2468
|
+
let currentContent = "";
|
|
2469
|
+
try {
|
|
2470
|
+
currentContent = await readFile8(settingsPath, "utf-8");
|
|
2471
|
+
} catch {
|
|
2472
|
+
}
|
|
2473
|
+
if (currentContent === mergedContent) {
|
|
2474
|
+
console.log(" Settings up to date.");
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
if (dryRun) {
|
|
2478
|
+
console.log(" Settings would be updated (dry-run).");
|
|
2479
|
+
return;
|
|
2480
|
+
}
|
|
2481
|
+
await mkdir2(dirname2(settingsPath), { recursive: true });
|
|
2482
|
+
await writeFile3(settingsPath, mergedContent, "utf-8");
|
|
2483
|
+
console.log(" Updated settings.json");
|
|
2484
|
+
}
|
|
2485
|
+
async function syncConfig(repoId, projectPath, dryRun) {
|
|
2486
|
+
const configPath = join7(projectPath, ".codebyplan.json");
|
|
2487
|
+
let currentConfig = {};
|
|
2488
|
+
try {
|
|
2489
|
+
const raw = await readFile8(configPath, "utf-8");
|
|
2490
|
+
currentConfig = JSON.parse(raw);
|
|
2491
|
+
} catch {
|
|
2492
|
+
currentConfig = { repo_id: repoId };
|
|
2493
|
+
}
|
|
2494
|
+
const repoRes = await apiGet(`/repos/${repoId}`);
|
|
2495
|
+
const repo = repoRes.data;
|
|
2496
|
+
let portAllocations = [];
|
|
2497
|
+
try {
|
|
2498
|
+
const portsRes = await apiGet(
|
|
2499
|
+
`/port-allocations`,
|
|
2500
|
+
{ repo_id: repoId }
|
|
2501
|
+
);
|
|
2502
|
+
const allAllocations = portsRes.data ?? [];
|
|
2503
|
+
const worktreeId2 = currentConfig.worktree_id;
|
|
2504
|
+
const filtered = worktreeId2 ? allAllocations.filter((a) => a.worktree_id === worktreeId2) : allAllocations.filter((a) => !a.worktree_id);
|
|
2505
|
+
const ALLOWED_FIELDS = [
|
|
2506
|
+
"id",
|
|
2507
|
+
"repo_id",
|
|
2508
|
+
"port",
|
|
2509
|
+
"label",
|
|
2510
|
+
"server_type",
|
|
2511
|
+
"auto_start",
|
|
2512
|
+
"command",
|
|
2513
|
+
"working_dir",
|
|
2514
|
+
"env_vars",
|
|
2515
|
+
"external_refs",
|
|
2516
|
+
"worktree_id",
|
|
2517
|
+
"created_at",
|
|
2518
|
+
"updated_at"
|
|
2519
|
+
];
|
|
2520
|
+
portAllocations = filtered.map((a) => {
|
|
2521
|
+
const clean = {};
|
|
2522
|
+
for (const key of ALLOWED_FIELDS) {
|
|
2523
|
+
if (key in a) clean[key] = a[key];
|
|
2524
|
+
}
|
|
2525
|
+
return clean;
|
|
2526
|
+
});
|
|
2527
|
+
} catch {
|
|
2528
|
+
}
|
|
2529
|
+
const worktreeId = currentConfig.worktree_id;
|
|
2530
|
+
const matchingAlloc = portAllocations[0];
|
|
2531
|
+
const defaultBranchConfig = {
|
|
2532
|
+
protected: ["main", "development"],
|
|
2533
|
+
integration: "development",
|
|
2534
|
+
production: "main",
|
|
2535
|
+
staging: null
|
|
2536
|
+
};
|
|
2537
|
+
const branchConfig = repo.branch_config ?? defaultBranchConfig;
|
|
2538
|
+
const newConfig = {
|
|
2539
|
+
repo_id: repoId,
|
|
2540
|
+
...worktreeId ? { worktree_id: worktreeId } : {},
|
|
2541
|
+
server_port: worktreeId && matchingAlloc ? matchingAlloc.port : repo.server_port,
|
|
2542
|
+
server_type: worktreeId && matchingAlloc ? matchingAlloc.server_type : repo.server_type,
|
|
2543
|
+
git_branch: repo.git_branch ?? "development",
|
|
2544
|
+
auto_push_enabled: repo.auto_push_enabled,
|
|
2545
|
+
branch_config: branchConfig,
|
|
2546
|
+
...portAllocations.length > 0 ? { port_allocations: portAllocations } : {}
|
|
2547
|
+
};
|
|
2548
|
+
const currentJson = JSON.stringify(currentConfig, null, 2);
|
|
2549
|
+
const newJson = JSON.stringify(newConfig, null, 2);
|
|
2550
|
+
if (currentJson === newJson) {
|
|
2551
|
+
console.log(" Config up to date.");
|
|
2552
|
+
return;
|
|
2553
|
+
}
|
|
2554
|
+
if (dryRun) {
|
|
2555
|
+
console.log(" Config would be updated (dry-run).");
|
|
2556
|
+
return;
|
|
2557
|
+
}
|
|
2558
|
+
await writeFile3(configPath, newJson + "\n", "utf-8");
|
|
2559
|
+
console.log(" Updated .codebyplan.json");
|
|
2560
|
+
}
|
|
2561
|
+
async function syncTechStack(repoId, projectPath, dryRun) {
|
|
2562
|
+
try {
|
|
2563
|
+
const { dependencies } = await scanAllDependencies(projectPath);
|
|
2564
|
+
if (dependencies.length === 0) {
|
|
2565
|
+
console.log(" No dependencies found.");
|
|
2566
|
+
return;
|
|
2567
|
+
}
|
|
2568
|
+
const sourcePaths = new Set(dependencies.map((d) => d.source_path));
|
|
2569
|
+
console.log(
|
|
2570
|
+
` ${dependencies.length} dependencies from ${sourcePaths.size} package.json file${sourcePaths.size !== 1 ? "s" : ""}`
|
|
2571
|
+
);
|
|
2572
|
+
if (!dryRun) {
|
|
2573
|
+
const result = await apiPost(`/repos/${repoId}/tech-stack`, { dependencies });
|
|
2574
|
+
if (result.data.stale_removed > 0) {
|
|
2575
|
+
console.log(
|
|
2576
|
+
` ${result.data.stale_removed} stale dependencies removed`
|
|
2577
|
+
);
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
const detected = await detectTechStack(projectPath);
|
|
2581
|
+
if (detected.flat.length > 0) {
|
|
2582
|
+
const repoRes = await apiGet(`/repos/${repoId}`);
|
|
2583
|
+
const remote = parseTechStackResult(repoRes.data.tech_stack);
|
|
2584
|
+
const { merged, added } = mergeTechStack(remote, detected);
|
|
2585
|
+
if (added.length > 0) {
|
|
2586
|
+
console.log(` ${added.length} new tech entries`);
|
|
2587
|
+
if (!dryRun) {
|
|
2588
|
+
await apiPut(`/repos/${repoId}`, { tech_stack: merged });
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
} catch {
|
|
2593
|
+
console.log(" Tech stack detection skipped.");
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
async function syncPortVerification(repoId, projectPath, dryRun, fix) {
|
|
2597
|
+
try {
|
|
2598
|
+
const portsRes = await apiGet(
|
|
2599
|
+
`/port-allocations`,
|
|
2600
|
+
{ repo_id: repoId }
|
|
2601
|
+
);
|
|
2602
|
+
const allocations = portsRes.data ?? [];
|
|
2603
|
+
if (allocations.length === 0) {
|
|
2604
|
+
console.log(" No port allocations found \u2014 skipping verification.");
|
|
2605
|
+
return;
|
|
2606
|
+
}
|
|
2607
|
+
const mismatches = await verifyPorts(projectPath, allocations);
|
|
2608
|
+
if (mismatches.length > 0) {
|
|
2609
|
+
console.log(` Port mismatches: ${mismatches.length}`);
|
|
2610
|
+
for (const m of mismatches) {
|
|
2611
|
+
console.log(` ! ${m.packageJsonPath}: ${m.reason}`);
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
const unallocated = await findUnallocatedApps(projectPath, allocations);
|
|
2615
|
+
if (unallocated.length > 0) {
|
|
2616
|
+
console.log(` Unallocated apps: ${unallocated.length}`);
|
|
2617
|
+
for (const app of unallocated) {
|
|
2618
|
+
console.log(
|
|
2619
|
+
` + ${app.name} (${app.framework}${app.detectedPort ? `, port ${app.detectedPort}` : ""})`
|
|
2620
|
+
);
|
|
2621
|
+
}
|
|
2622
|
+
if (fix && !dryRun) {
|
|
2623
|
+
const maxPort = Math.max(...allocations.map((a) => a.port), 2999);
|
|
2624
|
+
let nextPort = maxPort + 1;
|
|
2625
|
+
for (const app of unallocated) {
|
|
2626
|
+
const port = app.detectedPort ?? nextPort++;
|
|
2627
|
+
try {
|
|
2628
|
+
await apiPost("/port-allocations", {
|
|
2629
|
+
repo_id: repoId,
|
|
2630
|
+
port,
|
|
2631
|
+
label: app.name,
|
|
2632
|
+
server_type: app.framework,
|
|
2633
|
+
auto_start: "manual",
|
|
2634
|
+
command: app.command,
|
|
2635
|
+
working_dir: app.path
|
|
2636
|
+
});
|
|
2637
|
+
console.log(` Created allocation: ${app.name} \u2192 port ${port}`);
|
|
2638
|
+
} catch (err) {
|
|
2639
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2640
|
+
console.log(
|
|
2641
|
+
` Failed to create allocation for ${app.name}: ${msg}`
|
|
2642
|
+
);
|
|
2643
|
+
}
|
|
2644
|
+
if (app.detectedPort && app.detectedPort >= nextPort) {
|
|
2645
|
+
nextPort = app.detectedPort + 1;
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
} else if (fix && dryRun) {
|
|
2649
|
+
console.log(" (dry-run \u2014 would create allocations with --fix)");
|
|
2650
|
+
} else {
|
|
2651
|
+
console.log(" Run with --fix to auto-create allocations.");
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
if (mismatches.length === 0 && unallocated.length === 0) {
|
|
2655
|
+
console.log(" Ports verified.");
|
|
2656
|
+
}
|
|
2657
|
+
} catch {
|
|
2658
|
+
console.log(" Port verification skipped.");
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
function groupByType(items) {
|
|
2662
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2663
|
+
const typeLabels = {
|
|
2664
|
+
command: "Commands",
|
|
2665
|
+
agent: "Agents",
|
|
2666
|
+
skill: "Skills",
|
|
2667
|
+
rule: "Rules",
|
|
2668
|
+
hook: "Hooks",
|
|
2669
|
+
template: "Templates",
|
|
2670
|
+
settings: "Settings"
|
|
2671
|
+
};
|
|
2672
|
+
for (const item of items) {
|
|
2673
|
+
const label = typeLabels[item.type] ?? item.type;
|
|
2674
|
+
const group = groups.get(label) ?? [];
|
|
2675
|
+
group.push(item);
|
|
2676
|
+
groups.set(label, group);
|
|
2677
|
+
}
|
|
2678
|
+
return groups;
|
|
2679
|
+
}
|
|
2680
|
+
function getLocalFilePath(claudeDir, projectPath, remote) {
|
|
2681
|
+
const typeConfig2 = {
|
|
2682
|
+
command: { dir: "commands", ext: ".md" },
|
|
2683
|
+
agent: { dir: "agents", ext: ".md", subfolder: "AGENT" },
|
|
2684
|
+
skill: { dir: "skills", ext: ".md", subfolder: "SKILL" },
|
|
2685
|
+
rule: { dir: "rules", ext: ".md" },
|
|
2686
|
+
hook: { dir: "hooks", ext: ".sh" },
|
|
2687
|
+
template: { dir: "templates", ext: "" },
|
|
2688
|
+
claude_md: { dir: "", ext: "" },
|
|
2689
|
+
settings: { dir: "", ext: "" }
|
|
2690
|
+
};
|
|
2691
|
+
if (remote.type === "claude_md") return join7(projectPath, "CLAUDE.md");
|
|
2692
|
+
if (remote.type === "settings") return join7(claudeDir, "settings.json");
|
|
2693
|
+
const cfg = typeConfig2[remote.type];
|
|
2694
|
+
if (!cfg) return join7(claudeDir, remote.name);
|
|
2695
|
+
const typeDir = remote.type === "command" ? join7(claudeDir, cfg.dir, "cbp") : join7(claudeDir, cfg.dir);
|
|
2696
|
+
if (cfg.subfolder)
|
|
2697
|
+
return join7(typeDir, remote.name, `${cfg.subfolder}${cfg.ext}`);
|
|
2698
|
+
if (remote.type === "command" && remote.category)
|
|
2699
|
+
return join7(typeDir, remote.category, `${remote.name}${cfg.ext}`);
|
|
2700
|
+
if (remote.type === "template") return join7(typeDir, remote.name);
|
|
2701
|
+
return join7(typeDir, `${remote.name}${cfg.ext}`);
|
|
2702
|
+
}
|
|
2703
|
+
function getSyncVersion() {
|
|
2704
|
+
try {
|
|
2705
|
+
return "1.1.0";
|
|
2706
|
+
} catch {
|
|
2707
|
+
return "unknown";
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
function flattenSyncData(data) {
|
|
2711
|
+
const result = /* @__PURE__ */ new Map();
|
|
2712
|
+
const typeMap = {
|
|
2713
|
+
commands: "command",
|
|
2714
|
+
agents: "agent",
|
|
2715
|
+
skills: "skill",
|
|
2716
|
+
rules: "rule",
|
|
2717
|
+
hooks: "hook",
|
|
2718
|
+
templates: "template",
|
|
2719
|
+
settings: "settings"
|
|
2720
|
+
};
|
|
2721
|
+
for (const [syncKey, typeName] of Object.entries(typeMap)) {
|
|
2722
|
+
const files = data[syncKey] ?? [];
|
|
2723
|
+
for (const file of files) {
|
|
2724
|
+
const key = compositeKey(typeName, file.name, file.category ?? null);
|
|
2725
|
+
result.set(key, {
|
|
2726
|
+
id: file.id,
|
|
2727
|
+
type: typeName,
|
|
2728
|
+
name: file.name,
|
|
2729
|
+
content: file.content,
|
|
2730
|
+
category: file.category,
|
|
2731
|
+
updated_at: file.updated_at,
|
|
2732
|
+
content_hash: file.content_hash
|
|
2733
|
+
});
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
return result;
|
|
2737
|
+
}
|
|
2738
|
+
var init_sync = __esm({
|
|
2739
|
+
"src/cli/sync.ts"() {
|
|
2740
|
+
"use strict";
|
|
2741
|
+
init_config();
|
|
2742
|
+
init_fileMapper();
|
|
2743
|
+
init_confirm();
|
|
2744
|
+
init_api();
|
|
2745
|
+
init_variables();
|
|
2746
|
+
init_tech_detect();
|
|
2747
|
+
init_settings_merge();
|
|
2748
|
+
init_hook_registry();
|
|
2749
|
+
init_port_verify();
|
|
2750
|
+
}
|
|
2751
|
+
});
|
|
2752
|
+
|
|
2753
|
+
// src/index.ts
|
|
2754
|
+
init_version();
|
|
2755
|
+
import { readFileSync } from "node:fs";
|
|
2756
|
+
import { resolve } from "node:path";
|
|
2757
|
+
if (!process.env.CODEBYPLAN_API_KEY) {
|
|
2758
|
+
try {
|
|
2759
|
+
const envPath = resolve(process.cwd(), ".env.local");
|
|
2760
|
+
const content = readFileSync(envPath, "utf-8");
|
|
2761
|
+
for (const line of content.split("\n")) {
|
|
2762
|
+
const trimmed = line.trim();
|
|
2763
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2764
|
+
const eq = trimmed.indexOf("=");
|
|
2765
|
+
if (eq === -1) continue;
|
|
2766
|
+
const key = trimmed.slice(0, eq).trim();
|
|
2767
|
+
const val = trimmed.slice(eq + 1).trim().replace(/^["']|["']$/g, "");
|
|
2768
|
+
if (!process.env[key]) process.env[key] = val;
|
|
2769
|
+
}
|
|
2770
|
+
} catch {
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
if (process.env.CODEBYPLAN_API_KEY?.startsWith("CODEBYPLAN_API_KEY=")) {
|
|
2774
|
+
process.env.CODEBYPLAN_API_KEY = process.env.CODEBYPLAN_API_KEY.slice(
|
|
2775
|
+
"CODEBYPLAN_API_KEY=".length
|
|
2776
|
+
);
|
|
2777
|
+
}
|
|
2778
|
+
var arg = process.argv[2];
|
|
2779
|
+
if (arg === "--version" || arg === "-v") {
|
|
2780
|
+
console.log(VERSION);
|
|
2781
|
+
process.exit(0);
|
|
2782
|
+
}
|
|
2783
|
+
if (arg === "setup") {
|
|
2784
|
+
const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
|
|
2785
|
+
await runSetup2();
|
|
2786
|
+
process.exit(0);
|
|
2787
|
+
}
|
|
2788
|
+
if (arg === "sync") {
|
|
2789
|
+
const { runSync: runSync2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
2790
|
+
const { SyncCancelledError: SyncCancelledError2 } = await Promise.resolve().then(() => (init_confirm(), confirm_exports));
|
|
2791
|
+
try {
|
|
2792
|
+
await runSync2();
|
|
2793
|
+
} catch (err) {
|
|
2794
|
+
if (err instanceof SyncCancelledError2) {
|
|
2795
|
+
console.log("\n Sync cancelled.\n");
|
|
2796
|
+
process.exit(0);
|
|
2797
|
+
}
|
|
2798
|
+
throw err;
|
|
2799
|
+
}
|
|
2800
|
+
process.exit(0);
|
|
2801
|
+
}
|
|
2802
|
+
if (arg === "help" || arg === "--help" || arg === "-h" || arg === void 0) {
|
|
2803
|
+
console.log(`
|
|
2804
|
+
CodeByPlan CLI v${VERSION}
|
|
2805
|
+
|
|
2806
|
+
Usage:
|
|
2807
|
+
codebyplan setup Interactive setup (API key + project init + first sync)
|
|
2808
|
+
codebyplan sync Bidirectional sync (pull + push + config)
|
|
2809
|
+
codebyplan help Show this help message
|
|
2810
|
+
codebyplan --version Print version
|
|
2811
|
+
|
|
2812
|
+
Sync options:
|
|
2813
|
+
--path <dir> Project root directory (default: cwd)
|
|
2814
|
+
--repo-id <uuid> Repository ID (or set via .codebyplan.json)
|
|
2815
|
+
--dry-run Preview changes without writing
|
|
2816
|
+
--force Skip confirmation and conflict prompts
|
|
2817
|
+
--fix Auto-create missing port allocations
|
|
2818
|
+
|
|
2819
|
+
MCP Server:
|
|
2820
|
+
Claude Code connects to CodeByPlan via remote MCP:
|
|
2821
|
+
URL: https://codebyplan.com/mcp
|
|
2822
|
+
Auth: x-api-key header (configured during setup)
|
|
2823
|
+
|
|
2824
|
+
Learn more: https://codebyplan.com
|
|
2825
|
+
`);
|
|
2826
|
+
process.exit(0);
|
|
2827
|
+
}
|
|
2828
|
+
console.error(`Unknown command: ${arg}`);
|
|
2829
|
+
console.error("Run 'codebyplan help' for usage.");
|
|
2830
|
+
process.exit(1);
|