@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
package/dist/bin.cjs
CHANGED
|
@@ -34,7 +34,7 @@ var import_commander = require("commander");
|
|
|
34
34
|
// package.json
|
|
35
35
|
var package_default = {
|
|
36
36
|
name: "@rolino/cli",
|
|
37
|
-
version: "0.
|
|
37
|
+
version: "0.6.0",
|
|
38
38
|
description: "Agent-friendly command-line interface for Rolino",
|
|
39
39
|
type: "module",
|
|
40
40
|
license: "MIT",
|
|
@@ -91,14 +91,14 @@ var package_default = {
|
|
|
91
91
|
dev: "tsx src/bin.ts"
|
|
92
92
|
},
|
|
93
93
|
dependencies: {
|
|
94
|
-
"@rolino/contracts": "0.
|
|
95
|
-
"@rolino/local-auth": "0.
|
|
96
|
-
"@rolino/sdk": "0.
|
|
94
|
+
"@rolino/contracts": "0.6.0",
|
|
95
|
+
"@rolino/local-auth": "0.6.0",
|
|
96
|
+
"@rolino/sdk": "0.6.0",
|
|
97
97
|
commander: "^15.0.0",
|
|
98
98
|
open: "^11.0.0"
|
|
99
99
|
},
|
|
100
100
|
devDependencies: {
|
|
101
|
-
tsx: "^4.
|
|
101
|
+
tsx: "^4.23.12"
|
|
102
102
|
},
|
|
103
103
|
engines: {
|
|
104
104
|
node: ">=20.19.0"
|
|
@@ -106,23 +106,25 @@ var package_default = {
|
|
|
106
106
|
};
|
|
107
107
|
|
|
108
108
|
// src/cli.ts
|
|
109
|
-
var
|
|
109
|
+
var import_contracts = require("@rolino/contracts");
|
|
110
110
|
var import_sdk = require("@rolino/sdk");
|
|
111
111
|
|
|
112
112
|
// src/browser-login.ts
|
|
113
113
|
var import_node_crypto = require("crypto");
|
|
114
114
|
var import_node_http = require("http");
|
|
115
|
-
var
|
|
116
|
-
var import_contracts = require("@rolino/contracts");
|
|
115
|
+
var import_local_auth = require("@rolino/local-auth");
|
|
117
116
|
var import_open = __toESM(require("open"), 1);
|
|
118
|
-
var
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
"
|
|
123
|
-
"
|
|
124
|
-
|
|
125
|
-
|
|
117
|
+
var OAUTH_CALLBACK_PORT = 48391;
|
|
118
|
+
var OAUTH_CALLBACK_PATH = "/oauth/callback";
|
|
119
|
+
var OAUTH_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
120
|
+
var DEFAULT_SCOPES = [
|
|
121
|
+
"offline_access",
|
|
122
|
+
"identity:read",
|
|
123
|
+
"projects:read",
|
|
124
|
+
"posts:read",
|
|
125
|
+
"integrations:read",
|
|
126
|
+
"calendar:read"
|
|
127
|
+
];
|
|
126
128
|
function escapeHtml(value) {
|
|
127
129
|
return value.replace(/[&<>"']/g, (character) => ({
|
|
128
130
|
"&": "&",
|
|
@@ -132,86 +134,18 @@ function escapeHtml(value) {
|
|
|
132
134
|
"'": "'"
|
|
133
135
|
})[character]);
|
|
134
136
|
}
|
|
135
|
-
function
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
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>';
|
|
139
|
-
return `<!doctype html>
|
|
140
|
-
<html lang="en">
|
|
141
|
-
<head>
|
|
142
|
-
<meta charset="utf-8">
|
|
143
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
144
|
-
<meta name="color-scheme" content="light">
|
|
145
|
-
<title>${escapeHtml(options.title)} \xB7 Rolino</title>
|
|
146
|
-
<style>
|
|
147
|
-
*{box-sizing:border-box}
|
|
148
|
-
html,body{height:100%;margin:0}
|
|
149
|
-
body{min-height:100svh;overflow:hidden;background:#faf9f1;color:#0f172a;font-family:"Poppins","Avenir Next","Trebuchet MS",ui-rounded,system-ui,sans-serif}
|
|
150
|
-
.shell{position:relative;isolation:isolate;display:grid;min-height:100svh;grid-template-rows:auto 1fr auto;padding:clamp(18px,3vw,38px);overflow:hidden}
|
|
151
|
-
.shape{position:absolute;z-index:-1;border-radius:999px;pointer-events:none}
|
|
152
|
-
.shape-a{top:-160px;left:-130px;width:390px;height:390px;background:#facc1530;filter:blur(2px)}
|
|
153
|
-
.shape-b{right:-100px;bottom:-160px;width:360px;height:360px;background:#34c78a20}
|
|
154
|
-
.shape-c{right:9%;top:14%;width:22px;height:22px;background:#ef6a5b;transform:rotate(18deg);border-radius:7px}
|
|
155
|
-
.topbar,.footer{display:flex;width:min(100%,1100px);margin:auto;align-items:center;justify-content:space-between}
|
|
156
|
-
.brand{display:flex;align-items:center;gap:10px;color:#0f172a;font-size:22px;font-weight:800;letter-spacing:-.04em}
|
|
157
|
-
.brand svg{display:block;width:36px;height:36px}
|
|
158
|
-
.secure{display:flex;align-items:center;gap:8px;color:#52607a;font-size:11px;font-weight:800;letter-spacing:.14em;text-transform:uppercase}
|
|
159
|
-
.secure-dot{display:block;width:8px;height:8px;border-radius:50%;background:#34c78a;box-shadow:0 0 0 5px #34c78a1f}
|
|
160
|
-
.stage{display:grid;min-height:0;place-items:center;padding:12px 0}
|
|
161
|
-
.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}
|
|
162
|
-
.mascot-wrap{position:relative;display:grid;width:112px;height:96px;margin:0 auto 12px;place-items:center}
|
|
163
|
-
.mascot-wrap:before{position:absolute;width:88px;height:88px;border-radius:50%;background:#fff4a8;content:"";box-shadow:0 0 0 14px #fff9d8}
|
|
164
|
-
.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}
|
|
165
|
-
.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}
|
|
166
|
-
.eyebrow:before{width:7px;height:7px;border-radius:50%;background:#34c78a;content:""}
|
|
167
|
-
.failure .eyebrow{background:#fee6df;color:#9f2f24}
|
|
168
|
-
.failure .eyebrow:before{background:#ef6a5b}
|
|
169
|
-
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}
|
|
170
|
-
.message{max-width:450px;margin:14px auto 0;color:#52607a;font-size:15px;line-height:1.65}
|
|
171
|
-
.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}
|
|
172
|
-
.terminal-bar{display:flex;align-items:center;gap:6px;padding-bottom:12px;color:#8f9bb1;font-size:9px;font-weight:800;letter-spacing:.13em}
|
|
173
|
-
.terminal-bar i{display:block;width:7px;height:7px;border-radius:50%;background:#ef6a5b}
|
|
174
|
-
.terminal-bar i:nth-child(2){background:#facc15}
|
|
175
|
-
.terminal-bar i:nth-child(3){margin-right:5px;background:#34c78a}
|
|
176
|
-
.command{font-family:"Cascadia Code",Consolas,monospace;font-size:13px}
|
|
177
|
-
.command b{color:#facc15}
|
|
178
|
-
.terminal-result{display:flex;align-items:center;gap:8px;margin-top:9px;color:#aeb9ca;font-size:12px}
|
|
179
|
-
.terminal-result span{color:#52d49e;font-weight:900}
|
|
180
|
-
.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}
|
|
181
|
-
.return-note>span{font-size:24px}
|
|
182
|
-
.return-note strong,.return-note small{display:block}
|
|
183
|
-
.return-note small{margin-top:3px;color:#875b56}
|
|
184
|
-
.footer{justify-content:center;color:#6f788a;font-size:11px}
|
|
185
|
-
@keyframes arrive{from{opacity:0;transform:translateY(12px) scale(.985)}to{opacity:1;transform:none}}
|
|
186
|
-
@keyframes mascot-arrive{from{opacity:0;transform:translateY(10px) rotate(-4deg) scale(.88)}to{opacity:1;transform:none}}
|
|
187
|
-
@media(max-width:600px){.secure{display:none}.panel{border-radius:24px}.shape-c{display:none}}
|
|
188
|
-
@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}}
|
|
189
|
-
@media(prefers-reduced-motion:reduce){.panel,.mascot-wrap svg{animation:none}}
|
|
190
|
-
</style>
|
|
191
|
-
</head>
|
|
192
|
-
<body class="${tone}">
|
|
193
|
-
<main class="shell">
|
|
194
|
-
<span class="shape shape-a"></span><span class="shape shape-b"></span><span class="shape shape-c"></span>
|
|
195
|
-
<header class="topbar">
|
|
196
|
-
<div class="brand">${ROLINO_MASCOT_SVG}<span>Rolino</span></div>
|
|
197
|
-
<div class="secure"><span class="secure-dot"></span>Secure CLI authorization</div>
|
|
198
|
-
</header>
|
|
199
|
-
<section class="stage">
|
|
200
|
-
<div class="panel">
|
|
201
|
-
<div class="mascot-wrap">${ROLINO_MASCOT_SVG}</div>
|
|
202
|
-
<p class="eyebrow">${eyebrow}</p>
|
|
203
|
-
<h1>${escapeHtml(options.title)}</h1>
|
|
204
|
-
<p class="message">${escapeHtml(options.message)}</p>
|
|
205
|
-
${detail}
|
|
206
|
-
</div>
|
|
207
|
-
</section>
|
|
208
|
-
<footer class="footer">Secure browser handoff \xB7 no password shared</footer>
|
|
209
|
-
</main>
|
|
210
|
-
</body>
|
|
211
|
-
</html>`;
|
|
137
|
+
function renderOAuthAuthorizationResultPage(options) {
|
|
138
|
+
const color = options.success ? "#087f5b" : "#b42318";
|
|
139
|
+
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>`;
|
|
212
140
|
}
|
|
213
141
|
function writePage(response, status, html) {
|
|
214
|
-
response.writeHead(status,
|
|
142
|
+
response.writeHead(status, {
|
|
143
|
+
"cache-control": "no-store",
|
|
144
|
+
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; frame-ancestors 'none'",
|
|
145
|
+
"content-type": "text/html; charset=utf-8",
|
|
146
|
+
"referrer-policy": "no-referrer",
|
|
147
|
+
"x-content-type-options": "nosniff"
|
|
148
|
+
});
|
|
215
149
|
response.end(html);
|
|
216
150
|
}
|
|
217
151
|
function secretsMatch(left, right) {
|
|
@@ -219,41 +153,63 @@ function secretsMatch(left, right) {
|
|
|
219
153
|
const rightBytes = Buffer.from(right);
|
|
220
154
|
return leftBytes.length === rightBytes.length && (0, import_node_crypto.timingSafeEqual)(leftBytes, rightBytes);
|
|
221
155
|
}
|
|
222
|
-
function createBrowserAuthorizationRequest(
|
|
156
|
+
function createBrowserAuthorizationRequest(baseUrl) {
|
|
157
|
+
const configuration = (0, import_local_auth.oauthConfiguration)(baseUrl);
|
|
223
158
|
const state = (0, import_node_crypto.randomBytes)(32).toString("base64url");
|
|
224
159
|
const codeVerifier = (0, import_node_crypto.randomBytes)(32).toString("base64url");
|
|
225
160
|
const codeChallenge = (0, import_node_crypto.createHash)("sha256").update(codeVerifier).digest("base64url");
|
|
226
|
-
const
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
url.searchParams.set("redirect_uri",
|
|
161
|
+
const redirectUri = `http://127.0.0.1:${OAUTH_CALLBACK_PORT}${OAUTH_CALLBACK_PATH}`;
|
|
162
|
+
const url = new URL(`${configuration.issuer}/oauth2/authorize`);
|
|
163
|
+
url.searchParams.set("response_type", "code");
|
|
164
|
+
url.searchParams.set("client_id", configuration.clientId);
|
|
165
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
166
|
+
url.searchParams.set("scope", DEFAULT_SCOPES.join(" "));
|
|
167
|
+
url.searchParams.set("resource", configuration.resource);
|
|
231
168
|
url.searchParams.set("state", state);
|
|
232
169
|
url.searchParams.set("code_challenge", codeChallenge);
|
|
233
170
|
url.searchParams.set("code_challenge_method", "S256");
|
|
234
|
-
url.
|
|
235
|
-
"client_name",
|
|
236
|
-
(options.clientName ?? `Rolino CLI on ${(0, import_node_os.hostname)()}`).slice(0, 32)
|
|
237
|
-
);
|
|
238
|
-
url.searchParams.set("request_expires_at", requestExpiresAt);
|
|
239
|
-
return {
|
|
240
|
-
url: url.toString(),
|
|
241
|
-
state,
|
|
242
|
-
codeVerifier,
|
|
243
|
-
codeChallenge,
|
|
244
|
-
requestExpiresAt
|
|
245
|
-
};
|
|
171
|
+
return { configuration, url: url.toString(), redirectUri, state, codeVerifier, codeChallenge };
|
|
246
172
|
}
|
|
247
|
-
var
|
|
173
|
+
var OAuthAuthorizationDeniedError = class extends Error {
|
|
248
174
|
constructor() {
|
|
249
175
|
super("Authorization was denied.");
|
|
250
|
-
this.name = "
|
|
176
|
+
this.name = "OAuthAuthorizationDeniedError";
|
|
251
177
|
}
|
|
252
178
|
};
|
|
179
|
+
async function exchangeAuthorizationCode(options) {
|
|
180
|
+
const { authorization } = options;
|
|
181
|
+
const response = await options.fetch(`${authorization.configuration.issuer}/oauth2/token`, {
|
|
182
|
+
method: "POST",
|
|
183
|
+
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
|
|
184
|
+
body: new URLSearchParams({
|
|
185
|
+
grant_type: "authorization_code",
|
|
186
|
+
code: options.code,
|
|
187
|
+
redirect_uri: authorization.redirectUri,
|
|
188
|
+
client_id: authorization.configuration.clientId,
|
|
189
|
+
code_verifier: authorization.codeVerifier,
|
|
190
|
+
resource: authorization.configuration.resource
|
|
191
|
+
})
|
|
192
|
+
});
|
|
193
|
+
const payload = await response.json().catch(() => null);
|
|
194
|
+
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.");
|
|
195
|
+
const expiresIn = typeof payload.expires_in === "number" ? payload.expires_in : 300;
|
|
196
|
+
return {
|
|
197
|
+
host: authorization.configuration.host,
|
|
198
|
+
issuer: authorization.configuration.issuer,
|
|
199
|
+
clientId: authorization.configuration.clientId,
|
|
200
|
+
resource: authorization.configuration.resource,
|
|
201
|
+
accessToken: payload.access_token,
|
|
202
|
+
accessTokenExpiresAt: new Date(Date.now() + expiresIn * 1e3).toISOString(),
|
|
203
|
+
refreshToken: payload.refresh_token,
|
|
204
|
+
refreshTokenExpiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3).toISOString(),
|
|
205
|
+
scopes: typeof payload.scope === "string" ? payload.scope.split(/\s+/).filter(Boolean) : [...DEFAULT_SCOPES]
|
|
206
|
+
};
|
|
207
|
+
}
|
|
253
208
|
async function loginWithBrowser(options) {
|
|
209
|
+
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
254
210
|
return new Promise((resolve3, reject) => {
|
|
255
211
|
let settled = false;
|
|
256
|
-
|
|
212
|
+
const authorization = createBrowserAuthorizationRequest(options.baseUrl);
|
|
257
213
|
const finish = (callback) => {
|
|
258
214
|
if (settled) return;
|
|
259
215
|
settled = true;
|
|
@@ -263,82 +219,46 @@ async function loginWithBrowser(options) {
|
|
|
263
219
|
};
|
|
264
220
|
const server = (0, import_node_http.createServer)((request, response) => {
|
|
265
221
|
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
266
|
-
if (request.method !== "GET" || url.pathname !==
|
|
222
|
+
if (request.method !== "GET" || url.pathname !== OAUTH_CALLBACK_PATH) {
|
|
267
223
|
response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
268
224
|
response.end("Not found");
|
|
269
225
|
return;
|
|
270
226
|
}
|
|
271
227
|
void (async () => {
|
|
272
|
-
if (!authorization) {
|
|
273
|
-
writePage(response, 503, renderCliAuthorizationResultPage({
|
|
274
|
-
title: "Authorization is starting",
|
|
275
|
-
message: "Return to your terminal and try the browser link again in a moment.",
|
|
276
|
-
success: false
|
|
277
|
-
}));
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
228
|
if (!secretsMatch(url.searchParams.get("state") ?? "", authorization.state)) {
|
|
281
|
-
writePage(response, 400,
|
|
282
|
-
|
|
283
|
-
message: "The callback state did not match. Return to your terminal and try again.",
|
|
284
|
-
success: false
|
|
285
|
-
}));
|
|
286
|
-
finish(() => reject(new Error("The browser callback state did not match.")));
|
|
229
|
+
writePage(response, 400, renderOAuthAuthorizationResultPage({ title: "Authorization failed", message: "The callback state did not match.", success: false }));
|
|
230
|
+
finish(() => reject(new Error("The OAuth callback state did not match.")));
|
|
287
231
|
return;
|
|
288
232
|
}
|
|
289
233
|
if (url.searchParams.get("error") === "access_denied") {
|
|
290
|
-
writePage(response, 200,
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
234
|
+
writePage(response, 200, renderOAuthAuthorizationResultPage({ title: "Access denied", message: "No OAuth token was saved.", success: false }));
|
|
235
|
+
finish(() => reject(new OAuthAuthorizationDeniedError()));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (url.searchParams.get("iss") !== authorization.configuration.issuer) {
|
|
239
|
+
writePage(response, 400, renderOAuthAuthorizationResultPage({ title: "Authorization failed", message: "The authorization server did not match.", success: false }));
|
|
240
|
+
finish(() => reject(new Error("The OAuth callback issuer did not match.")));
|
|
296
241
|
return;
|
|
297
242
|
}
|
|
298
243
|
const code = url.searchParams.get("code");
|
|
299
|
-
if (!code ||
|
|
300
|
-
writePage(response, 400,
|
|
301
|
-
|
|
302
|
-
message: "Rolino did not return a valid one-time code. Return to your terminal and try again.",
|
|
303
|
-
success: false
|
|
304
|
-
}));
|
|
305
|
-
finish(() => reject(new Error("The browser callback did not include a valid code.")));
|
|
244
|
+
if (!code || code.length > 2048) {
|
|
245
|
+
writePage(response, 400, renderOAuthAuthorizationResultPage({ title: "Authorization failed", message: "Rolino did not return a valid code.", success: false }));
|
|
246
|
+
finish(() => reject(new Error("The OAuth callback did not include a valid code.")));
|
|
306
247
|
return;
|
|
307
248
|
}
|
|
308
249
|
try {
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
});
|
|
313
|
-
writePage(response, 200, renderCliAuthorizationResultPage({
|
|
314
|
-
title: "Terminal connected",
|
|
315
|
-
message: "Rolino is ready in your terminal. You can close this window.",
|
|
316
|
-
success: true
|
|
317
|
-
}));
|
|
318
|
-
finish(() => resolve3(credential));
|
|
250
|
+
const tokenSet = await exchangeAuthorizationCode({ authorization, code, fetch: fetchImplementation });
|
|
251
|
+
writePage(response, 200, renderOAuthAuthorizationResultPage({ title: "Terminal connected", message: "You can close this window and return to your terminal.", success: true }));
|
|
252
|
+
finish(() => resolve3(tokenSet));
|
|
319
253
|
} catch (error) {
|
|
320
|
-
writePage(response, 502,
|
|
321
|
-
title: "Authorization failed",
|
|
322
|
-
message: "The CLI could not exchange its one-time code. Return to your terminal and try again.",
|
|
323
|
-
success: false
|
|
324
|
-
}));
|
|
254
|
+
writePage(response, 502, renderOAuthAuthorizationResultPage({ title: "Authorization failed", message: "The CLI could not exchange the code.", success: false }));
|
|
325
255
|
finish(() => reject(error));
|
|
326
256
|
}
|
|
327
257
|
})();
|
|
328
258
|
});
|
|
329
259
|
server.once("error", (error) => finish(() => reject(error)));
|
|
330
|
-
server.listen(
|
|
331
|
-
|
|
332
|
-
if (!address || typeof address === "string") {
|
|
333
|
-
finish(() => reject(new Error("Rolino could not start the local authorization callback.")));
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
authorization = createBrowserAuthorizationRequest({
|
|
337
|
-
baseUrl: options.baseUrl,
|
|
338
|
-
redirectUri: `http://127.0.0.1:${address.port}/callback`
|
|
339
|
-
});
|
|
340
|
-
options.stderr.write(`Opening Rolino in your browser\u2026
|
|
341
|
-
`);
|
|
260
|
+
server.listen(OAUTH_CALLBACK_PORT, "127.0.0.1", () => {
|
|
261
|
+
options.stderr.write("Opening Rolino OAuth sign-in in your browser\u2026\n");
|
|
342
262
|
options.stderr.write(`If it does not open, visit:
|
|
343
263
|
${authorization.url}
|
|
344
264
|
`);
|
|
@@ -346,13 +266,13 @@ ${authorization.url}
|
|
|
346
266
|
void (0, import_open.default)(authorization.url).catch(() => void 0);
|
|
347
267
|
});
|
|
348
268
|
const timeout = setTimeout(() => {
|
|
349
|
-
finish(() => reject(new Error("
|
|
350
|
-
},
|
|
269
|
+
finish(() => reject(new Error("OAuth sign-in timed out after 5 minutes.")));
|
|
270
|
+
}, OAUTH_TIMEOUT_MS);
|
|
351
271
|
});
|
|
352
272
|
}
|
|
353
273
|
|
|
354
274
|
// src/cli.ts
|
|
355
|
-
var
|
|
275
|
+
var import_local_auth2 = require("@rolino/local-auth");
|
|
356
276
|
|
|
357
277
|
// src/exit-codes.ts
|
|
358
278
|
var EXIT_CODES = {
|
|
@@ -634,12 +554,12 @@ var import_node_child_process = require("child_process");
|
|
|
634
554
|
var import_node_fs = require("fs");
|
|
635
555
|
var import_promises = require("fs/promises");
|
|
636
556
|
var import_node_module = require("module");
|
|
637
|
-
var
|
|
557
|
+
var import_node_os = require("os");
|
|
638
558
|
var import_node_path = require("path");
|
|
639
559
|
var CODEX_BEGIN = "# BEGIN ROLINO MCP (managed by rolino setup mcp)";
|
|
640
560
|
var CODEX_END = "# END ROLINO MCP";
|
|
641
561
|
function homeDirectory(env) {
|
|
642
|
-
return env.USERPROFILE || env.HOME || (0,
|
|
562
|
+
return env.USERPROFILE || env.HOME || (0, import_node_os.homedir)();
|
|
643
563
|
}
|
|
644
564
|
function tomlString(value) {
|
|
645
565
|
return JSON.stringify(value);
|
|
@@ -905,40 +825,40 @@ function outputFormat(value) {
|
|
|
905
825
|
throw new import_commander.InvalidArgumentError("Output must be human, json, or jsonl.");
|
|
906
826
|
}
|
|
907
827
|
function postStatus(value) {
|
|
908
|
-
const parsed =
|
|
828
|
+
const parsed = import_contracts.PostStatusSchema.safeParse(value.toUpperCase());
|
|
909
829
|
if (parsed.success) return parsed.data;
|
|
910
830
|
throw new import_commander.InvalidArgumentError(
|
|
911
|
-
`Status must be one of ${
|
|
831
|
+
`Status must be one of ${import_contracts.PostStatusSchema.options.join(", ")}.`
|
|
912
832
|
);
|
|
913
833
|
}
|
|
914
834
|
function seoOpportunityKind(value) {
|
|
915
|
-
const parsed =
|
|
835
|
+
const parsed = import_contracts.SeoOpportunityKindSchema.safeParse(value.toUpperCase());
|
|
916
836
|
if (parsed.success) return parsed.data;
|
|
917
|
-
throw new import_commander.InvalidArgumentError(`SEO kind must be one of ${
|
|
837
|
+
throw new import_commander.InvalidArgumentError(`SEO kind must be one of ${import_contracts.SeoOpportunityKindSchema.options.join(", ")}.`);
|
|
918
838
|
}
|
|
919
839
|
function seoExpectedImpact(value) {
|
|
920
|
-
const parsed =
|
|
840
|
+
const parsed = import_contracts.SeoExpectedImpactSchema.safeParse(value.toUpperCase());
|
|
921
841
|
if (parsed.success) return parsed.data;
|
|
922
842
|
throw new import_commander.InvalidArgumentError("SEO impact must be HIGH, MEDIUM, or LOW.");
|
|
923
843
|
}
|
|
924
844
|
function seoReportCompleteness(value) {
|
|
925
|
-
const parsed =
|
|
845
|
+
const parsed = import_contracts.SeoReportCompletenessSchema.safeParse(value.toUpperCase());
|
|
926
846
|
if (parsed.success) return parsed.data;
|
|
927
847
|
throw new import_commander.InvalidArgumentError("Report status must be COMPLETE or PARTIAL.");
|
|
928
848
|
}
|
|
929
849
|
function projectType(value) {
|
|
930
|
-
const parsed =
|
|
850
|
+
const parsed = import_contracts.ProjectTypeSchema.safeParse(value.toUpperCase());
|
|
931
851
|
if (parsed.success) return parsed.data;
|
|
932
852
|
throw new import_commander.InvalidArgumentError(
|
|
933
|
-
`Type must be one of ${
|
|
853
|
+
`Type must be one of ${import_contracts.ProjectTypeSchema.options.join(", ")}.`
|
|
934
854
|
);
|
|
935
855
|
}
|
|
936
856
|
function publishingProvider(value) {
|
|
937
857
|
const normalized = value.toUpperCase();
|
|
938
|
-
const parsed =
|
|
858
|
+
const parsed = import_contracts.PublishingProviderSchema.safeParse(normalized);
|
|
939
859
|
if (parsed.success) return parsed.data;
|
|
940
860
|
throw new import_commander.InvalidArgumentError(
|
|
941
|
-
`Platform must be one of ${
|
|
861
|
+
`Platform must be one of ${import_contracts.PublishingProviderSchema.options.join(", ")}.`
|
|
942
862
|
);
|
|
943
863
|
}
|
|
944
864
|
function collectPublishingProvider(value, previous) {
|
|
@@ -949,10 +869,10 @@ function collectString(value, previous) {
|
|
|
949
869
|
}
|
|
950
870
|
function deliveryOptionsProvider(value) {
|
|
951
871
|
const normalized = value.toUpperCase();
|
|
952
|
-
const parsed =
|
|
872
|
+
const parsed = import_contracts.ProviderDeliveryOptionsProviderSchema.safeParse(normalized);
|
|
953
873
|
if (parsed.success) return parsed.data;
|
|
954
874
|
throw new import_commander.InvalidArgumentError(
|
|
955
|
-
`Provider must be one of ${
|
|
875
|
+
`Provider must be one of ${import_contracts.ProviderDeliveryOptionsProviderSchema.options.join(", ")}.`
|
|
956
876
|
);
|
|
957
877
|
}
|
|
958
878
|
function tiktokPostMode(value) {
|
|
@@ -990,7 +910,7 @@ function yesOrNo(value) {
|
|
|
990
910
|
function youtubeSettings(options) {
|
|
991
911
|
const hasYouTubeOptions = options.youtubeTitle !== void 0 || options.youtubeCategoryId !== void 0 || options.youtubePrivacy !== void 0 || options.youtubeMadeForKids !== void 0 || options.youtubeSyntheticMedia === true || options.youtubeNotifySubscribers === false || (options.youtubeTag?.length ?? 0) > 0;
|
|
992
912
|
if (!hasYouTubeOptions) return null;
|
|
993
|
-
return
|
|
913
|
+
return import_contracts.YouTubePostSettingsSchema.parse({
|
|
994
914
|
title: options.youtubeTitle,
|
|
995
915
|
categoryId: options.youtubeCategoryId,
|
|
996
916
|
privacyStatus: options.youtubePrivacy,
|
|
@@ -1053,6 +973,27 @@ async function requireWriteConsent(options) {
|
|
|
1053
973
|
);
|
|
1054
974
|
}
|
|
1055
975
|
}
|
|
976
|
+
var BLOG_WEEKDAY_VALUES = {
|
|
977
|
+
sun: 0,
|
|
978
|
+
mon: 1,
|
|
979
|
+
tue: 2,
|
|
980
|
+
wed: 3,
|
|
981
|
+
thu: 4,
|
|
982
|
+
fri: 5,
|
|
983
|
+
sat: 6
|
|
984
|
+
};
|
|
985
|
+
function parseBlogWeekdays(value) {
|
|
986
|
+
const names = value.split(",").map((day) => day.trim().toLowerCase());
|
|
987
|
+
const weekdays = names.map((day) => BLOG_WEEKDAY_VALUES[day]).filter((day) => day !== void 0);
|
|
988
|
+
if (!weekdays.length || weekdays.length !== names.length || new Set(weekdays).size !== weekdays.length) {
|
|
989
|
+
throw new TypeError("--weekdays must contain unique comma-separated sun,mon,tue,wed,thu,fri,sat values.");
|
|
990
|
+
}
|
|
991
|
+
return weekdays;
|
|
992
|
+
}
|
|
993
|
+
function requireBlogExecutionConsent(options) {
|
|
994
|
+
if (options.yes || resolveFormat(options.global, options.runtime) !== "human") return;
|
|
995
|
+
throw new TypeError("This Blog execute command requires explicit consent. Review the confirmed operation and pass --yes.");
|
|
996
|
+
}
|
|
1056
997
|
function formatMcpSetupPreview(result) {
|
|
1057
998
|
return [
|
|
1058
999
|
`Client: ${result.client === "codex" ? "Codex" : "Claude Code"}`,
|
|
@@ -1079,16 +1020,16 @@ function baseUrlFor(options, runtime) {
|
|
|
1079
1020
|
function clientFor(options, runtime) {
|
|
1080
1021
|
const timeoutMs = options.timeout ?? (runtime.env.ROLINO_TIMEOUT ? duration(runtime.env.ROLINO_TIMEOUT) : 15e3);
|
|
1081
1022
|
const baseUrl = baseUrlFor(options, runtime);
|
|
1082
|
-
const
|
|
1023
|
+
const token = runtime.env.ROLINO_TOKEN ?? (0, import_local_auth2.createOAuthAccessTokenProvider)({ baseUrl, env: runtime.env, fetch: runtime.fetch });
|
|
1083
1024
|
return new import_sdk.RolinoClient({
|
|
1084
1025
|
baseUrl,
|
|
1085
|
-
token
|
|
1026
|
+
token,
|
|
1086
1027
|
timeoutMs,
|
|
1087
1028
|
fetch: runtime.fetch
|
|
1088
1029
|
});
|
|
1089
1030
|
}
|
|
1090
1031
|
function exitCodeFor(error) {
|
|
1091
|
-
if (error instanceof
|
|
1032
|
+
if (error instanceof OAuthAuthorizationDeniedError || error instanceof CliUserCancelledError) {
|
|
1092
1033
|
return EXIT_CODES.cancelled;
|
|
1093
1034
|
}
|
|
1094
1035
|
if (error instanceof import_sdk.RolinoNetworkError) {
|
|
@@ -1121,7 +1062,7 @@ function exitCodeFor(error) {
|
|
|
1121
1062
|
return EXIT_CODES.unexpected;
|
|
1122
1063
|
}
|
|
1123
1064
|
function errorForOutput(error) {
|
|
1124
|
-
if (error instanceof
|
|
1065
|
+
if (error instanceof OAuthAuthorizationDeniedError || error instanceof CliUserCancelledError) {
|
|
1125
1066
|
return { code: "CANCELLED", message: error.message };
|
|
1126
1067
|
}
|
|
1127
1068
|
if (error instanceof import_sdk.RolinoApiError) {
|
|
@@ -1146,7 +1087,7 @@ function errorForOutput(error) {
|
|
|
1146
1087
|
function tiktokSettings(options) {
|
|
1147
1088
|
const hasTikTokOptions = options.tiktokMode !== void 0 || options.tiktokVisibility !== void 0 || options.tiktokComments !== void 0 || options.tiktokDuet !== void 0 || options.tiktokStitch !== void 0 || options.tiktokCommercialContent !== void 0 || options.tiktokPromotesOwnBrand !== void 0 || options.tiktokPromotesThirdParty !== void 0 || options.tiktokAiGenerated !== void 0 || options.tiktokCoverTimestampMs !== void 0 || options.tiktokSettingsReviewed === true;
|
|
1148
1089
|
if (!hasTikTokOptions) return void 0;
|
|
1149
|
-
return
|
|
1090
|
+
return import_contracts.TikTokDraftSettingsSchema.parse({
|
|
1150
1091
|
postMode: options.tiktokMode,
|
|
1151
1092
|
privacyLevel: options.tiktokVisibility,
|
|
1152
1093
|
allowComment: options.tiktokComments,
|
|
@@ -1290,7 +1231,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1290
1231
|
global,
|
|
1291
1232
|
runtime,
|
|
1292
1233
|
async action(context, client) {
|
|
1293
|
-
const parsed =
|
|
1234
|
+
const parsed = import_contracts.ProjectCreateInputSchema.safeParse({
|
|
1294
1235
|
name: local.name,
|
|
1295
1236
|
type: local.type,
|
|
1296
1237
|
websiteUrl: local.website,
|
|
@@ -1382,22 +1323,14 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1382
1323
|
"ROLINO_TOKEN is set and overrides browser login. Unset it before saving a CLI credential."
|
|
1383
1324
|
);
|
|
1384
1325
|
}
|
|
1385
|
-
const
|
|
1386
|
-
if (!meta.authentication.cliBrowserAuthorization) {
|
|
1387
|
-
throw new TypeError(
|
|
1388
|
-
"This Rolino server does not support browser CLI authorization."
|
|
1389
|
-
);
|
|
1390
|
-
}
|
|
1391
|
-
const credential = await loginWithBrowser({
|
|
1326
|
+
const tokenSet = await loginWithBrowser({
|
|
1392
1327
|
baseUrl: client.baseUrl,
|
|
1393
1328
|
stderr: runtime.stderr,
|
|
1394
|
-
|
|
1395
|
-
requestId: context.requestId
|
|
1396
|
-
})
|
|
1329
|
+
fetch: runtime.fetch
|
|
1397
1330
|
});
|
|
1398
1331
|
const authenticatedClient = new import_sdk.RolinoClient({
|
|
1399
1332
|
baseUrl: client.baseUrl,
|
|
1400
|
-
token:
|
|
1333
|
+
token: tokenSet.accessToken,
|
|
1401
1334
|
timeoutMs: client.timeoutMs,
|
|
1402
1335
|
fetch: runtime.fetch
|
|
1403
1336
|
});
|
|
@@ -1406,12 +1339,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1406
1339
|
const actor = await authenticatedClient.whoami({
|
|
1407
1340
|
requestId: context.requestId
|
|
1408
1341
|
});
|
|
1409
|
-
const path = (0,
|
|
1410
|
-
token: credential.token,
|
|
1411
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1412
|
-
expiresAt: credential.expiresAt,
|
|
1413
|
-
organization: credential.organization
|
|
1414
|
-
}, runtime.env);
|
|
1342
|
+
const path = (0, import_local_auth2.saveOAuthTokenSet)(tokenSet, runtime.env);
|
|
1415
1343
|
credentialSaved = true;
|
|
1416
1344
|
writeSuccess(
|
|
1417
1345
|
context,
|
|
@@ -1420,23 +1348,20 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1420
1348
|
user: actor.user,
|
|
1421
1349
|
organization: actor.organization,
|
|
1422
1350
|
capabilities: actor.capabilities,
|
|
1423
|
-
expiresAt:
|
|
1351
|
+
expiresAt: tokenSet.accessTokenExpiresAt
|
|
1424
1352
|
},
|
|
1425
1353
|
[
|
|
1426
1354
|
`Authenticated as ${actor.user.email}.`,
|
|
1427
1355
|
`Workspace: ${actor.organization.name}`,
|
|
1428
|
-
`
|
|
1356
|
+
`Access token expires: ${tokenSet.accessTokenExpiresAt}`,
|
|
1429
1357
|
`Credential saved in a permission-restricted file at ${path}`
|
|
1430
1358
|
].join("\n"),
|
|
1431
1359
|
["rolino whoami --agent", "rolino projects list --agent"]
|
|
1432
1360
|
);
|
|
1433
1361
|
} catch (error) {
|
|
1434
1362
|
if (credentialSaved) {
|
|
1435
|
-
(0,
|
|
1363
|
+
(0, import_local_auth2.clearOAuthTokenSet)(client.baseUrl, runtime.env);
|
|
1436
1364
|
}
|
|
1437
|
-
await authenticatedClient.auth.revokeCurrentCredential({
|
|
1438
|
-
requestId: context.requestId
|
|
1439
|
-
}).catch(() => void 0);
|
|
1440
1365
|
throw error;
|
|
1441
1366
|
}
|
|
1442
1367
|
}
|
|
@@ -1449,7 +1374,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1449
1374
|
global,
|
|
1450
1375
|
runtime,
|
|
1451
1376
|
async action(context, client) {
|
|
1452
|
-
const credential = (0,
|
|
1377
|
+
const credential = (0, import_local_auth2.resolveLocalAuthentication)(client.baseUrl, runtime.env);
|
|
1453
1378
|
if (credential.source === "none") {
|
|
1454
1379
|
writeSuccess(
|
|
1455
1380
|
context,
|
|
@@ -1468,7 +1393,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1468
1393
|
baseUrl: client.baseUrl,
|
|
1469
1394
|
user: actor.user,
|
|
1470
1395
|
organization: actor.organization,
|
|
1471
|
-
...credential.source === "
|
|
1396
|
+
...credential.source === "oauth" && credential.tokenSet ? { expiresAt: credential.tokenSet.accessTokenExpiresAt } : {}
|
|
1472
1397
|
},
|
|
1473
1398
|
[
|
|
1474
1399
|
`Authenticated as ${actor.user.email}.`,
|
|
@@ -1486,7 +1411,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1486
1411
|
global,
|
|
1487
1412
|
runtime,
|
|
1488
1413
|
async action(context, client) {
|
|
1489
|
-
const credential = (0,
|
|
1414
|
+
const credential = (0, import_local_auth2.resolveLocalAuthentication)(client.baseUrl, runtime.env);
|
|
1490
1415
|
if (credential.source === "environment") {
|
|
1491
1416
|
throw new TypeError(
|
|
1492
1417
|
"ROLINO_TOKEN is set. Remove it from the environment instead of using auth logout."
|
|
@@ -1500,15 +1425,36 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1500
1425
|
);
|
|
1501
1426
|
return;
|
|
1502
1427
|
}
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1428
|
+
const tokenSet = credential.tokenSet;
|
|
1429
|
+
const fetchImplementation = runtime.fetch ?? globalThis.fetch;
|
|
1430
|
+
const accessToken = await (0, import_local_auth2.createOAuthAccessTokenProvider)({
|
|
1431
|
+
baseUrl: client.baseUrl,
|
|
1432
|
+
env: runtime.env,
|
|
1433
|
+
fetch: fetchImplementation
|
|
1434
|
+
})();
|
|
1435
|
+
if (accessToken) {
|
|
1436
|
+
const disconnect = await fetchImplementation(`${client.baseUrl}/api/v1/oauth/logout`, {
|
|
1437
|
+
method: "POST",
|
|
1438
|
+
headers: {
|
|
1439
|
+
authorization: `Bearer ${accessToken}`,
|
|
1440
|
+
accept: "application/json",
|
|
1441
|
+
"x-request-id": context.requestId
|
|
1442
|
+
}
|
|
1506
1443
|
});
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1444
|
+
if (!disconnect.ok && disconnect.status !== 401 && disconnect.status !== 404) {
|
|
1445
|
+
throw new TypeError("Rolino could not disconnect the OAuth workspace grant.");
|
|
1446
|
+
}
|
|
1510
1447
|
}
|
|
1511
|
-
|
|
1448
|
+
await fetchImplementation(`${tokenSet.issuer}/oauth2/revoke`, {
|
|
1449
|
+
method: "POST",
|
|
1450
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
1451
|
+
body: new URLSearchParams({
|
|
1452
|
+
token: tokenSet.refreshToken,
|
|
1453
|
+
token_type_hint: "refresh_token",
|
|
1454
|
+
client_id: tokenSet.clientId
|
|
1455
|
+
})
|
|
1456
|
+
}).catch(() => void 0);
|
|
1457
|
+
(0, import_local_auth2.clearOAuthTokenSet)(client.baseUrl, runtime.env);
|
|
1512
1458
|
writeSuccess(
|
|
1513
1459
|
context,
|
|
1514
1460
|
{ revoked: true, baseUrl: client.baseUrl },
|
|
@@ -1587,7 +1533,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1587
1533
|
});
|
|
1588
1534
|
});
|
|
1589
1535
|
const posts = program.command("posts").description("Read and prepare posts in a Rolino project");
|
|
1590
|
-
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
|
|
1536
|
+
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) => {
|
|
1591
1537
|
const global = program.opts();
|
|
1592
1538
|
commandExitCode = await execute({
|
|
1593
1539
|
command: "posts create",
|
|
@@ -1614,7 +1560,8 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1614
1560
|
...local.tiktokCaption === void 0 ? {} : { TIKTOK: local.tiktokCaption },
|
|
1615
1561
|
...local.youtubeCaption === void 0 ? {} : { YOUTUBE: local.youtubeCaption },
|
|
1616
1562
|
...local.blueskyCaption === void 0 ? {} : { BLUESKY: local.blueskyCaption },
|
|
1617
|
-
...local.linkedinCaption === void 0 ? {} : { LINKEDIN: local.linkedinCaption }
|
|
1563
|
+
...local.linkedinCaption === void 0 ? {} : { LINKEDIN: local.linkedinCaption },
|
|
1564
|
+
...local.googleBusinessProfileCaption === void 0 ? {} : { GOOGLE_BUSINESS_PROFILE: local.googleBusinessProfileCaption }
|
|
1618
1565
|
},
|
|
1619
1566
|
tiktokSettings: tiktokSettings(local) ?? null,
|
|
1620
1567
|
youtubeSettings: youtubeSettings(local)
|
|
@@ -1632,7 +1579,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1632
1579
|
}
|
|
1633
1580
|
});
|
|
1634
1581
|
});
|
|
1635
|
-
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) => {
|
|
1582
|
+
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) => {
|
|
1636
1583
|
const global = program.opts();
|
|
1637
1584
|
commandExitCode = await execute({
|
|
1638
1585
|
command: "posts update",
|
|
@@ -1661,7 +1608,8 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1661
1608
|
...local.tiktokCaption === void 0 ? {} : { TIKTOK: local.tiktokCaption },
|
|
1662
1609
|
...local.youtubeCaption === void 0 ? {} : { YOUTUBE: local.youtubeCaption },
|
|
1663
1610
|
...local.blueskyCaption === void 0 ? {} : { BLUESKY: local.blueskyCaption },
|
|
1664
|
-
...local.linkedinCaption === void 0 ? {} : { LINKEDIN: local.linkedinCaption }
|
|
1611
|
+
...local.linkedinCaption === void 0 ? {} : { LINKEDIN: local.linkedinCaption },
|
|
1612
|
+
...local.googleBusinessProfileCaption === void 0 ? {} : { GOOGLE_BUSINESS_PROFILE: local.googleBusinessProfileCaption }
|
|
1665
1613
|
};
|
|
1666
1614
|
const resolvedTikTokSettings = tiktokSettings(local);
|
|
1667
1615
|
const resolvedYouTubeSettings = youtubeSettings(local);
|
|
@@ -1809,7 +1757,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1809
1757
|
});
|
|
1810
1758
|
});
|
|
1811
1759
|
const publish = posts.command("publish").description("Preview and queue server-confirmed immediate publishing");
|
|
1812
|
-
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
|
|
1760
|
+
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) => {
|
|
1813
1761
|
const global = program.opts();
|
|
1814
1762
|
commandExitCode = await execute({
|
|
1815
1763
|
command: "posts publish preview",
|
|
@@ -1966,6 +1914,52 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1966
1914
|
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
1967
1915
|
} });
|
|
1968
1916
|
});
|
|
1917
|
+
const blogWebhook = blogPublishing.command("webhook").description("Configure a signed custom Blog publishing webhook");
|
|
1918
|
+
const addWebhookChangeOptions = (command, executeChange) => {
|
|
1919
|
+
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");
|
|
1920
|
+
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");
|
|
1921
|
+
return command;
|
|
1922
|
+
};
|
|
1923
|
+
addWebhookChangeOptions(blogWebhook.command("preview"), false).action(async (local) => {
|
|
1924
|
+
const input = { destinationId: local.destination, siteId: local.site, name: local.name, endpoint: local.endpoint, semantics: local.semantics.toUpperCase() };
|
|
1925
|
+
const global = program.opts();
|
|
1926
|
+
commandExitCode = await execute({ command: "blog destinations webhook preview", global, runtime, async action(context, client) {
|
|
1927
|
+
const data = await client.blog.publishing.webhook.preview(local.project, input, { requestId: context.requestId });
|
|
1928
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
1929
|
+
} });
|
|
1930
|
+
});
|
|
1931
|
+
addWebhookChangeOptions(blogWebhook.command("execute"), true).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 execute", global, runtime, async action(context, client) {
|
|
1935
|
+
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.` });
|
|
1936
|
+
const data = await client.blog.publishing.webhook.execute(local.project, { ...input, confirmationToken: local.confirmationToken, idempotencyKey: local.idempotencyKey }, { requestId: context.requestId });
|
|
1937
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
1938
|
+
} });
|
|
1939
|
+
});
|
|
1940
|
+
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) => {
|
|
1941
|
+
const global = program.opts();
|
|
1942
|
+
commandExitCode = await execute({ command: "blog destinations webhook test", global, runtime, async action(context, client) {
|
|
1943
|
+
const data = await client.blog.publishing.webhook.test(local.project, { siteId: local.site, destinationId: local.destination, makePrimary: local.makePrimary }, { requestId: context.requestId });
|
|
1944
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
1945
|
+
} });
|
|
1946
|
+
});
|
|
1947
|
+
const blogWebhookRotate = blogWebhook.command("rotate").description("Rotate the server-only signing secret");
|
|
1948
|
+
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) => {
|
|
1949
|
+
const global = program.opts();
|
|
1950
|
+
commandExitCode = await execute({ command: "blog destinations webhook rotate preview", global, runtime, async action(context, client) {
|
|
1951
|
+
const data = await client.blog.publishing.webhook.previewRotation(local.project, { siteId: local.site, destinationId: local.destination }, { requestId: context.requestId });
|
|
1952
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
1953
|
+
} });
|
|
1954
|
+
});
|
|
1955
|
+
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) => {
|
|
1956
|
+
const global = program.opts();
|
|
1957
|
+
commandExitCode = await execute({ command: "blog destinations webhook rotate execute", global, runtime, async action(context, client) {
|
|
1958
|
+
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.` });
|
|
1959
|
+
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 });
|
|
1960
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
1961
|
+
} });
|
|
1962
|
+
});
|
|
1969
1963
|
const blogSetup = blog.command("setup").description("Inspect agent-first Blog setup readiness");
|
|
1970
1964
|
blogSetup.command("status").requiredOption("--project <project-id>", "exact Rolino project ID").action(async (local) => {
|
|
1971
1965
|
const global = program.opts();
|
|
@@ -1998,9 +1992,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
1998
1992
|
});
|
|
1999
1993
|
const blogPlan = blog.command("plan").description("Create, retry, and review a Blog cadence plan");
|
|
2000
1994
|
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) => {
|
|
2001
|
-
const
|
|
2002
|
-
const weekdays = local.weekdays.split(",").map((day) => weekdayMap[day.trim().toLowerCase()]).filter((day) => day !== void 0);
|
|
2003
|
-
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.");
|
|
1995
|
+
const weekdays = parseBlogWeekdays(local.weekdays);
|
|
2004
1996
|
const global = program.opts();
|
|
2005
1997
|
commandExitCode = await execute({ command: "blog plan create", global, runtime, async action(context, client) {
|
|
2006
1998
|
const data = await client.blog.plans.create(local.project, { startsOn: local.start, weekdays, timeZone: local.timeZone }, local.idempotencyKey, { requestId: context.requestId });
|
|
@@ -2014,6 +2006,25 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
2014
2006
|
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2015
2007
|
} });
|
|
2016
2008
|
});
|
|
2009
|
+
const blogPlanCadence = blogPlan.command("cadence").description("Preview or apply editorial cadence changes without publishing");
|
|
2010
|
+
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) => {
|
|
2011
|
+
const global = program.opts();
|
|
2012
|
+
commandExitCode = await execute({ command: "blog plan cadence preview", global, runtime, async action(context, client) {
|
|
2013
|
+
const data = await client.blog.plans.previewCadence(local.project, local.plan, { weekdays: parseBlogWeekdays(local.weekdays), expectedPlanVersion: local.expectedPlanVersion }, { requestId: context.requestId });
|
|
2014
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2015
|
+
} });
|
|
2016
|
+
});
|
|
2017
|
+
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) => {
|
|
2018
|
+
const global = program.opts();
|
|
2019
|
+
commandExitCode = await execute({ command: "blog plan cadence apply", global, runtime, async action(context, client) {
|
|
2020
|
+
await requireWriteConsent({ yes: local.yes, global, runtime, preview: `Apply the previewed editorial cadence change?
|
|
2021
|
+
Project: ${local.project}
|
|
2022
|
+
Plan: ${local.plan}
|
|
2023
|
+
This moves eligible editorial dates only. It does not schedule, publish, or unpublish an article.` });
|
|
2024
|
+
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 });
|
|
2025
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2026
|
+
} });
|
|
2027
|
+
});
|
|
2017
2028
|
blogPlan.command("items").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--plan <plan-id>", "exact Blog plan ID").action(async (local) => {
|
|
2018
2029
|
const global = program.opts();
|
|
2019
2030
|
commandExitCode = await execute({ command: "blog plan items", global, runtime, async action(context, client) {
|
|
@@ -2094,7 +2105,7 @@ async function runCli(argv = process.argv, overrides = {}) {
|
|
|
2094
2105
|
blogArticles.command("update").argument("<article-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--input <json-file>", "complete Blog draft JSON file").option("--yes", "confirm the draft save").action(async (articleId, local) => {
|
|
2095
2106
|
const global = program.opts();
|
|
2096
2107
|
commandExitCode = await execute({ command: "blog articles update", global, runtime, async action(context, client) {
|
|
2097
|
-
const draft =
|
|
2108
|
+
const draft = import_contracts.AgentBlogDraftUpdateSchema.parse(JSON.parse(await (0, import_promises2.readFile)((0, import_node_path2.resolve)(runtime.cwd, local.input), "utf8")));
|
|
2098
2109
|
await requireWriteConsent({ yes: local.yes, global, runtime, preview: `Save a new immutable Blog draft revision?
|
|
2099
2110
|
Project: ${local.project}
|
|
2100
2111
|
Article: ${articleId}
|
|
@@ -2113,6 +2124,103 @@ Article: ${articleId}` });
|
|
|
2113
2124
|
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2114
2125
|
} });
|
|
2115
2126
|
});
|
|
2127
|
+
const blogImages = blog.command("images").description("Create and review Blog images with blog:write; cannot approve or publish");
|
|
2128
|
+
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) => {
|
|
2129
|
+
const global = program.opts();
|
|
2130
|
+
commandExitCode = await execute({ command: "blog images generate", global, runtime, async action(context, client) {
|
|
2131
|
+
const data = await client.blog.articles.generateImage(local.project, articleId, { revisionId: local.revision, idempotencyKey: local.idempotencyKey ?? context.requestId, editorialBrief: local.editorialBrief }, { requestId: context.requestId });
|
|
2132
|
+
writeSuccess(context, data, `Blog image generation queued.
|
|
2133
|
+
Image: ${data.image.id}
|
|
2134
|
+
Job: ${data.job.id}`);
|
|
2135
|
+
} });
|
|
2136
|
+
});
|
|
2137
|
+
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) => {
|
|
2138
|
+
const global = program.opts();
|
|
2139
|
+
commandExitCode = await execute({ command: "blog images upload", global, runtime, async action(context, client) {
|
|
2140
|
+
const absolutePath = (0, import_node_path2.resolve)(runtime.cwd, filePath);
|
|
2141
|
+
const details = await (0, import_promises2.stat)(absolutePath).catch(() => null);
|
|
2142
|
+
if (!details?.isFile()) throw new TypeError("The Blog image path must point to a readable file.");
|
|
2143
|
+
const contentTypes = { ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".webp": "image/webp" };
|
|
2144
|
+
const extension = (0, import_node_path2.extname)(absolutePath).toLowerCase();
|
|
2145
|
+
const contentType = contentTypes[extension];
|
|
2146
|
+
if (!contentType) throw new TypeError("Use a JPEG, PNG, or WebP Blog image.");
|
|
2147
|
+
const body = await (0, import_node_fs2.openAsBlob)(absolutePath, { type: contentType });
|
|
2148
|
+
const data = await client.blog.articles.uploadImage(local.project, articleId, { revisionId: local.revision, fileName: (0, import_node_path2.basename)(absolutePath), contentType, fileSize: details.size, altText: local.altText, body }, { requestId: context.requestId });
|
|
2149
|
+
writeSuccess(context, data, `Blog image uploaded for review.
|
|
2150
|
+
Image: ${data.id}
|
|
2151
|
+
State: ${data.state}`);
|
|
2152
|
+
} });
|
|
2153
|
+
});
|
|
2154
|
+
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) => {
|
|
2155
|
+
const decision = local.decision.toUpperCase();
|
|
2156
|
+
if (decision !== "APPROVED" && decision !== "REJECTED") throw new TypeError("--decision must be APPROVED or REJECTED.");
|
|
2157
|
+
const global = program.opts();
|
|
2158
|
+
commandExitCode = await execute({ command: "blog images review", global, runtime, async action(context, client) {
|
|
2159
|
+
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 });
|
|
2160
|
+
writeSuccess(context, data, `Blog image review saved.
|
|
2161
|
+
Image: ${data.id}
|
|
2162
|
+
State: ${data.state}`);
|
|
2163
|
+
} });
|
|
2164
|
+
});
|
|
2165
|
+
const blogApprove = blog.command("approve").description("Approve one exact Blog bundle with blog:approve; cannot publish");
|
|
2166
|
+
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) => {
|
|
2167
|
+
const global = program.opts();
|
|
2168
|
+
commandExitCode = await execute({ command: "blog approve preview", global, runtime, async action(context, client) {
|
|
2169
|
+
const data = await client.blog.articles.previewApproval(local.project, articleId, { revisionId: local.revision }, { requestId: context.requestId });
|
|
2170
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2171
|
+
} });
|
|
2172
|
+
});
|
|
2173
|
+
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) => {
|
|
2174
|
+
const global = program.opts();
|
|
2175
|
+
commandExitCode = await execute({ command: "blog approve execute", global, runtime, async action(context, client) {
|
|
2176
|
+
requireBlogExecutionConsent({ yes: local.yes, global, runtime });
|
|
2177
|
+
const data = await client.blog.articles.executeApproval(local.project, articleId, { revisionId: local.revision, confirmationToken: local.confirmationToken, idempotencyKey: local.idempotencyKey }, { requestId: context.requestId });
|
|
2178
|
+
writeSuccess(context, data, `Exact Blog bundle approved.
|
|
2179
|
+
Revision: ${data.revisionId}
|
|
2180
|
+
Publishing still requires blog:publish.`);
|
|
2181
|
+
} });
|
|
2182
|
+
});
|
|
2183
|
+
const blogSchedule = blog.command("schedule").description("Schedule exact approved Blog bundles with blog:publish; does not grant approval");
|
|
2184
|
+
const addScheduleOptions = (command, executeSchedule) => {
|
|
2185
|
+
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");
|
|
2186
|
+
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");
|
|
2187
|
+
return command;
|
|
2188
|
+
};
|
|
2189
|
+
addScheduleOptions(blogSchedule.command("preview").description("Preview an exact future Blog schedule with blog:publish; does not publish now"), false).action(async (articleId, local) => {
|
|
2190
|
+
const global = program.opts();
|
|
2191
|
+
commandExitCode = await execute({ command: "blog schedule preview", global, runtime, async action(context, client) {
|
|
2192
|
+
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 });
|
|
2193
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2194
|
+
} });
|
|
2195
|
+
});
|
|
2196
|
+
addScheduleOptions(blogSchedule.command("execute").description("Execute a confirmed future Blog schedule with blog:publish; causes future external publication"), true).action(async (articleId, local) => {
|
|
2197
|
+
const global = program.opts();
|
|
2198
|
+
commandExitCode = await execute({ command: "blog schedule execute", global, runtime, async action(context, client) {
|
|
2199
|
+
requireBlogExecutionConsent({ yes: local.yes, global, runtime });
|
|
2200
|
+
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 });
|
|
2201
|
+
writeSuccess(context, data, `Blog article scheduled.
|
|
2202
|
+
Time: ${data.scheduledAt}
|
|
2203
|
+
Timezone: ${data.timezone}`);
|
|
2204
|
+
} });
|
|
2205
|
+
});
|
|
2206
|
+
const blogScheduleCancel = blogSchedule.command("cancel").description("Cancel one future Blog schedule with blog:publish; never unpublishes a live article");
|
|
2207
|
+
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) => {
|
|
2208
|
+
const global = program.opts();
|
|
2209
|
+
commandExitCode = await execute({ command: "blog schedule cancel preview", global, runtime, async action(context, client) {
|
|
2210
|
+
const data = await client.blog.articles.previewScheduleCancellation(local.project, articleId, { requestId: context.requestId });
|
|
2211
|
+
writeSuccess(context, data, JSON.stringify(data, null, 2));
|
|
2212
|
+
} });
|
|
2213
|
+
});
|
|
2214
|
+
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) => {
|
|
2215
|
+
const global = program.opts();
|
|
2216
|
+
commandExitCode = await execute({ command: "blog schedule cancel execute", global, runtime, async action(context, client) {
|
|
2217
|
+
requireBlogExecutionConsent({ yes: local.yes, global, runtime });
|
|
2218
|
+
const data = await client.blog.articles.executeScheduleCancellation(local.project, articleId, { confirmationToken: local.confirmationToken, idempotencyKey: local.idempotencyKey }, { requestId: context.requestId });
|
|
2219
|
+
writeSuccess(context, data, `Future Blog schedule canceled.
|
|
2220
|
+
Article: ${data.articleId}
|
|
2221
|
+
A live article was not unpublished.`);
|
|
2222
|
+
} });
|
|
2223
|
+
});
|
|
2116
2224
|
blog.command("job").argument("<job-id>").requiredOption("--project <project-id>", "exact Rolino project ID").action(async (jobId, local) => {
|
|
2117
2225
|
const global = program.opts();
|
|
2118
2226
|
commandExitCode = await execute({ command: "blog job", global, runtime, async action(context, client) {
|
|
@@ -2236,7 +2344,7 @@ Revision: ${local.revision}` });
|
|
|
2236
2344
|
message: meta.mcp.streamableHttp ? "Remote Streamable HTTP is available." : "Remote MCP is gated; use the stdio server."
|
|
2237
2345
|
}
|
|
2238
2346
|
];
|
|
2239
|
-
const credential = (0,
|
|
2347
|
+
const credential = (0, import_local_auth2.resolveLocalAuthentication)(client.baseUrl, runtime.env);
|
|
2240
2348
|
if (credential.source !== "none") {
|
|
2241
2349
|
const actor = await client.whoami({ requestId: context.requestId });
|
|
2242
2350
|
checks.push({
|