@tribe-nest/operator
v0.7.0
Published
tribenest-operator: operator CLI for TribeNest backstage (outreach POC workspaces and code website templates)
Maintainers
Readme
@tribe-nest/operator
tribenest-operator is the operator CLI for TribeNest backstage. It creates, lists and
deletes proof-of-concept (POC) demo workspaces for outreach, against TribeNest or
Mandetara (same codebase, different hosts), and it creates the code website templates
that the tribenest CLI then builds.
It only ever calls /backstage/* endpoints. The HTTP client refuses any other path
before a request is made, so an operator session cannot be pointed at the tenant API.
Installing it gives nobody any access. Every command needs an operator session, and
login only completes for someone who already has a backstage operator account and
approves the login with Google.
npm install -g @tribe-nest/operator
tribenest-operator login --env mandetara --api-url https://api.example.com --backstage-url https://backstage.example.com
tribenest-operator poc create --from batch.json > results.jsonCommands
Every command takes --env <name>. Without it, the configured default env is used,
and without a default, production.
| Command | What it does |
| --- | --- |
| login [--env] [--api-url] [--backstage-url] | Browser approval, then saves an operator session for the env. |
| logout [--env] | Ends the session on the server, then clears it locally even if that call failed. |
| whoami [--env] [--json] | Prints the operator's email and permissions. |
| envs | Lists configured envs. * marks the default. |
| poc create --name <n> --subdomain <s> [--owner-email <e>] [--template <slug>] [--label <l>] [--vertical <edition>] | Creates one POC. --vertical legal-us makes a Mandatera POC, served under mandatera.com. Every POC takes Stripe test payments only, on a test account made by API; Stripe needs about a minute before it can charge. |
| poc create ... --practice-area <name[:price[:minutes]]> [--practice-areas-file <file>] [--timezone <zone>] | One consultation per practice area, sold by the POC's intake and its /book page. See below. |
| poc create ... --feature <switch> [--feature <switch>] | Also turns on profile feature switches for the POC, e.g. --feature voice_agent. Repeatable. |
| poc create --from <batch.json> | Creates many POCs from a JSON array. |
| poc features --list [--json] | Lists the feature switches a POC can carry, with what each does. |
| poc features <subdomain> [--add <switch>] [--remove <switch>] [--json] | Shows a POC's switches, or adds and removes them. --add and --remove repeat. |
| poc list [--older-than <Nd>] [--search <q>] [--json] | Lists every POC, paging through all results. |
| poc setup <subdomain> [--practice-area ...] [--json] | Finishes a legal-us POC's demo setup, repeating only the steps that are not done; adds consultations for new practice areas. |
| poc delete <subdomain> [--yes] | Deletes a POC. Asks for confirmation unless --yes. |
| template create | Asks for a template's name, slug, category, demo business and owner, creates it, then offers to clone it. |
| template create --from <batch.json> | Creates many templates from a JSON array. |
| template clone [slug] [dir] | Clones a template's site with tribenest, to build it. |
| template list [--status] [--json] | Lists code templates. |
| template status [slug] | Whether the site is published and the template is in the gallery. |
| template check [slug] | Whether starting from the template can rewrite all its words: lists any content field the code makes unreadable, with file and line. Exit 1 when any. |
| template tag [slug] --add <a,b> --remove <c> | Adds or removes a template's tags, which a business search matches ("yoga studio" finds a fitness template tagged with it). Merges with the existing tags; refuses on a backend that does not list them. |
| template publish [slug] | Lists a template in the gallery. Refused until its site is published and every field reads. |
| template archive [slug] | Takes a template out of the gallery. Sites made from it are not affected. |
| template categories | Lists categories as a tree: groups and their subcategories. |
| template category create [--parent <group>] | Adds a group, or a subcategory under one. |
| template category sync --from <file> [--dry-run] | Makes the tree match a file of groups and subcategories. Creates and fixes, never deletes. |
template
A code template is a real website on a demo profile, at template-<slug>
(docs/code-website-templates.md). The work is split between the two CLIs:
tribenest-operatorowns the catalogue: the category, the demo profile, the template row and whether it is in the gallery.tribenestbuilds the site, as it builds any website. The demo profile's owner (you, unless you name another account) has the access to clone, push and publish it.
Every template command asks for what it needs, so there are no arguments to
remember. A command that acts on one template shows a numbered list of them. Any
question can still be answered with its flag (--name, --slug, --category,
--demo-name, --demo-subdomain, --owner-email, --description, --tags), which is
how a script runs it without a terminal.
$ tribenest-operator template create
Environment: production ([email protected])
Template name: Brightline
Slug [brightline]:
Category:
1) Coaches
2) Home Services
3) + New category
Enter a number (1-3): 2
Demo business name [Brightline]: Brightline Electric
Demo subdomain [brightline-demo]:
Owner email [[email protected]]:
Description (optional):
Tags, comma separated (optional): electrician, trades
Create it on production? (Y/n)
✓ Created Brightline. Site: template-brightline
Clone it now into ./brightline? (Y/n)Categories are a two-level tree: a group ("Hospitality") and its
subcategories ("Restaurants & Cafes", "Hotels & Stays"). template create asks
for the group, then the subcategory inside it, and can add a new subcategory on
the spot. The tree itself lives in .claude/skills/template-factory/taxonomy.json
and is applied with template category sync.
The handoff. Cloning runs tribenest clone <dir> --env <env> --website template-<slug>
and writes TEMPLATE.md into the checkout: what the template is, the rules that make it
a template, and how it ships. From there it is ordinary tribenest work: seed the demo
profile, tribenest dev, push, publish. Then template check until every content
field reads (starting from the template rewrites them all for the new business, so one
the code hides would keep its demo words), and template publish lists it in the
gallery.
A batch file is a JSON array; demoSubdomain defaults to <slug>-demo, and every
category must already exist:
[
{ "name": "Brightline", "slug": "brightline", "category": "home_services", "demoName": "Brightline Electric", "tags": ["electrician"] },
{ "name": "Ledger", "slug": "ledger", "category": "professional_services", "ownerEmail": "[email protected]" }
]Re-running a batch is safe: a slug that already exists is reported with
created: false and left alone.
poc create
A batch file is a JSON array:
[
{ "name": "Smith & Lee Law", "subdomain": "smithlee-demo", "templateSlug": "mandetara-poc", "demoLabel": "wave-1" },
{ "name": "Garcia Immigration", "subdomain": "garcia-demo", "ownerEmail": "[email protected]" },
{ "name": "Reyes Law", "subdomain": "reyes-demo", "vertical": "legal-us", "features": ["voice_agent", "telephony"] }
]features is an optional array of profile feature switch names. A malformed list (not an
array, an empty name, more than 20 names) stops the whole batch before anything is sent.
A name the server does not know comes back as a 400 that lists the valid names; run
poc features --list to see them first.
Each entry is validated locally first: name is required, and subdomain must be 4 to
50 characters, match ^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$ and end in -demo. An
invalid entry is reported as failed and never sent.
Entries are created one at a time and the batch continues past failures. Creation is
idempotent by subdomain on the server, so re-running a batch is safe: existing POCs come
back with created: false.
If the server answers 429 (the daily POC cap), the batch stops there and every remaining entry is reported as failed with that reason. An expired session that cannot be refreshed also stops the batch.
Output. With --json (the default when --from is used; --no-json turns it off),
stdout carries exactly ONE JSON array, one entry per input, in input order:
[
{ "ok": true, "profileId": "...", "subdomain": "smithlee-demo", "websiteId": "...", "websiteVersionId": "...", "url": "https://smithlee-demo...", "created": true },
{ "ok": false, "subdomain": "garcia-demo", "status": 409, "error": "..." }
]With features, each ok entry also carries
"featureSwitches": [{ "name": "voice_agent", "assigned": true, "switchEnabled": true }].
Without --json, the line ends features: voice_agent, telephony (switch off platform-wide).
switchEnabled: false means the POC is enrolled but the switch is off for the whole
platform, so the feature stays off until an operator turns the switch on in backstage.
assigned: false means that one switch failed; the POC itself was made, and running the
create again (or poc features <subdomain> --add) finishes it.
status is null for a local validation failure or a network error. Progress and logs
go to stderr only, so stdout can be piped straight into another tool. The exit code is
1 if any entry failed (or a setup step failed, below), else 0.
A legal-us POC is a working demo straight away. The server sets it up in the same
call and reports each step under setup: the website agent on the owner's plan, demo
credits, voice_agent (no --feature needed), the Stripe test account (pending until
Stripe switches payments on, about a minute), one consultation per practice area (below;
without any, one "Initial consultation", 30 minutes, USD 150), the "New client intake"
playbook (conflict check, then the matching consultation through Book or buy), the
"Front office" agent on the website (chat and "Call us"), and a demo matter, "Demo:
Smith v. Jordan", so a conflict can be shown by naming Jamie Jordan. Without --json the
line ends setup: done, or setup: 6/8 done followed by one line per step that is not
done and its reason.
Practice areas. Each becomes a booking product " consultation" with weekday availability in the firm's time zone, and the intake sells all of them:
tribenest-operator poc create --vertical legal-us --name "The Town Law LLC" --subdomain townlaw-demo \
--timezone America/New_York \
--practice-area "Criminal Defense:0" --practice-area "DUI Defense:150:30" --practice-area "Gun Charges (VUFA):200"--practice-area "Name[:price[:minutes]]" repeats: the price is in dollars (0 is a free
consultation, the default is 150) and the length in minutes (15 to 120, default 30). Or
--practice-areas-file firm.json, either a list of { name, priceCents?, durationMinutes?,
description? } (or plain names) or { "practiceAreas": [...], "timezone": "...", "hours":
[{ "dayOfWeek": 1, "from": "09:00", "to": "18:00" }] } (0 is Sunday; default Monday to
Friday, 9:00 to 17:00). 1 to 12 areas. In a --from batch, put practiceAreas,
timezone and hours on each entry.
poc setup
tribenest-operator poc setup reyes-demo
tribenest-operator poc setup townlaw-demo --practice-areas-file firm.jsonRuns every setup step again and makes only what is missing, so it is safe to repeat:
after a step failed, or a minute after create to see Stripe's pending turn done.
It takes the same practice area flags: an area not there yet gets its consultation and
the intake is published again to sell it; an area left out of the list keeps its
consultation (the service line names it). One line per step (done, pending,
failed, skipped) with the reason when it is not done, and the consultations on the
service line. Exit code 1 when a step failed. A POC that is not legal-us has no setup
and says so.
poc list
--older-than 30d (or 30) only lists POCs created more than that many days ago.
--json prints the full array of { profileId, name, subdomain, url, ownerEmail, createdAt, websiteVersionId, featureSwitches }.
poc features
tribenest-operator poc features --list
tribenest-operator poc features reyes-demo
tribenest-operator poc features reyes-demo --add voice_agent --add telephony --remove film_rentalsOnly POCs are reachable: any other subdomain is "No POC with the subdomain". Adding a switch the POC already has, or removing one it does not, changes nothing.
poc delete
Prompts for confirmation on a terminal. In a non-interactive shell it refuses unless
--yes is passed. A 404 is reported as "No POC with the subdomain".
Config
~/.tribenest-operator/config.json, written with mode 0600 because it holds session
tokens (set TRIBENEST_OPERATOR_CONFIG_DIR to use another directory):
{
"defaultEnv": "mandetara",
"envs": {
"mandetara": {
"apiUrl": "https://api.example.com",
"backstageUrl": "https://backstage.example.com",
"token": "...",
"refreshToken": "...",
"operatorEmail": "[email protected]"
}
}
}Env presets:
| Env | API | Backstage |
| --- | --- | --- |
| production | https://api.tribenest.co | https://backstage.tribenest.co |
| local | http://localhost:8000 | http://localhost:5174 |
Any other env (for example mandetara) has no preset: pass --api-url and
--backstage-url on its first login. Later logins reuse the saved URLs.
The login flow
Operators sign in to backstage with Google only, so the CLI never handles a password.
- The CLI starts an HTTP server on
127.0.0.1on a random free port and makes a randomstate. - It opens the browser (and prints the URL) at
${backstageUrl}/cli-auth?port=<port>&state=<state>. - The operator, already signed in to backstage, approves. Backstage redirects to
http://127.0.0.1:<port>/callback?code=<code>&state=<state>. - The CLI checks
state, shows a "you can close this tab" page, then callsPOST ${apiUrl}/backstage/cli-auth/exchangewith{ code }and receives{ token, refreshToken }. Only a one-time code ever appears in a URL. - It saves the session, makes the env the default, calls
GET /backstage/auth/meand prints who is logged in.
The CLI gives up after 5 minutes without a callback.
Sessions. Every request sends Authorization: Bearer <token>. On a 401 the CLI
refreshes once with POST /backstage/auth/refresh { refreshToken }, saves the new
{ token, refreshToken }, and retries the request once. If the refresh fails, it tells
you to run tribenest-operator login --env <env>.
HTTP calls
| Command | Call |
| --- | --- |
| login | POST /backstage/cli-auth/exchange { code } (no auth), then GET /backstage/auth/me |
| logout | DELETE /backstage/auth/session |
| whoami | GET /backstage/auth/me |
| poc create | POST /backstage/pocs { name, subdomain, ownerEmail?, templateSlug?, demoLabel?, vertical?, featureSwitches?, practiceAreas?, timezone?, hours? } per entry |
| poc list | GET /backstage/pocs?page&limit&search&olderThanDays, page by page |
| poc features --list | GET /backstage/pocs/feature-switches |
| poc features <subdomain> | GET /backstage/pocs/<subdomain> |
| poc features <subdomain> --add/--remove | PUT /backstage/pocs/<subdomain>/feature-switches { add?, remove? } |
| poc setup <subdomain> | POST /backstage/pocs/<subdomain>/setup { practiceAreas?, timezone?, hours? } |
| template create | GET /backstage/templates/categories, POST /backstage/templates/categories for a new one, POST /backstage/templates/code { slug, name, categoryId, description, tags, demo: { name, subdomain, ownerEmail } } |
| template list / pickers | GET /backstage/templates?kind=code&status |
| template status / clone | GET /backstage/templates/:id/site |
| template check | GET /backstage/templates/:id/field-check |
| template tag | GET /backstage/templates (current tags), PATCH /backstage/templates/:id/metadata |
| template publish | GET /backstage/templates/:id/site and /field-check, then POST /backstage/templates/:id/publish |
| template archive | POST /backstage/templates/:id/archive |
| poc delete | DELETE /backstage/pocs/<subdomain> |
| any, on 401 | POST /backstage/auth/refresh { refreshToken } |
Development
npm run build # tsup, single ESM bundle in dist/
npm test # vitest, fetch is stubbed
npm run check-types