octwin-cli 0.3.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/README.md +211 -210
- package/dist/index.js +244 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,52 @@ Format: [Keep a Changelog](https://keepachangelog.com/) — newest first, bucket
|
|
|
5
5
|
**Added · Changed · Deprecated · Removed · Fixed · Security**. The platform-wide view lives in the
|
|
6
6
|
repo root [`CHANGELOG.md`](../../CHANGELOG.md); this file is the CLI-only cut that ships with the package.
|
|
7
7
|
|
|
8
|
+
## [0.5.1] - 2026-08-01
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **`validate --remote` blamed the platform's version for a bad `--project`.** Every 404 from
|
|
12
|
+
`POST /api/self/p/packs/validate` printed *"this platform has no /packs/validate endpoint yet
|
|
13
|
+
(older version)"*. That was accurate when the route resolved nothing, but it now resolves the
|
|
14
|
+
tenant and project **before** validating — and an unknown project, or one outside a token's pin,
|
|
15
|
+
answers 404 by design. So a typo in `--project` sent you looking for a version mismatch instead of
|
|
16
|
+
at the flag. The two cases are now told apart by the response **body**, not the status: a missing
|
|
17
|
+
route is Fastify's `error: 'Not Found'` and keeps the old wording, while the platform's own
|
|
18
|
+
`project '<slug>' not found under tenant` is printed as-is, with the flags to check.
|
|
19
|
+
|
|
20
|
+
## [0.5.0] - 2026-08-01
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- **`octwin seed [--pack <packId>]`** — apply the pack's demo/reference data to the project it is
|
|
24
|
+
installed on, without redeploying: `xrm.yaml` `demo:` records + scheduling availability, the
|
|
25
|
+
commerce catalog, and the demo operator topology. Reports what **each kind** produced.
|
|
26
|
+
Previously seeding was reachable only as `deploy --seed`, because the platform's seed endpoint was
|
|
27
|
+
keyed on an install id, guarded platform-admin, and carried no tenant/project segments — so the
|
|
28
|
+
`/api/self/**` surface could not reach it and a `pack:deploy` token never could. Re-seeding meant
|
|
29
|
+
a full redeploy or asking an operator. Idempotent and cheap to re-run: records upsert, and
|
|
30
|
+
existing media is REUSED rather than regenerated, so a second pass reports zero images.
|
|
31
|
+
|
|
32
|
+
## [0.4.0] - 2026-08-01
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
- **`octwin projects create` / `octwin projects rm`** — a **disposable end-to-end environment** from
|
|
36
|
+
the CLI, which is what marketplace developers have been missing. `octwin deploy` has always needed
|
|
37
|
+
a project that already existed and the CLI could only *list* them, so standing up a throwaway
|
|
38
|
+
full deployment meant opening the console or asking an operator. Now:
|
|
39
|
+
```bash
|
|
40
|
+
octwin projects create "Scratch" # → slug `scratch`
|
|
41
|
+
octwin deploy --project scratch --seed # publish + install + demo data
|
|
42
|
+
octwin chat "hi" --project scratch # talk to it (--script for a whole conversation)
|
|
43
|
+
octwin projects rm scratch --yes # throw it away
|
|
44
|
+
```
|
|
45
|
+
A throwaway environment is deliberately **not a special kind of thing** — it is an ordinary
|
|
46
|
+
project in your own workspace, so it inherits your plan, entitlements, RBAC and teardown with no
|
|
47
|
+
separate lifecycle. `create` derives the URL slug from the name (`--slug` pins one, `--pack`
|
|
48
|
+
installs an already-published pack). `rm` is a HARD delete — the project and everything cascading
|
|
49
|
+
from it, no undo, not the same as archiving — so **without `--yes` it only prints what would be
|
|
50
|
+
destroyed**, making the dry run the default; the impact list is derived from `pg_constraint`, the
|
|
51
|
+
same payload the console's confirm dialog renders. Both need `projects:write`, which a
|
|
52
|
+
`pack:deploy` token does **not** confer — the CLI names that scope in the 403 hint.
|
|
53
|
+
|
|
8
54
|
## [0.3.0] - 2026-07-31
|
|
9
55
|
|
|
10
56
|
### Added
|
package/README.md
CHANGED
|
@@ -1,210 +1,211 @@
|
|
|
1
|
-
# octwin-cli
|
|
2
|
-
|
|
3
|
-
> The **Octwin** external-pack developer CLI — scaffold, validate, deploy, and manage pure-YAML packs on your own tenant. By **CEQUENS**.
|
|
4
|
-
|
|
5
|
-
[](https://www.npmjs.com/package/octwin-cli)
|
|
6
|
-
[](https://www.npmjs.com/package/octwin-cli)
|
|
7
|
-
[](https://nodejs.org)
|
|
8
|
-
|
|
9
|
-
Octwin is a pack-pluggable conversational-agent platform for WhatsApp and web. A **pack** is a
|
|
10
|
-
self-contained bot domain — its agent, conversation flows, prompts, and data model — declared
|
|
11
|
-
entirely in YAML. `octwin-cli` lets you build a **pure-YAML pack** in your own repo and deploy it
|
|
12
|
-
to a running Octwin platform to test live on your own tenant: no platform checkout, no build step,
|
|
13
|
-
and nothing untrusted to run (a pure-YAML pack is declarative data, so the platform can safely run
|
|
14
|
-
it alongside other tenants).
|
|
15
|
-
|
|
16
|
-
> The npm package is **`octwin-cli`**; the command it installs is **`octwin`**.
|
|
17
|
-
|
|
18
|
-
## Requirements
|
|
19
|
-
|
|
20
|
-
- **Node.js ≥ 20**
|
|
21
|
-
- Access to an **Octwin platform** (its base URL), a **tenant** (your workspace) on it, and a
|
|
22
|
-
**deploy token** (generated in the Octwin console — see [Authentication](#authentication)).
|
|
23
|
-
|
|
24
|
-
## Install
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
# zero-install — always the latest version
|
|
28
|
-
npx octwin-cli@latest <command>
|
|
29
|
-
|
|
30
|
-
# …or install the `octwin` command globally
|
|
31
|
-
npm install -g octwin-cli
|
|
32
|
-
octwin <command>
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## Quick start
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
# 1. Scaffold a standalone pure-YAML pack (this is your repo)
|
|
39
|
-
octwin init ./my-pack --id my-pack --description "My business bot"
|
|
40
|
-
cd ./my-pack
|
|
41
|
-
git init && git add -A && git commit -m "init pack"
|
|
42
|
-
|
|
43
|
-
# 2. Author it — edit manifest.yaml, flows/tools/main.flow.yaml (+ its locale),
|
|
44
|
-
# and prompts/identity.md. Everything is pure YAML.
|
|
45
|
-
|
|
46
|
-
# 3. Point it at your platform — one command, no config file
|
|
47
|
-
# (Octwin console → your workspace → API tokens → Generate)
|
|
48
|
-
octwin login --url https://your-octwin.example.com --token oct_…
|
|
49
|
-
|
|
50
|
-
# 4. Validate → deploy → confirm it's live
|
|
51
|
-
octwin validate
|
|
52
|
-
octwin deploy --seed # --seed also loads any demo data the pack declares
|
|
53
|
-
octwin status # "✓ live and current" once it's warm
|
|
54
|
-
|
|
55
|
-
# 5. Chat with it on your tenant (web widget / console test page). Edit and
|
|
56
|
-
# `octwin deploy` again — a redeploy hot-loads with no restart.
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
## Commands
|
|
60
|
-
|
|
61
|
-
| Command | What it does |
|
|
62
|
-
| --- | --- |
|
|
63
|
-
| `octwin init <dir>` | Scaffold a new pure-YAML pack into `<dir>` (writes a starter `manifest.yaml`, flow, prompt, `.gitignore` and `README.md` — pack content only). Options: `--id`, `--description`, `--display-name`. |
|
|
64
|
-
| `octwin validate` | Check the pack locally (structure + pure-YAML rules). `--remote` additionally runs the platform's **flow lint** — conventions the schema can't express, like an `assign:` whose value is a quoted literal, or a `$t()` key with no namespace. The lint needs a template-expanded `FlowDef`, which only the server builds, so it is a `--remote`-only check; a local ✓ does not cover it. |
|
|
65
|
-
| `octwin login` | Save a deploy token for a platform URL **and make that URL the default target** (both stored in `~/.octwin/credentials.json`). `--url`, `--token`. |
|
|
66
|
-
| `octwin whoami` | Verify the saved/passed token is valid for a tenant. `--url`, `--tenant`. |
|
|
67
|
-
| `octwin deploy` | Upload + install the pack onto your tenant's project. `--seed` also runs the pack's demo seed. Reports the **marketplace-listing verdict** when the manifest carries `listing.public: true`. |
|
|
68
|
-
| `octwin status` | Report what the platform has live for this pack — installed version, the **content sha** the instance loaded vs. the one the catalog holds (a redeploy of the *same* version changes it), its flows, and whether it is live on the public marketplace. |
|
|
69
|
-
| `octwin pull <packId>` | Write a **deployed** pack's source back to disk — the inverse of `deploy`, and how a pack pushed from one machine is recovered. Defaults to the version installed on the target project; `--version` overrides, `--dir` defaults to `./<packId>`, a non-empty dir needs `--force`. You may pull a pack your tenant **owns**. |
|
|
70
|
-
| `octwin chat "msg"` | Drive a turn through the dev web channel and print **every render with its tap ids**. `--as <handle>` picks the test user; `--tap "<tap-id>"` presses a rendered button/list row; `--json` dumps the raw envelopes. |
|
|
71
|
-
| `octwin logs` | List recent conversations (handle, status, last activity; `--as` filters), or show one conversation's full event timeline — including what each turn rendered. `--json` for raw payloads. |
|
|
72
|
-
| `octwin records` | Inspect the pack's XRM data (needs a `records:read` token). No args = list entities. |
|
|
73
|
-
| `octwin cases` | Inspect casework (support tickets): the inbox, one case + its timeline and decisions, or `--queues` for queue keys + open counts. |
|
|
74
|
-
| `octwin projects` | The `--project <slug>` values this token can name, with the plan's project cap. `--archived` includes archived ones. A `pack:deploy` token reaches it — it names a project in every other command, so this turns "guess the slug" into "read the list". |
|
|
75
|
-
| `octwin agents` | The agent roster with each agent's **effective** model / history window and **which layer set it** (project override → platform default → pack manifest) — an operator platform default can override what your manifest declares. `--prompt` prints the exact system prompt the LLM sees. Needs `agents:read`. |
|
|
76
|
-
| `octwin orders` | The orders a conversation produced. No args = the list; with a `reference_id` = line items, the subtotal/tax/shipping/discount/total breakdown, `payment_ref`, and the allowed transitions. Needs `orders:read` + the `orders` plan feature. |
|
|
77
|
-
| `octwin analytics` | Stage-by-stage conversion for **any** entity declared with a `pipeline:` (`--overview` / `--milestones` / `--trends` / `--cost`; `--stage <id>` lists the records currently at a stage). Needs `records:read`. |
|
|
78
|
-
| `octwin catalog` | Commerce products with price / availability / stock, plus the WhatsApp catalog binding. `--readiness` runs the Meta Graph checklist. Needs `catalog:read` + the `catalog` plan feature. |
|
|
79
|
-
| `octwin scheduling` | The scheduling engine's state, or `--slots <resourceRecordId>` for the slots one bookable resource actually computes — how you verify the availability rules `deploy --seed` created. Needs `scheduling:read`. |
|
|
80
|
-
| `octwin media generate "<prompt>"` | AI-generate an image, store it as a public asset, and print its `MEDIA-` handle + serve URL. `--out` downloads the bytes (WhatsApp renders only `.png`/`.jpg`); `--size`; `--json`. Pairs with `octwin chat --media` to drive media-collect flows. Needs `media:generate`. |
|
|
81
|
-
| `octwin platform-kb pull` | Pull the platform's capability reference into `.octwin/platform-kb/` for the **`octwin-pack`** Claude Code authoring plugin: guides as markdown, plus **one JSON file per capability** (`primitives/record_list.json`, `render-intents/carousel.json`, `declarations/xrm.json`, …) and an **`INDEX.md`** mapping every entry to its file — so a lookup is a small targeted read, not a whole catalog. |
|
|
82
|
-
| `octwin test` | Alias for `octwin validate --remote` — the platform's full manifest + flow-DSL check. |
|
|
83
|
-
| `octwin feedback` | Submit this pack's `FEEDBACK.md` to the platform team, with the pack version, your CLI version and the `content_hash` of the capability reference you pulled — the two facts that separate a real platform gap from something already fixed or a stale KB. |
|
|
84
|
-
| `octwin help` | Show usage. Every subcommand also answers `--help`. |
|
|
85
|
-
|
|
86
|
-
### Writing, not just reading
|
|
87
|
-
|
|
88
|
-
Every read command above has a write half behind a **leading verb**, so `octwin cases` reads and
|
|
89
|
-
`octwin cases note <id> "…"` writes. Each needs the matching `:write` scope — `octwin <cmd> --help`
|
|
90
|
-
lists the verbs and their exact flags.
|
|
91
|
-
|
|
92
|
-
| Command | Verbs |
|
|
93
|
-
| --- | --- |
|
|
94
|
-
| `octwin records` | `create <entity> --set k=v` · `patch <id> --entity <e>` · `stage <id> --to <s>` · `note <id> "…"` · `tasks` · `task complete <id>` |
|
|
95
|
-
| `octwin cases` | `assign <id> --to user:<uuid>\|none` · `note` · `transition <id> --to <status>` · `decide <id> --action <a> [--dry-run]` |
|
|
96
|
-
| `octwin orders` | `transition <ref> --to <status>` · `refund <ref> --force` |
|
|
97
|
-
| `octwin catalog` | `availability <sku> --to "in stock"` · `stock <sku> [--set-on-hand n]` |
|
|
98
|
-
| `octwin scheduling` | `rules --resource <id>` · `rule add\|rm` · `exception add\|rm` |
|
|
99
|
-
| `octwin agents` | `set <ref> [--model m] [--enable-tool t] [--disable-tool t]` |
|
|
100
|
-
|
|
101
|
-
`--set k=v` coerces JSON scalars (`--set rating=4.5` sends a number); `--fields-json` takes anything
|
|
102
|
-
nested. Destructive verbs want `--force` rather than a prompt — the CLI is non-interactive by
|
|
103
|
-
design. `cases decide --dry-run` previews the customer-facing copy and the resulting status without
|
|
104
|
-
committing, and needs only `cases:read`.
|
|
105
|
-
|
|
106
|
-
Every command that talks to the platform accepts `--dir <path>` (the pack directory; defaults to
|
|
107
|
-
the current directory) plus the target overrides `--url` / `--tenant` / `--project` / `--token`.
|
|
108
|
-
|
|
109
|
-
### Debugging a live conversation
|
|
110
|
-
|
|
111
|
-
The platform keeps **one open conversation per `--as` handle**, so consecutive `octwin chat` calls
|
|
112
|
-
with the same handle **continue the same conversation** — agent memory, suspended flows, and all:
|
|
113
|
-
|
|
114
|
-
```bash
|
|
115
|
-
octwin chat "hi" --as tester1 # turn 1 — prints the menu with each row's tap id
|
|
116
|
-
octwin chat --tap "t:invoke:my-flow:x=1" --as tester1 # turn 2 — press a rendered row
|
|
117
|
-
octwin chat "3 bedrooms" --as tester1 # turn 3 — free text into the running flow
|
|
118
|
-
octwin logs --as tester1 # find the conversation, then:
|
|
119
|
-
octwin logs <conversationId> # the full timeline (taps, renders, tool events)
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
### Reading back the state your pack created
|
|
123
|
-
|
|
124
|
-
`chat`/`logs` show what the bot *said*; these show what it *did*. A 401/403 on any of them names the
|
|
125
|
-
token scope (and plan feature) that command needs, so you can mint a wider token instead of guessing.
|
|
126
|
-
|
|
127
|
-
```bash
|
|
128
|
-
octwin agents # effective model per agent + which layer set it
|
|
129
|
-
octwin agents my-pack::assistant --prompt # the exact system prompt the LLM sees
|
|
130
|
-
octwin orders # then: octwin orders <reference_id>
|
|
131
|
-
octwin analytics # then: octwin analytics <entity> [--stage <id>]
|
|
132
|
-
octwin catalog # products + stock + the WhatsApp binding
|
|
133
|
-
octwin scheduling --slots <resourceRecordId> # the slots your availability rules compute
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
Three things worth knowing when you read the output:
|
|
137
|
-
|
|
138
|
-
- **A `pending` payment is usually correct.** The forward payment lifecycle is **webhook-owned**, and a
|
|
139
|
-
workspace with no gateway runs the credential-free `manual` driver — so `payment_request` takes its
|
|
140
|
-
`empty` port and your flow should confirm pay-on-delivery. `octwin orders <ref>` says this inline.
|
|
141
|
-
- **Your declared model may not be the one running.** An operator platform default overrides the pack
|
|
142
|
-
manifest; `octwin agents` is where that becomes visible.
|
|
143
|
-
- **An empty funnel has two causes** — the entity has no `pipeline:`, or your token's role has no `view`
|
|
144
|
-
grant on `record.<entity>`. The command prints both rather than a bare "no data".
|
|
145
|
-
|
|
146
|
-
## Configuration
|
|
147
|
-
|
|
148
|
-
**`octwin login` is the configuration.** There is no config file in your pack — a pack directory
|
|
149
|
-
holds pack content and nothing else, so the same repo deploys from any machine:
|
|
150
|
-
|
|
151
|
-
```bash
|
|
152
|
-
octwin login --url https://your-octwin.example.com --token oct_…
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
That stores the token *and* makes the URL your default target, in `~/.octwin/credentials.json`:
|
|
156
|
-
|
|
157
|
-
```jsonc
|
|
158
|
-
{
|
|
159
|
-
"default_url": "https://your-octwin.example.com", // set by the last `octwin login`
|
|
160
|
-
"https://your-octwin.example.com": "oct_…" // token, keyed by platform url
|
|
161
|
-
}
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
Tenant and project need no setting at all — the **token carries its own tenant**, plus an optional
|
|
165
|
-
project pin. `--tenant` / `--project` exist only as overrides (a multi-workspace human, or an
|
|
166
|
-
unpinned token that must name a project).
|
|
167
|
-
|
|
168
|
-
Each setting resolves **flag → environment variable → saved login**:
|
|
169
|
-
|
|
170
|
-
| Setting | Flag | Env var | Saved login |
|
|
171
|
-
| --- | --- | --- | --- |
|
|
172
|
-
| Platform URL | `--url` | `PACK_PLATFORM_URL` | `default_url` |
|
|
173
|
-
| Deploy token | `--token` | `PACK_TOKEN` | token for that URL |
|
|
174
|
-
| Tenant slug *(override)* | `--tenant` | `PACK_TENANT` | — *(from the token)* |
|
|
175
|
-
| Project slug *(override)* | `--project` | `PACK_PROJECT` | — *(from the token's pin)* |
|
|
176
|
-
|
|
177
|
-
For **CI**, skip `login` entirely and pass `PACK_PLATFORM_URL` + `PACK_TOKEN` as environment
|
|
178
|
-
variables (add `PACK_PROJECT` only if the token isn't pinned).
|
|
179
|
-
|
|
180
|
-
## Authentication
|
|
181
|
-
|
|
182
|
-
You authenticate with a tenant-scoped **deploy token** (prefixed `oct_…`) — not a password and not
|
|
183
|
-
an operator token. Generate it in the Octwin console (**your workspace → API tokens → Generate**).
|
|
184
|
-
It is **least-privilege** (scope `pack:deploy`): it can deploy packs to your tenant but cannot
|
|
185
|
-
manage members, billing, or other tenants, and it is revocable at any time.
|
|
186
|
-
|
|
187
|
-
Add the optional **`media:generate`** scope to let a `--seed` deploy AI-generate seed images
|
|
188
|
-
(for a demo record field like `photo: "generate:<prompt>"`); without it, such fields are seeded as
|
|
189
|
-
text only.
|
|
190
|
-
|
|
191
|
-
## What a pack may contain
|
|
192
|
-
|
|
193
|
-
A pack is **pure declarative data** — `.yaml` / `.yml` / `.md` / `.
|
|
194
|
-
code (`.ts`/`.js`), HTTP routes, DB clients,
|
|
195
|
-
makes an external pack safe to run on a shared platform; the server enforces it on
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
- **
|
|
206
|
-
- **
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
1
|
+
# octwin-cli
|
|
2
|
+
|
|
3
|
+
> The **Octwin** external-pack developer CLI — scaffold, validate, deploy, and manage pure-YAML packs on your own tenant. By **CEQUENS**.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/octwin-cli)
|
|
6
|
+
[](https://www.npmjs.com/package/octwin-cli)
|
|
7
|
+
[](https://nodejs.org)
|
|
8
|
+
|
|
9
|
+
Octwin is a pack-pluggable conversational-agent platform for WhatsApp and web. A **pack** is a
|
|
10
|
+
self-contained bot domain — its agent, conversation flows, prompts, and data model — declared
|
|
11
|
+
entirely in YAML. `octwin-cli` lets you build a **pure-YAML pack** in your own repo and deploy it
|
|
12
|
+
to a running Octwin platform to test live on your own tenant: no platform checkout, no build step,
|
|
13
|
+
and nothing untrusted to run (a pure-YAML pack is declarative data, so the platform can safely run
|
|
14
|
+
it alongside other tenants).
|
|
15
|
+
|
|
16
|
+
> The npm package is **`octwin-cli`**; the command it installs is **`octwin`**.
|
|
17
|
+
|
|
18
|
+
## Requirements
|
|
19
|
+
|
|
20
|
+
- **Node.js ≥ 20**
|
|
21
|
+
- Access to an **Octwin platform** (its base URL), a **tenant** (your workspace) on it, and a
|
|
22
|
+
**deploy token** (generated in the Octwin console — see [Authentication](#authentication)).
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# zero-install — always the latest version
|
|
28
|
+
npx octwin-cli@latest <command>
|
|
29
|
+
|
|
30
|
+
# …or install the `octwin` command globally
|
|
31
|
+
npm install -g octwin-cli
|
|
32
|
+
octwin <command>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# 1. Scaffold a standalone pure-YAML pack (this is your repo)
|
|
39
|
+
octwin init ./my-pack --id my-pack --description "My business bot"
|
|
40
|
+
cd ./my-pack
|
|
41
|
+
git init && git add -A && git commit -m "init pack"
|
|
42
|
+
|
|
43
|
+
# 2. Author it — edit manifest.yaml, flows/tools/main.flow.yaml (+ its locale),
|
|
44
|
+
# and prompts/identity.md. Everything is pure YAML.
|
|
45
|
+
|
|
46
|
+
# 3. Point it at your platform — one command, no config file
|
|
47
|
+
# (Octwin console → your workspace → API tokens → Generate)
|
|
48
|
+
octwin login --url https://your-octwin.example.com --token oct_…
|
|
49
|
+
|
|
50
|
+
# 4. Validate → deploy → confirm it's live
|
|
51
|
+
octwin validate
|
|
52
|
+
octwin deploy --seed # --seed also loads any demo data the pack declares
|
|
53
|
+
octwin status # "✓ live and current" once it's warm
|
|
54
|
+
|
|
55
|
+
# 5. Chat with it on your tenant (web widget / console test page). Edit and
|
|
56
|
+
# `octwin deploy` again — a redeploy hot-loads with no restart.
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Commands
|
|
60
|
+
|
|
61
|
+
| Command | What it does |
|
|
62
|
+
| --- | --- |
|
|
63
|
+
| `octwin init <dir>` | Scaffold a new pure-YAML pack into `<dir>` (writes a starter `manifest.yaml`, flow, prompt, `.gitignore` and `README.md` — pack content only). Options: `--id`, `--description`, `--display-name`. |
|
|
64
|
+
| `octwin validate` | Check the pack locally (structure + pure-YAML rules). `--remote` additionally runs the platform's **flow lint** — conventions the schema can't express, like an `assign:` whose value is a quoted literal, or a `$t()` key with no namespace. The lint needs a template-expanded `FlowDef`, which only the server builds, so it is a `--remote`-only check; a local ✓ does not cover it. |
|
|
65
|
+
| `octwin login` | Save a deploy token for a platform URL **and make that URL the default target** (both stored in `~/.octwin/credentials.json`). `--url`, `--token`. |
|
|
66
|
+
| `octwin whoami` | Verify the saved/passed token is valid for a tenant. `--url`, `--tenant`. |
|
|
67
|
+
| `octwin deploy` | Upload + install the pack onto your tenant's project. `--seed` also runs the pack's demo seed. Reports the **marketplace-listing verdict** when the manifest carries `listing.public: true`. |
|
|
68
|
+
| `octwin status` | Report what the platform has live for this pack — installed version, the **content sha** the instance loaded vs. the one the catalog holds (a redeploy of the *same* version changes it), its flows, and whether it is live on the public marketplace. |
|
|
69
|
+
| `octwin pull <packId>` | Write a **deployed** pack's source back to disk — the inverse of `deploy`, and how a pack pushed from one machine is recovered. Defaults to the version installed on the target project; `--version` overrides, `--dir` defaults to `./<packId>`, a non-empty dir needs `--force`. You may pull a pack your tenant **owns**. |
|
|
70
|
+
| `octwin chat "msg"` | Drive a turn through the dev web channel and print **every render with its tap ids**. `--as <handle>` picks the test user; `--tap "<tap-id>"` presses a rendered button/list row; `--json` dumps the raw envelopes. |
|
|
71
|
+
| `octwin logs` | List recent conversations (handle, status, last activity; `--as` filters), or show one conversation's full event timeline — including what each turn rendered. `--json` for raw payloads. |
|
|
72
|
+
| `octwin records` | Inspect the pack's XRM data (needs a `records:read` token). No args = list entities. |
|
|
73
|
+
| `octwin cases` | Inspect casework (support tickets): the inbox, one case + its timeline and decisions, or `--queues` for queue keys + open counts. |
|
|
74
|
+
| `octwin projects` | The `--project <slug>` values this token can name, with the plan's project cap. `--archived` includes archived ones. A `pack:deploy` token reaches it — it names a project in every other command, so this turns "guess the slug" into "read the list". |
|
|
75
|
+
| `octwin agents` | The agent roster with each agent's **effective** model / history window and **which layer set it** (project override → platform default → pack manifest) — an operator platform default can override what your manifest declares. `--prompt` prints the exact system prompt the LLM sees. Needs `agents:read`. |
|
|
76
|
+
| `octwin orders` | The orders a conversation produced. No args = the list; with a `reference_id` = line items, the subtotal/tax/shipping/discount/total breakdown, `payment_ref`, and the allowed transitions. Needs `orders:read` + the `orders` plan feature. |
|
|
77
|
+
| `octwin analytics` | Stage-by-stage conversion for **any** entity declared with a `pipeline:` (`--overview` / `--milestones` / `--trends` / `--cost`; `--stage <id>` lists the records currently at a stage). Needs `records:read`. |
|
|
78
|
+
| `octwin catalog` | Commerce products with price / availability / stock, plus the WhatsApp catalog binding. `--readiness` runs the Meta Graph checklist. Needs `catalog:read` + the `catalog` plan feature. |
|
|
79
|
+
| `octwin scheduling` | The scheduling engine's state, or `--slots <resourceRecordId>` for the slots one bookable resource actually computes — how you verify the availability rules `deploy --seed` created. Needs `scheduling:read`. |
|
|
80
|
+
| `octwin media generate "<prompt>"` | AI-generate an image, store it as a public asset, and print its `MEDIA-` handle + serve URL. `--out` downloads the bytes (WhatsApp renders only `.png`/`.jpg`); `--size`; `--json`. Pairs with `octwin chat --media` to drive media-collect flows. Needs `media:generate`. |
|
|
81
|
+
| `octwin platform-kb pull` | Pull the platform's capability reference into `.octwin/platform-kb/` for the **`octwin-pack`** Claude Code authoring plugin: guides as markdown, plus **one JSON file per capability** (`primitives/record_list.json`, `render-intents/carousel.json`, `declarations/xrm.json`, …) and an **`INDEX.md`** mapping every entry to its file — so a lookup is a small targeted read, not a whole catalog. |
|
|
82
|
+
| `octwin test` | Alias for `octwin validate --remote` — the platform's full manifest + flow-DSL check. |
|
|
83
|
+
| `octwin feedback` | Submit this pack's `FEEDBACK.md` to the platform team, with the pack version, your CLI version and the `content_hash` of the capability reference you pulled — the two facts that separate a real platform gap from something already fixed or a stale KB. |
|
|
84
|
+
| `octwin help` | Show usage. Every subcommand also answers `--help`. |
|
|
85
|
+
|
|
86
|
+
### Writing, not just reading
|
|
87
|
+
|
|
88
|
+
Every read command above has a write half behind a **leading verb**, so `octwin cases` reads and
|
|
89
|
+
`octwin cases note <id> "…"` writes. Each needs the matching `:write` scope — `octwin <cmd> --help`
|
|
90
|
+
lists the verbs and their exact flags.
|
|
91
|
+
|
|
92
|
+
| Command | Verbs |
|
|
93
|
+
| --- | --- |
|
|
94
|
+
| `octwin records` | `create <entity> --set k=v` · `patch <id> --entity <e>` · `stage <id> --to <s>` · `note <id> "…"` · `tasks` · `task complete <id>` |
|
|
95
|
+
| `octwin cases` | `assign <id> --to user:<uuid>\|none` · `note` · `transition <id> --to <status>` · `decide <id> --action <a> [--dry-run]` |
|
|
96
|
+
| `octwin orders` | `transition <ref> --to <status>` · `refund <ref> --force` |
|
|
97
|
+
| `octwin catalog` | `availability <sku> --to "in stock"` · `stock <sku> [--set-on-hand n]` |
|
|
98
|
+
| `octwin scheduling` | `rules --resource <id>` · `rule add\|rm` · `exception add\|rm` |
|
|
99
|
+
| `octwin agents` | `set <ref> [--model m] [--enable-tool t] [--disable-tool t]` |
|
|
100
|
+
|
|
101
|
+
`--set k=v` coerces JSON scalars (`--set rating=4.5` sends a number); `--fields-json` takes anything
|
|
102
|
+
nested. Destructive verbs want `--force` rather than a prompt — the CLI is non-interactive by
|
|
103
|
+
design. `cases decide --dry-run` previews the customer-facing copy and the resulting status without
|
|
104
|
+
committing, and needs only `cases:read`.
|
|
105
|
+
|
|
106
|
+
Every command that talks to the platform accepts `--dir <path>` (the pack directory; defaults to
|
|
107
|
+
the current directory) plus the target overrides `--url` / `--tenant` / `--project` / `--token`.
|
|
108
|
+
|
|
109
|
+
### Debugging a live conversation
|
|
110
|
+
|
|
111
|
+
The platform keeps **one open conversation per `--as` handle**, so consecutive `octwin chat` calls
|
|
112
|
+
with the same handle **continue the same conversation** — agent memory, suspended flows, and all:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
octwin chat "hi" --as tester1 # turn 1 — prints the menu with each row's tap id
|
|
116
|
+
octwin chat --tap "t:invoke:my-flow:x=1" --as tester1 # turn 2 — press a rendered row
|
|
117
|
+
octwin chat "3 bedrooms" --as tester1 # turn 3 — free text into the running flow
|
|
118
|
+
octwin logs --as tester1 # find the conversation, then:
|
|
119
|
+
octwin logs <conversationId> # the full timeline (taps, renders, tool events)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Reading back the state your pack created
|
|
123
|
+
|
|
124
|
+
`chat`/`logs` show what the bot *said*; these show what it *did*. A 401/403 on any of them names the
|
|
125
|
+
token scope (and plan feature) that command needs, so you can mint a wider token instead of guessing.
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
octwin agents # effective model per agent + which layer set it
|
|
129
|
+
octwin agents my-pack::assistant --prompt # the exact system prompt the LLM sees
|
|
130
|
+
octwin orders # then: octwin orders <reference_id>
|
|
131
|
+
octwin analytics # then: octwin analytics <entity> [--stage <id>]
|
|
132
|
+
octwin catalog # products + stock + the WhatsApp binding
|
|
133
|
+
octwin scheduling --slots <resourceRecordId> # the slots your availability rules compute
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Three things worth knowing when you read the output:
|
|
137
|
+
|
|
138
|
+
- **A `pending` payment is usually correct.** The forward payment lifecycle is **webhook-owned**, and a
|
|
139
|
+
workspace with no gateway runs the credential-free `manual` driver — so `payment_request` takes its
|
|
140
|
+
`empty` port and your flow should confirm pay-on-delivery. `octwin orders <ref>` says this inline.
|
|
141
|
+
- **Your declared model may not be the one running.** An operator platform default overrides the pack
|
|
142
|
+
manifest; `octwin agents` is where that becomes visible.
|
|
143
|
+
- **An empty funnel has two causes** — the entity has no `pipeline:`, or your token's role has no `view`
|
|
144
|
+
grant on `record.<entity>`. The command prints both rather than a bare "no data".
|
|
145
|
+
|
|
146
|
+
## Configuration
|
|
147
|
+
|
|
148
|
+
**`octwin login` is the configuration.** There is no config file in your pack — a pack directory
|
|
149
|
+
holds pack content and nothing else, so the same repo deploys from any machine:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
octwin login --url https://your-octwin.example.com --token oct_…
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
That stores the token *and* makes the URL your default target, in `~/.octwin/credentials.json`:
|
|
156
|
+
|
|
157
|
+
```jsonc
|
|
158
|
+
{
|
|
159
|
+
"default_url": "https://your-octwin.example.com", // set by the last `octwin login`
|
|
160
|
+
"https://your-octwin.example.com": "oct_…" // token, keyed by platform url
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Tenant and project need no setting at all — the **token carries its own tenant**, plus an optional
|
|
165
|
+
project pin. `--tenant` / `--project` exist only as overrides (a multi-workspace human, or an
|
|
166
|
+
unpinned token that must name a project).
|
|
167
|
+
|
|
168
|
+
Each setting resolves **flag → environment variable → saved login**:
|
|
169
|
+
|
|
170
|
+
| Setting | Flag | Env var | Saved login |
|
|
171
|
+
| --- | --- | --- | --- |
|
|
172
|
+
| Platform URL | `--url` | `PACK_PLATFORM_URL` | `default_url` |
|
|
173
|
+
| Deploy token | `--token` | `PACK_TOKEN` | token for that URL |
|
|
174
|
+
| Tenant slug *(override)* | `--tenant` | `PACK_TENANT` | — *(from the token)* |
|
|
175
|
+
| Project slug *(override)* | `--project` | `PACK_PROJECT` | — *(from the token's pin)* |
|
|
176
|
+
|
|
177
|
+
For **CI**, skip `login` entirely and pass `PACK_PLATFORM_URL` + `PACK_TOKEN` as environment
|
|
178
|
+
variables (add `PACK_PROJECT` only if the token isn't pinned).
|
|
179
|
+
|
|
180
|
+
## Authentication
|
|
181
|
+
|
|
182
|
+
You authenticate with a tenant-scoped **deploy token** (prefixed `oct_…`) — not a password and not
|
|
183
|
+
an operator token. Generate it in the Octwin console (**your workspace → API tokens → Generate**).
|
|
184
|
+
It is **least-privilege** (scope `pack:deploy`): it can deploy packs to your tenant but cannot
|
|
185
|
+
manage members, billing, or other tenants, and it is revocable at any time.
|
|
186
|
+
|
|
187
|
+
Add the optional **`media:generate`** scope to let a `--seed` deploy AI-generate seed images
|
|
188
|
+
(for a demo record field like `photo: "generate:<prompt>"`); without it, such fields are seeded as
|
|
189
|
+
text only.
|
|
190
|
+
|
|
191
|
+
## What a pack may contain
|
|
192
|
+
|
|
193
|
+
A pack is **pure declarative data** — `.yaml` / `.yml` / `.md` / `.json` only. Executable
|
|
194
|
+
code (`.ts`/`.js`), HTTP routes, DB clients, custom primitives and **`.sql`** are **not** allowed
|
|
195
|
+
(this is what makes an external pack safe to run on a shared platform; the server enforces it on
|
|
196
|
+
deploy, and so does `octwin validate`). For
|
|
197
|
+
domain records, use Octwin's first-class storage modules — **XRM** (records with stage pipelines),
|
|
198
|
+
**catalog** (products), or **casework** (tickets) — declared in `xrm.yaml` and `worklist.yaml`, so a
|
|
199
|
+
pack needs **no database of its own**. (Casework rides `worklist.yaml`'s `work.<entity>` block;
|
|
200
|
+
there is no ~~`cases.yaml`~~ grammar. `octwin platform-kb pull` ships the authoritative list of
|
|
201
|
+
declaration files — read its `INDEX.md` rather than this paragraph.)
|
|
202
|
+
|
|
203
|
+
## Links
|
|
204
|
+
|
|
205
|
+
- **npm:** <https://www.npmjs.com/package/octwin-cli>
|
|
206
|
+
- **Command help:** `octwin help` (each subcommand also answers `--help`)
|
|
207
|
+
- **Changelog:** [CHANGELOG.md](./CHANGELOG.md)
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
[MIT](./LICENSE) © CEQUENS
|
package/dist/index.js
CHANGED
|
@@ -197,9 +197,15 @@ const VERB_REQUIREMENTS = {
|
|
|
197
197
|
'scheduling rule': { scope: 'scheduling:write' },
|
|
198
198
|
'scheduling exception': { scope: 'scheduling:write' },
|
|
199
199
|
'agents set': { scope: 'agents:write' },
|
|
200
|
+
// Creating and destroying a project are the same scope as editing one. Worth
|
|
201
|
+
// spelling out because the natural token for the deploy loop is `pack:deploy`,
|
|
202
|
+
// which does NOT confer this — that 403 is otherwise baffling.
|
|
203
|
+
'projects create': { scope: 'projects:write' },
|
|
204
|
+
'projects rm': { scope: 'projects:write' },
|
|
200
205
|
};
|
|
201
206
|
const COMMAND_REQUIREMENTS = {
|
|
202
207
|
deploy: { scope: 'pack:deploy' },
|
|
208
|
+
seed: { scope: 'pack:deploy' },
|
|
203
209
|
validate: { scope: 'pack:deploy' },
|
|
204
210
|
status: { scope: 'pack:deploy' },
|
|
205
211
|
test: { scope: 'pack:deploy' },
|
|
@@ -587,7 +593,8 @@ function commandTouchesPlatform(command, flags) {
|
|
|
587
593
|
case 'analytics':
|
|
588
594
|
case 'catalog':
|
|
589
595
|
case 'scheduling':
|
|
590
|
-
case 'projects':
|
|
596
|
+
case 'projects':
|
|
597
|
+
case 'seed': return true;
|
|
591
598
|
default: return false;
|
|
592
599
|
}
|
|
593
600
|
}
|
|
@@ -730,9 +737,25 @@ async function cmdValidate(flags) {
|
|
|
730
737
|
json = text;
|
|
731
738
|
}
|
|
732
739
|
if (!res.ok) {
|
|
733
|
-
// 404
|
|
734
|
-
|
|
735
|
-
|
|
740
|
+
// A 404 here is AMBIGUOUS and must not be collapsed. The route resolves the
|
|
741
|
+
// tenant and the project BEFORE it validates anything, so a 404 is usually an
|
|
742
|
+
// unknown `--tenant`/`--project` — and a token's project PIN answers 404 by
|
|
743
|
+
// design (an out-of-pin project is deliberately indistinguishable from one that
|
|
744
|
+
// does not exist). Reporting all of those as "older platform" sends the author
|
|
745
|
+
// hunting for a version mismatch that does not exist.
|
|
746
|
+
//
|
|
747
|
+
// The two are told apart by the BODY, not the status: the platform has no
|
|
748
|
+
// custom not-found handler, so a missing route is Fastify's default
|
|
749
|
+
// `{ statusCode, error: 'Not Found', message: 'Route … not found' }`, whereas
|
|
750
|
+
// `resolveTenantOr404`/`resolveProjectOr404` send a bare `{ error: "<what> not
|
|
751
|
+
// found" }`. The server's own message already names the slug it tried, so the
|
|
752
|
+
// hint carries the fix rather than repeating the target.
|
|
753
|
+
if (res.status === 404) {
|
|
754
|
+
const routeMissing = typeof json !== 'object' || json === null || json.error === 'Not Found';
|
|
755
|
+
if (routeMissing)
|
|
756
|
+
die('this platform has no /packs/validate endpoint yet (older version) — deploy runs the full check');
|
|
757
|
+
die(`remote validate${errDetail(json)} — check --tenant/--project (or PACK_TENANT/PACK_PROJECT); \`octwin projects\` lists what this token can reach`);
|
|
758
|
+
}
|
|
736
759
|
console.error(`✗ remote validate failed (HTTP ${res.status})`);
|
|
737
760
|
printAuthHint(res.status, url);
|
|
738
761
|
console.error(typeof json === 'string' ? json : JSON.stringify(json, null, 2));
|
|
@@ -1015,6 +1038,71 @@ function printDeploySuccess(id, version, t, r) {
|
|
|
1015
1038
|
printPublicListing(r?.public_listing, r?.public_review_note);
|
|
1016
1039
|
console.log(`\nChat with it: octwin chat "hi" --as tester (or the web widget / console test page).`);
|
|
1017
1040
|
}
|
|
1041
|
+
/**
|
|
1042
|
+
* `octwin seed [--pack <id>]` — apply the pack's demo/reference data to the project it
|
|
1043
|
+
* is installed on, without redeploying.
|
|
1044
|
+
*
|
|
1045
|
+
* Exists because seeding used to be reachable only as `deploy --seed`: the platform's
|
|
1046
|
+
* seed endpoint was keyed on an install id, guarded `requirePlatformAdmin`, and carried
|
|
1047
|
+
* no tenant/project segments — so the `/api/self/**` rewrite could not reach it and a
|
|
1048
|
+
* `pack:deploy` token never could. Re-seeding meant a full redeploy, or asking an
|
|
1049
|
+
* operator.
|
|
1050
|
+
*
|
|
1051
|
+
* Reuses `readDeployProgress` verbatim: the platform emits ONE seed-progress vocabulary
|
|
1052
|
+
* now (`stage:'seed'` with a `kind`), so a second reader would only be a second thing to
|
|
1053
|
+
* keep in step.
|
|
1054
|
+
*/
|
|
1055
|
+
async function cmdSeed(flags) {
|
|
1056
|
+
const t = resolveTarget(flags);
|
|
1057
|
+
const { url } = t;
|
|
1058
|
+
const packId = typeof flags.pack === 'string' ? flags.pack : undefined;
|
|
1059
|
+
console.log(`→ Seeding ${packId ?? 'the installed pack'} on ${targetLabel(t)} …`);
|
|
1060
|
+
const res = await fetchOrDie(`${url}/api/self/p/packs/seed`, {
|
|
1061
|
+
method: 'POST',
|
|
1062
|
+
headers: { 'content-type': 'application/json', accept: 'text/event-stream', ...authHeaders(t) },
|
|
1063
|
+
body: JSON.stringify(packId ? { pack_id: packId } : {}),
|
|
1064
|
+
}, 'seed');
|
|
1065
|
+
if (res.ok && (res.headers.get('content-type') ?? '').includes('text/event-stream') && res.body) {
|
|
1066
|
+
const { terminal: final, stepErrors } = await readDeployProgress(res.body);
|
|
1067
|
+
if (!final || final.stage === 'error')
|
|
1068
|
+
die(`seed failed${final?.message ? `: ${final.message}` : ' (stream ended early)'}`);
|
|
1069
|
+
console.log(`
|
|
1070
|
+
✓ ${final.message ?? 'seed complete'}`);
|
|
1071
|
+
printSeedCounts(final.result?.seeded);
|
|
1072
|
+
if (stepErrors.length) {
|
|
1073
|
+
// A kind failed but the rest ran — the reconcile softens each step. Say which,
|
|
1074
|
+
// and exit non-zero so a scripted `seed && chat` doesn't read as clean.
|
|
1075
|
+
console.error(`
|
|
1076
|
+
⚠ ${stepErrors.length} step${stepErrors.length === 1 ? '' : 's'} failed — data may be incomplete:`);
|
|
1077
|
+
for (const e of stepErrors)
|
|
1078
|
+
console.error(` • ${e}`);
|
|
1079
|
+
process.exit(1);
|
|
1080
|
+
}
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
const text = await res.text();
|
|
1084
|
+
let json;
|
|
1085
|
+
try {
|
|
1086
|
+
json = JSON.parse(text);
|
|
1087
|
+
}
|
|
1088
|
+
catch {
|
|
1089
|
+
json = text;
|
|
1090
|
+
}
|
|
1091
|
+
if (!res.ok) {
|
|
1092
|
+
console.error(`✗ seed failed (HTTP ${res.status})${errDetail(json)}`);
|
|
1093
|
+
printAuthHint(res.status, url);
|
|
1094
|
+
process.exit(1);
|
|
1095
|
+
}
|
|
1096
|
+
console.log('✓ seed complete');
|
|
1097
|
+
printSeedCounts(json?.seeded);
|
|
1098
|
+
}
|
|
1099
|
+
/** Per-kind counts, one line each. Prints nothing when the pack declared nothing. */
|
|
1100
|
+
function printSeedCounts(seeded) {
|
|
1101
|
+
for (const [kind, counts] of Object.entries(seeded ?? {})) {
|
|
1102
|
+
const detail = Object.entries(counts).filter(([, v]) => v > 0).map(([k, v]) => `${v} ${k}`).join(' · ');
|
|
1103
|
+
console.log(` ${kind.padEnd(11)} ${detail || '—'}`);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1018
1106
|
async function cmdDeploy(flags) {
|
|
1019
1107
|
const packDir = resolve(flags.dir ?? '.');
|
|
1020
1108
|
const t = resolveTarget(flags);
|
|
@@ -2523,6 +2611,10 @@ async function cmdAgentsWrite(flags) {
|
|
|
2523
2611
|
* (`/api/self/t/`), unlike `agents` — the list is a property of the workspace.
|
|
2524
2612
|
*/
|
|
2525
2613
|
async function cmdProjects(flags) {
|
|
2614
|
+
if (flags._[0] === 'create')
|
|
2615
|
+
return cmdProjectsCreate(flags);
|
|
2616
|
+
if (flags._[0] === 'rm')
|
|
2617
|
+
return cmdProjectsRm(flags);
|
|
2526
2618
|
const t = resolveTarget(flags);
|
|
2527
2619
|
const { url } = t;
|
|
2528
2620
|
const asJson = flags.json === true;
|
|
@@ -2558,6 +2650,128 @@ async function cmdProjects(flags) {
|
|
|
2558
2650
|
console.log('\nUse one as: octwin deploy --project <slug>');
|
|
2559
2651
|
if (!archived)
|
|
2560
2652
|
console.log('Archived too: octwin projects --archived');
|
|
2653
|
+
console.log('New one: octwin projects create "<name>"');
|
|
2654
|
+
}
|
|
2655
|
+
/**
|
|
2656
|
+
* `octwin projects create "<name>" [--slug <slug>] [--pack <packId>]`
|
|
2657
|
+
*
|
|
2658
|
+
* The missing half of the deploy loop. `octwin deploy` has always needed a project
|
|
2659
|
+
* that already exists, and the CLI could only LIST them — so standing up a throwaway
|
|
2660
|
+
* end-to-end deployment meant opening the console or asking an operator. With this,
|
|
2661
|
+
* a full disposable environment is two commands:
|
|
2662
|
+
*
|
|
2663
|
+
* octwin projects create "Scratch" # → slug `scratch`
|
|
2664
|
+
* octwin deploy --project scratch --seed # publish + install + demo data
|
|
2665
|
+
* octwin chat "hi" --project scratch # talk to it
|
|
2666
|
+
* octwin projects rm scratch --yes # throw it away
|
|
2667
|
+
*
|
|
2668
|
+
* A demo is deliberately NOT a special kind of thing — it is an ordinary project in
|
|
2669
|
+
* the developer's own workspace, so it inherits their plan, entitlements, RBAC and
|
|
2670
|
+
* teardown with no bespoke lifecycle to keep honest.
|
|
2671
|
+
*
|
|
2672
|
+
* `packs: []` is the default because the very next step is normally `octwin deploy`,
|
|
2673
|
+
* which publishes the working tree AND installs it. `--pack` is for an ALREADY
|
|
2674
|
+
* published pack (it resolves through `pack_registry` and fails fast if absent).
|
|
2675
|
+
*/
|
|
2676
|
+
async function cmdProjectsCreate(flags) {
|
|
2677
|
+
// Argument check BEFORE `resolveTarget`, so a missing name reports the usage line
|
|
2678
|
+
// rather than "no platform url" — an argument mistake must not be masked by a
|
|
2679
|
+
// config one the author may not even have.
|
|
2680
|
+
const name = flags._[1];
|
|
2681
|
+
if (!name)
|
|
2682
|
+
die('usage: octwin projects create "<name>" [--slug <slug>] [--pack <packId>]');
|
|
2683
|
+
const t = resolveTarget(flags);
|
|
2684
|
+
const { url } = t;
|
|
2685
|
+
// The Project URL is DERIVED from the name and uniquified server-side unless the
|
|
2686
|
+
// caller pins one — same contract the console's create form uses, so the two
|
|
2687
|
+
// cannot disagree about what slug a given name produces.
|
|
2688
|
+
const body = { name, packs: flags.pack ? [flags.pack] : [] };
|
|
2689
|
+
if (typeof flags.slug === 'string')
|
|
2690
|
+
body.slug = flags.slug;
|
|
2691
|
+
console.log(`→ Creating project "${name}" in ${targetLabel(t)} …`);
|
|
2692
|
+
const { status, json } = await apiSend('POST', `${url}/api/self/t/projects`, body, t);
|
|
2693
|
+
// 402 is the plan cap, and it is the ONE failure here with a non-obvious fix, so it
|
|
2694
|
+
// gets the server's own sentence rather than a generic write failure.
|
|
2695
|
+
if (status === 402)
|
|
2696
|
+
die(`${json?.error ?? 'project limit reached'} — free the slot with \`octwin projects rm <slug> --yes\`, or upgrade the plan.`);
|
|
2697
|
+
if (status !== 200 && status !== 201)
|
|
2698
|
+
writeFail(`create project "${name}"`, status, json, url);
|
|
2699
|
+
if (flags.json === true) {
|
|
2700
|
+
console.log(JSON.stringify(json, null, 2));
|
|
2701
|
+
return;
|
|
2702
|
+
}
|
|
2703
|
+
const slug = json?.slug ?? flags.slug ?? '(unknown)';
|
|
2704
|
+
const installed = (json?.installed_packs ?? []);
|
|
2705
|
+
console.log(`✓ Project created — ${slug}`);
|
|
2706
|
+
for (const p of installed)
|
|
2707
|
+
console.log(` installed ${p.pack_id}@${p.version}`);
|
|
2708
|
+
console.log('\nNext:');
|
|
2709
|
+
console.log(` octwin deploy --project ${slug} --seed`);
|
|
2710
|
+
console.log(` octwin chat "hi" --project ${slug}`);
|
|
2711
|
+
}
|
|
2712
|
+
/**
|
|
2713
|
+
* `octwin projects rm <slug> --yes`
|
|
2714
|
+
*
|
|
2715
|
+
* HARD delete — the row and everything the FK graph cascades from it (conversations,
|
|
2716
|
+
* contacts, records, installs, webhooks). Not the archive verb; there is no undo.
|
|
2717
|
+
*
|
|
2718
|
+
* `--yes` is required rather than prompted because the CLI is non-interactive by
|
|
2719
|
+
* design (it runs under `npx`, in scripts and in CI, where a prompt reads EOF and a
|
|
2720
|
+
* "safe" default would be a lie). Without it this prints the same impact preview the
|
|
2721
|
+
* console's confirm dialog shows — derived from `pg_constraint`, not a hand-written
|
|
2722
|
+
* list — and stops. That makes the dry run the DEFAULT, which is the right way round
|
|
2723
|
+
* for an irreversible verb.
|
|
2724
|
+
*/
|
|
2725
|
+
async function cmdProjectsRm(flags) {
|
|
2726
|
+
const slug = flags._[1];
|
|
2727
|
+
if (!slug)
|
|
2728
|
+
die('usage: octwin projects rm <slug> --yes (omit --yes to preview what it destroys)');
|
|
2729
|
+
const t = resolveTarget(flags);
|
|
2730
|
+
const { url } = t;
|
|
2731
|
+
const preview = await apiGet(`${url}/api/self/t/projects/${encodeURIComponent(slug)}/preview-hard-delete`, t);
|
|
2732
|
+
if (preview.status === 404)
|
|
2733
|
+
die(`no project '${slug}' in ${targetLabel(t)} — \`octwin projects\` lists them`);
|
|
2734
|
+
if (preview.status !== 200)
|
|
2735
|
+
die(`could not preview the delete (HTTP ${preview.status})${errDetail(preview.json)}${authFailureDetail(preview.status, url)}`);
|
|
2736
|
+
if (flags.json === true && flags.yes !== true) {
|
|
2737
|
+
console.log(JSON.stringify(preview.json, null, 2));
|
|
2738
|
+
return;
|
|
2739
|
+
}
|
|
2740
|
+
// Shapes come from `HardDeletePreview` (routes/_hard-delete-preview.ts) — the same
|
|
2741
|
+
// payload the console's confirm dialog renders, so the two can't disagree about
|
|
2742
|
+
// what a delete costs.
|
|
2743
|
+
const tables = (preview.json?.tables ?? []);
|
|
2744
|
+
const hits = tables.filter(r => r.count > 0);
|
|
2745
|
+
const totals = preview.json?.totals ?? {};
|
|
2746
|
+
console.log(`Deleting project ${slug} from ${targetLabel(t)} destroys:`);
|
|
2747
|
+
if (hits.length === 0)
|
|
2748
|
+
console.log(' (nothing — the project has no rows yet)');
|
|
2749
|
+
for (const r of hits) {
|
|
2750
|
+
const mark = r.disposition === 'cascade' ? '' : ` [${r.disposition}]`;
|
|
2751
|
+
console.log(` ${r.count}${r.capped ? '+' : ''}\t${r.schema}.${r.table}${mark}`);
|
|
2752
|
+
}
|
|
2753
|
+
if (hits.length > 0) {
|
|
2754
|
+
console.log(` — ${totals.rows_deleted}${totals.rows_deleted_capped ? '+' : ''} rows across ${totals.tables_affected} tables`);
|
|
2755
|
+
}
|
|
2756
|
+
// Side effects no FK walk can see (storage blobs, agent memory, Meta registrations).
|
|
2757
|
+
// Anything not `deleted` is what SURVIVES the delete — the part worth reading.
|
|
2758
|
+
const residue = (preview.json?.residue ?? []);
|
|
2759
|
+
const surviving = residue.filter(r => r.disposition !== 'deleted');
|
|
2760
|
+
if (surviving.length > 0) {
|
|
2761
|
+
console.log('\nNot removed by the cascade:');
|
|
2762
|
+
for (const r of surviving)
|
|
2763
|
+
console.log(` [${r.disposition}] ${r.label}${r.count != null ? ` (${r.count})` : ''} — ${r.detail}`);
|
|
2764
|
+
}
|
|
2765
|
+
if (totals.blocked > 0)
|
|
2766
|
+
console.log(`\n! ${totals.blocked} table(s) would BLOCK this delete.`);
|
|
2767
|
+
if (flags.yes !== true) {
|
|
2768
|
+
console.log('\nNothing was deleted. Re-run with --yes to go through with it.');
|
|
2769
|
+
return;
|
|
2770
|
+
}
|
|
2771
|
+
const { status, json } = await apiSend('DELETE', `${url}/api/self/t/projects/${encodeURIComponent(slug)}/hard`, undefined, t);
|
|
2772
|
+
if (status !== 200 && status !== 204)
|
|
2773
|
+
writeFail(`delete project '${slug}'`, status, json, url);
|
|
2774
|
+
console.log(`\n✓ Deleted ${slug}.`);
|
|
2561
2775
|
}
|
|
2562
2776
|
async function cmdAgents(flags) {
|
|
2563
2777
|
if (flags._[0] === 'set')
|
|
@@ -3287,10 +3501,32 @@ const COMMAND_HELP = {
|
|
|
3287
3501
|
projects: `octwin projects [--archived] [--json]
|
|
3288
3502
|
List the workspace's projects — the slugs every --project flag takes, with the
|
|
3289
3503
|
plan's project cap. --archived includes archived ones. A pack:deploy token
|
|
3290
|
-
reaches this (it names a project in every other command)
|
|
3504
|
+
reaches this (it names a project in every other command).
|
|
3505
|
+
|
|
3506
|
+
octwin projects create "<name>" [--slug <slug>] [--pack <packId>]
|
|
3507
|
+
Create a project. The URL slug is derived from the name unless --slug pins one.
|
|
3508
|
+
--pack installs an ALREADY-published pack; the usual next step is instead
|
|
3509
|
+
\`octwin deploy --project <slug>\`, which publishes this working tree and installs it.
|
|
3510
|
+
|
|
3511
|
+
octwin projects rm <slug> [--yes]
|
|
3512
|
+
HARD delete — the project and everything cascading from it (conversations,
|
|
3513
|
+
contacts, records, installs). No undo, and not the same as archiving.
|
|
3514
|
+
WITHOUT --yes it only previews what would be destroyed, so the dry run is the
|
|
3515
|
+
default. Together these make a disposable end-to-end environment:
|
|
3516
|
+
octwin projects create "Scratch" && octwin deploy --project scratch --seed
|
|
3517
|
+
octwin chat "hi" --project scratch
|
|
3518
|
+
octwin projects rm scratch --yes
|
|
3519
|
+
Both verbs need the \`projects:write\` scope — a pack:deploy token does NOT confer it.`,
|
|
3291
3520
|
deploy: `octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
|
|
3292
3521
|
Upload the pack bundle, validate server-side, install onto the project.
|
|
3293
3522
|
--seed additionally applies the pack's demo seed (streams progress).`,
|
|
3523
|
+
seed: `octwin seed [--pack <packId>]
|
|
3524
|
+
Apply the pack's demo/reference data to the project it is installed on, without
|
|
3525
|
+
redeploying: xrm \`demo:\` records + scheduling availability, the commerce catalog,
|
|
3526
|
+
and the demo operator topology. Reports what each kind produced.
|
|
3527
|
+
Idempotent and safe to re-run — records upsert, and existing media is REUSED rather
|
|
3528
|
+
than regenerated, so a second pass costs nothing. --pack is only needed when a
|
|
3529
|
+
project somehow runs more than one.`,
|
|
3294
3530
|
status: `octwin status [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>]
|
|
3295
3531
|
Show installed vs live version + the flow list for this pack.`,
|
|
3296
3532
|
records: `octwin records [entity] [id] [--limit 50] [--offset n]
|
|
@@ -3498,6 +3734,9 @@ async function main() {
|
|
|
3498
3734
|
case 'media':
|
|
3499
3735
|
await cmdMedia(flags);
|
|
3500
3736
|
break;
|
|
3737
|
+
case 'seed':
|
|
3738
|
+
await cmdSeed(flags);
|
|
3739
|
+
break;
|
|
3501
3740
|
case 'projects':
|
|
3502
3741
|
await cmdProjects(flags);
|
|
3503
3742
|
break;
|
package/package.json
CHANGED