@elixpo/lixblogs-cli 1.4.5 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -156
- package/dist/lixblogs.mjs +34 -21
- package/package.json +1 -1
- package/skills/lixblogs-author/SKILL.md +3 -1
- package/skills/lixblogs-media/SKILL.md +53 -0
- package/skills/lixblogs-publish/SKILL.md +1 -0
package/README.md
CHANGED
|
@@ -1,75 +1,31 @@
|
|
|
1
1
|
# @elixpo/lixblogs-cli
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
This package implements production device-flow authentication and the core
|
|
7
|
-
blog lifecycle over the stable LixBlogs API v1 contract. Its output stays
|
|
8
|
-
compact and predictable for both terminals and automation.
|
|
9
|
-
|
|
10
|
-
## Install (local development)
|
|
11
|
-
|
|
12
|
-
```bash
|
|
13
|
-
cd packages/lixblogs-cli
|
|
14
|
-
npm install
|
|
15
|
-
```
|
|
3
|
+
Publish, manage, and inspect LixBlogs through its stable API v1. The CLI uses
|
|
4
|
+
device authorization and predictable terminal or JSON output.
|
|
16
5
|
|
|
17
6
|
```bash
|
|
18
7
|
npm install -g @elixpo/lixblogs-cli
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
## Usage
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
node bin/lixblogs.mjs --help
|
|
8
|
+
lixblogs --help
|
|
25
9
|
```
|
|
26
10
|
|
|
27
11
|
### Authentication
|
|
28
12
|
|
|
29
13
|
```bash
|
|
30
|
-
# Log in via device authorization
|
|
31
14
|
node bin/lixblogs.mjs login
|
|
32
|
-
# Credentials are saved under the authenticated username and made active.
|
|
33
|
-
|
|
34
|
-
# Save credentials under an explicit local profile name
|
|
35
15
|
node bin/lixblogs.mjs login --profile personal
|
|
36
|
-
|
|
37
|
-
# Check login status
|
|
38
16
|
node bin/lixblogs.mjs whoami
|
|
39
|
-
|
|
40
|
-
# List profiles and choose the active one
|
|
41
17
|
node bin/lixblogs.mjs profiles
|
|
42
18
|
node bin/lixblogs.mjs use work
|
|
43
|
-
|
|
44
|
-
# Log out (clears local credentials only)
|
|
45
19
|
node bin/lixblogs.mjs logout
|
|
46
|
-
|
|
47
|
-
# Revoke the token server-side and clear local credentials (destructive)
|
|
48
20
|
node bin/lixblogs.mjs auth revoke --yes
|
|
49
21
|
```
|
|
50
22
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
to a VPS. Device authorization does not require a localhost callback or an
|
|
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.
|
|
23
|
+
Press Enter to open the verification URL or copy it to another device. The
|
|
24
|
+
username becomes the profile alias unless `--profile` overrides it. Use
|
|
25
|
+
`profiles` and `use <username>` to switch accounts.
|
|
59
26
|
|
|
60
27
|
### Blog lifecycle
|
|
61
28
|
|
|
62
|
-
Request the permissions needed for the operations you intend to use:
|
|
63
|
-
|
|
64
|
-
```bash
|
|
65
|
-
node bin/lixblogs.mjs auth login \
|
|
66
|
-
--scope openid --scope profile --scope lixblogs:blog:read \
|
|
67
|
-
--scope lixblogs:blog:write --scope lixblogs:blog:publish \
|
|
68
|
-
--scope lixblogs:blog:delete
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
Then work with Markdown without any database or Cloudflare credentials:
|
|
72
|
-
|
|
73
29
|
```bash
|
|
74
30
|
lixblogs blog list --status draft
|
|
75
31
|
lixblogs blog create --file post.md --title "A new post" --tag engineering
|
|
@@ -80,8 +36,12 @@ lixblogs blog unpublish <id> --yes
|
|
|
80
36
|
lixblogs blog delete <id> --yes
|
|
81
37
|
lixblogs blog list --status trashed
|
|
82
38
|
lixblogs blog restore <id> --yes
|
|
39
|
+
lixblogs blog history <id>
|
|
40
|
+
lixblogs blog restore-version <id> --version <version-id> --yes
|
|
83
41
|
```
|
|
84
42
|
|
|
43
|
+
Titles, subtitles, slugs, tags, icon emoji, cover URL/position/zoom, publication target, collection, comment policy, membership, secret state, and published/unlisted visibility are supported by `blog create`, `blog edit`, and `blog publish`.
|
|
44
|
+
|
|
85
45
|
Inspect valid publication targets before assigning organization metadata:
|
|
86
46
|
|
|
87
47
|
```bash
|
|
@@ -92,9 +52,6 @@ lixblogs org members ORG_ID
|
|
|
92
52
|
lixblogs org targets --json
|
|
93
53
|
```
|
|
94
54
|
|
|
95
|
-
Organization lookup is membership-bound. A slug alone never grants access;
|
|
96
|
-
the API resolves the authenticated user's role before returning tenant data.
|
|
97
|
-
|
|
98
55
|
Editorial collaboration stays separate from publishing:
|
|
99
56
|
|
|
100
57
|
```bash
|
|
@@ -106,10 +63,6 @@ lixblogs collab accept BLOG_ID --yes
|
|
|
106
63
|
lixblogs collab decline BLOG_ID --yes
|
|
107
64
|
```
|
|
108
65
|
|
|
109
|
-
Viewer, editor, and admin roles grant different editorial authority. None of
|
|
110
|
-
these commands publishes a post; public-state changes still use `blog publish`
|
|
111
|
-
with the publish scope and a separate confirmation.
|
|
112
|
-
|
|
113
66
|
### Creator analytics
|
|
114
67
|
|
|
115
68
|
Analytics is read-only and uses bounded date ranges and dimensions:
|
|
@@ -122,9 +75,27 @@ lixblogs analytics query --scope org:ORG_ID --range custom \
|
|
|
122
75
|
lixblogs analytics export --dimension timeline --format csv --output analytics.csv
|
|
123
76
|
```
|
|
124
77
|
|
|
125
|
-
Organization analytics also requires `lixblogs:organizations:read`. Results
|
|
126
|
-
|
|
127
|
-
|
|
78
|
+
Organization analytics also requires `lixblogs:organizations:read`. Results are
|
|
79
|
+
aggregate-only, and exports refuse to overwrite an existing file.
|
|
80
|
+
|
|
81
|
+
### Comments and media
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
lixblogs comment list BLOG_ID
|
|
85
|
+
lixblogs comment add BLOG_ID --content "Clear explanation"
|
|
86
|
+
lixblogs comment reply BLOG_ID --parent COMMENT_ID --content "Following up"
|
|
87
|
+
lixblogs comment delete BLOG_ID --comment COMMENT_ID --yes
|
|
88
|
+
|
|
89
|
+
lixblogs media upload --file diagram.webp --blog BLOG_ID --type inline --attach
|
|
90
|
+
lixblogs integrations pollinations-status --json
|
|
91
|
+
lixblogs media generate --prompt "Editorial illustration" --model flux \
|
|
92
|
+
--blog BLOG_ID --type cover --attach --output cover.jpg
|
|
93
|
+
lixblogs media delete MEDIA_ID --yes
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Pollinations generation uses the creator's BYOP connection in Settings. The
|
|
97
|
+
CLI never stores its key or retries a billable generation automatically. Keep
|
|
98
|
+
the local output for a manual `media upload` retry.
|
|
128
99
|
|
|
129
100
|
### Agent skills
|
|
130
101
|
|
|
@@ -137,116 +108,32 @@ lixblogs skill install lixblogs-author --target .agents/skills --dry-run
|
|
|
137
108
|
lixblogs skill install lixblogs-author --target .agents/skills --yes
|
|
138
109
|
```
|
|
139
110
|
|
|
140
|
-
Install only the
|
|
141
|
-
|
|
142
|
-
minimum compatible CLI version and scopes.
|
|
111
|
+
Install only the needed skill. Existing files require explicit `--force --yes`.
|
|
112
|
+
Each skill declares its minimum CLI version and scopes.
|
|
143
113
|
|
|
144
114
|
`create`, `edit`, `publish`, `unpublish`, `delete`, and `restore` accept
|
|
145
115
|
`--dry-run`. Content input is mutually exclusive: `--file`, `--stdin`,
|
|
146
|
-
`--content`, or `--editor`. Permanent deletion
|
|
116
|
+
`--content`, or `--editor`. Permanent deletion requires
|
|
147
117
|
`--permanent --yes` and the `lixblogs:blog:delete:permanent` scope.
|
|
148
118
|
|
|
149
|
-
Edits use the server ETag
|
|
150
|
-
|
|
151
|
-
`.lixblogs-conflicts/`; it never overwrites the newer server revision.
|
|
152
|
-
|
|
153
|
-
Global flags:
|
|
154
|
-
- `--profile <name>` — override the username-based local account alias
|
|
155
|
-
- `--env <environment>` — override environment (`development` | `staging` | `production`)
|
|
156
|
-
- `--scope <scope>` — request an additional/alternate OAuth scope; repeatable
|
|
157
|
-
- `--open` — open the verification URL with the device code pre-filled
|
|
158
|
-
- `--accounts-url <url>` — override the Accounts issuer for local/staging tests
|
|
159
|
-
- `--api-url <url>` — override the LixBlogs API origin; production defaults to
|
|
160
|
-
`https://blogs.elixpo.com`
|
|
161
|
-
- `--json` — machine-readable JSON output
|
|
162
|
-
- `--quiet` — suppress non-essential output
|
|
163
|
-
- `--yes`, `-y` — confirm publishing and destructive state changes
|
|
164
|
-
- `--allow-insecure-fallback` — explicit opt-in: if the OS keychain is
|
|
165
|
-
unavailable, use a non-persistent in-memory store instead of failing
|
|
119
|
+
Edits use the server ETag. Conflicts exit with code 3 and retain both versions
|
|
120
|
+
under `.lixblogs-conflicts/` without overwriting the server revision.
|
|
166
121
|
|
|
167
122
|
### Service boundary
|
|
168
123
|
|
|
169
124
|
- `https://accounts.elixpo.com` issues, refreshes, and revokes OAuth tokens.
|
|
170
125
|
- `https://blogs.elixpo.com/api/v1` is the only production resource API.
|
|
171
|
-
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
- The resource contract, scopes, pagination, errors, and mutation guarantees
|
|
176
|
-
are documented in the [API reference](https://github.com/elixpo/blogs.elixpo/blob/main/packages/lixblogs-cli/API.md).
|
|
177
|
-
- Release compatibility, provenance, smoke gates, and rollback are documented
|
|
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).
|
|
181
|
-
|
|
182
|
-
The production client is public and has no client secret. Never add one to
|
|
183
|
-
CLI configuration, package files, or GitHub secrets.
|
|
184
|
-
|
|
185
|
-
### Configuration precedence
|
|
186
|
-
|
|
187
|
-
Configuration resolves in this order: command flags, `LIXBLOGS_*` environment
|
|
188
|
-
variables, the selected named profile, then production-safe defaults. Use
|
|
189
|
-
`lixblogs whoami --json --no-input` to verify the active profile, environment,
|
|
190
|
-
granted scopes, and expiry before automation. Flags are best for one command;
|
|
191
|
-
environment values are best for a contained CI job. Credentials remain in the
|
|
192
|
-
OS keychain and are never read from environment variables.
|
|
126
|
+
- Incompatible discovery metadata and unexpected origins are rejected.
|
|
127
|
+
- See the [API contract](https://github.com/elixpo/blogs.elixpo/blob/main/packages/lixblogs-cli/API.md), [release policy](https://github.com/elixpo/blogs.elixpo/blob/main/packages/lixblogs-cli/RELEASE.md), and [changelog](https://github.com/elixpo/blogs.elixpo/blob/main/packages/lixblogs-cli/CHANGELOG.md).
|
|
128
|
+
|
|
129
|
+
The public production client has no client secret. Never add one.
|
|
193
130
|
|
|
194
131
|
### Troubleshooting
|
|
195
132
|
|
|
196
|
-
- `invalid_scope`: Accounts has not registered the requested permission
|
|
197
|
-
this client; do not substitute a broader token.
|
|
133
|
+
- `invalid_scope`: Accounts has not registered the requested permission.
|
|
198
134
|
- `insufficient_scope`: log in again with only the reported missing scope.
|
|
199
|
-
- `account_not_provisioned`: sign in to LixBlogs once
|
|
200
|
-
identity before retrying the CLI.
|
|
135
|
+
- `account_not_provisioned`: sign in to LixBlogs once, then retry.
|
|
201
136
|
- `precondition_failed`: fetch the current post, reconcile the retained
|
|
202
137
|
conflict copy, and retry with the new revision.
|
|
203
138
|
- `rate_limit_exceeded`: honor `Retry-After`; do not fan out retries.
|
|
204
|
-
-
|
|
205
|
-
|
|
206
|
-
## Development
|
|
207
|
-
|
|
208
|
-
```bash
|
|
209
|
-
npm test # runs the full CLI test suite
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
Tests exercise both a mocked auth provider and, where relevant, the real
|
|
213
|
-
OS keychain backend on whatever machine runs them — see
|
|
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`
|
|
216
|
-
for known platform-specific behavior (e.g. a documented WSL/keyring-rs quirk).
|
|
217
|
-
|
|
218
|
-
## Architecture
|
|
219
|
-
|
|
220
|
-
```
|
|
221
|
-
bin/lixblogs.mjs Source CLI entry point (Node's native util.parseArgs)
|
|
222
|
-
dist/lixblogs.mjs Minified executable shipped in the npm package
|
|
223
|
-
src/auth/ Accounts provider, development mock, refresh-safe
|
|
224
|
-
authenticated client, and production safety gate
|
|
225
|
-
src/commands/auth/ Command logic (login, status, logout, revoke) —
|
|
226
|
-
framework-agnostic, testable independently of the CLI shell
|
|
227
|
-
src/commands/blog/ Blog lifecycle commands and Markdown/editor input
|
|
228
|
-
src/api/ Versioned LixBlogs resource client and stable errors
|
|
229
|
-
src/content/ Dependency-free Markdown/block conversion
|
|
230
|
-
src/config/ Credential storage (real keychain + gated fallback),
|
|
231
|
-
profile registry, config resolution, token redaction
|
|
232
|
-
tests/ Full test suite
|
|
233
|
-
THREAT_MODEL.md Security threat model (repository documentation)
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
Command logic under `src/commands/` is deliberately decoupled from the CLI
|
|
237
|
-
parsing layer in `bin/`, so the parser (or any other interface built on top
|
|
238
|
-
of these commands later) can change without touching command logic or its
|
|
239
|
-
tests.
|
|
240
|
-
|
|
241
|
-
## Roadmap
|
|
242
|
-
|
|
243
|
-
See [#135](https://github.com/elixpo/blogs.elixpo/issues/135) for the full
|
|
244
|
-
scope. Rough remaining order:
|
|
245
|
-
|
|
246
|
-
1. Media commands
|
|
247
|
-
2. Interactive terminal UI and branding in a separate issue
|
|
248
|
-
|
|
249
|
-
## Contributing
|
|
250
|
-
|
|
251
|
-
This package is part of the [blogs.elixpo](https://github.com/elixpo/blogs.elixpo)
|
|
252
|
-
monorepo. See the root repository's contribution guidelines.
|
|
139
|
+
- Report the request ID, never a token or credential.
|
package/dist/lixblogs.mjs
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{parseArgs as xr}from"node:util";import{spawn as _r}from"node:child_process";var z={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??z.environment,o=t.profile??e.LIXBLOGS_PROFILE??z.profile,i=Ee[r]||Ee.production,n=t.authProvider??e.LIXBLOGS_AUTH_PROVIDER??(r==="production"?"elixpo":"mock"),a=t.accountsUrl??e.LIXBLOGS_ACCOUNTS_URL??z.accountsBaseUrl,s=t.apiUrl??e.LIXBLOGS_API_URL??z.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 R=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-"},Dt=5,Se=0;function Nt(t){return Se+=1,`${t}${Se}`}var G=class extends R{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=Nt(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:Dt}:{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",Bt=1,Mt=15e3,Ft={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(Ft[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 zt(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 Gt(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 V(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 V(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 J=class extends R{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=Mt}={}){if(super(),typeof n!="function")throw new TypeError("A fetch implementation is required.");this.accountsBaseUrl=Gt(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]!==Bt)throw new I("Accounts uses an unsupported device-flow contract version.");if(!zt(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 V(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 V(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 V(i,o)}}};var Pe="elixpo",le=class extends Error{constructor(e){super(e),this.name="ProductionAuthGateError"}};function Re({providerId:t,environment:e}){if(e==="production"&&t!==Pe)throw new le(`Provider "${t}" is not approved for production. Only "${Pe}" 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 G:new J({accountsBaseUrl:t.accountsBaseUrl,clientId:t.clientId,audience:t.audience,cliVersion:t.cliVersion||"1.2.0",fetchImpl:t.fetchImpl});return Re({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")}},H=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()]}},K=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 Vt}from"@napi-rs/keyring";var Jt="lixblogs-cli",Ht="__lixblogs_availability_probe__";function W(t){return new Vt(Jt,t)}async function Oe(){let t=W(Ht);try{return t.setPassword("probe"),t.deletePassword(),{available:!0}}catch(e){return{available:!1,error:String(e.message??e).split(`
|
|
3
|
-
`)[0].trim()}}}var
|
|
4
|
-
`),new H}let o=e||new g,i=new X(o);return new K(i)}var de="[REDACTED]",Xt=/token|refresh|secret|password|authorization/i,Yt=/^(mock-(access|refresh)-|Bearer\s+)\S+/i;function Le(t){return typeof t=="string"&&Yt.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))Xt.test(r)?e[r]=de:typeof o=="object"&&o!==null?e[r]=ue(o):e[r]=Le(o);return e}return Le(t)}function Z(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 De({credentialStore:t,profileId:e}){return await t.delete(e),{ok:!0}}async function Ne({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 Zt=6e4,ze=new WeakMap;function Qt(t){let e=ze.get(t);return e||(e=new Map,ze.set(t,e)),e}var Q=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."}},D=class{constructor({provider:e,credentialStore:r,profileId:o,apiBaseUrl:i="https://blogs.elixpo.com",fetchImpl:n=globalThis.fetch,refreshSkewMs:a=Zt}){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=Qt(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 Q(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 Q(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 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{payload:e,etag:t.headers.get("etag")}}var N=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 er(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 tr(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 ee=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 tr(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 te}from"node:crypto";async function rr(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 re=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 rr(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=te()}){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=te()}){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=te()}={}){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=te()}){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 or(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 oe=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),or(await this.http.request(`/api/v1/analytics?${o}`,{headers:{accept:"application/json"}}))}};var f=Object.freeze({OK:0,ERROR:1,USAGE:2,CONFLICT:3,AUTH:4,CONFIRMATION:5}),ir=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=ir[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=f.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
|
|
2
|
+
import{parseArgs as Mr}from"node:util";import{spawn as Fr}from"node:child_process";var V={environment:"production",profile:"default",accountsBaseUrl:"https://accounts.elixpo.com",apiBaseUrl:"https://blogs.elixpo.com"},Re={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 I({flags:t={},env:e=process.env}={}){let r=t.env??e.LIXBLOGS_ENV??V.environment,o=t.profile??e.LIXBLOGS_PROFILE??V.profile,i=Re[r]||Re.production,n=t.authProvider??e.LIXBLOGS_AUTH_PROVIDER??(r==="production"?"elixpo":"mock"),a=t.accountsUrl??e.LIXBLOGS_ACCOUNTS_URL??V.accountsBaseUrl,s=t.apiUrl??e.LIXBLOGS_API_URL??V.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 Pe={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-"},ir=5,Te=0;function nr(t){return Te+=1,`${t}${Te}`}var J=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=Pe[r]??Pe.APPROVE_IMMEDIATELY,i=nr(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:ir}:{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 Oe="urn:ietf:params:oauth:grant-type:device_code",sr=1,ar=15e3,lr={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."},x=class extends Error{constructor(e,{status:r=0,requiresLogin:o=!1}={}){super(lr[e]||"Authentication failed."),this.name="AuthProviderError",this.code=e||"authentication_failed",this.status=r,this.requiresLogin=o}},k=class extends Error{constructor(e){super(e),this.name="CompatibilityError",this.code="incompatible_accounts_contract"}};function de(t){return String(t||"0.0.0").split(".").slice(0,3).map(e=>Number.parseInt(e,10)||0)}function cr(t,e){let r=de(t),o=de(e);for(let i=0;i<3;i+=1)if(r[i]!==o[i])return r[i]>o[i];return!0}function ur(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 k("Accounts must use HTTPS outside local development.");return e.toString().replace(/\/$/,"")}async function F(t){try{return await t.json()}catch{throw new x("server_error",{status:t.status})}}function H(t,e){let r=typeof t?.error=="string"?t.error:"server_error";return new x(r,{status:e.status,requiresLogin:r==="invalid_grant"||r==="access_denied"||r==="expired_token"})}function je(t,e){if(!e.ok)throw H(t,e);if(typeof t?.access_token!="string"||typeof t?.refresh_token!="string"||!Number.isFinite(Number(t?.expires_in)))throw new x("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 W=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=ar}={}){if(super(),typeof n!="function")throw new TypeError("A fetch implementation is required.");this.accountsBaseUrl=ur(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 x("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 x("invalid_scope");return this._metadata}async _loadDiscovery(){let e=await this._fetch(`${this.accountsBaseUrl}/.well-known/oauth-authorization-server`),r=await F(e);if(!e.ok)throw new x("temporarily_unavailable",{status:e.status});if(de(r.elixpo_contract_version)[0]!==sr)throw new k("Accounts uses an unsupported device-flow contract version.");if(!cr(this.cliVersion,r.elixpo_min_compatible_cli_version))throw new k(`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(Oe))throw new k("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 k(`Accounts discovery is missing ${n}.`);let a=new URL(r[n]),s=new URL(this.accountsBaseUrl);if(a.origin!==s.origin)throw new k(`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 F(o);if(!o.ok)throw H(i,o);if(typeof i.device_code!="string"||typeof i.user_code!="string"||typeof i.verification_uri!="string")throw new x("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:Oe,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 F(i);if(i.ok)return{status:"approved",token:je(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 H(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 F(n);return je(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 F(o);throw H(i,o)}}};var Ce="elixpo",pe=class extends Error{constructor(e){super(e),this.name="ProductionAuthGateError"}};function Le({providerId:t,environment:e}){if(e==="production"&&t!==Ce)throw new pe(`Provider "${t}" is not approved for production. Only "${Ce}" may be used in production.`)}function T(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 J:new W({accountsBaseUrl:t.accountsBaseUrl,clientId:t.clientId,audience:t.audience,cliVersion:t.cliVersion||"1.2.0",fetchImpl:t.fetchImpl});return Le({providerId:e.providerId,environment:t.environment}),e}var S=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")}},X=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()]}},K=class extends A{constructor(e){super(),this._realStore=e}async get(e){try{return await this._realStore.get(e)}catch(r){throw new S(`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 S(`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 S(`OS keychain is unavailable: ${r.message}`)}}async listProfiles(){try{return await this._realStore.listProfiles()}catch(e){throw new S(`OS keychain is unavailable: ${e.message}`)}}};import{Entry as dr}from"@napi-rs/keyring";var pr="lixblogs-cli",fr="__lixblogs_availability_probe__";function Y(t){return new dr(pr,t)}async function De(){let t=Y(fr);try{return t.setPassword("probe"),t.deletePassword(),{available:!0}}catch(e){return{available:!1,error:String(e.message??e).split(`
|
|
3
|
+
`)[0].trim()}}}var fe=class extends A{async get(e){let r=Y(e),o;try{o=r.getPassword()}catch(i){if(Ue(i))return null;throw i}return o==null?null:JSON.parse(o)}async set(e,r){Y(e).setPassword(JSON.stringify(r))}async delete(e){let r=Y(e);try{r.deletePassword()}catch(o){if(Ue(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 Ue(t){let e=String(t?.message??"");return/no such|not found|nosuchkeyring|nosuchitem/i.test(e)}var Z=class extends A{constructor(e){super(),this._keychain=new fe,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 Q}from"node:fs";import Ne from"node:path";import mr from"node:os";function gr(){return Ne.join(mr.homedir(),".config","lixblogs","profiles.json")}var w=class{constructor(e=gr()){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){E(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 Q.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){E(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 Q.mkdir(Ne.dirname(this._path),{recursive:!0});let o=`${this._path}.${process.pid}.tmp`;await Q.writeFile(o,JSON.stringify({activeProfile:r,profiles:e},null,2),{encoding:"utf8",mode:384}),await Q.rename(o,this._path)}};function E(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 Be({allowInsecureFallback:t=!1,profileRegistry:e}={}){let r=await De();if(!r.available){if(!t)throw new S(`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 X}let o=e||new w,i=new Z(o);return new K(i)}var ge="[REDACTED]",hr=/token|refresh|secret|password|authorization/i,yr=/^(mock-(access|refresh)-|Bearer\s+)\S+/i;function Me(t){return typeof t=="string"&&yr.test(t)?ge:t}function me(t){if(Array.isArray(t))return t.map(e=>me(e));if(t&&typeof t=="object"){let e={};for(let[r,o]of Object.entries(t))hr.test(r)?e[r]=ge:typeof o=="object"&&o!==null?e[r]=me(o):e[r]=Me(o);return e}return Me(t)}function ee(t,e){return JSON.stringify(me(t),null,e)}function O(t){return typeof t!="string"?t:t.replace(/(mock-(access|refresh)-\S+|Bearer\s+\S+)/gi,ge)}async function Fe({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:O(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,d=Date.now()+l.expiresInSeconds*1e3;for(;Date.now()<d;){await a(u);let c;try{c=await t.pollDeviceCode({deviceCode:l.deviceCode})}catch(p){return{ok:!1,reason:O(p.message)}}if(c.status==="approved"){let p=r;if(n)try{p=await n({accessToken:c.token.accessToken,requestedProfileId:r})}catch(v){return{ok:!1,reason:O(v.message)}}return await e.set(p,{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:p}}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 ze({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 Ge({credentialStore:t,profileId:e}){return await t.delete(e),{ok:!0}}async function Ve({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 Je({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 He({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 We({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 E(n.data.username)}var wr=6e4,Xe=new WeakMap;function br(t){let e=Xe.get(t);return e||(e=new Map,Xe.set(t,e)),e}var te=class extends Error{constructor(e){super(`Profile "${e}" needs to log in again.`),this.name="LoginRequiredError",this.code="login_required"}},he=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."}},j=class{constructor({provider:e,credentialStore:r,profileId:o,apiBaseUrl:i="https://blogs.elixpo.com",fetchImpl:n=globalThis.fetch,refreshSkewMs:a=wr}){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=br(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 x&&s.requiresLogin?(await this.credentialStore.delete(this.profileId),new te(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 te(this.profileId);return e||r.expiresAt-Date.now()<=this.refreshSkewMs?this._refresh(r,{force:e}):r}async request(e,r={}){let o=await this.requestRaw(e,r),i=o.headers.get("content-type")||"";if(!i.toLowerCase().includes("application/json"))throw new he(o.status,i);return o}async requestRaw(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();return a.status===401&&(i=await this.credentials({forceRefresh:!0}),a=await n()),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 Ke}from"node:crypto";var h=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 vr(t){let e;try{e=await t.json()}catch{e=null}if(!t.ok||e?.error)throw new h(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 z=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 vr(s)}catch(s){if(!n||a>0||s instanceof h||s?.code)throw s;await this.sleep(250)}throw new h("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=Ke()}={}){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,status:o="published",idempotencyKey:i=Ke()}){return await this.requireScopes(["lixblogs:blog:publish"]),(await this.request(`/api/v1/blogs/${encodeURIComponent(e)}/publish`,{method:"POST",headers:{"if-match":r,"idempotency-key":i},body:JSON.stringify({status: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 versions(e){return await this.requireScopes(["lixblogs:blog:read"]),(await this.request(`/api/v1/blogs/${encodeURIComponent(e)}/versions`)).payload.data}async restoreVersion(e,r,{etag:o}){return await this.requireScopes(["lixblogs:blog:write"]),(await this.request(`/api/v1/blogs/${encodeURIComponent(e)}/versions`,{method:"POST",headers:{"if-match":o},body:JSON.stringify({versionId:r})})).payload.data}async comments(e){return await this.requireScopes(["lixblogs:blog:read"]),(await this.request(`/api/v1/blogs/${encodeURIComponent(e)}/comments`)).payload.data}async comment(e,r,{parentId:o}={}){return await this.requireScopes(["lixblogs:blog:write"]),(await this.request(`/api/v1/blogs/${encodeURIComponent(e)}/comments`,{method:"POST",body:JSON.stringify({content:r,parentId:o})})).payload.data}async deleteComment(e,r){return await this.requireScopes(["lixblogs:blog:write"]),(await this.request(`/api/v1/blogs/${encodeURIComponent(e)}/comments/${encodeURIComponent(r)}`,{method:"DELETE"})).payload.data}};async function xr(t){let e;try{e=await t.json()}catch{e=null}if(!t.ok||e?.error)throw new h(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 re=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 xr(s)}catch(s){if(!n||a>0||s instanceof h||s?.code)throw s;await this.sleep(250)}throw new h("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 oe}from"node:crypto";async function Ir(t){let e;try{e=await t.json()}catch{e=null}if(!t.ok||e?.error)throw new h(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 ie=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 Ir(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=oe()}){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=oe()}){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=oe()}={}){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=oe()}){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 h(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 ne=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 se=class{constructor(e){this.http=e}async requireScopes(e){typeof this.http.requireScopes=="function"&&await this.http.requireScopes(e)}async _request(e,r={}){let o=await this.http.request(e,r),i;try{i=await o.json()}catch{i=null}if(!o.ok||i?.error){let n=new Error(i?.error?.message||`Request failed with HTTP ${o.status}`);throw n.code=i?.error?.code||`http_${o.status}`,n.status=o.status,n.requestId=i?.error?.requestId||o.headers.get("x-request-id")||null,n.details=i?.error?.details||null,n}return i.data}async cloudinaryStatus(){return await this.requireScopes(["lixblogs:integrations:cloudinary:read"]),this._request("/api/v1/integrations/cloudinary")}async cloudinaryDisconnect(){return await this.requireScopes(["lixblogs:integrations:cloudinary:disconnect"]),this._request("/api/v1/integrations/cloudinary",{method:"DELETE"})}async pollinationsStatus({refresh:e=!1}={}){return await this.requireScopes(["lixblogs:media:read"]),this._request(`/api/v1/integrations/pollinations${e?"?refresh=1":""}`)}async pollinationsDisconnect(){return await this.requireScopes(["lixblogs:media:write"]),this._request("/api/v1/integrations/pollinations",{method:"DELETE"})}};import{randomUUID as Ye}from"node:crypto";var _r=Object.freeze({"image/avif":"avif","image/bmp":"bmp","image/jpeg":"jpg","image/png":"png","image/svg+xml":"svg","image/webp":"webp"});async function ye(t){let e=await t.json().catch(()=>({})),r=new Error(e.error?.message||e.error||`Media request failed with HTTP ${t.status}`);return r.code=e.error?.code||e.code||`http_${t.status}`,r.status=t.status,r}var ae=class{constructor(e){this.http=e}async generate({prompt:e,model:r="flux",seed:o,width:i,height:n,destination:a="inline",generationId:s=Ye(),reference:l}){await this.http.requireScopes(["lixblogs:media:write"]);let u,d;if(l){u=new FormData;for(let[p,v]of Object.entries({prompt:e,model:r,seed:o,width:i,height:n,destination:a,generationId:s}))v!==void 0&&u.append(p,String(v));u.append("referenceImage",new Blob([l.bytes],{type:l.mimeType}),l.name||"reference-image"),d={accept:"image/*, application/json"}}else u=JSON.stringify({prompt:e,model:r,seed:o,width:i,height:n,destination:a,generationId:s}),d={"content-type":"application/json",accept:"image/*, application/json"};let c=await this.http.requestRaw("/api/v1/media/generate",{method:"POST",headers:d,body:u});if(!c.ok)throw await ye(c);return{bytes:new Uint8Array(await c.arrayBuffer()),mimeType:c.headers.get("content-type")||"image/jpeg",generationId:s}}async upload({bytes:e,mimeType:r,blogId:o,mediaType:i="inline",uploadId:n=Ye()}){await this.http.requireScopes(["lixblogs:media:write"]);let a=new FormData,s=_r[r];if(!s)throw new Error(`Unsupported image MIME type: ${r}`);a.append("file",new Blob([e],{type:r}),`lixblogs-${n}.${s}`),a.append("type",i),a.append("uploadId",n),o&&a.append("blogId",o);let l=await this.http.requestRaw("/api/v1/media/upload",{method:"POST",body:a,headers:{accept:"application/json"}});if(!l.ok)throw await ye(l);return l.json()}async delete(e){if(!e)throw new Error("A media ID is required.");await this.http.requireScopes(["lixblogs:media:write"]);let r=await this.http.request(`/api/v1/media/${encodeURIComponent(e)}`,{method:"DELETE"});if(!r.ok)throw await ye(r);let o=await r.json();return o.data||o}};var m=Object.freeze({OK:0,ERROR:1,USAGE:2,CONFLICT:3,AUTH:4,CONFIRMATION:5}),kr=Object.freeze({login:["auth","login"],logout:["auth","logout"],whoami:["auth","whoami"],profiles:["auth","profiles"],use:["auth","use"]});function Ze(t){let[e,...r]=t,o=kr[e];return o?[...o,...r]:t}function Qe(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 g(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=m.CONFIRMATION,r}var q=Object.freeze({reset:"\x1B[0m",bold:"\x1B[1m",dim:"\x1B[2m",violet:"\x1B[38;5;141m",green:"\x1B[38;5;42m"});function we(t=process.stdout,e=process.env){return!!t.isTTY&&e.NO_COLOR===void 0&&e.TERM!=="dumb"}function R(t,e,r){return r?`${e}${t}${q.reset}`:t}function et({url:t,code:e,expiresInSeconds:r,profile:o,interactive:i,color:n=!1}){let a=`${R("\u25C6",q.violet,n)} ${R("LixBlogs",q.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}`,` ${R("Device login",q.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 ${R(e,q.bold,n)}`,` Expires ${Math.ceil(r/60)} min`,o?` Profile ${o} ${R("(local credential slot)",q.dim,n)}`:` Profile ${R("your Accounts username after approval",q.dim,n)}`,"",` ${s}`," No localhost callback or exposed port is required.",""].join(`
|
|
5
|
+
`)}function tt(t,e=!1){return` ${R("\u2713",q.green,e)} ${t}`}function rt({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 C(t){return[{type:"text",text:t}]}function ot(t){let e=String(t||"").replace(/\r\n/g,`
|
|
6
6
|
`).split(`
|
|
7
|
-
`),r=[],o=[],i=()=>{o.length&&(r.push({type:"paragraph",content:
|
|
8
|
-
`)}}:{type:"codeBlock",props:{language:l[1].toLowerCase()},content:
|
|
9
|
-
`))});continue}let u=s.match(/^(#{1,3})\s+(.+)/);if(u){i(),r.push({type:"heading",props:{level:String(u[1].length)},content:
|
|
7
|
+
`),r=[],o=[],i=()=>{o.length&&(r.push({type:"paragraph",content:C(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 ue=[];for(n+=1;n<e.length&&!/^```/.test(e[n].trim());)ue.push(e[n++]);r.push(l[1].toLowerCase()==="mermaid"?{type:"mermaidBlock",props:{diagram:ue.join(`
|
|
8
|
+
`)}}:{type:"codeBlock",props:{language:l[1].toLowerCase()},content:C(ue.join(`
|
|
9
|
+
`))});continue}let u=s.match(/^(#{1,3})\s+(.+)/);if(u){i(),r.push({type:"heading",props:{level:String(u[1].length)},content:C(u[2])});continue}let d=s.match(/^[-*]\s+(.+)/);if(d){i(),r.push({type:"bulletListItem",content:C(d[1])});continue}let c=s.match(/^\d+\.\s+(.+)/);if(c){i(),r.push({type:"numberedListItem",content:C(c[1])});continue}let p=s.match(/^>\s?(.*)/);if(p){i(),r.push({type:"quote",content:C(p[1])});continue}let v=s.match(/^!\[([^\]]*)\]\((https:\/\/[^)]+)\)$/);if(v){i(),r.push({type:"image",props:{url:v[2],caption:v[1]}});continue}if(/^([-*_])\1{2,}$/.test(s)){i(),r.push({type:"divider"});continue}o.push(s)}return i(),r}function Sr(t){return(t?.content||[]).map(e=>typeof e=="string"?e:e?.text||"").join("")}function le(t){return(t||[]).map(e=>{let r=Sr(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
10
|
${r}
|
|
11
11
|
\`\`\``:e.type==="mermaidBlock"?`\`\`\`mermaid
|
|
12
12
|
${e.props?.diagram||""}
|
|
13
13
|
\`\`\``:e.type==="image"?``:e.type==="divider"?"---":r}).join(`
|
|
14
14
|
|
|
15
|
-
`)}import{promises as B}from"node:fs";import{tmpdir as sr}from"node:os";import he from"node:path";import{spawn as ar}from"node:child_process";async function lr(t){let e="";t.setEncoding("utf8");for await(let r of t)e+=r;return e}async function cr(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(sr(),"lixblogs-")),o=he.join(r,"post.md");await B.writeFile(o,t,{mode:384});try{return await new Promise((i,n)=>{let a=ar(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 lr(e):t.content!==void 0?i=t.content:i=await cr(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 ur(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 ne(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(ur(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:ie(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 ne(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:ie(i.content)}),a={...ge(r),...n?{content:n.blocks}:{}};if(!Object.keys(a).length)throw new Error("No blog changes were provided.");if(ne(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,ie(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 ne(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 dr,cp as pr,readFile as fr,readdir as hr}from"node:fs/promises";import b from"node:path";import{fileURLToPath as mr}from"node:url";var xe=b.dirname(mr(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 dr(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 fr(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 hr(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 pr(i,a,{recursive:!0,force:!!e.force}),{installed:!0,name:o,target:a}}import{writeFile as gr}from"node:fs/promises";var wr=new Set(["overview","timeline","posts","sources","devices","countries"]),yr=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(!wr.has(e))throw new Error(`Unsupported analytics dimension: ${e}.`);if(!yr.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=
|
|
17
|
-
${n.map(s=>a.map(l=>
|
|
15
|
+
`)}import{promises as G}from"node:fs";import{tmpdir as qr}from"node:os";import be from"node:path";import{spawn as $r}from"node:child_process";async function Ar(t){let e="";t.setEncoding("utf8");for await(let r of t)e+=r;return e}async function Rr(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 G.mkdtemp(be.join(qr(),"lixblogs-")),o=be.join(r,"post.md");await G.writeFile(o,t,{mode:384});try{return await new Promise((i,n)=>{let a=$r(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 G.readFile(o,"utf8")}finally{await G.rm(r,{recursive:!0,force:!0})}}async function ve(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 G.readFile(be.resolve(t.file),"utf8"):t.stdin?i=await Ar(e):t.content!==void 0?i=t.content:i=await Rr(r),{markdown:i,blocks:ot(i)}}function xe(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),t["allow-comments"]&&(e.allowComments=!0),t["no-comments"]&&(e.allowComments=!1),(t["cover-x"]!==void 0||t["cover-y"]!==void 0)&&(e.coverPosition={x:Number(t["cover-x"]??50),y:Number(t["cover-y"]??50)}),t["cover-zoom"]!==void 0&&(e.coverZoom=Number(t["cover-zoom"])),e}function Pr(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 ce(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(Pr(t.content)<20)throw new Error("A post needs at least 20 words before publishing.")}return t}import{promises as Ie}from"node:fs";import Ee from"node:path";async function it({client:t,options:e}){return t.list({status:e.status,limit:e.limit,cursor:e.cursor})}async function _e({client:t,id:e}){if(!e)throw new Error("A blog ID is required.");let r=await t.get(e);return{...r,markdown:le(r.content)}}async function nt({client:t,options:e,stdin:r}){let o=await ve(e,{stdin:r}),i={...xe(e),content:o?.blocks||[]};return ce(i),e["dry-run"]?{dryRun:!0,input:i,markdown:o?.markdown||""}:t.create(i,{idempotencyKey:e["idempotency-key"]})}async function st({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 ve(r,{stdin:o,initial:le(i.content)}),a={...xe(r),...n?{content:n.blocks}:{}};if(!Object.keys(a).length)throw new Error("No blog changes were provided.");if(ce(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 h)||s.code!=="revision_conflict")throw s;let l=await t.get(e),u=r.conflictDirectory||Ee.resolve(".lixblogs-conflicts");await Ie.mkdir(u,{recursive:!0});let d=e.replace(/[^A-Za-z0-9._-]/g,"_"),c=Ee.join(u,`${d}-local.json`),p=Ee.join(u,`${d}-server.md`);throw await Promise.all([Ie.writeFile(c,JSON.stringify(a,null,2),{mode:384}),Ie.writeFile(p,le(l.content),{mode:384})]),s.details={...s.details,localPath:c,serverPath:p,serverEtag:l.etag},s}}async function at({client:t,id:e,options:r}){if(!e)throw new Error("A blog ID is required.");let o=await t.get(e);ce(o,{publishing:!0});let i=r.status||"published";if(!["published","unlisted"].includes(i))throw new Error("--status must be published or unlisted.");return r["dry-run"]?{dryRun:!0,id:e,from:o.status,to:i}:(g(r,"Publishing this blog"),t.publish(e,{etag:r.etag||o.etag,status:i,idempotencyKey:r["idempotency-key"]}))}async function lt({client:t,id:e}){if(!e)throw new Error("A blog ID is required.");return{data:await t.versions(e)}}async function ct({client:t,id:e,options:r}){if(!e||!r.version)throw new Error("A blog ID and --version are required.");g(r,"Restoring this historical version");let o=await t.get(e);return t.restoreVersion(e,r.version,{etag:r.etag||o.etag})}async function ut({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"}:(g(r,"Unpublishing this blog"),t.unpublish(e,{etag:r.etag||o.etag}))}async function ke({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 dt({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"}:(g(r,"Restoring this blog"),t.restore(e,{etag:r.etag||o.etag}))}async function pt({client:t}){return t.list()}async function ft({client:t,id:e}){if(!e)throw new Error("An organization ID or handle is required.");return t.get(e)}async function mt({client:t,id:e}){if(!e)throw new Error("An organization ID or handle is required.");return t.collections(e)}async function gt({client:t,id:e}){if(!e)throw new Error("An organization ID or handle is required.");return t.members(e)}async function ht({client:t}){return t.targets()}function L(t){if(!t)throw new Error("A blog ID is required.")}async function yt({client:t,id:e}){return L(e),t.list(e)}async function wt({client:t}){return t.invitations()}async function bt({client:t,id:e,options:r}){if(L(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}:(g(r,"Inviting this collaborator"),t.invite(e,{user:r.user,role:r.role,idempotencyKey:r["idempotency-key"]}))}async function vt({client:t,id:e,options:r}){if(L(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}:(g(r,"Changing this collaborator role"),t.role(e,{user:r.user,role:r.role,idempotencyKey:r["idempotency-key"]}))}async function xt({client:t,id:e,options:r}){return L(e),r["dry-run"]?{dryRun:!0,action:"remove",blogId:e,user:r.user||"self"}:(g(r,"Removing this collaborator or invitation"),t.remove(e,{user:r.user,idempotencyKey:r["idempotency-key"]}))}async function It({client:t,id:e,options:r}){return L(e),r["dry-run"]?{dryRun:!0,action:"accept",blogId:e,showOnProfile:!r["hide-on-profile"]}:(g(r,"Accepting this collaboration invitation"),t.resolveInvitation(e,{action:"accept",showOnProfile:!r["hide-on-profile"],idempotencyKey:r["idempotency-key"]}))}async function Et({client:t,id:e,options:r}){return L(e),r["dry-run"]?{dryRun:!0,action:"decline",blogId:e}:(g(r,"Declining this collaboration invitation"),t.resolveInvitation(e,{action:"decline",idempotencyKey:r["idempotency-key"]}))}import{access as Tr,cp as Or,readFile as jr,readdir as Cr}from"node:fs/promises";import b from"node:path";import{fileURLToPath as Lr}from"node:url";var Se=b.dirname(Lr(import.meta.url)),St=b.basename(Se)==="dist"?b.resolve(Se,".."):b.resolve(Se,"../../.."),_t=b.join(St,"skills"),kt=b.resolve(St,"../..",".agents","skills");async function U(t){try{return await Tr(t),!0}catch{return!1}}async function qe(){if(await U(_t))return _t;if(await U(kt))return kt;let t=new Error("No bundled LixBlogs skills were found. Reinstall @elixpo/lixblogs-cli.");throw t.code="skills_unavailable",t}function qt(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 $t(t,e){let r=await jr(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 At(){let t=await qe(),e=await Cr(t,{withFileTypes:!0});return Promise.all(e.filter(r=>r.isDirectory()&&r.name.startsWith("lixblogs-")).map(r=>$t(t,r.name))).then(r=>r.map(({content:o,...i})=>i).sort((o,i)=>o.name.localeCompare(i.name)))}async function Rt({name:t}){let e=await qe(),r=qt(t);if(!await U(b.join(e,r,"SKILL.md"))){let o=new Error(`Skill "${r}" is not bundled.`);throw o.code="skill_not_found",o}return $t(e,r)}async function Pt({name:t,options:e}){let r=await qe(),o=qt(t),i=b.join(r,o);if(!await U(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 U(a)};if(await U(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}g(e,`Replacing ${a}`)}else g(e,`Installing ${o} into ${n}`);return await Or(i,a,{recursive:!0,force:!!e.force}),{installed:!0,name:o,target:a}}import{writeFile as Ur}from"node:fs/promises";var Dr=new Set(["overview","timeline","posts","sources","devices","countries"]),Nr=new Set(["7d","30d","90d","12m","custom"]);function jt(t={}){let e=t.dimension||"overview",r=t.range||(t.from||t.to?"custom":"30d");if(!Dr.has(e))throw new Error(`Unsupported analytics dimension: ${e}.`);if(!Nr.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 Ct({client:t,options:e}){return t.query(jt(e))}function Tt(t){let e=t==null?"":typeof t=="object"?JSON.stringify(t):String(t);return/[",\n]/.test(e)?`"${e.replaceAll('"','""')}"`:e}function Ot(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 Lt({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(jt(e)),i;if(r==="json")i=`${JSON.stringify(o,null,2)}
|
|
16
|
+
`;else{let n=Ot(o),a=[...new Set(n.flatMap(s=>Object.keys(s)))];i=`${a.map(Tt).join(",")}
|
|
17
|
+
${n.map(s=>a.map(l=>Tt(s[l])).join(",")).join(`
|
|
18
18
|
`)}
|
|
19
|
-
`}return await
|
|
19
|
+
`}return await Ur(e.output,i,{encoding:"utf8",flag:"wx"}),{output:e.output,format:r,rows:Ot(o).length}}async function Ut({integrationsClient:t,confirmed:e}){if(e!==!0)return{ok:!1,reason:"Disconnect was not confirmed. This is a destructive action and requires explicit confirmation (interactive prompt, or --yes in a non-interactive session)."};try{return{ok:!0,data:await t.cloudinaryDisconnect()}}catch(r){return{ok:!1,error:r}}}async function Dt({integrationsClient:t}){try{return{ok:!0,data:await t.cloudinaryStatus()}}catch(e){return{ok:!1,error:e}}}import{randomUUID as Nt}from"node:crypto";import{promises as $e}from"node:fs";import D from"node:path";function Br(t){let e=r=>t[r]===void 0?void 0:Number.parseInt(t[r],10);return{width:e("width"),height:e("height"),seed:e("seed")}}var Bt=Object.freeze({".avif":"image/avif",".bmp":"image/bmp",".jpeg":"image/jpeg",".jpg":"image/jpeg",".png":"image/png",".svg":"image/svg+xml",".webp":"image/webp"});async function Mt({blogClient:t,blogId:e,media:r,type:o,caption:i}){if(!e)return null;let n=await t.get(e);if(o==="cover")return t.update(e,{coverUrl:r.url},{etag:n.etag});let a=[...n.content||[],{id:Nt(),type:"image",props:{url:r.url,caption:i||"",_mediaId:r.id||""},content:[],children:[]}];return t.update(e,{content:a},{etag:n.etag})}async function Ft({mediaClient:t,blogClient:e,options:r}){let o=r.prompt?.trim();if(!o)throw new Error("--prompt is required.");let i=r.type||"inline";if(!["inline","cover"].includes(i))throw new Error("--type must be inline or cover.");let n;if(r.reference){let c=D.resolve(r.reference),p=D.extname(c).toLowerCase(),v=Bt[p];if(!v)throw new Error("Unsupported reference image type. Use AVIF, BMP, JPEG, PNG, SVG, or WebP.");n={bytes:await $e.readFile(c),mimeType:v,name:D.basename(c)}}let a=await t.generate({prompt:o,model:r.model||"flux",destination:i,reference:n,...Br(r)}),s=a.mimeType.includes("png")?"png":a.mimeType.includes("webp")?"webp":"jpg",l=D.resolve(r.output||`lixblogs-${a.generationId}.${s}`);await $e.writeFile(l,a.bytes,{mode:384});let u=null,d=null;return r.blog&&(u=await t.upload({bytes:a.bytes,mimeType:a.mimeType,blogId:r.blog,mediaType:i,uploadId:a.generationId}),r.attach&&(d=await Mt({blogClient:e,blogId:r.blog,media:u,type:i,caption:r.caption}))),{generationId:a.generationId,output:l,mimeType:a.mimeType,media:u,blog:d}}async function zt({mediaClient:t,blogClient:e,options:r}){if(!r.file)throw new Error("--file is required.");if(!r.blog)throw new Error("--blog is required.");let o=r.type||"inline",i=D.resolve(r.file),n=await $e.readFile(i),a=D.extname(i).toLowerCase(),s=Bt[a];if(!s)throw new Error("Unsupported image type. Use AVIF, BMP, JPEG, PNG, SVG, or WebP.");let l=await t.upload({bytes:n,mimeType:s,blogId:r.blog,mediaType:o,uploadId:r["upload-id"]||Nt()}),u=r.attach?await Mt({blogClient:e,blogId:r.blog,media:l,type:o,caption:r.caption}):null;return{media:l,blog:u}}async function Gt({mediaClient:t,id:e,options:r}){if(!e)throw new Error("A media ID is required.");g(r,"Deleting this media asset from its storage provider");let o=await t.delete(e);return o?.data||o}async function Vt({client:t,id:e}){if(!e)throw new Error("A blog ID is required.");return t.comments(e)}async function Jt({client:t,id:e,options:r}){if(!e)throw new Error("A blog ID is required.");if(!r.content?.trim())throw new Error("--content is required.");return t.comment(e,r.content.trim())}async function Ht({client:t,id:e,options:r}){if(!e||!r.parent)throw new Error("A blog ID and --parent comment ID are required.");if(!r.content?.trim())throw new Error("--content is required.");return t.comment(e,r.content.trim(),{parentId:r.parent})}async function Wt({client:t,id:e,options:r}){if(!e||!r.comment)throw new Error("A blog ID and --comment ID are required.");return g(r,"Deleting this comment"),t.deleteComment(e,r.comment)}var zr={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},prompt:{type:"string"},reference:{type:"string"},model:{type:"string"},seed:{type:"string"},width:{type:"string"},height:{type:"string"},blog:{type:"string"},type:{type:"string"},attach:{type:"boolean",default:!1},caption:{type:"string"},"upload-id":{type:"string"},version:{type:"string"},parent:{type:"string"},comment:{type:"string"},"allow-comments":{type:"boolean",default:!1},"no-comments":{type:"boolean",default:!1},"cover-x":{type:"string"},"cover-y":{type:"string"},"cover-zoom":{type:"string"},help:{type:"boolean",short:"h",default:!1}},Gr=`lixblogs \u2014 LixBlogs CLI
|
|
20
20
|
|
|
21
21
|
Usage:
|
|
22
22
|
lixblogs login [--profile <name>] [--open]
|
|
@@ -41,6 +41,12 @@ Usage:
|
|
|
41
41
|
lixblogs blog delete <id> --yes [--permanent] [--dry-run] [--json]
|
|
42
42
|
lixblogs blog trash <id> --yes [--dry-run] [--json]
|
|
43
43
|
lixblogs blog restore <id> --yes [--dry-run] [--json]
|
|
44
|
+
lixblogs blog history <id> [--json]
|
|
45
|
+
lixblogs blog restore-version <id> --version <version-id> --yes [--json]
|
|
46
|
+
lixblogs comment list <blog-id> [--json]
|
|
47
|
+
lixblogs comment add <blog-id> --content <text> [--json]
|
|
48
|
+
lixblogs comment reply <blog-id> --parent <comment-id> --content <text> [--json]
|
|
49
|
+
lixblogs comment delete <blog-id> --comment <comment-id> --yes [--json]
|
|
44
50
|
lixblogs org list [--json]
|
|
45
51
|
lixblogs org get <id> [--json]
|
|
46
52
|
lixblogs org collections <id> [--json]
|
|
@@ -55,10 +61,17 @@ Usage:
|
|
|
55
61
|
lixblogs collab decline <blog-id> --yes
|
|
56
62
|
lixblogs analytics query [--scope personal|org:<id>] [--range 30d] [--dimension overview]
|
|
57
63
|
lixblogs analytics export --output <file> [--format json|csv] [query options]
|
|
64
|
+
lixblogs integrations cloudinary-status [--json]
|
|
65
|
+
lixblogs integrations cloudinary-disconnect --yes [--json]
|
|
66
|
+
lixblogs integrations pollinations-status [--json]
|
|
67
|
+
lixblogs integrations pollinations-disconnect --yes [--json]
|
|
68
|
+
lixblogs media generate --prompt <text> [--model flux] [--reference <image>] [--blog <id> --type inline|cover --attach] [--output <file>]
|
|
69
|
+
lixblogs media upload --file <image> --blog <id> [--type inline|cover] [--attach]
|
|
70
|
+
lixblogs media delete <media-id> --yes [--json]
|
|
58
71
|
lixblogs skill list [--json]
|
|
59
72
|
lixblogs skill inspect <name> [--json]
|
|
60
73
|
lixblogs skill install <name> [--target <directory>] [--dry-run] --yes
|
|
61
|
-
lixblogs disconnect cloudinary
|
|
74
|
+
lixblogs disconnect cloudinary --yes
|
|
62
75
|
lixblogs disconnect pollinations
|
|
63
76
|
|
|
64
77
|
Global flags:
|
|
@@ -89,16 +102,16 @@ Global flags:
|
|
|
89
102
|
Machine mode:
|
|
90
103
|
--json --no-input produces stable JSON on stdout, diagnostics on stderr, and
|
|
91
104
|
never prompts. Publishing and destructive state changes require --yes.
|
|
92
|
-
`,
|
|
93
|
-
`)}function
|
|
105
|
+
`,Vr=["openid","profile","email","lixblogs:profile:read","lixblogs:profile:write","lixblogs:blog:read","lixblogs:blog:write","lixblogs:blog:publish","lixblogs:blog:delete","lixblogs:media:read","lixblogs:media:write","lixblogs:organizations:read","lixblogs:organizations:write","lixblogs:collaboration:read","lixblogs:collaboration:write","lixblogs:analytics:read","lixblogs:notifications:read"];function $(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 B(t,e){return t.profileExplicit?E(t.profile):await e.getActive()||E(t.profile)}async function Ae(t){let e=process.platform==="darwin"?"open":process.platform==="win32"?"cmd":"xdg-open",r=process.platform==="win32"?["/c","start","",t]:[t],o=Fr(e,r,{detached:!0,stdio:"ignore"});o.on("error",()=>{}),o.unref()}function y(t,e){t.json&&process.stdout.write(ee(e)+`
|
|
106
|
+
`)}function f(t,e,r=m.ERROR){let o=e&&typeof e=="object"?e:{message:String(e)},i=O(o.message),n=Qe({...o,message:i});t.json?process.stdout.write(ee(n)+`
|
|
94
107
|
`):t.quiet||(process.stderr.write(`Error: ${i}
|
|
95
108
|
`),o.hint&&process.stderr.write(`Hint: ${o.hint}
|
|
96
109
|
`),o.requestId&&process.stderr.write(`Request: ${o.requestId}
|
|
97
|
-
`)),process.exitCode=o.exitCode||r}async function
|
|
98
|
-
${c.markdown||""}`)
|
|
99
|
-
`),process.exitCode=c.status===412?3:1;return}return
|
|
100
|
-
`),process.exitCode=
|
|
101
|
-
`),process.stderr.write(`Available categories: ${Object.keys(
|
|
102
|
-
`),process.exitCode=
|
|
110
|
+
`)),process.exitCode=o.exitCode||r}async function _(t,e){try{return await Be({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 Kt(t){let e=I({flags:$(t)}),r=new w,o=E(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=T(e)}catch(u){return f(t,u.message)}let a=await _(t,r);if(!a)return;let s=()=>{},l;try{l=await Fe({provider:n,credentialStore:a,profileId:o,scopes:i,openBrowser:t.open?Ae:void 0,resolveProfileId:e.profileExplicit?void 0:({accessToken:u})=>We({accessToken:u,apiBaseUrl:e.apiBaseUrl}),onStatus:u=>{if(t.json){u.type!=="pending"&&y(t,{event:u.type,...u});return}if(!t.quiet)if(u.type==="verification_pending"){let d=u.verificationUriComplete||u.verificationUri,c=!!process.stdin.isTTY&&!t["no-input"];process.stdout.write(et({url:d,code:u.userCode,expiresInSeconds:u.expiresInSeconds,profile:e.profileExplicit?o:null,interactive:c,color:we()})),c&&!t.open&&(s=rt({input:process.stdin,open:Ae,url:d}))}else u.type==="approved"?console.log(tt("Access approved by Elixpo Accounts.",we())):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),y(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 Jr(t){let e=I({flags:$(t)}),r=new w,o=await B(e,r),i=await _(t,r);if(!i)return;let n=await ze({credentialStore:i,profileId:o});if(y(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 M(t){let e=I({flags:$(t)}),r=new w,o=await B(e,r),i=await _(t,r);if(!i)return null;let n;try{n=T(e)}catch(s){return f(t,s),null}let a=new j({provider:n,credentialStore:i,profileId:o,apiBaseUrl:e.apiBaseUrl});return{client:new z(a),http:a,config:e,credentialStore:i,profileId:o}}async function Hr(t){let e=await M(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};y(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?m.AUTH:m.ERROR)}}async function Wr(t){let e=I({flags:$(t)}),r=new URL("/register",e.accountsBaseUrl).toString();if(t["no-input"]){y(t,{ok:!0,registrationUrl:r,next:"lixblogs login"}),!t.json&&!t.quiet&&console.log(r);return}await Ae(r),t.quiet||console.log(`Create your account at ${r}, then approve the device login.`),await Kt(t)}async function Xr(t){let e=I({flags:$(t)}),r=new w,o=await B(e,r),i=await _(t,r);if(!i)return;let n=await Ge({credentialStore:i,profileId:o});y(t,n),!t.json&&!t.quiet&&console.log(`Logged out profile "${o}".`)}async function Kr(t){let e=I({flags:$(t)}),r=new w,o=await B(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=T(e)}catch(s){return f(t,s.message)}let n=await _(t,r);if(!n)return;let a=await Ve({provider:i,credentialStore:n,profileId:o,confirmed:!0});if(!a.ok)return f(t,a.reason);y(t,a),!t.json&&!t.quiet&&console.log(`Revoked and logged out profile "${o}".`)}async function N(t,e,r){let o=I({flags:$(t)}),i=new w,n=await B(o,i);if((r==="cloudinary-disconnect"||r==="pollinations-disconnect")&&!t.yes)return f(t,"This is a destructive action. Re-run with --yes to confirm (interactive confirmation prompt not yet implemented).");let a;try{a=T(o)}catch(c){return f(t,c.message)}let s=await _(t,i);if(!s)return;let l=new j({provider:a,credentialStore:s,profileId:n,apiBaseUrl:o.apiBaseUrl}),u=new se(l),d;if(r==="cloudinary-status")d=await Dt({integrationsClient:u});else if(r==="cloudinary-disconnect")d=await Ut({integrationsClient:u,confirmed:!0});else try{d={ok:!0,data:r==="pollinations-status"?await u.pollinationsStatus({refresh:t.force}):await u.pollinationsDisconnect()}}catch(c){d={ok:!1,error:c}}if(!d.ok)return f(t,d.error||d.reason);y(t,d),!t.json&&!t.quiet&&console.log(r==="cloudinary-status"?`Cloudinary: ${d.data.connected?`connected (${d.data.cloudName})`:"not connected"}`:r==="pollinations-status"?`Pollinations: ${d.data.connected?`connected${d.data.handle?` as ${d.data.handle}`:""} \xB7 ${d.data.balance??"unknown"} Pollen`:`${d.data.status}. Connect at ${d.data.connectUrl||"https://blogs.elixpo.com/settings?tab=integrations"}`}`:`${r.startsWith("pollinations")?"Pollinations":"Cloudinary"} connection disconnected.`)}async function Yr(t){let e=new w,r=await _(t,e);if(!r)return;let o=await Je({credentialStore:r,profileRegistry:e});if(y(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 Zr(t,e){let r;try{r=E(e[0])}catch(a){return f(t,a.message)}let o=new w,i=await _(t,o);if(!i)return;let n=await He({credentialStore:i,profileRegistry:o,profileId:r});if(!n.ok)return f(t,n.reason);y(t,n),!t.json&&!t.quiet&&console.log(`Using profile "${r}".`)}var Yt={list:it,get:_e,preview:_e,create:nt,edit:st,publish:at,unpublish:ut,delete:ke,trash:ke,restore:dt,history:lt,"restore-version":ct},Zt={list:pt,get:ft,collections:mt,members:gt,targets:ht},Qt={list:yt,invitations:wt,invite:bt,role:vt,remove:xt,accept:It,decline:Et},er={list:({options:t})=>At(t),inspect:({id:t})=>Rt({name:t}),install:({id:t,options:e})=>Pt({name:t,options:e})},tr={query:Ct,export:Lt},rr={generate:Ft,upload:zt,delete:Gt},or={list:Vt,add:Jt,reply:Ht,delete:Wt};async function Qr(t,e,r){let o=I({flags:$(t)}),i=new w,n=await B(o,i),a=await _(t,i);if(!a)return;let s;try{s=T(o)}catch(c){return f(t,c.message)}let l=new j({provider:s,credentialStore:a,profileId:n,apiBaseUrl:o.apiBaseUrl}),u=new z(l),d={...t,limit:t.limit===void 0?void 0:Number.parseInt(t.limit,10)};try{let c=await Yt[r]({client:u,id:e[0],options:d,stdin:process.stdin});if(y(t,{ok:!0,...c}),!t.json&&!t.quiet)if(r==="list"){for(let p of c.data||[])console.log(`${p.id} ${p.status} ${p.title||"(untitled)"}`);c.meta?.nextCursor&&console.log(`Next cursor: ${c.meta.nextCursor}`)}else if(r==="get")console.log(`${c.title||"(untitled)"} [${c.status}]
|
|
111
|
+
${c.markdown||""}`);else if(r==="history")for(let p of c.data||[])console.log(`${p.id} ${p.label||"snapshot"} ${p.created_at} ${p.username||"system"}`);else 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 h){process.stdout.write(ee({ok:!1,error:{code:c.code,message:c.message,requestId:c.requestId,details:c.details}})+`
|
|
112
|
+
`),process.exitCode=c.status===412?3:1;return}return f(t,c,c.status===412?m.CONFLICT:m.ERROR)}}async function eo(t,e,r){let o=await M(t);if(!o)return;let i=new re(o.http);try{let n=await Zt[r]({client:i,id:e[0],options:t});if(y(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?m.AUTH:m.ERROR)}}async function to(t,e,r){let o=await M(t);if(o)try{let i=await rr[r]({mediaClient:new ae(o.http),blogClient:o.client,id:e[0],options:t});y(t,{ok:!0,data:i}),!t.json&&!t.quiet&&(console.log(r==="delete"?`Deleted media ${i.id}.`:`${r==="generate"?"Generated":"Uploaded"} image: ${i.media?.url||i.output||i.media?.publicId}`),i.blog&&console.log(`Attached to blog ${t.blog}.`))}catch(i){f(t,i,i.status===401||i.status===403?m.AUTH:m.ERROR)}}async function ro(t,e,r){let o=await M(t);if(o)try{let i=await or[r]({client:o.client,id:e[0],options:t});if(y(t,{ok:!0,data:i}),!t.json&&!t.quiet)if(r==="list")for(let n of i)console.log(`${n.id} ${n.parent_id?"reply":"comment"} ${n.display_name||n.username||"Anonymous"} ${n.content}`);else console.log(`${r} completed for ${i.id}.`)}catch(i){f(t,i,i.status===401||i.status===403?m.AUTH:m.ERROR)}}async function oo(t,e,r){let o=await M(t);if(!o)return;let i=new ie(o.http);try{let n=await Qt[r]({client:i,id:e[0],options:t});if(y(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?m.AUTH:m.ERROR)}}async function io(t,e,r){try{let o=await er[r]({id:e[0],options:t});if(y(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 no(t,e,r){let o=await M(t);if(!o)return;let i=new ne(o.http),n={...t,limit:t.limit===void 0?void 0:Number.parseInt(t.limit,10)};try{let a=await tr[r]({client:i,options:n});if(y(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?m.AUTH:m.ERROR)}}var Xt={auth:{login:Kt,status:Jr,whoami:Hr,logout:Xr,revoke:Kr,profiles:Yr,use:Zr},blog:Object.fromEntries(Object.keys(Yt).map(t=>[t,(e,r)=>Qr(e,r,t)])),org:Object.fromEntries(Object.keys(Zt).map(t=>[t,(e,r)=>eo(e,r,t)])),collab:Object.fromEntries(Object.keys(Qt).map(t=>[t,(e,r)=>oo(e,r,t)])),skill:Object.fromEntries(Object.keys(er).map(t=>[t,(e,r)=>io(e,r,t)])),analytics:Object.fromEntries(Object.keys(tr).map(t=>[t,(e,r)=>no(e,r,t)])),media:Object.fromEntries(Object.keys(rr).map(t=>[t,(e,r)=>to(e,r,t)])),comment:Object.fromEntries(Object.keys(or).map(t=>[t,(e,r)=>ro(e,r,t)])),integrations:{"cloudinary-status":(t,e)=>N(t,e,"cloudinary-status"),"cloudinary-disconnect":(t,e)=>N(t,e,"cloudinary-disconnect"),"pollinations-status":(t,e)=>N(t,e,"pollinations-status"),"pollinations-disconnect":(t,e)=>N(t,e,"pollinations-disconnect")},disconnect:{cloudinary:(t,e)=>N(t,e,"cloudinary-disconnect"),pollinations:(t,e)=>N(t,e,"pollinations-disconnect")}};async function so(){let t,e;try{({values:t,positionals:e}=Mr({args:process.argv.slice(2),options:zr,allowPositionals:!0,strict:!0}))}catch(a){process.stderr.write(`Error: Invalid flag. ${a.message}
|
|
113
|
+
`),process.exitCode=m.USAGE;return}if(t.help||e.length===0){process.stdout.write(Gr);return}if(e[0]==="register"){await Wr(t);return}e=Ze(e);let[r,o]=e,i=Xt[r];if(!i){process.stderr.write(`Error: Unknown command category "${r}".
|
|
114
|
+
`),process.stderr.write(`Available categories: ${Object.keys(Xt).join(", ")}
|
|
115
|
+
`),process.exitCode=m.USAGE;return}let n=i[o];if(!n){process.stderr.write(`Error: Unknown ${r} command "${o}".
|
|
103
116
|
`),process.stderr.write(`Available commands: ${Object.keys(i).map(a=>`${r} ${a}`).join(", ")}
|
|
104
|
-
`),process.exitCode=
|
|
117
|
+
`),process.exitCode=m.USAGE;return}await n(t,e.slice(2))}so();
|
package/package.json
CHANGED
|
@@ -36,7 +36,9 @@ lixblogs blog edit BLOG_ID --file post.md --dry-run --json --no-input
|
|
|
36
36
|
lixblogs blog edit BLOG_ID --file post.md --json --no-input
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
Metadata-only revisions use `--title`, `--subtitle`, `--slug`, repeatable `--tag`, `--emoji`, `--cover`, `--publication`, and `--
|
|
39
|
+
Metadata-only revisions use `--title`, `--subtitle`, `--slug`, repeatable `--tag`, `--emoji`, `--cover`, `--cover-x`, `--cover-y`, `--cover-zoom`, `--publication`, `--collection`, `--member-only` / `--no-member-only`, `--allow-comments` / `--no-comments`, and `--secret` / `--not-secret`. Content inputs `--file`, `--stdin`, `--content`, and `--editor` are mutually exclusive.
|
|
40
|
+
|
|
41
|
+
Use `lixblogs blog history BLOG_ID` to inspect snapshots and `lixblogs blog restore-version BLOG_ID --version VERSION_ID --yes` only with explicit approval. Use the separate `lixblogs-media` skill for uploads or billable Pollinations generation.
|
|
40
42
|
|
|
41
43
|
## Recovery
|
|
42
44
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lixblogs-media
|
|
3
|
+
description: Generate, upload, and attach LixBlogs images through the supported CLI. Use when an agent needs an inline image or cover stored in the creator's selected Cloudinary space, optionally generated through their connected Pollinations BYOP account.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# LixBlogs media
|
|
7
|
+
|
|
8
|
+
Use `@elixpo/lixblogs-cli` 1.5.0 or newer with `--json --no-input`. Never read integration keys, call Pollinations or Cloudinary directly, persist provider URLs, or use session cookies.
|
|
9
|
+
|
|
10
|
+
## Access and cost
|
|
11
|
+
|
|
12
|
+
- Inspect connection: `lixblogs:media:read`
|
|
13
|
+
- Generate or upload: `lixblogs:media:write`; attaching also needs `lixblogs:blog:read` and `lixblogs:blog:write`
|
|
14
|
+
- Pollinations generation spends the creator's approved Pollen budget. Generate only when the current user request explicitly authorizes that image. One command is one billable attempt; never automatically retry a failed generation.
|
|
15
|
+
- A disconnected, expired, or revoked connection must be repaired by the user at `https://blogs.elixpo.com/settings?tab=integrations`.
|
|
16
|
+
|
|
17
|
+
Check the connection before offering generation:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
lixblogs integrations pollinations-status --json --no-input
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Generate and attach
|
|
24
|
+
|
|
25
|
+
Keep `--output`: it retains a local recovery copy if Cloudinary persistence fails, so retry `media upload` instead of paying for another generation.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
lixblogs media generate --prompt "Editorial illustration of…" --model flux --output image.jpg --json --no-input
|
|
29
|
+
lixblogs media generate --prompt "Wide cover…" --blog BLOG_ID --type cover --attach --output cover.jpg --json --no-input
|
|
30
|
+
lixblogs media generate --prompt "Diagram…" --blog BLOG_ID --type inline --caption "System flow" --attach --output diagram.jpg --json --no-input
|
|
31
|
+
lixblogs media generate --prompt "Restyle this reference…" --reference source.webp --blog BLOG_ID --type inline --attach --output result.jpg --json --no-input
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Use a new generation only for a genuinely new explicit request. Duplicate submissions are rejected to prevent double spending. Do not retry `401`, `402`, `403`, or `429` responses automatically.
|
|
35
|
+
|
|
36
|
+
## Upload existing media
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
lixblogs media upload --file image.webp --blog BLOG_ID --type inline --caption "Alt context" --attach --json --no-input
|
|
40
|
+
lixblogs media upload --file cover.webp --blog BLOG_ID --type cover --attach --json --no-input
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Uploads pass through LixBlogs metadata stripping, storage quotas, ownership checks, idempotent tracking, and the creator's selected global or personal Cloudinary space. Use a stable `--upload-id` when retrying the same local file.
|
|
44
|
+
|
|
45
|
+
After attachment, fetch the blog once and confirm the returned media URL appears in the intended cover or content block. Use `lixblogs-author` for placement or prose changes and `lixblogs-publish` for public-state changes.
|
|
46
|
+
|
|
47
|
+
Delete an owned tracked asset only with explicit approval:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
lixblogs media delete MEDIA_ID --yes --json --no-input
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Deletion removes the provider asset and its LixBlogs tracking record. If personal Cloudinary authorization has expired, ask the user to reconnect that storage space; never remove only the database record.
|
|
@@ -35,6 +35,7 @@ lixblogs blog preview BLOG_ID --json --no-input
|
|
|
35
35
|
```bash
|
|
36
36
|
lixblogs blog publish BLOG_ID --etag ETAG --idempotency-key KEY --dry-run --json --no-input
|
|
37
37
|
lixblogs blog publish BLOG_ID --etag ETAG --idempotency-key KEY --yes --json --no-input
|
|
38
|
+
lixblogs blog publish BLOG_ID --status unlisted --etag ETAG --idempotency-key KEY --yes --json --no-input
|
|
38
39
|
lixblogs blog unpublish BLOG_ID --etag ETAG --yes --json --no-input
|
|
39
40
|
lixblogs blog trash BLOG_ID --etag ETAG --yes --json --no-input
|
|
40
41
|
lixblogs blog restore BLOG_ID --etag ETAG --yes --json --no-input
|