codebyplan 1.3.1 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +732 -26
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -14,7 +14,7 @@ var VERSION, PACKAGE_NAME;
14
14
  var init_version = __esm({
15
15
  "src/lib/version.ts"() {
16
16
  "use strict";
17
- VERSION = "1.3.1";
17
+ VERSION = "1.4.1";
18
18
  PACKAGE_NAME = "codebyplan";
19
19
  }
20
20
  });
@@ -2074,16 +2074,686 @@ var init_port_verify = __esm({
2074
2074
  }
2075
2075
  });
2076
2076
 
2077
+ // src/lib/eslint-generator.ts
2078
+ import { createHash } from "node:crypto";
2079
+ function parseFragment(fragment) {
2080
+ if (!fragment) return { imports: [], configComments: [] };
2081
+ const lines = fragment.split("\n");
2082
+ const imports = [];
2083
+ const configComments = [];
2084
+ for (const line of lines) {
2085
+ const trimmed = line.trim();
2086
+ if (!trimmed) continue;
2087
+ if (trimmed.startsWith("import ") || trimmed.startsWith("const ") || trimmed.startsWith("// @ts-check")) {
2088
+ imports.push(trimmed);
2089
+ } else if (trimmed.startsWith("//")) {
2090
+ configComments.push(trimmed);
2091
+ }
2092
+ }
2093
+ return { imports, configComments };
2094
+ }
2095
+ function importKey(line) {
2096
+ const fromMatch = line.match(/from\s+["']([^"']+)["']/);
2097
+ if (fromMatch) return fromMatch[1];
2098
+ const requireMatch = line.match(/require\(["']([^"']+)["']\)/);
2099
+ if (requireMatch) return requireMatch[1];
2100
+ return line;
2101
+ }
2102
+ function deduplicateImports(allImports) {
2103
+ const seen = /* @__PURE__ */ new Map();
2104
+ for (const imp of allImports) {
2105
+ const key = importKey(imp);
2106
+ if (!seen.has(key)) {
2107
+ seen.set(key, imp);
2108
+ }
2109
+ }
2110
+ return Array.from(seen.values());
2111
+ }
2112
+ function collectDependencies(presets) {
2113
+ const deps = /* @__PURE__ */ new Map();
2114
+ for (const preset of presets) {
2115
+ const presetDeps = preset.dependencies;
2116
+ if (!presetDeps || typeof presetDeps !== "object") continue;
2117
+ for (const [pkg, version] of Object.entries(presetDeps)) {
2118
+ if (typeof version === "string" && !deps.has(pkg)) {
2119
+ deps.set(pkg, version);
2120
+ }
2121
+ }
2122
+ }
2123
+ return deps;
2124
+ }
2125
+ function hashConfig(content) {
2126
+ return createHash("sha256").update(content).digest("hex");
2127
+ }
2128
+ function buildRules(presets, userOverrides) {
2129
+ const merged = {};
2130
+ for (const preset of presets) {
2131
+ const rules = preset.rules;
2132
+ if (rules && typeof rules === "object") {
2133
+ Object.assign(merged, rules);
2134
+ }
2135
+ }
2136
+ if (userOverrides) {
2137
+ Object.assign(merged, userOverrides);
2138
+ }
2139
+ return merged;
2140
+ }
2141
+ function formatRules(rules, indent) {
2142
+ const entries = Object.entries(rules);
2143
+ if (entries.length === 0) return "{}";
2144
+ const lines = entries.map(([key, value]) => {
2145
+ const formattedValue = JSON.stringify(value);
2146
+ return `${indent} "${key}": ${formattedValue},`;
2147
+ });
2148
+ return `{
2149
+ ${lines.join("\n")}
2150
+ ${indent}}`;
2151
+ }
2152
+ function generateEslintConfig(input) {
2153
+ const { presets, ruleOverrides, tsconfigRootDir, ignorePatterns } = input;
2154
+ const allImports = [];
2155
+ const allConfigComments = [];
2156
+ for (const preset of presets) {
2157
+ const parsed = parseFragment(preset.config_fragment);
2158
+ allImports.push(...parsed.imports);
2159
+ allConfigComments.push(
2160
+ ...parsed.configComments.map((c) => ` ${c} (from: ${preset.name})`)
2161
+ );
2162
+ }
2163
+ const dedupedImports = deduplicateImports(allImports);
2164
+ const tsCheck = dedupedImports.find((i) => i === "// @ts-check");
2165
+ const importLines = dedupedImports.filter((i) => i !== "// @ts-check");
2166
+ const rules = buildRules(presets, ruleOverrides);
2167
+ const defaultIgnores = [
2168
+ "eslint.config.mjs",
2169
+ "node_modules/**",
2170
+ "dist/**",
2171
+ ".next/**",
2172
+ "coverage/**"
2173
+ ];
2174
+ const ignores = [.../* @__PURE__ */ new Set([...defaultIgnores, ...ignorePatterns ?? []])];
2175
+ const rootDir = tsconfigRootDir ?? "import.meta.dirname";
2176
+ const rootDirValue = rootDir === "import.meta.dirname" ? "import.meta.dirname" : `"${rootDir}"`;
2177
+ const hasNextJs = presets.some((p) => p.is_system && p.name === "nextjs");
2178
+ const hasReact = presets.some((p) => p.is_system && p.name === "react");
2179
+ const hasNode = presets.some((p) => p.is_system && p.name === "node");
2180
+ const hasTesting = presets.some((p) => p.is_system && p.name === "testing");
2181
+ const hasTestingReact = presets.some(
2182
+ (p) => p.is_system && p.name === "testing-react"
2183
+ );
2184
+ const hasTestingE2e = presets.some(
2185
+ (p) => p.is_system && p.name === "testing-e2e"
2186
+ );
2187
+ const hasCli = presets.some((p) => p.is_system && p.name === "cli");
2188
+ const sections = [];
2189
+ if (tsCheck) {
2190
+ sections.push("// @ts-check");
2191
+ }
2192
+ sections.push(
2193
+ "/**",
2194
+ " * ESLint flat config \u2014 generated by CodeByPlan CLI.",
2195
+ " * Edit rule overrides via the web UI, then run `codebyplan eslint sync`.",
2196
+ " * Manual edits will be detected as drift.",
2197
+ " */",
2198
+ ""
2199
+ );
2200
+ for (const imp of importLines) {
2201
+ sections.push(imp);
2202
+ }
2203
+ const hasGlobalsImport = importLines.some((i) => i.includes("globals"));
2204
+ if ((hasNode || hasReact || hasNextJs) && !hasGlobalsImport) {
2205
+ sections.push('import globals from "globals";');
2206
+ }
2207
+ sections.push("");
2208
+ sections.push("export default [");
2209
+ sections.push(` { ignores: ${JSON.stringify(ignores)} },`);
2210
+ sections.push("");
2211
+ const hasBase = presets.some((p) => p.is_system && p.name === "base");
2212
+ if (hasBase) {
2213
+ sections.push(" // Base: TypeScript + security + Prettier");
2214
+ sections.push(" eslint.configs.recommended,");
2215
+ sections.push(" ...tseslint.configs.recommendedTypeChecked,");
2216
+ sections.push(" security.configs.recommended,");
2217
+ sections.push(' { plugins: { "no-secrets": noSecrets } },');
2218
+ sections.push(" {");
2219
+ sections.push(" languageOptions: {");
2220
+ sections.push(" parserOptions: {");
2221
+ sections.push(" projectService: true,");
2222
+ sections.push(` tsconfigRootDir: ${rootDirValue},`);
2223
+ sections.push(" },");
2224
+ sections.push(" },");
2225
+ sections.push(" },");
2226
+ sections.push("");
2227
+ }
2228
+ if (hasNextJs) {
2229
+ sections.push(" // Next.js: Core Web Vitals + TypeScript");
2230
+ sections.push(" ...nextCoreWebVitals,");
2231
+ sections.push(" ...nextTypescript,");
2232
+ sections.push(" { rules: jsxA11y.flatConfigs.strict.rules },");
2233
+ sections.push(' { plugins: { "react-compiler": reactCompiler } },');
2234
+ sections.push("");
2235
+ }
2236
+ if (hasReact && !hasNextJs) {
2237
+ sections.push(" // React: hooks + compiler + a11y");
2238
+ sections.push(" {");
2239
+ sections.push(' files: ["**/*.{ts,tsx,jsx}"],');
2240
+ sections.push(" plugins: {");
2241
+ sections.push(" react,");
2242
+ sections.push(' "react-hooks": reactHooks,');
2243
+ sections.push(' "react-compiler": reactCompiler,');
2244
+ sections.push(" },");
2245
+ sections.push(" languageOptions: {");
2246
+ sections.push(" parserOptions: { ecmaFeatures: { jsx: true } },");
2247
+ sections.push(" globals: { ...globals.browser },");
2248
+ sections.push(" },");
2249
+ sections.push(' settings: { react: { version: "detect" } },');
2250
+ sections.push(" rules: {");
2251
+ sections.push(" ...react.configs.flat.recommended.rules,");
2252
+ sections.push(' ...react.configs.flat["jsx-runtime"].rules,');
2253
+ sections.push(" },");
2254
+ sections.push(" },");
2255
+ sections.push(" jsxA11y.flatConfigs.strict,");
2256
+ sections.push("");
2257
+ }
2258
+ if (hasNode) {
2259
+ sections.push(" // Node.js globals");
2260
+ sections.push(" {");
2261
+ sections.push(" languageOptions: {");
2262
+ sections.push(" globals: { ...globals.node },");
2263
+ sections.push(' sourceType: "module",');
2264
+ sections.push(" },");
2265
+ sections.push(" },");
2266
+ sections.push("");
2267
+ }
2268
+ if (hasBase) {
2269
+ sections.push(" // Prettier (must be last base config)");
2270
+ sections.push(" prettier,");
2271
+ sections.push("");
2272
+ }
2273
+ if (Object.keys(rules).length > 0) {
2274
+ sections.push(" // Rule overrides");
2275
+ sections.push(" {");
2276
+ sections.push(` rules: ${formatRules(rules, " ")},`);
2277
+ sections.push(" },");
2278
+ sections.push("");
2279
+ }
2280
+ if (hasCli) {
2281
+ sections.push(" // CLI overrides");
2282
+ sections.push(" {");
2283
+ sections.push(" rules: {");
2284
+ sections.push(' "no-console": "off",');
2285
+ sections.push(' "security/detect-non-literal-fs-filename": "off",');
2286
+ sections.push(' "security/detect-object-injection": "off",');
2287
+ sections.push(" },");
2288
+ sections.push(" },");
2289
+ sections.push("");
2290
+ }
2291
+ if (hasTesting) {
2292
+ sections.push(" // Testing: Vitest");
2293
+ sections.push(" {");
2294
+ sections.push(
2295
+ ' files: ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx"],'
2296
+ );
2297
+ sections.push(" plugins: { vitest },");
2298
+ sections.push(" rules: {");
2299
+ sections.push(" ...vitest.configs.recommended.rules,");
2300
+ sections.push(' "@typescript-eslint/no-explicit-any": "off",');
2301
+ sections.push(' "@typescript-eslint/no-unsafe-assignment": "off",');
2302
+ sections.push(' "@typescript-eslint/no-unsafe-member-access": "off",');
2303
+ sections.push(' "@typescript-eslint/no-unsafe-call": "off",');
2304
+ sections.push(' "@typescript-eslint/no-unsafe-argument": "off",');
2305
+ sections.push(' "@typescript-eslint/no-unsafe-return": "off",');
2306
+ sections.push(" },");
2307
+ sections.push(" },");
2308
+ sections.push("");
2309
+ }
2310
+ if (hasTestingReact) {
2311
+ sections.push(" // Testing: React Testing Library + jest-dom");
2312
+ sections.push(" {");
2313
+ sections.push(
2314
+ ' files: ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx"],'
2315
+ );
2316
+ sections.push(
2317
+ ' plugins: { "testing-library": testingLibrary, "jest-dom": jestDom },'
2318
+ );
2319
+ sections.push(" rules: {");
2320
+ sections.push(' ...testingLibrary.configs["flat/react"].rules,');
2321
+ sections.push(' ...jestDom.configs["flat/recommended"].rules,');
2322
+ sections.push(" },");
2323
+ sections.push(" },");
2324
+ sections.push("");
2325
+ }
2326
+ if (hasTestingE2e) {
2327
+ sections.push(" // Testing: Playwright E2E");
2328
+ sections.push(" {");
2329
+ sections.push(' files: ["e2e/**"],');
2330
+ sections.push(" plugins: { playwright },");
2331
+ sections.push(" rules: {");
2332
+ sections.push(' ...playwright.configs["flat/recommended"].rules,');
2333
+ sections.push(' "no-console": "off",');
2334
+ sections.push(" },");
2335
+ sections.push(" },");
2336
+ sections.push("");
2337
+ }
2338
+ sections.push("];");
2339
+ sections.push("");
2340
+ return sections.join("\n");
2341
+ }
2342
+ var init_eslint_generator = __esm({
2343
+ "src/lib/eslint-generator.ts"() {
2344
+ "use strict";
2345
+ }
2346
+ });
2347
+
2348
+ // src/cli/eslint.ts
2349
+ var eslint_exports = {};
2350
+ __export(eslint_exports, {
2351
+ checkEslintDrift: () => checkEslintDrift,
2352
+ eslintInit: () => eslintInit,
2353
+ eslintSync: () => eslintSync,
2354
+ runEslint: () => runEslint
2355
+ });
2356
+ import { readFile as readFile8, writeFile as writeFile3, access as access2 } from "node:fs/promises";
2357
+ import { join as join7, relative as relative2 } from "node:path";
2358
+ async function fileExists2(filePath) {
2359
+ try {
2360
+ await access2(filePath);
2361
+ return true;
2362
+ } catch {
2363
+ return false;
2364
+ }
2365
+ }
2366
+ function detectPackageManager(projectPath) {
2367
+ return (async () => {
2368
+ if (await fileExists2(join7(projectPath, "pnpm-lock.yaml"))) return "pnpm";
2369
+ if (await fileExists2(join7(projectPath, "yarn.lock"))) return "yarn";
2370
+ return "npm";
2371
+ })();
2372
+ }
2373
+ async function getInstalledDeps(pkgJsonPath) {
2374
+ try {
2375
+ const raw = await readFile8(pkgJsonPath, "utf-8");
2376
+ const pkg = JSON.parse(raw);
2377
+ const all = /* @__PURE__ */ new Set();
2378
+ for (const name of Object.keys(pkg.dependencies ?? {})) all.add(name);
2379
+ for (const name of Object.keys(pkg.devDependencies ?? {})) all.add(name);
2380
+ return all;
2381
+ } catch {
2382
+ return /* @__PURE__ */ new Set();
2383
+ }
2384
+ }
2385
+ function buildInstallCommand(pm, packages, workspaceRoot) {
2386
+ const pkgStr = packages.join(" ");
2387
+ switch (pm) {
2388
+ case "pnpm":
2389
+ return workspaceRoot ? `pnpm add -D -w ${pkgStr}` : `pnpm add -D ${pkgStr}`;
2390
+ case "yarn":
2391
+ return workspaceRoot ? `yarn add -D -W ${pkgStr}` : `yarn add -D ${pkgStr}`;
2392
+ case "npm":
2393
+ return `npm install -D ${pkgStr}`;
2394
+ }
2395
+ }
2396
+ async function resolvePresetsForTechStack(techNames) {
2397
+ const techParam = techNames.join(",");
2398
+ const res = await apiGet("/eslint-presets", {
2399
+ tech_stack: techParam
2400
+ });
2401
+ return res.data ?? [];
2402
+ }
2403
+ async function eslintInit(repoId, projectPath) {
2404
+ console.log("\n ESLint Init");
2405
+ console.log(` Repo: ${repoId}`);
2406
+ console.log(` Path: ${projectPath}
2407
+ `);
2408
+ const apps = await discoverMonorepoApps(projectPath);
2409
+ const isMonorepo = apps.length > 0;
2410
+ const targets = [];
2411
+ if (isMonorepo) {
2412
+ console.log(` Monorepo detected: ${apps.length} apps/packages
2413
+ `);
2414
+ for (const app of apps) {
2415
+ const detected = await detectTechStack(app.absPath);
2416
+ targets.push({
2417
+ name: app.name,
2418
+ sourcePath: app.path,
2419
+ absPath: app.absPath,
2420
+ techStack: detected.flat
2421
+ });
2422
+ }
2423
+ } else {
2424
+ const detected = await detectTechStack(projectPath);
2425
+ targets.push({
2426
+ name: "root",
2427
+ sourcePath: ".",
2428
+ absPath: projectPath,
2429
+ techStack: detected.flat
2430
+ });
2431
+ }
2432
+ const allRequiredDeps = /* @__PURE__ */ new Map();
2433
+ const configsToWrite = [];
2434
+ for (const target of targets) {
2435
+ const techNames = target.techStack.map((t) => t.name);
2436
+ console.log(
2437
+ ` ${target.name}: ${techNames.length > 0 ? techNames.join(", ") : "(no tech detected)"}`
2438
+ );
2439
+ if (techNames.length === 0) {
2440
+ console.log(` Skipping \u2014 no tech stack detected.
2441
+ `);
2442
+ continue;
2443
+ }
2444
+ const presets = await resolvePresetsForTechStack(techNames);
2445
+ if (presets.length === 0) {
2446
+ console.log(` No matching presets found.
2447
+ `);
2448
+ continue;
2449
+ }
2450
+ console.log(` Presets: ${presets.map((p) => p.name).join(", ")}`);
2451
+ let userOverrides;
2452
+ try {
2453
+ const configRes = await apiGet(`/repos/${repoId}/eslint-config`);
2454
+ const existing = configRes.data?.find(
2455
+ (c) => c.source_path === target.sourcePath
2456
+ );
2457
+ if (existing?.rule_overrides) {
2458
+ const overrides = existing.rule_overrides;
2459
+ if (Object.keys(overrides).length > 0) {
2460
+ userOverrides = overrides;
2461
+ console.log(
2462
+ ` User overrides: ${Object.keys(overrides).length} rule(s)`
2463
+ );
2464
+ }
2465
+ }
2466
+ } catch {
2467
+ }
2468
+ const content = generateEslintConfig({
2469
+ presets,
2470
+ ruleOverrides: userOverrides
2471
+ });
2472
+ const hash = hashConfig(content);
2473
+ const configPath = join7(target.absPath, "eslint.config.mjs");
2474
+ configsToWrite.push({
2475
+ target,
2476
+ presets,
2477
+ content,
2478
+ hash,
2479
+ configPath,
2480
+ userOverrides
2481
+ });
2482
+ const deps = collectDependencies(presets);
2483
+ for (const [pkg, version] of deps) {
2484
+ if (!allRequiredDeps.has(pkg)) {
2485
+ allRequiredDeps.set(pkg, version);
2486
+ }
2487
+ }
2488
+ console.log("");
2489
+ }
2490
+ if (configsToWrite.length === 0) {
2491
+ console.log(" No configs to generate.\n");
2492
+ return;
2493
+ }
2494
+ const pm = await detectPackageManager(projectPath);
2495
+ const rootPkgJsonPath = join7(projectPath, "package.json");
2496
+ const installed = await getInstalledDeps(rootPkgJsonPath);
2497
+ if (isMonorepo) {
2498
+ for (const { target } of configsToWrite) {
2499
+ const appPkgJson = join7(target.absPath, "package.json");
2500
+ const appDeps = await getInstalledDeps(appPkgJson);
2501
+ for (const dep of appDeps) {
2502
+ installed.add(dep);
2503
+ }
2504
+ }
2505
+ }
2506
+ const missingPkgs = [];
2507
+ for (const [pkg] of allRequiredDeps) {
2508
+ if (!installed.has(pkg)) {
2509
+ missingPkgs.push(pkg);
2510
+ }
2511
+ }
2512
+ if (missingPkgs.length > 0) {
2513
+ console.log(` Missing npm packages (${missingPkgs.length}):`);
2514
+ for (const pkg of missingPkgs) {
2515
+ console.log(` - ${pkg}`);
2516
+ }
2517
+ const installCmd = buildInstallCommand(pm, missingPkgs, isMonorepo);
2518
+ console.log(`
2519
+ Install command: ${installCmd}`);
2520
+ const confirmed = await confirmProceed(
2521
+ `
2522
+ Install ${missingPkgs.length} missing packages? [Y/n] `
2523
+ );
2524
+ if (confirmed) {
2525
+ const { execSync } = await import("node:child_process");
2526
+ try {
2527
+ execSync(installCmd, { cwd: projectPath, stdio: "inherit" });
2528
+ console.log(" Packages installed.\n");
2529
+ } catch (err) {
2530
+ console.error(
2531
+ ` Package installation failed: ${err instanceof Error ? err.message : String(err)}`
2532
+ );
2533
+ console.log(" You can install manually and re-run.\n");
2534
+ }
2535
+ } else {
2536
+ console.log(
2537
+ " Skipping installation. Generated configs may not work without these packages.\n"
2538
+ );
2539
+ }
2540
+ }
2541
+ for (const {
2542
+ target,
2543
+ presets,
2544
+ content,
2545
+ hash,
2546
+ configPath,
2547
+ userOverrides
2548
+ } of configsToWrite) {
2549
+ if (await fileExists2(configPath)) {
2550
+ try {
2551
+ const existing = await readFile8(configPath, "utf-8");
2552
+ const existingHash = hashConfig(existing);
2553
+ if (existingHash === hash) {
2554
+ console.log(
2555
+ ` ${target.name}: eslint.config.mjs already up to date.`
2556
+ );
2557
+ continue;
2558
+ }
2559
+ } catch {
2560
+ }
2561
+ const overwrite = await confirmProceed(
2562
+ ` ${target.name}: eslint.config.mjs already exists. Overwrite? [Y/n] `
2563
+ );
2564
+ if (!overwrite) {
2565
+ console.log(` ${target.name}: Skipped.
2566
+ `);
2567
+ continue;
2568
+ }
2569
+ }
2570
+ try {
2571
+ await writeFile3(configPath, content, "utf-8");
2572
+ } catch (err) {
2573
+ console.error(
2574
+ ` ${target.name}: Failed to write config: ${err instanceof Error ? err.message : String(err)}`
2575
+ );
2576
+ continue;
2577
+ }
2578
+ console.log(` ${target.name}: wrote ${relative2(projectPath, configPath)}`);
2579
+ try {
2580
+ await apiPut(`/repos/${repoId}/eslint-config`, {
2581
+ source_path: target.sourcePath,
2582
+ preset_ids: presets.map((p) => p.id),
2583
+ rule_overrides: userOverrides ?? {},
2584
+ generated_hash: hash
2585
+ });
2586
+ } catch (err) {
2587
+ console.error(
2588
+ ` Warning: Failed to save config to server: ${err instanceof Error ? err.message : String(err)}`
2589
+ );
2590
+ }
2591
+ }
2592
+ console.log("\n ESLint init complete.\n");
2593
+ }
2594
+ async function eslintSync(repoId, projectPath) {
2595
+ console.log("\n ESLint Sync");
2596
+ console.log(` Repo: ${repoId}`);
2597
+ console.log(` Path: ${projectPath}
2598
+ `);
2599
+ let configs;
2600
+ try {
2601
+ const res = await apiGet(
2602
+ `/repos/${repoId}/eslint-config`
2603
+ );
2604
+ configs = res.data ?? [];
2605
+ } catch {
2606
+ console.log(
2607
+ " No existing ESLint config found. Run `codebyplan eslint init` first.\n"
2608
+ );
2609
+ return;
2610
+ }
2611
+ if (configs.length === 0) {
2612
+ console.log(
2613
+ " No ESLint configs registered. Run `codebyplan eslint init` first.\n"
2614
+ );
2615
+ return;
2616
+ }
2617
+ let updatedCount = 0;
2618
+ let skippedCount = 0;
2619
+ let driftCount = 0;
2620
+ for (const config of configs) {
2621
+ const absPath = config.source_path === "." ? projectPath : join7(projectPath, config.source_path);
2622
+ const configPath = join7(absPath, "eslint.config.mjs");
2623
+ const detected = await detectTechStack(absPath);
2624
+ const techNames = detected.flat.map((t) => t.name);
2625
+ const currentPresets = await resolvePresetsForTechStack(techNames);
2626
+ const currentPresetIds = currentPresets.map((p) => p.id).sort();
2627
+ const savedPresetIds = [...config.active_preset_ids ?? []].sort();
2628
+ const presetsChanged = currentPresetIds.length !== savedPresetIds.length || currentPresetIds.some((id) => !savedPresetIds.includes(id));
2629
+ if (!presetsChanged) {
2630
+ if (await fileExists2(configPath)) {
2631
+ try {
2632
+ const currentContent = await readFile8(configPath, "utf-8");
2633
+ const currentHash = hashConfig(currentContent);
2634
+ if (config.generated_hash && currentHash !== config.generated_hash) {
2635
+ console.log(
2636
+ ` ${config.source_path}: drift detected (manually edited). Not overwriting.`
2637
+ );
2638
+ driftCount++;
2639
+ continue;
2640
+ }
2641
+ skippedCount++;
2642
+ continue;
2643
+ } catch {
2644
+ console.warn(
2645
+ ` ${config.source_path}: config file unreadable, regenerating...`
2646
+ );
2647
+ }
2648
+ } else {
2649
+ console.log(
2650
+ ` ${config.source_path}: config file missing, regenerating...`
2651
+ );
2652
+ }
2653
+ }
2654
+ console.log(` ${config.source_path}: presets changed, regenerating...`);
2655
+ const userOverrides = config.rule_overrides;
2656
+ const content = generateEslintConfig({
2657
+ presets: currentPresets,
2658
+ ruleOverrides: userOverrides && Object.keys(userOverrides).length > 0 ? userOverrides : void 0
2659
+ });
2660
+ try {
2661
+ await writeFile3(configPath, content, "utf-8");
2662
+ } catch (err) {
2663
+ console.error(
2664
+ ` ${config.source_path}: Failed to write config: ${err instanceof Error ? err.message : String(err)}`
2665
+ );
2666
+ continue;
2667
+ }
2668
+ const newHash = hashConfig(content);
2669
+ try {
2670
+ await apiPut(`/repos/${repoId}/eslint-config`, {
2671
+ source_path: config.source_path,
2672
+ preset_ids: currentPresetIds,
2673
+ rule_overrides: userOverrides ?? {},
2674
+ generated_hash: newHash
2675
+ });
2676
+ } catch (err) {
2677
+ console.error(
2678
+ ` Warning: Failed to update server: ${err instanceof Error ? err.message : String(err)}`
2679
+ );
2680
+ }
2681
+ updatedCount++;
2682
+ }
2683
+ console.log(
2684
+ `
2685
+ Sync: ${updatedCount} updated, ${skippedCount} unchanged, ${driftCount} drift detected.
2686
+ `
2687
+ );
2688
+ }
2689
+ async function checkEslintDrift(repoId, projectPath) {
2690
+ try {
2691
+ const res = await apiGet(
2692
+ `/repos/${repoId}/eslint-config`
2693
+ );
2694
+ const configs = res.data ?? [];
2695
+ for (const config of configs) {
2696
+ if (!config.generated_hash) continue;
2697
+ const absPath = config.source_path === "." ? projectPath : join7(projectPath, config.source_path);
2698
+ const configPath = join7(absPath, "eslint.config.mjs");
2699
+ if (!await fileExists2(configPath)) continue;
2700
+ try {
2701
+ const content = await readFile8(configPath, "utf-8");
2702
+ const currentHash = hashConfig(content);
2703
+ if (currentHash !== config.generated_hash) {
2704
+ return true;
2705
+ }
2706
+ } catch {
2707
+ }
2708
+ }
2709
+ return false;
2710
+ } catch {
2711
+ return false;
2712
+ }
2713
+ }
2714
+ async function runEslint() {
2715
+ const subcommand = process.argv[3];
2716
+ const flags = parseFlags(4);
2717
+ validateApiKey();
2718
+ const config = await resolveConfig(flags);
2719
+ const { repoId, projectPath } = config;
2720
+ switch (subcommand) {
2721
+ case "init":
2722
+ await eslintInit(repoId, projectPath);
2723
+ break;
2724
+ case "sync":
2725
+ await eslintSync(repoId, projectPath);
2726
+ break;
2727
+ default:
2728
+ console.log(`
2729
+ Usage:
2730
+ codebyplan eslint init Detect tech stack, resolve presets, generate eslint.config.mjs
2731
+ codebyplan eslint sync Regenerate if presets changed, detect drift
2732
+ `);
2733
+ break;
2734
+ }
2735
+ }
2736
+ var init_eslint = __esm({
2737
+ "src/cli/eslint.ts"() {
2738
+ "use strict";
2739
+ init_config();
2740
+ init_confirm();
2741
+ init_api();
2742
+ init_tech_detect();
2743
+ init_eslint_generator();
2744
+ }
2745
+ });
2746
+
2077
2747
  // src/cli/sync.ts
