@workser/cli 0.2.6 → 0.3.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
@@ -4404,6 +4404,156 @@ workser storage get <key> [dest] # download an object (or print its URL)
4404
4404
  infrastructure on the project's own Neon branch, available only on dedicated tenancy
4405
4405
  in a supported region. They are different stores \u2014 a file put in one is not visible
4406
4406
  in the other. See \`reference/neon-backend.md\`.
4407
+ `
4408
+ },
4409
+ {
4410
+ topic: "tasks",
4411
+ title: "Project tasks & subtasks",
4412
+ summary: "The ticket you are working inside: read it, break it into steps, and ask before starting.",
4413
+ commands: ["task"],
4414
+ source: "skills/workser/reference/tasks.md",
4415
+ body: `# Project tasks & subtasks
4416
+
4417
+ A **project task** is a ticket the owner filed. You are usually running inside
4418
+ one \u2014 Orbit sets \`WORKSER_PROJECT_TASK_ID\` on your process, so every command
4419
+ below defaults to it and you rarely pass an id at all.
4420
+
4421
+ \`\`\`
4422
+ workser task list [--status <value>] [--label <value>] [--limit <n>]
4423
+ workser task show [id] # the task you are in, with its steps
4424
+
4425
+ workser task subtask add <title> [--role <value>] [--kind <value>]
4426
+ [--note <text>] [--app <id...>]
4427
+ [--infra <ref...>] [--scope <path...>]
4428
+ [--depends-on <key...>]
4429
+ workser task subtask list [taskId]
4430
+ workser task subtask update <id> [--title|--note|--role|--kind|--scope]
4431
+ workser task subtask remove <id>
4432
+
4433
+ workser task can-start [id] # may work begin? refuses until approved
4434
+ workser task approval request # tell the owner the plan is ready
4435
+ workser task move <id> <status>
4436
+ workser task done [id] --summary <text>
4437
+ \`\`\`
4438
+
4439
+ ## This is not \`workser board\`
4440
+
4441
+ \`board\` is the Orbit Board \u2014 a human's list of work items. \`task\` is the AI Tech
4442
+ Team's own table. Filing your plan on the Board puts it somewhere the owner's
4443
+ task page never reads: they see "created work item" and an empty plan. Use
4444
+ \`task subtask add\`.
4445
+
4446
+ ## Planning a task
4447
+
4448
+ Read the project first, then propose. One \`subtask add\` per step:
4449
+
4450
+ \`\`\`
4451
+ workser task subtask add "Build the upload endpoint" \\
4452
+ --role api --kind service \\
4453
+ --note "Accept a file, work out its type, hand it to the right analyzer." \\
4454
+ --scope src/app/api/analyze/route.ts
4455
+
4456
+ workser task subtask add "Check every supported file type end to end" \\
4457
+ --role qa --depends-on RIZZ-15
4458
+ \`\`\`
4459
+
4460
+ \`--role\` is one of: pm, architect, web, api, automation, qa.
4461
+ \`--kind\` is what the step produces: data_reports, web, mobile, service,
4462
+ automation, docs. It is not the same fact as the role \u2014 the same engineer
4463
+ writing a screen, the docs for it and the service behind it is three kinds of
4464
+ work.
4465
+
4466
+ \`--scope\` is what that step OWNS. Two steps naming the same file cannot run at
4467
+ the same time, so keeping scopes apart is what lets the team work in parallel.
4468
+ \`--depends-on\` takes the keys you read off \`task show\`.
4469
+
4470
+ Between three and six steps. If it needs more, say the task is too big instead.
4471
+ The step that CHECKS work must not be the same role as the one that built it.
4472
+
4473
+ ## Nothing runs until the owner approves
4474
+
4475
+ \`\`\`
4476
+ workser task can-start
4477
+ \`\`\`
4478
+
4479
+ This exits non-zero, with the reason, until they have approved the plan \u2014 that
4480
+ refusal is the product working, not an error to route around. Ask with
4481
+ \`workser task approval request\`; only a person can answer.
4482
+
4483
+ ## Finishing a step
4484
+
4485
+ \`\`\`
4486
+ workser task done --summary "The report now shows cost per KOL, with six months of history."
4487
+ \`\`\`
4488
+
4489
+ Write the summary for someone who runs a business and does not read code.
4490
+ `
4491
+ },
4492
+ {
4493
+ topic: "version-control",
4494
+ title: "Saving, undoing & syncing your work",
4495
+ summary: "Checkpoint before risky edits, restore when they go wrong, sync with Workser.",
4496
+ commands: ["checkpoint", "restore", "sync"],
4497
+ source: "skills/workser/reference/version-control.md",
4498
+ body: `# Saving, undoing & syncing your work
4499
+
4500
+ \`\`\`
4501
+ workser checkpoint ["what you're about to try"] # save the folder as it stands now
4502
+ workser restore # go back to the newest checkpoint
4503
+ workser restore --list # see the checkpoints you can go back to
4504
+ workser restore <ref> # go back to a specific one
4505
+ workser sync [--branch dev] [--app <id>] # reconcile this folder with Workser
4506
+ \`\`\`
4507
+
4508
+ ## Why these exist instead of git
4509
+
4510
+ This folder is a git repository, but **the history is Workser's**. There is no
4511
+ remote: code moves to and from Workser over its API as git bundles, using the
4512
+ same session everything else here uses. No credential is read, and the user's
4513
+ own git keys and identity are never touched.
4514
+
4515
+ That is what makes any of this work on a computer with no access to the
4516
+ Workser-managed repository \u2014 which is every computer. Nobody has repo access;
4517
+ the API does.
4518
+
4519
+ So the ordinary git verbs are wrong here, and these three replace them:
4520
+
4521
+ | Instead of | Run |
4522
+ |---|---|
4523
+ | \`git stash\` | \`workser checkpoint\` |
4524
+ | \`git commit\` | \`workser checkpoint\` |
4525
+ | \`git reset --hard\` / \`git checkout .\` | \`workser restore\` |
4526
+ | \`git pull\` / \`git push\` | \`workser sync\` |
4527
+
4528
+ Reading is always fine: \`git log\`, \`git diff\`, \`git show\`, \`git status\`.
4529
+
4530
+ ## Notes that matter
4531
+
4532
+ - **Checkpoint before anything you might want to take back** \u2014 a refactor across
4533
+ many files, a dependency upgrade, deleting something large. It costs a second.
4534
+ - **\`git stash\` is the one that actually loses work.** Workser publishes what is
4535
+ on disk, so a deploy while your changes are stashed ships the version *without
4536
+ them* \u2014 and if the run is stopped between \`stash\` and \`stash pop\`, the user
4537
+ opens their folder and their work is gone. \`workser checkpoint\` has neither
4538
+ failure mode.
4539
+ - **Restoring never discards anything.** The current state is saved first, then
4540
+ the older state is restored on top. If you restore the wrong thing, restore
4541
+ again. Say this to the user \u2014 it is the reason they can afford to say yes.
4542
+ - **These are the same checkpoints as the app's Undo button.** One history, not
4543
+ two: a checkpoint you take here appears in Workser, and vice versa.
4544
+ - **\`sync\` pulls before it pushes.** If it reports \`diverged\`, both sides changed
4545
+ and it stops rather than picking a winner \u2014 tell the user to resolve it in
4546
+ Workser instead of forcing it.
4547
+ - **These three need the Workser app running on this computer**, because they
4548
+ work with files. If you get \`needs_local_app\`, you are on a machine that only
4549
+ has the CLI and a token \u2014 say so rather than looking for a workaround. Every
4550
+ read-only command (\`projects\`, \`env\`, \`db\`, \`logs\`, \`status\`) still works.
4551
+
4552
+ ## Shipping is separate
4553
+
4554
+ \`workser sync\` reconciles the folder. \`workser deploy\` puts it in front of
4555
+ users. Syncing does not deploy, and deploying does not require you to sync
4556
+ first \u2014 deploy publishes what is on disk. See \`reference/deploy.md\`.
4407
4557
  `
4408
4558
  }
4409
4559
  ];
@@ -4565,11 +4715,37 @@ function buildContext(opts) {
4565
4715
  const projectId = opts.project || process.env.WORKSER_PROJECT_ID || link?.projectId || session.defaultProjectId;
4566
4716
  const runId = process.env.WORKSER_RUN_ID || void 0;
4567
4717
  const conversationId = process.env.WORKSER_CONVERSATION_ID || void 0;
4568
- return { endpoint, socketPath, token, mode: mode2, cwd, projectId, runId, conversationId };
4718
+ const projectTaskId = process.env.WORKSER_PROJECT_TASK_ID || void 0;
4719
+ return {
4720
+ endpoint,
4721
+ socketPath,
4722
+ token,
4723
+ mode: mode2,
4724
+ cwd,
4725
+ projectId,
4726
+ runId,
4727
+ conversationId,
4728
+ projectTaskId
4729
+ };
4569
4730
  }
4570
4731
  function runTarget(ctx) {
4571
4732
  return ctx.runId || "current";
4572
4733
  }
