codebyplan 1.3.0 → 1.4.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.
Files changed (2) hide show
  1. package/dist/cli.js +742 -27
  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.0";
17
+ VERSION = "1.4.0";
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);
@@ -2224,6 +2894,10 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
2224
2894
  claudeFileId: null
2225
2895
  });
2226
2896
  } else if (!local && remote) {
2897
+ const remoteScope = remote.scope ?? "shared";
2898
+ if (remoteScope.startsWith("local:") && remoteScope !== `local:${repoData.name}`) {
2899
+ continue;
2900
+ }
2227
2901
  const resolvedContent = substituteVariables(remote.content, repoData);
2228
2902
  const hadSyncedThisFile = remote.id ? fileRepoByClaudeFileId.has(remote.id) : fileRepoHashes.has(key);
2229
2903
  const recommended = hadSyncedThisFile ? "delete" : "pull";
@@ -2244,6 +2918,10 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
2244
2918
  claudeFileId: remote.id ?? null
2245
2919
  });
2246
2920
  } else if (local && remote) {
2921
+ const remoteScope = remote.scope ?? "shared";
2922
+ if (remoteScope.startsWith("local:") && remoteScope !== `local:${repoData.name}`) {
2923
+ continue;
2924
+ }
2247
2925
  const resolvedRemote = substituteVariables(remote.content, repoData);
2248
2926
  if (local.content === resolvedRemote) {
2249
2927
  continue;
@@ -2373,7 +3051,7 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
2373
3051
  for (const p of toPull) {
2374
3052
  if (p.filePath && p.remoteContent !== null) {
2375
3053
  await mkdir2(dirname2(p.filePath), { recursive: true });
2376
- await writeFile3(p.filePath, p.remoteContent, "utf-8");
3054
+ await writeFile4(p.filePath, p.remoteContent, "utf-8");
2377
3055
  if (p.isHook) await chmod2(p.filePath, 493);
2378
3056
  }
2379
3057
  }
@@ -2517,12 +3195,14 @@ async function runSyncInner(repoId, projectPath, dryRun, force, fix = false) {
2517
3195
  await syncConfig(repoId, projectPath, dryRun);
2518
3196
  console.log(" Tech stack...");
2519
3197
  await syncTechStack(repoId, projectPath, dryRun);
3198
+ console.log(" ESLint config...");
3199
+ await syncEslintDriftCheck(repoId, projectPath);
2520
3200
  console.log(" Port verification...");
2521
3201
  await syncPortVerification(repoId, projectPath, dryRun, fix);
2522
3202
  console.log("\n Sync complete.\n");
2523
3203
  }
2524
3204
  async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun) {
2525
- const settingsPath = join7(claudeDir, "settings.json");
3205
+ const settingsPath = join8(claudeDir, "settings.json");
2526
3206
  const globalSettingsFiles = syncData.global_settings ?? [];
2527
3207
  let globalSettings = {};
2528
3208
  for (const gf of globalSettingsFiles) {
@@ -2542,11 +3222,11 @@ async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun)
2542
3222
  globalSettings,
2543
3223
  repoSettings
2544
3224
  );
2545
- const hooksDir = join7(projectPath, ".claude", "hooks");
3225
+ const hooksDir = join8(projectPath, ".claude", "hooks");
2546
3226
  const discovered = await discoverHooks(hooksDir);
2547
3227
  let localSettings = {};
2548
3228
  try {
2549
- const raw = await readFile8(settingsPath, "utf-8");
3229
+ const raw = await readFile9(settingsPath, "utf-8");
2550
3230
  localSettings = JSON.parse(raw);
2551
3231
  } catch {
2552
3232
  }
@@ -2561,7 +3241,7 @@ async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun)
2561
3241
  const mergedContent = JSON.stringify(merged, null, 2) + "\n";
2562
3242
  let currentContent = "";
2563
3243
  try {
2564
- currentContent = await readFile8(settingsPath, "utf-8");
3244
+ currentContent = await readFile9(settingsPath, "utf-8");
2565
3245
  } catch {
2566
3246
  }
2567
3247
  if (currentContent === mergedContent) {
@@ -2573,14 +3253,14 @@ async function syncSettings(claudeDir, projectPath, syncData, repoData, dryRun)
2573
3253
  return;
2574
3254
  }
2575
3255
  await mkdir2(dirname2(settingsPath), { recursive: true });
2576
- await writeFile3(settingsPath, mergedContent, "utf-8");
3256
+ await writeFile4(settingsPath, mergedContent, "utf-8");
2577
3257
  console.log(" Updated settings.json");
2578
3258
  }
