@elixpo/lixblogs-cli 1.3.3 → 1.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +9 -7
  2. package/dist/lixblogs.mjs +102 -0
  3. package/package.json +9 -10
  4. package/API.md +0 -104
  5. package/CHANGELOG.md +0 -10
  6. package/RELEASE.md +0 -30
  7. package/THREAT_MODEL.md +0 -91
  8. package/bin/lixblogs.mjs +0 -802
  9. package/src/api/AnalyticsClient.js +0 -40
  10. package/src/api/BlogClient.js +0 -140
  11. package/src/api/CollaborationClient.js +0 -73
  12. package/src/api/OrgClient.js +0 -158
  13. package/src/auth/AuthProvider.js +0 -90
  14. package/src/auth/AuthenticatedClient.js +0 -131
  15. package/src/auth/ElixpoAuthProvider.js +0 -281
  16. package/src/auth/MockAuthProvider.js +0 -170
  17. package/src/auth/productionGate.js +0 -44
  18. package/src/cli/contract.js +0 -46
  19. package/src/cli/ui.js +0 -54
  20. package/src/commands/analytics/index.js +0 -57
  21. package/src/commands/auth/login.js +0 -117
  22. package/src/commands/auth/logout.js +0 -21
  23. package/src/commands/auth/profileAlias.js +0 -29
  24. package/src/commands/auth/profiles.js +0 -27
  25. package/src/commands/auth/revoke.js +0 -45
  26. package/src/commands/auth/status.js +0 -33
  27. package/src/commands/blog/index.js +0 -85
  28. package/src/commands/blog/input.js +0 -59
  29. package/src/commands/collab/index.js +0 -53
  30. package/src/commands/org/index.js +0 -22
  31. package/src/commands/skill/index.js +0 -83
  32. package/src/config/CredentialStore.js +0 -142
  33. package/src/config/KeychainCredentialStore.js +0 -180
  34. package/src/config/ProfileRegistry.js +0 -105
  35. package/src/config/config.js +0 -60
  36. package/src/config/credentialStoreFactory.js +0 -63
  37. package/src/config/providerFactory.js +0 -42
  38. package/src/config/redact.js +0 -74
  39. package/src/content/markdown.js +0 -68
  40. package/src/content/validate.js +0 -45
