@ramon-sg/issue-flow 0.2.7 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -13
- package/dist/cli-setup.d.ts +5 -3
- package/dist/cli-setup.js +18 -8
- package/dist/cli.js +1 -0
- package/dist/commands/apply.d.ts +21 -0
- package/dist/commands/apply.js +231 -0
- package/dist/commands/doctor.js +40 -20
- package/dist/commands/pr-worktree.js +10 -10
- package/dist/git.d.ts +16 -0
- package/dist/git.js +23 -0
- package/dist/package-edits.d.ts +18 -30
- package/dist/package-edits.js +30 -42
- package/dist/settings-hooks.d.ts +1 -1
- package/dist/settings-hooks.js +3 -3
- package/dist/setup-files.d.ts +21 -4
- package/dist/setup-files.js +29 -9
- package/dist/start-modes.js +2 -1
- package/dist/start-place.js +11 -7
- package/dist/start-summary.d.ts +1 -1
- package/dist/start-summary.js +2 -2
- package/dist/usage.d.ts +1 -1
- package/dist/usage.js +7 -6
- package/package.json +2 -2
- package/dist/commands/init.d.ts +0 -24
- package/dist/commands/init.js +0 -134
- package/dist/commands/upgrade.d.ts +0 -16
- package/dist/commands/upgrade.js +0 -72
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Flujo de trabajo de issue a PR para cualquier repo, en local y en las sesiones d
|
|
|
6
6
|
|
|
7
7
|
## Qué va a tener
|
|
8
8
|
|
|
9
|
-
- **`issue-flow
|
|
9
|
+
- **`issue-flow apply|doctor`:** deja como lo pide su versión, y revisa, lo que el flujo deja en el repo: `.agents/flow.json` con sus recetas, las dependencias y los hooks. Los corre `agent-kitchen`.
|
|
10
10
|
- **`issue-flow issue branch <n>`:** propone la rama de una issue (con Claude por defecto, o desde el título). Con `--missing`, agrega el slug de rama a todas las issues abiertas que no lo tienen.
|
|
11
11
|
- **`issue-flow labels sync`:** crea en GitHub los labels que el flujo espera y le faltan al repo: los de tipo y `P1` a `P4`, con los mismos colores en todos los repos.
|
|
12
12
|
- **`issue-flow issue start <n>`:** crea la rama y el worktree, hace el commit inicial, abre el PR en draft, asigna la issue y abre Claude. Con `--cloud`, lo hace en una sesión en la nube.
|
|
@@ -181,30 +181,33 @@ Es el hook `SessionStart` de Claude Code. Lo que imprime le llega a Claude como
|
|
|
181
181
|
4. Borra el Docker de los worktrees que ya no existen: Claude Code no llama al hook `WorktreeRemove` de los worktrees que no hizo un hook `WorktreeCreate`.
|
|
182
182
|
5. Si hay `.envrc` y `direnv`, agrega su entorno a `CLAUDE_ENV_FILE`, para que los comandos de la sesión lo tengan.
|
|
183
183
|
|
|
184
|
-
## Lo que el flujo deja en el repo: `issue-flow
|
|
184
|
+
## Lo que el flujo deja en el repo: `issue-flow apply`
|
|
185
185
|
|
|
186
|
-
`npx agent-kitchen apply` corre
|
|
186
|
+
`npx agent-kitchen apply` corre `apply` en la versión que elige y después mueve las skills del flujo a la misma tag. Correrlo a mano sirve para probar el flujo en un repo sin lo demás.
|
|
187
187
|
|
|
188
188
|
```sh
|
|
189
|
-
npx @ramon-sg/issue-flow
|
|
190
|
-
npx @ramon-sg/issue-flow
|
|
191
|
-
npx @ramon-sg/issue-flow
|
|
192
|
-
npx @ramon-sg/issue-flow
|
|
189
|
+
npx @ramon-sg/issue-flow@<versión> apply # deja el repo como lo pide esa versión
|
|
190
|
+
npx @ramon-sg/issue-flow@<versión> apply --yes # sin preguntas, con las recetas que sugiere el repo
|
|
191
|
+
npx @ramon-sg/issue-flow apply --recipe docker-compose # suma una receta más tarde
|
|
192
|
+
npx @ramon-sg/issue-flow apply --dry-run # solo dice qué cambiaría (--json para agent-kitchen)
|
|
193
|
+
npx @ramon-sg/issue-flow doctor # revisa que el repo esté como lo deja apply (--json)
|
|
193
194
|
```
|
|
194
195
|
|
|
195
|
-
`
|
|
196
|
+
`apply` deja el repo como lo pide la versión que corre, y correrlo otra vez no cambia nada. La versión la elige quien actualiza, al elegir cuál correr: `apply` no cambia la versión de ningún otro paquete.
|
|
196
197
|
|
|
197
|
-
1. **`.agents/flow.json`:** si no existe, lo crea con el `$schema` de esta versión y, si el repo tiene `paseo.json`, con su plantilla de rama y sus tipos.
|
|
198
|
-
2. **`package.json`:** `@ramon-sg/issue-flow
|
|
198
|
+
1. **`.agents/flow.json`:** si no existe, lo crea con el `$schema` de esta versión y, si el repo tiene `paseo.json`, con su plantilla de rama y sus tipos. Esa primera vez propone las recetas que sugieren los archivos del repo (con `--yes`, todas; sin terminal y sin `--yes`, no escribe nada y lo dice). Si ya existe, no pregunta: corre las migraciones de esta versión, mueve el `$schema` y suma las recetas de `--recipe`.
|
|
199
|
+
2. **`package.json`:** `@ramon-sg/issue-flow` en esta versión, exacta. `@ramon-sg/github-rest` y los scripts `issue:start`, `worktree:prune` y `pr:worktree` solo si faltan: un `github-rest` que el repo ya tiene se queda, con un aviso si no llega a la versión que necesitan las skills.
|
|
199
200
|
3. **`.claude/settings.json`:** los hooks `SessionStart`, `WorktreeCreate` y `WorktreeRemove`, fijados a esta versión y con 600 segundos. El de `SessionStart` corre `claude-cloud session` en la nube, así que un hook propio de `claude-cloud` se quita para que no corra dos veces.
|
|
200
201
|
|
|
201
|
-
|
|
202
|
+
No consulta npm, y nunca baja de versión: si el repo ya tiene una issue-flow más nueva (por ejemplo, `package.json` subido a mano antes del `install`), o un rango como `^0.2.6`, se detiene y dice qué correr. `--json` da `{ version, changes, kept, written, next }`. Sale con 0 si aplicó o no había nada que hacer, y con 1 si se negó o tenía que preguntar y no pudo.
|
|
202
203
|
|
|
203
|
-
**`
|
|
204
|
+
**`doctor`** no cambia nada. Revisa la config y su `$schema`, las dependencias y que los hooks estén en esta versión. Cada problema dice qué lo arregla: `npx agent-kitchen apply` si el repo fija otra issue-flow (también mueve las skills), y `npx @ramon-sg/issue-flow apply` si la versión es esta pero sus archivos quedaron atrás.
|
|
205
|
+
|
|
206
|
+
**`init` y `upgrade`** siguen funcionando hasta la 0.4.0, con un aviso: `init` es `apply`, y `upgrade` es `apply --yes` sin recetas, porque nunca preguntaba.
|
|
204
207
|
|
|
205
208
|
### Recetas
|
|
206
209
|
|
|
207
|
-
Piezas de `flow.json` para lo que muchos repos comparten. `
|
|
210
|
+
Piezas de `flow.json` para lo que muchos repos comparten. `apply` ofrece, la primera vez, las que sugieren los archivos del repo; `--recipe a,b` elige otras. Una receta solo agrega lo que falta: un paso con el mismo nombre o una clave que el repo ya tiene no se tocan.
|
|
208
211
|
|
|
209
212
|
| Receta | La sugiere | Qué agrega |
|
|
210
213
|
|---|---|---|
|
package/dist/cli-setup.d.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file The `
|
|
3
|
-
* options.
|
|
2
|
+
* @file The `apply` and `doctor` commands of the CLI, and `init` and `upgrade`, which run
|
|
3
|
+
* `apply` now and say so: their flags turned into options.
|
|
4
4
|
*/
|
|
5
5
|
import type { Flags } from "./cli.js";
|
|
6
6
|
import { type Output } from "./output.js";
|
|
7
7
|
/** The setup commands. */
|
|
8
8
|
export declare const SETUP_COMMANDS: string[];
|
|
9
9
|
/**
|
|
10
|
-
* Runs `issue-flow
|
|
10
|
+
* Runs `issue-flow apply` or `doctor`. `init` is `apply` with its flags; `upgrade` is
|
|
11
|
+
* `apply --yes` without recipes, as it never asked, so an `agent-kitchen` that still calls
|
|
12
|
+
* it keeps working.
|
|
11
13
|
* @param command - The command.
|
|
12
14
|
* @param flags - The flags, already checked to be the command's own.
|
|
13
15
|
* @param rest - The positionals after it.
|
package/dist/cli-setup.js
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
|
+
import { apply } from "./commands/apply.js";
|
|
1
2
|
import { doctor } from "./commands/doctor.js";
|
|
2
|
-
import {
|
|
3
|
-
import { upgrade } from "./commands/upgrade.js";
|
|
4
|
-
import { UsageError } from "./output.js";
|
|
3
|
+
import { UsageError, warn } from "./output.js";
|
|
5
4
|
/** The setup commands. */
|
|
6
|
-
export const SETUP_COMMANDS = ["
|
|
5
|
+
export const SETUP_COMMANDS = ["apply", "init", "upgrade", "doctor"];
|
|
7
6
|
/**
|
|
8
|
-
*
|
|
7
|
+
* The names given with `--recipe`.
|
|
8
|
+
* @param value - The flag.
|
|
9
|
+
* @returns The names, or null without the flag.
|
|
10
|
+
*/
|
|
11
|
+
function recipesOf(value) {
|
|
12
|
+
return value === undefined ? null : value.split(",").map((name) => name.trim()).filter((name) => name !== "");
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Runs `issue-flow apply` or `doctor`. `init` is `apply` with its flags; `upgrade` is
|
|
16
|
+
* `apply --yes` without recipes, as it never asked, so an `agent-kitchen` that still calls
|
|
17
|
+
* it keeps working.
|
|
9
18
|
* @param command - The command.
|
|
10
19
|
* @param flags - The flags, already checked to be the command's own.
|
|
11
20
|
* @param rest - The positionals after it.
|
|
@@ -19,8 +28,9 @@ export async function runSetupCommand(command, flags, rest, run) {
|
|
|
19
28
|
throw new UsageError(`${command} takes no arguments.`);
|
|
20
29
|
if (command === "doctor")
|
|
21
30
|
return doctor(run.cwd, flags.json ?? false, run.out);
|
|
31
|
+
if (command === "init" || command === "upgrade")
|
|
32
|
+
warn(run.out, `${command} is now apply: run 'issue-flow apply'. ${command} goes away in 0.4.0.`);
|
|
22
33
|
if (command === "upgrade")
|
|
23
|
-
return
|
|
24
|
-
|
|
25
|
-
return init({ cwd: run.cwd, yes: flags.yes ?? false, recipes }, run.out);
|
|
34
|
+
return apply({ cwd: run.cwd, yes: true, dryRun: flags["dry-run"] ?? false, json: false, recipes: [] }, run.out);
|
|
35
|
+
return apply({ cwd: run.cwd, yes: flags.yes ?? false, dryRun: flags["dry-run"] ?? false, json: flags.json ?? false, recipes: recipesOf(flags.recipe) }, run.out);
|
|
26
36
|
}
|
package/dist/cli.js
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Output } from "../output.js";
|
|
2
|
+
/** Options of `issue-flow apply`. */
|
|
3
|
+
export interface ApplyOptions {
|
|
4
|
+
cwd: string;
|
|
5
|
+
/** Takes the recipes the repo's files suggest without asking. */
|
|
6
|
+
yes: boolean;
|
|
7
|
+
dryRun: boolean;
|
|
8
|
+
json: boolean;
|
|
9
|
+
/** The recipes named with `--recipe`, or null to pick them the first time. */
|
|
10
|
+
recipes: string[] | null;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Applies the flow to the repo; see the file's header.
|
|
14
|
+
* @param options - The options.
|
|
15
|
+
* @param out - Where to write and ask.
|
|
16
|
+
* @returns 0 when applied or already there, 1 when it had to ask and could not.
|
|
17
|
+
* @throws SetupError outside the main checkout, with a broken file, or when it refuses the
|
|
18
|
+
* version; ConfigError if the config would be invalid; UsageError for an unknown recipe.
|
|
19
|
+
* @example await apply({ cwd, yes: true, dryRun: false, json: false, recipes: null }, out)
|
|
20
|
+
*/
|
|
21
|
+
export declare function apply(options: ApplyOptions, out: Output): Promise<number>;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file `issue-flow apply [--yes] [--dry-run] [--json] [--recipe <name>,...]`: leaves the
|
|
3
|
+
* repo as the version that runs needs it, and running it again changes nothing. It writes
|
|
4
|
+
* `.agents/flow.json` the first time (from `paseo.json` if the repo has one, plus the recipes
|
|
5
|
+
* it picks), runs the config's migrations, pins `@ramon-sg/issue-flow` to this version, adds
|
|
6
|
+
* `github-rest` and the scripts if they are missing, and moves the hooks and the `$schema`
|
|
7
|
+
* to this version. It changes no other version: who updates picks the version by picking
|
|
8
|
+
* which one to run (`npx @ramon-sg/issue-flow@<version> apply`). It refuses when the repo
|
|
9
|
+
* already has a newer issue-flow, or a range, since the hooks need an exact one. The skills,
|
|
10
|
+
* `claude-cloud` and `claude-repo-guard` are `agent-kitchen`'s, which runs this command.
|
|
11
|
+
*/
|
|
12
|
+
import { confirm } from "../ask.js";
|
|
13
|
+
import { CONFIG_PATH, isObject } from "../config-base.js";
|
|
14
|
+
import { parseConfig } from "../config.js";
|
|
15
|
+
import { checkoutRoot, locateRepo } from "../git.js";
|
|
16
|
+
import { emitJson, UsageError, warn } from "../output.js";
|
|
17
|
+
import { dependencyVersion, isNewer, KEPT, withFlowPackages } from "../package-edits.js";
|
|
18
|
+
import { applyRecipe, detectedRecipes, loadRecipes, projectPrefix } from "../recipes.js";
|
|
19
|
+
import { hookVersions, registerHooks } from "../settings-hooks.js";
|
|
20
|
+
import { isStaleSchema, ownVersions, PACKAGE, packageManager, readJson, schemaUrl, SETTINGS_FILE, SetupError, writeJson } from "../setup-files.js";
|
|
21
|
+
/** An exact version, as the hooks need one (a leading `=` means the same). */
|
|
22
|
+
const EXACT = /^=?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
23
|
+
/**
|
|
24
|
+
* The config's migrations, oldest first. Each one sees the config as the earlier ones left
|
|
25
|
+
* it and returns the new config, or null when there is nothing to do, so running them twice
|
|
26
|
+
* changes nothing. A key the flow renames goes here.
|
|
27
|
+
*/
|
|
28
|
+
const MIGRATIONS = [];
|
|
29
|
+
/**
|
|
30
|
+
* Runs the migrations and moves the `$schema` to this version (a `$schema` that is not the
|
|
31
|
+
* flow's stays).
|
|
32
|
+
* @param raw - The config.
|
|
33
|
+
* @param version - This package's version.
|
|
34
|
+
* @returns The config and what changed.
|
|
35
|
+
*/
|
|
36
|
+
function migrate(raw, version) {
|
|
37
|
+
let next = raw;
|
|
38
|
+
const changes = [];
|
|
39
|
+
for (const migration of MIGRATIONS) {
|
|
40
|
+
const moved = migration.run(next);
|
|
41
|
+
if (moved === null)
|
|
42
|
+
continue;
|
|
43
|
+
next = moved;
|
|
44
|
+
changes.push(migration.name);
|
|
45
|
+
}
|
|
46
|
+
if (isStaleSchema(next.$schema, version)) {
|
|
47
|
+
next = { ...next, $schema: schemaUrl(version) };
|
|
48
|
+
changes.push(`$schema at v${version}`);
|
|
49
|
+
}
|
|
50
|
+
return { raw: next, changes };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A new config: the schema, and the branch rules of `paseo.json` if the repo has one.
|
|
54
|
+
* @param root - The repo.
|
|
55
|
+
* @param version - This package's version.
|
|
56
|
+
* @returns The config and what it took from `paseo.json`.
|
|
57
|
+
*/
|
|
58
|
+
function freshConfig(root, version) {
|
|
59
|
+
const raw = { $schema: schemaUrl(version) };
|
|
60
|
+
const issues = readJson(root, "paseo.json")?.project;
|
|
61
|
+
const paseo = isObject(issues) && isObject(issues.issues) ? issues.issues : null;
|
|
62
|
+
if (paseo === null)
|
|
63
|
+
return { raw, changes: ["new"] };
|
|
64
|
+
const branch = {};
|
|
65
|
+
if (typeof paseo.branchTemplate === "string")
|
|
66
|
+
branch.template = paseo.branchTemplate;
|
|
67
|
+
if (isObject(paseo.types))
|
|
68
|
+
branch.types = paseo.types;
|
|
69
|
+
if (Object.keys(branch).length > 0)
|
|
70
|
+
raw.branch = branch;
|
|
71
|
+
return { raw, changes: ["new, with the branch rules of paseo.json"] };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The recipes to apply: the ones named with `--recipe`; with no config yet, each one the
|
|
75
|
+
* repo's files suggest, after asking (all of them with `--yes`); otherwise none.
|
|
76
|
+
* @param root - The repo.
|
|
77
|
+
* @param options - The options.
|
|
78
|
+
* @param fresh - Whether the config is new.
|
|
79
|
+
* @param out - Where to ask.
|
|
80
|
+
* @returns The recipes, or null when it would have to ask and cannot.
|
|
81
|
+
* @throws UsageError if `--recipe` names one that does not exist.
|
|
82
|
+
*/
|
|
83
|
+
async function pickRecipes(root, options, fresh, out) {
|
|
84
|
+
const recipes = loadRecipes();
|
|
85
|
+
if (options.recipes !== null) {
|
|
86
|
+
const unknown = options.recipes.find((name) => !recipes.some((recipe) => recipe.name === name));
|
|
87
|
+
if (unknown !== undefined)
|
|
88
|
+
throw new UsageError(`unknown recipe ${unknown}; there are ${recipes.map((recipe) => recipe.name).join(", ")}.`);
|
|
89
|
+
return recipes.filter((recipe) => options.recipes?.includes(recipe.name));
|
|
90
|
+
}
|
|
91
|
+
if (!fresh)
|
|
92
|
+
return [];
|
|
93
|
+
const detected = detectedRecipes(root, recipes);
|
|
94
|
+
const suggested = recipes.filter((item) => detected.includes(item.name));
|
|
95
|
+
// NOTE: a dry run shows what --yes would write, so the first apply can be previewed.
|
|
96
|
+
if (options.yes || options.dryRun || suggested.length === 0)
|
|
97
|
+
return suggested;
|
|
98
|
+
if (!out.interactive)
|
|
99
|
+
return null;
|
|
100
|
+
const picked = [];
|
|
101
|
+
for (const recipe of suggested)
|
|
102
|
+
if (await confirm(out, `Recipe ${recipe.name}: ${recipe.description} Add it?`))
|
|
103
|
+
picked.push(recipe);
|
|
104
|
+
return picked;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Refuses to move the repo back, or to pin over a range: package.json must have an exact
|
|
108
|
+
* issue-flow, and neither it nor any hook may be newer than this one.
|
|
109
|
+
* @param root - The repo.
|
|
110
|
+
* @param versions - This package's versions.
|
|
111
|
+
* @throws SetupError if the repo has a newer issue-flow or one that is not exact.
|
|
112
|
+
*/
|
|
113
|
+
function checkVersion(root, versions) {
|
|
114
|
+
const pinned = dependencyVersion(readJson(root, "package.json"), PACKAGE);
|
|
115
|
+
if (pinned !== null && !EXACT.test(pinned))
|
|
116
|
+
throw new SetupError(`package.json has issue-flow ${pinned}, not an exact version: the hooks need one. Pin one (such as ${versions.issueFlow}) and run 'npx ${PACKAGE}@<that version> apply'.`);
|
|
117
|
+
const found = [
|
|
118
|
+
...(pinned === null ? [] : [{ where: "package.json", version: pinned.replace(/^=/, "") }]),
|
|
119
|
+
...Object.values(hookVersions(readJson(root, SETTINGS_FILE)))
|
|
120
|
+
.filter((version) => version !== null)
|
|
121
|
+
.map((version) => ({ where: `a hook of ${SETTINGS_FILE}`, version })),
|
|
122
|
+
];
|
|
123
|
+
const newer = found.find((item) => isNewer(item.version, versions.issueFlow));
|
|
124
|
+
if (newer !== undefined)
|
|
125
|
+
throw new SetupError(`${newer.where} has issue-flow ${newer.version}, newer than this ${versions.issueFlow}; run 'npx ${PACKAGE}@${newer.version} apply', or install first if package.json was bumped by hand.`);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* What `apply` would write: the config, `package.json` and the settings' hooks.
|
|
129
|
+
* @param root - The repo.
|
|
130
|
+
* @param versions - This package's versions.
|
|
131
|
+
* @param options - The options.
|
|
132
|
+
* @param out - Where to ask about the recipes.
|
|
133
|
+
* @returns The files, or null if it had to ask about the recipes and could not.
|
|
134
|
+
* @throws ConfigError if the resulting config is invalid; SetupError if a file is broken.
|
|
135
|
+
*/
|
|
136
|
+
async function plan(root, versions, options, out) {
|
|
137
|
+
const existing = readJson(root, CONFIG_PATH);
|
|
138
|
+
const recipes = await pickRecipes(root, options, existing === null, out);
|
|
139
|
+
if (recipes === null)
|
|
140
|
+
return null;
|
|
141
|
+
const start = existing === null ? freshConfig(root, versions.issueFlow) : migrate(existing, versions.issueFlow);
|
|
142
|
+
let { raw } = start;
|
|
143
|
+
const configChanges = [...start.changes];
|
|
144
|
+
for (const recipe of recipes) {
|
|
145
|
+
const applied = applyRecipe(raw, recipe, projectPrefix(root));
|
|
146
|
+
raw = applied.raw;
|
|
147
|
+
configChanges.push(...applied.changes.map((change) => (change.startsWith(KEPT) ? `${change} (${recipe.name})` : `${recipe.name}: ${change}`)));
|
|
148
|
+
}
|
|
149
|
+
parseConfig(raw);
|
|
150
|
+
const pkg = withFlowPackages(readJson(root, "package.json"), versions);
|
|
151
|
+
const hooks = registerHooks(readJson(root, SETTINGS_FILE), versions.issueFlow);
|
|
152
|
+
const file = (path, value, lines) => ({
|
|
153
|
+
path,
|
|
154
|
+
value,
|
|
155
|
+
changes: lines.filter((line) => !line.startsWith(KEPT)).map((line) => `${path}: ${line}`),
|
|
156
|
+
kept: lines.filter((line) => line.startsWith(KEPT)).map((line) => `${path}: ${line}`),
|
|
157
|
+
});
|
|
158
|
+
// NOTE: the settings go first: the sandbox of claude-repo-guard is likeliest to block them,
|
|
159
|
+
// and then nothing else has been written.
|
|
160
|
+
return [file(SETTINGS_FILE, hooks.settings, hooks.changes), file(CONFIG_PATH, raw, configChanges), file("package.json", pkg.pkg, pkg.changes)];
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Writes a file, saying what to do if the sandbox of `claude-repo-guard` blocks it.
|
|
164
|
+
* @param root - The repo.
|
|
165
|
+
* @param file - The file.
|
|
166
|
+
* @param done - The files already written, to say so if this one fails.
|
|
167
|
+
* @throws SetupError if it cannot be written for lack of permission.
|
|
168
|
+
*/
|
|
169
|
+
function write(root, file, done) {
|
|
170
|
+
try {
|
|
171
|
+
writeJson(root, file.path, file.value);
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
const code = error.code;
|
|
175
|
+
if (code !== "EPERM" && code !== "EACCES")
|
|
176
|
+
throw error;
|
|
177
|
+
const already = done.length === 0 ? "" : ` It already wrote ${done.join(" and ")}; running it again finishes the rest.`;
|
|
178
|
+
throw new SetupError(`could not write ${file.path} (${code}): inside a Claude session the sandbox blocks it; run apply from your terminal.${already}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Applies the flow to the repo; see the file's header.
|
|
183
|
+
* @param options - The options.
|
|
184
|
+
* @param out - Where to write and ask.
|
|
185
|
+
* @returns 0 when applied or already there, 1 when it had to ask and could not.
|
|
186
|
+
* @throws SetupError outside the main checkout, with a broken file, or when it refuses the
|
|
187
|
+
* version; ConfigError if the config would be invalid; UsageError for an unknown recipe.
|
|
188
|
+
* @example await apply({ cwd, yes: true, dryRun: false, json: false, recipes: null }, out)
|
|
189
|
+
*/
|
|
190
|
+
export async function apply(options, out) {
|
|
191
|
+
const located = locateRepo(options.cwd);
|
|
192
|
+
if (located === null)
|
|
193
|
+
throw new SetupError("run apply inside a checkout of the repo.");
|
|
194
|
+
if (checkoutRoot(options.cwd) !== located.mainRoot)
|
|
195
|
+
throw new SetupError(`run apply in the main checkout, ${located.mainRoot}, not in a worktree.`);
|
|
196
|
+
const root = located.mainRoot;
|
|
197
|
+
const versions = ownVersions();
|
|
198
|
+
checkVersion(root, versions);
|
|
199
|
+
const planned = await plan(root, versions, options, out);
|
|
200
|
+
if (planned === null) {
|
|
201
|
+
warn(out, "nothing written: the repo's files suggest recipes; pass --yes to take them, or --recipe to pick.");
|
|
202
|
+
if (options.json)
|
|
203
|
+
emitJson(out, { version: versions.issueFlow, changes: [], kept: [], written: false, next: ["pass --yes or --recipe"] });
|
|
204
|
+
return 1;
|
|
205
|
+
}
|
|
206
|
+
const files = planned.filter((file) => file.changes.length > 0);
|
|
207
|
+
const changes = files.flatMap((file) => file.changes);
|
|
208
|
+
const kept = planned.flatMap((file) => file.kept);
|
|
209
|
+
for (const line of kept)
|
|
210
|
+
warn(out, line);
|
|
211
|
+
const written = !options.dryRun && files.length > 0;
|
|
212
|
+
const done = [];
|
|
213
|
+
if (written)
|
|
214
|
+
for (const file of files) {
|
|
215
|
+
write(root, file, done);
|
|
216
|
+
done.push(file.path);
|
|
217
|
+
}
|
|
218
|
+
const pkgChanged = files.some((file) => file.path === "package.json");
|
|
219
|
+
const next = files.length === 0 ? [] : [...(pkgChanged ? [`${packageManager(root, readJson(root, "package.json"))} install`] : []), "review the diff and commit it"];
|
|
220
|
+
if (options.json) {
|
|
221
|
+
emitJson(out, { version: versions.issueFlow, changes, kept, written, next });
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
if (changes.length === 0)
|
|
225
|
+
out.stdout(`Already at issue-flow ${versions.issueFlow}.\n`);
|
|
226
|
+
else
|
|
227
|
+
out.stdout(`issue-flow ${versions.issueFlow}${options.dryRun ? " would write" : ""}:\n${changes.map((line) => ` ${line}\n`).join("")}`);
|
|
228
|
+
if (written)
|
|
229
|
+
out.stdout(`Next: ${pkgChanged ? `run '${next[0]}', ` : ""}review the diff and commit it.\n`);
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,60 +1,79 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file `issue-flow doctor [--json]`: whether the repo's flow files are as `
|
|
3
|
-
* them, at this version: the config
|
|
4
|
-
* each problem says what fixes it
|
|
2
|
+
* @file `issue-flow doctor [--json]`: whether the repo's flow files are as `apply` leaves
|
|
3
|
+
* them, at this version: the config and its `$schema`, the dev dependencies and the hooks.
|
|
4
|
+
* It changes nothing; each problem says what fixes it: `npx agent-kitchen apply` when the
|
|
5
|
+
* repo pins another issue-flow (it also moves the skills), and `issue-flow apply` when the
|
|
6
|
+
* version is this one but its files fell behind. `--json` prints the checks for `agent-kitchen`,
|
|
5
7
|
* which adds those of the skills and the other tools.
|
|
6
8
|
*/
|
|
7
|
-
import { ConfigError } from "../config-base.js";
|
|
9
|
+
import { CONFIG_PATH, ConfigError } from "../config-base.js";
|
|
8
10
|
import { loadConfig } from "../config.js";
|
|
9
11
|
import { locateRepo } from "../git.js";
|
|
10
|
-
import { dependencyVersion } from "../package-edits.js";
|
|
12
|
+
import { dependencyVersion, GITHUB_REST, reaches } from "../package-edits.js";
|
|
11
13
|
import { hookVersions } from "../settings-hooks.js";
|
|
12
|
-
import { ownVersions, PACKAGE, readJson, SETTINGS_FILE, SetupError } from "../setup-files.js";
|
|
13
|
-
/** What fixes
|
|
14
|
+
import { isStaleSchema, ownVersions, PACKAGE, readJson, SETTINGS_FILE, SetupError } from "../setup-files.js";
|
|
15
|
+
/** What fixes a repo at another version: it runs apply at the right one and moves the skills. */
|
|
14
16
|
const RUN_AGENT_KITCHEN = "run npx agent-kitchen apply";
|
|
17
|
+
/** What fixes the files of a repo already at this version. */
|
|
18
|
+
const RUN_APPLY = `run npx ${PACKAGE} apply`;
|
|
15
19
|
/**
|
|
16
|
-
*
|
|
20
|
+
* What fixes a problem: `issue-flow apply` if the repo pins this version, else agent-kitchen.
|
|
21
|
+
* @param pinned - The issue-flow the repo pins, or null.
|
|
22
|
+
* @param version - This package's version.
|
|
23
|
+
* @returns The hint.
|
|
24
|
+
*/
|
|
25
|
+
function fix(pinned, version) {
|
|
26
|
+
return pinned === version ? RUN_APPLY : RUN_AGENT_KITCHEN;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Checks the config, and that its `$schema` (if it is the flow's) is at this version.
|
|
17
30
|
* @param root - The repo.
|
|
31
|
+
* @param hint - What fixes a stale `$schema`.
|
|
32
|
+
* @param version - This package's version.
|
|
18
33
|
* @returns The check.
|
|
19
34
|
*/
|
|
20
|
-
function configCheck(root) {
|
|
35
|
+
function configCheck(root, hint, version) {
|
|
21
36
|
try {
|
|
22
37
|
loadConfig(root);
|
|
23
|
-
return { name: ".agents/flow.json", ok: true, detail: "valid" };
|
|
24
38
|
}
|
|
25
39
|
catch (error) {
|
|
26
40
|
if (!(error instanceof ConfigError))
|
|
27
41
|
throw error;
|
|
28
|
-
return { name:
|
|
42
|
+
return { name: CONFIG_PATH, ok: false, detail: error.message };
|
|
29
43
|
}
|
|
44
|
+
const stale = isStaleSchema(readJson(root, CONFIG_PATH)?.$schema, version);
|
|
45
|
+
return stale ? { name: CONFIG_PATH, ok: false, detail: `valid, but its $schema is not at v${version}: ${hint}` } : { name: CONFIG_PATH, ok: true, detail: "valid" };
|
|
30
46
|
}
|
|
31
47
|
/**
|
|
32
48
|
* Checks the dev dependencies: `issue-flow` at this version and `github-rest`, which the
|
|
33
|
-
* skills call.
|
|
49
|
+
* skills call, at the version the flow needs or newer.
|
|
34
50
|
* @param root - The repo.
|
|
35
|
-
* @param
|
|
51
|
+
* @param versions - This package's version and the lowest github-rest.
|
|
36
52
|
* @returns The checks.
|
|
37
53
|
*/
|
|
38
|
-
function packageChecks(root,
|
|
54
|
+
function packageChecks(root, versions) {
|
|
39
55
|
const pkg = readJson(root, "package.json");
|
|
40
56
|
const flow = dependencyVersion(pkg, PACKAGE);
|
|
41
|
-
const rest = dependencyVersion(pkg,
|
|
57
|
+
const rest = dependencyVersion(pkg, GITHUB_REST);
|
|
58
|
+
const version = versions.issueFlow;
|
|
59
|
+
const restDetail = rest === null ? `not in package.json, and the skills call it: ${fix(flow, version)}` : reaches(rest, versions.githubRest) ? rest : `${rest}, and the skills need ${versions.githubRest} or newer: raise it in package.json`;
|
|
42
60
|
return [
|
|
43
61
|
{ name: PACKAGE, ok: flow === version, detail: flow === null ? `not in package.json: ${RUN_AGENT_KITCHEN}` : flow === version ? version : `package.json has ${flow}, this is ${version}: ${RUN_AGENT_KITCHEN}` },
|
|
44
|
-
{ name:
|
|
62
|
+
{ name: GITHUB_REST, ok: rest !== null && reaches(rest, versions.githubRest), detail: restDetail },
|
|
45
63
|
];
|
|
46
64
|
}
|
|
47
65
|
/**
|
|
48
66
|
* Checks that each hook is there at this version.
|
|
49
67
|
* @param root - The repo.
|
|
50
68
|
* @param version - This package's version.
|
|
69
|
+
* @param hint - What fixes a missing or stale hook.
|
|
51
70
|
* @returns The checks.
|
|
52
71
|
*/
|
|
53
|
-
function hookChecks(root, version) {
|
|
72
|
+
function hookChecks(root, version, hint) {
|
|
54
73
|
return Object.entries(hookVersions(readJson(root, SETTINGS_FILE))).map(([event, found]) => ({
|
|
55
74
|
name: `${event} hook`,
|
|
56
75
|
ok: found === version,
|
|
57
|
-
detail: found === null ? `not in ${SETTINGS_FILE}: ${
|
|
76
|
+
detail: found === null ? `not in ${SETTINGS_FILE}: ${hint}` : found === version ? version : `at ${found}, this is ${version}: ${hint}`,
|
|
58
77
|
}));
|
|
59
78
|
}
|
|
60
79
|
/**
|
|
@@ -71,10 +90,11 @@ export function doctor(cwd, json, out) {
|
|
|
71
90
|
if (located === null)
|
|
72
91
|
throw new SetupError("run doctor inside a checkout of the repo.");
|
|
73
92
|
const root = located.mainRoot;
|
|
74
|
-
const
|
|
93
|
+
const versions = ownVersions();
|
|
75
94
|
let checks;
|
|
76
95
|
try {
|
|
77
|
-
|
|
96
|
+
const hint = fix(dependencyVersion(readJson(root, "package.json"), PACKAGE), versions.issueFlow);
|
|
97
|
+
checks = [configCheck(root, hint, versions.issueFlow), ...packageChecks(root, versions), ...hookChecks(root, versions.issueFlow, hint)];
|
|
78
98
|
}
|
|
79
99
|
catch (error) {
|
|
80
100
|
if (!(error instanceof SetupError))
|
|
@@ -13,7 +13,7 @@ import { existsSync } from "node:fs";
|
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { getPr } from "@ramon-sg/github-rest";
|
|
15
15
|
import { canonicalPath } from "../docker.js";
|
|
16
|
-
import { checkedOutAt, git, hasLocal, locateRepo, worktrees } from "../git.js";
|
|
16
|
+
import { checkedOutAt, git, hasLocal, locateRepo, trackOrigin, untracked, worktrees } from "../git.js";
|
|
17
17
|
import { permissionArgs } from "../launch.js";
|
|
18
18
|
import { warn } from "../output.js";
|
|
19
19
|
import { copyWorktreeIncludes } from "../start-place.js";
|
|
@@ -45,7 +45,7 @@ function mustGit(cwd, args, message) {
|
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
47
|
* Makes the worktree on the PR's branch: the local branch if there is one (moved forward to
|
|
48
|
-
* origin's when it can be), or a new one tracking origin's.
|
|
48
|
+
* origin's when it can be), or a new one; either way tracking origin's when it can.
|
|
49
49
|
* @param mainRoot - The main checkout.
|
|
50
50
|
* @param branch - The PR's branch.
|
|
51
51
|
* @param path - The worktree.
|
|
@@ -57,14 +57,14 @@ function addWorktree(mainRoot, branch, path, out) {
|
|
|
57
57
|
const elsewhere = checkedOutAt(mainRoot, branch);
|
|
58
58
|
if (elsewhere !== null)
|
|
59
59
|
throw new StartError(`${branch} is checked out in ${elsewhere}; review it there, or switch that checkout to another branch.`);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
mustGit(mainRoot, ["worktree", "add", "-q",
|
|
65
|
-
if (!
|
|
66
|
-
warn(out,
|
|
67
|
-
if (!git(path, ["merge", "-q", "--ff-only", `origin/${branch}`]).ok)
|
|
60
|
+
// NOTE: the tracking goes apart, after the worktree: it writes .git/config, which Claude
|
|
61
|
+
// Code's sandbox forbids, and the worktree is still useful without it.
|
|
62
|
+
const local = hasLocal(mainRoot, branch);
|
|
63
|
+
const args = local ? [path, branch] : ["--no-track", "-b", branch, path, `origin/${branch}`];
|
|
64
|
+
mustGit(mainRoot, ["worktree", "add", "-q", ...args], `could not make the worktree of ${branch}`);
|
|
65
|
+
if (!trackOrigin(path, branch))
|
|
66
|
+
warn(out, untracked(branch));
|
|
67
|
+
if (local && !git(path, ["merge", "-q", "--ff-only", `origin/${branch}`]).ok)
|
|
68
68
|
warn(out, `the local ${branch} does not move forward to origin/${branch}: you see other commits than the PR's.`);
|
|
69
69
|
}
|
|
70
70
|
/**
|
package/dist/git.d.ts
CHANGED
|
@@ -28,6 +28,22 @@ export declare function gitLines(cwd: string, args: string[]): string[];
|
|
|
28
28
|
* @example hasRef("/repo", "refs/heads/main")
|
|
29
29
|
*/
|
|
30
30
|
export declare function hasRef(cwd: string, ref: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Makes a branch track origin's of the same name, unless it already does. The upstream lives
|
|
33
|
+
* in `.git/config`, which Claude Code's sandbox forbids writing, so this is the only step that
|
|
34
|
+
* writes it: branches are made with `--no-track` and pushed without `-u`.
|
|
35
|
+
* @param cwd - The checkout.
|
|
36
|
+
* @param branch - The branch.
|
|
37
|
+
* @returns True if it tracks `origin/<branch>` now; false if git could not write it.
|
|
38
|
+
* @example trackOrigin("/repo", "feat/42-x")
|
|
39
|
+
*/
|
|
40
|
+
export declare function trackOrigin(cwd: string, branch: string): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* The warning when a branch cannot track origin's.
|
|
43
|
+
* @param branch - The branch.
|
|
44
|
+
* @returns The warning.
|
|
45
|
+
*/
|
|
46
|
+
export declare function untracked(branch: string): string;
|
|
31
47
|
/**
|
|
32
48
|
* Whether the branch exists locally.
|
|
33
49
|
* @param cwd - The checkout.
|
package/dist/git.js
CHANGED
|
@@ -41,6 +41,29 @@ export function gitLines(cwd, args) {
|
|
|
41
41
|
export function hasRef(cwd, ref) {
|
|
42
42
|
return git(cwd, ["rev-parse", "-q", "--verify", ref]).ok;
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Makes a branch track origin's of the same name, unless it already does. The upstream lives
|
|
46
|
+
* in `.git/config`, which Claude Code's sandbox forbids writing, so this is the only step that
|
|
47
|
+
* writes it: branches are made with `--no-track` and pushed without `-u`.
|
|
48
|
+
* @param cwd - The checkout.
|
|
49
|
+
* @param branch - The branch.
|
|
50
|
+
* @returns True if it tracks `origin/<branch>` now; false if git could not write it.
|
|
51
|
+
* @example trackOrigin("/repo", "feat/42-x")
|
|
52
|
+
*/
|
|
53
|
+
export function trackOrigin(cwd, branch) {
|
|
54
|
+
const upstream = git(cwd, ["rev-parse", "-q", "--symbolic-full-name", `${branch}@{u}`]);
|
|
55
|
+
if (upstream.ok && upstream.out === `refs/remotes/origin/${branch}`)
|
|
56
|
+
return true;
|
|
57
|
+
return git(cwd, ["branch", "-q", `--set-upstream-to=origin/${branch}`, branch]).ok;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The warning when a branch cannot track origin's.
|
|
61
|
+
* @param branch - The branch.
|
|
62
|
+
* @returns The warning.
|
|
63
|
+
*/
|
|
64
|
+
export function untracked(branch) {
|
|
65
|
+
return `could not make ${branch} track origin/${branch} (is .git/config read-only, as in a sandbox?); push with 'git push origin HEAD'.`;
|
|
66
|
+
}
|
|
44
67
|
/**
|
|
45
68
|
* Whether the branch exists locally.
|
|
46
69
|
* @param cwd - The checkout.
|