@rolino/cli 0.5.0 → 0.6.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.
@@ -285,7 +285,7 @@ import { Command, CommanderError, InvalidArgumentError } from "commander";
285
285
  // package.json
286
286
  var package_default = {
287
287
  name: "@rolino/cli",
288
- version: "0.5.0",
288
+ version: "0.6.0",
289
289
  description: "Agent-friendly command-line interface for Rolino",
290
290
  type: "module",
291
291
  license: "MIT",
@@ -342,14 +342,14 @@ var package_default = {
342
342
  dev: "tsx src/bin.ts"
343
343
  },
344
344
  dependencies: {
345
- "@rolino/contracts": "0.5.0",
346
- "@rolino/local-auth": "0.5.0",
347
- "@rolino/sdk": "0.5.0",
345
+ "@rolino/contracts": "0.6.0",
346
+ "@rolino/local-auth": "0.6.0",
347
+ "@rolino/sdk": "0.6.0",
348
348
  commander: "^15.0.0",
349
349
  open: "^11.0.0"
350
350
  },
351
351
  devDependencies: {
352
- tsx: "^4.21.0"
352
+ tsx: "^4.23.12"
353
353
  },
354
354
  engines: {
355
355
  node: ">=20.19.0"
@@ -381,19 +381,21 @@ import {
381
381
  // src/browser-login.ts
382
382
  import { createHash, randomBytes, timingSafeEqual } from "crypto";
383
383
  import { createServer } from "http";
384
- import { hostname } from "os";
385
384
  import {
386
- CLI_BROWSER_AUTHORIZATION_TIMEOUT_MS
387
- } from "@rolino/contracts";
385
+ oauthConfiguration
386
+ } from "@rolino/local-auth";
388
387
  import open from "open";
389
- var PAGE_HEADERS = {
390
- "cache-control": "no-store",
391
- "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
392
- "content-type": "text/html; charset=utf-8",
393
- "referrer-policy": "no-referrer",
394
- "x-content-type-options": "nosniff"
395
- };
396
- var ROLINO_MASCOT_SVG = '<svg viewBox="0 0 512 512" aria-hidden="true" focusable="false"><path fill="#34C78A" d="M256 142c-48-29-71-58-62-84 28-4 52 11 62 36 10-25 34-40 62-36 9 26-14 55-62 84Z"/><path fill="#FACC15" stroke="#0F172A" stroke-width="12" stroke-linejoin="round" d="M256 112c-89 0-156 74-156 176v57c0 31 19 54 50 54h212c31 0 50-23 50-54v-57c0-102-67-176-156-176Z"/><circle cx="204" cy="272" r="18" fill="#0F172A"/><circle cx="308" cy="272" r="18" fill="#0F172A"/><path fill="none" stroke="#0F172A" stroke-linecap="round" stroke-width="16" d="M226 318c18 25 42 25 60 0"/></svg>';
388
+ var OAUTH_CALLBACK_PORT = 48391;
389
+ var OAUTH_CALLBACK_PATH = "/oauth/callback";
390
+ var OAUTH_TIMEOUT_MS = 5 * 60 * 1e3;
391
+ var DEFAULT_SCOPES = [
392
+ "offline_access",
393
+ "identity:read",
394
+ "projects:read",
395
+ "posts:read",
396
+ "integrations:read",
397
+ "calendar:read"
398
+ ];
397
399
  function escapeHtml(value) {
398
400
  return value.replace(/[&<>"']/g, (character) => ({
399
401
  "&": "&amp;",
@@ -403,86 +405,18 @@ function escapeHtml(value) {
403
405
  "'": "&#039;"
404
406
  })[character]);
405
407
  }
406
- function renderCliAuthorizationResultPage(options) {
407
- const tone = options.success ? "success" : "failure";
408
- const eyebrow = options.success ? "CLI handoff complete" : "CLI handoff paused";
409
- const detail = options.success ? '<div class="terminal" aria-label="Terminal authentication status"><div class="terminal-bar"><i></i><i></i><i></i><span>ROLINO CLI</span></div><div class="command"><b>$</b> rolino whoami</div><div class="terminal-result"><span>\u2713</span> Authenticated. Ready for your next command.</div></div>' : '<div class="return-note"><span>\u2192</span><div><strong>Return to your terminal</strong><small>Start the login flow again when you are ready.</small></div></div>';
410
- return `<!doctype html>
411
- <html lang="en">
412
- <head>
413
- <meta charset="utf-8">
414
- <meta name="viewport" content="width=device-width,initial-scale=1">
415
- <meta name="color-scheme" content="light">
416
- <title>${escapeHtml(options.title)} \xB7 Rolino</title>
417
- <style>
418
- *{box-sizing:border-box}
419
- html,body{height:100%;margin:0}
420
- body{min-height:100svh;overflow:hidden;background:#faf9f1;color:#0f172a;font-family:"Poppins","Avenir Next","Trebuchet MS",ui-rounded,system-ui,sans-serif}
421
- .shell{position:relative;isolation:isolate;display:grid;min-height:100svh;grid-template-rows:auto 1fr auto;padding:clamp(18px,3vw,38px);overflow:hidden}
422
- .shape{position:absolute;z-index:-1;border-radius:999px;pointer-events:none}
423
- .shape-a{top:-160px;left:-130px;width:390px;height:390px;background:#facc1530;filter:blur(2px)}
424
- .shape-b{right:-100px;bottom:-160px;width:360px;height:360px;background:#34c78a20}
425
- .shape-c{right:9%;top:14%;width:22px;height:22px;background:#ef6a5b;transform:rotate(18deg);border-radius:7px}
426
- .topbar,.footer{display:flex;width:min(100%,1100px);margin:auto;align-items:center;justify-content:space-between}
427
- .brand{display:flex;align-items:center;gap:10px;color:#0f172a;font-size:22px;font-weight:800;letter-spacing:-.04em}
428
- .brand svg{display:block;width:36px;height:36px}
429
- .secure{display:flex;align-items:center;gap:8px;color:#52607a;font-size:11px;font-weight:800;letter-spacing:.14em;text-transform:uppercase}
430
- .secure-dot{display:block;width:8px;height:8px;border-radius:50%;background:#34c78a;box-shadow:0 0 0 5px #34c78a1f}
431
- .stage{display:grid;min-height:0;place-items:center;padding:12px 0}
432
- .panel{width:min(100%,590px);padding:clamp(26px,4vw,46px);text-align:center;background:#fffefa;border:1px solid #e3e6ed;border-radius:32px;box-shadow:0 36px 90px -52px #0f172a73;animation:arrive .55s cubic-bezier(.22,1,.36,1) both}
433
- .mascot-wrap{position:relative;display:grid;width:112px;height:96px;margin:0 auto 12px;place-items:center}
434
- .mascot-wrap:before{position:absolute;width:88px;height:88px;border-radius:50%;background:#fff4a8;content:"";box-shadow:0 0 0 14px #fff9d8}
435
- .mascot-wrap svg{position:relative;width:78px;height:78px;filter:drop-shadow(0 10px 12px #c58e0026);animation:mascot-arrive .65s .08s cubic-bezier(.22,1,.36,1) both}
436
- .eyebrow{display:inline-flex;align-items:center;gap:7px;margin:0 0 12px;padding:6px 11px;border-radius:999px;background:#fff8b8;color:#7a4d00;font-size:10px;font-weight:900;letter-spacing:.14em;text-transform:uppercase}
437
- .eyebrow:before{width:7px;height:7px;border-radius:50%;background:#34c78a;content:""}
438
- .failure .eyebrow{background:#fee6df;color:#9f2f24}
439
- .failure .eyebrow:before{background:#ef6a5b}
440
- h1{margin:0;color:#0f172a;font-family:"Outfit","Avenir Next","Trebuchet MS",ui-rounded,system-ui,sans-serif;font-size:clamp(34px,5vw,48px);line-height:1;letter-spacing:-.045em}
441
- .message{max-width:450px;margin:14px auto 0;color:#52607a;font-size:15px;line-height:1.65}
442
- .terminal{max-width:450px;margin:24px auto 0;padding:14px 16px 16px;text-align:left;background:#111b30;color:#eff3fa;border:1px solid #293550;border-radius:18px;box-shadow:0 18px 42px -26px #0f172acc}
443
- .terminal-bar{display:flex;align-items:center;gap:6px;padding-bottom:12px;color:#8f9bb1;font-size:9px;font-weight:800;letter-spacing:.13em}
444
- .terminal-bar i{display:block;width:7px;height:7px;border-radius:50%;background:#ef6a5b}
445
- .terminal-bar i:nth-child(2){background:#facc15}
446
- .terminal-bar i:nth-child(3){margin-right:5px;background:#34c78a}
447
- .command{font-family:"Cascadia Code",Consolas,monospace;font-size:13px}
448
- .command b{color:#facc15}
449
- .terminal-result{display:flex;align-items:center;gap:8px;margin-top:9px;color:#aeb9ca;font-size:12px}
450
- .terminal-result span{color:#52d49e;font-weight:900}
451
- .return-note{display:flex;max-width:420px;margin:24px auto 0;padding:14px 16px;align-items:center;gap:12px;text-align:left;background:#fff2ed;border:1px solid #f6c9bd;border-radius:18px;color:#7e2f29}
452
- .return-note>span{font-size:24px}
453
- .return-note strong,.return-note small{display:block}
454
- .return-note small{margin-top:3px;color:#875b56}
455
- .footer{justify-content:center;color:#6f788a;font-size:11px}
456
- @keyframes arrive{from{opacity:0;transform:translateY(12px) scale(.985)}to{opacity:1;transform:none}}
457
- @keyframes mascot-arrive{from{opacity:0;transform:translateY(10px) rotate(-4deg) scale(.88)}to{opacity:1;transform:none}}
458
- @media(max-width:600px){.secure{display:none}.panel{border-radius:24px}.shape-c{display:none}}
459
- @media(max-height:700px){.shell{padding:16px}.stage{padding:6px 0}.panel{padding:20px 26px}.mascot-wrap{width:88px;height:72px;margin-bottom:6px}.mascot-wrap:before{width:68px;height:68px;box-shadow:0 0 0 10px #fff9d8}.mascot-wrap svg{width:60px;height:60px}.eyebrow{margin-bottom:8px}h1{font-size:32px}.message{margin-top:10px}.terminal,.return-note{margin-top:14px}}
460
- @media(prefers-reduced-motion:reduce){.panel,.mascot-wrap svg{animation:none}}
461
- </style>
462
- </head>
463
- <body class="${tone}">
464
- <main class="shell">
465
- <span class="shape shape-a"></span><span class="shape shape-b"></span><span class="shape shape-c"></span>
466
- <header class="topbar">
467
- <div class="brand">${ROLINO_MASCOT_SVG}<span>Rolino</span></div>
468
- <div class="secure"><span class="secure-dot"></span>Secure CLI authorization</div>
469
- </header>
470
- <section class="stage">
471
- <div class="panel">
472
- <div class="mascot-wrap">${ROLINO_MASCOT_SVG}</div>
473
- <p class="eyebrow">${eyebrow}</p>
474
- <h1>${escapeHtml(options.title)}</h1>
475
- <p class="message">${escapeHtml(options.message)}</p>
476
- ${detail}
477
- </div>
478
- </section>
479
- <footer class="footer">Secure browser handoff \xB7 no password shared</footer>
480
- </main>
481
- </body>
482
- </html>`;
408
+ function renderOAuthAuthorizationResultPage(options) {
409
+ const color = options.success ? "#087f5b" : "#b42318";
410
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="light dark"><title>${escapeHtml(options.title)} \xB7 Rolino</title><style>body{min-height:100vh;margin:0;display:grid;place-items:center;background:#faf9f1;color:#0f172a;font:16px/1.5 system-ui,sans-serif}.card{width:min(34rem,calc(100% - 2rem));box-sizing:border-box;padding:2.5rem;border:1px solid #dfe3ea;border-radius:1.5rem;background:#fff;text-align:center;box-shadow:0 24px 70px #0f172a20}h1{margin:0 0 .75rem;color:${color};font-size:2rem}p{margin:0;color:#52607a}@media(prefers-color-scheme:dark){body{background:#111827;color:#f8fafc}.card{background:#182235;border-color:#344258}p{color:#cbd5e1}}</style></head><body><main class="card"><h1>${escapeHtml(options.title)}</h1><p>${escapeHtml(options.message)}</p></main></body></html>`;
483
411
  }
484
412
  function writePage(response, status, html) {
485
- response.writeHead(status, PAGE_HEADERS);
413
+ response.writeHead(status, {
414
+ "cache-control": "no-store",
415
+ "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; frame-ancestors 'none'",
416
+ "content-type": "text/html; charset=utf-8",
417
+ "referrer-policy": "no-referrer",
418
+ "x-content-type-options": "nosniff"
419
+ });
486
420
  response.end(html);
487
421
  }
488
422
  function secretsMatch(left, right) {
@@ -490,41 +424,63 @@ function secretsMatch(left, right) {
490
424
  const rightBytes = Buffer.from(right);
491
425
  return leftBytes.length === rightBytes.length && timingSafeEqual(leftBytes, rightBytes);
492
426
  }
493
- function createBrowserAuthorizationRequest(options) {
427
+ function createBrowserAuthorizationRequest(baseUrl) {
428
+ const configuration = oauthConfiguration(baseUrl);
494
429
  const state = randomBytes(32).toString("base64url");
495
430
  const codeVerifier = randomBytes(32).toString("base64url");
496
431
  const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
497
- const url = new URL("/cli/authorize", options.baseUrl);
498
- const requestExpiresAt = new Date(
499
- Date.now() + CLI_BROWSER_AUTHORIZATION_TIMEOUT_MS
500
- ).toISOString();
501
- url.searchParams.set("redirect_uri", options.redirectUri);
432
+ const redirectUri = `http://127.0.0.1:${OAUTH_CALLBACK_PORT}${OAUTH_CALLBACK_PATH}`;
433
+ const url = new URL(`${configuration.issuer}/oauth2/authorize`);
434
+ url.searchParams.set("response_type", "code");
435
+ url.searchParams.set("client_id", configuration.clientId);
436
+ url.searchParams.set("redirect_uri", redirectUri);
437
+ url.searchParams.set("scope", DEFAULT_SCOPES.join(" "));
438
+ url.searchParams.set("resource", configuration.resource);
502
439
  url.searchParams.set("state", state);
503
440
  url.searchParams.set("code_challenge", codeChallenge);
504
441
  url.searchParams.set("code_challenge_method", "S256");
505
- url.searchParams.set(
506
- "client_name",
507
- (options.clientName ?? `Rolino CLI on ${hostname()}`).slice(0, 32)
508
- );
509
- url.searchParams.set("request_expires_at", requestExpiresAt);
510
- return {
511
- url: url.toString(),
512
- state,
513
- codeVerifier,
514
- codeChallenge,
515
- requestExpiresAt
516
- };
442
+ return { configuration, url: url.toString(), redirectUri, state, codeVerifier, codeChallenge };
517
443
  }
518
- var CliAuthorizationDeniedError = class extends Error {
444
+ var OAuthAuthorizationDeniedError = class extends Error {
519
445
  constructor() {
520
446
  super("Authorization was denied.");
521
- this.name = "CliAuthorizationDeniedError";
447
+ this.name = "OAuthAuthorizationDeniedError";
522
448
  }
523
449
  };
450
+ async function exchangeAuthorizationCode(options) {
451
+ const { authorization } = options;
452
+ const response = await options.fetch(`${authorization.configuration.issuer}/oauth2/token`, {
453
+ method: "POST",
454
+ headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
455
+ body: new URLSearchParams({
456
+ grant_type: "authorization_code",
457
+ code: options.code,
458
+ redirect_uri: authorization.redirectUri,
459
+ client_id: authorization.configuration.clientId,
460
+ code_verifier: authorization.codeVerifier,
461
+ resource: authorization.configuration.resource
462
+ })
463
+ });
464
+ const payload = await response.json().catch(() => null);
465
+ if (!response.ok || !payload || typeof payload.access_token !== "string" || typeof payload.refresh_token !== "string") throw new Error("Rolino could not exchange the OAuth authorization code.");
466
+ const expiresIn = typeof payload.expires_in === "number" ? payload.expires_in : 300;
467
+ return {
468
+ host: authorization.configuration.host,
469
+ issuer: authorization.configuration.issuer,
470
+ clientId: authorization.configuration.clientId,
471
+ resource: authorization.configuration.resource,
472
+ accessToken: payload.access_token,
473
+ accessTokenExpiresAt: new Date(Date.now() + expiresIn * 1e3).toISOString(),
474
+ refreshToken: payload.refresh_token,
475
+ refreshTokenExpiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3).toISOString(),
476
+ scopes: typeof payload.scope === "string" ? payload.scope.split(/\s+/).filter(Boolean) : [...DEFAULT_SCOPES]
477
+ };
478
+ }
524
479
  async function loginWithBrowser(options) {
480
+ const fetchImplementation = options.fetch ?? globalThis.fetch;
525
481
  return new Promise((resolve3, reject) => {
526
482
  let settled = false;
527
- let authorization = null;
483
+ const authorization = createBrowserAuthorizationRequest(options.baseUrl);
528
484
  const finish = (callback) => {
529
485
  if (settled) return;
530
486
  settled = true;
@@ -534,82 +490,46 @@ async function loginWithBrowser(options) {
534
490
  };
535
491
  const server = createServer((request, response) => {
536
492
  const url = new URL(request.url ?? "/", "http://127.0.0.1");
537
- if (request.method !== "GET" || url.pathname !== "/callback") {
493
+ if (request.method !== "GET" || url.pathname !== OAUTH_CALLBACK_PATH) {
538
494
  response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
539
495
  response.end("Not found");
540
496
  return;
541
497
  }
542
498
  void (async () => {
543
- if (!authorization) {
544
- writePage(response, 503, renderCliAuthorizationResultPage({
545
- title: "Authorization is starting",
546
- message: "Return to your terminal and try the browser link again in a moment.",
547
- success: false
548
- }));
549
- return;
550
- }
551
499
  if (!secretsMatch(url.searchParams.get("state") ?? "", authorization.state)) {
552
- writePage(response, 400, renderCliAuthorizationResultPage({
553
- title: "Authorization failed",
554
- message: "The callback state did not match. Return to your terminal and try again.",
555
- success: false
556
- }));
557
- finish(() => reject(new Error("The browser callback state did not match.")));
500
+ writePage(response, 400, renderOAuthAuthorizationResultPage({ title: "Authorization failed", message: "The callback state did not match.", success: false }));
501
+ finish(() => reject(new Error("The OAuth callback state did not match.")));
558
502
  return;
559
503
  }
560
504
  if (url.searchParams.get("error") === "access_denied") {
561
- writePage(response, 200, renderCliAuthorizationResultPage({
562
- title: "Access denied",
563
- message: "No credential was created. You can close this window and return to your terminal.",
564
- success: false
565
- }));
566
- finish(() => reject(new CliAuthorizationDeniedError()));
505
+ writePage(response, 200, renderOAuthAuthorizationResultPage({ title: "Access denied", message: "No OAuth token was saved.", success: false }));
506
+ finish(() => reject(new OAuthAuthorizationDeniedError()));
507
+ return;
508
+ }
509
+ if (url.searchParams.get("iss") !== authorization.configuration.issuer) {
510
+ writePage(response, 400, renderOAuthAuthorizationResultPage({ title: "Authorization failed", message: "The authorization server did not match.", success: false }));
511
+ finish(() => reject(new Error("The OAuth callback issuer did not match.")));
567
512
  return;
568
513
  }
569
514
  const code = url.searchParams.get("code");
570
- if (!code || !/^[A-Za-z0-9_-]{43,128}$/.test(code)) {
571
- writePage(response, 400, renderCliAuthorizationResultPage({
572
- title: "Authorization failed",
573
- message: "Rolino did not return a valid one-time code. Return to your terminal and try again.",
574
- success: false
575
- }));
576
- finish(() => reject(new Error("The browser callback did not include a valid code.")));
515
+ if (!code || code.length > 2048) {
516
+ writePage(response, 400, renderOAuthAuthorizationResultPage({ title: "Authorization failed", message: "Rolino did not return a valid code.", success: false }));
517
+ finish(() => reject(new Error("The OAuth callback did not include a valid code.")));
577
518
  return;
578
519
  }
579
520
  try {
580
- const credential = await options.exchange({
581
- code,
582
- codeVerifier: authorization.codeVerifier
583
- });
584
- writePage(response, 200, renderCliAuthorizationResultPage({
585
- title: "Terminal connected",
586
- message: "Rolino is ready in your terminal. You can close this window.",
587
- success: true
588
- }));
589
- finish(() => resolve3(credential));
521
+ const tokenSet = await exchangeAuthorizationCode({ authorization, code, fetch: fetchImplementation });
522
+ writePage(response, 200, renderOAuthAuthorizationResultPage({ title: "Terminal connected", message: "You can close this window and return to your terminal.", success: true }));
523
+ finish(() => resolve3(tokenSet));
590
524
  } catch (error) {
591
- writePage(response, 502, renderCliAuthorizationResultPage({
592
- title: "Authorization failed",
593
- message: "The CLI could not exchange its one-time code. Return to your terminal and try again.",
594
- success: false
595
- }));
525
+ writePage(response, 502, renderOAuthAuthorizationResultPage({ title: "Authorization failed", message: "The CLI could not exchange the code.", success: false }));
596
526
  finish(() => reject(error));
597
527
  }
598
528
  })();
599
529
  });
600
530
  server.once("error", (error) => finish(() => reject(error)));
601
- server.listen(0, "127.0.0.1", () => {
602
- const address = server.address();
603
- if (!address || typeof address === "string") {
604
- finish(() => reject(new Error("Rolino could not start the local authorization callback.")));
605
- return;
606
- }
607
- authorization = createBrowserAuthorizationRequest({
608
- baseUrl: options.baseUrl,
609
- redirectUri: `http://127.0.0.1:${address.port}/callback`
610
- });
611
- options.stderr.write(`Opening Rolino in your browser\u2026
612
- `);
531
+ server.listen(OAUTH_CALLBACK_PORT, "127.0.0.1", () => {
532
+ options.stderr.write("Opening Rolino OAuth sign-in in your browser\u2026\n");
613
533
  options.stderr.write(`If it does not open, visit:
614
534
  ${authorization.url}
615
535
  `);
@@ -617,16 +537,17 @@ ${authorization.url}
617
537
  void open(authorization.url).catch(() => void 0);
618
538
  });
619
539
  const timeout = setTimeout(() => {
620
- finish(() => reject(new Error("Browser authorization timed out after 5 minutes.")));
621
- }, CLI_BROWSER_AUTHORIZATION_TIMEOUT_MS);
540
+ finish(() => reject(new Error("OAuth sign-in timed out after 5 minutes.")));
541
+ }, OAUTH_TIMEOUT_MS);
622
542
  });
623
543
  }
624
544
 
625
545
  // src/cli.ts
626
546
  import {
627
- clearStoredCredential,
628
- resolveCredential,
629
- saveStoredCredential
547
+ clearOAuthTokenSet,
548
+ createOAuthAccessTokenProvider,
549
+ resolveLocalAuthentication,
550
+ saveOAuthTokenSet
630
551
  } from "@rolino/local-auth";
631
552
 
632
553
  // src/output.ts
@@ -1060,6 +981,23 @@ async function requireWriteConsent(options) {
1060
981
  );
1061
982
  }
1062
983
  }
984
+ var BLOG_WEEKDAY_VALUES = {
985
+ sun: 0,
986
+ mon: 1,
987
+ tue: 2,
988
+ wed: 3,
989
+ thu: 4,
990
+ fri: 5,
991
+ sat: 6
992
+ };
993
+ function parseBlogWeekdays(value) {
994
+ const names = value.split(",").map((day) => day.trim().toLowerCase());
995
+ const weekdays = names.map((day) => BLOG_WEEKDAY_VALUES[day]).filter((day) => day !== void 0);
996
+ if (!weekdays.length || weekdays.length !== names.length || new Set(weekdays).size !== weekdays.length) {
997
+ throw new TypeError("--weekdays must contain unique comma-separated sun,mon,tue,wed,thu,fri,sat values.");
998
+ }
999
+ return weekdays;
1000
+ }
1063
1001
  function requireBlogExecutionConsent(options) {
1064
1002
  if (options.yes || resolveFormat(options.global, options.runtime) !== "human") return;
1065
1003
  throw new TypeError("This Blog execute command requires explicit consent. Review the confirmed operation and pass --yes.");
@@ -1090,16 +1028,16 @@ function baseUrlFor(options, runtime) {
1090
1028
  function clientFor(options, runtime) {
1091
1029
  const timeoutMs = options.timeout ?? (runtime.env.ROLINO_TIMEOUT ? duration(runtime.env.ROLINO_TIMEOUT) : 15e3);
1092
1030
  const baseUrl = baseUrlFor(options, runtime);
1093
- const credential = resolveCredential(baseUrl, runtime.env);
1031
+ const token = runtime.env.ROLINO_TOKEN ?? createOAuthAccessTokenProvider({ baseUrl, env: runtime.env, fetch: runtime.fetch });
1094
1032
  return new RolinoClient({
1095
1033
  baseUrl,
1096
- token: credential.token ?? void 0,
1034
+ token,
1097
1035
  timeoutMs,
1098
1036
  fetch: runtime.fetch
1099
1037
  });
1100
1038
  }
1101
1039
  function exitCodeFor(error) {
1102
- if (error instanceof CliAuthorizationDeniedError || error instanceof CliUserCancelledError) {
1040
+ if (error instanceof OAuthAuthorizationDeniedError || error instanceof CliUserCancelledError) {
1103
1041
  return EXIT_CODES.cancelled;
1104
1042
  }
1105
1043
  if (error instanceof RolinoNetworkError) {
@@ -1132,7 +1070,7 @@ function exitCodeFor(error) {
1132
1070
  return EXIT_CODES.unexpected;
1133
1071
  }
1134
1072
  function errorForOutput(error) {
1135
- if (error instanceof CliAuthorizationDeniedError || error instanceof CliUserCancelledError) {
1073
+ if (error instanceof OAuthAuthorizationDeniedError || error instanceof CliUserCancelledError) {
1136
1074
  return { code: "CANCELLED", message: error.message };
1137
1075
  }
1138
1076
  if (error instanceof RolinoApiError) {
@@ -1393,22 +1331,14 @@ async function runCli(argv = process.argv, overrides = {}) {
1393
1331
  "ROLINO_TOKEN is set and overrides browser login. Unset it before saving a CLI credential."
1394
1332
  );
1395
1333
  }
1396
- const meta = await client.meta({ requestId: context.requestId });
1397
- if (!meta.authentication.cliBrowserAuthorization) {
1398
- throw new TypeError(
1399
- "This Rolino server does not support browser CLI authorization."
1400
- );
1401
- }
1402
- const credential = await loginWithBrowser({
1334
+ const tokenSet = await loginWithBrowser({
1403
1335
  baseUrl: client.baseUrl,
1404
1336
  stderr: runtime.stderr,
1405
- exchange: (input) => client.auth.exchangeCliAuthorization(input, {
1406
- requestId: context.requestId
1407
- })
1337
+ fetch: runtime.fetch
1408
1338
  });
1409
1339
  const authenticatedClient = new RolinoClient({
1410
1340
  baseUrl: client.baseUrl,
1411
- token: credential.token,
1341
+ token: tokenSet.accessToken,
1412
1342
  timeoutMs: client.timeoutMs,
1413
1343
  fetch: runtime.fetch
1414
1344
  });
@@ -1417,12 +1347,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1417
1347
  const actor = await authenticatedClient.whoami({
1418
1348
  requestId: context.requestId
1419
1349
  });
1420
- const path = saveStoredCredential(client.baseUrl, {
1421
- token: credential.token,
1422
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1423
- expiresAt: credential.expiresAt,
1424
- organization: credential.organization
1425
- }, runtime.env);
1350
+ const path = saveOAuthTokenSet(tokenSet, runtime.env);
1426
1351
  credentialSaved = true;
1427
1352
  writeSuccess(
1428
1353
  context,
@@ -1431,23 +1356,20 @@ async function runCli(argv = process.argv, overrides = {}) {
1431
1356
  user: actor.user,
1432
1357
  organization: actor.organization,
1433
1358
  capabilities: actor.capabilities,
1434
- expiresAt: credential.expiresAt
1359
+ expiresAt: tokenSet.accessTokenExpiresAt
1435
1360
  },
1436
1361
  [
1437
1362
  `Authenticated as ${actor.user.email}.`,
1438
1363
  `Workspace: ${actor.organization.name}`,
1439
- `Expires: ${credential.expiresAt}`,
1364
+ `Access token expires: ${tokenSet.accessTokenExpiresAt}`,
1440
1365
  `Credential saved in a permission-restricted file at ${path}`
1441
1366
  ].join("\n"),
1442
1367
  ["rolino whoami --agent", "rolino projects list --agent"]
1443
1368
  );
1444
1369
  } catch (error) {
1445
1370
  if (credentialSaved) {
1446
- clearStoredCredential(client.baseUrl, runtime.env);
1371
+ clearOAuthTokenSet(client.baseUrl, runtime.env);
1447
1372
  }
1448
- await authenticatedClient.auth.revokeCurrentCredential({
1449
- requestId: context.requestId
1450
- }).catch(() => void 0);
1451
1373
  throw error;
1452
1374
  }
1453
1375
  }
@@ -1460,7 +1382,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1460
1382
  global,
1461
1383
  runtime,
1462
1384
  async action(context, client) {
1463
- const credential = resolveCredential(client.baseUrl, runtime.env);
1385
+ const credential = resolveLocalAuthentication(client.baseUrl, runtime.env);
1464
1386
  if (credential.source === "none") {
1465
1387
  writeSuccess(
1466
1388
  context,
@@ -1479,7 +1401,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1479
1401
  baseUrl: client.baseUrl,
1480
1402
  user: actor.user,
1481
1403
  organization: actor.organization,
1482
- ...credential.source === "stored" ? { expiresAt: credential.credential.expiresAt } : {}
1404
+ ...credential.source === "oauth" && credential.tokenSet ? { expiresAt: credential.tokenSet.accessTokenExpiresAt } : {}
1483
1405
  },
1484
1406
  [
1485
1407
  `Authenticated as ${actor.user.email}.`,
@@ -1497,7 +1419,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1497
1419
  global,
1498
1420
  runtime,
1499
1421
  async action(context, client) {
1500
- const credential = resolveCredential(client.baseUrl, runtime.env);
1422
+ const credential = resolveLocalAuthentication(client.baseUrl, runtime.env);
1501
1423
  if (credential.source === "environment") {
1502
1424
  throw new TypeError(
1503
1425
  "ROLINO_TOKEN is set. Remove it from the environment instead of using auth logout."
@@ -1511,15 +1433,36 @@ async function runCli(argv = process.argv, overrides = {}) {
1511
1433
  );
1512
1434
  return;
1513
1435
  }
1514
- try {
1515
- await client.auth.revokeCurrentCredential({
1516
- requestId: context.requestId
1436
+ const tokenSet = credential.tokenSet;
1437
+ const fetchImplementation = runtime.fetch ?? globalThis.fetch;
1438
+ const accessToken = await createOAuthAccessTokenProvider({
1439
+ baseUrl: client.baseUrl,
1440
+ env: runtime.env,
1441
+ fetch: fetchImplementation
1442
+ })();
1443
+ if (accessToken) {
1444
+ const disconnect = await fetchImplementation(`${client.baseUrl}/api/v1/oauth/logout`, {
1445
+ method: "POST",
1446
+ headers: {
1447
+ authorization: `Bearer ${accessToken}`,
1448
+ accept: "application/json",
1449
+ "x-request-id": context.requestId
1450
+ }
1517
1451
  });
1518
- } catch (error) {
1519
- const alreadyUnavailable = error instanceof RolinoApiError && (error.code === "AUTH_REQUIRED" || error.code === "NOT_FOUND");
1520
- if (!alreadyUnavailable) throw error;
1452
+ if (!disconnect.ok && disconnect.status !== 401 && disconnect.status !== 404) {
1453
+ throw new TypeError("Rolino could not disconnect the OAuth workspace grant.");
1454
+ }
1521
1455
  }
1522
- clearStoredCredential(client.baseUrl, runtime.env);
1456
+ await fetchImplementation(`${tokenSet.issuer}/oauth2/revoke`, {
1457
+ method: "POST",
1458
+ headers: { "content-type": "application/x-www-form-urlencoded" },
1459
+ body: new URLSearchParams({
1460
+ token: tokenSet.refreshToken,
1461
+ token_type_hint: "refresh_token",
1462
+ client_id: tokenSet.clientId
1463
+ })
1464
+ }).catch(() => void 0);
1465
+ clearOAuthTokenSet(client.baseUrl, runtime.env);
1523
1466
  writeSuccess(
1524
1467
  context,
1525
1468
  { revoked: true, baseUrl: client.baseUrl },
@@ -1598,7 +1541,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1598
1541
  });
1599
1542
  });
1600
1543
  const posts = program.command("posts").description("Read and prepare posts in a Rolino project");
1601
- posts.command("create").description("Create a draft post without scheduling or publishing it").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--caption <text>", "draft caption; use an empty string for media-only drafts").option("--platform <provider>", "INSTAGRAM, TIKTOK, YOUTUBE, BLUESKY, or LINKEDIN; repeat as needed", collectPublishingProvider, []).option("--media <asset-id>", "existing project media asset ID; repeat for multiple", collectString, []).option("--instagram-caption <text>", "Instagram-specific caption override").option("--tiktok-caption <text>", "TikTok-specific caption override").option("--tiktok-mode <mode>", "direct publishing or inbox draft delivery", tiktokPostMode).option("--tiktok-visibility <value>", "exact visibility returned by integrations delivery-options").option("--tiktok-comments <yes|no>", "allow TikTok comments", yesOrNo).option("--tiktok-duet <yes|no>", "allow TikTok duets", yesOrNo).option("--tiktok-stitch <yes|no>", "allow TikTok stitches", yesOrNo).option("--tiktok-commercial-content <yes|no>", "declare commercial TikTok content", yesOrNo).option("--tiktok-promotes-own-brand <yes|no>", "declare own-brand promotion", yesOrNo).option("--tiktok-promotes-third-party <yes|no>", "declare third-party promotion", yesOrNo).option("--tiktok-ai-generated <yes|no>", "declare AI-generated TikTok content", yesOrNo).option("--tiktok-cover-timestamp-ms <number>", "video cover timestamp in milliseconds", nonnegativeInteger).option("--tiktok-settings-reviewed", "confirm the TikTok account choices were reviewed; separate from --yes").option("--youtube-caption <text>", "YouTube description override; otherwise the shared caption is used").option("--bluesky-caption <text>", "Bluesky-specific caption override; limited to 300 graphemes").option("--linkedin-caption <text>", "LinkedIn-specific caption override; limited to 3,000 characters").option("--youtube-title <text>", "required YouTube video title").option("--youtube-category-id <id>", "required numeric YouTube video category ID").option("--youtube-privacy <status>", "required PUBLIC, UNLISTED, or PRIVATE visibility", youtubePrivacy).option("--youtube-made-for-kids <yes|no>", "required explicit YouTube audience declaration", yesOrNo).option("--youtube-synthetic-media", "declare realistic altered or synthetic media to YouTube").option("--no-youtube-notify-subscribers", "disable eligible YouTube subscriber notifications").option("--youtube-tag <tag>", "YouTube tag; repeat as needed", collectString, []).option("--idempotency-key <key>", "stable retry key; defaults to the request ID").option("--yes", "confirm creation without an interactive prompt").action(async (local) => {
1544
+ posts.command("create").description("Create a draft post without scheduling or publishing it").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--caption <text>", "draft caption; use an empty string for media-only drafts").option("--platform <provider>", "INSTAGRAM, TIKTOK, YOUTUBE, BLUESKY, LINKEDIN, or GOOGLE_BUSINESS_PROFILE; repeat as needed", collectPublishingProvider, []).option("--media <asset-id>", "existing project media asset ID; repeat for multiple", collectString, []).option("--instagram-caption <text>", "Instagram-specific caption override").option("--tiktok-caption <text>", "TikTok-specific caption override").option("--tiktok-mode <mode>", "direct publishing or inbox draft delivery", tiktokPostMode).option("--tiktok-visibility <value>", "exact visibility returned by integrations delivery-options").option("--tiktok-comments <yes|no>", "allow TikTok comments", yesOrNo).option("--tiktok-duet <yes|no>", "allow TikTok duets", yesOrNo).option("--tiktok-stitch <yes|no>", "allow TikTok stitches", yesOrNo).option("--tiktok-commercial-content <yes|no>", "declare commercial TikTok content", yesOrNo).option("--tiktok-promotes-own-brand <yes|no>", "declare own-brand promotion", yesOrNo).option("--tiktok-promotes-third-party <yes|no>", "declare third-party promotion", yesOrNo).option("--tiktok-ai-generated <yes|no>", "declare AI-generated TikTok content", yesOrNo).option("--tiktok-cover-timestamp-ms <number>", "video cover timestamp in milliseconds", nonnegativeInteger).option("--tiktok-settings-reviewed", "confirm the TikTok account choices were reviewed; separate from --yes").option("--youtube-caption <text>", "YouTube description override; otherwise the shared caption is used").option("--bluesky-caption <text>", "Bluesky-specific caption override; limited to 300 graphemes").option("--linkedin-caption <text>", "LinkedIn-specific caption override; limited to 3,000 characters").option("--google-business-profile-caption <text>", "Google Business Profile update override; limited to 1,500 characters").option("--youtube-title <text>", "required YouTube video title").option("--youtube-category-id <id>", "required numeric YouTube video category ID").option("--youtube-privacy <status>", "required PUBLIC, UNLISTED, or PRIVATE visibility", youtubePrivacy).option("--youtube-made-for-kids <yes|no>", "required explicit YouTube audience declaration", yesOrNo).option("--youtube-synthetic-media", "declare realistic altered or synthetic media to YouTube").option("--no-youtube-notify-subscribers", "disable eligible YouTube subscriber notifications").option("--youtube-tag <tag>", "YouTube tag; repeat as needed", collectString, []).option("--idempotency-key <key>", "stable retry key; defaults to the request ID").option("--yes", "confirm creation without an interactive prompt").action(async (local) => {
1602
1545
  const global = program.opts();
1603
1546
  commandExitCode = await execute({
1604
1547
  command: "posts create",
@@ -1625,7 +1568,8 @@ async function runCli(argv = process.argv, overrides = {}) {
1625
1568
  ...local.tiktokCaption === void 0 ? {} : { TIKTOK: local.tiktokCaption },
1626
1569
  ...local.youtubeCaption === void 0 ? {} : { YOUTUBE: local.youtubeCaption },
1627
1570
  ...local.blueskyCaption === void 0 ? {} : { BLUESKY: local.blueskyCaption },
1628
- ...local.linkedinCaption === void 0 ? {} : { LINKEDIN: local.linkedinCaption }
1571
+ ...local.linkedinCaption === void 0 ? {} : { LINKEDIN: local.linkedinCaption },
1572
+ ...local.googleBusinessProfileCaption === void 0 ? {} : { GOOGLE_BUSINESS_PROFILE: local.googleBusinessProfileCaption }
1629
1573
  },
1630
1574
  tiktokSettings: tiktokSettings(local) ?? null,
1631
1575
  youtubeSettings: youtubeSettings(local)
@@ -1643,7 +1587,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1643
1587
  }
1644
1588
  });
1645
1589
  });
1646
- posts.command("update").description("Update selected draft fields using optimistic concurrency").argument("<post-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--expected-version <number>", "current post version from posts show", positiveInteger).option("--caption <text>", "replacement draft caption; omitted fields are preserved").option("--platform <provider>", "replacement destinations; repeat as needed", collectPublishingProvider).option("--clear-platforms", "remove every draft destination").option("--media <asset-id>", "replacement media asset ID; repeat for multiple", collectString).option("--clear-media", "remove every media asset from the draft").option("--instagram-caption <text>", "Instagram-specific caption override").option("--tiktok-caption <text>", "TikTok-specific caption override").option("--tiktok-mode <mode>", "direct publishing or inbox draft delivery", tiktokPostMode).option("--tiktok-visibility <value>", "exact visibility returned by integrations delivery-options").option("--tiktok-comments <yes|no>", "allow TikTok comments", yesOrNo).option("--tiktok-duet <yes|no>", "allow TikTok duets", yesOrNo).option("--tiktok-stitch <yes|no>", "allow TikTok stitches", yesOrNo).option("--tiktok-commercial-content <yes|no>", "declare commercial TikTok content", yesOrNo).option("--tiktok-promotes-own-brand <yes|no>", "declare own-brand promotion", yesOrNo).option("--tiktok-promotes-third-party <yes|no>", "declare third-party promotion", yesOrNo).option("--tiktok-ai-generated <yes|no>", "declare AI-generated TikTok content", yesOrNo).option("--tiktok-cover-timestamp-ms <number>", "video cover timestamp in milliseconds", nonnegativeInteger).option("--tiktok-settings-reviewed", "confirm the TikTok account choices were reviewed; separate from --yes").option("--youtube-caption <text>", "YouTube description override; otherwise the shared caption is used").option("--bluesky-caption <text>", "Bluesky-specific caption override; limited to 300 graphemes").option("--linkedin-caption <text>", "LinkedIn-specific caption override; limited to 3,000 characters").option("--youtube-title <text>", "required YouTube video title").option("--youtube-category-id <id>", "required numeric YouTube video category ID").option("--youtube-privacy <status>", "required PUBLIC, UNLISTED, or PRIVATE visibility", youtubePrivacy).option("--youtube-made-for-kids <yes|no>", "required explicit YouTube audience declaration", yesOrNo).option("--youtube-synthetic-media", "declare realistic altered or synthetic media to YouTube").option("--no-youtube-notify-subscribers", "disable eligible YouTube subscriber notifications").option("--youtube-tag <tag>", "replacement YouTube tag; repeat as needed", collectString).option("--idempotency-key <key>", "stable retry key; defaults to the request ID").option("--yes", "confirm the update without an interactive prompt").action(async (postId, local) => {
1590
+ posts.command("update").description("Update selected draft fields using optimistic concurrency").argument("<post-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--expected-version <number>", "current post version from posts show", positiveInteger).option("--caption <text>", "replacement draft caption; omitted fields are preserved").option("--platform <provider>", "replacement destinations; repeat as needed", collectPublishingProvider).option("--clear-platforms", "remove every draft destination").option("--media <asset-id>", "replacement media asset ID; repeat for multiple", collectString).option("--clear-media", "remove every media asset from the draft").option("--instagram-caption <text>", "Instagram-specific caption override").option("--tiktok-caption <text>", "TikTok-specific caption override").option("--tiktok-mode <mode>", "direct publishing or inbox draft delivery", tiktokPostMode).option("--tiktok-visibility <value>", "exact visibility returned by integrations delivery-options").option("--tiktok-comments <yes|no>", "allow TikTok comments", yesOrNo).option("--tiktok-duet <yes|no>", "allow TikTok duets", yesOrNo).option("--tiktok-stitch <yes|no>", "allow TikTok stitches", yesOrNo).option("--tiktok-commercial-content <yes|no>", "declare commercial TikTok content", yesOrNo).option("--tiktok-promotes-own-brand <yes|no>", "declare own-brand promotion", yesOrNo).option("--tiktok-promotes-third-party <yes|no>", "declare third-party promotion", yesOrNo).option("--tiktok-ai-generated <yes|no>", "declare AI-generated TikTok content", yesOrNo).option("--tiktok-cover-timestamp-ms <number>", "video cover timestamp in milliseconds", nonnegativeInteger).option("--tiktok-settings-reviewed", "confirm the TikTok account choices were reviewed; separate from --yes").option("--youtube-caption <text>", "YouTube description override; otherwise the shared caption is used").option("--bluesky-caption <text>", "Bluesky-specific caption override; limited to 300 graphemes").option("--linkedin-caption <text>", "LinkedIn-specific caption override; limited to 3,000 characters").option("--google-business-profile-caption <text>", "Google Business Profile update override; limited to 1,500 characters").option("--youtube-title <text>", "required YouTube video title").option("--youtube-category-id <id>", "required numeric YouTube video category ID").option("--youtube-privacy <status>", "required PUBLIC, UNLISTED, or PRIVATE visibility", youtubePrivacy).option("--youtube-made-for-kids <yes|no>", "required explicit YouTube audience declaration", yesOrNo).option("--youtube-synthetic-media", "declare realistic altered or synthetic media to YouTube").option("--no-youtube-notify-subscribers", "disable eligible YouTube subscriber notifications").option("--youtube-tag <tag>", "replacement YouTube tag; repeat as needed", collectString).option("--idempotency-key <key>", "stable retry key; defaults to the request ID").option("--yes", "confirm the update without an interactive prompt").action(async (postId, local) => {
1647
1591
  const global = program.opts();
1648
1592
  commandExitCode = await execute({
1649
1593
  command: "posts update",
@@ -1672,7 +1616,8 @@ async function runCli(argv = process.argv, overrides = {}) {
1672
1616
  ...local.tiktokCaption === void 0 ? {} : { TIKTOK: local.tiktokCaption },
1673
1617
  ...local.youtubeCaption === void 0 ? {} : { YOUTUBE: local.youtubeCaption },
1674
1618
  ...local.blueskyCaption === void 0 ? {} : { BLUESKY: local.blueskyCaption },
1675
- ...local.linkedinCaption === void 0 ? {} : { LINKEDIN: local.linkedinCaption }
1619
+ ...local.linkedinCaption === void 0 ? {} : { LINKEDIN: local.linkedinCaption },
1620
+ ...local.googleBusinessProfileCaption === void 0 ? {} : { GOOGLE_BUSINESS_PROFILE: local.googleBusinessProfileCaption }
1676
1621
  };
1677
1622
  const resolvedTikTokSettings = tiktokSettings(local);
1678
1623
  const resolvedYouTubeSettings = youtubeSettings(local);
@@ -1820,7 +1765,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1820
1765
  });
1821
1766
  });
1822
1767
  const publish = posts.command("publish").description("Preview and queue server-confirmed immediate publishing");
1823
- publish.command("preview").description("Validate exact destinations and issue a five-minute confirmation").argument("<post-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--expected-version <number>", "current post version from posts show", positiveInteger).requiredOption("--platform <provider>", "INSTAGRAM, TIKTOK, YOUTUBE, BLUESKY, or LINKEDIN; repeat as needed", collectPublishingProvider).action(async (postId, local) => {
1768
+ publish.command("preview").description("Validate exact destinations and issue a five-minute confirmation").argument("<post-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--expected-version <number>", "current post version from posts show", positiveInteger).requiredOption("--platform <provider>", "INSTAGRAM, TIKTOK, YOUTUBE, BLUESKY, LINKEDIN, or GOOGLE_BUSINESS_PROFILE; repeat as needed", collectPublishingProvider).action(async (postId, local) => {
1824
1769
  const global = program.opts();
1825
1770
  commandExitCode = await execute({
1826
1771
  command: "posts publish preview",
@@ -1977,6 +1922,52 @@ async function runCli(argv = process.argv, overrides = {}) {
1977
1922
  writeSuccess(context, data, JSON.stringify(data, null, 2));
1978
1923
  } });
1979
1924
  });
1925
+ const blogWebhook = blogPublishing.command("webhook").description("Configure a signed custom Blog publishing webhook");
1926
+ const addWebhookChangeOptions = (command, executeChange) => {
1927
+ command.requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--site <site-id>", "exact Blog site ID").requiredOption("--name <name>", "destination name").requiredOption("--endpoint <https-url>", "public HTTPS receiver URL").requiredOption("--semantics <mode>", "DRAFT or LIVE").option("--destination <destination-id>", "saved destination to update");
1928
+ if (executeChange) command.requiredOption("--confirmation-token <token>", "short-lived token returned by preview").requiredOption("--idempotency-key <key>", "stable retry key").option("--yes", "confirm the exact webhook change");
1929
+ return command;
1930
+ };
1931
+ addWebhookChangeOptions(blogWebhook.command("preview"), false).action(async (local) => {
1932
+ const input = { destinationId: local.destination, siteId: local.site, name: local.name, endpoint: local.endpoint, semantics: local.semantics.toUpperCase() };
1933
+ const global = program.opts();
1934
+ commandExitCode = await execute({ command: "blog destinations webhook preview", global, runtime, async action(context, client) {
1935
+ const data = await client.blog.publishing.webhook.preview(local.project, input, { requestId: context.requestId });
1936
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
1937
+ } });
1938
+ });
1939
+ addWebhookChangeOptions(blogWebhook.command("execute"), true).action(async (local) => {
1940
+ const input = { destinationId: local.destination, siteId: local.site, name: local.name, endpoint: local.endpoint, semantics: local.semantics.toUpperCase() };
1941
+ const global = program.opts();
1942
+ commandExitCode = await execute({ command: "blog destinations webhook execute", global, runtime, async action(context, client) {
1943
+ await requireWriteConsent({ yes: local.yes, global, runtime, preview: `Apply the previewed custom webhook change for project ${local.project}? Store a returned signing secret only in the receiver's server environment. Test the connection before publication.` });
1944
+ const data = await client.blog.publishing.webhook.execute(local.project, { ...input, confirmationToken: local.confirmationToken, idempotencyKey: local.idempotencyKey }, { requestId: context.requestId });
1945
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
1946
+ } });
1947
+ });
1948
+ blogWebhook.command("test").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--site <site-id>", "exact Blog site ID").requiredOption("--destination <destination-id>", "saved destination ID").option("--make-primary", "make a successful LIVE destination primary").action(async (local) => {
1949
+ const global = program.opts();
1950
+ commandExitCode = await execute({ command: "blog destinations webhook test", global, runtime, async action(context, client) {
1951
+ const data = await client.blog.publishing.webhook.test(local.project, { siteId: local.site, destinationId: local.destination, makePrimary: local.makePrimary }, { requestId: context.requestId });
1952
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
1953
+ } });
1954
+ });
1955
+ const blogWebhookRotate = blogWebhook.command("rotate").description("Rotate the server-only signing secret");
1956
+ blogWebhookRotate.command("preview").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--site <site-id>", "exact Blog site ID").requiredOption("--destination <destination-id>", "saved destination ID").action(async (local) => {
1957
+ const global = program.opts();
1958
+ commandExitCode = await execute({ command: "blog destinations webhook rotate preview", global, runtime, async action(context, client) {
1959
+ const data = await client.blog.publishing.webhook.previewRotation(local.project, { siteId: local.site, destinationId: local.destination }, { requestId: context.requestId });
1960
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
1961
+ } });
1962
+ });
1963
+ blogWebhookRotate.command("execute").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--site <site-id>", "exact Blog site ID").requiredOption("--destination <destination-id>", "saved destination ID").requiredOption("--confirmation-token <token>", "short-lived token returned by rotation preview").requiredOption("--idempotency-key <key>", "stable retry key").option("--yes", "confirm immediate secret invalidation").action(async (local) => {
1964
+ const global = program.opts();
1965
+ commandExitCode = await execute({ command: "blog destinations webhook rotate execute", global, runtime, async action(context, client) {
1966
+ await requireWriteConsent({ yes: local.yes, global, runtime, preview: `Rotate the signing secret for destination ${local.destination}? The old secret stops working at once. Store the new secret only in the receiver's server environment, then test before publication.` });
1967
+ const data = await client.blog.publishing.webhook.executeRotation(local.project, { siteId: local.site, destinationId: local.destination, confirmationToken: local.confirmationToken, idempotencyKey: local.idempotencyKey }, { requestId: context.requestId });
1968
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
1969
+ } });
1970
+ });
1980
1971
  const blogSetup = blog.command("setup").description("Inspect agent-first Blog setup readiness");
1981
1972
  blogSetup.command("status").requiredOption("--project <project-id>", "exact Rolino project ID").action(async (local) => {
1982
1973
  const global = program.opts();
@@ -2009,9 +2000,7 @@ async function runCli(argv = process.argv, overrides = {}) {
2009
2000
  });
2010
2001
  const blogPlan = blog.command("plan").description("Create, retry, and review a Blog cadence plan");
2011
2002
  blogPlan.command("create").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--start <date>", "local start date as YYYY-MM-DD").requiredOption("--weekdays <days>", "comma-separated weekday names such as mon,wed,fri").option("--time-zone <zone>", "IANA time zone", "UTC").requiredOption("--idempotency-key <key>", "stable retry key").action(async (local) => {
2012
- const weekdayMap = { sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6 };
2013
- const weekdays = local.weekdays.split(",").map((day) => weekdayMap[day.trim().toLowerCase()]).filter((day) => day !== void 0);
2014
- if (!weekdays.length || weekdays.length !== local.weekdays.split(",").length) throw new TypeError("--weekdays must contain comma-separated sun,mon,tue,wed,thu,fri,sat values.");
2003
+ const weekdays = parseBlogWeekdays(local.weekdays);
2015
2004
  const global = program.opts();
2016
2005
  commandExitCode = await execute({ command: "blog plan create", global, runtime, async action(context, client) {
2017
2006
  const data = await client.blog.plans.create(local.project, { startsOn: local.start, weekdays, timeZone: local.timeZone }, local.idempotencyKey, { requestId: context.requestId });
@@ -2025,6 +2014,25 @@ async function runCli(argv = process.argv, overrides = {}) {
2025
2014
  writeSuccess(context, data, JSON.stringify(data, null, 2));
2026
2015
  } });
2027
2016
  });
2017
+ const blogPlanCadence = blogPlan.command("cadence").description("Preview or apply editorial cadence changes without publishing");
2018
+ blogPlanCadence.command("preview").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--plan <plan-id>", "exact Blog plan ID").requiredOption("--weekdays <days>", "unique comma-separated weekday names such as mon,wed,fri").requiredOption("--expected-plan-version <version>", "version returned by the latest plan read", Number).action(async (local) => {
2019
+ const global = program.opts();
2020
+ commandExitCode = await execute({ command: "blog plan cadence preview", global, runtime, async action(context, client) {
2021
+ const data = await client.blog.plans.previewCadence(local.project, local.plan, { weekdays: parseBlogWeekdays(local.weekdays), expectedPlanVersion: local.expectedPlanVersion }, { requestId: context.requestId });
2022
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2023
+ } });
2024
+ });
2025
+ blogPlanCadence.command("apply").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--plan <plan-id>", "exact Blog plan ID").requiredOption("--weekdays <days>", "unchanged weekdays from cadence preview").requiredOption("--expected-plan-version <version>", "unchanged plan version from cadence preview", Number).requiredOption("--expected-schedule-digest <digest>", "unchanged schedule digest from cadence preview").requiredOption("--confirmation-token <token>", "short-lived token returned by cadence preview").requiredOption("--idempotency-key <key>", "stable retry key").option("--yes", "confirm the editorial cadence change").action(async (local) => {
2026
+ const global = program.opts();
2027
+ commandExitCode = await execute({ command: "blog plan cadence apply", global, runtime, async action(context, client) {
2028
+ await requireWriteConsent({ yes: local.yes, global, runtime, preview: `Apply the previewed editorial cadence change?
2029
+ Project: ${local.project}
2030
+ Plan: ${local.plan}
2031
+ This moves eligible editorial dates only. It does not schedule, publish, or unpublish an article.` });
2032
+ const data = await client.blog.plans.executeCadence(local.project, local.plan, { weekdays: parseBlogWeekdays(local.weekdays), expectedPlanVersion: local.expectedPlanVersion, expectedScheduleDigest: local.expectedScheduleDigest, confirmationToken: local.confirmationToken, idempotencyKey: local.idempotencyKey }, { requestId: context.requestId });
2033
+ writeSuccess(context, data, JSON.stringify(data, null, 2));
2034
+ } });
2035
+ });
2028
2036
  blogPlan.command("items").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--plan <plan-id>", "exact Blog plan ID").action(async (local) => {
2029
2037
  const global = program.opts();
2030
2038
  commandExitCode = await execute({ command: "blog plan items", global, runtime, async action(context, client) {
@@ -2344,7 +2352,7 @@ Revision: ${local.revision}` });
2344
2352
  message: meta.mcp.streamableHttp ? "Remote Streamable HTTP is available." : "Remote MCP is gated; use the stdio server."
2345
2353
  }
2346
2354
  ];
2347
- const credential = resolveCredential(client.baseUrl, runtime.env);
2355
+ const credential = resolveLocalAuthentication(client.baseUrl, runtime.env);
2348
2356
  if (credential.source !== "none") {
2349
2357
  const actor = await client.whoami({ requestId: context.requestId });
2350
2358
  checks.push({
@@ -2412,4 +2420,4 @@ export {
2412
2420
  ROLINO_CLI_VERSION,
2413
2421
  runCli
2414
2422
  };
2415
- //# sourceMappingURL=chunk-MBZL2CK6.js.map
2423
+ //# sourceMappingURL=chunk-3WBZCTM5.js.map