@elixpo/lixblogs-cli 1.3.1 → 1.4.2

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.
Files changed (39) hide show
  1. package/README.md +15 -10
  2. package/dist/lixblogs.mjs +102 -0
  3. package/package.json +9 -10
  4. package/API.md +0 -104
  5. package/CHANGELOG.md +0 -10
  6. package/RELEASE.md +0 -30
  7. package/THREAT_MODEL.md +0 -91
  8. package/bin/lixblogs.mjs +0 -789
  9. package/src/api/AnalyticsClient.js +0 -40
  10. package/src/api/BlogClient.js +0 -140
  11. package/src/api/CollaborationClient.js +0 -73
  12. package/src/api/OrgClient.js +0 -158
  13. package/src/auth/AuthProvider.js +0 -90
  14. package/src/auth/AuthenticatedClient.js +0 -131
  15. package/src/auth/ElixpoAuthProvider.js +0 -281
  16. package/src/auth/MockAuthProvider.js +0 -170
  17. package/src/auth/productionGate.js +0 -44
  18. package/src/cli/contract.js +0 -46
  19. package/src/cli/ui.js +0 -52
  20. package/src/commands/analytics/index.js +0 -57
  21. package/src/commands/auth/login.js +0 -103
  22. package/src/commands/auth/logout.js +0 -21
  23. package/src/commands/auth/profiles.js +0 -27
  24. package/src/commands/auth/revoke.js +0 -45
  25. package/src/commands/auth/status.js +0 -33
  26. package/src/commands/blog/index.js +0 -85
  27. package/src/commands/blog/input.js +0 -59
  28. package/src/commands/collab/index.js +0 -53
  29. package/src/commands/org/index.js +0 -22
  30. package/src/commands/skill/index.js +0 -83
  31. package/src/config/CredentialStore.js +0 -142
  32. package/src/config/KeychainCredentialStore.js +0 -180
  33. package/src/config/ProfileRegistry.js +0 -105
  34. package/src/config/config.js +0 -60
  35. package/src/config/credentialStoreFactory.js +0 -63
  36. package/src/config/providerFactory.js +0 -42
  37. package/src/config/redact.js +0 -74
  38. package/src/content/markdown.js +0 -68
  39. package/src/content/validate.js +0 -45
package/README.md CHANGED
@@ -29,6 +29,7 @@ node bin/lixblogs.mjs --help
29
29
  ```bash
30
30
  # Log in via device authorization
31
31
  node bin/lixblogs.mjs login
32
+ # Credentials are saved under the authenticated username and made active.
32
33
 
33
34
  # Save credentials under an explicit local profile name
34
35
  node bin/lixblogs.mjs login --profile personal
@@ -51,8 +52,10 @@ Interactive login shows one compact device card and stays quiet while Accounts
51
52
  approval is pending. Press Enter to open the verification URL locally, use
52
53
  `--open` to open it immediately, or copy the URL to any browser when connected
53
54
  to a VPS. Device authorization does not require a localhost callback or an
54
- exposed port. Profile names such as `personal`, `work`, or `test` are local
55
- credential slots; `whoami` reports the actual Accounts identity.
55
+ exposed port. The authenticated username becomes the local profile alias unless
56
+ `--profile` explicitly overrides it. Run `lixblogs login` again to add another
57
+ account, `lixblogs profiles` to list saved accounts, and
58
+ `lixblogs use <username>` to switch the active one.
56
59
 
57
60
  ### Blog lifecycle
58
61
 
@@ -148,7 +151,7 @@ command exits with code 3 and retains both versions under
148
151
  `.lixblogs-conflicts/`; it never overwrites the newer server revision.
149
152
 
150
153
  Global flags:
151
- - `--profile <name>` — named profile to use (default: `"default"`)
154
+ - `--profile <name>` — override the username-based local account alias
152
155
  - `--env <environment>` — override environment (`development` | `staging` | `production`)
153
156
  - `--scope <scope>` — request an additional/alternate OAuth scope; repeatable
154
157
  - `--open` — open the verification URL with the device code pre-filled
@@ -170,10 +173,11 @@ Global flags:
170
173
  - The mock provider is available only with an explicit non-production
171
174
  environment, for example `--env development --auth-provider mock`.
172
175
  - The resource contract, scopes, pagination, errors, and mutation guarantees
173
- are documented in [API.md](API.md).
176
+ are documented in the [API reference](https://github.com/elixpo/blogs.elixpo/blob/main/packages/lixblogs-cli/API.md).
174
177
  - Release compatibility, provenance, smoke gates, and rollback are documented
175
- in [RELEASE.md](RELEASE.md). Contract changes are summarized in
176
- [CHANGELOG.md](CHANGELOG.md).
178
+ in the [release policy](https://github.com/elixpo/blogs.elixpo/blob/main/packages/lixblogs-cli/RELEASE.md).
179
+ Contract changes are summarized in the
180
+ [changelog](https://github.com/elixpo/blogs.elixpo/blob/main/packages/lixblogs-cli/CHANGELOG.md).
177
181
 
178
182
  The production client is public and has no client secret. Never add one to
179
183
  CLI configuration, package files, or GitHub secrets.
@@ -207,14 +211,15 @@ npm test # runs the full CLI test suite
207
211
 
208
212
  Tests exercise both a mocked auth provider and, where relevant, the real
209
213
  OS keychain backend on whatever machine runs them — see
210
- `THREAT_MODEL.md` and inline comments in `src/config/KeychainCredentialStore.js`
214
+ the [threat model](https://github.com/elixpo/blogs.elixpo/blob/main/packages/lixblogs-cli/THREAT_MODEL.md)
215
+ and inline comments in `src/config/KeychainCredentialStore.js`
211
216
  for known platform-specific behavior (e.g. a documented WSL/keyring-rs quirk).
212
217
 
213
218
  ## Architecture
214
219
 
215
220
  ```
