@aictiq/cli 0.1.0 → 0.2.0

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/index.js CHANGED
@@ -299,7 +299,7 @@ async function promptSecret(question) {
299
299
  const muted = rl;
300
300
  const original = muted.output.write.bind(muted.output);
301
301
  muted.output.write = (() => true);
302
- const answer = await new Promise((resolve2) => rl.question("", resolve2));
302
+ const answer = await new Promise((resolve3) => rl.question("", resolve3));
303
303
  muted.output.write = original;
304
304
  process.stdout.write("\n");
305
305
  return answer.trim();
@@ -948,7 +948,7 @@ function mcpCommand(globals) {
948
948
  };
949
949
  process.on("SIGINT", () => void shutdown());
950
950
  process.on("SIGTERM", () => void shutdown());
951
- await new Promise((resolve2) => process.stdin.once("close", resolve2));
951
+ await new Promise((resolve3) => process.stdin.once("close", resolve3));
952
952
  await shutdown();
953
953
  });
954
954
  }
@@ -1146,7 +1146,7 @@ async function streamLog(ctx, orgSlug, runId, follow, pollMs) {
1146
1146
  if (page.items.length < logPageSize) break;
1147
1147
  }
1148
1148
  if (finished) return;
1149
- await new Promise((resolve2) => setTimeout(resolve2, pollMs));
1149
+ await new Promise((resolve3) => setTimeout(resolve3, pollMs));
1150
1150
  }
1151
1151
  }
