@modeldesk/cli 0.1.0 → 0.2.0

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/README.md CHANGED
@@ -10,7 +10,7 @@ not render, host, or publish documents.
10
10
  The CLI runs on Node.js (version 20 or newer). Run it on demand with `npx`, or install it globally.
11
11
 
12
12
  ```bash
13
- npx @modeldesk/cli bundle pull --model <modelId> --stack respec --template conceptueel-toc
13
+ npx @modeldesk/cli@latest bundle pull --model <modelId> --stack respec --template conceptueel-toc
14
14
  # or install it once
15
15
  npm install -g @modeldesk/cli
16
16
  ```
@@ -41,6 +41,38 @@ modeldesk auth status # shows who you are logged in as, and whether the crede
41
41
  modeldesk auth logout # removes the stored credential for the host
42
42
  ```
43
43
 
44
+ ## Authenticate in CI
45
+
46
+ In a pipeline the CLI authenticates with a **service account** (org settings → Service accounts),
47
+ in one of two ways. A machine run refuses to take the host from the committed `.modeldesk/config.json`,
48
+ so a PR-editable file can never decide where a credential is sent: against `modeldesk.io` set nothing
49
+ (it is the default), against any other instance pass `--host` or set `MODELDESK_HOST`, and leave
50
+ `host` out of the committed config in a repository that pulls from CI. Model, stack, template, and
51
+ version still come from the committed config.
52
+
53
+ **GitHub Actions (federated — no stored secrets).** Add a trust rule to the service account, grant
54
+ the workflow `id-token: write`, and run the CLI; it exchanges the workflow's own OIDC token itself:
55
+
56
+ ```yaml
57
+ permissions:
58
+ contents: read # any permissions block drops the defaults, so restate what the job needs
59
+ id-token: write # lets the workflow request an OIDC token; the CLI does the rest
60
+ steps:
61
+ - uses: actions/checkout@v7
62
+ - name: Pull the bundle
63
+ run: npx @modeldesk/cli@latest bundle pull --version 1.0.0
64
+ ```
65
+
66
+ **Any other CI (issued secret).** Store an issued secret in the pipeline's secret manager and set
67
+ `MODELDESK_CLIENT_ID` (the account's UUID) and `MODELDESK_CLIENT_SECRET` (the `mdsk_…` value). The
68
+ CLI mints short-lived tokens itself; nothing is written to disk.
69
+
70
+ Selection is deterministic (secret vars > GitHub federation > interactive) and a machine run never
71
+ falls back to browser login — a CI runner without machine credentials fails immediately with
72
+ guidance. `modeldesk auth status` verifies the credential with a real mint (exit non-zero on
73
+ rejection); use it sparingly, since each probe counts as credential use. Never set `NODE_DEBUG=http`
74
+ or `DEBUG=*` in CI: Node's HTTP diagnostics print the `Authorization` header.
75
+
44
76
  ## Configuration
45
77
 
46
78
  The model, stack, template, host, and version each resolve by precedence: a command-line flag, then
@@ -48,6 +80,10 @@ a `MODELDESK_*` environment variable, then a committed `.modeldesk/config.json`,
48
80
  only) a built-in default. A configured repository therefore runs `modeldesk bundle pull` with no
49
81
  arguments, while CI can override any value per build with an environment variable.
50
82
 
83
+ The CLI operates on the current working directory: run it from the directory that holds
84
+ `.modeldesk/config.json` (typically the repository root). Config in parent directories is not
85
+ searched, and a pull writes its content and manifest under the directory you run it from.
86
+
51
87
  ```jsonc
52
88
  // .modeldesk/config.json (committed to your repository)
