@ramon-sg/issue-flow 0.2.6 → 0.3.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.
- package/README.md +17 -14
- 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 +41 -21
- package/dist/package-edits.d.ts +35 -16
- package/dist/package-edits.js +120 -31
- 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/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 -136
- package/dist/commands/upgrade.d.ts +0 -16
- package/dist/commands/upgrade.js +0 -85
package/README.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Flujo de trabajo de issue a PR para cualquier repo, en local y en las sesiones de Claude en la nube: nombra la rama de cada issue, crea su worktree con puertos y Docker propios, abre el PR en draft y limpia cuando se mergea. Trae las skills que usan ese flujo.
|
|
4
4
|
|
|
5
|
-
> **Instalar en un repo:** `npx agent-kitchen`, que instala este flujo con sus skills, las skills generales, `claude-cloud` y `claude-repo-guard`, y los actualiza ([agent-kitchen](https://github.com/ramon-sg/agent-kitchen)). El paquete es `@ramon-sg/issue-flow` y el comando, `issue-flow`; sin el scope, `issue-flow` en npm es otro paquete.
|
|
5
|
+
> **Instalar en un repo:** `npx agent-kitchen apply`, que instala este flujo con sus skills, las skills generales, `claude-cloud` y `claude-repo-guard`, y los actualiza ([agent-kitchen](https://github.com/ramon-sg/agent-kitchen)). El paquete es `@ramon-sg/issue-flow` y el comando, `issue-flow`; sin el scope, `issue-flow` en npm es otro paquete.
|
|
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` 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
|
-
const RUN_AGENT_KITCHEN = "run npx agent-kitchen";
|
|
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. */
|
|
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))
|
package/dist/package-edits.d.ts
CHANGED
|
@@ -1,17 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file The repo's `package.json` as the flow needs it: `issue-flow
|
|
3
|
-
* `
|
|
4
|
-
* uses the repo's copy), and the scripts to start an issue, prune worktrees and review
|
|
5
|
-
*
|
|
2
|
+
* @file The repo's `package.json` as the flow needs it: `issue-flow` at the version that
|
|
3
|
+
* runs and `github-rest` as exact dev dependencies (the skills call them with `npx`, which
|
|
4
|
+
* then uses the repo's copy), and the scripts to start an issue, prune worktrees and review
|
|
5
|
+
* a PR. `apply` adds `github-rest` only if it is missing: its version is the repo's to pick,
|
|
6
|
+
* and `doctor` says when it falls short of what the flow needs. A script the repo already
|
|
7
|
+
* has with another value stays as it is.
|
|
6
8
|
*/
|
|
7
|
-
import type
|
|
9
|
+
import { type OwnVersions } from "./setup-files.js";
|
|
8
10
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* @
|
|
12
|
-
* @
|
|
11
|
+
* Whether a version is newer than another; false if either is not a version. A leading
|
|
12
|
+
* range sign is dropped, so `^0.3.0` reads as 0.3.0.
|
|
13
|
+
* @param version - The version, maybe with a range sign.
|
|
14
|
+
* @param than - The other.
|
|
15
|
+
* @returns True if it is newer.
|
|
16
|
+
* @example isNewer("1.10.0", "1.9.0") // true
|
|
17
|
+
*/
|
|
18
|
+
export declare function isNewer(version: string, than: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Whether the repo's version of a package reaches the one the flow needs, so `apply` and
|
|
21
|
+
* `doctor` accept it: an exact version as new or newer, a range that allows it or newer
|
|
22
|
+
* (`^1.2.0` for 1.3.0, not `^0.1.0` for 0.3.0), or a source it cannot read (`workspace:*`,
|
|
23
|
+
* `github:…`, `latest`), which the repo chose on purpose.
|
|
24
|
+
* @param current - The repo's version or range.
|
|
25
|
+
* @param tested - The version the flow needs.
|
|
26
|
+
* @returns True if it reaches it.
|
|
27
|
+
* @example reaches("1.3.0", "1.2.0") // true
|
|
13
28
|
*/
|
|
14
|
-
export declare function
|
|
29
|
+
export declare function reaches(current: string, tested: string): boolean;
|
|
30
|
+
/** How a change that keeps what the repo had starts. */
|
|
31
|
+
export declare const KEPT = "kept ";
|
|
32
|
+
/** The package the skills call, which the repo gets if it lacks it. */
|
|
33
|
+
export declare const GITHUB_REST = "@ramon-sg/github-rest";
|
|
15
34
|
/**
|
|
16
35
|
* The version of a package in the repo, as a dependency or a dev dependency.
|
|
17
36
|
* @param pkg - The package.json.
|
|
@@ -21,15 +40,15 @@ export declare function pinnedPackages(versions: OwnVersions): Record<string, st
|
|
|
21
40
|
*/
|
|
22
41
|
export declare function dependencyVersion(pkg: Record<string, unknown> | null, name: string): string | null;
|
|
23
42
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
43
|
+
* Pins `issue-flow` to the version that runs, adds `github-rest` if it is missing and the
|
|
44
|
+
* scripts that are missing. Nothing else changes: a `github-rest` the repo has stays, with a
|
|
45
|
+
* note if it falls short of the version the flow needs.
|
|
26
46
|
* @param pkg - The package.json, or null if there is none.
|
|
27
|
-
* @param versions -
|
|
28
|
-
* @param move - Whether to move the dependencies already there.
|
|
47
|
+
* @param versions - This package's versions.
|
|
29
48
|
* @returns The new package.json and what changed or was kept.
|
|
30
|
-
* @example withFlowPackages(null, versions
|
|
49
|
+
* @example withFlowPackages(null, versions).changes // ["devDependencies: @ramon-sg/issue-flow 0.1.0", …]
|
|
31
50
|
*/
|
|
32
|
-
export declare function withFlowPackages(pkg: Record<string, unknown> | null, versions: OwnVersions
|
|
51
|
+
export declare function withFlowPackages(pkg: Record<string, unknown> | null, versions: OwnVersions): {
|
|
33
52
|
pkg: Record<string, unknown>;
|
|
34
53
|
changes: string[];
|
|
35
54
|
};
|
package/dist/package-edits.js
CHANGED
|
@@ -1,22 +1,111 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* @file The repo's `package.json` as the flow needs it: `issue-flow` at the version that
|
|
3
|
+
* runs and `github-rest` as exact dev dependencies (the skills call them with `npx`, which
|
|
4
|
+
* then uses the repo's copy), and the scripts to start an issue, prune worktrees and review
|
|
5
|
+
* a PR. `apply` adds `github-rest` only if it is missing: its version is the repo's to pick,
|
|
6
|
+
* and `doctor` says when it falls short of what the flow needs. A script the repo already
|
|
7
|
+
* has with another value stays as it is.
|
|
8
|
+
*/
|
|
9
|
+
import { PACKAGE } from "./setup-files.js";
|
|
10
|
+
/** A version: X.Y.Z, with an optional pre-release and build. */
|
|
11
|
+
const VERSION = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
12
|
+
/** A range this file understands: a sign (`^`, `~`, `>=`, `=`) before a version. */
|
|
13
|
+
const SIMPLE_RANGE = /^(\^|~|>=|=)\s*(\S+)$/;
|
|
14
|
+
/**
|
|
15
|
+
* Parses a version.
|
|
16
|
+
* @param text - The version, without a range sign.
|
|
17
|
+
* @returns It, or null if it is not one.
|
|
18
|
+
*/
|
|
19
|
+
function parse(text) {
|
|
20
|
+
const match = VERSION.exec(text);
|
|
21
|
+
if (match === null)
|
|
22
|
+
return null;
|
|
23
|
+
return { parts: [Number(match[1]), Number(match[2]), Number(match[3])], pre: match[4] ?? null };
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Compares two versions, semver-style: by the numbers, then a pre-release before its release.
|
|
27
|
+
* @param left - One.
|
|
28
|
+
* @param right - The other.
|
|
29
|
+
* @returns Negative if left is older, 0 if equal, positive if newer.
|
|
30
|
+
*/
|
|
31
|
+
function compare(left, right) {
|
|
32
|
+
for (let index = 0; index < 3; index += 1) {
|
|
33
|
+
const diff = left.parts[index] - right.parts[index];
|
|
34
|
+
if (diff !== 0)
|
|
35
|
+
return diff;
|
|
36
|
+
}
|
|
37
|
+
if (left.pre === right.pre)
|
|
38
|
+
return 0;
|
|
39
|
+
if (left.pre === null)
|
|
40
|
+
return 1;
|
|
41
|
+
if (right.pre === null)
|
|
42
|
+
return -1;
|
|
43
|
+
return left.pre.localeCompare(right.pre, "en", { numeric: true });
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Whether a version is newer than another; false if either is not a version. A leading
|
|
47
|
+
* range sign is dropped, so `^0.3.0` reads as 0.3.0.
|
|
48
|
+
* @param version - The version, maybe with a range sign.
|
|
49
|
+
* @param than - The other.
|
|
50
|
+
* @returns True if it is newer.
|
|
51
|
+
* @example isNewer("1.10.0", "1.9.0") // true
|
|
52
|
+
*/
|
|
53
|
+
export function isNewer(version, than) {
|
|
54
|
+
const [left, right] = [parse(version.replace(/^[\^~=]/, "")), parse(than.replace(/^[\^~=]/, ""))];
|
|
55
|
+
return left !== null && right !== null && compare(left, right) > 0;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The first version a simple range leaves out, or null if it has no ceiling (`>=`).
|
|
59
|
+
* @param sign - `^`, `~` or `>=`.
|
|
60
|
+
* @param floor - Its version.
|
|
61
|
+
* @returns The ceiling.
|
|
62
|
+
*/
|
|
63
|
+
function ceiling(sign, floor) {
|
|
64
|
+
const [major, minor, patch] = floor.parts;
|
|
65
|
+
if (sign === ">=")
|
|
66
|
+
return null;
|
|
67
|
+
if (sign === "~")
|
|
68
|
+
return { parts: [major, minor + 1, 0], pre: null };
|
|
69
|
+
if (major > 0)
|
|
70
|
+
return { parts: [major + 1, 0, 0], pre: null };
|
|
71
|
+
return minor > 0 ? { parts: [0, minor + 1, 0], pre: null } : { parts: [0, 0, patch + 1], pre: null };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Whether the repo's version of a package reaches the one the flow needs, so `apply` and
|
|
75
|
+
* `doctor` accept it: an exact version as new or newer, a range that allows it or newer
|
|
76
|
+
* (`^1.2.0` for 1.3.0, not `^0.1.0` for 0.3.0), or a source it cannot read (`workspace:*`,
|
|
77
|
+
* `github:…`, `latest`), which the repo chose on purpose.
|
|
78
|
+
* @param current - The repo's version or range.
|
|
79
|
+
* @param tested - The version the flow needs.
|
|
80
|
+
* @returns True if it reaches it.
|
|
81
|
+
* @example reaches("1.3.0", "1.2.0") // true
|
|
82
|
+
*/
|
|
83
|
+
export function reaches(current, tested) {
|
|
84
|
+
const wanted = parse(tested);
|
|
85
|
+
if (wanted === null)
|
|
86
|
+
return true;
|
|
87
|
+
const exact = parse(current);
|
|
88
|
+
if (exact !== null)
|
|
89
|
+
return compare(exact, wanted) >= 0;
|
|
90
|
+
const range = SIMPLE_RANGE.exec(current.trim());
|
|
91
|
+
const floor = range === null ? null : parse(range[2]);
|
|
92
|
+
if (range === null || floor === null)
|
|
93
|
+
return true;
|
|
94
|
+
if (range[1] === "=")
|
|
95
|
+
return compare(floor, wanted) >= 0;
|
|
96
|
+
const top = ceiling(range[1], floor);
|
|
97
|
+
return top === null || compare(wanted, top) < 0;
|
|
98
|
+
}
|
|
99
|
+
/** How a change that keeps what the repo had starts. */
|
|
100
|
+
export const KEPT = "kept ";
|
|
101
|
+
/** The package the skills call, which the repo gets if it lacks it. */
|
|
102
|
+
export const GITHUB_REST = "@ramon-sg/github-rest";
|
|
103
|
+
/** The scripts `apply` adds. */
|
|
2
104
|
const FLOW_SCRIPTS = {
|
|
3
105
|
"issue:start": "issue-flow issue start",
|
|
4
106
|
"worktree:prune": "issue-flow worktree prune",
|
|
5
107
|
"pr:worktree": "issue-flow pr worktree",
|
|
6
108
|
};
|
|
7
|
-
/**
|
|
8
|
-
* The dependencies the flow pins, with their versions.
|
|
9
|
-
* @param versions - This package's versions.
|
|
10
|
-
* @returns Package to version.
|
|
11
|
-
* @example pinnedPackages(versions)["@ramon-sg/issue-flow"] // "0.1.0"
|
|
12
|
-
*/
|
|
13
|
-
export function pinnedPackages(versions) {
|
|
14
|
-
return {
|
|
15
|
-
"@ramon-sg/issue-flow": versions.issueFlow,
|
|
16
|
-
"@ramon-sg/github-rest": versions.githubRest,
|
|
17
|
-
"@ramon-sg/skills-manager": versions.skillsManager,
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
109
|
/**
|
|
21
110
|
* Where a package is in the repo's package.json, and its version.
|
|
22
111
|
* @param pkg - The package.json.
|
|
@@ -43,30 +132,30 @@ export function dependencyVersion(pkg, name) {
|
|
|
43
132
|
return dependencyOf(pkg, name)?.version ?? null;
|
|
44
133
|
}
|
|
45
134
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
135
|
+
* Pins `issue-flow` to the version that runs, adds `github-rest` if it is missing and the
|
|
136
|
+
* scripts that are missing. Nothing else changes: a `github-rest` the repo has stays, with a
|
|
137
|
+
* note if it falls short of the version the flow needs.
|
|
48
138
|
* @param pkg - The package.json, or null if there is none.
|
|
49
|
-
* @param versions -
|
|
50
|
-
* @param move - Whether to move the dependencies already there.
|
|
139
|
+
* @param versions - This package's versions.
|
|
51
140
|
* @returns The new package.json and what changed or was kept.
|
|
52
|
-
* @example withFlowPackages(null, versions
|
|
141
|
+
* @example withFlowPackages(null, versions).changes // ["devDependencies: @ramon-sg/issue-flow 0.1.0", …]
|
|
53
142
|
*/
|
|
54
|
-
export function withFlowPackages(pkg, versions
|
|
143
|
+
export function withFlowPackages(pkg, versions) {
|
|
55
144
|
const next = pkg === null ? { private: true } : { ...pkg };
|
|
56
145
|
const scripts = { ...(next.scripts ?? {}) };
|
|
57
146
|
const changes = [];
|
|
58
|
-
|
|
59
|
-
const current = dependencyOf(next, name);
|
|
60
|
-
if (current?.version === version)
|
|
61
|
-
continue;
|
|
62
|
-
if (current !== null && !move) {
|
|
63
|
-
changes.push(`kept ${name} ${current.version} (issue-flow ${versions.issueFlow} was tested with ${version})`);
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
const field = current?.field ?? "devDependencies";
|
|
147
|
+
const pin = (name, version, field, from) => {
|
|
67
148
|
next[field] = { ...(next[field] ?? {}), [name]: version };
|
|
68
|
-
changes.push(`${field}: ${name} ${
|
|
69
|
-
}
|
|
149
|
+
changes.push(`${field}: ${name} ${from === null ? "" : `${from} → `}${version}`);
|
|
150
|
+
};
|
|
151
|
+
const flow = dependencyOf(next, PACKAGE);
|
|
152
|
+
if (flow?.version !== versions.issueFlow)
|
|
153
|
+
pin(PACKAGE, versions.issueFlow, flow?.field ?? "devDependencies", flow?.version ?? null);
|
|
154
|
+
const rest = dependencyOf(next, GITHUB_REST);
|
|
155
|
+
if (rest === null)
|
|
156
|
+
pin(GITHUB_REST, versions.githubRest, "devDependencies", null);
|
|
157
|
+
else if (!reaches(rest.version, versions.githubRest))
|
|
158
|
+
changes.push(`${KEPT}${GITHUB_REST} ${rest.version} (the skills need ${versions.githubRest} or newer: raise it)`);
|
|
70
159
|
for (const [name, command] of Object.entries(FLOW_SCRIPTS)) {
|
|
71
160
|
if (scripts[name] !== undefined)
|
|
72
161
|
continue;
|
package/dist/settings-hooks.d.ts
CHANGED
package/dist/settings-hooks.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file The Claude Code hooks `
|
|
2
|
+
* @file The Claude Code hooks `apply` registers in `.claude/settings.json`: `SessionStart`
|
|
3
3
|
* (`issue-flow session`) and `WorktreeCreate` and `WorktreeRemove` (`issue-flow hook …`), each
|
|
4
4
|
* pinned to this package's version. They are found again by their command, so running
|
|
5
|
-
* `
|
|
5
|
+
* `apply` twice changes nothing, and a new version only moves the version. The `SessionStart`
|
|
6
6
|
* command names `claude-cloud session`, which `issue-flow session` runs in the cloud: that
|
|
7
7
|
* way `claude-cloud init` sees it and does not add its own, and a hook of `claude-cloud`
|
|
8
8
|
* already there is dropped, so it does not run twice.
|
|
@@ -19,7 +19,7 @@ const HOOK_COMMANDS = {
|
|
|
19
19
|
WorktreeCreate: { sub: "hook worktree-create", note: "" },
|
|
20
20
|
WorktreeRemove: { sub: "hook worktree-remove", note: "" },
|
|
21
21
|
};
|
|
22
|
-
/** The hook events `
|
|
22
|
+
/** The hook events `apply` registers. */
|
|
23
23
|
export const HOOK_EVENTS = Object.keys(HOOK_COMMANDS);
|
|
24
24
|
/**
|
|
25
25
|
* The command of one of the hooks, at a version.
|
package/dist/setup-files.d.ts
CHANGED
|
@@ -2,20 +2,37 @@
|
|
|
2
2
|
export declare const PACKAGE = "@ramon-sg/issue-flow";
|
|
3
3
|
/** The GitHub repo of this package, whose tags hold the config's schema of each version. */
|
|
4
4
|
export declare const FLOW_REPO = "ramon-sg/issue-flow";
|
|
5
|
+
/** The start of the config's schema URL, at any version. */
|
|
6
|
+
export declare const SCHEMA_PREFIX = "https://raw.githubusercontent.com/ramon-sg/issue-flow/";
|
|
7
|
+
/**
|
|
8
|
+
* The URL of the config's schema at a version.
|
|
9
|
+
* @param version - This package's version.
|
|
10
|
+
* @returns The URL.
|
|
11
|
+
* @example schemaUrl("0.1.0") // "https://raw.githubusercontent.com/ramon-sg/issue-flow/v0.1.0/schema.json"
|
|
12
|
+
*/
|
|
13
|
+
export declare function schemaUrl(version: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Whether a config's `$schema` is the flow's at another version (one that is not the
|
|
16
|
+
* flow's is the repo's choice and never stale).
|
|
17
|
+
* @param schema - The config's `$schema`.
|
|
18
|
+
* @param version - This package's version.
|
|
19
|
+
* @returns True if `apply` would move it.
|
|
20
|
+
*/
|
|
21
|
+
export declare function isStaleSchema(schema: unknown, version: string): boolean;
|
|
5
22
|
/** Claude Code's settings of the repo. */
|
|
6
23
|
export declare const SETTINGS_FILE = ".claude/settings.json";
|
|
7
|
-
/** Error of `
|
|
24
|
+
/** Error of `apply` or `doctor`: exits 1 with the message. */
|
|
8
25
|
export declare class SetupError extends Error {
|
|
9
26
|
}
|
|
10
27
|
/** The versions this package pins in the repo. */
|
|
11
28
|
export interface OwnVersions {
|
|
12
29
|
issueFlow: string;
|
|
30
|
+
/** The lowest `github-rest` the flow works with: the floor of its own dependency. */
|
|
13
31
|
githubRest: string;
|
|
14
|
-
skillsManager: string;
|
|
15
32
|
}
|
|
16
33
|
/**
|
|
17
|
-
* This package's version and the
|
|
18
|
-
*
|
|
34
|
+
* This package's version and the lowest `github-rest` it works with, from its own
|
|
35
|
+
* `package.json` (`"^0.3.0"` gives 0.3.0).
|
|
19
36
|
* @returns The versions.
|
|
20
37
|
* @throws SetupError if the package.json does not have them.
|
|
21
38
|
* @example ownVersions().issueFlow // "0.1.0"
|
package/dist/setup-files.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file What `
|
|
2
|
+
* @file What `apply` and `doctor` share: this package's own versions, the JSON
|
|
3
3
|
* files of the repo they read and write, and its package manager. Every package goes by its
|
|
4
4
|
* scoped name: the unscoped `issue-flow`, `github-rest` and `skills-manager` on npm belong
|
|
5
5
|
* to other people.
|
|
@@ -12,14 +12,35 @@ import { isObject } from "./config-base.js";
|
|
|
12
12
|
export const PACKAGE = "@ramon-sg/issue-flow";
|
|
13
13
|
/** The GitHub repo of this package, whose tags hold the config's schema of each version. */
|
|
14
14
|
export const FLOW_REPO = "ramon-sg/issue-flow";
|
|
15
|
+
/** The start of the config's schema URL, at any version. */
|
|
16
|
+
export const SCHEMA_PREFIX = `https://raw.githubusercontent.com/${FLOW_REPO}/`;
|
|
17
|
+
/**
|
|
18
|
+
* The URL of the config's schema at a version.
|
|
19
|
+
* @param version - This package's version.
|
|
20
|
+
* @returns The URL.
|
|
21
|
+
* @example schemaUrl("0.1.0") // "https://raw.githubusercontent.com/ramon-sg/issue-flow/v0.1.0/schema.json"
|
|
22
|
+
*/
|
|
23
|
+
export function schemaUrl(version) {
|
|
24
|
+
return `${SCHEMA_PREFIX}v${version}/schema.json`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Whether a config's `$schema` is the flow's at another version (one that is not the
|
|
28
|
+
* flow's is the repo's choice and never stale).
|
|
29
|
+
* @param schema - The config's `$schema`.
|
|
30
|
+
* @param version - This package's version.
|
|
31
|
+
* @returns True if `apply` would move it.
|
|
32
|
+
*/
|
|
33
|
+
export function isStaleSchema(schema, version) {
|
|
34
|
+
return typeof schema === "string" && schema.startsWith(SCHEMA_PREFIX) && schema !== schemaUrl(version);
|
|
35
|
+
}
|
|
15
36
|
/** Claude Code's settings of the repo. */
|
|
16
37
|
export const SETTINGS_FILE = ".claude/settings.json";
|
|
17
|
-
/** Error of `
|
|
38
|
+
/** Error of `apply` or `doctor`: exits 1 with the message. */
|
|
18
39
|
export class SetupError extends Error {
|
|
19
40
|
}
|
|
20
41
|
/**
|
|
21
|
-
* This package's version and the
|
|
22
|
-
*
|
|
42
|
+
* This package's version and the lowest `github-rest` it works with, from its own
|
|
43
|
+
* `package.json` (`"^0.3.0"` gives 0.3.0).
|
|
23
44
|
* @returns The versions.
|
|
24
45
|
* @throws SetupError if the package.json does not have them.
|
|
25
46
|
* @example ownVersions().issueFlow // "0.1.0"
|
|
@@ -27,11 +48,10 @@ export class SetupError extends Error {
|
|
|
27
48
|
export function ownVersions() {
|
|
28
49
|
const file = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
29
50
|
const pkg = JSON.parse(readFileSync(file, "utf8"));
|
|
30
|
-
const githubRest = pkg.dependencies?.["@ramon-sg/github-rest"];
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return { issueFlow: pkg.version, githubRest, skillsManager };
|
|
51
|
+
const githubRest = pkg.dependencies?.["@ramon-sg/github-rest"]?.replace(/^[\^~]/, "");
|
|
52
|
+
if (pkg.version === undefined || githubRest === undefined)
|
|
53
|
+
throw new SetupError(`${file} lacks the version of issue-flow or github-rest.`);
|
|
54
|
+
return { issueFlow: pkg.version, githubRest };
|
|
35
55
|
}
|
|
36
56
|
/**
|
|
37
57
|
* Reads a JSON object of the repo.
|
package/dist/usage.d.ts
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* @file The usage text of the CLI, printed with `--help` and after a usage error.
|
|
3
3
|
*/
|
|
4
4
|
/** Usage text. */
|
|
5
|
-
export declare const USAGE = "Usage:\n issue-flow issue branch <n>... [--namer claude|title] [--json] [-R owner/repo]\n issue-flow issue branch --missing [--limit <n>] [--namer claude|title] [--dry-run | --yes] [--json] [-R owner/repo]\n issue-flow issue start <n> [--branch] [--dry-run | --yes] [--resume <branch> | --new] [--no-launch]\n [--ask-permissions | --free] [--namer claude|title] [-R owner/repo]\n issue-flow issue start <n> --plan [--ask-permissions | --free] [--namer claude|title] [-R owner/repo]\n issue-flow issue start <n> --cloud | --here | --teleport | --adopt [...]\n issue-flow worktree setup [--only <step>]\n issue-flow worktree teardown [--path <dir>]\n issue-flow worktree ports [--next]\n issue-flow worktree cleanup [--path <dir>]\n issue-flow worktree prune [--only <dir>] [--yes] [-R owner/repo]\n issue-flow pr worktree <n> [--no-setup] [-R owner/repo] [-- [<command>...]]\n issue-flow adapter setup [--only <step>] | issue [<url>] | archive [-R owner/repo]\n issue-flow session\n issue-flow hook worktree-create | worktree-remove\n issue-flow
|
|
5
|
+
export declare const USAGE = "Usage:\n issue-flow issue branch <n>... [--namer claude|title] [--json] [-R owner/repo]\n issue-flow issue branch --missing [--limit <n>] [--namer claude|title] [--dry-run | --yes] [--json] [-R owner/repo]\n issue-flow issue start <n> [--branch] [--dry-run | --yes] [--resume <branch> | --new] [--no-launch]\n [--ask-permissions | --free] [--namer claude|title] [-R owner/repo]\n issue-flow issue start <n> --plan [--ask-permissions | --free] [--namer claude|title] [-R owner/repo]\n issue-flow issue start <n> --cloud | --here | --teleport | --adopt [...]\n issue-flow worktree setup [--only <step>]\n issue-flow worktree teardown [--path <dir>]\n issue-flow worktree ports [--next]\n issue-flow worktree cleanup [--path <dir>]\n issue-flow worktree prune [--only <dir>] [--yes] [-R owner/repo]\n issue-flow pr worktree <n> [--no-setup] [-R owner/repo] [-- [<command>...]]\n issue-flow adapter setup [--only <step>] | issue [<url>] | archive [-R owner/repo]\n issue-flow session\n issue-flow hook worktree-create | worktree-remove\n issue-flow apply [--yes] [--dry-run] [--json] [--recipe <name>,...]\n issue-flow doctor [--json]\n issue-flow labels sync [--update] [--dry-run] [--json] [-R owner/repo]\n\nThe slug comes from the issue's \"Slug de rama\" block, then from Claude, then from the title,\nand the branch from the repo's template (an older \"Rama sugerida\" block is used if it fits).\n--missing adds the slug block to every open issue without a block, after confirming.\n\nissue start makes the branch in a worktree (or, with --branch, in this checkout), the\nstart commit, the push, the draft PR and the assignment, after confirming, and opens\nClaude: in auto mode if the repo has claude-repo-guard, with your settings with\n--ask-permissions, and with no sandbox nor questions with --free. --plan prints the branch\nand the prompt as JSON and changes nothing.\n\n--cloud confirms here and opens a Claude cloud session that starts the issue there.\n--here, inside a cloud session, starts it in this checkout and opens nothing. --teleport\nbrings the cloud session noted in the PR to the worktree. --adopt starts it in the\nworktree another tool (Orca, Paseo) just made, without asking nor opening Claude.\n\nworktree runs the steps of \"worktrees\" in .agents/flow.json: setup prepares this worktree,\nteardown cleans it before it goes (steps, ports, Docker), ports prints or moves its block\nof ports, cleanup deletes the Docker of deleted worktrees, and prune deletes the worktrees\nwhose PR was merged or closed. session is the SessionStart hook of Claude Code.\n\npr worktree makes (or reuses) a worktree on a PR's own branch, to review and fix it; with\n--, it runs the command there (Claude if none). adapter is what Orca and Paseo run: setup\nwhen they make a worktree (its steps and, if it came from an issue, issue start --adopt),\nissue to start one by hand, and archive before they delete it. hook implements Claude\nCode's WorktreeCreate and WorktreeRemove for claude --worktree.\n\napply leaves the repo as this version needs it: .agents/flow.json (the first time, with\nthe recipes its files suggest), issue-flow at this version and github-rest and the scripts\nif missing in package.json, and the hooks; it changes no other version, and refuses a newer\nissue-flow or a range. doctor checks them. npx agent-kitchen apply runs apply at the version\nit picks, along with the skills, claude-cloud and claude-repo-guard. init and upgrade still\nrun apply, with a warning, until 0.4.0.\n\nlabels sync makes in GitHub the labels the flow wants (\"labels\" in .agents/flow.json: the\nissue types and P1 to P4 by default) that the repo lacks. Names ignore case and are never\nrenamed nor deleted; --update also fixes the color and description of those that differ.";
|
package/dist/usage.js
CHANGED
|
@@ -18,9 +18,8 @@ export const USAGE = `Usage:
|
|
|
18
18
|
issue-flow adapter setup [--only <step>] | issue [<url>] | archive [-R owner/repo]
|
|
19
19
|
issue-flow session
|
|
20
20
|
issue-flow hook worktree-create | worktree-remove
|
|
21
|
-
issue-flow
|
|
21
|
+
issue-flow apply [--yes] [--dry-run] [--json] [--recipe <name>,...]
|
|
22
22
|
issue-flow doctor [--json]
|
|
23
|
-
issue-flow upgrade [--dry-run]
|
|
24
23
|
issue-flow labels sync [--update] [--dry-run] [--json] [-R owner/repo]
|
|
25
24
|
|
|
26
25
|
The slug comes from the issue's "Slug de rama" block, then from Claude, then from the title,
|
|
@@ -49,10 +48,12 @@ when they make a worktree (its steps and, if it came from an issue, issue start
|
|
|
49
48
|
issue to start one by hand, and archive before they delete it. hook implements Claude
|
|
50
49
|
Code's WorktreeCreate and WorktreeRemove for claude --worktree.
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
the
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
apply leaves the repo as this version needs it: .agents/flow.json (the first time, with
|
|
52
|
+
the recipes its files suggest), issue-flow at this version and github-rest and the scripts
|
|
53
|
+
if missing in package.json, and the hooks; it changes no other version, and refuses a newer
|
|
54
|
+
issue-flow or a range. doctor checks them. npx agent-kitchen apply runs apply at the version
|
|
55
|
+
it picks, along with the skills, claude-cloud and claude-repo-guard. init and upgrade still
|
|
56
|
+
run apply, with a warning, until 0.4.0.
|
|
56
57
|
|
|
57
58
|
labels sync makes in GitHub the labels the flow wants ("labels" in .agents/flow.json: the
|
|
58
59
|
issue types and P1 to P4 by default) that the repo lacks. Names ignore case and are never
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ramon-sg/issue-flow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|
|
@@ -17,6 +17,6 @@
|
|
|
17
17
|
"test": "tsc && node --test test/*.test.mjs",
|
|
18
18
|
"prepublishOnly": "npm test"
|
|
19
19
|
},
|
|
20
|
-
"dependencies": { "@ramon-sg/github-rest": "0.3.0" },
|
|
20
|
+
"dependencies": { "@ramon-sg/github-rest": "^0.3.0" },
|
|
21
21
|
"devDependencies": { "@ramon-sg/skills-manager": "1.2.0", "@types/node": "^24.0.0", "typescript": "^5.9.3" }
|
|
22
22
|
}
|
package/dist/commands/init.d.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { type Output } from "../output.js";
|
|
2
|
-
/** Options of `issue-flow init`. */
|
|
3
|
-
export interface InitOptions {
|
|
4
|
-
cwd: string;
|
|
5
|
-
yes: boolean;
|
|
6
|
-
recipes: string[] | null;
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* The URL of the config's schema at a version.
|
|
10
|
-
* @param version - This package's version.
|
|
11
|
-
* @returns The URL.
|
|
12
|
-
* @example schemaUrl("0.1.0") // "https://raw.githubusercontent.com/ramon-sg/issue-flow/v0.1.0/schema.json"
|
|
13
|
-
*/
|
|
14
|
-
export declare function schemaUrl(version: string): string;
|
|
15
|
-
/**
|
|
16
|
-
* Sets the repo up for the flow; see the file's header.
|
|
17
|
-
* @param options - The options.
|
|
18
|
-
* @param out - Where to write and ask.
|
|
19
|
-
* @returns 0, or 1 if the files were not written.
|
|
20
|
-
* @throws SetupError outside the main checkout or with a broken file; ConfigError if the
|
|
21
|
-
* config would be invalid; UsageError for an unknown recipe.
|
|
22
|
-
* @example await init({ cwd, yes: true, recipes: null }, out)
|
|
23
|
-
*/
|
|
24
|
-
export declare function init(options: InitOptions, out: Output): Promise<number>;
|
package/dist/commands/init.js
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file `issue-flow init`: sets a repo up for the flow, and can run again to fill in what
|
|
3
|
-
* is missing. It writes `.agents/flow.json` (from `paseo.json` if the repo has one, plus the
|
|
4
|
-
* recipes it picks), the flow's dev dependencies and scripts in `package.json`, and the
|
|
5
|
-
* `SessionStart`, `WorktreeCreate` and `WorktreeRemove` hooks. Only the flow's own files:
|
|
6
|
-
* the skills, `claude-cloud` and `claude-repo-guard` are installed by `agent-kitchen`,
|
|
7
|
-
* which runs this command.
|
|
8
|
-
*/
|
|
9
|
-
import { confirm } from "../ask.js";
|
|
10
|
-
import { CONFIG_PATH, isObject } from "../config-base.js";
|
|
11
|
-
import { parseConfig } from "../config.js";
|
|
12
|
-
import { checkoutRoot, locateRepo } from "../git.js";
|
|
13
|
-
import { UsageError, warn } from "../output.js";
|
|
14
|
-
import { withFlowPackages } from "../package-edits.js";
|
|
15
|
-
import { applyRecipe, detectedRecipes, loadRecipes, projectPrefix } from "../recipes.js";
|
|
16
|
-
import { registerHooks } from "../settings-hooks.js";
|
|
17
|
-
import { FLOW_REPO, ownVersions, packageManager, readJson, SETTINGS_FILE, SetupError, writeJson } from "../setup-files.js";
|
|
18
|
-
/**
|
|
19
|
-
* The URL of the config's schema at a version.
|
|
20
|
-
* @param version - This package's version.
|
|
21
|
-
* @returns The URL.
|
|
22
|
-
* @example schemaUrl("0.1.0") // "https://raw.githubusercontent.com/ramon-sg/issue-flow/v0.1.0/schema.json"
|
|
23
|
-
*/
|
|
24
|
-
export function schemaUrl(version) {
|
|
25
|
-
return `https://raw.githubusercontent.com/${FLOW_REPO}/v${version}/schema.json`;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* A new config: the schema, and the branch rules of `paseo.json` if the repo has one.
|
|
29
|
-
* @param root - The repo.
|
|
30
|
-
* @param version - This package's version.
|
|
31
|
-
* @returns The config and what it took from `paseo.json`.
|
|
32
|
-
*/
|
|
33
|
-
function freshConfig(root, version) {
|
|
34
|
-
const raw = { $schema: schemaUrl(version) };
|
|
35
|
-
const issues = readJson(root, "paseo.json")?.project;
|
|
36
|
-
const paseo = isObject(issues) && isObject(issues.issues) ? issues.issues : null;
|
|
37
|
-
if (paseo === null)
|
|
38
|
-
return { raw, changes: ["new"] };
|
|
39
|
-
const branch = {};
|
|
40
|
-
if (typeof paseo.branchTemplate === "string")
|
|
41
|
-
branch.template = paseo.branchTemplate;
|
|
42
|
-
if (isObject(paseo.types))
|
|
43
|
-
branch.types = paseo.types;
|
|
44
|
-
if (Object.keys(branch).length > 0)
|
|
45
|
-
raw.branch = branch;
|
|
46
|
-
return { raw, changes: ["new, with the branch rules of paseo.json"] };
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* The recipes to apply: the ones named with `--recipe`, or each one the repo's files
|
|
50
|
-
* suggest, after asking (all of them with `--yes`).
|
|
51
|
-
* @param root - The repo.
|
|
52
|
-
* @param options - The options.
|
|
53
|
-
* @param out - Where to ask.
|
|
54
|
-
* @returns The recipes.
|
|
55
|
-
* @throws UsageError if `--recipe` names one that does not exist.
|
|
56
|
-
*/
|
|
57
|
-
async function pickRecipes(root, options, out) {
|
|
58
|
-
const recipes = loadRecipes();
|
|
59
|
-
if (options.recipes !== null) {
|
|
60
|
-
const unknown = options.recipes.find((name) => !recipes.some((recipe) => recipe.name === name));
|
|
61
|
-
if (unknown !== undefined)
|
|
62
|
-
throw new UsageError(`unknown recipe ${unknown}; there are ${recipes.map((recipe) => recipe.name).join(", ")}.`);
|
|
63
|
-
return recipes.filter((recipe) => options.recipes?.includes(recipe.name));
|
|
64
|
-
}
|
|
65
|
-
const picked = [];
|
|
66
|
-
for (const recipe of recipes.filter((item) => detectedRecipes(root, recipes).includes(item.name)))
|
|
67
|
-
if (options.yes || (await confirm(out, `Recipe ${recipe.name}: ${recipe.description} Add it?`)))
|
|
68
|
-
picked.push(recipe);
|
|
69
|
-
return picked;
|
|
70
|
-
}
|
|
71
|
-
/** How a line of a change that keeps what the repo had starts. */
|
|
72
|
-
const KEPT = "kept ";
|
|
73
|
-
/**
|
|
74
|
-
* What `init` would write: the config, `package.json` and the settings' hooks.
|
|
75
|
-
* @param root - The repo.
|
|
76
|
-
* @param versions - This package's versions.
|
|
77
|
-
* @param recipes - The recipes to apply.
|
|
78
|
-
* @returns The files that change.
|
|
79
|
-
* @throws ConfigError if the resulting config is invalid; SetupError if a file is broken.
|
|
80
|
-
*/
|
|
81
|
-
function plan(root, versions, recipes) {
|
|
82
|
-
const existing = readJson(root, CONFIG_PATH);
|
|
83
|
-
const start = existing === null ? freshConfig(root, versions.issueFlow) : { raw: existing, changes: [] };
|
|
84
|
-
let { raw } = start;
|
|
85
|
-
const configChanges = [...start.changes];
|
|
86
|
-
for (const recipe of recipes) {
|
|
87
|
-
const applied = applyRecipe(raw, recipe, projectPrefix(root));
|
|
88
|
-
raw = applied.raw;
|
|
89
|
-
configChanges.push(...applied.changes.map((change) => (change.startsWith(KEPT) ? `${change} (${recipe.name})` : `${recipe.name}: ${change}`)));
|
|
90
|
-
}
|
|
91
|
-
parseConfig(raw);
|
|
92
|
-
const pkg = withFlowPackages(readJson(root, "package.json"), versions, false);
|
|
93
|
-
const hooks = registerHooks(readJson(root, SETTINGS_FILE), versions.issueFlow);
|
|
94
|
-
const file = (path, value, lines) => ({
|
|
95
|
-
path,
|
|
96
|
-
value,
|
|
97
|
-
changes: lines.filter((line) => !line.startsWith(KEPT)).map((line) => `${path}: ${line}`),
|
|
98
|
-
kept: lines.filter((line) => line.startsWith(KEPT)).map((line) => `${path}: ${line}`),
|
|
99
|
-
});
|
|
100
|
-
return [file(CONFIG_PATH, raw, configChanges), file("package.json", pkg.pkg, pkg.changes), file(SETTINGS_FILE, hooks.settings, hooks.changes)];
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* Sets the repo up for the flow; see the file's header.
|
|
104
|
-
* @param options - The options.
|
|
105
|
-
* @param out - Where to write and ask.
|
|
106
|
-
* @returns 0, or 1 if the files were not written.
|
|
107
|
-
* @throws SetupError outside the main checkout or with a broken file; ConfigError if the
|
|
108
|
-
* config would be invalid; UsageError for an unknown recipe.
|
|
109
|
-
* @example await init({ cwd, yes: true, recipes: null }, out)
|
|
110
|
-
*/
|
|
111
|
-
export async function init(options, out) {
|
|
112
|
-
const located = locateRepo(options.cwd);
|
|
113
|
-
if (located === null)
|
|
114
|
-
throw new SetupError("run init inside a checkout of the repo.");
|
|
115
|
-
if (checkoutRoot(options.cwd) !== located.mainRoot)
|
|
116
|
-
throw new SetupError(`run init in the main checkout, ${located.mainRoot}, not in a worktree.`);
|
|
117
|
-
const root = located.mainRoot;
|
|
118
|
-
const versions = ownVersions();
|
|
119
|
-
const files = plan(root, versions, await pickRecipes(root, options, out)).filter((file) => file.changes.length > 0 || file.kept.length > 0);
|
|
120
|
-
for (const line of files.flatMap((file) => file.kept))
|
|
121
|
-
warn(out, line);
|
|
122
|
-
const lines = files.flatMap((file) => file.changes);
|
|
123
|
-
if (lines.length === 0)
|
|
124
|
-
out.stdout("The config, package.json and the hooks are already set up.\n");
|
|
125
|
-
else
|
|
126
|
-
out.stdout(`issue-flow ${versions.issueFlow} will write:\n${lines.map((line) => ` ${line}\n`).join("")}`);
|
|
127
|
-
if (lines.length > 0 && !options.yes && !(await confirm(out, "Write it?"))) {
|
|
128
|
-
warn(out, "nothing written.");
|
|
129
|
-
return 1;
|
|
130
|
-
}
|
|
131
|
-
for (const file of files.filter((item) => item.changes.length > 0))
|
|
132
|
-
writeJson(root, file.path, file.value);
|
|
133
|
-
const manager = packageManager(root, readJson(root, "package.json"));
|
|
134
|
-
out.stdout(`Next: run '${manager} install', and commit .agents/, .claude/ and package.json.\n`);
|
|
135
|
-
return 0;
|
|
136
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { Output } from "../output.js";
|
|
2
|
-
/** Options of `issue-flow upgrade`. */
|
|
3
|
-
export interface UpgradeOptions {
|
|
4
|
-
cwd: string;
|
|
5
|
-
dryRun: boolean;
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* Moves the repo's flow files to this version; see the file's header.
|
|
9
|
-
* @param options - The options.
|
|
10
|
-
* @param out - Where to write.
|
|
11
|
-
* @returns 0.
|
|
12
|
-
* @throws SetupError outside the main checkout, if a file is broken, or if the repo is
|
|
13
|
-
* already at a newer version (a stale local copy must not move it back).
|
|
14
|
-
* @example upgrade({ cwd, dryRun: false }, out)
|
|
15
|
-
*/
|
|
16
|
-
export declare function upgrade(options: UpgradeOptions, out: Output): number;
|
package/dist/commands/upgrade.js
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file `issue-flow upgrade [--dry-run]`: moves the repo's flow files to the version that
|
|
3
|
-
* runs: the dev dependencies to the ones it pins, and the hooks and the config's schema to
|
|
4
|
-
* it. `agent-kitchen` runs it as `npx @ramon-sg/issue-flow@<version> upgrade` and then
|
|
5
|
-
* moves the flow's skills to the same tag, because the skills call this CLI.
|
|
6
|
-
*/
|
|
7
|
-
import { CONFIG_PATH } from "../config-base.js";
|
|
8
|
-
import { checkoutRoot, locateRepo } from "../git.js";
|
|
9
|
-
import { dependencyVersion, withFlowPackages } from "../package-edits.js";
|
|
10
|
-
import { hookVersions, registerHooks } from "../settings-hooks.js";
|
|
11
|
-
import { FLOW_REPO, ownVersions, PACKAGE, packageManager, readJson, SETTINGS_FILE, SetupError, writeJson } from "../setup-files.js";
|
|
12
|
-
import { schemaUrl } from "./init.js";
|
|
13
|
-
/** The start of the schema URL `init` writes, at any version. */
|
|
14
|
-
const SCHEMA_PREFIX = `https://raw.githubusercontent.com/${FLOW_REPO}/`;
|
|
15
|
-
/** A release version, after an optional range sign. */
|
|
16
|
-
const RELEASE = /^[\^~=]?(\d+)\.(\d+)\.(\d+)$/;
|
|
17
|
-
/**
|
|
18
|
-
* Whether a version is newer than another; false if either is not a release.
|
|
19
|
-
* @param version - The version, maybe with a range sign.
|
|
20
|
-
* @param than - The other.
|
|
21
|
-
* @returns True if it is newer.
|
|
22
|
-
*/
|
|
23
|
-
function isNewer(version, than) {
|
|
24
|
-
const [left, right] = [RELEASE.exec(version), RELEASE.exec(than)];
|
|
25
|
-
if (left === null || right === null)
|
|
26
|
-
return false;
|
|
27
|
-
for (let index = 1; index <= 3; index += 1) {
|
|
28
|
-
const diff = Number(left[index]) - Number(right[index]);
|
|
29
|
-
if (diff !== 0)
|
|
30
|
-
return diff > 0;
|
|
31
|
-
}
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* The newest version of the flow the repo already has, in package.json or a hook.
|
|
36
|
-
* @param root - The repo.
|
|
37
|
-
* @returns The version, or null if it has none.
|
|
38
|
-
*/
|
|
39
|
-
function repoVersion(root) {
|
|
40
|
-
const found = [dependencyVersion(readJson(root, "package.json"), PACKAGE), ...Object.values(hookVersions(readJson(root, SETTINGS_FILE)))];
|
|
41
|
-
return found.filter((version) => version !== null).reduce((newest, version) => (newest === null || isNewer(version, newest) ? version : newest), null);
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Moves the repo's flow files to this version; see the file's header.
|
|
45
|
-
* @param options - The options.
|
|
46
|
-
* @param out - Where to write.
|
|
47
|
-
* @returns 0.
|
|
48
|
-
* @throws SetupError outside the main checkout, if a file is broken, or if the repo is
|
|
49
|
-
* already at a newer version (a stale local copy must not move it back).
|
|
50
|
-
* @example upgrade({ cwd, dryRun: false }, out)
|
|
51
|
-
*/
|
|
52
|
-
export function upgrade(options, out) {
|
|
53
|
-
const located = locateRepo(options.cwd);
|
|
54
|
-
if (located === null)
|
|
55
|
-
throw new SetupError("run upgrade inside a checkout of the repo.");
|
|
56
|
-
const root = located.mainRoot;
|
|
57
|
-
if (checkoutRoot(options.cwd) !== root)
|
|
58
|
-
throw new SetupError(`run upgrade in the main checkout, ${root}, not in a worktree.`);
|
|
59
|
-
const versions = ownVersions();
|
|
60
|
-
const current = repoVersion(root);
|
|
61
|
-
if (current !== null && isNewer(current, versions.issueFlow))
|
|
62
|
-
throw new SetupError(`the repo is at issue-flow ${current}, newer than this ${versions.issueFlow}; run 'npx ${PACKAGE}@${current.replace(/^[\^~=]/, "")} upgrade' or 'npx agent-kitchen'.`);
|
|
63
|
-
const writes = [];
|
|
64
|
-
const lines = [];
|
|
65
|
-
const pkg = withFlowPackages(readJson(root, "package.json"), versions, true);
|
|
66
|
-
if (pkg.changes.length > 0)
|
|
67
|
-
writes.push({ path: "package.json", value: pkg.pkg });
|
|
68
|
-
lines.push(...pkg.changes.map((change) => `package.json: ${change}`));
|
|
69
|
-
const hooks = registerHooks(readJson(root, SETTINGS_FILE), versions.issueFlow);
|
|
70
|
-
if (hooks.changes.length > 0)
|
|
71
|
-
writes.push({ path: SETTINGS_FILE, value: hooks.settings });
|
|
72
|
-
lines.push(...hooks.changes.map((change) => `${SETTINGS_FILE}: ${change}`));
|
|
73
|
-
const config = readJson(root, CONFIG_PATH);
|
|
74
|
-
if (config !== null && typeof config.$schema === "string" && config.$schema.startsWith(SCHEMA_PREFIX) && config.$schema !== schemaUrl(versions.issueFlow)) {
|
|
75
|
-
writes.push({ path: CONFIG_PATH, value: { ...config, $schema: schemaUrl(versions.issueFlow) } });
|
|
76
|
-
lines.push(`${CONFIG_PATH}: $schema at v${versions.issueFlow}`);
|
|
77
|
-
}
|
|
78
|
-
out.stdout(lines.length === 0 ? `Already at issue-flow ${versions.issueFlow}.\n` : `To issue-flow ${versions.issueFlow}:\n${lines.map((line) => ` ${line}\n`).join("")}`);
|
|
79
|
-
if (options.dryRun || writes.length === 0)
|
|
80
|
-
return 0;
|
|
81
|
-
for (const write of writes)
|
|
82
|
-
writeJson(root, write.path, write.value);
|
|
83
|
-
out.stdout(`Next: run '${packageManager(root, readJson(root, "package.json"))} install', review the diff and commit it.\n`);
|
|
84
|
-
return 0;
|
|
85
|
-
}
|