@lahat-group/cli 2.1.2 → 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 +2 -2
- package/package.json +1 -1
- package/skill/SKILL.md +92 -60
- package/skill/staff.md +59 -0
package/dist/index.js
CHANGED
|
@@ -22,7 +22,7 @@ Expecting one of '${n.join("', '")}'`);return this._lifeCycleHooks[e]?this._life
|
|
|
22
22
|
`),this._exit(0,"commander.version",e)}),this}description(e,t){return e===void 0&&t===void 0?this._description:(this._description=e,t&&(this._argsDescription=t),this)}summary(e){return e===void 0?this._summary:(this._summary=e,this)}alias(e){if(e===void 0)return this._aliases[0];let t=this;if(this.commands.length!==0&&this.commands[this.commands.length-1]._executableHandler&&(t=this.commands[this.commands.length-1]),e===t._name)throw new Error("Command alias can't be the same as its name");let n=this.parent?._findCommand(e);if(n){let i=[n.name()].concat(n.aliases()).join("|");throw new Error(`cannot add alias '${e}' to command '${this.name()}' as already have command '${i}'`)}return t._aliases.push(e),this}aliases(e){return e===void 0?this._aliases:(e.forEach(t=>this.alias(t)),this)}usage(e){if(e===void 0){if(this._usage)return this._usage;let t=this.registeredArguments.map(n=>xn(n));return[].concat(this.options.length||this._helpOption!==null?"[options]":[],this.commands.length?"[command]":[],this.registeredArguments.length?t:[]).join(" ")}return this._usage=e,this}name(e){return e===void 0?this._name:(this._name=e,this)}nameFromFilename(e){return this._name=$.basename(e,$.extname(e)),this}executableDir(e){return e===void 0?this._executableDir:(this._executableDir=e,this)}helpInformation(e){let t=this.createHelp();return t.helpWidth===void 0&&(t.helpWidth=e&&e.error?this._outputConfiguration.getErrHelpWidth():this._outputConfiguration.getOutHelpWidth()),t.formatHelp(this,t)}_getHelpContext(e){e=e||{};let t={error:!!e.error},n;return t.error?n=i=>this._outputConfiguration.writeErr(i):n=i=>this._outputConfiguration.writeOut(i),t.write=e.write||n,t.command=this,t}outputHelp(e){let t;typeof e=="function"&&(t=e,e=void 0);let n=this._getHelpContext(e);this._getCommandAndAncestors().reverse().forEach(o=>o.emit("beforeAllHelp",n)),this.emit("beforeHelp",n);let i=this.helpInformation(n);if(t&&(i=t(i),typeof i!="string"&&!Buffer.isBuffer(i)))throw new Error("outputHelp callback must return a string or a Buffer");n.write(i),this._getHelpOption()?.long&&this.emit(this._getHelpOption().long),this.emit("afterHelp",n),this._getCommandAndAncestors().forEach(o=>o.emit("afterAllHelp",n))}helpOption(e,t){return typeof e=="boolean"?(e?this._helpOption=this._helpOption??void 0:this._helpOption=null,this):(e=e??"-h, --help",t=t??"display help for command",this._helpOption=this.createOption(e,t),this)}_getHelpOption(){return this._helpOption===void 0&&this.helpOption(void 0,void 0),this._helpOption}addHelpOption(e){return this._helpOption=e,this}help(e){this.outputHelp(e);let t=g.exitCode||0;t===0&&e&&typeof e!="function"&&e.error&&(t=1),this._exit(t,"commander.help","(outputHelp)")}addHelpText(e,t){let n=["beforeAll","before","after","afterAll"];if(!n.includes(e))throw new Error(`Unexpected value for position to addHelpText.
|
|
23
23
|
Expecting one of '${n.join("', '")}'`);let i=`${e}Help`;return this.on(i,o=>{let s;typeof t=="function"?s=t({error:o.error,command:o.command}):s=t,s&&o.write(`${s}
|
|
24
24
|
`)}),this}_outputHelpIfRequested(e){let t=this._getHelpOption();t&&e.find(i=>t.is(i))&&(this.outputHelp(),this._exit(0,"commander.helpDisplayed","(outputHelp)"))}};function Le(r){return r.map(e=>{if(!e.startsWith("--inspect"))return e;let t,n="127.0.0.1",i="9229",o;return(o=e.match(/^(--inspect(-brk)?)$/))!==null?t=o[1]:(o=e.match(/^(--inspect(-brk|-port)?)=([^:]+)$/))!==null?(t=o[1],/^\d+$/.test(o[3])?i=o[3]:n=o[3]):(o=e.match(/^(--inspect(-brk|-port)?)=([^:]+):(\d+)$/))!==null&&(t=o[1],n=o[3],i=o[4]),t&&i!=="0"?`${t}=${n}:${parseInt(i)+1}`:e})}Me.Command=ge});var ze=k(x=>{"use strict";u();var{Argument:Ue}=z(),{Command:be}=Be(),{CommanderError:An,InvalidArgumentError:We}=F(),{Help:Sn}=ce(),{Option:Je}=he();x.program=new be;x.createCommand=r=>new be(r);x.createOption=(r,e)=>new Je(r,e);x.createArgument=(r,e)=>new Ue(r,e);x.Command=be;x.Option=Je;x.Argument=Ue;x.Help=Sn;x.CommanderError=An;x.InvalidArgumentError=We;x.InvalidOptionArgumentError=We});u();u();var Ge=mn(ze(),1),{program:b,createCommand:Er,createArgument:Pr,createOption:kr,CommanderError:Rr,InvalidArgumentError:Tr,InvalidOptionArgumentError:qr,Command:Nr,Argument:Ir,Option:Dr,Help:Hr}=Ge.default;u();import{existsSync as Wn}from"fs";u();u();u();u();var O=class extends Error{response;request;options;constructor(e,t,n){let i=e.status||e.status===0?e.status:"",o=e.statusText??"",s=`${i} ${o}`.trim(),c=s?`status code ${s}`:"an unknown error";super(`Request failed with ${c}: ${t.method} ${t.url}`),this.name="HTTPError",this.response=e,this.request=t,this.options=n}};u();var D=class extends Error{name="NonError";value;constructor(e){let t="Non-error value was thrown";try{typeof e=="string"?t=e:e&&typeof e=="object"&&"message"in e&&typeof e.message=="string"&&(t=e.message)}catch{}super(t),this.value=e}};u();var H=class extends Error{name="ForceRetryError";customDelay;code;customRequest;constructor(e){let t=e?.cause?e.cause instanceof Error?e.cause:new D(e.cause):void 0;super(e?.code?`Forced retry: ${e.code}`:"Forced retry",t?{cause:t}:void 0),this.customDelay=e?.delay,this.code=e?.code,this.customRequest=e?.request}};u();u();var ye=(()=>{let r=!1,e=!1,t=typeof globalThis.ReadableStream=="function",n=typeof globalThis.Request=="function";if(t&&n)try{e=new globalThis.Request("https://empty.invalid",{body:new globalThis.ReadableStream,method:"POST",get duplex(){return r=!0,"half"}}).headers.has("Content-Type")}catch(i){if(i instanceof Error&&i.message==="unsupported BodyInit type")return!1;throw i}return r&&!e})(),Ke=typeof globalThis.AbortController=="function",G=typeof globalThis.AbortSignal=="function"&&typeof globalThis.AbortSignal.any=="function",Ye=typeof globalThis.ReadableStream=="function",Qe=typeof globalThis.FormData=="function",K=["get","post","put","patch","head","delete"],En=()=>{};En();var Xe={json:"application/json",text:"text/*",formData:"multipart/form-data",arrayBuffer:"*/*",blob:"*/*",bytes:"*/*"},Y=2147483647,Ze=new TextEncoder().encode("------WebKitFormBoundaryaxpyiPgbbPti10Rw").length,Q=Symbol("stop"),L=class{options;constructor(e){this.options=e}},et=r=>new L(r),tt={json:!0,parseJson:!0,stringifyJson:!0,searchParams:!0,prefixUrl:!0,retry:!0,timeout:!0,hooks:!0,throwHttpErrors:!0,onDownloadProgress:!0,onUploadProgress:!0,fetch:!0,context:!0},nt={next:!0},rt={method:!0,headers:!0,body:!0,mode:!0,credentials:!0,cache:!0,redirect:!0,referrer:!0,referrerPolicy:!0,integrity:!0,keepalive:!0,signal:!0,window:!0,duplex:!0};var Pn=r=>{if(!r)return 0;if(r instanceof FormData){let e=0;for(let[t,n]of r)e+=Ze,e+=new TextEncoder().encode(`Content-Disposition: form-data; name="${t}"`).length,e+=typeof n=="string"?new TextEncoder().encode(n).length:n.size;return e}if(r instanceof Blob)return r.size;if(r instanceof ArrayBuffer)return r.byteLength;if(typeof r=="string")return new TextEncoder().encode(r).length;if(r instanceof URLSearchParams)return new TextEncoder().encode(r.toString()).length;if("byteLength"in r)return r.byteLength;if(typeof r=="object"&&r!==null)try{let e=JSON.stringify(r);return new TextEncoder().encode(e).length}catch{return 0}return 0},it=(r,e,t)=>{let n,i=0;return r.pipeThrough(new TransformStream({transform(o,s){if(s.enqueue(o),n){i+=n.byteLength;let c=e===0?0:i/e;c>=1&&(c=1-Number.EPSILON),t?.({percent:c,totalBytes:Math.max(e,i),transferredBytes:i},n)}n=o},flush(){n&&(i+=n.byteLength,t?.({percent:1,totalBytes:Math.max(e,i),transferredBytes:i},n))}}))},ot=(r,e)=>{if(!r.body)return r;if(r.status===204)return new Response(null,{status:r.status,statusText:r.statusText,headers:r.headers});let t=Math.max(0,Number(r.headers.get("content-length"))||0);return new Response(it(r.body,t,e),{status:r.status,statusText:r.statusText,headers:r.headers})},st=(r,e,t)=>{if(!r.body)return r;let n=Pn(t??r.body);return new Request(r,{duplex:"half",body:it(r.body,n,e)})};u();u();var S=r=>r!==null&&typeof r=="object";var M=(...r)=>{for(let e of r)if((!S(e)||Array.isArray(e))&&e!==void 0)throw new TypeError("The `options` argument must be an object");return je({},...r)},we=(r={},e={})=>{let t=new globalThis.Headers(r),n=e instanceof globalThis.Headers,i=new globalThis.Headers(e);for(let[o,s]of i.entries())n&&s==="undefined"||s===void 0?t.delete(o):t.set(o,s);return t};function X(r,e,t){return Object.hasOwn(e,t)&&e[t]===void 0?[]:je(r[t]??[],e[t]??[])}var _e=(r={},e={})=>({beforeRequest:X(r,e,"beforeRequest"),beforeRetry:X(r,e,"beforeRetry"),afterResponse:X(r,e,"afterResponse"),beforeError:X(r,e,"beforeError")}),kn=(r,e)=>{let t=new URLSearchParams;for(let n of[r,e])if(n!==void 0)if(n instanceof URLSearchParams)for(let[i,o]of n.entries())t.append(i,o);else if(Array.isArray(n))for(let i of n){if(!Array.isArray(i)||i.length!==2)throw new TypeError("Array search parameters must be provided in [[key, value], ...] format");t.append(String(i[0]),String(i[1]))}else if(S(n))for(let[i,o]of Object.entries(n))o!==void 0&&t.append(i,String(o));else{let i=new URLSearchParams(n);for(let[o,s]of i.entries())t.append(o,s)}return t},je=(...r)=>{let e={},t={},n={},i,o=[];for(let s of r)if(Array.isArray(s))Array.isArray(e)||(e=[]),e=[...e,...s];else if(S(s)){for(let[c,a]of Object.entries(s)){if(c==="signal"&&a instanceof globalThis.AbortSignal){o.push(a);continue}if(c==="context"){if(a!=null&&(!S(a)||Array.isArray(a)))throw new TypeError("The `context` option must be an object");e={...e,context:a==null?{}:{...e.context,...a}};continue}if(c==="searchParams"){a==null?i=void 0:i=i===void 0?a:kn(i,a);continue}S(a)&&c in e&&(a=je(e[c],a)),e={...e,[c]:a}}S(s.hooks)&&(n=_e(n,s.hooks),e.hooks=n),S(s.headers)&&(t=we(t,s.headers),e.headers=t)}return i!==void 0&&(e.searchParams=i),o.length>0&&(o.length===1?e.signal=o[0]:G?e.signal=AbortSignal.any(o):e.signal=o.at(-1)),e};u();var ct=r=>K.includes(r)?r.toUpperCase():r,Rn=["get","put","head","delete","options","trace"],Tn=[408,413,429,500,502,503,504],qn=[413,429,503],at={limit:2,methods:Rn,statusCodes:Tn,afterStatusCodes:qn,maxRetryAfter:Number.POSITIVE_INFINITY,backoffLimit:Number.POSITIVE_INFINITY,delay:r=>.3*2**(r-1)*1e3,jitter:void 0,retryOnTimeout:!1},ut=(r={})=>{if(typeof r=="number")return{...at,limit:r};if(r.methods&&!Array.isArray(r.methods))throw new Error("retry.methods must be an array");if(r.methods&&=r.methods.map(t=>t.toLowerCase()),r.statusCodes&&!Array.isArray(r.statusCodes))throw new Error("retry.statusCodes must be an array");let e=Object.fromEntries(Object.entries(r).filter(([,t])=>t!==void 0));return{...at,...e}};u();u();var R=class extends Error{request;constructor(e){super(`Request timed out: ${e.method} ${e.url}`),this.name="TimeoutError",this.request=e}};async function ve(r,e,t,n){return new Promise((i,o)=>{let s=setTimeout(()=>{t&&t.abort(),o(new R(r))},n.timeout);n.fetch(r,e).then(i).catch(o).then(()=>{clearTimeout(s)})})}u();async function Oe(r,{signal:e}){return new Promise((t,n)=>{e&&(e.throwIfAborted(),e.addEventListener("abort",i,{once:!0}));function i(){clearTimeout(o),n(e.reason)}let o=setTimeout(()=>{e?.removeEventListener("abort",i),t()},r)})}u();var lt=(r,e)=>{let t={};for(let n in e)Object.hasOwn(e,n)&&!(n in rt)&&!(n in tt)&&(!(n in r)||n in nt)&&(t[n]=e[n]);return t},dt=r=>r===void 0?!1:Array.isArray(r)?r.length>0:r instanceof URLSearchParams?r.size>0:typeof r=="object"?Object.keys(r).length>0:typeof r=="string"?r.trim().length>0:!!r;u();function ht(r){return r instanceof O||r?.name===O.name}function mt(r){return r instanceof R||r?.name===R.name}var B=class r{static create(e,t){let n=new r(e,t),i=async()=>{if(typeof n.#e.timeout=="number"&&n.#e.timeout>Y)throw new RangeError(`The \`timeout\` option cannot be greater than ${Y}`);await Promise.resolve();let s=await n.#b();for(let c of n.#e.hooks.afterResponse){let a=n.#h(s.clone()),l;try{l=await c(n.request,n.#c(),a,{retryCount:n.#n})}catch(f){throw n.#o(a),n.#o(s),f}if(l instanceof L)throw n.#o(a),n.#o(s),new H(l.options);let d=l instanceof globalThis.Response?l:s;a!==d&&n.#o(a),s!==d&&n.#o(s),s=d}if(n.#h(s),!s.ok&&(typeof n.#e.throwHttpErrors=="function"?n.#e.throwHttpErrors(s.status):n.#e.throwHttpErrors)){let c=new O(s,n.request,n.#c());for(let a of n.#e.hooks.beforeError)c=await a(c,{retryCount:n.#n});throw c}if(n.#e.onDownloadProgress){if(typeof n.#e.onDownloadProgress!="function")throw new TypeError("The `onDownloadProgress` option must be a function");if(!Ye)throw new Error("Streams are not supported in your environment. `ReadableStream` is missing.");let c=s.clone();return n.#o(s),ot(c,n.#e.onDownloadProgress)}return s},o=n.#m(i).finally(()=>{let s=n.#s;n.#l(s?.body??void 0),n.#l(n.request.body??void 0)});for(let[s,c]of Object.entries(Xe))s==="bytes"&&typeof globalThis.Response?.prototype?.bytes!="function"||(o[s]=async()=>{n.request.headers.set("accept",n.request.headers.get("accept")||c);let a=await o;if(s==="json"){if(a.status===204)return"";let l=await a.text();return l===""?"":t.parseJson?t.parseJson(l):JSON.parse(l)}return a[s]()});return o}static#f(e){return e&&typeof e=="object"&&!Array.isArray(e)&&!(e instanceof URLSearchParams)?Object.fromEntries(Object.entries(e).filter(([,t])=>t!==void 0)):e}request;#r;#n=0;#t;#e;#s;#i;#a;constructor(e,t={}){if(this.#t=e,this.#e={...t,headers:we(this.#t.headers,t.headers),hooks:_e({beforeRequest:[],beforeRetry:[],beforeError:[],afterResponse:[]},t.hooks),method:ct(t.method??this.#t.method??"GET"),prefixUrl:String(t.prefixUrl||""),retry:ut(t.retry),throwHttpErrors:t.throwHttpErrors??!0,timeout:t.timeout??1e4,fetch:t.fetch??globalThis.fetch.bind(globalThis),context:t.context??{}},typeof this.#t!="string"&&!(this.#t instanceof URL||this.#t instanceof globalThis.Request))throw new TypeError("`input` must be a string, URL, or Request");if(this.#e.prefixUrl&&typeof this.#t=="string"){if(this.#t.startsWith("/"))throw new Error("`input` must not begin with a slash when using `prefixUrl`");this.#e.prefixUrl.endsWith("/")||(this.#e.prefixUrl+="/"),this.#t=this.#e.prefixUrl+this.#t}Ke&&G&&(this.#i=this.#e.signal??this.#t.signal,this.#r=new globalThis.AbortController,this.#e.signal=this.#i?AbortSignal.any([this.#i,this.#r.signal]):this.#r.signal),ye&&(this.#e.duplex="half"),this.#e.json!==void 0&&(this.#e.body=this.#e.stringifyJson?.(this.#e.json)??JSON.stringify(this.#e.json),this.#e.headers.set("content-type",this.#e.headers.get("content-type")??"application/json"));let n=t.headers&&new globalThis.Headers(t.headers).has("content-type");if(this.#t instanceof globalThis.Request&&(Qe&&this.#e.body instanceof globalThis.FormData||this.#e.body instanceof URLSearchParams)&&!n&&this.#e.headers.delete("content-type"),this.request=new globalThis.Request(this.#t,this.#e),dt(this.#e.searchParams)){let o="?"+(typeof this.#e.searchParams=="string"?this.#e.searchParams.replace(/^\?/,""):new URLSearchParams(r.#f(this.#e.searchParams)).toString()),s=this.request.url.replace(/(?:\?.*?)?(?=#|$)/,o);this.request=new globalThis.Request(s,this.#e)}if(this.#e.onUploadProgress){if(typeof this.#e.onUploadProgress!="function")throw new TypeError("The `onUploadProgress` option must be a function");if(!ye)throw new Error("Request streams are not supported in your environment. The `duplex` option for `Request` is not available.");this.request=this.#p(this.request,this.#e.body??void 0)}}#u(){let e=this.#e.retry.delay(this.#n),t=e;this.#e.retry.jitter===!0?t=Math.random()*e:typeof this.#e.retry.jitter=="function"&&(t=this.#e.retry.jitter(e),(!Number.isFinite(t)||t<0)&&(t=e));let n=this.#e.retry.backoffLimit??Number.POSITIVE_INFINITY;return Math.min(n,t)}async#g(e){if(this.#n++,this.#n>this.#e.retry.limit)throw e;let t=e instanceof Error?e:new D(e);if(t instanceof H)return t.customDelay??this.#u();if(!this.#e.retry.methods.includes(this.request.method.toLowerCase()))throw e;if(this.#e.retry.shouldRetry!==void 0){let n=await this.#e.retry.shouldRetry({error:t,retryCount:this.#n});if(n===!1)throw e;if(n===!0)return this.#u()}if(mt(e)&&!this.#e.retry.retryOnTimeout)throw e;if(ht(e)){if(!this.#e.retry.statusCodes.includes(e.response.status))throw e;let n=e.response.headers.get("Retry-After")??e.response.headers.get("RateLimit-Reset")??e.response.headers.get("X-RateLimit-Retry-After")??e.response.headers.get("X-RateLimit-Reset")??e.response.headers.get("X-Rate-Limit-Reset");if(n&&this.#e.retry.afterStatusCodes.includes(e.response.status)){let i=Number(n)*1e3;Number.isNaN(i)?i=Date.parse(n)-Date.now():i>=Date.parse("2024-01-01")&&(i-=Date.now());let o=this.#e.retry.maxRetryAfter??i;return i<o?i:o}if(e.response.status===413)throw e}return this.#u()}#h(e){return this.#e.parseJson&&(e.json=async()=>this.#e.parseJson(await e.text())),e}#l(e){e&&e.cancel().catch(()=>{})}#o(e){this.#l(e.body??void 0)}async#m(e){try{return await e()}catch(t){let n=Math.min(await this.#g(t),Y);if(this.#n<1)throw t;if(await Oe(n,this.#i?{signal:this.#i}:{}),t instanceof H&&t.customRequest){let i=this.#e.signal?new globalThis.Request(t.customRequest,{signal:this.#e.signal}):new globalThis.Request(t.customRequest);this.#d(i)}for(let i of this.#e.hooks.beforeRetry){let o=await i({request:this.request,options:this.#c(),error:t,retryCount:this.#n});if(o instanceof globalThis.Request){this.#d(o);break}if(o instanceof globalThis.Response)return o;if(o===Q)return}return this.#m(e)}}async#b(){this.#r?.signal.aborted&&(this.#r=new globalThis.AbortController,this.#e.signal=this.#i?AbortSignal.any([this.#i,this.#r.signal]):this.#r.signal,this.request=new globalThis.Request(this.request,{signal:this.#e.signal}));for(let t of this.#e.hooks.beforeRequest){let n=await t(this.request,this.#c(),{retryCount:this.#n});if(n instanceof Response)return n;if(n instanceof globalThis.Request){this.#d(n);break}}let e=lt(this.request,this.#e);return this.#s=this.request,this.request=this.#s.clone(),this.#e.timeout===!1?this.#e.fetch(this.#s,e):ve(this.#s,e,this.#r,this.#e)}#c(){if(!this.#a){let{hooks:e,...t}=this.#e;this.#a=Object.freeze(t)}return this.#a}#d(e){this.#a=void 0,this.request=this.#p(e)}#p(e,t){return!this.#e.onUploadProgress||!e.body?e:st(e,this.#e.onUploadProgress,t??this.#e.body??void 0)}};var xe=r=>{let e=(t,n)=>B.create(t,M(r,n));for(let t of K)e[t]=(n,i)=>B.create(n,M(r,i,{method:t}));return e.create=t=>xe(M(t)),e.extend=t=>(typeof t=="function"&&(t=t(r??{})),xe(M(r,t))),e.stop=Q,e.retry=et,e},Nn=xe(),U=Nn;var In="https://pro.lahat.group/api";function $e(){return process.env.WARDROBE_API_URL??In}function j(r){if(!r)throw new Error("Not authenticated. Run `wardrobe login` first.");return U.create({prefixUrl:$e(),headers:{Authorization:`Bearer ${r}`},retry:0})}u();import{readFileSync as Dn,writeFileSync as Hn,mkdirSync as Vn,rmSync as Fn,existsSync as Ln}from"fs";import{homedir as Mn}from"os";import{join as pt}from"path";function ft(){return process.env.WARDROBE_CONFIG_DIR??pt(Mn(),".config","wardrobe")}function Ce(){return pt(ft(),"config.json")}function _(){let r=Ce();if(!Ln(r))return null;try{return JSON.parse(Dn(r,"utf8"))}catch{return null}}function gt(r){Vn(ft(),{recursive:!0,mode:448}),Hn(Ce(),JSON.stringify(r,null,2),{mode:384})}function bt(){Fn(Ce(),{force:!0})}u();import{readFileSync as Bn}from"fs";function Un(){return new Promise((r,e)=>{let t="";process.stdin.setEncoding("utf8"),process.stdin.on("data",n=>t+=n),process.stdin.on("end",()=>r(t)),process.stdin.on("error",e)})}async function C(r){let e=r==="-"?await Un():Bn(r,"utf8");try{return JSON.parse(e)}catch(t){throw new Error(`Failed to parse JSON input: ${t.message}`)}}function h(r,e,t){e?process.stdout.write(JSON.stringify(r,null,2)+`
|
|
25
|
-
`):t()}function Ae(){return j(_()?.accessToken??null)}async function yt(r={}){let e={};for(let n of["width","height","depth"])r[n]!==void 0&&(e[n]=r[n]);let t=await Ae().get("configs/init",{searchParams:e}).json();process.stdout.write(JSON.stringify(t.config,null,2)+`
|
|
25
|
+
`):t()}function Ae(){return j(_()?.accessToken??null)}async function yt(r={}){let e={};for(let n of["width","height","ceiling","depth","position"])r[n]!==void 0&&(e[n]=r[n]);let t=await Ae().get("configs/init",{searchParams:e}).json();process.stdout.write(JSON.stringify(t.config,null,2)+`
|
|
26
26
|
`)}async function wt(r,e={}){let t=await C(r),n=await Ae().post("configs/validate",{json:{config:t}}).json();h(n,!!e.json,()=>{if(n.valid)process.stdout.write(`valid
|
|
27
27
|
`);else for(let i of n.errors)process.stdout.write(`${i.severity}: ${i.path} \u2014 ${i.message}
|
|
28
28
|
`)}),n.valid||(process.exitCode=1)}async function _t(r,e={}){let t=Ae(),n=r==="-"||Wn(r)?await C(r):(await t.get(`designs/${r}`).json()).config,i=await t.post("configs/describe",{json:{config:n,locale:e.locale??"en"}}).json();h(i.lines,!!e.json,()=>process.stdout.write(i.lines.join(`
|
|
@@ -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.2";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",r=>Number(r)).option("--height <cm>","height in cm",r=>Number(r)).option("--depth <cm>","depth in cm",r=>Number(r)).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,17 +52,34 @@ 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
|
-
1. **init** — `wardrobe init [--width <cm>] [--height <cm>] [--depth <cm>] > wardrobe.json`
|
|
48
|
-
emits a fresh, valid `WardrobeConfig` (full-width,
|
|
72
|
+
1. **init** — `wardrobe init [--width <cm>] [--position <pos>] [--height <cm> | --ceiling <cm>] [--depth <cm>] > wardrobe.json`
|
|
73
|
+
emits a fresh, valid `WardrobeConfig` on the Lahat Pro defaults below (full-width,
|
|
74
|
+
split into columns). `--width` is the **gross** wall measurement; the server turns it
|
|
75
|
+
into the net width for `--position`, and sets the height/ceiling pair.
|
|
49
76
|
2. **edit** — modify `wardrobe.json` directly (see shape below).
|
|
50
77
|
3. **validate** — `wardrobe validate wardrobe.json` (or `-` for stdin). Runs the
|
|
51
78
|
**live** constraint pipeline server-side — the same rules `wardrobe price` enforces,
|
|
52
79
|
so a config `validate` approves can never be one `price` rejects.
|
|
53
80
|
Exit 0 = valid; exit 1 + structured errors otherwise. Do this after every edit.
|
|
54
81
|
4. **price** — `wardrobe price wardrobe.json` → authoritative total in **NIS** from
|
|
55
|
-
the API's unified area×rate engine.
|
|
82
|
+
the API's unified area×rate engine — **before VAT** (see below).
|
|
56
83
|
5. **plan** — `wardrobe plan wardrobe.json` → dry-run price **plus a BOM preview**.
|
|
57
84
|
Mutates nothing. Use this to preview before applying.
|
|
58
85
|
6. **apply** — `wardrobe apply wardrobe.json --project <id> [--name <name>]` → saves
|
|
@@ -60,13 +87,48 @@ retired — they are not in `catalog materials`, so don't use them.
|
|
|
60
87
|
required.** `wardrobe designs create <file> --project <id>` is the same verb under
|
|
61
88
|
the `designs` group, if that reads more clearly in context.
|
|
62
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
|
+
|
|
101
|
+
### Lahat Pro defaults — apply them unless the user says otherwise
|
|
102
|
+
|
|
103
|
+
`init`, `wardrobe prompt` and the web wizard all start from these; keep to them when you
|
|
104
|
+
edit a config by hand.
|
|
105
|
+
|
|
106
|
+
- **Width is gross.** Every width the user gives is what they measure on the wall (a niche:
|
|
107
|
+
its opening). Pass it to `init --width` with the right `--position`. The config's own
|
|
108
|
+
`width` is **net** — if you change it by hand, net = gross − 5 (corner), − 4
|
|
109
|
+
(freestanding), − 6 (niche).
|
|
110
|
+
- **Position.** Not in a niche → against a side wall: **`right-corner`** unless the user
|
|
111
|
+
says left. `freestanding` only when they say it stands free of side walls.
|
|
112
|
+
- **Height and ceiling stand 3 cm apart.** A ceiling → `--ceiling` (height = ceiling − 3,
|
|
113
|
+
at most 278). A wardrobe height → `--height` (ceiling = height + 3). Neither → 277 under
|
|
114
|
+
a 280 ceiling. Editing JSON, keep `ceilingHeight` = `height` + 3.
|
|
115
|
+
- **Depth 60 cm** (doors included) unless stated. **On the 8 cm sockel** (`"sockel": true`).
|
|
116
|
+
- **Interior colour:** warm oak mix (אלון חם מיקס, `interior-kh62-dt`) unless asked.
|
|
117
|
+
- **Drawers:** "one drawer" → a pair of small (15 cm) drawers; "two drawers" → two large
|
|
118
|
+
(33.5 cm) drawers; both on the **second level from the bottom**. A size the user names
|
|
119
|
+
wins. `wardrobe prompt` and the wizard place them; editing modules, leave one level free
|
|
120
|
+
under the stack.
|
|
121
|
+
- **Hanging rail:** one level below the highest position (one shelf above it), back at the
|
|
122
|
+
top only when it would not clear the drawers. A standard rail — a pull-down rail
|
|
123
|
+
(סרווטו, `railKind: "pull-down"`) only when the user asks for one.
|
|
124
|
+
|
|
63
125
|
### Describing a wardrobe in words
|
|
64
126
|
|
|
65
127
|
If the user describes what they want instead of giving dimensions —
|
|
66
128
|
"a 2.4 m wardrobe, drawers in the middle, glass doors on the first column" — use
|
|
67
129
|
`wardrobe prompt <projectId> "<their words>" [--locale he|en] [--out draft.json]`. It
|
|
68
130
|
writes a draft config to a file (default `./prompt-<timestamp>.json`) and prints the
|
|
69
|
-
**assumptions** it made (e.g. "assumed
|
|
131
|
+
**assumptions** it made (e.g. "assumed a right corner"), plus any adjustments and warnings.
|
|
70
132
|
Saves nothing. Then: **show the user the assumptions and ask them to confirm** → `validate`
|
|
71
133
|
→ `price` → `apply`. Treat the draft as a starting point, never as a finished design.
|
|
72
134
|
|
|
@@ -117,8 +179,8 @@ Two refusals to expect, neither of which is retryable:
|
|
|
117
179
|
user's intent on top, and tell them it changed underneath you. **Never** loop.
|
|
118
180
|
- **`423 projectLocked`** — the project has a live (non-cancelled) order, which froze
|
|
119
181
|
its designs. No design in it can be created, updated or deleted, and no new quotation
|
|
120
|
-
revision minted, until a human cancels that order —
|
|
121
|
-
|
|
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
|
|
122
184
|
`wardrobe orders list --project <id> --open` first to tell the user *why* before trying.)
|
|
123
185
|
|
|
124
186
|
Note that updating a config **clears the design's thumbnail** (the stored image was of
|
|
@@ -139,24 +201,22 @@ or a saved design id.
|
|
|
139
201
|
|
|
140
202
|
## Reading what exists
|
|
141
203
|
|
|
142
|
-
Every read the key can reach has a verb
|
|
143
|
-
customers,
|
|
144
|
-
|
|
145
|
-
`
|
|
146
|
-
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`.
|
|
147
208
|
|
|
148
209
|
| Verb | Shows |
|
|
149
210
|
|---|---|
|
|
150
211
|
| `wardrobe projects list` | Projects you can access (needed for `--project`) |
|
|
151
212
|
| `wardrobe projects list --phone <number>` | Only projects whose **customer** has that number |
|
|
152
|
-
| `wardrobe projects list --designer <idOrName>` | Only projects owned by that designer |
|
|
153
213
|
| `wardrobe projects get <id>` | One project: customer + owning designer |
|
|
154
214
|
| `wardrobe projects members <id>` | Owner plus every invited member, with roles |
|
|
155
215
|
| `wardrobe customers list` | Your customers |
|
|
156
216
|
| `wardrobe customers get <id>` | One customer |
|
|
157
217
|
| `wardrobe customers projects <id>` | That customer's projects |
|
|
158
218
|
| `wardrobe designs list --project <id>` | Designs saved in a project |
|
|
159
|
-
| `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 |
|
|
160
220
|
| `wardrobe designs list --limit <n>` `--since <ISO>` | Same, paged — `--limit` caps rows (default 20, max 100), `--since` filters by `updatedAt >=` |
|
|
161
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) |
|
|
162
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 |
|
|
@@ -166,23 +226,19 @@ answer `403 insufficient_scope` whoever signed in).
|
|
|
166
226
|
| `wardrobe orders list --limit <n>` `--since <ISO>` | Same, paged (`--since` filters by `updatedAt >=`) |
|
|
167
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 |
|
|
168
228
|
| `wardrobe orders pdf <id> --out f.pdf` | The order PDF |
|
|
169
|
-
| `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 |
|
|
170
229
|
| `wardrobe quotations list --project <id>` | That project's quote number + full revision history |
|
|
171
230
|
| `wardrobe quotations list` | Every quotation you can see, **newest-generated first** — this is "latest quotes generation" |
|
|
172
231
|
| `wardrobe quotations list --limit <n>` `--since <ISO>` | Same, paged (`--since` filters by `updatedAt >=`, i.e. last generated at/after) |
|
|
173
232
|
| `wardrobe quotations pdf <projectId> --out f.pdf` | The project's **current** quotation revision's PDF — fetches only, never mints one |
|
|
174
233
|
| `wardrobe quotations pdf <projectId> --revision <n> --out f.pdf` | A specific historical revision's PDF instead |
|
|
175
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 |
|
|
176
|
-
| `wardrobe users list` | Registered accounts, **newest-registered first** — staff only, see below |
|
|
177
|
-
| `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" |
|
|
178
|
-
| `wardrobe users by-phone <number>` | The account(s) whose **own** phone is that number (`+972…`, `05…`, `052-…` all match) — staff only |
|
|
179
235
|
| `wardrobe help-texts` | The help-text table |
|
|
180
236
|
|
|
181
|
-
`projects list` already returns each project's **customer
|
|
182
|
-
|
|
183
|
-
|
|
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.
|
|
184
240
|
|
|
185
|
-
Every cross-project
|
|
241
|
+
Every cross-project list (`designs`, `orders`, `quotations`) takes
|
|
186
242
|
`--limit` (1–100, default 20) and a `--since` cursor — pass the last row's timestamp
|
|
187
243
|
back as `--since` to page further instead of guessing an offset. A malformed `--since` or
|
|
188
244
|
an out-of-range `--limit` is a `400`, not a silently-ignored filter — read the error
|
|
@@ -193,37 +249,14 @@ rather than retrying blind.
|
|
|
193
249
|
The list is bounded by the key, so an empty result is as often a **visibility** answer as
|
|
194
250
|
a data one. `wardrobe whoami --json` reports the truth under `agent.scopes` and `role`:
|
|
195
251
|
|
|
196
|
-
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
- A key approved by an **admin or support** user *and* carrying **`staff:read`** sees
|
|
202
|
-
**every** project, customer, order and quotation in the system — and can use
|
|
203
|
-
`wardrobe users list` / `users by-phone` at all. That scope is granted only to
|
|
204
|
-
staff, only for GETs, and is listed by name on the consent screen. The same approver
|
|
205
|
-
also receives **`staff:write`**, which lets `designs create` / `designs update` land in
|
|
206
|
-
a project the admin does not own. A key logged in before 2026-09-05 lacks `staff:read`
|
|
207
|
-
and before 2026-09-12 lacks `quotations:read` — a `403 insufficient_scope` on
|
|
208
|
-
`quotations list`, or a `403 Forbidden` on a write to someone else's project, means
|
|
209
|
-
`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.
|
|
210
257
|
|
|
211
258
|
Never tell a user something does not exist when you only know your key cannot see it.
|
|
212
|
-
Say which it is: `403 Forbidden` means the row is someone else's
|
|
213
|
-
your key isn't staff at all), `404` means it is gone.
|
|
214
|
-
|
|
215
|
-
### `wardrobe users` is a directory lookup, not a customer lookup
|
|
216
|
-
|
|
217
|
-
`users list` / `users by-phone` read the `User` **account** table — designers, admins,
|
|
218
|
-
support and self-serve clients who can sign in — not the per-project `Customer` contact
|
|
219
|
-
card `wardrobe customers` reads. Each row carries the account's own `phone` (empty until
|
|
220
|
-
that user passes the onboarding phone step; staff never do). `users by-phone` matches
|
|
221
|
-
**only** that field: a customer's number never resolves to the designer who serves them,
|
|
222
|
-
so its answer is "who is this person", safe to act on as the caller's identity. Numbers
|
|
223
|
-
match as the national number, so `+972526665153` and `0526665153` are the same. More than
|
|
224
|
-
one row means the number is ambiguous (the field is self-declared, not unique) — say so
|
|
225
|
-
rather than picking one. Zero rows is normal and not an error; for the customer side of
|
|
226
|
-
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.
|
|
227
260
|
|
|
228
261
|
### Not available here
|
|
229
262
|
|
|
@@ -237,13 +270,14 @@ Don't reach for these; they are not CLI verbs and not agent-reachable:
|
|
|
237
270
|
|
|
238
271
|
```jsonc
|
|
239
272
|
{
|
|
240
|
-
"roomPosition": "
|
|
241
|
-
"width":
|
|
273
|
+
"roomPosition": "right-corner", // placement
|
|
274
|
+
"width": 115, "height": 277, "ceilingHeight": 280, "depth": 60, // cm; width is NET
|
|
275
|
+
"sockel": true,
|
|
242
276
|
"material": "<externalMaterialId>",
|
|
243
277
|
"materialInternal": "<internalMaterialId>",
|
|
244
278
|
"materialExternal": "<externalMaterialId>",
|
|
245
279
|
"columns": [ // one or more columns, widths sum to `width`
|
|
246
|
-
{ "id": "<nanoid>", "width":
|
|
280
|
+
{ "id": "<nanoid>", "width": 115, "modules": [ /* rails, shelves, drawers */ ] }
|
|
247
281
|
],
|
|
248
282
|
"ruleVersion": "<current rule version>"
|
|
249
283
|
}
|
|
@@ -257,12 +291,10 @@ Prefer generating the skeleton with `wardrobe init` rather than hand-writing it
|
|
|
257
291
|
|
|
258
292
|
- **The CLI can read orders but never place one.** Its key carries `profile:read`,
|
|
259
293
|
`projects:read`, `customers:read`, `designs:read`, `designs:write`, `orders:read`,
|
|
260
|
-
`price:read`, `quotations:read`, `quotations:write`
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
`403 insufficient_scope` (a key logged in before 2026-10-02 lacks `quotations:write` —
|
|
265
|
-
`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
|
|
266
298
|
project that has never been quoted on the web app has no revision to fetch —
|
|
267
299
|
`quotations pdf`/`quotations list --project` answer `404 quotation_not_found`, not an
|
|
268
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.
|