4734
+ function requireLocalApp(ctx, what) {
4735
+ if (ctx.mode === "daemon") return;
4736
+ throw new WorkserError(
4737
+ `\`workser ${what}\` works with the code in this folder, so it needs the Workser app running on this computer.
4738
+
4739
+ This shell is talking to ${ctx.endpoint} instead of a local app.
4740
+
4741
+ \u2022 On your own computer: open Workser and try again.
4742
+ \u2022 On a computer without Workser: install it from https://workser.ai/download,
4743
+ sign in, and open this project \u2014 that is what puts the code here.
4744
+
4745
+ Commands that only read your account (projects, env, db, logs, status) work as normal.`,
4746
+ { code: "needs_local_app" }
4747
+ );
4748
+ }
4573
4749
  function requireProject(ctx) {
4574
4750
  if (!ctx.projectId) {
4575
4751
  throw new WorkserError(
@@ -5521,6 +5697,7 @@ function registerDeploy(program3) {
5521
5697
  "which app to publish (default: the app this folder is linked to)"
5522
5698
  ).action(
5523
5699
  action(async ({ ctx, opts }) => {
5700
+ requireLocalApp(ctx, "deploy");
5524
5701
  const projectId = requireProject(ctx);
5525
5702
  const dep = await api(ctx, `/v1/projects/${projectId}/deploy`, {
5526
5703
  body: {
@@ -5675,6 +5852,7 @@ function openUrl(url) {
5675
5852
 
5676
5853
  // src/commands/doctor.ts
5677
5854
  var import_picocolors13 = __toESM(require_picocolors(), 1);
5855
+ import { spawnSync } from "child_process";
5678
5856
  function registerDoctor(program3) {
5679
5857
  program3.command("doctor").description("Print the resolved endpoint, mode, token presence (masked), and current project").action(
5680
5858
  action(({ ctx, opts }) => {
@@ -5685,12 +5863,14 @@ function registerDoctor(program3) {
5685
5863
  const tokenSource = opts.token ? "--token" : process.env.WORKSER_TOKEN ? "$WORKSER_TOKEN" : session.token ? "session" : void 0;
5686
5864
  const endpointSource = opts.endpoint ? "--endpoint" : process.env.WORKSER_DAEMON_URL ? "$WORKSER_DAEMON_URL" : session.endpoint ? "session" : process.env.WORKSER_API_URL ? "$WORKSER_API_URL" : `cloud-default: ${env}`;
5687
5865
  const projectSource = opts.project ? "--project" : link?.projectId ? ".workser link" : session.defaultProjectId ? "session" : void 0;
5866
+ const git = gitVersion();
5688
5867
  const report = {
5689
5868
  endpoint: ctx.endpoint,
5690
5869
  endpointSource,
5691
5870
  env,
5692
5871
  envIgnored,
5693
5872
  mode: ctx.mode,
5873
+ git: { present: git !== null, version: git },
5694
5874
  token: {
5695
5875
  present: Boolean(ctx.token),
5696
5876
  masked: ctx.token ? maskToken(ctx.token) : null,
@@ -5718,6 +5898,20 @@ function registerDoctor(program3) {
5718
5898
  ` project: ${ctx.projectId ?? import_picocolors13.default.dim("none")}` + (link?.name ? ` ${import_picocolors13.default.dim(`(${link.name})`)}` : "") + (projectSource ? import_picocolors13.default.dim(` [${projectSource}]`) : "")
5719
5899
  );
5720
5900
  line(` cwd: ${ctx.cwd}`);
5901
+ line(` git: ${git ?? import_picocolors13.default.dim("not on this shell's PATH")}`);
5902
+ if (!git) {
5903
+ line("");
5904
+ line(
5905
+ import_picocolors13.default.dim(
5906
+ " Workser brings its own git, so syncing and publishing still work."
5907
+ )
5908
+ );
5909
+ line(
5910
+ import_picocolors13.default.dim(
5911
+ ` Only needed if you want to run git yourself here. ${GIT_INSTALL_HINT}`
5912
+ )
5913
+ );
5914
+ }
5721
5915
  if (envIgnored) {
5722
5916
  line("");
5723
5917
  line(
@@ -5736,6 +5930,24 @@ function registerDoctor(program3) {
5736
5930
  })
5737
5931
  );
5738
5932
  }
5933
+ function gitVersion() {
5934
+ try {
5935
+ const res = spawnSync("git", ["--version"], {
5936
+ encoding: "utf8",
5937
+ timeout: 5e3,
5938
+ // A macOS box with no Command Line Tools has a `git` SHIM that pops a GUI
5939
+ // installer when run. Inheriting stdio would surface that dialog from a
5940
+ // command the user expects to just print a report.
5941
+ stdio: ["ignore", "pipe", "ignore"]
5942
+ });
5943
+ if (res.error || res.status !== 0) return null;
5944
+ const out = (res.stdout || "").toString().trim();
5945
+ return out || null;
5946
+ } catch {
5947
+ return null;
5948
+ }
5949
+ }
5950
+ var GIT_INSTALL_HINT = process.platform === "darwin" ? "Install it with: xcode-select --install" : process.platform === "win32" ? "Install it from: https://git-scm.com/download/win" : "Install it with your package manager, e.g. apt install git";
5739
5951
  function maskToken(token) {
5740
5952
  if (token.length <= 8) return "\u2022".repeat(token.length);
5741
5953
  return `${token.slice(0, 3)}${"\u2022".repeat(Math.min(token.length - 6, 12))}${token.slice(-3)}`;
@@ -5863,6 +6075,7 @@ function registerVerify(program3) {
5863
6075
  "comma-separated subset, e.g. --only typecheck,build"
5864
6076
  ).action(
5865
6077
  action(async ({ ctx, opts }) => {
6078
+ requireLocalApp(ctx, "verify");
5866
6079
  const only = opts.only ? String(opts.only).split(",").map((s) => s.trim()).filter(Boolean) : void 0;
5867
6080
  const res = await api(ctx, `/v1/verify`, {
5868
6081
  body: { cwd: ctx.cwd, ...only ? { only } : {} }
@@ -5890,8 +6103,139 @@ function printVerify(res) {
5890
6103
  );
5891
6104
  }
5892
6105
 
5893
- // src/commands/workflow.ts
6106
+ // src/commands/checkpoint.ts
5894
6107
  var import_picocolors16 = __toESM(require_picocolors(), 1);
6108
+ function registerCheckpoint(program3) {
6109
+ program3.command("checkpoint [label]").description(
6110
+ "Save the current state of this folder so you can come back to it"
6111
+ ).addHelpText(
6112
+ "after",
6113
+ "\nUse this before a risky change. To come back: `workser restore`.\nNever use `git stash` or `git commit` here \u2014 Workser owns this history.\n"
6114
+ ).action(
6115
+ action(async ({ ctx, args }) => {
6116
+ requireLocalApp(ctx, "checkpoint");
6117
+ const label = typeof args[0] === "string" ? args[0] : "";
6118
+ const res = await api(ctx, `/v1/checkpoints`, {
6119
+ body: { cwd: ctx.cwd, ...label ? { label } : {} }
6120
+ });
6121
+ ok(res, () => {
6122
+ const p = res?.point;
6123
+ success(`Saved a checkpoint${p?.label ? `: ${p.label}` : ""}`);
6124
+ if (p?.ref) line(import_picocolors16.default.dim(` ${p.ref.slice(0, 7)}`));
6125
+ line(import_picocolors16.default.dim(" Come back to it with `workser restore`."));
6126
+ });
6127
+ })
6128
+ );
6129
+ const restore = program3.command("restore [ref]").description(
6130
+ "Put this folder back to a saved checkpoint (default: the most recent)"
6131
+ ).option("--list", "show the checkpoints you can go back to", false).action(
6132
+ action(async ({ ctx, args, opts }) => {
6133
+ requireLocalApp(ctx, "restore");
6134
+ if (opts.list) {
6135
+ const res2 = await api(ctx, `/v1/checkpoints`, {
6136
+ query: { cwd: ctx.cwd }
6137
+ });
6138
+ const points = res2?.points ?? [];
6139
+ return ok(res2, () => printPoints(points));
6140
+ }
6141
+ const ref = typeof args[0] === "string" ? args[0].trim() : "";
6142
+ const res = await api(ctx, `/v1/undo`, {
6143
+ body: { cwd: ctx.cwd, ...ref ? { ref } : {} }
6144
+ });
6145
+ ok(res, () => {
6146
+ success(
6147
+ `Put the folder back${res?.restoredTo ? ` to ${String(res.restoredTo).slice(0, 7)}` : ""}`
6148
+ );
6149
+ if (res?.filesChanged) {
6150
+ line(
6151
+ import_picocolors16.default.dim(
6152
+ ` ${res.filesChanged} file${res.filesChanged === 1 ? "" : "s"} changed`
6153
+ )
6154
+ );
6155
+ }
6156
+ line(import_picocolors16.default.dim(" This is reversible: `workser restore` again."));
6157
+ });
6158
+ })
6159
+ );
6160
+ restore.command("list").description("Show the checkpoints you can go back to").action(
6161
+ action(async ({ ctx }) => {
6162
+ requireLocalApp(ctx, "restore list");
6163
+ const res = await api(ctx, `/v1/checkpoints`, {
6164
+ query: { cwd: ctx.cwd }
6165
+ });
6166
+ ok(res, () => printPoints(res?.points ?? []));
6167
+ })
6168
+ );
6169
+ }
6170
+ function printPoints(points) {
6171
+ if (!points.length) {
6172
+ info("No checkpoints yet for this folder.");
6173
+ line(import_picocolors16.default.dim(" Take one with `workser checkpoint`."));
6174
+ return;
6175
+ }
6176
+ line(import_picocolors16.default.bold("Checkpoints"));
6177
+ for (const p of points) {
6178
+ const when = p.at ? new Date(p.at).toLocaleString() : "";
6179
+ line(
6180
+ ` ${import_picocolors16.default.dim(p.ref.slice(0, 7))} ${p.label}${when ? import_picocolors16.default.dim(` ${when}`) : ""}`
6181
+ );
6182
+ }
6183
+ line(
6184
+ import_picocolors16.default.dim(
6185
+ "\nGo back with `workser restore <ref>`, or just `workser restore` for the newest."
6186
+ )
6187
+ );
6188
+ }
6189
+
6190
+ // src/commands/sync.ts
6191
+ var import_picocolors17 = __toESM(require_picocolors(), 1);
6192
+ function registerSync(program3) {
6193
+ program3.command("sync").description(
6194
+ "Reconcile this folder with the copy Workser holds (pull, then push)"
6195
+ ).option("--branch <name>", "which branch to sync (default: this folder's)").option(
6196
+ "--app <webAppId>",
6197
+ "which app's code this folder holds (default: resolved from the folder)"
6198
+ ).addHelpText(
6199
+ "after",
6200
+ "\nUse this instead of `git pull` / `git push` \u2014 this folder has no remote.\n"
6201
+ ).action(
6202
+ action(async ({ ctx, opts }) => {
6203
+ requireLocalApp(ctx, "sync");
6204
+ const projectId = requireProject(ctx);
6205
+ const res = await api(ctx, `/v1/projects/${projectId}/git/sync`, {
6206
+ body: {
6207
+ cwd: ctx.cwd,
6208
+ ...opts.branch ? { branch: opts.branch } : {},
6209
+ ...opts.app ? { webAppId: opts.app } : {}
6210
+ }
6211
+ });
6212
+ const refused = res?.ok === false || res?.state === "diverged";
6213
+ ok(res, () => {
6214
+ if (refused) {
6215
+ warn(res?.message ?? "Couldn't sync this folder.");
6216
+ if (res?.state === "diverged") {
6217
+ line(
6218
+ import_picocolors17.default.dim(
6219
+ " This folder and Workser's copy have both changed. Open Workser to resolve it."
6220
+ )
6221
+ );
6222
+ }
6223
+ return;
6224
+ }
6225
+ if (res?.state === "empty") {
6226
+ line("Nothing to sync yet \u2014 there's no code here or on Workser.");
6227
+ return;
6228
+ }
6229
+ success("Synced");
6230
+ if (res?.ref) line(import_picocolors17.default.dim(` ${String(res.ref).slice(0, 7)}`));
6231
+ });
6232
+ if (refused) process.exitCode = 1;
6233
+ })
6234
+ );
6235
+ }
6236
+
6237
+ // src/commands/workflow.ts
6238
+ var import_picocolors18 = __toESM(require_picocolors(), 1);
5895
6239
  function registerWorkflow(program3) {
5896
6240
  const wf = program3.command("workflow").description("Create, run, and inspect workflow automations for the project");
5897
6241
  wf.command("list").description("List the project's workflows").action(
@@ -5899,10 +6243,10 @@ function registerWorkflow(program3) {
5899
6243
  const projectId = requireProject(ctx);
5900
6244
  const items = await api(ctx, `/v1/projects/${projectId}/workflows`);
5901
6245
  ok(items, () => {
5902
- if (!items?.length) return line(import_picocolors16.default.dim("No workflows yet. `workser workflow create`."));
6246
+ if (!items?.length) return line(import_picocolors18.default.dim("No workflows yet. `workser workflow create`."));
5903
6247
  for (const w of items) {
5904
- const status = w.is_active ? import_picocolors16.default.green("active") : import_picocolors16.default.dim("inactive");
5905
- line(`${w.id} ${import_picocolors16.default.bold(w.name ?? "Untitled")} ${status}`);
6248
+ const status = w.is_active ? import_picocolors18.default.green("active") : import_picocolors18.default.dim("inactive");
6249
+ line(`${w.id} ${import_picocolors18.default.bold(w.name ?? "Untitled")} ${status}`);
5906
6250
  }
5907
6251
  });
5908
6252
  })
@@ -5914,7 +6258,7 @@ function registerWorkflow(program3) {
5914
6258
  const res = await api(ctx, `/v1/projects/${projectId}/workflows`, {
5915
6259
  body: { name: args[0], ...extra }
5916
6260
  });
5917
- ok(res, () => line(`Created workflow ${import_picocolors16.default.bold(res.id)}.`));
6261
+ ok(res, () => line(`Created workflow ${import_picocolors18.default.bold(res.id)}.`));
5918
6262
  })
5919
6263
  );
5920
6264
  wf.command("get <id>").description("Show a workflow's full definition").action(
@@ -5949,8 +6293,8 @@ function registerWorkflow(program3) {
5949
6293
  action(async ({ ctx, args }) => {
5950
6294
  const items = await api(ctx, `/v1/workflows/${args[0]}/executions`);
5951
6295
  ok(items, () => {
5952
- if (!items?.length) return line(import_picocolors16.default.dim("No runs yet."));
5953
- for (const e of items) line(`${e.id} ${e.status ?? ""} ${import_picocolors16.default.dim(e.started_at ?? "")}`);
6296
+ if (!items?.length) return line(import_picocolors18.default.dim("No runs yet."));
6297
+ for (const e of items) line(`${e.id} ${e.status ?? ""} ${import_picocolors18.default.dim(e.started_at ?? "")}`);
5954
6298
  });
5955
6299
  })
5956
6300
  );
@@ -5958,15 +6302,15 @@ function registerWorkflow(program3) {
5958
6302
  action(async ({ ctx, args }) => {
5959
6303
  const items = await api(ctx, `/v1/node-types`, { query: { q: args[0] } });
5960
6304
  ok(items, () => {
5961
- if (!items?.length) return line(import_picocolors16.default.dim("No matching node types."));
5962
- for (const n of items) line(`${n.name ?? n.type} ${import_picocolors16.default.dim(n.category ?? "")}`);
6305
+ if (!items?.length) return line(import_picocolors18.default.dim("No matching node types."));
6306
+ for (const n of items) line(`${n.name ?? n.type} ${import_picocolors18.default.dim(n.category ?? "")}`);
5963
6307
  });
5964
6308
  })
5965
6309
  );
5966
6310
  }
5967
6311
 
5968
6312
  // src/commands/app.ts
5969
- var import_picocolors17 = __toESM(require_picocolors(), 1);
6313
+ var import_picocolors19 = __toESM(require_picocolors(), 1);
5970
6314
  function registerApp(program3) {
5971
6315
  const appCmd = program3.command("app").description("Connect and use third-party app integrations (Gmail, Slack, Stripe, ...)");
5972
6316
  appCmd.command("list").description("List connectable toolkits and this project's existing connections").option("--toolkit <slug>", "filter connections to one toolkit").action(
@@ -5979,8 +6323,8 @@ function registerApp(program3) {
5979
6323
  ok({ catalog, connections }, () => {
5980
6324
  const connected = new Set((connections ?? []).map((c) => c.toolkit ?? c.composio_app));
5981
6325
  for (const t of catalog ?? []) {
5982
- const status = connected.has(t.slug) ? import_picocolors17.default.green("connected") : import_picocolors17.default.dim("not connected");
5983
- line(`${t.slug} ${import_picocolors17.default.bold(t.name ?? t.slug)} ${status}`);
6326
+ const status = connected.has(t.slug) ? import_picocolors19.default.green("connected") : import_picocolors19.default.dim("not connected");
6327
+ line(`${t.slug} ${import_picocolors19.default.bold(t.name ?? t.slug)} ${status}`);
5984
6328
  }
5985
6329
  });
5986
6330
  })
@@ -5997,7 +6341,7 @@ function registerApp(program3) {
5997
6341
  });
5998
6342
  ok(
5999
6343
  res,
6000
- () => res.oauth_url ? line(`Open this URL to finish connecting: ${import_picocolors17.default.underline(res.oauth_url)}`) : line(`Connection ${res.connection_id} is ${res.status}.`)
6344
+ () => res.oauth_url ? line(`Open this URL to finish connecting: ${import_picocolors19.default.underline(res.oauth_url)}`) : line(`Connection ${res.connection_id} is ${res.status}.`)
6001
6345
  );
6002
6346
  })
6003
6347
  );
@@ -6015,8 +6359,8 @@ function registerApp(program3) {
6015
6359
  const projectId = requireProject(ctx);
6016
6360
  const items = await api(ctx, `/v1/projects/${projectId}/integrations/${args[0]}/tools`);
6017
6361
  ok(items, () => {
6018
- if (!items?.length) return line(import_picocolors17.default.dim("No tools found."));
6019
- for (const t of items) line(`${t.slug} ${import_picocolors17.default.dim(t.description ?? "")}`);
6362
+ if (!items?.length) return line(import_picocolors19.default.dim("No tools found."));
6363
+ for (const t of items) line(`${t.slug} ${import_picocolors19.default.dim(t.description ?? "")}`);
6020
6364
  });
6021
6365
  })
6022
6366
  );
@@ -6032,7 +6376,7 @@ function registerApp(program3) {
6032
6376
  }
6033
6377
 
6034
6378
  // src/commands/tool.ts
6035
- var import_picocolors18 = __toESM(require_picocolors(), 1);
6379
+ var import_picocolors20 = __toESM(require_picocolors(), 1);
6036
6380
  function registerTool(program3) {
6037
6381
  const tool = program3.command("tool").description(
6038
6382
  "Computer-use tools: filesystem, shell, screenshot, input control, clipboard, notifications, basic browser"
@@ -6041,7 +6385,7 @@ function registerTool(program3) {
6041
6385
  action(async ({ ctx }) => {
6042
6386
  const tools = await api(ctx, "/v1/tool/list");
6043
6387
  ok(tools, () => {
6044
- if (!tools?.length) return line(import_picocolors18.default.dim("No tools available."));
6388
+ if (!tools?.length) return line(import_picocolors20.default.dim("No tools available."));
6045
6389
  const byCategory = /* @__PURE__ */ new Map();
6046
6390
  for (const t of tools) {
6047
6391
  const list = byCategory.get(t.category) ?? [];
@@ -6049,9 +6393,9 @@ function registerTool(program3) {
6049
6393
  byCategory.set(t.category, list);
6050
6394
  }
6051
6395
  for (const [category, items] of byCategory) {
6052
- line(import_picocolors18.default.bold(category) + ":");
6396
+ line(import_picocolors20.default.bold(category) + ":");
6053
6397
  for (const t of items) {
6054
- line(` ${t.name} ${import_picocolors18.default.dim(t.description ?? "")}`);
6398
+ line(` ${t.name} ${import_picocolors20.default.dim(t.description ?? "")}`);
6055
6399
  }
6056
6400
  }
6057
6401
  });
@@ -6069,7 +6413,7 @@ function registerTool(program3) {
6069
6413
  }
6070
6414
 
6071
6415
  // src/commands/memory.ts
6072
- var import_picocolors19 = __toESM(require_picocolors(), 1);
6416
+ var import_picocolors21 = __toESM(require_picocolors(), 1);
6073
6417
  function registerMemory(program3) {
6074
6418
  const memory = program3.command("memory").description("Durable, cross-conversation project memory (shared with cloud agents on the same project)");
6075
6419
  memory.command("add <content>").description("Store something worth remembering across future conversations").option("--metadata <json>", "extra metadata for filtering, as a JSON string").option("--id <customId>", "custom id for dedup/updates").action(
@@ -6093,9 +6437,9 @@ function registerMemory(program3) {
6093
6437
  });
6094
6438
  ok(res, () => {
6095
6439
  const results = res?.results ?? res ?? [];
6096
- if (!results?.length) return line(import_picocolors19.default.dim("No matching memories."));
6440
+ if (!results?.length) return line(import_picocolors21.default.dim("No matching memories."));
6097
6441
  for (const r of results) {
6098
- line(`${import_picocolors19.default.dim(r.id ?? "?")} ${r.memory ?? r.content ?? ""}`);
6442
+ line(`${import_picocolors21.default.dim(r.id ?? "?")} ${r.memory ?? r.content ?? ""}`);
6099
6443
  }
6100
6444
  });
6101
6445
  })
@@ -6112,7 +6456,7 @@ function registerMemory(program3) {
6112
6456
  }
6113
6457
 
6114
6458
  // src/commands/business.ts
6115
- var import_picocolors20 = __toESM(require_picocolors(), 1);
6459
+ var import_picocolors22 = __toESM(require_picocolors(), 1);
6116
6460
  var RESOURCE_PATHS = {
6117
6461
  "business-config": "business-config",
6118
6462
  "business-settings": "business-settings",
@@ -6172,7 +6516,7 @@ function registerBusiness(program3) {
6172
6516
  const projectId = requireProject(ctx);
6173
6517
  const [resource] = args;
6174
6518
  const res = await api(ctx, businessPath(projectId, resource), { body: JSON.parse(opts.body) });
6175
- ok(res, () => line(`Created ${resource} ${import_picocolors20.default.bold(res?.id ?? "")}.`));
6519
+ ok(res, () => line(`Created ${resource} ${import_picocolors22.default.bold(res?.id ?? "")}.`));
6176
6520
  })
6177
6521
  );
6178
6522
  biz.command("update <resource> <id>").description("Update a record by id (PATCH/PUT \u2014 matches the underlying route)").option("--body <json>", "changed fields as a JSON object string", "{}").action(
@@ -6214,7 +6558,7 @@ function businessPath(projectId, resource, subpath) {
6214
6558
  }
6215
6559
 
6216
6560
  // src/commands/artifact.ts
6217
- var import_picocolors21 = __toESM(require_picocolors(), 1);
6561
+ var import_picocolors23 = __toESM(require_picocolors(), 1);
6218
6562
  import { existsSync as existsSync2, statSync } from "fs";
6219
6563
  import { resolve as resolve2, basename as basename3 } from "path";
6220
6564
  var KINDS = [
@@ -6280,7 +6624,7 @@ function registerArtifact(program3) {
6280
6624
  ok(
6281
6625
  res,
6282
6626
  () => success(
6283
- `Recorded ${import_picocolors21.default.bold(res?.title ?? "artifact")}${res?.kind ? import_picocolors21.default.dim(` (${res.kind})`) : ""}`
6627
+ `Recorded ${import_picocolors23.default.bold(res?.title ?? "artifact")}${res?.kind ? import_picocolors23.default.dim(` (${res.kind})`) : ""}`
6284
6628
  )
6285
6629
  );
6286
6630
  })
@@ -6294,11 +6638,11 @@ function registerArtifact(program3) {
6294
6638
  }
6295
6639
  function printRun(run) {
6296
6640
  if (!run) return;
6297
- line(` run ${import_picocolors21.default.bold(run.runId)}`);
6641
+ line(` run ${import_picocolors23.default.bold(run.runId)}`);
6298
6642
  if (run.taskId) line(` task ${run.taskId}`);
6299
6643
  if (run.conversationId) line(` chat ${run.conversationId}`);
6300
6644
  if (run.projectId) line(` project ${run.projectId}`);
6301
- if (run.cwd) line(` folder ${import_picocolors21.default.dim(run.cwd)}`);
6645
+ if (run.cwd) line(` folder ${import_picocolors23.default.dim(run.cwd)}`);
6302
6646
  }
6303
6647
 
6304
6648
  // src/commands/image.ts
@@ -6372,7 +6716,7 @@ async function download(url, output) {
6372
6716
  }
6373
6717
 
6374
6718
  // src/commands/ask.ts
6375
- var import_picocolors22 = __toESM(require_picocolors(), 1);
6719
+ var import_picocolors24 = __toESM(require_picocolors(), 1);
6376
6720
  var TYPES = [
6377
6721
  "input",
6378
6722
  "choice",
@@ -6423,7 +6767,7 @@ function registerAsk(program3) {
6423
6767
  code: "bad_request"
6424
6768
  });
6425
6769
  }
6426
- info(import_picocolors22.default.dim("Waiting for the user to answer\u2026"));
6770
+ info(import_picocolors24.default.dim("Waiting for the user to answer\u2026"));
6427
6771
  const res = await api(ctx, `/v1/runs/${runTarget(ctx)}/ask`, {
6428
6772
  body: {
6429
6773
  type,
@@ -6449,12 +6793,12 @@ function deriveTitle(message) {
6449
6793
  function printAnswer(res) {
6450
6794
  if (!res) return;
6451
6795
  if (res.status === "answered") {
6452
- line(` ${import_picocolors22.default.green("answered")}`);
6796
+ line(` ${import_picocolors24.default.green("answered")}`);
6453
6797
  const value = extract(res.response);
6454
6798
  if (value) line(` ${value}`);
6455
6799
  return;
6456
6800
  }
6457
- line(` ${import_picocolors22.default.yellow(res.status)} ${import_picocolors22.default.dim(res.reason ?? "")}`);
6801
+ line(` ${import_picocolors24.default.yellow(res.status)} ${import_picocolors24.default.dim(res.reason ?? "")}`);
6458
6802
  }
6459
6803
  function extract(response) {
6460
6804
  if (response == null) return "";
@@ -6473,7 +6817,7 @@ function extract(response) {
6473
6817
  }
6474
6818
 
6475
6819
  // src/commands/search.ts
6476
- var import_picocolors23 = __toESM(require_picocolors(), 1);
6820
+ var import_picocolors25 = __toESM(require_picocolors(), 1);
6477
6821
  function registerSearch(program3) {
6478
6822
  program3.command("search <query>").description("Search the web (Google-grounded, server-side)").option("-n, --max-results <n>", "max results", "5").action(
6479
6823
  action(async ({ ctx, args, opts }) => {
@@ -6486,9 +6830,9 @@ function registerSearch(program3) {
6486
6830
  line("");
6487
6831
  }
6488
6832
  const results = res?.results ?? [];
6489
- if (!results.length) return line(import_picocolors23.default.dim("No results."));
6833
+ if (!results.length) return line(import_picocolors25.default.dim("No results."));
6490
6834
  for (const r of results) {
6491
- line(`${r.title || import_picocolors23.default.dim("(untitled)")} ${import_picocolors23.default.dim(r.url)}`);
6835
+ line(`${r.title || import_picocolors25.default.dim("(untitled)")} ${import_picocolors25.default.dim(r.url)}`);
6492
6836
  }
6493
6837
  });
6494
6838
  })
@@ -6496,7 +6840,7 @@ function registerSearch(program3) {
6496
6840
  }
6497
6841
 
6498
6842
  // src/commands/board.ts
6499
- var import_picocolors24 = __toESM(require_picocolors(), 1);
6843
+ var import_picocolors26 = __toESM(require_picocolors(), 1);
6500
6844
 
6501
6845
  // src/commands/record-step.ts
6502
6846
  async function recordEntityStep(ctx, opts) {
@@ -6535,7 +6879,7 @@ function registerBoard(program3) {
6535
6879
  }
6536
6880
  ok(rows, () => {
6537
6881
  if (!rows.length) {
6538
- line(import_picocolors24.default.dim("No cards on the Board yet."));
6882
+ line(import_picocolors26.default.dim("No cards on the Board yet."));
6539
6883
  return;
6540
6884
  }
6541
6885
  for (const r of rows) line(formatRow(r));
@@ -6550,7 +6894,7 @@ function registerBoard(program3) {
6550
6894
  `/v1/projects/${projectId}/work-items/${args[0]}`
6551
6895
  );
6552
6896
  ok(row, () => {
6553
- line(`${import_picocolors24.default.bold(row.title)} ${import_picocolors24.default.dim(row.id)}`);
6897
+ line(`${import_picocolors26.default.bold(row.title)} ${import_picocolors26.default.dim(row.id)}`);
6554
6898
  line(`${statusTag(row.status)} priority ${row.priority}`);
6555
6899
  if (row.ownerHuman) line(`owner: ${row.ownerHuman}`);
6556
6900
  if (row.labels?.length) line(`labels: ${row.labels.join(", ")}`);
@@ -6591,7 +6935,7 @@ ${row.description}`);
6591
6935
  refId: row?.id,
6592
6936
  output: { workItem: row }
6593
6937
  });
6594
- ok(row, () => line(`Created work item ${import_picocolors24.default.bold(row?.id ?? "")} \u2014 ${title}`));
6938
+ ok(row, () => line(`Created work item ${import_picocolors26.default.bold(row?.id ?? "")} \u2014 ${title}`));
6595
6939
  })
6596
6940
  );
6597
6941
  board.command("update <id>").description("Change fields on a card \u2014 pass only what changes").option("--title <text>", "new title").option("--description <text>", "new description").option("--status <value>", STATUSES.join(" | ")).option("--priority <value>", PRIORITIES.join(" | ")).option(
@@ -6625,7 +6969,7 @@ ${row.description}`);
6625
6969
  );
6626
6970
  }
6627
6971
  const row = await patchItem(ctx, projectId, String(args[0]), body);
6628
- ok(row, () => line(`Updated ${import_picocolors24.default.bold(row.id)} \u2014 ${row.title} ${statusTag(row.status)}`));
6972
+ ok(row, () => line(`Updated ${import_picocolors26.default.bold(row.id)} \u2014 ${row.title} ${statusTag(row.status)}`));
6629
6973
  })
6630
6974
  );
6631
6975
  board.command("move <id> <status>").description(
@@ -6636,14 +6980,14 @@ ${row.description}`);
6636
6980
  const status = String(args[1]);
6637
6981
  assertStatus(status);
6638
6982
  const row = await patchItem(ctx, projectId, String(args[0]), { status });
6639
- ok(row, () => line(`Moved ${import_picocolors24.default.bold(row.title)} \u2192 ${statusTag(row.status)}`));
6983
+ ok(row, () => line(`Moved ${import_picocolors26.default.bold(row.title)} \u2192 ${statusTag(row.status)}`));
6640
6984
  })
6641
6985
  );
6642
6986
  board.command("close <id>").description("Shorthand for `board move <id> done`").action(
6643
6987
  action(async ({ ctx, args }) => {
6644
6988
  const projectId = requireProject(ctx);
6645
6989
  const row = await patchItem(ctx, projectId, String(args[0]), { status: "done" });
6646
- ok(row, () => line(`Closed ${import_picocolors24.default.bold(row.title)} ${statusTag(row.status)}`));
6990
+ ok(row, () => line(`Closed ${import_picocolors26.default.bold(row.title)} ${statusTag(row.status)}`));
6647
6991
  })
6648
6992
  );
6649
6993
  }
@@ -6676,23 +7020,293 @@ function assertPriority(value) {
6676
7020
  }
6677
7021
  }
6678
7022
  function formatRow(r) {
6679
- const labels = r.labels?.length ? import_picocolors24.default.dim(` [${r.labels.join(", ")}]`) : "";
6680
- const owner = r.ownerHuman ? import_picocolors24.default.dim(` @${r.ownerHuman}`) : "";
6681
- return `${import_picocolors24.default.dim(r.id)} ${statusTag(r.status)} ${r.title}${labels}${owner}`;
7023
+ const labels = r.labels?.length ? import_picocolors26.default.dim(` [${r.labels.join(", ")}]`) : "";
7024
+ const owner = r.ownerHuman ? import_picocolors26.default.dim(` @${r.ownerHuman}`) : "";
7025
+ return `${import_picocolors26.default.dim(r.id)} ${statusTag(r.status)} ${r.title}${labels}${owner}`;
6682
7026
  }
6683
7027
  function statusTag(status) {
6684
7028
  const label = status.padEnd(11);
6685
- if (status === "done") return import_picocolors24.default.green(label);
6686
- if (status === "in-progress") return import_picocolors24.default.yellow(label);
6687
- if (status === "in-review") return import_picocolors24.default.cyan(label);
6688
- return import_picocolors24.default.dim(label);
7029
+ if (status === "done") return import_picocolors26.default.green(label);
7030
+ if (status === "in-progress") return import_picocolors26.default.yellow(label);
7031
+ if (status === "in-review") return import_picocolors26.default.cyan(label);
7032
+ return import_picocolors26.default.dim(label);
6689
7033
  }
6690
7034
  function collect2(value, previous) {
6691
7035
  return [...previous, value];
6692
7036
  }
6693
7037
 
7038
+ // src/commands/task.ts
7039
+ var import_picocolors27 = __toESM(require_picocolors(), 1);
7040
+ var STATUSES2 = ["todo", "working", "checking", "ready", "accepted", "archived"];
7041
+ var ROLES = ["pm", "architect", "web", "api", "automation", "qa"];
7042
+ var KINDS2 = ["data_reports", "web", "mobile", "service", "automation", "docs"];
7043
+ function registerTask(program3) {
7044
+ const task = program3.command("task").description("The project's tasks and their subtasks (AI Tech Team)");
7045
+ task.command("list").description("List the board's tasks \u2014 run this before starting anything").option("--status <value>", `only tasks in this status (${STATUSES2.join(" | ")})`).option("--label <value>", "only tasks carrying this label").option("--limit <n>", "cap the number of tasks returned").action(
7046
+ action(async ({ ctx, opts }) => {
7047
+ requireProject(ctx);
7048
+ if (opts.status !== void 0) assertOneOf("--status", opts.status, STATUSES2);
7049
+ const rows = await api(ctx, "/v1/project-tasks", {
7050
+ query: {
7051
+ status: opts.status,
7052
+ label: opts.label,
7053
+ limit: opts.limit
7054
+ }
7055
+ }) ?? [];
7056
+ ok(rows, () => {
7057
+ if (!rows.length) {
7058
+ line(import_picocolors27.default.dim("No tasks on the board yet."));
7059
+ return;
7060
+ }
7061
+ for (const r of rows) line(formatRow2(r));
7062
+ });
7063
+ })
7064
+ );
7065
+ task.command("show [id]").description(
7066
+ "Show one task with its subtasks. Defaults to the task this run is inside."
7067
+ ).action(
7068
+ action(async ({ ctx, args }) => {
7069
+ const id = resolveTaskId(ctx, args[0]);
7070
+ const row = await api(ctx, `/v1/project-tasks/${encodeURIComponent(id)}`);
7071
+ ok(row, () => printTask(row));
7072
+ })
7073
+ );
7074
+ const subtask = task.command("subtask").description("The steps a task is broken into");
7075
+ subtask.command("add <title>").description(
7076
+ 'Add one step, e.g. `workser task subtask add "Build the upload screen" --role web`'
7077
+ ).option("--task <id>", "the parent task (defaults to the task this run is inside)").option("--role <value>", `who does it (${ROLES.join(" | ")})`).option("--kind <value>", `what it produces (${KINDS2.join(" | ")})`).option("--note <text>", "one sentence on what this step does").option("--app <id...>", "app ids this step touches").option("--infra <ref...>", "shared setup it touches (database | storage | auth | hosting | jobs)").option("--scope <path...>", "files or folders THIS step owns").option("--depends-on <id...>", "steps that must finish first (key or id)").action(
7078
+ action(async ({ ctx, args, opts }) => {
7079
+ const parent = resolveTaskId(ctx, opts.task);
7080
+ if (opts.role !== void 0) assertOneOf("--role", opts.role, ROLES);
7081
+ if (opts.kind !== void 0) assertOneOf("--kind", opts.kind, KINDS2);
7082
+ const dependsOn = await resolveDeps(ctx, parent, opts.dependsOn ?? []);
7083
+ const row = await api(ctx, "/v1/project-tasks", {
7084
+ body: {
7085
+ parentTaskId: parent,
7086
+ title: args[0],
7087
+ summary: opts.note,
7088
+ role: opts.role,
7089
+ category: opts.kind,
7090
+ scopePaths: opts.scope,
7091
+ dependsOn,
7092
+ targets: [
7093
+ ...(opts.app ?? []).map((appId) => ({ kind: "app", appId })),
7094
+ ...(opts.infra ?? []).map((ref) => ({ kind: "infra", ref }))
7095
+ ]
7096
+ }
7097
+ });
7098
+ ok(row, () => {
7099
+ line(`${import_picocolors27.default.green("added")} ${import_picocolors27.default.bold(row.title)} ${import_picocolors27.default.dim(row.key ?? row.id)}`);
7100
+ if (row.role) line(import_picocolors27.default.dim(`role: ${row.role}`));
7101
+ });
7102
+ })
7103
+ );
7104
+ subtask.command("list [taskId]").description("The steps of a task, in order").action(
7105
+ action(async ({ ctx, args }) => {
7106
+ const id = resolveTaskId(ctx, args[0]);
7107
+ const row = await api(ctx, `/v1/project-tasks/${encodeURIComponent(id)}`);
7108
+ const rows = row.subtasks ?? [];
7109
+ ok(rows, () => {
7110
+ if (!rows.length) {
7111
+ line(import_picocolors27.default.dim("No steps yet."));
7112
+ return;
7113
+ }
7114
+ rows.forEach((r, i) => line(formatSubtask(r, i + 1)));
7115
+ });
7116
+ })
7117
+ );
7118
+ subtask.command("update <id>").description("Change a step \u2014 its title, its role, what it touches").option("--title <text>").option("--note <text>").option("--role <value>", ROLES.join(" | ")).option("--kind <value>", KINDS2.join(" | ")).option("--scope <path...>", "replaces the step's scope entirely").action(
7119
+ action(async ({ ctx, args, opts }) => {
7120
+ if (opts.role !== void 0) assertOneOf("--role", opts.role, ROLES);
7121
+ if (opts.kind !== void 0) assertOneOf("--kind", opts.kind, KINDS2);
7122
+ const row = await api(
7123
+ ctx,
7124
+ `/v1/project-tasks/${encodeURIComponent(args[0])}`,
7125
+ {
7126
+ method: "PATCH",
7127
+ body: {
7128
+ title: opts.title,
7129
+ summary: opts.note,
7130
+ role: opts.role,
7131
+ category: opts.kind,
7132
+ scopePaths: opts.scope
7133
+ }
7134
+ }
7135
+ );
7136
+ ok(row, () => line(`${import_picocolors27.default.green("updated")} ${import_picocolors27.default.bold(row.title)}`));
7137
+ })
7138
+ );
7139
+ subtask.command("remove <id>").description("Drop a step from the plan (only before the work starts)").action(
7140
+ action(async ({ ctx, args }) => {
7141
+ await api(ctx, `/v1/project-tasks/${encodeURIComponent(args[0])}`, {
7142
+ method: "DELETE"
7143
+ });
7144
+ ok({ removed: args[0] }, () => line(import_picocolors27.default.green("removed")));
7145
+ })
7146
+ );
7147
+ task.command("move <id> <status>").description(`Move a task or step along the board (${STATUSES2.join(" | ")})`).action(
7148
+ action(async ({ ctx, args }) => {
7149
+ assertOneOf("<status>", args[1], STATUSES2);
7150
+ const row = await api(
7151
+ ctx,
7152
+ `/v1/project-tasks/${encodeURIComponent(args[0])}/move`,
7153
+ { body: { status: args[1] } }
7154
+ );
7155
+ ok(row, () => line(`${import_picocolors27.default.green("moved")} ${import_picocolors27.default.bold(row.title)} \u2192 ${args[1]}`));
7156
+ })
7157
+ );
7158
+ task.command("can-start [id]").description("Ask whether work on this task may begin. Refuses until the owner approves.").action(
7159
+ action(async ({ ctx, args }) => {
7160
+ const id = resolveTaskId(ctx, args[0]);
7161
+ const row = await api(
7162
+ ctx,
7163
+ `/v1/project-tasks/${encodeURIComponent(id)}/dispatch-check`,
7164
+ { method: "POST" }
7165
+ );
7166
+ ok(row, () => line(import_picocolors27.default.green("approved \u2014 you may start")));
7167
+ })
7168
+ );
7169
+ task.command("approval").description("Ask the owner to approve the plan, or record their decision").argument("<request|approve|decline>").option("--task <id>", "defaults to the task this run is inside").option("--note <text>", "why").action(
7170
+ action(async ({ ctx, args, opts }) => {
7171
+ const id = resolveTaskId(ctx, opts.task);
7172
+ const what = args[0];
7173
+ if (what === "request") {
7174
+ const row2 = await api(
7175
+ ctx,
7176
+ `/v1/project-tasks/${encodeURIComponent(id)}`
7177
+ );
7178
+ ok({ awaiting: row2.approval_state === "awaiting", task: row2 }, () => {
7179
+ line(
7180
+ row2.approval_state === "awaiting" ? import_picocolors27.default.yellow("The plan is waiting on the owner. They see it in the task.") : `Already ${row2.approval_state}.`
7181
+ );
7182
+ });
7183
+ return;
7184
+ }
7185
+ if (what !== "approve" && what !== "decline") {
7186
+ throw new WorkserError(
7187
+ `Expected "request", "approve" or "decline", got "${what}".`,
7188
+ { code: "bad_request" }
7189
+ );
7190
+ }
7191
+ const row = await api(
7192
+ ctx,
7193
+ `/v1/project-tasks/${encodeURIComponent(id)}/approval`,
7194
+ {
7195
+ body: {
7196
+ decision: what === "approve" ? "approved" : "declined",
7197
+ note: opts.note
7198
+ }
7199
+ }
7200
+ );
7201
+ ok(row, () => line(`${import_picocolors27.default.green(row.approval_state)} ${import_picocolors27.default.bold(row.title)}`));
7202
+ })
7203
+ );
7204
+ task.command("done [id]").description("Record what a step produced, and move it to ready").option("--summary <text>", "what changed, in the owner's words").action(
7205
+ action(async ({ ctx, args, opts }) => {
7206
+ const id = resolveTaskId(ctx, args[0]);
7207
+ await api(ctx, `/v1/project-tasks/${encodeURIComponent(id)}`, {
7208
+ method: "PATCH",
7209
+ body: { resultSummary: opts.summary }
7210
+ });
7211
+ const row = await api(
7212
+ ctx,
7213
+ `/v1/project-tasks/${encodeURIComponent(id)}/move`,
7214
+ { body: { status: "ready" } }
7215
+ );
7216
+ ok(row, () => line(`${import_picocolors27.default.green("ready")} ${import_picocolors27.default.bold(row.title)}`));
7217
+ })
7218
+ );
7219
+ }
7220
+ function resolveTaskId(ctx, given) {
7221
+ const id = given || ctx.projectTaskId;
7222
+ if (!id) {
7223
+ throw new WorkserError(
7224
+ "No task. This run isn't inside one (WORKSER_PROJECT_TASK_ID is unset), so pass --task <id or key>.",
7225
+ { code: "no_task" }
7226
+ );
7227
+ }
7228
+ return id;
7229
+ }
7230
+ async function resolveDeps(ctx, parentId, given) {
7231
+ if (!given.length) return void 0;
7232
+ const parent = await api(
7233
+ ctx,
7234
+ `/v1/project-tasks/${encodeURIComponent(parentId)}`
7235
+ );
7236
+ const byKey = /* @__PURE__ */ new Map();
7237
+ for (const s of parent.subtasks ?? []) {
7238
+ byKey.set(s.id, s.id);
7239
+ if (s.key) byKey.set(s.key, s.id);
7240
+ }
7241
+ return given.map((g) => {
7242
+ const id = byKey.get(g);
7243
+ if (!id) {
7244
+ throw new WorkserError(
7245
+ `"${g}" is not a step of this task. Run \`workser task subtask list\` to see them.`,
7246
+ { code: "bad_request" }
7247
+ );
7248
+ }
7249
+ return id;
7250
+ });
7251
+ }
7252
+ function assertOneOf(flag, value, allowed) {
7253
+ if (!allowed.includes(value)) {
7254
+ throw new WorkserError(
7255
+ `${flag} must be one of: ${allowed.join(", ")} \u2014 got "${value}".`,
7256
+ { code: "bad_request" }
7257
+ );
7258
+ }
7259
+ }
7260
+ function formatRow2(r) {
7261
+ const key = import_picocolors27.default.dim((r.key ?? r.id.slice(0, 8)).padEnd(10));
7262
+ const steps = r.subtaskTotal ? import_picocolors27.default.dim(` ${r.subtaskDone}/${r.subtaskTotal}`) : "";
7263
+ const gate = r.approval_state === "awaiting" ? import_picocolors27.default.yellow(" awaiting approval") : "";
7264
+ return `${key} ${statusTag2(r.status)} ${r.title}${steps}${gate}`;
7265
+ }
7266
+ function formatSubtask(r, index) {
7267
+ const n = import_picocolors27.default.dim(String(index).padStart(2, "0"));
7268
+ const role = r.role ? import_picocolors27.default.dim(` [${r.role}]`) : "";
7269
+ const scope = r.scope_paths?.length ? import_picocolors27.default.dim(` owns: ${r.scope_paths.join(", ")}`) : "";
7270
+ return `${n} ${statusTag2(r.status)} ${r.title}${role}${scope}`;
7271
+ }
7272
+ function printTask(row) {
7273
+ line(`${import_picocolors27.default.bold(row.title)} ${import_picocolors27.default.dim(row.key ?? row.id)}`);
7274
+ line(`${statusTag2(row.status)} approval: ${row.approval_state}`);
7275
+ if (row.summary) line(`
7276
+ ${row.summary}`);
7277
+ if (row.targets?.length) {
7278
+ line(
7279
+ `
7280
+ touches: ${row.targets.map((t) => t.appName ?? t.ref ?? t.kind).join(", ")}`
7281
+ );
7282
+ }
7283
+ if (row.subtasks?.length) {
7284
+ line(`
7285
+ ${import_picocolors27.default.bold("steps")}`);
7286
+ row.subtasks.forEach((s, i) => line(formatSubtask(s, i + 1)));
7287
+ } else {
7288
+ line(import_picocolors27.default.dim("\nNo steps yet."));
7289
+ }
7290
+ }
7291
+ function statusTag2(status) {
7292
+ switch (status) {
7293
+ case "ready":
7294
+ return import_picocolors27.default.green("[ready]");
7295
+ case "working":
7296
+ return import_picocolors27.default.blue("[working]");
7297
+ case "checking":
7298
+ return import_picocolors27.default.cyan("[checking]");
7299
+ case "accepted":
7300
+ return import_picocolors27.default.green("[accepted]");
7301
+ case "archived":
7302
+ return import_picocolors27.default.dim("[archived]");
7303
+ default:
7304
+ return import_picocolors27.default.dim("[todo]");
7305
+ }
7306
+ }
7307
+
6694
7308
  // src/commands/decision.ts
6695
- var import_picocolors25 = __toESM(require_picocolors(), 1);
7309
+ var import_picocolors28 = __toESM(require_picocolors(), 1);
6696
7310
  function registerDecision(program3) {
6697
7311
  const decision = program3.command("decision").description("Read and record the project's architecture decisions");
6698
7312
  decision.command("list").description("Every decision on record \u2014 read this before changing how something works").option("--limit <n>", "cap the number returned (newest first)").action(
@@ -6705,11 +7319,11 @@ function registerDecision(program3) {
6705
7319
  rows = applyLimit(rows, opts.limit);
6706
7320
  ok(rows, () => {
6707
7321
  if (!rows.length) {
6708
- line(import_picocolors25.default.dim("No decisions recorded yet."));
7322
+ line(import_picocolors28.default.dim("No decisions recorded yet."));
6709
7323
  return;
6710
7324
  }
6711
7325
  for (const r of rows) {
6712
- line(`${import_picocolors25.default.dim(r.id)} ${import_picocolors25.default.dim(shortDate(r.createdAt))} ${r.title}`);
7326
+ line(`${import_picocolors28.default.dim(r.id)} ${import_picocolors28.default.dim(shortDate(r.createdAt))} ${r.title}`);
6713
7327
  line(` ${truncate(r.decision, 100)}`);
6714
7328
  }
6715
7329
  });
@@ -6723,16 +7337,16 @@ function registerDecision(program3) {
6723
7337
  `/v1/projects/${projectId}/architecture-decisions/${args[0]}`
6724
7338
  );
6725
7339
  ok(row, () => {
6726
- line(`${import_picocolors25.default.bold(row.title)} ${import_picocolors25.default.dim(row.id)}`);
6727
- line(import_picocolors25.default.dim(`${row.status} \xB7 ${shortDate(row.createdAt)}`));
7340
+ line(`${import_picocolors28.default.bold(row.title)} ${import_picocolors28.default.dim(row.id)}`);
7341
+ line(import_picocolors28.default.dim(`${row.status} \xB7 ${shortDate(row.createdAt)}`));
6728
7342
  line(`
6729
- ${import_picocolors25.default.bold("Context")}
7343
+ ${import_picocolors28.default.bold("Context")}
6730
7344
  ${row.context}`);
6731
7345
  line(`
6732
- ${import_picocolors25.default.bold("Decision")}
7346
+ ${import_picocolors28.default.bold("Decision")}
6733
7347
  ${row.decision}`);
6734
7348
  if (row.consequences) line(`
6735
- ${import_picocolors25.default.bold("Consequences")}
7349
+ ${import_picocolors28.default.bold("Consequences")}
6736
7350
  ${row.consequences}`);
6737
7351
  });
6738
7352
  })
@@ -6761,7 +7375,7 @@ ${row.consequences}`);
6761
7375
  refId: row?.id,
6762
7376
  output: { decision: row }
6763
7377
  });
6764
- ok(row, () => line(`Recorded decision ${import_picocolors25.default.bold(row?.id ?? "")} \u2014 ${title}`));
7378
+ ok(row, () => line(`Recorded decision ${import_picocolors28.default.bold(row?.id ?? "")} \u2014 ${title}`));
6765
7379
  })
6766
7380
  );
6767
7381
  const requirement = program3.command("requirement").description("Read and record the project's requirements");
@@ -6773,11 +7387,11 @@ ${row.consequences}`);
6773
7387
  rows = applyLimit(rows, opts.limit);
6774
7388
  ok(rows, () => {
6775
7389
  if (!rows.length) {
6776
- line(import_picocolors25.default.dim("No requirements recorded yet."));
7390
+ line(import_picocolors28.default.dim("No requirements recorded yet."));
6777
7391
  return;
6778
7392
  }
6779
7393
  for (const r of rows) {
6780
- line(`${import_picocolors25.default.dim(r.id)} ${r.status.padEnd(9)} ${r.title}`);
7394
+ line(`${import_picocolors28.default.dim(r.id)} ${r.status.padEnd(9)} ${r.title}`);
6781
7395
  }
6782
7396
  });
6783
7397
  })
@@ -6790,8 +7404,8 @@ ${row.consequences}`);
6790
7404
  `/v1/projects/${projectId}/requirements/${args[0]}`
6791
7405
  );
6792
7406
  ok(row, () => {
6793
- line(`${import_picocolors25.default.bold(row.title)} ${import_picocolors25.default.dim(row.id)}`);
6794
- line(import_picocolors25.default.dim(`${row.status} \xB7 ${shortDate(row.createdAt)}`));
7407
+ line(`${import_picocolors28.default.bold(row.title)} ${import_picocolors28.default.dim(row.id)}`);
7408
+ line(import_picocolors28.default.dim(`${row.status} \xB7 ${shortDate(row.createdAt)}`));
6795
7409
  line(`
6796
7410
  ${row.body}`);
6797
7411
  });
@@ -6817,7 +7431,7 @@ ${row.body}`);
6817
7431
  refId: row?.id,
6818
7432
  output: { requirement: row }
6819
7433
  });
6820
- ok(row, () => line(`Recorded requirement ${import_picocolors25.default.bold(row?.id ?? "")} \u2014 ${title}`));
7434
+ ok(row, () => line(`Recorded requirement ${import_picocolors28.default.bold(row?.id ?? "")} \u2014 ${title}`));
6821
7435
  })
6822
7436
  );
6823
7437
  requirement.command("update <id>").description(
@@ -6846,7 +7460,7 @@ ${row.body}`);
6846
7460
  code: "bad_request"
6847
7461
  });
6848
7462
  }
6849
- ok(row, () => line(`Updated requirement ${import_picocolors25.default.bold(row.id)} \u2014 ${row.title} (${row.status})`));
7463
+ ok(row, () => line(`Updated requirement ${import_picocolors28.default.bold(row.id)} \u2014 ${row.title} (${row.status})`));
6850
7464
  })
6851
7465
  );
6852
7466
  }
@@ -6869,7 +7483,7 @@ function shortDate(iso) {
6869
7483
  }
6870
7484
 
6871
7485
  // src/commands/doc.ts
6872
- var import_picocolors26 = __toESM(require_picocolors(), 1);
7486
+ var import_picocolors29 = __toESM(require_picocolors(), 1);
6873
7487
  function registerDoc(program3) {
6874
7488
  const doc = program3.command("doc").description("Read and write project documents");
6875
7489
  doc.command("list").description("List the project's documents \u2014 check here before writing a new one").option("--work-item <id>", "the document linked to this card, if there is one").action(
@@ -6880,13 +7494,13 @@ function registerDoc(program3) {
6880
7494
  }) ?? [];
6881
7495
  ok(rows, () => {
6882
7496
  if (!rows.length) {
6883
- line(import_picocolors26.default.dim("No documents yet."));
7497
+ line(import_picocolors29.default.dim("No documents yet."));
6884
7498
  return;
6885
7499
  }
6886
7500
  for (const r of rows) {
6887
- const link = r.workItemId ? import_picocolors26.default.dim(` \u21B3 ${r.workItemId}`) : "";
6888
- const file = r.filePath ? import_picocolors26.default.dim(` ${r.filePath}`) : "";
6889
- line(`${import_picocolors26.default.dim(r.id)} ${r.title}${link}${file}`);
7501
+ const link = r.workItemId ? import_picocolors29.default.dim(` \u21B3 ${r.workItemId}`) : "";
7502
+ const file = r.filePath ? import_picocolors29.default.dim(` ${r.filePath}`) : "";
7503
+ line(`${import_picocolors29.default.dim(r.id)} ${r.title}${link}${file}`);
6890
7504
  }
6891
7505
  });
6892
7506
  })
@@ -6900,17 +7514,17 @@ function registerDoc(program3) {
6900
7514
  );
6901
7515
  if (opts.markdown) {
6902
7516
  ok({ id: row.id, title: row.title, filePath: row.filePath }, () => {
6903
- line(`${import_picocolors26.default.bold(row.title)} ${import_picocolors26.default.dim(row.id)}`);
7517
+ line(`${import_picocolors29.default.bold(row.title)} ${import_picocolors29.default.dim(row.id)}`);
6904
7518
  line(
6905
- row.filePath ? `Read it at ${import_picocolors26.default.bold(row.filePath)} (relative to the project folder).` : import_picocolors26.default.dim("This document has no markdown mirror on disk yet.")
7519
+ row.filePath ? `Read it at ${import_picocolors29.default.bold(row.filePath)} (relative to the project folder).` : import_picocolors29.default.dim("This document has no markdown mirror on disk yet.")
6906
7520
  );
6907
7521
  });
6908
7522
  return;
6909
7523
  }
6910
7524
  ok(row, () => {
6911
- line(`${import_picocolors26.default.bold(row.title)} ${import_picocolors26.default.dim(row.id)}`);
6912
- if (row.workItemId) line(import_picocolors26.default.dim(`linked to work item ${row.workItemId}`));
6913
- if (row.filePath) line(import_picocolors26.default.dim(`markdown mirror: ${row.filePath}`));
7525
+ line(`${import_picocolors29.default.bold(row.title)} ${import_picocolors29.default.dim(row.id)}`);
7526
+ if (row.workItemId) line(import_picocolors29.default.dim(`linked to work item ${row.workItemId}`));
7527
+ if (row.filePath) line(import_picocolors29.default.dim(`markdown mirror: ${row.filePath}`));
6914
7528
  line("");
6915
7529
  line(row.contentJson);
6916
7530
  });
@@ -6939,7 +7553,7 @@ function registerDoc(program3) {
6939
7553
  refId: row?.id,
6940
7554
  output: { document: row }
6941
7555
  });
6942
- ok(row, () => line(`Created document ${import_picocolors26.default.bold(row?.id ?? "")} \u2014 ${title}`));
7556
+ ok(row, () => line(`Created document ${import_picocolors29.default.bold(row?.id ?? "")} \u2014 ${title}`));
6943
7557
  })
6944
7558
  );
6945
7559
  doc.command("update <id>").description("Revise an existing document rather than creating a second copy of it").option("--title <text>", "new title").option("--markdown <text>", "replace the body with this markdown").option("--content-json <json>", "replace the body with this rich-text content JSON").action(
@@ -6966,13 +7580,13 @@ function registerDoc(program3) {
6966
7580
  code: "bad_request"
6967
7581
  });
6968
7582
  }
6969
- ok(row, () => line(`Updated document ${import_picocolors26.default.bold(row.id)} \u2014 ${row.title}`));
7583
+ ok(row, () => line(`Updated document ${import_picocolors29.default.bold(row.id)} \u2014 ${row.title}`));
6970
7584
  })
6971
7585
  );
6972
7586
  }
6973
7587
 
6974
7588
  // src/commands/design.ts
6975
- var import_picocolors27 = __toESM(require_picocolors(), 1);
7589
+ var import_picocolors30 = __toESM(require_picocolors(), 1);
6976
7590
  function registerDesign(program3) {
6977
7591
  const design = program3.command("design").description("Read the project's brand (colours, fonts, logo)");
6978
7592
  design.command("show").description("Show this project's brand \u2014 read it before writing any UI").option("--raw", "print the generated token files verbatim instead of a summary").action(
@@ -6986,11 +7600,11 @@ function registerDesign(program3) {
6986
7600
  if (opts.raw) {
6987
7601
  ok(files, () => {
6988
7602
  if (!files.length) {
6989
- line(import_picocolors27.default.dim("No brand set for this project."));
7603
+ line(import_picocolors30.default.dim("No brand set for this project."));
6990
7604
  return;
6991
7605
  }
6992
7606
  for (const f of files) {
6993
- line(import_picocolors27.default.bold(f.path));
7607
+ line(import_picocolors30.default.bold(f.path));
6994
7608
  line(f.contents);
6995
7609
  line("");
6996
7610
  }
@@ -7007,21 +7621,21 @@ function registerDesign(program3) {
7007
7621
  } : { hasBrand: false, colors: {}, fonts: {}, brand: {}, files: [] };
7008
7622
  ok(summary, () => {
7009
7623
  if (!tokens) {
7010
- line(import_picocolors27.default.dim("No brand set for this project \u2014 choose sensible styling yourself."));
7624
+ line(import_picocolors30.default.dim("No brand set for this project \u2014 choose sensible styling yourself."));
7011
7625
  return;
7012
7626
  }
7013
7627
  for (const [name, value] of Object.entries(tokens.brand)) {
7014
- line(`${import_picocolors27.default.dim(name.padEnd(12))} ${value}`);
7628
+ line(`${import_picocolors30.default.dim(name.padEnd(12))} ${value}`);
7015
7629
  }
7016
7630
  for (const [name, value] of Object.entries(tokens.color)) {
7017
- line(`${import_picocolors27.default.dim(`color.${name}`.padEnd(12))} ${value}`);
7631
+ line(`${import_picocolors30.default.dim(`color.${name}`.padEnd(12))} ${value}`);
7018
7632
  }
7019
7633
  for (const [name, value] of Object.entries(tokens.font)) {
7020
- line(`${import_picocolors27.default.dim(`font.${name}`.padEnd(12))} ${value}`);
7634
+ line(`${import_picocolors30.default.dim(`font.${name}`.padEnd(12))} ${value}`);
7021
7635
  }
7022
7636
  line("");
7023
7637
  line(
7024
- import_picocolors27.default.dim(
7638
+ import_picocolors30.default.dim(
7025
7639
  `Generated into the working tree as ${files.map((f) => f.path).join(", ")} \u2014 wire those in, never edit them.`
7026
7640
  )
7027
7641
  );
@@ -7053,7 +7667,7 @@ function unwrap(group) {
7053
7667
 
7054
7668
  // src/index.ts
7055
7669
  var pkg = {
7056
- version: true ? "0.2.6" : "0.0.0-dev"
7670
+ version: true ? "0.3.0" : "0.0.0-dev"
7057
7671
  };
7058
7672
  var program2 = new Command();
7059
7673
  program2.name("workser").description(
@@ -7086,6 +7700,8 @@ registerOpen(program2);
7086
7700
  registerDoctor(program2);
7087
7701
  registerAgent(program2);
7088
7702
  registerVerify(program2);
7703
+ registerCheckpoint(program2);
7704
+ registerSync(program2);
7089
7705
  registerWorkflow(program2);
7090
7706
  registerApp(program2);
7091
7707
  registerTool(program2);
@@ -7096,6 +7712,7 @@ registerImage(program2);
7096
7712
  registerAsk(program2);
7097
7713
  registerSearch(program2);
7098
7714
  registerBoard(program2);
7715
+ registerTask(program2);
7099
7716
  registerDecision(program2);
7100
7717
  registerDoc(program2);
7101
7718
  registerDesign(program2);