supipowers 1.2.6 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +118 -56
- package/bin/install.ts +48 -128
- package/package.json +11 -3
- package/skills/code-review/SKILL.md +137 -40
- package/skills/context-mode/SKILL.md +67 -56
- package/skills/creating-supi-agents/SKILL.md +204 -0
- package/skills/debugging/SKILL.md +86 -40
- package/skills/fix-pr/SKILL.md +96 -65
- package/skills/planning/SKILL.md +103 -46
- package/skills/qa-strategy/SKILL.md +68 -46
- package/skills/receiving-code-review/SKILL.md +60 -53
- package/skills/release/SKILL.md +111 -39
- package/skills/tdd/SKILL.md +118 -67
- package/skills/verification/SKILL.md +71 -37
- package/src/bootstrap.ts +27 -5
- package/src/commands/agents.ts +249 -0
- package/src/commands/ai-review.ts +1113 -0
- package/src/commands/config.ts +224 -95
- package/src/commands/doctor.ts +19 -13
- package/src/commands/fix-pr.ts +8 -11
- package/src/commands/generate.ts +200 -0
- package/src/commands/model-picker.ts +5 -15
- package/src/commands/model.ts +4 -5
- package/src/commands/optimize-context.ts +202 -0
- package/src/commands/plan.ts +148 -92
- package/src/commands/qa.ts +14 -23
- package/src/commands/release.ts +504 -275
- package/src/commands/review.ts +643 -86
- package/src/commands/status.ts +44 -17
- package/src/commands/supi.ts +69 -41
- package/src/commands/update.ts +57 -2
- package/src/config/defaults.ts +6 -39
- package/src/config/loader.ts +388 -40
- package/src/config/model-resolver.ts +26 -22
- package/src/config/schema.ts +113 -48
- package/src/context/analyzer.ts +61 -2
- package/src/context/optimizer.ts +199 -0
- package/src/context-mode/compressor.ts +14 -11
- package/src/context-mode/detector.ts +16 -54
- package/src/context-mode/event-extractor.ts +45 -16
- package/src/context-mode/event-store.ts +225 -16
- package/src/context-mode/hooks.ts +195 -22
- package/src/context-mode/knowledge/chunker.ts +235 -0
- package/src/context-mode/knowledge/store.ts +187 -0
- package/src/context-mode/routing.ts +12 -23
- package/src/context-mode/sandbox/executor.ts +183 -0
- package/src/context-mode/sandbox/runners.ts +40 -0
- package/src/context-mode/snapshot-builder.ts +243 -7
- package/src/context-mode/tools.ts +440 -0
- package/src/context-mode/web/fetcher.ts +117 -0
- package/src/context-mode/web/html-to-md.ts +293 -0
- package/src/debug/logger.ts +107 -0
- package/src/deps/registry.ts +0 -20
- package/src/docs/drift.ts +454 -0
- package/src/fix-pr/fetch-comments.ts +66 -0
- package/src/git/commit-msg.ts +2 -1
- package/src/git/commit.ts +123 -141
- package/src/git/conventions.ts +2 -2
- package/src/git/status.ts +4 -1
- package/src/lsp/bridge.ts +138 -12
- package/src/planning/approval-flow.ts +125 -19
- package/src/planning/plan-writer-prompt.ts +4 -11
- package/src/planning/planning-ask-tool.ts +81 -0
- package/src/planning/prompt-builder.ts +9 -169
- package/src/planning/system-prompt.ts +290 -0
- package/src/platform/omp.ts +50 -4
- package/src/platform/progress.ts +182 -0
- package/src/platform/test-utils.ts +4 -1
- package/src/platform/tui-colors.ts +30 -0
- package/src/platform/types.ts +1 -0
- package/src/qa/detect-app-type.ts +102 -0
- package/src/qa/discover-routes.ts +353 -0
- package/src/quality/ai-session.ts +96 -0
- package/src/quality/ai-setup.ts +86 -0
- package/src/quality/gates/ai-review.ts +129 -0
- package/src/quality/gates/build.ts +8 -0
- package/src/quality/gates/command.ts +150 -0
- package/src/quality/gates/format.ts +28 -0
- package/src/quality/gates/lint.ts +22 -0
- package/src/quality/gates/lsp-diagnostics.ts +84 -0
- package/src/quality/gates/test-suite.ts +8 -0
- package/src/quality/gates/typecheck.ts +22 -0
- package/src/quality/registry.ts +25 -0
- package/src/quality/review-gates.ts +33 -0
- package/src/quality/runner.ts +268 -0
- package/src/quality/schemas.ts +48 -0
- package/src/quality/setup.ts +227 -0
- package/src/release/changelog.ts +7 -3
- package/src/release/channels/custom.ts +43 -0
- package/src/release/channels/gitea.ts +35 -0
- package/src/release/channels/github.ts +35 -0
- package/src/release/channels/gitlab.ts +35 -0
- package/src/release/channels/registry.ts +52 -0
- package/src/release/channels/types.ts +27 -0
- package/src/release/detector.ts +10 -63
- package/src/release/executor.ts +61 -51
- package/src/release/prompt.ts +38 -38
- package/src/release/version.ts +129 -10
- package/src/review/agent-loader.ts +331 -0
- package/src/review/consolidator.ts +180 -0
- package/src/review/default-agents/correctness.md +72 -0
- package/src/review/default-agents/maintainability.md +64 -0
- package/src/review/default-agents/security.md +67 -0
- package/src/review/fixer.ts +219 -0
- package/src/review/multi-agent-runner.ts +135 -0
- package/src/review/output.ts +147 -0
- package/src/review/prompts/agent-review-wrapper.md +36 -0
- package/src/review/prompts/fix-findings.md +32 -0
- package/src/review/prompts/fix-output-schema.md +18 -0
- package/src/review/prompts/invalid-output-retry.md +22 -0
- package/src/review/prompts/output-instructions.md +14 -0
- package/src/review/prompts/review-output-schema.md +38 -0
- package/src/review/prompts/single-review.md +53 -0
- package/src/review/prompts/validation-review.md +30 -0
- package/src/review/runner.ts +128 -0
- package/src/review/scope.ts +353 -0
- package/src/review/template.ts +15 -0
- package/src/review/types.ts +296 -0
- package/src/review/validator.ts +160 -0
- package/src/storage/plans.ts +5 -3
- package/src/storage/reports.ts +50 -7
- package/src/storage/review-sessions.ts +117 -0
- package/src/text.ts +19 -0
- package/src/types.ts +336 -26
- package/src/utils/paths.ts +39 -0
- package/src/visual/companion.ts +5 -3
- package/src/visual/start-server.ts +101 -0
- package/src/visual/stop-server.ts +39 -0
- package/bin/ctx-mode-wrapper.mjs +0 -66
- package/src/config/profiles.ts +0 -64
- package/src/context-mode/installer.ts +0 -38
- package/src/quality/ai-review-gate.ts +0 -43
- package/src/quality/gate-runner.ts +0 -67
- package/src/quality/lsp-gate.ts +0 -24
- package/src/quality/test-gate.ts +0 -39
- package/src/visual/scripts/start-server.sh +0 -98
- package/src/visual/scripts/stop-server.sh +0 -21
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// src/release/channels/gitlab.ts — Built-in GitLab release channel
|
|
2
|
+
import type { ChannelHandler, ChannelPublishContext, ChannelStatus, ExecFn } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export const gitlab: ChannelHandler = {
|
|
5
|
+
id: "gitlab",
|
|
6
|
+
label: "GitLab Releases",
|
|
7
|
+
|
|
8
|
+
async detect(exec: ExecFn, cwd: string): Promise<ChannelStatus> {
|
|
9
|
+
try {
|
|
10
|
+
const result = await exec("glab", ["auth", "status"], { cwd });
|
|
11
|
+
if (result.code === 0) {
|
|
12
|
+
return { channel: "gitlab", available: true, detail: "Authenticated with GitLab CLI" };
|
|
13
|
+
}
|
|
14
|
+
return { channel: "gitlab", available: false, detail: "GitLab CLI not authenticated (run: glab auth login)" };
|
|
15
|
+
} catch {
|
|
16
|
+
return { channel: "gitlab", available: false, detail: "GitLab CLI not installed (install: https://gitlab.com/gitlab-org/cli)" };
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
async publish(exec: ExecFn, ctx: ChannelPublishContext): Promise<{ success: boolean; error?: string }> {
|
|
21
|
+
try {
|
|
22
|
+
const result = await exec(
|
|
23
|
+
"glab",
|
|
24
|
+
["release", "create", ctx.tag, "--notes", ctx.changelog],
|
|
25
|
+
{ cwd: ctx.cwd },
|
|
26
|
+
);
|
|
27
|
+
if (result.code !== 0) {
|
|
28
|
+
return { success: false, error: result.stderr || result.stdout || `glab release create exited with code ${result.code}` };
|
|
29
|
+
}
|
|
30
|
+
return { success: true };
|
|
31
|
+
} catch (err) {
|
|
32
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// src/release/channels/registry.ts — Channel handler resolution and discovery
|
|
2
|
+
import type { CustomChannelConfig } from "../../types.js";
|
|
3
|
+
import type { ChannelHandler, ChannelStatus, ExecFn } from "./types.js";
|
|
4
|
+
import { github } from "./github.js";
|
|
5
|
+
import { gitlab } from "./gitlab.js";
|
|
6
|
+
import { gitea } from "./gitea.js";
|
|
7
|
+
import { createCustomHandler } from "./custom.js";
|
|
8
|
+
|
|
9
|
+
const BUILTIN_CHANNELS: ChannelHandler[] = [github, gitlab, gitea];
|
|
10
|
+
|
|
11
|
+
const BUILTIN_MAP = new Map(BUILTIN_CHANNELS.map((h) => [h.id, h]));
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve a channel ID to a handler. Custom channel configs override built-in
|
|
15
|
+
* handlers when the IDs match. Returns null for unknown IDs with no custom config.
|
|
16
|
+
*/
|
|
17
|
+
export function resolveChannelHandler(
|
|
18
|
+
id: string,
|
|
19
|
+
customChannels: Record<string, CustomChannelConfig>,
|
|
20
|
+
): ChannelHandler | null {
|
|
21
|
+
if (id in customChannels) {
|
|
22
|
+
return createCustomHandler(id, customChannels[id]);
|
|
23
|
+
}
|
|
24
|
+
return BUILTIN_MAP.get(id) ?? null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Detect availability of all built-in channels plus any custom channels.
|
|
29
|
+
* Returns a status entry for each, ordered built-ins first, then custom.
|
|
30
|
+
*/
|
|
31
|
+
export async function getAllAvailableChannels(
|
|
32
|
+
exec: ExecFn,
|
|
33
|
+
cwd: string,
|
|
34
|
+
customChannels: Record<string, CustomChannelConfig>,
|
|
35
|
+
): Promise<ChannelStatus[]> {
|
|
36
|
+
const handlers: ChannelHandler[] = [];
|
|
37
|
+
|
|
38
|
+
for (const builtin of BUILTIN_CHANNELS) {
|
|
39
|
+
const handler = resolveChannelHandler(builtin.id, customChannels);
|
|
40
|
+
if (handler) {
|
|
41
|
+
handlers.push(handler);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (const [id, config] of Object.entries(customChannels)) {
|
|
46
|
+
if (!BUILTIN_MAP.has(id)) {
|
|
47
|
+
handlers.push(createCustomHandler(id, config));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return Promise.all(handlers.map((handler) => handler.detect(exec, cwd)));
|
|
52
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/release/channels/types.ts — Shared types for release channel handlers
|
|
2
|
+
|
|
3
|
+
export type ExecFn = (
|
|
4
|
+
cmd: string,
|
|
5
|
+
args: string[],
|
|
6
|
+
opts?: { cwd?: string; env?: Record<string, string> },
|
|
7
|
+
) => Promise<{ stdout: string; stderr: string; code: number }>;
|
|
8
|
+
|
|
9
|
+
export interface ChannelStatus {
|
|
10
|
+
channel: string;
|
|
11
|
+
available: boolean;
|
|
12
|
+
detail: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ChannelPublishContext {
|
|
16
|
+
version: string;
|
|
17
|
+
tag: string;
|
|
18
|
+
changelog: string;
|
|
19
|
+
cwd: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ChannelHandler {
|
|
23
|
+
id: string;
|
|
24
|
+
label: string;
|
|
25
|
+
detect(exec: ExecFn, cwd: string): Promise<ChannelStatus>;
|
|
26
|
+
publish(exec: ExecFn, ctx: ChannelPublishContext): Promise<{ success: boolean; error?: string }>;
|
|
27
|
+
}
|
package/src/release/detector.ts
CHANGED
|
@@ -1,69 +1,16 @@
|
|
|
1
1
|
// src/release/detector.ts — Detect which release channels are available
|
|
2
|
-
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
type ExecFn = (
|
|
6
|
-
cmd: string,
|
|
7
|
-
args: string[],
|
|
8
|
-
opts?: { cwd?: string },
|
|
9
|
-
) => Promise<{ stdout: string; stderr: string; code: number }>;
|
|
10
|
-
|
|
11
|
-
export interface ChannelStatus {
|
|
12
|
-
channel: ReleaseChannel;
|
|
13
|
-
available: boolean;
|
|
14
|
-
detail: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
async function checkGitHub(exec: ExecFn, cwd: string): Promise<ChannelStatus> {
|
|
18
|
-
try {
|
|
19
|
-
const result = await exec("gh", ["auth", "status"], { cwd });
|
|
20
|
-
if (result.code === 0) {
|
|
21
|
-
return { channel: "github", available: true, detail: "Authenticated with GitHub CLI" };
|
|
22
|
-
}
|
|
23
|
-
return {
|
|
24
|
-
channel: "github",
|
|
25
|
-
available: false,
|
|
26
|
-
detail: "GitHub CLI not authenticated (run: gh auth login)",
|
|
27
|
-
};
|
|
28
|
-
} catch {
|
|
29
|
-
return {
|
|
30
|
-
channel: "github",
|
|
31
|
-
available: false,
|
|
32
|
-
detail: "GitHub CLI not authenticated (run: gh auth login)",
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function checkNpm(exec: ExecFn, cwd: string): Promise<ChannelStatus> {
|
|
38
|
-
try {
|
|
39
|
-
const result = await exec("npm", ["whoami"], { cwd });
|
|
40
|
-
if (result.code === 0) {
|
|
41
|
-
return {
|
|
42
|
-
channel: "npm",
|
|
43
|
-
available: true,
|
|
44
|
-
detail: `Logged in as ${result.stdout.trim()}`,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
return {
|
|
48
|
-
channel: "npm",
|
|
49
|
-
available: false,
|
|
50
|
-
detail: "npm not authenticated (run: npm login)",
|
|
51
|
-
};
|
|
52
|
-
} catch {
|
|
53
|
-
return {
|
|
54
|
-
channel: "npm",
|
|
55
|
-
available: false,
|
|
56
|
-
detail: "npm not authenticated (run: npm login)",
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
}
|
|
2
|
+
import type { CustomChannelConfig } from "../types.js";
|
|
3
|
+
import type { ChannelStatus, ExecFn } from "./channels/types.js";
|
|
4
|
+
import { getAllAvailableChannels } from "./channels/registry.js";
|
|
60
5
|
|
|
61
6
|
/**
|
|
62
7
|
* Detect which release channels are available in the current environment.
|
|
63
|
-
*
|
|
64
|
-
* affect the other.
|
|
8
|
+
* Checks all built-in channels plus any user-defined custom channels.
|
|
65
9
|
*/
|
|
66
|
-
export async function detectChannels(
|
|
67
|
-
|
|
68
|
-
|
|
10
|
+
export async function detectChannels(
|
|
11
|
+
exec: ExecFn,
|
|
12
|
+
cwd: string,
|
|
13
|
+
customChannels: Record<string, CustomChannelConfig> = {},
|
|
14
|
+
): Promise<ChannelStatus[]> {
|
|
15
|
+
return getAllAvailableChannels(exec, cwd, customChannels);
|
|
69
16
|
}
|
package/src/release/executor.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
// src/release/executor.ts — Release execution: build, version bump, git ops, channel publishing
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import type { ReleaseChannel, ReleaseResult } from "../types.js";
|
|
4
|
+
import type { CustomChannelConfig, ReleaseChannel, ReleaseResult } from "../types.js";
|
|
5
5
|
import { commitStaged } from "../git/commit.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
args: string[],
|
|
10
|
-
opts?: { cwd?: string },
|
|
11
|
-
) => Promise<{ stdout: string; stderr: string; code: number }>;
|
|
6
|
+
import { formatTag } from "./version.js";
|
|
7
|
+
import { resolveChannelHandler } from "./channels/registry.js";
|
|
8
|
+
import type { ExecFn } from "./channels/types.js";
|
|
12
9
|
|
|
13
10
|
/** Callback to report step progress during release execution. */
|
|
14
11
|
export type ReleaseProgressFn = (step: string, status: "active" | "done" | "error", detail?: string) => void;
|
|
@@ -20,9 +17,12 @@ export interface ExecuteReleaseOptions {
|
|
|
20
17
|
changelog: string;
|
|
21
18
|
channels: ReleaseChannel[];
|
|
22
19
|
dryRun: boolean;
|
|
20
|
+
tagFormat: string;
|
|
21
|
+
/** User-defined custom channel configurations */
|
|
22
|
+
customChannels?: Record<string, CustomChannelConfig>;
|
|
23
23
|
/** Skip the package.json version write (version was already set locally). */
|
|
24
24
|
skipBump?: boolean;
|
|
25
|
-
/**
|
|
25
|
+
/** Reuse/update the local git tag after the rebase step instead of creating a new one. */
|
|
26
26
|
skipTag?: boolean;
|
|
27
27
|
/** Optional callback for step-by-step progress reporting. */
|
|
28
28
|
onProgress?: ReleaseProgressFn;
|
|
@@ -39,18 +39,21 @@ export interface ExecuteReleaseOptions {
|
|
|
39
39
|
* happen (all flags true) so callers can preview without side-effects.
|
|
40
40
|
*/
|
|
41
41
|
export async function executeRelease(opts: ExecuteReleaseOptions): Promise<ReleaseResult> {
|
|
42
|
-
const { exec, cwd, version, changelog, channels, dryRun, skipBump, skipTag, onProgress } = opts;
|
|
42
|
+
const { exec, cwd, version, changelog, channels, dryRun, tagFormat, skipBump, skipTag, onProgress, customChannels = {} } = opts;
|
|
43
43
|
const progress = onProgress ?? (() => {});
|
|
44
|
+
const tagName = formatTag(version, tagFormat);
|
|
45
|
+
const tagMessage = `Release ${tagName}\n\n${changelog}`;
|
|
44
46
|
|
|
45
47
|
if (dryRun) {
|
|
46
48
|
console.log(`[dry-run] Would build (if scripts.build exists)`);
|
|
47
49
|
console.log(`[dry-run] Would bump version to ${version}`);
|
|
48
50
|
console.log(`[dry-run] Would git add -A`);
|
|
49
|
-
console.log(`[dry-run] Would git commit -m "chore(release):
|
|
51
|
+
console.log(`[dry-run] Would git commit -m "chore(release): ${tagName}"`);
|
|
52
|
+
console.log(`[dry-run] Would git pull --rebase origin`);
|
|
50
53
|
if (skipTag) {
|
|
51
|
-
console.log(`[dry-run] Would
|
|
54
|
+
console.log(`[dry-run] Would refresh existing git tag ${tagName} to the rebased HEAD`);
|
|
52
55
|
} else {
|
|
53
|
-
console.log(`[dry-run] Would git tag -a
|
|
56
|
+
console.log(`[dry-run] Would git tag -a ${tagName}`);
|
|
54
57
|
}
|
|
55
58
|
console.log(`[dry-run] Would git push origin HEAD --follow-tags`);
|
|
56
59
|
for (const ch of channels) {
|
|
@@ -107,7 +110,7 @@ export async function executeRelease(opts: ExecuteReleaseOptions): Promise<Relea
|
|
|
107
110
|
}
|
|
108
111
|
progress("git-add", "done");
|
|
109
112
|
|
|
110
|
-
const commitMessage = `chore(release):
|
|
113
|
+
const commitMessage = `chore(release): ${formatTag(version, tagFormat)}`;
|
|
111
114
|
progress("git-commit", "active", commitMessage);
|
|
112
115
|
const commitResult = await commitStaged(exec, cwd, commitMessage);
|
|
113
116
|
if (!commitResult.success) {
|
|
@@ -117,26 +120,36 @@ export async function executeRelease(opts: ExecuteReleaseOptions): Promise<Relea
|
|
|
117
120
|
progress("git-commit", "done");
|
|
118
121
|
}
|
|
119
122
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (gitTag.code !== 0) {
|
|
127
|
-
const detail = gitTag.stderr || gitTag.stdout || `exit code ${gitTag.code}`;
|
|
128
|
-
progress("git-tag", "error", detail);
|
|
129
|
-
return { version, tagCreated: false, pushed: false, channels: [], error: `git tag: ${detail}` };
|
|
130
|
-
}
|
|
131
|
-
progress("git-tag", "done");
|
|
123
|
+
progress("git-pull", "active", "Pulling latest from origin");
|
|
124
|
+
const gitPull = await exec("git", ["pull", "--rebase", "origin"], { cwd });
|
|
125
|
+
if (gitPull.code !== 0) {
|
|
126
|
+
const detail = gitPull.stderr || gitPull.stdout || `exit code ${gitPull.code}`;
|
|
127
|
+
progress("git-pull", "error", detail);
|
|
128
|
+
return { version, tagCreated: Boolean(skipTag), pushed: false, channels: [], error: `git pull: ${detail}` };
|
|
132
129
|
}
|
|
130
|
+
progress("git-pull", "done");
|
|
131
|
+
|
|
132
|
+
progress("git-tag", "active", skipTag ? `Refreshing ${tagName}` : tagName);
|
|
133
|
+
const gitTag = await exec(
|
|
134
|
+
"git",
|
|
135
|
+
skipTag
|
|
136
|
+
? ["tag", "-a", "-f", tagName, "-m", tagMessage]
|
|
137
|
+
: ["tag", "-a", tagName, "-m", tagMessage],
|
|
138
|
+
{ cwd },
|
|
139
|
+
);
|
|
140
|
+
if (gitTag.code !== 0) {
|
|
141
|
+
const detail = gitTag.stderr || gitTag.stdout || `exit code ${gitTag.code}`;
|
|
142
|
+
progress("git-tag", "error", detail);
|
|
143
|
+
return { version, tagCreated: Boolean(skipTag), pushed: false, channels: [], error: `git tag: ${detail}` };
|
|
144
|
+
}
|
|
145
|
+
progress("git-tag", "done", skipTag ? "Refreshed existing tag" : undefined);
|
|
133
146
|
|
|
134
147
|
progress("git-push", "active", "Pushing to origin");
|
|
135
148
|
const gitPush = await exec("git", ["push", "origin", "HEAD", "--follow-tags"], { cwd });
|
|
136
149
|
if (gitPush.code !== 0) {
|
|
137
150
|
const detail = gitPush.stderr || gitPush.stdout || `exit code ${gitPush.code}`;
|
|
138
151
|
progress("git-push", "error", detail);
|
|
139
|
-
// Tag was created locally but push failed
|
|
152
|
+
// Tag was created or refreshed locally but push failed
|
|
140
153
|
return { version, tagCreated: true, pushed: false, channels: [], error: `git push: ${detail}` };
|
|
141
154
|
}
|
|
142
155
|
progress("git-push", "done");
|
|
@@ -146,31 +159,28 @@ export async function executeRelease(opts: ExecuteReleaseOptions): Promise<Relea
|
|
|
146
159
|
|
|
147
160
|
for (const channel of channels) {
|
|
148
161
|
progress(`publish-${channel}`, "active", `Publishing to ${channel}`);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
172
|
-
progress(`publish-${channel}`, "error", msg);
|
|
173
|
-
channelResults.push({ channel, success: false, error: msg });
|
|
162
|
+
|
|
163
|
+
const handler = resolveChannelHandler(channel, customChannels);
|
|
164
|
+
if (!handler) {
|
|
165
|
+
const err = `Unknown channel '${channel}' — no built-in handler and no custom config found`;
|
|
166
|
+
progress(`publish-${channel}`, "error", err);
|
|
167
|
+
channelResults.push({ channel, success: false, error: err });
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const result = await handler.publish(exec, {
|
|
172
|
+
version,
|
|
173
|
+
tag: formatTag(version, tagFormat),
|
|
174
|
+
changelog,
|
|
175
|
+
cwd,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
if (result.success) {
|
|
179
|
+
progress(`publish-${channel}`, "done");
|
|
180
|
+
channelResults.push({ channel, success: true });
|
|
181
|
+
} else {
|
|
182
|
+
progress(`publish-${channel}`, "error", result.error);
|
|
183
|
+
channelResults.push({ channel, success: false, error: result.error });
|
|
174
184
|
}
|
|
175
185
|
}
|
|
176
186
|
|
package/src/release/prompt.ts
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { formatTag } from "./version.js";
|
|
2
2
|
|
|
3
3
|
export interface BuildPolishPromptOpts {
|
|
4
4
|
changelog: string;
|
|
5
5
|
version: string;
|
|
6
|
-
|
|
7
|
-
channels: ReleaseChannel[];
|
|
8
|
-
commands: string[];
|
|
6
|
+
tagFormat?: string;
|
|
9
7
|
}
|
|
10
8
|
|
|
11
9
|
/**
|
|
12
|
-
* Build a
|
|
13
|
-
*
|
|
10
|
+
* Build a prompt for a headless agent session that polishes raw changelog
|
|
11
|
+
* text into clear, user-facing release notes.
|
|
12
|
+
*
|
|
13
|
+
* The agent returns only the polished markdown — no commands, no confirmation.
|
|
14
14
|
*/
|
|
15
15
|
export function buildPolishPrompt(opts: BuildPolishPromptOpts): string {
|
|
16
|
-
const { changelog, version
|
|
17
|
-
|
|
18
|
-
const channelList = channels.map((c) => `- ${c}`).join("\n");
|
|
19
|
-
const commandList = commands.map((c) => `- \`${c}\``).join("\n");
|
|
16
|
+
const { changelog, version } = opts;
|
|
20
17
|
|
|
21
18
|
const changelogSection =
|
|
22
19
|
changelog.trim().length > 0
|
|
@@ -24,48 +21,51 @@ export function buildPolishPrompt(opts: BuildPolishPromptOpts): string {
|
|
|
24
21
|
: "_No notable changes in this release._";
|
|
25
22
|
|
|
26
23
|
return [
|
|
27
|
-
"#
|
|
28
|
-
"",
|
|
29
|
-
`You are preparing a release of **${version}** (from ${currentVersion}).`,
|
|
24
|
+
"# Polish Release Notes",
|
|
30
25
|
"",
|
|
31
|
-
|
|
26
|
+
`You are polishing changelog notes for **${formatTag(version, opts.tagFormat ?? "v${version}")}**.`,
|
|
32
27
|
"",
|
|
33
|
-
|
|
28
|
+
"## Raw Changelog",
|
|
34
29
|
"",
|
|
35
|
-
|
|
30
|
+
changelogSection,
|
|
36
31
|
"",
|
|
37
|
-
"
|
|
38
|
-
"terse commit-message language.",
|
|
32
|
+
"## Instructions",
|
|
39
33
|
"",
|
|
40
|
-
|
|
34
|
+
"Rewrite the raw changelog above into clear, user-facing release notes.",
|
|
41
35
|
"",
|
|
42
|
-
"
|
|
36
|
+
"### Content rules",
|
|
43
37
|
"",
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
38
|
+
"| Rule | Details |",
|
|
39
|
+
"|------|---------|",
|
|
40
|
+
"| Plain English | Rewrite terse commit descriptions so a user understands the impact |",
|
|
41
|
+
"| Group related | Merge closely related changes into a single bullet |",
|
|
42
|
+
"| Remove noise | Drop chores, reformats, typo fixes — unless they fix a user-visible bug |",
|
|
43
|
+
"| No invented changes | Every bullet must trace back to the raw changelog |",
|
|
44
|
+
"| Preserve headings | Keep section order: Breaking Changes, Features, Fixes, Improvements, Maintenance, Other. Omit empty sections |",
|
|
45
|
+
"| Preserve versions | Do **not** change version numbers |",
|
|
51
46
|
"",
|
|
52
|
-
"
|
|
47
|
+
"### Superseded changes",
|
|
53
48
|
"",
|
|
54
|
-
"
|
|
55
|
-
' > "Does this look good? Type **yes** to proceed with the release, or **no** to abort."',
|
|
49
|
+
"Drop entries whose effect is entirely negated or replaced by a later entry in the same release.",
|
|
56
50
|
"",
|
|
57
|
-
"
|
|
51
|
+
"Detect these patterns:",
|
|
52
|
+
"- **Replaced**: feature A is added, then later replaced by feature B → keep only B.",
|
|
53
|
+
"- **Reverted**: a change is introduced and then reverted → drop both.",
|
|
54
|
+
"- **Evolved**: an item is added, then renamed/rewritten → keep only the final form.",
|
|
58
55
|
"",
|
|
59
|
-
"
|
|
56
|
+
"The changelog describes the **net state** of the release — what a user upgrading from the previous version actually gets — not the development history that produced it.",
|
|
60
57
|
"",
|
|
61
|
-
|
|
58
|
+
"```",
|
|
59
|
+
"before (wrong):",
|
|
60
|
+
"- Added caching layer for API responses",
|
|
61
|
+
"- Replaced caching layer with Redis-backed store",
|
|
62
62
|
"",
|
|
63
|
-
"
|
|
64
|
-
"
|
|
63
|
+
"after (correct):",
|
|
64
|
+
"- Added Redis-backed caching for API responses",
|
|
65
|
+
"```",
|
|
65
66
|
"",
|
|
66
|
-
"##
|
|
67
|
+
"## Output",
|
|
67
68
|
"",
|
|
68
|
-
"
|
|
69
|
-
"Inform the user: _Release aborted. No changes were made._",
|
|
69
|
+
"Return **only** the polished markdown changelog. No preamble, no explanation, no code fences wrapping the whole output.",
|
|
70
70
|
].join("\n");
|
|
71
71
|
}
|
package/src/release/version.ts
CHANGED
|
@@ -8,6 +8,14 @@ type ExecFn = (
|
|
|
8
8
|
opts?: { cwd?: string },
|
|
9
9
|
) => Promise<{ stdout: string; stderr: string; code: number }>;
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Replace `${version}` in a tag format template with the actual version string.
|
|
13
|
+
* If the template contains no placeholder, it is returned as-is.
|
|
14
|
+
*/
|
|
15
|
+
export function formatTag(version: string, tagFormat: string): string {
|
|
16
|
+
return tagFormat.replace("${version}", version);
|
|
17
|
+
}
|
|
18
|
+
|
|
11
19
|
/**
|
|
12
20
|
* Suggest a semver bump type based on categorized commits.
|
|
13
21
|
* Breaking changes win over features; features win over everything else.
|
|
@@ -59,22 +67,133 @@ export function getCurrentVersion(cwd: string): string {
|
|
|
59
67
|
}
|
|
60
68
|
}
|
|
61
69
|
|
|
70
|
+
interface ParsedReleaseTag {
|
|
71
|
+
tag: string;
|
|
72
|
+
version: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const SEMVER_CAPTURE = "([0-9]+\\.[0-9]+\\.[0-9]+(?:-[0-9A-Za-z.-]+)?)";
|
|
76
|
+
|
|
77
|
+
function escapeRegExp(value: string): string {
|
|
78
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function extractVersionFromTag(tag: string, tagFormat: string): string | null {
|
|
82
|
+
if (!tagFormat.includes("${version}")) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const pattern = `^${escapeRegExp(tagFormat).replace(escapeRegExp("${version}"), SEMVER_CAPTURE)}$`;
|
|
87
|
+
const match = tag.match(new RegExp(pattern));
|
|
88
|
+
return match?.[1] ?? null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function parseReleaseTag(tag: string, tagFormat: string): ParsedReleaseTag | null {
|
|
92
|
+
const version = extractVersionFromTag(tag, tagFormat) ?? extractVersionFromTag(tag, LEGACY_RELEASE_TAG_FORMAT);
|
|
93
|
+
return version ? { tag, version } : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function compareSemver(left: string, right: string): number {
|
|
97
|
+
const [leftCore] = left.split("-");
|
|
98
|
+
const [rightCore] = right.split("-");
|
|
99
|
+
const leftParts = leftCore.split(".").map(Number);
|
|
100
|
+
const rightParts = rightCore.split(".").map(Number);
|
|
101
|
+
|
|
102
|
+
for (let index = 0; index < 3; index += 1) {
|
|
103
|
+
const delta = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
|
|
104
|
+
if (delta !== 0) {
|
|
105
|
+
return delta;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (left === right) {
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return left.includes("-") ? -1 : 1;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function isTagAtHead(exec: ExecFn, cwd: string, tag: string): Promise<boolean> {
|
|
117
|
+
const [tagCommit, headCommit] = await Promise.all([
|
|
118
|
+
exec("git", ["rev-list", "-n", "1", tag], { cwd }),
|
|
119
|
+
exec("git", ["rev-parse", "HEAD"], { cwd }),
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
return tagCommit.code === 0
|
|
123
|
+
&& headCommit.code === 0
|
|
124
|
+
&& tagCommit.stdout.trim() !== ""
|
|
125
|
+
&& tagCommit.stdout.trim() === headCommit.stdout.trim();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function findResumableLocalRelease(
|
|
129
|
+
exec: ExecFn,
|
|
130
|
+
cwd: string,
|
|
131
|
+
currentVersion: string,
|
|
132
|
+
tagFormat: string,
|
|
133
|
+
): Promise<{ version: string; tag: string } | null> {
|
|
134
|
+
try {
|
|
135
|
+
const localTags = await exec("git", ["tag", "--merged", "HEAD"], { cwd });
|
|
136
|
+
if (localTags.code !== 0) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const candidates = localTags.stdout
|
|
141
|
+
.split(/\r?\n/)
|
|
142
|
+
.map((tag) => tag.trim())
|
|
143
|
+
.filter(Boolean)
|
|
144
|
+
.map((tag) => parseReleaseTag(tag, tagFormat))
|
|
145
|
+
.filter((candidate): candidate is ParsedReleaseTag => Boolean(candidate))
|
|
146
|
+
.filter((candidate) => compareSemver(candidate.version, currentVersion) > 0)
|
|
147
|
+
.sort((left, right) => compareSemver(right.version, left.version));
|
|
148
|
+
|
|
149
|
+
for (const candidate of candidates) {
|
|
150
|
+
const remoteTag = await exec("git", ["ls-remote", "--tags", "origin", candidate.tag], { cwd });
|
|
151
|
+
if (remoteTag.code !== 0 || remoteTag.stdout.trim() !== "") {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (await isTagAtHead(exec, cwd, candidate.tag)) {
|
|
156
|
+
return { version: candidate.version, tag: candidate.tag };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return null;
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
const LEGACY_RELEASE_TAG_FORMAT = "v${version}";
|
|
168
|
+
|
|
169
|
+
function getReleaseTagCandidates(version: string, tagFormat: string): string[] {
|
|
170
|
+
return [...new Set([formatTag(version, tagFormat), formatTag(version, LEGACY_RELEASE_TAG_FORMAT)])];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function hasMatchingReleaseTag(
|
|
174
|
+
exec: ExecFn,
|
|
175
|
+
cwd: string,
|
|
176
|
+
args: string[],
|
|
177
|
+
version: string,
|
|
178
|
+
tagFormat: string,
|
|
179
|
+
): Promise<boolean> {
|
|
180
|
+
const result = await exec("git", [...args, ...getReleaseTagCandidates(version, tagFormat)], { cwd });
|
|
181
|
+
return result.code === 0 && result.stdout.trim().length > 0;
|
|
182
|
+
}
|
|
62
183
|
|
|
63
184
|
/**
|
|
64
185
|
* Check whether a tag for the given version already exists locally.
|
|
65
|
-
* Returns `true` when
|
|
66
|
-
*
|
|
186
|
+
* Returns `true` when either the current-format tag or the legacy `v{version}`
|
|
187
|
+
* tag is already known locally, which means this version was already released.
|
|
67
188
|
*/
|
|
68
189
|
export async function isVersionReleased(
|
|
69
190
|
exec: ExecFn,
|
|
70
191
|
cwd: string,
|
|
71
192
|
version: string,
|
|
193
|
+
tagFormat: string,
|
|
72
194
|
): Promise<boolean> {
|
|
73
195
|
try {
|
|
74
|
-
|
|
75
|
-
const result = await exec("git", ["tag", "-l", tag], { cwd });
|
|
76
|
-
// git tag -l prints matching tags, one per line. Non-empty = exists.
|
|
77
|
-
return result.code === 0 && result.stdout.trim().length > 0;
|
|
196
|
+
return await hasMatchingReleaseTag(exec, cwd, ["tag", "-l"], version, tagFormat);
|
|
78
197
|
} catch {
|
|
79
198
|
return false;
|
|
80
199
|
}
|
|
@@ -82,17 +201,17 @@ export async function isVersionReleased(
|
|
|
82
201
|
|
|
83
202
|
/**
|
|
84
203
|
* Check whether a tag for the given version exists on the remote (origin).
|
|
85
|
-
* Returns true when
|
|
204
|
+
* Returns true when either the current-format tag or the legacy `v{version}`
|
|
205
|
+
* tag is found via `git ls-remote --tags origin`.
|
|
86
206
|
*/
|
|
87
207
|
export async function isTagOnRemote(
|
|
88
208
|
exec: ExecFn,
|
|
89
209
|
cwd: string,
|
|
90
210
|
version: string,
|
|
211
|
+
tagFormat: string,
|
|
91
212
|
): Promise<boolean> {
|
|
92
213
|
try {
|
|
93
|
-
|
|
94
|
-
const result = await exec("git", ["ls-remote", "--tags", "origin", tag], { cwd });
|
|
95
|
-
return result.code === 0 && result.stdout.trim().length > 0;
|
|
214
|
+
return await hasMatchingReleaseTag(exec, cwd, ["ls-remote", "--tags", "origin"], version, tagFormat);
|
|
96
215
|
} catch {
|
|
97
216
|
return false;
|
|
98
217
|
}
|