@ulpi/cli 0.1.4 → 0.1.5
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/{auth-ECQ3IB4E.js → auth-BFFBUJUC.js} +1 -1
- package/dist/{chunk-YM2HV4IA.js → chunk-26LLDX2T.js} +50 -2
- package/dist/{chunk-QJ5GSMEC.js → chunk-5SCG7UYM.js} +2 -1
- package/dist/{chunk-5J6NLQUN.js → chunk-6OURRFP7.js} +8 -8
- package/dist/chunk-AV5RB3N2.js +173 -0
- package/dist/{chunk-7LXY5UVC.js → chunk-DDRLI6JU.js} +2 -1
- package/dist/{chunk-ZLYRPD7I.js → chunk-DOIKS6C5.js} +1 -1
- package/dist/{chunk-SPOI23SB.js → chunk-EIWYSP3A.js} +1 -1
- package/dist/{chunk-7AL4DOEJ.js → chunk-ELTGWMDE.js} +3 -3
- package/dist/{chunk-6OCEY7JY.js → chunk-IFATANHR.js} +34 -3
- package/dist/{chunk-2HEE5OKX.js → chunk-K4OVPFY2.js} +1 -1
- package/dist/{chunk-JGBXM5NC.js → chunk-L3PWNHSA.js} +2 -2
- package/dist/{chunk-2VYFVYJL.js → chunk-LD52XG3X.js} +24 -24
- package/dist/{chunk-BZL5H4YQ.js → chunk-P2RESJRN.js} +2 -2
- package/dist/{chunk-3SBPZRB5.js → chunk-RJIRWQJD.js} +1 -1
- package/dist/{chunk-2CLNOKPA.js → chunk-RSFJ6QSR.js} +18 -0
- package/dist/{chunk-PDR55ZNW.js → chunk-UCMT5OKP.js} +4 -4
- package/dist/{chunk-2MZER6ND.js → chunk-YYZOFYS6.js} +2 -2
- package/dist/ci-JQ56YIKC.js +756 -0
- package/dist/{codemap-RKSD4MIE.js → codemap-HMYBXJL2.js} +36 -36
- package/dist/{config-EGAXXCGL.js → config-YYWEN7U2.js} +1 -1
- package/dist/dist-2K7IEVTA.js +43 -0
- package/dist/{dist-UKMCJBB2.js → dist-4XTJ6HLM.js} +7 -7
- package/dist/{dist-QAU3LGJN.js → dist-5R4RYNQO.js} +3 -3
- package/dist/{dist-CB5D5LMO.js → dist-6MFVWIFF.js} +8 -8
- package/dist/{dist-GJYT2OQV.js → dist-7WLLPWWB.js} +8 -8
- package/dist/{dist-RKOGLK7R.js → dist-GWGTAHNM.js} +1 -1
- package/dist/{dist-CS2VKNYS.js → dist-U7ZIJMZD.js} +8 -8
- package/dist/{dist-YA2BWZB2.js → dist-WAMAQVPK.js} +2 -2
- package/dist/dist-XD4YI27T.js +26 -0
- package/dist/dist-XG2GG5SD.js +36 -0
- package/dist/{history-3MOBX4MA.js → history-RNUWO4JZ.js} +7 -7
- package/dist/hooks-installer-K2JXEBNN.js +19 -0
- package/dist/index.js +42 -42
- package/dist/{init-6CH4HV5T.js → init-NQWFZPKO.js} +11 -11
- package/dist/{launchd-LF2QMSKZ.js → launchd-OYXUAVW6.js} +2 -2
- package/dist/{mcp-installer-NQCGKQ23.js → mcp-installer-TOYDP77X.js} +1 -1
- package/dist/{memory-Y6OZTXJ2.js → memory-D6ZFFCI2.js} +17 -17
- package/dist/{openai-E7G2YAHU-UYY4ZWON.js → openai-E7G2YAHU-IG33BFYF.js} +2 -2
- package/dist/{projects-ATHDD3D6.js → projects-COUJP4ZC.js} +3 -3
- package/dist/{review-ADUPV3PN.js → review-KMGP2S25.js} +2 -2
- package/dist/{rules-E427DKYJ.js → rules-3OFGWHP4.js} +1 -1
- package/dist/server-USLHY6GH-F4JSXCWA.js +18 -0
- package/dist/server-X5P6WH2M-ULZF5WHZ.js +11 -0
- package/dist/{skills-CX73O3IV.js → skills-GY2CTPWN.js} +2 -2
- package/dist/{status-4DFHDJMN.js → status-SE43TIFJ.js} +2 -2
- package/dist/{templates-U7T6MARD.js → templates-O2XDKB5R.js} +5 -5
- package/dist/{ui-OWXZ3YSR.js → ui-4SM2SUI6.js} +13 -13
- package/dist/{ulpi-RMMCUAGP-JCJ273T6.js → ulpi-RMMCUAGP-EWYUE7RU.js} +1 -1
- package/dist/{uninstall-6SW35IK4.js → uninstall-KWGSGZTI.js} +3 -3
- package/dist/{update-M6IBJNYP.js → update-QYZA4D23.js} +3 -3
- package/dist/{version-checker-Q6YTYAGP.js → version-checker-MVB74DEX.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-B55DDP24.js +0 -136
- package/dist/ci-STSL2LSP.js +0 -370
- package/dist/server-USLHY6GH-AEOJC5ST.js +0 -18
- package/dist/server-X5P6WH2M-7K2RY34N.js +0 -11
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildPrompt
|
|
3
|
+
} from "./chunk-AV5RB3N2.js";
|
|
4
|
+
import {
|
|
5
|
+
external_exports
|
|
6
|
+
} from "./chunk-KIKPIH6N.js";
|
|
7
|
+
import "./chunk-4VNS5WPM.js";
|
|
8
|
+
|
|
9
|
+
// src/commands/ci.ts
|
|
10
|
+
import * as fs from "fs";
|
|
11
|
+
import * as path from "path";
|
|
12
|
+
import { spawn, execFileSync } from "child_process";
|
|
13
|
+
import chalk from "chalk";
|
|
14
|
+
|
|
15
|
+
// ../../packages/contracts-ci/dist/index.js
|
|
16
|
+
var CiCommandTypeSchema = external_exports.enum(["run", "cancel", "fork", "status"]);
|
|
17
|
+
var CiCommandSchema = external_exports.object({
|
|
18
|
+
type: CiCommandTypeSchema,
|
|
19
|
+
instruction: external_exports.string().optional(),
|
|
20
|
+
prNumber: external_exports.number().int().positive(),
|
|
21
|
+
repoFullName: external_exports.string(),
|
|
22
|
+
// "org/repo"
|
|
23
|
+
commentId: external_exports.number().int().positive(),
|
|
24
|
+
commentAuthor: external_exports.string(),
|
|
25
|
+
forkName: external_exports.string().optional()
|
|
26
|
+
});
|
|
27
|
+
var JobStatusSchema = external_exports.enum([
|
|
28
|
+
"queued",
|
|
29
|
+
"provisioning",
|
|
30
|
+
"running",
|
|
31
|
+
"completing",
|
|
32
|
+
"completed",
|
|
33
|
+
"failed",
|
|
34
|
+
"cancelled",
|
|
35
|
+
"timed_out"
|
|
36
|
+
]);
|
|
37
|
+
var JobConfigSchema = external_exports.object({
|
|
38
|
+
repoUrl: external_exports.string().url(),
|
|
39
|
+
ref: external_exports.string(),
|
|
40
|
+
baseBranch: external_exports.string(),
|
|
41
|
+
prNumber: external_exports.number().int().positive(),
|
|
42
|
+
repoFullName: external_exports.string(),
|
|
43
|
+
instruction: external_exports.string(),
|
|
44
|
+
jiraContext: external_exports.string().optional(),
|
|
45
|
+
timeoutMinutes: external_exports.number().int().positive().default(1440),
|
|
46
|
+
envVars: external_exports.record(external_exports.string()).optional()
|
|
47
|
+
});
|
|
48
|
+
var JobResultSchema = external_exports.object({
|
|
49
|
+
jobId: external_exports.string(),
|
|
50
|
+
status: JobStatusSchema,
|
|
51
|
+
exitCode: external_exports.number().int().optional(),
|
|
52
|
+
diff: external_exports.string().optional(),
|
|
53
|
+
testOutput: external_exports.string().optional(),
|
|
54
|
+
summary: external_exports.string().optional(),
|
|
55
|
+
filesChanged: external_exports.array(external_exports.string()),
|
|
56
|
+
sessionEvents: external_exports.number().int().default(0),
|
|
57
|
+
durationMs: external_exports.number().int(),
|
|
58
|
+
error: external_exports.string().optional()
|
|
59
|
+
});
|
|
60
|
+
var JobSchema = external_exports.object({
|
|
61
|
+
id: external_exports.string(),
|
|
62
|
+
config: JobConfigSchema,
|
|
63
|
+
status: JobStatusSchema,
|
|
64
|
+
result: JobResultSchema.optional(),
|
|
65
|
+
containerId: external_exports.string().optional(),
|
|
66
|
+
workspaceDir: external_exports.string().optional(),
|
|
67
|
+
parentJobId: external_exports.string().optional(),
|
|
68
|
+
createdAt: external_exports.string().datetime(),
|
|
69
|
+
startedAt: external_exports.string().datetime().optional(),
|
|
70
|
+
completedAt: external_exports.string().datetime().optional(),
|
|
71
|
+
cancelledBy: external_exports.string().optional(),
|
|
72
|
+
commentId: external_exports.number().int().positive(),
|
|
73
|
+
commentAuthor: external_exports.string()
|
|
74
|
+
});
|
|
75
|
+
var GitHubAppAuthSchema = external_exports.object({
|
|
76
|
+
appId: external_exports.string(),
|
|
77
|
+
privateKeyPath: external_exports.string(),
|
|
78
|
+
webhookSecret: external_exports.string(),
|
|
79
|
+
installationId: external_exports.number().int().positive().optional()
|
|
80
|
+
});
|
|
81
|
+
var GitHubAuthConfigSchema = external_exports.discriminatedUnion("mode", [
|
|
82
|
+
external_exports.object({ mode: external_exports.literal("app"), config: GitHubAppAuthSchema }),
|
|
83
|
+
external_exports.object({ mode: external_exports.literal("pat"), token: external_exports.string() })
|
|
84
|
+
]);
|
|
85
|
+
var JiraConfigSchema = external_exports.object({
|
|
86
|
+
baseUrl: external_exports.string().url(),
|
|
87
|
+
email: external_exports.string().email(),
|
|
88
|
+
apiToken: external_exports.string(),
|
|
89
|
+
defaultProject: external_exports.string().optional()
|
|
90
|
+
});
|
|
91
|
+
var DockerConfigSchema = external_exports.object({
|
|
92
|
+
image: external_exports.string(),
|
|
93
|
+
network: external_exports.string().optional(),
|
|
94
|
+
memoryLimit: external_exports.string().optional(),
|
|
95
|
+
// e.g. "4g"
|
|
96
|
+
cpuLimit: external_exports.number().positive().optional(),
|
|
97
|
+
// e.g. 2.0
|
|
98
|
+
volumeBaseDir: external_exports.string(),
|
|
99
|
+
// Host path — used for Docker bind mounts
|
|
100
|
+
localVolumeBaseDir: external_exports.string().optional()
|
|
101
|
+
// Container-local path — used for fs ops (defaults to volumeBaseDir)
|
|
102
|
+
});
|
|
103
|
+
var SecurityConfigSchema = external_exports.object({
|
|
104
|
+
requireWriteAccess: external_exports.boolean().default(true),
|
|
105
|
+
allowedRepos: external_exports.array(external_exports.string()).optional(),
|
|
106
|
+
allowedAuthors: external_exports.array(external_exports.string()).optional(),
|
|
107
|
+
allowedOrgs: external_exports.array(external_exports.string()).optional(),
|
|
108
|
+
apiToken: external_exports.string(),
|
|
109
|
+
rateLimitWebhook: external_exports.number().int().positive().default(60),
|
|
110
|
+
rateLimitApi: external_exports.number().int().positive().default(200)
|
|
111
|
+
});
|
|
112
|
+
var OrchestratorConfigSchema = external_exports.object({
|
|
113
|
+
github: GitHubAuthConfigSchema,
|
|
114
|
+
jira: JiraConfigSchema.optional(),
|
|
115
|
+
docker: DockerConfigSchema,
|
|
116
|
+
jobs: external_exports.object({
|
|
117
|
+
maxConcurrent: external_exports.number().int().positive().default(3),
|
|
118
|
+
timeoutMinutes: external_exports.number().int().positive().default(1440),
|
|
119
|
+
retainCompletedHours: external_exports.number().int().positive().default(72)
|
|
120
|
+
}),
|
|
121
|
+
security: SecurityConfigSchema,
|
|
122
|
+
port: external_exports.number().int().positive().default(3e3),
|
|
123
|
+
host: external_exports.string().default("0.0.0.0")
|
|
124
|
+
});
|
|
125
|
+
var ClaudeCredentialsSchema = external_exports.object({
|
|
126
|
+
blob: external_exports.string(),
|
|
127
|
+
// base64-encoded credentials directory contents
|
|
128
|
+
createdAt: external_exports.string().datetime(),
|
|
129
|
+
expiresAt: external_exports.string().datetime().optional()
|
|
130
|
+
});
|
|
131
|
+
var WorkerInputSchema = external_exports.object({
|
|
132
|
+
jobId: external_exports.string(),
|
|
133
|
+
config: JobConfigSchema,
|
|
134
|
+
claudeModel: external_exports.string().optional()
|
|
135
|
+
});
|
|
136
|
+
var WorkerOutputSchema = external_exports.object({
|
|
137
|
+
jobId: external_exports.string(),
|
|
138
|
+
result: JobResultSchema
|
|
139
|
+
});
|
|
140
|
+
var WorkerProgressSchema = external_exports.object({
|
|
141
|
+
phase: external_exports.string(),
|
|
142
|
+
message: external_exports.string(),
|
|
143
|
+
timestamp: external_exports.string().datetime()
|
|
144
|
+
});
|
|
145
|
+
var JiraTicketSchema = external_exports.object({
|
|
146
|
+
key: external_exports.string(),
|
|
147
|
+
// e.g. "ABC-123"
|
|
148
|
+
summary: external_exports.string(),
|
|
149
|
+
description: external_exports.string().optional(),
|
|
150
|
+
status: external_exports.string(),
|
|
151
|
+
assignee: external_exports.string().optional(),
|
|
152
|
+
priority: external_exports.string().optional(),
|
|
153
|
+
labels: external_exports.array(external_exports.string()),
|
|
154
|
+
acceptanceCriteria: external_exports.string().optional()
|
|
155
|
+
});
|
|
156
|
+
var JiraReferenceSchema = external_exports.object({
|
|
157
|
+
key: external_exports.string(),
|
|
158
|
+
url: external_exports.string().url()
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// src/commands/ci.ts
|
|
162
|
+
async function runCi(args, projectDir) {
|
|
163
|
+
const subcommand = args[0];
|
|
164
|
+
switch (subcommand) {
|
|
165
|
+
case "run":
|
|
166
|
+
return runCiJob(args.slice(1));
|
|
167
|
+
default:
|
|
168
|
+
console.log(`
|
|
169
|
+
${chalk.bold("ulpi ci")} \u2014 CI/PR worker mode
|
|
170
|
+
|
|
171
|
+
Usage: ulpi ci <command>
|
|
172
|
+
|
|
173
|
+
Commands:
|
|
174
|
+
run Execute a CI job (used inside worker containers)
|
|
175
|
+
|
|
176
|
+
Options:
|
|
177
|
+
--job-id <id> Job identifier
|
|
178
|
+
--config <json> Job config (JSON string or from ULPI_JOB_CONFIG env)
|
|
179
|
+
--output <path> Output file path for results
|
|
180
|
+
--model <model> Claude model to use (default: claude-sonnet-4-20250514)
|
|
181
|
+
`.trim());
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async function runCiJob(args) {
|
|
185
|
+
let jobId = "";
|
|
186
|
+
let configJson = "";
|
|
187
|
+
let outputPath = "";
|
|
188
|
+
let model = "claude-sonnet-4-20250514";
|
|
189
|
+
for (let i = 0; i < args.length; i++) {
|
|
190
|
+
switch (args[i]) {
|
|
191
|
+
case "--job-id":
|
|
192
|
+
jobId = args[++i] ?? "";
|
|
193
|
+
break;
|
|
194
|
+
case "--config":
|
|
195
|
+
configJson = args[++i] ?? "";
|
|
196
|
+
break;
|
|
197
|
+
case "--output":
|
|
198
|
+
outputPath = args[++i] ?? "";
|
|
199
|
+
break;
|
|
200
|
+
case "--model":
|
|
201
|
+
model = args[++i] ?? model;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
jobId = jobId || process.env.ULPI_JOB_ID || "";
|
|
206
|
+
configJson = configJson || process.env.ULPI_JOB_CONFIG || "";
|
|
207
|
+
outputPath = outputPath || "/workspace/.ulpi-ci/output.json";
|
|
208
|
+
if (!jobId) {
|
|
209
|
+
console.error(chalk.red("Error: --job-id is required"));
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
if (!configJson) {
|
|
213
|
+
console.error(chalk.red("Error: --config is required (or set ULPI_JOB_CONFIG env)"));
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
let rawConfig;
|
|
217
|
+
try {
|
|
218
|
+
rawConfig = JSON.parse(configJson);
|
|
219
|
+
} catch {
|
|
220
|
+
console.error(chalk.red("Error: Invalid JSON in --config"));
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
const parseResult = JobConfigSchema.safeParse(rawConfig);
|
|
224
|
+
if (!parseResult.success) {
|
|
225
|
+
console.error(chalk.red("Error: Invalid job config:"));
|
|
226
|
+
console.error(parseResult.error.format());
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
const config = parseResult.data;
|
|
230
|
+
const startTime = Date.now();
|
|
231
|
+
console.log(chalk.blue(`[ulpi-ci] Job ${jobId} starting`));
|
|
232
|
+
console.log(chalk.blue(`[ulpi-ci] Instruction: ${config.instruction}`));
|
|
233
|
+
console.log(chalk.blue(`[ulpi-ci] Repo: ${config.repoFullName} PR#${config.prNumber}`));
|
|
234
|
+
const projectDir = process.cwd();
|
|
235
|
+
const subsystemInfo = await initUlpiSubsystems(projectDir);
|
|
236
|
+
const prompt = buildPrompt(config, {
|
|
237
|
+
stackContext: subsystemInfo.stackContext
|
|
238
|
+
});
|
|
239
|
+
const claudePath = findClaudeBinary();
|
|
240
|
+
if (!claudePath) {
|
|
241
|
+
console.error(chalk.red("Error: claude binary not found in PATH"));
|
|
242
|
+
writeOutput(outputPath, {
|
|
243
|
+
jobId,
|
|
244
|
+
status: "failed",
|
|
245
|
+
filesChanged: [],
|
|
246
|
+
sessionEvents: 0,
|
|
247
|
+
durationMs: Date.now() - startTime,
|
|
248
|
+
error: "claude binary not found"
|
|
249
|
+
});
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
console.log(chalk.blue(`[ulpi-ci] Using claude at: ${claudePath}`));
|
|
253
|
+
const claudeArgs = [
|
|
254
|
+
"--print",
|
|
255
|
+
"--verbose",
|
|
256
|
+
"--output-format",
|
|
257
|
+
"stream-json",
|
|
258
|
+
"--dangerously-skip-permissions",
|
|
259
|
+
"--model",
|
|
260
|
+
model
|
|
261
|
+
];
|
|
262
|
+
console.log(chalk.blue("[ulpi-ci] Spawning Claude Code..."));
|
|
263
|
+
const result = await spawnClaude(claudePath, claudeArgs, prompt, config.timeoutMinutes * 60 * 1e3);
|
|
264
|
+
commitUnstagedWork(process.cwd());
|
|
265
|
+
await postSessionFinalize(process.cwd());
|
|
266
|
+
const baseBranch = config.baseBranch;
|
|
267
|
+
let diff = "";
|
|
268
|
+
let filesChanged = [];
|
|
269
|
+
try {
|
|
270
|
+
diff = execFileSync("git", ["diff", `origin/${baseBranch}...HEAD`], {
|
|
271
|
+
cwd: process.cwd(),
|
|
272
|
+
encoding: "utf-8",
|
|
273
|
+
timeout: 3e4,
|
|
274
|
+
maxBuffer: 10 * 1024 * 1024
|
|
275
|
+
});
|
|
276
|
+
} catch {
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
const output = execFileSync("git", ["diff", "--name-only", `origin/${baseBranch}...HEAD`], {
|
|
280
|
+
cwd: process.cwd(),
|
|
281
|
+
encoding: "utf-8",
|
|
282
|
+
timeout: 3e4
|
|
283
|
+
});
|
|
284
|
+
filesChanged = output.trim().split("\n").filter(Boolean);
|
|
285
|
+
} catch {
|
|
286
|
+
}
|
|
287
|
+
const jobResult = {
|
|
288
|
+
jobId,
|
|
289
|
+
status: result.exitCode === 0 ? "completed" : "failed",
|
|
290
|
+
exitCode: result.exitCode,
|
|
291
|
+
diff: diff || void 0,
|
|
292
|
+
summary: result.summary.slice(0, 5e3) || void 0,
|
|
293
|
+
filesChanged,
|
|
294
|
+
sessionEvents: result.toolUseCount,
|
|
295
|
+
durationMs: Date.now() - startTime,
|
|
296
|
+
error: result.exitCode !== 0 ? `Claude exited with code ${result.exitCode}` : void 0
|
|
297
|
+
};
|
|
298
|
+
writeOutput(outputPath, jobResult);
|
|
299
|
+
console.log(chalk.blue(`[ulpi-ci] Job ${jobId} ${jobResult.status} in ${formatDuration(jobResult.durationMs)}`));
|
|
300
|
+
console.log(chalk.blue(`[ulpi-ci] Files changed: ${filesChanged.length}`));
|
|
301
|
+
console.log(chalk.blue(`[ulpi-ci] Tool uses: ${result.toolUseCount}`));
|
|
302
|
+
process.exit(result.exitCode === 0 ? 0 : 1);
|
|
303
|
+
}
|
|
304
|
+
async function initUlpiSubsystems(projectDir) {
|
|
305
|
+
console.log(chalk.blue("[ulpi-ci] Initializing ULPI subsystems..."));
|
|
306
|
+
const info = {};
|
|
307
|
+
const rulesDir = path.join(projectDir, ".ulpi");
|
|
308
|
+
fs.mkdirSync(rulesDir, { recursive: true });
|
|
309
|
+
try {
|
|
310
|
+
const { detectStack } = await import("./dist-2K7IEVTA.js");
|
|
311
|
+
const stack = detectStack(projectDir);
|
|
312
|
+
const parts = [];
|
|
313
|
+
if (stack.runtime) parts.push(`- **Runtime:** ${stack.runtime.id}`);
|
|
314
|
+
if (stack.language) parts.push(`- **Language:** ${stack.language.id}`);
|
|
315
|
+
if (stack.framework) parts.push(`- **Framework:** ${stack.framework.id}`);
|
|
316
|
+
if (stack.packageManager) parts.push(`- **Package manager:** ${stack.packageManager.id}`);
|
|
317
|
+
if (stack.testRunner) parts.push(`- **Test runner:** ${stack.testRunner.id}`);
|
|
318
|
+
if (stack.linter) parts.push(`- **Linter:** ${stack.linter.id}`);
|
|
319
|
+
if (stack.formatter) parts.push(`- **Formatter:** ${stack.formatter.id}`);
|
|
320
|
+
if (stack.orm) parts.push(`- **ORM:** ${stack.orm.id}`);
|
|
321
|
+
if (stack.gitWorkflow) parts.push(`- **Git workflow:** ${stack.gitWorkflow.id}`);
|
|
322
|
+
if (parts.length > 0) {
|
|
323
|
+
parts.unshift("Detected project stack. Use these technologies when writing code:");
|
|
324
|
+
info.stackContext = parts.join("\n");
|
|
325
|
+
}
|
|
326
|
+
const guardsPath = path.join(rulesDir, "guards.yml");
|
|
327
|
+
if (fs.existsSync(guardsPath)) {
|
|
328
|
+
console.log(chalk.blue("[ulpi-ci] guards.yml found \u2014 rules will be enforced"));
|
|
329
|
+
} else {
|
|
330
|
+
const { loadBundledTemplates, composeTemplates, resolveTemplate } = await import("./dist-XG2GG5SD.js");
|
|
331
|
+
const stackConfig = {
|
|
332
|
+
name: path.basename(projectDir),
|
|
333
|
+
runtime: stack.runtime?.id ?? "unknown",
|
|
334
|
+
language: stack.language?.id,
|
|
335
|
+
framework: stack.framework?.id,
|
|
336
|
+
package_manager: stack.packageManager?.id ?? "npm",
|
|
337
|
+
orm: stack.orm?.id,
|
|
338
|
+
test_runner: stack.testRunner?.id,
|
|
339
|
+
formatter: stack.formatter?.id,
|
|
340
|
+
linter: stack.linter?.id,
|
|
341
|
+
git_workflow: stack.gitWorkflow?.id
|
|
342
|
+
};
|
|
343
|
+
const all = loadBundledTemplates();
|
|
344
|
+
const selected = [all.find((t) => t.id === "quality-of-life")].filter(Boolean);
|
|
345
|
+
const detectors = [
|
|
346
|
+
stack.runtime,
|
|
347
|
+
stack.language,
|
|
348
|
+
stack.packageManager,
|
|
349
|
+
stack.framework,
|
|
350
|
+
stack.formatter,
|
|
351
|
+
stack.linter,
|
|
352
|
+
stack.testRunner,
|
|
353
|
+
stack.orm,
|
|
354
|
+
stack.gitWorkflow
|
|
355
|
+
];
|
|
356
|
+
for (const det of detectors) {
|
|
357
|
+
if (det) {
|
|
358
|
+
const t = all.find((l) => l.id === det.id);
|
|
359
|
+
if (t && !selected.includes(t)) selected.push(t);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const composed = composeTemplates(selected);
|
|
363
|
+
const resolved = resolveTemplate(composed, stackConfig);
|
|
364
|
+
const rulesYaml = generateGuardsYaml(resolved, stackConfig);
|
|
365
|
+
fs.writeFileSync(guardsPath, rulesYaml, "utf-8");
|
|
366
|
+
console.log(chalk.blue("[ulpi-ci] Generated guards.yml from detected stack"));
|
|
367
|
+
}
|
|
368
|
+
} catch (err) {
|
|
369
|
+
console.log(chalk.yellow(`[ulpi-ci] Warning: Could not detect stack/generate guards.yml: ${err instanceof Error ? err.message : String(err)}`));
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
const { installHooks } = await import("./hooks-installer-K2JXEBNN.js");
|
|
373
|
+
installHooks(projectDir);
|
|
374
|
+
console.log(chalk.blue("[ulpi-ci] Hooks installed"));
|
|
375
|
+
} catch (err) {
|
|
376
|
+
console.log(chalk.yellow(`[ulpi-ci] Warning: Could not install hooks: ${err instanceof Error ? err.message : String(err)}`));
|
|
377
|
+
}
|
|
378
|
+
try {
|
|
379
|
+
const { installMcpServer, installMemoryMcpServer } = await import("./mcp-installer-TOYDP77X.js");
|
|
380
|
+
installMcpServer(projectDir);
|
|
381
|
+
installMemoryMcpServer(projectDir);
|
|
382
|
+
console.log(chalk.blue("[ulpi-ci] MCP servers registered"));
|
|
383
|
+
} catch (err) {
|
|
384
|
+
console.log(chalk.yellow(`[ulpi-ci] Warning: Could not register MCP servers: ${err instanceof Error ? err.message : String(err)}`));
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
387
|
+
execFileSync("git", ["config", "user.name", "ULPI CI"], { cwd: projectDir, encoding: "utf-8", timeout: 5e3 });
|
|
388
|
+
execFileSync("git", ["config", "user.email", "ci@ulpi.io"], { cwd: projectDir, encoding: "utf-8", timeout: 5e3 });
|
|
389
|
+
} catch {
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
const { historyBranchExists, initHistoryBranch } = await import("./dist-5R4RYNQO.js");
|
|
393
|
+
if (!historyBranchExists(projectDir)) {
|
|
394
|
+
const projectName = path.basename(projectDir);
|
|
395
|
+
const version = true ? "0.1.5" : "0.0.0";
|
|
396
|
+
initHistoryBranch(projectDir, projectName, version);
|
|
397
|
+
console.log(chalk.blue("[ulpi-ci] History branch initialized"));
|
|
398
|
+
} else {
|
|
399
|
+
console.log(chalk.blue("[ulpi-ci] History branch exists"));
|
|
400
|
+
}
|
|
401
|
+
} catch (err) {
|
|
402
|
+
console.log(chalk.yellow(`[ulpi-ci] Warning: Could not init history: ${err instanceof Error ? err.message : String(err)}`));
|
|
403
|
+
}
|
|
404
|
+
try {
|
|
405
|
+
const { isMemoryInitialized, saveMemoryConfig, DEFAULT_MEMORY_CONFIG, initMemoryBranch } = await import("./dist-7WLLPWWB.js");
|
|
406
|
+
const { projectMemoryDir } = await import("./dist-GWGTAHNM.js");
|
|
407
|
+
if (!isMemoryInitialized(projectDir)) {
|
|
408
|
+
const memDir = projectMemoryDir(projectDir);
|
|
409
|
+
fs.mkdirSync(memDir, { recursive: true });
|
|
410
|
+
saveMemoryConfig(projectDir, { ...DEFAULT_MEMORY_CONFIG, enabled: true });
|
|
411
|
+
console.log(chalk.blue("[ulpi-ci] Memory initialized"));
|
|
412
|
+
}
|
|
413
|
+
try {
|
|
414
|
+
initMemoryBranch(projectDir);
|
|
415
|
+
} catch {
|
|
416
|
+
}
|
|
417
|
+
} catch (err) {
|
|
418
|
+
console.log(chalk.yellow(`[ulpi-ci] Warning: Could not init memory: ${err instanceof Error ? err.message : String(err)}`));
|
|
419
|
+
}
|
|
420
|
+
try {
|
|
421
|
+
const { resolveApiKey } = await import("./dist-GWGTAHNM.js");
|
|
422
|
+
const hasOpenAiKey = !!resolveApiKey("openai");
|
|
423
|
+
const hasUlpiKey = !!resolveApiKey("ulpi");
|
|
424
|
+
if (hasOpenAiKey || hasUlpiKey) {
|
|
425
|
+
const { runInitPipeline } = await import("./dist-6MFVWIFF.js");
|
|
426
|
+
console.log(chalk.blue("[ulpi-ci] Indexing codebase..."));
|
|
427
|
+
await runInitPipeline(projectDir, (progress) => {
|
|
428
|
+
if (progress.message) {
|
|
429
|
+
process.stdout.write(`\r ${chalk.dim(progress.message)}${"".padEnd(20)}`);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
process.stdout.write("\r");
|
|
433
|
+
console.log(chalk.blue("[ulpi-ci] Codemap index ready"));
|
|
434
|
+
} else {
|
|
435
|
+
console.log(chalk.dim("[ulpi-ci] Skipping codemap (no embedding API key)"));
|
|
436
|
+
}
|
|
437
|
+
} catch (err) {
|
|
438
|
+
console.log(chalk.yellow(`[ulpi-ci] Warning: Could not init codemap: ${err instanceof Error ? err.message : String(err)}`));
|
|
439
|
+
}
|
|
440
|
+
console.log(chalk.blue("[ulpi-ci] Subsystem initialization complete"));
|
|
441
|
+
return info;
|
|
442
|
+
}
|
|
443
|
+
function generateGuardsYaml(resolved, config) {
|
|
444
|
+
const lines = [
|
|
445
|
+
"# ULPI \u2014 Rules Configuration",
|
|
446
|
+
"# Generated automatically for CI worker.",
|
|
447
|
+
`# Project: ${config.name}`,
|
|
448
|
+
"",
|
|
449
|
+
"project:",
|
|
450
|
+
` name: "${config.name}"`,
|
|
451
|
+
` runtime: "${config.runtime}"`,
|
|
452
|
+
` package_manager: "${config.package_manager}"`,
|
|
453
|
+
""
|
|
454
|
+
];
|
|
455
|
+
const sections = ["preconditions", "postconditions", "permissions", "pipelines"];
|
|
456
|
+
for (const section of sections) {
|
|
457
|
+
const data = resolved[section];
|
|
458
|
+
if (data && Object.keys(data).length > 0) {
|
|
459
|
+
lines.push(`${section}:`);
|
|
460
|
+
for (const [key, rule] of Object.entries(data)) {
|
|
461
|
+
lines.push(` ${key}:`);
|
|
462
|
+
writeYamlFields(rule, lines, 4);
|
|
463
|
+
}
|
|
464
|
+
lines.push("");
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return lines.join("\n") + "\n";
|
|
468
|
+
}
|
|
469
|
+
function writeYamlFields(obj, lines, indent) {
|
|
470
|
+
const pad = " ".repeat(indent);
|
|
471
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
472
|
+
if (k === "id" || k === "type" || k === "source") continue;
|
|
473
|
+
if (typeof v === "string") {
|
|
474
|
+
lines.push(`${pad}${k}: "${v}"`);
|
|
475
|
+
} else if (typeof v === "boolean" || typeof v === "number") {
|
|
476
|
+
lines.push(`${pad}${k}: ${v}`);
|
|
477
|
+
} else if (Array.isArray(v)) {
|
|
478
|
+
lines.push(`${pad}${k}:`);
|
|
479
|
+
for (const item of v) {
|
|
480
|
+
if (typeof item === "string") {
|
|
481
|
+
lines.push(`${pad} - "${item}"`);
|
|
482
|
+
} else if (typeof item === "object" && item !== null) {
|
|
483
|
+
lines.push(`${pad} -`);
|
|
484
|
+
writeYamlFields(item, lines, indent + 4);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
} else if (v !== null && typeof v === "object") {
|
|
488
|
+
lines.push(`${pad}${k}:`);
|
|
489
|
+
writeYamlFields(v, lines, indent + 2);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
function commitUnstagedWork(cwd) {
|
|
494
|
+
try {
|
|
495
|
+
const status = execFileSync("git", ["status", "--porcelain"], {
|
|
496
|
+
cwd,
|
|
497
|
+
encoding: "utf-8",
|
|
498
|
+
timeout: 1e4
|
|
499
|
+
}).trim();
|
|
500
|
+
if (!status) return;
|
|
501
|
+
const fileCount = status.split("\n").filter(Boolean).length;
|
|
502
|
+
console.log(chalk.blue(`[ulpi-ci] Committing ${fileCount} uncommitted files...`));
|
|
503
|
+
execFileSync("git", ["add", "-A"], { cwd, timeout: 3e4 });
|
|
504
|
+
execFileSync("git", ["commit", "-m", "wip: auto-commit from ULPI CI worker"], {
|
|
505
|
+
cwd,
|
|
506
|
+
timeout: 3e4
|
|
507
|
+
});
|
|
508
|
+
console.log(chalk.blue("[ulpi-ci] WIP commit created"));
|
|
509
|
+
} catch (err) {
|
|
510
|
+
console.log(chalk.yellow(`[ulpi-ci] Warning: Could not auto-commit: ${err instanceof Error ? err.message : String(err)}`));
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
async function postSessionFinalize(projectDir) {
|
|
514
|
+
try {
|
|
515
|
+
const {
|
|
516
|
+
historyBranchExists,
|
|
517
|
+
listBranchOnlyCommits,
|
|
518
|
+
entryExists,
|
|
519
|
+
getCommitMetadata,
|
|
520
|
+
getCommitDiffStats,
|
|
521
|
+
getCommitRawDiff,
|
|
522
|
+
writeHistoryEntry,
|
|
523
|
+
readBranchMeta,
|
|
524
|
+
DEFAULT_HISTORY_CONFIG
|
|
525
|
+
} = await import("./dist-5R4RYNQO.js");
|
|
526
|
+
const { loadActiveGuards } = await import("./dist-5R4RYNQO.js");
|
|
527
|
+
if (historyBranchExists(projectDir)) {
|
|
528
|
+
const commits = listBranchOnlyCommits(projectDir, 50);
|
|
529
|
+
const meta = readBranchMeta(projectDir);
|
|
530
|
+
const maxDiffSize = meta?.config?.maxDiffSize ?? DEFAULT_HISTORY_CONFIG.maxDiffSize;
|
|
531
|
+
let captured = 0;
|
|
532
|
+
for (const sha of commits) {
|
|
533
|
+
if (entryExists(projectDir, sha)) continue;
|
|
534
|
+
try {
|
|
535
|
+
const metadata = getCommitMetadata(projectDir, sha);
|
|
536
|
+
const diffStats = getCommitDiffStats(projectDir, sha);
|
|
537
|
+
const rawDiffResult = getCommitRawDiff(projectDir, sha, maxDiffSize);
|
|
538
|
+
const guardsYaml = loadActiveGuards(projectDir);
|
|
539
|
+
await writeHistoryEntry(projectDir, {
|
|
540
|
+
version: 1,
|
|
541
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
542
|
+
commit: metadata,
|
|
543
|
+
diff: diffStats,
|
|
544
|
+
rawDiff: rawDiffResult.diff || void 0,
|
|
545
|
+
diffTruncated: rawDiffResult.truncated || void 0,
|
|
546
|
+
session: null,
|
|
547
|
+
enrichment: null,
|
|
548
|
+
reviewPlans: null
|
|
549
|
+
}, {
|
|
550
|
+
state: null,
|
|
551
|
+
events: [],
|
|
552
|
+
guardsYaml
|
|
553
|
+
});
|
|
554
|
+
captured++;
|
|
555
|
+
} catch {
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (captured > 0) {
|
|
559
|
+
console.log(chalk.blue(`[ulpi-ci] History backfill: captured ${captured} commits`));
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
} catch (err) {
|
|
563
|
+
console.log(chalk.yellow(`[ulpi-ci] Warning: History backfill failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
564
|
+
}
|
|
565
|
+
try {
|
|
566
|
+
const { isMemoryInitialized, classifySession } = await import("./dist-7WLLPWWB.js");
|
|
567
|
+
const { ULPI_GLOBAL_DIR } = await import("./dist-GWGTAHNM.js");
|
|
568
|
+
const { JsonSessionStore } = await import("./dist-XD4YI27T.js");
|
|
569
|
+
if (isMemoryInitialized(projectDir)) {
|
|
570
|
+
const store = new JsonSessionStore(ULPI_GLOBAL_DIR, projectDir);
|
|
571
|
+
const latest = store.getLatestForProject(projectDir);
|
|
572
|
+
if (latest) {
|
|
573
|
+
console.log(chalk.blue(`[ulpi-ci] Classifying session ${latest.sessionId}...`));
|
|
574
|
+
const result = await classifySession(projectDir, latest.sessionId, (p) => {
|
|
575
|
+
if (p.message) {
|
|
576
|
+
process.stdout.write(`\r ${chalk.dim(p.message)}${"".padEnd(20)}`);
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
process.stdout.write("\r");
|
|
580
|
+
if (result.memoriesStored > 0) {
|
|
581
|
+
console.log(chalk.blue(`[ulpi-ci] Memory: ${result.memoriesStored} memories extracted`));
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
} catch (err) {
|
|
586
|
+
console.log(chalk.yellow(`[ulpi-ci] Warning: Memory classify failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
function findClaudeBinary() {
|
|
590
|
+
try {
|
|
591
|
+
const result = execFileSync("which", ["claude"], { encoding: "utf-8", timeout: 5e3 });
|
|
592
|
+
return result.trim();
|
|
593
|
+
} catch {
|
|
594
|
+
}
|
|
595
|
+
const commonPaths = ["/usr/local/bin/claude", "/usr/bin/claude"];
|
|
596
|
+
for (const p of commonPaths) {
|
|
597
|
+
if (fs.existsSync(p)) return p;
|
|
598
|
+
}
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
function logStreamEvent(line) {
|
|
602
|
+
let event;
|
|
603
|
+
try {
|
|
604
|
+
event = JSON.parse(line);
|
|
605
|
+
} catch {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
const type = event.type;
|
|
609
|
+
switch (type) {
|
|
610
|
+
case "assistant": {
|
|
611
|
+
const msg = event.message;
|
|
612
|
+
if (!msg) break;
|
|
613
|
+
const content = msg.content;
|
|
614
|
+
if (!content) break;
|
|
615
|
+
for (const block of content) {
|
|
616
|
+
if (block.type === "tool_use") {
|
|
617
|
+
const name = block.name;
|
|
618
|
+
const input = block.input;
|
|
619
|
+
let detail = "";
|
|
620
|
+
if (input) {
|
|
621
|
+
if (input.file_path) detail = ` ${input.file_path}`;
|
|
622
|
+
else if (input.command) detail = ` ${String(input.command).slice(0, 80)}`;
|
|
623
|
+
else if (input.pattern) detail = ` ${input.pattern}`;
|
|
624
|
+
else if (input.query) detail = ` ${String(input.query).slice(0, 80)}`;
|
|
625
|
+
else if (input.prompt) detail = ` ${String(input.prompt).slice(0, 80)}`;
|
|
626
|
+
}
|
|
627
|
+
console.log(chalk.cyan(`[claude] tool: ${name}${detail}`));
|
|
628
|
+
} else if (block.type === "text") {
|
|
629
|
+
const text = String(block.text).trim();
|
|
630
|
+
if (text) {
|
|
631
|
+
const firstLine = text.split("\n")[0].slice(0, 120);
|
|
632
|
+
console.log(chalk.gray(`[claude] ${firstLine}`));
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
case "tool_result": {
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
case "result": {
|
|
642
|
+
const cost = event.total_cost_usd;
|
|
643
|
+
const turns = event.num_turns;
|
|
644
|
+
const duration = event.duration_ms;
|
|
645
|
+
console.log(chalk.blue(
|
|
646
|
+
`[claude] Done \u2014 ${turns ?? "?"} turns, ${duration ? formatDuration(duration) : "?"}, $${cost?.toFixed(4) ?? "?"}`
|
|
647
|
+
));
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
650
|
+
case "system": {
|
|
651
|
+
const subtype = event.subtype;
|
|
652
|
+
if (subtype === "init") {
|
|
653
|
+
const model = event.model;
|
|
654
|
+
const version = event.claude_code_version;
|
|
655
|
+
console.log(chalk.blue(`[claude] Initialized \u2014 model: ${model ?? "?"}, version: ${version ?? "?"}`));
|
|
656
|
+
}
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
function spawnClaude(claudePath, args, prompt, timeoutMs) {
|
|
662
|
+
return new Promise((resolve) => {
|
|
663
|
+
const proc = spawn(claudePath, args, {
|
|
664
|
+
cwd: process.cwd(),
|
|
665
|
+
env: process.env,
|
|
666
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
667
|
+
});
|
|
668
|
+
let stdout = "";
|
|
669
|
+
let stderr = "";
|
|
670
|
+
let resultText = "";
|
|
671
|
+
let toolUseCount = 0;
|
|
672
|
+
let lineBuffer = "";
|
|
673
|
+
proc.stdout.on("data", (data) => {
|
|
674
|
+
const text = data.toString();
|
|
675
|
+
stdout += text;
|
|
676
|
+
lineBuffer += text;
|
|
677
|
+
const lines = lineBuffer.split("\n");
|
|
678
|
+
lineBuffer = lines.pop() ?? "";
|
|
679
|
+
for (const line of lines) {
|
|
680
|
+
const trimmed = line.trim();
|
|
681
|
+
if (!trimmed) continue;
|
|
682
|
+
logStreamEvent(trimmed);
|
|
683
|
+
try {
|
|
684
|
+
const event = JSON.parse(trimmed);
|
|
685
|
+
if (event.type === "result") {
|
|
686
|
+
resultText = event.result ?? "";
|
|
687
|
+
} else if (event.type === "assistant") {
|
|
688
|
+
const msg = event.message;
|
|
689
|
+
const content = msg?.content;
|
|
690
|
+
if (content) {
|
|
691
|
+
for (const block of content) {
|
|
692
|
+
if (block.type === "tool_use") toolUseCount++;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
} catch {
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
proc.stderr.on("data", (data) => {
|
|
701
|
+
const text = data.toString();
|
|
702
|
+
stderr += text;
|
|
703
|
+
process.stderr.write(text);
|
|
704
|
+
});
|
|
705
|
+
proc.stdin.write(prompt);
|
|
706
|
+
proc.stdin.end();
|
|
707
|
+
const timer = setTimeout(() => {
|
|
708
|
+
console.log(chalk.yellow("[ulpi-ci] Timeout reached, killing Claude process..."));
|
|
709
|
+
proc.kill("SIGTERM");
|
|
710
|
+
setTimeout(() => {
|
|
711
|
+
if (!proc.killed) proc.kill("SIGKILL");
|
|
712
|
+
}, 1e4);
|
|
713
|
+
}, timeoutMs);
|
|
714
|
+
proc.on("close", (code) => {
|
|
715
|
+
clearTimeout(timer);
|
|
716
|
+
if (lineBuffer.trim()) {
|
|
717
|
+
logStreamEvent(lineBuffer.trim());
|
|
718
|
+
try {
|
|
719
|
+
const event = JSON.parse(lineBuffer.trim());
|
|
720
|
+
if (event.type === "result") {
|
|
721
|
+
resultText = event.result ?? "";
|
|
722
|
+
}
|
|
723
|
+
} catch {
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
resolve({ exitCode: code ?? 1, summary: resultText, toolUseCount });
|
|
727
|
+
});
|
|
728
|
+
const onSigterm = () => {
|
|
729
|
+
console.log(chalk.yellow("[ulpi-ci] Received SIGTERM, stopping Claude..."));
|
|
730
|
+
proc.kill("SIGTERM");
|
|
731
|
+
};
|
|
732
|
+
process.on("SIGTERM", onSigterm);
|
|
733
|
+
proc.on("close", () => {
|
|
734
|
+
process.removeListener("SIGTERM", onSigterm);
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
function writeOutput(outputPath, result) {
|
|
739
|
+
try {
|
|
740
|
+
const dir = path.dirname(outputPath);
|
|
741
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
742
|
+
fs.writeFileSync(outputPath, JSON.stringify({ jobId: result.jobId, result }, null, 2), "utf-8");
|
|
743
|
+
} catch (err) {
|
|
744
|
+
console.error(chalk.red(`Error writing output: ${err instanceof Error ? err.message : String(err)}`));
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
function formatDuration(ms) {
|
|
748
|
+
const seconds = Math.floor(ms / 1e3);
|
|
749
|
+
if (seconds < 60) return `${seconds}s`;
|
|
750
|
+
const minutes = Math.floor(seconds / 60);
|
|
751
|
+
const remainingSeconds = seconds % 60;
|
|
752
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
753
|
+
}
|
|
754
|
+
export {
|
|
755
|
+
runCi
|
|
756
|
+
};
|