@workser/cli 0.3.1 → 0.6.1
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 +2768 -359
- package/package.json +4 -2
- package/skills/workser/SKILL.md +19 -20
- package/skills/workser/reference/analysis.md +54 -0
- package/skills/workser/reference/api.md +79 -0
- package/skills/workser/reference/brand.md +33 -0
- package/skills/workser/reference/checks.md +70 -0
- package/skills/workser/reference/deliverables.md +38 -0
- package/skills/workser/reference/deploy.md +37 -24
- package/skills/workser/reference/docs.md +55 -0
- package/skills/workser/reference/env.md +115 -0
- package/skills/workser/reference/neon-backend.md +42 -8
- package/skills/workser/reference/sdlc-entities.md +38 -87
- package/skills/workser/reference/tasks.md +34 -0
- package/skills/workser/reference/usage.md +48 -0
package/dist/index.js
CHANGED
|
@@ -3655,6 +3655,141 @@ var import_picocolors2 = __toESM(require_picocolors(), 1);
|
|
|
3655
3655
|
|
|
3656
3656
|
// src/help-content.ts
|
|
3657
3657
|
var HELP_TOPICS = [
|
|
3658
|
+
{
|
|
3659
|
+
topic: "analysis",
|
|
3660
|
+
title: "Analysis \u2014 running Python on this project's data",
|
|
3661
|
+
summary: "Run pandas locally under a sandbox and a clock, so the code, the output and the timing land in the task where the owner can see them.",
|
|
3662
|
+
commands: ["analysis"],
|
|
3663
|
+
source: "skills/workser/reference/analysis.md",
|
|
3664
|
+
body: `# Analysis \u2014 running Python on this project's data
|
|
3665
|
+
|
|
3666
|
+
\`\`\`
|
|
3667
|
+
workser analysis runtime [--app <webAppId>]
|
|
3668
|
+
workser analysis run --app <webAppId> --file report.py [--timeout <ms>]
|
|
3669
|
+
workser analysis run --app <webAppId> --code 'print(1)'
|
|
3670
|
+
\`\`\`
|
|
3671
|
+
|
|
3672
|
+
## Why not just run \`python\` yourself
|
|
3673
|
+
|
|
3674
|
+
Two reasons, and the second matters more.
|
|
3675
|
+
|
|
3676
|
+
It runs inside the same OS sandbox a structured agent run gets, scoped to the
|
|
3677
|
+
app's own folder \u2014 so a script that goes wrong goes wrong in one directory.
|
|
3678
|
+
|
|
3679
|
+
And it is **recorded**. The code, the output and how long it took land in the
|
|
3680
|
+
task, where the owner can see them. An analysis nobody can see is an assertion,
|
|
3681
|
+
which is the same problem \`workser api call\` solves for a service with no
|
|
3682
|
+
screen. If a number is going to end up in front of the customer, run it here.
|
|
3683
|
+
|
|
3684
|
+
## Check the runtime before you write the script
|
|
3685
|
+
|
|
3686
|
+
\`\`\`
|
|
3687
|
+
workser analysis runtime --app <id> --json
|
|
3688
|
+
\`\`\`
|
|
3689
|
+
|
|
3690
|
+
It reports the interpreter that would be used \u2014 the app's own \`.venv\` first, if
|
|
3691
|
+
it has one \u2014 and whether \`pandas\` and \`matplotlib\` are importable. Exit code is
|
|
3692
|
+
non-zero when Python is missing, so you find out in a second rather than after
|
|
3693
|
+
writing a hundred lines.
|
|
3694
|
+
|
|
3695
|
+
## Limits, said plainly
|
|
3696
|
+
|
|
3697
|
+
Five minutes by default, fifteen at most. Output is capped per stream and
|
|
3698
|
+
truncation is reported. Nothing is silently dropped.
|
|
3699
|
+
|
|
3700
|
+
These are local limits and they are the right ones: this work does **not** go in
|
|
3701
|
+
a deployed function. \`maxDuration\` is for a slow request; an analysis reads a lot
|
|
3702
|
+
of rows and takes as long as it takes, so putting it behind a serverless timeout
|
|
3703
|
+
means the useful analyses are exactly the ones that fail.
|
|
3704
|
+
|
|
3705
|
+
## What counts as evidence
|
|
3706
|
+
|
|
3707
|
+
A number on its own is not a finding. When you report a result, say what the
|
|
3708
|
+
query was, how many rows it covered, and what window of time \u2014 a figure with no
|
|
3709
|
+
denominator is the easiest thing in this product to get wrong and the hardest
|
|
3710
|
+
for the owner to check.
|
|
3711
|
+
`
|
|
3712
|
+
},
|
|
3713
|
+
{
|
|
3714
|
+
topic: "api",
|
|
3715
|
+
title: "Services \u2014 calling one, and describing it",
|
|
3716
|
+
summary: "Call this project's API through the same console the owner sees, and make sure every route it serves is written down before you call the task done.",
|
|
3717
|
+
commands: ["api"],
|
|
3718
|
+
source: "skills/workser/reference/api.md",
|
|
3719
|
+
body: `# Services \u2014 calling one, and describing it
|
|
3720
|
+
|
|
3721
|
+
A service has no screen. Everything else you build can be looked at; an API can
|
|
3722
|
+
only be *called* \u2014 so unless the calls go somewhere the owner can see, "the API
|
|
3723
|
+
works" is an assertion with nothing behind it.
|
|
3724
|
+
|
|
3725
|
+
\`\`\`
|
|
3726
|
+
workser api list [--app <webAppId>]
|
|
3727
|
+
workser api call <path> [--app <id>] [--method <verb>] [--body <text>]
|
|
3728
|
+
[--header 'Name: value'] [--env local|preview|production]
|
|
3729
|
+
workser api spec [--check]
|
|
3730
|
+
\`\`\`
|
|
3731
|
+
|
|
3732
|
+
## Call it through the console, not through curl
|
|
3733
|
+
|
|
3734
|
+
\`workser api call\` goes through the same request console the owner has open. The
|
|
3735
|
+
status, the timing and the body you see are the ones they see, and the
|
|
3736
|
+
credentials come from the app's own environment rather than from your command
|
|
3737
|
+
line \u2014 so a token never lands in a transcript.
|
|
3738
|
+
|
|
3739
|
+
\`\`\`
|
|
3740
|
+
workser api call /orders --app <id> --json
|
|
3741
|
+
workser api call /orders --app <id> --method POST --body '{"item":"latte"}' --json
|
|
3742
|
+
\`\`\`
|
|
3743
|
+
|
|
3744
|
+
\`--env\` picks which copy to call. \`local\` is the dev server on this machine and
|
|
3745
|
+
is the default; \`preview\` and \`production\` are the deployments. The host always
|
|
3746
|
+
comes from that choice \u2014 a path is a path, never a URL, and passing one is
|
|
3747
|
+
refused.
|
|
3748
|
+
|
|
3749
|
+
Exit code: non-zero only when nothing answered. A 404 or a 422 is a *successful*
|
|
3750
|
+
call with an informative answer, so checking that a route correctly rejects bad
|
|
3751
|
+
input works exactly as you would expect.
|
|
3752
|
+
|
|
3753
|
+
## Save the calls that matter
|
|
3754
|
+
|
|
3755
|
+
Requests saved at \`api/requests.json\` in the service's repo show up in the
|
|
3756
|
+
owner's console. Write the handful that describe what the service does \u2014 not a
|
|
3757
|
+
test suite:
|
|
3758
|
+
|
|
3759
|
+
\`\`\`json
|
|
3760
|
+
[
|
|
3761
|
+
{ "id": "list-orders", "name": "List today's orders", "method": "GET",
|
|
3762
|
+
"path": "/api/orders", "note": "What the shop screen loads." },
|
|
3763
|
+
{ "id": "place-order", "name": "Place an order", "method": "POST",
|
|
3764
|
+
"path": "/api/orders", "body": "{\\"item\\":\\"latte\\"}" }
|
|
3765
|
+
]
|
|
3766
|
+
\`\`\`
|
|
3767
|
+
|
|
3768
|
+
They are in the repo on purpose: a call worth saving outlives the session that
|
|
3769
|
+
saved it, and it shows up in the diff.
|
|
3770
|
+
|
|
3771
|
+
## Describe every route before you call it done
|
|
3772
|
+
|
|
3773
|
+
\`\`\`
|
|
3774
|
+
workser api spec --check
|
|
3775
|
+
\`\`\`
|
|
3776
|
+
|
|
3777
|
+
It compares the routes the repo actually serves \u2014 read from the file layout, so
|
|
3778
|
+
it cannot be fooled by a comment \u2014 with the paths your OpenAPI document
|
|
3779
|
+
declares, and fails when one is missing. Write the document at
|
|
3780
|
+
\`api/openapi.json\` (YAML also works).
|
|
3781
|
+
|
|
3782
|
+
It is not an OpenAPI validator and does not check schemas or responses. It asks
|
|
3783
|
+
one question, so that it is cheap enough to run every time: is every route
|
|
3784
|
+
written down. A health probe is exempt. A path in the spec that the repo does
|
|
3785
|
+
not serve is reported but does not fail \u2014 you may be documenting something
|
|
3786
|
+
built next.
|
|
3787
|
+
|
|
3788
|
+
Run it alongside \`workser verify\` before declaring an API task finished. An API
|
|
3789
|
+
somebody can call and an API somebody can integrate with are different products,
|
|
3790
|
+
and the spec is the difference.
|
|
3791
|
+
`
|
|
3792
|
+
},
|
|
3658
3793
|
{
|
|
3659
3794
|
topic: "automation",
|
|
3660
3795
|
title: "Workflows & connected apps",
|
|
@@ -3770,6 +3905,39 @@ other surface the brand feeds.
|
|
|
3770
3905
|
|
|
3771
3906
|
For generating artwork *in* the brand's palette, see \`workser help images\` \u2014
|
|
3772
3907
|
put the colours from \`design show\` into the prompt.
|
|
3908
|
+
|
|
3909
|
+
## The three places design lives \u2014 and which one you are in
|
|
3910
|
+
|
|
3911
|
+
They are separate on purpose, and confusing them is the commonest mistake here.
|
|
3912
|
+
|
|
3913
|
+
| What | Scope | Where |
|
|
3914
|
+
| --- | --- | --- |
|
|
3915
|
+
| **Brand** \u2014 colours, fonts, logo | The **project** | \`business_settings\`, read with \`design show\` |
|
|
3916
|
+
| **Design files** \u2014 the \`.fig\` work | The **project**, many files | the project's design workspace folder |
|
|
3917
|
+
| **Layout options** \u2014 choices to show the owner | **One app** | \`design/options.json\` in that app's folder |
|
|
3918
|
+
|
|
3919
|
+
**Design is not an app.** It has no port, no URL, nothing to deploy. Never create
|
|
3920
|
+
an app for it.
|
|
3921
|
+
|
|
3922
|
+
When you write \`design/options.json\`, put the design file each option came from
|
|
3923
|
+
in a \`source\` field:
|
|
3924
|
+
|
|
3925
|
+
\`\`\`json
|
|
3926
|
+
{ "options": [
|
|
3927
|
+
{ "id": "warm", "name": "Warm and simple", "route": "/",
|
|
3928
|
+
"note": "Bigger type, more space.", "source": "hero-v2.fig" }
|
|
3929
|
+
] }
|
|
3930
|
+
\`\`\`
|
|
3931
|
+
|
|
3932
|
+
\`source\` is a path **inside the project's design workspace** \u2014 relative, no
|
|
3933
|
+
\`..\`, no absolute paths, no URLs. Anything else is dropped. Leave it out when the
|
|
3934
|
+
option was written straight into code with no design file behind it; that is an
|
|
3935
|
+
ordinary case and inventing a source is worse than omitting one.
|
|
3936
|
+
|
|
3937
|
+
Why it matters: the owner picks an option in one app and later opens the design
|
|
3938
|
+
workspace. Without \`source\`, nothing connects the decision they just made to the
|
|
3939
|
+
file it came from, and "why does the site look like this?" has three unrelated
|
|
3940
|
+
answers.
|
|
3773
3941
|
`
|
|
3774
3942
|
},
|
|
3775
3943
|
{
|
|
@@ -3822,6 +3990,77 @@ copy is the current one.
|
|
|
3822
3990
|
\`workser business\` is how **you** inspect and fix data while building. The app reads
|
|
3823
3991
|
the same records at runtime through \`workser.business\` in \`@workser/app\` \u2014 see the
|
|
3824
3992
|
\`workser-sdk\` skill. An app shelling out to this CLI per request is wrong.
|
|
3993
|
+
`
|
|
3994
|
+
},
|
|
3995
|
+
{
|
|
3996
|
+
topic: "checks",
|
|
3997
|
+
title: "Checks \u2014 is it safe, and is it still up",
|
|
3998
|
+
summary: "Scan the code for leaked secrets, known-bad dependencies and over-broad permissions, and check that what you published is still answering.",
|
|
3999
|
+
commands: ["scan", "health"],
|
|
4000
|
+
source: "skills/workser/reference/checks.md",
|
|
4001
|
+
body: `# Checks \u2014 is it safe, and is it still up
|
|
4002
|
+
|
|
4003
|
+
Two questions nothing else in this CLI asks. \`verify\` tells you the code
|
|
4004
|
+
compiles. These tell you it is not dangerous, and that it is still working an
|
|
4005
|
+
hour after you shipped it.
|
|
4006
|
+
|
|
4007
|
+
Run both before you say a task is done.
|
|
4008
|
+
|
|
4009
|
+
\`\`\`
|
|
4010
|
+
workser scan # deps \xB7 secrets \xB7 permissions, over this folder
|
|
4011
|
+
workser scan --check # same, but exits non-zero on anything serious
|
|
4012
|
+
workser scan --only secrets # one check: deps, secrets, permissions
|
|
4013
|
+
workser scan --staged # look at staged changes only
|
|
4014
|
+
|
|
4015
|
+
workser health # is every published app still answering?
|
|
4016
|
+
workser health --app <webAppId> # just one
|
|
4017
|
+
\`\`\`
|
|
4018
|
+
|
|
4019
|
+
## scan
|
|
4020
|
+
|
|
4021
|
+
Three checks, all local \u2014 no login, no project, no network except for \`deps\`.
|
|
4022
|
+
|
|
4023
|
+
**secrets** looks at what your changes ADD (\`git diff HEAD\`), not at the whole
|
|
4024
|
+
tree, so it fires on the key you just wrote rather than on every example file
|
|
4025
|
+
forever. It knows the shapes that are actually credentials \u2014 AWS, GitHub,
|
|
4026
|
+
Stripe, OpenAI, Anthropic, Google, Slack, private keys, database URLs with a
|
|
4027
|
+
password in them, and \`API_KEY = "\u2026"\` with something real on the right. It
|
|
4028
|
+
ignores placeholders (\`your-api-key\`, \`process.env.X\`, \`<REPLACE_ME>\`) and
|
|
4029
|
+
example, fixture and lockfile paths.
|
|
4030
|
+
|
|
4031
|
+
**deps** runs \`npm audit\` and reports high and critical only. Moderate and low
|
|
4032
|
+
advisories on transitive dev dependencies are real and are not worth a report
|
|
4033
|
+
nobody finishes reading.
|
|
4034
|
+
|
|
4035
|
+
**permissions** catches three specific mistakes: a \`NEXT_PUBLIC_\u2026SECRET\`
|
|
4036
|
+
compiled into the browser bundle, an API that accepts credentialed requests
|
|
4037
|
+
from any website, and a real \`.env\` committed to the repository.
|
|
4038
|
+
|
|
4039
|
+
**A check that could not run says so.** Offline, \`deps\` reports "not checked"
|
|
4040
|
+
with the reason \u2014 never "nothing found". If you are quoting a scan result, quote
|
|
4041
|
+
what it checked as well as what it found.
|
|
4042
|
+
|
|
4043
|
+
If it finds a secret: move the value to \`workser env set\`, and treat the old one
|
|
4044
|
+
as leaked. Rotating it is the owner's decision, not yours \u2014 say so and let them.
|
|
4045
|
+
|
|
4046
|
+
## health
|
|
4047
|
+
|
|
4048
|
+
Probes the stable preview and production addresses of every app in the project
|
|
4049
|
+
and reports up or down, with the round trip. It exits non-zero if anything is
|
|
4050
|
+
down, so a step can gate on it.
|
|
4051
|
+
|
|
4052
|
+
Two things worth knowing:
|
|
4053
|
+
|
|
4054
|
+
* It is the same check the desktop runs on a timer. Both fold into one streak,
|
|
4055
|
+
so a run of yours counts toward the same total.
|
|
4056
|
+
* After three failed checks in a row on a **production** address that has
|
|
4057
|
+
worked before, an incident task is opened on the owner's board automatically.
|
|
4058
|
+
A preview address is checked and reported but never escalated \u2014 it is not
|
|
4059
|
+
customer-facing, and waking the owner for it teaches them to ignore the ones
|
|
4060
|
+
that are.
|
|
4061
|
+
|
|
4062
|
+
An app that has never been published has no address, so there is nothing to
|
|
4063
|
+
check. That is reported as a note, not as a pass.
|
|
3825
4064
|
`
|
|
3826
4065
|
},
|
|
3827
4066
|
{
|
|
@@ -3916,6 +4155,7 @@ them.
|
|
|
3916
4155
|
\`\`\`
|
|
3917
4156
|
workser artifact add <path> [--kind <k>] [-d <text>] # record a finished deliverable
|
|
3918
4157
|
workser artifact add --url <url> --kind app # record a deployed app
|
|
4158
|
+
workser artifact add <path> --kind <shape> --data <json> [--promote]
|
|
3919
4159
|
workser artifact run # which task you're attached to
|
|
3920
4160
|
|
|
3921
4161
|
workser ask "<question>" [--type <t>] [--option <o>] # ask the user, WAIT for the answer
|
|
@@ -3942,6 +4182,43 @@ are detected automatically); pass it explicitly for \`app\` / \`url\`.
|
|
|
3942
4182
|
To publish an app: \`workser deploy\` (preview) or \`workser deploy --prod\` (live), then
|
|
3943
4183
|
register the URL it returns as an \`app\` artifact so the user can open it from the task.
|
|
3944
4184
|
|
|
4185
|
+
## The shapes the task draws as cards
|
|
4186
|
+
|
|
4187
|
+
Most kinds say what kind of FILE something is. A few say what the user **asked
|
|
4188
|
+
for**, and those get a card of their own on the task:
|
|
4189
|
+
|
|
4190
|
+
| \`--kind\` | What the card shows | \`--data\` it reads |
|
|
4191
|
+
|---|---|---|
|
|
4192
|
+
| \`report\` | the chart behind a number | \u2014 |
|
|
4193
|
+
| \`walkthrough\` | the flow, as frames | \`frames\` |
|
|
4194
|
+
| \`before_after\` | a wipe between two pictures | \`shots: [{url,label}, \u2026]\` |
|
|
4195
|
+
| \`checks\` | what was tested | \`passed\`, \`total\` |
|
|
4196
|
+
| \`web_app\` | a published app | \`deployedAt\`, \`pagesChanged\` |
|
|
4197
|
+
| \`service\` | a job and what it reaches | \`nextRun\` |
|
|
4198
|
+
| \`design\` | a layout | \u2014 |
|
|
4199
|
+
|
|
4200
|
+
\`\`\`
|
|
4201
|
+
workser artifact add ./checks.json --kind checks --data '{"passed":12,"total":12}'
|
|
4202
|
+
workser artifact add --url https://acme.workser.app --kind web_app \\
|
|
4203
|
+
--data '{"deployedAt":"2026-08-20T14:02:00Z","pagesChanged":3}'
|
|
4204
|
+
\`\`\`
|
|
4205
|
+
|
|
4206
|
+
**Every \`--data\` field is optional, and a missing one is left off the card \u2014 it
|
|
4207
|
+
is never drawn as zero.** \`0 pages changed\` is a claim you cannot support and
|
|
4208
|
+
reads as "it did nothing"; saying nothing reads as "not measured", which is
|
|
4209
|
+
true. Only pass a figure you actually counted.
|
|
4210
|
+
|
|
4211
|
+
## Handing something up to the task
|
|
4212
|
+
|
|
4213
|
+
\`--promote\` marks an artifact as one of the things the user asked for, so it
|
|
4214
|
+
appears on the task itself instead of inside your step.
|
|
4215
|
+
|
|
4216
|
+
Use it when you know: the report they wanted, the app you published, the
|
|
4217
|
+
document explaining what changed. Do **not** promote working material \u2014
|
|
4218
|
+
screenshots you took to check your own work, intermediate exports, a scratch
|
|
4219
|
+
file. A task that hands up everything buries the six things they wanted under
|
|
4220
|
+
sixty they did not.
|
|
4221
|
+
|
|
3945
4222
|
## Ask the user something (and get an answer back)
|
|
3946
4223
|
|
|
3947
4224
|
When you're blocked \u2014 a missing value, an ambiguous requirement, permission for
|
|
@@ -3970,42 +4247,62 @@ without you ever seeing it).
|
|
|
3970
4247
|
},
|
|
3971
4248
|
{
|
|
3972
4249
|
topic: "deploy",
|
|
3973
|
-
title: "Deploy,
|
|
3974
|
-
summary: "Ship the app,
|
|
3975
|
-
commands: ["deploy", "
|
|
4250
|
+
title: "Deploy, addresses & logs",
|
|
4251
|
+
summary: "Ship the app, find its address, and find out why it is down.",
|
|
4252
|
+
commands: ["deploy", "logs", "versions", "urls", "deployments", "domain", "open", "verify"],
|
|
3976
4253
|
source: "skills/workser/reference/deploy.md",
|
|
3977
4254
|
body: `# Deploy, environment variables & logs
|
|
3978
4255
|
|
|
3979
4256
|
Getting the app online and configured, and finding out why it isn't.
|
|
3980
4257
|
|
|
3981
4258
|
\`\`\`
|
|
3982
|
-
workser deploy [--
|
|
4259
|
+
workser deploy [--env production] [--watch] # deploy (git \u2192 Vercel); default is preview
|
|
3983
4260
|
workser deploy status [id] # status of a deploy (default: latest)
|
|
3984
|
-
workser
|
|
3985
|
-
workser
|
|
3986
|
-
workser
|
|
4261
|
+
workser urls # every app's stable preview + live address
|
|
4262
|
+
workser logs [-n 100] [-f] [--env production] [--app <id>]
|
|
4263
|
+
workser versions [--env production] # history; the badge says which env is live
|
|
4264
|
+
workser deployments list [--env production] [--app <id>]
|
|
4265
|
+
workser deployments inspect <id> [--logs]
|
|
4266
|
+
workser deployments promote # ship the latest build (the owner confirms)
|
|
4267
|
+
workser deployments rollback <version> # put an earlier one back (the owner confirms)
|
|
4268
|
+
workser domain list # custom domains
|
|
4269
|
+
workser domain add shop.co.th # attach one (the owner confirms)
|
|
4270
|
+
workser domain add app.shop.co.th --app <webAppId>
|
|
4271
|
+
workser domain rm shop.co.th # detach one (the owner confirms)
|
|
3987
4272
|
workser open # open the live app
|
|
3988
4273
|
workser verify # run typecheck/lint/build
|
|
3989
4274
|
|
|
3990
|
-
workser env set KEY=VALUE [K2=V2\u2026] # set env vars
|
|
3991
|
-
workser env list # list keys (values masked)
|
|
3992
|
-
workser env get KEY # one value (sensitive)
|
|
3993
4275
|
\`\`\`
|
|
3994
4276
|
|
|
4277
|
+
Settings \u2014 \`workser env\` \u2014 are their own topic: \`workser help env\`.
|
|
4278
|
+
|
|
3995
4279
|
## Notes that matter
|
|
3996
4280
|
|
|
3997
4281
|
- **\`verify\` gates "done".** Run \`workser verify --json\` before you say a task is
|
|
3998
4282
|
finished. \`"ok": false\` means fix the listed errors and re-run \u2014 a green build is
|
|
3999
4283
|
the bar, not your reading of the diff.
|
|
4000
|
-
- **\`deploy\` without \`--
|
|
4001
|
-
risky; \`--
|
|
4284
|
+
- **\`deploy\` without \`--env\` is a preview.** Preview first when the change is
|
|
4285
|
+
risky; \`--env production\` (or the older \`--prod\`, which means the same) puts it
|
|
4286
|
+
in front of real users. Passing both, disagreeing, is refused rather than
|
|
4287
|
+
resolved.
|
|
4288
|
+
- **\`urls\` is where the address comes from \u2014 not the deploy response.** The host
|
|
4289
|
+
in a deploy response is per-build and the next deploy retires it. \`urls\`
|
|
4290
|
+
returns the stable ones, and says why an app has none rather than printing a
|
|
4291
|
+
blank.
|
|
4292
|
+
- **\`promote\` and \`rollback\` are the same upstream call and two commands on
|
|
4293
|
+
purpose.** Promote ships the newest build; rollback puts version N back. Both
|
|
4294
|
+
ask the owner and return exit 7 (\`awaiting_approval\`) until they answer \u2014 and
|
|
4295
|
+
that gate holds even on a "just do it" run.
|
|
4296
|
+
- **There is no \`deployments cancel\`.** Nothing upstream can stop a build that is
|
|
4297
|
+
already running. Wait for it and then promote or roll back.
|
|
4002
4298
|
- **\`--watch\` blocks until there's a live URL.** Without it you get a deploy id and
|
|
4003
4299
|
have to poll \`deploy status\`.
|
|
4004
|
-
- **\`
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4300
|
+
- **\`domain add\` and \`domain rm\` ask the owner to confirm** and return exit 7
|
|
4301
|
+
(\`awaiting_approval\`) until they do \u2014 tell them to approve, then retry the same
|
|
4302
|
+
command. Domains Workser owns (\`workser.ai\` and its subdomains) and hostnames
|
|
4303
|
+
the hosting provider assigns (\`*.vercel.app\`) are refused outright: those are
|
|
4304
|
+
not attachable, and the app's own preview and live URLs already exist without
|
|
4305
|
+
attaching anything.
|
|
4009
4306
|
|
|
4010
4307
|
## After a successful deploy
|
|
4011
4308
|
|
|
@@ -4016,13 +4313,178 @@ workser artifact add --url https://acme.workser.app --kind app -t "Storefront"
|
|
|
4016
4313
|
\`\`\`
|
|
4017
4314
|
|
|
4018
4315
|
See \`reference/deliverables.md\`.
|
|
4316
|
+
`
|
|
4317
|
+
},
|
|
4318
|
+
{
|
|
4319
|
+
topic: "docs",
|
|
4320
|
+
title: "Project documents",
|
|
4321
|
+
summary: "Write and revise the project's pages, keep the markdown mirror readable, and put the diagram in the document rather than in your reply.",
|
|
4322
|
+
commands: ["doc"],
|
|
4323
|
+
source: "skills/workser/reference/docs.md",
|
|
4324
|
+
body: `# Project documents
|
|
4325
|
+
|
|
4326
|
+
A document is a page in the project's Docs panel and a git-tracked markdown
|
|
4327
|
+
mirror at \`.workser/docs/<id>.md\`. Both are the same document: the panel renders
|
|
4328
|
+
the rich text, the mirror is what you, git and the next agent can read as text.
|
|
4329
|
+
|
|
4330
|
+
\`\`\`
|
|
4331
|
+
workser doc list [--work-item <id>]
|
|
4332
|
+
workser doc show <id> [--markdown]
|
|
4333
|
+
workser doc create <title> [--work-item <id>] [--markdown <text>]
|
|
4334
|
+
[--content-json <json>]
|
|
4335
|
+
workser doc update <id> [--title <text>] [--markdown <text>]
|
|
4336
|
+
workser doc diagram <id> [--check]
|
|
4337
|
+
\`\`\`
|
|
4338
|
+
|
|
4339
|
+
## Revise the page that exists
|
|
4340
|
+
|
|
4341
|
+
The project outlives your session, and a second copy of a page is worse than no
|
|
4342
|
+
page \u2014 nobody can tell which one is current.
|
|
4343
|
+
|
|
4344
|
+
\`\`\`
|
|
4345
|
+
workser doc list --json # is there already a page for this?
|
|
4346
|
+
workser doc update <id> --markdown "$(cat updated.md)"
|
|
4347
|
+
\`\`\`
|
|
4348
|
+
|
|
4349
|
+
\`workser doc show <id> --markdown\` reports the mirror's path so you can open the
|
|
4350
|
+
file with your normal tools instead of reconstructing prose from blocks.
|
|
4351
|
+
|
|
4352
|
+
\`--work-item <id>\` links a document to a Board card (a card has at most one). A
|
|
4353
|
+
linked document renders on its card and is *hidden* from the Docs panel, so a
|
|
4354
|
+
plan spanning several phases should stay unlinked \u2014 it belongs to the project,
|
|
4355
|
+
not to phase 1.
|
|
4356
|
+
|
|
4357
|
+
## Put the diagram in the document
|
|
4358
|
+
|
|
4359
|
+
A page explaining how something fits together should contain the picture, not a
|
|
4360
|
+
paragraph describing one. Write it as a \`\`\`mermaid fence in the markdown: the
|
|
4361
|
+
Docs panel renders it, \`git diff\` shows it as changed lines, and the next agent
|
|
4362
|
+
reads it without a screenshot.
|
|
4363
|
+
|
|
4364
|
+
\`\`\`
|
|
4365
|
+
workser doc diagram <id> --check # exits non-zero when the page has none
|
|
4366
|
+
\`\`\`
|
|
4367
|
+
|
|
4368
|
+
Use \`--check\` on any page whose job is to explain a structure \u2014 an architecture
|
|
4369
|
+
page, a data model, a flow. It reads the mirror on disk rather than the block
|
|
4370
|
+
content, which is deliberate: a diagram that exists in the editor but not in the
|
|
4371
|
+
mirror is invisible to git, to you, and to whoever opens the file next.
|
|
4372
|
+
`
|
|
4373
|
+
},
|
|
4374
|
+
{
|
|
4375
|
+
topic: "env",
|
|
4376
|
+
title: "Settings \u2014 cloud, per environment, and on this computer",
|
|
4377
|
+
summary: "Set and read an app's settings, hold a different value in production, and pull them onto this computer without clobbering local ones.",
|
|
4378
|
+
commands: ["env"],
|
|
4379
|
+
source: "skills/workser/reference/env.md",
|
|
4380
|
+
body: `# Settings \u2014 cloud, per environment, and on this computer
|
|
4381
|
+
|
|
4382
|
+
\`\`\`
|
|
4383
|
+
workser env set KEY=VALUE [K2=V2\u2026] [--env production]
|
|
4384
|
+
workser env list [--env production] # keys, masked; marks where they differ
|
|
4385
|
+
workser env get KEY [--env production] # one value (sensitive)
|
|
4386
|
+
workser env pull [--env production] [--overwrite]
|
|
4387
|
+
\`\`\`
|
|
4388
|
+
|
|
4389
|
+
## \`--env\` \u2014 say which environment you mean
|
|
4390
|
+
|
|
4391
|
+
\`deploy\`, \`logs\` and \`versions\` take \`--env preview\` or \`--env production\`.
|
|
4392
|
+
\`env set\` takes those and \`--env development\` as well.
|
|
4393
|
+
|
|
4394
|
+
Without it: \`deploy\` builds a preview, \`logs\` and \`versions\` talk about
|
|
4395
|
+
whichever deployment is newest, and \`env set\` writes to all three environments.
|
|
4396
|
+
Those are the old defaults and they have not changed.
|
|
4397
|
+
|
|
4398
|
+
**There is no development deployment.** Nothing is ever built into it \u2014 it is
|
|
4399
|
+
the environment the app uses when it runs on this computer \u2014 so \`deploy --env
|
|
4400
|
+
development\` and \`logs --env development\` are refused rather than quietly shown
|
|
4401
|
+
preview.
|
|
4402
|
+
|
|
4403
|
+
**A key can now hold a different value per environment.** \`env set --env
|
|
4404
|
+
production DATABASE_URL=\u2026\` writes an override; every other environment keeps
|
|
4405
|
+
the shared value. \`env list --env production\` and \`env get KEY --env production\`
|
|
4406
|
+
read it back.
|
|
4019
4407
|
|
|
4020
|
-
|
|
4408
|
+
Without \`--env\` you get the **shared** value \u2014 what the key is everywhere unless
|
|
4409
|
+
overridden \u2014 and \`env list\` marks which keys differ:
|
|
4021
4410
|
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4411
|
+
\`\`\`
|
|
4412
|
+
API_KEY = sk-\u2022\u2022\u2022 (different in production)
|
|
4413
|
+
\`\`\`
|
|
4414
|
+
|
|
4415
|
+
That marker is the one to read before changing anything: editing the shared
|
|
4416
|
+
value will not touch production if production has its own.
|
|
4417
|
+
|
|
4418
|
+
\`env rm KEY --env production\` removes just that override and the key keeps its
|
|
4419
|
+
shared value. \`env rm KEY\` removes the key entirely.
|
|
4420
|
+
|
|
4421
|
+
## Local settings are this computer's, and are not overwritten
|
|
4422
|
+
|
|
4423
|
+
\`env pull\` writes into the app folder's own env file \u2014 \`.env\` for most runtimes,
|
|
4424
|
+
\`.env.local\` for Next.js, because that is the file each one actually reads.
|
|
4425
|
+
|
|
4426
|
+
**It fills in what is missing and leaves what is already there alone.** A local
|
|
4427
|
+
\`DATABASE_URL\` usually points at the developer's own database on purpose;
|
|
4428
|
+
replacing it because somebody asked to pull one missing key destroys work
|
|
4429
|
+
Workser cannot give back. Keys it left alone are **named** in the output.
|
|
4430
|
+
|
|
4431
|
+
\`\`\`
|
|
4432
|
+
workser env pull # fill the gaps, touch nothing else
|
|
4433
|
+
workser env pull --env production # fill them from production's values
|
|
4434
|
+
workser env pull --overwrite # replace local values too
|
|
4435
|
+
\`\`\`
|
|
4436
|
+
|
|
4437
|
+
Starting an app for the first time does the same thing automatically, with the
|
|
4438
|
+
same rule.
|
|
4439
|
+
|
|
4440
|
+
## A key can hold a different value per environment
|
|
4441
|
+
|
|
4442
|
+
\`workser env set --env production DATABASE_URL=\u2026\` writes an override; every
|
|
4443
|
+
other environment keeps the shared value.
|
|
4444
|
+
|
|
4445
|
+
Without \`--env\` you get the **shared** value \u2014 what the key is everywhere unless
|
|
4446
|
+
overridden \u2014 and \`env list\` marks which keys differ:
|
|
4447
|
+
|
|
4448
|
+
\`\`\`
|
|
4449
|
+
API_KEY = sk-\u2022\u2022\u2022 (different in production)
|
|
4450
|
+
\`\`\`
|
|
4451
|
+
|
|
4452
|
+
Read that marker before changing anything: editing the shared value will not
|
|
4453
|
+
touch production if production has its own.
|
|
4454
|
+
|
|
4455
|
+
\`env rm KEY --env production\` removes just that override and the key keeps its
|
|
4456
|
+
shared value. \`env rm KEY\` removes the key entirely.
|
|
4457
|
+
|
|
4458
|
+
## Local settings are this computer's, and are not overwritten
|
|
4459
|
+
|
|
4460
|
+
\`env pull\` writes into the app folder's own env file \u2014 \`.env\` for most runtimes,
|
|
4461
|
+
\`.env.local\` for Next.js, because that is the file each one actually reads.
|
|
4462
|
+
|
|
4463
|
+
**It fills in what is missing and leaves what is already there alone.** A local
|
|
4464
|
+
\`DATABASE_URL\` usually points at the developer's own database on purpose;
|
|
4465
|
+
replacing it because somebody asked to pull one missing key destroys work
|
|
4466
|
+
Workser cannot give back. Keys it left alone are **named** in the output.
|
|
4467
|
+
|
|
4468
|
+
\`\`\`
|
|
4469
|
+
workser env pull # fill the gaps, touch nothing else
|
|
4470
|
+
workser env pull --env production # fill them from production's values
|
|
4471
|
+
workser env pull --overwrite # replace local values too
|
|
4472
|
+
\`\`\`
|
|
4473
|
+
|
|
4474
|
+
Starting an app for the first time does the same thing automatically, with the
|
|
4475
|
+
same rule.
|
|
4476
|
+
|
|
4477
|
+
## Notes that matter
|
|
4478
|
+
|
|
4479
|
+
- **\`env set\` writes a value you never see.** That's the point \u2014 when the user
|
|
4480
|
+
has a secret, have them run it (or set it in Orbit) rather than pasting it to
|
|
4481
|
+
you.
|
|
4482
|
+
- **\`env get\` returns a secret.** Don't echo it into the conversation.
|
|
4483
|
+
- **\`env rm\` is owner-only** (exit 6). Tell the user to do it in Orbit; don't
|
|
4484
|
+
look for a workaround.
|
|
4485
|
+
- **Cloud and local are different environments.** \`env set\` configures the
|
|
4486
|
+
cloud; the files in the app folder configure this computer. Don't hand-edit
|
|
4487
|
+
one to change the other.
|
|
4026
4488
|
`
|
|
4027
4489
|
},
|
|
4028
4490
|
{
|
|
@@ -4098,18 +4560,31 @@ can \`memory search\` and find it.
|
|
|
4098
4560
|
},
|
|
4099
4561
|
{
|
|
4100
4562
|
topic: "neon",
|
|
4101
|
-
title: "The project's own
|
|
4102
|
-
summary: "Neon-branch object storage and functions. Dedicated tenancy only.",
|
|
4563
|
+
title: "The project's own database",
|
|
4564
|
+
summary: "Branches, databases, compute, plus Neon-branch object storage and functions. Dedicated tenancy only.",
|
|
4103
4565
|
commands: ["neon"],
|
|
4104
4566
|
source: "skills/workser/reference/neon-backend.md",
|
|
4105
|
-
body: `# The project's own
|
|
4567
|
+
body: `# The project's own database
|
|
4106
4568
|
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4569
|
+
The project's database, run the way an operator runs one: branches (copies of
|
|
4570
|
+
the data), the databases on them, and the compute that serves them. Plus
|
|
4571
|
+
S3-compatible object storage and Node.js HTTP functions on the same branch.
|
|
4110
4572
|
|
|
4111
4573
|
\`\`\`
|
|
4112
4574
|
workser neon status # tenancy + toggles + region verdict
|
|
4575
|
+
|
|
4576
|
+
workser neon branch list # copies of the data; the live one is marked
|
|
4577
|
+
workser neon branch create qa-run # a copy to work on, made in a second
|
|
4578
|
+
workser neon branch create qa --from <id> --no-compute
|
|
4579
|
+
workser neon branch reset <branchId> # throw its changes away (asks the owner)
|
|
4580
|
+
workser neon branch rm <branchId> # delete it and its data (asks the owner)
|
|
4581
|
+
|
|
4582
|
+
workser neon database list [--branch <id>]
|
|
4583
|
+
workser neon database create <name> [--branch <id>] [--owner <role>]
|
|
4584
|
+
workser neon database rm <name> [--branch <id>] # asks the owner
|
|
4585
|
+
|
|
4586
|
+
workser neon endpoints # what compute is running, and idle
|
|
4587
|
+
|
|
4113
4588
|
workser neon storage list | create <name> | rm <bucket>
|
|
4114
4589
|
workser neon storage ls <bucket> [prefix]
|
|
4115
4590
|
workser neon storage put <bucket> <local> [key]
|
|
@@ -4127,6 +4602,24 @@ Region is fixed when the project is created. \`regionSupportsNeonBackend: false\
|
|
|
4127
4602
|
**final, not retryable** \u2014 no amount of waiting or retrying changes it. When you see
|
|
4128
4603
|
it, say so plainly and fall back to \`workser storage\` (the default bucket).
|
|
4129
4604
|
|
|
4605
|
+
## Branches are the useful one
|
|
4606
|
+
|
|
4607
|
+
A branch is a **full copy of the data**, made in about a second, costing almost
|
|
4608
|
+
nothing until something writes to it. That is what lets a check run against real
|
|
4609
|
+
data without being able to damage it \u2014 give a QA step its own branch instead of
|
|
4610
|
+
pointing it at the live database.
|
|
4611
|
+
|
|
4612
|
+
Two things cannot happen at all, whatever anyone approves: **the branch the app
|
|
4613
|
+
runs on cannot be deleted or reset**, and neither can **the database it connects
|
|
4614
|
+
to**. Those refusals come from the server, not from the approval prompt. If you
|
|
4615
|
+
meant to reset a copy and got that message, you named the live one.
|
|
4616
|
+
|
|
4617
|
+
\`reset\` deletes nothing by name and destroys just as much: it replaces a
|
|
4618
|
+
branch's contents with its source's. It asks the owner for exactly that reason.
|
|
4619
|
+
|
|
4620
|
+
\`--no-compute\` makes a branch with no compute. It is cheaper and **nothing can
|
|
4621
|
+
connect to it** \u2014 useful as a snapshot, useless as somewhere to run tests.
|
|
4622
|
+
|
|
4130
4623
|
## Notes that matter
|
|
4131
4624
|
|
|
4132
4625
|
- **\`neon storage rm <bucket>\` deletes the bucket and everything in it.** Not
|
|
@@ -4135,8 +4628,11 @@ it, say so plainly and fall back to \`workser storage\` (the default bucket).
|
|
|
4135
4628
|
through Workser.
|
|
4136
4629
|
- **Functions deploy from a zip.** Build the bundle first, then
|
|
4137
4630
|
\`workser neon functions deploy <slug> <zip>\`.
|
|
4138
|
-
-
|
|
4139
|
-
|
|
4631
|
+
- **\`neon endpoints\` is the cost question.** \`active\` means it is billing;
|
|
4632
|
+
\`idle\` means it is not. It is the only place in the product that answers "what
|
|
4633
|
+
is this database costing me while nothing is happening".
|
|
4634
|
+
- **Most apps never need the storage or functions half.** If the user just wants
|
|
4635
|
+
to store uploads, the default bucket in \`reference/storage.md\` is the answer.
|
|
4140
4636
|
`
|
|
4141
4637
|
},
|
|
4142
4638
|
{
|
|
@@ -4189,28 +4685,27 @@ real scope, not just a repeat of the task.
|
|
|
4189
4685
|
},
|
|
4190
4686
|
{
|
|
4191
4687
|
topic: "sdlc-entities",
|
|
4192
|
-
title: "
|
|
4193
|
-
summary: "Read what this project already
|
|
4194
|
-
commands: ["board", "decision", "requirement"
|
|
4688
|
+
title: "Decisions and requirements",
|
|
4689
|
+
summary: "Read what this project already decided, and record what a future maintainer will need. Phased work itself is subtasks, not board cards \u2014 see `workser help tasks`.",
|
|
4690
|
+
commands: ["board", "decision", "requirement"],
|
|
4195
4691
|
source: "skills/workser/reference/sdlc-entities.md",
|
|
4196
|
-
body: `#
|
|
4692
|
+
body: `# Decisions and requirements
|
|
4197
4693
|
|
|
4198
4694
|
These are the project's memory across sessions. They write to the **same tables**
|
|
4199
|
-
the Orbit desktop's
|
|
4200
|
-
|
|
4201
|
-
|
|
4695
|
+
the Orbit desktop's Project Memory panel uses, so anything here appears there too
|
|
4696
|
+
\u2014 and, inside an Orbit-spawned run, as an inline card in the conversation.
|
|
4697
|
+
Documents have their own guide: \`workser help docs\`. Phased work is tracked as
|
|
4698
|
+
subtasks, not here \u2014 see \`workser help tasks\`.
|
|
4699
|
+
|
|
4700
|
+
> **The Board (\`workser board ...\`) is deprecated for agent use.** It used to be
|
|
4701
|
+
> where a multi-phase plan went, one card per phase \u2014 and the phase was ALSO a
|
|
4702
|
+
> subtask the planning turn had just filed for the same piece of work. That gave
|
|
4703
|
+
> a task two competing plans, one of which this task's own page never reads and
|
|
4704
|
+
> nothing kept in sync with the other. Phases are \`project_tasks\` subtasks now,
|
|
4705
|
+
> full stop: \`workser task subtask add\`. Do not run \`workser board create\` for
|
|
4706
|
+
> planned work \u2014 see \`workser help tasks\`.
|
|
4202
4707
|
|
|
4203
4708
|
\`\`\`
|
|
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
4709
|
workser decision list [--limit <n>]
|
|
4215
4710
|
workser decision show <id>
|
|
4216
4711
|
workser decision create <title> --context <text> --decision <text>
|
|
@@ -4221,11 +4716,6 @@ workser requirement show <id>
|
|
|
4221
4716
|
workser requirement create <title> --body <text> [--status <text>]
|
|
4222
4717
|
workser requirement update <id> [--title <text>] [--body <text>] [--status <text>]
|
|
4223
4718
|
|
|
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
4719
|
\`\`\`
|
|
4230
4720
|
|
|
4231
4721
|
## Read first \u2014 this is the part that matters
|
|
@@ -4233,7 +4723,6 @@ workser doc update <id> [--title <text>] [--markdown <text>]
|
|
|
4233
4723
|
Before starting anything beyond a trivial edit:
|
|
4234
4724
|
|
|
4235
4725
|
\`\`\`
|
|
4236
|
-
workser board list --json # what's already tracked (don't re-file it)
|
|
4237
4726
|
workser decision list --json # what was already decided (don't reverse it)
|
|
4238
4727
|
\`\`\`
|
|
4239
4728
|
|
|
@@ -4243,20 +4732,18 @@ purpose \u2014 \`workser decision show <id>\` gives you the context and conseque
|
|
|
4243
4732
|
not just the title. Reach for \`workser doc list\` / \`workser requirement list\`
|
|
4244
4733
|
the same way when the task touches documented behaviour.
|
|
4245
4734
|
|
|
4246
|
-
## Work with phases \u2192
|
|
4735
|
+
## Work with phases \u2192 subtasks + a plan doc, before you build
|
|
4247
4736
|
|
|
4248
4737
|
The moment you split a task into more than one phase, file it \u2014 not afterwards,
|
|
4249
4738
|
and not only in your reply, which is gone once the conversation scrolls.
|
|
4250
4739
|
|
|
4251
4740
|
\`\`\`bash
|
|
4252
|
-
#
|
|
4253
|
-
workser
|
|
4254
|
-
--
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
# the plan itself, ONE doc, deliberately NOT linked to a card
|
|
4741
|
+
# the phases themselves \u2014 this task's own subtask list, not the Board
|
|
4742
|
+
workser task subtask add "Phase 1 \u2014 schema + migration" --role api \\
|
|
4743
|
+
--note "Add orders/line_items tables and the migration."
|
|
4744
|
+
workser task subtask add "Phase 2 \u2014 checkout API" --role api --note "\u2026"
|
|
4745
|
+
|
|
4746
|
+
# the plan's narrative, ONE doc, deliberately NOT linked to a subtask
|
|
4260
4747
|
workser doc create "Checkout \u2014 implementation plan" --markdown "$(cat plan.md)" --json
|
|
4261
4748
|
|
|
4262
4749
|
# the approach, if the plan settled something with real alternatives
|
|
@@ -4264,49 +4751,23 @@ workser decision create "Carts live server-side" --context "\u2026" --decision "
|
|
|
4264
4751
|
\`\`\`
|
|
4265
4752
|
|
|
4266
4753
|
**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
|
|
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
|
|
4754
|
+
its card and is *hidden* from the Docs panel; a plan spanning three phases
|
|
4755
|
+
belongs to the project, not to phase 1.
|
|
4274
4756
|
|
|
4275
|
-
|
|
4276
|
-
|
|
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.
|
|
4757
|
+
The bar: if the user closed this conversation now, the subtask list should still
|
|
4758
|
+
show what's left and the doc should still explain the plan to whoever continues
|
|
4759
|
+
it.
|
|
4297
4760
|
|
|
4298
4761
|
## Decisions are append-only
|
|
4299
4762
|
|
|
4300
4763
|
\`decision create\` is for something with real tradeoffs worth a paper trail:
|
|
4301
|
-
\`--context\` is why it came up, \`--decision\`
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
being worth reading.
|
|
4764
|
+
\`--context\` is why it came up, \`--decision\` what was decided, \`--consequences\`
|
|
4765
|
+
the follow-on effects. There is deliberately **no \`decision update\`** \u2014 a record
|
|
4766
|
+
states what was decided at a point in time. When it stops being right, record a
|
|
4767
|
+
new decision that supersedes it and say so in its \`--context\`. Editing the
|
|
4768
|
+
history is how a decision log stops being worth reading.
|
|
4307
4769
|
|
|
4308
|
-
Requirements
|
|
4309
|
-
\`update\`.
|
|
4770
|
+
Requirements legitimately move along, so they do have \`update\`.
|
|
4310
4771
|
|
|
4311
4772
|
\`\`\`
|
|
4312
4773
|
workser requirement create "Support SSO" --body "Enterprise customers need SAML." \\
|
|
@@ -4317,25 +4778,11 @@ workser requirement update <id> --status done
|
|
|
4317
4778
|
## Docs
|
|
4318
4779
|
|
|
4319
4780
|
\`--markdown\` is the normal way to write one. The body is stored both as the
|
|
4320
|
-
rich
|
|
4321
|
-
|
|
4322
|
-
|
|
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
|
|
4781
|
+
rich text the Docs panel renders and as a git-tracked mirror at
|
|
4782
|
+
\`.workser/docs/<id>.md\`; \`workser doc show <id> --markdown\` reports that path so
|
|
4783
|
+
you can read the file with your normal tools.
|
|
4334
4784
|
|
|
4335
|
-
|
|
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.
|
|
4785
|
+
Revise trd saying "fix the bug" is not fixing the bug.
|
|
4339
4786
|
`
|
|
4340
4787
|
},
|
|
4341
4788
|
{
|
|
@@ -4421,6 +4868,9 @@ below defaults to it and you rarely pass an id at all.
|
|
|
4421
4868
|
\`\`\`
|
|
4422
4869
|
workser task list [--status <value>] [--label <value>] [--limit <n>]
|
|
4423
4870
|
workser task show [id] # the task you are in, with its steps
|
|
4871
|
+
workser task create <title> [--note <text>] [--kind <value>]
|
|
4872
|
+
[--label <value...>] [--app <id...>]
|
|
4873
|
+
[--infra <ref...>]
|
|
4424
4874
|
|
|
4425
4875
|
workser task subtask add <title> [--role <value>] [--kind <value>]
|
|
4426
4876
|
[--note <text>] [--app <id...>]
|
|
@@ -4429,6 +4879,7 @@ workser task subtask add <title> [--role <value>] [--kind <value>]
|
|
|
4429
4879
|
workser task subtask list [taskId]
|
|
4430
4880
|
workser task subtask update <id> [--title|--note|--role|--kind|--scope]
|
|
4431
4881
|
workser task subtask remove <id>
|
|
4882
|
+
workser task subtask send-back <id> --note <text> # redo it, and say why
|
|
4432
4883
|
|
|
4433
4884
|
workser task can-start [id] # may work begin? refuses until approved
|
|
4434
4885
|
workser task approval request # tell the owner the plan is ready
|
|
@@ -4443,6 +4894,16 @@ Team's own table. Filing your plan on the Board puts it somewhere the owner's
|
|
|
4443
4894
|
task page never reads: they see "created work item" and an empty plan. Use
|
|
4444
4895
|
\`task subtask add\`.
|
|
4445
4896
|
|
|
4897
|
+
## Opening work from a project channel
|
|
4898
|
+
|
|
4899
|
+
When a project-channel conversation produces actionable work, the PM may record
|
|
4900
|
+
it with \`workser task create\`. Orbit supplies the channel and source-message IDs;
|
|
4901
|
+
the command records them and posts the new task card as a Project Manager message
|
|
4902
|
+
automatically. Do not invent or ask for those IDs.
|
|
4903
|
+
|
|
4904
|
+
Opening a task does **not** approve it or start implementation. The task remains
|
|
4905
|
+
awaiting the owner. Never approve or dispatch a task you opened yourself.
|
|
4906
|
+
|
|
4446
4907
|
## Planning a task
|
|
4447
4908
|
|
|
4448
4909
|
Read the project first, then propose. One \`subtask add\` per step:
|
|
@@ -4487,6 +4948,75 @@ workser task done --summary "The report now shows cost per KOL, with six months
|
|
|
4487
4948
|
\`\`\`
|
|
4488
4949
|
|
|
4489
4950
|
Write the summary for someone who runs a business and does not read code.
|
|
4951
|
+
|
|
4952
|
+
## Sending a step back
|
|
4953
|
+
|
|
4954
|
+
A step that finished but is not good enough is **sent back**, not replaced:
|
|
4955
|
+
|
|
4956
|
+
\`\`\`
|
|
4957
|
+
workser task subtask send-back 3f2a\u2026 --note "The totals ignore refunds."
|
|
4958
|
+
\`\`\`
|
|
4959
|
+
|
|
4960
|
+
That puts it in the queue again as a **second attempt** on the same step. Two
|
|
4961
|
+
reasons it matters that this is not a new step:
|
|
4962
|
+
|
|
4963
|
+
- The owner's screen can then say *"1 send-back, fixed \u2014 2nd run passed"*. A
|
|
4964
|
+
replacement step says only that two steps exist, which tells them nothing
|
|
4965
|
+
about whether their team caught its own mistake.
|
|
4966
|
+
- \`--note\` is the reason, and it is recorded against the attempt being
|
|
4967
|
+
rejected. Without it the history can say a step ran twice but not why.
|
|
4968
|
+
|
|
4969
|
+
It refuses a step that is still working. Let it finish first \u2014 the run is
|
|
4970
|
+
still writing to it.
|
|
4971
|
+
`
|
|
4972
|
+
},
|
|
4973
|
+
{
|
|
4974
|
+
topic: "usage",
|
|
4975
|
+
title: "Usage \u2014 what is being used, against the plan",
|
|
4976
|
+
summary: "How much database, file storage, projects and apps are in use, and how close that is to what the plan allows.",
|
|
4977
|
+
commands: ["usage"],
|
|
4978
|
+
source: "skills/workser/reference/usage.md",
|
|
4979
|
+
body: `# Usage \u2014 what is being used, against the plan
|
|
4980
|
+
|
|
4981
|
+
\`\`\`
|
|
4982
|
+
workser usage # storage, projects, apps \u2014 and how close each is to the limit
|
|
4983
|
+
\`\`\`
|
|
4984
|
+
|
|
4985
|
+
Run it before you propose anything that adds to a count. "Create another
|
|
4986
|
+
project" is a plan you can only sensibly make if you know the plan allows two
|
|
4987
|
+
and two already exist.
|
|
4988
|
+
|
|
4989
|
+
## Two scopes in one answer, on purpose
|
|
4990
|
+
|
|
4991
|
+
* **Database and files are ORGANISATION-wide.** One pool across every project.
|
|
4992
|
+
There is no per-project storage limit, and reporting one would invent it.
|
|
4993
|
+
* **Projects, and apps in this project, are counted where they apply.** These
|
|
4994
|
+
are the limits people actually hit.
|
|
4995
|
+
|
|
4996
|
+
## Two kinds of limit, which do not mean the same thing
|
|
4997
|
+
|
|
4998
|
+
* **Hard cap** \u2014 projects, apps. Going over is **refused**. \`workser usage\`
|
|
4999
|
+
exits non-zero when one is reached, so a step can gate on it.
|
|
5000
|
+
* **Soft allowance** \u2014 database, files. Going over is **billed as extra**,
|
|
5001
|
+
never blocked. It does not fail the command, because a customer growing past
|
|
5002
|
+
their allowance should not have their automation start breaking that day.
|
|
5003
|
+
|
|
5004
|
+
## "not measured" is not zero
|
|
5005
|
+
|
|
5006
|
+
A figure that could not be read prints as \`not measured\`, with the reason, and
|
|
5007
|
+
draws no bar. Do not report it as \`0\`, and do not tell the user they have room
|
|
5008
|
+
based on it \u2014 nobody looked.
|
|
5009
|
+
|
|
5010
|
+
If a scan comes back with a figure missing, say which one and why. "Your
|
|
5011
|
+
database is using 2.5 GB of 10; the file total could not be read" is a useful
|
|
5012
|
+
sentence. "You are using 2.5 GB of 20" is not, and it is wrong.
|
|
5013
|
+
|
|
5014
|
+
## What to do with it
|
|
5015
|
+
|
|
5016
|
+
- Near a **soft** limit: tell the owner what the extra will cost them, and what
|
|
5017
|
+
is taking the space. Do not delete anything to make a number look better.
|
|
5018
|
+
- At a **hard** cap: say which plan raises it. Do not attempt the create \u2014 it
|
|
5019
|
+
will be refused, and a failed attempt reads to the owner as a broken product.
|
|
4490
5020
|
`
|
|
4491
5021
|
},
|
|
4492
5022
|
{
|
|
@@ -4618,11 +5148,11 @@ function registerHelp(program3) {
|
|
|
4618
5148
|
var import_picocolors3 = __toESM(require_picocolors(), 1);
|
|
4619
5149
|
|
|
4620
5150
|
// src/context.ts
|
|
4621
|
-
import { resolve } from "path";
|
|
5151
|
+
import { resolve as resolve2 } from "path";
|
|
4622
5152
|
|
|
4623
5153
|
// src/config.ts
|
|
4624
5154
|
import { homedir } from "os";
|
|
4625
|
-
import { join } from "path";
|
|
5155
|
+
import { dirname, join, resolve } from "path";
|
|
4626
5156
|
import {
|
|
4627
5157
|
chmodSync,
|
|
4628
5158
|
existsSync,
|
|
@@ -4630,6 +5160,8 @@ import {
|
|
|
4630
5160
|
readFileSync,
|
|
4631
5161
|
writeFileSync
|
|
4632
5162
|
} from "fs";
|
|
5163
|
+
var PROJECT_MARKER = ".workser-project";
|
|
5164
|
+
var APP_MARKER = ".workser-app";
|
|
4633
5165
|
var GLOBAL_DIR = join(homedir(), ".workser");
|
|
4634
5166
|
var SESSION_FILE = join(GLOBAL_DIR, "session.json");
|
|
4635
5167
|
function readSession() {
|
|
@@ -4660,10 +5192,82 @@ function projectLinkPath(cwd) {
|
|
|
4660
5192
|
return join(cwd, ".workser", "project.json");
|
|
4661
5193
|
}
|
|
4662
5194
|
function readProjectLink(cwd) {
|
|
5195
|
+
const found = readFolderIdentity(cwd);
|
|
5196
|
+
if (!found) return null;
|
|
5197
|
+
return {
|
|
5198
|
+
projectId: found.projectId,
|
|
5199
|
+
name: found.projectName,
|
|
5200
|
+
orgId: found.orgId,
|
|
5201
|
+
root: found.projectRoot
|
|
5202
|
+
};
|
|
5203
|
+
}
|
|
5204
|
+
function readFolderIdentity(cwd) {
|
|
5205
|
+
let app = null;
|
|
5206
|
+
for (const dir of ancestors(cwd)) {
|
|
5207
|
+
if (!app) {
|
|
5208
|
+
const marker2 = readJson(join(dir, APP_MARKER));
|
|
5209
|
+
if (marker2 && typeof marker2.appId === "string") {
|
|
5210
|
+
app = { marker: marker2, root: dir };
|
|
5211
|
+
}
|
|
5212
|
+
}
|
|
5213
|
+
const link = readJson(projectLinkPath(dir));
|
|
5214
|
+
if (link && typeof link.projectId === "string") {
|
|
5215
|
+
return {
|
|
5216
|
+
projectRoot: dir,
|
|
5217
|
+
projectId: link.projectId,
|
|
5218
|
+
projectName: link.name,
|
|
5219
|
+
orgId: link.orgId,
|
|
5220
|
+
...appFields(app)
|
|
5221
|
+
};
|
|
5222
|
+
}
|
|
5223
|
+
const marker = readJson(join(dir, PROJECT_MARKER));
|
|
5224
|
+
if (marker && typeof marker.projectId === "string") {
|
|
5225
|
+
return {
|
|
5226
|
+
projectRoot: dir,
|
|
5227
|
+
projectId: marker.projectId,
|
|
5228
|
+
projectName: marker.projectName ?? void 0,
|
|
5229
|
+
orgId: marker.orgId ?? void 0,
|
|
5230
|
+
...appFields(app)
|
|
5231
|
+
};
|
|
5232
|
+
}
|
|
5233
|
+
}
|
|
5234
|
+
if (app && typeof app.marker.projectId === "string") {
|
|
5235
|
+
return {
|
|
5236
|
+
projectRoot: app.root,
|
|
5237
|
+
projectId: app.marker.projectId,
|
|
5238
|
+
projectName: app.marker.projectName ?? void 0,
|
|
5239
|
+
orgId: app.marker.orgId ?? void 0,
|
|
5240
|
+
...appFields(app)
|
|
5241
|
+
};
|
|
5242
|
+
}
|
|
5243
|
+
return null;
|
|
5244
|
+
}
|
|
5245
|
+
function appFields(app) {
|
|
5246
|
+
if (!app) return {};
|
|
5247
|
+
return {
|
|
5248
|
+
appId: app.marker.appId,
|
|
5249
|
+
appName: app.marker.appName ?? void 0,
|
|
5250
|
+
appRoot: app.root
|
|
5251
|
+
};
|
|
5252
|
+
}
|
|
5253
|
+
function ancestors(cwd) {
|
|
5254
|
+
const out = [];
|
|
5255
|
+
const home = homedir();
|
|
5256
|
+
let dir = resolve(cwd);
|
|
5257
|
+
for (let i = 0; i < 64; i++) {
|
|
5258
|
+
out.push(dir);
|
|
5259
|
+
if (dir === home) break;
|
|
5260
|
+
const parent = dirname(dir);
|
|
5261
|
+
if (parent === dir) break;
|
|
5262
|
+
dir = parent;
|
|
5263
|
+
}
|
|
5264
|
+
return out;
|
|
5265
|
+
}
|
|
5266
|
+
function readJson(file) {
|
|
4663
5267
|
try {
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
return
|
|
5268
|
+
if (!existsSync(file)) return null;
|
|
5269
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
5270
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
4667
5271
|
} catch {
|
|
4668
5272
|
return null;
|
|
4669
5273
|
}
|
|
@@ -4702,7 +5306,7 @@ function cloudBaseUrl() {
|
|
|
4702
5306
|
// src/context.ts
|
|
4703
5307
|
function buildContext(opts) {
|
|
4704
5308
|
const session = readSession();
|
|
4705
|
-
const cwd = opts.cwd ?
|
|
5309
|
+
const cwd = opts.cwd ? resolve2(opts.cwd) : process.cwd();
|
|
4706
5310
|
const token = opts.token || process.env.WORKSER_TOKEN || session.token;
|
|
4707
5311
|
const overridden = Boolean(
|
|
4708
5312
|
opts.endpoint || opts.token || process.env.WORKSER_DAEMON_URL || process.env.WORKSER_TOKEN
|
|
@@ -4711,11 +5315,18 @@ function buildContext(opts) {
|
|
|
4711
5315
|
const endpointRaw = socketPath ? "http://localhost" : opts.endpoint || process.env.WORKSER_DAEMON_URL || session.endpoint || cloudBaseUrl();
|
|
4712
5316
|
const endpoint = endpointRaw.replace(/\/+$/, "");
|
|
4713
5317
|
const mode2 = socketPath ? "daemon" : /^https?:\/\/(127\.0\.0\.1|localhost|\[::1\])(:|\/|$)/i.test(endpoint) ? "daemon" : "cloud";
|
|
4714
|
-
const
|
|
4715
|
-
const projectId = opts.project || process.env.WORKSER_PROJECT_ID ||
|
|
5318
|
+
const folder = readFolderIdentity(cwd);
|
|
5319
|
+
const projectId = opts.project || process.env.WORKSER_PROJECT_ID || folder?.projectId || session.defaultProjectId;
|
|
5320
|
+
const inThisProject = folder?.projectId === projectId;
|
|
4716
5321
|
const runId = process.env.WORKSER_RUN_ID || void 0;
|
|
4717
5322
|
const conversationId = process.env.WORKSER_CONVERSATION_ID || void 0;
|
|
4718
5323
|
const projectTaskId = process.env.WORKSER_PROJECT_TASK_ID || void 0;
|
|
5324
|
+
const parentTaskId = process.env.WORKSER_PARENT_TASK_ID || void 0;
|
|
5325
|
+
const projectChannelId = process.env.WORKSER_PROJECT_CHANNEL_ID || void 0;
|
|
5326
|
+
const projectChannelMessageId = process.env.WORKSER_PROJECT_CHANNEL_MESSAGE_ID || void 0;
|
|
5327
|
+
const agentRole = process.env.WORKSER_AGENT_ROLE || void 0;
|
|
5328
|
+
const agentType = process.env.WORKSER_AGENT_TYPE || void 0;
|
|
5329
|
+
const agentModel = process.env.WORKSER_AGENT_MODEL || void 0;
|
|
4719
5330
|
return {
|
|
4720
5331
|
endpoint,
|
|
4721
5332
|
socketPath,
|
|
@@ -4723,9 +5334,18 @@ function buildContext(opts) {
|
|
|
4723
5334
|
mode: mode2,
|
|
4724
5335
|
cwd,
|
|
4725
5336
|
projectId,
|
|
5337
|
+
projectRoot: inThisProject ? folder?.projectRoot : void 0,
|
|
5338
|
+
appId: inThisProject ? folder?.appId : void 0,
|
|
5339
|
+
appName: inThisProject ? folder?.appName : void 0,
|
|
4726
5340
|
runId,
|
|
4727
5341
|
conversationId,
|
|
4728
|
-
projectTaskId
|
|
5342
|
+
projectTaskId,
|
|
5343
|
+
parentTaskId,
|
|
5344
|
+
projectChannelId,
|
|
5345
|
+
projectChannelMessageId,
|
|
5346
|
+
agentRole,
|
|
5347
|
+
agentType,
|
|
5348
|
+
agentModel
|
|
4729
5349
|
};
|
|
4730
5350
|
}
|
|
4731
5351
|
function runTarget(ctx) {
|
|
@@ -4746,10 +5366,22 @@ Commands that only read your account (projects, env, db, logs, status) work as n
|
|
|
4746
5366
|
{ code: "needs_local_app" }
|
|
4747
5367
|
);
|
|
4748
5368
|
}
|
|
5369
|
+
function requireDaemon(ctx, what, why) {
|
|
5370
|
+
if (ctx.mode === "daemon") return;
|
|
5371
|
+
throw new WorkserError(
|
|
5372
|
+
`\`workser ${what}\` ${why}, so it needs the Workser app running on this computer.
|
|
5373
|
+
|
|
5374
|
+
This shell is talking to ${ctx.endpoint} instead of a local app.
|
|
5375
|
+
|
|
5376
|
+
\u2022 On your own computer: open Workser and try again.
|
|
5377
|
+
\u2022 On a computer without Workser: install it from https://workser.ai/download and sign in.`,
|
|
5378
|
+
{ code: "needs_local_app" }
|
|
5379
|
+
);
|
|
5380
|
+
}
|
|
4749
5381
|
function requireProject(ctx) {
|
|
4750
5382
|
if (!ctx.projectId) {
|
|
4751
5383
|
throw new WorkserError(
|
|
4752
|
-
"No project selected
|
|
5384
|
+
"No project selected.\n\nWorkser keeps each project in its own folder \u2014 `~/workser/<org>/<project>/`,\nwith that project's apps inside it. Running from anywhere in that tree is\nenough; this shell is not in one.\n\n \u2022 cd into the project's folder (Workser Orbit's Files tab shows where it is), or\n \u2022 pass --project <id> for a one-off.",
|
|
4753
5385
|
{ code: "no_project" }
|
|
4754
5386
|
);
|
|
4755
5387
|
}
|
|
@@ -4774,7 +5406,7 @@ function action(fn) {
|
|
|
4774
5406
|
// src/client.ts
|
|
4775
5407
|
import * as http from "http";
|
|
4776
5408
|
function requestOverSocket(socketPath, pathWithQuery, init) {
|
|
4777
|
-
return new Promise((
|
|
5409
|
+
return new Promise((resolve5, reject) => {
|
|
4778
5410
|
const req = http.request(
|
|
4779
5411
|
{ socketPath, path: pathWithQuery, method: init.method, headers: init.headers },
|
|
4780
5412
|
(res) => {
|
|
@@ -4783,7 +5415,7 @@ function requestOverSocket(socketPath, pathWithQuery, init) {
|
|
|
4783
5415
|
res.on("data", (c) => text += c);
|
|
4784
5416
|
res.on(
|
|
4785
5417
|
"end",
|
|
4786
|
-
() =>
|
|
5418
|
+
() => resolve5({
|
|
4787
5419
|
ok: (res.statusCode ?? 0) >= 200 && (res.statusCode ?? 0) < 300,
|
|
4788
5420
|
status: res.statusCode ?? 0,
|
|
4789
5421
|
statusText: res.statusMessage ?? "",
|
|
@@ -4880,16 +5512,31 @@ function registerStatus(program3) {
|
|
|
4880
5512
|
program3.command("status").description("Show connection, current workspace/project, and latest deploy").action(
|
|
4881
5513
|
action(async ({ ctx }) => {
|
|
4882
5514
|
const data = await api(ctx, "/v1/status", { query: { project: ctx.projectId } });
|
|
4883
|
-
|
|
5515
|
+
const merged = {
|
|
5516
|
+
...data,
|
|
5517
|
+
folder: {
|
|
5518
|
+
cwd: ctx.cwd,
|
|
5519
|
+
projectRoot: ctx.projectRoot ?? null,
|
|
5520
|
+
appId: ctx.appId ?? null,
|
|
5521
|
+
appName: ctx.appName ?? null
|
|
5522
|
+
}
|
|
5523
|
+
};
|
|
5524
|
+
ok(merged, () => {
|
|
4884
5525
|
line(import_picocolors3.default.bold("Workser") + import_picocolors3.default.dim(` (${ctx.mode} \xB7 ${ctx.endpoint})`));
|
|
4885
5526
|
line(` user: ${data.user?.email ?? "\u2014"}`);
|
|
4886
5527
|
line(` workspace: ${data.workspace?.name ?? "\u2014"}`);
|
|
4887
5528
|
line(
|
|
4888
5529
|
` project: ${data.project?.name ?? "\u2014"}` + (data.project?.id ? import_picocolors3.default.dim(` (${data.project.id})`) : "")
|
|
4889
5530
|
);
|
|
4890
|
-
if (
|
|
5531
|
+
if (ctx.projectRoot) line(` folder: ${import_picocolors3.default.dim(ctx.projectRoot)}`);
|
|
5532
|
+
if (ctx.appId) {
|
|
4891
5533
|
line(
|
|
4892
|
-
`
|
|
5534
|
+
` app: ${ctx.appName ?? "\u2014"}` + import_picocolors3.default.dim(` (${ctx.appId})`)
|
|
5535
|
+
);
|
|
5536
|
+
}
|
|
5537
|
+
if (data.latestDeploy) {
|
|
5538
|
+
line(
|
|
5539
|
+
` deploy: ${colorStatus(data.latestDeploy.status)}` + (data.latestDeploy.url ? ` ${import_picocolors3.default.cyan(data.latestDeploy.url)}` : "")
|
|
4893
5540
|
);
|
|
4894
5541
|
}
|
|
4895
5542
|
});
|
|
@@ -5223,7 +5870,7 @@ function registerAuth(program3) {
|
|
|
5223
5870
|
// src/commands/storage.ts
|
|
5224
5871
|
var import_picocolors7 = __toESM(require_picocolors(), 1);
|
|
5225
5872
|
import { readFile, writeFile } from "fs/promises";
|
|
5226
|
-
import { basename, dirname } from "path";
|
|
5873
|
+
import { basename, dirname as dirname2 } from "path";
|
|
5227
5874
|
function registerStorage(program3) {
|
|
5228
5875
|
const storage = program3.command("storage").description("Provision and work with the project's object storage (Cloudflare R2)");
|
|
5229
5876
|
storage.command("create [name]").description("Provision a storage bucket for the project (idempotent)").action(
|
|
@@ -5269,7 +5916,7 @@ function registerStorage(program3) {
|
|
|
5269
5916
|
const bytes = await readFile(localPath).catch(() => {
|
|
5270
5917
|
throw new WorkserError(`Can't read local file: ${localPath}`, { code: "bad_input" });
|
|
5271
5918
|
});
|
|
5272
|
-
const dir =
|
|
5919
|
+
const dir = dirname2(key);
|
|
5273
5920
|
const res = await api(ctx, `/v1/projects/${projectId}/storage/upload-base64`, {
|
|
5274
5921
|
body: {
|
|
5275
5922
|
filename: basename(key),
|
|
@@ -5335,7 +5982,7 @@ import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
|
5335
5982
|
import { basename as basename2 } from "path";
|
|
5336
5983
|
function registerNeon(program3) {
|
|
5337
5984
|
const neon = program3.command("neon").description(
|
|
5338
|
-
"The project's own
|
|
5985
|
+
"The project's own database: branches, databases, compute, storage and functions"
|
|
5339
5986
|
);
|
|
5340
5987
|
neon.command("status").description(
|
|
5341
5988
|
"Whether this project can use Neon storage/functions (tenancy, toggles, region)"
|
|
@@ -5557,6 +6204,137 @@ function registerNeon(program3) {
|
|
|
5557
6204
|
ok(res, () => success(`Deleted function ${args[0]}.`));
|
|
5558
6205
|
})
|
|
5559
6206
|
);
|
|
6207
|
+
const branch = neon.command("branch").description("Copies of the project's data, to work on without touching it");
|
|
6208
|
+
branch.command("list").description("Every branch, with the one the app runs on marked").action(
|
|
6209
|
+
action(async ({ ctx }) => {
|
|
6210
|
+
const projectId = requireProject(ctx);
|
|
6211
|
+
const items = await api(ctx, `/v1/projects/${projectId}/neon-branches`);
|
|
6212
|
+
ok(items, () => {
|
|
6213
|
+
if (!items?.length) return line(import_picocolors8.default.dim("No branches."));
|
|
6214
|
+
for (const b of items) {
|
|
6215
|
+
const mark = b.isProjectBranch ? import_picocolors8.default.green(" \u2190 the app runs on this") : "";
|
|
6216
|
+
line(
|
|
6217
|
+
`${import_picocolors8.default.bold(b.name ?? "?")} ${import_picocolors8.default.dim(b.id ?? "")}${b.parent_id ? import_picocolors8.default.dim(" from " + b.parent_id) : ""}${mark}`
|
|
6218
|
+
);
|
|
6219
|
+
}
|
|
6220
|
+
});
|
|
6221
|
+
})
|
|
6222
|
+
);
|
|
6223
|
+
branch.command("create <name>").description("Make a copy of the data to work on").option("--from <branchId>", "copy this branch instead of the one in use").option(
|
|
6224
|
+
"--no-compute",
|
|
6225
|
+
"create it without compute \u2014 cheaper, and nothing can connect to it"
|
|
6226
|
+
).action(
|
|
6227
|
+
action(async ({ ctx, args, opts }) => {
|
|
6228
|
+
const projectId = requireProject(ctx);
|
|
6229
|
+
const noCompute = opts.compute === false;
|
|
6230
|
+
const res = await api(ctx, `/v1/projects/${projectId}/neon-branches`, {
|
|
6231
|
+
body: {
|
|
6232
|
+
name: args[0],
|
|
6233
|
+
...opts.from ? { fromBranchId: String(opts.from) } : {},
|
|
6234
|
+
// Commander turns `--no-compute` into `compute: false`. The typed
|
|
6235
|
+
// opts bag is loose here, so the comparison is written against the
|
|
6236
|
+
// value rather than the declared type.
|
|
6237
|
+
...noCompute ? { withEndpoint: false } : {}
|
|
6238
|
+
}
|
|
6239
|
+
});
|
|
6240
|
+
ok(res, () => {
|
|
6241
|
+
success(`Created branch ${args[0]}.`);
|
|
6242
|
+
const id = res?.branch?.id ?? res?.id;
|
|
6243
|
+
if (id) line(import_picocolors8.default.dim(id));
|
|
6244
|
+
if (noCompute) {
|
|
6245
|
+
line(
|
|
6246
|
+
import_picocolors8.default.dim(
|
|
6247
|
+
"It has no compute, so nothing can connect to it until one is added."
|
|
6248
|
+
)
|
|
6249
|
+
);
|
|
6250
|
+
}
|
|
6251
|
+
});
|
|
6252
|
+
})
|
|
6253
|
+
);
|
|
6254
|
+
branch.command("reset <branchId>").description("Throw away a branch's changes and take its source data again (asks for approval)").action(
|
|
6255
|
+
action(async ({ ctx, args }) => {
|
|
6256
|
+
const projectId = requireProject(ctx);
|
|
6257
|
+
const res = await api(
|
|
6258
|
+
ctx,
|
|
6259
|
+
`/v1/projects/${projectId}/neon-branches/${encodeURIComponent(args[0])}/reset`,
|
|
6260
|
+
{ body: {} }
|
|
6261
|
+
);
|
|
6262
|
+
ok(res, () => success(`Reset ${args[0]} to its source.`));
|
|
6263
|
+
})
|
|
6264
|
+
);
|
|
6265
|
+
branch.command("rm <branchId>").description("Delete a branch and its data (asks for approval)").action(
|
|
6266
|
+
action(async ({ ctx, args }) => {
|
|
6267
|
+
const projectId = requireProject(ctx);
|
|
6268
|
+
const res = await api(
|
|
6269
|
+
ctx,
|
|
6270
|
+
`/v1/projects/${projectId}/neon-branches/${encodeURIComponent(args[0])}`,
|
|
6271
|
+
{ method: "DELETE" }
|
|
6272
|
+
);
|
|
6273
|
+
ok(res, () => success(`Deleted branch ${args[0]}.`));
|
|
6274
|
+
})
|
|
6275
|
+
);
|
|
6276
|
+
const database = neon.command("database").description("Databases on a branch");
|
|
6277
|
+
database.command("list").description("Databases on a branch (default: the one the app uses)").option("--branch <branchId>", "look at another branch").action(
|
|
6278
|
+
action(async ({ ctx, opts }) => {
|
|
6279
|
+
const projectId = requireProject(ctx);
|
|
6280
|
+
const items = await api(ctx, `/v1/projects/${projectId}/neon-databases`, {
|
|
6281
|
+
query: opts.branch ? { branchId: String(opts.branch) } : void 0
|
|
6282
|
+
});
|
|
6283
|
+
ok(items, () => {
|
|
6284
|
+
if (!items?.length) return line(import_picocolors8.default.dim("No databases."));
|
|
6285
|
+
for (const d of items) {
|
|
6286
|
+
const mark = d.isProjectDatabase ? import_picocolors8.default.green(" \u2190 the app connects to this") : "";
|
|
6287
|
+
line(`${import_picocolors8.default.bold(d.name ?? "?")}${import_picocolors8.default.dim(" owner " + (d.owner_name ?? "?"))}${mark}`);
|
|
6288
|
+
}
|
|
6289
|
+
});
|
|
6290
|
+
})
|
|
6291
|
+
);
|
|
6292
|
+
database.command("create <name>").description("Create a database on a branch").option("--branch <branchId>", "which branch (default: the one the app uses)").option("--owner <role>", "which role owns it (default: the branch's own owner)").action(
|
|
6293
|
+
action(async ({ ctx, args, opts }) => {
|
|
6294
|
+
const projectId = requireProject(ctx);
|
|
6295
|
+
const res = await api(ctx, `/v1/projects/${projectId}/neon-databases`, {
|
|
6296
|
+
body: {
|
|
6297
|
+
name: args[0],
|
|
6298
|
+
...opts.branch ? { branchId: String(opts.branch) } : {},
|
|
6299
|
+
...opts.owner ? { ownerName: String(opts.owner) } : {}
|
|
6300
|
+
}
|
|
6301
|
+
});
|
|
6302
|
+
ok(res, () => success(`Created database ${args[0]}.`));
|
|
6303
|
+
})
|
|
6304
|
+
);
|
|
6305
|
+
database.command("rm <name>").description("Delete a database and every table in it (asks for approval)").option("--branch <branchId>", "which branch (default: the one the app uses)").action(
|
|
6306
|
+
action(async ({ ctx, args, opts }) => {
|
|
6307
|
+
const projectId = requireProject(ctx);
|
|
6308
|
+
const res = await api(
|
|
6309
|
+
ctx,
|
|
6310
|
+
`/v1/projects/${projectId}/neon-databases/${encodeURIComponent(args[0])}`,
|
|
6311
|
+
{
|
|
6312
|
+
method: "DELETE",
|
|
6313
|
+
query: opts.branch ? { branchId: String(opts.branch) } : void 0
|
|
6314
|
+
}
|
|
6315
|
+
);
|
|
6316
|
+
ok(res, () => success(`Deleted database ${args[0]}.`));
|
|
6317
|
+
})
|
|
6318
|
+
);
|
|
6319
|
+
neon.command("endpoints").description("The compute that runs the database \u2014 what is on, and what it costs idle").action(
|
|
6320
|
+
action(async ({ ctx }) => {
|
|
6321
|
+
const projectId = requireProject(ctx);
|
|
6322
|
+
const items = await api(ctx, `/v1/projects/${projectId}/neon-endpoints`);
|
|
6323
|
+
ok(items, () => {
|
|
6324
|
+
if (!items?.length) {
|
|
6325
|
+
return line(
|
|
6326
|
+
import_picocolors8.default.dim("No compute endpoints \u2014 nothing can connect to this database.")
|
|
6327
|
+
);
|
|
6328
|
+
}
|
|
6329
|
+
for (const e of items) {
|
|
6330
|
+
const state = e.current_state === "active" ? import_picocolors8.default.green("active") : import_picocolors8.default.dim(e.current_state ?? "?");
|
|
6331
|
+
line(
|
|
6332
|
+
`${import_picocolors8.default.bold(e.type ?? "endpoint")} ${state} ${import_picocolors8.default.dim(e.branch_id ?? "")}` + (e.host ? " " + import_picocolors8.default.cyan(e.host) : "")
|
|
6333
|
+
);
|
|
6334
|
+
}
|
|
6335
|
+
});
|
|
6336
|
+
})
|
|
6337
|
+
);
|
|
5560
6338
|
}
|
|
5561
6339
|
async function presign(ctx, projectId, bucket, key, operation, expiresInSeconds) {
|
|
5562
6340
|
const res = await api(
|
|
@@ -5577,50 +6355,170 @@ async function presign(ctx, projectId, bucket, key, operation, expiresInSeconds)
|
|
|
5577
6355
|
// src/commands/env.ts
|
|
5578
6356
|
var import_picocolors9 = __toESM(require_picocolors(), 1);
|
|
5579
6357
|
import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
6358
|
+
|
|
6359
|
+
// src/environments.ts
|
|
6360
|
+
var ENVIRONMENTS = ["development", "preview", "production"];
|
|
6361
|
+
var DEPLOY_ENVIRONMENTS = ["preview", "production"];
|
|
6362
|
+
var ALIASES2 = {
|
|
6363
|
+
dev: "development",
|
|
6364
|
+
development: "development",
|
|
6365
|
+
local: "development",
|
|
6366
|
+
preview: "preview",
|
|
6367
|
+
staging: "preview",
|
|
6368
|
+
stage: "preview",
|
|
6369
|
+
test: "preview",
|
|
6370
|
+
prod: "production",
|
|
6371
|
+
production: "production",
|
|
6372
|
+
live: "production",
|
|
6373
|
+
main: "production"
|
|
6374
|
+
};
|
|
6375
|
+
function parseEnvironment(raw) {
|
|
6376
|
+
if (raw === void 0 || raw === null || raw === "") return { ok: true };
|
|
6377
|
+
const text = String(raw).trim().toLowerCase();
|
|
6378
|
+
const value = ALIASES2[text];
|
|
6379
|
+
if (!value) {
|
|
6380
|
+
return {
|
|
6381
|
+
ok: false,
|
|
6382
|
+
error: `--env takes ${ENVIRONMENTS.join(", ")} (or dev/prod). "${raw}" is not one of them.`
|
|
6383
|
+
};
|
|
6384
|
+
}
|
|
6385
|
+
return { ok: true, value };
|
|
6386
|
+
}
|
|
6387
|
+
function parseDeployEnvironment(raw, verb) {
|
|
6388
|
+
const parsed = parseEnvironment(raw);
|
|
6389
|
+
if (!parsed.ok || !parsed.value) return parsed;
|
|
6390
|
+
if (parsed.value === "development") {
|
|
6391
|
+
return {
|
|
6392
|
+
ok: false,
|
|
6393
|
+
error: `Nothing is ever deployed to development \u2014 it is the environment your app uses when it runs on this computer. \`${verb}\` can address preview or production.`
|
|
6394
|
+
};
|
|
6395
|
+
}
|
|
6396
|
+
return parsed;
|
|
6397
|
+
}
|
|
6398
|
+
function envTargets(environment) {
|
|
6399
|
+
if (!environment) return [...ENVIRONMENTS];
|
|
6400
|
+
if (environment === "preview") return ["preview", "development"];
|
|
6401
|
+
return [environment];
|
|
6402
|
+
}
|
|
6403
|
+
function targetSummary(environment) {
|
|
6404
|
+
if (!environment) return "in every environment";
|
|
6405
|
+
if (environment === "preview") return "in preview and development";
|
|
6406
|
+
return `in ${environment}`;
|
|
6407
|
+
}
|
|
6408
|
+
function urlRows(apps) {
|
|
6409
|
+
const rows = [];
|
|
6410
|
+
for (const app of apps) {
|
|
6411
|
+
const appId = typeof app?.id === "string" ? app.id : "";
|
|
6412
|
+
if (!appId) continue;
|
|
6413
|
+
const appName = (app.name ?? "").trim() || "Untitled app";
|
|
6414
|
+
for (const environment of DEPLOY_ENVIRONMENTS) {
|
|
6415
|
+
const url = environment === "production" ? app.productionUrl : app.previewUrl;
|
|
6416
|
+
rows.push({
|
|
6417
|
+
appId,
|
|
6418
|
+
appName,
|
|
6419
|
+
environment,
|
|
6420
|
+
url: url && url.trim() ? url.trim() : null,
|
|
6421
|
+
note: url && url.trim() ? null : noteFor(environment)
|
|
6422
|
+
});
|
|
6423
|
+
}
|
|
6424
|
+
}
|
|
6425
|
+
return rows;
|
|
6426
|
+
}
|
|
6427
|
+
function noteFor(environment) {
|
|
6428
|
+
return environment === "production" ? "not live yet \u2014 `workser deploy --env production` publishes it" : "no preview yet \u2014 `workser deploy` builds one";
|
|
6429
|
+
}
|
|
6430
|
+
function urlsSummary(rows) {
|
|
6431
|
+
if (!rows.length) return "This project has no apps yet.";
|
|
6432
|
+
const live = rows.filter((r) => r.environment === "production" && r.url).length;
|
|
6433
|
+
const apps = new Set(rows.map((r) => r.appId)).size;
|
|
6434
|
+
if (!live) {
|
|
6435
|
+
return `${apps} ${apps === 1 ? "app" : "apps"}, none live yet.`;
|
|
6436
|
+
}
|
|
6437
|
+
return `${apps} ${apps === 1 ? "app" : "apps"} \u2014 ${live} live.`;
|
|
6438
|
+
}
|
|
6439
|
+
|
|
6440
|
+
// src/commands/env.ts
|
|
6441
|
+
function appQuery(opts, environment) {
|
|
6442
|
+
const params = new URLSearchParams();
|
|
6443
|
+
if (typeof opts?.app === "string" && opts.app) params.set("webAppId", opts.app);
|
|
6444
|
+
if (environment) params.set("environment", environment);
|
|
6445
|
+
const query = params.toString();
|
|
6446
|
+
return query ? `?${query}` : "";
|
|
5583
6447
|
}
|
|
5584
6448
|
var APP_FLAG_HELP = "Which web app to target (defaults to the project's primary app)";
|
|
6449
|
+
var ENV_FLAG_HELP = "Which environment: production, preview or development (default: all three)";
|
|
6450
|
+
function readEnv(opts) {
|
|
6451
|
+
const parsed = parseEnvironment(opts?.env);
|
|
6452
|
+
if (!parsed.ok) throw new WorkserError(parsed.error, { code: "bad_input" });
|
|
6453
|
+
return parsed.value;
|
|
6454
|
+
}
|
|
5585
6455
|
function registerEnv(program3) {
|
|
5586
6456
|
const env = program3.command("env").description("Manage web app environment variables");
|
|
5587
|
-
env.command("set <pairs...>").description("Set one or more KEY=VALUE variables").option("--app <webAppId>", APP_FLAG_HELP).action(
|
|
6457
|
+
env.command("set <pairs...>").description("Set one or more KEY=VALUE variables").option("--app <webAppId>", APP_FLAG_HELP).option("--env <environment>", ENV_FLAG_HELP).action(
|
|
5588
6458
|
action(async ({ ctx, args, opts }) => {
|
|
5589
6459
|
const projectId = requireProject(ctx);
|
|
6460
|
+
const environment = readEnv(opts);
|
|
5590
6461
|
const pairs = args[0].map((p) => {
|
|
5591
6462
|
const i = p.indexOf("=");
|
|
5592
6463
|
if (i < 0) throw new WorkserError(`Invalid pair "${p}". Use KEY=VALUE.`, { code: "bad_input" });
|
|
5593
|
-
return {
|
|
6464
|
+
return {
|
|
6465
|
+
key: p.slice(0, i),
|
|
6466
|
+
value: p.slice(i + 1),
|
|
6467
|
+
// Sent only when asked for. Omitted means all three, which is what
|
|
6468
|
+
// this command has always done — and quietly narrowing that default
|
|
6469
|
+
// would have un-set production for every script the day it shipped.
|
|
6470
|
+
...environment ? { target: envTargets(environment) } : {}
|
|
6471
|
+
};
|
|
5594
6472
|
});
|
|
5595
6473
|
const res = await api(ctx, `/v1/projects/${projectId}/env${appQuery(opts)}`, {
|
|
5596
6474
|
body: { vars: pairs }
|
|
5597
6475
|
});
|
|
5598
6476
|
const count = typeof res?.count === "number" ? res.count : pairs.length;
|
|
5599
6477
|
ok(res, () => {
|
|
5600
|
-
success(
|
|
6478
|
+
success(
|
|
6479
|
+
`Set ${count} variable(s) ${targetSummary(environment)}: ` + pairs.map((p) => p.key).join(", ")
|
|
6480
|
+
);
|
|
5601
6481
|
if (res?.usedDefault && res?.webAppName) {
|
|
5602
6482
|
line(import_picocolors9.default.dim(`on ${res.webAppName} (primary app) \u2014 use --app to target another`));
|
|
5603
6483
|
}
|
|
5604
6484
|
});
|
|
5605
6485
|
})
|
|
5606
6486
|
);
|
|
5607
|
-
env.command("get <key>").description("Print one variable's value (sensitive)").option("--app <webAppId>", APP_FLAG_HELP).action(
|
|
6487
|
+
env.command("get <key>").description("Print one variable's value (sensitive)").option("--app <webAppId>", APP_FLAG_HELP).option("--env <environment>", ENV_FLAG_HELP).action(
|
|
5608
6488
|
action(async ({ ctx, args, opts }) => {
|
|
5609
6489
|
const projectId = requireProject(ctx);
|
|
6490
|
+
const environment = readEnv(opts);
|
|
5610
6491
|
const res = await api(
|
|
5611
6492
|
ctx,
|
|
5612
|
-
`/v1/projects/${projectId}/env/${encodeURIComponent(args[0])}${appQuery(opts)}`
|
|
6493
|
+
`/v1/projects/${projectId}/env/${encodeURIComponent(args[0])}${appQuery(opts, environment)}`
|
|
5613
6494
|
);
|
|
5614
6495
|
ok(res, () => line(res.value ?? ""));
|
|
5615
6496
|
})
|
|
5616
6497
|
);
|
|
5617
|
-
env.command("list").description("List variable keys (values masked)").option("--app <webAppId>", APP_FLAG_HELP).action(
|
|
6498
|
+
env.command("list").description("List variable keys (values masked)").option("--app <webAppId>", APP_FLAG_HELP).option("--env <environment>", ENV_FLAG_HELP).action(
|
|
5618
6499
|
action(async ({ ctx, opts }) => {
|
|
5619
6500
|
const projectId = requireProject(ctx);
|
|
5620
|
-
const
|
|
6501
|
+
const environment = readEnv(opts);
|
|
6502
|
+
const items = await api(
|
|
6503
|
+
ctx,
|
|
6504
|
+
`/v1/projects/${projectId}/env${appQuery(opts, environment)}`
|
|
6505
|
+
);
|
|
5621
6506
|
ok(items, () => {
|
|
5622
|
-
if (!items?.length)
|
|
5623
|
-
|
|
6507
|
+
if (!items?.length) {
|
|
6508
|
+
return line(
|
|
6509
|
+
import_picocolors9.default.dim(
|
|
6510
|
+
environment ? `No variables apply in ${environment}.` : "No variables set."
|
|
6511
|
+
)
|
|
6512
|
+
);
|
|
6513
|
+
}
|
|
6514
|
+
for (const v of items) {
|
|
6515
|
+
const differs = !environment && v.overriddenIn?.length ? import_picocolors9.default.yellow(` (different in ${v.overriddenIn.join(", ")})`) : "";
|
|
6516
|
+
line(`${v.key}${import_picocolors9.default.dim(" = " + (v.masked ?? "\u2022\u2022\u2022\u2022"))}${differs}`);
|
|
6517
|
+
}
|
|
6518
|
+
if (environment) {
|
|
6519
|
+
line(import_picocolors9.default.dim(`
|
|
6520
|
+
Showing the values that apply in ${environment}.`));
|
|
6521
|
+
}
|
|
5624
6522
|
});
|
|
5625
6523
|
})
|
|
5626
6524
|
);
|
|
@@ -5633,12 +6531,17 @@ function registerEnv(program3) {
|
|
|
5633
6531
|
})
|
|
5634
6532
|
)
|
|
5635
6533
|
);
|
|
5636
|
-
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").
|
|
6534
|
+
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("--env <environment>", ENV_FLAG_HELP).option("--out <file>", "Local file to write", ".env.local").option(
|
|
6535
|
+
"--overwrite",
|
|
6536
|
+
"replace values this computer already has (default: fill in only what is missing)",
|
|
6537
|
+
false
|
|
6538
|
+
).action(
|
|
5637
6539
|
action(async ({ ctx, opts }) => {
|
|
5638
6540
|
const projectId = requireProject(ctx);
|
|
6541
|
+
const environment = readEnv(opts);
|
|
5639
6542
|
const items = await api(
|
|
5640
6543
|
ctx,
|
|
5641
|
-
`/v1/projects/${projectId}/env${appQuery(opts)}`
|
|
6544
|
+
`/v1/projects/${projectId}/env${appQuery(opts, environment)}`
|
|
5642
6545
|
);
|
|
5643
6546
|
const outPath = typeof opts?.out === "string" ? opts.out : ".env.local";
|
|
5644
6547
|
if (!items?.length) {
|
|
@@ -5651,38 +6554,66 @@ function registerEnv(program3) {
|
|
|
5651
6554
|
for (const item of items) {
|
|
5652
6555
|
const res = await api(
|
|
5653
6556
|
ctx,
|
|
5654
|
-
`/v1/projects/${projectId}/env/${encodeURIComponent(item.key)}${appQuery(opts)}`
|
|
6557
|
+
`/v1/projects/${projectId}/env/${encodeURIComponent(item.key)}${appQuery(opts, environment)}`
|
|
5655
6558
|
);
|
|
5656
6559
|
pulled.push({ key: item.key, value: res?.value ?? "" });
|
|
5657
6560
|
}
|
|
5658
|
-
await mergeEnvFile(outPath, pulled
|
|
6561
|
+
const result = await mergeEnvFile(outPath, pulled, {
|
|
6562
|
+
overwrite: !!opts.overwrite
|
|
6563
|
+
});
|
|
5659
6564
|
ok(
|
|
5660
|
-
{
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
6565
|
+
{
|
|
6566
|
+
file: outPath,
|
|
6567
|
+
pulled: result.written,
|
|
6568
|
+
skipped: result.skipped,
|
|
6569
|
+
overwrite: !!opts.overwrite
|
|
6570
|
+
},
|
|
6571
|
+
() => {
|
|
6572
|
+
success(
|
|
6573
|
+
`Pulled ${result.written.length} variable(s) into ${import_picocolors9.default.bold(outPath)}.`
|
|
6574
|
+
);
|
|
6575
|
+
if (result.skipped.length) {
|
|
6576
|
+
line(
|
|
6577
|
+
import_picocolors9.default.dim(
|
|
6578
|
+
`Left alone (this computer already has ${result.skipped.length === 1 ? "it" : "them"}): ${result.skipped.join(", ")}`
|
|
6579
|
+
)
|
|
6580
|
+
);
|
|
6581
|
+
line(import_picocolors9.default.dim("Use --overwrite to replace them."));
|
|
6582
|
+
}
|
|
6583
|
+
}
|
|
5664
6584
|
);
|
|
5665
6585
|
})
|
|
5666
6586
|
);
|
|
5667
6587
|
}
|
|
5668
6588
|
var ENV_KEY_LINE = /^([A-Za-z_][A-Za-z0-9_]*)=/;
|
|
5669
|
-
async function mergeEnvFile(path, vars) {
|
|
6589
|
+
async function mergeEnvFile(path, vars, opts = {}) {
|
|
5670
6590
|
const existing = await readFile3(path, "utf8").catch(() => "");
|
|
5671
6591
|
const lines = existing.length ? existing.split(/\r?\n/) : [];
|
|
5672
6592
|
const remaining = new Map(vars.map((v) => [v.key, v.value]));
|
|
6593
|
+
const skipped = [];
|
|
6594
|
+
const replaced = [];
|
|
5673
6595
|
const merged = lines.map((rawLine) => {
|
|
5674
6596
|
const match = ENV_KEY_LINE.exec(rawLine);
|
|
5675
6597
|
if (!match || !remaining.has(match[1])) return rawLine;
|
|
5676
6598
|
const key = match[1];
|
|
6599
|
+
if (!opts.overwrite) {
|
|
6600
|
+
remaining.delete(key);
|
|
6601
|
+
skipped.push(key);
|
|
6602
|
+
return rawLine;
|
|
6603
|
+
}
|
|
5677
6604
|
const value = remaining.get(key);
|
|
5678
6605
|
remaining.delete(key);
|
|
6606
|
+
replaced.push(key);
|
|
5679
6607
|
return `${key}=${formatEnvValue(value)}`;
|
|
5680
6608
|
});
|
|
5681
6609
|
while (merged.length && merged[merged.length - 1] === "") merged.pop();
|
|
6610
|
+
const written = [...replaced];
|
|
5682
6611
|
for (const [key, value] of remaining) {
|
|
5683
6612
|
merged.push(`${key}=${formatEnvValue(value)}`);
|
|
6613
|
+
written.push(key);
|
|
5684
6614
|
}
|
|
5685
6615
|
await writeFile3(path, merged.join("\n") + "\n", "utf8");
|
|
6616
|
+
return { written, skipped };
|
|
5686
6617
|
}
|
|
5687
6618
|
function formatEnvValue(value) {
|
|
5688
6619
|
return /[\s"'#]/.test(value) ? JSON.stringify(value) : value;
|
|
@@ -5692,16 +6623,30 @@ function formatEnvValue(value) {
|
|
|
5692
6623
|
var import_picocolors10 = __toESM(require_picocolors(), 1);
|
|
5693
6624
|
var TERMINAL = /* @__PURE__ */ new Set(["ready", "live", "success", "error", "failed", "canceled"]);
|
|
5694
6625
|
function registerDeploy(program3) {
|
|
5695
|
-
const deploy = program3.command("deploy").description("Deploy the current project to Workser (git \u2192 Vercel) and return a live URL").option("--prod", "deploy to production
|
|
6626
|
+
const deploy = program3.command("deploy").description("Deploy the current project to Workser (git \u2192 Vercel) and return a live URL").option("--prod", "deploy to production (same as --env production)", false).option(
|
|
6627
|
+
"--env <environment>",
|
|
6628
|
+
"which environment to deploy: preview (default) or production"
|
|
6629
|
+
).option("--watch", "wait for the deploy to finish, streaming status", false).option(
|
|
5696
6630
|
"--app <webAppId>",
|
|
5697
6631
|
"which app to publish (default: the app this folder is linked to)"
|
|
5698
6632
|
).action(
|
|
5699
6633
|
action(async ({ ctx, opts }) => {
|
|
5700
6634
|
requireLocalApp(ctx, "deploy");
|
|
5701
6635
|
const projectId = requireProject(ctx);
|
|
6636
|
+
const parsedEnv = parseDeployEnvironment(opts.env, "deploy");
|
|
6637
|
+
if (!parsedEnv.ok) {
|
|
6638
|
+
throw new WorkserError(parsedEnv.error, { code: "bad_input" });
|
|
6639
|
+
}
|
|
6640
|
+
if (opts.prod && parsedEnv.value === "preview") {
|
|
6641
|
+
throw new WorkserError(
|
|
6642
|
+
"`--prod` and `--env preview` ask for different things. Pass one.",
|
|
6643
|
+
{ code: "bad_input" }
|
|
6644
|
+
);
|
|
6645
|
+
}
|
|
6646
|
+
const prod = Boolean(opts.prod) || parsedEnv.value === "production";
|
|
5702
6647
|
const dep = await api(ctx, `/v1/projects/${projectId}/deploy`, {
|
|
5703
6648
|
body: {
|
|
5704
|
-
prod
|
|
6649
|
+
prod,
|
|
5705
6650
|
cwd: ctx.cwd,
|
|
5706
6651
|
...opts.app ? { webAppId: opts.app } : {}
|
|
5707
6652
|
}
|
|
@@ -5743,10 +6688,22 @@ function printDeploy(dep) {
|
|
|
5743
6688
|
// src/commands/versions.ts
|
|
5744
6689
|
var import_picocolors11 = __toESM(require_picocolors(), 1);
|
|
5745
6690
|
function registerVersions(program3) {
|
|
5746
|
-
program3.command("versions").description("List the Workser-managed versions of the project (deploy history)").
|
|
5747
|
-
|
|
6691
|
+
program3.command("versions").description("List the Workser-managed versions of the project (deploy history)").option("--app <webAppId>", "which app (defaults to the primary app)").option(
|
|
6692
|
+
"--env <environment>",
|
|
6693
|
+
"which environment `deployed` should mean: preview or production"
|
|
6694
|
+
).action(
|
|
6695
|
+
action(async ({ ctx, opts }) => {
|
|
5748
6696
|
const projectId = requireProject(ctx);
|
|
5749
|
-
const
|
|
6697
|
+
const parsed = parseDeployEnvironment(opts.env, "versions");
|
|
6698
|
+
if (!parsed.ok) {
|
|
6699
|
+
throw new WorkserError(parsed.error, { code: "bad_input" });
|
|
6700
|
+
}
|
|
6701
|
+
const items = await api(ctx, `/v1/projects/${projectId}/versions`, {
|
|
6702
|
+
query: {
|
|
6703
|
+
...opts.app ? { webAppId: String(opts.app) } : {},
|
|
6704
|
+
...parsed.value ? { environment: parsed.value } : {}
|
|
6705
|
+
}
|
|
6706
|
+
});
|
|
5750
6707
|
ok(items, () => {
|
|
5751
6708
|
if (!items?.length) return line(import_picocolors11.default.dim("No versions yet. `workser deploy` to create one."));
|
|
5752
6709
|
for (const v of items) {
|
|
@@ -5760,7 +6717,7 @@ function formatVersion(v) {
|
|
|
5760
6717
|
const ref = import_picocolors11.default.yellow(shortRef(v.ref));
|
|
5761
6718
|
const when = import_picocolors11.default.dim(formatTime(v.createdAt));
|
|
5762
6719
|
const msg = (v.message ?? "").trim() || import_picocolors11.default.dim("(no message)");
|
|
5763
|
-
const badge = v.deployed ? " " + import_picocolors11.default.green("deployed") : "";
|
|
6720
|
+
const badge = v.deployed ? " " + import_picocolors11.default.green(v.deployedEnvironment ? `live in ${v.deployedEnvironment}` : "deployed") : "";
|
|
5764
6721
|
const url = v.url ? " " + import_picocolors11.default.cyan(v.url) : "";
|
|
5765
6722
|
return `${ref} ${when} ${msg}${badge}${url}`;
|
|
5766
6723
|
}
|
|
@@ -5775,19 +6732,30 @@ function formatTime(t) {
|
|
|
5775
6732
|
}
|
|
5776
6733
|
|
|
5777
6734
|
// src/commands/logs.ts
|
|
6735
|
+
var import_picocolors12 = __toESM(require_picocolors(), 1);
|
|
5778
6736
|
function registerLogs(program3) {
|
|
5779
|
-
program3.command("logs").description("Show
|
|
6737
|
+
program3.command("logs").description("Show the build logs of the latest deployment").option("-n, --lines <n>", "number of lines", "100").option("-f, --follow", "keep streaming new logs", false).option("--app <webAppId>", "which app (defaults to the primary app)").option("--env <environment>", "preview or production (default: whichever deployed last)").action(
|
|
5780
6738
|
action(async ({ ctx, opts }) => {
|
|
5781
6739
|
const projectId = requireProject(ctx);
|
|
6740
|
+
const parsed = parseDeployEnvironment(opts.env, "logs");
|
|
6741
|
+
if (!parsed.ok) {
|
|
6742
|
+
throw new WorkserError(parsed.error, { code: "bad_input" });
|
|
6743
|
+
}
|
|
6744
|
+
const scope = {
|
|
6745
|
+
...opts.app ? { webAppId: String(opts.app) } : {},
|
|
6746
|
+
...parsed.value ? { environment: parsed.value } : {}
|
|
6747
|
+
};
|
|
5782
6748
|
const first = await api(ctx, `/v1/projects/${projectId}/logs`, {
|
|
5783
|
-
query: { lines: opts.lines }
|
|
6749
|
+
query: { lines: opts.lines, ...scope }
|
|
5784
6750
|
});
|
|
5785
|
-
ok(first, () => (first
|
|
6751
|
+
ok(first, () => print(first));
|
|
5786
6752
|
if (opts.follow && !isJson()) {
|
|
5787
6753
|
let cursor = first.cursor;
|
|
5788
6754
|
for (; ; ) {
|
|
5789
6755
|
await sleep(2e3);
|
|
5790
|
-
const next = await api(ctx, `/v1/projects/${projectId}/logs`, {
|
|
6756
|
+
const next = await api(ctx, `/v1/projects/${projectId}/logs`, {
|
|
6757
|
+
query: { after: cursor, ...scope }
|
|
6758
|
+
});
|
|
5791
6759
|
(next.entries ?? []).forEach((e) => line(formatLog(e)));
|
|
5792
6760
|
if (next.cursor) cursor = next.cursor;
|
|
5793
6761
|
}
|
|
@@ -5795,31 +6763,112 @@ function registerLogs(program3) {
|
|
|
5795
6763
|
})
|
|
5796
6764
|
);
|
|
5797
6765
|
}
|
|
6766
|
+
function print(res) {
|
|
6767
|
+
const entries = res?.entries ?? [];
|
|
6768
|
+
for (const e of entries) line(formatLog(e));
|
|
6769
|
+
if (!entries.length && res?.note) line(import_picocolors12.default.dim(res.note));
|
|
6770
|
+
if (entries.length && res?.deploymentId) {
|
|
6771
|
+
const where = res.environment ? ` (${res.environment})` : "";
|
|
6772
|
+
line(import_picocolors12.default.dim(`\u2014 build ${res.deploymentId}${where}`));
|
|
6773
|
+
}
|
|
6774
|
+
}
|
|
5798
6775
|
function formatLog(e) {
|
|
5799
6776
|
if (typeof e === "string") return e;
|
|
5800
6777
|
return `${e.ts ?? ""} ${e.level ? `[${e.level}] ` : ""}${e.message ?? ""}`.trim();
|
|
5801
6778
|
}
|
|
5802
6779
|
|
|
5803
6780
|
// src/commands/domain.ts
|
|
5804
|
-
var
|
|
6781
|
+
var import_picocolors13 = __toESM(require_picocolors(), 1);
|
|
6782
|
+
|
|
6783
|
+
// src/reserved-domains.ts
|
|
6784
|
+
var RESERVED_APEX = [
|
|
6785
|
+
"workser.ai",
|
|
6786
|
+
"workser.app",
|
|
6787
|
+
"workser.dev"
|
|
6788
|
+
];
|
|
6789
|
+
var PROVIDER_HOSTS = ["vercel.app", "neon.tech", "expo.dev"];
|
|
6790
|
+
function normaliseDomain(input) {
|
|
6791
|
+
return input.trim().toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/:\d+$/, "").replace(/\.$/, "");
|
|
6792
|
+
}
|
|
6793
|
+
function isUnder(host, suffix) {
|
|
6794
|
+
return host === suffix || host.endsWith(`.${suffix}`);
|
|
6795
|
+
}
|
|
6796
|
+
function checkDomain(input) {
|
|
6797
|
+
const host = normaliseDomain(input);
|
|
6798
|
+
if (!host) {
|
|
6799
|
+
return { allowed: false, reason: "No domain given." };
|
|
6800
|
+
}
|
|
6801
|
+
if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/.test(host)) {
|
|
6802
|
+
return {
|
|
6803
|
+
allowed: false,
|
|
6804
|
+
reason: `"${input}" is not a valid domain name.`
|
|
6805
|
+
};
|
|
6806
|
+
}
|
|
6807
|
+
for (const apex of RESERVED_APEX) {
|
|
6808
|
+
if (isUnder(host, apex)) {
|
|
6809
|
+
return {
|
|
6810
|
+
allowed: false,
|
|
6811
|
+
reason: `${host} belongs to Workser and cannot be attached to a project. Use a domain the customer owns. Workser's own preview and live addresses are assigned automatically \u2014 there is nothing to attach.`
|
|
6812
|
+
};
|
|
6813
|
+
}
|
|
6814
|
+
}
|
|
6815
|
+
for (const provider of PROVIDER_HOSTS) {
|
|
6816
|
+
if (isUnder(host, provider)) {
|
|
6817
|
+
return {
|
|
6818
|
+
allowed: false,
|
|
6819
|
+
reason: `${host} is assigned by the hosting provider, not attached as a custom domain. If you were trying to find where this app is already served, read its URLs instead of adding a domain.`
|
|
6820
|
+
};
|
|
6821
|
+
}
|
|
6822
|
+
}
|
|
6823
|
+
return { allowed: true };
|
|
6824
|
+
}
|
|
6825
|
+
function assertDomainAllowed(input) {
|
|
6826
|
+
const verdict = checkDomain(input);
|
|
6827
|
+
if (!verdict.allowed) throw new Error(verdict.reason);
|
|
6828
|
+
return normaliseDomain(input);
|
|
6829
|
+
}
|
|
6830
|
+
|
|
6831
|
+
// src/commands/domain.ts
|
|
5805
6832
|
function registerDomain(program3) {
|
|
5806
|
-
const domain = program3.command("domain").description("
|
|
5807
|
-
domain.command("
|
|
5808
|
-
action(
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
6833
|
+
const domain = program3.command("domain").description("Manage the project's custom domains");
|
|
6834
|
+
domain.command("add <domain>").description("Attach a custom domain or subdomain (asks the owner to confirm)").option("--app <webAppId>", "Which app to attach it to").action(
|
|
6835
|
+
action(async ({ ctx, args, opts }) => {
|
|
6836
|
+
const projectId = requireProject(ctx);
|
|
6837
|
+
const host = assertDomainAllowed(String(args[0]));
|
|
6838
|
+
const result = await api(
|
|
6839
|
+
ctx,
|
|
6840
|
+
opts.app ? `/v1/projects/${projectId}/web-apps/${opts.app}/domains` : `/v1/projects/${projectId}/domains`,
|
|
6841
|
+
{ method: "POST", body: { domain: host } }
|
|
6842
|
+
);
|
|
6843
|
+
ok(result, () => {
|
|
6844
|
+
line(`${import_picocolors13.default.green("Attached")} ${host}`);
|
|
6845
|
+
line(
|
|
6846
|
+
import_picocolors13.default.dim(
|
|
6847
|
+
"It goes live once DNS points at us. Run `workser domain list` to see its status."
|
|
6848
|
+
)
|
|
6849
|
+
);
|
|
6850
|
+
});
|
|
6851
|
+
})
|
|
6852
|
+
);
|
|
6853
|
+
domain.command("rm <domain>").description("Detach a custom domain (asks the owner to confirm)").option("--app <webAppId>", "Which app it is attached to").action(
|
|
6854
|
+
action(async ({ ctx, args, opts }) => {
|
|
6855
|
+
const projectId = requireProject(ctx);
|
|
6856
|
+
const host = String(args[0]).trim().toLowerCase();
|
|
6857
|
+
const result = await api(
|
|
6858
|
+
ctx,
|
|
6859
|
+
opts.app ? `/v1/projects/${projectId}/web-apps/${opts.app}/domains/${encodeURIComponent(host)}` : `/v1/projects/${projectId}/domains/${encodeURIComponent(host)}`,
|
|
6860
|
+
{ method: "DELETE" }
|
|
6861
|
+
);
|
|
6862
|
+
ok(result, () => line(`${import_picocolors13.default.yellow("Detached")} ${host}`));
|
|
6863
|
+
})
|
|
5815
6864
|
);
|
|
5816
6865
|
domain.command("list").description("List domains attached to the project").action(
|
|
5817
6866
|
action(async ({ ctx }) => {
|
|
5818
6867
|
const projectId = requireProject(ctx);
|
|
5819
6868
|
const items = await api(ctx, `/v1/projects/${projectId}/domains`);
|
|
5820
6869
|
ok(items, () => {
|
|
5821
|
-
if (!items?.length) return line(
|
|
5822
|
-
for (const d of items) line(`${d.domain}${
|
|
6870
|
+
if (!items?.length) return line(import_picocolors13.default.dim("No custom domains."));
|
|
6871
|
+
for (const d of items) line(`${d.domain}${import_picocolors13.default.dim(" " + (d.status ?? ""))}`);
|
|
5823
6872
|
});
|
|
5824
6873
|
})
|
|
5825
6874
|
);
|
|
@@ -5851,7 +6900,7 @@ function openUrl(url) {
|
|
|
5851
6900
|
}
|
|
5852
6901
|
|
|
5853
6902
|
// src/commands/doctor.ts
|
|
5854
|
-
var
|
|
6903
|
+
var import_picocolors14 = __toESM(require_picocolors(), 1);
|
|
5855
6904
|
import { spawnSync } from "child_process";
|
|
5856
6905
|
function registerDoctor(program3) {
|
|
5857
6906
|
program3.command("doctor").description("Print the resolved endpoint, mode, token presence (masked), and current project").action(
|
|
@@ -5863,14 +6912,14 @@ function registerDoctor(program3) {
|
|
|
5863
6912
|
const tokenSource = opts.token ? "--token" : process.env.WORKSER_TOKEN ? "$WORKSER_TOKEN" : session.token ? "session" : void 0;
|
|
5864
6913
|
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}`;
|
|
5865
6914
|
const projectSource = opts.project ? "--project" : link?.projectId ? ".workser link" : session.defaultProjectId ? "session" : void 0;
|
|
5866
|
-
const
|
|
6915
|
+
const git2 = gitVersion();
|
|
5867
6916
|
const report = {
|
|
5868
6917
|
endpoint: ctx.endpoint,
|
|
5869
6918
|
endpointSource,
|
|
5870
6919
|
env,
|
|
5871
6920
|
envIgnored,
|
|
5872
6921
|
mode: ctx.mode,
|
|
5873
|
-
git: { present:
|
|
6922
|
+
git: { present: git2 !== null, version: git2 },
|
|
5874
6923
|
token: {
|
|
5875
6924
|
present: Boolean(ctx.token),
|
|
5876
6925
|
masked: ctx.token ? maskToken(ctx.token) : null,
|
|
@@ -5885,29 +6934,29 @@ function registerDoctor(program3) {
|
|
|
5885
6934
|
workspace: session.workspaceName ?? null
|
|
5886
6935
|
};
|
|
5887
6936
|
ok(report, () => {
|
|
5888
|
-
line(
|
|
5889
|
-
line(` endpoint: ${ctx.endpoint} ${
|
|
6937
|
+
line(import_picocolors14.default.bold("workser doctor"));
|
|
6938
|
+
line(` endpoint: ${ctx.endpoint} ${import_picocolors14.default.dim(`(${endpointSource})`)}`);
|
|
5890
6939
|
line(
|
|
5891
|
-
` env: ${env === "prod" ?
|
|
6940
|
+
` env: ${env === "prod" ? import_picocolors14.default.yellow(env) : env}` + import_picocolors14.default.dim(process.env.WORKSER_ENV ? " ($WORKSER_ENV)" : " (default)")
|
|
5892
6941
|
);
|
|
5893
6942
|
line(` mode: ${ctx.mode}`);
|
|
5894
6943
|
line(
|
|
5895
|
-
` token: ${ctx.token ? `${maskToken(ctx.token)} ${
|
|
6944
|
+
` token: ${ctx.token ? `${maskToken(ctx.token)} ${import_picocolors14.default.dim(`(${tokenSource})`)}` : import_picocolors14.default.yellow("none \u2014 run `workser login`")}`
|
|
5896
6945
|
);
|
|
5897
6946
|
line(
|
|
5898
|
-
` project: ${ctx.projectId ??
|
|
6947
|
+
` project: ${ctx.projectId ?? import_picocolors14.default.dim("none")}` + (link?.name ? ` ${import_picocolors14.default.dim(`(${link.name})`)}` : "") + (projectSource ? import_picocolors14.default.dim(` [${projectSource}]`) : "")
|
|
5899
6948
|
);
|
|
5900
6949
|
line(` cwd: ${ctx.cwd}`);
|
|
5901
|
-
line(` git: ${
|
|
5902
|
-
if (!
|
|
6950
|
+
line(` git: ${git2 ?? import_picocolors14.default.dim("not on this shell's PATH")}`);
|
|
6951
|
+
if (!git2) {
|
|
5903
6952
|
line("");
|
|
5904
6953
|
line(
|
|
5905
|
-
|
|
6954
|
+
import_picocolors14.default.dim(
|
|
5906
6955
|
" Workser brings its own git, so syncing and publishing still work."
|
|
5907
6956
|
)
|
|
5908
6957
|
);
|
|
5909
6958
|
line(
|
|
5910
|
-
|
|
6959
|
+
import_picocolors14.default.dim(
|
|
5911
6960
|
` Only needed if you want to run git yourself here. ${GIT_INSTALL_HINT}`
|
|
5912
6961
|
)
|
|
5913
6962
|
);
|
|
@@ -5915,16 +6964,16 @@ function registerDoctor(program3) {
|
|
|
5915
6964
|
if (envIgnored) {
|
|
5916
6965
|
line("");
|
|
5917
6966
|
line(
|
|
5918
|
-
|
|
6967
|
+
import_picocolors14.default.yellow(
|
|
5919
6968
|
` WORKSER_ENV=${env} is not in effect \u2014 ${endpointSource} wins.`
|
|
5920
6969
|
)
|
|
5921
6970
|
);
|
|
5922
6971
|
line(
|
|
5923
|
-
|
|
6972
|
+
import_picocolors14.default.dim(
|
|
5924
6973
|
` Re-run \`workser login\` to switch (the saved token is tied to ${ctx.endpoint}),`
|
|
5925
6974
|
)
|
|
5926
6975
|
);
|
|
5927
|
-
line(
|
|
6976
|
+
line(import_picocolors14.default.dim(` or pass --endpoint ${ENV_BASE_URLS[env]}.`));
|
|
5928
6977
|
}
|
|
5929
6978
|
});
|
|
5930
6979
|
})
|
|
@@ -5954,7 +7003,7 @@ function maskToken(token) {
|
|
|
5954
7003
|
}
|
|
5955
7004
|
|
|
5956
7005
|
// src/commands/agent.ts
|
|
5957
|
-
var
|
|
7006
|
+
var import_picocolors15 = __toESM(require_picocolors(), 1);
|
|
5958
7007
|
var SPAWNABLE_AGENTS = ["claude_code", "codex", "kimi", "opencode", "grok"];
|
|
5959
7008
|
function registerAgent(program3) {
|
|
5960
7009
|
const agent = program3.command("agent").description("Delegate focused subtasks to your configured agent roles (each runs isolated)");
|
|
@@ -5962,20 +7011,20 @@ function registerAgent(program3) {
|
|
|
5962
7011
|
action(async ({ ctx }) => {
|
|
5963
7012
|
const cfg = await api(ctx, "/v1/agents");
|
|
5964
7013
|
ok(cfg, () => {
|
|
5965
|
-
line(
|
|
5966
|
-
line(
|
|
7014
|
+
line(import_picocolors15.default.bold("main agent:") + " " + (cfg?.mainAgent ?? import_picocolors15.default.dim("none")));
|
|
7015
|
+
line(import_picocolors15.default.bold("backup agent:") + " " + (cfg?.backupAgent ?? import_picocolors15.default.dim("none")));
|
|
5967
7016
|
if (cfg?.effectiveMainAgent && cfg.effectiveMainAgent !== cfg.mainAgent) {
|
|
5968
7017
|
line(
|
|
5969
|
-
|
|
7018
|
+
import_picocolors15.default.yellow(
|
|
5970
7019
|
` \u2937 failover active: runs use ${cfg.effectiveMainAgent} (main not available)`
|
|
5971
7020
|
)
|
|
5972
7021
|
);
|
|
5973
7022
|
}
|
|
5974
7023
|
const roles = cfg?.roles ?? [];
|
|
5975
7024
|
if (!roles.length) {
|
|
5976
|
-
line(
|
|
7025
|
+
line(import_picocolors15.default.dim("No subagents configured. Add them in the Workser Orbit Agents screen."));
|
|
5977
7026
|
} else {
|
|
5978
|
-
line(
|
|
7027
|
+
line(import_picocolors15.default.bold("subagents:"));
|
|
5979
7028
|
for (const r of roles) line(" " + formatRole(r));
|
|
5980
7029
|
}
|
|
5981
7030
|
const detected = cfg?.detected ?? [];
|
|
@@ -5983,7 +7032,7 @@ function registerAgent(program3) {
|
|
|
5983
7032
|
(d) => d?.installed && d?.authed !== false
|
|
5984
7033
|
);
|
|
5985
7034
|
line(
|
|
5986
|
-
|
|
7035
|
+
import_picocolors15.default.bold("spawnable (workser agent spawn <agent>):") + " " + (spawnable.length ? spawnable.map((d) => toContractId(d.id)).join(", ") : import_picocolors15.default.dim("none connected"))
|
|
5987
7036
|
);
|
|
5988
7037
|
});
|
|
5989
7038
|
})
|
|
@@ -5997,8 +7046,8 @@ function registerAgent(program3) {
|
|
|
5997
7046
|
backupAgent: cfg?.backupAgent ?? null
|
|
5998
7047
|
},
|
|
5999
7048
|
() => {
|
|
6000
|
-
line(
|
|
6001
|
-
line(
|
|
7049
|
+
line(import_picocolors15.default.bold("main agent:") + " " + (cfg?.mainAgent ?? import_picocolors15.default.dim("none")));
|
|
7050
|
+
line(import_picocolors15.default.bold("backup agent:") + " " + (cfg?.backupAgent ?? import_picocolors15.default.dim("none")));
|
|
6002
7051
|
}
|
|
6003
7052
|
);
|
|
6004
7053
|
})
|
|
@@ -6023,7 +7072,10 @@ function registerAgent(program3) {
|
|
|
6023
7072
|
).option("--role <label>", "display label for this run (default: <agent>)").option(
|
|
6024
7073
|
"--instructions <text>",
|
|
6025
7074
|
"system prompt for this one run \u2014 this teammate's expertise/scope"
|
|
6026
|
-
).option("--model <model>", "model override for the backing CLI").option("--effort <level>", "reasoning effort, where the backing CLI supports it").
|
|
7075
|
+
).option("--model <model>", "model override for the backing CLI").option("--effort <level>", "reasoning effort, where the backing CLI supports it").option(
|
|
7076
|
+
"--read-only",
|
|
7077
|
+
"inspection only \u2014 the teammate runs in the CLI's read-only sandbox and cannot change anything"
|
|
7078
|
+
).action(
|
|
6027
7079
|
action(async ({ ctx, args, opts }) => {
|
|
6028
7080
|
const spawnAgent = args[0];
|
|
6029
7081
|
const task = args[1].join(" ");
|
|
@@ -6034,7 +7086,10 @@ function registerAgent(program3) {
|
|
|
6034
7086
|
role: opts.role,
|
|
6035
7087
|
instructions: opts.instructions,
|
|
6036
7088
|
model: opts.model,
|
|
6037
|
-
effort: opts.effort
|
|
7089
|
+
effort: opts.effort,
|
|
7090
|
+
// Enforced by the backing CLI's own sandbox, not by asking the
|
|
7091
|
+
// spawned agent to behave — see the daemon's `/agents/run`.
|
|
7092
|
+
readOnly: opts.readOnly === true
|
|
6038
7093
|
}
|
|
6039
7094
|
});
|
|
6040
7095
|
const exitCode = res?.exitCode ?? 0;
|
|
@@ -6052,21 +7107,21 @@ function toContractId(id) {
|
|
|
6052
7107
|
return id === "claude" ? "claude_code" : id;
|
|
6053
7108
|
}
|
|
6054
7109
|
function formatRole(r) {
|
|
6055
|
-
const label =
|
|
6056
|
-
const agent =
|
|
6057
|
-
const enabled = r.enabled === false ?
|
|
7110
|
+
const label = import_picocolors15.default.yellow(r.role);
|
|
7111
|
+
const agent = import_picocolors15.default.dim("\xB7 " + (r.agent ?? "?"));
|
|
7112
|
+
const enabled = r.enabled === false ? import_picocolors15.default.red("disabled") : import_picocolors15.default.green("enabled");
|
|
6058
7113
|
const runnable = r.installed && r.authed !== false;
|
|
6059
|
-
const ready = runnable ?
|
|
7114
|
+
const ready = runnable ? import_picocolors15.default.green("runnable") : import_picocolors15.default.dim("not runnable");
|
|
6060
7115
|
const extras = [];
|
|
6061
7116
|
if (r.model) extras.push(`model ${r.model}`);
|
|
6062
7117
|
if (Array.isArray(r.apps) && r.apps.length) extras.push(`apps: ${r.apps.join(",")}`);
|
|
6063
7118
|
if (Array.isArray(r.mcp) && r.mcp.length) extras.push(`mcp: ${r.mcp.length}`);
|
|
6064
|
-
const tail = extras.length ? " " +
|
|
7119
|
+
const tail = extras.length ? " " + import_picocolors15.default.dim(extras.join(" \xB7 ")) : "";
|
|
6065
7120
|
return `${label} ${agent} ${enabled} ${ready}${tail}`;
|
|
6066
7121
|
}
|
|
6067
7122
|
|
|
6068
7123
|
// src/commands/verify.ts
|
|
6069
|
-
var
|
|
7124
|
+
var import_picocolors16 = __toESM(require_picocolors(), 1);
|
|
6070
7125
|
function registerVerify(program3) {
|
|
6071
7126
|
program3.command("verify").description(
|
|
6072
7127
|
"Run the project's checks (typecheck/lint/build) \u2014 use before declaring a task done"
|
|
@@ -6088,23 +7143,23 @@ function registerVerify(program3) {
|
|
|
6088
7143
|
function printVerify(res) {
|
|
6089
7144
|
if (!res) return;
|
|
6090
7145
|
if (!res.checks?.length) {
|
|
6091
|
-
line(
|
|
7146
|
+
line(import_picocolors16.default.dim(res.note ?? "No checks detected."));
|
|
6092
7147
|
return;
|
|
6093
7148
|
}
|
|
6094
7149
|
for (const c of res.checks) {
|
|
6095
7150
|
line(
|
|
6096
|
-
` ${c.ok ?
|
|
7151
|
+
` ${c.ok ? import_picocolors16.default.green("\u2713") : import_picocolors16.default.red("\u2717")} ${c.name}${c.ok ? "" : import_picocolors16.default.dim(` (exit ${c.exitCode})`)}`
|
|
6097
7152
|
);
|
|
6098
7153
|
}
|
|
6099
7154
|
if (res.ok) success("All checks passed");
|
|
6100
7155
|
else
|
|
6101
7156
|
line(
|
|
6102
|
-
|
|
7157
|
+
import_picocolors16.default.red("Some checks failed \u2014 fix the errors above and re-run ") + import_picocolors16.default.bold("workser verify") + import_picocolors16.default.red(".")
|
|
6103
7158
|
);
|
|
6104
7159
|
}
|
|
6105
7160
|
|
|
6106
7161
|
// src/commands/checkpoint.ts
|
|
6107
|
-
var
|
|
7162
|
+
var import_picocolors17 = __toESM(require_picocolors(), 1);
|
|
6108
7163
|
function registerCheckpoint(program3) {
|
|
6109
7164
|
program3.command("checkpoint [label]").description(
|
|
6110
7165
|
"Save the current state of this folder so you can come back to it"
|
|
@@ -6121,8 +7176,8 @@ function registerCheckpoint(program3) {
|
|
|
6121
7176
|
ok(res, () => {
|
|
6122
7177
|
const p = res?.point;
|
|
6123
7178
|
success(`Saved a checkpoint${p?.label ? `: ${p.label}` : ""}`);
|
|
6124
|
-
if (p?.ref) line(
|
|
6125
|
-
line(
|
|
7179
|
+
if (p?.ref) line(import_picocolors17.default.dim(` ${p.ref.slice(0, 7)}`));
|
|
7180
|
+
line(import_picocolors17.default.dim(" Come back to it with `workser restore`."));
|
|
6126
7181
|
});
|
|
6127
7182
|
})
|
|
6128
7183
|
);
|
|
@@ -6148,12 +7203,12 @@ function registerCheckpoint(program3) {
|
|
|
6148
7203
|
);
|
|
6149
7204
|
if (res?.filesChanged) {
|
|
6150
7205
|
line(
|
|
6151
|
-
|
|
7206
|
+
import_picocolors17.default.dim(
|
|
6152
7207
|
` ${res.filesChanged} file${res.filesChanged === 1 ? "" : "s"} changed`
|
|
6153
7208
|
)
|
|
6154
7209
|
);
|
|
6155
7210
|
}
|
|
6156
|
-
line(
|
|
7211
|
+
line(import_picocolors17.default.dim(" This is reversible: `workser restore` again."));
|
|
6157
7212
|
});
|
|
6158
7213
|
})
|
|
6159
7214
|
);
|
|
@@ -6170,25 +7225,25 @@ function registerCheckpoint(program3) {
|
|
|
6170
7225
|
function printPoints(points) {
|
|
6171
7226
|
if (!points.length) {
|
|
6172
7227
|
info("No checkpoints yet for this folder.");
|
|
6173
|
-
line(
|
|
7228
|
+
line(import_picocolors17.default.dim(" Take one with `workser checkpoint`."));
|
|
6174
7229
|
return;
|
|
6175
7230
|
}
|
|
6176
|
-
line(
|
|
7231
|
+
line(import_picocolors17.default.bold("Checkpoints"));
|
|
6177
7232
|
for (const p of points) {
|
|
6178
7233
|
const when = p.at ? new Date(p.at).toLocaleString() : "";
|
|
6179
7234
|
line(
|
|
6180
|
-
` ${
|
|
7235
|
+
` ${import_picocolors17.default.dim(p.ref.slice(0, 7))} ${p.label}${when ? import_picocolors17.default.dim(` ${when}`) : ""}`
|
|
6181
7236
|
);
|
|
6182
7237
|
}
|
|
6183
7238
|
line(
|
|
6184
|
-
|
|
7239
|
+
import_picocolors17.default.dim(
|
|
6185
7240
|
"\nGo back with `workser restore <ref>`, or just `workser restore` for the newest."
|
|
6186
7241
|
)
|
|
6187
7242
|
);
|
|
6188
7243
|
}
|
|
6189
7244
|
|
|
6190
7245
|
// src/commands/sync.ts
|
|
6191
|
-
var
|
|
7246
|
+
var import_picocolors18 = __toESM(require_picocolors(), 1);
|
|
6192
7247
|
function registerSync(program3) {
|
|
6193
7248
|
program3.command("sync").description(
|
|
6194
7249
|
"Reconcile this folder with the copy Workser holds (pull, then push)"
|
|
@@ -6215,7 +7270,7 @@ function registerSync(program3) {
|
|
|
6215
7270
|
warn(res?.message ?? "Couldn't sync this folder.");
|
|
6216
7271
|
if (res?.state === "diverged") {
|
|
6217
7272
|
line(
|
|
6218
|
-
|
|
7273
|
+
import_picocolors18.default.dim(
|
|
6219
7274
|
" This folder and Workser's copy have both changed. Open Workser to resolve it."
|
|
6220
7275
|
)
|
|
6221
7276
|
);
|
|
@@ -6227,7 +7282,7 @@ function registerSync(program3) {
|
|
|
6227
7282
|
return;
|
|
6228
7283
|
}
|
|
6229
7284
|
success("Synced");
|
|
6230
|
-
if (res?.ref) line(
|
|
7285
|
+
if (res?.ref) line(import_picocolors18.default.dim(` ${String(res.ref).slice(0, 7)}`));
|
|
6231
7286
|
});
|
|
6232
7287
|
if (refused) process.exitCode = 1;
|
|
6233
7288
|
})
|
|
@@ -6235,7 +7290,7 @@ function registerSync(program3) {
|
|
|
6235
7290
|
}
|
|
6236
7291
|
|
|
6237
7292
|
// src/commands/workflow.ts
|
|
6238
|
-
var
|
|
7293
|
+
var import_picocolors19 = __toESM(require_picocolors(), 1);
|
|
6239
7294
|
function registerWorkflow(program3) {
|
|
6240
7295
|
const wf = program3.command("workflow").description("Create, run, and inspect workflow automations for the project");
|
|
6241
7296
|
wf.command("list").description("List the project's workflows").action(
|
|
@@ -6243,10 +7298,10 @@ function registerWorkflow(program3) {
|
|
|
6243
7298
|
const projectId = requireProject(ctx);
|
|
6244
7299
|
const items = await api(ctx, `/v1/projects/${projectId}/workflows`);
|
|
6245
7300
|
ok(items, () => {
|
|
6246
|
-
if (!items?.length) return line(
|
|
7301
|
+
if (!items?.length) return line(import_picocolors19.default.dim("No workflows yet. `workser workflow create`."));
|
|
6247
7302
|
for (const w of items) {
|
|
6248
|
-
const status = w.is_active ?
|
|
6249
|
-
line(`${w.id} ${
|
|
7303
|
+
const status = w.is_active ? import_picocolors19.default.green("active") : import_picocolors19.default.dim("inactive");
|
|
7304
|
+
line(`${w.id} ${import_picocolors19.default.bold(w.name ?? "Untitled")} ${status}`);
|
|
6250
7305
|
}
|
|
6251
7306
|
});
|
|
6252
7307
|
})
|
|
@@ -6258,7 +7313,7 @@ function registerWorkflow(program3) {
|
|
|
6258
7313
|
const res = await api(ctx, `/v1/projects/${projectId}/workflows`, {
|
|
6259
7314
|
body: { name: args[0], ...extra }
|
|
6260
7315
|
});
|
|
6261
|
-
ok(res, () => line(`Created workflow ${
|
|
7316
|
+
ok(res, () => line(`Created workflow ${import_picocolors19.default.bold(res.id)}.`));
|
|
6262
7317
|
})
|
|
6263
7318
|
);
|
|
6264
7319
|
wf.command("get <id>").description("Show a workflow's full definition").action(
|
|
@@ -6293,8 +7348,8 @@ function registerWorkflow(program3) {
|
|
|
6293
7348
|
action(async ({ ctx, args }) => {
|
|
6294
7349
|
const items = await api(ctx, `/v1/workflows/${args[0]}/executions`);
|
|
6295
7350
|
ok(items, () => {
|
|
6296
|
-
if (!items?.length) return line(
|
|
6297
|
-
for (const e of items) line(`${e.id} ${e.status ?? ""} ${
|
|
7351
|
+
if (!items?.length) return line(import_picocolors19.default.dim("No runs yet."));
|
|
7352
|
+
for (const e of items) line(`${e.id} ${e.status ?? ""} ${import_picocolors19.default.dim(e.started_at ?? "")}`);
|
|
6298
7353
|
});
|
|
6299
7354
|
})
|
|
6300
7355
|
);
|
|
@@ -6302,15 +7357,15 @@ function registerWorkflow(program3) {
|
|
|
6302
7357
|
action(async ({ ctx, args }) => {
|
|
6303
7358
|
const items = await api(ctx, `/v1/node-types`, { query: { q: args[0] } });
|
|
6304
7359
|
ok(items, () => {
|
|
6305
|
-
if (!items?.length) return line(
|
|
6306
|
-
for (const n of items) line(`${n.name ?? n.type} ${
|
|
7360
|
+
if (!items?.length) return line(import_picocolors19.default.dim("No matching node types."));
|
|
7361
|
+
for (const n of items) line(`${n.name ?? n.type} ${import_picocolors19.default.dim(n.category ?? "")}`);
|
|
6307
7362
|
});
|
|
6308
7363
|
})
|
|
6309
7364
|
);
|
|
6310
7365
|
}
|
|
6311
7366
|
|
|
6312
7367
|
// src/commands/app.ts
|
|
6313
|
-
var
|
|
7368
|
+
var import_picocolors20 = __toESM(require_picocolors(), 1);
|
|
6314
7369
|
function registerApp(program3) {
|
|
6315
7370
|
const appCmd = program3.command("app").description("Connect and use third-party app integrations (Gmail, Slack, Stripe, ...)");
|
|
6316
7371
|
appCmd.command("list").description("List connectable toolkits and this project's existing connections").option("--toolkit <slug>", "filter connections to one toolkit").action(
|
|
@@ -6323,8 +7378,8 @@ function registerApp(program3) {
|
|
|
6323
7378
|
ok({ catalog, connections }, () => {
|
|
6324
7379
|
const connected = new Set((connections ?? []).map((c) => c.toolkit ?? c.composio_app));
|
|
6325
7380
|
for (const t of catalog ?? []) {
|
|
6326
|
-
const status = connected.has(t.slug) ?
|
|
6327
|
-
line(`${t.slug} ${
|
|
7381
|
+
const status = connected.has(t.slug) ? import_picocolors20.default.green("connected") : import_picocolors20.default.dim("not connected");
|
|
7382
|
+
line(`${t.slug} ${import_picocolors20.default.bold(t.name ?? t.slug)} ${status}`);
|
|
6328
7383
|
}
|
|
6329
7384
|
});
|
|
6330
7385
|
})
|
|
@@ -6341,7 +7396,7 @@ function registerApp(program3) {
|
|
|
6341
7396
|
});
|
|
6342
7397
|
ok(
|
|
6343
7398
|
res,
|
|
6344
|
-
() => res.oauth_url ? line(`Open this URL to finish connecting: ${
|
|
7399
|
+
() => res.oauth_url ? line(`Open this URL to finish connecting: ${import_picocolors20.default.underline(res.oauth_url)}`) : line(`Connection ${res.connection_id} is ${res.status}.`)
|
|
6345
7400
|
);
|
|
6346
7401
|
})
|
|
6347
7402
|
);
|
|
@@ -6359,8 +7414,8 @@ function registerApp(program3) {
|
|
|
6359
7414
|
const projectId = requireProject(ctx);
|
|
6360
7415
|
const items = await api(ctx, `/v1/projects/${projectId}/integrations/${args[0]}/tools`);
|
|
6361
7416
|
ok(items, () => {
|
|
6362
|
-
if (!items?.length) return line(
|
|
6363
|
-
for (const t of items) line(`${t.slug} ${
|
|
7417
|
+
if (!items?.length) return line(import_picocolors20.default.dim("No tools found."));
|
|
7418
|
+
for (const t of items) line(`${t.slug} ${import_picocolors20.default.dim(t.description ?? "")}`);
|
|
6364
7419
|
});
|
|
6365
7420
|
})
|
|
6366
7421
|
);
|
|
@@ -6376,7 +7431,7 @@ function registerApp(program3) {
|
|
|
6376
7431
|
}
|
|
6377
7432
|
|
|
6378
7433
|
// src/commands/tool.ts
|
|
6379
|
-
var
|
|
7434
|
+
var import_picocolors21 = __toESM(require_picocolors(), 1);
|
|
6380
7435
|
function registerTool(program3) {
|
|
6381
7436
|
const tool = program3.command("tool").description(
|
|
6382
7437
|
"Computer-use tools: filesystem, shell, screenshot, input control, clipboard, notifications, basic browser"
|
|
@@ -6385,7 +7440,7 @@ function registerTool(program3) {
|
|
|
6385
7440
|
action(async ({ ctx }) => {
|
|
6386
7441
|
const tools = await api(ctx, "/v1/tool/list");
|
|
6387
7442
|
ok(tools, () => {
|
|
6388
|
-
if (!tools?.length) return line(
|
|
7443
|
+
if (!tools?.length) return line(import_picocolors21.default.dim("No tools available."));
|
|
6389
7444
|
const byCategory = /* @__PURE__ */ new Map();
|
|
6390
7445
|
for (const t of tools) {
|
|
6391
7446
|
const list = byCategory.get(t.category) ?? [];
|
|
@@ -6393,9 +7448,9 @@ function registerTool(program3) {
|
|
|
6393
7448
|
byCategory.set(t.category, list);
|
|
6394
7449
|
}
|
|
6395
7450
|
for (const [category, items] of byCategory) {
|
|
6396
|
-
line(
|
|
7451
|
+
line(import_picocolors21.default.bold(category) + ":");
|
|
6397
7452
|
for (const t of items) {
|
|
6398
|
-
line(` ${t.name} ${
|
|
7453
|
+
line(` ${t.name} ${import_picocolors21.default.dim(t.description ?? "")}`);
|
|
6399
7454
|
}
|
|
6400
7455
|
}
|
|
6401
7456
|
});
|
|
@@ -6413,7 +7468,7 @@ function registerTool(program3) {
|
|
|
6413
7468
|
}
|
|
6414
7469
|
|
|
6415
7470
|
// src/commands/memory.ts
|
|
6416
|
-
var
|
|
7471
|
+
var import_picocolors22 = __toESM(require_picocolors(), 1);
|
|
6417
7472
|
function registerMemory(program3) {
|
|
6418
7473
|
const memory = program3.command("memory").description("Durable, cross-conversation project memory (shared with cloud agents on the same project)");
|
|
6419
7474
|
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(
|
|
@@ -6437,9 +7492,9 @@ function registerMemory(program3) {
|
|
|
6437
7492
|
});
|
|
6438
7493
|
ok(res, () => {
|
|
6439
7494
|
const results = res?.results ?? res ?? [];
|
|
6440
|
-
if (!results?.length) return line(
|
|
7495
|
+
if (!results?.length) return line(import_picocolors22.default.dim("No matching memories."));
|
|
6441
7496
|
for (const r of results) {
|
|
6442
|
-
line(`${
|
|
7497
|
+
line(`${import_picocolors22.default.dim(r.id ?? "?")} ${r.memory ?? r.content ?? ""}`);
|
|
6443
7498
|
}
|
|
6444
7499
|
});
|
|
6445
7500
|
})
|
|
@@ -6456,7 +7511,7 @@ function registerMemory(program3) {
|
|
|
6456
7511
|
}
|
|
6457
7512
|
|
|
6458
7513
|
// src/commands/business.ts
|
|
6459
|
-
var
|
|
7514
|
+
var import_picocolors23 = __toESM(require_picocolors(), 1);
|
|
6460
7515
|
var RESOURCE_PATHS = {
|
|
6461
7516
|
"business-config": "business-config",
|
|
6462
7517
|
"business-settings": "business-settings",
|
|
@@ -6516,7 +7571,7 @@ function registerBusiness(program3) {
|
|
|
6516
7571
|
const projectId = requireProject(ctx);
|
|
6517
7572
|
const [resource] = args;
|
|
6518
7573
|
const res = await api(ctx, businessPath(projectId, resource), { body: JSON.parse(opts.body) });
|
|
6519
|
-
ok(res, () => line(`Created ${resource} ${
|
|
7574
|
+
ok(res, () => line(`Created ${resource} ${import_picocolors23.default.bold(res?.id ?? "")}.`));
|
|
6520
7575
|
})
|
|
6521
7576
|
);
|
|
6522
7577
|
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(
|
|
@@ -6558,9 +7613,9 @@ function businessPath(projectId, resource, subpath) {
|
|
|
6558
7613
|
}
|
|
6559
7614
|
|
|
6560
7615
|
// src/commands/artifact.ts
|
|
6561
|
-
var
|
|
7616
|
+
var import_picocolors24 = __toESM(require_picocolors(), 1);
|
|
6562
7617
|
import { existsSync as existsSync2, statSync } from "fs";
|
|
6563
|
-
import { resolve as
|
|
7618
|
+
import { resolve as resolve3, basename as basename3 } from "path";
|
|
6564
7619
|
var KINDS = [
|
|
6565
7620
|
"file",
|
|
6566
7621
|
"folder",
|
|
@@ -6575,14 +7630,28 @@ var KINDS = [
|
|
|
6575
7630
|
"audio",
|
|
6576
7631
|
"document",
|
|
6577
7632
|
"archive",
|
|
6578
|
-
"other"
|
|
7633
|
+
"other",
|
|
7634
|
+
// Deliverable shapes — see above.
|
|
7635
|
+
"report",
|
|
7636
|
+
"walkthrough",
|
|
7637
|
+
"before_after",
|
|
7638
|
+
"checks",
|
|
7639
|
+
"web_app",
|
|
7640
|
+
"service",
|
|
7641
|
+
"design"
|
|
6579
7642
|
];
|
|
6580
7643
|
function registerArtifact(program3) {
|
|
6581
7644
|
const artifact = program3.command("artifact").description("Record the files, folders and apps this task produced");
|
|
6582
7645
|
artifact.command("add [path]").description("Register a deliverable so it shows on the task").option(
|
|
6583
7646
|
"-k, --kind <kind>",
|
|
6584
7647
|
`what it is: ${KINDS.join(" | ")} (default: inferred from the path)`
|
|
6585
|
-
).option("-t, --title <title>", "display name (default: the file name)").option("-d, --description <text>", "one line on what it is / what it's for").option("-u, --url <url>", "a hosted deliverable (deployed app, public file)").
|
|
7648
|
+
).option("-t, --title <title>", "display name (default: the file name)").option("-d, --description <text>", "one line on what it is / what it's for").option("-u, --url <url>", "a hosted deliverable (deployed app, public file)").option(
|
|
7649
|
+
"--data <json>",
|
|
7650
|
+
`figures the card shows, as JSON \u2014 e.g. '{"passed":12,"total":12}'`
|
|
7651
|
+
).option(
|
|
7652
|
+
"--promote",
|
|
7653
|
+
"hand it straight to the task as one of the things the owner asked for"
|
|
7654
|
+
).action(
|
|
6586
7655
|
action(async ({ ctx, opts, args }) => {
|
|
6587
7656
|
const rawPath = args[0];
|
|
6588
7657
|
const url = opts.url;
|
|
@@ -6595,7 +7664,7 @@ function registerArtifact(program3) {
|
|
|
6595
7664
|
let absPath;
|
|
6596
7665
|
let kind = opts.kind;
|
|
6597
7666
|
if (rawPath) {
|
|
6598
|
-
absPath =
|
|
7667
|
+
absPath = resolve3(ctx.cwd, rawPath);
|
|
6599
7668
|
if (!existsSync2(absPath)) {
|
|
6600
7669
|
throw new WorkserError(
|
|
6601
7670
|
`Nothing at ${absPath}. Register the file after you've written it.`,
|
|
@@ -6612,23 +7681,64 @@ function registerArtifact(program3) {
|
|
|
6612
7681
|
{ code: "bad_request" }
|
|
6613
7682
|
);
|
|
6614
7683
|
}
|
|
7684
|
+
let data;
|
|
7685
|
+
if (opts.data) {
|
|
7686
|
+
try {
|
|
7687
|
+
const parsed = JSON.parse(String(opts.data));
|
|
7688
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
7689
|
+
throw new Error("not an object");
|
|
7690
|
+
}
|
|
7691
|
+
data = parsed;
|
|
7692
|
+
} catch (err) {
|
|
7693
|
+
throw new WorkserError(
|
|
7694
|
+
`--data must be a JSON object. ${err instanceof Error ? err.message : String(err)}`,
|
|
7695
|
+
{ code: "bad_request" }
|
|
7696
|
+
);
|
|
7697
|
+
}
|
|
7698
|
+
}
|
|
6615
7699
|
const res = await api(ctx, `/v1/runs/${runTarget(ctx)}/artifacts`, {
|
|
6616
7700
|
body: {
|
|
6617
7701
|
path: absPath,
|
|
6618
7702
|
url,
|
|
6619
7703
|
kind,
|
|
6620
7704
|
title: opts.title || (absPath ? basename3(absPath) : url),
|
|
6621
|
-
description: opts.description
|
|
7705
|
+
description: opts.description,
|
|
7706
|
+
data,
|
|
7707
|
+
promote: opts.promote ? true : void 0
|
|
6622
7708
|
}
|
|
6623
7709
|
});
|
|
6624
7710
|
ok(
|
|
6625
7711
|
res,
|
|
6626
7712
|
() => success(
|
|
6627
|
-
`Recorded ${
|
|
7713
|
+
`Recorded ${import_picocolors24.default.bold(res?.title ?? "artifact")}${res?.kind ? import_picocolors24.default.dim(` (${res.kind})`) : ""}`
|
|
6628
7714
|
)
|
|
6629
7715
|
);
|
|
6630
7716
|
})
|
|
6631
7717
|
);
|
|
7718
|
+
artifact.command("list").description("What this task's steps have produced so far").option("--task <id>", "a specific task (default: the plan this run is in)").option("--step <id>", "one step's output alone, across every attempt").option("--mine", "only what THIS step has produced").action(
|
|
7719
|
+
action(async ({ ctx, opts }) => {
|
|
7720
|
+
const stepId = opts.step || (opts.mine ? ctx.projectTaskId : void 0);
|
|
7721
|
+
if (opts.mine && !stepId) {
|
|
7722
|
+
throw new WorkserError(
|
|
7723
|
+
"--mine needs a step: this run isn't inside one (WORKSER_PROJECT_TASK_ID is unset)."
|
|
7724
|
+
);
|
|
7725
|
+
}
|
|
7726
|
+
let path;
|
|
7727
|
+
if (stepId) {
|
|
7728
|
+
path = `/v1/project-subtasks/${encodeURIComponent(stepId)}/artifacts`;
|
|
7729
|
+
} else {
|
|
7730
|
+
const taskId = opts.task || ctx.parentTaskId || ctx.projectTaskId;
|
|
7731
|
+
if (!taskId) {
|
|
7732
|
+
throw new WorkserError(
|
|
7733
|
+
"No task. This run isn't inside one, so pass --task <id> or --step <id>."
|
|
7734
|
+
);
|
|
7735
|
+
}
|
|
7736
|
+
path = `/v1/project-tasks/${encodeURIComponent(taskId)}/artifacts?scope=all`;
|
|
7737
|
+
}
|
|
7738
|
+
const rows = await api(ctx, path) ?? [];
|
|
7739
|
+
ok(rows, () => printArtifacts(rows));
|
|
7740
|
+
})
|
|
7741
|
+
);
|
|
6632
7742
|
artifact.command("run").description("Show the task/conversation this agent run is attached to").action(
|
|
6633
7743
|
action(async ({ ctx }) => {
|
|
6634
7744
|
const res = await api(ctx, `/v1/runs/${runTarget(ctx)}`);
|
|
@@ -6636,18 +7746,43 @@ function registerArtifact(program3) {
|
|
|
6636
7746
|
})
|
|
6637
7747
|
);
|
|
6638
7748
|
}
|
|
7749
|
+
function printArtifacts(rows) {
|
|
7750
|
+
if (!rows.length) {
|
|
7751
|
+
line(import_picocolors24.default.dim("Nothing produced yet."));
|
|
7752
|
+
return;
|
|
7753
|
+
}
|
|
7754
|
+
const byStep = /* @__PURE__ */ new Map();
|
|
7755
|
+
for (const r of rows) {
|
|
7756
|
+
const list = byStep.get(r.subtask_id) ?? [];
|
|
7757
|
+
list.push(r);
|
|
7758
|
+
byStep.set(r.subtask_id, list);
|
|
7759
|
+
}
|
|
7760
|
+
for (const [stepId, items] of byStep) {
|
|
7761
|
+
line(import_picocolors24.default.bold(`step ${stepId}`));
|
|
7762
|
+
for (const a of items) {
|
|
7763
|
+
const flag = a.promoted_at ? import_picocolors24.default.green(" *") : " ";
|
|
7764
|
+
const where = a.local_path || a.cloud_url || "";
|
|
7765
|
+
line(
|
|
7766
|
+
`${flag} ${import_picocolors24.default.dim(`[${a.kind}]`)} ${a.title ?? "(untitled)"}` + (where ? import_picocolors24.default.dim(` ${where}`) : "")
|
|
7767
|
+
);
|
|
7768
|
+
if (a.description) line(import_picocolors24.default.dim(` ${a.description}`));
|
|
7769
|
+
}
|
|
7770
|
+
line("");
|
|
7771
|
+
}
|
|
7772
|
+
line(import_picocolors24.default.dim("* = handed over as a deliverable; the rest is working material."));
|
|
7773
|
+
}
|
|
6639
7774
|
function printRun(run) {
|
|
6640
7775
|
if (!run) return;
|
|
6641
|
-
line(` run ${
|
|
7776
|
+
line(` run ${import_picocolors24.default.bold(run.runId)}`);
|
|
6642
7777
|
if (run.taskId) line(` task ${run.taskId}`);
|
|
6643
7778
|
if (run.conversationId) line(` chat ${run.conversationId}`);
|
|
6644
7779
|
if (run.projectId) line(` project ${run.projectId}`);
|
|
6645
|
-
if (run.cwd) line(` folder ${
|
|
7780
|
+
if (run.cwd) line(` folder ${import_picocolors24.default.dim(run.cwd)}`);
|
|
6646
7781
|
}
|
|
6647
7782
|
|
|
6648
7783
|
// src/commands/image.ts
|
|
6649
7784
|
import { writeFile as writeFile4, mkdir } from "fs/promises";
|
|
6650
|
-
import { dirname as
|
|
7785
|
+
import { dirname as dirname3, resolve as resolve4 } from "path";
|
|
6651
7786
|
function registerImage(program3) {
|
|
6652
7787
|
const image = program3.command("image").description("Generate images from a text prompt");
|
|
6653
7788
|
image.command("generate <prompt>").alias("gen").description("Generate an image and return its public URL").option(
|
|
@@ -6702,7 +7837,7 @@ function registerImage(program3) {
|
|
|
6702
7837
|
);
|
|
6703
7838
|
}
|
|
6704
7839
|
async function download(url, output) {
|
|
6705
|
-
const target =
|
|
7840
|
+
const target = resolve4(output);
|
|
6706
7841
|
const res = await fetch(url);
|
|
6707
7842
|
if (!res.ok) {
|
|
6708
7843
|
throw new WorkserError(
|
|
@@ -6710,13 +7845,13 @@ async function download(url, output) {
|
|
|
6710
7845
|
{ code: "download_failed" }
|
|
6711
7846
|
);
|
|
6712
7847
|
}
|
|
6713
|
-
await mkdir(
|
|
7848
|
+
await mkdir(dirname3(target), { recursive: true });
|
|
6714
7849
|
await writeFile4(target, Buffer.from(await res.arrayBuffer()));
|
|
6715
7850
|
return target;
|
|
6716
7851
|
}
|
|
6717
7852
|
|
|
6718
7853
|
// src/commands/ask.ts
|
|
6719
|
-
var
|
|
7854
|
+
var import_picocolors25 = __toESM(require_picocolors(), 1);
|
|
6720
7855
|
var TYPES = [
|
|
6721
7856
|
"input",
|
|
6722
7857
|
"choice",
|
|
@@ -6767,7 +7902,7 @@ function registerAsk(program3) {
|
|
|
6767
7902
|
code: "bad_request"
|
|
6768
7903
|
});
|
|
6769
7904
|
}
|
|
6770
|
-
info(
|
|
7905
|
+
info(import_picocolors25.default.dim("Waiting for the user to answer\u2026"));
|
|
6771
7906
|
const res = await api(ctx, `/v1/runs/${runTarget(ctx)}/ask`, {
|
|
6772
7907
|
body: {
|
|
6773
7908
|
type,
|
|
@@ -6793,12 +7928,12 @@ function deriveTitle(message) {
|
|
|
6793
7928
|
function printAnswer(res) {
|
|
6794
7929
|
if (!res) return;
|
|
6795
7930
|
if (res.status === "answered") {
|
|
6796
|
-
line(` ${
|
|
7931
|
+
line(` ${import_picocolors25.default.green("answered")}`);
|
|
6797
7932
|
const value = extract(res.response);
|
|
6798
7933
|
if (value) line(` ${value}`);
|
|
6799
7934
|
return;
|
|
6800
7935
|
}
|
|
6801
|
-
line(` ${
|
|
7936
|
+
line(` ${import_picocolors25.default.yellow(res.status)} ${import_picocolors25.default.dim(res.reason ?? "")}`);
|
|
6802
7937
|
}
|
|
6803
7938
|
function extract(response) {
|
|
6804
7939
|
if (response == null) return "";
|
|
@@ -6817,7 +7952,7 @@ function extract(response) {
|
|
|
6817
7952
|
}
|
|
6818
7953
|
|
|
6819
7954
|
// src/commands/search.ts
|
|
6820
|
-
var
|
|
7955
|
+
var import_picocolors26 = __toESM(require_picocolors(), 1);
|
|
6821
7956
|
function registerSearch(program3) {
|
|
6822
7957
|
program3.command("search <query>").description("Search the web (Google-grounded, server-side)").option("-n, --max-results <n>", "max results", "5").action(
|
|
6823
7958
|
action(async ({ ctx, args, opts }) => {
|
|
@@ -6830,9 +7965,9 @@ function registerSearch(program3) {
|
|
|
6830
7965
|
line("");
|
|
6831
7966
|
}
|
|
6832
7967
|
const results = res?.results ?? [];
|
|
6833
|
-
if (!results.length) return line(
|
|
7968
|
+
if (!results.length) return line(import_picocolors26.default.dim("No results."));
|
|
6834
7969
|
for (const r of results) {
|
|
6835
|
-
line(`${r.title ||
|
|
7970
|
+
line(`${r.title || import_picocolors26.default.dim("(untitled)")} ${import_picocolors26.default.dim(r.url)}`);
|
|
6836
7971
|
}
|
|
6837
7972
|
});
|
|
6838
7973
|
})
|
|
@@ -6840,7 +7975,7 @@ function registerSearch(program3) {
|
|
|
6840
7975
|
}
|
|
6841
7976
|
|
|
6842
7977
|
// src/commands/board.ts
|
|
6843
|
-
var
|
|
7978
|
+
var import_picocolors27 = __toESM(require_picocolors(), 1);
|
|
6844
7979
|
|
|
6845
7980
|
// src/commands/record-step.ts
|
|
6846
7981
|
async function recordEntityStep(ctx, opts) {
|
|
@@ -6879,7 +8014,7 @@ function registerBoard(program3) {
|
|
|
6879
8014
|
}
|
|
6880
8015
|
ok(rows, () => {
|
|
6881
8016
|
if (!rows.length) {
|
|
6882
|
-
line(
|
|
8017
|
+
line(import_picocolors27.default.dim("No cards on the Board yet."));
|
|
6883
8018
|
return;
|
|
6884
8019
|
}
|
|
6885
8020
|
for (const r of rows) line(formatRow(r));
|
|
@@ -6894,7 +8029,7 @@ function registerBoard(program3) {
|
|
|
6894
8029
|
`/v1/projects/${projectId}/work-items/${args[0]}`
|
|
6895
8030
|
);
|
|
6896
8031
|
ok(row, () => {
|
|
6897
|
-
line(`${
|
|
8032
|
+
line(`${import_picocolors27.default.bold(row.title)} ${import_picocolors27.default.dim(row.id)}`);
|
|
6898
8033
|
line(`${statusTag(row.status)} priority ${row.priority}`);
|
|
6899
8034
|
if (row.ownerHuman) line(`owner: ${row.ownerHuman}`);
|
|
6900
8035
|
if (row.labels?.length) line(`labels: ${row.labels.join(", ")}`);
|
|
@@ -6935,7 +8070,7 @@ ${row.description}`);
|
|
|
6935
8070
|
refId: row?.id,
|
|
6936
8071
|
output: { workItem: row }
|
|
6937
8072
|
});
|
|
6938
|
-
ok(row, () => line(`Created work item ${
|
|
8073
|
+
ok(row, () => line(`Created work item ${import_picocolors27.default.bold(row?.id ?? "")} \u2014 ${title}`));
|
|
6939
8074
|
})
|
|
6940
8075
|
);
|
|
6941
8076
|
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(
|
|
@@ -6969,7 +8104,7 @@ ${row.description}`);
|
|
|
6969
8104
|
);
|
|
6970
8105
|
}
|
|
6971
8106
|
const row = await patchItem(ctx, projectId, String(args[0]), body);
|
|
6972
|
-
ok(row, () => line(`Updated ${
|
|
8107
|
+
ok(row, () => line(`Updated ${import_picocolors27.default.bold(row.id)} \u2014 ${row.title} ${statusTag(row.status)}`));
|
|
6973
8108
|
})
|
|
6974
8109
|
);
|
|
6975
8110
|
board.command("move <id> <status>").description(
|
|
@@ -6980,14 +8115,14 @@ ${row.description}`);
|
|
|
6980
8115
|
const status = String(args[1]);
|
|
6981
8116
|
assertStatus(status);
|
|
6982
8117
|
const row = await patchItem(ctx, projectId, String(args[0]), { status });
|
|
6983
|
-
ok(row, () => line(`Moved ${
|
|
8118
|
+
ok(row, () => line(`Moved ${import_picocolors27.default.bold(row.title)} \u2192 ${statusTag(row.status)}`));
|
|
6984
8119
|
})
|
|
6985
8120
|
);
|
|
6986
8121
|
board.command("close <id>").description("Shorthand for `board move <id> done`").action(
|
|
6987
8122
|
action(async ({ ctx, args }) => {
|
|
6988
8123
|
const projectId = requireProject(ctx);
|
|
6989
8124
|
const row = await patchItem(ctx, projectId, String(args[0]), { status: "done" });
|
|
6990
|
-
ok(row, () => line(`Closed ${
|
|
8125
|
+
ok(row, () => line(`Closed ${import_picocolors27.default.bold(row.title)} ${statusTag(row.status)}`));
|
|
6991
8126
|
})
|
|
6992
8127
|
);
|
|
6993
8128
|
}
|
|
@@ -7020,23 +8155,23 @@ function assertPriority(value) {
|
|
|
7020
8155
|
}
|
|
7021
8156
|
}
|
|
7022
8157
|
function formatRow(r) {
|
|
7023
|
-
const labels = r.labels?.length ?
|
|
7024
|
-
const owner = r.ownerHuman ?
|
|
7025
|
-
return `${
|
|
8158
|
+
const labels = r.labels?.length ? import_picocolors27.default.dim(` [${r.labels.join(", ")}]`) : "";
|
|
8159
|
+
const owner = r.ownerHuman ? import_picocolors27.default.dim(` @${r.ownerHuman}`) : "";
|
|
8160
|
+
return `${import_picocolors27.default.dim(r.id)} ${statusTag(r.status)} ${r.title}${labels}${owner}`;
|
|
7026
8161
|
}
|
|
7027
8162
|
function statusTag(status) {
|
|
7028
8163
|
const label = status.padEnd(11);
|
|
7029
|
-
if (status === "done") return
|
|
7030
|
-
if (status === "in-progress") return
|
|
7031
|
-
if (status === "in-review") return
|
|
7032
|
-
return
|
|
8164
|
+
if (status === "done") return import_picocolors27.default.green(label);
|
|
8165
|
+
if (status === "in-progress") return import_picocolors27.default.yellow(label);
|
|
8166
|
+
if (status === "in-review") return import_picocolors27.default.cyan(label);
|
|
8167
|
+
return import_picocolors27.default.dim(label);
|
|
7033
8168
|
}
|
|
7034
8169
|
function collect2(value, previous) {
|
|
7035
8170
|
return [...previous, value];
|
|
7036
8171
|
}
|
|
7037
8172
|
|
|
7038
8173
|
// src/commands/task.ts
|
|
7039
|
-
var
|
|
8174
|
+
var import_picocolors28 = __toESM(require_picocolors(), 1);
|
|
7040
8175
|
var STATUSES2 = ["todo", "working", "checking", "ready", "accepted", "archived"];
|
|
7041
8176
|
var ROLES = ["pm", "architect", "web", "api", "automation", "qa"];
|
|
7042
8177
|
var KINDS2 = ["data_reports", "web", "mobile", "service", "automation", "docs"];
|
|
@@ -7055,7 +8190,7 @@ function registerTask(program3) {
|
|
|
7055
8190
|
}) ?? [];
|
|
7056
8191
|
ok(rows, () => {
|
|
7057
8192
|
if (!rows.length) {
|
|
7058
|
-
line(
|
|
8193
|
+
line(import_picocolors28.default.dim("No tasks on the board yet."));
|
|
7059
8194
|
return;
|
|
7060
8195
|
}
|
|
7061
8196
|
for (const r of rows) line(formatRow2(r));
|
|
@@ -7063,23 +8198,91 @@ function registerTask(program3) {
|
|
|
7063
8198
|
})
|
|
7064
8199
|
);
|
|
7065
8200
|
task.command("show [id]").description(
|
|
7066
|
-
"Show
|
|
8201
|
+
"Show a task with its subtasks, what each one did, and what they produced. Defaults to the plan this run is part of."
|
|
7067
8202
|
).action(
|
|
7068
8203
|
action(async ({ ctx, args }) => {
|
|
7069
|
-
const id =
|
|
8204
|
+
const id = args[0] || ctx.parentTaskId || resolveTaskId(ctx);
|
|
7070
8205
|
const row = await api(ctx, `/v1/project-tasks/${encodeURIComponent(id)}`);
|
|
7071
8206
|
ok(row, () => printTask(row));
|
|
7072
8207
|
})
|
|
7073
8208
|
);
|
|
7074
|
-
|
|
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(
|
|
8209
|
+
task.command("create <title>").description("Open a new root task for the project team").option("--note <text>", "context, constraints and expected outcome").option("--kind <value>", `what it produces (${KINDS2.join(" | ")})`).option("--label <value...>", "labels to put on the task").option("--app <id...>", "apps the task touches").option("--infra <ref...>", "shared setup it touches").action(
|
|
7078
8210
|
action(async ({ ctx, args, opts }) => {
|
|
7079
|
-
|
|
7080
|
-
if (opts.role !== void 0) assertOneOf("--role", opts.role, ROLES);
|
|
8211
|
+
requireProject(ctx);
|
|
7081
8212
|
if (opts.kind !== void 0) assertOneOf("--kind", opts.kind, KINDS2);
|
|
7082
|
-
const
|
|
8213
|
+
const hasChannelOrigin = Boolean(
|
|
8214
|
+
ctx.projectChannelId && ctx.projectChannelMessageId
|
|
8215
|
+
);
|
|
8216
|
+
const row = await api(ctx, "/v1/project-tasks", {
|
|
8217
|
+
body: {
|
|
8218
|
+
title: args[0],
|
|
8219
|
+
summary: opts.note,
|
|
8220
|
+
category: opts.kind,
|
|
8221
|
+
labels: opts.label,
|
|
8222
|
+
targets: [
|
|
8223
|
+
...(opts.app ?? []).map((appId) => ({ kind: "app", appId })),
|
|
8224
|
+
...(opts.infra ?? []).map((ref) => ({ kind: "infra", ref }))
|
|
8225
|
+
],
|
|
8226
|
+
...hasChannelOrigin ? {
|
|
8227
|
+
channelId: ctx.projectChannelId,
|
|
8228
|
+
createdFromMessageId: ctx.projectChannelMessageId,
|
|
8229
|
+
createdByKind: "agent"
|
|
8230
|
+
} : {}
|
|
8231
|
+
}
|
|
8232
|
+
});
|
|
8233
|
+
let channelMessage;
|
|
8234
|
+
let channelMessageError;
|
|
8235
|
+
if (hasChannelOrigin && ctx.projectChannelId && ctx.projectChannelMessageId) {
|
|
8236
|
+
try {
|
|
8237
|
+
channelMessage = await api(
|
|
8238
|
+
ctx,
|
|
8239
|
+
`/v1/project-channels/${encodeURIComponent(ctx.projectChannelId)}/messages`,
|
|
8240
|
+
{
|
|
8241
|
+
body: {
|
|
8242
|
+
content: "",
|
|
8243
|
+
agentRole: ctx.agentRole ?? "pm",
|
|
8244
|
+
agentType: ctx.agentType ?? "workser",
|
|
8245
|
+
agentModel: ctx.agentModel ?? "Agent default",
|
|
8246
|
+
attachments: [{ resourceType: "task", resourceId: row.id }]
|
|
8247
|
+
}
|
|
8248
|
+
}
|
|
8249
|
+
);
|
|
8250
|
+
} catch (error) {
|
|
8251
|
+
const failure = error instanceof WorkserError ? error : new WorkserError(String(error));
|
|
8252
|
+
channelMessageError = {
|
|
8253
|
+
code: failure.code,
|
|
8254
|
+
message: failure.message
|
|
8255
|
+
};
|
|
8256
|
+
}
|
|
8257
|
+
}
|
|
8258
|
+
const result = {
|
|
8259
|
+
...row,
|
|
8260
|
+
...channelMessage ? { channelMessage } : {},
|
|
8261
|
+
...channelMessageError ? { channelMessageError } : {}
|
|
8262
|
+
};
|
|
8263
|
+
ok(result, () => {
|
|
8264
|
+
line(`${import_picocolors28.default.green("opened")} ${import_picocolors28.default.bold(row.title)} ${import_picocolors28.default.dim(row.key ?? row.id)}`);
|
|
8265
|
+
if (channelMessage) line(import_picocolors28.default.dim("posted to the channel as Project Manager"));
|
|
8266
|
+
if (channelMessageError) {
|
|
8267
|
+
warn(
|
|
8268
|
+
`Task opened, but its Project Manager card could not be posted: ${channelMessageError.message}`
|
|
8269
|
+
);
|
|
8270
|
+
}
|
|
8271
|
+
});
|
|
8272
|
+
})
|
|
8273
|
+
);
|
|
8274
|
+
const subtask = task.command("subtask").description("The steps a task is broken into");
|
|
8275
|
+
subtask.command("add <title>").description(
|
|
8276
|
+
'Add one step, e.g. `workser task subtask add "Build the upload screen" --role web`'
|
|
8277
|
+
).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(
|
|
8278
|
+
"--app <id...>",
|
|
8279
|
+
"the app this step is for \u2014 one id runs the step inside that app's folder; leave it off and the step runs at the project, seeing every app"
|
|
8280
|
+
).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(
|
|
8281
|
+
action(async ({ ctx, args, opts }) => {
|
|
8282
|
+
const parent = resolveTaskId(ctx, opts.task);
|
|
8283
|
+
if (opts.role !== void 0) assertOneOf("--role", opts.role, ROLES);
|
|
8284
|
+
if (opts.kind !== void 0) assertOneOf("--kind", opts.kind, KINDS2);
|
|
8285
|
+
const dependsOn = await resolveDeps(ctx, parent, opts.dependsOn ?? []);
|
|
7083
8286
|
const row = await api(ctx, "/v1/project-tasks", {
|
|
7084
8287
|
body: {
|
|
7085
8288
|
parentTaskId: parent,
|
|
@@ -7096,8 +8299,8 @@ function registerTask(program3) {
|
|
|
7096
8299
|
}
|
|
7097
8300
|
});
|
|
7098
8301
|
ok(row, () => {
|
|
7099
|
-
line(`${
|
|
7100
|
-
if (row.role) line(
|
|
8302
|
+
line(`${import_picocolors28.default.green("added")} ${import_picocolors28.default.bold(row.title)} ${import_picocolors28.default.dim(row.key ?? row.id)}`);
|
|
8303
|
+
if (row.role) line(import_picocolors28.default.dim(`role: ${row.role}`));
|
|
7101
8304
|
});
|
|
7102
8305
|
})
|
|
7103
8306
|
);
|
|
@@ -7108,7 +8311,7 @@ function registerTask(program3) {
|
|
|
7108
8311
|
const rows = row.subtasks ?? [];
|
|
7109
8312
|
ok(rows, () => {
|
|
7110
8313
|
if (!rows.length) {
|
|
7111
|
-
line(
|
|
8314
|
+
line(import_picocolors28.default.dim("No steps yet."));
|
|
7112
8315
|
return;
|
|
7113
8316
|
}
|
|
7114
8317
|
rows.forEach((r, i) => line(formatSubtask(r, i + 1)));
|
|
@@ -7133,7 +8336,7 @@ function registerTask(program3) {
|
|
|
7133
8336
|
}
|
|
7134
8337
|
}
|
|
7135
8338
|
);
|
|
7136
|
-
ok(row, () => line(`${
|
|
8339
|
+
ok(row, () => line(`${import_picocolors28.default.green("updated")} ${import_picocolors28.default.bold(row.title)}`));
|
|
7137
8340
|
})
|
|
7138
8341
|
);
|
|
7139
8342
|
subtask.command("remove <id>").description("Drop a step from the plan (only before the work starts)").action(
|
|
@@ -7141,7 +8344,7 @@ function registerTask(program3) {
|
|
|
7141
8344
|
await api(ctx, `/v1/project-tasks/${encodeURIComponent(args[0])}`, {
|
|
7142
8345
|
method: "DELETE"
|
|
7143
8346
|
});
|
|
7144
|
-
ok({ removed: args[0] }, () => line(
|
|
8347
|
+
ok({ removed: args[0] }, () => line(import_picocolors28.default.green("removed")));
|
|
7145
8348
|
})
|
|
7146
8349
|
);
|
|
7147
8350
|
task.command("move <id> <status>").description(`Move a task or step along the board (${STATUSES2.join(" | ")})`).action(
|
|
@@ -7152,7 +8355,45 @@ function registerTask(program3) {
|
|
|
7152
8355
|
`/v1/project-tasks/${encodeURIComponent(args[0])}/move`,
|
|
7153
8356
|
{ body: { status: args[1] } }
|
|
7154
8357
|
);
|
|
7155
|
-
ok(row, () => line(`${
|
|
8358
|
+
ok(row, () => line(`${import_picocolors28.default.green("moved")} ${import_picocolors28.default.bold(row.title)} \u2192 ${args[1]}`));
|
|
8359
|
+
})
|
|
8360
|
+
);
|
|
8361
|
+
task.command("resume [id]").description(
|
|
8362
|
+
"Put a step the owner stopped back in the queue \u2014 only when they ask you to carry on"
|
|
8363
|
+
).action(
|
|
8364
|
+
action(async ({ ctx, args }) => {
|
|
8365
|
+
const id = resolveTaskId(ctx, args[0]);
|
|
8366
|
+
const row = await api(
|
|
8367
|
+
ctx,
|
|
8368
|
+
`/v1/project-tasks/${encodeURIComponent(id)}/retry`,
|
|
8369
|
+
{ method: "POST" }
|
|
8370
|
+
);
|
|
8371
|
+
ok(
|
|
8372
|
+
row,
|
|
8373
|
+
() => line(
|
|
8374
|
+
row?.queued ? `${import_picocolors28.default.green("resumed")} \u2014 it is back in the queue and will start on the next dispatch` : import_picocolors28.default.yellow("could not resume it \u2014 check the step id")
|
|
8375
|
+
)
|
|
8376
|
+
);
|
|
8377
|
+
})
|
|
8378
|
+
);
|
|
8379
|
+
subtask.command("send-back <id>").description("Send a finished step back to be done again, with the reason").requiredOption("--note <text>", "what was wrong with it").action(
|
|
8380
|
+
action(async ({ ctx, args, opts }) => {
|
|
8381
|
+
const row = await api(
|
|
8382
|
+
ctx,
|
|
8383
|
+
`/v1/project-subtasks/${encodeURIComponent(String(args[0]))}/reopen`,
|
|
8384
|
+
{ method: "POST", body: { note: opts.note } }
|
|
8385
|
+
);
|
|
8386
|
+
ok(row, () => {
|
|
8387
|
+
if (row?.reopened) {
|
|
8388
|
+
line(`${import_picocolors28.default.green("sent back")} \u2014 it will be picked up again`);
|
|
8389
|
+
return;
|
|
8390
|
+
}
|
|
8391
|
+
line(
|
|
8392
|
+
import_picocolors28.default.yellow(
|
|
8393
|
+
row?.reason === "already_open" ? "already waiting to be picked up \u2014 nothing to send back" : row?.reason === "still_working" ? "still working \u2014 let it finish before sending it back" : "could not send that step back"
|
|
8394
|
+
)
|
|
8395
|
+
);
|
|
8396
|
+
});
|
|
7156
8397
|
})
|
|
7157
8398
|
);
|
|
7158
8399
|
task.command("can-start [id]").description("Ask whether work on this task may begin. Refuses until the owner approves.").action(
|
|
@@ -7163,7 +8404,7 @@ function registerTask(program3) {
|
|
|
7163
8404
|
`/v1/project-tasks/${encodeURIComponent(id)}/dispatch-check`,
|
|
7164
8405
|
{ method: "POST" }
|
|
7165
8406
|
);
|
|
7166
|
-
ok(row, () => line(
|
|
8407
|
+
ok(row, () => line(import_picocolors28.default.green("approved \u2014 you may start")));
|
|
7167
8408
|
})
|
|
7168
8409
|
);
|
|
7169
8410
|
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(
|
|
@@ -7177,7 +8418,7 @@ function registerTask(program3) {
|
|
|
7177
8418
|
);
|
|
7178
8419
|
ok({ awaiting: row2.approval_state === "awaiting", task: row2 }, () => {
|
|
7179
8420
|
line(
|
|
7180
|
-
row2.approval_state === "awaiting" ?
|
|
8421
|
+
row2.approval_state === "awaiting" ? import_picocolors28.default.yellow("The plan is waiting on the owner. They see it in the task.") : `Already ${row2.approval_state}.`
|
|
7181
8422
|
);
|
|
7182
8423
|
});
|
|
7183
8424
|
return;
|
|
@@ -7198,7 +8439,7 @@ function registerTask(program3) {
|
|
|
7198
8439
|
}
|
|
7199
8440
|
}
|
|
7200
8441
|
);
|
|
7201
|
-
ok(row, () => line(`${
|
|
8442
|
+
ok(row, () => line(`${import_picocolors28.default.green(row.approval_state)} ${import_picocolors28.default.bold(row.title)}`));
|
|
7202
8443
|
})
|
|
7203
8444
|
);
|
|
7204
8445
|
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(
|
|
@@ -7213,7 +8454,7 @@ function registerTask(program3) {
|
|
|
7213
8454
|
`/v1/project-tasks/${encodeURIComponent(id)}/move`,
|
|
7214
8455
|
{ body: { status: "ready" } }
|
|
7215
8456
|
);
|
|
7216
|
-
ok(row, () => line(`${
|
|
8457
|
+
ok(row, () => line(`${import_picocolors28.default.green("ready")} ${import_picocolors28.default.bold(row.title)}`));
|
|
7217
8458
|
})
|
|
7218
8459
|
);
|
|
7219
8460
|
}
|
|
@@ -7258,19 +8499,24 @@ function assertOneOf(flag, value, allowed) {
|
|
|
7258
8499
|
}
|
|
7259
8500
|
}
|
|
7260
8501
|
function formatRow2(r) {
|
|
7261
|
-
const key =
|
|
7262
|
-
const steps = r.subtaskTotal ?
|
|
7263
|
-
const gate = r.approval_state === "awaiting" ?
|
|
8502
|
+
const key = import_picocolors28.default.dim((r.key ?? r.id.slice(0, 8)).padEnd(10));
|
|
8503
|
+
const steps = r.subtaskTotal ? import_picocolors28.default.dim(` ${r.subtaskDone}/${r.subtaskTotal}`) : "";
|
|
8504
|
+
const gate = r.approval_state === "awaiting" ? import_picocolors28.default.yellow(" awaiting approval") : "";
|
|
7264
8505
|
return `${key} ${statusTag2(r.status)} ${r.title}${steps}${gate}`;
|
|
7265
8506
|
}
|
|
7266
8507
|
function formatSubtask(r, index) {
|
|
7267
|
-
const n =
|
|
7268
|
-
const role = r.role ?
|
|
7269
|
-
const scope = r.scope_paths?.length ?
|
|
7270
|
-
|
|
8508
|
+
const n = import_picocolors28.default.dim(String(index).padStart(2, "0"));
|
|
8509
|
+
const role = r.role ? import_picocolors28.default.dim(` [${r.role}]`) : "";
|
|
8510
|
+
const scope = r.scope_paths?.length ? import_picocolors28.default.dim(` owns: ${r.scope_paths.join(", ")}`) : "";
|
|
8511
|
+
const head = `${n} ${statusTag2(r.status)} ${r.title}${role}${scope}`;
|
|
8512
|
+
const summary = (r.result_summary ?? "").trim();
|
|
8513
|
+
if (!summary) return head;
|
|
8514
|
+
const wrapped = summary.split("\n").map((l) => ` ${l}`).join("\n");
|
|
8515
|
+
return `${head}
|
|
8516
|
+
${import_picocolors28.default.dim(wrapped)}`;
|
|
7271
8517
|
}
|
|
7272
8518
|
function printTask(row) {
|
|
7273
|
-
line(`${
|
|
8519
|
+
line(`${import_picocolors28.default.bold(row.title)} ${import_picocolors28.default.dim(row.key ?? row.id)}`);
|
|
7274
8520
|
line(`${statusTag2(row.status)} approval: ${row.approval_state}`);
|
|
7275
8521
|
if (row.summary) line(`
|
|
7276
8522
|
${row.summary}`);
|
|
@@ -7282,35 +8528,51 @@ touches: ${row.targets.map((t) => t.appName ?? t.ref ?? t.kind).join(", ")}`
|
|
|
7282
8528
|
}
|
|
7283
8529
|
if (row.subtasks?.length) {
|
|
7284
8530
|
line(`
|
|
7285
|
-
${
|
|
8531
|
+
${import_picocolors28.default.bold("steps")}`);
|
|
7286
8532
|
row.subtasks.forEach((s, i) => line(formatSubtask(s, i + 1)));
|
|
8533
|
+
line(
|
|
8534
|
+
import_picocolors28.default.dim(
|
|
8535
|
+
`
|
|
8536
|
+
Run \`workser artifact list\` to see what these steps produced,`
|
|
8537
|
+
)
|
|
8538
|
+
);
|
|
8539
|
+
line(
|
|
8540
|
+
import_picocolors28.default.dim(
|
|
8541
|
+
`or \`workser artifact list --step <id>\` for one step's output alone.`
|
|
8542
|
+
)
|
|
8543
|
+
);
|
|
7287
8544
|
} else {
|
|
7288
|
-
line(
|
|
8545
|
+
line(import_picocolors28.default.dim("\nNo steps yet."));
|
|
7289
8546
|
}
|
|
7290
8547
|
}
|
|
7291
8548
|
function statusTag2(status) {
|
|
7292
8549
|
switch (status) {
|
|
7293
8550
|
case "ready":
|
|
7294
|
-
return
|
|
8551
|
+
return import_picocolors28.default.green("[ready]");
|
|
7295
8552
|
case "working":
|
|
7296
|
-
return
|
|
8553
|
+
return import_picocolors28.default.blue("[working]");
|
|
7297
8554
|
case "checking":
|
|
7298
|
-
return
|
|
8555
|
+
return import_picocolors28.default.cyan("[checking]");
|
|
7299
8556
|
case "accepted":
|
|
7300
|
-
return
|
|
8557
|
+
return import_picocolors28.default.green("[accepted]");
|
|
7301
8558
|
case "archived":
|
|
7302
|
-
return
|
|
8559
|
+
return import_picocolors28.default.dim("[archived]");
|
|
7303
8560
|
default:
|
|
7304
|
-
return
|
|
8561
|
+
return import_picocolors28.default.dim("[todo]");
|
|
7305
8562
|
}
|
|
7306
8563
|
}
|
|
7307
8564
|
|
|
7308
8565
|
// src/role-guard.ts
|
|
7309
8566
|
var READS = [
|
|
8567
|
+
// `requirement` sits beside `doc` and `decision` because it is the same kind
|
|
8568
|
+
// of thing and was simply forgotten: the verb shipped, no role could run it,
|
|
8569
|
+
// and on 2026-08-23 a channel PM reported to the owner that "requirements are
|
|
8570
|
+
// not readable by this PM role" — which was exactly true, for every role.
|
|
7310
8571
|
"task",
|
|
7311
8572
|
"board",
|
|
7312
8573
|
"doc",
|
|
7313
8574
|
"decision",
|
|
8575
|
+
"requirement",
|
|
7314
8576
|
"memory",
|
|
7315
8577
|
"search",
|
|
7316
8578
|
"verify",
|
|
@@ -7322,7 +8584,22 @@ var READS = [
|
|
|
7322
8584
|
"auth",
|
|
7323
8585
|
"project",
|
|
7324
8586
|
"open",
|
|
7325
|
-
"doctor"
|
|
8587
|
+
"doctor",
|
|
8588
|
+
// Both READ and report. `scan` reads files and shells out to npm; `health`
|
|
8589
|
+
// makes a GET request to an address that is already public. Neither can
|
|
8590
|
+
// change anything, which is why the roles that exist to look — qa, security,
|
|
8591
|
+
// sre, analyst — get them without getting anything else.
|
|
8592
|
+
"scan",
|
|
8593
|
+
"health",
|
|
8594
|
+
// Read-only views of what is running. Added with Phase 6a: an SRE that can
|
|
8595
|
+
// read logs but cannot list deployments or read the app's address is being
|
|
8596
|
+
// asked to diagnose an outage with one eye shut.
|
|
8597
|
+
"urls",
|
|
8598
|
+
"deployments",
|
|
8599
|
+
// Reading the plan and what is used against it. An agent proposing "add
|
|
8600
|
+
// another project" can only sensibly propose it if it can find out the plan
|
|
8601
|
+
// allows two and two already exist.
|
|
8602
|
+
"usage"
|
|
7326
8603
|
];
|
|
7327
8604
|
var BUILDS = [
|
|
7328
8605
|
...READS,
|
|
@@ -7355,6 +8632,12 @@ var ROLE_VERBS = {
|
|
|
7355
8632
|
analyst: READS,
|
|
7356
8633
|
sre: [...READS, "deploy", "domain", "versions"],
|
|
7357
8634
|
devops: [...BUILDS, "deploy", "domain", "versions"]
|
|
8635
|
+
// NOTE: `deployments` reaches READS above, so every role can LIST and
|
|
8636
|
+
// INSPECT. That is correct — history is a read. The two verbs that change
|
|
8637
|
+
// production (`promote`, `rollback`) are not gated here at all, and must not
|
|
8638
|
+
// be: they are gated in the DAEMON, as `deploy.prod`, which is a door "just
|
|
8639
|
+
// do it" cannot open. A second, verb-name-based rule here would be a weaker
|
|
8640
|
+
// copy of a control that already works.
|
|
7358
8641
|
};
|
|
7359
8642
|
var NEVER = {
|
|
7360
8643
|
// Approving is the owner's, full stop. An agent that can approve the plan it
|
|
@@ -7364,10 +8647,11 @@ var NEVER = {
|
|
|
7364
8647
|
function assertRoleMayRun(argv) {
|
|
7365
8648
|
const role = (process.env.WORKSER_ROLE ?? "").trim();
|
|
7366
8649
|
if (!role) return;
|
|
7367
|
-
const
|
|
8650
|
+
const commandArgv = stripLeadingGlobalOptions(argv);
|
|
8651
|
+
const verb = commandArgv[0];
|
|
7368
8652
|
if (!verb) return;
|
|
7369
|
-
const pair = `${
|
|
7370
|
-
if (NEVER[pair] && !(pair === "task approval" && (
|
|
8653
|
+
const pair = `${commandArgv[0]} ${commandArgv[1] ?? ""}`.trim();
|
|
8654
|
+
if (NEVER[pair] && !(pair === "task approval" && (commandArgv[2] === "request" || !commandArgv[2]))) {
|
|
7371
8655
|
throw new WorkserError(NEVER[pair], { code: "role_forbidden" });
|
|
7372
8656
|
}
|
|
7373
8657
|
const allowed = ROLE_VERBS[role];
|
|
@@ -7379,9 +8663,38 @@ function assertRoleMayRun(argv) {
|
|
|
7379
8663
|
);
|
|
7380
8664
|
}
|
|
7381
8665
|
}
|
|
8666
|
+
function stripLeadingGlobalOptions(argv) {
|
|
8667
|
+
const takesValue = /* @__PURE__ */ new Set([
|
|
8668
|
+
"-p",
|
|
8669
|
+
"--project",
|
|
8670
|
+
"-C",
|
|
8671
|
+
"--cwd",
|
|
8672
|
+
"--endpoint",
|
|
8673
|
+
"--token"
|
|
8674
|
+
]);
|
|
8675
|
+
const flags = /* @__PURE__ */ new Set(["--json", "-q", "--quiet", "-v", "--version"]);
|
|
8676
|
+
let index = 0;
|
|
8677
|
+
while (index < argv.length) {
|
|
8678
|
+
const token = argv[index];
|
|
8679
|
+
if (flags.has(token)) {
|
|
8680
|
+
index += 1;
|
|
8681
|
+
continue;
|
|
8682
|
+
}
|
|
8683
|
+
if (takesValue.has(token)) {
|
|
8684
|
+
index += 2;
|
|
8685
|
+
continue;
|
|
8686
|
+
}
|
|
8687
|
+
if (token.startsWith("--project=") || token.startsWith("--cwd=") || token.startsWith("--endpoint=") || token.startsWith("--token=")) {
|
|
8688
|
+
index += 1;
|
|
8689
|
+
continue;
|
|
8690
|
+
}
|
|
8691
|
+
break;
|
|
8692
|
+
}
|
|
8693
|
+
return argv.slice(index);
|
|
8694
|
+
}
|
|
7382
8695
|
|
|
7383
8696
|
// src/commands/decision.ts
|
|
7384
|
-
var
|
|
8697
|
+
var import_picocolors29 = __toESM(require_picocolors(), 1);
|
|
7385
8698
|
function registerDecision(program3) {
|
|
7386
8699
|
const decision = program3.command("decision").description("Read and record the project's architecture decisions");
|
|
7387
8700
|
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(
|
|
@@ -7394,11 +8707,11 @@ function registerDecision(program3) {
|
|
|
7394
8707
|
rows = applyLimit(rows, opts.limit);
|
|
7395
8708
|
ok(rows, () => {
|
|
7396
8709
|
if (!rows.length) {
|
|
7397
|
-
line(
|
|
8710
|
+
line(import_picocolors29.default.dim("No decisions recorded yet."));
|
|
7398
8711
|
return;
|
|
7399
8712
|
}
|
|
7400
8713
|
for (const r of rows) {
|
|
7401
|
-
line(`${
|
|
8714
|
+
line(`${import_picocolors29.default.dim(r.id)} ${import_picocolors29.default.dim(shortDate(r.createdAt))} ${r.title}`);
|
|
7402
8715
|
line(` ${truncate(r.decision, 100)}`);
|
|
7403
8716
|
}
|
|
7404
8717
|
});
|
|
@@ -7412,16 +8725,16 @@ function registerDecision(program3) {
|
|
|
7412
8725
|
`/v1/projects/${projectId}/architecture-decisions/${args[0]}`
|
|
7413
8726
|
);
|
|
7414
8727
|
ok(row, () => {
|
|
7415
|
-
line(`${
|
|
7416
|
-
line(
|
|
8728
|
+
line(`${import_picocolors29.default.bold(row.title)} ${import_picocolors29.default.dim(row.id)}`);
|
|
8729
|
+
line(import_picocolors29.default.dim(`${row.status} \xB7 ${shortDate(row.createdAt)}`));
|
|
7417
8730
|
line(`
|
|
7418
|
-
${
|
|
8731
|
+
${import_picocolors29.default.bold("Context")}
|
|
7419
8732
|
${row.context}`);
|
|
7420
8733
|
line(`
|
|
7421
|
-
${
|
|
8734
|
+
${import_picocolors29.default.bold("Decision")}
|
|
7422
8735
|
${row.decision}`);
|
|
7423
8736
|
if (row.consequences) line(`
|
|
7424
|
-
${
|
|
8737
|
+
${import_picocolors29.default.bold("Consequences")}
|
|
7425
8738
|
${row.consequences}`);
|
|
7426
8739
|
});
|
|
7427
8740
|
})
|
|
@@ -7450,7 +8763,7 @@ ${row.consequences}`);
|
|
|
7450
8763
|
refId: row?.id,
|
|
7451
8764
|
output: { decision: row }
|
|
7452
8765
|
});
|
|
7453
|
-
ok(row, () => line(`Recorded decision ${
|
|
8766
|
+
ok(row, () => line(`Recorded decision ${import_picocolors29.default.bold(row?.id ?? "")} \u2014 ${title}`));
|
|
7454
8767
|
})
|
|
7455
8768
|
);
|
|
7456
8769
|
const requirement = program3.command("requirement").description("Read and record the project's requirements");
|
|
@@ -7462,11 +8775,11 @@ ${row.consequences}`);
|
|
|
7462
8775
|
rows = applyLimit(rows, opts.limit);
|
|
7463
8776
|
ok(rows, () => {
|
|
7464
8777
|
if (!rows.length) {
|
|
7465
|
-
line(
|
|
8778
|
+
line(import_picocolors29.default.dim("No requirements recorded yet."));
|
|
7466
8779
|
return;
|
|
7467
8780
|
}
|
|
7468
8781
|
for (const r of rows) {
|
|
7469
|
-
line(`${
|
|
8782
|
+
line(`${import_picocolors29.default.dim(r.id)} ${r.status.padEnd(9)} ${r.title}`);
|
|
7470
8783
|
}
|
|
7471
8784
|
});
|
|
7472
8785
|
})
|
|
@@ -7479,8 +8792,8 @@ ${row.consequences}`);
|
|
|
7479
8792
|
`/v1/projects/${projectId}/requirements/${args[0]}`
|
|
7480
8793
|
);
|
|
7481
8794
|
ok(row, () => {
|
|
7482
|
-
line(`${
|
|
7483
|
-
line(
|
|
8795
|
+
line(`${import_picocolors29.default.bold(row.title)} ${import_picocolors29.default.dim(row.id)}`);
|
|
8796
|
+
line(import_picocolors29.default.dim(`${row.status} \xB7 ${shortDate(row.createdAt)}`));
|
|
7484
8797
|
line(`
|
|
7485
8798
|
${row.body}`);
|
|
7486
8799
|
});
|
|
@@ -7506,7 +8819,7 @@ ${row.body}`);
|
|
|
7506
8819
|
refId: row?.id,
|
|
7507
8820
|
output: { requirement: row }
|
|
7508
8821
|
});
|
|
7509
|
-
ok(row, () => line(`Recorded requirement ${
|
|
8822
|
+
ok(row, () => line(`Recorded requirement ${import_picocolors29.default.bold(row?.id ?? "")} \u2014 ${title}`));
|
|
7510
8823
|
})
|
|
7511
8824
|
);
|
|
7512
8825
|
requirement.command("update <id>").description(
|
|
@@ -7535,7 +8848,7 @@ ${row.body}`);
|
|
|
7535
8848
|
code: "bad_request"
|
|
7536
8849
|
});
|
|
7537
8850
|
}
|
|
7538
|
-
ok(row, () => line(`Updated requirement ${
|
|
8851
|
+
ok(row, () => line(`Updated requirement ${import_picocolors29.default.bold(row.id)} \u2014 ${row.title} (${row.status})`));
|
|
7539
8852
|
})
|
|
7540
8853
|
);
|
|
7541
8854
|
}
|
|
@@ -7558,7 +8871,41 @@ function shortDate(iso) {
|
|
|
7558
8871
|
}
|
|
7559
8872
|
|
|
7560
8873
|
// src/commands/doc.ts
|
|
7561
|
-
var
|
|
8874
|
+
var import_picocolors30 = __toESM(require_picocolors(), 1);
|
|
8875
|
+
|
|
8876
|
+
// src/mermaid-fences.ts
|
|
8877
|
+
function hasDiagram(code) {
|
|
8878
|
+
if (!code) return false;
|
|
8879
|
+
return code.split("\n").some((line2) => {
|
|
8880
|
+
const text = line2.trim();
|
|
8881
|
+
return !!text && !text.startsWith("%%");
|
|
8882
|
+
});
|
|
8883
|
+
}
|
|
8884
|
+
function diagramKind(code) {
|
|
8885
|
+
if (!code) return null;
|
|
8886
|
+
for (const line2 of code.split("\n")) {
|
|
8887
|
+
const text = line2.trim();
|
|
8888
|
+
if (!text || text.startsWith("%%")) continue;
|
|
8889
|
+
const match = /^([A-Za-z][A-Za-z-]*)/.exec(text);
|
|
8890
|
+
return match ? match[1] : null;
|
|
8891
|
+
}
|
|
8892
|
+
return null;
|
|
8893
|
+
}
|
|
8894
|
+
function extractDiagrams(markdown) {
|
|
8895
|
+
if (!markdown) return [];
|
|
8896
|
+
const out = [];
|
|
8897
|
+
const pattern = /^[ \t]*```[ \t]*mermaid[^\n]*\n([\s\S]*?)^[ \t]*```[ \t]*$/gm;
|
|
8898
|
+
let match;
|
|
8899
|
+
while ((match = pattern.exec(markdown)) !== null) {
|
|
8900
|
+
const body = match[1].replace(/\s+$/, "");
|
|
8901
|
+
if (hasDiagram(body)) out.push(body);
|
|
8902
|
+
}
|
|
8903
|
+
return out;
|
|
8904
|
+
}
|
|
8905
|
+
|
|
8906
|
+
// src/commands/doc.ts
|
|
8907
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
8908
|
+
import { isAbsolute, join as join2 } from "path";
|
|
7562
8909
|
function registerDoc(program3) {
|
|
7563
8910
|
const doc = program3.command("doc").description("Read and write project documents");
|
|
7564
8911
|
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(
|
|
@@ -7569,13 +8916,13 @@ function registerDoc(program3) {
|
|
|
7569
8916
|
}) ?? [];
|
|
7570
8917
|
ok(rows, () => {
|
|
7571
8918
|
if (!rows.length) {
|
|
7572
|
-
line(
|
|
8919
|
+
line(import_picocolors30.default.dim("No documents yet."));
|
|
7573
8920
|
return;
|
|
7574
8921
|
}
|
|
7575
8922
|
for (const r of rows) {
|
|
7576
|
-
const link = r.workItemId ?
|
|
7577
|
-
const file = r.filePath ?
|
|
7578
|
-
line(`${
|
|
8923
|
+
const link = r.workItemId ? import_picocolors30.default.dim(` \u21B3 ${r.workItemId}`) : "";
|
|
8924
|
+
const file = r.filePath ? import_picocolors30.default.dim(` ${r.filePath}`) : "";
|
|
8925
|
+
line(`${import_picocolors30.default.dim(r.id)} ${r.title}${link}${file}`);
|
|
7579
8926
|
}
|
|
7580
8927
|
});
|
|
7581
8928
|
})
|
|
@@ -7589,17 +8936,17 @@ function registerDoc(program3) {
|
|
|
7589
8936
|
);
|
|
7590
8937
|
if (opts.markdown) {
|
|
7591
8938
|
ok({ id: row.id, title: row.title, filePath: row.filePath }, () => {
|
|
7592
|
-
line(`${
|
|
8939
|
+
line(`${import_picocolors30.default.bold(row.title)} ${import_picocolors30.default.dim(row.id)}`);
|
|
7593
8940
|
line(
|
|
7594
|
-
row.filePath ? `Read it at ${
|
|
8941
|
+
row.filePath ? `Read it at ${import_picocolors30.default.bold(row.filePath)} (relative to the project folder).` : import_picocolors30.default.dim("This document has no markdown mirror on disk yet.")
|
|
7595
8942
|
);
|
|
7596
8943
|
});
|
|
7597
8944
|
return;
|
|
7598
8945
|
}
|
|
7599
8946
|
ok(row, () => {
|
|
7600
|
-
line(`${
|
|
7601
|
-
if (row.workItemId) line(
|
|
7602
|
-
if (row.filePath) line(
|
|
8947
|
+
line(`${import_picocolors30.default.bold(row.title)} ${import_picocolors30.default.dim(row.id)}`);
|
|
8948
|
+
if (row.workItemId) line(import_picocolors30.default.dim(`linked to work item ${row.workItemId}`));
|
|
8949
|
+
if (row.filePath) line(import_picocolors30.default.dim(`markdown mirror: ${row.filePath}`));
|
|
7603
8950
|
line("");
|
|
7604
8951
|
line(row.contentJson);
|
|
7605
8952
|
});
|
|
@@ -7628,7 +8975,54 @@ function registerDoc(program3) {
|
|
|
7628
8975
|
refId: row?.id,
|
|
7629
8976
|
output: { document: row }
|
|
7630
8977
|
});
|
|
7631
|
-
ok(row, () => line(`Created document ${
|
|
8978
|
+
ok(row, () => line(`Created document ${import_picocolors30.default.bold(row?.id ?? "")} \u2014 ${title}`));
|
|
8979
|
+
})
|
|
8980
|
+
);
|
|
8981
|
+
doc.command("diagram <id>").description(
|
|
8982
|
+
"List the diagrams in a document \u2014 `--check` fails when it has none"
|
|
8983
|
+
).option("--check", "exit non-zero when the document contains no diagram").action(
|
|
8984
|
+
action(async ({ ctx, args, opts }) => {
|
|
8985
|
+
const projectId = requireProject(ctx);
|
|
8986
|
+
const row = await api(
|
|
8987
|
+
ctx,
|
|
8988
|
+
`/v1/projects/${projectId}/documents/${args[0]}`
|
|
8989
|
+
);
|
|
8990
|
+
if (!row) {
|
|
8991
|
+
throw new WorkserError(`No document with id "${args[0]}" on this project.`, {
|
|
8992
|
+
code: "bad_request"
|
|
8993
|
+
});
|
|
8994
|
+
}
|
|
8995
|
+
const markdown = readMirror(ctx.cwd, row.filePath);
|
|
8996
|
+
const diagrams = markdown === null ? [] : extractDiagrams(markdown);
|
|
8997
|
+
const payload = {
|
|
8998
|
+
id: row.id,
|
|
8999
|
+
title: row.title,
|
|
9000
|
+
filePath: row.filePath,
|
|
9001
|
+
diagrams: diagrams.map((code) => ({ kind: diagramKind(code), code }))
|
|
9002
|
+
};
|
|
9003
|
+
ok(payload, () => {
|
|
9004
|
+
line(`${import_picocolors30.default.bold(row.title)} ${import_picocolors30.default.dim(row.id)}`);
|
|
9005
|
+
if (markdown === null) {
|
|
9006
|
+
line(
|
|
9007
|
+
import_picocolors30.default.dim(
|
|
9008
|
+
row.filePath ? `Could not read ${row.filePath} from this folder.` : "This document has no markdown mirror on disk yet."
|
|
9009
|
+
)
|
|
9010
|
+
);
|
|
9011
|
+
} else if (!diagrams.length) {
|
|
9012
|
+
line(import_picocolors30.default.dim("No diagrams in this document."));
|
|
9013
|
+
} else {
|
|
9014
|
+
for (const [i, code] of diagrams.entries()) {
|
|
9015
|
+
const kind = diagramKind(code) ?? "diagram";
|
|
9016
|
+
line(`${import_picocolors30.default.dim(String(i + 1))} ${kind} ${import_picocolors30.default.dim(`${code.split("\n").length} lines`)}`);
|
|
9017
|
+
}
|
|
9018
|
+
}
|
|
9019
|
+
});
|
|
9020
|
+
if (opts.check && !diagrams.length) {
|
|
9021
|
+
throw new WorkserError(
|
|
9022
|
+
markdown === null ? `"${row.title}" has no markdown on disk to check. Save it from the Docs panel, or write it with \`workser doc update ${row.id} --markdown ...\`.` : `"${row.title}" has no diagram. Add one with a \`\`\`mermaid fence describing how the pieces fit together.`,
|
|
9023
|
+
{ code: "bad_request" }
|
|
9024
|
+
);
|
|
9025
|
+
}
|
|
7632
9026
|
})
|
|
7633
9027
|
);
|
|
7634
9028
|
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(
|
|
@@ -7655,13 +9049,21 @@ function registerDoc(program3) {
|
|
|
7655
9049
|
code: "bad_request"
|
|
7656
9050
|
});
|
|
7657
9051
|
}
|
|
7658
|
-
ok(row, () => line(`Updated document ${
|
|
9052
|
+
ok(row, () => line(`Updated document ${import_picocolors30.default.bold(row.id)} \u2014 ${row.title}`));
|
|
7659
9053
|
})
|
|
7660
9054
|
);
|
|
7661
9055
|
}
|
|
9056
|
+
function readMirror(cwd, filePath) {
|
|
9057
|
+
if (!filePath) return null;
|
|
9058
|
+
try {
|
|
9059
|
+
return readFileSync2(isAbsolute(filePath) ? filePath : join2(cwd, filePath), "utf8");
|
|
9060
|
+
} catch {
|
|
9061
|
+
return null;
|
|
9062
|
+
}
|
|
9063
|
+
}
|
|
7662
9064
|
|
|
7663
9065
|
// src/commands/design.ts
|
|
7664
|
-
var
|
|
9066
|
+
var import_picocolors31 = __toESM(require_picocolors(), 1);
|
|
7665
9067
|
function registerDesign(program3) {
|
|
7666
9068
|
const design = program3.command("design").description("Read the project's brand (colours, fonts, logo)");
|
|
7667
9069
|
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(
|
|
@@ -7675,11 +9077,11 @@ function registerDesign(program3) {
|
|
|
7675
9077
|
if (opts.raw) {
|
|
7676
9078
|
ok(files, () => {
|
|
7677
9079
|
if (!files.length) {
|
|
7678
|
-
line(
|
|
9080
|
+
line(import_picocolors31.default.dim("No brand set for this project."));
|
|
7679
9081
|
return;
|
|
7680
9082
|
}
|
|
7681
9083
|
for (const f of files) {
|
|
7682
|
-
line(
|
|
9084
|
+
line(import_picocolors31.default.bold(f.path));
|
|
7683
9085
|
line(f.contents);
|
|
7684
9086
|
line("");
|
|
7685
9087
|
}
|
|
@@ -7696,21 +9098,21 @@ function registerDesign(program3) {
|
|
|
7696
9098
|
} : { hasBrand: false, colors: {}, fonts: {}, brand: {}, files: [] };
|
|
7697
9099
|
ok(summary, () => {
|
|
7698
9100
|
if (!tokens) {
|
|
7699
|
-
line(
|
|
9101
|
+
line(import_picocolors31.default.dim("No brand set for this project \u2014 choose sensible styling yourself."));
|
|
7700
9102
|
return;
|
|
7701
9103
|
}
|
|
7702
9104
|
for (const [name, value] of Object.entries(tokens.brand)) {
|
|
7703
|
-
line(`${
|
|
9105
|
+
line(`${import_picocolors31.default.dim(name.padEnd(12))} ${value}`);
|
|
7704
9106
|
}
|
|
7705
9107
|
for (const [name, value] of Object.entries(tokens.color)) {
|
|
7706
|
-
line(`${
|
|
9108
|
+
line(`${import_picocolors31.default.dim(`color.${name}`.padEnd(12))} ${value}`);
|
|
7707
9109
|
}
|
|
7708
9110
|
for (const [name, value] of Object.entries(tokens.font)) {
|
|
7709
|
-
line(`${
|
|
9111
|
+
line(`${import_picocolors31.default.dim(`font.${name}`.padEnd(12))} ${value}`);
|
|
7710
9112
|
}
|
|
7711
9113
|
line("");
|
|
7712
9114
|
line(
|
|
7713
|
-
|
|
9115
|
+
import_picocolors31.default.dim(
|
|
7714
9116
|
`Generated into the working tree as ${files.map((f) => f.path).join(", ")} \u2014 wire those in, never edit them.`
|
|
7715
9117
|
)
|
|
7716
9118
|
);
|
|
@@ -7740,9 +9142,1005 @@ function unwrap(group) {
|
|
|
7740
9142
|
);
|
|
7741
9143
|
}
|
|
7742
9144
|
|
|
9145
|
+
// src/commands/api.ts
|
|
9146
|
+
var import_picocolors32 = __toESM(require_picocolors(), 1);
|
|
9147
|
+
import { readdirSync, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
9148
|
+
import { join as join3, relative, sep } from "path";
|
|
9149
|
+
|
|
9150
|
+
// src/api-spec.ts
|
|
9151
|
+
var SPEC_FILES = [
|
|
9152
|
+
"api/openapi.json",
|
|
9153
|
+
"openapi.json",
|
|
9154
|
+
"api/openapi.yaml",
|
|
9155
|
+
"openapi.yaml",
|
|
9156
|
+
"api/openapi.yml",
|
|
9157
|
+
"openapi.yml"
|
|
9158
|
+
];
|
|
9159
|
+
function fromNextRoute(file) {
|
|
9160
|
+
const match = /^app\/(.*\/)?route\.(t|j)sx?$/.exec(file);
|
|
9161
|
+
if (!match) return null;
|
|
9162
|
+
const segments = (match[1] ?? "").split("/").filter(Boolean).filter((s) => !(s.startsWith("(") && s.endsWith(")"))).map((s) => {
|
|
9163
|
+
const dynamic = /^\[\.{0,3}(.+?)\]$/.exec(s);
|
|
9164
|
+
return dynamic ? `{${dynamic[1]}}` : s;
|
|
9165
|
+
});
|
|
9166
|
+
return `/${segments.join("/")}`.replace(/\/+$/, "") || "/";
|
|
9167
|
+
}
|
|
9168
|
+
function fromPythonFile(file) {
|
|
9169
|
+
const match = /^api\/(.+)\.py$/.exec(file);
|
|
9170
|
+
if (!match) return null;
|
|
9171
|
+
const stem = match[1];
|
|
9172
|
+
if (stem === "index") return "/api";
|
|
9173
|
+
if (stem.endsWith("/index")) return `/api/${stem.slice(0, -"/index".length)}`;
|
|
9174
|
+
return `/api/${stem}`;
|
|
9175
|
+
}
|
|
9176
|
+
function discoverRoutes(files) {
|
|
9177
|
+
const seen = /* @__PURE__ */ new Map();
|
|
9178
|
+
for (const raw of files) {
|
|
9179
|
+
const file = raw.replace(/^\.\//, "");
|
|
9180
|
+
const path = fromNextRoute(file) ?? fromPythonFile(file);
|
|
9181
|
+
if (!path) continue;
|
|
9182
|
+
if (!seen.has(path)) seen.set(path, { path, file });
|
|
9183
|
+
}
|
|
9184
|
+
return [...seen.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
9185
|
+
}
|
|
9186
|
+
function specPaths(text) {
|
|
9187
|
+
if (!text) return [];
|
|
9188
|
+
const trimmed = text.trim();
|
|
9189
|
+
if (!trimmed) return [];
|
|
9190
|
+
if (trimmed.startsWith("{")) {
|
|
9191
|
+
try {
|
|
9192
|
+
const parsed = JSON.parse(trimmed);
|
|
9193
|
+
const paths = parsed?.paths;
|
|
9194
|
+
if (!paths || typeof paths !== "object") return [];
|
|
9195
|
+
return Object.keys(paths).filter((k) => k.startsWith("/")).sort();
|
|
9196
|
+
} catch {
|
|
9197
|
+
return [];
|
|
9198
|
+
}
|
|
9199
|
+
}
|
|
9200
|
+
const out = [];
|
|
9201
|
+
let inPaths = false;
|
|
9202
|
+
let indent = 0;
|
|
9203
|
+
for (const line2 of trimmed.split(/\r?\n/)) {
|
|
9204
|
+
if (!line2.trim() || line2.trim().startsWith("#")) continue;
|
|
9205
|
+
const leading = line2.length - line2.trimStart().length;
|
|
9206
|
+
if (!inPaths) {
|
|
9207
|
+
if (/^paths\s*:/.test(line2.trim()) && leading === 0) {
|
|
9208
|
+
inPaths = true;
|
|
9209
|
+
indent = -1;
|
|
9210
|
+
}
|
|
9211
|
+
continue;
|
|
9212
|
+
}
|
|
9213
|
+
if (indent === -1) {
|
|
9214
|
+
if (leading === 0) break;
|
|
9215
|
+
indent = leading;
|
|
9216
|
+
}
|
|
9217
|
+
if (leading < indent) break;
|
|
9218
|
+
if (leading > indent) continue;
|
|
9219
|
+
const key = /^\s*["']?(\/[^"':]*)["']?\s*:/.exec(line2);
|
|
9220
|
+
if (key) out.push(key[1].trim());
|
|
9221
|
+
}
|
|
9222
|
+
return [...new Set(out)].sort();
|
|
9223
|
+
}
|
|
9224
|
+
var EXEMPT = /* @__PURE__ */ new Set(["/api/health", "/health", "/api", "/"]);
|
|
9225
|
+
function specReport(routes, documented) {
|
|
9226
|
+
const declared = new Set(documented);
|
|
9227
|
+
const served = new Set(routes.map((r) => r.path));
|
|
9228
|
+
const missing = routes.filter(
|
|
9229
|
+
(r) => !declared.has(r.path) && !EXEMPT.has(r.path)
|
|
9230
|
+
);
|
|
9231
|
+
const stale = documented.filter((p) => !served.has(p)).sort();
|
|
9232
|
+
return {
|
|
9233
|
+
routes,
|
|
9234
|
+
documented: [...declared].sort(),
|
|
9235
|
+
missing,
|
|
9236
|
+
stale,
|
|
9237
|
+
ok: missing.length === 0
|
|
9238
|
+
};
|
|
9239
|
+
}
|
|
9240
|
+
function specSummary(report, specFile) {
|
|
9241
|
+
if (!specFile) {
|
|
9242
|
+
return `This service has no API description. Write one at ${SPEC_FILES[0]} listing the routes it serves.`;
|
|
9243
|
+
}
|
|
9244
|
+
if (report.ok) {
|
|
9245
|
+
const n = report.routes.length;
|
|
9246
|
+
return n === 1 ? `The one route this service has is described in ${specFile}.` : `All ${n} routes are described in ${specFile}.`;
|
|
9247
|
+
}
|
|
9248
|
+
const names = report.missing.map((m) => m.path).join(", ");
|
|
9249
|
+
return `${report.missing.length} route${report.missing.length === 1 ? " is" : "s are"} not described in ${specFile}: ${names}`;
|
|
9250
|
+
}
|
|
9251
|
+
|
|
9252
|
+
// src/commands/api.ts
|
|
9253
|
+
function registerApi(program3) {
|
|
9254
|
+
const cmd = program3.command("api").description("Call this service and check it describes its own routes");
|
|
9255
|
+
cmd.command("list").description("The requests saved with this service, in api/requests.json").option("--app <webAppId>", "the service, when this folder holds more than one").action(
|
|
9256
|
+
action(async ({ ctx, opts }) => {
|
|
9257
|
+
const appId = requireApp(opts.app);
|
|
9258
|
+
const res = await api(ctx, `/v1/apps/${encodeURIComponent(appId)}/api/requests`);
|
|
9259
|
+
ok(res, () => {
|
|
9260
|
+
for (const note of res?.notes ?? []) line(import_picocolors32.default.dim(note));
|
|
9261
|
+
for (const r of res?.requests ?? []) {
|
|
9262
|
+
line(
|
|
9263
|
+
`${import_picocolors32.default.dim(r.method.padEnd(6))}${r.path}${r.note ? import_picocolors32.default.dim(` ${r.note}`) : ""}`
|
|
9264
|
+
);
|
|
9265
|
+
}
|
|
9266
|
+
});
|
|
9267
|
+
})
|
|
9268
|
+
);
|
|
9269
|
+
cmd.command("call <path>").description(
|
|
9270
|
+
`workser api call /orders --method POST --body '{"item":1}' [--env local|preview|production]`
|
|
9271
|
+
).option("--app <webAppId>", "the service, when this folder holds more than one").option("--method <verb>", "GET by default").option("--body <text>", "request body, already serialised").option(
|
|
9272
|
+
"--header <name:value>",
|
|
9273
|
+
"extra header; repeat for more than one",
|
|
9274
|
+
collectHeader,
|
|
9275
|
+
{}
|
|
9276
|
+
).option(
|
|
9277
|
+
"--env <name>",
|
|
9278
|
+
"local (default), preview or production \u2014 the host comes from here, never from the path"
|
|
9279
|
+
).action(
|
|
9280
|
+
action(async ({ ctx, args, opts }) => {
|
|
9281
|
+
const appId = requireApp(opts.app);
|
|
9282
|
+
const res = await api(
|
|
9283
|
+
ctx,
|
|
9284
|
+
`/v1/apps/${encodeURIComponent(appId)}/api/call`,
|
|
9285
|
+
{
|
|
9286
|
+
body: {
|
|
9287
|
+
environment: opts.env,
|
|
9288
|
+
method: opts.method,
|
|
9289
|
+
path: args[0],
|
|
9290
|
+
headers: opts.header,
|
|
9291
|
+
body: opts.body
|
|
9292
|
+
}
|
|
9293
|
+
}
|
|
9294
|
+
);
|
|
9295
|
+
ok(res, () => {
|
|
9296
|
+
if (!res?.ok) {
|
|
9297
|
+
line(import_picocolors32.default.red(res?.error ?? "The service did not answer."));
|
|
9298
|
+
return;
|
|
9299
|
+
}
|
|
9300
|
+
const code = `${res.status}${res.statusText ? ` ${res.statusText}` : ""}`;
|
|
9301
|
+
const colour2 = res.status && res.status < 300 ? import_picocolors32.default.green : res.status && res.status < 500 ? import_picocolors32.default.yellow : import_picocolors32.default.red;
|
|
9302
|
+
line(`${colour2(code)} ${import_picocolors32.default.dim(`${res.durationMs}ms ${res.url}`)}`);
|
|
9303
|
+
if (res.body) line(res.body);
|
|
9304
|
+
if (res.truncated) line(import_picocolors32.default.dim("(answer truncated)"));
|
|
9305
|
+
});
|
|
9306
|
+
if (!res?.ok) process.exitCode = 1;
|
|
9307
|
+
})
|
|
9308
|
+
);
|
|
9309
|
+
cmd.command("spec").description(
|
|
9310
|
+
"Compare the routes this repo serves with the ones it documents \u2014 `--check` fails on a gap"
|
|
9311
|
+
).option("--check", "exit non-zero when a route has no entry in the spec").action(
|
|
9312
|
+
action(async ({ ctx, opts }) => {
|
|
9313
|
+
requireLocalApp(ctx, "api spec");
|
|
9314
|
+
const files = listRepoFiles(ctx.cwd);
|
|
9315
|
+
const routes = discoverRoutes(files);
|
|
9316
|
+
const specFile = SPEC_FILES.find((f) => files.includes(f)) ?? null;
|
|
9317
|
+
const documented = specFile ? specPaths(readIfPresent(join3(ctx.cwd, specFile))) : [];
|
|
9318
|
+
const report = specReport(routes, documented);
|
|
9319
|
+
const summary = specSummary(report, specFile);
|
|
9320
|
+
ok({ ...report, specFile, summary }, () => {
|
|
9321
|
+
for (const r of report.routes) {
|
|
9322
|
+
const known = report.missing.some((m) => m.path === r.path);
|
|
9323
|
+
line(
|
|
9324
|
+
`${known ? import_picocolors32.default.yellow("undocumented") : import_picocolors32.default.green("documented ")} ${r.path}${import_picocolors32.default.dim(` ${r.file}`)}`
|
|
9325
|
+
);
|
|
9326
|
+
}
|
|
9327
|
+
for (const p of report.stale) {
|
|
9328
|
+
line(`${import_picocolors32.default.dim("in spec only ")} ${p}`);
|
|
9329
|
+
}
|
|
9330
|
+
line("");
|
|
9331
|
+
if (report.ok && specFile) success(summary);
|
|
9332
|
+
else line(import_picocolors32.default.yellow(summary));
|
|
9333
|
+
});
|
|
9334
|
+
if (opts.check && !report.ok) {
|
|
9335
|
+
throw new WorkserError(summary, { code: "bad_request" });
|
|
9336
|
+
}
|
|
9337
|
+
if (opts.check && !specFile) {
|
|
9338
|
+
throw new WorkserError(summary, { code: "bad_request" });
|
|
9339
|
+
}
|
|
9340
|
+
})
|
|
9341
|
+
);
|
|
9342
|
+
}
|
|
9343
|
+
function collectHeader(raw, previous) {
|
|
9344
|
+
const at = raw.indexOf(":");
|
|
9345
|
+
if (at <= 0) return previous;
|
|
9346
|
+
return { ...previous, [raw.slice(0, at).trim()]: raw.slice(at + 1).trim() };
|
|
9347
|
+
}
|
|
9348
|
+
function requireApp(app) {
|
|
9349
|
+
const value = typeof app === "string" ? app.trim() : "";
|
|
9350
|
+
if (value) return value;
|
|
9351
|
+
throw new WorkserError(
|
|
9352
|
+
"Which service? Pass --app <webAppId>; `workser app list` shows them.",
|
|
9353
|
+
{ code: "bad_request" }
|
|
9354
|
+
);
|
|
9355
|
+
}
|
|
9356
|
+
function readIfPresent(file) {
|
|
9357
|
+
try {
|
|
9358
|
+
return readFileSync3(file, "utf8");
|
|
9359
|
+
} catch {
|
|
9360
|
+
return null;
|
|
9361
|
+
}
|
|
9362
|
+
}
|
|
9363
|
+
function listRepoFiles(root, maxDepth = 8) {
|
|
9364
|
+
const SKIP = /* @__PURE__ */ new Set([
|
|
9365
|
+
"node_modules",
|
|
9366
|
+
".git",
|
|
9367
|
+
".next",
|
|
9368
|
+
"dist",
|
|
9369
|
+
"build",
|
|
9370
|
+
".vercel",
|
|
9371
|
+
"__pycache__",
|
|
9372
|
+
".venv",
|
|
9373
|
+
"venv"
|
|
9374
|
+
]);
|
|
9375
|
+
const out = [];
|
|
9376
|
+
const walk = (dir, depth) => {
|
|
9377
|
+
if (depth > maxDepth || out.length > 5e3) return;
|
|
9378
|
+
let entries;
|
|
9379
|
+
try {
|
|
9380
|
+
entries = readdirSync(dir);
|
|
9381
|
+
} catch {
|
|
9382
|
+
return;
|
|
9383
|
+
}
|
|
9384
|
+
for (const name of entries) {
|
|
9385
|
+
if (name.startsWith(".") && name !== ".well-known") continue;
|
|
9386
|
+
if (SKIP.has(name)) continue;
|
|
9387
|
+
const full = join3(dir, name);
|
|
9388
|
+
let isDir = false;
|
|
9389
|
+
try {
|
|
9390
|
+
isDir = statSync2(full).isDirectory();
|
|
9391
|
+
} catch {
|
|
9392
|
+
continue;
|
|
9393
|
+
}
|
|
9394
|
+
if (isDir) walk(full, depth + 1);
|
|
9395
|
+
else out.push(relative(root, full).split(sep).join("/"));
|
|
9396
|
+
}
|
|
9397
|
+
};
|
|
9398
|
+
walk(root, 0);
|
|
9399
|
+
return out;
|
|
9400
|
+
}
|
|
9401
|
+
|
|
9402
|
+
// src/commands/analysis.ts
|
|
9403
|
+
var import_picocolors33 = __toESM(require_picocolors(), 1);
|
|
9404
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
9405
|
+
function registerAnalysis(program3) {
|
|
9406
|
+
const cmd = program3.command("analysis").description("Run Python analysis locally, recorded in the task");
|
|
9407
|
+
cmd.command("runtime").description("Is Python here, and does it have what an analysis needs?").option("--app <webAppId>", "check the interpreter this app would use").action(
|
|
9408
|
+
action(async ({ ctx, opts }) => {
|
|
9409
|
+
const path = opts.app ? `/v1/apps/${encodeURIComponent(String(opts.app))}/analysis/runtime` : "/v1/analysis/runtime";
|
|
9410
|
+
const res = await api(ctx, path);
|
|
9411
|
+
ok(res, () => {
|
|
9412
|
+
line(
|
|
9413
|
+
`${res?.available ? import_picocolors33.default.green("python") : import_picocolors33.default.red("python")} ${res?.version ?? "not found"} ${import_picocolors33.default.dim(res?.python ?? "")}`
|
|
9414
|
+
);
|
|
9415
|
+
for (const lib of res?.libraries ?? []) {
|
|
9416
|
+
line(
|
|
9417
|
+
`${lib.present ? import_picocolors33.default.green(lib.name) : import_picocolors33.default.yellow(lib.name)}${import_picocolors33.default.dim(lib.present ? "" : " missing")}`
|
|
9418
|
+
);
|
|
9419
|
+
}
|
|
9420
|
+
for (const note of res?.notes ?? []) line(import_picocolors33.default.dim(note));
|
|
9421
|
+
});
|
|
9422
|
+
if (!res?.available) process.exitCode = 1;
|
|
9423
|
+
})
|
|
9424
|
+
);
|
|
9425
|
+
cmd.command("run").description(
|
|
9426
|
+
"workser analysis run --app <id> --file report.py (or --code '<python>')"
|
|
9427
|
+
).requiredOption("--app <webAppId>", "the app whose folder the script runs in").option("--file <path>", "a Python file to run").option("--code <python>", "the script itself, for something short").option("--timeout <ms>", "how long to allow, capped at 15 minutes").action(
|
|
9428
|
+
action(async ({ ctx, opts }) => {
|
|
9429
|
+
const code = readCode(opts.file, opts.code);
|
|
9430
|
+
const res = await api(
|
|
9431
|
+
ctx,
|
|
9432
|
+
`/v1/apps/${encodeURIComponent(String(opts.app))}/analysis/run`,
|
|
9433
|
+
{
|
|
9434
|
+
body: {
|
|
9435
|
+
code,
|
|
9436
|
+
timeoutMs: opts.timeout ? Number(opts.timeout) : void 0
|
|
9437
|
+
}
|
|
9438
|
+
}
|
|
9439
|
+
);
|
|
9440
|
+
ok(res, () => {
|
|
9441
|
+
if (res?.stdout) line(res.stdout.replace(/\n$/, ""));
|
|
9442
|
+
if (res?.stderr) line(import_picocolors33.default.dim(res.stderr.replace(/\n$/, "")));
|
|
9443
|
+
if (res?.truncated) line(import_picocolors33.default.dim("(output truncated)"));
|
|
9444
|
+
const took = `${Math.round((res?.durationMs ?? 0) / 100) / 10}s`;
|
|
9445
|
+
line(
|
|
9446
|
+
res?.ok ? import_picocolors33.default.green(`\u2713 ${res.summary ?? "It finished."}`) + import_picocolors33.default.dim(` ${took}`) : import_picocolors33.default.yellow(res?.summary ?? "It did not finish.") + import_picocolors33.default.dim(` ${took}`)
|
|
9447
|
+
);
|
|
9448
|
+
if (res && !res.sandboxed) {
|
|
9449
|
+
line(
|
|
9450
|
+
import_picocolors33.default.dim(
|
|
9451
|
+
"This platform has no OS sandbox, so the script ran with your own file access."
|
|
9452
|
+
)
|
|
9453
|
+
);
|
|
9454
|
+
}
|
|
9455
|
+
});
|
|
9456
|
+
if (!res?.ok) process.exitCode = 1;
|
|
9457
|
+
})
|
|
9458
|
+
);
|
|
9459
|
+
}
|
|
9460
|
+
function readCode(file, inline) {
|
|
9461
|
+
if (typeof inline === "string" && inline.trim()) return inline;
|
|
9462
|
+
if (typeof file === "string" && file.trim()) {
|
|
9463
|
+
try {
|
|
9464
|
+
return readFileSync4(file, "utf8");
|
|
9465
|
+
} catch (err) {
|
|
9466
|
+
throw new WorkserError(
|
|
9467
|
+
`Could not read ${file}: ${err instanceof Error ? err.message : String(err)}`,
|
|
9468
|
+
{ code: "bad_request" }
|
|
9469
|
+
);
|
|
9470
|
+
}
|
|
9471
|
+
}
|
|
9472
|
+
throw new WorkserError("Pass --file <path> or --code '<python>'.", {
|
|
9473
|
+
code: "bad_request"
|
|
9474
|
+
});
|
|
9475
|
+
}
|
|
9476
|
+
|
|
9477
|
+
// src/commands/scan.ts
|
|
9478
|
+
var import_picocolors34 = __toESM(require_picocolors(), 1);
|
|
9479
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
9480
|
+
import { existsSync as existsSync3, readFileSync as readFileSync5, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
9481
|
+
import { join as join4, relative as relative2, sep as sep2 } from "path";
|
|
9482
|
+
|
|
9483
|
+
// src/scan.ts
|
|
9484
|
+
var SECRET_PATTERNS = [
|
|
9485
|
+
{ name: "a private key", re: /-----BEGIN[A-Z ]*PRIVATE KEY-----/, severity: "high" },
|
|
9486
|
+
{ name: "an AWS access key", re: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/, severity: "high" },
|
|
9487
|
+
{ name: "a GitHub token", re: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/, severity: "high" },
|
|
9488
|
+
{ name: "a Stripe live key", re: /\b[sr]k_live_[A-Za-z0-9]{16,}\b/, severity: "high" },
|
|
9489
|
+
{ name: "an OpenAI key", re: /\bsk-(?:proj-)?[A-Za-z0-9_-]{32,}\b/, severity: "high" },
|
|
9490
|
+
{ name: "an Anthropic key", re: /\bsk-ant-[A-Za-z0-9_-]{32,}\b/, severity: "high" },
|
|
9491
|
+
{ name: "a Google API key", re: /\bAIza[0-9A-Za-z_-]{35}\b/, severity: "high" },
|
|
9492
|
+
{ name: "a Slack token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/, severity: "high" },
|
|
9493
|
+
{
|
|
9494
|
+
name: "a database password in a connection string",
|
|
9495
|
+
re: /\b(?:postgres|postgresql|mysql|mongodb(?:\+srv)?|redis|amqp):\/\/[^\s:@/]+:[^\s:@/]+@/,
|
|
9496
|
+
severity: "high"
|
|
9497
|
+
},
|
|
9498
|
+
{
|
|
9499
|
+
// The everyday one: `API_KEY = "…"` with something real on the right.
|
|
9500
|
+
name: "a secret written into the code",
|
|
9501
|
+
re: /(?:secret|password|passwd|api[_-]?key|access[_-]?token|auth[_-]?token|private[_-]?key)\s*[:=]\s*["'`][^"'`\s]{12,}["'`]/i,
|
|
9502
|
+
severity: "high"
|
|
9503
|
+
}
|
|
9504
|
+
];
|
|
9505
|
+
var PLACEHOLDER = /(?:example|placeholder|changeme|your[_-]?|xxx+|\.\.\.|<[^>]+>|\$\{|process\.env|os\.environ|REPLACE|dummy|sample|test[_-]?key|fake)/i;
|
|
9506
|
+
var SECRET_EXEMPT = /(?:\.example$|\.sample$|\.template$|(?:^|\/)(?:fixtures?|__fixtures__|__tests__|test|tests|spec|mocks?)\/|\.(?:test|spec)\.[jt]sx?$|(?:^|\/)(?:package-lock\.json|yarn\.lock|pnpm-lock\.yaml)$|(?:^|\/)workser-scan\.md$)/;
|
|
9507
|
+
function addedLines(diff) {
|
|
9508
|
+
const out = [];
|
|
9509
|
+
let file = null;
|
|
9510
|
+
let lineNo = 0;
|
|
9511
|
+
for (const raw of diff.split("\n")) {
|
|
9512
|
+
if (raw.startsWith("+++ ")) {
|
|
9513
|
+
const path = raw.slice(4).trim();
|
|
9514
|
+
file = path === "/dev/null" ? null : path.replace(/^b\//, "");
|
|
9515
|
+
continue;
|
|
9516
|
+
}
|
|
9517
|
+
if (raw.startsWith("--- ") || raw.startsWith("diff --git")) continue;
|
|
9518
|
+
const hunk = raw.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
9519
|
+
if (hunk) {
|
|
9520
|
+
lineNo = Number(hunk[1]);
|
|
9521
|
+
continue;
|
|
9522
|
+
}
|
|
9523
|
+
if (!file) continue;
|
|
9524
|
+
if (raw.startsWith("+")) {
|
|
9525
|
+
out.push({ file, line: lineNo, text: raw.slice(1) });
|
|
9526
|
+
lineNo++;
|
|
9527
|
+
continue;
|
|
9528
|
+
}
|
|
9529
|
+
if (raw.startsWith(" ")) lineNo++;
|
|
9530
|
+
}
|
|
9531
|
+
return out;
|
|
9532
|
+
}
|
|
9533
|
+
function secretFindings(diff) {
|
|
9534
|
+
const findings = [];
|
|
9535
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9536
|
+
for (const added of addedLines(diff)) {
|
|
9537
|
+
if (SECRET_EXEMPT.test(added.file)) continue;
|
|
9538
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
9539
|
+
if (!pattern.re.test(added.text)) continue;
|
|
9540
|
+
if (PLACEHOLDER.test(added.text)) continue;
|
|
9541
|
+
const key = `${added.file}:${pattern.name}`;
|
|
9542
|
+
if (seen.has(key)) continue;
|
|
9543
|
+
seen.add(key);
|
|
9544
|
+
findings.push({
|
|
9545
|
+
check: "secrets",
|
|
9546
|
+
severity: pattern.severity,
|
|
9547
|
+
title: `This change adds ${pattern.name} to the code`,
|
|
9548
|
+
file: added.file,
|
|
9549
|
+
line: added.line,
|
|
9550
|
+
fix: "Move the value into an environment setting (`workser env set`) and use it from there. If it has already been committed, treat it as leaked and replace it at the source."
|
|
9551
|
+
});
|
|
9552
|
+
break;
|
|
9553
|
+
}
|
|
9554
|
+
}
|
|
9555
|
+
return findings;
|
|
9556
|
+
}
|
|
9557
|
+
function depFindings(auditJson) {
|
|
9558
|
+
if (!auditJson) return [];
|
|
9559
|
+
let parsed;
|
|
9560
|
+
try {
|
|
9561
|
+
parsed = JSON.parse(auditJson);
|
|
9562
|
+
} catch {
|
|
9563
|
+
return [];
|
|
9564
|
+
}
|
|
9565
|
+
const vulns = parsed?.vulnerabilities;
|
|
9566
|
+
if (!vulns || typeof vulns !== "object") return [];
|
|
9567
|
+
const findings = [];
|
|
9568
|
+
for (const [name, raw] of Object.entries(vulns)) {
|
|
9569
|
+
const severity = String(raw?.severity ?? "").toLowerCase();
|
|
9570
|
+
if (severity !== "high" && severity !== "critical") continue;
|
|
9571
|
+
const direct = raw?.isDirect === true;
|
|
9572
|
+
const fixable = raw?.fixAvailable;
|
|
9573
|
+
findings.push({
|
|
9574
|
+
check: "deps",
|
|
9575
|
+
severity: "high",
|
|
9576
|
+
title: `${name} has a known ${severity} security problem`,
|
|
9577
|
+
fix: fixable === true ? `Run \`npm audit fix\`${direct ? "" : " \u2014 it is pulled in by another package"}.` : typeof fixable === "object" && fixable?.name ? `Fixing it means moving to ${fixable.name}@${fixable.version}, which is a breaking change. Decide it deliberately.` : "There is no published fix yet. Decide whether this package is worth keeping."
|
|
9578
|
+
});
|
|
9579
|
+
}
|
|
9580
|
+
return findings.sort((a, b) => a.title.localeCompare(b.title));
|
|
9581
|
+
}
|
|
9582
|
+
function permissionFindings(files, trackedFiles = []) {
|
|
9583
|
+
const findings = [];
|
|
9584
|
+
for (const file of files) {
|
|
9585
|
+
for (const match of file.content.matchAll(
|
|
9586
|
+
/NEXT_PUBLIC_[A-Z0-9_]*(?:SECRET|PASSWORD|TOKEN|API_KEY|PRIVATE)[A-Z0-9_]*/g
|
|
9587
|
+
)) {
|
|
9588
|
+
findings.push({
|
|
9589
|
+
check: "permissions",
|
|
9590
|
+
severity: "high",
|
|
9591
|
+
title: `${match[0]} is sent to every visitor's browser`,
|
|
9592
|
+
file: file.path,
|
|
9593
|
+
fix: "Anything named NEXT_PUBLIC_ is public by design. Rename it without that prefix and read it on the server only."
|
|
9594
|
+
});
|
|
9595
|
+
}
|
|
9596
|
+
if (/["']Access-Control-Allow-Origin["']\s*[:,]\s*["']\*["']/.test(file.content) && /Access-Control-Allow-Credentials/.test(file.content)) {
|
|
9597
|
+
findings.push({
|
|
9598
|
+
check: "permissions",
|
|
9599
|
+
severity: "high",
|
|
9600
|
+
title: "This service accepts credentialed requests from any website",
|
|
9601
|
+
file: file.path,
|
|
9602
|
+
fix: "Name the sites allowed to call it instead of `*`, or stop sending credentials."
|
|
9603
|
+
});
|
|
9604
|
+
}
|
|
9605
|
+
}
|
|
9606
|
+
for (const tracked of trackedFiles) {
|
|
9607
|
+
const base = tracked.split("/").pop() ?? tracked;
|
|
9608
|
+
if (!/^\.env(\.[a-z0-9-]+)?$/i.test(base)) continue;
|
|
9609
|
+
if (/\.(example|sample|template)$/i.test(base)) continue;
|
|
9610
|
+
findings.push({
|
|
9611
|
+
check: "permissions",
|
|
9612
|
+
severity: "high",
|
|
9613
|
+
title: `${tracked} is committed to the repository`,
|
|
9614
|
+
file: tracked,
|
|
9615
|
+
fix: "Add it to .gitignore, remove it from the repo, and treat every value in it as leaked."
|
|
9616
|
+
});
|
|
9617
|
+
}
|
|
9618
|
+
return findings;
|
|
9619
|
+
}
|
|
9620
|
+
function buildReport(input) {
|
|
9621
|
+
const counts = { high: 0, medium: 0, low: 0 };
|
|
9622
|
+
for (const f of input.findings) counts[f.severity]++;
|
|
9623
|
+
return {
|
|
9624
|
+
findings: [...input.findings].sort(
|
|
9625
|
+
(a, b) => rank(b.severity) - rank(a.severity) || a.title.localeCompare(b.title)
|
|
9626
|
+
),
|
|
9627
|
+
checked: input.checked,
|
|
9628
|
+
skipped: input.skipped,
|
|
9629
|
+
counts,
|
|
9630
|
+
ok: counts.high === 0
|
|
9631
|
+
};
|
|
9632
|
+
}
|
|
9633
|
+
function rank(s) {
|
|
9634
|
+
return s === "high" ? 3 : s === "medium" ? 2 : 1;
|
|
9635
|
+
}
|
|
9636
|
+
function scanSummary(report) {
|
|
9637
|
+
const ran = report.checked.length;
|
|
9638
|
+
if (!ran) return "Nothing could be checked \u2014 see the reasons above.";
|
|
9639
|
+
const what = report.checked.join(", ");
|
|
9640
|
+
const total = report.findings.length;
|
|
9641
|
+
if (!total) {
|
|
9642
|
+
return `Checked ${what} \u2014 nothing found.`;
|
|
9643
|
+
}
|
|
9644
|
+
const high = report.counts.high;
|
|
9645
|
+
return `Checked ${what} \u2014 ${total} ${total === 1 ? "thing" : "things"} to look at` + (high ? `, ${high} of them serious.` : ".");
|
|
9646
|
+
}
|
|
9647
|
+
|
|
9648
|
+
// src/commands/scan.ts
|
|
9649
|
+
function registerScan(program3) {
|
|
9650
|
+
program3.command("scan").description("Check this folder for known-bad dependencies, leaked secrets and over-broad permissions").option("--check", "exit non-zero if anything serious is found", false).option(
|
|
9651
|
+
"--only <checks>",
|
|
9652
|
+
"comma-separated subset: deps, secrets, permissions"
|
|
9653
|
+
).option(
|
|
9654
|
+
"--staged",
|
|
9655
|
+
"look at staged changes only, rather than everything not yet committed",
|
|
9656
|
+
false
|
|
9657
|
+
).action(
|
|
9658
|
+
action(async ({ ctx, opts }) => {
|
|
9659
|
+
const only = parseOnly(opts.only);
|
|
9660
|
+
const findings = [];
|
|
9661
|
+
const checked = [];
|
|
9662
|
+
const skipped = [];
|
|
9663
|
+
if (only.has("secrets")) runSecrets(ctx.cwd, !!opts.staged, findings, checked, skipped);
|
|
9664
|
+
if (only.has("deps")) runDeps(ctx.cwd, findings, checked, skipped);
|
|
9665
|
+
if (only.has("permissions")) runPermissions(ctx.cwd, findings, checked, skipped);
|
|
9666
|
+
const report = buildReport({ findings, checked, skipped });
|
|
9667
|
+
const summary = scanSummary(report);
|
|
9668
|
+
ok({ ...report, summary }, () => print2(report, summary));
|
|
9669
|
+
if (opts.check && !report.ok) {
|
|
9670
|
+
throw new WorkserError(summary, { code: "bad_request" });
|
|
9671
|
+
}
|
|
9672
|
+
})
|
|
9673
|
+
);
|
|
9674
|
+
}
|
|
9675
|
+
function parseOnly(raw) {
|
|
9676
|
+
const all = ["deps", "secrets", "permissions"];
|
|
9677
|
+
if (typeof raw !== "string" || !raw.trim()) return new Set(all);
|
|
9678
|
+
const wanted = raw.split(",").map((s) => s.trim().toLowerCase()).filter((s) => all.includes(s));
|
|
9679
|
+
if (!wanted.length) {
|
|
9680
|
+
throw new WorkserError(
|
|
9681
|
+
`--only takes any of: ${all.join(", ")}.`,
|
|
9682
|
+
{ code: "bad_request" }
|
|
9683
|
+
);
|
|
9684
|
+
}
|
|
9685
|
+
return new Set(wanted);
|
|
9686
|
+
}
|
|
9687
|
+
function runSecrets(cwd, staged, findings, checked, skipped) {
|
|
9688
|
+
const args = staged ? ["diff", "--cached", "--unified=0"] : (
|
|
9689
|
+
// Against HEAD, so it covers both staged and unstaged work: the agent
|
|
9690
|
+
// that just wrote the key has not staged anything.
|
|
9691
|
+
["diff", "HEAD", "--unified=0"]
|
|
9692
|
+
);
|
|
9693
|
+
const diff = git(cwd, args);
|
|
9694
|
+
if (diff === null) {
|
|
9695
|
+
skipped.push({
|
|
9696
|
+
check: "secrets",
|
|
9697
|
+
reason: "This folder isn\u2019t a git repository yet, so there are no changes to look through."
|
|
9698
|
+
});
|
|
9699
|
+
return;
|
|
9700
|
+
}
|
|
9701
|
+
checked.push("secrets");
|
|
9702
|
+
findings.push(...secretFindings(diff));
|
|
9703
|
+
}
|
|
9704
|
+
function runDeps(cwd, findings, checked, skipped) {
|
|
9705
|
+
const hasLock = ["package-lock.json", "npm-shrinkwrap.json"].some(
|
|
9706
|
+
(f) => existsSync3(join4(cwd, f))
|
|
9707
|
+
);
|
|
9708
|
+
if (!hasLock) {
|
|
9709
|
+
skipped.push({
|
|
9710
|
+
check: "deps",
|
|
9711
|
+
reason: existsSync3(join4(cwd, "package.json")) ? "There\u2019s no package-lock.json, so the exact versions in use aren\u2019t known. Run `npm install` once." : "This folder has no npm packages to check."
|
|
9712
|
+
});
|
|
9713
|
+
return;
|
|
9714
|
+
}
|
|
9715
|
+
const res = spawnSync2("npm", ["audit", "--json", "--audit-level=high"], {
|
|
9716
|
+
cwd,
|
|
9717
|
+
encoding: "utf8",
|
|
9718
|
+
timeout: 6e4,
|
|
9719
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
9720
|
+
// `npm audit` exits 1 when it FINDS something, which is a successful run.
|
|
9721
|
+
// The failures that matter are the ones with no JSON on stdout.
|
|
9722
|
+
});
|
|
9723
|
+
const out = (res.stdout || "").trim();
|
|
9724
|
+
if (!out || res.error) {
|
|
9725
|
+
skipped.push({
|
|
9726
|
+
check: "deps",
|
|
9727
|
+
reason: "Couldn\u2019t reach the package registry, so known problems in your dependencies weren\u2019t checked. This needs an internet connection."
|
|
9728
|
+
});
|
|
9729
|
+
return;
|
|
9730
|
+
}
|
|
9731
|
+
checked.push("deps");
|
|
9732
|
+
findings.push(...depFindings(out));
|
|
9733
|
+
}
|
|
9734
|
+
function runPermissions(cwd, findings, checked, skipped) {
|
|
9735
|
+
const paths = listRepoFiles2(cwd).filter(isReadable);
|
|
9736
|
+
const files = [];
|
|
9737
|
+
for (const path of paths.slice(0, 1500)) {
|
|
9738
|
+
try {
|
|
9739
|
+
const content = readFileSync5(join4(cwd, path), "utf8");
|
|
9740
|
+
if (content.length > 4e5) continue;
|
|
9741
|
+
files.push({ path, content });
|
|
9742
|
+
} catch {
|
|
9743
|
+
}
|
|
9744
|
+
}
|
|
9745
|
+
const tracked = git(cwd, ["ls-files"]);
|
|
9746
|
+
checked.push("permissions");
|
|
9747
|
+
findings.push(
|
|
9748
|
+
...permissionFindings(
|
|
9749
|
+
files,
|
|
9750
|
+
tracked === null ? [] : tracked.split("\n").filter(Boolean)
|
|
9751
|
+
)
|
|
9752
|
+
);
|
|
9753
|
+
if (tracked === null) {
|
|
9754
|
+
skipped.push({
|
|
9755
|
+
check: "permissions",
|
|
9756
|
+
reason: "Not a git repository, so we couldn\u2019t check whether a .env file has been committed."
|
|
9757
|
+
});
|
|
9758
|
+
}
|
|
9759
|
+
}
|
|
9760
|
+
function print2(report, summary) {
|
|
9761
|
+
for (const s of report.skipped) {
|
|
9762
|
+
line(`${import_picocolors34.default.yellow("not checked")} ${s.check}${import_picocolors34.default.dim(` \u2014 ${s.reason}`)}`);
|
|
9763
|
+
}
|
|
9764
|
+
for (const f of report.findings) {
|
|
9765
|
+
const where = f.file ? import_picocolors34.default.dim(` ${f.file}${f.line ? `:${f.line}` : ""}`) : "";
|
|
9766
|
+
line(`${severityTag(f.severity)} ${f.title}${where}`);
|
|
9767
|
+
line(` ${import_picocolors34.default.dim(f.fix)}`);
|
|
9768
|
+
}
|
|
9769
|
+
if (report.findings.length || report.skipped.length) line("");
|
|
9770
|
+
if (report.ok && !report.skipped.length) success(summary);
|
|
9771
|
+
else if (report.ok) line(import_picocolors34.default.yellow(summary));
|
|
9772
|
+
else line(import_picocolors34.default.red(summary));
|
|
9773
|
+
}
|
|
9774
|
+
function severityTag(severity) {
|
|
9775
|
+
if (severity === "high") return import_picocolors34.default.red("serious ");
|
|
9776
|
+
if (severity === "medium") return import_picocolors34.default.yellow("worth fixing");
|
|
9777
|
+
return import_picocolors34.default.dim("minor ");
|
|
9778
|
+
}
|
|
9779
|
+
function git(cwd, args) {
|
|
9780
|
+
try {
|
|
9781
|
+
const res = spawnSync2("git", args, {
|
|
9782
|
+
cwd,
|
|
9783
|
+
encoding: "utf8",
|
|
9784
|
+
timeout: 2e4,
|
|
9785
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
9786
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
9787
|
+
});
|
|
9788
|
+
if (res.error || res.status !== 0) return null;
|
|
9789
|
+
return res.stdout || "";
|
|
9790
|
+
} catch {
|
|
9791
|
+
return null;
|
|
9792
|
+
}
|
|
9793
|
+
}
|
|
9794
|
+
var READABLE = /\.(?:[jt]sx?|mjs|cjs|json|ya?ml|toml|env|py|rb|go|rs|java|php|sh|sql|md|txt|html|css)$/i;
|
|
9795
|
+
function isReadable(path) {
|
|
9796
|
+
return READABLE.test(path);
|
|
9797
|
+
}
|
|
9798
|
+
function listRepoFiles2(root, maxDepth = 8) {
|
|
9799
|
+
const SKIP = /* @__PURE__ */ new Set([
|
|
9800
|
+
"node_modules",
|
|
9801
|
+
".git",
|
|
9802
|
+
".next",
|
|
9803
|
+
"dist",
|
|
9804
|
+
"build",
|
|
9805
|
+
".vercel",
|
|
9806
|
+
"__pycache__",
|
|
9807
|
+
".venv",
|
|
9808
|
+
"venv"
|
|
9809
|
+
]);
|
|
9810
|
+
const out = [];
|
|
9811
|
+
const walk = (dir, depth) => {
|
|
9812
|
+
if (depth > maxDepth || out.length > 5e3) return;
|
|
9813
|
+
let entries;
|
|
9814
|
+
try {
|
|
9815
|
+
entries = readdirSync2(dir);
|
|
9816
|
+
} catch {
|
|
9817
|
+
return;
|
|
9818
|
+
}
|
|
9819
|
+
for (const name of entries) {
|
|
9820
|
+
if (SKIP.has(name)) continue;
|
|
9821
|
+
if (name.startsWith(".") && !name.startsWith(".env")) continue;
|
|
9822
|
+
const full = join4(dir, name);
|
|
9823
|
+
let isDir = false;
|
|
9824
|
+
try {
|
|
9825
|
+
isDir = statSync3(full).isDirectory();
|
|
9826
|
+
} catch {
|
|
9827
|
+
continue;
|
|
9828
|
+
}
|
|
9829
|
+
if (isDir) walk(full, depth + 1);
|
|
9830
|
+
else out.push(relative2(root, full).split(sep2).join("/"));
|
|
9831
|
+
}
|
|
9832
|
+
};
|
|
9833
|
+
walk(root, 0);
|
|
9834
|
+
return out;
|
|
9835
|
+
}
|
|
9836
|
+
|
|
9837
|
+
// src/commands/health.ts
|
|
9838
|
+
var import_picocolors35 = __toESM(require_picocolors(), 1);
|
|
9839
|
+
function registerHealth(program3) {
|
|
9840
|
+
program3.command("health").description("Check that the published apps in this project are still answering").option("--app <webAppId>", "check one app rather than all of them").action(
|
|
9841
|
+
action(async ({ ctx, opts }) => {
|
|
9842
|
+
requireDaemon(ctx, "health", "checks your published sites from this computer");
|
|
9843
|
+
const path = opts.app ? `/v1/apps/${encodeURIComponent(String(opts.app))}/health` : `/v1/health`;
|
|
9844
|
+
const res = await api(ctx, path);
|
|
9845
|
+
ok(res, () => print3(res));
|
|
9846
|
+
if (res?.checks?.some((c) => !c.ok)) process.exitCode = 1;
|
|
9847
|
+
})
|
|
9848
|
+
);
|
|
9849
|
+
}
|
|
9850
|
+
function print3(res) {
|
|
9851
|
+
if (!res?.checks?.length) {
|
|
9852
|
+
line(import_picocolors35.default.dim(res?.note ?? "Nothing to check."));
|
|
9853
|
+
return;
|
|
9854
|
+
}
|
|
9855
|
+
for (const c of res.checks) {
|
|
9856
|
+
const mark = c.ok ? import_picocolors35.default.green("up ") : import_picocolors35.default.red("down");
|
|
9857
|
+
const timing = import_picocolors35.default.dim(`${c.ms}ms`);
|
|
9858
|
+
const detail = c.ok ? timing : import_picocolors35.default.dim(`${c.error ?? "no answer"}${c.failures > 1 ? ` \xB7 ${c.failures} in a row` : ""}`);
|
|
9859
|
+
line(` ${mark} ${c.appName} ${import_picocolors35.default.dim(`(${c.environment})`)} ${c.url} ${detail}`);
|
|
9860
|
+
if (c.incidentOpened) {
|
|
9861
|
+
line(import_picocolors35.default.yellow(` An incident has been opened on the board for this.`));
|
|
9862
|
+
}
|
|
9863
|
+
}
|
|
9864
|
+
const down = res.checks.filter((c) => !c.ok);
|
|
9865
|
+
line("");
|
|
9866
|
+
if (!down.length) {
|
|
9867
|
+
success(
|
|
9868
|
+
`Everything published is answering (${res.checks.length} ${res.checks.length === 1 ? "address" : "addresses"} checked).`
|
|
9869
|
+
);
|
|
9870
|
+
return;
|
|
9871
|
+
}
|
|
9872
|
+
const production = down.filter((c) => c.environment === "production").length;
|
|
9873
|
+
line(
|
|
9874
|
+
import_picocolors35.default.red(
|
|
9875
|
+
`${down.length} of ${res.checks.length} not answering` + (production ? ` \u2014 ${production} customer-facing.` : " (preview only).")
|
|
9876
|
+
)
|
|
9877
|
+
);
|
|
9878
|
+
}
|
|
9879
|
+
|
|
9880
|
+
// src/commands/urls.ts
|
|
9881
|
+
var import_picocolors36 = __toESM(require_picocolors(), 1);
|
|
9882
|
+
function registerUrls(program3) {
|
|
9883
|
+
program3.command("urls").description("The stable preview and production addresses of every app in this project").option("--app <webAppId>", "just one app").action(
|
|
9884
|
+
action(async ({ ctx, opts }) => {
|
|
9885
|
+
const projectId = requireProject(ctx);
|
|
9886
|
+
const apps = await api(ctx, `/v1/apps`, {
|
|
9887
|
+
query: { project: projectId }
|
|
9888
|
+
});
|
|
9889
|
+
const list = Array.isArray(apps) ? apps : apps?.apps ?? [];
|
|
9890
|
+
const wanted = opts.app ? list.filter((a) => a.id === String(opts.app)) : list;
|
|
9891
|
+
const rows = urlRows(wanted);
|
|
9892
|
+
const summary = urlsSummary(rows);
|
|
9893
|
+
ok({ rows, summary }, () => {
|
|
9894
|
+
for (const row of rows) {
|
|
9895
|
+
const label = import_picocolors36.default.dim(row.environment.padEnd(10));
|
|
9896
|
+
const value = row.url ? import_picocolors36.default.cyan(row.url) : import_picocolors36.default.dim(row.note ?? "not published");
|
|
9897
|
+
line(` ${row.appName.padEnd(22)} ${label} ${value}`);
|
|
9898
|
+
}
|
|
9899
|
+
line("");
|
|
9900
|
+
if (rows.some((r) => r.url)) success(summary);
|
|
9901
|
+
else line(import_picocolors36.default.yellow(summary));
|
|
9902
|
+
});
|
|
9903
|
+
})
|
|
9904
|
+
);
|
|
9905
|
+
}
|
|
9906
|
+
|
|
9907
|
+
// src/commands/deployments.ts
|
|
9908
|
+
var import_picocolors37 = __toESM(require_picocolors(), 1);
|
|
9909
|
+
function registerDeployments(program3) {
|
|
9910
|
+
const cmd = program3.command("deployments").description("Deployment history, and putting a build in front of customers");
|
|
9911
|
+
cmd.command("list").description("What has been built, newest first").option("--app <webAppId>", "just one app (default: every app in the project)").option("--env <environment>", "preview or production").option("--limit <n>", "how many to show", "20").action(
|
|
9912
|
+
action(async ({ ctx, opts }) => {
|
|
9913
|
+
const projectId = requireProject(ctx);
|
|
9914
|
+
const environment = readEnv2(opts.env, "deployments list");
|
|
9915
|
+
const res = await api(ctx, `/v1/projects/${projectId}/deployments`, {
|
|
9916
|
+
query: {
|
|
9917
|
+
...opts.app ? { webAppId: String(opts.app) } : {},
|
|
9918
|
+
...environment ? { environment } : {},
|
|
9919
|
+
limit: String(opts.limit ?? "20")
|
|
9920
|
+
}
|
|
9921
|
+
});
|
|
9922
|
+
const items = res?.deployments ?? [];
|
|
9923
|
+
ok(res, () => {
|
|
9924
|
+
if (!items.length) {
|
|
9925
|
+
return line(
|
|
9926
|
+
import_picocolors37.default.dim(
|
|
9927
|
+
environment ? `Nothing has been deployed to ${environment} yet.` : "Nothing has been deployed yet. `workser deploy` builds the first one."
|
|
9928
|
+
)
|
|
9929
|
+
);
|
|
9930
|
+
}
|
|
9931
|
+
for (const d of items) line(formatDeployment(d));
|
|
9932
|
+
});
|
|
9933
|
+
})
|
|
9934
|
+
);
|
|
9935
|
+
cmd.command("inspect <id>").description("One deployment in full, with its build log").option("--logs", "include the build output", false).action(
|
|
9936
|
+
action(async ({ ctx, args, opts }) => {
|
|
9937
|
+
const projectId = requireProject(ctx);
|
|
9938
|
+
const id = String(args[0]);
|
|
9939
|
+
const dep = await api(ctx, `/v1/deployments/${encodeURIComponent(id)}`);
|
|
9940
|
+
const logs = opts.logs ? await api(
|
|
9941
|
+
ctx,
|
|
9942
|
+
`/v1/projects/${projectId}/deployments/${encodeURIComponent(id)}/logs`
|
|
9943
|
+
).catch(() => null) : null;
|
|
9944
|
+
ok({ ...dep, logs }, () => {
|
|
9945
|
+
line(formatDeployment(dep));
|
|
9946
|
+
if (dep?.error_message) line(import_picocolors37.default.red(` ${dep.error_message}`));
|
|
9947
|
+
const events = logs?.events ?? [];
|
|
9948
|
+
for (const e of events) {
|
|
9949
|
+
line(` ${import_picocolors37.default.dim(String(e.type ?? "log"))} ${e.text ?? ""}`);
|
|
9950
|
+
}
|
|
9951
|
+
if (opts.logs && !events.length) {
|
|
9952
|
+
line(import_picocolors37.default.dim(" That build produced no output."));
|
|
9953
|
+
}
|
|
9954
|
+
});
|
|
9955
|
+
})
|
|
9956
|
+
);
|
|
9957
|
+
cmd.command("promote").description("Put the latest ready build in front of customers (asks you first)").option("--app <webAppId>", "which app (defaults to the primary app)").action(
|
|
9958
|
+
action(async ({ ctx, opts }) => {
|
|
9959
|
+
const projectId = requireProject(ctx);
|
|
9960
|
+
const res = await api(ctx, `/v1/projects/${projectId}/deployments/promote`, {
|
|
9961
|
+
body: { ...opts.app ? { webAppId: String(opts.app) } : {} }
|
|
9962
|
+
});
|
|
9963
|
+
ok(res, () => printPromoted(res, null));
|
|
9964
|
+
})
|
|
9965
|
+
);
|
|
9966
|
+
cmd.command("rollback <version>").description("Put an earlier version back in front of customers (asks you first)").option("--app <webAppId>", "which app (defaults to the primary app)").action(
|
|
9967
|
+
action(async ({ ctx, args, opts }) => {
|
|
9968
|
+
const projectId = requireProject(ctx);
|
|
9969
|
+
const version = Number(args[0]);
|
|
9970
|
+
if (!Number.isInteger(version) || version < 1) {
|
|
9971
|
+
throw new WorkserError(
|
|
9972
|
+
`"${args[0]}" is not a version number. \`workser deployments list\` shows them.`,
|
|
9973
|
+
{ code: "bad_input" }
|
|
9974
|
+
);
|
|
9975
|
+
}
|
|
9976
|
+
const res = await api(ctx, `/v1/projects/${projectId}/deployments/promote`, {
|
|
9977
|
+
body: {
|
|
9978
|
+
version,
|
|
9979
|
+
...opts.app ? { webAppId: String(opts.app) } : {}
|
|
9980
|
+
}
|
|
9981
|
+
});
|
|
9982
|
+
ok(res, () => printPromoted(res, version));
|
|
9983
|
+
})
|
|
9984
|
+
);
|
|
9985
|
+
}
|
|
9986
|
+
function readEnv2(raw, verb) {
|
|
9987
|
+
const parsed = parseDeployEnvironment(raw, verb);
|
|
9988
|
+
if (!parsed.ok) throw new WorkserError(parsed.error, { code: "bad_input" });
|
|
9989
|
+
return parsed.value;
|
|
9990
|
+
}
|
|
9991
|
+
function printPromoted(res, version) {
|
|
9992
|
+
if (!res) return;
|
|
9993
|
+
const what = version === null ? "the latest build" : `version ${version}`;
|
|
9994
|
+
const url = res.url ?? res.vercel_url;
|
|
9995
|
+
success(`Production is being rebuilt from ${what}.`);
|
|
9996
|
+
if (url) line(import_picocolors37.default.dim(`It will be at ${url}`));
|
|
9997
|
+
line(import_picocolors37.default.dim("`workser deploy status` follows it."));
|
|
9998
|
+
}
|
|
9999
|
+
function formatDeployment(d) {
|
|
10000
|
+
if (!d) return "";
|
|
10001
|
+
const version = d.version !== void 0 ? import_picocolors37.default.yellow(`v${d.version}`) : import_picocolors37.default.dim("v?");
|
|
10002
|
+
const env = import_picocolors37.default.dim((d.environment ?? "?").padEnd(10));
|
|
10003
|
+
const app = d.webAppName ? `${d.webAppName} ` : "";
|
|
10004
|
+
const when = import_picocolors37.default.dim(formatTime2(d.created_at));
|
|
10005
|
+
const url = d.url ? " " + import_picocolors37.default.cyan(d.url) : "";
|
|
10006
|
+
return `${version} ${env} ${colorStatus(d.status ?? "")} ${app}${when}${url}`;
|
|
10007
|
+
}
|
|
10008
|
+
function formatTime2(t) {
|
|
10009
|
+
if (!t) return "\u2014";
|
|
10010
|
+
const d = new Date(t);
|
|
10011
|
+
return Number.isNaN(d.getTime()) ? String(t) : d.toISOString().replace("T", " ").replace(/\.\d+Z$/, "Z");
|
|
10012
|
+
}
|
|
10013
|
+
|
|
10014
|
+
// src/commands/usage.ts
|
|
10015
|
+
var import_picocolors38 = __toESM(require_picocolors(), 1);
|
|
10016
|
+
|
|
10017
|
+
// src/usage.ts
|
|
10018
|
+
var NEAR_LIMIT_FRACTION = 0.8;
|
|
10019
|
+
function isUnlimited(limit) {
|
|
10020
|
+
return limit === null || limit === void 0 || !Number.isFinite(limit) || limit >= Number.MAX_SAFE_INTEGER;
|
|
10021
|
+
}
|
|
10022
|
+
function fractionUsed(d) {
|
|
10023
|
+
if (d.used === null) return null;
|
|
10024
|
+
if (isUnlimited(d.limit)) return null;
|
|
10025
|
+
const limit = d.limit;
|
|
10026
|
+
if (limit <= 0) return d.used > 0 ? 1 : 0;
|
|
10027
|
+
return d.used / limit;
|
|
10028
|
+
}
|
|
10029
|
+
function usageState(d) {
|
|
10030
|
+
if (d.used === null) return "unknown";
|
|
10031
|
+
const fraction = fractionUsed(d);
|
|
10032
|
+
if (fraction === null) return "fine";
|
|
10033
|
+
if (fraction >= 1) return "over";
|
|
10034
|
+
if (fraction >= NEAR_LIMIT_FRACTION) return "near";
|
|
10035
|
+
return "fine";
|
|
10036
|
+
}
|
|
10037
|
+
function formatAmount(value, unit) {
|
|
10038
|
+
if (unit === "count") return String(Math.round(value));
|
|
10039
|
+
if (Number.isInteger(value)) return `${value} GB`;
|
|
10040
|
+
if (value < 0.01) return "<0.01 GB";
|
|
10041
|
+
if (value < 10) return `${value.toFixed(2)} GB`;
|
|
10042
|
+
return `${value.toFixed(1)} GB`;
|
|
10043
|
+
}
|
|
10044
|
+
function bar(d, width = 20) {
|
|
10045
|
+
const fraction = fractionUsed(d);
|
|
10046
|
+
if (fraction === null) return "";
|
|
10047
|
+
const filled = Math.min(width, Math.round(fraction * width));
|
|
10048
|
+
return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
|
|
10049
|
+
}
|
|
10050
|
+
function dimensionLine(d) {
|
|
10051
|
+
if (d.used === null) {
|
|
10052
|
+
return `${d.label}: not measured${d.note ? ` \u2014 ${d.note}` : ""}`;
|
|
10053
|
+
}
|
|
10054
|
+
const used = formatAmount(d.used, d.unit);
|
|
10055
|
+
if (isUnlimited(d.limit)) return `${d.label}: ${used} (no limit on this plan)`;
|
|
10056
|
+
const limit = formatAmount(d.limit, d.unit);
|
|
10057
|
+
if (usageState(d) === "over") {
|
|
10058
|
+
return d.kind === "hard" ? `${d.label}: ${used} of ${limit} \u2014 at the limit` : `${d.label}: ${used} of ${limit} included \u2014 the rest is billed as extra`;
|
|
10059
|
+
}
|
|
10060
|
+
return `${d.label}: ${used} of ${limit}`;
|
|
10061
|
+
}
|
|
10062
|
+
function usageSummary(report) {
|
|
10063
|
+
const dims = report.dimensions ?? [];
|
|
10064
|
+
const unknown = dims.filter((d) => d.used === null);
|
|
10065
|
+
const over = dims.filter((d) => usageState(d) === "over");
|
|
10066
|
+
const near = dims.filter((d) => usageState(d) === "near");
|
|
10067
|
+
const parts = [];
|
|
10068
|
+
if (over.length) {
|
|
10069
|
+
parts.push(
|
|
10070
|
+
`${over.length === 1 ? "One thing is" : `${over.length} things are`} over the ${report.tier} plan's limit`
|
|
10071
|
+
);
|
|
10072
|
+
} else if (near.length) {
|
|
10073
|
+
parts.push(
|
|
10074
|
+
`${near.length === 1 ? "One thing is" : `${near.length} things are`} close to the ${report.tier} plan's limit`
|
|
10075
|
+
);
|
|
10076
|
+
} else if (dims.some((d) => d.used !== null)) {
|
|
10077
|
+
parts.push(`Everything is comfortably inside the ${report.tier} plan`);
|
|
10078
|
+
}
|
|
10079
|
+
if (unknown.length) {
|
|
10080
|
+
parts.push(
|
|
10081
|
+
`${unknown.length} ${unknown.length === 1 ? "figure" : "figures"} could not be read`
|
|
10082
|
+
);
|
|
10083
|
+
}
|
|
10084
|
+
if (!parts.length) return "Nothing could be measured.";
|
|
10085
|
+
return `${parts.join("; ")}.`;
|
|
10086
|
+
}
|
|
10087
|
+
function shouldFail(report) {
|
|
10088
|
+
return (report.dimensions ?? []).some(
|
|
10089
|
+
(d) => d.kind === "hard" && usageState(d) === "over"
|
|
10090
|
+
);
|
|
10091
|
+
}
|
|
10092
|
+
|
|
10093
|
+
// src/commands/usage.ts
|
|
10094
|
+
function registerUsage(program3) {
|
|
10095
|
+
program3.command("usage").description("What this project and your plan are using \u2014 storage, projects, apps").action(
|
|
10096
|
+
action(async ({ ctx }) => {
|
|
10097
|
+
const projectId = requireProject(ctx);
|
|
10098
|
+
const report = await api(
|
|
10099
|
+
ctx,
|
|
10100
|
+
`/v1/projects/${projectId}/usage`
|
|
10101
|
+
);
|
|
10102
|
+
ok(report, () => print4(report));
|
|
10103
|
+
if (shouldFail(report)) process.exitCode = 1;
|
|
10104
|
+
})
|
|
10105
|
+
);
|
|
10106
|
+
}
|
|
10107
|
+
function print4(report) {
|
|
10108
|
+
const dims = report.dimensions ?? [];
|
|
10109
|
+
if (!dims.length) {
|
|
10110
|
+
return line(import_picocolors38.default.dim("Nothing to measure for this project yet."));
|
|
10111
|
+
}
|
|
10112
|
+
const width = Math.max(...dims.map((d) => d.label.length));
|
|
10113
|
+
for (const d of dims) {
|
|
10114
|
+
line(` ${colour(d)(dimensionLine(d).padEnd(0))}${gauge(d, width)}`);
|
|
10115
|
+
}
|
|
10116
|
+
line("");
|
|
10117
|
+
const summary = usageSummary(report);
|
|
10118
|
+
const worst = dims.map(usageState);
|
|
10119
|
+
if (worst.includes("over")) line(import_picocolors38.default.red(summary));
|
|
10120
|
+
else if (worst.includes("near") || worst.includes("unknown"))
|
|
10121
|
+
line(import_picocolors38.default.yellow(summary));
|
|
10122
|
+
else success(summary);
|
|
10123
|
+
}
|
|
10124
|
+
function gauge(d, _labelWidth) {
|
|
10125
|
+
const drawn = bar(d);
|
|
10126
|
+
return drawn ? ` ${import_picocolors38.default.dim(drawn)}` : "";
|
|
10127
|
+
}
|
|
10128
|
+
function colour(d) {
|
|
10129
|
+
switch (usageState(d)) {
|
|
10130
|
+
case "over":
|
|
10131
|
+
return d.kind === "hard" ? import_picocolors38.default.red : import_picocolors38.default.yellow;
|
|
10132
|
+
case "near":
|
|
10133
|
+
return import_picocolors38.default.yellow;
|
|
10134
|
+
case "unknown":
|
|
10135
|
+
return import_picocolors38.default.dim;
|
|
10136
|
+
default:
|
|
10137
|
+
return (s) => s;
|
|
10138
|
+
}
|
|
10139
|
+
}
|
|
10140
|
+
|
|
7743
10141
|
// src/index.ts
|
|
7744
10142
|
var pkg = {
|
|
7745
|
-
version: true ? "0.
|
|
10143
|
+
version: true ? "0.6.1" : "0.0.0-dev"
|
|
7746
10144
|
};
|
|
7747
10145
|
var program2 = new Command();
|
|
7748
10146
|
program2.name("workser").description(
|
|
@@ -7775,6 +10173,13 @@ registerOpen(program2);
|
|
|
7775
10173
|
registerDoctor(program2);
|
|
7776
10174
|
registerAgent(program2);
|
|
7777
10175
|
registerVerify(program2);
|
|
10176
|
+
registerApi(program2);
|
|
10177
|
+
registerAnalysis(program2);
|
|
10178
|
+
registerScan(program2);
|
|
10179
|
+
registerHealth(program2);
|
|
10180
|
+
registerUrls(program2);
|
|
10181
|
+
registerDeployments(program2);
|
|
10182
|
+
registerUsage(program2);
|
|
7778
10183
|
registerCheckpoint(program2);
|
|
7779
10184
|
registerSync(program2);
|
|
7780
10185
|
registerWorkflow(program2);
|
|
@@ -7791,6 +10196,10 @@ registerTask(program2);
|
|
|
7791
10196
|
registerDecision(program2);
|
|
7792
10197
|
registerDoc(program2);
|
|
7793
10198
|
registerDesign(program2);
|
|
10199
|
+
configureOutput({
|
|
10200
|
+
json: process.argv.includes("--json"),
|
|
10201
|
+
quiet: process.argv.includes("--quiet") || process.argv.includes("-q")
|
|
10202
|
+
});
|
|
7794
10203
|
try {
|
|
7795
10204
|
assertRoleMayRun(process.argv.slice(2));
|
|
7796
10205
|
} catch (e) {
|