2078
2748
  var sync_exports = {};
2079
2749
  __export(sync_exports, {
2080
2750
  runSync: () => runSync
2081
2751
  });
2082
- import { createHash } from "node:crypto";
2083
- import { readFile as readFile8, writeFile as writeFile3, mkdir as mkdir2, chmod as chmod2, unlink as unlink2 } from "node:fs/promises";
2084
- import { join as join7, dirname as dirname2 } from "node:path";
2752
+ import { createHash as createHash2 } from "node:crypto";
2753
+ import { readFile as readFile9, writeFile as writeFile4, mkdir as mkdir2, chmod as chmod2, unlink as unlink2 } from "node:fs/promises";
2754
+ import { join as join8, dirname as dirname2 } from "node:path";
2085
2755
  function contentHash(content) {
2086
- return createHash("sha256").update(content).digest("hex");
2756
+ return createHash2("sha256").update(content).digest("hex");
2087
2757
  }
2088
2758
  async function runSync() {
2089
2759
  const flags = parseFlags(3);
@@ -2149,7 +2819,7 @@ async function runSync() {
2149
2819
  }
2150
2820
  async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
2151
2821
  console.log(" Reading local and remote state...");
2152
- const claudeDir = join7(projectPath, ".claude");
2822
+ const claudeDir = join8(projectPath, ".claude");
2153
2823
  let localFiles = /* @__PURE__ */ new Map();
2154
2824
  try {
2155
2825
  localFiles = await scanLocalFiles(claudeDir, projectPath);
@@ -2381,7 +3051,7 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
2381
3051
  for (const p of toPull) {
2382
3052
  if (p.filePath && p.remoteContent !== null) {
2383
3053
  await mkdir2(dirname2(p.filePath), { recursive: true });
2384
- await writeFile3(p.filePath, p.remoteContent, "utf-8");
3054
+ await writeFile4(p.filePath, p.remoteContent, "utf-8");
2385
3055
  if (p.isHook) await chmod2(p.filePath, 493);
2386
3056
  }
2387
3057
  }
@@ -2525,12 +3195,14 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
2525
3195
  await syncConfig(repoId, projectPath, dryRun);
2526
3196
  console.log(" Tech stack...");
2527
3197
  await syncTechStack(repoId, projectPath, dryRun);
3198
+ console.log(" ESLint config...");
3199
+ await syncEslintDriftCheck(repoId, projectPath);
2528
3200
  console.log(" Port verification...");
2529
3201
  await syncPortVerification(repoId, projectPath, dryRun, fix);
2530
3202
  console.log("\n Sync complete.\n");
2531
3203
  }
2532
3204
  async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun) {
2533
- const settingsPath = join7(claudeDir, "settings.json");
3205
+ const settingsPath = join8(claudeDir, "settings.json");
2534
3206
  const globalSettingsFiles = syncData.global_settings ?? [];
2535
3207
  let globalSettings = {};
2536
3208
  for (const gf of globalSettingsFiles) {
@@ -2550,11 +3222,11 @@ async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun)
2550
3222
  globalSettings,
2551
3223
  repoSettings
2552
3224
  );
