@inteeka/task-cli 0.2.16 → 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 +623 -121
- package/dist/cli.js.map +1 -1
- package/package.json +4 -2
package/dist/cli.js
CHANGED
|
@@ -247,7 +247,7 @@ import open from "open";
|
|
|
247
247
|
import ora from "ora";
|
|
248
248
|
|
|
249
249
|
// src/config/credentials.ts
|
|
250
|
-
import { mkdir, readFile, writeFile, unlink, chmod, stat } from "fs/promises";
|
|
250
|
+
import { mkdir, readFile, writeFile, unlink, chmod, stat, rename } from "fs/promises";
|
|
251
251
|
import { homedir } from "os";
|
|
252
252
|
import { dirname, join } from "path";
|
|
253
253
|
var CONFIG_DIR = join(homedir(), ".config", "task");
|
|
@@ -289,8 +289,10 @@ async function readCredentials() {
|
|
|
289
289
|
}
|
|
290
290
|
async function writeCredentials(creds) {
|
|
291
291
|
await ensureDir(dirname(CREDENTIALS_PATH));
|
|
292
|
-
|
|
293
|
-
await
|
|
292
|
+
const tmpPath = `${CREDENTIALS_PATH}.${process.pid}.tmp`;
|
|
293
|
+
await writeFile(tmpPath, JSON.stringify(creds, null, 2), { mode: 384 });
|
|
294
|
+
await chmod(tmpPath, 384);
|
|
295
|
+
await rename(tmpPath, CREDENTIALS_PATH);
|
|
294
296
|
}
|
|
295
297
|
async function clearCredentials() {
|
|
296
298
|
try {
|
|
@@ -299,6 +301,7 @@ async function clearCredentials() {
|
|
|
299
301
|
if (err.code !== "ENOENT") throw err;
|
|
300
302
|
}
|
|
301
303
|
}
|
|
304
|
+
var CREDENTIALS_DIR = CONFIG_DIR;
|
|
302
305
|
|
|
303
306
|
// src/util/host.ts
|
|
304
307
|
import { createHash } from "crypto";
|
|
@@ -449,13 +452,34 @@ function sleep(ms) {
|
|
|
449
452
|
|
|
450
453
|
// src/api/client.ts
|
|
451
454
|
import { request as request3 } from "undici";
|
|
452
|
-
import { mkdir as
|
|
455
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
453
456
|
import { homedir as homedir2 } from "os";
|
|
454
|
-
import { join as
|
|
457
|
+
import { join as join3 } from "path";
|
|
455
458
|
|
|
456
459
|
// src/auth/refresh.ts
|
|
457
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";
|
|
458
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
|
+
}
|
|
459
483
|
async function ensureFreshAccessToken(creds) {
|
|
460
484
|
const expiresMs = new Date(creds.access_expires_at).getTime();
|
|
461
485
|
if (Number.isFinite(expiresMs) && expiresMs - Date.now() > REFRESH_LEEWAY_MS) {
|
|
@@ -464,6 +488,18 @@ async function ensureFreshAccessToken(creds) {
|
|
|
464
488
|
return performRefresh(creds);
|
|
465
489
|
}
|
|
466
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) {
|
|
467
503
|
const { hostId } = getHostInfo();
|
|
468
504
|
const apiUrl = creds.api_url.replace(/\/$/, "");
|
|
469
505
|
const res = await request2(`${apiUrl}/api/v1/cli/auth/refresh`, {
|
|
@@ -481,6 +517,16 @@ async function performRefresh(creds) {
|
|
|
481
517
|
headersTimeout: 15e3
|
|
482
518
|
});
|
|
483
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
|
+
}
|
|
484
530
|
await clearCredentials();
|
|
485
531
|
throw new CliError(
|
|
486
532
|
CLI_EXIT_CODES.UNAUTHORISED,
|
|
@@ -518,13 +564,13 @@ async function manualRefresh() {
|
|
|
518
564
|
// src/api/client.ts
|
|
519
565
|
async function dumpServerError(method, path, status, rawBody, headers) {
|
|
520
566
|
try {
|
|
521
|
-
const dir =
|
|
522
|
-
await
|
|
523
|
-
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`);
|
|
524
570
|
const safeHeaders = { ...headers };
|
|
525
571
|
delete safeHeaders["Authorization"];
|
|
526
572
|
delete safeHeaders["authorization"];
|
|
527
|
-
await
|
|
573
|
+
await writeFile3(
|
|
528
574
|
file,
|
|
529
575
|
[
|
|
530
576
|
`## ${method} ${path}`,
|
|
@@ -666,10 +712,10 @@ async function apiCallOrThrow(method, path, options = {}) {
|
|
|
666
712
|
}
|
|
667
713
|
|
|
668
714
|
// src/config/local-config.ts
|
|
669
|
-
import { mkdir as
|
|
715
|
+
import { mkdir as mkdir4, readFile as readFile2, writeFile as writeFile4 } from "fs/promises";
|
|
670
716
|
import { homedir as homedir3 } from "os";
|
|
671
|
-
import { dirname as dirname2, join as
|
|
672
|
-
var CONFIG_PATH =
|
|
717
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
718
|
+
var CONFIG_PATH = join4(homedir3(), ".config", "task", "config.json");
|
|
673
719
|
var DEFAULT_CONFIG = {
|
|
674
720
|
api_url: process.env["TASK_API_URL"] ?? "http://localhost:3400",
|
|
675
721
|
default_project: null,
|
|
@@ -689,8 +735,8 @@ async function readLocalConfig() {
|
|
|
689
735
|
}
|
|
690
736
|
}
|
|
691
737
|
async function writeLocalConfig(config) {
|
|
692
|
-
await
|
|
693
|
-
await
|
|
738
|
+
await mkdir4(dirname2(CONFIG_PATH), { recursive: true });
|
|
739
|
+
await writeFile4(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
694
740
|
}
|
|
695
741
|
async function setConfigValue(key, value) {
|
|
696
742
|
const cfg = await readLocalConfig();
|
|
@@ -809,14 +855,14 @@ function registerAuthRefresh(program2) {
|
|
|
809
855
|
}
|
|
810
856
|
|
|
811
857
|
// src/commands/link.ts
|
|
812
|
-
import { readFile as readFile4, writeFile as
|
|
858
|
+
import { readFile as readFile4, writeFile as writeFile6, appendFile, access } from "fs/promises";
|
|
813
859
|
import { constants as fsConstants } from "fs";
|
|
814
|
-
import { join as
|
|
860
|
+
import { join as join6 } from "path";
|
|
815
861
|
import inquirer from "inquirer";
|
|
816
862
|
|
|
817
863
|
// src/config/project.ts
|
|
818
|
-
import { mkdir as
|
|
819
|
-
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";
|
|
820
866
|
import { execSync } from "child_process";
|
|
821
867
|
function findRepoRoot(start = process.cwd()) {
|
|
822
868
|
try {
|
|
@@ -827,7 +873,7 @@ function findRepoRoot(start = process.cwd()) {
|
|
|
827
873
|
}
|
|
828
874
|
}
|
|
829
875
|
function configPath(repoRoot) {
|
|
830
|
-
return
|
|
876
|
+
return join5(repoRoot ?? findRepoRoot(), ".task", "config.json");
|
|
831
877
|
}
|
|
832
878
|
async function readProjectConfig(repoRoot) {
|
|
833
879
|
const path = configPath(repoRoot);
|
|
@@ -841,8 +887,8 @@ async function readProjectConfig(repoRoot) {
|
|
|
841
887
|
}
|
|
842
888
|
async function writeProjectConfig(config, repoRoot) {
|
|
843
889
|
const path = configPath(repoRoot);
|
|
844
|
-
await
|
|
845
|
-
await
|
|
890
|
+
await mkdir5(dirname3(path), { recursive: true });
|
|
891
|
+
await writeFile5(path, JSON.stringify(config, null, 2));
|
|
846
892
|
}
|
|
847
893
|
async function clearProjectConfig(repoRoot) {
|
|
848
894
|
const path = configPath(repoRoot);
|
|
@@ -941,7 +987,7 @@ async function resolveProject(projects, opts) {
|
|
|
941
987
|
return picked;
|
|
942
988
|
}
|
|
943
989
|
async function ensureGitignored(repoRoot) {
|
|
944
|
-
const gitignorePath =
|
|
990
|
+
const gitignorePath = join6(repoRoot, ".gitignore");
|
|
945
991
|
let existing = null;
|
|
946
992
|
try {
|
|
947
993
|
await access(gitignorePath, fsConstants.F_OK);
|
|
@@ -956,7 +1002,7 @@ async function ensureGitignored(repoRoot) {
|
|
|
956
1002
|
await appendFile(gitignorePath, block);
|
|
957
1003
|
return "added";
|
|
958
1004
|
}
|
|
959
|
-
await
|
|
1005
|
+
await writeFile6(gitignorePath, "# task CLI link config\n.task/\n");
|
|
960
1006
|
return "created";
|
|
961
1007
|
}
|
|
962
1008
|
|
|
@@ -1468,9 +1514,9 @@ import inquirer2 from "inquirer";
|
|
|
1468
1514
|
|
|
1469
1515
|
// src/agent/agent-service.ts
|
|
1470
1516
|
import { spawn } from "child_process";
|
|
1471
|
-
import { mkdir as
|
|
1517
|
+
import { mkdir as mkdir6, writeFile as writeFile7 } from "fs/promises";
|
|
1472
1518
|
import { homedir as homedir4 } from "os";
|
|
1473
|
-
import { join as
|
|
1519
|
+
import { join as join7 } from "path";
|
|
1474
1520
|
|
|
1475
1521
|
// src/agent/allowed-tools.ts
|
|
1476
1522
|
var ALLOWED_TOOLS = CLI_ALLOWED_TOOLS;
|
|
@@ -1527,10 +1573,10 @@ async function runAgent(args) {
|
|
|
1527
1573
|
let outputLogPath = null;
|
|
1528
1574
|
let logHandle = null;
|
|
1529
1575
|
if (args.silent) {
|
|
1530
|
-
const dir =
|
|
1531
|
-
await
|
|
1532
|
-
outputLogPath =
|
|
1533
|
-
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, "");
|
|
1534
1580
|
const { createWriteStream } = await import("fs");
|
|
1535
1581
|
logHandle = createWriteStream(outputLogPath, { flags: "a" });
|
|
1536
1582
|
}
|
|
@@ -1731,11 +1777,111 @@ async function runProjectTest(args) {
|
|
|
1731
1777
|
});
|
|
1732
1778
|
}
|
|
1733
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
|
+
|
|
1734
1880
|
// src/util/progress.ts
|
|
1735
|
-
import { mkdir as
|
|
1881
|
+
import { mkdir as mkdir7, writeFile as writeFile8, rename as rename2, unlink as unlink3, readdir, stat as stat3 } from "fs/promises";
|
|
1736
1882
|
import { tmpdir } from "os";
|
|
1737
|
-
import { join as
|
|
1738
|
-
var PROGRESS_DIR =
|
|
1883
|
+
import { join as join9 } from "path";
|
|
1884
|
+
var PROGRESS_DIR = join9(tmpdir(), "task-progress");
|
|
1739
1885
|
var STALE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
1740
1886
|
var ProgressWriter = class {
|
|
1741
1887
|
path;
|
|
@@ -1748,7 +1894,7 @@ var ProgressWriter = class {
|
|
|
1748
1894
|
this.ticketId = ticketId;
|
|
1749
1895
|
const deliveryId = process.env["TASK_DELIVERY_ID"]?.trim();
|
|
1750
1896
|
const fileBase = deliveryId && deliveryId.length > 0 ? deliveryId : `manual-${process.pid}`;
|
|
1751
|
-
this.path =
|
|
1897
|
+
this.path = join9(PROGRESS_DIR, `${fileBase}.json`);
|
|
1752
1898
|
}
|
|
1753
1899
|
/** Switch the in-flight ticket between phases (used by `task scan` which iterates). */
|
|
1754
1900
|
setTicketId(ticketId) {
|
|
@@ -1782,13 +1928,13 @@ var ProgressWriter = class {
|
|
|
1782
1928
|
});
|
|
1783
1929
|
}
|
|
1784
1930
|
async writeAtomic(payload) {
|
|
1785
|
-
await
|
|
1931
|
+
await mkdir7(PROGRESS_DIR, { recursive: true }).catch(() => {
|
|
1786
1932
|
});
|
|
1787
1933
|
const body = JSON.stringify(payload);
|
|
1788
1934
|
const tmp = `${this.path}.tmp`;
|
|
1789
1935
|
try {
|
|
1790
|
-
await
|
|
1791
|
-
await
|
|
1936
|
+
await writeFile8(tmp, body, { encoding: "utf8", mode: 384 });
|
|
1937
|
+
await rename2(tmp, this.path);
|
|
1792
1938
|
} catch {
|
|
1793
1939
|
await unlink3(tmp).catch(() => {
|
|
1794
1940
|
});
|
|
@@ -1804,9 +1950,9 @@ var ProgressWriter = class {
|
|
|
1804
1950
|
const cutoff = Date.now() - STALE_MAX_AGE_MS;
|
|
1805
1951
|
await Promise.all(
|
|
1806
1952
|
entries.filter((name) => name.endsWith(".json") || name.endsWith(".json.tmp")).map(async (name) => {
|
|
1807
|
-
const p =
|
|
1953
|
+
const p = join9(PROGRESS_DIR, name);
|
|
1808
1954
|
try {
|
|
1809
|
-
const s = await
|
|
1955
|
+
const s = await stat3(p);
|
|
1810
1956
|
if (s.mtimeMs < cutoff) {
|
|
1811
1957
|
await unlink3(p).catch(() => {
|
|
1812
1958
|
});
|
|
@@ -2170,6 +2316,50 @@ ${detail.ai_fix_approval_notes}` : ""
|
|
|
2170
2316
|
await progress.setPhase("testing", {
|
|
2171
2317
|
detail: testCommand ?? "pnpm typecheck"
|
|
2172
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
|
+
}
|
|
2173
2363
|
if (!silent)
|
|
2174
2364
|
process.stdout.write(c.dim(` running pre-push test: ${testCommand ?? "pnpm typecheck"}
|
|
2175
2365
|
`));
|
|
@@ -3050,14 +3240,17 @@ async function jsonRequest(url, init) {
|
|
|
3050
3240
|
};
|
|
3051
3241
|
}
|
|
3052
3242
|
var AutopilotApi = class {
|
|
3243
|
+
creds;
|
|
3053
3244
|
constructor(opts) {
|
|
3054
|
-
this.
|
|
3245
|
+
this.creds = opts.creds;
|
|
3246
|
+
this.apiUrl = opts.apiUrl;
|
|
3055
3247
|
}
|
|
3056
|
-
|
|
3248
|
+
apiUrl;
|
|
3249
|
+
async userHeaders() {
|
|
3250
|
+
this.creds = await ensureFreshAccessToken(this.creds);
|
|
3057
3251
|
return {
|
|
3058
3252
|
"Content-Type": "application/json",
|
|
3059
|
-
Authorization: `Bearer ${this.
|
|
3060
|
-
"X-Actor-Email": this.opts.actorEmail,
|
|
3253
|
+
Authorization: `Bearer ${this.creds.access_token}`,
|
|
3061
3254
|
"User-Agent": "task-cli/scan"
|
|
3062
3255
|
};
|
|
3063
3256
|
}
|
|
@@ -3070,12 +3263,13 @@ var AutopilotApi = class {
|
|
|
3070
3263
|
};
|
|
3071
3264
|
}
|
|
3072
3265
|
async listEligibleProjects() {
|
|
3073
|
-
const url = `${this.
|
|
3266
|
+
const url = `${this.apiUrl}/api/v1/cli/projects`;
|
|
3074
3267
|
const result = await jsonRequest(url, {
|
|
3075
3268
|
method: "GET",
|
|
3076
|
-
headers: this.
|
|
3269
|
+
headers: await this.userHeaders()
|
|
3077
3270
|
});
|
|
3078
3271
|
if (!result.ok) {
|
|
3272
|
+
await handleUserAuthFailure(result.code, result.status);
|
|
3079
3273
|
throw new CliError(
|
|
3080
3274
|
autopilotExitCode(result.code, result.status),
|
|
3081
3275
|
`${result.code}: ${result.message}`
|
|
@@ -3084,10 +3278,10 @@ var AutopilotApi = class {
|
|
|
3084
3278
|
return result.data ?? [];
|
|
3085
3279
|
}
|
|
3086
3280
|
async issueSkillToken(args) {
|
|
3087
|
-
const url = `${this.
|
|
3281
|
+
const url = `${this.apiUrl}/api/v1/cli/issue-skill-token`;
|
|
3088
3282
|
const result = await jsonRequest(url, {
|
|
3089
3283
|
method: "POST",
|
|
3090
|
-
headers: this.
|
|
3284
|
+
headers: await this.userHeaders(),
|
|
3091
3285
|
body: {
|
|
3092
3286
|
project_id: args.project_id,
|
|
3093
3287
|
scope: "fix_prompt_sync",
|
|
@@ -3096,6 +3290,7 @@ var AutopilotApi = class {
|
|
|
3096
3290
|
}
|
|
3097
3291
|
});
|
|
3098
3292
|
if (!result.ok) {
|
|
3293
|
+
await handleUserAuthFailure(result.code, result.status);
|
|
3099
3294
|
throw new CliError(
|
|
3100
3295
|
autopilotExitCode(result.code, result.status),
|
|
3101
3296
|
`${result.code}: ${result.message}`
|
|
@@ -3104,7 +3299,7 @@ var AutopilotApi = class {
|
|
|
3104
3299
|
return result.data;
|
|
3105
3300
|
}
|
|
3106
3301
|
async prepare(skillToken, batchSize, idempotencyKey) {
|
|
3107
|
-
const url = `${this.
|
|
3302
|
+
const url = `${this.apiUrl}/api/v1/cli/fix-prompt-sync/prepare`;
|
|
3108
3303
|
const result = await jsonRequest(url, {
|
|
3109
3304
|
method: "POST",
|
|
3110
3305
|
headers: this.skillHeaders(skillToken, { "Idempotency-Key": idempotencyKey }),
|
|
@@ -3122,7 +3317,7 @@ var AutopilotApi = class {
|
|
|
3122
3317
|
return result.data;
|
|
3123
3318
|
}
|
|
3124
3319
|
async submit(args) {
|
|
3125
|
-
const url = `${this.
|
|
3320
|
+
const url = `${this.apiUrl}/api/v1/cli/fix-prompt-sync/submit`;
|
|
3126
3321
|
const result = await jsonRequest(url, {
|
|
3127
3322
|
method: "POST",
|
|
3128
3323
|
headers: this.skillHeaders(args.skillToken, { "X-Prepare-Nonce": args.nonce }),
|
|
@@ -3148,7 +3343,7 @@ var AutopilotApi = class {
|
|
|
3148
3343
|
}
|
|
3149
3344
|
async abort(skillToken, ticketIds) {
|
|
3150
3345
|
if (ticketIds.length === 0) return;
|
|
3151
|
-
const url = `${this.
|
|
3346
|
+
const url = `${this.apiUrl}/api/v1/cli/fix-prompt-sync/abort`;
|
|
3152
3347
|
await jsonRequest(url, {
|
|
3153
3348
|
method: "POST",
|
|
3154
3349
|
headers: this.skillHeaders(skillToken),
|
|
@@ -3156,7 +3351,7 @@ var AutopilotApi = class {
|
|
|
3156
3351
|
}).catch(() => void 0);
|
|
3157
3352
|
}
|
|
3158
3353
|
async runSummary(skillToken, summary) {
|
|
3159
|
-
const url = `${this.
|
|
3354
|
+
const url = `${this.apiUrl}/api/v1/cli/fix-prompt-sync/run-summary`;
|
|
3160
3355
|
await jsonRequest(url, {
|
|
3161
3356
|
method: "POST",
|
|
3162
3357
|
headers: this.skillHeaders(skillToken),
|
|
@@ -3164,6 +3359,24 @@ var AutopilotApi = class {
|
|
|
3164
3359
|
}).catch(() => void 0);
|
|
3165
3360
|
}
|
|
3166
3361
|
};
|
|
3362
|
+
async function handleUserAuthFailure(code, status) {
|
|
3363
|
+
if (status === 401 && (code === "UNAUTHORIZED" || code === "TOKEN_EXPIRED")) {
|
|
3364
|
+
await clearCredentials();
|
|
3365
|
+
throw new CliError(
|
|
3366
|
+
CLI_EXIT_CODES.UNAUTHORISED,
|
|
3367
|
+
"Your CLI session is no longer valid",
|
|
3368
|
+
"Run 'task login' to authenticate again."
|
|
3369
|
+
);
|
|
3370
|
+
}
|
|
3371
|
+
if (status === 403 && code === "CLI_ACCESS_REVOKED") {
|
|
3372
|
+
await clearCredentials();
|
|
3373
|
+
throw new CliError(
|
|
3374
|
+
CLI_EXIT_CODES.UNAUTHORISED,
|
|
3375
|
+
"CLI access has been revoked",
|
|
3376
|
+
"Ask a project admin to re-grant access from the Agentic CLI page."
|
|
3377
|
+
);
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3167
3380
|
function autopilotExitCode(code, status) {
|
|
3168
3381
|
if (status === 401 || status === 403) return CLI_EXIT_CODES.UNAUTHORISED;
|
|
3169
3382
|
if (code === "TIER_LIMIT_EXCEEDED" || code === "NO_GIT_INTEGRATION") {
|
|
@@ -3174,10 +3387,10 @@ function autopilotExitCode(code, status) {
|
|
|
3174
3387
|
}
|
|
3175
3388
|
|
|
3176
3389
|
// src/scan/llm.ts
|
|
3177
|
-
import { spawn as
|
|
3178
|
-
import { mkdir as
|
|
3390
|
+
import { spawn as spawn4 } from "child_process";
|
|
3391
|
+
import { mkdir as mkdir8, writeFile as writeFile9 } from "fs/promises";
|
|
3179
3392
|
import { homedir as homedir5 } from "os";
|
|
3180
|
-
import { join as
|
|
3393
|
+
import { join as join10 } from "path";
|
|
3181
3394
|
var FIX_PROMPT_JSON_SCHEMA = {
|
|
3182
3395
|
type: "object",
|
|
3183
3396
|
// Phase 3 — confidence_reason is REQUIRED unconditionally so the
|
|
@@ -3266,7 +3479,7 @@ async function generateFixPromptJson(args) {
|
|
|
3266
3479
|
return new Promise((resolve2, reject) => {
|
|
3267
3480
|
let child;
|
|
3268
3481
|
try {
|
|
3269
|
-
child =
|
|
3482
|
+
child = spawn4(claude, cliArgs, {
|
|
3270
3483
|
stdio: ["pipe", "pipe", "pipe"],
|
|
3271
3484
|
signal: args.signal
|
|
3272
3485
|
});
|
|
@@ -3392,10 +3605,10 @@ function readEnvelopeTokens(raw, userPrompt, innerText) {
|
|
|
3392
3605
|
async function maybeDumpDebug(ticketId, stdout, stderr) {
|
|
3393
3606
|
if (!DEBUG && stdout.length === 0 && stderr.length === 0) return null;
|
|
3394
3607
|
try {
|
|
3395
|
-
const dir =
|
|
3396
|
-
await
|
|
3397
|
-
const path =
|
|
3398
|
-
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(
|
|
3399
3612
|
path,
|
|
3400
3613
|
["## ticket_id", ticketId, "", "## stdout", stdout, "", "## stderr", stderr].join("\n")
|
|
3401
3614
|
);
|
|
@@ -3467,7 +3680,7 @@ function registerScan(program2) {
|
|
|
3467
3680
|
"Restrict to one project (default: the linked project from .task/config.json, falling back to every visible project if the repo is not linked)"
|
|
3468
3681
|
).option(
|
|
3469
3682
|
"--all-projects",
|
|
3470
|
-
"Override the linked-project default and scan every CLI-eligible project the
|
|
3683
|
+
"Override the linked-project default and scan every CLI-eligible project the signed-in user has cli_access on"
|
|
3471
3684
|
).option("--max <n>", "Max submissions per project token", "50").option("--batch <n>", "Tickets per /prepare batch (1-10)", "5").option("--api-url <url>", "Override TASK_API_URL").option("--silent", "Suppress per-ticket progress chrome").action(async (opts) => {
|
|
3472
3685
|
await runScan(opts);
|
|
3473
3686
|
});
|
|
@@ -3489,25 +3702,18 @@ async function runScan(opts) {
|
|
|
3489
3702
|
}
|
|
3490
3703
|
}
|
|
3491
3704
|
async function runScanImpl(opts, progress) {
|
|
3492
|
-
|
|
3493
|
-
if (!
|
|
3494
|
-
throw new CliError(
|
|
3495
|
-
CLI_EXIT_CODES.MISCONFIGURATION,
|
|
3496
|
-
"TASK_API_KEY is missing or shorter than 32 chars",
|
|
3497
|
-
"Set TASK_API_KEY in your environment. The autopilot loop authenticates with the shared admin secret, not the per-user CLI bearer."
|
|
3498
|
-
);
|
|
3499
|
-
}
|
|
3500
|
-
const actorEmail = process.env["TASK_API_KEY_OWNER_EMAIL"];
|
|
3501
|
-
if (!actorEmail || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(actorEmail)) {
|
|
3705
|
+
let creds = await readCredentials();
|
|
3706
|
+
if (!creds) {
|
|
3502
3707
|
throw new CliError(
|
|
3503
3708
|
CLI_EXIT_CODES.MISCONFIGURATION,
|
|
3504
|
-
"
|
|
3505
|
-
"
|
|
3709
|
+
"Not signed in",
|
|
3710
|
+
"Run 'task login' to authenticate. The scan autopilot uses the same per-user OAuth bearer as every other CLI command."
|
|
3506
3711
|
);
|
|
3507
3712
|
}
|
|
3713
|
+
creds = await ensureFreshAccessToken(creds);
|
|
3508
3714
|
const localCfg = await readLocalConfig();
|
|
3509
3715
|
const linkedProject = await readProjectConfig(findRepoRoot());
|
|
3510
|
-
const apiUrl = (opts.apiUrl ?? process.env["TASK_API_URL"] ?? localCfg.api_url ?? linkedProject?.api_url ?? "http://localhost:3400").replace(/\/$/, "");
|
|
3716
|
+
const apiUrl = (opts.apiUrl ?? process.env["TASK_API_URL"] ?? creds.api_url ?? localCfg.api_url ?? linkedProject?.api_url ?? "http://localhost:3400").replace(/\/$/, "");
|
|
3511
3717
|
const max = clampInt(opts.max, 1, 500, 50);
|
|
3512
3718
|
const batchSize = clampInt(opts.batch, 1, 10, 5);
|
|
3513
3719
|
const silent = !!opts.silent || localCfg.silent;
|
|
@@ -3526,7 +3732,7 @@ async function runScanImpl(opts, progress) {
|
|
|
3526
3732
|
projectFilter = linkedProject.project_id;
|
|
3527
3733
|
filterSource = "link";
|
|
3528
3734
|
}
|
|
3529
|
-
const api = new AutopilotApi({ apiUrl,
|
|
3735
|
+
const api = new AutopilotApi({ apiUrl, creds });
|
|
3530
3736
|
if (!silent) process.stdout.write(`${c.dim("Discovering eligible projects\u2026")}
|
|
3531
3737
|
`);
|
|
3532
3738
|
const all = await api.listEligibleProjects();
|
|
@@ -3542,7 +3748,7 @@ async function runScanImpl(opts, progress) {
|
|
|
3542
3748
|
throw new CliError(
|
|
3543
3749
|
CLI_EXIT_CODES.GENERIC_ERROR,
|
|
3544
3750
|
`Linked project ${linkedProject.organisation_slug}/${linkedProject.project_slug} has no CLI-eligible tickets right now`,
|
|
3545
|
-
"Mark a ticket as CLI-eligible from the dashboard, or pass --all-projects to scan
|
|
3751
|
+
"Mark a ticket as CLI-eligible from the dashboard, or pass --all-projects to scan every project you have cli_access on."
|
|
3546
3752
|
);
|
|
3547
3753
|
}
|
|
3548
3754
|
throw new CliError(
|
|
@@ -3742,6 +3948,252 @@ function clampInt(raw, min, max, fallback) {
|
|
|
3742
3948
|
return Math.min(v, max);
|
|
3743
3949
|
}
|
|
3744
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
|
+
|
|
3745
4197
|
// src/commands/pr-test.ts
|
|
3746
4198
|
import { execFileSync as execFileSync8 } from "child_process";
|
|
3747
4199
|
function registerPrTest(program2) {
|
|
@@ -3880,16 +4332,16 @@ ${c.err("\u2717 pr-test failed")}: ${err.message}
|
|
|
3880
4332
|
}
|
|
3881
4333
|
|
|
3882
4334
|
// src/commands/scheduled-task.ts
|
|
3883
|
-
import { randomUUID as
|
|
4335
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
3884
4336
|
|
|
3885
4337
|
// src/scheduler/index.ts
|
|
3886
4338
|
import { platform as platform2 } from "os";
|
|
3887
4339
|
|
|
3888
4340
|
// src/scheduler/launchd.ts
|
|
3889
|
-
import { mkdir as
|
|
4341
|
+
import { mkdir as mkdir9, readFile as readFile5, writeFile as writeFile10, unlink as unlink4, readdir as readdir2 } from "fs/promises";
|
|
3890
4342
|
import { homedir as homedir6 } from "os";
|
|
3891
|
-
import { join as
|
|
3892
|
-
import { execFileSync as execFileSync9, spawn as
|
|
4343
|
+
import { join as join11 } from "path";
|
|
4344
|
+
import { execFileSync as execFileSync9, spawn as spawn5 } from "child_process";
|
|
3893
4345
|
|
|
3894
4346
|
// src/scheduler/cron-translate.ts
|
|
3895
4347
|
function translateToLaunchd(cron) {
|
|
@@ -3990,14 +4442,14 @@ function expandField(field, min, max) {
|
|
|
3990
4442
|
}
|
|
3991
4443
|
|
|
3992
4444
|
// src/scheduler/launchd.ts
|
|
3993
|
-
var PLIST_DIR =
|
|
4445
|
+
var PLIST_DIR = join11(homedir6(), "Library", "LaunchAgents");
|
|
3994
4446
|
var LABEL_PREFIX = "com.inteeka.task.cli.";
|
|
3995
4447
|
var SAFE_ID_RE = /^[0-9a-zA-Z._-]+$/;
|
|
3996
4448
|
function plistPath(id) {
|
|
3997
4449
|
if (!SAFE_ID_RE.test(id) || id.includes("..")) {
|
|
3998
4450
|
throw new Error(`Refusing to compute plist path for unsafe id: ${id}`);
|
|
3999
4451
|
}
|
|
4000
|
-
return
|
|
4452
|
+
return join11(PLIST_DIR, `${LABEL_PREFIX}${id}.plist`);
|
|
4001
4453
|
}
|
|
4002
4454
|
function buildPlist(entry) {
|
|
4003
4455
|
const calendars = translateToLaunchd(entry.cron);
|
|
@@ -4033,9 +4485,9 @@ ${fields}
|
|
|
4033
4485
|
` <string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>`,
|
|
4034
4486
|
` </dict>`,
|
|
4035
4487
|
` <key>StandardOutPath</key>`,
|
|
4036
|
-
` <string>${escapeXml(
|
|
4488
|
+
` <string>${escapeXml(join11(homedir6(), ".cache", "task", "launchd-stdout.log"))}</string>`,
|
|
4037
4489
|
` <key>StandardErrorPath</key>`,
|
|
4038
|
-
` <string>${escapeXml(
|
|
4490
|
+
` <string>${escapeXml(join11(homedir6(), ".cache", "task", "launchd-stderr.log"))}</string>`,
|
|
4039
4491
|
!entry.enabled ? ` <key>Disabled</key>
|
|
4040
4492
|
<true/>` : "",
|
|
4041
4493
|
"</dict>",
|
|
@@ -4053,9 +4505,9 @@ function bootstrapDomain() {
|
|
|
4053
4505
|
}
|
|
4054
4506
|
var launchdAdapter = {
|
|
4055
4507
|
async upsert(entry) {
|
|
4056
|
-
await
|
|
4508
|
+
await mkdir9(PLIST_DIR, { recursive: true });
|
|
4057
4509
|
const path = plistPath(entry.id);
|
|
4058
|
-
await
|
|
4510
|
+
await writeFile10(path, buildPlist(entry));
|
|
4059
4511
|
try {
|
|
4060
4512
|
execFileSync9("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
|
|
4061
4513
|
} catch {
|
|
@@ -4084,7 +4536,7 @@ var launchdAdapter = {
|
|
|
4084
4536
|
for (const file of ours) {
|
|
4085
4537
|
const id = file.slice(LABEL_PREFIX.length, -".plist".length);
|
|
4086
4538
|
try {
|
|
4087
|
-
const xml = await readFile5(
|
|
4539
|
+
const xml = await readFile5(join11(PLIST_DIR, file), "utf8");
|
|
4088
4540
|
const cron = xml.match(/<key>StartCalendarInterval<\/key>[\s\S]*?<\/array>/)?.[0] ?? "";
|
|
4089
4541
|
const command = xml.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/)?.[1] ?? "";
|
|
4090
4542
|
const disabled = /<key>Disabled<\/key>\s*<true\/>/.test(xml);
|
|
@@ -4107,7 +4559,7 @@ var launchdAdapter = {
|
|
|
4107
4559
|
return new Promise((resolve2) => {
|
|
4108
4560
|
const args = entry.command.match(/(?:[^\s"]+|"[^"]*")+/g) ?? [entry.command];
|
|
4109
4561
|
const cmd = args.shift() ?? entry.command;
|
|
4110
|
-
const child =
|
|
4562
|
+
const child = spawn5(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
4111
4563
|
let stdoutTail = "";
|
|
4112
4564
|
let stderrTail = "";
|
|
4113
4565
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -4130,7 +4582,7 @@ var launchdAdapter = {
|
|
|
4130
4582
|
}
|
|
4131
4583
|
if (enabled) {
|
|
4132
4584
|
xml = xml.replace(/\s*<key>Disabled<\/key>\s*<true\/>/, "");
|
|
4133
|
-
await
|
|
4585
|
+
await writeFile10(path, xml);
|
|
4134
4586
|
try {
|
|
4135
4587
|
execFileSync9("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
|
|
4136
4588
|
} catch {
|
|
@@ -4142,7 +4594,7 @@ var launchdAdapter = {
|
|
|
4142
4594
|
"</dict>\n</plist>",
|
|
4143
4595
|
" <key>Disabled</key>\n <true/>\n</dict>\n</plist>"
|
|
4144
4596
|
);
|
|
4145
|
-
await
|
|
4597
|
+
await writeFile10(path, xml);
|
|
4146
4598
|
}
|
|
4147
4599
|
try {
|
|
4148
4600
|
execFileSync9("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
|
|
@@ -4153,7 +4605,7 @@ var launchdAdapter = {
|
|
|
4153
4605
|
};
|
|
4154
4606
|
|
|
4155
4607
|
// src/scheduler/cron.ts
|
|
4156
|
-
import { execFileSync as execFileSync10, spawn as
|
|
4608
|
+
import { execFileSync as execFileSync10, spawn as spawn6 } from "child_process";
|
|
4157
4609
|
|
|
4158
4610
|
// src/scheduler/safe-command.ts
|
|
4159
4611
|
var FORBIDDEN = /[;&|`$()<>\\]/;
|
|
@@ -4214,7 +4666,7 @@ function readCrontab() {
|
|
|
4214
4666
|
}
|
|
4215
4667
|
}
|
|
4216
4668
|
function writeCrontab(text) {
|
|
4217
|
-
const child =
|
|
4669
|
+
const child = spawn6("crontab", ["-"], { stdio: ["pipe", "inherit", "inherit"] });
|
|
4218
4670
|
child.stdin.write(text);
|
|
4219
4671
|
child.stdin.end();
|
|
4220
4672
|
}
|
|
@@ -4295,7 +4747,7 @@ var cronAdapter = {
|
|
|
4295
4747
|
return Promise.resolve({ exitCode: 1, stdoutTail: "", stderrTail: `rejected: ${reason}` });
|
|
4296
4748
|
}
|
|
4297
4749
|
return new Promise((resolve2) => {
|
|
4298
|
-
const child =
|
|
4750
|
+
const child = spawn6(parsed.bin, parsed.args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
4299
4751
|
let stdoutTail = "";
|
|
4300
4752
|
let stderrTail = "";
|
|
4301
4753
|
child.stdout?.on(
|
|
@@ -4323,7 +4775,7 @@ var cronAdapter = {
|
|
|
4323
4775
|
};
|
|
4324
4776
|
|
|
4325
4777
|
// src/scheduler/windows.ts
|
|
4326
|
-
import { execFileSync as execFileSync11, spawn as
|
|
4778
|
+
import { execFileSync as execFileSync11, spawn as spawn7 } from "child_process";
|
|
4327
4779
|
var TASK_PREFIX = "TaskCLI_";
|
|
4328
4780
|
function taskName(id) {
|
|
4329
4781
|
return `${TASK_PREFIX}${id.replace(/[^A-Za-z0-9_-]/g, "_")}`;
|
|
@@ -4436,7 +4888,7 @@ var windowsAdapter = {
|
|
|
4436
4888
|
return Promise.resolve({ exitCode: 1, stdoutTail: "", stderrTail: `rejected: ${reason}` });
|
|
4437
4889
|
}
|
|
4438
4890
|
return new Promise((resolve2) => {
|
|
4439
|
-
const child =
|
|
4891
|
+
const child = spawn7(parsed.bin, parsed.args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
4440
4892
|
let stdoutTail = "";
|
|
4441
4893
|
let stderrTail = "";
|
|
4442
4894
|
child.stdout?.on(
|
|
@@ -4495,10 +4947,10 @@ var unsupportedAdapter = {
|
|
|
4495
4947
|
};
|
|
4496
4948
|
|
|
4497
4949
|
// src/scheduler/registry.ts
|
|
4498
|
-
import { mkdir as
|
|
4950
|
+
import { mkdir as mkdir10, readFile as readFile6, writeFile as writeFile11 } from "fs/promises";
|
|
4499
4951
|
import { homedir as homedir7 } from "os";
|
|
4500
|
-
import { dirname as
|
|
4501
|
-
var REGISTRY_PATH =
|
|
4952
|
+
import { dirname as dirname5, join as join12 } from "path";
|
|
4953
|
+
var REGISTRY_PATH = join12(homedir7(), ".config", "task", "schedules.json");
|
|
4502
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;
|
|
4503
4955
|
function looksLikeRegistryRow(value) {
|
|
4504
4956
|
if (!value || typeof value !== "object") return false;
|
|
@@ -4518,8 +4970,8 @@ async function readRegistry() {
|
|
|
4518
4970
|
}
|
|
4519
4971
|
}
|
|
4520
4972
|
async function writeRegistry(rows) {
|
|
4521
|
-
await
|
|
4522
|
-
await
|
|
4973
|
+
await mkdir10(dirname5(REGISTRY_PATH), { recursive: true });
|
|
4974
|
+
await writeFile11(REGISTRY_PATH, JSON.stringify(rows, null, 2));
|
|
4523
4975
|
}
|
|
4524
4976
|
async function upsertRegistry(row) {
|
|
4525
4977
|
if (!UUID_RE.test(row.id)) {
|
|
@@ -4607,7 +5059,7 @@ function registerScheduledTask(program2) {
|
|
|
4607
5059
|
const max = Math.min(100, Math.max(1, parseInt(opts.max, 10) || 5));
|
|
4608
5060
|
const command = opts.command ?? `task work --auto --silent --max ${max}`;
|
|
4609
5061
|
const { hostId, hostLabel } = getHostInfo();
|
|
4610
|
-
const id =
|
|
5062
|
+
const id = randomUUID4();
|
|
4611
5063
|
const created = await apiCall("POST", "/api/v1/cli/schedules", {
|
|
4612
5064
|
body: {
|
|
4613
5065
|
name,
|
|
@@ -4760,7 +5212,7 @@ function stripAnsi(s) {
|
|
|
4760
5212
|
// src/commands/runs.ts
|
|
4761
5213
|
import { readFile as readFile7 } from "fs/promises";
|
|
4762
5214
|
import { homedir as homedir8 } from "os";
|
|
4763
|
-
import { join as
|
|
5215
|
+
import { join as join13 } from "path";
|
|
4764
5216
|
function registerRuns(program2) {
|
|
4765
5217
|
const cmd = program2.command("runs").description("Inspect agentic CLI run history");
|
|
4766
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) => {
|
|
@@ -4789,7 +5241,7 @@ function registerRuns(program2) {
|
|
|
4789
5241
|
process.stdout.write(JSON.stringify(row, null, 2) + "\n");
|
|
4790
5242
|
});
|
|
4791
5243
|
cmd.command("logs <id>").description("Show captured agent output for a run, if available").action(async (id) => {
|
|
4792
|
-
const localPath =
|
|
5244
|
+
const localPath = join13(homedir8(), ".cache", "task", "runs", `${id}.log`);
|
|
4793
5245
|
try {
|
|
4794
5246
|
const text = await readFile7(localPath, "utf8");
|
|
4795
5247
|
process.stdout.write(text);
|
|
@@ -4862,32 +5314,43 @@ function registerConfig(program2) {
|
|
|
4862
5314
|
|
|
4863
5315
|
// src/commands/doctor.ts
|
|
4864
5316
|
import { execFileSync as execFileSync12 } from "child_process";
|
|
4865
|
-
import { readFile as readFile8, writeFile as
|
|
4866
|
-
import { join as
|
|
5317
|
+
import { readFile as readFile8, writeFile as writeFile12 } from "fs/promises";
|
|
5318
|
+
import { join as join14 } from "path";
|
|
4867
5319
|
import { request as request5 } from "undici";
|
|
4868
5320
|
var ALLOWED_TEST_EXECUTABLES = /* @__PURE__ */ new Set(["pnpm", "npm", "yarn", "bun", "node", "npx"]);
|
|
4869
5321
|
var DEFAULT_TEST_COMMAND = "pnpm typecheck";
|
|
4870
5322
|
function registerDoctor(program2) {
|
|
4871
|
-
program2.command("doctor").description(
|
|
5323
|
+
program2.command("doctor").description(
|
|
5324
|
+
"Diagnose your CLI setup \u2014 Identity (who you are signed in as) first, then Setup checks"
|
|
5325
|
+
).option("--fix", "attempt to auto-remediate fixable problems (e.g. add a typecheck script)").action(async (opts) => {
|
|
4872
5326
|
const checks = [];
|
|
4873
5327
|
const creds = await readCredentials();
|
|
5328
|
+
let accessLite = null;
|
|
5329
|
+
if (creds) {
|
|
5330
|
+
accessLite = await fetchAccessLite();
|
|
5331
|
+
}
|
|
5332
|
+
const authDetail = creds ? renderAuthDetail(creds.email, creds.access_expires_at, accessLite) : "not signed in \u2014 run 'task login'";
|
|
4874
5333
|
checks.push({
|
|
5334
|
+
group: "identity",
|
|
4875
5335
|
name: "auth",
|
|
4876
|
-
ok: !!creds,
|
|
4877
|
-
detail:
|
|
5336
|
+
ok: !!creds && (accessLite?.has_access ?? true),
|
|
5337
|
+
detail: authDetail,
|
|
5338
|
+
remediation: !creds ? "run 'task login' to authenticate" : accessLite && !accessLite.has_access ? "Your account has no project with cli_access \u2014 ask an admin to grant access on the Agentic CLI page." : void 0
|
|
4878
5339
|
});
|
|
4879
5340
|
const root = findRepoRoot();
|
|
4880
5341
|
const project = await readProjectConfig(root);
|
|
4881
5342
|
checks.push({
|
|
5343
|
+
group: "identity",
|
|
4882
5344
|
name: "project link",
|
|
4883
5345
|
ok: !!project,
|
|
4884
5346
|
detail: project ? `${project.organisation_slug}/${project.project_slug}` : "no link \u2014 run 'task link'"
|
|
4885
5347
|
});
|
|
4886
5348
|
const cfg = await readLocalConfig();
|
|
4887
|
-
checks.push(checkBinary("claude", cfg.claude_path ?? "claude"));
|
|
4888
|
-
checks.push(checkBinary("git", "git"));
|
|
5349
|
+
checks.push({ group: "setup", ...checkBinary("claude", cfg.claude_path ?? "claude") });
|
|
5350
|
+
checks.push({ group: "setup", ...checkBinary("git", "git") });
|
|
4889
5351
|
const { kind } = getSchedulerAdapter();
|
|
4890
5352
|
checks.push({
|
|
5353
|
+
group: "setup",
|
|
4891
5354
|
name: "scheduler",
|
|
4892
5355
|
ok: kind !== "unsupported",
|
|
4893
5356
|
detail: kind === "unsupported" ? "unsupported platform" : kind
|
|
@@ -4901,12 +5364,14 @@ function registerDoctor(program2) {
|
|
|
4901
5364
|
});
|
|
4902
5365
|
await res.body.dump();
|
|
4903
5366
|
checks.push({
|
|
5367
|
+
group: "setup",
|
|
4904
5368
|
name: "api reachable",
|
|
4905
5369
|
ok: true,
|
|
4906
5370
|
detail: `${apiUrl} (HTTP ${res.statusCode})`
|
|
4907
5371
|
});
|
|
4908
5372
|
} catch (err) {
|
|
4909
5373
|
checks.push({
|
|
5374
|
+
group: "setup",
|
|
4910
5375
|
name: "api reachable",
|
|
4911
5376
|
ok: false,
|
|
4912
5377
|
detail: `${apiUrl}: ${err.message}`
|
|
@@ -4917,6 +5382,7 @@ function registerDoctor(program2) {
|
|
|
4917
5382
|
try {
|
|
4918
5383
|
const exists = remoteBranchExists(root, baseBranch);
|
|
4919
5384
|
checks.push({
|
|
5385
|
+
group: "setup",
|
|
4920
5386
|
name: "base branch on origin",
|
|
4921
5387
|
ok: exists,
|
|
4922
5388
|
detail: exists ? `origin/${baseBranch} reachable` : `origin has no branch "${baseBranch}"`,
|
|
@@ -4924,6 +5390,7 @@ function registerDoctor(program2) {
|
|
|
4924
5390
|
});
|
|
4925
5391
|
} catch (err) {
|
|
4926
5392
|
checks.push({
|
|
5393
|
+
group: "setup",
|
|
4927
5394
|
name: "base branch on origin",
|
|
4928
5395
|
ok: false,
|
|
4929
5396
|
detail: err instanceof CliError ? err.message : `could not check origin: ${err.message}`,
|
|
@@ -4937,41 +5404,57 @@ function registerDoctor(program2) {
|
|
|
4937
5404
|
encoding: "utf8"
|
|
4938
5405
|
}).trim();
|
|
4939
5406
|
checks.push({
|
|
5407
|
+
group: "setup",
|
|
4940
5408
|
name: "working tree",
|
|
4941
5409
|
ok: dirty.length === 0,
|
|
4942
5410
|
detail: dirty.length === 0 ? "clean" : "has uncommitted changes"
|
|
4943
5411
|
});
|
|
4944
5412
|
} catch {
|
|
4945
|
-
checks.push({
|
|
5413
|
+
checks.push({
|
|
5414
|
+
group: "setup",
|
|
5415
|
+
name: "working tree",
|
|
5416
|
+
ok: false,
|
|
5417
|
+
detail: "not in a git repo"
|
|
5418
|
+
});
|
|
4946
5419
|
}
|
|
4947
5420
|
const testCheck = await checkPrePushTest(
|
|
4948
5421
|
root,
|
|
4949
5422
|
project?.cli_test_command ?? null,
|
|
4950
5423
|
opts.fix === true
|
|
4951
5424
|
);
|
|
4952
|
-
checks.push(testCheck);
|
|
5425
|
+
checks.push({ group: "setup", ...testCheck });
|
|
4953
5426
|
let inFlight = [];
|
|
4954
5427
|
if (creds && project) {
|
|
4955
5428
|
inFlight = await listInFlightTickets(project.project_id, root);
|
|
4956
5429
|
checks.push({
|
|
5430
|
+
group: "setup",
|
|
4957
5431
|
name: "in-flight tickets",
|
|
4958
5432
|
ok: true,
|
|
4959
5433
|
detail: inFlight.length === 0 ? "none" : `${inFlight.length} ticket(s) waiting to be resumed`
|
|
4960
5434
|
});
|
|
4961
5435
|
}
|
|
4962
5436
|
let allOk = true;
|
|
4963
|
-
|
|
4964
|
-
const
|
|
4965
|
-
|
|
5437
|
+
const renderGroup = (label, group) => {
|
|
5438
|
+
const groupChecks = checks.filter((ch) => ch.group === group);
|
|
5439
|
+
if (groupChecks.length === 0) return;
|
|
5440
|
+
process.stdout.write(`${c.bold(label)}
|
|
5441
|
+
`);
|
|
5442
|
+
for (const check of groupChecks) {
|
|
5443
|
+
const sym = check.ok ? c.ok("\u2713") : c.err("\u2717");
|
|
5444
|
+
process.stdout.write(`${sym} ${check.name.padEnd(20)} ${c.dim(check.detail)}
|
|
4966
5445
|
`);
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
5446
|
+
if (!check.ok) {
|
|
5447
|
+
allOk = false;
|
|
5448
|
+
if (check.remediation) {
|
|
5449
|
+
process.stdout.write(` ${c.dim("\u2192 " + check.remediation)}
|
|
4971
5450
|
`);
|
|
5451
|
+
}
|
|
4972
5452
|
}
|
|
4973
5453
|
}
|
|
4974
|
-
}
|
|
5454
|
+
};
|
|
5455
|
+
renderGroup("Identity", "identity");
|
|
5456
|
+
process.stdout.write("\n");
|
|
5457
|
+
renderGroup("Setup", "setup");
|
|
4975
5458
|
for (const t of inFlight) {
|
|
4976
5459
|
const status = t.branchPresent ? c.ok("local branch present") : c.err("local branch missing");
|
|
4977
5460
|
process.stdout.write(
|
|
@@ -4982,6 +5465,24 @@ function registerDoctor(program2) {
|
|
|
4982
5465
|
if (!allOk) process.exit(1);
|
|
4983
5466
|
});
|
|
4984
5467
|
}
|
|
5468
|
+
async function fetchAccessLite() {
|
|
5469
|
+
try {
|
|
5470
|
+
return await apiCallOrThrow("GET", "/api/v1/cli/access");
|
|
5471
|
+
} catch {
|
|
5472
|
+
return null;
|
|
5473
|
+
}
|
|
5474
|
+
}
|
|
5475
|
+
function renderAuthDetail(email, expiresAt, access2) {
|
|
5476
|
+
const who = `signed in as ${email ?? "(unknown)"}`;
|
|
5477
|
+
const expiry = `expires ${expiresAt}`;
|
|
5478
|
+
if (!access2) return `${who}, ${expiry}`;
|
|
5479
|
+
if (access2.projects.length === 0) {
|
|
5480
|
+
return `${who}, ${expiry}, no cli_access projects`;
|
|
5481
|
+
}
|
|
5482
|
+
const sample = access2.projects.slice(0, 3).map((p) => `${p.organisation_slug}/${p.slug}`).join(", ");
|
|
5483
|
+
const more = access2.projects.length > 3 ? `, +${access2.projects.length - 3} more` : "";
|
|
5484
|
+
return `${who}, ${expiry}, ${access2.projects.length} project${access2.projects.length === 1 ? "" : "s"} (${sample}${more})`;
|
|
5485
|
+
}
|
|
4985
5486
|
async function listInFlightTickets(projectId, cwd) {
|
|
4986
5487
|
const result = await apiCall(
|
|
4987
5488
|
"GET",
|
|
@@ -5028,7 +5529,7 @@ async function checkPrePushTest(root, configuredCommand, fix) {
|
|
|
5028
5529
|
detail: `${command} (non-script executable, not statically verifiable)`
|
|
5029
5530
|
};
|
|
5030
5531
|
}
|
|
5031
|
-
const pkgPath =
|
|
5532
|
+
const pkgPath = join14(root, "package.json");
|
|
5032
5533
|
let pkgRaw;
|
|
5033
5534
|
try {
|
|
5034
5535
|
pkgRaw = await readFile8(pkgPath, "utf8");
|
|
@@ -5064,7 +5565,7 @@ async function checkPrePushTest(root, configuredCommand, fix) {
|
|
|
5064
5565
|
pkg.scripts = { ...scripts, typecheck: "tsc --noEmit" };
|
|
5065
5566
|
const indent = detectIndent(pkgRaw);
|
|
5066
5567
|
const trailingNewline = pkgRaw.endsWith("\n") ? "\n" : "";
|
|
5067
|
-
await
|
|
5568
|
+
await writeFile12(pkgPath, JSON.stringify(pkg, null, indent) + trailingNewline);
|
|
5068
5569
|
return {
|
|
5069
5570
|
name: "pre-push test",
|
|
5070
5571
|
ok: true,
|
|
@@ -5113,7 +5614,7 @@ function checkBinary(name, command) {
|
|
|
5113
5614
|
}
|
|
5114
5615
|
|
|
5115
5616
|
// src/commands/version.ts
|
|
5116
|
-
var CLI_VERSION = true ? "0.2.
|
|
5617
|
+
var CLI_VERSION = true ? "0.2.18" : "0.0.0-dev";
|
|
5117
5618
|
function registerVersion(program2) {
|
|
5118
5619
|
program2.command("version").description("Print the CLI version").action(() => {
|
|
5119
5620
|
process.stdout.write(CLI_VERSION + "\n");
|
|
@@ -5140,6 +5641,7 @@ registerMultiWork(program);
|
|
|
5140
5641
|
registerResume(program);
|
|
5141
5642
|
registerReset(program);
|
|
5142
5643
|
registerScan(program);
|
|
5644
|
+
registerFastTrack(program);
|
|
5143
5645
|
registerPrTest(program);
|
|
5144
5646
|
registerScheduledTask(program);
|
|
5145
5647
|
registerRuns(program);
|