@inteeka/task-cli 0.2.17 → 0.2.18
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 +509 -74
- 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
|
);
|
|
@@ -3760,6 +3948,252 @@ function clampInt(raw, min, max, fallback) {
|
|
|
3760
3948
|
return Math.min(v, max);
|
|
3761
3949
|
}
|
|
3762
3950
|
|
|
3951
|
+
// src/commands/fast-track.ts
|
|
3952
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
3953
|
+
import ora3 from "ora";
|
|
3954
|
+
function registerFastTrack(program2) {
|
|
3955
|
+
program2.command("fast-track").description(
|
|
3956
|
+
"End-to-end: scan + auto-approve + work on the next CLI-eligible ticket(s) in the linked project \u2014 no admin review step"
|
|
3957
|
+
).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(
|
|
3958
|
+
"--reset",
|
|
3959
|
+
"DESTRUCTIVE: discard local working-tree changes before the first ticket. Requires --confirm in non-TTY contexts."
|
|
3960
|
+
).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) => {
|
|
3961
|
+
await runFastTrack(opts);
|
|
3962
|
+
});
|
|
3963
|
+
}
|
|
3964
|
+
async function runFastTrack(opts) {
|
|
3965
|
+
let creds = await readCredentials();
|
|
3966
|
+
if (!creds) {
|
|
3967
|
+
throw new CliError(
|
|
3968
|
+
CLI_EXIT_CODES.MISCONFIGURATION,
|
|
3969
|
+
"Not signed in",
|
|
3970
|
+
"Run 'task login' to authenticate."
|
|
3971
|
+
);
|
|
3972
|
+
}
|
|
3973
|
+
creds = await ensureFreshAccessToken(creds);
|
|
3974
|
+
const localCfg = await readLocalConfig();
|
|
3975
|
+
const linkedProject = await readProjectConfig(findRepoRoot());
|
|
3976
|
+
if (!linkedProject) {
|
|
3977
|
+
throw new CliError(
|
|
3978
|
+
CLI_EXIT_CODES.MISCONFIGURATION,
|
|
3979
|
+
"No project link in this repo",
|
|
3980
|
+
"Run 'task link' first \u2014 fast-track operates on the linked project only."
|
|
3981
|
+
);
|
|
3982
|
+
}
|
|
3983
|
+
const apiUrl = (opts.apiUrl ?? process.env["TASK_API_URL"] ?? creds.api_url ?? localCfg.api_url ?? linkedProject.api_url ?? "http://localhost:3400").replace(/\/$/, "");
|
|
3984
|
+
const max = Math.max(1, parseInt(opts.max, 10) || 1);
|
|
3985
|
+
const silent = !!opts.silent || localCfg.silent;
|
|
3986
|
+
const ctx = await buildWorkContext({ max: "1", silent: opts.silent });
|
|
3987
|
+
const api = new AutopilotApi({ apiUrl, creds });
|
|
3988
|
+
const claudePath = localCfg.claude_path ?? void 0;
|
|
3989
|
+
let firstIteration = true;
|
|
3990
|
+
const results = [];
|
|
3991
|
+
for (let i = 0; i < max; i++) {
|
|
3992
|
+
const innerWorkOpts = {
|
|
3993
|
+
auto: false,
|
|
3994
|
+
// We pin a specific ticketId per iteration.
|
|
3995
|
+
next: false,
|
|
3996
|
+
max: "1",
|
|
3997
|
+
silent: opts.silent,
|
|
3998
|
+
...opts.dryRun ? { dryRun: true } : {},
|
|
3999
|
+
...opts.scheduleId ? { scheduleId: opts.scheduleId } : {},
|
|
4000
|
+
...firstIteration && opts.reset ? { reset: true } : {},
|
|
4001
|
+
...firstIteration && opts.confirm ? { confirm: true } : {}
|
|
4002
|
+
};
|
|
4003
|
+
const outcome = await fastTrackOneTicket({
|
|
4004
|
+
api,
|
|
4005
|
+
apiUrl,
|
|
4006
|
+
project: linkedProject,
|
|
4007
|
+
ctx,
|
|
4008
|
+
claudePath,
|
|
4009
|
+
silent,
|
|
4010
|
+
innerWorkOpts,
|
|
4011
|
+
scheduleId: opts.scheduleId
|
|
4012
|
+
});
|
|
4013
|
+
if (outcome.kind === "no_eligible") {
|
|
4014
|
+
if (results.length === 0 && !silent) {
|
|
4015
|
+
process.stdout.write(c.dim("No CLI-eligible tickets available to fast-track.\n"));
|
|
4016
|
+
}
|
|
4017
|
+
break;
|
|
4018
|
+
}
|
|
4019
|
+
results.push(outcome.result);
|
|
4020
|
+
firstIteration = false;
|
|
4021
|
+
if (i < max - 1 && outcome.result.status === "completed") {
|
|
4022
|
+
enforceBaseBranchClean(ctx.cwd, ctx.baseBranch);
|
|
4023
|
+
}
|
|
4024
|
+
}
|
|
4025
|
+
if (!silent) {
|
|
4026
|
+
summarise(results);
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
async function fastTrackOneTicket(args) {
|
|
4030
|
+
const { api, project, ctx, claudePath, silent, innerWorkOpts, scheduleId } = args;
|
|
4031
|
+
const issued = await api.issueSkillToken({
|
|
4032
|
+
project_id: project.project_id,
|
|
4033
|
+
max_submits: 1
|
|
4034
|
+
});
|
|
4035
|
+
const skillToken = issued.token;
|
|
4036
|
+
const prepared = await api.prepare(skillToken, 1, randomUUID3());
|
|
4037
|
+
if (prepared.tickets.length === 0) {
|
|
4038
|
+
return { kind: "no_eligible" };
|
|
4039
|
+
}
|
|
4040
|
+
const ticket = prepared.tickets[0];
|
|
4041
|
+
const nonce = prepared.prepare_nonce;
|
|
4042
|
+
const spinner = silent ? null : ora3(`#${ticket.sequence_number} ${ticket.title.slice(0, 60)} \u2014 scanning`).start();
|
|
4043
|
+
let generated;
|
|
4044
|
+
try {
|
|
4045
|
+
generated = await generateFixPromptJson({
|
|
4046
|
+
systemPrompt: ticket.system_prompt,
|
|
4047
|
+
repoOverviewBlock: ticket.repo_overview_block,
|
|
4048
|
+
ticketBlock: ticket.ticket_block,
|
|
4049
|
+
outputSchemaHint: ticket.output_schema_hint,
|
|
4050
|
+
modelId: ticket.model_id,
|
|
4051
|
+
ticketId: ticket.ticket_id,
|
|
4052
|
+
...claudePath ? { claudePath } : {}
|
|
4053
|
+
});
|
|
4054
|
+
} catch (err) {
|
|
4055
|
+
await api.abort(skillToken, [ticket.ticket_id]).catch(() => void 0);
|
|
4056
|
+
const reason = err instanceof LlmGenerationError ? `${err.reason}: ${err.message.slice(0, 200)}` : "";
|
|
4057
|
+
spinner?.fail(`#${ticket.sequence_number} scan failed${reason ? ` (${reason})` : ""}`);
|
|
4058
|
+
throw err instanceof CliError ? err : new CliError(
|
|
4059
|
+
CLI_EXIT_CODES.GENERIC_ERROR,
|
|
4060
|
+
`Fix-prompt generation failed for #${ticket.sequence_number}: ${err.message.slice(0, 200)}`
|
|
4061
|
+
);
|
|
4062
|
+
}
|
|
4063
|
+
const submitted = await api.submit({
|
|
4064
|
+
skillToken,
|
|
4065
|
+
nonce,
|
|
4066
|
+
ticketId: ticket.ticket_id,
|
|
4067
|
+
structured: generated.structured,
|
|
4068
|
+
inputTokens: generated.inputTokens,
|
|
4069
|
+
outputTokens: generated.outputTokens,
|
|
4070
|
+
model: ticket.model_id
|
|
4071
|
+
});
|
|
4072
|
+
if (submitted.status === "skip") {
|
|
4073
|
+
spinner?.warn(`#${ticket.sequence_number} server rejected submission (${submitted.reason})`);
|
|
4074
|
+
return {
|
|
4075
|
+
kind: "processed",
|
|
4076
|
+
result: {
|
|
4077
|
+
sequenceNumber: ticket.sequence_number,
|
|
4078
|
+
ticketId: ticket.ticket_id,
|
|
4079
|
+
previousFixStatus: submitted.reason,
|
|
4080
|
+
status: "scan_skipped",
|
|
4081
|
+
error: submitted.reason
|
|
4082
|
+
}
|
|
4083
|
+
};
|
|
4084
|
+
}
|
|
4085
|
+
const denylistHit = submitted.status === "needs_review";
|
|
4086
|
+
if (spinner) {
|
|
4087
|
+
spinner.text = `#${ticket.sequence_number} approving${denylistHit ? " (denylist override)" : ""}`;
|
|
4088
|
+
}
|
|
4089
|
+
let approved;
|
|
4090
|
+
try {
|
|
4091
|
+
approved = await apiCallOrThrow(
|
|
4092
|
+
"POST",
|
|
4093
|
+
`/api/v1/cli/me/tickets/${ticket.ticket_id}/ai-fix-prompt/fast-approve`,
|
|
4094
|
+
{
|
|
4095
|
+
body: {
|
|
4096
|
+
...denylistHit ? { denylist_acknowledged: true } : {},
|
|
4097
|
+
reason: `fast-track: ${denylistHit ? "denylist override" : "auto-approve"} by CLI session`
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
4100
|
+
);
|
|
4101
|
+
} catch (err) {
|
|
4102
|
+
spinner?.fail(
|
|
4103
|
+
`#${ticket.sequence_number} fast-approve failed: ${err.message.slice(0, 120)}`
|
|
4104
|
+
);
|
|
4105
|
+
throw err;
|
|
4106
|
+
}
|
|
4107
|
+
if (spinner) {
|
|
4108
|
+
spinner.text = `#${ticket.sequence_number} building`;
|
|
4109
|
+
}
|
|
4110
|
+
const innerOpts = { ...innerWorkOpts };
|
|
4111
|
+
if (scheduleId !== void 0) innerOpts.scheduleId = scheduleId;
|
|
4112
|
+
try {
|
|
4113
|
+
const outcome = await processOneTicket(ctx, innerOpts, ticket.ticket_id);
|
|
4114
|
+
if (outcome.kind === "no_eligible") {
|
|
4115
|
+
spinner?.warn(`#${ticket.sequence_number} unexpectedly invisible to work after approve`);
|
|
4116
|
+
return {
|
|
4117
|
+
kind: "processed",
|
|
4118
|
+
result: {
|
|
4119
|
+
sequenceNumber: ticket.sequence_number,
|
|
4120
|
+
ticketId: ticket.ticket_id,
|
|
4121
|
+
previousFixStatus: approved.from_status ?? "unknown",
|
|
4122
|
+
status: "failed",
|
|
4123
|
+
error: "ticket vanished between approve and claim"
|
|
4124
|
+
}
|
|
4125
|
+
};
|
|
4126
|
+
}
|
|
4127
|
+
if (outcome.kind === "completed") {
|
|
4128
|
+
spinner?.succeed(
|
|
4129
|
+
`#${ticket.sequence_number} PR opened${outcome.prUrl ? ` ${outcome.prUrl}` : ""}`
|
|
4130
|
+
);
|
|
4131
|
+
const result = {
|
|
4132
|
+
sequenceNumber: outcome.sequenceNumber,
|
|
4133
|
+
ticketId: ticket.ticket_id,
|
|
4134
|
+
previousFixStatus: approved.from_status ?? "unknown",
|
|
4135
|
+
status: "completed"
|
|
4136
|
+
};
|
|
4137
|
+
if (outcome.prNumber !== void 0) result.prNumber = outcome.prNumber;
|
|
4138
|
+
if (outcome.prUrl !== void 0) result.prUrl = outcome.prUrl;
|
|
4139
|
+
return { kind: "processed", result };
|
|
4140
|
+
}
|
|
4141
|
+
if (outcome.kind === "dry_run") {
|
|
4142
|
+
spinner?.succeed(`#${ticket.sequence_number} dry-run (no PR)`);
|
|
4143
|
+
return {
|
|
4144
|
+
kind: "processed",
|
|
4145
|
+
result: {
|
|
4146
|
+
sequenceNumber: outcome.sequenceNumber,
|
|
4147
|
+
ticketId: ticket.ticket_id,
|
|
4148
|
+
previousFixStatus: approved.from_status ?? "unknown",
|
|
4149
|
+
status: "dry_run"
|
|
4150
|
+
}
|
|
4151
|
+
};
|
|
4152
|
+
}
|
|
4153
|
+
spinner?.warn(`#${ticket.sequence_number} agent produced no changes`);
|
|
4154
|
+
return {
|
|
4155
|
+
kind: "processed",
|
|
4156
|
+
result: {
|
|
4157
|
+
sequenceNumber: outcome.sequenceNumber,
|
|
4158
|
+
ticketId: ticket.ticket_id,
|
|
4159
|
+
previousFixStatus: approved.from_status ?? "unknown",
|
|
4160
|
+
status: "no_changes"
|
|
4161
|
+
}
|
|
4162
|
+
};
|
|
4163
|
+
} catch (err) {
|
|
4164
|
+
spinner?.fail(
|
|
4165
|
+
`#${ticket.sequence_number} work failed: ${err.message.slice(0, 120)}`
|
|
4166
|
+
);
|
|
4167
|
+
throw err;
|
|
4168
|
+
}
|
|
4169
|
+
}
|
|
4170
|
+
function summarise(results) {
|
|
4171
|
+
if (results.length === 0) return;
|
|
4172
|
+
const counts = results.reduce(
|
|
4173
|
+
(acc, r) => {
|
|
4174
|
+
acc[r.status] = (acc[r.status] ?? 0) + 1;
|
|
4175
|
+
return acc;
|
|
4176
|
+
},
|
|
4177
|
+
{}
|
|
4178
|
+
);
|
|
4179
|
+
const parts = [];
|
|
4180
|
+
if (counts.completed) parts.push(`${c.ok(String(counts.completed))} PR(s) opened`);
|
|
4181
|
+
if (counts.dry_run) parts.push(`${c.dim(String(counts.dry_run))} dry-run`);
|
|
4182
|
+
if (counts.no_changes) parts.push(`${c.dim(String(counts.no_changes))} no-changes`);
|
|
4183
|
+
if (counts.scan_skipped) parts.push(`${c.warn(String(counts.scan_skipped))} scan skipped`);
|
|
4184
|
+
if (counts.failed) parts.push(`${c.err(String(counts.failed))} failed`);
|
|
4185
|
+
process.stdout.write(`
|
|
4186
|
+
${c.bold("Fast-track summary")}: ${parts.join(", ")}
|
|
4187
|
+
`);
|
|
4188
|
+
const denylistOverrides = results.filter((r) => r.previousFixStatus === "needs_review").length;
|
|
4189
|
+
if (denylistOverrides > 0) {
|
|
4190
|
+
process.stdout.write(
|
|
4191
|
+
`${c.warn("\u26A0")} ${denylistOverrides} ticket(s) bypassed the denylist gate \u2014 review the PR diff(s) carefully.
|
|
4192
|
+
`
|
|
4193
|
+
);
|
|
4194
|
+
}
|
|
4195
|
+
}
|
|
4196
|
+
|
|
3763
4197
|
// src/commands/pr-test.ts
|
|
3764
4198
|
import { execFileSync as execFileSync8 } from "child_process";
|
|
3765
4199
|
function registerPrTest(program2) {
|
|
@@ -3898,16 +4332,16 @@ ${c.err("\u2717 pr-test failed")}: ${err.message}
|
|
|
3898
4332
|
}
|
|
3899
4333
|
|
|
3900
4334
|
// src/commands/scheduled-task.ts
|
|
3901
|
-
import { randomUUID as
|
|
4335
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
3902
4336
|
|
|
3903
4337
|
// src/scheduler/index.ts
|
|
3904
4338
|
import { platform as platform2 } from "os";
|
|
3905
4339
|
|
|
3906
4340
|
// src/scheduler/launchd.ts
|
|
3907
|
-
import { mkdir as
|
|
4341
|
+
import { mkdir as mkdir9, readFile as readFile5, writeFile as writeFile10, unlink as unlink4, readdir as readdir2 } from "fs/promises";
|
|
3908
4342
|
import { homedir as homedir6 } from "os";
|
|
3909
|
-
import { join as
|
|
3910
|
-
import { execFileSync as execFileSync9, spawn as
|
|
4343
|
+
import { join as join11 } from "path";
|
|
4344
|
+
import { execFileSync as execFileSync9, spawn as spawn5 } from "child_process";
|
|
3911
4345
|
|
|
3912
4346
|
// src/scheduler/cron-translate.ts
|
|
3913
4347
|
function translateToLaunchd(cron) {
|
|
@@ -4008,14 +4442,14 @@ function expandField(field, min, max) {
|
|
|
4008
4442
|
}
|
|
4009
4443
|
|
|
4010
4444
|
// src/scheduler/launchd.ts
|
|
4011
|
-
var PLIST_DIR =
|
|
4445
|
+
var PLIST_DIR = join11(homedir6(), "Library", "LaunchAgents");
|
|
4012
4446
|
var LABEL_PREFIX = "com.inteeka.task.cli.";
|
|
4013
4447
|
var SAFE_ID_RE = /^[0-9a-zA-Z._-]+$/;
|
|
4014
4448
|
function plistPath(id) {
|
|
4015
4449
|
if (!SAFE_ID_RE.test(id) || id.includes("..")) {
|
|
4016
4450
|
throw new Error(`Refusing to compute plist path for unsafe id: ${id}`);
|
|
4017
4451
|
}
|
|
4018
|
-
return
|
|
4452
|
+
return join11(PLIST_DIR, `${LABEL_PREFIX}${id}.plist`);
|
|
4019
4453
|
}
|
|
4020
4454
|
function buildPlist(entry) {
|
|
4021
4455
|
const calendars = translateToLaunchd(entry.cron);
|
|
@@ -4051,9 +4485,9 @@ ${fields}
|
|
|
4051
4485
|
` <string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>`,
|
|
4052
4486
|
` </dict>`,
|
|
4053
4487
|
` <key>StandardOutPath</key>`,
|
|
4054
|
-
` <string>${escapeXml(
|
|
4488
|
+
` <string>${escapeXml(join11(homedir6(), ".cache", "task", "launchd-stdout.log"))}</string>`,
|
|
4055
4489
|
` <key>StandardErrorPath</key>`,
|
|
4056
|
-
` <string>${escapeXml(
|
|
4490
|
+
` <string>${escapeXml(join11(homedir6(), ".cache", "task", "launchd-stderr.log"))}</string>`,
|
|
4057
4491
|
!entry.enabled ? ` <key>Disabled</key>
|
|
4058
4492
|
<true/>` : "",
|
|
4059
4493
|
"</dict>",
|
|
@@ -4071,9 +4505,9 @@ function bootstrapDomain() {
|
|
|
4071
4505
|
}
|
|
4072
4506
|
var launchdAdapter = {
|
|
4073
4507
|
async upsert(entry) {
|
|
4074
|
-
await
|
|
4508
|
+
await mkdir9(PLIST_DIR, { recursive: true });
|
|
4075
4509
|
const path = plistPath(entry.id);
|
|
4076
|
-
await
|
|
4510
|
+
await writeFile10(path, buildPlist(entry));
|
|
4077
4511
|
try {
|
|
4078
4512
|
execFileSync9("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
|
|
4079
4513
|
} catch {
|
|
@@ -4102,7 +4536,7 @@ var launchdAdapter = {
|
|
|
4102
4536
|
for (const file of ours) {
|
|
4103
4537
|
const id = file.slice(LABEL_PREFIX.length, -".plist".length);
|
|
4104
4538
|
try {
|
|
4105
|
-
const xml = await readFile5(
|
|
4539
|
+
const xml = await readFile5(join11(PLIST_DIR, file), "utf8");
|
|
4106
4540
|
const cron = xml.match(/<key>StartCalendarInterval<\/key>[\s\S]*?<\/array>/)?.[0] ?? "";
|
|
4107
4541
|
const command = xml.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/)?.[1] ?? "";
|
|
4108
4542
|
const disabled = /<key>Disabled<\/key>\s*<true\/>/.test(xml);
|
|
@@ -4125,7 +4559,7 @@ var launchdAdapter = {
|
|
|
4125
4559
|
return new Promise((resolve2) => {
|
|
4126
4560
|
const args = entry.command.match(/(?:[^\s"]+|"[^"]*")+/g) ?? [entry.command];
|
|
4127
4561
|
const cmd = args.shift() ?? entry.command;
|
|
4128
|
-
const child =
|
|
4562
|
+
const child = spawn5(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
4129
4563
|
let stdoutTail = "";
|
|
4130
4564
|
let stderrTail = "";
|
|
4131
4565
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -4148,7 +4582,7 @@ var launchdAdapter = {
|
|
|
4148
4582
|
}
|
|
4149
4583
|
if (enabled) {
|
|
4150
4584
|
xml = xml.replace(/\s*<key>Disabled<\/key>\s*<true\/>/, "");
|
|
4151
|
-
await
|
|
4585
|
+
await writeFile10(path, xml);
|
|
4152
4586
|
try {
|
|
4153
4587
|
execFileSync9("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
|
|
4154
4588
|
} catch {
|
|
@@ -4160,7 +4594,7 @@ var launchdAdapter = {
|
|
|
4160
4594
|
"</dict>\n</plist>",
|
|
4161
4595
|
" <key>Disabled</key>\n <true/>\n</dict>\n</plist>"
|
|
4162
4596
|
);
|
|
4163
|
-
await
|
|
4597
|
+
await writeFile10(path, xml);
|
|
4164
4598
|
}
|
|
4165
4599
|
try {
|
|
4166
4600
|
execFileSync9("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
|
|
@@ -4171,7 +4605,7 @@ var launchdAdapter = {
|
|
|
4171
4605
|
};
|
|
4172
4606
|
|
|
4173
4607
|
// src/scheduler/cron.ts
|
|
4174
|
-
import { execFileSync as execFileSync10, spawn as
|
|
4608
|
+
import { execFileSync as execFileSync10, spawn as spawn6 } from "child_process";
|
|
4175
4609
|
|
|
4176
4610
|
// src/scheduler/safe-command.ts
|
|
4177
4611
|
var FORBIDDEN = /[;&|`$()<>\\]/;
|
|
@@ -4232,7 +4666,7 @@ function readCrontab() {
|
|
|
4232
4666
|
}
|
|
4233
4667
|
}
|
|
4234
4668
|
function writeCrontab(text) {
|
|
4235
|
-
const child =
|
|
4669
|
+
const child = spawn6("crontab", ["-"], { stdio: ["pipe", "inherit", "inherit"] });
|
|
4236
4670
|
child.stdin.write(text);
|
|
4237
4671
|
child.stdin.end();
|
|
4238
4672
|
}
|
|
@@ -4313,7 +4747,7 @@ var cronAdapter = {
|
|
|
4313
4747
|
return Promise.resolve({ exitCode: 1, stdoutTail: "", stderrTail: `rejected: ${reason}` });
|
|
4314
4748
|
}
|
|
4315
4749
|
return new Promise((resolve2) => {
|
|
4316
|
-
const child =
|
|
4750
|
+
const child = spawn6(parsed.bin, parsed.args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
4317
4751
|
let stdoutTail = "";
|
|
4318
4752
|
let stderrTail = "";
|
|
4319
4753
|
child.stdout?.on(
|
|
@@ -4341,7 +4775,7 @@ var cronAdapter = {
|
|
|
4341
4775
|
};
|
|
4342
4776
|
|
|
4343
4777
|
// src/scheduler/windows.ts
|
|
4344
|
-
import { execFileSync as execFileSync11, spawn as
|
|
4778
|
+
import { execFileSync as execFileSync11, spawn as spawn7 } from "child_process";
|
|
4345
4779
|
var TASK_PREFIX = "TaskCLI_";
|
|
4346
4780
|
function taskName(id) {
|
|
4347
4781
|
return `${TASK_PREFIX}${id.replace(/[^A-Za-z0-9_-]/g, "_")}`;
|
|
@@ -4454,7 +4888,7 @@ var windowsAdapter = {
|
|
|
4454
4888
|
return Promise.resolve({ exitCode: 1, stdoutTail: "", stderrTail: `rejected: ${reason}` });
|
|
4455
4889
|
}
|
|
4456
4890
|
return new Promise((resolve2) => {
|
|
4457
|
-
const child =
|
|
4891
|
+
const child = spawn7(parsed.bin, parsed.args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
4458
4892
|
let stdoutTail = "";
|
|
4459
4893
|
let stderrTail = "";
|
|
4460
4894
|
child.stdout?.on(
|
|
@@ -4513,10 +4947,10 @@ var unsupportedAdapter = {
|
|
|
4513
4947
|
};
|
|
4514
4948
|
|
|
4515
4949
|
// src/scheduler/registry.ts
|
|
4516
|
-
import { mkdir as
|
|
4950
|
+
import { mkdir as mkdir10, readFile as readFile6, writeFile as writeFile11 } from "fs/promises";
|
|
4517
4951
|
import { homedir as homedir7 } from "os";
|
|
4518
|
-
import { dirname as
|
|
4519
|
-
var REGISTRY_PATH =
|
|
4952
|
+
import { dirname as dirname5, join as join12 } from "path";
|
|
4953
|
+
var REGISTRY_PATH = join12(homedir7(), ".config", "task", "schedules.json");
|
|
4520
4954
|
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
4955
|
function looksLikeRegistryRow(value) {
|
|
4522
4956
|
if (!value || typeof value !== "object") return false;
|
|
@@ -4536,8 +4970,8 @@ async function readRegistry() {
|
|
|
4536
4970
|
}
|
|
4537
4971
|
}
|
|
4538
4972
|
async function writeRegistry(rows) {
|
|
4539
|
-
await
|
|
4540
|
-
await
|
|
4973
|
+
await mkdir10(dirname5(REGISTRY_PATH), { recursive: true });
|
|
4974
|
+
await writeFile11(REGISTRY_PATH, JSON.stringify(rows, null, 2));
|
|
4541
4975
|
}
|
|
4542
4976
|
async function upsertRegistry(row) {
|
|
4543
4977
|
if (!UUID_RE.test(row.id)) {
|
|
@@ -4625,7 +5059,7 @@ function registerScheduledTask(program2) {
|
|
|
4625
5059
|
const max = Math.min(100, Math.max(1, parseInt(opts.max, 10) || 5));
|
|
4626
5060
|
const command = opts.command ?? `task work --auto --silent --max ${max}`;
|
|
4627
5061
|
const { hostId, hostLabel } = getHostInfo();
|
|
4628
|
-
const id =
|
|
5062
|
+
const id = randomUUID4();
|
|
4629
5063
|
const created = await apiCall("POST", "/api/v1/cli/schedules", {
|
|
4630
5064
|
body: {
|
|
4631
5065
|
name,
|
|
@@ -4778,7 +5212,7 @@ function stripAnsi(s) {
|
|
|
4778
5212
|
// src/commands/runs.ts
|
|
4779
5213
|
import { readFile as readFile7 } from "fs/promises";
|
|
4780
5214
|
import { homedir as homedir8 } from "os";
|
|
4781
|
-
import { join as
|
|
5215
|
+
import { join as join13 } from "path";
|
|
4782
5216
|
function registerRuns(program2) {
|
|
4783
5217
|
const cmd = program2.command("runs").description("Inspect agentic CLI run history");
|
|
4784
5218
|
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 +5241,7 @@ function registerRuns(program2) {
|
|
|
4807
5241
|
process.stdout.write(JSON.stringify(row, null, 2) + "\n");
|
|
4808
5242
|
});
|
|
4809
5243
|
cmd.command("logs <id>").description("Show captured agent output for a run, if available").action(async (id) => {
|
|
4810
|
-
const localPath =
|
|
5244
|
+
const localPath = join13(homedir8(), ".cache", "task", "runs", `${id}.log`);
|
|
4811
5245
|
try {
|
|
4812
5246
|
const text = await readFile7(localPath, "utf8");
|
|
4813
5247
|
process.stdout.write(text);
|
|
@@ -4880,8 +5314,8 @@ function registerConfig(program2) {
|
|
|
4880
5314
|
|
|
4881
5315
|
// src/commands/doctor.ts
|
|
4882
5316
|
import { execFileSync as execFileSync12 } from "child_process";
|
|
4883
|
-
import { readFile as readFile8, writeFile as
|
|
4884
|
-
import { join as
|
|
5317
|
+
import { readFile as readFile8, writeFile as writeFile12 } from "fs/promises";
|
|
5318
|
+
import { join as join14 } from "path";
|
|
4885
5319
|
import { request as request5 } from "undici";
|
|
4886
5320
|
var ALLOWED_TEST_EXECUTABLES = /* @__PURE__ */ new Set(["pnpm", "npm", "yarn", "bun", "node", "npx"]);
|
|
4887
5321
|
var DEFAULT_TEST_COMMAND = "pnpm typecheck";
|
|
@@ -5095,7 +5529,7 @@ async function checkPrePushTest(root, configuredCommand, fix) {
|
|
|
5095
5529
|
detail: `${command} (non-script executable, not statically verifiable)`
|
|
5096
5530
|
};
|
|
5097
5531
|
}
|
|
5098
|
-
const pkgPath =
|
|
5532
|
+
const pkgPath = join14(root, "package.json");
|
|
5099
5533
|
let pkgRaw;
|
|
5100
5534
|
try {
|
|
5101
5535
|
pkgRaw = await readFile8(pkgPath, "utf8");
|
|
@@ -5131,7 +5565,7 @@ async function checkPrePushTest(root, configuredCommand, fix) {
|
|
|
5131
5565
|
pkg.scripts = { ...scripts, typecheck: "tsc --noEmit" };
|
|
5132
5566
|
const indent = detectIndent(pkgRaw);
|
|
5133
5567
|
const trailingNewline = pkgRaw.endsWith("\n") ? "\n" : "";
|
|
5134
|
-
await
|
|
5568
|
+
await writeFile12(pkgPath, JSON.stringify(pkg, null, indent) + trailingNewline);
|
|
5135
5569
|
return {
|
|
5136
5570
|
name: "pre-push test",
|
|
5137
5571
|
ok: true,
|
|
@@ -5180,7 +5614,7 @@ function checkBinary(name, command) {
|
|
|
5180
5614
|
}
|
|
5181
5615
|
|
|
5182
5616
|
// src/commands/version.ts
|
|
5183
|
-
var CLI_VERSION = true ? "0.2.
|
|
5617
|
+
var CLI_VERSION = true ? "0.2.18" : "0.0.0-dev";
|
|
5184
5618
|
function registerVersion(program2) {
|
|
5185
5619
|
program2.command("version").description("Print the CLI version").action(() => {
|
|
5186
5620
|
process.stdout.write(CLI_VERSION + "\n");
|
|
@@ -5207,6 +5641,7 @@ registerMultiWork(program);
|
|
|
5207
5641
|
registerResume(program);
|
|
5208
5642
|
registerReset(program);
|
|
5209
5643
|
registerScan(program);
|
|
5644
|
+
registerFastTrack(program);
|
|
5210
5645
|
registerPrTest(program);
|
|
5211
5646
|
registerScheduledTask(program);
|
|
5212
5647
|
registerRuns(program);
|