@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/dist/index.js CHANGED
@@ -3650,16 +3650,580 @@ function exitCodeFor(e) {
3650
3650
  return 1;
3651
3651
  }
3652
3652
 
3653
- // src/commands/status.ts
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 { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
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 mode2 = /^https?:\/\/(127\.0\.0\.1|localhost|\[::1\])(:|\/|$)/i.test(
3707
- endpoint
3708
- ) ? "daemon" : "cloud";
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
- res = await fetch(url, {
3759
- method: opts.method ?? (opts.body !== void 0 ? "POST" : "GET"),
3760
- headers,
3761
- body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
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
- { code: "not_connected", details: e instanceof Error ? e.message : String(e) }
4379
+ {
4380
+ code: "not_connected",
4381
+ details: e instanceof Error ? e.message : String(e)
4382
+ }
3767
4383
  );
3768
4384
  }
3769
- const text = await res.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(import_picocolors2.default.bold("Workser") + import_picocolors2.default.dim(` (${ctx.mode} \xB7 ${ctx.endpoint})`));
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 ? import_picocolors2.default.dim(` (${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 ? ` ${import_picocolors2.default.cyan(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 import_picocolors2.default.green(s);
4442
+ return import_picocolors3.default.green(s);
3827
4443
  case "error":
3828
4444
  case "failed":
3829
4445
  case "canceled":
3830
- return import_picocolors2.default.red(s);
4446
+ return import_picocolors3.default.red(s);
3831
4447
  case "building":
3832
4448
  case "queued":
3833
4449
  case "deploying":
3834
- return import_picocolors2.default.yellow(s);
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 import_picocolors3 = __toESM(require_picocolors(), 1);
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(`${import_picocolors3.default.bold(p.name ?? "\u2014")}${p.id ? import_picocolors3.default.dim(` (${p.id})`) : ""}`);
3904
- if (p.url) line(import_picocolors3.default.cyan(p.url));
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(import_picocolors3.default.dim("No projects."));
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 ? import_picocolors3.default.green("\u25CF ") : " ";
3933
- line(`${pinned}${p.name ?? "\u2014"}${p.id ? import_picocolors3.default.dim(` (${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 import_picocolors4 = __toESM(require_picocolors(), 1);
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${import_picocolors4.default.dim(` (${res.name ?? "db"})`)}.` : `Provisioned database ${import_picocolors4.default.bold(res.name ?? "db")}${import_picocolors4.default.dim(` ${res.region ?? ""} ${res.status ?? ""}`.trimEnd())}.`
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(import_picocolors4.default.dim("No database yet. `workser db create`."));
3962
- for (const d of items) line(`${d.name}${import_picocolors4.default.dim(` ${d.region ?? ""} ${d.status ?? ""}`.trimEnd())}`);
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(import_picocolors4.default.dim("No tables."));
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" ? import_picocolors4.default.dim(`${t.table_schema}.`) : "";
3982
- const cols = t.column_count != null ? import_picocolors4.default.dim(` ${t.column_count} cols`) : "";
3983
- const count = t.row_count != null ? import_picocolors4.default.dim(` ${t.row_count} rows`) : "";
3984
- const size = t.table_size != null ? import_picocolors4.default.dim(` ${fmtBytes(t.table_size)}`) : "";
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(import_picocolors4.default.dim("No columns (does the table exist?)."));
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 ? import_picocolors4.default.dim(" not null") : "";
3999
- const def = c.column_default ? import_picocolors4.default.dim(` default ${c.column_default}`) : "";
4000
- line(`${c.column_name} ${import_picocolors4.default.cyan(c.data_type)}${nn}${def}`);
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(import_picocolors4.default.dim("(0 rows)"));
4699
+ line(import_picocolors5.default.dim("(0 rows)"));
4027
4700
  return;
4028
4701
  }
4029
4702
  const cols = Object.keys(rows[0]);
4030
- line(import_picocolors4.default.dim(cols.join(" ")));
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
- import_picocolors4.default.dim(
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 import_picocolors4.default.dim("\u2205");
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 import_picocolors5 = __toESM(require_picocolors(), 1);
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${import_picocolors5.default.dim(` (${providers})`)}.` : `Enabled auth ${import_picocolors5.default.dim(`(${providers})`)}.`
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 ${import_picocolors5.default.dim(`(${providers})`)}`);
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 import_picocolors6 = __toESM(require_picocolors(), 1);
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${import_picocolors6.default.dim(` (${res.bucket})`)}.` : `Provisioned bucket ${import_picocolors6.default.bold(res.bucket || "(pending)")}.`
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(import_picocolors6.default.dim("No bucket yet. `workser storage create`."));
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(import_picocolors6.default.dim("No objects."));
4796
+ if (!objects.length) return line(import_picocolors7.default.dim("No objects."));
4124
4797
  for (const o of objects) {
4125
- line(`${o.key}${import_picocolors6.default.dim(` ${fmtSize(o.size)}${o.lastModified ? " " + o.lastModified : ""}`)}`);
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 ${import_picocolors6.default.bold(res.key ?? key)} ${import_picocolors6.default.dim(`(${fmtSize(bytes.length)})`)}.`)
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 ${import_picocolors6.default.bold(key)} \u2192 ${out} ${import_picocolors6.default.dim(`(${fmtSize(bytes.length)})`)}.`)
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 import_picocolors7 = __toESM(require_picocolors(), 1);
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(import_picocolors7.default.dim(`on ${res.webAppName} (primary app) \u2014 use --app to target another`));
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(import_picocolors7.default.dim("No variables set."));
4242
- for (const v of items) line(`${v.key}${import_picocolors7.default.dim(" = " + (v.masked ?? "\u2022\u2022\u2022\u2022"))}`);
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 import_picocolors8 = __toESM(require_picocolors(), 1);
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).action(
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: { prod: Boolean(opts.prod), cwd: ctx.cwd }
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 ? " " + import_picocolors8.default.cyan(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 ${import_picocolors8.default.cyan(dep.url)}`);
4298
- else line(`deploy ${colorStatus(dep.status)} ${import_picocolors8.default.dim(`(${dep.id ?? "?"})`)}${dep.url ? " " + dep.url : ""}`);
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 import_picocolors9 = __toESM(require_picocolors(), 1);
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(import_picocolors9.default.dim("No versions yet. `workser deploy` to create one."));
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 = import_picocolors9.default.yellow(shortRef(v.ref));
4319
- const when = import_picocolors9.default.dim(formatTime(v.createdAt));
4320
- const msg = (v.message ?? "").trim() || import_picocolors9.default.dim("(no message)");
4321
- const badge = v.deployed ? " " + import_picocolors9.default.green("deployed") : "";
4322
- const url = v.url ? " " + import_picocolors9.default.cyan(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 import_picocolors10 = __toESM(require_picocolors(), 1);
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(import_picocolors10.default.dim("No custom domains."));
4380
- for (const d of items) line(`${d.domain}${import_picocolors10.default.dim(" " + (d.status ?? ""))}`);
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 import_picocolors11 = __toESM(require_picocolors(), 1);
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(import_picocolors11.default.bold("workser doctor"));
4440
- line(` endpoint: ${ctx.endpoint} ${import_picocolors11.default.dim(`(${endpointSource})`)}`);
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)} ${import_picocolors11.default.dim(`(${tokenSource})`)}` : import_picocolors11.default.yellow("none \u2014 run `workser login`")}`
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 ?? import_picocolors11.default.dim("none")}` + (link?.name ? ` ${import_picocolors11.default.dim(`(${link.name})`)}` : "") + (projectSource ? import_picocolors11.default.dim(` [${projectSource}]`) : "")
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 import_picocolors12 = __toESM(require_picocolors(), 1);
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(import_picocolors12.default.bold("main agent:") + " " + (cfg?.mainAgent ?? import_picocolors12.default.dim("none")));
4467
- line(import_picocolors12.default.bold("backup agent:") + " " + (cfg?.backupAgent ?? import_picocolors12.default.dim("none")));
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
- import_picocolors12.default.yellow(
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(import_picocolors12.default.dim("No subagents configured. Add them in the Workser Orbit Agents screen."));
4477
- line(import_picocolors12.default.bold("subagents:"));
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(import_picocolors12.default.bold("main agent:") + " " + (cfg?.mainAgent ?? import_picocolors12.default.dim("none")));
4492
- line(import_picocolors12.default.bold("backup agent:") + " " + (cfg?.backupAgent ?? import_picocolors12.default.dim("none")));
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 = import_picocolors12.default.yellow(r.role);
4515
- const agent = import_picocolors12.default.dim("\xB7 " + (r.agent ?? "?"));
4516
- const enabled = r.enabled === false ? import_picocolors12.default.red("disabled") : import_picocolors12.default.green("enabled");
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 ? import_picocolors12.default.green("runnable") : import_picocolors12.default.dim("not 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 ? " " + import_picocolors12.default.dim(extras.join(" \xB7 ")) : "";
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 import_picocolors13 = __toESM(require_picocolors(), 1);
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(import_picocolors13.default.dim(res.note ?? "No checks detected."));
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 ? import_picocolors13.default.green("\u2713") : import_picocolors13.default.red("\u2717")} ${c.name}${c.ok ? "" : import_picocolors13.default.dim(` (exit ${c.exitCode})`)}`
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
- import_picocolors13.default.red("Some checks failed \u2014 fix the errors above and re-run ") + import_picocolors13.default.bold("workser verify") + import_picocolors13.default.red(".")
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 import_picocolors14 = __toESM(require_picocolors(), 1);
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(import_picocolors14.default.dim("No workflows yet. `workser workflow create`."));
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 ? import_picocolors14.default.green("active") : import_picocolors14.default.dim("inactive");
4576
- line(`${w.id} ${import_picocolors14.default.bold(w.name ?? "Untitled")} ${status}`);
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 ${import_picocolors14.default.bold(res.id)}.`));
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(import_picocolors14.default.dim("No runs yet."));
4624
- for (const e of items) line(`${e.id} ${e.status ?? ""} ${import_picocolors14.default.dim(e.started_at ?? "")}`);
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(import_picocolors14.default.dim("No matching node types."));
4633
- for (const n of items) line(`${n.name ?? n.type} ${import_picocolors14.default.dim(n.category ?? "")}`);
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 import_picocolors15 = __toESM(require_picocolors(), 1);
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) ? import_picocolors15.default.green("connected") : import_picocolors15.default.dim("not connected");
4654
- line(`${t.slug} ${import_picocolors15.default.bold(t.name ?? t.slug)} ${status}`);
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: ${import_picocolors15.default.underline(res.oauth_url)}`) : line(`Connection ${res.connection_id} is ${res.status}.`)
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(import_picocolors15.default.dim("No tools found."));
4690
- for (const t of items) line(`${t.slug} ${import_picocolors15.default.dim(t.description ?? "")}`);
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 import_picocolors16 = __toESM(require_picocolors(), 1);
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(import_picocolors16.default.dim("No tools available."));
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(import_picocolors16.default.bold(category) + ":");
5648
+ line(import_picocolors18.default.bold(category) + ":");
4724
5649
  for (const t of items) {
4725
- line(` ${t.name} ${import_picocolors16.default.dim(t.description ?? "")}`);
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 import_picocolors17 = __toESM(require_picocolors(), 1);
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(import_picocolors17.default.dim("No matching memories."));
5692
+ if (!results?.length) return line(import_picocolors19.default.dim("No matching memories."));
4768
5693
  for (const r of results) {
4769
- line(`${import_picocolors17.default.dim(r.id ?? "?")} ${r.memory ?? r.content ?? ""}`);
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 import_picocolors18 = __toESM(require_picocolors(), 1);
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} ${import_picocolors18.default.bold(res?.id ?? "")}.`));
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 import_picocolors19 = __toESM(require_picocolors(), 1);
5813
+ var import_picocolors21 = __toESM(require_picocolors(), 1);
4889
5814
  import { existsSync as existsSync2, statSync } from "fs";
4890
- import { resolve as resolve2, basename as basename2 } from "path";
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 ? basename2(absPath) : url),
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 ${import_picocolors19.default.bold(res?.title ?? "artifact")}${res?.kind ? import_picocolors19.default.dim(` (${res.kind})`) : ""}`
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 ${import_picocolors19.default.bold(run.runId)}`);
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 ${import_picocolors19.default.dim(run.cwd)}`);
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 import_picocolors20 = __toESM(require_picocolors(), 1);
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(import_picocolors20.default.dim("Waiting for the user to answer\u2026"));
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(` ${import_picocolors20.default.green("answered")}`);
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(` ${import_picocolors20.default.yellow(res.status)} ${import_picocolors20.default.dim(res.reason ?? "")}`);
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.1.0" : "0.0.0-dev"
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("--json", "machine-readable JSON output (always use this from agents/scripts)").option("-q, --quiet", "suppress non-essential output").option("-p, --project <id>", "target project id (overrides the linked project)").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) => {
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));