@inteeka/task-cli 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +543 -28
- package/dist/cli.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -191,6 +191,9 @@ var CLI_AUDIT_ACTIONS = Object.freeze([
|
|
|
191
191
|
"cli.run.tests_failed",
|
|
192
192
|
"cli.run.pr_opened",
|
|
193
193
|
"cli.run.pr_failed",
|
|
194
|
+
"cli.run.push_failed",
|
|
195
|
+
"cli.run.resumed",
|
|
196
|
+
"cli.work.pr_recovered",
|
|
194
197
|
"cli.schedule.created",
|
|
195
198
|
"cli.schedule.paused",
|
|
196
199
|
"cli.schedule.resumed",
|
|
@@ -224,6 +227,13 @@ var c = {
|
|
|
224
227
|
var CliError = class extends Error {
|
|
225
228
|
code;
|
|
226
229
|
hint;
|
|
230
|
+
/**
|
|
231
|
+
* Discriminator for the multi-work loop's between-iteration cleanup.
|
|
232
|
+
* `post_push` means the per-ticket branch is on origin (with a commit)
|
|
233
|
+
* and should be preserved for `task resume`. Pre-push failures (or
|
|
234
|
+
* unset / 'pre_push') let the loop purge the branch as usual.
|
|
235
|
+
*/
|
|
236
|
+
phase;
|
|
227
237
|
constructor(code, message, hint) {
|
|
228
238
|
super(message);
|
|
229
239
|
this.code = code;
|
|
@@ -1374,6 +1384,9 @@ function currentBranch(cwd) {
|
|
|
1374
1384
|
}
|
|
1375
1385
|
}
|
|
1376
1386
|
|
|
1387
|
+
// src/commands/work.ts
|
|
1388
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
1389
|
+
|
|
1377
1390
|
// src/git/branch.ts
|
|
1378
1391
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
1379
1392
|
var VALID_BRANCH = /^[A-Za-z0-9._/-]{1,200}$/;
|
|
@@ -1636,7 +1649,13 @@ async function buildWorkContext(opts) {
|
|
|
1636
1649
|
};
|
|
1637
1650
|
}
|
|
1638
1651
|
function registerWork(program2) {
|
|
1639
|
-
program2.command("work [ticketId]").description("Run the agent on a CLI-approved ticket \u2014 cuts a per-ticket branch and opens a PR").option("--auto", "Pick the next eligible ticket without prompting").option("--next", "Alias for --auto --max 1").option("--dry-run", "Run the agent + tests but do not commit, push, or open a PR").option("--no-push", "[deprecated in Phase 2 \u2014 task work always pushes via per-ticket branch]").option("--max <n>", "Process up to N tickets in this invocation", "1").option("--silent", "Suppress TTY output (used by scheduled tasks)").option(
|
|
1652
|
+
program2.command("work [ticketId]").description("Run the agent on a CLI-approved ticket \u2014 cuts a per-ticket branch and opens a PR").option("--auto", "Pick the next eligible ticket without prompting").option("--next", "Alias for --auto --max 1").option("--dry-run", "Run the agent + tests but do not commit, push, or open a PR").option("--no-push", "[deprecated in Phase 2 \u2014 task work always pushes via per-ticket branch]").option("--max <n>", "Process up to N tickets in this invocation", "1").option("--silent", "Suppress TTY output (used by scheduled tasks)").option(
|
|
1653
|
+
"--reset",
|
|
1654
|
+
"DESTRUCTIVE: discard all local working-tree changes before running. Requires interactive y/n; pair with --confirm in non-TTY contexts."
|
|
1655
|
+
).option(
|
|
1656
|
+
"--confirm",
|
|
1657
|
+
"Confirm --reset in non-TTY (silent / scheduled-task) contexts. Has no effect without --reset."
|
|
1658
|
+
).option("--schedule-id <id>", "Internal: schedule id when invoked from a scheduled task").action(async (ticketId, opts) => {
|
|
1640
1659
|
await runWork(ticketId, opts);
|
|
1641
1660
|
});
|
|
1642
1661
|
}
|
|
@@ -1668,6 +1687,9 @@ async function runWork(ticketId, opts) {
|
|
|
1668
1687
|
}
|
|
1669
1688
|
async function processOneTicket(ctx, opts, ticketIdHint) {
|
|
1670
1689
|
const { cwd, baseBranch, silent } = ctx;
|
|
1690
|
+
if (opts.reset) {
|
|
1691
|
+
await purgeWorkingTreeWithConsent(ctx, opts);
|
|
1692
|
+
}
|
|
1671
1693
|
try {
|
|
1672
1694
|
assertBaseBranch(cwd, baseBranch);
|
|
1673
1695
|
} catch (err) {
|
|
@@ -1973,11 +1995,12 @@ Claude session: ${runId}
|
|
|
1973
1995
|
body: {
|
|
1974
1996
|
ticket_id: detail.id,
|
|
1975
1997
|
schedule_id: opts.scheduleId,
|
|
1976
|
-
event: "
|
|
1998
|
+
event: "push_failed",
|
|
1977
1999
|
claude_session_id: runId,
|
|
1978
2000
|
output_excerpt: err.message.slice(0, 4e3)
|
|
1979
2001
|
}
|
|
1980
2002
|
});
|
|
2003
|
+
if (err instanceof CliError) err.phase = "post_push";
|
|
1981
2004
|
throw err;
|
|
1982
2005
|
}
|
|
1983
2006
|
if (!silent)
|
|
@@ -2024,6 +2047,7 @@ Claude session: ${runId}
|
|
|
2024
2047
|
output_excerpt: err.message.slice(0, 4e3)
|
|
2025
2048
|
}
|
|
2026
2049
|
});
|
|
2050
|
+
if (err instanceof CliError) err.phase = "post_push";
|
|
2027
2051
|
throw err;
|
|
2028
2052
|
}
|
|
2029
2053
|
try {
|
|
@@ -2063,6 +2087,79 @@ function buildPrBody(args) {
|
|
|
2063
2087
|
"Please review carefully \u2014 this is an AI-generated change."
|
|
2064
2088
|
].filter(Boolean).join("\n");
|
|
2065
2089
|
}
|
|
2090
|
+
async function purgeWorkingTreeWithConsent(ctx, opts) {
|
|
2091
|
+
const { cwd, baseBranch, silent } = ctx;
|
|
2092
|
+
const isTty = !silent && process.stdin.isTTY === true;
|
|
2093
|
+
const status = (() => {
|
|
2094
|
+
try {
|
|
2095
|
+
return execFileSync6("git", ["status", "--short"], { cwd, encoding: "utf8" }).trim();
|
|
2096
|
+
} catch {
|
|
2097
|
+
return "";
|
|
2098
|
+
}
|
|
2099
|
+
})();
|
|
2100
|
+
const onBranch = (() => {
|
|
2101
|
+
try {
|
|
2102
|
+
return currentBranch(cwd);
|
|
2103
|
+
} catch {
|
|
2104
|
+
return "(unknown)";
|
|
2105
|
+
}
|
|
2106
|
+
})();
|
|
2107
|
+
const willSwitchBranch = onBranch !== baseBranch && onBranch !== "(unknown)";
|
|
2108
|
+
if (!isTty) {
|
|
2109
|
+
if (!opts.confirm) {
|
|
2110
|
+
throw new CliError(
|
|
2111
|
+
CLI_EXIT_CODES.MISCONFIGURATION,
|
|
2112
|
+
"--reset requires --confirm in non-interactive (silent / scheduled-task) contexts",
|
|
2113
|
+
"Re-run with both flags only after confirming the destructive purge is intentional."
|
|
2114
|
+
);
|
|
2115
|
+
}
|
|
2116
|
+
if (!silent) {
|
|
2117
|
+
process.stdout.write(c.dim(" --reset --confirm: discarding working-tree changes\n"));
|
|
2118
|
+
if (willSwitchBranch) {
|
|
2119
|
+
process.stdout.write(
|
|
2120
|
+
c.dim(` --reset --confirm: also switching from "${onBranch}" to "${baseBranch}"
|
|
2121
|
+
`)
|
|
2122
|
+
);
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
} else {
|
|
2126
|
+
if (!silent) {
|
|
2127
|
+
process.stdout.write(`${c.bold("Current branch:")} ${c.cyan(onBranch)}
|
|
2128
|
+
`);
|
|
2129
|
+
process.stdout.write(`${c.bold("Working tree changes:")}
|
|
2130
|
+
`);
|
|
2131
|
+
process.stdout.write(status.length > 0 ? `${c.dim(status)}
|
|
2132
|
+
` : c.dim(" (none)\n"));
|
|
2133
|
+
process.stdout.write(`
|
|
2134
|
+
${c.bold("--reset will:")}
|
|
2135
|
+
`);
|
|
2136
|
+
process.stdout.write(
|
|
2137
|
+
c.dim(" \u2022 discard ALL working-tree changes above (git restore --staged --worktree .)\n")
|
|
2138
|
+
);
|
|
2139
|
+
process.stdout.write(c.dim(" \u2022 delete untracked files + directories (git clean -fd)\n"));
|
|
2140
|
+
if (willSwitchBranch) {
|
|
2141
|
+
process.stdout.write(
|
|
2142
|
+
c.dim(` \u2022 switch from ${c.cyan(onBranch)} ${c.dim("to")} ${c.cyan(baseBranch)}
|
|
2143
|
+
`)
|
|
2144
|
+
);
|
|
2145
|
+
} else {
|
|
2146
|
+
process.stdout.write(
|
|
2147
|
+
c.dim(` \u2022 stay on ${c.cyan(baseBranch)} ${c.dim("(already on base)")}
|
|
2148
|
+
`)
|
|
2149
|
+
);
|
|
2150
|
+
}
|
|
2151
|
+
process.stdout.write("\n");
|
|
2152
|
+
}
|
|
2153
|
+
const answer = await inquirer2.prompt([
|
|
2154
|
+
{ type: "confirm", name: "confirm", message: "Proceed?", default: false }
|
|
2155
|
+
]);
|
|
2156
|
+
if (!answer.confirm) {
|
|
2157
|
+
if (!silent) process.stdout.write(c.dim("Aborted \u2014 working tree untouched.\n"));
|
|
2158
|
+
process.exit(CLI_EXIT_CODES.SUCCESS);
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
enforceBaseBranchClean(cwd, baseBranch);
|
|
2162
|
+
}
|
|
2066
2163
|
async function pickNextEligible(projectId) {
|
|
2067
2164
|
const result = await apiCall("GET", "/api/v1/cli/me/tickets", {
|
|
2068
2165
|
query: { project_id: projectId, limit: 1 }
|
|
@@ -2109,20 +2206,26 @@ function registerMultiWork(program2) {
|
|
|
2109
2206
|
).option("--max <n>", "Process up to N tickets in this batch", "10").option("--dry-run", "Run the agent + tests but do not commit, push, or open PRs").option("--silent", "Suppress TTY output (used by scheduled tasks)").option(
|
|
2110
2207
|
"--abort-on-failure",
|
|
2111
2208
|
"Stop the batch on the first per-ticket failure (default: skip and continue)"
|
|
2112
|
-
).option(
|
|
2209
|
+
).option(
|
|
2210
|
+
"--reset",
|
|
2211
|
+
"DESTRUCTIVE: discard local working-tree changes before the first ticket. Requires --confirm in non-TTY contexts."
|
|
2212
|
+
).option("--confirm", "Confirm --reset in non-TTY (silent / scheduled-task) contexts.").option("--schedule-id <id>", "Internal: schedule id when invoked from a scheduled task").action(async (opts) => {
|
|
2113
2213
|
await runMultiWork(opts);
|
|
2114
2214
|
});
|
|
2115
2215
|
}
|
|
2116
2216
|
async function runMultiWork(opts) {
|
|
2117
2217
|
const ctx = await buildWorkContext({ max: opts.max, silent: opts.silent });
|
|
2118
2218
|
const max = Math.max(1, parseInt(opts.max, 10) || 10);
|
|
2219
|
+
let firstIteration = true;
|
|
2119
2220
|
const innerOpts = {
|
|
2120
2221
|
auto: true,
|
|
2121
2222
|
next: false,
|
|
2122
2223
|
dryRun: opts.dryRun,
|
|
2123
2224
|
max: "1",
|
|
2124
2225
|
silent: opts.silent,
|
|
2125
|
-
scheduleId: opts.scheduleId
|
|
2226
|
+
scheduleId: opts.scheduleId,
|
|
2227
|
+
reset: opts.reset,
|
|
2228
|
+
confirm: opts.confirm
|
|
2126
2229
|
};
|
|
2127
2230
|
const results = [];
|
|
2128
2231
|
let processed = 0;
|
|
@@ -2137,11 +2240,28 @@ async function runMultiWork(opts) {
|
|
|
2137
2240
|
let caughtError = null;
|
|
2138
2241
|
try {
|
|
2139
2242
|
outcome = await processOneTicket(ctx, innerOpts, null);
|
|
2243
|
+
if (firstIteration) {
|
|
2244
|
+
firstIteration = false;
|
|
2245
|
+
innerOpts.reset = false;
|
|
2246
|
+
innerOpts.confirm = false;
|
|
2247
|
+
}
|
|
2140
2248
|
} catch (err) {
|
|
2249
|
+
if (firstIteration) {
|
|
2250
|
+
firstIteration = false;
|
|
2251
|
+
innerOpts.reset = false;
|
|
2252
|
+
innerOpts.confirm = false;
|
|
2253
|
+
}
|
|
2141
2254
|
caughtError = err instanceof Error ? err.message : String(err);
|
|
2255
|
+
const phaseTag = err instanceof CliError && err.phase === "post_push" ? "post_push" : "pre_push";
|
|
2142
2256
|
if (!ctx.silent) {
|
|
2143
2257
|
process.stderr.write(`${c.err("\u2717 Ticket failed")}: ${caughtError}
|
|
2144
2258
|
`);
|
|
2259
|
+
if (phaseTag === "post_push") {
|
|
2260
|
+
process.stderr.write(
|
|
2261
|
+
`${c.dim(" Branch kept on disk + remote \u2014 run `task resume` to retry the PR.")}
|
|
2262
|
+
`
|
|
2263
|
+
);
|
|
2264
|
+
}
|
|
2145
2265
|
}
|
|
2146
2266
|
if (opts.abortOnFailure) {
|
|
2147
2267
|
try {
|
|
@@ -2224,6 +2344,248 @@ ${c.bold("Batch summary")}
|
|
|
2224
2344
|
);
|
|
2225
2345
|
}
|
|
2226
2346
|
|
|
2347
|
+
// src/commands/resume.ts
|
|
2348
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
2349
|
+
import inquirer3 from "inquirer";
|
|
2350
|
+
function registerResume(program2) {
|
|
2351
|
+
program2.command("resume [ticketRef]").description(
|
|
2352
|
+
"Resume a ticket whose previous `task work` run failed after the per-ticket branch was pushed (i.e. ai_fix_status='building'). Re-pushes the branch and re-attempts the PR \u2014 idempotent end-to-end."
|
|
2353
|
+
).option("--silent", "Suppress TTY output").action(async (ticketRef, opts) => {
|
|
2354
|
+
await runResume(ticketRef, opts);
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
async function runResume(ticketRef, opts) {
|
|
2358
|
+
const cwd = findRepoRoot();
|
|
2359
|
+
const project = await readProjectConfig(cwd);
|
|
2360
|
+
if (!project) {
|
|
2361
|
+
throw new CliError(
|
|
2362
|
+
CLI_EXIT_CODES.MISCONFIGURATION,
|
|
2363
|
+
"No project link in this repo",
|
|
2364
|
+
"Run 'task link' first."
|
|
2365
|
+
);
|
|
2366
|
+
}
|
|
2367
|
+
const baseBranch = project.cli_base_branch ?? "development";
|
|
2368
|
+
const silent = !!opts.silent;
|
|
2369
|
+
assertBaseBranch(cwd, baseBranch);
|
|
2370
|
+
const access2 = await apiCallOrThrow("GET", "/api/v1/cli/access");
|
|
2371
|
+
const ticketId = await resolveTicketId(project, ticketRef, silent);
|
|
2372
|
+
if (!ticketId) {
|
|
2373
|
+
if (!silent) {
|
|
2374
|
+
process.stdout.write(c.dim("No in-flight tickets to resume.\n"));
|
|
2375
|
+
}
|
|
2376
|
+
return;
|
|
2377
|
+
}
|
|
2378
|
+
const detail = await apiCallOrThrow(
|
|
2379
|
+
"GET",
|
|
2380
|
+
`/api/v1/cli/me/tickets/${ticketId}`
|
|
2381
|
+
);
|
|
2382
|
+
if (detail.project_id !== project.project_id) {
|
|
2383
|
+
throw new CliError(
|
|
2384
|
+
CLI_EXIT_CODES.MISCONFIGURATION,
|
|
2385
|
+
`Ticket #${detail.sequence_number} belongs to a different project than this repo's link`,
|
|
2386
|
+
"cd to the project repo this ticket belongs to (or re-link with `task link`) and retry."
|
|
2387
|
+
);
|
|
2388
|
+
}
|
|
2389
|
+
if (detail.ai_fix_status !== "building") {
|
|
2390
|
+
throw new CliError(
|
|
2391
|
+
CLI_EXIT_CODES.GENERIC_ERROR,
|
|
2392
|
+
`Ticket #${detail.sequence_number} is in ai_fix_status='${detail.ai_fix_status}', not 'building' \u2014 nothing to resume`,
|
|
2393
|
+
"A previous resume attempt may already have succeeded. Check the dashboard for the ticket's PR."
|
|
2394
|
+
);
|
|
2395
|
+
}
|
|
2396
|
+
if (detail.claimed_by_user_id && detail.claimed_by_user_id !== access2.user_id) {
|
|
2397
|
+
throw new CliError(
|
|
2398
|
+
CLI_EXIT_CODES.UNAUTHORISED,
|
|
2399
|
+
`Ticket #${detail.sequence_number} is claimed by another user`,
|
|
2400
|
+
'Ask that user to resume it, or have an admin reset ai_fix_status to "approved" so the ticket can be re-claimed fresh.'
|
|
2401
|
+
);
|
|
2402
|
+
}
|
|
2403
|
+
const branchName = branchSlug(detail.sequence_number, detail.title);
|
|
2404
|
+
const ticketBaseBranch = detail.project_cli_base_branch || baseBranch;
|
|
2405
|
+
if (!localBranchExists(cwd, branchName)) {
|
|
2406
|
+
throw new CliError(
|
|
2407
|
+
CLI_EXIT_CODES.GENERIC_ERROR,
|
|
2408
|
+
`Local branch '${branchName}' is missing \u2014 cannot resume`,
|
|
2409
|
+
'The branch was deleted (or never existed locally). Ask an admin to set ai_fix_status back to "approved" so `task work` can produce a fresh fix.'
|
|
2410
|
+
);
|
|
2411
|
+
}
|
|
2412
|
+
if (!isAncestor(cwd, ticketBaseBranch, branchName)) {
|
|
2413
|
+
throw new CliError(
|
|
2414
|
+
CLI_EXIT_CODES.GENERIC_ERROR,
|
|
2415
|
+
`Base branch '${ticketBaseBranch}' is no longer an ancestor of '${branchName}' \u2014 cannot resume`,
|
|
2416
|
+
'The base branch has moved (rebase/force-push). Have an admin reset ai_fix_status to "approved" and re-run `task work` for a fresh branch.'
|
|
2417
|
+
);
|
|
2418
|
+
}
|
|
2419
|
+
if (!silent) {
|
|
2420
|
+
process.stdout.write(`
|
|
2421
|
+
${c.bold(`Resuming #${detail.sequence_number}: ${detail.title}`)}
|
|
2422
|
+
`);
|
|
2423
|
+
process.stdout.write(c.dim(` branch: ${branchName} \u2192 ${ticketBaseBranch}
|
|
2424
|
+
`));
|
|
2425
|
+
}
|
|
2426
|
+
checkoutBranch(cwd, branchName);
|
|
2427
|
+
try {
|
|
2428
|
+
pushBranch(cwd, branchName);
|
|
2429
|
+
if (!silent) process.stdout.write(c.dim(" \u2713 pushed (idempotent)\n"));
|
|
2430
|
+
} catch (err) {
|
|
2431
|
+
try {
|
|
2432
|
+
checkoutBranch(cwd, baseBranch);
|
|
2433
|
+
} catch {
|
|
2434
|
+
}
|
|
2435
|
+
await apiCall("POST", "/api/v1/cli/me/runs", {
|
|
2436
|
+
body: {
|
|
2437
|
+
ticket_id: detail.id,
|
|
2438
|
+
event: "push_failed",
|
|
2439
|
+
output_excerpt: err.message.slice(0, 4e3)
|
|
2440
|
+
}
|
|
2441
|
+
});
|
|
2442
|
+
throw err;
|
|
2443
|
+
}
|
|
2444
|
+
const prTitle = `task #${detail.sequence_number}: ${detail.title}`.slice(0, 200);
|
|
2445
|
+
const prBody = buildResumePrBody(detail, branchName, ticketBaseBranch);
|
|
2446
|
+
let prResp;
|
|
2447
|
+
try {
|
|
2448
|
+
prResp = await apiCallOrThrow("POST", `/api/v1/cli/me/tickets/${detail.id}/pull-requests`, {
|
|
2449
|
+
body: {
|
|
2450
|
+
source_branch: branchName,
|
|
2451
|
+
base_branch: ticketBaseBranch,
|
|
2452
|
+
title: prTitle,
|
|
2453
|
+
body: prBody
|
|
2454
|
+
}
|
|
2455
|
+
});
|
|
2456
|
+
} catch (err) {
|
|
2457
|
+
try {
|
|
2458
|
+
checkoutBranch(cwd, baseBranch);
|
|
2459
|
+
} catch {
|
|
2460
|
+
}
|
|
2461
|
+
await apiCall("POST", "/api/v1/cli/me/runs", {
|
|
2462
|
+
body: {
|
|
2463
|
+
ticket_id: detail.id,
|
|
2464
|
+
event: "pr_failed",
|
|
2465
|
+
output_excerpt: err.message.slice(0, 4e3)
|
|
2466
|
+
}
|
|
2467
|
+
});
|
|
2468
|
+
throw err;
|
|
2469
|
+
}
|
|
2470
|
+
await apiCall("POST", "/api/v1/cli/me/runs", {
|
|
2471
|
+
body: {
|
|
2472
|
+
ticket_id: detail.id,
|
|
2473
|
+
event: "resumed",
|
|
2474
|
+
output_excerpt: `${prResp.recovered ? "recovered" : "opened"} PR #${prResp.pr_number}: ${prResp.pr_url}`
|
|
2475
|
+
}
|
|
2476
|
+
});
|
|
2477
|
+
if (!silent) {
|
|
2478
|
+
const tag = prResp.recovered ? c.ok("\u2713 Recovered PR") : c.ok("\u2713 PR opened");
|
|
2479
|
+
process.stdout.write(`${tag} ${c.cyan(prResp.pr_url)}
|
|
2480
|
+
`);
|
|
2481
|
+
}
|
|
2482
|
+
try {
|
|
2483
|
+
checkoutBranch(cwd, baseBranch);
|
|
2484
|
+
} catch {
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
async function resolveTicketId(project, ticketRef, silent) {
|
|
2488
|
+
if (ticketRef && ticketRef.length > 0) {
|
|
2489
|
+
if (/^[0-9a-f-]{36}$/i.test(ticketRef)) return ticketRef;
|
|
2490
|
+
const seqMatch = ticketRef.match(/^#?(\d+)$/);
|
|
2491
|
+
if (seqMatch) {
|
|
2492
|
+
const result2 = await apiCall("GET", "/api/v1/cli/me/tickets", {
|
|
2493
|
+
query: { project_id: project.project_id, ai_fix_status: "building", limit: 100 }
|
|
2494
|
+
});
|
|
2495
|
+
if (!result2.ok || !result2.data) {
|
|
2496
|
+
throw new CliError(
|
|
2497
|
+
CLI_EXIT_CODES.GENERIC_ERROR,
|
|
2498
|
+
`Could not list in-flight tickets (HTTP ${result2.status})${result2.error?.message ? `: ${result2.error.message}` : ""}`
|
|
2499
|
+
);
|
|
2500
|
+
}
|
|
2501
|
+
const seq = parseInt(seqMatch[1] ?? "", 10);
|
|
2502
|
+
const match = result2.data.find((t) => t.sequence_number === seq);
|
|
2503
|
+
if (!match) {
|
|
2504
|
+
throw new CliError(
|
|
2505
|
+
CLI_EXIT_CODES.GENERIC_ERROR,
|
|
2506
|
+
`No in-flight ticket #${seq} claimed by you on this project`,
|
|
2507
|
+
"Run `task doctor` to list your in-flight tickets, or pass the UUID directly."
|
|
2508
|
+
);
|
|
2509
|
+
}
|
|
2510
|
+
return match.id;
|
|
2511
|
+
}
|
|
2512
|
+
throw new CliError(
|
|
2513
|
+
CLI_EXIT_CODES.MISCONFIGURATION,
|
|
2514
|
+
`Invalid ticket reference: ${ticketRef}`,
|
|
2515
|
+
"Pass either a UUID or a sequence number like #42."
|
|
2516
|
+
);
|
|
2517
|
+
}
|
|
2518
|
+
const result = await apiCall("GET", "/api/v1/cli/me/tickets", {
|
|
2519
|
+
query: { project_id: project.project_id, ai_fix_status: "building", limit: 100 }
|
|
2520
|
+
});
|
|
2521
|
+
if (!result.ok) {
|
|
2522
|
+
throw new CliError(
|
|
2523
|
+
CLI_EXIT_CODES.GENERIC_ERROR,
|
|
2524
|
+
`Could not list in-flight tickets (HTTP ${result.status})${result.error?.message ? `: ${result.error.message}` : ""}`
|
|
2525
|
+
);
|
|
2526
|
+
}
|
|
2527
|
+
if (!result.data || result.data.length === 0) return null;
|
|
2528
|
+
if (result.data.length === 1) {
|
|
2529
|
+
const only = result.data[0];
|
|
2530
|
+
return only ? only.id : null;
|
|
2531
|
+
}
|
|
2532
|
+
if (silent) {
|
|
2533
|
+
throw new CliError(
|
|
2534
|
+
CLI_EXIT_CODES.MISCONFIGURATION,
|
|
2535
|
+
`${result.data.length} in-flight tickets \u2014 pass a specific reference in silent mode`,
|
|
2536
|
+
"Run `task doctor` to see the list, then `task resume #N` for the one you want."
|
|
2537
|
+
);
|
|
2538
|
+
}
|
|
2539
|
+
const answer = await inquirer3.prompt([
|
|
2540
|
+
{
|
|
2541
|
+
type: "list",
|
|
2542
|
+
name: "ticketId",
|
|
2543
|
+
message: "Pick an in-flight ticket to resume:",
|
|
2544
|
+
choices: result.data.map((t) => ({
|
|
2545
|
+
name: `#${t.sequence_number} \u2014 ${t.title}`,
|
|
2546
|
+
value: t.id
|
|
2547
|
+
}))
|
|
2548
|
+
}
|
|
2549
|
+
]);
|
|
2550
|
+
return answer.ticketId;
|
|
2551
|
+
}
|
|
2552
|
+
function localBranchExists(cwd, branchName) {
|
|
2553
|
+
try {
|
|
2554
|
+
execFileSync7("git", ["rev-parse", "--verify", `refs/heads/${branchName}`], {
|
|
2555
|
+
cwd,
|
|
2556
|
+
stdio: ["ignore", "ignore", "ignore"]
|
|
2557
|
+
});
|
|
2558
|
+
return true;
|
|
2559
|
+
} catch {
|
|
2560
|
+
return false;
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
function isAncestor(cwd, ancestor, descendant) {
|
|
2564
|
+
try {
|
|
2565
|
+
execFileSync7("git", ["merge-base", "--is-ancestor", ancestor, descendant], {
|
|
2566
|
+
cwd,
|
|
2567
|
+
stdio: ["ignore", "ignore", "ignore"]
|
|
2568
|
+
});
|
|
2569
|
+
return true;
|
|
2570
|
+
} catch {
|
|
2571
|
+
return false;
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
function buildResumePrBody(detail, branchName, baseBranch) {
|
|
2575
|
+
return [
|
|
2576
|
+
`Resolves ticket #${detail.sequence_number}: ${detail.title}`,
|
|
2577
|
+
"",
|
|
2578
|
+
detail.description ? `> ${detail.description.slice(0, 1500)}` : "",
|
|
2579
|
+
"",
|
|
2580
|
+
"---",
|
|
2581
|
+
"",
|
|
2582
|
+
`**Generated by:** \`task resume\` (recovery of a previous \`task work\` run)`,
|
|
2583
|
+
`**Branch:** \`${branchName}\` \u2190 \`${baseBranch}\``,
|
|
2584
|
+
"",
|
|
2585
|
+
"Please review carefully \u2014 this is an AI-generated change."
|
|
2586
|
+
].filter(Boolean).join("\n");
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2227
2589
|
// src/commands/scan.ts
|
|
2228
2590
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
2229
2591
|
import ora2 from "ora";
|
|
@@ -2931,7 +3293,7 @@ function clampInt(raw, min, max, fallback) {
|
|
|
2931
3293
|
}
|
|
2932
3294
|
|
|
2933
3295
|
// src/commands/pr-test.ts
|
|
2934
|
-
import { execFileSync as
|
|
3296
|
+
import { execFileSync as execFileSync8 } from "child_process";
|
|
2935
3297
|
function registerPrTest(program2) {
|
|
2936
3298
|
program2.command("pr-test").description(
|
|
2937
3299
|
"Dry-run the full PR pipeline \u2014 cuts a throwaway branch, opens a real PR via the dashboard, then cleans up. Use this to verify your git integration before running task work on real tickets."
|
|
@@ -2974,7 +3336,7 @@ async function runPrTest(opts) {
|
|
|
2974
3336
|
try {
|
|
2975
3337
|
if (!silent) process.stdout.write(`${c.dim("Step 3/6: empty commit\u2026")}
|
|
2976
3338
|
`);
|
|
2977
|
-
|
|
3339
|
+
execFileSync8(
|
|
2978
3340
|
"git",
|
|
2979
3341
|
["commit", "--allow-empty", "-m", `task pr-test: connectivity probe ${timestamp}`],
|
|
2980
3342
|
{ cwd, stdio: ["ignore", "pipe", "pipe"] }
|
|
@@ -3077,7 +3439,7 @@ import { platform as platform2 } from "os";
|
|
|
3077
3439
|
import { mkdir as mkdir7, readFile as readFile5, writeFile as writeFile8, unlink as unlink3, readdir } from "fs/promises";
|
|
3078
3440
|
import { homedir as homedir6 } from "os";
|
|
3079
3441
|
import { join as join8 } from "path";
|
|
3080
|
-
import { execFileSync as
|
|
3442
|
+
import { execFileSync as execFileSync9, spawn as spawn4 } from "child_process";
|
|
3081
3443
|
|
|
3082
3444
|
// src/scheduler/cron-translate.ts
|
|
3083
3445
|
function translateToLaunchd(cron) {
|
|
@@ -3245,17 +3607,17 @@ var launchdAdapter = {
|
|
|
3245
3607
|
const path = plistPath(entry.id);
|
|
3246
3608
|
await writeFile8(path, buildPlist(entry));
|
|
3247
3609
|
try {
|
|
3248
|
-
|
|
3610
|
+
execFileSync9("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
|
|
3249
3611
|
} catch {
|
|
3250
3612
|
}
|
|
3251
3613
|
if (entry.enabled) {
|
|
3252
|
-
|
|
3614
|
+
execFileSync9("launchctl", ["bootstrap", bootstrapDomain(), path]);
|
|
3253
3615
|
}
|
|
3254
3616
|
},
|
|
3255
3617
|
async remove(id) {
|
|
3256
3618
|
const path = plistPath(id);
|
|
3257
3619
|
try {
|
|
3258
|
-
|
|
3620
|
+
execFileSync9("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
|
|
3259
3621
|
} catch {
|
|
3260
3622
|
}
|
|
3261
3623
|
try {
|
|
@@ -3320,10 +3682,10 @@ var launchdAdapter = {
|
|
|
3320
3682
|
xml = xml.replace(/\s*<key>Disabled<\/key>\s*<true\/>/, "");
|
|
3321
3683
|
await writeFile8(path, xml);
|
|
3322
3684
|
try {
|
|
3323
|
-
|
|
3685
|
+
execFileSync9("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
|
|
3324
3686
|
} catch {
|
|
3325
3687
|
}
|
|
3326
|
-
|
|
3688
|
+
execFileSync9("launchctl", ["bootstrap", bootstrapDomain(), path]);
|
|
3327
3689
|
} else {
|
|
3328
3690
|
if (!/<key>Disabled<\/key>/.test(xml)) {
|
|
3329
3691
|
xml = xml.replace(
|
|
@@ -3333,7 +3695,7 @@ var launchdAdapter = {
|
|
|
3333
3695
|
await writeFile8(path, xml);
|
|
3334
3696
|
}
|
|
3335
3697
|
try {
|
|
3336
|
-
|
|
3698
|
+
execFileSync9("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
|
|
3337
3699
|
} catch {
|
|
3338
3700
|
}
|
|
3339
3701
|
}
|
|
@@ -3341,7 +3703,7 @@ var launchdAdapter = {
|
|
|
3341
3703
|
};
|
|
3342
3704
|
|
|
3343
3705
|
// src/scheduler/cron.ts
|
|
3344
|
-
import { execFileSync as
|
|
3706
|
+
import { execFileSync as execFileSync10, spawn as spawn5 } from "child_process";
|
|
3345
3707
|
|
|
3346
3708
|
// src/scheduler/safe-command.ts
|
|
3347
3709
|
var FORBIDDEN = /[;&|`$()<>\\]/;
|
|
@@ -3396,7 +3758,7 @@ var MARK_OPEN = (id) => `# task-cli:${id}:start`;
|
|
|
3396
3758
|
var MARK_CLOSE = (id) => `# task-cli:${id}:end`;
|
|
3397
3759
|
function readCrontab() {
|
|
3398
3760
|
try {
|
|
3399
|
-
return
|
|
3761
|
+
return execFileSync10("crontab", ["-l"], { encoding: "utf8" });
|
|
3400
3762
|
} catch {
|
|
3401
3763
|
return "";
|
|
3402
3764
|
}
|
|
@@ -3511,7 +3873,7 @@ var cronAdapter = {
|
|
|
3511
3873
|
};
|
|
3512
3874
|
|
|
3513
3875
|
// src/scheduler/windows.ts
|
|
3514
|
-
import { execFileSync as
|
|
3876
|
+
import { execFileSync as execFileSync11, spawn as spawn6 } from "child_process";
|
|
3515
3877
|
var TASK_PREFIX = "TaskCLI_";
|
|
3516
3878
|
function taskName(id) {
|
|
3517
3879
|
return `${TASK_PREFIX}${id.replace(/[^A-Za-z0-9_-]/g, "_")}`;
|
|
@@ -3576,22 +3938,22 @@ function pad(v) {
|
|
|
3576
3938
|
var windowsAdapter = {
|
|
3577
3939
|
async upsert(entry) {
|
|
3578
3940
|
const args = buildSchtasksArgs(entry, entry.command);
|
|
3579
|
-
|
|
3941
|
+
execFileSync11("schtasks.exe", args, { stdio: "ignore" });
|
|
3580
3942
|
if (!entry.enabled) {
|
|
3581
|
-
|
|
3943
|
+
execFileSync11("schtasks.exe", ["/Change", "/TN", taskName(entry.id), "/DISABLE"], {
|
|
3582
3944
|
stdio: "ignore"
|
|
3583
3945
|
});
|
|
3584
3946
|
}
|
|
3585
3947
|
},
|
|
3586
3948
|
async remove(id) {
|
|
3587
3949
|
try {
|
|
3588
|
-
|
|
3950
|
+
execFileSync11("schtasks.exe", ["/Delete", "/TN", taskName(id), "/F"], { stdio: "ignore" });
|
|
3589
3951
|
} catch {
|
|
3590
3952
|
}
|
|
3591
3953
|
},
|
|
3592
3954
|
async list() {
|
|
3593
3955
|
try {
|
|
3594
|
-
const csv =
|
|
3956
|
+
const csv = execFileSync11("schtasks.exe", ["/Query", "/FO", "CSV", "/V"], {
|
|
3595
3957
|
encoding: "utf8"
|
|
3596
3958
|
});
|
|
3597
3959
|
const lines = csv.split(/\r?\n/);
|
|
@@ -3641,7 +4003,7 @@ var windowsAdapter = {
|
|
|
3641
4003
|
},
|
|
3642
4004
|
async setEnabled(id, enabled) {
|
|
3643
4005
|
try {
|
|
3644
|
-
|
|
4006
|
+
execFileSync11(
|
|
3645
4007
|
"schtasks.exe",
|
|
3646
4008
|
["/Change", "/TN", taskName(id), enabled ? "/ENABLE" : "/DISABLE"],
|
|
3647
4009
|
{ stdio: "ignore" }
|
|
@@ -4049,10 +4411,14 @@ function registerConfig(program2) {
|
|
|
4049
4411
|
}
|
|
4050
4412
|
|
|
4051
4413
|
// src/commands/doctor.ts
|
|
4052
|
-
import { execFileSync as
|
|
4414
|
+
import { execFileSync as execFileSync12 } from "child_process";
|
|
4415
|
+
import { readFile as readFile8, writeFile as writeFile10 } from "fs/promises";
|
|
4416
|
+
import { join as join11 } from "path";
|
|
4053
4417
|
import { request as request5 } from "undici";
|
|
4418
|
+
var ALLOWED_TEST_EXECUTABLES = /* @__PURE__ */ new Set(["pnpm", "npm", "yarn", "bun", "node", "npx"]);
|
|
4419
|
+
var DEFAULT_TEST_COMMAND = "pnpm typecheck";
|
|
4054
4420
|
function registerDoctor(program2) {
|
|
4055
|
-
program2.command("doctor").description("Diagnose your CLI setup").action(async () => {
|
|
4421
|
+
program2.command("doctor").description("Diagnose your CLI setup").option("--fix", "attempt to auto-remediate fixable problems (e.g. add a typecheck script)").action(async (opts) => {
|
|
4056
4422
|
const checks = [];
|
|
4057
4423
|
const creds = await readCredentials();
|
|
4058
4424
|
checks.push({
|
|
@@ -4097,7 +4463,7 @@ function registerDoctor(program2) {
|
|
|
4097
4463
|
});
|
|
4098
4464
|
}
|
|
4099
4465
|
try {
|
|
4100
|
-
const dirty =
|
|
4466
|
+
const dirty = execFileSync12("git", ["status", "--porcelain"], {
|
|
4101
4467
|
cwd: root,
|
|
4102
4468
|
encoding: "utf8"
|
|
4103
4469
|
}).trim();
|
|
@@ -4109,19 +4475,167 @@ function registerDoctor(program2) {
|
|
|
4109
4475
|
} catch {
|
|
4110
4476
|
checks.push({ name: "working tree", ok: false, detail: "not in a git repo" });
|
|
4111
4477
|
}
|
|
4478
|
+
const testCheck = await checkPrePushTest(
|
|
4479
|
+
root,
|
|
4480
|
+
project?.cli_test_command ?? null,
|
|
4481
|
+
opts.fix === true
|
|
4482
|
+
);
|
|
4483
|
+
checks.push(testCheck);
|
|
4484
|
+
let inFlight = [];
|
|
4485
|
+
if (creds && project) {
|
|
4486
|
+
inFlight = await listInFlightTickets(project.project_id, root);
|
|
4487
|
+
checks.push({
|
|
4488
|
+
name: "in-flight tickets",
|
|
4489
|
+
ok: true,
|
|
4490
|
+
detail: inFlight.length === 0 ? "none" : `${inFlight.length} ticket(s) waiting to be resumed`
|
|
4491
|
+
});
|
|
4492
|
+
}
|
|
4112
4493
|
let allOk = true;
|
|
4113
4494
|
for (const check of checks) {
|
|
4114
4495
|
const sym = check.ok ? c.ok("\u2713") : c.err("\u2717");
|
|
4115
|
-
process.stdout.write(`${sym} ${check.name.padEnd(
|
|
4496
|
+
process.stdout.write(`${sym} ${check.name.padEnd(20)} ${c.dim(check.detail)}
|
|
4116
4497
|
`);
|
|
4117
|
-
if (!check.ok)
|
|
4498
|
+
if (!check.ok) {
|
|
4499
|
+
allOk = false;
|
|
4500
|
+
if (check.remediation) {
|
|
4501
|
+
process.stdout.write(` ${c.dim("\u2192 " + check.remediation)}
|
|
4502
|
+
`);
|
|
4503
|
+
}
|
|
4504
|
+
}
|
|
4505
|
+
}
|
|
4506
|
+
for (const t of inFlight) {
|
|
4507
|
+
const status = t.branchPresent ? c.ok("local branch present") : c.err("local branch missing");
|
|
4508
|
+
process.stdout.write(
|
|
4509
|
+
` ${c.dim("\u2192")} #${t.sequenceNumber} "${t.title}" \u2014 ${status} \u2014 ${c.cyan(`task resume #${t.sequenceNumber}`)}
|
|
4510
|
+
`
|
|
4511
|
+
);
|
|
4118
4512
|
}
|
|
4119
4513
|
if (!allOk) process.exit(1);
|
|
4120
4514
|
});
|
|
4121
4515
|
}
|
|
4516
|
+
async function listInFlightTickets(projectId, cwd) {
|
|
4517
|
+
const result = await apiCall(
|
|
4518
|
+
"GET",
|
|
4519
|
+
"/api/v1/cli/me/tickets",
|
|
4520
|
+
{
|
|
4521
|
+
query: { project_id: projectId, ai_fix_status: "building", limit: 100 }
|
|
4522
|
+
}
|
|
4523
|
+
);
|
|
4524
|
+
if (!result.ok || !result.data) return [];
|
|
4525
|
+
return result.data.map((t) => ({
|
|
4526
|
+
sequenceNumber: t.sequence_number,
|
|
4527
|
+
title: t.title,
|
|
4528
|
+
branchPresent: localBranchExists2(cwd, branchSlug(t.sequence_number, t.title))
|
|
4529
|
+
}));
|
|
4530
|
+
}
|
|
4531
|
+
function localBranchExists2(cwd, branchName) {
|
|
4532
|
+
try {
|
|
4533
|
+
execFileSync12("git", ["rev-parse", "--verify", `refs/heads/${branchName}`], {
|
|
4534
|
+
cwd,
|
|
4535
|
+
stdio: ["ignore", "ignore", "ignore"]
|
|
4536
|
+
});
|
|
4537
|
+
return true;
|
|
4538
|
+
} catch {
|
|
4539
|
+
return false;
|
|
4540
|
+
}
|
|
4541
|
+
}
|
|
4542
|
+
async function checkPrePushTest(root, configuredCommand, fix) {
|
|
4543
|
+
const command = configuredCommand && configuredCommand.trim().length > 0 ? configuredCommand.trim() : DEFAULT_TEST_COMMAND;
|
|
4544
|
+
const argv = command.split(/\s+/).filter((s) => s.length > 0);
|
|
4545
|
+
const exe = argv[0];
|
|
4546
|
+
if (!exe || !ALLOWED_TEST_EXECUTABLES.has(exe)) {
|
|
4547
|
+
return {
|
|
4548
|
+
name: "pre-push test",
|
|
4549
|
+
ok: false,
|
|
4550
|
+
detail: `command "${command}" not allowlisted (allowed: ${[...ALLOWED_TEST_EXECUTABLES].join(", ")})`,
|
|
4551
|
+
remediation: "update projects.cli_test_command via the dashboard"
|
|
4552
|
+
};
|
|
4553
|
+
}
|
|
4554
|
+
const scriptName = resolveScriptName(argv);
|
|
4555
|
+
if (!scriptName) {
|
|
4556
|
+
return {
|
|
4557
|
+
name: "pre-push test",
|
|
4558
|
+
ok: true,
|
|
4559
|
+
detail: `${command} (non-script executable, not statically verifiable)`
|
|
4560
|
+
};
|
|
4561
|
+
}
|
|
4562
|
+
const pkgPath = join11(root, "package.json");
|
|
4563
|
+
let pkgRaw;
|
|
4564
|
+
try {
|
|
4565
|
+
pkgRaw = await readFile8(pkgPath, "utf8");
|
|
4566
|
+
} catch {
|
|
4567
|
+
return {
|
|
4568
|
+
name: "pre-push test",
|
|
4569
|
+
ok: false,
|
|
4570
|
+
detail: `no package.json at repo root for "${command}"`,
|
|
4571
|
+
remediation: `add a package.json with a "${scriptName}" script, or set projects.cli_test_command in the dashboard`
|
|
4572
|
+
};
|
|
4573
|
+
}
|
|
4574
|
+
let pkg;
|
|
4575
|
+
try {
|
|
4576
|
+
pkg = JSON.parse(pkgRaw);
|
|
4577
|
+
} catch (err) {
|
|
4578
|
+
return {
|
|
4579
|
+
name: "pre-push test",
|
|
4580
|
+
ok: false,
|
|
4581
|
+
detail: `package.json is invalid JSON: ${err.message}`
|
|
4582
|
+
};
|
|
4583
|
+
}
|
|
4584
|
+
const scripts = pkg.scripts ?? {};
|
|
4585
|
+
if (typeof scripts[scriptName] === "string" && scripts[scriptName].trim().length > 0) {
|
|
4586
|
+
return {
|
|
4587
|
+
name: "pre-push test",
|
|
4588
|
+
ok: true,
|
|
4589
|
+
detail: `${command} \u2192 "${scripts[scriptName]}"`
|
|
4590
|
+
};
|
|
4591
|
+
}
|
|
4592
|
+
const isDefaultTypecheck = command === DEFAULT_TEST_COMMAND && scriptName === "typecheck";
|
|
4593
|
+
const hasTypeScript = !!pkg.devDependencies?.typescript || !!pkg.dependencies?.typescript;
|
|
4594
|
+
if (fix && isDefaultTypecheck && hasTypeScript) {
|
|
4595
|
+
pkg.scripts = { ...scripts, typecheck: "tsc --noEmit" };
|
|
4596
|
+
const indent = detectIndent(pkgRaw);
|
|
4597
|
+
const trailingNewline = pkgRaw.endsWith("\n") ? "\n" : "";
|
|
4598
|
+
await writeFile10(pkgPath, JSON.stringify(pkg, null, indent) + trailingNewline);
|
|
4599
|
+
return {
|
|
4600
|
+
name: "pre-push test",
|
|
4601
|
+
ok: true,
|
|
4602
|
+
detail: `added "typecheck": "tsc --noEmit" to ${pkgPath}`
|
|
4603
|
+
};
|
|
4604
|
+
}
|
|
4605
|
+
const remediation = isDefaultTypecheck ? hasTypeScript ? 're-run with --fix to add "typecheck": "tsc --noEmit" to package.json' : 'add a "typecheck" script to package.json, or set a different cli_test_command in the dashboard' : `add a "${scriptName}" script to package.json, or update cli_test_command in the dashboard`;
|
|
4606
|
+
return {
|
|
4607
|
+
name: "pre-push test",
|
|
4608
|
+
ok: false,
|
|
4609
|
+
detail: `"${scriptName}" script missing \u2014 "${command}" will exit 254 (ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL)`,
|
|
4610
|
+
remediation
|
|
4611
|
+
};
|
|
4612
|
+
}
|
|
4613
|
+
function resolveScriptName(argv) {
|
|
4614
|
+
const [exe, ...rest] = argv;
|
|
4615
|
+
if (!exe || rest.length === 0) return null;
|
|
4616
|
+
if (exe === "pnpm" || exe === "yarn" || exe === "bun") {
|
|
4617
|
+
const next = rest[0];
|
|
4618
|
+
if (!next) return null;
|
|
4619
|
+
if (next === "run") return rest[1] ?? null;
|
|
4620
|
+
if (next.startsWith("-")) return null;
|
|
4621
|
+
return next;
|
|
4622
|
+
}
|
|
4623
|
+
if (exe === "npm") {
|
|
4624
|
+
if (rest[0] === "run" || rest[0] === "run-script") return rest[1] ?? null;
|
|
4625
|
+
return null;
|
|
4626
|
+
}
|
|
4627
|
+
return null;
|
|
4628
|
+
}
|
|
4629
|
+
function detectIndent(raw) {
|
|
4630
|
+
const match = raw.match(/^(\s+)"/m);
|
|
4631
|
+
if (!match) return 2;
|
|
4632
|
+
const ws = match[1] ?? " ";
|
|
4633
|
+
if (ws.startsWith(" ")) return " ";
|
|
4634
|
+
return ws.length;
|
|
4635
|
+
}
|
|
4122
4636
|
function checkBinary(name, command) {
|
|
4123
4637
|
try {
|
|
4124
|
-
const out =
|
|
4638
|
+
const out = execFileSync12(command, ["--version"], { encoding: "utf8" }).trim();
|
|
4125
4639
|
return { name, ok: true, detail: out.split("\n")[0] ?? out };
|
|
4126
4640
|
} catch {
|
|
4127
4641
|
return { name, ok: false, detail: `'${command}' not found on PATH` };
|
|
@@ -4129,7 +4643,7 @@ function checkBinary(name, command) {
|
|
|
4129
4643
|
}
|
|
4130
4644
|
|
|
4131
4645
|
// src/commands/version.ts
|
|
4132
|
-
var CLI_VERSION = true ? "0.2.
|
|
4646
|
+
var CLI_VERSION = true ? "0.2.5" : "0.0.0-dev";
|
|
4133
4647
|
function registerVersion(program2) {
|
|
4134
4648
|
program2.command("version").description("Print the CLI version").action(() => {
|
|
4135
4649
|
process.stdout.write(CLI_VERSION + "\n");
|
|
@@ -4153,6 +4667,7 @@ registerTickets(program);
|
|
|
4153
4667
|
registerTicket(program);
|
|
4154
4668
|
registerWork(program);
|
|
4155
4669
|
registerMultiWork(program);
|
|
4670
|
+
registerResume(program);
|
|
4156
4671
|
registerScan(program);
|
|
4157
4672
|
registerPrTest(program);
|
|
4158
4673
|
registerScheduledTask(program);
|