@modeldesk/cli 0.0.0-reserved.0 → 0.1.1
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/LICENSE +21 -0
- package/README.md +162 -5
- package/dist/modeldesk.js +41 -0
- package/package.json +50 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ModelDesk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,9 +1,166 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ModelDesk CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The [ModelDesk](https://modeldesk.io) command-line interface pulls a model's generated documentation
|
|
4
|
+
**content bundle** into your repository, so your own documentation pipeline (for example ReSpec with
|
|
5
|
+
GitHub Actions) can render and publish it. ModelDesk generates the content and hands it off; it does
|
|
6
|
+
not render, host, or publish documents.
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
The CLI runs on Node.js (version 20 or newer). Run it on demand with `npx`, or install it globally.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npx @modeldesk/cli bundle pull --model <modelId> --stack respec --template conceptueel-toc
|
|
14
|
+
# or install it once
|
|
15
|
+
npm install -g @modeldesk/cli
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Commands
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
modeldesk bundle pull Materialize a model's content bundle into the repository
|
|
22
|
+
modeldesk bundle templates List a model's document templates (its stack/template pairs)
|
|
23
|
+
modeldesk auth login Log in through the browser (OAuth authorization code + PKCE)
|
|
24
|
+
modeldesk auth logout Remove the stored credential for the active host
|
|
25
|
+
modeldesk auth status Show the active host, identity, and token validity
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Global options: `--help` (`-h`), `--version` (`-v`). Every command takes named flags only; there are
|
|
29
|
+
no positional arguments.
|
|
30
|
+
|
|
31
|
+
## Log in
|
|
32
|
+
|
|
33
|
+
`modeldesk auth login` opens your browser to the ModelDesk sign-in page. You authenticate there (the
|
|
34
|
+
CLI never sees your password), and after a successful login the CLI stores a refresh token in
|
|
35
|
+
`~/.modeldesk/credentials.json` (readable only by you). Later commands run unattended: the CLI
|
|
36
|
+
refreshes silently and only re-opens the browser when the stored credential lapses.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
modeldesk auth login --host modeldesk.io
|
|
40
|
+
modeldesk auth status # shows who you are logged in as, and whether the credential is valid
|
|
41
|
+
modeldesk auth logout # removes the stored credential for the host
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Configuration
|
|
45
|
+
|
|
46
|
+
The model, stack, template, host, and version each resolve by precedence: a command-line flag, then
|
|
47
|
+
a `MODELDESK_*` environment variable, then a committed `.modeldesk/config.json`, then (for the host
|
|
48
|
+
only) a built-in default. A configured repository therefore runs `modeldesk bundle pull` with no
|
|
49
|
+
arguments, while CI can override any value per build with an environment variable.
|
|
50
|
+
|
|
51
|
+
The CLI operates on the current working directory: run it from the directory that holds
|
|
52
|
+
`.modeldesk/config.json` (typically the repository root). Config in parent directories is not
|
|
53
|
+
searched, and a pull writes its content and manifest under the directory you run it from.
|
|
54
|
+
|
|
55
|
+
```jsonc
|
|
56
|
+
// .modeldesk/config.json (committed to your repository)
|
|
57
|
+
{
|
|
58
|
+
"model": "3f2b…-model-uuid",
|
|
59
|
+
"stack": "respec",
|
|
60
|
+
"template": "conceptueel-toc",
|
|
61
|
+
"host": "modeldesk.io",
|
|
62
|
+
"version": "1.0.0", // optional: pin a released model version
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
| Setting | Flag | Environment variable | Config key | Default |
|
|
67
|
+
| -------- | ------------ | -------------------- | ---------- | -------------- |
|
|
68
|
+
| Model | `--model` | `MODELDESK_MODEL` | `model` | (required) |
|
|
69
|
+
| Stack | `--stack` | `MODELDESK_STACK` | `stack` | (required) |
|
|
70
|
+
| Template | `--template` | `MODELDESK_TEMPLATE` | `template` | (required) |
|
|
71
|
+
| Host | `--host` | `MODELDESK_HOST` | `host` | `modeldesk.io` |
|
|
72
|
+
| Version | `--version` | `MODELDESK_VERSION` | `version` | live HEAD |
|
|
73
|
+
|
|
74
|
+
- **`model`** is the model's UUID (copy it from the model's "Command-line access" panel in the app).
|
|
75
|
+
- **`host`** is your instance's domain — `modeldesk.io` resolves to `https://api.modeldesk.io` and
|
|
76
|
+
`https://auth.modeldesk.io`. Use `localhost` against a local development stack.
|
|
77
|
+
- **`stack`** and **`template`** together identify the document shape to pull. A template slug is
|
|
78
|
+
unique only within its stack, so both are required — pass `--stack`/`--template`, set the env
|
|
79
|
+
vars, or commit them. Run `modeldesk bundle templates` to list the valid `stack` / `template`
|
|
80
|
+
pairs for a model.
|
|
81
|
+
- The API key for the (future) non-interactive CI mode is read only from `MODELDESK_API_KEY` and is
|
|
82
|
+
never placed in the committed config.
|
|
83
|
+
|
|
84
|
+
## `bundle pull`
|
|
85
|
+
|
|
86
|
+
A pull is a **full sync** of the content root (for ReSpec, `data/model/`). It writes every
|
|
87
|
+
generated fragment, removes files a previous pull left orphaned, and renders and fetches the
|
|
88
|
+
per-diagram images, streaming the render progress to your terminal. The whole pull is pinned to a
|
|
89
|
+
single commit, so a moving HEAD cannot mix fragments. Text is written first: a failure in the slower
|
|
90
|
+
diagram phase leaves valid text on disk and exits non-zero, and the pull stays re-runnable.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
modeldesk bundle pull # in a configured repository (model, stack, template from config)
|
|
94
|
+
modeldesk bundle pull --model <modelId> --stack respec --template conceptueel-toc --host modeldesk.io
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
> **The content root is CLI-managed.** Everything under the content root (`data/model/`) is owned by
|
|
98
|
+
> the CLI: hand edits there are overwritten, and stale files are removed. Keep hand-authored prose
|
|
99
|
+
> outside the content root (your template references the generated files with `data-include`). Run
|
|
100
|
+
> `--dry-run` first to preview exactly what a pull would write, overwrite, and remove.
|
|
101
|
+
|
|
102
|
+
### Flags
|
|
103
|
+
|
|
104
|
+
| Flag | Effect |
|
|
105
|
+
| ---------------- | --------------------------------------------------------------------------------- |
|
|
106
|
+
| `--model <id>` | The model's UUID (required, unless set via env or config). |
|
|
107
|
+
| `--stack <id>` | The content stack to render (required; see `bundle templates`). |
|
|
108
|
+
| `--template <s>` | The document shape within the stack (required; see `bundle templates`). |
|
|
109
|
+
| `--host <host>` | Target a specific instance (default: `modeldesk.io`). |
|
|
110
|
+
| `--version <n>` | Pin a released model version for a reproducible pull (default: live HEAD). |
|
|
111
|
+
| `--dry-run` | Preview the plan (writes, overwrites, removals, diagram count) and touch nothing. |
|
|
112
|
+
| `--no-diagrams` | Write text fragments only; skip the diagram render and fetch (a fast inner loop). |
|
|
113
|
+
| `--adopt` | Allow the CLI to prune a content root it did not previously manage. |
|
|
114
|
+
| `--yes` | Skip the confirmation for an unusually large removal set. |
|
|
115
|
+
| `--allow-empty` | Permit a pull that returns no fragments (a genuinely emptied model). |
|
|
116
|
+
|
|
117
|
+
`--no-diagrams` is convenient while iterating on text, but any diagram `data-include` will 404 at
|
|
118
|
+
bake time until you run a full pull.
|
|
119
|
+
|
|
120
|
+
> **Ignore the manifest.** Each pull records what it wrote in `.modeldesk/<stack>.manifest.json`
|
|
121
|
+
> (for ReSpec, `.modeldesk/respec.manifest.json`). It is regenerated every pull and is used for
|
|
122
|
+
> precise cleanup and rename hints, so add `.modeldesk/*.manifest.json` to your `.gitignore` —
|
|
123
|
+
> committing it is unnecessary and a stale committed copy can only mislead a later pull.
|
|
124
|
+
|
|
125
|
+
### Safe removal
|
|
126
|
+
|
|
127
|
+
A pull's one destructive step — removing orphaned files — is guarded so a misconfiguration cannot
|
|
128
|
+
delete files the CLI did not create:
|
|
129
|
+
|
|
130
|
+
- A pull that returns **no fragments** (usually a wrong model or template) writes and deletes nothing
|
|
131
|
+
and exits with an error; use `--allow-empty` only for a genuinely emptied model.
|
|
132
|
+
- In a repository the CLI already manages, it removes **only files it recorded writing**.
|
|
133
|
+
- A non-empty content root that the CLI has **not** managed before is not pruned until you confirm it
|
|
134
|
+
(interactively, or with `--adopt` in CI).
|
|
135
|
+
- An unusually large removal set asks for confirmation (or `--yes`).
|
|
136
|
+
- All writes and removals stay within the content root; a manifest path that tries to escape it is
|
|
137
|
+
refused.
|
|
138
|
+
|
|
139
|
+
The removal list is always printed. In CI, run a `--dry-run` first as a safety gate.
|
|
140
|
+
|
|
141
|
+
## `bundle templates`
|
|
142
|
+
|
|
143
|
+
Lists the document templates available for a model — the shapes you can pass to `--template`. The
|
|
144
|
+
output is a table: the content `STACK`, the `TEMPLATE` slug (the `--template` value), and its display
|
|
145
|
+
`NAME`. A **stack** (for example `respec`) is the render engine and content root a template produces
|
|
146
|
+
for; a template slug is unique only _within_ a stack, so the stack namespaces it — read a row as
|
|
147
|
+
`stack / template`.
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
modeldesk bundle templates --model <modelId> --host modeldesk.io
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
STACK TEMPLATE NAME
|
|
155
|
+
respec conceptueel MIM conceptueel
|
|
156
|
+
respec conceptueel-toc MIM conceptueel (TOC hiërarchisch)
|
|
157
|
+
respec logisch MIM logisch
|
|
158
|
+
respec logisch-toc MIM logisch (TOC hiërarchisch)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Take a `STACK` / `TEMPLATE` pair from the table straight into a pull. Pass `--stack <id>` to list
|
|
162
|
+
only the templates of one stack.
|
|
163
|
+
|
|
164
|
+
---
|
|
8
165
|
|
|
9
166
|
Learn more at [modeldesk.io](https://modeldesk.io).
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'module'; const require = createRequire(import.meta.url);
|
|
3
|
+
import{chmodSync as fe,existsSync as pt,mkdirSync as ft,readFileSync as gt,writeFileSync as ut}from"node:fs";import{homedir as ht}from"node:os";import{join as ge}from"node:path";var ue=448,he=384,u=class{dir;file;constructor(e=ge(ht(),".modeldesk")){this.dir=e,this.file=ge(e,"credentials.json")}getRefreshToken(e){let o=this.read().hosts[e]?.refreshToken;return typeof o=="string"?o:void 0}saveRefreshToken(e,o){let n=this.read();n.hosts[e]={refreshToken:o},this.write(n)}saveRefreshTokenIfPresent(e,o){o&&this.saveRefreshToken(e,o)}clearHost(e){let o=this.read();return o.hosts[e]?(delete o.hosts[e],this.write(o),!0):!1}read(){if(!pt(this.file))return{hosts:{}};try{let e=JSON.parse(gt(this.file,"utf8"));if(e&&typeof e=="object"&&e.hosts&&typeof e.hosts=="object")return e}catch{}return{hosts:{}}}write(e){ft(this.dir,{recursive:!0,mode:ue}),ut(this.file,`${JSON.stringify(e,null,2)}
|
|
4
|
+
`,{mode:he}),fe(this.dir,ue),fe(this.file,he)}};var yt=t=>{let e=t.split(".");if(e.length===3)try{let o=Buffer.from(e[1],"base64url").toString("utf8"),n=JSON.parse(o);return n&&typeof n=="object"?n:void 0}catch{return}},I=t=>{let e=yt(t);if(e)return e.name&&e.email?`${e.name} <${e.email}>`:e.email??e.name??e.sub};import{spawn as wt}from"node:child_process";import{platform as vt}from"node:process";var ye=t=>{if(!kt(t))return;let{command:e,args:o,verbatim:n}=Ct(t);try{let r=wt(e,o,{stdio:"ignore",detached:!0,windowsVerbatimArguments:n});r.on("error",()=>{}),r.unref()}catch{}},kt=t=>{try{let{protocol:e}=new URL(t);return e==="http:"||e==="https:"}catch{return!1}},Ct=(t,e=vt)=>{switch(e){case"darwin":return{command:"open",args:[t]};case"win32":return{command:"cmd",args:["/c","start",'""',xt(t)],verbatim:!0};default:return{command:"xdg-open",args:[t]}}},xt=t=>t.replace(/[&|<>^()%"]/g,e=>`^${e}`);var P="modeldesk-cli",we="openid",G="/callback";var i=class extends Error{exitCode;constructor(e,o=1){super(e),this.name="CliError",this.exitCode=o}};var w=async t=>{let e=`${t.replace(/\/+$/,"")}/.well-known/openid-configuration`,o;try{o=await fetch(e,{headers:{Accept:"application/json"},signal:AbortSignal.timeout(3e4)})}catch(r){throw new i(`Could not reach the auth server at ${t} (${r.message}).`)}if(!o.ok)throw new i(`OpenID discovery failed at ${e}: ${o.status} ${o.statusText}`);let n=await o.json();if(!n.authorization_endpoint||!n.token_endpoint)throw new i(`OpenID discovery at ${e} did not return the authorization and token endpoints.`);return{authorizationEndpoint:n.authorization_endpoint,tokenEndpoint:n.token_endpoint}};import{createServer as bt}from"node:http";var Tt=3e5,ve=(t,e=Tt)=>new Promise((o,n)=>{let r,s=new Promise((l,f)=>{r={resolve:l,reject:f}}),a=bt((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}Pt(p,f,t,r)}),c=setTimeout(()=>r.reject(new i("Timed out waiting for the browser login to complete.")),e);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})})}),Pt=(t,e,o,n)=>{if(t.searchParams.get("state")!==o){e.writeHead(404,{"Content-Type":"text/plain",Connection:"close"}).end("Unexpected request");return}let r=t.searchParams.get("error");if(r){let a=t.searchParams.get("error_description");q(e,"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=t.searchParams.get("code");if(!s){q(e,"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(e,"Signed in","You are signed in to the ModelDesk CLI. You can close this window and return to the terminal."),n.resolve(s)},q=(t,e,o)=>{let n=`<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${e}</title></head><body style="font-family: system-ui, sans-serif; text-align: center; padding: 3rem;"><h1>${e}</h1><p>${o}</p></body></html>`;t.writeHead(200,{"Content-Type":"text/html; charset=utf-8",Connection:"close"}).end(n)};import{createHash as Et,randomBytes as ke}from"node:crypto";var V=t=>t.toString("base64url"),Ce=()=>{let t=V(ke(32)),e=V(Et("sha256").update(t).digest());return{verifier:t,challenge:e,method:"S256"}},xe=()=>V(ke(16));var L=class extends i{oauthError;constructor(e,o){super(e),this.name="TokenEndpointError",this.oauthError=o}},be=t=>t instanceof L&&t.oauthError==="invalid_grant",Te=(t,e)=>Pe(t,new URLSearchParams({grant_type:"authorization_code",code:e.code,redirect_uri:e.redirectUri,client_id:P,code_verifier:e.codeVerifier})),A=(t,e)=>Pe(t,new URLSearchParams({grant_type:"refresh_token",refresh_token:e,client_id:P})),Pe=async(t,e)=>{let o;try{o=await fetch(t,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Accept:"application/json"},body:e.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 $t(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}},$t=async t=>{try{if((t.headers.get("content-type")??"").includes("json")){let o=await t.json();if(o.error)return{message:o.error_description?`${o.error} (${o.error_description})`:o.error,code:o.error}}}catch{}return{message:`${t.status} ${t.statusText}`.trim()}};var _={info:t=>process.stderr.write(`${t}
|
|
5
|
+
`)},M=async(t,e)=>{let{authorizationEndpoint:o,tokenEndpoint:n}=await w(t.issuer),r=Ce(),s=xe(),a=await ve(s);try{let c=St(o,a.redirectUri,r.challenge,s);e.info(`Opening your browser to sign in to ${t.host}...`),e.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()}},St=(t,e,o,n)=>{let r=new URL(t);return r.searchParams.set("response_type","code"),r.searchParams.set("client_id",P),r.searchParams.set("redirect_uri",e),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 Rt="http://localhost:8080",It="http://localhost:8082",Lt="localhost",At=t=>{let e=t.trim(),o=/:\/\//.test(e)?e:`https://${e}`;try{return new URL(o).hostname}catch{return e.replace(/^[a-z][a-z0-9+.-]*:\/\//i,"").split("/")[0].split(":")[0]}},_t=t=>t==="localhost"||t==="127.0.0.1"||t==="::1"||t==="[::1]",Y=t=>{let e=At(t);return _t(e)?{host:Lt,apiBase:Rt,issuer:It}:{host:e,apiBase:`https://api.${e}`,issuer:`https://auth.${e}`}};import{existsSync as Mt,readFileSync as Ot}from"node:fs";import{join as Dt}from"node:path";var O=".modeldesk",Ft="config.json",D=t=>t.configPath!==void 0,Q=t=>{let e=Dt(t,O,Ft);return Mt(e)?{config:Nt(e),configPath:e,repoRoot:t}:{config:{},repoRoot:t}},Nt=t=>{let e;try{e=Ot(t,"utf8")}catch(r){throw new i(`Could not read ${t}: ${r.message}`)}let o;try{o=JSON.parse(e)}catch(r){throw new i(`${t} is not valid JSON: ${r.message}`)}if(o===null||typeof o!="object"||Array.isArray(o))throw new i(`${t} must contain a JSON object.`);let n=o;return{host:E(n,"host",t),model:E(n,"model",t),stack:E(n,"stack",t),template:E(n,"template",t),version:E(n,"version",t)}},E=(t,e,o)=>{let n=t[e];if(n!==void 0){if(typeof n!="string")throw new i(`${o}: "${e}" 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"},$=(...t)=>t.find(e=>e!==void 0&&e!==""),Z=(t,e,o)=>({host:$(t.host,e[g.host],o.host)??X,model:$(t.model,e[g.model],o.model),stack:$(t.stack,e[g.stack],o.stack),template:$(t.template,e[g.template],o.template),version:$(t.version,e[g.version],o.version)});import{parseArgs as jt}from"node:util";var v=(t,e)=>{try{return jt({args:t,options:e,allowPositionals:!1,strict:!0})}catch(o){throw new i(o.message)}};var Ee=(t,e,o)=>{let n=e!==void 0||o[g.host]!==void 0,r=t.config.host;!n&&D(t)&&r&&r!==X&&process.stderr.write(`note: using host '${r}' from ${t.configPath} (it drives login and API calls).
|
|
7
|
+
`)},k=t=>{let{values:e}=v(t.argv,{host:{type:"string"}}),o=Q(t.cwd),n=Z({host:e.host},t.env,o.config);return Ee(o,e.host,t.env),Y(n.host)},F=(t,e)=>{let o=Q(t.cwd),n=Z(e,t.env,o.config);if(Ee(o,e.host,t.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=(t,e,o,n)=>{let r=t?.trim();if(!r)throw new i(`No ${e} specified. Pass ${n}, set ${o}, or add "${e}" to .modeldesk/config.json. Run 'modeldesk bundle templates' to list a model's stacks and templates.`);return r};var $e=async t=>{let e=k(t),o=await M(e,_),n=new u;o.refreshToken?n.saveRefreshToken(e.host,o.refreshToken):n.clearHost(e.host);let r=I(o.accessToken);return process.stdout.write(`Logged in to ${e.host}${r?` as ${r}`:""}.
|
|
8
|
+
`),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 Se=async t=>{let e=k(t),o=new u().clearHost(e.host);return process.stdout.write(o?`Logged out of ${e.host}.
|
|
10
|
+
`:`No stored credential for ${e.host}.
|
|
11
|
+
`),0};var Re=async t=>{let e=k(t),o=s=>{process.stdout.write(`${s}
|
|
12
|
+
`)};o(`Host: ${e.host}`),o(`API: ${e.apiBase}`);let n=new u,r=n.getRefreshToken(e.host);if(!r)return o("Status: not logged in. Run 'modeldesk auth login'."),1;try{let{tokenEndpoint:s}=await w(e.issuer),a=await A(s,r);n.saveRefreshTokenIfPresent(e.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 Bt=6e4,N=class{constructor(e,o={}){this.instance=e;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-Bt)return this.accessToken;let e=this.store.getRefreshToken(this.instance.host);if(e){let o=await this.tryRefresh(e);if(o)return o}return this.login()}async tryRefresh(e){let{tokenEndpoint:o}=await this.getEndpoints();try{return this.apply(await A(o,e))}catch(n){if(be(n))return;throw n}}async login(){return this.apply(await M(this.instance,this.io))}apply(e){return this.accessToken=e.accessToken,this.accessTokenExpiresAt=this.now()+e.expiresInSeconds*1e3,this.store.saveRefreshTokenIfPresent(this.instance.host,e.refreshToken),e.accessToken}async getEndpoints(){return this.endpoints||(this.endpoints=await w(this.instance.issuer)),this.endpoints}};var j=(t,e)=>{if(e[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(t)};import{existsSync as te,lstatSync as Ie,realpathSync as Ht}from"node:fs";import{homedir as Ut}from"node:os";import{basename as zt,dirname as Jt,isAbsolute as B,join as Le,relative as oe,resolve as C}from"node:path";var U=(t,e,o)=>{if(B(o))throw new i(`Refusing an absolute path from the bundle manifest: ${o}`);let n=C(t,e),r=C(t,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},Wt=t=>{try{return Ie(t)}catch{return}},H=t=>{let e=[],o=t;for(;!te(o);){let r=Jt(o);if(r===o)return t;e.unshift(zt(o)),o=r}let n=Ht(o);return e.length>0?Le(n,...e):n},ne=t=>{if(Wt(t)?.isSymbolicLink())throw new i(`Refusing to write through an existing symlink at a managed path: ${t}`)},z=(t,e)=>{let o=H(t),n=H(e),r=oe(o,n);if(r!==""&&(r.startsWith("..")||B(r)))throw new i(`Refusing a path that escapes the content root through a symlink: ${e}`)},re=(t,e)=>{let o=C(t,e);if(o===C(t))throw new i(`The content root resolves to the repository root (${o}); refusing to sync it.`);if(o===C(Ut()))throw new i("The content root resolves to your home directory; refusing to sync it.");if(te(o)){if(Ie(o).isSymbolicLink())throw new i(`The content root ${o} is a symlink; refusing to sync it.`);if(te(Le(o,".git")))throw new i(`The content root ${o} contains a .git directory (it looks like a repo root); refusing.`)}let n=H(C(t)),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 Kt={respec:"data/model"},Ae=t=>{let e=Kt[t];if(!e)throw new i(`The CLI does not know the content root for stack '${t}'.`);return e};var Gt=1500,qt=6e5,Vt=t=>new Promise(e=>setTimeout(e,t)),Yt=t=>t==="COMPLETED"||t==="COMPLETED_WITH_ERRORS"||t==="FAILED",_e=async(t,e,o)=>{let n=await t.postForJson(`/models/${e}/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},Me=async(t,e,o,n,r,s={})=>{let a=s.sleep??Vt,c=s.now??Date.now,m=c()+qt;for(n.update(0,r);;){let d=await t.getJson(`/models/${e}/export-jobs/${o}`);if(d.progress&&n.update(d.progress.completed,d.progress.total,d.progress.currentItem),Yt(d.status))return n.done(),d;if(c()>=m)throw new i("Timed out waiting for the diagram render to finish.");await a(Gt)}},Oe=(t,e,o,n)=>t.getBytes(`/models/${e}/content-bundle/media`,{path:o,template:n.template,commit:n.commit});var De=t=>{let e=new Map;for(let o of t.fragments??[])o.realization?.path&&e.set(o.elementId,o.realization.path);return e},Fe=(t,e)=>{let o=De(t),n=[];for(let[r,s]of De(e)){let a=o.get(r);a&&a!==s&&n.push({elementId:r,oldPath:a,newPath:s})}return n};var se=t=>{let e=t.payloads??{},o=[];for(let n of t.manifest.fragments??[]){let r=n.realization?.path;if(!r)continue;let s=e[n.address];s!==void 0&&o.push({path:r,content:s})}return o},ie=t=>{let e=[];for(let o of t.fragments??[])for(let n of o.realization?.media??[])e.push(n);return[...new Set(e)]};var Ne=t=>`${JSON.stringify(t,null,2)}
|
|
13
|
+
`;import{existsSync as Qt,mkdirSync as Xt,readFileSync as Zt,writeFileSync as eo}from"node:fs";import{join as je}from"node:path";var Be=(t,e)=>je(t,O,`${e}.manifest.json`),He=(t,e)=>{let o=Be(t,e);if(!Qt(o))return;let n;try{n=JSON.parse(Zt(o,"utf8"))}catch{return}return to(n)?n:void 0},to=t=>{if(t===null||typeof t!="object"||Array.isArray(t))return!1;let{fragments:e}=t;return e===void 0?!0:Array.isArray(e)&&e.every(o=>o!==null&&typeof o=="object")},Ue=(t,e,o)=>{Xt(je(t,O),{recursive:!0}),eo(Be(t,e),Ne(o))},ze=t=>{let e=[];for(let o of t.fragments??[]){o.realization?.path&&e.push(o.realization.path);for(let n of o.realization?.media??[])e.push(n)}return e};import{existsSync as Je,mkdirSync as We,readdirSync as oo,rmSync as no,writeFileSync as Ke}from"node:fs";import{dirname as Ge,join as S,relative as ro,sep as so}from"node:path";var qe=(t,e)=>{let o=S(t,e);if(!Je(o))return[];let n=[],r=s=>{for(let a of oo(s,{withFileTypes:!0})){let c=S(s,a.name);a.isDirectory()?r(c):a.isFile()&&n.push(ro(t,c).split(so).join("/"))}};return r(o),n},Ve=(t,e,o)=>{let n=S(t,e);for(let r of o){let s=U(t,e,r.path);ne(s),z(n,s),We(Ge(s),{recursive:!0}),Ke(s,r.content)}},Ye=(t,e,o,n)=>{let r=S(t,e),s=U(t,e,o);ne(s),z(r,s),We(Ge(s),{recursive:!0}),Ke(s,n)},Qe=(t,e,o)=>{let n=S(t,e);for(let r of o){let s=U(t,e,r);z(n,s),Je(s)&&no(s)}};var io=100,Ze=t=>{if(t.newTextPaths.length===0&&!t.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 e=Xe([...t.newTextPaths,...t.newMediaPaths]).sort(),o=new Set(e),n=new Set(t.existingFiles),r=e.filter(l=>n.has(l)),s=t.priorManifestPaths!==void 0?"manifest-tracked":"full-sync",a=t.priorManifestPaths??t.existingFiles,c=Xe(a).filter(l=>!o.has(l)).sort(),m=c.length>0&&!t.adopted,d=c.length>0&&(c.length>e.length||c.length>io);return{writes:e,overwrites:r,orphanCandidates:c,mode:s,adoptionNeeded:m,outsized:d}},ae=(t,e)=>{if(t.orphanCandidates.length===0)return{deletes:[],warnings:[]};if(t.adoptionNeeded&&!e.adopt)return{deletes:[],warnings:[`${t.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(t.outsized&&!e.yes)throw new i(`This pull would remove ${t.orphanCandidates.length} file(s), more than the ${t.writes.length} it writes. That often signals a wrong model or format. Nothing was changed. Re-run with --yes if intended.`);return{deletes:t.orphanCandidates,warnings:[]}},Xe=t=>[...new Set(t)];var ao=6e4,x=class{constructor(e,o){this.apiBase=e;this.tokens=o}apiBase;tokens;async getJson(e,o){return await(await this.request(e,{method:"GET"},o)).json()}async postForJson(e,o){return await(await this.request(e,{method:"POST"},o)).json()}async getBytes(e,o){let n=await this.request(e,{method:"GET",headers:{Accept:"image/png"}},o);return new Uint8Array(await n.arrayBuffer())}async request(e,o,n){let r=await this.tokens.getAccessToken(),s=co(this.apiBase,e,n),a;try{a=await fetch(s,{...o,headers:{Authorization:`Bearer ${r}`,Accept:"application/json",...o.headers??{}},signal:AbortSignal.timeout(ao)})}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 mo(a,e);return a}},co=(t,e,o)=>{let n=new URL(e,t);if(o)for(let[r,s]of Object.entries(o))s!==void 0&&n.searchParams.set(r,s);return n.toString()},mo=async(t,e)=>{let o=`${t.status} ${t.statusText}`.trim();try{if((t.headers.get("content-type")??"").includes("json")){let r=await t.json(),s=r.detail??r.title;s&&(o=`${t.status} ${s}`)}}catch{}return t.status===401?new i(`Authentication failed (${o}). Run 'modeldesk auth login' and try again.`):t.status===403?new i(`Not authorized (${o}). Your account may lack access to this model.`):new i(`Request to ${e} failed: ${o}`)};var lo=(t,e)=>{let o=e>0?Math.min(1,t/e):0,n=Math.round(o*24);return`[${"#".repeat(n)}${"-".repeat(24-n)}]`},et=(t,e=o=>{process.stderr.write(o)})=>{let o=-1;return{update(n,r,s){if(t){let a=s?` ${s}`:"";e(`\r${lo(n,r)} ${n}/${r}${a}\x1B[K`)}else if(n!==o){let a=s?` (${s})`:"";e(`Rendering diagrams: ${n}/${r}${a}
|
|
14
|
+
`),o=n}},done(){t&&e(`
|
|
15
|
+
`)}}};import{createInterface as po}from"node:readline";var J=()=>!!(process.stdin.isTTY&&process.stdout.isTTY),ce=async t=>{if(!J())return!1;let e=po({input:process.stdin,output:process.stderr});try{let o=await new Promise(n=>e.question(`${t} [y/N] `,n));return/^y(es)?$/i.test(o.trim())}finally{e.close()}};var h=t=>{process.stdout.write(t)},y=t=>{process.stderr.write(t)},tt=t=>{for(let e of t)y(` ${e}
|
|
16
|
+
`)},nt=async t=>{let{values:e}=v(t.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(t,{host:e.host,model:e.model,stack:e.stack,template:e.template,version:e.version}),n={noDiagrams:!!e["no-diagrams"],dryRun:!!e["dry-run"],allowEmpty:!!e["allow-empty"],adopt:!!e.adopt,yes:!!e.yes},r=new x(o.instance.apiBase,j(o.instance,t.env)),s=ee(o.stack,"stack",g.stack,"--stack"),a=ee(o.template,"template",g.template,"--template"),m=!n.noDiagrams&&!n.dryRun?await _e(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=Ae(s);re(o.loaded.repoRoot,f);let p=se(l),b=ie(l.manifest),T=He(o.loaded.repoRoot,s),K=Ze({newTextPaths:p.map(lt=>lt.path),newMediaPaths:b,priorManifestPaths:T?ze(T):void 0,existingFiles:qe(o.loaded.repoRoot,f),adopted:T!==void 0||D(o.loaded),allowEmpty:n.allowEmpty}),me=T?Fe(T,l.manifest):[],le={stack:s,version:l.manifest.stackVersion};if(n.dryRun)return ot(o,a,le,d,K,void 0,me,b.length,!0),0;let pe=await fo(K,n);return ot(o,a,le,d,K,pe,me,b.length,!1),re(o.loaded.repoRoot,f),Ve(o.loaded.repoRoot,f,p),Qe(o.loaded.repoRoot,f,pe.deletes),Ue(o.loaded.repoRoot,s,l.manifest),b.length===0||!m?0:go(r,o,a,d,m,b,f)},fo=async(t,e)=>{let{adopt:o,yes:n}=e;if(t.orphanCandidates.length===0)return ae(t,{adopt:o,yes:n});let r=J();return t.adoptionNeeded&&!o&&(y(`The content root has ${t.orphanCandidates.length} file(s) the CLI does not track:
|
|
17
|
+
`),tt(t.orphanCandidates),r&&(o=await ce("Adopt this directory and allow the CLI to remove these files?"))),t.outsized&&!n&&(o||!t.adoptionNeeded)&&(y(`This pull would remove ${t.orphanCandidates.length} file(s), more than the ${t.writes.length} it writes:
|
|
18
|
+
`),tt(t.orphanCandidates),r&&(n=await ce("Proceed with removing them?"))),ae(t,{adopt:o,yes:n})},go=async(t,e,o,n,r,s,a)=>{let c=et(J()),m=await Me(t,e.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 Oe(t,e.model,l,{template:o,commit:n});Ye(e.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},ot=(t,e,o,n,r,s,a,c,m)=>{let d=n?`commit ${n}`:t.version?`version ${t.version}`:"HEAD",l=`${o.stack} ${o.version}`;h(`${m?"Dry run: ":""}Pulling ${t.model} (stack ${l}, template ${e}) at ${d} into ${t.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=t=>{let e=[];for(let o of t)o.forEach((n,r)=>{e[r]=Math.max(e[r]??0,n.length)});return e},R=(t,{indent:e="",gap:o=2,widths:n}={})=>{let s=W(t).map((c,m)=>n?.[m]??c),a=" ".repeat(o);return t.map(c=>e+c.map((m,d)=>d===c.length-1?m:m.padEnd(s[d])).join(a))};var uo=["STACK","TEMPLATE","NAME"],rt=async t=>{let{values:e}=v(t.argv,{host:{type:"string"},model:{type:"string"},stack:{type:"string"}}),o=F(t,{host:e.host,model:e.model}),n=new x(o.instance.apiBase,j(o.instance,t.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=e.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([uo,...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:nt},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:$e},logout:{summary:"Remove the stored credential for the active host",usage:"auth logout [--host <host>]",handler:Se},status:{summary:"Show the active host, logged-in identity, and token validity",usage:"auth status [--host <host>]",handler:Re}}};var st={name:"@modeldesk/cli",version:"0.1.1",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 it=st.version;var yo={out:t=>process.stdout.write(t),err:t=>process.stderr.write(t)},ct=new Set(["--help","-h","help"]),mt=async(t,e=yo)=>{try{return await wo(t,e)}catch(o){return o instanceof i?(e.err(`error: ${o.message}
|
|
35
|
+
`),o.exitCode):(e.err(`error: ${o instanceof Error?o.message:String(o)}
|
|
36
|
+
`),1)}},wo=async(t,e)=>{let[o,...n]=t;if(o===void 0||ct.has(o))return e.out(vo()),0;if(o==="--version"||o==="-v")return e.out(`${it}
|
|
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(ct.has(s))return e.out(ko(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},dt=[["--help, -h","Show help"],["--version, -v","Show the CLI version"]],vo=()=>{let t=[];for(let[n,r]of Object.entries(de))for(let[s,a]of Object.entries(r))t.push([`${n} ${s}`,a.summary]);let[e]=W([...t,...dt]);return`${["ModelDesk CLI","","Usage: modeldesk <command> <subcommand> [options]","","Commands:",...R(t,{indent:" ",widths:[e]}),"","Global options:",...R(dt,{indent:" ",widths:[e]}),""].join(`
|
|
38
|
+
`)}
|
|
39
|
+
`},ko=(t,e)=>{let[o=0]=W(Object.keys(e).map(r=>[r])),n=[`Usage: modeldesk ${t} <subcommand> [options]`,"","Subcommands:"];for(let[r,s]of Object.entries(e))n.push(` ${r.padEnd(o)}${" ".repeat(2)}${s.summary}`),n.push(` ${" ".repeat(o+2)}modeldesk ${s.usage}`);return n.push(""),`${n.join(`
|
|
40
|
+
`)}
|
|
41
|
+
`};var Co=await mt(process.argv.slice(2));process.exitCode=Co;
|
package/package.json
CHANGED
|
@@ -1,10 +1,57 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modeldesk/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Command-line interface (CLI) for the ModelDesk platform",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"modeldesk",
|
|
7
|
+
"cli",
|
|
8
|
+
"documentation",
|
|
9
|
+
"content-bundle",
|
|
10
|
+
"respec",
|
|
11
|
+
"mim",
|
|
12
|
+
"information-model"
|
|
13
|
+
],
|
|
6
14
|
"homepage": "https://modeldesk.io",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/model-desk/modeldesk.git",
|
|
18
|
+
"directory": "packages/cli"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"bin": {
|
|
23
|
+
"modeldesk": "dist/modeldesk.js"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
7
28
|
"publishConfig": {
|
|
8
29
|
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=20"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "node esbuild.config.mjs",
|
|
36
|
+
"prepublishOnly": "npm run build",
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"lint": "eslint",
|
|
39
|
+
"lint:fix": "eslint --fix",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"test:watch": "vitest"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@eslint/js": "^10.0.1",
|
|
45
|
+
"@modeldesk/content-bundle": "*",
|
|
46
|
+
"@types/node": "^24.13.2",
|
|
47
|
+
"esbuild": "^0.28.1",
|
|
48
|
+
"eslint": "^10.7.0",
|
|
49
|
+
"eslint-config-prettier": "^10.1.5",
|
|
50
|
+
"eslint-plugin-prettier": "^5.5.6",
|
|
51
|
+
"globals": "^17.7.0",
|
|
52
|
+
"prettier": "^3.9.5",
|
|
53
|
+
"typescript": "^6.0.3",
|
|
54
|
+
"typescript-eslint": "^8.64.0",
|
|
55
|
+
"vitest": "^4.1.9"
|
|
9
56
|
}
|
|
10
57
|
}
|