@task0/cli 0.10.0 → 0.11.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.
Files changed (2) hide show
  1. package/dist/main.js +149 -25
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -1909,11 +1909,11 @@ async function request(method, pathname, body) {
1909
1909
  }
1910
1910
  }
1911
1911
  var api = {
1912
- get: (path32) => request("GET", path32),
1913
- post: (path32, body) => request("POST", path32, body ?? {}),
1914
- put: (path32, body) => request("PUT", path32, body ?? {}),
1915
- patch: (path32, body) => request("PATCH", path32, body ?? {}),
1916
- del: (path32) => request("DELETE", path32)
1912
+ get: (path33) => request("GET", path33),
1913
+ post: (path33, body) => request("POST", path33, body ?? {}),
1914
+ put: (path33, body) => request("PUT", path33, body ?? {}),
1915
+ patch: (path33, body) => request("PATCH", path33, body ?? {}),
1916
+ del: (path33) => request("DELETE", path33)
1917
1917
  };
1918
1918
 
1919
1919
  // src/commands/task/triage.ts
@@ -6327,15 +6327,119 @@ function readCliVersion() {
6327
6327
  return cached2;
6328
6328
  }
6329
6329
 
6330
+ // src/core/project-watcher.ts
6331
+ init_node();
6332
+ import fs32 from "fs";
6333
+ import path31 from "path";
6334
+ var ProjectWatcher = class {
6335
+ perProject = /* @__PURE__ */ new Map();
6336
+ debounceTimers = /* @__PURE__ */ new Map();
6337
+ debounceMs;
6338
+ onChange;
6339
+ constructor(opts) {
6340
+ this.onChange = opts.onChange;
6341
+ this.debounceMs = opts.debounceMs ?? 300;
6342
+ }
6343
+ /**
6344
+ * Reconcile watchers against the desired project list. Idempotent — call it
6345
+ * whenever the daemon's project set changes (project_add / project_remove /
6346
+ * project_set_enabled RPCs, or on reconnect with the latest config).
6347
+ */
6348
+ setProjects(projects) {
6349
+ const wanted = /* @__PURE__ */ new Map();
6350
+ for (const p of projects) {
6351
+ if (p.enabled) wanted.set(p.name, p);
6352
+ }
6353
+ for (const [name, entry] of this.perProject) {
6354
+ const target = wanted.get(name);
6355
+ if (!target || path31.resolve(target.path) !== entry.rootPath) {
6356
+ this.stopProject(name);
6357
+ }
6358
+ }
6359
+ for (const [name, proj] of wanted) {
6360
+ if (!this.perProject.has(name)) {
6361
+ this.startProject(proj);
6362
+ }
6363
+ }
6364
+ }
6365
+ close() {
6366
+ for (const name of [...this.perProject.keys()]) this.stopProject(name);
6367
+ for (const [, t] of this.debounceTimers) clearTimeout(t);
6368
+ this.debounceTimers.clear();
6369
+ }
6370
+ startProject(proj) {
6371
+ const rootPath = path31.resolve(proj.path);
6372
+ const projectYml = path31.join(rootPath, "task0.yml");
6373
+ const cfg = readYaml(projectYml);
6374
+ if (!cfg || typeof cfg.tasks_dir !== "string" || cfg.tasks_dir.length === 0) {
6375
+ return;
6376
+ }
6377
+ const tasksDir = path31.resolve(rootPath, cfg.tasks_dir);
6378
+ const watchers = [];
6379
+ try {
6380
+ const w = fs32.watch(tasksDir, { recursive: true }, () => this.notify(proj.name));
6381
+ w.on("error", () => {
6382
+ });
6383
+ watchers.push(w);
6384
+ } catch {
6385
+ }
6386
+ try {
6387
+ const parentDir = path31.dirname(tasksDir);
6388
+ const tasksDirName = path31.basename(tasksDir);
6389
+ const tarName = tasksDirName + ".tar";
6390
+ const w = fs32.watch(parentDir, (_event, filename) => {
6391
+ if (!filename) return;
6392
+ if (filename === tasksDirName || filename === tarName) {
6393
+ this.notify(proj.name);
6394
+ }
6395
+ });
6396
+ w.on("error", () => {
6397
+ });
6398
+ watchers.push(w);
6399
+ } catch {
6400
+ }
6401
+ this.perProject.set(proj.name, { watchers, rootPath });
6402
+ }
6403
+ stopProject(name) {
6404
+ const entry = this.perProject.get(name);
6405
+ if (!entry) return;
6406
+ for (const w of entry.watchers) {
6407
+ try {
6408
+ w.close();
6409
+ } catch {
6410
+ }
6411
+ }
6412
+ this.perProject.delete(name);
6413
+ const timer = this.debounceTimers.get(name);
6414
+ if (timer) {
6415
+ clearTimeout(timer);
6416
+ this.debounceTimers.delete(name);
6417
+ }
6418
+ }
6419
+ notify(projectName) {
6420
+ const prev = this.debounceTimers.get(projectName);
6421
+ if (prev) clearTimeout(prev);
6422
+ const t = setTimeout(() => {
6423
+ this.debounceTimers.delete(projectName);
6424
+ try {
6425
+ this.onChange(projectName);
6426
+ } catch (err) {
6427
+ console.error(`project watcher onChange threw for ${projectName}:`, err);
6428
+ }
6429
+ }, this.debounceMs);
6430
+ this.debounceTimers.set(projectName, t);
6431
+ }
6432
+ };
6433
+
6330
6434
  // src/commands/daemon.ts
