@ramon-sg/issue-flow 0.2.5 → 0.2.7
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 +38 -3
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +23 -0
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/init.js +1 -3
- package/dist/commands/labels.d.ts +54 -0
- package/dist/commands/labels.js +113 -0
- package/dist/commands/upgrade.d.ts +1 -1
- package/dist/commands/upgrade.js +12 -25
- package/dist/config.d.ts +6 -0
- package/dist/config.js +61 -1
- package/dist/package-edits.d.ts +37 -6
- package/dist/package-edits.js +110 -9
- package/dist/usage.d.ts +1 -1
- package/dist/usage.js +7 -2
- package/package.json +1 -1
- package/schema.json +44 -0
package/README.md
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
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
9
|
- **`issue-flow init|doctor|upgrade`:** escribe, revisa y mueve a su versión 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
|
+
- **`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.
|
|
11
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.
|
|
12
13
|
- **`issue-flow worktree setup|teardown|ports|cleanup|prune`:** prepara y limpia cada worktree con los pasos del repo, le da un bloque de puertos y borra los worktrees con el PR cerrado.
|
|
13
14
|
- **`issue-flow pr worktree <n>`:** abre un PR en su propio worktree, en la rama del PR, para revisarlo sin soltar lo que estás haciendo.
|
|
@@ -182,7 +183,7 @@ Es el hook `SessionStart` de Claude Code. Lo que imprime le llega a Claude como
|
|
|
182
183
|
|
|
183
184
|
## Lo que el flujo deja en el repo: `issue-flow init`
|
|
184
185
|
|
|
185
|
-
`npx agent-kitchen` corre estos tres comandos en la versión que corresponde. Correrlos a mano sirve para probar el flujo en un repo sin lo demás.
|
|
186
|
+
`npx agent-kitchen apply` corre estos tres comandos en la versión que corresponde. Correrlos a mano sirve para probar el flujo en un repo sin lo demás.
|
|
186
187
|
|
|
187
188
|
```sh
|
|
188
189
|
npx @ramon-sg/issue-flow init # pregunta cada receta y pide el OK
|
|
@@ -199,7 +200,7 @@ npx @ramon-sg/issue-flow@<versión> upgrade # mueve el repo a esa versión (--d
|
|
|
199
200
|
|
|
200
201
|
**`doctor`** no cambia nada. Revisa la config, las dependencias y que los hooks estén en esta versión. Cada problema dice qué lo arregla.
|
|
201
202
|
|
|
202
|
-
**`upgrade`** mueve el repo a la versión que corre:
|
|
203
|
+
**`upgrade`** mueve el repo a la versión que corre: `@ramon-sg/issue-flow`, los hooks y el `$schema`. `github-rest` y `skills-manager` solo suben si no llegan a la versión con que se probó issue-flow: una versión igual o más nueva, un rango que la permite (`^1.2.0` para 1.3.0, no `^0.1.0` para 0.3.0) o una fuente como `workspace:*` se deja como está y se avisa (`kept …`). No consulta npm, así que se corre con la versión: `npx @ramon-sg/issue-flow@<versión> upgrade`. Nunca baja de versión: si el repo ya tiene una más nueva (por ejemplo, una copia local vieja en `node_modules`), se detiene y dice qué correr. Las skills del flujo llaman a la CLI, así que tienen que ir a la misma tag: `agent-kitchen` las mueve después. `--dry-run` solo dice qué cambiaría.
|
|
203
204
|
|
|
204
205
|
### Recetas
|
|
205
206
|
|
|
@@ -270,6 +271,40 @@ Todo es opcional; sin el archivo valen los valores por defecto. Una clave descon
|
|
|
270
271
|
| `start.cloudPrompt` | `/start-issue {number} --yes{flags}` | Lo que `--cloud` le pide a la sesión; `{flags}` es `--new` o `--resume <rama>` |
|
|
271
272
|
| `plans.dirs` | `docs/plans`, `docs/specs` | Dónde buscar el plan de la issue: lo que tocó la rama, o el documento cuyo `prs:` nombra su PR |
|
|
272
273
|
| `colors` | `critical-path` y `P1:` rojo, `P2:` naranjo, `P3:` amarillo, `P4:` cian | Color de la sesión por prefijo de label; gana el primero que calce |
|
|
274
|
+
| `labels` | Los 8 de abajo | Labels que crea `labels sync`; lo del repo se suma al default (ver abajo) |
|
|
275
|
+
|
|
276
|
+
## Labels: `issue-flow labels sync`
|
|
277
|
+
|
|
278
|
+
```sh
|
|
279
|
+
npx @ramon-sg/issue-flow labels sync --dry-run # qué haría
|
|
280
|
+
npx @ramon-sg/issue-flow labels sync # crea los que faltan
|
|
281
|
+
npx @ramon-sg/issue-flow labels sync --update # también corrige color y descripción
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Crea en GitHub los labels de `labels` que el repo no tiene. Los nombres se comparan sin mayúsculas, como en GitHub: un `Bug` cuenta como `bug` y nunca se renombra. Uno que existe con otro color o descripción solo se avisa, salvo con `--update`. Nunca borra. `--json` da la lista con el estado de cada uno: `created`, `updated`, `ok`, `differs`, o `to-create` y `to-update` con `--dry-run`. Si GitHub rechaza uno, sigue con los demás, lo marca `failed` y sale con 1.
|
|
285
|
+
|
|
286
|
+
| Label | Color | Descripción |
|
|
287
|
+
|---|---|---|
|
|
288
|
+
| `bug` | `d73a4a` | Something isn't working |
|
|
289
|
+
| `enhancement` | `a2eeef` | New feature or request |
|
|
290
|
+
| `documentation` | `0075ca` | Improvements or additions to documentation |
|
|
291
|
+
| `spike` | `fbca04` | Time-boxed research before building |
|
|
292
|
+
| `P1: urgent` | `B60205` | Breaks production or blocks work |
|
|
293
|
+
| `P2: important` | `F9D0C4` | Real impact, with some slack |
|
|
294
|
+
| `P3: minor` | `FEF2C0` | Default when in doubt |
|
|
295
|
+
| `P4: low` | `BFDADC` | Nice-to-have |
|
|
296
|
+
|
|
297
|
+
Los tres primeros son los de GitHub, con sus colores y textos. A diferencia de `branch.types` y `colors`, lo que pone el repo **se suma** al default: un nombre reemplaza al de por defecto con ese nombre, y `null` lo quita. La taxonomía del repo va acá y no en el `project.md` de `github-issues`, porque con `claude-repo-guard` el agente no puede escribir en `.agents/skills/`:
|
|
298
|
+
|
|
299
|
+
```json
|
|
300
|
+
{
|
|
301
|
+
"labels": {
|
|
302
|
+
"area:api": { "color": "1d76db", "description": "packages/api" },
|
|
303
|
+
"size:S": { "color": "c5def5", "description": "Menos de 1 día" },
|
|
304
|
+
"spike": null
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
```
|
|
273
308
|
|
|
274
309
|
## Desarrollo
|
|
275
310
|
|
package/dist/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -8,6 +8,7 @@ import { parseArgs } from "node:util";
|
|
|
8
8
|
import { GitHubError, InvalidRepoEnvError, resolveRepo } from "@ramon-sg/github-rest";
|
|
9
9
|
import { branchMissing, branchOf } from "./commands/issue-branch.js";
|
|
10
10
|
import { issueStart } from "./commands/issue-start.js";
|
|
11
|
+
import { labelsSync } from "./commands/labels.js";
|
|
11
12
|
import { ADAPTER_COMMANDS, HOOK_COMMANDS, runAdapter, runHook, runSession, runWorktree, WORKTREE_COMMANDS } from "./cli-worktree.js";
|
|
12
13
|
import { runSetupCommand, SETUP_COMMANDS } from "./cli-setup.js";
|
|
13
14
|
import { prWorktree } from "./commands/pr-worktree.js";
|
|
@@ -41,6 +42,7 @@ const OPTIONS = {
|
|
|
41
42
|
next: { type: "boolean" },
|
|
42
43
|
"no-setup": { type: "boolean" },
|
|
43
44
|
recipe: { type: "string" },
|
|
45
|
+
update: { type: "boolean" },
|
|
44
46
|
};
|
|
45
47
|
/** Flags each command takes, besides `--repo`, `--help` and `--namer`. */
|
|
46
48
|
const COMMAND_FLAGS = {
|
|
@@ -75,6 +77,7 @@ const COMMAND_FLAGS = {
|
|
|
75
77
|
init: ["yes", "recipe"],
|
|
76
78
|
doctor: ["json"],
|
|
77
79
|
upgrade: ["dry-run"],
|
|
80
|
+
"labels sync": ["update", "dry-run", "json"],
|
|
78
81
|
};
|
|
79
82
|
/** An issue number: digits only. */
|
|
80
83
|
const NUMBER = /^\d+$/;
|
|
@@ -216,6 +219,8 @@ export async function main(argv, out, cwd) {
|
|
|
216
219
|
throw new UsageError("-- only goes with pr worktree.");
|
|
217
220
|
if (group === "pr")
|
|
218
221
|
return runPr(sub, rest, flags, { out, cwd, command });
|
|
222
|
+
if (group === "labels")
|
|
223
|
+
return runLabels(sub, rest, flags, { out, cwd });
|
|
219
224
|
if (group !== undefined && SETUP_COMMANDS.includes(group)) {
|
|
220
225
|
ownFlags(group, flags);
|
|
221
226
|
return runSetupCommand(group, flags, [sub, ...rest].filter((item) => item !== undefined), { out, cwd });
|
|
@@ -253,6 +258,24 @@ async function runPr(sub, rest, flags, run) {
|
|
|
253
258
|
const config = loadConfig(run.cwd);
|
|
254
259
|
return prWorktree({ repo: repoOf(flags.repo, config, run.cwd), config, cwd: run.cwd, number: Number(text), setup: !(flags["no-setup"] ?? false), command: run.command }, run.out);
|
|
255
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* Runs `issue-flow labels sync`.
|
|
263
|
+
* @param sub - The word after `labels`.
|
|
264
|
+
* @param rest - The positionals after it.
|
|
265
|
+
* @param flags - The flags.
|
|
266
|
+
* @param run - Where it writes and runs.
|
|
267
|
+
* @returns The exit code.
|
|
268
|
+
* @throws UsageError if the arguments do not fit; GitHubError if GitHub fails.
|
|
269
|
+
*/
|
|
270
|
+
async function runLabels(sub, rest, flags, run) {
|
|
271
|
+
if (sub !== "sync")
|
|
272
|
+
throw new UsageError("labels expects sync.");
|
|
273
|
+
ownFlags("labels sync", flags);
|
|
274
|
+
if (rest.length > 0)
|
|
275
|
+
throw new UsageError("labels sync takes no arguments.");
|
|
276
|
+
const config = loadConfig(run.cwd);
|
|
277
|
+
return labelsSync({ repo: repoOf(flags.repo, config, run.cwd), labels: config.labels, update: flags.update ?? false, dryRun: flags["dry-run"] ?? false, json: flags.json ?? false }, run.out);
|
|
278
|
+
}
|
|
256
279
|
/**
|
|
257
280
|
* Runs the `issue`, `worktree` and `session` commands.
|
|
258
281
|
* @param group - The first word.
|
package/dist/commands/doctor.js
CHANGED
|
@@ -11,7 +11,7 @@ import { dependencyVersion } from "../package-edits.js";
|
|
|
11
11
|
import { hookVersions } from "../settings-hooks.js";
|
|
12
12
|
import { ownVersions, PACKAGE, readJson, SETTINGS_FILE, SetupError } from "../setup-files.js";
|
|
13
13
|
/** What fixes every problem: it runs init or upgrade at the right version. */
|
|
14
|
-
const RUN_AGENT_KITCHEN = "run npx agent-kitchen";
|
|
14
|
+
const RUN_AGENT_KITCHEN = "run npx agent-kitchen apply";
|
|
15
15
|
/**
|
|
16
16
|
* Checks the config.
|
|
17
17
|
* @param root - The repo.
|
package/dist/commands/init.js
CHANGED
|
@@ -11,7 +11,7 @@ import { CONFIG_PATH, isObject } from "../config-base.js";
|
|
|
11
11
|
import { parseConfig } from "../config.js";
|
|
12
12
|
import { checkoutRoot, locateRepo } from "../git.js";
|
|
13
13
|
import { UsageError, warn } from "../output.js";
|
|
14
|
-
import { withFlowPackages } from "../package-edits.js";
|
|
14
|
+
import { KEPT, withFlowPackages } from "../package-edits.js";
|
|
15
15
|
import { applyRecipe, detectedRecipes, loadRecipes, projectPrefix } from "../recipes.js";
|
|
16
16
|
import { registerHooks } from "../settings-hooks.js";
|
|
17
17
|
import { FLOW_REPO, ownVersions, packageManager, readJson, SETTINGS_FILE, SetupError, writeJson } from "../setup-files.js";
|
|
@@ -68,8 +68,6 @@ async function pickRecipes(root, options, out) {
|
|
|
68
68
|
picked.push(recipe);
|
|
69
69
|
return picked;
|
|
70
70
|
}
|
|
71
|
-
/** How a line of a change that keeps what the repo had starts. */
|
|
72
|
-
const KEPT = "kept ";
|
|
73
71
|
/**
|
|
74
72
|
* What `init` would write: the config, `package.json` and the settings' hooks.
|
|
75
73
|
* @param root - The repo.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file `issue-flow labels sync [--update] [--dry-run] [--json]`: makes in GitHub the labels
|
|
3
|
+
* of `labels` in `.agents/flow.json` that the repo lacks. Names compare ignoring case, as
|
|
4
|
+
* GitHub does, so `Bug` counts as `bug` and is never renamed. A label that exists with
|
|
5
|
+
* another color or description is only reported, unless `--update`. It never deletes. A
|
|
6
|
+
* write that fails does not stop the rest: every label is reported, and it exits 1.
|
|
7
|
+
*/
|
|
8
|
+
import { type Label } from "@ramon-sg/github-rest";
|
|
9
|
+
import type { LabelSpec } from "../config.js";
|
|
10
|
+
import { type Output } from "../output.js";
|
|
11
|
+
/**
|
|
12
|
+
* What happened to one label: with `--dry-run`, `to-create` and `to-update` instead of
|
|
13
|
+
* `created` and `updated`; `failed` when GitHub refused the write.
|
|
14
|
+
*/
|
|
15
|
+
export type LabelStatus = "created" | "updated" | "ok" | "differs" | "to-create" | "to-update" | "failed";
|
|
16
|
+
/** One label of the result. */
|
|
17
|
+
export interface LabelResult {
|
|
18
|
+
name: string;
|
|
19
|
+
status: LabelStatus;
|
|
20
|
+
color: string;
|
|
21
|
+
description: string;
|
|
22
|
+
/** What GitHub has, when it differs. */
|
|
23
|
+
found?: {
|
|
24
|
+
color: string;
|
|
25
|
+
description: string;
|
|
26
|
+
};
|
|
27
|
+
/** Why the write failed. */
|
|
28
|
+
error?: string;
|
|
29
|
+
}
|
|
30
|
+
/** The options of `labels sync`. */
|
|
31
|
+
export interface LabelsSyncOptions {
|
|
32
|
+
repo: string;
|
|
33
|
+
labels: Record<string, LabelSpec>;
|
|
34
|
+
update: boolean;
|
|
35
|
+
dryRun: boolean;
|
|
36
|
+
json: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* What to do with each label, before writing anything.
|
|
40
|
+
* @param wanted - The labels of the config.
|
|
41
|
+
* @param existing - The labels in GitHub.
|
|
42
|
+
* @param update - Whether to fix the ones that differ.
|
|
43
|
+
* @returns Each label with its status, as if every write worked, and its name in GitHub if it exists.
|
|
44
|
+
*/
|
|
45
|
+
export declare function planLabels(wanted: Record<string, LabelSpec>, existing: Label[], update: boolean): LabelResult[];
|
|
46
|
+
/**
|
|
47
|
+
* Runs `labels sync`; see the file's header.
|
|
48
|
+
* @param options - The repo, the labels and the flags.
|
|
49
|
+
* @param out - Where to write.
|
|
50
|
+
* @returns 0 when done; 1 if GitHub's list came back cut short (nothing is written then) or a write failed.
|
|
51
|
+
* @throws GitHubError if GitHub fails to list the labels.
|
|
52
|
+
* @example await labelsSync({ repo: "acme/widgets", labels, update: false, dryRun: true, json: false }, out)
|
|
53
|
+
*/
|
|
54
|
+
export declare function labelsSync(options: LabelsSyncOptions, out: Output): Promise<number>;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file `issue-flow labels sync [--update] [--dry-run] [--json]`: makes in GitHub the labels
|
|
3
|
+
* of `labels` in `.agents/flow.json` that the repo lacks. Names compare ignoring case, as
|
|
4
|
+
* GitHub does, so `Bug` counts as `bug` and is never renamed. A label that exists with
|
|
5
|
+
* another color or description is only reported, unless `--update`. It never deletes. A
|
|
6
|
+
* write that fails does not stop the rest: every label is reported, and it exits 1.
|
|
7
|
+
*/
|
|
8
|
+
import { createLabel, listLabels } from "@ramon-sg/github-rest";
|
|
9
|
+
import { emitJson } from "../output.js";
|
|
10
|
+
/** Width of the status column in the text output. */
|
|
11
|
+
const STATUS_WIDTH = 10;
|
|
12
|
+
/**
|
|
13
|
+
* Whether a label in GitHub has the wanted color and description (colors ignore case).
|
|
14
|
+
* @param found - The label in GitHub.
|
|
15
|
+
* @param spec - The wanted one.
|
|
16
|
+
* @returns True if they match.
|
|
17
|
+
*/
|
|
18
|
+
function matches(found, spec) {
|
|
19
|
+
return found.color.toLowerCase() === spec.color.toLowerCase() && found.description === spec.description;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* What to do with each label, before writing anything.
|
|
23
|
+
* @param wanted - The labels of the config.
|
|
24
|
+
* @param existing - The labels in GitHub.
|
|
25
|
+
* @param update - Whether to fix the ones that differ.
|
|
26
|
+
* @returns Each label with its status, as if every write worked, and its name in GitHub if it exists.
|
|
27
|
+
*/
|
|
28
|
+
export function planLabels(wanted, existing, update) {
|
|
29
|
+
const byName = new Map(existing.map((label) => [label.name.toLowerCase(), label]));
|
|
30
|
+
return Object.entries(wanted).map(([name, spec]) => {
|
|
31
|
+
const found = byName.get(name.toLowerCase());
|
|
32
|
+
const base = { name, color: spec.color, description: spec.description };
|
|
33
|
+
if (found === undefined)
|
|
34
|
+
return { ...base, status: "created" };
|
|
35
|
+
if (matches(found, spec))
|
|
36
|
+
return { ...base, name: found.name, status: "ok" };
|
|
37
|
+
return { ...base, name: found.name, status: update ? "updated" : "differs", found: { color: found.color, description: found.description } };
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/** The word of each status in the text output. */
|
|
41
|
+
const WORDS = {
|
|
42
|
+
created: "created",
|
|
43
|
+
updated: "updated",
|
|
44
|
+
ok: "ok",
|
|
45
|
+
differs: "differs",
|
|
46
|
+
"to-create": "to create",
|
|
47
|
+
"to-update": "to update",
|
|
48
|
+
failed: "failed",
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* One line of the text output.
|
|
52
|
+
* @param result - The label.
|
|
53
|
+
* @returns The line.
|
|
54
|
+
*/
|
|
55
|
+
function line(result) {
|
|
56
|
+
const head = `${WORDS[result.status].padEnd(STATUS_WIDTH)}${result.name}`;
|
|
57
|
+
if (result.error !== undefined)
|
|
58
|
+
return `${head}: ${result.error}`;
|
|
59
|
+
if (result.found === undefined)
|
|
60
|
+
return head;
|
|
61
|
+
const was = `${result.found.color} "${result.found.description}"`;
|
|
62
|
+
const wanted = `${result.color} "${result.description}"`;
|
|
63
|
+
return result.status === "differs" ? `${head}: has ${was}, the flow wants ${wanted} (--update fixes it)` : `${head}: ${was} → ${wanted}`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Writes one label and says how it went. A label that appeared in GitHub after the list
|
|
67
|
+
* was read fails without `--update`, with a message that says to run it again.
|
|
68
|
+
* @param repo - `owner/repo`.
|
|
69
|
+
* @param result - The label, as planned.
|
|
70
|
+
* @returns The label with its final status.
|
|
71
|
+
*/
|
|
72
|
+
async function write(repo, result) {
|
|
73
|
+
try {
|
|
74
|
+
await createLabel(repo, { name: result.name, color: result.color, description: result.description, force: result.status === "updated" });
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
79
|
+
const appeared = result.status === "created" && message.includes("already exists");
|
|
80
|
+
return { ...result, status: "failed", error: appeared ? "it appeared in GitHub meanwhile: run labels sync again" : message };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Runs `labels sync`; see the file's header.
|
|
85
|
+
* @param options - The repo, the labels and the flags.
|
|
86
|
+
* @param out - Where to write.
|
|
87
|
+
* @returns 0 when done; 1 if GitHub's list came back cut short (nothing is written then) or a write failed.
|
|
88
|
+
* @throws GitHubError if GitHub fails to list the labels.
|
|
89
|
+
* @example await labelsSync({ repo: "acme/widgets", labels, update: false, dryRun: true, json: false }, out)
|
|
90
|
+
*/
|
|
91
|
+
export async function labelsSync(options, out) {
|
|
92
|
+
const { labels: existing, truncated } = await listLabels(options.repo);
|
|
93
|
+
if (truncated) {
|
|
94
|
+
out.stderr(`issue-flow: could not read every label of ${options.repo}; nothing written, so no label is made twice.\n`);
|
|
95
|
+
return 1;
|
|
96
|
+
}
|
|
97
|
+
const results = [];
|
|
98
|
+
for (const item of planLabels(options.labels, existing, options.update)) {
|
|
99
|
+
const writes = item.status === "created" || item.status === "updated";
|
|
100
|
+
// NOTE: one at a time, as GitHub asks of writes, to stay clear of its secondary rate limit.
|
|
101
|
+
if (!writes)
|
|
102
|
+
results.push(item);
|
|
103
|
+
else if (options.dryRun)
|
|
104
|
+
results.push({ ...item, status: item.status === "created" ? "to-create" : "to-update" });
|
|
105
|
+
else
|
|
106
|
+
results.push(await write(options.repo, item));
|
|
107
|
+
}
|
|
108
|
+
if (options.json)
|
|
109
|
+
emitJson(out, { repo: options.repo, dryRun: options.dryRun, labels: results });
|
|
110
|
+
else
|
|
111
|
+
out.stdout(`${results.map(line).join("\n")}\n`);
|
|
112
|
+
return results.some((result) => result.status === "failed") ? 1 : 0;
|
|
113
|
+
}
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -1,36 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file `issue-flow upgrade [--dry-run]`: moves the repo's flow files to the version that
|
|
3
|
-
* runs:
|
|
4
|
-
*
|
|
3
|
+
* runs: `@ramon-sg/issue-flow`, the hooks and the config's schema to it, and `github-rest`
|
|
4
|
+
* and `skills-manager` up to the versions it was tested with only if they fall short (a
|
|
5
|
+
* newer one, or a range that allows it, is kept and reported). `agent-kitchen` runs it as `npx @ramon-sg/issue-flow@<version> upgrade` and then
|
|
5
6
|
* moves the flow's skills to the same tag, because the skills call this CLI.
|
|
6
7
|
*/
|
|
7
8
|
import { CONFIG_PATH } from "../config-base.js";
|
|
8
9
|
import { checkoutRoot, locateRepo } from "../git.js";
|
|
9
|
-
import {
|
|
10
|
+
import { warn } from "../output.js";
|
|
11
|
+
import { dependencyVersion, isNewer, KEPT, withFlowPackages } from "../package-edits.js";
|
|
10
12
|
import { hookVersions, registerHooks } from "../settings-hooks.js";
|
|
11
13
|
import { FLOW_REPO, ownVersions, PACKAGE, packageManager, readJson, SETTINGS_FILE, SetupError, writeJson } from "../setup-files.js";
|
|
12
14
|
import { schemaUrl } from "./init.js";
|
|
13
15
|
/** The start of the schema URL `init` writes, at any version. */
|
|
14
16
|
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
17
|
/**
|
|
35
18
|
* The newest version of the flow the repo already has, in package.json or a hook.
|
|
36
19
|
* @param root - The repo.
|
|
@@ -59,13 +42,17 @@ export function upgrade(options, out) {
|
|
|
59
42
|
const versions = ownVersions();
|
|
60
43
|
const current = repoVersion(root);
|
|
61
44
|
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'.`);
|
|
45
|
+
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 apply'.`);
|
|
63
46
|
const writes = [];
|
|
64
47
|
const lines = [];
|
|
65
48
|
const pkg = withFlowPackages(readJson(root, "package.json"), versions, true);
|
|
66
|
-
|
|
49
|
+
const kept = pkg.changes.filter((change) => change.startsWith(KEPT));
|
|
50
|
+
const moved = pkg.changes.filter((change) => !change.startsWith(KEPT));
|
|
51
|
+
for (const change of kept)
|
|
52
|
+
warn(out, `package.json: ${change}`);
|
|
53
|
+
if (moved.length > 0)
|
|
67
54
|
writes.push({ path: "package.json", value: pkg.pkg });
|
|
68
|
-
lines.push(...
|
|
55
|
+
lines.push(...moved.map((change) => `package.json: ${change}`));
|
|
69
56
|
const hooks = registerHooks(readJson(root, SETTINGS_FILE), versions.issueFlow);
|
|
70
57
|
if (hooks.changes.length > 0)
|
|
71
58
|
writes.push({ path: SETTINGS_FILE, value: hooks.settings });
|
package/dist/config.d.ts
CHANGED
|
@@ -6,6 +6,11 @@ export interface TypeRules {
|
|
|
6
6
|
labels: Record<string, string>;
|
|
7
7
|
titlePrefixes: Record<string, string>;
|
|
8
8
|
}
|
|
9
|
+
/** A label the flow wants in the repo: its color (hex, no `#`) and description. */
|
|
10
|
+
export interface LabelSpec {
|
|
11
|
+
color: string;
|
|
12
|
+
description: string;
|
|
13
|
+
}
|
|
9
14
|
/** The whole config, with every default applied. */
|
|
10
15
|
export interface FlowConfig {
|
|
11
16
|
repo: string | null;
|
|
@@ -30,6 +35,7 @@ export interface FlowConfig {
|
|
|
30
35
|
dirs: string[];
|
|
31
36
|
};
|
|
32
37
|
colors: Record<string, string>;
|
|
38
|
+
labels: Record<string, LabelSpec>;
|
|
33
39
|
}
|
|
34
40
|
/** The defaults, the same rules Abastio uses. */
|
|
35
41
|
export declare const DEFAULT_CONFIG: FlowConfig;
|
package/dist/config.js
CHANGED
|
@@ -49,6 +49,16 @@ export const DEFAULT_CONFIG = {
|
|
|
49
49
|
"P3:": "yellow",
|
|
50
50
|
"P4:": "cyan",
|
|
51
51
|
},
|
|
52
|
+
labels: {
|
|
53
|
+
bug: { color: "d73a4a", description: "Something isn't working" },
|
|
54
|
+
enhancement: { color: "a2eeef", description: "New feature or request" },
|
|
55
|
+
documentation: { color: "0075ca", description: "Improvements or additions to documentation" },
|
|
56
|
+
spike: { color: "fbca04", description: "Time-boxed research before building" },
|
|
57
|
+
"P1: urgent": { color: "B60205", description: "Breaks production or blocks work" },
|
|
58
|
+
"P2: important": { color: "F9D0C4", description: "Real impact, with some slack" },
|
|
59
|
+
"P3: minor": { color: "FEF2C0", description: "Default when in doubt" },
|
|
60
|
+
"P4: low": { color: "BFDADC", description: "Nice-to-have" },
|
|
61
|
+
},
|
|
52
62
|
};
|
|
53
63
|
/** The colors a Claude session takes with `/color`. */
|
|
54
64
|
export const SESSION_COLORS = ["red", "blue", "green", "yellow", "purple", "orange", "pink", "cyan"];
|
|
@@ -171,6 +181,55 @@ function colorsSection(value) {
|
|
|
171
181
|
}
|
|
172
182
|
return value;
|
|
173
183
|
}
|
|
184
|
+
/** A label color: six hex digits, without `#`. */
|
|
185
|
+
const LABEL_COLOR = /^[0-9a-fA-F]{6}$/;
|
|
186
|
+
/** A label name: no spaces at the ends, since GitHub trims them. */
|
|
187
|
+
const LABEL_NAME = /^\S(?:.*\S)?$/;
|
|
188
|
+
/** GitHub's limits for a label's name and description, in characters. */
|
|
189
|
+
const LABEL_NAME_MAX = 50;
|
|
190
|
+
const LABEL_DESCRIPTION_MAX = 100;
|
|
191
|
+
/**
|
|
192
|
+
* The `labels` map: the labels `labels sync` makes in the repo. Unlike the other maps, it
|
|
193
|
+
* merges over the default, because a repo usually adds labels: a name replaces the default
|
|
194
|
+
* of the same name (ignoring case, as GitHub does) and `null` drops it.
|
|
195
|
+
* @param value - The map, or undefined.
|
|
196
|
+
* @returns The labels.
|
|
197
|
+
* @throws ConfigError if a label is invalid.
|
|
198
|
+
* @example labelsSection({ spike: null, "area:api": { color: "1d76db" } })["area:api"].description // ""
|
|
199
|
+
*/
|
|
200
|
+
function labelsSection(value) {
|
|
201
|
+
if (value === undefined)
|
|
202
|
+
return DEFAULT_CONFIG.labels;
|
|
203
|
+
if (!isObject(value))
|
|
204
|
+
throw new ConfigError(`${CONFIG_PATH}: "labels" must be an object.`);
|
|
205
|
+
// NOTE: a Map, so a "__proto__" key from JSON.parse stays a label and not the prototype.
|
|
206
|
+
const labels = new Map(Object.entries(DEFAULT_CONFIG.labels));
|
|
207
|
+
const seen = new Set();
|
|
208
|
+
for (const [name, spec] of Object.entries(value)) {
|
|
209
|
+
const where = `labels.${name}`;
|
|
210
|
+
if (!LABEL_NAME.test(name) || [...name].length > LABEL_NAME_MAX)
|
|
211
|
+
throw new ConfigError(`${CONFIG_PATH}: "${where}" needs a name of 1 to ${LABEL_NAME_MAX} characters, without spaces at the ends.`);
|
|
212
|
+
const key = name.toLowerCase();
|
|
213
|
+
if (seen.has(key))
|
|
214
|
+
throw new ConfigError(`${CONFIG_PATH}: "labels" has "${name}" twice (names ignore case).`);
|
|
215
|
+
seen.add(key);
|
|
216
|
+
for (const known of [...labels.keys()])
|
|
217
|
+
if (known.toLowerCase() === key)
|
|
218
|
+
labels.delete(known);
|
|
219
|
+
if (spec === null)
|
|
220
|
+
continue;
|
|
221
|
+
if (!isObject(spec))
|
|
222
|
+
throw new ConfigError(`${CONFIG_PATH}: "${where}" must be an object or null.`);
|
|
223
|
+
onlyKeys(spec, ["color", "description"], where);
|
|
224
|
+
if (typeof spec.color !== "string" || !LABEL_COLOR.test(spec.color))
|
|
225
|
+
throw new ConfigError(`${CONFIG_PATH}: "${where}.color" must be six hex digits, without #, such as "1d76db".`);
|
|
226
|
+
const description = spec.description ?? "";
|
|
227
|
+
if (typeof description !== "string" || [...description].length > LABEL_DESCRIPTION_MAX)
|
|
228
|
+
throw new ConfigError(`${CONFIG_PATH}: "${where}.description" must be a string of up to ${LABEL_DESCRIPTION_MAX} characters.`);
|
|
229
|
+
labels.set(name, { color: spec.color, description });
|
|
230
|
+
}
|
|
231
|
+
return Object.fromEntries(labels);
|
|
232
|
+
}
|
|
174
233
|
/**
|
|
175
234
|
* Validates a parsed config and fills in the defaults.
|
|
176
235
|
* @param raw - The parsed JSON.
|
|
@@ -181,7 +240,7 @@ function colorsSection(value) {
|
|
|
181
240
|
export function parseConfig(raw) {
|
|
182
241
|
if (!isObject(raw))
|
|
183
242
|
throw new ConfigError(`${CONFIG_PATH} must be a JSON object.`);
|
|
184
|
-
onlyKeys(raw, ["repo", "baseBranch", "branch", "worktrees", "start", "plans", "colors"], "");
|
|
243
|
+
onlyKeys(raw, ["repo", "baseBranch", "branch", "worktrees", "start", "plans", "colors", "labels"], "");
|
|
185
244
|
const repo = text(raw.repo, "repo", null);
|
|
186
245
|
if (repo !== null && !REPO.test(repo))
|
|
187
246
|
throw new ConfigError(`${CONFIG_PATH}: "repo" must be owner/repo.`);
|
|
@@ -193,6 +252,7 @@ export function parseConfig(raw) {
|
|
|
193
252
|
start: textSection(raw.start, "start", DEFAULT_CONFIG.start),
|
|
194
253
|
plans: plansSection(raw.plans),
|
|
195
254
|
colors: colorsSection(raw.colors),
|
|
255
|
+
labels: labelsSection(raw.labels),
|
|
196
256
|
};
|
|
197
257
|
}
|
|
198
258
|
/**
|
package/dist/package-edits.d.ts
CHANGED
|
@@ -2,16 +2,46 @@
|
|
|
2
2
|
* @file The repo's `package.json` as the flow needs it: `issue-flow`, `github-rest` and
|
|
3
3
|
* `skills-manager` as exact dev dependencies (the skills call them with `npx`, which then
|
|
4
4
|
* uses the repo's copy), and the scripts to start an issue, prune worktrees and review a PR.
|
|
5
|
-
* A dependency or script the repo already has with another value stays as it is
|
|
5
|
+
* A dependency or script the repo already has with another value stays as it is, except
|
|
6
|
+
* that `upgrade` moves `issue-flow` to the version that runs and raises the other two only
|
|
7
|
+
* when they fall short of it: a newer version or a range that allows it is never moved back.
|
|
6
8
|
*/
|
|
7
|
-
import type
|
|
9
|
+
import { type OwnVersions } from "./setup-files.js";
|
|
8
10
|
/**
|
|
9
|
-
*
|
|
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 was tested with, so
|
|
21
|
+
* `upgrade` keeps 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 was tested with.
|
|
26
|
+
* @returns True to keep it.
|
|
27
|
+
* @example reaches("1.3.0", "1.2.0") // true
|
|
28
|
+
*/
|
|
29
|
+
export declare function reaches(current: string, tested: string): boolean;
|
|
30
|
+
/** A dependency the flow pins: its version, and whether `upgrade` keeps one that reaches it. */
|
|
31
|
+
interface Pin {
|
|
32
|
+
version: string;
|
|
33
|
+
keepsReaching: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** How a change that keeps what the repo had starts. */
|
|
36
|
+
export declare const KEPT = "kept ";
|
|
37
|
+
/**
|
|
38
|
+
* The dependencies the flow pins, with their versions. `issue-flow` always follows the
|
|
39
|
+
* version that runs; the other two are a floor, so `upgrade` keeps what reaches it.
|
|
10
40
|
* @param versions - This package's versions.
|
|
11
|
-
* @returns Package to
|
|
12
|
-
* @example pinnedPackages(versions)["@ramon-sg/issue-flow"] // "0.1.0"
|
|
41
|
+
* @returns Package to pin.
|
|
42
|
+
* @example pinnedPackages(versions)["@ramon-sg/issue-flow"].version // "0.1.0"
|
|
13
43
|
*/
|
|
14
|
-
export declare function pinnedPackages(versions: OwnVersions): Record<string,
|
|
44
|
+
export declare function pinnedPackages(versions: OwnVersions): Record<string, Pin>;
|
|
15
45
|
/**
|
|
16
46
|
* The version of a package in the repo, as a dependency or a dev dependency.
|
|
17
47
|
* @param pkg - The package.json.
|
|
@@ -33,3 +63,4 @@ export declare function withFlowPackages(pkg: Record<string, unknown> | null, ve
|
|
|
33
63
|
pkg: Record<string, unknown>;
|
|
34
64
|
changes: string[];
|
|
35
65
|
};
|
|
66
|
+
export {};
|
package/dist/package-edits.js
CHANGED
|
@@ -1,3 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The repo's `package.json` as the flow needs it: `issue-flow`, `github-rest` and
|
|
3
|
+
* `skills-manager` as exact dev dependencies (the skills call them with `npx`, which then
|
|
4
|
+
* uses the repo's copy), and the scripts to start an issue, prune worktrees and review a PR.
|
|
5
|
+
* A dependency or script the repo already has with another value stays as it is, except
|
|
6
|
+
* that `upgrade` moves `issue-flow` to the version that runs and raises the other two only
|
|
7
|
+
* when they fall short of it: a newer version or a range that allows it is never moved back.
|
|
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 was tested with, so
|
|
75
|
+
* `upgrade` keeps 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 was tested with.
|
|
80
|
+
* @returns True to keep 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 ";
|
|
1
101
|
/** The scripts `init` adds. */
|
|
2
102
|
const FLOW_SCRIPTS = {
|
|
3
103
|
"issue:start": "issue-flow issue start",
|
|
@@ -5,16 +105,17 @@ const FLOW_SCRIPTS = {
|
|
|
5
105
|
"pr:worktree": "issue-flow pr worktree",
|
|
6
106
|
};
|
|
7
107
|
/**
|
|
8
|
-
* The dependencies the flow pins, with their versions.
|
|
108
|
+
* The dependencies the flow pins, with their versions. `issue-flow` always follows the
|
|
109
|
+
* version that runs; the other two are a floor, so `upgrade` keeps what reaches it.
|
|
9
110
|
* @param versions - This package's versions.
|
|
10
|
-
* @returns Package to
|
|
11
|
-
* @example pinnedPackages(versions)["@ramon-sg/issue-flow"] // "0.1.0"
|
|
111
|
+
* @returns Package to pin.
|
|
112
|
+
* @example pinnedPackages(versions)["@ramon-sg/issue-flow"].version // "0.1.0"
|
|
12
113
|
*/
|
|
13
114
|
export function pinnedPackages(versions) {
|
|
14
115
|
return {
|
|
15
|
-
|
|
16
|
-
"@ramon-sg/github-rest": versions.githubRest,
|
|
17
|
-
"@ramon-sg/skills-manager": versions.skillsManager,
|
|
116
|
+
[PACKAGE]: { version: versions.issueFlow, keepsReaching: false },
|
|
117
|
+
"@ramon-sg/github-rest": { version: versions.githubRest, keepsReaching: true },
|
|
118
|
+
"@ramon-sg/skills-manager": { version: versions.skillsManager, keepsReaching: true },
|
|
18
119
|
};
|
|
19
120
|
}
|
|
20
121
|
/**
|
|
@@ -55,12 +156,12 @@ export function withFlowPackages(pkg, versions, move) {
|
|
|
55
156
|
const next = pkg === null ? { private: true } : { ...pkg };
|
|
56
157
|
const scripts = { ...(next.scripts ?? {}) };
|
|
57
158
|
const changes = [];
|
|
58
|
-
for (const [name, version] of Object.entries(pinnedPackages(versions))) {
|
|
159
|
+
for (const [name, { version, keepsReaching }] of Object.entries(pinnedPackages(versions))) {
|
|
59
160
|
const current = dependencyOf(next, name);
|
|
60
161
|
if (current?.version === version)
|
|
61
162
|
continue;
|
|
62
|
-
if (current !== null && !move) {
|
|
63
|
-
changes.push(
|
|
163
|
+
if (current !== null && (!move || (keepsReaching && reaches(current.version, version)))) {
|
|
164
|
+
changes.push(`${KEPT}${name} ${current.version} (issue-flow ${versions.issueFlow} was tested with ${version})`);
|
|
64
165
|
continue;
|
|
65
166
|
}
|
|
66
167
|
const field = current?.field ?? "devDependencies";
|
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 init [--yes] [--recipe <name>,...]\n issue-flow doctor [--json]\n issue-flow upgrade [--dry-run]\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\ninit sets the flow up in the repo: .agents/flow.json (with the recipes its files suggest),\nthe dev dependencies and scripts, and the hooks. doctor checks them at this version, and\nupgrade moves them to this version. npx agent-kitchen runs the three, along with the\nskills, claude-cloud and claude-repo-guard.";
|
|
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 init [--yes] [--recipe <name>,...]\n issue-flow doctor [--json]\n issue-flow upgrade [--dry-run]\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\ninit sets the flow up in the repo: .agents/flow.json (with the recipes its files suggest),\nthe dev dependencies and scripts, and the hooks. doctor checks them at this version, and\nupgrade moves them to this version. npx agent-kitchen apply runs the three, along with the\nskills, claude-cloud and claude-repo-guard.\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
|
@@ -21,6 +21,7 @@ export const USAGE = `Usage:
|
|
|
21
21
|
issue-flow init [--yes] [--recipe <name>,...]
|
|
22
22
|
issue-flow doctor [--json]
|
|
23
23
|
issue-flow upgrade [--dry-run]
|
|
24
|
+
issue-flow labels sync [--update] [--dry-run] [--json] [-R owner/repo]
|
|
24
25
|
|
|
25
26
|
The slug comes from the issue's "Slug de rama" block, then from Claude, then from the title,
|
|
26
27
|
and the branch from the repo's template (an older "Rama sugerida" block is used if it fits).
|
|
@@ -50,5 +51,9 @@ Code's WorktreeCreate and WorktreeRemove for claude --worktree.
|
|
|
50
51
|
|
|
51
52
|
init sets the flow up in the repo: .agents/flow.json (with the recipes its files suggest),
|
|
52
53
|
the dev dependencies and scripts, and the hooks. doctor checks them at this version, and
|
|
53
|
-
upgrade moves them to this version. npx agent-kitchen runs the three, along with the
|
|
54
|
-
skills, claude-cloud and claude-repo-guard
|
|
54
|
+
upgrade moves them to this version. npx agent-kitchen apply runs the three, along with the
|
|
55
|
+
skills, claude-cloud and claude-repo-guard.
|
|
56
|
+
|
|
57
|
+
labels sync makes in GitHub the labels the flow wants ("labels" in .agents/flow.json: the
|
|
58
|
+
issue types and P1 to P4 by default) that the repo lacks. Names ignore case and are never
|
|
59
|
+
renamed nor deleted; --update also fixes the color and description of those that differ.`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ramon-sg/issue-flow",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "Issue-to-PR workflow for any repo: names each issue's branch, creates its worktree, opens the draft PR and launches Claude Code, locally and in Claude's cloud.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/schema.json
CHANGED
|
@@ -267,6 +267,50 @@
|
|
|
267
267
|
"P3:": "yellow",
|
|
268
268
|
"P4:": "cyan"
|
|
269
269
|
}
|
|
270
|
+
},
|
|
271
|
+
"labels": {
|
|
272
|
+
"description": "Labels that `issue-flow labels sync` makes in GitHub when the repo lacks them. Merges over the default: a name replaces the default of the same name (ignoring case) and null drops it.",
|
|
273
|
+
"type": "object",
|
|
274
|
+
"propertyNames": {
|
|
275
|
+
"pattern": "^\\S(?:.*\\S)?$",
|
|
276
|
+
"maxLength": 50
|
|
277
|
+
},
|
|
278
|
+
"additionalProperties": {
|
|
279
|
+
"oneOf": [
|
|
280
|
+
{
|
|
281
|
+
"type": "null"
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
"type": "object",
|
|
285
|
+
"additionalProperties": false,
|
|
286
|
+
"required": [
|
|
287
|
+
"color"
|
|
288
|
+
],
|
|
289
|
+
"properties": {
|
|
290
|
+
"color": {
|
|
291
|
+
"description": "Six hex digits, without #.",
|
|
292
|
+
"type": "string",
|
|
293
|
+
"pattern": "^[0-9a-fA-F]{6}$"
|
|
294
|
+
},
|
|
295
|
+
"description": {
|
|
296
|
+
"type": "string",
|
|
297
|
+
"maxLength": 100,
|
|
298
|
+
"default": ""
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
]
|
|
303
|
+
},
|
|
304
|
+
"default": {
|
|
305
|
+
"bug": { "color": "d73a4a", "description": "Something isn't working" },
|
|
306
|
+
"enhancement": { "color": "a2eeef", "description": "New feature or request" },
|
|
307
|
+
"documentation": { "color": "0075ca", "description": "Improvements or additions to documentation" },
|
|
308
|
+
"spike": { "color": "fbca04", "description": "Time-boxed research before building" },
|
|
309
|
+
"P1: urgent": { "color": "B60205", "description": "Breaks production or blocks work" },
|
|
310
|
+
"P2: important": { "color": "F9D0C4", "description": "Real impact, with some slack" },
|
|
311
|
+
"P3: minor": { "color": "FEF2C0", "description": "Default when in doubt" },
|
|
312
|
+
"P4: low": { "color": "BFDADC", "description": "Nice-to-have" }
|
|
313
|
+
}
|
|
270
314
|
}
|
|
271
315
|
},
|
|
272
316
|
"$defs": {
|