codebyplan 1.11.2 → 1.13.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 (46) hide show
  1. package/dist/cli.js +590 -405
  2. package/package.json +1 -1
  3. package/templates/hooks/README.md +1 -13
  4. package/templates/hooks/cbp-statusline.mjs +44 -0
  5. package/templates/hooks/cbp-statusline.py +24 -2
  6. package/templates/hooks/cbp-statusline.sh +22 -2
  7. package/templates/hooks/cbp-test-coverage-gate.sh +8 -0
  8. package/templates/hooks/cbp-test-hooks.sh +0 -42
  9. package/templates/hooks/hooks.json +0 -9
  10. package/templates/rules/README.md +8 -1
  11. package/templates/rules/supabase-branch-lifecycle.md +99 -0
  12. package/templates/settings.project.base.json +1 -2
  13. package/templates/skills/cbp-build-cc-settings/reference/cbp-conventions.md +1 -2
  14. package/templates/skills/cbp-checkpoint-create/SKILL.md +2 -0
  15. package/templates/skills/cbp-checkpoint-end/SKILL.md +27 -5
  16. package/templates/skills/cbp-checkpoint-start/SKILL.md +2 -2
  17. package/templates/skills/cbp-git-worktree-remove/SKILL.md +17 -1
  18. package/templates/skills/cbp-session-start/SKILL.md +28 -3
  19. package/templates/skills/{cbp-e2e-setup → cbp-setup-e2e}/SKILL.md +1 -1
  20. package/templates/skills/cbp-setup-eslint/SKILL.md +199 -0
  21. package/templates/skills/cbp-setup-eslint/reference/base.md +82 -0
  22. package/templates/skills/cbp-setup-eslint/reference/cli.md +56 -0
  23. package/templates/skills/cbp-setup-eslint/reference/e2e.md +68 -0
  24. package/templates/skills/cbp-setup-eslint/reference/jest.md +59 -0
  25. package/templates/skills/cbp-setup-eslint/reference/nestjs.md +69 -0
  26. package/templates/skills/cbp-setup-eslint/reference/nextjs.md +63 -0
  27. package/templates/skills/cbp-setup-eslint/reference/node.md +74 -0
  28. package/templates/skills/cbp-setup-eslint/reference/react-native.md +60 -0
  29. package/templates/skills/cbp-setup-eslint/reference/react.md +82 -0
  30. package/templates/skills/cbp-setup-eslint/reference/tailwind.md +64 -0
  31. package/templates/skills/cbp-setup-eslint/reference/testing-react.md +57 -0
  32. package/templates/skills/cbp-setup-eslint/reference/vitest.md +62 -0
  33. package/templates/skills/cbp-ship-main/SKILL.md +13 -0
  34. package/templates/skills/cbp-supabase-branch-check/SKILL.md +12 -5
  35. package/templates/skills/cbp-supabase-migrate/SKILL.md +139 -9
  36. package/templates/skills/cbp-supabase-migrate/reference/preflight-dry-run.md +1 -1
  37. package/templates/skills/cbp-supabase-setup/SKILL.md +13 -7
  38. package/templates/skills/cbp-supabase-setup/reference/branching-setup.md +2 -2
  39. package/templates/skills/cbp-task-complete/SKILL.md +1 -3
  40. package/templates/skills/cbp-task-start/SKILL.md +5 -3
  41. package/templates/hooks/cbp-mcp-worktree-inject.sh +0 -76
  42. /package/templates/skills/{cbp-e2e-setup → cbp-setup-e2e}/reference/maestro.md +0 -0
  43. /package/templates/skills/{cbp-e2e-setup → cbp-setup-e2e}/reference/playwright.md +0 -0
  44. /package/templates/skills/{cbp-e2e-setup → cbp-setup-e2e}/reference/tauri.md +0 -0
  45. /package/templates/skills/{cbp-e2e-setup → cbp-setup-e2e}/reference/vscode.md +0 -0
  46. /package/templates/skills/{cbp-e2e-setup → cbp-setup-e2e}/reference/xcuitest.md +0 -0
package/dist/cli.js CHANGED
@@ -14,26 +14,163 @@ var VERSION, PACKAGE_NAME;
14
14
  var init_version = __esm({
15
15
  "src/lib/version.ts"() {
16
16
  "use strict";
17
- VERSION = "1.11.2";
17
+ VERSION = "1.13.0";
18
18
  PACKAGE_NAME = "codebyplan";
19
19
  }
20
20
  });
21
21
 
22
+ // src/lib/gitignore-block.ts
23
+ import { readFile, writeFile } from "node:fs/promises";
24
+ import * as path from "node:path";
25
+ async function ensureManagedGitignoreBlock(projectDir, dryRun = false) {
26
+ const gitignorePath = path.join(projectDir, ".gitignore");
27
+ let existing = null;
28
+ try {
29
+ existing = await readFile(gitignorePath, "utf-8");
30
+ } catch {
31
+ }
32
+ if (existing === null) {
33
+ if (!dryRun) {
34
+ const block = buildBlock("\n");
35
+ await writeFile(gitignorePath, block, "utf-8");
36
+ }
37
+ return "created";
38
+ }
39
+ const nl = existing.includes("\r\n") ? "\r\n" : "\n";
40
+ const lines = existing.split(/\r?\n/);
41
+ const startIdx = lines.findIndex((l) => l === GITIGNORE_BLOCK_START);
42
+ const endIdx = lines.findIndex((l) => l === GITIGNORE_BLOCK_END);
43
+ const hasValidBlock = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx;
44
+ if (!hasValidBlock) {
45
+ const hasOrphanMarker = startIdx !== -1 || endIdx !== -1;
46
+ if (hasOrphanMarker) {
47
+ const hadTrailingNewline = existing.endsWith("\n");
48
+ const cleanedLines = lines.filter(
49
+ (l) => l !== GITIGNORE_BLOCK_START && l !== GITIGNORE_BLOCK_END
50
+ );
51
+ if (hadTrailingNewline && cleanedLines.length > 0 && cleanedLines[cleanedLines.length - 1] === "") {
52
+ cleanedLines.pop();
53
+ }
54
+ let content2 = cleanedLines.join(nl);
55
+ if (content2.length > 0) content2 += nl;
56
+ const newContent3 = content2 + buildBlock(nl);
57
+ if (!dryRun) {
58
+ await writeFile(gitignorePath, newContent3, "utf-8");
59
+ }
60
+ return "refreshed";
61
+ }
62
+ let content = existing;
63
+ if (!content.endsWith("\n") && !content.endsWith("\r\n")) {
64
+ content = content + nl;
65
+ }
66
+ const block = buildBlock(nl);
67
+ const newContent2 = content + block;
68
+ if (!dryRun) {
69
+ await writeFile(gitignorePath, newContent2, "utf-8");
70
+ }
71
+ return "appended";
72
+ }
73
+ const blockBodyLines = lines.slice(startIdx + 1, endIdx);
74
+ const canonicalBodyLines = [...CANONICAL_GITIGNORE_ENTRIES];
75
+ const bodyMatches = blockBodyLines.length === canonicalBodyLines.length && blockBodyLines.every((l, i) => l === canonicalBodyLines[i]);
76
+ if (bodyMatches) {
77
+ return "unchanged";
78
+ }
79
+ const blockLines = [
80
+ GITIGNORE_BLOCK_START,
81
+ ...CANONICAL_GITIGNORE_ENTRIES,
82
+ GITIGNORE_BLOCK_END
83
+ ];
84
+ const newLines = [
85
+ ...lines.slice(0, startIdx),
86
+ ...blockLines,
87
+ ...lines.slice(endIdx + 1)
88
+ ];
89
+ const newContent = newLines.join(nl);
90
+ const finalContent = newContent.endsWith(nl) ? newContent : newContent + nl;
91
+ if (!dryRun) {
92
+ await writeFile(gitignorePath, finalContent, "utf-8");
93
+ }
94
+ return "refreshed";
95
+ }
96
+ async function removeManagedGitignoreBlock(projectDir, dryRun = false) {
97
+ const gitignorePath = path.join(projectDir, ".gitignore");
98
+ let existing = null;
99
+ try {
100
+ existing = await readFile(gitignorePath, "utf-8");
101
+ } catch {
102
+ return "unchanged";
103
+ }
104
+ const nl = existing.includes("\r\n") ? "\r\n" : "\n";
105
+ const hadTrailingNewline = existing.endsWith("\n");
106
+ const lines = existing.split(/\r?\n/);
107
+ if (hadTrailingNewline && lines.length > 0 && lines[lines.length - 1] === "") {
108
+ lines.pop();
109
+ }
110
+ const startIdx = lines.findIndex((l) => l === GITIGNORE_BLOCK_START);
111
+ const endIdx = lines.findIndex((l) => l === GITIGNORE_BLOCK_END);
112
+ const hasValidBlock = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx;
113
+ if (!hasValidBlock) {
114
+ const hasOrphanMarker = startIdx !== -1 || endIdx !== -1;
115
+ if (!hasOrphanMarker) {
116
+ return "unchanged";
117
+ }
118
+ const cleaned = lines.filter(
119
+ (l) => l !== GITIGNORE_BLOCK_START && l !== GITIGNORE_BLOCK_END
120
+ );
121
+ const orphanContent = cleaned.length > 0 ? cleaned.join(nl) + nl : "";
122
+ if (!dryRun) {
123
+ await writeFile(gitignorePath, orphanContent, "utf-8");
124
+ }
125
+ return "removed";
126
+ }
127
+ const before = lines.slice(0, startIdx);
128
+ const after = lines.slice(endIdx + 1);
129
+ if (before.length > 0 && after.length > 0 && before[before.length - 1].trim() === "" && after[0].trim() === "") {
130
+ after.shift();
131
+ }
132
+ const merged = [...before, ...after];
133
+ const newContent = merged.length > 0 ? merged.join(nl) + nl : "";
134
+ if (!dryRun) {
135
+ await writeFile(gitignorePath, newContent, "utf-8");
136
+ }
137
+ return "removed";
138
+ }
139
+ function buildBlock(nl) {
140
+ return GITIGNORE_BLOCK_START + nl + CANONICAL_GITIGNORE_ENTRIES.join(nl) + nl + GITIGNORE_BLOCK_END + nl;
141
+ }
142
+ var CANONICAL_GITIGNORE_ENTRIES, GITIGNORE_BLOCK_START, GITIGNORE_BLOCK_END;
143
+ var init_gitignore_block = __esm({
144
+ "src/lib/gitignore-block.ts"() {
145
+ "use strict";
146
+ CANONICAL_GITIGNORE_ENTRIES = [
147
+ ".claude/settings.local.json",
148
+ ".claude/scheduled_tasks.lock",
149
+ ".codebyplan/device.local.json",
150
+ ".codebyplan/statusline.local.json",
151
+ ".codebyplan.local.json",
152
+ ".mcp.json"
153
+ ];
154
+ GITIGNORE_BLOCK_START = "# >>> codebyplan (managed) >>>";
155
+ GITIGNORE_BLOCK_END = "# <<< codebyplan <<<";
156
+ }
157
+ });
158
+
22
159
  // src/lib/local-config.ts
23
160
  import { execSync } from "node:child_process";
24
161
  import { createHash } from "node:crypto";
25
- import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
162
+ import { mkdir, readFile as readFile2, stat, writeFile as writeFile2 } from "node:fs/promises";
26
163
  import { hostname } from "node:os";
27
- import { dirname, join } from "node:path";
164
+ import { dirname, join as join2 } from "node:path";
28
165
  function localConfigPath(projectPath) {
29
- return join(projectPath, ".codebyplan", "device.local.json");
166
+ return join2(projectPath, ".codebyplan", "device.local.json");
30
167
  }
31
168
  function legacyLocalConfigPath(projectPath) {
32
- return join(projectPath, ".codebyplan.local.json");
169
+ return join2(projectPath, ".codebyplan.local.json");
33
170
  }
34
171
  async function readLocalConfig(projectPath, onMigrationNotice) {
35
172
  try {
36
- const raw = await readFile(localConfigPath(projectPath), "utf-8");
173
+ const raw = await readFile2(localConfigPath(projectPath), "utf-8");
37
174
  const parsed = JSON.parse(raw);
38
175
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) && typeof parsed.device_id === "string") {
39
176
  return parsed;
@@ -68,7 +205,7 @@ async function readLocalConfig(projectPath, onMigrationNotice) {
68
205
  }
69
206
  }
