@rolino/cli 0.5.0-beta.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.
- package/CHANGELOG.md +60 -0
- package/README.md +107 -38
- package/dist/bin.cjs +345 -237
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-TFNGSKT3.js → chunk-3WBZCTM5.js} +331 -222
- package/dist/chunk-3WBZCTM5.js.map +1 -0
- package/dist/index.cjs +345 -237
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +5 -5
- package/dist/chunk-TFNGSKT3.js.map +0 -1
|
@@ -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.
|
|
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.
|
|
346
|
-
"@rolino/local-auth": "0.
|
|
347
|
-
"@rolino/sdk": "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.
|
|
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
|
-
|
|
387
|
-
} from "@rolino/
|
|
385
|
+
oauthConfiguration
|
|
386
|
+
} from "@rolino/local-auth";
|
|
388
387
|
import open from "open";
|
|
389
|
-
var
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
"
|
|
394
|
-
"
|
|
395
|
-
|
|
396
|
-
|
|
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
|
"&": "&",
|
|
@@ -403,86 +405,18 @@ function escapeHtml(value) {
|
|
|
403
405
|
"'": "'"
|
|
404
406
|
})[character]);
|
|
405
407
|
}
|
|
406
|
-
function
|
|
407
|
-
const
|
|
408
|
-
|
|
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,
|
|
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(
|
|
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
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
url.searchParams.set("redirect_uri",
|
|
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.
|
|
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
|
|
444
|
+
var OAuthAuthorizationDeniedError = class extends Error {
|
|
519
445
|
constructor() {
|
|
520
446
|
super("Authorization was denied.");
|
|
521
|
-
this.name = "
|
|
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
|
-
|
|
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 !==
|
|
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,
|
|
553
|
-
|
|
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,
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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 ||
|
|
571
|
-
writePage(response, 400,
|
|
572
|
-
|
|
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
|
|
581
|
-
|
|
582
|
-
|
|
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,
|
|
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(
|
|
602
|
-
|
|
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("
|
|
621
|
-
},
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
547
|
+
clearOAuthTokenSet,
|
|
548
|
+
createOAuthAccessTokenProvider,
|
|
549
|
+
resolveLocalAuthentication,
|
|
550
|
+
saveOAuthTokenSet
|
|
630
551
|
} from "@rolino/local-auth";
|
|
631
552
|
|
|
632
553
|
// src/output.ts
|
|
@@ -1060,6 +981,27 @@ 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
|
+
}
|
|
1001
|
+
function requireBlogExecutionConsent(options) {
|
|
1002
|
+
if (options.yes || resolveFormat(options.global, options.runtime) !== "human") return;
|
|
1003
|
+
throw new TypeError("This Blog execute command requires explicit consent. Review the confirmed operation and pass --yes.");
|
|
1004
|
+
}
|
|
1063
1005
|
function formatMcpSetupPreview(result) {
|
|
1064
1006
|
return [
|
|
1065
1007
|
`Client: ${result.client === "codex" ? "Codex" : "Claude Code"}`,
|
|
@@ -1086,16 +1028,16 @@ function baseUrlFor(options, runtime) {
|
|
|
1086
1028
|
function clientFor(options, runtime) {
|
|
1087
1029
|
const timeoutMs = options.timeout ?? (runtime.env.ROLINO_TIMEOUT ? duration(runtime.env.ROLINO_TIMEOUT) : 15e3);
|
|
1088
1030
|
const baseUrl = baseUrlFor(options, runtime);
|
|
1089
|
-
const
|
|
1031
|
+
const token = runtime.env.ROLINO_TOKEN ?? createOAuthAccessTokenProvider({ baseUrl, env: runtime.env, fetch: runtime.fetch });
|
|
1090
1032
|
return new RolinoClient({
|
|
1091
1033
|
baseUrl,
|
|
1092
|
-
token
|
|
1034
|
+
token,
|
|
1093
1035
|
timeoutMs,
|
|
1094
1036
|
fetch: runtime.fetch
|
|
1095
1037
|
});
|
|
1096
1038
|
}
|
|
1097
1039
|
function exitCodeFor(error) {
|
|
1098
|
-
if (error instanceof
|
|
1040
|
+
if (error instanceof OAuthAuthorizationDeniedError || error instanceof CliUserCancelledError) {
|
|
1099
1041
|
return EXIT_CODES.cancelled;
|
|
1100
1042
|
}
|
|
1101
1043
|
if (error instanceof RolinoNetworkError) {
|
|
@@ -1128,7 +1070,7 @@ function exitCodeFor(error) {
|
|
|
1128
1070
|
return EXIT_CODES.unexpected;
|
|
1129
1071
|
}
|
|
1130
1072
|
function errorForOutput(error) {
|
|
1131
|
-
if (error instanceof
|
|
1073
|
+
if (error instanceof OAuthAuthorizationDeniedError || error instanceof CliUserCancelledError) {
|
|
1132
1074
|
return { code: "CANCELLED", message: error.message };
|
|
1133
1075
|
}
|
|
1134
1076
|
if (error instanceof RolinoApiError) {
|
|
@@ -1389,22 +1331,14 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1389
1331
|
"ROLINO_TOKEN is set and overrides browser login. Unset it before saving a CLI credential."
|
|
1390
1332
|
);
|
|
1391
1333
|
}
|
|
1392
|
-
const
|
|
1393
|
-
if (!meta.authentication.cliBrowserAuthorization) {
|
|
1394
|
-
throw new TypeError(
|
|
1395
|
-
"This Rolino server does not support browser CLI authorization."
|
|
1396
|
-
);
|
|
1397
|
-
}
|
|
1398
|
-
const credential = await loginWithBrowser({
|
|
1334
|
+
const tokenSet = await loginWithBrowser({
|
|
1399
1335
|
baseUrl: client.baseUrl,
|
|
1400
1336
|
stderr: runtime.stderr,
|
|
1401
|
-
|
|
1402
|
-
requestId: context.requestId
|
|
1403
|
-
})
|
|
1337
|
+
fetch: runtime.fetch
|
|
1404
1338
|
});
|
|
1405
1339
|
const authenticatedClient = new RolinoClient({
|
|
1406
1340
|
baseUrl: client.baseUrl,
|
|
1407
|
-
token:
|
|
1341
|
+
token: tokenSet.accessToken,
|
|
1408
1342
|
timeoutMs: client.timeoutMs,
|
|
1409
1343
|
fetch: runtime.fetch
|
|
1410
1344
|
});
|
|
@@ -1413,12 +1347,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1413
1347
|
const actor = await authenticatedClient.whoami({
|
|
1414
1348
|
requestId: context.requestId
|
|
1415
1349
|
});
|
|
1416
|
-
const path =
|
|
1417
|
-
token: credential.token,
|
|
1418
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1419
|
-
expiresAt: credential.expiresAt,
|
|
1420
|
-
organization: credential.organization
|
|
1421
|
-
}, runtime.env);
|
|
1350
|
+
const path = saveOAuthTokenSet(tokenSet, runtime.env);
|
|
1422
1351
|
credentialSaved = true;
|
|
1423
1352
|
writeSuccess(
|
|
1424
1353
|
context,
|
|
@@ -1427,23 +1356,20 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1427
1356
|
user: actor.user,
|
|
1428
1357
|
organization: actor.organization,
|
|
1429
1358
|
capabilities: actor.capabilities,
|
|
1430
|
-
expiresAt:
|
|
1359
|
+
expiresAt: tokenSet.accessTokenExpiresAt
|
|
1431
1360
|
},
|
|
1432
1361
|
[
|
|
1433
1362
|
`Authenticated as ${actor.user.email}.`,
|
|
1434
1363
|
`Workspace: ${actor.organization.name}`,
|
|
1435
|
-
`
|
|
1364
|
+
`Access token expires: ${tokenSet.accessTokenExpiresAt}`,
|
|
1436
1365
|
`Credential saved in a permission-restricted file at ${path}`
|
|
1437
1366
|
].join("\n"),
|
|
1438
1367
|
["rolino whoami --agent", "rolino projects list --agent"]
|
|
1439
1368
|
);
|
|
1440
1369
|
} catch (error) {
|
|
1441
1370
|
if (credentialSaved) {
|
|
1442
|
-
|
|
1371
|
+
clearOAuthTokenSet(client.baseUrl, runtime.env);
|
|
1443
1372
|
}
|
|
1444
|
-
await authenticatedClient.auth.revokeCurrentCredential({
|
|
1445
|
-
requestId: context.requestId
|
|
1446
|
-
}).catch(() => void 0);
|
|
1447
1373
|
throw error;
|
|
1448
1374
|
}
|
|
1449
1375
|
}
|
|
@@ -1456,7 +1382,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1456
1382
|
global,
|
|
1457
1383
|
runtime,
|
|
1458
1384
|
async action(context, client) {
|
|
1459
|
-
const credential =
|
|
1385
|
+
const credential = resolveLocalAuthentication(client.baseUrl, runtime.env);
|
|
1460
1386
|
if (credential.source === "none") {
|
|
1461
1387
|
writeSuccess(
|
|
1462
1388
|
context,
|
|
@@ -1475,7 +1401,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1475
1401
|
baseUrl: client.baseUrl,
|
|
1476
1402
|
user: actor.user,
|
|
1477
1403
|
organization: actor.organization,
|
|
1478
|
-
...credential.source === "
|
|
1404
|
+
...credential.source === "oauth" && credential.tokenSet ? { expiresAt: credential.tokenSet.accessTokenExpiresAt } : {}
|
|
1479
1405
|
},
|
|
1480
1406
|
[
|
|
1481
1407
|
`Authenticated as ${actor.user.email}.`,
|
|
@@ -1493,7 +1419,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1493
1419
|
global,
|
|
1494
1420
|
runtime,
|
|
1495
1421
|
async action(context, client) {
|
|
1496
|
-
const credential =
|
|
1422
|
+
const credential = resolveLocalAuthentication(client.baseUrl, runtime.env);
|
|
1497
1423
|
if (credential.source === "environment") {
|
|
1498
1424
|
throw new TypeError(
|
|
1499
1425
|
"ROLINO_TOKEN is set. Remove it from the environment instead of using auth logout."
|
|
@@ -1507,15 +1433,36 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1507
1433
|
);
|
|
1508
1434
|
return;
|
|
1509
1435
|
}
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
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
|
+
}
|
|
1513
1451
|
});
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1452
|
+
if (!disconnect.ok && disconnect.status !== 401 && disconnect.status !== 404) {
|
|
1453
|
+
throw new TypeError("Rolino could not disconnect the OAuth workspace grant.");
|
|
1454
|
+
}
|
|
1517
1455
|
}
|
|
1518
|
-
|
|
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);
|
|
1519
1466
|
writeSuccess(
|
|
1520
1467
|
context,
|
|
1521
1468
|
{ revoked: true, baseUrl: client.baseUrl },
|
|
@@ -1594,7 +1541,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1594
1541
|
});
|
|
1595
1542
|
});
|
|
1596
1543
|
const posts = program.command("posts").description("Read and prepare posts in a Rolino project");
|
|
1597
|
-
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
|
|
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) => {
|
|
1598
1545
|
const global = program.opts();
|
|
1599
1546
|
commandExitCode = await execute({
|
|
1600
1547
|
command: "posts create",
|
|
@@ -1621,7 +1568,8 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1621
1568
|
...local.tiktokCaption === void 0 ? {} : { TIKTOK: local.tiktokCaption },
|
|
1622
1569
|
...local.youtubeCaption === void 0 ? {} : { YOUTUBE: local.youtubeCaption },
|
|
1623
1570
|
...local.blueskyCaption === void 0 ? {} : { BLUESKY: local.blueskyCaption },
|
|
1624
|
-
...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 }
|
|
1625
1573
|
},
|
|
1626
1574
|
tiktokSettings: tiktokSettings(local) ?? null,
|
|
1627
1575
|
youtubeSettings: youtubeSettings(local)
|
|
@@ -1639,7 +1587,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1639
1587
|
}
|
|
1640
1588
|
});
|
|
1641
1589
|
});
|
|
1642
|
-
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) => {
|
|
1643
1591
|
const global = program.opts();
|
|
1644
1592
|
commandExitCode = await execute({
|
|
1645
1593
|
command: "posts update",
|
|
@@ -1668,7 +1616,8 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1668
1616
|
...local.tiktokCaption === void 0 ? {} : { TIKTOK: local.tiktokCaption },
|
|
1669
1617
|
...local.youtubeCaption === void 0 ? {} : { YOUTUBE: local.youtubeCaption },
|
|
1670
1618
|
...local.blueskyCaption === void 0 ? {} : { BLUESKY: local.blueskyCaption },
|
|
1671
|
-
...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 }
|
|
1672
1621
|
};
|
|
1673
1622
|
const resolvedTikTokSettings = tiktokSettings(local);
|
|
1674
1623
|
const resolvedYouTubeSettings = youtubeSettings(local);
|
|
@@ -1816,7 +1765,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1816
1765
|
});
|
|
1817
1766
|
});
|
|
1818
1767
|
const publish = posts.command("publish").description("Preview and queue server-confirmed immediate publishing");
|
|
1819
|
-
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
|
|
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) => {
|
|
1820
1769
|
const global = program.opts();
|
|
1821
1770
|
commandExitCode = await execute({
|
|
1822
1771
|
command: "posts publish preview",
|
|
@@ -1973,6 +1922,52 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1973
1922
|
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
1974
1923
|
} });
|
|
1975
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
|
+
});
|
|
1976
1971
|
const blogSetup = blog.command("setup").description("Inspect agent-first Blog setup readiness");
|
|
1977
1972
|
blogSetup.command("status").requiredOption("--project <project-id>", "exact Rolino project ID").action(async (local) => {
|
|
1978
1973
|
const global = program.opts();
|
|
@@ -2005,9 +2000,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
2005
2000
|
});
|
|
2006
2001
|
const blogPlan = blog.command("plan").description("Create, retry, and review a Blog cadence plan");
|
|
2007
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) => {
|
|
2008
|
-
const
|
|
2009
|
-
const weekdays = local.weekdays.split(",").map((day) => weekdayMap[day.trim().toLowerCase()]).filter((day) => day !== void 0);
|
|
2010
|
-
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);
|
|
2011
2004
|
const global = program.opts();
|
|
2012
2005
|
commandExitCode = await execute({ command: "blog plan create", global, runtime, async action(context, client) {
|
|
2013
2006
|
const data = await client.blog.plans.create(local.project, { startsOn: local.start, weekdays, timeZone: local.timeZone }, local.idempotencyKey, { requestId: context.requestId });
|
|
@@ -2021,6 +2014,25 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
2021
2014
|
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2022
2015
|
} });
|
|
2023
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
|
+
});
|
|
2024
2036
|
blogPlan.command("items").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--plan <plan-id>", "exact Blog plan ID").action(async (local) => {
|
|
2025
2037
|
const global = program.opts();
|
|
2026
2038
|
commandExitCode = await execute({ command: "blog plan items", global, runtime, async action(context, client) {
|
|
@@ -2120,6 +2132,103 @@ Article: ${articleId}` });
|
|
|
2120
2132
|
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2121
2133
|
} });
|
|
2122
2134
|
});
|
|
2135
|
+
const blogImages = blog.command("images").description("Create and review Blog images with blog:write; cannot approve or publish");
|
|
2136
|
+
blogImages.command("generate").description("Queue one exact-revision featured image with blog:write; cannot approve or publish").argument("<article-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--revision <revision-id>", "exact Blog revision ID").option("--editorial-brief <text>", "bounded visual direction; provider controls stay on the server").option("--idempotency-key <key>", "stable retry key; defaults to request ID").action(async (articleId, local) => {
|
|
2137
|
+
const global = program.opts();
|
|
2138
|
+
commandExitCode = await execute({ command: "blog images generate", global, runtime, async action(context, client) {
|
|
2139
|
+
const data = await client.blog.articles.generateImage(local.project, articleId, { revisionId: local.revision, idempotencyKey: local.idempotencyKey ?? context.requestId, editorialBrief: local.editorialBrief }, { requestId: context.requestId });
|
|
2140
|
+
writeSuccess(context, data, `Blog image generation queued.
|
|
2141
|
+
Image: ${data.image.id}
|
|
2142
|
+
Job: ${data.job.id}`);
|
|
2143
|
+
} });
|
|
2144
|
+
});
|
|
2145
|
+
blogImages.command("upload").description("Upload one JPEG, PNG, or WebP for an exact Blog revision with blog:write; cannot approve or publish").argument("<article-id>").argument("<file-path>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--revision <revision-id>", "exact Blog revision ID").requiredOption("--alt-text <text>", "reviewable image alt text").action(async (articleId, filePath, local) => {
|
|
2146
|
+
const global = program.opts();
|
|
2147
|
+
commandExitCode = await execute({ command: "blog images upload", global, runtime, async action(context, client) {
|
|
2148
|
+
const absolutePath = resolve2(runtime.cwd, filePath);
|
|
2149
|
+
const details = await stat(absolutePath).catch(() => null);
|
|
2150
|
+
if (!details?.isFile()) throw new TypeError("The Blog image path must point to a readable file.");
|
|
2151
|
+
const contentTypes = { ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".webp": "image/webp" };
|
|
2152
|
+
const extension = extname(absolutePath).toLowerCase();
|
|
2153
|
+
const contentType = contentTypes[extension];
|
|
2154
|
+
if (!contentType) throw new TypeError("Use a JPEG, PNG, or WebP Blog image.");
|
|
2155
|
+
const body = await openAsBlob(absolutePath, { type: contentType });
|
|
2156
|
+
const data = await client.blog.articles.uploadImage(local.project, articleId, { revisionId: local.revision, fileName: basename(absolutePath), contentType, fileSize: details.size, altText: local.altText, body }, { requestId: context.requestId });
|
|
2157
|
+
writeSuccess(context, data, `Blog image uploaded for review.
|
|
2158
|
+
Image: ${data.id}
|
|
2159
|
+
State: ${data.state}`);
|
|
2160
|
+
} });
|
|
2161
|
+
});
|
|
2162
|
+
blogImages.command("review").description("Review one exact Blog image with blog:write; does not approve the article or publish").argument("<article-id>").argument("<image-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--decision <decision>", "APPROVED or REJECTED").requiredOption("--expected-version <version>", "current image version", positiveInteger).option("--alt-text <text>", "final image alt text; required by the server for approval").option("--idempotency-key <key>", "stable retry key; defaults to request ID").action(async (articleId, imageId, local) => {
|
|
2163
|
+
const decision = local.decision.toUpperCase();
|
|
2164
|
+
if (decision !== "APPROVED" && decision !== "REJECTED") throw new TypeError("--decision must be APPROVED or REJECTED.");
|
|
2165
|
+
const global = program.opts();
|
|
2166
|
+
commandExitCode = await execute({ command: "blog images review", global, runtime, async action(context, client) {
|
|
2167
|
+
const data = await client.blog.articles.reviewImage(local.project, articleId, imageId, { decision, expectedVersion: local.expectedVersion, altText: local.altText ?? "", idempotencyKey: local.idempotencyKey ?? context.requestId }, { requestId: context.requestId });
|
|
2168
|
+
writeSuccess(context, data, `Blog image review saved.
|
|
2169
|
+
Image: ${data.id}
|
|
2170
|
+
State: ${data.state}`);
|
|
2171
|
+
} });
|
|
2172
|
+
});
|
|
2173
|
+
const blogApprove = blog.command("approve").description("Approve one exact Blog bundle with blog:approve; cannot publish");
|
|
2174
|
+
blogApprove.command("preview").description("Preview one exact approval bundle with blog:approve; creates a confirmation but cannot publish").argument("<article-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--revision <revision-id>", "exact Blog revision ID").action(async (articleId, local) => {
|
|
2175
|
+
const global = program.opts();
|
|
2176
|
+
commandExitCode = await execute({ command: "blog approve preview", global, runtime, async action(context, client) {
|
|
2177
|
+
const data = await client.blog.articles.previewApproval(local.project, articleId, { revisionId: local.revision }, { requestId: context.requestId });
|
|
2178
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2179
|
+
} });
|
|
2180
|
+
});
|
|
2181
|
+
blogApprove.command("execute").description("Approve the confirmed exact bundle with blog:approve; separate blog:publish access is still required").argument("<article-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--revision <revision-id>", "same exact revision used for preview").requiredOption("--confirmation-token <token>", "short-lived token returned by preview").requiredOption("--idempotency-key <key>", "stable retry key").option("--yes", "confirm exact bundle approval").action(async (articleId, local) => {
|
|
2182
|
+
const global = program.opts();
|
|
2183
|
+
commandExitCode = await execute({ command: "blog approve execute", global, runtime, async action(context, client) {
|
|
2184
|
+
requireBlogExecutionConsent({ yes: local.yes, global, runtime });
|
|
2185
|
+
const data = await client.blog.articles.executeApproval(local.project, articleId, { revisionId: local.revision, confirmationToken: local.confirmationToken, idempotencyKey: local.idempotencyKey }, { requestId: context.requestId });
|
|
2186
|
+
writeSuccess(context, data, `Exact Blog bundle approved.
|
|
2187
|
+
Revision: ${data.revisionId}
|
|
2188
|
+
Publishing still requires blog:publish.`);
|
|
2189
|
+
} });
|
|
2190
|
+
});
|
|
2191
|
+
const blogSchedule = blog.command("schedule").description("Schedule exact approved Blog bundles with blog:publish; does not grant approval");
|
|
2192
|
+
const addScheduleOptions = (command, executeSchedule) => {
|
|
2193
|
+
command.argument("<article-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--revision <revision-id>", "exact approved Blog revision ID").requiredOption("--at <iso>", "future ISO date-time with UTC offset").requiredOption("--timezone <iana>", "explicit IANA timezone").option("--destination <destination-id...>", "exact destination IDs; defaults to the current primary");
|
|
2194
|
+
if (executeSchedule) command.requiredOption("--confirmation-token <token>", "short-lived token returned by preview").requiredOption("--idempotency-key <key>", "stable retry key").option("--yes", "confirm future external publication");
|
|
2195
|
+
return command;
|
|
2196
|
+
};
|
|
2197
|
+
addScheduleOptions(blogSchedule.command("preview").description("Preview an exact future Blog schedule with blog:publish; does not publish now"), false).action(async (articleId, local) => {
|
|
2198
|
+
const global = program.opts();
|
|
2199
|
+
commandExitCode = await execute({ command: "blog schedule preview", global, runtime, async action(context, client) {
|
|
2200
|
+
const data = await client.blog.articles.previewSchedule(local.project, articleId, { revisionId: local.revision, scheduledAt: local.at, timezone: local.timezone, destinationIds: local.destination }, { requestId: context.requestId });
|
|
2201
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2202
|
+
} });
|
|
2203
|
+
});
|
|
2204
|
+
addScheduleOptions(blogSchedule.command("execute").description("Execute a confirmed future Blog schedule with blog:publish; causes future external publication"), true).action(async (articleId, local) => {
|
|
2205
|
+
const global = program.opts();
|
|
2206
|
+
commandExitCode = await execute({ command: "blog schedule execute", global, runtime, async action(context, client) {
|
|
2207
|
+
requireBlogExecutionConsent({ yes: local.yes, global, runtime });
|
|
2208
|
+
const data = await client.blog.articles.executeSchedule(local.project, articleId, { revisionId: local.revision, scheduledAt: local.at, timezone: local.timezone, destinationIds: local.destination, confirmationToken: local.confirmationToken, idempotencyKey: local.idempotencyKey }, { requestId: context.requestId });
|
|
2209
|
+
writeSuccess(context, data, `Blog article scheduled.
|
|
2210
|
+
Time: ${data.scheduledAt}
|
|
2211
|
+
Timezone: ${data.timezone}`);
|
|
2212
|
+
} });
|
|
2213
|
+
});
|
|
2214
|
+
const blogScheduleCancel = blogSchedule.command("cancel").description("Cancel one future Blog schedule with blog:publish; never unpublishes a live article");
|
|
2215
|
+
blogScheduleCancel.command("preview").description("Preview exact schedule cancellation with blog:publish; does not unpublish").argument("<article-id>").requiredOption("--project <project-id>", "exact Rolino project ID").action(async (articleId, local) => {
|
|
2216
|
+
const global = program.opts();
|
|
2217
|
+
commandExitCode = await execute({ command: "blog schedule cancel preview", global, runtime, async action(context, client) {
|
|
2218
|
+
const data = await client.blog.articles.previewScheduleCancellation(local.project, articleId, { requestId: context.requestId });
|
|
2219
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2220
|
+
} });
|
|
2221
|
+
});
|
|
2222
|
+
blogScheduleCancel.command("execute").description("Cancel the confirmed future schedule with blog:publish; never unpublishes a live article").argument("<article-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--confirmation-token <token>", "short-lived token returned by preview").requiredOption("--idempotency-key <key>", "stable retry key").option("--yes", "confirm schedule cancellation").action(async (articleId, local) => {
|
|
2223
|
+
const global = program.opts();
|
|
2224
|
+
commandExitCode = await execute({ command: "blog schedule cancel execute", global, runtime, async action(context, client) {
|
|
2225
|
+
requireBlogExecutionConsent({ yes: local.yes, global, runtime });
|
|
2226
|
+
const data = await client.blog.articles.executeScheduleCancellation(local.project, articleId, { confirmationToken: local.confirmationToken, idempotencyKey: local.idempotencyKey }, { requestId: context.requestId });
|
|
2227
|
+
writeSuccess(context, data, `Future Blog schedule canceled.
|
|
2228
|
+
Article: ${data.articleId}
|
|
2229
|
+
A live article was not unpublished.`);
|
|
2230
|
+
} });
|
|
2231
|
+
});
|
|
2123
2232
|
blog.command("job").argument("<job-id>").requiredOption("--project <project-id>", "exact Rolino project ID").action(async (jobId, local) => {
|
|
2124
2233
|
const global = program.opts();
|
|
2125
2234
|
commandExitCode = await execute({ command: "blog job", global, runtime, async action(context, client) {
|
|
@@ -2243,7 +2352,7 @@ Revision: ${local.revision}` });
|
|
|
2243
2352
|
message: meta.mcp.streamableHttp ? "Remote Streamable HTTP is available." : "Remote MCP is gated; use the stdio server."
|
|
2244
2353
|
}
|
|
2245
2354
|
];
|
|
2246
|
-
const credential =
|
|
2355
|
+
const credential = resolveLocalAuthentication(client.baseUrl, runtime.env);
|
|
2247
2356
|
if (credential.source !== "none") {
|
|
2248
2357
|
const actor = await client.whoami({ requestId: context.requestId });
|
|
2249
2358
|
checks.push({
|
|
@@ -2311,4 +2420,4 @@ export {
|
|
|
2311
2420
|
ROLINO_CLI_VERSION,
|
|
2312
2421
|
runCli
|
|
2313
2422
|
};
|
|
2314
|
-
//# sourceMappingURL=chunk-
|
|
2423
|
+
//# sourceMappingURL=chunk-3WBZCTM5.js.map
|