1152
1152
  function formatLogEntry(entry) {
@@ -1203,7 +1203,7 @@ async function resolveAgentId(ctx, agent) {
1203
1203
  import { Command as Command7 } from "commander";
1204
1204
  import { existsSync as existsSync3 } from "fs";
1205
1205
  import { arch, platform } from "os";
1206
- import { resolve } from "path";
1206
+ import { resolve as resolve2 } from "path";
1207
1207
 
1208
1208
  // src/runner/client.ts
1209
1209
  var RunnerHttpError = class extends Error {
@@ -1344,9 +1344,9 @@ import { dirname as dirname2, join as join3 } from "path";
1344
1344
 
1345
1345
  // src/runner/workspace.ts
1346
1346
  import { spawn } from "child_process";
1347
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
1347
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, realpathSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
1348
1348
  import { homedir as homedir2 } from "os";
1349
- import { join as join2 } from "path";
1349
+ import { isAbsolute, join as join2, relative, resolve } from "path";
1350
1350
 
1351
1351
  // src/runner/types.ts
1352
1352
  var RunFailure = class extends Error {
@@ -1521,18 +1521,52 @@ function uniqueFileName(value, existing) {
1521
1521
  function formatBytes(value) {
1522
1522
  return value >= 1024 * 1024 ? `${(value / (1024 * 1024)).toFixed(1)} MiB` : `${value} bytes`;
1523
1523
  }
1524
+ function resolveLocalRepository(run, options) {
1525
+ const mapped = options.repositories[run.projectKey];
1526
+ if (mapped !== void 0) return { path: mapped, root: null };
1527
+ const hint = run.repo.localPathHint?.trim();
1528
+ const roots = options.repoRoots ?? [];
1529
+ if (hint) {
1530
+ const expanded = expandHome(hint);
1531
+ if (isAbsolute(expanded)) {
1532
+ const real = realpathOrSelf(expanded);
1533
+ for (const configured of roots) {
1534
+ const expandedRoot = expandHome(configured);
1535
+ if (!isAbsolute(expandedRoot)) continue;
1536
+ const root = realpathOrSelf(expandedRoot);
1537
+ if (isWithin(real, root)) return { path: real, root };
1538
+ }
1539
+ }
1540
+ }
1541
+ const example = `("${run.projectKey}": "/path/to/repository")`;
1542
+ const detail = !hint ? ` Add it to the \`workspaces\` map in runner.json ${example}, or set the project's path hint and list its parent directory in \`repoRoots\`.` : roots.length === 0 ? ` The project suggests ${hint}. Add its parent directory to \`repoRoots\` in runner.json (\`aictiq runner root <path>\`), or map it in \`workspaces\` ${example}.` : ` The project suggests ${hint}, which is not under any of this runner's repoRoots (${roots.join(", ")}). Add a root that contains it, or map it in \`workspaces\` ${example}.`;
1543
+ throw new RunFailure(
1544
+ "no-local-repository",
1545
+ `No local repository is mapped for project ${run.projectKey}.${detail}`
1546
+ );
1547
+ }
1548
+ function expandHome(path) {
1549
+ if (path === "~") return homedir2();
1550
+ return path.startsWith("~/") ? join2(homedir2(), path.slice(2)) : path;
1551
+ }
1552
+ function realpathOrSelf(path) {
1553
+ const normalised = resolve(path);
1554
+ try {
1555
+ return realpathSync(normalised);
1556
+ } catch {
1557
+ return normalised;
1558
+ }
1559
+ }
1560
+ function isWithin(path, root) {
1561
+ const rel = relative(root, path);
1562
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
1563
+ }
1524
1564
  function message(error) {
1525
1565
  return error instanceof Error ? error.message : String(error);
1526
1566
  }
1527
1567
  async function provisionLocal(run, checkout, options, env) {
1528
- const mapped = options.repositories[run.projectKey];
1529
- if (mapped === void 0) {
1530
- const hint = run.repo.localPathHint ? ` The project suggests ${run.repo.localPathHint}.` : "";
1531
- throw new RunFailure(
1532
- "no-local-repository",
1533
- `No local repository is mapped for project ${run.projectKey}. Add it to the \`workspaces\` map in runner.json ("${run.projectKey}": "/path/to/repository").${hint}`
1534
- );
1535
- }
1568
+ const { path: mapped, root } = resolveLocalRepository(run, options);
1569
+ const origin = root === null ? "mapped for project" : "the path hint for project";
1536
1570
  const toplevel = await git(["rev-parse", "--show-toplevel"], {
1537
1571
  cwd: mapped,
1538
1572
  env,
@@ -1541,9 +1575,15 @@ async function provisionLocal(run, checkout, options, env) {
1541
1575
  if (isAbort(error)) throw error;
1542
1576
  throw new RunFailure(
1543
1577
  "no-local-repository",
1544
- `${mapped}, mapped for project ${run.projectKey} in runner.json, is not a git repository.`
1578
+ `${mapped}, ${origin} ${run.projectKey}, is not a git repository.`
1545
1579
  );
1546
1580
  });
1581
+ if (root !== null && !isWithin(realpathOrSelf(toplevel.trim()), root)) {
1582
+ throw new RunFailure(
1583
+ "no-local-repository",
1584
+ `${mapped}, the path hint for project ${run.projectKey}, is not a git repository under ${root}.`
1585
+ );
1586
+ }
1547
1587
  const repo = toplevel.trim();
1548
1588
  const call = (args) => gitStep(args, { cwd: repo, env, signal: options.signal });
1549
1589
  const hasOrigin = await git(["remote", "get-url", "origin"], {
@@ -1672,7 +1712,7 @@ ${tail}` : error.message;
1672
1712
  }
1673
1713
  }
1674
1714
  function git(args, options) {
1675
- return new Promise((resolve2, reject) => {
1715
+ return new Promise((resolve3, reject) => {
1676
1716
  const child = spawn("git", args, {
1677
1717
  cwd: options.cwd,
1678
1718
  env: { ...options.env, GIT_TERMINAL_PROMPT: "0" },
@@ -1687,7 +1727,7 @@ function git(args, options) {
1687
1727
  });
1688
1728
  child.on("error", reject);
1689
1729
  child.on("close", (code) => {
1690
- if (code === 0) resolve2(stdout);
1730
+ if (code === 0) resolve3(stdout);
1691
1731
  else reject(new GitError(args, code, stderr));
1692
1732
  });
1693
1733
  });
@@ -1710,7 +1750,7 @@ function readRunnerConfig(path = runnerConfigPath()) {
1710
1750
  return null;
1711
1751
  }
1712
1752
  if (typeof parsed !== "object" || parsed === null) return null;
1713
- const { url, token, name, workspaces, attachments } = parsed;
1753
+ const { url, token, name, workspaces, repoRoots, attachments } = parsed;
1714
1754
  if (typeof url !== "string" || typeof token !== "string") return null;
1715
1755
  const mapped = {};
1716
1756
  if (typeof workspaces === "object" && workspaces !== null) {
@@ -1718,12 +1758,14 @@ function readRunnerConfig(path = runnerConfigPath()) {
1718
1758
  if (typeof value === "string") mapped[key] = value;
1719
1759
  }
1720
1760
  }
1761
+ const roots = Array.isArray(repoRoots) ? repoRoots.filter((root) => typeof root === "string" && root.trim() !== "") : [];
1721
1762
  const rawLimits = typeof attachments === "object" && attachments !== null ? attachments : {};
1722
1763
  return {
1723
1764
  url,
1724
1765
  token,
1725
1766
  ...typeof name === "string" ? { name } : {},
1726
1767
  workspaces: mapped,
1768
+ repoRoots: roots,
1727
1769
  attachments: {
1728
1770
  maxCount: validLimit(rawLimits.maxCount, DefaultAttachmentMaxCount),
1729
1771
  maxBytes: validLimit(rawLimits.maxBytes, DefaultAttachmentMaxBytes)
@@ -1872,7 +1914,7 @@ function splitByBytes(text, maxBytes) {
1872
1914
  return pieces;
1873
1915
  }
1874
1916
  function delay(ms) {
1875
- return new Promise((resolve2) => setTimeout(resolve2, ms));
1917
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
1876
1918
  }
1877
1919
 
1878
1920
  // src/runner/process.ts
@@ -1885,12 +1927,12 @@ function spawnSupervised(options) {
1885
1927
  detached: process.platform !== "win32",
1886
1928
  stdio: ["pipe", "pipe", "pipe"]
1887
1929
  });
1888
- const exited = new Promise((resolve2) => {
1930
+ const exited = new Promise((resolve3) => {
1889
1931
  let settled = false;
1890
1932
  const settle = (code) => {
1891
1933
  if (settled) return;
1892
1934
  settled = true;
1893
- resolve2(code);
1935
+ resolve3(code);
1894
1936
  };
1895
1937
  child.on("error", (error) => {
1896
1938
  options.onLine("stderr", `Could not start ${options.command}: ${error.message}`);
@@ -2210,7 +2252,7 @@ async function sendFinish(client, run, report, local) {
2210
2252
  return true;
2211
2253
  } catch (error) {
2212
2254
  if (error instanceof RunnerNetworkError && attempt < 30) {
2213
- await new Promise((resolve2) => setTimeout(resolve2, Math.min(3e4, 1e3 * 2 ** attempt)));
2255
+ await new Promise((resolve3) => setTimeout(resolve3, Math.min(3e4, 1e3 * 2 ** attempt)));
2214
2256
  continue;
2215
2257
  }
2216
2258
  if (error instanceof RunnerHttpError && error.revoked) throw error;
@@ -2220,14 +2262,14 @@ async function sendFinish(client, run, report, local) {
2220
2262
  }
2221
2263
  }
2222
2264
  function findPullRequest(checkout, branch, env) {
2223
- return new Promise((resolve2) => {
2265
+ return new Promise((resolve3) => {
2224
2266
  execFile(
2225
2267
  "gh",
2226
2268
  ["pr", "view", branch, "--json", "url", "--jq", ".url"],
2227
2269
  { cwd: checkout, env, timeout: 15e3 },
2228
2270
  (error, stdout) => {
2229
2271
  const url = error ? "" : stdout.trim();
2230
- resolve2(/^https:\/\/\S+\/pull\/\d+$/.test(url) ? url : null);
2272
+ resolve3(/^https:\/\/\S+\/pull\/\d+$/.test(url) ? url : null);
2231
2273
  }
2232
2274
  );
2233
2275
  });
@@ -2245,12 +2287,12 @@ import { execFile as execFile2 } from "child_process";
2245
2287
  var limit2 = (value, length) => value.length > length ? `${value.slice(0, Math.max(0, length - 1))}\u2026` : value;
2246
2288
  var recordOf = (value) => typeof value === "object" && value !== null ? value : null;
2247
2289
  function versionOf(command, env = process.env) {
2248
- return new Promise((resolve2) => {
2290
+ return new Promise((resolve3) => {
2249
2291
  execFile2(command, ["--version"], { env, timeout: 5e3 }, (error, stdout, stderr) => {
2250
- if (error) return resolve2(null);
2292
+ if (error) return resolve3(null);
2251
2293
  const output = String(stdout || stderr).trim();
2252
2294
  const firstLine = output.split(/\r?\n/)[0]?.slice(0, 100) ?? "";
2253
- resolve2(firstLine || null);
2295
+ resolve3(firstLine || null);
2254
2296
  });
2255
2297
  });
2256
2298
  }
@@ -2696,20 +2738,20 @@ var RunnerLoop = class {
2696
2738
  return null;
2697
2739
  }
2698
2740
  waitForSlot() {
2699
- return new Promise((resolve2) => {
2741
+ return new Promise((resolve3) => {
2700
2742
  this.wake = () => {
2701
2743
  this.wake = void 0;
2702
- resolve2();
2744
+ resolve3();
2703
2745
  };
2704
2746
  });
2705
2747
  }
2706
2748
  sleep(ms) {
2707
- return new Promise((resolve2) => {
2749
+ return new Promise((resolve3) => {
2708
2750
  const timer = setTimeout(done, ms);
2709
2751
  this.wake = done;
2710
2752
  function done() {
2711
2753
  clearTimeout(timer);
2712
- resolve2();
2754
+ resolve3();
2713
2755
  }
2714
2756
  });
2715
2757
  }
@@ -2748,6 +2790,7 @@ function runnerCommand(globals) {
2748
2790
  ...options.name ? { name: options.name } : existing?.name ? { name: existing.name } : {},
2749
2791
  // Re-registering (a rotated secret) keeps the repository map someone built up.
2750
2792
  workspaces: existing?.workspaces ?? {},
2793
+ repoRoots: existing?.repoRoots ?? [],
2751
2794
  attachments: existing?.attachments ?? { maxCount: DefaultAttachmentMaxCount, maxBytes: DefaultAttachmentMaxBytes }
2752
2795
  };
2753
2796
  writeRunnerConfig(config);
@@ -2769,7 +2812,7 @@ Config written to ${runnerConfigPath()}. Start it with \`aictiq runner start\`.`
2769
2812
  if (options.remove) {
2770
2813
  delete config.workspaces[key];
2771
2814
  } else {
2772
- const absolute = resolve(path ?? ".");
2815
+ const absolute = resolve2(path ?? ".");
2773
2816
  if (!existsSync3(absolute))
2774
2817
  throw new CliError(`${absolute} does not exist.`, ExitCode.Validation);
2775
2818
  config.workspaces[key] = absolute;
@@ -2779,6 +2822,24 @@ Config written to ${runnerConfigPath()}. Start it with \`aictiq runner start\`.`
2779
2822
  options.remove ? `Removed the mapping for ${key}.` : `${key} \u2192 ${config.workspaces[key]}`
2780
2823
  );
2781
2824
  });
2825
+ runner.command("root").description(
2826
+ "Trust the web UI's path hint for local projects whose clone is under this directory"
2827
+ ).argument("[path]", "Directory that holds your clones (default: the current directory)").option("--remove", "Stop trusting path hints under this directory").action((path, options) => {
2828
+ const config = requireConfig();
2829
+ const absolute = resolve2(path ?? ".");
2830
+ if (options.remove) {
2831
+ config.repoRoots = config.repoRoots.filter((root) => resolve2(root) !== absolute);
2832
+ } else {
2833
+ if (!existsSync3(absolute))
2834
+ throw new CliError(`${absolute} does not exist.`, ExitCode.Validation);
2835
+ if (!config.repoRoots.some((root) => resolve2(root) === absolute))
2836
+ config.repoRoots.push(absolute);
2837
+ }
2838
+ writeRunnerConfig(config);
2839
+ print(
2840
+ options.remove ? `Path hints under ${absolute} are no longer used.` : `Path hints under ${absolute} are used for projects without a mapping.`
2841
+ );
2842
+ });
2782
2843
  runner.command("status").description("Show the registration, detected harnesses and mapped repositories").action(async () => {
2783
2844
  const config = requireConfig();
2784
2845
  const capabilities = await probe(1);
@@ -2796,7 +2857,7 @@ Config written to ${runnerConfigPath()}. Start it with \`aictiq runner start\`.`
2796
2857
  if (error instanceof RunnerHttpError && error.revoked) process.exitCode = ExitCode.Auth;
2797
2858
  }
2798
2859
  if (globals().json) {
2799
- printJson({ url: config.url, registration, capabilities, workspaces: config.workspaces, attachments: config.attachments });
2860
+ printJson({ url: config.url, registration, capabilities, workspaces: config.workspaces, repoRoots: config.repoRoots, attachments: config.attachments });
2800
2861
  return;
2801
2862
  }
2802
2863
  print(
@@ -2824,6 +2885,10 @@ Config written to ${runnerConfigPath()}. Start it with \`aictiq runner start\`.`
2824
2885
  { header: "REPOSITORY", value: ([, path]) => path }
2825
2886
  ])
2826
2887
  );
2888
+ print("");
2889
+ print(
2890
+ config.repoRoots.length === 0 ? "No repository roots; path hints from the web UI are not used (`aictiq runner root <path>`)." : renderTable(config.repoRoots, [{ header: "REPOSITORY ROOT", value: (root) => root }])
2891
+ );
2827
2892
  });
2828
2893
  runner.command("start").description("Poll for runs and execute them until interrupted").option("--parallel <n>", "Runs executed at the same time", "1").option("--keep-workspaces", "Leave each run\u2019s checkout on disk after it finishes").option("--workspace-root <path>", "Where run checkouts are created").action(
2829
2894
  async (options) => {
@@ -2835,28 +2900,32 @@ Config written to ${runnerConfigPath()}. Start it with \`aictiq runner start\`.`
2835
2900
  const client = new RunnerClient({ baseUrl: config.url, token: config.token });
2836
2901
  const local = (message3) => process.stderr.write(`${(/* @__PURE__ */ new Date()).toISOString()} ${message3}
2837
2902
  `);
2838
- const workspaceRoot = resolve(options.workspaceRoot ?? defaultWorkspaceRoot());
2903
+ const workspaceRoot = resolve2(options.workspaceRoot ?? defaultWorkspaceRoot());
2839
2904
  const loop = new RunnerLoop({
2840
2905
  client,
2841
2906
  parallel,
2842
2907
  probe: () => probe(parallel),
2843
2908
  local,
2844
- execute: (run, hello, shutdown) => executeRun(run, {
2845
- client,
2846
- hello,
2847
- adapters: harnesses,
2848
- runnerToken: config.token,
2849
- shutdown,
2850
- local,
2851
- workspace: {
2852
- root: workspaceRoot,
2853
- repositories: config.workspaces,
2854
- keep: options.keepWorkspaces === true,
2855
- mcpServer: mcpServerCommand(),
2856
- attachmentMaxCount: config.attachments.maxCount,
2857
- attachmentMaxBytes: config.attachments.maxBytes
2858
- }
2859
- })
2909
+ execute: (run, hello, shutdown) => {
2910
+ const current = readRunnerConfig() ?? config;
2911
+ return executeRun(run, {
2912
+ client,
2913
+ hello,
2914
+ adapters: harnesses,
2915
+ runnerToken: config.token,
2916
+ shutdown,
2917
+ local,
2918
+ workspace: {
2919
+ root: workspaceRoot,
2920
+ repositories: current.workspaces,
2921
+ repoRoots: current.repoRoots,
2922
+ keep: options.keepWorkspaces === true,
2923
+ mcpServer: mcpServerCommand(),
2924
+ attachmentMaxCount: current.attachments.maxCount,
2925
+ attachmentMaxBytes: current.attachments.maxBytes
2926
+ }
2927
+ });
2928
+ }
2860
2929
  });
2861
2930
  let interrupts = 0;
2862
2931
  const onSignal = (signal2) => {
@@ -2930,7 +2999,7 @@ async function probe(maxParallel) {
2930
2999
  };
2931
3000
  }
2932
3001
  function cliEntry() {
2933
- return resolve(process.argv[1] ?? "aictiq");
3002
+ return resolve2(process.argv[1] ?? "aictiq");
2934
3003
  }
2935
3004
  function mcpServerCommand() {
2936
3005
  return { command: process.execPath, args: [cliEntry(), "mcp"] };