@ramon-sg/issue-flow 0.3.3 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/commands/adapter.js +3 -5
- package/dist/commands/claude-hooks.js +1 -1
- package/dist/commands/session.js +1 -1
- package/dist/commands/worktree.js +5 -6
- package/dist/worktree-steps.d.ts +30 -3
- package/dist/worktree-steps.js +70 -6
- package/package.json +1 -1
- package/schema.json +1 -1
package/README.md
CHANGED
|
@@ -119,7 +119,7 @@ Lo propio de cada repo (instalar, armar la base, levantar Docker) son **pasos**
|
|
|
119
119
|
```
|
|
120
120
|
|
|
121
121
|
- **Cuándo corren:** `issue start` corre el `setup` en cada worktree nuevo, después de copiar `.worktreeinclude`. El hook `session` lo corre al abrir Claude en un worktree. El `teardown` corre antes de borrar un worktree, con `prune` o al limpiar después del merge.
|
|
122
|
-
- **De dónde salen:**
|
|
122
|
+
- **De dónde salen:** del `.agents/flow.json` de `origin/<base>`, nunca del worktree. Así una rama no puede sumar comandos a su propio setup, y un checkout principal que quedó atrás no deja al worktree sin su `install`. Si los pasos del checkout principal son otros, `issue-flow` lo avisa. Sin `origin/<base>`, o si ahí el archivo no es válido, usa los del checkout principal. Sin pasos, `worktree setup` e `issue start` dicen de qué archivo los leyeron. En la nube no hay otro checkout: ahí la config es la de la rama de la sesión. El teardown sigue usando la config del checkout principal.
|
|
123
123
|
- **Pueden correr más de una vez:** `issue start` y el hook `session` corren el `setup` del mismo worktree, así que un paso sin `once` tiene que poder repetirse.
|
|
124
124
|
- **`trusted`:** el paso corre solo si el código del worktree está en `origin/<base>` y no tiene cambios. Es para un install, que corre los scripts de `package.json` de esa rama. En la nube corre siempre, porque la máquina es descartable.
|
|
125
125
|
- **`once`:** el paso no se repite una vez que funcionó (deja una marca en el git dir del worktree).
|
package/dist/commands/adapter.js
CHANGED
|
@@ -22,11 +22,11 @@ import { canonicalPath, dockerCleanup } from "../docker.js";
|
|
|
22
22
|
import { checkoutRoot, currentBranch, locateRepo } from "../git.js";
|
|
23
23
|
import { issueOfBranch } from "../issue-text.js";
|
|
24
24
|
import { inPath } from "../launch.js";
|
|
25
|
-
import {
|
|
25
|
+
import { warn } from "../output.js";
|
|
26
26
|
import { releasePortClaims } from "../ports.js";
|
|
27
27
|
import { copyWorktreeIncludes } from "../start-place.js";
|
|
28
28
|
import { StartError } from "../start-state.js";
|
|
29
|
-
import { runSetup, teardownWorktree } from "../worktree-steps.js";
|
|
29
|
+
import { checkOnly, runSetup, teardownWorktree } from "../worktree-steps.js";
|
|
30
30
|
import { issueStart } from "./issue-start.js";
|
|
31
31
|
import { stepPlace } from "./worktree.js";
|
|
32
32
|
/** Orca's CLI, which knows which issue a workspace came from; `ISSUE_FLOW_ORCA_CLI` changes it. */
|
|
@@ -162,9 +162,7 @@ function linkedPlace(options, out) {
|
|
|
162
162
|
* @example await adapterSetup(options, out)
|
|
163
163
|
*/
|
|
164
164
|
export async function adapterSetup(options, out) {
|
|
165
|
-
|
|
166
|
-
if (options.only !== null && !names.includes(options.only))
|
|
167
|
-
throw new UsageError(`--only expects a setup step of .agents/flow.json (${names.join(", ") || "it has none"}): ${options.only}`);
|
|
165
|
+
checkOnly(stepPlace(options, out), options.only);
|
|
168
166
|
const place = linkedPlace(options, out);
|
|
169
167
|
if (place === null)
|
|
170
168
|
return 0;
|
|
@@ -111,7 +111,7 @@ export async function worktreeCreate(input, config, cwd, out) {
|
|
|
111
111
|
throw new StartError(`could not make the worktree ${path}: ${add.err}`);
|
|
112
112
|
copyWorktreeIncludes(mainRoot, path, stderrOnly);
|
|
113
113
|
}
|
|
114
|
-
await runSetup({ root: path, mainRoot, base, config, out: stderrOnly, remote: false }, null);
|
|
114
|
+
await runSetup({ root: path, mainRoot, base, config, out: stderrOnly, remote: false }, null, true);
|
|
115
115
|
out.stdout(`${path}\n`);
|
|
116
116
|
return 0;
|
|
117
117
|
}
|
package/dist/commands/session.js
CHANGED
|
@@ -90,7 +90,7 @@ export async function session(options, out) {
|
|
|
90
90
|
if (remote)
|
|
91
91
|
cloudSession(root, say);
|
|
92
92
|
if (root !== mainRoot || remote)
|
|
93
|
-
await runSetup(place, null);
|
|
93
|
+
await runSetup(place, null, true);
|
|
94
94
|
const unstarted = !remote && root !== mainRoot ? unstartedIssue(root) : null;
|
|
95
95
|
if (unstarted !== null)
|
|
96
96
|
say(`issue #${unstarted} was not started: its branch is not published and has no draft PR. Before working, run the start-issue skill (${unstarted}).`);
|
|
@@ -12,11 +12,11 @@ import { resolve } from "node:path";
|
|
|
12
12
|
import { canonicalPath, dockerCleanup } from "../docker.js";
|
|
13
13
|
import { checkoutRoot, git, isClean, locateRepo, safeToDelete, worktrees } from "../git.js";
|
|
14
14
|
import { forgetSession } from "../launch.js";
|
|
15
|
-
import {
|
|
15
|
+
import { warn } from "../output.js";
|
|
16
16
|
import { claimPortBase, nextPortBase, portBase } from "../ports.js";
|
|
17
17
|
import { isStale, LOCK_REASON } from "../session-lock.js";
|
|
18
18
|
import { StartError } from "../start-state.js";
|
|
19
|
-
import { runSetup, teardownWorktree } from "../worktree-steps.js";
|
|
19
|
+
import { checkOnly, runSetup, teardownWorktree } from "../worktree-steps.js";
|
|
20
20
|
/** The reason `claude --worktree` locks the worktrees it makes while its session lives. */
|
|
21
21
|
const CLAUDE_LOCK_REASON = "claude session";
|
|
22
22
|
/**
|
|
@@ -57,10 +57,9 @@ export function stepPlace(options, out) {
|
|
|
57
57
|
* @example await worktreeSetup(options, out)
|
|
58
58
|
*/
|
|
59
59
|
export async function worktreeSetup(options, out) {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return (await runSetup(stepPlace(options, out), options.only)) ? 0 : 1;
|
|
60
|
+
const place = stepPlace(options, out);
|
|
61
|
+
checkOnly(place, options.only);
|
|
62
|
+
return (await runSetup(place, options.only)) ? 0 : 1;
|
|
64
63
|
}
|
|
65
64
|
/**
|
|
66
65
|
* `worktree teardown [--path <dir>]`: cleans a worktree before deleting it (its steps, its
|
package/dist/worktree-steps.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type FlowConfig } from "./config.js";
|
|
2
2
|
import { type Output } from "./output.js";
|
|
3
|
+
import { type Step } from "./worktree-config.js";
|
|
3
4
|
/** Folder, in the worktree's git dir, with a mark per `once` step that worked. */
|
|
4
5
|
export declare const SETUP_MARKS_DIR = "issue-flow-setup";
|
|
5
6
|
/** Where the steps run, and with what. */
|
|
@@ -30,17 +31,43 @@ export declare function trustedCode(root: string, base: string): boolean;
|
|
|
30
31
|
* @example stepEnv(place, 21430).ISSUE_FLOW_PORT_BASE // "21430"
|
|
31
32
|
*/
|
|
32
33
|
export declare function stepEnv(place: StepPlace, port: number | null): Record<string, string>;
|
|
34
|
+
/** The setup steps a worktree runs, and where they were read. */
|
|
35
|
+
export interface SetupSteps {
|
|
36
|
+
steps: Step[];
|
|
37
|
+
source: string;
|
|
38
|
+
warning: string | null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The setup steps: those of `.agents/flow.json` in `origin/<base>`, which a worktree made
|
|
42
|
+
* from it has; none if the file is not there. In a cloud session, without `origin/<base>`,
|
|
43
|
+
* or with a file there that is not valid, those of the checkout's config. The warning says
|
|
44
|
+
* when the main checkout's steps differ, or why they were used.
|
|
45
|
+
* @param place - Where.
|
|
46
|
+
* @returns The steps, where they come from and a warning, or null.
|
|
47
|
+
* @example setupSteps(place).source // "origin/main:.agents/flow.json"
|
|
48
|
+
*/
|
|
49
|
+
export declare function setupSteps(place: StepPlace): SetupSteps;
|
|
50
|
+
/**
|
|
51
|
+
* Checks that `--only` names one of the setup steps.
|
|
52
|
+
* @param place - Where.
|
|
53
|
+
* @param only - The step, or null.
|
|
54
|
+
* @throws UsageError if it names no step.
|
|
55
|
+
* @example checkOnly(place, "deps")
|
|
56
|
+
*/
|
|
57
|
+
export declare function checkOnly(place: StepPlace, only: string | null): void;
|
|
33
58
|
/**
|
|
34
59
|
* Prepares a worktree with the setup steps: one (`only`) or all, in order. In the main
|
|
35
60
|
* checkout it does nothing, except in a cloud session, whose checkout is not a worktree. It
|
|
36
61
|
* fixes the block of ports first, so the steps (and the apps after them) agree on it.
|
|
37
|
-
* Never stops on a failed step: it says what to run and goes on.
|
|
62
|
+
* Never stops on a failed step: it says what to run and goes on. Without steps it says so,
|
|
63
|
+
* unless `quiet` (the session hooks, which run at every start).
|
|
38
64
|
* @param place - Where.
|
|
39
65
|
* @param only - The step to run, or null for all.
|
|
66
|
+
* @param quiet - True to say nothing when there are no steps.
|
|
40
67
|
* @returns True if every step that ran worked.
|
|
41
68
|
* @example await runSetup(place, null)
|
|
42
69
|
*/
|
|
43
|
-
export declare function runSetup(place: StepPlace, only: string | null): Promise<boolean>;
|
|
70
|
+
export declare function runSetup(place: StepPlace, only: string | null, quiet?: boolean): Promise<boolean>;
|
|
44
71
|
/**
|
|
45
72
|
* Cleans a worktree before it is deleted: its apps and then its teardown steps (if the
|
|
46
73
|
* folder still exists), so a step that drops a database finds nobody connected; then its
|
package/dist/worktree-steps.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file Runs the steps of `worktrees.setup` and `worktrees.teardown` in a worktree. The
|
|
3
|
-
* steps come from
|
|
4
|
-
* commands to its own setup
|
|
3
|
+
* setup steps come from `.agents/flow.json` in `origin/<base>`, not from the worktree, so a
|
|
4
|
+
* branch cannot add commands to its own setup, and a main checkout nobody pulled does not
|
|
5
|
+
* leave a new worktree without its install. The teardown steps come from the main
|
|
6
|
+
* checkout's config. A `trusted` step (an install, which runs the lifecycle scripts
|
|
5
7
|
* of `package.json`) runs only if the worktree's code is in `origin/<base>` and has no
|
|
6
8
|
* changes; a `once` step leaves a mark when it works and is not repeated. Two setups of one
|
|
7
9
|
* worktree at a time (the tool's and the session hook's) take turns with a lock. Each step
|
|
@@ -12,11 +14,14 @@ import { spawnSync } from "node:child_process";
|
|
|
12
14
|
import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
13
15
|
import { basename, join } from "node:path";
|
|
14
16
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
17
|
+
import { CONFIG_PATH, isObject } from "./config-base.js";
|
|
18
|
+
import { DEFAULT_CONFIG } from "./config.js";
|
|
15
19
|
import { dockerCleanup, sanitizeLabel } from "./docker.js";
|
|
16
20
|
import { git, isClean } from "./git.js";
|
|
17
|
-
import { warn } from "./output.js";
|
|
21
|
+
import { UsageError, warn } from "./output.js";
|
|
18
22
|
import { claimPortBase, portBase, releasePortClaims, sha256 } from "./ports.js";
|
|
19
23
|
import { stopApps } from "./stop-apps.js";
|
|
24
|
+
import { worktreesSection } from "./worktree-config.js";
|
|
20
25
|
/** Folder, in the worktree's git dir, with a mark per `once` step that worked. */
|
|
21
26
|
export const SETUP_MARKS_DIR = "issue-flow-setup";
|
|
22
27
|
/** The lock of the setup, in the worktree's git dir. */
|
|
@@ -78,6 +83,58 @@ function runStep(place, step, env) {
|
|
|
78
83
|
warn(place.out, `step "${step.name}" failed${result.status === null ? "" : ` (exit ${result.status})`}; run '${step.run}' in ${place.root}.`);
|
|
79
84
|
return false;
|
|
80
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* The setup steps: those of `.agents/flow.json` in `origin/<base>`, which a worktree made
|
|
88
|
+
* from it has; none if the file is not there. In a cloud session, without `origin/<base>`,
|
|
89
|
+
* or with a file there that is not valid, those of the checkout's config. The warning says
|
|
90
|
+
* when the main checkout's steps differ, or why they were used.
|
|
91
|
+
* @param place - Where.
|
|
92
|
+
* @returns The steps, where they come from and a warning, or null.
|
|
93
|
+
* @example setupSteps(place).source // "origin/main:.agents/flow.json"
|
|
94
|
+
*/
|
|
95
|
+
export function setupSteps(place) {
|
|
96
|
+
const local = place.config.worktrees.setup;
|
|
97
|
+
const ref = `origin/${place.base}`;
|
|
98
|
+
const fallback = { steps: local, source: `the main checkout's ${CONFIG_PATH}` };
|
|
99
|
+
// NOTE: a cloud session has no other checkout: its steps are those of the session's branch.
|
|
100
|
+
if (place.remote || !git(place.mainRoot, ["rev-parse", "-q", "--verify", `${ref}^{commit}`]).ok)
|
|
101
|
+
return { ...fallback, warning: null };
|
|
102
|
+
const source = `${ref}:${CONFIG_PATH}`;
|
|
103
|
+
const shown = git(place.mainRoot, ["show", source]);
|
|
104
|
+
let steps;
|
|
105
|
+
try {
|
|
106
|
+
if (!shown.ok)
|
|
107
|
+
steps = DEFAULT_CONFIG.worktrees.setup;
|
|
108
|
+
else {
|
|
109
|
+
const raw = JSON.parse(shown.out);
|
|
110
|
+
steps = worktreesSection(isObject(raw) ? raw.worktrees : undefined, DEFAULT_CONFIG.worktrees).setup;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
115
|
+
return { ...fallback, warning: `${source} is not valid: ${reason}; using the main checkout's steps.` };
|
|
116
|
+
}
|
|
117
|
+
const differ = JSON.stringify(steps) !== JSON.stringify(local);
|
|
118
|
+
return {
|
|
119
|
+
steps,
|
|
120
|
+
source,
|
|
121
|
+
warning: differ ? `the setup steps come from ${source}; the main checkout's differ (is it behind? pull it).` : null,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Checks that `--only` names one of the setup steps.
|
|
126
|
+
* @param place - Where.
|
|
127
|
+
* @param only - The step, or null.
|
|
128
|
+
* @throws UsageError if it names no step.
|
|
129
|
+
* @example checkOnly(place, "deps")
|
|
130
|
+
*/
|
|
131
|
+
export function checkOnly(place, only) {
|
|
132
|
+
if (only === null)
|
|
133
|
+
return;
|
|
134
|
+
const names = setupSteps(place).steps.map((step) => step.name);
|
|
135
|
+
if (!names.includes(only))
|
|
136
|
+
throw new UsageError(`--only expects a setup step of ${CONFIG_PATH} (${names.join(", ") || "it has none"}): ${only}`);
|
|
137
|
+
}
|
|
81
138
|
/**
|
|
82
139
|
* Takes the setup's lock: a folder with the pid. A lock of a process that is gone is taken
|
|
83
140
|
* over, and so is one without a pid that is some seconds old (its process died before
|
|
@@ -126,17 +183,24 @@ function alive(pid) {
|
|
|
126
183
|
* Prepares a worktree with the setup steps: one (`only`) or all, in order. In the main
|
|
127
184
|
* checkout it does nothing, except in a cloud session, whose checkout is not a worktree. It
|
|
128
185
|
* fixes the block of ports first, so the steps (and the apps after them) agree on it.
|
|
129
|
-
* Never stops on a failed step: it says what to run and goes on.
|
|
186
|
+
* Never stops on a failed step: it says what to run and goes on. Without steps it says so,
|
|
187
|
+
* unless `quiet` (the session hooks, which run at every start).
|
|
130
188
|
* @param place - Where.
|
|
131
189
|
* @param only - The step to run, or null for all.
|
|
190
|
+
* @param quiet - True to say nothing when there are no steps.
|
|
132
191
|
* @returns True if every step that ran worked.
|
|
133
192
|
* @example await runSetup(place, null)
|
|
134
193
|
*/
|
|
135
|
-
export async function runSetup(place, only) {
|
|
194
|
+
export async function runSetup(place, only, quiet = false) {
|
|
136
195
|
const { root, mainRoot, config, out } = place;
|
|
137
196
|
if (root === mainRoot && !place.remote)
|
|
138
197
|
return true;
|
|
139
|
-
const
|
|
198
|
+
const found = setupSteps(place);
|
|
199
|
+
if (found.warning !== null)
|
|
200
|
+
warn(out, found.warning);
|
|
201
|
+
if (found.steps.length === 0 && !quiet)
|
|
202
|
+
warn(out, `no setup steps in ${found.source}; nothing ran.`);
|
|
203
|
+
const chosen = found.steps.filter((step) => only === null || step.name === only);
|
|
140
204
|
const ports = config.worktrees.ports;
|
|
141
205
|
const port = ports === null ? null : await claimPortBase(ports, root, null);
|
|
142
206
|
if (ports !== null && port === null)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ramon-sg/issue-flow",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Issue-to-PR workflow for any repo: names each issue's branch, creates its worktree, opens the draft PR and launches Claude Code, locally and in Claude's cloud.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/schema.json
CHANGED
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"default": ".claude/worktrees"
|
|
77
77
|
},
|
|
78
78
|
"setup": {
|
|
79
|
-
"description": "Steps that prepare a worktree, in order. They come from the main checkout's
|
|
79
|
+
"description": "Steps that prepare a worktree, in order. They come from this file in origin/<base> (the main checkout's if origin has none to read), never the worktree's.",
|
|
80
80
|
"type": "array",
|
|
81
81
|
"items": {
|
|
82
82
|
"type": "object",
|