@workser/cli 0.1.0 → 0.2.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/AGENTS.md +35 -0
- package/dist/index.js +1139 -135
- package/package.json +4 -3
- package/skills/workser/SKILL.md +57 -164
- package/skills/workser/reference/automation.md +55 -0
- package/skills/workser/reference/business-data.md +51 -0
- package/skills/workser/reference/computer-use.md +35 -0
- package/skills/workser/reference/database.md +41 -0
- package/skills/workser/reference/deliverables.md +65 -0
- package/skills/workser/reference/deploy.md +56 -0
- package/skills/workser/reference/images.md +35 -0
- package/skills/workser/reference/memory.md +34 -0
- package/skills/workser/reference/neon-backend.md +42 -0
- package/skills/workser/reference/roles.md +33 -0
- package/skills/workser/reference/storage.md +37 -0
package/dist/index.js
CHANGED
|
@@ -3650,16 +3650,580 @@ function exitCodeFor(e) {
|
|
|
3650
3650
|
return 1;
|
|
3651
3651
|
}
|
|
3652
3652
|
|
|
3653
|
-
// src/commands/
|
|
3653
|
+
// src/commands/help.ts
|
|
3654
3654
|
var import_picocolors2 = __toESM(require_picocolors(), 1);
|
|
3655
3655
|
|
|
3656
|
+
// src/help-content.ts
|
|
3657
|
+
var HELP_TOPICS = [
|
|
3658
|
+
{
|
|
3659
|
+
topic: "automation",
|
|
3660
|
+
title: "Workflows & connected apps",
|
|
3661
|
+
summary: "Build automations that outlive the run; use Gmail, Slack, Stripe, Sheets.",
|
|
3662
|
+
commands: ["workflow", "app"],
|
|
3663
|
+
source: "skills/workser/reference/automation.md",
|
|
3664
|
+
body: `# Workflows & connected apps
|
|
3665
|
+
|
|
3666
|
+
Wire up **automations** that keep running after you're done, and use third-party
|
|
3667
|
+
accounts (Gmail, Slack, Stripe, Google Sheets) the project has connected.
|
|
3668
|
+
|
|
3669
|
+
\`\`\`
|
|
3670
|
+
workser workflow list | create <name> [--body <json>] | get <id>
|
|
3671
|
+
workser workflow activate <id> | deactivate <id> | run <id> [--wait] [--body <json>]
|
|
3672
|
+
workser workflow runs <id> # past executions of a workflow
|
|
3673
|
+
workser workflow nodes [query] # search the node-type catalog
|
|
3674
|
+
|
|
3675
|
+
workser app list [--toolkit <slug>] # connectable + connected third-party apps
|
|
3676
|
+
workser app connect <toolkit> | disconnect <connectionId>
|
|
3677
|
+
workser app tools <toolkit> # a connected app's callable actions
|
|
3678
|
+
workser app run <toolSlug> [--body <json>] # execute one action
|
|
3679
|
+
\`\`\`
|
|
3680
|
+
|
|
3681
|
+
## Building a workflow
|
|
3682
|
+
|
|
3683
|
+
\`workser workflow create\` builds an event-driven, multi-step automation \u2014 the same
|
|
3684
|
+
engine Workser's own web Workflow tab uses. Nodes, connections and triggers go in
|
|
3685
|
+
\`--body\` as JSON.
|
|
3686
|
+
|
|
3687
|
+
**Browse \`workser workflow nodes\` first.** Inventing a node type that doesn't exist
|
|
3688
|
+
produces a workflow that saves and then never runs.
|
|
3689
|
+
|
|
3690
|
+
Created workflows start inactive: \`workser workflow activate <id>\` when it's ready.
|
|
3691
|
+
|
|
3692
|
+
## Using a connected app
|
|
3693
|
+
|
|
3694
|
+
1. \`workser app list\` \u2014 check what's already connected before asking for anything.
|
|
3695
|
+
2. If it isn't: \`workser app connect <toolkit>\` returns an OAuth link. The **user**
|
|
3696
|
+
must open it; you cannot complete OAuth on their behalf. Wait, then continue.
|
|
3697
|
+
3. \`workser app tools <toolkit>\` \u2014 read the argument schema rather than guessing
|
|
3698
|
+
field names.
|
|
3699
|
+
4. \`workser app run <toolSlug> --body '{"\u2026":\u2026}'\` \u2014 e.g. \`GOOGLESHEETS_APPEND_ROW\`,
|
|
3700
|
+
\`GMAIL_SEND_EMAIL\`.
|
|
3701
|
+
|
|
3702
|
+
**A \`run\` is a real side effect in someone's real account.** Sending an email or
|
|
3703
|
+
charging a card is not a dry run \u2014 say what you're about to do before you do it.
|
|
3704
|
+
|
|
3705
|
+
## The half people forget
|
|
3706
|
+
|
|
3707
|
+
A workflow-backed feature is two-way. Triggering it is the outbound half; when the
|
|
3708
|
+
workflow produces a result the app needs, its final node has to POST back to a
|
|
3709
|
+
webhook route in the app. Build only the trigger and the workflow runs perfectly
|
|
3710
|
+
while nothing ever appears in the product. The app-side receiver is covered in the
|
|
3711
|
+
\`workser-sdk\` skill under workflows.
|
|
3712
|
+
`
|
|
3713
|
+
},
|
|
3714
|
+
{
|
|
3715
|
+
topic: "business",
|
|
3716
|
+
title: "Business hub data",
|
|
3717
|
+
summary: "Products, orders, customers, sales, content, marketing, support, analytics.",
|
|
3718
|
+
commands: ["business"],
|
|
3719
|
+
source: "skills/workser/reference/business-data.md",
|
|
3720
|
+
body: `# Business hub data
|
|
3721
|
+
|
|
3722
|
+
The project's own commerce and CRM records \u2014 the **same rows** the Orbit desktop
|
|
3723
|
+
Business tab and workser-web's Business hub show. One generic CRUD surface across
|
|
3724
|
+
every resource rather than a command per domain.
|
|
3725
|
+
|
|
3726
|
+
\`\`\`
|
|
3727
|
+
workser business resources # the known resource names
|
|
3728
|
+
workser business list <resource> [subpath] # list/read (--query '<json>')
|
|
3729
|
+
workser business get <resource> <id>
|
|
3730
|
+
workser business create <resource> --body '<json>'
|
|
3731
|
+
workser business update <resource> <id> --body '<json>'
|
|
3732
|
+
workser business delete <resource> <id>
|
|
3733
|
+
workser business action <resource> <id> <verb> # POST .../<id>/<verb>
|
|
3734
|
+
\`\`\`
|
|
3735
|
+
|
|
3736
|
+
## Resources
|
|
3737
|
+
|
|
3738
|
+
\`business-config\`, \`business-settings\`, \`products\`, \`collections\`, \`navigations\`,
|
|
3739
|
+
\`orders\`, \`customers\`, \`sales-pipelines\`, \`sales-deals\`, \`pages\`, \`blog-posts\`,
|
|
3740
|
+
\`media\`, \`campaigns\`, \`email-templates\`, \`discounts\`, \`seo-configs\`,
|
|
3741
|
+
\`social-accounts\`, \`support-conversations\`, \`automation-rules\`, \`analytics\`.
|
|
3742
|
+
|
|
3743
|
+
Run \`workser business resources --json\` rather than trusting this list \u2014 the CLI's
|
|
3744
|
+
copy is the current one.
|
|
3745
|
+
|
|
3746
|
+
## Notes that matter
|
|
3747
|
+
|
|
3748
|
+
- **Sales and Support nest.** \`sales-deals\` maps to \`/sales/deals\`,
|
|
3749
|
+
\`support-conversations\` to \`/support/conversations\`. Use the dashed names above;
|
|
3750
|
+
the flat \`resource/id\` shape then works for \`get\`/\`update\`/\`delete\`/\`action\`.
|
|
3751
|
+
- **\`list\` takes a raw subpath** for anything the map doesn't cover:
|
|
3752
|
+
\`workser business list sales-pipelines <id>/stages\`.
|
|
3753
|
+
- **\`action\` is for named verbs** \u2014 \`workser business action orders <id> cancel\`,
|
|
3754
|
+
\`workser business action sales-deals <dealId> win\`. Check the resource's routes
|
|
3755
|
+
before inventing a verb.
|
|
3756
|
+
- **These are real business records.** Cancelling an order or deleting a customer is
|
|
3757
|
+
not a dry run. Say what you're about to do first.
|
|
3758
|
+
|
|
3759
|
+
## This is for you, not for the app
|
|
3760
|
+
|
|
3761
|
+
\`workser business\` is how **you** inspect and fix data while building. The app reads
|
|
3762
|
+
the same records at runtime through \`workser.business\` in \`@workser/app\` \u2014 see the
|
|
3763
|
+
\`workser-sdk\` skill. An app shelling out to this CLI per request is wrong.
|
|
3764
|
+
`
|
|
3765
|
+
},
|
|
3766
|
+
{
|
|
3767
|
+
topic: "computer-use",
|
|
3768
|
+
title: "Computer-use tools",
|
|
3769
|
+
summary: "Files, shell, screen, input, clipboard and browser on this machine.",
|
|
3770
|
+
commands: ["tool"],
|
|
3771
|
+
source: "skills/workser/reference/computer-use.md",
|
|
3772
|
+
body: `# Computer-use tools \u2014 your hands on this machine
|
|
3773
|
+
|
|
3774
|
+
\`\`\`
|
|
3775
|
+
workser tool list # what's available to you right now
|
|
3776
|
+
workser tool run <name> [--body <json>] # run one
|
|
3777
|
+
\`\`\`
|
|
3778
|
+
|
|
3779
|
+
\`workser tool list\` shows what's available \u2014 filesystem (read/write/list/delete/move),
|
|
3780
|
+
shell (run a command / Python / Node), screenshots and screen info, mouse and keyboard
|
|
3781
|
+
input, clipboard, notifications, and basic browser control (open a URL, read the page,
|
|
3782
|
+
click, fill, type, screenshot).
|
|
3783
|
+
|
|
3784
|
+
This is the **same engine** Workser's cloud Computer Use agent uses when it controls a
|
|
3785
|
+
user's machine remotely \u2014 you're getting it locally, gated by the same safety policy.
|
|
3786
|
+
|
|
3787
|
+
## Notes that matter
|
|
3788
|
+
|
|
3789
|
+
- **Check \`tool list\` rather than assuming a capability exists.** This is a curated
|
|
3790
|
+
subset, not full desktop automation.
|
|
3791
|
+
- **The safety policy applies.** Blocked paths (\`~/.ssh\` and friends), blocked
|
|
3792
|
+
destructive commands, rate limits. Refusals are the policy working, not a bug to
|
|
3793
|
+
route around.
|
|
3794
|
+
- **Sensitive actions are approval-gated.** Writing or deleting files, running a shell
|
|
3795
|
+
command, clicking or typing may return \`awaiting_approval\` (exit 5) \u2014 tell the user
|
|
3796
|
+
to approve in Orbit, then retry.
|
|
3797
|
+
- **You already have your own tools.** For editing files in this repo, use them. Reach
|
|
3798
|
+
for \`workser tool\` when you need something *outside* the project \u2014 the screen, the
|
|
3799
|
+
clipboard, a browser, another app on the machine.
|
|
3800
|
+
`
|
|
3801
|
+
},
|
|
3802
|
+
{
|
|
3803
|
+
topic: "database",
|
|
3804
|
+
title: "Database & end users",
|
|
3805
|
+
summary: "Provision Postgres, browse tables, run SQL, provision auth.",
|
|
3806
|
+
commands: ["db", "auth"],
|
|
3807
|
+
source: "skills/workser/reference/database.md",
|
|
3808
|
+
body: `# Database & end users
|
|
3809
|
+
|
|
3810
|
+
The project's Postgres (Neon behind Workser) and its end-user auth. Provisioning is
|
|
3811
|
+
idempotent \u2014 running \`create\` twice is safe.
|
|
3812
|
+
|
|
3813
|
+
\`\`\`
|
|
3814
|
+
workser db create # provision the Neon Postgres database (idempotent)
|
|
3815
|
+
workser db url # connection string (sensitive; least-privilege role)
|
|
3816
|
+
workser db list # database status
|
|
3817
|
+
workser db tables # list tables in the database
|
|
3818
|
+
workser db schema <table> # a table's columns
|
|
3819
|
+
workser db data <table> [-n N] [--offset N] # read rows
|
|
3820
|
+
workser db query "<sql>" # run SQL (writes are approval-gated)
|
|
3821
|
+
|
|
3822
|
+
workser auth enable # provision auth for the project (idempotent)
|
|
3823
|
+
workser auth status # is auth enabled? + Neon auth mode
|
|
3824
|
+
\`\`\`
|
|
3825
|
+
|
|
3826
|
+
## Notes that matter
|
|
3827
|
+
|
|
3828
|
+
- **\`db url\` is a credential.** Don't print it into the conversation, don't paste it
|
|
3829
|
+
into a file the user will commit. The app gets it from its environment already.
|
|
3830
|
+
- **Writes are approval-gated.** A \`db query\` that mutates may return
|
|
3831
|
+
\`awaiting_approval\` (exit 5). Ask the user to approve in Orbit, then retry.
|
|
3832
|
+
- **\`DROP\` / \`TRUNCATE\` are refused** by the safety policy. Change schema with a
|
|
3833
|
+
migration in the app's own migration folder, not with a destructive one-off.
|
|
3834
|
+
- **The database is the project's, not the app's.** Sibling apps in the same project
|
|
3835
|
+
share it. Don't assume a table is yours because you created it.
|
|
3836
|
+
|
|
3837
|
+
## Reading rows vs. reading data at runtime
|
|
3838
|
+
|
|
3839
|
+
\`db data\` / \`db query\` are for **you**, inspecting while you build. The app itself
|
|
3840
|
+
should read through \`@workser/app\` (\`workser.db\`, \`workser.business\`) \u2014 see the
|
|
3841
|
+
\`workser-sdk\` skill. An app that shells out to the CLI at request time is wrong.
|
|
3842
|
+
`
|
|
3843
|
+
},
|
|
3844
|
+
{
|
|
3845
|
+
topic: "deliverables",
|
|
3846
|
+
title: "Deliverables & asking the user",
|
|
3847
|
+
summary: "Record finished output on the task, and ask a blocking question.",
|
|
3848
|
+
commands: ["artifact", "ask"],
|
|
3849
|
+
source: "skills/workser/reference/deliverables.md",
|
|
3850
|
+
body: `# Deliverables & asking the user
|
|
3851
|
+
|
|
3852
|
+
Two things that reach the user directly: what you produced, and what you need from
|
|
3853
|
+
them.
|
|
3854
|
+
|
|
3855
|
+
\`\`\`
|
|
3856
|
+
workser artifact add <path> [--kind <k>] [-d <text>] # record a finished deliverable
|
|
3857
|
+
workser artifact add --url <url> --kind app # record a deployed app
|
|
3858
|
+
workser artifact run # which task you're attached to
|
|
3859
|
+
|
|
3860
|
+
workser ask "<question>" [--type <t>] [--option <o>] # ask the user, WAIT for the answer
|
|
3861
|
+
\`\`\`
|
|
3862
|
+
|
|
3863
|
+
## Record what you produced
|
|
3864
|
+
|
|
3865
|
+
Workser shows the user a **Deliverables** list on the task. If you don't say what you
|
|
3866
|
+
made, it has to guess \u2014 it watches your file edits and treats any path it sees as a
|
|
3867
|
+
deliverable, so scratch files and half-finished drafts show up next to the real
|
|
3868
|
+
output, and things that aren't files at all (a folder of results, a deployed app)
|
|
3869
|
+
can't show up correctly.
|
|
3870
|
+
|
|
3871
|
+
\`\`\`
|
|
3872
|
+
workser artifact add ./report.pdf -d "Q3 sales summary"
|
|
3873
|
+
workser artifact add ./exports --kind folder -d "generated CSVs"
|
|
3874
|
+
workser artifact add --url https://acme.workser.app --kind app -t "Storefront"
|
|
3875
|
+
\`\`\`
|
|
3876
|
+
|
|
3877
|
+
Only register **finished** output the user should get \u2014 not temp files, not
|
|
3878
|
+
intermediate steps. \`--kind\` is inferred from the path when you omit it (directories
|
|
3879
|
+
are detected automatically); pass it explicitly for \`app\` / \`url\`.
|
|
3880
|
+
|
|
3881
|
+
To publish an app: \`workser deploy\` (preview) or \`workser deploy --prod\` (live), then
|
|
3882
|
+
register the URL it returns as an \`app\` artifact so the user can open it from the task.
|
|
3883
|
+
|
|
3884
|
+
## Ask the user something (and get an answer back)
|
|
3885
|
+
|
|
3886
|
+
When you're blocked \u2014 a missing value, an ambiguous requirement, permission for
|
|
3887
|
+
something consequential \u2014 don't guess, and don't just write the question into your
|
|
3888
|
+
final message where nobody will answer it.
|
|
3889
|
+
|
|
3890
|
+
\`\`\`
|
|
3891
|
+
workser ask "Which email should order confirmations come from?"
|
|
3892
|
+
workser ask "Which plan should I wire up?" --option Free --option Pro --option Team
|
|
3893
|
+
workser ask "Delete the 1,240 archived rows?" --type approval
|
|
3894
|
+
\`\`\`
|
|
3895
|
+
|
|
3896
|
+
This shows the user a real card in the conversation and **blocks until they answer**,
|
|
3897
|
+
then prints their answer \u2014 so you ask, read the reply, and keep working in the same
|
|
3898
|
+
turn.
|
|
3899
|
+
|
|
3900
|
+
Types: \`input\` (default, free text), \`choice\` (with \`--option\`), \`approval\`
|
|
3901
|
+
(permission), \`confirmation\` (check an assumption), \`information\` (FYI, no answer
|
|
3902
|
+
needed). It times out (default 10 min) rather than hanging forever; if it does, carry
|
|
3903
|
+
on and state clearly what you assumed.
|
|
3904
|
+
|
|
3905
|
+
**Never ask for a secret value this way** \u2014 the answer is stored and displayed. Ask
|
|
3906
|
+
*where* a key should go, then have the user set it (\`workser env set\` writes it
|
|
3907
|
+
without you ever seeing it).
|
|
3908
|
+
`
|
|
3909
|
+
},
|
|
3910
|
+
{
|
|
3911
|
+
topic: "deploy",
|
|
3912
|
+
title: "Deploy, environment variables & logs",
|
|
3913
|
+
summary: "Ship the app, configure it, and find out why it is down.",
|
|
3914
|
+
commands: ["deploy", "env", "logs", "versions", "domain", "open", "verify"],
|
|
3915
|
+
source: "skills/workser/reference/deploy.md",
|
|
3916
|
+
body: `# Deploy, environment variables & logs
|
|
3917
|
+
|
|
3918
|
+
Getting the app online and configured, and finding out why it isn't.
|
|
3919
|
+
|
|
3920
|
+
\`\`\`
|
|
3921
|
+
workser deploy [--prod] [--watch] # deploy (git \u2192 Vercel); --watch waits for live URL
|
|
3922
|
+
workser deploy status [id] # status of a deploy (default: latest)
|
|
3923
|
+
workser logs [-n 100] [-f] # recent logs
|
|
3924
|
+
workser versions # deploy history
|
|
3925
|
+
workser domain list # custom domains (read)
|
|
3926
|
+
workser open # open the live app
|
|
3927
|
+
workser verify # run typecheck/lint/build
|
|
3928
|
+
|
|
3929
|
+
workser env set KEY=VALUE [K2=V2\u2026] # set env vars
|
|
3930
|
+
workser env list # list keys (values masked)
|
|
3931
|
+
workser env get KEY # one value (sensitive)
|
|
3932
|
+
\`\`\`
|
|
3933
|
+
|
|
3934
|
+
## Notes that matter
|
|
3935
|
+
|
|
3936
|
+
- **\`verify\` gates "done".** Run \`workser verify --json\` before you say a task is
|
|
3937
|
+
finished. \`"ok": false\` means fix the listed errors and re-run \u2014 a green build is
|
|
3938
|
+
the bar, not your reading of the diff.
|
|
3939
|
+
- **\`deploy\` without \`--prod\` is a preview.** Preview first when the change is
|
|
3940
|
+
risky; \`--prod\` puts it in front of real users.
|
|
3941
|
+
- **\`--watch\` blocks until there's a live URL.** Without it you get a deploy id and
|
|
3942
|
+
have to poll \`deploy status\`.
|
|
3943
|
+
- **\`env set\` writes a value you never see.** That's the point \u2014 when the user has
|
|
3944
|
+
a secret, have them run it (or set it in Orbit) rather than pasting it to you.
|
|
3945
|
+
- **\`env get\` returns a secret.** Don't echo it into the conversation.
|
|
3946
|
+
- **\`env rm\` and \`domain set\` are owner-only** (exit 6). Tell the user to do it in
|
|
3947
|
+
Orbit; don't look for a workaround.
|
|
3948
|
+
|
|
3949
|
+
## After a successful deploy
|
|
3950
|
+
|
|
3951
|
+
Register the URL so it shows up on the user's task:
|
|
3952
|
+
|
|
3953
|
+
\`\`\`
|
|
3954
|
+
workser artifact add --url https://acme.workser.app --kind app -t "Storefront"
|
|
3955
|
+
\`\`\`
|
|
3956
|
+
|
|
3957
|
+
See \`reference/deliverables.md\`.
|
|
3958
|
+
|
|
3959
|
+
## Local vs cloud environment
|
|
3960
|
+
|
|
3961
|
+
\`env set\` configures the **cloud** environment (production and preview). The \`.env\`
|
|
3962
|
+
files in the app folder configure **this computer** \u2014 the user edits those in Orbit
|
|
3963
|
+
under Settings \u2192 "On this computer", and saving there restarts the dev server. Don't
|
|
3964
|
+
hand-edit \`.env.local\` to change cloud behaviour; they are different environments.
|
|
3965
|
+
`
|
|
3966
|
+
},
|
|
3967
|
+
{
|
|
3968
|
+
topic: "images",
|
|
3969
|
+
title: "Image generation",
|
|
3970
|
+
summary: "Generate images from a prompt, optionally conditioned on existing images.",
|
|
3971
|
+
commands: ["image"],
|
|
3972
|
+
source: "skills/workser/reference/images.md",
|
|
3973
|
+
body: `# Image generation
|
|
3974
|
+
|
|
3975
|
+
\`\`\`
|
|
3976
|
+
workser image generate "<prompt>" # alias: workser image gen
|
|
3977
|
+
-r, --reference <url...> # condition on existing images (up to 4)
|
|
3978
|
+
-o, --output <path> # also download the first image locally
|
|
3979
|
+
\`\`\`
|
|
3980
|
+
|
|
3981
|
+
Returns the generated image's public URL, so the usual move is to generate, then use
|
|
3982
|
+
that URL directly in the app.
|
|
3983
|
+
|
|
3984
|
+
\`\`\`bash
|
|
3985
|
+
workser image generate "flat illustration of a farm delivery van, brand colors" --json
|
|
3986
|
+
workser image gen "same van, from the side" -r https://\u2026 -o ./public/van.png --json
|
|
3987
|
+
\`\`\`
|
|
3988
|
+
|
|
3989
|
+
## Notes that matter
|
|
3990
|
+
|
|
3991
|
+
- **Reference images are image-to-image conditioning**, not attachments. Up to 4;
|
|
3992
|
+
anything beyond that is dropped.
|
|
3993
|
+
- **The model sometimes narrates instead of drawing** \u2014 a refusal or a clarifying
|
|
3994
|
+
question comes back as text rather than an image. Check that you actually got an
|
|
3995
|
+
image before wiring the URL into a page; an empty result is not a transport error
|
|
3996
|
+
to retry.
|
|
3997
|
+
- **\`--output\` writes only the first image.** If you asked for several, the rest
|
|
3998
|
+
exist only as URLs.
|
|
3999
|
+
- **Placeholder art is not a deliverable.** Generating a hero image to unblock a
|
|
4000
|
+
layout is fine; shipping it as the user's brand asset without asking is not.
|
|
4001
|
+
`
|
|
4002
|
+
},
|
|
4003
|
+
{
|
|
4004
|
+
topic: "memory",
|
|
4005
|
+
title: "Memory across conversations",
|
|
4006
|
+
summary: "Store and recall durable project knowledge shared with cloud agents.",
|
|
4007
|
+
commands: ["memory"],
|
|
4008
|
+
source: "skills/workser/reference/memory.md",
|
|
4009
|
+
body: `# Memory \u2014 remember across conversations, not just this one
|
|
4010
|
+
|
|
4011
|
+
\`\`\`
|
|
4012
|
+
workser memory add "<content>" [--metadata <json>] # remember for future conversations
|
|
4013
|
+
workser memory search "<query>" [--limit N] # recall what was learned before
|
|
4014
|
+
workser memory forget <memoryId> # soft-delete an outdated memory
|
|
4015
|
+
\`\`\`
|
|
4016
|
+
|
|
4017
|
+
## Why this exists
|
|
4018
|
+
|
|
4019
|
+
Every conversation you run is otherwise a fresh start \u2014 no memory of what you or the
|
|
4020
|
+
user decided last time. This stores durable, searchable memory for the **current
|
|
4021
|
+
project**, and it is the **same memory space** Workser's cloud agents write to for
|
|
4022
|
+
this project. Add something here and a cloud agent \u2014 or your own next conversation \u2014
|
|
4023
|
+
can \`memory search\` and find it.
|
|
4024
|
+
|
|
4025
|
+
## Using it well
|
|
4026
|
+
|
|
4027
|
+
- **Search before assuming you don't know.** Before concluding something about this
|
|
4028
|
+
project is undocumented, run \`workser memory search "<topic>"\`. It may already be
|
|
4029
|
+
recorded.
|
|
4030
|
+
- **Store decisions, not chatter.** User preferences, decisions made, constraints,
|
|
4031
|
+
requirements \u2014 things worth knowing next week. Not "the build passed".
|
|
4032
|
+
- **\`forget\` is a soft delete.** The content stays retrievable by id but is excluded
|
|
4033
|
+
from future searches. Use it when something is wrong or outdated, rather than
|
|
4034
|
+
adding a contradicting memory on top.
|
|
4035
|
+
- **Never store a secret.** Memory is retrievable and displayable.
|
|
4036
|
+
`
|
|
4037
|
+
},
|
|
4038
|
+
{
|
|
4039
|
+
topic: "neon",
|
|
4040
|
+
title: "The project's own Neon backend",
|
|
4041
|
+
summary: "Neon-branch object storage and functions. Dedicated tenancy only.",
|
|
4042
|
+
commands: ["neon"],
|
|
4043
|
+
source: "skills/workser/reference/neon-backend.md",
|
|
4044
|
+
body: `# The project's own Neon backend
|
|
4045
|
+
|
|
4046
|
+
S3-compatible object storage and Node.js HTTP functions on the project's own Neon
|
|
4047
|
+
branch \u2014 they branch with the database. **Additive** infrastructure, not a
|
|
4048
|
+
replacement for \`workser storage\`.
|
|
4049
|
+
|
|
4050
|
+
\`\`\`
|
|
4051
|
+
workser neon status # tenancy + toggles + region verdict
|
|
4052
|
+
workser neon storage list | create <name> | rm <bucket>
|
|
4053
|
+
workser neon storage ls <bucket> [prefix]
|
|
4054
|
+
workser neon storage put <bucket> <local> [key]
|
|
4055
|
+
workser neon storage get <bucket> <key> [dest]
|
|
4056
|
+
workser neon storage url <bucket> <key> # temporary download URL
|
|
4057
|
+
workser neon functions list | deploy <slug> <zip> | rm <slug>
|
|
4058
|
+
\`\`\`
|
|
4059
|
+
|
|
4060
|
+
## Check \`neon status\` first \u2014 always
|
|
4061
|
+
|
|
4062
|
+
Three things must all be true: **dedicated tenancy**, the capability **switched on**,
|
|
4063
|
+
and a **supported region**.
|
|
4064
|
+
|
|
4065
|
+
Region is fixed when the project is created. \`regionSupportsNeonBackend: false\` is
|
|
4066
|
+
**final, not retryable** \u2014 no amount of waiting or retrying changes it. When you see
|
|
4067
|
+
it, say so plainly and fall back to \`workser storage\` (the default bucket).
|
|
4068
|
+
|
|
4069
|
+
## Notes that matter
|
|
4070
|
+
|
|
4071
|
+
- **\`neon storage rm <bucket>\` deletes the bucket and everything in it.** Not
|
|
4072
|
+
reversible. Confirm with the user first.
|
|
4073
|
+
- **\`neon storage url\`** issues a short-lived URL \u2014 prefer it to moving large files
|
|
4074
|
+
through Workser.
|
|
4075
|
+
- **Functions deploy from a zip.** Build the bundle first, then
|
|
4076
|
+
\`workser neon functions deploy <slug> <zip>\`.
|
|
4077
|
+
- **Most apps don't need this.** If the user just wants to store uploads, the default
|
|
4078
|
+
bucket in \`reference/storage.md\` is the answer.
|
|
4079
|
+
`
|
|
4080
|
+
},
|
|
4081
|
+
{
|
|
4082
|
+
topic: "roles",
|
|
4083
|
+
title: "Delegate to roles",
|
|
4084
|
+
summary: "Hand a focused subtask to another configured local agent.",
|
|
4085
|
+
commands: ["agent"],
|
|
4086
|
+
source: "skills/workser/reference/roles.md",
|
|
4087
|
+
body: `# Delegate to roles
|
|
4088
|
+
|
|
4089
|
+
The user can configure **roles** \u2014 named specialists each backed by a local CLI agent
|
|
4090
|
+
(e.g. \`qa\` \u2192 codex, \`designer\` \u2192 claude_code).
|
|
4091
|
+
|
|
4092
|
+
\`\`\`
|
|
4093
|
+
workser agent list # main agent + configured roles (+ which are runnable)
|
|
4094
|
+
workser agent run <role> "<task>" # delegate a focused subtask (runs isolated)
|
|
4095
|
+
workser agent main # show the configured main agent
|
|
4096
|
+
\`\`\`
|
|
4097
|
+
|
|
4098
|
+
## How to use it
|
|
4099
|
+
|
|
4100
|
+
Run \`workser agent list --json\` first \u2014 it tells you which roles exist **and** which
|
|
4101
|
+
are actually runnable on this machine. Delegating to a role that isn't installed just
|
|
4102
|
+
fails.
|
|
4103
|
+
|
|
4104
|
+
\`workser agent run <role> "<task>" --json\` runs the role as an **isolated local
|
|
4105
|
+
subagent** with its own context and returns \`{role, agent, output, exitCode}\`.
|
|
4106
|
+
|
|
4107
|
+
- Hand off focused subtasks \u2014 review this diff, design this screen \u2014 to keep your own
|
|
4108
|
+
context lean and get a specialized second perspective.
|
|
4109
|
+
- **A non-zero \`exitCode\` means the role's run failed.** Surface that; don't quietly
|
|
4110
|
+
treat empty output as success.
|
|
4111
|
+
- The subagent doesn't share your context. Put everything it needs in the task
|
|
4112
|
+
string; it cannot see the conversation you're in.
|
|
4113
|
+
`
|
|
4114
|
+
},
|
|
4115
|
+
{
|
|
4116
|
+
topic: "storage",
|
|
4117
|
+
title: "File storage",
|
|
4118
|
+
summary: "The project's default bucket \u2014 upload, list, download.",
|
|
4119
|
+
commands: ["storage"],
|
|
4120
|
+
source: "skills/workser/reference/storage.md",
|
|
4121
|
+
body: `# File storage
|
|
4122
|
+
|
|
4123
|
+
The project's default bucket (Cloudflare R2 behind Workser). Every project gets one;
|
|
4124
|
+
\`create\` is idempotent.
|
|
4125
|
+
|
|
4126
|
+
\`\`\`
|
|
4127
|
+
workser storage create [name] # provision the bucket (idempotent)
|
|
4128
|
+
workser storage list # the project's bucket
|
|
4129
|
+
workser storage ls [prefix] # list objects in the bucket
|
|
4130
|
+
workser storage put <local> <key> # upload a file into the bucket
|
|
4131
|
+
workser storage get <key> [dest] # download an object (or print its URL)
|
|
4132
|
+
\`\`\`
|
|
4133
|
+
|
|
4134
|
+
## Notes that matter
|
|
4135
|
+
|
|
4136
|
+
- **One bucket per project, shared by its apps.** Namespace your keys by app or
|
|
4137
|
+
feature (\`invoices/2026/\u2026\`) rather than assuming the root is yours.
|
|
4138
|
+
- **\`storage get\` with no destination prints a URL** instead of writing a file \u2014
|
|
4139
|
+
useful when you just want to hand the user something to click.
|
|
4140
|
+
- **This is not where app uploads should go through you.** At runtime the app uses
|
|
4141
|
+
\`workser.storage\` from \`@workser/app\`, and for anything large it should request a
|
|
4142
|
+
presigned upload URL so the bytes never pass through Workser. See the
|
|
4143
|
+
\`workser-sdk\` skill.
|
|
4144
|
+
|
|
4145
|
+
## Not the same as \`workser neon storage\`
|
|
4146
|
+
|
|
4147
|
+
\`storage\` is the default R2 bucket every project has. \`neon storage\` is additive
|
|
4148
|
+
infrastructure on the project's own Neon branch, available only on dedicated tenancy
|
|
4149
|
+
in a supported region. They are different stores \u2014 a file put in one is not visible
|
|
4150
|
+
in the other. See \`reference/neon-backend.md\`.
|
|
4151
|
+
`
|
|
4152
|
+
}
|
|
4153
|
+
];
|
|
4154
|
+
|
|
4155
|
+
// src/commands/help.ts
|
|
4156
|
+
function findTopic(name) {
|
|
4157
|
+
const wanted = name.trim().toLowerCase();
|
|
4158
|
+
return HELP_TOPICS.find((t) => t.topic === wanted) ?? // A command name is what an agent reaches for first — `workser help db`
|
|
4159
|
+
// should not be a dead end just because the topic is called "database".
|
|
4160
|
+
HELP_TOPICS.find((t) => t.commands.includes(wanted));
|
|
4161
|
+
}
|
|
4162
|
+
function listTopics() {
|
|
4163
|
+
ok(
|
|
4164
|
+
HELP_TOPICS.map((t) => ({
|
|
4165
|
+
topic: t.topic,
|
|
4166
|
+
title: t.title,
|
|
4167
|
+
summary: t.summary,
|
|
4168
|
+
commands: t.commands
|
|
4169
|
+
})),
|
|
4170
|
+
() => {
|
|
4171
|
+
line(import_picocolors2.default.bold("Guides") + import_picocolors2.default.dim(" \u2014 workser help <topic>"));
|
|
4172
|
+
line();
|
|
4173
|
+
const width = Math.max(...HELP_TOPICS.map((t) => t.topic.length));
|
|
4174
|
+
for (const t of HELP_TOPICS) {
|
|
4175
|
+
line(` ${import_picocolors2.default.cyan(t.topic.padEnd(width))} ${t.summary}`);
|
|
4176
|
+
}
|
|
4177
|
+
line();
|
|
4178
|
+
line(import_picocolors2.default.dim(" workser <command> --help exact flags, generated from the code"));
|
|
4179
|
+
}
|
|
4180
|
+
);
|
|
4181
|
+
}
|
|
4182
|
+
function registerHelp(program3) {
|
|
4183
|
+
const withHelpCommand = program3;
|
|
4184
|
+
withHelpCommand.helpCommand?.(false);
|
|
4185
|
+
program3.command("help [topic]").description("guides for using this CLI (`workser help` lists them)").action((topic) => {
|
|
4186
|
+
if (!topic) {
|
|
4187
|
+
listTopics();
|
|
4188
|
+
return;
|
|
4189
|
+
}
|
|
4190
|
+
const found = findTopic(topic);
|
|
4191
|
+
if (found) {
|
|
4192
|
+
ok({ topic: found.topic, title: found.title, content: found.body }, () => {
|
|
4193
|
+
process.stdout.write(found.body.endsWith("\n") ? found.body : found.body + "\n");
|
|
4194
|
+
});
|
|
4195
|
+
return;
|
|
4196
|
+
}
|
|
4197
|
+
const command = program3.commands.find(
|
|
4198
|
+
(c) => c.name() === topic || c.aliases().includes(topic)
|
|
4199
|
+
);
|
|
4200
|
+
if (command && !isJson()) {
|
|
4201
|
+
command.outputHelp();
|
|
4202
|
+
return;
|
|
4203
|
+
}
|
|
4204
|
+
throw new WorkserError(
|
|
4205
|
+
`No guide for "${topic}". Run \`workser help\` to list them` + (command ? `, or \`workser ${topic} --help\` for its flags` : "") + ".",
|
|
4206
|
+
{ code: "not_found" }
|
|
4207
|
+
);
|
|
4208
|
+
});
|
|
4209
|
+
}
|
|
4210
|
+
|
|
4211
|
+
// src/commands/status.ts
|
|
4212
|
+
var import_picocolors3 = __toESM(require_picocolors(), 1);
|
|
4213
|
+
|
|
3656
4214
|
// src/context.ts
|
|
3657
4215
|
import { resolve } from "path";
|
|
3658
4216
|
|
|
3659
4217
|
// src/config.ts
|
|
3660
4218
|
import { homedir } from "os";
|
|
3661
4219
|
import { join } from "path";
|
|
3662
|
-
import {
|
|
4220
|
+
import {
|
|
4221
|
+
chmodSync,
|
|
4222
|
+
existsSync,
|
|
4223
|
+
mkdirSync,
|
|
4224
|
+
readFileSync,
|
|
4225
|
+
writeFileSync
|
|
4226
|
+
} from "fs";
|
|
3663
4227
|
var GLOBAL_DIR = join(homedir(), ".workser");
|
|
3664
4228
|
var SESSION_FILE = join(GLOBAL_DIR, "session.json");
|
|
3665
4229
|
function readSession() {
|
|
@@ -3672,13 +4236,17 @@ function readSession() {
|
|
|
3672
4236
|
}
|
|
3673
4237
|
function writeSession(patch) {
|
|
3674
4238
|
const next = { ...readSession(), ...patch };
|
|
3675
|
-
mkdirSync(GLOBAL_DIR, { recursive: true });
|
|
3676
|
-
writeFileSync(SESSION_FILE, JSON.stringify(next, null, 2));
|
|
4239
|
+
mkdirSync(GLOBAL_DIR, { recursive: true, mode: 448 });
|
|
4240
|
+
writeFileSync(SESSION_FILE, JSON.stringify(next, null, 2), { mode: 384 });
|
|
4241
|
+
try {
|
|
4242
|
+
chmodSync(SESSION_FILE, 384);
|
|
4243
|
+
} catch {
|
|
4244
|
+
}
|
|
3677
4245
|
return next;
|
|
3678
4246
|
}
|
|
3679
4247
|
function clearSession() {
|
|
3680
4248
|
try {
|
|
3681
|
-
writeFileSync(SESSION_FILE, "{}");
|
|
4249
|
+
writeFileSync(SESSION_FILE, "{}", { mode: 384 });
|
|
3682
4250
|
} catch {
|
|
3683
4251
|
}
|
|
3684
4252
|
}
|
|
@@ -3700,16 +4268,18 @@ var CLOUD_DEFAULT = process.env.WORKSER_API_URL || "https://api.workser.ai";
|
|
|
3700
4268
|
function buildContext(opts) {
|
|
3701
4269
|
const session = readSession();
|
|
3702
4270
|
const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd();
|
|
3703
|
-
const endpointRaw = opts.endpoint || process.env.WORKSER_DAEMON_URL || session.endpoint || CLOUD_DEFAULT;
|
|
3704
|
-
const endpoint = endpointRaw.replace(/\/+$/, "");
|
|
3705
4271
|
const token = opts.token || process.env.WORKSER_TOKEN || session.token;
|
|
3706
|
-
const
|
|
3707
|
-
endpoint
|
|
3708
|
-
)
|
|
4272
|
+
const overridden = Boolean(
|
|
4273
|
+
opts.endpoint || opts.token || process.env.WORKSER_DAEMON_URL || process.env.WORKSER_TOKEN
|
|
4274
|
+
);
|
|
4275
|
+
const socketPath = overridden ? void 0 : session.socketPath;
|
|
4276
|
+
const endpointRaw = socketPath ? "http://localhost" : opts.endpoint || process.env.WORKSER_DAEMON_URL || session.endpoint || CLOUD_DEFAULT;
|
|
4277
|
+
const endpoint = endpointRaw.replace(/\/+$/, "");
|
|
4278
|
+
const mode2 = socketPath ? "daemon" : /^https?:\/\/(127\.0\.0\.1|localhost|\[::1\])(:|\/|$)/i.test(endpoint) ? "daemon" : "cloud";
|
|
3709
4279
|
const link = readProjectLink(cwd);
|
|
3710
4280
|
const projectId = opts.project || process.env.WORKSER_PROJECT_ID || link?.projectId || session.defaultProjectId;
|
|
3711
4281
|
const runId = process.env.WORKSER_RUN_ID || void 0;
|
|
3712
|
-
return { endpoint, token, mode: mode2, cwd, projectId, runId };
|
|
4282
|
+
return { endpoint, socketPath, token, mode: mode2, cwd, projectId, runId };
|
|
3713
4283
|
}
|
|
3714
4284
|
function runTarget(ctx) {
|
|
3715
4285
|
return ctx.runId || "current";
|
|
@@ -3740,6 +4310,31 @@ function action(fn) {
|
|
|
3740
4310
|
}
|
|
3741
4311
|
|
|
3742
4312
|
// src/client.ts
|
|
4313
|
+
import * as http from "http";
|
|
4314
|
+
function requestOverSocket(socketPath, pathWithQuery, init) {
|
|
4315
|
+
return new Promise((resolve4, reject) => {
|
|
4316
|
+
const req = http.request(
|
|
4317
|
+
{ socketPath, path: pathWithQuery, method: init.method, headers: init.headers },
|
|
4318
|
+
(res) => {
|
|
4319
|
+
let text = "";
|
|
4320
|
+
res.setEncoding("utf8");
|
|
4321
|
+
res.on("data", (c) => text += c);
|
|
4322
|
+
res.on(
|
|
4323
|
+
"end",
|
|
4324
|
+
() => resolve4({
|
|
4325
|
+
ok: (res.statusCode ?? 0) >= 200 && (res.statusCode ?? 0) < 300,
|
|
4326
|
+
status: res.statusCode ?? 0,
|
|
4327
|
+
statusText: res.statusMessage ?? "",
|
|
4328
|
+
text
|
|
4329
|
+
})
|
|
4330
|
+
);
|
|
4331
|
+
}
|
|
4332
|
+
);
|
|
4333
|
+
req.on("error", reject);
|
|
4334
|
+
if (init.body !== void 0) req.write(init.body);
|
|
4335
|
+
req.end();
|
|
4336
|
+
});
|
|
4337
|
+
}
|
|
3743
4338
|
async function api(ctx, path, opts = {}) {
|
|
3744
4339
|
const url = new URL(ctx.endpoint + path);
|
|
3745
4340
|
if (opts.query) {
|
|
@@ -3752,21 +4347,42 @@ async function api(ctx, path, opts = {}) {
|
|
|
3752
4347
|
"user-agent": "workser-cli"
|
|
3753
4348
|
};
|
|
3754
4349
|
if (ctx.token) headers.authorization = `Bearer ${ctx.token}`;
|
|
4350
|
+
if (ctx.mode === "daemon" && ctx.runId) {
|
|
4351
|
+
headers["x-workser-run-id"] = ctx.runId;
|
|
4352
|
+
}
|
|
3755
4353
|
if (opts.body !== void 0) headers["content-type"] = "application/json";
|
|
4354
|
+
const method = opts.method ?? (opts.body !== void 0 ? "POST" : "GET");
|
|
4355
|
+
const bodyText = opts.body !== void 0 ? JSON.stringify(opts.body) : void 0;
|
|
3756
4356
|
let res;
|
|
3757
4357
|
try {
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
4358
|
+
if (ctx.socketPath) {
|
|
4359
|
+
if (bodyText !== void 0) {
|
|
4360
|
+
headers["content-length"] = String(Buffer.byteLength(bodyText));
|
|
4361
|
+
}
|
|
4362
|
+
res = await requestOverSocket(ctx.socketPath, url.pathname + url.search, {
|
|
4363
|
+
method,
|
|
4364
|
+
headers,
|
|
4365
|
+
body: bodyText
|
|
4366
|
+
});
|
|
4367
|
+
} else {
|
|
4368
|
+
const r = await fetch(url, { method, headers, body: bodyText });
|
|
4369
|
+
res = {
|
|
4370
|
+
ok: r.ok,
|
|
4371
|
+
status: r.status,
|
|
4372
|
+
statusText: r.statusText,
|
|
4373
|
+
text: await r.text()
|
|
4374
|
+
};
|
|
4375
|
+
}
|
|
3763
4376
|
} catch (e) {
|
|
3764
4377
|
throw new WorkserError(
|
|
3765
4378
|
ctx.mode === "daemon" ? "Can't reach Workser Orbit (local daemon). Is the app running?" : `Can't reach Workser at ${ctx.endpoint}.`,
|
|
3766
|
-
{
|
|
4379
|
+
{
|
|
4380
|
+
code: "not_connected",
|
|
4381
|
+
details: e instanceof Error ? e.message : String(e)
|
|
4382
|
+
}
|
|
3767
4383
|
);
|
|
3768
4384
|
}
|
|
3769
|
-
const text =
|
|
4385
|
+
const text = res.text;
|
|
3770
4386
|
const data = text ? safeJson(text) : void 0;
|
|
3771
4387
|
if (!res.ok) {
|
|
3772
4388
|
const nested = data && typeof data === "object" && data.error && typeof data.error === "object" ? data.error : void 0;
|
|
@@ -3803,15 +4419,15 @@ function registerStatus(program3) {
|
|
|
3803
4419
|
action(async ({ ctx }) => {
|
|
3804
4420
|
const data = await api(ctx, "/v1/status", { query: { project: ctx.projectId } });
|
|
3805
4421
|
ok(data, () => {
|
|
3806
|
-
line(
|
|
4422
|
+
line(import_picocolors3.default.bold("Workser") + import_picocolors3.default.dim(` (${ctx.mode} \xB7 ${ctx.endpoint})`));
|
|
3807
4423
|
line(` user: ${data.user?.email ?? "\u2014"}`);
|
|
3808
4424
|
line(` workspace: ${data.workspace?.name ?? "\u2014"}`);
|
|
3809
4425
|
line(
|
|
3810
|
-
` project: ${data.project?.name ?? "\u2014"}` + (data.project?.id ?
|
|
4426
|
+
` project: ${data.project?.name ?? "\u2014"}` + (data.project?.id ? import_picocolors3.default.dim(` (${data.project.id})`) : "")
|
|
3811
4427
|
);
|
|
3812
4428
|
if (data.latestDeploy) {
|
|
3813
4429
|
line(
|
|
3814
|
-
` deploy: ${colorStatus(data.latestDeploy.status)}` + (data.latestDeploy.url ? ` ${
|
|
4430
|
+
` deploy: ${colorStatus(data.latestDeploy.status)}` + (data.latestDeploy.url ? ` ${import_picocolors3.default.cyan(data.latestDeploy.url)}` : "")
|
|
3815
4431
|
);
|
|
3816
4432
|
}
|
|
3817
4433
|
});
|
|
@@ -3823,15 +4439,15 @@ function colorStatus(s) {
|
|
|
3823
4439
|
case "ready":
|
|
3824
4440
|
case "live":
|
|
3825
4441
|
case "success":
|
|
3826
|
-
return
|
|
4442
|
+
return import_picocolors3.default.green(s);
|
|
3827
4443
|
case "error":
|
|
3828
4444
|
case "failed":
|
|
3829
4445
|
case "canceled":
|
|
3830
|
-
return
|
|
4446
|
+
return import_picocolors3.default.red(s);
|
|
3831
4447
|
case "building":
|
|
3832
4448
|
case "queued":
|
|
3833
4449
|
case "deploying":
|
|
3834
|
-
return
|
|
4450
|
+
return import_picocolors3.default.yellow(s);
|
|
3835
4451
|
default:
|
|
3836
4452
|
return s ?? "\u2014";
|
|
3837
4453
|
}
|
|
@@ -3892,7 +4508,7 @@ function registerWhoami(program3) {
|
|
|
3892
4508
|
}
|
|
3893
4509
|
|
|
3894
4510
|
// src/commands/project.ts
|
|
3895
|
-
var
|
|
4511
|
+
var import_picocolors4 = __toESM(require_picocolors(), 1);
|
|
3896
4512
|
function registerProject(program3) {
|
|
3897
4513
|
const project = program3.command("project").description("Inspect the project linked to this directory");
|
|
3898
4514
|
project.command("show").description("Show the project pinned to this directory").action(
|
|
@@ -3900,8 +4516,8 @@ function registerProject(program3) {
|
|
|
3900
4516
|
const projectId = requireProject(ctx);
|
|
3901
4517
|
const p = await api(ctx, `/v1/projects/${encodeURIComponent(projectId)}`);
|
|
3902
4518
|
ok(p, () => {
|
|
3903
|
-
line(`${
|
|
3904
|
-
if (p.url) line(
|
|
4519
|
+
line(`${import_picocolors4.default.bold(p.name ?? "\u2014")}${p.id ? import_picocolors4.default.dim(` (${p.id})`) : ""}`);
|
|
4520
|
+
if (p.url) line(import_picocolors4.default.cyan(p.url));
|
|
3905
4521
|
});
|
|
3906
4522
|
})
|
|
3907
4523
|
);
|
|
@@ -3923,22 +4539,79 @@ function registerProject(program3) {
|
|
|
3923
4539
|
})
|
|
3924
4540
|
)
|
|
3925
4541
|
);
|
|
4542
|
+
project.command("apps").description("List the project's apps (id, type, status, URL, local folder)").action(
|
|
4543
|
+
action(async ({ ctx }) => {
|
|
4544
|
+
const projectId = requireProject(ctx);
|
|
4545
|
+
const [apps, folders] = await Promise.all([
|
|
4546
|
+
api(ctx, `/v1/apps`, { query: { project: projectId } }),
|
|
4547
|
+
api(ctx, `/v1/app-folders`, { query: { project: projectId } }).catch(
|
|
4548
|
+
() => []
|
|
4549
|
+
)
|
|
4550
|
+
]);
|
|
4551
|
+
const withFolders = (apps ?? []).map((a) => ({
|
|
4552
|
+
...a,
|
|
4553
|
+
localPath: folderFor(folders, a.id)
|
|
4554
|
+
}));
|
|
4555
|
+
ok(withFolders, () => {
|
|
4556
|
+
if (!withFolders.length) return line(import_picocolors4.default.dim("No apps in this project."));
|
|
4557
|
+
for (const a of withFolders) printApp(a);
|
|
4558
|
+
});
|
|
4559
|
+
})
|
|
4560
|
+
);
|
|
4561
|
+
project.command("app <id>").description("Show one app: type, status, URLs and where its code lives here").action(
|
|
4562
|
+
action(async ({ ctx, args }) => {
|
|
4563
|
+
const projectId = requireProject(ctx);
|
|
4564
|
+
const id = args[0];
|
|
4565
|
+
const [app, folders] = await Promise.all([
|
|
4566
|
+
api(ctx, `/v1/apps/${encodeURIComponent(id)}`),
|
|
4567
|
+
api(ctx, `/v1/app-folders`, { query: { project: projectId } }).catch(
|
|
4568
|
+
() => []
|
|
4569
|
+
)
|
|
4570
|
+
]);
|
|
4571
|
+
const merged = { ...app, localPath: folderFor(folders, app?.id ?? id) };
|
|
4572
|
+
ok(merged, () => {
|
|
4573
|
+
printApp(merged);
|
|
4574
|
+
if (merged.previewUrl) line(` preview ${import_picocolors4.default.cyan(merged.previewUrl)}`);
|
|
4575
|
+
if (merged.productionUrl)
|
|
4576
|
+
line(` production ${import_picocolors4.default.cyan(merged.productionUrl)}`);
|
|
4577
|
+
if (!merged.localPath) {
|
|
4578
|
+
line(
|
|
4579
|
+
import_picocolors4.default.dim(
|
|
4580
|
+
" no folder linked on this computer \u2014 link one in Workser Orbit"
|
|
4581
|
+
)
|
|
4582
|
+
);
|
|
4583
|
+
}
|
|
4584
|
+
});
|
|
4585
|
+
})
|
|
4586
|
+
);
|
|
3926
4587
|
project.command("list").description("List the workspace's projects").action(
|
|
3927
4588
|
action(async ({ ctx }) => {
|
|
3928
4589
|
const items = await api(ctx, `/v1/projects`);
|
|
3929
4590
|
ok(items, () => {
|
|
3930
|
-
if (!items?.length) return line(
|
|
4591
|
+
if (!items?.length) return line(import_picocolors4.default.dim("No projects."));
|
|
3931
4592
|
for (const p of items) {
|
|
3932
|
-
const pinned = ctx.projectId && p.id === ctx.projectId ?
|
|
3933
|
-
line(`${pinned}${p.name ?? "\u2014"}${p.id ?
|
|
4593
|
+
const pinned = ctx.projectId && p.id === ctx.projectId ? import_picocolors4.default.green("\u25CF ") : " ";
|
|
4594
|
+
line(`${pinned}${p.name ?? "\u2014"}${p.id ? import_picocolors4.default.dim(` (${p.id})`) : ""}`);
|
|
3934
4595
|
}
|
|
3935
4596
|
});
|
|
3936
4597
|
})
|
|
3937
4598
|
);
|
|
3938
4599
|
}
|
|
4600
|
+
function folderFor(folders, appId) {
|
|
4601
|
+
if (!Array.isArray(folders)) return void 0;
|
|
4602
|
+
const hit = folders.find((f) => f?.web_app_id === appId);
|
|
4603
|
+
return hit?.local_path || void 0;
|
|
4604
|
+
}
|
|
4605
|
+
function printApp(a) {
|
|
4606
|
+
const bits = [a.type, a.status].filter(Boolean).join(" \xB7 ");
|
|
4607
|
+
line(
|
|
4608
|
+
`${import_picocolors4.default.bold(a.name ?? "\u2014")}${a.id ? import_picocolors4.default.dim(` (${a.id})`) : ""}${bits ? " " + import_picocolors4.default.dim(bits) : ""}`
|
|
4609
|
+
);
|
|
4610
|
+
if (a.localPath) line(import_picocolors4.default.dim(` ${a.localPath}`));
|
|
4611
|
+
}
|
|
3939
4612
|
|
|
3940
4613
|
// src/commands/db.ts
|
|
3941
|
-
var
|
|
4614
|
+
var import_picocolors5 = __toESM(require_picocolors(), 1);
|
|
3942
4615
|
function registerDb(program3) {
|
|
3943
4616
|
const db = program3.command("db").description("Provision and inspect the project's Neon Postgres database");
|
|
3944
4617
|
db.command("create").description("Provision a Postgres database for the project (idempotent)").action(
|
|
@@ -3948,7 +4621,7 @@ function registerDb(program3) {
|
|
|
3948
4621
|
ok(
|
|
3949
4622
|
res,
|
|
3950
4623
|
() => line(
|
|
3951
|
-
res.created === false ? `Database already provisioned${
|
|
4624
|
+
res.created === false ? `Database already provisioned${import_picocolors5.default.dim(` (${res.name ?? "db"})`)}.` : `Provisioned database ${import_picocolors5.default.bold(res.name ?? "db")}${import_picocolors5.default.dim(` ${res.region ?? ""} ${res.status ?? ""}`.trimEnd())}.`
|
|
3952
4625
|
)
|
|
3953
4626
|
);
|
|
3954
4627
|
})
|
|
@@ -3958,8 +4631,8 @@ function registerDb(program3) {
|
|
|
3958
4631
|
const projectId = requireProject(ctx);
|
|
3959
4632
|
const items = await api(ctx, `/v1/projects/${projectId}/databases`);
|
|
3960
4633
|
ok(items, () => {
|
|
3961
|
-
if (!items?.length) return line(
|
|
3962
|
-
for (const d of items) line(`${d.name}${
|
|
4634
|
+
if (!items?.length) return line(import_picocolors5.default.dim("No database yet. `workser db create`."));
|
|
4635
|
+
for (const d of items) line(`${d.name}${import_picocolors5.default.dim(` ${d.region ?? ""} ${d.status ?? ""}`.trimEnd())}`);
|
|
3963
4636
|
});
|
|
3964
4637
|
})
|
|
3965
4638
|
);
|
|
@@ -3976,12 +4649,12 @@ function registerDb(program3) {
|
|
|
3976
4649
|
const projectId = requireProject(ctx);
|
|
3977
4650
|
const rows = await api(ctx, `/v1/projects/${projectId}/db/tables`);
|
|
3978
4651
|
ok(rows, () => {
|
|
3979
|
-
if (!rows?.length) return line(
|
|
4652
|
+
if (!rows?.length) return line(import_picocolors5.default.dim("No tables."));
|
|
3980
4653
|
for (const t of rows) {
|
|
3981
|
-
const schema = t.table_schema && t.table_schema !== "public" ?
|
|
3982
|
-
const cols = t.column_count != null ?
|
|
3983
|
-
const count = t.row_count != null ?
|
|
3984
|
-
const size = t.table_size != null ?
|
|
4654
|
+
const schema = t.table_schema && t.table_schema !== "public" ? import_picocolors5.default.dim(`${t.table_schema}.`) : "";
|
|
4655
|
+
const cols = t.column_count != null ? import_picocolors5.default.dim(` ${t.column_count} cols`) : "";
|
|
4656
|
+
const count = t.row_count != null ? import_picocolors5.default.dim(` ${t.row_count} rows`) : "";
|
|
4657
|
+
const size = t.table_size != null ? import_picocolors5.default.dim(` ${fmtBytes(t.table_size)}`) : "";
|
|
3985
4658
|
line(`${schema}${t.table_name}${cols}${count}${size}`);
|
|
3986
4659
|
}
|
|
3987
4660
|
});
|
|
@@ -3993,11 +4666,11 @@ function registerDb(program3) {
|
|
|
3993
4666
|
const table = args[0];
|
|
3994
4667
|
const cols = await api(ctx, `/v1/projects/${projectId}/db/tables/${encodeURIComponent(table)}/schema`);
|
|
3995
4668
|
ok(cols, () => {
|
|
3996
|
-
if (!cols?.length) return line(
|
|
4669
|
+
if (!cols?.length) return line(import_picocolors5.default.dim("No columns (does the table exist?)."));
|
|
3997
4670
|
for (const c of cols) {
|
|
3998
|
-
const nn = c.is_nullable === "NO" || c.is_nullable === false ?
|
|
3999
|
-
const def = c.column_default ?
|
|
4000
|
-
line(`${c.column_name} ${
|
|
4671
|
+
const nn = c.is_nullable === "NO" || c.is_nullable === false ? import_picocolors5.default.dim(" not null") : "";
|
|
4672
|
+
const def = c.column_default ? import_picocolors5.default.dim(` default ${c.column_default}`) : "";
|
|
4673
|
+
line(`${c.column_name} ${import_picocolors5.default.cyan(c.data_type)}${nn}${def}`);
|
|
4001
4674
|
}
|
|
4002
4675
|
});
|
|
4003
4676
|
})
|
|
@@ -4023,23 +4696,23 @@ function registerDb(program3) {
|
|
|
4023
4696
|
}
|
|
4024
4697
|
function printRows(rows, total) {
|
|
4025
4698
|
if (!rows?.length) {
|
|
4026
|
-
line(
|
|
4699
|
+
line(import_picocolors5.default.dim("(0 rows)"));
|
|
4027
4700
|
return;
|
|
4028
4701
|
}
|
|
4029
4702
|
const cols = Object.keys(rows[0]);
|
|
4030
|
-
line(
|
|
4703
|
+
line(import_picocolors5.default.dim(cols.join(" ")));
|
|
4031
4704
|
for (const r of rows) {
|
|
4032
4705
|
line(cols.map((c) => fmtCell(r[c])).join(" "));
|
|
4033
4706
|
}
|
|
4034
4707
|
const shown = rows.length;
|
|
4035
4708
|
line(
|
|
4036
|
-
|
|
4709
|
+
import_picocolors5.default.dim(
|
|
4037
4710
|
total != null && total > shown ? `(${shown} of ${total} rows)` : `(${shown} row${shown === 1 ? "" : "s"})`
|
|
4038
4711
|
)
|
|
4039
4712
|
);
|
|
4040
4713
|
}
|
|
4041
4714
|
function fmtCell(v) {
|
|
4042
|
-
if (v === null || v === void 0) return
|
|
4715
|
+
if (v === null || v === void 0) return import_picocolors5.default.dim("\u2205");
|
|
4043
4716
|
if (typeof v === "object") return JSON.stringify(v);
|
|
4044
4717
|
return String(v);
|
|
4045
4718
|
}
|
|
@@ -4052,7 +4725,7 @@ function fmtBytes(n) {
|
|
|
4052
4725
|
}
|
|
4053
4726
|
|
|
4054
4727
|
// src/commands/auth.ts
|
|
4055
|
-
var
|
|
4728
|
+
var import_picocolors6 = __toESM(require_picocolors(), 1);
|
|
4056
4729
|
function registerAuth(program3) {
|
|
4057
4730
|
const auth = program3.command("auth").description("Provision and inspect the project's auth (Better Auth)");
|
|
4058
4731
|
auth.command("enable").description("Enable auth for the project (idempotent)").action(
|
|
@@ -4062,7 +4735,7 @@ function registerAuth(program3) {
|
|
|
4062
4735
|
ok(res, () => {
|
|
4063
4736
|
const providers = (res.providers ?? []).join(", ") || "email";
|
|
4064
4737
|
line(
|
|
4065
|
-
res.created === false ? `Auth already enabled${
|
|
4738
|
+
res.created === false ? `Auth already enabled${import_picocolors6.default.dim(` (${providers})`)}.` : `Enabled auth ${import_picocolors6.default.dim(`(${providers})`)}.`
|
|
4066
4739
|
);
|
|
4067
4740
|
});
|
|
4068
4741
|
})
|
|
@@ -4074,7 +4747,7 @@ function registerAuth(program3) {
|
|
|
4074
4747
|
ok(res, () => {
|
|
4075
4748
|
if (!res.enabled) return line("disabled");
|
|
4076
4749
|
const providers = (res.providers ?? []).join(", ") || "email";
|
|
4077
|
-
line(`enabled ${
|
|
4750
|
+
line(`enabled ${import_picocolors6.default.dim(`(${providers})`)}`);
|
|
4078
4751
|
if (res.authMode) line(` mode: ${res.authMode}`);
|
|
4079
4752
|
if (res.authMode === "neon_managed") {
|
|
4080
4753
|
if (res.neonAuthOwnedBy) line(` owned by: ${res.neonAuthOwnedBy}`);
|
|
@@ -4086,7 +4759,7 @@ function registerAuth(program3) {
|
|
|
4086
4759
|
}
|
|
4087
4760
|
|
|
4088
4761
|
// src/commands/storage.ts
|
|
4089
|
-
var
|
|
4762
|
+
var import_picocolors7 = __toESM(require_picocolors(), 1);
|
|
4090
4763
|
import { readFile, writeFile } from "fs/promises";
|
|
4091
4764
|
import { basename, dirname } from "path";
|
|
4092
4765
|
function registerStorage(program3) {
|
|
@@ -4100,7 +4773,7 @@ function registerStorage(program3) {
|
|
|
4100
4773
|
ok(
|
|
4101
4774
|
res,
|
|
4102
4775
|
() => line(
|
|
4103
|
-
res.created === false ? `Bucket already exists${
|
|
4776
|
+
res.created === false ? `Bucket already exists${import_picocolors7.default.dim(` (${res.bucket})`)}.` : `Provisioned bucket ${import_picocolors7.default.bold(res.bucket || "(pending)")}.`
|
|
4104
4777
|
)
|
|
4105
4778
|
);
|
|
4106
4779
|
})
|
|
@@ -4110,7 +4783,7 @@ function registerStorage(program3) {
|
|
|
4110
4783
|
const projectId = requireProject(ctx);
|
|
4111
4784
|
const items = await api(ctx, `/v1/projects/${projectId}/storage`);
|
|
4112
4785
|
ok(items, () => {
|
|
4113
|
-
if (!items?.length) return line(
|
|
4786
|
+
if (!items?.length) return line(import_picocolors7.default.dim("No bucket yet. `workser storage create`."));
|
|
4114
4787
|
for (const b of items) line(b.bucket ?? b.name);
|
|
4115
4788
|
});
|
|
4116
4789
|
})
|
|
@@ -4120,9 +4793,9 @@ function registerStorage(program3) {
|
|
|
4120
4793
|
const projectId = requireProject(ctx);
|
|
4121
4794
|
const objects = await listFiles(ctx, projectId, args[0]);
|
|
4122
4795
|
ok(objects, () => {
|
|
4123
|
-
if (!objects.length) return line(
|
|
4796
|
+
if (!objects.length) return line(import_picocolors7.default.dim("No objects."));
|
|
4124
4797
|
for (const o of objects) {
|
|
4125
|
-
line(`${o.key}${
|
|
4798
|
+
line(`${o.key}${import_picocolors7.default.dim(` ${fmtSize(o.size)}${o.lastModified ? " " + o.lastModified : ""}`)}`);
|
|
4126
4799
|
}
|
|
4127
4800
|
});
|
|
4128
4801
|
})
|
|
@@ -4144,7 +4817,7 @@ function registerStorage(program3) {
|
|
|
4144
4817
|
});
|
|
4145
4818
|
ok(
|
|
4146
4819
|
res,
|
|
4147
|
-
() => success(`Uploaded ${
|
|
4820
|
+
() => success(`Uploaded ${import_picocolors7.default.bold(res.key ?? key)} ${import_picocolors7.default.dim(`(${fmtSize(bytes.length)})`)}.`)
|
|
4148
4821
|
);
|
|
4149
4822
|
})
|
|
4150
4823
|
);
|
|
@@ -4163,7 +4836,7 @@ function registerStorage(program3) {
|
|
|
4163
4836
|
await writeFile(out, bytes);
|
|
4164
4837
|
ok(
|
|
4165
4838
|
{ key, dest: out, bytes: bytes.length },
|
|
4166
|
-
() => success(`Downloaded ${
|
|
4839
|
+
() => success(`Downloaded ${import_picocolors7.default.bold(key)} \u2192 ${out} ${import_picocolors7.default.dim(`(${fmtSize(bytes.length)})`)}.`)
|
|
4167
4840
|
);
|
|
4168
4841
|
})
|
|
4169
4842
|
);
|
|
@@ -4194,8 +4867,253 @@ function fmtSize(n) {
|
|
|
4194
4867
|
return `${(n / 1024 / 1024).toFixed(1)}MB`;
|
|
4195
4868
|
}
|
|
4196
4869
|
|
|
4870
|
+
// src/commands/neon.ts
|
|
4871
|
+
var import_picocolors8 = __toESM(require_picocolors(), 1);
|
|
4872
|
+
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
4873
|
+
import { basename as basename2 } from "path";
|
|
4874
|
+
function registerNeon(program3) {
|
|
4875
|
+
const neon = program3.command("neon").description(
|
|
4876
|
+
"The project's own Neon backend: object storage buckets and functions"
|
|
4877
|
+
);
|
|
4878
|
+
neon.command("status").description(
|
|
4879
|
+
"Whether this project can use Neon storage/functions (tenancy, toggles, region)"
|
|
4880
|
+
).action(
|
|
4881
|
+
action(async ({ ctx }) => {
|
|
4882
|
+
const projectId = requireProject(ctx);
|
|
4883
|
+
const s = await api(ctx, `/v1/projects/${projectId}/neon-backend/status`);
|
|
4884
|
+
ok(s, () => {
|
|
4885
|
+
line(
|
|
4886
|
+
`Dedicated infrastructure: ${s.dedicated ? import_picocolors8.default.green("yes") : import_picocolors8.default.yellow("no")}`
|
|
4887
|
+
);
|
|
4888
|
+
line(
|
|
4889
|
+
`Region: ${s.regionId ? import_picocolors8.default.bold(s.regionId) : import_picocolors8.default.dim("unknown")}` + (s.regionId ? s.regionSupportsNeonBackend ? import_picocolors8.default.green(" (supported)") : import_picocolors8.default.red(" (Neon storage/functions unavailable here)") : "")
|
|
4890
|
+
);
|
|
4891
|
+
line(`Object storage: ${s.neonBackendStorageEnabled ? "on" : "off"}`);
|
|
4892
|
+
line(`Functions: ${s.neonBackendFunctionsEnabled ? "on" : "off"}`);
|
|
4893
|
+
if (s.regionId && !s.regionSupportsNeonBackend) {
|
|
4894
|
+
line(
|
|
4895
|
+
import_picocolors8.default.dim(
|
|
4896
|
+
`Supported regions: ${(s.supportedRegions ?? []).join(", ")}. A project's region is fixed at creation \u2014 this cannot be changed here.`
|
|
4897
|
+
)
|
|
4898
|
+
);
|
|
4899
|
+
}
|
|
4900
|
+
});
|
|
4901
|
+
})
|
|
4902
|
+
);
|
|
4903
|
+
const storage = neon.command("storage").description("S3-compatible buckets on the project's Neon branch");
|
|
4904
|
+
storage.command("list").description("List the project's Neon buckets").action(
|
|
4905
|
+
action(async ({ ctx }) => {
|
|
4906
|
+
const projectId = requireProject(ctx);
|
|
4907
|
+
const buckets = await api(
|
|
4908
|
+
ctx,
|
|
4909
|
+
`/v1/projects/${projectId}/neon-storage/buckets`
|
|
4910
|
+
);
|
|
4911
|
+
ok(buckets, () => {
|
|
4912
|
+
if (!buckets?.length)
|
|
4913
|
+
return line(import_picocolors8.default.dim("No buckets. `workser neon storage create <name>`."));
|
|
4914
|
+
for (const b of buckets)
|
|
4915
|
+
line(`${b.bucket_name}${import_picocolors8.default.dim(` (${b.access_level})`)}`);
|
|
4916
|
+
});
|
|
4917
|
+
})
|
|
4918
|
+
);
|
|
4919
|
+
storage.command("create <name>").description("Create a bucket on the project's Neon branch").option("--public", "Allow public reads (default: private)").action(
|
|
4920
|
+
action(async ({ ctx, args, opts }) => {
|
|
4921
|
+
const projectId = requireProject(ctx);
|
|
4922
|
+
const res = await api(
|
|
4923
|
+
ctx,
|
|
4924
|
+
`/v1/projects/${projectId}/neon-storage/buckets`,
|
|
4925
|
+
{
|
|
4926
|
+
body: {
|
|
4927
|
+
name: args[0],
|
|
4928
|
+
accessLevel: opts.public ? "public_read" : "private"
|
|
4929
|
+
}
|
|
4930
|
+
}
|
|
4931
|
+
);
|
|
4932
|
+
ok(res, () => success(`Created bucket ${import_picocolors8.default.bold(res.bucket_name ?? args[0])}.`));
|
|
4933
|
+
})
|
|
4934
|
+
);
|
|
4935
|
+
storage.command("rm <bucket>").description("Delete a bucket AND everything in it (asks for approval)").action(
|
|
4936
|
+
action(async ({ ctx, args }) => {
|
|
4937
|
+
const projectId = requireProject(ctx);
|
|
4938
|
+
const res = await api(
|
|
4939
|
+
ctx,
|
|
4940
|
+
`/v1/projects/${projectId}/neon-storage/buckets/${encodeURIComponent(args[0])}`,
|
|
4941
|
+
{ method: "DELETE" }
|
|
4942
|
+
);
|
|
4943
|
+
ok(res, () => success(`Deleted bucket ${args[0]}.`));
|
|
4944
|
+
})
|
|
4945
|
+
);
|
|
4946
|
+
storage.command("ls <bucket> [prefix]").description("List objects in a bucket").action(
|
|
4947
|
+
action(async ({ ctx, args }) => {
|
|
4948
|
+
const projectId = requireProject(ctx);
|
|
4949
|
+
const res = await api(
|
|
4950
|
+
ctx,
|
|
4951
|
+
`/v1/projects/${projectId}/neon-storage/buckets/${encodeURIComponent(args[0])}/objects`,
|
|
4952
|
+
{ query: { prefix: args[1] } }
|
|
4953
|
+
);
|
|
4954
|
+
const objects = res?.objects ?? res ?? [];
|
|
4955
|
+
ok(res, () => {
|
|
4956
|
+
if (!objects.length) return line(import_picocolors8.default.dim("Empty."));
|
|
4957
|
+
for (const o of objects)
|
|
4958
|
+
line(`${o.key ?? o.name}${o.size ? import_picocolors8.default.dim(` ${o.size} bytes`) : ""}`);
|
|
4959
|
+
});
|
|
4960
|
+
})
|
|
4961
|
+
);
|
|
4962
|
+
storage.command("put <bucket> <local> [key]").description("Upload a file (key defaults to the file's name)").action(
|
|
4963
|
+
action(async ({ ctx, args }) => {
|
|
4964
|
+
const projectId = requireProject(ctx);
|
|
4965
|
+
const [bucket, local] = args;
|
|
4966
|
+
const key = args[2] || basename2(local);
|
|
4967
|
+
const body = await readFile2(local).catch(() => {
|
|
4968
|
+
throw new WorkserError(`Can't read ${local}.`, { code: "not_found" });
|
|
4969
|
+
});
|
|
4970
|
+
const signed = await presign(ctx, projectId, bucket, key, "upload");
|
|
4971
|
+
const res = await fetch(signed.url, {
|
|
4972
|
+
method: "PUT",
|
|
4973
|
+
body,
|
|
4974
|
+
headers: signed.headers ?? {}
|
|
4975
|
+
});
|
|
4976
|
+
if (!res.ok) {
|
|
4977
|
+
throw new WorkserError(
|
|
4978
|
+
`Upload failed (${res.status} ${res.statusText}).`,
|
|
4979
|
+
{ code: "upload_failed", status: res.status }
|
|
4980
|
+
);
|
|
4981
|
+
}
|
|
4982
|
+
ok(
|
|
4983
|
+
{ bucket, key, bytes: body.length },
|
|
4984
|
+
() => success(`Uploaded ${key} to ${bucket} ${import_picocolors8.default.dim(`(${body.length} bytes)`)}.`)
|
|
4985
|
+
);
|
|
4986
|
+
})
|
|
4987
|
+
);
|
|
4988
|
+
storage.command("get <bucket> <key> [dest]").description("Download an object (dest defaults to the key's file name)").action(
|
|
4989
|
+
action(async ({ ctx, args }) => {
|
|
4990
|
+
const projectId = requireProject(ctx);
|
|
4991
|
+
const [bucket, key] = args;
|
|
4992
|
+
const dest = args[2] || basename2(key);
|
|
4993
|
+
const signed = await presign(ctx, projectId, bucket, key, "download");
|
|
4994
|
+
const res = await fetch(signed.url);
|
|
4995
|
+
if (!res.ok) {
|
|
4996
|
+
throw new WorkserError(
|
|
4997
|
+
`Download failed (${res.status} ${res.statusText}).`,
|
|
4998
|
+
{ code: "download_failed", status: res.status }
|
|
4999
|
+
);
|
|
5000
|
+
}
|
|
5001
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
5002
|
+
await writeFile2(dest, buf);
|
|
5003
|
+
ok(
|
|
5004
|
+
{ bucket, key, dest, bytes: buf.length },
|
|
5005
|
+
() => success(`Saved ${dest} ${import_picocolors8.default.dim(`(${buf.length} bytes)`)}.`)
|
|
5006
|
+
);
|
|
5007
|
+
})
|
|
5008
|
+
);
|
|
5009
|
+
storage.command("url <bucket> <key>").description("Print a temporary download URL for one object").option("--expires <seconds>", "Lifetime of the URL", "3600").action(
|
|
5010
|
+
action(async ({ ctx, args, opts }) => {
|
|
5011
|
+
const projectId = requireProject(ctx);
|
|
5012
|
+
const signed = await presign(
|
|
5013
|
+
ctx,
|
|
5014
|
+
projectId,
|
|
5015
|
+
args[0],
|
|
5016
|
+
args[1],
|
|
5017
|
+
"download",
|
|
5018
|
+
Number(opts.expires) || 3600
|
|
5019
|
+
);
|
|
5020
|
+
ok(signed, () => line(signed.url));
|
|
5021
|
+
})
|
|
5022
|
+
);
|
|
5023
|
+
storage.command("rm-object <bucket> <key>").description("Delete one object from a bucket (asks for approval)").action(
|
|
5024
|
+
action(async ({ ctx, args }) => {
|
|
5025
|
+
const projectId = requireProject(ctx);
|
|
5026
|
+
const res = await api(
|
|
5027
|
+
ctx,
|
|
5028
|
+
`/v1/projects/${projectId}/neon-storage/buckets/${encodeURIComponent(args[0])}/objects`,
|
|
5029
|
+
{ method: "DELETE", query: { key: args[1] } }
|
|
5030
|
+
);
|
|
5031
|
+
ok(res, () => success(`Deleted ${args[1]} from ${args[0]}.`));
|
|
5032
|
+
})
|
|
5033
|
+
);
|
|
5034
|
+
const functions = neon.command("functions").description("Node.js HTTP functions on the project's Neon branch");
|
|
5035
|
+
functions.command("list").description("List the project's Neon functions").action(
|
|
5036
|
+
action(async ({ ctx }) => {
|
|
5037
|
+
const projectId = requireProject(ctx);
|
|
5038
|
+
const fns = await api(ctx, `/v1/projects/${projectId}/neon-functions`);
|
|
5039
|
+
ok(fns, () => {
|
|
5040
|
+
if (!fns?.length)
|
|
5041
|
+
return line(import_picocolors8.default.dim("No functions. `workser neon functions deploy`."));
|
|
5042
|
+
for (const f of fns)
|
|
5043
|
+
line(`${f.slug ?? f.name}${f.url ? import_picocolors8.default.dim(` ${f.url}`) : ""}`);
|
|
5044
|
+
});
|
|
5045
|
+
})
|
|
5046
|
+
);
|
|
5047
|
+
functions.command("deploy <slug> <zip>").description("Deploy a function from a zip bundle").option(
|
|
5048
|
+
"--env <pairs...>",
|
|
5049
|
+
"Environment variables for the function (KEY=VALUE)"
|
|
5050
|
+
).option("--runtime <runtime>", "Runtime override").action(
|
|
5051
|
+
action(async ({ ctx, args, opts }) => {
|
|
5052
|
+
const projectId = requireProject(ctx);
|
|
5053
|
+
const [slug, zipPath] = args;
|
|
5054
|
+
const zip = await readFile2(zipPath).catch(() => {
|
|
5055
|
+
throw new WorkserError(`Can't read ${zipPath}.`, { code: "not_found" });
|
|
5056
|
+
});
|
|
5057
|
+
const environment = {};
|
|
5058
|
+
for (const pair of opts.env ?? []) {
|
|
5059
|
+
const eq = String(pair).indexOf("=");
|
|
5060
|
+
if (eq <= 0) {
|
|
5061
|
+
throw new WorkserError(
|
|
5062
|
+
`--env expects KEY=VALUE, got "${pair}".`,
|
|
5063
|
+
{ code: "bad_request" }
|
|
5064
|
+
);
|
|
5065
|
+
}
|
|
5066
|
+
environment[String(pair).slice(0, eq)] = String(pair).slice(eq + 1);
|
|
5067
|
+
}
|
|
5068
|
+
const res = await api(ctx, `/v1/projects/${projectId}/neon-functions`, {
|
|
5069
|
+
body: {
|
|
5070
|
+
slug,
|
|
5071
|
+
// JSON rather than multipart: the caller is an agent shelling out,
|
|
5072
|
+
// and base64 in a JSON body is the shape it can produce unaided.
|
|
5073
|
+
zipBase64: zip.toString("base64"),
|
|
5074
|
+
zipFilename: basename2(zipPath),
|
|
5075
|
+
runtime: opts.runtime,
|
|
5076
|
+
environment: Object.keys(environment).length ? environment : void 0
|
|
5077
|
+
}
|
|
5078
|
+
});
|
|
5079
|
+
ok(
|
|
5080
|
+
res,
|
|
5081
|
+
() => success(
|
|
5082
|
+
`Deployed ${import_picocolors8.default.bold(slug)}${res?.url ? import_picocolors8.default.dim(` ${res.url}`) : ""}.`
|
|
5083
|
+
)
|
|
5084
|
+
);
|
|
5085
|
+
})
|
|
5086
|
+
);
|
|
5087
|
+
functions.command("rm <slug>").description("Delete a function (asks for approval)").action(
|
|
5088
|
+
action(async ({ ctx, args }) => {
|
|
5089
|
+
const projectId = requireProject(ctx);
|
|
5090
|
+
const res = await api(
|
|
5091
|
+
ctx,
|
|
5092
|
+
`/v1/projects/${projectId}/neon-functions/${encodeURIComponent(args[0])}`,
|
|
5093
|
+
{ method: "DELETE" }
|
|
5094
|
+
);
|
|
5095
|
+
ok(res, () => success(`Deleted function ${args[0]}.`));
|
|
5096
|
+
})
|
|
5097
|
+
);
|
|
5098
|
+
}
|
|
5099
|
+
async function presign(ctx, projectId, bucket, key, operation, expiresInSeconds) {
|
|
5100
|
+
const res = await api(
|
|
5101
|
+
ctx,
|
|
5102
|
+
`/v1/projects/${projectId}/neon-storage/buckets/${encodeURIComponent(bucket)}/presign`,
|
|
5103
|
+
{ body: { key, operation, expiresInSeconds } }
|
|
5104
|
+
);
|
|
5105
|
+
const url = res?.url ?? res?.signedUrl ?? res?.presignedUrl;
|
|
5106
|
+
if (!url) {
|
|
5107
|
+
throw new WorkserError(
|
|
5108
|
+
`The daemon did not return a presigned URL for ${key}.`,
|
|
5109
|
+
{ code: "unexpected_response", details: res }
|
|
5110
|
+
);
|
|
5111
|
+
}
|
|
5112
|
+
return { url, headers: res?.headers };
|
|
5113
|
+
}
|
|
5114
|
+
|
|
4197
5115
|
// src/commands/env.ts
|
|
4198
|
-
var
|
|
5116
|
+
var import_picocolors9 = __toESM(require_picocolors(), 1);
|
|
4199
5117
|
function appQuery(opts) {
|
|
4200
5118
|
const app = typeof opts?.app === "string" ? opts.app : "";
|
|
4201
5119
|
return app ? `?webAppId=${encodeURIComponent(app)}` : "";
|
|
@@ -4218,7 +5136,7 @@ function registerEnv(program3) {
|
|
|
4218
5136
|
ok(res, () => {
|
|
4219
5137
|
success(`Set ${count} variable(s): ${pairs.map((p) => p.key).join(", ")}`);
|
|
4220
5138
|
if (res?.usedDefault && res?.webAppName) {
|
|
4221
|
-
line(
|
|
5139
|
+
line(import_picocolors9.default.dim(`on ${res.webAppName} (primary app) \u2014 use --app to target another`));
|
|
4222
5140
|
}
|
|
4223
5141
|
});
|
|
4224
5142
|
})
|
|
@@ -4238,8 +5156,8 @@ function registerEnv(program3) {
|
|
|
4238
5156
|
const projectId = requireProject(ctx);
|
|
4239
5157
|
const items = await api(ctx, `/v1/projects/${projectId}/env${appQuery(opts)}`);
|
|
4240
5158
|
ok(items, () => {
|
|
4241
|
-
if (!items?.length) return line(
|
|
4242
|
-
for (const v of items) line(`${v.key}${
|
|
5159
|
+
if (!items?.length) return line(import_picocolors9.default.dim("No variables set."));
|
|
5160
|
+
for (const v of items) line(`${v.key}${import_picocolors9.default.dim(" = " + (v.masked ?? "\u2022\u2022\u2022\u2022"))}`);
|
|
4243
5161
|
});
|
|
4244
5162
|
})
|
|
4245
5163
|
);
|
|
@@ -4255,14 +5173,21 @@ function registerEnv(program3) {
|
|
|
4255
5173
|
}
|
|
4256
5174
|
|
|
4257
5175
|
// src/commands/deploy.ts
|
|
4258
|
-
var
|
|
5176
|
+
var import_picocolors10 = __toESM(require_picocolors(), 1);
|
|
4259
5177
|
var TERMINAL = /* @__PURE__ */ new Set(["ready", "live", "success", "error", "failed", "canceled"]);
|
|
4260
5178
|
function registerDeploy(program3) {
|
|
4261
|
-
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", false).option("--watch", "wait for the deploy to finish, streaming status", false).
|
|
5179
|
+
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", false).option("--watch", "wait for the deploy to finish, streaming status", false).option(
|
|
5180
|
+
"--app <webAppId>",
|
|
5181
|
+
"which app to publish (default: the app this folder is linked to)"
|
|
5182
|
+
).action(
|
|
4262
5183
|
action(async ({ ctx, opts }) => {
|
|
4263
5184
|
const projectId = requireProject(ctx);
|
|
4264
5185
|
const dep = await api(ctx, `/v1/projects/${projectId}/deploy`, {
|
|
4265
|
-
body: {
|
|
5186
|
+
body: {
|
|
5187
|
+
prod: Boolean(opts.prod),
|
|
5188
|
+
cwd: ctx.cwd,
|
|
5189
|
+
...opts.app ? { webAppId: opts.app } : {}
|
|
5190
|
+
}
|
|
4266
5191
|
});
|
|
4267
5192
|
if (opts.watch && dep?.id) {
|
|
4268
5193
|
const final = await watchDeploy(ctx, dep.id);
|
|
@@ -4284,7 +5209,7 @@ async function watchDeploy(ctx, id) {
|
|
|
4284
5209
|
for (; ; ) {
|
|
4285
5210
|
const dep = await api(ctx, `/v1/deployments/${encodeURIComponent(id)}`);
|
|
4286
5211
|
if (!isJson() && dep.status !== last) {
|
|
4287
|
-
line(` ${colorStatus(dep.status)}${dep.url ? " " +
|
|
5212
|
+
line(` ${colorStatus(dep.status)}${dep.url ? " " + import_picocolors10.default.cyan(dep.url) : ""}`);
|
|
4288
5213
|
last = dep.status;
|
|
4289
5214
|
}
|
|
4290
5215
|
if (TERMINAL.has(String(dep.status).toLowerCase())) return dep;
|
|
@@ -4294,19 +5219,19 @@ async function watchDeploy(ctx, id) {
|
|
|
4294
5219
|
function printDeploy(dep) {
|
|
4295
5220
|
if (!dep) return;
|
|
4296
5221
|
const ready = ["ready", "live", "success"].includes(String(dep.status).toLowerCase());
|
|
4297
|
-
if (ready && dep.url) success(`Live at ${
|
|
4298
|
-
else line(`deploy ${colorStatus(dep.status)} ${
|
|
5222
|
+
if (ready && dep.url) success(`Live at ${import_picocolors10.default.cyan(dep.url)}`);
|
|
5223
|
+
else line(`deploy ${colorStatus(dep.status)} ${import_picocolors10.default.dim(`(${dep.id ?? "?"})`)}${dep.url ? " " + dep.url : ""}`);
|
|
4299
5224
|
}
|
|
4300
5225
|
|
|
4301
5226
|
// src/commands/versions.ts
|
|
4302
|
-
var
|
|
5227
|
+
var import_picocolors11 = __toESM(require_picocolors(), 1);
|
|
4303
5228
|
function registerVersions(program3) {
|
|
4304
5229
|
program3.command("versions").description("List the Workser-managed versions of the project (deploy history)").action(
|
|
4305
5230
|
action(async ({ ctx }) => {
|
|
4306
5231
|
const projectId = requireProject(ctx);
|
|
4307
5232
|
const items = await api(ctx, `/v1/projects/${projectId}/versions`);
|
|
4308
5233
|
ok(items, () => {
|
|
4309
|
-
if (!items?.length) return line(
|
|
5234
|
+
if (!items?.length) return line(import_picocolors11.default.dim("No versions yet. `workser deploy` to create one."));
|
|
4310
5235
|
for (const v of items) {
|
|
4311
5236
|
line(formatVersion(v));
|
|
4312
5237
|
}
|
|
@@ -4315,11 +5240,11 @@ function registerVersions(program3) {
|
|
|
4315
5240
|
);
|
|
4316
5241
|
}
|
|
4317
5242
|
function formatVersion(v) {
|
|
4318
|
-
const ref =
|
|
4319
|
-
const when =
|
|
4320
|
-
const msg = (v.message ?? "").trim() ||
|
|
4321
|
-
const badge = v.deployed ? " " +
|
|
4322
|
-
const url = v.url ? " " +
|
|
5243
|
+
const ref = import_picocolors11.default.yellow(shortRef(v.ref));
|
|
5244
|
+
const when = import_picocolors11.default.dim(formatTime(v.createdAt));
|
|
5245
|
+
const msg = (v.message ?? "").trim() || import_picocolors11.default.dim("(no message)");
|
|
5246
|
+
const badge = v.deployed ? " " + import_picocolors11.default.green("deployed") : "";
|
|
5247
|
+
const url = v.url ? " " + import_picocolors11.default.cyan(v.url) : "";
|
|
4323
5248
|
return `${ref} ${when} ${msg}${badge}${url}`;
|
|
4324
5249
|
}
|
|
4325
5250
|
function shortRef(ref) {
|
|
@@ -4359,7 +5284,7 @@ function formatLog(e) {
|
|
|
4359
5284
|
}
|
|
4360
5285
|
|
|
4361
5286
|
// src/commands/domain.ts
|
|
4362
|
-
var
|
|
5287
|
+
var import_picocolors12 = __toESM(require_picocolors(), 1);
|
|
4363
5288
|
function registerDomain(program3) {
|
|
4364
5289
|
const domain = program3.command("domain").description("Inspect the project's custom domains");
|
|
4365
5290
|
domain.command("set <domain>").description("(owner-only) Attach a custom domain \u2014 do this in Workser Orbit").action(
|
|
@@ -4376,8 +5301,8 @@ function registerDomain(program3) {
|
|
|
4376
5301
|
const projectId = requireProject(ctx);
|
|
4377
5302
|
const items = await api(ctx, `/v1/projects/${projectId}/domains`);
|
|
4378
5303
|
ok(items, () => {
|
|
4379
|
-
if (!items?.length) return line(
|
|
4380
|
-
for (const d of items) line(`${d.domain}${
|
|
5304
|
+
if (!items?.length) return line(import_picocolors12.default.dim("No custom domains."));
|
|
5305
|
+
for (const d of items) line(`${d.domain}${import_picocolors12.default.dim(" " + (d.status ?? ""))}`);
|
|
4381
5306
|
});
|
|
4382
5307
|
})
|
|
4383
5308
|
);
|
|
@@ -4409,7 +5334,7 @@ function openUrl(url) {
|
|
|
4409
5334
|
}
|
|
4410
5335
|
|
|
4411
5336
|
// src/commands/doctor.ts
|
|
4412
|
-
var
|
|
5337
|
+
var import_picocolors13 = __toESM(require_picocolors(), 1);
|
|
4413
5338
|
function registerDoctor(program3) {
|
|
4414
5339
|
program3.command("doctor").description("Print the resolved endpoint, mode, token presence (masked), and current project").action(
|
|
4415
5340
|
action(({ ctx, opts }) => {
|
|
@@ -4436,14 +5361,14 @@ function registerDoctor(program3) {
|
|
|
4436
5361
|
workspace: session.workspaceName ?? null
|
|
4437
5362
|
};
|
|
4438
5363
|
ok(report, () => {
|
|
4439
|
-
line(
|
|
4440
|
-
line(` endpoint: ${ctx.endpoint} ${
|
|
5364
|
+
line(import_picocolors13.default.bold("workser doctor"));
|
|
5365
|
+
line(` endpoint: ${ctx.endpoint} ${import_picocolors13.default.dim(`(${endpointSource})`)}`);
|
|
4441
5366
|
line(` mode: ${ctx.mode}`);
|
|
4442
5367
|
line(
|
|
4443
|
-
` token: ${ctx.token ? `${maskToken(ctx.token)} ${
|
|
5368
|
+
` token: ${ctx.token ? `${maskToken(ctx.token)} ${import_picocolors13.default.dim(`(${tokenSource})`)}` : import_picocolors13.default.yellow("none \u2014 run `workser login`")}`
|
|
4444
5369
|
);
|
|
4445
5370
|
line(
|
|
4446
|
-
` project: ${ctx.projectId ??
|
|
5371
|
+
` project: ${ctx.projectId ?? import_picocolors13.default.dim("none")}` + (link?.name ? ` ${import_picocolors13.default.dim(`(${link.name})`)}` : "") + (projectSource ? import_picocolors13.default.dim(` [${projectSource}]`) : "")
|
|
4447
5372
|
);
|
|
4448
5373
|
line(` cwd: ${ctx.cwd}`);
|
|
4449
5374
|
});
|
|
@@ -4456,25 +5381,25 @@ function maskToken(token) {
|
|
|
4456
5381
|
}
|
|
4457
5382
|
|
|
4458
5383
|
// src/commands/agent.ts
|
|
4459
|
-
var
|
|
5384
|
+
var import_picocolors14 = __toESM(require_picocolors(), 1);
|
|
4460
5385
|
function registerAgent(program3) {
|
|
4461
5386
|
const agent = program3.command("agent").description("Delegate focused subtasks to your configured agent roles (each runs isolated)");
|
|
4462
5387
|
agent.command("list").description("List the main agent (+ backup) and the configured subagents").action(
|
|
4463
5388
|
action(async ({ ctx }) => {
|
|
4464
5389
|
const cfg = await api(ctx, "/v1/agents");
|
|
4465
5390
|
ok(cfg, () => {
|
|
4466
|
-
line(
|
|
4467
|
-
line(
|
|
5391
|
+
line(import_picocolors14.default.bold("main agent:") + " " + (cfg?.mainAgent ?? import_picocolors14.default.dim("none")));
|
|
5392
|
+
line(import_picocolors14.default.bold("backup agent:") + " " + (cfg?.backupAgent ?? import_picocolors14.default.dim("none")));
|
|
4468
5393
|
if (cfg?.effectiveMainAgent && cfg.effectiveMainAgent !== cfg.mainAgent) {
|
|
4469
5394
|
line(
|
|
4470
|
-
|
|
5395
|
+
import_picocolors14.default.yellow(
|
|
4471
5396
|
` \u2937 failover active: runs use ${cfg.effectiveMainAgent} (main not available)`
|
|
4472
5397
|
)
|
|
4473
5398
|
);
|
|
4474
5399
|
}
|
|
4475
5400
|
const roles = cfg?.roles ?? [];
|
|
4476
|
-
if (!roles.length) return line(
|
|
4477
|
-
line(
|
|
5401
|
+
if (!roles.length) return line(import_picocolors14.default.dim("No subagents configured. Add them in the Workser Orbit Agents screen."));
|
|
5402
|
+
line(import_picocolors14.default.bold("subagents:"));
|
|
4478
5403
|
for (const r of roles) line(" " + formatRole(r));
|
|
4479
5404
|
});
|
|
4480
5405
|
})
|
|
@@ -4488,8 +5413,8 @@ function registerAgent(program3) {
|
|
|
4488
5413
|
backupAgent: cfg?.backupAgent ?? null
|
|
4489
5414
|
},
|
|
4490
5415
|
() => {
|
|
4491
|
-
line(
|
|
4492
|
-
line(
|
|
5416
|
+
line(import_picocolors14.default.bold("main agent:") + " " + (cfg?.mainAgent ?? import_picocolors14.default.dim("none")));
|
|
5417
|
+
line(import_picocolors14.default.bold("backup agent:") + " " + (cfg?.backupAgent ?? import_picocolors14.default.dim("none")));
|
|
4493
5418
|
}
|
|
4494
5419
|
);
|
|
4495
5420
|
})
|
|
@@ -4511,21 +5436,21 @@ function registerAgent(program3) {
|
|
|
4511
5436
|
);
|
|
4512
5437
|
}
|
|
4513
5438
|
function formatRole(r) {
|
|
4514
|
-
const label =
|
|
4515
|
-
const agent =
|
|
4516
|
-
const enabled = r.enabled === false ?
|
|
5439
|
+
const label = import_picocolors14.default.yellow(r.role);
|
|
5440
|
+
const agent = import_picocolors14.default.dim("\xB7 " + (r.agent ?? "?"));
|
|
5441
|
+
const enabled = r.enabled === false ? import_picocolors14.default.red("disabled") : import_picocolors14.default.green("enabled");
|
|
4517
5442
|
const runnable = r.installed && r.authed !== false;
|
|
4518
|
-
const ready = runnable ?
|
|
5443
|
+
const ready = runnable ? import_picocolors14.default.green("runnable") : import_picocolors14.default.dim("not runnable");
|
|
4519
5444
|
const extras = [];
|
|
4520
5445
|
if (r.model) extras.push(`model ${r.model}`);
|
|
4521
5446
|
if (Array.isArray(r.apps) && r.apps.length) extras.push(`apps: ${r.apps.join(",")}`);
|
|
4522
5447
|
if (Array.isArray(r.mcp) && r.mcp.length) extras.push(`mcp: ${r.mcp.length}`);
|
|
4523
|
-
const tail = extras.length ? " " +
|
|
5448
|
+
const tail = extras.length ? " " + import_picocolors14.default.dim(extras.join(" \xB7 ")) : "";
|
|
4524
5449
|
return `${label} ${agent} ${enabled} ${ready}${tail}`;
|
|
4525
5450
|
}
|
|
4526
5451
|
|
|
4527
5452
|
// src/commands/verify.ts
|
|
4528
|
-
var
|
|
5453
|
+
var import_picocolors15 = __toESM(require_picocolors(), 1);
|
|
4529
5454
|
function registerVerify(program3) {
|
|
4530
5455
|
program3.command("verify").description(
|
|
4531
5456
|
"Run the project's checks (typecheck/lint/build) \u2014 use before declaring a task done"
|
|
@@ -4546,23 +5471,23 @@ function registerVerify(program3) {
|
|
|
4546
5471
|
function printVerify(res) {
|
|
4547
5472
|
if (!res) return;
|
|
4548
5473
|
if (!res.checks?.length) {
|
|
4549
|
-
line(
|
|
5474
|
+
line(import_picocolors15.default.dim(res.note ?? "No checks detected."));
|
|
4550
5475
|
return;
|
|
4551
5476
|
}
|
|
4552
5477
|
for (const c of res.checks) {
|
|
4553
5478
|
line(
|
|
4554
|
-
` ${c.ok ?
|
|
5479
|
+
` ${c.ok ? import_picocolors15.default.green("\u2713") : import_picocolors15.default.red("\u2717")} ${c.name}${c.ok ? "" : import_picocolors15.default.dim(` (exit ${c.exitCode})`)}`
|
|
4555
5480
|
);
|
|
4556
5481
|
}
|
|
4557
5482
|
if (res.ok) success("All checks passed");
|
|
4558
5483
|
else
|
|
4559
5484
|
line(
|
|
4560
|
-
|
|
5485
|
+
import_picocolors15.default.red("Some checks failed \u2014 fix the errors above and re-run ") + import_picocolors15.default.bold("workser verify") + import_picocolors15.default.red(".")
|
|
4561
5486
|
);
|
|
4562
5487
|
}
|
|
4563
5488
|
|
|
4564
5489
|
// src/commands/workflow.ts
|
|
4565
|
-
var
|
|
5490
|
+
var import_picocolors16 = __toESM(require_picocolors(), 1);
|
|
4566
5491
|
function registerWorkflow(program3) {
|
|
4567
5492
|
const wf = program3.command("workflow").description("Create, run, and inspect workflow automations for the project");
|
|
4568
5493
|
wf.command("list").description("List the project's workflows").action(
|
|
@@ -4570,10 +5495,10 @@ function registerWorkflow(program3) {
|
|
|
4570
5495
|
const projectId = requireProject(ctx);
|
|
4571
5496
|
const items = await api(ctx, `/v1/projects/${projectId}/workflows`);
|
|
4572
5497
|
ok(items, () => {
|
|
4573
|
-
if (!items?.length) return line(
|
|
5498
|
+
if (!items?.length) return line(import_picocolors16.default.dim("No workflows yet. `workser workflow create`."));
|
|
4574
5499
|
for (const w of items) {
|
|
4575
|
-
const status = w.is_active ?
|
|
4576
|
-
line(`${w.id} ${
|
|
5500
|
+
const status = w.is_active ? import_picocolors16.default.green("active") : import_picocolors16.default.dim("inactive");
|
|
5501
|
+
line(`${w.id} ${import_picocolors16.default.bold(w.name ?? "Untitled")} ${status}`);
|
|
4577
5502
|
}
|
|
4578
5503
|
});
|
|
4579
5504
|
})
|
|
@@ -4585,7 +5510,7 @@ function registerWorkflow(program3) {
|
|
|
4585
5510
|
const res = await api(ctx, `/v1/projects/${projectId}/workflows`, {
|
|
4586
5511
|
body: { name: args[0], ...extra }
|
|
4587
5512
|
});
|
|
4588
|
-
ok(res, () => line(`Created workflow ${
|
|
5513
|
+
ok(res, () => line(`Created workflow ${import_picocolors16.default.bold(res.id)}.`));
|
|
4589
5514
|
})
|
|
4590
5515
|
);
|
|
4591
5516
|
wf.command("get <id>").description("Show a workflow's full definition").action(
|
|
@@ -4620,8 +5545,8 @@ function registerWorkflow(program3) {
|
|
|
4620
5545
|
action(async ({ ctx, args }) => {
|
|
4621
5546
|
const items = await api(ctx, `/v1/workflows/${args[0]}/executions`);
|
|
4622
5547
|
ok(items, () => {
|
|
4623
|
-
if (!items?.length) return line(
|
|
4624
|
-
for (const e of items) line(`${e.id} ${e.status ?? ""} ${
|
|
5548
|
+
if (!items?.length) return line(import_picocolors16.default.dim("No runs yet."));
|
|
5549
|
+
for (const e of items) line(`${e.id} ${e.status ?? ""} ${import_picocolors16.default.dim(e.started_at ?? "")}`);
|
|
4625
5550
|
});
|
|
4626
5551
|
})
|
|
4627
5552
|
);
|
|
@@ -4629,15 +5554,15 @@ function registerWorkflow(program3) {
|
|
|
4629
5554
|
action(async ({ ctx, args }) => {
|
|
4630
5555
|
const items = await api(ctx, `/v1/node-types`, { query: { q: args[0] } });
|
|
4631
5556
|
ok(items, () => {
|
|
4632
|
-
if (!items?.length) return line(
|
|
4633
|
-
for (const n of items) line(`${n.name ?? n.type} ${
|
|
5557
|
+
if (!items?.length) return line(import_picocolors16.default.dim("No matching node types."));
|
|
5558
|
+
for (const n of items) line(`${n.name ?? n.type} ${import_picocolors16.default.dim(n.category ?? "")}`);
|
|
4634
5559
|
});
|
|
4635
5560
|
})
|
|
4636
5561
|
);
|
|
4637
5562
|
}
|
|
4638
5563
|
|
|
4639
5564
|
// src/commands/app.ts
|
|
4640
|
-
var
|
|
5565
|
+
var import_picocolors17 = __toESM(require_picocolors(), 1);
|
|
4641
5566
|
function registerApp(program3) {
|
|
4642
5567
|
const appCmd = program3.command("app").description("Connect and use third-party app integrations (Gmail, Slack, Stripe, ...)");
|
|
4643
5568
|
appCmd.command("list").description("List connectable toolkits and this project's existing connections").option("--toolkit <slug>", "filter connections to one toolkit").action(
|
|
@@ -4650,8 +5575,8 @@ function registerApp(program3) {
|
|
|
4650
5575
|
ok({ catalog, connections }, () => {
|
|
4651
5576
|
const connected = new Set((connections ?? []).map((c) => c.toolkit ?? c.composio_app));
|
|
4652
5577
|
for (const t of catalog ?? []) {
|
|
4653
|
-
const status = connected.has(t.slug) ?
|
|
4654
|
-
line(`${t.slug} ${
|
|
5578
|
+
const status = connected.has(t.slug) ? import_picocolors17.default.green("connected") : import_picocolors17.default.dim("not connected");
|
|
5579
|
+
line(`${t.slug} ${import_picocolors17.default.bold(t.name ?? t.slug)} ${status}`);
|
|
4655
5580
|
}
|
|
4656
5581
|
});
|
|
4657
5582
|
})
|
|
@@ -4668,7 +5593,7 @@ function registerApp(program3) {
|
|
|
4668
5593
|
});
|
|
4669
5594
|
ok(
|
|
4670
5595
|
res,
|
|
4671
|
-
() => res.oauth_url ? line(`Open this URL to finish connecting: ${
|
|
5596
|
+
() => res.oauth_url ? line(`Open this URL to finish connecting: ${import_picocolors17.default.underline(res.oauth_url)}`) : line(`Connection ${res.connection_id} is ${res.status}.`)
|
|
4672
5597
|
);
|
|
4673
5598
|
})
|
|
4674
5599
|
);
|
|
@@ -4686,8 +5611,8 @@ function registerApp(program3) {
|
|
|
4686
5611
|
const projectId = requireProject(ctx);
|
|
4687
5612
|
const items = await api(ctx, `/v1/projects/${projectId}/integrations/${args[0]}/tools`);
|
|
4688
5613
|
ok(items, () => {
|
|
4689
|
-
if (!items?.length) return line(
|
|
4690
|
-
for (const t of items) line(`${t.slug} ${
|
|
5614
|
+
if (!items?.length) return line(import_picocolors17.default.dim("No tools found."));
|
|
5615
|
+
for (const t of items) line(`${t.slug} ${import_picocolors17.default.dim(t.description ?? "")}`);
|
|
4691
5616
|
});
|
|
4692
5617
|
})
|
|
4693
5618
|
);
|
|
@@ -4703,7 +5628,7 @@ function registerApp(program3) {
|
|
|
4703
5628
|
}
|
|
4704
5629
|
|
|
4705
5630
|
// src/commands/tool.ts
|
|
4706
|
-
var
|
|
5631
|
+
var import_picocolors18 = __toESM(require_picocolors(), 1);
|
|
4707
5632
|
function registerTool(program3) {
|
|
4708
5633
|
const tool = program3.command("tool").description(
|
|
4709
5634
|
"Computer-use tools: filesystem, shell, screenshot, input control, clipboard, notifications, basic browser"
|
|
@@ -4712,7 +5637,7 @@ function registerTool(program3) {
|
|
|
4712
5637
|
action(async ({ ctx }) => {
|
|
4713
5638
|
const tools = await api(ctx, "/v1/tool/list");
|
|
4714
5639
|
ok(tools, () => {
|
|
4715
|
-
if (!tools?.length) return line(
|
|
5640
|
+
if (!tools?.length) return line(import_picocolors18.default.dim("No tools available."));
|
|
4716
5641
|
const byCategory = /* @__PURE__ */ new Map();
|
|
4717
5642
|
for (const t of tools) {
|
|
4718
5643
|
const list = byCategory.get(t.category) ?? [];
|
|
@@ -4720,9 +5645,9 @@ function registerTool(program3) {
|
|
|
4720
5645
|
byCategory.set(t.category, list);
|
|
4721
5646
|
}
|
|
4722
5647
|
for (const [category, items] of byCategory) {
|
|
4723
|
-
line(
|
|
5648
|
+
line(import_picocolors18.default.bold(category) + ":");
|
|
4724
5649
|
for (const t of items) {
|
|
4725
|
-
line(` ${t.name} ${
|
|
5650
|
+
line(` ${t.name} ${import_picocolors18.default.dim(t.description ?? "")}`);
|
|
4726
5651
|
}
|
|
4727
5652
|
}
|
|
4728
5653
|
});
|
|
@@ -4740,7 +5665,7 @@ function registerTool(program3) {
|
|
|
4740
5665
|
}
|
|
4741
5666
|
|
|
4742
5667
|
// src/commands/memory.ts
|
|
4743
|
-
var
|
|
5668
|
+
var import_picocolors19 = __toESM(require_picocolors(), 1);
|
|
4744
5669
|
function registerMemory(program3) {
|
|
4745
5670
|
const memory = program3.command("memory").description("Durable, cross-conversation project memory (shared with cloud agents on the same project)");
|
|
4746
5671
|
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(
|
|
@@ -4764,9 +5689,9 @@ function registerMemory(program3) {
|
|
|
4764
5689
|
});
|
|
4765
5690
|
ok(res, () => {
|
|
4766
5691
|
const results = res?.results ?? res ?? [];
|
|
4767
|
-
if (!results?.length) return line(
|
|
5692
|
+
if (!results?.length) return line(import_picocolors19.default.dim("No matching memories."));
|
|
4768
5693
|
for (const r of results) {
|
|
4769
|
-
line(`${
|
|
5694
|
+
line(`${import_picocolors19.default.dim(r.id ?? "?")} ${r.memory ?? r.content ?? ""}`);
|
|
4770
5695
|
}
|
|
4771
5696
|
});
|
|
4772
5697
|
})
|
|
@@ -4783,7 +5708,7 @@ function registerMemory(program3) {
|
|
|
4783
5708
|
}
|
|
4784
5709
|
|
|
4785
5710
|
// src/commands/business.ts
|
|
4786
|
-
var
|
|
5711
|
+
var import_picocolors20 = __toESM(require_picocolors(), 1);
|
|
4787
5712
|
var RESOURCE_PATHS = {
|
|
4788
5713
|
"business-config": "business-config",
|
|
4789
5714
|
"business-settings": "business-settings",
|
|
@@ -4843,7 +5768,7 @@ function registerBusiness(program3) {
|
|
|
4843
5768
|
const projectId = requireProject(ctx);
|
|
4844
5769
|
const [resource] = args;
|
|
4845
5770
|
const res = await api(ctx, businessPath(projectId, resource), { body: JSON.parse(opts.body) });
|
|
4846
|
-
ok(res, () => line(`Created ${resource} ${
|
|
5771
|
+
ok(res, () => line(`Created ${resource} ${import_picocolors20.default.bold(res?.id ?? "")}.`));
|
|
4847
5772
|
})
|
|
4848
5773
|
);
|
|
4849
5774
|
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(
|
|
@@ -4885,9 +5810,9 @@ function businessPath(projectId, resource, subpath) {
|
|
|
4885
5810
|
}
|
|
4886
5811
|
|
|
4887
5812
|
// src/commands/artifact.ts
|
|
4888
|
-
var
|
|
5813
|
+
var import_picocolors21 = __toESM(require_picocolors(), 1);
|
|
4889
5814
|
import { existsSync as existsSync2, statSync } from "fs";
|
|
4890
|
-
import { resolve as resolve2, basename as
|
|
5815
|
+
import { resolve as resolve2, basename as basename3 } from "path";
|
|
4891
5816
|
var KINDS = [
|
|
4892
5817
|
"file",
|
|
4893
5818
|
"folder",
|
|
@@ -4944,14 +5869,14 @@ function registerArtifact(program3) {
|
|
|
4944
5869
|
path: absPath,
|
|
4945
5870
|
url,
|
|
4946
5871
|
kind,
|
|
4947
|
-
title: opts.title || (absPath ?
|
|
5872
|
+
title: opts.title || (absPath ? basename3(absPath) : url),
|
|
4948
5873
|
description: opts.description
|
|
4949
5874
|
}
|
|
4950
5875
|
});
|
|
4951
5876
|
ok(
|
|
4952
5877
|
res,
|
|
4953
5878
|
() => success(
|
|
4954
|
-
`Recorded ${
|
|
5879
|
+
`Recorded ${import_picocolors21.default.bold(res?.title ?? "artifact")}${res?.kind ? import_picocolors21.default.dim(` (${res.kind})`) : ""}`
|
|
4955
5880
|
)
|
|
4956
5881
|
);
|
|
4957
5882
|
})
|
|
@@ -4965,15 +5890,85 @@ function registerArtifact(program3) {
|
|
|
4965
5890
|
}
|
|
4966
5891
|
function printRun(run) {
|
|
4967
5892
|
if (!run) return;
|
|
4968
|
-
line(` run ${
|
|
5893
|
+
line(` run ${import_picocolors21.default.bold(run.runId)}`);
|
|
4969
5894
|
if (run.taskId) line(` task ${run.taskId}`);
|
|
4970
5895
|
if (run.conversationId) line(` chat ${run.conversationId}`);
|
|
4971
5896
|
if (run.projectId) line(` project ${run.projectId}`);
|
|
4972
|
-
if (run.cwd) line(` folder ${
|
|
5897
|
+
if (run.cwd) line(` folder ${import_picocolors21.default.dim(run.cwd)}`);
|
|
5898
|
+
}
|
|
5899
|
+
|
|
5900
|
+
// src/commands/image.ts
|
|
5901
|
+
import { writeFile as writeFile3, mkdir } from "fs/promises";
|
|
5902
|
+
import { dirname as dirname2, resolve as resolve3 } from "path";
|
|
5903
|
+
function registerImage(program3) {
|
|
5904
|
+
const image = program3.command("image").description("Generate images from a text prompt");
|
|
5905
|
+
image.command("generate <prompt>").alias("gen").description("Generate an image and return its public URL").option(
|
|
5906
|
+
"-r, --reference <url...>",
|
|
5907
|
+
"condition on existing image URLs (image-to-image); up to 4"
|
|
5908
|
+
).option(
|
|
5909
|
+
"-o, --output <path>",
|
|
5910
|
+
"also download the first image to this local path"
|
|
5911
|
+
).action(
|
|
5912
|
+
action(async ({ ctx, opts, args }) => {
|
|
5913
|
+
const projectId = requireProject(ctx);
|
|
5914
|
+
const prompt = String(args[0] ?? "").trim();
|
|
5915
|
+
if (!prompt) {
|
|
5916
|
+
throw new WorkserError("A prompt is required.", {
|
|
5917
|
+
code: "bad_request"
|
|
5918
|
+
});
|
|
5919
|
+
}
|
|
5920
|
+
const references = opts.reference?.filter(
|
|
5921
|
+
Boolean
|
|
5922
|
+
);
|
|
5923
|
+
const res = await api(
|
|
5924
|
+
ctx,
|
|
5925
|
+
`/projects/${projectId}/images/generate`,
|
|
5926
|
+
{
|
|
5927
|
+
method: "POST",
|
|
5928
|
+
body: {
|
|
5929
|
+
prompt,
|
|
5930
|
+
...references?.length ? { referenceImageUrls: references.slice(0, 4) } : {}
|
|
5931
|
+
}
|
|
5932
|
+
}
|
|
5933
|
+
);
|
|
5934
|
+
const images = res.images ?? [];
|
|
5935
|
+
if (!images.length) {
|
|
5936
|
+
const said = res.texts?.join(" ").trim();
|
|
5937
|
+
throw new WorkserError(
|
|
5938
|
+
said ? `No image was generated. The model said: ${said}` : "No image was generated.",
|
|
5939
|
+
{ code: "no_image" }
|
|
5940
|
+
);
|
|
5941
|
+
}
|
|
5942
|
+
let savedTo;
|
|
5943
|
+
if (opts.output) {
|
|
5944
|
+
savedTo = await download(images[0].publicUrl, String(opts.output));
|
|
5945
|
+
}
|
|
5946
|
+
ok({ images, texts: res.texts, savedTo }, () => {
|
|
5947
|
+
for (const img of images) {
|
|
5948
|
+
success(img.publicUrl);
|
|
5949
|
+
}
|
|
5950
|
+
if (savedTo) info(`Saved to ${savedTo}`);
|
|
5951
|
+
for (const text of res.texts ?? []) line(text);
|
|
5952
|
+
});
|
|
5953
|
+
})
|
|
5954
|
+
);
|
|
5955
|
+
}
|
|
5956
|
+
async function download(url, output) {
|
|
5957
|
+
const target = resolve3(output);
|
|
5958
|
+
const res = await fetch(url);
|
|
5959
|
+
if (!res.ok) {
|
|
5960
|
+
throw new WorkserError(
|
|
5961
|
+
`The image was generated but could not be downloaded (${res.status}). It is still available at ${url}`,
|
|
5962
|
+
{ code: "download_failed" }
|
|
5963
|
+
);
|
|
5964
|
+
}
|
|
5965
|
+
await mkdir(dirname2(target), { recursive: true });
|
|
5966
|
+
await writeFile3(target, Buffer.from(await res.arrayBuffer()));
|
|
5967
|
+
return target;
|
|
4973
5968
|
}
|
|
4974
5969
|
|
|
4975
5970
|
// src/commands/ask.ts
|
|
4976
|
-
var
|
|
5971
|
+
var import_picocolors22 = __toESM(require_picocolors(), 1);
|
|
4977
5972
|
var TYPES = [
|
|
4978
5973
|
"input",
|
|
4979
5974
|
"choice",
|
|
@@ -5024,7 +6019,7 @@ function registerAsk(program3) {
|
|
|
5024
6019
|
code: "bad_request"
|
|
5025
6020
|
});
|
|
5026
6021
|
}
|
|
5027
|
-
info(
|
|
6022
|
+
info(import_picocolors22.default.dim("Waiting for the user to answer\u2026"));
|
|
5028
6023
|
const res = await api(ctx, `/v1/runs/${runTarget(ctx)}/ask`, {
|
|
5029
6024
|
body: {
|
|
5030
6025
|
type,
|
|
@@ -5050,12 +6045,12 @@ function deriveTitle(message) {
|
|
|
5050
6045
|
function printAnswer(res) {
|
|
5051
6046
|
if (!res) return;
|
|
5052
6047
|
if (res.status === "answered") {
|
|
5053
|
-
line(` ${
|
|
6048
|
+
line(` ${import_picocolors22.default.green("answered")}`);
|
|
5054
6049
|
const value = extract(res.response);
|
|
5055
6050
|
if (value) line(` ${value}`);
|
|
5056
6051
|
return;
|
|
5057
6052
|
}
|
|
5058
|
-
line(` ${
|
|
6053
|
+
line(` ${import_picocolors22.default.yellow(res.status)} ${import_picocolors22.default.dim(res.reason ?? "")}`);
|
|
5059
6054
|
}
|
|
5060
6055
|
function extract(response) {
|
|
5061
6056
|
if (response == null) return "";
|
|
@@ -5075,15 +6070,22 @@ function extract(response) {
|
|
|
5075
6070
|
|
|
5076
6071
|
// src/index.ts
|
|
5077
6072
|
var pkg = {
|
|
5078
|
-
version: true ? "0.
|
|
6073
|
+
version: true ? "0.2.0" : "0.0.0-dev"
|
|
5079
6074
|
};
|
|
5080
6075
|
var program2 = new Command();
|
|
5081
6076
|
program2.name("workser").description(
|
|
5082
6077
|
"Workser CLI \u2014 give your local AI agent native DevOps & infrastructure.\nThe agent runs `workser \u2026` to provision databases, deploy, and manage real apps\non Workser \u2014 on the user's own tokens, through the Orbit cockpit (auth + approvals)."
|
|
5083
|
-
).version(pkg.version, "-v, --version", "print the CLI version").option(
|
|
6078
|
+
).version(pkg.version, "-v, --version", "print the CLI version").option(
|
|
6079
|
+
"--json",
|
|
6080
|
+
"machine-readable JSON output (always use this from agents/scripts)"
|
|
6081
|
+
).option("-q, --quiet", "suppress non-essential output").option(
|
|
6082
|
+
"-p, --project <id>",
|
|
6083
|
+
"target project id (overrides the linked project)"
|
|
6084
|
+
).option("-C, --cwd <dir>", "run as if started in <dir>").option("--endpoint <url>", "override the Workser endpoint (daemon or cloud)").option("--token <token>", "override the auth token").hook("preAction", (thisCommand, actionCommand) => {
|
|
5084
6085
|
const o = actionCommand.optsWithGlobals();
|
|
5085
6086
|
configureOutput({ json: o.json, quiet: o.quiet });
|
|
5086
6087
|
});
|
|
6088
|
+
registerHelp(program2);
|
|
5087
6089
|
registerStatus(program2);
|
|
5088
6090
|
registerLogin(program2);
|
|
5089
6091
|
registerWhoami(program2);
|
|
@@ -5091,6 +6093,7 @@ registerProject(program2);
|
|
|
5091
6093
|
registerDb(program2);
|
|
5092
6094
|
registerAuth(program2);
|
|
5093
6095
|
registerStorage(program2);
|
|
6096
|
+
registerNeon(program2);
|
|
5094
6097
|
registerEnv(program2);
|
|
5095
6098
|
registerDeploy(program2);
|
|
5096
6099
|
registerVersions(program2);
|
|
@@ -5106,5 +6109,6 @@ registerTool(program2);
|
|
|
5106
6109
|
registerMemory(program2);
|
|
5107
6110
|
registerBusiness(program2);
|
|
5108
6111
|
registerArtifact(program2);
|
|
6112
|
+
registerImage(program2);
|
|
5109
6113
|
registerAsk(program2);
|
|
5110
6114
|
program2.parseAsync(process.argv).catch((e) => fail(e));
|