@elixpo/lixblogs-cli 1.3.1 → 1.4.2

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