@inteeka/task-cli 0.2.17 → 0.2.19
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 +514 -76
- package/dist/cli.js.map +1 -1
- package/package.json +3 -1
package/dist/cli.js
CHANGED
|
@@ -301,6 +301,7 @@ async function clearCredentials() {
|
|
|
301
301
|
if (err.code !== "ENOENT") throw err;
|
|
302
302
|
}
|
|
303
303
|
}
|
|
304
|
+
var CREDENTIALS_DIR = CONFIG_DIR;
|
|
304
305
|
|
|
305
306
|
// src/util/host.ts
|
|
306
307
|
import { createHash } from "crypto";
|
|
@@ -451,13 +452,34 @@ function sleep(ms) {
|
|
|
451
452
|
|
|
452
453
|
// src/api/client.ts
|
|
453
454
|
import { request as request3 } from "undici";
|
|
454
|
-
import { mkdir as
|
|
455
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
455
456
|
import { homedir as homedir2 } from "os";
|
|
456
|
-
import { join as
|
|
457
|
+
import { join as join3 } from "path";
|
|
457
458
|
|
|
458
459
|
// src/auth/refresh.ts
|
|
459
460
|
import { request as request2 } from "undici";
|
|
461
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
462
|
+
import { join as join2 } from "path";
|
|
463
|
+
import { lock } from "proper-lockfile";
|
|
460
464
|
var REFRESH_LEEWAY_MS = 6e4;
|
|
465
|
+
var REFRESH_LOCK_PATH = join2(CREDENTIALS_DIR, ".refresh.lock");
|
|
466
|
+
async function ensureLockFileExists() {
|
|
467
|
+
await mkdir2(CREDENTIALS_DIR, { recursive: true, mode: 448 });
|
|
468
|
+
await writeFile2(REFRESH_LOCK_PATH, "", { mode: 384, flag: "a" });
|
|
469
|
+
}
|
|
470
|
+
async function withRefreshLock(fn) {
|
|
471
|
+
await ensureLockFileExists();
|
|
472
|
+
const release = await lock(REFRESH_LOCK_PATH, {
|
|
473
|
+
retries: { retries: 50, minTimeout: 100, maxTimeout: 1e3, factor: 1.5 },
|
|
474
|
+
stale: 3e4,
|
|
475
|
+
update: 1e4
|
|
476
|
+
});
|
|
477
|
+
try {
|
|
478
|
+
return await fn();
|
|
479
|
+
} finally {
|
|
480
|
+
await release();
|
|
481
|
+
}
|
|
482
|
+
}
|
|
461
483
|
async function ensureFreshAccessToken(creds) {
|
|
462
484
|
const expiresMs = new Date(creds.access_expires_at).getTime();
|
|
463
485
|
if (Number.isFinite(expiresMs) && expiresMs - Date.now() > REFRESH_LEEWAY_MS) {
|
|
@@ -466,6 +488,18 @@ async function ensureFreshAccessToken(creds) {
|
|
|
466
488
|
return performRefresh(creds);
|
|
467
489
|
}
|
|
468
490
|
async function performRefresh(creds) {
|
|
491
|
+
return withRefreshLock(async () => {
|
|
492
|
+
const onDisk = await readCredentials();
|
|
493
|
+
if (onDisk) {
|
|
494
|
+
const expiresMs = new Date(onDisk.access_expires_at).getTime();
|
|
495
|
+
if (Number.isFinite(expiresMs) && expiresMs - Date.now() > REFRESH_LEEWAY_MS) {
|
|
496
|
+
return onDisk;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return refreshHttp(onDisk ?? creds);
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
async function refreshHttp(creds, retriesLeft = 1) {
|
|
469
503
|
const { hostId } = getHostInfo();
|
|
470
504
|
const apiUrl = creds.api_url.replace(/\/$/, "");
|
|
471
505
|
const res = await request2(`${apiUrl}/api/v1/cli/auth/refresh`, {
|
|
@@ -483,6 +517,16 @@ async function performRefresh(creds) {
|
|
|
483
517
|
headersTimeout: 15e3
|
|
484
518
|
});
|
|
485
519
|
if (res.statusCode === 401 || res.statusCode === 403) {
|
|
520
|
+
const onDisk = await readCredentials();
|
|
521
|
+
if (onDisk && onDisk.refresh_token !== creds.refresh_token) {
|
|
522
|
+
const expiresMs = new Date(onDisk.access_expires_at).getTime();
|
|
523
|
+
if (Number.isFinite(expiresMs) && expiresMs - Date.now() > REFRESH_LEEWAY_MS) {
|
|
524
|
+
return onDisk;
|
|
525
|
+
}
|
|
526
|
+
if (retriesLeft > 0) {
|
|
527
|
+
return refreshHttp(onDisk, retriesLeft - 1);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
486
530
|
await clearCredentials();
|
|
487
531
|
throw new CliError(
|
|
488
532
|
CLI_EXIT_CODES.UNAUTHORISED,
|
|
@@ -520,13 +564,13 @@ async function manualRefresh() {
|
|
|
520
564
|
// src/api/client.ts
|
|
521
565
|
async function dumpServerError(method, path, status, rawBody, headers) {
|
|
522
566
|
try {
|
|
523
|
-
const dir =
|
|
524
|
-
await
|
|
525
|
-
const file =
|
|
567
|
+
const dir = join3(homedir2(), ".cache", "task", "api-debug");
|
|
568
|
+
await mkdir3(dir, { recursive: true });
|
|
569
|
+
const file = join3(dir, `${Date.now()}-${status}.log`);
|
|
526
570
|
const safeHeaders = { ...headers };
|
|
527
571
|
delete safeHeaders["Authorization"];
|
|
528
572
|
delete safeHeaders["authorization"];
|
|
529
|
-
await
|
|
573
|
+
await writeFile3(
|
|
530
574
|
file,
|
|
531
575
|
[
|
|
532
576
|
`## ${method} ${path}`,
|
|
@@ -668,10 +712,10 @@ async function apiCallOrThrow(method, path, options = {}) {
|
|
|
668
712
|
}
|
|
669
713
|
|
|
670
714
|
// src/config/local-config.ts
|
|
671
|
-
import { mkdir as
|
|
715
|
+
import { mkdir as mkdir4, readFile as readFile2, writeFile as writeFile4 } from "fs/promises";
|
|
672
716
|
import { homedir as homedir3 } from "os";
|
|
673
|
-
import { dirname as dirname2, join as
|
|
674
|
-
var CONFIG_PATH =
|
|
717
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
718
|
+
var CONFIG_PATH = join4(homedir3(), ".config", "task", "config.json");
|
|
675
719
|
var DEFAULT_CONFIG = {
|
|
676
720
|
api_url: process.env["TASK_API_URL"] ?? "http://localhost:3400",
|
|
677
721
|
default_project: null,
|
|
@@ -691,8 +735,8 @@ async function readLocalConfig() {
|
|
|
691
735
|
}
|
|
692
736
|
}
|
|
693
737
|
async function writeLocalConfig(config) {
|
|
694
|
-
await
|
|
695
|
-
await
|
|
738
|
+
await mkdir4(dirname2(CONFIG_PATH), { recursive: true });
|
|
739
|
+
await writeFile4(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
696
740
|
}
|
|
697
741
|
async function setConfigValue(key, value) {
|
|
698
742
|
const cfg = await readLocalConfig();
|
|
@@ -811,14 +855,14 @@ function registerAuthRefresh(program2) {
|
|
|
811
855
|
}
|
|
812
856
|
|
|
813
857
|
// src/commands/link.ts
|
|
814
|
-
import { readFile as readFile4, writeFile as
|
|
858
|
+
import { readFile as readFile4, writeFile as writeFile6, appendFile, access } from "fs/promises";
|
|
815
859
|
import { constants as fsConstants } from "fs";
|
|
816
|
-
import { join as
|
|
860
|
+
import { join as join6 } from "path";
|
|
817
861
|
import inquirer from "inquirer";
|
|
818
862
|
|
|
819
863
|
// src/config/project.ts
|
|
820
|
-
import { mkdir as
|
|
821
|
-
import { dirname as dirname3, join as
|
|
864
|
+
import { mkdir as mkdir5, readFile as readFile3, writeFile as writeFile5, unlink as unlink2 } from "fs/promises";
|
|
865
|
+
import { dirname as dirname3, join as join5, resolve } from "path";
|
|
822
866
|
import { execSync } from "child_process";
|
|
823
867
|
function findRepoRoot(start = process.cwd()) {
|
|
824
868
|
try {
|
|
@@ -829,7 +873,7 @@ function findRepoRoot(start = process.cwd()) {
|
|
|
829
873
|
}
|
|
830
874
|
}
|
|
831
875
|
function configPath(repoRoot) {
|
|
832
|
-
return
|
|
876
|
+
return join5(repoRoot ?? findRepoRoot(), ".task", "config.json");
|
|
833
877
|
}
|
|
834
878
|
async function readProjectConfig(repoRoot) {
|
|
835
879
|
const path = configPath(repoRoot);
|
|
@@ -843,8 +887,8 @@ async function readProjectConfig(repoRoot) {
|
|
|
843
887
|
}
|
|
844
888
|
async function writeProjectConfig(config, repoRoot) {
|
|
845
889
|
const path = configPath(repoRoot);
|
|
846
|
-
await
|
|
847
|
-
await
|
|
890
|
+
await mkdir5(dirname3(path), { recursive: true });
|
|
891
|
+
await writeFile5(path, JSON.stringify(config, null, 2));
|
|
848
892
|
}
|
|
849
893
|
async function clearProjectConfig(repoRoot) {
|
|
850
894
|
const path = configPath(repoRoot);
|
|
@@ -943,7 +987,7 @@ async function resolveProject(projects, opts) {
|
|
|
943
987
|
return picked;
|
|
944
988
|
}
|
|
945
989
|
async function ensureGitignored(repoRoot) {
|
|
946
|
-
const gitignorePath =
|
|
990
|
+
const gitignorePath = join6(repoRoot, ".gitignore");
|
|
947
991
|
let existing = null;
|
|
948
992
|
try {
|
|
949
993
|
await access(gitignorePath, fsConstants.F_OK);
|
|
@@ -958,7 +1002,7 @@ async function ensureGitignored(repoRoot) {
|
|
|
958
1002
|
await appendFile(gitignorePath, block);
|
|
959
1003
|
return "added";
|
|
960
1004
|
}
|
|
961
|
-
await
|
|
1005
|
+
await writeFile6(gitignorePath, "# task CLI link config\n.task/\n");
|
|
962
1006
|
return "created";
|
|
963
1007
|
}
|
|
964
1008
|
|
|
@@ -1470,9 +1514,9 @@ import inquirer2 from "inquirer";
|
|
|
1470
1514
|
|
|
1471
1515
|
// src/agent/agent-service.ts
|
|
1472
1516
|
import { spawn } from "child_process";
|
|
1473
|
-
import { mkdir as
|
|
1517
|
+
import { mkdir as mkdir6, writeFile as writeFile7 } from "fs/promises";
|
|
1474
1518
|
import { homedir as homedir4 } from "os";
|
|
1475
|
-
import { join as
|
|
1519
|
+
import { join as join7 } from "path";
|
|
1476
1520
|
|
|
1477
1521
|
// src/agent/allowed-tools.ts
|
|
1478
1522
|
var ALLOWED_TOOLS = CLI_ALLOWED_TOOLS;
|
|
@@ -1529,10 +1573,10 @@ async function runAgent(args) {
|
|
|
1529
1573
|
let outputLogPath = null;
|
|
1530
1574
|
let logHandle = null;
|
|
1531
1575
|
if (args.silent) {
|
|
1532
|
-
const dir =
|
|
1533
|
-
await
|
|
1534
|
-
outputLogPath =
|
|
1535
|
-
await
|
|
1576
|
+
const dir = join7(homedir4(), ".cache", "task", "runs");
|
|
1577
|
+
await mkdir6(dir, { recursive: true });
|
|
1578
|
+
outputLogPath = join7(dir, `${args.runId}.log`);
|
|
1579
|
+
await writeFile7(outputLogPath, "");
|
|
1536
1580
|
const { createWriteStream } = await import("fs");
|
|
1537
1581
|
logHandle = createWriteStream(outputLogPath, { flags: "a" });
|
|
1538
1582
|
}
|
|
@@ -1733,11 +1777,111 @@ async function runProjectTest(args) {
|
|
|
1733
1777
|
});
|
|
1734
1778
|
}
|
|
1735
1779
|
|
|
1780
|
+
// src/test-runner/auto-install.ts
|
|
1781
|
+
import { spawn as spawn3 } from "child_process";
|
|
1782
|
+
import { stat as stat2 } from "fs/promises";
|
|
1783
|
+
import { dirname as dirname4, join as join8 } from "path";
|
|
1784
|
+
var INSTALL_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
1785
|
+
var TAIL_BYTES2 = 4e3;
|
|
1786
|
+
async function findPnpmWorkspaceRoot(start) {
|
|
1787
|
+
let cur = start;
|
|
1788
|
+
for (; ; ) {
|
|
1789
|
+
try {
|
|
1790
|
+
await stat2(join8(cur, "pnpm-lock.yaml"));
|
|
1791
|
+
return cur;
|
|
1792
|
+
} catch {
|
|
1793
|
+
}
|
|
1794
|
+
const parent = dirname4(cur);
|
|
1795
|
+
if (parent === cur) return null;
|
|
1796
|
+
cur = parent;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
async function mtimeMs(path) {
|
|
1800
|
+
try {
|
|
1801
|
+
return (await stat2(path)).mtimeMs;
|
|
1802
|
+
} catch {
|
|
1803
|
+
return null;
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
async function ensureWorkspaceInstalled(args) {
|
|
1807
|
+
const start = Date.now();
|
|
1808
|
+
const workspaceRoot = await findPnpmWorkspaceRoot(args.cwd);
|
|
1809
|
+
if (!workspaceRoot) {
|
|
1810
|
+
return {
|
|
1811
|
+
ok: true,
|
|
1812
|
+
skipped: true,
|
|
1813
|
+
reason: "no pnpm-lock.yaml on the path \u2014 not a pnpm workspace",
|
|
1814
|
+
workspaceRoot: null,
|
|
1815
|
+
durationMs: Date.now() - start,
|
|
1816
|
+
exitCode: null,
|
|
1817
|
+
tail: ""
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
const lockMtime = await mtimeMs(join8(workspaceRoot, "pnpm-lock.yaml"));
|
|
1821
|
+
const markerMtime = await mtimeMs(join8(workspaceRoot, "node_modules", ".modules.yaml"));
|
|
1822
|
+
if (lockMtime !== null && markerMtime !== null && markerMtime >= lockMtime) {
|
|
1823
|
+
return {
|
|
1824
|
+
ok: true,
|
|
1825
|
+
skipped: true,
|
|
1826
|
+
reason: "node_modules in sync with pnpm-lock.yaml",
|
|
1827
|
+
workspaceRoot,
|
|
1828
|
+
durationMs: Date.now() - start,
|
|
1829
|
+
exitCode: null,
|
|
1830
|
+
tail: ""
|
|
1831
|
+
};
|
|
1832
|
+
}
|
|
1833
|
+
const reason = markerMtime === null ? "node_modules missing \u2014 cold install" : "pnpm-lock.yaml newer than install marker";
|
|
1834
|
+
return new Promise((resolve2) => {
|
|
1835
|
+
const child = spawn3("pnpm", ["install", "--frozen-lockfile"], {
|
|
1836
|
+
cwd: workspaceRoot,
|
|
1837
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1838
|
+
shell: false,
|
|
1839
|
+
env: { ...process.env, CI: "1" },
|
|
1840
|
+
...args.signal ? { signal: args.signal } : {}
|
|
1841
|
+
});
|
|
1842
|
+
let buf = "";
|
|
1843
|
+
const append = (chunk) => {
|
|
1844
|
+
buf += chunk.toString("utf8");
|
|
1845
|
+
if (buf.length > TAIL_BYTES2 * 2) buf = buf.slice(-TAIL_BYTES2);
|
|
1846
|
+
};
|
|
1847
|
+
child.stdout?.on("data", append);
|
|
1848
|
+
child.stderr?.on("data", append);
|
|
1849
|
+
const timeoutHandle = setTimeout(() => {
|
|
1850
|
+
child.kill("SIGKILL");
|
|
1851
|
+
}, INSTALL_TIMEOUT_MS);
|
|
1852
|
+
child.on("close", (code) => {
|
|
1853
|
+
clearTimeout(timeoutHandle);
|
|
1854
|
+
resolve2({
|
|
1855
|
+
ok: code === 0,
|
|
1856
|
+
skipped: false,
|
|
1857
|
+
reason,
|
|
1858
|
+
workspaceRoot,
|
|
1859
|
+
durationMs: Date.now() - start,
|
|
1860
|
+
exitCode: code,
|
|
1861
|
+
tail: buf.slice(-TAIL_BYTES2)
|
|
1862
|
+
});
|
|
1863
|
+
});
|
|
1864
|
+
child.on("error", (err) => {
|
|
1865
|
+
clearTimeout(timeoutHandle);
|
|
1866
|
+
const msg = err.code === "ENOENT" ? "`pnpm` not on PATH" : err.message;
|
|
1867
|
+
resolve2({
|
|
1868
|
+
ok: false,
|
|
1869
|
+
skipped: false,
|
|
1870
|
+
reason: `${reason} \u2014 spawn error: ${msg}`,
|
|
1871
|
+
workspaceRoot,
|
|
1872
|
+
durationMs: Date.now() - start,
|
|
1873
|
+
exitCode: null,
|
|
1874
|
+
tail: buf.slice(-TAIL_BYTES2)
|
|
1875
|
+
});
|
|
1876
|
+
});
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1736
1880
|
// src/util/progress.ts
|
|
1737
|
-
import { mkdir as
|
|
1881
|
+
import { mkdir as mkdir7, writeFile as writeFile8, rename as rename2, unlink as unlink3, readdir, stat as stat3 } from "fs/promises";
|
|
1738
1882
|
import { tmpdir } from "os";
|
|
1739
|
-
import { join as
|
|
1740
|
-
var PROGRESS_DIR =
|
|
1883
|
+
import { join as join9 } from "path";
|
|
1884
|
+
var PROGRESS_DIR = join9(tmpdir(), "task-progress");
|
|
1741
1885
|
var STALE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
1742
1886
|
var ProgressWriter = class {
|
|
1743
1887
|
path;
|
|
@@ -1750,7 +1894,7 @@ var ProgressWriter = class {
|
|
|
1750
1894
|
this.ticketId = ticketId;
|
|
1751
1895
|
const deliveryId = process.env["TASK_DELIVERY_ID"]?.trim();
|
|
1752
1896
|
const fileBase = deliveryId && deliveryId.length > 0 ? deliveryId : `manual-${process.pid}`;
|
|
1753
|
-
this.path =
|
|
1897
|
+
this.path = join9(PROGRESS_DIR, `${fileBase}.json`);
|
|
1754
1898
|
}
|
|
1755
1899
|
/** Switch the in-flight ticket between phases (used by `task scan` which iterates). */
|
|
1756
1900
|
setTicketId(ticketId) {
|
|
@@ -1784,12 +1928,12 @@ var ProgressWriter = class {
|
|
|
1784
1928
|
});
|
|
1785
1929
|
}
|
|
1786
1930
|
async writeAtomic(payload) {
|
|
1787
|
-
await
|
|
1931
|
+
await mkdir7(PROGRESS_DIR, { recursive: true }).catch(() => {
|
|
1788
1932
|
});
|
|
1789
1933
|
const body = JSON.stringify(payload);
|
|
1790
1934
|
const tmp = `${this.path}.tmp`;
|
|
1791
1935
|
try {
|
|
1792
|
-
await
|
|
1936
|
+
await writeFile8(tmp, body, { encoding: "utf8", mode: 384 });
|
|
1793
1937
|
await rename2(tmp, this.path);
|
|
1794
1938
|
} catch {
|
|
1795
1939
|
await unlink3(tmp).catch(() => {
|
|
@@ -1806,9 +1950,9 @@ var ProgressWriter = class {
|
|
|
1806
1950
|
const cutoff = Date.now() - STALE_MAX_AGE_MS;
|
|
1807
1951
|
await Promise.all(
|
|
1808
1952
|
entries.filter((name) => name.endsWith(".json") || name.endsWith(".json.tmp")).map(async (name) => {
|
|
1809
|
-
const p =
|
|
1953
|
+
const p = join9(PROGRESS_DIR, name);
|
|
1810
1954
|
try {
|
|
1811
|
-
const s = await
|
|
1955
|
+
const s = await stat3(p);
|
|
1812
1956
|
if (s.mtimeMs < cutoff) {
|
|
1813
1957
|
await unlink3(p).catch(() => {
|
|
1814
1958
|
});
|
|
@@ -2172,6 +2316,50 @@ ${detail.ai_fix_approval_notes}` : ""
|
|
|
2172
2316
|
await progress.setPhase("testing", {
|
|
2173
2317
|
detail: testCommand ?? "pnpm typecheck"
|
|
2174
2318
|
});
|
|
2319
|
+
const installResult = await ensureWorkspaceInstalled({ cwd });
|
|
2320
|
+
if (!installResult.ok) {
|
|
2321
|
+
discardWorkingTreeChanges(cwd);
|
|
2322
|
+
try {
|
|
2323
|
+
checkoutBranch(cwd, baseBranch);
|
|
2324
|
+
} catch {
|
|
2325
|
+
}
|
|
2326
|
+
deleteLocalBranch(cwd, branchName);
|
|
2327
|
+
await apiCall("POST", "/api/v1/cli/me/runs", {
|
|
2328
|
+
body: {
|
|
2329
|
+
ticket_id: detail.id,
|
|
2330
|
+
schedule_id: opts.scheduleId,
|
|
2331
|
+
event: "tests_failed",
|
|
2332
|
+
claude_session_id: runId,
|
|
2333
|
+
duration_ms: installResult.durationMs,
|
|
2334
|
+
output_excerpt: `pnpm install --frozen-lockfile failed (${installResult.reason})
|
|
2335
|
+
${installResult.tail}`.slice(
|
|
2336
|
+
0,
|
|
2337
|
+
4e3
|
|
2338
|
+
)
|
|
2339
|
+
}
|
|
2340
|
+
});
|
|
2341
|
+
if (!silent) {
|
|
2342
|
+
process.stdout.write(
|
|
2343
|
+
`${c.err("\u2717 pnpm install --frozen-lockfile failed")} (exit ${installResult.exitCode}) \u2014 branch deleted, no push.
|
|
2344
|
+
`
|
|
2345
|
+
);
|
|
2346
|
+
if (installResult.tail.trim().length > 0) {
|
|
2347
|
+
process.stdout.write(c.dim(installResult.tail.slice(-1e3) + "\n"));
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
throw new CliError(
|
|
2351
|
+
CLI_EXIT_CODES.GENERIC_ERROR,
|
|
2352
|
+
`pnpm install --frozen-lockfile failed (exit ${installResult.exitCode}) \u2014 ${installResult.reason}`
|
|
2353
|
+
);
|
|
2354
|
+
}
|
|
2355
|
+
if (!installResult.skipped && !silent) {
|
|
2356
|
+
process.stdout.write(
|
|
2357
|
+
c.dim(
|
|
2358
|
+
` pnpm install --frozen-lockfile (${installResult.reason}) \u2014 ${installResult.durationMs}ms
|
|
2359
|
+
`
|
|
2360
|
+
)
|
|
2361
|
+
);
|
|
2362
|
+
}
|
|
2175
2363
|
if (!silent)
|
|
2176
2364
|
process.stdout.write(c.dim(` running pre-push test: ${testCommand ?? "pnpm typecheck"}
|
|
2177
2365
|
`));
|
|
@@ -3199,10 +3387,10 @@ function autopilotExitCode(code, status) {
|
|
|
3199
3387
|
}
|
|
3200
3388
|
|
|
3201
3389
|
// src/scan/llm.ts
|
|
3202
|
-
import { spawn as
|
|
3203
|
-
import { mkdir as
|
|
3390
|
+
import { spawn as spawn4 } from "child_process";
|
|
3391
|
+
import { mkdir as mkdir8, writeFile as writeFile9 } from "fs/promises";
|
|
3204
3392
|
import { homedir as homedir5 } from "os";
|
|
3205
|
-
import { join as
|
|
3393
|
+
import { join as join10 } from "path";
|
|
3206
3394
|
var FIX_PROMPT_JSON_SCHEMA = {
|
|
3207
3395
|
type: "object",
|
|
3208
3396
|
// Phase 3 — confidence_reason is REQUIRED unconditionally so the
|
|
@@ -3291,7 +3479,7 @@ async function generateFixPromptJson(args) {
|
|
|
3291
3479
|
return new Promise((resolve2, reject) => {
|
|
3292
3480
|
let child;
|
|
3293
3481
|
try {
|
|
3294
|
-
child =
|
|
3482
|
+
child = spawn4(claude, cliArgs, {
|
|
3295
3483
|
stdio: ["pipe", "pipe", "pipe"],
|
|
3296
3484
|
signal: args.signal
|
|
3297
3485
|
});
|
|
@@ -3417,10 +3605,10 @@ function readEnvelopeTokens(raw, userPrompt, innerText) {
|
|
|
3417
3605
|
async function maybeDumpDebug(ticketId, stdout, stderr) {
|
|
3418
3606
|
if (!DEBUG && stdout.length === 0 && stderr.length === 0) return null;
|
|
3419
3607
|
try {
|
|
3420
|
-
const dir =
|
|
3421
|
-
await
|
|
3422
|
-
const path =
|
|
3423
|
-
await
|
|
3608
|
+
const dir = join10(homedir5(), ".cache", "task", "scan-debug");
|
|
3609
|
+
await mkdir8(dir, { recursive: true });
|
|
3610
|
+
const path = join10(dir, `${ticketId}-${Date.now()}.log`);
|
|
3611
|
+
await writeFile9(
|
|
3424
3612
|
path,
|
|
3425
3613
|
["## ticket_id", ticketId, "", "## stdout", stdout, "", "## stderr", stderr].join("\n")
|
|
3426
3614
|
);
|
|
@@ -3667,10 +3855,13 @@ ${c.bold(`${project.organisation_slug}/${project.project_slug}`)} ${c.dim(`(${pr
|
|
|
3667
3855
|
for (const ticket of prepared.tickets) {
|
|
3668
3856
|
if (args.isInterrupted()) break;
|
|
3669
3857
|
inFlight.add(ticket.ticket_id);
|
|
3670
|
-
const
|
|
3858
|
+
const specialistSuffix = ticket.specialist ? ` \xB7 Specialist: ${ticket.specialist.name} (${ticket.specialist.skill_count} skill${ticket.specialist.skill_count === 1 ? "" : "s"})` : "";
|
|
3859
|
+
const spinner = silent ? null : ora2(
|
|
3860
|
+
`#${ticket.sequence_number} ${ticket.title.slice(0, 60)}${specialistSuffix}`
|
|
3861
|
+
).start();
|
|
3671
3862
|
progress.setTicketId(ticket.ticket_id);
|
|
3672
3863
|
await progress.setPhase("analysing", {
|
|
3673
|
-
detail: `Generating fix prompt for #${ticket.sequence_number}`
|
|
3864
|
+
detail: ticket.specialist ? `Generating fix prompt for #${ticket.sequence_number} as ${ticket.specialist.name}` : `Generating fix prompt for #${ticket.sequence_number}`
|
|
3674
3865
|
});
|
|
3675
3866
|
try {
|
|
3676
3867
|
const generated = await safeGenerate(ticket, claudePath);
|
|
@@ -3760,6 +3951,252 @@ function clampInt(raw, min, max, fallback) {
|
|
|
3760
3951
|
return Math.min(v, max);
|
|
3761
3952
|
}
|
|
3762
3953
|
|
|
3954
|
+
// src/commands/fast-track.ts
|
|
3955
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
3956
|
+
import ora3 from "ora";
|
|
3957
|
+
function registerFastTrack(program2) {
|
|
3958
|
+
program2.command("fast-track").description(
|
|
3959
|
+
"End-to-end: scan + auto-approve + work on the next CLI-eligible ticket(s) in the linked project \u2014 no admin review step"
|
|
3960
|
+
).option("--max <n>", "Process up to N tickets in this invocation", "1").option("--api-url <url>", "Override TASK_API_URL").option("--silent", "Suppress per-ticket progress chrome").option("--dry-run", "Run scan + approve + agent + tests but do not commit, push, or open a PR").option(
|
|
3961
|
+
"--reset",
|
|
3962
|
+
"DESTRUCTIVE: discard local working-tree changes before the first ticket. Requires --confirm in non-TTY contexts."
|
|
3963
|
+
).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) => {
|
|
3964
|
+
await runFastTrack(opts);
|
|
3965
|
+
});
|
|
3966
|
+
}
|
|
3967
|
+
async function runFastTrack(opts) {
|
|
3968
|
+
let creds = await readCredentials();
|
|
3969
|
+
if (!creds) {
|
|
3970
|
+
throw new CliError(
|
|
3971
|
+
CLI_EXIT_CODES.MISCONFIGURATION,
|
|
3972
|
+
"Not signed in",
|
|
3973
|
+
"Run 'task login' to authenticate."
|
|
3974
|
+
);
|
|
3975
|
+
}
|
|
3976
|
+
creds = await ensureFreshAccessToken(creds);
|
|
3977
|
+
const localCfg = await readLocalConfig();
|
|
3978
|
+
const linkedProject = await readProjectConfig(findRepoRoot());
|
|
3979
|
+
if (!linkedProject) {
|
|
3980
|
+
throw new CliError(
|
|
3981
|
+
CLI_EXIT_CODES.MISCONFIGURATION,
|
|
3982
|
+
"No project link in this repo",
|
|
3983
|
+
"Run 'task link' first \u2014 fast-track operates on the linked project only."
|
|
3984
|
+
);
|
|
3985
|
+
}
|
|
3986
|
+
const apiUrl = (opts.apiUrl ?? process.env["TASK_API_URL"] ?? creds.api_url ?? localCfg.api_url ?? linkedProject.api_url ?? "http://localhost:3400").replace(/\/$/, "");
|
|
3987
|
+
const max = Math.max(1, parseInt(opts.max, 10) || 1);
|
|
3988
|
+
const silent = !!opts.silent || localCfg.silent;
|
|
3989
|
+
const ctx = await buildWorkContext({ max: "1", silent: opts.silent });
|
|
3990
|
+
const api = new AutopilotApi({ apiUrl, creds });
|
|
3991
|
+
const claudePath = localCfg.claude_path ?? void 0;
|
|
3992
|
+
let firstIteration = true;
|
|
3993
|
+
const results = [];
|
|
3994
|
+
for (let i = 0; i < max; i++) {
|
|
3995
|
+
const innerWorkOpts = {
|
|
3996
|
+
auto: false,
|
|
3997
|
+
// We pin a specific ticketId per iteration.
|
|
3998
|
+
next: false,
|
|
3999
|
+
max: "1",
|
|
4000
|
+
silent: opts.silent,
|
|
4001
|
+
...opts.dryRun ? { dryRun: true } : {},
|
|
4002
|
+
...opts.scheduleId ? { scheduleId: opts.scheduleId } : {},
|
|
4003
|
+
...firstIteration && opts.reset ? { reset: true } : {},
|
|
4004
|
+
...firstIteration && opts.confirm ? { confirm: true } : {}
|
|
4005
|
+
};
|
|
4006
|
+
const outcome = await fastTrackOneTicket({
|
|
4007
|
+
api,
|
|
4008
|
+
apiUrl,
|
|
4009
|
+
project: linkedProject,
|
|
4010
|
+
ctx,
|
|
4011
|
+
claudePath,
|
|
4012
|
+
silent,
|
|
4013
|
+
innerWorkOpts,
|
|
4014
|
+
scheduleId: opts.scheduleId
|
|
4015
|
+
});
|
|
4016
|
+
if (outcome.kind === "no_eligible") {
|
|
4017
|
+
if (results.length === 0 && !silent) {
|
|
4018
|
+
process.stdout.write(c.dim("No CLI-eligible tickets available to fast-track.\n"));
|
|
4019
|
+
}
|
|
4020
|
+
break;
|
|
4021
|
+
}
|
|
4022
|
+
results.push(outcome.result);
|
|
4023
|
+
firstIteration = false;
|
|
4024
|
+
if (i < max - 1 && outcome.result.status === "completed") {
|
|
4025
|
+
enforceBaseBranchClean(ctx.cwd, ctx.baseBranch);
|
|
4026
|
+
}
|
|
4027
|
+
}
|
|
4028
|
+
if (!silent) {
|
|
4029
|
+
summarise(results);
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
4032
|
+
async function fastTrackOneTicket(args) {
|
|
4033
|
+
const { api, project, ctx, claudePath, silent, innerWorkOpts, scheduleId } = args;
|
|
4034
|
+
const issued = await api.issueSkillToken({
|
|
4035
|
+
project_id: project.project_id,
|
|
4036
|
+
max_submits: 1
|
|
4037
|
+
});
|
|
4038
|
+
const skillToken = issued.token;
|
|
4039
|
+
const prepared = await api.prepare(skillToken, 1, randomUUID3());
|
|
4040
|
+
if (prepared.tickets.length === 0) {
|
|
4041
|
+
return { kind: "no_eligible" };
|
|
4042
|
+
}
|
|
4043
|
+
const ticket = prepared.tickets[0];
|
|
4044
|
+
const nonce = prepared.prepare_nonce;
|
|
4045
|
+
const spinner = silent ? null : ora3(`#${ticket.sequence_number} ${ticket.title.slice(0, 60)} \u2014 scanning`).start();
|
|
4046
|
+
let generated;
|
|
4047
|
+
try {
|
|
4048
|
+
generated = await generateFixPromptJson({
|
|
4049
|
+
systemPrompt: ticket.system_prompt,
|
|
4050
|
+
repoOverviewBlock: ticket.repo_overview_block,
|
|
4051
|
+
ticketBlock: ticket.ticket_block,
|
|
4052
|
+
outputSchemaHint: ticket.output_schema_hint,
|
|
4053
|
+
modelId: ticket.model_id,
|
|
4054
|
+
ticketId: ticket.ticket_id,
|
|
4055
|
+
...claudePath ? { claudePath } : {}
|
|
4056
|
+
});
|
|
4057
|
+
} catch (err) {
|
|
4058
|
+
await api.abort(skillToken, [ticket.ticket_id]).catch(() => void 0);
|
|
4059
|
+
const reason = err instanceof LlmGenerationError ? `${err.reason}: ${err.message.slice(0, 200)}` : "";
|
|
4060
|
+
spinner?.fail(`#${ticket.sequence_number} scan failed${reason ? ` (${reason})` : ""}`);
|
|
4061
|
+
throw err instanceof CliError ? err : new CliError(
|
|
4062
|
+
CLI_EXIT_CODES.GENERIC_ERROR,
|
|
4063
|
+
`Fix-prompt generation failed for #${ticket.sequence_number}: ${err.message.slice(0, 200)}`
|
|
4064
|
+
);
|
|
4065
|
+
}
|
|
4066
|
+
const submitted = await api.submit({
|
|
4067
|
+
skillToken,
|
|
4068
|
+
nonce,
|
|
4069
|
+
ticketId: ticket.ticket_id,
|
|
4070
|
+
structured: generated.structured,
|
|
4071
|
+
inputTokens: generated.inputTokens,
|
|
4072
|
+
outputTokens: generated.outputTokens,
|
|
4073
|
+
model: ticket.model_id
|
|
4074
|
+
});
|
|
4075
|
+
if (submitted.status === "skip") {
|
|
4076
|
+
spinner?.warn(`#${ticket.sequence_number} server rejected submission (${submitted.reason})`);
|
|
4077
|
+
return {
|
|
4078
|
+
kind: "processed",
|
|
4079
|
+
result: {
|
|
4080
|
+
sequenceNumber: ticket.sequence_number,
|
|
4081
|
+
ticketId: ticket.ticket_id,
|
|
4082
|
+
previousFixStatus: submitted.reason,
|
|
4083
|
+
status: "scan_skipped",
|
|
4084
|
+
error: submitted.reason
|
|
4085
|
+
}
|
|
4086
|
+
};
|
|
4087
|
+
}
|
|
4088
|
+
const denylistHit = submitted.status === "needs_review";
|
|
4089
|
+
if (spinner) {
|
|
4090
|
+
spinner.text = `#${ticket.sequence_number} approving${denylistHit ? " (denylist override)" : ""}`;
|
|
4091
|
+
}
|
|
4092
|
+
let approved;
|
|
4093
|
+
try {
|
|
4094
|
+
approved = await apiCallOrThrow(
|
|
4095
|
+
"POST",
|
|
4096
|
+
`/api/v1/cli/me/tickets/${ticket.ticket_id}/ai-fix-prompt/fast-approve`,
|
|
4097
|
+
{
|
|
4098
|
+
body: {
|
|
4099
|
+
...denylistHit ? { denylist_acknowledged: true } : {},
|
|
4100
|
+
reason: `fast-track: ${denylistHit ? "denylist override" : "auto-approve"} by CLI session`
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
);
|
|
4104
|
+
} catch (err) {
|
|
4105
|
+
spinner?.fail(
|
|
4106
|
+
`#${ticket.sequence_number} fast-approve failed: ${err.message.slice(0, 120)}`
|
|
4107
|
+
);
|
|
4108
|
+
throw err;
|
|
4109
|
+
}
|
|
4110
|
+
if (spinner) {
|
|
4111
|
+
spinner.text = `#${ticket.sequence_number} building`;
|
|
4112
|
+
}
|
|
4113
|
+
const innerOpts = { ...innerWorkOpts };
|
|
4114
|
+
if (scheduleId !== void 0) innerOpts.scheduleId = scheduleId;
|
|
4115
|
+
try {
|
|
4116
|
+
const outcome = await processOneTicket(ctx, innerOpts, ticket.ticket_id);
|
|
4117
|
+
if (outcome.kind === "no_eligible") {
|
|
4118
|
+
spinner?.warn(`#${ticket.sequence_number} unexpectedly invisible to work after approve`);
|
|
4119
|
+
return {
|
|
4120
|
+
kind: "processed",
|
|
4121
|
+
result: {
|
|
4122
|
+
sequenceNumber: ticket.sequence_number,
|
|
4123
|
+
ticketId: ticket.ticket_id,
|
|
4124
|
+
previousFixStatus: approved.from_status ?? "unknown",
|
|
4125
|
+
status: "failed",
|
|
4126
|
+
error: "ticket vanished between approve and claim"
|
|
4127
|
+
}
|
|
4128
|
+
};
|
|
4129
|
+
}
|
|
4130
|
+
if (outcome.kind === "completed") {
|
|
4131
|
+
spinner?.succeed(
|
|
4132
|
+
`#${ticket.sequence_number} PR opened${outcome.prUrl ? ` ${outcome.prUrl}` : ""}`
|
|
4133
|
+
);
|
|
4134
|
+
const result = {
|
|
4135
|
+
sequenceNumber: outcome.sequenceNumber,
|
|
4136
|
+
ticketId: ticket.ticket_id,
|
|
4137
|
+
previousFixStatus: approved.from_status ?? "unknown",
|
|
4138
|
+
status: "completed"
|
|
4139
|
+
};
|
|
4140
|
+
if (outcome.prNumber !== void 0) result.prNumber = outcome.prNumber;
|
|
4141
|
+
if (outcome.prUrl !== void 0) result.prUrl = outcome.prUrl;
|
|
4142
|
+
return { kind: "processed", result };
|
|
4143
|
+
}
|
|
4144
|
+
if (outcome.kind === "dry_run") {
|
|
4145
|
+
spinner?.succeed(`#${ticket.sequence_number} dry-run (no PR)`);
|
|
4146
|
+
return {
|
|
4147
|
+
kind: "processed",
|
|
4148
|
+
result: {
|
|
4149
|
+
sequenceNumber: outcome.sequenceNumber,
|
|
4150
|
+
ticketId: ticket.ticket_id,
|
|
4151
|
+
previousFixStatus: approved.from_status ?? "unknown",
|
|
4152
|
+
status: "dry_run"
|
|
4153
|
+
}
|
|
4154
|
+
};
|
|
4155
|
+
}
|
|
4156
|
+
spinner?.warn(`#${ticket.sequence_number} agent produced no changes`);
|
|
4157
|
+
return {
|
|
4158
|
+
kind: "processed",
|
|
4159
|
+
result: {
|
|
4160
|
+
sequenceNumber: outcome.sequenceNumber,
|
|
4161
|
+
ticketId: ticket.ticket_id,
|
|
4162
|
+
previousFixStatus: approved.from_status ?? "unknown",
|
|
4163
|
+
status: "no_changes"
|
|
4164
|
+
}
|
|
4165
|
+
};
|
|
4166
|
+
} catch (err) {
|
|
4167
|
+
spinner?.fail(
|
|
4168
|
+
`#${ticket.sequence_number} work failed: ${err.message.slice(0, 120)}`
|
|
4169
|
+
);
|
|
4170
|
+
throw err;
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
function summarise(results) {
|
|
4174
|
+
if (results.length === 0) return;
|
|
4175
|
+
const counts = results.reduce(
|
|
4176
|
+
(acc, r) => {
|
|
4177
|
+
acc[r.status] = (acc[r.status] ?? 0) + 1;
|
|
4178
|
+
return acc;
|
|
4179
|
+
},
|
|
4180
|
+
{}
|
|
4181
|
+
);
|
|
4182
|
+
const parts = [];
|
|
4183
|
+
if (counts.completed) parts.push(`${c.ok(String(counts.completed))} PR(s) opened`);
|
|
4184
|
+
if (counts.dry_run) parts.push(`${c.dim(String(counts.dry_run))} dry-run`);
|
|
4185
|
+
if (counts.no_changes) parts.push(`${c.dim(String(counts.no_changes))} no-changes`);
|
|
4186
|
+
if (counts.scan_skipped) parts.push(`${c.warn(String(counts.scan_skipped))} scan skipped`);
|
|
4187
|
+
if (counts.failed) parts.push(`${c.err(String(counts.failed))} failed`);
|
|
4188
|
+
process.stdout.write(`
|
|
4189
|
+
${c.bold("Fast-track summary")}: ${parts.join(", ")}
|
|
4190
|
+
`);
|
|
4191
|
+
const denylistOverrides = results.filter((r) => r.previousFixStatus === "needs_review").length;
|
|
4192
|
+
if (denylistOverrides > 0) {
|
|
4193
|
+
process.stdout.write(
|
|
4194
|
+
`${c.warn("\u26A0")} ${denylistOverrides} ticket(s) bypassed the denylist gate \u2014 review the PR diff(s) carefully.
|
|
4195
|
+
`
|
|
4196
|
+
);
|
|
4197
|
+
}
|
|
4198
|
+
}
|
|
4199
|
+
|
|
3763
4200
|
// src/commands/pr-test.ts
|
|
3764
4201
|
import { execFileSync as execFileSync8 } from "child_process";
|
|
3765
4202
|
function registerPrTest(program2) {
|
|
@@ -3898,16 +4335,16 @@ ${c.err("\u2717 pr-test failed")}: ${err.message}
|
|
|
3898
4335
|
}
|
|
3899
4336
|
|
|
3900
4337
|
// src/commands/scheduled-task.ts
|
|
3901
|
-
import { randomUUID as
|
|
4338
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
3902
4339
|
|
|
3903
4340
|
// src/scheduler/index.ts
|
|
3904
4341
|
import { platform as platform2 } from "os";
|
|
3905
4342
|
|
|
3906
4343
|
// src/scheduler/launchd.ts
|
|
3907
|
-
import { mkdir as
|
|
4344
|
+
import { mkdir as mkdir9, readFile as readFile5, writeFile as writeFile10, unlink as unlink4, readdir as readdir2 } from "fs/promises";
|
|
3908
4345
|
import { homedir as homedir6 } from "os";
|
|
3909
|
-
import { join as
|
|
3910
|
-
import { execFileSync as execFileSync9, spawn as
|
|
4346
|
+
import { join as join11 } from "path";
|
|
4347
|
+
import { execFileSync as execFileSync9, spawn as spawn5 } from "child_process";
|
|
3911
4348
|
|
|
3912
4349
|
// src/scheduler/cron-translate.ts
|
|
3913
4350
|
function translateToLaunchd(cron) {
|
|
@@ -4008,14 +4445,14 @@ function expandField(field, min, max) {
|
|
|
4008
4445
|
}
|
|
4009
4446
|
|
|
4010
4447
|
// src/scheduler/launchd.ts
|
|
4011
|
-
var PLIST_DIR =
|
|
4448
|
+
var PLIST_DIR = join11(homedir6(), "Library", "LaunchAgents");
|
|
4012
4449
|
var LABEL_PREFIX = "com.inteeka.task.cli.";
|
|
4013
4450
|
var SAFE_ID_RE = /^[0-9a-zA-Z._-]+$/;
|
|
4014
4451
|
function plistPath(id) {
|
|
4015
4452
|
if (!SAFE_ID_RE.test(id) || id.includes("..")) {
|
|
4016
4453
|
throw new Error(`Refusing to compute plist path for unsafe id: ${id}`);
|
|
4017
4454
|
}
|
|
4018
|
-
return
|
|
4455
|
+
return join11(PLIST_DIR, `${LABEL_PREFIX}${id}.plist`);
|
|
4019
4456
|
}
|
|
4020
4457
|
function buildPlist(entry) {
|
|
4021
4458
|
const calendars = translateToLaunchd(entry.cron);
|
|
@@ -4051,9 +4488,9 @@ ${fields}
|
|
|
4051
4488
|
` <string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>`,
|
|
4052
4489
|
` </dict>`,
|
|
4053
4490
|
` <key>StandardOutPath</key>`,
|
|
4054
|
-
` <string>${escapeXml(
|
|
4491
|
+
` <string>${escapeXml(join11(homedir6(), ".cache", "task", "launchd-stdout.log"))}</string>`,
|
|
4055
4492
|
` <key>StandardErrorPath</key>`,
|
|
4056
|
-
` <string>${escapeXml(
|
|
4493
|
+
` <string>${escapeXml(join11(homedir6(), ".cache", "task", "launchd-stderr.log"))}</string>`,
|
|
4057
4494
|
!entry.enabled ? ` <key>Disabled</key>
|
|
4058
4495
|
<true/>` : "",
|
|
4059
4496
|
"</dict>",
|
|
@@ -4071,9 +4508,9 @@ function bootstrapDomain() {
|
|
|
4071
4508
|
}
|
|
4072
4509
|
var launchdAdapter = {
|
|
4073
4510
|
async upsert(entry) {
|
|
4074
|
-
await
|
|
4511
|
+
await mkdir9(PLIST_DIR, { recursive: true });
|
|
4075
4512
|
const path = plistPath(entry.id);
|
|
4076
|
-
await
|
|
4513
|
+
await writeFile10(path, buildPlist(entry));
|
|
4077
4514
|
try {
|
|
4078
4515
|
execFileSync9("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
|
|
4079
4516
|
} catch {
|
|
@@ -4102,7 +4539,7 @@ var launchdAdapter = {
|
|
|
4102
4539
|
for (const file of ours) {
|
|
4103
4540
|
const id = file.slice(LABEL_PREFIX.length, -".plist".length);
|
|
4104
4541
|
try {
|
|
4105
|
-
const xml = await readFile5(
|
|
4542
|
+
const xml = await readFile5(join11(PLIST_DIR, file), "utf8");
|
|
4106
4543
|
const cron = xml.match(/<key>StartCalendarInterval<\/key>[\s\S]*?<\/array>/)?.[0] ?? "";
|
|
4107
4544
|
const command = xml.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/)?.[1] ?? "";
|
|
4108
4545
|
const disabled = /<key>Disabled<\/key>\s*<true\/>/.test(xml);
|
|
@@ -4125,7 +4562,7 @@ var launchdAdapter = {
|
|
|
4125
4562
|
return new Promise((resolve2) => {
|
|
4126
4563
|
const args = entry.command.match(/(?:[^\s"]+|"[^"]*")+/g) ?? [entry.command];
|
|
4127
4564
|
const cmd = args.shift() ?? entry.command;
|
|
4128
|
-
const child =
|
|
4565
|
+
const child = spawn5(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
4129
4566
|
let stdoutTail = "";
|
|
4130
4567
|
let stderrTail = "";
|
|
4131
4568
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -4148,7 +4585,7 @@ var launchdAdapter = {
|
|
|
4148
4585
|
}
|
|
4149
4586
|
if (enabled) {
|
|
4150
4587
|
xml = xml.replace(/\s*<key>Disabled<\/key>\s*<true\/>/, "");
|
|
4151
|
-
await
|
|
4588
|
+
await writeFile10(path, xml);
|
|
4152
4589
|
try {
|
|
4153
4590
|
execFileSync9("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
|
|
4154
4591
|
} catch {
|
|
@@ -4160,7 +4597,7 @@ var launchdAdapter = {
|
|
|
4160
4597
|
"</dict>\n</plist>",
|
|
4161
4598
|
" <key>Disabled</key>\n <true/>\n</dict>\n</plist>"
|
|
4162
4599
|
);
|
|
4163
|
-
await
|
|
4600
|
+
await writeFile10(path, xml);
|
|
4164
4601
|
}
|
|
4165
4602
|
try {
|
|
4166
4603
|
execFileSync9("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
|
|
@@ -4171,7 +4608,7 @@ var launchdAdapter = {
|
|
|
4171
4608
|
};
|
|
4172
4609
|
|
|
4173
4610
|
// src/scheduler/cron.ts
|
|
4174
|
-
import { execFileSync as execFileSync10, spawn as
|
|
4611
|
+
import { execFileSync as execFileSync10, spawn as spawn6 } from "child_process";
|
|
4175
4612
|
|
|
4176
4613
|
// src/scheduler/safe-command.ts
|
|
4177
4614
|
var FORBIDDEN = /[;&|`$()<>\\]/;
|
|
@@ -4232,7 +4669,7 @@ function readCrontab() {
|
|
|
4232
4669
|
}
|
|
4233
4670
|
}
|
|
4234
4671
|
function writeCrontab(text) {
|
|
4235
|
-
const child =
|
|
4672
|
+
const child = spawn6("crontab", ["-"], { stdio: ["pipe", "inherit", "inherit"] });
|
|
4236
4673
|
child.stdin.write(text);
|
|
4237
4674
|
child.stdin.end();
|
|
4238
4675
|
}
|
|
@@ -4313,7 +4750,7 @@ var cronAdapter = {
|
|
|
4313
4750
|
return Promise.resolve({ exitCode: 1, stdoutTail: "", stderrTail: `rejected: ${reason}` });
|
|
4314
4751
|
}
|
|
4315
4752
|
return new Promise((resolve2) => {
|
|
4316
|
-
const child =
|
|
4753
|
+
const child = spawn6(parsed.bin, parsed.args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
4317
4754
|
let stdoutTail = "";
|
|
4318
4755
|
let stderrTail = "";
|
|
4319
4756
|
child.stdout?.on(
|
|
@@ -4341,7 +4778,7 @@ var cronAdapter = {
|
|
|
4341
4778
|
};
|
|
4342
4779
|
|
|
4343
4780
|
// src/scheduler/windows.ts
|
|
4344
|
-
import { execFileSync as execFileSync11, spawn as
|
|
4781
|
+
import { execFileSync as execFileSync11, spawn as spawn7 } from "child_process";
|
|
4345
4782
|
var TASK_PREFIX = "TaskCLI_";
|
|
4346
4783
|
function taskName(id) {
|
|
4347
4784
|
return `${TASK_PREFIX}${id.replace(/[^A-Za-z0-9_-]/g, "_")}`;
|
|
@@ -4454,7 +4891,7 @@ var windowsAdapter = {
|
|
|
4454
4891
|
return Promise.resolve({ exitCode: 1, stdoutTail: "", stderrTail: `rejected: ${reason}` });
|
|
4455
4892
|
}
|
|
4456
4893
|
return new Promise((resolve2) => {
|
|
4457
|
-
const child =
|
|
4894
|
+
const child = spawn7(parsed.bin, parsed.args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
4458
4895
|
let stdoutTail = "";
|
|
4459
4896
|
let stderrTail = "";
|
|
4460
4897
|
child.stdout?.on(
|
|
@@ -4513,10 +4950,10 @@ var unsupportedAdapter = {
|
|
|
4513
4950
|
};
|
|
4514
4951
|
|
|
4515
4952
|
// src/scheduler/registry.ts
|
|
4516
|
-
import { mkdir as
|
|
4953
|
+
import { mkdir as mkdir10, readFile as readFile6, writeFile as writeFile11 } from "fs/promises";
|
|
4517
4954
|
import { homedir as homedir7 } from "os";
|
|
4518
|
-
import { dirname as
|
|
4519
|
-
var REGISTRY_PATH =
|
|
4955
|
+
import { dirname as dirname5, join as join12 } from "path";
|
|
4956
|
+
var REGISTRY_PATH = join12(homedir7(), ".config", "task", "schedules.json");
|
|
4520
4957
|
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4521
4958
|
function looksLikeRegistryRow(value) {
|
|
4522
4959
|
if (!value || typeof value !== "object") return false;
|
|
@@ -4536,8 +4973,8 @@ async function readRegistry() {
|
|
|
4536
4973
|
}
|
|
4537
4974
|
}
|
|
4538
4975
|
async function writeRegistry(rows) {
|
|
4539
|
-
await
|
|
4540
|
-
await
|
|
4976
|
+
await mkdir10(dirname5(REGISTRY_PATH), { recursive: true });
|
|
4977
|
+
await writeFile11(REGISTRY_PATH, JSON.stringify(rows, null, 2));
|
|
4541
4978
|
}
|
|
4542
4979
|
async function upsertRegistry(row) {
|
|
4543
4980
|
if (!UUID_RE.test(row.id)) {
|
|
@@ -4625,7 +5062,7 @@ function registerScheduledTask(program2) {
|
|
|
4625
5062
|
const max = Math.min(100, Math.max(1, parseInt(opts.max, 10) || 5));
|
|
4626
5063
|
const command = opts.command ?? `task work --auto --silent --max ${max}`;
|
|
4627
5064
|
const { hostId, hostLabel } = getHostInfo();
|
|
4628
|
-
const id =
|
|
5065
|
+
const id = randomUUID4();
|
|
4629
5066
|
const created = await apiCall("POST", "/api/v1/cli/schedules", {
|
|
4630
5067
|
body: {
|
|
4631
5068
|
name,
|
|
@@ -4778,7 +5215,7 @@ function stripAnsi(s) {
|
|
|
4778
5215
|
// src/commands/runs.ts
|
|
4779
5216
|
import { readFile as readFile7 } from "fs/promises";
|
|
4780
5217
|
import { homedir as homedir8 } from "os";
|
|
4781
|
-
import { join as
|
|
5218
|
+
import { join as join13 } from "path";
|
|
4782
5219
|
function registerRuns(program2) {
|
|
4783
5220
|
const cmd = program2.command("runs").description("Inspect agentic CLI run history");
|
|
4784
5221
|
cmd.command("list").description("List recent runs").option("--limit <n>", "Max rows", "50").option("--ticket <id>", "Filter by ticket").option("--schedule <id>", "Filter by schedule").action(async (opts) => {
|
|
@@ -4807,7 +5244,7 @@ function registerRuns(program2) {
|
|
|
4807
5244
|
process.stdout.write(JSON.stringify(row, null, 2) + "\n");
|
|
4808
5245
|
});
|
|
4809
5246
|
cmd.command("logs <id>").description("Show captured agent output for a run, if available").action(async (id) => {
|
|
4810
|
-
const localPath =
|
|
5247
|
+
const localPath = join13(homedir8(), ".cache", "task", "runs", `${id}.log`);
|
|
4811
5248
|
try {
|
|
4812
5249
|
const text = await readFile7(localPath, "utf8");
|
|
4813
5250
|
process.stdout.write(text);
|
|
@@ -4880,8 +5317,8 @@ function registerConfig(program2) {
|
|
|
4880
5317
|
|
|
4881
5318
|
// src/commands/doctor.ts
|
|
4882
5319
|
import { execFileSync as execFileSync12 } from "child_process";
|
|
4883
|
-
import { readFile as readFile8, writeFile as
|
|
4884
|
-
import { join as
|
|
5320
|
+
import { readFile as readFile8, writeFile as writeFile12 } from "fs/promises";
|
|
5321
|
+
import { join as join14 } from "path";
|
|
4885
5322
|
import { request as request5 } from "undici";
|
|
4886
5323
|
var ALLOWED_TEST_EXECUTABLES = /* @__PURE__ */ new Set(["pnpm", "npm", "yarn", "bun", "node", "npx"]);
|
|
4887
5324
|
var DEFAULT_TEST_COMMAND = "pnpm typecheck";
|
|
@@ -5095,7 +5532,7 @@ async function checkPrePushTest(root, configuredCommand, fix) {
|
|
|
5095
5532
|
detail: `${command} (non-script executable, not statically verifiable)`
|
|
5096
5533
|
};
|
|
5097
5534
|
}
|
|
5098
|
-
const pkgPath =
|
|
5535
|
+
const pkgPath = join14(root, "package.json");
|
|
5099
5536
|
let pkgRaw;
|
|
5100
5537
|
try {
|
|
5101
5538
|
pkgRaw = await readFile8(pkgPath, "utf8");
|
|
@@ -5131,7 +5568,7 @@ async function checkPrePushTest(root, configuredCommand, fix) {
|
|
|
5131
5568
|
pkg.scripts = { ...scripts, typecheck: "tsc --noEmit" };
|
|
5132
5569
|
const indent = detectIndent(pkgRaw);
|
|
5133
5570
|
const trailingNewline = pkgRaw.endsWith("\n") ? "\n" : "";
|
|
5134
|
-
await
|
|
5571
|
+
await writeFile12(pkgPath, JSON.stringify(pkg, null, indent) + trailingNewline);
|
|
5135
5572
|
return {
|
|
5136
5573
|
name: "pre-push test",
|
|
5137
5574
|
ok: true,
|
|
@@ -5180,7 +5617,7 @@ function checkBinary(name, command) {
|
|
|
5180
5617
|
}
|
|
5181
5618
|
|
|
5182
5619
|
// src/commands/version.ts
|
|
5183
|
-
var CLI_VERSION = true ? "0.2.
|
|
5620
|
+
var CLI_VERSION = true ? "0.2.19" : "0.0.0-dev";
|
|
5184
5621
|
function registerVersion(program2) {
|
|
5185
5622
|
program2.command("version").description("Print the CLI version").action(() => {
|
|
5186
5623
|
process.stdout.write(CLI_VERSION + "\n");
|
|
@@ -5207,6 +5644,7 @@ registerMultiWork(program);
|
|
|
5207
5644
|
registerResume(program);
|
|
5208
5645
|
registerReset(program);
|
|
5209
5646
|
registerScan(program);
|
|
5647
|
+
registerFastTrack(program);
|
|
5210
5648
|
registerPrTest(program);
|
|
5211
5649
|
registerScheduledTask(program);
|
|
5212
5650
|
registerRuns(program);
|