2579
3259
  async function syncConfig(repoId, projectPath, dryRun) {
2580
- const configPath = join7(projectPath, ".codebyplan.json");
3260
+ const configPath = join8(projectPath, ".codebyplan.json");
2581
3261
  let currentConfig = {};
2582
3262
  try {
2583
- const raw = await readFile8(configPath, "utf-8");
3263
+ const raw = await readFile9(configPath, "utf-8");
2584
3264
  currentConfig = JSON.parse(raw);
2585
3265
  } catch {
2586
3266
  currentConfig = { repo_id: repoId };
@@ -2649,7 +3329,7 @@ async function syncConfig(repoId, projectPath, dryRun) {
2649
3329
  console.log(" Config would be updated (dry-run).");
2650
3330
  return;
2651
3331
  }
2652
- await writeFile3(configPath, newJson + "\n", "utf-8");
3332
+ await writeFile4(configPath, newJson + "\n", "utf-8");
2653
3333
  console.log(" Updated .codebyplan.json");
2654
3334
  }
2655
3335
  async function syncTechStack(repoId, projectPath, dryRun) {
@@ -2687,6 +3367,20 @@ async function syncTechStack(repoId, projectPath, dryRun) {
2687
3367
  console.log(" Tech stack detection skipped.");
2688
3368
  }
2689
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
+ }
2690
3384
  async function syncPortVerification(repoId, projectPath, dryRun, fix) {
2691
3385
  try {
2692
3386
  const portsRes = await apiGet(
@@ -2783,28 +3477,28 @@ function getLocalFilePath(claudeDir, projectPath, remote) {
2783
3477
  hook: { dir: "hooks", ext: ".sh" },
2784
3478
  template: { dir: "templates", ext: "" },
2785
3479
  context: { dir: "context", ext: ".md" },
2786
- docs_stack: { dir: join7("docs", "stack"), ext: ".md" },
3480
+ docs_stack: { dir: join8("docs", "stack"), ext: ".md" },
2787
3481
  docs: { dir: "docs", ext: ".md" },
2788
3482
  claude_md: { dir: "", ext: "" },
2789
3483
  settings: { dir: "", ext: "" }
2790
3484
  };
2791
- if (remote.type === "claude_md") return join7(projectPath, "CLAUDE.md");
2792
- 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");
2793
3487
  const cfg = typeConfig2[remote.type];
2794
- if (!cfg) return join7(claudeDir, remote.name);
2795
- 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);
2796
3490
  if (cfg.subfolder)
2797
- return join7(typeDir, remote.name, `${cfg.subfolder}${cfg.ext}`);
3491
+ return join8(typeDir, remote.name, `${cfg.subfolder}${cfg.ext}`);
2798
3492
  if (remote.type === "command" && remote.category)
2799
- return join7(typeDir, remote.category, `${remote.name}${cfg.ext}`);
2800
- 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);
2801
3495
  if (remote.category && (remote.type === "context" || remote.type === "docs_stack" || remote.type === "docs"))
2802
- return join7(typeDir, remote.category, `${remote.name}${cfg.ext}`);
2803
- 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}`);
2804
3498
  }
2805
3499
  function getSyncVersion() {
2806
3500
  try {
2807
- return "1.3.0";
3501
+ return "1.4.0";
2808
3502
  } catch {
2809
3503
  return "unknown";
2810
3504
  }
@@ -2834,7 +3528,8 @@ function flattenSyncData(data) {
2834
3528
  content: file.content,
2835
3529
  category: file.category,
2836
3530
  updated_at: file.updated_at,
2837
- content_hash: file.content_hash
3531
+ content_hash: file.content_hash,
3532
+ scope: file.scope
2838
3533
  });
2839
3534
  }
2840
3535
  }
@@ -2852,6 +3547,7 @@ var init_sync = __esm({
2852
3547
  init_settings_merge();
2853
3548
  init_hook_registry();
2854
3549
  init_port_verify();
3550
+ init_eslint();
2855
3551
  }
2856
3552
  });
2857
3553
 
@@ -2904,6 +3600,20 @@ if (arg === "sync") {
2904
3600
  }
2905
3601
  process.exit(0);
2906
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
+ }
2907
3617
  if (arg === "help" || arg === "--help" || arg === "-h" || arg === void 0) {
2908
3618
  console.log(`
2909
3619
  CodeByPlan CLI v${VERSION}
@@ -2911,6 +3621,7 @@ if (arg === "help" || arg === "--help" || arg === "-h" || arg === void 0) {
2911
3621
  Usage:
2912
3622
  codebyplan setup Interactive setup (API key + project init + first sync)
2913
3623
  codebyplan sync Bidirectional sync (pull + push + config)
3624
+ codebyplan eslint ESLint config management (init, sync)
2914
3625
  codebyplan help Show this help message
2915
3626
  codebyplan --version Print version
2916
3627
 
@@ -2921,6 +3632,10 @@ if (arg === "help" || arg === "--help" || arg === "-h" || arg === void 0) {
2921
3632
  --force Skip confirmation and conflict prompts
2922
3633
  --fix Auto-create missing port allocations
2923
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
+
2924
3639
  MCP Server:
2925
3640
  Claude Code connects to CodeByPlan via remote MCP:
2926
3641
  URL: https://codebyplan.com/mcp
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebyplan",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "CLI for CodeByPlan — AI-powered development planning and tracking",
5
5
  "type": "module",
6
6
  "bin": {