@mediaruntime/cli 0.1.0 → 0.2.1
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 +14 -0
- package/README.md +27 -9
- package/dist/cli.d.ts +39 -1
- package/dist/cli.js +439 -42
- package/dist/cli.js.map +1 -1
- package/docs/AUTH.md +85 -0
- package/docs/CLI_SDD.md +45 -15
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
4
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
5
5
|
import { constants, realpathSync } from "fs";
|
|
6
6
|
import {
|
|
7
7
|
access,
|
|
@@ -52,6 +52,312 @@ var BundleDownloadError = class extends CliError {
|
|
|
52
52
|
}
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
+
// src/auth/api.ts
|
|
56
|
+
var PRODUCTION_ORIGIN = "https://mediaruntime.com";
|
|
57
|
+
var AuthApiError = class extends CliError {
|
|
58
|
+
status;
|
|
59
|
+
constructor(code, message, status) {
|
|
60
|
+
super(code, message, status >= 500 ? 5 : 3);
|
|
61
|
+
this.name = "AuthApiError";
|
|
62
|
+
this.status = status;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
function endpoint(baseUrl, path) {
|
|
66
|
+
return `${baseUrl.replace(/\/+$/, "")}/api/cli/auth/${path}`;
|
|
67
|
+
}
|
|
68
|
+
async function responsePayload(response) {
|
|
69
|
+
try {
|
|
70
|
+
const value = await response.json();
|
|
71
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
72
|
+
} catch {
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function post(baseUrl, path, body, apiKey) {
|
|
77
|
+
let response;
|
|
78
|
+
try {
|
|
79
|
+
response = await fetch(endpoint(baseUrl, path), {
|
|
80
|
+
method: "POST",
|
|
81
|
+
redirect: "error",
|
|
82
|
+
headers: {
|
|
83
|
+
"Content-Type": "application/json",
|
|
84
|
+
...apiKey ? { "X-API-Key": apiKey } : {}
|
|
85
|
+
},
|
|
86
|
+
body: JSON.stringify(body)
|
|
87
|
+
});
|
|
88
|
+
} catch (error) {
|
|
89
|
+
throw new CliError("auth_connection_error", "Could not reach MediaRuntime authentication", 5, { cause: error });
|
|
90
|
+
}
|
|
91
|
+
const payload = await responsePayload(response);
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
const errorPayload = payload;
|
|
94
|
+
throw new AuthApiError(
|
|
95
|
+
typeof errorPayload.error === "string" ? errorPayload.error : "authorization_failed",
|
|
96
|
+
typeof errorPayload.detail === "string" ? errorPayload.detail : `Authentication failed with HTTP ${response.status}`,
|
|
97
|
+
response.status
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return payload;
|
|
101
|
+
}
|
|
102
|
+
function requireString(payload, key) {
|
|
103
|
+
const value = payload[key];
|
|
104
|
+
if (typeof value !== "string" || !value) throw new CliError("invalid_auth_response", "MediaRuntime returned an invalid authentication response", 5);
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
async function startAuthorization(baseUrl, codeChallenge) {
|
|
108
|
+
const payload = await post(baseUrl, "start", { codeChallenge, clientName: "MediaRuntime CLI" });
|
|
109
|
+
return {
|
|
110
|
+
deviceCode: requireString(payload, "deviceCode"),
|
|
111
|
+
userCode: requireString(payload, "userCode"),
|
|
112
|
+
verificationUri: requireString(payload, "verificationUri"),
|
|
113
|
+
verificationUriComplete: requireString(payload, "verificationUriComplete"),
|
|
114
|
+
expiresIn: Number(payload.expiresIn),
|
|
115
|
+
interval: Number(payload.interval)
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
async function exchangeAuthorization(baseUrl, deviceCode, codeVerifier) {
|
|
119
|
+
const payload = await post(baseUrl, "token", { deviceCode, codeVerifier });
|
|
120
|
+
const credentialType = requireString(payload, "credentialType");
|
|
121
|
+
if (credentialType !== "cli") throw new CliError("invalid_auth_response", "MediaRuntime returned the wrong credential type", 5);
|
|
122
|
+
return {
|
|
123
|
+
apiKey: requireString(payload, "apiKey"),
|
|
124
|
+
accountId: requireString(payload, "accountId"),
|
|
125
|
+
keyId: requireString(payload, "keyId"),
|
|
126
|
+
credentialType
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
async function inspectCredential(baseUrl, apiKey) {
|
|
130
|
+
const payload = await post(baseUrl, "status", {}, apiKey);
|
|
131
|
+
const credentialType = requireString(payload, "credentialType");
|
|
132
|
+
if (credentialType !== "cli" && credentialType !== "api_key") {
|
|
133
|
+
throw new CliError("invalid_auth_response", "MediaRuntime returned the wrong credential type", 5);
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
status: "authenticated",
|
|
137
|
+
accountId: requireString(payload, "accountId"),
|
|
138
|
+
keyId: requireString(payload, "keyId"),
|
|
139
|
+
credentialType
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
async function revokeCredential(baseUrl, apiKey) {
|
|
143
|
+
await post(baseUrl, "revoke", {}, apiKey);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/commands/auth.ts
|
|
147
|
+
import { createHash, randomBytes } from "crypto";
|
|
148
|
+
|
|
149
|
+
// src/auth/browser.ts
|
|
150
|
+
import { spawn } from "child_process";
|
|
151
|
+
async function openBrowser(url) {
|
|
152
|
+
const command = process.platform === "darwin" ? { executable: "open", args: [url] } : process.platform === "win32" ? { executable: "rundll32", args: ["url.dll,FileProtocolHandler", url] } : { executable: "xdg-open", args: [url] };
|
|
153
|
+
await new Promise((resolve2, reject) => {
|
|
154
|
+
const child = spawn(command.executable, command.args, {
|
|
155
|
+
detached: true,
|
|
156
|
+
stdio: "ignore"
|
|
157
|
+
});
|
|
158
|
+
child.once("error", (error) => reject(new CliError(
|
|
159
|
+
"browser_open_failed",
|
|
160
|
+
"Could not open a browser; rerun with --no-browser and open the printed URL",
|
|
161
|
+
2,
|
|
162
|
+
{ cause: error }
|
|
163
|
+
)));
|
|
164
|
+
child.once("spawn", () => {
|
|
165
|
+
child.unref();
|
|
166
|
+
resolve2();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/auth/credential-store.ts
|
|
172
|
+
var SERVICE = "mediaruntime-cli";
|
|
173
|
+
function vaultAccount(baseUrl) {
|
|
174
|
+
return new URL(baseUrl).host.toLowerCase();
|
|
175
|
+
}
|
|
176
|
+
function validCredential(value) {
|
|
177
|
+
if (!value || typeof value !== "object") return false;
|
|
178
|
+
const item = value;
|
|
179
|
+
return typeof item.apiKey === "string" && item.apiKey.startsWith("sk_") && typeof item.accountId === "string" && item.accountId.length > 0 && typeof item.keyId === "string" && item.keyId.length > 0 && typeof item.baseUrl === "string" && item.baseUrl.length > 0 && typeof item.createdAt === "string" && item.createdAt.length > 0;
|
|
180
|
+
}
|
|
181
|
+
async function entry(baseUrl) {
|
|
182
|
+
try {
|
|
183
|
+
const { Entry } = await import("@napi-rs/keyring");
|
|
184
|
+
return new Entry(SERVICE, vaultAccount(baseUrl));
|
|
185
|
+
} catch (error) {
|
|
186
|
+
throw new CliError(
|
|
187
|
+
"credential_store_unavailable",
|
|
188
|
+
"The operating-system credential vault is unavailable; use MEDIARUNTIME_API_KEY on this machine",
|
|
189
|
+
2,
|
|
190
|
+
{ cause: error }
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
var OsCredentialStore = class {
|
|
195
|
+
async get(baseUrl) {
|
|
196
|
+
try {
|
|
197
|
+
const raw = (await entry(baseUrl)).getPassword();
|
|
198
|
+
if (!raw) return null;
|
|
199
|
+
const parsed = JSON.parse(raw);
|
|
200
|
+
if (!validCredential(parsed) || parsed.baseUrl !== baseUrl) {
|
|
201
|
+
throw new Error("Stored credential has an invalid shape");
|
|
202
|
+
}
|
|
203
|
+
return parsed;
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (error instanceof SyntaxError || error instanceof Error && error.message.includes("invalid shape")) {
|
|
206
|
+
throw new CliError("credential_store_invalid", "The stored MediaRuntime credential is invalid; run mediaruntime logout --local-only", 3);
|
|
207
|
+
}
|
|
208
|
+
if (error instanceof CliError) throw error;
|
|
209
|
+
if (error instanceof Error && /not found|no entry|no matching|item.*exist/i.test(error.message)) return null;
|
|
210
|
+
throw new CliError("credential_store_error", "Could not read the operating-system credential vault", 2, { cause: error });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async set(credential) {
|
|
214
|
+
try {
|
|
215
|
+
(await entry(credential.baseUrl)).setPassword(JSON.stringify(credential));
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (error instanceof CliError) throw error;
|
|
218
|
+
throw new CliError("credential_store_error", "Could not save the login in the operating-system credential vault", 2, { cause: error });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async delete(baseUrl) {
|
|
222
|
+
try {
|
|
223
|
+
(await entry(baseUrl)).deletePassword();
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (error instanceof CliError) throw error;
|
|
226
|
+
if (error instanceof Error && /not found|no entry|no matching|item.*exist/i.test(error.message)) return;
|
|
227
|
+
throw new CliError("credential_store_error", "Could not remove the login from the operating-system credential vault", 2, { cause: error });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// src/commands/auth.ts
|
|
233
|
+
var defaultSleep = (milliseconds) => new Promise((resolve2) => setTimeout(resolve2, milliseconds));
|
|
234
|
+
function verifierChallenge(verifier) {
|
|
235
|
+
return createHash("sha256").update(verifier).digest("base64url");
|
|
236
|
+
}
|
|
237
|
+
function defaultVerifier() {
|
|
238
|
+
return randomBytes(32).toString("base64url");
|
|
239
|
+
}
|
|
240
|
+
function store(dependencies) {
|
|
241
|
+
return dependencies.credentialStore ?? new OsCredentialStore();
|
|
242
|
+
}
|
|
243
|
+
function assertOnly(args, allowed) {
|
|
244
|
+
const unexpected = args.find((arg) => !allowed.includes(arg));
|
|
245
|
+
if (unexpected) throw new UsageError(`Unknown authentication option: ${unexpected}`);
|
|
246
|
+
}
|
|
247
|
+
async function resolveCredential(baseUrl, dependencies = {}) {
|
|
248
|
+
const environment = process.env.MEDIARUNTIME_API_KEY?.trim();
|
|
249
|
+
if (environment) return { apiKey: environment, source: "environment" };
|
|
250
|
+
const stored = await store(dependencies).get(baseUrl);
|
|
251
|
+
return stored ? { apiKey: stored.apiKey, source: "keychain", stored } : null;
|
|
252
|
+
}
|
|
253
|
+
async function runLoginCommand(args, baseUrl, dependencies = {}) {
|
|
254
|
+
assertOnly(args, ["--no-browser"]);
|
|
255
|
+
const noBrowser = args.includes("--no-browser");
|
|
256
|
+
const write = dependencies.writeStdout ?? ((text) => process.stdout.write(text));
|
|
257
|
+
const existing = await store(dependencies).get(baseUrl);
|
|
258
|
+
if (existing) {
|
|
259
|
+
try {
|
|
260
|
+
await inspectCredential(baseUrl, existing.apiKey);
|
|
261
|
+
write(`Already logged in to ${baseUrl} (account ${existing.accountId}).
|
|
262
|
+
`);
|
|
263
|
+
return 0;
|
|
264
|
+
} catch (error) {
|
|
265
|
+
if (!(error instanceof AuthApiError) || error.status >= 500) throw error;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const verifier = dependencies.randomVerifier?.() ?? defaultVerifier();
|
|
269
|
+
if (verifier.length < 43 || verifier.length > 128) {
|
|
270
|
+
throw new CliError("invalid_verifier", "Generated PKCE verifier is invalid", 1);
|
|
271
|
+
}
|
|
272
|
+
const authorization = await startAuthorization(baseUrl, verifierChallenge(verifier));
|
|
273
|
+
if (!Number.isFinite(authorization.expiresIn) || authorization.expiresIn <= 0 || !Number.isFinite(authorization.interval) || authorization.interval < 1) {
|
|
274
|
+
throw new CliError("invalid_auth_response", "MediaRuntime returned invalid authorization timing", 5);
|
|
275
|
+
}
|
|
276
|
+
write(`Open this URL to authorize MediaRuntime CLI:
|
|
277
|
+
${authorization.verificationUriComplete}
|
|
278
|
+
|
|
279
|
+
`);
|
|
280
|
+
write(`Confirm code: ${authorization.userCode}
|
|
281
|
+
`);
|
|
282
|
+
if (!noBrowser) await (dependencies.openBrowser ?? openBrowser)(authorization.verificationUriComplete);
|
|
283
|
+
write("Waiting for browser approval\u2026\n");
|
|
284
|
+
const now = dependencies.now ?? Date.now;
|
|
285
|
+
const sleep = dependencies.sleep ?? defaultSleep;
|
|
286
|
+
const deadline = now() + authorization.expiresIn * 1e3;
|
|
287
|
+
let credential;
|
|
288
|
+
while (now() < deadline) {
|
|
289
|
+
try {
|
|
290
|
+
credential = await exchangeAuthorization(baseUrl, authorization.deviceCode, verifier);
|
|
291
|
+
break;
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if (error instanceof AuthApiError && error.code === "authorization_pending") {
|
|
294
|
+
await sleep(authorization.interval * 1e3);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (!credential) throw new CliError("authorization_timeout", "Browser authorization expired; run mediaruntime login again", 7);
|
|
301
|
+
try {
|
|
302
|
+
await inspectCredential(baseUrl, credential.apiKey);
|
|
303
|
+
await store(dependencies).set({
|
|
304
|
+
apiKey: credential.apiKey,
|
|
305
|
+
accountId: credential.accountId,
|
|
306
|
+
keyId: credential.keyId,
|
|
307
|
+
baseUrl,
|
|
308
|
+
createdAt: new Date(now()).toISOString()
|
|
309
|
+
});
|
|
310
|
+
} catch (error) {
|
|
311
|
+
await revokeCredential(baseUrl, credential.apiKey).catch(() => void 0);
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
write(`Logged in to ${baseUrl} (account ${credential.accountId}).
|
|
315
|
+
`);
|
|
316
|
+
if (process.env.MEDIARUNTIME_API_KEY?.trim()) {
|
|
317
|
+
write("MEDIARUNTIME_API_KEY is set and will take precedence over this login.\n");
|
|
318
|
+
}
|
|
319
|
+
return 0;
|
|
320
|
+
}
|
|
321
|
+
async function runAuthStatusCommand(args, baseUrl, dependencies = {}) {
|
|
322
|
+
assertOnly(args, ["--json"]);
|
|
323
|
+
const json = args.includes("--json");
|
|
324
|
+
const write = dependencies.writeStdout ?? ((text) => process.stdout.write(text));
|
|
325
|
+
const resolved = await resolveCredential(baseUrl, dependencies);
|
|
326
|
+
if (!resolved) throw new CliError("authentication_required", "Run mediaruntime login or set MEDIARUNTIME_API_KEY", 3);
|
|
327
|
+
const status = await inspectCredential(baseUrl, resolved.apiKey);
|
|
328
|
+
if (json) {
|
|
329
|
+
write(`${JSON.stringify({ authenticated: true, source: resolved.source, ...status })}
|
|
330
|
+
`);
|
|
331
|
+
} else {
|
|
332
|
+
write(`Authenticated via ${resolved.source} (account ${status.accountId}, key ${status.keyId}).
|
|
333
|
+
`);
|
|
334
|
+
}
|
|
335
|
+
return 0;
|
|
336
|
+
}
|
|
337
|
+
async function runLogoutCommand(args, baseUrl, dependencies = {}) {
|
|
338
|
+
assertOnly(args, ["--local-only"]);
|
|
339
|
+
const localOnly = args.includes("--local-only");
|
|
340
|
+
const write = dependencies.writeStdout ?? ((text) => process.stdout.write(text));
|
|
341
|
+
const credentialStore = store(dependencies);
|
|
342
|
+
const credential = await credentialStore.get(baseUrl);
|
|
343
|
+
if (!credential) {
|
|
344
|
+
write(`No stored login for ${baseUrl}.
|
|
345
|
+
`);
|
|
346
|
+
if (process.env.MEDIARUNTIME_API_KEY?.trim()) write("MEDIARUNTIME_API_KEY remains active in this shell.\n");
|
|
347
|
+
return 0;
|
|
348
|
+
}
|
|
349
|
+
if (!localOnly) await revokeCredential(baseUrl, credential.apiKey);
|
|
350
|
+
await credentialStore.delete(baseUrl);
|
|
351
|
+
write(localOnly ? "Removed the local CLI login without server revocation.\n" : "Logged out and revoked the CLI credential.\n");
|
|
352
|
+
if (process.env.MEDIARUNTIME_API_KEY?.trim()) write("MEDIARUNTIME_API_KEY remains active in this shell.\n");
|
|
353
|
+
return 0;
|
|
354
|
+
}
|
|
355
|
+
async function runAuthCommand(args, baseUrl, dependencies = {}) {
|
|
356
|
+
const subcommand = args[0];
|
|
357
|
+
if (subcommand !== "status") throw new UsageError("Usage: mediaruntime auth status [--json]");
|
|
358
|
+
return runAuthStatusCommand(args.slice(1), baseUrl, dependencies);
|
|
359
|
+
}
|
|
360
|
+
|
|
55
361
|
// src/commands/jobs.ts
|
|
56
362
|
var UNSUCCESSFUL_TERMINAL_STATUSES = /* @__PURE__ */ new Set(["FAILED", "REJECTED", "PARTIAL"]);
|
|
57
363
|
function optionValue(args, index, option) {
|
|
@@ -360,47 +666,63 @@ async function runCommand(args, dependencies) {
|
|
|
360
666
|
...options.metadata === void 0 ? {} : { metadata: options.metadata },
|
|
361
667
|
...options.idempotencyKey === void 0 ? {} : { idempotencyKey: options.idempotencyKey }
|
|
362
668
|
};
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
`);
|
|
367
|
-
else writeHumanReceipt(submitted, dependencies.writeStdout);
|
|
368
|
-
return UNSUCCESSFUL_TERMINAL_STATUSES2.has(String(submitted.status).toUpperCase()) ? 6 : 0;
|
|
369
|
-
}
|
|
370
|
-
const details = await submitted.wait(
|
|
371
|
-
options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
|
|
669
|
+
const isLocalSource = !/^(?:https?|gs):\/\//i.test(options.source);
|
|
670
|
+
dependencies.activity.start(
|
|
671
|
+
isLocalSource ? "Uploading local source and creating job\u2026" : "Creating job\u2026"
|
|
372
672
|
);
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
if (options.
|
|
673
|
+
try {
|
|
674
|
+
const submitted = await dependencies.jobs.create(params);
|
|
675
|
+
if (!options.wait) {
|
|
676
|
+
dependencies.activity.stop();
|
|
677
|
+
if (options.json) dependencies.writeStdout(`${JSON.stringify(receiptProjection(submitted))}
|
|
376
678
|
`);
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
if (options.download) {
|
|
380
|
-
const url = details.bundle.downloadUrl;
|
|
381
|
-
if (!details.bundle.available || !url) {
|
|
382
|
-
throw new BundleDownloadError("Completed job does not have an available canonical bundle");
|
|
679
|
+
else writeHumanReceipt(submitted, dependencies.writeStdout);
|
|
680
|
+
return UNSUCCESSFUL_TERMINAL_STATUSES2.has(String(submitted.status).toUpperCase()) ? 6 : 0;
|
|
383
681
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
if (
|
|
392
|
-
|
|
682
|
+
dependencies.activity.update(`Waiting for job ${submitted.id} to complete\u2026`);
|
|
683
|
+
const details = await submitted.wait(
|
|
684
|
+
options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
|
|
685
|
+
);
|
|
686
|
+
if (String(details.status).toUpperCase() !== "COMPLETED") {
|
|
687
|
+
dependencies.activity.stop();
|
|
688
|
+
if (!options.json) writeHumanDetails(details, dependencies.writeStdout);
|
|
689
|
+
if (options.json) dependencies.writeStdout(`${JSON.stringify(detailsProjection2(details))}
|
|
690
|
+
`);
|
|
691
|
+
return 6;
|
|
692
|
+
}
|
|
693
|
+
if (options.download) {
|
|
694
|
+
const url = details.bundle.downloadUrl;
|
|
695
|
+
if (!details.bundle.available || !url) {
|
|
696
|
+
throw new BundleDownloadError("Completed job does not have an available canonical bundle");
|
|
697
|
+
}
|
|
698
|
+
dependencies.activity.update("Downloading and verifying ZIP bundle\u2026");
|
|
699
|
+
try {
|
|
700
|
+
await dependencies.downloadBundle(url, options.download, {
|
|
701
|
+
force: options.force,
|
|
702
|
+
expectedSizeBytes: details.bundle.sizeBytes,
|
|
703
|
+
expectedSha256: details.bundle.sha256
|
|
704
|
+
});
|
|
705
|
+
} catch (error) {
|
|
706
|
+
if (error instanceof BundleDownloadError) throw error;
|
|
707
|
+
throw new BundleDownloadError(`Could not download bundle to ${options.download}`, { cause: error });
|
|
708
|
+
}
|
|
393
709
|
}
|
|
394
|
-
|
|
710
|
+
dependencies.activity.stop();
|
|
711
|
+
if (!options.json) {
|
|
712
|
+
writeHumanDetails(details, dependencies.writeStdout);
|
|
713
|
+
if (options.download) dependencies.writeStdout(`Downloaded bundle to ${options.download}
|
|
395
714
|
`);
|
|
396
|
-
|
|
397
|
-
|
|
715
|
+
}
|
|
716
|
+
if (options.json) dependencies.writeStdout(`${JSON.stringify(detailsProjection2(details))}
|
|
398
717
|
`);
|
|
399
|
-
|
|
718
|
+
return 0;
|
|
719
|
+
} finally {
|
|
720
|
+
dependencies.activity.stop();
|
|
721
|
+
}
|
|
400
722
|
}
|
|
401
723
|
|
|
402
724
|
// src/commands/trigger.ts
|
|
403
|
-
import { randomBytes, randomUUID } from "crypto";
|
|
725
|
+
import { randomBytes as randomBytes2, randomUUID } from "crypto";
|
|
404
726
|
import { readFile } from "fs/promises";
|
|
405
727
|
|
|
406
728
|
// src/trigger/destination.ts
|
|
@@ -628,7 +950,7 @@ async function runTriggerCommand(args, dependencies = {}) {
|
|
|
628
950
|
fetch: dependencies.fetch ?? globalThis.fetch,
|
|
629
951
|
now: dependencies.now ?? Date.now,
|
|
630
952
|
randomUUID: dependencies.randomUUID ?? randomUUID,
|
|
631
|
-
randomBytes: dependencies.randomBytes ??
|
|
953
|
+
randomBytes: dependencies.randomBytes ?? randomBytes2,
|
|
632
954
|
readFile: dependencies.readFile ?? readFile,
|
|
633
955
|
writeStdout: dependencies.writeStdout ?? ((text) => process.stdout.write(text))
|
|
634
956
|
};
|
|
@@ -676,8 +998,49 @@ async function runTriggerCommand(args, dependencies = {}) {
|
|
|
676
998
|
return 0;
|
|
677
999
|
}
|
|
678
1000
|
|
|
1001
|
+
// src/ui/activity.ts
|
|
1002
|
+
var FRAMES = ["\u28CB", "\u28D9", "\u28F9", "\u28F8", "\u28FC", "\u28F4", "\u28E6", "\u28E7", "\u28C7", "\u28CF"];
|
|
1003
|
+
var CLEAR_LINE = "\r\x1B[2K";
|
|
1004
|
+
function createActivityIndicator(write, enabled) {
|
|
1005
|
+
let timer;
|
|
1006
|
+
let frameIndex = 0;
|
|
1007
|
+
let message = "";
|
|
1008
|
+
let startedAt = 0;
|
|
1009
|
+
const render = () => {
|
|
1010
|
+
if (!timer && startedAt === 0) return;
|
|
1011
|
+
const elapsedSeconds = Math.max(0, Math.floor((Date.now() - startedAt) / 1e3));
|
|
1012
|
+
const elapsed = elapsedSeconds > 0 ? ` (${elapsedSeconds}s)` : "";
|
|
1013
|
+
write(`${CLEAR_LINE}${FRAMES[frameIndex % FRAMES.length]} ${message}${elapsed}`);
|
|
1014
|
+
frameIndex += 1;
|
|
1015
|
+
};
|
|
1016
|
+
return {
|
|
1017
|
+
start(nextMessage) {
|
|
1018
|
+
if (!enabled) return;
|
|
1019
|
+
if (timer) clearInterval(timer);
|
|
1020
|
+
message = nextMessage;
|
|
1021
|
+
startedAt = Date.now();
|
|
1022
|
+
frameIndex = 0;
|
|
1023
|
+
render();
|
|
1024
|
+
timer = setInterval(render, 80);
|
|
1025
|
+
timer.unref?.();
|
|
1026
|
+
},
|
|
1027
|
+
update(nextMessage) {
|
|
1028
|
+
if (!enabled || startedAt === 0) return;
|
|
1029
|
+
message = nextMessage;
|
|
1030
|
+
render();
|
|
1031
|
+
},
|
|
1032
|
+
stop() {
|
|
1033
|
+
if (!enabled || startedAt === 0) return;
|
|
1034
|
+
if (timer) clearInterval(timer);
|
|
1035
|
+
timer = void 0;
|
|
1036
|
+
startedAt = 0;
|
|
1037
|
+
write(CLEAR_LINE);
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
|
|
679
1042
|
// src/cli.ts
|
|
680
|
-
var VERSION = "0.1
|
|
1043
|
+
var VERSION = "0.2.1";
|
|
681
1044
|
var HELP = `MediaRuntime CLI
|
|
682
1045
|
|
|
683
1046
|
Usage:
|
|
@@ -685,6 +1048,9 @@ Usage:
|
|
|
685
1048
|
mediaruntime jobs list [--status <status>] [--limit <n>] [--cursor <cursor>]
|
|
686
1049
|
mediaruntime jobs get <job_id> [--download <bundle.zip>] [--force]
|
|
687
1050
|
mediaruntime trigger <job.completed|job.failed|job.rejected> --to <local-url>
|
|
1051
|
+
mediaruntime login [--no-browser]
|
|
1052
|
+
mediaruntime auth status
|
|
1053
|
+
mediaruntime logout [--local-only]
|
|
688
1054
|
|
|
689
1055
|
Global options:
|
|
690
1056
|
--base-url <url> Developer override; normally leave unset
|
|
@@ -694,7 +1060,7 @@ Global options:
|
|
|
694
1060
|
Run/jobs options:
|
|
695
1061
|
--json Machine-readable, URL-redacted output
|
|
696
1062
|
|
|
697
|
-
Authentication uses MEDIARUNTIME_API_KEY.
|
|
1063
|
+
Authentication uses a secure login or MEDIARUNTIME_API_KEY.
|
|
698
1064
|
`;
|
|
699
1065
|
function isFileExistsError(error) {
|
|
700
1066
|
return error instanceof Error && "code" in error && error.code === "EEXIST";
|
|
@@ -728,7 +1094,7 @@ async function downloadBundleAtomically(url, destination, options) {
|
|
|
728
1094
|
throw new BundleDownloadError(`Bundle download failed with HTTP ${response.status}`);
|
|
729
1095
|
}
|
|
730
1096
|
if (!response.body) throw new BundleDownloadError("Bundle download returned an empty response body");
|
|
731
|
-
const hash =
|
|
1097
|
+
const hash = createHash2("sha256");
|
|
732
1098
|
let sizeBytes = 0;
|
|
733
1099
|
const integrity = new Transform({
|
|
734
1100
|
transform(chunk, _encoding, callback) {
|
|
@@ -832,6 +1198,7 @@ async function executeCli(argv, dependencies = {}) {
|
|
|
832
1198
|
const writeStdout = dependencies.writeStdout ?? ((text) => process.stdout.write(text));
|
|
833
1199
|
const writeStderr = dependencies.writeStderr ?? ((text) => process.stderr.write(text));
|
|
834
1200
|
const json = argv.includes("--json");
|
|
1201
|
+
const isStderrTTY = dependencies.isStderrTTY ?? (dependencies.writeStderr === void 0 && process.stderr.isTTY === true);
|
|
835
1202
|
try {
|
|
836
1203
|
const global = extractGlobalOptions(argv);
|
|
837
1204
|
const command = global.args[0];
|
|
@@ -849,14 +1216,39 @@ async function executeCli(argv, dependencies = {}) {
|
|
|
849
1216
|
}
|
|
850
1217
|
const configuredBaseUrl = global.baseUrl ?? process.env.MEDIARUNTIME_API_URL;
|
|
851
1218
|
const baseUrl = configuredBaseUrl ? normalizeBaseUrl(configuredBaseUrl) : void 0;
|
|
852
|
-
|
|
853
|
-
|
|
1219
|
+
const authBaseUrl = baseUrl ?? PRODUCTION_ORIGIN;
|
|
1220
|
+
const authDependencies = {
|
|
1221
|
+
...dependencies.credentialStore ? { credentialStore: dependencies.credentialStore } : {},
|
|
1222
|
+
...dependencies.openBrowser ? { openBrowser: dependencies.openBrowser } : {},
|
|
1223
|
+
...dependencies.sleep ? { sleep: dependencies.sleep } : {},
|
|
1224
|
+
...dependencies.now ? { now: dependencies.now } : {},
|
|
1225
|
+
...dependencies.randomVerifier ? { randomVerifier: dependencies.randomVerifier } : {},
|
|
1226
|
+
writeStdout
|
|
1227
|
+
};
|
|
1228
|
+
if (command === "login") return await runLoginCommand(global.args.slice(1), authBaseUrl, authDependencies);
|
|
1229
|
+
if (command === "auth") return await runAuthCommand(global.args.slice(1), authBaseUrl, authDependencies);
|
|
1230
|
+
if (command === "logout") return await runLogoutCommand(global.args.slice(1), authBaseUrl, authDependencies);
|
|
1231
|
+
let apiKey = process.env.MEDIARUNTIME_API_KEY?.trim();
|
|
1232
|
+
if (!apiKey && (!dependencies.createClient || dependencies.credentialStore)) {
|
|
1233
|
+
apiKey = (await resolveCredential(authBaseUrl, authDependencies))?.apiKey;
|
|
1234
|
+
}
|
|
1235
|
+
if (!dependencies.createClient && !apiKey) {
|
|
1236
|
+
throw new UsageError("Run mediaruntime login or set MEDIARUNTIME_API_KEY before using run or jobs");
|
|
854
1237
|
}
|
|
855
1238
|
const client = dependencies.createClient?.({
|
|
856
|
-
...baseUrl === void 0 ? {} : { baseUrl }
|
|
857
|
-
|
|
1239
|
+
...baseUrl === void 0 ? {} : { baseUrl },
|
|
1240
|
+
...apiKey === void 0 ? {} : { apiKey }
|
|
1241
|
+
}) ?? new MediaRuntime({
|
|
1242
|
+
...baseUrl === void 0 ? {} : { baseUrl },
|
|
1243
|
+
...apiKey === void 0 ? {} : { apiKey }
|
|
1244
|
+
});
|
|
858
1245
|
const downloadBundle = dependencies.downloadBundle ?? downloadBundleAtomically;
|
|
859
|
-
const commandDependencies = {
|
|
1246
|
+
const commandDependencies = {
|
|
1247
|
+
jobs: client.jobs,
|
|
1248
|
+
writeStdout,
|
|
1249
|
+
downloadBundle,
|
|
1250
|
+
activity: createActivityIndicator(writeStderr, isStderrTTY && !json)
|
|
1251
|
+
};
|
|
860
1252
|
if (command === "run") return await runCommand(global.args.slice(1), commandDependencies);
|
|
861
1253
|
if (command === "jobs") {
|
|
862
1254
|
return await runJobsCommand(global.args.slice(1), commandDependencies);
|
|
@@ -889,6 +1281,11 @@ export {
|
|
|
889
1281
|
downloadBundleAtomically,
|
|
890
1282
|
executeCli,
|
|
891
1283
|
parseTriggerArguments,
|
|
1284
|
+
resolveCredential,
|
|
1285
|
+
runAuthCommand,
|
|
1286
|
+
runAuthStatusCommand,
|
|
1287
|
+
runLoginCommand,
|
|
1288
|
+
runLogoutCommand,
|
|
892
1289
|
runTriggerCommand,
|
|
893
1290
|
signSyntheticWebhook
|
|
894
1291
|
};
|