@idea1/cli 1.7.7 → 1.8.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 (195) hide show
  1. package/README.md +126 -60
  2. package/dist/cli/named-lists.d.ts.map +1 -1
  3. package/dist/cli/named-lists.js +5 -0
  4. package/dist/cli/named-lists.js.map +1 -1
  5. package/dist/commands/approve-pr.d.ts.map +1 -1
  6. package/dist/commands/approve-pr.js +94 -8
  7. package/dist/commands/approve-pr.js.map +1 -1
  8. package/dist/commands/attach-project-item.d.ts.map +1 -1
  9. package/dist/commands/attach-project-item.js +28 -41
  10. package/dist/commands/attach-project-item.js.map +1 -1
  11. package/dist/commands/close-worktree.d.ts.map +1 -1
  12. package/dist/commands/close-worktree.js +50 -84
  13. package/dist/commands/close-worktree.js.map +1 -1
  14. package/dist/commands/create-worktree.d.ts.map +1 -1
  15. package/dist/commands/create-worktree.js +1 -4
  16. package/dist/commands/create-worktree.js.map +1 -1
  17. package/dist/commands/get-project-item-url.d.ts.map +1 -1
  18. package/dist/commands/get-project-item-url.js +3 -1
  19. package/dist/commands/get-project-item-url.js.map +1 -1
  20. package/dist/commands/help.js +13 -13
  21. package/dist/commands/help.js.map +1 -1
  22. package/dist/commands/index.d.ts +1 -2
  23. package/dist/commands/index.d.ts.map +1 -1
  24. package/dist/commands/index.js +1 -3
  25. package/dist/commands/index.js.map +1 -1
  26. package/dist/commands/join-project-item.d.ts.map +1 -1
  27. package/dist/commands/join-project-item.js +18 -60
  28. package/dist/commands/join-project-item.js.map +1 -1
  29. package/dist/commands/next-workflow-step.d.ts.map +1 -1
  30. package/dist/commands/next-workflow-step.js +16 -22
  31. package/dist/commands/next-workflow-step.js.map +1 -1
  32. package/dist/commands/push.d.ts.map +1 -1
  33. package/dist/commands/push.js +4 -3
  34. package/dist/commands/push.js.map +1 -1
  35. package/dist/commands/ready-for-approval.d.ts.map +1 -1
  36. package/dist/commands/ready-for-approval.js +94 -8
  37. package/dist/commands/ready-for-approval.js.map +1 -1
  38. package/dist/commands/release.d.ts.map +1 -1
  39. package/dist/commands/release.js +4 -15
  40. package/dist/commands/release.js.map +1 -1
  41. package/dist/commands/start-agent-implementation.d.ts.map +1 -1
  42. package/dist/commands/start-agent-implementation.js +10 -13
  43. package/dist/commands/start-agent-implementation.js.map +1 -1
  44. package/dist/commands/start-dev-implementation.d.ts.map +1 -1
  45. package/dist/commands/start-dev-implementation.js +7 -8
  46. package/dist/commands/start-dev-implementation.js.map +1 -1
  47. package/dist/commands/start.d.ts.map +1 -1
  48. package/dist/commands/start.js +21 -15
  49. package/dist/commands/start.js.map +1 -1
  50. package/dist/commands/update-branch.d.ts.map +1 -1
  51. package/dist/commands/update-branch.js +28 -75
  52. package/dist/commands/update-branch.js.map +1 -1
  53. package/dist/interactive.d.ts.map +1 -1
  54. package/dist/interactive.js +11 -23
  55. package/dist/interactive.js.map +1 -1
  56. package/dist/lib/claude-options.d.ts +11 -10
  57. package/dist/lib/claude-options.d.ts.map +1 -1
  58. package/dist/lib/claude-options.js +38 -39
  59. package/dist/lib/claude-options.js.map +1 -1
  60. package/dist/lib/settings.d.ts +23 -27
  61. package/dist/lib/settings.d.ts.map +1 -1
  62. package/dist/lib/settings.js +35 -50
  63. package/dist/lib/settings.js.map +1 -1
  64. package/dist/prompts/platform-prompts.d.ts +30 -0
  65. package/dist/prompts/platform-prompts.d.ts.map +1 -0
  66. package/dist/prompts/platform-prompts.js +38 -0
  67. package/dist/prompts/platform-prompts.js.map +1 -0
  68. package/dist/prompts/project-item-prompts.d.ts +44 -0
  69. package/dist/prompts/project-item-prompts.d.ts.map +1 -0
  70. package/dist/prompts/project-item-prompts.js +41 -0
  71. package/dist/prompts/project-item-prompts.js.map +1 -0
  72. package/dist/services/claude.d.ts +27 -33
  73. package/dist/services/claude.d.ts.map +1 -1
  74. package/dist/services/claude.js +40 -140
  75. package/dist/services/claude.js.map +1 -1
  76. package/dist/services/git.d.ts +24 -121
  77. package/dist/services/git.d.ts.map +1 -1
  78. package/dist/services/git.js +73 -470
  79. package/dist/services/git.js.map +1 -1
  80. package/dist/services/github.d.ts +20 -4
  81. package/dist/services/github.d.ts.map +1 -1
  82. package/dist/services/github.js +77 -6
  83. package/dist/services/github.js.map +1 -1
  84. package/dist/services/platform.d.ts +75 -0
  85. package/dist/services/platform.d.ts.map +1 -0
  86. package/dist/services/platform.js +237 -0
  87. package/dist/services/platform.js.map +1 -0
  88. package/dist/services/setup.d.ts +1 -1
  89. package/dist/services/setup.js +1 -1
  90. package/dist/services/template-renderer.d.ts +10 -0
  91. package/dist/services/template-renderer.d.ts.map +1 -0
  92. package/dist/services/template-renderer.js +20 -0
  93. package/dist/services/template-renderer.js.map +1 -0
  94. package/dist/services/worktree.d.ts +17 -16
  95. package/dist/services/worktree.d.ts.map +1 -1
  96. package/dist/services/worktree.js +41 -65
  97. package/dist/services/worktree.js.map +1 -1
  98. package/dist/types/index.d.ts +14 -58
  99. package/dist/types/index.d.ts.map +1 -1
  100. package/dist/version.d.ts +1 -1
  101. package/dist/version.js +1 -1
  102. package/dist/workflow/data/project-item.d.ts +21 -0
  103. package/dist/workflow/data/project-item.d.ts.map +1 -0
  104. package/dist/workflow/data/project-item.js +2 -0
  105. package/dist/workflow/data/project-item.js.map +1 -0
  106. package/dist/workflow/data/workflow-context.d.ts +46 -0
  107. package/dist/workflow/data/workflow-context.d.ts.map +1 -0
  108. package/dist/workflow/data/workflow-context.js +14 -0
  109. package/dist/workflow/data/workflow-context.js.map +1 -0
  110. package/dist/{lib → workflow/data}/workflow-states.d.ts +1 -1
  111. package/dist/workflow/data/workflow-states.d.ts.map +1 -0
  112. package/dist/{lib → workflow/data}/workflow-states.generated.d.ts +1 -1
  113. package/dist/workflow/data/workflow-states.generated.d.ts.map +1 -0
  114. package/dist/{lib → workflow/data}/workflow-states.generated.js +1 -1
  115. package/dist/workflow/data/workflow-states.generated.js.map +1 -0
  116. package/dist/{lib → workflow/data}/workflow-states.js +1 -1
  117. package/dist/workflow/data/workflow-states.js.map +1 -0
  118. package/dist/workflow/engine/approval-flow.d.ts +53 -0
  119. package/dist/workflow/engine/approval-flow.d.ts.map +1 -0
  120. package/dist/workflow/engine/approval-flow.js +111 -0
  121. package/dist/workflow/engine/approval-flow.js.map +1 -0
  122. package/dist/{services → workflow/engine}/branch-setup.d.ts +2 -1
  123. package/dist/workflow/engine/branch-setup.d.ts.map +1 -0
  124. package/dist/{services → workflow/engine}/branch-setup.js +6 -5
  125. package/dist/workflow/engine/branch-setup.js.map +1 -0
  126. package/dist/workflow/engine/handlers/agent-implementation.d.ts +10 -0
  127. package/dist/workflow/engine/handlers/agent-implementation.d.ts.map +1 -0
  128. package/dist/workflow/engine/handlers/agent-implementation.js +42 -0
  129. package/dist/workflow/engine/handlers/agent-implementation.js.map +1 -0
  130. package/dist/{services/workflow-handlers → workflow/engine/handlers}/approve-pr.d.ts +7 -4
  131. package/dist/workflow/engine/handlers/approve-pr.d.ts.map +1 -0
  132. package/dist/workflow/engine/handlers/approve-pr.js +63 -0
  133. package/dist/workflow/engine/handlers/approve-pr.js.map +1 -0
  134. package/dist/{services/workflow-handlers → workflow/engine/handlers}/dev-implementation.d.ts +3 -2
  135. package/dist/workflow/engine/handlers/dev-implementation.d.ts.map +1 -0
  136. package/dist/workflow/engine/handlers/dev-implementation.js +36 -0
  137. package/dist/workflow/engine/handlers/dev-implementation.js.map +1 -0
  138. package/dist/workflow/engine/handlers/index.d.ts +21 -0
  139. package/dist/workflow/engine/handlers/index.d.ts.map +1 -0
  140. package/dist/{services/workflow-handlers → workflow/engine/handlers}/index.js +10 -6
  141. package/dist/workflow/engine/handlers/index.js.map +1 -0
  142. package/dist/workflow/engine/handlers/ready-for-approval.d.ts +11 -0
  143. package/dist/workflow/engine/handlers/ready-for-approval.d.ts.map +1 -0
  144. package/dist/workflow/engine/handlers/ready-for-approval.js +56 -0
  145. package/dist/workflow/engine/handlers/ready-for-approval.js.map +1 -0
  146. package/dist/{services → workflow/engine}/release-flow.d.ts +19 -10
  147. package/dist/workflow/engine/release-flow.d.ts.map +1 -0
  148. package/dist/{services → workflow/engine}/release-flow.js +33 -16
  149. package/dist/workflow/engine/release-flow.js.map +1 -0
  150. package/dist/workflow/engine/workflow-engine.d.ts +162 -0
  151. package/dist/workflow/engine/workflow-engine.d.ts.map +1 -0
  152. package/dist/workflow/engine/workflow-engine.js +405 -0
  153. package/dist/workflow/engine/workflow-engine.js.map +1 -0
  154. package/package.json +2 -1
  155. package/dist/commands/integrate.d.ts +0 -13
  156. package/dist/commands/integrate.d.ts.map +0 -1
  157. package/dist/commands/integrate.js +0 -128
  158. package/dist/commands/integrate.js.map +0 -1
  159. package/dist/lib/workflow-states.d.ts.map +0 -1
  160. package/dist/lib/workflow-states.generated.d.ts.map +0 -1
  161. package/dist/lib/workflow-states.generated.js.map +0 -1
  162. package/dist/lib/workflow-states.js.map +0 -1
  163. package/dist/services/branch-setup.d.ts.map +0 -1
  164. package/dist/services/branch-setup.js.map +0 -1
  165. package/dist/services/context.d.ts +0 -26
  166. package/dist/services/context.d.ts.map +0 -1
  167. package/dist/services/context.js +0 -304
  168. package/dist/services/context.js.map +0 -1
  169. package/dist/services/release-flow.d.ts.map +0 -1
  170. package/dist/services/release-flow.js.map +0 -1
  171. package/dist/services/workflow-engine.d.ts +0 -67
  172. package/dist/services/workflow-engine.d.ts.map +0 -1
  173. package/dist/services/workflow-engine.js +0 -371
  174. package/dist/services/workflow-engine.js.map +0 -1
  175. package/dist/services/workflow-handlers/agent-implementation.d.ts +0 -9
  176. package/dist/services/workflow-handlers/agent-implementation.d.ts.map +0 -1
  177. package/dist/services/workflow-handlers/agent-implementation.js +0 -38
  178. package/dist/services/workflow-handlers/agent-implementation.js.map +0 -1
  179. package/dist/services/workflow-handlers/approve-pr.d.ts.map +0 -1
  180. package/dist/services/workflow-handlers/approve-pr.js +0 -122
  181. package/dist/services/workflow-handlers/approve-pr.js.map +0 -1
  182. package/dist/services/workflow-handlers/dev-implementation.d.ts.map +0 -1
  183. package/dist/services/workflow-handlers/dev-implementation.js +0 -34
  184. package/dist/services/workflow-handlers/dev-implementation.js.map +0 -1
  185. package/dist/services/workflow-handlers/index.d.ts +0 -18
  186. package/dist/services/workflow-handlers/index.d.ts.map +0 -1
  187. package/dist/services/workflow-handlers/index.js.map +0 -1
  188. package/dist/services/workflow-handlers/ready-for-approval.d.ts +0 -10
  189. package/dist/services/workflow-handlers/ready-for-approval.d.ts.map +0 -1
  190. package/dist/services/workflow-handlers/ready-for-approval.js +0 -59
  191. package/dist/services/workflow-handlers/ready-for-approval.js.map +0 -1
  192. package/dist/services/workflow.d.ts +0 -14
  193. package/dist/services/workflow.d.ts.map +0 -1
  194. package/dist/services/workflow.js +0 -37
  195. package/dist/services/workflow.js.map +0 -1
