picture-it 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -29
- package/dist/index.js +4870 -0
- package/fonts/DMSerifDisplay-Regular.ttf +0 -0
- package/fonts/Inter-Bold.ttf +0 -0
- package/fonts/Inter-Regular.ttf +0 -0
- package/fonts/Inter-SemiBold.ttf +0 -0
- package/fonts/SpaceGrotesk-Bold.ttf +0 -0
- package/fonts/SpaceGrotesk-Medium.ttf +0 -0
- package/hero-v2.png +0 -0
- package/package.json +9 -8
- package/hero.png +0 -0
- package/index.ts +0 -493
- package/scripts/download-fonts.ts +0 -14
- package/src/compositor.ts +0 -614
- package/src/config.ts +0 -102
- package/src/contrast.ts +0 -155
- package/src/fal.ts +0 -218
- package/src/fonts.ts +0 -165
- package/src/model-router.ts +0 -78
- package/src/operations.ts +0 -85
- package/src/pipeline.ts +0 -243
- package/src/postprocess.ts +0 -124
- package/src/presets.ts +0 -105
- package/src/satori-jsx.ts +0 -17
- package/src/templates/index.ts +0 -457
- package/src/types.ts +0 -226
- package/src/zones.ts +0 -63
package/dist/index.js
ADDED
|
@@ -0,0 +1,4870 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @bun
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
var __create = Object.create;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __defProp = Object.defineProperty;
|
|
7
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
10
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
11
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
12
|
+
for (let key of __getOwnPropNames(mod))
|
|
13
|
+
if (!__hasOwnProp.call(to, key))
|
|
14
|
+
__defProp(to, key, {
|
|
15
|
+
get: () => mod[key],
|
|
16
|
+
enumerable: true
|
|
17
|
+
});
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
21
|
+
var __export = (target, all) => {
|
|
22
|
+
for (var name in all)
|
|
23
|
+
__defProp(target, name, {
|
|
24
|
+
get: all[name],
|
|
25
|
+
enumerable: true,
|
|
26
|
+
configurable: true,
|
|
27
|
+
set: (newValue) => all[name] = () => newValue
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
31
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
32
|
+
|
|
33
|
+
// node_modules/commander/lib/error.js
|
|
34
|
+
var require_error = __commonJS((exports) => {
|
|
35
|
+
class CommanderError extends Error {
|
|
36
|
+
constructor(exitCode, code, message) {
|
|
37
|
+
super(message);
|
|
38
|
+
Error.captureStackTrace(this, this.constructor);
|
|
39
|
+
this.name = this.constructor.name;
|
|
40
|
+
this.code = code;
|
|
41
|
+
this.exitCode = exitCode;
|
|
42
|
+
this.nestedError = undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
class InvalidArgumentError extends CommanderError {
|
|
47
|
+
constructor(message) {
|
|
48
|
+
super(1, "commander.invalidArgument", message);
|
|
49
|
+
Error.captureStackTrace(this, this.constructor);
|
|
50
|
+
this.name = this.constructor.name;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
exports.CommanderError = CommanderError;
|
|
54
|
+
exports.InvalidArgumentError = InvalidArgumentError;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// node_modules/commander/lib/argument.js
|
|
58
|
+
var require_argument = __commonJS((exports) => {
|
|
59
|
+
var { InvalidArgumentError } = require_error();
|
|
60
|
+
|
|
61
|
+
class Argument {
|
|
62
|
+
constructor(name, description) {
|
|
63
|
+
this.description = description || "";
|
|
64
|
+
this.variadic = false;
|
|
65
|
+
this.parseArg = undefined;
|
|
66
|
+
this.defaultValue = undefined;
|
|
67
|
+
this.defaultValueDescription = undefined;
|
|
68
|
+
this.argChoices = undefined;
|
|
69
|
+
switch (name[0]) {
|
|
70
|
+
case "<":
|
|
71
|
+
this.required = true;
|
|
72
|
+
this._name = name.slice(1, -1);
|
|
73
|
+
break;
|
|
74
|
+
case "[":
|
|
75
|
+
this.required = false;
|
|
76
|
+
this._name = name.slice(1, -1);
|
|
77
|
+
break;
|
|
78
|
+
default:
|
|
79
|
+
this.required = true;
|
|
80
|
+
this._name = name;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
if (this._name.endsWith("...")) {
|
|
84
|
+
this.variadic = true;
|
|
85
|
+
this._name = this._name.slice(0, -3);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
name() {
|
|
89
|
+
return this._name;
|
|
90
|
+
}
|
|
91
|
+
_collectValue(value, previous) {
|
|
92
|
+
if (previous === this.defaultValue || !Array.isArray(previous)) {
|
|
93
|
+
return [value];
|
|
94
|
+
}
|
|
95
|
+
previous.push(value);
|
|
96
|
+
return previous;
|
|
97
|
+
}
|
|
98
|
+
default(value, description) {
|
|
99
|
+
this.defaultValue = value;
|
|
100
|
+
this.defaultValueDescription = description;
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
argParser(fn) {
|
|
104
|
+
this.parseArg = fn;
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
choices(values) {
|
|
108
|
+
this.argChoices = values.slice();
|
|
109
|
+
this.parseArg = (arg, previous) => {
|
|
110
|
+
if (!this.argChoices.includes(arg)) {
|
|
111
|
+
throw new InvalidArgumentError(`Allowed choices are ${this.argChoices.join(", ")}.`);
|
|
112
|
+
}
|
|
113
|
+
if (this.variadic) {
|
|
114
|
+
return this._collectValue(arg, previous);
|
|
115
|
+
}
|
|
116
|
+
return arg;
|
|
117
|
+
};
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
argRequired() {
|
|
121
|
+
this.required = true;
|
|
122
|
+
return this;
|
|
123
|
+
}
|
|
124
|
+
argOptional() {
|
|
125
|
+
this.required = false;
|
|
126
|
+
return this;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function humanReadableArgName(arg) {
|
|
130
|
+
const nameOutput = arg.name() + (arg.variadic === true ? "..." : "");
|
|
131
|
+
return arg.required ? "<" + nameOutput + ">" : "[" + nameOutput + "]";
|
|
132
|
+
}
|
|
133
|
+
exports.Argument = Argument;
|
|
134
|
+
exports.humanReadableArgName = humanReadableArgName;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// node_modules/commander/lib/help.js
|
|
138
|
+
var require_help = __commonJS((exports) => {
|
|
139
|
+
var { humanReadableArgName } = require_argument();
|
|
140
|
+
|
|
141
|
+
class Help {
|
|
142
|
+
constructor() {
|
|
143
|
+
this.helpWidth = undefined;
|
|
144
|
+
this.minWidthToWrap = 40;
|
|
145
|
+
this.sortSubcommands = false;
|
|
146
|
+
this.sortOptions = false;
|
|
147
|
+
this.showGlobalOptions = false;
|
|
148
|
+
}
|
|
149
|
+
prepareContext(contextOptions) {
|
|
150
|
+
this.helpWidth = this.helpWidth ?? contextOptions.helpWidth ?? 80;
|
|
151
|
+
}
|
|
152
|
+
visibleCommands(cmd) {
|
|
153
|
+
const visibleCommands = cmd.commands.filter((cmd2) => !cmd2._hidden);
|
|
154
|
+
const helpCommand = cmd._getHelpCommand();
|
|
155
|
+
if (helpCommand && !helpCommand._hidden) {
|
|
156
|
+
visibleCommands.push(helpCommand);
|
|
157
|
+
}
|
|
158
|
+
if (this.sortSubcommands) {
|
|
159
|
+
visibleCommands.sort((a, b) => {
|
|
160
|
+
return a.name().localeCompare(b.name());
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return visibleCommands;
|
|
164
|
+
}
|
|
165
|
+
compareOptions(a, b) {
|
|
166
|
+
const getSortKey = (option) => {
|
|
167
|
+
return option.short ? option.short.replace(/^-/, "") : option.long.replace(/^--/, "");
|
|
168
|
+
};
|
|
169
|
+
return getSortKey(a).localeCompare(getSortKey(b));
|
|
170
|
+
}
|
|
171
|
+
visibleOptions(cmd) {
|
|
172
|
+
const visibleOptions = cmd.options.filter((option) => !option.hidden);
|
|
173
|
+
const helpOption = cmd._getHelpOption();
|
|
174
|
+
if (helpOption && !helpOption.hidden) {
|
|
175
|
+
const removeShort = helpOption.short && cmd._findOption(helpOption.short);
|
|
176
|
+
const removeLong = helpOption.long && cmd._findOption(helpOption.long);
|
|
177
|
+
if (!removeShort && !removeLong) {
|
|
178
|
+
visibleOptions.push(helpOption);
|
|
179
|
+
} else if (helpOption.long && !removeLong) {
|
|
180
|
+
visibleOptions.push(cmd.createOption(helpOption.long, helpOption.description));
|
|
181
|
+
} else if (helpOption.short && !removeShort) {
|
|
182
|
+
visibleOptions.push(cmd.createOption(helpOption.short, helpOption.description));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (this.sortOptions) {
|
|
186
|
+
visibleOptions.sort(this.compareOptions);
|
|
187
|
+
}
|
|
188
|
+
return visibleOptions;
|
|
189
|
+
}
|
|
190
|
+
visibleGlobalOptions(cmd) {
|
|
191
|
+
if (!this.showGlobalOptions)
|
|
192
|
+
return [];
|
|
193
|
+
const globalOptions = [];
|
|
194
|
+
for (let ancestorCmd = cmd.parent;ancestorCmd; ancestorCmd = ancestorCmd.parent) {
|
|
195
|
+
const visibleOptions = ancestorCmd.options.filter((option) => !option.hidden);
|
|
196
|
+
globalOptions.push(...visibleOptions);
|
|
197
|
+
}
|
|
198
|
+
if (this.sortOptions) {
|
|
199
|
+
globalOptions.sort(this.compareOptions);
|
|
200
|
+
}
|
|
201
|
+
return globalOptions;
|
|
202
|
+
}
|
|
203
|
+
visibleArguments(cmd) {
|
|
204
|
+
if (cmd._argsDescription) {
|
|
205
|
+
cmd.registeredArguments.forEach((argument) => {
|
|
206
|
+
argument.description = argument.description || cmd._argsDescription[argument.name()] || "";
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
if (cmd.registeredArguments.find((argument) => argument.description)) {
|
|
210
|
+
return cmd.registeredArguments;
|
|
211
|
+
}
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
subcommandTerm(cmd) {
|
|
215
|
+
const args = cmd.registeredArguments.map((arg) => humanReadableArgName(arg)).join(" ");
|
|
216
|
+
return cmd._name + (cmd._aliases[0] ? "|" + cmd._aliases[0] : "") + (cmd.options.length ? " [options]" : "") + (args ? " " + args : "");
|
|
217
|
+
}
|
|
218
|
+
optionTerm(option) {
|
|
219
|
+
return option.flags;
|
|
220
|
+
}
|
|
221
|
+
argumentTerm(argument) {
|
|
222
|
+
return argument.name();
|
|
223
|
+
}
|
|
224
|
+
longestSubcommandTermLength(cmd, helper) {
|
|
225
|
+
return helper.visibleCommands(cmd).reduce((max, command) => {
|
|
226
|
+
return Math.max(max, this.displayWidth(helper.styleSubcommandTerm(helper.subcommandTerm(command))));
|
|
227
|
+
}, 0);
|
|
228
|
+
}
|
|
229
|
+
longestOptionTermLength(cmd, helper) {
|
|
230
|
+
return helper.visibleOptions(cmd).reduce((max, option) => {
|
|
231
|
+
return Math.max(max, this.displayWidth(helper.styleOptionTerm(helper.optionTerm(option))));
|
|
232
|
+
}, 0);
|
|
233
|
+
}
|
|
234
|
+
longestGlobalOptionTermLength(cmd, helper) {
|
|
235
|
+
return helper.visibleGlobalOptions(cmd).reduce((max, option) => {
|
|
236
|
+
return Math.max(max, this.displayWidth(helper.styleOptionTerm(helper.optionTerm(option))));
|
|
237
|
+
}, 0);
|
|
238
|
+
}
|
|
239
|
+
longestArgumentTermLength(cmd, helper) {
|
|
240
|
+
return helper.visibleArguments(cmd).reduce((max, argument) => {
|
|
241
|
+
return Math.max(max, this.displayWidth(helper.styleArgumentTerm(helper.argumentTerm(argument))));
|
|
242
|
+
}, 0);
|
|
243
|
+
}
|
|
244
|
+
commandUsage(cmd) {
|
|
245
|
+
let cmdName = cmd._name;
|
|
246
|
+
if (cmd._aliases[0]) {
|
|
247
|
+
cmdName = cmdName + "|" + cmd._aliases[0];
|
|
248
|
+
}
|
|
249
|
+
let ancestorCmdNames = "";
|
|
250
|
+
for (let ancestorCmd = cmd.parent;ancestorCmd; ancestorCmd = ancestorCmd.parent) {
|
|
251
|
+
ancestorCmdNames = ancestorCmd.name() + " " + ancestorCmdNames;
|
|
252
|
+
}
|
|
253
|
+
return ancestorCmdNames + cmdName + " " + cmd.usage();
|
|
254
|
+
}
|
|
255
|
+
commandDescription(cmd) {
|
|
256
|
+
return cmd.description();
|
|
257
|
+
}
|
|
258
|
+
subcommandDescription(cmd) {
|
|
259
|
+
return cmd.summary() || cmd.description();
|
|
260
|
+
}
|
|
261
|
+
optionDescription(option) {
|
|
262
|
+
const extraInfo = [];
|
|
263
|
+
if (option.argChoices) {
|
|
264
|
+
extraInfo.push(`choices: ${option.argChoices.map((choice) => JSON.stringify(choice)).join(", ")}`);
|
|
265
|
+
}
|
|
266
|
+
if (option.defaultValue !== undefined) {
|
|
267
|
+
const showDefault = option.required || option.optional || option.isBoolean() && typeof option.defaultValue === "boolean";
|
|
268
|
+
if (showDefault) {
|
|
269
|
+
extraInfo.push(`default: ${option.defaultValueDescription || JSON.stringify(option.defaultValue)}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (option.presetArg !== undefined && option.optional) {
|
|
273
|
+
extraInfo.push(`preset: ${JSON.stringify(option.presetArg)}`);
|
|
274
|
+
}
|
|
275
|
+
if (option.envVar !== undefined) {
|
|
276
|
+
extraInfo.push(`env: ${option.envVar}`);
|
|
277
|
+
}
|
|
278
|
+
if (extraInfo.length > 0) {
|
|
279
|
+
const extraDescription = `(${extraInfo.join(", ")})`;
|
|
280
|
+
if (option.description) {
|
|
281
|
+
return `${option.description} ${extraDescription}`;
|
|
282
|
+
}
|
|
283
|
+
return extraDescription;
|
|
284
|
+
}
|
|
285
|
+
return option.description;
|
|
286
|
+
}
|
|
287
|
+
argumentDescription(argument) {
|
|
288
|
+
const extraInfo = [];
|
|
289
|
+
if (argument.argChoices) {
|
|
290
|
+
extraInfo.push(`choices: ${argument.argChoices.map((choice) => JSON.stringify(choice)).join(", ")}`);
|
|
291
|
+
}
|
|
292
|
+
if (argument.defaultValue !== undefined) {
|
|
293
|
+
extraInfo.push(`default: ${argument.defaultValueDescription || JSON.stringify(argument.defaultValue)}`);
|
|
294
|
+
}
|
|
295
|
+
if (extraInfo.length > 0) {
|
|
296
|
+
const extraDescription = `(${extraInfo.join(", ")})`;
|
|
297
|
+
if (argument.description) {
|
|
298
|
+
return `${argument.description} ${extraDescription}`;
|
|
299
|
+
}
|
|
300
|
+
return extraDescription;
|
|
301
|
+
}
|
|
302
|
+
return argument.description;
|
|
303
|
+
}
|
|
304
|
+
formatItemList(heading, items, helper) {
|
|
305
|
+
if (items.length === 0)
|
|
306
|
+
return [];
|
|
307
|
+
return [helper.styleTitle(heading), ...items, ""];
|
|
308
|
+
}
|
|
309
|
+
groupItems(unsortedItems, visibleItems, getGroup) {
|
|
310
|
+
const result = new Map;
|
|
311
|
+
unsortedItems.forEach((item) => {
|
|
312
|
+
const group = getGroup(item);
|
|
313
|
+
if (!result.has(group))
|
|
314
|
+
result.set(group, []);
|
|
315
|
+
});
|
|
316
|
+
visibleItems.forEach((item) => {
|
|
317
|
+
const group = getGroup(item);
|
|
318
|
+
if (!result.has(group)) {
|
|
319
|
+
result.set(group, []);
|
|
320
|
+
}
|
|
321
|
+
result.get(group).push(item);
|
|
322
|
+
});
|
|
323
|
+
return result;
|
|
324
|
+
}
|
|
325
|
+
formatHelp(cmd, helper) {
|
|
326
|
+
const termWidth = helper.padWidth(cmd, helper);
|
|
327
|
+
const helpWidth = helper.helpWidth ?? 80;
|
|
328
|
+
function callFormatItem(term, description) {
|
|
329
|
+
return helper.formatItem(term, termWidth, description, helper);
|
|
330
|
+
}
|
|
331
|
+
let output = [
|
|
332
|
+
`${helper.styleTitle("Usage:")} ${helper.styleUsage(helper.commandUsage(cmd))}`,
|
|
333
|
+
""
|
|
334
|
+
];
|
|
335
|
+
const commandDescription = helper.commandDescription(cmd);
|
|
336
|
+
if (commandDescription.length > 0) {
|
|
337
|
+
output = output.concat([
|
|
338
|
+
helper.boxWrap(helper.styleCommandDescription(commandDescription), helpWidth),
|
|
339
|
+
""
|
|
340
|
+
]);
|
|
341
|
+
}
|
|
342
|
+
const argumentList = helper.visibleArguments(cmd).map((argument) => {
|
|
343
|
+
return callFormatItem(helper.styleArgumentTerm(helper.argumentTerm(argument)), helper.styleArgumentDescription(helper.argumentDescription(argument)));
|
|
344
|
+
});
|
|
345
|
+
output = output.concat(this.formatItemList("Arguments:", argumentList, helper));
|
|
346
|
+
const optionGroups = this.groupItems(cmd.options, helper.visibleOptions(cmd), (option) => option.helpGroupHeading ?? "Options:");
|
|
347
|
+
optionGroups.forEach((options, group) => {
|
|
348
|
+
const optionList = options.map((option) => {
|
|
349
|
+
return callFormatItem(helper.styleOptionTerm(helper.optionTerm(option)), helper.styleOptionDescription(helper.optionDescription(option)));
|
|
350
|
+
});
|
|
351
|
+
output = output.concat(this.formatItemList(group, optionList, helper));
|
|
352
|
+
});
|
|
353
|
+
if (helper.showGlobalOptions) {
|
|
354
|
+
const globalOptionList = helper.visibleGlobalOptions(cmd).map((option) => {
|
|
355
|
+
return callFormatItem(helper.styleOptionTerm(helper.optionTerm(option)), helper.styleOptionDescription(helper.optionDescription(option)));
|
|
356
|
+
});
|
|
357
|
+
output = output.concat(this.formatItemList("Global Options:", globalOptionList, helper));
|
|
358
|
+
}
|
|
359
|
+
const commandGroups = this.groupItems(cmd.commands, helper.visibleCommands(cmd), (sub) => sub.helpGroup() || "Commands:");
|
|
360
|
+
commandGroups.forEach((commands, group) => {
|
|
361
|
+
const commandList = commands.map((sub) => {
|
|
362
|
+
return callFormatItem(helper.styleSubcommandTerm(helper.subcommandTerm(sub)), helper.styleSubcommandDescription(helper.subcommandDescription(sub)));
|
|
363
|
+
});
|
|
364
|
+
output = output.concat(this.formatItemList(group, commandList, helper));
|
|
365
|
+
});
|
|
366
|
+
return output.join(`
|
|
367
|
+
`);
|
|
368
|
+
}
|
|
369
|
+
displayWidth(str) {
|
|
370
|
+
return stripColor(str).length;
|
|
371
|
+
}
|
|
372
|
+
styleTitle(str) {
|
|
373
|
+
return str;
|
|
374
|
+
}
|
|
375
|
+
styleUsage(str) {
|
|
376
|
+
return str.split(" ").map((word) => {
|
|
377
|
+
if (word === "[options]")
|
|
378
|
+
return this.styleOptionText(word);
|
|
379
|
+
if (word === "[command]")
|
|
380
|
+
return this.styleSubcommandText(word);
|
|
381
|
+
if (word[0] === "[" || word[0] === "<")
|
|
382
|
+
return this.styleArgumentText(word);
|
|
383
|
+
return this.styleCommandText(word);
|
|
384
|
+
}).join(" ");
|
|
385
|
+
}
|
|
386
|
+
styleCommandDescription(str) {
|
|
387
|
+
return this.styleDescriptionText(str);
|
|
388
|
+
}
|
|
389
|
+
styleOptionDescription(str) {
|
|
390
|
+
return this.styleDescriptionText(str);
|
|
391
|
+
}
|
|
392
|
+
styleSubcommandDescription(str) {
|
|
393
|
+
return this.styleDescriptionText(str);
|
|
394
|
+
}
|
|
395
|
+
styleArgumentDescription(str) {
|
|
396
|
+
return this.styleDescriptionText(str);
|
|
397
|
+
}
|
|
398
|
+
styleDescriptionText(str) {
|
|
399
|
+
return str;
|
|
400
|
+
}
|
|
401
|
+
styleOptionTerm(str) {
|
|
402
|
+
return this.styleOptionText(str);
|
|
403
|
+
}
|
|
404
|
+
styleSubcommandTerm(str) {
|
|
405
|
+
return str.split(" ").map((word) => {
|
|
406
|
+
if (word === "[options]")
|
|
407
|
+
return this.styleOptionText(word);
|
|
408
|
+
if (word[0] === "[" || word[0] === "<")
|
|
409
|
+
return this.styleArgumentText(word);
|
|
410
|
+
return this.styleSubcommandText(word);
|
|
411
|
+
}).join(" ");
|
|
412
|
+
}
|
|
413
|
+
styleArgumentTerm(str) {
|
|
414
|
+
return this.styleArgumentText(str);
|
|
415
|
+
}
|
|
416
|
+
styleOptionText(str) {
|
|
417
|
+
return str;
|
|
418
|
+
}
|
|
419
|
+
styleArgumentText(str) {
|
|
420
|
+
return str;
|
|
421
|
+
}
|
|
422
|
+
styleSubcommandText(str) {
|
|
423
|
+
return str;
|
|
424
|
+
}
|
|
425
|
+
styleCommandText(str) {
|
|
426
|
+
return str;
|
|
427
|
+
}
|
|
428
|
+
padWidth(cmd, helper) {
|
|
429
|
+
return Math.max(helper.longestOptionTermLength(cmd, helper), helper.longestGlobalOptionTermLength(cmd, helper), helper.longestSubcommandTermLength(cmd, helper), helper.longestArgumentTermLength(cmd, helper));
|
|
430
|
+
}
|
|
431
|
+
preformatted(str) {
|
|
432
|
+
return /\n[^\S\r\n]/.test(str);
|
|
433
|
+
}
|
|
434
|
+
formatItem(term, termWidth, description, helper) {
|
|
435
|
+
const itemIndent = 2;
|
|
436
|
+
const itemIndentStr = " ".repeat(itemIndent);
|
|
437
|
+
if (!description)
|
|
438
|
+
return itemIndentStr + term;
|
|
439
|
+
const paddedTerm = term.padEnd(termWidth + term.length - helper.displayWidth(term));
|
|
440
|
+
const spacerWidth = 2;
|
|
441
|
+
const helpWidth = this.helpWidth ?? 80;
|
|
442
|
+
const remainingWidth = helpWidth - termWidth - spacerWidth - itemIndent;
|
|
443
|
+
let formattedDescription;
|
|
444
|
+
if (remainingWidth < this.minWidthToWrap || helper.preformatted(description)) {
|
|
445
|
+
formattedDescription = description;
|
|
446
|
+
} else {
|
|
447
|
+
const wrappedDescription = helper.boxWrap(description, remainingWidth);
|
|
448
|
+
formattedDescription = wrappedDescription.replace(/\n/g, `
|
|
449
|
+
` + " ".repeat(termWidth + spacerWidth));
|
|
450
|
+
}
|
|
451
|
+
return itemIndentStr + paddedTerm + " ".repeat(spacerWidth) + formattedDescription.replace(/\n/g, `
|
|
452
|
+
${itemIndentStr}`);
|
|
453
|
+
}
|
|
454
|
+
boxWrap(str, width) {
|
|
455
|
+
if (width < this.minWidthToWrap)
|
|
456
|
+
return str;
|
|
457
|
+
const rawLines = str.split(/\r\n|\n/);
|
|
458
|
+
const chunkPattern = /[\s]*[^\s]+/g;
|
|
459
|
+
const wrappedLines = [];
|
|
460
|
+
rawLines.forEach((line) => {
|
|
461
|
+
const chunks = line.match(chunkPattern);
|
|
462
|
+
if (chunks === null) {
|
|
463
|
+
wrappedLines.push("");
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
let sumChunks = [chunks.shift()];
|
|
467
|
+
let sumWidth = this.displayWidth(sumChunks[0]);
|
|
468
|
+
chunks.forEach((chunk) => {
|
|
469
|
+
const visibleWidth = this.displayWidth(chunk);
|
|
470
|
+
if (sumWidth + visibleWidth <= width) {
|
|
471
|
+
sumChunks.push(chunk);
|
|
472
|
+
sumWidth += visibleWidth;
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
wrappedLines.push(sumChunks.join(""));
|
|
476
|
+
const nextChunk = chunk.trimStart();
|
|
477
|
+
sumChunks = [nextChunk];
|
|
478
|
+
sumWidth = this.displayWidth(nextChunk);
|
|
479
|
+
});
|
|
480
|
+
wrappedLines.push(sumChunks.join(""));
|
|
481
|
+
});
|
|
482
|
+
return wrappedLines.join(`
|
|
483
|
+
`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function stripColor(str) {
|
|
487
|
+
const sgrPattern = /\x1b\[\d*(;\d*)*m/g;
|
|
488
|
+
return str.replace(sgrPattern, "");
|
|
489
|
+
}
|
|
490
|
+
exports.Help = Help;
|
|
491
|
+
exports.stripColor = stripColor;
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// node_modules/commander/lib/option.js
|
|
495
|
+
var require_option = __commonJS((exports) => {
|
|
496
|
+
var { InvalidArgumentError } = require_error();
|
|
497
|
+
|
|
498
|
+
class Option {
|
|
499
|
+
constructor(flags, description) {
|
|
500
|
+
this.flags = flags;
|
|
501
|
+
this.description = description || "";
|
|
502
|
+
this.required = flags.includes("<");
|
|
503
|
+
this.optional = flags.includes("[");
|
|
504
|
+
this.variadic = /\w\.\.\.[>\]]$/.test(flags);
|
|
505
|
+
this.mandatory = false;
|
|
506
|
+
const optionFlags = splitOptionFlags(flags);
|
|
507
|
+
this.short = optionFlags.shortFlag;
|
|
508
|
+
this.long = optionFlags.longFlag;
|
|
509
|
+
this.negate = false;
|
|
510
|
+
if (this.long) {
|
|
511
|
+
this.negate = this.long.startsWith("--no-");
|
|
512
|
+
}
|
|
513
|
+
this.defaultValue = undefined;
|
|
514
|
+
this.defaultValueDescription = undefined;
|
|
515
|
+
this.presetArg = undefined;
|
|
516
|
+
this.envVar = undefined;
|
|
517
|
+
this.parseArg = undefined;
|
|
518
|
+
this.hidden = false;
|
|
519
|
+
this.argChoices = undefined;
|
|
520
|
+
this.conflictsWith = [];
|
|
521
|
+
this.implied = undefined;
|
|
522
|
+
this.helpGroupHeading = undefined;
|
|
523
|
+
}
|
|
524
|
+
default(value, description) {
|
|
525
|
+
this.defaultValue = value;
|
|
526
|
+
this.defaultValueDescription = description;
|
|
527
|
+
return this;
|
|
528
|
+
}
|
|
529
|
+
preset(arg) {
|
|
530
|
+
this.presetArg = arg;
|
|
531
|
+
return this;
|
|
532
|
+
}
|
|
533
|
+
conflicts(names) {
|
|
534
|
+
this.conflictsWith = this.conflictsWith.concat(names);
|
|
535
|
+
return this;
|
|
536
|
+
}
|
|
537
|
+
implies(impliedOptionValues) {
|
|
538
|
+
let newImplied = impliedOptionValues;
|
|
539
|
+
if (typeof impliedOptionValues === "string") {
|
|
540
|
+
newImplied = { [impliedOptionValues]: true };
|
|
541
|
+
}
|
|
542
|
+
this.implied = Object.assign(this.implied || {}, newImplied);
|
|
543
|
+
return this;
|
|
544
|
+
}
|
|
545
|
+
env(name) {
|
|
546
|
+
this.envVar = name;
|
|
547
|
+
return this;
|
|
548
|
+
}
|
|
549
|
+
argParser(fn) {
|
|
550
|
+
this.parseArg = fn;
|
|
551
|
+
return this;
|
|
552
|
+
}
|
|
553
|
+
makeOptionMandatory(mandatory = true) {
|
|
554
|
+
this.mandatory = !!mandatory;
|
|
555
|
+
return this;
|
|
556
|
+
}
|
|
557
|
+
hideHelp(hide = true) {
|
|
558
|
+
this.hidden = !!hide;
|
|
559
|
+
return this;
|
|
560
|
+
}
|
|
561
|
+
_collectValue(value, previous) {
|
|
562
|
+
if (previous === this.defaultValue || !Array.isArray(previous)) {
|
|
563
|
+
return [value];
|
|
564
|
+
}
|
|
565
|
+
previous.push(value);
|
|
566
|
+
return previous;
|
|
567
|
+
}
|
|
568
|
+
choices(values) {
|
|
569
|
+
this.argChoices = values.slice();
|
|
570
|
+
this.parseArg = (arg, previous) => {
|
|
571
|
+
if (!this.argChoices.includes(arg)) {
|
|
572
|
+
throw new InvalidArgumentError(`Allowed choices are ${this.argChoices.join(", ")}.`);
|
|
573
|
+
}
|
|
574
|
+
if (this.variadic) {
|
|
575
|
+
return this._collectValue(arg, previous);
|
|
576
|
+
}
|
|
577
|
+
return arg;
|
|
578
|
+
};
|
|
579
|
+
return this;
|
|
580
|
+
}
|
|
581
|
+
name() {
|
|
582
|
+
if (this.long) {
|
|
583
|
+
return this.long.replace(/^--/, "");
|
|
584
|
+
}
|
|
585
|
+
return this.short.replace(/^-/, "");
|
|
586
|
+
}
|
|
587
|
+
attributeName() {
|
|
588
|
+
if (this.negate) {
|
|
589
|
+
return camelcase(this.name().replace(/^no-/, ""));
|
|
590
|
+
}
|
|
591
|
+
return camelcase(this.name());
|
|
592
|
+
}
|
|
593
|
+
helpGroup(heading) {
|
|
594
|
+
this.helpGroupHeading = heading;
|
|
595
|
+
return this;
|
|
596
|
+
}
|
|
597
|
+
is(arg) {
|
|
598
|
+
return this.short === arg || this.long === arg;
|
|
599
|
+
}
|
|
600
|
+
isBoolean() {
|
|
601
|
+
return !this.required && !this.optional && !this.negate;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
class DualOptions {
|
|
606
|
+
constructor(options) {
|
|
607
|
+
this.positiveOptions = new Map;
|
|
608
|
+
this.negativeOptions = new Map;
|
|
609
|
+
this.dualOptions = new Set;
|
|
610
|
+
options.forEach((option) => {
|
|
611
|
+
if (option.negate) {
|
|
612
|
+
this.negativeOptions.set(option.attributeName(), option);
|
|
613
|
+
} else {
|
|
614
|
+
this.positiveOptions.set(option.attributeName(), option);
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
this.negativeOptions.forEach((value, key) => {
|
|
618
|
+
if (this.positiveOptions.has(key)) {
|
|
619
|
+
this.dualOptions.add(key);
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
valueFromOption(value, option) {
|
|
624
|
+
const optionKey = option.attributeName();
|
|
625
|
+
if (!this.dualOptions.has(optionKey))
|
|
626
|
+
return true;
|
|
627
|
+
const preset = this.negativeOptions.get(optionKey).presetArg;
|
|
628
|
+
const negativeValue = preset !== undefined ? preset : false;
|
|
629
|
+
return option.negate === (negativeValue === value);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
function camelcase(str) {
|
|
633
|
+
return str.split("-").reduce((str2, word) => {
|
|
634
|
+
return str2 + word[0].toUpperCase() + word.slice(1);
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
function splitOptionFlags(flags) {
|
|
638
|
+
let shortFlag;
|
|
639
|
+
let longFlag;
|
|
640
|
+
const shortFlagExp = /^-[^-]$/;
|
|
641
|
+
const longFlagExp = /^--[^-]/;
|
|
642
|
+
const flagParts = flags.split(/[ |,]+/).concat("guard");
|
|
643
|
+
if (shortFlagExp.test(flagParts[0]))
|
|
644
|
+
shortFlag = flagParts.shift();
|
|
645
|
+
if (longFlagExp.test(flagParts[0]))
|
|
646
|
+
longFlag = flagParts.shift();
|
|
647
|
+
if (!shortFlag && shortFlagExp.test(flagParts[0]))
|
|
648
|
+
shortFlag = flagParts.shift();
|
|
649
|
+
if (!shortFlag && longFlagExp.test(flagParts[0])) {
|
|
650
|
+
shortFlag = longFlag;
|
|
651
|
+
longFlag = flagParts.shift();
|
|
652
|
+
}
|
|
653
|
+
if (flagParts[0].startsWith("-")) {
|
|
654
|
+
const unsupportedFlag = flagParts[0];
|
|
655
|
+
const baseError = `option creation failed due to '${unsupportedFlag}' in option flags '${flags}'`;
|
|
656
|
+
if (/^-[^-][^-]/.test(unsupportedFlag))
|
|
657
|
+
throw new Error(`${baseError}
|
|
658
|
+
- a short flag is a single dash and a single character
|
|
659
|
+
- either use a single dash and a single character (for a short flag)
|
|
660
|
+
- or use a double dash for a long option (and can have two, like '--ws, --workspace')`);
|
|
661
|
+
if (shortFlagExp.test(unsupportedFlag))
|
|
662
|
+
throw new Error(`${baseError}
|
|
663
|
+
- too many short flags`);
|
|
664
|
+
if (longFlagExp.test(unsupportedFlag))
|
|
665
|
+
throw new Error(`${baseError}
|
|
666
|
+
- too many long flags`);
|
|
667
|
+
throw new Error(`${baseError}
|
|
668
|
+
- unrecognised flag format`);
|
|
669
|
+
}
|
|
670
|
+
if (shortFlag === undefined && longFlag === undefined)
|
|
671
|
+
throw new Error(`option creation failed due to no flags found in '${flags}'.`);
|
|
672
|
+
return { shortFlag, longFlag };
|
|
673
|
+
}
|
|
674
|
+
exports.Option = Option;
|
|
675
|
+
exports.DualOptions = DualOptions;
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
// node_modules/commander/lib/suggestSimilar.js
|
|
679
|
+
var require_suggestSimilar = __commonJS((exports) => {
|
|
680
|
+
var maxDistance = 3;
|
|
681
|
+
function editDistance(a, b) {
|
|
682
|
+
if (Math.abs(a.length - b.length) > maxDistance)
|
|
683
|
+
return Math.max(a.length, b.length);
|
|
684
|
+
const d = [];
|
|
685
|
+
for (let i = 0;i <= a.length; i++) {
|
|
686
|
+
d[i] = [i];
|
|
687
|
+
}
|
|
688
|
+
for (let j = 0;j <= b.length; j++) {
|
|
689
|
+
d[0][j] = j;
|
|
690
|
+
}
|
|
691
|
+
for (let j = 1;j <= b.length; j++) {
|
|
692
|
+
for (let i = 1;i <= a.length; i++) {
|
|
693
|
+
let cost = 1;
|
|
694
|
+
if (a[i - 1] === b[j - 1]) {
|
|
695
|
+
cost = 0;
|
|
696
|
+
} else {
|
|
697
|
+
cost = 1;
|
|
698
|
+
}
|
|
699
|
+
d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
|
|
700
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
701
|
+
d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return d[a.length][b.length];
|
|
706
|
+
}
|
|
707
|
+
function suggestSimilar(word, candidates) {
|
|
708
|
+
if (!candidates || candidates.length === 0)
|
|
709
|
+
return "";
|
|
710
|
+
candidates = Array.from(new Set(candidates));
|
|
711
|
+
const searchingOptions = word.startsWith("--");
|
|
712
|
+
if (searchingOptions) {
|
|
713
|
+
word = word.slice(2);
|
|
714
|
+
candidates = candidates.map((candidate) => candidate.slice(2));
|
|
715
|
+
}
|
|
716
|
+
let similar = [];
|
|
717
|
+
let bestDistance = maxDistance;
|
|
718
|
+
const minSimilarity = 0.4;
|
|
719
|
+
candidates.forEach((candidate) => {
|
|
720
|
+
if (candidate.length <= 1)
|
|
721
|
+
return;
|
|
722
|
+
const distance = editDistance(word, candidate);
|
|
723
|
+
const length = Math.max(word.length, candidate.length);
|
|
724
|
+
const similarity = (length - distance) / length;
|
|
725
|
+
if (similarity > minSimilarity) {
|
|
726
|
+
if (distance < bestDistance) {
|
|
727
|
+
bestDistance = distance;
|
|
728
|
+
similar = [candidate];
|
|
729
|
+
} else if (distance === bestDistance) {
|
|
730
|
+
similar.push(candidate);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
similar.sort((a, b) => a.localeCompare(b));
|
|
735
|
+
if (searchingOptions) {
|
|
736
|
+
similar = similar.map((candidate) => `--${candidate}`);
|
|
737
|
+
}
|
|
738
|
+
if (similar.length > 1) {
|
|
739
|
+
return `
|
|
740
|
+
(Did you mean one of ${similar.join(", ")}?)`;
|
|
741
|
+
}
|
|
742
|
+
if (similar.length === 1) {
|
|
743
|
+
return `
|
|
744
|
+
(Did you mean ${similar[0]}?)`;
|
|
745
|
+
}
|
|
746
|
+
return "";
|
|
747
|
+
}
|
|
748
|
+
exports.suggestSimilar = suggestSimilar;
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
// node_modules/commander/lib/command.js
|
|
752
|
+
var require_command = __commonJS((exports) => {
|
|
753
|
+
var EventEmitter = __require("node:events").EventEmitter;
|
|
754
|
+
var childProcess = __require("node:child_process");
|
|
755
|
+
var path = __require("node:path");
|
|
756
|
+
var fs = __require("node:fs");
|
|
757
|
+
var process2 = __require("node:process");
|
|
758
|
+
var { Argument, humanReadableArgName } = require_argument();
|
|
759
|
+
var { CommanderError } = require_error();
|
|
760
|
+
var { Help, stripColor } = require_help();
|
|
761
|
+
var { Option, DualOptions } = require_option();
|
|
762
|
+
var { suggestSimilar } = require_suggestSimilar();
|
|
763
|
+
|
|
764
|
+
class Command extends EventEmitter {
|
|
765
|
+
constructor(name) {
|
|
766
|
+
super();
|
|
767
|
+
this.commands = [];
|
|
768
|
+
this.options = [];
|
|
769
|
+
this.parent = null;
|
|
770
|
+
this._allowUnknownOption = false;
|
|
771
|
+
this._allowExcessArguments = false;
|
|
772
|
+
this.registeredArguments = [];
|
|
773
|
+
this._args = this.registeredArguments;
|
|
774
|
+
this.args = [];
|
|
775
|
+
this.rawArgs = [];
|
|
776
|
+
this.processedArgs = [];
|
|
777
|
+
this._scriptPath = null;
|
|
778
|
+
this._name = name || "";
|
|
779
|
+
this._optionValues = {};
|
|
780
|
+
this._optionValueSources = {};
|
|
781
|
+
this._storeOptionsAsProperties = false;
|
|
782
|
+
this._actionHandler = null;
|
|
783
|
+
this._executableHandler = false;
|
|
784
|
+
this._executableFile = null;
|
|
785
|
+
this._executableDir = null;
|
|
786
|
+
this._defaultCommandName = null;
|
|
787
|
+
this._exitCallback = null;
|
|
788
|
+
this._aliases = [];
|
|
789
|
+
this._combineFlagAndOptionalValue = true;
|
|
790
|
+
this._description = "";
|
|
791
|
+
this._summary = "";
|
|
792
|
+
this._argsDescription = undefined;
|
|
793
|
+
this._enablePositionalOptions = false;
|
|
794
|
+
this._passThroughOptions = false;
|
|
795
|
+
this._lifeCycleHooks = {};
|
|
796
|
+
this._showHelpAfterError = false;
|
|
797
|
+
this._showSuggestionAfterError = true;
|
|
798
|
+
this._savedState = null;
|
|
799
|
+
this._outputConfiguration = {
|
|
800
|
+
writeOut: (str) => process2.stdout.write(str),
|
|
801
|
+
writeErr: (str) => process2.stderr.write(str),
|
|
802
|
+
outputError: (str, write) => write(str),
|
|
803
|
+
getOutHelpWidth: () => process2.stdout.isTTY ? process2.stdout.columns : undefined,
|
|
804
|
+
getErrHelpWidth: () => process2.stderr.isTTY ? process2.stderr.columns : undefined,
|
|
805
|
+
getOutHasColors: () => useColor() ?? (process2.stdout.isTTY && process2.stdout.hasColors?.()),
|
|
806
|
+
getErrHasColors: () => useColor() ?? (process2.stderr.isTTY && process2.stderr.hasColors?.()),
|
|
807
|
+
stripColor: (str) => stripColor(str)
|
|
808
|
+
};
|
|
809
|
+
this._hidden = false;
|
|
810
|
+
this._helpOption = undefined;
|
|
811
|
+
this._addImplicitHelpCommand = undefined;
|
|
812
|
+
this._helpCommand = undefined;
|
|
813
|
+
this._helpConfiguration = {};
|
|
814
|
+
this._helpGroupHeading = undefined;
|
|
815
|
+
this._defaultCommandGroup = undefined;
|
|
816
|
+
this._defaultOptionGroup = undefined;
|
|
817
|
+
}
|
|
818
|
+
copyInheritedSettings(sourceCommand) {
|
|
819
|
+
this._outputConfiguration = sourceCommand._outputConfiguration;
|
|
820
|
+
this._helpOption = sourceCommand._helpOption;
|
|
821
|
+
this._helpCommand = sourceCommand._helpCommand;
|
|
822
|
+
this._helpConfiguration = sourceCommand._helpConfiguration;
|
|
823
|
+
this._exitCallback = sourceCommand._exitCallback;
|
|
824
|
+
this._storeOptionsAsProperties = sourceCommand._storeOptionsAsProperties;
|
|
825
|
+
this._combineFlagAndOptionalValue = sourceCommand._combineFlagAndOptionalValue;
|
|
826
|
+
this._allowExcessArguments = sourceCommand._allowExcessArguments;
|
|
827
|
+
this._enablePositionalOptions = sourceCommand._enablePositionalOptions;
|
|
828
|
+
this._showHelpAfterError = sourceCommand._showHelpAfterError;
|
|
829
|
+
this._showSuggestionAfterError = sourceCommand._showSuggestionAfterError;
|
|
830
|
+
return this;
|
|
831
|
+
}
|
|
832
|
+
_getCommandAndAncestors() {
|
|
833
|
+
const result = [];
|
|
834
|
+
for (let command = this;command; command = command.parent) {
|
|
835
|
+
result.push(command);
|
|
836
|
+
}
|
|
837
|
+
return result;
|
|
838
|
+
}
|
|
839
|
+
command(nameAndArgs, actionOptsOrExecDesc, execOpts) {
|
|
840
|
+
let desc = actionOptsOrExecDesc;
|
|
841
|
+
let opts = execOpts;
|
|
842
|
+
if (typeof desc === "object" && desc !== null) {
|
|
843
|
+
opts = desc;
|
|
844
|
+
desc = null;
|
|
845
|
+
}
|
|
846
|
+
opts = opts || {};
|
|
847
|
+
const [, name, args] = nameAndArgs.match(/([^ ]+) *(.*)/);
|
|
848
|
+
const cmd = this.createCommand(name);
|
|
849
|
+
if (desc) {
|
|
850
|
+
cmd.description(desc);
|
|
851
|
+
cmd._executableHandler = true;
|
|
852
|
+
}
|
|
853
|
+
if (opts.isDefault)
|
|
854
|
+
this._defaultCommandName = cmd._name;
|
|
855
|
+
cmd._hidden = !!(opts.noHelp || opts.hidden);
|
|
856
|
+
cmd._executableFile = opts.executableFile || null;
|
|
857
|
+
if (args)
|
|
858
|
+
cmd.arguments(args);
|
|
859
|
+
this._registerCommand(cmd);
|
|
860
|
+
cmd.parent = this;
|
|
861
|
+
cmd.copyInheritedSettings(this);
|
|
862
|
+
if (desc)
|
|
863
|
+
return this;
|
|
864
|
+
return cmd;
|
|
865
|
+
}
|
|
866
|
+
createCommand(name) {
|
|
867
|
+
return new Command(name);
|
|
868
|
+
}
|
|
869
|
+
createHelp() {
|
|
870
|
+
return Object.assign(new Help, this.configureHelp());
|
|
871
|
+
}
|
|
872
|
+
configureHelp(configuration) {
|
|
873
|
+
if (configuration === undefined)
|
|
874
|
+
return this._helpConfiguration;
|
|
875
|
+
this._helpConfiguration = configuration;
|
|
876
|
+
return this;
|
|
877
|
+
}
|
|
878
|
+
configureOutput(configuration) {
|
|
879
|
+
if (configuration === undefined)
|
|
880
|
+
return this._outputConfiguration;
|
|
881
|
+
this._outputConfiguration = {
|
|
882
|
+
...this._outputConfiguration,
|
|
883
|
+
...configuration
|
|
884
|
+
};
|
|
885
|
+
return this;
|
|
886
|
+
}
|
|
887
|
+
showHelpAfterError(displayHelp = true) {
|
|
888
|
+
if (typeof displayHelp !== "string")
|
|
889
|
+
displayHelp = !!displayHelp;
|
|
890
|
+
this._showHelpAfterError = displayHelp;
|
|
891
|
+
return this;
|
|
892
|
+
}
|
|
893
|
+
showSuggestionAfterError(displaySuggestion = true) {
|
|
894
|
+
this._showSuggestionAfterError = !!displaySuggestion;
|
|
895
|
+
return this;
|
|
896
|
+
}
|
|
897
|
+
addCommand(cmd, opts) {
|
|
898
|
+
if (!cmd._name) {
|
|
899
|
+
throw new Error(`Command passed to .addCommand() must have a name
|
|
900
|
+
- specify the name in Command constructor or using .name()`);
|
|
901
|
+
}
|
|
902
|
+
opts = opts || {};
|
|
903
|
+
if (opts.isDefault)
|
|
904
|
+
this._defaultCommandName = cmd._name;
|
|
905
|
+
if (opts.noHelp || opts.hidden)
|
|
906
|
+
cmd._hidden = true;
|
|
907
|
+
this._registerCommand(cmd);
|
|
908
|
+
cmd.parent = this;
|
|
909
|
+
cmd._checkForBrokenPassThrough();
|
|
910
|
+
return this;
|
|
911
|
+
}
|
|
912
|
+
createArgument(name, description) {
|
|
913
|
+
return new Argument(name, description);
|
|
914
|
+
}
|
|
915
|
+
argument(name, description, parseArg, defaultValue) {
|
|
916
|
+
const argument = this.createArgument(name, description);
|
|
917
|
+
if (typeof parseArg === "function") {
|
|
918
|
+
argument.default(defaultValue).argParser(parseArg);
|
|
919
|
+
} else {
|
|
920
|
+
argument.default(parseArg);
|
|
921
|
+
}
|
|
922
|
+
this.addArgument(argument);
|
|
923
|
+
return this;
|
|
924
|
+
}
|
|
925
|
+
arguments(names) {
|
|
926
|
+
names.trim().split(/ +/).forEach((detail) => {
|
|
927
|
+
this.argument(detail);
|
|
928
|
+
});
|
|
929
|
+
return this;
|
|
930
|
+
}
|
|
931
|
+
addArgument(argument) {
|
|
932
|
+
const previousArgument = this.registeredArguments.slice(-1)[0];
|
|
933
|
+
if (previousArgument?.variadic) {
|
|
934
|
+
throw new Error(`only the last argument can be variadic '${previousArgument.name()}'`);
|
|
935
|
+
}
|
|
936
|
+
if (argument.required && argument.defaultValue !== undefined && argument.parseArg === undefined) {
|
|
937
|
+
throw new Error(`a default value for a required argument is never used: '${argument.name()}'`);
|
|
938
|
+
}
|
|
939
|
+
this.registeredArguments.push(argument);
|
|
940
|
+
return this;
|
|
941
|
+
}
|
|
942
|
+
helpCommand(enableOrNameAndArgs, description) {
|
|
943
|
+
if (typeof enableOrNameAndArgs === "boolean") {
|
|
944
|
+
this._addImplicitHelpCommand = enableOrNameAndArgs;
|
|
945
|
+
if (enableOrNameAndArgs && this._defaultCommandGroup) {
|
|
946
|
+
this._initCommandGroup(this._getHelpCommand());
|
|
947
|
+
}
|
|
948
|
+
return this;
|
|
949
|
+
}
|
|
950
|
+
const nameAndArgs = enableOrNameAndArgs ?? "help [command]";
|
|
951
|
+
const [, helpName, helpArgs] = nameAndArgs.match(/([^ ]+) *(.*)/);
|
|
952
|
+
const helpDescription = description ?? "display help for command";
|
|
953
|
+
const helpCommand = this.createCommand(helpName);
|
|
954
|
+
helpCommand.helpOption(false);
|
|
955
|
+
if (helpArgs)
|
|
956
|
+
helpCommand.arguments(helpArgs);
|
|
957
|
+
if (helpDescription)
|
|
958
|
+
helpCommand.description(helpDescription);
|
|
959
|
+
this._addImplicitHelpCommand = true;
|
|
960
|
+
this._helpCommand = helpCommand;
|
|
961
|
+
if (enableOrNameAndArgs || description)
|
|
962
|
+
this._initCommandGroup(helpCommand);
|
|
963
|
+
return this;
|
|
964
|
+
}
|
|
965
|
+
addHelpCommand(helpCommand, deprecatedDescription) {
|
|
966
|
+
if (typeof helpCommand !== "object") {
|
|
967
|
+
this.helpCommand(helpCommand, deprecatedDescription);
|
|
968
|
+
return this;
|
|
969
|
+
}
|
|
970
|
+
this._addImplicitHelpCommand = true;
|
|
971
|
+
this._helpCommand = helpCommand;
|
|
972
|
+
this._initCommandGroup(helpCommand);
|
|
973
|
+
return this;
|
|
974
|
+
}
|
|
975
|
+
_getHelpCommand() {
|
|
976
|
+
const hasImplicitHelpCommand = this._addImplicitHelpCommand ?? (this.commands.length && !this._actionHandler && !this._findCommand("help"));
|
|
977
|
+
if (hasImplicitHelpCommand) {
|
|
978
|
+
if (this._helpCommand === undefined) {
|
|
979
|
+
this.helpCommand(undefined, undefined);
|
|
980
|
+
}
|
|
981
|
+
return this._helpCommand;
|
|
982
|
+
}
|
|
983
|
+
return null;
|
|
984
|
+
}
|
|
985
|
+
hook(event, listener) {
|
|
986
|
+
const allowedValues = ["preSubcommand", "preAction", "postAction"];
|
|
987
|
+
if (!allowedValues.includes(event)) {
|
|
988
|
+
throw new Error(`Unexpected value for event passed to hook : '${event}'.
|
|
989
|
+
Expecting one of '${allowedValues.join("', '")}'`);
|
|
990
|
+
}
|
|
991
|
+
if (this._lifeCycleHooks[event]) {
|
|
992
|
+
this._lifeCycleHooks[event].push(listener);
|
|
993
|
+
} else {
|
|
994
|
+
this._lifeCycleHooks[event] = [listener];
|
|
995
|
+
}
|
|
996
|
+
return this;
|
|
997
|
+
}
|
|
998
|
+
exitOverride(fn) {
|
|
999
|
+
if (fn) {
|
|
1000
|
+
this._exitCallback = fn;
|
|
1001
|
+
} else {
|
|
1002
|
+
this._exitCallback = (err) => {
|
|
1003
|
+
if (err.code !== "commander.executeSubCommandAsync") {
|
|
1004
|
+
throw err;
|
|
1005
|
+
} else {}
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
return this;
|
|
1009
|
+
}
|
|
1010
|
+
_exit(exitCode, code, message) {
|
|
1011
|
+
if (this._exitCallback) {
|
|
1012
|
+
this._exitCallback(new CommanderError(exitCode, code, message));
|
|
1013
|
+
}
|
|
1014
|
+
process2.exit(exitCode);
|
|
1015
|
+
}
|
|
1016
|
+
action(fn) {
|
|
1017
|
+
const listener = (args) => {
|
|
1018
|
+
const expectedArgsCount = this.registeredArguments.length;
|
|
1019
|
+
const actionArgs = args.slice(0, expectedArgsCount);
|
|
1020
|
+
if (this._storeOptionsAsProperties) {
|
|
1021
|
+
actionArgs[expectedArgsCount] = this;
|
|
1022
|
+
} else {
|
|
1023
|
+
actionArgs[expectedArgsCount] = this.opts();
|
|
1024
|
+
}
|
|
1025
|
+
actionArgs.push(this);
|
|
1026
|
+
return fn.apply(this, actionArgs);
|
|
1027
|
+
};
|
|
1028
|
+
this._actionHandler = listener;
|
|
1029
|
+
return this;
|
|
1030
|
+
}
|
|
1031
|
+
createOption(flags, description) {
|
|
1032
|
+
return new Option(flags, description);
|
|
1033
|
+
}
|
|
1034
|
+
_callParseArg(target, value, previous, invalidArgumentMessage) {
|
|
1035
|
+
try {
|
|
1036
|
+
return target.parseArg(value, previous);
|
|
1037
|
+
} catch (err) {
|
|
1038
|
+
if (err.code === "commander.invalidArgument") {
|
|
1039
|
+
const message = `${invalidArgumentMessage} ${err.message}`;
|
|
1040
|
+
this.error(message, { exitCode: err.exitCode, code: err.code });
|
|
1041
|
+
}
|
|
1042
|
+
throw err;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
_registerOption(option) {
|
|
1046
|
+
const matchingOption = option.short && this._findOption(option.short) || option.long && this._findOption(option.long);
|
|
1047
|
+
if (matchingOption) {
|
|
1048
|
+
const matchingFlag = option.long && this._findOption(option.long) ? option.long : option.short;
|
|
1049
|
+
throw new Error(`Cannot add option '${option.flags}'${this._name && ` to command '${this._name}'`} due to conflicting flag '${matchingFlag}'
|
|
1050
|
+
- already used by option '${matchingOption.flags}'`);
|
|
1051
|
+
}
|
|
1052
|
+
this._initOptionGroup(option);
|
|
1053
|
+
this.options.push(option);
|
|
1054
|
+
}
|
|
1055
|
+
_registerCommand(command) {
|
|
1056
|
+
const knownBy = (cmd) => {
|
|
1057
|
+
return [cmd.name()].concat(cmd.aliases());
|
|
1058
|
+
};
|
|
1059
|
+
const alreadyUsed = knownBy(command).find((name) => this._findCommand(name));
|
|
1060
|
+
if (alreadyUsed) {
|
|
1061
|
+
const existingCmd = knownBy(this._findCommand(alreadyUsed)).join("|");
|
|
1062
|
+
const newCmd = knownBy(command).join("|");
|
|
1063
|
+
throw new Error(`cannot add command '${newCmd}' as already have command '${existingCmd}'`);
|
|
1064
|
+
}
|
|
1065
|
+
this._initCommandGroup(command);
|
|
1066
|
+
this.commands.push(command);
|
|
1067
|
+
}
|
|
1068
|
+
addOption(option) {
|
|
1069
|
+
this._registerOption(option);
|
|
1070
|
+
const oname = option.name();
|
|
1071
|
+
const name = option.attributeName();
|
|
1072
|
+
if (option.negate) {
|
|
1073
|
+
const positiveLongFlag = option.long.replace(/^--no-/, "--");
|
|
1074
|
+
if (!this._findOption(positiveLongFlag)) {
|
|
1075
|
+
this.setOptionValueWithSource(name, option.defaultValue === undefined ? true : option.defaultValue, "default");
|
|
1076
|
+
}
|
|
1077
|
+
} else if (option.defaultValue !== undefined) {
|
|
1078
|
+
this.setOptionValueWithSource(name, option.defaultValue, "default");
|
|
1079
|
+
}
|
|
1080
|
+
const handleOptionValue = (val, invalidValueMessage, valueSource) => {
|
|
1081
|
+
if (val == null && option.presetArg !== undefined) {
|
|
1082
|
+
val = option.presetArg;
|
|
1083
|
+
}
|
|
1084
|
+
const oldValue = this.getOptionValue(name);
|
|
1085
|
+
if (val !== null && option.parseArg) {
|
|
1086
|
+
val = this._callParseArg(option, val, oldValue, invalidValueMessage);
|
|
1087
|
+
} else if (val !== null && option.variadic) {
|
|
1088
|
+
val = option._collectValue(val, oldValue);
|
|
1089
|
+
}
|
|
1090
|
+
if (val == null) {
|
|
1091
|
+
if (option.negate) {
|
|
1092
|
+
val = false;
|
|
1093
|
+
} else if (option.isBoolean() || option.optional) {
|
|
1094
|
+
val = true;
|
|
1095
|
+
} else {
|
|
1096
|
+
val = "";
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
this.setOptionValueWithSource(name, val, valueSource);
|
|
1100
|
+
};
|
|
1101
|
+
this.on("option:" + oname, (val) => {
|
|
1102
|
+
const invalidValueMessage = `error: option '${option.flags}' argument '${val}' is invalid.`;
|
|
1103
|
+
handleOptionValue(val, invalidValueMessage, "cli");
|
|
1104
|
+
});
|
|
1105
|
+
if (option.envVar) {
|
|
1106
|
+
this.on("optionEnv:" + oname, (val) => {
|
|
1107
|
+
const invalidValueMessage = `error: option '${option.flags}' value '${val}' from env '${option.envVar}' is invalid.`;
|
|
1108
|
+
handleOptionValue(val, invalidValueMessage, "env");
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
return this;
|
|
1112
|
+
}
|
|
1113
|
+
_optionEx(config, flags, description, fn, defaultValue) {
|
|
1114
|
+
if (typeof flags === "object" && flags instanceof Option) {
|
|
1115
|
+
throw new Error("To add an Option object use addOption() instead of option() or requiredOption()");
|
|
1116
|
+
}
|
|
1117
|
+
const option = this.createOption(flags, description);
|
|
1118
|
+
option.makeOptionMandatory(!!config.mandatory);
|
|
1119
|
+
if (typeof fn === "function") {
|
|
1120
|
+
option.default(defaultValue).argParser(fn);
|
|
1121
|
+
} else if (fn instanceof RegExp) {
|
|
1122
|
+
const regex = fn;
|
|
1123
|
+
fn = (val, def) => {
|
|
1124
|
+
const m = regex.exec(val);
|
|
1125
|
+
return m ? m[0] : def;
|
|
1126
|
+
};
|
|
1127
|
+
option.default(defaultValue).argParser(fn);
|
|
1128
|
+
} else {
|
|
1129
|
+
option.default(fn);
|
|
1130
|
+
}
|
|
1131
|
+
return this.addOption(option);
|
|
1132
|
+
}
|
|
1133
|
+
option(flags, description, parseArg, defaultValue) {
|
|
1134
|
+
return this._optionEx({}, flags, description, parseArg, defaultValue);
|
|
1135
|
+
}
|
|
1136
|
+
requiredOption(flags, description, parseArg, defaultValue) {
|
|
1137
|
+
return this._optionEx({ mandatory: true }, flags, description, parseArg, defaultValue);
|
|
1138
|
+
}
|
|
1139
|
+
combineFlagAndOptionalValue(combine = true) {
|
|
1140
|
+
this._combineFlagAndOptionalValue = !!combine;
|
|
1141
|
+
return this;
|
|
1142
|
+
}
|
|
1143
|
+
allowUnknownOption(allowUnknown = true) {
|
|
1144
|
+
this._allowUnknownOption = !!allowUnknown;
|
|
1145
|
+
return this;
|
|
1146
|
+
}
|
|
1147
|
+
allowExcessArguments(allowExcess = true) {
|
|
1148
|
+
this._allowExcessArguments = !!allowExcess;
|
|
1149
|
+
return this;
|
|
1150
|
+
}
|
|
1151
|
+
enablePositionalOptions(positional = true) {
|
|
1152
|
+
this._enablePositionalOptions = !!positional;
|
|
1153
|
+
return this;
|
|
1154
|
+
}
|
|
1155
|
+
passThroughOptions(passThrough = true) {
|
|
1156
|
+
this._passThroughOptions = !!passThrough;
|
|
1157
|
+
this._checkForBrokenPassThrough();
|
|
1158
|
+
return this;
|
|
1159
|
+
}
|
|
1160
|
+
_checkForBrokenPassThrough() {
|
|
1161
|
+
if (this.parent && this._passThroughOptions && !this.parent._enablePositionalOptions) {
|
|
1162
|
+
throw new Error(`passThroughOptions cannot be used for '${this._name}' without turning on enablePositionalOptions for parent command(s)`);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
storeOptionsAsProperties(storeAsProperties = true) {
|
|
1166
|
+
if (this.options.length) {
|
|
1167
|
+
throw new Error("call .storeOptionsAsProperties() before adding options");
|
|
1168
|
+
}
|
|
1169
|
+
if (Object.keys(this._optionValues).length) {
|
|
1170
|
+
throw new Error("call .storeOptionsAsProperties() before setting option values");
|
|
1171
|
+
}
|
|
1172
|
+
this._storeOptionsAsProperties = !!storeAsProperties;
|
|
1173
|
+
return this;
|
|
1174
|
+
}
|
|
1175
|
+
getOptionValue(key) {
|
|
1176
|
+
if (this._storeOptionsAsProperties) {
|
|
1177
|
+
return this[key];
|
|
1178
|
+
}
|
|
1179
|
+
return this._optionValues[key];
|
|
1180
|
+
}
|
|
1181
|
+
setOptionValue(key, value) {
|
|
1182
|
+
return this.setOptionValueWithSource(key, value, undefined);
|
|
1183
|
+
}
|
|
1184
|
+
setOptionValueWithSource(key, value, source) {
|
|
1185
|
+
if (this._storeOptionsAsProperties) {
|
|
1186
|
+
this[key] = value;
|
|
1187
|
+
} else {
|
|
1188
|
+
this._optionValues[key] = value;
|
|
1189
|
+
}
|
|
1190
|
+
this._optionValueSources[key] = source;
|
|
1191
|
+
return this;
|
|
1192
|
+
}
|
|
1193
|
+
getOptionValueSource(key) {
|
|
1194
|
+
return this._optionValueSources[key];
|
|
1195
|
+
}
|
|
1196
|
+
getOptionValueSourceWithGlobals(key) {
|
|
1197
|
+
let source;
|
|
1198
|
+
this._getCommandAndAncestors().forEach((cmd) => {
|
|
1199
|
+
if (cmd.getOptionValueSource(key) !== undefined) {
|
|
1200
|
+
source = cmd.getOptionValueSource(key);
|
|
1201
|
+
}
|
|
1202
|
+
});
|
|
1203
|
+
return source;
|
|
1204
|
+
}
|
|
1205
|
+
_prepareUserArgs(argv, parseOptions) {
|
|
1206
|
+
if (argv !== undefined && !Array.isArray(argv)) {
|
|
1207
|
+
throw new Error("first parameter to parse must be array or undefined");
|
|
1208
|
+
}
|
|
1209
|
+
parseOptions = parseOptions || {};
|
|
1210
|
+
if (argv === undefined && parseOptions.from === undefined) {
|
|
1211
|
+
if (process2.versions?.electron) {
|
|
1212
|
+
parseOptions.from = "electron";
|
|
1213
|
+
}
|
|
1214
|
+
const execArgv = process2.execArgv ?? [];
|
|
1215
|
+
if (execArgv.includes("-e") || execArgv.includes("--eval") || execArgv.includes("-p") || execArgv.includes("--print")) {
|
|
1216
|
+
parseOptions.from = "eval";
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
if (argv === undefined) {
|
|
1220
|
+
argv = process2.argv;
|
|
1221
|
+
}
|
|
1222
|
+
this.rawArgs = argv.slice();
|
|
1223
|
+
let userArgs;
|
|
1224
|
+
switch (parseOptions.from) {
|
|
1225
|
+
case undefined:
|
|
1226
|
+
case "node":
|
|
1227
|
+
this._scriptPath = argv[1];
|
|
1228
|
+
userArgs = argv.slice(2);
|
|
1229
|
+
break;
|
|
1230
|
+
case "electron":
|
|
1231
|
+
if (process2.defaultApp) {
|
|
1232
|
+
this._scriptPath = argv[1];
|
|
1233
|
+
userArgs = argv.slice(2);
|
|
1234
|
+
} else {
|
|
1235
|
+
userArgs = argv.slice(1);
|
|
1236
|
+
}
|
|
1237
|
+
break;
|
|
1238
|
+
case "user":
|
|
1239
|
+
userArgs = argv.slice(0);
|
|
1240
|
+
break;
|
|
1241
|
+
case "eval":
|
|
1242
|
+
userArgs = argv.slice(1);
|
|
1243
|
+
break;
|
|
1244
|
+
default:
|
|
1245
|
+
throw new Error(`unexpected parse option { from: '${parseOptions.from}' }`);
|
|
1246
|
+
}
|
|
1247
|
+
if (!this._name && this._scriptPath)
|
|
1248
|
+
this.nameFromFilename(this._scriptPath);
|
|
1249
|
+
this._name = this._name || "program";
|
|
1250
|
+
return userArgs;
|
|
1251
|
+
}
|
|
1252
|
+
parse(argv, parseOptions) {
|
|
1253
|
+
this._prepareForParse();
|
|
1254
|
+
const userArgs = this._prepareUserArgs(argv, parseOptions);
|
|
1255
|
+
this._parseCommand([], userArgs);
|
|
1256
|
+
return this;
|
|
1257
|
+
}
|
|
1258
|
+
async parseAsync(argv, parseOptions) {
|
|
1259
|
+
this._prepareForParse();
|
|
1260
|
+
const userArgs = this._prepareUserArgs(argv, parseOptions);
|
|
1261
|
+
await this._parseCommand([], userArgs);
|
|
1262
|
+
return this;
|
|
1263
|
+
}
|
|
1264
|
+
_prepareForParse() {
|
|
1265
|
+
if (this._savedState === null) {
|
|
1266
|
+
this.saveStateBeforeParse();
|
|
1267
|
+
} else {
|
|
1268
|
+
this.restoreStateBeforeParse();
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
saveStateBeforeParse() {
|
|
1272
|
+
this._savedState = {
|
|
1273
|
+
_name: this._name,
|
|
1274
|
+
_optionValues: { ...this._optionValues },
|
|
1275
|
+
_optionValueSources: { ...this._optionValueSources }
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
restoreStateBeforeParse() {
|
|
1279
|
+
if (this._storeOptionsAsProperties)
|
|
1280
|
+
throw new Error(`Can not call parse again when storeOptionsAsProperties is true.
|
|
1281
|
+
- either make a new Command for each call to parse, or stop storing options as properties`);
|
|
1282
|
+
this._name = this._savedState._name;
|
|
1283
|
+
this._scriptPath = null;
|
|
1284
|
+
this.rawArgs = [];
|
|
1285
|
+
this._optionValues = { ...this._savedState._optionValues };
|
|
1286
|
+
this._optionValueSources = { ...this._savedState._optionValueSources };
|
|
1287
|
+
this.args = [];
|
|
1288
|
+
this.processedArgs = [];
|
|
1289
|
+
}
|
|
1290
|
+
_checkForMissingExecutable(executableFile, executableDir, subcommandName) {
|
|
1291
|
+
if (fs.existsSync(executableFile))
|
|
1292
|
+
return;
|
|
1293
|
+
const executableDirMessage = executableDir ? `searched for local subcommand relative to directory '${executableDir}'` : "no directory for search for local subcommand, use .executableDir() to supply a custom directory";
|
|
1294
|
+
const executableMissing = `'${executableFile}' does not exist
|
|
1295
|
+
- if '${subcommandName}' is not meant to be an executable command, remove description parameter from '.command()' and use '.description()' instead
|
|
1296
|
+
- if the default executable name is not suitable, use the executableFile option to supply a custom name or path
|
|
1297
|
+
- ${executableDirMessage}`;
|
|
1298
|
+
throw new Error(executableMissing);
|
|
1299
|
+
}
|
|
1300
|
+
_executeSubCommand(subcommand, args) {
|
|
1301
|
+
args = args.slice();
|
|
1302
|
+
let launchWithNode = false;
|
|
1303
|
+
const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
|
|
1304
|
+
function findFile(baseDir, baseName) {
|
|
1305
|
+
const localBin = path.resolve(baseDir, baseName);
|
|
1306
|
+
if (fs.existsSync(localBin))
|
|
1307
|
+
return localBin;
|
|
1308
|
+
if (sourceExt.includes(path.extname(baseName)))
|
|
1309
|
+
return;
|
|
1310
|
+
const foundExt = sourceExt.find((ext) => fs.existsSync(`${localBin}${ext}`));
|
|
1311
|
+
if (foundExt)
|
|
1312
|
+
return `${localBin}${foundExt}`;
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
this._checkForMissingMandatoryOptions();
|
|
1316
|
+
this._checkForConflictingOptions();
|
|
1317
|
+
let executableFile = subcommand._executableFile || `${this._name}-${subcommand._name}`;
|
|
1318
|
+
let executableDir = this._executableDir || "";
|
|
1319
|
+
if (this._scriptPath) {
|
|
1320
|
+
let resolvedScriptPath;
|
|
1321
|
+
try {
|
|
1322
|
+
resolvedScriptPath = fs.realpathSync(this._scriptPath);
|
|
1323
|
+
} catch {
|
|
1324
|
+
resolvedScriptPath = this._scriptPath;
|
|
1325
|
+
}
|
|
1326
|
+
executableDir = path.resolve(path.dirname(resolvedScriptPath), executableDir);
|
|
1327
|
+
}
|
|
1328
|
+
if (executableDir) {
|
|
1329
|
+
let localFile = findFile(executableDir, executableFile);
|
|
1330
|
+
if (!localFile && !subcommand._executableFile && this._scriptPath) {
|
|
1331
|
+
const legacyName = path.basename(this._scriptPath, path.extname(this._scriptPath));
|
|
1332
|
+
if (legacyName !== this._name) {
|
|
1333
|
+
localFile = findFile(executableDir, `${legacyName}-${subcommand._name}`);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
executableFile = localFile || executableFile;
|
|
1337
|
+
}
|
|
1338
|
+
launchWithNode = sourceExt.includes(path.extname(executableFile));
|
|
1339
|
+
let proc;
|
|
1340
|
+
if (process2.platform !== "win32") {
|
|
1341
|
+
if (launchWithNode) {
|
|
1342
|
+
args.unshift(executableFile);
|
|
1343
|
+
args = incrementNodeInspectorPort(process2.execArgv).concat(args);
|
|
1344
|
+
proc = childProcess.spawn(process2.argv[0], args, { stdio: "inherit" });
|
|
1345
|
+
} else {
|
|
1346
|
+
proc = childProcess.spawn(executableFile, args, { stdio: "inherit" });
|
|
1347
|
+
}
|
|
1348
|
+
} else {
|
|
1349
|
+
this._checkForMissingExecutable(executableFile, executableDir, subcommand._name);
|
|
1350
|
+
args.unshift(executableFile);
|
|
1351
|
+
args = incrementNodeInspectorPort(process2.execArgv).concat(args);
|
|
1352
|
+
proc = childProcess.spawn(process2.execPath, args, { stdio: "inherit" });
|
|
1353
|
+
}
|
|
1354
|
+
if (!proc.killed) {
|
|
1355
|
+
const signals = ["SIGUSR1", "SIGUSR2", "SIGTERM", "SIGINT", "SIGHUP"];
|
|
1356
|
+
signals.forEach((signal) => {
|
|
1357
|
+
process2.on(signal, () => {
|
|
1358
|
+
if (proc.killed === false && proc.exitCode === null) {
|
|
1359
|
+
proc.kill(signal);
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
const exitCallback = this._exitCallback;
|
|
1365
|
+
proc.on("close", (code) => {
|
|
1366
|
+
code = code ?? 1;
|
|
1367
|
+
if (!exitCallback) {
|
|
1368
|
+
process2.exit(code);
|
|
1369
|
+
} else {
|
|
1370
|
+
exitCallback(new CommanderError(code, "commander.executeSubCommandAsync", "(close)"));
|
|
1371
|
+
}
|
|
1372
|
+
});
|
|
1373
|
+
proc.on("error", (err) => {
|
|
1374
|
+
if (err.code === "ENOENT") {
|
|
1375
|
+
this._checkForMissingExecutable(executableFile, executableDir, subcommand._name);
|
|
1376
|
+
} else if (err.code === "EACCES") {
|
|
1377
|
+
throw new Error(`'${executableFile}' not executable`);
|
|
1378
|
+
}
|
|
1379
|
+
if (!exitCallback) {
|
|
1380
|
+
process2.exit(1);
|
|
1381
|
+
} else {
|
|
1382
|
+
const wrappedError = new CommanderError(1, "commander.executeSubCommandAsync", "(error)");
|
|
1383
|
+
wrappedError.nestedError = err;
|
|
1384
|
+
exitCallback(wrappedError);
|
|
1385
|
+
}
|
|
1386
|
+
});
|
|
1387
|
+
this.runningCommand = proc;
|
|
1388
|
+
}
|
|
1389
|
+
_dispatchSubcommand(commandName, operands, unknown) {
|
|
1390
|
+
const subCommand = this._findCommand(commandName);
|
|
1391
|
+
if (!subCommand)
|
|
1392
|
+
this.help({ error: true });
|
|
1393
|
+
subCommand._prepareForParse();
|
|
1394
|
+
let promiseChain;
|
|
1395
|
+
promiseChain = this._chainOrCallSubCommandHook(promiseChain, subCommand, "preSubcommand");
|
|
1396
|
+
promiseChain = this._chainOrCall(promiseChain, () => {
|
|
1397
|
+
if (subCommand._executableHandler) {
|
|
1398
|
+
this._executeSubCommand(subCommand, operands.concat(unknown));
|
|
1399
|
+
} else {
|
|
1400
|
+
return subCommand._parseCommand(operands, unknown);
|
|
1401
|
+
}
|
|
1402
|
+
});
|
|
1403
|
+
return promiseChain;
|
|
1404
|
+
}
|
|
1405
|
+
_dispatchHelpCommand(subcommandName) {
|
|
1406
|
+
if (!subcommandName) {
|
|
1407
|
+
this.help();
|
|
1408
|
+
}
|
|
1409
|
+
const subCommand = this._findCommand(subcommandName);
|
|
1410
|
+
if (subCommand && !subCommand._executableHandler) {
|
|
1411
|
+
subCommand.help();
|
|
1412
|
+
}
|
|
1413
|
+
return this._dispatchSubcommand(subcommandName, [], [this._getHelpOption()?.long ?? this._getHelpOption()?.short ?? "--help"]);
|
|
1414
|
+
}
|
|
1415
|
+
_checkNumberOfArguments() {
|
|
1416
|
+
this.registeredArguments.forEach((arg, i) => {
|
|
1417
|
+
if (arg.required && this.args[i] == null) {
|
|
1418
|
+
this.missingArgument(arg.name());
|
|
1419
|
+
}
|
|
1420
|
+
});
|
|
1421
|
+
if (this.registeredArguments.length > 0 && this.registeredArguments[this.registeredArguments.length - 1].variadic) {
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
if (this.args.length > this.registeredArguments.length) {
|
|
1425
|
+
this._excessArguments(this.args);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
_processArguments() {
|
|
1429
|
+
const myParseArg = (argument, value, previous) => {
|
|
1430
|
+
let parsedValue = value;
|
|
1431
|
+
if (value !== null && argument.parseArg) {
|
|
1432
|
+
const invalidValueMessage = `error: command-argument value '${value}' is invalid for argument '${argument.name()}'.`;
|
|
1433
|
+
parsedValue = this._callParseArg(argument, value, previous, invalidValueMessage);
|
|
1434
|
+
}
|
|
1435
|
+
return parsedValue;
|
|
1436
|
+
};
|
|
1437
|
+
this._checkNumberOfArguments();
|
|
1438
|
+
const processedArgs = [];
|
|
1439
|
+
this.registeredArguments.forEach((declaredArg, index) => {
|
|
1440
|
+
let value = declaredArg.defaultValue;
|
|
1441
|
+
if (declaredArg.variadic) {
|
|
1442
|
+
if (index < this.args.length) {
|
|
1443
|
+
value = this.args.slice(index);
|
|
1444
|
+
if (declaredArg.parseArg) {
|
|
1445
|
+
value = value.reduce((processed, v) => {
|
|
1446
|
+
return myParseArg(declaredArg, v, processed);
|
|
1447
|
+
}, declaredArg.defaultValue);
|
|
1448
|
+
}
|
|
1449
|
+
} else if (value === undefined) {
|
|
1450
|
+
value = [];
|
|
1451
|
+
}
|
|
1452
|
+
} else if (index < this.args.length) {
|
|
1453
|
+
value = this.args[index];
|
|
1454
|
+
if (declaredArg.parseArg) {
|
|
1455
|
+
value = myParseArg(declaredArg, value, declaredArg.defaultValue);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
processedArgs[index] = value;
|
|
1459
|
+
});
|
|
1460
|
+
this.processedArgs = processedArgs;
|
|
1461
|
+
}
|
|
1462
|
+
_chainOrCall(promise, fn) {
|
|
1463
|
+
if (promise?.then && typeof promise.then === "function") {
|
|
1464
|
+
return promise.then(() => fn());
|
|
1465
|
+
}
|
|
1466
|
+
return fn();
|
|
1467
|
+
}
|
|
1468
|
+
_chainOrCallHooks(promise, event) {
|
|
1469
|
+
let result = promise;
|
|
1470
|
+
const hooks = [];
|
|
1471
|
+
this._getCommandAndAncestors().reverse().filter((cmd) => cmd._lifeCycleHooks[event] !== undefined).forEach((hookedCommand) => {
|
|
1472
|
+
hookedCommand._lifeCycleHooks[event].forEach((callback) => {
|
|
1473
|
+
hooks.push({ hookedCommand, callback });
|
|
1474
|
+
});
|
|
1475
|
+
});
|
|
1476
|
+
if (event === "postAction") {
|
|
1477
|
+
hooks.reverse();
|
|
1478
|
+
}
|
|
1479
|
+
hooks.forEach((hookDetail) => {
|
|
1480
|
+
result = this._chainOrCall(result, () => {
|
|
1481
|
+
return hookDetail.callback(hookDetail.hookedCommand, this);
|
|
1482
|
+
});
|
|
1483
|
+
});
|
|
1484
|
+
return result;
|
|
1485
|
+
}
|
|
1486
|
+
_chainOrCallSubCommandHook(promise, subCommand, event) {
|
|
1487
|
+
let result = promise;
|
|
1488
|
+
if (this._lifeCycleHooks[event] !== undefined) {
|
|
1489
|
+
this._lifeCycleHooks[event].forEach((hook) => {
|
|
1490
|
+
result = this._chainOrCall(result, () => {
|
|
1491
|
+
return hook(this, subCommand);
|
|
1492
|
+
});
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
return result;
|
|
1496
|
+
}
|
|
1497
|
+
_parseCommand(operands, unknown) {
|
|
1498
|
+
const parsed = this.parseOptions(unknown);
|
|
1499
|
+
this._parseOptionsEnv();
|
|
1500
|
+
this._parseOptionsImplied();
|
|
1501
|
+
operands = operands.concat(parsed.operands);
|
|
1502
|
+
unknown = parsed.unknown;
|
|
1503
|
+
this.args = operands.concat(unknown);
|
|
1504
|
+
if (operands && this._findCommand(operands[0])) {
|
|
1505
|
+
return this._dispatchSubcommand(operands[0], operands.slice(1), unknown);
|
|
1506
|
+
}
|
|
1507
|
+
if (this._getHelpCommand() && operands[0] === this._getHelpCommand().name()) {
|
|
1508
|
+
return this._dispatchHelpCommand(operands[1]);
|
|
1509
|
+
}
|
|
1510
|
+
if (this._defaultCommandName) {
|
|
1511
|
+
this._outputHelpIfRequested(unknown);
|
|
1512
|
+
return this._dispatchSubcommand(this._defaultCommandName, operands, unknown);
|
|
1513
|
+
}
|
|
1514
|
+
if (this.commands.length && this.args.length === 0 && !this._actionHandler && !this._defaultCommandName) {
|
|
1515
|
+
this.help({ error: true });
|
|
1516
|
+
}
|
|
1517
|
+
this._outputHelpIfRequested(parsed.unknown);
|
|
1518
|
+
this._checkForMissingMandatoryOptions();
|
|
1519
|
+
this._checkForConflictingOptions();
|
|
1520
|
+
const checkForUnknownOptions = () => {
|
|
1521
|
+
if (parsed.unknown.length > 0) {
|
|
1522
|
+
this.unknownOption(parsed.unknown[0]);
|
|
1523
|
+
}
|
|
1524
|
+
};
|
|
1525
|
+
const commandEvent = `command:${this.name()}`;
|
|
1526
|
+
if (this._actionHandler) {
|
|
1527
|
+
checkForUnknownOptions();
|
|
1528
|
+
this._processArguments();
|
|
1529
|
+
let promiseChain;
|
|
1530
|
+
promiseChain = this._chainOrCallHooks(promiseChain, "preAction");
|
|
1531
|
+
promiseChain = this._chainOrCall(promiseChain, () => this._actionHandler(this.processedArgs));
|
|
1532
|
+
if (this.parent) {
|
|
1533
|
+
promiseChain = this._chainOrCall(promiseChain, () => {
|
|
1534
|
+
this.parent.emit(commandEvent, operands, unknown);
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
promiseChain = this._chainOrCallHooks(promiseChain, "postAction");
|
|
1538
|
+
return promiseChain;
|
|
1539
|
+
}
|
|
1540
|
+
if (this.parent?.listenerCount(commandEvent)) {
|
|
1541
|
+
checkForUnknownOptions();
|
|
1542
|
+
this._processArguments();
|
|
1543
|
+
this.parent.emit(commandEvent, operands, unknown);
|
|
1544
|
+
} else if (operands.length) {
|
|
1545
|
+
if (this._findCommand("*")) {
|
|
1546
|
+
return this._dispatchSubcommand("*", operands, unknown);
|
|
1547
|
+
}
|
|
1548
|
+
if (this.listenerCount("command:*")) {
|
|
1549
|
+
this.emit("command:*", operands, unknown);
|
|
1550
|
+
} else if (this.commands.length) {
|
|
1551
|
+
this.unknownCommand();
|
|
1552
|
+
} else {
|
|
1553
|
+
checkForUnknownOptions();
|
|
1554
|
+
this._processArguments();
|
|
1555
|
+
}
|
|
1556
|
+
} else if (this.commands.length) {
|
|
1557
|
+
checkForUnknownOptions();
|
|
1558
|
+
this.help({ error: true });
|
|
1559
|
+
} else {
|
|
1560
|
+
checkForUnknownOptions();
|
|
1561
|
+
this._processArguments();
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
_findCommand(name) {
|
|
1565
|
+
if (!name)
|
|
1566
|
+
return;
|
|
1567
|
+
return this.commands.find((cmd) => cmd._name === name || cmd._aliases.includes(name));
|
|
1568
|
+
}
|
|
1569
|
+
_findOption(arg) {
|
|
1570
|
+
return this.options.find((option) => option.is(arg));
|
|
1571
|
+
}
|
|
1572
|
+
_checkForMissingMandatoryOptions() {
|
|
1573
|
+
this._getCommandAndAncestors().forEach((cmd) => {
|
|
1574
|
+
cmd.options.forEach((anOption) => {
|
|
1575
|
+
if (anOption.mandatory && cmd.getOptionValue(anOption.attributeName()) === undefined) {
|
|
1576
|
+
cmd.missingMandatoryOptionValue(anOption);
|
|
1577
|
+
}
|
|
1578
|
+
});
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
_checkForConflictingLocalOptions() {
|
|
1582
|
+
const definedNonDefaultOptions = this.options.filter((option) => {
|
|
1583
|
+
const optionKey = option.attributeName();
|
|
1584
|
+
if (this.getOptionValue(optionKey) === undefined) {
|
|
1585
|
+
return false;
|
|
1586
|
+
}
|
|
1587
|
+
return this.getOptionValueSource(optionKey) !== "default";
|
|
1588
|
+
});
|
|
1589
|
+
const optionsWithConflicting = definedNonDefaultOptions.filter((option) => option.conflictsWith.length > 0);
|
|
1590
|
+
optionsWithConflicting.forEach((option) => {
|
|
1591
|
+
const conflictingAndDefined = definedNonDefaultOptions.find((defined) => option.conflictsWith.includes(defined.attributeName()));
|
|
1592
|
+
if (conflictingAndDefined) {
|
|
1593
|
+
this._conflictingOption(option, conflictingAndDefined);
|
|
1594
|
+
}
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
_checkForConflictingOptions() {
|
|
1598
|
+
this._getCommandAndAncestors().forEach((cmd) => {
|
|
1599
|
+
cmd._checkForConflictingLocalOptions();
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
parseOptions(args) {
|
|
1603
|
+
const operands = [];
|
|
1604
|
+
const unknown = [];
|
|
1605
|
+
let dest = operands;
|
|
1606
|
+
function maybeOption(arg) {
|
|
1607
|
+
return arg.length > 1 && arg[0] === "-";
|
|
1608
|
+
}
|
|
1609
|
+
const negativeNumberArg = (arg) => {
|
|
1610
|
+
if (!/^-(\d+|\d*\.\d+)(e[+-]?\d+)?$/.test(arg))
|
|
1611
|
+
return false;
|
|
1612
|
+
return !this._getCommandAndAncestors().some((cmd) => cmd.options.map((opt) => opt.short).some((short) => /^-\d$/.test(short)));
|
|
1613
|
+
};
|
|
1614
|
+
let activeVariadicOption = null;
|
|
1615
|
+
let activeGroup = null;
|
|
1616
|
+
let i = 0;
|
|
1617
|
+
while (i < args.length || activeGroup) {
|
|
1618
|
+
const arg = activeGroup ?? args[i++];
|
|
1619
|
+
activeGroup = null;
|
|
1620
|
+
if (arg === "--") {
|
|
1621
|
+
if (dest === unknown)
|
|
1622
|
+
dest.push(arg);
|
|
1623
|
+
dest.push(...args.slice(i));
|
|
1624
|
+
break;
|
|
1625
|
+
}
|
|
1626
|
+
if (activeVariadicOption && (!maybeOption(arg) || negativeNumberArg(arg))) {
|
|
1627
|
+
this.emit(`option:${activeVariadicOption.name()}`, arg);
|
|
1628
|
+
continue;
|
|
1629
|
+
}
|
|
1630
|
+
activeVariadicOption = null;
|
|
1631
|
+
if (maybeOption(arg)) {
|
|
1632
|
+
const option = this._findOption(arg);
|
|
1633
|
+
if (option) {
|
|
1634
|
+
if (option.required) {
|
|
1635
|
+
const value = args[i++];
|
|
1636
|
+
if (value === undefined)
|
|
1637
|
+
this.optionMissingArgument(option);
|
|
1638
|
+
this.emit(`option:${option.name()}`, value);
|
|
1639
|
+
} else if (option.optional) {
|
|
1640
|
+
let value = null;
|
|
1641
|
+
if (i < args.length && (!maybeOption(args[i]) || negativeNumberArg(args[i]))) {
|
|
1642
|
+
value = args[i++];
|
|
1643
|
+
}
|
|
1644
|
+
this.emit(`option:${option.name()}`, value);
|
|
1645
|
+
} else {
|
|
1646
|
+
this.emit(`option:${option.name()}`);
|
|
1647
|
+
}
|
|
1648
|
+
activeVariadicOption = option.variadic ? option : null;
|
|
1649
|
+
continue;
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
if (arg.length > 2 && arg[0] === "-" && arg[1] !== "-") {
|
|
1653
|
+
const option = this._findOption(`-${arg[1]}`);
|
|
1654
|
+
if (option) {
|
|
1655
|
+
if (option.required || option.optional && this._combineFlagAndOptionalValue) {
|
|
1656
|
+
this.emit(`option:${option.name()}`, arg.slice(2));
|
|
1657
|
+
} else {
|
|
1658
|
+
this.emit(`option:${option.name()}`);
|
|
1659
|
+
activeGroup = `-${arg.slice(2)}`;
|
|
1660
|
+
}
|
|
1661
|
+
continue;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
if (/^--[^=]+=/.test(arg)) {
|
|
1665
|
+
const index = arg.indexOf("=");
|
|
1666
|
+
const option = this._findOption(arg.slice(0, index));
|
|
1667
|
+
if (option && (option.required || option.optional)) {
|
|
1668
|
+
this.emit(`option:${option.name()}`, arg.slice(index + 1));
|
|
1669
|
+
continue;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
if (dest === operands && maybeOption(arg) && !(this.commands.length === 0 && negativeNumberArg(arg))) {
|
|
1673
|
+
dest = unknown;
|
|
1674
|
+
}
|
|
1675
|
+
if ((this._enablePositionalOptions || this._passThroughOptions) && operands.length === 0 && unknown.length === 0) {
|
|
1676
|
+
if (this._findCommand(arg)) {
|
|
1677
|
+
operands.push(arg);
|
|
1678
|
+
unknown.push(...args.slice(i));
|
|
1679
|
+
break;
|
|
1680
|
+
} else if (this._getHelpCommand() && arg === this._getHelpCommand().name()) {
|
|
1681
|
+
operands.push(arg, ...args.slice(i));
|
|
1682
|
+
break;
|
|
1683
|
+
} else if (this._defaultCommandName) {
|
|
1684
|
+
unknown.push(arg, ...args.slice(i));
|
|
1685
|
+
break;
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
if (this._passThroughOptions) {
|
|
1689
|
+
dest.push(arg, ...args.slice(i));
|
|
1690
|
+
break;
|
|
1691
|
+
}
|
|
1692
|
+
dest.push(arg);
|
|
1693
|
+
}
|
|
1694
|
+
return { operands, unknown };
|
|
1695
|
+
}
|
|
1696
|
+
opts() {
|
|
1697
|
+
if (this._storeOptionsAsProperties) {
|
|
1698
|
+
const result = {};
|
|
1699
|
+
const len = this.options.length;
|
|
1700
|
+
for (let i = 0;i < len; i++) {
|
|
1701
|
+
const key = this.options[i].attributeName();
|
|
1702
|
+
result[key] = key === this._versionOptionName ? this._version : this[key];
|
|
1703
|
+
}
|
|
1704
|
+
return result;
|
|
1705
|
+
}
|
|
1706
|
+
return this._optionValues;
|
|
1707
|
+
}
|
|
1708
|
+
optsWithGlobals() {
|
|
1709
|
+
return this._getCommandAndAncestors().reduce((combinedOptions, cmd) => Object.assign(combinedOptions, cmd.opts()), {});
|
|
1710
|
+
}
|
|
1711
|
+
error(message, errorOptions) {
|
|
1712
|
+
this._outputConfiguration.outputError(`${message}
|
|
1713
|
+
`, this._outputConfiguration.writeErr);
|
|
1714
|
+
if (typeof this._showHelpAfterError === "string") {
|
|
1715
|
+
this._outputConfiguration.writeErr(`${this._showHelpAfterError}
|
|
1716
|
+
`);
|
|
1717
|
+
} else if (this._showHelpAfterError) {
|
|
1718
|
+
this._outputConfiguration.writeErr(`
|
|
1719
|
+
`);
|
|
1720
|
+
this.outputHelp({ error: true });
|
|
1721
|
+
}
|
|
1722
|
+
const config = errorOptions || {};
|
|
1723
|
+
const exitCode = config.exitCode || 1;
|
|
1724
|
+
const code = config.code || "commander.error";
|
|
1725
|
+
this._exit(exitCode, code, message);
|
|
1726
|
+
}
|
|
1727
|
+
_parseOptionsEnv() {
|
|
1728
|
+
this.options.forEach((option) => {
|
|
1729
|
+
if (option.envVar && option.envVar in process2.env) {
|
|
1730
|
+
const optionKey = option.attributeName();
|
|
1731
|
+
if (this.getOptionValue(optionKey) === undefined || ["default", "config", "env"].includes(this.getOptionValueSource(optionKey))) {
|
|
1732
|
+
if (option.required || option.optional) {
|
|
1733
|
+
this.emit(`optionEnv:${option.name()}`, process2.env[option.envVar]);
|
|
1734
|
+
} else {
|
|
1735
|
+
this.emit(`optionEnv:${option.name()}`);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
_parseOptionsImplied() {
|
|
1742
|
+
const dualHelper = new DualOptions(this.options);
|
|
1743
|
+
const hasCustomOptionValue = (optionKey) => {
|
|
1744
|
+
return this.getOptionValue(optionKey) !== undefined && !["default", "implied"].includes(this.getOptionValueSource(optionKey));
|
|
1745
|
+
};
|
|
1746
|
+
this.options.filter((option) => option.implied !== undefined && hasCustomOptionValue(option.attributeName()) && dualHelper.valueFromOption(this.getOptionValue(option.attributeName()), option)).forEach((option) => {
|
|
1747
|
+
Object.keys(option.implied).filter((impliedKey) => !hasCustomOptionValue(impliedKey)).forEach((impliedKey) => {
|
|
1748
|
+
this.setOptionValueWithSource(impliedKey, option.implied[impliedKey], "implied");
|
|
1749
|
+
});
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
missingArgument(name) {
|
|
1753
|
+
const message = `error: missing required argument '${name}'`;
|
|
1754
|
+
this.error(message, { code: "commander.missingArgument" });
|
|
1755
|
+
}
|
|
1756
|
+
optionMissingArgument(option) {
|
|
1757
|
+
const message = `error: option '${option.flags}' argument missing`;
|
|
1758
|
+
this.error(message, { code: "commander.optionMissingArgument" });
|
|
1759
|
+
}
|
|
1760
|
+
missingMandatoryOptionValue(option) {
|
|
1761
|
+
const message = `error: required option '${option.flags}' not specified`;
|
|
1762
|
+
this.error(message, { code: "commander.missingMandatoryOptionValue" });
|
|
1763
|
+
}
|
|
1764
|
+
_conflictingOption(option, conflictingOption) {
|
|
1765
|
+
const findBestOptionFromValue = (option2) => {
|
|
1766
|
+
const optionKey = option2.attributeName();
|
|
1767
|
+
const optionValue = this.getOptionValue(optionKey);
|
|
1768
|
+
const negativeOption = this.options.find((target) => target.negate && optionKey === target.attributeName());
|
|
1769
|
+
const positiveOption = this.options.find((target) => !target.negate && optionKey === target.attributeName());
|
|
1770
|
+
if (negativeOption && (negativeOption.presetArg === undefined && optionValue === false || negativeOption.presetArg !== undefined && optionValue === negativeOption.presetArg)) {
|
|
1771
|
+
return negativeOption;
|
|
1772
|
+
}
|
|
1773
|
+
return positiveOption || option2;
|
|
1774
|
+
};
|
|
1775
|
+
const getErrorMessage = (option2) => {
|
|
1776
|
+
const bestOption = findBestOptionFromValue(option2);
|
|
1777
|
+
const optionKey = bestOption.attributeName();
|
|
1778
|
+
const source = this.getOptionValueSource(optionKey);
|
|
1779
|
+
if (source === "env") {
|
|
1780
|
+
return `environment variable '${bestOption.envVar}'`;
|
|
1781
|
+
}
|
|
1782
|
+
return `option '${bestOption.flags}'`;
|
|
1783
|
+
};
|
|
1784
|
+
const message = `error: ${getErrorMessage(option)} cannot be used with ${getErrorMessage(conflictingOption)}`;
|
|
1785
|
+
this.error(message, { code: "commander.conflictingOption" });
|
|
1786
|
+
}
|
|
1787
|
+
unknownOption(flag) {
|
|
1788
|
+
if (this._allowUnknownOption)
|
|
1789
|
+
return;
|
|
1790
|
+
let suggestion = "";
|
|
1791
|
+
if (flag.startsWith("--") && this._showSuggestionAfterError) {
|
|
1792
|
+
let candidateFlags = [];
|
|
1793
|
+
let command = this;
|
|
1794
|
+
do {
|
|
1795
|
+
const moreFlags = command.createHelp().visibleOptions(command).filter((option) => option.long).map((option) => option.long);
|
|
1796
|
+
candidateFlags = candidateFlags.concat(moreFlags);
|
|
1797
|
+
command = command.parent;
|
|
1798
|
+
} while (command && !command._enablePositionalOptions);
|
|
1799
|
+
suggestion = suggestSimilar(flag, candidateFlags);
|
|
1800
|
+
}
|
|
1801
|
+
const message = `error: unknown option '${flag}'${suggestion}`;
|
|
1802
|
+
this.error(message, { code: "commander.unknownOption" });
|
|
1803
|
+
}
|
|
1804
|
+
_excessArguments(receivedArgs) {
|
|
1805
|
+
if (this._allowExcessArguments)
|
|
1806
|
+
return;
|
|
1807
|
+
const expected = this.registeredArguments.length;
|
|
1808
|
+
const s = expected === 1 ? "" : "s";
|
|
1809
|
+
const forSubcommand = this.parent ? ` for '${this.name()}'` : "";
|
|
1810
|
+
const message = `error: too many arguments${forSubcommand}. Expected ${expected} argument${s} but got ${receivedArgs.length}.`;
|
|
1811
|
+
this.error(message, { code: "commander.excessArguments" });
|
|
1812
|
+
}
|
|
1813
|
+
unknownCommand() {
|
|
1814
|
+
const unknownName = this.args[0];
|
|
1815
|
+
let suggestion = "";
|
|
1816
|
+
if (this._showSuggestionAfterError) {
|
|
1817
|
+
const candidateNames = [];
|
|
1818
|
+
this.createHelp().visibleCommands(this).forEach((command) => {
|
|
1819
|
+
candidateNames.push(command.name());
|
|
1820
|
+
if (command.alias())
|
|
1821
|
+
candidateNames.push(command.alias());
|
|
1822
|
+
});
|
|
1823
|
+
suggestion = suggestSimilar(unknownName, candidateNames);
|
|
1824
|
+
}
|
|
1825
|
+
const message = `error: unknown command '${unknownName}'${suggestion}`;
|
|
1826
|
+
this.error(message, { code: "commander.unknownCommand" });
|
|
1827
|
+
}
|
|
1828
|
+
version(str, flags, description) {
|
|
1829
|
+
if (str === undefined)
|
|
1830
|
+
return this._version;
|
|
1831
|
+
this._version = str;
|
|
1832
|
+
flags = flags || "-V, --version";
|
|
1833
|
+
description = description || "output the version number";
|
|
1834
|
+
const versionOption = this.createOption(flags, description);
|
|
1835
|
+
this._versionOptionName = versionOption.attributeName();
|
|
1836
|
+
this._registerOption(versionOption);
|
|
1837
|
+
this.on("option:" + versionOption.name(), () => {
|
|
1838
|
+
this._outputConfiguration.writeOut(`${str}
|
|
1839
|
+
`);
|
|
1840
|
+
this._exit(0, "commander.version", str);
|
|
1841
|
+
});
|
|
1842
|
+
return this;
|
|
1843
|
+
}
|
|
1844
|
+
description(str, argsDescription) {
|
|
1845
|
+
if (str === undefined && argsDescription === undefined)
|
|
1846
|
+
return this._description;
|
|
1847
|
+
this._description = str;
|
|
1848
|
+
if (argsDescription) {
|
|
1849
|
+
this._argsDescription = argsDescription;
|
|
1850
|
+
}
|
|
1851
|
+
return this;
|
|
1852
|
+
}
|
|
1853
|
+
summary(str) {
|
|
1854
|
+
if (str === undefined)
|
|
1855
|
+
return this._summary;
|
|
1856
|
+
this._summary = str;
|
|
1857
|
+
return this;
|
|
1858
|
+
}
|
|
1859
|
+
alias(alias) {
|
|
1860
|
+
if (alias === undefined)
|
|
1861
|
+
return this._aliases[0];
|
|
1862
|
+
let command = this;
|
|
1863
|
+
if (this.commands.length !== 0 && this.commands[this.commands.length - 1]._executableHandler) {
|
|
1864
|
+
command = this.commands[this.commands.length - 1];
|
|
1865
|
+
}
|
|
1866
|
+
if (alias === command._name)
|
|
1867
|
+
throw new Error("Command alias can't be the same as its name");
|
|
1868
|
+
const matchingCommand = this.parent?._findCommand(alias);
|
|
1869
|
+
if (matchingCommand) {
|
|
1870
|
+
const existingCmd = [matchingCommand.name()].concat(matchingCommand.aliases()).join("|");
|
|
1871
|
+
throw new Error(`cannot add alias '${alias}' to command '${this.name()}' as already have command '${existingCmd}'`);
|
|
1872
|
+
}
|
|
1873
|
+
command._aliases.push(alias);
|
|
1874
|
+
return this;
|
|
1875
|
+
}
|
|
1876
|
+
aliases(aliases) {
|
|
1877
|
+
if (aliases === undefined)
|
|
1878
|
+
return this._aliases;
|
|
1879
|
+
aliases.forEach((alias) => this.alias(alias));
|
|
1880
|
+
return this;
|
|
1881
|
+
}
|
|
1882
|
+
usage(str) {
|
|
1883
|
+
if (str === undefined) {
|
|
1884
|
+
if (this._usage)
|
|
1885
|
+
return this._usage;
|
|
1886
|
+
const args = this.registeredArguments.map((arg) => {
|
|
1887
|
+
return humanReadableArgName(arg);
|
|
1888
|
+
});
|
|
1889
|
+
return [].concat(this.options.length || this._helpOption !== null ? "[options]" : [], this.commands.length ? "[command]" : [], this.registeredArguments.length ? args : []).join(" ");
|
|
1890
|
+
}
|
|
1891
|
+
this._usage = str;
|
|
1892
|
+
return this;
|
|
1893
|
+
}
|
|
1894
|
+
name(str) {
|
|
1895
|
+
if (str === undefined)
|
|
1896
|
+
return this._name;
|
|
1897
|
+
this._name = str;
|
|
1898
|
+
return this;
|
|
1899
|
+
}
|
|
1900
|
+
helpGroup(heading) {
|
|
1901
|
+
if (heading === undefined)
|
|
1902
|
+
return this._helpGroupHeading ?? "";
|
|
1903
|
+
this._helpGroupHeading = heading;
|
|
1904
|
+
return this;
|
|
1905
|
+
}
|
|
1906
|
+
commandsGroup(heading) {
|
|
1907
|
+
if (heading === undefined)
|
|
1908
|
+
return this._defaultCommandGroup ?? "";
|
|
1909
|
+
this._defaultCommandGroup = heading;
|
|
1910
|
+
return this;
|
|
1911
|
+
}
|
|
1912
|
+
optionsGroup(heading) {
|
|
1913
|
+
if (heading === undefined)
|
|
1914
|
+
return this._defaultOptionGroup ?? "";
|
|
1915
|
+
this._defaultOptionGroup = heading;
|
|
1916
|
+
return this;
|
|
1917
|
+
}
|
|
1918
|
+
_initOptionGroup(option) {
|
|
1919
|
+
if (this._defaultOptionGroup && !option.helpGroupHeading)
|
|
1920
|
+
option.helpGroup(this._defaultOptionGroup);
|
|
1921
|
+
}
|
|
1922
|
+
_initCommandGroup(cmd) {
|
|
1923
|
+
if (this._defaultCommandGroup && !cmd.helpGroup())
|
|
1924
|
+
cmd.helpGroup(this._defaultCommandGroup);
|
|
1925
|
+
}
|
|
1926
|
+
nameFromFilename(filename) {
|
|
1927
|
+
this._name = path.basename(filename, path.extname(filename));
|
|
1928
|
+
return this;
|
|
1929
|
+
}
|
|
1930
|
+
executableDir(path2) {
|
|
1931
|
+
if (path2 === undefined)
|
|
1932
|
+
return this._executableDir;
|
|
1933
|
+
this._executableDir = path2;
|
|
1934
|
+
return this;
|
|
1935
|
+
}
|
|
1936
|
+
helpInformation(contextOptions) {
|
|
1937
|
+
const helper = this.createHelp();
|
|
1938
|
+
const context = this._getOutputContext(contextOptions);
|
|
1939
|
+
helper.prepareContext({
|
|
1940
|
+
error: context.error,
|
|
1941
|
+
helpWidth: context.helpWidth,
|
|
1942
|
+
outputHasColors: context.hasColors
|
|
1943
|
+
});
|
|
1944
|
+
const text = helper.formatHelp(this, helper);
|
|
1945
|
+
if (context.hasColors)
|
|
1946
|
+
return text;
|
|
1947
|
+
return this._outputConfiguration.stripColor(text);
|
|
1948
|
+
}
|
|
1949
|
+
_getOutputContext(contextOptions) {
|
|
1950
|
+
contextOptions = contextOptions || {};
|
|
1951
|
+
const error = !!contextOptions.error;
|
|
1952
|
+
let baseWrite;
|
|
1953
|
+
let hasColors;
|
|
1954
|
+
let helpWidth;
|
|
1955
|
+
if (error) {
|
|
1956
|
+
baseWrite = (str) => this._outputConfiguration.writeErr(str);
|
|
1957
|
+
hasColors = this._outputConfiguration.getErrHasColors();
|
|
1958
|
+
helpWidth = this._outputConfiguration.getErrHelpWidth();
|
|
1959
|
+
} else {
|
|
1960
|
+
baseWrite = (str) => this._outputConfiguration.writeOut(str);
|
|
1961
|
+
hasColors = this._outputConfiguration.getOutHasColors();
|
|
1962
|
+
helpWidth = this._outputConfiguration.getOutHelpWidth();
|
|
1963
|
+
}
|
|
1964
|
+
const write = (str) => {
|
|
1965
|
+
if (!hasColors)
|
|
1966
|
+
str = this._outputConfiguration.stripColor(str);
|
|
1967
|
+
return baseWrite(str);
|
|
1968
|
+
};
|
|
1969
|
+
return { error, write, hasColors, helpWidth };
|
|
1970
|
+
}
|
|
1971
|
+
outputHelp(contextOptions) {
|
|
1972
|
+
let deprecatedCallback;
|
|
1973
|
+
if (typeof contextOptions === "function") {
|
|
1974
|
+
deprecatedCallback = contextOptions;
|
|
1975
|
+
contextOptions = undefined;
|
|
1976
|
+
}
|
|
1977
|
+
const outputContext = this._getOutputContext(contextOptions);
|
|
1978
|
+
const eventContext = {
|
|
1979
|
+
error: outputContext.error,
|
|
1980
|
+
write: outputContext.write,
|
|
1981
|
+
command: this
|
|
1982
|
+
};
|
|
1983
|
+
this._getCommandAndAncestors().reverse().forEach((command) => command.emit("beforeAllHelp", eventContext));
|
|
1984
|
+
this.emit("beforeHelp", eventContext);
|
|
1985
|
+
let helpInformation = this.helpInformation({ error: outputContext.error });
|
|
1986
|
+
if (deprecatedCallback) {
|
|
1987
|
+
helpInformation = deprecatedCallback(helpInformation);
|
|
1988
|
+
if (typeof helpInformation !== "string" && !Buffer.isBuffer(helpInformation)) {
|
|
1989
|
+
throw new Error("outputHelp callback must return a string or a Buffer");
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
outputContext.write(helpInformation);
|
|
1993
|
+
if (this._getHelpOption()?.long) {
|
|
1994
|
+
this.emit(this._getHelpOption().long);
|
|
1995
|
+
}
|
|
1996
|
+
this.emit("afterHelp", eventContext);
|
|
1997
|
+
this._getCommandAndAncestors().forEach((command) => command.emit("afterAllHelp", eventContext));
|
|
1998
|
+
}
|
|
1999
|
+
helpOption(flags, description) {
|
|
2000
|
+
if (typeof flags === "boolean") {
|
|
2001
|
+
if (flags) {
|
|
2002
|
+
if (this._helpOption === null)
|
|
2003
|
+
this._helpOption = undefined;
|
|
2004
|
+
if (this._defaultOptionGroup) {
|
|
2005
|
+
this._initOptionGroup(this._getHelpOption());
|
|
2006
|
+
}
|
|
2007
|
+
} else {
|
|
2008
|
+
this._helpOption = null;
|
|
2009
|
+
}
|
|
2010
|
+
return this;
|
|
2011
|
+
}
|
|
2012
|
+
this._helpOption = this.createOption(flags ?? "-h, --help", description ?? "display help for command");
|
|
2013
|
+
if (flags || description)
|
|
2014
|
+
this._initOptionGroup(this._helpOption);
|
|
2015
|
+
return this;
|
|
2016
|
+
}
|
|
2017
|
+
_getHelpOption() {
|
|
2018
|
+
if (this._helpOption === undefined) {
|
|
2019
|
+
this.helpOption(undefined, undefined);
|
|
2020
|
+
}
|
|
2021
|
+
return this._helpOption;
|
|
2022
|
+
}
|
|
2023
|
+
addHelpOption(option) {
|
|
2024
|
+
this._helpOption = option;
|
|
2025
|
+
this._initOptionGroup(option);
|
|
2026
|
+
return this;
|
|
2027
|
+
}
|
|
2028
|
+
help(contextOptions) {
|
|
2029
|
+
this.outputHelp(contextOptions);
|
|
2030
|
+
let exitCode = Number(process2.exitCode ?? 0);
|
|
2031
|
+
if (exitCode === 0 && contextOptions && typeof contextOptions !== "function" && contextOptions.error) {
|
|
2032
|
+
exitCode = 1;
|
|
2033
|
+
}
|
|
2034
|
+
this._exit(exitCode, "commander.help", "(outputHelp)");
|
|
2035
|
+
}
|
|
2036
|
+
addHelpText(position, text) {
|
|
2037
|
+
const allowedValues = ["beforeAll", "before", "after", "afterAll"];
|
|
2038
|
+
if (!allowedValues.includes(position)) {
|
|
2039
|
+
throw new Error(`Unexpected value for position to addHelpText.
|
|
2040
|
+
Expecting one of '${allowedValues.join("', '")}'`);
|
|
2041
|
+
}
|
|
2042
|
+
const helpEvent = `${position}Help`;
|
|
2043
|
+
this.on(helpEvent, (context) => {
|
|
2044
|
+
let helpStr;
|
|
2045
|
+
if (typeof text === "function") {
|
|
2046
|
+
helpStr = text({ error: context.error, command: context.command });
|
|
2047
|
+
} else {
|
|
2048
|
+
helpStr = text;
|
|
2049
|
+
}
|
|
2050
|
+
if (helpStr) {
|
|
2051
|
+
context.write(`${helpStr}
|
|
2052
|
+
`);
|
|
2053
|
+
}
|
|
2054
|
+
});
|
|
2055
|
+
return this;
|
|
2056
|
+
}
|
|
2057
|
+
_outputHelpIfRequested(args) {
|
|
2058
|
+
const helpOption = this._getHelpOption();
|
|
2059
|
+
const helpRequested = helpOption && args.find((arg) => helpOption.is(arg));
|
|
2060
|
+
if (helpRequested) {
|
|
2061
|
+
this.outputHelp();
|
|
2062
|
+
this._exit(0, "commander.helpDisplayed", "(outputHelp)");
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
function incrementNodeInspectorPort(args) {
|
|
2067
|
+
return args.map((arg) => {
|
|
2068
|
+
if (!arg.startsWith("--inspect")) {
|
|
2069
|
+
return arg;
|
|
2070
|
+
}
|
|
2071
|
+
let debugOption;
|
|
2072
|
+
let debugHost = "127.0.0.1";
|
|
2073
|
+
let debugPort = "9229";
|
|
2074
|
+
let match;
|
|
2075
|
+
if ((match = arg.match(/^(--inspect(-brk)?)$/)) !== null) {
|
|
2076
|
+
debugOption = match[1];
|
|
2077
|
+
} else if ((match = arg.match(/^(--inspect(-brk|-port)?)=([^:]+)$/)) !== null) {
|
|
2078
|
+
debugOption = match[1];
|
|
2079
|
+
if (/^\d+$/.test(match[3])) {
|
|
2080
|
+
debugPort = match[3];
|
|
2081
|
+
} else {
|
|
2082
|
+
debugHost = match[3];
|
|
2083
|
+
}
|
|
2084
|
+
} else if ((match = arg.match(/^(--inspect(-brk|-port)?)=([^:]+):(\d+)$/)) !== null) {
|
|
2085
|
+
debugOption = match[1];
|
|
2086
|
+
debugHost = match[3];
|
|
2087
|
+
debugPort = match[4];
|
|
2088
|
+
}
|
|
2089
|
+
if (debugOption && debugPort !== "0") {
|
|
2090
|
+
return `${debugOption}=${debugHost}:${parseInt(debugPort) + 1}`;
|
|
2091
|
+
}
|
|
2092
|
+
return arg;
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
2095
|
+
function useColor() {
|
|
2096
|
+
if (process2.env.NO_COLOR || process2.env.FORCE_COLOR === "0" || process2.env.FORCE_COLOR === "false")
|
|
2097
|
+
return false;
|
|
2098
|
+
if (process2.env.FORCE_COLOR || process2.env.CLICOLOR_FORCE !== undefined)
|
|
2099
|
+
return true;
|
|
2100
|
+
return;
|
|
2101
|
+
}
|
|
2102
|
+
exports.Command = Command;
|
|
2103
|
+
exports.useColor = useColor;
|
|
2104
|
+
});
|
|
2105
|
+
|
|
2106
|
+
// node_modules/commander/index.js
|
|
2107
|
+
var require_commander = __commonJS((exports) => {
|
|
2108
|
+
var { Argument } = require_argument();
|
|
2109
|
+
var { Command } = require_command();
|
|
2110
|
+
var { CommanderError, InvalidArgumentError } = require_error();
|
|
2111
|
+
var { Help } = require_help();
|
|
2112
|
+
var { Option } = require_option();
|
|
2113
|
+
exports.program = new Command;
|
|
2114
|
+
exports.createCommand = (name) => new Command(name);
|
|
2115
|
+
exports.createOption = (flags, description) => new Option(flags, description);
|
|
2116
|
+
exports.createArgument = (name, description) => new Argument(name, description);
|
|
2117
|
+
exports.Command = Command;
|
|
2118
|
+
exports.Option = Option;
|
|
2119
|
+
exports.Argument = Argument;
|
|
2120
|
+
exports.Help = Help;
|
|
2121
|
+
exports.CommanderError = CommanderError;
|
|
2122
|
+
exports.InvalidArgumentError = InvalidArgumentError;
|
|
2123
|
+
exports.InvalidOptionArgumentError = InvalidArgumentError;
|
|
2124
|
+
});
|
|
2125
|
+
|
|
2126
|
+
// src/presets.ts
|
|
2127
|
+
var PLATFORM_PRESETS, DEPTH_ORDER, AUTO_SHADOW;
|
|
2128
|
+
var init_presets = __esm(() => {
|
|
2129
|
+
PLATFORM_PRESETS = {
|
|
2130
|
+
"blog-featured": {
|
|
2131
|
+
width: 1200,
|
|
2132
|
+
height: 630,
|
|
2133
|
+
safeZone: "10% inset all sides",
|
|
2134
|
+
minHeading: 48,
|
|
2135
|
+
defaultGrade: "cinematic"
|
|
2136
|
+
},
|
|
2137
|
+
"blog-inline": {
|
|
2138
|
+
width: 800,
|
|
2139
|
+
height: 450,
|
|
2140
|
+
safeZone: "5% inset"
|
|
2141
|
+
},
|
|
2142
|
+
"og-image": {
|
|
2143
|
+
width: 1200,
|
|
2144
|
+
height: 630,
|
|
2145
|
+
safeZone: "key content within center 1000x500"
|
|
2146
|
+
},
|
|
2147
|
+
"twitter-header": {
|
|
2148
|
+
width: 1500,
|
|
2149
|
+
height: 500,
|
|
2150
|
+
safeZone: "center 60% only (sides crop on mobile)"
|
|
2151
|
+
},
|
|
2152
|
+
"instagram-square": {
|
|
2153
|
+
width: 1080,
|
|
2154
|
+
height: 1080,
|
|
2155
|
+
safeZone: "10% inset, avoid bottom 15%"
|
|
2156
|
+
},
|
|
2157
|
+
"instagram-story": {
|
|
2158
|
+
width: 1080,
|
|
2159
|
+
height: 1920,
|
|
2160
|
+
safeZone: "avoid top 15% and bottom 20%"
|
|
2161
|
+
},
|
|
2162
|
+
"linkedin-post": {
|
|
2163
|
+
width: 1200,
|
|
2164
|
+
height: 627,
|
|
2165
|
+
safeZone: "similar to OG"
|
|
2166
|
+
},
|
|
2167
|
+
"youtube-thumbnail": {
|
|
2168
|
+
width: 1280,
|
|
2169
|
+
height: 720,
|
|
2170
|
+
safeZone: "avoid bottom-right 20% (duration badge)",
|
|
2171
|
+
notes: "High contrast required, text readable at small size"
|
|
2172
|
+
},
|
|
2173
|
+
"shopify-app-listing": {
|
|
2174
|
+
width: 1200,
|
|
2175
|
+
height: 628,
|
|
2176
|
+
safeZone: "10% inset"
|
|
2177
|
+
}
|
|
2178
|
+
};
|
|
2179
|
+
DEPTH_ORDER = {
|
|
2180
|
+
background: 0,
|
|
2181
|
+
midground: 1,
|
|
2182
|
+
foreground: 2,
|
|
2183
|
+
overlay: 3,
|
|
2184
|
+
frame: 4
|
|
2185
|
+
};
|
|
2186
|
+
AUTO_SHADOW = {
|
|
2187
|
+
midground: { blur: 10, offset: 4, opacity: 0.2 },
|
|
2188
|
+
foreground: { blur: 20, offset: 8, opacity: 0.3 },
|
|
2189
|
+
overlay: { blur: 4, offset: 2, opacity: 0.15 }
|
|
2190
|
+
};
|
|
2191
|
+
});
|
|
2192
|
+
|
|
2193
|
+
// src/operations.ts
|
|
2194
|
+
import sharp2 from "sharp";
|
|
2195
|
+
import fs2 from "fs";
|
|
2196
|
+
import path2 from "path";
|
|
2197
|
+
function log2(msg) {
|
|
2198
|
+
process.stderr.write(`[picture-it] ${msg}
|
|
2199
|
+
`);
|
|
2200
|
+
}
|
|
2201
|
+
function parseSize2(sizeStr, platform) {
|
|
2202
|
+
if (sizeStr) {
|
|
2203
|
+
const [w, h] = sizeStr.split("x").map(Number);
|
|
2204
|
+
if (w && h)
|
|
2205
|
+
return { width: w, height: h };
|
|
2206
|
+
}
|
|
2207
|
+
if (platform && PLATFORM_PRESETS[platform]) {
|
|
2208
|
+
return {
|
|
2209
|
+
width: PLATFORM_PRESETS[platform].width,
|
|
2210
|
+
height: PLATFORM_PRESETS[platform].height
|
|
2211
|
+
};
|
|
2212
|
+
}
|
|
2213
|
+
return { width: 1200, height: 630 };
|
|
2214
|
+
}
|
|
2215
|
+
async function writeOutput2(buffer, outputPath) {
|
|
2216
|
+
const resolved = path2.resolve(outputPath);
|
|
2217
|
+
const ext = path2.extname(resolved).toLowerCase();
|
|
2218
|
+
let img = sharp2(buffer);
|
|
2219
|
+
switch (ext) {
|
|
2220
|
+
case ".jpg":
|
|
2221
|
+
case ".jpeg":
|
|
2222
|
+
await img.jpeg({ quality: 90 }).toFile(resolved);
|
|
2223
|
+
break;
|
|
2224
|
+
case ".webp":
|
|
2225
|
+
await img.webp({ quality: 90 }).toFile(resolved);
|
|
2226
|
+
break;
|
|
2227
|
+
default:
|
|
2228
|
+
await img.png({ quality: 90 }).toFile(resolved);
|
|
2229
|
+
break;
|
|
2230
|
+
}
|
|
2231
|
+
return resolved;
|
|
2232
|
+
}
|
|
2233
|
+
function ensureFalKey2() {
|
|
2234
|
+
const config = loadFalKey2();
|
|
2235
|
+
if (!config) {
|
|
2236
|
+
log2("No FAL API key configured. Run 'picture-it auth --fal <key>' to set up.");
|
|
2237
|
+
process.exit(1);
|
|
2238
|
+
}
|
|
2239
|
+
return config;
|
|
2240
|
+
}
|
|
2241
|
+
function loadFalKey2() {
|
|
2242
|
+
if (process.env["FAL_KEY"])
|
|
2243
|
+
return process.env["FAL_KEY"];
|
|
2244
|
+
const configPath = path2.join(process.env["HOME"] || "~", ".picture-it", "config.json");
|
|
2245
|
+
try {
|
|
2246
|
+
const raw = fs2.readFileSync(configPath, "utf-8");
|
|
2247
|
+
const config = JSON.parse(raw);
|
|
2248
|
+
return config.fal_key;
|
|
2249
|
+
} catch {
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
var init_operations = __esm(() => {
|
|
2254
|
+
init_presets();
|
|
2255
|
+
});
|
|
2256
|
+
|
|
2257
|
+
// src/model-router.ts
|
|
2258
|
+
function getEndpoint(model) {
|
|
2259
|
+
return MODEL_ENDPOINTS[model];
|
|
2260
|
+
}
|
|
2261
|
+
function getCost(model) {
|
|
2262
|
+
return MODEL_COSTS[model];
|
|
2263
|
+
}
|
|
2264
|
+
function selectGenerateModel(explicit, verbose = false) {
|
|
2265
|
+
if (explicit && explicit in MODEL_ENDPOINTS)
|
|
2266
|
+
return explicit;
|
|
2267
|
+
const model = "flux-schnell";
|
|
2268
|
+
if (verbose)
|
|
2269
|
+
log2(`Model: ${model} ($${getCost(model)}) — fast generation`);
|
|
2270
|
+
return model;
|
|
2271
|
+
}
|
|
2272
|
+
function selectEditModel(inputCount, explicit, verbose = false) {
|
|
2273
|
+
if (explicit && explicit in MODEL_ENDPOINTS)
|
|
2274
|
+
return explicit;
|
|
2275
|
+
let model;
|
|
2276
|
+
let reason;
|
|
2277
|
+
if (inputCount > 10) {
|
|
2278
|
+
model = "banana2";
|
|
2279
|
+
reason = `${inputCount} inputs (>10), needs banana2`;
|
|
2280
|
+
} else {
|
|
2281
|
+
model = "seedream";
|
|
2282
|
+
reason = "default edit model, $0.04";
|
|
2283
|
+
}
|
|
2284
|
+
if (verbose)
|
|
2285
|
+
log2(`Model: ${model} ($${getCost(model)}) — ${reason}`);
|
|
2286
|
+
return model;
|
|
2287
|
+
}
|
|
2288
|
+
function mapAspectRatio(width, height) {
|
|
2289
|
+
const ratio = width / height;
|
|
2290
|
+
if (Math.abs(ratio - 1) < 0.1)
|
|
2291
|
+
return "1:1";
|
|
2292
|
+
if (Math.abs(ratio - 16 / 9) < 0.15)
|
|
2293
|
+
return "16:9";
|
|
2294
|
+
if (Math.abs(ratio - 9 / 16) < 0.15)
|
|
2295
|
+
return "9:16";
|
|
2296
|
+
if (Math.abs(ratio - 4 / 3) < 0.15)
|
|
2297
|
+
return "4:3";
|
|
2298
|
+
if (Math.abs(ratio - 3 / 4) < 0.15)
|
|
2299
|
+
return "3:4";
|
|
2300
|
+
if (Math.abs(ratio - 3 / 2) < 0.15)
|
|
2301
|
+
return "3:2";
|
|
2302
|
+
if (Math.abs(ratio - 2 / 3) < 0.15)
|
|
2303
|
+
return "2:3";
|
|
2304
|
+
if (Math.abs(ratio - 21 / 9) < 0.2)
|
|
2305
|
+
return "21:9";
|
|
2306
|
+
if (ratio >= 3.5)
|
|
2307
|
+
return "4:1";
|
|
2308
|
+
return "auto";
|
|
2309
|
+
}
|
|
2310
|
+
function mapResolution(width, height) {
|
|
2311
|
+
const maxDim = Math.max(width, height);
|
|
2312
|
+
if (maxDim <= 512)
|
|
2313
|
+
return "0.5K";
|
|
2314
|
+
if (maxDim <= 1024)
|
|
2315
|
+
return "1K";
|
|
2316
|
+
if (maxDim <= 2048)
|
|
2317
|
+
return "2K";
|
|
2318
|
+
return "4K";
|
|
2319
|
+
}
|
|
2320
|
+
var MODEL_ENDPOINTS, MODEL_COSTS;
|
|
2321
|
+
var init_model_router = __esm(() => {
|
|
2322
|
+
init_operations();
|
|
2323
|
+
MODEL_ENDPOINTS = {
|
|
2324
|
+
seedream: "fal-ai/bytedance/seedream/v4.5/edit",
|
|
2325
|
+
banana2: "fal-ai/nano-banana-2/edit",
|
|
2326
|
+
"banana-pro": "fal-ai/nano-banana-pro/edit",
|
|
2327
|
+
"flux-dev": "fal-ai/flux/dev",
|
|
2328
|
+
"flux-schnell": "fal-ai/flux/schnell"
|
|
2329
|
+
};
|
|
2330
|
+
MODEL_COSTS = {
|
|
2331
|
+
seedream: 0.04,
|
|
2332
|
+
banana2: 0.08,
|
|
2333
|
+
"banana-pro": 0.15,
|
|
2334
|
+
"flux-dev": 0.03,
|
|
2335
|
+
"flux-schnell": 0.003
|
|
2336
|
+
};
|
|
2337
|
+
});
|
|
2338
|
+
|
|
2339
|
+
// src/types.ts
|
|
2340
|
+
var exports_types = {};
|
|
2341
|
+
__export(exports_types, {
|
|
2342
|
+
ZONES: () => ZONES
|
|
2343
|
+
});
|
|
2344
|
+
var ZONES;
|
|
2345
|
+
var init_types = __esm(() => {
|
|
2346
|
+
ZONES = {
|
|
2347
|
+
"hero-center": { x: 50, y: 45 },
|
|
2348
|
+
"title-area": { x: 50, y: 75 },
|
|
2349
|
+
"top-bar": { x: 50, y: 8 },
|
|
2350
|
+
"bottom-bar": { x: 50, y: 92 },
|
|
2351
|
+
"left-third": { x: 25, y: 50 },
|
|
2352
|
+
"right-third": { x: 75, y: 50 },
|
|
2353
|
+
"top-left-safe": { x: 15, y: 12 },
|
|
2354
|
+
"top-right-safe": { x: 85, y: 12 },
|
|
2355
|
+
"bottom-left-safe": { x: 15, y: 88 },
|
|
2356
|
+
"bottom-right-safe": { x: 85, y: 88 },
|
|
2357
|
+
"center-left": { x: 30, y: 50 },
|
|
2358
|
+
"center-right": { x: 70, y: 50 }
|
|
2359
|
+
};
|
|
2360
|
+
});
|
|
2361
|
+
|
|
2362
|
+
// src/zones.ts
|
|
2363
|
+
var exports_zones = {};
|
|
2364
|
+
__export(exports_zones, {
|
|
2365
|
+
resolvePosition: () => resolvePosition,
|
|
2366
|
+
resolveDimension: () => resolveDimension
|
|
2367
|
+
});
|
|
2368
|
+
function resolvePosition(zone, canvasWidth, canvasHeight, elementWidth, elementHeight, anchor = "center") {
|
|
2369
|
+
let centerX;
|
|
2370
|
+
let centerY;
|
|
2371
|
+
if (typeof zone === "string") {
|
|
2372
|
+
const z = ZONES[zone];
|
|
2373
|
+
if (!z)
|
|
2374
|
+
throw new Error(`Unknown zone: ${zone}`);
|
|
2375
|
+
centerX = z.x / 100 * canvasWidth;
|
|
2376
|
+
centerY = z.y / 100 * canvasHeight;
|
|
2377
|
+
} else {
|
|
2378
|
+
centerX = zone.x <= 100 && zone.x >= 0 ? zone.x / 100 * canvasWidth : zone.x;
|
|
2379
|
+
centerY = zone.y <= 100 && zone.y >= 0 ? zone.y / 100 * canvasHeight : zone.y;
|
|
2380
|
+
}
|
|
2381
|
+
switch (anchor) {
|
|
2382
|
+
case "center":
|
|
2383
|
+
return {
|
|
2384
|
+
x: Math.round(centerX - elementWidth / 2),
|
|
2385
|
+
y: Math.round(centerY - elementHeight / 2)
|
|
2386
|
+
};
|
|
2387
|
+
case "top-left":
|
|
2388
|
+
return { x: Math.round(centerX), y: Math.round(centerY) };
|
|
2389
|
+
case "top-right":
|
|
2390
|
+
return {
|
|
2391
|
+
x: Math.round(centerX - elementWidth),
|
|
2392
|
+
y: Math.round(centerY)
|
|
2393
|
+
};
|
|
2394
|
+
case "bottom-left":
|
|
2395
|
+
return {
|
|
2396
|
+
x: Math.round(centerX),
|
|
2397
|
+
y: Math.round(centerY - elementHeight)
|
|
2398
|
+
};
|
|
2399
|
+
case "bottom-right":
|
|
2400
|
+
return {
|
|
2401
|
+
x: Math.round(centerX - elementWidth),
|
|
2402
|
+
y: Math.round(centerY - elementHeight)
|
|
2403
|
+
};
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
function resolveDimension(value, canvasSize, fallback) {
|
|
2407
|
+
if (value === undefined)
|
|
2408
|
+
return fallback;
|
|
2409
|
+
if (typeof value === "number")
|
|
2410
|
+
return value;
|
|
2411
|
+
if (value.endsWith("%")) {
|
|
2412
|
+
return Math.round(parseFloat(value) / 100 * canvasSize);
|
|
2413
|
+
}
|
|
2414
|
+
return parseInt(value, 10) || fallback;
|
|
2415
|
+
}
|
|
2416
|
+
var init_zones = __esm(() => {
|
|
2417
|
+
init_types();
|
|
2418
|
+
});
|
|
2419
|
+
|
|
2420
|
+
// src/fal.ts
|
|
2421
|
+
var exports_fal = {};
|
|
2422
|
+
__export(exports_fal, {
|
|
2423
|
+
upscale: () => upscale2,
|
|
2424
|
+
uploadFile: () => uploadFile2,
|
|
2425
|
+
uploadBuffer: () => uploadBuffer2,
|
|
2426
|
+
removeBg: () => removeBg2,
|
|
2427
|
+
generate: () => generate2,
|
|
2428
|
+
edit: () => edit2,
|
|
2429
|
+
cropToExact: () => cropToExact2,
|
|
2430
|
+
configureFal: () => configureFal2
|
|
2431
|
+
});
|
|
2432
|
+
import { fal as fal2 } from "@fal-ai/client";
|
|
2433
|
+
import fs5 from "fs";
|
|
2434
|
+
import sharp6 from "sharp";
|
|
2435
|
+
function configureFal2(apiKey) {
|
|
2436
|
+
fal2.config({ credentials: apiKey });
|
|
2437
|
+
}
|
|
2438
|
+
async function uploadFile2(filePath) {
|
|
2439
|
+
const buffer = fs5.readFileSync(filePath);
|
|
2440
|
+
const filename = filePath.split("/").pop() || "image.png";
|
|
2441
|
+
const file = new File([buffer], filename, { type: "image/png" });
|
|
2442
|
+
return fal2.storage.upload(file);
|
|
2443
|
+
}
|
|
2444
|
+
async function uploadBuffer2(buffer, filename) {
|
|
2445
|
+
const file = new File([buffer], filename, { type: "image/png" });
|
|
2446
|
+
return fal2.storage.upload(file);
|
|
2447
|
+
}
|
|
2448
|
+
async function generate2(opts) {
|
|
2449
|
+
const model = selectGenerateModel(opts.model, opts.verbose);
|
|
2450
|
+
const endpoint = getEndpoint(model);
|
|
2451
|
+
const cost = getCost(model);
|
|
2452
|
+
log2(`FAL generate: ${model} @ $${cost.toFixed(3)}`);
|
|
2453
|
+
const w = opts.width || 1200;
|
|
2454
|
+
const h = opts.height || 630;
|
|
2455
|
+
const input = {
|
|
2456
|
+
prompt: opts.prompt,
|
|
2457
|
+
num_images: 1,
|
|
2458
|
+
image_size: mapFluxSize2(w, h)
|
|
2459
|
+
};
|
|
2460
|
+
const result = await fal2.subscribe(endpoint, {
|
|
2461
|
+
input,
|
|
2462
|
+
logs: true,
|
|
2463
|
+
onQueueUpdate: (update) => {
|
|
2464
|
+
if (update.status === "IN_PROGRESS" && opts.verbose) {
|
|
2465
|
+
for (const entry of update.logs || []) {
|
|
2466
|
+
log2(`FAL: ${entry.message}`);
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
});
|
|
2471
|
+
return downloadResult2(result);
|
|
2472
|
+
}
|
|
2473
|
+
async function edit2(opts) {
|
|
2474
|
+
const model = selectEditModel(opts.inputUrls.length, opts.model, opts.verbose);
|
|
2475
|
+
const endpoint = getEndpoint(model);
|
|
2476
|
+
const cost = getCost(model);
|
|
2477
|
+
log2(`FAL edit: ${model} @ $${cost.toFixed(2)} | ${opts.inputUrls.length} input(s)`);
|
|
2478
|
+
const w = opts.width || 1200;
|
|
2479
|
+
const h = opts.height || 630;
|
|
2480
|
+
let input;
|
|
2481
|
+
if (model === "seedream") {
|
|
2482
|
+
input = {
|
|
2483
|
+
prompt: opts.prompt,
|
|
2484
|
+
image_urls: opts.inputUrls,
|
|
2485
|
+
image_size: seedreamSize2(w, h),
|
|
2486
|
+
num_images: 1,
|
|
2487
|
+
max_images: 1
|
|
2488
|
+
};
|
|
2489
|
+
} else if (model === "banana2") {
|
|
2490
|
+
input = {
|
|
2491
|
+
prompt: opts.prompt,
|
|
2492
|
+
image_urls: opts.inputUrls,
|
|
2493
|
+
aspect_ratio: mapAspectRatio(w, h),
|
|
2494
|
+
resolution: mapResolution(w, h),
|
|
2495
|
+
output_format: "png",
|
|
2496
|
+
num_images: 1,
|
|
2497
|
+
limit_generations: true
|
|
2498
|
+
};
|
|
2499
|
+
} else {
|
|
2500
|
+
input = {
|
|
2501
|
+
prompt: opts.prompt,
|
|
2502
|
+
image_urls: opts.inputUrls,
|
|
2503
|
+
num_images: 1
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
const result = await fal2.subscribe(endpoint, {
|
|
2507
|
+
input,
|
|
2508
|
+
logs: true,
|
|
2509
|
+
onQueueUpdate: (update) => {
|
|
2510
|
+
if (update.status === "IN_PROGRESS" && opts.verbose) {
|
|
2511
|
+
for (const entry of update.logs || []) {
|
|
2512
|
+
log2(`FAL: ${entry.message}`);
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
});
|
|
2517
|
+
return downloadResult2(result);
|
|
2518
|
+
}
|
|
2519
|
+
async function removeBg2(opts) {
|
|
2520
|
+
const model = opts.model || "bria";
|
|
2521
|
+
const endpoint = BG_REMOVAL_ENDPOINTS2[model] || BG_REMOVAL_ENDPOINTS2.bria;
|
|
2522
|
+
log2(`FAL: ${model} background removal`);
|
|
2523
|
+
const result = await fal2.subscribe(endpoint, {
|
|
2524
|
+
input: { image_url: opts.inputUrl }
|
|
2525
|
+
});
|
|
2526
|
+
const data = result.data;
|
|
2527
|
+
const outputUrl = data?.image?.url || data?.images?.[0]?.url;
|
|
2528
|
+
if (!outputUrl)
|
|
2529
|
+
throw new Error(`${model} returned no image`);
|
|
2530
|
+
const response = await fetch(outputUrl);
|
|
2531
|
+
return Buffer.from(await response.arrayBuffer());
|
|
2532
|
+
}
|
|
2533
|
+
async function upscale2(opts) {
|
|
2534
|
+
log2(`FAL: upscale ${opts.scale || 2}x`);
|
|
2535
|
+
const result = await fal2.subscribe("fal-ai/creative-upscaler", {
|
|
2536
|
+
input: {
|
|
2537
|
+
image_url: opts.inputUrl,
|
|
2538
|
+
scale: opts.scale || 2
|
|
2539
|
+
}
|
|
2540
|
+
});
|
|
2541
|
+
return downloadResult2(result);
|
|
2542
|
+
}
|
|
2543
|
+
async function cropToExact2(buffer, width, height, position = "attention") {
|
|
2544
|
+
return sharp6(buffer).resize(width, height, {
|
|
2545
|
+
fit: "cover",
|
|
2546
|
+
position
|
|
2547
|
+
}).png().toBuffer();
|
|
2548
|
+
}
|
|
2549
|
+
async function downloadResult2(result) {
|
|
2550
|
+
const url = result?.data?.images?.[0]?.url || result?.data?.image?.url;
|
|
2551
|
+
if (!url)
|
|
2552
|
+
throw new Error("FAL returned no image URL");
|
|
2553
|
+
const response = await fetch(url);
|
|
2554
|
+
if (!response.ok)
|
|
2555
|
+
throw new Error(`Failed to download: ${response.status}`);
|
|
2556
|
+
return Buffer.from(await response.arrayBuffer());
|
|
2557
|
+
}
|
|
2558
|
+
function mapFluxSize2(w, h) {
|
|
2559
|
+
const ratio = w / h;
|
|
2560
|
+
if (Math.abs(ratio - 1) < 0.1)
|
|
2561
|
+
return "square_hd";
|
|
2562
|
+
if (ratio > 1.5)
|
|
2563
|
+
return "landscape_16_9";
|
|
2564
|
+
if (ratio < 0.67)
|
|
2565
|
+
return "portrait_16_9";
|
|
2566
|
+
if (ratio > 1)
|
|
2567
|
+
return "landscape_4_3";
|
|
2568
|
+
return "portrait_4_3";
|
|
2569
|
+
}
|
|
2570
|
+
function seedreamSize2(w, h) {
|
|
2571
|
+
if (w >= 1920 && h >= 1080 && w <= 4096 && h <= 4096) {
|
|
2572
|
+
return { width: w, height: h };
|
|
2573
|
+
}
|
|
2574
|
+
return "auto_2K";
|
|
2575
|
+
}
|
|
2576
|
+
var BG_REMOVAL_ENDPOINTS2;
|
|
2577
|
+
var init_fal = __esm(() => {
|
|
2578
|
+
init_operations();
|
|
2579
|
+
init_model_router();
|
|
2580
|
+
BG_REMOVAL_ENDPOINTS2 = {
|
|
2581
|
+
birefnet: "fal-ai/birefnet",
|
|
2582
|
+
bria: "fal-ai/bria/background/remove",
|
|
2583
|
+
pixelcut: "fal-ai/pixelcut/background-removal",
|
|
2584
|
+
rembg: "fal-ai/smoretalk-ai/rembg-enhance"
|
|
2585
|
+
};
|
|
2586
|
+
});
|
|
2587
|
+
|
|
2588
|
+
// node_modules/commander/esm.mjs
|
|
2589
|
+
var import__ = __toESM(require_commander(), 1);
|
|
2590
|
+
var {
|
|
2591
|
+
program,
|
|
2592
|
+
createCommand,
|
|
2593
|
+
createArgument,
|
|
2594
|
+
createOption,
|
|
2595
|
+
CommanderError,
|
|
2596
|
+
InvalidArgumentError,
|
|
2597
|
+
InvalidOptionArgumentError,
|
|
2598
|
+
Command,
|
|
2599
|
+
Argument,
|
|
2600
|
+
Option,
|
|
2601
|
+
Help
|
|
2602
|
+
} = import__.default;
|
|
2603
|
+
|
|
2604
|
+
// index.ts
|
|
2605
|
+
import fs9 from "fs";
|
|
2606
|
+
import path10 from "path";
|
|
2607
|
+
import sharp10 from "sharp";
|
|
2608
|
+
|
|
2609
|
+
// src/operations.ts
|
|
2610
|
+
init_presets();
|
|
2611
|
+
import sharp from "sharp";
|
|
2612
|
+
import fs from "fs";
|
|
2613
|
+
import path from "path";
|
|
2614
|
+
function log(msg) {
|
|
2615
|
+
process.stderr.write(`[picture-it] ${msg}
|
|
2616
|
+
`);
|
|
2617
|
+
}
|
|
2618
|
+
function parseSize(sizeStr, platform) {
|
|
2619
|
+
if (sizeStr) {
|
|
2620
|
+
const [w, h] = sizeStr.split("x").map(Number);
|
|
2621
|
+
if (w && h)
|
|
2622
|
+
return { width: w, height: h };
|
|
2623
|
+
}
|
|
2624
|
+
if (platform && PLATFORM_PRESETS[platform]) {
|
|
2625
|
+
return {
|
|
2626
|
+
width: PLATFORM_PRESETS[platform].width,
|
|
2627
|
+
height: PLATFORM_PRESETS[platform].height
|
|
2628
|
+
};
|
|
2629
|
+
}
|
|
2630
|
+
return { width: 1200, height: 630 };
|
|
2631
|
+
}
|
|
2632
|
+
async function readInput(inputPath) {
|
|
2633
|
+
if (!fs.existsSync(inputPath)) {
|
|
2634
|
+
log(`Input not found: ${inputPath}`);
|
|
2635
|
+
process.exit(1);
|
|
2636
|
+
}
|
|
2637
|
+
return sharp(inputPath).png().toBuffer();
|
|
2638
|
+
}
|
|
2639
|
+
async function writeOutput(buffer, outputPath) {
|
|
2640
|
+
const resolved = path.resolve(outputPath);
|
|
2641
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
2642
|
+
let img = sharp(buffer);
|
|
2643
|
+
switch (ext) {
|
|
2644
|
+
case ".jpg":
|
|
2645
|
+
case ".jpeg":
|
|
2646
|
+
await img.jpeg({ quality: 90 }).toFile(resolved);
|
|
2647
|
+
break;
|
|
2648
|
+
case ".webp":
|
|
2649
|
+
await img.webp({ quality: 90 }).toFile(resolved);
|
|
2650
|
+
break;
|
|
2651
|
+
default:
|
|
2652
|
+
await img.png({ quality: 90 }).toFile(resolved);
|
|
2653
|
+
break;
|
|
2654
|
+
}
|
|
2655
|
+
return resolved;
|
|
2656
|
+
}
|
|
2657
|
+
function ensureFalKey() {
|
|
2658
|
+
const config = loadFalKey();
|
|
2659
|
+
if (!config) {
|
|
2660
|
+
log("No FAL API key configured. Run 'picture-it auth --fal <key>' to set up.");
|
|
2661
|
+
process.exit(1);
|
|
2662
|
+
}
|
|
2663
|
+
return config;
|
|
2664
|
+
}
|
|
2665
|
+
function loadFalKey() {
|
|
2666
|
+
if (process.env["FAL_KEY"])
|
|
2667
|
+
return process.env["FAL_KEY"];
|
|
2668
|
+
const configPath = path.join(process.env["HOME"] || "~", ".picture-it", "config.json");
|
|
2669
|
+
try {
|
|
2670
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
2671
|
+
const config = JSON.parse(raw);
|
|
2672
|
+
return config.fal_key;
|
|
2673
|
+
} catch {
|
|
2674
|
+
return;
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
// src/fal.ts
|
|
2679
|
+
init_operations();
|
|
2680
|
+
init_model_router();
|
|
2681
|
+
import { fal } from "@fal-ai/client";
|
|
2682
|
+
import fs3 from "fs";
|
|
2683
|
+
import sharp3 from "sharp";
|
|
2684
|
+
function configureFal(apiKey) {
|
|
2685
|
+
fal.config({ credentials: apiKey });
|
|
2686
|
+
}
|
|
2687
|
+
async function uploadFile(filePath) {
|
|
2688
|
+
const buffer = fs3.readFileSync(filePath);
|
|
2689
|
+
const filename = filePath.split("/").pop() || "image.png";
|
|
2690
|
+
const file = new File([buffer], filename, { type: "image/png" });
|
|
2691
|
+
return fal.storage.upload(file);
|
|
2692
|
+
}
|
|
2693
|
+
async function uploadBuffer(buffer, filename) {
|
|
2694
|
+
const file = new File([buffer], filename, { type: "image/png" });
|
|
2695
|
+
return fal.storage.upload(file);
|
|
2696
|
+
}
|
|
2697
|
+
async function generate(opts) {
|
|
2698
|
+
const model = selectGenerateModel(opts.model, opts.verbose);
|
|
2699
|
+
const endpoint = getEndpoint(model);
|
|
2700
|
+
const cost = getCost(model);
|
|
2701
|
+
log2(`FAL generate: ${model} @ $${cost.toFixed(3)}`);
|
|
2702
|
+
const w = opts.width || 1200;
|
|
2703
|
+
const h = opts.height || 630;
|
|
2704
|
+
const input = {
|
|
2705
|
+
prompt: opts.prompt,
|
|
2706
|
+
num_images: 1,
|
|
2707
|
+
image_size: mapFluxSize(w, h)
|
|
2708
|
+
};
|
|
2709
|
+
const result = await fal.subscribe(endpoint, {
|
|
2710
|
+
input,
|
|
2711
|
+
logs: true,
|
|
2712
|
+
onQueueUpdate: (update) => {
|
|
2713
|
+
if (update.status === "IN_PROGRESS" && opts.verbose) {
|
|
2714
|
+
for (const entry of update.logs || []) {
|
|
2715
|
+
log2(`FAL: ${entry.message}`);
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
});
|
|
2720
|
+
return downloadResult(result);
|
|
2721
|
+
}
|
|
2722
|
+
async function edit(opts) {
|
|
2723
|
+
const model = selectEditModel(opts.inputUrls.length, opts.model, opts.verbose);
|
|
2724
|
+
const endpoint = getEndpoint(model);
|
|
2725
|
+
const cost = getCost(model);
|
|
2726
|
+
log2(`FAL edit: ${model} @ $${cost.toFixed(2)} | ${opts.inputUrls.length} input(s)`);
|
|
2727
|
+
const w = opts.width || 1200;
|
|
2728
|
+
const h = opts.height || 630;
|
|
2729
|
+
let input;
|
|
2730
|
+
if (model === "seedream") {
|
|
2731
|
+
input = {
|
|
2732
|
+
prompt: opts.prompt,
|
|
2733
|
+
image_urls: opts.inputUrls,
|
|
2734
|
+
image_size: seedreamSize(w, h),
|
|
2735
|
+
num_images: 1,
|
|
2736
|
+
max_images: 1
|
|
2737
|
+
};
|
|
2738
|
+
} else if (model === "banana2") {
|
|
2739
|
+
input = {
|
|
2740
|
+
prompt: opts.prompt,
|
|
2741
|
+
image_urls: opts.inputUrls,
|
|
2742
|
+
aspect_ratio: mapAspectRatio(w, h),
|
|
2743
|
+
resolution: mapResolution(w, h),
|
|
2744
|
+
output_format: "png",
|
|
2745
|
+
num_images: 1,
|
|
2746
|
+
limit_generations: true
|
|
2747
|
+
};
|
|
2748
|
+
} else {
|
|
2749
|
+
input = {
|
|
2750
|
+
prompt: opts.prompt,
|
|
2751
|
+
image_urls: opts.inputUrls,
|
|
2752
|
+
num_images: 1
|
|
2753
|
+
};
|
|
2754
|
+
}
|
|
2755
|
+
const result = await fal.subscribe(endpoint, {
|
|
2756
|
+
input,
|
|
2757
|
+
logs: true,
|
|
2758
|
+
onQueueUpdate: (update) => {
|
|
2759
|
+
if (update.status === "IN_PROGRESS" && opts.verbose) {
|
|
2760
|
+
for (const entry of update.logs || []) {
|
|
2761
|
+
log2(`FAL: ${entry.message}`);
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
});
|
|
2766
|
+
return downloadResult(result);
|
|
2767
|
+
}
|
|
2768
|
+
var BG_REMOVAL_ENDPOINTS = {
|
|
2769
|
+
birefnet: "fal-ai/birefnet",
|
|
2770
|
+
bria: "fal-ai/bria/background/remove",
|
|
2771
|
+
pixelcut: "fal-ai/pixelcut/background-removal",
|
|
2772
|
+
rembg: "fal-ai/smoretalk-ai/rembg-enhance"
|
|
2773
|
+
};
|
|
2774
|
+
async function removeBg(opts) {
|
|
2775
|
+
const model = opts.model || "bria";
|
|
2776
|
+
const endpoint = BG_REMOVAL_ENDPOINTS[model] || BG_REMOVAL_ENDPOINTS.bria;
|
|
2777
|
+
log2(`FAL: ${model} background removal`);
|
|
2778
|
+
const result = await fal.subscribe(endpoint, {
|
|
2779
|
+
input: { image_url: opts.inputUrl }
|
|
2780
|
+
});
|
|
2781
|
+
const data = result.data;
|
|
2782
|
+
const outputUrl = data?.image?.url || data?.images?.[0]?.url;
|
|
2783
|
+
if (!outputUrl)
|
|
2784
|
+
throw new Error(`${model} returned no image`);
|
|
2785
|
+
const response = await fetch(outputUrl);
|
|
2786
|
+
return Buffer.from(await response.arrayBuffer());
|
|
2787
|
+
}
|
|
2788
|
+
async function upscale(opts) {
|
|
2789
|
+
log2(`FAL: upscale ${opts.scale || 2}x`);
|
|
2790
|
+
const result = await fal.subscribe("fal-ai/creative-upscaler", {
|
|
2791
|
+
input: {
|
|
2792
|
+
image_url: opts.inputUrl,
|
|
2793
|
+
scale: opts.scale || 2
|
|
2794
|
+
}
|
|
2795
|
+
});
|
|
2796
|
+
return downloadResult(result);
|
|
2797
|
+
}
|
|
2798
|
+
async function cropToExact(buffer, width, height, position = "attention") {
|
|
2799
|
+
return sharp3(buffer).resize(width, height, {
|
|
2800
|
+
fit: "cover",
|
|
2801
|
+
position
|
|
2802
|
+
}).png().toBuffer();
|
|
2803
|
+
}
|
|
2804
|
+
async function downloadResult(result) {
|
|
2805
|
+
const url = result?.data?.images?.[0]?.url || result?.data?.image?.url;
|
|
2806
|
+
if (!url)
|
|
2807
|
+
throw new Error("FAL returned no image URL");
|
|
2808
|
+
const response = await fetch(url);
|
|
2809
|
+
if (!response.ok)
|
|
2810
|
+
throw new Error(`Failed to download: ${response.status}`);
|
|
2811
|
+
return Buffer.from(await response.arrayBuffer());
|
|
2812
|
+
}
|
|
2813
|
+
function mapFluxSize(w, h) {
|
|
2814
|
+
const ratio = w / h;
|
|
2815
|
+
if (Math.abs(ratio - 1) < 0.1)
|
|
2816
|
+
return "square_hd";
|
|
2817
|
+
if (ratio > 1.5)
|
|
2818
|
+
return "landscape_16_9";
|
|
2819
|
+
if (ratio < 0.67)
|
|
2820
|
+
return "portrait_16_9";
|
|
2821
|
+
if (ratio > 1)
|
|
2822
|
+
return "landscape_4_3";
|
|
2823
|
+
return "portrait_4_3";
|
|
2824
|
+
}
|
|
2825
|
+
function seedreamSize(w, h) {
|
|
2826
|
+
if (w >= 1920 && h >= 1080 && w <= 4096 && h <= 4096) {
|
|
2827
|
+
return { width: w, height: h };
|
|
2828
|
+
}
|
|
2829
|
+
return "auto_2K";
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
// src/compositor.ts
|
|
2833
|
+
init_zones();
|
|
2834
|
+
init_presets();
|
|
2835
|
+
import sharp4 from "sharp";
|
|
2836
|
+
import { Resvg } from "@resvg/resvg-js";
|
|
2837
|
+
import satori from "satori";
|
|
2838
|
+
import path5 from "path";
|
|
2839
|
+
|
|
2840
|
+
// src/fonts.ts
|
|
2841
|
+
import fs4 from "fs";
|
|
2842
|
+
import path4 from "path";
|
|
2843
|
+
|
|
2844
|
+
// src/config.ts
|
|
2845
|
+
import path3 from "path";
|
|
2846
|
+
var APP_DIR = path3.join(process.env["HOME"] || process.env["USERPROFILE"] || "~", ".picture-it");
|
|
2847
|
+
var CONFIG_PATH = path3.join(APP_DIR, "config.json");
|
|
2848
|
+
|
|
2849
|
+
// src/fonts.ts
|
|
2850
|
+
var USER_FONT_DIR = path4.join(APP_DIR, "fonts");
|
|
2851
|
+
var LOCAL_FONT_DIR = path4.join(import.meta.dirname, "..", "fonts");
|
|
2852
|
+
var FONT_FILES = [
|
|
2853
|
+
{
|
|
2854
|
+
name: "Inter",
|
|
2855
|
+
file: "Inter-Regular.ttf",
|
|
2856
|
+
weight: 400,
|
|
2857
|
+
style: "normal",
|
|
2858
|
+
url: "https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfMZg.ttf"
|
|
2859
|
+
},
|
|
2860
|
+
{
|
|
2861
|
+
name: "Inter",
|
|
2862
|
+
file: "Inter-SemiBold.ttf",
|
|
2863
|
+
weight: 600,
|
|
2864
|
+
style: "normal",
|
|
2865
|
+
url: "https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuGKYMZg.ttf"
|
|
2866
|
+
},
|
|
2867
|
+
{
|
|
2868
|
+
name: "Inter",
|
|
2869
|
+
file: "Inter-Bold.ttf",
|
|
2870
|
+
weight: 700,
|
|
2871
|
+
style: "normal",
|
|
2872
|
+
url: "https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuFuYMZg.ttf"
|
|
2873
|
+
},
|
|
2874
|
+
{
|
|
2875
|
+
name: "Space Grotesk",
|
|
2876
|
+
file: "SpaceGrotesk-Medium.ttf",
|
|
2877
|
+
weight: 500,
|
|
2878
|
+
style: "normal",
|
|
2879
|
+
url: "https://fonts.gstatic.com/s/spacegrotesk/v22/V8mQoQDjQSkFtoMM3T6r8E7mF71Q-gOoraIAEj7aUUsj.ttf"
|
|
2880
|
+
},
|
|
2881
|
+
{
|
|
2882
|
+
name: "Space Grotesk",
|
|
2883
|
+
file: "SpaceGrotesk-Bold.ttf",
|
|
2884
|
+
weight: 700,
|
|
2885
|
+
style: "normal",
|
|
2886
|
+
url: "https://fonts.gstatic.com/s/spacegrotesk/v22/V8mQoQDjQSkFtoMM3T6r8E7mF71Q-gOoraIAEj4PVksj.ttf"
|
|
2887
|
+
},
|
|
2888
|
+
{
|
|
2889
|
+
name: "DM Serif Display",
|
|
2890
|
+
file: "DMSerifDisplay-Regular.ttf",
|
|
2891
|
+
weight: 400,
|
|
2892
|
+
style: "normal",
|
|
2893
|
+
url: "https://fonts.gstatic.com/s/dmserifdisplay/v17/-nFnOHM81r4j6k0gjAW3mujVU2B2K_c.ttf"
|
|
2894
|
+
}
|
|
2895
|
+
];
|
|
2896
|
+
var cachedFonts = null;
|
|
2897
|
+
function getFontPathCandidates(file) {
|
|
2898
|
+
return [
|
|
2899
|
+
path4.join(USER_FONT_DIR, file),
|
|
2900
|
+
path4.join(LOCAL_FONT_DIR, file)
|
|
2901
|
+
];
|
|
2902
|
+
}
|
|
2903
|
+
function readFontFile(file) {
|
|
2904
|
+
for (const fontPath of getFontPathCandidates(file)) {
|
|
2905
|
+
try {
|
|
2906
|
+
return fs4.readFileSync(fontPath);
|
|
2907
|
+
} catch {}
|
|
2908
|
+
}
|
|
2909
|
+
return null;
|
|
2910
|
+
}
|
|
2911
|
+
async function loadFonts() {
|
|
2912
|
+
if (cachedFonts)
|
|
2913
|
+
return cachedFonts;
|
|
2914
|
+
const fonts = [];
|
|
2915
|
+
const available = [];
|
|
2916
|
+
const missing = [];
|
|
2917
|
+
for (const f of FONT_FILES) {
|
|
2918
|
+
const data = readFontFile(f.file);
|
|
2919
|
+
if (!data) {
|
|
2920
|
+
missing.push(f.file);
|
|
2921
|
+
continue;
|
|
2922
|
+
}
|
|
2923
|
+
fonts.push({
|
|
2924
|
+
name: f.name,
|
|
2925
|
+
data: data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength),
|
|
2926
|
+
weight: f.weight,
|
|
2927
|
+
style: f.style
|
|
2928
|
+
});
|
|
2929
|
+
available.push(f.file);
|
|
2930
|
+
}
|
|
2931
|
+
if (missing.length > 0) {
|
|
2932
|
+
process.stderr.write(`[picture-it] Warning: missing fonts: ${missing.join(", ")}
|
|
2933
|
+
`);
|
|
2934
|
+
process.stderr.write(`[picture-it] Run: picture-it download-fonts to fetch them
|
|
2935
|
+
`);
|
|
2936
|
+
}
|
|
2937
|
+
if (fonts.length === 0) {
|
|
2938
|
+
throw new Error("No fonts available. Run: picture-it download-fonts");
|
|
2939
|
+
}
|
|
2940
|
+
cachedFonts = fonts;
|
|
2941
|
+
return fonts;
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
// src/satori-jsx.ts
|
|
2945
|
+
function jsxToReact(node) {
|
|
2946
|
+
if (typeof node === "string")
|
|
2947
|
+
return node;
|
|
2948
|
+
const { tag, props = {}, children = [] } = node;
|
|
2949
|
+
const childElements = children.map((c) => jsxToReact(c));
|
|
2950
|
+
return {
|
|
2951
|
+
type: tag,
|
|
2952
|
+
props: {
|
|
2953
|
+
...props,
|
|
2954
|
+
children: childElements.length === 1 ? childElements[0] : childElements.length > 0 ? childElements : undefined
|
|
2955
|
+
}
|
|
2956
|
+
};
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
// src/compositor.ts
|
|
2960
|
+
init_operations();
|
|
2961
|
+
async function composite(baseImage, overlays, width, height, assetDir, verbose = false) {
|
|
2962
|
+
let canvasBuffer = await sharp4(baseImage).resize(width, height, { fit: "cover", position: "center" }).png().toBuffer();
|
|
2963
|
+
const sorted = [...overlays].sort((a, b) => {
|
|
2964
|
+
const da = DEPTH_ORDER[a.depth || "overlay"] ?? 3;
|
|
2965
|
+
const db = DEPTH_ORDER[b.depth || "overlay"] ?? 3;
|
|
2966
|
+
return da - db;
|
|
2967
|
+
});
|
|
2968
|
+
for (const overlay of sorted) {
|
|
2969
|
+
if (verbose)
|
|
2970
|
+
log2(`Compositing ${overlay.type} at depth ${overlay.depth || "overlay"}`);
|
|
2971
|
+
canvasBuffer = await compositeOverlay(canvasBuffer, overlay, width, height, assetDir, verbose);
|
|
2972
|
+
}
|
|
2973
|
+
return canvasBuffer;
|
|
2974
|
+
}
|
|
2975
|
+
async function compositeOverlay(canvasBuffer, overlay, canvasWidth, canvasHeight, assetDir, verbose) {
|
|
2976
|
+
switch (overlay.type) {
|
|
2977
|
+
case "image":
|
|
2978
|
+
return compositeImage(canvasBuffer, overlay, canvasWidth, canvasHeight, assetDir, verbose);
|
|
2979
|
+
case "satori-text":
|
|
2980
|
+
return compositeSatoriText(canvasBuffer, overlay, canvasWidth, canvasHeight, verbose);
|
|
2981
|
+
case "shape":
|
|
2982
|
+
return compositeShape(canvasBuffer, overlay, canvasWidth, canvasHeight, verbose);
|
|
2983
|
+
case "gradient-overlay":
|
|
2984
|
+
return compositeGradient(canvasBuffer, overlay, canvasWidth, canvasHeight, verbose);
|
|
2985
|
+
case "watermark":
|
|
2986
|
+
return compositeWatermark(canvasBuffer, overlay, canvasWidth, canvasHeight, assetDir, verbose);
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
async function compositeImage(canvasBuffer, overlay, canvasWidth, canvasHeight, assetDir, verbose) {
|
|
2990
|
+
const assetPath = path5.resolve(assetDir, overlay.src);
|
|
2991
|
+
let asset = sharp4(assetPath);
|
|
2992
|
+
const meta = await asset.metadata();
|
|
2993
|
+
const origW = meta.width || 100;
|
|
2994
|
+
const origH = meta.height || 100;
|
|
2995
|
+
let targetW = resolveDimension(overlay.width, canvasWidth, origW);
|
|
2996
|
+
let targetH = resolveDimension(overlay.height, canvasHeight, origH);
|
|
2997
|
+
if (overlay.width && !overlay.height) {
|
|
2998
|
+
targetH = Math.round(targetW * (origH / origW));
|
|
2999
|
+
} else if (overlay.height && !overlay.width) {
|
|
3000
|
+
targetW = Math.round(targetH * (origW / origH));
|
|
3001
|
+
}
|
|
3002
|
+
if (overlay.mask) {
|
|
3003
|
+
asset = await applyMask(asset, targetW, targetH, overlay.mask);
|
|
3004
|
+
}
|
|
3005
|
+
if (overlay.borderRadius) {
|
|
3006
|
+
asset = await applyBorderRadius(asset, targetW, targetH, overlay.borderRadius);
|
|
3007
|
+
}
|
|
3008
|
+
asset = asset.resize(targetW, targetH, { fit: "cover" });
|
|
3009
|
+
if (overlay.rotation) {
|
|
3010
|
+
asset = asset.rotate(overlay.rotation, { background: { r: 0, g: 0, b: 0, alpha: 0 } });
|
|
3011
|
+
}
|
|
3012
|
+
let assetBuffer = await asset.png().toBuffer();
|
|
3013
|
+
const pos = resolvePosition(overlay.zone || "hero-center", canvasWidth, canvasHeight, targetW, targetH, overlay.anchor || "center");
|
|
3014
|
+
const composites = [];
|
|
3015
|
+
if (overlay.shadow) {
|
|
3016
|
+
const shadowConfig = resolveShadow(overlay.shadow, overlay.depth);
|
|
3017
|
+
if (shadowConfig) {
|
|
3018
|
+
const shadowBuf = await createShadow(assetBuffer, targetW, targetH, shadowConfig);
|
|
3019
|
+
composites.push({
|
|
3020
|
+
input: shadowBuf,
|
|
3021
|
+
left: Math.max(0, pos.x + shadowConfig.offsetX),
|
|
3022
|
+
top: Math.max(0, pos.y + shadowConfig.offsetY),
|
|
3023
|
+
blend: "over"
|
|
3024
|
+
});
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
if (overlay.glow) {
|
|
3028
|
+
const glowBuf = await createGlow(assetBuffer, targetW, targetH, overlay.glow);
|
|
3029
|
+
const glowExtend = overlay.glow.spread + overlay.glow.blur;
|
|
3030
|
+
composites.push({
|
|
3031
|
+
input: glowBuf,
|
|
3032
|
+
left: Math.max(0, pos.x - glowExtend),
|
|
3033
|
+
top: Math.max(0, pos.y - glowExtend),
|
|
3034
|
+
blend: "over"
|
|
3035
|
+
});
|
|
3036
|
+
}
|
|
3037
|
+
if (overlay.reflection) {
|
|
3038
|
+
const reflBuf = await createReflection(assetBuffer, targetW, targetH, overlay.reflection);
|
|
3039
|
+
composites.push({
|
|
3040
|
+
input: reflBuf,
|
|
3041
|
+
left: pos.x,
|
|
3042
|
+
top: pos.y + targetH + 2,
|
|
3043
|
+
blend: "over"
|
|
3044
|
+
});
|
|
3045
|
+
}
|
|
3046
|
+
const opacity = overlay.opacity ?? 1;
|
|
3047
|
+
if (opacity < 1) {
|
|
3048
|
+
assetBuffer = await applyOpacity(assetBuffer, opacity);
|
|
3049
|
+
}
|
|
3050
|
+
composites.push({
|
|
3051
|
+
input: assetBuffer,
|
|
3052
|
+
left: Math.max(0, pos.x),
|
|
3053
|
+
top: Math.max(0, pos.y),
|
|
3054
|
+
blend: "over"
|
|
3055
|
+
});
|
|
3056
|
+
return sharp4(canvasBuffer).composite(composites).png().toBuffer();
|
|
3057
|
+
}
|
|
3058
|
+
async function compositeSatoriText(canvasBuffer, overlay, canvasWidth, canvasHeight, verbose) {
|
|
3059
|
+
const textW = overlay.width || canvasWidth;
|
|
3060
|
+
const textH = overlay.height || canvasHeight;
|
|
3061
|
+
const fonts = await loadFonts();
|
|
3062
|
+
const reactElement = jsxToReact(overlay.jsx);
|
|
3063
|
+
const svg = await satori(reactElement, {
|
|
3064
|
+
width: textW,
|
|
3065
|
+
height: textH,
|
|
3066
|
+
fonts
|
|
3067
|
+
});
|
|
3068
|
+
const resvg = new Resvg(svg, {
|
|
3069
|
+
fitTo: { mode: "width", value: textW }
|
|
3070
|
+
});
|
|
3071
|
+
const pngBuffer = resvg.render().asPng();
|
|
3072
|
+
const pos = resolvePosition(overlay.zone || "title-area", canvasWidth, canvasHeight, textW, textH, overlay.anchor || "center");
|
|
3073
|
+
let input = Buffer.from(pngBuffer);
|
|
3074
|
+
const opacity = overlay.opacity ?? 1;
|
|
3075
|
+
if (opacity < 1) {
|
|
3076
|
+
input = await applyOpacity(input, opacity);
|
|
3077
|
+
}
|
|
3078
|
+
return sharp4(canvasBuffer).composite([
|
|
3079
|
+
{
|
|
3080
|
+
input,
|
|
3081
|
+
left: Math.max(0, pos.x),
|
|
3082
|
+
top: Math.max(0, pos.y),
|
|
3083
|
+
blend: "over"
|
|
3084
|
+
}
|
|
3085
|
+
]).png().toBuffer();
|
|
3086
|
+
}
|
|
3087
|
+
async function compositeShape(canvasBuffer, overlay, canvasWidth, canvasHeight, verbose) {
|
|
3088
|
+
const w = overlay.width || 100;
|
|
3089
|
+
const h = overlay.height || 100;
|
|
3090
|
+
let svgContent = "";
|
|
3091
|
+
const fill = overlay.fill || "white";
|
|
3092
|
+
const stroke = overlay.stroke || "none";
|
|
3093
|
+
const strokeW = overlay.strokeWidth || 0;
|
|
3094
|
+
switch (overlay.shape) {
|
|
3095
|
+
case "rect":
|
|
3096
|
+
svgContent = `<rect x="${strokeW / 2}" y="${strokeW / 2}" width="${w - strokeW}" height="${h - strokeW}" rx="${overlay.borderRadius || 0}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}"/>`;
|
|
3097
|
+
break;
|
|
3098
|
+
case "circle":
|
|
3099
|
+
svgContent = `<ellipse cx="${w / 2}" cy="${h / 2}" rx="${(w - strokeW) / 2}" ry="${(h - strokeW) / 2}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}"/>`;
|
|
3100
|
+
break;
|
|
3101
|
+
case "line":
|
|
3102
|
+
if (overlay.from && overlay.to) {
|
|
3103
|
+
svgContent = `<line x1="${overlay.from.x}" y1="${overlay.from.y}" x2="${overlay.to.x}" y2="${overlay.to.y}" stroke="${stroke || fill}" stroke-width="${strokeW || 2}"/>`;
|
|
3104
|
+
}
|
|
3105
|
+
break;
|
|
3106
|
+
case "arrow":
|
|
3107
|
+
if (overlay.from && overlay.to) {
|
|
3108
|
+
const headSize = overlay.headSize || 10;
|
|
3109
|
+
svgContent = `
|
|
3110
|
+
<defs><marker id="ah" markerWidth="${headSize}" markerHeight="${headSize}" refX="${headSize}" refY="${headSize / 2}" orient="auto">
|
|
3111
|
+
<polygon points="0 0, ${headSize} ${headSize / 2}, 0 ${headSize}" fill="${stroke || fill}" />
|
|
3112
|
+
</marker></defs>
|
|
3113
|
+
<line x1="${overlay.from.x}" y1="${overlay.from.y}" x2="${overlay.to.x}" y2="${overlay.to.y}" stroke="${stroke || fill}" stroke-width="${strokeW || 2}" marker-end="url(#ah)"/>`;
|
|
3114
|
+
}
|
|
3115
|
+
break;
|
|
3116
|
+
}
|
|
3117
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">${svgContent}</svg>`;
|
|
3118
|
+
const resvg = new Resvg(svg, { fitTo: { mode: "width", value: w } });
|
|
3119
|
+
let pngBuffer = Buffer.from(resvg.render().asPng());
|
|
3120
|
+
const pos = resolvePosition(overlay.zone || "hero-center", canvasWidth, canvasHeight, w, h, "center");
|
|
3121
|
+
const opacity = overlay.opacity ?? 1;
|
|
3122
|
+
if (opacity < 1) {
|
|
3123
|
+
pngBuffer = await applyOpacity(pngBuffer, opacity);
|
|
3124
|
+
}
|
|
3125
|
+
return sharp4(canvasBuffer).composite([
|
|
3126
|
+
{
|
|
3127
|
+
input: pngBuffer,
|
|
3128
|
+
left: Math.max(0, pos.x),
|
|
3129
|
+
top: Math.max(0, pos.y),
|
|
3130
|
+
blend: "over"
|
|
3131
|
+
}
|
|
3132
|
+
]).png().toBuffer();
|
|
3133
|
+
}
|
|
3134
|
+
async function compositeGradient(canvasBuffer, overlay, canvasWidth, canvasHeight, verbose) {
|
|
3135
|
+
const fonts = await loadFonts();
|
|
3136
|
+
const jsx = {
|
|
3137
|
+
type: "div",
|
|
3138
|
+
props: {
|
|
3139
|
+
style: {
|
|
3140
|
+
width: canvasWidth,
|
|
3141
|
+
height: canvasHeight,
|
|
3142
|
+
backgroundImage: overlay.gradient,
|
|
3143
|
+
display: "flex"
|
|
3144
|
+
},
|
|
3145
|
+
children: []
|
|
3146
|
+
}
|
|
3147
|
+
};
|
|
3148
|
+
const svg = await satori(jsx, {
|
|
3149
|
+
width: canvasWidth,
|
|
3150
|
+
height: canvasHeight,
|
|
3151
|
+
fonts
|
|
3152
|
+
});
|
|
3153
|
+
const resvg = new Resvg(svg, { fitTo: { mode: "width", value: canvasWidth } });
|
|
3154
|
+
let gradBuffer = Buffer.from(resvg.render().asPng());
|
|
3155
|
+
const opacity = overlay.opacity ?? 1;
|
|
3156
|
+
if (opacity < 1) {
|
|
3157
|
+
gradBuffer = await applyOpacity(gradBuffer, opacity);
|
|
3158
|
+
}
|
|
3159
|
+
const blend = overlay.blend || "normal";
|
|
3160
|
+
const sharpBlend = blend === "normal" ? "over" : blend;
|
|
3161
|
+
return sharp4(canvasBuffer).composite([
|
|
3162
|
+
{
|
|
3163
|
+
input: gradBuffer,
|
|
3164
|
+
left: 0,
|
|
3165
|
+
top: 0,
|
|
3166
|
+
blend: sharpBlend
|
|
3167
|
+
}
|
|
3168
|
+
]).png().toBuffer();
|
|
3169
|
+
}
|
|
3170
|
+
async function compositeWatermark(canvasBuffer, overlay, canvasWidth, canvasHeight, assetDir, verbose) {
|
|
3171
|
+
const assetPath = path5.resolve(assetDir, overlay.src);
|
|
3172
|
+
const size = overlay.size || 48;
|
|
3173
|
+
const margin = overlay.margin || 20;
|
|
3174
|
+
const opacity = overlay.opacity ?? 0.3;
|
|
3175
|
+
let asset = await sharp4(assetPath).resize(size, size, { fit: "inside" }).png().toBuffer();
|
|
3176
|
+
const meta = await sharp4(asset).metadata();
|
|
3177
|
+
const w = meta.width || size;
|
|
3178
|
+
const h = meta.height || size;
|
|
3179
|
+
if (opacity < 1) {
|
|
3180
|
+
asset = await applyOpacity(asset, opacity);
|
|
3181
|
+
}
|
|
3182
|
+
let x, y;
|
|
3183
|
+
switch (overlay.position || "bottom-right") {
|
|
3184
|
+
case "bottom-right":
|
|
3185
|
+
x = canvasWidth - w - margin;
|
|
3186
|
+
y = canvasHeight - h - margin;
|
|
3187
|
+
break;
|
|
3188
|
+
case "bottom-left":
|
|
3189
|
+
x = margin;
|
|
3190
|
+
y = canvasHeight - h - margin;
|
|
3191
|
+
break;
|
|
3192
|
+
case "top-right":
|
|
3193
|
+
x = canvasWidth - w - margin;
|
|
3194
|
+
y = margin;
|
|
3195
|
+
break;
|
|
3196
|
+
case "top-left":
|
|
3197
|
+
x = margin;
|
|
3198
|
+
y = margin;
|
|
3199
|
+
break;
|
|
3200
|
+
}
|
|
3201
|
+
return sharp4(canvasBuffer).composite([
|
|
3202
|
+
{
|
|
3203
|
+
input: asset,
|
|
3204
|
+
left: x,
|
|
3205
|
+
top: y,
|
|
3206
|
+
blend: "over"
|
|
3207
|
+
}
|
|
3208
|
+
]).png().toBuffer();
|
|
3209
|
+
}
|
|
3210
|
+
function resolveShadow(shadow, depth) {
|
|
3211
|
+
if (shadow === "auto") {
|
|
3212
|
+
const preset = AUTO_SHADOW[depth || "foreground"];
|
|
3213
|
+
if (!preset)
|
|
3214
|
+
return null;
|
|
3215
|
+
return {
|
|
3216
|
+
blur: preset.blur,
|
|
3217
|
+
color: `rgba(0,0,0,0.5)`,
|
|
3218
|
+
offsetX: preset.offset,
|
|
3219
|
+
offsetY: preset.offset,
|
|
3220
|
+
opacity: preset.opacity
|
|
3221
|
+
};
|
|
3222
|
+
}
|
|
3223
|
+
return shadow;
|
|
3224
|
+
}
|
|
3225
|
+
async function createShadow(assetBuffer, width, height, config) {
|
|
3226
|
+
let shadow = sharp4(assetBuffer).ensureAlpha().tint(config.color).blur(Math.max(0.3, config.blur));
|
|
3227
|
+
let buf = await shadow.png().toBuffer();
|
|
3228
|
+
if (config.opacity !== undefined && config.opacity < 1) {
|
|
3229
|
+
buf = await applyOpacity(buf, config.opacity);
|
|
3230
|
+
}
|
|
3231
|
+
return buf;
|
|
3232
|
+
}
|
|
3233
|
+
async function createGlow(assetBuffer, width, height, config) {
|
|
3234
|
+
const extend = config.spread + config.blur;
|
|
3235
|
+
let glow = sharp4(assetBuffer).ensureAlpha().tint(config.color).extend({
|
|
3236
|
+
top: extend,
|
|
3237
|
+
bottom: extend,
|
|
3238
|
+
left: extend,
|
|
3239
|
+
right: extend,
|
|
3240
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
3241
|
+
}).blur(Math.max(0.3, config.blur));
|
|
3242
|
+
return glow.png().toBuffer();
|
|
3243
|
+
}
|
|
3244
|
+
async function createReflection(assetBuffer, width, height, config) {
|
|
3245
|
+
const flipped = await sharp4(assetBuffer).flip().png().toBuffer();
|
|
3246
|
+
const fadeH = Math.round(height * (config.fadeHeight / 100));
|
|
3247
|
+
const gradientSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
|
|
3248
|
+
<defs>
|
|
3249
|
+
<linearGradient id="fade" x1="0" y1="0" x2="0" y2="1">
|
|
3250
|
+
<stop offset="0" stop-color="white" stop-opacity="1"/>
|
|
3251
|
+
<stop offset="${fadeH / height}" stop-color="white" stop-opacity="0"/>
|
|
3252
|
+
</linearGradient>
|
|
3253
|
+
</defs>
|
|
3254
|
+
<rect width="${width}" height="${height}" fill="url(#fade)"/>
|
|
3255
|
+
</svg>`;
|
|
3256
|
+
const maskResvg = new Resvg(gradientSvg, { fitTo: { mode: "width", value: width } });
|
|
3257
|
+
const maskBuffer = Buffer.from(maskResvg.render().asPng());
|
|
3258
|
+
let result = await sharp4(flipped).composite([{ input: maskBuffer, blend: "dest-in" }]).png().toBuffer();
|
|
3259
|
+
if (config.opacity < 1) {
|
|
3260
|
+
result = await applyOpacity(result, config.opacity);
|
|
3261
|
+
}
|
|
3262
|
+
return result;
|
|
3263
|
+
}
|
|
3264
|
+
async function applyMask(asset, width, height, mask) {
|
|
3265
|
+
let svgShape;
|
|
3266
|
+
switch (mask) {
|
|
3267
|
+
case "circle":
|
|
3268
|
+
svgShape = `<ellipse cx="${width / 2}" cy="${height / 2}" rx="${width / 2}" ry="${height / 2}" fill="white"/>`;
|
|
3269
|
+
break;
|
|
3270
|
+
case "rounded":
|
|
3271
|
+
svgShape = `<rect width="${width}" height="${height}" rx="${Math.min(width, height) * 0.1}" ry="${Math.min(width, height) * 0.1}" fill="white"/>`;
|
|
3272
|
+
break;
|
|
3273
|
+
case "hexagon": {
|
|
3274
|
+
const cx = width / 2, cy = height / 2;
|
|
3275
|
+
const r = Math.min(width, height) / 2;
|
|
3276
|
+
const pts = Array.from({ length: 6 }, (_, i) => {
|
|
3277
|
+
const angle = Math.PI / 3 * i - Math.PI / 2;
|
|
3278
|
+
return `${cx + r * Math.cos(angle)},${cy + r * Math.sin(angle)}`;
|
|
3279
|
+
}).join(" ");
|
|
3280
|
+
svgShape = `<polygon points="${pts}" fill="white"/>`;
|
|
3281
|
+
break;
|
|
3282
|
+
}
|
|
3283
|
+
case "diamond": {
|
|
3284
|
+
const cx = width / 2, cy = height / 2;
|
|
3285
|
+
svgShape = `<polygon points="${cx},0 ${width},${cy} ${cx},${height} 0,${cy}" fill="white"/>`;
|
|
3286
|
+
break;
|
|
3287
|
+
}
|
|
3288
|
+
default:
|
|
3289
|
+
svgShape = `<path d="${mask}" fill="white"/>`;
|
|
3290
|
+
}
|
|
3291
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">${svgShape}</svg>`;
|
|
3292
|
+
const resvg = new Resvg(svg, { fitTo: { mode: "width", value: width } });
|
|
3293
|
+
const maskBuffer = Buffer.from(resvg.render().asPng());
|
|
3294
|
+
const resized = await asset.resize(width, height, { fit: "cover" }).png().toBuffer();
|
|
3295
|
+
const masked = await sharp4(resized).composite([{ input: maskBuffer, blend: "dest-in" }]).png().toBuffer();
|
|
3296
|
+
return sharp4(masked);
|
|
3297
|
+
}
|
|
3298
|
+
async function applyBorderRadius(asset, width, height, radius) {
|
|
3299
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
|
|
3300
|
+
<rect width="${width}" height="${height}" rx="${radius}" ry="${radius}" fill="white"/>
|
|
3301
|
+
</svg>`;
|
|
3302
|
+
const resvg = new Resvg(svg, { fitTo: { mode: "width", value: width } });
|
|
3303
|
+
const maskBuffer = Buffer.from(resvg.render().asPng());
|
|
3304
|
+
const resized = await asset.resize(width, height, { fit: "cover" }).png().toBuffer();
|
|
3305
|
+
const masked = await sharp4(resized).composite([{ input: maskBuffer, blend: "dest-in" }]).png().toBuffer();
|
|
3306
|
+
return sharp4(masked);
|
|
3307
|
+
}
|
|
3308
|
+
async function applyOpacity(buffer, opacity) {
|
|
3309
|
+
const { data, info } = await sharp4(buffer).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
3310
|
+
for (let i = 3;i < data.length; i += 4) {
|
|
3311
|
+
data[i] = Math.round(data[i] * opacity);
|
|
3312
|
+
}
|
|
3313
|
+
return sharp4(data, {
|
|
3314
|
+
raw: { width: info.width, height: info.height, channels: 4 }
|
|
3315
|
+
}).png().toBuffer();
|
|
3316
|
+
}
|
|
3317
|
+
|
|
3318
|
+
// src/postprocess.ts
|
|
3319
|
+
import sharp5 from "sharp";
|
|
3320
|
+
import { Resvg as Resvg2 } from "@resvg/resvg-js";
|
|
3321
|
+
async function applyColorGrade(buffer, grade) {
|
|
3322
|
+
let img = sharp5(buffer);
|
|
3323
|
+
switch (grade) {
|
|
3324
|
+
case "cinematic":
|
|
3325
|
+
img = img.recomb([
|
|
3326
|
+
[1.05, 0, 0.05],
|
|
3327
|
+
[0, 1.1, 0],
|
|
3328
|
+
[0.05, 0, 1.15]
|
|
3329
|
+
]).linear(1.1, -10);
|
|
3330
|
+
break;
|
|
3331
|
+
case "moody":
|
|
3332
|
+
img = img.modulate({ saturation: 0.8 }).linear(1.15, 5);
|
|
3333
|
+
break;
|
|
3334
|
+
case "vibrant":
|
|
3335
|
+
img = img.modulate({ saturation: 1.3 });
|
|
3336
|
+
break;
|
|
3337
|
+
case "clean":
|
|
3338
|
+
img = img.sharpen({ sigma: 0.5 });
|
|
3339
|
+
break;
|
|
3340
|
+
case "warm-editorial":
|
|
3341
|
+
img = img.tint({ r: 255, g: 220, b: 180 }).modulate({ saturation: 0.9 });
|
|
3342
|
+
break;
|
|
3343
|
+
case "cool-tech":
|
|
3344
|
+
img = img.tint({ r: 180, g: 200, b: 255 }).linear(1.2, -15);
|
|
3345
|
+
break;
|
|
3346
|
+
}
|
|
3347
|
+
return img.png().toBuffer();
|
|
3348
|
+
}
|
|
3349
|
+
async function applyGrain(buffer, intensity = 0.07) {
|
|
3350
|
+
const meta = await sharp5(buffer).metadata();
|
|
3351
|
+
const w = meta.width;
|
|
3352
|
+
const h = meta.height;
|
|
3353
|
+
const noiseData = Buffer.alloc(w * h * 4);
|
|
3354
|
+
for (let i = 0;i < noiseData.length; i += 4) {
|
|
3355
|
+
const v = Math.round(128 + (Math.random() - 0.5) * 80);
|
|
3356
|
+
noiseData[i] = v;
|
|
3357
|
+
noiseData[i + 1] = v;
|
|
3358
|
+
noiseData[i + 2] = v;
|
|
3359
|
+
noiseData[i + 3] = Math.round(255 * intensity);
|
|
3360
|
+
}
|
|
3361
|
+
const noiseBuffer = await sharp5(noiseData, {
|
|
3362
|
+
raw: { width: w, height: h, channels: 4 }
|
|
3363
|
+
}).png().toBuffer();
|
|
3364
|
+
return sharp5(buffer).composite([{ input: noiseBuffer, blend: "overlay" }]).png().toBuffer();
|
|
3365
|
+
}
|
|
3366
|
+
async function applyVignette(buffer, opacity = 0.35) {
|
|
3367
|
+
const meta = await sharp5(buffer).metadata();
|
|
3368
|
+
const w = meta.width;
|
|
3369
|
+
const h = meta.height;
|
|
3370
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">
|
|
3371
|
+
<defs>
|
|
3372
|
+
<radialGradient id="vig" cx="50%" cy="50%" r="70%">
|
|
3373
|
+
<stop offset="50%" stop-color="black" stop-opacity="0"/>
|
|
3374
|
+
<stop offset="100%" stop-color="black" stop-opacity="${opacity}"/>
|
|
3375
|
+
</radialGradient>
|
|
3376
|
+
</defs>
|
|
3377
|
+
<rect width="${w}" height="${h}" fill="url(#vig)"/>
|
|
3378
|
+
</svg>`;
|
|
3379
|
+
const resvg = new Resvg2(svg, { fitTo: { mode: "width", value: w } });
|
|
3380
|
+
const vigBuffer = Buffer.from(resvg.render().asPng());
|
|
3381
|
+
return sharp5(buffer).composite([{ input: vigBuffer, blend: "multiply" }]).png().toBuffer();
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3384
|
+
// src/pipeline.ts
|
|
3385
|
+
init_operations();
|
|
3386
|
+
init_fal();
|
|
3387
|
+
import sharp9 from "sharp";
|
|
3388
|
+
import satori3 from "satori";
|
|
3389
|
+
import { Resvg as Resvg5 } from "@resvg/resvg-js";
|
|
3390
|
+
import path7 from "path";
|
|
3391
|
+
import fs6 from "fs";
|
|
3392
|
+
|
|
3393
|
+
// src/compositor.ts
|
|
3394
|
+
init_zones();
|
|
3395
|
+
init_presets();
|
|
3396
|
+
import sharp7 from "sharp";
|
|
3397
|
+
import { Resvg as Resvg3 } from "@resvg/resvg-js";
|
|
3398
|
+
import satori2 from "satori";
|
|
3399
|
+
import path6 from "path";
|
|
3400
|
+
init_operations();
|
|
3401
|
+
async function composite2(baseImage, overlays, width, height, assetDir, verbose = false) {
|
|
3402
|
+
let canvasBuffer = await sharp7(baseImage).resize(width, height, { fit: "cover", position: "center" }).png().toBuffer();
|
|
3403
|
+
const sorted = [...overlays].sort((a, b) => {
|
|
3404
|
+
const da = DEPTH_ORDER[a.depth || "overlay"] ?? 3;
|
|
3405
|
+
const db = DEPTH_ORDER[b.depth || "overlay"] ?? 3;
|
|
3406
|
+
return da - db;
|
|
3407
|
+
});
|
|
3408
|
+
for (const overlay of sorted) {
|
|
3409
|
+
if (verbose)
|
|
3410
|
+
log2(`Compositing ${overlay.type} at depth ${overlay.depth || "overlay"}`);
|
|
3411
|
+
canvasBuffer = await compositeOverlay2(canvasBuffer, overlay, width, height, assetDir, verbose);
|
|
3412
|
+
}
|
|
3413
|
+
return canvasBuffer;
|
|
3414
|
+
}
|
|
3415
|
+
async function compositeOverlay2(canvasBuffer, overlay, canvasWidth, canvasHeight, assetDir, verbose) {
|
|
3416
|
+
switch (overlay.type) {
|
|
3417
|
+
case "image":
|
|
3418
|
+
return compositeImage2(canvasBuffer, overlay, canvasWidth, canvasHeight, assetDir, verbose);
|
|
3419
|
+
case "satori-text":
|
|
3420
|
+
return compositeSatoriText2(canvasBuffer, overlay, canvasWidth, canvasHeight, verbose);
|
|
3421
|
+
case "shape":
|
|
3422
|
+
return compositeShape2(canvasBuffer, overlay, canvasWidth, canvasHeight, verbose);
|
|
3423
|
+
case "gradient-overlay":
|
|
3424
|
+
return compositeGradient2(canvasBuffer, overlay, canvasWidth, canvasHeight, verbose);
|
|
3425
|
+
case "watermark":
|
|
3426
|
+
return compositeWatermark2(canvasBuffer, overlay, canvasWidth, canvasHeight, assetDir, verbose);
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
async function compositeImage2(canvasBuffer, overlay, canvasWidth, canvasHeight, assetDir, verbose) {
|
|
3430
|
+
const assetPath = path6.resolve(assetDir, overlay.src);
|
|
3431
|
+
let asset = sharp7(assetPath);
|
|
3432
|
+
const meta = await asset.metadata();
|
|
3433
|
+
const origW = meta.width || 100;
|
|
3434
|
+
const origH = meta.height || 100;
|
|
3435
|
+
let targetW = resolveDimension(overlay.width, canvasWidth, origW);
|
|
3436
|
+
let targetH = resolveDimension(overlay.height, canvasHeight, origH);
|
|
3437
|
+
if (overlay.width && !overlay.height) {
|
|
3438
|
+
targetH = Math.round(targetW * (origH / origW));
|
|
3439
|
+
} else if (overlay.height && !overlay.width) {
|
|
3440
|
+
targetW = Math.round(targetH * (origW / origH));
|
|
3441
|
+
}
|
|
3442
|
+
if (overlay.mask) {
|
|
3443
|
+
asset = await applyMask2(asset, targetW, targetH, overlay.mask);
|
|
3444
|
+
}
|
|
3445
|
+
if (overlay.borderRadius) {
|
|
3446
|
+
asset = await applyBorderRadius2(asset, targetW, targetH, overlay.borderRadius);
|
|
3447
|
+
}
|
|
3448
|
+
asset = asset.resize(targetW, targetH, { fit: "cover" });
|
|
3449
|
+
if (overlay.rotation) {
|
|
3450
|
+
asset = asset.rotate(overlay.rotation, { background: { r: 0, g: 0, b: 0, alpha: 0 } });
|
|
3451
|
+
}
|
|
3452
|
+
let assetBuffer = await asset.png().toBuffer();
|
|
3453
|
+
const pos = resolvePosition(overlay.zone || "hero-center", canvasWidth, canvasHeight, targetW, targetH, overlay.anchor || "center");
|
|
3454
|
+
const composites = [];
|
|
3455
|
+
if (overlay.shadow) {
|
|
3456
|
+
const shadowConfig = resolveShadow2(overlay.shadow, overlay.depth);
|
|
3457
|
+
if (shadowConfig) {
|
|
3458
|
+
const shadowBuf = await createShadow2(assetBuffer, targetW, targetH, shadowConfig);
|
|
3459
|
+
composites.push({
|
|
3460
|
+
input: shadowBuf,
|
|
3461
|
+
left: Math.max(0, pos.x + shadowConfig.offsetX),
|
|
3462
|
+
top: Math.max(0, pos.y + shadowConfig.offsetY),
|
|
3463
|
+
blend: "over"
|
|
3464
|
+
});
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
if (overlay.glow) {
|
|
3468
|
+
const glowBuf = await createGlow2(assetBuffer, targetW, targetH, overlay.glow);
|
|
3469
|
+
const glowExtend = overlay.glow.spread + overlay.glow.blur;
|
|
3470
|
+
composites.push({
|
|
3471
|
+
input: glowBuf,
|
|
3472
|
+
left: Math.max(0, pos.x - glowExtend),
|
|
3473
|
+
top: Math.max(0, pos.y - glowExtend),
|
|
3474
|
+
blend: "over"
|
|
3475
|
+
});
|
|
3476
|
+
}
|
|
3477
|
+
if (overlay.reflection) {
|
|
3478
|
+
const reflBuf = await createReflection2(assetBuffer, targetW, targetH, overlay.reflection);
|
|
3479
|
+
composites.push({
|
|
3480
|
+
input: reflBuf,
|
|
3481
|
+
left: pos.x,
|
|
3482
|
+
top: pos.y + targetH + 2,
|
|
3483
|
+
blend: "over"
|
|
3484
|
+
});
|
|
3485
|
+
}
|
|
3486
|
+
const opacity = overlay.opacity ?? 1;
|
|
3487
|
+
if (opacity < 1) {
|
|
3488
|
+
assetBuffer = await applyOpacity2(assetBuffer, opacity);
|
|
3489
|
+
}
|
|
3490
|
+
composites.push({
|
|
3491
|
+
input: assetBuffer,
|
|
3492
|
+
left: Math.max(0, pos.x),
|
|
3493
|
+
top: Math.max(0, pos.y),
|
|
3494
|
+
blend: "over"
|
|
3495
|
+
});
|
|
3496
|
+
return sharp7(canvasBuffer).composite(composites).png().toBuffer();
|
|
3497
|
+
}
|
|
3498
|
+
async function compositeSatoriText2(canvasBuffer, overlay, canvasWidth, canvasHeight, verbose) {
|
|
3499
|
+
const textW = overlay.width || canvasWidth;
|
|
3500
|
+
const textH = overlay.height || canvasHeight;
|
|
3501
|
+
const fonts = await loadFonts();
|
|
3502
|
+
const reactElement = jsxToReact(overlay.jsx);
|
|
3503
|
+
const svg = await satori2(reactElement, {
|
|
3504
|
+
width: textW,
|
|
3505
|
+
height: textH,
|
|
3506
|
+
fonts
|
|
3507
|
+
});
|
|
3508
|
+
const resvg = new Resvg3(svg, {
|
|
3509
|
+
fitTo: { mode: "width", value: textW }
|
|
3510
|
+
});
|
|
3511
|
+
const pngBuffer = resvg.render().asPng();
|
|
3512
|
+
const pos = resolvePosition(overlay.zone || "title-area", canvasWidth, canvasHeight, textW, textH, overlay.anchor || "center");
|
|
3513
|
+
let input = Buffer.from(pngBuffer);
|
|
3514
|
+
const opacity = overlay.opacity ?? 1;
|
|
3515
|
+
if (opacity < 1) {
|
|
3516
|
+
input = await applyOpacity2(input, opacity);
|
|
3517
|
+
}
|
|
3518
|
+
return sharp7(canvasBuffer).composite([
|
|
3519
|
+
{
|
|
3520
|
+
input,
|
|
3521
|
+
left: Math.max(0, pos.x),
|
|
3522
|
+
top: Math.max(0, pos.y),
|
|
3523
|
+
blend: "over"
|
|
3524
|
+
}
|
|
3525
|
+
]).png().toBuffer();
|
|
3526
|
+
}
|
|
3527
|
+
async function compositeShape2(canvasBuffer, overlay, canvasWidth, canvasHeight, verbose) {
|
|
3528
|
+
const w = overlay.width || 100;
|
|
3529
|
+
const h = overlay.height || 100;
|
|
3530
|
+
let svgContent = "";
|
|
3531
|
+
const fill = overlay.fill || "white";
|
|
3532
|
+
const stroke = overlay.stroke || "none";
|
|
3533
|
+
const strokeW = overlay.strokeWidth || 0;
|
|
3534
|
+
switch (overlay.shape) {
|
|
3535
|
+
case "rect":
|
|
3536
|
+
svgContent = `<rect x="${strokeW / 2}" y="${strokeW / 2}" width="${w - strokeW}" height="${h - strokeW}" rx="${overlay.borderRadius || 0}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}"/>`;
|
|
3537
|
+
break;
|
|
3538
|
+
case "circle":
|
|
3539
|
+
svgContent = `<ellipse cx="${w / 2}" cy="${h / 2}" rx="${(w - strokeW) / 2}" ry="${(h - strokeW) / 2}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}"/>`;
|
|
3540
|
+
break;
|
|
3541
|
+
case "line":
|
|
3542
|
+
if (overlay.from && overlay.to) {
|
|
3543
|
+
svgContent = `<line x1="${overlay.from.x}" y1="${overlay.from.y}" x2="${overlay.to.x}" y2="${overlay.to.y}" stroke="${stroke || fill}" stroke-width="${strokeW || 2}"/>`;
|
|
3544
|
+
}
|
|
3545
|
+
break;
|
|
3546
|
+
case "arrow":
|
|
3547
|
+
if (overlay.from && overlay.to) {
|
|
3548
|
+
const headSize = overlay.headSize || 10;
|
|
3549
|
+
svgContent = `
|
|
3550
|
+
<defs><marker id="ah" markerWidth="${headSize}" markerHeight="${headSize}" refX="${headSize}" refY="${headSize / 2}" orient="auto">
|
|
3551
|
+
<polygon points="0 0, ${headSize} ${headSize / 2}, 0 ${headSize}" fill="${stroke || fill}" />
|
|
3552
|
+
</marker></defs>
|
|
3553
|
+
<line x1="${overlay.from.x}" y1="${overlay.from.y}" x2="${overlay.to.x}" y2="${overlay.to.y}" stroke="${stroke || fill}" stroke-width="${strokeW || 2}" marker-end="url(#ah)"/>`;
|
|
3554
|
+
}
|
|
3555
|
+
break;
|
|
3556
|
+
}
|
|
3557
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">${svgContent}</svg>`;
|
|
3558
|
+
const resvg = new Resvg3(svg, { fitTo: { mode: "width", value: w } });
|
|
3559
|
+
let pngBuffer = Buffer.from(resvg.render().asPng());
|
|
3560
|
+
const pos = resolvePosition(overlay.zone || "hero-center", canvasWidth, canvasHeight, w, h, "center");
|
|
3561
|
+
const opacity = overlay.opacity ?? 1;
|
|
3562
|
+
if (opacity < 1) {
|
|
3563
|
+
pngBuffer = await applyOpacity2(pngBuffer, opacity);
|
|
3564
|
+
}
|
|
3565
|
+
return sharp7(canvasBuffer).composite([
|
|
3566
|
+
{
|
|
3567
|
+
input: pngBuffer,
|
|
3568
|
+
left: Math.max(0, pos.x),
|
|
3569
|
+
top: Math.max(0, pos.y),
|
|
3570
|
+
blend: "over"
|
|
3571
|
+
}
|
|
3572
|
+
]).png().toBuffer();
|
|
3573
|
+
}
|
|
3574
|
+
async function compositeGradient2(canvasBuffer, overlay, canvasWidth, canvasHeight, verbose) {
|
|
3575
|
+
const fonts = await loadFonts();
|
|
3576
|
+
const jsx = {
|
|
3577
|
+
type: "div",
|
|
3578
|
+
props: {
|
|
3579
|
+
style: {
|
|
3580
|
+
width: canvasWidth,
|
|
3581
|
+
height: canvasHeight,
|
|
3582
|
+
backgroundImage: overlay.gradient,
|
|
3583
|
+
display: "flex"
|
|
3584
|
+
},
|
|
3585
|
+
children: []
|
|
3586
|
+
}
|
|
3587
|
+
};
|
|
3588
|
+
const svg = await satori2(jsx, {
|
|
3589
|
+
width: canvasWidth,
|
|
3590
|
+
height: canvasHeight,
|
|
3591
|
+
fonts
|
|
3592
|
+
});
|
|
3593
|
+
const resvg = new Resvg3(svg, { fitTo: { mode: "width", value: canvasWidth } });
|
|
3594
|
+
let gradBuffer = Buffer.from(resvg.render().asPng());
|
|
3595
|
+
const opacity = overlay.opacity ?? 1;
|
|
3596
|
+
if (opacity < 1) {
|
|
3597
|
+
gradBuffer = await applyOpacity2(gradBuffer, opacity);
|
|
3598
|
+
}
|
|
3599
|
+
const blend = overlay.blend || "normal";
|
|
3600
|
+
const sharpBlend = blend === "normal" ? "over" : blend;
|
|
3601
|
+
return sharp7(canvasBuffer).composite([
|
|
3602
|
+
{
|
|
3603
|
+
input: gradBuffer,
|
|
3604
|
+
left: 0,
|
|
3605
|
+
top: 0,
|
|
3606
|
+
blend: sharpBlend
|
|
3607
|
+
}
|
|
3608
|
+
]).png().toBuffer();
|
|
3609
|
+
}
|
|
3610
|
+
async function compositeWatermark2(canvasBuffer, overlay, canvasWidth, canvasHeight, assetDir, verbose) {
|
|
3611
|
+
const assetPath = path6.resolve(assetDir, overlay.src);
|
|
3612
|
+
const size = overlay.size || 48;
|
|
3613
|
+
const margin = overlay.margin || 20;
|
|
3614
|
+
const opacity = overlay.opacity ?? 0.3;
|
|
3615
|
+
let asset = await sharp7(assetPath).resize(size, size, { fit: "inside" }).png().toBuffer();
|
|
3616
|
+
const meta = await sharp7(asset).metadata();
|
|
3617
|
+
const w = meta.width || size;
|
|
3618
|
+
const h = meta.height || size;
|
|
3619
|
+
if (opacity < 1) {
|
|
3620
|
+
asset = await applyOpacity2(asset, opacity);
|
|
3621
|
+
}
|
|
3622
|
+
let x, y;
|
|
3623
|
+
switch (overlay.position || "bottom-right") {
|
|
3624
|
+
case "bottom-right":
|
|
3625
|
+
x = canvasWidth - w - margin;
|
|
3626
|
+
y = canvasHeight - h - margin;
|
|
3627
|
+
break;
|
|
3628
|
+
case "bottom-left":
|
|
3629
|
+
x = margin;
|
|
3630
|
+
y = canvasHeight - h - margin;
|
|
3631
|
+
break;
|
|
3632
|
+
case "top-right":
|
|
3633
|
+
x = canvasWidth - w - margin;
|
|
3634
|
+
y = margin;
|
|
3635
|
+
break;
|
|
3636
|
+
case "top-left":
|
|
3637
|
+
x = margin;
|
|
3638
|
+
y = margin;
|
|
3639
|
+
break;
|
|
3640
|
+
}
|
|
3641
|
+
return sharp7(canvasBuffer).composite([
|
|
3642
|
+
{
|
|
3643
|
+
input: asset,
|
|
3644
|
+
left: x,
|
|
3645
|
+
top: y,
|
|
3646
|
+
blend: "over"
|
|
3647
|
+
}
|
|
3648
|
+
]).png().toBuffer();
|
|
3649
|
+
}
|
|
3650
|
+
function resolveShadow2(shadow, depth) {
|
|
3651
|
+
if (shadow === "auto") {
|
|
3652
|
+
const preset = AUTO_SHADOW[depth || "foreground"];
|
|
3653
|
+
if (!preset)
|
|
3654
|
+
return null;
|
|
3655
|
+
return {
|
|
3656
|
+
blur: preset.blur,
|
|
3657
|
+
color: `rgba(0,0,0,0.5)`,
|
|
3658
|
+
offsetX: preset.offset,
|
|
3659
|
+
offsetY: preset.offset,
|
|
3660
|
+
opacity: preset.opacity
|
|
3661
|
+
};
|
|
3662
|
+
}
|
|
3663
|
+
return shadow;
|
|
3664
|
+
}
|
|
3665
|
+
async function createShadow2(assetBuffer, width, height, config) {
|
|
3666
|
+
let shadow = sharp7(assetBuffer).ensureAlpha().tint(config.color).blur(Math.max(0.3, config.blur));
|
|
3667
|
+
let buf = await shadow.png().toBuffer();
|
|
3668
|
+
if (config.opacity !== undefined && config.opacity < 1) {
|
|
3669
|
+
buf = await applyOpacity2(buf, config.opacity);
|
|
3670
|
+
}
|
|
3671
|
+
return buf;
|
|
3672
|
+
}
|
|
3673
|
+
async function createGlow2(assetBuffer, width, height, config) {
|
|
3674
|
+
const extend = config.spread + config.blur;
|
|
3675
|
+
let glow = sharp7(assetBuffer).ensureAlpha().tint(config.color).extend({
|
|
3676
|
+
top: extend,
|
|
3677
|
+
bottom: extend,
|
|
3678
|
+
left: extend,
|
|
3679
|
+
right: extend,
|
|
3680
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
3681
|
+
}).blur(Math.max(0.3, config.blur));
|
|
3682
|
+
return glow.png().toBuffer();
|
|
3683
|
+
}
|
|
3684
|
+
async function createReflection2(assetBuffer, width, height, config) {
|
|
3685
|
+
const flipped = await sharp7(assetBuffer).flip().png().toBuffer();
|
|
3686
|
+
const fadeH = Math.round(height * (config.fadeHeight / 100));
|
|
3687
|
+
const gradientSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
|
|
3688
|
+
<defs>
|
|
3689
|
+
<linearGradient id="fade" x1="0" y1="0" x2="0" y2="1">
|
|
3690
|
+
<stop offset="0" stop-color="white" stop-opacity="1"/>
|
|
3691
|
+
<stop offset="${fadeH / height}" stop-color="white" stop-opacity="0"/>
|
|
3692
|
+
</linearGradient>
|
|
3693
|
+
</defs>
|
|
3694
|
+
<rect width="${width}" height="${height}" fill="url(#fade)"/>
|
|
3695
|
+
</svg>`;
|
|
3696
|
+
const maskResvg = new Resvg3(gradientSvg, { fitTo: { mode: "width", value: width } });
|
|
3697
|
+
const maskBuffer = Buffer.from(maskResvg.render().asPng());
|
|
3698
|
+
let result = await sharp7(flipped).composite([{ input: maskBuffer, blend: "dest-in" }]).png().toBuffer();
|
|
3699
|
+
if (config.opacity < 1) {
|
|
3700
|
+
result = await applyOpacity2(result, config.opacity);
|
|
3701
|
+
}
|
|
3702
|
+
return result;
|
|
3703
|
+
}
|
|
3704
|
+
async function applyMask2(asset, width, height, mask) {
|
|
3705
|
+
let svgShape;
|
|
3706
|
+
switch (mask) {
|
|
3707
|
+
case "circle":
|
|
3708
|
+
svgShape = `<ellipse cx="${width / 2}" cy="${height / 2}" rx="${width / 2}" ry="${height / 2}" fill="white"/>`;
|
|
3709
|
+
break;
|
|
3710
|
+
case "rounded":
|
|
3711
|
+
svgShape = `<rect width="${width}" height="${height}" rx="${Math.min(width, height) * 0.1}" ry="${Math.min(width, height) * 0.1}" fill="white"/>`;
|
|
3712
|
+
break;
|
|
3713
|
+
case "hexagon": {
|
|
3714
|
+
const cx = width / 2, cy = height / 2;
|
|
3715
|
+
const r = Math.min(width, height) / 2;
|
|
3716
|
+
const pts = Array.from({ length: 6 }, (_, i) => {
|
|
3717
|
+
const angle = Math.PI / 3 * i - Math.PI / 2;
|
|
3718
|
+
return `${cx + r * Math.cos(angle)},${cy + r * Math.sin(angle)}`;
|
|
3719
|
+
}).join(" ");
|
|
3720
|
+
svgShape = `<polygon points="${pts}" fill="white"/>`;
|
|
3721
|
+
break;
|
|
3722
|
+
}
|
|
3723
|
+
case "diamond": {
|
|
3724
|
+
const cx = width / 2, cy = height / 2;
|
|
3725
|
+
svgShape = `<polygon points="${cx},0 ${width},${cy} ${cx},${height} 0,${cy}" fill="white"/>`;
|
|
3726
|
+
break;
|
|
3727
|
+
}
|
|
3728
|
+
default:
|
|
3729
|
+
svgShape = `<path d="${mask}" fill="white"/>`;
|
|
3730
|
+
}
|
|
3731
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">${svgShape}</svg>`;
|
|
3732
|
+
const resvg = new Resvg3(svg, { fitTo: { mode: "width", value: width } });
|
|
3733
|
+
const maskBuffer = Buffer.from(resvg.render().asPng());
|
|
3734
|
+
const resized = await asset.resize(width, height, { fit: "cover" }).png().toBuffer();
|
|
3735
|
+
const masked = await sharp7(resized).composite([{ input: maskBuffer, blend: "dest-in" }]).png().toBuffer();
|
|
3736
|
+
return sharp7(masked);
|
|
3737
|
+
}
|
|
3738
|
+
async function applyBorderRadius2(asset, width, height, radius) {
|
|
3739
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
|
|
3740
|
+
<rect width="${width}" height="${height}" rx="${radius}" ry="${radius}" fill="white"/>
|
|
3741
|
+
</svg>`;
|
|
3742
|
+
const resvg = new Resvg3(svg, { fitTo: { mode: "width", value: width } });
|
|
3743
|
+
const maskBuffer = Buffer.from(resvg.render().asPng());
|
|
3744
|
+
const resized = await asset.resize(width, height, { fit: "cover" }).png().toBuffer();
|
|
3745
|
+
const masked = await sharp7(resized).composite([{ input: maskBuffer, blend: "dest-in" }]).png().toBuffer();
|
|
3746
|
+
return sharp7(masked);
|
|
3747
|
+
}
|
|
3748
|
+
async function applyOpacity2(buffer, opacity) {
|
|
3749
|
+
const { data, info } = await sharp7(buffer).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
3750
|
+
for (let i = 3;i < data.length; i += 4) {
|
|
3751
|
+
data[i] = Math.round(data[i] * opacity);
|
|
3752
|
+
}
|
|
3753
|
+
return sharp7(data, {
|
|
3754
|
+
raw: { width: info.width, height: info.height, channels: 4 }
|
|
3755
|
+
}).png().toBuffer();
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3758
|
+
// src/postprocess.ts
|
|
3759
|
+
import sharp8 from "sharp";
|
|
3760
|
+
import { Resvg as Resvg4 } from "@resvg/resvg-js";
|
|
3761
|
+
async function applyColorGrade2(buffer, grade) {
|
|
3762
|
+
let img = sharp8(buffer);
|
|
3763
|
+
switch (grade) {
|
|
3764
|
+
case "cinematic":
|
|
3765
|
+
img = img.recomb([
|
|
3766
|
+
[1.05, 0, 0.05],
|
|
3767
|
+
[0, 1.1, 0],
|
|
3768
|
+
[0.05, 0, 1.15]
|
|
3769
|
+
]).linear(1.1, -10);
|
|
3770
|
+
break;
|
|
3771
|
+
case "moody":
|
|
3772
|
+
img = img.modulate({ saturation: 0.8 }).linear(1.15, 5);
|
|
3773
|
+
break;
|
|
3774
|
+
case "vibrant":
|
|
3775
|
+
img = img.modulate({ saturation: 1.3 });
|
|
3776
|
+
break;
|
|
3777
|
+
case "clean":
|
|
3778
|
+
img = img.sharpen({ sigma: 0.5 });
|
|
3779
|
+
break;
|
|
3780
|
+
case "warm-editorial":
|
|
3781
|
+
img = img.tint({ r: 255, g: 220, b: 180 }).modulate({ saturation: 0.9 });
|
|
3782
|
+
break;
|
|
3783
|
+
case "cool-tech":
|
|
3784
|
+
img = img.tint({ r: 180, g: 200, b: 255 }).linear(1.2, -15);
|
|
3785
|
+
break;
|
|
3786
|
+
}
|
|
3787
|
+
return img.png().toBuffer();
|
|
3788
|
+
}
|
|
3789
|
+
async function applyGrain2(buffer, intensity = 0.07) {
|
|
3790
|
+
const meta = await sharp8(buffer).metadata();
|
|
3791
|
+
const w = meta.width;
|
|
3792
|
+
const h = meta.height;
|
|
3793
|
+
const noiseData = Buffer.alloc(w * h * 4);
|
|
3794
|
+
for (let i = 0;i < noiseData.length; i += 4) {
|
|
3795
|
+
const v = Math.round(128 + (Math.random() - 0.5) * 80);
|
|
3796
|
+
noiseData[i] = v;
|
|
3797
|
+
noiseData[i + 1] = v;
|
|
3798
|
+
noiseData[i + 2] = v;
|
|
3799
|
+
noiseData[i + 3] = Math.round(255 * intensity);
|
|
3800
|
+
}
|
|
3801
|
+
const noiseBuffer = await sharp8(noiseData, {
|
|
3802
|
+
raw: { width: w, height: h, channels: 4 }
|
|
3803
|
+
}).png().toBuffer();
|
|
3804
|
+
return sharp8(buffer).composite([{ input: noiseBuffer, blend: "overlay" }]).png().toBuffer();
|
|
3805
|
+
}
|
|
3806
|
+
async function applyVignette2(buffer, opacity = 0.35) {
|
|
3807
|
+
const meta = await sharp8(buffer).metadata();
|
|
3808
|
+
const w = meta.width;
|
|
3809
|
+
const h = meta.height;
|
|
3810
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">
|
|
3811
|
+
<defs>
|
|
3812
|
+
<radialGradient id="vig" cx="50%" cy="50%" r="70%">
|
|
3813
|
+
<stop offset="50%" stop-color="black" stop-opacity="0"/>
|
|
3814
|
+
<stop offset="100%" stop-color="black" stop-opacity="${opacity}"/>
|
|
3815
|
+
</radialGradient>
|
|
3816
|
+
</defs>
|
|
3817
|
+
<rect width="${w}" height="${h}" fill="url(#vig)"/>
|
|
3818
|
+
</svg>`;
|
|
3819
|
+
const resvg = new Resvg4(svg, { fitTo: { mode: "width", value: w } });
|
|
3820
|
+
const vigBuffer = Buffer.from(resvg.render().asPng());
|
|
3821
|
+
return sharp8(buffer).composite([{ input: vigBuffer, blend: "multiply" }]).png().toBuffer();
|
|
3822
|
+
}
|
|
3823
|
+
|
|
3824
|
+
// src/pipeline.ts
|
|
3825
|
+
async function executePipeline(steps, outputPath, verbose = false) {
|
|
3826
|
+
let buffer = null;
|
|
3827
|
+
const falKey = ensureFalKey2();
|
|
3828
|
+
configureFal2(falKey);
|
|
3829
|
+
for (let i = 0;i < steps.length; i++) {
|
|
3830
|
+
const step = steps[i];
|
|
3831
|
+
if (verbose)
|
|
3832
|
+
log2(`Pipeline step ${i + 1}/${steps.length}: ${step.op}`);
|
|
3833
|
+
switch (step.op) {
|
|
3834
|
+
case "generate": {
|
|
3835
|
+
const { width, height } = parseSize2(step.size, step.platform);
|
|
3836
|
+
buffer = await generate2({
|
|
3837
|
+
prompt: step.prompt,
|
|
3838
|
+
model: step.model,
|
|
3839
|
+
width,
|
|
3840
|
+
height,
|
|
3841
|
+
verbose
|
|
3842
|
+
});
|
|
3843
|
+
buffer = await cropToExact2(buffer, width, height);
|
|
3844
|
+
break;
|
|
3845
|
+
}
|
|
3846
|
+
case "edit": {
|
|
3847
|
+
if (!buffer && (!step.assets || step.assets.length === 0)) {
|
|
3848
|
+
throw new Error("Edit step requires input buffer or assets");
|
|
3849
|
+
}
|
|
3850
|
+
const urls = [];
|
|
3851
|
+
if (buffer) {
|
|
3852
|
+
urls.push(await uploadBuffer2(buffer, "input.png"));
|
|
3853
|
+
}
|
|
3854
|
+
if (step.assets) {
|
|
3855
|
+
for (const asset of step.assets) {
|
|
3856
|
+
urls.push(await uploadFile2(path7.resolve(asset)));
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
const size = step.size ? parseSize2(step.size) : buffer ? await getBufferSize(buffer) : { width: 1200, height: 630 };
|
|
3860
|
+
buffer = await edit2({
|
|
3861
|
+
inputUrls: urls,
|
|
3862
|
+
prompt: step.prompt,
|
|
3863
|
+
model: step.model,
|
|
3864
|
+
width: size.width,
|
|
3865
|
+
height: size.height,
|
|
3866
|
+
verbose
|
|
3867
|
+
});
|
|
3868
|
+
buffer = await cropToExact2(buffer, size.width, size.height);
|
|
3869
|
+
break;
|
|
3870
|
+
}
|
|
3871
|
+
case "remove-bg": {
|
|
3872
|
+
if (!buffer)
|
|
3873
|
+
throw new Error("remove-bg requires input");
|
|
3874
|
+
const url = await uploadBuffer2(buffer, "input.png");
|
|
3875
|
+
buffer = await removeBg2({ inputUrl: url, verbose });
|
|
3876
|
+
break;
|
|
3877
|
+
}
|
|
3878
|
+
case "replace-bg": {
|
|
3879
|
+
if (!buffer)
|
|
3880
|
+
throw new Error("replace-bg requires input");
|
|
3881
|
+
const cutoutUrl = await uploadBuffer2(buffer, "input.png");
|
|
3882
|
+
const cutout = await removeBg2({ inputUrl: cutoutUrl, verbose });
|
|
3883
|
+
const size = await getBufferSize(buffer);
|
|
3884
|
+
const bg = await generate2({
|
|
3885
|
+
prompt: step.prompt,
|
|
3886
|
+
model: step.model,
|
|
3887
|
+
width: size.width,
|
|
3888
|
+
height: size.height,
|
|
3889
|
+
verbose
|
|
3890
|
+
});
|
|
3891
|
+
const bgCropped = await cropToExact2(bg, size.width, size.height);
|
|
3892
|
+
buffer = await sharp9(bgCropped).composite([{ input: cutout, blend: "over" }]).png().toBuffer();
|
|
3893
|
+
break;
|
|
3894
|
+
}
|
|
3895
|
+
case "crop": {
|
|
3896
|
+
if (!buffer)
|
|
3897
|
+
throw new Error("crop requires input");
|
|
3898
|
+
const { width, height } = parseSize2(step.size);
|
|
3899
|
+
const pos = step.position || "attention";
|
|
3900
|
+
buffer = await cropToExact2(buffer, width, height, pos);
|
|
3901
|
+
break;
|
|
3902
|
+
}
|
|
3903
|
+
case "grade": {
|
|
3904
|
+
if (!buffer)
|
|
3905
|
+
throw new Error("grade requires input");
|
|
3906
|
+
buffer = await applyColorGrade2(buffer, step.name);
|
|
3907
|
+
break;
|
|
3908
|
+
}
|
|
3909
|
+
case "grain": {
|
|
3910
|
+
if (!buffer)
|
|
3911
|
+
throw new Error("grain requires input");
|
|
3912
|
+
buffer = await applyGrain2(buffer, step.intensity);
|
|
3913
|
+
break;
|
|
3914
|
+
}
|
|
3915
|
+
case "vignette": {
|
|
3916
|
+
if (!buffer)
|
|
3917
|
+
throw new Error("vignette requires input");
|
|
3918
|
+
buffer = await applyVignette2(buffer, step.opacity);
|
|
3919
|
+
break;
|
|
3920
|
+
}
|
|
3921
|
+
case "text": {
|
|
3922
|
+
if (!buffer)
|
|
3923
|
+
throw new Error("text requires input");
|
|
3924
|
+
buffer = await renderTextOnto(buffer, step);
|
|
3925
|
+
break;
|
|
3926
|
+
}
|
|
3927
|
+
case "compose": {
|
|
3928
|
+
if (!buffer)
|
|
3929
|
+
throw new Error("compose requires input");
|
|
3930
|
+
let overlays;
|
|
3931
|
+
if (typeof step.overlays === "string") {
|
|
3932
|
+
overlays = JSON.parse(fs6.readFileSync(path7.resolve(step.overlays), "utf-8"));
|
|
3933
|
+
} else {
|
|
3934
|
+
overlays = step.overlays;
|
|
3935
|
+
}
|
|
3936
|
+
const size = await getBufferSize(buffer);
|
|
3937
|
+
buffer = await composite2(buffer, overlays, size.width, size.height, process.cwd(), verbose);
|
|
3938
|
+
break;
|
|
3939
|
+
}
|
|
3940
|
+
case "upscale": {
|
|
3941
|
+
if (!buffer)
|
|
3942
|
+
throw new Error("upscale requires input");
|
|
3943
|
+
const { upscale: upscaleFn } = await Promise.resolve().then(() => (init_fal(), exports_fal));
|
|
3944
|
+
const url = await uploadBuffer2(buffer, "input.png");
|
|
3945
|
+
buffer = await upscaleFn({ inputUrl: url, scale: step.scale, verbose });
|
|
3946
|
+
break;
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
}
|
|
3950
|
+
if (!buffer)
|
|
3951
|
+
throw new Error("Pipeline produced no output");
|
|
3952
|
+
const finalPath = await writeOutput2(buffer, outputPath);
|
|
3953
|
+
return finalPath;
|
|
3954
|
+
}
|
|
3955
|
+
async function getBufferSize(buffer) {
|
|
3956
|
+
const meta = await sharp9(buffer).metadata();
|
|
3957
|
+
return { width: meta.width || 1200, height: meta.height || 630 };
|
|
3958
|
+
}
|
|
3959
|
+
async function renderTextOnto(buffer, step) {
|
|
3960
|
+
const meta = await sharp9(buffer).metadata();
|
|
3961
|
+
const w = meta.width || 1200;
|
|
3962
|
+
const h = meta.height || 630;
|
|
3963
|
+
const fonts = await loadFonts();
|
|
3964
|
+
const fontSize = step.fontSize || 64;
|
|
3965
|
+
const fontFamily = step.font || "Space Grotesk";
|
|
3966
|
+
const color = step.color || "white";
|
|
3967
|
+
const jsx = {
|
|
3968
|
+
type: "div",
|
|
3969
|
+
props: {
|
|
3970
|
+
style: {
|
|
3971
|
+
display: "flex",
|
|
3972
|
+
alignItems: "center",
|
|
3973
|
+
justifyContent: "center",
|
|
3974
|
+
width: "100%",
|
|
3975
|
+
height: "100%"
|
|
3976
|
+
},
|
|
3977
|
+
children: {
|
|
3978
|
+
type: "span",
|
|
3979
|
+
props: {
|
|
3980
|
+
style: {
|
|
3981
|
+
fontSize,
|
|
3982
|
+
fontFamily,
|
|
3983
|
+
fontWeight: 700,
|
|
3984
|
+
color,
|
|
3985
|
+
textShadow: "0 2px 10px rgba(0,0,0,0.5)",
|
|
3986
|
+
textAlign: "center"
|
|
3987
|
+
},
|
|
3988
|
+
children: step.title
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
3992
|
+
};
|
|
3993
|
+
const textW = Math.round(w * 0.9);
|
|
3994
|
+
const textH = Math.round(h * 0.3);
|
|
3995
|
+
const svg = await satori3(jsx, { width: textW, height: textH, fonts });
|
|
3996
|
+
const resvg = new Resvg5(svg, { fitTo: { mode: "width", value: textW } });
|
|
3997
|
+
const textPng = Buffer.from(resvg.render().asPng());
|
|
3998
|
+
const { resolvePosition: resolvePosition2 } = await Promise.resolve().then(() => (init_zones(), exports_zones));
|
|
3999
|
+
const { ZONES: ZONES2 } = await Promise.resolve().then(() => (init_types(), exports_types));
|
|
4000
|
+
const zoneName = step.zone || "hero-center";
|
|
4001
|
+
const pos = resolvePosition2(zoneName, w, h, textW, textH, "center");
|
|
4002
|
+
return sharp9(buffer).composite([{ input: textPng, left: Math.max(0, pos.x), top: Math.max(0, pos.y), blend: "over" }]).png().toBuffer();
|
|
4003
|
+
}
|
|
4004
|
+
async function createGradientBackground(gradient, width, height) {
|
|
4005
|
+
const fonts = await loadFonts();
|
|
4006
|
+
const jsx = {
|
|
4007
|
+
type: "div",
|
|
4008
|
+
props: {
|
|
4009
|
+
style: { width, height, backgroundImage: gradient, display: "flex" },
|
|
4010
|
+
children: []
|
|
4011
|
+
}
|
|
4012
|
+
};
|
|
4013
|
+
const svg = await satori3(jsx, { width, height, fonts });
|
|
4014
|
+
const resvg = new Resvg5(svg, { fitTo: { mode: "width", value: width } });
|
|
4015
|
+
return Buffer.from(resvg.render().asPng());
|
|
4016
|
+
}
|
|
4017
|
+
|
|
4018
|
+
// src/templates/index.ts
|
|
4019
|
+
var TEMPLATES = {
|
|
4020
|
+
"vs-comparison": vsComparison,
|
|
4021
|
+
"feature-hero": featureHero,
|
|
4022
|
+
"text-hero": textHero,
|
|
4023
|
+
"social-card": socialCard
|
|
4024
|
+
};
|
|
4025
|
+
function vsComparison(data, w, h) {
|
|
4026
|
+
const leftLogo = data["leftLogo"] || data["left-logo"];
|
|
4027
|
+
const rightLogo = data["rightLogo"] || data["right-logo"];
|
|
4028
|
+
const vsText = data["vsText"] || "VS";
|
|
4029
|
+
const glowLeft = data["glowColorLeft"] || data["glow-color"] || "#7c3aed";
|
|
4030
|
+
const glowRight = data["glowColorRight"] || "#ef4444";
|
|
4031
|
+
const leftLabel = data["leftLabel"];
|
|
4032
|
+
const rightLabel = data["rightLabel"];
|
|
4033
|
+
const bg = data["background"] || `linear-gradient(135deg, #0f0f23 0%, #1a1a3e 50%, #0f0f23 100%)`;
|
|
4034
|
+
const logoSize = Math.round(Math.min(w, h) * 0.25);
|
|
4035
|
+
const overlays = [];
|
|
4036
|
+
if (leftLogo) {
|
|
4037
|
+
overlays.push({
|
|
4038
|
+
type: "image",
|
|
4039
|
+
src: leftLogo,
|
|
4040
|
+
zone: "left-third",
|
|
4041
|
+
width: logoSize,
|
|
4042
|
+
height: logoSize,
|
|
4043
|
+
anchor: "center",
|
|
4044
|
+
glow: { color: glowLeft, blur: 30, spread: 10 },
|
|
4045
|
+
shadow: "auto",
|
|
4046
|
+
depth: "foreground"
|
|
4047
|
+
});
|
|
4048
|
+
}
|
|
4049
|
+
if (rightLogo) {
|
|
4050
|
+
overlays.push({
|
|
4051
|
+
type: "image",
|
|
4052
|
+
src: rightLogo,
|
|
4053
|
+
zone: "right-third",
|
|
4054
|
+
width: logoSize,
|
|
4055
|
+
height: logoSize,
|
|
4056
|
+
anchor: "center",
|
|
4057
|
+
glow: { color: glowRight, blur: 30, spread: 10 },
|
|
4058
|
+
shadow: "auto",
|
|
4059
|
+
depth: "foreground"
|
|
4060
|
+
});
|
|
4061
|
+
}
|
|
4062
|
+
overlays.push({
|
|
4063
|
+
type: "satori-text",
|
|
4064
|
+
jsx: {
|
|
4065
|
+
tag: "div",
|
|
4066
|
+
props: {
|
|
4067
|
+
style: {
|
|
4068
|
+
display: "flex",
|
|
4069
|
+
alignItems: "center",
|
|
4070
|
+
justifyContent: "center",
|
|
4071
|
+
width: "100%",
|
|
4072
|
+
height: "100%"
|
|
4073
|
+
}
|
|
4074
|
+
},
|
|
4075
|
+
children: [
|
|
4076
|
+
{
|
|
4077
|
+
tag: "span",
|
|
4078
|
+
props: {
|
|
4079
|
+
style: {
|
|
4080
|
+
fontSize: Math.round(h * 0.15),
|
|
4081
|
+
fontFamily: "Space Grotesk",
|
|
4082
|
+
fontWeight: 700,
|
|
4083
|
+
color: "white",
|
|
4084
|
+
textShadow: "0 0 40px rgba(255,255,255,0.3)",
|
|
4085
|
+
letterSpacing: "0.05em"
|
|
4086
|
+
}
|
|
4087
|
+
},
|
|
4088
|
+
children: [vsText]
|
|
4089
|
+
}
|
|
4090
|
+
]
|
|
4091
|
+
},
|
|
4092
|
+
zone: "hero-center",
|
|
4093
|
+
width: Math.round(w * 0.2),
|
|
4094
|
+
height: Math.round(h * 0.3),
|
|
4095
|
+
anchor: "center",
|
|
4096
|
+
depth: "overlay"
|
|
4097
|
+
});
|
|
4098
|
+
if (leftLabel) {
|
|
4099
|
+
overlays.push({
|
|
4100
|
+
type: "satori-text",
|
|
4101
|
+
jsx: {
|
|
4102
|
+
tag: "div",
|
|
4103
|
+
props: {
|
|
4104
|
+
style: { display: "flex", alignItems: "center", justifyContent: "center", width: "100%", height: "100%" }
|
|
4105
|
+
},
|
|
4106
|
+
children: [{
|
|
4107
|
+
tag: "span",
|
|
4108
|
+
props: {
|
|
4109
|
+
style: {
|
|
4110
|
+
fontSize: Math.round(h * 0.04),
|
|
4111
|
+
fontFamily: "Inter",
|
|
4112
|
+
fontWeight: 600,
|
|
4113
|
+
color: "rgba(255,255,255,0.8)",
|
|
4114
|
+
textShadow: "0 2px 4px rgba(0,0,0,0.5)"
|
|
4115
|
+
}
|
|
4116
|
+
},
|
|
4117
|
+
children: [leftLabel]
|
|
4118
|
+
}]
|
|
4119
|
+
},
|
|
4120
|
+
zone: { x: 25, y: 75 },
|
|
4121
|
+
width: Math.round(w * 0.3),
|
|
4122
|
+
height: Math.round(h * 0.08),
|
|
4123
|
+
anchor: "center",
|
|
4124
|
+
depth: "overlay"
|
|
4125
|
+
});
|
|
4126
|
+
}
|
|
4127
|
+
if (rightLabel) {
|
|
4128
|
+
overlays.push({
|
|
4129
|
+
type: "satori-text",
|
|
4130
|
+
jsx: {
|
|
4131
|
+
tag: "div",
|
|
4132
|
+
props: {
|
|
4133
|
+
style: { display: "flex", alignItems: "center", justifyContent: "center", width: "100%", height: "100%" }
|
|
4134
|
+
},
|
|
4135
|
+
children: [{
|
|
4136
|
+
tag: "span",
|
|
4137
|
+
props: {
|
|
4138
|
+
style: {
|
|
4139
|
+
fontSize: Math.round(h * 0.04),
|
|
4140
|
+
fontFamily: "Inter",
|
|
4141
|
+
fontWeight: 600,
|
|
4142
|
+
color: "rgba(255,255,255,0.8)",
|
|
4143
|
+
textShadow: "0 2px 4px rgba(0,0,0,0.5)"
|
|
4144
|
+
}
|
|
4145
|
+
},
|
|
4146
|
+
children: [rightLabel]
|
|
4147
|
+
}]
|
|
4148
|
+
},
|
|
4149
|
+
zone: { x: 75, y: 75 },
|
|
4150
|
+
width: Math.round(w * 0.3),
|
|
4151
|
+
height: Math.round(h * 0.08),
|
|
4152
|
+
anchor: "center",
|
|
4153
|
+
depth: "overlay"
|
|
4154
|
+
});
|
|
4155
|
+
}
|
|
4156
|
+
return { overlays, background: bg };
|
|
4157
|
+
}
|
|
4158
|
+
function featureHero(data, w, h) {
|
|
4159
|
+
const logo = data["logo"];
|
|
4160
|
+
const title = data["title"] || "";
|
|
4161
|
+
const subtitle = data["subtitle"];
|
|
4162
|
+
const glowColor = data["glowColor"] || data["glow-color"] || "#3b82f6";
|
|
4163
|
+
const position = data["position"] || "center";
|
|
4164
|
+
const bg = data["background"] || `linear-gradient(135deg, #0a0a1a 0%, #1a1a3e 100%)`;
|
|
4165
|
+
const overlays = [];
|
|
4166
|
+
const xZone = position === "left" ? "center-left" : position === "right" ? "center-right" : "hero-center";
|
|
4167
|
+
if (logo) {
|
|
4168
|
+
const logoSize = Math.round(Math.min(w, h) * 0.3);
|
|
4169
|
+
overlays.push({
|
|
4170
|
+
type: "image",
|
|
4171
|
+
src: logo,
|
|
4172
|
+
zone: xZone,
|
|
4173
|
+
width: logoSize,
|
|
4174
|
+
height: logoSize,
|
|
4175
|
+
anchor: "center",
|
|
4176
|
+
glow: { color: glowColor, blur: 40, spread: 15 },
|
|
4177
|
+
shadow: "auto",
|
|
4178
|
+
depth: "foreground"
|
|
4179
|
+
});
|
|
4180
|
+
}
|
|
4181
|
+
const textChildren = [
|
|
4182
|
+
{
|
|
4183
|
+
tag: "span",
|
|
4184
|
+
props: {
|
|
4185
|
+
style: {
|
|
4186
|
+
fontSize: Math.round(h * 0.08),
|
|
4187
|
+
fontFamily: "Space Grotesk",
|
|
4188
|
+
fontWeight: 700,
|
|
4189
|
+
color: "white",
|
|
4190
|
+
textShadow: "0 2px 10px rgba(0,0,0,0.5)",
|
|
4191
|
+
textAlign: "center"
|
|
4192
|
+
}
|
|
4193
|
+
},
|
|
4194
|
+
children: [title]
|
|
4195
|
+
}
|
|
4196
|
+
];
|
|
4197
|
+
if (subtitle) {
|
|
4198
|
+
textChildren.push({
|
|
4199
|
+
tag: "span",
|
|
4200
|
+
props: {
|
|
4201
|
+
style: {
|
|
4202
|
+
fontSize: Math.round(h * 0.04),
|
|
4203
|
+
fontFamily: "Inter",
|
|
4204
|
+
fontWeight: 400,
|
|
4205
|
+
color: "rgba(255,255,255,0.7)",
|
|
4206
|
+
textShadow: "0 1px 4px rgba(0,0,0,0.5)",
|
|
4207
|
+
textAlign: "center",
|
|
4208
|
+
marginTop: 12
|
|
4209
|
+
}
|
|
4210
|
+
},
|
|
4211
|
+
children: [subtitle]
|
|
4212
|
+
});
|
|
4213
|
+
}
|
|
4214
|
+
overlays.push({
|
|
4215
|
+
type: "satori-text",
|
|
4216
|
+
jsx: {
|
|
4217
|
+
tag: "div",
|
|
4218
|
+
props: {
|
|
4219
|
+
style: {
|
|
4220
|
+
display: "flex",
|
|
4221
|
+
flexDirection: "column",
|
|
4222
|
+
alignItems: "center",
|
|
4223
|
+
justifyContent: "center",
|
|
4224
|
+
width: "100%",
|
|
4225
|
+
height: "100%"
|
|
4226
|
+
}
|
|
4227
|
+
},
|
|
4228
|
+
children: textChildren
|
|
4229
|
+
},
|
|
4230
|
+
zone: "title-area",
|
|
4231
|
+
width: Math.round(w * 0.8),
|
|
4232
|
+
height: Math.round(h * 0.25),
|
|
4233
|
+
anchor: "center",
|
|
4234
|
+
depth: "overlay"
|
|
4235
|
+
});
|
|
4236
|
+
return { overlays, background: bg };
|
|
4237
|
+
}
|
|
4238
|
+
function textHero(data, w, h) {
|
|
4239
|
+
const title = data["title"] || "";
|
|
4240
|
+
const subtitle = data["subtitle"];
|
|
4241
|
+
const badge = data["badge"];
|
|
4242
|
+
const textColor = data["textColor"] || "white";
|
|
4243
|
+
const bg = data["background"] || `linear-gradient(135deg, #667eea 0%, #764ba2 100%)`;
|
|
4244
|
+
const overlays = [];
|
|
4245
|
+
const children = [];
|
|
4246
|
+
if (badge) {
|
|
4247
|
+
children.push({
|
|
4248
|
+
tag: "div",
|
|
4249
|
+
props: {
|
|
4250
|
+
style: {
|
|
4251
|
+
display: "flex",
|
|
4252
|
+
backgroundColor: "rgba(255,255,255,0.15)",
|
|
4253
|
+
borderRadius: 20,
|
|
4254
|
+
padding: "6px 16px",
|
|
4255
|
+
marginBottom: 16
|
|
4256
|
+
}
|
|
4257
|
+
},
|
|
4258
|
+
children: [{
|
|
4259
|
+
tag: "span",
|
|
4260
|
+
props: {
|
|
4261
|
+
style: {
|
|
4262
|
+
fontSize: Math.round(h * 0.03),
|
|
4263
|
+
fontFamily: "Inter",
|
|
4264
|
+
fontWeight: 600,
|
|
4265
|
+
color: textColor
|
|
4266
|
+
}
|
|
4267
|
+
},
|
|
4268
|
+
children: [badge]
|
|
4269
|
+
}]
|
|
4270
|
+
});
|
|
4271
|
+
}
|
|
4272
|
+
children.push({
|
|
4273
|
+
tag: "span",
|
|
4274
|
+
props: {
|
|
4275
|
+
style: {
|
|
4276
|
+
fontSize: Math.round(h * 0.1),
|
|
4277
|
+
fontFamily: "Space Grotesk",
|
|
4278
|
+
fontWeight: 700,
|
|
4279
|
+
color: textColor,
|
|
4280
|
+
textShadow: "0 2px 10px rgba(0,0,0,0.3)",
|
|
4281
|
+
textAlign: "center",
|
|
4282
|
+
lineHeight: 1.1
|
|
4283
|
+
}
|
|
4284
|
+
},
|
|
4285
|
+
children: [title]
|
|
4286
|
+
});
|
|
4287
|
+
if (subtitle) {
|
|
4288
|
+
children.push({
|
|
4289
|
+
tag: "span",
|
|
4290
|
+
props: {
|
|
4291
|
+
style: {
|
|
4292
|
+
fontSize: Math.round(h * 0.04),
|
|
4293
|
+
fontFamily: "Inter",
|
|
4294
|
+
fontWeight: 400,
|
|
4295
|
+
color: textColor,
|
|
4296
|
+
opacity: 0.8,
|
|
4297
|
+
textAlign: "center",
|
|
4298
|
+
marginTop: 16
|
|
4299
|
+
}
|
|
4300
|
+
},
|
|
4301
|
+
children: [subtitle]
|
|
4302
|
+
});
|
|
4303
|
+
}
|
|
4304
|
+
overlays.push({
|
|
4305
|
+
type: "satori-text",
|
|
4306
|
+
jsx: {
|
|
4307
|
+
tag: "div",
|
|
4308
|
+
props: {
|
|
4309
|
+
style: {
|
|
4310
|
+
display: "flex",
|
|
4311
|
+
flexDirection: "column",
|
|
4312
|
+
alignItems: "center",
|
|
4313
|
+
justifyContent: "center",
|
|
4314
|
+
width: "100%",
|
|
4315
|
+
height: "100%"
|
|
4316
|
+
}
|
|
4317
|
+
},
|
|
4318
|
+
children
|
|
4319
|
+
},
|
|
4320
|
+
zone: "hero-center",
|
|
4321
|
+
width: Math.round(w * 0.8),
|
|
4322
|
+
height: Math.round(h * 0.7),
|
|
4323
|
+
anchor: "center",
|
|
4324
|
+
depth: "overlay"
|
|
4325
|
+
});
|
|
4326
|
+
return { overlays, background: bg };
|
|
4327
|
+
}
|
|
4328
|
+
function socialCard(data, w, h) {
|
|
4329
|
+
const title = data["title"] || "";
|
|
4330
|
+
const description = data["description"];
|
|
4331
|
+
const logo = data["logo"];
|
|
4332
|
+
const siteName = data["siteName"];
|
|
4333
|
+
const authorName = data["authorName"];
|
|
4334
|
+
const bg = data["background"] || `linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)`;
|
|
4335
|
+
const overlays = [];
|
|
4336
|
+
if (logo) {
|
|
4337
|
+
overlays.push({
|
|
4338
|
+
type: "image",
|
|
4339
|
+
src: logo,
|
|
4340
|
+
zone: "top-left-safe",
|
|
4341
|
+
width: Math.round(h * 0.08),
|
|
4342
|
+
height: Math.round(h * 0.08),
|
|
4343
|
+
anchor: "top-left",
|
|
4344
|
+
depth: "overlay"
|
|
4345
|
+
});
|
|
4346
|
+
}
|
|
4347
|
+
const children = [];
|
|
4348
|
+
children.push({
|
|
4349
|
+
tag: "span",
|
|
4350
|
+
props: {
|
|
4351
|
+
style: {
|
|
4352
|
+
fontSize: Math.round(h * 0.08),
|
|
4353
|
+
fontFamily: "Space Grotesk",
|
|
4354
|
+
fontWeight: 700,
|
|
4355
|
+
color: "white",
|
|
4356
|
+
textShadow: "0 2px 8px rgba(0,0,0,0.4)",
|
|
4357
|
+
lineHeight: 1.2
|
|
4358
|
+
}
|
|
4359
|
+
},
|
|
4360
|
+
children: [title]
|
|
4361
|
+
});
|
|
4362
|
+
if (description) {
|
|
4363
|
+
children.push({
|
|
4364
|
+
tag: "span",
|
|
4365
|
+
props: {
|
|
4366
|
+
style: {
|
|
4367
|
+
fontSize: Math.round(h * 0.035),
|
|
4368
|
+
fontFamily: "Inter",
|
|
4369
|
+
fontWeight: 400,
|
|
4370
|
+
color: "rgba(255,255,255,0.7)",
|
|
4371
|
+
marginTop: 12,
|
|
4372
|
+
lineHeight: 1.4
|
|
4373
|
+
}
|
|
4374
|
+
},
|
|
4375
|
+
children: [description]
|
|
4376
|
+
});
|
|
4377
|
+
}
|
|
4378
|
+
const bottomParts = [];
|
|
4379
|
+
if (siteName)
|
|
4380
|
+
bottomParts.push(siteName);
|
|
4381
|
+
if (authorName)
|
|
4382
|
+
bottomParts.push(authorName);
|
|
4383
|
+
if (bottomParts.length > 0) {
|
|
4384
|
+
children.push({
|
|
4385
|
+
tag: "span",
|
|
4386
|
+
props: {
|
|
4387
|
+
style: {
|
|
4388
|
+
fontSize: Math.round(h * 0.03),
|
|
4389
|
+
fontFamily: "Inter",
|
|
4390
|
+
fontWeight: 600,
|
|
4391
|
+
color: "rgba(255,255,255,0.5)",
|
|
4392
|
+
marginTop: 24
|
|
4393
|
+
}
|
|
4394
|
+
},
|
|
4395
|
+
children: [bottomParts.join(" · ")]
|
|
4396
|
+
});
|
|
4397
|
+
}
|
|
4398
|
+
overlays.push({
|
|
4399
|
+
type: "satori-text",
|
|
4400
|
+
jsx: {
|
|
4401
|
+
tag: "div",
|
|
4402
|
+
props: {
|
|
4403
|
+
style: {
|
|
4404
|
+
display: "flex",
|
|
4405
|
+
flexDirection: "column",
|
|
4406
|
+
justifyContent: "center",
|
|
4407
|
+
padding: Math.round(w * 0.08),
|
|
4408
|
+
width: "100%",
|
|
4409
|
+
height: "100%"
|
|
4410
|
+
}
|
|
4411
|
+
},
|
|
4412
|
+
children
|
|
4413
|
+
},
|
|
4414
|
+
zone: "hero-center",
|
|
4415
|
+
width: Math.round(w * 0.9),
|
|
4416
|
+
height: Math.round(h * 0.8),
|
|
4417
|
+
anchor: "center",
|
|
4418
|
+
depth: "overlay"
|
|
4419
|
+
});
|
|
4420
|
+
return { overlays, background: bg };
|
|
4421
|
+
}
|
|
4422
|
+
function getTemplate(name) {
|
|
4423
|
+
return TEMPLATES[name];
|
|
4424
|
+
}
|
|
4425
|
+
|
|
4426
|
+
// src/config.ts
|
|
4427
|
+
import fs7 from "fs";
|
|
4428
|
+
import path8 from "path";
|
|
4429
|
+
var APP_DIR2 = path8.join(process.env["HOME"] || process.env["USERPROFILE"] || "~", ".picture-it");
|
|
4430
|
+
var CONFIG_PATH2 = path8.join(APP_DIR2, "config.json");
|
|
4431
|
+
function loadConfigFile() {
|
|
4432
|
+
try {
|
|
4433
|
+
const raw = fs7.readFileSync(CONFIG_PATH2, "utf-8");
|
|
4434
|
+
return JSON.parse(raw);
|
|
4435
|
+
} catch {
|
|
4436
|
+
return {};
|
|
4437
|
+
}
|
|
4438
|
+
}
|
|
4439
|
+
function saveConfigFile(config) {
|
|
4440
|
+
fs7.mkdirSync(APP_DIR2, { recursive: true });
|
|
4441
|
+
fs7.writeFileSync(CONFIG_PATH2, JSON.stringify(config, null, 2), {
|
|
4442
|
+
mode: 384
|
|
4443
|
+
});
|
|
4444
|
+
}
|
|
4445
|
+
function setConfigValue(key, value) {
|
|
4446
|
+
const config = loadConfigFile();
|
|
4447
|
+
config[key] = value;
|
|
4448
|
+
saveConfigFile(config);
|
|
4449
|
+
}
|
|
4450
|
+
function getConfigValue(key) {
|
|
4451
|
+
const config = loadConfigFile();
|
|
4452
|
+
return config[key];
|
|
4453
|
+
}
|
|
4454
|
+
function listConfig() {
|
|
4455
|
+
return loadConfigFile();
|
|
4456
|
+
}
|
|
4457
|
+
function clearConfig() {
|
|
4458
|
+
try {
|
|
4459
|
+
fs7.unlinkSync(CONFIG_PATH2);
|
|
4460
|
+
} catch {}
|
|
4461
|
+
}
|
|
4462
|
+
function maskKey(key) {
|
|
4463
|
+
if (key.length <= 8)
|
|
4464
|
+
return "****";
|
|
4465
|
+
return key.slice(0, 4) + "..." + key.slice(-4);
|
|
4466
|
+
}
|
|
4467
|
+
function getKeySource(key) {
|
|
4468
|
+
const envKey = key === "fal_key" ? "FAL_KEY" : "ANTHROPIC_API_KEY";
|
|
4469
|
+
if (process.env[envKey]) {
|
|
4470
|
+
return { value: process.env[envKey], source: "env variable" };
|
|
4471
|
+
}
|
|
4472
|
+
const file = loadConfigFile();
|
|
4473
|
+
const fileVal = file[key];
|
|
4474
|
+
if (fileVal) {
|
|
4475
|
+
return { value: fileVal, source: "config.json" };
|
|
4476
|
+
}
|
|
4477
|
+
return null;
|
|
4478
|
+
}
|
|
4479
|
+
|
|
4480
|
+
// src/fonts.ts
|
|
4481
|
+
import fs8 from "fs";
|
|
4482
|
+
import path9 from "path";
|
|
4483
|
+
var USER_FONT_DIR2 = path9.join(APP_DIR, "fonts");
|
|
4484
|
+
var LOCAL_FONT_DIR2 = path9.join(import.meta.dirname, "..", "fonts");
|
|
4485
|
+
var FONT_FILES2 = [
|
|
4486
|
+
{
|
|
4487
|
+
name: "Inter",
|
|
4488
|
+
file: "Inter-Regular.ttf",
|
|
4489
|
+
weight: 400,
|
|
4490
|
+
style: "normal",
|
|
4491
|
+
url: "https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfMZg.ttf"
|
|
4492
|
+
},
|
|
4493
|
+
{
|
|
4494
|
+
name: "Inter",
|
|
4495
|
+
file: "Inter-SemiBold.ttf",
|
|
4496
|
+
weight: 600,
|
|
4497
|
+
style: "normal",
|
|
4498
|
+
url: "https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuGKYMZg.ttf"
|
|
4499
|
+
},
|
|
4500
|
+
{
|
|
4501
|
+
name: "Inter",
|
|
4502
|
+
file: "Inter-Bold.ttf",
|
|
4503
|
+
weight: 700,
|
|
4504
|
+
style: "normal",
|
|
4505
|
+
url: "https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuFuYMZg.ttf"
|
|
4506
|
+
},
|
|
4507
|
+
{
|
|
4508
|
+
name: "Space Grotesk",
|
|
4509
|
+
file: "SpaceGrotesk-Medium.ttf",
|
|
4510
|
+
weight: 500,
|
|
4511
|
+
style: "normal",
|
|
4512
|
+
url: "https://fonts.gstatic.com/s/spacegrotesk/v22/V8mQoQDjQSkFtoMM3T6r8E7mF71Q-gOoraIAEj7aUUsj.ttf"
|
|
4513
|
+
},
|
|
4514
|
+
{
|
|
4515
|
+
name: "Space Grotesk",
|
|
4516
|
+
file: "SpaceGrotesk-Bold.ttf",
|
|
4517
|
+
weight: 700,
|
|
4518
|
+
style: "normal",
|
|
4519
|
+
url: "https://fonts.gstatic.com/s/spacegrotesk/v22/V8mQoQDjQSkFtoMM3T6r8E7mF71Q-gOoraIAEj4PVksj.ttf"
|
|
4520
|
+
},
|
|
4521
|
+
{
|
|
4522
|
+
name: "DM Serif Display",
|
|
4523
|
+
file: "DMSerifDisplay-Regular.ttf",
|
|
4524
|
+
weight: 400,
|
|
4525
|
+
style: "normal",
|
|
4526
|
+
url: "https://fonts.gstatic.com/s/dmserifdisplay/v17/-nFnOHM81r4j6k0gjAW3mujVU2B2K_c.ttf"
|
|
4527
|
+
}
|
|
4528
|
+
];
|
|
4529
|
+
var cachedFonts2 = null;
|
|
4530
|
+
function getFontDirectory() {
|
|
4531
|
+
return USER_FONT_DIR2;
|
|
4532
|
+
}
|
|
4533
|
+
async function downloadFonts(options = {}) {
|
|
4534
|
+
const { force = false, onProgress } = options;
|
|
4535
|
+
const downloaded = [];
|
|
4536
|
+
const skipped = [];
|
|
4537
|
+
fs8.mkdirSync(USER_FONT_DIR2, { recursive: true });
|
|
4538
|
+
for (const font of FONT_FILES2) {
|
|
4539
|
+
const outPath = path9.join(USER_FONT_DIR2, font.file);
|
|
4540
|
+
if (!force && fs8.existsSync(outPath) && fs8.statSync(outPath).size > 0) {
|
|
4541
|
+
skipped.push(font.file);
|
|
4542
|
+
onProgress?.(`Already installed: ${font.file}`);
|
|
4543
|
+
continue;
|
|
4544
|
+
}
|
|
4545
|
+
onProgress?.(`Downloading: ${font.file}`);
|
|
4546
|
+
const res = await fetch(font.url);
|
|
4547
|
+
if (!res.ok) {
|
|
4548
|
+
throw new Error(`Failed to download ${font.file}: ${res.status} ${res.statusText}`);
|
|
4549
|
+
}
|
|
4550
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
4551
|
+
fs8.writeFileSync(outPath, buf);
|
|
4552
|
+
downloaded.push(font.file);
|
|
4553
|
+
onProgress?.(`Saved: ${font.file} (${(buf.length / 1024).toFixed(0)} KB)`);
|
|
4554
|
+
}
|
|
4555
|
+
cachedFonts2 = null;
|
|
4556
|
+
return {
|
|
4557
|
+
dir: USER_FONT_DIR2,
|
|
4558
|
+
downloaded,
|
|
4559
|
+
skipped
|
|
4560
|
+
};
|
|
4561
|
+
}
|
|
4562
|
+
|
|
4563
|
+
// index.ts
|
|
4564
|
+
var program2 = new Command;
|
|
4565
|
+
program2.name("picture-it").description("Photoshop for AI agents \u2014 composable image operations").version("0.2.0");
|
|
4566
|
+
program2.command("generate").description("Generate an image from a text prompt").requiredOption("--prompt <text>", "Image description").option("--model <name>", "FAL model (flux-schnell, flux-dev, seedream)").option("--size <WxH>", "Output dimensions").option("--platform <name>", "Platform preset for size").option("-o, --output <path>", "Output file", "output.png").option("--verbose", "Detailed output").action(async (opts) => {
|
|
4567
|
+
const falKey = ensureFalKey();
|
|
4568
|
+
configureFal(falKey);
|
|
4569
|
+
const { width, height } = parseSize(opts.size, opts.platform);
|
|
4570
|
+
const buf = await generate({ prompt: opts.prompt, model: opts.model, width, height, verbose: opts.verbose });
|
|
4571
|
+
const cropped = await cropToExact(buf, width, height);
|
|
4572
|
+
const out = await writeOutput(cropped, opts.output);
|
|
4573
|
+
console.log(out);
|
|
4574
|
+
});
|
|
4575
|
+
program2.command("edit").description("Edit images using AI \u2014 the primary command").requiredOption("--prompt <text>", "Edit instructions").requiredOption("-i, --input <paths...>", "Input image(s)").option("--model <name>", "FAL model (seedream, banana2, banana-pro)").option("--size <WxH>", "Output dimensions").option("-o, --output <path>", "Output file", "output.png").option("--verbose", "Detailed output").action(async (opts) => {
|
|
4576
|
+
const falKey = ensureFalKey();
|
|
4577
|
+
configureFal(falKey);
|
|
4578
|
+
const inputs = opts.input;
|
|
4579
|
+
for (const f of inputs) {
|
|
4580
|
+
if (!fs9.existsSync(f)) {
|
|
4581
|
+
log(`Not found: ${f}`);
|
|
4582
|
+
process.exit(1);
|
|
4583
|
+
}
|
|
4584
|
+
}
|
|
4585
|
+
if (opts.verbose)
|
|
4586
|
+
log(`Uploading ${inputs.length} image(s)...`);
|
|
4587
|
+
const urls = await Promise.all(inputs.map((f) => uploadFile(path10.resolve(f))));
|
|
4588
|
+
const meta = await sharp10(inputs[0]).metadata();
|
|
4589
|
+
const { width, height } = opts.size ? parseSize(opts.size) : { width: meta.width || 1200, height: meta.height || 630 };
|
|
4590
|
+
const buf = await edit({ inputUrls: urls, prompt: opts.prompt, model: opts.model, width, height, verbose: opts.verbose });
|
|
4591
|
+
const cropped = await cropToExact(buf, width, height);
|
|
4592
|
+
const out = await writeOutput(cropped, opts.output);
|
|
4593
|
+
console.log(out);
|
|
4594
|
+
});
|
|
4595
|
+
program2.command("remove-bg").description("Remove background from an image").requiredOption("-i, --input <path>", "Input image").option("--model <name>", "Model: bria (default), birefnet, pixelcut, rembg").option("-o, --output <path>", "Output file", "output.png").option("--verbose", "Detailed output").action(async (opts) => {
|
|
4596
|
+
const falKey = ensureFalKey();
|
|
4597
|
+
configureFal(falKey);
|
|
4598
|
+
const url = await uploadFile(path10.resolve(opts.input));
|
|
4599
|
+
const buf = await removeBg({ inputUrl: url, model: opts.model, verbose: opts.verbose });
|
|
4600
|
+
const out = await writeOutput(buf, opts.output);
|
|
4601
|
+
console.log(out);
|
|
4602
|
+
});
|
|
4603
|
+
program2.command("replace-bg").description("Remove background and generate a new one").requiredOption("-i, --input <path>", "Input image").requiredOption("--prompt <text>", "New background description").option("--model <name>", "FAL model for new background").option("-o, --output <path>", "Output file", "output.png").option("--verbose", "Detailed output").action(async (opts) => {
|
|
4604
|
+
const falKey = ensureFalKey();
|
|
4605
|
+
configureFal(falKey);
|
|
4606
|
+
const inputBuf = await readInput(opts.input);
|
|
4607
|
+
const meta = await sharp10(inputBuf).metadata();
|
|
4608
|
+
const w = meta.width || 1200;
|
|
4609
|
+
const h = meta.height || 630;
|
|
4610
|
+
if (opts.verbose)
|
|
4611
|
+
log("Removing background...");
|
|
4612
|
+
const url = await uploadBuffer(inputBuf, "input.png");
|
|
4613
|
+
const cutout = await removeBg({ inputUrl: url, verbose: opts.verbose });
|
|
4614
|
+
if (opts.verbose)
|
|
4615
|
+
log("Generating new background...");
|
|
4616
|
+
const bg = await generate({ prompt: opts.prompt, model: opts.model, width: w, height: h, verbose: opts.verbose });
|
|
4617
|
+
const bgCropped = await cropToExact(bg, w, h);
|
|
4618
|
+
if (opts.verbose)
|
|
4619
|
+
log("Compositing...");
|
|
4620
|
+
const result = await sharp10(bgCropped).composite([{ input: cutout, blend: "over" }]).png().toBuffer();
|
|
4621
|
+
const out = await writeOutput(result, opts.output);
|
|
4622
|
+
console.log(out);
|
|
4623
|
+
});
|
|
4624
|
+
program2.command("upscale").description("AI upscale an image").requiredOption("-i, --input <path>", "Input image").option("--scale <n>", "Scale factor", "2").option("-o, --output <path>", "Output file", "output.png").option("--verbose", "Detailed output").action(async (opts) => {
|
|
4625
|
+
const falKey = ensureFalKey();
|
|
4626
|
+
configureFal(falKey);
|
|
4627
|
+
const url = await uploadFile(path10.resolve(opts.input));
|
|
4628
|
+
const buf = await upscale({ inputUrl: url, scale: parseInt(opts.scale), verbose: opts.verbose });
|
|
4629
|
+
const out = await writeOutput(buf, opts.output);
|
|
4630
|
+
console.log(out);
|
|
4631
|
+
});
|
|
4632
|
+
program2.command("crop").description("Crop/resize an image to exact dimensions").requiredOption("-i, --input <path>", "Input image").requiredOption("--size <WxH>", "Target dimensions").option("--position <pos>", "Crop position (attention, center, entropy, top, bottom)", "attention").option("-o, --output <path>", "Output file", "output.png").action(async (opts) => {
|
|
4633
|
+
const inputBuf = await readInput(opts.input);
|
|
4634
|
+
const { width, height } = parseSize(opts.size);
|
|
4635
|
+
const buf = await cropToExact(inputBuf, width, height, opts.position);
|
|
4636
|
+
const out = await writeOutput(buf, opts.output);
|
|
4637
|
+
console.log(out);
|
|
4638
|
+
});
|
|
4639
|
+
program2.command("grade").description("Apply color grading").requiredOption("-i, --input <path>", "Input image").requiredOption("--name <grade>", "Grade: cinematic, moody, vibrant, clean, warm-editorial, cool-tech").option("-o, --output <path>", "Output file", "output.png").action(async (opts) => {
|
|
4640
|
+
const buf = await applyColorGrade(await readInput(opts.input), opts.name);
|
|
4641
|
+
const out = await writeOutput(buf, opts.output);
|
|
4642
|
+
console.log(out);
|
|
4643
|
+
});
|
|
4644
|
+
program2.command("grain").description("Add film grain").requiredOption("-i, --input <path>", "Input image").option("--intensity <n>", "Grain intensity 0-1", "0.07").option("-o, --output <path>", "Output file", "output.png").action(async (opts) => {
|
|
4645
|
+
const buf = await applyGrain(await readInput(opts.input), parseFloat(opts.intensity));
|
|
4646
|
+
const out = await writeOutput(buf, opts.output);
|
|
4647
|
+
console.log(out);
|
|
4648
|
+
});
|
|
4649
|
+
program2.command("vignette").description("Add edge vignette").requiredOption("-i, --input <path>", "Input image").option("--opacity <n>", "Vignette opacity 0-1", "0.35").option("-o, --output <path>", "Output file", "output.png").action(async (opts) => {
|
|
4650
|
+
const buf = await applyVignette(await readInput(opts.input), parseFloat(opts.opacity));
|
|
4651
|
+
const out = await writeOutput(buf, opts.output);
|
|
4652
|
+
console.log(out);
|
|
4653
|
+
});
|
|
4654
|
+
program2.command("text").description("Render text onto an image using Satori").requiredOption("-i, --input <path>", "Input image").option("--title <text>", "Text to render (simple mode)").option("--font <name>", "Font family", "Space Grotesk").option("--color <color>", "Text color", "white").option("--font-size <n>", "Font size in pixels", "64").option("--zone <name>", "Position zone", "hero-center").option("--jsx <path>", "JSX overlay JSON file (advanced mode)").option("-o, --output <path>", "Output file", "output.png").option("--verbose", "Detailed output").action(async (opts) => {
|
|
4655
|
+
const inputBuf = await readInput(opts.input);
|
|
4656
|
+
const meta = await sharp10(inputBuf).metadata();
|
|
4657
|
+
const w = meta.width || 1200;
|
|
4658
|
+
const h = meta.height || 630;
|
|
4659
|
+
let overlays;
|
|
4660
|
+
if (opts.jsx) {
|
|
4661
|
+
overlays = JSON.parse(fs9.readFileSync(path10.resolve(opts.jsx), "utf-8"));
|
|
4662
|
+
} else if (opts.title) {
|
|
4663
|
+
overlays = [{
|
|
4664
|
+
type: "satori-text",
|
|
4665
|
+
jsx: {
|
|
4666
|
+
tag: "div",
|
|
4667
|
+
props: {
|
|
4668
|
+
style: { display: "flex", alignItems: "center", justifyContent: "center", width: "100%", height: "100%" }
|
|
4669
|
+
},
|
|
4670
|
+
children: [{
|
|
4671
|
+
tag: "span",
|
|
4672
|
+
props: {
|
|
4673
|
+
style: {
|
|
4674
|
+
fontSize: parseInt(opts.fontSize),
|
|
4675
|
+
fontFamily: opts.font,
|
|
4676
|
+
fontWeight: 700,
|
|
4677
|
+
color: opts.color,
|
|
4678
|
+
textShadow: "0 2px 10px rgba(0,0,0,0.5)",
|
|
4679
|
+
textAlign: "center"
|
|
4680
|
+
}
|
|
4681
|
+
},
|
|
4682
|
+
children: [opts.title]
|
|
4683
|
+
}]
|
|
4684
|
+
},
|
|
4685
|
+
zone: opts.zone,
|
|
4686
|
+
width: Math.round(w * 0.9),
|
|
4687
|
+
height: Math.round(h * 0.3),
|
|
4688
|
+
anchor: "center",
|
|
4689
|
+
depth: "overlay"
|
|
4690
|
+
}];
|
|
4691
|
+
} else {
|
|
4692
|
+
log("Provide --title or --jsx");
|
|
4693
|
+
process.exit(1);
|
|
4694
|
+
}
|
|
4695
|
+
const result = await composite(inputBuf, overlays, w, h, process.cwd(), opts.verbose);
|
|
4696
|
+
const out = await writeOutput(result, opts.output);
|
|
4697
|
+
console.log(out);
|
|
4698
|
+
});
|
|
4699
|
+
program2.command("compose").description("Composite overlays onto a background image").requiredOption("-i, --input <path>", "Background image").requiredOption("--overlays <path>", "Overlays JSON file").option("--size <WxH>", "Output dimensions").option("-o, --output <path>", "Output file", "output.png").option("--verbose", "Detailed output").action(async (opts) => {
|
|
4700
|
+
const bgBuf = await readInput(opts.input);
|
|
4701
|
+
const bgMeta = await sharp10(bgBuf).metadata();
|
|
4702
|
+
const { width, height } = opts.size ? parseSize(opts.size) : { width: bgMeta.width || 1200, height: bgMeta.height || 630 };
|
|
4703
|
+
const overlays = JSON.parse(fs9.readFileSync(path10.resolve(opts.overlays), "utf-8"));
|
|
4704
|
+
const result = await composite(bgBuf, overlays, width, height, process.cwd(), opts.verbose);
|
|
4705
|
+
const out = await writeOutput(result, opts.output);
|
|
4706
|
+
console.log(out);
|
|
4707
|
+
});
|
|
4708
|
+
program2.command("template <name>").description("Generate from a built-in template (no AI)").option("--platform <name>", "Platform preset", "blog-featured").option("--size <WxH>", "Output dimensions").option("-o, --output <path>", "Output file", "output.png").option("--verbose", "Detailed output").option("--left-logo <path>", "Left logo").option("--right-logo <path>", "Right logo").option("--logo <path>", "Logo asset").option("--title <text>", "Title text").option("--subtitle <text>", "Subtitle").option("--badge <text>", "Badge text").option("--glow-color <hex>", "Glow color").option("--vs-text <text>", "VS text").option("--left-label <text>", "Left label").option("--right-label <text>", "Right label").option("--text-color <color>", "Text color").option("--background <css>", "CSS gradient").option("--site-name <text>", "Site name").option("--author-name <text>", "Author name").option("--description <text>", "Description").option("--position <pos>", "Position").action(async (name, opts) => {
|
|
4709
|
+
const tmpl = getTemplate(name);
|
|
4710
|
+
if (!tmpl) {
|
|
4711
|
+
log(`Unknown template: ${name}. Available: vs-comparison, feature-hero, text-hero, social-card`);
|
|
4712
|
+
process.exit(1);
|
|
4713
|
+
}
|
|
4714
|
+
const { width, height } = parseSize(opts.size, opts.platform);
|
|
4715
|
+
const data = {};
|
|
4716
|
+
for (const [key, value] of Object.entries(opts)) {
|
|
4717
|
+
if (value !== undefined && typeof value !== "function") {
|
|
4718
|
+
data[key.replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = value;
|
|
4719
|
+
}
|
|
4720
|
+
}
|
|
4721
|
+
const result = tmpl(data, width, height);
|
|
4722
|
+
const bgBuffer = await createGradientBackground(result.background, width, height);
|
|
4723
|
+
const output = await composite(bgBuffer, result.overlays, width, height, process.cwd(), opts.verbose);
|
|
4724
|
+
const out = await writeOutput(output, opts.output);
|
|
4725
|
+
console.log(out);
|
|
4726
|
+
});
|
|
4727
|
+
program2.command("pipeline").description("Execute a multi-step pipeline from JSON").requiredOption("--spec <path>", "Pipeline JSON file").option("-o, --output <path>", "Output file", "output.png").option("--verbose", "Detailed output").action(async (opts) => {
|
|
4728
|
+
const specPath = path10.resolve(opts.spec);
|
|
4729
|
+
if (!fs9.existsSync(specPath)) {
|
|
4730
|
+
log(`Not found: ${specPath}`);
|
|
4731
|
+
process.exit(1);
|
|
4732
|
+
}
|
|
4733
|
+
const steps = JSON.parse(fs9.readFileSync(specPath, "utf-8"));
|
|
4734
|
+
const out = await executePipeline(steps, opts.output, opts.verbose);
|
|
4735
|
+
console.log(out);
|
|
4736
|
+
});
|
|
4737
|
+
program2.command("batch").description("Execute multiple pipelines from JSON").requiredOption("--spec <path>", "Batch spec JSON file").option("--output-dir <dir>", "Output directory", ".").option("--verbose", "Detailed output").action(async (opts) => {
|
|
4738
|
+
const specPath = path10.resolve(opts.spec);
|
|
4739
|
+
if (!fs9.existsSync(specPath)) {
|
|
4740
|
+
log(`Not found: ${specPath}`);
|
|
4741
|
+
process.exit(1);
|
|
4742
|
+
}
|
|
4743
|
+
const entries = JSON.parse(fs9.readFileSync(specPath, "utf-8"));
|
|
4744
|
+
const outputDir = path10.resolve(opts.outputDir);
|
|
4745
|
+
fs9.mkdirSync(outputDir, { recursive: true });
|
|
4746
|
+
const results = [];
|
|
4747
|
+
for (const entry of entries) {
|
|
4748
|
+
const outputPath = path10.resolve(outputDir, entry.output || `${entry.id}.png`);
|
|
4749
|
+
try {
|
|
4750
|
+
await executePipeline(entry.pipeline, outputPath, opts.verbose);
|
|
4751
|
+
results.push(outputPath);
|
|
4752
|
+
if (opts.verbose)
|
|
4753
|
+
log(`Done: ${outputPath}`);
|
|
4754
|
+
} catch (e) {
|
|
4755
|
+
log(`Failed ${entry.id}: ${e.message}`);
|
|
4756
|
+
}
|
|
4757
|
+
}
|
|
4758
|
+
console.log(JSON.stringify(results));
|
|
4759
|
+
});
|
|
4760
|
+
program2.command("info").description("Analyze an image \u2014 dimensions, colors, content type").requiredOption("-i, --input <path>", "Input image").action(async (opts) => {
|
|
4761
|
+
const inputPath = path10.resolve(opts.input);
|
|
4762
|
+
if (!fs9.existsSync(inputPath)) {
|
|
4763
|
+
log(`Not found: ${inputPath}`);
|
|
4764
|
+
process.exit(1);
|
|
4765
|
+
}
|
|
4766
|
+
const img = sharp10(inputPath);
|
|
4767
|
+
const meta = await img.metadata();
|
|
4768
|
+
const stats = await img.stats();
|
|
4769
|
+
const fileStat = fs9.statSync(inputPath);
|
|
4770
|
+
const w = meta.width || 0;
|
|
4771
|
+
const h = meta.height || 0;
|
|
4772
|
+
const ratio = w / (h || 1);
|
|
4773
|
+
const hasAlpha = meta.hasAlpha === true && meta.channels === 4;
|
|
4774
|
+
const { data } = await img.resize(8, 8, { fit: "cover" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
4775
|
+
const colorCounts = new Map;
|
|
4776
|
+
for (let i = 0;i < data.length; i += 4) {
|
|
4777
|
+
if (data[i + 3] < 128)
|
|
4778
|
+
continue;
|
|
4779
|
+
const hex = "#" + [data[i], data[i + 1], data[i + 2]].map((c) => (Math.round(c / 32) * 32).toString(16).padStart(2, "0")).join("");
|
|
4780
|
+
colorCounts.set(hex, (colorCounts.get(hex) || 0) + 1);
|
|
4781
|
+
}
|
|
4782
|
+
const colors = [...colorCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([h2]) => h2);
|
|
4783
|
+
let contentType = "photo";
|
|
4784
|
+
if (Math.abs(ratio - 1) < 0.15 && hasAlpha)
|
|
4785
|
+
contentType = w <= 256 ? "avatar" : "icon";
|
|
4786
|
+
else if (Math.abs(ratio - 1) < 0.15 && !hasAlpha && w <= 256)
|
|
4787
|
+
contentType = "avatar";
|
|
4788
|
+
else if (ratio > 1.3 && !hasAlpha)
|
|
4789
|
+
contentType = "screenshot";
|
|
4790
|
+
else if (hasAlpha)
|
|
4791
|
+
contentType = "cutout";
|
|
4792
|
+
console.log(JSON.stringify({
|
|
4793
|
+
path: inputPath,
|
|
4794
|
+
filename: path10.basename(inputPath),
|
|
4795
|
+
width: w,
|
|
4796
|
+
height: h,
|
|
4797
|
+
aspectRatio: Math.round(ratio * 100) / 100,
|
|
4798
|
+
format: meta.format,
|
|
4799
|
+
hasTransparency: hasAlpha,
|
|
4800
|
+
dominantColors: colors,
|
|
4801
|
+
contentType,
|
|
4802
|
+
sizeBytes: fileStat.size
|
|
4803
|
+
}, null, 2));
|
|
4804
|
+
});
|
|
4805
|
+
program2.command("download-fonts").description("Download fonts required for text and template commands").option("--force", "Redownload existing font files").action(async (opts) => {
|
|
4806
|
+
const result = await downloadFonts({
|
|
4807
|
+
force: opts.force,
|
|
4808
|
+
onProgress: (message) => log(message)
|
|
4809
|
+
});
|
|
4810
|
+
log(`Fonts ready in ${result.dir}`);
|
|
4811
|
+
log("Text, compose, and template commands can use them immediately.");
|
|
4812
|
+
});
|
|
4813
|
+
program2.command("auth").description("Configure API keys").option("--fal <key>", "Set FAL API key").option("--status", "Show key status").option("--clear", "Remove all keys").action(async (opts) => {
|
|
4814
|
+
if (opts.status) {
|
|
4815
|
+
const falInfo = getKeySource("fal_key");
|
|
4816
|
+
log(falInfo ? `FAL_KEY: ${maskKey(falInfo.value)} (${falInfo.source}) \u2713` : "FAL_KEY: not configured");
|
|
4817
|
+
log(`Fonts: ${getFontDirectory()}`);
|
|
4818
|
+
return;
|
|
4819
|
+
}
|
|
4820
|
+
if (opts.clear) {
|
|
4821
|
+
clearConfig();
|
|
4822
|
+
log("Keys cleared.");
|
|
4823
|
+
return;
|
|
4824
|
+
}
|
|
4825
|
+
if (opts.fal) {
|
|
4826
|
+
setConfigValue("fal_key", opts.fal);
|
|
4827
|
+
log(`FAL key saved: ${maskKey(opts.fal)}`);
|
|
4828
|
+
return;
|
|
4829
|
+
}
|
|
4830
|
+
const readline = await import("readline");
|
|
4831
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
4832
|
+
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
4833
|
+
const falKey = await ask("FAL API key: ");
|
|
4834
|
+
if (falKey.trim()) {
|
|
4835
|
+
setConfigValue("fal_key", falKey.trim());
|
|
4836
|
+
log(`FAL key saved: ${maskKey(falKey.trim())}`);
|
|
4837
|
+
}
|
|
4838
|
+
rl.close();
|
|
4839
|
+
});
|
|
4840
|
+
program2.command("config").description("Manage configuration").argument("<action>", "set, get, or list").argument("[key]", "Config key").argument("[value]", "Config value").action((action, key, value) => {
|
|
4841
|
+
switch (action) {
|
|
4842
|
+
case "set":
|
|
4843
|
+
if (!key || !value) {
|
|
4844
|
+
log("Usage: picture-it config set <key> <value>");
|
|
4845
|
+
process.exit(1);
|
|
4846
|
+
}
|
|
4847
|
+
setConfigValue(key, value);
|
|
4848
|
+
log(`Set ${key} = ${value}`);
|
|
4849
|
+
break;
|
|
4850
|
+
case "get":
|
|
4851
|
+
if (!key) {
|
|
4852
|
+
log("Usage: picture-it config get <key>");
|
|
4853
|
+
process.exit(1);
|
|
4854
|
+
}
|
|
4855
|
+
const val = getConfigValue(key);
|
|
4856
|
+
val ? console.log(val) : log(`${key} not set`);
|
|
4857
|
+
break;
|
|
4858
|
+
case "list": {
|
|
4859
|
+
const cfg = listConfig();
|
|
4860
|
+
for (const [k, v] of Object.entries(cfg)) {
|
|
4861
|
+
log(k.includes("key") ? `${k}: ${maskKey(v)}` : `${k}: ${v}`);
|
|
4862
|
+
}
|
|
4863
|
+
break;
|
|
4864
|
+
}
|
|
4865
|
+
default:
|
|
4866
|
+
log(`Unknown: ${action}. Use set, get, or list.`);
|
|
4867
|
+
process.exit(1);
|
|
4868
|
+
}
|
|
4869
|
+
});
|
|
4870
|
+
program2.parse();
|