@workser/cli 0.2.3 → 0.2.5
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/AGENTS.md +32 -0
- package/dist/index.js +831 -4
- package/package.json +1 -1
- package/skills/workser/SKILL.md +34 -23
- package/skills/workser/reference/brand.md +60 -0
- package/skills/workser/reference/sdlc-entities.md +150 -0
package/AGENTS.md
CHANGED
|
@@ -42,6 +42,7 @@ what you *can* do.
|
|
|
42
42
|
workser status | whoami # orient (read)
|
|
43
43
|
workser project show | list # your pinned project + the workspace
|
|
44
44
|
workser env set KEY=VALUE… | list | get KEY # env vars
|
|
45
|
+
workser env pull [--out .env.local] # write cloud env vars into a local file
|
|
45
46
|
workser db create # provision the Neon Postgres database
|
|
46
47
|
workser db url | list # DB connection string + status
|
|
47
48
|
workser db tables # list tables in the database
|
|
@@ -92,6 +93,33 @@ workser ask "<question>" [--type <t>] [--option <o>] # ask the user, WAIT for th
|
|
|
92
93
|
# project create, project use, env rm, domain set
|
|
93
94
|
```
|
|
94
95
|
|
|
96
|
+
## Project tracking (the Board, Docs and Memory the user sees in Orbit)
|
|
97
|
+
```
|
|
98
|
+
workser board list | show <id> # what's already tracked
|
|
99
|
+
workser board create "<title>" [--description <t>] [--status <s>] [--priority <p>]
|
|
100
|
+
workser board move <id> <backlog|in-progress|in-review|done> | close <id>
|
|
101
|
+
workser decision list | show <id> # what was already decided
|
|
102
|
+
workser decision create "<title>" --context <t> --decision <t> [--consequences <t>]
|
|
103
|
+
workser requirement list | show <id> | create "<title>" --body <t> | update <id> --status <s>
|
|
104
|
+
workser doc list | show <id> [--markdown]
|
|
105
|
+
workser doc create "<title>" --markdown <text> | doc update <id> --markdown <text>
|
|
106
|
+
```
|
|
107
|
+
**Read before you plan** — `board list` and `decision list` tell you what someone is
|
|
108
|
+
already doing and what this project chose on purpose, so you don't re-file work or
|
|
109
|
+
quietly reverse a decision.
|
|
110
|
+
|
|
111
|
+
**A plan with phases goes on the Board before you build it.** The moment you split a
|
|
112
|
+
task into more than one phase: one `board create` card per phase (only the one you're
|
|
113
|
+
doing goes to `in-progress`), the plan itself as one `doc create --markdown` — with
|
|
114
|
+
**no `--work-item`**, because a linked doc is shown on its card and hidden from the
|
|
115
|
+
Docs panel — and a `decision create` if the plan settled a real tradeoff. A plan that
|
|
116
|
+
lives only in your reply is gone as soon as the conversation scrolls.
|
|
117
|
+
|
|
118
|
+
**Keep it true as you work.** `board move <id> in-progress` when you pick it up,
|
|
119
|
+
`in-review` when it's ready to look at, `board close <id>` when it's done and
|
|
120
|
+
verified. A Board still reading `backlog` after you shipped is worse than no Board.
|
|
121
|
+
Decisions are append-only: supersede an old one with a new record, never edit it.
|
|
122
|
+
|
|
95
123
|
## Memory (remember across conversations, not just this one)
|
|
96
124
|
Every conversation you run is otherwise a fresh start — no memory of what you or the
|
|
97
125
|
user decided last time. `workser memory add "<content>"` fixes that: it stores durable,
|
|
@@ -205,8 +233,12 @@ keep your own context lean and get a specialized second perspective; a non-zero
|
|
|
205
233
|
## Example
|
|
206
234
|
```bash
|
|
207
235
|
workser status --json # orient: which project, last deploy
|
|
236
|
+
workser board list --json # what's tracked; decision list for what's decided
|
|
237
|
+
workser board create "Phase 1 — …" --status in-progress --json # phased plan -> cards
|
|
238
|
+
workser doc create "Plan" --markdown "…" --json # …plus the plan doc
|
|
208
239
|
workser env set STRIPE_KEY=sk_live_… --json
|
|
209
240
|
# … you write the app code with your normal tools …
|
|
210
241
|
workser deploy --prod --watch --json # -> .data.url is the live URL
|
|
242
|
+
workser board close <id> --json # the Board now matches reality
|
|
211
243
|
```
|
|
212
244
|
Report results to the user in plain language, not raw JSON.
|
package/dist/index.js
CHANGED
|
@@ -3709,6 +3709,67 @@ workflow produces a result the app needs, its final node has to POST back to a
|
|
|
3709
3709
|
webhook route in the app. Build only the trigger and the workflow runs perfectly
|
|
3710
3710
|
while nothing ever appears in the product. The app-side receiver is covered in the
|
|
3711
3711
|
\`workser-sdk\` skill under workflows.
|
|
3712
|
+
`
|
|
3713
|
+
},
|
|
3714
|
+
{
|
|
3715
|
+
topic: "brand",
|
|
3716
|
+
title: "The project's brand",
|
|
3717
|
+
summary: "Read the owner's colours, fonts and logo before writing UI, and wire the generated design tokens into the app's theme.",
|
|
3718
|
+
commands: ["design"],
|
|
3719
|
+
source: "skills/workser/reference/brand.md",
|
|
3720
|
+
body: `# The project's brand
|
|
3721
|
+
|
|
3722
|
+
\`\`\`
|
|
3723
|
+
workser design show [--raw]
|
|
3724
|
+
\`\`\`
|
|
3725
|
+
|
|
3726
|
+
The owner sets their brand once in Workser (logo, colours, fonts). Everything
|
|
3727
|
+
you build should follow it. Read it **before writing any UI**:
|
|
3728
|
+
|
|
3729
|
+
\`\`\`bash
|
|
3730
|
+
workser design show --json
|
|
3731
|
+
\`\`\`
|
|
3732
|
+
|
|
3733
|
+
\`\`\`json
|
|
3734
|
+
{"ok":true,"data":{"hasBrand":true,
|
|
3735
|
+
"colors":{"primary":"#1f7a4d","accent":"#f5a623"},
|
|
3736
|
+
"fonts":{"heading":"Inter","body":"Inter"},
|
|
3737
|
+
"brand":{"name":"Green Grocer"},
|
|
3738
|
+
"files":["design/tokens.json","design/tokens.css"]}}
|
|
3739
|
+
\`\`\`
|
|
3740
|
+
|
|
3741
|
+
This asks the server, which derives the answer from the brand record itself \u2014 so
|
|
3742
|
+
it is correct even if nothing has been written into the working tree yet. That's
|
|
3743
|
+
the reason to use it instead of looking for a file and guessing when it's absent.
|
|
3744
|
+
|
|
3745
|
+
\`"hasBrand": false\` is an ordinary state \u2014 most projects have no brand set.
|
|
3746
|
+
Choose sensible styling yourself; **don't** stop and ask the user to define a
|
|
3747
|
+
brand first.
|
|
3748
|
+
|
|
3749
|
+
## Using it in the code
|
|
3750
|
+
|
|
3751
|
+
The same values are generated into the working tree as two files:
|
|
3752
|
+
|
|
3753
|
+
- \`design/tokens.json\` \u2014 the tokens in DTCG form
|
|
3754
|
+
- \`design/tokens.css\` \u2014 plain CSS custom properties (\`--ws-color-*\`, \`--ws-font-*\`)
|
|
3755
|
+
|
|
3756
|
+
Wire \`design/tokens.css\` into the app's theme the way this app's setup expects \u2014
|
|
3757
|
+
it assumes no particular Tailwind version, so map it rather than pasting it.
|
|
3758
|
+
|
|
3759
|
+
**Both files are generated and overwritten wholesale.** Never hand-edit them;
|
|
3760
|
+
your edit disappears on the next sync. \`workser design show --raw\` prints them
|
|
3761
|
+
verbatim if you need to see exactly what's in them.
|
|
3762
|
+
|
|
3763
|
+
## Changing the brand is the owner's job
|
|
3764
|
+
|
|
3765
|
+
There is no \`workser design set\` \u2014 the brand is one record the owner controls in
|
|
3766
|
+
the Workser app, and it drives everything generated from it. If the user asks you
|
|
3767
|
+
to change their brand colour, tell them where it lives (Design \u2192 Brand in Orbit)
|
|
3768
|
+
rather than writing the colour into the app by hand, which would drift from every
|
|
3769
|
+
other surface the brand feeds.
|
|
3770
|
+
|
|
3771
|
+
For generating artwork *in* the brand's palette, see \`workser help images\` \u2014
|
|
3772
|
+
put the colours from \`design show\` into the prompt.
|
|
3712
3773
|
`
|
|
3713
3774
|
},
|
|
3714
3775
|
{
|
|
@@ -4124,6 +4185,157 @@ real scope, not just a repeat of the task.
|
|
|
4124
4185
|
conversation you're in.
|
|
4125
4186
|
- Never give a subagent \u2014 configured or spawned \u2014 a task that tells it to delegate or
|
|
4126
4187
|
spawn further. It has no supervision loop to stop a runaway chain.
|
|
4188
|
+
`
|
|
4189
|
+
},
|
|
4190
|
+
{
|
|
4191
|
+
topic: "sdlc-entities",
|
|
4192
|
+
title: "Board cards, decisions, requirements, and docs",
|
|
4193
|
+
summary: "Read what this project already tracks and decided, keep the Board honest as you work, and record what a future maintainer will need.",
|
|
4194
|
+
commands: ["board", "decision", "requirement", "doc"],
|
|
4195
|
+
source: "skills/workser/reference/sdlc-entities.md",
|
|
4196
|
+
body: `# Board cards, decisions, requirements, and docs
|
|
4197
|
+
|
|
4198
|
+
These are the project's memory across sessions. They write to the **same tables**
|
|
4199
|
+
the Orbit desktop's Board, Project Memory, and Docs panels use, so anything here
|
|
4200
|
+
appears there too \u2014 and (when this CLI runs inside an Orbit-spawned agent run)
|
|
4201
|
+
as an inline card in the conversation you're working in.
|
|
4202
|
+
|
|
4203
|
+
\`\`\`
|
|
4204
|
+
workser board list [--status <value>] [--label <value>] [--limit <n>]
|
|
4205
|
+
workser board show <id>
|
|
4206
|
+
workser board create <title> [--description <text>] [--status <value>]
|
|
4207
|
+
[--priority <value>] [--label <value>]
|
|
4208
|
+
[--owner <name>] [--milestone <id>]
|
|
4209
|
+
workser board update <id> [--title|--description|--status|--priority
|
|
4210
|
+
|--label|--owner|--milestone ...]
|
|
4211
|
+
workser board move <id> <backlog|in-progress|in-review|done>
|
|
4212
|
+
workser board close <id>
|
|
4213
|
+
|
|
4214
|
+
workser decision list [--limit <n>]
|
|
4215
|
+
workser decision show <id>
|
|
4216
|
+
workser decision create <title> --context <text> --decision <text>
|
|
4217
|
+
[--consequences <text>]
|
|
4218
|
+
|
|
4219
|
+
workser requirement list [--status <value>] [--limit <n>]
|
|
4220
|
+
workser requirement show <id>
|
|
4221
|
+
workser requirement create <title> --body <text> [--status <text>]
|
|
4222
|
+
workser requirement update <id> [--title <text>] [--body <text>] [--status <text>]
|
|
4223
|
+
|
|
4224
|
+
workser doc list [--work-item <id>]
|
|
4225
|
+
workser doc show <id> [--markdown]
|
|
4226
|
+
workser doc create <title> [--work-item <id>] [--markdown <text>]
|
|
4227
|
+
[--content-json <json>]
|
|
4228
|
+
workser doc update <id> [--title <text>] [--markdown <text>]
|
|
4229
|
+
\`\`\`
|
|
4230
|
+
|
|
4231
|
+
## Read first \u2014 this is the part that matters
|
|
4232
|
+
|
|
4233
|
+
Before starting anything beyond a trivial edit:
|
|
4234
|
+
|
|
4235
|
+
\`\`\`
|
|
4236
|
+
workser board list --json # what's already tracked (don't re-file it)
|
|
4237
|
+
workser decision list --json # what was already decided (don't reverse it)
|
|
4238
|
+
\`\`\`
|
|
4239
|
+
|
|
4240
|
+
The project outlives your session. A decision recorded three weeks ago is the
|
|
4241
|
+
only thing standing between you and quietly undoing a choice someone made on
|
|
4242
|
+
purpose \u2014 \`workser decision show <id>\` gives you the context and consequences,
|
|
4243
|
+
not just the title. Reach for \`workser doc list\` / \`workser requirement list\`
|
|
4244
|
+
the same way when the task touches documented behaviour.
|
|
4245
|
+
|
|
4246
|
+
## Work with phases \u2192 cards + a plan doc, before you build
|
|
4247
|
+
|
|
4248
|
+
The moment you split a task into more than one phase, file it \u2014 not afterwards,
|
|
4249
|
+
and not only in your reply, which is gone once the conversation scrolls.
|
|
4250
|
+
|
|
4251
|
+
\`\`\`bash
|
|
4252
|
+
# one card per phase; only the one you're doing goes to in-progress
|
|
4253
|
+
workser board create "Phase 1 \u2014 schema + migration" \\
|
|
4254
|
+
--description "Add orders/line_items tables and the migration." \\
|
|
4255
|
+
--status in-progress --json
|
|
4256
|
+
workser board create "Phase 2 \u2014 checkout API" --description "\u2026" --json
|
|
4257
|
+
workser board create "Phase 3 \u2014 cart UI" --description "\u2026" --json
|
|
4258
|
+
|
|
4259
|
+
# the plan itself, ONE doc, deliberately NOT linked to a card
|
|
4260
|
+
workser doc create "Checkout \u2014 implementation plan" --markdown "$(cat plan.md)" --json
|
|
4261
|
+
|
|
4262
|
+
# the approach, if the plan settled something with real alternatives
|
|
4263
|
+
workser decision create "Carts live server-side" --context "\u2026" --decision "\u2026" --json
|
|
4264
|
+
\`\`\`
|
|
4265
|
+
|
|
4266
|
+
**Don't pass \`--work-item\` for a multi-phase plan.** A linked document renders on
|
|
4267
|
+
its card and is *hidden* from the Docs panel; a plan spanning three phases belongs
|
|
4268
|
+
to the project, not to phase 1.
|
|
4269
|
+
|
|
4270
|
+
The bar: if the user closed this conversation now, the Board should still show
|
|
4271
|
+
what's left and the doc should still explain the plan to whoever continues it.
|
|
4272
|
+
|
|
4273
|
+
## Keep the Board honest while you work
|
|
4274
|
+
|
|
4275
|
+
A Board still reading \`backlog\` after the feature shipped tells the user the
|
|
4276
|
+
opposite of the truth. Moving the card is part of finishing the work:
|
|
4277
|
+
|
|
4278
|
+
\`\`\`
|
|
4279
|
+
workser board move <id> in-progress # you picked it up
|
|
4280
|
+
workser board move <id> in-review # ready for the user to look at
|
|
4281
|
+
workser board close <id> # done and verified
|
|
4282
|
+
\`\`\`
|
|
4283
|
+
|
|
4284
|
+
\`--status\` is one of \`backlog | in-progress | in-review | done\` (default
|
|
4285
|
+
\`backlog\`). \`--priority\` is one of \`low | normal | high | urgent\` (default
|
|
4286
|
+
\`normal\`). \`--label\` repeats for more than one label:
|
|
4287
|
+
|
|
4288
|
+
\`\`\`
|
|
4289
|
+
workser board create "Fix the login bug" --status in-progress --priority high \\
|
|
4290
|
+
--label bug --label auth
|
|
4291
|
+
\`\`\`
|
|
4292
|
+
|
|
4293
|
+
\`board update\` replaces the labels you pass rather than merging them, and
|
|
4294
|
+
touches only the fields you name. There is no \`board delete\` \u2014 \`done\` is the
|
|
4295
|
+
terminal state for finished work, and removing a card the user filed is theirs
|
|
4296
|
+
to do in Orbit.
|
|
4297
|
+
|
|
4298
|
+
## Decisions are append-only
|
|
4299
|
+
|
|
4300
|
+
\`decision create\` is for something with real tradeoffs worth a paper trail:
|
|
4301
|
+
\`--context\` is why it came up, \`--decision\` is what was decided,
|
|
4302
|
+
\`--consequences\` is the follow-on effects. There is deliberately **no
|
|
4303
|
+
\`decision update\`** \u2014 a decision record states what was decided at a point in
|
|
4304
|
+
time. When it stops being right, record a new decision that supersedes it and
|
|
4305
|
+
say so in its \`--context\`. Editing the history is how a decision log stops
|
|
4306
|
+
being worth reading.
|
|
4307
|
+
|
|
4308
|
+
Requirements are different: they legitimately move along, so they do have
|
|
4309
|
+
\`update\`.
|
|
4310
|
+
|
|
4311
|
+
\`\`\`
|
|
4312
|
+
workser requirement create "Support SSO" --body "Enterprise customers need SAML." \\
|
|
4313
|
+
--status proposed
|
|
4314
|
+
workser requirement update <id> --status done
|
|
4315
|
+
\`\`\`
|
|
4316
|
+
|
|
4317
|
+
## Docs
|
|
4318
|
+
|
|
4319
|
+
\`--markdown\` is the normal way to write one. The body is stored both as the
|
|
4320
|
+
rich-text content the Docs panel renders and as a git-tracked markdown mirror
|
|
4321
|
+
at \`.workser/docs/<id>.md\` \u2014 \`workser doc show <id> --markdown\` reports that
|
|
4322
|
+
path so you can read the file with your normal tools.
|
|
4323
|
+
|
|
4324
|
+
Revise the page that exists rather than creating a second copy of it:
|
|
4325
|
+
|
|
4326
|
+
\`\`\`
|
|
4327
|
+
workser doc list --json # is there already a page for this?
|
|
4328
|
+
workser doc update <id> --markdown "$(cat updated.md)"
|
|
4329
|
+
\`\`\`
|
|
4330
|
+
|
|
4331
|
+
\`--work-item <id>\` links a document to a Board card (a card has at most one).
|
|
4332
|
+
|
|
4333
|
+
## When to record, and when not to
|
|
4334
|
+
|
|
4335
|
+
Record what a future maintainer would need: follow-up work you found but didn't
|
|
4336
|
+
do, a choice between real alternatives, a behaviour worth writing down. Don't
|
|
4337
|
+
narrate every small step \u2014 and never treat filing a card as a substitute for the
|
|
4338
|
+
work. A card saying "fix the bug" is not fixing the bug.
|
|
4127
4339
|
`
|
|
4128
4340
|
},
|
|
4129
4341
|
{
|
|
@@ -4352,7 +4564,8 @@ function buildContext(opts) {
|
|
|
4352
4564
|
const link = readProjectLink(cwd);
|
|
4353
4565
|
const projectId = opts.project || process.env.WORKSER_PROJECT_ID || link?.projectId || session.defaultProjectId;
|
|
4354
4566
|
const runId = process.env.WORKSER_RUN_ID || void 0;
|
|
4355
|
-
|
|
4567
|
+
const conversationId = process.env.WORKSER_CONVERSATION_ID || void 0;
|
|
4568
|
+
return { endpoint, socketPath, token, mode: mode2, cwd, projectId, runId, conversationId };
|
|
4356
4569
|
}
|
|
4357
4570
|
function runTarget(ctx) {
|
|
4358
4571
|
return ctx.runId || "current";
|
|
@@ -5187,6 +5400,7 @@ async function presign(ctx, projectId, bucket, key, operation, expiresInSeconds)
|
|
|
5187
5400
|
|
|
5188
5401
|
// src/commands/env.ts
|
|
5189
5402
|
var import_picocolors9 = __toESM(require_picocolors(), 1);
|
|
5403
|
+
import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
5190
5404
|
function appQuery(opts) {
|
|
5191
5405
|
const app = typeof opts?.app === "string" ? opts.app : "";
|
|
5192
5406
|
return app ? `?webAppId=${encodeURIComponent(app)}` : "";
|
|
@@ -5243,6 +5457,59 @@ function registerEnv(program3) {
|
|
|
5243
5457
|
})
|
|
5244
5458
|
)
|
|
5245
5459
|
);
|
|
5460
|
+
env.command("pull").description("Write this app's cloud env vars into a local file (default .env.local)").option("--app <webAppId>", APP_FLAG_HELP).option("--out <file>", "Local file to write", ".env.local").action(
|
|
5461
|
+
action(async ({ ctx, opts }) => {
|
|
5462
|
+
const projectId = requireProject(ctx);
|
|
5463
|
+
const items = await api(
|
|
5464
|
+
ctx,
|
|
5465
|
+
`/v1/projects/${projectId}/env${appQuery(opts)}`
|
|
5466
|
+
);
|
|
5467
|
+
const outPath = typeof opts?.out === "string" ? opts.out : ".env.local";
|
|
5468
|
+
if (!items?.length) {
|
|
5469
|
+
return ok(
|
|
5470
|
+
{ file: outPath, pulled: [] },
|
|
5471
|
+
() => line(import_picocolors9.default.dim("No cloud variables to pull."))
|
|
5472
|
+
);
|
|
5473
|
+
}
|
|
5474
|
+
const pulled = [];
|
|
5475
|
+
for (const item of items) {
|
|
5476
|
+
const res = await api(
|
|
5477
|
+
ctx,
|
|
5478
|
+
`/v1/projects/${projectId}/env/${encodeURIComponent(item.key)}${appQuery(opts)}`
|
|
5479
|
+
);
|
|
5480
|
+
pulled.push({ key: item.key, value: res?.value ?? "" });
|
|
5481
|
+
}
|
|
5482
|
+
await mergeEnvFile(outPath, pulled);
|
|
5483
|
+
ok(
|
|
5484
|
+
{ file: outPath, pulled: pulled.map((p) => p.key) },
|
|
5485
|
+
() => success(
|
|
5486
|
+
`Pulled ${pulled.length} variable(s) into ${import_picocolors9.default.bold(outPath)}.`
|
|
5487
|
+
)
|
|
5488
|
+
);
|
|
5489
|
+
})
|
|
5490
|
+
);
|
|
5491
|
+
}
|
|
5492
|
+
var ENV_KEY_LINE = /^([A-Za-z_][A-Za-z0-9_]*)=/;
|
|
5493
|
+
async function mergeEnvFile(path, vars) {
|
|
5494
|
+
const existing = await readFile3(path, "utf8").catch(() => "");
|
|
5495
|
+
const lines = existing.length ? existing.split(/\r?\n/) : [];
|
|
5496
|
+
const remaining = new Map(vars.map((v) => [v.key, v.value]));
|
|
5497
|
+
const merged = lines.map((rawLine) => {
|
|
5498
|
+
const match = ENV_KEY_LINE.exec(rawLine);
|
|
5499
|
+
if (!match || !remaining.has(match[1])) return rawLine;
|
|
5500
|
+
const key = match[1];
|
|
5501
|
+
const value = remaining.get(key);
|
|
5502
|
+
remaining.delete(key);
|
|
5503
|
+
return `${key}=${formatEnvValue(value)}`;
|
|
5504
|
+
});
|
|
5505
|
+
while (merged.length && merged[merged.length - 1] === "") merged.pop();
|
|
5506
|
+
for (const [key, value] of remaining) {
|
|
5507
|
+
merged.push(`${key}=${formatEnvValue(value)}`);
|
|
5508
|
+
}
|
|
5509
|
+
await writeFile3(path, merged.join("\n") + "\n", "utf8");
|
|
5510
|
+
}
|
|
5511
|
+
function formatEnvValue(value) {
|
|
5512
|
+
return /[\s"'#]/.test(value) ? JSON.stringify(value) : value;
|
|
5246
5513
|
}
|
|
5247
5514
|
|
|
5248
5515
|
// src/commands/deploy.ts
|
|
@@ -6035,7 +6302,7 @@ function printRun(run) {
|
|
|
6035
6302
|
}
|
|
6036
6303
|
|
|
6037
6304
|
// src/commands/image.ts
|
|
6038
|
-
import { writeFile as
|
|
6305
|
+
import { writeFile as writeFile4, mkdir } from "fs/promises";
|
|
6039
6306
|
import { dirname as dirname2, resolve as resolve3 } from "path";
|
|
6040
6307
|
function registerImage(program3) {
|
|
6041
6308
|
const image = program3.command("image").description("Generate images from a text prompt");
|
|
@@ -6100,7 +6367,7 @@ async function download(url, output) {
|
|
|
6100
6367
|
);
|
|
6101
6368
|
}
|
|
6102
6369
|
await mkdir(dirname2(target), { recursive: true });
|
|
6103
|
-
await
|
|
6370
|
+
await writeFile4(target, Buffer.from(await res.arrayBuffer()));
|
|
6104
6371
|
return target;
|
|
6105
6372
|
}
|
|
6106
6373
|
|
|
@@ -6228,9 +6495,565 @@ function registerSearch(program3) {
|
|
|
6228
6495
|
);
|
|
6229
6496
|
}
|
|
6230
6497
|
|
|
6498
|
+
// src/commands/board.ts
|
|
6499
|
+
var import_picocolors24 = __toESM(require_picocolors(), 1);
|
|
6500
|
+
|
|
6501
|
+
// src/commands/record-step.ts
|
|
6502
|
+
async function recordEntityStep(ctx, opts) {
|
|
6503
|
+
if (!ctx.runId) return;
|
|
6504
|
+
await api(ctx, `/v1/runs/${ctx.runId}/steps`, {
|
|
6505
|
+
body: {
|
|
6506
|
+
title: opts.title,
|
|
6507
|
+
refType: opts.refType,
|
|
6508
|
+
refId: opts.refId,
|
|
6509
|
+
output: opts.output
|
|
6510
|
+
}
|
|
6511
|
+
}).catch(() => void 0);
|
|
6512
|
+
}
|
|
6513
|
+
|
|
6514
|
+
// src/commands/board.ts
|
|
6515
|
+
var STATUSES = ["backlog", "in-progress", "in-review", "done"];
|
|
6516
|
+
var PRIORITIES = ["low", "normal", "high", "urgent"];
|
|
6517
|
+
function registerBoard(program3) {
|
|
6518
|
+
const board = program3.command("board").description("Read and update the project's Board (work items)");
|
|
6519
|
+
board.command("list").description("List the Board's cards \u2014 run this before starting work").option("--status <value>", `only cards in this status (${STATUSES.join(" | ")})`).option("--label <value>", "only cards carrying this label").option("--limit <n>", "cap the number of cards returned").action(
|
|
6520
|
+
action(async ({ ctx, opts }) => {
|
|
6521
|
+
const projectId = requireProject(ctx);
|
|
6522
|
+
if (opts.status !== void 0) assertStatus(opts.status);
|
|
6523
|
+
const all = await api(ctx, `/v1/projects/${projectId}/work-items`) ?? [];
|
|
6524
|
+
let rows = all;
|
|
6525
|
+
if (opts.status) rows = rows.filter((r) => r.status === opts.status);
|
|
6526
|
+
if (opts.label) rows = rows.filter((r) => (r.labels ?? []).includes(opts.label));
|
|
6527
|
+
if (opts.limit) {
|
|
6528
|
+
const n = Number(opts.limit);
|
|
6529
|
+
if (!Number.isFinite(n) || n < 1) {
|
|
6530
|
+
throw new WorkserError(`--limit must be a positive number, got "${opts.limit}".`, {
|
|
6531
|
+
code: "bad_request"
|
|
6532
|
+
});
|
|
6533
|
+
}
|
|
6534
|
+
rows = rows.slice(0, n);
|
|
6535
|
+
}
|
|
6536
|
+
ok(rows, () => {
|
|
6537
|
+
if (!rows.length) {
|
|
6538
|
+
line(import_picocolors24.default.dim("No cards on the Board yet."));
|
|
6539
|
+
return;
|
|
6540
|
+
}
|
|
6541
|
+
for (const r of rows) line(formatRow(r));
|
|
6542
|
+
});
|
|
6543
|
+
})
|
|
6544
|
+
);
|
|
6545
|
+
board.command("show <id>").description("Show one card in full, e.g. `workser board show <id>`").action(
|
|
6546
|
+
action(async ({ ctx, args }) => {
|
|
6547
|
+
const projectId = requireProject(ctx);
|
|
6548
|
+
const row = await api(
|
|
6549
|
+
ctx,
|
|
6550
|
+
`/v1/projects/${projectId}/work-items/${args[0]}`
|
|
6551
|
+
);
|
|
6552
|
+
ok(row, () => {
|
|
6553
|
+
line(`${import_picocolors24.default.bold(row.title)} ${import_picocolors24.default.dim(row.id)}`);
|
|
6554
|
+
line(`${statusTag(row.status)} priority ${row.priority}`);
|
|
6555
|
+
if (row.ownerHuman) line(`owner: ${row.ownerHuman}`);
|
|
6556
|
+
if (row.labels?.length) line(`labels: ${row.labels.join(", ")}`);
|
|
6557
|
+
if (row.description) line(`
|
|
6558
|
+
${row.description}`);
|
|
6559
|
+
});
|
|
6560
|
+
})
|
|
6561
|
+
);
|
|
6562
|
+
board.command("create <title>").description('Create a work item, e.g. `workser board create "Fix the login bug"`').option("--description <text>", "longer description").option("--status <value>", `${STATUSES.join(" | ")} (default: backlog)`).option("--priority <value>", `${PRIORITIES.join(" | ")} (default: normal)`).option(
|
|
6563
|
+
"--label <value>",
|
|
6564
|
+
"a label to attach (repeat for more)",
|
|
6565
|
+
collect2,
|
|
6566
|
+
[]
|
|
6567
|
+
).option("--owner <name>", "the human who owns this card").option("--milestone <id>", "milestone id to attach this card to").action(
|
|
6568
|
+
action(async ({ ctx, args, opts }) => {
|
|
6569
|
+
const projectId = requireProject(ctx);
|
|
6570
|
+
const title = String(args[0] ?? "").trim();
|
|
6571
|
+
if (!title) {
|
|
6572
|
+
throw new WorkserError("A work item needs a title.", { code: "bad_request" });
|
|
6573
|
+
}
|
|
6574
|
+
if (opts.status !== void 0) assertStatus(opts.status);
|
|
6575
|
+
if (opts.priority !== void 0) assertPriority(opts.priority);
|
|
6576
|
+
const labels = opts.label;
|
|
6577
|
+
const row = await api(ctx, `/v1/projects/${projectId}/work-items`, {
|
|
6578
|
+
body: {
|
|
6579
|
+
title,
|
|
6580
|
+
description: opts.description,
|
|
6581
|
+
status: opts.status,
|
|
6582
|
+
priority: opts.priority,
|
|
6583
|
+
labels: labels.length ? labels : void 0,
|
|
6584
|
+
ownerHuman: opts.owner,
|
|
6585
|
+
milestoneId: opts.milestone
|
|
6586
|
+
}
|
|
6587
|
+
});
|
|
6588
|
+
await recordEntityStep(ctx, {
|
|
6589
|
+
title: `Created work item: ${title}`,
|
|
6590
|
+
refType: "agent_created_work_item",
|
|
6591
|
+
refId: row?.id,
|
|
6592
|
+
output: { workItem: row }
|
|
6593
|
+
});
|
|
6594
|
+
ok(row, () => line(`Created work item ${import_picocolors24.default.bold(row?.id ?? "")} \u2014 ${title}`));
|
|
6595
|
+
})
|
|
6596
|
+
);
|
|
6597
|
+
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(
|
|
6598
|
+
"--label <value>",
|
|
6599
|
+
"replace the card's labels with these (repeat for more)",
|
|
6600
|
+
collect2,
|
|
6601
|
+
[]
|
|
6602
|
+
).option("--owner <name>", "the human who owns this card").option("--milestone <id>", "milestone id to attach this card to").action(
|
|
6603
|
+
action(async ({ ctx, args, opts }) => {
|
|
6604
|
+
const projectId = requireProject(ctx);
|
|
6605
|
+
if (opts.status !== void 0) assertStatus(opts.status);
|
|
6606
|
+
if (opts.priority !== void 0) assertPriority(opts.priority);
|
|
6607
|
+
const labels = opts.label;
|
|
6608
|
+
const body = {
|
|
6609
|
+
title: opts.title,
|
|
6610
|
+
description: opts.description,
|
|
6611
|
+
status: opts.status,
|
|
6612
|
+
priority: opts.priority,
|
|
6613
|
+
// Commander's default `[]` is indistinguishable from "the user asked
|
|
6614
|
+
// for zero labels", so an empty array means "not specified" here.
|
|
6615
|
+
// Clearing labels is a rare enough intent to leave to the UI rather
|
|
6616
|
+
// than overload a flag that cannot express it unambiguously.
|
|
6617
|
+
labels: labels.length ? labels : void 0,
|
|
6618
|
+
ownerHuman: opts.owner,
|
|
6619
|
+
milestoneId: opts.milestone
|
|
6620
|
+
};
|
|
6621
|
+
if (Object.values(body).every((v) => v === void 0)) {
|
|
6622
|
+
throw new WorkserError(
|
|
6623
|
+
"Nothing to update \u2014 pass at least one of --title, --description, --status, --priority, --label, --owner, --milestone.",
|
|
6624
|
+
{ code: "bad_request" }
|
|
6625
|
+
);
|
|
6626
|
+
}
|
|
6627
|
+
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)}`));
|
|
6629
|
+
})
|
|
6630
|
+
);
|
|
6631
|
+
board.command("move <id> <status>").description(
|
|
6632
|
+
`Move a card to ${STATUSES.join(" | ")} \u2014 the one-liner for keeping the Board honest`
|
|
6633
|
+
).action(
|
|
6634
|
+
action(async ({ ctx, args }) => {
|
|
6635
|
+
const projectId = requireProject(ctx);
|
|
6636
|
+
const status = String(args[1]);
|
|
6637
|
+
assertStatus(status);
|
|
6638
|
+
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)}`));
|
|
6640
|
+
})
|
|
6641
|
+
);
|
|
6642
|
+
board.command("close <id>").description("Shorthand for `board move <id> done`").action(
|
|
6643
|
+
action(async ({ ctx, args }) => {
|
|
6644
|
+
const projectId = requireProject(ctx);
|
|
6645
|
+
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)}`));
|
|
6647
|
+
})
|
|
6648
|
+
);
|
|
6649
|
+
}
|
|
6650
|
+
async function patchItem(ctx, projectId, id, body) {
|
|
6651
|
+
const row = await api(ctx, `/v1/projects/${projectId}/work-items/${id}`, {
|
|
6652
|
+
method: "PATCH",
|
|
6653
|
+
body
|
|
6654
|
+
});
|
|
6655
|
+
if (!row) {
|
|
6656
|
+
throw new WorkserError(`No work item with id "${id}" on this project.`, {
|
|
6657
|
+
code: "bad_request"
|
|
6658
|
+
});
|
|
6659
|
+
}
|
|
6660
|
+
return row;
|
|
6661
|
+
}
|
|
6662
|
+
function assertStatus(value) {
|
|
6663
|
+
if (!STATUSES.includes(value)) {
|
|
6664
|
+
throw new WorkserError(
|
|
6665
|
+
`Unknown status "${value}". Use one of: ${STATUSES.join(", ")}.`,
|
|
6666
|
+
{ code: "bad_request" }
|
|
6667
|
+
);
|
|
6668
|
+
}
|
|
6669
|
+
}
|
|
6670
|
+
function assertPriority(value) {
|
|
6671
|
+
if (!PRIORITIES.includes(value)) {
|
|
6672
|
+
throw new WorkserError(
|
|
6673
|
+
`Unknown priority "${value}". Use one of: ${PRIORITIES.join(", ")}.`,
|
|
6674
|
+
{ code: "bad_request" }
|
|
6675
|
+
);
|
|
6676
|
+
}
|
|
6677
|
+
}
|
|
6678
|
+
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}`;
|
|
6682
|
+
}
|
|
6683
|
+
function statusTag(status) {
|
|
6684
|
+
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);
|
|
6689
|
+
}
|
|
6690
|
+
function collect2(value, previous) {
|
|
6691
|
+
return [...previous, value];
|
|
6692
|
+
}
|
|
6693
|
+
|
|
6694
|
+
// src/commands/decision.ts
|
|
6695
|
+
var import_picocolors25 = __toESM(require_picocolors(), 1);
|
|
6696
|
+
function registerDecision(program3) {
|
|
6697
|
+
const decision = program3.command("decision").description("Read and record the project's architecture decisions");
|
|
6698
|
+
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(
|
|
6699
|
+
action(async ({ ctx, opts }) => {
|
|
6700
|
+
const projectId = requireProject(ctx);
|
|
6701
|
+
let rows = await api(
|
|
6702
|
+
ctx,
|
|
6703
|
+
`/v1/projects/${projectId}/architecture-decisions`
|
|
6704
|
+
) ?? [];
|
|
6705
|
+
rows = applyLimit(rows, opts.limit);
|
|
6706
|
+
ok(rows, () => {
|
|
6707
|
+
if (!rows.length) {
|
|
6708
|
+
line(import_picocolors25.default.dim("No decisions recorded yet."));
|
|
6709
|
+
return;
|
|
6710
|
+
}
|
|
6711
|
+
for (const r of rows) {
|
|
6712
|
+
line(`${import_picocolors25.default.dim(r.id)} ${import_picocolors25.default.dim(shortDate(r.createdAt))} ${r.title}`);
|
|
6713
|
+
line(` ${truncate(r.decision, 100)}`);
|
|
6714
|
+
}
|
|
6715
|
+
});
|
|
6716
|
+
})
|
|
6717
|
+
);
|
|
6718
|
+
decision.command("show <id>").description("The full record \u2014 context, decision, and consequences").action(
|
|
6719
|
+
action(async ({ ctx, args }) => {
|
|
6720
|
+
const projectId = requireProject(ctx);
|
|
6721
|
+
const row = await api(
|
|
6722
|
+
ctx,
|
|
6723
|
+
`/v1/projects/${projectId}/architecture-decisions/${args[0]}`
|
|
6724
|
+
);
|
|
6725
|
+
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)}`));
|
|
6728
|
+
line(`
|
|
6729
|
+
${import_picocolors25.default.bold("Context")}
|
|
6730
|
+
${row.context}`);
|
|
6731
|
+
line(`
|
|
6732
|
+
${import_picocolors25.default.bold("Decision")}
|
|
6733
|
+
${row.decision}`);
|
|
6734
|
+
if (row.consequences) line(`
|
|
6735
|
+
${import_picocolors25.default.bold("Consequences")}
|
|
6736
|
+
${row.consequences}`);
|
|
6737
|
+
});
|
|
6738
|
+
})
|
|
6739
|
+
);
|
|
6740
|
+
decision.command("create <title>").description(
|
|
6741
|
+
'workser decision create "Use Postgres" --context ... --decision ... [--consequences ...]'
|
|
6742
|
+
).requiredOption("--context <text>", "why this decision was needed").requiredOption("--decision <text>", "what was decided").option("--consequences <text>", "tradeoffs / follow-on effects").action(
|
|
6743
|
+
action(async ({ ctx, args, opts }) => {
|
|
6744
|
+
const projectId = requireProject(ctx);
|
|
6745
|
+
const title = String(args[0] ?? "").trim();
|
|
6746
|
+
if (!title) {
|
|
6747
|
+
throw new WorkserError("A decision needs a title.", { code: "bad_request" });
|
|
6748
|
+
}
|
|
6749
|
+
const row = await api(ctx, `/v1/projects/${projectId}/architecture-decisions`, {
|
|
6750
|
+
body: {
|
|
6751
|
+
title,
|
|
6752
|
+
context: opts.context,
|
|
6753
|
+
decision: opts.decision,
|
|
6754
|
+
consequences: opts.consequences,
|
|
6755
|
+
conversationId: ctx.conversationId
|
|
6756
|
+
}
|
|
6757
|
+
});
|
|
6758
|
+
await recordEntityStep(ctx, {
|
|
6759
|
+
title: `Recorded decision: ${title}`,
|
|
6760
|
+
refType: "agent_created_decision",
|
|
6761
|
+
refId: row?.id,
|
|
6762
|
+
output: { decision: row }
|
|
6763
|
+
});
|
|
6764
|
+
ok(row, () => line(`Recorded decision ${import_picocolors25.default.bold(row?.id ?? "")} \u2014 ${title}`));
|
|
6765
|
+
})
|
|
6766
|
+
);
|
|
6767
|
+
const requirement = program3.command("requirement").description("Read and record the project's requirements");
|
|
6768
|
+
requirement.command("list").description("Every requirement on record \u2014 read this before deciding what to build").option("--status <value>", "only requirements in this status, e.g. accepted").option("--limit <n>", "cap the number returned (newest first)").action(
|
|
6769
|
+
action(async ({ ctx, opts }) => {
|
|
6770
|
+
const projectId = requireProject(ctx);
|
|
6771
|
+
let rows = await api(ctx, `/v1/projects/${projectId}/requirements`) ?? [];
|
|
6772
|
+
if (opts.status) rows = rows.filter((r) => r.status === opts.status);
|
|
6773
|
+
rows = applyLimit(rows, opts.limit);
|
|
6774
|
+
ok(rows, () => {
|
|
6775
|
+
if (!rows.length) {
|
|
6776
|
+
line(import_picocolors25.default.dim("No requirements recorded yet."));
|
|
6777
|
+
return;
|
|
6778
|
+
}
|
|
6779
|
+
for (const r of rows) {
|
|
6780
|
+
line(`${import_picocolors25.default.dim(r.id)} ${r.status.padEnd(9)} ${r.title}`);
|
|
6781
|
+
}
|
|
6782
|
+
});
|
|
6783
|
+
})
|
|
6784
|
+
);
|
|
6785
|
+
requirement.command("show <id>").description("The full requirement text").action(
|
|
6786
|
+
action(async ({ ctx, args }) => {
|
|
6787
|
+
const projectId = requireProject(ctx);
|
|
6788
|
+
const row = await api(
|
|
6789
|
+
ctx,
|
|
6790
|
+
`/v1/projects/${projectId}/requirements/${args[0]}`
|
|
6791
|
+
);
|
|
6792
|
+
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)}`));
|
|
6795
|
+
line(`
|
|
6796
|
+
${row.body}`);
|
|
6797
|
+
});
|
|
6798
|
+
})
|
|
6799
|
+
);
|
|
6800
|
+
requirement.command("create <title>").description('workser requirement create "Support SSO" --body ... [--status ...]').requiredOption("--body <text>", "the requirement's text").option("--status <text>", "e.g. proposed | accepted | done").action(
|
|
6801
|
+
action(async ({ ctx, args, opts }) => {
|
|
6802
|
+
const projectId = requireProject(ctx);
|
|
6803
|
+
const title = String(args[0] ?? "").trim();
|
|
6804
|
+
if (!title) {
|
|
6805
|
+
throw new WorkserError("A requirement needs a title.", { code: "bad_request" });
|
|
6806
|
+
}
|
|
6807
|
+
const row = await api(ctx, `/v1/projects/${projectId}/requirements`, {
|
|
6808
|
+
body: {
|
|
6809
|
+
title,
|
|
6810
|
+
body: opts.body,
|
|
6811
|
+
status: opts.status
|
|
6812
|
+
}
|
|
6813
|
+
});
|
|
6814
|
+
await recordEntityStep(ctx, {
|
|
6815
|
+
title: `Recorded requirement: ${title}`,
|
|
6816
|
+
refType: "agent_created_requirement",
|
|
6817
|
+
refId: row?.id,
|
|
6818
|
+
output: { requirement: row }
|
|
6819
|
+
});
|
|
6820
|
+
ok(row, () => line(`Recorded requirement ${import_picocolors25.default.bold(row?.id ?? "")} \u2014 ${title}`));
|
|
6821
|
+
})
|
|
6822
|
+
);
|
|
6823
|
+
requirement.command("update <id>").description(
|
|
6824
|
+
"Move a requirement along, e.g. `workser requirement update <id> --status done`"
|
|
6825
|
+
).option("--title <text>", "new title").option("--body <text>", "new requirement text").option("--status <text>", "e.g. proposed | accepted | done").action(
|
|
6826
|
+
action(async ({ ctx, args, opts }) => {
|
|
6827
|
+
const projectId = requireProject(ctx);
|
|
6828
|
+
const body = {
|
|
6829
|
+
title: opts.title,
|
|
6830
|
+
body: opts.body,
|
|
6831
|
+
status: opts.status
|
|
6832
|
+
};
|
|
6833
|
+
if (Object.values(body).every((v) => v === void 0)) {
|
|
6834
|
+
throw new WorkserError(
|
|
6835
|
+
"Nothing to update \u2014 pass at least one of --title, --body, --status.",
|
|
6836
|
+
{ code: "bad_request" }
|
|
6837
|
+
);
|
|
6838
|
+
}
|
|
6839
|
+
const row = await api(
|
|
6840
|
+
ctx,
|
|
6841
|
+
`/v1/projects/${projectId}/requirements/${args[0]}`,
|
|
6842
|
+
{ method: "PATCH", body }
|
|
6843
|
+
);
|
|
6844
|
+
if (!row) {
|
|
6845
|
+
throw new WorkserError(`No requirement with id "${args[0]}" on this project.`, {
|
|
6846
|
+
code: "bad_request"
|
|
6847
|
+
});
|
|
6848
|
+
}
|
|
6849
|
+
ok(row, () => line(`Updated requirement ${import_picocolors25.default.bold(row.id)} \u2014 ${row.title} (${row.status})`));
|
|
6850
|
+
})
|
|
6851
|
+
);
|
|
6852
|
+
}
|
|
6853
|
+
function applyLimit(rows, limit) {
|
|
6854
|
+
if (limit === void 0) return rows;
|
|
6855
|
+
const n = Number(limit);
|
|
6856
|
+
if (!Number.isFinite(n) || n < 1) {
|
|
6857
|
+
throw new WorkserError(`--limit must be a positive number, got "${limit}".`, {
|
|
6858
|
+
code: "bad_request"
|
|
6859
|
+
});
|
|
6860
|
+
}
|
|
6861
|
+
return rows.slice(0, n);
|
|
6862
|
+
}
|
|
6863
|
+
function truncate(text, max) {
|
|
6864
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
6865
|
+
return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
|
|
6866
|
+
}
|
|
6867
|
+
function shortDate(iso) {
|
|
6868
|
+
return iso ? iso.slice(0, 10) : "";
|
|
6869
|
+
}
|
|
6870
|
+
|
|
6871
|
+
// src/commands/doc.ts
|
|
6872
|
+
var import_picocolors26 = __toESM(require_picocolors(), 1);
|
|
6873
|
+
function registerDoc(program3) {
|
|
6874
|
+
const doc = program3.command("doc").description("Read and write project documents");
|
|
6875
|
+
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(
|
|
6876
|
+
action(async ({ ctx, opts }) => {
|
|
6877
|
+
const projectId = requireProject(ctx);
|
|
6878
|
+
const rows = await api(ctx, `/v1/projects/${projectId}/documents`, {
|
|
6879
|
+
query: { workItemId: opts.workItem }
|
|
6880
|
+
}) ?? [];
|
|
6881
|
+
ok(rows, () => {
|
|
6882
|
+
if (!rows.length) {
|
|
6883
|
+
line(import_picocolors26.default.dim("No documents yet."));
|
|
6884
|
+
return;
|
|
6885
|
+
}
|
|
6886
|
+
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}`);
|
|
6890
|
+
}
|
|
6891
|
+
});
|
|
6892
|
+
})
|
|
6893
|
+
);
|
|
6894
|
+
doc.command("show <id>").description("Show a document \u2014 `--markdown` prints the file mirror's path to read").option("--markdown", "report the repo-relative markdown mirror instead of the block content").action(
|
|
6895
|
+
action(async ({ ctx, args, opts }) => {
|
|
6896
|
+
const projectId = requireProject(ctx);
|
|
6897
|
+
const row = await api(
|
|
6898
|
+
ctx,
|
|
6899
|
+
`/v1/projects/${projectId}/documents/${args[0]}`
|
|
6900
|
+
);
|
|
6901
|
+
if (opts.markdown) {
|
|
6902
|
+
ok({ id: row.id, title: row.title, filePath: row.filePath }, () => {
|
|
6903
|
+
line(`${import_picocolors26.default.bold(row.title)} ${import_picocolors26.default.dim(row.id)}`);
|
|
6904
|
+
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.")
|
|
6906
|
+
);
|
|
6907
|
+
});
|
|
6908
|
+
return;
|
|
6909
|
+
}
|
|
6910
|
+
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}`));
|
|
6914
|
+
line("");
|
|
6915
|
+
line(row.contentJson);
|
|
6916
|
+
});
|
|
6917
|
+
})
|
|
6918
|
+
);
|
|
6919
|
+
doc.command("create <title>").description(
|
|
6920
|
+
'workser doc create "Onboarding" [--work-item <id>] [--markdown ...] [--content-json ...]'
|
|
6921
|
+
).option("--work-item <id>", "link this document to a work item").option("--markdown <text>", "document body as markdown").option("--content-json <json>", "document body as rich-text content JSON").action(
|
|
6922
|
+
action(async ({ ctx, args, opts }) => {
|
|
6923
|
+
const projectId = requireProject(ctx);
|
|
6924
|
+
const title = String(args[0] ?? "").trim();
|
|
6925
|
+
if (!title) {
|
|
6926
|
+
throw new WorkserError("A document needs a title.", { code: "bad_request" });
|
|
6927
|
+
}
|
|
6928
|
+
const row = await api(ctx, `/v1/projects/${projectId}/documents`, {
|
|
6929
|
+
body: {
|
|
6930
|
+
title,
|
|
6931
|
+
workItemId: opts.workItem,
|
|
6932
|
+
markdown: opts.markdown,
|
|
6933
|
+
contentJson: opts.contentJson
|
|
6934
|
+
}
|
|
6935
|
+
});
|
|
6936
|
+
await recordEntityStep(ctx, {
|
|
6937
|
+
title: `Created document: ${title}`,
|
|
6938
|
+
refType: "agent_created_document",
|
|
6939
|
+
refId: row?.id,
|
|
6940
|
+
output: { document: row }
|
|
6941
|
+
});
|
|
6942
|
+
ok(row, () => line(`Created document ${import_picocolors26.default.bold(row?.id ?? "")} \u2014 ${title}`));
|
|
6943
|
+
})
|
|
6944
|
+
);
|
|
6945
|
+
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(
|
|
6946
|
+
action(async ({ ctx, args, opts }) => {
|
|
6947
|
+
const projectId = requireProject(ctx);
|
|
6948
|
+
const body = {
|
|
6949
|
+
title: opts.title,
|
|
6950
|
+
markdown: opts.markdown,
|
|
6951
|
+
contentJson: opts.contentJson
|
|
6952
|
+
};
|
|
6953
|
+
if (Object.values(body).every((v) => v === void 0)) {
|
|
6954
|
+
throw new WorkserError(
|
|
6955
|
+
"Nothing to update \u2014 pass at least one of --title, --markdown, --content-json.",
|
|
6956
|
+
{ code: "bad_request" }
|
|
6957
|
+
);
|
|
6958
|
+
}
|
|
6959
|
+
const row = await api(
|
|
6960
|
+
ctx,
|
|
6961
|
+
`/v1/projects/${projectId}/documents/${args[0]}`,
|
|
6962
|
+
{ method: "PATCH", body }
|
|
6963
|
+
);
|
|
6964
|
+
if (!row) {
|
|
6965
|
+
throw new WorkserError(`No document with id "${args[0]}" on this project.`, {
|
|
6966
|
+
code: "bad_request"
|
|
6967
|
+
});
|
|
6968
|
+
}
|
|
6969
|
+
ok(row, () => line(`Updated document ${import_picocolors26.default.bold(row.id)} \u2014 ${row.title}`));
|
|
6970
|
+
})
|
|
6971
|
+
);
|
|
6972
|
+
}
|
|
6973
|
+
|
|
6974
|
+
// src/commands/design.ts
|
|
6975
|
+
var import_picocolors27 = __toESM(require_picocolors(), 1);
|
|
6976
|
+
function registerDesign(program3) {
|
|
6977
|
+
const design = program3.command("design").description("Read the project's brand (colours, fonts, logo)");
|
|
6978
|
+
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(
|
|
6979
|
+
action(async ({ ctx, opts }) => {
|
|
6980
|
+
const projectId = requireProject(ctx);
|
|
6981
|
+
const res = await api(
|
|
6982
|
+
ctx,
|
|
6983
|
+
`/v1/projects/${projectId}/design/files`
|
|
6984
|
+
);
|
|
6985
|
+
const files = res?.files ?? [];
|
|
6986
|
+
if (opts.raw) {
|
|
6987
|
+
ok(files, () => {
|
|
6988
|
+
if (!files.length) {
|
|
6989
|
+
line(import_picocolors27.default.dim("No brand set for this project."));
|
|
6990
|
+
return;
|
|
6991
|
+
}
|
|
6992
|
+
for (const f of files) {
|
|
6993
|
+
line(import_picocolors27.default.bold(f.path));
|
|
6994
|
+
line(f.contents);
|
|
6995
|
+
line("");
|
|
6996
|
+
}
|
|
6997
|
+
});
|
|
6998
|
+
return;
|
|
6999
|
+
}
|
|
7000
|
+
const tokens = parseTokens(files);
|
|
7001
|
+
const summary = tokens ? {
|
|
7002
|
+
hasBrand: true,
|
|
7003
|
+
colors: tokens.color,
|
|
7004
|
+
fonts: tokens.font,
|
|
7005
|
+
brand: tokens.brand,
|
|
7006
|
+
files: files.map((f) => f.path)
|
|
7007
|
+
} : { hasBrand: false, colors: {}, fonts: {}, brand: {}, files: [] };
|
|
7008
|
+
ok(summary, () => {
|
|
7009
|
+
if (!tokens) {
|
|
7010
|
+
line(import_picocolors27.default.dim("No brand set for this project \u2014 choose sensible styling yourself."));
|
|
7011
|
+
return;
|
|
7012
|
+
}
|
|
7013
|
+
for (const [name, value] of Object.entries(tokens.brand)) {
|
|
7014
|
+
line(`${import_picocolors27.default.dim(name.padEnd(12))} ${value}`);
|
|
7015
|
+
}
|
|
7016
|
+
for (const [name, value] of Object.entries(tokens.color)) {
|
|
7017
|
+
line(`${import_picocolors27.default.dim(`color.${name}`.padEnd(12))} ${value}`);
|
|
7018
|
+
}
|
|
7019
|
+
for (const [name, value] of Object.entries(tokens.font)) {
|
|
7020
|
+
line(`${import_picocolors27.default.dim(`font.${name}`.padEnd(12))} ${value}`);
|
|
7021
|
+
}
|
|
7022
|
+
line("");
|
|
7023
|
+
line(
|
|
7024
|
+
import_picocolors27.default.dim(
|
|
7025
|
+
`Generated into the working tree as ${files.map((f) => f.path).join(", ")} \u2014 wire those in, never edit them.`
|
|
7026
|
+
)
|
|
7027
|
+
);
|
|
7028
|
+
});
|
|
7029
|
+
})
|
|
7030
|
+
);
|
|
7031
|
+
}
|
|
7032
|
+
function parseTokens(files) {
|
|
7033
|
+
const doc = files.find((f) => f.path.endsWith("tokens.json"));
|
|
7034
|
+
if (!doc) return null;
|
|
7035
|
+
try {
|
|
7036
|
+
const parsed = JSON.parse(doc.contents);
|
|
7037
|
+
return {
|
|
7038
|
+
color: unwrap(parsed.color),
|
|
7039
|
+
font: unwrap(parsed.font),
|
|
7040
|
+
brand: Object.fromEntries(
|
|
7041
|
+
Object.entries(parsed.brand ?? {}).map(([k, v]) => [k, String(v)])
|
|
7042
|
+
)
|
|
7043
|
+
};
|
|
7044
|
+
} catch {
|
|
7045
|
+
return null;
|
|
7046
|
+
}
|
|
7047
|
+
}
|
|
7048
|
+
function unwrap(group) {
|
|
7049
|
+
return Object.fromEntries(
|
|
7050
|
+
Object.entries(group ?? {}).map(([k, v]) => [k, v?.$value ?? ""])
|
|
7051
|
+
);
|
|
7052
|
+
}
|
|
7053
|
+
|
|
6231
7054
|
// src/index.ts
|
|
6232
7055
|
var pkg = {
|
|
6233
|
-
version: true ? "0.2.
|
|
7056
|
+
version: true ? "0.2.5" : "0.0.0-dev"
|
|
6234
7057
|
};
|
|
6235
7058
|
var program2 = new Command();
|
|
6236
7059
|
program2.name("workser").description(
|
|
@@ -6272,4 +7095,8 @@ registerArtifact(program2);
|
|
|
6272
7095
|
registerImage(program2);
|
|
6273
7096
|
registerAsk(program2);
|
|
6274
7097
|
registerSearch(program2);
|
|
7098
|
+
registerBoard(program2);
|
|
7099
|
+
registerDecision(program2);
|
|
7100
|
+
registerDoc(program2);
|
|
7101
|
+
registerDesign(program2);
|
|
6275
7102
|
program2.parseAsync(process.argv).catch((e) => fail(e));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workser/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Workser CLI — give your local AI agent native DevOps & infrastructure on Workser. The agent runs `workser …` to provision, deploy, and manage real apps.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/skills/workser/SKILL.md
CHANGED
|
@@ -21,6 +21,8 @@ command. Don't print them all; you are paying for every line you load.
|
|
|
21
21
|
|
|
22
22
|
| You need to… | Commands | Run |
|
|
23
23
|
| --- | --- | --- |
|
|
24
|
+
| See what's tracked, decided, or written down for this project | `board …`, `decision …`, `requirement …`, `doc …` | `workser help sdlc-entities` |
|
|
25
|
+
| Follow the project's brand — colours, fonts, logo | `design …` | `workser help brand` |
|
|
24
26
|
| Provision or query Postgres; list end users | `db …`, `auth …` | `workser help database` |
|
|
25
27
|
| Deploy, set env vars, read logs, check a domain | `deploy`, `env …`, `logs`, `versions`, `domain`, `open` | `workser help deploy` |
|
|
26
28
|
| Put files in the project's bucket | `storage …` | `workser help storage` |
|
|
@@ -33,10 +35,9 @@ command. Don't print them all; you are paying for every line you load.
|
|
|
33
35
|
| Record finished output, or ask the user a question | `artifact …`, `ask` | `workser help deliverables` |
|
|
34
36
|
| Control this machine — files, shell, screen, browser | `tool …` | `workser help computer-use` |
|
|
35
37
|
|
|
36
|
-
`workser help` with no topic lists them. **The CLI is the source of truth
|
|
37
|
-
these guides
|
|
38
|
-
|
|
39
|
-
cannot be out of date.
|
|
38
|
+
`workser help` with no topic lists them. **The CLI is the source of truth** — it
|
|
39
|
+
ships these guides itself, so they match the version you are running, and
|
|
40
|
+
`workser <command> --help` is generated from the implementation.
|
|
40
41
|
|
|
41
42
|
## Orientation (no guide needed)
|
|
42
43
|
|
|
@@ -59,13 +60,11 @@ SQL, provision the bucket + auth, deploy, set env vars, manage files. Sensitive
|
|
|
59
60
|
actions are **gated** — the daemon may return `error.code = "awaiting_approval"`
|
|
60
61
|
(exit 5) and wait for the user to approve in Orbit; ask them to approve, then retry.
|
|
61
62
|
|
|
62
|
-
What you **cannot** do is administer the project set or destroy config:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
Owner-only, for reference: `project create` · `project use` · `env rm` · `domain set`.
|
|
63
|
+
What you **cannot** do is administer the project set or destroy config:
|
|
64
|
+
`project create` · `project use` · `env rm` · `domain set` return
|
|
65
|
+
`error.code = "owner_only"` (exit 6). Tell the user it's an owner action to do in
|
|
66
|
+
Orbit, then continue with what you can. The project is already selected; you don't
|
|
67
|
+
pick or switch it.
|
|
69
68
|
|
|
70
69
|
## Golden rules
|
|
71
70
|
|
|
@@ -73,19 +72,29 @@ Owner-only, for reference: `project create` · `project use` · `env rm` · `dom
|
|
|
73
72
|
`{"ok":true,"data":...}` or `{"ok":false,"error":{"code","message",...}}`. Parse it.
|
|
74
73
|
2. **Orient first.** Run `workser status --json` to see the connection, the pinned
|
|
75
74
|
project, and the latest deploy before acting. You don't pick or switch projects.
|
|
76
|
-
|
|
75
|
+
For anything beyond a trivial edit, also read what the project already knows:
|
|
76
|
+
`workser board list --json` (what's tracked), `workser decision list --json`
|
|
77
|
+
(what was already decided, so you don't quietly reverse it), and
|
|
78
|
+
`workser design show --json` before writing UI. This project outlives your
|
|
79
|
+
session; that context is how you don't start from zero.
|
|
80
|
+
3. **A phased plan goes on the Board before you build it.** One `board create`
|
|
81
|
+
card per phase, the plan itself as one `doc create` (no `--work-item`, or it's
|
|
82
|
+
hidden from the Docs panel), and a `decision create` if it settled a real
|
|
83
|
+
tradeoff. A plan that lives only in your reply is gone when the conversation
|
|
84
|
+
scrolls. Details: `workser help sdlc-entities`.
|
|
85
|
+
4. **Stay in your lane.** `error.code = "owner_only"` (exit 6) means the action is
|
|
77
86
|
reserved for the owner in Orbit. Don't retry or look for a workaround — tell the
|
|
78
87
|
user, then continue. Provisioning the *pinned project's own* db / bucket / auth is
|
|
79
88
|
allowed (it may be approval-gated, not owner-only).
|
|
80
|
-
|
|
89
|
+
5. **Approvals are normal.** Some allowed actions may return
|
|
81
90
|
`{"error":{"code":"awaiting_approval"}}` (exit 5) while the user approves in the
|
|
82
91
|
Orbit UI. Tell the user to approve, then retry — do **not** try to bypass it.
|
|
83
|
-
|
|
84
|
-
|
|
92
|
+
6. **Never ask for or store credentials.** Auth is handled by Orbit; you never see keys.
|
|
93
|
+
7. **Verify before "done".** Before telling the user a task is complete, run
|
|
85
94
|
`workser verify --json` (runs the project's typecheck/lint/build). If it
|
|
86
95
|
reports `"ok": false`, fix the errors it lists and re-run until it passes —
|
|
87
96
|
a green build is the bar for "done", not your own judgement.
|
|
88
|
-
|
|
97
|
+
8. **Destructive shell actions are blocked.** Irreversible commands (`rm -rf /`,
|
|
89
98
|
`git reset --hard`, `DROP`/`TRUNCATE`, `curl | sh`, …) are refused by Workser's
|
|
90
99
|
safety policy — don't attempt them; use migrations + scoped changes instead.
|
|
91
100
|
|
|
@@ -93,16 +102,18 @@ Owner-only, for reference: `project create` · `project use` · `env rm` · `dom
|
|
|
93
102
|
|
|
94
103
|
```bash
|
|
95
104
|
workser status --json # 1. orient (project is already pinned)
|
|
96
|
-
workser
|
|
97
|
-
workser
|
|
105
|
+
workser board list --json # 2. what's already tracked
|
|
106
|
+
workser decision list --json # …and already decided
|
|
107
|
+
workser board create "Phase 2 — …" --json # 3. phases? file them + the plan doc
|
|
108
|
+
workser board move <id> in-progress --json # 4. claim the card you're doing
|
|
109
|
+
workser db create --json # 5. provision infra the app needs (idempotent)
|
|
110
|
+
workser env set STRIPE_KEY=sk_live_… --json # 6. configure it
|
|
98
111
|
# … you write the app code with your normal tools …
|
|
99
|
-
workser
|
|
112
|
+
workser verify --json # 7. green build is the bar for "done"
|
|
113
|
+
workser deploy --prod --watch --json # 8. ship; returns the live URL
|
|
114
|
+
workser board close <id> --json # 9. the Board now matches reality
|
|
100
115
|
```
|
|
101
116
|
|
|
102
|
-
Provisioning the pinned project's own database / bucket / auth is yours to do
|
|
103
|
-
(`db create`, `storage create`, `auth enable`) — the user may need to approve it in
|
|
104
|
-
Orbit (`awaiting_approval`). Only a **custom domain** is an owner action.
|
|
105
|
-
|
|
106
117
|
## Reading results
|
|
107
118
|
|
|
108
119
|
- Success: use `.data` (e.g. `.data.url` after deploy, `.data` array after `list`).
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
topic: brand
|
|
3
|
+
title: The project's brand
|
|
4
|
+
summary: Read the owner's colours, fonts and logo before writing UI, and wire the generated design tokens into the app's theme.
|
|
5
|
+
commands: [design]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# The project's brand
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
workser design show [--raw]
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The owner sets their brand once in Workser (logo, colours, fonts). Everything
|
|
15
|
+
you build should follow it. Read it **before writing any UI**:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
workser design show --json
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{"ok":true,"data":{"hasBrand":true,
|
|
23
|
+
"colors":{"primary":"#1f7a4d","accent":"#f5a623"},
|
|
24
|
+
"fonts":{"heading":"Inter","body":"Inter"},
|
|
25
|
+
"brand":{"name":"Green Grocer"},
|
|
26
|
+
"files":["design/tokens.json","design/tokens.css"]}}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
This asks the server, which derives the answer from the brand record itself — so
|
|
30
|
+
it is correct even if nothing has been written into the working tree yet. That's
|
|
31
|
+
the reason to use it instead of looking for a file and guessing when it's absent.
|
|
32
|
+
|
|
33
|
+
`"hasBrand": false` is an ordinary state — most projects have no brand set.
|
|
34
|
+
Choose sensible styling yourself; **don't** stop and ask the user to define a
|
|
35
|
+
brand first.
|
|
36
|
+
|
|
37
|
+
## Using it in the code
|
|
38
|
+
|
|
39
|
+
The same values are generated into the working tree as two files:
|
|
40
|
+
|
|
41
|
+
- `design/tokens.json` — the tokens in DTCG form
|
|
42
|
+
- `design/tokens.css` — plain CSS custom properties (`--ws-color-*`, `--ws-font-*`)
|
|
43
|
+
|
|
44
|
+
Wire `design/tokens.css` into the app's theme the way this app's setup expects —
|
|
45
|
+
it assumes no particular Tailwind version, so map it rather than pasting it.
|
|
46
|
+
|
|
47
|
+
**Both files are generated and overwritten wholesale.** Never hand-edit them;
|
|
48
|
+
your edit disappears on the next sync. `workser design show --raw` prints them
|
|
49
|
+
verbatim if you need to see exactly what's in them.
|
|
50
|
+
|
|
51
|
+
## Changing the brand is the owner's job
|
|
52
|
+
|
|
53
|
+
There is no `workser design set` — the brand is one record the owner controls in
|
|
54
|
+
the Workser app, and it drives everything generated from it. If the user asks you
|
|
55
|
+
to change their brand colour, tell them where it lives (Design → Brand in Orbit)
|
|
56
|
+
rather than writing the colour into the app by hand, which would drift from every
|
|
57
|
+
other surface the brand feeds.
|
|
58
|
+
|
|
59
|
+
For generating artwork *in* the brand's palette, see `workser help images` —
|
|
60
|
+
put the colours from `design show` into the prompt.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
---
|
|
2
|
+
topic: sdlc-entities
|
|
3
|
+
title: Board cards, decisions, requirements, and docs
|
|
4
|
+
summary: Read what this project already tracks and decided, keep the Board honest as you work, and record what a future maintainer will need.
|
|
5
|
+
commands: [board, decision, requirement, doc]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Board cards, decisions, requirements, and docs
|
|
9
|
+
|
|
10
|
+
These are the project's memory across sessions. They write to the **same tables**
|
|
11
|
+
the Orbit desktop's Board, Project Memory, and Docs panels use, so anything here
|
|
12
|
+
appears there too — and (when this CLI runs inside an Orbit-spawned agent run)
|
|
13
|
+
as an inline card in the conversation you're working in.
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
workser board list [--status <value>] [--label <value>] [--limit <n>]
|
|
17
|
+
workser board show <id>
|
|
18
|
+
workser board create <title> [--description <text>] [--status <value>]
|
|
19
|
+
[--priority <value>] [--label <value>]
|
|
20
|
+
[--owner <name>] [--milestone <id>]
|
|
21
|
+
workser board update <id> [--title|--description|--status|--priority
|
|
22
|
+
|--label|--owner|--milestone ...]
|
|
23
|
+
workser board move <id> <backlog|in-progress|in-review|done>
|
|
24
|
+
workser board close <id>
|
|
25
|
+
|
|
26
|
+
workser decision list [--limit <n>]
|
|
27
|
+
workser decision show <id>
|
|
28
|
+
workser decision create <title> --context <text> --decision <text>
|
|
29
|
+
[--consequences <text>]
|
|
30
|
+
|
|
31
|
+
workser requirement list [--status <value>] [--limit <n>]
|
|
32
|
+
workser requirement show <id>
|
|
33
|
+
workser requirement create <title> --body <text> [--status <text>]
|
|
34
|
+
workser requirement update <id> [--title <text>] [--body <text>] [--status <text>]
|
|
35
|
+
|
|
36
|
+
workser doc list [--work-item <id>]
|
|
37
|
+
workser doc show <id> [--markdown]
|
|
38
|
+
workser doc create <title> [--work-item <id>] [--markdown <text>]
|
|
39
|
+
[--content-json <json>]
|
|
40
|
+
workser doc update <id> [--title <text>] [--markdown <text>]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Read first — this is the part that matters
|
|
44
|
+
|
|
45
|
+
Before starting anything beyond a trivial edit:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
workser board list --json # what's already tracked (don't re-file it)
|
|
49
|
+
workser decision list --json # what was already decided (don't reverse it)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The project outlives your session. A decision recorded three weeks ago is the
|
|
53
|
+
only thing standing between you and quietly undoing a choice someone made on
|
|
54
|
+
purpose — `workser decision show <id>` gives you the context and consequences,
|
|
55
|
+
not just the title. Reach for `workser doc list` / `workser requirement list`
|
|
56
|
+
the same way when the task touches documented behaviour.
|
|
57
|
+
|
|
58
|
+
## Work with phases → cards + a plan doc, before you build
|
|
59
|
+
|
|
60
|
+
The moment you split a task into more than one phase, file it — not afterwards,
|
|
61
|
+
and not only in your reply, which is gone once the conversation scrolls.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# one card per phase; only the one you're doing goes to in-progress
|
|
65
|
+
workser board create "Phase 1 — schema + migration" \
|
|
66
|
+
--description "Add orders/line_items tables and the migration." \
|
|
67
|
+
--status in-progress --json
|
|
68
|
+
workser board create "Phase 2 — checkout API" --description "…" --json
|
|
69
|
+
workser board create "Phase 3 — cart UI" --description "…" --json
|
|
70
|
+
|
|
71
|
+
# the plan itself, ONE doc, deliberately NOT linked to a card
|
|
72
|
+
workser doc create "Checkout — implementation plan" --markdown "$(cat plan.md)" --json
|
|
73
|
+
|
|
74
|
+
# the approach, if the plan settled something with real alternatives
|
|
75
|
+
workser decision create "Carts live server-side" --context "…" --decision "…" --json
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Don't pass `--work-item` for a multi-phase plan.** A linked document renders on
|
|
79
|
+
its card and is *hidden* from the Docs panel; a plan spanning three phases belongs
|
|
80
|
+
to the project, not to phase 1.
|
|
81
|
+
|
|
82
|
+
The bar: if the user closed this conversation now, the Board should still show
|
|
83
|
+
what's left and the doc should still explain the plan to whoever continues it.
|
|
84
|
+
|
|
85
|
+
## Keep the Board honest while you work
|
|
86
|
+
|
|
87
|
+
A Board still reading `backlog` after the feature shipped tells the user the
|
|
88
|
+
opposite of the truth. Moving the card is part of finishing the work:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
workser board move <id> in-progress # you picked it up
|
|
92
|
+
workser board move <id> in-review # ready for the user to look at
|
|
93
|
+
workser board close <id> # done and verified
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`--status` is one of `backlog | in-progress | in-review | done` (default
|
|
97
|
+
`backlog`). `--priority` is one of `low | normal | high | urgent` (default
|
|
98
|
+
`normal`). `--label` repeats for more than one label:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
workser board create "Fix the login bug" --status in-progress --priority high \
|
|
102
|
+
--label bug --label auth
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`board update` replaces the labels you pass rather than merging them, and
|
|
106
|
+
touches only the fields you name. There is no `board delete` — `done` is the
|
|
107
|
+
terminal state for finished work, and removing a card the user filed is theirs
|
|
108
|
+
to do in Orbit.
|
|
109
|
+
|
|
110
|
+
## Decisions are append-only
|
|
111
|
+
|
|
112
|
+
`decision create` is for something with real tradeoffs worth a paper trail:
|
|
113
|
+
`--context` is why it came up, `--decision` is what was decided,
|
|
114
|
+
`--consequences` is the follow-on effects. There is deliberately **no
|
|
115
|
+
`decision update`** — a decision record states what was decided at a point in
|
|
116
|
+
time. When it stops being right, record a new decision that supersedes it and
|
|
117
|
+
say so in its `--context`. Editing the history is how a decision log stops
|
|
118
|
+
being worth reading.
|
|
119
|
+
|
|
120
|
+
Requirements are different: they legitimately move along, so they do have
|
|
121
|
+
`update`.
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
workser requirement create "Support SSO" --body "Enterprise customers need SAML." \
|
|
125
|
+
--status proposed
|
|
126
|
+
workser requirement update <id> --status done
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Docs
|
|
130
|
+
|
|
131
|
+
`--markdown` is the normal way to write one. The body is stored both as the
|
|
132
|
+
rich-text content the Docs panel renders and as a git-tracked markdown mirror
|
|
133
|
+
at `.workser/docs/<id>.md` — `workser doc show <id> --markdown` reports that
|
|
134
|
+
path so you can read the file with your normal tools.
|
|
135
|
+
|
|
136
|
+
Revise the page that exists rather than creating a second copy of it:
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
workser doc list --json # is there already a page for this?
|
|
140
|
+
workser doc update <id> --markdown "$(cat updated.md)"
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`--work-item <id>` links a document to a Board card (a card has at most one).
|
|
144
|
+
|
|
145
|
+
## When to record, and when not to
|
|
146
|
+
|
|
147
|
+
Record what a future maintainer would need: follow-up work you found but didn't
|
|
148
|
+
do, a choice between real alternatives, a behaviour worth writing down. Don't
|
|
149
|
+
narrate every small step — and never treat filing a card as a substitute for the
|
|
150
|
+
work. A card saying "fix the bug" is not fixing the bug.
|