2553
- const hooksDir = join7(projectPath, ".claude", "hooks");
3225
+ const hooksDir = join8(projectPath, ".claude", "hooks");
2554
3226
  const discovered = await discoverHooks(hooksDir);
2555
3227
  let localSettings = {};
2556
3228
  try {
2557
- const raw = await readFile8(settingsPath, "utf-8");
3229
+ const raw = await readFile9(settingsPath, "utf-8");
2558
3230
  localSettings = JSON.parse(raw);
2559
3231
  } catch {
2560
3232
  }
@@ -2569,7 +3241,7 @@ async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun)
2569
3241
  const mergedContent = JSON.stringify(merged, null, 2) + "\n";
2570
3242
  let currentContent = "";
2571
3243
  try {
2572
- currentContent = await readFile8(settingsPath, "utf-8");
3244
+ currentContent = await readFile9(settingsPath, "utf-8");
2573
3245
  } catch {
2574
3246
  }
2575
3247
  if (currentContent === mergedContent) {
@@ -2581,14 +3253,14 @@ async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun)
2581
3253
  return;
2582
3254
  }
2583
3255
  await mkdir2(dirname2(settingsPath), { recursive: true });
2584
- await writeFile3(settingsPath, mergedContent, "utf-8");
3256
+ await writeFile4(settingsPath, mergedContent, "utf-8");
2585
3257
  console.log(" Updated settings.json");
