@kitschpatrol/chagent 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/dist/bin/cli.js +184 -0
- package/dist/lib/bumpp.d.ts +14 -0
- package/dist/lib/bumpp.js +72 -0
- package/dist/lib/index.d.ts +31 -0
- package/dist/lib/index.js +4 -0
- package/dist/lib-CXMRRbJf.js +175 -0
- package/dist/progress-DQtRnH1N.js +74 -0
- package/dist/review-LYSADnul.js +2310 -0
- package/dist/types-Cms1Tu5k.d.ts +162 -0
- package/license.txt +21 -0
- package/package.json +93 -3
- package/readme.md +313 -0
- package/README.md +0 -3
package/dist/bin/cli.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { C as version, S as maxTimeoutMs, b as defaultModels, i as generateChangelog, l as agentNames, n as previewChangelog, r as reviewChangelog, s as writeChangelog, t as assertReviewTerminal, u as efforts, x as defaults, y as setLogger } from "../review-LYSADnul.js";
|
|
3
|
+
import { n as assertAmendable, t as amendRelease } from "../lib-CXMRRbJf.js";
|
|
4
|
+
import { t as createProgressLine } from "../progress-DQtRnH1N.js";
|
|
5
|
+
import { createLogger } from "lognow";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import yargs from "yargs";
|
|
8
|
+
import { hideBin } from "yargs/helpers";
|
|
9
|
+
|
|
10
|
+
// src/bin/cli.ts
|
|
11
|
+
/**
|
|
12
|
+
* Route library output: a timer on a terminal, plain lines otherwise or when
|
|
13
|
+
* verbose.
|
|
14
|
+
*/
|
|
15
|
+
function createReporter(verbose, signal) {
|
|
16
|
+
const progress = !verbose && process.stderr.isTTY ? createProgressLine(process.stderr) : void 0;
|
|
17
|
+
signal.addEventListener("abort", () => {
|
|
18
|
+
progress?.stop();
|
|
19
|
+
}, { once: true });
|
|
20
|
+
const logDetail = (...args) => {
|
|
21
|
+
if (progress === void 0) console.error(...args);
|
|
22
|
+
};
|
|
23
|
+
const logDebug = (...args) => {
|
|
24
|
+
if (verbose) console.error(...args);
|
|
25
|
+
};
|
|
26
|
+
const logProblem = (...args) => {
|
|
27
|
+
progress?.clear();
|
|
28
|
+
console.error(...args);
|
|
29
|
+
};
|
|
30
|
+
setLogger(createLogger({
|
|
31
|
+
logToConsole: {
|
|
32
|
+
colorize: process.stderr.isTTY === true,
|
|
33
|
+
getTerminalWidth: () => process.stderr.isTTY ? process.stderr.columns : Number.MAX_SAFE_INTEGER,
|
|
34
|
+
logger: {
|
|
35
|
+
debug: logDebug,
|
|
36
|
+
error: logProblem,
|
|
37
|
+
info: logDetail,
|
|
38
|
+
trace: logDebug,
|
|
39
|
+
warn: logProblem
|
|
40
|
+
},
|
|
41
|
+
showLevel: verbose,
|
|
42
|
+
showName: false,
|
|
43
|
+
showTime: verbose
|
|
44
|
+
},
|
|
45
|
+
name: "chagent",
|
|
46
|
+
verbose: true
|
|
47
|
+
}));
|
|
48
|
+
return {
|
|
49
|
+
report(message) {
|
|
50
|
+
progress?.update(message);
|
|
51
|
+
},
|
|
52
|
+
async track(work) {
|
|
53
|
+
progress?.start("Reading repository history.");
|
|
54
|
+
try {
|
|
55
|
+
return await work();
|
|
56
|
+
} finally {
|
|
57
|
+
progress?.stop();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const controller = new AbortController();
|
|
63
|
+
for (const signal of [
|
|
64
|
+
"SIGHUP",
|
|
65
|
+
"SIGINT",
|
|
66
|
+
"SIGTERM"
|
|
67
|
+
]) process.once(signal, () => {
|
|
68
|
+
controller.abort();
|
|
69
|
+
});
|
|
70
|
+
const cli = yargs(hideBin(process.argv));
|
|
71
|
+
try {
|
|
72
|
+
await cli.scriptName("chagent").command("$0", "Initialize or update changelogs from repository history.", (command) => command.option("cwd", {
|
|
73
|
+
default: process.cwd(),
|
|
74
|
+
defaultDescription: "current directory",
|
|
75
|
+
description: "Repository directory",
|
|
76
|
+
type: "string"
|
|
77
|
+
}).option("agent", {
|
|
78
|
+
choices: agentNames,
|
|
79
|
+
defaultDescription: defaults.agent,
|
|
80
|
+
description: "Installed agent CLI",
|
|
81
|
+
type: "string"
|
|
82
|
+
}).option("model", {
|
|
83
|
+
defaultDescription: `${defaultModels.codex} (Codex); ${defaultModels.claude} (Claude)`,
|
|
84
|
+
description: "Agent model",
|
|
85
|
+
type: "string"
|
|
86
|
+
}).option("effort", {
|
|
87
|
+
choices: efforts,
|
|
88
|
+
defaultDescription: defaults.effort,
|
|
89
|
+
description: "Reasoning effort",
|
|
90
|
+
type: "string"
|
|
91
|
+
}).option("interactive", {
|
|
92
|
+
alias: "i",
|
|
93
|
+
default: false,
|
|
94
|
+
description: "Review a diff and optionally edit before writing",
|
|
95
|
+
type: "boolean"
|
|
96
|
+
}).option("editor", {
|
|
97
|
+
defaultDescription: "git var GIT_EDITOR",
|
|
98
|
+
description: "Editor command for interactive review; configure GUI editors to wait",
|
|
99
|
+
type: "string"
|
|
100
|
+
}).option("dry-run", {
|
|
101
|
+
default: false,
|
|
102
|
+
description: "Print a diff without applying changes",
|
|
103
|
+
type: "boolean"
|
|
104
|
+
}).option("rebuild", {
|
|
105
|
+
default: false,
|
|
106
|
+
description: "Reconstruct all history, replacing existing release text",
|
|
107
|
+
type: "boolean"
|
|
108
|
+
}).option("amend", {
|
|
109
|
+
default: false,
|
|
110
|
+
description: "Amend a verified unpublished release commit and matching local tags",
|
|
111
|
+
type: "boolean"
|
|
112
|
+
}).option("github", {
|
|
113
|
+
defaultDescription: String(defaults.github),
|
|
114
|
+
description: "Enrich Git evidence using gh when available; disable with --no-github",
|
|
115
|
+
type: "boolean"
|
|
116
|
+
}).option("cache", {
|
|
117
|
+
defaultDescription: String(defaults.cache),
|
|
118
|
+
description: "Cache drafts and incremental state; disable with --no-cache",
|
|
119
|
+
type: "boolean"
|
|
120
|
+
}).option("config", {
|
|
121
|
+
defaultDescription: "auto-detected from repository root",
|
|
122
|
+
description: "Configuration file path",
|
|
123
|
+
type: "string"
|
|
124
|
+
}).option("timeout", {
|
|
125
|
+
defaultDescription: String(defaults.timeoutMs / 1e3),
|
|
126
|
+
description: "Agent timeout in seconds",
|
|
127
|
+
type: "number"
|
|
128
|
+
}).option("verbose", {
|
|
129
|
+
default: false,
|
|
130
|
+
description: "Log commands, cache decisions, and session details to stderr",
|
|
131
|
+
type: "boolean"
|
|
132
|
+
}).check((args) => {
|
|
133
|
+
if (args.timeout !== void 0 && (!Number.isFinite(args.timeout) || args.timeout <= 0 || args.timeout * 1e3 > 2147483647)) throw new Error(`--timeout must be a positive number of seconds, up to ${Math.floor(maxTimeoutMs / 1e3)}.`);
|
|
134
|
+
return true;
|
|
135
|
+
}), async (args) => {
|
|
136
|
+
if (!args.dryRun && args.interactive) assertReviewTerminal();
|
|
137
|
+
if (!args.dryRun && args.amend) await assertAmendable(args.cwd);
|
|
138
|
+
const reporter = createReporter(args.verbose, controller.signal);
|
|
139
|
+
let plan = await reporter.track(async () => generateChangelog({
|
|
140
|
+
agent: args.agent,
|
|
141
|
+
cache: args.cache,
|
|
142
|
+
configFile: args.config,
|
|
143
|
+
cwd: args.cwd,
|
|
144
|
+
effort: args.effort,
|
|
145
|
+
github: args.github,
|
|
146
|
+
model: args.model,
|
|
147
|
+
onProgress(message) {
|
|
148
|
+
reporter.report(message);
|
|
149
|
+
},
|
|
150
|
+
rebuild: args.rebuild,
|
|
151
|
+
signal: controller.signal,
|
|
152
|
+
timeoutMs: args.timeout === void 0 ? void 0 : args.timeout * 1e3
|
|
153
|
+
}));
|
|
154
|
+
try {
|
|
155
|
+
if (args.dryRun) {
|
|
156
|
+
process.stdout.write(await previewChangelog(plan));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (args.interactive) {
|
|
160
|
+
const reviewed = await reviewChangelog(plan, { editor: args.editor });
|
|
161
|
+
if (reviewed === void 0) {
|
|
162
|
+
process.stderr.write("Changelog review cancelled.\n");
|
|
163
|
+
process.exitCode = 1;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
plan = reviewed;
|
|
167
|
+
}
|
|
168
|
+
if (args.amend) await amendRelease(plan);
|
|
169
|
+
else await writeChangelog(plan);
|
|
170
|
+
} finally {
|
|
171
|
+
for (const diagnostic of plan.diagnostics) process.stderr.write(diagnostic + "\n");
|
|
172
|
+
}
|
|
173
|
+
process.stderr.write(plan.files.length === 0 ? "Changelogs are up to date.\n" : `Updated ${plan.files.map((file) => path.relative(plan.root, file.path)).join(", ")}.\n`);
|
|
174
|
+
}).alias("h", "help").version(version).alias("v", "version").help().strict().fail((message, error, instance) => {
|
|
175
|
+
if (error !== void 0) throw error;
|
|
176
|
+
instance.showHelp();
|
|
177
|
+
throw new Error(message);
|
|
178
|
+
}).wrap(process.stdout.isTTY ? Math.min(120, cli.terminalWidth()) : 0).parse();
|
|
179
|
+
} catch (error) {
|
|
180
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
181
|
+
process.exitCode = 1;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export { };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { u as GenerateOptions } from "../types-Cms1Tu5k.js";
|
|
2
|
+
import { VersionBumpOptions } from "bumpp";
|
|
3
|
+
type ExecuteHook = Exclude<NonNullable<VersionBumpOptions['execute']>, string>;
|
|
4
|
+
export type BumppHookOptions = Omit<GenerateOptions, 'cwd' | 'release'> & {
|
|
5
|
+
editor?: string;
|
|
6
|
+
interactive?: boolean;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Generate changelogs inside bumpp's execute phase and include only their
|
|
10
|
+
* files. Progress and diagnostics go to stderr, unless `onProgress` is given to
|
|
11
|
+
* handle status messages.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createBumppHook(options?: BumppHookOptions): ExecuteHook;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { g as git, i as generateChangelog, m as localDate, r as reviewChangelog, s as writeChangelog, t as assertReviewTerminal } from "../review-LYSADnul.js";
|
|
2
|
+
import { t as createProgressLine } from "../progress-DQtRnH1N.js";
|
|
3
|
+
|
|
4
|
+
// src/lib/bumpp.ts
|
|
5
|
+
const TAG_TOKEN = /\{(version|oldVersion|tag|releaseType|major|minor|patch|date)\}/gv;
|
|
6
|
+
/** Mirror bumpp's tag rendering, which it does not export. */
|
|
7
|
+
function tagName(template, version, oldVersion, releaseType = "") {
|
|
8
|
+
if (template === void 0) return;
|
|
9
|
+
const [major = "", minor = "", patch = ""] = version.split("-", 1)[0].split(".", 3);
|
|
10
|
+
const tokens = {
|
|
11
|
+
date: localDate(),
|
|
12
|
+
major,
|
|
13
|
+
minor,
|
|
14
|
+
oldVersion,
|
|
15
|
+
patch,
|
|
16
|
+
releaseType,
|
|
17
|
+
tag: "",
|
|
18
|
+
version
|
|
19
|
+
};
|
|
20
|
+
const rendered = template.replaceAll(TAG_TOKEN, (_match, key) => tokens[key]);
|
|
21
|
+
if (rendered !== template) return rendered;
|
|
22
|
+
return template.includes("%s") ? template.replaceAll("%s", () => version) : template + version;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Generate changelogs inside bumpp's execute phase and include only their
|
|
26
|
+
* files. Progress and diagnostics go to stderr, unless `onProgress` is given to
|
|
27
|
+
* handle status messages.
|
|
28
|
+
*/
|
|
29
|
+
function createBumppHook(options = {}) {
|
|
30
|
+
const { editor, interactive, onProgress, ...generateOptions } = options;
|
|
31
|
+
return async (operation) => {
|
|
32
|
+
if (interactive === true) assertReviewTerminal();
|
|
33
|
+
const progress = onProgress === void 0 && process.stderr.isTTY ? createProgressLine(process.stderr) : void 0;
|
|
34
|
+
progress?.start("Reading repository history.");
|
|
35
|
+
let plan;
|
|
36
|
+
try {
|
|
37
|
+
plan = await generateChangelog({
|
|
38
|
+
...generateOptions,
|
|
39
|
+
cwd: operation.options.cwd,
|
|
40
|
+
onProgress: onProgress ?? ((message) => {
|
|
41
|
+
if (progress === void 0) process.stderr.write(`${message}\n`);
|
|
42
|
+
else progress.update(message);
|
|
43
|
+
}),
|
|
44
|
+
release: {
|
|
45
|
+
tag: tagName(operation.options.tag?.name, operation.state.newVersion, operation.state.currentVersion, operation.state.release),
|
|
46
|
+
version: operation.state.newVersion
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
} finally {
|
|
50
|
+
progress?.stop();
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
if (interactive === true) {
|
|
54
|
+
const reviewed = await reviewChangelog(plan, { editor });
|
|
55
|
+
if (reviewed === void 0) throw new Error("Changelog review cancelled. The release has not been committed or tagged.");
|
|
56
|
+
plan = reviewed;
|
|
57
|
+
}
|
|
58
|
+
const files = await writeChangelog(plan);
|
|
59
|
+
if (files.length === 0) return;
|
|
60
|
+
await git(plan.root, [
|
|
61
|
+
"add",
|
|
62
|
+
"--",
|
|
63
|
+
...files
|
|
64
|
+
]);
|
|
65
|
+
operation.update({ updatedFiles: [.../* @__PURE__ */ new Set([...operation.state.updatedFiles, ...files])] });
|
|
66
|
+
} finally {
|
|
67
|
+
for (const diagnostic of plan.diagnostics) process.stderr.write(`${diagnostic}\n`);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export { createBumppHook };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { _ as Config, a as ChangeSet, c as Evidence, d as GenerationRequest, f as GenerationResponse, g as RunSettings, h as ReleaseSummary, i as Change, l as FileEdit, m as ReleaseRequest, n as AgentName, o as ChangelogPlan, p as ReleaseContext, r as Category, s as Effort, t as AgentAdapter, u as GenerateOptions, v as defineConfig, y as loadConfig } from "../types-Cms1Tu5k.js";
|
|
2
|
+
import { ILogBasic, ILogLayer } from "lognow";
|
|
3
|
+
/** Create an adapter for an installed CLI using its existing authentication. */
|
|
4
|
+
export declare function createAgent(name: AgentName): AgentAdapter;
|
|
5
|
+
/**
|
|
6
|
+
* Explicitly amend an unpublished version-only release and its matching local
|
|
7
|
+
* tags.
|
|
8
|
+
*/
|
|
9
|
+
export declare function amendRelease(plan: ChangelogPlan): Promise<string>;
|
|
10
|
+
/** Generate reviewable edits from Git history without changing tracked files. */
|
|
11
|
+
export declare function generateChangelog(options?: GenerateOptions): Promise<ChangelogPlan>;
|
|
12
|
+
/**
|
|
13
|
+
* Set the logger instance for the module. Export this for library consumers to
|
|
14
|
+
* inject their own logger.
|
|
15
|
+
*
|
|
16
|
+
* @param logger - Accepts either a LogLayer instance or a Console- or
|
|
17
|
+
* Stream-like log target
|
|
18
|
+
*/
|
|
19
|
+
export declare function setLogger(logger?: ILogBasic | ILogLayer<unknown>): void;
|
|
20
|
+
/** Produce a unified diff without modifying the repository or index. */
|
|
21
|
+
export declare function previewChangelog(plan: ChangelogPlan): Promise<string>;
|
|
22
|
+
/** Review a diff and optionally edit temporary candidates before applying them. */
|
|
23
|
+
export declare function reviewChangelog(plan: ChangelogPlan, options?: {
|
|
24
|
+
editor?: string;
|
|
25
|
+
}): Promise<ChangelogPlan | undefined>;
|
|
26
|
+
/**
|
|
27
|
+
* Apply reviewed edits, rejecting concurrent changes and rolling back failed
|
|
28
|
+
* writes.
|
|
29
|
+
*/
|
|
30
|
+
export declare function writeChangelog(plan: ChangelogPlan): Promise<string[]>;
|
|
31
|
+
export { type AgentAdapter, type AgentName, type Category, type Change, type ChangeSet, type ChangelogPlan, type Config, type Effort, type Evidence, type FileEdit, type GenerateOptions, type GenerationRequest, type GenerationResponse, type ReleaseContext, type ReleaseRequest, type ReleaseSummary, type RunSettings, defineConfig, loadConfig };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { a as defineConfig, c as createAgent, i as generateChangelog, n as previewChangelog, o as loadConfig, r as reviewChangelog, s as writeChangelog, y as setLogger } from "../review-LYSADnul.js";
|
|
2
|
+
import { t as amendRelease } from "../lib-CXMRRbJf.js";
|
|
3
|
+
|
|
4
|
+
export { amendRelease, createAgent, defineConfig, generateChangelog, loadConfig, previewChangelog, reviewChangelog, setLogger, writeChangelog };
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { _ as isAncestor, d as LOCKFILE_PATH, f as inspectRepository, g as git, h as manifestAt, p as isVersionOnlyChange, s as writeChangelog, v as runCommand } from "./review-LYSADnul.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { realpath } from "node:fs/promises";
|
|
4
|
+
|
|
5
|
+
// src/lib/amend.ts
|
|
6
|
+
const SIGNATURE = /-----BEGIN (?:PGP|SSH) SIGNATURE-----/v;
|
|
7
|
+
const RELEASE_MESSAGE = /^(?:release\b|(?:chore|build)(?:\(release\))?:?\s*(?:release\b|v?\d))/iv;
|
|
8
|
+
async function assertUnpublished(root, head, tags) {
|
|
9
|
+
const remotes = await git(root, ["remote"]);
|
|
10
|
+
for (const remote of remotes.split("\n")) {
|
|
11
|
+
if (remote === "") continue;
|
|
12
|
+
const refs = await git(root, [
|
|
13
|
+
"ls-remote",
|
|
14
|
+
"--heads",
|
|
15
|
+
"--tags",
|
|
16
|
+
remote
|
|
17
|
+
]);
|
|
18
|
+
for (const line of refs.split("\n")) {
|
|
19
|
+
const [object, ref] = line.split(" ", 2);
|
|
20
|
+
if (object === void 0 || ref === void 0) continue;
|
|
21
|
+
if (object === head || tags.some((tag) => ref === `refs/tags/${tag.name}`)) throw new Error(`The release is already advertised by ${remote}; refusing to amend it.`);
|
|
22
|
+
if (!ref.startsWith("refs/heads/")) continue;
|
|
23
|
+
if ((await runCommand("git", [
|
|
24
|
+
"cat-file",
|
|
25
|
+
"-e",
|
|
26
|
+
object + "^{commit}"
|
|
27
|
+
], {
|
|
28
|
+
allowFailure: true,
|
|
29
|
+
cwd: root
|
|
30
|
+
})).code !== 0) throw new Error(`Cannot verify unpublished status against ${remote}. Fetch its branches and retry.`);
|
|
31
|
+
if (await isAncestor(root, head, object)) throw new Error(`The release commit is already reachable on ${remote}; refusing to amend it.`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function inspectRelease(root, head) {
|
|
36
|
+
if (await git(root, [
|
|
37
|
+
"symbolic-ref",
|
|
38
|
+
"-q",
|
|
39
|
+
"HEAD"
|
|
40
|
+
], true) === "") throw new Error("Amendment requires a checked-out branch.");
|
|
41
|
+
if ((await git(root, [
|
|
42
|
+
"rev-list",
|
|
43
|
+
"--parents",
|
|
44
|
+
"-n",
|
|
45
|
+
"1",
|
|
46
|
+
head
|
|
47
|
+
])).split(" ").length !== 2) throw new Error("Amendment requires a release commit with exactly one parent.");
|
|
48
|
+
const subject = await git(root, [
|
|
49
|
+
"log",
|
|
50
|
+
"-1",
|
|
51
|
+
"--format=%s"
|
|
52
|
+
]);
|
|
53
|
+
if (!RELEASE_MESSAGE.test(subject)) throw new Error("HEAD does not have a recognizable release commit message. Use the bumpp hook for automatic release integration.");
|
|
54
|
+
await assertVersionBump(root, head);
|
|
55
|
+
const repository = await inspectRepository(root);
|
|
56
|
+
const versions = [repository.rootPackage, ...repository.packages].map((pkg) => pkg.version).filter((version) => version !== void 0);
|
|
57
|
+
const names = await git(root, [
|
|
58
|
+
"tag",
|
|
59
|
+
"--points-at",
|
|
60
|
+
head
|
|
61
|
+
]);
|
|
62
|
+
const tags = [];
|
|
63
|
+
for (const name of names.split("\n")) {
|
|
64
|
+
if (name === "") continue;
|
|
65
|
+
if (versions.every((version) => name !== version && name !== `v${version}` && !name.endsWith(`@${version}`) && !name.endsWith(`/v${version}`))) throw new Error(`HEAD also has an unrelated tag (${name}); refusing to move it.`);
|
|
66
|
+
const object = await git(root, ["rev-parse", `refs/tags/${name}`]);
|
|
67
|
+
const contents = await tagContents(root, object);
|
|
68
|
+
if (contents !== void 0 && SIGNATURE.test(contents)) throw new Error(`Tag ${name} is signed. Use the bumpp hook before tagging, or re-sign this release manually.`);
|
|
69
|
+
tags.push({
|
|
70
|
+
contents,
|
|
71
|
+
name,
|
|
72
|
+
object
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
await assertUnpublished(root, head, tags);
|
|
76
|
+
return tags;
|
|
77
|
+
}
|
|
78
|
+
async function tagContents(root, object) {
|
|
79
|
+
if (await git(root, [
|
|
80
|
+
"cat-file",
|
|
81
|
+
"-t",
|
|
82
|
+
object
|
|
83
|
+
]) !== "tag") return;
|
|
84
|
+
return (await runCommand("git", [
|
|
85
|
+
"cat-file",
|
|
86
|
+
"-p",
|
|
87
|
+
object
|
|
88
|
+
], { cwd: root })).stdout;
|
|
89
|
+
}
|
|
90
|
+
/** Refuse an ineligible amendment before generation spends an agent session. */
|
|
91
|
+
async function assertAmendable(cwd) {
|
|
92
|
+
const root = await realpath(await git(cwd, ["rev-parse", "--show-toplevel"]));
|
|
93
|
+
await inspectRelease(root, await git(root, ["rev-parse", "HEAD"]));
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Explicitly amend an unpublished version-only release and its matching local
|
|
97
|
+
* tags.
|
|
98
|
+
*/
|
|
99
|
+
async function amendRelease(plan) {
|
|
100
|
+
if (await git(plan.root, ["rev-parse", "HEAD"]) !== plan.head) throw new Error("HEAD changed during generation; refusing to amend.");
|
|
101
|
+
const tags = await inspectRelease(plan.root, plan.head);
|
|
102
|
+
const files = await writeChangelog(plan);
|
|
103
|
+
if (files.length === 0) return plan.head;
|
|
104
|
+
if (await git(plan.root, ["rev-parse", "HEAD"]) !== plan.head) throw new Error("HEAD changed while writing changelogs; refusing to amend.");
|
|
105
|
+
const originalCommit = await git(plan.root, [
|
|
106
|
+
"cat-file",
|
|
107
|
+
"-p",
|
|
108
|
+
plan.head
|
|
109
|
+
]);
|
|
110
|
+
await git(plan.root, [
|
|
111
|
+
"add",
|
|
112
|
+
"--",
|
|
113
|
+
...files
|
|
114
|
+
]);
|
|
115
|
+
await git(plan.root, [
|
|
116
|
+
"commit",
|
|
117
|
+
"--amend",
|
|
118
|
+
"--no-edit",
|
|
119
|
+
"--only",
|
|
120
|
+
...originalCommit.includes("\ngpgsig ") ? ["--gpg-sign"] : [],
|
|
121
|
+
"--",
|
|
122
|
+
...files
|
|
123
|
+
]);
|
|
124
|
+
const amended = await git(plan.root, ["rev-parse", "HEAD"]);
|
|
125
|
+
try {
|
|
126
|
+
const updates = ["start"];
|
|
127
|
+
for (const tag of tags) {
|
|
128
|
+
let object = amended;
|
|
129
|
+
if (tag.contents !== void 0) {
|
|
130
|
+
const contents = tag.contents.replace(`object ${plan.head}`, () => `object ${amended}`);
|
|
131
|
+
object = (await runCommand("git", ["mktag"], {
|
|
132
|
+
cwd: plan.root,
|
|
133
|
+
input: contents
|
|
134
|
+
})).stdout.trim();
|
|
135
|
+
}
|
|
136
|
+
updates.push(`update refs/tags/${tag.name} ${object} ${tag.object}`);
|
|
137
|
+
}
|
|
138
|
+
updates.push("prepare", "commit");
|
|
139
|
+
await runCommand("git", ["update-ref", "--stdin"], {
|
|
140
|
+
cwd: plan.root,
|
|
141
|
+
input: updates.join("\n") + "\n"
|
|
142
|
+
});
|
|
143
|
+
} catch (error) {
|
|
144
|
+
await git(plan.root, [
|
|
145
|
+
"update-ref",
|
|
146
|
+
"HEAD",
|
|
147
|
+
plan.head,
|
|
148
|
+
amended
|
|
149
|
+
]);
|
|
150
|
+
throw new Error("Could not retarget the release tags. Restored the original release commit; changelog edits remain available.", { cause: error });
|
|
151
|
+
}
|
|
152
|
+
return amended;
|
|
153
|
+
}
|
|
154
|
+
async function assertVersionBump(root, head) {
|
|
155
|
+
const files = await git(root, [
|
|
156
|
+
"diff-tree",
|
|
157
|
+
"--no-commit-id",
|
|
158
|
+
"--name-only",
|
|
159
|
+
"-r",
|
|
160
|
+
head
|
|
161
|
+
]);
|
|
162
|
+
let versionChanged = false;
|
|
163
|
+
for (const file of files.split("\n")) {
|
|
164
|
+
if (path.basename(file).toLowerCase() === "changelog.md" || LOCKFILE_PATH.test(file)) continue;
|
|
165
|
+
if (path.basename(file) !== "package.json") throw new Error(`The release commit also changes ${file}; automatic amendment requires a version-only release commit.`);
|
|
166
|
+
const before = await manifestAt(root, path.posix.dirname(file), head + "^");
|
|
167
|
+
const after = await manifestAt(root, path.posix.dirname(file), head);
|
|
168
|
+
if (before === void 0 || after === void 0 || before.version === after.version) throw new Error(`No verifiable version bump in ${file}.`);
|
|
169
|
+
if (!await isVersionOnlyChange(root, file, head + "^", head)) throw new Error(`The release changes more than the version in ${file}. Use the bumpp hook instead.`);
|
|
170
|
+
versionChanged = true;
|
|
171
|
+
}
|
|
172
|
+
if (!versionChanged) throw new Error("HEAD does not contain a version bump.");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export { assertAmendable as n, amendRelease as t };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { styleText } from "node:util";
|
|
2
|
+
import { unicode } from "@clack/prompts";
|
|
3
|
+
|
|
4
|
+
// src/lib/progress.ts
|
|
5
|
+
const FRAMES = unicode ? [
|
|
6
|
+
"◒",
|
|
7
|
+
"◐",
|
|
8
|
+
"◓",
|
|
9
|
+
"◑"
|
|
10
|
+
] : [
|
|
11
|
+
"•",
|
|
12
|
+
"o",
|
|
13
|
+
"O",
|
|
14
|
+
"0"
|
|
15
|
+
];
|
|
16
|
+
const FRAME_INTERVAL_MS = 80;
|
|
17
|
+
const FRAME_PADDING = " ";
|
|
18
|
+
const CLEAR_TO_LINE_END = "\x1B[K";
|
|
19
|
+
const HIDE_CURSOR = "\x1B[?25l";
|
|
20
|
+
const SHOW_CURSOR = "\x1B[?25h";
|
|
21
|
+
const TRAILING_PERIOD = /\.$/v;
|
|
22
|
+
function formatElapsed(milliseconds) {
|
|
23
|
+
const seconds = Math.floor(milliseconds / 1e3);
|
|
24
|
+
const minutes = Math.floor(seconds / 60);
|
|
25
|
+
return minutes > 0 ? `[${minutes}m ${seconds % 60}s]` : `[${seconds}s]`;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Create a status line with an elapsed timer. Each redraw is one write that
|
|
29
|
+
* overwrites the line in place, so the terminal never shows it blank.
|
|
30
|
+
*/
|
|
31
|
+
function createProgressLine(stream) {
|
|
32
|
+
let message = "";
|
|
33
|
+
let started = 0;
|
|
34
|
+
let tick = 0;
|
|
35
|
+
let timer;
|
|
36
|
+
const line = () => {
|
|
37
|
+
const frame = FRAMES[tick % FRAMES.length] ?? "";
|
|
38
|
+
tick += 1;
|
|
39
|
+
const elapsed = formatElapsed(performance.now() - started);
|
|
40
|
+
const { columns = 0 } = stream;
|
|
41
|
+
const width = columns > 0 ? Math.max(0, columns - 1 - frame.length - 2) : Infinity;
|
|
42
|
+
const text = `${message.slice(0, Math.max(0, width - elapsed.length - 1))} ${elapsed}`.slice(0, width);
|
|
43
|
+
return `\r${styleText("magenta", frame, { stream })}${FRAME_PADDING}${text}${CLEAR_TO_LINE_END}`;
|
|
44
|
+
};
|
|
45
|
+
const update = (text) => {
|
|
46
|
+
message = text.replace(TRAILING_PERIOD, "");
|
|
47
|
+
};
|
|
48
|
+
const stop = () => {
|
|
49
|
+
if (timer === void 0) return;
|
|
50
|
+
clearInterval(timer);
|
|
51
|
+
timer = void 0;
|
|
52
|
+
process.off("exit", stop);
|
|
53
|
+
stream.write(`\r${CLEAR_TO_LINE_END}${SHOW_CURSOR}`);
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
/** Blank the line so other output can print; the next redraw restores it. */
|
|
57
|
+
clear() {
|
|
58
|
+
if (timer !== void 0) stream.write(`\r${CLEAR_TO_LINE_END}`);
|
|
59
|
+
},
|
|
60
|
+
start(text) {
|
|
61
|
+
update(text);
|
|
62
|
+
started = performance.now();
|
|
63
|
+
process.once("exit", stop);
|
|
64
|
+
stream.write(`${HIDE_CURSOR}${line()}`);
|
|
65
|
+
timer = setInterval(() => {
|
|
66
|
+
stream.write(line());
|
|
67
|
+
}, FRAME_INTERVAL_MS);
|
|
68
|
+
},
|
|
69
|
+
stop,
|
|
70
|
+
update
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export { createProgressLine as t };
|