6331
- async function dispatchRpc(ws, id, method, params) {
6435
+ async function dispatchRpc(ws, id, method, params, notifyManifestChanged) {
6332
6436
  const handler = rpcHandlers[method];
6333
6437
  if (!handler) {
6334
6438
  sendRpc(ws, { type: "rpc_error", id, error: { code: "unknown_method", message: `unknown method: ${method}` } });
6335
6439
  return;
6336
6440
  }
6337
6441
  try {
6338
- const ctx = { notifyManifestChanged: () => pushManifest(ws) };
6442
+ const ctx = { notifyManifestChanged };
6339
6443
  const result = await handler(params ?? {}, ctx);
6340
6444
  sendRpc(ws, { type: "rpc_response", id, result });
6341
6445
  } catch (error2) {
@@ -6382,6 +6486,9 @@ function buildManifest() {
6382
6486
  }
6383
6487
  return { type: "manifest", projects, tasks, scan_errors: scanErrors };
6384
6488
  }
6489
+ function currentWatchedProjects() {
6490
+ return loadConfig().sources.filter((source2) => source2.type === "project").map((source2) => ({ name: source2.name, path: source2.path, enabled: source2.enabled }));
6491
+ }
6385
6492
  function pushManifest(ws) {
6386
6493
  if (ws.readyState !== ws.OPEN) return;
6387
6494
  const manifest = buildManifest();
@@ -6599,11 +6706,21 @@ daemonCmd.command("run").description("Run the daemon WebSocket loop in foregroun
6599
6706
  let reconnectDelay = 1e3;
6600
6707
  let shouldRun = true;
6601
6708
  let activeWs = null;
6709
+ let activeWatcher = null;
6602
6710
  function connect() {
6603
6711
  const ws = new WebSocket(wsUrl, {
6604
6712
  headers: { authorization: `Bearer ${identity.token}` }
6605
6713
  });
6606
6714
  activeWs = ws;
6715
+ const watcher = new ProjectWatcher({
6716
+ onChange: () => pushManifest(ws),
6717
+ debounceMs: 300
6718
+ });
6719
+ activeWatcher = watcher;
6720
+ const notifyManifestChanged = () => {
6721
+ pushManifest(ws);
6722
+ watcher.setProjects(currentWatchedProjects());
6723
+ };
6607
6724
  ws.on("open", () => {
6608
6725
  reconnectDelay = 1e3;
6609
6726
  console.log(chalk19.green(`[${(/* @__PURE__ */ new Date()).toISOString()}] connected`));
@@ -6629,6 +6746,7 @@ daemonCmd.command("run").description("Run the daemon WebSocket loop in foregroun
6629
6746
  const manifest = buildManifest();
6630
6747
  ws.send(JSON.stringify(manifest));
6631
6748
  console.log(chalk19.dim(`pushed manifest: ${manifest.projects.length} project(s)`));
6749
+ watcher.setProjects(currentWatchedProjects());
6632
6750
  const sink = {
6633
6751
  send: (frame) => {
6634
6752
  if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(frame));
@@ -6651,7 +6769,7 @@ daemonCmd.command("run").description("Run the daemon WebSocket loop in foregroun
6651
6769
  } else if (msg.type === "error") {
6652
6770
  console.error(chalk19.yellow(`server error: ${msg.message}`));
6653
6771
  } else if (msg.type === "rpc_request") {
6654
- void dispatchRpc(ws, msg.id, msg.method, msg.params);
6772
+ void dispatchRpc(ws, msg.id, msg.method, msg.params, notifyManifestChanged);
6655
6773
  } else if (msg.type === "agent_run_resume_request") {
6656
6774
  const sent = replayAfterRanges(msg.ranges);
6657
6775
  if (sent > 0) {
@@ -6662,6 +6780,8 @@ daemonCmd.command("run").description("Run the daemon WebSocket loop in foregroun
6662
6780
  ws.on("close", (code, reason) => {
6663
6781
  activeWs = null;
6664
6782
  bindAgentRunFrameSink(null);
6783
+ watcher.close();
6784
+ if (activeWatcher === watcher) activeWatcher = null;
6665
6785
  const reasonText = reason.toString("utf-8") || "no reason";
6666
6786
  console.log(chalk19.yellow(`[${(/* @__PURE__ */ new Date()).toISOString()}] disconnected (code=${code}, ${reasonText})`));
6667
6787
  if (code === 4001) {
@@ -6679,6 +6799,10 @@ daemonCmd.command("run").description("Run the daemon WebSocket loop in foregroun
6679
6799
  }
6680
6800
  const shutdown = () => {
6681
6801
  shouldRun = false;
6802
+ try {
6803
+ activeWatcher?.close();
6804
+ } catch {
6805
+ }
6682
6806
  try {
6683
6807
  activeWs?.close(1e3, "shutdown");
6684
6808
  } catch {
@@ -6807,8 +6931,8 @@ function fail7(message, code = 1) {
6807
6931
  console.error(chalk20.red(message));
6808
6932
  process.exit(code);
6809
6933
  }
6810
- async function callServer(path32, init = {}) {
6811
- const url = `${serverBase2()}${path32}`;
6934
+ async function callServer(path33, init = {}) {
6935
+ const url = `${serverBase2()}${path33}`;
6812
6936
  let auth;
6813
6937
  try {
6814
6938
  auth = adminAuthHeader();
@@ -7313,8 +7437,8 @@ automation.command("runs <id>").description("List recent runs for an automation"
7313
7437
 
7314
7438
  // src/commands/profile.ts
7315
7439
  init_node();
7316
- import fs32 from "fs";
7317
- import path31 from "path";
7440
+ import fs33 from "fs";
7441
+ import path32 from "path";
7318
7442
  import { Command as Command23 } from "commander";
7319
7443
  import chalk23 from "chalk";
7320
7444
  import yaml11 from "js-yaml";
@@ -7327,19 +7451,19 @@ function fail9(msg) {
7327
7451
  process.exit(1);
7328
7452
  }
7329
7453
  function readDaemonAt(dir) {
7330
- const file = path31.join(dir, "daemon.json");
7331
- if (!fs32.existsSync(file)) return null;
7454
+ const file = path32.join(dir, "daemon.json");
7455
+ if (!fs33.existsSync(file)) return null;
7332
7456
  try {
7333
- return JSON.parse(fs32.readFileSync(file, "utf-8"));
7457
+ return JSON.parse(fs33.readFileSync(file, "utf-8"));
7334
7458
  } catch {
7335
7459
  return null;
7336
7460
  }
7337
7461
  }
7338
7462
  function readProfileApiUrl(name) {
7339
- const file = path31.join(profileDir(name), "config.yml");
7340
- if (!fs32.existsSync(file)) return void 0;
7463
+ const file = path32.join(profileDir(name), "config.yml");
7464
+ if (!fs33.existsSync(file)) return void 0;
7341
7465
  try {
7342
- const data = yaml11.load(fs32.readFileSync(file, "utf-8"));
7466
+ const data = yaml11.load(fs33.readFileSync(file, "utf-8"));
7343
7467
  return data?.api_url;
7344
7468
  } catch {
7345
7469
  return void 0;
@@ -7368,10 +7492,10 @@ profile2.command("add <name>").description("Create a new profile directory (use
7368
7492
  fail9(`"${DEFAULT_PROFILE_NAME}" is reserved; it always exists.`);
7369
7493
  }
7370
7494
  const dir = profileDir(name);
7371
- if (fs32.existsSync(dir)) {
7495
+ if (fs33.existsSync(dir)) {
7372
7496
  fail9(`Profile "${name}" already exists at ${dir}.`);
7373
7497
  }
7374
- fs32.mkdirSync(dir, { recursive: true });
7498
+ fs33.mkdirSync(dir, { recursive: true });
7375
7499
  const prev = process.env.TASK0_PROFILE;
7376
7500
  process.env.TASK0_PROFILE = name;
7377
7501
  try {
@@ -7395,14 +7519,14 @@ profile2.command("remove <name>").description('Delete a profile directory (refus
7395
7519
  fail9(`"${DEFAULT_PROFILE_NAME}" cannot be removed.`);
7396
7520
  }
7397
7521
  const dir = profileDir(name);
7398
- if (!fs32.existsSync(dir)) {
7522
+ if (!fs33.existsSync(dir)) {
7399
7523
  fail9(`Profile "${name}" not found.`);
7400
7524
  }
7401
7525
  const current = currentProfileName();
7402
7526
  if (current === name && !opts.force) {
7403
7527
  fail9(`Profile "${name}" is current. Re-run with --force to remove it.`);
7404
7528
  }
7405
- fs32.rmSync(dir, { recursive: true, force: true });
7529
+ fs33.rmSync(dir, { recursive: true, force: true });
7406
7530
  if (current === name) {
7407
7531
  writeCurrentProfile(null);
7408
7532
  console.log(chalk23.dim('Current profile reset to "default".'));
@@ -7413,7 +7537,7 @@ profile2.command("use <name>").description("Set the current profile").action((na
7413
7537
  if (!isValidProfileName(name)) {
7414
7538
  fail9(`Invalid profile name "${name}".`);
7415
7539
  }
7416
- if (!fs32.existsSync(profileDir(name))) {
7540
+ if (!fs33.existsSync(profileDir(name))) {
7417
7541
  const names = listProfileNames();
7418
7542
  fail9(`Profile "${name}" not found. Available: ${names.join(", ")}.`);
7419
7543
  }
@@ -7448,7 +7572,7 @@ profile2.command("show [name]").description("Show a profile's configuration and
7448
7572
  fail9(`Invalid profile name "${target}".`);
7449
7573
  }
7450
7574
  const dir = profileDir(target);
7451
- if (!fs32.existsSync(dir)) {
7575
+ if (!fs33.existsSync(dir)) {
7452
7576
  fail9(`Profile "${target}" not found.`);
7453
7577
  }
7454
7578
  const current = currentProfileName();
@@ -7458,7 +7582,7 @@ profile2.command("show [name]").description("Show a profile's configuration and
7458
7582
  const effective = envOverride ?? apiUrl;
7459
7583
  console.log(`${chalk23.bold(target)}${current === target ? chalk23.green(" (current)") : ""}`);
7460
7584
  console.log(` dir: ${dir}`);
7461
- console.log(` config: ${path31.join(dir, "config.yml")}`);
7585
+ console.log(` config: ${path32.join(dir, "config.yml")}`);
7462
7586
  if (envOverride && apiUrl && envOverride !== apiUrl) {
7463
7587
  console.log(` api_url: ${envOverride} ${chalk23.dim("(TASK0_API_URL env, overrides profile)")}`);
7464
7588
  console.log(` ${chalk23.dim(`profile: ${apiUrl}`)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@task0/cli",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "task0 — task-centric agent workflow CLI",
5
5
  "homepage": "https://github.com/cy0-labs/task0#readme",
6
6
  "repository": {