2586
3258
  }
2587
3259
  async function syncConfig(repoId, projectPath, dryRun) {
2588
- const configPath = join7(projectPath, ".codebyplan.json");
3260
+ const configPath = join8(projectPath, ".codebyplan.json");
2589
3261
  let currentConfig = {};
2590
3262
  try {
2591
- const raw = await readFile8(configPath, "utf-8");
3263
+ const raw = await readFile9(configPath, "utf-8");
2592
3264
  currentConfig = JSON.parse(raw);
2593
3265
  } catch {
2594
3266
  currentConfig = { repo_id: repoId };
@@ -2657,7 +3329,7 @@ async function syncConfig(repoId, projectPath, dryRun) {
2657
3329
  console.log(" Config would be updated (dry-run).");
2658
3330
  return;
2659
3331
  }
2660
- await writeFile3(configPath, newJson + "\n", "utf-8");
3332
+ await writeFile4(configPath, newJson + "\n", "utf-8");
2661
3333
  console.log(" Updated .codebyplan.json");
2662
3334
  }
2663
3335
  async function syncTechStack(repoId, projectPath, dryRun) {
@@ -2695,6 +3367,20 @@ async function syncTechStack(repoId, projectPath, dryRun) {
2695
3367
  console.log(" Tech stack detection skipped.");
2696
3368
  }
2697
3369
  }
3370
+ async function syncEslintDriftCheck(repoId, projectPath) {
3371
+ try {
3372
+ const hasDrift = await checkEslintDrift(repoId, projectPath);
3373
+ if (hasDrift) {
3374
+ console.log(
3375
+ " ESLint config drift detected. Run `codebyplan eslint sync` to update."
3376
+ );
3377
+ } else {
3378
+ console.log(" ESLint configs up to date.");
3379
+ }
3380
+ } catch (error) {
3381
+ console.warn(" ESLint drift check skipped:", error);
3382
+ }
3383
+ }
2698
3384
  async function syncPortVerification(repoId, projectPath, dryRun, fix) {
2699
3385
  try {
2700
3386
  const portsRes = await apiGet(
@@ -2791,28 +3477,28 @@ function getLocalFilePath(claudeDir, projectPath, remote) {
2791
3477
  hook: { dir: "hooks", ext: ".sh" },
2792
3478
  template: { dir: "templates", ext: "" },
2793
3479
  context: { dir: "context", ext: ".md" },
2794
- docs_stack: { dir: join7("docs", "stack"), ext: ".md" },
3480
+ docs_stack: { dir: join8("docs", "stack"), ext: ".md" },
2795
3481
  docs: { dir: "docs", ext: ".md" },
2796
3482
  claude_md: { dir: "", ext: "" },
2797
3483
  settings: { dir: "", ext: "" }
2798
3484
  };
2799
- if (remote.type === "claude_md") return join7(projectPath, "CLAUDE.md");
2800
- if (remote.type === "settings") return join7(claudeDir, "settings.json");
3485
+ if (remote.type === "claude_md") return join8(projectPath, "CLAUDE.md");
3486
+ if (remote.type === "settings") return join8(claudeDir, "settings.json");
2801
3487
  const cfg = typeConfig2[remote.type];
2802
- if (!cfg) return join7(claudeDir, remote.name);
2803
- const typeDir = remote.type === "command" ? join7(claudeDir, cfg.dir, "cbp") : join7(claudeDir, cfg.dir);
3488
+ if (!cfg) return join8(claudeDir, remote.name);
3489
+ const typeDir = remote.type === "command" ? join8(claudeDir, cfg.dir, "cbp") : join8(claudeDir, cfg.dir);
2804
3490
  if (cfg.subfolder)
2805
- return join7(typeDir, remote.name, `${cfg.subfolder}${cfg.ext}`);
3491
+ return join8(typeDir, remote.name, `${cfg.subfolder}${cfg.ext}`);
2806
3492
  if (remote.type === "command" && remote.category)
2807
- return join7(typeDir, remote.category, `${remote.name}${cfg.ext}`);
2808
- if (remote.type === "template") return join7(typeDir, remote.name);
3493
+ return join8(typeDir, remote.category, `${remote.name}${cfg.ext}`);
3494
+ if (remote.type === "template") return join8(typeDir, remote.name);
2809
3495
  if (remote.category && (remote.type === "context" || remote.type === "docs_stack" || remote.type === "docs"))
2810
- return join7(typeDir, remote.category, `${remote.name}${cfg.ext}`);
2811
- return join7(typeDir, `${remote.name}${cfg.ext}`);
3496
+ return join8(typeDir, remote.category, `${remote.name}${cfg.ext}`);
3497
+ return join8(typeDir, `${remote.name}${cfg.ext}`);
2812
3498
  }
2813
3499
  function getSyncVersion() {
2814
3500
  try {
2815
- return "1.3.1";
3501
+ return "1.4.1";
2816
3502
  } catch {
2817
3503
  return "unknown";
2818
3504
  }
@@ -2861,6 +3547,7 @@ var init_sync = __esm({
2861
3547
  init_settings_merge();
2862
3548
  init_hook_registry();
2863
3549
  init_port_verify();
3550
+ init_eslint();
2864
3551
  }
2865
3552
  });
2866
3553
 
@@ -2913,6 +3600,20 @@ if (arg === "sync") {
2913
3600
  }
2914
3601
  process.exit(0);
2915
3602
  }
3603
+ if (arg === "eslint") {
3604
+ const { runEslint: runEslint2 } = await Promise.resolve().then(() => (init_eslint(), eslint_exports));
3605
+ const { SyncCancelledError: SyncCancelledError2 } = await Promise.resolve().then(() => (init_confirm(), confirm_exports));
3606
+ try {
3607
+ await runEslint2();
3608
+ } catch (err) {
3609
+ if (err instanceof SyncCancelledError2) {
3610
+ console.log("\n Cancelled.\n");
3611
+ process.exit(0);
3612
+ }
3613
+ throw err;
3614
+ }
3615
+ process.exit(0);
3616
+ }
2916
3617
  if (arg === "help" || arg === "--help" || arg === "-h" || arg === void 0) {
2917
3618
  console.log(`
2918
3619
  CodeByPlan CLI v${VERSION}
@@ -2920,6 +3621,7 @@ if (arg === "help" || arg === "--help" || arg === "-h" || arg === void 0) {
2920
3621
  Usage:
2921
3622
  codebyplan setup Interactive setup (API key + project init + first sync)
2922
3623
  codebyplan sync Bidirectional sync (pull + push + config)
3624
+ codebyplan eslint ESLint config management (init, sync)
2923
3625
  codebyplan help Show this help message
2924
3626
  codebyplan --version Print version
2925
3627
 
@@ -2930,6 +3632,10 @@ if (arg === "help" || arg === "--help" || arg === "-h" || arg === void 0) {
2930
3632
  --force Skip confirmation and conflict prompts
2931
3633
  --fix Auto-create missing port allocations
2932
3634
 
3635
+ ESLint commands:
3636
+ codebyplan eslint init Detect tech stack, resolve presets, generate configs
3637
+ codebyplan eslint sync Regenerate if presets changed, detect drift
3638
+
2933
3639
  MCP Server:
2934
3640
  Claude Code connects to CodeByPlan via remote MCP:
2935
3641
  URL: https://codebyplan.com/mcp
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebyplan",
3
- "version": "1.3.1",
3
+ "version": "1.4.1",
4
4
  "description": "CLI for CodeByPlan — AI-powered development planning and tracking",
5
5
  "type": "module",
6
6
  "bin": {