@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,31 @@
|
|
|
1
|
+
/** The hook events `init` registers. */
|
|
2
|
+
export declare const HOOK_EVENTS: string[];
|
|
3
|
+
/**
|
|
4
|
+
* The command of one of the hooks, at a version.
|
|
5
|
+
* @param event - The hook event.
|
|
6
|
+
* @param version - This package's version.
|
|
7
|
+
* @returns The command.
|
|
8
|
+
* @example hookCommand("WorktreeCreate", "0.1.0") // "npx --yes @ramon-sg/issue-flow@0.1.0 hook worktree-create"
|
|
9
|
+
*/
|
|
10
|
+
export declare function hookCommand(event: string, version: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Registers our hooks at a version: moves the version of the ones already there and adds
|
|
13
|
+
* the missing ones. In `SessionStart` it drops a `claude-cloud session` hook that is not
|
|
14
|
+
* ours.
|
|
15
|
+
* @param settings - The settings, or null if there is no file.
|
|
16
|
+
* @param version - This package's version.
|
|
17
|
+
* @returns The new settings and what changed, one line each.
|
|
18
|
+
* @throws SetupError if `hooks` or an event of it is not what Claude Code expects.
|
|
19
|
+
* @example registerHooks(null, "0.1.0").changes // ["added the SessionStart hook", …]
|
|
20
|
+
*/
|
|
21
|
+
export declare function registerHooks(settings: Record<string, unknown> | null, version: string): {
|
|
22
|
+
settings: Record<string, unknown>;
|
|
23
|
+
changes: string[];
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* The version of each of our hooks in the settings.
|
|
27
|
+
* @param settings - The settings, or null.
|
|
28
|
+
* @returns Event to version, or null if the hook is missing.
|
|
29
|
+
* @example hookVersions(settings).SessionStart // "0.1.0"
|
|
30
|
+
*/
|
|
31
|
+
export declare function hookVersions(settings: Record<string, unknown> | null): Record<string, string | null>;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The Claude Code hooks `init` registers in `.claude/settings.json`: `SessionStart`
|
|
3
|
+
* (`issue-flow session`) and `WorktreeCreate` and `WorktreeRemove` (`issue-flow hook …`), each
|
|
4
|
+
* pinned to this package's version. They are found again by their command, so running
|
|
5
|
+
* `init` twice changes nothing and `upgrade` only moves the version. The `SessionStart`
|
|
6
|
+
* command names `claude-cloud session`, which `issue-flow session` runs in the cloud: that
|
|
7
|
+
* way `claude-cloud init` sees it and does not add its own, and a hook of `claude-cloud`
|
|
8
|
+
* already there is dropped, so it does not run twice.
|
|
9
|
+
*/
|
|
10
|
+
import { isObject } from "./config-base.js";
|
|
11
|
+
import { PACKAGE, SETTINGS_FILE, SetupError } from "./setup-files.js";
|
|
12
|
+
/** Seconds each hook may take: the setup steps can install dependencies. */
|
|
13
|
+
const HOOK_TIMEOUT_SECONDS = 600;
|
|
14
|
+
/** What `claude-cloud init` looks for in a `SessionStart` command. */
|
|
15
|
+
const CLOUD_MARKER = "claude-cloud session";
|
|
16
|
+
/** The subcommand of each hook, and what follows it. */
|
|
17
|
+
const HOOK_COMMANDS = {
|
|
18
|
+
SessionStart: { sub: "session", note: ` # runs ${CLOUD_MARKER} in the cloud` },
|
|
19
|
+
WorktreeCreate: { sub: "hook worktree-create", note: "" },
|
|
20
|
+
WorktreeRemove: { sub: "hook worktree-remove", note: "" },
|
|
21
|
+
};
|
|
22
|
+
/** The hook events `init` registers. */
|
|
23
|
+
export const HOOK_EVENTS = Object.keys(HOOK_COMMANDS);
|
|
24
|
+
/**
|
|
25
|
+
* The command of one of the hooks, at a version.
|
|
26
|
+
* @param event - The hook event.
|
|
27
|
+
* @param version - This package's version.
|
|
28
|
+
* @returns The command.
|
|
29
|
+
* @example hookCommand("WorktreeCreate", "0.1.0") // "npx --yes @ramon-sg/issue-flow@0.1.0 hook worktree-create"
|
|
30
|
+
*/
|
|
31
|
+
export function hookCommand(event, version) {
|
|
32
|
+
const hook = HOOK_COMMANDS[event];
|
|
33
|
+
if (hook === undefined)
|
|
34
|
+
throw new SetupError(`unknown hook event ${event}.`);
|
|
35
|
+
return `npx --yes ${PACKAGE}@${version} ${hook.sub}${hook.note}`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The version of our hook in a command, or null if the command is not ours for that event.
|
|
39
|
+
* @param command - The command.
|
|
40
|
+
* @param event - The event.
|
|
41
|
+
* @returns The version.
|
|
42
|
+
*/
|
|
43
|
+
function ourVersion(command, event) {
|
|
44
|
+
const sub = HOOK_COMMANDS[event]?.sub;
|
|
45
|
+
if (typeof command !== "string" || sub === undefined)
|
|
46
|
+
return null;
|
|
47
|
+
const match = new RegExp(`${PACKAGE.replace("/", "\\/")}@(\\S+) ${sub}(?:\\s|$)`).exec(command);
|
|
48
|
+
return match?.[1] ?? null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The entries of an event in the settings, checked.
|
|
52
|
+
* @param hooks - The `hooks` object.
|
|
53
|
+
* @param event - The event.
|
|
54
|
+
* @returns Its entries (a new array if there were none).
|
|
55
|
+
* @throws SetupError if they are not a list of objects.
|
|
56
|
+
*/
|
|
57
|
+
function entriesOf(hooks, event) {
|
|
58
|
+
const entries = hooks[event] ?? [];
|
|
59
|
+
if (!Array.isArray(entries) || !entries.every(isObject))
|
|
60
|
+
throw new SetupError(`${SETTINGS_FILE}: hooks.${event} is not a list of entries; fix it and run again.`);
|
|
61
|
+
return entries;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The hooks of an entry.
|
|
65
|
+
* @param entry - The entry.
|
|
66
|
+
* @returns Its hooks, or an empty list.
|
|
67
|
+
*/
|
|
68
|
+
function hooksOf(entry) {
|
|
69
|
+
return Array.isArray(entry.hooks) ? entry.hooks.filter(isObject) : [];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Registers our hooks at a version: moves the version of the ones already there and adds
|
|
73
|
+
* the missing ones. In `SessionStart` it drops a `claude-cloud session` hook that is not
|
|
74
|
+
* ours.
|
|
75
|
+
* @param settings - The settings, or null if there is no file.
|
|
76
|
+
* @param version - This package's version.
|
|
77
|
+
* @returns The new settings and what changed, one line each.
|
|
78
|
+
* @throws SetupError if `hooks` or an event of it is not what Claude Code expects.
|
|
79
|
+
* @example registerHooks(null, "0.1.0").changes // ["added the SessionStart hook", …]
|
|
80
|
+
*/
|
|
81
|
+
export function registerHooks(settings, version) {
|
|
82
|
+
const next = { ...(settings ?? {}) };
|
|
83
|
+
const hooks = next.hooks ?? {};
|
|
84
|
+
if (!isObject(hooks))
|
|
85
|
+
throw new SetupError(`${SETTINGS_FILE}: "hooks" is not an object; fix it and run again.`);
|
|
86
|
+
next.hooks = { ...hooks };
|
|
87
|
+
const changes = [];
|
|
88
|
+
for (const event of HOOK_EVENTS) {
|
|
89
|
+
const entries = entriesOf(hooks, event);
|
|
90
|
+
const command = hookCommand(event, version);
|
|
91
|
+
let found = false;
|
|
92
|
+
const kept = entries.map((entry) => {
|
|
93
|
+
const inner = hooksOf(entry).flatMap((hook) => {
|
|
94
|
+
const current = ourVersion(hook.command, event);
|
|
95
|
+
if (current !== null) {
|
|
96
|
+
found = true;
|
|
97
|
+
if (hook.command === command)
|
|
98
|
+
return [hook];
|
|
99
|
+
changes.push(`moved the ${event} hook from ${current} to ${version}`);
|
|
100
|
+
return [{ ...hook, command }];
|
|
101
|
+
}
|
|
102
|
+
if (event === "SessionStart" && typeof hook.command === "string" && hook.command.includes(CLOUD_MARKER)) {
|
|
103
|
+
changes.push(`dropped the ${CLOUD_MARKER} SessionStart hook: issue-flow session runs it`);
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
return [hook];
|
|
107
|
+
});
|
|
108
|
+
return Array.isArray(entry.hooks) ? { ...entry, hooks: inner } : entry;
|
|
109
|
+
}).filter((entry) => !Array.isArray(entry.hooks) || entry.hooks.length > 0);
|
|
110
|
+
if (!found) {
|
|
111
|
+
kept.push({ hooks: [{ type: "command", command, timeout: HOOK_TIMEOUT_SECONDS }] });
|
|
112
|
+
changes.push(`added the ${event} hook`);
|
|
113
|
+
}
|
|
114
|
+
next.hooks[event] = kept;
|
|
115
|
+
}
|
|
116
|
+
return { settings: next, changes };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The version of each of our hooks in the settings.
|
|
120
|
+
* @param settings - The settings, or null.
|
|
121
|
+
* @returns Event to version, or null if the hook is missing.
|
|
122
|
+
* @example hookVersions(settings).SessionStart // "0.1.0"
|
|
123
|
+
*/
|
|
124
|
+
export function hookVersions(settings) {
|
|
125
|
+
const hooks = isObject(settings?.hooks) ? settings.hooks : {};
|
|
126
|
+
const versions = {};
|
|
127
|
+
for (const event of HOOK_EVENTS) {
|
|
128
|
+
const entries = Array.isArray(hooks[event]) ? hooks[event].filter(isObject) : [];
|
|
129
|
+
const commands = entries.flatMap((entry) => hooksOf(entry).map((hook) => hook.command));
|
|
130
|
+
versions[event] = commands.map((command) => ourVersion(command, event)).find((found) => found !== null) ?? null;
|
|
131
|
+
}
|
|
132
|
+
return versions;
|
|
133
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** This package's name on npm. */
|
|
2
|
+
export declare const PACKAGE = "@ramon-sg/issue-flow";
|
|
3
|
+
/** The GitHub repo of this package, which is also the catalog of the flow skills. */
|
|
4
|
+
export declare const FLOW_CATALOG = "ramon-sg/issue-flow";
|
|
5
|
+
/** The catalog of the general skills. */
|
|
6
|
+
export declare const GENERAL_CATALOG = "ramon-sg/agent-skills";
|
|
7
|
+
/** Where `skills-manager` keeps the repo's skills manifest. */
|
|
8
|
+
export declare const SKILLS_MANIFEST = ".agents/skills.json";
|
|
9
|
+
/** Where `skills-manager` keeps the repo's skills lock. */
|
|
10
|
+
export declare const SKILLS_LOCK = ".agents/skills.lock.json";
|
|
11
|
+
/** Claude Code's settings of the repo. */
|
|
12
|
+
export declare const SETTINGS_FILE = ".claude/settings.json";
|
|
13
|
+
/** The options of `claude-repo-guard` in the repo. */
|
|
14
|
+
export declare const GUARD_CONFIG = ".claude/repo-guard.config.json";
|
|
15
|
+
/** How `claude-repo-guard` runs: it is not on npm. */
|
|
16
|
+
export declare const GUARD_SPEC = "github:ramon-sg/claude-repo-guard";
|
|
17
|
+
/** How `claude-cloud` runs. */
|
|
18
|
+
export declare const CLOUD_SPEC = "@ramon-sg/claude-cloud@1";
|
|
19
|
+
/** Error of `init`, `doctor` or `upgrade`: exits 1 with the message. */
|
|
20
|
+
export declare class SetupError extends Error {
|
|
21
|
+
}
|
|
22
|
+
/** The versions this package pins in the repo. */
|
|
23
|
+
export interface OwnVersions {
|
|
24
|
+
issueFlow: string;
|
|
25
|
+
githubRest: string;
|
|
26
|
+
skillsManager: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* This package's version and the exact versions of `github-rest` and `skills-manager` it
|
|
30
|
+
* was built and tested with, from its own `package.json`.
|
|
31
|
+
* @returns The versions.
|
|
32
|
+
* @throws SetupError if the package.json does not have them.
|
|
33
|
+
* @example ownVersions().issueFlow // "0.1.0"
|
|
34
|
+
*/
|
|
35
|
+
export declare function ownVersions(): OwnVersions;
|
|
36
|
+
/**
|
|
37
|
+
* Reads a JSON object of the repo.
|
|
38
|
+
* @param root - The repo.
|
|
39
|
+
* @param path - The file, relative to the repo.
|
|
40
|
+
* @returns The object, or null if the file does not exist.
|
|
41
|
+
* @throws SetupError if it is not valid JSON or not an object: it is never overwritten then.
|
|
42
|
+
* @example readJson(root, ".claude/settings.json")
|
|
43
|
+
*/
|
|
44
|
+
export declare function readJson(root: string, path: string): Record<string, unknown> | null;
|
|
45
|
+
/**
|
|
46
|
+
* Writes a JSON file of the repo, indented by two spaces, creating its folder.
|
|
47
|
+
* @param root - The repo.
|
|
48
|
+
* @param path - The file, relative to the repo.
|
|
49
|
+
* @param value - What to write.
|
|
50
|
+
* @example writeJson(root, ".agents/flow.json", { baseBranch: "main" })
|
|
51
|
+
*/
|
|
52
|
+
export declare function writeJson(root: string, path: string, value: unknown): void;
|
|
53
|
+
/**
|
|
54
|
+
* The repo's package manager: the `packageManager` field, then its lockfile, then npm.
|
|
55
|
+
* @param root - The repo.
|
|
56
|
+
* @param pkg - Its package.json, or null.
|
|
57
|
+
* @returns `npm`, `pnpm`, `bun` or `yarn`.
|
|
58
|
+
* @example packageManager(root, { packageManager: "pnpm@9.0.0" }) // "pnpm"
|
|
59
|
+
*/
|
|
60
|
+
export declare function packageManager(root: string, pkg: Record<string, unknown> | null): string;
|
|
61
|
+
/**
|
|
62
|
+
* Runs `npx <args>` in the repo, its output on this terminal.
|
|
63
|
+
* @param root - The repo.
|
|
64
|
+
* @param args - The package and its arguments.
|
|
65
|
+
* @returns Whether it exited 0.
|
|
66
|
+
* @example runNpx(root, ["@ramon-sg/claude-cloud@1", "init"])
|
|
67
|
+
*/
|
|
68
|
+
export declare function runNpx(root: string, args: string[]): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Runs `npx <args>` in the repo and returns what it printed.
|
|
71
|
+
* @param root - The repo.
|
|
72
|
+
* @param args - The package and its arguments.
|
|
73
|
+
* @returns Its stdout and exit code, or null if npx could not start.
|
|
74
|
+
* @example captureNpx(root, ["github:ramon-sg/claude-repo-guard", "doctor", "--json"])
|
|
75
|
+
*/
|
|
76
|
+
export declare function captureNpx(root: string, args: string[]): {
|
|
77
|
+
out: string;
|
|
78
|
+
status: number | null;
|
|
79
|
+
} | null;
|
|
80
|
+
/**
|
|
81
|
+
* The flow's skills in the repo's skills lock, with the tag each one is at.
|
|
82
|
+
* @param lock - The lock, or null.
|
|
83
|
+
* @returns Name and tag of each skill of the flow's catalog.
|
|
84
|
+
* @example flowSkills(lock) // [{ name: "close-pr", tag: "v0.1.0" }]
|
|
85
|
+
*/
|
|
86
|
+
export declare function flowSkills(lock: Record<string, unknown> | null): {
|
|
87
|
+
name: string;
|
|
88
|
+
tag: string | null;
|
|
89
|
+
}[];
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file What `init`, `doctor` and `upgrade` share: this package's own versions, the JSON
|
|
3
|
+
* files of the repo they read and write, its package manager, and running another tool
|
|
4
|
+
* with `npx`. Every package goes by its scoped name: the unscoped `issue-flow`,
|
|
5
|
+
* `github-rest` and `skills-manager` on npm belong to other people.
|
|
6
|
+
*/
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { isObject } from "./config-base.js";
|
|
12
|
+
/** This package's name on npm. */
|
|
13
|
+
export const PACKAGE = "@ramon-sg/issue-flow";
|
|
14
|
+
/** The GitHub repo of this package, which is also the catalog of the flow skills. */
|
|
15
|
+
export const FLOW_CATALOG = "ramon-sg/issue-flow";
|
|
16
|
+
/** The catalog of the general skills. */
|
|
17
|
+
export const GENERAL_CATALOG = "ramon-sg/agent-skills";
|
|
18
|
+
/** Where `skills-manager` keeps the repo's skills manifest. */
|
|
19
|
+
export const SKILLS_MANIFEST = ".agents/skills.json";
|
|
20
|
+
/** Where `skills-manager` keeps the repo's skills lock. */
|
|
21
|
+
export const SKILLS_LOCK = ".agents/skills.lock.json";
|
|
22
|
+
/** Claude Code's settings of the repo. */
|
|
23
|
+
export const SETTINGS_FILE = ".claude/settings.json";
|
|
24
|
+
/** The options of `claude-repo-guard` in the repo. */
|
|
25
|
+
export const GUARD_CONFIG = ".claude/repo-guard.config.json";
|
|
26
|
+
/** How `claude-repo-guard` runs: it is not on npm. */
|
|
27
|
+
export const GUARD_SPEC = "github:ramon-sg/claude-repo-guard";
|
|
28
|
+
/** How `claude-cloud` runs. */
|
|
29
|
+
export const CLOUD_SPEC = "@ramon-sg/claude-cloud@1";
|
|
30
|
+
/** Error of `init`, `doctor` or `upgrade`: exits 1 with the message. */
|
|
31
|
+
export class SetupError extends Error {
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* This package's version and the exact versions of `github-rest` and `skills-manager` it
|
|
35
|
+
* was built and tested with, from its own `package.json`.
|
|
36
|
+
* @returns The versions.
|
|
37
|
+
* @throws SetupError if the package.json does not have them.
|
|
38
|
+
* @example ownVersions().issueFlow // "0.1.0"
|
|
39
|
+
*/
|
|
40
|
+
export function ownVersions() {
|
|
41
|
+
const file = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
42
|
+
const pkg = JSON.parse(readFileSync(file, "utf8"));
|
|
43
|
+
const githubRest = pkg.dependencies?.["@ramon-sg/github-rest"];
|
|
44
|
+
const skillsManager = pkg.devDependencies?.["@ramon-sg/skills-manager"];
|
|
45
|
+
if (pkg.version === undefined || githubRest === undefined || skillsManager === undefined)
|
|
46
|
+
throw new SetupError(`${file} lacks the version of issue-flow, github-rest or skills-manager.`);
|
|
47
|
+
return { issueFlow: pkg.version, githubRest, skillsManager };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Reads a JSON object of the repo.
|
|
51
|
+
* @param root - The repo.
|
|
52
|
+
* @param path - The file, relative to the repo.
|
|
53
|
+
* @returns The object, or null if the file does not exist.
|
|
54
|
+
* @throws SetupError if it is not valid JSON or not an object: it is never overwritten then.
|
|
55
|
+
* @example readJson(root, ".claude/settings.json")
|
|
56
|
+
*/
|
|
57
|
+
export function readJson(root, path) {
|
|
58
|
+
const file = join(root, path);
|
|
59
|
+
if (!existsSync(file))
|
|
60
|
+
return null;
|
|
61
|
+
let value;
|
|
62
|
+
try {
|
|
63
|
+
value = JSON.parse(readFileSync(file, "utf8"));
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
throw new SetupError(`${path} is not valid JSON (${error instanceof Error ? error.message : String(error)}); fix it and run again.`);
|
|
67
|
+
}
|
|
68
|
+
if (!isObject(value))
|
|
69
|
+
throw new SetupError(`${path} is not a JSON object; fix it and run again.`);
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Writes a JSON file of the repo, indented by two spaces, creating its folder.
|
|
74
|
+
* @param root - The repo.
|
|
75
|
+
* @param path - The file, relative to the repo.
|
|
76
|
+
* @param value - What to write.
|
|
77
|
+
* @example writeJson(root, ".agents/flow.json", { baseBranch: "main" })
|
|
78
|
+
*/
|
|
79
|
+
export function writeJson(root, path, value) {
|
|
80
|
+
const file = join(root, path);
|
|
81
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
82
|
+
writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
83
|
+
}
|
|
84
|
+
/** The lockfile of each package manager, in the order they are looked for. */
|
|
85
|
+
const LOCKFILES = [
|
|
86
|
+
["bun.lock", "bun"],
|
|
87
|
+
["bun.lockb", "bun"],
|
|
88
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
89
|
+
["yarn.lock", "yarn"],
|
|
90
|
+
["package-lock.json", "npm"],
|
|
91
|
+
];
|
|
92
|
+
/**
|
|
93
|
+
* The repo's package manager: the `packageManager` field, then its lockfile, then npm.
|
|
94
|
+
* @param root - The repo.
|
|
95
|
+
* @param pkg - Its package.json, or null.
|
|
96
|
+
* @returns `npm`, `pnpm`, `bun` or `yarn`.
|
|
97
|
+
* @example packageManager(root, { packageManager: "pnpm@9.0.0" }) // "pnpm"
|
|
98
|
+
*/
|
|
99
|
+
export function packageManager(root, pkg) {
|
|
100
|
+
const field = typeof pkg?.packageManager === "string" ? pkg.packageManager.split("@")[0] : undefined;
|
|
101
|
+
if (field !== undefined && field !== "")
|
|
102
|
+
return field;
|
|
103
|
+
return LOCKFILES.find(([file]) => existsSync(join(root, file)))?.[1] ?? "npm";
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Runs `npx <args>` in the repo, its output on this terminal.
|
|
107
|
+
* @param root - The repo.
|
|
108
|
+
* @param args - The package and its arguments.
|
|
109
|
+
* @returns Whether it exited 0.
|
|
110
|
+
* @example runNpx(root, ["@ramon-sg/claude-cloud@1", "init"])
|
|
111
|
+
*/
|
|
112
|
+
export function runNpx(root, args) {
|
|
113
|
+
const result = spawnSync("npx", ["--yes", ...args], { cwd: root, stdio: "inherit" });
|
|
114
|
+
return result.status === 0;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Runs `npx <args>` in the repo and returns what it printed.
|
|
118
|
+
* @param root - The repo.
|
|
119
|
+
* @param args - The package and its arguments.
|
|
120
|
+
* @returns Its stdout and exit code, or null if npx could not start.
|
|
121
|
+
* @example captureNpx(root, ["github:ramon-sg/claude-repo-guard", "doctor", "--json"])
|
|
122
|
+
*/
|
|
123
|
+
export function captureNpx(root, args) {
|
|
124
|
+
const result = spawnSync("npx", ["--yes", ...args], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
|
|
125
|
+
return result.error === undefined ? { out: result.stdout, status: result.status } : null;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The flow's skills in the repo's skills lock, with the tag each one is at.
|
|
129
|
+
* @param lock - The lock, or null.
|
|
130
|
+
* @returns Name and tag of each skill of the flow's catalog.
|
|
131
|
+
* @example flowSkills(lock) // [{ name: "close-pr", tag: "v0.1.0" }]
|
|
132
|
+
*/
|
|
133
|
+
export function flowSkills(lock) {
|
|
134
|
+
const skills = isObject(lock?.skills) ? lock.skills : {};
|
|
135
|
+
return Object.entries(skills)
|
|
136
|
+
.filter(([, entry]) => isObject(entry) && entry.source === `catalog:${FLOW_CATALOG}`)
|
|
137
|
+
.map(([name, entry]) => ({ name, tag: isObject(entry) && typeof entry.tag === "string" ? entry.tag : null }));
|
|
138
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { type StartMode } from "./issue-text.js";
|
|
2
|
+
import { type Output } from "./output.js";
|
|
3
|
+
import { type Adopted, type StartState } from "./start-state.js";
|
|
4
|
+
import type { WorkState } from "./work-state.js";
|
|
5
|
+
/**
|
|
6
|
+
* The fetch of origin. In a cloud session the clone has one branch, so `--here` passes
|
|
7
|
+
* the refspec: without it neither the base nor the issue's branch would come.
|
|
8
|
+
* @param mainRoot - The main checkout.
|
|
9
|
+
* @param here - Whether it runs with `--here`.
|
|
10
|
+
* @returns True if it worked.
|
|
11
|
+
* @example fetchOrigin(root, false)
|
|
12
|
+
*/
|
|
13
|
+
export declare function fetchOrigin(mainRoot: string, here: boolean): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* The branch of the worktree `--adopt` runs in, and whether it is fresh: not in origin and
|
|
16
|
+
* with no commits of its own, so the tool that made the worktree just made it.
|
|
17
|
+
* @param startRoot - The checkout where it runs.
|
|
18
|
+
* @param mainRoot - The main checkout.
|
|
19
|
+
* @param base - The base branch.
|
|
20
|
+
* @returns The branch and whether it is fresh.
|
|
21
|
+
* @throws StartError in the main checkout or on a detached HEAD.
|
|
22
|
+
* @example readAdopted(wt, root, "main") // { current: "ramon/x", fresh: true }
|
|
23
|
+
*/
|
|
24
|
+
export declare function readAdopted(startRoot: string, mainRoot: string, base: string): Adopted;
|
|
25
|
+
/**
|
|
26
|
+
* The issue's work without the adopted branch when it is fresh: it is neither earlier work
|
|
27
|
+
* nor a used name.
|
|
28
|
+
* @param work - The work.
|
|
29
|
+
* @param adopted - The adopted branch.
|
|
30
|
+
* @returns The work.
|
|
31
|
+
* @example withoutFresh(work, { current: "ramon/x", fresh: true })
|
|
32
|
+
*/
|
|
33
|
+
export declare function withoutFresh(work: WorkState, adopted: Adopted): WorkState;
|
|
34
|
+
/**
|
|
35
|
+
* Which branch `--adopt` takes. A live branch of the issue that the worktree is on is
|
|
36
|
+
* resumed. A fresh branch with the issue's number is the new branch as it is, with other
|
|
37
|
+
* branches or without (Paseo lets you edit the name; Orca makes one without the number).
|
|
38
|
+
* Otherwise, with no live branches the branch is renamed to the proposed one, and with
|
|
39
|
+
* some it stops: the issue goes on elsewhere.
|
|
40
|
+
* @param mainRoot - The main checkout.
|
|
41
|
+
* @param work - The issue's work.
|
|
42
|
+
* @param adopted - The adopted branch.
|
|
43
|
+
* @param number - The issue.
|
|
44
|
+
* @returns The branch resumed, or the new one already named, or neither (propose one).
|
|
45
|
+
* @throws StartError if the issue has live branches elsewhere.
|
|
46
|
+
* @example chooseAdopted(root, work, adopted, 214) // { found: null, named: "fix/214-x" }
|
|
47
|
+
*/
|
|
48
|
+
export declare function chooseAdopted(mainRoot: string, work: WorkState, adopted: Adopted, number: number): {
|
|
49
|
+
found: string | null;
|
|
50
|
+
named: string | null;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* The place of `--adopt`: the worktree where it runs. If it is already on the branch there
|
|
54
|
+
* is nothing to check (an `--adopt` repeated or resumed); if not, it must be fresh and
|
|
55
|
+
* without uncommitted changes, so a branch with other work is never renamed. Untracked
|
|
56
|
+
* files do not count: the tool may leave its own.
|
|
57
|
+
* @param state - What it knows: the branch, the start checkout and the adopted branch.
|
|
58
|
+
* @returns The mode and the folder.
|
|
59
|
+
* @throws StartError if the worktree is not fresh.
|
|
60
|
+
* @example placeAdopted({ branch, startRoot, adopted })
|
|
61
|
+
*/
|
|
62
|
+
export declare function placeAdopted(state: {
|
|
63
|
+
branch: string;
|
|
64
|
+
startRoot: string;
|
|
65
|
+
adopted: Adopted;
|
|
66
|
+
}): {
|
|
67
|
+
mode: StartMode;
|
|
68
|
+
workDir: string;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Renames the adopted worktree's branch to the issue's (to the same name, it does
|
|
72
|
+
* nothing). The tool that made the worktree knows it by its path, so the rename does not
|
|
73
|
+
* unlink it.
|
|
74
|
+
* @param state - The start.
|
|
75
|
+
* @throws StartError if git cannot.
|
|
76
|
+
* @example renameAdopted(state)
|
|
77
|
+
*/
|
|
78
|
+
export declare function renameAdopted(state: StartState): void;
|
|
79
|
+
/**
|
|
80
|
+
* The place of `--here`: the checkout where it runs. Stops if the branch is checked out in
|
|
81
|
+
* another one, or this one has uncommitted changes (untracked files do not count: the
|
|
82
|
+
* cloud's hook leaves its own, and git says if one is in the way).
|
|
83
|
+
* @param mainRoot - The main checkout.
|
|
84
|
+
* @param startRoot - The checkout where it runs.
|
|
85
|
+
* @param branch - The branch.
|
|
86
|
+
* @param number - The issue.
|
|
87
|
+
* @returns The mode and the folder.
|
|
88
|
+
* @throws StartError if the place is taken.
|
|
89
|
+
* @example placeHere(root, root, "feat/42-x", 42)
|
|
90
|
+
*/
|
|
91
|
+
export declare function placeHere(mainRoot: string, startRoot: string, branch: string, number: number): {
|
|
92
|
+
mode: StartMode;
|
|
93
|
+
workDir: string;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* The place of `--cloud`: the cloud session does the work, launched from the main
|
|
97
|
+
* checkout. Stops if the branch is checked out, or exists only locally, where the cloud
|
|
98
|
+
* would not see it.
|
|
99
|
+
* @param mainRoot - The main checkout.
|
|
100
|
+
* @param branch - The branch.
|
|
101
|
+
* @param found - The branch being resumed, or null.
|
|
102
|
+
* @param number - The issue.
|
|
103
|
+
* @returns The mode and the folder.
|
|
104
|
+
* @throws StartError if the cloud cannot take the branch.
|
|
105
|
+
* @example placeCloud(root, "feat/42-x", null, 42)
|
|
106
|
+
*/
|
|
107
|
+
export declare function placeCloud(mainRoot: string, branch: string, found: string | null, number: number): {
|
|
108
|
+
mode: StartMode;
|
|
109
|
+
workDir: string;
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* With `--here`, leaves the checkout on the branch without needing a local base: a new one
|
|
113
|
+
* comes from `origin/<base>`, an existing one from `origin/<branch>` (the fetch of every
|
|
114
|
+
* branch already brought it). A branch missing from origin is only local, somewhere else;
|
|
115
|
+
* a local branch with commits origin lacks stops it, so `-B` does not overwrite them.
|
|
116
|
+
* @param state - The start.
|
|
117
|
+
* @throws StartError if git cannot or the branch cannot be brought.
|
|
118
|
+
* @example prepareHere(state)
|
|
119
|
+
*/
|
|
120
|
+
export declare function prepareHere(state: StartState): void;
|
|
121
|
+
/**
|
|
122
|
+
* The cloud session noted in the PR's body, or null (no PR, none noted, or the PR cannot
|
|
123
|
+
* be read).
|
|
124
|
+
* @param repo - The repo.
|
|
125
|
+
* @param pr - The PR, or null.
|
|
126
|
+
* @returns The id, or null.
|
|
127
|
+
* @example await prCloudSession("acme/widgets", 7)
|
|
128
|
+
*/
|
|
129
|
+
export declare function prCloudSession(repo: string, pr: number | null): Promise<string | null>;
|
|
130
|
+
/**
|
|
131
|
+
* The end of `--here`: notes this cloud session in the PR (if it runs in one and the PR
|
|
132
|
+
* does not have it yet), so `--teleport` finds it from any machine, and prints the
|
|
133
|
+
* `Prompt:` line the skill follows. It does not open Claude: it already runs in one.
|
|
134
|
+
* @param state - The start.
|
|
135
|
+
* @param pr - The PR.
|
|
136
|
+
* @param prompt - What the session goes on with.
|
|
137
|
+
* @example await finishHere(state, 900, prompt)
|
|
138
|
+
*/
|
|
139
|
+
export declare function finishHere(state: StartState, pr: number, prompt: string): Promise<void>;
|
|
140
|
+
/**
|
|
141
|
+
* The flags with which the cloud session picks the branch: `--new` if the issue has live
|
|
142
|
+
* branches and a new one was chosen here (`--new`, or `n` in the terminal), `--resume
|
|
143
|
+
* <branch>` if one of several was chosen. With one live branch resumed, none: the session
|
|
144
|
+
* resumes it.
|
|
145
|
+
* @param state - The start.
|
|
146
|
+
* @returns The flags, with a space before each.
|
|
147
|
+
* @throws StartError if the branch has characters a shell would read (anyone who can push
|
|
148
|
+
* names branches).
|
|
149
|
+
* @example cloudFlags(state) // " --resume fix/214-old"
|
|
150
|
+
*/
|
|
151
|
+
export declare function cloudFlags(state: StartState): string;
|
|
152
|
+
/**
|
|
153
|
+
* `--cloud` after the yes: opens the cloud session with the skill that starts the issue
|
|
154
|
+
* there, in the foreground and with the terminal (`claude --cloud` needs one), from the
|
|
155
|
+
* main checkout. Without `claude` or with `--no-launch`, says how to open it.
|
|
156
|
+
* @param state - The start.
|
|
157
|
+
* @param out - Where to write.
|
|
158
|
+
* @returns The exit code.
|
|
159
|
+
* @throws StartError if the session does not open.
|
|
160
|
+
* @example launchCloud(state, out)
|
|
161
|
+
*/
|
|
162
|
+
export declare function launchCloud(state: StartState, out: Output): number;
|