70
207
  try {
71
- const raw = await readFile(legacyLocalConfigPath(projectPath), "utf-8");
208
+ const raw = await readFile2(legacyLocalConfigPath(projectPath), "utf-8");
72
209
  const parsed = JSON.parse(raw);
73
210
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) && typeof parsed.device_id === "string") {
74
211
  onMigrationNotice?.(
@@ -88,8 +225,8 @@ async function readLocalConfig(projectPath, onMigrationNotice) {
88
225
  }
89
226
  async function writeLocalConfig(projectPath, config) {
90
227
  const content = { device_id: config.device_id };
91
- const path6 = localConfigPath(projectPath);
92
- const dirPath = dirname(path6);
228
+ const path7 = localConfigPath(projectPath);
229
+ const dirPath = dirname(path7);
93
230
  let phase = "stat config directory";
94
231
  try {
95
232
  try {
@@ -109,7 +246,7 @@ async function writeLocalConfig(projectPath, config) {
109
246
  phase = "create config directory";
110
247
  await mkdir(dirPath, { recursive: true });
111
248
  phase = "write local config";
112
- await writeFile(path6, JSON.stringify(content, null, 2) + "\n", "utf-8");
249
+ await writeFile2(path7, JSON.stringify(content, null, 2) + "\n", "utf-8");
113
250
  } catch (err) {
114
251
  const code = err.code;
115
252
  if (code === "LEGACY_FILE_BLOCKS_DIR") {
@@ -121,7 +258,7 @@ async function writeLocalConfig(projectPath, config) {
121
258
  }
122
259
  async function resolveMachineSeed() {
123
260
  try {
124
- const raw = await readFile("/etc/machine-id", "utf-8");
261
+ const raw = await readFile2("/etc/machine-id", "utf-8");
125
262
  const trimmed = raw.trim();
126
263
  if (trimmed) return trimmed;
127
264
  } catch {
@@ -153,13 +290,13 @@ var init_local_config = __esm({
153
290
 
154
291
  // src/lib/statusline-config.ts
155
292
  import { spawnSync } from "node:child_process";
156
- import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
157
- import { join as join2 } from "node:path";
293
+ import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
294
+ import { join as join3 } from "node:path";
158
295
  function statuslineConfigPath(projectPath) {
159
- return join2(projectPath, ".codebyplan", "statusline.json");
296
+ return join3(projectPath, ".codebyplan", "statusline.json");
160
297
  }
161
298
  function statuslineLocalConfigPath(projectPath) {
162
- return join2(projectPath, ".codebyplan", "statusline.local.json");
299
+ return join3(projectPath, ".codebyplan", "statusline.local.json");
163
300
  }
164
301
  function isValidRenderer(value) {
165
302
  return typeof value === "string" && VALID_RENDERERS.has(value);
@@ -188,7 +325,7 @@ function mergeOverDefaults(raw) {
188
325
  }
189
326
  async function readStatuslineConfig(projectPath) {
190
327
  try {
191
- const raw = await readFile2(statuslineConfigPath(projectPath), "utf-8");
328
+ const raw = await readFile3(statuslineConfigPath(projectPath), "utf-8");
192
329
  const parsed = JSON.parse(raw);
193
330
  return mergeOverDefaults(parsed);
194
331
  } catch {
@@ -197,7 +334,7 @@ async function readStatuslineConfig(projectPath) {
197
334
  }
198
335
  async function readStatuslineLocalConfig(projectPath) {
199
336
  try {
200
- const raw = await readFile2(statuslineLocalConfigPath(projectPath), "utf-8");
337
+ const raw = await readFile3(statuslineLocalConfigPath(projectPath), "utf-8");
201
338
  const parsed = JSON.parse(raw);
202
339
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
203
340
  const obj = parsed;
@@ -217,8 +354,8 @@ async function writeStatuslineLocalConfig(projectPath, localConfig) {
217
354
  );
218
355
  }
219
356
  const filePath = statuslineLocalConfigPath(projectPath);
220
- await mkdir2(join2(projectPath, ".codebyplan"), { recursive: true });
221
- await writeFile2(
357
+ await mkdir2(join3(projectPath, ".codebyplan"), { recursive: true });
358
+ await writeFile3(
222
359
  filePath,
223
360
  JSON.stringify(localConfig, null, 2) + "\n",
224
361
  "utf-8"
@@ -258,7 +395,8 @@ var init_statusline_config = __esm({
258
395
  cost: true,
259
396
  rate_limits: true,
260
397
  repo_pr: true,
261
- worktree: true
398
+ worktree: true,
399
+ infra_drift: true
262
400
  },
263
401
  no_color: false
264
402
  };
@@ -297,9 +435,9 @@ var init_jwt_decode = __esm({
297
435
  });
298
436
 
299
437
  // src/oauth/keychain.ts
300
- import { chmod, mkdir as mkdir3, readFile as readFile3, unlink, writeFile as writeFile3 } from "node:fs/promises";
438
+ import { chmod, mkdir as mkdir3, readFile as readFile4, unlink, writeFile as writeFile4 } from "node:fs/promises";
301
439
  import { homedir, platform } from "node:os";
302
- import { dirname as dirname2, join as join3 } from "node:path";
440
+ import { dirname as dirname2, join as join4 } from "node:path";
303
441
  async function loadKeyring() {
304
442
  if (keyringOverride !== void 0) return keyringOverride;
305
443
  try {
@@ -311,30 +449,30 @@ async function loadKeyring() {
311
449
  }
312
450
  function fallbackDir() {
313
451
  if (platform() === "win32") {
314
- const appData = process.env.APPDATA ?? join3(homedir(), "AppData", "Roaming");
315
- return join3(appData, "codebyplan");
452
+ const appData = process.env.APPDATA ?? join4(homedir(), "AppData", "Roaming");
453
+ return join4(appData, "codebyplan");
316
454
  }
317
- const xdg = process.env.XDG_CONFIG_HOME ?? join3(homedir(), ".config");
318
- return join3(xdg, "codebyplan");
455
+ const xdg = process.env.XDG_CONFIG_HOME ?? join4(homedir(), ".config");
456
+ return join4(xdg, "codebyplan");
319
457
  }
320
458
  function fallbackFile(filename) {
321
- return join3(fallbackDir(), filename);
459
+ return join4(fallbackDir(), filename);
322
460
  }
323
461
  async function readFallback(filename) {
324
462
  try {
325
- const raw = await readFile3(fallbackFile(filename), "utf-8");
463
+ const raw = await readFile4(fallbackFile(filename), "utf-8");
326
464
  return JSON.parse(raw);
327
465
  } catch {
328
466
  return null;
329
467
  }
330
468
  }
331
469
  async function writeFallback(filename, data) {
332
- const path6 = fallbackFile(filename);
333
- await mkdir3(dirname2(path6), { recursive: true });
334
- await writeFile3(path6, JSON.stringify(data, null, 2) + "\n", "utf-8");
470
+ const path7 = fallbackFile(filename);
471
+ await mkdir3(dirname2(path7), { recursive: true });
472
+ await writeFile4(path7, JSON.stringify(data, null, 2) + "\n", "utf-8");
335
473
  if (platform() !== "win32") {
336
474
  try {
337
- await chmod(path6, 384);
475
+ await chmod(path7, 384);
338
476
  } catch {
339
477
  }
340
478
  }
@@ -541,8 +679,8 @@ async function getAuthHeaders() {
541
679
  return { headers: { "x-api-key": key }, via: "api_key" };
542
680
  }
543
681
  }
544
- function buildUrl(path6, params) {
545
- const url = new URL(`${baseUrl()}/api${path6}`);
682
+ function buildUrl(path7, params) {
683
+ const url = new URL(`${baseUrl()}/api${path7}`);
546
684
  if (params) {
547
685
  for (const [key, value] of Object.entries(params)) {
548
686
  if (value !== void 0) {
@@ -561,8 +699,8 @@ function isRetryable(err) {
561
699
  function delay(ms) {
562
700
  return new Promise((resolve5) => setTimeout(resolve5, ms));
563
701
  }
564
- async function request(method, path6, options) {
565
- const url = buildUrl(path6, options?.params);
702
+ async function request(method, path7, options) {
703
+ const url = buildUrl(path7, options?.params);
566
704
  const auth = await getAuthHeaders();
567
705
  let lastError;
568
706
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
@@ -582,7 +720,7 @@ async function request(method, path6, options) {
582
720
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
583
721
  });
584
722
  if (!res.ok) {
585
- let message = `API ${method} ${path6} failed with status ${res.status}`;
723
+ let message = `API ${method} ${path7} failed with status ${res.status}`;
586
724
  let code;
587
725
  try {
588
726
  const body = await res.json();
@@ -616,14 +754,14 @@ async function request(method, path6, options) {
616
754
  }
617
755
  throw lastError;
618
756
  }
619
- async function apiGet(path6, params) {
620
- return request("GET", path6, { params });
757
+ async function apiGet(path7, params) {
758
+ return request("GET", path7, { params });
621
759
  }
622
- async function apiPost(path6, body) {
623
- return request("POST", path6, { body });
760
+ async function apiPost(path7, body) {
761
+ return request("POST", path7, { body });
624
762
  }
625
- async function apiPut(path6, body) {
626
- return request("PUT", path6, { body });
763
+ async function apiPut(path7, body) {
764
+ return request("PUT", path7, { body });
627
765
  }
628
766
  async function callMcpTool(toolName, params) {
629
767
  const url = mcpEndpoint();
@@ -682,8 +820,8 @@ var init_api = __esm({
682
820
  });
683
821
 
684
822
  // src/lib/resolve-worktree.ts
685
- import { readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
686
- import { join as join4 } from "node:path";
823
+ import { readFile as readFile5, writeFile as writeFile5 } from "node:fs/promises";
824
+ import { join as join5 } from "node:path";
687
825
  async function resolveAndCacheWorktreeId(repoId, projectPath, options) {
688
826
  let worktreeId;
689
827
  try {
@@ -705,10 +843,10 @@ async function resolveAndCacheWorktreeId(repoId, projectPath, options) {
705
843
  if (options?.skipWrite) {
706
844
  return worktreeId;
707
845
  }
708
- const codebyplanPath = join4(projectPath, ".codebyplan.json");
846
+ const codebyplanPath = join5(projectPath, ".codebyplan.json");
709
847
  let currentConfig = {};
710
848
  try {
711
- const raw = await readFile4(codebyplanPath, "utf-8");
849
+ const raw = await readFile5(codebyplanPath, "utf-8");
712
850
  const parsed = JSON.parse(raw);
713
851
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
714
852
  currentConfig = parsed;
@@ -723,7 +861,7 @@ async function resolveAndCacheWorktreeId(repoId, projectPath, options) {
723
861
  worktree_id: worktreeId
724
862
  };
725
863
  try {
726
- await writeFile4(
864
+ await writeFile5(
727
865
  codebyplanPath,
728
866
  JSON.stringify(merged, null, 2) + "\n",
729
867
  "utf-8"
@@ -840,28 +978,36 @@ var init_browser = __esm({
840
978
  });
841
979
 
842
980
  // src/oauth/device-flow.ts
843
- async function initiateDeviceCode(clientId) {
981
+ async function initiateDeviceCode(clientId, scope, resource = "https://mcp.codebyplan.com/") {
982
+ const body = { client_id: clientId, resource };
983
+ if (scope !== void 0) {
984
+ body.scope = scope;
985
+ }
844
986
  const res = await fetch(deviceEndpoint(), {
845
987
  method: "POST",
846
988
  headers: { "Content-Type": "application/json" },
847
- body: JSON.stringify({ client_id: clientId }),
989
+ body: JSON.stringify(body),
848
990
  signal: AbortSignal.timeout(1e4)
849
991
  });
850
992
  if (!res.ok) {
851
- const body = await res.json().catch(() => ({}));
852
- if (body.error === "invalid_client") {
993
+ const body2 = await res.json().catch(() => ({}));
994
+ if (body2.error === "invalid_client") {
853
995
  throw new OAuthInvalidClientError();
854
996
  }
855
- const msg = body.error_description ?? body.error ?? `HTTP ${res.status}`;
997
+ const msg = body2.error_description ?? body2.error ?? `HTTP ${res.status}`;
856
998
  throw new Error(`Failed to initiate device flow: ${msg}`);
857
999
  }
858
- return await res.json();
1000
+ const dcResponse = await res.json();
1001
+ return { ...dcResponse, resource };
859
1002
  }
860
- async function pollOnce(deviceCode, clientId) {
1003
+ async function pollOnce(deviceCode, clientId, resource) {
861
1004
  const params = new URLSearchParams();
862
1005
  params.set("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
863
1006
  params.set("device_code", deviceCode);
864
1007
  params.set("client_id", clientId);
1008
+ if (resource !== void 0) {
1009
+ params.set("resource", resource);
1010
+ }
865
1011
  const res = await fetch(tokenEndpoint(), {
866
1012
  method: "POST",
867
1013
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
@@ -880,15 +1026,19 @@ async function pollOnce(deviceCode, clientId) {
880
1026
  return { kind: "error", message: description };
881
1027
  }
882
1028
  async function pollUntilSettled(opts) {
883
- const sleep = opts.sleep ?? defaultSleep;
1029
+ const sleep2 = opts.sleep ?? defaultSleep;
884
1030
  const now = opts.now ?? Date.now;
885
1031
  const startMs = now();
886
1032
  const expiresAtMs = startMs + opts.expiresInSec * 1e3;
887
1033
  while (now() < expiresAtMs) {
888
- const outcome = await pollOnce(opts.deviceCode, opts.clientId);
1034
+ const outcome = await pollOnce(
1035
+ opts.deviceCode,
1036
+ opts.clientId,
1037
+ opts.resource
1038
+ );
889
1039
  if (outcome.kind !== "pending") return outcome;
890
1040
  if (opts.onTick) opts.onTick(Math.floor((now() - startMs) / 1e3));
891
- await sleep(opts.intervalSec * 1e3);
1041
+ await sleep2(opts.intervalSec * 1e3);
892
1042
  }
893
1043
  return { kind: "expired" };
894
1044
  }
@@ -988,10 +1138,11 @@ async function fetchEmail(accessToken) {
988
1138
  return null;
989
1139
  }
990
1140
  }
991
- async function runLogin() {
1141
+ async function runLogin(options = {}) {
1142
+ const scope = options.admin ? "mcp:read mcp:write mcp:admin" : "mcp:read mcp:write";
992
1143
  console.log("\n CodeByPlan login\n");
993
1144
  const { clientId, result: dc } = await ensureClientFor(
994
- (id) => initiateDeviceCode(id)
1145
+ (id) => initiateDeviceCode(id, scope)
995
1146
  );
996
1147
  const verificationUri = dc.verification_uri_complete ?? dc.verification_uri;
997
1148
  console.log(` Visit: ${verificationUri}`);
@@ -1005,7 +1156,8 @@ async function runLogin() {
1005
1156
  deviceCode: dc.device_code,
1006
1157
  clientId,
1007
1158
  intervalSec: dc.interval,
1008
- expiresInSec: dc.expires_in
1159
+ expiresInSec: dc.expires_in,
1160
+ resource: dc.resource
1009
1161
  });
1010
1162
  if (outcome.kind === "denied") {
1011
1163
  console.log(" Denied. Authorization request was rejected.\n");
@@ -1057,21 +1209,21 @@ var setup_exports = {};
1057
1209
  __export(setup_exports, {
1058
1210
  runSetup: () => runSetup
1059
1211
  });
1060
- import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile5 } from "node:fs/promises";
1212
+ import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile6 } from "node:fs/promises";
1061
1213
  import { homedir as homedir2 } from "node:os";
1062
- import { join as join5 } from "node:path";
1214
+ import { join as join6 } from "node:path";
1063
1215
  import { stdin, stdout as stdout2 } from "node:process";
1064
1216
  import { createInterface } from "node:readline/promises";
1065
1217
  function getConfigPath(scope) {
1066
- return scope === "user" ? join5(homedir2(), ".claude.json") : join5(process.cwd(), ".mcp.json");
1218
+ return scope === "user" ? join6(homedir2(), ".claude.json") : join6(process.cwd(), ".mcp.json");
1067
1219
  }
1068
1220
  function legacyMcpUrl() {
1069
1221
  const baseUrl2 = process.env.CODEBYPLAN_API_URL ?? "https://www.codebyplan.com";
1070
1222
  return `${baseUrl2.replace(/\/$/, "")}/mcp`;
1071
1223
  }
1072
- async function readConfig(path6) {
1224
+ async function readConfig(path7) {
1073
1225
  try {
1074
- const raw = await readFile5(path6, "utf-8");
1226
+ const raw = await readFile6(path7, "utf-8");
1075
1227
  const parsed = JSON.parse(raw);
1076
1228
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1077
1229
  return parsed;
@@ -1094,7 +1246,7 @@ async function writeMcpConfig(scope, auth) {
1094
1246
  config.mcpServers = {};
1095
1247
  }
1096
1248
  config.mcpServers.codebyplan = buildMcpEntry(auth);
1097
- await writeFile5(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1249
+ await writeFile6(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1098
1250
  return configPath;
1099
1251
  }
1100
1252
  async function fetchRepos(auth) {
@@ -1149,7 +1301,7 @@ async function chooseAuthMode(rl) {
1149
1301
  return { kind: "legacy", apiKey };
1150
1302
  }
1151
1303
  async function writeCodebyplanDirectory(projectPath, selectedRepo, deviceId) {
1152
- const codebyplanDir = join5(projectPath, ".codebyplan");
1304
+ const codebyplanDir = join6(projectPath, ".codebyplan");
1153
1305
  await mkdir4(codebyplanDir, { recursive: true });
1154
1306
  const repoJson = {
1155
1307
  repo_id: selectedRepo.id
@@ -1161,13 +1313,13 @@ async function writeCodebyplanDirectory(projectPath, selectedRepo, deviceId) {
1161
1313
  if (typeof repoAny.project_id === "string") {
1162
1314
  repoJson.project_id = repoAny.project_id;
1163
1315
  }
1164
- await writeFile5(
1165
- join5(codebyplanDir, "repo.json"),
1316
+ await writeFile6(
1317
+ join6(codebyplanDir, "repo.json"),
1166
1318
  JSON.stringify(repoJson, null, 2) + "\n",
1167
1319
  "utf-8"
1168
1320
  );
1169
- await writeFile5(
1170
- join5(codebyplanDir, "server.json"),
1321
+ await writeFile6(
1322
+ join6(codebyplanDir, "server.json"),
1171
1323
  JSON.stringify(
1172
1324
  {
1173
1325
  server_port: null,
@@ -1180,35 +1332,40 @@ async function writeCodebyplanDirectory(projectPath, selectedRepo, deviceId) {
1180
1332
  ) + "\n",
1181
1333
  "utf-8"
1182
1334
  );
1183
- await writeFile5(
1184
- join5(codebyplanDir, "git.json"),
1335
+ await writeFile6(
1336
+ join6(codebyplanDir, "git.json"),
1185
1337
  JSON.stringify({ git_branch: null, branch_config: null }, null, 2) + "\n",
1186
1338
  "utf-8"
1187
1339
  );
1188
- await writeFile5(
1189
- join5(codebyplanDir, "shipment.json"),
1340
+ await writeFile6(
1341
+ join6(codebyplanDir, "shipment.json"),
1190
1342
  JSON.stringify({ shipment: null }, null, 2) + "\n",
1191
1343
  "utf-8"
1192
1344
  );
1193
- await writeFile5(
1194
- join5(codebyplanDir, "vendor.json"),
1345
+ await writeFile6(
1346
+ join6(codebyplanDir, "vendor.json"),
1347
+ JSON.stringify({}, null, 2) + "\n",
1348
+ "utf-8"
1349
+ );
1350
+ await writeFile6(
1351
+ join6(codebyplanDir, "e2e.json"),
1195
1352
  JSON.stringify({}, null, 2) + "\n",
1196
1353
  "utf-8"
1197
1354
  );
1198
- await writeFile5(
1199
- join5(codebyplanDir, "e2e.json"),
1355
+ await writeFile6(
1356
+ join6(codebyplanDir, "eslint.json"),
1200
1357
  JSON.stringify({}, null, 2) + "\n",
1201
1358
  "utf-8"
1202
1359
  );
1203
- const statuslinePath = join5(codebyplanDir, "statusline.json");
1360
+ const statuslinePath = join6(codebyplanDir, "statusline.json");
1204
1361
  let statuslineExists = false;
1205
1362
  try {
1206
- await readFile5(statuslinePath, "utf-8");
1363
+ await readFile6(statuslinePath, "utf-8");
1207
1364
  statuslineExists = true;
1208
1365
  } catch {
1209
1366
  }
1210
1367
  if (!statuslineExists) {
1211
- await writeFile5(
1368
+ await writeFile6(
1212
1369
  statuslinePath,
1213
1370
  JSON.stringify(STATUSLINE_DEFAULTS, null, 2) + "\n",
1214
1371
  "utf-8"
@@ -1217,36 +1374,12 @@ async function writeCodebyplanDirectory(projectPath, selectedRepo, deviceId) {
1217
1374
  await writeLocalConfig(projectPath, { device_id: deviceId });
1218
1375
  console.log(` Created ${codebyplanDir}/`);
1219
1376
  console.log(
1220
- ` repo.json, server.json, git.json, shipment.json, vendor.json, e2e.json, statusline.json`
1377
+ ` repo.json, server.json, git.json, shipment.json, vendor.json, e2e.json, eslint.json, statusline.json`
1221
1378
  );
1222
1379
  console.log(` device.local.json (gitignored)`);
1223
- const gitignorePath = join5(projectPath, ".gitignore");
1224
- const gitignoreEntry = ".codebyplan/device.local.json";
1225
- const statuslineLocalEntry = ".codebyplan/statusline.local.json";
1226
- try {
1227
- const existing = await readFile5(gitignorePath, "utf-8");
1228
- const lines = existing.split("\n");
1229
- let content = existing;
1230
- if (!lines.some((l) => l.trimEnd() === gitignoreEntry)) {
1231
- content = (content.endsWith("\n") ? content : content + "\n") + gitignoreEntry + "\n";
1232
- console.log(` Added '${gitignoreEntry}' to .gitignore`);
1233
- }
1234
- if (!lines.some((l) => l.trimEnd() === statuslineLocalEntry)) {
1235
- content = (content.endsWith("\n") ? content : content + "\n") + statuslineLocalEntry + "\n";
1236
- console.log(` Added '${statuslineLocalEntry}' to .gitignore`);
1237
- }
1238
- if (content !== existing) {
1239
- await writeFile5(gitignorePath, content, "utf-8");
1240
- }
1241
- } catch {
1242
- await writeFile5(
1243
- gitignorePath,
1244
- gitignoreEntry + "\n" + statuslineLocalEntry + "\n",
1245
- "utf-8"
1246
- );
1247
- console.log(
1248
- ` Created .gitignore with '${gitignoreEntry}' and '${statuslineLocalEntry}'`
1249
- );
1380
+ const gitignoreAction = await ensureManagedGitignoreBlock(projectPath);
1381
+ if (gitignoreAction !== "unchanged") {
1382
+ console.log(" Updated .gitignore (codebyplan managed block)");
1250
1383
  }
1251
1384
  }
1252
1385
  async function runSetup() {
@@ -1377,6 +1510,7 @@ async function runSetup() {
1377
1510
  var init_setup = __esm({
1378
1511
  "src/cli/setup.ts"() {
1379
1512
  "use strict";
1513
+ init_gitignore_block();
1380
1514
  init_local_config();
1381
1515
  init_statusline_config();
1382
1516
  init_resolve_worktree();
@@ -1387,21 +1521,21 @@ var init_setup = __esm({
1387
1521
  });
1388
1522
 
1389
1523
  // src/lib/flags.ts
1390
- import { readFile as readFile6 } from "node:fs/promises";
1391
- import { join as join6, resolve } from "node:path";
1524
+ import { readFile as readFile7 } from "node:fs/promises";
1525
+ import { join as join7, resolve } from "node:path";
1392
1526
  async function findCodebyplanConfig(startDir, maxDepth = 20) {
1393
1527
  let cursor = resolve(startDir);
1394
1528
  for (let depth = 0; depth < maxDepth; depth++) {
1395
- const sentinelPath2 = join6(cursor, ".codebyplan", "repo.json");
1529
+ const sentinelPath2 = join7(cursor, ".codebyplan", "repo.json");
1396
1530
  try {
1397
- const raw = await readFile6(sentinelPath2, "utf-8");
1531
+ const raw = await readFile7(sentinelPath2, "utf-8");
1398
1532
  const parsed = JSON.parse(raw);
1399
1533
  return { path: sentinelPath2, contents: parsed };
1400
1534
  } catch {
1401
1535
  }
1402
- const legacyPath = join6(cursor, ".codebyplan.json");
1536
+ const legacyPath = join7(cursor, ".codebyplan.json");
1403
1537
  try {
1404
- const raw = await readFile6(legacyPath, "utf-8");
1538
+ const raw = await readFile7(legacyPath, "utf-8");
1405
1539
  const parsed = JSON.parse(raw);
1406
1540
  return { path: legacyPath, contents: parsed };
1407
1541
  } catch {
@@ -1628,15 +1762,15 @@ var upgrade_auth_exports = {};
1628
1762
  __export(upgrade_auth_exports, {
1629
1763
  runUpgradeAuth: () => runUpgradeAuth
1630
1764
  });
1631
- import { readFile as readFile7, writeFile as writeFile6 } from "node:fs/promises";
1765
+ import { readFile as readFile8, writeFile as writeFile7 } from "node:fs/promises";
1632
1766
  import { homedir as homedir3 } from "node:os";
1633
- import { join as join7 } from "node:path";
1767
+ import { join as join8 } from "node:path";
1634
1768
  function configPaths() {
1635
- return [join7(homedir3(), ".claude.json"), join7(process.cwd(), ".mcp.json")];
1769
+ return [join8(homedir3(), ".claude.json"), join8(process.cwd(), ".mcp.json")];
1636
1770
  }
1637
- async function readConfig2(path6) {
1771
+ async function readConfig2(path7) {
1638
1772
  try {
1639
- const raw = await readFile7(path6, "utf-8");
1773
+ const raw = await readFile8(path7, "utf-8");
1640
1774
  const parsed = JSON.parse(raw);
1641
1775
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1642
1776
  return parsed;
@@ -1650,14 +1784,14 @@ function entryHasLegacyApiKey(entry) {
1650
1784
  if (!entry || !entry.headers) return false;
1651
1785
  return "x-api-key" in entry.headers;
1652
1786
  }
1653
- async function rewriteConfig(path6, config, newUrl) {
1787
+ async function rewriteConfig(path7, config, newUrl) {
1654
1788
  const servers = config.mcpServers;
1655
1789
  if (!servers) return false;
1656
1790
  const entry = servers.codebyplan;
1657
1791
  if (!entry) return false;
1658
1792
  if (!entryHasLegacyApiKey(entry) && entry.url === newUrl) return false;
1659
1793
  servers.codebyplan = { url: newUrl };
1660
- await writeFile6(path6, JSON.stringify(config, null, 2) + "\n", "utf-8");
1794
+ await writeFile7(path7, JSON.stringify(config, null, 2) + "\n", "utf-8");
1661
1795
  return true;
1662
1796
  }
1663
1797
  async function runUpgradeAuth() {
@@ -1665,12 +1799,12 @@ async function runUpgradeAuth() {
1665
1799
  await runLogin();
1666
1800
  const newUrl = mcpEndpoint();
1667
1801
  let migrated = 0;
1668
- for (const path6 of configPaths()) {
1669
- const config = await readConfig2(path6);
1802
+ for (const path7 of configPaths()) {
1803
+ const config = await readConfig2(path7);
1670
1804
  if (!config) continue;
1671
- const changed = await rewriteConfig(path6, config, newUrl);
1805
+ const changed = await rewriteConfig(path7, config, newUrl);
1672
1806
  if (changed) {
1673
- console.log(` Updated ${path6}`);
1807
+ console.log(` Updated ${path7}`);
1674
1808
  migrated++;
1675
1809
  }
1676
1810
  }
@@ -1738,8 +1872,8 @@ var init_confirm = __esm({
1738
1872
  });
1739
1873
 
1740
1874
  // src/lib/tech-detect.ts
1741
- import { readFile as readFile8, access, readdir } from "node:fs/promises";
1742
- import { join as join8, relative } from "node:path";
1875
+ import { readFile as readFile9, access, readdir } from "node:fs/promises";
1876
+ import { join as join9, relative } from "node:path";
1743
1877
  async function fileExists(filePath) {
1744
1878
  try {
1745
1879
  await access(filePath);
@@ -1752,8 +1886,8 @@ async function discoverMonorepoApps(projectPath) {
1752
1886
  const apps = [];
1753
1887
  const patterns = [];
1754
1888
  try {
1755
- const raw = await readFile8(
1756
- join8(projectPath, "pnpm-workspace.yaml"),
1889
+ const raw = await readFile9(
1890
+ join9(projectPath, "pnpm-workspace.yaml"),
1757
1891
  "utf-8"
1758
1892
  );
1759
1893
  const matches = raw.match(/^\s*-\s*['"]?([^'"#\n]+)['"]?/gm);
@@ -1767,7 +1901,7 @@ async function discoverMonorepoApps(projectPath) {
1767
1901
  }
1768
1902
  if (patterns.length === 0) {
1769
1903
  try {
1770
- const raw = await readFile8(join8(projectPath, "package.json"), "utf-8");
1904
+ const raw = await readFile9(join9(projectPath, "package.json"), "utf-8");
1771
1905
  const pkg = JSON.parse(raw);
1772
1906
  const ws = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages;
1773
1907
  if (ws) patterns.push(...ws);
@@ -1777,14 +1911,14 @@ async function discoverMonorepoApps(projectPath) {
1777
1911
  for (const pattern of patterns) {
1778
1912
  if (pattern.endsWith("/*")) {
1779
1913
  const dir = pattern.slice(0, -2);
1780
- const absDir = join8(projectPath, dir);
1914
+ const absDir = join9(projectPath, dir);
1781
1915
  try {
1782
1916
  const entries = await readdir(absDir, { withFileTypes: true });
1783
1917
  for (const entry of entries) {
1784
1918
  if (entry.isDirectory()) {
1785
- const relPath = join8(dir, entry.name);
1786
- const absPath = join8(absDir, entry.name);
1787
- if (await fileExists(join8(absPath, "package.json"))) {
1919
+ const relPath = join9(dir, entry.name);
1920
+ const absPath = join9(absDir, entry.name);
1921
+ if (await fileExists(join9(absPath, "package.json"))) {
1788
1922
  apps.push({ name: entry.name, path: relPath, absPath });
1789
1923
  }
1790
1924
  }
@@ -1803,7 +1937,7 @@ async function hasJsxFile(dir, depth = 0) {
1803
1937
  const name = entry.name;
1804
1938
  if (entry.isDirectory()) {
1805
1939
  if (SKIP_DIRS.has(name) || JSX_SKIP_DIRS.has(name)) continue;
1806
- if (await hasJsxFile(join8(dir, name), depth + 1)) return true;
1940
+ if (await hasJsxFile(join9(dir, name), depth + 1)) return true;
1807
1941
  } else if (entry.isFile()) {
1808
1942
  if (JSX_TEST_PATTERN.test(name)) continue;
1809
1943
  if (name.endsWith(".tsx") || name.endsWith(".jsx")) return true;
@@ -1822,7 +1956,7 @@ async function hasJsxFile(dir, depth = 0) {
1822
1956
  async function detectCapabilities(dirPath, pkgJson) {
1823
1957
  const caps = /* @__PURE__ */ new Set();
1824
1958
  for (const sub of JSX_SCAN_DIRS) {
1825
- if (await hasJsxFile(join8(dirPath, sub))) {
1959
+ if (await hasJsxFile(join9(dirPath, sub))) {
1826
1960
  caps.add("jsx");
1827
1961
  break;
1828
1962
  }
@@ -1844,7 +1978,7 @@ async function detectCapabilities(dirPath, pkgJson) {
1844
1978
  }
1845
1979
  }
1846
1980
  }
1847
- if (!caps.has("node-server") && await fileExists(join8(dirPath, "src", "main.ts"))) {
1981
+ if (!caps.has("node-server") && await fileExists(join9(dirPath, "src", "main.ts"))) {
1848
1982
  caps.add("node-server");
1849
1983
  }
1850
1984
  if (pkgJson && pkgJson.bin) {
@@ -1860,7 +1994,7 @@ async function detectFromDirectory(dirPath) {
1860
1994
  const seen = /* @__PURE__ */ new Map();
1861
1995
  let pkgJson = null;
1862
1996
  try {
1863
- const raw = await readFile8(join8(dirPath, "package.json"), "utf-8");
1997
+ const raw = await readFile9(join9(dirPath, "package.json"), "utf-8");
1864
1998
  pkgJson = JSON.parse(raw);
1865
1999
  const allDeps = {
1866
2000
  ...pkgJson.dependencies ?? {},
@@ -1892,7 +2026,7 @@ async function detectFromDirectory(dirPath) {
1892
2026
  }
1893
2027
  for (const { file, rule } of CONFIG_FILE_MAP) {
1894
2028
  const key = rule.name.toLowerCase();
1895
- if (!seen.has(key) && await fileExists(join8(dirPath, file))) {
2029
+ if (!seen.has(key) && await fileExists(join9(dirPath, file))) {
1896
2030
  seen.set(key, { name: rule.name, category: rule.category });
1897
2031
  }
1898
2032
  }
@@ -2070,7 +2204,7 @@ function categorizeDependency(depName) {
2070
2204
  async function findPackageJsonFiles(dir, projectPath, depth = 0) {
2071
2205
  if (depth > 4) return [];
2072
2206
  const results = [];
2073
- const pkgPath = join8(dir, "package.json");
2207
+ const pkgPath = join9(dir, "package.json");
2074
2208
  if (await fileExists(pkgPath)) {
2075
2209
  results.push(pkgPath);
2076
2210
  }
@@ -2079,7 +2213,7 @@ async function findPackageJsonFiles(dir, projectPath, depth = 0) {
2079
2213
  for (const entry of entries) {
2080
2214
  if (!entry.isDirectory() || SKIP_DIRS.has(entry.name)) continue;
2081
2215
  const subResults = await findPackageJsonFiles(
2082
- join8(dir, entry.name),
2216
+ join9(dir, entry.name),
2083
2217
  projectPath,
2084
2218
  depth + 1
2085
2219
  );
@@ -2094,7 +2228,7 @@ async function scanAllDependencies(projectPath) {
2094
2228
  const dependencies = [];
2095
2229
  for (const pkgPath of packageJsonPaths) {
2096
2230
  try {
2097
- const raw = await readFile8(pkgPath, "utf-8");
2231
+ const raw = await readFile9(pkgPath, "utf-8");
2098
2232
  const pkg = JSON.parse(raw);
2099
2233
  const sourcePath = relative(projectPath, pkgPath);
2100
2234
  const depSections = [
@@ -2718,8 +2852,8 @@ __export(eslint_exports, {
2718
2852
  eslintInit: () => eslintInit,
2719
2853
  runEslint: () => runEslint
2720
2854
  });
2721
- import { readFile as readFile9, writeFile as writeFile7, access as access2, readdir as readdir2 } from "node:fs/promises";
2722
- import { join as join9, relative as relative2 } from "node:path";
2855
+ import { readFile as readFile10, writeFile as writeFile8, access as access2, readdir as readdir2 } from "node:fs/promises";
2856
+ import { join as join10, relative as relative2 } from "node:path";
2723
2857
  async function fileExists2(filePath) {
2724
2858
  try {
2725
2859
  await access2(filePath);
@@ -2730,7 +2864,7 @@ async function fileExists2(filePath) {
2730
2864
  }
2731
2865
  async function autoDetectIgnorePatterns(absPath) {
2732
2866
  const patterns = [];
2733
- if (await fileExists2(join9(absPath, "esbuild.js"))) {
2867
+ if (await fileExists2(join10(absPath, "esbuild.js"))) {
2734
2868
  patterns.push("esbuild.js");
2735
2869
  }
2736
2870
  let entries = [];
@@ -2750,19 +2884,19 @@ async function autoDetectIgnorePatterns(absPath) {
2750
2884
  }
2751
2885
  for (const ext of ["ts", "mts", "js", "mjs"]) {
2752
2886
  const candidate = `vitest.config.${ext}`;
2753
- if (await fileExists2(join9(absPath, candidate))) {
2887
+ if (await fileExists2(join10(absPath, candidate))) {
2754
2888
  patterns.push(candidate);
2755
2889
  break;
2756
2890
  }
2757
2891
  }
2758
2892
  for (const ext of ["ts", "mts", "js", "mjs"]) {
2759
2893
  const candidate = `vite.config.${ext}`;
2760
- if (await fileExists2(join9(absPath, candidate))) {
2894
+ if (await fileExists2(join10(absPath, candidate))) {
2761
2895
  patterns.push(candidate);
2762
2896
  break;
2763
2897
  }
2764
2898
  }
2765
- if (await fileExists2(join9(absPath, "tauri.conf.json"))) {
2899
+ if (await fileExists2(join10(absPath, "tauri.conf.json"))) {
2766
2900
  patterns.push("src-tauri/**");
2767
2901
  patterns.push("**/*.d.ts");
2768
2902
  }
@@ -2770,14 +2904,14 @@ async function autoDetectIgnorePatterns(absPath) {
2770
2904
  }
2771
2905
  function detectPackageManager(projectPath) {
2772
2906
  return (async () => {
2773
- if (await fileExists2(join9(projectPath, "pnpm-lock.yaml"))) return "pnpm";
2774
- if (await fileExists2(join9(projectPath, "yarn.lock"))) return "yarn";
2907
+ if (await fileExists2(join10(projectPath, "pnpm-lock.yaml"))) return "pnpm";
2908
+ if (await fileExists2(join10(projectPath, "yarn.lock"))) return "yarn";
2775
2909
  return "npm";
2776
2910
  })();
2777
2911
  }
2778
2912
  async function getInstalledDeps(pkgJsonPath) {
2779
2913
  try {
2780
- const raw = await readFile9(pkgJsonPath, "utf-8");
2914
+ const raw = await readFile10(pkgJsonPath, "utf-8");
2781
2915
  const pkg = JSON.parse(raw);
2782
2916
  const all = /* @__PURE__ */ new Set();
2783
2917
  for (const name of Object.keys(pkg.dependencies ?? {})) all.add(name);
@@ -2890,7 +3024,7 @@ async function eslintInit(repoId, projectPath) {
2890
3024
  ignorePatterns: detectedIgnores
2891
3025
  });
2892
3026
  const hash = hashConfig(content);
2893
- const configPath = join9(target.absPath, "eslint.config.mjs");
3027
+ const configPath = join10(target.absPath, "eslint.config.mjs");
2894
3028
  configsToWrite.push({
2895
3029
  target,
2896
3030
  presets,
@@ -2912,11 +3046,11 @@ async function eslintInit(repoId, projectPath) {
2912
3046
  return;
2913
3047
  }
2914
3048
  const pm = await detectPackageManager(projectPath);
2915
- const rootPkgJsonPath = join9(projectPath, "package.json");
3049
+ const rootPkgJsonPath = join10(projectPath, "package.json");
2916
3050
  const installed = await getInstalledDeps(rootPkgJsonPath);
2917
3051
  if (isMonorepo) {
2918
3052
  for (const { target } of configsToWrite) {
2919
- const appPkgJson = join9(target.absPath, "package.json");
3053
+ const appPkgJson = join10(target.absPath, "package.json");
2920
3054
  const appDeps = await getInstalledDeps(appPkgJson);
2921
3055
  for (const dep of appDeps) {
2922
3056
  installed.add(dep);
@@ -2968,7 +3102,7 @@ async function eslintInit(repoId, projectPath) {
2968
3102
  } of configsToWrite) {
2969
3103
  if (await fileExists2(configPath)) {
2970
3104
  try {
2971
- const existing = await readFile9(configPath, "utf-8");
3105
+ const existing = await readFile10(configPath, "utf-8");
2972
3106
  const existingHash = hashConfig(existing);
2973
3107
  if (existingHash === hash) {
2974
3108
  console.log(
@@ -2988,7 +3122,7 @@ async function eslintInit(repoId, projectPath) {
2988
3122
  }
2989
3123
  }
2990
3124
  try {
2991
- await writeFile7(configPath, content, "utf-8");
3125
+ await writeFile8(configPath, content, "utf-8");
2992
3126
  } catch (err) {
2993
3127
  console.error(
2994
3128
  ` ${target.name}: Failed to write config: ${err instanceof Error ? err.message : String(err)}`
@@ -3292,12 +3426,48 @@ var init_sync_approvals = __esm({
3292
3426
  // src/cli/round.ts
3293
3427
  var round_exports = {};
3294
3428
  __export(round_exports, {
3429
+ RETRY_DELAY_MS: () => RETRY_DELAY_MS,
3430
+ fetchRoundsWithRetry: () => fetchRoundsWithRetry,
3431
+ isTransientMcpError: () => isTransientMcpError,
3295
3432
  runRoundCommand: () => runRoundCommand,
3296
- runRoundSyncApprovals: () => runRoundSyncApprovals
3433
+ runRoundSyncApprovals: () => runRoundSyncApprovals,
3434
+ setRetryDelayMs: () => setRetryDelayMs
3297
3435
  });
3298
3436
  import { access as access3 } from "node:fs/promises";
3299
- import { join as join10 } from "node:path";
3437
+ import { join as join11 } from "node:path";
3300
3438
  import { execSync as execSync2 } from "node:child_process";
3439
+ function setRetryDelayMs(ms) {
3440
+ RETRY_DELAY_MS = ms;
3441
+ }
3442
+ function sleep(ms) {
3443
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
3444
+ }
3445
+ function isTransientMcpError(err) {
3446
+ if (!(err instanceof McpError)) return false;
3447
+ if (err.status !== void 0) {
3448
+ return err.status >= 500 && err.status <= 599;
3449
+ }
3450
+ return err.message.toLowerCase().includes("network error");
3451
+ }
3452
+ async function fetchRoundsWithRetry(taskId, options = {}) {
3453
+ const maxRetries = options.retries ?? 2;
3454
+ const delayMs = options.delayMs ?? RETRY_DELAY_MS;
3455
+ let lastErr;
3456
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
3457
+ try {
3458
+ return await mcpCall("get_rounds", { task_id: taskId });
3459
+ } catch (err) {
3460
+ if (!isTransientMcpError(err)) {
3461
+ throw err;
3462
+ }
3463
+ lastErr = err;
3464
+ if (attempt < maxRetries) {
3465
+ await sleep(delayMs);
3466
+ }
3467
+ }
3468
+ }
3469
+ throw lastErr;
3470
+ }
3301
3471
  async function runRoundCommand(args) {
3302
3472
  const subcommand = args[0];
3303
3473
  if (subcommand === "sync-approvals") {
@@ -3321,7 +3491,7 @@ Run 'codebyplan round help' for usage.
3321
3491
  }
3322
3492
  function printRoundHelp() {
3323
3493
  process.stdout.write(
3324
- "\n codebyplan round <subcommand>\n\n Subcommands:\n sync-approvals Sync git diff and approvals with round/task state\n\n sync-approvals flags:\n --round-id <uuid> Round UUID (required)\n --task-id <uuid> Task UUID (required)\n --worktree-id <uuid> Worktree UUID (optional; auto-resolved if absent)\n --dry-run Print merged payload to stdout without writing\n\n"
3494
+ "\n codebyplan round <subcommand>\n\n Subcommands:\n sync-approvals Sync git diff and approvals with round/task state\n\n sync-approvals flags:\n --round-id <uuid> Round UUID (required)\n --task-id <uuid> Task UUID (required)\n --dry-run Print merged payload to stdout without writing\n\n"
3325
3495
  );
3326
3496
  }
3327
3497
  function parseFlagsFromArgs(args) {
@@ -3359,95 +3529,95 @@ async function runRoundSyncApprovals(args) {
3359
3529
  );
3360
3530
  process.exit(1);
3361
3531
  }
3532
+ let skipReason = null;
3362
3533
  let stdoutPayload = null;
3363
3534
  try {
3364
- let callerWorktreeId = flags["worktree-id"];
3365
- if (!callerWorktreeId) {
3366
- callerWorktreeId = await autoResolveWorktreeId();
3367
- }
3368
3535
  const found = await findCodebyplanConfig(process.cwd());
3369
3536
  const repoRoot = found ? (
3370
3537
  // Walk up to the directory containing .codebyplan/ or .codebyplan.json
3371
3538
  found.path.replace(/\/.codebyplan(\.json|\/repo\.json)$/, "")
3372
3539
  ) : process.cwd();
3373
- const rounds = await mcpCall("get_rounds", {
3374
- task_id: taskId
3375
- });
3376
- const currentRound = rounds.find((r) => r.id === roundId);
3377
- if (!currentRound) {
3378
- throw new Error(`Round ${roundId} not found for task ${taskId}`);
3379
- }
3380
- const taskResponse = await apiGet(`/tasks/${taskId}`);
3381
- const currentTask = taskResponse.data;
3382
- let gitStatusOutput = "";
3383
- try {
3384
- gitStatusOutput = execSync2("git status --short --porcelain -z", {
3385
- cwd: repoRoot,
3386
- encoding: "utf-8"
3387
- });
3388
- } catch {
3389
- process.stderr.write(
3390
- "sync-approvals: git status failed; proceeding with empty diff\n"
3391
- );
3392
- }
3393
- const hookPath = join10(
3394
- repoRoot,
3395
- ".claude",
3396
- "hooks",
3397
- "lint-format-on-edit.sh"
3398
- );
3399
- let lintFormatHookExists = false;
3540
+ let rounds;
3400
3541
  try {
3401
- await access3(hookPath);
3402
- lintFormatHookExists = true;
3403
- } catch {
3542
+ rounds = await fetchRoundsWithRetry(taskId);
3543
+ } catch (err) {
3544
+ if (isTransientMcpError(err)) {
3545
+ const reason = err instanceof McpError ? err.message : err instanceof Error ? err.message : String(err);
3546
+ skipReason = reason;
3547
+ rounds = [];
3548
+ } else {
3549
+ throw err;
3550
+ }
3404
3551
  }
3405
- const result = runSyncApprovals({
3406
- currentRound,
3407
- currentTask,
3408
- gitStatusOutput,
3409
- lintFormatHookExists
3410
- });
3411
- if (dryRun) {
3412
- stdoutPayload = JSON.stringify(
3413
- {
3414
- added: result.added,
3415
- stale_marked: result.stale_marked,
3416
- reactivated: result.reactivated,
3417
- total_files: result.total_files,
3418
- merged_files_changed: result.merged_files_changed
3419
- },
3420
- null,
3421
- 2
3422
- );
3423
- } else {
3424
- const roundArgs = {
3425
- round_id: roundId,
3426
- files_changed: result.merged_files_changed
3427
- };
3428
- if (callerWorktreeId) {
3429
- roundArgs["caller_worktree_id"] = callerWorktreeId;
3552
+ if (!skipReason) {
3553
+ const currentRound = rounds.find((r) => r.id === roundId);
3554
+ if (!currentRound) {
3555
+ throw new Error(`Round ${roundId} not found for task ${taskId}`);
3430
3556
  }
3431
- await mcpCall("update_round", roundArgs);
3432
- const taskArgs = {
3433
- task_id: taskId,
3434
- files_changed: result.merged_files_changed,
3435
- app_file_approval_by_user: false
3436
- };
3437
- if (callerWorktreeId) {
3438
- taskArgs["caller_worktree_id"] = callerWorktreeId;
3557
+ const taskResponse = await apiGet(`/tasks/${taskId}`);
3558
+ const currentTask = taskResponse.data;
3559
+ let gitStatusOutput = "";
3560
+ try {
3561
+ gitStatusOutput = execSync2("git status --short --porcelain -z", {
3562
+ cwd: repoRoot,
3563
+ encoding: "utf-8"
3564
+ });
3565
+ } catch {
3566
+ process.stderr.write(
3567
+ "sync-approvals: git status failed; proceeding with empty diff\n"
3568
+ );
3439
3569
  }
3440
- await mcpCall("update_task", taskArgs);
3441
- stdoutPayload = JSON.stringify(
3442
- {
3443
- added: result.added,
3444
- stale_marked: result.stale_marked,
3445
- reactivated: result.reactivated,
3446
- total_files: result.total_files
3447
- },
3448
- null,
3449
- 2
3570
+ const hookPath = join11(
3571
+ repoRoot,
3572
+ ".claude",
3573
+ "hooks",
3574
+ "lint-format-on-edit.sh"
3450
3575
  );
3576
+ let lintFormatHookExists = false;
3577
+ try {
3578
+ await access3(hookPath);
3579
+ lintFormatHookExists = true;
3580
+ } catch {
3581
+ }
3582
+ const result = runSyncApprovals({
3583
+ currentRound,
3584
+ currentTask,
3585
+ gitStatusOutput,
3586
+ lintFormatHookExists
3587
+ });
3588
+ if (dryRun) {
3589
+ stdoutPayload = JSON.stringify(
3590
+ {
3591
+ added: result.added,
3592
+ stale_marked: result.stale_marked,
3593
+ reactivated: result.reactivated,
3594
+ total_files: result.total_files,
3595
+ merged_files_changed: result.merged_files_changed
3596
+ },
3597
+ null,
3598
+ 2
3599
+ );
3600
+ } else {
3601
+ await mcpCall("update_round", {
3602
+ round_id: roundId,
3603
+ files_changed: result.merged_files_changed
3604
+ });
3605
+ await mcpCall("update_task", {
3606
+ task_id: taskId,
3607
+ files_changed: result.merged_files_changed,
3608
+ app_file_approval_by_user: false
3609
+ });
3610
+ stdoutPayload = JSON.stringify(
3611
+ {
3612
+ added: result.added,
3613
+ stale_marked: result.stale_marked,
3614
+ reactivated: result.reactivated,
3615
+ total_files: result.total_files
3616
+ },
3617
+ null,
3618
+ 2
3619
+ );
3620
+ }
3451
3621
  }
3452
3622
  } catch (err) {
3453
3623
  process.stderr.write(
@@ -3456,6 +3626,13 @@ async function runRoundSyncApprovals(args) {
3456
3626
  );
3457
3627
  process.exit(1);
3458
3628
  }
3629
+ if (skipReason !== null) {
3630
+ process.stderr.write(
3631
+ `sync-approvals: MCP temporarily unavailable (${skipReason}); skipping approval sync. Re-run /cbp-round-update when the service recovers.
3632
+ `
3633
+ );
3634
+ process.exit(0);
3635
+ }
3459
3636
  if (stdoutPayload === null) {
3460
3637
  process.stderr.write("sync-approvals: internal error \u2014 payload not set\n");
3461
3638
  process.exit(1);
@@ -3463,57 +3640,21 @@ async function runRoundSyncApprovals(args) {
3463
3640
  process.stdout.write(stdoutPayload + "\n");
3464
3641
  process.exit(0);
3465
3642
  }
3466
- async function autoResolveWorktreeId() {
3467
- try {
3468
- const projectPath = process.cwd();
3469
- const found = await findCodebyplanConfig(projectPath);
3470
- if (!found?.contents.repo_id) return void 0;
3471
- const repoId = found.contents.repo_id;
3472
- const deviceId = await getOrCreateDeviceId(projectPath);
3473
- let branch = "";
3474
- try {
3475
- branch = execSync2("git symbolic-ref --short HEAD", {
3476
- cwd: projectPath,
3477
- encoding: "utf-8"
3478
- }).trim();
3479
- } catch {
3480
- }
3481
- const worktreeId = await resolveWorktreeId({
3482
- repoId,
3483
- repoPath: projectPath,
3484
- branch,
3485
- deviceId
3486
- });
3487
- if (worktreeId) return worktreeId;
3488
- const fallbackId = await resolveWorktreeByBranch({
3489
- repoId,
3490
- deviceId,
3491
- branch
3492
- });
3493
- if (fallbackId) return fallbackId;
3494
- process.stderr.write(
3495
- "sync-approvals: could not resolve worktree id; proceeding without caller_worktree_id\n"
3496
- );
3497
- return void 0;
3498
- } catch {
3499
- return void 0;
3500
- }
3501
- }
3643
+ var RETRY_DELAY_MS;
3502
3644
  var init_round = __esm({
3503
3645
  "src/cli/round.ts"() {
3504
3646
  "use strict";
3505
3647
  init_api();
3506
3648
  init_mcp_client();
3507
3649
  init_flags();
3508
- init_resolve_worktree();
3509
- init_local_config();
3510
3650
  init_sync_approvals();
3651
+ RETRY_DELAY_MS = 1e3;
3511
3652
  }
3512
3653
  });
3513
3654
 
3514
3655
  // src/lib/migrate-branch-model.ts
3515
- import { readFile as readFile10, writeFile as writeFile8 } from "node:fs/promises";
3516
- import { join as join11 } from "node:path";
3656
+ import { readFile as readFile11, writeFile as writeFile9 } from "node:fs/promises";
3657
+ import { join as join12 } from "node:path";
3517
3658
  import { execSync as execSync3 } from "node:child_process";
3518
3659
  function assertValidBranchName(branch) {
3519
3660
  if (!/^[a-zA-Z0-9/_.-]+$/.test(branch)) {
@@ -3523,7 +3664,7 @@ function assertValidBranchName(branch) {
3523
3664
  }
3524
3665
  }
3525
3666
  async function readJsonFile(filePath) {
3526
- const raw = await readFile10(filePath, "utf-8");
3667
+ const raw = await readFile11(filePath, "utf-8");
3527
3668
  const parsed = JSON.parse(raw);
3528
3669
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
3529
3670
  throw new Error(`${filePath} does not contain a JSON object`);
@@ -3592,12 +3733,12 @@ async function runBranchMigration(opts) {
3592
3733
  if (found) {
3593
3734
  if (found.path.endsWith("/repo.json")) {
3594
3735
  const dir = found.path.slice(0, found.path.lastIndexOf("/"));
3595
- configPath = join11(dir, "git.json");
3736
+ configPath = join12(dir, "git.json");
3596
3737
  } else {
3597
3738
  configPath = found.path;
3598
3739
  }
3599
3740
  } else {
3600
- configPath = join11(cwd, ".codebyplan", "git.json");
3741
+ configPath = join12(cwd, ".codebyplan", "git.json");
3601
3742
  }
3602
3743
  let fileRaw;
3603
3744
  let fileParsed;
@@ -3671,7 +3812,7 @@ async function runBranchMigration(opts) {
3671
3812
  const updatedParsed = { ...fileParsed, branch_config: after };
3672
3813
  const newJson = JSON.stringify(updatedParsed, null, 2) + "\n";
3673
3814
  if (newJson !== fileRaw) {
3674
- await writeFile8(configPath, newJson, "utf-8");
3815
+ await writeFile9(configPath, newJson, "utf-8");
3675
3816
  }
3676
3817
  }
3677
3818
  return {
@@ -3777,8 +3918,8 @@ var init_branch = __esm({
3777
3918
  });
3778
3919
 
3779
3920
  // src/lib/ship.ts
3780
- import { readFile as readFile11 } from "node:fs/promises";
3781
- import { join as join12 } from "node:path";
3921
+ import { readFile as readFile12 } from "node:fs/promises";
3922
+ import { join as join13 } from "node:path";
3782
3923
  import { execSync as execSync4, spawnSync as spawnSync2 } from "node:child_process";
3783
3924
  function assertValidBranchName2(branch, label) {
3784
3925
  if (!/^[a-zA-Z0-9/_.-]+$/.test(branch)) {
@@ -3793,15 +3934,15 @@ async function readBaseBranch(cwd) {
3793
3934
  if (found) {
3794
3935
  if (found.path.endsWith("/repo.json")) {
3795
3936
  const dir = found.path.slice(0, found.path.lastIndexOf("/"));
3796
- gitJsonPath = join12(dir, "git.json");
3937
+ gitJsonPath = join13(dir, "git.json");
3797
3938
  } else {
3798
3939
  gitJsonPath = found.path;
3799
3940
  }
3800
3941
  } else {
3801
- gitJsonPath = join12(cwd, ".codebyplan", "git.json");
3942
+ gitJsonPath = join13(cwd, ".codebyplan", "git.json");
3802
3943
  }
3803
3944
  try {
3804
- const raw = await readFile11(gitJsonPath, "utf-8");
3945
+ const raw = await readFile12(gitJsonPath, "utf-8");
3805
3946
  const parsed = JSON.parse(raw);
3806
3947
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
3807
3948
  return "main";
@@ -4333,19 +4474,19 @@ var init_resolve_worktree2 = __esm({
4333
4474
  });
4334
4475
 
4335
4476
  // src/lib/migrate-local-config.ts
4336
- import { mkdir as mkdir5, readFile as readFile12, unlink as unlink2, writeFile as writeFile9 } from "node:fs/promises";
4337
- import { join as join13 } from "node:path";
4477
+ import { mkdir as mkdir5, readFile as readFile13, unlink as unlink2, writeFile as writeFile10 } from "node:fs/promises";
4478
+ import { join as join14 } from "node:path";
4338
4479
  function legacySharedPath(projectPath) {
4339
- return join13(projectPath, ".codebyplan.json");
4480
+ return join14(projectPath, ".codebyplan.json");
4340
4481
  }
4341
4482
  function legacyLocalPath(projectPath) {
4342
- return join13(projectPath, ".codebyplan.local.json");
4483
+ return join14(projectPath, ".codebyplan.local.json");
4343
4484
  }
4344
4485
  function newDirPath(projectPath) {
4345
- return join13(projectPath, ".codebyplan");
4486
+ return join14(projectPath, ".codebyplan");
4346
4487
  }
4347
4488
  function sentinelPath(projectPath) {
4348
- return join13(projectPath, ".codebyplan", "repo.json");
4489
+ return join14(projectPath, ".codebyplan", "repo.json");
4349
4490
  }
4350
4491
  async function statSafe(p) {
4351
4492
  const { stat: stat2 } = await import("node:fs/promises");
@@ -4384,7 +4525,7 @@ async function runLocalMigration(projectPath) {
4384
4525
  }
4385
4526
  let legacyRaw;
4386
4527
  try {
4387
- legacyRaw = await readFile12(legacySharedPath(projectPath), "utf-8");
4528
+ legacyRaw = await readFile13(legacySharedPath(projectPath), "utf-8");
4388
4529
  } catch {
4389
4530
  return {
4390
4531
  migrated: true,
@@ -4411,7 +4552,7 @@ async function runLocalMigration(projectPath) {
4411
4552
  let deviceId;
4412
4553
  let deviceWrittenByHelper = false;
4413
4554
  try {
4414
- const localRaw = await readFile12(legacyLocalPath(projectPath), "utf-8");
4555
+ const localRaw = await readFile13(legacyLocalPath(projectPath), "utf-8");
4415
4556
  const localParsed = JSON.parse(localRaw);
4416
4557
  if (typeof localParsed.device_id === "string") {
4417
4558
  deviceId = localParsed.device_id;
@@ -4438,8 +4579,8 @@ async function runLocalMigration(projectPath) {
4438
4579
  if ("repo_id" in cfg) repoJson.repo_id = cfg.repo_id;
4439
4580
  if ("organization_id" in cfg) repoJson.organization_id = cfg.organization_id;
4440
4581
  if ("project_id" in cfg) repoJson.project_id = cfg.project_id;
4441
- await writeFile9(
4442
- join13(projectPath, ".codebyplan", "repo.json"),
4582
+ await writeFile10(
4583
+ join14(projectPath, ".codebyplan", "repo.json"),
4443
4584
  JSON.stringify(repoJson, null, 2) + "\n",
4444
4585
  "utf-8"
4445
4586
  );
@@ -4451,8 +4592,8 @@ async function runLocalMigration(projectPath) {
4451
4592
  serverJson.auto_push_enabled = cfg.auto_push_enabled;
4452
4593
  if ("port_allocations" in cfg)
4453
4594
  serverJson.port_allocations = cfg.port_allocations;
4454
- await writeFile9(
4455
- join13(projectPath, ".codebyplan", "server.json"),
4595
+ await writeFile10(
4596
+ join14(projectPath, ".codebyplan", "server.json"),
4456
4597
  JSON.stringify(serverJson, null, 2) + "\n",
4457
4598
  "utf-8"
4458
4599
  );
@@ -4460,37 +4601,44 @@ async function runLocalMigration(projectPath) {
4460
4601
  const gitJson = {};
4461
4602
  if ("git_branch" in cfg) gitJson.git_branch = cfg.git_branch;
4462
4603
  if ("branch_config" in cfg) gitJson.branch_config = cfg.branch_config;
4463
- await writeFile9(
4464
- join13(projectPath, ".codebyplan", "git.json"),
4604
+ await writeFile10(
4605
+ join14(projectPath, ".codebyplan", "git.json"),
4465
4606
  JSON.stringify(gitJson, null, 2) + "\n",
4466
4607
  "utf-8"
4467
4608
  );
4468
4609
  filesChanged.push(".codebyplan/git.json");
4469
4610
  const shipmentJson = {};
4470
4611
  if ("shipment" in cfg) shipmentJson.shipment = cfg.shipment;
4471
- await writeFile9(
4472
- join13(projectPath, ".codebyplan", "shipment.json"),
4612
+ await writeFile10(
4613
+ join14(projectPath, ".codebyplan", "shipment.json"),
4473
4614
  JSON.stringify(shipmentJson, null, 2) + "\n",
4474
4615
  "utf-8"
4475
4616
  );
4476
4617
  filesChanged.push(".codebyplan/shipment.json");
4477
4618
  const vendorJson = {};
4478
- await writeFile9(
4479
- join13(projectPath, ".codebyplan", "vendor.json"),
4619
+ await writeFile10(
4620
+ join14(projectPath, ".codebyplan", "vendor.json"),
4480
4621
  JSON.stringify(vendorJson, null, 2) + "\n",
4481
4622
  "utf-8"
4482
4623
  );
4483
4624
  filesChanged.push(".codebyplan/vendor.json");
4484
4625
  const e2eJson = {};
4485
- await writeFile9(
4486
- join13(projectPath, ".codebyplan", "e2e.json"),
4626
+ await writeFile10(
4627
+ join14(projectPath, ".codebyplan", "e2e.json"),
4487
4628
  JSON.stringify(e2eJson, null, 2) + "\n",
4488
4629
  "utf-8"
4489
4630
  );
4490
4631
  filesChanged.push(".codebyplan/e2e.json");
4632
+ const eslintJson = {};
4633
+ await writeFile10(
4634
+ join14(projectPath, ".codebyplan", "eslint.json"),
4635
+ JSON.stringify(eslintJson, null, 2) + "\n",
4636
+ "utf-8"
4637
+ );
4638
+ filesChanged.push(".codebyplan/eslint.json");
4491
4639
  if (!deviceWrittenByHelper) {
4492
- await writeFile9(
4493
- join13(projectPath, ".codebyplan", "device.local.json"),
4640
+ await writeFile10(
4641
+ join14(projectPath, ".codebyplan", "device.local.json"),
4494
4642
  JSON.stringify({ device_id: deviceId }, null, 2) + "\n",
4495
4643
  "utf-8"
4496
4644
  );
@@ -4502,9 +4650,9 @@ async function runLocalMigration(projectPath) {
4502
4650
  "Migration write incomplete: .codebyplan/repo.json was not persisted. Re-run migration to retry from a clean state."
4503
4651
  );
4504
4652
  }
4505
- const gitignorePath = join13(projectPath, ".gitignore");
4653
+ const gitignorePath = join14(projectPath, ".gitignore");
4506
4654
  try {
4507
- const gitignoreContent = await readFile12(gitignorePath, "utf-8");
4655
+ const gitignoreContent = await readFile13(gitignorePath, "utf-8");
4508
4656
  const legacyLine = ".codebyplan.local.json";
4509
4657
  const newLine = ".codebyplan/device.local.json";
4510
4658
  const hasLegacy = gitignoreContent.split("\n").some((l) => l.trimEnd() === legacyLine);
@@ -4523,7 +4671,7 @@ async function runLocalMigration(projectPath) {
4523
4671
  updated = gitignoreContent;
4524
4672
  }
4525
4673
  if (updated !== gitignoreContent) {
4526
- await writeFile9(gitignorePath, updated, "utf-8");
4674
+ await writeFile10(gitignorePath, updated, "utf-8");
4527
4675
  filesChanged.push(".gitignore");
4528
4676
  }
4529
4677
  } catch {
@@ -4563,8 +4711,8 @@ __export(config_exports, {
4563
4711
  readVendorConfig: () => readVendorConfig,
4564
4712
  runConfig: () => runConfig
4565
4713
  });
4566
- import { mkdir as mkdir6, readFile as readFile13, writeFile as writeFile10 } from "node:fs/promises";
4567
- import { join as join14 } from "node:path";
4714
+ import { mkdir as mkdir6, readFile as readFile14, writeFile as writeFile11 } from "node:fs/promises";
4715
+ import { join as join15 } from "node:path";
4568
4716
  async function runConfig() {
4569
4717
  const flags = parseFlags(3);
4570
4718
  const dryRun = hasFlag("dry-run", 3);
@@ -4597,7 +4745,7 @@ async function runConfig() {
4597
4745
  console.log("\n Config complete.\n");
4598
4746
  }
4599
4747
  async function syncConfigToFile(repoId, projectPath, dryRun) {
4600
- const codebyplanDir = join14(projectPath, ".codebyplan");
4748
+ const codebyplanDir = join15(projectPath, ".codebyplan");
4601
4749
  let resolvedWorktreeId;
4602
4750
  try {
4603
4751
  const deviceId = await getOrCreateDeviceId(projectPath);
@@ -4723,6 +4871,7 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
4723
4871
  }
4724
4872
  const vendorPayload = {};
4725
4873
  const e2ePayload = {};
4874
+ const eslintPayload = {};
4726
4875
  if (dryRun) {
4727
4876
  console.log(" Config would be updated (dry-run).");
4728
4877
  return;
@@ -4734,20 +4883,21 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
4734
4883
  { name: "git.json", payload: gitPayload },
4735
4884
  { name: "shipment.json", payload: shipmentPayload },
4736
4885
  { name: "vendor.json", payload: vendorPayload },
4737
- { name: "e2e.json", payload: e2ePayload, createOnly: true }
4886
+ { name: "e2e.json", payload: e2ePayload, createOnly: true },
4887
+ { name: "eslint.json", payload: eslintPayload, createOnly: true }
4738
4888
  ];
4739
4889
  let anyUpdated = false;
4740
4890
  for (const { name, payload, createOnly } of files) {
4741
- const filePath = join14(codebyplanDir, name);
4891
+ const filePath = join15(codebyplanDir, name);
4742
4892
  const newJson = JSON.stringify(payload, null, 2) + "\n";
4743
4893
  let currentJson = "";
4744
4894
  try {
4745
- currentJson = await readFile13(filePath, "utf-8");
4895
+ currentJson = await readFile14(filePath, "utf-8");
4746
4896
  } catch {
4747
4897
  }
4748
4898
  if (createOnly && currentJson !== "") continue;
4749
4899
  if (currentJson === newJson) continue;
4750
- await writeFile10(filePath, newJson, "utf-8");
4900
+ await writeFile11(filePath, newJson, "utf-8");
4751
4901
  console.log(` Updated .codebyplan/${name}`);
4752
4902
  anyUpdated = true;
4753
4903
  }
@@ -4757,8 +4907,8 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
4757
4907
  }
4758
4908
  async function readRepoConfig(projectPath) {
4759
4909
  try {
4760
- const raw = await readFile13(
4761
- join14(projectPath, ".codebyplan", "repo.json"),
4910
+ const raw = await readFile14(
4911
+ join15(projectPath, ".codebyplan", "repo.json"),
4762
4912
  "utf-8"
4763
4913
  );
4764
4914
  return JSON.parse(raw);
@@ -4768,8 +4918,8 @@ async function readRepoConfig(projectPath) {
4768
4918
  }
4769
4919
  async function readServerConfig(projectPath) {
4770
4920
  try {
4771
- const raw = await readFile13(
4772
- join14(projectPath, ".codebyplan", "server.json"),
4921
+ const raw = await readFile14(
4922
+ join15(projectPath, ".codebyplan", "server.json"),
4773
4923
  "utf-8"
4774
4924
  );
4775
4925
  return JSON.parse(raw);
@@ -4779,8 +4929,8 @@ async function readServerConfig(projectPath) {
4779
4929
  }
4780
4930
  async function readGitConfig(projectPath) {
4781
4931
  try {
4782
- const raw = await readFile13(
4783
- join14(projectPath, ".codebyplan", "git.json"),
4932
+ const raw = await readFile14(
4933
+ join15(projectPath, ".codebyplan", "git.json"),
4784
4934
  "utf-8"
4785
4935
  );
4786
4936
  return JSON.parse(raw);
@@ -4790,8 +4940,8 @@ async function readGitConfig(projectPath) {
4790
4940
  }
4791
4941
  async function readShipmentConfig(projectPath) {
4792
4942
  try {
4793
- const raw = await readFile13(
4794
- join14(projectPath, ".codebyplan", "shipment.json"),
4943
+ const raw = await readFile14(
4944
+ join15(projectPath, ".codebyplan", "shipment.json"),
4795
4945
  "utf-8"
4796
4946
  );
4797
4947
  return JSON.parse(raw);
@@ -4801,8 +4951,8 @@ async function readShipmentConfig(projectPath) {
4801
4951
  }
4802
4952
  async function readVendorConfig(projectPath) {
4803
4953
  try {
4804
- const raw = await readFile13(
4805
- join14(projectPath, ".codebyplan", "vendor.json"),
4954
+ const raw = await readFile14(
4955
+ join15(projectPath, ".codebyplan", "vendor.json"),
4806
4956
  "utf-8"
4807
4957
  );
4808
4958
  return JSON.parse(raw);
@@ -4812,8 +4962,8 @@ async function readVendorConfig(projectPath) {
4812
4962
  }
4813
4963
  async function readE2eConfig(projectPath) {
4814
4964
  try {
4815
- const raw = await readFile13(
4816
- join14(projectPath, ".codebyplan", "e2e.json"),
4965
+ const raw = await readFile14(
4966
+ join15(projectPath, ".codebyplan", "e2e.json"),
4817
4967
  "utf-8"
4818
4968
  );
4819
4969
  return JSON.parse(raw);
@@ -4869,14 +5019,14 @@ var init_server_detect = __esm({
4869
5019
  });
4870
5020
 
4871
5021
  // src/lib/port-verify.ts
4872
- import { readFile as readFile14 } from "node:fs/promises";
5022
+ import { readFile as readFile15 } from "node:fs/promises";
4873
5023
  async function verifyPorts(projectPath, portAllocations) {
4874
5024
  const mismatches = [];
4875
5025
  const allocatedPorts = new Set(portAllocations.map((a) => a.port));
4876
5026
  const packageJsonPaths = await findPackageJsonFiles(projectPath, projectPath);
4877
5027
  for (const pkgPath of packageJsonPaths) {
4878
5028
  try {
4879
- const raw = await readFile14(pkgPath, "utf-8");
5029
+ const raw = await readFile15(pkgPath, "utf-8");
4880
5030
  const pkg = JSON.parse(raw);
4881
5031
  const scriptPort = detectPortFromScripts(pkg);
4882
5032
  if (scriptPort !== null && !allocatedPorts.has(scriptPort)) {
@@ -4939,7 +5089,7 @@ async function findUnallocatedApps(projectPath, portAllocations) {
4939
5089
  }
4940
5090
  let pkg;
4941
5091
  try {
4942
- const raw = await readFile14(`${app.absPath}/package.json`, "utf-8");
5092
+ const raw = await readFile15(`${app.absPath}/package.json`, "utf-8");
4943
5093
  pkg = JSON.parse(raw);
4944
5094
  } catch {
4945
5095
  continue;
@@ -5309,7 +5459,7 @@ var init_hash = __esm({
5309
5459
 
5310
5460
  // src/lib/template-walker.ts
5311
5461
  import * as fs from "node:fs";
5312
- import * as path from "node:path";
5462
+ import * as path2 from "node:path";
5313
5463
  function walkTemplates(templatesDir) {
5314
5464
  const absRoot = fs.realpathSync(templatesDir);
5315
5465
  const visited = /* @__PURE__ */ new Set();
@@ -5322,7 +5472,7 @@ function walkTemplates(templatesDir) {
5322
5472
  visited.add(realDir);
5323
5473
  const entries = fs.readdirSync(absDir, { withFileTypes: true });
5324
5474
  for (const entry of entries) {
5325
- const absPath = path.join(absDir, entry.name);
5475
+ const absPath = path2.join(absDir, entry.name);
5326
5476
  if (entry.isDirectory()) {
5327
5477
  recurse(absPath);
5328
5478
  continue;
@@ -5331,7 +5481,7 @@ function walkTemplates(templatesDir) {
5331
5481
  continue;
5332
5482
  }
5333
5483
  if (entry.name === ".gitkeep") continue;
5334
- const relPosix = path.relative(absRoot, absPath).split(path.sep).join("/");
5484
+ const relPosix = path2.relative(absRoot, absPath).split(path2.sep).join("/");
5335
5485
  if (EXCLUDED_RELATIVE_PATHS.has(relPosix)) {
5336
5486
  continue;
5337
5487
  }
@@ -5359,6 +5509,10 @@ var init_template_walker = __esm({
5359
5509
  "rules/README.md",
5360
5510
  "settings.project.base.json",
5361
5511
  "settings.user.base.json",
5512
+ // .gitignore — managed by ensureManagedGitignoreBlock; never copied into
5513
+ // consuming projects' .claude/ tree (it would overwrite the project root
5514
+ // .gitignore with a stale single-entry file).
5515
+ ".gitignore",
5362
5516
  // CBP-internal hooks — see templates/hooks/README.md "Hooks NOT included and why"
5363
5517
  "hooks/validate-structure.sh",
5364
5518
  "hooks/validate-structure-lib.sh",
@@ -5376,15 +5530,15 @@ var init_template_walker = __esm({
5376
5530
  // src/lib/manifest.ts
5377
5531
  import * as fs2 from "node:fs";
5378
5532
  import * as os from "node:os";
5379
- import * as path2 from "node:path";
5533
+ import * as path3 from "node:path";
5380
5534
  function manifestPath(projectDir) {
5381
- return path2.join(projectDir, ".claude", NEW_MANIFEST_FILENAME);
5535
+ return path3.join(projectDir, ".claude", NEW_MANIFEST_FILENAME);
5382
5536
  }
5383
5537
  function midManifestPath(projectDir) {
5384
- return path2.join(projectDir, ".claude", MID_MANIFEST_FILENAME);
5538
+ return path3.join(projectDir, ".claude", MID_MANIFEST_FILENAME);
5385
5539
  }
5386
5540
  function oldManifestPath(projectDir) {
5387
- return path2.join(projectDir, ".claude", OLD_MANIFEST_FILENAME);
5541
+ return path3.join(projectDir, ".claude", OLD_MANIFEST_FILENAME);
5388
5542
  }
5389
5543
  function readManifest(projectDir) {
5390
5544
  const newFile = manifestPath(projectDir);
@@ -5406,7 +5560,7 @@ function readManifest(projectDir) {
5406
5560
  }
5407
5561
  function writeManifest(projectDir, manifest) {
5408
5562
  const file = manifestPath(projectDir);
5409
- fs2.mkdirSync(path2.dirname(file), { recursive: true });
5563
+ fs2.mkdirSync(path3.dirname(file), { recursive: true });
5410
5564
  fs2.writeFileSync(file, JSON.stringify(manifest, null, 2) + "\n", "utf8");
5411
5565
  const mid = midManifestPath(projectDir);
5412
5566
  if (fs2.existsSync(mid)) {
@@ -5425,16 +5579,16 @@ function defaultManifest() {
5425
5579
  };
5426
5580
  }
5427
5581
  function userManifestPath(userDir) {
5428
- const dir = userDir ?? path2.join(os.homedir(), ".claude");
5429
- return path2.join(dir, NEW_MANIFEST_FILENAME);
5582
+ const dir = userDir ?? path3.join(os.homedir(), ".claude");
5583
+ return path3.join(dir, NEW_MANIFEST_FILENAME);
5430
5584
  }
5431
5585
  function userMidManifestPath(userDir) {
5432
- const dir = userDir ?? path2.join(os.homedir(), ".claude");
5433
- return path2.join(dir, MID_MANIFEST_FILENAME);
5586
+ const dir = userDir ?? path3.join(os.homedir(), ".claude");
5587
+ return path3.join(dir, MID_MANIFEST_FILENAME);
5434
5588
  }
5435
5589
  function userOldManifestPath(userDir) {
5436
- const dir = userDir ?? path2.join(os.homedir(), ".claude");
5437
- return path2.join(dir, OLD_MANIFEST_FILENAME);
5590
+ const dir = userDir ?? path3.join(os.homedir(), ".claude");
5591
+ return path3.join(dir, OLD_MANIFEST_FILENAME);
5438
5592
  }
5439
5593
  function readManifestForScope(scope, arg2) {
5440
5594
  if (scope === "user") {
@@ -5460,7 +5614,7 @@ function readManifestForScope(scope, arg2) {
5460
5614
  function writeManifestForScope(scope, manifest, arg3) {
5461
5615
  if (scope === "user") {
5462
5616
  const file = userManifestPath(arg3);
5463
- fs2.mkdirSync(path2.dirname(file), { recursive: true });
5617
+ fs2.mkdirSync(path3.dirname(file), { recursive: true });
5464
5618
  fs2.writeFileSync(file, JSON.stringify(manifest, null, 2) + "\n", "utf8");
5465
5619
  const mid = userMidManifestPath(arg3);
5466
5620
  if (fs2.existsSync(mid)) {
@@ -5716,14 +5870,14 @@ __export(install_exports, {
5716
5870
  });
5717
5871
  import * as fs3 from "node:fs";
5718
5872
  import * as os2 from "node:os";
5719
- import * as path3 from "node:path";
5873
+ import * as path4 from "node:path";
5720
5874
  import { fileURLToPath } from "node:url";
5721
5875
  function resolveTemplatesDir() {
5722
- const here = path3.dirname(fileURLToPath(import.meta.url));
5876
+ const here = path4.dirname(fileURLToPath(import.meta.url));
5723
5877
  const candidates = [
5724
- path3.resolve(here, "..", "templates"),
5725
- path3.resolve(here, "..", "..", "templates"),
5726
- path3.resolve(here, "..", "..", "..", "templates")
5878
+ path4.resolve(here, "..", "templates"),
5879
+ path4.resolve(here, "..", "..", "templates"),
5880
+ path4.resolve(here, "..", "..", "..", "templates")
5727
5881
  ];
5728
5882
  for (const c of candidates) {
5729
5883
  if (fs3.existsSync(c) && fs3.statSync(c).isDirectory()) {
@@ -5764,14 +5918,14 @@ async function runInstall(opts, deps = {}) {
5764
5918
  const files = walkTemplates(templatesDir);
5765
5919
  const manifestEntries = [];
5766
5920
  for (const f of files) {
5767
- const absDest = path3.join(projectDir, ".claude", f.dest);
5768
- const absSrc = path3.join(templatesDir, f.src);
5921
+ const absDest = path4.join(projectDir, ".claude", f.dest);
5922
+ const absSrc = path4.join(templatesDir, f.src);
5769
5923
  if (opts.dryRun) {
5770
5924
  if (opts.verbose) {
5771
5925
  console.log(`[dry-run] would copy ${f.src} \u2192 .claude/${f.dest}`);
5772
5926
  }
5773
5927
  } else {
5774
- fs3.mkdirSync(path3.dirname(absDest), { recursive: true });
5928
+ fs3.mkdirSync(path4.dirname(absDest), { recursive: true });
5775
5929
  fs3.copyFileSync(absSrc, absDest);
5776
5930
  if (opts.verbose) {
5777
5931
  console.log(`copied ${f.src} \u2192 .claude/${f.dest}`);
@@ -5779,15 +5933,15 @@ async function runInstall(opts, deps = {}) {
5779
5933
  }
5780
5934
  manifestEntries.push({ src: f.src, dest: f.dest, hash: f.hash });
5781
5935
  }
5782
- const hooksJsonPath = path3.join(templatesDir, "hooks", "hooks.json");
5783
- const baseSettingsPath = path3.join(
5936
+ const hooksJsonPath = path4.join(templatesDir, "hooks", "hooks.json");
5937
+ const baseSettingsPath = path4.join(
5784
5938
  templatesDir,
5785
5939
  "settings.project.base.json"
5786
5940
  );
5787
5941
  const hasHooks = fs3.existsSync(hooksJsonPath);
5788
5942
  const hasBase = fs3.existsSync(baseSettingsPath);
5789
5943
  if (hasHooks || hasBase) {
5790
- const settingsPath = path3.join(projectDir, ".claude", "settings.json");
5944
+ const settingsPath = path4.join(projectDir, ".claude", "settings.json");
5791
5945
  const existingSettings = fs3.existsSync(settingsPath) ? JSON.parse(fs3.readFileSync(settingsPath, "utf8")) : {};
5792
5946
  if (hasBase) {
5793
5947
  const base = JSON.parse(
@@ -5802,7 +5956,7 @@ async function runInstall(opts, deps = {}) {
5802
5956
  mergeHooksIntoSettings(existingSettings, hooksJson);
5803
5957
  }
5804
5958
  if (!opts.dryRun) {
5805
- fs3.mkdirSync(path3.dirname(settingsPath), { recursive: true });
5959
+ fs3.mkdirSync(path4.dirname(settingsPath), { recursive: true });
5806
5960
  fs3.writeFileSync(
5807
5961
  settingsPath,
5808
5962
  JSON.stringify(existingSettings, null, 2) + "\n",
@@ -5810,10 +5964,19 @@ async function runInstall(opts, deps = {}) {
5810
5964
  );
5811
5965
  } else if (opts.verbose) {
5812
5966
  console.log(
5813
- `[dry-run] would merge settings into ${path3.relative(projectDir, settingsPath)}`
5967
+ `[dry-run] would merge settings into ${path4.relative(projectDir, settingsPath)}`
5814
5968
  );
5815
5969
  }
5816
5970
  }
5971
+ const gitignoreAction = await ensureManagedGitignoreBlock(
5972
+ projectDir,
5973
+ opts.dryRun
5974
+ );
5975
+ if (opts.verbose && gitignoreAction !== "unchanged") {
5976
+ console.log(
5977
+ `${opts.dryRun ? "[dry-run] would " : ""}${gitignoreAction} managed .gitignore block in ${path4.relative(projectDir, path4.join(projectDir, ".gitignore"))}`
5978
+ );
5979
+ }
5817
5980
  if (!opts.dryRun) {
5818
5981
  const manifest = defaultManifest();
5819
5982
  manifest.files = manifestEntries;
@@ -5844,9 +6007,9 @@ function runInstallUser(opts, deps) {
5844
6007
  return;
5845
6008
  }
5846
6009
  try {
5847
- const userDir = deps.userDir ?? path3.join(os2.homedir(), ".claude");
5848
- const settingsPath = path3.join(userDir, "settings.json");
5849
- const userBaseSettingsPath = path3.join(
6010
+ const userDir = deps.userDir ?? path4.join(os2.homedir(), ".claude");
6011
+ const settingsPath = path4.join(userDir, "settings.json");
6012
+ const userBaseSettingsPath = path4.join(
5850
6013
  templatesDir,
5851
6014
  "settings.user.base.json"
5852
6015
  );
@@ -5888,7 +6051,7 @@ function runInstallUser(opts, deps) {
5888
6051
  }
5889
6052
  }
5890
6053
  function countHookEntries(templatesDir) {
5891
- const p = path3.join(templatesDir, "hooks", "hooks.json");
6054
+ const p = path4.join(templatesDir, "hooks", "hooks.json");
5892
6055
  if (!fs3.existsSync(p)) return 0;
5893
6056
  try {
5894
6057
  const j = JSON.parse(fs3.readFileSync(p, "utf8"));
@@ -5908,6 +6071,7 @@ var init_install = __esm({
5908
6071
  "src/cli/claude/install.ts"() {
5909
6072
  "use strict";
5910
6073
  init_template_walker();
6074
+ init_gitignore_block();
5911
6075
  init_manifest();
5912
6076
  init_settings_merge();
5913
6077
  init_statusline_config();
@@ -6032,7 +6196,7 @@ __export(update_exports, {
6032
6196
  });
6033
6197
  import * as fs4 from "node:fs";
6034
6198
  import * as os3 from "node:os";
6035
- import * as path4 from "node:path";
6199
+ import * as path5 from "node:path";
6036
6200
  import { fileURLToPath as fileURLToPath2 } from "node:url";
6037
6201
  async function runUpdate(opts, deps = {}) {
6038
6202
  await Promise.resolve();
@@ -6072,9 +6236,9 @@ async function runUpdate(opts, deps = {}) {
6072
6236
  finalManifestEntries.push(e);
6073
6237
  }
6074
6238
  for (const { packaged, absSrc } of plan.overwriteSafe) {
6075
- const absDest = path4.join(projectDir, ".claude", packaged.dest);
6239
+ const absDest = path5.join(projectDir, ".claude", packaged.dest);
6076
6240
  if (!opts.dryRun) {
6077
- fs4.mkdirSync(path4.dirname(absDest), { recursive: true });
6241
+ fs4.mkdirSync(path5.dirname(absDest), { recursive: true });
6078
6242
  fs4.copyFileSync(absSrc, absDest);
6079
6243
  if (opts.verbose) console.log(`updated ${packaged.dest}`);
6080
6244
  } else if (opts.verbose) {
@@ -6087,7 +6251,7 @@ async function runUpdate(opts, deps = {}) {
6087
6251
  absSrc,
6088
6252
  onDiskContent
6089
6253
  } of plan.overwriteHandEdited) {
6090
- const absDest = path4.join(projectDir, ".claude", packaged.dest);
6254
+ const absDest = path5.join(projectDir, ".claude", packaged.dest);
6091
6255
  const newContent = fs4.readFileSync(absSrc);
6092
6256
  const showDiff = () => {
6093
6257
  console.log(
@@ -6100,7 +6264,7 @@ async function runUpdate(opts, deps = {}) {
6100
6264
  const answer = await promptOverwrite(packaged.dest, opts, showDiff);
6101
6265
  if (answer === "overwrite") {
6102
6266
  if (!opts.dryRun) {
6103
- fs4.mkdirSync(path4.dirname(absDest), { recursive: true });
6267
+ fs4.mkdirSync(path5.dirname(absDest), { recursive: true });
6104
6268
  fs4.copyFileSync(absSrc, absDest);
6105
6269
  }
6106
6270
  finalManifestEntries.push(packaged);
@@ -6116,9 +6280,9 @@ async function runUpdate(opts, deps = {}) {
6116
6280
  for (const { packaged, absSrc } of plan.newOptIn) {
6117
6281
  const answer = await promptOptIn(packaged.dest, opts);
6118
6282
  if (answer === "opt-in") {
6119
- const absDest = path4.join(projectDir, ".claude", packaged.dest);
6283
+ const absDest = path5.join(projectDir, ".claude", packaged.dest);
6120
6284
  if (!opts.dryRun) {
6121
- fs4.mkdirSync(path4.dirname(absDest), { recursive: true });
6285
+ fs4.mkdirSync(path5.dirname(absDest), { recursive: true });
6122
6286
  fs4.copyFileSync(absSrc, absDest);
6123
6287
  }
6124
6288
  finalManifestEntries.push(packaged);
@@ -6130,25 +6294,25 @@ async function runUpdate(opts, deps = {}) {
6130
6294
  for (const e of plan.removedFromPackage) {
6131
6295
  const answer = await promptRemove(e.dest, opts);
6132
6296
  if (answer === "remove") {
6133
- const absDest = path4.join(projectDir, ".claude", e.dest);
6297
+ const absDest = path5.join(projectDir, ".claude", e.dest);
6134
6298
  if (!opts.dryRun && fs4.existsSync(absDest)) {
6135
6299
  fs4.rmSync(absDest);
6136
- const claudeDir = path4.join(projectDir, ".claude");
6137
- let cur = path4.dirname(absDest);
6138
- while (cur !== claudeDir && cur !== path4.dirname(cur)) {
6139
- if (path4.dirname(cur) === claudeDir) break;
6300
+ const claudeDir = path5.join(projectDir, ".claude");
6301
+ let cur = path5.dirname(absDest);
6302
+ while (cur !== claudeDir && cur !== path5.dirname(cur)) {
6303
+ if (path5.dirname(cur) === claudeDir) break;
6140
6304
  try {
6141
6305
  fs4.rmdirSync(cur);
6142
6306
  if (opts.verbose)
6143
6307
  console.log(
6144
- `pruned empty dir ${path4.relative(claudeDir, cur)}`
6308
+ `pruned empty dir ${path5.relative(claudeDir, cur)}`
6145
6309
  );
6146
- cur = path4.dirname(cur);
6310
+ cur = path5.dirname(cur);
6147
6311
  } catch (err) {
6148
6312
  const code = err.code;
6149
6313
  if (code !== "ENOTEMPTY" && code !== "ENOENT") {
6150
6314
  console.warn(
6151
- `codebyplan claude: could not prune empty dir ${path4.relative(claudeDir, cur)}: ${err.message}`
6315
+ `codebyplan claude: could not prune empty dir ${path5.relative(claudeDir, cur)}: ${err.message}`
6152
6316
  );
6153
6317
  }
6154
6318
  break;
@@ -6160,16 +6324,16 @@ async function runUpdate(opts, deps = {}) {
6160
6324
  if (opts.verbose) console.log(`kept (untracked) ${e.dest}`);
6161
6325
  }
6162
6326
  }
6163
- const hooksJsonPath = path4.join(templatesDir, "hooks", "hooks.json");
6327
+ const hooksJsonPath = path5.join(templatesDir, "hooks", "hooks.json");
6164
6328
  if (fs4.existsSync(hooksJsonPath)) {
6165
6329
  const hooksJson = JSON.parse(
6166
6330
  fs4.readFileSync(hooksJsonPath, "utf8")
6167
6331
  );
6168
- const settingsPath = path4.join(projectDir, ".claude", "settings.json");
6332
+ const settingsPath = path5.join(projectDir, ".claude", "settings.json");
6169
6333
  const existingSettings = fs4.existsSync(settingsPath) ? JSON.parse(fs4.readFileSync(settingsPath, "utf8")) : {};
6170
6334
  mergeHooksIntoSettings(existingSettings, hooksJson);
6171
6335
  if (!opts.dryRun) {
6172
- fs4.mkdirSync(path4.dirname(settingsPath), { recursive: true });
6336
+ fs4.mkdirSync(path5.dirname(settingsPath), { recursive: true });
6173
6337
  fs4.writeFileSync(
6174
6338
  settingsPath,
6175
6339
  JSON.stringify(existingSettings, null, 2) + "\n",
@@ -6177,6 +6341,15 @@ async function runUpdate(opts, deps = {}) {
6177
6341
  );
6178
6342
  }
6179
6343
  }
6344
+ const gitignoreAction = await ensureManagedGitignoreBlock(
6345
+ projectDir,
6346
+ opts.dryRun
6347
+ );
6348
+ if (opts.verbose && gitignoreAction !== "unchanged") {
6349
+ console.log(
6350
+ `${opts.dryRun ? "[dry-run] would " : ""}${gitignoreAction} managed .gitignore block in ${path5.relative(projectDir, path5.join(projectDir, ".gitignore"))}`
6351
+ );
6352
+ }
6180
6353
  if (!opts.dryRun) {
6181
6354
  const manifest = defaultManifest();
6182
6355
  manifest.files = finalManifestEntries.sort(
@@ -6209,9 +6382,9 @@ function runUpdateUser(opts, deps) {
6209
6382
  return;
6210
6383
  }
6211
6384
  try {
6212
- const userDir = deps.userDir ?? path4.join(os3.homedir(), ".claude");
6213
- const settingsPath = path4.join(userDir, "settings.json");
6214
- const userBaseSettingsPath = path4.join(
6385
+ const userDir = deps.userDir ?? path5.join(os3.homedir(), ".claude");
6386
+ const settingsPath = path5.join(userDir, "settings.json");
6387
+ const userBaseSettingsPath = path5.join(
6215
6388
  templatesDir,
6216
6389
  "settings.user.base.json"
6217
6390
  );
@@ -6273,8 +6446,8 @@ function buildPlan(projectDir, templatesDir, manifest) {
6273
6446
  };
6274
6447
  for (const pkg of packaged) {
6275
6448
  const inManifest = manifestBySrc.get(pkg.src);
6276
- const absDest = path4.join(projectDir, ".claude", pkg.dest);
6277
- const absSrc = path4.join(templatesDir, pkg.src);
6449
+ const absDest = path5.join(projectDir, ".claude", pkg.dest);
6450
+ const absSrc = path5.join(templatesDir, pkg.src);
6278
6451
  if (!inManifest) {
6279
6452
  plan.newOptIn.push({
6280
6453
  packaged: { src: pkg.src, dest: pkg.dest, hash: pkg.hash },
@@ -6310,11 +6483,11 @@ function buildPlan(projectDir, templatesDir, manifest) {
6310
6483
  return plan;
6311
6484
  }
6312
6485
  function resolveTemplatesDirFromInstall() {
6313
- const here = path4.dirname(fileURLToPath2(import.meta.url));
6486
+ const here = path5.dirname(fileURLToPath2(import.meta.url));
6314
6487
  const candidates = [
6315
- path4.resolve(here, "..", "templates"),
6316
- path4.resolve(here, "..", "..", "templates"),
6317
- path4.resolve(here, "..", "..", "..", "templates")
6488
+ path5.resolve(here, "..", "templates"),
6489
+ path5.resolve(here, "..", "..", "templates"),
6490
+ path5.resolve(here, "..", "..", "..", "templates")
6318
6491
  ];
6319
6492
  for (const c of candidates) {
6320
6493
  if (fs4.existsSync(c) && fs4.statSync(c).isDirectory()) {
@@ -6332,6 +6505,7 @@ var init_update = __esm({
6332
6505
  "src/cli/claude/update.ts"() {
6333
6506
  "use strict";
6334
6507
  init_template_walker();
6508
+ init_gitignore_block();
6335
6509
  init_hash();
6336
6510
  init_manifest();
6337
6511
  init_settings_merge();
@@ -6347,7 +6521,7 @@ __export(uninstall_exports, {
6347
6521
  });
6348
6522
  import * as fs5 from "node:fs";
6349
6523
  import * as os4 from "node:os";
6350
- import * as path5 from "node:path";
6524
+ import * as path6 from "node:path";
6351
6525
  async function runUninstall(opts, deps = {}) {
6352
6526
  await Promise.resolve();
6353
6527
  const scope = opts.scope ?? "project";
@@ -6376,7 +6550,7 @@ async function runUninstall(opts, deps = {}) {
6376
6550
  let removed = 0;
6377
6551
  let warnings = 0;
6378
6552
  for (const entry of manifest.files) {
6379
- const abs = path5.join(projectDir, ".claude", entry.dest);
6553
+ const abs = path6.join(projectDir, ".claude", entry.dest);
6380
6554
  if (!fs5.existsSync(abs)) {
6381
6555
  console.warn(
6382
6556
  `codebyplan claude uninstall: ${entry.dest} already absent (skipping).`
@@ -6400,12 +6574,12 @@ async function runUninstall(opts, deps = {}) {
6400
6574
  if (!opts.dryRun) {
6401
6575
  pruneEmptyManagedDirs(projectDir);
6402
6576
  }
6403
- const settingsPath = path5.join(projectDir, ".claude", "settings.json");
6577
+ const settingsPath = path6.join(projectDir, ".claude", "settings.json");
6404
6578
  if (fs5.existsSync(settingsPath)) {
6405
6579
  const settings = JSON.parse(
6406
6580
  fs5.readFileSync(settingsPath, "utf8")
6407
6581
  );
6408
- const baseSettingsPath = templatesDir ? path5.join(templatesDir, "settings.project.base.json") : null;
6582
+ const baseSettingsPath = templatesDir ? path6.join(templatesDir, "settings.project.base.json") : null;
6409
6583
  if (baseSettingsPath && fs5.existsSync(baseSettingsPath)) {
6410
6584
  const base = JSON.parse(
6411
6585
  fs5.readFileSync(baseSettingsPath, "utf8")
@@ -6426,6 +6600,15 @@ async function runUninstall(opts, deps = {}) {
6426
6600
  }
6427
6601
  }
6428
6602
  }
6603
+ const gitignoreAction = await removeManagedGitignoreBlock(
6604
+ projectDir,
6605
+ opts.dryRun
6606
+ );
6607
+ if (opts.verbose) {
6608
+ console.log(
6609
+ gitignoreAction === "removed" ? `${opts.dryRun ? "[dry-run] would remove" : "removed"} managed .gitignore block` : "managed .gitignore block already absent (no change)"
6610
+ );
6611
+ }
6429
6612
  if (!opts.dryRun) {
6430
6613
  const m = manifestPath(projectDir);
6431
6614
  if (fs5.existsSync(m)) fs5.rmSync(m);
@@ -6454,7 +6637,7 @@ function runUninstallUser(opts, deps) {
6454
6637
  }
6455
6638
  }
6456
6639
  try {
6457
- const userDir = deps.userDir ?? path5.join(os4.homedir(), ".claude");
6640
+ const userDir = deps.userDir ?? path6.join(os4.homedir(), ".claude");
6458
6641
  const existingManifest = readManifestForScope("user", userDir);
6459
6642
  if (!existingManifest) {
6460
6643
  console.error(
@@ -6463,12 +6646,12 @@ function runUninstallUser(opts, deps) {
6463
6646
  process.exitCode = 1;
6464
6647
  return;
6465
6648
  }
6466
- const settingsPath = path5.join(userDir, "settings.json");
6649
+ const settingsPath = path6.join(userDir, "settings.json");
6467
6650
  if (fs5.existsSync(settingsPath)) {
6468
6651
  const settings = JSON.parse(
6469
6652
  fs5.readFileSync(settingsPath, "utf8")
6470
6653
  );
6471
- const userBaseSettingsPath = templatesDir != null ? path5.join(templatesDir, "settings.user.base.json") : null;
6654
+ const userBaseSettingsPath = templatesDir != null ? path6.join(templatesDir, "settings.user.base.json") : null;
6472
6655
  if (userBaseSettingsPath && fs5.existsSync(userBaseSettingsPath)) {
6473
6656
  const userBase = JSON.parse(
6474
6657
  fs5.readFileSync(userBaseSettingsPath, "utf8")
@@ -6509,7 +6692,7 @@ function runUninstallUser(opts, deps) {
6509
6692
  function pruneEmptyManagedDirs(projectDir) {
6510
6693
  const managedRoots = ["skills", "agents", "hooks", "rules"];
6511
6694
  for (const root of managedRoots) {
6512
- const abs = path5.join(projectDir, ".claude", root);
6695
+ const abs = path6.join(projectDir, ".claude", root);
6513
6696
  if (!fs5.existsSync(abs)) continue;
6514
6697
  pruneLeafFirst(abs);
6515
6698
  }
@@ -6520,7 +6703,7 @@ function pruneLeafFirst(dir) {
6520
6703
  if (!stat2.isDirectory()) return;
6521
6704
  for (const entry of fs5.readdirSync(dir, { withFileTypes: true })) {
6522
6705
  if (entry.isDirectory()) {
6523
- pruneLeafFirst(path5.join(dir, entry.name));
6706
+ pruneLeafFirst(path6.join(dir, entry.name));
6524
6707
  }
6525
6708
  }
6526
6709
  const remaining = fs5.readdirSync(dir);
@@ -6532,6 +6715,7 @@ var init_uninstall = __esm({
6532
6715
  "src/cli/claude/uninstall.ts"() {
6533
6716
  "use strict";
6534
6717
  init_hash();
6718
+ init_gitignore_block();
6535
6719
  init_manifest();
6536
6720
  init_settings_merge();
6537
6721
  init_install();
@@ -6582,8 +6766,9 @@ void (async () => {
6582
6766
  }
6583
6767
  if (arg === "login") {
6584
6768
  const { runLogin: runLogin2 } = await Promise.resolve().then(() => (init_login(), login_exports));
6769
+ const admin = process.argv.includes("--admin");
6585
6770
  try {
6586
- await runLogin2();
6771
+ await runLogin2({ admin });
6587
6772
  process.exit(0);
6588
6773
  } catch {
6589
6774
  process.exit(1);
@@ -6727,7 +6912,7 @@ void (async () => {
6727
6912
 
6728
6913
  Usage:
6729
6914
  codebyplan setup Interactive setup (OAuth + project init)
6730
- codebyplan login Authenticate via OAuth device-code flow
6915
+ codebyplan login [--admin] Authenticate via OAuth device-code flow
6731
6916
  codebyplan logout Clear cached OAuth tokens
6732
6917
  codebyplan whoami Show currently authenticated identity
6733
6918
  codebyplan upgrade-auth Migrate legacy x-api-key MCP config to OAuth