@lahat-group/cli 1.1.6 → 1.1.8
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 +167 -15
- package/package.json +1 -1
- package/skill/SKILL.md +86 -10
package/dist/index.js
CHANGED
|
@@ -9447,7 +9447,8 @@ var AGENT_SCOPES = {
|
|
|
9447
9447
|
"orders:write": { he: "\u05DC\u05D4\u05DB\u05D9\u05DF \u05D8\u05D9\u05D5\u05D8\u05EA \u05D4\u05D6\u05DE\u05E0\u05D4 \u05DC\u05D0\u05D9\u05E9\u05D5\u05E8\u05DA", en: "Draft an order for you to confirm" },
|
|
9448
9448
|
"orders:commit": { he: "\u05DC\u05D0\u05E9\u05E8 \u05D4\u05D6\u05DE\u05E0\u05D4 \u05D1\u05E9\u05DE\u05DA", en: "Confirm an order on your behalf" },
|
|
9449
9449
|
"invitations:write": { he: "\u05DC\u05D4\u05D6\u05DE\u05D9\u05DF \u05D0\u05E0\u05E9\u05D9\u05DD \u05DC\u05E4\u05E8\u05D5\u05D9\u05E7\u05D8\u05D9\u05DD \u05E9\u05DC\u05DA", en: "Invite people to your projects" },
|
|
9450
|
-
"staff:read": { he: "\u05DC\u05E7\u05E8\u05D5\u05D0 \u05D0\u05EA \u05D4\u05E0\u05EA\u05D5\u05E0\u05D9\u05DD \u05E9\u05DC \u05DB\u05DC \u05D4\u05DE\u05E9\u05EA\u05DE\u05E9\u05D9\u05DD \u05D1\u05DE\u05E2\u05E8\u05DB\u05EA", en: "Read every user's data in the system" }
|
|
9450
|
+
"staff:read": { he: "\u05DC\u05E7\u05E8\u05D5\u05D0 \u05D0\u05EA \u05D4\u05E0\u05EA\u05D5\u05E0\u05D9\u05DD \u05E9\u05DC \u05DB\u05DC \u05D4\u05DE\u05E9\u05EA\u05DE\u05E9\u05D9\u05DD \u05D1\u05DE\u05E2\u05E8\u05DB\u05EA", en: "Read every user's data in the system" },
|
|
9451
|
+
"staff:write": { he: "\u05DC\u05E2\u05E8\u05D5\u05DA \u05D0\u05EA \u05D4\u05E0\u05EA\u05D5\u05E0\u05D9\u05DD \u05E9\u05DC \u05DB\u05DC \u05D4\u05DE\u05E9\u05EA\u05DE\u05E9\u05D9\u05DD \u05D1\u05DE\u05E2\u05E8\u05DB\u05EA", en: "Edit every user's data in the system" }
|
|
9451
9452
|
};
|
|
9452
9453
|
var ALL_AGENT_SCOPES = Object.keys(AGENT_SCOPES);
|
|
9453
9454
|
|
|
@@ -11831,8 +11832,8 @@ To authorize this CLI, visit:
|
|
|
11831
11832
|
// src/commands/account.ts
|
|
11832
11833
|
init_esm_shims();
|
|
11833
11834
|
async function whoami(opts = {}) {
|
|
11834
|
-
const
|
|
11835
|
-
const me = await
|
|
11835
|
+
const client4 = createClient(readConfig()?.accessToken ?? null);
|
|
11836
|
+
const me = await client4.get("auth/me").json();
|
|
11836
11837
|
emit(me, !!opts.json, () => {
|
|
11837
11838
|
process.stdout.write(`${me.displayName} <${me.email}> \u2014 account role: ${me.role}
|
|
11838
11839
|
`);
|
|
@@ -11847,39 +11848,159 @@ function logout(opts = {}) {
|
|
|
11847
11848
|
|
|
11848
11849
|
// src/commands/network.ts
|
|
11849
11850
|
init_esm_shims();
|
|
11851
|
+
|
|
11852
|
+
// src/commands/read.ts
|
|
11853
|
+
init_esm_shims();
|
|
11854
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
11850
11855
|
function client() {
|
|
11851
11856
|
return createClient(readConfig()?.accessToken ?? null);
|
|
11852
11857
|
}
|
|
11858
|
+
function phoneKey(value) {
|
|
11859
|
+
const digits = (value ?? "").replace(/\D/g, "");
|
|
11860
|
+
return digits.startsWith("972") ? `0${digits.slice(3)}` : digits;
|
|
11861
|
+
}
|
|
11862
|
+
async function getProject(id, opts = {}) {
|
|
11863
|
+
const p = await client().get(`projects/${id}`).json();
|
|
11864
|
+
emit(p, !!opts.json, () => {
|
|
11865
|
+
process.stdout.write(`${p.id} ${p.name}
|
|
11866
|
+
`);
|
|
11867
|
+
if (p.customer) {
|
|
11868
|
+
process.stdout.write(`customer: ${p.customer.customerName ?? "\u2014"} ${p.customer.phoneNumber ?? ""}
|
|
11869
|
+
`);
|
|
11870
|
+
}
|
|
11871
|
+
if (p.createdBy) process.stdout.write(`designer: ${p.createdBy.displayName ?? "\u2014"} (${p.createdBy.id ?? ""})
|
|
11872
|
+
`);
|
|
11873
|
+
});
|
|
11874
|
+
}
|
|
11875
|
+
async function projectMembers(id, opts = {}) {
|
|
11876
|
+
const res = await client().get(`projects/${id}/members`).json();
|
|
11877
|
+
emit(res.members, !!opts.json, () => {
|
|
11878
|
+
for (const m of res.members) {
|
|
11879
|
+
process.stdout.write(`${m.userId} ${m.role} ${m.displayName ?? m.email ?? ""}
|
|
11880
|
+
`);
|
|
11881
|
+
}
|
|
11882
|
+
});
|
|
11883
|
+
}
|
|
11884
|
+
async function listCustomers(opts = {}) {
|
|
11885
|
+
const res = await client().get("customers").json();
|
|
11886
|
+
emit(res.customers, !!opts.json, () => {
|
|
11887
|
+
for (const c of res.customers) {
|
|
11888
|
+
process.stdout.write(`${c.id} ${c.customerName ?? ""} ${c.phoneNumber ?? ""}
|
|
11889
|
+
`);
|
|
11890
|
+
}
|
|
11891
|
+
});
|
|
11892
|
+
}
|
|
11893
|
+
async function getCustomer(id, opts = {}) {
|
|
11894
|
+
const c = await client().get(`customers/${id}`).json();
|
|
11895
|
+
emit(
|
|
11896
|
+
c,
|
|
11897
|
+
!!opts.json,
|
|
11898
|
+
() => process.stdout.write(`${c.id} ${c.customerName ?? ""} ${c.phoneNumber ?? ""} ${c.email ?? ""}
|
|
11899
|
+
`)
|
|
11900
|
+
);
|
|
11901
|
+
}
|
|
11902
|
+
async function customerProjects(id, opts = {}) {
|
|
11903
|
+
const res = await client().get(`customers/${id}/projects`).json();
|
|
11904
|
+
const projects2 = res.projects ?? [];
|
|
11905
|
+
emit(projects2, !!opts.json, () => {
|
|
11906
|
+
for (const p of projects2) process.stdout.write(`${p.id} ${p.name}
|
|
11907
|
+
`);
|
|
11908
|
+
});
|
|
11909
|
+
}
|
|
11910
|
+
async function listOrders(opts = {}) {
|
|
11911
|
+
if (!opts.project) throw new Error("--project <id> is required.");
|
|
11912
|
+
const path2 = opts.open ? "orders/open" : "orders";
|
|
11913
|
+
const res = await client().get(path2, { searchParams: { projectId: opts.project } }).json();
|
|
11914
|
+
const orders2 = Array.isArray(res?.orders) ? res.orders : res ? [res] : [];
|
|
11915
|
+
emit(opts.open ? res : orders2, !!opts.json, () => {
|
|
11916
|
+
if (orders2.length === 0) {
|
|
11917
|
+
process.stdout.write("no orders\n");
|
|
11918
|
+
return;
|
|
11919
|
+
}
|
|
11920
|
+
for (const o of orders2) {
|
|
11921
|
+
const total = o.totalPrice !== void 0 ? `${o.totalPrice} ${o.currency ?? "NIS"}` : "";
|
|
11922
|
+
process.stdout.write(`${o.id ?? ""} #${o.orderNumber ?? "\u2014"} rev${o.revisionNumber ?? 1} ${o.status ?? ""} ${total}
|
|
11923
|
+
`);
|
|
11924
|
+
}
|
|
11925
|
+
});
|
|
11926
|
+
}
|
|
11927
|
+
async function orderPdf(id, opts = {}) {
|
|
11928
|
+
const out = opts.out ?? `order-${id}.pdf`;
|
|
11929
|
+
const buf = Buffer.from(await client().get(`orders/${id}/pdf`).arrayBuffer());
|
|
11930
|
+
writeFileSync2(out, buf);
|
|
11931
|
+
emit(
|
|
11932
|
+
{ path: out, bytes: buf.length },
|
|
11933
|
+
!!opts.json,
|
|
11934
|
+
() => process.stdout.write(`${out} ${buf.length} bytes
|
|
11935
|
+
`)
|
|
11936
|
+
);
|
|
11937
|
+
}
|
|
11938
|
+
async function designThumbnail(id, opts = {}) {
|
|
11939
|
+
const out = opts.out ?? `design-${id}.jpg`;
|
|
11940
|
+
const buf = Buffer.from(await client().get(`designs/${id}/thumbnail`).arrayBuffer());
|
|
11941
|
+
writeFileSync2(out, buf);
|
|
11942
|
+
emit(
|
|
11943
|
+
{ path: out, bytes: buf.length },
|
|
11944
|
+
!!opts.json,
|
|
11945
|
+
() => process.stdout.write(`${out} ${buf.length} bytes
|
|
11946
|
+
`)
|
|
11947
|
+
);
|
|
11948
|
+
}
|
|
11949
|
+
async function helpTexts(opts = {}) {
|
|
11950
|
+
const res = await client().get("help-texts").json();
|
|
11951
|
+
emit(res, !!opts.json, () => process.stdout.write(`${Object.keys(res).length} help text(s)
|
|
11952
|
+
`));
|
|
11953
|
+
}
|
|
11954
|
+
|
|
11955
|
+
// src/commands/network.ts
|
|
11956
|
+
function client2() {
|
|
11957
|
+
return createClient(readConfig()?.accessToken ?? null);
|
|
11958
|
+
}
|
|
11853
11959
|
async function price(file, opts = {}) {
|
|
11854
11960
|
const config = await readJsonInput(file);
|
|
11855
|
-
const res = await
|
|
11961
|
+
const res = await client2().post("price", { json: { config } }).json();
|
|
11856
11962
|
emit(res, !!opts.json, () => process.stdout.write(`${res.total} ${res.currency}
|
|
11857
11963
|
`));
|
|
11858
11964
|
}
|
|
11859
11965
|
async function listDesigns(opts = {}) {
|
|
11860
11966
|
if (!opts.project) throw new Error("--project <id> is required.");
|
|
11861
|
-
const res = await
|
|
11967
|
+
const res = await client2().get("designs", { searchParams: { projectId: opts.project } }).json();
|
|
11862
11968
|
emit(res.designs, !!opts.json, () => {
|
|
11863
11969
|
for (const d of res.designs) process.stdout.write(`${d.id} ${d.name}
|
|
11864
11970
|
`);
|
|
11865
11971
|
});
|
|
11866
11972
|
}
|
|
11867
11973
|
async function listProjects(opts = {}) {
|
|
11868
|
-
const res = await
|
|
11869
|
-
|
|
11870
|
-
|
|
11974
|
+
const res = await client2().get("projects").json();
|
|
11975
|
+
let projects2 = res.projects;
|
|
11976
|
+
if (opts.phone) {
|
|
11977
|
+
const want = phoneKey(opts.phone);
|
|
11978
|
+
projects2 = projects2.filter((p) => phoneKey(p.customer?.phoneNumber) === want);
|
|
11979
|
+
}
|
|
11980
|
+
if (opts.designer) {
|
|
11981
|
+
const needle = opts.designer.toLowerCase();
|
|
11982
|
+
projects2 = projects2.filter(
|
|
11983
|
+
(p) => p.createdBy?.id === opts.designer || (p.createdBy?.displayName ?? "").toLowerCase().includes(needle)
|
|
11984
|
+
);
|
|
11985
|
+
}
|
|
11986
|
+
emit(projects2, !!opts.json, () => {
|
|
11987
|
+
for (const p of projects2) {
|
|
11988
|
+
const who = p.createdBy?.displayName ? ` ${p.createdBy.displayName}` : "";
|
|
11989
|
+
const cust = p.customer?.customerName ? ` ${p.customer.customerName}` : "";
|
|
11990
|
+
process.stdout.write(`${p.id} ${p.name}${cust}${who}
|
|
11871
11991
|
`);
|
|
11992
|
+
}
|
|
11872
11993
|
});
|
|
11873
11994
|
}
|
|
11874
11995
|
|
|
11875
11996
|
// src/commands/mutate.ts
|
|
11876
11997
|
init_esm_shims();
|
|
11877
|
-
function
|
|
11998
|
+
function client3() {
|
|
11878
11999
|
return createClient(readConfig()?.accessToken ?? null);
|
|
11879
12000
|
}
|
|
11880
12001
|
async function plan(file, opts = {}) {
|
|
11881
12002
|
const config = await readJsonInput(file);
|
|
11882
|
-
const res = await
|
|
12003
|
+
const res = await client3().post("price", { json: { config }, searchParams: { preview: "bom" } }).json();
|
|
11883
12004
|
emit(res, !!opts.json, () => {
|
|
11884
12005
|
process.stdout.write(`${res.total} ${res.currency}
|
|
11885
12006
|
`);
|
|
@@ -11892,12 +12013,30 @@ async function plan(file, opts = {}) {
|
|
|
11892
12013
|
async function apply(file, opts = {}) {
|
|
11893
12014
|
if (!opts.project) throw new Error("--project <id> is required.");
|
|
11894
12015
|
const config = await readJsonInput(file);
|
|
11895
|
-
const res = await
|
|
12016
|
+
const res = await client3().post("designs", {
|
|
11896
12017
|
json: { projectId: opts.project, name: opts.name ?? "CLI design", config }
|
|
11897
12018
|
}).json();
|
|
11898
12019
|
emit(res, !!opts.json, () => process.stdout.write(`${res.id}
|
|
11899
12020
|
`));
|
|
11900
12021
|
}
|
|
12022
|
+
async function updateDesign(id, opts = {}) {
|
|
12023
|
+
if (!opts.file && !opts.name) {
|
|
12024
|
+
throw new Error("Nothing to update \u2014 pass --file <config> and/or --name <name>.");
|
|
12025
|
+
}
|
|
12026
|
+
const api = client3();
|
|
12027
|
+
const current = await api.get(`designs/${id}`).json();
|
|
12028
|
+
const body = {};
|
|
12029
|
+
if (opts.file) body.config = await readJsonInput(opts.file);
|
|
12030
|
+
if (opts.name) body.name = opts.name;
|
|
12031
|
+
if (current.version !== void 0) body.version = current.version;
|
|
12032
|
+
const res = await api.put(`designs/${id}`, { json: body }).json();
|
|
12033
|
+
emit(
|
|
12034
|
+
res,
|
|
12035
|
+
!!opts.json,
|
|
12036
|
+
() => process.stdout.write(`${res.id} v${res.version ?? "?"}
|
|
12037
|
+
`)
|
|
12038
|
+
);
|
|
12039
|
+
}
|
|
11901
12040
|
|
|
11902
12041
|
// src/commands/skill.ts
|
|
11903
12042
|
init_esm_shims();
|
|
@@ -11966,7 +12105,7 @@ async function formatCliError(err, json) {
|
|
|
11966
12105
|
|
|
11967
12106
|
// src/version.ts
|
|
11968
12107
|
init_esm_shims();
|
|
11969
|
-
var CLI_VERSION = "1.1.
|
|
12108
|
+
var CLI_VERSION = "1.1.8";
|
|
11970
12109
|
|
|
11971
12110
|
// src/index.ts
|
|
11972
12111
|
program.name("wardrobe").description("Wardrobe configurator CLI").version(CLI_VERSION);
|
|
@@ -11991,8 +12130,8 @@ program.command("describe <idOrFile>").description("Describe a config (local fil
|
|
|
11991
12130
|
if (idOrFile === "-" || existsSync3(idOrFile)) {
|
|
11992
12131
|
config = await readJsonInput(idOrFile);
|
|
11993
12132
|
} else {
|
|
11994
|
-
const
|
|
11995
|
-
const design = await
|
|
12133
|
+
const client4 = createClient(readConfig()?.accessToken ?? null);
|
|
12134
|
+
const design = await client4.get(`designs/${idOrFile}`).json();
|
|
11996
12135
|
config = design.config;
|
|
11997
12136
|
}
|
|
11998
12137
|
const lines = describeInput(config, o.locale ?? "en");
|
|
@@ -12003,8 +12142,21 @@ program.command("plan <file>").description("Dry-run: price a config + BOM previe
|
|
|
12003
12142
|
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
12143
|
var designs = program.command("designs").description("Manage saved designs");
|
|
12005
12144
|
designs.command("list").description("List designs in a project").requiredOption("--project <id>", "project id").option("--json", "machine-readable output").action((o) => listDesigns(o));
|
|
12145
|
+
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));
|
|
12146
|
+
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));
|
|
12147
|
+
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
12148
|
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));
|
|
12149
|
+
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));
|
|
12150
|
+
projects.command("get <id>").description("Show one project").option("--json", "machine-readable output").action((id, o) => getProject(id, o));
|
|
12151
|
+
projects.command("members <id>").description("List a project's owner and members").option("--json", "machine-readable output").action((id, o) => projectMembers(id, o));
|
|
12152
|
+
var customers = program.command("customers").description("Read customers");
|
|
12153
|
+
customers.command("list").description("List your customers").option("--json", "machine-readable output").action((o) => listCustomers(o));
|
|
12154
|
+
customers.command("get <id>").description("Show one customer").option("--json", "machine-readable output").action((id, o) => getCustomer(id, o));
|
|
12155
|
+
customers.command("projects <id>").description("List a customer's projects").option("--json", "machine-readable output").action((id, o) => customerProjects(id, o));
|
|
12156
|
+
var orders = program.command("orders").description("Read orders");
|
|
12157
|
+
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));
|
|
12158
|
+
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));
|
|
12159
|
+
program.command("help-texts").description("Read the help-text table").option("--json", "machine-readable output").action((o) => helpTexts(o));
|
|
12008
12160
|
var skill = program.command("skill").description("Manage the bundled Claude Skill");
|
|
12009
12161
|
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
12162
|
program.parseAsync().catch(async (err) => {
|
package/package.json
CHANGED
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,89 @@ 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
|
|
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
|
-
##
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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. The same approver
|
|
103
|
+
also receives **`staff:write`**, which lets `designs create` / `designs update` land in
|
|
104
|
+
a project the admin does not own. A key logged in before 2026-09-05 lacks it — a `403
|
|
105
|
+
Forbidden` on a write to someone else's project means `wardrobe login` again.
|
|
106
|
+
|
|
107
|
+
Never tell a user something does not exist when you only know your key cannot see it.
|
|
108
|
+
Say which it is: `403 Forbidden` means the row is someone else's, `404` means it is gone.
|
|
109
|
+
|
|
110
|
+
### Not available here
|
|
111
|
+
|
|
112
|
+
Don't reach for these; they are not CLI verbs and not agent-reachable:
|
|
113
|
+
|
|
114
|
+
- **Anything under `/admin`**, plus commissions and invitations — barred to every agent
|
|
115
|
+
key at the mount (`403 agent_not_permitted`). No scope unlocks them.
|
|
116
|
+
- **Quotation history.** `POST /quotations/pdf` is open to other clients, but reading a
|
|
117
|
+
project's past quotes is JWT-only — a human in the web app.
|
|
118
|
+
- **Manufacturing-files export.** Staff-only, through the web app.
|
|
45
119
|
|
|
46
120
|
## WardrobeConfig shape (top-level)
|
|
47
121
|
|
|
@@ -65,10 +139,12 @@ Prefer generating the skeleton with `wardrobe init` rather than hand-writing it
|
|
|
65
139
|
|
|
66
140
|
## Guardrails — committing to an order requires a human
|
|
67
141
|
|
|
68
|
-
- **The CLI
|
|
69
|
-
`
|
|
70
|
-
|
|
71
|
-
|
|
142
|
+
- **The CLI can read orders but never place one.** Its key carries `profile:read`,
|
|
143
|
+
`projects:read`, `customers:read`, `designs:read`, `designs:write`, `orders:read`,
|
|
144
|
+
`price:read` (+ `staff:read` and `staff:write` for staff) — **every write beyond designs is absent**,
|
|
145
|
+
and so is every admin scope. `wardrobe orders list` and `orders pdf` work; creating
|
|
146
|
+
or changing an order, a customer or a quotation answers `403 insufficient_scope`.
|
|
147
|
+
Don't attempt it, and don't attempt admin/catalog operations.
|
|
72
148
|
- To turn a saved design into an order, direct the **human** to the web app: they
|
|
73
149
|
open the project, review the design, and complete the **verify-details** flow —
|
|
74
150
|
which computes the final price, freezes the PDF, and advances the order. Only a
|