53
89
  {
@@ -74,8 +110,9 @@ arguments, while CI can override any value per build with an environment variabl
74
110
  unique only within its stack, so both are required — pass `--stack`/`--template`, set the env
75
111
  vars, or commit them. Run `modeldesk bundle templates` to list the valid `stack` / `template`
76
112
  pairs for a model.
77
- - The API key for the (future) non-interactive CI mode is read only from `MODELDESK_API_KEY` and is
78
- never placed in the committed config.
113
+ - Machine credentials (`MODELDESK_CLIENT_ID`, `MODELDESK_CLIENT_SECRET`) are read from the
114
+ environment only and are never placed in the committed config. In a machine run the host must
115
+ also come from `--host` or `MODELDESK_HOST` (see [Authenticate in CI](#authenticate-in-ci)).
79
116
 
80
117
  ## `bundle pull`
81
118
 
package/dist/modeldesk.js CHANGED
@@ -1,41 +1,43 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'module'; const require = createRequire(import.meta.url);
3
- import{chmodSync as fe,existsSync as gt,mkdirSync as ut,readFileSync as ht,writeFileSync as yt}from"node:fs";import{homedir as wt}from"node:os";import{join as ge}from"node:path";var ue=448,he=384,u=class{dir;file;constructor(t=ge(wt(),".modeldesk")){this.dir=t,this.file=ge(t,"credentials.json")}getRefreshToken(t){let o=this.read().hosts[t]?.refreshToken;return typeof o=="string"?o:void 0}saveRefreshToken(t,o){let n=this.read();n.hosts[t]={refreshToken:o},this.write(n)}saveRefreshTokenIfPresent(t,o){o&&this.saveRefreshToken(t,o)}clearHost(t){let o=this.read();return o.hosts[t]?(delete o.hosts[t],this.write(o),!0):!1}read(){if(!gt(this.file))return{hosts:{}};try{let t=JSON.parse(ht(this.file,"utf8"));if(t&&typeof t=="object"&&t.hosts&&typeof t.hosts=="object")return t}catch{}return{hosts:{}}}write(t){ut(this.dir,{recursive:!0,mode:ue}),yt(this.file,`${JSON.stringify(t,null,2)}
4
- `,{mode:he}),fe(this.dir,ue),fe(this.file,he)}};var vt=e=>{let t=e.split(".");if(t.length===3)try{let o=Buffer.from(t[1],"base64url").toString("utf8"),n=JSON.parse(o);return n&&typeof n=="object"?n:void 0}catch{return}},I=e=>{let t=vt(e);if(t)return t.name&&t.email?`${t.name} <${t.email}>`:t.email??t.name??t.sub};import{spawn as kt}from"node:child_process";import{platform as Ct}from"node:process";var ye=e=>{if(!xt(e))return;let{command:t,args:o,verbatim:n}=bt(e);try{let r=kt(t,o,{stdio:"ignore",detached:!0,windowsVerbatimArguments:n});r.on("error",()=>{}),r.unref()}catch{}},xt=e=>{try{let{protocol:t}=new URL(e);return t==="http:"||t==="https:"}catch{return!1}},bt=(e,t=Ct)=>{switch(t){case"darwin":return{command:"open",args:[e]};case"win32":return{command:"cmd",args:["/c","start",'""',Tt(e)],verbatim:!0};default:return{command:"xdg-open",args:[e]}}},Tt=e=>e.replace(/[&|<>^()%"]/g,t=>`^${t}`);var P="modeldesk-cli",we="openid",G="/callback";var i=class extends Error{exitCode;constructor(t,o=1){super(t),this.name="CliError",this.exitCode=o}};var w=async e=>{let t=`${e.replace(/\/+$/,"")}/.well-known/openid-configuration`,o;try{o=await fetch(t,{headers:{Accept:"application/json"},signal:AbortSignal.timeout(3e4)})}catch(r){throw new i(`Could not reach the auth server at ${e} (${r.message}).`)}if(!o.ok)throw new i(`OpenID discovery failed at ${t}: ${o.status} ${o.statusText}`);let n=await o.json();if(!n.authorization_endpoint||!n.token_endpoint)throw new i(`OpenID discovery at ${t} did not return the authorization and token endpoints.`);return{authorizationEndpoint:n.authorization_endpoint,tokenEndpoint:n.token_endpoint}};import{createServer as Pt}from"node:http";var Et=3e5,ve=(e,t=Et)=>new Promise((o,n)=>{let r,s=new Promise((l,f)=>{r={resolve:l,reject:f}}),a=Pt((l,f)=>{let p=new URL(l.url??"/","http://127.0.0.1");if(p.pathname!==G){f.writeHead(404,{"Content-Type":"text/plain",Connection:"close"}).end("Not found");return}$t(p,f,e,r)}),c=setTimeout(()=>r.reject(new i("Timed out waiting for the browser login to complete.")),t);c.unref();let m=!1,d=()=>{m||(m=!0,clearTimeout(c),a.close())};s.then(d,d),a.once("error",l=>n(new i(`Could not start the loopback listener: ${l.message}`))),a.listen(0,"127.0.0.1",()=>{let{port:l}=a.address();o({redirectUri:`http://127.0.0.1:${l}${G}`,waitForCode:()=>s,close:d})})}),$t=(e,t,o,n)=>{if(e.searchParams.get("state")!==o){t.writeHead(404,{"Content-Type":"text/plain",Connection:"close"}).end("Unexpected request");return}let r=e.searchParams.get("error");if(r){let a=e.searchParams.get("error_description");q(t,"Sign-in failed","Sign-in failed. You can close this window and return to the terminal."),n.reject(new i(`Authorization failed: ${a?`${r} (${a})`:r}`));return}let s=e.searchParams.get("code");if(!s){q(t,"Sign-in failed","No authorization code was returned. You can close this window."),n.reject(new i("Login failed: no authorization code was returned."));return}q(t,"Signed in","You are signed in to the ModelDesk CLI. You can close this window and return to the terminal."),n.resolve(s)},q=(e,t,o)=>{let n=`<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${t}</title></head><body style="font-family: system-ui, sans-serif; text-align: center; padding: 3rem;"><h1>${t}</h1><p>${o}</p></body></html>`;e.writeHead(200,{"Content-Type":"text/html; charset=utf-8",Connection:"close"}).end(n)};import{createHash as St,randomBytes as ke}from"node:crypto";var V=e=>e.toString("base64url"),Ce=()=>{let e=V(ke(32)),t=V(St("sha256").update(e).digest());return{verifier:e,challenge:t,method:"S256"}},xe=()=>V(ke(16));var L=class extends i{oauthError;constructor(t,o){super(t),this.name="TokenEndpointError",this.oauthError=o}},be=e=>e instanceof L&&e.oauthError==="invalid_grant",Te=(e,t)=>Pe(e,new URLSearchParams({grant_type:"authorization_code",code:t.code,redirect_uri:t.redirectUri,client_id:P,code_verifier:t.codeVerifier})),A=(e,t)=>Pe(e,new URLSearchParams({grant_type:"refresh_token",refresh_token:t,client_id:P})),Pe=async(e,t)=>{let o;try{o=await fetch(e,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Accept:"application/json"},body:t.toString(),signal:AbortSignal.timeout(3e4)})}catch(r){throw new i(`Could not reach the token endpoint (${r.message}).`)}if(!o.ok){let{message:r,code:s}=await Rt(o);throw new L(`Token request failed: ${r}`,s)}let n=await o.json();if(!n.access_token)throw new i("The token response did not include an access token.");return{accessToken:n.access_token,refreshToken:n.refresh_token,expiresInSeconds:n.expires_in??0}},Rt=async e=>{try{if((e.headers.get("content-type")??"").includes("json")){let o=await e.json();if(o.error)return{message:o.error_description?`${o.error} (${o.error_description})`:o.error,code:o.error}}}catch{}return{message:`${e.status} ${e.statusText}`.trim()}};var _={info:e=>process.stderr.write(`${e}
5
- `)},M=async(e,t)=>{let{authorizationEndpoint:o,tokenEndpoint:n}=await w(e.issuer),r=Ce(),s=xe(),a=await ve(s);try{let c=It(o,a.redirectUri,r.challenge,s);t.info(`Opening your browser to sign in to ${e.host}...`),t.info(`If it does not open automatically, visit:
6
- ${c}`),ye(c);let m=await a.waitForCode();return await Te(n,{code:m,redirectUri:a.redirectUri,codeVerifier:r.verifier})}finally{a.close()}},It=(e,t,o,n)=>{let r=new URL(e);return r.searchParams.set("response_type","code"),r.searchParams.set("client_id",P),r.searchParams.set("redirect_uri",t),r.searchParams.set("scope",we),r.searchParams.set("state",n),r.searchParams.set("code_challenge",o),r.searchParams.set("code_challenge_method","S256"),r.toString()};var Lt="http://localhost:8080",At="http://localhost:8082",_t="localhost",Mt=e=>{let t=e.trim(),o=/:\/\//.test(t)?t:`https://${t}`;try{return new URL(o).hostname}catch{return t.replace(/^[a-z][a-z0-9+.-]*:\/\//i,"").split("/")[0].split(":")[0]}},Ot=e=>e==="localhost"||e==="127.0.0.1"||e==="::1"||e==="[::1]",Y=e=>{let t=Mt(e);return Ot(t)?{host:_t,apiBase:Lt,issuer:At}:{host:t,apiBase:`https://api.${t}`,issuer:`https://auth.${t}`}};import{existsSync as Ee,readFileSync as Dt}from"node:fs";import{dirname as Ft,join as $e,parse as Nt}from"node:path";var O=".modeldesk",jt="config.json",D=e=>e.configPath!==void 0,Q=e=>{let t=Nt(e).root,o=e;for(;;){let n=$e(o,O);if(Ee(n)){let r=$e(n,jt),s=Ee(r);return{config:s?Bt(r):{},configPath:s?r:void 0,repoRoot:o}}if(o===t)return{config:{},repoRoot:e};o=Ft(o)}},Bt=e=>{let t;try{t=Dt(e,"utf8")}catch(r){throw new i(`Could not read ${e}: ${r.message}`)}let o;try{o=JSON.parse(t)}catch(r){throw new i(`${e} is not valid JSON: ${r.message}`)}if(o===null||typeof o!="object"||Array.isArray(o))throw new i(`${e} must contain a JSON object.`);let n=o;return{host:E(n,"host",e),model:E(n,"model",e),stack:E(n,"stack",e),template:E(n,"template",e),version:E(n,"version",e)}},E=(e,t,o)=>{let n=e[t];if(n!==void 0){if(typeof n!="string")throw new i(`${o}: "${t}" must be a string.`);return n}};var X="modeldesk.io",g={host:"MODELDESK_HOST",model:"MODELDESK_MODEL",stack:"MODELDESK_STACK",template:"MODELDESK_TEMPLATE",version:"MODELDESK_VERSION",apiKey:"MODELDESK_API_KEY"},$=(...e)=>e.find(t=>t!==void 0&&t!==""),Z=(e,t,o)=>({host:$(e.host,t[g.host],o.host)??X,model:$(e.model,t[g.model],o.model),stack:$(e.stack,t[g.stack],o.stack),template:$(e.template,t[g.template],o.template),version:$(e.version,t[g.version],o.version)});import{parseArgs as Ht}from"node:util";var v=(e,t)=>{try{return Ht({args:e,options:t,allowPositionals:!1,strict:!0})}catch(o){throw new i(o.message)}};var Se=(e,t,o)=>{let n=t!==void 0||o[g.host]!==void 0,r=e.config.host;!n&&D(e)&&r&&r!==X&&process.stderr.write(`note: using host '${r}' from ${e.configPath} (it drives login and API calls).
7
- `)},k=e=>{let{values:t}=v(e.argv,{host:{type:"string"}}),o=Q(e.cwd),n=Z({host:t.host},e.env,o.config);return Se(o,t.host,e.env),Y(n.host)},F=(e,t)=>{let o=Q(e.cwd),n=Z(t,e.env,o.config);if(Se(o,t.host,e.env),!n.model)throw new i(`No model specified. Pass --model, set ${g.model}, or add "model" to .modeldesk/config.json.`);return{instance:Y(n.host),model:n.model,stack:n.stack,template:n.template,version:n.version,loaded:o}},ee=(e,t,o,n)=>{let r=e?.trim();if(!r)throw new i(`No ${t} specified. Pass ${n}, set ${o}, or add "${t}" to .modeldesk/config.json. Run 'modeldesk bundle templates' to list a model's stacks and templates.`);return r};var Re=async e=>{let t=k(e),o=await M(t,_),n=new u;o.refreshToken?n.saveRefreshToken(t.host,o.refreshToken):n.clearHost(t.host);let r=I(o.accessToken);return process.stdout.write(`Logged in to ${t.host}${r?` as ${r}`:""}.
3
+ import{chmodSync as xe,existsSync as Ft,mkdirSync as Ht,readFileSync as Ut,writeFileSync as jt}from"node:fs";import{homedir as Bt}from"node:os";import{join as $e}from"node:path";var Ie=448,Pe=384,f=class{dir;file;constructor(t=$e(Bt(),".modeldesk")){this.dir=t,this.file=$e(t,"credentials.json")}getRefreshToken(t){let n=this.read().hosts[t]?.refreshToken;return typeof n=="string"?n:void 0}saveRefreshToken(t,n){let o=this.read();o.hosts[t]={refreshToken:n},this.write(o)}saveRefreshTokenIfPresent(t,n){n&&this.saveRefreshToken(t,n)}clearHost(t){let n=this.read();return n.hosts[t]?(delete n.hosts[t],this.write(n),!0):!1}read(){if(!Ft(this.file))return{hosts:{}};try{let t=JSON.parse(Ut(this.file,"utf8"));if(t&&typeof t=="object"&&t.hosts&&typeof t.hosts=="object")return t}catch{}return{hosts:{}}}write(t){Ht(this.dir,{recursive:!0,mode:Ie}),jt(this.file,`${JSON.stringify(t,null,2)}
4
+ `,{mode:Pe}),xe(this.dir,Ie),xe(this.file,Pe)}};var te=e=>{let t=e.split(".");if(t.length===3)try{let n=Buffer.from(t[1],"base64url").toString("utf8"),o=JSON.parse(n);return o&&typeof o=="object"&&!Array.isArray(o)?o:void 0}catch{return}},D=e=>{let t=te(e);if(t)return t.name&&t.email?`${t.name} <${t.email}>`:t.email??t.name??t.sub};import{spawn as zt}from"node:child_process";import{platform as Jt}from"node:process";var Re=e=>{if(!Gt(e))return;let{command:t,args:n,verbatim:o}=qt(e);try{let r=zt(t,n,{stdio:"ignore",detached:!0,windowsVerbatimArguments:o});r.on("error",()=>{}),r.unref()}catch{}},Gt=e=>{try{let{protocol:t}=new URL(e);return t==="http:"||t==="https:"}catch{return!1}},qt=(e,t=Jt)=>{switch(t){case"darwin":return{command:"open",args:[e]};case"win32":return{command:"cmd",args:["/c","start",'""',Wt(e)],verbatim:!0};default:return{command:"xdg-open",args:[e]}}},Wt=e=>e.replace(/[&|<>^()%"]/g,t=>`^${t}`);var R="modeldesk-cli",Ae="modeldesk-federation",_e="openid",ne="/callback";var i=class extends Error{exitCode;constructor(t,n=1){super(t),this.name="CliError",this.exitCode=n}};var w=async e=>{let t=`${e.replace(/\/+$/,"")}/.well-known/openid-configuration`,n;try{n=await fetch(t,{headers:{Accept:"application/json"},signal:AbortSignal.timeout(3e4)})}catch(r){throw new i(`Could not reach the auth server at ${e} (${r.message}).`)}if(!n.ok)throw new i(`OpenID discovery failed at ${t}: ${n.status} ${n.statusText}`);let o=await n.json();if(!o.authorization_endpoint||!o.token_endpoint)throw new i(`OpenID discovery at ${t} did not return the authorization and token endpoints.`);return{authorizationEndpoint:o.authorization_endpoint,tokenEndpoint:o.token_endpoint}};import{createServer as Kt}from"node:http";var Vt=3e5,Me=(e,t=Vt)=>new Promise((n,o)=>{let r,s=new Promise((p,u)=>{r={resolve:p,reject:u}}),a=Kt((p,u)=>{let h=new URL(p.url??"/","http://127.0.0.1");if(h.pathname!==ne){u.writeHead(404,{"Content-Type":"text/plain",Connection:"close"}).end("Not found");return}Yt(h,u,e,r)}),c=setTimeout(()=>r.reject(new i("Timed out waiting for the browser login to complete.")),t);c.unref();let l=!1,d=()=>{l||(l=!0,clearTimeout(c),a.close())};s.then(d,d),a.once("error",p=>o(new i(`Could not start the loopback listener: ${p.message}`))),a.listen(0,"127.0.0.1",()=>{let{port:p}=a.address();n({redirectUri:`http://127.0.0.1:${p}${ne}`,waitForCode:()=>s,close:d})})}),Yt=(e,t,n,o)=>{if(e.searchParams.get("state")!==n){t.writeHead(404,{"Content-Type":"text/plain",Connection:"close"}).end("Unexpected request");return}let r=e.searchParams.get("error");if(r){let a=e.searchParams.get("error_description");oe(t,"Sign-in failed","Sign-in failed. You can close this window and return to the terminal."),o.reject(new i(`Authorization failed: ${a?`${r} (${a})`:r}`));return}let s=e.searchParams.get("code");if(!s){oe(t,"Sign-in failed","No authorization code was returned. You can close this window."),o.reject(new i("Login failed: no authorization code was returned."));return}oe(t,"Signed in","You are signed in to the ModelDesk CLI. You can close this window and return to the terminal."),o.resolve(s)},oe=(e,t,n)=>{let o=`<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${t}</title></head><body style="font-family: system-ui, sans-serif; text-align: center; padding: 3rem;"><h1>${t}</h1><p>${n}</p></body></html>`;e.writeHead(200,{"Content-Type":"text/html; charset=utf-8",Connection:"close"}).end(o)};import{createHash as Qt,randomBytes as Le}from"node:crypto";var re=e=>e.toString("base64url"),Oe=()=>{let e=re(Le(32)),t=re(Qt("sha256").update(e).digest());return{verifier:e,challenge:t,method:"S256"}},De=()=>re(Le(16));var b=class extends i{oauthError;errorDescription;constructor(t,n,o){super(t),this.name="TokenEndpointError",this.oauthError=n,this.errorDescription=o}},Ne=e=>e instanceof b&&e.oauthError==="invalid_grant",Fe=(e,t)=>F(e,new URLSearchParams({grant_type:"authorization_code",code:t.code,redirect_uri:t.redirectUri,client_id:R,code_verifier:t.codeVerifier})),N=(e,t)=>F(e,new URLSearchParams({grant_type:"refresh_token",refresh_token:t,client_id:R})),He=(e,t)=>F(e,new URLSearchParams({grant_type:"client_credentials",client_id:t.clientId,client_secret:t.clientSecret})),Ue=(e,t)=>F(e,new URLSearchParams({grant_type:"urn:ietf:params:oauth:grant-type:token-exchange",subject_token:t,subject_token_type:"urn:ietf:params:oauth:token-type:jwt",client_id:Ae})),F=async(e,t)=>{let n;try{n=await fetch(e,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Accept:"application/json"},body:t.toString(),signal:AbortSignal.timeout(3e4)})}catch(r){throw new i(`Could not reach the token endpoint (${r.message}).`)}if(!n.ok){let{message:r,code:s,description:a}=await Xt(n);throw new b(`Token request failed: ${r}`,s,a)}let o=await n.json();if(!o.access_token)throw new i("The token response did not include an access token.");return{accessToken:o.access_token,refreshToken:o.refresh_token,expiresInSeconds:o.expires_in??0}},Xt=async e=>{try{if((e.headers.get("content-type")??"").includes("json")){let n=await e.json();if(n.error)return{message:n.error_description?`${n.error} (${n.error_description})`:n.error,code:n.error,description:n.error_description}}}catch{}return{message:`${e.status} ${e.statusText}`.trim()}};var H={info:e=>process.stderr.write(`${e}
5
+ `)},U=async(e,t)=>{let{authorizationEndpoint:n,tokenEndpoint:o}=await w(e.issuer),r=Oe(),s=De(),a=await Me(s);try{let c=Zt(n,a.redirectUri,r.challenge,s);t.info(`Opening your browser to sign in to ${e.host}...`),t.info(`If it does not open automatically, visit:
6
+ ${c}`),Re(c);let l=await a.waitForCode();return await Fe(o,{code:l,redirectUri:a.redirectUri,codeVerifier:r.verifier})}finally{a.close()}},Zt=(e,t,n,o)=>{let r=new URL(e);return r.searchParams.set("response_type","code"),r.searchParams.set("client_id",R),r.searchParams.set("redirect_uri",t),r.searchParams.set("scope",_e),r.searchParams.set("state",o),r.searchParams.set("code_challenge",n),r.searchParams.set("code_challenge_method","S256"),r.toString()};var se="modeldesk.io",m={host:"MODELDESK_HOST",model:"MODELDESK_MODEL",stack:"MODELDESK_STACK",template:"MODELDESK_TEMPLATE",version:"MODELDESK_VERSION",clientId:"MODELDESK_CLIENT_ID",clientSecret:"MODELDESK_CLIENT_SECRET",apiKey:"MODELDESK_API_KEY"},A=(...e)=>e.find(t=>t!==void 0&&t!==""),ie=(e,t,n)=>({host:A(e.host,t[m.host],n.host)??se,model:A(e.model,t[m.model],n.model),stack:A(e.stack,t[m.stack],n.stack),template:A(e.template,t[m.template],n.template),version:A(e.version,t[m.version],n.version)});var je={requestUrl:"ACTIONS_ID_TOKEN_REQUEST_URL",requestToken:"ACTIONS_ID_TOKEN_REQUEST_TOKEN"},k=e=>e!==void 0&&e!=="",Be=e=>e==="true"||e==="1",en=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,ae="mdsk_",ze=57,C=(e,t)=>({mode:"error",reason:e,message:t}),v="org settings \u2192 Service accounts",_=e=>{if(k(e[m.apiKey]))return C("legacy-api-key",`${m.apiKey} is no longer supported: the CI credential shipped as service accounts, not bearer keys. Use secret mode (set ${m.clientId} and ${m.clientSecret} from ${v}) or, on GitHub Actions, federated auth (grant 'permissions: id-token: write' and add a trust rule). Unset ${m.apiKey}.`);let t=e[m.clientId],n=e[m.clientSecret];if(k(t)!==k(n)){let[s,a]=k(t)?[m.clientId,m.clientSecret]:[m.clientSecret,m.clientId];return C("partial-secret-config",`${s} is set but ${a} is not \u2014 both are required for secret-mode auth. Check the pipeline's secret configuration.`)}if(k(t)&&k(n))return en.test(t)?n.startsWith(ae)?n.length!==ze?C("invalid-client-secret",`${m.clientSecret} has the wrong length: expected ${ze} characters, got ${n.length}. Check for truncation in the pipeline's secret store.`):{mode:"secret",clientId:t,clientSecret:n}:C("invalid-client-secret",`${m.clientSecret} does not start with '${ae}' (an issued secret always does). Check for quoting or truncation in the pipeline's secret store.`):C("invalid-client-id",`${m.clientId} is not a service-account id: expected a UUID, got a ${t.length}-character value. Copy the account id from ${v}.`);let o=e[je.requestUrl],r=e[je.requestToken];return k(o)&&k(r)?{mode:"federated",oidcRequestUrl:o,oidcRequestToken:r}:Be(e.GITHUB_ACTIONS)?C("headless-ci",`Running on GitHub Actions without machine credentials. Grant 'permissions: id-token: write' to use federated auth (with a trust rule in ${v}), or set ${m.clientId} and ${m.clientSecret}. The CLI never opens a browser login in CI.`):Be(e.CI)?C("headless-ci",`Running in CI without machine credentials. Set ${m.clientId} and ${m.clientSecret} (${v}). The CLI never opens a browser login in CI.`):{mode:"interactive"}},T=e=>{let t=_(e);return t.mode==="secret"||t.mode==="federated"?t.mode:void 0},Je=e=>`${e.slice(0,ae.length+8)}\u2026`,Ge=`Check ${m.clientId} and ${m.clientSecret}; issue or rotate a secret in ${v}.`,qe=`The service account may be disabled or deleted \u2014 check ${v}.`,We={issuer:"the auth server does not trust this token issuer \u2014 federated auth currently supports GitHub Actions only.",signature:"the OIDC token's signature could not be verified \u2014 re-run the job; if it persists, the issuer's keys and the auth server disagree.",audience:`the workflow's OIDC audience does not match this host \u2014 check that --host/${m.host} names the ModelDesk instance the trust rule lives on.`,expired:"the OIDC token expired before the exchange \u2014 re-run the job.",repository:"no active trust rule matches this repository. This is also the failure for a pull-request-triggered run unless the rule enables 'Allow pull requests', and for a revoked rule.",binding:"the trust rule pins a branch or environment that does not match this run."},j={unauthorized:`The machine token was rejected. A disabled or deleted service account fails on the next request; a revoked secret within five minutes. Check ${v}.`,forbidden:`The service account's scopes do not permit this \u2014 pulling content needs the 'model:content' scope (${v}).`,notFound:"Check the model id. A machine credential reads SHARED models only (creator-private models are not visible), and the model's metamodel must be entitled under the organization's subscription."};var tn="http://localhost:8080",nn="http://localhost:8082",on="localhost",rn=e=>{let t=e.trim(),n=/:\/\//.test(t)?t:`https://${t}`;try{return new URL(n).hostname}catch{return t.replace(/^[a-z][a-z0-9+.-]*:\/\//i,"").split("/")[0].split(":")[0]}},sn=e=>e==="localhost"||e==="127.0.0.1"||e==="::1"||e==="[::1]",ce=e=>{let t=rn(e);return sn(t)?{host:on,apiBase:tn,issuer:nn}:{host:t,apiBase:`https://api.${t}`,issuer:`https://auth.${t}`}};import{existsSync as an,readFileSync as cn}from"node:fs";import{join as dn}from"node:path";var B=".modeldesk",mn="config.json",z=e=>e.configPath!==void 0,de=e=>{let t=dn(e,B,mn);return an(t)?{config:ln(t),configPath:t,repoRoot:e}:{config:{},repoRoot:e}},ln=e=>{let t;try{t=cn(e,"utf8")}catch(r){throw new i(`Could not read ${e}: ${r.message}`)}let n;try{n=JSON.parse(t)}catch(r){throw new i(`${e} is not valid JSON: ${r.message}`)}if(n===null||typeof n!="object"||Array.isArray(n))throw new i(`${e} must contain a JSON object.`);let o=n;return{host:M(o,"host",e),model:M(o,"model",e),stack:M(o,"stack",e),template:M(o,"template",e),version:M(o,"version",e)}},M=(e,t,n)=>{let o=e[t];if(o!==void 0){if(typeof o!="string")throw new i(`${n}: "${t}" must be a string.`);return o}};import{parseArgs as pn}from"node:util";var S=(e,t)=>{try{return pn({args:e,options:t,allowPositionals:!1,strict:!0})}catch(n){throw new i(n.message)}};var Ke=(e,t,n)=>{let o=n[m.host];return!(t!==void 0&&t!==""||o!==void 0&&o!=="")&&z(e)&&!!e.config.host},Ve=(e,t,n)=>{Ke(e,t,n)&&e.config.host!==se&&process.stderr.write(`note: using host '${e.config.host}' from ${e.configPath} (it drives login and API calls).
7
+ `)},Ye=(e,t,n)=>{if(T(n)&&Ke(e,t,n))throw new i(`In a machine-auth run the host must be set explicitly: pass --host or set ${m.host}. The committed ${e.configPath??".modeldesk/config.json"} is not a trusted source for where machine credentials are sent.`)},x=(e,t={})=>{let{values:n}=S(e.argv,{host:{type:"string"}}),o=de(e.cwd),r=ie({host:n.host},e.env,o.config);return t.gateMachineHost&&Ye(o,n.host,e.env),Ve(o,n.host,e.env),ce(r.host)},J=(e,t)=>{let n=de(e.cwd),o=ie(t,e.env,n.config);if(Ye(n,t.host,e.env),Ve(n,t.host,e.env),!o.model)throw new i(`No model specified. Pass --model, set ${m.model}, or add "model" to .modeldesk/config.json.`);return{instance:ce(o.host),model:o.model,stack:o.stack,template:o.template,version:o.version,loaded:n}},me=(e,t,n,o)=>{let r=e?.trim();if(!r)throw new i(`No ${t} specified. Pass ${o}, set ${n}, or add "${t}" to .modeldesk/config.json. Run 'modeldesk bundle templates' to list a model's stacks and templates.`);return r};var Qe=async e=>{let t=T(e.env);t&&process.stderr.write(`warning: machine auth (${t} mode) is active in this environment and takes precedence for API commands; this login only affects interactive use.
8
+ `);let n=x(e),o=await U(n,H),r=new f;o.refreshToken?r.saveRefreshToken(n.host,o.refreshToken):r.clearHost(n.host);let s=D(o.accessToken);return process.stdout.write(`Logged in to ${n.host}${s?` as ${s}`:""}.
8
9
  `),o.refreshToken||process.stderr.write(`warning: the server returned no refresh token, so later commands will re-open the browser each time instead of running unattended.
9
- `),0};var Ie=async e=>{let t=k(e),o=new u().clearHost(t.host);return process.stdout.write(o?`Logged out of ${t.host}.
10
- `:`No stored credential for ${t.host}.
11
- `),0};var Le=async e=>{let t=k(e),o=s=>{process.stdout.write(`${s}
12
- `)};o(`Host: ${t.host}`),o(`API: ${t.apiBase}`);let n=new u,r=n.getRefreshToken(t.host);if(!r)return o("Status: not logged in. Run 'modeldesk auth login'."),1;try{let{tokenEndpoint:s}=await w(t.issuer),a=await A(s,r);n.saveRefreshTokenIfPresent(t.host,a.refreshToken);let c=I(a.accessToken);return o(`Status: logged in${c?` as ${c}`:""} (credential valid).`),0}catch{return o("Status: the stored credential could not be verified (it may be expired or revoked, or the server is unreachable). Run 'modeldesk auth login'."),1}};var Ut=6e4,N=class{constructor(t,o={}){this.instance=t;this.store=o.store??new u,this.io=o.io??_,this.now=o.now??Date.now}instance;store;io;now;accessToken;accessTokenExpiresAt=0;endpoints;async getAccessToken(){if(this.accessToken&&this.now()<this.accessTokenExpiresAt-Ut)return this.accessToken;let t=this.store.getRefreshToken(this.instance.host);if(t){let o=await this.tryRefresh(t);if(o)return o}return this.login()}async tryRefresh(t){let{tokenEndpoint:o}=await this.getEndpoints();try{return this.apply(await A(o,t))}catch(n){if(be(n))return;throw n}}async login(){return this.apply(await M(this.instance,this.io))}apply(t){return this.accessToken=t.accessToken,this.accessTokenExpiresAt=this.now()+t.expiresInSeconds*1e3,this.store.saveRefreshTokenIfPresent(this.instance.host,t.refreshToken),t.accessToken}async getEndpoints(){return this.endpoints||(this.endpoints=await w(this.instance.issuer)),this.endpoints}};var j=(e,t)=>{if(t[g.apiKey])throw new i("CI API-key auth is not yet available. Unset MODELDESK_API_KEY to authenticate with interactive browser login.");return new N(e)};import{existsSync as te,lstatSync as Ae,realpathSync as zt}from"node:fs";import{homedir as Jt}from"node:os";import{basename as Wt,dirname as Kt,isAbsolute as B,join as _e,relative as oe,resolve as C}from"node:path";var U=(e,t,o)=>{if(B(o))throw new i(`Refusing an absolute path from the bundle manifest: ${o}`);let n=C(e,t),r=C(e,o),s=oe(n,r);if(s===""||s.startsWith("..")||B(s))throw new i(`Refusing a bundle path that escapes the content root: ${o}`);return r},Gt=e=>{try{return Ae(e)}catch{return}},H=e=>{let t=[],o=e;for(;!te(o);){let r=Kt(o);if(r===o)return e;t.unshift(Wt(o)),o=r}let n=zt(o);return t.length>0?_e(n,...t):n},ne=e=>{if(Gt(e)?.isSymbolicLink())throw new i(`Refusing to write through an existing symlink at a managed path: ${e}`)},z=(e,t)=>{let o=H(e),n=H(t),r=oe(o,n);if(r!==""&&(r.startsWith("..")||B(r)))throw new i(`Refusing a path that escapes the content root through a symlink: ${t}`)},re=(e,t)=>{let o=C(e,t);if(o===C(e))throw new i(`The content root resolves to the repository root (${o}); refusing to sync it.`);if(o===C(Jt()))throw new i("The content root resolves to your home directory; refusing to sync it.");if(te(o)){if(Ae(o).isSymbolicLink())throw new i(`The content root ${o} is a symlink; refusing to sync it.`);if(te(_e(o,".git")))throw new i(`The content root ${o} contains a .git directory (it looks like a repo root); refusing.`)}let n=H(C(e)),r=oe(n,H(o));if(r!==""&&(r.startsWith("..")||B(r)))throw new i(`The content root ${o} resolves outside the repository via a symlinked ancestor; refusing.`);return o};var qt={respec:"data/model"},Me=e=>{let t=qt[e];if(!t)throw new i(`The CLI does not know the content root for stack '${e}'.`);return t};var Vt=1500,Yt=6e5,Qt=e=>new Promise(t=>setTimeout(t,e)),Xt=e=>e==="COMPLETED"||e==="COMPLETED_WITH_ERRORS"||e==="FAILED",Oe=async(e,t,o)=>{let n=await e.postForJson(`/models/${t}/content-bundle/diagrams`,{template:o.template,version:o.version});if(!n.commit)throw new i("The diagram render did not return a resolved commit; cannot pin the pull.");return n},De=async(e,t,o,n,r,s={})=>{let a=s.sleep??Qt,c=s.now??Date.now,m=c()+Yt;for(n.update(0,r);;){let d=await e.getJson(`/models/${t}/export-jobs/${o}`);if(d.progress&&n.update(d.progress.completed,d.progress.total,d.progress.currentItem),Xt(d.status))return n.done(),d;if(c()>=m)throw new i("Timed out waiting for the diagram render to finish.");await a(Vt)}},Fe=(e,t,o,n)=>e.getBytes(`/models/${t}/content-bundle/media`,{path:o,template:n.template,commit:n.commit});var Ne=e=>{let t=new Map;for(let o of e.fragments??[])o.realization?.path&&t.set(o.elementId,o.realization.path);return t},je=(e,t)=>{let o=Ne(e),n=[];for(let[r,s]of Ne(t)){let a=o.get(r);a&&a!==s&&n.push({elementId:r,oldPath:a,newPath:s})}return n};var se=e=>{let t=e.payloads??{},o=[];for(let n of e.manifest.fragments??[]){let r=n.realization?.path;if(!r)continue;let s=t[n.address];s!==void 0&&o.push({path:r,content:s})}return o},ie=e=>{let t=[];for(let o of e.fragments??[])for(let n of o.realization?.media??[])t.push(n);return[...new Set(t)]};var Be=e=>`${JSON.stringify(e,null,2)}
13
- `;import{existsSync as Zt,mkdirSync as eo,readFileSync as to,writeFileSync as oo}from"node:fs";import{join as He}from"node:path";var Ue=(e,t)=>He(e,O,`${t}.manifest.json`),ze=(e,t)=>{let o=Ue(e,t);if(!Zt(o))return;let n;try{n=JSON.parse(to(o,"utf8"))}catch{return}return no(n)?n:void 0},no=e=>{if(e===null||typeof e!="object"||Array.isArray(e))return!1;let{fragments:t}=e;return t===void 0?!0:Array.isArray(t)&&t.every(o=>o!==null&&typeof o=="object")},Je=(e,t,o)=>{eo(He(e,O),{recursive:!0}),oo(Ue(e,t),Be(o))},We=e=>{let t=[];for(let o of e.fragments??[]){o.realization?.path&&t.push(o.realization.path);for(let n of o.realization?.media??[])t.push(n)}return t};import{existsSync as Ke,mkdirSync as Ge,readdirSync as ro,rmSync as so,writeFileSync as qe}from"node:fs";import{dirname as Ve,join as S,relative as io,sep as ao}from"node:path";var Ye=(e,t)=>{let o=S(e,t);if(!Ke(o))return[];let n=[],r=s=>{for(let a of ro(s,{withFileTypes:!0})){let c=S(s,a.name);a.isDirectory()?r(c):a.isFile()&&n.push(io(e,c).split(ao).join("/"))}};return r(o),n},Qe=(e,t,o)=>{let n=S(e,t);for(let r of o){let s=U(e,t,r.path);ne(s),z(n,s),Ge(Ve(s),{recursive:!0}),qe(s,r.content)}},Xe=(e,t,o,n)=>{let r=S(e,t),s=U(e,t,o);ne(s),z(r,s),Ge(Ve(s),{recursive:!0}),qe(s,n)},Ze=(e,t,o)=>{let n=S(e,t);for(let r of o){let s=U(e,t,r);z(n,s),Ke(s)&&so(s)}};var co=100,tt=e=>{if(e.newTextPaths.length===0&&!e.allowEmpty)throw new i("The content fetch returned no fragments (usually the wrong model or format, or a server error). Nothing was written or deleted. Re-run with --allow-empty only if the model is genuinely empty.");let t=et([...e.newTextPaths,...e.newMediaPaths]).sort(),o=new Set(t),n=new Set(e.existingFiles),r=t.filter(l=>n.has(l)),s=e.priorManifestPaths!==void 0?"manifest-tracked":"full-sync",a=e.priorManifestPaths??e.existingFiles,c=et(a).filter(l=>!o.has(l)).sort(),m=c.length>0&&!e.adopted,d=c.length>0&&(c.length>t.length||c.length>co);return{writes:t,overwrites:r,orphanCandidates:c,mode:s,adoptionNeeded:m,outsized:d}},ae=(e,t)=>{if(e.orphanCandidates.length===0)return{deletes:[],warnings:[]};if(e.adoptionNeeded&&!t.adopt)return{deletes:[],warnings:[`${e.orphanCandidates.length} file(s) under the content root are not tracked by the CLI and were left in place. Pass --adopt to let the CLI manage (and prune) this directory.`]};if(e.outsized&&!t.yes)throw new i(`This pull would remove ${e.orphanCandidates.length} file(s), more than the ${e.writes.length} it writes. That often signals a wrong model or format. Nothing was changed. Re-run with --yes if intended.`);return{deletes:e.orphanCandidates,warnings:[]}},et=e=>[...new Set(e)];var mo=6e4,x=class{constructor(t,o){this.apiBase=t;this.tokens=o}apiBase;tokens;async getJson(t,o){return await(await this.request(t,{method:"GET"},o)).json()}async postForJson(t,o){return await(await this.request(t,{method:"POST"},o)).json()}async getBytes(t,o){let n=await this.request(t,{method:"GET",headers:{Accept:"image/png"}},o);return new Uint8Array(await n.arrayBuffer())}async request(t,o,n){let r=await this.tokens.getAccessToken(),s=lo(this.apiBase,t,n),a;try{a=await fetch(s,{...o,headers:{Authorization:`Bearer ${r}`,Accept:"application/json",...o.headers??{}},signal:AbortSignal.timeout(mo)})}catch(c){throw new i(`Could not reach ${this.apiBase} (${c.message}). Check the host and that the ModelDesk server is reachable.`)}if(!a.ok)throw await po(a,t);return a}},lo=(e,t,o)=>{let n=new URL(t,e);if(o)for(let[r,s]of Object.entries(o))s!==void 0&&n.searchParams.set(r,s);return n.toString()},po=async(e,t)=>{let o=`${e.status} ${e.statusText}`.trim();try{if((e.headers.get("content-type")??"").includes("json")){let r=await e.json(),s=r.detail??r.title;s&&(o=`${e.status} ${s}`)}}catch{}return e.status===401?new i(`Authentication failed (${o}). Run 'modeldesk auth login' and try again.`):e.status===403?new i(`Not authorized (${o}). Your account may lack access to this model.`):new i(`Request to ${t} failed: ${o}`)};var fo=(e,t)=>{let o=t>0?Math.min(1,e/t):0,n=Math.round(o*24);return`[${"#".repeat(n)}${"-".repeat(24-n)}]`},ot=(e,t=o=>{process.stderr.write(o)})=>{let o=-1;return{update(n,r,s){if(e){let a=s?` ${s}`:"";t(`\r${fo(n,r)} ${n}/${r}${a}\x1B[K`)}else if(n!==o){let a=s?` (${s})`:"";t(`Rendering diagrams: ${n}/${r}${a}
14
- `),o=n}},done(){e&&t(`
15
- `)}}};import{createInterface as go}from"node:readline";var J=()=>!!(process.stdin.isTTY&&process.stdout.isTTY),ce=async e=>{if(!J())return!1;let t=go({input:process.stdin,output:process.stderr});try{let o=await new Promise(n=>t.question(`${e} [y/N] `,n));return/^y(es)?$/i.test(o.trim())}finally{t.close()}};var h=e=>{process.stdout.write(e)},y=e=>{process.stderr.write(e)},nt=e=>{for(let t of e)y(` ${t}
16
- `)},st=async e=>{let{values:t}=v(e.argv,{host:{type:"string"},model:{type:"string"},stack:{type:"string"},template:{type:"string"},version:{type:"string"},"no-diagrams":{type:"boolean"},"dry-run":{type:"boolean"},"allow-empty":{type:"boolean"},adopt:{type:"boolean"},yes:{type:"boolean"}}),o=F(e,{host:t.host,model:t.model,stack:t.stack,template:t.template,version:t.version}),n={noDiagrams:!!t["no-diagrams"],dryRun:!!t["dry-run"],allowEmpty:!!t["allow-empty"],adopt:!!t.adopt,yes:!!t.yes},r=new x(o.instance.apiBase,j(o.instance,e.env)),s=ee(o.stack,"stack",g.stack,"--stack"),a=ee(o.template,"template",g.template,"--template"),m=!n.noDiagrams&&!n.dryRun?await Oe(r,o.model,{template:a,version:o.version}):void 0,d=m?.commit,l=await r.getJson(`/models/${o.model}/content-bundle`,{template:a,commit:d,version:d?void 0:o.version});if(l.manifest.stack!==s)throw new i(`Requested stack '${s}', but template '${a}' renders under stack '${l.manifest.stack}'. Run 'modeldesk bundle templates' to see valid stack/template pairs.`);let f=Me(s);re(o.loaded.repoRoot,f);let p=se(l),b=ie(l.manifest),T=ze(o.loaded.repoRoot,s),K=tt({newTextPaths:p.map(ft=>ft.path),newMediaPaths:b,priorManifestPaths:T?We(T):void 0,existingFiles:Ye(o.loaded.repoRoot,f),adopted:T!==void 0||D(o.loaded),allowEmpty:n.allowEmpty}),me=T?je(T,l.manifest):[],le={stack:s,version:l.manifest.stackVersion};if(n.dryRun)return rt(o,a,le,d,K,void 0,me,b.length,!0),0;let pe=await uo(K,n);return rt(o,a,le,d,K,pe,me,b.length,!1),re(o.loaded.repoRoot,f),Qe(o.loaded.repoRoot,f,p),Ze(o.loaded.repoRoot,f,pe.deletes),Je(o.loaded.repoRoot,s,l.manifest),b.length===0||!m?0:ho(r,o,a,d,m,b,f)},uo=async(e,t)=>{let{adopt:o,yes:n}=t;if(e.orphanCandidates.length===0)return ae(e,{adopt:o,yes:n});let r=J();return e.adoptionNeeded&&!o&&(y(`The content root has ${e.orphanCandidates.length} file(s) the CLI does not track:
17
- `),nt(e.orphanCandidates),r&&(o=await ce("Adopt this directory and allow the CLI to remove these files?"))),e.outsized&&!n&&(o||!e.adoptionNeeded)&&(y(`This pull would remove ${e.orphanCandidates.length} file(s), more than the ${e.writes.length} it writes:
18
- `),nt(e.orphanCandidates),r&&(n=await ce("Proceed with removing them?"))),ae(e,{adopt:o,yes:n})},ho=async(e,t,o,n,r,s,a)=>{let c=ot(J()),m=await De(e,t.model,r.id,c,s.length);if(m.status==="FAILED")return y(`warning: the diagram render failed. Text fragments are written; media was not fetched. Re-run to retry.
19
- `),1;let d=0;for(let l of s)try{let f=await Fe(e,t.model,l,{template:o,commit:n});Xe(t.loaded.repoRoot,a,l,f)}catch(f){d+=1,y(`warning: could not fetch media ${l}: ${f instanceof Error?f.message:String(f)}
20
- `)}return m.status==="COMPLETED_WITH_ERRORS"||d>0?(y(`warning: ${d} media file(s) could not be written; the pull is re-runnable.
21
- `),1):0},rt=(e,t,o,n,r,s,a,c,m)=>{let d=n?`commit ${n}`:e.version?`version ${e.version}`:"HEAD",l=`${o.stack} ${o.version}`;h(`${m?"Dry run: ":""}Pulling ${e.model} (stack ${l}, template ${t}) at ${d} into ${e.instance.host}.
22
- `),h(` Write: ${r.writes.length} file(s)${r.overwrites.length?` (${r.overwrites.length} overwritten)`:""}
23
- `),h(` Diagrams: ${c}
24
- `);let f=m?r.orphanCandidates:s?.deletes??[];h(` Remove: ${f.length} file(s) [${r.mode}]
25
- `);for(let p of f)h(` - ${p}
26
- `);if(m)r.adoptionNeeded&&h(` note: removing these needs --adopt (the root is not CLI-managed yet).
27
- `),r.outsized&&h(` note: this removal is larger than the write set and needs --yes.
28
- `);else for(let p of s?.warnings??[])y(`warning: ${p}
29
- `);for(let p of a)h(` moved: ${p.oldPath} -> ${p.newPath}
30
- `);if(m)for(let p of r.writes)h(` write ${p}
31
- `)};var W=e=>{let t=[];for(let o of e)o.forEach((n,r)=>{t[r]=Math.max(t[r]??0,n.length)});return t},R=(e,{indent:t="",gap:o=2,widths:n}={})=>{let s=W(e).map((c,m)=>n?.[m]??c),a=" ".repeat(o);return e.map(c=>t+c.map((m,d)=>d===c.length-1?m:m.padEnd(s[d])).join(a))};var yo=["STACK","TEMPLATE","NAME"],it=async e=>{let{values:t}=v(e.argv,{host:{type:"string"},model:{type:"string"},stack:{type:"string"}}),o=F(e,{host:t.host,model:t.model}),n=new x(o.instance.apiBase,j(o.instance,e.env)),r=await n.getJson(`/models/${o.model}`),s=(r.metamodelExtensions??[]).map(p=>p.id),a=s.length>0?{metamodelExtensions:s.join(",")}:void 0,{items:c}=await n.getJson(`/metamodels/${r.metamodel.id}/templates`,a),m=c??[];if(m.length===0)return process.stdout.write(`This model has no document templates.
32
- `),0;let d=t.stack,l=d?m.filter(p=>p.stack===d):m;if(l.length===0)return process.stdout.write(`No document templates for stack '${d}'.
33
- `),0;let f=l.map(p=>[p.stack??"",p.key??p.id,p.name]);for(let p of R([yo,...f]))process.stdout.write(`${p}
34
- `);return 0};var de={bundle:{pull:{summary:"Materialize a model's content bundle into a local folder",usage:"bundle pull --model <id> --stack <id> --template <slug> [--host <host>] [--version <name>] [--no-diagrams] [--dry-run]",handler:st},templates:{summary:"List a model's document templates",usage:"bundle templates --model <id> [--stack <id>] [--host <host>]",handler:it}},auth:{login:{summary:"Log in through the browser (OAuth authorization code + PKCE)",usage:"auth login [--host <host>]",handler:Re},logout:{summary:"Remove the stored credential for the active host",usage:"auth logout [--host <host>]",handler:Ie},status:{summary:"Show the active host, logged-in identity, and token validity",usage:"auth status [--host <host>]",handler:Le}}};var at={name:"@modeldesk/cli",version:"0.1.0",description:"Command-line interface (CLI) for the ModelDesk platform",keywords:["modeldesk","cli","documentation","content-bundle","respec","mim","information-model"],homepage:"https://modeldesk.io",repository:{type:"git",url:"git+https://github.com/model-desk/modeldesk.git",directory:"packages/cli"},license:"MIT",type:"module",bin:{modeldesk:"dist/modeldesk.js"},files:["dist"],publishConfig:{access:"public"},engines:{node:">=20"},scripts:{build:"node esbuild.config.mjs",prepublishOnly:"npm run build",typecheck:"tsc --noEmit",lint:"eslint","lint:fix":"eslint --fix",test:"vitest run","test:watch":"vitest"},devDependencies:{"@eslint/js":"^10.0.1","@modeldesk/content-bundle":"*","@types/node":"^24.13.2",esbuild:"^0.28.1",eslint:"^10.7.0","eslint-config-prettier":"^10.1.5","eslint-plugin-prettier":"^5.5.6",globals:"^17.7.0",prettier:"^3.9.5",typescript:"^6.0.3","typescript-eslint":"^8.64.0",vitest:"^4.1.9"}};var ct=at.version;var vo={out:e=>process.stdout.write(e),err:e=>process.stderr.write(e)},mt=new Set(["--help","-h","help"]),pt=async(e,t=vo)=>{try{return await ko(e,t)}catch(o){return o instanceof i?(t.err(`error: ${o.message}
35
- `),o.exitCode):(t.err(`error: ${o instanceof Error?o.message:String(o)}
36
- `),1)}},ko=async(e,t)=>{let[o,...n]=e;if(o===void 0||mt.has(o))return t.out(Co()),0;if(o==="--version"||o==="-v")return t.out(`${ct}
37
- `),0;let r=de[o];if(!r)throw new i(`Unknown command '${o}'. Run 'modeldesk --help' to see available commands.`);let[s,...a]=n;if(s===void 0){let l=Object.keys(r).join(", ");throw new i(`'${o}' requires a subcommand (${l}). Run 'modeldesk ${o} --help'.`)}if(mt.has(s))return t.out(xo(o,r)),0;let c=r[s];if(!c)throw new i(`Unknown '${o}' command '${s}'. Run 'modeldesk ${o} --help'.`);let m={argv:a,env:process.env,cwd:process.cwd()},d=await c.handler(m);return typeof d=="number"?d:0},lt=[["--help, -h","Show help"],["--version, -v","Show the CLI version"]],Co=()=>{let e=[];for(let[n,r]of Object.entries(de))for(let[s,a]of Object.entries(r))e.push([`${n} ${s}`,a.summary]);let[t]=W([...e,...lt]);return`${["ModelDesk CLI","","Usage: modeldesk <command> <subcommand> [options]","","Commands:",...R(e,{indent:" ",widths:[t]}),"","Global options:",...R(lt,{indent:" ",widths:[t]}),""].join(`
10
+ `),0};var Xe=async e=>{let t=T(e.env);t&&process.stderr.write(`warning: machine auth (${t} mode) is active in this environment and takes precedence for API commands; this logout does not remove machine credentials.
11
+ `);let n=x(e),o=new f().clearHost(n.host);return process.stdout.write(o?`Logged out of ${n.host}.
12
+ `:`No stored credential for ${n.host}.
13
+ `),0};var le=6e4,G=class{constructor(t,n={}){this.instance=t;this.store=n.store??new f,this.io=n.io??H,this.now=n.now??Date.now}instance;store;io;now;accessToken;accessTokenExpiresAt=0;endpoints;async getAccessToken(){if(this.accessToken&&this.now()<this.accessTokenExpiresAt-le)return this.accessToken;let t=this.store.getRefreshToken(this.instance.host);if(t){let n=await this.tryRefresh(t);if(n)return n}return this.login()}async tryRefresh(t){let{tokenEndpoint:n}=await this.getEndpoints();try{return this.apply(await N(n,t))}catch(o){if(Ne(o))return;throw o}}async login(){return this.apply(await U(this.instance,this.io))}apply(t){return this.accessToken=t.accessToken,this.accessTokenExpiresAt=this.now()+t.expiresInSeconds*1e3,this.store.saveRefreshTokenIfPresent(this.instance.host,t.refreshToken),t.accessToken}async getEndpoints(){return this.endpoints||(this.endpoints=await w(this.instance.issuer)),this.endpoints}};var pe=class{constructor(t,n,o,r={}){this.instance=t;this.mode=n;this.mint=o;this.now=r.now??Date.now,this.discover=r.discover??w}instance;mode;mint;now;discover;accessToken;accessTokenExpiresAt=0;endpoints;inflight;async getAccessToken(){return this.accessToken&&this.now()<this.accessTokenExpiresAt-le?this.accessToken:(this.inflight||(this.inflight=this.mintFresh().finally(()=>{this.inflight=void 0})),this.inflight)}async mintFresh(){let{tokenEndpoint:t}=await this.getEndpoints(),n;try{n=await this.mint(t)}catch(o){throw un(o,this.mode)}if(n.expiresInSeconds<=0)throw new i("The token response did not include a token lifetime (expires_in), so the machine token cannot be cached. This indicates a server problem.");return this.accessToken=n.accessToken,this.accessTokenExpiresAt=this.now()+n.expiresInSeconds*1e3,n.accessToken}async getEndpoints(){return this.endpoints||(this.endpoints=await this.discover(this.instance.issuer)),this.endpoints}},hn=async(e,t,n)=>{let o;try{o=await fetch(`${e}&audience=${encodeURIComponent(n)}`,{headers:{Authorization:`bearer ${t}`,Accept:"application/json"},signal:AbortSignal.timeout(3e4)})}catch(s){throw new i(`Could not reach the GitHub OIDC token endpoint (${s.message}).`)}if(!o.ok)throw new i(`The GitHub OIDC token request failed (${o.status} ${o.statusText}). Check that the workflow grants 'permissions: id-token: write'.`);let r=await o.json().catch(()=>{});if(!r?.value)throw new i("The GitHub OIDC token response did not include a token value. Check that the workflow grants 'permissions: id-token: write'.");return r.value},q=(e,t,n={})=>{let o=t.mode==="secret"?r=>He(r,{clientId:t.clientId,clientSecret:t.clientSecret}):async r=>Ue(r,await hn(t.oidcRequestUrl,t.oidcRequestToken,e.issuer));return new pe(e,t.mode,o,n)},un=(e,t)=>{if(!(e instanceof b)||!e.oauthError)return e;if(t==="secret")return e.oauthError==="invalid_client"?new i(`${e.message} ${Ge}`):e;if(e.errorDescription===void 0)return e.oauthError==="invalid_grant"?new i(`${e.message} ${qe}`):e;let n=We[e.errorDescription];return n?new i(`${e.message} \u2014 ${n}`):e};var g=e=>{process.stdout.write(`${e}
14
+ `)},Ze=async e=>{let t=_(e.env);if(t.mode==="error")throw new i(t.message);let n=t.mode==="secret"||t.mode==="federated",o=x(e,{gateMachineHost:n});return g(`Host: ${o.host}`),g(`API: ${o.apiBase}`),t.mode==="secret"||t.mode==="federated"?fn(o,t,e.env):gn(o)},fn=async(e,t,n)=>{t.mode==="secret"?g(`Auth: service account (secret mode), client ${t.clientId}, secret ${Je(t.clientSecret)}`):g(`Auth: federated (GitHub Actions), repository ${n.GITHUB_REPOSITORY??"unknown"}, ref ${n.GITHUB_REF??"unknown"}`);let o=await q(e,t).getAccessToken(),r=te(o)??{};g(`Status: credential valid. Account ${r.sub??"unknown"}, organization ${r.mdsk_org??"unknown"}.`);let s=typeof r.scope=="string"?r.scope.trim():"";return g(s?`Scopes: ${s}`:"Scopes: enforced server-side from the account's current scopes."),0},gn=async e=>{let t=new f,n=t.getRefreshToken(e.host);if(!n)return g("Status: not logged in. Run 'modeldesk auth login'."),1;try{let{tokenEndpoint:o}=await w(e.issuer),r=await N(o,n);t.saveRefreshTokenIfPresent(e.host,r.refreshToken);let s=D(r.accessToken);return g(`Status: logged in${s?` as ${s}`:""} (credential valid).`),0}catch{return g("Status: the stored credential could not be verified (it may be expired or revoked, or the server is unreachable). Run 'modeldesk auth login'."),1}};import{existsSync as he,lstatSync as et,realpathSync as yn}from"node:fs";import{homedir as wn}from"node:os";import{basename as kn,dirname as vn,isAbsolute as W,join as tt,relative as ue,resolve as $}from"node:path";var V=(e,t,n)=>{if(W(n))throw new i(`Refusing an absolute path from the bundle manifest: ${n}`);let o=$(e,t),r=$(e,n),s=ue(o,r);if(s===""||s.startsWith("..")||W(s))throw new i(`Refusing a bundle path that escapes the content root: ${n}`);return r},Tn=e=>{try{return et(e)}catch{return}},K=e=>{let t=[],n=e;for(;!he(n);){let r=vn(n);if(r===n)return e;t.unshift(kn(n)),n=r}let o=yn(n);return t.length>0?tt(o,...t):o},fe=e=>{if(Tn(e)?.isSymbolicLink())throw new i(`Refusing to write through an existing symlink at a managed path: ${e}`)},Y=(e,t)=>{let n=K(e),o=K(t),r=ue(n,o);if(r!==""&&(r.startsWith("..")||W(r)))throw new i(`Refusing a path that escapes the content root through a symlink: ${t}`)},ge=(e,t)=>{let n=$(e,t);if(n===$(e))throw new i(`The content root resolves to the repository root (${n}); refusing to sync it.`);if(n===$(wn()))throw new i("The content root resolves to your home directory; refusing to sync it.");if(he(n)){if(et(n).isSymbolicLink())throw new i(`The content root ${n} is a symlink; refusing to sync it.`);if(he(tt(n,".git")))throw new i(`The content root ${n} contains a .git directory (it looks like a repo root); refusing.`)}let o=K($(e)),r=ue(o,K(n));if(r!==""&&(r.startsWith("..")||W(r)))throw new i(`The content root ${n} resolves outside the repository via a symlinked ancestor; refusing.`);return n};var Cn={respec:"data/model"},nt=e=>{let t=Cn[e];if(!t)throw new i(`The CLI does not know the content root for stack '${e}'.`);return t};var En=1500,bn=6e5,Sn=e=>new Promise(t=>setTimeout(t,e)),xn=e=>e==="COMPLETED"||e==="COMPLETED_WITH_ERRORS"||e==="FAILED",ot=async(e,t,n)=>{let o=await e.postForJson(`/models/${t}/content-bundle/diagrams`,{template:n.template,version:n.version});if(!o.commit)throw new i("The diagram render did not return a resolved commit; cannot pin the pull.");return o},rt=async(e,t,n,o,r,s={})=>{let a=s.sleep??Sn,c=s.now??Date.now,l=c()+bn;for(o.update(0,r);;){let d=await e.getJson(`/models/${t}/export-jobs/${n}`);if(d.progress&&o.update(d.progress.completed,d.progress.total,d.progress.currentItem),xn(d.status))return o.done(),d;if(c()>=l)throw new i("Timed out waiting for the diagram render to finish.");await a(En)}},st=(e,t,n,o)=>e.getBytes(`/models/${t}/content-bundle/media`,{path:n,template:o.template,commit:o.commit});var it=e=>{let t=new Map;for(let n of e.fragments??[])n.realization?.path&&t.set(n.elementId,n.realization.path);return t},at=(e,t)=>{let n=it(e),o=[];for(let[r,s]of it(t)){let a=n.get(r);a&&a!==s&&o.push({elementId:r,oldPath:a,newPath:s})}return o};var ye=e=>{let t=e.payloads??{},n=[];for(let o of e.manifest.fragments??[]){let r=o.realization?.path;if(!r)continue;let s=t[o.address];s!==void 0&&n.push({path:r,content:s})}return n},we=e=>{let t=[];for(let n of e.fragments??[])for(let o of n.realization?.media??[])t.push(o);return[...new Set(t)]};var ct=e=>`${JSON.stringify(e,null,2)}
15
+ `;import{existsSync as $n,mkdirSync as In,readFileSync as Pn,writeFileSync as Rn}from"node:fs";import{join as dt}from"node:path";var mt=(e,t)=>dt(e,B,`${t}.manifest.json`),lt=(e,t)=>{let n=mt(e,t);if(!$n(n))return;let o;try{o=JSON.parse(Pn(n,"utf8"))}catch{return}return An(o)?o:void 0},An=e=>{if(e===null||typeof e!="object"||Array.isArray(e))return!1;let{fragments:t}=e;return t===void 0?!0:Array.isArray(t)&&t.every(n=>n!==null&&typeof n=="object")},pt=(e,t,n)=>{In(dt(e,B),{recursive:!0}),Rn(mt(e,t),ct(n))},ht=e=>{let t=[];for(let n of e.fragments??[]){n.realization?.path&&t.push(n.realization.path);for(let o of n.realization?.media??[])t.push(o)}return t};import{existsSync as ut,mkdirSync as ft,readdirSync as _n,rmSync as Mn,writeFileSync as gt}from"node:fs";import{dirname as yt,join as L,relative as Ln,sep as On}from"node:path";var wt=(e,t)=>{let n=L(e,t);if(!ut(n))return[];let o=[],r=s=>{for(let a of _n(s,{withFileTypes:!0})){let c=L(s,a.name);a.isDirectory()?r(c):a.isFile()&&o.push(Ln(e,c).split(On).join("/"))}};return r(n),o},kt=(e,t,n)=>{let o=L(e,t);for(let r of n){let s=V(e,t,r.path);fe(s),Y(o,s),ft(yt(s),{recursive:!0}),gt(s,r.content)}},vt=(e,t,n,o)=>{let r=L(e,t),s=V(e,t,n);fe(s),Y(r,s),ft(yt(s),{recursive:!0}),gt(s,o)},Tt=(e,t,n)=>{let o=L(e,t);for(let r of n){let s=V(e,t,r);Y(o,s),ut(s)&&Mn(s)}};var Dn=100,Et=e=>{if(e.newTextPaths.length===0&&!e.allowEmpty)throw new i("The content fetch returned no fragments (usually the wrong model or format, or a server error). Nothing was written or deleted. Re-run with --allow-empty only if the model is genuinely empty.");let t=Ct([...e.newTextPaths,...e.newMediaPaths]).sort(),n=new Set(t),o=new Set(e.existingFiles),r=t.filter(p=>o.has(p)),s=e.priorManifestPaths!==void 0?"manifest-tracked":"full-sync",a=e.priorManifestPaths??e.existingFiles,c=Ct(a).filter(p=>!n.has(p)).sort(),l=c.length>0&&!e.adopted,d=c.length>0&&(c.length>t.length||c.length>Dn);return{writes:t,overwrites:r,orphanCandidates:c,mode:s,adoptionNeeded:l,outsized:d}},ke=(e,t)=>{if(e.orphanCandidates.length===0)return{deletes:[],warnings:[]};if(e.adoptionNeeded&&!t.adopt)return{deletes:[],warnings:[`${e.orphanCandidates.length} file(s) under the content root are not tracked by the CLI and were left in place. Pass --adopt to let the CLI manage (and prune) this directory.`]};if(e.outsized&&!t.yes)throw new i(`This pull would remove ${e.orphanCandidates.length} file(s), more than the ${e.writes.length} it writes. That often signals a wrong model or format. Nothing was changed. Re-run with --yes if intended.`);return{deletes:e.orphanCandidates,warnings:[]}},Ct=e=>[...new Set(e)];var bt=(e,t)=>{let n=_(t);switch(n.mode){case"error":throw new i(n.message);case"secret":case"federated":return q(e,n);case"interactive":return new G(e)}};var Nn=6e4,St={unauthorized:"Run 'modeldesk auth login' and try again.",forbidden:"Your account may lack access to this model."},Fn={unauthorized:j.unauthorized,forbidden:j.forbidden,notFound:j.notFound},Q=(e,t)=>new ve(e.apiBase,bt(e,t),T(t)?Fn:St),ve=class{constructor(t,n,o=St){this.apiBase=t;this.tokens=n;this.hints=o}apiBase;tokens;hints;async getJson(t,n){return await(await this.request(t,{method:"GET"},n)).json()}async postForJson(t,n){return await(await this.request(t,{method:"POST"},n)).json()}async getBytes(t,n){let o=await this.request(t,{method:"GET",headers:{Accept:"image/png"}},n);return new Uint8Array(await o.arrayBuffer())}async request(t,n,o){let r=await this.tokens.getAccessToken(),s=Hn(this.apiBase,t,o),a;try{a=await fetch(s,{...n,headers:{Authorization:`Bearer ${r}`,Accept:"application/json",...n.headers??{}},signal:AbortSignal.timeout(Nn)})}catch(c){throw new i(`Could not reach ${this.apiBase} (${c.message}). Check the host and that the ModelDesk server is reachable.`)}if(!a.ok)throw await Un(a,t,this.hints);return a}},Hn=(e,t,n)=>{let o=new URL(t,e);if(n)for(let[r,s]of Object.entries(n))s!==void 0&&o.searchParams.set(r,s);return o.toString()},Un=async(e,t,n)=>{let o=`${e.status} ${e.statusText}`.trim();try{if((e.headers.get("content-type")??"").includes("json")){let s=await e.json(),a=s.detail??s.title;a&&(o=`${e.status} ${a}`)}}catch{}return e.status===401?new i(`Authentication failed (${o}). ${n.unauthorized}`):e.status===403?new i(`Not authorized (${o}). ${n.forbidden}`):e.status===404&&n.notFound?new i(`Not found (${o}). ${n.notFound}`):new i(`Request to ${t} failed: ${o}`)};var jn=(e,t)=>{let n=t>0?Math.min(1,e/t):0,o=Math.round(n*24);return`[${"#".repeat(o)}${"-".repeat(24-o)}]`},xt=(e,t=n=>{process.stderr.write(n)})=>{let n=-1;return{update(o,r,s){if(e){let a=s?` ${s}`:"";t(`\r${jn(o,r)} ${o}/${r}${a}\x1B[K`)}else if(o!==n){let a=s?` (${s})`:"";t(`Rendering diagrams: ${o}/${r}${a}
16
+ `),n=o}},done(){e&&t(`
17
+ `)}}};import{createInterface as Bn}from"node:readline";var X=()=>!!(process.stdin.isTTY&&process.stdout.isTTY),Te=async e=>{if(!X())return!1;let t=Bn({input:process.stdin,output:process.stderr});try{let n=await new Promise(o=>t.question(`${e} [y/N] `,o));return/^y(es)?$/i.test(n.trim())}finally{t.close()}};var y=e=>{process.stdout.write(e)},E=e=>{process.stderr.write(e)},$t=e=>{for(let t of e)E(` ${t}
18
+ `)},Pt=async e=>{let{values:t}=S(e.argv,{host:{type:"string"},model:{type:"string"},stack:{type:"string"},template:{type:"string"},version:{type:"string"},"no-diagrams":{type:"boolean"},"dry-run":{type:"boolean"},"allow-empty":{type:"boolean"},adopt:{type:"boolean"},yes:{type:"boolean"}}),n=J(e,{host:t.host,model:t.model,stack:t.stack,template:t.template,version:t.version}),o={noDiagrams:!!t["no-diagrams"],dryRun:!!t["dry-run"],allowEmpty:!!t["allow-empty"],adopt:!!t.adopt,yes:!!t.yes},r=Q(n.instance,e.env),s=me(n.stack,"stack",m.stack,"--stack"),a=me(n.template,"template",m.template,"--template"),l=!o.noDiagrams&&!o.dryRun?await ot(r,n.model,{template:a,version:n.version}):void 0,d=l?.commit,p=await r.getJson(`/models/${n.model}/content-bundle`,{template:a,commit:d,version:d?void 0:n.version});if(p.manifest.stack!==s)throw new i(`Requested stack '${s}', but template '${a}' renders under stack '${p.manifest.stack}'. Run 'modeldesk bundle templates' to see valid stack/template pairs.`);let u=nt(s);ge(n.loaded.repoRoot,u);let h=ye(p),I=we(p.manifest),P=lt(n.loaded.repoRoot,s),ee=Et({newTextPaths:h.map(Nt=>Nt.path),newMediaPaths:I,priorManifestPaths:P?ht(P):void 0,existingFiles:wt(n.loaded.repoRoot,u),adopted:P!==void 0||z(n.loaded),allowEmpty:o.allowEmpty}),Ee=P?at(P,p.manifest):[],be={stack:s,version:p.manifest.stackVersion};if(o.dryRun)return It(n,a,be,d,ee,void 0,Ee,I.length,!0),0;let Se=await zn(ee,o);return It(n,a,be,d,ee,Se,Ee,I.length,!1),ge(n.loaded.repoRoot,u),kt(n.loaded.repoRoot,u,h),Tt(n.loaded.repoRoot,u,Se.deletes),pt(n.loaded.repoRoot,s,p.manifest),I.length===0||!l?0:Jn(r,n,a,d,l,I,u)},zn=async(e,t)=>{let{adopt:n,yes:o}=t;if(e.orphanCandidates.length===0)return ke(e,{adopt:n,yes:o});let r=X();return e.adoptionNeeded&&!n&&(E(`The content root has ${e.orphanCandidates.length} file(s) the CLI does not track:
19
+ `),$t(e.orphanCandidates),r&&(n=await Te("Adopt this directory and allow the CLI to remove these files?"))),e.outsized&&!o&&(n||!e.adoptionNeeded)&&(E(`This pull would remove ${e.orphanCandidates.length} file(s), more than the ${e.writes.length} it writes:
20
+ `),$t(e.orphanCandidates),r&&(o=await Te("Proceed with removing them?"))),ke(e,{adopt:n,yes:o})},Jn=async(e,t,n,o,r,s,a)=>{let c=xt(X()),l=await rt(e,t.model,r.id,c,s.length);if(l.status==="FAILED")return E(`warning: the diagram render failed. Text fragments are written; media was not fetched. Re-run to retry.
21
+ `),1;let d=0;for(let p of s)try{let u=await st(e,t.model,p,{template:n,commit:o});vt(t.loaded.repoRoot,a,p,u)}catch(u){d+=1,E(`warning: could not fetch media ${p}: ${u instanceof Error?u.message:String(u)}
22
+ `)}return l.status==="COMPLETED_WITH_ERRORS"||d>0?(E(`warning: ${d} media file(s) could not be written; the pull is re-runnable.
23
+ `),1):0},It=(e,t,n,o,r,s,a,c,l)=>{let d=o?`commit ${o}`:e.version?`version ${e.version}`:"HEAD",p=`${n.stack} ${n.version}`;y(`${l?"Dry run: ":""}Pulling ${e.model} (stack ${p}, template ${t}) at ${d} into ${e.instance.host}.
24
+ `),y(` Write: ${r.writes.length} file(s)${r.overwrites.length?` (${r.overwrites.length} overwritten)`:""}
25
+ `),y(` Diagrams: ${c}
26
+ `);let u=l?r.orphanCandidates:s?.deletes??[];y(` Remove: ${u.length} file(s) [${r.mode}]
27
+ `);for(let h of u)y(` - ${h}
28
+ `);if(l)r.adoptionNeeded&&y(` note: removing these needs --adopt (the root is not CLI-managed yet).
29
+ `),r.outsized&&y(` note: this removal is larger than the write set and needs --yes.
30
+ `);else for(let h of s?.warnings??[])E(`warning: ${h}
31
+ `);for(let h of a)y(` moved: ${h.oldPath} -> ${h.newPath}
32
+ `);if(l)for(let h of r.writes)y(` write ${h}
33
+ `)};var Z=e=>{let t=[];for(let n of e)n.forEach((o,r)=>{t[r]=Math.max(t[r]??0,o.length)});return t},O=(e,{indent:t="",gap:n=2,widths:o}={})=>{let s=Z(e).map((c,l)=>o?.[l]??c),a=" ".repeat(n);return e.map(c=>t+c.map((l,d)=>d===c.length-1?l:l.padEnd(s[d])).join(a))};var Gn=["STACK","TEMPLATE","NAME"],Rt=async e=>{let{values:t}=S(e.argv,{host:{type:"string"},model:{type:"string"},stack:{type:"string"}}),n=J(e,{host:t.host,model:t.model}),o=Q(n.instance,e.env),r=await o.getJson(`/models/${n.model}`),s=(r.metamodelExtensions??[]).map(h=>h.id),a=s.length>0?{metamodelExtensions:s.join(",")}:void 0,{items:c}=await o.getJson(`/metamodels/${r.metamodel.id}/templates`,a),l=c??[];if(l.length===0)return process.stdout.write(`This model has no document templates.
34
+ `),0;let d=t.stack,p=d?l.filter(h=>h.stack===d):l;if(p.length===0)return process.stdout.write(`No document templates for stack '${d}'.
35
+ `),0;let u=p.map(h=>[h.stack??"",h.key??h.id,h.name]);for(let h of O([Gn,...u]))process.stdout.write(`${h}
36
+ `);return 0};var Ce={bundle:{pull:{summary:"Materialize a model's content bundle into a local folder",usage:"bundle pull --model <id> --stack <id> --template <slug> [--host <host>] [--version <name>] [--no-diagrams] [--dry-run]",handler:Pt},templates:{summary:"List a model's document templates",usage:"bundle templates --model <id> [--stack <id>] [--host <host>]",handler:Rt}},auth:{login:{summary:"Log in through the browser (OAuth authorization code + PKCE)",usage:"auth login [--host <host>]",handler:Qe},logout:{summary:"Remove the stored credential for the active host",usage:"auth logout [--host <host>]",handler:Xe},status:{summary:"Show the active host, logged-in identity, and token validity",usage:"auth status [--host <host>]",handler:Ze}}};var At={name:"@modeldesk/cli",version:"0.2.0",description:"Command-line interface (CLI) for the ModelDesk platform",keywords:["modeldesk","cli","documentation","content-bundle","respec","mim","information-model"],homepage:"https://modeldesk.io",repository:{type:"git",url:"git+https://github.com/model-desk/modeldesk.git",directory:"packages/cli"},license:"MIT",type:"module",bin:{modeldesk:"dist/modeldesk.js"},files:["dist"],publishConfig:{access:"public"},engines:{node:">=20"},scripts:{build:"node esbuild.config.mjs",prepublishOnly:"npm run build",typecheck:"tsc --noEmit",lint:"eslint","lint:fix":"eslint --fix",test:"vitest run","test:watch":"vitest"},devDependencies:{"@eslint/js":"^10.0.1","@modeldesk/content-bundle":"*","@types/node":"^24.13.2",esbuild:"^0.28.1",eslint:"^10.8.0","eslint-config-prettier":"^10.1.5","eslint-plugin-prettier":"^5.5.6",globals:"^17.8.0",prettier:"^3.9.5",typescript:"^6.0.3","typescript-eslint":"^8.65.0",vitest:"^4.1.9"}};var _t=At.version;var Wn={out:e=>process.stdout.write(e),err:e=>process.stderr.write(e)},Lt=new Set(["--help","-h","help"]),Dt=async(e,t=Wn)=>{try{return await Kn(e,t)}catch(n){return n instanceof i?(t.err(`error: ${n.message}
37
+ `),n.exitCode):(t.err(`error: ${n instanceof Error?n.message:String(n)}
38
+ `),1)}},Kn=async(e,t)=>{let[n,...o]=e;if(n===void 0||Lt.has(n))return t.out(Vn()),0;if(n==="--version"||n==="-v")return t.out(`${_t}
39
+ `),0;let r=Ce[n];if(!r)throw new i(`Unknown command '${n}'. Run 'modeldesk --help' to see available commands.`);let[s,...a]=o;if(s===void 0){let p=Object.keys(r).join(", ");throw new i(`'${n}' requires a subcommand (${p}). Run 'modeldesk ${n} --help'.`)}if(Lt.has(s))return t.out(Yn(n,r)),0;let c=r[s];if(!c)throw new i(`Unknown '${n}' command '${s}'. Run 'modeldesk ${n} --help'.`);let l={argv:a,env:process.env,cwd:process.cwd()},d=await c.handler(l);return typeof d=="number"?d:0},Ot=[["--help, -h","Show help"],["--version, -v","Show the CLI version"]],Vn=()=>{let e=[];for(let[o,r]of Object.entries(Ce))for(let[s,a]of Object.entries(r))e.push([`${o} ${s}`,a.summary]);let[t]=Z([...e,...Ot]);return`${["ModelDesk CLI","","Usage: modeldesk <command> <subcommand> [options]","","Commands:",...O(e,{indent:" ",widths:[t]}),"","Global options:",...O(Ot,{indent:" ",widths:[t]}),""].join(`
38
40
  `)}
39
- `},xo=(e,t)=>{let[o=0]=W(Object.keys(t).map(r=>[r])),n=[`Usage: modeldesk ${e} <subcommand> [options]`,"","Subcommands:"];for(let[r,s]of Object.entries(t))n.push(` ${r.padEnd(o)}${" ".repeat(2)}${s.summary}`),n.push(` ${" ".repeat(o+2)}modeldesk ${s.usage}`);return n.push(""),`${n.join(`
41
+ `},Yn=(e,t)=>{let[n=0]=Z(Object.keys(t).map(r=>[r])),o=[`Usage: modeldesk ${e} <subcommand> [options]`,"","Subcommands:"];for(let[r,s]of Object.entries(t))o.push(` ${r.padEnd(n)}${" ".repeat(2)}${s.summary}`),o.push(` ${" ".repeat(n+2)}modeldesk ${s.usage}`);return o.push(""),`${o.join(`
40
42
  `)}
41
- `};var bo=await pt(process.argv.slice(2));process.exitCode=bo;
43
+ `};var Qn=await Dt(process.argv.slice(2));process.exitCode=Qn;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modeldesk/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Command-line interface (CLI) for the ModelDesk platform",
5
5
  "keywords": [
6
6
  "modeldesk",
@@ -45,13 +45,13 @@
45
45
  "@modeldesk/content-bundle": "*",
46
46
  "@types/node": "^24.13.2",
47
47
  "esbuild": "^0.28.1",
48
- "eslint": "^10.7.0",
48
+ "eslint": "^10.8.0",
49
49
  "eslint-config-prettier": "^10.1.5",
50
50
  "eslint-plugin-prettier": "^5.5.6",
51
- "globals": "^17.7.0",
51
+ "globals": "^17.8.0",
52
52
  "prettier": "^3.9.5",
53
53
  "typescript": "^6.0.3",
54
- "typescript-eslint": "^8.64.0",
54
+ "typescript-eslint": "^8.65.0",
55
55
  "vitest": "^4.1.9"
56
56
  }
57
57
  }