@open330/oac 2026.2.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/chunk-UVK4T7KV.js +2238 -0
- package/dist/chunk-UVK4T7KV.js.map +1 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.js +9 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
|
@@ -0,0 +1,2238 @@
|
|
|
1
|
+
// src/cli.ts
|
|
2
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
3
|
+
import { Command as Command9 } from "commander";
|
|
4
|
+
|
|
5
|
+
// src/commands/doctor.ts
|
|
6
|
+
import { spawn } from "child_process";
|
|
7
|
+
import chalk, { Chalk } from "chalk";
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
var MINIMUM_NODE_VERSION = "24.0.0";
|
|
10
|
+
function createDoctorCommand() {
|
|
11
|
+
const command = new Command("doctor");
|
|
12
|
+
command.description("Check local environment readiness").action(async (_options, cmd) => {
|
|
13
|
+
const globalOptions = getGlobalOptions(cmd);
|
|
14
|
+
const ui = createUi(globalOptions);
|
|
15
|
+
const checks = await runDoctorChecks();
|
|
16
|
+
const allPassed = checks.every((check) => check.status === "pass");
|
|
17
|
+
if (globalOptions.json) {
|
|
18
|
+
console.log(
|
|
19
|
+
JSON.stringify(
|
|
20
|
+
{
|
|
21
|
+
checks,
|
|
22
|
+
allPassed
|
|
23
|
+
},
|
|
24
|
+
null,
|
|
25
|
+
2
|
|
26
|
+
)
|
|
27
|
+
);
|
|
28
|
+
} else {
|
|
29
|
+
renderDoctorOutput(ui, checks, allPassed);
|
|
30
|
+
}
|
|
31
|
+
if (!allPassed) {
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
return command;
|
|
36
|
+
}
|
|
37
|
+
async function runDoctorChecks() {
|
|
38
|
+
const checks = [];
|
|
39
|
+
const nodeVersion = process.versions.node;
|
|
40
|
+
checks.push({
|
|
41
|
+
id: "node",
|
|
42
|
+
name: "Node.js",
|
|
43
|
+
requirement: `>= ${MINIMUM_NODE_VERSION}`,
|
|
44
|
+
value: `v${nodeVersion}`,
|
|
45
|
+
status: isVersionAtLeast(nodeVersion, MINIMUM_NODE_VERSION) ? "pass" : "fail",
|
|
46
|
+
message: `Node.js ${MINIMUM_NODE_VERSION}+ is required.`
|
|
47
|
+
});
|
|
48
|
+
const gitResult = await runCommand("git", ["--version"]);
|
|
49
|
+
const gitVersion = extractVersion(gitResult.stdout) ?? "--";
|
|
50
|
+
checks.push({
|
|
51
|
+
id: "git",
|
|
52
|
+
name: "git",
|
|
53
|
+
requirement: "installed",
|
|
54
|
+
value: gitVersion,
|
|
55
|
+
status: gitResult.ok ? "pass" : "fail",
|
|
56
|
+
message: gitResult.ok ? void 0 : explainCommandFailure("git", gitResult)
|
|
57
|
+
});
|
|
58
|
+
const githubAuthCheck = await checkGithubAuth();
|
|
59
|
+
checks.push(githubAuthCheck);
|
|
60
|
+
const claudeResult = await runCommand("claude", ["--version"]);
|
|
61
|
+
const claudeVersion = extractVersion(claudeResult.stdout) ?? "--";
|
|
62
|
+
checks.push({
|
|
63
|
+
id: "claude-cli",
|
|
64
|
+
name: "Claude CLI",
|
|
65
|
+
requirement: "installed",
|
|
66
|
+
value: claudeVersion,
|
|
67
|
+
status: claudeResult.ok ? "pass" : "fail",
|
|
68
|
+
message: claudeResult.ok ? void 0 : explainCommandFailure("claude", claudeResult)
|
|
69
|
+
});
|
|
70
|
+
const codexResult = await runCommand("codex", ["--version"]);
|
|
71
|
+
const codexVersion = extractVersion(codexResult.stdout) ?? "--";
|
|
72
|
+
checks.push({
|
|
73
|
+
id: "codex-cli",
|
|
74
|
+
name: "Codex CLI",
|
|
75
|
+
requirement: "installed",
|
|
76
|
+
value: codexVersion,
|
|
77
|
+
status: codexResult.ok ? "pass" : "fail",
|
|
78
|
+
message: codexResult.ok ? void 0 : explainCommandFailure("codex", codexResult)
|
|
79
|
+
});
|
|
80
|
+
return checks;
|
|
81
|
+
}
|
|
82
|
+
async function checkGithubAuth() {
|
|
83
|
+
const envToken = process.env.GITHUB_TOKEN?.trim();
|
|
84
|
+
if (envToken) {
|
|
85
|
+
return {
|
|
86
|
+
id: "github-auth",
|
|
87
|
+
name: "GitHub auth",
|
|
88
|
+
requirement: "gh auth status or GITHUB_TOKEN",
|
|
89
|
+
value: `env:${maskToken(envToken)}`,
|
|
90
|
+
status: "pass"
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const authResult = await runCommand("gh", ["auth", "status"]);
|
|
94
|
+
if (authResult.ok) {
|
|
95
|
+
return {
|
|
96
|
+
id: "github-auth",
|
|
97
|
+
name: "GitHub auth",
|
|
98
|
+
requirement: "gh auth status or GITHUB_TOKEN",
|
|
99
|
+
value: "gh auth status",
|
|
100
|
+
status: "pass"
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
id: "github-auth",
|
|
105
|
+
name: "GitHub auth",
|
|
106
|
+
requirement: "gh auth status or GITHUB_TOKEN",
|
|
107
|
+
value: "--",
|
|
108
|
+
status: "fail",
|
|
109
|
+
message: "No GitHub authentication detected. Set GITHUB_TOKEN or run `gh auth login`."
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function getGlobalOptions(command) {
|
|
113
|
+
const options = command.optsWithGlobals();
|
|
114
|
+
return {
|
|
115
|
+
config: options.config ?? "oac.config.ts",
|
|
116
|
+
verbose: options.verbose === true,
|
|
117
|
+
json: options.json === true,
|
|
118
|
+
color: options.color !== false
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function createUi(options) {
|
|
122
|
+
const noColorEnv = Object.prototype.hasOwnProperty.call(process.env, "NO_COLOR");
|
|
123
|
+
const colorEnabled = options.color && !noColorEnv;
|
|
124
|
+
return new Chalk({ level: colorEnabled ? chalk.level : 0 });
|
|
125
|
+
}
|
|
126
|
+
function renderDoctorOutput(ui, checks, allPassed) {
|
|
127
|
+
console.log("Checking environment...");
|
|
128
|
+
console.log("");
|
|
129
|
+
for (const check of checks) {
|
|
130
|
+
const icon = check.status === "pass" ? ui.green("[OK]") : ui.red("[X]");
|
|
131
|
+
const status = check.status === "pass" ? ui.green("PASS") : ui.red("FAIL");
|
|
132
|
+
const name = check.name.padEnd(12, " ");
|
|
133
|
+
const requirement = check.requirement.padEnd(30, " ");
|
|
134
|
+
const value = check.value.padEnd(14, " ");
|
|
135
|
+
console.log(` ${icon} ${name} ${requirement} ${value} ${status}`);
|
|
136
|
+
if (check.status === "fail" && check.message) {
|
|
137
|
+
console.log(` ${ui.red(check.message)}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
console.log("");
|
|
141
|
+
if (allPassed) {
|
|
142
|
+
console.log(ui.green("All checks passed."));
|
|
143
|
+
} else {
|
|
144
|
+
console.log(ui.red("Some checks failed."));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function extractVersion(output) {
|
|
148
|
+
const match = output.match(/v?(\d+\.\d+\.\d+)/i);
|
|
149
|
+
if (!match) {
|
|
150
|
+
return void 0;
|
|
151
|
+
}
|
|
152
|
+
return `v${match[1]}`;
|
|
153
|
+
}
|
|
154
|
+
function explainCommandFailure(commandName, result) {
|
|
155
|
+
if (result.errorCode === "ENOENT") {
|
|
156
|
+
return `${commandName} is not installed or not in PATH.`;
|
|
157
|
+
}
|
|
158
|
+
if (result.errorMessage) {
|
|
159
|
+
return result.errorMessage;
|
|
160
|
+
}
|
|
161
|
+
if (result.stderr.trim().length > 0) {
|
|
162
|
+
return result.stderr.trim();
|
|
163
|
+
}
|
|
164
|
+
return `${commandName} exited with code ${String(result.exitCode)}.`;
|
|
165
|
+
}
|
|
166
|
+
function maskToken(token) {
|
|
167
|
+
if (token.length <= 8) {
|
|
168
|
+
return `${token.slice(0, 2)}****`;
|
|
169
|
+
}
|
|
170
|
+
return `${token.slice(0, 4)}****${token.slice(-2)}`;
|
|
171
|
+
}
|
|
172
|
+
function isVersionAtLeast(version, minimum) {
|
|
173
|
+
const current = version.split(".").map((part) => Number.parseInt(part, 10));
|
|
174
|
+
const required = minimum.split(".").map((part) => Number.parseInt(part, 10));
|
|
175
|
+
const length = Math.max(current.length, required.length);
|
|
176
|
+
for (let index = 0; index < length; index += 1) {
|
|
177
|
+
const currentPart = current[index] ?? 0;
|
|
178
|
+
const requiredPart = required[index] ?? 0;
|
|
179
|
+
if (currentPart > requiredPart) {
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
if (currentPart < requiredPart) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
function runCommand(command, args) {
|
|
189
|
+
return new Promise((resolvePromise) => {
|
|
190
|
+
const child = spawn(command, args, {
|
|
191
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
192
|
+
env: process.env
|
|
193
|
+
});
|
|
194
|
+
let stdout = "";
|
|
195
|
+
let stderr = "";
|
|
196
|
+
let resolved = false;
|
|
197
|
+
child.stdout?.setEncoding("utf8");
|
|
198
|
+
child.stderr?.setEncoding("utf8");
|
|
199
|
+
child.stdout?.on("data", (chunk) => {
|
|
200
|
+
stdout += chunk;
|
|
201
|
+
});
|
|
202
|
+
child.stderr?.on("data", (chunk) => {
|
|
203
|
+
stderr += chunk;
|
|
204
|
+
});
|
|
205
|
+
child.once("error", (error) => {
|
|
206
|
+
if (resolved) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
resolved = true;
|
|
210
|
+
const errorWithCode = error;
|
|
211
|
+
resolvePromise({
|
|
212
|
+
ok: false,
|
|
213
|
+
exitCode: null,
|
|
214
|
+
stdout,
|
|
215
|
+
stderr,
|
|
216
|
+
errorCode: errorWithCode.code,
|
|
217
|
+
errorMessage: error.message
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
child.once("close", (exitCode) => {
|
|
221
|
+
if (resolved) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
resolved = true;
|
|
225
|
+
resolvePromise({
|
|
226
|
+
ok: exitCode === 0,
|
|
227
|
+
exitCode,
|
|
228
|
+
stdout,
|
|
229
|
+
stderr
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/commands/init.ts
|
|
236
|
+
import { constants as fsConstants } from "fs";
|
|
237
|
+
import { access, mkdir, writeFile } from "fs/promises";
|
|
238
|
+
import { resolve } from "path";
|
|
239
|
+
import { checkbox, confirm, input } from "@inquirer/prompts";
|
|
240
|
+
import chalk2, { Chalk as Chalk2 } from "chalk";
|
|
241
|
+
import { Command as Command2 } from "commander";
|
|
242
|
+
var OAC_LOGO = [
|
|
243
|
+
" ___ _ ___",
|
|
244
|
+
" / _ \\ /_\\ / __|",
|
|
245
|
+
"| (_) / _ \\ (__",
|
|
246
|
+
" \\___/_/ \\_\\___|"
|
|
247
|
+
].join("\n");
|
|
248
|
+
var OWNER_REPO_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:\.git)?$/;
|
|
249
|
+
function createInitCommand() {
|
|
250
|
+
const command = new Command2("init");
|
|
251
|
+
command.description("Initialize OAC in the current directory").action(async (_options, cmd) => {
|
|
252
|
+
const globalOptions = getGlobalOptions2(cmd);
|
|
253
|
+
const ui = createUi2(globalOptions);
|
|
254
|
+
if (!globalOptions.json) {
|
|
255
|
+
console.log(ui.blue(OAC_LOGO));
|
|
256
|
+
console.log(ui.bold("Welcome to Open Agent Contribution."));
|
|
257
|
+
console.log("");
|
|
258
|
+
}
|
|
259
|
+
const selectedProviders = await checkbox({
|
|
260
|
+
message: "Select AI provider(s):",
|
|
261
|
+
choices: [
|
|
262
|
+
{ name: "Claude Code", value: "claude-code", checked: true },
|
|
263
|
+
{ name: "Codex CLI", value: "codex-cli" },
|
|
264
|
+
{ name: "OpenCode", value: "opencode" }
|
|
265
|
+
],
|
|
266
|
+
validate: (value) => value.length > 0 ? true : "Select at least one provider to continue."
|
|
267
|
+
});
|
|
268
|
+
const budgetInput = await input({
|
|
269
|
+
message: "Monthly token budget:",
|
|
270
|
+
default: "100000",
|
|
271
|
+
validate: (value) => {
|
|
272
|
+
const parsed = Number.parseInt(value, 10);
|
|
273
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
274
|
+
return "Enter a positive integer.";
|
|
275
|
+
}
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
const firstRepoInput = await input({
|
|
280
|
+
message: "Add your first repo (owner/repo or GitHub URL):",
|
|
281
|
+
validate: (value) => {
|
|
282
|
+
if (isValidRepoInput(value)) {
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
return "Enter a valid GitHub repo like owner/repo.";
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
const repo = normalizeRepoInput(firstRepoInput);
|
|
289
|
+
const budgetTokens = Number.parseInt(budgetInput, 10);
|
|
290
|
+
const provider = selectedProviders[0] ?? "claude-code";
|
|
291
|
+
const configPath = resolve(process.cwd(), "oac.config.ts");
|
|
292
|
+
const trackingDirectory = resolve(process.cwd(), ".oac");
|
|
293
|
+
if (await pathExists(configPath)) {
|
|
294
|
+
const shouldOverwrite = await confirm({
|
|
295
|
+
message: "oac.config.ts already exists. Overwrite it?",
|
|
296
|
+
default: false
|
|
297
|
+
});
|
|
298
|
+
if (!shouldOverwrite) {
|
|
299
|
+
if (globalOptions.json) {
|
|
300
|
+
console.log(
|
|
301
|
+
JSON.stringify(
|
|
302
|
+
{
|
|
303
|
+
cancelled: true,
|
|
304
|
+
reason: "oac.config.ts exists and overwrite was declined"
|
|
305
|
+
},
|
|
306
|
+
null,
|
|
307
|
+
2
|
|
308
|
+
)
|
|
309
|
+
);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
console.log(ui.yellow("Initialization cancelled."));
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const configContent = buildConfigFile({
|
|
317
|
+
provider,
|
|
318
|
+
providers: selectedProviders,
|
|
319
|
+
budgetTokens,
|
|
320
|
+
repo
|
|
321
|
+
});
|
|
322
|
+
await writeFile(configPath, configContent, "utf8");
|
|
323
|
+
await mkdir(trackingDirectory, { recursive: true });
|
|
324
|
+
const summary = {
|
|
325
|
+
configPath,
|
|
326
|
+
trackingDirectory,
|
|
327
|
+
provider,
|
|
328
|
+
providers: selectedProviders,
|
|
329
|
+
budgetTokens,
|
|
330
|
+
repo
|
|
331
|
+
};
|
|
332
|
+
if (globalOptions.json) {
|
|
333
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
console.log(ui.green("Created: oac.config.ts"));
|
|
337
|
+
console.log(ui.green("Created: .oac/"));
|
|
338
|
+
console.log("");
|
|
339
|
+
console.log("Run `oac doctor` to verify or `oac scan` to discover tasks.");
|
|
340
|
+
});
|
|
341
|
+
return command;
|
|
342
|
+
}
|
|
343
|
+
function getGlobalOptions2(command) {
|
|
344
|
+
const options = command.optsWithGlobals();
|
|
345
|
+
return {
|
|
346
|
+
config: options.config ?? "oac.config.ts",
|
|
347
|
+
verbose: options.verbose === true,
|
|
348
|
+
json: options.json === true,
|
|
349
|
+
color: options.color !== false
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function createUi2(options) {
|
|
353
|
+
const noColorEnv = Object.prototype.hasOwnProperty.call(process.env, "NO_COLOR");
|
|
354
|
+
const colorEnabled = options.color && !noColorEnv;
|
|
355
|
+
return new Chalk2({ level: colorEnabled ? chalk2.level : 0 });
|
|
356
|
+
}
|
|
357
|
+
function buildConfigFile(input2) {
|
|
358
|
+
const enabledProviders = input2.providers.map((provider) => `'${provider}'`).join(", ");
|
|
359
|
+
return `import { defineConfig } from '@open330/oac-core';
|
|
360
|
+
|
|
361
|
+
export default defineConfig({
|
|
362
|
+
repos: ['${input2.repo}'],
|
|
363
|
+
provider: {
|
|
364
|
+
id: '${input2.provider}',
|
|
365
|
+
options: {
|
|
366
|
+
enabledProviders: [${enabledProviders}],
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
budget: {
|
|
370
|
+
totalTokens: ${input2.budgetTokens},
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
`;
|
|
374
|
+
}
|
|
375
|
+
function normalizeRepoInput(input2) {
|
|
376
|
+
const trimmed = input2.trim();
|
|
377
|
+
if (OWNER_REPO_PATTERN.test(trimmed)) {
|
|
378
|
+
return stripGitSuffix(trimmed);
|
|
379
|
+
}
|
|
380
|
+
const normalizedUrlInput = trimmed.startsWith("github.com/") ? `https://${trimmed}` : trimmed;
|
|
381
|
+
try {
|
|
382
|
+
const url = new URL(normalizedUrlInput);
|
|
383
|
+
if (url.hostname !== "github.com") {
|
|
384
|
+
return trimmed;
|
|
385
|
+
}
|
|
386
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
387
|
+
if (segments.length < 2) {
|
|
388
|
+
return trimmed;
|
|
389
|
+
}
|
|
390
|
+
const owner = segments[0];
|
|
391
|
+
const repo = stripGitSuffix(segments[1] ?? "");
|
|
392
|
+
if (!owner || !repo) {
|
|
393
|
+
return trimmed;
|
|
394
|
+
}
|
|
395
|
+
return `${owner}/${repo}`;
|
|
396
|
+
} catch {
|
|
397
|
+
return trimmed;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
function stripGitSuffix(value) {
|
|
401
|
+
return value.endsWith(".git") ? value.slice(0, -4) : value;
|
|
402
|
+
}
|
|
403
|
+
function isValidRepoInput(input2) {
|
|
404
|
+
const normalized = normalizeRepoInput(input2);
|
|
405
|
+
return OWNER_REPO_PATTERN.test(normalized);
|
|
406
|
+
}
|
|
407
|
+
async function pathExists(path) {
|
|
408
|
+
try {
|
|
409
|
+
await access(path, fsConstants.F_OK);
|
|
410
|
+
return true;
|
|
411
|
+
} catch {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// src/commands/leaderboard.ts
|
|
417
|
+
import { readFile, readdir } from "fs/promises";
|
|
418
|
+
import { resolve as resolve2 } from "path";
|
|
419
|
+
import { contributionLogSchema } from "@open330/oac-tracking";
|
|
420
|
+
import Table from "cli-table3";
|
|
421
|
+
import { Command as Command3 } from "commander";
|
|
422
|
+
function createLeaderboardCommand() {
|
|
423
|
+
const command = new Command3("leaderboard");
|
|
424
|
+
command.description("Show contribution rankings").option("--limit <number>", "Max entries to show", parseInteger, 10).option("--sort <field>", "Sort by: runs, tasks, tokens, prs", "tasks").action(async (options, cmd) => {
|
|
425
|
+
if (options.limit <= 0) {
|
|
426
|
+
throw new Error("--limit must be a positive integer.");
|
|
427
|
+
}
|
|
428
|
+
const globalOptions = getGlobalOptions3(cmd);
|
|
429
|
+
const sortField = normalizeSortField(options.sort);
|
|
430
|
+
const entries = await loadLeaderboardEntries(process.cwd());
|
|
431
|
+
const sortedEntries = sortEntries(entries, sortField).slice(0, options.limit);
|
|
432
|
+
if (globalOptions.json) {
|
|
433
|
+
console.log(
|
|
434
|
+
JSON.stringify(
|
|
435
|
+
{
|
|
436
|
+
total: sortedEntries.length,
|
|
437
|
+
sort: sortField,
|
|
438
|
+
entries: sortedEntries
|
|
439
|
+
},
|
|
440
|
+
null,
|
|
441
|
+
2
|
|
442
|
+
)
|
|
443
|
+
);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (sortedEntries.length === 0) {
|
|
447
|
+
console.log("No leaderboard data found.");
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const table = new Table({
|
|
451
|
+
head: ["Rank", "User", "Tasks", "Tokens Used", "PRs Created", "PRs Merged"]
|
|
452
|
+
});
|
|
453
|
+
for (let index = 0; index < sortedEntries.length; index += 1) {
|
|
454
|
+
const entry = sortedEntries[index];
|
|
455
|
+
table.push([
|
|
456
|
+
String(index + 1),
|
|
457
|
+
entry.githubUsername,
|
|
458
|
+
String(entry.totalTasksCompleted),
|
|
459
|
+
formatInteger(entry.totalTokensDonated),
|
|
460
|
+
String(entry.totalPRsCreated),
|
|
461
|
+
String(entry.totalPRsMerged)
|
|
462
|
+
]);
|
|
463
|
+
}
|
|
464
|
+
console.log(table.toString());
|
|
465
|
+
});
|
|
466
|
+
return command;
|
|
467
|
+
}
|
|
468
|
+
function getGlobalOptions3(command) {
|
|
469
|
+
const options = command.optsWithGlobals();
|
|
470
|
+
return {
|
|
471
|
+
config: options.config ?? "oac.config.ts",
|
|
472
|
+
verbose: options.verbose === true,
|
|
473
|
+
json: options.json === true,
|
|
474
|
+
color: options.color !== false
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
async function loadLeaderboardEntries(repoPath) {
|
|
478
|
+
const leaderboardPath = resolve2(repoPath, ".oac", "leaderboard.json");
|
|
479
|
+
try {
|
|
480
|
+
const leaderboardRaw = await readFile(leaderboardPath, "utf8");
|
|
481
|
+
const leaderboardPayload = JSON.parse(leaderboardRaw);
|
|
482
|
+
return parseStoredLeaderboardEntries(leaderboardPayload);
|
|
483
|
+
} catch (error) {
|
|
484
|
+
if (!isFileNotFoundError(error)) {
|
|
485
|
+
throw error;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
const logs = await readContributionLogs(repoPath);
|
|
489
|
+
return buildEntriesFromLogs(logs);
|
|
490
|
+
}
|
|
491
|
+
function parseStoredLeaderboardEntries(payload) {
|
|
492
|
+
if (!isRecord(payload)) {
|
|
493
|
+
return [];
|
|
494
|
+
}
|
|
495
|
+
const entries = payload.entries;
|
|
496
|
+
if (!Array.isArray(entries)) {
|
|
497
|
+
return [];
|
|
498
|
+
}
|
|
499
|
+
const parsedEntries = [];
|
|
500
|
+
for (const entry of entries) {
|
|
501
|
+
if (!isRecord(entry)) {
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
const githubUsername = entry.githubUsername;
|
|
505
|
+
const totalRuns = entry.totalRuns;
|
|
506
|
+
const totalTasksCompleted = entry.totalTasksCompleted;
|
|
507
|
+
const totalTokensDonated = entry.totalTokensDonated;
|
|
508
|
+
const totalPRsCreated = entry.totalPRsCreated;
|
|
509
|
+
const totalPRsMerged = entry.totalPRsMerged;
|
|
510
|
+
if (typeof githubUsername !== "string" || typeof totalRuns !== "number" || typeof totalTasksCompleted !== "number" || typeof totalTokensDonated !== "number" || typeof totalPRsCreated !== "number" || typeof totalPRsMerged !== "number") {
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
parsedEntries.push({
|
|
514
|
+
githubUsername,
|
|
515
|
+
totalRuns,
|
|
516
|
+
totalTasksCompleted,
|
|
517
|
+
totalTokensDonated,
|
|
518
|
+
totalPRsCreated,
|
|
519
|
+
totalPRsMerged
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
return parsedEntries;
|
|
523
|
+
}
|
|
524
|
+
async function readContributionLogs(repoPath) {
|
|
525
|
+
const contributionsPath = resolve2(repoPath, ".oac", "contributions");
|
|
526
|
+
let entries;
|
|
527
|
+
try {
|
|
528
|
+
entries = await readdir(contributionsPath, { withFileTypes: true, encoding: "utf8" });
|
|
529
|
+
} catch (error) {
|
|
530
|
+
if (isFileNotFoundError(error)) {
|
|
531
|
+
return [];
|
|
532
|
+
}
|
|
533
|
+
throw error;
|
|
534
|
+
}
|
|
535
|
+
const fileNames = entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name).sort((a, b) => a.localeCompare(b));
|
|
536
|
+
const logs = await Promise.all(
|
|
537
|
+
fileNames.map(async (fileName) => {
|
|
538
|
+
const filePath = resolve2(contributionsPath, fileName);
|
|
539
|
+
try {
|
|
540
|
+
const content = await readFile(filePath, "utf8");
|
|
541
|
+
const payload = JSON.parse(content);
|
|
542
|
+
const parsed = contributionLogSchema.safeParse(payload);
|
|
543
|
+
return parsed.success ? parsed.data : null;
|
|
544
|
+
} catch {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
})
|
|
548
|
+
);
|
|
549
|
+
return logs.filter((log) => log !== null);
|
|
550
|
+
}
|
|
551
|
+
function buildEntriesFromLogs(logs) {
|
|
552
|
+
const byUser = /* @__PURE__ */ new Map();
|
|
553
|
+
for (const log of logs) {
|
|
554
|
+
const username = log.contributor.githubUsername;
|
|
555
|
+
const existing = byUser.get(username) ?? {
|
|
556
|
+
githubUsername: username,
|
|
557
|
+
totalRuns: 0,
|
|
558
|
+
totalTasksCompleted: 0,
|
|
559
|
+
totalTokensDonated: 0,
|
|
560
|
+
totalPRsCreated: 0,
|
|
561
|
+
totalPRsMerged: 0
|
|
562
|
+
};
|
|
563
|
+
existing.totalRuns += 1;
|
|
564
|
+
existing.totalTasksCompleted += log.tasks.filter((task) => task.status !== "failed").length;
|
|
565
|
+
existing.totalTokensDonated += log.budget.totalTokensUsed;
|
|
566
|
+
existing.totalPRsCreated += log.tasks.filter((task) => Boolean(task.pr)).length;
|
|
567
|
+
existing.totalPRsMerged += log.tasks.filter((task) => task.pr?.status === "merged").length;
|
|
568
|
+
byUser.set(username, existing);
|
|
569
|
+
}
|
|
570
|
+
return Array.from(byUser.values());
|
|
571
|
+
}
|
|
572
|
+
function sortEntries(entries, field) {
|
|
573
|
+
return [...entries].sort((a, b) => {
|
|
574
|
+
const first = sortValue(b, field) - sortValue(a, field);
|
|
575
|
+
if (first !== 0) {
|
|
576
|
+
return first;
|
|
577
|
+
}
|
|
578
|
+
if (b.totalTasksCompleted !== a.totalTasksCompleted) {
|
|
579
|
+
return b.totalTasksCompleted - a.totalTasksCompleted;
|
|
580
|
+
}
|
|
581
|
+
if (b.totalRuns !== a.totalRuns) {
|
|
582
|
+
return b.totalRuns - a.totalRuns;
|
|
583
|
+
}
|
|
584
|
+
return a.githubUsername.localeCompare(b.githubUsername);
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
function normalizeSortField(value) {
|
|
588
|
+
const normalized = value.trim().toLowerCase();
|
|
589
|
+
if (normalized === "runs" || normalized === "tasks" || normalized === "tokens" || normalized === "prs") {
|
|
590
|
+
return normalized;
|
|
591
|
+
}
|
|
592
|
+
throw new Error(`Unsupported --sort value "${value}". Use runs, tasks, tokens, or prs.`);
|
|
593
|
+
}
|
|
594
|
+
function sortValue(entry, field) {
|
|
595
|
+
if (field === "runs") {
|
|
596
|
+
return entry.totalRuns;
|
|
597
|
+
}
|
|
598
|
+
if (field === "tasks") {
|
|
599
|
+
return entry.totalTasksCompleted;
|
|
600
|
+
}
|
|
601
|
+
if (field === "tokens") {
|
|
602
|
+
return entry.totalTokensDonated;
|
|
603
|
+
}
|
|
604
|
+
return entry.totalPRsCreated;
|
|
605
|
+
}
|
|
606
|
+
function parseInteger(value) {
|
|
607
|
+
const parsed = Number.parseInt(value, 10);
|
|
608
|
+
if (!Number.isFinite(parsed)) {
|
|
609
|
+
throw new Error(`Expected an integer but received "${value}".`);
|
|
610
|
+
}
|
|
611
|
+
return parsed;
|
|
612
|
+
}
|
|
613
|
+
function formatInteger(value) {
|
|
614
|
+
return new Intl.NumberFormat("en-US").format(value);
|
|
615
|
+
}
|
|
616
|
+
function isRecord(value) {
|
|
617
|
+
return typeof value === "object" && value !== null;
|
|
618
|
+
}
|
|
619
|
+
function isFileNotFoundError(error) {
|
|
620
|
+
if (!isRecord(error)) {
|
|
621
|
+
return false;
|
|
622
|
+
}
|
|
623
|
+
return error.code === "ENOENT";
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// src/commands/log.ts
|
|
627
|
+
import { readFile as readFile2, readdir as readdir2 } from "fs/promises";
|
|
628
|
+
import { resolve as resolve3 } from "path";
|
|
629
|
+
import { contributionLogSchema as contributionLogSchema2 } from "@open330/oac-tracking";
|
|
630
|
+
import Table2 from "cli-table3";
|
|
631
|
+
import { Command as Command4 } from "commander";
|
|
632
|
+
function createLogCommand() {
|
|
633
|
+
const command = new Command4("log");
|
|
634
|
+
command.description("View contribution history").option("--limit <number>", "Max entries to show", parseInteger2, 20).option("--repo <name>", "Filter by repo name").option("--source <type>", "Filter by task source").option("--since <date>", "Filter contributions after date (ISO string)").action(async (options, cmd) => {
|
|
635
|
+
if (options.limit <= 0) {
|
|
636
|
+
throw new Error("--limit must be a positive integer.");
|
|
637
|
+
}
|
|
638
|
+
const globalOptions = getGlobalOptions4(cmd);
|
|
639
|
+
const sinceDate = parseSinceDate(options.since);
|
|
640
|
+
const repoFilter = options.repo?.trim();
|
|
641
|
+
const sourceFilter = options.source?.trim().toLowerCase();
|
|
642
|
+
const logs = await readContributionLogs2(process.cwd());
|
|
643
|
+
const filteredLogs = logs.filter((log) => repoFilter ? log.repo.fullName === repoFilter : true).filter(
|
|
644
|
+
(log) => sourceFilter ? log.tasks.some((task) => task.source === sourceFilter) : true
|
|
645
|
+
).filter((log) => sinceDate ? Date.parse(log.timestamp) >= sinceDate.getTime() : true).sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp)).slice(0, options.limit);
|
|
646
|
+
if (globalOptions.json) {
|
|
647
|
+
console.log(
|
|
648
|
+
JSON.stringify(
|
|
649
|
+
{
|
|
650
|
+
total: filteredLogs.length,
|
|
651
|
+
entries: filteredLogs
|
|
652
|
+
},
|
|
653
|
+
null,
|
|
654
|
+
2
|
|
655
|
+
)
|
|
656
|
+
);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (filteredLogs.length === 0) {
|
|
660
|
+
console.log("No contribution logs found.");
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
const table = new Table2({
|
|
664
|
+
head: ["Date", "Repo", "Tasks", "Tokens", "PRs", "Source"]
|
|
665
|
+
});
|
|
666
|
+
for (const log of filteredLogs) {
|
|
667
|
+
table.push([
|
|
668
|
+
formatDate(log.timestamp),
|
|
669
|
+
log.repo.fullName,
|
|
670
|
+
String(log.tasks.length),
|
|
671
|
+
formatInteger2(log.budget.totalTokensUsed),
|
|
672
|
+
String(log.tasks.filter((task) => Boolean(task.pr)).length),
|
|
673
|
+
summarizeSources(log)
|
|
674
|
+
]);
|
|
675
|
+
}
|
|
676
|
+
console.log(table.toString());
|
|
677
|
+
});
|
|
678
|
+
return command;
|
|
679
|
+
}
|
|
680
|
+
function getGlobalOptions4(command) {
|
|
681
|
+
const options = command.optsWithGlobals();
|
|
682
|
+
return {
|
|
683
|
+
config: options.config ?? "oac.config.ts",
|
|
684
|
+
verbose: options.verbose === true,
|
|
685
|
+
json: options.json === true,
|
|
686
|
+
color: options.color !== false
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
async function readContributionLogs2(repoPath) {
|
|
690
|
+
const contributionsPath = resolve3(repoPath, ".oac", "contributions");
|
|
691
|
+
let entries;
|
|
692
|
+
try {
|
|
693
|
+
entries = await readdir2(contributionsPath, { withFileTypes: true, encoding: "utf8" });
|
|
694
|
+
} catch (error) {
|
|
695
|
+
if (isFileNotFoundError2(error)) {
|
|
696
|
+
return [];
|
|
697
|
+
}
|
|
698
|
+
throw error;
|
|
699
|
+
}
|
|
700
|
+
const files = entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name).sort((a, b) => a.localeCompare(b));
|
|
701
|
+
const logs = await Promise.all(
|
|
702
|
+
files.map(async (fileName) => {
|
|
703
|
+
const filePath = resolve3(contributionsPath, fileName);
|
|
704
|
+
try {
|
|
705
|
+
const content = await readFile2(filePath, "utf8");
|
|
706
|
+
const payload = JSON.parse(content);
|
|
707
|
+
const parsed = contributionLogSchema2.safeParse(payload);
|
|
708
|
+
return parsed.success ? parsed.data : null;
|
|
709
|
+
} catch {
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
})
|
|
713
|
+
);
|
|
714
|
+
return logs.filter((log) => log !== null);
|
|
715
|
+
}
|
|
716
|
+
function parseInteger2(value) {
|
|
717
|
+
const parsed = Number.parseInt(value, 10);
|
|
718
|
+
if (!Number.isFinite(parsed)) {
|
|
719
|
+
throw new Error(`Expected an integer but received "${value}".`);
|
|
720
|
+
}
|
|
721
|
+
return parsed;
|
|
722
|
+
}
|
|
723
|
+
function parseSinceDate(value) {
|
|
724
|
+
if (!value) {
|
|
725
|
+
return null;
|
|
726
|
+
}
|
|
727
|
+
const parsed = Date.parse(value);
|
|
728
|
+
if (!Number.isFinite(parsed)) {
|
|
729
|
+
throw new Error(`Invalid --since value "${value}". Expected an ISO date string.`);
|
|
730
|
+
}
|
|
731
|
+
return new Date(parsed);
|
|
732
|
+
}
|
|
733
|
+
function summarizeSources(log) {
|
|
734
|
+
const uniqueSources = [...new Set(log.tasks.map((task) => task.source))].sort(
|
|
735
|
+
(a, b) => a.localeCompare(b)
|
|
736
|
+
);
|
|
737
|
+
if (uniqueSources.length === 0) {
|
|
738
|
+
return "-";
|
|
739
|
+
}
|
|
740
|
+
return uniqueSources.join(", ");
|
|
741
|
+
}
|
|
742
|
+
function formatDate(timestamp) {
|
|
743
|
+
const date = new Date(timestamp);
|
|
744
|
+
if (Number.isNaN(date.getTime())) {
|
|
745
|
+
return timestamp;
|
|
746
|
+
}
|
|
747
|
+
return date.toISOString();
|
|
748
|
+
}
|
|
749
|
+
function formatInteger2(value) {
|
|
750
|
+
return new Intl.NumberFormat("en-US").format(value);
|
|
751
|
+
}
|
|
752
|
+
function isFileNotFoundError2(error) {
|
|
753
|
+
if (typeof error !== "object" || error === null) {
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
756
|
+
const code = error.code;
|
|
757
|
+
return code === "ENOENT";
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// src/commands/plan.ts
|
|
761
|
+
import { constants as fsConstants2 } from "fs";
|
|
762
|
+
import { access as access2 } from "fs/promises";
|
|
763
|
+
import { resolve as resolve4 } from "path";
|
|
764
|
+
import { pathToFileURL } from "url";
|
|
765
|
+
import { buildExecutionPlan, estimateTokens } from "@open330/oac-budget";
|
|
766
|
+
import { loadConfig } from "@open330/oac-core";
|
|
767
|
+
import {
|
|
768
|
+
CompositeScanner,
|
|
769
|
+
LintScanner,
|
|
770
|
+
TodoScanner,
|
|
771
|
+
rankTasks
|
|
772
|
+
} from "@open330/oac-discovery";
|
|
773
|
+
import { cloneRepo, resolveRepo } from "@open330/oac-repo";
|
|
774
|
+
import chalk3, { Chalk as Chalk3 } from "chalk";
|
|
775
|
+
import Table3 from "cli-table3";
|
|
776
|
+
import { Command as Command5 } from "commander";
|
|
777
|
+
import ora from "ora";
|
|
778
|
+
function createPlanCommand() {
|
|
779
|
+
const command = new Command5("plan");
|
|
780
|
+
command.description("Build an execution plan from discovered tasks").option("--repo <owner/repo>", "Target repository (owner/repo or GitHub URL)").option("--tokens <number>", "Token budget for planning", parseInteger3).option("--provider <id>", "Agent provider id").action(async (options, cmd) => {
|
|
781
|
+
const globalOptions = getGlobalOptions5(cmd);
|
|
782
|
+
const ui = createUi3(globalOptions);
|
|
783
|
+
const outputJson = globalOptions.json;
|
|
784
|
+
const config = await loadOptionalConfig(globalOptions.config, globalOptions.verbose, ui);
|
|
785
|
+
const repoInput = resolveRepoInput(options.repo, config);
|
|
786
|
+
const providerId = resolveProviderId(options.provider, config);
|
|
787
|
+
const totalBudget = resolveBudget(options.tokens, config);
|
|
788
|
+
const minPriority = config?.discovery.minPriority ?? 20;
|
|
789
|
+
const scannerSelection = selectScannersFromConfig(config);
|
|
790
|
+
const resolveSpinner = createSpinner(outputJson, "Resolving repository...");
|
|
791
|
+
const resolvedRepo = await resolveRepo(repoInput);
|
|
792
|
+
resolveSpinner?.succeed(`Resolved ${resolvedRepo.fullName}`);
|
|
793
|
+
const cloneSpinner = createSpinner(outputJson, "Preparing local clone...");
|
|
794
|
+
await cloneRepo(resolvedRepo);
|
|
795
|
+
cloneSpinner?.succeed(`Repository ready at ${resolvedRepo.localPath}`);
|
|
796
|
+
const scanSpinner = createSpinner(
|
|
797
|
+
outputJson,
|
|
798
|
+
`Running scanners: ${scannerSelection.enabled.join(", ")}`
|
|
799
|
+
);
|
|
800
|
+
const scannedTasks = await scannerSelection.scanner.scan(resolvedRepo.localPath, {
|
|
801
|
+
exclude: config?.discovery.exclude,
|
|
802
|
+
maxTasks: config?.discovery.maxTasks,
|
|
803
|
+
repo: resolvedRepo
|
|
804
|
+
});
|
|
805
|
+
scanSpinner?.succeed(`Discovered ${scannedTasks.length} raw task(s)`);
|
|
806
|
+
const rankedTasks = rankTasks(scannedTasks).filter((task) => task.priority >= minPriority);
|
|
807
|
+
const estimateSpinner = createSpinner(
|
|
808
|
+
outputJson,
|
|
809
|
+
`Estimating tokens for ${rankedTasks.length} task(s)...`
|
|
810
|
+
);
|
|
811
|
+
const estimates = await estimateTaskMap(rankedTasks, providerId);
|
|
812
|
+
estimateSpinner?.succeed("Token estimation completed");
|
|
813
|
+
const plan = buildExecutionPlan(rankedTasks, estimates, totalBudget);
|
|
814
|
+
if (outputJson) {
|
|
815
|
+
console.log(
|
|
816
|
+
JSON.stringify(
|
|
817
|
+
{
|
|
818
|
+
repo: resolvedRepo.fullName,
|
|
819
|
+
provider: providerId,
|
|
820
|
+
budget: totalBudget,
|
|
821
|
+
tasksDiscovered: rankedTasks.length,
|
|
822
|
+
plan
|
|
823
|
+
},
|
|
824
|
+
null,
|
|
825
|
+
2
|
|
826
|
+
)
|
|
827
|
+
);
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
renderPlan(ui, {
|
|
831
|
+
repo: resolvedRepo.fullName,
|
|
832
|
+
provider: providerId,
|
|
833
|
+
budget: totalBudget,
|
|
834
|
+
plan
|
|
835
|
+
});
|
|
836
|
+
});
|
|
837
|
+
return command;
|
|
838
|
+
}
|
|
839
|
+
function getGlobalOptions5(command) {
|
|
840
|
+
const options = command.optsWithGlobals();
|
|
841
|
+
return {
|
|
842
|
+
config: options.config ?? "oac.config.ts",
|
|
843
|
+
verbose: options.verbose === true,
|
|
844
|
+
json: options.json === true,
|
|
845
|
+
color: options.color !== false
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
function createUi3(options) {
|
|
849
|
+
const noColorEnv = Object.prototype.hasOwnProperty.call(process.env, "NO_COLOR");
|
|
850
|
+
const colorEnabled = options.color && !noColorEnv;
|
|
851
|
+
return new Chalk3({ level: colorEnabled ? chalk3.level : 0 });
|
|
852
|
+
}
|
|
853
|
+
function createSpinner(enabled, text) {
|
|
854
|
+
if (enabled) {
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
857
|
+
return ora({ text, color: "blue" }).start();
|
|
858
|
+
}
|
|
859
|
+
function parseInteger3(value) {
|
|
860
|
+
const parsed = Number.parseInt(value, 10);
|
|
861
|
+
if (!Number.isFinite(parsed)) {
|
|
862
|
+
throw new Error(`Expected an integer but received "${value}".`);
|
|
863
|
+
}
|
|
864
|
+
return parsed;
|
|
865
|
+
}
|
|
866
|
+
async function loadOptionalConfig(configPath, verbose, ui) {
|
|
867
|
+
const absolutePath = resolve4(process.cwd(), configPath);
|
|
868
|
+
if (!await pathExists2(absolutePath)) {
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
try {
|
|
872
|
+
const imported = await import(`${pathToFileURL(absolutePath).href}?t=${Date.now()}`);
|
|
873
|
+
const candidate = imported.default ?? imported.config ?? imported;
|
|
874
|
+
return loadConfig(candidate);
|
|
875
|
+
} catch (error) {
|
|
876
|
+
if (verbose) {
|
|
877
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
878
|
+
console.warn(ui.yellow(`[oac] Failed to load config at ${configPath}: ${message}`));
|
|
879
|
+
}
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
function resolveRepoInput(repoOption, config) {
|
|
884
|
+
const fromFlag = repoOption?.trim();
|
|
885
|
+
if (fromFlag) {
|
|
886
|
+
return fromFlag;
|
|
887
|
+
}
|
|
888
|
+
const firstConfiguredRepo = config?.repos[0];
|
|
889
|
+
if (typeof firstConfiguredRepo === "string") {
|
|
890
|
+
return firstConfiguredRepo;
|
|
891
|
+
}
|
|
892
|
+
if (firstConfiguredRepo && typeof firstConfiguredRepo === "object" && "name" in firstConfiguredRepo && typeof firstConfiguredRepo.name === "string") {
|
|
893
|
+
return firstConfiguredRepo.name;
|
|
894
|
+
}
|
|
895
|
+
throw new Error("No repository specified. Use --repo or configure repos in oac.config.ts.");
|
|
896
|
+
}
|
|
897
|
+
function resolveProviderId(providerOption, config) {
|
|
898
|
+
const fromFlag = providerOption?.trim();
|
|
899
|
+
if (fromFlag) {
|
|
900
|
+
return fromFlag;
|
|
901
|
+
}
|
|
902
|
+
return config?.provider.id ?? "claude-code";
|
|
903
|
+
}
|
|
904
|
+
function resolveBudget(tokensOption, config) {
|
|
905
|
+
const budget = tokensOption ?? config?.budget.totalTokens ?? 1e5;
|
|
906
|
+
if (!Number.isFinite(budget) || budget <= 0) {
|
|
907
|
+
throw new Error("Token budget must be a positive number.");
|
|
908
|
+
}
|
|
909
|
+
return Math.floor(budget);
|
|
910
|
+
}
|
|
911
|
+
function selectScannersFromConfig(config) {
|
|
912
|
+
const enabled = [];
|
|
913
|
+
if (config?.discovery.scanners.lint !== false) {
|
|
914
|
+
enabled.push("lint");
|
|
915
|
+
}
|
|
916
|
+
if (config?.discovery.scanners.todo !== false) {
|
|
917
|
+
enabled.push("todo");
|
|
918
|
+
}
|
|
919
|
+
if (enabled.length === 0) {
|
|
920
|
+
enabled.push("lint", "todo");
|
|
921
|
+
}
|
|
922
|
+
const uniqueEnabled = [...new Set(enabled)];
|
|
923
|
+
const scannerInstances = uniqueEnabled.map(
|
|
924
|
+
(scannerName) => scannerName === "lint" ? new LintScanner() : new TodoScanner()
|
|
925
|
+
);
|
|
926
|
+
return {
|
|
927
|
+
enabled: uniqueEnabled,
|
|
928
|
+
scanner: new CompositeScanner(scannerInstances)
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
async function estimateTaskMap(tasks, providerId) {
|
|
932
|
+
const entries = await Promise.all(
|
|
933
|
+
tasks.map(async (task) => {
|
|
934
|
+
const estimate = await estimateTokens(task, providerId);
|
|
935
|
+
return [task.id, estimate];
|
|
936
|
+
})
|
|
937
|
+
);
|
|
938
|
+
return new Map(entries);
|
|
939
|
+
}
|
|
940
|
+
function renderPlan(ui, data) {
|
|
941
|
+
const table = new Table3({
|
|
942
|
+
head: ["#", "Task", "Est. Tokens", "Cumulative", "Confidence"]
|
|
943
|
+
});
|
|
944
|
+
for (let index = 0; index < data.plan.selectedTasks.length; index += 1) {
|
|
945
|
+
const entry = data.plan.selectedTasks[index];
|
|
946
|
+
table.push([
|
|
947
|
+
String(index + 1),
|
|
948
|
+
truncate(entry.task.title, 56),
|
|
949
|
+
formatInteger3(entry.estimate.totalEstimatedTokens),
|
|
950
|
+
formatInteger3(entry.cumulativeBudgetUsed),
|
|
951
|
+
entry.estimate.confidence.toFixed(2)
|
|
952
|
+
]);
|
|
953
|
+
}
|
|
954
|
+
console.log(ui.bold(`Execution Plan for ${data.repo}`));
|
|
955
|
+
console.log(`Provider: ${data.provider}`);
|
|
956
|
+
console.log("");
|
|
957
|
+
if (data.plan.selectedTasks.length > 0) {
|
|
958
|
+
console.log(table.toString());
|
|
959
|
+
console.log("");
|
|
960
|
+
} else {
|
|
961
|
+
console.log(ui.yellow("No tasks selected for execution."));
|
|
962
|
+
console.log("");
|
|
963
|
+
}
|
|
964
|
+
const effectiveBudget = data.plan.totalBudget - data.plan.reserveTokens;
|
|
965
|
+
const budgetUsed = data.plan.selectedTasks[data.plan.selectedTasks.length - 1]?.cumulativeBudgetUsed ?? 0;
|
|
966
|
+
console.log(
|
|
967
|
+
`Budget used: ${formatInteger3(budgetUsed)} / ${formatInteger3(effectiveBudget)} (effective)`
|
|
968
|
+
);
|
|
969
|
+
console.log(`Reserve: ${formatInteger3(data.plan.reserveTokens)} (10%)`);
|
|
970
|
+
console.log(`Remaining: ${formatInteger3(data.plan.remainingTokens)}`);
|
|
971
|
+
if (data.plan.deferredTasks.length > 0) {
|
|
972
|
+
console.log("");
|
|
973
|
+
console.log(ui.yellow(`Deferred (${data.plan.deferredTasks.length}):`));
|
|
974
|
+
for (const deferred of data.plan.deferredTasks) {
|
|
975
|
+
const reason = deferred.reason.replaceAll("_", " ");
|
|
976
|
+
console.log(
|
|
977
|
+
` - ${truncate(deferred.task.title, 72)} (${formatInteger3(
|
|
978
|
+
deferred.estimate.totalEstimatedTokens
|
|
979
|
+
)} tokens, ${reason})`
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
function formatInteger3(value) {
|
|
985
|
+
return new Intl.NumberFormat("en-US").format(value);
|
|
986
|
+
}
|
|
987
|
+
function truncate(value, maxLength) {
|
|
988
|
+
if (value.length <= maxLength) {
|
|
989
|
+
return value;
|
|
990
|
+
}
|
|
991
|
+
return `${value.slice(0, Math.max(0, maxLength - 3))}...`;
|
|
992
|
+
}
|
|
993
|
+
async function pathExists2(path) {
|
|
994
|
+
try {
|
|
995
|
+
await access2(path, fsConstants2.F_OK);
|
|
996
|
+
return true;
|
|
997
|
+
} catch {
|
|
998
|
+
return false;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// src/commands/run.ts
|
|
1003
|
+
import { randomUUID } from "crypto";
|
|
1004
|
+
import { constants as fsConstants3 } from "fs";
|
|
1005
|
+
import { access as access3 } from "fs/promises";
|
|
1006
|
+
import { resolve as resolve5 } from "path";
|
|
1007
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
1008
|
+
import { buildExecutionPlan as buildExecutionPlan2, estimateTokens as estimateTokens2 } from "@open330/oac-budget";
|
|
1009
|
+
import {
|
|
1010
|
+
UNLIMITED_BUDGET,
|
|
1011
|
+
createEventBus,
|
|
1012
|
+
loadConfig as loadConfig2
|
|
1013
|
+
} from "@open330/oac-core";
|
|
1014
|
+
import {
|
|
1015
|
+
CompositeScanner as CompositeScanner2,
|
|
1016
|
+
GitHubIssuesScanner,
|
|
1017
|
+
LintScanner as LintScanner2,
|
|
1018
|
+
TodoScanner as TodoScanner2,
|
|
1019
|
+
rankTasks as rankTasks2
|
|
1020
|
+
} from "@open330/oac-discovery";
|
|
1021
|
+
import {
|
|
1022
|
+
CodexAdapter,
|
|
1023
|
+
createSandbox,
|
|
1024
|
+
executeTask as workerExecuteTask
|
|
1025
|
+
} from "@open330/oac-execution";
|
|
1026
|
+
import { cloneRepo as cloneRepo2, resolveRepo as resolveRepo2 } from "@open330/oac-repo";
|
|
1027
|
+
import { writeContributionLog } from "@open330/oac-tracking";
|
|
1028
|
+
import chalk4, { Chalk as Chalk4 } from "chalk";
|
|
1029
|
+
import Table4 from "cli-table3";
|
|
1030
|
+
import { Command as Command6 } from "commander";
|
|
1031
|
+
import { execa } from "execa";
|
|
1032
|
+
import ora2 from "ora";
|
|
1033
|
+
var DEFAULT_TIMEOUT_SECONDS = 300;
|
|
1034
|
+
var DEFAULT_CONCURRENCY = 2;
|
|
1035
|
+
function createRunCommand() {
|
|
1036
|
+
const command = new Command6("run");
|
|
1037
|
+
command.description("Run the full OAC pipeline").option("--repo <owner/repo>", "Target repository (owner/repo or GitHub URL)").option("--tokens <value>", 'Token budget (number or "unlimited")', parseTokens).option("--provider <id>", "Agent provider id").option("--concurrency <number>", "Maximum parallel task executions", parseInteger4).option("--dry-run", "Show plan without executing tasks", false).option("--mode <mode>", "Execution mode: new-pr|update-pr|direct-commit").option("--max-tasks <number>", "Maximum number of discovered tasks to consider", parseInteger4).option("--timeout <seconds>", "Per-task timeout in seconds", parseInteger4).option("--source <source>", "Filter tasks by source (lint, todo, github-issue, test-gap)").action(async (options, cmd) => {
|
|
1038
|
+
const globalOptions = getGlobalOptions6(cmd);
|
|
1039
|
+
const ui = createUi4(globalOptions);
|
|
1040
|
+
const outputJson = globalOptions.json;
|
|
1041
|
+
validateRunOptions(options);
|
|
1042
|
+
const config = await loadOptionalConfig2(globalOptions.config, globalOptions.verbose, ui);
|
|
1043
|
+
const repoInput = resolveRepoInput2(options.repo, config);
|
|
1044
|
+
const providerId = resolveProviderId2(options.provider, config);
|
|
1045
|
+
const totalBudget = resolveBudget2(options.tokens, config);
|
|
1046
|
+
const mode = resolveMode(options.mode, config);
|
|
1047
|
+
const concurrency = resolveConcurrency(options.concurrency, config);
|
|
1048
|
+
const timeoutSeconds = resolveTimeout(options.timeout, config);
|
|
1049
|
+
const minPriority = config?.discovery.minPriority ?? 20;
|
|
1050
|
+
const maxTasks = options.maxTasks ?? void 0;
|
|
1051
|
+
const scannerSelection = selectScannersFromConfig2(config);
|
|
1052
|
+
const runStartedAt = Date.now();
|
|
1053
|
+
const runId = randomUUID();
|
|
1054
|
+
if (!outputJson) {
|
|
1055
|
+
console.log(
|
|
1056
|
+
ui.blue(
|
|
1057
|
+
`Starting OAC run (budget: ${formatBudgetDisplay(totalBudget)} tokens, concurrency: ${concurrency})`
|
|
1058
|
+
)
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
const resolveSpinner = createSpinner2(outputJson, "Resolving repository...");
|
|
1062
|
+
const resolvedRepo = await resolveRepo2(repoInput);
|
|
1063
|
+
resolveSpinner?.succeed(`Resolved ${resolvedRepo.fullName}`);
|
|
1064
|
+
const cloneSpinner = createSpinner2(outputJson, "Preparing local clone...");
|
|
1065
|
+
await cloneRepo2(resolvedRepo);
|
|
1066
|
+
cloneSpinner?.succeed(`Repository ready at ${resolvedRepo.localPath}`);
|
|
1067
|
+
const scanSpinner = createSpinner2(
|
|
1068
|
+
outputJson,
|
|
1069
|
+
`Running scanners: ${scannerSelection.enabled.join(", ")}`
|
|
1070
|
+
);
|
|
1071
|
+
const scannedTasks = await scannerSelection.scanner.scan(resolvedRepo.localPath, {
|
|
1072
|
+
exclude: config?.discovery.exclude,
|
|
1073
|
+
maxTasks: config?.discovery.maxTasks,
|
|
1074
|
+
repo: resolvedRepo
|
|
1075
|
+
});
|
|
1076
|
+
scanSpinner?.succeed(`Discovered ${scannedTasks.length} raw task(s)`);
|
|
1077
|
+
let candidateTasks = rankTasks2(scannedTasks).filter((task) => task.priority >= minPriority);
|
|
1078
|
+
if (options.source) {
|
|
1079
|
+
candidateTasks = candidateTasks.filter((task) => task.source === options.source);
|
|
1080
|
+
}
|
|
1081
|
+
if (typeof maxTasks === "number") {
|
|
1082
|
+
candidateTasks = candidateTasks.slice(0, maxTasks);
|
|
1083
|
+
}
|
|
1084
|
+
if (candidateTasks.length === 0) {
|
|
1085
|
+
const emptySummary = {
|
|
1086
|
+
runId,
|
|
1087
|
+
repo: resolvedRepo.fullName,
|
|
1088
|
+
provider: providerId,
|
|
1089
|
+
dryRun: Boolean(options.dryRun),
|
|
1090
|
+
selectedTasks: 0,
|
|
1091
|
+
deferredTasks: 0,
|
|
1092
|
+
tasksCompleted: 0,
|
|
1093
|
+
tasksFailed: 0,
|
|
1094
|
+
prsCreated: 0,
|
|
1095
|
+
tokensUsed: 0,
|
|
1096
|
+
tokensBudgeted: totalBudget
|
|
1097
|
+
};
|
|
1098
|
+
if (outputJson) {
|
|
1099
|
+
console.log(JSON.stringify({ summary: emptySummary, plan: null }, null, 2));
|
|
1100
|
+
} else {
|
|
1101
|
+
console.log(ui.yellow("No tasks discovered for execution."));
|
|
1102
|
+
}
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
const estimateSpinner = createSpinner2(
|
|
1106
|
+
outputJson,
|
|
1107
|
+
`Estimating tokens for ${candidateTasks.length} task(s)...`
|
|
1108
|
+
);
|
|
1109
|
+
const estimates = await estimateTaskMap2(candidateTasks, providerId);
|
|
1110
|
+
estimateSpinner?.succeed("Token estimation completed");
|
|
1111
|
+
const plan = buildExecutionPlan2(candidateTasks, estimates, totalBudget);
|
|
1112
|
+
if (options.dryRun) {
|
|
1113
|
+
const dryRunSummary = {
|
|
1114
|
+
runId,
|
|
1115
|
+
repo: resolvedRepo.fullName,
|
|
1116
|
+
provider: providerId,
|
|
1117
|
+
dryRun: true,
|
|
1118
|
+
selectedTasks: plan.selectedTasks.length,
|
|
1119
|
+
deferredTasks: plan.deferredTasks.length,
|
|
1120
|
+
tasksCompleted: 0,
|
|
1121
|
+
tasksFailed: 0,
|
|
1122
|
+
prsCreated: 0,
|
|
1123
|
+
tokensUsed: 0,
|
|
1124
|
+
tokensBudgeted: totalBudget
|
|
1125
|
+
};
|
|
1126
|
+
if (outputJson) {
|
|
1127
|
+
console.log(
|
|
1128
|
+
JSON.stringify(
|
|
1129
|
+
{
|
|
1130
|
+
summary: dryRunSummary,
|
|
1131
|
+
plan
|
|
1132
|
+
},
|
|
1133
|
+
null,
|
|
1134
|
+
2
|
|
1135
|
+
)
|
|
1136
|
+
);
|
|
1137
|
+
} else {
|
|
1138
|
+
renderSelectedPlanTable(ui, plan, totalBudget);
|
|
1139
|
+
console.log("");
|
|
1140
|
+
console.log(ui.blue("Dry run complete. No tasks were executed."));
|
|
1141
|
+
}
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
const codexAdapter = new CodexAdapter();
|
|
1145
|
+
const codexAvailability = await codexAdapter.checkAvailability();
|
|
1146
|
+
const useRealExecution = providerId.includes("codex") && codexAvailability.available;
|
|
1147
|
+
if (!outputJson && globalOptions.verbose) {
|
|
1148
|
+
if (useRealExecution) {
|
|
1149
|
+
console.log(
|
|
1150
|
+
ui.green(
|
|
1151
|
+
`[oac] Using Codex CLI v${codexAvailability.version ?? "unknown"} for execution.`
|
|
1152
|
+
)
|
|
1153
|
+
);
|
|
1154
|
+
} else {
|
|
1155
|
+
console.log(ui.yellow("[oac] Codex CLI not available. Using simulated execution."));
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
const executionSpinner = createSpinner2(
|
|
1159
|
+
outputJson,
|
|
1160
|
+
`Executing ${plan.selectedTasks.length} planned task(s)...`
|
|
1161
|
+
);
|
|
1162
|
+
let completedCount = 0;
|
|
1163
|
+
const executedTasks = await runWithConcurrency(
|
|
1164
|
+
plan.selectedTasks,
|
|
1165
|
+
concurrency,
|
|
1166
|
+
async (entry) => {
|
|
1167
|
+
let execution;
|
|
1168
|
+
let sandbox;
|
|
1169
|
+
if (useRealExecution) {
|
|
1170
|
+
const result = await executeWithCodex({
|
|
1171
|
+
task: entry.task,
|
|
1172
|
+
estimate: entry.estimate,
|
|
1173
|
+
codexAdapter,
|
|
1174
|
+
repoPath: resolvedRepo.localPath,
|
|
1175
|
+
baseBranch: resolvedRepo.meta.defaultBranch,
|
|
1176
|
+
timeoutSeconds
|
|
1177
|
+
});
|
|
1178
|
+
execution = result.execution;
|
|
1179
|
+
sandbox = result.sandbox;
|
|
1180
|
+
} else {
|
|
1181
|
+
execution = await simulateExecution(entry.task, entry.estimate);
|
|
1182
|
+
}
|
|
1183
|
+
completedCount += 1;
|
|
1184
|
+
if (executionSpinner) {
|
|
1185
|
+
executionSpinner.text = `Executing tasks... (${completedCount}/${plan.selectedTasks.length})`;
|
|
1186
|
+
}
|
|
1187
|
+
return {
|
|
1188
|
+
task: entry.task,
|
|
1189
|
+
estimate: entry.estimate,
|
|
1190
|
+
execution,
|
|
1191
|
+
sandbox
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
);
|
|
1195
|
+
executionSpinner?.succeed("Execution stage finished");
|
|
1196
|
+
const completionSpinner = createSpinner2(outputJson, "Completing task outputs...");
|
|
1197
|
+
const completedTasks = await runWithConcurrency(
|
|
1198
|
+
executedTasks,
|
|
1199
|
+
concurrency,
|
|
1200
|
+
async (result) => {
|
|
1201
|
+
if (mode === "direct-commit") {
|
|
1202
|
+
return result;
|
|
1203
|
+
}
|
|
1204
|
+
if (!result.execution.success) {
|
|
1205
|
+
return result;
|
|
1206
|
+
}
|
|
1207
|
+
const pr = await createPullRequest({
|
|
1208
|
+
task: result.task,
|
|
1209
|
+
execution: result.execution,
|
|
1210
|
+
sandbox: result.sandbox,
|
|
1211
|
+
repoFullName: resolvedRepo.fullName,
|
|
1212
|
+
baseBranch: resolvedRepo.meta.defaultBranch
|
|
1213
|
+
});
|
|
1214
|
+
if (!pr) {
|
|
1215
|
+
return result;
|
|
1216
|
+
}
|
|
1217
|
+
return {
|
|
1218
|
+
...result,
|
|
1219
|
+
pr
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
);
|
|
1223
|
+
completionSpinner?.succeed("Completion stage finished");
|
|
1224
|
+
const tasksCompleted = completedTasks.filter((task) => task.execution.success).length;
|
|
1225
|
+
const tasksFailed = completedTasks.length - tasksCompleted;
|
|
1226
|
+
const prsCreated = completedTasks.filter((task) => Boolean(task.pr)).length;
|
|
1227
|
+
const tokensUsed = completedTasks.reduce(
|
|
1228
|
+
(sum, task) => sum + task.execution.totalTokensUsed,
|
|
1229
|
+
0
|
|
1230
|
+
);
|
|
1231
|
+
const runDurationSeconds = (Date.now() - runStartedAt) / 1e3;
|
|
1232
|
+
const contributionLog = buildContributionLog({
|
|
1233
|
+
runId,
|
|
1234
|
+
repoFullName: resolvedRepo.fullName,
|
|
1235
|
+
repoHeadSha: resolvedRepo.git.headSha,
|
|
1236
|
+
defaultBranch: resolvedRepo.meta.defaultBranch,
|
|
1237
|
+
repoOwner: resolvedRepo.owner,
|
|
1238
|
+
providerId,
|
|
1239
|
+
totalBudget,
|
|
1240
|
+
runDurationSeconds,
|
|
1241
|
+
discoveredTasks: candidateTasks.length,
|
|
1242
|
+
taskResults: completedTasks
|
|
1243
|
+
});
|
|
1244
|
+
const trackingSpinner = createSpinner2(outputJson, "Writing contribution log...");
|
|
1245
|
+
let logPath;
|
|
1246
|
+
try {
|
|
1247
|
+
logPath = await writeContributionLog(contributionLog, resolvedRepo.localPath);
|
|
1248
|
+
trackingSpinner?.succeed(`Contribution log written: ${logPath}`);
|
|
1249
|
+
} catch (error) {
|
|
1250
|
+
trackingSpinner?.fail("Failed to write contribution log");
|
|
1251
|
+
if (globalOptions.verbose && !outputJson) {
|
|
1252
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1253
|
+
console.warn(ui.yellow(`[oac] Tracking failed: ${message}`));
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
const summary = {
|
|
1257
|
+
runId,
|
|
1258
|
+
repo: resolvedRepo.fullName,
|
|
1259
|
+
provider: providerId,
|
|
1260
|
+
dryRun: false,
|
|
1261
|
+
selectedTasks: plan.selectedTasks.length,
|
|
1262
|
+
deferredTasks: plan.deferredTasks.length,
|
|
1263
|
+
tasksCompleted,
|
|
1264
|
+
tasksFailed,
|
|
1265
|
+
prsCreated,
|
|
1266
|
+
tokensUsed,
|
|
1267
|
+
tokensBudgeted: totalBudget,
|
|
1268
|
+
logPath
|
|
1269
|
+
};
|
|
1270
|
+
if (outputJson) {
|
|
1271
|
+
console.log(
|
|
1272
|
+
JSON.stringify(
|
|
1273
|
+
{
|
|
1274
|
+
summary,
|
|
1275
|
+
plan,
|
|
1276
|
+
tasks: completedTasks
|
|
1277
|
+
},
|
|
1278
|
+
null,
|
|
1279
|
+
2
|
|
1280
|
+
)
|
|
1281
|
+
);
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
renderTaskResults(ui, completedTasks);
|
|
1285
|
+
console.log("");
|
|
1286
|
+
console.log(ui.bold("Run Summary"));
|
|
1287
|
+
console.log(` Tasks completed: ${tasksCompleted}/${completedTasks.length}`);
|
|
1288
|
+
console.log(` Tasks failed: ${tasksFailed}`);
|
|
1289
|
+
console.log(` PRs created: ${prsCreated}`);
|
|
1290
|
+
console.log(
|
|
1291
|
+
` Tokens used: ${formatInteger4(tokensUsed)} / ${formatBudgetDisplay(totalBudget)}`
|
|
1292
|
+
);
|
|
1293
|
+
console.log(` Duration: ${formatDuration(runDurationSeconds)}`);
|
|
1294
|
+
if (logPath) {
|
|
1295
|
+
console.log(` Log: ${logPath}`);
|
|
1296
|
+
}
|
|
1297
|
+
});
|
|
1298
|
+
return command;
|
|
1299
|
+
}
|
|
1300
|
+
function getGlobalOptions6(command) {
|
|
1301
|
+
const options = command.optsWithGlobals();
|
|
1302
|
+
return {
|
|
1303
|
+
config: options.config ?? "oac.config.ts",
|
|
1304
|
+
verbose: options.verbose === true,
|
|
1305
|
+
json: options.json === true,
|
|
1306
|
+
color: options.color !== false
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
function createUi4(options) {
|
|
1310
|
+
const noColorEnv = Object.prototype.hasOwnProperty.call(process.env, "NO_COLOR");
|
|
1311
|
+
const colorEnabled = options.color && !noColorEnv;
|
|
1312
|
+
return new Chalk4({ level: colorEnabled ? chalk4.level : 0 });
|
|
1313
|
+
}
|
|
1314
|
+
function createSpinner2(enabled, text) {
|
|
1315
|
+
if (enabled) {
|
|
1316
|
+
return null;
|
|
1317
|
+
}
|
|
1318
|
+
return ora2({ text, color: "blue" }).start();
|
|
1319
|
+
}
|
|
1320
|
+
function parseInteger4(value) {
|
|
1321
|
+
const parsed = Number.parseInt(value, 10);
|
|
1322
|
+
if (!Number.isFinite(parsed)) {
|
|
1323
|
+
throw new Error(`Expected an integer but received "${value}".`);
|
|
1324
|
+
}
|
|
1325
|
+
return parsed;
|
|
1326
|
+
}
|
|
1327
|
+
function parseTokens(value) {
|
|
1328
|
+
if (value.toLowerCase() === "unlimited") {
|
|
1329
|
+
return UNLIMITED_BUDGET;
|
|
1330
|
+
}
|
|
1331
|
+
return parseInteger4(value);
|
|
1332
|
+
}
|
|
1333
|
+
function formatBudgetDisplay(budget) {
|
|
1334
|
+
if (budget >= UNLIMITED_BUDGET) {
|
|
1335
|
+
return "unlimited";
|
|
1336
|
+
}
|
|
1337
|
+
return formatInteger4(budget);
|
|
1338
|
+
}
|
|
1339
|
+
function validateRunOptions(options) {
|
|
1340
|
+
if (typeof options.concurrency === "number" && options.concurrency <= 0) {
|
|
1341
|
+
throw new Error("--concurrency must be greater than zero.");
|
|
1342
|
+
}
|
|
1343
|
+
if (typeof options.timeout === "number" && options.timeout <= 0) {
|
|
1344
|
+
throw new Error("--timeout must be greater than zero.");
|
|
1345
|
+
}
|
|
1346
|
+
if (typeof options.maxTasks === "number" && options.maxTasks <= 0) {
|
|
1347
|
+
throw new Error("--max-tasks must be greater than zero when provided.");
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
async function loadOptionalConfig2(configPath, verbose, ui) {
|
|
1351
|
+
const absolutePath = resolve5(process.cwd(), configPath);
|
|
1352
|
+
if (!await pathExists3(absolutePath)) {
|
|
1353
|
+
return null;
|
|
1354
|
+
}
|
|
1355
|
+
try {
|
|
1356
|
+
const imported = await import(`${pathToFileURL2(absolutePath).href}?t=${Date.now()}`);
|
|
1357
|
+
const candidate = imported.default ?? imported.config ?? imported;
|
|
1358
|
+
return loadConfig2(candidate);
|
|
1359
|
+
} catch (error) {
|
|
1360
|
+
if (verbose) {
|
|
1361
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1362
|
+
console.warn(ui.yellow(`[oac] Failed to load config at ${configPath}: ${message}`));
|
|
1363
|
+
}
|
|
1364
|
+
return null;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
function resolveRepoInput2(repoOption, config) {
|
|
1368
|
+
const fromFlag = repoOption?.trim();
|
|
1369
|
+
if (fromFlag) {
|
|
1370
|
+
return fromFlag;
|
|
1371
|
+
}
|
|
1372
|
+
const firstConfiguredRepo = config?.repos[0];
|
|
1373
|
+
if (typeof firstConfiguredRepo === "string") {
|
|
1374
|
+
return firstConfiguredRepo;
|
|
1375
|
+
}
|
|
1376
|
+
if (firstConfiguredRepo && typeof firstConfiguredRepo === "object" && "name" in firstConfiguredRepo && typeof firstConfiguredRepo.name === "string") {
|
|
1377
|
+
return firstConfiguredRepo.name;
|
|
1378
|
+
}
|
|
1379
|
+
throw new Error("No repository specified. Use --repo or configure repos in oac.config.ts.");
|
|
1380
|
+
}
|
|
1381
|
+
function resolveProviderId2(providerOption, config) {
|
|
1382
|
+
const fromFlag = providerOption?.trim();
|
|
1383
|
+
if (fromFlag) {
|
|
1384
|
+
return fromFlag;
|
|
1385
|
+
}
|
|
1386
|
+
return config?.provider.id ?? "claude-code";
|
|
1387
|
+
}
|
|
1388
|
+
function resolveBudget2(tokensOption, config) {
|
|
1389
|
+
const budget = tokensOption ?? config?.budget.totalTokens ?? 1e5;
|
|
1390
|
+
if (!Number.isFinite(budget) || budget <= 0) {
|
|
1391
|
+
throw new Error("Token budget must be a positive number.");
|
|
1392
|
+
}
|
|
1393
|
+
return Math.floor(budget);
|
|
1394
|
+
}
|
|
1395
|
+
function resolveMode(modeOption, config) {
|
|
1396
|
+
const candidate = (modeOption ?? config?.execution.mode ?? "new-pr").trim();
|
|
1397
|
+
if (candidate === "new-pr" || candidate === "update-pr" || candidate === "direct-commit") {
|
|
1398
|
+
return candidate;
|
|
1399
|
+
}
|
|
1400
|
+
throw new Error(`Invalid --mode value "${candidate}".`);
|
|
1401
|
+
}
|
|
1402
|
+
function resolveConcurrency(concurrencyOption, config) {
|
|
1403
|
+
const configuredConcurrency = typeof concurrencyOption === "number" ? concurrencyOption : config?.execution.concurrency ?? DEFAULT_CONCURRENCY;
|
|
1404
|
+
if (!Number.isFinite(configuredConcurrency) || configuredConcurrency <= 0) {
|
|
1405
|
+
throw new Error("Concurrency must be a positive integer.");
|
|
1406
|
+
}
|
|
1407
|
+
return Math.floor(configuredConcurrency);
|
|
1408
|
+
}
|
|
1409
|
+
function resolveTimeout(timeoutOption, config) {
|
|
1410
|
+
const configuredTimeout = typeof timeoutOption === "number" ? timeoutOption : config?.execution.taskTimeout ?? DEFAULT_TIMEOUT_SECONDS;
|
|
1411
|
+
if (!Number.isFinite(configuredTimeout) || configuredTimeout <= 0) {
|
|
1412
|
+
throw new Error("Timeout must be a positive integer.");
|
|
1413
|
+
}
|
|
1414
|
+
return Math.floor(configuredTimeout);
|
|
1415
|
+
}
|
|
1416
|
+
function selectScannersFromConfig2(config) {
|
|
1417
|
+
const enabled = [];
|
|
1418
|
+
if (config?.discovery.scanners.lint !== false) {
|
|
1419
|
+
enabled.push("lint");
|
|
1420
|
+
}
|
|
1421
|
+
if (config?.discovery.scanners.todo !== false) {
|
|
1422
|
+
enabled.push("todo");
|
|
1423
|
+
}
|
|
1424
|
+
if (process.env.GITHUB_TOKEN) {
|
|
1425
|
+
enabled.push("github-issues");
|
|
1426
|
+
}
|
|
1427
|
+
if (enabled.length === 0) {
|
|
1428
|
+
enabled.push("lint", "todo");
|
|
1429
|
+
}
|
|
1430
|
+
const uniqueEnabled = [...new Set(enabled)];
|
|
1431
|
+
const scannerInstances = uniqueEnabled.map((scannerName) => {
|
|
1432
|
+
if (scannerName === "lint") return new LintScanner2();
|
|
1433
|
+
if (scannerName === "github-issues") return new GitHubIssuesScanner();
|
|
1434
|
+
return new TodoScanner2();
|
|
1435
|
+
});
|
|
1436
|
+
return {
|
|
1437
|
+
enabled: uniqueEnabled,
|
|
1438
|
+
scanner: new CompositeScanner2(scannerInstances)
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
async function estimateTaskMap2(tasks, providerId) {
|
|
1442
|
+
const entries = await Promise.all(
|
|
1443
|
+
tasks.map(async (task) => {
|
|
1444
|
+
const estimate = await estimateTokens2(task, providerId);
|
|
1445
|
+
return [task.id, estimate];
|
|
1446
|
+
})
|
|
1447
|
+
);
|
|
1448
|
+
return new Map(entries);
|
|
1449
|
+
}
|
|
1450
|
+
async function executeWithCodex(input2) {
|
|
1451
|
+
const startedAt = Date.now();
|
|
1452
|
+
const taskSlug = input2.task.id.replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").slice(0, 30);
|
|
1453
|
+
const branchName = `oac/${Date.now()}-${taskSlug}`;
|
|
1454
|
+
const sandbox = await createSandbox(input2.repoPath, branchName, input2.baseBranch);
|
|
1455
|
+
const eventBus = createEventBus();
|
|
1456
|
+
const sandboxInfo = {
|
|
1457
|
+
branchName,
|
|
1458
|
+
sandboxPath: sandbox.path,
|
|
1459
|
+
cleanup: sandbox.cleanup
|
|
1460
|
+
};
|
|
1461
|
+
try {
|
|
1462
|
+
const result = await workerExecuteTask(input2.codexAdapter, input2.task, sandbox, eventBus, {
|
|
1463
|
+
tokenBudget: input2.estimate.totalEstimatedTokens,
|
|
1464
|
+
timeoutMs: input2.timeoutSeconds * 1e3
|
|
1465
|
+
});
|
|
1466
|
+
const commitResult = await commitSandboxChanges(sandbox.path, input2.task);
|
|
1467
|
+
const filesChanged = commitResult.filesChanged.length > 0 ? commitResult.filesChanged : result.filesChanged.length > 0 ? result.filesChanged : [];
|
|
1468
|
+
return {
|
|
1469
|
+
execution: {
|
|
1470
|
+
success: result.success || commitResult.hasChanges,
|
|
1471
|
+
exitCode: result.exitCode,
|
|
1472
|
+
totalTokensUsed: result.totalTokensUsed,
|
|
1473
|
+
filesChanged,
|
|
1474
|
+
duration: result.duration > 0 ? result.duration / 1e3 : (Date.now() - startedAt) / 1e3,
|
|
1475
|
+
error: result.error
|
|
1476
|
+
},
|
|
1477
|
+
sandbox: sandboxInfo
|
|
1478
|
+
};
|
|
1479
|
+
} catch (error) {
|
|
1480
|
+
const commitResult = await commitSandboxChanges(sandbox.path, input2.task);
|
|
1481
|
+
if (commitResult.hasChanges) {
|
|
1482
|
+
return {
|
|
1483
|
+
execution: {
|
|
1484
|
+
success: true,
|
|
1485
|
+
exitCode: 0,
|
|
1486
|
+
totalTokensUsed: 0,
|
|
1487
|
+
filesChanged: commitResult.filesChanged,
|
|
1488
|
+
duration: (Date.now() - startedAt) / 1e3
|
|
1489
|
+
},
|
|
1490
|
+
sandbox: sandboxInfo
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1494
|
+
return {
|
|
1495
|
+
execution: {
|
|
1496
|
+
success: false,
|
|
1497
|
+
exitCode: 1,
|
|
1498
|
+
totalTokensUsed: 0,
|
|
1499
|
+
filesChanged: [],
|
|
1500
|
+
duration: (Date.now() - startedAt) / 1e3,
|
|
1501
|
+
error: message
|
|
1502
|
+
},
|
|
1503
|
+
sandbox: sandboxInfo
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
async function createPullRequest(input2) {
|
|
1508
|
+
if (!input2.sandbox) {
|
|
1509
|
+
return void 0;
|
|
1510
|
+
}
|
|
1511
|
+
const { branchName, sandboxPath } = input2.sandbox;
|
|
1512
|
+
const [owner, repo] = input2.repoFullName.split("/");
|
|
1513
|
+
try {
|
|
1514
|
+
await execa("git", ["push", "--set-upstream", "origin", branchName], { cwd: sandboxPath });
|
|
1515
|
+
const prTitle = `[OAC] ${input2.task.title}`;
|
|
1516
|
+
const prBody = [
|
|
1517
|
+
"## Summary",
|
|
1518
|
+
"",
|
|
1519
|
+
input2.task.description || `Automated contribution for task "${input2.task.title}".`,
|
|
1520
|
+
"",
|
|
1521
|
+
"## Context",
|
|
1522
|
+
"",
|
|
1523
|
+
`- **Task source:** ${input2.task.source}`,
|
|
1524
|
+
`- **Complexity:** ${input2.task.complexity}`,
|
|
1525
|
+
`- **Tokens used:** ${input2.execution.totalTokensUsed}`,
|
|
1526
|
+
`- **Files changed:** ${input2.execution.filesChanged.length}`,
|
|
1527
|
+
"",
|
|
1528
|
+
"---",
|
|
1529
|
+
"*This PR was automatically generated by [OAC](https://github.com/Open330/open-agent-contribution).*"
|
|
1530
|
+
].join("\n");
|
|
1531
|
+
const ghResult = await execa(
|
|
1532
|
+
"gh",
|
|
1533
|
+
[
|
|
1534
|
+
"pr",
|
|
1535
|
+
"create",
|
|
1536
|
+
"--repo",
|
|
1537
|
+
input2.repoFullName,
|
|
1538
|
+
"--title",
|
|
1539
|
+
prTitle,
|
|
1540
|
+
"--body",
|
|
1541
|
+
prBody,
|
|
1542
|
+
"--head",
|
|
1543
|
+
branchName,
|
|
1544
|
+
"--base",
|
|
1545
|
+
input2.baseBranch
|
|
1546
|
+
],
|
|
1547
|
+
{ cwd: sandboxPath }
|
|
1548
|
+
);
|
|
1549
|
+
const prUrl = ghResult.stdout.trim();
|
|
1550
|
+
const prNumberMatch = prUrl.match(/\/pull\/(\d+)/);
|
|
1551
|
+
const prNumber = prNumberMatch ? Number.parseInt(prNumberMatch[1], 10) : 0;
|
|
1552
|
+
return {
|
|
1553
|
+
number: prNumber,
|
|
1554
|
+
url: prUrl,
|
|
1555
|
+
status: "open"
|
|
1556
|
+
};
|
|
1557
|
+
} catch (error) {
|
|
1558
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1559
|
+
console.warn(`[oac] PR creation failed: ${message}`);
|
|
1560
|
+
return void 0;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
async function simulateExecution(task, estimate) {
|
|
1564
|
+
const start = Date.now();
|
|
1565
|
+
const delayMs = Math.min(1500, Math.max(150, Math.round(estimate.totalEstimatedTokens / 40)));
|
|
1566
|
+
await sleep(delayMs);
|
|
1567
|
+
return {
|
|
1568
|
+
success: true,
|
|
1569
|
+
exitCode: 0,
|
|
1570
|
+
totalTokensUsed: Math.max(1, Math.round(estimate.totalEstimatedTokens * 0.9)),
|
|
1571
|
+
filesChanged: task.targetFiles.length > 0 ? task.targetFiles.slice(0, Math.min(task.targetFiles.length, 4)) : [],
|
|
1572
|
+
duration: (Date.now() - start) / 1e3
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
async function commitSandboxChanges(sandboxPath, task) {
|
|
1576
|
+
try {
|
|
1577
|
+
const statusResult = await execa("git", ["status", "--porcelain"], { cwd: sandboxPath });
|
|
1578
|
+
if (!statusResult.stdout.trim()) {
|
|
1579
|
+
return { hasChanges: false, filesChanged: [] };
|
|
1580
|
+
}
|
|
1581
|
+
await execa("git", ["add", "-A"], { cwd: sandboxPath });
|
|
1582
|
+
await execa(
|
|
1583
|
+
"git",
|
|
1584
|
+
["commit", "-m", `[OAC] ${task.title}
|
|
1585
|
+
|
|
1586
|
+
Automated contribution by OAC using Codex CLI.`],
|
|
1587
|
+
{ cwd: sandboxPath }
|
|
1588
|
+
);
|
|
1589
|
+
const diffResult = await execa("git", ["diff", "--name-only", "HEAD~1", "HEAD"], {
|
|
1590
|
+
cwd: sandboxPath
|
|
1591
|
+
});
|
|
1592
|
+
const changedFiles = diffResult.stdout.trim().split("\n").filter(Boolean);
|
|
1593
|
+
return { hasChanges: true, filesChanged: changedFiles };
|
|
1594
|
+
} catch {
|
|
1595
|
+
return { hasChanges: false, filesChanged: [] };
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
function buildContributionLog(input2) {
|
|
1599
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1600
|
+
const contributor = resolveGithubUsername(input2.repoOwner);
|
|
1601
|
+
const contributionTasks = input2.taskResults.map((result) => ({
|
|
1602
|
+
taskId: result.task.id,
|
|
1603
|
+
title: result.task.title,
|
|
1604
|
+
source: result.task.source,
|
|
1605
|
+
complexity: result.task.complexity,
|
|
1606
|
+
status: deriveTaskStatus(result.execution),
|
|
1607
|
+
tokensUsed: Math.max(0, Math.floor(result.execution.totalTokensUsed)),
|
|
1608
|
+
duration: Math.max(0, result.execution.duration),
|
|
1609
|
+
filesChanged: result.execution.filesChanged,
|
|
1610
|
+
pr: result.pr,
|
|
1611
|
+
linkedIssue: result.task.linkedIssue ? {
|
|
1612
|
+
number: result.task.linkedIssue.number,
|
|
1613
|
+
url: result.task.linkedIssue.url
|
|
1614
|
+
} : void 0,
|
|
1615
|
+
error: result.execution.error
|
|
1616
|
+
}));
|
|
1617
|
+
const tasksSucceeded = contributionTasks.filter((task) => task.status !== "failed").length;
|
|
1618
|
+
const tasksFailed = contributionTasks.length - tasksSucceeded;
|
|
1619
|
+
const totalTokensUsed = contributionTasks.reduce((sum, task) => sum + task.tokensUsed, 0);
|
|
1620
|
+
const totalFilesChanged = contributionTasks.reduce(
|
|
1621
|
+
(sum, task) => sum + task.filesChanged.length,
|
|
1622
|
+
0
|
|
1623
|
+
);
|
|
1624
|
+
return {
|
|
1625
|
+
version: "1.0",
|
|
1626
|
+
runId: input2.runId,
|
|
1627
|
+
timestamp,
|
|
1628
|
+
contributor: {
|
|
1629
|
+
githubUsername: contributor,
|
|
1630
|
+
email: process.env.GIT_AUTHOR_EMAIL ?? void 0
|
|
1631
|
+
},
|
|
1632
|
+
repo: {
|
|
1633
|
+
fullName: input2.repoFullName,
|
|
1634
|
+
headSha: input2.repoHeadSha,
|
|
1635
|
+
defaultBranch: input2.defaultBranch
|
|
1636
|
+
},
|
|
1637
|
+
budget: {
|
|
1638
|
+
provider: input2.providerId,
|
|
1639
|
+
totalTokensBudgeted: input2.totalBudget,
|
|
1640
|
+
totalTokensUsed
|
|
1641
|
+
},
|
|
1642
|
+
tasks: contributionTasks,
|
|
1643
|
+
metrics: {
|
|
1644
|
+
tasksDiscovered: input2.discoveredTasks,
|
|
1645
|
+
tasksAttempted: contributionTasks.length,
|
|
1646
|
+
tasksSucceeded,
|
|
1647
|
+
tasksFailed,
|
|
1648
|
+
totalDuration: Math.max(0, input2.runDurationSeconds),
|
|
1649
|
+
totalFilesChanged,
|
|
1650
|
+
totalLinesAdded: 0,
|
|
1651
|
+
totalLinesRemoved: 0
|
|
1652
|
+
}
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
function deriveTaskStatus(execution) {
|
|
1656
|
+
if (execution.success) {
|
|
1657
|
+
return "success";
|
|
1658
|
+
}
|
|
1659
|
+
if (execution.filesChanged.length > 0) {
|
|
1660
|
+
return "partial";
|
|
1661
|
+
}
|
|
1662
|
+
return "failed";
|
|
1663
|
+
}
|
|
1664
|
+
function resolveGithubUsername(fallback) {
|
|
1665
|
+
const candidates = [
|
|
1666
|
+
process.env.GITHUB_USER,
|
|
1667
|
+
process.env.GITHUB_USERNAME,
|
|
1668
|
+
process.env.USER,
|
|
1669
|
+
process.env.LOGNAME,
|
|
1670
|
+
fallback,
|
|
1671
|
+
"oac-user"
|
|
1672
|
+
];
|
|
1673
|
+
for (const candidate of candidates) {
|
|
1674
|
+
const normalized = sanitizeGithubUsername(candidate ?? "");
|
|
1675
|
+
if (normalized) {
|
|
1676
|
+
return normalized;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
return "oac-user";
|
|
1680
|
+
}
|
|
1681
|
+
function sanitizeGithubUsername(value) {
|
|
1682
|
+
const trimmed = value.trim();
|
|
1683
|
+
if (!trimmed) {
|
|
1684
|
+
return null;
|
|
1685
|
+
}
|
|
1686
|
+
const cleaned = trimmed.replace(/[^A-Za-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
1687
|
+
if (cleaned.length === 0 || cleaned.length > 39) {
|
|
1688
|
+
return null;
|
|
1689
|
+
}
|
|
1690
|
+
if (!/^(?!-)[A-Za-z0-9-]+(?<!-)$/.test(cleaned)) {
|
|
1691
|
+
return null;
|
|
1692
|
+
}
|
|
1693
|
+
return cleaned;
|
|
1694
|
+
}
|
|
1695
|
+
function renderSelectedPlanTable(ui, plan, budget) {
|
|
1696
|
+
const table = new Table4({
|
|
1697
|
+
head: ["#", "Task", "Est. Tokens", "Cumulative", "Confidence"]
|
|
1698
|
+
});
|
|
1699
|
+
for (let index = 0; index < plan.selectedTasks.length; index += 1) {
|
|
1700
|
+
const entry = plan.selectedTasks[index];
|
|
1701
|
+
table.push([
|
|
1702
|
+
String(index + 1),
|
|
1703
|
+
truncate2(entry.task.title, 56),
|
|
1704
|
+
formatInteger4(entry.estimate.totalEstimatedTokens),
|
|
1705
|
+
formatInteger4(entry.cumulativeBudgetUsed),
|
|
1706
|
+
entry.estimate.confidence.toFixed(2)
|
|
1707
|
+
]);
|
|
1708
|
+
}
|
|
1709
|
+
if (plan.selectedTasks.length > 0) {
|
|
1710
|
+
console.log(table.toString());
|
|
1711
|
+
} else {
|
|
1712
|
+
console.log(ui.yellow("No tasks selected for execution."));
|
|
1713
|
+
}
|
|
1714
|
+
console.log("");
|
|
1715
|
+
console.log(
|
|
1716
|
+
`Budget used: ${formatInteger4(
|
|
1717
|
+
plan.selectedTasks[plan.selectedTasks.length - 1]?.cumulativeBudgetUsed ?? 0
|
|
1718
|
+
)} / ${formatBudgetDisplay(budget - plan.reserveTokens)} (effective)`
|
|
1719
|
+
);
|
|
1720
|
+
console.log(`Reserve: ${formatBudgetDisplay(plan.reserveTokens)} (10%)`);
|
|
1721
|
+
console.log(`Remaining: ${formatBudgetDisplay(plan.remainingTokens)}`);
|
|
1722
|
+
if (plan.deferredTasks.length > 0) {
|
|
1723
|
+
console.log("");
|
|
1724
|
+
console.log(ui.yellow(`Deferred (${plan.deferredTasks.length}):`));
|
|
1725
|
+
for (const deferred of plan.deferredTasks) {
|
|
1726
|
+
console.log(
|
|
1727
|
+
` - ${truncate2(deferred.task.title, 72)} (${formatInteger4(
|
|
1728
|
+
deferred.estimate.totalEstimatedTokens
|
|
1729
|
+
)} tokens, ${deferred.reason.replaceAll("_", " ")})`
|
|
1730
|
+
);
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
function renderTaskResults(ui, taskResults) {
|
|
1735
|
+
for (let index = 0; index < taskResults.length; index += 1) {
|
|
1736
|
+
const result = taskResults[index];
|
|
1737
|
+
const icon = result.execution.success ? ui.green("[OK]") : ui.red("[X]");
|
|
1738
|
+
const status = result.execution.success ? ui.green("SUCCESS") : ui.red("FAILED");
|
|
1739
|
+
console.log(`${icon} [${index + 1}/${taskResults.length}] ${result.task.title}`);
|
|
1740
|
+
console.log(
|
|
1741
|
+
` ${status} | tokens ${formatInteger4(result.execution.totalTokensUsed)} | duration ${formatDuration(
|
|
1742
|
+
result.execution.duration
|
|
1743
|
+
)}`
|
|
1744
|
+
);
|
|
1745
|
+
if (result.pr) {
|
|
1746
|
+
console.log(` PR #${result.pr.number}: ${result.pr.url}`);
|
|
1747
|
+
}
|
|
1748
|
+
if (result.execution.error) {
|
|
1749
|
+
console.log(` Error: ${result.execution.error}`);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
async function runWithConcurrency(items, concurrency, worker) {
|
|
1754
|
+
if (items.length === 0) {
|
|
1755
|
+
return [];
|
|
1756
|
+
}
|
|
1757
|
+
const results = new Array(items.length);
|
|
1758
|
+
let nextIndex = 0;
|
|
1759
|
+
const runWorker = async () => {
|
|
1760
|
+
while (true) {
|
|
1761
|
+
const currentIndex = nextIndex;
|
|
1762
|
+
nextIndex += 1;
|
|
1763
|
+
if (currentIndex >= items.length) {
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
results[currentIndex] = await worker(items[currentIndex], currentIndex);
|
|
1767
|
+
}
|
|
1768
|
+
};
|
|
1769
|
+
const workerCount = Math.min(concurrency, items.length);
|
|
1770
|
+
await Promise.all(Array.from({ length: workerCount }, () => runWorker()));
|
|
1771
|
+
return results;
|
|
1772
|
+
}
|
|
1773
|
+
function sleep(ms) {
|
|
1774
|
+
return new Promise((resolvePromise) => {
|
|
1775
|
+
setTimeout(resolvePromise, ms);
|
|
1776
|
+
});
|
|
1777
|
+
}
|
|
1778
|
+
function formatInteger4(value) {
|
|
1779
|
+
return new Intl.NumberFormat("en-US").format(value);
|
|
1780
|
+
}
|
|
1781
|
+
function formatDuration(seconds) {
|
|
1782
|
+
if (!Number.isFinite(seconds) || seconds < 0) {
|
|
1783
|
+
return "0s";
|
|
1784
|
+
}
|
|
1785
|
+
if (seconds < 60) {
|
|
1786
|
+
return `${seconds.toFixed(1)}s`;
|
|
1787
|
+
}
|
|
1788
|
+
const minutes = Math.floor(seconds / 60);
|
|
1789
|
+
const remainingSeconds = Math.round(seconds % 60);
|
|
1790
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
1791
|
+
}
|
|
1792
|
+
function truncate2(value, maxLength) {
|
|
1793
|
+
if (value.length <= maxLength) {
|
|
1794
|
+
return value;
|
|
1795
|
+
}
|
|
1796
|
+
return `${value.slice(0, Math.max(0, maxLength - 3))}...`;
|
|
1797
|
+
}
|
|
1798
|
+
async function pathExists3(path) {
|
|
1799
|
+
try {
|
|
1800
|
+
await access3(path, fsConstants3.F_OK);
|
|
1801
|
+
return true;
|
|
1802
|
+
} catch {
|
|
1803
|
+
return false;
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
// src/commands/scan.ts
|
|
1808
|
+
import { constants as fsConstants4 } from "fs";
|
|
1809
|
+
import { access as access4 } from "fs/promises";
|
|
1810
|
+
import { resolve as resolve6 } from "path";
|
|
1811
|
+
import { pathToFileURL as pathToFileURL3 } from "url";
|
|
1812
|
+
import { loadConfig as loadConfig3 } from "@open330/oac-core";
|
|
1813
|
+
import {
|
|
1814
|
+
CompositeScanner as CompositeScanner3,
|
|
1815
|
+
LintScanner as LintScanner3,
|
|
1816
|
+
TodoScanner as TodoScanner3,
|
|
1817
|
+
rankTasks as rankTasks3
|
|
1818
|
+
} from "@open330/oac-discovery";
|
|
1819
|
+
import { cloneRepo as cloneRepo3, resolveRepo as resolveRepo3 } from "@open330/oac-repo";
|
|
1820
|
+
import chalk5, { Chalk as Chalk5 } from "chalk";
|
|
1821
|
+
import Table5 from "cli-table3";
|
|
1822
|
+
import { Command as Command7 } from "commander";
|
|
1823
|
+
import ora3 from "ora";
|
|
1824
|
+
var SUPPORTED_SCANNERS = ["lint", "todo"];
|
|
1825
|
+
function createScanCommand() {
|
|
1826
|
+
const command = new Command7("scan");
|
|
1827
|
+
command.description("Discover tasks in a repository").option("--repo <owner/repo>", "Target repository (owner/repo or GitHub URL)").option("--scanners <names>", "Comma-separated scanner filter (lint,todo)").option("--min-priority <number>", "Minimum priority threshold (0-100)", parseInteger5, 20).option("--format <format>", "Output format: table|json", "table").action(async (options, cmd) => {
|
|
1828
|
+
const globalOptions = getGlobalOptions7(cmd);
|
|
1829
|
+
const ui = createUi5(globalOptions);
|
|
1830
|
+
const outputFormat = normalizeOutputFormat(options.format);
|
|
1831
|
+
const outputJson = globalOptions.json || outputFormat === "json";
|
|
1832
|
+
if (options.minPriority < 0 || options.minPriority > 100) {
|
|
1833
|
+
throw new Error("--min-priority must be between 0 and 100.");
|
|
1834
|
+
}
|
|
1835
|
+
const config = await loadOptionalConfig3(globalOptions.config, globalOptions.verbose, ui);
|
|
1836
|
+
const repoInput = resolveRepoInput3(options.repo, config);
|
|
1837
|
+
const scannerSelection = selectScanners(options.scanners, config);
|
|
1838
|
+
if (!outputJson && scannerSelection.unknown.length > 0) {
|
|
1839
|
+
console.log(
|
|
1840
|
+
ui.yellow(
|
|
1841
|
+
`Ignoring unsupported scanner(s): ${scannerSelection.unknown.join(", ")}. Supported scanners: ${SUPPORTED_SCANNERS.join(", ")}.`
|
|
1842
|
+
)
|
|
1843
|
+
);
|
|
1844
|
+
}
|
|
1845
|
+
const resolveSpinner = createSpinner3(outputJson, "Resolving repository...");
|
|
1846
|
+
const resolvedRepo = await resolveRepo3(repoInput);
|
|
1847
|
+
resolveSpinner?.succeed(`Resolved ${resolvedRepo.fullName}`);
|
|
1848
|
+
const cloneSpinner = createSpinner3(outputJson, "Preparing local clone...");
|
|
1849
|
+
await cloneRepo3(resolvedRepo);
|
|
1850
|
+
cloneSpinner?.succeed(`Repository ready at ${resolvedRepo.localPath}`);
|
|
1851
|
+
const scanSpinner = createSpinner3(
|
|
1852
|
+
outputJson,
|
|
1853
|
+
`Running scanners: ${scannerSelection.enabled.join(", ")}`
|
|
1854
|
+
);
|
|
1855
|
+
const scannedTasks = await scannerSelection.scanner.scan(resolvedRepo.localPath, {
|
|
1856
|
+
exclude: config?.discovery.exclude,
|
|
1857
|
+
maxTasks: config?.discovery.maxTasks,
|
|
1858
|
+
repo: resolvedRepo
|
|
1859
|
+
});
|
|
1860
|
+
scanSpinner?.succeed(`Scanned ${resolvedRepo.fullName}`);
|
|
1861
|
+
const rankedTasks = rankTasks3(scannedTasks).filter(
|
|
1862
|
+
(task) => task.priority >= options.minPriority
|
|
1863
|
+
);
|
|
1864
|
+
if (outputJson) {
|
|
1865
|
+
console.log(
|
|
1866
|
+
JSON.stringify(
|
|
1867
|
+
{
|
|
1868
|
+
repo: resolvedRepo.fullName,
|
|
1869
|
+
scanners: scannerSelection.enabled,
|
|
1870
|
+
minPriority: options.minPriority,
|
|
1871
|
+
totalTasks: rankedTasks.length,
|
|
1872
|
+
tasks: rankedTasks
|
|
1873
|
+
},
|
|
1874
|
+
null,
|
|
1875
|
+
2
|
|
1876
|
+
)
|
|
1877
|
+
);
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
if (rankedTasks.length === 0) {
|
|
1881
|
+
console.log(ui.yellow("No tasks discovered for the selected criteria."));
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
const table = new Table5({
|
|
1885
|
+
head: ["ID", "Title", "Source", "Priority", "Complexity"]
|
|
1886
|
+
});
|
|
1887
|
+
for (const task of rankedTasks) {
|
|
1888
|
+
table.push([
|
|
1889
|
+
task.id,
|
|
1890
|
+
truncate3(task.title, 60),
|
|
1891
|
+
task.source,
|
|
1892
|
+
String(task.priority),
|
|
1893
|
+
task.complexity
|
|
1894
|
+
]);
|
|
1895
|
+
}
|
|
1896
|
+
console.log(table.toString());
|
|
1897
|
+
console.log("");
|
|
1898
|
+
console.log(
|
|
1899
|
+
ui.blue(
|
|
1900
|
+
`Found ${rankedTasks.length} task(s). Use \`oac plan --repo ${resolvedRepo.fullName} --tokens <n>\` to build an execution plan.`
|
|
1901
|
+
)
|
|
1902
|
+
);
|
|
1903
|
+
});
|
|
1904
|
+
return command;
|
|
1905
|
+
}
|
|
1906
|
+
function getGlobalOptions7(command) {
|
|
1907
|
+
const options = command.optsWithGlobals();
|
|
1908
|
+
return {
|
|
1909
|
+
config: options.config ?? "oac.config.ts",
|
|
1910
|
+
verbose: options.verbose === true,
|
|
1911
|
+
json: options.json === true,
|
|
1912
|
+
color: options.color !== false
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
function createUi5(options) {
|
|
1916
|
+
const noColorEnv = Object.prototype.hasOwnProperty.call(process.env, "NO_COLOR");
|
|
1917
|
+
const colorEnabled = options.color && !noColorEnv;
|
|
1918
|
+
return new Chalk5({ level: colorEnabled ? chalk5.level : 0 });
|
|
1919
|
+
}
|
|
1920
|
+
function createSpinner3(enabled, text) {
|
|
1921
|
+
if (enabled) {
|
|
1922
|
+
return null;
|
|
1923
|
+
}
|
|
1924
|
+
return ora3({ text, color: "blue" }).start();
|
|
1925
|
+
}
|
|
1926
|
+
function parseInteger5(value) {
|
|
1927
|
+
const parsed = Number.parseInt(value, 10);
|
|
1928
|
+
if (!Number.isFinite(parsed)) {
|
|
1929
|
+
throw new Error(`Expected an integer but received "${value}".`);
|
|
1930
|
+
}
|
|
1931
|
+
return parsed;
|
|
1932
|
+
}
|
|
1933
|
+
function normalizeOutputFormat(value) {
|
|
1934
|
+
const normalized = value.trim().toLowerCase();
|
|
1935
|
+
if (normalized === "table" || normalized === "json") {
|
|
1936
|
+
return normalized;
|
|
1937
|
+
}
|
|
1938
|
+
throw new Error(`Unsupported --format value "${value}". Use "table" or "json".`);
|
|
1939
|
+
}
|
|
1940
|
+
async function loadOptionalConfig3(configPath, verbose, ui) {
|
|
1941
|
+
const absolutePath = resolve6(process.cwd(), configPath);
|
|
1942
|
+
if (!await pathExists4(absolutePath)) {
|
|
1943
|
+
return null;
|
|
1944
|
+
}
|
|
1945
|
+
try {
|
|
1946
|
+
const imported = await import(`${pathToFileURL3(absolutePath).href}?t=${Date.now()}`);
|
|
1947
|
+
const candidate = imported.default ?? imported.config ?? imported;
|
|
1948
|
+
return loadConfig3(candidate);
|
|
1949
|
+
} catch (error) {
|
|
1950
|
+
if (verbose) {
|
|
1951
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1952
|
+
console.warn(ui.yellow(`[oac] Failed to load config at ${configPath}: ${message}`));
|
|
1953
|
+
}
|
|
1954
|
+
return null;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
function resolveRepoInput3(repoOption, config) {
|
|
1958
|
+
const fromFlag = repoOption?.trim();
|
|
1959
|
+
if (fromFlag) {
|
|
1960
|
+
return fromFlag;
|
|
1961
|
+
}
|
|
1962
|
+
const firstConfiguredRepo = config?.repos[0];
|
|
1963
|
+
if (typeof firstConfiguredRepo === "string") {
|
|
1964
|
+
return firstConfiguredRepo;
|
|
1965
|
+
}
|
|
1966
|
+
if (firstConfiguredRepo && typeof firstConfiguredRepo === "object" && "name" in firstConfiguredRepo && typeof firstConfiguredRepo.name === "string") {
|
|
1967
|
+
return firstConfiguredRepo.name;
|
|
1968
|
+
}
|
|
1969
|
+
throw new Error("No repository specified. Use --repo or configure repos in oac.config.ts.");
|
|
1970
|
+
}
|
|
1971
|
+
function selectScanners(scannerOption, config) {
|
|
1972
|
+
const requested = scannerOption ? parseCsv(scannerOption) : scannersFromConfig(config) ?? [...SUPPORTED_SCANNERS];
|
|
1973
|
+
const enabled = [];
|
|
1974
|
+
const unknown = [];
|
|
1975
|
+
for (const scannerName of requested) {
|
|
1976
|
+
const normalized = scannerName.toLowerCase();
|
|
1977
|
+
if (normalized === "lint" || normalized === "todo") {
|
|
1978
|
+
enabled.push(normalized);
|
|
1979
|
+
} else {
|
|
1980
|
+
unknown.push(scannerName);
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
const uniqueEnabled = [...new Set(enabled)];
|
|
1984
|
+
if (uniqueEnabled.length === 0) {
|
|
1985
|
+
throw new Error(
|
|
1986
|
+
`No supported scanners selected. Supported scanners: ${SUPPORTED_SCANNERS.join(", ")}.`
|
|
1987
|
+
);
|
|
1988
|
+
}
|
|
1989
|
+
const scannerInstances = uniqueEnabled.map(
|
|
1990
|
+
(name) => name === "lint" ? new LintScanner3() : new TodoScanner3()
|
|
1991
|
+
);
|
|
1992
|
+
return {
|
|
1993
|
+
enabled: uniqueEnabled,
|
|
1994
|
+
unknown,
|
|
1995
|
+
scanner: new CompositeScanner3(scannerInstances)
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
function scannersFromConfig(config) {
|
|
1999
|
+
if (!config) {
|
|
2000
|
+
return null;
|
|
2001
|
+
}
|
|
2002
|
+
const configured = [];
|
|
2003
|
+
if (config.discovery.scanners.lint) {
|
|
2004
|
+
configured.push("lint");
|
|
2005
|
+
}
|
|
2006
|
+
if (config.discovery.scanners.todo) {
|
|
2007
|
+
configured.push("todo");
|
|
2008
|
+
}
|
|
2009
|
+
if (configured.length === 0) {
|
|
2010
|
+
return null;
|
|
2011
|
+
}
|
|
2012
|
+
return configured;
|
|
2013
|
+
}
|
|
2014
|
+
function parseCsv(value) {
|
|
2015
|
+
return value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
2016
|
+
}
|
|
2017
|
+
function truncate3(value, maxLength) {
|
|
2018
|
+
if (value.length <= maxLength) {
|
|
2019
|
+
return value;
|
|
2020
|
+
}
|
|
2021
|
+
return `${value.slice(0, Math.max(0, maxLength - 3))}...`;
|
|
2022
|
+
}
|
|
2023
|
+
async function pathExists4(path) {
|
|
2024
|
+
try {
|
|
2025
|
+
await access4(path, fsConstants4.F_OK);
|
|
2026
|
+
return true;
|
|
2027
|
+
} catch {
|
|
2028
|
+
return false;
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
// src/commands/status.ts
|
|
2033
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
2034
|
+
import { resolve as resolve7 } from "path";
|
|
2035
|
+
import { Command as Command8 } from "commander";
|
|
2036
|
+
var WATCH_INTERVAL_MS = 2e3;
|
|
2037
|
+
function createStatusCommand() {
|
|
2038
|
+
const command = new Command8("status");
|
|
2039
|
+
command.description("Show current job status").option("--watch", "Poll every 2 seconds", false).action(async (options, cmd) => {
|
|
2040
|
+
const globalOptions = getGlobalOptions8(cmd);
|
|
2041
|
+
const render = async () => {
|
|
2042
|
+
const status = await readRunStatus(process.cwd());
|
|
2043
|
+
renderStatusOutput(status, globalOptions.json);
|
|
2044
|
+
};
|
|
2045
|
+
await render();
|
|
2046
|
+
if (!options.watch) {
|
|
2047
|
+
return;
|
|
2048
|
+
}
|
|
2049
|
+
setInterval(() => {
|
|
2050
|
+
console.clear();
|
|
2051
|
+
void render().catch((error) => {
|
|
2052
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2053
|
+
console.error(message);
|
|
2054
|
+
process.exitCode = 1;
|
|
2055
|
+
});
|
|
2056
|
+
}, WATCH_INTERVAL_MS);
|
|
2057
|
+
});
|
|
2058
|
+
return command;
|
|
2059
|
+
}
|
|
2060
|
+
function getGlobalOptions8(command) {
|
|
2061
|
+
const options = command.optsWithGlobals();
|
|
2062
|
+
return {
|
|
2063
|
+
config: options.config ?? "oac.config.ts",
|
|
2064
|
+
verbose: options.verbose === true,
|
|
2065
|
+
json: options.json === true,
|
|
2066
|
+
color: options.color !== false
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
async function readRunStatus(repoPath) {
|
|
2070
|
+
const statusPath = resolve7(repoPath, ".oac", "status.json");
|
|
2071
|
+
try {
|
|
2072
|
+
const raw = await readFile3(statusPath, "utf8");
|
|
2073
|
+
const payload = JSON.parse(raw);
|
|
2074
|
+
return parseRunStatus(payload);
|
|
2075
|
+
} catch (error) {
|
|
2076
|
+
if (isFileNotFoundError3(error)) {
|
|
2077
|
+
return null;
|
|
2078
|
+
}
|
|
2079
|
+
throw error;
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
function parseRunStatus(payload) {
|
|
2083
|
+
if (!isRecord2(payload)) {
|
|
2084
|
+
throw new Error("Invalid .oac/status.json format.");
|
|
2085
|
+
}
|
|
2086
|
+
const runId = payload.runId;
|
|
2087
|
+
const startedAt = payload.startedAt;
|
|
2088
|
+
const agent = payload.agent;
|
|
2089
|
+
const tasks = payload.tasks;
|
|
2090
|
+
if (typeof runId !== "string" || typeof startedAt !== "string" || typeof agent !== "string" || !Array.isArray(tasks)) {
|
|
2091
|
+
throw new Error("Invalid .oac/status.json format.");
|
|
2092
|
+
}
|
|
2093
|
+
return {
|
|
2094
|
+
runId,
|
|
2095
|
+
startedAt,
|
|
2096
|
+
agent,
|
|
2097
|
+
tasks: tasks.map((task, index) => parseRunStatusTask(task, index))
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
function parseRunStatusTask(task, index) {
|
|
2101
|
+
if (!isRecord2(task)) {
|
|
2102
|
+
throw new Error(`Invalid task at index ${String(index)} in .oac/status.json.`);
|
|
2103
|
+
}
|
|
2104
|
+
const taskId = task.taskId;
|
|
2105
|
+
const title = task.title;
|
|
2106
|
+
const status = task.status;
|
|
2107
|
+
const startedAt = task.startedAt;
|
|
2108
|
+
const completedAt = task.completedAt;
|
|
2109
|
+
const error = task.error;
|
|
2110
|
+
if (typeof taskId !== "string" || typeof title !== "string" || status !== "pending" && status !== "running" && status !== "completed" && status !== "failed") {
|
|
2111
|
+
throw new Error(`Invalid task at index ${String(index)} in .oac/status.json.`);
|
|
2112
|
+
}
|
|
2113
|
+
if (startedAt !== void 0 && typeof startedAt !== "string") {
|
|
2114
|
+
throw new Error(`Invalid task at index ${String(index)} in .oac/status.json.`);
|
|
2115
|
+
}
|
|
2116
|
+
if (completedAt !== void 0 && typeof completedAt !== "string") {
|
|
2117
|
+
throw new Error(`Invalid task at index ${String(index)} in .oac/status.json.`);
|
|
2118
|
+
}
|
|
2119
|
+
if (error !== void 0 && typeof error !== "string") {
|
|
2120
|
+
throw new Error(`Invalid task at index ${String(index)} in .oac/status.json.`);
|
|
2121
|
+
}
|
|
2122
|
+
return {
|
|
2123
|
+
taskId,
|
|
2124
|
+
title,
|
|
2125
|
+
status,
|
|
2126
|
+
startedAt,
|
|
2127
|
+
completedAt,
|
|
2128
|
+
error
|
|
2129
|
+
};
|
|
2130
|
+
}
|
|
2131
|
+
function renderStatusOutput(status, outputJson) {
|
|
2132
|
+
if (outputJson) {
|
|
2133
|
+
if (!status) {
|
|
2134
|
+
console.log(
|
|
2135
|
+
JSON.stringify(
|
|
2136
|
+
{
|
|
2137
|
+
active: false,
|
|
2138
|
+
message: "No active runs"
|
|
2139
|
+
},
|
|
2140
|
+
null,
|
|
2141
|
+
2
|
|
2142
|
+
)
|
|
2143
|
+
);
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
2146
|
+
console.log(
|
|
2147
|
+
JSON.stringify(
|
|
2148
|
+
{
|
|
2149
|
+
active: true,
|
|
2150
|
+
status
|
|
2151
|
+
},
|
|
2152
|
+
null,
|
|
2153
|
+
2
|
|
2154
|
+
)
|
|
2155
|
+
);
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2158
|
+
if (!status) {
|
|
2159
|
+
console.log("No active runs");
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
2162
|
+
const runningTasks = status.tasks.filter((task) => task.status === "running");
|
|
2163
|
+
const completedTasks = status.tasks.filter((task) => task.status === "completed");
|
|
2164
|
+
const failedTasks = status.tasks.filter((task) => task.status === "failed");
|
|
2165
|
+
console.log(`Run ID: ${status.runId}`);
|
|
2166
|
+
console.log(`Start Time: ${status.startedAt}`);
|
|
2167
|
+
console.log(`Agent: ${status.agent}`);
|
|
2168
|
+
console.log(
|
|
2169
|
+
`Tasks In Progress (${String(runningTasks.length)}): ${formatTaskList(runningTasks)}`
|
|
2170
|
+
);
|
|
2171
|
+
console.log(
|
|
2172
|
+
`Completed Tasks (${String(completedTasks.length)}): ${formatTaskList(completedTasks)}`
|
|
2173
|
+
);
|
|
2174
|
+
if (failedTasks.length === 0) {
|
|
2175
|
+
console.log("Errors: none");
|
|
2176
|
+
return;
|
|
2177
|
+
}
|
|
2178
|
+
console.log(`Errors (${String(failedTasks.length)}):`);
|
|
2179
|
+
for (const task of failedTasks) {
|
|
2180
|
+
console.log(`- ${task.taskId}: ${task.error ?? "Unknown error"}`);
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
function formatTaskList(tasks) {
|
|
2184
|
+
if (tasks.length === 0) {
|
|
2185
|
+
return "-";
|
|
2186
|
+
}
|
|
2187
|
+
return tasks.map((task) => `${task.taskId} (${task.title})`).join(", ");
|
|
2188
|
+
}
|
|
2189
|
+
function isRecord2(value) {
|
|
2190
|
+
return typeof value === "object" && value !== null;
|
|
2191
|
+
}
|
|
2192
|
+
function isFileNotFoundError3(error) {
|
|
2193
|
+
if (!isRecord2(error)) {
|
|
2194
|
+
return false;
|
|
2195
|
+
}
|
|
2196
|
+
return error.code === "ENOENT";
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
// src/cli.ts
|
|
2200
|
+
async function readCliVersion() {
|
|
2201
|
+
try {
|
|
2202
|
+
const packageJsonPath = new URL("../package.json", import.meta.url);
|
|
2203
|
+
const packageJsonRaw = await readFile4(packageJsonPath, "utf8");
|
|
2204
|
+
const packageJson = JSON.parse(packageJsonRaw);
|
|
2205
|
+
if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
|
|
2206
|
+
return packageJson.version;
|
|
2207
|
+
}
|
|
2208
|
+
} catch {
|
|
2209
|
+
}
|
|
2210
|
+
return "0.0.0";
|
|
2211
|
+
}
|
|
2212
|
+
function registerCommands(program) {
|
|
2213
|
+
program.addCommand(createInitCommand());
|
|
2214
|
+
program.addCommand(createDoctorCommand());
|
|
2215
|
+
program.addCommand(createScanCommand());
|
|
2216
|
+
program.addCommand(createPlanCommand());
|
|
2217
|
+
program.addCommand(createRunCommand());
|
|
2218
|
+
program.addCommand(createLogCommand());
|
|
2219
|
+
program.addCommand(createLeaderboardCommand());
|
|
2220
|
+
program.addCommand(createStatusCommand());
|
|
2221
|
+
}
|
|
2222
|
+
async function createCliProgram() {
|
|
2223
|
+
const version = await readCliVersion();
|
|
2224
|
+
const program = new Command9();
|
|
2225
|
+
program.name("oac").description("Open Agent Contribution CLI").version(version).option("--config <path>", "Config file path", "oac.config.ts").option("--verbose", "Enable verbose logging", false).option("--json", "Output machine-readable JSON", false).option("--no-color", "Disable ANSI colors");
|
|
2226
|
+
registerCommands(program);
|
|
2227
|
+
return program;
|
|
2228
|
+
}
|
|
2229
|
+
async function runCli(argv = process.argv) {
|
|
2230
|
+
const program = await createCliProgram();
|
|
2231
|
+
await program.parseAsync([...argv]);
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
export {
|
|
2235
|
+
createCliProgram,
|
|
2236
|
+
runCli
|
|
2237
|
+
};
|
|
2238
|
+
//# sourceMappingURL=chunk-UVK4T7KV.js.map
|