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,268 @@
|
|
|
1
|
+
// src/quality/runner.ts
|
|
2
|
+
import type { ExecOptions, ExecResult, Platform } from "../platform/types.js";
|
|
3
|
+
import type {
|
|
4
|
+
GateDefinition,
|
|
5
|
+
GateExecutionContext,
|
|
6
|
+
GateFilters,
|
|
7
|
+
GateId,
|
|
8
|
+
GateResult,
|
|
9
|
+
GateSummary,
|
|
10
|
+
QualityGatesConfig,
|
|
11
|
+
ResolvedModel,
|
|
12
|
+
ReviewReport,
|
|
13
|
+
} from "../types.js";
|
|
14
|
+
import { CANONICAL_GATE_ORDER, type GateRegistry } from "./registry.js";
|
|
15
|
+
import { collectLspDiagnostics } from "../lsp/bridge.js";
|
|
16
|
+
|
|
17
|
+
interface ReviewScope {
|
|
18
|
+
changedFiles: string[];
|
|
19
|
+
scopeFiles: string[];
|
|
20
|
+
fileScope: GateExecutionContext["fileScope"];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ReviewRunEvent =
|
|
24
|
+
| {
|
|
25
|
+
type: "scope-discovered";
|
|
26
|
+
changedFiles: number;
|
|
27
|
+
scopeFiles: number;
|
|
28
|
+
fileScope: GateExecutionContext["fileScope"];
|
|
29
|
+
}
|
|
30
|
+
| { type: "gate-started"; gateId: GateId }
|
|
31
|
+
| { type: "gate-skipped"; gateId: GateId; reason: string }
|
|
32
|
+
| {
|
|
33
|
+
type: "gate-completed";
|
|
34
|
+
gateId: GateId;
|
|
35
|
+
status: GateResult["status"];
|
|
36
|
+
summary: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export interface RunQualityGatesInput {
|
|
40
|
+
platform: Pick<Platform, "exec" | "getActiveTools" | "createAgentSession">;
|
|
41
|
+
cwd: string;
|
|
42
|
+
gates: QualityGatesConfig;
|
|
43
|
+
filters: GateFilters;
|
|
44
|
+
reviewModel: ResolvedModel;
|
|
45
|
+
gateRegistry?: GateRegistry;
|
|
46
|
+
getLspDiagnostics?: GateExecutionContext["getLspDiagnostics"];
|
|
47
|
+
now?: () => Date;
|
|
48
|
+
onEvent?: (event: ReviewRunEvent) => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isGateEnabled(config: QualityGatesConfig[GateId] | undefined): boolean {
|
|
52
|
+
if (!config) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return config.enabled === true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeFileList(...chunks: string[]): string[] {
|
|
60
|
+
const seen = new Set<string>();
|
|
61
|
+
|
|
62
|
+
for (const chunk of chunks) {
|
|
63
|
+
for (const line of chunk.split("\n")) {
|
|
64
|
+
const file = line.trim();
|
|
65
|
+
if (file.length === 0) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
seen.add(file);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return [...seen];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function safeExec(
|
|
76
|
+
exec: Platform["exec"],
|
|
77
|
+
cmd: string,
|
|
78
|
+
args: string[],
|
|
79
|
+
opts?: ExecOptions,
|
|
80
|
+
): Promise<ExecResult> {
|
|
81
|
+
try {
|
|
82
|
+
return await exec(cmd, args, opts);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return {
|
|
85
|
+
stdout: "",
|
|
86
|
+
stderr: (error as Error).message,
|
|
87
|
+
code: 1,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function discoverReviewScope(
|
|
93
|
+
exec: Platform["exec"],
|
|
94
|
+
cwd: string,
|
|
95
|
+
): Promise<ReviewScope> {
|
|
96
|
+
const head = await safeExec(exec, "git", ["diff", "--name-only", "HEAD"], { cwd });
|
|
97
|
+
const cached = await safeExec(exec, "git", ["diff", "--name-only", "--cached"], { cwd });
|
|
98
|
+
const untracked = await safeExec(exec, "git", ["ls-files", "--others", "--exclude-standard"], { cwd });
|
|
99
|
+
const changedFiles = normalizeFileList(head.stdout, cached.stdout, untracked.stdout);
|
|
100
|
+
|
|
101
|
+
if (changedFiles.length > 0) {
|
|
102
|
+
return {
|
|
103
|
+
changedFiles,
|
|
104
|
+
scopeFiles: changedFiles,
|
|
105
|
+
fileScope: "changed-files",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const tracked = await safeExec(exec, "git", ["ls-files"], { cwd });
|
|
110
|
+
const scopeFiles = normalizeFileList(tracked.stdout);
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
changedFiles: [],
|
|
114
|
+
scopeFiles,
|
|
115
|
+
fileScope: "all-files",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function selectConfiguredGates(gates: QualityGatesConfig, filters: GateFilters): GateId[] {
|
|
120
|
+
const enabledGates = CANONICAL_GATE_ORDER.filter((gateId) =>
|
|
121
|
+
isGateEnabled(gates[gateId]),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
if (filters.only && filters.only.length > 0) {
|
|
125
|
+
const only = new Set(filters.only);
|
|
126
|
+
return enabledGates.filter((gateId) => only.has(gateId));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return enabledGates;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function createExecShell(exec: Platform["exec"]): GateExecutionContext["execShell"] {
|
|
133
|
+
return async (command: string, opts?: ExecOptions): Promise<ExecResult> => {
|
|
134
|
+
if (process.platform === "win32") {
|
|
135
|
+
return exec("cmd", ["/d", "/s", "/c", command], opts);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return exec("sh", ["-lc", command], opts);
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function createGateExecutionContext(
|
|
143
|
+
input: RunQualityGatesInput,
|
|
144
|
+
scope: ReviewScope,
|
|
145
|
+
): GateExecutionContext {
|
|
146
|
+
const exec = input.platform.exec.bind(input.platform);
|
|
147
|
+
const createAgentSession = input.platform.createAgentSession.bind(input.platform);
|
|
148
|
+
const activeTools = input.platform.getActiveTools();
|
|
149
|
+
const reviewModel = {
|
|
150
|
+
model: input.reviewModel.model,
|
|
151
|
+
thinkingLevel: input.reviewModel.thinkingLevel,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
cwd: input.cwd,
|
|
156
|
+
changedFiles: scope.changedFiles,
|
|
157
|
+
scopeFiles: scope.scopeFiles,
|
|
158
|
+
fileScope: scope.fileScope,
|
|
159
|
+
exec,
|
|
160
|
+
execShell: createExecShell(exec),
|
|
161
|
+
getLspDiagnostics:
|
|
162
|
+
input.getLspDiagnostics ??
|
|
163
|
+
((scopeFiles, fileScope) =>
|
|
164
|
+
collectLspDiagnostics({
|
|
165
|
+
cwd: input.cwd,
|
|
166
|
+
scopeFiles,
|
|
167
|
+
fileScope,
|
|
168
|
+
createAgentSession,
|
|
169
|
+
reviewModel,
|
|
170
|
+
})),
|
|
171
|
+
createAgentSession,
|
|
172
|
+
activeTools,
|
|
173
|
+
reviewModel,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function createSkippedGateResult(gate: GateId, reason = "Skipped by filter"): GateResult {
|
|
178
|
+
return {
|
|
179
|
+
gate,
|
|
180
|
+
status: "skipped",
|
|
181
|
+
summary: reason,
|
|
182
|
+
issues: [],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function runConfiguredGate(
|
|
187
|
+
gateId: GateId,
|
|
188
|
+
registry: GateRegistry,
|
|
189
|
+
context: GateExecutionContext,
|
|
190
|
+
gates: QualityGatesConfig,
|
|
191
|
+
): Promise<GateResult> {
|
|
192
|
+
const definition = registry[gateId] as GateDefinition<NonNullable<QualityGatesConfig[typeof gateId]>> | undefined;
|
|
193
|
+
if (!definition) {
|
|
194
|
+
throw new Error(`Gate definition not registered: ${gateId}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const config = gates[gateId];
|
|
198
|
+
if (!config || config.enabled !== true) {
|
|
199
|
+
throw new Error(`Gate ${gateId} is not enabled`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return definition.run(context, config as NonNullable<QualityGatesConfig[typeof gateId]>);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function summarizeGateStatuses(gates: GateResult[]): GateSummary {
|
|
206
|
+
return gates.reduce<GateSummary>(
|
|
207
|
+
(summary, gate) => {
|
|
208
|
+
summary[gate.status] += 1;
|
|
209
|
+
return summary;
|
|
210
|
+
},
|
|
211
|
+
{ passed: 0, failed: 0, skipped: 0, blocked: 0 },
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function computeOverallStatus(summary: GateSummary): ReviewReport["overallStatus"] {
|
|
216
|
+
if (summary.blocked > 0) {
|
|
217
|
+
return "blocked";
|
|
218
|
+
}
|
|
219
|
+
if (summary.failed > 0) {
|
|
220
|
+
return "failed";
|
|
221
|
+
}
|
|
222
|
+
return "passed";
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export async function runQualityGates(input: RunQualityGatesInput): Promise<ReviewReport> {
|
|
226
|
+
const selectedGates = selectConfiguredGates(input.gates, input.filters);
|
|
227
|
+
const scope = await discoverReviewScope(input.platform.exec.bind(input.platform), input.cwd);
|
|
228
|
+
input.onEvent?.({
|
|
229
|
+
type: "scope-discovered",
|
|
230
|
+
changedFiles: scope.changedFiles.length,
|
|
231
|
+
scopeFiles: scope.scopeFiles.length,
|
|
232
|
+
fileScope: scope.fileScope,
|
|
233
|
+
});
|
|
234
|
+
const context = createGateExecutionContext(input, scope);
|
|
235
|
+
const skipped = new Set(input.filters.skip ?? []);
|
|
236
|
+
const registry = input.gateRegistry ?? {};
|
|
237
|
+
const gates: GateResult[] = [];
|
|
238
|
+
|
|
239
|
+
// Run all gates concurrently — each gate is independent (no shared mutable state)
|
|
240
|
+
const promises = selectedGates.map(async (gateId): Promise<GateResult> => {
|
|
241
|
+
if (skipped.has(gateId)) {
|
|
242
|
+
input.onEvent?.({ type: "gate-skipped", gateId, reason: "Skipped by filter" });
|
|
243
|
+
return createSkippedGateResult(gateId);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
input.onEvent?.({ type: "gate-started", gateId });
|
|
247
|
+
const result = await runConfiguredGate(gateId, registry, context, input.gates);
|
|
248
|
+
input.onEvent?.({
|
|
249
|
+
type: "gate-completed",
|
|
250
|
+
gateId,
|
|
251
|
+
status: result.status,
|
|
252
|
+
summary: result.summary,
|
|
253
|
+
});
|
|
254
|
+
return result;
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
gates.push(...await Promise.all(promises));
|
|
258
|
+
|
|
259
|
+
const summary = summarizeGateStatuses(gates);
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
timestamp: (input.now ?? (() => new Date()))().toISOString(),
|
|
263
|
+
selectedGates,
|
|
264
|
+
gates,
|
|
265
|
+
summary,
|
|
266
|
+
overallStatus: computeOverallStatus(summary),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { TSchema } from "@sinclair/typebox";
|
|
3
|
+
import type { GateId } from "../types.js";
|
|
4
|
+
|
|
5
|
+
export const LspDiagnosticsGateConfigSchema = Type.Object(
|
|
6
|
+
{
|
|
7
|
+
enabled: Type.Boolean(),
|
|
8
|
+
},
|
|
9
|
+
{ additionalProperties: false },
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
export const CommandGateConfigSchema = Type.Union([
|
|
13
|
+
Type.Object(
|
|
14
|
+
{
|
|
15
|
+
enabled: Type.Literal(false),
|
|
16
|
+
command: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
17
|
+
},
|
|
18
|
+
{ additionalProperties: false },
|
|
19
|
+
),
|
|
20
|
+
Type.Object(
|
|
21
|
+
{
|
|
22
|
+
enabled: Type.Literal(true),
|
|
23
|
+
command: Type.String({ minLength: 1 }),
|
|
24
|
+
},
|
|
25
|
+
{ additionalProperties: false },
|
|
26
|
+
),
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
export const QualityGatesSchema = Type.Object(
|
|
30
|
+
{
|
|
31
|
+
"lsp-diagnostics": Type.Optional(LspDiagnosticsGateConfigSchema),
|
|
32
|
+
lint: Type.Optional(CommandGateConfigSchema),
|
|
33
|
+
typecheck: Type.Optional(CommandGateConfigSchema),
|
|
34
|
+
format: Type.Optional(CommandGateConfigSchema),
|
|
35
|
+
"test-suite": Type.Optional(CommandGateConfigSchema),
|
|
36
|
+
build: Type.Optional(CommandGateConfigSchema),
|
|
37
|
+
},
|
|
38
|
+
{ additionalProperties: false },
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
export const GATE_CONFIG_SCHEMAS: Record<GateId, TSchema> = {
|
|
42
|
+
"lsp-diagnostics": LspDiagnosticsGateConfigSchema,
|
|
43
|
+
lint: CommandGateConfigSchema,
|
|
44
|
+
typecheck: CommandGateConfigSchema,
|
|
45
|
+
format: CommandGateConfigSchema,
|
|
46
|
+
"test-suite": CommandGateConfigSchema,
|
|
47
|
+
build: CommandGateConfigSchema,
|
|
48
|
+
};
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { Platform, PlatformContext } from "../platform/types.js";
|
|
4
|
+
import type {
|
|
5
|
+
ConfigScope,
|
|
6
|
+
ProjectFacts,
|
|
7
|
+
QualityGatesConfig,
|
|
8
|
+
SetupGatesResult,
|
|
9
|
+
SetupProposal,
|
|
10
|
+
} from "../types.js";
|
|
11
|
+
import type { InspectionLoadResult } from "../config/schema.js";
|
|
12
|
+
import { validateQualityGates } from "../config/schema.js";
|
|
13
|
+
import { writeQualityGatesConfig } from "../config/loader.js";
|
|
14
|
+
import { CANONICAL_GATE_ORDER } from "./registry.js";
|
|
15
|
+
import { detectReviewGates } from "./review-gates.js";
|
|
16
|
+
import { suggestQualityGatesWithAi } from "./ai-setup.js";
|
|
17
|
+
|
|
18
|
+
export type GateSetupMode = "deterministic" | "ai-assisted";
|
|
19
|
+
|
|
20
|
+
export type SetupGatesProgressEvent =
|
|
21
|
+
| { type: "collecting-project-facts" }
|
|
22
|
+
| { type: "baseline-ready" }
|
|
23
|
+
| { type: "ai-analysis-started" }
|
|
24
|
+
| { type: "ai-analysis-completed" };
|
|
25
|
+
|
|
26
|
+
export interface SetupGatesOptions {
|
|
27
|
+
mode?: GateSetupMode;
|
|
28
|
+
onProgress?: (event: SetupGatesProgressEvent) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface GateSetupDialogOptions {
|
|
32
|
+
title?: string;
|
|
33
|
+
intro?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SetupGatesDependencies {
|
|
37
|
+
suggestWithAi?: (input: {
|
|
38
|
+
platform: Platform;
|
|
39
|
+
cwd: string;
|
|
40
|
+
projectFacts: ProjectFacts;
|
|
41
|
+
proposal: SetupProposal;
|
|
42
|
+
}) => Promise<QualityGatesConfig>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readPackageScripts(cwd: string): Record<string, string> {
|
|
46
|
+
const packageJsonPath = path.join(cwd, "package.json");
|
|
47
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) as {
|
|
53
|
+
scripts?: Record<string, string>;
|
|
54
|
+
};
|
|
55
|
+
return parsed.scripts ?? {};
|
|
56
|
+
} catch {
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function detectLockfiles(cwd: string): string[] {
|
|
62
|
+
return ["bun.lock", "bun.lockb", "package-lock.json", "pnpm-lock.yaml", "yarn.lock"]
|
|
63
|
+
.filter((file) => fs.existsSync(path.join(cwd, file)));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function collectProjectFacts(
|
|
67
|
+
cwd: string,
|
|
68
|
+
inspection: InspectionLoadResult,
|
|
69
|
+
activeTools: string[],
|
|
70
|
+
): ProjectFacts {
|
|
71
|
+
return {
|
|
72
|
+
cwd,
|
|
73
|
+
packageScripts: readPackageScripts(cwd),
|
|
74
|
+
lockfiles: detectLockfiles(cwd),
|
|
75
|
+
activeTools,
|
|
76
|
+
existingGates: inspection.effectiveConfig?.quality.gates ?? {},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function summarizeEnabledGates(gates: QualityGatesConfig): string {
|
|
81
|
+
const enabled = CANONICAL_GATE_ORDER.filter((gateId) => gates[gateId]?.enabled === true);
|
|
82
|
+
|
|
83
|
+
return enabled.length > 0 ? enabled.join(", ") : "none";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function formatGateProposal(proposal: SetupProposal): string {
|
|
87
|
+
const entries = CANONICAL_GATE_ORDER
|
|
88
|
+
.filter((gateId) => proposal.gates[gateId] !== undefined)
|
|
89
|
+
.map((gateId) => [gateId, proposal.gates[gateId]] as const);
|
|
90
|
+
if (entries.length === 0) {
|
|
91
|
+
return "No gates suggested.";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return entries
|
|
95
|
+
.map(([gateId, config]) => `${gateId}: ${JSON.stringify(config)}`)
|
|
96
|
+
.join("\n");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function buildDeterministicSuggestion(projectFacts: ProjectFacts): SetupProposal {
|
|
100
|
+
return { gates: detectReviewGates(projectFacts) };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function setupGates(
|
|
104
|
+
platform: Platform,
|
|
105
|
+
cwd: string,
|
|
106
|
+
inspection: InspectionLoadResult,
|
|
107
|
+
options: SetupGatesOptions = {},
|
|
108
|
+
deps: SetupGatesDependencies = {},
|
|
109
|
+
): Promise<SetupGatesResult> {
|
|
110
|
+
options.onProgress?.({ type: "collecting-project-facts" });
|
|
111
|
+
const projectFacts = collectProjectFacts(cwd, inspection, platform.getActiveTools());
|
|
112
|
+
let proposal = buildDeterministicSuggestion(projectFacts);
|
|
113
|
+
options.onProgress?.({ type: "baseline-ready" });
|
|
114
|
+
|
|
115
|
+
if (options.mode === "ai-assisted") {
|
|
116
|
+
options.onProgress?.({ type: "ai-analysis-started" });
|
|
117
|
+
const suggestWithAi = deps.suggestWithAi ?? suggestQualityGatesWithAi;
|
|
118
|
+
proposal = {
|
|
119
|
+
gates: await suggestWithAi({
|
|
120
|
+
platform,
|
|
121
|
+
cwd,
|
|
122
|
+
projectFacts,
|
|
123
|
+
proposal,
|
|
124
|
+
}),
|
|
125
|
+
};
|
|
126
|
+
options.onProgress?.({ type: "ai-analysis-completed" });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const validation = validateQualityGates(proposal.gates);
|
|
130
|
+
if (!validation.valid) {
|
|
131
|
+
return {
|
|
132
|
+
status: "invalid",
|
|
133
|
+
proposal,
|
|
134
|
+
errors: validation.errors,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
status: "proposed",
|
|
140
|
+
proposal,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function parseRevisedProposal(raw: string): SetupProposal {
|
|
145
|
+
const parsed = JSON.parse(raw) as QualityGatesConfig;
|
|
146
|
+
const validation = validateQualityGates(parsed);
|
|
147
|
+
if (!validation.valid) {
|
|
148
|
+
throw new Error(validation.errors.join("\n"));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { gates: parsed };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function labelForScope(scope: ConfigScope): string {
|
|
155
|
+
return scope === "project"
|
|
156
|
+
? "Project (.omp/supipowers/config.json)"
|
|
157
|
+
: "Global (~/.omp/supipowers/config.json)";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function selectSaveScope(ctx: PlatformContext): Promise<ConfigScope | null> {
|
|
161
|
+
const choice = await ctx.ui.select(
|
|
162
|
+
"Save quality gates to",
|
|
163
|
+
[labelForScope("project"), labelForScope("global"), "Cancel"],
|
|
164
|
+
{
|
|
165
|
+
initialIndex: 0,
|
|
166
|
+
helpText: "Choose whether review gates apply only to this project or all projects.",
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
if (!choice || choice === "Cancel") {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return choice === labelForScope("global") ? "global" : "project";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function buildProposalHelpText(proposal: SetupProposal, options?: GateSetupDialogOptions): string {
|
|
178
|
+
return [options?.intro, formatGateProposal(proposal)].filter(Boolean).join("\n\n");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function interactivelySaveGateSetup(
|
|
182
|
+
ctx: PlatformContext,
|
|
183
|
+
paths: Platform["paths"],
|
|
184
|
+
cwd: string,
|
|
185
|
+
initial: SetupProposal,
|
|
186
|
+
options: GateSetupDialogOptions = {},
|
|
187
|
+
): Promise<"saved" | "cancelled"> {
|
|
188
|
+
let proposal = initial;
|
|
189
|
+
|
|
190
|
+
while (true) {
|
|
191
|
+
const choice = await ctx.ui.select(
|
|
192
|
+
options.title ?? "Quality gate setup",
|
|
193
|
+
["Accept", "Revise", "Cancel"],
|
|
194
|
+
{ helpText: buildProposalHelpText(proposal, options) },
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
if (!choice || choice === "Cancel") {
|
|
198
|
+
return "cancelled";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (choice === "Revise") {
|
|
202
|
+
const revised = await ctx.ui.input(
|
|
203
|
+
"Edit quality.gates JSON",
|
|
204
|
+
{ value: JSON.stringify(proposal.gates, null, 2) },
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
if (!revised) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
proposal = parseRevisedProposal(revised);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
ctx.ui.notify((error as Error).message, "error");
|
|
215
|
+
}
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const scope = await selectSaveScope(ctx);
|
|
220
|
+
if (!scope) {
|
|
221
|
+
return "cancelled";
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
writeQualityGatesConfig(paths, cwd, scope, proposal.gates);
|
|
225
|
+
return "saved";
|
|
226
|
+
}
|
|
227
|
+
}
|
package/src/release/changelog.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// src/release/changelog.ts — Conventional commit parsing and changelog generation
|
|
2
2
|
|
|
3
3
|
import type { CategorizedCommits, CommitEntry } from "../types.js";
|
|
4
|
+
import { normalizeLineEndings } from "../text.js";
|
|
5
|
+
import { formatTag } from "./version.js";
|
|
4
6
|
import { IMPROVEMENT_TYPES, MAINTENANCE_TYPES, type ConventionalCommitType } from "./commit-types.js";
|
|
5
7
|
|
|
6
8
|
// Matches: feat(scope)!: message or feat!: message or feat(scope): message or feat: message
|
|
@@ -39,6 +41,7 @@ function parseLine(line: string): { hash: string; raw: string } | null {
|
|
|
39
41
|
*/
|
|
40
42
|
|
|
41
43
|
export function parseConventionalCommits(gitLog: string): CategorizedCommits {
|
|
44
|
+
const normalizedGitLog = normalizeLineEndings(gitLog);
|
|
42
45
|
const result: CategorizedCommits = {
|
|
43
46
|
features: [],
|
|
44
47
|
fixes: [],
|
|
@@ -48,7 +51,7 @@ export function parseConventionalCommits(gitLog: string): CategorizedCommits {
|
|
|
48
51
|
other: [],
|
|
49
52
|
};
|
|
50
53
|
|
|
51
|
-
for (const line of
|
|
54
|
+
for (const line of normalizedGitLog.split("\n")) {
|
|
52
55
|
const parsed = parseLine(line);
|
|
53
56
|
if (!parsed) continue;
|
|
54
57
|
|
|
@@ -106,11 +109,12 @@ function formatEntry(entry: CommitEntry): string {
|
|
|
106
109
|
*/
|
|
107
110
|
export function buildChangelogMarkdown(
|
|
108
111
|
commits: CategorizedCommits,
|
|
109
|
-
version: string
|
|
112
|
+
version: string,
|
|
113
|
+
tagFormat: string = "v${version}",
|
|
110
114
|
): string {
|
|
111
115
|
const lines: string[] = [];
|
|
112
116
|
|
|
113
|
-
lines.push(`##
|
|
117
|
+
lines.push(`## ${formatTag(version, tagFormat)}`);
|
|
114
118
|
lines.push(`_${isoDate(new Date())}_`);
|
|
115
119
|
lines.push("");
|
|
116
120
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// src/release/channels/custom.ts — Wraps user-defined custom channel config into a ChannelHandler
|
|
2
|
+
import type { CustomChannelConfig } from "../../types.js";
|
|
3
|
+
import type { ChannelHandler, ChannelPublishContext, ChannelStatus, ExecFn } from "./types.js";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export function createCustomHandler(id: string, config: CustomChannelConfig): ChannelHandler {
|
|
7
|
+
return {
|
|
8
|
+
id,
|
|
9
|
+
label: config.label,
|
|
10
|
+
|
|
11
|
+
async detect(exec: ExecFn, cwd: string): Promise<ChannelStatus> {
|
|
12
|
+
if (!config.detectCommand) {
|
|
13
|
+
return { channel: id, available: true, detail: "No detect command configured — assumed available" };
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const result = await exec("sh", ["-c", config.detectCommand], { cwd });
|
|
17
|
+
if (result.code === 0) {
|
|
18
|
+
return { channel: id, available: true, detail: `Detect command succeeded` };
|
|
19
|
+
}
|
|
20
|
+
return { channel: id, available: false, detail: `Detect command exited with code ${result.code}` };
|
|
21
|
+
} catch {
|
|
22
|
+
return { channel: id, available: false, detail: "Detect command failed to execute" };
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
async publish(exec: ExecFn, ctx: ChannelPublishContext): Promise<{ success: boolean; error?: string }> {
|
|
27
|
+
try {
|
|
28
|
+
// `${tag}`/`${version}`/`${changelog}` stay in the shell template, but
|
|
29
|
+
// their values cross the shell boundary via environment variables.
|
|
30
|
+
const result = await exec("sh", ["-c", config.publishCommand], {
|
|
31
|
+
cwd: ctx.cwd,
|
|
32
|
+
env: { tag: ctx.tag, version: ctx.version, changelog: ctx.changelog },
|
|
33
|
+
});
|
|
34
|
+
if (result.code !== 0) {
|
|
35
|
+
return { success: false, error: result.stderr || result.stdout || `Custom channel '${id}' exited with code ${result.code}` };
|
|
36
|
+
}
|
|
37
|
+
return { success: true };
|
|
38
|
+
} catch (err) {
|
|
39
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// src/release/channels/gitea.ts — Built-in Gitea/Forgejo release channel
|
|
2
|
+
import type { ChannelHandler, ChannelPublishContext, ChannelStatus, ExecFn } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export const gitea: ChannelHandler = {
|
|
5
|
+
id: "gitea",
|
|
6
|
+
label: "Gitea Releases",
|
|
7
|
+
|
|
8
|
+
async detect(exec: ExecFn, cwd: string): Promise<ChannelStatus> {
|
|
9
|
+
try {
|
|
10
|
+
const result = await exec("tea", ["login", "list"], { cwd });
|
|
11
|
+
if (result.code === 0 && result.stdout.trim().length > 0) {
|
|
12
|
+
return { channel: "gitea", available: true, detail: "Authenticated with Gitea CLI" };
|
|
13
|
+
}
|
|
14
|
+
return { channel: "gitea", available: false, detail: "Gitea CLI not authenticated (run: tea login add)" };
|
|
15
|
+
} catch {
|
|
16
|
+
return { channel: "gitea", available: false, detail: "Gitea CLI not installed (install: https://gitea.com/gitea/tea)" };
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
async publish(exec: ExecFn, ctx: ChannelPublishContext): Promise<{ success: boolean; error?: string }> {
|
|
21
|
+
try {
|
|
22
|
+
const result = await exec(
|
|
23
|
+
"tea",
|
|
24
|
+
["release", "create", "--tag", ctx.tag, "--title", ctx.tag, "--note", ctx.changelog],
|
|
25
|
+
{ cwd: ctx.cwd },
|
|
26
|
+
);
|
|
27
|
+
if (result.code !== 0) {
|
|
28
|
+
return { success: false, error: result.stderr || result.stdout || `tea 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,35 @@
|
|
|
1
|
+
// src/release/channels/github.ts — Built-in GitHub release channel
|
|
2
|
+
import type { ChannelHandler, ChannelPublishContext, ChannelStatus, ExecFn } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export const github: ChannelHandler = {
|
|
5
|
+
id: "github",
|
|
6
|
+
label: "GitHub Releases",
|
|
7
|
+
|
|
8
|
+
async detect(exec: ExecFn, cwd: string): Promise<ChannelStatus> {
|
|
9
|
+
try {
|
|
10
|
+
const result = await exec("gh", ["auth", "status"], { cwd });
|
|
11
|
+
if (result.code === 0) {
|
|
12
|
+
return { channel: "github", available: true, detail: "Authenticated with GitHub CLI" };
|
|
13
|
+
}
|
|
14
|
+
return { channel: "github", available: false, detail: "GitHub CLI not authenticated (run: gh auth login)" };
|
|
15
|
+
} catch {
|
|
16
|
+
return { channel: "github", available: false, detail: "GitHub CLI not installed (install: https://cli.github.com)" };
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
async publish(exec: ExecFn, ctx: ChannelPublishContext): Promise<{ success: boolean; error?: string }> {
|
|
21
|
+
try {
|
|
22
|
+
const result = await exec(
|
|
23
|
+
"gh",
|
|
24
|
+
["release", "create", ctx.tag, "--title", ctx.tag, "--notes", ctx.changelog],
|
|
25
|
+
{ cwd: ctx.cwd },
|
|
26
|
+
);
|
|
27
|
+
if (result.code !== 0) {
|
|
28
|
+
return { success: false, error: result.stderr || result.stdout || `gh 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
|
+
};
|