216
- bin/lixblogs.mjs CLI entry point (Node's native util.parseArgs, no
217
- third-party parsing dependency)
221
+ bin/lixblogs.mjs Source CLI entry point (Node's native util.parseArgs)
222
+ dist/lixblogs.mjs Minified executable shipped in the npm package
218
223
  src/auth/ Accounts provider, development mock, refresh-safe
219
224
  authenticated client, and production safety gate
220
225
  src/commands/auth/ Command logic (login, status, logout, revoke) —
@@ -225,7 +230,7 @@ src/content/ Dependency-free Markdown/block conversion
225
230
  src/config/ Credential storage (real keychain + gated fallback),
226
231
  profile registry, config resolution, token redaction
227
232
  tests/ Full test suite
228
- THREAT_MODEL.md Security threat model for the auth system
233
+ THREAT_MODEL.md Security threat model (repository documentation)
229
234
  ```
230
235
 
231
236
  Command logic under `src/commands/` is deliberately decoupled from the CLI
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ import{parseArgs as gr}from"node:util";import{spawn as wr}from"node:child_process";var F={environment:"production",profile:"default",accountsBaseUrl:"https://accounts.elixpo.com",apiBaseUrl:"https://blogs.elixpo.com"},Ee={development:{clientId:"lixblogs-cli-dev",audience:"localhost"},staging:{clientId:"lixblogs-cli-staging",audience:"staging.blogs.elixpo.com"},production:{clientId:"lixblogs-cli-prod",audience:"blogs.elixpo.com"},test:{clientId:"lixblogs-cli-dev",audience:"localhost"}};function _({flags:t={},env:e=process.env}={}){let r=t.env??e.LIXBLOGS_ENV??F.environment,o=t.profile??e.LIXBLOGS_PROFILE??F.profile,i=Ee[r]||Ee.production,n=t.authProvider??e.LIXBLOGS_AUTH_PROVIDER??(r==="production"?"elixpo":"mock"),a=t.accountsUrl??e.LIXBLOGS_ACCOUNTS_URL??F.accountsBaseUrl,s=t.apiUrl??e.LIXBLOGS_API_URL??F.apiBaseUrl,l=t.clientId??e.LIXBLOGS_CLIENT_ID??i.clientId,u=t.audience??e.LIXBLOGS_AUDIENCE??i.audience;return{environment:r,profile:o,profileExplicit:t.profile!==void 0||e.LIXBLOGS_PROFILE!==void 0,authProvider:n,accountsBaseUrl:a,apiBaseUrl:s,clientId:l,audience:u}}var P=class{get providerId(){throw new Error("AuthProvider.providerId must be implemented by subclass")}async requestDeviceCode(e){throw new Error("AuthProvider.requestDeviceCode must be implemented by subclass")}async pollDeviceCode(e){throw new Error("AuthProvider.pollDeviceCode must be implemented by subclass")}async refresh(e){throw new Error("AuthProvider.refresh must be implemented by subclass")}async revoke(e){throw new Error("AuthProvider.revoke must be implemented by subclass")}};var ke={APPROVE_IMMEDIATELY:"mock-approve-",PENDING_THEN_APPROVE:"mock-pending-then-approve-",DENY:"mock-deny-",EXPIRE:"mock-expire-",SLOW_DOWN_THEN_APPROVE:"mock-slow-down-then-approve-"},Lt=5,Se=0;function jt(t){return Se+=1,`${t}${Se}`}var z=class extends P{constructor(){super(),this._devicesCodes=new Map,this._revoked=new Set,this._refreshWillFail=new Set}get providerId(){return"mock"}async requestDeviceCode({scopes:e,scenario:r="APPROVE_IMMEDIATELY"}){let o=ke[r]??ke.APPROVE_IMMEDIATELY,i=jt(o),n=i.slice(-6).toUpperCase();return this._devicesCodes.set(i,{scenario:r,pollCount:0,createdAt:Date.now(),scopes:[...e]}),{deviceCode:i,userCode:n,verificationUri:"https://mock.lixblogs.local/device",verificationUriComplete:`https://mock.lixblogs.local/device?user_code=${encodeURIComponent(n)}`,expiresInSeconds:r==="EXPIRE"?1:600,pollIntervalSeconds:1}}async pollDeviceCode({deviceCode:e}){let r=this._devicesCodes.get(e);return r?r.scenario==="EXPIRE"?{status:"expired"}:r.scenario==="DENY"?{status:"denied"}:r.scenario==="PENDING_THEN_APPROVE"&&(r.pollCount+=1,r.pollCount<2)?{status:"pending"}:r.scenario==="SLOW_DOWN_THEN_APPROVE"&&(r.pollCount+=1,r.pollCount<2)?{status:"slow_down",pollIntervalIncreaseSeconds:Lt}:{status:"approved",token:{accessToken:`mock-access-${e}`,refreshToken:`mock-refresh-${e}`,expiresInSeconds:3600,scopes:r.scopes}}:{status:"denied"}}async refresh({refreshToken:e,scopes:r=[]}){if(this._revoked.has(e))throw new Error("refresh token has been revoked");if(this._refreshWillFail.has(e))throw new Error("mock refresh failure (test-injected)");return{accessToken:`mock-access-refreshed-${e}`,refreshToken:e,expiresInSeconds:3600,scopes:r}}async revoke({token:e}){this._revoked.add(e)}_simulateRefreshFailureFor(e){this._refreshWillFail.add(e)}};var Ae="urn:ietf:params:oauth:grant-type:device_code",Ut=1,Nt=15e3,Dt={access_denied:"Login was denied.",authorization_pending:"Login is awaiting approval.",expired_token:"The device authorization expired. Start login again.",invalid_client:"The LixBlogs CLI client is not registered for this environment.",invalid_grant:"This session is no longer valid. Log in again.",invalid_request:"Accounts rejected the authentication request.",invalid_scope:"The requested LixBlogs permissions are not available for this client.",server_error:"Accounts could not complete authentication. Try again later.",slow_down:"Accounts requested slower polling.",temporarily_unavailable:"Accounts is temporarily unavailable. Try again later."},v=class extends Error{constructor(e,{status:r=0,requiresLogin:o=!1}={}){super(Dt[e]||"Authentication failed."),this.name="AuthProviderError",this.code=e||"authentication_failed",this.status=r,this.requiresLogin=o}},I=class extends Error{constructor(e){super(e),this.name="CompatibilityError",this.code="incompatible_accounts_contract"}};function ae(t){return String(t||"0.0.0").split(".").slice(0,3).map(e=>Number.parseInt(e,10)||0)}function Bt(t,e){let r=ae(t),o=ae(e);for(let i=0;i<3;i+=1)if(r[i]!==o[i])return r[i]>o[i];return!0}function Mt(t){let e=new URL(t);if(e.pathname=e.pathname.replace(/\/$/,""),e.search="",e.hash="",e.protocol!=="https:"&&e.hostname!=="localhost"&&e.hostname!=="127.0.0.1")throw new I("Accounts must use HTTPS outside local development.");return e.toString().replace(/\/$/,"")}async function j(t){try{return await t.json()}catch{throw new v("server_error",{status:t.status})}}function G(t,e){let r=typeof t?.error=="string"?t.error:"server_error";return new v(r,{status:e.status,requiresLogin:r==="invalid_grant"||r==="access_denied"||r==="expired_token"})}function $e(t,e){if(!e.ok)throw G(t,e);if(typeof t?.access_token!="string"||typeof t?.refresh_token!="string"||!Number.isFinite(Number(t?.expires_in)))throw new v("server_error",{status:e.status});return{accessToken:t.access_token,refreshToken:t.refresh_token,expiresInSeconds:Number(t.expires_in),scopes:typeof t.scope=="string"?t.scope.split(/\s+/).filter(Boolean):[]}}var V=class extends P{constructor({accountsBaseUrl:e="https://accounts.elixpo.com",clientId:r="lixblogs-cli-prod",audience:o="blogs.elixpo.com",cliVersion:i="1.2.0",fetchImpl:n=globalThis.fetch,timeoutMs:a=Nt}={}){if(super(),typeof n!="function")throw new TypeError("A fetch implementation is required.");this.accountsBaseUrl=Mt(e),this.clientId=r,this.audience=o,this.cliVersion=i,this.fetchImpl=n,this.timeoutMs=a,this._metadata=null,this._discoveryPromise=null}get providerId(){return"elixpo"}async _fetch(e,r={}){let o=new AbortController,i=setTimeout(()=>o.abort(),this.timeoutMs);try{return await this.fetchImpl(e,{...r,signal:r.signal||o.signal,headers:{accept:"application/json",...r.headers}})}catch{throw new v("temporarily_unavailable")}finally{clearTimeout(i)}}async discover({scopes:e=[]}={}){if(!this._metadata){this._discoveryPromise||(this._discoveryPromise=this._loadDiscovery());try{this._metadata=await this._discoveryPromise}finally{this._discoveryPromise=null}}if(e.filter(o=>!this._metadata.scopes_supported.includes(o)).length)throw new v("invalid_scope");return this._metadata}async _loadDiscovery(){let e=await this._fetch(`${this.accountsBaseUrl}/.well-known/oauth-authorization-server`),r=await j(e);if(!e.ok)throw new v("temporarily_unavailable",{status:e.status});if(ae(r.elixpo_contract_version)[0]!==Ut)throw new I("Accounts uses an unsupported device-flow contract version.");if(!Bt(this.cliVersion,r.elixpo_min_compatible_cli_version))throw new I(`This CLI is too old for Accounts. Upgrade to version ${r.elixpo_min_compatible_cli_version} or newer.`);if(!Array.isArray(r.grant_types_supported)||!r.grant_types_supported.includes(Ae))throw new I("Accounts does not advertise OAuth device authorization.");let i=["device_authorization_endpoint","token_endpoint","revocation_endpoint"];for(let n of i){if(typeof r[n]!="string")throw new I(`Accounts discovery is missing ${n}.`);let a=new URL(r[n]),s=new URL(this.accountsBaseUrl);if(a.origin!==s.origin)throw new I(`Accounts discovery returned an untrusted ${n}.`)}return{...r,scopes_supported:Array.isArray(r.scopes_supported)?r.scopes_supported:[]}}async requestDeviceCode({scopes:e}){let r=await this.discover({scopes:e}),o=await this._fetch(r.device_authorization_endpoint,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({client_id:this.clientId,scope:e.join(" "),audience:this.audience})}),i=await j(o);if(!o.ok)throw G(i,o);if(typeof i.device_code!="string"||typeof i.user_code!="string"||typeof i.verification_uri!="string")throw new v("server_error",{status:o.status});return{deviceCode:i.device_code,userCode:i.user_code,verificationUri:i.verification_uri,verificationUriComplete:i.verification_uri_complete||i.verification_uri,expiresInSeconds:Number(i.expires_in)||600,pollIntervalSeconds:Number(i.interval)||5}}async pollDeviceCode({deviceCode:e}){let r=await this.discover(),o=new URLSearchParams({grant_type:Ae,device_code:e,client_id:this.clientId}),i=await this._fetch(r.token_endpoint,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:o}),n=await j(i);if(i.ok)return{status:"approved",token:$e(n,i)};if(n?.error==="authorization_pending")return{status:"pending"};if(n?.error==="slow_down"){let a=r.elixpo_device_flow_polling||{};return{status:"slow_down",pollIntervalIncreaseSeconds:Math.max(5,Number(a.slow_down_interval_seconds||10)-Number(a.interval_seconds||5))}}if(n?.error==="access_denied")return{status:"denied"};if(n?.error==="expired_token")return{status:"expired"};throw G(n,i)}async refresh({refreshToken:e,scopes:r}){let o=await this.discover({scopes:r||[]}),i=new URLSearchParams({grant_type:"refresh_token",refresh_token:e,client_id:this.clientId});r?.length&&i.set("scope",r.join(" "));let n=await this._fetch(o.token_endpoint,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:i}),a=await j(n);return $e(a,n)}async revoke({token:e}){let r=await this.discover(),o=await this._fetch(r.revocation_endpoint,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({token:e,client_id:this.clientId})});if(!o.ok){let i=await j(o);throw G(i,o)}}};var Re="elixpo",le=class extends Error{constructor(e){super(e),this.name="ProductionAuthGateError"}};function Pe({providerId:t,environment:e}){if(e==="production"&&t!==Re)throw new le(`Provider "${t}" is not approved for production. Only "${Re}" may be used in production.`)}function U(t){if(t.authProvider==="mock"&&t.environment==="production")throw new Error("The mock auth provider cannot run in production.");if(t.authProvider!=="mock"&&t.authProvider!=="elixpo")throw new Error(`Unknown auth provider "${t.authProvider}".`);let e=t.authProvider==="mock"?new z:new V({accountsBaseUrl:t.accountsBaseUrl,clientId:t.clientId,audience:t.audience,cliVersion:t.cliVersion||"1.2.0",fetchImpl:t.fetchImpl});return Pe({providerId:e.providerId,environment:t.environment}),e}var E=class extends Error{constructor(e){super(e),this.name="CredentialStoreUnavailableError"}},A=class{async get(e){throw new Error("CredentialStore.get must be implemented by subclass")}async set(e,r){throw new Error("CredentialStore.set must be implemented by subclass")}async delete(e){throw new Error("CredentialStore.delete must be implemented by subclass")}async listProfiles(){throw new Error("CredentialStore.listProfiles must be implemented by subclass")}},J=class extends A{constructor(){super(),this._store=new Map}async get(e){return this._store.get(e)??null}async set(e,r){this._store.set(e,r)}async delete(e){this._store.delete(e)}async listProfiles(){return[...this._store.keys()]}},H=class extends A{constructor(e){super(),this._realStore=e}async get(e){try{return await this._realStore.get(e)}catch(r){throw new E(`OS keychain is unavailable: ${r.message}. Re-run with an explicit fallback flag if you want to opt in to a less secure storage method.`)}}async set(e,r){try{await this._realStore.set(e,r)}catch(o){throw new E(`OS keychain is unavailable: ${o.message}. Re-run with an explicit fallback flag if you want to opt in to a less secure storage method.`)}}async delete(e){try{await this._realStore.delete(e)}catch(r){throw new E(`OS keychain is unavailable: ${r.message}`)}}async listProfiles(){try{return await this._realStore.listProfiles()}catch(e){throw new E(`OS keychain is unavailable: ${e.message}`)}}};import{Entry as Ft}from"@napi-rs/keyring";var zt="lixblogs-cli",Gt="__lixblogs_availability_probe__";function K(t){return new Ft(zt,t)}async function Oe(){let t=K(Gt);try{return t.setPassword("probe"),t.deletePassword(),{available:!0}}catch(e){return{available:!1,error:String(e.message??e).split(`
3
+ `)[0].trim()}}}var ce=class extends A{async get(e){let r=K(e),o;try{o=r.getPassword()}catch(i){if(qe(i))return null;throw i}return o==null?null:JSON.parse(o)}async set(e,r){K(e).setPassword(JSON.stringify(r))}async delete(e){let r=K(e);try{r.deletePassword()}catch(o){if(qe(o))return;throw o}}async listProfiles(){throw new Error("KeychainCredentialStore.listProfiles is not supported directly \u2014 use ProfileRegistry to track known profile IDs, then look up each one via get().")}};function qe(t){let e=String(t?.message??"");return/no such|not found|nosuchkeyring|nosuchitem/i.test(e)}var W=class extends A{constructor(e){super(),this._keychain=new ce,this._registry=e}async get(e){return this._keychain.get(e)}async set(e,r){await this._keychain.set(e,r),await this._registry.add(e)}async delete(e){await this._keychain.delete(e),await this._registry.remove(e)}async listProfiles(){return this._registry.list()}};import{promises as X}from"node:fs";import Te from"node:path";import Vt from"node:os";function Jt(){return Te.join(Vt.homedir(),".config","lixblogs","profiles.json")}var g=class{constructor(e=Jt()){this._path=e}async list(){return(await this._read()).profiles}async getActive(){let e=await this._read();return e.activeProfile&&e.profiles.includes(e.activeProfile)?e.activeProfile:e.profiles[0]||null}async setActive(e){x(e);let r=await this._read();if(!r.profiles.includes(e))throw new Error(`Profile "${e}" does not exist. Log in with it first.`);await this._write(r.profiles,e)}async _read(){try{let e=await X.readFile(this._path,"utf8"),r=JSON.parse(e);return{profiles:Array.isArray(r.profiles)?r.profiles.filter(i=>typeof i=="string"):[],activeProfile:typeof r.activeProfile=="string"?r.activeProfile:null}}catch(e){if(e.code==="ENOENT")return{profiles:[],activeProfile:null};throw e}}async add(e){x(e);let r=await this._read(),o=new Set(r.profiles);o.add(e),await this._write([...o],r.activeProfile||e)}async remove(e){let r=await this._read(),o=r.profiles.filter(n=>n!==e),i=r.activeProfile===e?o[0]||null:r.activeProfile;await this._write(o,i)}async _write(e,r=null){await X.mkdir(Te.dirname(this._path),{recursive:!0});let o=`${this._path}.${process.pid}.tmp`;await X.writeFile(o,JSON.stringify({activeProfile:r,profiles:e},null,2),{encoding:"utf8",mode:384}),await X.rename(o,this._path)}};function x(t){if(!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(t||""))throw new Error("Profile names must be 1-64 characters using letters, numbers, dot, dash, or underscore.");return t}async function Ce({allowInsecureFallback:t=!1,profileRegistry:e}={}){let r=await Oe();if(!r.available){if(!t)throw new E(`OS keychain is unavailable: ${r.error}. Re-run with an explicit fallback flag if you want to opt in to a less secure storage method.`);return process.stderr.write(`warning: OS keychain unavailable (${r.error}); using in-memory fallback because --allow-insecure-fallback was passed. Credentials will NOT persist between CLI runs.
4
+ `),new J}let o=e||new g,i=new W(o);return new H(i)}var de="[REDACTED]",Ht=/token|refresh|secret|password|authorization/i,Kt=/^(mock-(access|refresh)-|Bearer\s+)\S+/i;function Le(t){return typeof t=="string"&&Kt.test(t)?de:t}function ue(t){if(Array.isArray(t))return t.map(e=>ue(e));if(t&&typeof t=="object"){let e={};for(let[r,o]of Object.entries(t))Ht.test(r)?e[r]=de:typeof o=="object"&&o!==null?e[r]=ue(o):e[r]=Le(o);return e}return Le(t)}function Y(t,e){return JSON.stringify(ue(t),null,e)}function q(t){return typeof t!="string"?t:t.replace(/(mock-(access|refresh)-\S+|Bearer\s+\S+)/gi,de)}async function je({provider:t,credentialStore:e,profileId:r,scopes:o,openBrowser:i,resolveProfileId:n,sleep:a=l=>new Promise(u=>setTimeout(u,l)),onStatus:s=()=>{}}){let l;try{l=await t.requestDeviceCode({scopes:o})}catch(c){return{ok:!1,reason:q(c.message)}}s({type:"verification_pending",verificationUri:l.verificationUri,verificationUriComplete:l.verificationUriComplete,userCode:l.userCode,expiresInSeconds:l.expiresInSeconds}),i&&await i(l.verificationUriComplete||l.verificationUri);let u=l.pollIntervalSeconds*1e3,y=Date.now()+l.expiresInSeconds*1e3;for(;Date.now()<y;){await a(u);let c;try{c=await t.pollDeviceCode({deviceCode:l.deviceCode})}catch(h){return{ok:!1,reason:q(h.message)}}if(c.status==="approved"){let h=r;if(n)try{h=await n({accessToken:c.token.accessToken,requestedProfileId:r})}catch(L){return{ok:!1,reason:q(L.message)}}return await e.set(h,{accessToken:c.token.accessToken,refreshToken:c.token.refreshToken,expiresAt:Date.now()+c.token.expiresInSeconds*1e3,scopes:c.token.scopes}),s({type:"approved"}),{ok:!0,profileId:h}}if(c.status==="denied")return s({type:"denied"}),{ok:!1,reason:"Login was denied."};if(c.status==="expired")return s({type:"expired"}),{ok:!1,reason:"Device code expired before login was approved."};if(c.status==="slow_down"){u+=c.pollIntervalIncreaseSeconds*1e3,s({type:"slow_down",newIntervalMs:u});continue}s({type:"pending"})}return{ok:!1,reason:"Device code expired before login was approved."}}async function Ue({credentialStore:t,profileId:e}){let r=e?[e]:await t.listProfiles(),o=[];for(let i of r){let n=await t.get(i);if(!n){o.push({profileId:i,loggedIn:!1});continue}o.push({profileId:i,loggedIn:!0,expired:Date.now()>=n.expiresAt,scopes:n.scopes})}return o}async function Ne({credentialStore:t,profileId:e}){return await t.delete(e),{ok:!0}}async function De({provider:t,credentialStore:e,profileId:r,confirmed:o}){if(o!==!0)return{ok:!1,reason:"Revoke was not confirmed. This is a destructive action and requires explicit confirmation (interactive prompt, or --yes in a non-interactive session)."};let i=await e.get(r);return i?(await t.revoke({token:i.refreshToken}),await e.delete(r),{ok:!0}):{ok:!1,reason:`No stored credentials for profile "${r}".`}}async function Be({credentialStore:t,profileRegistry:e}){let r=await e.getActive(),o=await t.listProfiles(),i=[];for(let n of o){let a=await t.get(n);i.push({profileId:n,active:n===r,loggedIn:!!a,expired:a?Date.now()>=a.expiresAt:void 0,scopes:a?.scopes||[]})}return{activeProfile:r,profiles:i}}async function Me({credentialStore:t,profileRegistry:e,profileId:r}){return await t.get(r)?(await e.setActive(r),{ok:!0,profileId:r}):{ok:!1,reason:`Profile "${r}" is not logged in.`}}async function Fe({accessToken:t,apiBaseUrl:e,fetchImpl:r=globalThis.fetch}){let o=new URL("/api/v1/me",e),i=await r(o,{headers:{accept:"application/json",authorization:`Bearer ${t}`}}),n;try{n=await i.json()}catch{throw new Error("LixBlogs could not resolve the signed-in username.")}if(!i.ok||typeof n?.data?.username!="string")throw new Error(n?.error?.message||"LixBlogs could not resolve the signed-in username.");return x(n.data.username)}var Wt=6e4,ze=new WeakMap;function Xt(t){let e=ze.get(t);return e||(e=new Map,ze.set(t,e)),e}var Z=class extends Error{constructor(e){super(`Profile "${e}" needs to log in again.`),this.name="LoginRequiredError",this.code="login_required"}},pe=class extends Error{constructor(e,r){super("The configured LixBlogs origin is not serving the API v1 JSON contract."),this.name="ApiContractUnavailableError",this.code="api_contract_unavailable",this.status=e,this.details={contentType:r||"unknown"},this.hint="Deploy the LixBlogs API v1 stack, or select an origin that exposes /api/v1."}},N=class{constructor({provider:e,credentialStore:r,profileId:o,apiBaseUrl:i="https://blogs.elixpo.com",fetchImpl:n=globalThis.fetch,refreshSkewMs:a=Wt}){this.provider=e,this.credentialStore=r,this.profileId=o,this.apiBaseUrl=new URL(i),this.fetchImpl=n,this.refreshSkewMs=a}async _refresh(e,{force:r=!1}={}){let o=Xt(this.credentialStore),i=o.get(this.profileId);if(i)return i;let n=(async()=>{let a=await this.credentialStore.get(this.profileId)||e;if(!r&&a.expiresAt-Date.now()>this.refreshSkewMs)return a;try{let s=await this.provider.refresh({refreshToken:a.refreshToken,scopes:a.scopes}),l={accessToken:s.accessToken,refreshToken:s.refreshToken,expiresAt:Date.now()+s.expiresInSeconds*1e3,scopes:s.scopes};return await this.credentialStore.set(this.profileId,l),l}catch(s){throw s instanceof v&&s.requiresLogin?(await this.credentialStore.delete(this.profileId),new Z(this.profileId)):s}})();o.set(this.profileId,n);try{return await n}finally{o.get(this.profileId)===n&&o.delete(this.profileId)}}async credentials({forceRefresh:e=!1}={}){let r=await this.credentialStore.get(this.profileId);if(!r)throw new Z(this.profileId);return e||r.expiresAt-Date.now()<=this.refreshSkewMs?this._refresh(r,{force:e}):r}async request(e,r={}){let o=new URL(e,this.apiBaseUrl);if(o.origin!==this.apiBaseUrl.origin||!o.pathname.startsWith("/api/v1/"))throw new Error("Authenticated CLI requests are restricted to the configured LixBlogs /api/v1 resource server.");let i=await this.credentials(),n=()=>this.fetchImpl(o.toString(),{...r,headers:{...r.headers,authorization:`Bearer ${i.accessToken}`}}),a=await n();a.status===401&&(i=await this.credentials({forceRefresh:!0}),a=await n());let s=a.headers.get("content-type")||"";if(!s.toLowerCase().includes("application/json"))throw new pe(a.status,s);return a}async requireScopes(e){let r=await this.credentials(),o=e.filter(i=>!r.scopes.includes(i));if(o.length){let i=new Error(`Login again with the required scope${o.length>1?"s":""}: ${o.join(", ")}`);throw i.name="InsufficientScopeError",i.code="insufficient_scope",i.missingScopes=o,i}}};import{randomUUID as Ge}from"node:crypto";var d=class extends Error{constructor(e,r,{status:o,requestId:i,details:n}={}){super(r),this.name="BlogApiError",this.code=e||"api_error",this.status=o||0,this.requestId=i||null,this.details=n||null}};async function Yt(t){let e;try{e=await t.json()}catch{e=null}if(!t.ok||e?.error)throw new d(e?.error?.code||`http_${t.status}`,e?.error?.message||`LixBlogs returned HTTP ${t.status}.`,{status:t.status,requestId:e?.error?.requestId||t.headers.get("x-request-id"),details:e?.error?.details});return{payload:e,etag:t.headers.get("etag")}}var D=class{constructor(e,{sleep:r=o=>new Promise(i=>setTimeout(i,o))}={}){this.http=e,this.sleep=r}async request(e,r={}){let o={...r,headers:{accept:"application/json",...r.body?{"content-type":"application/json"}:{},...r.headers}},n=(o.method||"GET")==="GET"||!!o.headers["idempotency-key"];for(let a=0;a<2;a+=1)try{let s=await this.http.request(e,o);if(n&&a===0&&(s.status===429||s.status>=500)){let l=Math.min(2,Number.parseInt(s.headers.get("retry-after")||"1",10)||1);await this.sleep(l*1e3);continue}return Yt(s)}catch(s){if(!n||a>0||s instanceof d||s?.code)throw s;await this.sleep(250)}throw new d("request_failed","The LixBlogs request failed after retrying.")}async requireScopes(e){typeof this.http.requireScopes=="function"&&await this.http.requireScopes(e)}async whoami(){return await this.requireScopes(["lixblogs:profile:read"]),(await this.request("/api/v1/me")).payload.data}async list({status:e="all",limit:r=20,cursor:o}={}){await this.requireScopes(["lixblogs:blog:read"]);let i=new URLSearchParams({status:e,limit:String(r)});return o&&i.set("cursor",o),(await this.request(`/api/v1/blogs?${i}`)).payload}async get(e){await this.requireScopes(["lixblogs:blog:read"]);let r=await this.request(`/api/v1/blogs/${encodeURIComponent(e)}`);return{...r.payload.data,etag:r.etag||r.payload.data.etag}}async create(e,{idempotencyKey:r=Ge()}={}){return await this.requireScopes(["lixblogs:blog:write"]),(await this.request("/api/v1/blogs",{method:"POST",headers:{"idempotency-key":r},body:JSON.stringify(e)})).payload.data}async update(e,r,{etag:o}){return await this.requireScopes(["lixblogs:blog:write"]),(await this.request(`/api/v1/blogs/${encodeURIComponent(e)}`,{method:"PATCH",headers:{"if-match":o},body:JSON.stringify(r)})).payload.data}async publish(e,{etag:r,idempotencyKey:o=Ge()}){return await this.requireScopes(["lixblogs:blog:publish"]),(await this.request(`/api/v1/blogs/${encodeURIComponent(e)}/publish`,{method:"POST",headers:{"if-match":r,"idempotency-key":o}})).payload.data}async unpublish(e,{etag:r}){return await this.requireScopes(["lixblogs:blog:publish"]),(await this.request(`/api/v1/blogs/${encodeURIComponent(e)}/unpublish`,{method:"POST",headers:{"if-match":r}})).payload.data}async delete(e,{etag:r,permanent:o=!1}){return await this.requireScopes(["lixblogs:blog:delete",...o?["lixblogs:blog:delete:permanent"]:[]]),(await this.request(`/api/v1/blogs/${encodeURIComponent(e)}${o?"?permanent=true":""}`,{method:"DELETE",headers:{"if-match":r,...o?{"x-confirm-permanent-delete":e}:{}}})).payload.data}async restore(e,{etag:r}){return await this.requireScopes(["lixblogs:blog:delete"]),(await this.request(`/api/v1/blogs/${encodeURIComponent(e)}/restore`,{method:"POST",headers:{"if-match":r}})).payload.data}};async function Zt(t){let e;try{e=await t.json()}catch{e=null}if(!t.ok||e?.error)throw new d(e?.error?.code||`http_${t.status}`,e?.error?.message||`LixBlogs returned HTTP ${t.status}.`,{status:t.status,requestId:e?.error?.requestId||t.headers.get("x-request-id"),details:e?.error?.details});return{payload:e,etag:t.headers.get("etag")}}var Q=class{constructor(e,{sleep:r=o=>new Promise(i=>setTimeout(i,o))}={}){this.http=e,this.sleep=r}async request(e,r={}){let o={...r,headers:{accept:"application/json",...r.body?{"content-type":"application/json"}:{},...r.headers}},n=(o.method||"GET")==="GET";for(let a=0;a<2;a+=1)try{let s=await this.http.request(e,o);if(n&&a===0&&(s.status===429||s.status>=500)){let l=Math.min(2,Number.parseInt(s.headers.get("retry-after")||"1",10)||1);await this.sleep(l*1e3);continue}return Zt(s)}catch(s){if(!n||a>0||s instanceof d||s?.code)throw s;await this.sleep(250)}throw new d("request_failed","The LixBlogs request failed after retrying.")}async requireScopes(e){typeof this.http.requireScopes=="function"&&await this.http.requireScopes(e)}async list(){return await this.requireScopes(["lixblogs:organizations:read"]),(await this.request("/api/v1/orgs")).payload}async get(e){if(!e)throw new Error("An organization ID or handle is required.");return await this.requireScopes(["lixblogs:organizations:read"]),(await this.request(`/api/v1/orgs/${encodeURIComponent(e)}`)).payload.data}async collections(e){if(!e)throw new Error("An organization ID or handle is required.");return await this.requireScopes(["lixblogs:organizations:read"]),(await this.request(`/api/v1/orgs/${encodeURIComponent(e)}/collections`)).payload.data}async members(e){if(!e)throw new Error("An organization ID or handle is required.");return await this.requireScopes(["lixblogs:organizations:read"]),(await this.request(`/api/v1/orgs/${encodeURIComponent(e)}/members`)).payload.data}async targets(){await this.requireScopes(["lixblogs:organizations:read"]);let o=((await this.list())?.data||[]).filter(n=>n.canWrite),i=await Promise.all(o.map(async n=>{let a=[];try{a=await this.collections(n.id)}catch{a=[]}return{target:`org:${n.id}`,orgId:n.id,slug:n.slug,name:n.name,role:n.role,collections:a.map(s=>({id:s.id,slug:s.slug,name:s.name}))}}));return{personal:{target:"personal",name:"Personal Blog"},organizations:i}}};import{randomUUID as ee}from"node:crypto";async function Qt(t){let e;try{e=await t.json()}catch{e=null}if(!t.ok||e?.error)throw new d(e?.error?.code||`http_${t.status}`,e?.error?.message||`LixBlogs returned HTTP ${t.status}.`,{status:t.status,requestId:e?.error?.requestId||t.headers.get("x-request-id"),details:e?.error?.details});return e.data}var te=class{constructor(e){this.http=e}async request(e,r={}){let o=await this.http.request(e,{...r,headers:{accept:"application/json",...r.body?{"content-type":"application/json"}:{},...r.headers}});return Qt(o)}async list(e){return await this.http.requireScopes(["lixblogs:collaboration:read"]),this.request(`/api/v1/blogs/${encodeURIComponent(e)}/collaborators`)}async invitations(){return await this.http.requireScopes(["lixblogs:collaboration:read"]),this.request("/api/v1/collaboration/invitations")}async invite(e,{user:r,role:o,idempotencyKey:i=ee()}){return await this.http.requireScopes(["lixblogs:collaboration:write"]),this.request(`/api/v1/blogs/${encodeURIComponent(e)}/collaborators`,{method:"POST",headers:{"idempotency-key":i},body:JSON.stringify({user:r,role:o})})}async role(e,{user:r,role:o,idempotencyKey:i=ee()}){return await this.http.requireScopes(["lixblogs:collaboration:write"]),this.request(`/api/v1/blogs/${encodeURIComponent(e)}/collaborators`,{method:"PATCH",headers:{"idempotency-key":i},body:JSON.stringify({user:r,role:o})})}async remove(e,{user:r,idempotencyKey:o=ee()}={}){return await this.http.requireScopes(["lixblogs:collaboration:write"]),this.request(`/api/v1/blogs/${encodeURIComponent(e)}/collaborators`,{method:"DELETE",headers:{"idempotency-key":o},body:JSON.stringify({...r?{user:r}:{}})})}async resolveInvitation(e,{action:r,showOnProfile:o=!0,idempotencyKey:i=ee()}){return await this.http.requireScopes(["lixblogs:collaboration:write"]),this.request("/api/v1/collaboration/invitations",{method:"POST",headers:{"idempotency-key":i},body:JSON.stringify({blogId:e,action:r,showOnProfile:o})})}};async function er(t){let e;try{e=await t.json()}catch{e=null}if(!t.ok||e?.error)throw new d(e?.error?.code||`http_${t.status}`,e?.error?.message||`LixBlogs returned HTTP ${t.status}.`,{status:t.status,requestId:e?.error?.requestId||t.headers.get("x-request-id"),details:e?.error?.details});return e}var re=class{constructor(e){this.http=e}async query(e={}){let r=e.scope||"personal";await this.http.requireScopes(["lixblogs:analytics:read",...r.startsWith("org:")?["lixblogs:organizations:read"]:[]]);let o=new URLSearchParams({scope:r,range:e.range||(e.from||e.to?"custom":"30d"),dimension:e.dimension||"overview",limit:String(e.limit||20)});return e.from&&o.set("from",e.from),e.to&&o.set("to",e.to),e.cursor&&o.set("cursor",e.cursor),er(await this.http.request(`/api/v1/analytics?${o}`,{headers:{accept:"application/json"}}))}};var p=Object.freeze({OK:0,ERROR:1,USAGE:2,CONFLICT:3,AUTH:4,CONFIRMATION:5}),tr=Object.freeze({login:["auth","login"],logout:["auth","logout"],whoami:["auth","whoami"],profiles:["auth","profiles"],use:["auth","use"]});function Ve(t){let[e,...r]=t,o=tr[e];return o?[...o,...r]:t}function Je(t,e="cli_error"){if(t&&typeof t=="object"&&t.error&&!Array.isArray(t.error))return t;let r=t&&typeof t=="object"?t:{message:String(t||"Command failed.")};return{ok:!1,error:{code:r.code||e,message:r.message||"Command failed.",hint:r.hint||null,requestId:r.requestId||null,...r.details?{details:r.details}:{}}}}function w(t,e){if(t.yes)return;let r=new Error(`${e} requires --yes in non-interactive operation.`);throw r.code="confirmation_required",r.hint="Review the operation, then run it again with --yes.",r.exitCode=p.CONFIRMATION,r}var k=Object.freeze({reset:"\x1B[0m",bold:"\x1B[1m",dim:"\x1B[2m",violet:"\x1B[38;5;141m",green:"\x1B[38;5;42m"});function fe(t=process.stdout,e=process.env){return!!t.isTTY&&e.NO_COLOR===void 0&&e.TERM!=="dumb"}function $(t,e,r){return r?`${e}${t}${k.reset}`:t}function He({url:t,code:e,expiresInSeconds:r,profile:o,interactive:i,color:n=!1}){let a=`${$("\u25C6",k.violet,n)} ${$("LixBlogs",k.bold,n)}`,s=i?"Press Enter to open here, or use the URL on another device.":"Open the URL in any browser and approve this device.";return["",` ${a}`,` ${$("Device login",k.dim,n)}`," \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",` URL ${t}`,` Code ${$(e,k.bold,n)}`,` Expires ${Math.ceil(r/60)} min`,o?` Profile ${o} ${$("(local credential slot)",k.dim,n)}`:` Profile ${$("your Accounts username after approval",k.dim,n)}`,"",` ${s}`," No localhost callback or exposed port is required.",""].join(`
5
+ `)}function Ke(t,e=!1){return` ${$("\u2713",k.green,e)} ${t}`}function We({input:t=process.stdin,open:e,url:r}){if(!t.isTTY||typeof e!="function")return()=>{};let o=()=>{Promise.resolve(e(r)).catch(()=>{})};return t.setEncoding?.("utf8"),t.once("data",o),t.resume?.(),()=>{t.off?.("data",o),t.pause?.()}}function O(t){return[{type:"text",text:t}]}function Xe(t){let e=String(t||"").replace(/\r\n/g,`
6
+ `).split(`
7
+ `),r=[],o=[],i=()=>{o.length&&(r.push({type:"paragraph",content:O(o.join(" ").trim())}),o=[])};for(let n=0;n<e.length;n+=1){let s=e[n].trim();if(!s){i();continue}let l=s.match(/^```([\w+-]*)/);if(l){i();let se=[];for(n+=1;n<e.length&&!/^```/.test(e[n].trim());)se.push(e[n++]);r.push(l[1].toLowerCase()==="mermaid"?{type:"mermaidBlock",props:{diagram:se.join(`
8
+ `)}}:{type:"codeBlock",props:{language:l[1].toLowerCase()},content:O(se.join(`
9
+ `))});continue}let u=s.match(/^(#{1,3})\s+(.+)/);if(u){i(),r.push({type:"heading",props:{level:String(u[1].length)},content:O(u[2])});continue}let y=s.match(/^[-*]\s+(.+)/);if(y){i(),r.push({type:"bulletListItem",content:O(y[1])});continue}let c=s.match(/^\d+\.\s+(.+)/);if(c){i(),r.push({type:"numberedListItem",content:O(c[1])});continue}let h=s.match(/^>\s?(.*)/);if(h){i(),r.push({type:"quote",content:O(h[1])});continue}let L=s.match(/^!\[([^\]]*)\]\((https:\/\/[^)]+)\)$/);if(L){i(),r.push({type:"image",props:{url:L[2],caption:L[1]}});continue}if(/^([-*_])\1{2,}$/.test(s)){i(),r.push({type:"divider"});continue}o.push(s)}return i(),r}function rr(t){return(t?.content||[]).map(e=>typeof e=="string"?e:e?.text||"").join("")}function oe(t){return(t||[]).map(e=>{let r=rr(e);return e.type==="heading"?`${"#".repeat(Number(e.props?.level)||1)} ${r}`:e.type==="bulletListItem"?`- ${r}`:e.type==="numberedListItem"?`1. ${r}`:e.type==="quote"?`> ${r}`:e.type==="codeBlock"?`\`\`\`${e.props?.language||""}
10
+ ${r}
11
+ \`\`\``:e.type==="mermaidBlock"?`\`\`\`mermaid
12
+ ${e.props?.diagram||""}
13
+ \`\`\``:e.type==="image"?`![${e.props?.caption||""}](${e.props?.url||""})`:e.type==="divider"?"---":r}).join(`
14
+
15
+ `)}import{promises as B}from"node:fs";import{tmpdir as or}from"node:os";import he from"node:path";import{spawn as ir}from"node:child_process";async function nr(t){let e="";t.setEncoding("utf8");for await(let r of t)e+=r;return e}async function sr(t="",e=process.env.EDITOR||process.env.VISUAL){if(!e)throw new Error("$EDITOR or $VISUAL must be set when using --editor.");let r=await B.mkdtemp(he.join(or(),"lixblogs-")),o=he.join(r,"post.md");await B.writeFile(o,t,{mode:384});try{return await new Promise((i,n)=>{let a=ir(e,[o],{stdio:"inherit",shell:!0});a.once("error",n),a.once("exit",s=>s===0?i():n(new Error(`Editor exited with code ${s}.`)))}),await B.readFile(o,"utf8")}finally{await B.rm(r,{recursive:!0,force:!0})}}async function me(t,{stdin:e=process.stdin,initial:r=""}={}){let o=[t.file!==void 0,t.stdin,t.content!==void 0,t.editor].filter(Boolean).length;if(o>1)throw new Error("Use only one of --file, --stdin, --content, or --editor.");if(!o)return null;let i;return t.file!==void 0?i=await B.readFile(he.resolve(t.file),"utf8"):t.stdin?i=await nr(e):t.content!==void 0?i=t.content:i=await sr(r),{markdown:i,blocks:Xe(i)}}function ge(t){let e={},r={title:"title",subtitle:"subtitle",slug:"slug",emoji:"emoji",publication:"publishedAs",collection:"collectionId",cover:"coverUrl"};for(let[o,i]of Object.entries(r))t[o]!==void 0&&(e[i]=t[o]);return t.tag!==void 0&&(e.tags=t.tag),t["member-only"]&&(e.memberOnly=!0),t["no-member-only"]&&(e.memberOnly=!1),t.secret&&(e.secret=!0),t["not-secret"]&&(e.secret=!1),e}function ar(t){let e=[],r=o=>{for(let i of o||[]){for(let n of i?.content||[]){let a=typeof n=="string"?n:n?.text||"";e.push(...a.trim().split(/\s+/).filter(Boolean))}i?.children&&r(i.children)}};return r(t),e.length}function ie(t,{publishing:e=!1}={}){if(t.title!==void 0&&(typeof t.title!="string"||t.title.length>300))throw new Error("Title must be 300 characters or fewer.");if(t.subtitle!==void 0&&(typeof t.subtitle!="string"||t.subtitle.length>500))throw new Error("Subtitle must be 500 characters or fewer.");if(t.tags!==void 0&&(!Array.isArray(t.tags)||t.tags.length>5))throw new Error("Use at most five tags.");if(t.coverUrl&&!/^https:\/\//i.test(t.coverUrl))throw new Error("Cover URLs must use HTTPS.");if(t.publishedAs&&t.publishedAs!=="personal"&&!/^org:[^:]+$/.test(t.publishedAs))throw new Error("Publication must be personal or org:<id>.");if(t.content!==void 0){if(!Array.isArray(t.content))throw new Error("Blog content must be a block array.");if(Buffer.byteLength(JSON.stringify(t.content),"utf8")>15e5)throw new Error("Blog content exceeds the 1.5 MB limit.")}if(e){if(!t.title?.trim())throw new Error("A title is required before publishing.");if(ar(t.content)<20)throw new Error("A post needs at least 20 words before publishing.")}return t}import{promises as we}from"node:fs";import ye from"node:path";async function Ye({client:t,options:e}){return t.list({status:e.status,limit:e.limit,cursor:e.cursor})}async function be({client:t,id:e}){if(!e)throw new Error("A blog ID is required.");let r=await t.get(e);return{...r,markdown:oe(r.content)}}async function Ze({client:t,options:e,stdin:r}){let o=await me(e,{stdin:r}),i={...ge(e),content:o?.blocks||[]};return ie(i),e["dry-run"]?{dryRun:!0,input:i,markdown:o?.markdown||""}:t.create(i,{idempotencyKey:e["idempotency-key"]})}async function Qe({client:t,id:e,options:r,stdin:o}){if(!e)throw new Error("A blog ID is required.");let i=await t.get(e),n=await me(r,{stdin:o,initial:oe(i.content)}),a={...ge(r),...n?{content:n.blocks}:{}};if(!Object.keys(a).length)throw new Error("No blog changes were provided.");if(ie(a),r["dry-run"])return{dryRun:!0,id:e,etag:i.etag,input:a,markdown:n?.markdown};try{return await t.update(e,a,{etag:r.etag||i.etag})}catch(s){if(!(s instanceof d)||s.code!=="revision_conflict")throw s;let l=await t.get(e),u=r.conflictDirectory||ye.resolve(".lixblogs-conflicts");await we.mkdir(u,{recursive:!0});let y=e.replace(/[^A-Za-z0-9._-]/g,"_"),c=ye.join(u,`${y}-local.json`),h=ye.join(u,`${y}-server.md`);throw await Promise.all([we.writeFile(c,JSON.stringify(a,null,2),{mode:384}),we.writeFile(h,oe(l.content),{mode:384})]),s.details={...s.details,localPath:c,serverPath:h,serverEtag:l.etag},s}}async function et({client:t,id:e,options:r}){if(!e)throw new Error("A blog ID is required.");let o=await t.get(e);return ie(o,{publishing:!0}),r["dry-run"]?{dryRun:!0,id:e,from:o.status,to:"published"}:(w(r,"Publishing this blog"),t.publish(e,{etag:r.etag||o.etag,idempotencyKey:r["idempotency-key"]}))}async function tt({client:t,id:e,options:r}){if(!e)throw new Error("A blog ID is required.");let o=await t.get(e);return r["dry-run"]?{dryRun:!0,id:e,from:o.status,to:"draft"}:(w(r,"Unpublishing this blog"),t.unpublish(e,{etag:r.etag||o.etag}))}async function ve({client:t,id:e,options:r}){if(!e)throw new Error("A blog ID is required.");if(!r.yes)throw new Error("Deletion requires --yes. Trash is the default; add --permanent for irreversible deletion.");let o=await t.get(e);return r["dry-run"]?{dryRun:!0,id:e,permanent:r.permanent}:t.delete(e,{etag:r.etag||o.etag,permanent:r.permanent})}async function rt({client:t,id:e,options:r}){if(!e)throw new Error("A blog ID is required.");let o=await t.get(e);return r["dry-run"]?{dryRun:!0,id:e,restoreTo:o.preDeleteStatus||"draft"}:(w(r,"Restoring this blog"),t.restore(e,{etag:r.etag||o.etag}))}async function ot({client:t}){return t.list()}async function it({client:t,id:e}){if(!e)throw new Error("An organization ID or handle is required.");return t.get(e)}async function nt({client:t,id:e}){if(!e)throw new Error("An organization ID or handle is required.");return t.collections(e)}async function st({client:t,id:e}){if(!e)throw new Error("An organization ID or handle is required.");return t.members(e)}async function at({client:t}){return t.targets()}function T(t){if(!t)throw new Error("A blog ID is required.")}async function lt({client:t,id:e}){return T(e),t.list(e)}async function ct({client:t}){return t.invitations()}async function ut({client:t,id:e,options:r}){if(T(e),!r.user)throw new Error("--user is required.");if(!["viewer","editor","admin"].includes(r.role))throw new Error("--role must be viewer, editor, or admin.");return r["dry-run"]?{dryRun:!0,action:"invite",blogId:e,user:r.user,role:r.role}:(w(r,"Inviting this collaborator"),t.invite(e,{user:r.user,role:r.role,idempotencyKey:r["idempotency-key"]}))}async function dt({client:t,id:e,options:r}){if(T(e),!r.user)throw new Error("--user is required.");if(!["viewer","editor","admin"].includes(r.role))throw new Error("--role must be viewer, editor, or admin.");return r["dry-run"]?{dryRun:!0,action:"role",blogId:e,user:r.user,role:r.role}:(w(r,"Changing this collaborator role"),t.role(e,{user:r.user,role:r.role,idempotencyKey:r["idempotency-key"]}))}async function pt({client:t,id:e,options:r}){return T(e),r["dry-run"]?{dryRun:!0,action:"remove",blogId:e,user:r.user||"self"}:(w(r,"Removing this collaborator or invitation"),t.remove(e,{user:r.user,idempotencyKey:r["idempotency-key"]}))}async function ft({client:t,id:e,options:r}){return T(e),r["dry-run"]?{dryRun:!0,action:"accept",blogId:e,showOnProfile:!r["hide-on-profile"]}:(w(r,"Accepting this collaboration invitation"),t.resolveInvitation(e,{action:"accept",showOnProfile:!r["hide-on-profile"],idempotencyKey:r["idempotency-key"]}))}async function ht({client:t,id:e,options:r}){return T(e),r["dry-run"]?{dryRun:!0,action:"decline",blogId:e}:(w(r,"Declining this collaboration invitation"),t.resolveInvitation(e,{action:"decline",idempotencyKey:r["idempotency-key"]}))}import{access as lr,cp as cr,readFile as ur,readdir as dr}from"node:fs/promises";import b from"node:path";import{fileURLToPath as pr}from"node:url";var xe=b.dirname(pr(import.meta.url)),wt=b.basename(xe)==="dist"?b.resolve(xe,".."):b.resolve(xe,"../../.."),mt=b.join(wt,"skills"),gt=b.resolve(wt,"../..",".agents","skills");async function C(t){try{return await lr(t),!0}catch{return!1}}async function _e(){if(await C(mt))return mt;if(await C(gt))return gt;let t=new Error("No bundled LixBlogs skills were found. Reinstall @elixpo/lixblogs-cli.");throw t.code="skills_unavailable",t}function yt(t){if(!/^lixblogs-[a-z0-9-]+$/.test(t||"")){let e=new Error("A valid lixblogs-* skill name is required.");throw e.code="invalid_skill_name",e}return t}async function bt(t,e){let r=await ur(b.join(t,e,"SKILL.md"),"utf8"),o=r.match(/^description:\s*(.+)$/m)?.[1]||r.match(/^description:\s*>-\s*\n\s*(.+)$/m)?.[1]||"",i=r.match(/`@elixpo\/lixblogs-cli`\s+([0-9.]+)/)?.[1]||null;return{name:e,description:o.trim(),minimumCliVersion:i,content:r}}async function vt(){let t=await _e(),e=await dr(t,{withFileTypes:!0});return Promise.all(e.filter(r=>r.isDirectory()&&r.name.startsWith("lixblogs-")).map(r=>bt(t,r.name))).then(r=>r.map(({content:o,...i})=>i).sort((o,i)=>o.name.localeCompare(i.name)))}async function xt({name:t}){let e=await _e(),r=yt(t);if(!await C(b.join(e,r,"SKILL.md"))){let o=new Error(`Skill "${r}" is not bundled.`);throw o.code="skill_not_found",o}return bt(e,r)}async function _t({name:t,options:e}){let r=await _e(),o=yt(t),i=b.join(r,o);if(!await C(b.join(i,"SKILL.md"))){let s=new Error(`Skill "${o}" is not bundled.`);throw s.code="skill_not_found",s}let n=b.resolve(e.target||".agents/skills"),a=b.join(n,o);if(e["dry-run"])return{dryRun:!0,name:o,target:a,replace:await C(a)};if(await C(a)){if(!e.force){let s=new Error(`Skill already exists at ${a}.`);throw s.code="skill_exists",s.hint="Inspect the existing skill or re-run with --force --yes to replace it.",s}w(e,`Replacing ${a}`)}else w(e,`Installing ${o} into ${n}`);return await cr(i,a,{recursive:!0,force:!!e.force}),{installed:!0,name:o,target:a}}import{writeFile as fr}from"node:fs/promises";var hr=new Set(["overview","timeline","posts","sources","devices","countries"]),mr=new Set(["7d","30d","90d","12m","custom"]);function kt(t={}){let e=t.dimension||"overview",r=t.range||(t.from||t.to?"custom":"30d");if(!hr.has(e))throw new Error(`Unsupported analytics dimension: ${e}.`);if(!mr.has(r))throw new Error(`Unsupported analytics range: ${r}.`);if(r==="custom"&&(!t.from||!t.to))throw new Error("Custom analytics ranges require --from and --to.");return{scope:t.scope?.[0]||t.publication||"personal",range:r,from:t.from,to:t.to,dimension:e,limit:t.limit,cursor:t.cursor}}async function St({client:t,options:e}){return t.query(kt(e))}function It(t){let e=t==null?"":typeof t=="object"?JSON.stringify(t):String(t);return/[",\n]/.test(e)?`"${e.replaceAll('"','""')}"`:e}function Et(t){let e=t?.data?.values;return Array.isArray(e)?e:e?.labels&&Array.isArray(e.labels)?e.labels.map((r,o)=>({label:r,views:e.views?.[o]||0,reads:e.reads?.[o]||0})):e?.totals?Object.entries(e.totals).map(([r,o])=>({metric:r,value:o,previous:e.previous?.[r],change:e.changes?.[r]})):[]}async function At({client:t,options:e}){if(!e.output)throw new Error("Analytics export requires --output <file>.");let r=e.format||"json";if(!["json","csv"].includes(r))throw new Error("Analytics export format must be json or csv.");let o=await t.query(kt(e)),i;if(r==="json")i=`${JSON.stringify(o,null,2)}
16
+ `;else{let n=Et(o),a=[...new Set(n.flatMap(s=>Object.keys(s)))];i=`${a.map(It).join(",")}
17
+ ${n.map(s=>a.map(l=>It(s[l])).join(",")).join(`
18
+ `)}
19
+ `}return await fr(e.output,i,{encoding:"utf8",flag:"wx"}),{output:e.output,format:r,rows:Et(o).length}}var yr={profile:{type:"string"},env:{type:"string"},json:{type:"boolean",default:!1},quiet:{type:"boolean",default:!1},yes:{type:"boolean",short:"y",default:!1},"allow-insecure-fallback":{type:"boolean",default:!1},"auth-provider":{type:"string"},"accounts-url":{type:"string"},"api-url":{type:"string"},"client-id":{type:"string"},audience:{type:"string"},scope:{type:"string",multiple:!0},open:{type:"boolean",default:!1},status:{type:"string"},limit:{type:"string"},cursor:{type:"string"},range:{type:"string"},from:{type:"string"},to:{type:"string"},dimension:{type:"string"},format:{type:"string"},output:{type:"string"},file:{type:"string"},stdin:{type:"boolean",default:!1},content:{type:"string"},editor:{type:"boolean",default:!1},title:{type:"string"},subtitle:{type:"string"},slug:{type:"string"},tag:{type:"string",multiple:!0},emoji:{type:"string"},publication:{type:"string"},collection:{type:"string"},cover:{type:"string"},"member-only":{type:"boolean",default:!1},"no-member-only":{type:"boolean",default:!1},secret:{type:"boolean",default:!1},"not-secret":{type:"boolean",default:!1},"dry-run":{type:"boolean",default:!1},"no-input":{type:"boolean",default:!1},etag:{type:"string"},permanent:{type:"boolean",default:!1},"idempotency-key":{type:"string"},user:{type:"string"},role:{type:"string"},"hide-on-profile":{type:"boolean",default:!1},target:{type:"string"},force:{type:"boolean",default:!1},help:{type:"boolean",short:"h",default:!1}},br=`lixblogs \u2014 LixBlogs CLI
20
+
21
+ Usage:
22
+ lixblogs login [--profile <name>] [--open]
23
+ lixblogs register [--profile <name>] [--open]
24
+ lixblogs logout [--profile <name>]
25
+ lixblogs whoami [--profile <name>] [--json]
26
+ lixblogs profiles [--json]
27
+ lixblogs use <name> [--json]
28
+ lixblogs auth login [--profile <name>] [--env <environment>] [--json] [--quiet] [--allow-insecure-fallback]
29
+ lixblogs auth status [--profile <name>] [--json]
30
+ lixblogs auth logout [--profile <name>] [--json] [--quiet]
31
+ lixblogs auth revoke [--profile <name>] [--json] [--quiet] --yes
32
+ lixblogs auth profiles [--json]
33
+ lixblogs auth use <name> [--json]
34
+ lixblogs blog list [--status <status>] [--limit <n>] [--cursor <cursor>] [--json]
35
+ lixblogs blog get <id> [--json]
36
+ lixblogs blog preview <id> [--json]
37
+ lixblogs blog create [--file <post.md>|--stdin|--content <markdown>|--editor] [metadata]
38
+ lixblogs blog edit <id> [--file <post.md>|--stdin|--content <markdown>|--editor] [metadata]
39
+ lixblogs blog publish <id> --yes [--dry-run] [--json]
40
+ lixblogs blog unpublish <id> --yes [--dry-run] [--json]
41
+ lixblogs blog delete <id> --yes [--permanent] [--dry-run] [--json]
42
+ lixblogs blog trash <id> --yes [--dry-run] [--json]
43
+ lixblogs blog restore <id> --yes [--dry-run] [--json]
44
+ lixblogs org list [--json]
45
+ lixblogs org get <id> [--json]
46
+ lixblogs org collections <id> [--json]
47
+ lixblogs org members <id> [--json]
48
+ lixblogs org targets [--json]
49
+ lixblogs collab list <blog-id> [--json]
50
+ lixblogs collab invitations [--json]
51
+ lixblogs collab invite <blog-id> --user <username> --role <viewer|editor|admin> --yes
52
+ lixblogs collab role <blog-id> --user <username-or-id> --role <viewer|editor|admin> --yes
53
+ lixblogs collab remove <blog-id> [--user <username-or-id>] --yes
54
+ lixblogs collab accept <blog-id> --yes [--hide-on-profile]
55
+ lixblogs collab decline <blog-id> --yes
56
+ lixblogs analytics query [--scope personal|org:<id>] [--range 30d] [--dimension overview]
57
+ lixblogs analytics export --output <file> [--format json|csv] [query options]
58
+ lixblogs skill list [--json]
59
+ lixblogs skill inspect <name> [--json]
60
+ lixblogs skill install <name> [--target <directory>] [--dry-run] --yes
61
+
62
+ Global flags:
63
+ --profile <name> local account alias (defaults to the signed-in username)
64
+ --env <environment> override environment (development|staging|production)
65
+ --auth-provider <provider> elixpo, or mock in development/test only
66
+ --accounts-url <url> override the Accounts discovery origin
67
+ --api-url <url> LixBlogs API origin (default: https://blogs.elixpo.com)
68
+ --scope <scope> request an OAuth scope (repeatable)
69
+ --file <path> read blog Markdown from a file
70
+ --stdin read blog Markdown from stdin
71
+ --content <markdown> use inline Markdown
72
+ --editor open the current blog in $EDITOR
73
+ --title/--subtitle/--slug update blog metadata
74
+ --tag <tag> set a tag (repeatable, up to five)
75
+ --publication <target> personal or org:<id>
76
+ --collection <id> organization collection ID
77
+ --dry-run validate and show the intended action without writing
78
+ --permanent permanently delete instead of moving to trash
79
+ --open open the device verification URL immediately
80
+ --json machine-readable JSON output
81
+ --quiet suppress non-essential output
82
+ --yes, -y auto-confirm destructive actions (required for revoke)
83
+ --allow-insecure-fallback explicit opt-in: if the OS keychain is unavailable, use a
84
+ non-persistent in-memory store instead of failing
85
+ --help, -h show this help
86
+
87
+ Machine mode:
88
+ --json --no-input produces stable JSON on stdout, diagnostics on stderr, and
89
+ never prompts. Publishing and destructive state changes require --yes.
90
+ `,vr=["openid","profile","email","lixblogs:profile:read","lixblogs:blog:read"];function R(t){return{profile:t.profile,env:t.env,authProvider:t["auth-provider"],accountsUrl:t["accounts-url"],apiUrl:t["api-url"],clientId:t["client-id"],audience:t.audience}}async function M(t,e){return t.profileExplicit?x(t.profile):await e.getActive()||x(t.profile)}async function Ie(t){let e=process.platform==="darwin"?"open":process.platform==="win32"?"cmd":"xdg-open",r=process.platform==="win32"?["/c","start","",t]:[t],o=wr(e,r,{detached:!0,stdio:"ignore"});o.on("error",()=>{}),o.unref()}function m(t,e){t.json&&process.stdout.write(Y(e)+`
91
+ `)}function f(t,e,r=p.ERROR){let o=e&&typeof e=="object"?e:{message:String(e)},i=q(o.message),n=Je({...o,message:i});t.json?process.stdout.write(Y(n)+`
92
+ `):t.quiet||(process.stderr.write(`Error: ${i}
93
+ `),o.hint&&process.stderr.write(`Hint: ${o.hint}
94
+ `),o.requestId&&process.stderr.write(`Request: ${o.requestId}
95
+ `)),process.exitCode=o.exitCode||r}async function S(t,e){try{return await Ce({allowInsecureFallback:t["allow-insecure-fallback"],profileRegistry:e})}catch(r){return f(t,`${r.message}${t["allow-insecure-fallback"]?"":" Re-run with --allow-insecure-fallback to opt in to non-persistent storage instead."}`),null}}async function Rt(t){let e=_({flags:R(t)}),r=new g,o=x(e.profile),i=t.scope?.length?[...t.scope]:[...vr];!e.profileExplicit&&!i.includes("lixblogs:profile:read")&&i.push("lixblogs:profile:read");let n;try{n=U(e)}catch(u){return f(t,u.message)}let a=await S(t,r);if(!a)return;let s=()=>{},l;try{l=await je({provider:n,credentialStore:a,profileId:o,scopes:i,openBrowser:t.open?Ie:void 0,resolveProfileId:e.profileExplicit?void 0:({accessToken:u})=>Fe({accessToken:u,apiBaseUrl:e.apiBaseUrl}),onStatus:u=>{if(t.json){u.type!=="pending"&&m(t,{event:u.type,...u});return}if(!t.quiet)if(u.type==="verification_pending"){let y=u.verificationUriComplete||u.verificationUri,c=!!process.stdin.isTTY&&!t["no-input"];process.stdout.write(He({url:y,code:u.userCode,expiresInSeconds:u.expiresInSeconds,profile:e.profileExplicit?o:null,interactive:c,color:fe()})),c&&!t.open&&(s=We({input:process.stdin,open:Ie,url:y}))}else u.type==="approved"?console.log(Ke("Access approved by Elixpo Accounts.",fe())):u.type==="denied"?console.log(" Access denied."):u.type==="expired"&&console.log(" Device code expired.")}})}finally{s()}if(!l.ok)return f(t,l.reason);await r.add(l.profileId),await r.setActive(l.profileId),m(t,{ok:!0,profile:l.profileId}),!t.json&&!t.quiet&&(console.log(` Credentials saved to local profile "${l.profileId}".`),console.log(" Tip: add another account with `lixblogs login`, list accounts with `lixblogs profiles`,"),console.log(" and switch with `lixblogs use <username>`."))}async function xr(t){let e=_({flags:R(t)}),r=new g,o=await M(e,r),i=await S(t,r);if(!i)return;let n=await Ue({credentialStore:i,profileId:o});if(m(t,n),!t.json)for(let a of n)a.loggedIn?console.log(`${a.profileId}: logged in${a.expired?" (expired)":""} \u2014 scopes: ${a.scopes.join(", ")}`):console.log(`${a.profileId}: not logged in`)}async function ne(t){let e=_({flags:R(t)}),r=new g,o=await M(e,r),i=await S(t,r);if(!i)return null;let n;try{n=U(e)}catch(s){return f(t,s),null}let a=new N({provider:n,credentialStore:i,profileId:o,apiBaseUrl:e.apiBaseUrl});return{client:new D(a),http:a,config:e,credentialStore:i,profileId:o}}async function _r(t){let e=await ne(t);if(e)try{let[r,o]=await Promise.all([e.client.whoami(),e.credentialStore.get(e.profileId)]),i={ok:!0,profile:e.profileId,environment:e.config.environment,identity:r,scopes:o?.scopes||[],expiresAt:o?.expiresAt?new Date(o.expiresAt).toISOString():null,expired:o?Date.now()>=o.expiresAt:!0};m(t,i),!t.json&&!t.quiet&&(console.log(`${r.displayName||r.username} (@${r.username})`),console.log(`Profile: ${e.profileId} \xB7 ${i.environment}`),console.log(`Scopes: ${i.scopes.join(", ")||"none"}`),console.log(`Expires: ${i.expiresAt||"unknown"}`))}catch(r){f(t,r,r.status===401||r.status===403?p.AUTH:p.ERROR)}}async function Ir(t){let e=_({flags:R(t)}),r=new URL("/register",e.accountsBaseUrl).toString();if(t["no-input"]){m(t,{ok:!0,registrationUrl:r,next:"lixblogs login"}),!t.json&&!t.quiet&&console.log(r);return}await Ie(r),t.quiet||console.log(`Create your account at ${r}, then approve the device login.`),await Rt(t)}async function Er(t){let e=_({flags:R(t)}),r=new g,o=await M(e,r),i=await S(t,r);if(!i)return;let n=await Ne({credentialStore:i,profileId:o});m(t,n),!t.json&&!t.quiet&&console.log(`Logged out profile "${o}".`)}async function kr(t){let e=_({flags:R(t)}),r=new g,o=await M(e,r);if(!t.yes)return f(t,"This is a destructive action. Re-run with --yes to confirm (interactive confirmation prompt not yet implemented).");let i;try{i=U(e)}catch(s){return f(t,s.message)}let n=await S(t,r);if(!n)return;let a=await De({provider:i,credentialStore:n,profileId:o,confirmed:!0});if(!a.ok)return f(t,a.reason);m(t,a),!t.json&&!t.quiet&&console.log(`Revoked and logged out profile "${o}".`)}async function Sr(t){let e=new g,r=await S(t,e);if(!r)return;let o=await Be({credentialStore:r,profileRegistry:e});if(m(t,o),!t.json){o.profiles.length||console.log("No profiles. Run `lixblogs auth login` first.");for(let i of o.profiles)console.log(`${i.active?"*":" "} ${i.profileId}${i.expired?" (expired)":""}`)}}async function Ar(t,e){let r;try{r=x(e[0])}catch(a){return f(t,a.message)}let o=new g,i=await S(t,o);if(!i)return;let n=await Me({credentialStore:i,profileRegistry:o,profileId:r});if(!n.ok)return f(t,n.reason);m(t,n),!t.json&&!t.quiet&&console.log(`Using profile "${r}".`)}var Pt={list:Ye,get:be,preview:be,create:Ze,edit:Qe,publish:et,unpublish:tt,delete:ve,trash:ve,restore:rt},qt={list:ot,get:it,collections:nt,members:st,targets:at},Ot={list:lt,invitations:ct,invite:ut,role:dt,remove:pt,accept:ft,decline:ht},Tt={list:({options:t})=>vt(t),inspect:({id:t})=>xt({name:t}),install:({id:t,options:e})=>_t({name:t,options:e})},Ct={query:St,export:At};async function $r(t,e,r){let o=_({flags:R(t)}),i=new g,n=await M(o,i),a=await S(t,i);if(!a)return;let s;try{s=U(o)}catch(c){return f(t,c.message)}let l=new N({provider:s,credentialStore:a,profileId:n,apiBaseUrl:o.apiBaseUrl}),u=new D(l),y={...t,limit:t.limit===void 0?void 0:Number.parseInt(t.limit,10)};try{let c=await Pt[r]({client:u,id:e[0],options:y,stdin:process.stdin});if(m(t,{ok:!0,...c}),!t.json&&!t.quiet)if(r==="list"){for(let h of c.data||[])console.log(`${h.id} ${h.status} ${h.title||"(untitled)"}`);c.meta?.nextCursor&&console.log(`Next cursor: ${c.meta.nextCursor}`)}else r==="get"?console.log(`${c.title||"(untitled)"} [${c.status}]
96
+ ${c.markdown||""}`):c.dryRun?console.log(`Dry run: ${r} validated; no changes sent.`):console.log(c.url||`${r} completed for ${c.id}.`)}catch(c){if(t.json&&c instanceof d){process.stdout.write(Y({ok:!1,error:{code:c.code,message:c.message,requestId:c.requestId,details:c.details}})+`
97
+ `),process.exitCode=c.status===412?3:1;return}return f(t,c,c.status===412?p.CONFLICT:p.ERROR)}}async function Rr(t,e,r){let o=await ne(t);if(!o)return;let i=new Q(o.http);try{let n=await qt[r]({client:i,id:e[0],options:t});if(m(t,{ok:!0,data:n}),t.json||t.quiet)return;if(r==="targets"){console.log("personal Personal Blog");for(let s of n.organizations||[]){console.log(`${s.target} ${s.role} ${s.name}`);for(let l of s.collections||[])console.log(` collection:${l.id} ${l.name}`)}return}let a=r==="list"?n.data||[]:Array.isArray(n)?n:[n];for(let s of a)console.log([s.id||s.userId||s.orgId,s.role,s.slug||s.username,s.name||s.displayName].filter(Boolean).join(" "))}catch(n){f(t,n,n.status===401||n.status===403?p.AUTH:p.ERROR)}}async function Pr(t,e,r){let o=await ne(t);if(!o)return;let i=new te(o.http);try{let n=await Ot[r]({client:i,id:e[0],options:t});if(m(t,{ok:!0,data:n}),t.json||t.quiet)return;if(n.dryRun){console.log(`Dry run: ${n.action} validated; no changes sent.`);return}let a=r==="invitations"?n:r==="list"?n.collaborators||[]:[n];for(let s of a)console.log([s.blogId||s.userId,s.status,s.role,s.username||s.title,s.notificationState].filter(Boolean).join(" "))}catch(n){f(t,n,n.status===401||n.status===403?p.AUTH:p.ERROR)}}async function qr(t,e,r){try{let o=await Tt[r]({id:e[0],options:t});if(m(t,{ok:!0,data:o}),t.json||t.quiet)return;if(r==="list")for(let i of o)console.log(`${i.name} CLI >= ${i.minimumCliVersion||"unknown"} ${i.description}`);else r==="inspect"?process.stdout.write(o.content):o.dryRun?console.log(`Dry run: install ${o.name} to ${o.target}${o.replace?" (replace)":""}.`):console.log(`Installed ${o.name} at ${o.target}.`)}catch(o){f(t,o)}}async function Or(t,e,r){let o=await ne(t);if(!o)return;let i=new re(o.http),n={...t,limit:t.limit===void 0?void 0:Number.parseInt(t.limit,10)};try{let a=await Ct[r]({client:i,options:n});if(m(t,{ok:!0,data:a}),t.json||t.quiet)return;if(r==="export"){console.log(`Exported ${a.rows} rows to ${a.output}.`);return}let s=a.data;if(console.log(`${s.scope.label} \xB7 ${s.dimension} \xB7 ${s.range.key}`),s.dimension==="overview")for(let[l,u]of Object.entries(s.values.totals))console.log(`${l} ${u} ${s.values.changes[l]}%`);else if(s.dimension==="timeline")s.values.labels.forEach((l,u)=>console.log(`${l} ${s.values.views[u]} ${s.values.reads[u]}`));else{for(let l of s.values)console.log(Object.values(l).join(" "));a.meta?.nextCursor&&console.log(`Next cursor: ${a.meta.nextCursor}`)}}catch(a){f(t,a,a.status===401||a.status===403?p.AUTH:p.ERROR)}}var $t={auth:{login:Rt,status:xr,whoami:_r,logout:Er,revoke:kr,profiles:Sr,use:Ar},blog:Object.fromEntries(Object.keys(Pt).map(t=>[t,(e,r)=>$r(e,r,t)])),org:Object.fromEntries(Object.keys(qt).map(t=>[t,(e,r)=>Rr(e,r,t)])),collab:Object.fromEntries(Object.keys(Ot).map(t=>[t,(e,r)=>Pr(e,r,t)])),skill:Object.fromEntries(Object.keys(Tt).map(t=>[t,(e,r)=>qr(e,r,t)])),analytics:Object.fromEntries(Object.keys(Ct).map(t=>[t,(e,r)=>Or(e,r,t)]))};async function Tr(){let t,e;try{({values:t,positionals:e}=gr({args:process.argv.slice(2),options:yr,allowPositionals:!0,strict:!0}))}catch(a){process.stderr.write(`Error: Invalid flag. ${a.message}
98
+ `),process.exitCode=p.USAGE;return}if(t.help||e.length===0){process.stdout.write(br);return}if(e[0]==="register"){await Ir(t);return}e=Ve(e);let[r,o]=e,i=$t[r];if(!i){process.stderr.write(`Error: Unknown command category "${r}".
99
+ `),process.stderr.write(`Available categories: ${Object.keys($t).join(", ")}
100
+ `),process.exitCode=p.USAGE;return}let n=i[o];if(!n){process.stderr.write(`Error: Unknown ${r} command "${o}".
101
+ `),process.stderr.write(`Available commands: ${Object.keys(i).map(a=>`${r} ${a}`).join(", ")}
102
+ `),process.exitCode=p.USAGE;return}await n(t,e.slice(2))}Tr();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elixpo/lixblogs-cli",
3
- "version": "1.3.1",
3
+ "version": "1.4.2",
4
4
  "description": "Official CLI for LixBlogs — publish, manage, and inspect blogs through the supported API. Built for creators and agent automation.",
5
5
  "author": {
6
6
  "name": "Elixpo",
@@ -15,22 +15,18 @@
15
15
  ],
16
16
  "type": "module",
17
17
  "bin": {
18
- "lixblogs": "bin/lixblogs.mjs"
18
+ "lixblogs": "dist/lixblogs.mjs"
19
19
  },
20
20
  "files": [
21
- "src",
22
- "bin",
21
+ "dist",
23
22
  "skills",
24
- "README.md",
25
- "API.md",
26
- "THREAT_MODEL.md",
27
- "RELEASE.md",
28
- "CHANGELOG.md"
23
+ "README.md"
29
24
  ],
30
25
  "scripts": {
26
+ "build": "esbuild bin/lixblogs.mjs --bundle --platform=node --format=esm --target=node18 --minify --tree-shaking=true --legal-comments=none --external:@napi-rs/keyring --outfile=dist/lixblogs.mjs",
31
27
  "test": "node --test tests/*.test.mjs",
32
28
  "skills:sync": "node scripts/sync-skills.mjs",
33
- "prepack": "npm run skills:sync && npm test",
29
+ "prepack": "npm run skills:sync && npm run build && npm test",
34
30
  "postpack": "node scripts/sync-skills.mjs --clean",
35
31
  "prepublishOnly": "npm test"
36
32
  },
@@ -73,5 +69,8 @@
73
69
  },
74
70
  "publishConfig": {
75
71
  "access": "public"
72
+ },
73
+ "devDependencies": {
74
+ "esbuild": "^0.27.3"
76
75
  }
77
76
  }
package/API.md DELETED
@@ -1,104 +0,0 @@
1
- # LixBlogs API v1 contract
2
-
3
- The CLI is a public OAuth client. Accounts issues credentials; LixBlogs is the
4
- resource server. The CLI must never connect to D1 or carry a client secret.
5
-
6
- ## Origins and discovery
7
-
8
- - OAuth issuer: `https://accounts.elixpo.com`
9
- - Token audience: `blogs.elixpo.com`
10
- - Resource root: `https://blogs.elixpo.com/api/v1`
11
- - Contract metadata: `GET /api/v1`
12
-
13
- Production bearer tokens are EdDSA access tokens issued to an allowlisted CLI
14
- client. LixBlogs verifies their signature, expiry, audience, client, scopes,
15
- and local account before querying creator data. Tokens are never persisted or
16
- logged by the resource API.
17
-
18
- ## Initial resources
19
-
20
- | Method | Path | Scope | Behavior |
21
- | --- | --- | --- | --- |
22
- | `GET` | `/api/v1` | public | API and compatibility metadata |
23
- | `GET` | `/api/v1/me` | `lixblogs:profile:read` | Current LixBlogs identity |
24
- | `GET` | `/api/v1/blogs` | `lixblogs:blog:read` | Accessible blog metadata |
25
- | `GET` | `/api/v1/blogs/{id}` | `lixblogs:blog:read` | One accessible blog and its content |
26
- | `POST` | `/api/v1/blogs` | `lixblogs:blog:write` | Create a draft |
27
- | `PATCH` | `/api/v1/blogs/{id}` | `lixblogs:blog:write` | Edit a draft or post |
28
- | `POST` | `/api/v1/blogs/{id}/publish` | `lixblogs:blog:publish` | Publish a post |
29
- | `POST` | `/api/v1/blogs/{id}/unpublish` | `lixblogs:blog:publish` | Return a post to draft |
30
- | `DELETE` | `/api/v1/blogs/{id}` | `lixblogs:blog:delete` | Move a post to trash |
31
- | `POST` | `/api/v1/blogs/{id}/restore` | `lixblogs:blog:delete` | Restore a trashed post |
32
- | `GET` | `/api/v1/orgs` | `lixblogs:organizations:read` | Organizations the caller belongs to |
33
- | `GET` | `/api/v1/orgs/{id}` | `lixblogs:organizations:read` | One caller-visible organization and role |
34
- | `GET` | `/api/v1/orgs/{id}/collections` | `lixblogs:organizations:read` | Collections within a caller membership |
35
- | `GET` | `/api/v1/orgs/{id}/members` | `lixblogs:organizations:read` | Members and roles within a caller membership |
36
- | `GET` | `/api/v1/blogs/{id}/collaborators` | `lixblogs:collaboration:read` | Editorial team and effective role |
37
- | `POST` | `/api/v1/blogs/{id}/collaborators` | `lixblogs:collaboration:write` | Invite or reassign a collaborator |
38
- | `PATCH` | `/api/v1/blogs/{id}/collaborators` | `lixblogs:collaboration:write` | Change a collaborator role |
39
- | `DELETE` | `/api/v1/blogs/{id}/collaborators` | `lixblogs:collaboration:write` | Remove a collaborator or leave a team |
40
- | `GET` | `/api/v1/collaboration/invitations` | `lixblogs:collaboration:read` | Current identity's invitations |
41
- | `POST` | `/api/v1/collaboration/invitations` | `lixblogs:collaboration:write` | Accept or decline an invitation |
42
- | `GET` | `/api/v1/analytics` | `lixblogs:analytics:read` | Bounded creator analytics dimensions |
43
-
44
- `GET /api/v1/analytics` accepts `scope=personal|org:<id>`, `range=7d|30d|90d|12m|custom`,
45
- `from`, `to`, `dimension=overview|timeline|posts|sources|devices|countries`, `limit=1..100`,
46
- and an opaque `cursor`. Organization queries additionally require `lixblogs:organizations:read`
47
- and an owner, admin, or maintain membership. Responses contain aggregate creator data;
48
- visitor identifiers are never returned.
49
-
50
- `GET /api/v1/blogs` accepts `status=all|draft|published`, `limit=1..100`, and
51
- an opaque `cursor`. Results include authored blogs, accepted collaborations,
52
- and blogs belonging to organizations of which the caller is a member. Access
53
- misses return `404` so resource existence is not disclosed.
54
-
55
- Permanent deletion uses `DELETE /api/v1/blogs/{id}?permanent=true`. It requires
56
- the additional `lixblogs:blog:delete:permanent` scope and an
57
- `X-Confirm-Permanent-Delete` header equal to the blog ID.
58
-
59
- ## Response shape
60
-
61
- Successful responses use:
62
-
63
- ```json
64
- { "data": {}, "meta": {} }
65
- ```
66
-
67
- Errors use a stable machine-readable envelope:
68
-
69
- ```json
70
- {
71
- "error": {
72
- "code": "invalid_token",
73
- "message": "The access token is malformed.",
74
- "requestId": "..."
75
- }
76
- }
77
- ```
78
-
79
- Every response carries `X-LixBlogs-API-Version` and `X-Request-ID`. Authenticated
80
- responses also expose bounded per-minute `X-RateLimit-*` values. Clients should
81
- use error codes rather than matching human-readable messages.
82
-
83
- ## Concurrency and retries
84
-
85
- Single-blog reads include a strong `ETag`. Future write endpoints require
86
- `If-Match` so a stale CLI cannot silently overwrite a newer browser edit.
87
-
88
- Mutation requests use an `Idempotency-Key` containing 8–128 URL-safe
89
- characters. Reservations live for 24 hours:
90
-
91
- - same key and same request: replay the retained response;
92
- - same key while running: `idempotency_in_progress`;
93
- - same key with different input: `idempotency_key_reused`.
94
-
95
- Operational rows are pruned in bounded batches. Audit events record the caller,
96
- client, action, resource, outcome, and request ID, never bearer material.
97
-
98
- ## Compatibility policy
99
-
100
- The `/api/v1` URL and envelope are stable for the v1 lifetime. Fields may be
101
- added without a major version change; existing fields and error codes are not
102
- removed or redefined. A breaking change requires `/api/v2`. The metadata
103
- endpoint advertises `minCliVersion`; clients below it must stop before making
104
- authenticated requests and present an upgrade instruction.
package/CHANGELOG.md DELETED
@@ -1,10 +0,0 @@
1
- # Changelog
2
-
3
- Release notes are generated from merged pull requests. This file records contract-level changes that users must see before upgrading.
4
-
5
- ## 1.3.0
6
-
7
- - Added scoped organization and editorial commands.
8
- - Added bounded, read-only creator analytics with JSON and CSV export.
9
- - Bundled five independently installable LixBlogs agent skills.
10
- - Added packed-artifact, Accounts device-flow, Blogs resource-contract, provenance, and rollback release gates.
package/RELEASE.md DELETED
@@ -1,30 +0,0 @@
1
- # Release and compatibility policy
2
-
3
- ## Compatibility
4
-
5
- - CLI `1.x` targets LixBlogs API `/api/v1` and the Accounts OAuth discovery contract.
6
- - Additive response fields and commands are minor releases. Fixes without contract changes are patch releases.
7
- - Removing a command, field, scope, or error code requires a CLI major release or API `/api/v2`.
8
- - The resource metadata `minCliVersion` is authoritative. An older client must stop before authenticated requests and ask the user to upgrade.
9
- - Node 18 is the supported runtime floor. Release gates run on Node 22.
10
-
11
- ## Release
12
-
13
- 1. Update the package version and `CHANGELOG.md` in a reviewed PR.
14
- 2. Run the manual **LixBlogs CLI release gate**. It tests the package, installs the exact tarball, checks bundled skills, verifies Blogs resource contracts, and runs the Accounts device-flow contract.
15
- 3. Tag the reviewed commit as `lixblogs-cli-vX.Y.Z`.
16
- 4. The publish workflow reruns the gate, checks that tag and package versions match, attests the tarball, publishes it through npm trusted publishing with provenance, and creates generated GitHub release notes.
17
-
18
- No npm token is stored in this workflow. The npm package must configure this repository and `publish-lixblogs-cli.yml` as a trusted publisher.
19
-
20
- ## Smoke criteria
21
-
22
- The packed artifact must install into an empty prefix, render `--help`, discover all five scoped skills, and pass auth, blog lifecycle, organization, collaboration, and analytics command tests. Accounts must pass device approval, refresh rotation, replay protection, and revocation. Blogs must pass bearer validation, concurrency/idempotency, analytics, and media request-boundary tests.
23
-
24
- ## Rollback
25
-
26
- 1. Stop a broken release with `npm deprecate @elixpo/lixblogs-cli@X.Y.Z "Do not use; upgrade to X.Y.N"`.
27
- 2. Restore the previous compatible version with `npm dist-tag add @elixpo/lixblogs-cli@GOOD latest`.
28
- 3. Open a patch PR; never reuse or delete the published version.
29
- 4. Re-run the release gate and publish a new patch tag.
30
- 5. If the API contract caused the failure, raise `minCliVersion` only after the compatible patch is available.
package/THREAT_MODEL.md DELETED
@@ -1,91 +0,0 @@
1
- # LixBlogs CLI — Auth Threat Model
2
-
3
- Tracking: elixpo/blogs.elixpo#137
4
- Status: **Resolved by implementer** — this doc's existence and scope was an
5
- open question on #137 ("should a threat model be written as part of this
6
- issue, and what should it contain?"). Decision: yes, scoped to the five
7
- areas #137 itself named. Flagged for the maintainer to expand or narrow.
8
-
9
- ## 1. Token storage
10
-
11
- - Access and refresh tokens are stored in the OS keychain
12
- (Keychain on macOS, Secret Service/libsecret on Linux, Credential Manager
13
- on Windows) — never in a plain config file or environment variable by
14
- default.
15
- - A fallback (e.g. encrypted file on disk) is only used if the keychain is
16
- unavailable, and only with explicit user opt-in at that moment — never a
17
- silent default.
18
- - Redaction: tokens must never appear in logs, `--json` output, telemetry,
19
- or crash reports. This is verified by test (see `auth.test.mjs`), not
20
- just documented — string values matching known token prefixes should be
21
- masked in any error-serialization path.
22
- - Risk if this fails: a leaked token grants whatever scopes it holds until
23
- revoked or expired — this is why short-lived access tokens (with refresh
24
- rotation) matter more than keychain storage alone.
25
-
26
- ## 2. Scope boundaries
27
-
28
- - Scopes are least-privilege by default; publishing and destructive actions
29
- require scopes distinct from read/draft scopes (per #135).
30
- - Accounts publishes the registered LixBlogs scope list. Login defaults to
31
- identity plus profile/blog read scopes; broader scopes must be requested
32
- explicitly and remain bounded by the public client's registration.
33
- - Risk: an overly broad default scope grant (e.g. `login` implicitly
34
- granting `publish`) would mean any compromised session can publish
35
- without the user having explicitly consented to that. Mitigation: the
36
- CLI must request only the scopes a given command needs, not a blanket
37
- "everything" scope at login time.
38
-
39
- ## 3. Blast radius of a compromised agent/CLI session
40
-
41
- - Worst case if a token is stolen or an agent is compromised: whatever
42
- scopes that specific token holds, until it's revoked or naturally
43
- expires (access tokens are short-lived; refresh tokens are the more
44
- valuable target).
45
- - Mitigations already in scope: `lixblogs auth revoke`, tested refresh
46
- rotation/reuse rejection, and multi-profile isolation (below).
47
- - Not yet mitigated / open for later work: there's no mechanism yet for a
48
- user to see "which sessions/devices currently hold a valid token for my
49
- account" and revoke just one — this would materially reduce blast radius
50
- and is worth a follow-up issue, not blocking this one.
51
-
52
- ## 4. Device-flow-specific risks
53
-
54
- - **Code interception**: the user code is short and meant to be read aloud/
55
- typed manually; the device code (long, unguessable) is what's actually
56
- exchanged for a token. If a device code leaks (e.g. via a compromised
57
- clipboard), same blast radius as a stolen token.
58
- - **Polling abuse**: a malicious client could poll rapidly to try to beat
59
- the user to approving/denying. Mitigated by `pollIntervalSeconds` and the
60
- `slow_down` response (see MockAuthProvider) which forces callers to back
61
- off — the real provider is expected to enforce this server-side too, not
62
- just suggest it.
63
- - **Expired/denied code handling**: both must be treated as fully dead ends
64
- — no retry-with-same-code path. The mock encodes this by design (an
65
- expired/denied device code never later returns `approved`).
66
- - **Reused-code replay**: an already-approved device code should not be
67
- usable to fetch a second, independent token. Not yet tested in the mock
68
- — worth adding once the "one device code → one token exchange" contract
69
- is confirmed for the real provider, since the mock doesn't currently
70
- invalidate a code after issuing a token for it.
71
-
72
- ## 5. Multi-profile / account isolation
73
-
74
- - Each named profile's tokens must be stored under a distinct keychain
75
- entry — one profile's credential lookup must never return another
76
- profile's token, even if both are logged in simultaneously.
77
- - Switching the active profile must not require re-entering credentials for
78
- profiles already logged in, but must also never leak which other profiles
79
- exist to a scope that shouldn't know (e.g. `--json` output for one
80
- profile shouldn't enumerate other profiles' identifiers).
81
- - The active profile is non-sensitive registry metadata. Each profile's
82
- credentials remain isolated in its own OS-keychain entry, and concurrent
83
- refreshes for one profile share a single rotation operation.
84
-
85
- ## Explicitly out of scope for this document
86
-
87
- - Threat modeling the API server itself (auth middleware, D1 access
88
- patterns) — that's elixpo/blogs.elixpo#136's concern.
89
- - Supply-chain risk on the npm package itself (dependency compromise,
90
- publish-time integrity) — worth a separate doc once packaging/release
91
- work (later phase) is underway.