@@ -2,9 +2,8 @@ import { exec, execOrThrow } from '../lib/shell.js';
2
2
  import { logger } from '../lib/logger.js';
3
3
  import { dirname, basename, join, resolve } from 'path';
4
4
  import { tmpdir } from 'os';
5
- import { mkdir, mkdtemp, writeFile, readFile, readdir, unlink, rm } from 'fs/promises';
5
+ import { mkdtemp, writeFile, rm } from 'fs/promises';
6
6
  import { access } from 'fs/promises';
7
- import { DEFAULT_BRANCH_SETTINGS, SETTINGS_FILES, getBranchPrefix, getIntegrationBranchPrefix } from '../lib/settings.js';
8
7
  /**
9
8
  * Check if a directory exists
10
9
  */
@@ -17,100 +16,6 @@ async function directoryExists(path) {
17
16
  return false;
18
17
  }
19
18
  }
20
- /**
21
- * Sanitize a branch name for use as a filename by replacing `/` with `--`
22
- */
23
- function sanitizeBranchNameForFilename(branchName) {
24
- return branchName.replace(/\//g, '--');
25
- }
26
- /**
27
- * Reverse of sanitizeBranchNameForFilename: `--` back to `/`
28
- */
29
- function branchNameFromFilename(filename) {
30
- return filename.replace(/\.json$/, '').replace(/--/g, '/');
31
- }
32
- /**
33
- * Path of a branch's metadata JSON file relative to the repository root
34
- */
35
- export function getBranchMetadataRelativePath(branchName) {
36
- return `.idea1/${sanitizeBranchNameForFilename(branchName)}.json`;
37
- }
38
- /**
39
- * Get the path to a branch's metadata JSON file inside `.idea1/`
40
- */
41
- function getBranchMetadataPath(worktreePath, branchName) {
42
- return join(worktreePath, getBranchMetadataRelativePath(branchName));
43
- }
44
- /**
45
- * The on-disk form of a branch metadata file. The web client writes the same
46
- * bytes through the GitHub API, so keep this exact (2-space indent, trailing newline).
47
- */
48
- export function serializeBranchMetadata(metadata) {
49
- return JSON.stringify(metadata, null, 2) + '\n';
50
- }
51
- /**
52
- * Read branch metadata from the JSON file in the current working tree.
53
- * Returns null if the file doesn't exist.
54
- */
55
- export async function readBranchMetadata(branchName) {
56
- const branch = branchName ?? await getCurrentBranch();
57
- const repoRoot = await getRepoRoot();
58
- const metadataPath = getBranchMetadataPath(repoRoot, branch);
59
- try {
60
- const content = await readFile(metadataPath, 'utf-8');
61
- return JSON.parse(content);
62
- }
63
- catch {
64
- return null;
65
- }
66
- }
67
- /**
68
- * Read a branch's committed metadata directly from a remote ref via `git show`,
69
- * without checking the branch out. Returns null if the file is missing or unparseable.
70
- * Lets callers recover durable per-branch state from a fresh clone.
71
- */
72
- export async function readBranchMetadataFromRef(branchName, remote = 'origin') {
73
- const filename = `${sanitizeBranchNameForFilename(branchName)}.json`;
74
- const result = await exec('git', ['show', `${remote}/${branchName}:.idea1/${filename}`]);
75
- if (result.exitCode !== 0)
76
- return null;
77
- try {
78
- return JSON.parse(result.stdout);
79
- }
80
- catch {
81
- return null;
82
- }
83
- }
84
- /**
85
- * Write branch metadata to the JSON file.
86
- * Returns false if `.idea1/` doesn't exist (e.g., main repo before worktree creation).
87
- */
88
- export async function writeBranchMetadata(metadata, worktreePath) {
89
- const root = worktreePath ?? await getRepoRoot();
90
- const idea1Dir = join(root, '.idea1');
91
- if (!await directoryExists(idea1Dir))
92
- return false;
93
- const metadataPath = getBranchMetadataPath(root, metadata.branchName);
94
- await writeFile(metadataPath, serializeBranchMetadata(metadata));
95
- return true;
96
- }
97
- /**
98
- * Partially update an existing branch metadata JSON file.
99
- * Silently skips if the file doesn't exist (e.g., store* called before worktree exists).
100
- */
101
- async function updateBranchMetadataFile(branchName, updates) {
102
- const repoRoot = await getRepoRoot();
103
- const metadataPath = getBranchMetadataPath(repoRoot, branchName);
104
- try {
105
- const content = await readFile(metadataPath, 'utf-8');
106
- const metadata = JSON.parse(content);
107
- Object.assign(metadata, updates);
108
- await writeFile(metadataPath, serializeBranchMetadata(metadata));
109
- }
110
- catch {
111
- // File doesn't exist yet — silently skip
112
- }
113
- }
114
19
  /**
115
20
  * Get the repository root path from git
116
21
  */
@@ -224,7 +129,7 @@ export async function updateBranchToLatest(branchName) {
224
129
  const worktreePath = await getWorktreePathForBranch(branchName);
225
130
  if (worktreePath) {
226
131
  logger.step(`Branch ${branchName} is in a worktree at ${worktreePath}, pulling there...`);
227
- await execOrThrow('git', ['pull', 'origin', branchName], { cwd: worktreePath });
132
+ await pullBranch(branchName, worktreePath);
228
133
  }
229
134
  else {
230
135
  await checkoutAndPullBranch(branchName);
@@ -237,7 +142,7 @@ export async function checkoutAndPullBranch(branchName) {
237
142
  logger.step(`Checking out ${branchName}...`);
238
143
  await execOrThrow('git', ['checkout', branchName]);
239
144
  logger.step(`Pulling latest from ${branchName}...`);
240
- await execOrThrow('git', ['pull', 'origin', branchName]);
145
+ await pullBranch(branchName);
241
146
  }
242
147
  /**
243
148
  * Create a new branch from a base branch and push it to origin
@@ -261,13 +166,13 @@ export async function prepareRepoForWorktree(fullBranchName, destBranchName, fal
261
166
  const destWorktreePath = await getWorktreePathForBranch(destBranchName);
262
167
  if (destWorktreePath) {
263
168
  logger.step(`Branch ${destBranchName} is in a worktree at ${destWorktreePath}, pulling there...`);
264
- await execOrThrow('git', ['pull', 'origin', destBranchName], { cwd: destWorktreePath });
169
+ await pullBranch(destBranchName, destWorktreePath);
265
170
  }
266
171
  else {
267
172
  await checkoutAndPullBranch(destBranchName);
268
173
  }
269
174
  await execOrThrow('git', ['checkout', fullBranchName]);
270
- await execOrThrow('git', ['pull', 'origin', fullBranchName]);
175
+ await pullBranch(fullBranchName);
271
176
  logger.success(`Branch ${fullBranchName} is ready for development`);
272
177
  // Must leave main working tree NOT on fullBranchName for worktree add to work
273
178
  if (destWorktreePath) {
@@ -282,7 +187,7 @@ export async function prepareRepoForWorktree(fullBranchName, destBranchName, fal
282
187
  }
283
188
  else {
284
189
  await execOrThrow('git', ['checkout', destBranchName]);
285
- await execOrThrow('git', ['pull', 'origin', destBranchName]);
190
+ await pullBranch(destBranchName);
286
191
  }
287
192
  }
288
193
  /**
@@ -359,7 +264,7 @@ export async function checkoutBranchInMainRepo(branchName) {
359
264
  await execOrThrow('git', ['checkout', '--track', `origin/${branchName}`]);
360
265
  }
361
266
  }
362
- await execOrThrow('git', ['pull', 'origin', branchName]);
267
+ await pullBranch(branchName);
363
268
  logger.success(`Branch ${branchName} is checked out in ${mainRepoRoot}`);
364
269
  return mainRepoRoot;
365
270
  }
@@ -402,228 +307,50 @@ export async function enterWorktreeAndSetup(worktreePath, fullBranchName) {
402
307
  await exec('git', ['push', '-u', 'origin', fullBranchName], { cwd: worktreePath });
403
308
  }
404
309
  /**
405
- * Store project item URL in git config for later retrieval
310
+ * Remember which project item a branch implements in this clone's git config
311
+ * (`branch.<name>.idea1ProjectItemUrl`). A local convenience only: the platform's record of the
312
+ * branch is the truth, and this is what it said last time the branch was resolved here.
406
313
  */
407
314
  export async function storeProjectItemUrl(projectItemUrl, branchName) {
408
- const branch = branchName ?? await getCurrentBranch();
409
- if (projectItemUrl && branch) {
410
- await execOrThrow('git', ['config', '--local', `branch.${branch}.idea1ProjectItemUrl`, projectItemUrl]);
411
- await updateBranchMetadataFile(branch, { projectItemUrl });
412
- logger.dim(`Project item URL stored in git config for ${branch}`);
413
- }
414
- }
415
- /**
416
- * Commit a `.idea1/<sanitized-branch>.json` metadata file on the branch so all
417
- * per-branch metadata is discoverable from any clone via `git show`.
418
- * Reads current values from git config and writes them into the JSON file.
419
- */
420
- export async function commitBranchMetadata(worktreePath, branchName, projectItemUrl) {
421
- const metadata = { branchName };
422
- // Gather all metadata from git config
423
- if (projectItemUrl)
424
- metadata.projectItemUrl = projectItemUrl;
425
- const storedUrl = await getProjectItemUrl(branchName);
426
- if (storedUrl && !metadata.projectItemUrl)
427
- metadata.projectItemUrl = storedUrl;
428
- const baseBranch = await getBaseBranch(branchName);
429
- if (baseBranch)
430
- metadata.baseBranch = baseBranch;
431
- const workflowState = await getWorkflowState(branchName);
432
- if (workflowState)
433
- metadata.workflowState = workflowState;
434
- const devWorkflowType = await getDevWorkflowType(branchName);
435
- if (devWorkflowType)
436
- metadata.devWorkflowType = devWorkflowType;
437
- const stackBranch = await getStackBranch(branchName);
438
- if (stackBranch)
439
- metadata.stackBranch = stackBranch;
440
- const integrationBranch = await getIntegrationBranch(branchName);
441
- if (integrationBranch)
442
- metadata.integrationBranch = integrationBranch;
443
- // Write JSON file
444
- const idea1Dir = join(worktreePath, '.idea1');
445
- await mkdir(idea1Dir, { recursive: true });
446
- const filename = `${sanitizeBranchNameForFilename(branchName)}.json`;
447
- await writeFile(join(idea1Dir, filename), serializeBranchMetadata(metadata));
448
- await execOrThrow('git', ['add', `.idea1/${filename}`], { cwd: worktreePath });
449
- await execOrThrow('git', ['commit', '-m', `idea1: link to ${metadata.projectItemUrl ?? branchName}`], { cwd: worktreePath });
450
- logger.dim('Committed branch metadata to branch');
451
- }
452
- /**
453
- * Remove the branch's `.idea1/<sanitized-branch>.json` file, commit, and push.
454
- * Must be called before the PR is merged so the metadata file doesn't leak into the target branch.
455
- */
456
- export async function removeBranchMetadataAndPush(branchName) {
457
- const repoRoot = await getRepoRoot();
458
- const filename = `${sanitizeBranchNameForFilename(branchName)}.json`;
459
- const filePath = join(repoRoot, '.idea1', filename);
460
- try {
461
- await access(filePath);
462
- }
463
- catch {
464
- // File doesn't exist — nothing to remove
465
- return;
466
- }
467
- await unlink(filePath);
468
- await execOrThrow('git', ['add', `.idea1/${filename}`]);
469
- await execOrThrow('git', ['commit', '-m', `idea1: remove branch metadata for ${branchName}`]);
470
- await execOrThrow('git', ['push']);
471
- logger.dim('Removed branch metadata file before merge');
472
- }
473
- /**
474
- * Stage and commit the branch metadata file if it has changed.
475
- * Returns true if a commit was made.
476
- */
477
- export async function commitBranchMetadataIfChanged(branchName) {
478
- const repoRoot = await getRepoRoot();
479
- const filename = `${sanitizeBranchNameForFilename(branchName)}.json`;
480
- const filePath = join(repoRoot, '.idea1', filename);
481
- try {
482
- await access(filePath);
483
- }
484
- catch {
485
- return false;
486
- }
487
- await execOrThrow('git', ['add', `.idea1/${filename}`]);
488
- const diffResult = await exec('git', ['diff', '--cached', '--quiet']);
489
- if (diffResult.exitCode === 0) {
490
- return false;
315
+ const current = await getProjectItemUrl(branchName);
316
+ if (current !== projectItemUrl) {
317
+ await execOrThrow('git', ['config', '--local', `branch.${branchName}.idea1ProjectItemUrl`, projectItemUrl]);
491
318
  }
492
- const url = await getProjectItemUrl(branchName);
493
- const commitRef = url ?? branchName;
494
- await execOrThrow('git', ['commit', '-m', `idea1: update metadata for ${commitRef}`]);
495
- logger.dim('Committed branch metadata update');
496
- return true;
497
319
  }
498
320
  /**
499
- * The remote-branch globs that cover every branch the workflow creates: the configured
500
- * branch prefix and, for integration workflows, the integration prefix. Derived from
501
- * settings rather than assuming `working/`, so a repo that names its branches
502
- * differently still resolves an item to its branch.
321
+ * The project item URL this clone remembers for a branch, or null for a branch it has yet to resolve.
503
322
  */
504
- async function workflowBranchGlobs() {
505
- const prefixes = new Set([DEFAULT_BRANCH_SETTINGS.branchPrefix, DEFAULT_BRANCH_SETTINGS.integrationBranchPrefix]);
506
- try {
507
- const repoRoot = await getRepoRoot();
508
- const [branchPrefix, integrationPrefix] = await Promise.all([
509
- getBranchPrefix(repoRoot),
510
- getIntegrationBranchPrefix(repoRoot)
511
- ]);
512
- if (branchPrefix)
513
- prefixes.add(branchPrefix);
514
- if (integrationPrefix)
515
- prefixes.add(integrationPrefix);
516
- }
517
- catch {
518
- // Not in a repo; the built-in prefixes are the best available guess.
519
- }
520
- return [...prefixes].map(prefix => `origin/${prefix}/*`);
323
+ export async function getProjectItemUrl(branchName) {
324
+ const result = await exec('git', ['config', '--local', '--get', `branch.${branchName}.idea1ProjectItemUrl`]);
325
+ return result.exitCode === 0 && result.stdout.trim() ? result.stdout.trim() : null;
521
326
  }
522
327
  /**
523
- * Find a branch that has a given project item URL committed in `.idea1/<branch>.json`.
524
- * Searches remote branches matching the configured branch prefixes.
525
- * Falls back to checking the legacy `.idea1/project-item-url` for pre-migration branches.
328
+ * The branch this clone remembers as implementing a project item, or null for an item it has yet
329
+ * to resolve.
526
330
  */
527
- export async function findBranchByProjectItemFile(projectItemUrl) {
528
- const globs = await workflowBranchGlobs();
529
- const result = await exec('git', ['branch', '-r', '--list', ...globs]);
530
- if (result.exitCode !== 0 || !result.stdout.trim())
331
+ export async function findBranchWithProjectItemUrl(projectItemUrl) {
332
+ const result = await exec('git', ['config', '--local', '--list']);
333
+ if (result.exitCode !== 0) {
531
334
  return null;
532
- const branches = result.stdout
533
- .split('\n')
534
- .map(line => line.trim())
535
- .filter(name => name.length > 0 && !name.includes('HEAD'));
536
- for (const remoteBranch of branches) {
537
- const localName = remoteBranch.replace('origin/', '');
538
- const jsonFilename = `${sanitizeBranchNameForFilename(localName)}.json`;
539
- // Check new per-branch JSON file
540
- const jsonResult = await exec('git', ['show', `${remoteBranch}:.idea1/${jsonFilename}`]);
541
- if (jsonResult.exitCode === 0) {
542
- try {
543
- const metadata = JSON.parse(jsonResult.stdout);
544
- if (metadata.projectItemUrl === projectItemUrl) {
545
- return localName;
546
- }
547
- }
548
- catch {
549
- // Malformed JSON — skip
550
- }
551
- }
552
- // Backward-compat: check legacy .idea1/project-item-url
553
- const legacyResult = await exec('git', ['show', `${remoteBranch}:.idea1/project-item-url`]);
554
- if (legacyResult.exitCode === 0 && legacyResult.stdout.trim() === projectItemUrl) {
555
- return localName;
556
- }
557
335
  }
558
- return null;
559
- }
560
- /**
561
- * Resolve the branch associated with a project item URL.
562
- * Fetches latest, then checks local git config (fast path) before falling back
563
- * to searching remote branches for the committed `.idea1/<branch>.json` metadata.
564
- * Returns null if no branch is found.
565
- */
566
- export async function resolveBranchFromProjectItemUrl(projectItemUrl) {
567
- // Fetch latest so remote refs are up-to-date
568
- await fetchLatest();
569
- // Fast path: search local git config
570
- const result = await exec('git', ['config', '--local', '--list']);
571
- if (result.exitCode === 0) {
572
- const lines = result.stdout.split('\n');
573
- for (const line of lines) {
574
- const match = line.match(/^branch\.(.+)\.idea1projectitemurl=(.+)$/i);
575
- if (match && match[2] === projectItemUrl) {
576
- return match[1];
577
- }
336
+ for (const line of result.stdout.split('\n')) {
337
+ const match = line.match(/^branch\.(.+)\.idea1projectitemurl=(.+)$/i);
338
+ if (match && match[2] === projectItemUrl) {
339
+ return match[1];
578
340
  }
579
341
  }
580
- // Fallback: search remote branches for committed metadata
581
- return findBranchByProjectItemFile(projectItemUrl);
582
- }
583
- /**
584
- * Read a per-branch value from git config, falling back to the committed
585
- * `.idea1/<branch>.json` metadata file (the durable, remote-persisted copy)
586
- * when no config value is set.
587
- *
588
- * @param branchName Branch to read for (defaults to the current branch).
589
- * @param configKey The `branch.<branch>.<configKey>` git config key.
590
- * @param fromMetadata Pulls the corresponding field out of the committed metadata.
591
- */
592
- async function readBranchValue(branchName, configKey, fromMetadata) {
593
- const branch = branchName ?? await getCurrentBranch();
594
- const result = await exec('git', ['config', '--local', '--get', `branch.${branch}.${configKey}`]);
595
- if (result.exitCode === 0 && result.stdout.trim()) {
596
- return result.stdout.trim();
597
- }
598
- const metadata = await readBranchMetadata(branch);
599
- return (metadata ? fromMetadata(metadata) : undefined) ?? null;
600
- }
601
- /**
602
- * Retrieve project item URL from git config, falling back to the committed
603
- * `.idea1/<branch>.json` metadata file (the durable, remote-persisted copy).
604
- */
605
- export async function getProjectItemUrl(branchName) {
606
- return readBranchValue(branchName, 'idea1ProjectItemUrl', m => m.projectItemUrl);
342
+ return null;
607
343
  }
608
344
  /**
609
- * Store base branch in git config for later retrieval
345
+ * The GitHub repository `origin` points at, as `owner/name`.
610
346
  */
611
- export async function storeBaseBranch(baseBranch, branchName) {
612
- const branch = branchName ?? await getCurrentBranch();
613
- if (baseBranch && branch) {
614
- await execOrThrow('git', ['config', '--local', `branch.${branch}.baseBranch`, baseBranch]);
615
- await updateBranchMetadataFile(branch, { baseBranch });
616
- logger.dim(`Base branch stored in git config for ${branch}: ${baseBranch}`);
347
+ export async function getRepositoryFullName() {
348
+ const url = (await execOrThrow('git', ['remote', 'get-url', 'origin'])).trim();
349
+ const match = url.match(/[:/]([^/:]+\/[^/:]+?)(?:\.git)?\/?$/);
350
+ if (!match) {
351
+ throw new Error(`Could not read the repository name from the origin URL: ${url}`);
617
352
  }
618
- }
619
- /**
620
- * Retrieve base branch for a branch (with hierarchical resolution)
621
- */
622
- export async function getBaseBranch(branchName) {
623
- // The authoritative source (an existing PR) is queried in the command layer.
624
- // Here we resolve branch-specific config, then the committed metadata file,
625
- // then null (caller decides the default).
626
- return readBranchValue(branchName, 'baseBranch', m => m.baseBranch);
353
+ return match[1];
627
354
  }
628
355
  /**
629
356
  * Validate that a branch exists
@@ -657,13 +384,14 @@ export async function commitAllChanges(message) {
657
384
  /**
658
385
  * Push to remote branch
659
386
  */
660
- export async function pushToRemote(branchName) {
387
+ export async function pushToRemote(branchName, cwd) {
661
388
  logger.step(`Pushing changes to remote branch ${branchName}...`);
662
- await execOrThrow('git', ['push', '--set-upstream', 'origin', branchName]);
389
+ await execOrThrow('git', ['push', '--set-upstream', 'origin', branchName], { cwd });
663
390
  logger.success('Changes pushed successfully');
664
391
  }
665
392
  /**
666
- * Garbage collect orphaned branch configs
393
+ * Forget the remembered item URLs (and the per-branch config older CLI versions kept) of branches
394
+ * that no longer exist.
667
395
  */
668
396
  export async function gcOrphanedBranchConfigs() {
669
397
  const result = await exec('git', ['config', '--local', '--list']);
@@ -688,103 +416,6 @@ export async function gcOrphanedBranchConfigs() {
688
416
  if (removedCount > 0) {
689
417
  logger.dim(`Removed ${removedCount} orphaned branch config entries`);
690
418
  }
691
- // Also clean up orphaned .idea1/*.json files
692
- await gcOrphanedBranchMetadataFiles();
693
- }
694
- /**
695
- * Remove `.idea1/*.json` files for branches that no longer exist (excluding the settings files)
696
- */
697
- async function gcOrphanedBranchMetadataFiles() {
698
- try {
699
- const repoRoot = await getRepoRoot();
700
- const idea1Dir = join(repoRoot, '.idea1');
701
- const files = await readdir(idea1Dir);
702
- let removedCount = 0;
703
- for (const file of files) {
704
- if (!file.endsWith('.json') || SETTINGS_FILES.includes(file))
705
- continue;
706
- const branchName = branchNameFromFilename(file);
707
- const exists = await branchExists(branchName);
708
- if (!exists) {
709
- await unlink(join(idea1Dir, file));
710
- removedCount++;
711
- }
712
- }
713
- if (removedCount > 0) {
714
- logger.dim(`Removed ${removedCount} orphaned branch metadata files`);
715
- }
716
- }
717
- catch {
718
- // .idea1/ directory doesn't exist or isn't readable — skip
719
- }
720
- }
721
- /**
722
- * Store workflow state in git config for later retrieval
723
- */
724
- export async function storeWorkflowState(state, branchName) {
725
- const branch = branchName ?? await getCurrentBranch();
726
- if (state && branch) {
727
- await execOrThrow('git', ['config', '--local', `branch.${branch}.idea1WorkflowState`, state]);
728
- await updateBranchMetadataFile(branch, { workflowState: state });
729
- logger.dim(`Workflow state stored in git config for ${branch}`);
730
- }
731
- }
732
- /**
733
- * Retrieve workflow state from git config
734
- */
735
- export async function getWorkflowState(branchName) {
736
- return readBranchValue(branchName, 'idea1WorkflowState', m => m.workflowState);
737
- }
738
- /**
739
- * Store the development workflow type in git config
740
- */
741
- export async function storeDevWorkflowType(workflowType, branchName) {
742
- const branch = branchName ?? await getCurrentBranch();
743
- if (workflowType && branch) {
744
- await execOrThrow('git', ['config', '--local', `branch.${branch}.idea1DevWorkflowType`, workflowType]);
745
- await updateBranchMetadataFile(branch, { devWorkflowType: workflowType });
746
- logger.dim(`Dev workflow type stored in git config for ${branch}: ${workflowType}`);
747
- }
748
- }
749
- /**
750
- * Retrieve the development workflow type from git config
751
- */
752
- export async function getDevWorkflowType(branchName) {
753
- return readBranchValue(branchName, 'idea1DevWorkflowType', m => m.devWorkflowType);
754
- }
755
- /**
756
- * Store the stack branch in git config (for stacked workflows)
757
- */
758
- export async function storeStackBranch(stackBranch, branchName) {
759
- const branch = branchName ?? await getCurrentBranch();
760
- if (stackBranch && branch) {
761
- await execOrThrow('git', ['config', '--local', `branch.${branch}.idea1StackBranch`, stackBranch]);
762
- await updateBranchMetadataFile(branch, { stackBranch });
763
- logger.dim(`Stack branch stored in git config for ${branch}: ${stackBranch}`);
764
- }
765
- }
766
- /**
767
- * Retrieve the stack branch from git config
768
- */
769
- export async function getStackBranch(branchName) {
770
- return readBranchValue(branchName, 'idea1StackBranch', m => m.stackBranch);
771
- }
772
- /**
773
- * Store the integration branch name in git config
774
- */
775
- export async function storeIntegrationBranch(integrationBranch, branchName) {
776
- const branch = branchName ?? await getCurrentBranch();
777
- if (integrationBranch && branch) {
778
- await execOrThrow('git', ['config', '--local', `branch.${branch}.idea1IntegrationBranch`, integrationBranch]);
779
- await updateBranchMetadataFile(branch, { integrationBranch });
780
- logger.dim(`Integration branch stored in git config for ${branch}: ${integrationBranch}`);
781
- }
782
- }
783
- /**
784
- * Retrieve the integration branch name from git config
785
- */
786
- export async function getIntegrationBranch(branchName) {
787
- return readBranchValue(branchName, 'idea1IntegrationBranch', m => m.integrationBranch);
788
419
  }
789
420
  /**
790
421
  * Fetch and pull a specific branch from origin, ensuring it's up to date
@@ -798,16 +429,7 @@ export async function fetchAndPullBranch(branchName) {
798
429
  // Update local branch to match remote without checking it out
799
430
  const currentBranch = await getCurrentBranch();
800
431
  if (currentBranch === branchName) {
801
- // Try fast-forward first; if not possible, try rebase; if conflicts, fall back to merge
802
- const ffResult = await exec('git', ['pull', '--ff-only', 'origin', branchName]);
803
- if (ffResult.exitCode !== 0) {
804
- const rebaseResult = await exec('git', ['pull', '--rebase', 'origin', branchName]);
805
- if (rebaseResult.exitCode !== 0) {
806
- await exec('git', ['rebase', '--abort']);
807
- logger.warning('Rebase had conflicts, falling back to merge commit');
808
- await execOrThrow('git', ['pull', '--no-rebase', 'origin', branchName]);
809
- }
810
- }
432
+ await pullBranch(branchName);
811
433
  }
812
434
  else {
813
435
  // Check if branch is in a secondary worktree — git fetch origin branch:branch
@@ -815,7 +437,7 @@ export async function fetchAndPullBranch(branchName) {
815
437
  const worktreePath = await getWorktreePathForBranch(branchName);
816
438
  if (worktreePath) {
817
439
  logger.step(`Branch ${branchName} is in a worktree at ${worktreePath}, pulling there...`);
818
- await execOrThrow('git', ['pull', 'origin', branchName], { cwd: worktreePath });
440
+ await pullBranch(branchName, worktreePath);
819
441
  }
820
442
  else {
821
443
  await execOrThrow('git', ['fetch', 'origin', `${branchName}:${branchName}`]);
@@ -824,12 +446,39 @@ export async function fetchAndPullBranch(branchName) {
824
446
  }
825
447
  }
826
448
  /**
827
- * Merge a source branch into the current branch with a merge commit
449
+ * Run a git command that merges into `targetBranch`, checked out at `cwd` (the current directory
450
+ * when omitted). A merge that stops on conflicts is left in progress for the developer to resolve
451
+ * and reported as an error saying so; nothing is aborted, retried, or rebased.
452
+ */
453
+ async function runMerge(args, sourceRef, targetBranch, cwd) {
454
+ const result = await exec('git', args, { cwd });
455
+ if (result.exitCode === 0) {
456
+ return;
457
+ }
458
+ const mergeInProgress = await exec('git', ['rev-parse', '-q', '--verify', 'MERGE_HEAD'], { cwd });
459
+ if (mergeInProgress.exitCode === 0) {
460
+ const location = cwd ?? (await getRepoRoot()).trim();
461
+ throw new Error(`Merging ${sourceRef} into ${targetBranch} has conflicts to resolve before the merge can complete. ` +
462
+ `Resolve them in ${location} (on ${targetBranch}), commit the merge, and push ${targetBranch}.`);
463
+ }
464
+ throw new Error(result.stderr || `Command failed: git ${args.join(' ')}`);
465
+ }
466
+ /**
467
+ * Pull `origin/<branch>` into the local branch checked out at `cwd` (the current directory when
468
+ * omitted). Pulls never rebase, whatever the clone's `pull.rebase` or `branch.<name>.rebase`
469
+ * config says: a branch that is behind fast-forwards, and one that has diverged gets a merge commit.
470
+ */
471
+ export async function pullBranch(branchName, cwd) {
472
+ await runMerge(['pull', '--no-rebase', '--no-edit', 'origin', branchName], `origin/${branchName}`, branchName, cwd);
473
+ }
474
+ /**
475
+ * Merge a source branch into `targetBranch`, checked out at `cwd` (the current directory when
476
+ * omitted), always with a merge commit.
828
477
  */
829
- export async function mergeBranch(sourceBranch) {
830
- logger.step(`Merging ${sourceBranch} into current branch...`);
831
- await execOrThrow('git', ['merge', '--no-ff', sourceBranch, '--no-edit']);
832
- logger.success(`Successfully merged ${sourceBranch}`);
478
+ export async function mergeBranch(sourceBranch, targetBranch, cwd) {
479
+ logger.step(`Merging ${sourceBranch} into ${targetBranch}...`);
480
+ await runMerge(['merge', '--no-ff', '--no-edit', sourceBranch], sourceBranch, targetBranch, cwd);
481
+ logger.success(`Merged ${sourceBranch} into ${targetBranch}`);
833
482
  }
834
483
  /**
835
484
  * Check if merging source into target would produce conflicts,
@@ -849,12 +498,6 @@ export async function checkMergeConflicts(source, target) {
849
498
  }
850
499
  return { hasConflicts: false };
851
500
  }
852
- /**
853
- * Abort an in-progress merge
854
- */
855
- export async function abortMerge() {
856
- await exec('git', ['merge', '--abort']);
857
- }
858
501
  /**
859
502
  * List remote branches matching a prefix pattern
860
503
  */
@@ -967,8 +610,7 @@ async function getGitCommonDir() {
967
610
  /**
968
611
  * Commit file changes on top of a ref without checking anything out, using a
969
612
  * temporary index. Returns the new commit sha (not yet on any branch). Used for
970
- * branches that have no local checkout, so the branch can carry its workflow
971
- * metadata before anyone works on it.
613
+ * branches that have no local checkout.
972
614
  */
973
615
  export async function commitFilesDetached(parentRef, changes, message) {
974
616
  const parentSha = (await execOrThrow('git', ['rev-parse', '--verify', `${parentRef}^{commit}`])).trim();
@@ -1008,32 +650,6 @@ export async function pushCommitToBranch(branchName, commitSha) {
1008
650
  await execOrThrow('git', ['update-ref', `refs/heads/${branchName}`, commitSha]);
1009
651
  }
1010
652
  }
1011
- /**
1012
- * Create a branch on origin from a source ref (e.g. `origin/working/release`) with
1013
- * its `.idea1/<branch>.json` metadata as the first commit. No checkout is needed.
1014
- */
1015
- export async function createBranchWithMetadataDetached(branchName, sourceRef, metadata) {
1016
- logger.step(`Creating branch ${branchName} from ${sourceRef}...`);
1017
- const commitSha = await commitFilesDetached(sourceRef, [{ path: getBranchMetadataRelativePath(branchName), content: serializeBranchMetadata(metadata) }], `idea1: link to ${metadata.projectItemUrl ?? branchName}`);
1018
- await pushCommitToBranch(branchName, commitSha);
1019
- logger.success(`Branch ${branchName} created on origin with its workflow metadata`);
1020
- }
1021
- /**
1022
- * Update fields of the committed `.idea1/<branch>.json` on origin for a branch
1023
- * that has no local checkout. Returns true when a commit was pushed.
1024
- */
1025
- export async function updateBranchMetadataDetached(branchName, updates) {
1026
- const remote = await readBranchMetadataFromRef(branchName);
1027
- const merged = { ...(remote ?? { branchName }), ...updates, branchName };
1028
- const unchanged = remote !== null && Object.keys(updates).every(key => remote[key] === merged[key]);
1029
- if (unchanged) {
1030
- return false;
1031
- }
1032
- const commitSha = await commitFilesDetached(`origin/${branchName}`, [{ path: getBranchMetadataRelativePath(branchName), content: serializeBranchMetadata(merged) }], `idea1: update metadata for ${merged.projectItemUrl ?? branchName}`);
1033
- await pushCommitToBranch(branchName, commitSha);
1034
- logger.dim('Committed branch metadata update on origin');
1035
- return true;
1036
- }
1037
653
  /**
1038
654
  * Create a branch on origin pointing at a source ref, without any local checkout.
1039
655
  */
@@ -1073,17 +689,4 @@ export async function fetchRemoteBranch(branchName) {
1073
689
  const result = await exec('git', ['fetch', 'origin', branchName]);
1074
690
  return result.exitCode === 0;
1075
691
  }
1076
- /**
1077
- * Remove the committed `.idea1/<branch>.json` from a branch that has no local
1078
- * checkout, so the file does not leak into the target branch when the PR merges.
1079
- */
1080
- export async function removeBranchMetadataDetached(branchName) {
1081
- const remote = await readBranchMetadataFromRef(branchName);
1082
- if (!remote) {
1083
- return;
1084
- }
1085
- const commitSha = await commitFilesDetached(`origin/${branchName}`, [{ path: getBranchMetadataRelativePath(branchName), content: null }], `idea1: remove branch metadata for ${branchName}`);
1086
- await pushCommitToBranch(branchName, commitSha);
1087
- logger.dim('Removed branch metadata file before merge');
1088
- }
1089
692
  //# sourceMappingURL=git.js.map