package/bin/lixblogs.mjs DELETED
@@ -1,802 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * bin/lixblogs.mjs — CLI entry point.
5
- *
6
- * Per maintainer direction: zero third-party dependencies for argument
7
- * parsing — uses Node's native util.parseArgs (built into Node 18+)
8
- * instead of commander/oclif/etc. UI/branding (panda welcome screen,
9
- * theming) is Divyanshu's territory later; this file only handles
10
- * parsing and dispatch, deliberately unstyled for now.
11
- *
12
- * Currently wires up `auth login|status|logout|revoke` only, per #137's
13
- * scope. Other command groups (blog, media, org, stats — see #135) are out
14
- * of scope for this issue and will be added in follow-up issues.
15
- *
16
- * Deliberately thin: all real logic lives in src/commands/**, this file
17
- * only parses args, resolves config, constructs dependencies via the
18
- * factories, and calls into the tested command functions. None of that
19
- * logic changed when swapping the parser out — this is exactly the
20
- * decoupling that made this swap fast.
21
- */
22
-
23
- import { parseArgs } from "node:util";
24
- import { spawn } from "node:child_process";
25
- import { resolveConfig } from "../src/config/config.js";
26
- import { createAuthProvider } from "../src/config/providerFactory.js";
27
- import { createCredentialStore } from "../src/config/credentialStoreFactory.js";
28
- import { safeJsonStringify, redactErrorMessage } from "../src/config/redact.js";
29
- import { authLogin } from "../src/commands/auth/login.js";
30
- import { authStatus } from "../src/commands/auth/status.js";
31
- import { authLogout } from "../src/commands/auth/logout.js";
32
- import { authRevoke } from "../src/commands/auth/revoke.js";
33
- import { authProfiles, authUse } from "../src/commands/auth/profiles.js";
34
- import { profileAliasFromIdentity } from "../src/commands/auth/profileAlias.js";
35
- import { ProfileRegistry, validateProfileId } from "../src/config/ProfileRegistry.js";
36
- import { AuthenticatedClient } from "../src/auth/AuthenticatedClient.js";
37
- import { BlogClient, BlogApiError } from "../src/api/BlogClient.js";
38
- import { OrgClient } from "../src/api/OrgClient.js";
39
- import { CollaborationClient } from "../src/api/CollaborationClient.js";
40
- import { AnalyticsClient } from "../src/api/AnalyticsClient.js";
41
- import { EXIT_CODES, errorEnvelope, normalizeCommand } from "../src/cli/contract.js";
42
- import { colorEnabled, listenForEnter, loginChallenge, successLine } from "../src/cli/ui.js";
43
- import {
44
- blogCreate,
45
- blogDelete,
46
- blogEdit,
47
- blogGet,
48
- blogList,
49
- blogPublish,
50
- blogRestore,
51
- blogUnpublish,
52
- } from "../src/commands/blog/index.js";
53
- import {
54
- orgCollections,
55
- orgGet,
56
- orgList,
57
- orgMembers,
58
- orgTargets,
59
- } from "../src/commands/org/index.js";
60
- import {
61
- collabAccept,
62
- collabDecline,
63
- collabInvitations,
64
- collabInvite,
65
- collabList,
66
- collabRemove,
67
- collabRole,
68
- } from "../src/commands/collab/index.js";
69
- import { skillInspect, skillInstall, skillList } from "../src/commands/skill/index.js";
70
- import { analyticsExport, analyticsQuery } from "../src/commands/analytics/index.js";
71
-
72
- const OPTIONS = {
73
- profile: { type: "string" },
74
- env: { type: "string" },
75
- json: { type: "boolean", default: false },
76
- quiet: { type: "boolean", default: false },
77
- yes: { type: "boolean", short: "y", default: false },
78
- "allow-insecure-fallback": { type: "boolean", default: false },
79
- "auth-provider": { type: "string" },
80
- "accounts-url": { type: "string" },
81
- "api-url": { type: "string" },
82
- "client-id": { type: "string" },
83
- audience: { type: "string" },
84
- scope: { type: "string", multiple: true },
85
- open: { type: "boolean", default: false },
86
- status: { type: "string" },
87
- limit: { type: "string" },
88
- cursor: { type: "string" },
89
- range: { type: "string" },
90
- from: { type: "string" },
91
- to: { type: "string" },
92
- dimension: { type: "string" },
93
- format: { type: "string" },
94
- output: { type: "string" },
95
- file: { type: "string" },
96
- stdin: { type: "boolean", default: false },
97
- content: { type: "string" },
98
- editor: { type: "boolean", default: false },
99
- title: { type: "string" },
100
- subtitle: { type: "string" },
101
- slug: { type: "string" },
102
- tag: { type: "string", multiple: true },
103
- emoji: { type: "string" },
104
- publication: { type: "string" },
105
- collection: { type: "string" },
106
- cover: { type: "string" },
107
- "member-only": { type: "boolean", default: false },
108
- "no-member-only": { type: "boolean", default: false },
109
- secret: { type: "boolean", default: false },
110
- "not-secret": { type: "boolean", default: false },
111
- "dry-run": { type: "boolean", default: false },
112
- "no-input": { type: "boolean", default: false },
113
- etag: { type: "string" },
114
- permanent: { type: "boolean", default: false },
115
- "idempotency-key": { type: "string" },
116
- user: { type: "string" },
117
- role: { type: "string" },
118
- "hide-on-profile": { type: "boolean", default: false },
119
- target: { type: "string" },
120
- force: { type: "boolean", default: false },
121
- help: { type: "boolean", short: "h", default: false },
122
- };
123
-
124
- const HELP_TEXT = `lixblogs — LixBlogs CLI
125
-
126
- Usage:
127
- lixblogs login [--profile <name>] [--open]
128
- lixblogs register [--profile <name>] [--open]
129
- lixblogs logout [--profile <name>]
130
- lixblogs whoami [--profile <name>] [--json]
131
- lixblogs profiles [--json]
132
- lixblogs use <name> [--json]
133
- lixblogs auth login [--profile <name>] [--env <environment>] [--json] [--quiet] [--allow-insecure-fallback]
134
- lixblogs auth status [--profile <name>] [--json]
135
- lixblogs auth logout [--profile <name>] [--json] [--quiet]
136
- lixblogs auth revoke [--profile <name>] [--json] [--quiet] --yes
137
- lixblogs auth profiles [--json]
138
- lixblogs auth use <name> [--json]
139
- lixblogs blog list [--status <status>] [--limit <n>] [--cursor <cursor>] [--json]
140
- lixblogs blog get <id> [--json]
141
- lixblogs blog preview <id> [--json]
142
- lixblogs blog create [--file <post.md>|--stdin|--content <markdown>|--editor] [metadata]
143
- lixblogs blog edit <id> [--file <post.md>|--stdin|--content <markdown>|--editor] [metadata]
144
- lixblogs blog publish <id> --yes [--dry-run] [--json]
145
- lixblogs blog unpublish <id> --yes [--dry-run] [--json]
146
- lixblogs blog delete <id> --yes [--permanent] [--dry-run] [--json]
147
- lixblogs blog trash <id> --yes [--dry-run] [--json]
148
- lixblogs blog restore <id> --yes [--dry-run] [--json]
149
- lixblogs org list [--json]
150
- lixblogs org get <id> [--json]
151
- lixblogs org collections <id> [--json]
152
- lixblogs org members <id> [--json]
153
- lixblogs org targets [--json]
154
- lixblogs collab list <blog-id> [--json]
155
- lixblogs collab invitations [--json]
156
- lixblogs collab invite <blog-id> --user <username> --role <viewer|editor|admin> --yes
157
- lixblogs collab role <blog-id> --user <username-or-id> --role <viewer|editor|admin> --yes
158
- lixblogs collab remove <blog-id> [--user <username-or-id>] --yes
159
- lixblogs collab accept <blog-id> --yes [--hide-on-profile]
160
- lixblogs collab decline <blog-id> --yes
161
- lixblogs analytics query [--scope personal|org:<id>] [--range 30d] [--dimension overview]
162
- lixblogs analytics export --output <file> [--format json|csv] [query options]
163
- lixblogs skill list [--json]
164
- lixblogs skill inspect <name> [--json]
165
- lixblogs skill install <name> [--target <directory>] [--dry-run] --yes
166
-
167
- Global flags:
168
- --profile <name> local account alias (defaults to the signed-in username)
169
- --env <environment> override environment (development|staging|production)
170
- --auth-provider <provider> elixpo, or mock in development/test only
171
- --accounts-url <url> override the Accounts discovery origin
172
- --api-url <url> LixBlogs API origin (default: https://blogs.elixpo.com)
173
- --scope <scope> request an OAuth scope (repeatable)
174
- --file <path> read blog Markdown from a file
175
- --stdin read blog Markdown from stdin
176
- --content <markdown> use inline Markdown
177
- --editor open the current blog in $EDITOR
178
- --title/--subtitle/--slug update blog metadata
179
- --tag <tag> set a tag (repeatable, up to five)
180
- --publication <target> personal or org:<id>
181
- --collection <id> organization collection ID
182
- --dry-run validate and show the intended action without writing
183
- --permanent permanently delete instead of moving to trash
184
- --open open the device verification URL immediately
185
- --json machine-readable JSON output
186
- --quiet suppress non-essential output
187
- --yes, -y auto-confirm destructive actions (required for revoke)
188
- --allow-insecure-fallback explicit opt-in: if the OS keychain is unavailable, use a
189
- non-persistent in-memory store instead of failing
190
- --help, -h show this help
191
-
192
- Machine mode:
193
- --json --no-input produces stable JSON on stdout, diagnostics on stderr, and
194
- never prompts. Publishing and destructive state changes require --yes.
195
- `;
196
-
197
- const DEFAULT_SCOPES = [
198
- "openid", "profile", "email",
199
- "lixblogs:profile:read", "lixblogs:blog:read",
200
- ];
201
-
202
- function configFlags(opts) {
203
- return {
204
- profile: opts.profile,
205
- env: opts.env,
206
- authProvider: opts["auth-provider"],
207
- accountsUrl: opts["accounts-url"],
208
- apiUrl: opts["api-url"],
209
- clientId: opts["client-id"],
210
- audience: opts.audience,
211
- };
212
- }
213
-
214
- async function selectedProfile(config, registry) {
215
- if (config.profileExplicit) return validateProfileId(config.profile);
216
- return (await registry.getActive()) || validateProfileId(config.profile);
217
- }
218
-
219
- async function openBrowser(url) {
220
- const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
221
- const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
222
- const child = spawn(command, args, { detached: true, stdio: "ignore" });
223
- child.on("error", () => {});
224
- child.unref();
225
- }
226
-
227
- function output(opts, data) {
228
- if (opts.json) {
229
- process.stdout.write(safeJsonStringify(data) + "\n");
230
- }
231
- }
232
-
233
- function fail(opts, error, exitCode = EXIT_CODES.ERROR) {
234
- const value = error && typeof error === 'object' ? error : { message: String(error) };
235
- const safeMessage = redactErrorMessage(value.message);
236
- const envelope = errorEnvelope({ ...value, message: safeMessage });
237
- if (opts.json) {
238
- process.stdout.write(safeJsonStringify(envelope) + "\n");
239
- } else if (!opts.quiet) {
240
- process.stderr.write(`Error: ${safeMessage}\n`);
241
- if (value.hint) process.stderr.write(`Hint: ${value.hint}\n`);
242
- if (value.requestId) process.stderr.write(`Request: ${value.requestId}\n`);
243
- }
244
- process.exitCode = value.exitCode || exitCode;
245
- }
246
-
247
- /**
248
- * Shared helper: constructs the credential store, surfacing a
249
- * CredentialStoreUnavailableError as a clean CLI-level failure (via fail())
250
- * rather than an uncaught stack trace, and pointing the user at
251
- * --allow-insecure-fallback if they haven't already opted in.
252
- * @returns {Promise<import("../src/config/CredentialStore.js").CredentialStore | null>}
253
- * null if construction failed and fail() was already called.
254
- */
255
- async function getCredentialStoreOrFail(opts, profileRegistry) {
256
- try {
257
- return await createCredentialStore({
258
- allowInsecureFallback: opts["allow-insecure-fallback"],
259
- profileRegistry,
260
- });
261
- } catch (err) {
262
- fail(
263
- opts,
264
- `${err.message}${opts["allow-insecure-fallback"] ? "" : " Re-run with --allow-insecure-fallback to opt in to non-persistent storage instead."}`
265
- );
266
- return null;
267
- }
268
- }
269
-
270
- async function runLogin(opts) {
271
- const config = resolveConfig({ flags: configFlags(opts) });
272
- const profileRegistry = new ProfileRegistry();
273
- const requestedProfileId = validateProfileId(config.profile);
274
- const scopes = opts.scope?.length ? [...opts.scope] : [...DEFAULT_SCOPES];
275
- if (!config.profileExplicit && !scopes.includes("lixblogs:profile:read")) {
276
- scopes.push("lixblogs:profile:read");
277
- }
278
-
279
- let provider;
280
- try {
281
- provider = createAuthProvider(config);
282
- } catch (err) {
283
- return fail(opts, err.message);
284
- }
285
-
286
- const credentialStore = await getCredentialStoreOrFail(opts, profileRegistry);
287
- if (!credentialStore) return;
288
-
289
- let stopEnterListener = () => {};
290
- let result;
291
- try {
292
- result = await authLogin({
293
- provider,
294
- credentialStore,
295
- profileId: requestedProfileId,
296
- scopes,
297
- openBrowser: opts.open ? openBrowser : undefined,
298
- resolveProfileId: config.profileExplicit
299
- ? undefined
300
- : ({ accessToken }) => profileAliasFromIdentity({
301
- accessToken,
302
- apiBaseUrl: config.apiBaseUrl,
303
- }),
304
- onStatus: (status) => {
305
- if (opts.json) {
306
- if (status.type !== "pending") output(opts, { event: status.type, ...status });
307
- return;
308
- }
309
- if (opts.quiet) return;
310
- if (status.type === "verification_pending") {
311
- const url = status.verificationUriComplete || status.verificationUri;
312
- const interactive = Boolean(process.stdin.isTTY) && !opts["no-input"];
313
- process.stdout.write(loginChallenge({
314
- url,
315
- code: status.userCode,
316
- expiresInSeconds: status.expiresInSeconds,
317
- profile: config.profileExplicit ? requestedProfileId : null,
318
- interactive,
319
- color: colorEnabled(),
320
- }));
321
- if (interactive && !opts.open) {
322
- stopEnterListener = listenForEnter({ input: process.stdin, open: openBrowser, url });
323
- }
324
- } else if (status.type === "approved") {
325
- console.log(successLine("Access approved by Elixpo Accounts.", colorEnabled()));
326
- } else if (status.type === "denied") {
327
- console.log(" Access denied.");
328
- } else if (status.type === "expired") {
329
- console.log(" Device code expired.");
330
- }
331
- },
332
- });
333
- } finally {
334
- stopEnterListener();
335
- }
336
-
337
- if (!result.ok) {
338
- return fail(opts, result.reason);
339
- }
340
- await profileRegistry.add(result.profileId);
341
- await profileRegistry.setActive(result.profileId);
342
- output(opts, { ok: true, profile: result.profileId });
343
- if (!opts.json && !opts.quiet) {
344
- console.log(` Credentials saved to local profile "${result.profileId}".`);
345
- console.log(" Tip: add another account with `lixblogs login`, list accounts with `lixblogs profiles`,");
346
- console.log(" and switch with `lixblogs use <username>`.");
347
- }
348
- }
349
-
350
- async function runStatus(opts) {
351
- const config = resolveConfig({ flags: configFlags(opts) });
352
- const profileRegistry = new ProfileRegistry();
353
- const profileId = await selectedProfile(config, profileRegistry);
354
- const credentialStore = await getCredentialStoreOrFail(opts, profileRegistry);
355
- if (!credentialStore) return;
356
-
357
- const result = await authStatus({ credentialStore, profileId });
358
-
359
- output(opts, result);
360
- if (!opts.json) {
361
- for (const entry of result) {
362
- if (!entry.loggedIn) {
363
- console.log(`${entry.profileId}: not logged in`);
364
- } else {
365
- console.log(
366
- `${entry.profileId}: logged in${entry.expired ? " (expired)" : ""} — scopes: ${entry.scopes.join(", ")}`
367
- );
368
- }
369
- }
370
- }
371
- }
372
-
373
- async function authenticatedBlogClient(opts) {
374
- const config = resolveConfig({ flags: configFlags(opts) });
375
- const profileRegistry = new ProfileRegistry();
376
- const profileId = await selectedProfile(config, profileRegistry);
377
- const credentialStore = await getCredentialStoreOrFail(opts, profileRegistry);
378
- if (!credentialStore) return null;
379
- let provider;
380
- try { provider = createAuthProvider(config); } catch (error) { fail(opts, error); return null; }
381
- const http = new AuthenticatedClient({ provider, credentialStore, profileId, apiBaseUrl: config.apiBaseUrl });
382
- return { client: new BlogClient(http), http, config, credentialStore, profileId };
383
- }
384
-
385
- async function runWhoami(opts) {
386
- const context = await authenticatedBlogClient(opts);
387
- if (!context) return;
388
- try {
389
- const [identity, credentials] = await Promise.all([
390
- context.client.whoami(),
391
- context.credentialStore.get(context.profileId),
392
- ]);
393
- const result = {
394
- ok: true,
395
- profile: context.profileId,
396
- environment: context.config.environment,
397
- identity,
398
- scopes: credentials?.scopes || [],
399
- expiresAt: credentials?.expiresAt ? new Date(credentials.expiresAt).toISOString() : null,
400
- expired: credentials ? Date.now() >= credentials.expiresAt : true,
401
- };
402
- output(opts, result);
403
- if (!opts.json && !opts.quiet) {
404
- console.log(`${identity.displayName || identity.username} (@${identity.username})`);
405
- console.log(`Profile: ${context.profileId} · ${result.environment}`);
406
- console.log(`Scopes: ${result.scopes.join(', ') || 'none'}`);
407
- console.log(`Expires: ${result.expiresAt || 'unknown'}`);
408
- }
409
- } catch (error) {
410
- fail(opts, error, error.status === 401 || error.status === 403 ? EXIT_CODES.AUTH : EXIT_CODES.ERROR);
411
- }
412
- }
413
-
414
- async function runRegister(opts) {
415
- const config = resolveConfig({ flags: configFlags(opts) });
416
- const registrationUrl = new URL('/register', config.accountsBaseUrl).toString();
417
- if (opts['no-input']) {
418
- output(opts, { ok: true, registrationUrl, next: 'lixblogs login' });
419
- if (!opts.json && !opts.quiet) console.log(registrationUrl);
420
- return;
421
- }
422
- await openBrowser(registrationUrl);
423
- if (!opts.quiet) console.log(`Create your account at ${registrationUrl}, then approve the device login.`);
424
- await runLogin(opts);
425
- }
426
-
427
- async function runLogout(opts) {
428
- const config = resolveConfig({ flags: configFlags(opts) });
429
- const profileRegistry = new ProfileRegistry();
430
- const profileId = await selectedProfile(config, profileRegistry);
431
- const credentialStore = await getCredentialStoreOrFail(opts, profileRegistry);
432
- if (!credentialStore) return;
433
-
434
- const result = await authLogout({ credentialStore, profileId });
435
- output(opts, result);
436
- if (!opts.json && !opts.quiet) {
437
- console.log(`Logged out profile "${profileId}".`);
438
- }
439
- }
440
-
441
- async function runRevoke(opts) {
442
- const config = resolveConfig({ flags: configFlags(opts) });
443
- const profileRegistry = new ProfileRegistry();
444
- const profileId = await selectedProfile(config, profileRegistry);
445
-
446
- // Destructive action: per #135, cannot run accidentally in a
447
- // non-interactive session. Interactive confirmation prompting is
448
- // CLI-shell/UX work (later issue) — for now, --yes is the only
449
- // supported path, and omitting it fails closed rather than silently
450
- // proceeding or silently doing nothing.
451
- if (!opts.yes) {
452
- return fail(
453
- opts,
454
- "This is a destructive action. Re-run with --yes to confirm (interactive confirmation prompt not yet implemented)."
455
- );
456
- }
457
-
458
- let provider;
459
- try {
460
- provider = createAuthProvider(config);
461
- } catch (err) {
462
- return fail(opts, err.message);
463
- }
464
- const credentialStore = await getCredentialStoreOrFail(opts, profileRegistry);
465
- if (!credentialStore) return;
466
-
467
- const result = await authRevoke({
468
- provider,
469
- credentialStore,
470
- profileId,
471
- confirmed: true,
472
- });
473
-
474
- if (!result.ok) {
475
- return fail(opts, result.reason);
476
- }
477
- output(opts, result);
478
- if (!opts.json && !opts.quiet) {
479
- console.log(`Revoked and logged out profile "${profileId}".`);
480
- }
481
- }
482
-
483
- async function runProfiles(opts) {
484
- const profileRegistry = new ProfileRegistry();
485
- const credentialStore = await getCredentialStoreOrFail(opts, profileRegistry);
486
- if (!credentialStore) return;
487
- const result = await authProfiles({ credentialStore, profileRegistry });
488
- output(opts, result);
489
- if (!opts.json) {
490
- if (!result.profiles.length) console.log("No profiles. Run `lixblogs auth login` first.");
491
- for (const profile of result.profiles) {
492
- console.log(`${profile.active ? "*" : " "} ${profile.profileId}${profile.expired ? " (expired)" : ""}`);
493
- }
494
- }
495
- }
496
-
497
- async function runUse(opts, args) {
498
- let profileId;
499
- try {
500
- profileId = validateProfileId(args[0]);
501
- } catch (error) {
502
- return fail(opts, error.message);
503
- }
504
- const profileRegistry = new ProfileRegistry();
505
- const credentialStore = await getCredentialStoreOrFail(opts, profileRegistry);
506
- if (!credentialStore) return;
507
- const result = await authUse({ credentialStore, profileRegistry, profileId });
508
- if (!result.ok) return fail(opts, result.reason);
509
- output(opts, result);
510
- if (!opts.json && !opts.quiet) console.log(`Using profile "${profileId}".`);
511
- }
512
-
513
- const BLOG_COMMANDS = {
514
- list: blogList,
515
- get: blogGet,
516
- preview: blogGet,
517
- create: blogCreate,
518
- edit: blogEdit,
519
- publish: blogPublish,
520
- unpublish: blogUnpublish,
521
- delete: blogDelete,
522
- trash: blogDelete,
523
- restore: blogRestore,
524
- };
525
-
526
- const ORG_COMMANDS = {
527
- list: orgList,
528
- get: orgGet,
529
- collections: orgCollections,
530
- members: orgMembers,
531
- targets: orgTargets,
532
- };
533
-
534
- const COLLAB_COMMANDS = {
535
- list: collabList,
536
- invitations: collabInvitations,
537
- invite: collabInvite,
538
- role: collabRole,
539
- remove: collabRemove,
540
- accept: collabAccept,
541
- decline: collabDecline,
542
- };
543
-
544
- const SKILL_COMMANDS = {
545
- list: ({ options }) => skillList(options),
546
- inspect: ({ id }) => skillInspect({ name: id }),
547
- install: ({ id, options }) => skillInstall({ name: id, options }),
548
- };
549
-
550
- const ANALYTICS_COMMANDS = {
551
- query: analyticsQuery,
552
- export: analyticsExport,
553
- };
554
-
555
- async function runBlog(opts, args, action) {
556
- const config = resolveConfig({ flags: configFlags(opts) });
557
- const profileRegistry = new ProfileRegistry();
558
- const profileId = await selectedProfile(config, profileRegistry);
559
- const credentialStore = await getCredentialStoreOrFail(opts, profileRegistry);
560
- if (!credentialStore) return;
561
- let provider;
562
- try { provider = createAuthProvider(config); } catch (error) { return fail(opts, error.message); }
563
- const http = new AuthenticatedClient({
564
- provider, credentialStore, profileId, apiBaseUrl: config.apiBaseUrl,
565
- });
566
- const client = new BlogClient(http);
567
- const normalized = {
568
- ...opts,
569
- limit: opts.limit === undefined ? undefined : Number.parseInt(opts.limit, 10),
570
- };
571
- try {
572
- const result = await BLOG_COMMANDS[action]({
573
- client, id: args[0], options: normalized, stdin: process.stdin,
574
- });
575
- output(opts, { ok: true, ...result });
576
- if (!opts.json && !opts.quiet) {
577
- if (action === 'list') {
578
- for (const blog of result.data || []) console.log(`${blog.id}\t${blog.status}\t${blog.title || '(untitled)'}`);
579
- if (result.meta?.nextCursor) console.log(`Next cursor: ${result.meta.nextCursor}`);
580
- } else if (action === 'get') {
581
- console.log(`${result.title || '(untitled)'} [${result.status}]\n${result.markdown || ''}`);
582
- } else if (result.dryRun) {
583
- console.log(`Dry run: ${action} validated; no changes sent.`);
584
- } else {
585
- console.log(result.url || `${action} completed for ${result.id}.`);
586
- }
587
- }
588
- } catch (error) {
589
- if (opts.json && error instanceof BlogApiError) {
590
- process.stdout.write(safeJsonStringify({
591
- ok: false,
592
- error: { code: error.code, message: error.message, requestId: error.requestId, details: error.details },
593
- }) + '\n');
594
- process.exitCode = error.status === 412 ? 3 : 1;
595
- return;
596
- }
597
- return fail(opts, error, error.status === 412 ? EXIT_CODES.CONFLICT : EXIT_CODES.ERROR);
598
- }
599
- }
600
-
601
- async function runOrg(opts, args, action) {
602
- const context = await authenticatedBlogClient(opts);
603
- if (!context) return;
604
- const client = new OrgClient(context.http);
605
- try {
606
- const result = await ORG_COMMANDS[action]({ client, id: args[0], options: opts });
607
- output(opts, { ok: true, data: result });
608
- if (opts.json || opts.quiet) return;
609
- if (action === 'targets') {
610
- console.log('personal\tPersonal Blog');
611
- for (const org of result.organizations || []) {
612
- console.log(`${org.target}\t${org.role}\t${org.name}`);
613
- for (const collection of org.collections || []) {
614
- console.log(` collection:${collection.id}\t${collection.name}`);
615
- }
616
- }
617
- return;
618
- }
619
- const rows = action === 'list' ? result.data || [] : Array.isArray(result) ? result : [result];
620
- for (const row of rows) {
621
- console.log([
622
- row.id || row.userId || row.orgId,
623
- row.role,
624
- row.slug || row.username,
625
- row.name || row.displayName,
626
- ].filter(Boolean).join('\t'));
627
- }
628
- } catch (error) {
629
- fail(opts, error, error.status === 401 || error.status === 403 ? EXIT_CODES.AUTH : EXIT_CODES.ERROR);
630
- }
631
- }
632
-
633
- async function runCollab(opts, args, action) {
634
- const context = await authenticatedBlogClient(opts);
635
- if (!context) return;
636
- const client = new CollaborationClient(context.http);
637
- try {
638
- const result = await COLLAB_COMMANDS[action]({ client, id: args[0], options: opts });
639
- output(opts, { ok: true, data: result });
640
- if (opts.json || opts.quiet) return;
641
- if (result.dryRun) {
642
- console.log(`Dry run: ${result.action} validated; no changes sent.`);
643
- return;
644
- }
645
- const rows = action === 'invitations'
646
- ? result
647
- : action === 'list'
648
- ? result.collaborators || []
649
- : [result];
650
- for (const row of rows) {
651
- console.log([
652
- row.blogId || row.userId,
653
- row.status,
654
- row.role,
655
- row.username || row.title,
656
- row.notificationState,
657
- ].filter(Boolean).join('\t'));
658
- }
659
- } catch (error) {
660
- fail(opts, error, error.status === 401 || error.status === 403 ? EXIT_CODES.AUTH : EXIT_CODES.ERROR);
661
- }
662
- }
663
-
664
- async function runSkill(opts, args, action) {
665
- try {
666
- const result = await SKILL_COMMANDS[action]({ id: args[0], options: opts });
667
- output(opts, { ok: true, data: result });
668
- if (opts.json || opts.quiet) return;
669
- if (action === 'list') {
670
- for (const skill of result) console.log(`${skill.name}\tCLI >= ${skill.minimumCliVersion || 'unknown'}\t${skill.description}`);
671
- } else if (action === 'inspect') {
672
- process.stdout.write(result.content);
673
- } else if (result.dryRun) {
674
- console.log(`Dry run: install ${result.name} to ${result.target}${result.replace ? ' (replace)' : ''}.`);
675
- } else {
676
- console.log(`Installed ${result.name} at ${result.target}.`);
677
- }
678
- } catch (error) {
679
- fail(opts, error);
680
- }
681
- }
682
-
683
- async function runAnalytics(opts, _args, action) {
684
- const context = await authenticatedBlogClient(opts);
685
- if (!context) return;
686
- const client = new AnalyticsClient(context.http);
687
- const normalized = {
688
- ...opts,
689
- limit: opts.limit === undefined ? undefined : Number.parseInt(opts.limit, 10),
690
- };
691
- try {
692
- const result = await ANALYTICS_COMMANDS[action]({ client, options: normalized });
693
- output(opts, { ok: true, data: result });
694
- if (opts.json || opts.quiet) return;
695
- if (action === 'export') {
696
- console.log(`Exported ${result.rows} rows to ${result.output}.`);
697
- return;
698
- }
699
- const payload = result.data;
700
- console.log(`${payload.scope.label} · ${payload.dimension} · ${payload.range.key}`);
701
- if (payload.dimension === 'overview') {
702
- for (const [metric, value] of Object.entries(payload.values.totals)) {
703
- console.log(`${metric}\t${value}\t${payload.values.changes[metric]}%`);
704
- }
705
- } else if (payload.dimension === 'timeline') {
706
- payload.values.labels.forEach((label, index) => console.log(`${label}\t${payload.values.views[index]}\t${payload.values.reads[index]}`));
707
- } else {
708
- for (const row of payload.values) console.log(Object.values(row).join('\t'));
709
- if (result.meta?.nextCursor) console.log(`Next cursor: ${result.meta.nextCursor}`);
710
- }
711
- } catch (error) {
712
- fail(opts, error, error.status === 401 || error.status === 403 ? EXIT_CODES.AUTH : EXIT_CODES.ERROR);
713
- }
714
- }
715
-
716
- const ROUTES = {
717
- auth: {
718
- login: runLogin,
719
- status: runStatus,
720
- whoami: runWhoami,
721
- logout: runLogout,
722
- revoke: runRevoke,
723
- profiles: runProfiles,
724
- use: runUse,
725
- },
726
- blog: Object.fromEntries(Object.keys(BLOG_COMMANDS).map((action) => [
727
- action,
728
- (opts, args) => runBlog(opts, args, action),
729
- ])),
730
- org: Object.fromEntries(Object.keys(ORG_COMMANDS).map((action) => [
731
- action,
732
- (opts, args) => runOrg(opts, args, action),
733
- ])),
734
- collab: Object.fromEntries(Object.keys(COLLAB_COMMANDS).map((action) => [
735
- action,
736
- (opts, args) => runCollab(opts, args, action),
737
- ])),
738
- skill: Object.fromEntries(Object.keys(SKILL_COMMANDS).map((action) => [
739
- action,
740
- (opts, args) => runSkill(opts, args, action),
741
- ])),
742
- analytics: Object.fromEntries(Object.keys(ANALYTICS_COMMANDS).map((action) => [
743
- action,
744
- (opts, args) => runAnalytics(opts, args, action),
745
- ])),
746
- };
747
-
748
- async function main() {
749
- let values, positionals;
750
- try {
751
- ({ values, positionals } = parseArgs({
752
- args: process.argv.slice(2),
753
- options: OPTIONS,
754
- allowPositionals: true,
755
- strict: true,
756
- }));
757
- } catch (err) {
758
- // strict: true makes parseArgs throw ERR_PARSE_ARGS_UNKNOWN_OPTION for
759
- // unrecognized flags rather than silently ignoring them — surface that
760
- // clearly instead of an unhandled exception.
761
- process.stderr.write(`Error: Invalid flag. ${err.message}\n`);
762
- process.exitCode = EXIT_CODES.USAGE;
763
- return;
764
- }
765
-
766
- if (values.help || positionals.length === 0) {
767
- process.stdout.write(HELP_TEXT);
768
- return;
769
- }
770
-
771
- if (positionals[0] === 'register') {
772
- await runRegister(values);
773
- return;
774
- }
775
-
776
- positionals = normalizeCommand(positionals);
777
- const [category, action] = positionals;
778
- const categoryRoutes = ROUTES[category];
779
-
780
- if (!categoryRoutes) {
781
- process.stderr.write(`Error: Unknown command category "${category}".\n`);
782
- process.stderr.write(`Available categories: ${Object.keys(ROUTES).join(", ")}\n`);
783
- process.exitCode = EXIT_CODES.USAGE;
784
- return;
785
- }
786
-
787
- const handler = categoryRoutes[action];
788
- if (!handler) {
789
- process.stderr.write(`Error: Unknown ${category} command "${action}".\n`);
790
- process.stderr.write(
791
- `Available commands: ${Object.keys(categoryRoutes)
792
- .map((a) => `${category} ${a}`)
793
- .join(", ")}\n`
794
- );
795
- process.exitCode = EXIT_CODES.USAGE;
796
- return;
797
- }
798
-
799
- await handler(values, positionals.slice(2));
800
- }
801
-
802
- main();