@viucraft/cli 0.1.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/README.md +279 -0
- package/dist/index.js +2928 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2928 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/lib/browser.ts
|
|
7
|
+
import { exec } from "child_process";
|
|
8
|
+
import { createWriteStream, existsSync, statSync } from "fs";
|
|
9
|
+
import { basename, join } from "path";
|
|
10
|
+
import { pipeline } from "stream/promises";
|
|
11
|
+
import { Readable } from "stream";
|
|
12
|
+
import { platform } from "os";
|
|
13
|
+
function openInBrowser(url) {
|
|
14
|
+
const cmd = platform() === "darwin" ? "open" : platform() === "win32" ? "start" : "xdg-open";
|
|
15
|
+
exec(`${cmd} ${url}`, (err) => {
|
|
16
|
+
if (err) {
|
|
17
|
+
console.log(`Open in your browser: ${url}`);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
async function downloadFile(url, outputPath) {
|
|
22
|
+
const response = await fetch(url);
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
throw new Error(`Download failed: ${response.status} ${response.statusText}`);
|
|
25
|
+
}
|
|
26
|
+
if (!response.body) {
|
|
27
|
+
throw new Error("Download failed: empty response body");
|
|
28
|
+
}
|
|
29
|
+
let finalPath = outputPath;
|
|
30
|
+
if (existsSync(outputPath) && statSync(outputPath).isDirectory()) {
|
|
31
|
+
const urlFilename = basename(new URL(url).pathname);
|
|
32
|
+
finalPath = join(outputPath, urlFilename);
|
|
33
|
+
}
|
|
34
|
+
const nodeStream = Readable.fromWeb(response.body);
|
|
35
|
+
const fileStream = createWriteStream(finalPath);
|
|
36
|
+
await pipeline(nodeStream, fileStream);
|
|
37
|
+
return finalPath;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/commands/auth.ts
|
|
41
|
+
import { password, input, confirm } from "@inquirer/prompts";
|
|
42
|
+
import chalk2 from "chalk";
|
|
43
|
+
|
|
44
|
+
// src/lib/client.ts
|
|
45
|
+
import { arch, platform as platform2 } from "os";
|
|
46
|
+
|
|
47
|
+
// src/lib/errors.ts
|
|
48
|
+
var CliError = class extends Error {
|
|
49
|
+
constructor(message, exitCode = 1, requestId) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.exitCode = exitCode;
|
|
52
|
+
this.requestId = requestId;
|
|
53
|
+
this.name = "CliError";
|
|
54
|
+
}
|
|
55
|
+
exitCode;
|
|
56
|
+
requestId;
|
|
57
|
+
};
|
|
58
|
+
var AuthenticationError = class extends CliError {
|
|
59
|
+
constructor(message = "Invalid or expired API key. Run `viucraft auth login`.", requestId) {
|
|
60
|
+
super(message, 2, requestId);
|
|
61
|
+
this.name = "AuthenticationError";
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
var ForbiddenError = class extends CliError {
|
|
65
|
+
constructor(message = "Access denied. Your plan may not include this feature.", requestId) {
|
|
66
|
+
super(message, 3, requestId);
|
|
67
|
+
this.name = "ForbiddenError";
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
var NotFoundError = class extends CliError {
|
|
71
|
+
constructor(message = "Resource not found.", requestId) {
|
|
72
|
+
super(message, 3, requestId);
|
|
73
|
+
this.name = "NotFoundError";
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
var ValidationError = class extends CliError {
|
|
77
|
+
constructor(message, requestId) {
|
|
78
|
+
super(message, 3, requestId);
|
|
79
|
+
this.name = "ValidationError";
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
var RateLimitError = class extends CliError {
|
|
83
|
+
constructor(message, retryAfter, requestId) {
|
|
84
|
+
super(message, 3, requestId);
|
|
85
|
+
this.retryAfter = retryAfter;
|
|
86
|
+
this.name = "RateLimitError";
|
|
87
|
+
}
|
|
88
|
+
retryAfter;
|
|
89
|
+
};
|
|
90
|
+
var ServerError = class extends CliError {
|
|
91
|
+
constructor(message = "Server error. Try again later.", requestId) {
|
|
92
|
+
super(message, 3, requestId);
|
|
93
|
+
this.name = "ServerError";
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// src/lib/masking.ts
|
|
98
|
+
function maskApiKey(key) {
|
|
99
|
+
if (key.startsWith("vc_live_") || key.startsWith("vc_test_")) {
|
|
100
|
+
const prefix = key.substring(0, 8);
|
|
101
|
+
const suffix = key.slice(-4);
|
|
102
|
+
return `${prefix}...${suffix}`;
|
|
103
|
+
}
|
|
104
|
+
return `...${key.slice(-4)}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/lib/client.ts
|
|
108
|
+
var MAX_RETRIES = 3;
|
|
109
|
+
var RETRY_STATUS_CODES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
|
|
110
|
+
var ApiClient = class {
|
|
111
|
+
apiKey;
|
|
112
|
+
baseUrl;
|
|
113
|
+
debug;
|
|
114
|
+
userAgent;
|
|
115
|
+
constructor(opts) {
|
|
116
|
+
if (!opts.insecure && opts.baseUrl.startsWith("http://")) {
|
|
117
|
+
throw new Error("HTTPS is required. Use --insecure for local development.");
|
|
118
|
+
}
|
|
119
|
+
this.apiKey = opts.apiKey;
|
|
120
|
+
this.baseUrl = opts.baseUrl.replace(/\/$/, "");
|
|
121
|
+
this.debug = opts.debug ?? false;
|
|
122
|
+
this.userAgent = `viucraft-cli/${"0.1.1"} (${platform2()}/${arch()})`;
|
|
123
|
+
}
|
|
124
|
+
async get(path, params) {
|
|
125
|
+
const url = new URL(path, this.baseUrl);
|
|
126
|
+
if (params) {
|
|
127
|
+
for (const [key, value] of Object.entries(params)) {
|
|
128
|
+
url.searchParams.set(key, value);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return this.request(url.toString(), { method: "GET" });
|
|
132
|
+
}
|
|
133
|
+
async post(path, body) {
|
|
134
|
+
const url = new URL(path, this.baseUrl);
|
|
135
|
+
const init = { method: "POST" };
|
|
136
|
+
if (body !== void 0) {
|
|
137
|
+
init.headers = { "Content-Type": "application/json" };
|
|
138
|
+
init.body = JSON.stringify(body);
|
|
139
|
+
}
|
|
140
|
+
return this.request(url.toString(), init);
|
|
141
|
+
}
|
|
142
|
+
async postForm(path, form) {
|
|
143
|
+
const url = new URL(path, this.baseUrl);
|
|
144
|
+
return this.request(url.toString(), { method: "POST", body: form });
|
|
145
|
+
}
|
|
146
|
+
async delete(path) {
|
|
147
|
+
const url = new URL(path, this.baseUrl);
|
|
148
|
+
await this.request(url.toString(), { method: "DELETE" });
|
|
149
|
+
}
|
|
150
|
+
async stream(path, lastEventId) {
|
|
151
|
+
const url = new URL(path, this.baseUrl);
|
|
152
|
+
const headers = {
|
|
153
|
+
"X-API-Key": this.apiKey,
|
|
154
|
+
"User-Agent": this.userAgent,
|
|
155
|
+
Accept: "text/event-stream"
|
|
156
|
+
};
|
|
157
|
+
if (lastEventId) headers["Last-Event-ID"] = lastEventId;
|
|
158
|
+
if (this.debug) {
|
|
159
|
+
console.error(`[debug] GET ${url} (SSE stream)`);
|
|
160
|
+
}
|
|
161
|
+
const response = await fetch(url.toString(), { headers });
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
164
|
+
const message = extractErrorMessage(errorBody, response.statusText);
|
|
165
|
+
this.throwForStatus(response.status, message, response);
|
|
166
|
+
}
|
|
167
|
+
return response;
|
|
168
|
+
}
|
|
169
|
+
async getRaw(path) {
|
|
170
|
+
const url = new URL(path, this.baseUrl);
|
|
171
|
+
const headers = {
|
|
172
|
+
"X-API-Key": this.apiKey,
|
|
173
|
+
"User-Agent": this.userAgent
|
|
174
|
+
};
|
|
175
|
+
if (this.debug) {
|
|
176
|
+
console.error(`[debug] GET ${url} (binary)`);
|
|
177
|
+
}
|
|
178
|
+
const response = await fetch(url.toString(), { headers });
|
|
179
|
+
if (!response.ok) {
|
|
180
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
181
|
+
const message = extractErrorMessage(errorBody, response.statusText);
|
|
182
|
+
this.throwForStatus(response.status, message, response);
|
|
183
|
+
}
|
|
184
|
+
return response;
|
|
185
|
+
}
|
|
186
|
+
async request(url, init, attempt = 1) {
|
|
187
|
+
const headers = new Headers(init.headers);
|
|
188
|
+
headers.set("X-API-Key", this.apiKey);
|
|
189
|
+
headers.set("User-Agent", this.userAgent);
|
|
190
|
+
headers.set("Accept", "application/json");
|
|
191
|
+
if (this.debug) {
|
|
192
|
+
const maskedKey = maskApiKey(this.apiKey);
|
|
193
|
+
console.error(`[debug] ${init.method ?? "GET"} ${url}`);
|
|
194
|
+
console.error(`[debug] X-API-Key: ${maskedKey}`);
|
|
195
|
+
}
|
|
196
|
+
const response = await fetch(url, { ...init, headers });
|
|
197
|
+
if (this.debug) {
|
|
198
|
+
console.error(`[debug] ${response.status} ${response.statusText}`);
|
|
199
|
+
}
|
|
200
|
+
if (response.status === 204) {
|
|
201
|
+
return void 0;
|
|
202
|
+
}
|
|
203
|
+
if (RETRY_STATUS_CODES.has(response.status) && attempt <= MAX_RETRIES) {
|
|
204
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
205
|
+
const delay = retryAfter ? parseInt(retryAfter, 10) * 1e3 : Math.min(1e3 * 2 ** (attempt - 1), 8e3) + Math.random() * 1e3;
|
|
206
|
+
console.error(
|
|
207
|
+
`Rate limited. Retrying in ${Math.ceil(delay / 1e3)}s... (attempt ${attempt}/${MAX_RETRIES})`
|
|
208
|
+
);
|
|
209
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
210
|
+
return this.request(url, init, attempt + 1);
|
|
211
|
+
}
|
|
212
|
+
if (!response.ok) {
|
|
213
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
214
|
+
const message = extractErrorMessage(errorBody, response.statusText);
|
|
215
|
+
this.throwForStatus(response.status, message, response);
|
|
216
|
+
}
|
|
217
|
+
return response.json();
|
|
218
|
+
}
|
|
219
|
+
throwForStatus(status, message, response) {
|
|
220
|
+
const requestId = response.headers.get("x-request-id") ?? void 0;
|
|
221
|
+
if (this.debug && requestId) {
|
|
222
|
+
console.error(`[debug] X-Request-Id: ${requestId}`);
|
|
223
|
+
}
|
|
224
|
+
switch (status) {
|
|
225
|
+
case 401:
|
|
226
|
+
throw new AuthenticationError(message, requestId);
|
|
227
|
+
case 403:
|
|
228
|
+
throw new ForbiddenError(message, requestId);
|
|
229
|
+
case 404:
|
|
230
|
+
throw new NotFoundError(message, requestId);
|
|
231
|
+
case 422:
|
|
232
|
+
throw new ValidationError(message, requestId);
|
|
233
|
+
case 429: {
|
|
234
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
235
|
+
throw new RateLimitError(
|
|
236
|
+
message,
|
|
237
|
+
retryAfter ? parseInt(retryAfter, 10) : void 0,
|
|
238
|
+
requestId
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
default:
|
|
242
|
+
throw new ServerError(message, requestId);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
function extractErrorMessage(body, fallback) {
|
|
247
|
+
if (typeof body !== "object" || body === null) return fallback;
|
|
248
|
+
const obj = body;
|
|
249
|
+
if (typeof obj.message === "string") return obj.message;
|
|
250
|
+
if (typeof obj.error === "string") return obj.error;
|
|
251
|
+
if (typeof obj.error === "object" && obj.error !== null) {
|
|
252
|
+
const nested = obj.error;
|
|
253
|
+
if (typeof nested.message === "string") return nested.message;
|
|
254
|
+
return JSON.stringify(obj.error);
|
|
255
|
+
}
|
|
256
|
+
return fallback;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/lib/config.ts
|
|
260
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync, statSync as statSync2, writeFileSync } from "fs";
|
|
261
|
+
import { homedir } from "os";
|
|
262
|
+
import { join as join2 } from "path";
|
|
263
|
+
import { parse, stringify } from "yaml";
|
|
264
|
+
var CONFIG_DIR = join2(homedir(), ".viucraft");
|
|
265
|
+
var CONFIG_FILE = join2(CONFIG_DIR, "config.yaml");
|
|
266
|
+
function getConfigPath() {
|
|
267
|
+
return CONFIG_FILE;
|
|
268
|
+
}
|
|
269
|
+
function loadConfig() {
|
|
270
|
+
if (!existsSync2(CONFIG_FILE)) {
|
|
271
|
+
return {
|
|
272
|
+
current_profile: "",
|
|
273
|
+
profiles: {},
|
|
274
|
+
defaults: {
|
|
275
|
+
output_format: "table",
|
|
276
|
+
color: true
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
checkConfigPermissions();
|
|
281
|
+
const raw = readFileSync(CONFIG_FILE, "utf-8");
|
|
282
|
+
const parsed = parse(raw);
|
|
283
|
+
return {
|
|
284
|
+
current_profile: parsed.current_profile ?? "",
|
|
285
|
+
profiles: parsed.profiles ?? {},
|
|
286
|
+
defaults: {
|
|
287
|
+
output_format: parsed.defaults?.output_format ?? "table",
|
|
288
|
+
color: parsed.defaults?.color ?? true
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function saveConfig(config) {
|
|
293
|
+
if (!existsSync2(CONFIG_DIR)) {
|
|
294
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
295
|
+
}
|
|
296
|
+
const yaml = stringify(config);
|
|
297
|
+
writeFileSync(CONFIG_FILE, yaml, { mode: 384 });
|
|
298
|
+
}
|
|
299
|
+
function checkConfigPermissions() {
|
|
300
|
+
try {
|
|
301
|
+
const stats = statSync2(CONFIG_FILE);
|
|
302
|
+
const mode = stats.mode & 511;
|
|
303
|
+
if (mode & 63) {
|
|
304
|
+
console.error(
|
|
305
|
+
`Warning: config file permissions are too open (${mode.toString(8)}). Run: chmod 600 ${CONFIG_FILE}`
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
} catch {
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// src/lib/auth.ts
|
|
313
|
+
function createClientFromGlobalOpts(globalOpts) {
|
|
314
|
+
const creds = resolveCredentials({
|
|
315
|
+
apiKey: globalOpts.apiKey,
|
|
316
|
+
profile: globalOpts.profile
|
|
317
|
+
});
|
|
318
|
+
return new ApiClient({
|
|
319
|
+
apiKey: creds.apiKey,
|
|
320
|
+
baseUrl: creds.apiUrl,
|
|
321
|
+
debug: globalOpts.debug,
|
|
322
|
+
insecure: globalOpts.insecure ?? creds.insecure
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
function resolveCredentials(opts) {
|
|
326
|
+
if (opts.apiKey) {
|
|
327
|
+
return {
|
|
328
|
+
apiKey: opts.apiKey,
|
|
329
|
+
apiUrl: "https://api.viucraft.com",
|
|
330
|
+
profile: "(flag)",
|
|
331
|
+
source: "flag"
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const envKey = process.env.VIUCRAFT_API_KEY;
|
|
335
|
+
if (envKey) {
|
|
336
|
+
return {
|
|
337
|
+
apiKey: envKey,
|
|
338
|
+
apiUrl: process.env.VIUCRAFT_API_URL ?? "https://api.viucraft.com",
|
|
339
|
+
profile: "(env)",
|
|
340
|
+
source: "env"
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
const config = loadConfig();
|
|
344
|
+
const profileName = opts.profile ?? process.env.VIUCRAFT_PROFILE ?? config.current_profile;
|
|
345
|
+
if (!profileName || !config.profiles[profileName]) {
|
|
346
|
+
throw new Error("No profile configured. Run `viucraft auth login` to set up a profile.");
|
|
347
|
+
}
|
|
348
|
+
const profile = config.profiles[profileName];
|
|
349
|
+
return {
|
|
350
|
+
apiKey: profile.api_key,
|
|
351
|
+
apiUrl: profile.api_url,
|
|
352
|
+
profile: profileName,
|
|
353
|
+
source: "profile",
|
|
354
|
+
insecure: profile.insecure,
|
|
355
|
+
subdomain: profile.subdomain
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
function detectEnvironment(apiKey) {
|
|
359
|
+
if (apiKey.startsWith("vc_test_")) return "test";
|
|
360
|
+
return "live";
|
|
361
|
+
}
|
|
362
|
+
function validateKeyFormat(apiKey) {
|
|
363
|
+
return apiKey.startsWith("vc_live_") || apiKey.startsWith("vc_test_");
|
|
364
|
+
}
|
|
365
|
+
function buildImageUrl(opts) {
|
|
366
|
+
const isLocal = opts.apiUrl.includes("localhost") || opts.apiUrl.includes("127.0.0.1");
|
|
367
|
+
const parsed = new URL(opts.apiUrl);
|
|
368
|
+
const port = parsed.port ? `:${parsed.port}` : "";
|
|
369
|
+
const protocol = opts.insecure || isLocal ? "http" : "https";
|
|
370
|
+
const baseDomain = isLocal ? `viucraft.com${port}` : "viucraft.com";
|
|
371
|
+
const opsPath = opts.ops ? `/${opts.ops}` : "";
|
|
372
|
+
return `${protocol}://${opts.subdomain}.${baseDomain}${opsPath}/${opts.uuid}.${opts.format}`;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/lib/output.ts
|
|
376
|
+
import chalk from "chalk";
|
|
377
|
+
function printTable(columns, rows) {
|
|
378
|
+
const widths = columns.map((col) => {
|
|
379
|
+
const dataWidths = rows.map((row) => String(row[col.key] ?? "").length);
|
|
380
|
+
return Math.max(col.header.length, ...dataWidths, col.width ?? 0);
|
|
381
|
+
});
|
|
382
|
+
const headerLine = columns.map((col, i) => chalk.bold(col.header.padEnd(widths[i]))).join(" ");
|
|
383
|
+
console.log(headerLine);
|
|
384
|
+
console.log(widths.map((w) => "\u2500".repeat(w)).join(" "));
|
|
385
|
+
for (const row of rows) {
|
|
386
|
+
const line = columns.map((col, i) => String(row[col.key] ?? "").padEnd(widths[i])).join(" ");
|
|
387
|
+
console.log(line);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function printJson(data) {
|
|
391
|
+
console.log(JSON.stringify(data, null, 2));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// src/commands/auth.ts
|
|
395
|
+
function registerAuthCommands(program2) {
|
|
396
|
+
const auth = program2.command("auth").description("Manage authentication and profiles");
|
|
397
|
+
auth.command("login").description("Authenticate with your VIUCraft API key").option("--profile <name>", "Profile name to save as").option("--api-key <key>", "API key (skips interactive prompt)").option("--api-url <url>", "API base URL (for dev/staging)").action(async (opts) => {
|
|
398
|
+
const globalOpts = program2.opts();
|
|
399
|
+
let apiKey = opts.apiKey ?? globalOpts.apiKey;
|
|
400
|
+
if (apiKey) {
|
|
401
|
+
console.error(
|
|
402
|
+
chalk2.yellow(
|
|
403
|
+
"Warning: passing API keys via flags may expose them in shell history. Consider using `viucraft auth login` interactively."
|
|
404
|
+
)
|
|
405
|
+
);
|
|
406
|
+
} else {
|
|
407
|
+
apiKey = await password({
|
|
408
|
+
message: "Enter your VIUCraft API key:"
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
if (!apiKey || !validateKeyFormat(apiKey)) {
|
|
412
|
+
console.error(
|
|
413
|
+
chalk2.red('Invalid API key format. Keys must start with "vc_live_" or "vc_test_".')
|
|
414
|
+
);
|
|
415
|
+
process.exit(2);
|
|
416
|
+
}
|
|
417
|
+
const environment = detectEnvironment(apiKey);
|
|
418
|
+
const baseUrl = opts.apiUrl ?? (globalOpts.insecure ? "http://localhost:8080" : "https://api.viucraft.com");
|
|
419
|
+
const client = new ApiClient({
|
|
420
|
+
apiKey,
|
|
421
|
+
baseUrl,
|
|
422
|
+
debug: globalOpts.debug,
|
|
423
|
+
insecure: globalOpts.insecure
|
|
424
|
+
});
|
|
425
|
+
let authResponse;
|
|
426
|
+
try {
|
|
427
|
+
authResponse = await client.post("/api/v1/cli/auth");
|
|
428
|
+
} catch (err) {
|
|
429
|
+
console.error(chalk2.red(`Authentication failed: ${err.message}`));
|
|
430
|
+
process.exit(2);
|
|
431
|
+
}
|
|
432
|
+
let profileName = opts.profile ?? globalOpts.profile;
|
|
433
|
+
const isNonInteractive = !!opts.apiKey || !!globalOpts.apiKey;
|
|
434
|
+
if (!profileName) {
|
|
435
|
+
if (isNonInteractive) {
|
|
436
|
+
profileName = "default";
|
|
437
|
+
} else {
|
|
438
|
+
profileName = await input({
|
|
439
|
+
message: "Profile name:",
|
|
440
|
+
default: "default"
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
const config = loadConfig();
|
|
445
|
+
if (config.profiles[profileName] && !isNonInteractive) {
|
|
446
|
+
const overwrite = await confirm({
|
|
447
|
+
message: `Profile "${profileName}" already exists. Overwrite?`,
|
|
448
|
+
default: false
|
|
449
|
+
});
|
|
450
|
+
if (!overwrite) {
|
|
451
|
+
console.log("Aborted.");
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
config.profiles[profileName] = {
|
|
456
|
+
api_key: apiKey,
|
|
457
|
+
api_url: baseUrl,
|
|
458
|
+
environment,
|
|
459
|
+
customer_email: authResponse.customer.email,
|
|
460
|
+
customer_name: authResponse.customer.name,
|
|
461
|
+
plan: authResponse.customer.plan,
|
|
462
|
+
subdomain: authResponse.customer.subdomain,
|
|
463
|
+
insecure: baseUrl.startsWith("http://") || void 0
|
|
464
|
+
};
|
|
465
|
+
const isFirstProfile = Object.keys(config.profiles).length === 1;
|
|
466
|
+
if (isFirstProfile || !config.current_profile || isNonInteractive) {
|
|
467
|
+
config.current_profile = profileName;
|
|
468
|
+
} else {
|
|
469
|
+
const switchTo = await confirm({
|
|
470
|
+
message: `Switch to profile "${profileName}"?`,
|
|
471
|
+
default: true
|
|
472
|
+
});
|
|
473
|
+
if (switchTo) {
|
|
474
|
+
config.current_profile = profileName;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
saveConfig(config);
|
|
478
|
+
console.log();
|
|
479
|
+
console.log(chalk2.green("Authenticated successfully."));
|
|
480
|
+
console.log();
|
|
481
|
+
const envColor = environment === "live" ? chalk2.green : chalk2.yellow;
|
|
482
|
+
console.log(` Profile: ${chalk2.bold(profileName)}`);
|
|
483
|
+
console.log(` Environment: ${envColor(environment)}`);
|
|
484
|
+
console.log(` Account: ${authResponse.customer.name} (${authResponse.customer.email})`);
|
|
485
|
+
console.log(` Plan: ${authResponse.customer.plan}`);
|
|
486
|
+
console.log(` Subdomain: ${authResponse.customer.subdomain}`);
|
|
487
|
+
console.log(` API Key: ${maskApiKey(apiKey)}`);
|
|
488
|
+
});
|
|
489
|
+
auth.command("status").description("Show current authentication status").action(async () => {
|
|
490
|
+
const globalOpts = program2.opts();
|
|
491
|
+
let creds;
|
|
492
|
+
try {
|
|
493
|
+
creds = resolveCredentials({
|
|
494
|
+
apiKey: globalOpts.apiKey,
|
|
495
|
+
profile: globalOpts.profile
|
|
496
|
+
});
|
|
497
|
+
} catch (err) {
|
|
498
|
+
console.error(chalk2.red(err.message));
|
|
499
|
+
process.exit(2);
|
|
500
|
+
}
|
|
501
|
+
const client = new ApiClient({
|
|
502
|
+
apiKey: creds.apiKey,
|
|
503
|
+
baseUrl: creds.apiUrl,
|
|
504
|
+
debug: globalOpts.debug,
|
|
505
|
+
insecure: globalOpts.insecure ?? creds.insecure
|
|
506
|
+
});
|
|
507
|
+
let whoami;
|
|
508
|
+
try {
|
|
509
|
+
whoami = await client.get("/api/v1/cli/whoami");
|
|
510
|
+
} catch (err) {
|
|
511
|
+
console.error(chalk2.red(`Failed: ${err.message}`));
|
|
512
|
+
process.exit(2);
|
|
513
|
+
}
|
|
514
|
+
const environment = detectEnvironment(creds.apiKey);
|
|
515
|
+
const envColor = environment === "live" ? chalk2.green : chalk2.yellow;
|
|
516
|
+
if (globalOpts.output === "json") {
|
|
517
|
+
printJson({
|
|
518
|
+
profile: creds.profile,
|
|
519
|
+
source: creds.source,
|
|
520
|
+
environment,
|
|
521
|
+
customer: whoami.customer,
|
|
522
|
+
usage: whoami.usage,
|
|
523
|
+
api_key: maskApiKey(creds.apiKey),
|
|
524
|
+
config_path: getConfigPath()
|
|
525
|
+
});
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
console.log(chalk2.bold("Authentication Status"));
|
|
529
|
+
console.log();
|
|
530
|
+
console.log(` Profile: ${chalk2.bold(creds.profile)}`);
|
|
531
|
+
console.log(` Source: ${creds.source}`);
|
|
532
|
+
console.log(` Environment: ${envColor(environment)}`);
|
|
533
|
+
console.log(` API Key: ${maskApiKey(creds.apiKey)}`);
|
|
534
|
+
console.log();
|
|
535
|
+
console.log(` Account: ${whoami.customer.name} (${whoami.customer.email})`);
|
|
536
|
+
console.log(` Plan: ${whoami.customer.plan}`);
|
|
537
|
+
console.log(` Subdomain: ${whoami.customer.subdomain}`);
|
|
538
|
+
console.log();
|
|
539
|
+
console.log(chalk2.bold("Usage"));
|
|
540
|
+
console.log(` Images: ${formatPercent(whoami.usage.images)}`);
|
|
541
|
+
console.log(` Storage: ${formatPercent(whoami.usage.storage)}`);
|
|
542
|
+
console.log(` Transformations: ${formatPercent(whoami.usage.transformations)}`);
|
|
543
|
+
console.log();
|
|
544
|
+
console.log(chalk2.dim(`Config: ${getConfigPath()}`));
|
|
545
|
+
});
|
|
546
|
+
auth.command("switch").description("Switch active profile").argument("<profile>", "Profile name to switch to").action(async (profileName) => {
|
|
547
|
+
const config = loadConfig();
|
|
548
|
+
if (!config.profiles[profileName]) {
|
|
549
|
+
console.error(chalk2.red(`Profile "${profileName}" not found.`));
|
|
550
|
+
console.log();
|
|
551
|
+
const names = Object.keys(config.profiles);
|
|
552
|
+
if (names.length > 0) {
|
|
553
|
+
console.log("Available profiles:");
|
|
554
|
+
for (const name of names) {
|
|
555
|
+
const marker = name === config.current_profile ? chalk2.green(" (current)") : "";
|
|
556
|
+
console.log(` ${name}${marker}`);
|
|
557
|
+
}
|
|
558
|
+
} else {
|
|
559
|
+
console.log("No profiles configured. Run `viucraft auth login` to create one.");
|
|
560
|
+
}
|
|
561
|
+
process.exit(1);
|
|
562
|
+
}
|
|
563
|
+
config.current_profile = profileName;
|
|
564
|
+
saveConfig(config);
|
|
565
|
+
const profile = config.profiles[profileName];
|
|
566
|
+
const envColor = profile.environment === "live" ? chalk2.green : chalk2.yellow;
|
|
567
|
+
console.log(chalk2.green(`Switched to profile "${profileName}".`));
|
|
568
|
+
console.log();
|
|
569
|
+
console.log(` Environment: ${envColor(profile.environment)}`);
|
|
570
|
+
console.log(` Account: ${profile.customer_name} (${profile.customer_email})`);
|
|
571
|
+
console.log(` Plan: ${profile.plan}`);
|
|
572
|
+
console.log(` Subdomain: ${profile.subdomain}`);
|
|
573
|
+
});
|
|
574
|
+
auth.command("list").description("List all configured profiles").action(async () => {
|
|
575
|
+
const globalOpts = program2.opts();
|
|
576
|
+
const config = loadConfig();
|
|
577
|
+
const names = Object.keys(config.profiles);
|
|
578
|
+
if (names.length === 0) {
|
|
579
|
+
console.log("No profiles configured. Run `viucraft auth login` to create one.");
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (globalOpts.output === "json") {
|
|
583
|
+
const profiles = names.map((name) => ({
|
|
584
|
+
name,
|
|
585
|
+
current: name === config.current_profile,
|
|
586
|
+
...config.profiles[name],
|
|
587
|
+
api_key: maskApiKey(config.profiles[name].api_key)
|
|
588
|
+
}));
|
|
589
|
+
printJson(profiles);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const rows = names.map((name) => {
|
|
593
|
+
const p = config.profiles[name];
|
|
594
|
+
const marker = name === config.current_profile ? "*" : " ";
|
|
595
|
+
return {
|
|
596
|
+
marker,
|
|
597
|
+
name,
|
|
598
|
+
environment: p.environment,
|
|
599
|
+
email: p.customer_email,
|
|
600
|
+
plan: p.plan,
|
|
601
|
+
api_key: maskApiKey(p.api_key)
|
|
602
|
+
};
|
|
603
|
+
});
|
|
604
|
+
printTable(
|
|
605
|
+
[
|
|
606
|
+
{ header: " ", key: "marker", width: 1 },
|
|
607
|
+
{ header: "Profile", key: "name" },
|
|
608
|
+
{ header: "Env", key: "environment" },
|
|
609
|
+
{ header: "Email", key: "email" },
|
|
610
|
+
{ header: "Plan", key: "plan" },
|
|
611
|
+
{ header: "API Key", key: "api_key" }
|
|
612
|
+
],
|
|
613
|
+
rows
|
|
614
|
+
);
|
|
615
|
+
});
|
|
616
|
+
auth.command("logout").description("Remove a profile").option("--profile <name>", "Profile to remove (default: current)").action(async (opts) => {
|
|
617
|
+
const config = loadConfig();
|
|
618
|
+
const profileName = opts.profile ?? config.current_profile;
|
|
619
|
+
if (!profileName || !config.profiles[profileName]) {
|
|
620
|
+
console.error(
|
|
621
|
+
chalk2.red(
|
|
622
|
+
profileName ? `Profile "${profileName}" not found.` : "No profile specified and no current profile set."
|
|
623
|
+
)
|
|
624
|
+
);
|
|
625
|
+
process.exit(1);
|
|
626
|
+
}
|
|
627
|
+
const doRemove = await confirm({
|
|
628
|
+
message: `Remove profile "${profileName}"?`,
|
|
629
|
+
default: false
|
|
630
|
+
});
|
|
631
|
+
if (!doRemove) {
|
|
632
|
+
console.log("Aborted.");
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
delete config.profiles[profileName];
|
|
636
|
+
if (config.current_profile === profileName) {
|
|
637
|
+
const remaining = Object.keys(config.profiles);
|
|
638
|
+
config.current_profile = remaining[0] ?? "";
|
|
639
|
+
if (config.current_profile) {
|
|
640
|
+
console.log(chalk2.dim(`Switched to profile "${config.current_profile}".`));
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
saveConfig(config);
|
|
644
|
+
console.log(chalk2.green(`Profile "${profileName}" removed.`));
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
function formatPercent(value) {
|
|
648
|
+
if (value == null || isNaN(value)) return chalk2.dim("n/a");
|
|
649
|
+
const pct = Math.round(value);
|
|
650
|
+
const bar = progressBar(value);
|
|
651
|
+
if (pct >= 90) return chalk2.red(`${bar} ${pct}%`);
|
|
652
|
+
if (pct >= 75) return chalk2.yellow(`${bar} ${pct}%`);
|
|
653
|
+
return chalk2.green(`${bar} ${pct}%`);
|
|
654
|
+
}
|
|
655
|
+
function progressBar(percent, width = 20) {
|
|
656
|
+
const clamped = Math.max(0, Math.min(percent, 100));
|
|
657
|
+
const filled = Math.round(clamped / 100 * width);
|
|
658
|
+
const empty = width - filled;
|
|
659
|
+
return `[${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}]`;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// src/commands/images.ts
|
|
663
|
+
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
664
|
+
import { readFileSync as readFileSync2, statSync as statSync3 } from "fs";
|
|
665
|
+
import { basename as basename2 } from "path";
|
|
666
|
+
import chalk4 from "chalk";
|
|
667
|
+
import ora from "ora";
|
|
668
|
+
|
|
669
|
+
// src/lib/operations.ts
|
|
670
|
+
import chalk3 from "chalk";
|
|
671
|
+
import { select, input as input2, number, confirm as confirm2 } from "@inquirer/prompts";
|
|
672
|
+
var OPERATIONS_CATALOG = [
|
|
673
|
+
{
|
|
674
|
+
id: "resize_crop",
|
|
675
|
+
name: "Resize & Crop",
|
|
676
|
+
operations: [
|
|
677
|
+
{
|
|
678
|
+
id: "resize",
|
|
679
|
+
name: "Resize",
|
|
680
|
+
shortName: "resize",
|
|
681
|
+
description: "Resize image to specified dimensions",
|
|
682
|
+
params: [
|
|
683
|
+
{
|
|
684
|
+
name: "width",
|
|
685
|
+
label: "Width",
|
|
686
|
+
type: "slider",
|
|
687
|
+
default: 800,
|
|
688
|
+
min: 1,
|
|
689
|
+
max: 1e4,
|
|
690
|
+
step: 1,
|
|
691
|
+
unit: "px"
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
name: "height",
|
|
695
|
+
label: "Height",
|
|
696
|
+
type: "slider",
|
|
697
|
+
default: 600,
|
|
698
|
+
min: 1,
|
|
699
|
+
max: 1e4,
|
|
700
|
+
step: 1,
|
|
701
|
+
unit: "px"
|
|
702
|
+
},
|
|
703
|
+
{ name: "force", label: "Force exact dimensions", type: "toggle", default: false },
|
|
704
|
+
{
|
|
705
|
+
name: "scale",
|
|
706
|
+
label: "Scale Factor",
|
|
707
|
+
type: "slider",
|
|
708
|
+
default: 1,
|
|
709
|
+
min: 0.1,
|
|
710
|
+
max: 4,
|
|
711
|
+
step: 0.05,
|
|
712
|
+
note: "When set, overrides width/height"
|
|
713
|
+
}
|
|
714
|
+
]
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
id: "crop",
|
|
718
|
+
name: "Crop",
|
|
719
|
+
shortName: "crop",
|
|
720
|
+
description: "Crop image to a specific region",
|
|
721
|
+
params: [
|
|
722
|
+
{
|
|
723
|
+
name: "width",
|
|
724
|
+
label: "Width",
|
|
725
|
+
type: "slider",
|
|
726
|
+
default: 500,
|
|
727
|
+
min: 1,
|
|
728
|
+
max: 1e4,
|
|
729
|
+
step: 1,
|
|
730
|
+
unit: "px"
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
name: "height",
|
|
734
|
+
label: "Height",
|
|
735
|
+
type: "slider",
|
|
736
|
+
default: 500,
|
|
737
|
+
min: 1,
|
|
738
|
+
max: 1e4,
|
|
739
|
+
step: 1,
|
|
740
|
+
unit: "px"
|
|
741
|
+
},
|
|
742
|
+
{
|
|
743
|
+
name: "left",
|
|
744
|
+
label: "Left Offset",
|
|
745
|
+
type: "slider",
|
|
746
|
+
default: 0,
|
|
747
|
+
min: 0,
|
|
748
|
+
max: 1e4,
|
|
749
|
+
step: 1,
|
|
750
|
+
unit: "px"
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
name: "top",
|
|
754
|
+
label: "Top Offset",
|
|
755
|
+
type: "slider",
|
|
756
|
+
default: 0,
|
|
757
|
+
min: 0,
|
|
758
|
+
max: 1e4,
|
|
759
|
+
step: 1,
|
|
760
|
+
unit: "px"
|
|
761
|
+
}
|
|
762
|
+
]
|
|
763
|
+
},
|
|
764
|
+
{
|
|
765
|
+
id: "thumbnail",
|
|
766
|
+
name: "Thumbnail",
|
|
767
|
+
shortName: "thumb",
|
|
768
|
+
description: "Generate a thumbnail with smart sizing",
|
|
769
|
+
params: [
|
|
770
|
+
{
|
|
771
|
+
name: "width",
|
|
772
|
+
label: "Width",
|
|
773
|
+
type: "slider",
|
|
774
|
+
default: 300,
|
|
775
|
+
min: 50,
|
|
776
|
+
max: 2e3,
|
|
777
|
+
step: 1,
|
|
778
|
+
unit: "px"
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
name: "height",
|
|
782
|
+
label: "Height",
|
|
783
|
+
type: "slider",
|
|
784
|
+
default: 300,
|
|
785
|
+
min: 50,
|
|
786
|
+
max: 2e3,
|
|
787
|
+
step: 1,
|
|
788
|
+
unit: "px"
|
|
789
|
+
},
|
|
790
|
+
{
|
|
791
|
+
name: "crop",
|
|
792
|
+
label: "Crop Strategy",
|
|
793
|
+
type: "dropdown",
|
|
794
|
+
default: "centre",
|
|
795
|
+
options: [
|
|
796
|
+
{ value: "centre", label: "Centre" },
|
|
797
|
+
{ value: "entropy", label: "Entropy" },
|
|
798
|
+
{ value: "attention", label: "Attention" },
|
|
799
|
+
{ value: "none", label: "None (Letterbox)" }
|
|
800
|
+
]
|
|
801
|
+
},
|
|
802
|
+
{ name: "background", label: "Letterbox Color", type: "color", default: "#ffffff" }
|
|
803
|
+
]
|
|
804
|
+
},
|
|
805
|
+
{
|
|
806
|
+
id: "smartcrop",
|
|
807
|
+
name: "Smart Crop",
|
|
808
|
+
shortName: "scrop",
|
|
809
|
+
description: "Intelligently crop to the most interesting region",
|
|
810
|
+
params: [
|
|
811
|
+
{
|
|
812
|
+
name: "width",
|
|
813
|
+
label: "Width",
|
|
814
|
+
type: "slider",
|
|
815
|
+
default: 500,
|
|
816
|
+
min: 50,
|
|
817
|
+
max: 1e4,
|
|
818
|
+
step: 1,
|
|
819
|
+
unit: "px"
|
|
820
|
+
},
|
|
821
|
+
{
|
|
822
|
+
name: "height",
|
|
823
|
+
label: "Height",
|
|
824
|
+
type: "slider",
|
|
825
|
+
default: 500,
|
|
826
|
+
min: 50,
|
|
827
|
+
max: 1e4,
|
|
828
|
+
step: 1,
|
|
829
|
+
unit: "px"
|
|
830
|
+
},
|
|
831
|
+
{
|
|
832
|
+
name: "strategy",
|
|
833
|
+
label: "Strategy",
|
|
834
|
+
type: "dropdown",
|
|
835
|
+
default: "attention",
|
|
836
|
+
options: [
|
|
837
|
+
{ value: "attention", label: "Attention (AI Focus)" },
|
|
838
|
+
{ value: "entropy", label: "Entropy (Detail)" },
|
|
839
|
+
{ value: "centre", label: "Centre" },
|
|
840
|
+
{ value: "high", label: "High (Top)" },
|
|
841
|
+
{ value: "low", label: "Low (Bottom)" }
|
|
842
|
+
]
|
|
843
|
+
}
|
|
844
|
+
]
|
|
845
|
+
}
|
|
846
|
+
]
|
|
847
|
+
},
|
|
848
|
+
{
|
|
849
|
+
id: "color",
|
|
850
|
+
name: "Color Adjustments",
|
|
851
|
+
operations: [
|
|
852
|
+
{
|
|
853
|
+
id: "brightness",
|
|
854
|
+
name: "Brightness",
|
|
855
|
+
shortName: "bright",
|
|
856
|
+
description: "Adjust image brightness",
|
|
857
|
+
params: [
|
|
858
|
+
{
|
|
859
|
+
name: "factor",
|
|
860
|
+
label: "Brightness Factor",
|
|
861
|
+
type: "slider",
|
|
862
|
+
default: 1,
|
|
863
|
+
min: 0.1,
|
|
864
|
+
max: 5,
|
|
865
|
+
step: 0.05
|
|
866
|
+
}
|
|
867
|
+
]
|
|
868
|
+
},
|
|
869
|
+
{
|
|
870
|
+
id: "contrast",
|
|
871
|
+
name: "Contrast",
|
|
872
|
+
shortName: "con",
|
|
873
|
+
description: "Adjust image contrast",
|
|
874
|
+
params: [
|
|
875
|
+
{
|
|
876
|
+
name: "factor",
|
|
877
|
+
label: "Contrast Factor",
|
|
878
|
+
type: "slider",
|
|
879
|
+
default: 1,
|
|
880
|
+
min: 0.1,
|
|
881
|
+
max: 5,
|
|
882
|
+
step: 0.05
|
|
883
|
+
}
|
|
884
|
+
]
|
|
885
|
+
},
|
|
886
|
+
{
|
|
887
|
+
id: "gamma",
|
|
888
|
+
name: "Gamma",
|
|
889
|
+
shortName: "gam",
|
|
890
|
+
description: "Adjust gamma correction level",
|
|
891
|
+
params: [
|
|
892
|
+
{
|
|
893
|
+
name: "exponent",
|
|
894
|
+
label: "Gamma",
|
|
895
|
+
type: "slider",
|
|
896
|
+
default: 2.2,
|
|
897
|
+
min: 0.1,
|
|
898
|
+
max: 10,
|
|
899
|
+
step: 0.1
|
|
900
|
+
}
|
|
901
|
+
]
|
|
902
|
+
},
|
|
903
|
+
{
|
|
904
|
+
id: "saturation",
|
|
905
|
+
name: "Saturation",
|
|
906
|
+
shortName: "sat",
|
|
907
|
+
description: "Adjust color saturation intensity",
|
|
908
|
+
params: [
|
|
909
|
+
{
|
|
910
|
+
name: "factor",
|
|
911
|
+
label: "Saturation Factor",
|
|
912
|
+
type: "slider",
|
|
913
|
+
default: 1,
|
|
914
|
+
min: 0,
|
|
915
|
+
max: 3,
|
|
916
|
+
step: 0.05
|
|
917
|
+
}
|
|
918
|
+
]
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
id: "hue",
|
|
922
|
+
name: "Hue Rotate",
|
|
923
|
+
shortName: "hue",
|
|
924
|
+
description: "Rotate the color hue of the image",
|
|
925
|
+
params: [
|
|
926
|
+
{
|
|
927
|
+
name: "angle",
|
|
928
|
+
label: "Hue Angle",
|
|
929
|
+
type: "slider",
|
|
930
|
+
default: 0,
|
|
931
|
+
min: 0,
|
|
932
|
+
max: 360,
|
|
933
|
+
step: 1,
|
|
934
|
+
unit: "deg"
|
|
935
|
+
}
|
|
936
|
+
]
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
id: "colorize",
|
|
940
|
+
name: "Colorize",
|
|
941
|
+
shortName: "col",
|
|
942
|
+
description: "Apply a color overlay with adjustable blend",
|
|
943
|
+
params: [
|
|
944
|
+
{ name: "color", label: "Color", type: "color", default: "#ff6600" },
|
|
945
|
+
{
|
|
946
|
+
name: "amount",
|
|
947
|
+
label: "Blend Amount",
|
|
948
|
+
type: "slider",
|
|
949
|
+
default: 0.5,
|
|
950
|
+
min: 0,
|
|
951
|
+
max: 1,
|
|
952
|
+
step: 0.05
|
|
953
|
+
}
|
|
954
|
+
]
|
|
955
|
+
},
|
|
956
|
+
{
|
|
957
|
+
id: "tint",
|
|
958
|
+
name: "Tint",
|
|
959
|
+
shortName: "tint",
|
|
960
|
+
description: "Map shadows and highlights to custom colors",
|
|
961
|
+
params: [
|
|
962
|
+
{ name: "highlight", label: "Highlight Color", type: "color", default: "#ffffff" },
|
|
963
|
+
{ name: "shadow", label: "Shadow Color", type: "color", default: "#000066" }
|
|
964
|
+
]
|
|
965
|
+
},
|
|
966
|
+
{
|
|
967
|
+
id: "invert",
|
|
968
|
+
name: "Invert",
|
|
969
|
+
shortName: "inv",
|
|
970
|
+
description: "Invert all image colors",
|
|
971
|
+
params: [{ name: "enabled", label: "Enabled", type: "toggle", default: true }]
|
|
972
|
+
},
|
|
973
|
+
{
|
|
974
|
+
id: "grayscale",
|
|
975
|
+
name: "Grayscale",
|
|
976
|
+
shortName: "gray",
|
|
977
|
+
description: "Convert image to grayscale",
|
|
978
|
+
params: [{ name: "enabled", label: "Enabled", type: "toggle", default: true }]
|
|
979
|
+
},
|
|
980
|
+
{
|
|
981
|
+
id: "sepia",
|
|
982
|
+
name: "Sepia",
|
|
983
|
+
shortName: "sepia",
|
|
984
|
+
description: "Apply a warm sepia tone effect",
|
|
985
|
+
params: [{ name: "enabled", label: "Enabled", type: "toggle", default: true }]
|
|
986
|
+
},
|
|
987
|
+
{
|
|
988
|
+
id: "autoenhance",
|
|
989
|
+
name: "Auto Enhance",
|
|
990
|
+
shortName: "auto",
|
|
991
|
+
description: "Automatically improve brightness, contrast, and sharpness",
|
|
992
|
+
params: [
|
|
993
|
+
{
|
|
994
|
+
name: "strength",
|
|
995
|
+
label: "Strength",
|
|
996
|
+
type: "slider",
|
|
997
|
+
default: 0.5,
|
|
998
|
+
min: 0,
|
|
999
|
+
max: 1,
|
|
1000
|
+
step: 0.05
|
|
1001
|
+
}
|
|
1002
|
+
]
|
|
1003
|
+
}
|
|
1004
|
+
]
|
|
1005
|
+
},
|
|
1006
|
+
{
|
|
1007
|
+
id: "effects",
|
|
1008
|
+
name: "Effects",
|
|
1009
|
+
operations: [
|
|
1010
|
+
{
|
|
1011
|
+
id: "blur",
|
|
1012
|
+
name: "Blur",
|
|
1013
|
+
shortName: "blur",
|
|
1014
|
+
description: "Apply Gaussian blur to the image",
|
|
1015
|
+
params: [
|
|
1016
|
+
{
|
|
1017
|
+
name: "sigma",
|
|
1018
|
+
label: "Blur Intensity",
|
|
1019
|
+
type: "slider",
|
|
1020
|
+
default: 5,
|
|
1021
|
+
min: 0.3,
|
|
1022
|
+
max: 100,
|
|
1023
|
+
step: 0.1
|
|
1024
|
+
}
|
|
1025
|
+
]
|
|
1026
|
+
},
|
|
1027
|
+
{
|
|
1028
|
+
id: "sharpen",
|
|
1029
|
+
name: "Sharpen",
|
|
1030
|
+
shortName: "sharp",
|
|
1031
|
+
description: "Sharpen image details and edges",
|
|
1032
|
+
params: [
|
|
1033
|
+
{
|
|
1034
|
+
name: "sigma",
|
|
1035
|
+
label: "Sharpen Radius",
|
|
1036
|
+
type: "slider",
|
|
1037
|
+
default: 1,
|
|
1038
|
+
min: 0.1,
|
|
1039
|
+
max: 10,
|
|
1040
|
+
step: 0.1
|
|
1041
|
+
},
|
|
1042
|
+
{
|
|
1043
|
+
name: "x1",
|
|
1044
|
+
label: "Flat Area Threshold",
|
|
1045
|
+
type: "slider",
|
|
1046
|
+
default: 2,
|
|
1047
|
+
min: 0,
|
|
1048
|
+
max: 10,
|
|
1049
|
+
step: 0.1
|
|
1050
|
+
},
|
|
1051
|
+
{
|
|
1052
|
+
name: "y2",
|
|
1053
|
+
label: "Jagged Area Level",
|
|
1054
|
+
type: "slider",
|
|
1055
|
+
default: 10,
|
|
1056
|
+
min: 0,
|
|
1057
|
+
max: 100,
|
|
1058
|
+
step: 0.5
|
|
1059
|
+
},
|
|
1060
|
+
{
|
|
1061
|
+
name: "y3",
|
|
1062
|
+
label: "Max Brightening",
|
|
1063
|
+
type: "slider",
|
|
1064
|
+
default: 20,
|
|
1065
|
+
min: 0,
|
|
1066
|
+
max: 100,
|
|
1067
|
+
step: 0.5
|
|
1068
|
+
}
|
|
1069
|
+
]
|
|
1070
|
+
},
|
|
1071
|
+
{
|
|
1072
|
+
id: "edge",
|
|
1073
|
+
name: "Edge Detect",
|
|
1074
|
+
shortName: "edge",
|
|
1075
|
+
description: "Detect and highlight edges in the image",
|
|
1076
|
+
params: [{ name: "enabled", label: "Enabled", type: "toggle", default: true }]
|
|
1077
|
+
},
|
|
1078
|
+
{
|
|
1079
|
+
id: "emboss",
|
|
1080
|
+
name: "Emboss",
|
|
1081
|
+
shortName: "emb",
|
|
1082
|
+
description: "Apply an emboss relief effect",
|
|
1083
|
+
params: [{ name: "enabled", label: "Enabled", type: "toggle", default: true }]
|
|
1084
|
+
},
|
|
1085
|
+
{
|
|
1086
|
+
id: "median",
|
|
1087
|
+
name: "Median Filter",
|
|
1088
|
+
shortName: "med",
|
|
1089
|
+
description: "Apply median filter for noise reduction",
|
|
1090
|
+
params: [
|
|
1091
|
+
{
|
|
1092
|
+
name: "size",
|
|
1093
|
+
label: "Filter Size",
|
|
1094
|
+
type: "slider",
|
|
1095
|
+
default: 3,
|
|
1096
|
+
min: 3,
|
|
1097
|
+
max: 15,
|
|
1098
|
+
step: 2,
|
|
1099
|
+
note: "Must be odd"
|
|
1100
|
+
}
|
|
1101
|
+
]
|
|
1102
|
+
},
|
|
1103
|
+
{
|
|
1104
|
+
id: "vignette",
|
|
1105
|
+
name: "Vignette",
|
|
1106
|
+
shortName: "vig",
|
|
1107
|
+
description: "Apply a dark vignette around the edges",
|
|
1108
|
+
params: [
|
|
1109
|
+
{
|
|
1110
|
+
name: "scale",
|
|
1111
|
+
label: "Inner Radius",
|
|
1112
|
+
type: "slider",
|
|
1113
|
+
default: 0.3,
|
|
1114
|
+
min: 0,
|
|
1115
|
+
max: 1,
|
|
1116
|
+
step: 0.05
|
|
1117
|
+
},
|
|
1118
|
+
{
|
|
1119
|
+
name: "opacity",
|
|
1120
|
+
label: "Darkness",
|
|
1121
|
+
type: "slider",
|
|
1122
|
+
default: 0.7,
|
|
1123
|
+
min: 0,
|
|
1124
|
+
max: 1,
|
|
1125
|
+
step: 0.05
|
|
1126
|
+
},
|
|
1127
|
+
{ name: "color", label: "Edge Color", type: "color", default: "#000000" }
|
|
1128
|
+
]
|
|
1129
|
+
},
|
|
1130
|
+
{
|
|
1131
|
+
id: "noise",
|
|
1132
|
+
name: "Noise",
|
|
1133
|
+
shortName: "noise",
|
|
1134
|
+
description: "Add random noise grain to the image",
|
|
1135
|
+
params: [
|
|
1136
|
+
{
|
|
1137
|
+
name: "amount",
|
|
1138
|
+
label: "Noise Amount",
|
|
1139
|
+
type: "slider",
|
|
1140
|
+
default: 0.2,
|
|
1141
|
+
min: 0.01,
|
|
1142
|
+
max: 1,
|
|
1143
|
+
step: 0.01
|
|
1144
|
+
},
|
|
1145
|
+
{
|
|
1146
|
+
name: "type",
|
|
1147
|
+
label: "Noise Type",
|
|
1148
|
+
type: "dropdown",
|
|
1149
|
+
default: "gaussian",
|
|
1150
|
+
options: [
|
|
1151
|
+
{ value: "gaussian", label: "Gaussian" },
|
|
1152
|
+
{ value: "salt", label: "Salt (White)" },
|
|
1153
|
+
{ value: "pepper", label: "Pepper (Black)" },
|
|
1154
|
+
{ value: "salt-pepper", label: "Salt & Pepper" }
|
|
1155
|
+
]
|
|
1156
|
+
}
|
|
1157
|
+
]
|
|
1158
|
+
},
|
|
1159
|
+
{
|
|
1160
|
+
id: "pixelate",
|
|
1161
|
+
name: "Pixelate",
|
|
1162
|
+
shortName: "pix",
|
|
1163
|
+
description: "Apply a pixelation mosaic effect",
|
|
1164
|
+
params: [
|
|
1165
|
+
{
|
|
1166
|
+
name: "size",
|
|
1167
|
+
label: "Pixel Size",
|
|
1168
|
+
type: "slider",
|
|
1169
|
+
default: 10,
|
|
1170
|
+
min: 2,
|
|
1171
|
+
max: 100,
|
|
1172
|
+
step: 1,
|
|
1173
|
+
unit: "px"
|
|
1174
|
+
}
|
|
1175
|
+
]
|
|
1176
|
+
}
|
|
1177
|
+
]
|
|
1178
|
+
},
|
|
1179
|
+
{
|
|
1180
|
+
id: "transform",
|
|
1181
|
+
name: "Transform",
|
|
1182
|
+
operations: [
|
|
1183
|
+
{
|
|
1184
|
+
id: "rotate",
|
|
1185
|
+
name: "Rotate",
|
|
1186
|
+
shortName: "rotate",
|
|
1187
|
+
description: "Rotate the image by a specified angle",
|
|
1188
|
+
params: [
|
|
1189
|
+
{
|
|
1190
|
+
name: "angle",
|
|
1191
|
+
label: "Rotation Angle",
|
|
1192
|
+
type: "slider",
|
|
1193
|
+
default: 90,
|
|
1194
|
+
min: 0,
|
|
1195
|
+
max: 360,
|
|
1196
|
+
step: 1,
|
|
1197
|
+
unit: "deg"
|
|
1198
|
+
},
|
|
1199
|
+
{ name: "background", label: "Background Color", type: "color", default: "#000000" }
|
|
1200
|
+
]
|
|
1201
|
+
},
|
|
1202
|
+
{
|
|
1203
|
+
id: "flip",
|
|
1204
|
+
name: "Flip",
|
|
1205
|
+
shortName: "flip",
|
|
1206
|
+
description: "Flip the image horizontally, vertically, or both",
|
|
1207
|
+
params: [
|
|
1208
|
+
{
|
|
1209
|
+
name: "direction",
|
|
1210
|
+
label: "Direction",
|
|
1211
|
+
type: "dropdown",
|
|
1212
|
+
default: "horizontal",
|
|
1213
|
+
options: [
|
|
1214
|
+
{ value: "horizontal", label: "Horizontal" },
|
|
1215
|
+
{ value: "vertical", label: "Vertical" },
|
|
1216
|
+
{ value: "both", label: "Both" }
|
|
1217
|
+
]
|
|
1218
|
+
}
|
|
1219
|
+
]
|
|
1220
|
+
}
|
|
1221
|
+
]
|
|
1222
|
+
},
|
|
1223
|
+
{
|
|
1224
|
+
id: "decoration",
|
|
1225
|
+
name: "Decoration",
|
|
1226
|
+
operations: [
|
|
1227
|
+
{
|
|
1228
|
+
id: "border",
|
|
1229
|
+
name: "Border",
|
|
1230
|
+
shortName: "brd",
|
|
1231
|
+
description: "Add a border with optional rounded corners and shadow",
|
|
1232
|
+
params: [
|
|
1233
|
+
{
|
|
1234
|
+
name: "width",
|
|
1235
|
+
label: "Border Width",
|
|
1236
|
+
type: "slider",
|
|
1237
|
+
default: 10,
|
|
1238
|
+
min: 1,
|
|
1239
|
+
max: 200,
|
|
1240
|
+
step: 1,
|
|
1241
|
+
unit: "px"
|
|
1242
|
+
},
|
|
1243
|
+
{ name: "color", label: "Border Color", type: "color", default: "#000000" },
|
|
1244
|
+
{
|
|
1245
|
+
name: "radius",
|
|
1246
|
+
label: "Corner Radius",
|
|
1247
|
+
type: "slider",
|
|
1248
|
+
default: 0,
|
|
1249
|
+
min: 0,
|
|
1250
|
+
max: 500,
|
|
1251
|
+
step: 1,
|
|
1252
|
+
unit: "px"
|
|
1253
|
+
},
|
|
1254
|
+
{
|
|
1255
|
+
name: "shadow_blur",
|
|
1256
|
+
label: "Shadow Blur",
|
|
1257
|
+
type: "slider",
|
|
1258
|
+
default: 0,
|
|
1259
|
+
min: 0,
|
|
1260
|
+
max: 50,
|
|
1261
|
+
step: 1
|
|
1262
|
+
},
|
|
1263
|
+
{ name: "shadow_color", label: "Shadow Color", type: "color", default: "#000000" },
|
|
1264
|
+
{
|
|
1265
|
+
name: "shadow_x",
|
|
1266
|
+
label: "Shadow X Offset",
|
|
1267
|
+
type: "slider",
|
|
1268
|
+
default: 4,
|
|
1269
|
+
min: -50,
|
|
1270
|
+
max: 50,
|
|
1271
|
+
step: 1,
|
|
1272
|
+
unit: "px"
|
|
1273
|
+
},
|
|
1274
|
+
{
|
|
1275
|
+
name: "shadow_y",
|
|
1276
|
+
label: "Shadow Y Offset",
|
|
1277
|
+
type: "slider",
|
|
1278
|
+
default: 4,
|
|
1279
|
+
min: -50,
|
|
1280
|
+
max: 50,
|
|
1281
|
+
step: 1,
|
|
1282
|
+
unit: "px"
|
|
1283
|
+
}
|
|
1284
|
+
]
|
|
1285
|
+
},
|
|
1286
|
+
{
|
|
1287
|
+
id: "watermark",
|
|
1288
|
+
name: "Text Watermark",
|
|
1289
|
+
shortName: "wmt",
|
|
1290
|
+
description: "Add a text watermark overlay",
|
|
1291
|
+
params: [
|
|
1292
|
+
{ name: "text", label: "Watermark Text", type: "text", default: "Sample" },
|
|
1293
|
+
{ name: "tiled", label: "Tiled (Repeat Pattern)", type: "toggle", default: false },
|
|
1294
|
+
{
|
|
1295
|
+
name: "opacity",
|
|
1296
|
+
label: "Opacity",
|
|
1297
|
+
type: "slider",
|
|
1298
|
+
default: 0.5,
|
|
1299
|
+
min: 0,
|
|
1300
|
+
max: 1,
|
|
1301
|
+
step: 0.05
|
|
1302
|
+
},
|
|
1303
|
+
{
|
|
1304
|
+
name: "position",
|
|
1305
|
+
label: "Position",
|
|
1306
|
+
type: "dropdown",
|
|
1307
|
+
default: "bottom-right",
|
|
1308
|
+
options: [
|
|
1309
|
+
{ value: "center", label: "Center" },
|
|
1310
|
+
{ value: "top-left", label: "Top Left" },
|
|
1311
|
+
{ value: "top-center", label: "Top Center" },
|
|
1312
|
+
{ value: "top-right", label: "Top Right" },
|
|
1313
|
+
{ value: "center-left", label: "Center Left" },
|
|
1314
|
+
{ value: "center-right", label: "Center Right" },
|
|
1315
|
+
{ value: "bottom-left", label: "Bottom Left" },
|
|
1316
|
+
{ value: "bottom-center", label: "Bottom Center" },
|
|
1317
|
+
{ value: "bottom-right", label: "Bottom Right" }
|
|
1318
|
+
]
|
|
1319
|
+
},
|
|
1320
|
+
{
|
|
1321
|
+
name: "size",
|
|
1322
|
+
label: "Font Size",
|
|
1323
|
+
type: "slider",
|
|
1324
|
+
default: 48,
|
|
1325
|
+
min: 8,
|
|
1326
|
+
max: 500,
|
|
1327
|
+
step: 1,
|
|
1328
|
+
unit: "px"
|
|
1329
|
+
},
|
|
1330
|
+
{ name: "color", label: "Text Color", type: "color", default: "#ffffff" },
|
|
1331
|
+
{
|
|
1332
|
+
name: "rotation",
|
|
1333
|
+
label: "Rotation",
|
|
1334
|
+
type: "slider",
|
|
1335
|
+
default: 0,
|
|
1336
|
+
min: 0,
|
|
1337
|
+
max: 360,
|
|
1338
|
+
step: 1,
|
|
1339
|
+
unit: "deg"
|
|
1340
|
+
},
|
|
1341
|
+
{
|
|
1342
|
+
name: "font",
|
|
1343
|
+
label: "Font",
|
|
1344
|
+
type: "dropdown",
|
|
1345
|
+
default: "sans-serif",
|
|
1346
|
+
options: [
|
|
1347
|
+
{ value: "sans-serif", label: "Sans Serif" },
|
|
1348
|
+
{ value: "serif", label: "Serif" },
|
|
1349
|
+
{ value: "monospace", label: "Monospace" }
|
|
1350
|
+
]
|
|
1351
|
+
},
|
|
1352
|
+
{
|
|
1353
|
+
name: "x",
|
|
1354
|
+
label: "X Offset",
|
|
1355
|
+
type: "slider",
|
|
1356
|
+
default: 0,
|
|
1357
|
+
min: -500,
|
|
1358
|
+
max: 500,
|
|
1359
|
+
step: 1,
|
|
1360
|
+
unit: "px"
|
|
1361
|
+
},
|
|
1362
|
+
{
|
|
1363
|
+
name: "y",
|
|
1364
|
+
label: "Y Offset",
|
|
1365
|
+
type: "slider",
|
|
1366
|
+
default: 0,
|
|
1367
|
+
min: -500,
|
|
1368
|
+
max: 500,
|
|
1369
|
+
step: 1,
|
|
1370
|
+
unit: "px"
|
|
1371
|
+
},
|
|
1372
|
+
{ name: "shadowcolor", label: "Shadow Color", type: "color", default: "#000000" },
|
|
1373
|
+
{
|
|
1374
|
+
name: "shadowx",
|
|
1375
|
+
label: "Shadow X",
|
|
1376
|
+
type: "slider",
|
|
1377
|
+
default: 0,
|
|
1378
|
+
min: -20,
|
|
1379
|
+
max: 20,
|
|
1380
|
+
step: 1,
|
|
1381
|
+
unit: "px"
|
|
1382
|
+
},
|
|
1383
|
+
{
|
|
1384
|
+
name: "shadowy",
|
|
1385
|
+
label: "Shadow Y",
|
|
1386
|
+
type: "slider",
|
|
1387
|
+
default: 0,
|
|
1388
|
+
min: -20,
|
|
1389
|
+
max: 20,
|
|
1390
|
+
step: 1,
|
|
1391
|
+
unit: "px"
|
|
1392
|
+
},
|
|
1393
|
+
{
|
|
1394
|
+
name: "spacing",
|
|
1395
|
+
label: "Tile Spacing",
|
|
1396
|
+
type: "slider",
|
|
1397
|
+
default: 20,
|
|
1398
|
+
min: 0,
|
|
1399
|
+
max: 200,
|
|
1400
|
+
step: 1,
|
|
1401
|
+
unit: "px",
|
|
1402
|
+
note: "Only applies when Tiled is enabled"
|
|
1403
|
+
}
|
|
1404
|
+
]
|
|
1405
|
+
},
|
|
1406
|
+
{
|
|
1407
|
+
id: "svgoverlay",
|
|
1408
|
+
name: "SVG Overlay",
|
|
1409
|
+
shortName: "svg",
|
|
1410
|
+
description: "Overlay an SVG graphic on the image",
|
|
1411
|
+
params: [
|
|
1412
|
+
{ name: "data", label: "SVG Data (base64)", type: "text", default: "" },
|
|
1413
|
+
{
|
|
1414
|
+
name: "x",
|
|
1415
|
+
label: "X Offset",
|
|
1416
|
+
type: "slider",
|
|
1417
|
+
default: 0,
|
|
1418
|
+
min: 0,
|
|
1419
|
+
max: 4e3,
|
|
1420
|
+
step: 1,
|
|
1421
|
+
unit: "px"
|
|
1422
|
+
},
|
|
1423
|
+
{
|
|
1424
|
+
name: "y",
|
|
1425
|
+
label: "Y Offset",
|
|
1426
|
+
type: "slider",
|
|
1427
|
+
default: 0,
|
|
1428
|
+
min: 0,
|
|
1429
|
+
max: 4e3,
|
|
1430
|
+
step: 1,
|
|
1431
|
+
unit: "px"
|
|
1432
|
+
},
|
|
1433
|
+
{
|
|
1434
|
+
name: "scale",
|
|
1435
|
+
label: "Scale",
|
|
1436
|
+
type: "slider",
|
|
1437
|
+
default: 1,
|
|
1438
|
+
min: 0.1,
|
|
1439
|
+
max: 5,
|
|
1440
|
+
step: 0.1
|
|
1441
|
+
},
|
|
1442
|
+
{
|
|
1443
|
+
name: "opacity",
|
|
1444
|
+
label: "Opacity",
|
|
1445
|
+
type: "slider",
|
|
1446
|
+
default: 100,
|
|
1447
|
+
min: 0,
|
|
1448
|
+
max: 100,
|
|
1449
|
+
step: 1,
|
|
1450
|
+
unit: "%"
|
|
1451
|
+
}
|
|
1452
|
+
]
|
|
1453
|
+
}
|
|
1454
|
+
]
|
|
1455
|
+
},
|
|
1456
|
+
{
|
|
1457
|
+
id: "output",
|
|
1458
|
+
name: "Output",
|
|
1459
|
+
operations: [
|
|
1460
|
+
{
|
|
1461
|
+
id: "quality",
|
|
1462
|
+
name: "Quality",
|
|
1463
|
+
shortName: "q",
|
|
1464
|
+
description: "Set output image quality (lower = smaller file)",
|
|
1465
|
+
params: [
|
|
1466
|
+
{
|
|
1467
|
+
name: "value",
|
|
1468
|
+
label: "Quality",
|
|
1469
|
+
type: "slider",
|
|
1470
|
+
default: 85,
|
|
1471
|
+
min: 1,
|
|
1472
|
+
max: 100,
|
|
1473
|
+
step: 1,
|
|
1474
|
+
unit: "%"
|
|
1475
|
+
}
|
|
1476
|
+
]
|
|
1477
|
+
},
|
|
1478
|
+
{
|
|
1479
|
+
id: "strip",
|
|
1480
|
+
name: "Strip Metadata",
|
|
1481
|
+
shortName: "strip",
|
|
1482
|
+
description: "Remove EXIF and other metadata from the output",
|
|
1483
|
+
params: [{ name: "enabled", label: "Strip Metadata", type: "toggle", default: true }]
|
|
1484
|
+
},
|
|
1485
|
+
{
|
|
1486
|
+
id: "placeholder",
|
|
1487
|
+
name: "Placeholder (LQIP)",
|
|
1488
|
+
shortName: "lqip",
|
|
1489
|
+
description: "Generate a tiny blurred placeholder for progressive loading",
|
|
1490
|
+
params: [
|
|
1491
|
+
{
|
|
1492
|
+
name: "size",
|
|
1493
|
+
label: "Size",
|
|
1494
|
+
type: "slider",
|
|
1495
|
+
default: 32,
|
|
1496
|
+
min: 8,
|
|
1497
|
+
max: 128,
|
|
1498
|
+
step: 1,
|
|
1499
|
+
unit: "px"
|
|
1500
|
+
}
|
|
1501
|
+
]
|
|
1502
|
+
},
|
|
1503
|
+
{
|
|
1504
|
+
id: "responsive",
|
|
1505
|
+
name: "Responsive Srcset",
|
|
1506
|
+
shortName: "resp",
|
|
1507
|
+
description: "Generate responsive image variants with srcset",
|
|
1508
|
+
params: [
|
|
1509
|
+
{ name: "widths", label: "Breakpoint Widths", type: "text", default: "320,640,1024" },
|
|
1510
|
+
{
|
|
1511
|
+
name: "format",
|
|
1512
|
+
label: "Format",
|
|
1513
|
+
type: "dropdown",
|
|
1514
|
+
default: "webp",
|
|
1515
|
+
options: [
|
|
1516
|
+
{ value: "webp", label: "WebP" },
|
|
1517
|
+
{ value: "jpg", label: "JPEG" },
|
|
1518
|
+
{ value: "png", label: "PNG" }
|
|
1519
|
+
]
|
|
1520
|
+
}
|
|
1521
|
+
]
|
|
1522
|
+
}
|
|
1523
|
+
]
|
|
1524
|
+
}
|
|
1525
|
+
];
|
|
1526
|
+
function isToggleOnly(op) {
|
|
1527
|
+
return op.params.length === 1 && op.params[0].type === "toggle";
|
|
1528
|
+
}
|
|
1529
|
+
function buildSyntaxString(op) {
|
|
1530
|
+
if (isToggleOnly(op)) return op.shortName;
|
|
1531
|
+
const paramParts = op.params.filter((p) => p.type !== "toggle").map((p) => `<${p.name}>`);
|
|
1532
|
+
if (paramParts.length === 0) return op.shortName;
|
|
1533
|
+
return `${op.shortName}-${paramParts.join("-")}`;
|
|
1534
|
+
}
|
|
1535
|
+
function formatOpsSegment(op, values) {
|
|
1536
|
+
if (isToggleOnly(op)) return op.shortName;
|
|
1537
|
+
const parts = [op.shortName];
|
|
1538
|
+
const nonToggleParams = op.params.filter((p) => p.type !== "toggle");
|
|
1539
|
+
let lastNonDefault = -1;
|
|
1540
|
+
for (let i = nonToggleParams.length - 1; i >= 0; i--) {
|
|
1541
|
+
const p = nonToggleParams[i];
|
|
1542
|
+
const val = values[p.name] ?? p.default;
|
|
1543
|
+
const normalized = p.type === "color" ? String(val).replace(/^#/, "") : val;
|
|
1544
|
+
const defaultNormalized = p.type === "color" ? String(p.default).replace(/^#/, "") : p.default;
|
|
1545
|
+
if (normalized !== defaultNormalized) {
|
|
1546
|
+
lastNonDefault = i;
|
|
1547
|
+
break;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
const includeUpTo = Math.max(0, lastNonDefault);
|
|
1551
|
+
for (let i = 0; i <= includeUpTo; i++) {
|
|
1552
|
+
const p = nonToggleParams[i];
|
|
1553
|
+
const val = values[p.name] ?? p.default;
|
|
1554
|
+
parts.push(p.type === "color" ? String(val).replace(/^#/, "") : val);
|
|
1555
|
+
}
|
|
1556
|
+
return parts.join("-");
|
|
1557
|
+
}
|
|
1558
|
+
function renderOperationsList(outputFormat) {
|
|
1559
|
+
if (outputFormat === "json") {
|
|
1560
|
+
printJson(OPERATIONS_CATALOG);
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
for (const category of OPERATIONS_CATALOG) {
|
|
1564
|
+
console.log();
|
|
1565
|
+
console.log(chalk3.bold.underline(category.name));
|
|
1566
|
+
console.log();
|
|
1567
|
+
const nameWidth = 18;
|
|
1568
|
+
const shortWidth = 10;
|
|
1569
|
+
const syntaxWidth = 30;
|
|
1570
|
+
console.log(
|
|
1571
|
+
` ${chalk3.dim(pad("Name", nameWidth))}${chalk3.dim(pad("Short", shortWidth))}${chalk3.dim(pad("Syntax", syntaxWidth))}${chalk3.dim("Description")}`
|
|
1572
|
+
);
|
|
1573
|
+
console.log(
|
|
1574
|
+
` ${chalk3.dim("\u2500".repeat(nameWidth))}${chalk3.dim("\u2500".repeat(shortWidth))}${chalk3.dim("\u2500".repeat(syntaxWidth))}${chalk3.dim("\u2500".repeat(30))}`
|
|
1575
|
+
);
|
|
1576
|
+
for (const op of category.operations) {
|
|
1577
|
+
const syntax = buildSyntaxString(op);
|
|
1578
|
+
console.log(
|
|
1579
|
+
` ${pad(op.name, nameWidth)}${chalk3.cyan(pad(op.shortName, shortWidth))}${chalk3.yellow(pad(syntax, syntaxWidth))}${chalk3.dim(op.description)}`
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
console.log();
|
|
1584
|
+
console.log(chalk3.dim("Chain operations with /: resize-800-600/sharp-1.2/q-85"));
|
|
1585
|
+
console.log(chalk3.dim("Use --interactive (-i) to build operations step by step."));
|
|
1586
|
+
console.log();
|
|
1587
|
+
}
|
|
1588
|
+
async function buildOpsInteractively() {
|
|
1589
|
+
const segments = [];
|
|
1590
|
+
for (; ; ) {
|
|
1591
|
+
if (segments.length > 0) {
|
|
1592
|
+
console.log();
|
|
1593
|
+
console.log(
|
|
1594
|
+
` ${chalk3.dim("Pipeline:")} ${segments.map((s) => chalk3.cyan(s)).join(chalk3.dim(" / "))}`
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
const categoryId = await select({
|
|
1598
|
+
message: "Select a category:",
|
|
1599
|
+
choices: OPERATIONS_CATALOG.map((cat) => ({
|
|
1600
|
+
value: cat.id,
|
|
1601
|
+
name: cat.name,
|
|
1602
|
+
description: `${cat.operations.length} operations`
|
|
1603
|
+
}))
|
|
1604
|
+
});
|
|
1605
|
+
const category = OPERATIONS_CATALOG.find((c) => c.id === categoryId);
|
|
1606
|
+
const opId = await select({
|
|
1607
|
+
message: `Select an operation:`,
|
|
1608
|
+
choices: category.operations.map((op2) => ({
|
|
1609
|
+
value: op2.id,
|
|
1610
|
+
name: `${op2.name} (${op2.shortName})`,
|
|
1611
|
+
description: op2.description
|
|
1612
|
+
}))
|
|
1613
|
+
});
|
|
1614
|
+
const op = category.operations.find((o) => o.id === opId);
|
|
1615
|
+
if (isToggleOnly(op)) {
|
|
1616
|
+
const apply = await confirm2({
|
|
1617
|
+
message: `Apply ${op.name}?`,
|
|
1618
|
+
default: true
|
|
1619
|
+
});
|
|
1620
|
+
if (apply) {
|
|
1621
|
+
segments.push(op.shortName);
|
|
1622
|
+
}
|
|
1623
|
+
} else {
|
|
1624
|
+
const values = {};
|
|
1625
|
+
for (const param of op.params) {
|
|
1626
|
+
values[param.name] = await promptForParam(param);
|
|
1627
|
+
}
|
|
1628
|
+
const segment = formatOpsSegment(op, values);
|
|
1629
|
+
segments.push(segment);
|
|
1630
|
+
console.log(` ${chalk3.dim("Added:")} ${chalk3.cyan(segment)}`);
|
|
1631
|
+
}
|
|
1632
|
+
const addMore = await confirm2({
|
|
1633
|
+
message: "Add another operation?",
|
|
1634
|
+
default: false
|
|
1635
|
+
});
|
|
1636
|
+
if (!addMore) break;
|
|
1637
|
+
}
|
|
1638
|
+
if (segments.length === 0) {
|
|
1639
|
+
return "";
|
|
1640
|
+
}
|
|
1641
|
+
const opsString = segments.join("/");
|
|
1642
|
+
console.log();
|
|
1643
|
+
console.log(chalk3.bold("Operations string:"));
|
|
1644
|
+
console.log(` ${chalk3.green(opsString)}`);
|
|
1645
|
+
console.log();
|
|
1646
|
+
const proceed = await confirm2({
|
|
1647
|
+
message: "Execute this transformation?",
|
|
1648
|
+
default: true
|
|
1649
|
+
});
|
|
1650
|
+
return proceed ? opsString : "";
|
|
1651
|
+
}
|
|
1652
|
+
async function promptForParam(param) {
|
|
1653
|
+
const label = param.unit ? `${param.label} (${param.unit})` : param.label;
|
|
1654
|
+
const message = param.note ? `${label} \u2014 ${chalk3.dim(param.note)}` : label;
|
|
1655
|
+
switch (param.type) {
|
|
1656
|
+
case "slider":
|
|
1657
|
+
return await number({
|
|
1658
|
+
message,
|
|
1659
|
+
default: param.default,
|
|
1660
|
+
min: param.min,
|
|
1661
|
+
max: param.max,
|
|
1662
|
+
step: param.step
|
|
1663
|
+
}) ?? param.default;
|
|
1664
|
+
case "toggle":
|
|
1665
|
+
return confirm2({
|
|
1666
|
+
message,
|
|
1667
|
+
default: param.default
|
|
1668
|
+
});
|
|
1669
|
+
case "dropdown":
|
|
1670
|
+
return select({
|
|
1671
|
+
message,
|
|
1672
|
+
choices: (param.options ?? []).map((o) => ({
|
|
1673
|
+
value: o.value,
|
|
1674
|
+
name: o.label
|
|
1675
|
+
})),
|
|
1676
|
+
default: param.default
|
|
1677
|
+
});
|
|
1678
|
+
case "color":
|
|
1679
|
+
return input2({
|
|
1680
|
+
message,
|
|
1681
|
+
default: param.default,
|
|
1682
|
+
validate: (v) => /^#[0-9a-fA-F]{6}$/.test(v) || "Enter a valid hex color (#rrggbb)"
|
|
1683
|
+
});
|
|
1684
|
+
case "text":
|
|
1685
|
+
return input2({
|
|
1686
|
+
message,
|
|
1687
|
+
default: param.default
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
function pad(str, width) {
|
|
1692
|
+
return str.length >= width ? str + " " : str + " ".repeat(width - str.length);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
// src/commands/images.ts
|
|
1696
|
+
function registerImagesCommands(program2) {
|
|
1697
|
+
const images = program2.command("images").description("Upload, list, transform, and delete images");
|
|
1698
|
+
images.command("list").description("List uploaded images").option("--page <number>", "Page number", "1").option("--limit <number>", "Results per page", "20").option("--search <query>", "Search by filename").action(async (opts) => {
|
|
1699
|
+
const globalOpts = program2.opts();
|
|
1700
|
+
const client = createClientFromGlobalOpts(globalOpts);
|
|
1701
|
+
const params = {
|
|
1702
|
+
page: opts.page,
|
|
1703
|
+
limit: opts.limit
|
|
1704
|
+
};
|
|
1705
|
+
if (opts.search) params.q = opts.search;
|
|
1706
|
+
const res = await client.get("/api/v1/cli/images", params);
|
|
1707
|
+
if (globalOpts.output === "json") {
|
|
1708
|
+
printJson(res);
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
if (res.images.length === 0) {
|
|
1712
|
+
console.log("No images found.");
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
const rows = res.images.map((img) => ({
|
|
1716
|
+
uuid: img.uuid,
|
|
1717
|
+
filename: img.filename,
|
|
1718
|
+
folder: img.folder,
|
|
1719
|
+
size: formatBytes(img.size_bytes),
|
|
1720
|
+
type: img.mime_type,
|
|
1721
|
+
created: formatDate(img.created_at)
|
|
1722
|
+
}));
|
|
1723
|
+
printTable(
|
|
1724
|
+
[
|
|
1725
|
+
{ header: "UUID", key: "uuid" },
|
|
1726
|
+
{ header: "Filename", key: "filename" },
|
|
1727
|
+
{ header: "Folder", key: "folder" },
|
|
1728
|
+
{ header: "Size", key: "size" },
|
|
1729
|
+
{ header: "Type", key: "type" },
|
|
1730
|
+
{ header: "Created", key: "created" }
|
|
1731
|
+
],
|
|
1732
|
+
rows
|
|
1733
|
+
);
|
|
1734
|
+
const { page, total, limit } = res.pagination;
|
|
1735
|
+
const totalPages = Math.ceil(total / limit);
|
|
1736
|
+
console.log();
|
|
1737
|
+
console.log(chalk4.dim(`Page ${page}/${totalPages} (${total} total images)`));
|
|
1738
|
+
});
|
|
1739
|
+
images.command("upload").description("Upload one or more images").argument("<files...>", "Files to upload").option("--folder <path>", "Destination folder", "/").option("--ops <operations>", "Transform operations to apply after upload").option("--format <ext>", "Output format for transform (jpg, png, webp)").action(async (files, opts) => {
|
|
1740
|
+
const globalOpts = program2.opts();
|
|
1741
|
+
const client = createClientFromGlobalOpts(globalOpts);
|
|
1742
|
+
if (opts.format && !opts.ops) {
|
|
1743
|
+
console.error(chalk4.yellow("Note: --format ignored without --ops."));
|
|
1744
|
+
}
|
|
1745
|
+
const results = [];
|
|
1746
|
+
for (const filePath of files) {
|
|
1747
|
+
const fileName = basename2(filePath);
|
|
1748
|
+
const spinner = ora(`Uploading ${fileName}...`).start();
|
|
1749
|
+
try {
|
|
1750
|
+
statSync3(filePath);
|
|
1751
|
+
} catch {
|
|
1752
|
+
spinner.fail(`File not found: ${filePath}`);
|
|
1753
|
+
results.push({
|
|
1754
|
+
file: fileName,
|
|
1755
|
+
status: "error",
|
|
1756
|
+
uuid: "",
|
|
1757
|
+
url: ""
|
|
1758
|
+
});
|
|
1759
|
+
continue;
|
|
1760
|
+
}
|
|
1761
|
+
try {
|
|
1762
|
+
const fileBuffer = readFileSync2(filePath);
|
|
1763
|
+
const blob = new Blob([fileBuffer]);
|
|
1764
|
+
const form = new FormData();
|
|
1765
|
+
form.append("file", blob, fileName);
|
|
1766
|
+
form.append("folder", opts.folder);
|
|
1767
|
+
const res = await client.postForm("/api/v1/cli/images/upload", form);
|
|
1768
|
+
if (res.status === "duplicate") {
|
|
1769
|
+
spinner.warn(`${fileName} (duplicate)`);
|
|
1770
|
+
} else {
|
|
1771
|
+
spinner.succeed(fileName);
|
|
1772
|
+
}
|
|
1773
|
+
results.push({
|
|
1774
|
+
file: fileName,
|
|
1775
|
+
status: res.status,
|
|
1776
|
+
uuid: res.image_id,
|
|
1777
|
+
url: res.url
|
|
1778
|
+
});
|
|
1779
|
+
} catch (err) {
|
|
1780
|
+
spinner.fail(`${fileName}: ${err.message}`);
|
|
1781
|
+
results.push({
|
|
1782
|
+
file: fileName,
|
|
1783
|
+
status: "error",
|
|
1784
|
+
uuid: "",
|
|
1785
|
+
url: ""
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
if (opts.ops) {
|
|
1790
|
+
for (const result of results) {
|
|
1791
|
+
if (result.status === "error" || !result.uuid) continue;
|
|
1792
|
+
const spinner = ora(`Transforming ${result.file}...`).start();
|
|
1793
|
+
try {
|
|
1794
|
+
const body = {
|
|
1795
|
+
uuid: result.uuid,
|
|
1796
|
+
operations: opts.ops
|
|
1797
|
+
};
|
|
1798
|
+
if (opts.format) body.format = opts.format;
|
|
1799
|
+
const tres = await client.post(
|
|
1800
|
+
"/api/v1/cli/images/transform",
|
|
1801
|
+
body
|
|
1802
|
+
);
|
|
1803
|
+
result.transform_url = tres.url;
|
|
1804
|
+
spinner.succeed(`Transformed ${result.file}`);
|
|
1805
|
+
} catch (err) {
|
|
1806
|
+
result.transform_url = `error: ${err.message}`;
|
|
1807
|
+
spinner.fail(`Transform failed for ${result.file}`);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
console.log();
|
|
1812
|
+
if (globalOpts.output === "json") {
|
|
1813
|
+
printJson(results);
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
const columns = [
|
|
1817
|
+
{ header: "File", key: "file" },
|
|
1818
|
+
{ header: "Status", key: "status" },
|
|
1819
|
+
{ header: "UUID", key: "uuid" },
|
|
1820
|
+
{ header: "URL", key: "url" }
|
|
1821
|
+
];
|
|
1822
|
+
if (opts.ops) {
|
|
1823
|
+
columns.push({ header: "Transform URL", key: "transform_url" });
|
|
1824
|
+
}
|
|
1825
|
+
printTable(columns, results);
|
|
1826
|
+
});
|
|
1827
|
+
images.command("delete").description("Delete one or more images").argument("<uuid...>", "Image UUID(s)").option("-y, --yes", "Skip confirmation prompt").action(async (uuids, opts) => {
|
|
1828
|
+
const globalOpts = program2.opts();
|
|
1829
|
+
const client = createClientFromGlobalOpts(globalOpts);
|
|
1830
|
+
if (!opts.yes) {
|
|
1831
|
+
const msg = uuids.length === 1 ? `Delete image ${uuids[0]}? This cannot be undone.` : `Delete ${uuids.length} images? This cannot be undone.`;
|
|
1832
|
+
const proceed = await confirm3({ message: msg, default: false });
|
|
1833
|
+
if (!proceed) {
|
|
1834
|
+
console.log("Aborted.");
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
const results = [];
|
|
1839
|
+
for (const uuid of uuids) {
|
|
1840
|
+
try {
|
|
1841
|
+
await client.delete(`/api/v1/cli/images/${uuid}`);
|
|
1842
|
+
results.push({ uuid, status: "deleted" });
|
|
1843
|
+
console.log(chalk4.green(` \u2713 ${uuid}`));
|
|
1844
|
+
} catch (err) {
|
|
1845
|
+
const msg = err.message;
|
|
1846
|
+
results.push({ uuid, status: "error", error: msg });
|
|
1847
|
+
console.log(chalk4.red(` \u2717 ${uuid}: ${msg}`));
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
if (globalOpts.output === "json") {
|
|
1851
|
+
printJson(results);
|
|
1852
|
+
}
|
|
1853
|
+
const deleted = results.filter((r) => r.status === "deleted").length;
|
|
1854
|
+
const failed = results.filter((r) => r.status === "error").length;
|
|
1855
|
+
console.log();
|
|
1856
|
+
if (failed > 0) {
|
|
1857
|
+
console.log(`${deleted} deleted, ${chalk4.red(`${failed} failed`)}.`);
|
|
1858
|
+
process.exit(1);
|
|
1859
|
+
} else {
|
|
1860
|
+
console.log(chalk4.green(`${deleted} deleted.`));
|
|
1861
|
+
}
|
|
1862
|
+
});
|
|
1863
|
+
images.command("info").description("Show image metadata").argument("<uuid>", "Image UUID").action(async (uuid) => {
|
|
1864
|
+
const globalOpts = program2.opts();
|
|
1865
|
+
const client = createClientFromGlobalOpts(globalOpts);
|
|
1866
|
+
const res = await client.get(`/api/v1/images/${uuid}/metadata`);
|
|
1867
|
+
if (globalOpts.output === "json") {
|
|
1868
|
+
printJson(res);
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1871
|
+
for (const [key, value] of Object.entries(res)) {
|
|
1872
|
+
console.log(` ${chalk4.bold(key)}: ${value}`);
|
|
1873
|
+
}
|
|
1874
|
+
});
|
|
1875
|
+
images.command("transform").description("Transform an image").option("--image <uuid>", "Image UUID").option("--ops <operations>", "Operations string (e.g. resize-800-600/sharp-1.2)").option("--format <ext>", "Output format (jpg, png, webp)").option("--list-ops", "List available operations and syntax").option("-i, --interactive", "Build operations string interactively").option("--open", "Open result URL in default browser").option("--download <path>", "Download result to local file").action(async (opts) => {
|
|
1876
|
+
const globalOpts = program2.opts();
|
|
1877
|
+
if (opts.listOps) {
|
|
1878
|
+
renderOperationsList(globalOpts.output);
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
if (opts.interactive) {
|
|
1882
|
+
if (!opts.image) {
|
|
1883
|
+
console.error(
|
|
1884
|
+
chalk4.red(
|
|
1885
|
+
"--image <uuid> is required. Use `viucraft images list` to find image UUIDs."
|
|
1886
|
+
)
|
|
1887
|
+
);
|
|
1888
|
+
process.exit(1);
|
|
1889
|
+
}
|
|
1890
|
+
if (opts.ops) {
|
|
1891
|
+
console.error(chalk4.yellow("Note: --ops ignored when --interactive is used."));
|
|
1892
|
+
}
|
|
1893
|
+
const opsString = await buildOpsInteractively();
|
|
1894
|
+
if (!opsString) {
|
|
1895
|
+
console.log("Aborted.");
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
opts.ops = opsString;
|
|
1899
|
+
}
|
|
1900
|
+
if (!opts.image) {
|
|
1901
|
+
console.error(chalk4.red("Missing required option: --image <uuid>"));
|
|
1902
|
+
process.exit(1);
|
|
1903
|
+
}
|
|
1904
|
+
if (!opts.ops) {
|
|
1905
|
+
console.error(chalk4.red("Missing required option: --ops <operations>"));
|
|
1906
|
+
console.error(chalk4.dim("Use --interactive (-i) to build operations step by step."));
|
|
1907
|
+
console.error(chalk4.dim("Use --list-ops to see available operations."));
|
|
1908
|
+
process.exit(1);
|
|
1909
|
+
}
|
|
1910
|
+
const client = createClientFromGlobalOpts(globalOpts);
|
|
1911
|
+
const body = {
|
|
1912
|
+
uuid: opts.image,
|
|
1913
|
+
operations: opts.ops
|
|
1914
|
+
};
|
|
1915
|
+
if (opts.format) body.format = opts.format;
|
|
1916
|
+
const res = await client.post("/api/v1/cli/images/transform", body);
|
|
1917
|
+
if (globalOpts.output === "json") {
|
|
1918
|
+
printJson(res);
|
|
1919
|
+
} else {
|
|
1920
|
+
console.log(res.url);
|
|
1921
|
+
}
|
|
1922
|
+
if (opts.open) {
|
|
1923
|
+
openInBrowser(res.url);
|
|
1924
|
+
console.log(chalk4.dim("Opened in browser."));
|
|
1925
|
+
}
|
|
1926
|
+
if (opts.download) {
|
|
1927
|
+
const spinner = ora("Downloading...").start();
|
|
1928
|
+
try {
|
|
1929
|
+
const saved = await downloadFile(res.url, opts.download);
|
|
1930
|
+
spinner.succeed(`Downloaded to ${saved}`);
|
|
1931
|
+
} catch (err) {
|
|
1932
|
+
spinner.fail(`Download failed: ${err.message}`);
|
|
1933
|
+
process.exit(1);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
});
|
|
1937
|
+
images.command("download").description("Download an image to a local file").argument("<uuid>", "Image UUID").option("--output <path>", "Output file path").option("--ops <operations>", "Apply operations before downloading").option("--format <ext>", "Output format (jpg, png, webp)").action(async (uuid, opts) => {
|
|
1938
|
+
const globalOpts = program2.opts();
|
|
1939
|
+
const client = createClientFromGlobalOpts(globalOpts);
|
|
1940
|
+
const creds = resolveCredentials({
|
|
1941
|
+
apiKey: globalOpts.apiKey,
|
|
1942
|
+
profile: globalOpts.profile
|
|
1943
|
+
});
|
|
1944
|
+
let subdomain = creds.subdomain;
|
|
1945
|
+
if (!subdomain) {
|
|
1946
|
+
const whoami = await client.get("/api/v1/cli/whoami");
|
|
1947
|
+
subdomain = whoami.customer.subdomain;
|
|
1948
|
+
}
|
|
1949
|
+
const metadata = await client.get(`/api/v1/images/${uuid}/metadata`);
|
|
1950
|
+
const mimeType = metadata.mime_type ?? "image/jpeg";
|
|
1951
|
+
const originalExt = mimeType.split("/").pop() ?? "jpg";
|
|
1952
|
+
const format = opts.format ?? originalExt;
|
|
1953
|
+
const url = buildImageUrl({
|
|
1954
|
+
subdomain,
|
|
1955
|
+
apiUrl: creds.apiUrl,
|
|
1956
|
+
uuid,
|
|
1957
|
+
format,
|
|
1958
|
+
ops: opts.ops,
|
|
1959
|
+
insecure: creds.insecure
|
|
1960
|
+
});
|
|
1961
|
+
const outputPath = opts.output ?? `${uuid}.${format}`;
|
|
1962
|
+
const spinner = ora(`Downloading ${uuid}...`).start();
|
|
1963
|
+
try {
|
|
1964
|
+
const saved = await downloadFile(url, outputPath);
|
|
1965
|
+
spinner.succeed(`Downloaded to ${saved}`);
|
|
1966
|
+
} catch (err) {
|
|
1967
|
+
spinner.fail(`Download failed: ${err.message}`);
|
|
1968
|
+
process.exit(1);
|
|
1969
|
+
}
|
|
1970
|
+
});
|
|
1971
|
+
images.command("open").description("Open an image in the default browser").argument("<uuid>", "Image UUID").option("--ops <operations>", "Apply operations to the URL").option("--format <ext>", "Output format (jpg, png, webp)").action(async (uuid, opts) => {
|
|
1972
|
+
const globalOpts = program2.opts();
|
|
1973
|
+
const client = createClientFromGlobalOpts(globalOpts);
|
|
1974
|
+
const creds = resolveCredentials({
|
|
1975
|
+
apiKey: globalOpts.apiKey,
|
|
1976
|
+
profile: globalOpts.profile
|
|
1977
|
+
});
|
|
1978
|
+
let subdomain = creds.subdomain;
|
|
1979
|
+
if (!subdomain) {
|
|
1980
|
+
const whoami = await client.get("/api/v1/cli/whoami");
|
|
1981
|
+
subdomain = whoami.customer.subdomain;
|
|
1982
|
+
}
|
|
1983
|
+
const metadata = await client.get(`/api/v1/images/${uuid}/metadata`);
|
|
1984
|
+
const mimeType = metadata.mime_type ?? "image/jpeg";
|
|
1985
|
+
const originalExt = mimeType.split("/").pop() ?? "jpg";
|
|
1986
|
+
const format = opts.format ?? originalExt;
|
|
1987
|
+
const url = buildImageUrl({
|
|
1988
|
+
subdomain,
|
|
1989
|
+
apiUrl: creds.apiUrl,
|
|
1990
|
+
uuid,
|
|
1991
|
+
format,
|
|
1992
|
+
ops: opts.ops,
|
|
1993
|
+
insecure: creds.insecure
|
|
1994
|
+
});
|
|
1995
|
+
if (globalOpts.output === "json") {
|
|
1996
|
+
printJson({ url });
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
console.log(url);
|
|
2000
|
+
openInBrowser(url);
|
|
2001
|
+
console.log(chalk4.dim("Opened in browser."));
|
|
2002
|
+
});
|
|
2003
|
+
}
|
|
2004
|
+
function formatBytes(bytes) {
|
|
2005
|
+
if (bytes === 0) return "0 B";
|
|
2006
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
2007
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
2008
|
+
const value = bytes / 1024 ** i;
|
|
2009
|
+
return `${value.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
|
2010
|
+
}
|
|
2011
|
+
function formatDate(iso) {
|
|
2012
|
+
const d = new Date(iso);
|
|
2013
|
+
return d.toLocaleDateString("en-CA");
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
// src/commands/usage.ts
|
|
2017
|
+
import chalk5 from "chalk";
|
|
2018
|
+
function registerUsageCommand(program2) {
|
|
2019
|
+
program2.command("usage").description("View account usage and limits").option("--daily", "Show daily breakdown").option("--start <date>", "Start date for daily view (YYYY-MM-DD)").option("--end <date>", "End date for daily view (YYYY-MM-DD)").action(async (opts) => {
|
|
2020
|
+
const globalOpts = program2.opts();
|
|
2021
|
+
const client = createClientFromGlobalOpts(globalOpts);
|
|
2022
|
+
if (opts.daily) {
|
|
2023
|
+
const params = { period: "daily" };
|
|
2024
|
+
if (opts.start) params.start_date = opts.start;
|
|
2025
|
+
if (opts.end) params.end_date = opts.end;
|
|
2026
|
+
const res = await client.get("/api/v1/usage", params);
|
|
2027
|
+
if (globalOpts.output === "json") {
|
|
2028
|
+
printJson(res);
|
|
2029
|
+
return;
|
|
2030
|
+
}
|
|
2031
|
+
if (res.data.length === 0) {
|
|
2032
|
+
console.log("No usage data for this period.");
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
const rows = res.data.map((entry) => ({
|
|
2036
|
+
date: entry.date,
|
|
2037
|
+
requests: String(entry.requests),
|
|
2038
|
+
storage: formatBytes2(entry.storage_bytes),
|
|
2039
|
+
bandwidth: formatBytes2(entry.bandwidth_bytes)
|
|
2040
|
+
}));
|
|
2041
|
+
printTable(
|
|
2042
|
+
[
|
|
2043
|
+
{ header: "Date", key: "date" },
|
|
2044
|
+
{ header: "Requests", key: "requests" },
|
|
2045
|
+
{ header: "Storage", key: "storage" },
|
|
2046
|
+
{ header: "Bandwidth", key: "bandwidth" }
|
|
2047
|
+
],
|
|
2048
|
+
rows
|
|
2049
|
+
);
|
|
2050
|
+
console.log();
|
|
2051
|
+
console.log(
|
|
2052
|
+
chalk5.dim(
|
|
2053
|
+
`${res.period.start_date} to ${res.period.end_date} | Plan: ${res.meta.plan} | ${res.meta.total_records} records`
|
|
2054
|
+
)
|
|
2055
|
+
);
|
|
2056
|
+
} else {
|
|
2057
|
+
const res = await client.get("/api/v1/usage", {
|
|
2058
|
+
period: "current"
|
|
2059
|
+
});
|
|
2060
|
+
if (globalOpts.output === "json") {
|
|
2061
|
+
printJson(res);
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
console.log(chalk5.bold("Current Billing Cycle"));
|
|
2065
|
+
console.log(
|
|
2066
|
+
chalk5.dim(`Plan: ${res.meta.plan} | Ends: ${formatDate2(res.meta.billing_cycle_ends)}`)
|
|
2067
|
+
);
|
|
2068
|
+
console.log();
|
|
2069
|
+
printMetric("Requests", res.data.requests);
|
|
2070
|
+
printStorageMetric("Storage", res.data.storage);
|
|
2071
|
+
printStorageMetric("Bandwidth", res.data.bandwidth);
|
|
2072
|
+
}
|
|
2073
|
+
});
|
|
2074
|
+
}
|
|
2075
|
+
function printMetric(label, metric) {
|
|
2076
|
+
if (!metric) {
|
|
2077
|
+
console.log(` ${chalk5.bold(label.padEnd(12))} ${chalk5.dim("n/a")}`);
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
const bar = progressBar2(metric.percent);
|
|
2081
|
+
const pct = Math.round(metric.percent);
|
|
2082
|
+
const color = pct >= 90 ? chalk5.red : pct >= 75 ? chalk5.yellow : chalk5.green;
|
|
2083
|
+
console.log(
|
|
2084
|
+
` ${chalk5.bold(label.padEnd(12))} ${color(`${bar} ${pct}%`)} ${metric.used.toLocaleString()} / ${metric.limit.toLocaleString()}`
|
|
2085
|
+
);
|
|
2086
|
+
}
|
|
2087
|
+
function printStorageMetric(label, metric) {
|
|
2088
|
+
if (!metric) {
|
|
2089
|
+
console.log(` ${chalk5.bold(label.padEnd(12))} ${chalk5.dim("n/a")}`);
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
const bar = progressBar2(metric.percent);
|
|
2093
|
+
const pct = Math.round(metric.percent);
|
|
2094
|
+
const color = pct >= 90 ? chalk5.red : pct >= 75 ? chalk5.yellow : chalk5.green;
|
|
2095
|
+
console.log(
|
|
2096
|
+
` ${chalk5.bold(label.padEnd(12))} ${color(`${bar} ${pct}%`)} ${metric.formatted_used} / ${metric.formatted_limit}`
|
|
2097
|
+
);
|
|
2098
|
+
}
|
|
2099
|
+
function progressBar2(percent, width = 20) {
|
|
2100
|
+
if (percent == null || isNaN(percent)) return "\u2591".repeat(width);
|
|
2101
|
+
const clamped = Math.max(0, Math.min(percent, 100));
|
|
2102
|
+
const filled = Math.round(clamped / 100 * width);
|
|
2103
|
+
const empty = width - filled;
|
|
2104
|
+
return `[${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}]`;
|
|
2105
|
+
}
|
|
2106
|
+
function formatBytes2(bytes) {
|
|
2107
|
+
if (bytes === 0) return "0 B";
|
|
2108
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
2109
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
2110
|
+
const value = bytes / 1024 ** i;
|
|
2111
|
+
return `${value.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
|
2112
|
+
}
|
|
2113
|
+
function formatDate2(iso) {
|
|
2114
|
+
return new Date(iso).toLocaleDateString("en-CA");
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
// src/commands/presets.ts
|
|
2118
|
+
import { confirm as confirm4 } from "@inquirer/prompts";
|
|
2119
|
+
import chalk6 from "chalk";
|
|
2120
|
+
function registerPresetsCommands(program2) {
|
|
2121
|
+
const presets = program2.command("presets").description("Manage transformation presets");
|
|
2122
|
+
presets.command("list").description("List all presets").action(async () => {
|
|
2123
|
+
const globalOpts = program2.opts();
|
|
2124
|
+
const client = createClientFromGlobalOpts(globalOpts);
|
|
2125
|
+
const res = await client.get("/api/v1/presets");
|
|
2126
|
+
if (globalOpts.output === "json") {
|
|
2127
|
+
printJson(res);
|
|
2128
|
+
return;
|
|
2129
|
+
}
|
|
2130
|
+
if (res.presets.length === 0) {
|
|
2131
|
+
console.log("No presets found.");
|
|
2132
|
+
return;
|
|
2133
|
+
}
|
|
2134
|
+
const rows = res.presets.map((p) => ({
|
|
2135
|
+
name: p.name,
|
|
2136
|
+
operations: formatOperations(p.operations),
|
|
2137
|
+
format: p.format ?? "-",
|
|
2138
|
+
quality: p.quality != null ? String(p.quality) : "-",
|
|
2139
|
+
custom: p.is_custom ? "yes" : ""
|
|
2140
|
+
}));
|
|
2141
|
+
printTable(
|
|
2142
|
+
[
|
|
2143
|
+
{ header: "Name", key: "name" },
|
|
2144
|
+
{ header: "Operations", key: "operations" },
|
|
2145
|
+
{ header: "Format", key: "format" },
|
|
2146
|
+
{ header: "Quality", key: "quality" },
|
|
2147
|
+
{ header: "Custom", key: "custom" }
|
|
2148
|
+
],
|
|
2149
|
+
rows
|
|
2150
|
+
);
|
|
2151
|
+
});
|
|
2152
|
+
presets.command("get").description("Get preset details").argument("<name>", "Preset name").action(async (name) => {
|
|
2153
|
+
const globalOpts = program2.opts();
|
|
2154
|
+
const client = createClientFromGlobalOpts(globalOpts);
|
|
2155
|
+
const res = await client.get(`/api/v1/presets/${name}`);
|
|
2156
|
+
if (globalOpts.output === "json") {
|
|
2157
|
+
printJson(res);
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
console.log(` ${chalk6.bold("Name")}: ${res.name}`);
|
|
2161
|
+
console.log(` ${chalk6.bold("Operations")}: ${formatOperations(res.operations)}`);
|
|
2162
|
+
console.log(` ${chalk6.bold("Format")}: ${res.format ?? "-"}`);
|
|
2163
|
+
console.log(` ${chalk6.bold("Quality")}: ${res.quality != null ? res.quality : "-"}`);
|
|
2164
|
+
console.log(` ${chalk6.bold("Custom")}: ${res.is_custom ? "yes" : "no"}`);
|
|
2165
|
+
});
|
|
2166
|
+
presets.command("create").description("Create a custom preset").requiredOption("--name <name>", "Preset name").requiredOption("--ops <operations>", "Operations (comma-separated)").option("--description <text>", "Preset description").action(async (opts) => {
|
|
2167
|
+
const globalOpts = program2.opts();
|
|
2168
|
+
const client = createClientFromGlobalOpts(globalOpts);
|
|
2169
|
+
const operations = opts.ops.split(",").map((s) => s.trim());
|
|
2170
|
+
const res = await client.post("/api/v1/presets", {
|
|
2171
|
+
name: opts.name,
|
|
2172
|
+
operations,
|
|
2173
|
+
description: opts.description
|
|
2174
|
+
});
|
|
2175
|
+
if (globalOpts.output === "json") {
|
|
2176
|
+
printJson(res);
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
console.log(chalk6.green(`Preset "${res.name}" created.`));
|
|
2180
|
+
});
|
|
2181
|
+
presets.command("delete").description("Delete a custom preset").argument("<name>", "Preset name").option("-y, --yes", "Skip confirmation prompt").action(async (name, opts) => {
|
|
2182
|
+
const globalOpts = program2.opts();
|
|
2183
|
+
const client = createClientFromGlobalOpts(globalOpts);
|
|
2184
|
+
if (!opts.yes) {
|
|
2185
|
+
const proceed = await confirm4({
|
|
2186
|
+
message: `Delete preset "${name}"?`,
|
|
2187
|
+
default: false
|
|
2188
|
+
});
|
|
2189
|
+
if (!proceed) {
|
|
2190
|
+
console.log("Aborted.");
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
await client.delete(`/api/v1/presets/${name}`);
|
|
2195
|
+
console.log(chalk6.green(`Preset "${name}" deleted.`));
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
2198
|
+
function formatOperations(ops) {
|
|
2199
|
+
if (!Array.isArray(ops)) return String(ops);
|
|
2200
|
+
return ops.map((op) => {
|
|
2201
|
+
if (typeof op === "string") return op;
|
|
2202
|
+
if (typeof op === "object" && op !== null && "name" in op) {
|
|
2203
|
+
const o = op;
|
|
2204
|
+
if (o.params) {
|
|
2205
|
+
const params = Object.entries(o.params).map(([k, v]) => `${k}=${v}`).join(",");
|
|
2206
|
+
return `${o.name}(${params})`;
|
|
2207
|
+
}
|
|
2208
|
+
return o.name;
|
|
2209
|
+
}
|
|
2210
|
+
return JSON.stringify(op);
|
|
2211
|
+
}).join(" \u2192 ");
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
// src/commands/batch.ts
|
|
2215
|
+
import { readFileSync as readFileSync3, createWriteStream as createWriteStream2 } from "fs";
|
|
2216
|
+
import { pipeline as pipeline2 } from "stream/promises";
|
|
2217
|
+
import { Readable as Readable2 } from "stream";
|
|
2218
|
+
import chalk7 from "chalk";
|
|
2219
|
+
import ora2 from "ora";
|
|
2220
|
+
|
|
2221
|
+
// src/lib/sse.ts
|
|
2222
|
+
import { createParser } from "eventsource-parser";
|
|
2223
|
+
var TERMINAL_EVENTS = /* @__PURE__ */ new Set(["job.completed", "job.failed"]);
|
|
2224
|
+
async function streamSSE(opts) {
|
|
2225
|
+
const { client, path, onEvent, onError } = opts;
|
|
2226
|
+
let lastEventId = opts.lastEventId;
|
|
2227
|
+
const response = await client.stream(path, lastEventId);
|
|
2228
|
+
if (!response.body) {
|
|
2229
|
+
throw new Error("No response body for SSE stream.");
|
|
2230
|
+
}
|
|
2231
|
+
return new Promise((resolve, reject) => {
|
|
2232
|
+
let done = false;
|
|
2233
|
+
const parser = createParser({
|
|
2234
|
+
onEvent(event) {
|
|
2235
|
+
if (done) return;
|
|
2236
|
+
if (event.id) lastEventId = event.id;
|
|
2237
|
+
if (!event.event) return;
|
|
2238
|
+
try {
|
|
2239
|
+
const data = JSON.parse(event.data);
|
|
2240
|
+
const sseEvent = { type: event.event, data };
|
|
2241
|
+
onEvent(sseEvent);
|
|
2242
|
+
if (TERMINAL_EVENTS.has(event.event)) {
|
|
2243
|
+
done = true;
|
|
2244
|
+
resolve();
|
|
2245
|
+
}
|
|
2246
|
+
} catch (err) {
|
|
2247
|
+
onError?.(err);
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
});
|
|
2251
|
+
const reader = response.body.getReader();
|
|
2252
|
+
const decoder = new TextDecoder();
|
|
2253
|
+
function read() {
|
|
2254
|
+
reader.read().then(({ done: streamDone, value }) => {
|
|
2255
|
+
if (done) return;
|
|
2256
|
+
if (streamDone) {
|
|
2257
|
+
resolve();
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
2260
|
+
const text = decoder.decode(value, { stream: true });
|
|
2261
|
+
parser.feed(text);
|
|
2262
|
+
read();
|
|
2263
|
+
}).catch((err) => {
|
|
2264
|
+
if (done) return;
|
|
2265
|
+
reject(err);
|
|
2266
|
+
});
|
|
2267
|
+
}
|
|
2268
|
+
read();
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
// src/commands/batch.ts
|
|
2273
|
+
function registerBatchCommands(program2) {
|
|
2274
|
+
const batch = program2.command("batch").description("Create and manage batch processing jobs");
|
|
2275
|
+
batch.command("create").description("Create a batch job").option("--file <path>", "JSON file with task definitions").option("--images <uuids>", "Comma-separated image UUIDs (alternative to --file)").option("--ops <operations>", "Operations string (required with --images)").option("--format <ext>", "Output format for inline tasks (jpg, png, webp)").option("--name <name>", "Job name").option("--webhook-url <url>", "Webhook URL for notifications").option("--priority <level>", "Priority: normal, high", "normal").action(async (opts) => {
|
|
2276
|
+
const globalOpts = program2.opts();
|
|
2277
|
+
if (opts.file && opts.images) {
|
|
2278
|
+
console.error(chalk7.red("Options --file and --images are mutually exclusive."));
|
|
2279
|
+
process.exit(1);
|
|
2280
|
+
}
|
|
2281
|
+
if (!opts.file && !opts.images) {
|
|
2282
|
+
console.error(chalk7.red("Provide --file <path> or --images <uuids> with --ops."));
|
|
2283
|
+
process.exit(1);
|
|
2284
|
+
}
|
|
2285
|
+
let tasks;
|
|
2286
|
+
if (opts.images) {
|
|
2287
|
+
if (!opts.ops) {
|
|
2288
|
+
console.error(chalk7.red("--ops is required when using --images."));
|
|
2289
|
+
process.exit(1);
|
|
2290
|
+
}
|
|
2291
|
+
const uuids = opts.images.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2292
|
+
if (uuids.length === 0) {
|
|
2293
|
+
console.error(chalk7.red("No valid image UUIDs provided."));
|
|
2294
|
+
process.exit(1);
|
|
2295
|
+
}
|
|
2296
|
+
tasks = uuids.map((uuid) => ({
|
|
2297
|
+
image_id: uuid,
|
|
2298
|
+
operations: opts.ops,
|
|
2299
|
+
...opts.format ? { format: opts.format } : {}
|
|
2300
|
+
}));
|
|
2301
|
+
} else {
|
|
2302
|
+
try {
|
|
2303
|
+
const raw = readFileSync3(opts.file, "utf-8");
|
|
2304
|
+
const parsed = JSON.parse(raw);
|
|
2305
|
+
tasks = Array.isArray(parsed) ? parsed : parsed.tasks;
|
|
2306
|
+
} catch (err) {
|
|
2307
|
+
console.error(chalk7.red(`Failed to read task file: ${err.message}`));
|
|
2308
|
+
process.exit(1);
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
const client = createClientFromGlobalOpts(globalOpts);
|
|
2312
|
+
const body = { tasks };
|
|
2313
|
+
if (opts.name) body.name = opts.name;
|
|
2314
|
+
if (opts.webhookUrl) body.webhook_url = opts.webhookUrl;
|
|
2315
|
+
if (opts.priority) body.priority = opts.priority;
|
|
2316
|
+
const spinner = ora2("Submitting batch job...").start();
|
|
2317
|
+
const res = await client.post("/api/v1/jobs", body);
|
|
2318
|
+
spinner.succeed("Batch job created.");
|
|
2319
|
+
if (globalOpts.output === "json") {
|
|
2320
|
+
printJson(res);
|
|
2321
|
+
return;
|
|
2322
|
+
}
|
|
2323
|
+
console.log();
|
|
2324
|
+
console.log(` Job ID: ${chalk7.bold(res.job_id)}`);
|
|
2325
|
+
console.log(` Name: ${res.name ?? "-"}`);
|
|
2326
|
+
console.log(` Status: ${res.status}`);
|
|
2327
|
+
console.log(` Tasks: ${res.task_count}`);
|
|
2328
|
+
console.log(` ETA: ${res.estimated_duration_seconds}s`);
|
|
2329
|
+
console.log();
|
|
2330
|
+
console.log(chalk7.dim(`Stream: viucraft batch stream ${res.job_id}`));
|
|
2331
|
+
});
|
|
2332
|
+
batch.command("list").description("List batch jobs").option("--page <number>", "Page number", "1").option("--per-page <number>", "Results per page", "20").action(async (opts) => {
|
|
2333
|
+
const globalOpts = program2.opts();
|
|
2334
|
+
const client = createClientFromGlobalOpts(globalOpts);
|
|
2335
|
+
const res = await client.get("/api/v1/jobs", {
|
|
2336
|
+
page: opts.page,
|
|
2337
|
+
per_page: opts.perPage
|
|
2338
|
+
});
|
|
2339
|
+
if (globalOpts.output === "json") {
|
|
2340
|
+
printJson(res);
|
|
2341
|
+
return;
|
|
2342
|
+
}
|
|
2343
|
+
if (res.jobs.length === 0) {
|
|
2344
|
+
console.log("No batch jobs found.");
|
|
2345
|
+
return;
|
|
2346
|
+
}
|
|
2347
|
+
const rows = res.jobs.map((j) => ({
|
|
2348
|
+
job_id: j.job_id,
|
|
2349
|
+
name: j.name ?? "-",
|
|
2350
|
+
status: colorStatus(j.status),
|
|
2351
|
+
tasks: String(j.task_count),
|
|
2352
|
+
completed: String(j.completed_count),
|
|
2353
|
+
failed: j.failed_count > 0 ? chalk7.red(String(j.failed_count)) : "0",
|
|
2354
|
+
priority: j.priority,
|
|
2355
|
+
created: formatDate3(j.created_at)
|
|
2356
|
+
}));
|
|
2357
|
+
printTable(
|
|
2358
|
+
[
|
|
2359
|
+
{ header: "Job ID", key: "job_id" },
|
|
2360
|
+
{ header: "Name", key: "name" },
|
|
2361
|
+
{ header: "Status", key: "status" },
|
|
2362
|
+
{ header: "Tasks", key: "tasks" },
|
|
2363
|
+
{ header: "Done", key: "completed" },
|
|
2364
|
+
{ header: "Failed", key: "failed" },
|
|
2365
|
+
{ header: "Priority", key: "priority" },
|
|
2366
|
+
{ header: "Created", key: "created" }
|
|
2367
|
+
],
|
|
2368
|
+
rows
|
|
2369
|
+
);
|
|
2370
|
+
});
|
|
2371
|
+
batch.command("status").description("Show job status").argument("<id>", "Job ID").action(async (id) => {
|
|
2372
|
+
const globalOpts = program2.opts();
|
|
2373
|
+
const client = createClientFromGlobalOpts(globalOpts);
|
|
2374
|
+
const res = await client.get(`/api/v1/jobs/${id}`);
|
|
2375
|
+
if (globalOpts.output === "json") {
|
|
2376
|
+
printJson(res);
|
|
2377
|
+
return;
|
|
2378
|
+
}
|
|
2379
|
+
const p = res.progress;
|
|
2380
|
+
console.log(`Job ${chalk7.bold(res.job_id)} \u2014 ${colorStatus(res.status)}`);
|
|
2381
|
+
console.log(
|
|
2382
|
+
`${progressBar3(p.percent)} ${Math.round(p.percent)}% ${p.completed}/${p.total} completed ${p.failed} failed`
|
|
2383
|
+
);
|
|
2384
|
+
console.log();
|
|
2385
|
+
if (res.tasks.data.length > 0) {
|
|
2386
|
+
const rows = res.tasks.data.map((t) => ({
|
|
2387
|
+
task_id: t.task_id,
|
|
2388
|
+
status: colorStatus(t.status),
|
|
2389
|
+
image: truncate(t.image_url, 40),
|
|
2390
|
+
time: t.processing_ms != null ? `${t.processing_ms}ms` : "-",
|
|
2391
|
+
error: t.error_message ?? ""
|
|
2392
|
+
}));
|
|
2393
|
+
printTable(
|
|
2394
|
+
[
|
|
2395
|
+
{ header: "Task", key: "task_id" },
|
|
2396
|
+
{ header: "Status", key: "status" },
|
|
2397
|
+
{ header: "Image", key: "image" },
|
|
2398
|
+
{ header: "Time", key: "time" },
|
|
2399
|
+
{ header: "Error", key: "error" }
|
|
2400
|
+
],
|
|
2401
|
+
rows
|
|
2402
|
+
);
|
|
2403
|
+
const pg = res.tasks.pagination;
|
|
2404
|
+
console.log();
|
|
2405
|
+
console.log(chalk7.dim(`Page ${pg.page}/${pg.total_pages} (${pg.total} total tasks)`));
|
|
2406
|
+
}
|
|
2407
|
+
});
|
|
2408
|
+
batch.command("stream").description("Live stream job progress (SSE)").argument("<id>", "Job ID").action(async (id) => {
|
|
2409
|
+
const globalOpts = program2.opts();
|
|
2410
|
+
const client = createClientFromGlobalOpts(globalOpts);
|
|
2411
|
+
let completed = 0;
|
|
2412
|
+
let failed = 0;
|
|
2413
|
+
let total = 0;
|
|
2414
|
+
let status = "connecting";
|
|
2415
|
+
function updateProgress() {
|
|
2416
|
+
const pct = total > 0 ? Math.round((completed + failed) / total * 100) : 0;
|
|
2417
|
+
const bar = progressBar3(pct);
|
|
2418
|
+
const failedStr = failed > 0 ? chalk7.red(` ${failed} failed`) : "";
|
|
2419
|
+
process.stderr.write(`\r${bar} ${pct}% ${completed}/${total} completed${failedStr} `);
|
|
2420
|
+
}
|
|
2421
|
+
console.log(`Streaming job ${chalk7.bold(id)}...
|
|
2422
|
+
`);
|
|
2423
|
+
try {
|
|
2424
|
+
await streamSSE({
|
|
2425
|
+
client,
|
|
2426
|
+
path: `/api/v1/jobs/${id}/stream`,
|
|
2427
|
+
onEvent(event) {
|
|
2428
|
+
switch (event.type) {
|
|
2429
|
+
case "job.started":
|
|
2430
|
+
status = event.data.status;
|
|
2431
|
+
console.log(chalk7.dim(`Job started \u2014 ${status}`));
|
|
2432
|
+
break;
|
|
2433
|
+
case "item.completed":
|
|
2434
|
+
completed++;
|
|
2435
|
+
updateProgress();
|
|
2436
|
+
console.log(
|
|
2437
|
+
`
|
|
2438
|
+
${chalk7.green("\u2713")} ${event.data.task_id} ${event.data.processing_ms}ms`
|
|
2439
|
+
);
|
|
2440
|
+
break;
|
|
2441
|
+
case "item.failed":
|
|
2442
|
+
failed++;
|
|
2443
|
+
updateProgress();
|
|
2444
|
+
console.log(
|
|
2445
|
+
`
|
|
2446
|
+
${chalk7.red("\u2717")} ${event.data.task_id} ${event.data.error_message}`
|
|
2447
|
+
);
|
|
2448
|
+
break;
|
|
2449
|
+
case "job.progress":
|
|
2450
|
+
completed = event.data.completed;
|
|
2451
|
+
failed = event.data.failed;
|
|
2452
|
+
total = event.data.total;
|
|
2453
|
+
updateProgress();
|
|
2454
|
+
break;
|
|
2455
|
+
case "job.completed":
|
|
2456
|
+
case "job.failed":
|
|
2457
|
+
status = event.data.status;
|
|
2458
|
+
completed = event.data.completed_count;
|
|
2459
|
+
failed = event.data.failed_count;
|
|
2460
|
+
total = event.data.task_count;
|
|
2461
|
+
updateProgress();
|
|
2462
|
+
console.log(`
|
|
2463
|
+
|
|
2464
|
+
Job ${colorStatus(status)}.`);
|
|
2465
|
+
console.log(` ${completed} completed, ${failed} failed out of ${total} tasks.`);
|
|
2466
|
+
if (status === "completed" || status === "completed_with_errors") {
|
|
2467
|
+
console.log(
|
|
2468
|
+
chalk7.dim(`
|
|
2469
|
+
Download: viucraft batch download ${event.data.job_id}`)
|
|
2470
|
+
);
|
|
2471
|
+
}
|
|
2472
|
+
break;
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
});
|
|
2476
|
+
} catch (err) {
|
|
2477
|
+
console.error(chalk7.red(`
|
|
2478
|
+
Stream error: ${err.message}`));
|
|
2479
|
+
process.exit(1);
|
|
2480
|
+
}
|
|
2481
|
+
});
|
|
2482
|
+
batch.command("cancel").description("Cancel a job").argument("<id>", "Job ID").action(async (id) => {
|
|
2483
|
+
const globalOpts = program2.opts();
|
|
2484
|
+
const client = createClientFromGlobalOpts(globalOpts);
|
|
2485
|
+
const res = await client.post(`/api/v1/jobs/${id}/cancel`);
|
|
2486
|
+
if (globalOpts.output === "json") {
|
|
2487
|
+
printJson(res);
|
|
2488
|
+
return;
|
|
2489
|
+
}
|
|
2490
|
+
console.log(chalk7.yellow(`Job ${res.job_id} cancelled.`));
|
|
2491
|
+
});
|
|
2492
|
+
batch.command("retry").description("Retry failed tasks in a job").argument("<id>", "Job ID").action(async (id) => {
|
|
2493
|
+
const globalOpts = program2.opts();
|
|
2494
|
+
const client = createClientFromGlobalOpts(globalOpts);
|
|
2495
|
+
const res = await client.post(`/api/v1/jobs/${id}/retry`);
|
|
2496
|
+
if (globalOpts.output === "json") {
|
|
2497
|
+
printJson(res);
|
|
2498
|
+
return;
|
|
2499
|
+
}
|
|
2500
|
+
console.log(chalk7.green(`Job ${res.job_id} retry submitted.`));
|
|
2501
|
+
console.log(` Status: ${res.status}`);
|
|
2502
|
+
console.log(` Tasks: ${res.task_count}`);
|
|
2503
|
+
console.log();
|
|
2504
|
+
console.log(chalk7.dim(`Stream: viucraft batch stream ${res.job_id}`));
|
|
2505
|
+
});
|
|
2506
|
+
batch.command("download").description("Download job results as ZIP").argument("<id>", "Job ID").option("--output <file>", "Output filename").action(async (id, opts) => {
|
|
2507
|
+
const globalOpts = program2.opts();
|
|
2508
|
+
const client = createClientFromGlobalOpts(globalOpts);
|
|
2509
|
+
const outputFile = opts.output ?? `batch-${id}.zip`;
|
|
2510
|
+
const spinner = ora2(`Downloading to ${outputFile}...`).start();
|
|
2511
|
+
try {
|
|
2512
|
+
const response = await client.getRaw(`/api/v1/jobs/${id}/download`);
|
|
2513
|
+
if (!response.body) {
|
|
2514
|
+
throw new Error("No response body.");
|
|
2515
|
+
}
|
|
2516
|
+
const nodeStream = Readable2.fromWeb(
|
|
2517
|
+
response.body
|
|
2518
|
+
);
|
|
2519
|
+
const fileStream = createWriteStream2(outputFile);
|
|
2520
|
+
await pipeline2(nodeStream, fileStream);
|
|
2521
|
+
spinner.succeed(`Downloaded ${outputFile}`);
|
|
2522
|
+
} catch (err) {
|
|
2523
|
+
spinner.fail(`Download failed: ${err.message}`);
|
|
2524
|
+
process.exit(1);
|
|
2525
|
+
}
|
|
2526
|
+
});
|
|
2527
|
+
}
|
|
2528
|
+
function progressBar3(percent, width = 20) {
|
|
2529
|
+
const clamped = Math.max(0, Math.min(percent, 100));
|
|
2530
|
+
const filled = Math.round(clamped / 100 * width);
|
|
2531
|
+
const empty = width - filled;
|
|
2532
|
+
return `[${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}]`;
|
|
2533
|
+
}
|
|
2534
|
+
function colorStatus(status) {
|
|
2535
|
+
switch (status) {
|
|
2536
|
+
case "completed":
|
|
2537
|
+
return chalk7.green(status);
|
|
2538
|
+
case "completed_with_errors":
|
|
2539
|
+
return chalk7.yellow(status);
|
|
2540
|
+
case "failed":
|
|
2541
|
+
case "cancelled":
|
|
2542
|
+
return chalk7.red(status);
|
|
2543
|
+
case "processing":
|
|
2544
|
+
return chalk7.cyan(status);
|
|
2545
|
+
case "queued":
|
|
2546
|
+
case "pending":
|
|
2547
|
+
return chalk7.dim(status);
|
|
2548
|
+
default:
|
|
2549
|
+
return status;
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
function formatDate3(iso) {
|
|
2553
|
+
return new Date(iso).toLocaleDateString("en-CA");
|
|
2554
|
+
}
|
|
2555
|
+
function truncate(str, max) {
|
|
2556
|
+
if (str.length <= max) return str;
|
|
2557
|
+
return str.slice(0, max - 3) + "...";
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
// src/commands/cache.ts
|
|
2561
|
+
import { confirm as confirm5 } from "@inquirer/prompts";
|
|
2562
|
+
import ora3 from "ora";
|
|
2563
|
+
function registerCacheCommands(program2) {
|
|
2564
|
+
const cache = program2.command("cache").description("Purge and warm image caches");
|
|
2565
|
+
cache.command("purge").description("Purge cached images").option("--image <uuid>", "Purge specific image (default: entire account)").option("-y, --yes", "Skip confirmation prompt").action(async (opts) => {
|
|
2566
|
+
const globalOpts = program2.opts();
|
|
2567
|
+
const client = createClientFromGlobalOpts(globalOpts);
|
|
2568
|
+
const scope = opts.image ? "image" : "account";
|
|
2569
|
+
if (scope === "account" && !opts.yes) {
|
|
2570
|
+
const proceed = await confirm5({
|
|
2571
|
+
message: "Purge all cached images for the entire account?",
|
|
2572
|
+
default: false
|
|
2573
|
+
});
|
|
2574
|
+
if (!proceed) {
|
|
2575
|
+
console.log("Aborted.");
|
|
2576
|
+
return;
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
const body = { scope };
|
|
2580
|
+
if (opts.image) body.image_id = opts.image;
|
|
2581
|
+
const spinner = ora3("Purging cache...").start();
|
|
2582
|
+
const res = await client.post("/api/cache/purge", body);
|
|
2583
|
+
spinner.succeed("Cache purged.");
|
|
2584
|
+
if (globalOpts.output === "json") {
|
|
2585
|
+
printJson(res);
|
|
2586
|
+
}
|
|
2587
|
+
});
|
|
2588
|
+
cache.command("warm").description("Pre-generate image variants").requiredOption("--image <uuid>", "Image UUID to warm").option("--ops <operations>", "Operations string (e.g. resize-300)").option("--format <ext>", "Output format (webp, jpg, png)", "webp").action(async (opts) => {
|
|
2589
|
+
const globalOpts = program2.opts();
|
|
2590
|
+
const client = createClientFromGlobalOpts(globalOpts);
|
|
2591
|
+
const body = {
|
|
2592
|
+
image_id: opts.image
|
|
2593
|
+
};
|
|
2594
|
+
if (opts.ops) {
|
|
2595
|
+
body.variants = [
|
|
2596
|
+
{
|
|
2597
|
+
name: "CLI warm",
|
|
2598
|
+
operations: opts.ops,
|
|
2599
|
+
format: opts.format
|
|
2600
|
+
}
|
|
2601
|
+
];
|
|
2602
|
+
}
|
|
2603
|
+
const spinner = ora3("Warming cache...").start();
|
|
2604
|
+
const res = await client.post("/api/cache/warm", body);
|
|
2605
|
+
spinner.succeed("Cache warmed.");
|
|
2606
|
+
if (globalOpts.output === "json") {
|
|
2607
|
+
printJson(res);
|
|
2608
|
+
return;
|
|
2609
|
+
}
|
|
2610
|
+
console.log();
|
|
2611
|
+
console.log(` Warmed: ${res.warmed}`);
|
|
2612
|
+
console.log(` Skipped: ${res.skipped}`);
|
|
2613
|
+
console.log(` Failed: ${res.failed}`);
|
|
2614
|
+
console.log(` Duration: ${res.duration_ms}ms`);
|
|
2615
|
+
console.log(` Generated: ${formatBytes3(res.bytes_generated)}`);
|
|
2616
|
+
});
|
|
2617
|
+
}
|
|
2618
|
+
function formatBytes3(bytes) {
|
|
2619
|
+
if (bytes === 0) return "0 B";
|
|
2620
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
2621
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
2622
|
+
const value = bytes / 1024 ** i;
|
|
2623
|
+
return `${value.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
// src/commands/webhooks.ts
|
|
2627
|
+
import { confirm as confirm6 } from "@inquirer/prompts";
|
|
2628
|
+
import chalk8 from "chalk";
|
|
2629
|
+
function registerWebhooksCommands(program2) {
|
|
2630
|
+
const webhooks = program2.command("webhooks").description("Manage webhook endpoints");
|
|
2631
|
+
webhooks.command("list").description("List configured webhooks").action(async () => {
|
|
2632
|
+
const globalOpts = program2.opts();
|
|
2633
|
+
const client = createClientFromGlobalOpts(globalOpts);
|
|
2634
|
+
const res = await client.get("/api/webhooks");
|
|
2635
|
+
if (globalOpts.output === "json") {
|
|
2636
|
+
printJson(res);
|
|
2637
|
+
return;
|
|
2638
|
+
}
|
|
2639
|
+
if (res.endpoints.length === 0) {
|
|
2640
|
+
console.log("No webhooks configured.");
|
|
2641
|
+
return;
|
|
2642
|
+
}
|
|
2643
|
+
const rows = res.endpoints.map((ep) => ({
|
|
2644
|
+
id: String(ep.id),
|
|
2645
|
+
url: ep.url,
|
|
2646
|
+
events: ep.events.join(", "),
|
|
2647
|
+
active: ep.is_active ? "yes" : "no",
|
|
2648
|
+
created: formatDate4(ep.created_at)
|
|
2649
|
+
}));
|
|
2650
|
+
printTable(
|
|
2651
|
+
[
|
|
2652
|
+
{ header: "ID", key: "id" },
|
|
2653
|
+
{ header: "URL", key: "url" },
|
|
2654
|
+
{ header: "Events", key: "events" },
|
|
2655
|
+
{ header: "Active", key: "active" },
|
|
2656
|
+
{ header: "Created", key: "created" }
|
|
2657
|
+
],
|
|
2658
|
+
rows
|
|
2659
|
+
);
|
|
2660
|
+
console.log();
|
|
2661
|
+
console.log(chalk8.dim(`${res.endpoints.length}/${res.limit} endpoints used`));
|
|
2662
|
+
});
|
|
2663
|
+
webhooks.command("create").description("Create a webhook").requiredOption("--url <url>", "Webhook endpoint URL (must be HTTPS)").requiredOption("--events <events>", "Comma-separated event types").option("--description <text>", "Endpoint description").action(async (opts) => {
|
|
2664
|
+
const globalOpts = program2.opts();
|
|
2665
|
+
const client = createClientFromGlobalOpts(globalOpts);
|
|
2666
|
+
const events = opts.events.split(",").map((s) => s.trim());
|
|
2667
|
+
const res = await client.post("/api/webhooks", {
|
|
2668
|
+
url: opts.url,
|
|
2669
|
+
events,
|
|
2670
|
+
description: opts.description
|
|
2671
|
+
});
|
|
2672
|
+
if (globalOpts.output === "json") {
|
|
2673
|
+
printJson(res);
|
|
2674
|
+
return;
|
|
2675
|
+
}
|
|
2676
|
+
console.log(chalk8.green("Webhook created."));
|
|
2677
|
+
console.log();
|
|
2678
|
+
console.log(` ID: ${res.id}`);
|
|
2679
|
+
console.log(` URL: ${res.url}`);
|
|
2680
|
+
console.log(` Events: ${res.events.join(", ")}`);
|
|
2681
|
+
console.log();
|
|
2682
|
+
console.log(chalk8.yellow.bold(" Secret: " + res.secret));
|
|
2683
|
+
console.log(chalk8.yellow(" Save this secret now \u2014 it will not be shown again."));
|
|
2684
|
+
});
|
|
2685
|
+
webhooks.command("delete").description("Delete a webhook").argument("<id>", "Webhook ID").option("-y, --yes", "Skip confirmation prompt").action(async (id, opts) => {
|
|
2686
|
+
const globalOpts = program2.opts();
|
|
2687
|
+
const client = createClientFromGlobalOpts(globalOpts);
|
|
2688
|
+
if (!opts.yes) {
|
|
2689
|
+
const proceed = await confirm6({
|
|
2690
|
+
message: `Delete webhook ${id}?`,
|
|
2691
|
+
default: false
|
|
2692
|
+
});
|
|
2693
|
+
if (!proceed) {
|
|
2694
|
+
console.log("Aborted.");
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
await client.delete(`/api/webhooks/${id}`);
|
|
2699
|
+
console.log(chalk8.green(`Webhook ${id} deleted.`));
|
|
2700
|
+
});
|
|
2701
|
+
webhooks.command("test").description("Send a test event to a webhook").argument("<id>", "Webhook ID").action(async (id) => {
|
|
2702
|
+
const globalOpts = program2.opts();
|
|
2703
|
+
const client = createClientFromGlobalOpts(globalOpts);
|
|
2704
|
+
const res = await client.post(`/api/webhooks/${id}/test`);
|
|
2705
|
+
if (globalOpts.output === "json") {
|
|
2706
|
+
printJson(res);
|
|
2707
|
+
return;
|
|
2708
|
+
}
|
|
2709
|
+
if (res.success) {
|
|
2710
|
+
console.log(
|
|
2711
|
+
chalk8.green(`\u2713 Webhook responded with ${res.statusCode} in ${res.responseTime}ms`)
|
|
2712
|
+
);
|
|
2713
|
+
} else {
|
|
2714
|
+
console.log(
|
|
2715
|
+
chalk8.red(
|
|
2716
|
+
`\u2717 Webhook failed: ${res.error ?? `HTTP ${res.statusCode}`} (${res.responseTime}ms)`
|
|
2717
|
+
)
|
|
2718
|
+
);
|
|
2719
|
+
}
|
|
2720
|
+
});
|
|
2721
|
+
}
|
|
2722
|
+
function formatDate4(iso) {
|
|
2723
|
+
return new Date(iso).toLocaleDateString("en-CA");
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
// src/commands/config.ts
|
|
2727
|
+
import chalk9 from "chalk";
|
|
2728
|
+
var KNOWN_KEYS = {
|
|
2729
|
+
"defaults.output_format": "Default output format (table or json)",
|
|
2730
|
+
"defaults.color": "Enable colored output (true or false)"
|
|
2731
|
+
};
|
|
2732
|
+
function registerConfigCommands(program2) {
|
|
2733
|
+
const config = program2.command("config").description("View and modify CLI configuration");
|
|
2734
|
+
config.command("set").description("Set a config value").argument("<key>", "Config key (e.g. defaults.output_format)").argument("<value>", "Config value").action(async (key, value) => {
|
|
2735
|
+
const cfg = loadConfig();
|
|
2736
|
+
if (!KNOWN_KEYS[key]) {
|
|
2737
|
+
console.error(chalk9.yellow(`Warning: "${key}" is not a recognized config key.`));
|
|
2738
|
+
console.log("Known keys:");
|
|
2739
|
+
for (const [k, desc] of Object.entries(KNOWN_KEYS)) {
|
|
2740
|
+
console.log(` ${k} \u2014 ${desc}`);
|
|
2741
|
+
}
|
|
2742
|
+
return;
|
|
2743
|
+
}
|
|
2744
|
+
const parts = key.split(".");
|
|
2745
|
+
let target = cfg;
|
|
2746
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
2747
|
+
if (typeof target[parts[i]] !== "object" || target[parts[i]] === null) {
|
|
2748
|
+
target[parts[i]] = {};
|
|
2749
|
+
}
|
|
2750
|
+
target = target[parts[i]];
|
|
2751
|
+
}
|
|
2752
|
+
const finalKey = parts[parts.length - 1];
|
|
2753
|
+
const parsed = value === "true" ? true : value === "false" ? false : value;
|
|
2754
|
+
target[finalKey] = parsed;
|
|
2755
|
+
saveConfig(cfg);
|
|
2756
|
+
console.log(chalk9.green(`Set ${key} = ${value}`));
|
|
2757
|
+
});
|
|
2758
|
+
config.command("get").description("Get a config value").argument("<key>", "Config key (e.g. defaults.output_format)").action(async (key) => {
|
|
2759
|
+
const cfg = loadConfig();
|
|
2760
|
+
const parts = key.split(".");
|
|
2761
|
+
let value = cfg;
|
|
2762
|
+
for (const part of parts) {
|
|
2763
|
+
if (typeof value !== "object" || value === null) {
|
|
2764
|
+
console.error(chalk9.red(`Key "${key}" not found.`));
|
|
2765
|
+
process.exit(1);
|
|
2766
|
+
}
|
|
2767
|
+
value = value[part];
|
|
2768
|
+
}
|
|
2769
|
+
if (value === void 0) {
|
|
2770
|
+
console.error(chalk9.red(`Key "${key}" not found.`));
|
|
2771
|
+
process.exit(1);
|
|
2772
|
+
}
|
|
2773
|
+
console.log(String(value));
|
|
2774
|
+
});
|
|
2775
|
+
config.command("list").description("Show all configuration").action(async () => {
|
|
2776
|
+
const globalOpts = program2.opts();
|
|
2777
|
+
const cfg = loadConfig();
|
|
2778
|
+
const masked = structuredClone(cfg);
|
|
2779
|
+
for (const profile of Object.values(masked.profiles)) {
|
|
2780
|
+
profile.api_key = maskApiKey(profile.api_key);
|
|
2781
|
+
}
|
|
2782
|
+
if (globalOpts.output === "json") {
|
|
2783
|
+
printJson(masked);
|
|
2784
|
+
return;
|
|
2785
|
+
}
|
|
2786
|
+
console.log(chalk9.bold("Configuration"));
|
|
2787
|
+
console.log(chalk9.dim(`File: ${getConfigPath()}`));
|
|
2788
|
+
console.log();
|
|
2789
|
+
console.log(` current_profile: ${cfg.current_profile || chalk9.dim("(none)")}`);
|
|
2790
|
+
console.log();
|
|
2791
|
+
console.log(chalk9.bold(" Defaults"));
|
|
2792
|
+
console.log(` output_format: ${cfg.defaults.output_format}`);
|
|
2793
|
+
console.log(` color: ${cfg.defaults.color}`);
|
|
2794
|
+
const profileNames = Object.keys(cfg.profiles);
|
|
2795
|
+
if (profileNames.length > 0) {
|
|
2796
|
+
console.log();
|
|
2797
|
+
console.log(chalk9.bold(" Profiles"));
|
|
2798
|
+
for (const name of profileNames) {
|
|
2799
|
+
const p = cfg.profiles[name];
|
|
2800
|
+
const marker = name === cfg.current_profile ? chalk9.green(" *") : "";
|
|
2801
|
+
console.log(` ${name}${marker}`);
|
|
2802
|
+
console.log(` api_key: ${maskApiKey(p.api_key)}`);
|
|
2803
|
+
console.log(` api_url: ${p.api_url}`);
|
|
2804
|
+
console.log(` env: ${p.environment}`);
|
|
2805
|
+
console.log(` email: ${p.customer_email}`);
|
|
2806
|
+
console.log(` plan: ${p.plan}`);
|
|
2807
|
+
console.log(` subdomain: ${p.subdomain}`);
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
});
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
// src/commands/version.ts
|
|
2814
|
+
import { arch as arch2, platform as platform3 } from "os";
|
|
2815
|
+
function registerVersionCommand(program2) {
|
|
2816
|
+
program2.command("version").description("Show CLI version information").option("--json", "Output as JSON").action((opts) => {
|
|
2817
|
+
const info = {
|
|
2818
|
+
version: "0.1.1",
|
|
2819
|
+
node: process.version,
|
|
2820
|
+
platform: `${platform3()}/${arch2()}`
|
|
2821
|
+
};
|
|
2822
|
+
if (opts.json) {
|
|
2823
|
+
console.log(JSON.stringify(info, null, 2));
|
|
2824
|
+
} else {
|
|
2825
|
+
console.log(`viucraft ${info.version}`);
|
|
2826
|
+
console.log(` Node: ${info.node}`);
|
|
2827
|
+
console.log(` Platform: ${info.platform}`);
|
|
2828
|
+
}
|
|
2829
|
+
});
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
// src/index.ts
|
|
2833
|
+
var program = new Command();
|
|
2834
|
+
program.name("viucraft").description("Official CLI for the VIUCraft image manipulation API").version("0.1.1", "-v, --version").option("--api-key <key>", "API key (overrides profile config)").option("--profile <name>", "Named profile to use").option("--output <format>", "Output format: table, json", "table").option("--no-color", "Disable colored output").option("--debug", "Enable debug output (shows HTTP requests)").option("--insecure", "Allow HTTP connections (dev only)");
|
|
2835
|
+
registerAuthCommands(program);
|
|
2836
|
+
registerImagesCommands(program);
|
|
2837
|
+
registerUsageCommand(program);
|
|
2838
|
+
registerPresetsCommands(program);
|
|
2839
|
+
registerBatchCommands(program);
|
|
2840
|
+
registerCacheCommands(program);
|
|
2841
|
+
registerWebhooksCommands(program);
|
|
2842
|
+
registerConfigCommands(program);
|
|
2843
|
+
registerVersionCommand(program);
|
|
2844
|
+
var CMDS = "auth images usage presets batch cache webhooks config completion docs version";
|
|
2845
|
+
var SUBCMDS = {
|
|
2846
|
+
auth: "login status switch list logout",
|
|
2847
|
+
images: "list upload delete info transform download open",
|
|
2848
|
+
presets: "list get create delete",
|
|
2849
|
+
batch: "create list status stream cancel retry download",
|
|
2850
|
+
cache: "purge warm",
|
|
2851
|
+
webhooks: "list create delete test",
|
|
2852
|
+
config: "set get list",
|
|
2853
|
+
completion: "bash zsh fish"
|
|
2854
|
+
};
|
|
2855
|
+
function generateCompletion(shell) {
|
|
2856
|
+
switch (shell) {
|
|
2857
|
+
case "bash":
|
|
2858
|
+
return [
|
|
2859
|
+
"# viucraft bash completion",
|
|
2860
|
+
"_viucraft() {",
|
|
2861
|
+
" local cur prev cmds",
|
|
2862
|
+
' cur="${COMP_WORDS[COMP_CWORD]}"',
|
|
2863
|
+
' prev="${COMP_WORDS[COMP_CWORD-1]}"',
|
|
2864
|
+
` cmds="${CMDS}"`,
|
|
2865
|
+
' case "${prev}" in',
|
|
2866
|
+
...Object.entries(SUBCMDS).map(
|
|
2867
|
+
([cmd, subs]) => ` ${cmd}) COMPREPLY=( $(compgen -W "${subs}" -- "\${cur}") ); return ;;`
|
|
2868
|
+
),
|
|
2869
|
+
" esac",
|
|
2870
|
+
" if [[ ${COMP_CWORD} -eq 1 ]]; then",
|
|
2871
|
+
' COMPREPLY=( $(compgen -W "${cmds}" -- "${cur}") )',
|
|
2872
|
+
" fi",
|
|
2873
|
+
"}",
|
|
2874
|
+
"complete -F _viucraft viucraft"
|
|
2875
|
+
].join("\n");
|
|
2876
|
+
case "zsh":
|
|
2877
|
+
return [
|
|
2878
|
+
"#compdef viucraft",
|
|
2879
|
+
"_viucraft() {",
|
|
2880
|
+
" local -a commands",
|
|
2881
|
+
" commands=(",
|
|
2882
|
+
...CMDS.split(" ").map((c) => ` '${c}:${c} command'`),
|
|
2883
|
+
" )",
|
|
2884
|
+
" _arguments '1: :->cmd' '2: :->sub'",
|
|
2885
|
+
" case $state in",
|
|
2886
|
+
" cmd) _describe 'command' commands ;;",
|
|
2887
|
+
" sub)",
|
|
2888
|
+
" case $words[2] in",
|
|
2889
|
+
...Object.entries(SUBCMDS).map(([cmd, subs]) => ` ${cmd}) compadd ${subs} ;;`),
|
|
2890
|
+
" esac ;;",
|
|
2891
|
+
" esac",
|
|
2892
|
+
"}",
|
|
2893
|
+
"_viucraft"
|
|
2894
|
+
].join("\n");
|
|
2895
|
+
case "fish":
|
|
2896
|
+
return [
|
|
2897
|
+
"# viucraft fish completion",
|
|
2898
|
+
...CMDS.split(" ").map((c) => `complete -c viucraft -n '__fish_use_subcommand' -a '${c}'`),
|
|
2899
|
+
...Object.entries(SUBCMDS).flatMap(
|
|
2900
|
+
([cmd, subs]) => subs.split(" ").map((s) => `complete -c viucraft -n '__fish_seen_subcommand_from ${cmd}' -a '${s}'`)
|
|
2901
|
+
)
|
|
2902
|
+
].join("\n");
|
|
2903
|
+
default:
|
|
2904
|
+
return `# Unsupported shell: ${shell}`;
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
var completion = program.command("completion").description("Generate shell completion scripts");
|
|
2908
|
+
for (const shell of ["bash", "zsh", "fish"]) {
|
|
2909
|
+
completion.command(shell).description(`Output ${shell} completion script`).action(() => {
|
|
2910
|
+
console.log(generateCompletion(shell));
|
|
2911
|
+
});
|
|
2912
|
+
}
|
|
2913
|
+
program.command("docs").description("Open VIUCraft documentation in browser").action(() => {
|
|
2914
|
+
const url = "https://docs.viucraft.com/cli";
|
|
2915
|
+
openInBrowser(url);
|
|
2916
|
+
});
|
|
2917
|
+
program.parseAsync().catch((err) => {
|
|
2918
|
+
if (err instanceof CliError) {
|
|
2919
|
+
console.error(err.message);
|
|
2920
|
+
if (err.requestId) {
|
|
2921
|
+
console.error(`Request ID: ${err.requestId}`);
|
|
2922
|
+
}
|
|
2923
|
+
process.exit(err.exitCode);
|
|
2924
|
+
}
|
|
2925
|
+
console.error(err.message ?? err);
|
|
2926
|
+
process.exit(1);
|
|
2927
|
+
});
|
|
2928
|
+
//# sourceMappingURL=index.js.map
|