@ramon-sg/issue-flow 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +304 -2
- package/dist/ask.d.ts +28 -0
- package/dist/ask.js +73 -0
- package/dist/branch-name.d.ts +113 -0
- package/dist/branch-name.js +226 -0
- package/dist/claude-namer.d.ts +51 -0
- package/dist/claude-namer.js +140 -0
- package/dist/cli-setup.d.ts +22 -0
- package/dist/cli-setup.js +26 -0
- package/dist/cli-worktree.d.ts +74 -0
- package/dist/cli-worktree.js +123 -0
- package/dist/cli.d.ts +132 -0
- package/dist/cli.js +348 -0
- package/dist/commands/adapter.d.ts +49 -0
- package/dist/commands/adapter.js +231 -0
- package/dist/commands/claude-hooks.d.ts +37 -0
- package/dist/commands/claude-hooks.js +162 -0
- package/dist/commands/doctor.d.ts +10 -0
- package/dist/commands/doctor.js +124 -0
- package/dist/commands/init.d.ts +27 -0
- package/dist/commands/init.js +213 -0
- package/dist/commands/issue-branch.d.ts +36 -0
- package/dist/commands/issue-branch.js +182 -0
- package/dist/commands/issue-start.d.ts +11 -0
- package/dist/commands/issue-start.js +361 -0
- package/dist/commands/pr-worktree.d.ts +31 -0
- package/dist/commands/pr-worktree.js +122 -0
- package/dist/commands/session.d.ts +15 -0
- package/dist/commands/session.js +102 -0
- package/dist/commands/upgrade.d.ts +16 -0
- package/dist/commands/upgrade.js +115 -0
- package/dist/commands/worktree.d.ts +82 -0
- package/dist/commands/worktree.js +244 -0
- package/dist/config-base.d.ts +32 -0
- package/dist/config-base.js +47 -0
- package/dist/config.d.ts +63 -0
- package/dist/config.js +230 -0
- package/dist/docker.d.ts +53 -0
- package/dist/docker.js +153 -0
- package/dist/git.d.ts +130 -0
- package/dist/git.js +192 -0
- package/dist/guard-config.d.ts +16 -0
- package/dist/guard-config.js +38 -0
- package/dist/issue-text.d.ts +128 -0
- package/dist/issue-text.js +210 -0
- package/dist/launch.d.ts +90 -0
- package/dist/launch.js +158 -0
- package/dist/output.d.ts +33 -0
- package/dist/output.js +37 -0
- package/dist/package-edits.d.ts +35 -0
- package/dist/package-edits.js +78 -0
- package/dist/plan-doc.d.ts +26 -0
- package/dist/plan-doc.js +93 -0
- package/dist/ports.d.ts +90 -0
- package/dist/ports.js +257 -0
- package/dist/propose.d.ts +42 -0
- package/dist/propose.js +111 -0
- package/dist/recipes.d.ts +43 -0
- package/dist/recipes.js +123 -0
- package/dist/session-lock.d.ts +32 -0
- package/dist/session-lock.js +69 -0
- package/dist/settings-hooks.d.ts +31 -0
- package/dist/settings-hooks.js +133 -0
- package/dist/setup-files.d.ts +89 -0
- package/dist/setup-files.js +138 -0
- package/dist/start-modes.d.ts +162 -0
- package/dist/start-modes.js +277 -0
- package/dist/start-place.d.ts +61 -0
- package/dist/start-place.js +301 -0
- package/dist/start-state.d.ts +72 -0
- package/dist/start-state.js +13 -0
- package/dist/start-summary.d.ts +42 -0
- package/dist/start-summary.js +217 -0
- package/dist/stop-apps.d.ts +14 -0
- package/dist/stop-apps.js +127 -0
- package/dist/usage.d.ts +5 -0
- package/dist/usage.js +54 -0
- package/dist/work-state.d.ts +51 -0
- package/dist/work-state.js +58 -0
- package/dist/worktree-config.d.ts +35 -0
- package/dist/worktree-config.js +139 -0
- package/dist/worktree-steps.d.ts +53 -0
- package/dist/worktree-steps.js +210 -0
- package/package.json +20 -4
- package/recipes/bun.json +5 -0
- package/recipes/direnv.json +17 -0
- package/recipes/docker-compose.json +30 -0
- package/recipes/npm.json +5 -0
- package/recipes/pnpm.json +5 -0
- package/recipes/staging-main.json +5 -0
- package/schema.json +281 -0
|
@@ -0,0 +1,213 @@
|
|
|
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. Then, each after asking, it
|
|
6
|
+
* runs `claude-cloud init`, installs `claude-repo-guard` with what the flow needs in its
|
|
7
|
+
* options, and opens the skills menu of `skills-manager` with the two catalogs (`init` if
|
|
8
|
+
* the repo has no manifest, `add` if it has one, because `init` stops then). A tool that
|
|
9
|
+
* fails is reported and the rest goes on.
|
|
10
|
+
*/
|
|
11
|
+
import { confirm } from "../ask.js";
|
|
12
|
+
import { CONFIG_PATH, isObject } from "../config-base.js";
|
|
13
|
+
import { parseConfig } from "../config.js";
|
|
14
|
+
import { checkoutRoot, locateRepo } from "../git.js";
|
|
15
|
+
import { guardOptions } from "../guard-config.js";
|
|
16
|
+
import { UsageError, warn } from "../output.js";
|
|
17
|
+
import { withFlowPackages } from "../package-edits.js";
|
|
18
|
+
import { applyRecipe, detectedRecipes, loadRecipes, projectPrefix } from "../recipes.js";
|
|
19
|
+
import { registerHooks } from "../settings-hooks.js";
|
|
20
|
+
import { CLOUD_SPEC, FLOW_CATALOG, GENERAL_CATALOG, GUARD_CONFIG, GUARD_SPEC, ownVersions, packageManager, readJson, runNpx, SETTINGS_FILE, SetupError, SKILLS_MANIFEST, writeJson, } from "../setup-files.js";
|
|
21
|
+
/** The skills picked without a terminal: the flow's, and the audit `close-pr` uses. */
|
|
22
|
+
const DEFAULT_SKILLS = [
|
|
23
|
+
...["github-issues", "start-issue", "close-pr", "pr-review-comments", "pr-worktree"].map((name) => `catalog:${FLOW_CATALOG}/${name}`),
|
|
24
|
+
`catalog:${GENERAL_CATALOG}/code-audit`,
|
|
25
|
+
];
|
|
26
|
+
/**
|
|
27
|
+
* The URL of the config's schema at a version.
|
|
28
|
+
* @param version - This package's version.
|
|
29
|
+
* @returns The URL.
|
|
30
|
+
* @example schemaUrl("0.1.0") // "https://raw.githubusercontent.com/ramon-sg/issue-flow/v0.1.0/schema.json"
|
|
31
|
+
*/
|
|
32
|
+
export function schemaUrl(version) {
|
|
33
|
+
return `https://raw.githubusercontent.com/${FLOW_CATALOG}/v${version}/schema.json`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A new config: the schema, and the branch rules of `paseo.json` if the repo has one.
|
|
37
|
+
* @param root - The repo.
|
|
38
|
+
* @param version - This package's version.
|
|
39
|
+
* @returns The config and what it took from `paseo.json`.
|
|
40
|
+
*/
|
|
41
|
+
function freshConfig(root, version) {
|
|
42
|
+
const raw = { $schema: schemaUrl(version) };
|
|
43
|
+
const issues = readJson(root, "paseo.json")?.project;
|
|
44
|
+
const paseo = isObject(issues) && isObject(issues.issues) ? issues.issues : null;
|
|
45
|
+
if (paseo === null)
|
|
46
|
+
return { raw, changes: ["new"] };
|
|
47
|
+
const branch = {};
|
|
48
|
+
if (typeof paseo.branchTemplate === "string")
|
|
49
|
+
branch.template = paseo.branchTemplate;
|
|
50
|
+
if (isObject(paseo.types))
|
|
51
|
+
branch.types = paseo.types;
|
|
52
|
+
if (Object.keys(branch).length > 0)
|
|
53
|
+
raw.branch = branch;
|
|
54
|
+
return { raw, changes: ["new, with the branch rules of paseo.json"] };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The recipes to apply: the ones named with `--recipe`, or each one the repo's files
|
|
58
|
+
* suggest, after asking (all of them with `--yes`).
|
|
59
|
+
* @param root - The repo.
|
|
60
|
+
* @param options - The options.
|
|
61
|
+
* @param out - Where to ask.
|
|
62
|
+
* @returns The recipes.
|
|
63
|
+
* @throws UsageError if `--recipe` names one that does not exist.
|
|
64
|
+
*/
|
|
65
|
+
async function pickRecipes(root, options, out) {
|
|
66
|
+
const recipes = loadRecipes();
|
|
67
|
+
if (options.recipes !== null) {
|
|
68
|
+
const unknown = options.recipes.find((name) => !recipes.some((recipe) => recipe.name === name));
|
|
69
|
+
if (unknown !== undefined)
|
|
70
|
+
throw new UsageError(`unknown recipe ${unknown}; there are ${recipes.map((recipe) => recipe.name).join(", ")}.`);
|
|
71
|
+
return recipes.filter((recipe) => options.recipes?.includes(recipe.name));
|
|
72
|
+
}
|
|
73
|
+
const picked = [];
|
|
74
|
+
for (const recipe of recipes.filter((item) => detectedRecipes(root, recipes).includes(item.name)))
|
|
75
|
+
if (options.yes || (await confirm(out, `Recipe ${recipe.name}: ${recipe.description} Add it?`)))
|
|
76
|
+
picked.push(recipe);
|
|
77
|
+
return picked;
|
|
78
|
+
}
|
|
79
|
+
/** How a line of a change that keeps what the repo had starts. */
|
|
80
|
+
const KEPT = "kept ";
|
|
81
|
+
/**
|
|
82
|
+
* What `init` would write: the config, `package.json` and the settings' hooks.
|
|
83
|
+
* @param root - The repo.
|
|
84
|
+
* @param versions - This package's versions.
|
|
85
|
+
* @param recipes - The recipes to apply.
|
|
86
|
+
* @returns The files that change.
|
|
87
|
+
* @throws ConfigError if the resulting config is invalid; SetupError if a file is broken.
|
|
88
|
+
*/
|
|
89
|
+
function plan(root, versions, recipes) {
|
|
90
|
+
const existing = readJson(root, CONFIG_PATH);
|
|
91
|
+
const start = existing === null ? freshConfig(root, versions.issueFlow) : { raw: existing, changes: [] };
|
|
92
|
+
let { raw } = start;
|
|
93
|
+
const configChanges = [...start.changes];
|
|
94
|
+
for (const recipe of recipes) {
|
|
95
|
+
const applied = applyRecipe(raw, recipe, projectPrefix(root));
|
|
96
|
+
raw = applied.raw;
|
|
97
|
+
configChanges.push(...applied.changes.map((change) => (change.startsWith(KEPT) ? `${change} (${recipe.name})` : `${recipe.name}: ${change}`)));
|
|
98
|
+
}
|
|
99
|
+
parseConfig(raw);
|
|
100
|
+
const pkg = withFlowPackages(readJson(root, "package.json"), versions, false);
|
|
101
|
+
const hooks = registerHooks(readJson(root, SETTINGS_FILE), versions.issueFlow);
|
|
102
|
+
const file = (path, value, lines) => ({
|
|
103
|
+
path,
|
|
104
|
+
value,
|
|
105
|
+
changes: lines.filter((line) => !line.startsWith(KEPT)).map((line) => `${path}: ${line}`),
|
|
106
|
+
kept: lines.filter((line) => line.startsWith(KEPT)).map((line) => `${path}: ${line}`),
|
|
107
|
+
});
|
|
108
|
+
return [file(CONFIG_PATH, raw, configChanges), file("package.json", pkg.pkg, pkg.changes), file(SETTINGS_FILE, hooks.settings, hooks.changes)];
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Runs one of the other tools after asking, and says if it failed.
|
|
112
|
+
* @param step - What it is and how it runs.
|
|
113
|
+
* @param options - The options.
|
|
114
|
+
* @param out - Where to ask and write.
|
|
115
|
+
* @returns False if it ran and failed.
|
|
116
|
+
*/
|
|
117
|
+
async function external(step, options, out) {
|
|
118
|
+
if (!options.yes && !(await confirm(out, step.question)))
|
|
119
|
+
return true;
|
|
120
|
+
if (step.run())
|
|
121
|
+
return true;
|
|
122
|
+
warn(out, step.failed);
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Installs `claude-repo-guard`, after writing in its options what the flow needs.
|
|
127
|
+
* @param root - The repo.
|
|
128
|
+
* @param out - Where to write.
|
|
129
|
+
* @returns Whether it installed.
|
|
130
|
+
*/
|
|
131
|
+
function installGuard(root, out) {
|
|
132
|
+
const flow = readJson(root, CONFIG_PATH);
|
|
133
|
+
const baseBranch = typeof flow?.baseBranch === "string" ? flow.baseBranch : null;
|
|
134
|
+
const { options, changes } = guardOptions(readJson(root, GUARD_CONFIG), baseBranch, process.platform);
|
|
135
|
+
if (changes.length > 0)
|
|
136
|
+
writeJson(root, GUARD_CONFIG, options);
|
|
137
|
+
for (const change of changes)
|
|
138
|
+
out.stdout(` ${change}\n`);
|
|
139
|
+
return runNpx(root, [GUARD_SPEC, "install", "--yes"]);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The ids of the default skills the manifest does not take yet: `skills-manager` leaves the
|
|
143
|
+
* ones it has out of the candidates, and fails if `--select` names one.
|
|
144
|
+
* @param manifest - The skills manifest, or null.
|
|
145
|
+
* @returns The ids to pick.
|
|
146
|
+
*/
|
|
147
|
+
function missingDefaults(manifest) {
|
|
148
|
+
const sources = Array.isArray(manifest?.sources) ? manifest.sources.filter(isObject) : [];
|
|
149
|
+
const taken = new Set(sources.flatMap((source) => (typeof source.catalog === "string" && Array.isArray(source.skills) ? source.skills.map((name) => `catalog:${String(source.catalog)}/${String(name)}`) : [])));
|
|
150
|
+
return DEFAULT_SKILLS.filter((id) => !taken.has(id));
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Opens the skills menu with the flow's catalog, at this version, and the general one; without
|
|
154
|
+
* a terminal or with `--yes`, it picks the flow's skills and `code-audit` that are not there yet.
|
|
155
|
+
* @param root - The repo.
|
|
156
|
+
* @param versions - This package's versions.
|
|
157
|
+
* @param pick - Whether to pick without the menu.
|
|
158
|
+
* @param out - Where to say there is nothing to add.
|
|
159
|
+
* @returns Whether `skills-manager` worked, or true if there was nothing to pick.
|
|
160
|
+
*/
|
|
161
|
+
function installSkills(root, versions, pick, out) {
|
|
162
|
+
const manifest = readJson(root, SKILLS_MANIFEST);
|
|
163
|
+
const command = manifest === null ? "init" : "add";
|
|
164
|
+
const args = [`@ramon-sg/skills-manager@${versions.skillsManager}`, command, "--catalog", `${FLOW_CATALOG}#v${versions.issueFlow}`, "--catalog", GENERAL_CATALOG];
|
|
165
|
+
if (!pick)
|
|
166
|
+
return runNpx(root, args);
|
|
167
|
+
const missing = missingDefaults(manifest);
|
|
168
|
+
if (missing.length === 0) {
|
|
169
|
+
out.stdout(`The flow's skills and code-audit are already in ${SKILLS_MANIFEST}.\n`);
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
return runNpx(root, [...args, "--select", missing.join(",")]);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Sets the repo up for the flow; see the file's header.
|
|
176
|
+
* @param options - The options.
|
|
177
|
+
* @param out - Where to write and ask.
|
|
178
|
+
* @returns 0, or 1 if a file was not written or another tool failed.
|
|
179
|
+
* @throws SetupError outside the main checkout or with a broken file; ConfigError if the
|
|
180
|
+
* config would be invalid; UsageError for an unknown recipe.
|
|
181
|
+
* @example await init({ cwd, yes: true, recipes: null, skills: true, guard: true, cloud: true }, out)
|
|
182
|
+
*/
|
|
183
|
+
export async function init(options, out) {
|
|
184
|
+
const located = locateRepo(options.cwd);
|
|
185
|
+
if (located === null)
|
|
186
|
+
throw new SetupError("run init inside a checkout of the repo.");
|
|
187
|
+
if (checkoutRoot(options.cwd) !== located.mainRoot)
|
|
188
|
+
throw new SetupError(`run init in the main checkout, ${located.mainRoot}, not in a worktree.`);
|
|
189
|
+
const root = located.mainRoot;
|
|
190
|
+
const versions = ownVersions();
|
|
191
|
+
const files = plan(root, versions, await pickRecipes(root, options, out)).filter((file) => file.changes.length > 0 || file.kept.length > 0);
|
|
192
|
+
for (const line of files.flatMap((file) => file.kept))
|
|
193
|
+
warn(out, line);
|
|
194
|
+
const lines = files.flatMap((file) => file.changes);
|
|
195
|
+
if (lines.length === 0)
|
|
196
|
+
out.stdout("The config, package.json and the hooks are already set up.\n");
|
|
197
|
+
else
|
|
198
|
+
out.stdout(`issue-flow ${versions.issueFlow} will write:\n${lines.map((line) => ` ${line}\n`).join("")}`);
|
|
199
|
+
if (lines.length > 0 && !options.yes && !(await confirm(out, "Write it?"))) {
|
|
200
|
+
warn(out, "nothing written.");
|
|
201
|
+
return 1;
|
|
202
|
+
}
|
|
203
|
+
for (const file of files.filter((item) => item.changes.length > 0))
|
|
204
|
+
writeJson(root, file.path, file.value);
|
|
205
|
+
const results = [
|
|
206
|
+
!options.cloud || await external({ question: "Set up Claude cloud sessions with claude-cloud init?", run: () => runNpx(root, [CLOUD_SPEC, "init"]), failed: `claude-cloud init failed; run 'npx ${CLOUD_SPEC} init' to retry.` }, options, out),
|
|
207
|
+
!options.guard || await external({ question: "Install claude-repo-guard (auto mode, with the sandbox)?", run: () => installGuard(root, out), failed: `claude-repo-guard did not install; run 'npx ${GUARD_SPEC} install' to retry.` }, options, out),
|
|
208
|
+
!options.skills || await external({ question: "Pick the skills now?", run: () => installSkills(root, versions, options.yes || !out.interactive, out), failed: "skills-manager failed; run issue-flow init again to retry." }, options, out),
|
|
209
|
+
];
|
|
210
|
+
const manager = packageManager(root, readJson(root, "package.json"));
|
|
211
|
+
out.stdout(`Next: run '${manager} install', and commit .agents/, .claude/ and package.json.\n`);
|
|
212
|
+
return results.every(Boolean) ? 0 : 1;
|
|
213
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { FlowConfig, Namer } from "../config.js";
|
|
2
|
+
import { type Output } from "../output.js";
|
|
3
|
+
/** Issues `--missing` proposes at most when no `--limit` is given. */
|
|
4
|
+
export declare const MISSING_LIMIT = 100;
|
|
5
|
+
/** Options of `issue branch`. */
|
|
6
|
+
export interface BranchOptions {
|
|
7
|
+
repo: string;
|
|
8
|
+
config: FlowConfig;
|
|
9
|
+
cwd: string;
|
|
10
|
+
namer?: Namer;
|
|
11
|
+
json: boolean;
|
|
12
|
+
limit?: number;
|
|
13
|
+
yes: boolean;
|
|
14
|
+
dryRun: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* `issue branch <n>...`: the branch of each issue, without writing anything.
|
|
18
|
+
* @param numbers - The issues.
|
|
19
|
+
* @param options - The options.
|
|
20
|
+
* @param out - Where to write.
|
|
21
|
+
* @returns The exit code: 1 if an issue does not exist.
|
|
22
|
+
* @throws GitHubError if GitHub fails.
|
|
23
|
+
*/
|
|
24
|
+
export declare function branchOf(numbers: number[], options: BranchOptions, out: Output): Promise<number>;
|
|
25
|
+
/**
|
|
26
|
+
* `issue branch --missing`: proposes the branch of every open issue without a block, new
|
|
27
|
+
* or old, in one Claude call, and adds the slug block after confirming. Only the slug is
|
|
28
|
+
* written, so a type Claude would pick is not used: the branch shown is the one the issue
|
|
29
|
+
* will give, and an issue without a type gets a warning if the template uses `{type}`. A failed edit does not stop the
|
|
30
|
+
* others.
|
|
31
|
+
* @param options - The options.
|
|
32
|
+
* @param out - Where to write.
|
|
33
|
+
* @returns The exit code: 1 if an edit failed.
|
|
34
|
+
* @throws GitHubError if the issues cannot be listed.
|
|
35
|
+
*/
|
|
36
|
+
export declare function branchMissing(options: BranchOptions, out: Output): Promise<number>;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file `issue-flow issue branch`: proposes the branch of some issues, or with `--missing`
|
|
3
|
+
* of every open issue without a branch block, and adds a "Slug de rama" block to those
|
|
4
|
+
* only after a confirmation (`--yes`, or answering in the terminal).
|
|
5
|
+
*/
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
7
|
+
import { editIssue, listIssues, viewIssue } from "@ramon-sg/github-rest";
|
|
8
|
+
import { hasBranchBlock, SLUG_HEADING, withSlugBlock } from "../branch-name.js";
|
|
9
|
+
import { proposeBranches } from "../propose.js";
|
|
10
|
+
import { emitJson, UsageError, warn } from "../output.js";
|
|
11
|
+
/** Issues `--missing` proposes at most when no `--limit` is given. */
|
|
12
|
+
export const MISSING_LIMIT = 100;
|
|
13
|
+
/**
|
|
14
|
+
* An issue of github-rest as what `proposeBranches` reads.
|
|
15
|
+
* @param issue - The issue.
|
|
16
|
+
* @returns Number, title, body and label names.
|
|
17
|
+
*/
|
|
18
|
+
function info(issue) {
|
|
19
|
+
return {
|
|
20
|
+
number: issue.number,
|
|
21
|
+
title: issue.title,
|
|
22
|
+
body: issue.body,
|
|
23
|
+
labels: issue.labels.map(({ name }) => name),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Prints the proposals: as JSON, or one line each (`#42 fix/42-cart (claude)`).
|
|
28
|
+
* @param proposals - The proposals.
|
|
29
|
+
* @param out - Where to write.
|
|
30
|
+
* @param json - Whether to print JSON.
|
|
31
|
+
*/
|
|
32
|
+
function print(proposals, out, json) {
|
|
33
|
+
if (json)
|
|
34
|
+
return emitJson(out, proposals);
|
|
35
|
+
for (const proposal of proposals)
|
|
36
|
+
out.stdout(`#${proposal.number} ${proposal.branch} (${proposal.source})\n`);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* `issue branch <n>...`: the branch of each issue, without writing anything.
|
|
40
|
+
* @param numbers - The issues.
|
|
41
|
+
* @param options - The options.
|
|
42
|
+
* @param out - Where to write.
|
|
43
|
+
* @returns The exit code: 1 if an issue does not exist.
|
|
44
|
+
* @throws GitHubError if GitHub fails.
|
|
45
|
+
*/
|
|
46
|
+
export async function branchOf(numbers, options, out) {
|
|
47
|
+
const issues = [];
|
|
48
|
+
let code = 0;
|
|
49
|
+
for (const number of numbers) {
|
|
50
|
+
const found = await viewIssue(options.repo, number);
|
|
51
|
+
if (found === null) {
|
|
52
|
+
warn(out, `issue #${number} does not exist in ${options.repo}.`);
|
|
53
|
+
code = 1;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
issues.push(info(found));
|
|
57
|
+
}
|
|
58
|
+
const { proposals, warnings } = proposeBranches(issues, options.config, {
|
|
59
|
+
cwd: options.cwd,
|
|
60
|
+
...(options.namer === undefined ? {} : { namer: options.namer }),
|
|
61
|
+
});
|
|
62
|
+
for (const line of [...warnings, ...proposals.flatMap((proposal) => proposal.warnings)])
|
|
63
|
+
warn(out, line);
|
|
64
|
+
print(proposals, out, options.json);
|
|
65
|
+
return code;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Asks in the terminal whether to write the blocks.
|
|
69
|
+
* @param count - How many issues.
|
|
70
|
+
* @returns True if the answer is yes.
|
|
71
|
+
*/
|
|
72
|
+
async function confirm(count) {
|
|
73
|
+
const prompt = createInterface({ input: process.stdin, output: process.stderr });
|
|
74
|
+
try {
|
|
75
|
+
const answer = await prompt.question(`Add the "Slug de rama" block to ${count} issue(s)? [y/N] `);
|
|
76
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
prompt.close();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Whether the blocks may be written: `--yes`, or a yes in the terminal. Never with
|
|
84
|
+
* `--dry-run`, and never without a terminal and without `--yes`.
|
|
85
|
+
* @param options - The options.
|
|
86
|
+
* @param count - How many issues.
|
|
87
|
+
* @param out - Where to write.
|
|
88
|
+
* @returns True if they may be written.
|
|
89
|
+
*/
|
|
90
|
+
async function mayWrite(options, count, out) {
|
|
91
|
+
if (options.dryRun)
|
|
92
|
+
return false;
|
|
93
|
+
if (options.yes)
|
|
94
|
+
return true;
|
|
95
|
+
if (!out.interactive) {
|
|
96
|
+
warn(out, "nothing written: pass --yes to add the blocks.");
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
return confirm(count);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Adds the block to one issue, from a copy read right before writing: Claude and the
|
|
103
|
+
* question can take minutes, and an edit made meanwhile must not be lost. If the issue got
|
|
104
|
+
* a block in the meantime, it is left alone.
|
|
105
|
+
* @param repo - `owner/repo`.
|
|
106
|
+
* @param proposal - The issue and its branch.
|
|
107
|
+
* @returns Whether the block was added (false if the issue already had one or is gone).
|
|
108
|
+
* @throws GitHubError if GitHub fails.
|
|
109
|
+
*/
|
|
110
|
+
async function addBlock(repo, proposal) {
|
|
111
|
+
const fresh = await viewIssue(repo, proposal.number);
|
|
112
|
+
if (fresh === null || hasBranchBlock(fresh.body))
|
|
113
|
+
return false;
|
|
114
|
+
await editIssue(repo, proposal.number, { body: withSlugBlock(fresh.body, proposal.slug) });
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The open issues without a block, new or old. GitHub can only skip one heading, so the
|
|
119
|
+
* pages are read without a limit (up to github-rest's page ceiling) and both are filtered
|
|
120
|
+
* here; otherwise issues with the old block would use up the limit and leave out, without
|
|
121
|
+
* a warning, issues that have none.
|
|
122
|
+
* @param options - The options (repo and limit).
|
|
123
|
+
* @param out - Where to warn that the list was cut short.
|
|
124
|
+
* @returns The issues, at most the limit.
|
|
125
|
+
* @throws GitHubError if the issues cannot be listed.
|
|
126
|
+
*/
|
|
127
|
+
async function missingIssues(options, out) {
|
|
128
|
+
const listed = await listIssues(options.repo, {
|
|
129
|
+
missingHeading: SLUG_HEADING,
|
|
130
|
+
limit: Number.POSITIVE_INFINITY,
|
|
131
|
+
});
|
|
132
|
+
const limit = options.limit ?? MISSING_LIMIT;
|
|
133
|
+
const issues = listed.issues.filter((issue) => !hasBranchBlock(issue.body));
|
|
134
|
+
if (listed.truncated || issues.length > limit)
|
|
135
|
+
warn(out, "GitHub has more issues than were read; run it again after this.");
|
|
136
|
+
return issues.slice(0, limit);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* `issue branch --missing`: proposes the branch of every open issue without a block, new
|
|
140
|
+
* or old, in one Claude call, and adds the slug block after confirming. Only the slug is
|
|
141
|
+
* written, so a type Claude would pick is not used: the branch shown is the one the issue
|
|
142
|
+
* will give, and an issue without a type gets a warning if the template uses `{type}`. A failed edit does not stop the
|
|
143
|
+
* others.
|
|
144
|
+
* @param options - The options.
|
|
145
|
+
* @param out - Where to write.
|
|
146
|
+
* @returns The exit code: 1 if an edit failed.
|
|
147
|
+
* @throws GitHubError if the issues cannot be listed.
|
|
148
|
+
*/
|
|
149
|
+
export async function branchMissing(options, out) {
|
|
150
|
+
if (options.json && !options.dryRun && !options.yes)
|
|
151
|
+
throw new UsageError("--missing --json needs --dry-run or --yes: there is no terminal to confirm in.");
|
|
152
|
+
const issues = await missingIssues(options, out);
|
|
153
|
+
const { proposals, warnings } = proposeBranches(issues.map(info), options.config, {
|
|
154
|
+
cwd: options.cwd,
|
|
155
|
+
claudeType: false,
|
|
156
|
+
...(options.namer === undefined ? {} : { namer: options.namer }),
|
|
157
|
+
});
|
|
158
|
+
const untyped = proposals
|
|
159
|
+
.filter((proposal) => proposal.type === null && options.config.branch.template.includes("{type}"))
|
|
160
|
+
.map((proposal) => `#${proposal.number}: it has no type label, so its branch has no type until it gets one.`);
|
|
161
|
+
for (const line of [...warnings, ...proposals.flatMap((proposal) => proposal.warnings), ...untyped])
|
|
162
|
+
warn(out, line);
|
|
163
|
+
print(proposals, out, options.json);
|
|
164
|
+
if (proposals.length === 0 || !(await mayWrite(options, proposals.length, out)))
|
|
165
|
+
return 0;
|
|
166
|
+
let code = 0;
|
|
167
|
+
for (const proposal of proposals) {
|
|
168
|
+
try {
|
|
169
|
+
if (!(await addBlock(options.repo, proposal))) {
|
|
170
|
+
warn(out, `#${proposal.number}: it got a branch block or was deleted meanwhile; left alone.`);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (!options.json)
|
|
174
|
+
out.stdout(`#${proposal.number}: block added.\n`);
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
warn(out, `#${proposal.number}: ${error instanceof Error ? error.message : String(error)}`);
|
|
178
|
+
code = 1;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return code;
|
|
182
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Output } from "../output.js";
|
|
2
|
+
import { type StartOptions } from "../start-state.js";
|
|
3
|
+
/**
|
|
4
|
+
* `issue start <n>`.
|
|
5
|
+
* @param options - The options.
|
|
6
|
+
* @param out - Where to write.
|
|
7
|
+
* @returns The exit code.
|
|
8
|
+
* @throws StartError if something stops the start; GitHubError if GitHub fails.
|
|
9
|
+
* @example await issueStart(options, processOutput())
|
|
10
|
+
*/
|
|
11
|
+
export declare function issueStart(options: StartOptions, out: Output): Promise<number>;
|