@lahat-group/cli 2.1.3 → 2.1.4
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 +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +60 -55
- package/skill/staff.md +59 -0
package/dist/index.js
CHANGED
|
@@ -86,5 +86,5 @@ To authorize this CLI, visit:
|
|
|
86
86
|
`);for(let s of i.warnings)process.stdout.write(`warning: ${s}
|
|
87
87
|
`)})}u();import{fileURLToPath as Qn}from"url";import{dirname as en,join as N}from"path";import{homedir as Xn}from"os";import{existsSync as Zn,mkdirSync as er,readdirSync as tr,copyFileSync as nr,statSync as rr}from"fs";function ir(){let r=en(Qn(import.meta.url));for(let e=0;e<6;e++){let t=N(r,"skill");if(Zn(N(t,"SKILL.md")))return t;r=en(r)}throw new Error("Bundled SKILL.md not found next to the CLI package")}function or(r){let e=r?N(process.cwd(),".claude"):N(Xn(),".claude");return N(e,"skills","wardrobe-configurator")}function sr(r={}){let e=r.sourceDir??ir(),t=r.targetDir??or(!!r.project);er(t,{recursive:!0});let n=[];for(let i of tr(e)){let o=N(e,i);rr(o).isFile()&&(nr(o,N(t,i)),n.push(i))}return{target:t,files:n}}function tn(r){let e=sr({project:r.project});h(e,!!r.json,()=>{process.stdout.write(`Installed ${e.files.length} file(s) to ${e.target}
|
|
88
88
|
`),process.stdout.write(`Restart Claude Code, then invoke /wardrobe-configurator
|
|
89
|
-
`)})}u();async function ar(r){if(!(r instanceof O))return null;try{return await r.response.json()}catch{return null}}async function nn(r,e){let t=await ar(r);if(e)return JSON.stringify(t?{error:t.error??"request_failed",...t}:{error:r.message},null,2);if(!t)return`Error: ${r.message}`;let n=[t.error??"Request failed"];return t.detail&&n.push(t.detail),t.fix_hint&&n.push(`fix: ${t.fix_hint}`),t.issues&&n.push(JSON.stringify(t.issues)),`Error: ${n.join(" \u2014 ")}`}u();var rn="2.1.3";function ee(r){return r.option("--assembly","buy assembly for this design").option("--no-assembly","do not buy assembly").option("--transport","buy transport for this design").option("--no-transport","do not buy transport").option("--measurement","buy a measurement visit for this design").option("--no-measurement","do not buy a measurement visit")}b.name("wardrobe").description("Wardrobe configurator CLI").version(rn);b.command("login").description("Authenticate via the OAuth device flow").option("--json","machine-readable output").action(r=>jt(r));b.command("whoami").description("Show the authenticated user").option("--json","machine-readable output").action(r=>vt(r));b.command("logout").description("Remove the stored agent key").option("--json","machine-readable output").action(r=>Ot(r));b.command("init").description("Emit a fresh, valid WardrobeConfig JSON (via the API)").option("--width <cm>","total width in cm, as measured on the wall (gross; a niche: its opening)",r=>Number(r)).option("--height <cm>","wardrobe height in cm (the ceiling follows, 3 cm above)",r=>Number(r)).option("--ceiling <cm>","room ceiling height in cm (the wardrobe stands 3 cm under it)",r=>Number(r)).option("--depth <cm>","depth in cm, doors included (default 60)",r=>Number(r)).option("--position <pos>","right-corner (default) | left-corner | niche | freestanding").action(r=>yt(r));b.command("validate <file>").description("Validate a config against the live rules (file or - for stdin)").option("--json","machine-readable output").action((r,e)=>wt(r,e));b.command("describe <idOrFile>").description("Describe a config (local file/-) or saved design (id), via the API").option("--locale <locale>","en or he","en").option("--json","machine-readable output").action((r,e)=>_t(r,e));b.command("catalog <kind>").description(`List what a config may name, from the live catalogs: ${ke.join(" | ")}. Read this before choosing a colour, handle or aluminium front \u2014 never invent ids`).option("--json","machine-readable output").action((r,e)=>Yt(r,e));b.command("prompt <projectId> <text>").description("Describe a wardrobe in words (he/en) and get a draft config file + the assumptions made. Saves nothing; counts against a daily per-key limit; takes 10-30 s").option("--locale <locale>","he or en","he").option("--out <file>","where to write the draft config (default ./prompt-<timestamp>.json)").option("--json","print the whole response (config, assumptions, adjustments, warnings) instead of writing a file").action((r,e,t)=>Zt(r,e,t));b.command("price <file>").description("Price a config via the API (file or - for stdin)").option("--json","machine-readable output").action((r,e)=>Vt(r,e));b.command("plan <file>").description("Dry-run: price a config + BOM preview (mutates nothing)").option("--json","machine-readable output").action((r,e)=>Bt(r,e));ee(b.command("apply <file>").description("Save a config as a design (file or - for stdin)").requiredOption("--project <id>","project id").option("--name <name>","design name").option("--json","machine-readable output")).action((r,e)=>Pe(r,e));var I=b.command("designs").description("Manage saved designs");I.command("list").description("List designs in a project, or the newest across every project with --project omitted").option("--project <id>","project id").option("--limit <n>","cross-project mode only: max rows (default 20, max 100)",r=>Number(r)).option("--since <iso>","cross-project mode only: only designs updated at/after this ISO 8601 timestamp").option("--json","machine-readable output").action(r=>Ft(r));ee(I.command("create <file>").description("Save a config as a new design in a project (same as `apply`)").requiredOption("--project <id>","project id").option("--name <name>","design name").option("--json","machine-readable output")).action((r,e)=>Pe(r,e));I.command("get <id>").description("Show one saved design with its stored config \u2014 read this before `designs update`, which replaces the whole config").option("--config","print only the bare config: exactly what `designs update --file` accepts").option("--json","machine-readable output: id, projectId, name, version, nisTotal, serviceTotal, config").action((r,e)=>Tt(r,e));ee(I.command("update <id>").description("Revise a saved design (config, name and/or services). Services you do not mention keep their current value").option("--file <file>","new config JSON (file or - for stdin)").option("--name <name>","new design name").option("--json","machine-readable output")).action((r,e)=>Ut(r,e));ee(I.command("duplicate <id>").description("Copy a design (into the same project unless --project is given). Services are not copied \u2014 pass the flags to set them").option("--project <id>","target project (default: the source design's project)").option("--name <name>",'name of the copy (default: "<name> \u05E2\u05D5\u05EA\u05E7")').option("--json","machine-readable output")).action((r,e)=>zt(r,e));I.command("delete <id>").description("Archive a design (soft delete; there is no restore in the app). Requires --yes").option("--yes","confirm the archive").option("--json","machine-readable output").action((r,e)=>Jt(r,e));I.command("thumbnail <id>").description("Download a design's thumbnail image; renders it on the server first if none exists yet").option("--out <file>","destination path").option("--render","re-render on the server even if a thumbnail exists").option("--json","machine-readable output").action((r,e)=>Nt(r,e));var Re=b.command("projects").description("Manage projects");Re.command("list").description("List accessible projects").option("--phone <number>","filter by the customer's phone number").option("--designer <idOrName>","filter by the designer who owns the project").option("--json","machine-readable output").action(r=>Mt(r));Re.command("get <id>").description("Show one project").option("--json","machine-readable output").action((r,e)=>$t(r,e));Re.command("members <id>").description("List a project's owner and members").option("--json","machine-readable output").action((r,e)=>Ct(r,e));var Te=b.command("customers").description("Read customers");Te.command("list").description("List your customers").option("--json","machine-readable output").action(r=>At(r));Te.command("get <id>").description("Show one customer").option("--json","machine-readable output").action((r,e)=>St(r,e));Te.command("projects <id>").description("List a customer's projects").option("--json","machine-readable output").action((r,e)=>Et(r,e));var te=b.command("orders").description("Read orders, or move one along the workflow (orders:transition, office accounts only)");te.command("list").description("List a project's orders, or the newest-updated across every project with --project omitted").option("--project <id>","project id").option("--open","only the project's active (non-cancelled) order (requires --project)").option("--limit <n>","cross-project mode only: max rows (default 20, max 100)",r=>Number(r)).option("--since <iso>","cross-project mode only: only orders updated at/after this ISO 8601 timestamp").option("--json","machine-readable output").action(r=>Pt(r));te.command("get <id>").description("Show one order, with its designs embedded").option("--json","machine-readable output").action((r,e)=>kt(r,e));te.command("pdf <id>").description("Download an order PDF").option("--out <file>","destination path").option("--json","machine-readable output").action((r,e)=>Rt(r,e));te.command("set-status <id> <status>").description("Move an order to its next status (e.g. paid, pending_assembly); only legal steps, never cancelled").option("--dry-run","check the step is allowed without changing anything").option("--note <text>","kept on the order's status history (e.g. a receipt number)").option("--json","machine-readable output").action((r,e,t)=>Gt(r,e,t));var qe=b.command("quotations").description("Read quotations (quotations:read), or mint a revision (quotations:write)");qe.command("list").description("Show a project's quote history, or the newest-generated across every project with --project omitted").option("--project <id>","project id").option("--limit <n>","cross-project mode only: max rows (default 20, max 100)",r=>Number(r)).option("--since <iso>","cross-project mode only: only quotations generated at/after this ISO 8601 timestamp").option("--json","machine-readable output").action(r=>Lt(r));qe.command("pdf <projectId>").description("Download a quotation PDF (default: the project's current revision) \u2014 never mints a new one").option("--revision <n>","a specific historical revision instead of the current one",r=>Number(r)).option("--out <file>","destination path").option("--json","machine-readable output").action((r,e)=>qt(r,e));qe.command("mint <projectId>").description("File the project's quotation at its current price and download the PDF \u2014 unlike `pdf`, this can mint a new revision (requires quotations:write)").option("--out <file>","destination path").option("--json","machine-readable output: path, bytes, quoteNumber, revision, minted").action((r,e)=>Wt(r,e));var on=b.command("users").description("Read registered accounts (requires staff:read)");on.command("list").description("List registered users, newest-registered first").option("--limit <n>","max rows (default 20, max 100)",r=>Number(r)).option("--since <iso>","only users registered at/after this ISO 8601 timestamp").option("--before <iso>","only users registered before this ISO 8601 timestamp (page back with the last row's createdAt)").option("--role <role>","only users with this role: admin, support, designer, client").option("--json","machine-readable output").action(r=>It(r));on.command("by-phone <phone>").description("Find the account(s) whose own phone is this number (+972\u2026 and 05\u2026 forms match)").option("--json","machine-readable output").action((r,e)=>Dt(r,e));b.command("help-texts").description("Read the help-text table").option("--json","machine-readable output").action(r=>Ht(r));var cr=b.command("skill").description("Manage the bundled Claude Skill");cr.command("install").description("Install the wardrobe-configurator Skill for Claude Code").option("--project","install into ./.claude/skills instead of ~/.claude/skills").option("--json","machine-readable output").action(r=>tn(r));b.parseAsync().catch(async r=>{let e=await nn(r,process.argv.includes("--json"));process.stderr.write(`${e}
|
|
89
|
+
`)})}u();async function ar(r){if(!(r instanceof O))return null;try{return await r.response.json()}catch{return null}}async function nn(r,e){let t=await ar(r);if(e)return JSON.stringify(t?{error:t.error??"request_failed",...t}:{error:r.message},null,2);if(!t)return`Error: ${r.message}`;let n=[t.error??"Request failed"];return t.detail&&n.push(t.detail),t.fix_hint&&n.push(`fix: ${t.fix_hint}`),t.issues&&n.push(JSON.stringify(t.issues)),`Error: ${n.join(" \u2014 ")}`}u();var rn="2.1.4";function ee(r){return r.option("--assembly","buy assembly for this design").option("--no-assembly","do not buy assembly").option("--transport","buy transport for this design").option("--no-transport","do not buy transport").option("--measurement","buy a measurement visit for this design").option("--no-measurement","do not buy a measurement visit")}b.name("wardrobe").description("Wardrobe configurator CLI").version(rn);b.command("login").description("Authenticate via the OAuth device flow").option("--json","machine-readable output").action(r=>jt(r));b.command("whoami").description("Show the authenticated user").option("--json","machine-readable output").action(r=>vt(r));b.command("logout").description("Remove the stored agent key").option("--json","machine-readable output").action(r=>Ot(r));b.command("init").description("Emit a fresh, valid WardrobeConfig JSON (via the API)").option("--width <cm>","total width in cm, as measured on the wall (gross; a niche: its opening)",r=>Number(r)).option("--height <cm>","wardrobe height in cm (the ceiling follows, 3 cm above)",r=>Number(r)).option("--ceiling <cm>","room ceiling height in cm (the wardrobe stands 3 cm under it)",r=>Number(r)).option("--depth <cm>","depth in cm, doors included (default 60)",r=>Number(r)).option("--position <pos>","right-corner (default) | left-corner | niche | freestanding").action(r=>yt(r));b.command("validate <file>").description("Validate a config against the live rules (file or - for stdin)").option("--json","machine-readable output").action((r,e)=>wt(r,e));b.command("describe <idOrFile>").description("Describe a config (local file/-) or saved design (id), via the API").option("--locale <locale>","en or he","en").option("--json","machine-readable output").action((r,e)=>_t(r,e));b.command("catalog <kind>").description(`List what a config may name, from the live catalogs: ${ke.join(" | ")}. Read this before choosing a colour, handle or aluminium front \u2014 never invent ids`).option("--json","machine-readable output").action((r,e)=>Yt(r,e));b.command("prompt <projectId> <text>").description("Describe a wardrobe in words (he/en) and get a draft config file + the assumptions made. Saves nothing; counts against a daily per-key limit; takes 10-30 s").option("--locale <locale>","he or en","he").option("--out <file>","where to write the draft config (default ./prompt-<timestamp>.json)").option("--json","print the whole response (config, assumptions, adjustments, warnings) instead of writing a file").action((r,e,t)=>Zt(r,e,t));b.command("price <file>").description("Price a config via the API (file or - for stdin)").option("--json","machine-readable output").action((r,e)=>Vt(r,e));b.command("plan <file>").description("Dry-run: price a config + BOM preview (mutates nothing)").option("--json","machine-readable output").action((r,e)=>Bt(r,e));ee(b.command("apply <file>").description("Save a config as a design (file or - for stdin)").requiredOption("--project <id>","project id").option("--name <name>","design name").option("--json","machine-readable output")).action((r,e)=>Pe(r,e));var I=b.command("designs").description("Manage saved designs");I.command("list").description("List designs in a project, or the newest across every project with --project omitted").option("--project <id>","project id").option("--limit <n>","cross-project mode only: max rows (default 20, max 100)",r=>Number(r)).option("--since <iso>","cross-project mode only: only designs updated at/after this ISO 8601 timestamp").option("--json","machine-readable output").action(r=>Ft(r));ee(I.command("create <file>").description("Save a config as a new design in a project (same as `apply`)").requiredOption("--project <id>","project id").option("--name <name>","design name").option("--json","machine-readable output")).action((r,e)=>Pe(r,e));I.command("get <id>").description("Show one saved design with its stored config \u2014 read this before `designs update`, which replaces the whole config").option("--config","print only the bare config: exactly what `designs update --file` accepts").option("--json","machine-readable output: id, projectId, name, version, nisTotal, serviceTotal, config").action((r,e)=>Tt(r,e));ee(I.command("update <id>").description("Revise a saved design (config, name and/or services). Services you do not mention keep their current value").option("--file <file>","new config JSON (file or - for stdin)").option("--name <name>","new design name").option("--json","machine-readable output")).action((r,e)=>Ut(r,e));ee(I.command("duplicate <id>").description("Copy a design (into the same project unless --project is given). Services are not copied \u2014 pass the flags to set them").option("--project <id>","target project (default: the source design's project)").option("--name <name>",'name of the copy (default: "<name> \u05E2\u05D5\u05EA\u05E7")').option("--json","machine-readable output")).action((r,e)=>zt(r,e));I.command("delete <id>").description("Archive a design (soft delete; there is no restore in the app). Requires --yes").option("--yes","confirm the archive").option("--json","machine-readable output").action((r,e)=>Jt(r,e));I.command("thumbnail <id>").description("Download a design's thumbnail image; renders it on the server first if none exists yet").option("--out <file>","destination path").option("--render","re-render on the server even if a thumbnail exists").option("--json","machine-readable output").action((r,e)=>Nt(r,e));var Re=b.command("projects").description("Manage projects");Re.command("list").description("List accessible projects").option("--phone <number>","filter by the customer's phone number").option("--designer <idOrName>","filter by the designer who owns the project").option("--json","machine-readable output").action(r=>Mt(r));Re.command("get <id>").description("Show one project").option("--json","machine-readable output").action((r,e)=>$t(r,e));Re.command("members <id>").description("List a project's owner and members").option("--json","machine-readable output").action((r,e)=>Ct(r,e));var Te=b.command("customers").description("Read customers");Te.command("list").description("List your customers").option("--json","machine-readable output").action(r=>At(r));Te.command("get <id>").description("Show one customer").option("--json","machine-readable output").action((r,e)=>St(r,e));Te.command("projects <id>").description("List a customer's projects").option("--json","machine-readable output").action((r,e)=>Et(r,e));var te=b.command("orders").description("Read orders, or move one along the workflow (orders:transition, office accounts only)");te.command("list").description("List a project's orders, or the newest-updated across every project with --project omitted").option("--project <id>","project id").option("--open","only the project's active (non-cancelled) order (requires --project)").option("--limit <n>","cross-project mode only: max rows (default 20, max 100)",r=>Number(r)).option("--since <iso>","cross-project mode only: only orders updated at/after this ISO 8601 timestamp").option("--json","machine-readable output").action(r=>Pt(r));te.command("get <id>").description("Show one order, with its designs embedded").option("--json","machine-readable output").action((r,e)=>kt(r,e));te.command("pdf <id>").description("Download an order PDF").option("--out <file>","destination path").option("--json","machine-readable output").action((r,e)=>Rt(r,e));te.command("set-status <id> <status>").description("Move an order to its next status (e.g. paid, pending_assembly); only legal steps, never cancelled").option("--dry-run","check the step is allowed without changing anything").option("--note <text>","kept on the order's status history (e.g. a receipt number)").option("--json","machine-readable output").action((r,e,t)=>Gt(r,e,t));var qe=b.command("quotations").description("Read quotations (quotations:read), or mint a revision (quotations:write)");qe.command("list").description("Show a project's quote history, or the newest-generated across every project with --project omitted").option("--project <id>","project id").option("--limit <n>","cross-project mode only: max rows (default 20, max 100)",r=>Number(r)).option("--since <iso>","cross-project mode only: only quotations generated at/after this ISO 8601 timestamp").option("--json","machine-readable output").action(r=>Lt(r));qe.command("pdf <projectId>").description("Download a quotation PDF (default: the project's current revision) \u2014 never mints a new one").option("--revision <n>","a specific historical revision instead of the current one",r=>Number(r)).option("--out <file>","destination path").option("--json","machine-readable output").action((r,e)=>qt(r,e));qe.command("mint <projectId>").description("File the project's quotation at its current price and download the PDF \u2014 unlike `pdf`, this can mint a new revision (requires quotations:write)").option("--out <file>","destination path").option("--json","machine-readable output: path, bytes, quoteNumber, revision, minted").action((r,e)=>Wt(r,e));var on=b.command("users").description("Read registered accounts (requires staff:read)");on.command("list").description("List registered users, newest-registered first").option("--limit <n>","max rows (default 20, max 100)",r=>Number(r)).option("--since <iso>","only users registered at/after this ISO 8601 timestamp").option("--before <iso>","only users registered before this ISO 8601 timestamp (page back with the last row's createdAt)").option("--role <role>","only users with this role: admin, support, designer, client").option("--json","machine-readable output").action(r=>It(r));on.command("by-phone <phone>").description("Find the account(s) whose own phone is this number (+972\u2026 and 05\u2026 forms match)").option("--json","machine-readable output").action((r,e)=>Dt(r,e));b.command("help-texts").description("Read the help-text table").option("--json","machine-readable output").action(r=>Ht(r));var cr=b.command("skill").description("Manage the bundled Claude Skill");cr.command("install").description("Install the wardrobe-configurator Skill for Claude Code").option("--project","install into ./.claude/skills instead of ~/.claude/skills").option("--json","machine-readable output").action(r=>tn(r));b.parseAsync().catch(async r=>{let e=await nn(r,process.argv.includes("--json"));process.stderr.write(`${e}
|
|
90
90
|
`),process.exit(1)});
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: wardrobe-configurator
|
|
3
|
-
description: Use when
|
|
3
|
+
description: Use when a designer (or office staff) wants to design, price, revise or quote a Lahat Pro wardrobe/closet from the terminal — phrases like "design a wardrobe for my customer", "a 2.4 m wardrobe with drawers", "price this design", "make it 3 m wide", "send the customer a quote", "validate my wardrobe config" — OR to look up their own projects, customers, designs, orders and quotations and their PDFs, e.g. "find the customer with phone 05x", "who is on this project", "what is the status of this order", "download the order PDF". Office staff keys can also look up registered users and other designers' projects (staff.md). Drives the `wardrobe` CLI (HTTP client over the configurator API); a WardrobeConfig JSON file on disk is the unit of work for design verbs, and every lookup verb is read-only.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Wardrobe Configurator CLI
|
|
@@ -9,6 +9,11 @@ Build, validate, price, save, and stage wardrobe designs from the terminal via t
|
|
|
9
9
|
`wardrobe` CLI. A **`WardrobeConfig` JSON file on disk is the unit of work**. The
|
|
10
10
|
agent loop is: **init → edit → validate → price → plan → apply**.
|
|
11
11
|
|
|
12
|
+
**Start with `wardrobe whoami --json`.** It tells you who you work for (`role`) and what
|
|
13
|
+
the key may do (`agent.scopes`). This file is everything a designer needs. If the scopes
|
|
14
|
+
include `staff:read` or `orders:transition`, the key belongs to office staff — also read
|
|
15
|
+
[staff.md](staff.md) for the extra verbs; otherwise don't reach for them.
|
|
16
|
+
|
|
12
17
|
## Never invent an id — read the catalog
|
|
13
18
|
|
|
14
19
|
A config names colours, a handle and (optionally) aluminium door fronts by **id**, and
|
|
@@ -26,6 +31,11 @@ quietly substituting a default colour. So before choosing one, read the live lis
|
|
|
26
31
|
Old ids you may remember (`white-melamine`, `oak-veneer`, `interior-eco-white`) are
|
|
27
32
|
retired — they are not in `catalog materials`, so don't use them.
|
|
28
33
|
|
|
34
|
+
**Ask for the exterior colour and the handle** before you save a design for a customer.
|
|
35
|
+
They are the two visible choices with no business default: left out, the design gets the
|
|
36
|
+
catalog's placeholder colour (turquoise) and whichever handle the catalog marks
|
|
37
|
+
`(default)`, which is rarely what anyone wanted.
|
|
38
|
+
|
|
29
39
|
## Prerequisites
|
|
30
40
|
|
|
31
41
|
- Install the CLI: `npm install -g @lahat-group/cli` (or run ad-hoc via `npx @lahat-group/cli <verb>`).
|
|
@@ -42,6 +52,21 @@ retired — they are not in `catalog materials`, so don't use them.
|
|
|
42
52
|
(e.g. `http://localhost:3001/api` for a local dev server).
|
|
43
53
|
- Every verb accepts `--json` for machine-readable output. Read those; don't scrape prose.
|
|
44
54
|
|
|
55
|
+
## Where a design goes — customers, projects and your Play room
|
|
56
|
+
|
|
57
|
+
Every design is saved into a **project**, and every project belongs to a **customer**.
|
|
58
|
+
`wardrobe projects list` shows yours with each one's customer; `--phone <number>` finds a
|
|
59
|
+
customer's project by their number.
|
|
60
|
+
|
|
61
|
+
- **The CLI cannot create a customer or a project.** For a new customer, ask the user to
|
|
62
|
+
add the customer in the web app and open them (that gives the customer their project),
|
|
63
|
+
then `wardrobe projects list` to get its id.
|
|
64
|
+
- **Your Play room** is the project whose customer is you (your own name on both). It is a
|
|
65
|
+
sandbox for trying things out — save experiments there, never a real customer's design,
|
|
66
|
+
and archive what you no longer need.
|
|
67
|
+
- A customer invited into a project is a **member** (`wardrobe projects members <id>`) and
|
|
68
|
+
sees every design saved there. Don't save drafts or experiments into a customer's project.
|
|
69
|
+
|
|
45
70
|
## The loop
|
|
46
71
|
|
|
47
72
|
1. **init** — `wardrobe init [--width <cm>] [--position <pos>] [--height <cm> | --ceiling <cm>] [--depth <cm>] > wardrobe.json`
|
|
@@ -54,7 +79,7 @@ retired — they are not in `catalog materials`, so don't use them.
|
|
|
54
79
|
so a config `validate` approves can never be one `price` rejects.
|
|
55
80
|
Exit 0 = valid; exit 1 + structured errors otherwise. Do this after every edit.
|
|
56
81
|
4. **price** — `wardrobe price wardrobe.json` → authoritative total in **NIS** from
|
|
57
|
-
the API's unified area×rate engine.
|
|
82
|
+
the API's unified area×rate engine — **before VAT** (see below).
|
|
58
83
|
5. **plan** — `wardrobe plan wardrobe.json` → dry-run price **plus a BOM preview**.
|
|
59
84
|
Mutates nothing. Use this to preview before applying.
|
|
60
85
|
6. **apply** — `wardrobe apply wardrobe.json --project <id> [--name <name>]` → saves
|
|
@@ -62,6 +87,17 @@ retired — they are not in `catalog materials`, so don't use them.
|
|
|
62
87
|
required.** `wardrobe designs create <file> --project <id>` is the same verb under
|
|
63
88
|
the `designs` group, if that reads more clearly in context.
|
|
64
89
|
|
|
90
|
+
### Prices are before VAT
|
|
91
|
+
|
|
92
|
+
Every number the CLI prints — `price`, `plan`, `designs get` (`nisTotal`, `serviceTotal`)
|
|
93
|
+
— is **before VAT**. The web app, the quotation PDF and the customer all see it **with
|
|
94
|
+
VAT, rounded to whole shekels**: `round(price × (1 + vatRate))`, where `vatRate` comes
|
|
95
|
+
from `wardrobe catalog service-pricing` (0.18 today). A design that prices at 5,052.94
|
|
96
|
+
here shows as **5,962** on the website. When you tell the user a price, say which one it
|
|
97
|
+
is — and use the VAT-inclusive figure for anything the customer will see. Services
|
|
98
|
+
(`serviceTotal`) are separate and also before VAT; the project total the customer pays is
|
|
99
|
+
the designs plus the services they bought, with VAT.
|
|
100
|
+
|
|
65
101
|
### Lahat Pro defaults — apply them unless the user says otherwise
|
|
66
102
|
|
|
67
103
|
`init`, `wardrobe prompt` and the web wizard all start from these; keep to them when you
|
|
@@ -92,7 +128,7 @@ If the user describes what they want instead of giving dimensions —
|
|
|
92
128
|
"a 2.4 m wardrobe, drawers in the middle, glass doors on the first column" — use
|
|
93
129
|
`wardrobe prompt <projectId> "<their words>" [--locale he|en] [--out draft.json]`. It
|
|
94
130
|
writes a draft config to a file (default `./prompt-<timestamp>.json`) and prints the
|
|
95
|
-
**assumptions** it made (e.g. "assumed
|
|
131
|
+
**assumptions** it made (e.g. "assumed a right corner"), plus any adjustments and warnings.
|
|
96
132
|
Saves nothing. Then: **show the user the assumptions and ask them to confirm** → `validate`
|
|
97
133
|
→ `price` → `apply`. Treat the draft as a starting point, never as a finished design.
|
|
98
134
|
|
|
@@ -143,8 +179,8 @@ Two refusals to expect, neither of which is retryable:
|
|
|
143
179
|
user's intent on top, and tell them it changed underneath you. **Never** loop.
|
|
144
180
|
- **`423 projectLocked`** — the project has a live (non-cancelled) order, which froze
|
|
145
181
|
its designs. No design in it can be created, updated or deleted, and no new quotation
|
|
146
|
-
revision minted, until a human cancels that order —
|
|
147
|
-
|
|
182
|
+
revision minted, until a human cancels that order — whoever's key it is. Say so; don't
|
|
183
|
+
try another design or another verb. (Check
|
|
148
184
|
`wardrobe orders list --project <id> --open` first to tell the user *why* before trying.)
|
|
149
185
|
|
|
150
186
|
Note that updating a config **clears the design's thumbnail** (the stored image was of
|
|
@@ -165,24 +201,22 @@ or a saved design id.
|
|
|
165
201
|
|
|
166
202
|
## Reading what exists
|
|
167
203
|
|
|
168
|
-
Every read the key can reach has a verb
|
|
169
|
-
customers,
|
|
170
|
-
|
|
171
|
-
`
|
|
172
|
-
answer `403 insufficient_scope` whoever signed in).
|
|
204
|
+
Every read the key can reach has a verb, and they are all reads — the CLI can read
|
|
205
|
+
customers, projects and orders but never create or change them (only office staff can
|
|
206
|
+
move an order along; see staff.md). Quotations are the one other thing you can write:
|
|
207
|
+
`quotations mint`.
|
|
173
208
|
|
|
174
209
|
| Verb | Shows |
|
|
175
210
|
|---|---|
|
|
176
211
|
| `wardrobe projects list` | Projects you can access (needed for `--project`) |
|
|
177
212
|
| `wardrobe projects list --phone <number>` | Only projects whose **customer** has that number |
|
|
178
|
-
| `wardrobe projects list --designer <idOrName>` | Only projects owned by that designer |
|
|
179
213
|
| `wardrobe projects get <id>` | One project: customer + owning designer |
|
|
180
214
|
| `wardrobe projects members <id>` | Owner plus every invited member, with roles |
|
|
181
215
|
| `wardrobe customers list` | Your customers |
|
|
182
216
|
| `wardrobe customers get <id>` | One customer |
|
|
183
217
|
| `wardrobe customers projects <id>` | That customer's projects |
|
|
184
218
|
| `wardrobe designs list --project <id>` | Designs saved in a project |
|
|
185
|
-
| `wardrobe designs list` | The newest designs across every project you can see
|
|
219
|
+
| `wardrobe designs list` | The newest designs across every project you can see |
|
|
186
220
|
| `wardrobe designs list --limit <n>` `--since <ISO>` | Same, paged — `--limit` caps rows (default 20, max 100), `--since` filters by `updatedAt >=` |
|
|
187
221
|
| `wardrobe designs get <id>` | One design: name, size, live price, version. `--json` adds `projectId`, `nisTotal`, `serviceTotal` and the full `config`; `--config` prints only the bare config (what `designs update --file` reads) |
|
|
188
222
|
| `wardrobe designs thumbnail <id> --out f.jpg` | The design's rendered thumbnail. A design nobody has opened in a browser has none yet, so the server renders it first (~20 s); `--render` forces a fresh render. `503` means the renderer is down — retry in a minute |
|
|
@@ -192,23 +226,19 @@ answer `403 insufficient_scope` whoever signed in).
|
|
|
192
226
|
| `wardrobe orders list --limit <n>` `--since <ISO>` | Same, paged (`--since` filters by `updatedAt >=`) |
|
|
193
227
|
| `wardrobe orders get <id>` | One order, with its designs' frozen config embedded — no second call to `designs` needed. Prints the buyer's delivery choice (`delivery: immediate` or `delivery: scheduled <date>`) once they have verified their details |
|
|
194
228
|
| `wardrobe orders pdf <id> --out f.pdf` | The order PDF |
|
|
195
|
-
| `wardrobe orders set-status <id> <status> [--dry-run] [--note <text>]` | **Office accounts only** (`orders:transition`, admin/support approvers). Moves the order one legal step: `paid`, `pending_measurement`, `sent_to_manufacturing`, `pending_assembly`, `provided` — never `cancelled`. Already there → "nothing changed"; an illegal step answers 409 with the reason (e.g. `pending_assembly` is only reachable from `in_manufacturing` when transport & installation was bought) — report that to a human, don't retry. `--dry-run` checks without changing; `--note` puts a receipt number on the order's history |
|
|
196
229
|
| `wardrobe quotations list --project <id>` | That project's quote number + full revision history |
|
|
197
230
|
| `wardrobe quotations list` | Every quotation you can see, **newest-generated first** — this is "latest quotes generation" |
|
|
198
231
|
| `wardrobe quotations list --limit <n>` `--since <ISO>` | Same, paged (`--since` filters by `updatedAt >=`, i.e. last generated at/after) |
|
|
199
232
|
| `wardrobe quotations pdf <projectId> --out f.pdf` | The project's **current** quotation revision's PDF — fetches only, never mints one |
|
|
200
233
|
| `wardrobe quotations pdf <projectId> --revision <n> --out f.pdf` | A specific historical revision's PDF instead |
|
|
201
234
|
| `wardrobe quotations mint <projectId> --out f.pdf` | File the quotation at the project's **current** price and download it. **Unlike `pdf`, this can mint a new revision** (1, 2, 3…, history kept) when the price moved since the last one; at an unchanged price it re-serves the stored revision. Output says which (`--json`: `minted`). Needs `quotations:write`; refused with `423` once the project has an order. Use it after a `designs update`, never speculatively — a new revision changes the number on the quote the customer holds |
|
|
202
|
-
| `wardrobe users list` | Registered accounts, **newest-registered first** — staff only, see below |
|
|
203
|
-
| `wardrobe users list --limit <n>` `--since <ISO>` | Same, paged (`--since` filters by `createdAt >=`) — `--limit 5` with no `--since` is "the latest registered users"; omit both, paging with `--since` off the last row's timestamp, for "every user in the system" |
|
|
204
|
-
| `wardrobe users by-phone <number>` | The account(s) whose **own** phone is that number (`+972…`, `05…`, `052-…` all match) — staff only |
|
|
205
235
|
| `wardrobe help-texts` | The help-text table |
|
|
206
236
|
|
|
207
|
-
`projects list` already returns each project's **customer
|
|
208
|
-
|
|
209
|
-
|
|
237
|
+
`projects list` already returns each project's **customer inline** — so "who is the
|
|
238
|
+
customer on each of my projects" is one call, never one call per project. Prefer
|
|
239
|
+
`--phone` over fetching and grepping.
|
|
210
240
|
|
|
211
|
-
Every cross-project
|
|
241
|
+
Every cross-project list (`designs`, `orders`, `quotations`) takes
|
|
212
242
|
`--limit` (1–100, default 20) and a `--since` cursor — pass the last row's timestamp
|
|
213
243
|
back as `--since` to page further instead of guessing an offset. A malformed `--since` or
|
|
214
244
|
an out-of-range `--limit` is a `400`, not a silently-ignored filter — read the error
|
|
@@ -219,37 +249,14 @@ rather than retrying blind.
|
|
|
219
249
|
The list is bounded by the key, so an empty result is as often a **visibility** answer as
|
|
220
250
|
a data one. `wardrobe whoami --json` reports the truth under `agent.scopes` and `role`:
|
|
221
251
|
|
|
222
|
-
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
- A key approved by an **admin or support** user *and* carrying **`staff:read`** sees
|
|
228
|
-
**every** project, customer, order and quotation in the system — and can use
|
|
229
|
-
`wardrobe users list` / `users by-phone` at all. That scope is granted only to
|
|
230
|
-
staff, only for GETs, and is listed by name on the consent screen. The same approver
|
|
231
|
-
also receives **`staff:write`**, which lets `designs create` / `designs update` land in
|
|
232
|
-
a project the admin does not own. A key logged in before 2026-09-05 lacks `staff:read`
|
|
233
|
-
and before 2026-09-12 lacks `quotations:read` — a `403 insufficient_scope` on
|
|
234
|
-
`quotations list`, or a `403 Forbidden` on a write to someone else's project, means
|
|
235
|
-
`wardrobe login` again.
|
|
252
|
+
- A designer sees **their own** projects, customers, designs, orders and quotations,
|
|
253
|
+
plus projects they were invited to. A project belonging to someone else answers
|
|
254
|
+
`403 Forbidden`, not `404`.
|
|
255
|
+
- `403 insufficient_scope` means the key lacks that permission — either the verb is for
|
|
256
|
+
office staff (staff.md), or the key is older than the feature: `wardrobe login` again.
|
|
236
257
|
|
|
237
258
|
Never tell a user something does not exist when you only know your key cannot see it.
|
|
238
|
-
Say which it is: `403 Forbidden` means the row is someone else's
|
|
239
|
-
your key isn't staff at all), `404` means it is gone.
|
|
240
|
-
|
|
241
|
-
### `wardrobe users` is a directory lookup, not a customer lookup
|
|
242
|
-
|
|
243
|
-
`users list` / `users by-phone` read the `User` **account** table — designers, admins,
|
|
244
|
-
support and self-serve clients who can sign in — not the per-project `Customer` contact
|
|
245
|
-
card `wardrobe customers` reads. Each row carries the account's own `phone` (empty until
|
|
246
|
-
that user passes the onboarding phone step; staff never do). `users by-phone` matches
|
|
247
|
-
**only** that field: a customer's number never resolves to the designer who serves them,
|
|
248
|
-
so its answer is "who is this person", safe to act on as the caller's identity. Numbers
|
|
249
|
-
match as the national number, so `+972526665153` and `0526665153` are the same. More than
|
|
250
|
-
one row means the number is ambiguous (the field is self-declared, not unique) — say so
|
|
251
|
-
rather than picking one. Zero rows is normal and not an error; for the customer side of
|
|
252
|
-
the same number use `wardrobe projects list --phone <number>`.
|
|
259
|
+
Say which it is: `403 Forbidden` means the row is someone else's, `404` means it is gone.
|
|
253
260
|
|
|
254
261
|
### Not available here
|
|
255
262
|
|
|
@@ -284,12 +291,10 @@ Prefer generating the skeleton with `wardrobe init` rather than hand-writing it
|
|
|
284
291
|
|
|
285
292
|
- **The CLI can read orders but never place one.** Its key carries `profile:read`,
|
|
286
293
|
`projects:read`, `customers:read`, `designs:read`, `designs:write`, `orders:read`,
|
|
287
|
-
`price:read`, `quotations:read`, `quotations:write`
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
`403 insufficient_scope` (a key logged in before 2026-10-02 lacks `quotations:write` —
|
|
292
|
-
`wardrobe login` again). A
|
|
294
|
+
`price:read`, `quotations:read`, `quotations:write` — **every write beyond designs and
|
|
295
|
+
quotations is absent**, and so is every admin scope. `wardrobe orders list`,
|
|
296
|
+
`orders pdf`, `quotations list`, `quotations pdf` and `quotations mint` work; creating or
|
|
297
|
+
changing an order or a customer answers `403 insufficient_scope`. A
|
|
293
298
|
project that has never been quoted on the web app has no revision to fetch —
|
|
294
299
|
`quotations pdf`/`quotations list --project` answer `404 quotation_not_found`, not an
|
|
295
300
|
empty document — `quotations mint` is how a first revision is filed. Don't attempt
|
package/skill/staff.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Wardrobe CLI — staff keys (admin / support)
|
|
2
|
+
|
|
3
|
+
Read this only when `wardrobe whoami --json` shows `staff:read` (or `orders:transition`)
|
|
4
|
+
under `agent.scopes`. A designer's or client's key has none of these, and every verb
|
|
5
|
+
below answers `403` for it — don't try them.
|
|
6
|
+
|
|
7
|
+
## What a staff key sees and may change
|
|
8
|
+
|
|
9
|
+
- A key approved by an **admin or support** user *and* carrying **`staff:read`** sees
|
|
10
|
+
**every** project, customer, order and quotation in the system — and can use
|
|
11
|
+
`wardrobe users list` / `users by-phone` at all. That scope is granted only to
|
|
12
|
+
staff, only for GETs, and is listed by name on the consent screen.
|
|
13
|
+
- The same approver also receives **`staff:write`**, which lets `designs create` /
|
|
14
|
+
`designs update` land in a project the admin does not own.
|
|
15
|
+
- **No key bypasses a locked project.** A project with a live order answers
|
|
16
|
+
`423 projectLocked` even to an admin's key with `staff:write` — the API holds agents
|
|
17
|
+
to the rule the web app relaxes for office staff. Revising an ordered design goes
|
|
18
|
+
through the web app.
|
|
19
|
+
- Old keys lack newer scopes: a key logged in before 2026-09-05 lacks `staff:read`,
|
|
20
|
+
before 2026-09-12 `quotations:read`, before 2026-10-02 `quotations:write`. A
|
|
21
|
+
`403 insufficient_scope` on those, or a `403 Forbidden` on a write to someone else's
|
|
22
|
+
project, means `wardrobe login` again.
|
|
23
|
+
|
|
24
|
+
## Staff-only verbs
|
|
25
|
+
|
|
26
|
+
| Verb | Shows / does |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `wardrobe projects list --designer <idOrName>` | Only projects owned by that designer |
|
|
29
|
+
| `wardrobe designs list` / `orders list` / `quotations list` | With `staff:read`, the newest across the **whole tenant**, not just yours |
|
|
30
|
+
| `wardrobe users list` | Registered accounts, **newest-registered first** |
|
|
31
|
+
| `wardrobe users list --limit <n>` `--since <ISO>` | Same, paged (`--since` filters by `createdAt >=`) — `--limit 5` with no `--since` is "the latest registered users"; omit both, paging with `--since` off the last row's timestamp, for "every user in the system" |
|
|
32
|
+
| `wardrobe users by-phone <number>` | The account(s) whose **own** phone is that number (`+972…`, `05…`, `052-…` all match) |
|
|
33
|
+
| `wardrobe orders set-status <id> <status> [--dry-run] [--note <text>]` | **Office accounts only** (`orders:transition`, admin/support approvers). Moves the order one legal step: `paid`, `pending_measurement`, `sent_to_manufacturing`, `pending_assembly`, `provided` — never `cancelled`. Already there → "nothing changed"; an illegal step answers 409 with the reason (e.g. `pending_assembly` is only reachable from `in_manufacturing` when transport & installation was bought) — report that to a human, don't retry. `--dry-run` checks without changing; `--note` puts a receipt number on the order's history |
|
|
34
|
+
|
|
35
|
+
`projects list` returns each project's **customer and owning designer inline** — so "who
|
|
36
|
+
is the customer on each of this designer's projects" is one call with `--designer`, never
|
|
37
|
+
one call per project.
|
|
38
|
+
|
|
39
|
+
`users list` takes the same `--limit` (1–100, default 20) / `--since` paging as the other
|
|
40
|
+
cross-project lists; a malformed value is a `400`, not an ignored filter.
|
|
41
|
+
|
|
42
|
+
## `wardrobe users` is a directory lookup, not a customer lookup
|
|
43
|
+
|
|
44
|
+
`users list` / `users by-phone` read the `User` **account** table — designers, admins,
|
|
45
|
+
support and self-serve clients who can sign in — not the per-project `Customer` contact
|
|
46
|
+
card `wardrobe customers` reads. Each row carries the account's own `phone` (empty until
|
|
47
|
+
that user passes the onboarding phone step; staff never do). `users by-phone` matches
|
|
48
|
+
**only** that field: a customer's number never resolves to the designer who serves them,
|
|
49
|
+
so its answer is "who is this person", safe to act on as the caller's identity. Numbers
|
|
50
|
+
match as the national number, so `+972526665153` and `0526665153` are the same. More than
|
|
51
|
+
one row means the number is ambiguous (the field is self-declared, not unique) — say so
|
|
52
|
+
rather than picking one. Zero rows is normal and not an error; for the customer side of
|
|
53
|
+
the same number use `wardrobe projects list --phone <number>`.
|
|
54
|
+
|
|
55
|
+
## Still not reachable, even for staff
|
|
56
|
+
|
|
57
|
+
Anything under `/admin` (catalog, rates, users' roles, commissions), invitations and the
|
|
58
|
+
manufacturing-files export stay in the web app — every agent key is barred from them
|
|
59
|
+
(`403 agent_not_permitted`), whatever its scopes.
|