@lahat-group/cli 1.1.6 → 1.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -11831,8 +11831,8 @@ To authorize this CLI, visit:
11831
11831
  // src/commands/account.ts
11832
11832
  init_esm_shims();
11833
11833
  async function whoami(opts = {}) {
11834
- const client3 = createClient(readConfig()?.accessToken ?? null);
11835
- const me = await client3.get("auth/me").json();
11834
+ const client4 = createClient(readConfig()?.accessToken ?? null);
11835
+ const me = await client4.get("auth/me").json();
11836
11836
  emit(me, !!opts.json, () => {
11837
11837
  process.stdout.write(`${me.displayName} <${me.email}> \u2014 account role: ${me.role}
11838
11838
  `);
@@ -11847,39 +11847,159 @@ function logout(opts = {}) {
11847
11847
 
11848
11848
  // src/commands/network.ts
11849
11849
  init_esm_shims();
11850
+
11851
+ // src/commands/read.ts
11852
+ init_esm_shims();
11853
+ import { writeFileSync as writeFileSync2 } from "fs";
11850
11854
  function client() {
11851
11855
  return createClient(readConfig()?.accessToken ?? null);
11852
11856
  }
11857
+ function phoneKey(value) {
11858
+ const digits = (value ?? "").replace(/\D/g, "");
11859
+ return digits.startsWith("972") ? `0${digits.slice(3)}` : digits;
11860
+ }
11861
+ async function getProject(id, opts = {}) {
11862
+ const p = await client().get(`projects/${id}`).json();
11863
+ emit(p, !!opts.json, () => {
11864
+ process.stdout.write(`${p.id} ${p.name}
11865
+ `);
11866
+ if (p.customer) {
11867
+ process.stdout.write(`customer: ${p.customer.customerName ?? "\u2014"} ${p.customer.phoneNumber ?? ""}
11868
+ `);
11869
+ }
11870
+ if (p.createdBy) process.stdout.write(`designer: ${p.createdBy.displayName ?? "\u2014"} (${p.createdBy.id ?? ""})
11871
+ `);
11872
+ });
11873
+ }
11874
+ async function projectMembers(id, opts = {}) {
11875
+ const res = await client().get(`projects/${id}/members`).json();
11876
+ emit(res.members, !!opts.json, () => {
11877
+ for (const m of res.members) {
11878
+ process.stdout.write(`${m.userId} ${m.role} ${m.displayName ?? m.email ?? ""}
11879
+ `);
11880
+ }
11881
+ });
11882
+ }
11883
+ async function listCustomers(opts = {}) {
11884
+ const res = await client().get("customers").json();
11885
+ emit(res.customers, !!opts.json, () => {
11886
+ for (const c of res.customers) {
11887
+ process.stdout.write(`${c.id} ${c.customerName ?? ""} ${c.phoneNumber ?? ""}
11888
+ `);
11889
+ }
11890
+ });
11891
+ }
11892
+ async function getCustomer(id, opts = {}) {
11893
+ const c = await client().get(`customers/${id}`).json();
11894
+ emit(
11895
+ c,
11896
+ !!opts.json,
11897
+ () => process.stdout.write(`${c.id} ${c.customerName ?? ""} ${c.phoneNumber ?? ""} ${c.email ?? ""}
11898
+ `)
11899
+ );
11900
+ }
11901
+ async function customerProjects(id, opts = {}) {
11902
+ const res = await client().get(`customers/${id}/projects`).json();
11903
+ const projects2 = res.projects ?? [];
11904
+ emit(projects2, !!opts.json, () => {
11905
+ for (const p of projects2) process.stdout.write(`${p.id} ${p.name}
11906
+ `);
11907
+ });
11908
+ }
11909
+ async function listOrders(opts = {}) {
11910
+ if (!opts.project) throw new Error("--project <id> is required.");
11911
+ const path2 = opts.open ? "orders/open" : "orders";
11912
+ const res = await client().get(path2, { searchParams: { projectId: opts.project } }).json();
11913
+ const orders2 = Array.isArray(res?.orders) ? res.orders : res ? [res] : [];
11914
+ emit(opts.open ? res : orders2, !!opts.json, () => {
11915
+ if (orders2.length === 0) {
11916
+ process.stdout.write("no orders\n");
11917
+ return;
11918
+ }
11919
+ for (const o of orders2) {
11920
+ const total = o.totalPrice !== void 0 ? `${o.totalPrice} ${o.currency ?? "NIS"}` : "";
11921
+ process.stdout.write(`${o.id ?? ""} #${o.orderNumber ?? "\u2014"} rev${o.revisionNumber ?? 1} ${o.status ?? ""} ${total}
11922
+ `);
11923
+ }
11924
+ });
11925
+ }
11926
+ async function orderPdf(id, opts = {}) {
11927
+ const out = opts.out ?? `order-${id}.pdf`;
11928
+ const buf = Buffer.from(await client().get(`orders/${id}/pdf`).arrayBuffer());
11929
+ writeFileSync2(out, buf);
11930
+ emit(
11931
+ { path: out, bytes: buf.length },
11932
+ !!opts.json,
11933
+ () => process.stdout.write(`${out} ${buf.length} bytes
11934
+ `)
11935
+ );
11936
+ }
11937
+ async function designThumbnail(id, opts = {}) {
11938
+ const out = opts.out ?? `design-${id}.jpg`;
11939
+ const buf = Buffer.from(await client().get(`designs/${id}/thumbnail`).arrayBuffer());
11940
+ writeFileSync2(out, buf);
11941
+ emit(
11942
+ { path: out, bytes: buf.length },
11943
+ !!opts.json,
11944
+ () => process.stdout.write(`${out} ${buf.length} bytes
11945
+ `)
11946
+ );
11947
+ }
11948
+ async function helpTexts(opts = {}) {
11949
+ const res = await client().get("help-texts").json();
11950
+ emit(res, !!opts.json, () => process.stdout.write(`${Object.keys(res).length} help text(s)
11951
+ `));
11952
+ }
11953
+
11954
+ // src/commands/network.ts
11955
+ function client2() {
11956
+ return createClient(readConfig()?.accessToken ?? null);
11957
+ }
11853
11958
  async function price(file, opts = {}) {
11854
11959
  const config = await readJsonInput(file);
11855
- const res = await client().post("price", { json: { config } }).json();
11960
+ const res = await client2().post("price", { json: { config } }).json();
11856
11961
  emit(res, !!opts.json, () => process.stdout.write(`${res.total} ${res.currency}
11857
11962
  `));
11858
11963
  }
11859
11964
  async function listDesigns(opts = {}) {
11860
11965
  if (!opts.project) throw new Error("--project <id> is required.");
11861
- const res = await client().get("designs", { searchParams: { projectId: opts.project } }).json();
11966
+ const res = await client2().get("designs", { searchParams: { projectId: opts.project } }).json();
11862
11967
  emit(res.designs, !!opts.json, () => {
11863
11968
  for (const d of res.designs) process.stdout.write(`${d.id} ${d.name}
11864
11969
  `);
11865
11970
  });
11866
11971
  }
11867
11972
  async function listProjects(opts = {}) {
11868
- const res = await client().get("projects").json();
11869
- emit(res.projects, !!opts.json, () => {
11870
- for (const p of res.projects) process.stdout.write(`${p.id} ${p.name}
11973
+ const res = await client2().get("projects").json();
11974
+ let projects2 = res.projects;
11975
+ if (opts.phone) {
11976
+ const want = phoneKey(opts.phone);
11977
+ projects2 = projects2.filter((p) => phoneKey(p.customer?.phoneNumber) === want);
11978
+ }
11979
+ if (opts.designer) {
11980
+ const needle = opts.designer.toLowerCase();
11981
+ projects2 = projects2.filter(
11982
+ (p) => p.createdBy?.id === opts.designer || (p.createdBy?.displayName ?? "").toLowerCase().includes(needle)
11983
+ );
11984
+ }
11985
+ emit(projects2, !!opts.json, () => {
11986
+ for (const p of projects2) {
11987
+ const who = p.createdBy?.displayName ? ` ${p.createdBy.displayName}` : "";
11988
+ const cust = p.customer?.customerName ? ` ${p.customer.customerName}` : "";
11989
+ process.stdout.write(`${p.id} ${p.name}${cust}${who}
11871
11990
  `);
11991
+ }
11872
11992
  });
11873
11993
  }
11874
11994
 
11875
11995
  // src/commands/mutate.ts
11876
11996
  init_esm_shims();
11877
- function client2() {
11997
+ function client3() {
11878
11998
  return createClient(readConfig()?.accessToken ?? null);
11879
11999
  }
11880
12000
  async function plan(file, opts = {}) {
11881
12001
  const config = await readJsonInput(file);
11882
- const res = await client2().post("price", { json: { config }, searchParams: { preview: "bom" } }).json();
12002
+ const res = await client3().post("price", { json: { config }, searchParams: { preview: "bom" } }).json();
11883
12003
  emit(res, !!opts.json, () => {
11884
12004
  process.stdout.write(`${res.total} ${res.currency}
11885
12005
  `);
@@ -11892,12 +12012,30 @@ async function plan(file, opts = {}) {
11892
12012
  async function apply(file, opts = {}) {
11893
12013
  if (!opts.project) throw new Error("--project <id> is required.");
11894
12014
  const config = await readJsonInput(file);
11895
- const res = await client2().post("designs", {
12015
+ const res = await client3().post("designs", {
11896
12016
  json: { projectId: opts.project, name: opts.name ?? "CLI design", config }
11897
12017
  }).json();
11898
12018
  emit(res, !!opts.json, () => process.stdout.write(`${res.id}
11899
12019
  `));
11900
12020
  }
12021
+ async function updateDesign(id, opts = {}) {
12022
+ if (!opts.file && !opts.name) {
12023
+ throw new Error("Nothing to update \u2014 pass --file <config> and/or --name <name>.");
12024
+ }
12025
+ const api = client3();
12026
+ const current = await api.get(`designs/${id}`).json();
12027
+ const body = {};
12028
+ if (opts.file) body.config = await readJsonInput(opts.file);
12029
+ if (opts.name) body.name = opts.name;
12030
+ if (current.version !== void 0) body.version = current.version;
12031
+ const res = await api.put(`designs/${id}`, { json: body }).json();
12032
+ emit(
12033
+ res,
12034
+ !!opts.json,
12035
+ () => process.stdout.write(`${res.id} v${res.version ?? "?"}
12036
+ `)
12037
+ );
12038
+ }
11901
12039
 
11902
12040
  // src/commands/skill.ts
11903
12041
  init_esm_shims();
@@ -11966,7 +12104,7 @@ async function formatCliError(err, json) {
11966
12104
 
11967
12105
  // src/version.ts
11968
12106
  init_esm_shims();
11969
- var CLI_VERSION = "1.1.6";
12107
+ var CLI_VERSION = "1.1.7";
11970
12108
 
11971
12109
  // src/index.ts
11972
12110
  program.name("wardrobe").description("Wardrobe configurator CLI").version(CLI_VERSION);
@@ -11991,8 +12129,8 @@ program.command("describe <idOrFile>").description("Describe a config (local fil
11991
12129
  if (idOrFile === "-" || existsSync3(idOrFile)) {
11992
12130
  config = await readJsonInput(idOrFile);
11993
12131
  } else {
11994
- const client3 = createClient(readConfig()?.accessToken ?? null);
11995
- const design = await client3.get(`designs/${idOrFile}`).json();
12132
+ const client4 = createClient(readConfig()?.accessToken ?? null);
12133
+ const design = await client4.get(`designs/${idOrFile}`).json();
11996
12134
  config = design.config;
11997
12135
  }
11998
12136
  const lines = describeInput(config, o.locale ?? "en");
@@ -12003,8 +12141,21 @@ program.command("plan <file>").description("Dry-run: price a config + BOM previe
12003
12141
  program.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((file, o) => apply(file, o));
12004
12142
  var designs = program.command("designs").description("Manage saved designs");
12005
12143
  designs.command("list").description("List designs in a project").requiredOption("--project <id>", "project id").option("--json", "machine-readable output").action((o) => listDesigns(o));
12144
+ designs.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((file, o) => apply(file, o));
12145
+ designs.command("update <id>").description("Revise a saved design (config and/or name)").option("--file <file>", "new config JSON (file or - for stdin)").option("--name <name>", "new design name").option("--json", "machine-readable output").action((id, o) => updateDesign(id, o));
12146
+ designs.command("thumbnail <id>").description("Download a design's thumbnail image").option("--out <file>", "destination path").option("--json", "machine-readable output").action((id, o) => designThumbnail(id, o));
12006
12147
  var projects = program.command("projects").description("Manage projects");
12007
- projects.command("list").description("List accessible projects").option("--json", "machine-readable output").action((o) => listProjects(o));
12148
+ projects.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((o) => listProjects(o));
12149
+ projects.command("get <id>").description("Show one project").option("--json", "machine-readable output").action((id, o) => getProject(id, o));
12150
+ projects.command("members <id>").description("List a project's owner and members").option("--json", "machine-readable output").action((id, o) => projectMembers(id, o));
12151
+ var customers = program.command("customers").description("Read customers");
12152
+ customers.command("list").description("List your customers").option("--json", "machine-readable output").action((o) => listCustomers(o));
12153
+ customers.command("get <id>").description("Show one customer").option("--json", "machine-readable output").action((id, o) => getCustomer(id, o));
12154
+ customers.command("projects <id>").description("List a customer's projects").option("--json", "machine-readable output").action((id, o) => customerProjects(id, o));
12155
+ var orders = program.command("orders").description("Read orders");
12156
+ orders.command("list").description("List a project's orders").requiredOption("--project <id>", "project id").option("--open", "only the project's active (non-cancelled) order").option("--json", "machine-readable output").action((o) => listOrders(o));
12157
+ orders.command("pdf <id>").description("Download an order PDF").option("--out <file>", "destination path").option("--json", "machine-readable output").action((id, o) => orderPdf(id, o));
12158
+ program.command("help-texts").description("Read the help-text table").option("--json", "machine-readable output").action((o) => helpTexts(o));
12008
12159
  var skill = program.command("skill").description("Manage the bundled Claude Skill");
12009
12160
  skill.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((o) => skillInstall(o));
12010
12161
  program.parseAsync().catch(async (err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lahat-group/cli",
3
- "version": "1.1.6",
3
+ "version": "1.1.7",
4
4
  "description": "CLI + Claude Skill to build, validate, price, and save wardrobe designs.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
package/skill/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: wardrobe-configurator
3
- description: Use when the user wants to build, configure, validate, price, or order a wardrobe/closet design from the terminal — phrases like "configure a wardrobe", "price a design", "build a closet layout", "validate my wardrobe config". Drives the `wardrobe` CLI (HTTP client over the configurator API); a WardrobeConfig JSON file on disk is the unit of work.
3
+ description: Use when the user wants to build, configure, validate, price, or order a wardrobe/closet design from the terminal — phrases like "configure a wardrobe", "price a design", "build a closet layout", "validate my wardrobe config" — OR wants to look up existing wardrobe data: projects, customers, designers, orders and their PDFs, e.g. "which projects does this designer have", "find the customer with phone 05x", "who is on this project", "what is the status of this order", "download the order PDF". 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
@@ -33,15 +33,86 @@ agent loop is: **init → edit → validate → price → plan → apply**.
33
33
  5. **plan** — `wardrobe plan wardrobe.json` → dry-run price **plus a BOM preview**.
34
34
  Mutates nothing. Use this to preview before applying.
35
35
  6. **apply** — `wardrobe apply wardrobe.json --project <id> [--name <name>]` → saves
36
- the config as a design and prints its `{id}`. **`--project` is required.**
36
+ the config as a design in that project and prints its `{id}`. **`--project` is
37
+ required.** `wardrobe designs create <file> --project <id>` is the same verb under
38
+ the `designs` group, if that reads more clearly in context.
39
+
40
+ ## Revising a saved design
41
+
42
+ `wardrobe designs update <id> [--file <config>] [--name <name>]` revises a design in
43
+ place — pass either or both. Prefer this over creating a second design when the user
44
+ is iterating; a project accumulating `CLI design`, `CLI design 2`, `CLI design 3` is
45
+ almost always a bug in how the agent worked, not what the user wanted.
46
+
47
+ Validate and price the new config **before** updating, same as for a create.
48
+
49
+ Two refusals to expect, neither of which is retryable:
50
+
51
+ - **`409 stale_version`** — someone edited that design (usually the human, in the web
52
+ app) after you read it. The CLI sends the version it last saw precisely so this
53
+ fails loudly instead of overwriting their edit. Re-read the design, re-apply the
54
+ user's intent on top, and tell them it changed underneath you. **Never** loop.
55
+ - **`423 projectLocked`** — the project has a live (non-cancelled) order, which froze
56
+ its designs. No design in it can be created, updated or deleted until a human
57
+ cancels or completes that order. Say so; don't try another design or another verb.
58
+
59
+ Note that updating a config **clears the design's thumbnail** (the stored image was of
60
+ the old config); the web app re-captures it on next view. That is expected, not an error.
37
61
 
38
62
  `wardrobe describe <idOrFile>` gives a human-readable summary of a local file/`-`
39
63
  or a saved design id.
40
64
 
41
- ## Listing context
42
-
43
- - `wardrobe projects list` — projects you can access (needed for `--project`).
44
- - `wardrobe designs list --project <id>` — designs saved in a project.
65
+ ## Reading what exists
66
+
67
+ Every read the key can reach has a verb. **All of them are GETs** — the CLI can read
68
+ customers and orders but never create or change them (`customers:write`,
69
+ `orders:write` and `quotations:write` are not on the CLI's key, so those routes
70
+ answer `403 insufficient_scope` whoever signed in).
71
+
72
+ | Verb | Shows |
73
+ |---|---|
74
+ | `wardrobe projects list` | Projects you can access (needed for `--project`) |
75
+ | `wardrobe projects list --phone <number>` | Only projects whose **customer** has that number |
76
+ | `wardrobe projects list --designer <idOrName>` | Only projects owned by that designer |
77
+ | `wardrobe projects get <id>` | One project: customer + owning designer |
78
+ | `wardrobe projects members <id>` | Owner plus every invited member, with roles |
79
+ | `wardrobe customers list` | Your customers |
80
+ | `wardrobe customers get <id>` | One customer |
81
+ | `wardrobe customers projects <id>` | That customer's projects |
82
+ | `wardrobe designs list --project <id>` | Designs saved in a project |
83
+ | `wardrobe designs thumbnail <id> --out f.jpg` | The design's rendered thumbnail |
84
+ | `wardrobe orders list --project <id>` | Every order the project has had |
85
+ | `wardrobe orders list --project <id> --open` | Only its active (non-cancelled) order |
86
+ | `wardrobe orders pdf <id> --out f.pdf` | The order PDF |
87
+ | `wardrobe help-texts` | The help-text table |
88
+
89
+ `projects list` already returns each project's **customer and owning designer inline** —
90
+ so "who is the customer on each of this designer's projects" is one call plus a filter,
91
+ never one call per project. Prefer `--phone` / `--designer` over fetching and grepping.
92
+
93
+ ### What you can see is not what exists
94
+
95
+ The list is bounded by the key, so an empty result is as often a **visibility** answer as
96
+ a data one. `wardrobe whoami --json` reports the truth under `agent.scopes` and `role`:
97
+
98
+ - An ordinary designer or client sees **their own** projects plus ones they were invited
99
+ to. A project belonging to someone else answers `403 Forbidden`, not `404`.
100
+ - A key approved by an **admin or support** user *and* carrying **`staff:read`** sees
101
+ **every** project, customer and order in the system. That scope is granted only to
102
+ staff, only for GETs, and is listed by name on the consent screen.
103
+
104
+ Never tell a user something does not exist when you only know your key cannot see it.
105
+ Say which it is: `403 Forbidden` means the row is someone else's, `404` means it is gone.
106
+
107
+ ### Not available here
108
+
109
+ Don't reach for these; they are not CLI verbs and not agent-reachable:
110
+
111
+ - **Anything under `/admin`**, plus commissions and invitations — barred to every agent
112
+ key at the mount (`403 agent_not_permitted`). No scope unlocks them.
113
+ - **Quotation history.** `POST /quotations/pdf` is open to other clients, but reading a
114
+ project's past quotes is JWT-only — a human in the web app.
115
+ - **Manufacturing-files export.** Staff-only, through the web app.
45
116
 
46
117
  ## WardrobeConfig shape (top-level)
47
118
 
@@ -65,10 +136,12 @@ Prefer generating the skeleton with `wardrobe init` rather than hand-writing it
65
136
 
66
137
  ## Guardrails — committing to an order requires a human
67
138
 
68
- - **The CLI cannot place orders.** The `wardrobe-cli` client is registered with
69
- `profile:read`, `projects:read`, `designs:read`, `designs:write`, `price:read` —
70
- **no order or admin scope** — so every order route answers `403 insufficient_scope`.
71
- Don't attempt to order, or to run admin/catalog operations.
139
+ - **The CLI can read orders but never place one.** Its key carries `profile:read`,
140
+ `projects:read`, `customers:read`, `designs:read`, `designs:write`, `orders:read`,
141
+ `price:read` (+ `staff:read` for staff) — **every write beyond designs is absent**,
142
+ and so is every admin scope. `wardrobe orders list` and `orders pdf` work; creating
143
+ or changing an order, a customer or a quotation answers `403 insufficient_scope`.
144
+ Don't attempt it, and don't attempt admin/catalog operations.
72
145
  - To turn a saved design into an order, direct the **human** to the web app: they
73
146
  open the project, review the design, and complete the **verify-details** flow —
74
147
  which computes the final price, freezes the PDF, and advances the order. Only a