@workser/cli 0.3.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2516 -315
- package/package.json +4 -2
- 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 +20 -43
- package/skills/workser/reference/tasks.md +21 -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,16 +4685,16 @@ real scope, not just a repeat of the task.
|
|
|
4189
4685
|
},
|
|
4190
4686
|
{
|
|
4191
4687
|
topic: "sdlc-entities",
|
|
4192
|
-
title: "Board cards, decisions
|
|
4688
|
+
title: "Board cards, decisions and requirements",
|
|
4193
4689
|
summary: "Read what this project already tracks and decided, keep the Board honest as you work, and record what a future maintainer will need.",
|
|
4194
|
-
commands: ["board", "decision", "requirement"
|
|
4690
|
+
commands: ["board", "decision", "requirement"],
|
|
4195
4691
|
source: "skills/workser/reference/sdlc-entities.md",
|
|
4196
|
-
body: `# Board cards, decisions
|
|
4692
|
+
body: `# Board cards, 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 Board
|
|
4200
|
-
|
|
4201
|
-
|
|
4695
|
+
the Orbit desktop's Board and Project Memory panels use, so anything here appears
|
|
4696
|
+
there too \u2014 and, inside an Orbit-spawned run, as an inline card in the
|
|
4697
|
+
conversation. Documents have their own guide: \`workser help docs\`.
|
|
4202
4698
|
|
|
4203
4699
|
\`\`\`
|
|
4204
4700
|
workser board list [--status <value>] [--label <value>] [--limit <n>]
|
|
@@ -4221,11 +4717,6 @@ workser requirement show <id>
|
|
|
4221
4717
|
workser requirement create <title> --body <text> [--status <text>]
|
|
4222
4718
|
workser requirement update <id> [--title <text>] [--body <text>] [--status <text>]
|
|
4223
4719
|
|
|
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
4720
|
\`\`\`
|
|
4230
4721
|
|
|
4231
4722
|
## Read first \u2014 this is the part that matters
|
|
@@ -4254,7 +4745,6 @@ workser board create "Phase 1 \u2014 schema + migration" \\
|
|
|
4254
4745
|
--description "Add orders/line_items tables and the migration." \\
|
|
4255
4746
|
--status in-progress --json
|
|
4256
4747
|
workser board create "Phase 2 \u2014 checkout API" --description "\u2026" --json
|
|
4257
|
-
workser board create "Phase 3 \u2014 cart UI" --description "\u2026" --json
|
|
4258
4748
|
|
|
4259
4749
|
# the plan itself, ONE doc, deliberately NOT linked to a card
|
|
4260
4750
|
workser doc create "Checkout \u2014 implementation plan" --markdown "$(cat plan.md)" --json
|
|
@@ -4264,8 +4754,8 @@ workser decision create "Carts live server-side" --context "\u2026" --decision "
|
|
|
4264
4754
|
\`\`\`
|
|
4265
4755
|
|
|
4266
4756
|
**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.
|
|
4757
|
+
its card and is *hidden* from the Docs panel; a plan spanning three phases
|
|
4758
|
+
belongs to the project, not to phase 1.
|
|
4269
4759
|
|
|
4270
4760
|
The bar: if the user closed this conversation now, the Board should still show
|
|
4271
4761
|
what's left and the doc should still explain the plan to whoever continues it.
|
|
@@ -4291,22 +4781,19 @@ workser board create "Fix the login bug" --status in-progress --priority high \\
|
|
|
4291
4781
|
\`\`\`
|
|
4292
4782
|
|
|
4293
4783
|
\`board update\` replaces the labels you pass rather than merging them, and
|
|
4294
|
-
touches only the fields you name. There is no \`board delete
|
|
4295
|
-
terminal state
|
|
4296
|
-
to do in Orbit.
|
|
4784
|
+
touches only the fields you name. There is no \`board delete\`: \`done\` is the
|
|
4785
|
+
terminal state, and removing a card the user filed is theirs to do in Orbit.
|
|
4297
4786
|
|
|
4298
4787
|
## Decisions are append-only
|
|
4299
4788
|
|
|
4300
4789
|
\`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.
|
|
4790
|
+
\`--context\` is why it came up, \`--decision\` what was decided, \`--consequences\`
|
|
4791
|
+
the follow-on effects. There is deliberately **no \`decision update\`** \u2014 a record
|
|
4792
|
+
states what was decided at a point in time. When it stops being right, record a
|
|
4793
|
+
new decision that supersedes it and say so in its \`--context\`. Editing the
|
|
4794
|
+
history is how a decision log stops being worth reading.
|
|
4307
4795
|
|
|
4308
|
-
Requirements
|
|
4309
|
-
\`update\`.
|
|
4796
|
+
Requirements legitimately move along, so they do have \`update\`.
|
|
4310
4797
|
|
|
4311
4798
|
\`\`\`
|
|
4312
4799
|
workser requirement create "Support SSO" --body "Enterprise customers need SAML." \\
|
|
@@ -4317,25 +4804,11 @@ workser requirement update <id> --status done
|
|
|
4317
4804
|
## Docs
|
|
4318
4805
|
|
|
4319
4806
|
\`--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
|
|
4807
|
+
rich text the Docs panel renders and as a git-tracked mirror at
|
|
4808
|
+
\`.workser/docs/<id>.md\`; \`workser doc show <id> --markdown\` reports that path so
|
|
4809
|
+
you can read the file with your normal tools.
|
|
4334
4810
|
|
|
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.
|
|
4811
|
+
Revise trd saying "fix the bug" is not fixing the bug.
|
|
4339
4812
|
`
|
|
4340
4813
|
},
|
|
4341
4814
|
{
|
|
@@ -4429,6 +4902,7 @@ workser task subtask add <title> [--role <value>] [--kind <value>]
|
|
|
4429
4902
|
workser task subtask list [taskId]
|
|
4430
4903
|
workser task subtask update <id> [--title|--note|--role|--kind|--scope]
|
|
4431
4904
|
workser task subtask remove <id>
|
|
4905
|
+
workser task subtask send-back <id> --note <text> # redo it, and say why
|
|
4432
4906
|
|
|
4433
4907
|
workser task can-start [id] # may work begin? refuses until approved
|
|
4434
4908
|
workser task approval request # tell the owner the plan is ready
|
|
@@ -4487,6 +4961,75 @@ workser task done --summary "The report now shows cost per KOL, with six months
|
|
|
4487
4961
|
\`\`\`
|
|
4488
4962
|
|
|
4489
4963
|
Write the summary for someone who runs a business and does not read code.
|
|
4964
|
+
|
|
4965
|
+
## Sending a step back
|
|
4966
|
+
|
|
4967
|
+
A step that finished but is not good enough is **sent back**, not replaced:
|
|
4968
|
+
|
|
4969
|
+
\`\`\`
|
|
4970
|
+
workser task subtask send-back 3f2a\u2026 --note "The totals ignore refunds."
|
|
4971
|
+
\`\`\`
|
|
4972
|
+
|
|
4973
|
+
That puts it in the queue again as a **second attempt** on the same step. Two
|
|
4974
|
+
reasons it matters that this is not a new step:
|
|
4975
|
+
|
|
4976
|
+
- The owner's screen can then say *"1 send-back, fixed \u2014 2nd run passed"*. A
|
|
4977
|
+
replacement step says only that two steps exist, which tells them nothing
|
|
4978
|
+
about whether their team caught its own mistake.
|
|
4979
|
+
- \`--note\` is the reason, and it is recorded against the attempt being
|
|
4980
|
+
rejected. Without it the history can say a step ran twice but not why.
|
|
4981
|
+
|
|
4982
|
+
It refuses a step that is still working. Let it finish first \u2014 the run is
|
|
4983
|
+
still writing to it.
|
|
4984
|
+
`
|
|
4985
|
+
},
|
|
4986
|
+
{
|
|
4987
|
+
topic: "usage",
|
|
4988
|
+
title: "Usage \u2014 what is being used, against the plan",
|
|
4989
|
+
summary: "How much database, file storage, projects and apps are in use, and how close that is to what the plan allows.",
|
|
4990
|
+
commands: ["usage"],
|
|
4991
|
+
source: "skills/workser/reference/usage.md",
|
|
4992
|
+
body: `# Usage \u2014 what is being used, against the plan
|
|
4993
|
+
|
|
4994
|
+
\`\`\`
|
|
4995
|
+
workser usage # storage, projects, apps \u2014 and how close each is to the limit
|
|
4996
|
+
\`\`\`
|
|
4997
|
+
|
|
4998
|
+
Run it before you propose anything that adds to a count. "Create another
|
|
4999
|
+
project" is a plan you can only sensibly make if you know the plan allows two
|
|
5000
|
+
and two already exist.
|
|
5001
|
+
|
|
5002
|
+
## Two scopes in one answer, on purpose
|
|
5003
|
+
|
|
5004
|
+
* **Database and files are ORGANISATION-wide.** One pool across every project.
|
|
5005
|
+
There is no per-project storage limit, and reporting one would invent it.
|
|
5006
|
+
* **Projects, and apps in this project, are counted where they apply.** These
|
|
5007
|
+
are the limits people actually hit.
|
|
5008
|
+
|
|
5009
|
+
## Two kinds of limit, which do not mean the same thing
|
|
5010
|
+
|
|
5011
|
+
* **Hard cap** \u2014 projects, apps. Going over is **refused**. \`workser usage\`
|
|
5012
|
+
exits non-zero when one is reached, so a step can gate on it.
|
|
5013
|
+
* **Soft allowance** \u2014 database, files. Going over is **billed as extra**,
|
|
5014
|
+
never blocked. It does not fail the command, because a customer growing past
|
|
5015
|
+
their allowance should not have their automation start breaking that day.
|
|
5016
|
+
|
|
5017
|
+
## "not measured" is not zero
|
|
5018
|
+
|
|
5019
|
+
A figure that could not be read prints as \`not measured\`, with the reason, and
|
|
5020
|
+
draws no bar. Do not report it as \`0\`, and do not tell the user they have room
|
|
5021
|
+
based on it \u2014 nobody looked.
|
|
5022
|
+
|
|
5023
|
+
If a scan comes back with a figure missing, say which one and why. "Your
|
|
5024
|
+
database is using 2.5 GB of 10; the file total could not be read" is a useful
|
|
5025
|
+
sentence. "You are using 2.5 GB of 20" is not, and it is wrong.
|
|
5026
|
+
|
|
5027
|
+
## What to do with it
|
|
5028
|
+
|
|
5029
|
+
- Near a **soft** limit: tell the owner what the extra will cost them, and what
|
|
5030
|
+
is taking the space. Do not delete anything to make a number look better.
|
|
5031
|
+
- At a **hard** cap: say which plan raises it. Do not attempt the create \u2014 it
|
|
5032
|
+
will be refused, and a failed attempt reads to the owner as a broken product.
|
|
4490
5033
|
`
|
|
4491
5034
|
},
|
|
4492
5035
|
{
|
|
@@ -4746,6 +5289,18 @@ Commands that only read your account (projects, env, db, logs, status) work as n
|
|
|
4746
5289
|
{ code: "needs_local_app" }
|
|
4747
5290
|
);
|
|
4748
5291
|
}
|
|
5292
|
+
function requireDaemon(ctx, what, why) {
|
|
5293
|
+
if (ctx.mode === "daemon") return;
|
|
5294
|
+
throw new WorkserError(
|
|
5295
|
+
`\`workser ${what}\` ${why}, so it needs the Workser app running on this computer.
|
|
5296
|
+
|
|
5297
|
+
This shell is talking to ${ctx.endpoint} instead of a local app.
|
|
5298
|
+
|
|
5299
|
+
\u2022 On your own computer: open Workser and try again.
|
|
5300
|
+
\u2022 On a computer without Workser: install it from https://workser.ai/download and sign in.`,
|
|
5301
|
+
{ code: "needs_local_app" }
|
|
5302
|
+
);
|
|
5303
|
+
}
|
|
4749
5304
|
function requireProject(ctx) {
|
|
4750
5305
|
if (!ctx.projectId) {
|
|
4751
5306
|
throw new WorkserError(
|
|
@@ -5335,7 +5890,7 @@ import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
|
5335
5890
|
import { basename as basename2 } from "path";
|
|
5336
5891
|
function registerNeon(program3) {
|
|
5337
5892
|
const neon = program3.command("neon").description(
|
|
5338
|
-
"The project's own
|
|
5893
|
+
"The project's own database: branches, databases, compute, storage and functions"
|
|
5339
5894
|
);
|
|
5340
5895
|
neon.command("status").description(
|
|
5341
5896
|
"Whether this project can use Neon storage/functions (tenancy, toggles, region)"
|
|
@@ -5557,70 +6112,321 @@ function registerNeon(program3) {
|
|
|
5557
6112
|
ok(res, () => success(`Deleted function ${args[0]}.`));
|
|
5558
6113
|
})
|
|
5559
6114
|
);
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
ctx,
|
|
5564
|
-
`/v1/projects/${projectId}/neon-storage/buckets/${encodeURIComponent(bucket)}/presign`,
|
|
5565
|
-
{ body: { key, operation, expiresInSeconds } }
|
|
5566
|
-
);
|
|
5567
|
-
const url = res?.url ?? res?.signedUrl ?? res?.presignedUrl;
|
|
5568
|
-
if (!url) {
|
|
5569
|
-
throw new WorkserError(
|
|
5570
|
-
`The daemon did not return a presigned URL for ${key}.`,
|
|
5571
|
-
{ code: "unexpected_response", details: res }
|
|
5572
|
-
);
|
|
5573
|
-
}
|
|
5574
|
-
return { url, headers: res?.headers };
|
|
5575
|
-
}
|
|
5576
|
-
|
|
5577
|
-
// src/commands/env.ts
|
|
5578
|
-
var import_picocolors9 = __toESM(require_picocolors(), 1);
|
|
5579
|
-
import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
5580
|
-
function appQuery(opts) {
|
|
5581
|
-
const app = typeof opts?.app === "string" ? opts.app : "";
|
|
5582
|
-
return app ? `?webAppId=${encodeURIComponent(app)}` : "";
|
|
5583
|
-
}
|
|
5584
|
-
var APP_FLAG_HELP = "Which web app to target (defaults to the project's primary app)";
|
|
5585
|
-
function registerEnv(program3) {
|
|
5586
|
-
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(
|
|
5588
|
-
action(async ({ ctx, args, opts }) => {
|
|
6115
|
+
const branch = neon.command("branch").description("Copies of the project's data, to work on without touching it");
|
|
6116
|
+
branch.command("list").description("Every branch, with the one the app runs on marked").action(
|
|
6117
|
+
action(async ({ ctx }) => {
|
|
5589
6118
|
const projectId = requireProject(ctx);
|
|
5590
|
-
const
|
|
5591
|
-
|
|
5592
|
-
if (
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
const count = typeof res?.count === "number" ? res.count : pairs.length;
|
|
5599
|
-
ok(res, () => {
|
|
5600
|
-
success(`Set ${count} variable(s): ${pairs.map((p) => p.key).join(", ")}`);
|
|
5601
|
-
if (res?.usedDefault && res?.webAppName) {
|
|
5602
|
-
line(import_picocolors9.default.dim(`on ${res.webAppName} (primary app) \u2014 use --app to target another`));
|
|
6119
|
+
const items = await api(ctx, `/v1/projects/${projectId}/neon-branches`);
|
|
6120
|
+
ok(items, () => {
|
|
6121
|
+
if (!items?.length) return line(import_picocolors8.default.dim("No branches."));
|
|
6122
|
+
for (const b of items) {
|
|
6123
|
+
const mark = b.isProjectBranch ? import_picocolors8.default.green(" \u2190 the app runs on this") : "";
|
|
6124
|
+
line(
|
|
6125
|
+
`${import_picocolors8.default.bold(b.name ?? "?")} ${import_picocolors8.default.dim(b.id ?? "")}${b.parent_id ? import_picocolors8.default.dim(" from " + b.parent_id) : ""}${mark}`
|
|
6126
|
+
);
|
|
5603
6127
|
}
|
|
5604
6128
|
});
|
|
5605
6129
|
})
|
|
5606
6130
|
);
|
|
5607
|
-
|
|
6131
|
+
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(
|
|
6132
|
+
"--no-compute",
|
|
6133
|
+
"create it without compute \u2014 cheaper, and nothing can connect to it"
|
|
6134
|
+
).action(
|
|
5608
6135
|
action(async ({ ctx, args, opts }) => {
|
|
5609
6136
|
const projectId = requireProject(ctx);
|
|
5610
|
-
const
|
|
5611
|
-
|
|
5612
|
-
|
|
6137
|
+
const noCompute = opts.compute === false;
|
|
6138
|
+
const res = await api(ctx, `/v1/projects/${projectId}/neon-branches`, {
|
|
6139
|
+
body: {
|
|
6140
|
+
name: args[0],
|
|
6141
|
+
...opts.from ? { fromBranchId: String(opts.from) } : {},
|
|
6142
|
+
// Commander turns `--no-compute` into `compute: false`. The typed
|
|
6143
|
+
// opts bag is loose here, so the comparison is written against the
|
|
6144
|
+
// value rather than the declared type.
|
|
6145
|
+
...noCompute ? { withEndpoint: false } : {}
|
|
6146
|
+
}
|
|
6147
|
+
});
|
|
6148
|
+
ok(res, () => {
|
|
6149
|
+
success(`Created branch ${args[0]}.`);
|
|
6150
|
+
const id = res?.branch?.id ?? res?.id;
|
|
6151
|
+
if (id) line(import_picocolors8.default.dim(id));
|
|
6152
|
+
if (noCompute) {
|
|
6153
|
+
line(
|
|
6154
|
+
import_picocolors8.default.dim(
|
|
6155
|
+
"It has no compute, so nothing can connect to it until one is added."
|
|
6156
|
+
)
|
|
6157
|
+
);
|
|
6158
|
+
}
|
|
6159
|
+
});
|
|
6160
|
+
})
|
|
6161
|
+
);
|
|
6162
|
+
branch.command("reset <branchId>").description("Throw away a branch's changes and take its source data again (asks for approval)").action(
|
|
6163
|
+
action(async ({ ctx, args }) => {
|
|
6164
|
+
const projectId = requireProject(ctx);
|
|
6165
|
+
const res = await api(
|
|
6166
|
+
ctx,
|
|
6167
|
+
`/v1/projects/${projectId}/neon-branches/${encodeURIComponent(args[0])}/reset`,
|
|
6168
|
+
{ body: {} }
|
|
6169
|
+
);
|
|
6170
|
+
ok(res, () => success(`Reset ${args[0]} to its source.`));
|
|
6171
|
+
})
|
|
6172
|
+
);
|
|
6173
|
+
branch.command("rm <branchId>").description("Delete a branch and its data (asks for approval)").action(
|
|
6174
|
+
action(async ({ ctx, args }) => {
|
|
6175
|
+
const projectId = requireProject(ctx);
|
|
6176
|
+
const res = await api(
|
|
6177
|
+
ctx,
|
|
6178
|
+
`/v1/projects/${projectId}/neon-branches/${encodeURIComponent(args[0])}`,
|
|
6179
|
+
{ method: "DELETE" }
|
|
6180
|
+
);
|
|
6181
|
+
ok(res, () => success(`Deleted branch ${args[0]}.`));
|
|
6182
|
+
})
|
|
6183
|
+
);
|
|
6184
|
+
const database = neon.command("database").description("Databases on a branch");
|
|
6185
|
+
database.command("list").description("Databases on a branch (default: the one the app uses)").option("--branch <branchId>", "look at another branch").action(
|
|
6186
|
+
action(async ({ ctx, opts }) => {
|
|
6187
|
+
const projectId = requireProject(ctx);
|
|
6188
|
+
const items = await api(ctx, `/v1/projects/${projectId}/neon-databases`, {
|
|
6189
|
+
query: opts.branch ? { branchId: String(opts.branch) } : void 0
|
|
6190
|
+
});
|
|
6191
|
+
ok(items, () => {
|
|
6192
|
+
if (!items?.length) return line(import_picocolors8.default.dim("No databases."));
|
|
6193
|
+
for (const d of items) {
|
|
6194
|
+
const mark = d.isProjectDatabase ? import_picocolors8.default.green(" \u2190 the app connects to this") : "";
|
|
6195
|
+
line(`${import_picocolors8.default.bold(d.name ?? "?")}${import_picocolors8.default.dim(" owner " + (d.owner_name ?? "?"))}${mark}`);
|
|
6196
|
+
}
|
|
6197
|
+
});
|
|
6198
|
+
})
|
|
6199
|
+
);
|
|
6200
|
+
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(
|
|
6201
|
+
action(async ({ ctx, args, opts }) => {
|
|
6202
|
+
const projectId = requireProject(ctx);
|
|
6203
|
+
const res = await api(ctx, `/v1/projects/${projectId}/neon-databases`, {
|
|
6204
|
+
body: {
|
|
6205
|
+
name: args[0],
|
|
6206
|
+
...opts.branch ? { branchId: String(opts.branch) } : {},
|
|
6207
|
+
...opts.owner ? { ownerName: String(opts.owner) } : {}
|
|
6208
|
+
}
|
|
6209
|
+
});
|
|
6210
|
+
ok(res, () => success(`Created database ${args[0]}.`));
|
|
6211
|
+
})
|
|
6212
|
+
);
|
|
6213
|
+
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(
|
|
6214
|
+
action(async ({ ctx, args, opts }) => {
|
|
6215
|
+
const projectId = requireProject(ctx);
|
|
6216
|
+
const res = await api(
|
|
6217
|
+
ctx,
|
|
6218
|
+
`/v1/projects/${projectId}/neon-databases/${encodeURIComponent(args[0])}`,
|
|
6219
|
+
{
|
|
6220
|
+
method: "DELETE",
|
|
6221
|
+
query: opts.branch ? { branchId: String(opts.branch) } : void 0
|
|
6222
|
+
}
|
|
6223
|
+
);
|
|
6224
|
+
ok(res, () => success(`Deleted database ${args[0]}.`));
|
|
6225
|
+
})
|
|
6226
|
+
);
|
|
6227
|
+
neon.command("endpoints").description("The compute that runs the database \u2014 what is on, and what it costs idle").action(
|
|
6228
|
+
action(async ({ ctx }) => {
|
|
6229
|
+
const projectId = requireProject(ctx);
|
|
6230
|
+
const items = await api(ctx, `/v1/projects/${projectId}/neon-endpoints`);
|
|
6231
|
+
ok(items, () => {
|
|
6232
|
+
if (!items?.length) {
|
|
6233
|
+
return line(
|
|
6234
|
+
import_picocolors8.default.dim("No compute endpoints \u2014 nothing can connect to this database.")
|
|
6235
|
+
);
|
|
6236
|
+
}
|
|
6237
|
+
for (const e of items) {
|
|
6238
|
+
const state = e.current_state === "active" ? import_picocolors8.default.green("active") : import_picocolors8.default.dim(e.current_state ?? "?");
|
|
6239
|
+
line(
|
|
6240
|
+
`${import_picocolors8.default.bold(e.type ?? "endpoint")} ${state} ${import_picocolors8.default.dim(e.branch_id ?? "")}` + (e.host ? " " + import_picocolors8.default.cyan(e.host) : "")
|
|
6241
|
+
);
|
|
6242
|
+
}
|
|
6243
|
+
});
|
|
6244
|
+
})
|
|
6245
|
+
);
|
|
6246
|
+
}
|
|
6247
|
+
async function presign(ctx, projectId, bucket, key, operation, expiresInSeconds) {
|
|
6248
|
+
const res = await api(
|
|
6249
|
+
ctx,
|
|
6250
|
+
`/v1/projects/${projectId}/neon-storage/buckets/${encodeURIComponent(bucket)}/presign`,
|
|
6251
|
+
{ body: { key, operation, expiresInSeconds } }
|
|
6252
|
+
);
|
|
6253
|
+
const url = res?.url ?? res?.signedUrl ?? res?.presignedUrl;
|
|
6254
|
+
if (!url) {
|
|
6255
|
+
throw new WorkserError(
|
|
6256
|
+
`The daemon did not return a presigned URL for ${key}.`,
|
|
6257
|
+
{ code: "unexpected_response", details: res }
|
|
6258
|
+
);
|
|
6259
|
+
}
|
|
6260
|
+
return { url, headers: res?.headers };
|
|
6261
|
+
}
|
|
6262
|
+
|
|
6263
|
+
// src/commands/env.ts
|
|
6264
|
+
var import_picocolors9 = __toESM(require_picocolors(), 1);
|
|
6265
|
+
import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
6266
|
+
|
|
6267
|
+
// src/environments.ts
|
|
6268
|
+
var ENVIRONMENTS = ["development", "preview", "production"];
|
|
6269
|
+
var DEPLOY_ENVIRONMENTS = ["preview", "production"];
|
|
6270
|
+
var ALIASES2 = {
|
|
6271
|
+
dev: "development",
|
|
6272
|
+
development: "development",
|
|
6273
|
+
local: "development",
|
|
6274
|
+
preview: "preview",
|
|
6275
|
+
staging: "preview",
|
|
6276
|
+
stage: "preview",
|
|
6277
|
+
test: "preview",
|
|
6278
|
+
prod: "production",
|
|
6279
|
+
production: "production",
|
|
6280
|
+
live: "production",
|
|
6281
|
+
main: "production"
|
|
6282
|
+
};
|
|
6283
|
+
function parseEnvironment(raw) {
|
|
6284
|
+
if (raw === void 0 || raw === null || raw === "") return { ok: true };
|
|
6285
|
+
const text = String(raw).trim().toLowerCase();
|
|
6286
|
+
const value = ALIASES2[text];
|
|
6287
|
+
if (!value) {
|
|
6288
|
+
return {
|
|
6289
|
+
ok: false,
|
|
6290
|
+
error: `--env takes ${ENVIRONMENTS.join(", ")} (or dev/prod). "${raw}" is not one of them.`
|
|
6291
|
+
};
|
|
6292
|
+
}
|
|
6293
|
+
return { ok: true, value };
|
|
6294
|
+
}
|
|
6295
|
+
function parseDeployEnvironment(raw, verb) {
|
|
6296
|
+
const parsed = parseEnvironment(raw);
|
|
6297
|
+
if (!parsed.ok || !parsed.value) return parsed;
|
|
6298
|
+
if (parsed.value === "development") {
|
|
6299
|
+
return {
|
|
6300
|
+
ok: false,
|
|
6301
|
+
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.`
|
|
6302
|
+
};
|
|
6303
|
+
}
|
|
6304
|
+
return parsed;
|
|
6305
|
+
}
|
|
6306
|
+
function envTargets(environment) {
|
|
6307
|
+
if (!environment) return [...ENVIRONMENTS];
|
|
6308
|
+
if (environment === "preview") return ["preview", "development"];
|
|
6309
|
+
return [environment];
|
|
6310
|
+
}
|
|
6311
|
+
function targetSummary(environment) {
|
|
6312
|
+
if (!environment) return "in every environment";
|
|
6313
|
+
if (environment === "preview") return "in preview and development";
|
|
6314
|
+
return `in ${environment}`;
|
|
6315
|
+
}
|
|
6316
|
+
function urlRows(apps) {
|
|
6317
|
+
const rows = [];
|
|
6318
|
+
for (const app of apps) {
|
|
6319
|
+
const appId = typeof app?.id === "string" ? app.id : "";
|
|
6320
|
+
if (!appId) continue;
|
|
6321
|
+
const appName = (app.name ?? "").trim() || "Untitled app";
|
|
6322
|
+
for (const environment of DEPLOY_ENVIRONMENTS) {
|
|
6323
|
+
const url = environment === "production" ? app.productionUrl : app.previewUrl;
|
|
6324
|
+
rows.push({
|
|
6325
|
+
appId,
|
|
6326
|
+
appName,
|
|
6327
|
+
environment,
|
|
6328
|
+
url: url && url.trim() ? url.trim() : null,
|
|
6329
|
+
note: url && url.trim() ? null : noteFor(environment)
|
|
6330
|
+
});
|
|
6331
|
+
}
|
|
6332
|
+
}
|
|
6333
|
+
return rows;
|
|
6334
|
+
}
|
|
6335
|
+
function noteFor(environment) {
|
|
6336
|
+
return environment === "production" ? "not live yet \u2014 `workser deploy --env production` publishes it" : "no preview yet \u2014 `workser deploy` builds one";
|
|
6337
|
+
}
|
|
6338
|
+
function urlsSummary(rows) {
|
|
6339
|
+
if (!rows.length) return "This project has no apps yet.";
|
|
6340
|
+
const live = rows.filter((r) => r.environment === "production" && r.url).length;
|
|
6341
|
+
const apps = new Set(rows.map((r) => r.appId)).size;
|
|
6342
|
+
if (!live) {
|
|
6343
|
+
return `${apps} ${apps === 1 ? "app" : "apps"}, none live yet.`;
|
|
6344
|
+
}
|
|
6345
|
+
return `${apps} ${apps === 1 ? "app" : "apps"} \u2014 ${live} live.`;
|
|
6346
|
+
}
|
|
6347
|
+
|
|
6348
|
+
// src/commands/env.ts
|
|
6349
|
+
function appQuery(opts, environment) {
|
|
6350
|
+
const params = new URLSearchParams();
|
|
6351
|
+
if (typeof opts?.app === "string" && opts.app) params.set("webAppId", opts.app);
|
|
6352
|
+
if (environment) params.set("environment", environment);
|
|
6353
|
+
const query = params.toString();
|
|
6354
|
+
return query ? `?${query}` : "";
|
|
6355
|
+
}
|
|
6356
|
+
var APP_FLAG_HELP = "Which web app to target (defaults to the project's primary app)";
|
|
6357
|
+
var ENV_FLAG_HELP = "Which environment: production, preview or development (default: all three)";
|
|
6358
|
+
function readEnv(opts) {
|
|
6359
|
+
const parsed = parseEnvironment(opts?.env);
|
|
6360
|
+
if (!parsed.ok) throw new WorkserError(parsed.error, { code: "bad_input" });
|
|
6361
|
+
return parsed.value;
|
|
6362
|
+
}
|
|
6363
|
+
function registerEnv(program3) {
|
|
6364
|
+
const env = program3.command("env").description("Manage web app environment variables");
|
|
6365
|
+
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(
|
|
6366
|
+
action(async ({ ctx, args, opts }) => {
|
|
6367
|
+
const projectId = requireProject(ctx);
|
|
6368
|
+
const environment = readEnv(opts);
|
|
6369
|
+
const pairs = args[0].map((p) => {
|
|
6370
|
+
const i = p.indexOf("=");
|
|
6371
|
+
if (i < 0) throw new WorkserError(`Invalid pair "${p}". Use KEY=VALUE.`, { code: "bad_input" });
|
|
6372
|
+
return {
|
|
6373
|
+
key: p.slice(0, i),
|
|
6374
|
+
value: p.slice(i + 1),
|
|
6375
|
+
// Sent only when asked for. Omitted means all three, which is what
|
|
6376
|
+
// this command has always done — and quietly narrowing that default
|
|
6377
|
+
// would have un-set production for every script the day it shipped.
|
|
6378
|
+
...environment ? { target: envTargets(environment) } : {}
|
|
6379
|
+
};
|
|
6380
|
+
});
|
|
6381
|
+
const res = await api(ctx, `/v1/projects/${projectId}/env${appQuery(opts)}`, {
|
|
6382
|
+
body: { vars: pairs }
|
|
6383
|
+
});
|
|
6384
|
+
const count = typeof res?.count === "number" ? res.count : pairs.length;
|
|
6385
|
+
ok(res, () => {
|
|
6386
|
+
success(
|
|
6387
|
+
`Set ${count} variable(s) ${targetSummary(environment)}: ` + pairs.map((p) => p.key).join(", ")
|
|
6388
|
+
);
|
|
6389
|
+
if (res?.usedDefault && res?.webAppName) {
|
|
6390
|
+
line(import_picocolors9.default.dim(`on ${res.webAppName} (primary app) \u2014 use --app to target another`));
|
|
6391
|
+
}
|
|
6392
|
+
});
|
|
6393
|
+
})
|
|
6394
|
+
);
|
|
6395
|
+
env.command("get <key>").description("Print one variable's value (sensitive)").option("--app <webAppId>", APP_FLAG_HELP).option("--env <environment>", ENV_FLAG_HELP).action(
|
|
6396
|
+
action(async ({ ctx, args, opts }) => {
|
|
6397
|
+
const projectId = requireProject(ctx);
|
|
6398
|
+
const environment = readEnv(opts);
|
|
6399
|
+
const res = await api(
|
|
6400
|
+
ctx,
|
|
6401
|
+
`/v1/projects/${projectId}/env/${encodeURIComponent(args[0])}${appQuery(opts, environment)}`
|
|
5613
6402
|
);
|
|
5614
6403
|
ok(res, () => line(res.value ?? ""));
|
|
5615
6404
|
})
|
|
5616
6405
|
);
|
|
5617
|
-
env.command("list").description("List variable keys (values masked)").option("--app <webAppId>", APP_FLAG_HELP).action(
|
|
6406
|
+
env.command("list").description("List variable keys (values masked)").option("--app <webAppId>", APP_FLAG_HELP).option("--env <environment>", ENV_FLAG_HELP).action(
|
|
5618
6407
|
action(async ({ ctx, opts }) => {
|
|
5619
6408
|
const projectId = requireProject(ctx);
|
|
5620
|
-
const
|
|
6409
|
+
const environment = readEnv(opts);
|
|
6410
|
+
const items = await api(
|
|
6411
|
+
ctx,
|
|
6412
|
+
`/v1/projects/${projectId}/env${appQuery(opts, environment)}`
|
|
6413
|
+
);
|
|
5621
6414
|
ok(items, () => {
|
|
5622
|
-
if (!items?.length)
|
|
5623
|
-
|
|
6415
|
+
if (!items?.length) {
|
|
6416
|
+
return line(
|
|
6417
|
+
import_picocolors9.default.dim(
|
|
6418
|
+
environment ? `No variables apply in ${environment}.` : "No variables set."
|
|
6419
|
+
)
|
|
6420
|
+
);
|
|
6421
|
+
}
|
|
6422
|
+
for (const v of items) {
|
|
6423
|
+
const differs = !environment && v.overriddenIn?.length ? import_picocolors9.default.yellow(` (different in ${v.overriddenIn.join(", ")})`) : "";
|
|
6424
|
+
line(`${v.key}${import_picocolors9.default.dim(" = " + (v.masked ?? "\u2022\u2022\u2022\u2022"))}${differs}`);
|
|
6425
|
+
}
|
|
6426
|
+
if (environment) {
|
|
6427
|
+
line(import_picocolors9.default.dim(`
|
|
6428
|
+
Showing the values that apply in ${environment}.`));
|
|
6429
|
+
}
|
|
5624
6430
|
});
|
|
5625
6431
|
})
|
|
5626
6432
|
);
|
|
@@ -5633,12 +6439,17 @@ function registerEnv(program3) {
|
|
|
5633
6439
|
})
|
|
5634
6440
|
)
|
|
5635
6441
|
);
|
|
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").
|
|
6442
|
+
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(
|
|
6443
|
+
"--overwrite",
|
|
6444
|
+
"replace values this computer already has (default: fill in only what is missing)",
|
|
6445
|
+
false
|
|
6446
|
+
).action(
|
|
5637
6447
|
action(async ({ ctx, opts }) => {
|
|
5638
6448
|
const projectId = requireProject(ctx);
|
|
6449
|
+
const environment = readEnv(opts);
|
|
5639
6450
|
const items = await api(
|
|
5640
6451
|
ctx,
|
|
5641
|
-
`/v1/projects/${projectId}/env${appQuery(opts)}`
|
|
6452
|
+
`/v1/projects/${projectId}/env${appQuery(opts, environment)}`
|
|
5642
6453
|
);
|
|
5643
6454
|
const outPath = typeof opts?.out === "string" ? opts.out : ".env.local";
|
|
5644
6455
|
if (!items?.length) {
|
|
@@ -5651,38 +6462,66 @@ function registerEnv(program3) {
|
|
|
5651
6462
|
for (const item of items) {
|
|
5652
6463
|
const res = await api(
|
|
5653
6464
|
ctx,
|
|
5654
|
-
`/v1/projects/${projectId}/env/${encodeURIComponent(item.key)}${appQuery(opts)}`
|
|
6465
|
+
`/v1/projects/${projectId}/env/${encodeURIComponent(item.key)}${appQuery(opts, environment)}`
|
|
5655
6466
|
);
|
|
5656
6467
|
pulled.push({ key: item.key, value: res?.value ?? "" });
|
|
5657
6468
|
}
|
|
5658
|
-
await mergeEnvFile(outPath, pulled
|
|
6469
|
+
const result = await mergeEnvFile(outPath, pulled, {
|
|
6470
|
+
overwrite: !!opts.overwrite
|
|
6471
|
+
});
|
|
5659
6472
|
ok(
|
|
5660
|
-
{
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
6473
|
+
{
|
|
6474
|
+
file: outPath,
|
|
6475
|
+
pulled: result.written,
|
|
6476
|
+
skipped: result.skipped,
|
|
6477
|
+
overwrite: !!opts.overwrite
|
|
6478
|
+
},
|
|
6479
|
+
() => {
|
|
6480
|
+
success(
|
|
6481
|
+
`Pulled ${result.written.length} variable(s) into ${import_picocolors9.default.bold(outPath)}.`
|
|
6482
|
+
);
|
|
6483
|
+
if (result.skipped.length) {
|
|
6484
|
+
line(
|
|
6485
|
+
import_picocolors9.default.dim(
|
|
6486
|
+
`Left alone (this computer already has ${result.skipped.length === 1 ? "it" : "them"}): ${result.skipped.join(", ")}`
|
|
6487
|
+
)
|
|
6488
|
+
);
|
|
6489
|
+
line(import_picocolors9.default.dim("Use --overwrite to replace them."));
|
|
6490
|
+
}
|
|
6491
|
+
}
|
|
5664
6492
|
);
|
|
5665
6493
|
})
|
|
5666
6494
|
);
|
|
5667
6495
|
}
|
|
5668
6496
|
var ENV_KEY_LINE = /^([A-Za-z_][A-Za-z0-9_]*)=/;
|
|
5669
|
-
async function mergeEnvFile(path, vars) {
|
|
6497
|
+
async function mergeEnvFile(path, vars, opts = {}) {
|
|
5670
6498
|
const existing = await readFile3(path, "utf8").catch(() => "");
|
|
5671
6499
|
const lines = existing.length ? existing.split(/\r?\n/) : [];
|
|
5672
6500
|
const remaining = new Map(vars.map((v) => [v.key, v.value]));
|
|
6501
|
+
const skipped = [];
|
|
6502
|
+
const replaced = [];
|
|
5673
6503
|
const merged = lines.map((rawLine) => {
|
|
5674
6504
|
const match = ENV_KEY_LINE.exec(rawLine);
|
|
5675
6505
|
if (!match || !remaining.has(match[1])) return rawLine;
|
|
5676
6506
|
const key = match[1];
|
|
6507
|
+
if (!opts.overwrite) {
|
|
6508
|
+
remaining.delete(key);
|
|
6509
|
+
skipped.push(key);
|
|
6510
|
+
return rawLine;
|
|
6511
|
+
}
|
|
5677
6512
|
const value = remaining.get(key);
|
|
5678
6513
|
remaining.delete(key);
|
|
6514
|
+
replaced.push(key);
|
|
5679
6515
|
return `${key}=${formatEnvValue(value)}`;
|
|
5680
6516
|
});
|
|
5681
6517
|
while (merged.length && merged[merged.length - 1] === "") merged.pop();
|
|
6518
|
+
const written = [...replaced];
|
|
5682
6519
|
for (const [key, value] of remaining) {
|
|
5683
6520
|
merged.push(`${key}=${formatEnvValue(value)}`);
|
|
6521
|
+
written.push(key);
|
|
5684
6522
|
}
|
|
5685
6523
|
await writeFile3(path, merged.join("\n") + "\n", "utf8");
|
|
6524
|
+
return { written, skipped };
|
|
5686
6525
|
}
|
|
5687
6526
|
function formatEnvValue(value) {
|
|
5688
6527
|
return /[\s"'#]/.test(value) ? JSON.stringify(value) : value;
|
|
@@ -5692,16 +6531,30 @@ function formatEnvValue(value) {
|
|
|
5692
6531
|
var import_picocolors10 = __toESM(require_picocolors(), 1);
|
|
5693
6532
|
var TERMINAL = /* @__PURE__ */ new Set(["ready", "live", "success", "error", "failed", "canceled"]);
|
|
5694
6533
|
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
|
|
6534
|
+
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(
|
|
6535
|
+
"--env <environment>",
|
|
6536
|
+
"which environment to deploy: preview (default) or production"
|
|
6537
|
+
).option("--watch", "wait for the deploy to finish, streaming status", false).option(
|
|
5696
6538
|
"--app <webAppId>",
|
|
5697
6539
|
"which app to publish (default: the app this folder is linked to)"
|
|
5698
6540
|
).action(
|
|
5699
6541
|
action(async ({ ctx, opts }) => {
|
|
5700
6542
|
requireLocalApp(ctx, "deploy");
|
|
5701
6543
|
const projectId = requireProject(ctx);
|
|
6544
|
+
const parsedEnv = parseDeployEnvironment(opts.env, "deploy");
|
|
6545
|
+
if (!parsedEnv.ok) {
|
|
6546
|
+
throw new WorkserError(parsedEnv.error, { code: "bad_input" });
|
|
6547
|
+
}
|
|
6548
|
+
if (opts.prod && parsedEnv.value === "preview") {
|
|
6549
|
+
throw new WorkserError(
|
|
6550
|
+
"`--prod` and `--env preview` ask for different things. Pass one.",
|
|
6551
|
+
{ code: "bad_input" }
|
|
6552
|
+
);
|
|
6553
|
+
}
|
|
6554
|
+
const prod = Boolean(opts.prod) || parsedEnv.value === "production";
|
|
5702
6555
|
const dep = await api(ctx, `/v1/projects/${projectId}/deploy`, {
|
|
5703
6556
|
body: {
|
|
5704
|
-
prod
|
|
6557
|
+
prod,
|
|
5705
6558
|
cwd: ctx.cwd,
|
|
5706
6559
|
...opts.app ? { webAppId: opts.app } : {}
|
|
5707
6560
|
}
|
|
@@ -5743,10 +6596,22 @@ function printDeploy(dep) {
|
|
|
5743
6596
|
// src/commands/versions.ts
|
|
5744
6597
|
var import_picocolors11 = __toESM(require_picocolors(), 1);
|
|
5745
6598
|
function registerVersions(program3) {
|
|
5746
|
-
program3.command("versions").description("List the Workser-managed versions of the project (deploy history)").
|
|
5747
|
-
|
|
6599
|
+
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(
|
|
6600
|
+
"--env <environment>",
|
|
6601
|
+
"which environment `deployed` should mean: preview or production"
|
|
6602
|
+
).action(
|
|
6603
|
+
action(async ({ ctx, opts }) => {
|
|
5748
6604
|
const projectId = requireProject(ctx);
|
|
5749
|
-
const
|
|
6605
|
+
const parsed = parseDeployEnvironment(opts.env, "versions");
|
|
6606
|
+
if (!parsed.ok) {
|
|
6607
|
+
throw new WorkserError(parsed.error, { code: "bad_input" });
|
|
6608
|
+
}
|
|
6609
|
+
const items = await api(ctx, `/v1/projects/${projectId}/versions`, {
|
|
6610
|
+
query: {
|
|
6611
|
+
...opts.app ? { webAppId: String(opts.app) } : {},
|
|
6612
|
+
...parsed.value ? { environment: parsed.value } : {}
|
|
6613
|
+
}
|
|
6614
|
+
});
|
|
5750
6615
|
ok(items, () => {
|
|
5751
6616
|
if (!items?.length) return line(import_picocolors11.default.dim("No versions yet. `workser deploy` to create one."));
|
|
5752
6617
|
for (const v of items) {
|
|
@@ -5760,7 +6625,7 @@ function formatVersion(v) {
|
|
|
5760
6625
|
const ref = import_picocolors11.default.yellow(shortRef(v.ref));
|
|
5761
6626
|
const when = import_picocolors11.default.dim(formatTime(v.createdAt));
|
|
5762
6627
|
const msg = (v.message ?? "").trim() || import_picocolors11.default.dim("(no message)");
|
|
5763
|
-
const badge = v.deployed ? " " + import_picocolors11.default.green("deployed") : "";
|
|
6628
|
+
const badge = v.deployed ? " " + import_picocolors11.default.green(v.deployedEnvironment ? `live in ${v.deployedEnvironment}` : "deployed") : "";
|
|
5764
6629
|
const url = v.url ? " " + import_picocolors11.default.cyan(v.url) : "";
|
|
5765
6630
|
return `${ref} ${when} ${msg}${badge}${url}`;
|
|
5766
6631
|
}
|
|
@@ -5775,19 +6640,30 @@ function formatTime(t) {
|
|
|
5775
6640
|
}
|
|
5776
6641
|
|
|
5777
6642
|
// src/commands/logs.ts
|
|
6643
|
+
var import_picocolors12 = __toESM(require_picocolors(), 1);
|
|
5778
6644
|
function registerLogs(program3) {
|
|
5779
|
-
program3.command("logs").description("Show
|
|
6645
|
+
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
6646
|
action(async ({ ctx, opts }) => {
|
|
5781
6647
|
const projectId = requireProject(ctx);
|
|
6648
|
+
const parsed = parseDeployEnvironment(opts.env, "logs");
|
|
6649
|
+
if (!parsed.ok) {
|
|
6650
|
+
throw new WorkserError(parsed.error, { code: "bad_input" });
|
|
6651
|
+
}
|
|
6652
|
+
const scope = {
|
|
6653
|
+
...opts.app ? { webAppId: String(opts.app) } : {},
|
|
6654
|
+
...parsed.value ? { environment: parsed.value } : {}
|
|
6655
|
+
};
|
|
5782
6656
|
const first = await api(ctx, `/v1/projects/${projectId}/logs`, {
|
|
5783
|
-
query: { lines: opts.lines }
|
|
6657
|
+
query: { lines: opts.lines, ...scope }
|
|
5784
6658
|
});
|
|
5785
|
-
ok(first, () => (first
|
|
6659
|
+
ok(first, () => print(first));
|
|
5786
6660
|
if (opts.follow && !isJson()) {
|
|
5787
6661
|
let cursor = first.cursor;
|
|
5788
6662
|
for (; ; ) {
|
|
5789
6663
|
await sleep(2e3);
|
|
5790
|
-
const next = await api(ctx, `/v1/projects/${projectId}/logs`, {
|
|
6664
|
+
const next = await api(ctx, `/v1/projects/${projectId}/logs`, {
|
|
6665
|
+
query: { after: cursor, ...scope }
|
|
6666
|
+
});
|
|
5791
6667
|
(next.entries ?? []).forEach((e) => line(formatLog(e)));
|
|
5792
6668
|
if (next.cursor) cursor = next.cursor;
|
|
5793
6669
|
}
|
|
@@ -5795,31 +6671,112 @@ function registerLogs(program3) {
|
|
|
5795
6671
|
})
|
|
5796
6672
|
);
|
|
5797
6673
|
}
|
|
6674
|
+
function print(res) {
|
|
6675
|
+
const entries = res?.entries ?? [];
|
|
6676
|
+
for (const e of entries) line(formatLog(e));
|
|
6677
|
+
if (!entries.length && res?.note) line(import_picocolors12.default.dim(res.note));
|
|
6678
|
+
if (entries.length && res?.deploymentId) {
|
|
6679
|
+
const where = res.environment ? ` (${res.environment})` : "";
|
|
6680
|
+
line(import_picocolors12.default.dim(`\u2014 build ${res.deploymentId}${where}`));
|
|
6681
|
+
}
|
|
6682
|
+
}
|
|
5798
6683
|
function formatLog(e) {
|
|
5799
6684
|
if (typeof e === "string") return e;
|
|
5800
6685
|
return `${e.ts ?? ""} ${e.level ? `[${e.level}] ` : ""}${e.message ?? ""}`.trim();
|
|
5801
6686
|
}
|
|
5802
6687
|
|
|
5803
6688
|
// src/commands/domain.ts
|
|
5804
|
-
var
|
|
6689
|
+
var import_picocolors13 = __toESM(require_picocolors(), 1);
|
|
6690
|
+
|
|
6691
|
+
// src/reserved-domains.ts
|
|
6692
|
+
var RESERVED_APEX = [
|
|
6693
|
+
"workser.ai",
|
|
6694
|
+
"workser.app",
|
|
6695
|
+
"workser.dev"
|
|
6696
|
+
];
|
|
6697
|
+
var PROVIDER_HOSTS = ["vercel.app", "neon.tech", "expo.dev"];
|
|
6698
|
+
function normaliseDomain(input) {
|
|
6699
|
+
return input.trim().toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/:\d+$/, "").replace(/\.$/, "");
|
|
6700
|
+
}
|
|
6701
|
+
function isUnder(host, suffix) {
|
|
6702
|
+
return host === suffix || host.endsWith(`.${suffix}`);
|
|
6703
|
+
}
|
|
6704
|
+
function checkDomain(input) {
|
|
6705
|
+
const host = normaliseDomain(input);
|
|
6706
|
+
if (!host) {
|
|
6707
|
+
return { allowed: false, reason: "No domain given." };
|
|
6708
|
+
}
|
|
6709
|
+
if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/.test(host)) {
|
|
6710
|
+
return {
|
|
6711
|
+
allowed: false,
|
|
6712
|
+
reason: `"${input}" is not a valid domain name.`
|
|
6713
|
+
};
|
|
6714
|
+
}
|
|
6715
|
+
for (const apex of RESERVED_APEX) {
|
|
6716
|
+
if (isUnder(host, apex)) {
|
|
6717
|
+
return {
|
|
6718
|
+
allowed: false,
|
|
6719
|
+
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.`
|
|
6720
|
+
};
|
|
6721
|
+
}
|
|
6722
|
+
}
|
|
6723
|
+
for (const provider of PROVIDER_HOSTS) {
|
|
6724
|
+
if (isUnder(host, provider)) {
|
|
6725
|
+
return {
|
|
6726
|
+
allowed: false,
|
|
6727
|
+
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.`
|
|
6728
|
+
};
|
|
6729
|
+
}
|
|
6730
|
+
}
|
|
6731
|
+
return { allowed: true };
|
|
6732
|
+
}
|
|
6733
|
+
function assertDomainAllowed(input) {
|
|
6734
|
+
const verdict = checkDomain(input);
|
|
6735
|
+
if (!verdict.allowed) throw new Error(verdict.reason);
|
|
6736
|
+
return normaliseDomain(input);
|
|
6737
|
+
}
|
|
6738
|
+
|
|
6739
|
+
// src/commands/domain.ts
|
|
5805
6740
|
function registerDomain(program3) {
|
|
5806
|
-
const domain = program3.command("domain").description("
|
|
5807
|
-
domain.command("
|
|
5808
|
-
action(
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
6741
|
+
const domain = program3.command("domain").description("Manage the project's custom domains");
|
|
6742
|
+
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(
|
|
6743
|
+
action(async ({ ctx, args, opts }) => {
|
|
6744
|
+
const projectId = requireProject(ctx);
|
|
6745
|
+
const host = assertDomainAllowed(String(args[0]));
|
|
6746
|
+
const result = await api(
|
|
6747
|
+
ctx,
|
|
6748
|
+
opts.app ? `/v1/projects/${projectId}/web-apps/${opts.app}/domains` : `/v1/projects/${projectId}/domains`,
|
|
6749
|
+
{ method: "POST", body: { domain: host } }
|
|
6750
|
+
);
|
|
6751
|
+
ok(result, () => {
|
|
6752
|
+
line(`${import_picocolors13.default.green("Attached")} ${host}`);
|
|
6753
|
+
line(
|
|
6754
|
+
import_picocolors13.default.dim(
|
|
6755
|
+
"It goes live once DNS points at us. Run `workser domain list` to see its status."
|
|
6756
|
+
)
|
|
6757
|
+
);
|
|
6758
|
+
});
|
|
6759
|
+
})
|
|
6760
|
+
);
|
|
6761
|
+
domain.command("rm <domain>").description("Detach a custom domain (asks the owner to confirm)").option("--app <webAppId>", "Which app it is attached to").action(
|
|
6762
|
+
action(async ({ ctx, args, opts }) => {
|
|
6763
|
+
const projectId = requireProject(ctx);
|
|
6764
|
+
const host = String(args[0]).trim().toLowerCase();
|
|
6765
|
+
const result = await api(
|
|
6766
|
+
ctx,
|
|
6767
|
+
opts.app ? `/v1/projects/${projectId}/web-apps/${opts.app}/domains/${encodeURIComponent(host)}` : `/v1/projects/${projectId}/domains/${encodeURIComponent(host)}`,
|
|
6768
|
+
{ method: "DELETE" }
|
|
6769
|
+
);
|
|
6770
|
+
ok(result, () => line(`${import_picocolors13.default.yellow("Detached")} ${host}`));
|
|
6771
|
+
})
|
|
5815
6772
|
);
|
|
5816
6773
|
domain.command("list").description("List domains attached to the project").action(
|
|
5817
6774
|
action(async ({ ctx }) => {
|
|
5818
6775
|
const projectId = requireProject(ctx);
|
|
5819
6776
|
const items = await api(ctx, `/v1/projects/${projectId}/domains`);
|
|
5820
6777
|
ok(items, () => {
|
|
5821
|
-
if (!items?.length) return line(
|
|
5822
|
-
for (const d of items) line(`${d.domain}${
|
|
6778
|
+
if (!items?.length) return line(import_picocolors13.default.dim("No custom domains."));
|
|
6779
|
+
for (const d of items) line(`${d.domain}${import_picocolors13.default.dim(" " + (d.status ?? ""))}`);
|
|
5823
6780
|
});
|
|
5824
6781
|
})
|
|
5825
6782
|
);
|
|
@@ -5851,7 +6808,7 @@ function openUrl(url) {
|
|
|
5851
6808
|
}
|
|
5852
6809
|
|
|
5853
6810
|
// src/commands/doctor.ts
|
|
5854
|
-
var
|
|
6811
|
+
var import_picocolors14 = __toESM(require_picocolors(), 1);
|
|
5855
6812
|
import { spawnSync } from "child_process";
|
|
5856
6813
|
function registerDoctor(program3) {
|
|
5857
6814
|
program3.command("doctor").description("Print the resolved endpoint, mode, token presence (masked), and current project").action(
|
|
@@ -5863,14 +6820,14 @@ function registerDoctor(program3) {
|
|
|
5863
6820
|
const tokenSource = opts.token ? "--token" : process.env.WORKSER_TOKEN ? "$WORKSER_TOKEN" : session.token ? "session" : void 0;
|
|
5864
6821
|
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
6822
|
const projectSource = opts.project ? "--project" : link?.projectId ? ".workser link" : session.defaultProjectId ? "session" : void 0;
|
|
5866
|
-
const
|
|
6823
|
+
const git2 = gitVersion();
|
|
5867
6824
|
const report = {
|
|
5868
6825
|
endpoint: ctx.endpoint,
|
|
5869
6826
|
endpointSource,
|
|
5870
6827
|
env,
|
|
5871
6828
|
envIgnored,
|
|
5872
6829
|
mode: ctx.mode,
|
|
5873
|
-
git: { present:
|
|
6830
|
+
git: { present: git2 !== null, version: git2 },
|
|
5874
6831
|
token: {
|
|
5875
6832
|
present: Boolean(ctx.token),
|
|
5876
6833
|
masked: ctx.token ? maskToken(ctx.token) : null,
|
|
@@ -5885,29 +6842,29 @@ function registerDoctor(program3) {
|
|
|
5885
6842
|
workspace: session.workspaceName ?? null
|
|
5886
6843
|
};
|
|
5887
6844
|
ok(report, () => {
|
|
5888
|
-
line(
|
|
5889
|
-
line(` endpoint: ${ctx.endpoint} ${
|
|
6845
|
+
line(import_picocolors14.default.bold("workser doctor"));
|
|
6846
|
+
line(` endpoint: ${ctx.endpoint} ${import_picocolors14.default.dim(`(${endpointSource})`)}`);
|
|
5890
6847
|
line(
|
|
5891
|
-
` env: ${env === "prod" ?
|
|
6848
|
+
` env: ${env === "prod" ? import_picocolors14.default.yellow(env) : env}` + import_picocolors14.default.dim(process.env.WORKSER_ENV ? " ($WORKSER_ENV)" : " (default)")
|
|
5892
6849
|
);
|
|
5893
6850
|
line(` mode: ${ctx.mode}`);
|
|
5894
6851
|
line(
|
|
5895
|
-
` token: ${ctx.token ? `${maskToken(ctx.token)} ${
|
|
6852
|
+
` token: ${ctx.token ? `${maskToken(ctx.token)} ${import_picocolors14.default.dim(`(${tokenSource})`)}` : import_picocolors14.default.yellow("none \u2014 run `workser login`")}`
|
|
5896
6853
|
);
|
|
5897
6854
|
line(
|
|
5898
|
-
` project: ${ctx.projectId ??
|
|
6855
|
+
` project: ${ctx.projectId ?? import_picocolors14.default.dim("none")}` + (link?.name ? ` ${import_picocolors14.default.dim(`(${link.name})`)}` : "") + (projectSource ? import_picocolors14.default.dim(` [${projectSource}]`) : "")
|
|
5899
6856
|
);
|
|
5900
6857
|
line(` cwd: ${ctx.cwd}`);
|
|
5901
|
-
line(` git: ${
|
|
5902
|
-
if (!
|
|
6858
|
+
line(` git: ${git2 ?? import_picocolors14.default.dim("not on this shell's PATH")}`);
|
|
6859
|
+
if (!git2) {
|
|
5903
6860
|
line("");
|
|
5904
6861
|
line(
|
|
5905
|
-
|
|
6862
|
+
import_picocolors14.default.dim(
|
|
5906
6863
|
" Workser brings its own git, so syncing and publishing still work."
|
|
5907
6864
|
)
|
|
5908
6865
|
);
|
|
5909
6866
|
line(
|
|
5910
|
-
|
|
6867
|
+
import_picocolors14.default.dim(
|
|
5911
6868
|
` Only needed if you want to run git yourself here. ${GIT_INSTALL_HINT}`
|
|
5912
6869
|
)
|
|
5913
6870
|
);
|
|
@@ -5915,16 +6872,16 @@ function registerDoctor(program3) {
|
|
|
5915
6872
|
if (envIgnored) {
|
|
5916
6873
|
line("");
|
|
5917
6874
|
line(
|
|
5918
|
-
|
|
6875
|
+
import_picocolors14.default.yellow(
|
|
5919
6876
|
` WORKSER_ENV=${env} is not in effect \u2014 ${endpointSource} wins.`
|
|
5920
6877
|
)
|
|
5921
6878
|
);
|
|
5922
6879
|
line(
|
|
5923
|
-
|
|
6880
|
+
import_picocolors14.default.dim(
|
|
5924
6881
|
` Re-run \`workser login\` to switch (the saved token is tied to ${ctx.endpoint}),`
|
|
5925
6882
|
)
|
|
5926
6883
|
);
|
|
5927
|
-
line(
|
|
6884
|
+
line(import_picocolors14.default.dim(` or pass --endpoint ${ENV_BASE_URLS[env]}.`));
|
|
5928
6885
|
}
|
|
5929
6886
|
});
|
|
5930
6887
|
})
|
|
@@ -5954,7 +6911,7 @@ function maskToken(token) {
|
|
|
5954
6911
|
}
|
|
5955
6912
|
|
|
5956
6913
|
// src/commands/agent.ts
|
|
5957
|
-
var
|
|
6914
|
+
var import_picocolors15 = __toESM(require_picocolors(), 1);
|
|
5958
6915
|
var SPAWNABLE_AGENTS = ["claude_code", "codex", "kimi", "opencode", "grok"];
|
|
5959
6916
|
function registerAgent(program3) {
|
|
5960
6917
|
const agent = program3.command("agent").description("Delegate focused subtasks to your configured agent roles (each runs isolated)");
|
|
@@ -5962,20 +6919,20 @@ function registerAgent(program3) {
|
|
|
5962
6919
|
action(async ({ ctx }) => {
|
|
5963
6920
|
const cfg = await api(ctx, "/v1/agents");
|
|
5964
6921
|
ok(cfg, () => {
|
|
5965
|
-
line(
|
|
5966
|
-
line(
|
|
6922
|
+
line(import_picocolors15.default.bold("main agent:") + " " + (cfg?.mainAgent ?? import_picocolors15.default.dim("none")));
|
|
6923
|
+
line(import_picocolors15.default.bold("backup agent:") + " " + (cfg?.backupAgent ?? import_picocolors15.default.dim("none")));
|
|
5967
6924
|
if (cfg?.effectiveMainAgent && cfg.effectiveMainAgent !== cfg.mainAgent) {
|
|
5968
6925
|
line(
|
|
5969
|
-
|
|
6926
|
+
import_picocolors15.default.yellow(
|
|
5970
6927
|
` \u2937 failover active: runs use ${cfg.effectiveMainAgent} (main not available)`
|
|
5971
6928
|
)
|
|
5972
6929
|
);
|
|
5973
6930
|
}
|
|
5974
6931
|
const roles = cfg?.roles ?? [];
|
|
5975
6932
|
if (!roles.length) {
|
|
5976
|
-
line(
|
|
6933
|
+
line(import_picocolors15.default.dim("No subagents configured. Add them in the Workser Orbit Agents screen."));
|
|
5977
6934
|
} else {
|
|
5978
|
-
line(
|
|
6935
|
+
line(import_picocolors15.default.bold("subagents:"));
|
|
5979
6936
|
for (const r of roles) line(" " + formatRole(r));
|
|
5980
6937
|
}
|
|
5981
6938
|
const detected = cfg?.detected ?? [];
|
|
@@ -5983,7 +6940,7 @@ function registerAgent(program3) {
|
|
|
5983
6940
|
(d) => d?.installed && d?.authed !== false
|
|
5984
6941
|
);
|
|
5985
6942
|
line(
|
|
5986
|
-
|
|
6943
|
+
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
6944
|
);
|
|
5988
6945
|
});
|
|
5989
6946
|
})
|
|
@@ -5997,8 +6954,8 @@ function registerAgent(program3) {
|
|
|
5997
6954
|
backupAgent: cfg?.backupAgent ?? null
|
|
5998
6955
|
},
|
|
5999
6956
|
() => {
|
|
6000
|
-
line(
|
|
6001
|
-
line(
|
|
6957
|
+
line(import_picocolors15.default.bold("main agent:") + " " + (cfg?.mainAgent ?? import_picocolors15.default.dim("none")));
|
|
6958
|
+
line(import_picocolors15.default.bold("backup agent:") + " " + (cfg?.backupAgent ?? import_picocolors15.default.dim("none")));
|
|
6002
6959
|
}
|
|
6003
6960
|
);
|
|
6004
6961
|
})
|
|
@@ -6052,21 +7009,21 @@ function toContractId(id) {
|
|
|
6052
7009
|
return id === "claude" ? "claude_code" : id;
|
|
6053
7010
|
}
|
|
6054
7011
|
function formatRole(r) {
|
|
6055
|
-
const label =
|
|
6056
|
-
const agent =
|
|
6057
|
-
const enabled = r.enabled === false ?
|
|
7012
|
+
const label = import_picocolors15.default.yellow(r.role);
|
|
7013
|
+
const agent = import_picocolors15.default.dim("\xB7 " + (r.agent ?? "?"));
|
|
7014
|
+
const enabled = r.enabled === false ? import_picocolors15.default.red("disabled") : import_picocolors15.default.green("enabled");
|
|
6058
7015
|
const runnable = r.installed && r.authed !== false;
|
|
6059
|
-
const ready = runnable ?
|
|
7016
|
+
const ready = runnable ? import_picocolors15.default.green("runnable") : import_picocolors15.default.dim("not runnable");
|
|
6060
7017
|
const extras = [];
|
|
6061
7018
|
if (r.model) extras.push(`model ${r.model}`);
|
|
6062
7019
|
if (Array.isArray(r.apps) && r.apps.length) extras.push(`apps: ${r.apps.join(",")}`);
|
|
6063
7020
|
if (Array.isArray(r.mcp) && r.mcp.length) extras.push(`mcp: ${r.mcp.length}`);
|
|
6064
|
-
const tail = extras.length ? " " +
|
|
7021
|
+
const tail = extras.length ? " " + import_picocolors15.default.dim(extras.join(" \xB7 ")) : "";
|
|
6065
7022
|
return `${label} ${agent} ${enabled} ${ready}${tail}`;
|
|
6066
7023
|
}
|
|
6067
7024
|
|
|
6068
7025
|
// src/commands/verify.ts
|
|
6069
|
-
var
|
|
7026
|
+
var import_picocolors16 = __toESM(require_picocolors(), 1);
|
|
6070
7027
|
function registerVerify(program3) {
|
|
6071
7028
|
program3.command("verify").description(
|
|
6072
7029
|
"Run the project's checks (typecheck/lint/build) \u2014 use before declaring a task done"
|
|
@@ -6088,23 +7045,23 @@ function registerVerify(program3) {
|
|
|
6088
7045
|
function printVerify(res) {
|
|
6089
7046
|
if (!res) return;
|
|
6090
7047
|
if (!res.checks?.length) {
|
|
6091
|
-
line(
|
|
7048
|
+
line(import_picocolors16.default.dim(res.note ?? "No checks detected."));
|
|
6092
7049
|
return;
|
|
6093
7050
|
}
|
|
6094
7051
|
for (const c of res.checks) {
|
|
6095
7052
|
line(
|
|
6096
|
-
` ${c.ok ?
|
|
7053
|
+
` ${c.ok ? import_picocolors16.default.green("\u2713") : import_picocolors16.default.red("\u2717")} ${c.name}${c.ok ? "" : import_picocolors16.default.dim(` (exit ${c.exitCode})`)}`
|
|
6097
7054
|
);
|
|
6098
7055
|
}
|
|
6099
7056
|
if (res.ok) success("All checks passed");
|
|
6100
7057
|
else
|
|
6101
7058
|
line(
|
|
6102
|
-
|
|
7059
|
+
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
7060
|
);
|
|
6104
7061
|
}
|
|
6105
7062
|
|
|
6106
7063
|
// src/commands/checkpoint.ts
|
|
6107
|
-
var
|
|
7064
|
+
var import_picocolors17 = __toESM(require_picocolors(), 1);
|
|
6108
7065
|
function registerCheckpoint(program3) {
|
|
6109
7066
|
program3.command("checkpoint [label]").description(
|
|
6110
7067
|
"Save the current state of this folder so you can come back to it"
|
|
@@ -6121,8 +7078,8 @@ function registerCheckpoint(program3) {
|
|
|
6121
7078
|
ok(res, () => {
|
|
6122
7079
|
const p = res?.point;
|
|
6123
7080
|
success(`Saved a checkpoint${p?.label ? `: ${p.label}` : ""}`);
|
|
6124
|
-
if (p?.ref) line(
|
|
6125
|
-
line(
|
|
7081
|
+
if (p?.ref) line(import_picocolors17.default.dim(` ${p.ref.slice(0, 7)}`));
|
|
7082
|
+
line(import_picocolors17.default.dim(" Come back to it with `workser restore`."));
|
|
6126
7083
|
});
|
|
6127
7084
|
})
|
|
6128
7085
|
);
|
|
@@ -6148,12 +7105,12 @@ function registerCheckpoint(program3) {
|
|
|
6148
7105
|
);
|
|
6149
7106
|
if (res?.filesChanged) {
|
|
6150
7107
|
line(
|
|
6151
|
-
|
|
7108
|
+
import_picocolors17.default.dim(
|
|
6152
7109
|
` ${res.filesChanged} file${res.filesChanged === 1 ? "" : "s"} changed`
|
|
6153
7110
|
)
|
|
6154
7111
|
);
|
|
6155
7112
|
}
|
|
6156
|
-
line(
|
|
7113
|
+
line(import_picocolors17.default.dim(" This is reversible: `workser restore` again."));
|
|
6157
7114
|
});
|
|
6158
7115
|
})
|
|
6159
7116
|
);
|
|
@@ -6170,25 +7127,25 @@ function registerCheckpoint(program3) {
|
|
|
6170
7127
|
function printPoints(points) {
|
|
6171
7128
|
if (!points.length) {
|
|
6172
7129
|
info("No checkpoints yet for this folder.");
|
|
6173
|
-
line(
|
|
7130
|
+
line(import_picocolors17.default.dim(" Take one with `workser checkpoint`."));
|
|
6174
7131
|
return;
|
|
6175
7132
|
}
|
|
6176
|
-
line(
|
|
7133
|
+
line(import_picocolors17.default.bold("Checkpoints"));
|
|
6177
7134
|
for (const p of points) {
|
|
6178
7135
|
const when = p.at ? new Date(p.at).toLocaleString() : "";
|
|
6179
7136
|
line(
|
|
6180
|
-
` ${
|
|
7137
|
+
` ${import_picocolors17.default.dim(p.ref.slice(0, 7))} ${p.label}${when ? import_picocolors17.default.dim(` ${when}`) : ""}`
|
|
6181
7138
|
);
|
|
6182
7139
|
}
|
|
6183
7140
|
line(
|
|
6184
|
-
|
|
7141
|
+
import_picocolors17.default.dim(
|
|
6185
7142
|
"\nGo back with `workser restore <ref>`, or just `workser restore` for the newest."
|
|
6186
7143
|
)
|
|
6187
7144
|
);
|
|
6188
7145
|
}
|
|
6189
7146
|
|
|
6190
7147
|
// src/commands/sync.ts
|
|
6191
|
-
var
|
|
7148
|
+
var import_picocolors18 = __toESM(require_picocolors(), 1);
|
|
6192
7149
|
function registerSync(program3) {
|
|
6193
7150
|
program3.command("sync").description(
|
|
6194
7151
|
"Reconcile this folder with the copy Workser holds (pull, then push)"
|
|
@@ -6215,7 +7172,7 @@ function registerSync(program3) {
|
|
|
6215
7172
|
warn(res?.message ?? "Couldn't sync this folder.");
|
|
6216
7173
|
if (res?.state === "diverged") {
|
|
6217
7174
|
line(
|
|
6218
|
-
|
|
7175
|
+
import_picocolors18.default.dim(
|
|
6219
7176
|
" This folder and Workser's copy have both changed. Open Workser to resolve it."
|
|
6220
7177
|
)
|
|
6221
7178
|
);
|
|
@@ -6227,7 +7184,7 @@ function registerSync(program3) {
|
|
|
6227
7184
|
return;
|
|
6228
7185
|
}
|
|
6229
7186
|
success("Synced");
|
|
6230
|
-
if (res?.ref) line(
|
|
7187
|
+
if (res?.ref) line(import_picocolors18.default.dim(` ${String(res.ref).slice(0, 7)}`));
|
|
6231
7188
|
});
|
|
6232
7189
|
if (refused) process.exitCode = 1;
|
|
6233
7190
|
})
|
|
@@ -6235,7 +7192,7 @@ function registerSync(program3) {
|
|
|
6235
7192
|
}
|
|
6236
7193
|
|
|
6237
7194
|
// src/commands/workflow.ts
|
|
6238
|
-
var
|
|
7195
|
+
var import_picocolors19 = __toESM(require_picocolors(), 1);
|
|
6239
7196
|
function registerWorkflow(program3) {
|
|
6240
7197
|
const wf = program3.command("workflow").description("Create, run, and inspect workflow automations for the project");
|
|
6241
7198
|
wf.command("list").description("List the project's workflows").action(
|
|
@@ -6243,10 +7200,10 @@ function registerWorkflow(program3) {
|
|
|
6243
7200
|
const projectId = requireProject(ctx);
|
|
6244
7201
|
const items = await api(ctx, `/v1/projects/${projectId}/workflows`);
|
|
6245
7202
|
ok(items, () => {
|
|
6246
|
-
if (!items?.length) return line(
|
|
7203
|
+
if (!items?.length) return line(import_picocolors19.default.dim("No workflows yet. `workser workflow create`."));
|
|
6247
7204
|
for (const w of items) {
|
|
6248
|
-
const status = w.is_active ?
|
|
6249
|
-
line(`${w.id} ${
|
|
7205
|
+
const status = w.is_active ? import_picocolors19.default.green("active") : import_picocolors19.default.dim("inactive");
|
|
7206
|
+
line(`${w.id} ${import_picocolors19.default.bold(w.name ?? "Untitled")} ${status}`);
|
|
6250
7207
|
}
|
|
6251
7208
|
});
|
|
6252
7209
|
})
|
|
@@ -6258,7 +7215,7 @@ function registerWorkflow(program3) {
|
|
|
6258
7215
|
const res = await api(ctx, `/v1/projects/${projectId}/workflows`, {
|
|
6259
7216
|
body: { name: args[0], ...extra }
|
|
6260
7217
|
});
|
|
6261
|
-
ok(res, () => line(`Created workflow ${
|
|
7218
|
+
ok(res, () => line(`Created workflow ${import_picocolors19.default.bold(res.id)}.`));
|
|
6262
7219
|
})
|
|
6263
7220
|
);
|
|
6264
7221
|
wf.command("get <id>").description("Show a workflow's full definition").action(
|
|
@@ -6293,8 +7250,8 @@ function registerWorkflow(program3) {
|
|
|
6293
7250
|
action(async ({ ctx, args }) => {
|
|
6294
7251
|
const items = await api(ctx, `/v1/workflows/${args[0]}/executions`);
|
|
6295
7252
|
ok(items, () => {
|
|
6296
|
-
if (!items?.length) return line(
|
|
6297
|
-
for (const e of items) line(`${e.id} ${e.status ?? ""} ${
|
|
7253
|
+
if (!items?.length) return line(import_picocolors19.default.dim("No runs yet."));
|
|
7254
|
+
for (const e of items) line(`${e.id} ${e.status ?? ""} ${import_picocolors19.default.dim(e.started_at ?? "")}`);
|
|
6298
7255
|
});
|
|
6299
7256
|
})
|
|
6300
7257
|
);
|
|
@@ -6302,15 +7259,15 @@ function registerWorkflow(program3) {
|
|
|
6302
7259
|
action(async ({ ctx, args }) => {
|
|
6303
7260
|
const items = await api(ctx, `/v1/node-types`, { query: { q: args[0] } });
|
|
6304
7261
|
ok(items, () => {
|
|
6305
|
-
if (!items?.length) return line(
|
|
6306
|
-
for (const n of items) line(`${n.name ?? n.type} ${
|
|
7262
|
+
if (!items?.length) return line(import_picocolors19.default.dim("No matching node types."));
|
|
7263
|
+
for (const n of items) line(`${n.name ?? n.type} ${import_picocolors19.default.dim(n.category ?? "")}`);
|
|
6307
7264
|
});
|
|
6308
7265
|
})
|
|
6309
7266
|
);
|
|
6310
7267
|
}
|
|
6311
7268
|
|
|
6312
7269
|
// src/commands/app.ts
|
|
6313
|
-
var
|
|
7270
|
+
var import_picocolors20 = __toESM(require_picocolors(), 1);
|
|
6314
7271
|
function registerApp(program3) {
|
|
6315
7272
|
const appCmd = program3.command("app").description("Connect and use third-party app integrations (Gmail, Slack, Stripe, ...)");
|
|
6316
7273
|
appCmd.command("list").description("List connectable toolkits and this project's existing connections").option("--toolkit <slug>", "filter connections to one toolkit").action(
|
|
@@ -6323,8 +7280,8 @@ function registerApp(program3) {
|
|
|
6323
7280
|
ok({ catalog, connections }, () => {
|
|
6324
7281
|
const connected = new Set((connections ?? []).map((c) => c.toolkit ?? c.composio_app));
|
|
6325
7282
|
for (const t of catalog ?? []) {
|
|
6326
|
-
const status = connected.has(t.slug) ?
|
|
6327
|
-
line(`${t.slug} ${
|
|
7283
|
+
const status = connected.has(t.slug) ? import_picocolors20.default.green("connected") : import_picocolors20.default.dim("not connected");
|
|
7284
|
+
line(`${t.slug} ${import_picocolors20.default.bold(t.name ?? t.slug)} ${status}`);
|
|
6328
7285
|
}
|
|
6329
7286
|
});
|
|
6330
7287
|
})
|
|
@@ -6341,7 +7298,7 @@ function registerApp(program3) {
|
|
|
6341
7298
|
});
|
|
6342
7299
|
ok(
|
|
6343
7300
|
res,
|
|
6344
|
-
() => res.oauth_url ? line(`Open this URL to finish connecting: ${
|
|
7301
|
+
() => 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
7302
|
);
|
|
6346
7303
|
})
|
|
6347
7304
|
);
|
|
@@ -6359,8 +7316,8 @@ function registerApp(program3) {
|
|
|
6359
7316
|
const projectId = requireProject(ctx);
|
|
6360
7317
|
const items = await api(ctx, `/v1/projects/${projectId}/integrations/${args[0]}/tools`);
|
|
6361
7318
|
ok(items, () => {
|
|
6362
|
-
if (!items?.length) return line(
|
|
6363
|
-
for (const t of items) line(`${t.slug} ${
|
|
7319
|
+
if (!items?.length) return line(import_picocolors20.default.dim("No tools found."));
|
|
7320
|
+
for (const t of items) line(`${t.slug} ${import_picocolors20.default.dim(t.description ?? "")}`);
|
|
6364
7321
|
});
|
|
6365
7322
|
})
|
|
6366
7323
|
);
|
|
@@ -6376,7 +7333,7 @@ function registerApp(program3) {
|
|
|
6376
7333
|
}
|
|
6377
7334
|
|
|
6378
7335
|
// src/commands/tool.ts
|
|
6379
|
-
var
|
|
7336
|
+
var import_picocolors21 = __toESM(require_picocolors(), 1);
|
|
6380
7337
|
function registerTool(program3) {
|
|
6381
7338
|
const tool = program3.command("tool").description(
|
|
6382
7339
|
"Computer-use tools: filesystem, shell, screenshot, input control, clipboard, notifications, basic browser"
|
|
@@ -6385,7 +7342,7 @@ function registerTool(program3) {
|
|
|
6385
7342
|
action(async ({ ctx }) => {
|
|
6386
7343
|
const tools = await api(ctx, "/v1/tool/list");
|
|
6387
7344
|
ok(tools, () => {
|
|
6388
|
-
if (!tools?.length) return line(
|
|
7345
|
+
if (!tools?.length) return line(import_picocolors21.default.dim("No tools available."));
|
|
6389
7346
|
const byCategory = /* @__PURE__ */ new Map();
|
|
6390
7347
|
for (const t of tools) {
|
|
6391
7348
|
const list = byCategory.get(t.category) ?? [];
|
|
@@ -6393,9 +7350,9 @@ function registerTool(program3) {
|
|
|
6393
7350
|
byCategory.set(t.category, list);
|
|
6394
7351
|
}
|
|
6395
7352
|
for (const [category, items] of byCategory) {
|
|
6396
|
-
line(
|
|
7353
|
+
line(import_picocolors21.default.bold(category) + ":");
|
|
6397
7354
|
for (const t of items) {
|
|
6398
|
-
line(` ${t.name} ${
|
|
7355
|
+
line(` ${t.name} ${import_picocolors21.default.dim(t.description ?? "")}`);
|
|
6399
7356
|
}
|
|
6400
7357
|
}
|
|
6401
7358
|
});
|
|
@@ -6413,7 +7370,7 @@ function registerTool(program3) {
|
|
|
6413
7370
|
}
|
|
6414
7371
|
|
|
6415
7372
|
// src/commands/memory.ts
|
|
6416
|
-
var
|
|
7373
|
+
var import_picocolors22 = __toESM(require_picocolors(), 1);
|
|
6417
7374
|
function registerMemory(program3) {
|
|
6418
7375
|
const memory = program3.command("memory").description("Durable, cross-conversation project memory (shared with cloud agents on the same project)");
|
|
6419
7376
|
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 +7394,9 @@ function registerMemory(program3) {
|
|
|
6437
7394
|
});
|
|
6438
7395
|
ok(res, () => {
|
|
6439
7396
|
const results = res?.results ?? res ?? [];
|
|
6440
|
-
if (!results?.length) return line(
|
|
7397
|
+
if (!results?.length) return line(import_picocolors22.default.dim("No matching memories."));
|
|
6441
7398
|
for (const r of results) {
|
|
6442
|
-
line(`${
|
|
7399
|
+
line(`${import_picocolors22.default.dim(r.id ?? "?")} ${r.memory ?? r.content ?? ""}`);
|
|
6443
7400
|
}
|
|
6444
7401
|
});
|
|
6445
7402
|
})
|
|
@@ -6456,7 +7413,7 @@ function registerMemory(program3) {
|
|
|
6456
7413
|
}
|
|
6457
7414
|
|
|
6458
7415
|
// src/commands/business.ts
|
|
6459
|
-
var
|
|
7416
|
+
var import_picocolors23 = __toESM(require_picocolors(), 1);
|
|
6460
7417
|
var RESOURCE_PATHS = {
|
|
6461
7418
|
"business-config": "business-config",
|
|
6462
7419
|
"business-settings": "business-settings",
|
|
@@ -6516,7 +7473,7 @@ function registerBusiness(program3) {
|
|
|
6516
7473
|
const projectId = requireProject(ctx);
|
|
6517
7474
|
const [resource] = args;
|
|
6518
7475
|
const res = await api(ctx, businessPath(projectId, resource), { body: JSON.parse(opts.body) });
|
|
6519
|
-
ok(res, () => line(`Created ${resource} ${
|
|
7476
|
+
ok(res, () => line(`Created ${resource} ${import_picocolors23.default.bold(res?.id ?? "")}.`));
|
|
6520
7477
|
})
|
|
6521
7478
|
);
|
|
6522
7479
|
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,7 +7515,7 @@ function businessPath(projectId, resource, subpath) {
|
|
|
6558
7515
|
}
|
|
6559
7516
|
|
|
6560
7517
|
// src/commands/artifact.ts
|
|
6561
|
-
var
|
|
7518
|
+
var import_picocolors24 = __toESM(require_picocolors(), 1);
|
|
6562
7519
|
import { existsSync as existsSync2, statSync } from "fs";
|
|
6563
7520
|
import { resolve as resolve2, basename as basename3 } from "path";
|
|
6564
7521
|
var KINDS = [
|
|
@@ -6575,14 +7532,28 @@ var KINDS = [
|
|
|
6575
7532
|
"audio",
|
|
6576
7533
|
"document",
|
|
6577
7534
|
"archive",
|
|
6578
|
-
"other"
|
|
7535
|
+
"other",
|
|
7536
|
+
// Deliverable shapes — see above.
|
|
7537
|
+
"report",
|
|
7538
|
+
"walkthrough",
|
|
7539
|
+
"before_after",
|
|
7540
|
+
"checks",
|
|
7541
|
+
"web_app",
|
|
7542
|
+
"service",
|
|
7543
|
+
"design"
|
|
6579
7544
|
];
|
|
6580
7545
|
function registerArtifact(program3) {
|
|
6581
7546
|
const artifact = program3.command("artifact").description("Record the files, folders and apps this task produced");
|
|
6582
7547
|
artifact.command("add [path]").description("Register a deliverable so it shows on the task").option(
|
|
6583
7548
|
"-k, --kind <kind>",
|
|
6584
7549
|
`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)").
|
|
7550
|
+
).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(
|
|
7551
|
+
"--data <json>",
|
|
7552
|
+
`figures the card shows, as JSON \u2014 e.g. '{"passed":12,"total":12}'`
|
|
7553
|
+
).option(
|
|
7554
|
+
"--promote",
|
|
7555
|
+
"hand it straight to the task as one of the things the owner asked for"
|
|
7556
|
+
).action(
|
|
6586
7557
|
action(async ({ ctx, opts, args }) => {
|
|
6587
7558
|
const rawPath = args[0];
|
|
6588
7559
|
const url = opts.url;
|
|
@@ -6612,19 +7583,36 @@ function registerArtifact(program3) {
|
|
|
6612
7583
|
{ code: "bad_request" }
|
|
6613
7584
|
);
|
|
6614
7585
|
}
|
|
7586
|
+
let data;
|
|
7587
|
+
if (opts.data) {
|
|
7588
|
+
try {
|
|
7589
|
+
const parsed = JSON.parse(String(opts.data));
|
|
7590
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
7591
|
+
throw new Error("not an object");
|
|
7592
|
+
}
|
|
7593
|
+
data = parsed;
|
|
7594
|
+
} catch (err) {
|
|
7595
|
+
throw new WorkserError(
|
|
7596
|
+
`--data must be a JSON object. ${err instanceof Error ? err.message : String(err)}`,
|
|
7597
|
+
{ code: "bad_request" }
|
|
7598
|
+
);
|
|
7599
|
+
}
|
|
7600
|
+
}
|
|
6615
7601
|
const res = await api(ctx, `/v1/runs/${runTarget(ctx)}/artifacts`, {
|
|
6616
7602
|
body: {
|
|
6617
7603
|
path: absPath,
|
|
6618
7604
|
url,
|
|
6619
7605
|
kind,
|
|
6620
7606
|
title: opts.title || (absPath ? basename3(absPath) : url),
|
|
6621
|
-
description: opts.description
|
|
7607
|
+
description: opts.description,
|
|
7608
|
+
data,
|
|
7609
|
+
promote: opts.promote ? true : void 0
|
|
6622
7610
|
}
|
|
6623
7611
|
});
|
|
6624
7612
|
ok(
|
|
6625
7613
|
res,
|
|
6626
7614
|
() => success(
|
|
6627
|
-
`Recorded ${
|
|
7615
|
+
`Recorded ${import_picocolors24.default.bold(res?.title ?? "artifact")}${res?.kind ? import_picocolors24.default.dim(` (${res.kind})`) : ""}`
|
|
6628
7616
|
)
|
|
6629
7617
|
);
|
|
6630
7618
|
})
|
|
@@ -6638,11 +7626,11 @@ function registerArtifact(program3) {
|
|
|
6638
7626
|
}
|
|
6639
7627
|
function printRun(run) {
|
|
6640
7628
|
if (!run) return;
|
|
6641
|
-
line(` run ${
|
|
7629
|
+
line(` run ${import_picocolors24.default.bold(run.runId)}`);
|
|
6642
7630
|
if (run.taskId) line(` task ${run.taskId}`);
|
|
6643
7631
|
if (run.conversationId) line(` chat ${run.conversationId}`);
|
|
6644
7632
|
if (run.projectId) line(` project ${run.projectId}`);
|
|
6645
|
-
if (run.cwd) line(` folder ${
|
|
7633
|
+
if (run.cwd) line(` folder ${import_picocolors24.default.dim(run.cwd)}`);
|
|
6646
7634
|
}
|
|
6647
7635
|
|
|
6648
7636
|
// src/commands/image.ts
|
|
@@ -6716,7 +7704,7 @@ async function download(url, output) {
|
|
|
6716
7704
|
}
|
|
6717
7705
|
|
|
6718
7706
|
// src/commands/ask.ts
|
|
6719
|
-
var
|
|
7707
|
+
var import_picocolors25 = __toESM(require_picocolors(), 1);
|
|
6720
7708
|
var TYPES = [
|
|
6721
7709
|
"input",
|
|
6722
7710
|
"choice",
|
|
@@ -6767,7 +7755,7 @@ function registerAsk(program3) {
|
|
|
6767
7755
|
code: "bad_request"
|
|
6768
7756
|
});
|
|
6769
7757
|
}
|
|
6770
|
-
info(
|
|
7758
|
+
info(import_picocolors25.default.dim("Waiting for the user to answer\u2026"));
|
|
6771
7759
|
const res = await api(ctx, `/v1/runs/${runTarget(ctx)}/ask`, {
|
|
6772
7760
|
body: {
|
|
6773
7761
|
type,
|
|
@@ -6793,12 +7781,12 @@ function deriveTitle(message) {
|
|
|
6793
7781
|
function printAnswer(res) {
|
|
6794
7782
|
if (!res) return;
|
|
6795
7783
|
if (res.status === "answered") {
|
|
6796
|
-
line(` ${
|
|
7784
|
+
line(` ${import_picocolors25.default.green("answered")}`);
|
|
6797
7785
|
const value = extract(res.response);
|
|
6798
7786
|
if (value) line(` ${value}`);
|
|
6799
7787
|
return;
|
|
6800
7788
|
}
|
|
6801
|
-
line(` ${
|
|
7789
|
+
line(` ${import_picocolors25.default.yellow(res.status)} ${import_picocolors25.default.dim(res.reason ?? "")}`);
|
|
6802
7790
|
}
|
|
6803
7791
|
function extract(response) {
|
|
6804
7792
|
if (response == null) return "";
|
|
@@ -6817,7 +7805,7 @@ function extract(response) {
|
|
|
6817
7805
|
}
|
|
6818
7806
|
|
|
6819
7807
|
// src/commands/search.ts
|
|
6820
|
-
var
|
|
7808
|
+
var import_picocolors26 = __toESM(require_picocolors(), 1);
|
|
6821
7809
|
function registerSearch(program3) {
|
|
6822
7810
|
program3.command("search <query>").description("Search the web (Google-grounded, server-side)").option("-n, --max-results <n>", "max results", "5").action(
|
|
6823
7811
|
action(async ({ ctx, args, opts }) => {
|
|
@@ -6830,9 +7818,9 @@ function registerSearch(program3) {
|
|
|
6830
7818
|
line("");
|
|
6831
7819
|
}
|
|
6832
7820
|
const results = res?.results ?? [];
|
|
6833
|
-
if (!results.length) return line(
|
|
7821
|
+
if (!results.length) return line(import_picocolors26.default.dim("No results."));
|
|
6834
7822
|
for (const r of results) {
|
|
6835
|
-
line(`${r.title ||
|
|
7823
|
+
line(`${r.title || import_picocolors26.default.dim("(untitled)")} ${import_picocolors26.default.dim(r.url)}`);
|
|
6836
7824
|
}
|
|
6837
7825
|
});
|
|
6838
7826
|
})
|
|
@@ -6840,7 +7828,7 @@ function registerSearch(program3) {
|
|
|
6840
7828
|
}
|
|
6841
7829
|
|
|
6842
7830
|
// src/commands/board.ts
|
|
6843
|
-
var
|
|
7831
|
+
var import_picocolors27 = __toESM(require_picocolors(), 1);
|
|
6844
7832
|
|
|
6845
7833
|
// src/commands/record-step.ts
|
|
6846
7834
|
async function recordEntityStep(ctx, opts) {
|
|
@@ -6879,7 +7867,7 @@ function registerBoard(program3) {
|
|
|
6879
7867
|
}
|
|
6880
7868
|
ok(rows, () => {
|
|
6881
7869
|
if (!rows.length) {
|
|
6882
|
-
line(
|
|
7870
|
+
line(import_picocolors27.default.dim("No cards on the Board yet."));
|
|
6883
7871
|
return;
|
|
6884
7872
|
}
|
|
6885
7873
|
for (const r of rows) line(formatRow(r));
|
|
@@ -6894,7 +7882,7 @@ function registerBoard(program3) {
|
|
|
6894
7882
|
`/v1/projects/${projectId}/work-items/${args[0]}`
|
|
6895
7883
|
);
|
|
6896
7884
|
ok(row, () => {
|
|
6897
|
-
line(`${
|
|
7885
|
+
line(`${import_picocolors27.default.bold(row.title)} ${import_picocolors27.default.dim(row.id)}`);
|
|
6898
7886
|
line(`${statusTag(row.status)} priority ${row.priority}`);
|
|
6899
7887
|
if (row.ownerHuman) line(`owner: ${row.ownerHuman}`);
|
|
6900
7888
|
if (row.labels?.length) line(`labels: ${row.labels.join(", ")}`);
|
|
@@ -6935,7 +7923,7 @@ ${row.description}`);
|
|
|
6935
7923
|
refId: row?.id,
|
|
6936
7924
|
output: { workItem: row }
|
|
6937
7925
|
});
|
|
6938
|
-
ok(row, () => line(`Created work item ${
|
|
7926
|
+
ok(row, () => line(`Created work item ${import_picocolors27.default.bold(row?.id ?? "")} \u2014 ${title}`));
|
|
6939
7927
|
})
|
|
6940
7928
|
);
|
|
6941
7929
|
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 +7957,7 @@ ${row.description}`);
|
|
|
6969
7957
|
);
|
|
6970
7958
|
}
|
|
6971
7959
|
const row = await patchItem(ctx, projectId, String(args[0]), body);
|
|
6972
|
-
ok(row, () => line(`Updated ${
|
|
7960
|
+
ok(row, () => line(`Updated ${import_picocolors27.default.bold(row.id)} \u2014 ${row.title} ${statusTag(row.status)}`));
|
|
6973
7961
|
})
|
|
6974
7962
|
);
|
|
6975
7963
|
board.command("move <id> <status>").description(
|
|
@@ -6980,14 +7968,14 @@ ${row.description}`);
|
|
|
6980
7968
|
const status = String(args[1]);
|
|
6981
7969
|
assertStatus(status);
|
|
6982
7970
|
const row = await patchItem(ctx, projectId, String(args[0]), { status });
|
|
6983
|
-
ok(row, () => line(`Moved ${
|
|
7971
|
+
ok(row, () => line(`Moved ${import_picocolors27.default.bold(row.title)} \u2192 ${statusTag(row.status)}`));
|
|
6984
7972
|
})
|
|
6985
7973
|
);
|
|
6986
7974
|
board.command("close <id>").description("Shorthand for `board move <id> done`").action(
|
|
6987
7975
|
action(async ({ ctx, args }) => {
|
|
6988
7976
|
const projectId = requireProject(ctx);
|
|
6989
7977
|
const row = await patchItem(ctx, projectId, String(args[0]), { status: "done" });
|
|
6990
|
-
ok(row, () => line(`Closed ${
|
|
7978
|
+
ok(row, () => line(`Closed ${import_picocolors27.default.bold(row.title)} ${statusTag(row.status)}`));
|
|
6991
7979
|
})
|
|
6992
7980
|
);
|
|
6993
7981
|
}
|
|
@@ -7020,23 +8008,23 @@ function assertPriority(value) {
|
|
|
7020
8008
|
}
|
|
7021
8009
|
}
|
|
7022
8010
|
function formatRow(r) {
|
|
7023
|
-
const labels = r.labels?.length ?
|
|
7024
|
-
const owner = r.ownerHuman ?
|
|
7025
|
-
return `${
|
|
8011
|
+
const labels = r.labels?.length ? import_picocolors27.default.dim(` [${r.labels.join(", ")}]`) : "";
|
|
8012
|
+
const owner = r.ownerHuman ? import_picocolors27.default.dim(` @${r.ownerHuman}`) : "";
|
|
8013
|
+
return `${import_picocolors27.default.dim(r.id)} ${statusTag(r.status)} ${r.title}${labels}${owner}`;
|
|
7026
8014
|
}
|
|
7027
8015
|
function statusTag(status) {
|
|
7028
8016
|
const label = status.padEnd(11);
|
|
7029
|
-
if (status === "done") return
|
|
7030
|
-
if (status === "in-progress") return
|
|
7031
|
-
if (status === "in-review") return
|
|
7032
|
-
return
|
|
8017
|
+
if (status === "done") return import_picocolors27.default.green(label);
|
|
8018
|
+
if (status === "in-progress") return import_picocolors27.default.yellow(label);
|
|
8019
|
+
if (status === "in-review") return import_picocolors27.default.cyan(label);
|
|
8020
|
+
return import_picocolors27.default.dim(label);
|
|
7033
8021
|
}
|
|
7034
8022
|
function collect2(value, previous) {
|
|
7035
8023
|
return [...previous, value];
|
|
7036
8024
|
}
|
|
7037
8025
|
|
|
7038
8026
|
// src/commands/task.ts
|
|
7039
|
-
var
|
|
8027
|
+
var import_picocolors28 = __toESM(require_picocolors(), 1);
|
|
7040
8028
|
var STATUSES2 = ["todo", "working", "checking", "ready", "accepted", "archived"];
|
|
7041
8029
|
var ROLES = ["pm", "architect", "web", "api", "automation", "qa"];
|
|
7042
8030
|
var KINDS2 = ["data_reports", "web", "mobile", "service", "automation", "docs"];
|
|
@@ -7055,7 +8043,7 @@ function registerTask(program3) {
|
|
|
7055
8043
|
}) ?? [];
|
|
7056
8044
|
ok(rows, () => {
|
|
7057
8045
|
if (!rows.length) {
|
|
7058
|
-
line(
|
|
8046
|
+
line(import_picocolors28.default.dim("No tasks on the board yet."));
|
|
7059
8047
|
return;
|
|
7060
8048
|
}
|
|
7061
8049
|
for (const r of rows) line(formatRow2(r));
|
|
@@ -7096,8 +8084,8 @@ function registerTask(program3) {
|
|
|
7096
8084
|
}
|
|
7097
8085
|
});
|
|
7098
8086
|
ok(row, () => {
|
|
7099
|
-
line(`${
|
|
7100
|
-
if (row.role) line(
|
|
8087
|
+
line(`${import_picocolors28.default.green("added")} ${import_picocolors28.default.bold(row.title)} ${import_picocolors28.default.dim(row.key ?? row.id)}`);
|
|
8088
|
+
if (row.role) line(import_picocolors28.default.dim(`role: ${row.role}`));
|
|
7101
8089
|
});
|
|
7102
8090
|
})
|
|
7103
8091
|
);
|
|
@@ -7108,7 +8096,7 @@ function registerTask(program3) {
|
|
|
7108
8096
|
const rows = row.subtasks ?? [];
|
|
7109
8097
|
ok(rows, () => {
|
|
7110
8098
|
if (!rows.length) {
|
|
7111
|
-
line(
|
|
8099
|
+
line(import_picocolors28.default.dim("No steps yet."));
|
|
7112
8100
|
return;
|
|
7113
8101
|
}
|
|
7114
8102
|
rows.forEach((r, i) => line(formatSubtask(r, i + 1)));
|
|
@@ -7133,7 +8121,7 @@ function registerTask(program3) {
|
|
|
7133
8121
|
}
|
|
7134
8122
|
}
|
|
7135
8123
|
);
|
|
7136
|
-
ok(row, () => line(`${
|
|
8124
|
+
ok(row, () => line(`${import_picocolors28.default.green("updated")} ${import_picocolors28.default.bold(row.title)}`));
|
|
7137
8125
|
})
|
|
7138
8126
|
);
|
|
7139
8127
|
subtask.command("remove <id>").description("Drop a step from the plan (only before the work starts)").action(
|
|
@@ -7141,7 +8129,7 @@ function registerTask(program3) {
|
|
|
7141
8129
|
await api(ctx, `/v1/project-tasks/${encodeURIComponent(args[0])}`, {
|
|
7142
8130
|
method: "DELETE"
|
|
7143
8131
|
});
|
|
7144
|
-
ok({ removed: args[0] }, () => line(
|
|
8132
|
+
ok({ removed: args[0] }, () => line(import_picocolors28.default.green("removed")));
|
|
7145
8133
|
})
|
|
7146
8134
|
);
|
|
7147
8135
|
task.command("move <id> <status>").description(`Move a task or step along the board (${STATUSES2.join(" | ")})`).action(
|
|
@@ -7152,7 +8140,27 @@ function registerTask(program3) {
|
|
|
7152
8140
|
`/v1/project-tasks/${encodeURIComponent(args[0])}/move`,
|
|
7153
8141
|
{ body: { status: args[1] } }
|
|
7154
8142
|
);
|
|
7155
|
-
ok(row, () => line(`${
|
|
8143
|
+
ok(row, () => line(`${import_picocolors28.default.green("moved")} ${import_picocolors28.default.bold(row.title)} \u2192 ${args[1]}`));
|
|
8144
|
+
})
|
|
8145
|
+
);
|
|
8146
|
+
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(
|
|
8147
|
+
action(async ({ ctx, args, opts }) => {
|
|
8148
|
+
const row = await api(
|
|
8149
|
+
ctx,
|
|
8150
|
+
`/v1/project-subtasks/${encodeURIComponent(String(args[0]))}/reopen`,
|
|
8151
|
+
{ method: "POST", body: { note: opts.note } }
|
|
8152
|
+
);
|
|
8153
|
+
ok(row, () => {
|
|
8154
|
+
if (row?.reopened) {
|
|
8155
|
+
line(`${import_picocolors28.default.green("sent back")} \u2014 it will be picked up again`);
|
|
8156
|
+
return;
|
|
8157
|
+
}
|
|
8158
|
+
line(
|
|
8159
|
+
import_picocolors28.default.yellow(
|
|
8160
|
+
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"
|
|
8161
|
+
)
|
|
8162
|
+
);
|
|
8163
|
+
});
|
|
7156
8164
|
})
|
|
7157
8165
|
);
|
|
7158
8166
|
task.command("can-start [id]").description("Ask whether work on this task may begin. Refuses until the owner approves.").action(
|
|
@@ -7163,7 +8171,7 @@ function registerTask(program3) {
|
|
|
7163
8171
|
`/v1/project-tasks/${encodeURIComponent(id)}/dispatch-check`,
|
|
7164
8172
|
{ method: "POST" }
|
|
7165
8173
|
);
|
|
7166
|
-
ok(row, () => line(
|
|
8174
|
+
ok(row, () => line(import_picocolors28.default.green("approved \u2014 you may start")));
|
|
7167
8175
|
})
|
|
7168
8176
|
);
|
|
7169
8177
|
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 +8185,7 @@ function registerTask(program3) {
|
|
|
7177
8185
|
);
|
|
7178
8186
|
ok({ awaiting: row2.approval_state === "awaiting", task: row2 }, () => {
|
|
7179
8187
|
line(
|
|
7180
|
-
row2.approval_state === "awaiting" ?
|
|
8188
|
+
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
8189
|
);
|
|
7182
8190
|
});
|
|
7183
8191
|
return;
|
|
@@ -7198,7 +8206,7 @@ function registerTask(program3) {
|
|
|
7198
8206
|
}
|
|
7199
8207
|
}
|
|
7200
8208
|
);
|
|
7201
|
-
ok(row, () => line(`${
|
|
8209
|
+
ok(row, () => line(`${import_picocolors28.default.green(row.approval_state)} ${import_picocolors28.default.bold(row.title)}`));
|
|
7202
8210
|
})
|
|
7203
8211
|
);
|
|
7204
8212
|
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 +8221,7 @@ function registerTask(program3) {
|
|
|
7213
8221
|
`/v1/project-tasks/${encodeURIComponent(id)}/move`,
|
|
7214
8222
|
{ body: { status: "ready" } }
|
|
7215
8223
|
);
|
|
7216
|
-
ok(row, () => line(`${
|
|
8224
|
+
ok(row, () => line(`${import_picocolors28.default.green("ready")} ${import_picocolors28.default.bold(row.title)}`));
|
|
7217
8225
|
})
|
|
7218
8226
|
);
|
|
7219
8227
|
}
|
|
@@ -7258,19 +8266,19 @@ function assertOneOf(flag, value, allowed) {
|
|
|
7258
8266
|
}
|
|
7259
8267
|
}
|
|
7260
8268
|
function formatRow2(r) {
|
|
7261
|
-
const key =
|
|
7262
|
-
const steps = r.subtaskTotal ?
|
|
7263
|
-
const gate = r.approval_state === "awaiting" ?
|
|
8269
|
+
const key = import_picocolors28.default.dim((r.key ?? r.id.slice(0, 8)).padEnd(10));
|
|
8270
|
+
const steps = r.subtaskTotal ? import_picocolors28.default.dim(` ${r.subtaskDone}/${r.subtaskTotal}`) : "";
|
|
8271
|
+
const gate = r.approval_state === "awaiting" ? import_picocolors28.default.yellow(" awaiting approval") : "";
|
|
7264
8272
|
return `${key} ${statusTag2(r.status)} ${r.title}${steps}${gate}`;
|
|
7265
8273
|
}
|
|
7266
8274
|
function formatSubtask(r, index) {
|
|
7267
|
-
const n =
|
|
7268
|
-
const role = r.role ?
|
|
7269
|
-
const scope = r.scope_paths?.length ?
|
|
8275
|
+
const n = import_picocolors28.default.dim(String(index).padStart(2, "0"));
|
|
8276
|
+
const role = r.role ? import_picocolors28.default.dim(` [${r.role}]`) : "";
|
|
8277
|
+
const scope = r.scope_paths?.length ? import_picocolors28.default.dim(` owns: ${r.scope_paths.join(", ")}`) : "";
|
|
7270
8278
|
return `${n} ${statusTag2(r.status)} ${r.title}${role}${scope}`;
|
|
7271
8279
|
}
|
|
7272
8280
|
function printTask(row) {
|
|
7273
|
-
line(`${
|
|
8281
|
+
line(`${import_picocolors28.default.bold(row.title)} ${import_picocolors28.default.dim(row.key ?? row.id)}`);
|
|
7274
8282
|
line(`${statusTag2(row.status)} approval: ${row.approval_state}`);
|
|
7275
8283
|
if (row.summary) line(`
|
|
7276
8284
|
${row.summary}`);
|
|
@@ -7282,31 +8290,127 @@ touches: ${row.targets.map((t) => t.appName ?? t.ref ?? t.kind).join(", ")}`
|
|
|
7282
8290
|
}
|
|
7283
8291
|
if (row.subtasks?.length) {
|
|
7284
8292
|
line(`
|
|
7285
|
-
${
|
|
8293
|
+
${import_picocolors28.default.bold("steps")}`);
|
|
7286
8294
|
row.subtasks.forEach((s, i) => line(formatSubtask(s, i + 1)));
|
|
7287
8295
|
} else {
|
|
7288
|
-
line(
|
|
8296
|
+
line(import_picocolors28.default.dim("\nNo steps yet."));
|
|
7289
8297
|
}
|
|
7290
8298
|
}
|
|
7291
8299
|
function statusTag2(status) {
|
|
7292
8300
|
switch (status) {
|
|
7293
8301
|
case "ready":
|
|
7294
|
-
return
|
|
8302
|
+
return import_picocolors28.default.green("[ready]");
|
|
7295
8303
|
case "working":
|
|
7296
|
-
return
|
|
8304
|
+
return import_picocolors28.default.blue("[working]");
|
|
7297
8305
|
case "checking":
|
|
7298
|
-
return
|
|
8306
|
+
return import_picocolors28.default.cyan("[checking]");
|
|
7299
8307
|
case "accepted":
|
|
7300
|
-
return
|
|
8308
|
+
return import_picocolors28.default.green("[accepted]");
|
|
7301
8309
|
case "archived":
|
|
7302
|
-
return
|
|
8310
|
+
return import_picocolors28.default.dim("[archived]");
|
|
7303
8311
|
default:
|
|
7304
|
-
return
|
|
8312
|
+
return import_picocolors28.default.dim("[todo]");
|
|
8313
|
+
}
|
|
8314
|
+
}
|
|
8315
|
+
|
|
8316
|
+
// src/role-guard.ts
|
|
8317
|
+
var READS = [
|
|
8318
|
+
"task",
|
|
8319
|
+
"board",
|
|
8320
|
+
"doc",
|
|
8321
|
+
"decision",
|
|
8322
|
+
"memory",
|
|
8323
|
+
"search",
|
|
8324
|
+
"verify",
|
|
8325
|
+
"logs",
|
|
8326
|
+
"status",
|
|
8327
|
+
"help",
|
|
8328
|
+
"whoami",
|
|
8329
|
+
"login",
|
|
8330
|
+
"auth",
|
|
8331
|
+
"project",
|
|
8332
|
+
"open",
|
|
8333
|
+
"doctor",
|
|
8334
|
+
// Both READ and report. `scan` reads files and shells out to npm; `health`
|
|
8335
|
+
// makes a GET request to an address that is already public. Neither can
|
|
8336
|
+
// change anything, which is why the roles that exist to look — qa, security,
|
|
8337
|
+
// sre, analyst — get them without getting anything else.
|
|
8338
|
+
"scan",
|
|
8339
|
+
"health",
|
|
8340
|
+
// Read-only views of what is running. Added with Phase 6a: an SRE that can
|
|
8341
|
+
// read logs but cannot list deployments or read the app's address is being
|
|
8342
|
+
// asked to diagnose an outage with one eye shut.
|
|
8343
|
+
"urls",
|
|
8344
|
+
"deployments",
|
|
8345
|
+
// Reading the plan and what is used against it. An agent proposing "add
|
|
8346
|
+
// another project" can only sensibly propose it if it can find out the plan
|
|
8347
|
+
// allows two and two already exist.
|
|
8348
|
+
"usage"
|
|
8349
|
+
];
|
|
8350
|
+
var BUILDS = [
|
|
8351
|
+
...READS,
|
|
8352
|
+
"app",
|
|
8353
|
+
"env",
|
|
8354
|
+
"db",
|
|
8355
|
+
"storage",
|
|
8356
|
+
"checkpoint",
|
|
8357
|
+
"artifact",
|
|
8358
|
+
"image",
|
|
8359
|
+
"design",
|
|
8360
|
+
"ask",
|
|
8361
|
+
"sync",
|
|
8362
|
+
"tool",
|
|
8363
|
+
"workflow",
|
|
8364
|
+
"neon",
|
|
8365
|
+
"business"
|
|
8366
|
+
];
|
|
8367
|
+
var ROLE_VERBS = {
|
|
8368
|
+
pm: [...READS, "ask", "app"],
|
|
8369
|
+
architect: [...BUILDS, "versions"],
|
|
8370
|
+
web: BUILDS,
|
|
8371
|
+
api: BUILDS,
|
|
8372
|
+
mobile: BUILDS,
|
|
8373
|
+
python: BUILDS,
|
|
8374
|
+
automation: BUILDS,
|
|
8375
|
+
designer: BUILDS,
|
|
8376
|
+
qa: READS,
|
|
8377
|
+
security: READS,
|
|
8378
|
+
analyst: READS,
|
|
8379
|
+
sre: [...READS, "deploy", "domain", "versions"],
|
|
8380
|
+
devops: [...BUILDS, "deploy", "domain", "versions"]
|
|
8381
|
+
// NOTE: `deployments` reaches READS above, so every role can LIST and
|
|
8382
|
+
// INSPECT. That is correct — history is a read. The two verbs that change
|
|
8383
|
+
// production (`promote`, `rollback`) are not gated here at all, and must not
|
|
8384
|
+
// be: they are gated in the DAEMON, as `deploy.prod`, which is a door "just
|
|
8385
|
+
// do it" cannot open. A second, verb-name-based rule here would be a weaker
|
|
8386
|
+
// copy of a control that already works.
|
|
8387
|
+
};
|
|
8388
|
+
var NEVER = {
|
|
8389
|
+
// Approving is the owner's, full stop. An agent that can approve the plan it
|
|
8390
|
+
// proposed has removed the only gate this product has.
|
|
8391
|
+
"task approval": "Only the owner can approve a plan."
|
|
8392
|
+
};
|
|
8393
|
+
function assertRoleMayRun(argv) {
|
|
8394
|
+
const role = (process.env.WORKSER_ROLE ?? "").trim();
|
|
8395
|
+
if (!role) return;
|
|
8396
|
+
const verb = argv[0];
|
|
8397
|
+
if (!verb) return;
|
|
8398
|
+
const pair = `${argv[0]} ${argv[1] ?? ""}`.trim();
|
|
8399
|
+
if (NEVER[pair] && !(pair === "task approval" && (argv[2] === "request" || !argv[2]))) {
|
|
8400
|
+
throw new WorkserError(NEVER[pair], { code: "role_forbidden" });
|
|
8401
|
+
}
|
|
8402
|
+
const allowed = ROLE_VERBS[role];
|
|
8403
|
+
const list = allowed ?? READS;
|
|
8404
|
+
if (!list.includes(verb)) {
|
|
8405
|
+
throw new WorkserError(
|
|
8406
|
+
`The ${role} role can't run \`workser ${verb}\`. Report what you found instead, and the step that owns this will do it.`,
|
|
8407
|
+
{ code: "role_forbidden" }
|
|
8408
|
+
);
|
|
7305
8409
|
}
|
|
7306
8410
|
}
|
|
7307
8411
|
|
|
7308
8412
|
// src/commands/decision.ts
|
|
7309
|
-
var
|
|
8413
|
+
var import_picocolors29 = __toESM(require_picocolors(), 1);
|
|
7310
8414
|
function registerDecision(program3) {
|
|
7311
8415
|
const decision = program3.command("decision").description("Read and record the project's architecture decisions");
|
|
7312
8416
|
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(
|
|
@@ -7319,11 +8423,11 @@ function registerDecision(program3) {
|
|
|
7319
8423
|
rows = applyLimit(rows, opts.limit);
|
|
7320
8424
|
ok(rows, () => {
|
|
7321
8425
|
if (!rows.length) {
|
|
7322
|
-
line(
|
|
8426
|
+
line(import_picocolors29.default.dim("No decisions recorded yet."));
|
|
7323
8427
|
return;
|
|
7324
8428
|
}
|
|
7325
8429
|
for (const r of rows) {
|
|
7326
|
-
line(`${
|
|
8430
|
+
line(`${import_picocolors29.default.dim(r.id)} ${import_picocolors29.default.dim(shortDate(r.createdAt))} ${r.title}`);
|
|
7327
8431
|
line(` ${truncate(r.decision, 100)}`);
|
|
7328
8432
|
}
|
|
7329
8433
|
});
|
|
@@ -7337,16 +8441,16 @@ function registerDecision(program3) {
|
|
|
7337
8441
|
`/v1/projects/${projectId}/architecture-decisions/${args[0]}`
|
|
7338
8442
|
);
|
|
7339
8443
|
ok(row, () => {
|
|
7340
|
-
line(`${
|
|
7341
|
-
line(
|
|
8444
|
+
line(`${import_picocolors29.default.bold(row.title)} ${import_picocolors29.default.dim(row.id)}`);
|
|
8445
|
+
line(import_picocolors29.default.dim(`${row.status} \xB7 ${shortDate(row.createdAt)}`));
|
|
7342
8446
|
line(`
|
|
7343
|
-
${
|
|
8447
|
+
${import_picocolors29.default.bold("Context")}
|
|
7344
8448
|
${row.context}`);
|
|
7345
8449
|
line(`
|
|
7346
|
-
${
|
|
8450
|
+
${import_picocolors29.default.bold("Decision")}
|
|
7347
8451
|
${row.decision}`);
|
|
7348
8452
|
if (row.consequences) line(`
|
|
7349
|
-
${
|
|
8453
|
+
${import_picocolors29.default.bold("Consequences")}
|
|
7350
8454
|
${row.consequences}`);
|
|
7351
8455
|
});
|
|
7352
8456
|
})
|
|
@@ -7375,7 +8479,7 @@ ${row.consequences}`);
|
|
|
7375
8479
|
refId: row?.id,
|
|
7376
8480
|
output: { decision: row }
|
|
7377
8481
|
});
|
|
7378
|
-
ok(row, () => line(`Recorded decision ${
|
|
8482
|
+
ok(row, () => line(`Recorded decision ${import_picocolors29.default.bold(row?.id ?? "")} \u2014 ${title}`));
|
|
7379
8483
|
})
|
|
7380
8484
|
);
|
|
7381
8485
|
const requirement = program3.command("requirement").description("Read and record the project's requirements");
|
|
@@ -7387,11 +8491,11 @@ ${row.consequences}`);
|
|
|
7387
8491
|
rows = applyLimit(rows, opts.limit);
|
|
7388
8492
|
ok(rows, () => {
|
|
7389
8493
|
if (!rows.length) {
|
|
7390
|
-
line(
|
|
8494
|
+
line(import_picocolors29.default.dim("No requirements recorded yet."));
|
|
7391
8495
|
return;
|
|
7392
8496
|
}
|
|
7393
8497
|
for (const r of rows) {
|
|
7394
|
-
line(`${
|
|
8498
|
+
line(`${import_picocolors29.default.dim(r.id)} ${r.status.padEnd(9)} ${r.title}`);
|
|
7395
8499
|
}
|
|
7396
8500
|
});
|
|
7397
8501
|
})
|
|
@@ -7404,8 +8508,8 @@ ${row.consequences}`);
|
|
|
7404
8508
|
`/v1/projects/${projectId}/requirements/${args[0]}`
|
|
7405
8509
|
);
|
|
7406
8510
|
ok(row, () => {
|
|
7407
|
-
line(`${
|
|
7408
|
-
line(
|
|
8511
|
+
line(`${import_picocolors29.default.bold(row.title)} ${import_picocolors29.default.dim(row.id)}`);
|
|
8512
|
+
line(import_picocolors29.default.dim(`${row.status} \xB7 ${shortDate(row.createdAt)}`));
|
|
7409
8513
|
line(`
|
|
7410
8514
|
${row.body}`);
|
|
7411
8515
|
});
|
|
@@ -7431,7 +8535,7 @@ ${row.body}`);
|
|
|
7431
8535
|
refId: row?.id,
|
|
7432
8536
|
output: { requirement: row }
|
|
7433
8537
|
});
|
|
7434
|
-
ok(row, () => line(`Recorded requirement ${
|
|
8538
|
+
ok(row, () => line(`Recorded requirement ${import_picocolors29.default.bold(row?.id ?? "")} \u2014 ${title}`));
|
|
7435
8539
|
})
|
|
7436
8540
|
);
|
|
7437
8541
|
requirement.command("update <id>").description(
|
|
@@ -7460,7 +8564,7 @@ ${row.body}`);
|
|
|
7460
8564
|
code: "bad_request"
|
|
7461
8565
|
});
|
|
7462
8566
|
}
|
|
7463
|
-
ok(row, () => line(`Updated requirement ${
|
|
8567
|
+
ok(row, () => line(`Updated requirement ${import_picocolors29.default.bold(row.id)} \u2014 ${row.title} (${row.status})`));
|
|
7464
8568
|
})
|
|
7465
8569
|
);
|
|
7466
8570
|
}
|
|
@@ -7483,7 +8587,41 @@ function shortDate(iso) {
|
|
|
7483
8587
|
}
|
|
7484
8588
|
|
|
7485
8589
|
// src/commands/doc.ts
|
|
7486
|
-
var
|
|
8590
|
+
var import_picocolors30 = __toESM(require_picocolors(), 1);
|
|
8591
|
+
|
|
8592
|
+
// src/mermaid-fences.ts
|
|
8593
|
+
function hasDiagram(code) {
|
|
8594
|
+
if (!code) return false;
|
|
8595
|
+
return code.split("\n").some((line2) => {
|
|
8596
|
+
const text = line2.trim();
|
|
8597
|
+
return !!text && !text.startsWith("%%");
|
|
8598
|
+
});
|
|
8599
|
+
}
|
|
8600
|
+
function diagramKind(code) {
|
|
8601
|
+
if (!code) return null;
|
|
8602
|
+
for (const line2 of code.split("\n")) {
|
|
8603
|
+
const text = line2.trim();
|
|
8604
|
+
if (!text || text.startsWith("%%")) continue;
|
|
8605
|
+
const match = /^([A-Za-z][A-Za-z-]*)/.exec(text);
|
|
8606
|
+
return match ? match[1] : null;
|
|
8607
|
+
}
|
|
8608
|
+
return null;
|
|
8609
|
+
}
|
|
8610
|
+
function extractDiagrams(markdown) {
|
|
8611
|
+
if (!markdown) return [];
|
|
8612
|
+
const out = [];
|
|
8613
|
+
const pattern = /^[ \t]*```[ \t]*mermaid[^\n]*\n([\s\S]*?)^[ \t]*```[ \t]*$/gm;
|
|
8614
|
+
let match;
|
|
8615
|
+
while ((match = pattern.exec(markdown)) !== null) {
|
|
8616
|
+
const body = match[1].replace(/\s+$/, "");
|
|
8617
|
+
if (hasDiagram(body)) out.push(body);
|
|
8618
|
+
}
|
|
8619
|
+
return out;
|
|
8620
|
+
}
|
|
8621
|
+
|
|
8622
|
+
// src/commands/doc.ts
|
|
8623
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
8624
|
+
import { isAbsolute, join as join2 } from "path";
|
|
7487
8625
|
function registerDoc(program3) {
|
|
7488
8626
|
const doc = program3.command("doc").description("Read and write project documents");
|
|
7489
8627
|
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(
|
|
@@ -7494,13 +8632,13 @@ function registerDoc(program3) {
|
|
|
7494
8632
|
}) ?? [];
|
|
7495
8633
|
ok(rows, () => {
|
|
7496
8634
|
if (!rows.length) {
|
|
7497
|
-
line(
|
|
8635
|
+
line(import_picocolors30.default.dim("No documents yet."));
|
|
7498
8636
|
return;
|
|
7499
8637
|
}
|
|
7500
8638
|
for (const r of rows) {
|
|
7501
|
-
const link = r.workItemId ?
|
|
7502
|
-
const file = r.filePath ?
|
|
7503
|
-
line(`${
|
|
8639
|
+
const link = r.workItemId ? import_picocolors30.default.dim(` \u21B3 ${r.workItemId}`) : "";
|
|
8640
|
+
const file = r.filePath ? import_picocolors30.default.dim(` ${r.filePath}`) : "";
|
|
8641
|
+
line(`${import_picocolors30.default.dim(r.id)} ${r.title}${link}${file}`);
|
|
7504
8642
|
}
|
|
7505
8643
|
});
|
|
7506
8644
|
})
|
|
@@ -7514,17 +8652,17 @@ function registerDoc(program3) {
|
|
|
7514
8652
|
);
|
|
7515
8653
|
if (opts.markdown) {
|
|
7516
8654
|
ok({ id: row.id, title: row.title, filePath: row.filePath }, () => {
|
|
7517
|
-
line(`${
|
|
8655
|
+
line(`${import_picocolors30.default.bold(row.title)} ${import_picocolors30.default.dim(row.id)}`);
|
|
7518
8656
|
line(
|
|
7519
|
-
row.filePath ? `Read it at ${
|
|
8657
|
+
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.")
|
|
7520
8658
|
);
|
|
7521
8659
|
});
|
|
7522
8660
|
return;
|
|
7523
8661
|
}
|
|
7524
8662
|
ok(row, () => {
|
|
7525
|
-
line(`${
|
|
7526
|
-
if (row.workItemId) line(
|
|
7527
|
-
if (row.filePath) line(
|
|
8663
|
+
line(`${import_picocolors30.default.bold(row.title)} ${import_picocolors30.default.dim(row.id)}`);
|
|
8664
|
+
if (row.workItemId) line(import_picocolors30.default.dim(`linked to work item ${row.workItemId}`));
|
|
8665
|
+
if (row.filePath) line(import_picocolors30.default.dim(`markdown mirror: ${row.filePath}`));
|
|
7528
8666
|
line("");
|
|
7529
8667
|
line(row.contentJson);
|
|
7530
8668
|
});
|
|
@@ -7553,7 +8691,54 @@ function registerDoc(program3) {
|
|
|
7553
8691
|
refId: row?.id,
|
|
7554
8692
|
output: { document: row }
|
|
7555
8693
|
});
|
|
7556
|
-
ok(row, () => line(`Created document ${
|
|
8694
|
+
ok(row, () => line(`Created document ${import_picocolors30.default.bold(row?.id ?? "")} \u2014 ${title}`));
|
|
8695
|
+
})
|
|
8696
|
+
);
|
|
8697
|
+
doc.command("diagram <id>").description(
|
|
8698
|
+
"List the diagrams in a document \u2014 `--check` fails when it has none"
|
|
8699
|
+
).option("--check", "exit non-zero when the document contains no diagram").action(
|
|
8700
|
+
action(async ({ ctx, args, opts }) => {
|
|
8701
|
+
const projectId = requireProject(ctx);
|
|
8702
|
+
const row = await api(
|
|
8703
|
+
ctx,
|
|
8704
|
+
`/v1/projects/${projectId}/documents/${args[0]}`
|
|
8705
|
+
);
|
|
8706
|
+
if (!row) {
|
|
8707
|
+
throw new WorkserError(`No document with id "${args[0]}" on this project.`, {
|
|
8708
|
+
code: "bad_request"
|
|
8709
|
+
});
|
|
8710
|
+
}
|
|
8711
|
+
const markdown = readMirror(ctx.cwd, row.filePath);
|
|
8712
|
+
const diagrams = markdown === null ? [] : extractDiagrams(markdown);
|
|
8713
|
+
const payload = {
|
|
8714
|
+
id: row.id,
|
|
8715
|
+
title: row.title,
|
|
8716
|
+
filePath: row.filePath,
|
|
8717
|
+
diagrams: diagrams.map((code) => ({ kind: diagramKind(code), code }))
|
|
8718
|
+
};
|
|
8719
|
+
ok(payload, () => {
|
|
8720
|
+
line(`${import_picocolors30.default.bold(row.title)} ${import_picocolors30.default.dim(row.id)}`);
|
|
8721
|
+
if (markdown === null) {
|
|
8722
|
+
line(
|
|
8723
|
+
import_picocolors30.default.dim(
|
|
8724
|
+
row.filePath ? `Could not read ${row.filePath} from this folder.` : "This document has no markdown mirror on disk yet."
|
|
8725
|
+
)
|
|
8726
|
+
);
|
|
8727
|
+
} else if (!diagrams.length) {
|
|
8728
|
+
line(import_picocolors30.default.dim("No diagrams in this document."));
|
|
8729
|
+
} else {
|
|
8730
|
+
for (const [i, code] of diagrams.entries()) {
|
|
8731
|
+
const kind = diagramKind(code) ?? "diagram";
|
|
8732
|
+
line(`${import_picocolors30.default.dim(String(i + 1))} ${kind} ${import_picocolors30.default.dim(`${code.split("\n").length} lines`)}`);
|
|
8733
|
+
}
|
|
8734
|
+
}
|
|
8735
|
+
});
|
|
8736
|
+
if (opts.check && !diagrams.length) {
|
|
8737
|
+
throw new WorkserError(
|
|
8738
|
+
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.`,
|
|
8739
|
+
{ code: "bad_request" }
|
|
8740
|
+
);
|
|
8741
|
+
}
|
|
7557
8742
|
})
|
|
7558
8743
|
);
|
|
7559
8744
|
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(
|
|
@@ -7580,13 +8765,21 @@ function registerDoc(program3) {
|
|
|
7580
8765
|
code: "bad_request"
|
|
7581
8766
|
});
|
|
7582
8767
|
}
|
|
7583
|
-
ok(row, () => line(`Updated document ${
|
|
8768
|
+
ok(row, () => line(`Updated document ${import_picocolors30.default.bold(row.id)} \u2014 ${row.title}`));
|
|
7584
8769
|
})
|
|
7585
8770
|
);
|
|
7586
8771
|
}
|
|
8772
|
+
function readMirror(cwd, filePath) {
|
|
8773
|
+
if (!filePath) return null;
|
|
8774
|
+
try {
|
|
8775
|
+
return readFileSync2(isAbsolute(filePath) ? filePath : join2(cwd, filePath), "utf8");
|
|
8776
|
+
} catch {
|
|
8777
|
+
return null;
|
|
8778
|
+
}
|
|
8779
|
+
}
|
|
7587
8780
|
|
|
7588
8781
|
// src/commands/design.ts
|
|
7589
|
-
var
|
|
8782
|
+
var import_picocolors31 = __toESM(require_picocolors(), 1);
|
|
7590
8783
|
function registerDesign(program3) {
|
|
7591
8784
|
const design = program3.command("design").description("Read the project's brand (colours, fonts, logo)");
|
|
7592
8785
|
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(
|
|
@@ -7600,11 +8793,11 @@ function registerDesign(program3) {
|
|
|
7600
8793
|
if (opts.raw) {
|
|
7601
8794
|
ok(files, () => {
|
|
7602
8795
|
if (!files.length) {
|
|
7603
|
-
line(
|
|
8796
|
+
line(import_picocolors31.default.dim("No brand set for this project."));
|
|
7604
8797
|
return;
|
|
7605
8798
|
}
|
|
7606
8799
|
for (const f of files) {
|
|
7607
|
-
line(
|
|
8800
|
+
line(import_picocolors31.default.bold(f.path));
|
|
7608
8801
|
line(f.contents);
|
|
7609
8802
|
line("");
|
|
7610
8803
|
}
|
|
@@ -7621,21 +8814,21 @@ function registerDesign(program3) {
|
|
|
7621
8814
|
} : { hasBrand: false, colors: {}, fonts: {}, brand: {}, files: [] };
|
|
7622
8815
|
ok(summary, () => {
|
|
7623
8816
|
if (!tokens) {
|
|
7624
|
-
line(
|
|
8817
|
+
line(import_picocolors31.default.dim("No brand set for this project \u2014 choose sensible styling yourself."));
|
|
7625
8818
|
return;
|
|
7626
8819
|
}
|
|
7627
8820
|
for (const [name, value] of Object.entries(tokens.brand)) {
|
|
7628
|
-
line(`${
|
|
8821
|
+
line(`${import_picocolors31.default.dim(name.padEnd(12))} ${value}`);
|
|
7629
8822
|
}
|
|
7630
8823
|
for (const [name, value] of Object.entries(tokens.color)) {
|
|
7631
|
-
line(`${
|
|
8824
|
+
line(`${import_picocolors31.default.dim(`color.${name}`.padEnd(12))} ${value}`);
|
|
7632
8825
|
}
|
|
7633
8826
|
for (const [name, value] of Object.entries(tokens.font)) {
|
|
7634
|
-
line(`${
|
|
8827
|
+
line(`${import_picocolors31.default.dim(`font.${name}`.padEnd(12))} ${value}`);
|
|
7635
8828
|
}
|
|
7636
8829
|
line("");
|
|
7637
8830
|
line(
|
|
7638
|
-
|
|
8831
|
+
import_picocolors31.default.dim(
|
|
7639
8832
|
`Generated into the working tree as ${files.map((f) => f.path).join(", ")} \u2014 wire those in, never edit them.`
|
|
7640
8833
|
)
|
|
7641
8834
|
);
|
|
@@ -7665,9 +8858,1005 @@ function unwrap(group) {
|
|
|
7665
8858
|
);
|
|
7666
8859
|
}
|
|
7667
8860
|
|
|
8861
|
+
// src/commands/api.ts
|
|
8862
|
+
var import_picocolors32 = __toESM(require_picocolors(), 1);
|
|
8863
|
+
import { readdirSync, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
8864
|
+
import { join as join3, relative, sep } from "path";
|
|
8865
|
+
|
|
8866
|
+
// src/api-spec.ts
|
|
8867
|
+
var SPEC_FILES = [
|
|
8868
|
+
"api/openapi.json",
|
|
8869
|
+
"openapi.json",
|
|
8870
|
+
"api/openapi.yaml",
|
|
8871
|
+
"openapi.yaml",
|
|
8872
|
+
"api/openapi.yml",
|
|
8873
|
+
"openapi.yml"
|
|
8874
|
+
];
|
|
8875
|
+
function fromNextRoute(file) {
|
|
8876
|
+
const match = /^app\/(.*\/)?route\.(t|j)sx?$/.exec(file);
|
|
8877
|
+
if (!match) return null;
|
|
8878
|
+
const segments = (match[1] ?? "").split("/").filter(Boolean).filter((s) => !(s.startsWith("(") && s.endsWith(")"))).map((s) => {
|
|
8879
|
+
const dynamic = /^\[\.{0,3}(.+?)\]$/.exec(s);
|
|
8880
|
+
return dynamic ? `{${dynamic[1]}}` : s;
|
|
8881
|
+
});
|
|
8882
|
+
return `/${segments.join("/")}`.replace(/\/+$/, "") || "/";
|
|
8883
|
+
}
|
|
8884
|
+
function fromPythonFile(file) {
|
|
8885
|
+
const match = /^api\/(.+)\.py$/.exec(file);
|
|
8886
|
+
if (!match) return null;
|
|
8887
|
+
const stem = match[1];
|
|
8888
|
+
if (stem === "index") return "/api";
|
|
8889
|
+
if (stem.endsWith("/index")) return `/api/${stem.slice(0, -"/index".length)}`;
|
|
8890
|
+
return `/api/${stem}`;
|
|
8891
|
+
}
|
|
8892
|
+
function discoverRoutes(files) {
|
|
8893
|
+
const seen = /* @__PURE__ */ new Map();
|
|
8894
|
+
for (const raw of files) {
|
|
8895
|
+
const file = raw.replace(/^\.\//, "");
|
|
8896
|
+
const path = fromNextRoute(file) ?? fromPythonFile(file);
|
|
8897
|
+
if (!path) continue;
|
|
8898
|
+
if (!seen.has(path)) seen.set(path, { path, file });
|
|
8899
|
+
}
|
|
8900
|
+
return [...seen.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
8901
|
+
}
|
|
8902
|
+
function specPaths(text) {
|
|
8903
|
+
if (!text) return [];
|
|
8904
|
+
const trimmed = text.trim();
|
|
8905
|
+
if (!trimmed) return [];
|
|
8906
|
+
if (trimmed.startsWith("{")) {
|
|
8907
|
+
try {
|
|
8908
|
+
const parsed = JSON.parse(trimmed);
|
|
8909
|
+
const paths = parsed?.paths;
|
|
8910
|
+
if (!paths || typeof paths !== "object") return [];
|
|
8911
|
+
return Object.keys(paths).filter((k) => k.startsWith("/")).sort();
|
|
8912
|
+
} catch {
|
|
8913
|
+
return [];
|
|
8914
|
+
}
|
|
8915
|
+
}
|
|
8916
|
+
const out = [];
|
|
8917
|
+
let inPaths = false;
|
|
8918
|
+
let indent = 0;
|
|
8919
|
+
for (const line2 of trimmed.split(/\r?\n/)) {
|
|
8920
|
+
if (!line2.trim() || line2.trim().startsWith("#")) continue;
|
|
8921
|
+
const leading = line2.length - line2.trimStart().length;
|
|
8922
|
+
if (!inPaths) {
|
|
8923
|
+
if (/^paths\s*:/.test(line2.trim()) && leading === 0) {
|
|
8924
|
+
inPaths = true;
|
|
8925
|
+
indent = -1;
|
|
8926
|
+
}
|
|
8927
|
+
continue;
|
|
8928
|
+
}
|
|
8929
|
+
if (indent === -1) {
|
|
8930
|
+
if (leading === 0) break;
|
|
8931
|
+
indent = leading;
|
|
8932
|
+
}
|
|
8933
|
+
if (leading < indent) break;
|
|
8934
|
+
if (leading > indent) continue;
|
|
8935
|
+
const key = /^\s*["']?(\/[^"':]*)["']?\s*:/.exec(line2);
|
|
8936
|
+
if (key) out.push(key[1].trim());
|
|
8937
|
+
}
|
|
8938
|
+
return [...new Set(out)].sort();
|
|
8939
|
+
}
|
|
8940
|
+
var EXEMPT = /* @__PURE__ */ new Set(["/api/health", "/health", "/api", "/"]);
|
|
8941
|
+
function specReport(routes, documented) {
|
|
8942
|
+
const declared = new Set(documented);
|
|
8943
|
+
const served = new Set(routes.map((r) => r.path));
|
|
8944
|
+
const missing = routes.filter(
|
|
8945
|
+
(r) => !declared.has(r.path) && !EXEMPT.has(r.path)
|
|
8946
|
+
);
|
|
8947
|
+
const stale = documented.filter((p) => !served.has(p)).sort();
|
|
8948
|
+
return {
|
|
8949
|
+
routes,
|
|
8950
|
+
documented: [...declared].sort(),
|
|
8951
|
+
missing,
|
|
8952
|
+
stale,
|
|
8953
|
+
ok: missing.length === 0
|
|
8954
|
+
};
|
|
8955
|
+
}
|
|
8956
|
+
function specSummary(report, specFile) {
|
|
8957
|
+
if (!specFile) {
|
|
8958
|
+
return `This service has no API description. Write one at ${SPEC_FILES[0]} listing the routes it serves.`;
|
|
8959
|
+
}
|
|
8960
|
+
if (report.ok) {
|
|
8961
|
+
const n = report.routes.length;
|
|
8962
|
+
return n === 1 ? `The one route this service has is described in ${specFile}.` : `All ${n} routes are described in ${specFile}.`;
|
|
8963
|
+
}
|
|
8964
|
+
const names = report.missing.map((m) => m.path).join(", ");
|
|
8965
|
+
return `${report.missing.length} route${report.missing.length === 1 ? " is" : "s are"} not described in ${specFile}: ${names}`;
|
|
8966
|
+
}
|
|
8967
|
+
|
|
8968
|
+
// src/commands/api.ts
|
|
8969
|
+
function registerApi(program3) {
|
|
8970
|
+
const cmd = program3.command("api").description("Call this service and check it describes its own routes");
|
|
8971
|
+
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(
|
|
8972
|
+
action(async ({ ctx, opts }) => {
|
|
8973
|
+
const appId = requireApp(opts.app);
|
|
8974
|
+
const res = await api(ctx, `/v1/apps/${encodeURIComponent(appId)}/api/requests`);
|
|
8975
|
+
ok(res, () => {
|
|
8976
|
+
for (const note of res?.notes ?? []) line(import_picocolors32.default.dim(note));
|
|
8977
|
+
for (const r of res?.requests ?? []) {
|
|
8978
|
+
line(
|
|
8979
|
+
`${import_picocolors32.default.dim(r.method.padEnd(6))}${r.path}${r.note ? import_picocolors32.default.dim(` ${r.note}`) : ""}`
|
|
8980
|
+
);
|
|
8981
|
+
}
|
|
8982
|
+
});
|
|
8983
|
+
})
|
|
8984
|
+
);
|
|
8985
|
+
cmd.command("call <path>").description(
|
|
8986
|
+
`workser api call /orders --method POST --body '{"item":1}' [--env local|preview|production]`
|
|
8987
|
+
).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(
|
|
8988
|
+
"--header <name:value>",
|
|
8989
|
+
"extra header; repeat for more than one",
|
|
8990
|
+
collectHeader,
|
|
8991
|
+
{}
|
|
8992
|
+
).option(
|
|
8993
|
+
"--env <name>",
|
|
8994
|
+
"local (default), preview or production \u2014 the host comes from here, never from the path"
|
|
8995
|
+
).action(
|
|
8996
|
+
action(async ({ ctx, args, opts }) => {
|
|
8997
|
+
const appId = requireApp(opts.app);
|
|
8998
|
+
const res = await api(
|
|
8999
|
+
ctx,
|
|
9000
|
+
`/v1/apps/${encodeURIComponent(appId)}/api/call`,
|
|
9001
|
+
{
|
|
9002
|
+
body: {
|
|
9003
|
+
environment: opts.env,
|
|
9004
|
+
method: opts.method,
|
|
9005
|
+
path: args[0],
|
|
9006
|
+
headers: opts.header,
|
|
9007
|
+
body: opts.body
|
|
9008
|
+
}
|
|
9009
|
+
}
|
|
9010
|
+
);
|
|
9011
|
+
ok(res, () => {
|
|
9012
|
+
if (!res?.ok) {
|
|
9013
|
+
line(import_picocolors32.default.red(res?.error ?? "The service did not answer."));
|
|
9014
|
+
return;
|
|
9015
|
+
}
|
|
9016
|
+
const code = `${res.status}${res.statusText ? ` ${res.statusText}` : ""}`;
|
|
9017
|
+
const colour2 = res.status && res.status < 300 ? import_picocolors32.default.green : res.status && res.status < 500 ? import_picocolors32.default.yellow : import_picocolors32.default.red;
|
|
9018
|
+
line(`${colour2(code)} ${import_picocolors32.default.dim(`${res.durationMs}ms ${res.url}`)}`);
|
|
9019
|
+
if (res.body) line(res.body);
|
|
9020
|
+
if (res.truncated) line(import_picocolors32.default.dim("(answer truncated)"));
|
|
9021
|
+
});
|
|
9022
|
+
if (!res?.ok) process.exitCode = 1;
|
|
9023
|
+
})
|
|
9024
|
+
);
|
|
9025
|
+
cmd.command("spec").description(
|
|
9026
|
+
"Compare the routes this repo serves with the ones it documents \u2014 `--check` fails on a gap"
|
|
9027
|
+
).option("--check", "exit non-zero when a route has no entry in the spec").action(
|
|
9028
|
+
action(async ({ ctx, opts }) => {
|
|
9029
|
+
requireLocalApp(ctx, "api spec");
|
|
9030
|
+
const files = listRepoFiles(ctx.cwd);
|
|
9031
|
+
const routes = discoverRoutes(files);
|
|
9032
|
+
const specFile = SPEC_FILES.find((f) => files.includes(f)) ?? null;
|
|
9033
|
+
const documented = specFile ? specPaths(readIfPresent(join3(ctx.cwd, specFile))) : [];
|
|
9034
|
+
const report = specReport(routes, documented);
|
|
9035
|
+
const summary = specSummary(report, specFile);
|
|
9036
|
+
ok({ ...report, specFile, summary }, () => {
|
|
9037
|
+
for (const r of report.routes) {
|
|
9038
|
+
const known = report.missing.some((m) => m.path === r.path);
|
|
9039
|
+
line(
|
|
9040
|
+
`${known ? import_picocolors32.default.yellow("undocumented") : import_picocolors32.default.green("documented ")} ${r.path}${import_picocolors32.default.dim(` ${r.file}`)}`
|
|
9041
|
+
);
|
|
9042
|
+
}
|
|
9043
|
+
for (const p of report.stale) {
|
|
9044
|
+
line(`${import_picocolors32.default.dim("in spec only ")} ${p}`);
|
|
9045
|
+
}
|
|
9046
|
+
line("");
|
|
9047
|
+
if (report.ok && specFile) success(summary);
|
|
9048
|
+
else line(import_picocolors32.default.yellow(summary));
|
|
9049
|
+
});
|
|
9050
|
+
if (opts.check && !report.ok) {
|
|
9051
|
+
throw new WorkserError(summary, { code: "bad_request" });
|
|
9052
|
+
}
|
|
9053
|
+
if (opts.check && !specFile) {
|
|
9054
|
+
throw new WorkserError(summary, { code: "bad_request" });
|
|
9055
|
+
}
|
|
9056
|
+
})
|
|
9057
|
+
);
|
|
9058
|
+
}
|
|
9059
|
+
function collectHeader(raw, previous) {
|
|
9060
|
+
const at = raw.indexOf(":");
|
|
9061
|
+
if (at <= 0) return previous;
|
|
9062
|
+
return { ...previous, [raw.slice(0, at).trim()]: raw.slice(at + 1).trim() };
|
|
9063
|
+
}
|
|
9064
|
+
function requireApp(app) {
|
|
9065
|
+
const value = typeof app === "string" ? app.trim() : "";
|
|
9066
|
+
if (value) return value;
|
|
9067
|
+
throw new WorkserError(
|
|
9068
|
+
"Which service? Pass --app <webAppId>; `workser app list` shows them.",
|
|
9069
|
+
{ code: "bad_request" }
|
|
9070
|
+
);
|
|
9071
|
+
}
|
|
9072
|
+
function readIfPresent(file) {
|
|
9073
|
+
try {
|
|
9074
|
+
return readFileSync3(file, "utf8");
|
|
9075
|
+
} catch {
|
|
9076
|
+
return null;
|
|
9077
|
+
}
|
|
9078
|
+
}
|
|
9079
|
+
function listRepoFiles(root, maxDepth = 8) {
|
|
9080
|
+
const SKIP = /* @__PURE__ */ new Set([
|
|
9081
|
+
"node_modules",
|
|
9082
|
+
".git",
|
|
9083
|
+
".next",
|
|
9084
|
+
"dist",
|
|
9085
|
+
"build",
|
|
9086
|
+
".vercel",
|
|
9087
|
+
"__pycache__",
|
|
9088
|
+
".venv",
|
|
9089
|
+
"venv"
|
|
9090
|
+
]);
|
|
9091
|
+
const out = [];
|
|
9092
|
+
const walk = (dir, depth) => {
|
|
9093
|
+
if (depth > maxDepth || out.length > 5e3) return;
|
|
9094
|
+
let entries;
|
|
9095
|
+
try {
|
|
9096
|
+
entries = readdirSync(dir);
|
|
9097
|
+
} catch {
|
|
9098
|
+
return;
|
|
9099
|
+
}
|
|
9100
|
+
for (const name of entries) {
|
|
9101
|
+
if (name.startsWith(".") && name !== ".well-known") continue;
|
|
9102
|
+
if (SKIP.has(name)) continue;
|
|
9103
|
+
const full = join3(dir, name);
|
|
9104
|
+
let isDir = false;
|
|
9105
|
+
try {
|
|
9106
|
+
isDir = statSync2(full).isDirectory();
|
|
9107
|
+
} catch {
|
|
9108
|
+
continue;
|
|
9109
|
+
}
|
|
9110
|
+
if (isDir) walk(full, depth + 1);
|
|
9111
|
+
else out.push(relative(root, full).split(sep).join("/"));
|
|
9112
|
+
}
|
|
9113
|
+
};
|
|
9114
|
+
walk(root, 0);
|
|
9115
|
+
return out;
|
|
9116
|
+
}
|
|
9117
|
+
|
|
9118
|
+
// src/commands/analysis.ts
|
|
9119
|
+
var import_picocolors33 = __toESM(require_picocolors(), 1);
|
|
9120
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
9121
|
+
function registerAnalysis(program3) {
|
|
9122
|
+
const cmd = program3.command("analysis").description("Run Python analysis locally, recorded in the task");
|
|
9123
|
+
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(
|
|
9124
|
+
action(async ({ ctx, opts }) => {
|
|
9125
|
+
const path = opts.app ? `/v1/apps/${encodeURIComponent(String(opts.app))}/analysis/runtime` : "/v1/analysis/runtime";
|
|
9126
|
+
const res = await api(ctx, path);
|
|
9127
|
+
ok(res, () => {
|
|
9128
|
+
line(
|
|
9129
|
+
`${res?.available ? import_picocolors33.default.green("python") : import_picocolors33.default.red("python")} ${res?.version ?? "not found"} ${import_picocolors33.default.dim(res?.python ?? "")}`
|
|
9130
|
+
);
|
|
9131
|
+
for (const lib of res?.libraries ?? []) {
|
|
9132
|
+
line(
|
|
9133
|
+
`${lib.present ? import_picocolors33.default.green(lib.name) : import_picocolors33.default.yellow(lib.name)}${import_picocolors33.default.dim(lib.present ? "" : " missing")}`
|
|
9134
|
+
);
|
|
9135
|
+
}
|
|
9136
|
+
for (const note of res?.notes ?? []) line(import_picocolors33.default.dim(note));
|
|
9137
|
+
});
|
|
9138
|
+
if (!res?.available) process.exitCode = 1;
|
|
9139
|
+
})
|
|
9140
|
+
);
|
|
9141
|
+
cmd.command("run").description(
|
|
9142
|
+
"workser analysis run --app <id> --file report.py (or --code '<python>')"
|
|
9143
|
+
).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(
|
|
9144
|
+
action(async ({ ctx, opts }) => {
|
|
9145
|
+
const code = readCode(opts.file, opts.code);
|
|
9146
|
+
const res = await api(
|
|
9147
|
+
ctx,
|
|
9148
|
+
`/v1/apps/${encodeURIComponent(String(opts.app))}/analysis/run`,
|
|
9149
|
+
{
|
|
9150
|
+
body: {
|
|
9151
|
+
code,
|
|
9152
|
+
timeoutMs: opts.timeout ? Number(opts.timeout) : void 0
|
|
9153
|
+
}
|
|
9154
|
+
}
|
|
9155
|
+
);
|
|
9156
|
+
ok(res, () => {
|
|
9157
|
+
if (res?.stdout) line(res.stdout.replace(/\n$/, ""));
|
|
9158
|
+
if (res?.stderr) line(import_picocolors33.default.dim(res.stderr.replace(/\n$/, "")));
|
|
9159
|
+
if (res?.truncated) line(import_picocolors33.default.dim("(output truncated)"));
|
|
9160
|
+
const took = `${Math.round((res?.durationMs ?? 0) / 100) / 10}s`;
|
|
9161
|
+
line(
|
|
9162
|
+
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}`)
|
|
9163
|
+
);
|
|
9164
|
+
if (res && !res.sandboxed) {
|
|
9165
|
+
line(
|
|
9166
|
+
import_picocolors33.default.dim(
|
|
9167
|
+
"This platform has no OS sandbox, so the script ran with your own file access."
|
|
9168
|
+
)
|
|
9169
|
+
);
|
|
9170
|
+
}
|
|
9171
|
+
});
|
|
9172
|
+
if (!res?.ok) process.exitCode = 1;
|
|
9173
|
+
})
|
|
9174
|
+
);
|
|
9175
|
+
}
|
|
9176
|
+
function readCode(file, inline) {
|
|
9177
|
+
if (typeof inline === "string" && inline.trim()) return inline;
|
|
9178
|
+
if (typeof file === "string" && file.trim()) {
|
|
9179
|
+
try {
|
|
9180
|
+
return readFileSync4(file, "utf8");
|
|
9181
|
+
} catch (err) {
|
|
9182
|
+
throw new WorkserError(
|
|
9183
|
+
`Could not read ${file}: ${err instanceof Error ? err.message : String(err)}`,
|
|
9184
|
+
{ code: "bad_request" }
|
|
9185
|
+
);
|
|
9186
|
+
}
|
|
9187
|
+
}
|
|
9188
|
+
throw new WorkserError("Pass --file <path> or --code '<python>'.", {
|
|
9189
|
+
code: "bad_request"
|
|
9190
|
+
});
|
|
9191
|
+
}
|
|
9192
|
+
|
|
9193
|
+
// src/commands/scan.ts
|
|
9194
|
+
var import_picocolors34 = __toESM(require_picocolors(), 1);
|
|
9195
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
9196
|
+
import { existsSync as existsSync3, readFileSync as readFileSync5, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
9197
|
+
import { join as join4, relative as relative2, sep as sep2 } from "path";
|
|
9198
|
+
|
|
9199
|
+
// src/scan.ts
|
|
9200
|
+
var SECRET_PATTERNS = [
|
|
9201
|
+
{ name: "a private key", re: /-----BEGIN[A-Z ]*PRIVATE KEY-----/, severity: "high" },
|
|
9202
|
+
{ name: "an AWS access key", re: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/, severity: "high" },
|
|
9203
|
+
{ name: "a GitHub token", re: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/, severity: "high" },
|
|
9204
|
+
{ name: "a Stripe live key", re: /\b[sr]k_live_[A-Za-z0-9]{16,}\b/, severity: "high" },
|
|
9205
|
+
{ name: "an OpenAI key", re: /\bsk-(?:proj-)?[A-Za-z0-9_-]{32,}\b/, severity: "high" },
|
|
9206
|
+
{ name: "an Anthropic key", re: /\bsk-ant-[A-Za-z0-9_-]{32,}\b/, severity: "high" },
|
|
9207
|
+
{ name: "a Google API key", re: /\bAIza[0-9A-Za-z_-]{35}\b/, severity: "high" },
|
|
9208
|
+
{ name: "a Slack token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/, severity: "high" },
|
|
9209
|
+
{
|
|
9210
|
+
name: "a database password in a connection string",
|
|
9211
|
+
re: /\b(?:postgres|postgresql|mysql|mongodb(?:\+srv)?|redis|amqp):\/\/[^\s:@/]+:[^\s:@/]+@/,
|
|
9212
|
+
severity: "high"
|
|
9213
|
+
},
|
|
9214
|
+
{
|
|
9215
|
+
// The everyday one: `API_KEY = "…"` with something real on the right.
|
|
9216
|
+
name: "a secret written into the code",
|
|
9217
|
+
re: /(?:secret|password|passwd|api[_-]?key|access[_-]?token|auth[_-]?token|private[_-]?key)\s*[:=]\s*["'`][^"'`\s]{12,}["'`]/i,
|
|
9218
|
+
severity: "high"
|
|
9219
|
+
}
|
|
9220
|
+
];
|
|
9221
|
+
var PLACEHOLDER = /(?:example|placeholder|changeme|your[_-]?|xxx+|\.\.\.|<[^>]+>|\$\{|process\.env|os\.environ|REPLACE|dummy|sample|test[_-]?key|fake)/i;
|
|
9222
|
+
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$)/;
|
|
9223
|
+
function addedLines(diff) {
|
|
9224
|
+
const out = [];
|
|
9225
|
+
let file = null;
|
|
9226
|
+
let lineNo = 0;
|
|
9227
|
+
for (const raw of diff.split("\n")) {
|
|
9228
|
+
if (raw.startsWith("+++ ")) {
|
|
9229
|
+
const path = raw.slice(4).trim();
|
|
9230
|
+
file = path === "/dev/null" ? null : path.replace(/^b\//, "");
|
|
9231
|
+
continue;
|
|
9232
|
+
}
|
|
9233
|
+
if (raw.startsWith("--- ") || raw.startsWith("diff --git")) continue;
|
|
9234
|
+
const hunk = raw.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
9235
|
+
if (hunk) {
|
|
9236
|
+
lineNo = Number(hunk[1]);
|
|
9237
|
+
continue;
|
|
9238
|
+
}
|
|
9239
|
+
if (!file) continue;
|
|
9240
|
+
if (raw.startsWith("+")) {
|
|
9241
|
+
out.push({ file, line: lineNo, text: raw.slice(1) });
|
|
9242
|
+
lineNo++;
|
|
9243
|
+
continue;
|
|
9244
|
+
}
|
|
9245
|
+
if (raw.startsWith(" ")) lineNo++;
|
|
9246
|
+
}
|
|
9247
|
+
return out;
|
|
9248
|
+
}
|
|
9249
|
+
function secretFindings(diff) {
|
|
9250
|
+
const findings = [];
|
|
9251
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9252
|
+
for (const added of addedLines(diff)) {
|
|
9253
|
+
if (SECRET_EXEMPT.test(added.file)) continue;
|
|
9254
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
9255
|
+
if (!pattern.re.test(added.text)) continue;
|
|
9256
|
+
if (PLACEHOLDER.test(added.text)) continue;
|
|
9257
|
+
const key = `${added.file}:${pattern.name}`;
|
|
9258
|
+
if (seen.has(key)) continue;
|
|
9259
|
+
seen.add(key);
|
|
9260
|
+
findings.push({
|
|
9261
|
+
check: "secrets",
|
|
9262
|
+
severity: pattern.severity,
|
|
9263
|
+
title: `This change adds ${pattern.name} to the code`,
|
|
9264
|
+
file: added.file,
|
|
9265
|
+
line: added.line,
|
|
9266
|
+
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."
|
|
9267
|
+
});
|
|
9268
|
+
break;
|
|
9269
|
+
}
|
|
9270
|
+
}
|
|
9271
|
+
return findings;
|
|
9272
|
+
}
|
|
9273
|
+
function depFindings(auditJson) {
|
|
9274
|
+
if (!auditJson) return [];
|
|
9275
|
+
let parsed;
|
|
9276
|
+
try {
|
|
9277
|
+
parsed = JSON.parse(auditJson);
|
|
9278
|
+
} catch {
|
|
9279
|
+
return [];
|
|
9280
|
+
}
|
|
9281
|
+
const vulns = parsed?.vulnerabilities;
|
|
9282
|
+
if (!vulns || typeof vulns !== "object") return [];
|
|
9283
|
+
const findings = [];
|
|
9284
|
+
for (const [name, raw] of Object.entries(vulns)) {
|
|
9285
|
+
const severity = String(raw?.severity ?? "").toLowerCase();
|
|
9286
|
+
if (severity !== "high" && severity !== "critical") continue;
|
|
9287
|
+
const direct = raw?.isDirect === true;
|
|
9288
|
+
const fixable = raw?.fixAvailable;
|
|
9289
|
+
findings.push({
|
|
9290
|
+
check: "deps",
|
|
9291
|
+
severity: "high",
|
|
9292
|
+
title: `${name} has a known ${severity} security problem`,
|
|
9293
|
+
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."
|
|
9294
|
+
});
|
|
9295
|
+
}
|
|
9296
|
+
return findings.sort((a, b) => a.title.localeCompare(b.title));
|
|
9297
|
+
}
|
|
9298
|
+
function permissionFindings(files, trackedFiles = []) {
|
|
9299
|
+
const findings = [];
|
|
9300
|
+
for (const file of files) {
|
|
9301
|
+
for (const match of file.content.matchAll(
|
|
9302
|
+
/NEXT_PUBLIC_[A-Z0-9_]*(?:SECRET|PASSWORD|TOKEN|API_KEY|PRIVATE)[A-Z0-9_]*/g
|
|
9303
|
+
)) {
|
|
9304
|
+
findings.push({
|
|
9305
|
+
check: "permissions",
|
|
9306
|
+
severity: "high",
|
|
9307
|
+
title: `${match[0]} is sent to every visitor's browser`,
|
|
9308
|
+
file: file.path,
|
|
9309
|
+
fix: "Anything named NEXT_PUBLIC_ is public by design. Rename it without that prefix and read it on the server only."
|
|
9310
|
+
});
|
|
9311
|
+
}
|
|
9312
|
+
if (/["']Access-Control-Allow-Origin["']\s*[:,]\s*["']\*["']/.test(file.content) && /Access-Control-Allow-Credentials/.test(file.content)) {
|
|
9313
|
+
findings.push({
|
|
9314
|
+
check: "permissions",
|
|
9315
|
+
severity: "high",
|
|
9316
|
+
title: "This service accepts credentialed requests from any website",
|
|
9317
|
+
file: file.path,
|
|
9318
|
+
fix: "Name the sites allowed to call it instead of `*`, or stop sending credentials."
|
|
9319
|
+
});
|
|
9320
|
+
}
|
|
9321
|
+
}
|
|
9322
|
+
for (const tracked of trackedFiles) {
|
|
9323
|
+
const base = tracked.split("/").pop() ?? tracked;
|
|
9324
|
+
if (!/^\.env(\.[a-z0-9-]+)?$/i.test(base)) continue;
|
|
9325
|
+
if (/\.(example|sample|template)$/i.test(base)) continue;
|
|
9326
|
+
findings.push({
|
|
9327
|
+
check: "permissions",
|
|
9328
|
+
severity: "high",
|
|
9329
|
+
title: `${tracked} is committed to the repository`,
|
|
9330
|
+
file: tracked,
|
|
9331
|
+
fix: "Add it to .gitignore, remove it from the repo, and treat every value in it as leaked."
|
|
9332
|
+
});
|
|
9333
|
+
}
|
|
9334
|
+
return findings;
|
|
9335
|
+
}
|
|
9336
|
+
function buildReport(input) {
|
|
9337
|
+
const counts = { high: 0, medium: 0, low: 0 };
|
|
9338
|
+
for (const f of input.findings) counts[f.severity]++;
|
|
9339
|
+
return {
|
|
9340
|
+
findings: [...input.findings].sort(
|
|
9341
|
+
(a, b) => rank(b.severity) - rank(a.severity) || a.title.localeCompare(b.title)
|
|
9342
|
+
),
|
|
9343
|
+
checked: input.checked,
|
|
9344
|
+
skipped: input.skipped,
|
|
9345
|
+
counts,
|
|
9346
|
+
ok: counts.high === 0
|
|
9347
|
+
};
|
|
9348
|
+
}
|
|
9349
|
+
function rank(s) {
|
|
9350
|
+
return s === "high" ? 3 : s === "medium" ? 2 : 1;
|
|
9351
|
+
}
|
|
9352
|
+
function scanSummary(report) {
|
|
9353
|
+
const ran = report.checked.length;
|
|
9354
|
+
if (!ran) return "Nothing could be checked \u2014 see the reasons above.";
|
|
9355
|
+
const what = report.checked.join(", ");
|
|
9356
|
+
const total = report.findings.length;
|
|
9357
|
+
if (!total) {
|
|
9358
|
+
return `Checked ${what} \u2014 nothing found.`;
|
|
9359
|
+
}
|
|
9360
|
+
const high = report.counts.high;
|
|
9361
|
+
return `Checked ${what} \u2014 ${total} ${total === 1 ? "thing" : "things"} to look at` + (high ? `, ${high} of them serious.` : ".");
|
|
9362
|
+
}
|
|
9363
|
+
|
|
9364
|
+
// src/commands/scan.ts
|
|
9365
|
+
function registerScan(program3) {
|
|
9366
|
+
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(
|
|
9367
|
+
"--only <checks>",
|
|
9368
|
+
"comma-separated subset: deps, secrets, permissions"
|
|
9369
|
+
).option(
|
|
9370
|
+
"--staged",
|
|
9371
|
+
"look at staged changes only, rather than everything not yet committed",
|
|
9372
|
+
false
|
|
9373
|
+
).action(
|
|
9374
|
+
action(async ({ ctx, opts }) => {
|
|
9375
|
+
const only = parseOnly(opts.only);
|
|
9376
|
+
const findings = [];
|
|
9377
|
+
const checked = [];
|
|
9378
|
+
const skipped = [];
|
|
9379
|
+
if (only.has("secrets")) runSecrets(ctx.cwd, !!opts.staged, findings, checked, skipped);
|
|
9380
|
+
if (only.has("deps")) runDeps(ctx.cwd, findings, checked, skipped);
|
|
9381
|
+
if (only.has("permissions")) runPermissions(ctx.cwd, findings, checked, skipped);
|
|
9382
|
+
const report = buildReport({ findings, checked, skipped });
|
|
9383
|
+
const summary = scanSummary(report);
|
|
9384
|
+
ok({ ...report, summary }, () => print2(report, summary));
|
|
9385
|
+
if (opts.check && !report.ok) {
|
|
9386
|
+
throw new WorkserError(summary, { code: "bad_request" });
|
|
9387
|
+
}
|
|
9388
|
+
})
|
|
9389
|
+
);
|
|
9390
|
+
}
|
|
9391
|
+
function parseOnly(raw) {
|
|
9392
|
+
const all = ["deps", "secrets", "permissions"];
|
|
9393
|
+
if (typeof raw !== "string" || !raw.trim()) return new Set(all);
|
|
9394
|
+
const wanted = raw.split(",").map((s) => s.trim().toLowerCase()).filter((s) => all.includes(s));
|
|
9395
|
+
if (!wanted.length) {
|
|
9396
|
+
throw new WorkserError(
|
|
9397
|
+
`--only takes any of: ${all.join(", ")}.`,
|
|
9398
|
+
{ code: "bad_request" }
|
|
9399
|
+
);
|
|
9400
|
+
}
|
|
9401
|
+
return new Set(wanted);
|
|
9402
|
+
}
|
|
9403
|
+
function runSecrets(cwd, staged, findings, checked, skipped) {
|
|
9404
|
+
const args = staged ? ["diff", "--cached", "--unified=0"] : (
|
|
9405
|
+
// Against HEAD, so it covers both staged and unstaged work: the agent
|
|
9406
|
+
// that just wrote the key has not staged anything.
|
|
9407
|
+
["diff", "HEAD", "--unified=0"]
|
|
9408
|
+
);
|
|
9409
|
+
const diff = git(cwd, args);
|
|
9410
|
+
if (diff === null) {
|
|
9411
|
+
skipped.push({
|
|
9412
|
+
check: "secrets",
|
|
9413
|
+
reason: "This folder isn\u2019t a git repository yet, so there are no changes to look through."
|
|
9414
|
+
});
|
|
9415
|
+
return;
|
|
9416
|
+
}
|
|
9417
|
+
checked.push("secrets");
|
|
9418
|
+
findings.push(...secretFindings(diff));
|
|
9419
|
+
}
|
|
9420
|
+
function runDeps(cwd, findings, checked, skipped) {
|
|
9421
|
+
const hasLock = ["package-lock.json", "npm-shrinkwrap.json"].some(
|
|
9422
|
+
(f) => existsSync3(join4(cwd, f))
|
|
9423
|
+
);
|
|
9424
|
+
if (!hasLock) {
|
|
9425
|
+
skipped.push({
|
|
9426
|
+
check: "deps",
|
|
9427
|
+
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."
|
|
9428
|
+
});
|
|
9429
|
+
return;
|
|
9430
|
+
}
|
|
9431
|
+
const res = spawnSync2("npm", ["audit", "--json", "--audit-level=high"], {
|
|
9432
|
+
cwd,
|
|
9433
|
+
encoding: "utf8",
|
|
9434
|
+
timeout: 6e4,
|
|
9435
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
9436
|
+
// `npm audit` exits 1 when it FINDS something, which is a successful run.
|
|
9437
|
+
// The failures that matter are the ones with no JSON on stdout.
|
|
9438
|
+
});
|
|
9439
|
+
const out = (res.stdout || "").trim();
|
|
9440
|
+
if (!out || res.error) {
|
|
9441
|
+
skipped.push({
|
|
9442
|
+
check: "deps",
|
|
9443
|
+
reason: "Couldn\u2019t reach the package registry, so known problems in your dependencies weren\u2019t checked. This needs an internet connection."
|
|
9444
|
+
});
|
|
9445
|
+
return;
|
|
9446
|
+
}
|
|
9447
|
+
checked.push("deps");
|
|
9448
|
+
findings.push(...depFindings(out));
|
|
9449
|
+
}
|
|
9450
|
+
function runPermissions(cwd, findings, checked, skipped) {
|
|
9451
|
+
const paths = listRepoFiles2(cwd).filter(isReadable);
|
|
9452
|
+
const files = [];
|
|
9453
|
+
for (const path of paths.slice(0, 1500)) {
|
|
9454
|
+
try {
|
|
9455
|
+
const content = readFileSync5(join4(cwd, path), "utf8");
|
|
9456
|
+
if (content.length > 4e5) continue;
|
|
9457
|
+
files.push({ path, content });
|
|
9458
|
+
} catch {
|
|
9459
|
+
}
|
|
9460
|
+
}
|
|
9461
|
+
const tracked = git(cwd, ["ls-files"]);
|
|
9462
|
+
checked.push("permissions");
|
|
9463
|
+
findings.push(
|
|
9464
|
+
...permissionFindings(
|
|
9465
|
+
files,
|
|
9466
|
+
tracked === null ? [] : tracked.split("\n").filter(Boolean)
|
|
9467
|
+
)
|
|
9468
|
+
);
|
|
9469
|
+
if (tracked === null) {
|
|
9470
|
+
skipped.push({
|
|
9471
|
+
check: "permissions",
|
|
9472
|
+
reason: "Not a git repository, so we couldn\u2019t check whether a .env file has been committed."
|
|
9473
|
+
});
|
|
9474
|
+
}
|
|
9475
|
+
}
|
|
9476
|
+
function print2(report, summary) {
|
|
9477
|
+
for (const s of report.skipped) {
|
|
9478
|
+
line(`${import_picocolors34.default.yellow("not checked")} ${s.check}${import_picocolors34.default.dim(` \u2014 ${s.reason}`)}`);
|
|
9479
|
+
}
|
|
9480
|
+
for (const f of report.findings) {
|
|
9481
|
+
const where = f.file ? import_picocolors34.default.dim(` ${f.file}${f.line ? `:${f.line}` : ""}`) : "";
|
|
9482
|
+
line(`${severityTag(f.severity)} ${f.title}${where}`);
|
|
9483
|
+
line(` ${import_picocolors34.default.dim(f.fix)}`);
|
|
9484
|
+
}
|
|
9485
|
+
if (report.findings.length || report.skipped.length) line("");
|
|
9486
|
+
if (report.ok && !report.skipped.length) success(summary);
|
|
9487
|
+
else if (report.ok) line(import_picocolors34.default.yellow(summary));
|
|
9488
|
+
else line(import_picocolors34.default.red(summary));
|
|
9489
|
+
}
|
|
9490
|
+
function severityTag(severity) {
|
|
9491
|
+
if (severity === "high") return import_picocolors34.default.red("serious ");
|
|
9492
|
+
if (severity === "medium") return import_picocolors34.default.yellow("worth fixing");
|
|
9493
|
+
return import_picocolors34.default.dim("minor ");
|
|
9494
|
+
}
|
|
9495
|
+
function git(cwd, args) {
|
|
9496
|
+
try {
|
|
9497
|
+
const res = spawnSync2("git", args, {
|
|
9498
|
+
cwd,
|
|
9499
|
+
encoding: "utf8",
|
|
9500
|
+
timeout: 2e4,
|
|
9501
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
9502
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
9503
|
+
});
|
|
9504
|
+
if (res.error || res.status !== 0) return null;
|
|
9505
|
+
return res.stdout || "";
|
|
9506
|
+
} catch {
|
|
9507
|
+
return null;
|
|
9508
|
+
}
|
|
9509
|
+
}
|
|
9510
|
+
var READABLE = /\.(?:[jt]sx?|mjs|cjs|json|ya?ml|toml|env|py|rb|go|rs|java|php|sh|sql|md|txt|html|css)$/i;
|
|
9511
|
+
function isReadable(path) {
|
|
9512
|
+
return READABLE.test(path);
|
|
9513
|
+
}
|
|
9514
|
+
function listRepoFiles2(root, maxDepth = 8) {
|
|
9515
|
+
const SKIP = /* @__PURE__ */ new Set([
|
|
9516
|
+
"node_modules",
|
|
9517
|
+
".git",
|
|
9518
|
+
".next",
|
|
9519
|
+
"dist",
|
|
9520
|
+
"build",
|
|
9521
|
+
".vercel",
|
|
9522
|
+
"__pycache__",
|
|
9523
|
+
".venv",
|
|
9524
|
+
"venv"
|
|
9525
|
+
]);
|
|
9526
|
+
const out = [];
|
|
9527
|
+
const walk = (dir, depth) => {
|
|
9528
|
+
if (depth > maxDepth || out.length > 5e3) return;
|
|
9529
|
+
let entries;
|
|
9530
|
+
try {
|
|
9531
|
+
entries = readdirSync2(dir);
|
|
9532
|
+
} catch {
|
|
9533
|
+
return;
|
|
9534
|
+
}
|
|
9535
|
+
for (const name of entries) {
|
|
9536
|
+
if (SKIP.has(name)) continue;
|
|
9537
|
+
if (name.startsWith(".") && !name.startsWith(".env")) continue;
|
|
9538
|
+
const full = join4(dir, name);
|
|
9539
|
+
let isDir = false;
|
|
9540
|
+
try {
|
|
9541
|
+
isDir = statSync3(full).isDirectory();
|
|
9542
|
+
} catch {
|
|
9543
|
+
continue;
|
|
9544
|
+
}
|
|
9545
|
+
if (isDir) walk(full, depth + 1);
|
|
9546
|
+
else out.push(relative2(root, full).split(sep2).join("/"));
|
|
9547
|
+
}
|
|
9548
|
+
};
|
|
9549
|
+
walk(root, 0);
|
|
9550
|
+
return out;
|
|
9551
|
+
}
|
|
9552
|
+
|
|
9553
|
+
// src/commands/health.ts
|
|
9554
|
+
var import_picocolors35 = __toESM(require_picocolors(), 1);
|
|
9555
|
+
function registerHealth(program3) {
|
|
9556
|
+
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(
|
|
9557
|
+
action(async ({ ctx, opts }) => {
|
|
9558
|
+
requireDaemon(ctx, "health", "checks your published sites from this computer");
|
|
9559
|
+
const path = opts.app ? `/v1/apps/${encodeURIComponent(String(opts.app))}/health` : `/v1/health`;
|
|
9560
|
+
const res = await api(ctx, path);
|
|
9561
|
+
ok(res, () => print3(res));
|
|
9562
|
+
if (res?.checks?.some((c) => !c.ok)) process.exitCode = 1;
|
|
9563
|
+
})
|
|
9564
|
+
);
|
|
9565
|
+
}
|
|
9566
|
+
function print3(res) {
|
|
9567
|
+
if (!res?.checks?.length) {
|
|
9568
|
+
line(import_picocolors35.default.dim(res?.note ?? "Nothing to check."));
|
|
9569
|
+
return;
|
|
9570
|
+
}
|
|
9571
|
+
for (const c of res.checks) {
|
|
9572
|
+
const mark = c.ok ? import_picocolors35.default.green("up ") : import_picocolors35.default.red("down");
|
|
9573
|
+
const timing = import_picocolors35.default.dim(`${c.ms}ms`);
|
|
9574
|
+
const detail = c.ok ? timing : import_picocolors35.default.dim(`${c.error ?? "no answer"}${c.failures > 1 ? ` \xB7 ${c.failures} in a row` : ""}`);
|
|
9575
|
+
line(` ${mark} ${c.appName} ${import_picocolors35.default.dim(`(${c.environment})`)} ${c.url} ${detail}`);
|
|
9576
|
+
if (c.incidentOpened) {
|
|
9577
|
+
line(import_picocolors35.default.yellow(` An incident has been opened on the board for this.`));
|
|
9578
|
+
}
|
|
9579
|
+
}
|
|
9580
|
+
const down = res.checks.filter((c) => !c.ok);
|
|
9581
|
+
line("");
|
|
9582
|
+
if (!down.length) {
|
|
9583
|
+
success(
|
|
9584
|
+
`Everything published is answering (${res.checks.length} ${res.checks.length === 1 ? "address" : "addresses"} checked).`
|
|
9585
|
+
);
|
|
9586
|
+
return;
|
|
9587
|
+
}
|
|
9588
|
+
const production = down.filter((c) => c.environment === "production").length;
|
|
9589
|
+
line(
|
|
9590
|
+
import_picocolors35.default.red(
|
|
9591
|
+
`${down.length} of ${res.checks.length} not answering` + (production ? ` \u2014 ${production} customer-facing.` : " (preview only).")
|
|
9592
|
+
)
|
|
9593
|
+
);
|
|
9594
|
+
}
|
|
9595
|
+
|
|
9596
|
+
// src/commands/urls.ts
|
|
9597
|
+
var import_picocolors36 = __toESM(require_picocolors(), 1);
|
|
9598
|
+
function registerUrls(program3) {
|
|
9599
|
+
program3.command("urls").description("The stable preview and production addresses of every app in this project").option("--app <webAppId>", "just one app").action(
|
|
9600
|
+
action(async ({ ctx, opts }) => {
|
|
9601
|
+
const projectId = requireProject(ctx);
|
|
9602
|
+
const apps = await api(ctx, `/v1/apps`, {
|
|
9603
|
+
query: { project: projectId }
|
|
9604
|
+
});
|
|
9605
|
+
const list = Array.isArray(apps) ? apps : apps?.apps ?? [];
|
|
9606
|
+
const wanted = opts.app ? list.filter((a) => a.id === String(opts.app)) : list;
|
|
9607
|
+
const rows = urlRows(wanted);
|
|
9608
|
+
const summary = urlsSummary(rows);
|
|
9609
|
+
ok({ rows, summary }, () => {
|
|
9610
|
+
for (const row of rows) {
|
|
9611
|
+
const label = import_picocolors36.default.dim(row.environment.padEnd(10));
|
|
9612
|
+
const value = row.url ? import_picocolors36.default.cyan(row.url) : import_picocolors36.default.dim(row.note ?? "not published");
|
|
9613
|
+
line(` ${row.appName.padEnd(22)} ${label} ${value}`);
|
|
9614
|
+
}
|
|
9615
|
+
line("");
|
|
9616
|
+
if (rows.some((r) => r.url)) success(summary);
|
|
9617
|
+
else line(import_picocolors36.default.yellow(summary));
|
|
9618
|
+
});
|
|
9619
|
+
})
|
|
9620
|
+
);
|
|
9621
|
+
}
|
|
9622
|
+
|
|
9623
|
+
// src/commands/deployments.ts
|
|
9624
|
+
var import_picocolors37 = __toESM(require_picocolors(), 1);
|
|
9625
|
+
function registerDeployments(program3) {
|
|
9626
|
+
const cmd = program3.command("deployments").description("Deployment history, and putting a build in front of customers");
|
|
9627
|
+
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(
|
|
9628
|
+
action(async ({ ctx, opts }) => {
|
|
9629
|
+
const projectId = requireProject(ctx);
|
|
9630
|
+
const environment = readEnv2(opts.env, "deployments list");
|
|
9631
|
+
const res = await api(ctx, `/v1/projects/${projectId}/deployments`, {
|
|
9632
|
+
query: {
|
|
9633
|
+
...opts.app ? { webAppId: String(opts.app) } : {},
|
|
9634
|
+
...environment ? { environment } : {},
|
|
9635
|
+
limit: String(opts.limit ?? "20")
|
|
9636
|
+
}
|
|
9637
|
+
});
|
|
9638
|
+
const items = res?.deployments ?? [];
|
|
9639
|
+
ok(res, () => {
|
|
9640
|
+
if (!items.length) {
|
|
9641
|
+
return line(
|
|
9642
|
+
import_picocolors37.default.dim(
|
|
9643
|
+
environment ? `Nothing has been deployed to ${environment} yet.` : "Nothing has been deployed yet. `workser deploy` builds the first one."
|
|
9644
|
+
)
|
|
9645
|
+
);
|
|
9646
|
+
}
|
|
9647
|
+
for (const d of items) line(formatDeployment(d));
|
|
9648
|
+
});
|
|
9649
|
+
})
|
|
9650
|
+
);
|
|
9651
|
+
cmd.command("inspect <id>").description("One deployment in full, with its build log").option("--logs", "include the build output", false).action(
|
|
9652
|
+
action(async ({ ctx, args, opts }) => {
|
|
9653
|
+
const projectId = requireProject(ctx);
|
|
9654
|
+
const id = String(args[0]);
|
|
9655
|
+
const dep = await api(ctx, `/v1/deployments/${encodeURIComponent(id)}`);
|
|
9656
|
+
const logs = opts.logs ? await api(
|
|
9657
|
+
ctx,
|
|
9658
|
+
`/v1/projects/${projectId}/deployments/${encodeURIComponent(id)}/logs`
|
|
9659
|
+
).catch(() => null) : null;
|
|
9660
|
+
ok({ ...dep, logs }, () => {
|
|
9661
|
+
line(formatDeployment(dep));
|
|
9662
|
+
if (dep?.error_message) line(import_picocolors37.default.red(` ${dep.error_message}`));
|
|
9663
|
+
const events = logs?.events ?? [];
|
|
9664
|
+
for (const e of events) {
|
|
9665
|
+
line(` ${import_picocolors37.default.dim(String(e.type ?? "log"))} ${e.text ?? ""}`);
|
|
9666
|
+
}
|
|
9667
|
+
if (opts.logs && !events.length) {
|
|
9668
|
+
line(import_picocolors37.default.dim(" That build produced no output."));
|
|
9669
|
+
}
|
|
9670
|
+
});
|
|
9671
|
+
})
|
|
9672
|
+
);
|
|
9673
|
+
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(
|
|
9674
|
+
action(async ({ ctx, opts }) => {
|
|
9675
|
+
const projectId = requireProject(ctx);
|
|
9676
|
+
const res = await api(ctx, `/v1/projects/${projectId}/deployments/promote`, {
|
|
9677
|
+
body: { ...opts.app ? { webAppId: String(opts.app) } : {} }
|
|
9678
|
+
});
|
|
9679
|
+
ok(res, () => printPromoted(res, null));
|
|
9680
|
+
})
|
|
9681
|
+
);
|
|
9682
|
+
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(
|
|
9683
|
+
action(async ({ ctx, args, opts }) => {
|
|
9684
|
+
const projectId = requireProject(ctx);
|
|
9685
|
+
const version = Number(args[0]);
|
|
9686
|
+
if (!Number.isInteger(version) || version < 1) {
|
|
9687
|
+
throw new WorkserError(
|
|
9688
|
+
`"${args[0]}" is not a version number. \`workser deployments list\` shows them.`,
|
|
9689
|
+
{ code: "bad_input" }
|
|
9690
|
+
);
|
|
9691
|
+
}
|
|
9692
|
+
const res = await api(ctx, `/v1/projects/${projectId}/deployments/promote`, {
|
|
9693
|
+
body: {
|
|
9694
|
+
version,
|
|
9695
|
+
...opts.app ? { webAppId: String(opts.app) } : {}
|
|
9696
|
+
}
|
|
9697
|
+
});
|
|
9698
|
+
ok(res, () => printPromoted(res, version));
|
|
9699
|
+
})
|
|
9700
|
+
);
|
|
9701
|
+
}
|
|
9702
|
+
function readEnv2(raw, verb) {
|
|
9703
|
+
const parsed = parseDeployEnvironment(raw, verb);
|
|
9704
|
+
if (!parsed.ok) throw new WorkserError(parsed.error, { code: "bad_input" });
|
|
9705
|
+
return parsed.value;
|
|
9706
|
+
}
|
|
9707
|
+
function printPromoted(res, version) {
|
|
9708
|
+
if (!res) return;
|
|
9709
|
+
const what = version === null ? "the latest build" : `version ${version}`;
|
|
9710
|
+
const url = res.url ?? res.vercel_url;
|
|
9711
|
+
success(`Production is being rebuilt from ${what}.`);
|
|
9712
|
+
if (url) line(import_picocolors37.default.dim(`It will be at ${url}`));
|
|
9713
|
+
line(import_picocolors37.default.dim("`workser deploy status` follows it."));
|
|
9714
|
+
}
|
|
9715
|
+
function formatDeployment(d) {
|
|
9716
|
+
if (!d) return "";
|
|
9717
|
+
const version = d.version !== void 0 ? import_picocolors37.default.yellow(`v${d.version}`) : import_picocolors37.default.dim("v?");
|
|
9718
|
+
const env = import_picocolors37.default.dim((d.environment ?? "?").padEnd(10));
|
|
9719
|
+
const app = d.webAppName ? `${d.webAppName} ` : "";
|
|
9720
|
+
const when = import_picocolors37.default.dim(formatTime2(d.created_at));
|
|
9721
|
+
const url = d.url ? " " + import_picocolors37.default.cyan(d.url) : "";
|
|
9722
|
+
return `${version} ${env} ${colorStatus(d.status ?? "")} ${app}${when}${url}`;
|
|
9723
|
+
}
|
|
9724
|
+
function formatTime2(t) {
|
|
9725
|
+
if (!t) return "\u2014";
|
|
9726
|
+
const d = new Date(t);
|
|
9727
|
+
return Number.isNaN(d.getTime()) ? String(t) : d.toISOString().replace("T", " ").replace(/\.\d+Z$/, "Z");
|
|
9728
|
+
}
|
|
9729
|
+
|
|
9730
|
+
// src/commands/usage.ts
|
|
9731
|
+
var import_picocolors38 = __toESM(require_picocolors(), 1);
|
|
9732
|
+
|
|
9733
|
+
// src/usage.ts
|
|
9734
|
+
var NEAR_LIMIT_FRACTION = 0.8;
|
|
9735
|
+
function isUnlimited(limit) {
|
|
9736
|
+
return limit === null || limit === void 0 || !Number.isFinite(limit) || limit >= Number.MAX_SAFE_INTEGER;
|
|
9737
|
+
}
|
|
9738
|
+
function fractionUsed(d) {
|
|
9739
|
+
if (d.used === null) return null;
|
|
9740
|
+
if (isUnlimited(d.limit)) return null;
|
|
9741
|
+
const limit = d.limit;
|
|
9742
|
+
if (limit <= 0) return d.used > 0 ? 1 : 0;
|
|
9743
|
+
return d.used / limit;
|
|
9744
|
+
}
|
|
9745
|
+
function usageState(d) {
|
|
9746
|
+
if (d.used === null) return "unknown";
|
|
9747
|
+
const fraction = fractionUsed(d);
|
|
9748
|
+
if (fraction === null) return "fine";
|
|
9749
|
+
if (fraction >= 1) return "over";
|
|
9750
|
+
if (fraction >= NEAR_LIMIT_FRACTION) return "near";
|
|
9751
|
+
return "fine";
|
|
9752
|
+
}
|
|
9753
|
+
function formatAmount(value, unit) {
|
|
9754
|
+
if (unit === "count") return String(Math.round(value));
|
|
9755
|
+
if (Number.isInteger(value)) return `${value} GB`;
|
|
9756
|
+
if (value < 0.01) return "<0.01 GB";
|
|
9757
|
+
if (value < 10) return `${value.toFixed(2)} GB`;
|
|
9758
|
+
return `${value.toFixed(1)} GB`;
|
|
9759
|
+
}
|
|
9760
|
+
function bar(d, width = 20) {
|
|
9761
|
+
const fraction = fractionUsed(d);
|
|
9762
|
+
if (fraction === null) return "";
|
|
9763
|
+
const filled = Math.min(width, Math.round(fraction * width));
|
|
9764
|
+
return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
|
|
9765
|
+
}
|
|
9766
|
+
function dimensionLine(d) {
|
|
9767
|
+
if (d.used === null) {
|
|
9768
|
+
return `${d.label}: not measured${d.note ? ` \u2014 ${d.note}` : ""}`;
|
|
9769
|
+
}
|
|
9770
|
+
const used = formatAmount(d.used, d.unit);
|
|
9771
|
+
if (isUnlimited(d.limit)) return `${d.label}: ${used} (no limit on this plan)`;
|
|
9772
|
+
const limit = formatAmount(d.limit, d.unit);
|
|
9773
|
+
if (usageState(d) === "over") {
|
|
9774
|
+
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`;
|
|
9775
|
+
}
|
|
9776
|
+
return `${d.label}: ${used} of ${limit}`;
|
|
9777
|
+
}
|
|
9778
|
+
function usageSummary(report) {
|
|
9779
|
+
const dims = report.dimensions ?? [];
|
|
9780
|
+
const unknown = dims.filter((d) => d.used === null);
|
|
9781
|
+
const over = dims.filter((d) => usageState(d) === "over");
|
|
9782
|
+
const near = dims.filter((d) => usageState(d) === "near");
|
|
9783
|
+
const parts = [];
|
|
9784
|
+
if (over.length) {
|
|
9785
|
+
parts.push(
|
|
9786
|
+
`${over.length === 1 ? "One thing is" : `${over.length} things are`} over the ${report.tier} plan's limit`
|
|
9787
|
+
);
|
|
9788
|
+
} else if (near.length) {
|
|
9789
|
+
parts.push(
|
|
9790
|
+
`${near.length === 1 ? "One thing is" : `${near.length} things are`} close to the ${report.tier} plan's limit`
|
|
9791
|
+
);
|
|
9792
|
+
} else if (dims.some((d) => d.used !== null)) {
|
|
9793
|
+
parts.push(`Everything is comfortably inside the ${report.tier} plan`);
|
|
9794
|
+
}
|
|
9795
|
+
if (unknown.length) {
|
|
9796
|
+
parts.push(
|
|
9797
|
+
`${unknown.length} ${unknown.length === 1 ? "figure" : "figures"} could not be read`
|
|
9798
|
+
);
|
|
9799
|
+
}
|
|
9800
|
+
if (!parts.length) return "Nothing could be measured.";
|
|
9801
|
+
return `${parts.join("; ")}.`;
|
|
9802
|
+
}
|
|
9803
|
+
function shouldFail(report) {
|
|
9804
|
+
return (report.dimensions ?? []).some(
|
|
9805
|
+
(d) => d.kind === "hard" && usageState(d) === "over"
|
|
9806
|
+
);
|
|
9807
|
+
}
|
|
9808
|
+
|
|
9809
|
+
// src/commands/usage.ts
|
|
9810
|
+
function registerUsage(program3) {
|
|
9811
|
+
program3.command("usage").description("What this project and your plan are using \u2014 storage, projects, apps").action(
|
|
9812
|
+
action(async ({ ctx }) => {
|
|
9813
|
+
const projectId = requireProject(ctx);
|
|
9814
|
+
const report = await api(
|
|
9815
|
+
ctx,
|
|
9816
|
+
`/v1/projects/${projectId}/usage`
|
|
9817
|
+
);
|
|
9818
|
+
ok(report, () => print4(report));
|
|
9819
|
+
if (shouldFail(report)) process.exitCode = 1;
|
|
9820
|
+
})
|
|
9821
|
+
);
|
|
9822
|
+
}
|
|
9823
|
+
function print4(report) {
|
|
9824
|
+
const dims = report.dimensions ?? [];
|
|
9825
|
+
if (!dims.length) {
|
|
9826
|
+
return line(import_picocolors38.default.dim("Nothing to measure for this project yet."));
|
|
9827
|
+
}
|
|
9828
|
+
const width = Math.max(...dims.map((d) => d.label.length));
|
|
9829
|
+
for (const d of dims) {
|
|
9830
|
+
line(` ${colour(d)(dimensionLine(d).padEnd(0))}${gauge(d, width)}`);
|
|
9831
|
+
}
|
|
9832
|
+
line("");
|
|
9833
|
+
const summary = usageSummary(report);
|
|
9834
|
+
const worst = dims.map(usageState);
|
|
9835
|
+
if (worst.includes("over")) line(import_picocolors38.default.red(summary));
|
|
9836
|
+
else if (worst.includes("near") || worst.includes("unknown"))
|
|
9837
|
+
line(import_picocolors38.default.yellow(summary));
|
|
9838
|
+
else success(summary);
|
|
9839
|
+
}
|
|
9840
|
+
function gauge(d, _labelWidth) {
|
|
9841
|
+
const drawn = bar(d);
|
|
9842
|
+
return drawn ? ` ${import_picocolors38.default.dim(drawn)}` : "";
|
|
9843
|
+
}
|
|
9844
|
+
function colour(d) {
|
|
9845
|
+
switch (usageState(d)) {
|
|
9846
|
+
case "over":
|
|
9847
|
+
return d.kind === "hard" ? import_picocolors38.default.red : import_picocolors38.default.yellow;
|
|
9848
|
+
case "near":
|
|
9849
|
+
return import_picocolors38.default.yellow;
|
|
9850
|
+
case "unknown":
|
|
9851
|
+
return import_picocolors38.default.dim;
|
|
9852
|
+
default:
|
|
9853
|
+
return (s) => s;
|
|
9854
|
+
}
|
|
9855
|
+
}
|
|
9856
|
+
|
|
7668
9857
|
// src/index.ts
|
|
7669
9858
|
var pkg = {
|
|
7670
|
-
version: true ? "0.
|
|
9859
|
+
version: true ? "0.6.0" : "0.0.0-dev"
|
|
7671
9860
|
};
|
|
7672
9861
|
var program2 = new Command();
|
|
7673
9862
|
program2.name("workser").description(
|
|
@@ -7700,6 +9889,13 @@ registerOpen(program2);
|
|
|
7700
9889
|
registerDoctor(program2);
|
|
7701
9890
|
registerAgent(program2);
|
|
7702
9891
|
registerVerify(program2);
|
|
9892
|
+
registerApi(program2);
|
|
9893
|
+
registerAnalysis(program2);
|
|
9894
|
+
registerScan(program2);
|
|
9895
|
+
registerHealth(program2);
|
|
9896
|
+
registerUrls(program2);
|
|
9897
|
+
registerDeployments(program2);
|
|
9898
|
+
registerUsage(program2);
|
|
7703
9899
|
registerCheckpoint(program2);
|
|
7704
9900
|
registerSync(program2);
|
|
7705
9901
|
registerWorkflow(program2);
|
|
@@ -7716,4 +9912,9 @@ registerTask(program2);
|
|
|
7716
9912
|
registerDecision(program2);
|
|
7717
9913
|
registerDoc(program2);
|
|
7718
9914
|
registerDesign(program2);
|
|
9915
|
+
try {
|
|
9916
|
+
assertRoleMayRun(process.argv.slice(2));
|
|
9917
|
+
} catch (e) {
|
|
9918
|
+
fail(e);
|
|
9919
|
+
}
|
|
7719
9920
|
program2.parseAsync(process.argv).catch((e) => fail(e));
|