@ai-agent-tools/picgen 0.1.0-alpha.10 → 0.1.0-alpha.3
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 +4 -35
- package/dist/cli.js +40 -966
- package/docs/release-alpha.md +3 -29
- package/package.json +1 -1
- package/skills/picgen/SKILL.md +5 -146
- package/docs/agent-install.md +0 -155
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
+
import "dotenv/config";
|
|
4
5
|
import { Command } from "commander";
|
|
5
6
|
|
|
6
7
|
// src/commands/create.ts
|
|
@@ -217,12 +218,12 @@ function padMilliseconds(value) {
|
|
|
217
218
|
function slug(value) {
|
|
218
219
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
|
|
219
220
|
}
|
|
220
|
-
function redactProviderImageData(value,
|
|
221
|
+
function redactProviderImageData(value, key) {
|
|
221
222
|
if (Array.isArray(value)) {
|
|
222
223
|
return value.map((item) => redactProviderImageData(item));
|
|
223
224
|
}
|
|
224
225
|
if (!value || typeof value !== "object") {
|
|
225
|
-
return shouldRedactImageDataKey(
|
|
226
|
+
return shouldRedactImageDataKey(key) && typeof value === "string" ? redactedProviderDataPlaceholder(value) : value;
|
|
226
227
|
}
|
|
227
228
|
return Object.fromEntries(
|
|
228
229
|
Object.entries(value).map(([entryKey, entryValue]) => [
|
|
@@ -231,8 +232,8 @@ function redactProviderImageData(value, key2) {
|
|
|
231
232
|
])
|
|
232
233
|
);
|
|
233
234
|
}
|
|
234
|
-
function shouldRedactImageDataKey(
|
|
235
|
-
return
|
|
235
|
+
function shouldRedactImageDataKey(key) {
|
|
236
|
+
return key === "b64_json" || key === "data" || key === "thoughtSignature" || key === "thought_signature";
|
|
236
237
|
}
|
|
237
238
|
function redactedProviderDataPlaceholder(value) {
|
|
238
239
|
return `[redacted provider data: ${value.length} chars]`;
|
|
@@ -274,9 +275,9 @@ function mimeTypeFromPath(path) {
|
|
|
274
275
|
}
|
|
275
276
|
|
|
276
277
|
// src/config/store.ts
|
|
277
|
-
import { mkdir as
|
|
278
|
-
import { dirname
|
|
279
|
-
import { homedir
|
|
278
|
+
import { mkdir as mkdir2, readFile, writeFile as writeFile2 } from "fs/promises";
|
|
279
|
+
import { dirname, join as join2 } from "path";
|
|
280
|
+
import { homedir } from "os";
|
|
280
281
|
import YAML from "yaml";
|
|
281
282
|
|
|
282
283
|
// src/config/defaults.ts
|
|
@@ -362,164 +363,6 @@ var defaultConfig = {
|
|
|
362
363
|
}
|
|
363
364
|
};
|
|
364
365
|
|
|
365
|
-
// src/config/env.ts
|
|
366
|
-
import { chmod, mkdir as mkdir2, readFile, writeFile as writeFile2 } from "fs/promises";
|
|
367
|
-
import { existsSync } from "fs";
|
|
368
|
-
import { createHash } from "crypto";
|
|
369
|
-
import { dirname, join as join2, resolve as resolve2 } from "path";
|
|
370
|
-
import { homedir } from "os";
|
|
371
|
-
import { parse } from "dotenv";
|
|
372
|
-
var loadedEnvSources = /* @__PURE__ */ new Map();
|
|
373
|
-
function getManagedEnvPath() {
|
|
374
|
-
return process.env.PICGEN_ENV_PATH ?? join2(homedir(), ".picgen", ".env");
|
|
375
|
-
}
|
|
376
|
-
async function loadPicgenEnv() {
|
|
377
|
-
const shellEnv = new Set(Object.keys(process.env));
|
|
378
|
-
await loadEnvFile(getManagedEnvPath(), shellEnv, false, "managed");
|
|
379
|
-
await loadEnvFile(resolve2(process.cwd(), ".env"), shellEnv, true, "project");
|
|
380
|
-
}
|
|
381
|
-
async function saveManagedEnvVar(name, value) {
|
|
382
|
-
const path = getManagedEnvPath();
|
|
383
|
-
const current = await readManagedEnvFile(path);
|
|
384
|
-
const next = {
|
|
385
|
-
...current,
|
|
386
|
-
[name]: value
|
|
387
|
-
};
|
|
388
|
-
await mkdir2(dirname(path), { recursive: true });
|
|
389
|
-
await writeFile2(path, stringifyEnv(next), "utf8");
|
|
390
|
-
await chmod(path, 384);
|
|
391
|
-
process.env[name] = value;
|
|
392
|
-
loadedEnvSources.set(name, { source: "managed", path });
|
|
393
|
-
return path;
|
|
394
|
-
}
|
|
395
|
-
async function inspectEnvVar(name) {
|
|
396
|
-
const shellValue = process.env[name];
|
|
397
|
-
if (shellValue !== void 0) {
|
|
398
|
-
const loadedSource = loadedEnvSources.get(name);
|
|
399
|
-
return describeEnvValue(
|
|
400
|
-
name,
|
|
401
|
-
shellValue,
|
|
402
|
-
loadedSource?.source ?? "shell",
|
|
403
|
-
loadedSource?.path
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
const projectPath = resolve2(process.cwd(), ".env");
|
|
407
|
-
const project = await readEnvFile(projectPath);
|
|
408
|
-
if (project[name] !== void 0) {
|
|
409
|
-
return describeEnvValue(name, project[name], "project", projectPath);
|
|
410
|
-
}
|
|
411
|
-
const managedPath = getManagedEnvPath();
|
|
412
|
-
const managed = await readEnvFile(managedPath);
|
|
413
|
-
if (managed[name] !== void 0) {
|
|
414
|
-
return describeEnvValue(name, managed[name], "managed", managedPath);
|
|
415
|
-
}
|
|
416
|
-
return {
|
|
417
|
-
name,
|
|
418
|
-
set: false
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
async function inspectEnvVars(names) {
|
|
422
|
-
const uniqueNames = [...new Set(names)];
|
|
423
|
-
return Promise.all(uniqueNames.map((name) => inspectEnvVar(name)));
|
|
424
|
-
}
|
|
425
|
-
async function readEnvVarValue(name) {
|
|
426
|
-
if (process.env[name] !== void 0) return process.env[name];
|
|
427
|
-
const project = await readEnvFile(resolve2(process.cwd(), ".env"));
|
|
428
|
-
if (project[name] !== void 0) return project[name];
|
|
429
|
-
const managed = await readEnvFile(getManagedEnvPath());
|
|
430
|
-
return managed[name];
|
|
431
|
-
}
|
|
432
|
-
async function loadEnvFile(path, shellEnv, overrideManagedValues, source) {
|
|
433
|
-
if (!existsSync(path)) return;
|
|
434
|
-
const parsed = parse(await readFile(path, "utf8"));
|
|
435
|
-
for (const [name, value] of Object.entries(parsed)) {
|
|
436
|
-
if (shellEnv.has(name)) continue;
|
|
437
|
-
if (!overrideManagedValues && process.env[name] !== void 0) continue;
|
|
438
|
-
process.env[name] = value;
|
|
439
|
-
loadedEnvSources.set(name, { source, path });
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
async function readManagedEnvFile(path) {
|
|
443
|
-
return readEnvFile(path);
|
|
444
|
-
}
|
|
445
|
-
async function readEnvFile(path) {
|
|
446
|
-
try {
|
|
447
|
-
return parse(await readFile(path, "utf8"));
|
|
448
|
-
} catch (error) {
|
|
449
|
-
if (error.code === "ENOENT") return {};
|
|
450
|
-
throw error;
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
function stringifyEnv(values) {
|
|
454
|
-
return `${Object.entries(values).map(([name, value]) => `${name}=${quoteEnvValue(value)}`).join("\n")}
|
|
455
|
-
`;
|
|
456
|
-
}
|
|
457
|
-
function quoteEnvValue(value) {
|
|
458
|
-
if (/^[A-Za-z0-9_./:@+-]+$/.test(value)) return value;
|
|
459
|
-
return JSON.stringify(value);
|
|
460
|
-
}
|
|
461
|
-
function describeEnvValue(name, value, source, path) {
|
|
462
|
-
return {
|
|
463
|
-
name,
|
|
464
|
-
set: true,
|
|
465
|
-
source,
|
|
466
|
-
path,
|
|
467
|
-
length: value.length,
|
|
468
|
-
preview: maskSecret(value),
|
|
469
|
-
fingerprint: createHash("sha256").update(value).digest("hex").slice(0, 12)
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
|
-
function maskSecret(value) {
|
|
473
|
-
if (value.length <= 11) return "*".repeat(value.length);
|
|
474
|
-
return `${value.slice(0, 7)}...${value.slice(-4)}`;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// src/config/providerKeys.ts
|
|
478
|
-
function nextAvailableProviderApiKeyEnv(config, baseEnv, providerName, existingEnv) {
|
|
479
|
-
if (existingEnv) return existingEnv;
|
|
480
|
-
const usedEnvs = new Set(Object.values(config.providers).map((provider2) => provider2.api_key_env));
|
|
481
|
-
if (!usedEnvs.has(baseEnv)) return baseEnv;
|
|
482
|
-
const providerEnv = providerNameToApiKeyEnv(providerName);
|
|
483
|
-
if (!usedEnvs.has(providerEnv)) return providerEnv;
|
|
484
|
-
let index = 2;
|
|
485
|
-
while (usedEnvs.has(`${providerEnv}_${index}`)) index += 1;
|
|
486
|
-
return `${providerEnv}_${index}`;
|
|
487
|
-
}
|
|
488
|
-
function providerNameToApiKeyEnv(providerName) {
|
|
489
|
-
const safeName = providerName.trim().toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
490
|
-
return `PICGEN_${safeName || "PROVIDER"}_KEY`;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// src/config/migrations.ts
|
|
494
|
-
async function migrateConfig(config) {
|
|
495
|
-
const migrated = structuredClone(config);
|
|
496
|
-
let changed = false;
|
|
497
|
-
const usedEnvs = /* @__PURE__ */ new Set();
|
|
498
|
-
for (const [providerName, provider2] of Object.entries(migrated.providers)) {
|
|
499
|
-
const currentEnv = provider2.api_key_env;
|
|
500
|
-
if (!usedEnvs.has(currentEnv)) {
|
|
501
|
-
usedEnvs.add(currentEnv);
|
|
502
|
-
continue;
|
|
503
|
-
}
|
|
504
|
-
const nextEnv = nextUniqueProviderEnv(usedEnvs, providerName);
|
|
505
|
-
const currentValue = await readEnvVarValue(currentEnv);
|
|
506
|
-
provider2.api_key_env = nextEnv;
|
|
507
|
-
usedEnvs.add(nextEnv);
|
|
508
|
-
changed = true;
|
|
509
|
-
if (currentValue && !await readEnvVarValue(nextEnv)) {
|
|
510
|
-
await saveManagedEnvVar(nextEnv, currentValue);
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
return { config: migrated, changed };
|
|
514
|
-
}
|
|
515
|
-
function nextUniqueProviderEnv(usedEnvs, providerName) {
|
|
516
|
-
const baseEnv = providerNameToApiKeyEnv(providerName);
|
|
517
|
-
if (!usedEnvs.has(baseEnv)) return baseEnv;
|
|
518
|
-
let index = 2;
|
|
519
|
-
while (usedEnvs.has(`${baseEnv}_${index}`)) index += 1;
|
|
520
|
-
return `${baseEnv}_${index}`;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
366
|
// src/config/schema.ts
|
|
524
367
|
import { z } from "zod";
|
|
525
368
|
var providerSchema = z.object({
|
|
@@ -586,19 +429,14 @@ function defaultCapabilitiesForProtocol(protocol) {
|
|
|
586
429
|
|
|
587
430
|
// src/config/store.ts
|
|
588
431
|
function getConfigPath() {
|
|
589
|
-
return process.env.PICGEN_CONFIG ??
|
|
432
|
+
return process.env.PICGEN_CONFIG ?? join2(homedir(), ".picgen", "config.yaml");
|
|
590
433
|
}
|
|
591
434
|
async function loadConfig() {
|
|
592
435
|
const path = getConfigPath();
|
|
593
436
|
try {
|
|
594
|
-
const raw = await
|
|
437
|
+
const raw = await readFile(path, "utf8");
|
|
595
438
|
const parsed = YAML.parse(raw);
|
|
596
|
-
|
|
597
|
-
const migrated = await migrateConfig(config);
|
|
598
|
-
if (migrated.changed) {
|
|
599
|
-
await writeConfigFile(path, migrated.config);
|
|
600
|
-
}
|
|
601
|
-
return migrated.config;
|
|
439
|
+
return picgenConfigSchema.parse(parsed);
|
|
602
440
|
} catch (error) {
|
|
603
441
|
if (error.code === "ENOENT") {
|
|
604
442
|
return structuredClone(defaultConfig);
|
|
@@ -609,11 +447,8 @@ async function loadConfig() {
|
|
|
609
447
|
async function saveConfig(config) {
|
|
610
448
|
const parsed = picgenConfigSchema.parse(config);
|
|
611
449
|
const path = getConfigPath();
|
|
612
|
-
await
|
|
613
|
-
|
|
614
|
-
async function writeConfigFile(path, config) {
|
|
615
|
-
await mkdir3(dirname2(path), { recursive: true });
|
|
616
|
-
await writeFile3(path, YAML.stringify(config), "utf8");
|
|
450
|
+
await mkdir2(dirname(path), { recursive: true });
|
|
451
|
+
await writeFile2(path, YAML.stringify(parsed), "utf8");
|
|
617
452
|
}
|
|
618
453
|
async function ensureConfig() {
|
|
619
454
|
const config = await loadConfig();
|
|
@@ -622,7 +457,7 @@ async function ensureConfig() {
|
|
|
622
457
|
}
|
|
623
458
|
|
|
624
459
|
// src/providers/gemini.ts
|
|
625
|
-
import { readFile as
|
|
460
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
626
461
|
|
|
627
462
|
// src/providers/timeout.ts
|
|
628
463
|
var FAST_PROVIDER_TIMEOUT_MS = 12e4;
|
|
@@ -762,7 +597,7 @@ async function readReferenceImageParts(plan) {
|
|
|
762
597
|
plan.referenceImages.map(async (image) => ({
|
|
763
598
|
inlineData: {
|
|
764
599
|
mimeType: image.mime_type,
|
|
765
|
-
data: (await
|
|
600
|
+
data: (await readFile2(image.path)).toString("base64")
|
|
766
601
|
}
|
|
767
602
|
}))
|
|
768
603
|
);
|
|
@@ -979,7 +814,7 @@ function getAdapter(protocol) {
|
|
|
979
814
|
}
|
|
980
815
|
|
|
981
816
|
// src/routing/resolve.ts
|
|
982
|
-
import { join as
|
|
817
|
+
import { join as join3 } from "path";
|
|
983
818
|
import { cwd } from "process";
|
|
984
819
|
function resolveGenerationPlan(config, options) {
|
|
985
820
|
const presetName = options.presetName ?? config.default_preset;
|
|
@@ -1013,7 +848,7 @@ function resolveGenerationPlan(config, options) {
|
|
|
1013
848
|
presetName,
|
|
1014
849
|
preset,
|
|
1015
850
|
modeName,
|
|
1016
|
-
outputDirectory: options.outputDirectory ??
|
|
851
|
+
outputDirectory: options.outputDirectory ?? join3(cwd(), "outputs", "picgen"),
|
|
1017
852
|
referenceImages: options.referenceImages ?? []
|
|
1018
853
|
};
|
|
1019
854
|
}
|
|
@@ -1214,13 +1049,13 @@ function inspectProviders(config) {
|
|
|
1214
1049
|
}
|
|
1215
1050
|
|
|
1216
1051
|
// src/commands/update.ts
|
|
1217
|
-
import { mkdir as
|
|
1218
|
-
import { dirname as
|
|
1219
|
-
import { homedir as
|
|
1052
|
+
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
1053
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
1054
|
+
import { homedir as homedir2 } from "os";
|
|
1220
1055
|
|
|
1221
1056
|
// src/version.ts
|
|
1222
1057
|
var PACKAGE_NAME = "@ai-agent-tools/picgen";
|
|
1223
|
-
var VERSION = "0.1.0-alpha.
|
|
1058
|
+
var VERSION = "0.1.0-alpha.3";
|
|
1224
1059
|
|
|
1225
1060
|
// src/commands/update.ts
|
|
1226
1061
|
var UPDATE_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -1315,7 +1150,7 @@ async function fetchLatestVersion() {
|
|
|
1315
1150
|
}
|
|
1316
1151
|
async function readFreshCache() {
|
|
1317
1152
|
try {
|
|
1318
|
-
const cache = JSON.parse(await
|
|
1153
|
+
const cache = JSON.parse(await readFile3(getUpdateCachePath(), "utf8"));
|
|
1319
1154
|
const checkedAt = Date.parse(cache.checked_at);
|
|
1320
1155
|
if (!Number.isFinite(checkedAt)) return void 0;
|
|
1321
1156
|
if (Date.now() - checkedAt > UPDATE_CACHE_TTL_MS) return void 0;
|
|
@@ -1327,12 +1162,12 @@ async function readFreshCache() {
|
|
|
1327
1162
|
}
|
|
1328
1163
|
async function writeCache(cache) {
|
|
1329
1164
|
const path = getUpdateCachePath();
|
|
1330
|
-
await
|
|
1331
|
-
await
|
|
1165
|
+
await mkdir3(dirname2(path), { recursive: true });
|
|
1166
|
+
await writeFile3(path, JSON.stringify(cache, null, 2), "utf8");
|
|
1332
1167
|
}
|
|
1333
1168
|
function getUpdateCachePath() {
|
|
1334
1169
|
if (process.env.PICGEN_UPDATE_CACHE_PATH) return process.env.PICGEN_UPDATE_CACHE_PATH;
|
|
1335
|
-
return
|
|
1170
|
+
return join4(homedir2(), ".picgen", "update-check.json");
|
|
1336
1171
|
}
|
|
1337
1172
|
function parseVersion(version) {
|
|
1338
1173
|
const [core, prerelease] = version.split("-", 2);
|
|
@@ -1391,84 +1226,6 @@ async function runDoctor(options) {
|
|
|
1391
1226
|
await maybePrintUpdateHint();
|
|
1392
1227
|
}
|
|
1393
1228
|
|
|
1394
|
-
// src/commands/key.ts
|
|
1395
|
-
import { execFile } from "child_process";
|
|
1396
|
-
import { promisify } from "util";
|
|
1397
|
-
import { password } from "@inquirer/prompts";
|
|
1398
|
-
var execFileAsync = promisify(execFile);
|
|
1399
|
-
async function setApiKey(name, options) {
|
|
1400
|
-
validateEnvName(name);
|
|
1401
|
-
const value = options.clipboard ? await readClipboard() : options.stdin ? await readStdin() : options.value ? options.value : await password({
|
|
1402
|
-
message: `Paste API key for ${name}`,
|
|
1403
|
-
mask: "*"
|
|
1404
|
-
});
|
|
1405
|
-
if (!value.trim()) {
|
|
1406
|
-
throw new Error("API key is empty.");
|
|
1407
|
-
}
|
|
1408
|
-
const path = await saveManagedEnvVar(name, value.trim());
|
|
1409
|
-
console.log(`Saved ${name} to ${path}`);
|
|
1410
|
-
}
|
|
1411
|
-
async function listApiKeys(options) {
|
|
1412
|
-
const config = await loadConfig();
|
|
1413
|
-
const names = Object.values(config.providers).map((provider2) => provider2.api_key_env);
|
|
1414
|
-
const inspections = await inspectEnvVars(names);
|
|
1415
|
-
if (options.json) {
|
|
1416
|
-
console.log(JSON.stringify(inspections, null, 2));
|
|
1417
|
-
return;
|
|
1418
|
-
}
|
|
1419
|
-
for (const inspection of inspections) {
|
|
1420
|
-
printInspection(inspection);
|
|
1421
|
-
}
|
|
1422
|
-
}
|
|
1423
|
-
async function showApiKey(name, options) {
|
|
1424
|
-
validateEnvName(name);
|
|
1425
|
-
const inspection = await inspectEnvVar(name);
|
|
1426
|
-
if (options.json) {
|
|
1427
|
-
console.log(JSON.stringify(inspection, null, 2));
|
|
1428
|
-
return;
|
|
1429
|
-
}
|
|
1430
|
-
printInspection(inspection);
|
|
1431
|
-
}
|
|
1432
|
-
function validateEnvName(name) {
|
|
1433
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
1434
|
-
throw new Error(`Invalid environment variable name: ${name}`);
|
|
1435
|
-
}
|
|
1436
|
-
}
|
|
1437
|
-
async function readStdin() {
|
|
1438
|
-
const chunks = [];
|
|
1439
|
-
for await (const chunk of process.stdin) {
|
|
1440
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1441
|
-
}
|
|
1442
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
1443
|
-
}
|
|
1444
|
-
async function readClipboard() {
|
|
1445
|
-
try {
|
|
1446
|
-
const { stdout } = await execFileAsync("pbpaste");
|
|
1447
|
-
return stdout;
|
|
1448
|
-
} catch {
|
|
1449
|
-
throw new Error("Could not read clipboard. Use --stdin or run picgen key set without flags.");
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
function printInspection(inspection) {
|
|
1453
|
-
if (!inspection.set) {
|
|
1454
|
-
console.log(`${inspection.name}: missing`);
|
|
1455
|
-
return;
|
|
1456
|
-
}
|
|
1457
|
-
const location = inspection.path ? ` ${inspection.path}` : "";
|
|
1458
|
-
console.log(
|
|
1459
|
-
`${inspection.name}: set source=${inspection.source}${location} length=${inspection.length} preview=${inspection.preview} fingerprint=${inspection.fingerprint}`
|
|
1460
|
-
);
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
// src/commands/open.ts
|
|
1464
|
-
import { createServer } from "http";
|
|
1465
|
-
import { randomBytes } from "crypto";
|
|
1466
|
-
import { execFile as execFile2 } from "child_process";
|
|
1467
|
-
import { readdir, readFile as readFile5, stat as stat2 } from "fs/promises";
|
|
1468
|
-
import { createReadStream } from "fs";
|
|
1469
|
-
import { extname as extname3, join as join6, resolve as resolve3 } from "path";
|
|
1470
|
-
import { cwd as cwd2 } from "process";
|
|
1471
|
-
|
|
1472
1229
|
// src/commands/provider.ts
|
|
1473
1230
|
import { input, select } from "@inquirer/prompts";
|
|
1474
1231
|
|
|
@@ -1614,32 +1371,6 @@ async function addProvider() {
|
|
|
1614
1371
|
await saveConfig(config);
|
|
1615
1372
|
console.log(`Added provider: ${provider2.name}`);
|
|
1616
1373
|
}
|
|
1617
|
-
async function quickAddProvider(templateName, options) {
|
|
1618
|
-
const config = await loadConfig();
|
|
1619
|
-
const template = quickProviderTemplate(templateName);
|
|
1620
|
-
const name = options.name ?? nextAvailableProviderName(config, template.name);
|
|
1621
|
-
const apiKeyEnv = options.keyEnv ?? nextAvailableProviderApiKeyEnv(config, template.api_key_env, name);
|
|
1622
|
-
const provider2 = {
|
|
1623
|
-
enabled: true,
|
|
1624
|
-
protocol: template.protocol,
|
|
1625
|
-
channel: template.channel,
|
|
1626
|
-
base_url: normalizeProviderBaseUrl(options.host ?? template.base_url),
|
|
1627
|
-
api_key_env: apiKeyEnv,
|
|
1628
|
-
models: parseModels(options.models ?? template.models.join(",")),
|
|
1629
|
-
capabilities: defaultCapabilitiesForProtocol2(template.protocol)
|
|
1630
|
-
};
|
|
1631
|
-
addProviderToConfig(config, name, provider2);
|
|
1632
|
-
if (options.prefer) {
|
|
1633
|
-
setPreferredProvider(config, name);
|
|
1634
|
-
}
|
|
1635
|
-
await saveConfig(config);
|
|
1636
|
-
console.log(`Added provider: ${name}`);
|
|
1637
|
-
console.log(`Protocol: ${provider2.protocol}`);
|
|
1638
|
-
console.log(`Host: ${provider2.base_url}`);
|
|
1639
|
-
console.log(`API key env: ${provider2.api_key_env}`);
|
|
1640
|
-
console.log(`Models: ${provider2.models.join(",")}`);
|
|
1641
|
-
if (options.prefer) console.log(`Preferred provider: ${name}`);
|
|
1642
|
-
}
|
|
1643
1374
|
function addProviderToConfig(config, name, provider2) {
|
|
1644
1375
|
config.providers[name] = provider2;
|
|
1645
1376
|
const knownProviders = [config.routing.default_provider, ...config.routing.fallback_providers];
|
|
@@ -1736,11 +1467,7 @@ async function promptProvider(config, existingName, existing) {
|
|
|
1736
1467
|
});
|
|
1737
1468
|
const apiKeyEnv = await input({
|
|
1738
1469
|
message: "API key environment variable",
|
|
1739
|
-
default: existing?.api_key_env ??
|
|
1740
|
-
config,
|
|
1741
|
-
protocol === "openai-images" ? channel === "official" ? "OPENAI_API_KEY" : "PICGEN_OPENAI_PROXY_KEY" : channel === "official" ? "GEMINI_API_KEY" : "PICGEN_GEMINI_PROXY_KEY",
|
|
1742
|
-
name
|
|
1743
|
-
)
|
|
1470
|
+
default: existing?.api_key_env ?? (protocol === "openai-images" ? channel === "official" ? "OPENAI_API_KEY" : "PICGEN_OPENAI_PROXY_KEY" : channel === "official" ? "GEMINI_API_KEY" : "PICGEN_GEMINI_PROXY_KEY")
|
|
1744
1471
|
});
|
|
1745
1472
|
const defaultModels = protocol === "openai-images" ? "gpt-image-2" : "gemini-3.1-flash-image-preview,gemini-3-pro-image-preview";
|
|
1746
1473
|
const modelsRaw = await input({
|
|
@@ -1770,562 +1497,6 @@ function nextAvailableProviderName(config, baseName, existingName) {
|
|
|
1770
1497
|
function defaultCapabilitiesForProtocol2(protocol) {
|
|
1771
1498
|
return protocol === "gemini" ? ["text-to-image", "reference-image"] : ["text-to-image"];
|
|
1772
1499
|
}
|
|
1773
|
-
function quickProviderTemplate(templateName) {
|
|
1774
|
-
switch (templateName.replaceAll("_", "-")) {
|
|
1775
|
-
case "openai-proxy":
|
|
1776
|
-
return {
|
|
1777
|
-
name: "openai_proxy",
|
|
1778
|
-
protocol: "openai-images",
|
|
1779
|
-
channel: "third_party",
|
|
1780
|
-
base_url: "https://www.pandai.vip",
|
|
1781
|
-
api_key_env: "PICGEN_OPENAI_PROXY_KEY",
|
|
1782
|
-
models: ["gpt-image-2"]
|
|
1783
|
-
};
|
|
1784
|
-
case "gemini-proxy":
|
|
1785
|
-
return {
|
|
1786
|
-
name: "gemini_proxy",
|
|
1787
|
-
protocol: "gemini",
|
|
1788
|
-
channel: "third_party",
|
|
1789
|
-
base_url: "https://www.pandai.vip",
|
|
1790
|
-
api_key_env: "PICGEN_GEMINI_PROXY_KEY",
|
|
1791
|
-
models: ["gemini-3.1-flash-image-preview", "gemini-3-pro-image-preview"]
|
|
1792
|
-
};
|
|
1793
|
-
case "openai-official":
|
|
1794
|
-
return {
|
|
1795
|
-
name: "openai_official",
|
|
1796
|
-
protocol: "openai-images",
|
|
1797
|
-
channel: "official",
|
|
1798
|
-
base_url: defaultProviderBaseUrl("openai-images"),
|
|
1799
|
-
api_key_env: "OPENAI_API_KEY",
|
|
1800
|
-
models: ["gpt-image-2"]
|
|
1801
|
-
};
|
|
1802
|
-
case "gemini-official":
|
|
1803
|
-
return {
|
|
1804
|
-
name: "gemini_official",
|
|
1805
|
-
protocol: "gemini",
|
|
1806
|
-
channel: "official",
|
|
1807
|
-
base_url: defaultProviderBaseUrl("gemini"),
|
|
1808
|
-
api_key_env: "GEMINI_API_KEY",
|
|
1809
|
-
models: ["gemini-3.1-flash-image-preview", "gemini-3-pro-image-preview"]
|
|
1810
|
-
};
|
|
1811
|
-
default:
|
|
1812
|
-
throw new Error(
|
|
1813
|
-
`Unknown provider template: ${templateName}. Use openai-proxy, gemini-proxy, openai-official, or gemini-official.`
|
|
1814
|
-
);
|
|
1815
|
-
}
|
|
1816
|
-
}
|
|
1817
|
-
function parseModels(raw) {
|
|
1818
|
-
return raw.split(",").map((model) => model.trim()).filter(Boolean);
|
|
1819
|
-
}
|
|
1820
|
-
|
|
1821
|
-
// src/commands/open.ts
|
|
1822
|
-
var DEFAULT_PORT = 8188;
|
|
1823
|
-
var MAX_PORT_ATTEMPTS = 30;
|
|
1824
|
-
async function runOpen(options) {
|
|
1825
|
-
const token = randomBytes(24).toString("hex");
|
|
1826
|
-
const port = await listenOnAvailablePort(Number(options.port ?? DEFAULT_PORT), token);
|
|
1827
|
-
const url = `http://127.0.0.1:${port}/?token=${token}`;
|
|
1828
|
-
console.log(`PicGen is open: ${url}`);
|
|
1829
|
-
console.log("Keep this terminal running while using PicGen. Press Ctrl+C to close.");
|
|
1830
|
-
if (options.open !== false) {
|
|
1831
|
-
await openBrowser(url);
|
|
1832
|
-
}
|
|
1833
|
-
}
|
|
1834
|
-
async function listenOnAvailablePort(startPort, token) {
|
|
1835
|
-
for (let offset = 0; offset < MAX_PORT_ATTEMPTS; offset += 1) {
|
|
1836
|
-
const port = startPort + offset;
|
|
1837
|
-
const server = createServer((request, response) => {
|
|
1838
|
-
handleRequest(request, response, token).catch((error) => {
|
|
1839
|
-
sendJson(response, 500, {
|
|
1840
|
-
ok: false,
|
|
1841
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1842
|
-
});
|
|
1843
|
-
});
|
|
1844
|
-
});
|
|
1845
|
-
const result = await tryListen(server, port);
|
|
1846
|
-
if (result) return port;
|
|
1847
|
-
}
|
|
1848
|
-
throw new Error(`Could not find an available port starting at ${startPort}.`);
|
|
1849
|
-
}
|
|
1850
|
-
function tryListen(server, port) {
|
|
1851
|
-
return new Promise((resolveListen, reject) => {
|
|
1852
|
-
server.once("error", (error) => {
|
|
1853
|
-
if (error.code === "EADDRINUSE") {
|
|
1854
|
-
resolveListen(false);
|
|
1855
|
-
return;
|
|
1856
|
-
}
|
|
1857
|
-
reject(error);
|
|
1858
|
-
});
|
|
1859
|
-
server.listen(port, "127.0.0.1", () => resolveListen(true));
|
|
1860
|
-
});
|
|
1861
|
-
}
|
|
1862
|
-
async function handleRequest(request, response, token) {
|
|
1863
|
-
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
1864
|
-
if (url.pathname === "/" || url.pathname === "/index.html") {
|
|
1865
|
-
if (url.searchParams.get("token") !== token) {
|
|
1866
|
-
sendText(response, 403, "Invalid PicGen session token.");
|
|
1867
|
-
return;
|
|
1868
|
-
}
|
|
1869
|
-
sendHtml(response, appHtml);
|
|
1870
|
-
return;
|
|
1871
|
-
}
|
|
1872
|
-
if (!isAuthorized(request, url, token)) {
|
|
1873
|
-
sendJson(response, 403, { ok: false, error: "Invalid PicGen session token." });
|
|
1874
|
-
return;
|
|
1875
|
-
}
|
|
1876
|
-
if (request.method === "GET" && url.pathname === "/api/state") {
|
|
1877
|
-
sendJson(response, 200, await buildState());
|
|
1878
|
-
return;
|
|
1879
|
-
}
|
|
1880
|
-
if (request.method === "POST" && url.pathname === "/api/providers") {
|
|
1881
|
-
sendJson(response, 200, await addProviderFromBody(await readJson(request)));
|
|
1882
|
-
return;
|
|
1883
|
-
}
|
|
1884
|
-
const providerTestMatch = /^\/api\/providers\/([^/]+)\/test$/.exec(url.pathname);
|
|
1885
|
-
if (request.method === "POST" && providerTestMatch) {
|
|
1886
|
-
const config = await loadConfig();
|
|
1887
|
-
const name = decodeURIComponent(providerTestMatch[1]);
|
|
1888
|
-
const provider2 = config.providers[name];
|
|
1889
|
-
if (!provider2) throw new Error(`Unknown provider: ${name}`);
|
|
1890
|
-
sendJson(response, 200, await testProvider(name, provider2));
|
|
1891
|
-
return;
|
|
1892
|
-
}
|
|
1893
|
-
const providerDefaultMatch = /^\/api\/providers\/([^/]+)\/default$/.exec(url.pathname);
|
|
1894
|
-
if (request.method === "POST" && providerDefaultMatch) {
|
|
1895
|
-
const config = await loadConfig();
|
|
1896
|
-
const name = decodeURIComponent(providerDefaultMatch[1]);
|
|
1897
|
-
setPreferredProvider(config, name);
|
|
1898
|
-
await saveConfig(config);
|
|
1899
|
-
sendJson(response, 200, await buildState());
|
|
1900
|
-
return;
|
|
1901
|
-
}
|
|
1902
|
-
const providerMatch = /^\/api\/providers\/([^/]+)$/.exec(url.pathname);
|
|
1903
|
-
if (providerMatch) {
|
|
1904
|
-
const name = decodeURIComponent(providerMatch[1]);
|
|
1905
|
-
if (request.method === "PATCH") {
|
|
1906
|
-
sendJson(response, 200, await patchProvider(name, await readJson(request)));
|
|
1907
|
-
return;
|
|
1908
|
-
}
|
|
1909
|
-
if (request.method === "DELETE") {
|
|
1910
|
-
sendJson(response, 200, await deleteProvider(name));
|
|
1911
|
-
return;
|
|
1912
|
-
}
|
|
1913
|
-
}
|
|
1914
|
-
if (request.method === "POST" && url.pathname === "/api/key") {
|
|
1915
|
-
const body = await readJson(request);
|
|
1916
|
-
if (typeof body.name !== "string" || typeof body.value !== "string") {
|
|
1917
|
-
throw new Error("Key name and value are required.");
|
|
1918
|
-
}
|
|
1919
|
-
await saveManagedEnvVar(body.name, body.value);
|
|
1920
|
-
sendJson(response, 200, { ok: true, state: await buildState() });
|
|
1921
|
-
return;
|
|
1922
|
-
}
|
|
1923
|
-
const keyMatch = /^\/api\/key\/([^/]+)$/.exec(url.pathname);
|
|
1924
|
-
if (request.method === "GET" && keyMatch) {
|
|
1925
|
-
const name = decodeURIComponent(keyMatch[1]);
|
|
1926
|
-
sendJson(response, 200, { ok: true, value: await readEnvVarValue(name) });
|
|
1927
|
-
return;
|
|
1928
|
-
}
|
|
1929
|
-
if (request.method === "POST" && url.pathname === "/api/plan") {
|
|
1930
|
-
sendJson(response, 200, await planGeneration(await readJson(request)));
|
|
1931
|
-
return;
|
|
1932
|
-
}
|
|
1933
|
-
if (request.method === "POST" && url.pathname === "/api/generate") {
|
|
1934
|
-
sendJson(response, 200, await generate(await readJson(request)));
|
|
1935
|
-
return;
|
|
1936
|
-
}
|
|
1937
|
-
if (request.method === "GET" && url.pathname === "/api/history") {
|
|
1938
|
-
sendJson(response, 200, { ok: true, runs: await listHistory() });
|
|
1939
|
-
return;
|
|
1940
|
-
}
|
|
1941
|
-
if (request.method === "GET" && url.pathname === "/api/file") {
|
|
1942
|
-
await sendOutputFile(response, url.searchParams.get("path"));
|
|
1943
|
-
return;
|
|
1944
|
-
}
|
|
1945
|
-
sendJson(response, 404, { ok: false, error: "Not found." });
|
|
1946
|
-
}
|
|
1947
|
-
function isAuthorized(request, url, token) {
|
|
1948
|
-
return request.headers["x-picgen-token"] === token || url.searchParams.get("token") === token;
|
|
1949
|
-
}
|
|
1950
|
-
async function buildState() {
|
|
1951
|
-
const config = await loadConfig();
|
|
1952
|
-
const keyInspections = await inspectEnvVars(
|
|
1953
|
-
Object.values(config.providers).map((provider2) => provider2.api_key_env)
|
|
1954
|
-
);
|
|
1955
|
-
const keys = Object.fromEntries(keyInspections.map((key2) => [key2.name, key2]));
|
|
1956
|
-
return {
|
|
1957
|
-
ok: true,
|
|
1958
|
-
config_path: getConfigPath(),
|
|
1959
|
-
key_file_path: getManagedEnvPath(),
|
|
1960
|
-
default_provider: config.routing.default_provider,
|
|
1961
|
-
fallback_providers: config.routing.fallback_providers,
|
|
1962
|
-
default_preset: config.default_preset,
|
|
1963
|
-
default_mode: config.routing.default_mode,
|
|
1964
|
-
providers: Object.entries(config.providers).map(([name, provider2]) => ({
|
|
1965
|
-
name,
|
|
1966
|
-
...provider2,
|
|
1967
|
-
key: keys[provider2.api_key_env],
|
|
1968
|
-
preference: name === config.routing.default_provider ? "default" : config.routing.fallback_providers.includes(name) ? "fallback" : "manual"
|
|
1969
|
-
})),
|
|
1970
|
-
presets: config.presets,
|
|
1971
|
-
modes: config.modes
|
|
1972
|
-
};
|
|
1973
|
-
}
|
|
1974
|
-
async function addProviderFromBody(body) {
|
|
1975
|
-
const config = await loadConfig();
|
|
1976
|
-
const protocol = body.protocol === "gemini" ? "gemini" : "openai-images";
|
|
1977
|
-
const channel = body.channel === "official" ? "official" : "third_party";
|
|
1978
|
-
const template = defaultProviderTemplate(protocol, channel);
|
|
1979
|
-
const name = typeof body.name === "string" && body.name.trim() ? body.name.trim() : nextAvailableProviderName(config, template.name);
|
|
1980
|
-
const apiKeyEnv = typeof body.api_key_env === "string" && body.api_key_env.trim() ? body.api_key_env.trim() : nextAvailableProviderApiKeyEnv(config, template.api_key_env, name);
|
|
1981
|
-
const models = typeof body.models === "string" && body.models.trim() ? parseModels2(body.models) : template.models;
|
|
1982
|
-
const provider2 = {
|
|
1983
|
-
enabled: true,
|
|
1984
|
-
protocol,
|
|
1985
|
-
channel,
|
|
1986
|
-
base_url: normalizeProviderBaseUrl(String(body.base_url ?? template.base_url)),
|
|
1987
|
-
api_key_env: apiKeyEnv,
|
|
1988
|
-
models,
|
|
1989
|
-
capabilities: defaultCapabilitiesForProtocol2(protocol)
|
|
1990
|
-
};
|
|
1991
|
-
addProviderToConfig(config, name, provider2);
|
|
1992
|
-
if (body.prefer === true || !config.providers[config.routing.default_provider]) {
|
|
1993
|
-
setPreferredProvider(config, name);
|
|
1994
|
-
}
|
|
1995
|
-
await saveConfig(config);
|
|
1996
|
-
if (typeof body.api_key === "string" && body.api_key.trim()) {
|
|
1997
|
-
await saveManagedEnvVar(apiKeyEnv, body.api_key.trim());
|
|
1998
|
-
}
|
|
1999
|
-
return { ok: true, state: await buildState() };
|
|
2000
|
-
}
|
|
2001
|
-
async function patchProvider(name, body) {
|
|
2002
|
-
const config = await loadConfig();
|
|
2003
|
-
const provider2 = config.providers[name];
|
|
2004
|
-
if (!provider2) throw new Error(`Unknown provider: ${name}`);
|
|
2005
|
-
if (typeof body.enabled === "boolean") provider2.enabled = body.enabled;
|
|
2006
|
-
if (typeof body.base_url === "string") provider2.base_url = normalizeProviderBaseUrl(body.base_url);
|
|
2007
|
-
if (typeof body.models === "string") provider2.models = parseModels2(body.models);
|
|
2008
|
-
if (typeof body.api_key === "string" && body.api_key.trim()) {
|
|
2009
|
-
await saveManagedEnvVar(provider2.api_key_env, body.api_key.trim());
|
|
2010
|
-
}
|
|
2011
|
-
await saveConfig(config);
|
|
2012
|
-
return { ok: true, state: await buildState() };
|
|
2013
|
-
}
|
|
2014
|
-
async function deleteProvider(name) {
|
|
2015
|
-
const config = await loadConfig();
|
|
2016
|
-
if (!config.providers[name]) throw new Error(`Unknown provider: ${name}`);
|
|
2017
|
-
delete config.providers[name];
|
|
2018
|
-
config.routing.fallback_providers = config.routing.fallback_providers.filter((item) => item !== name);
|
|
2019
|
-
if (config.routing.default_provider === name) {
|
|
2020
|
-
const nextDefault = config.routing.fallback_providers[0] ?? Object.keys(config.providers)[0];
|
|
2021
|
-
if (nextDefault) {
|
|
2022
|
-
config.routing.default_provider = nextDefault;
|
|
2023
|
-
config.routing.fallback_providers = config.routing.fallback_providers.filter(
|
|
2024
|
-
(item) => item !== nextDefault
|
|
2025
|
-
);
|
|
2026
|
-
}
|
|
2027
|
-
}
|
|
2028
|
-
await saveConfig(config);
|
|
2029
|
-
return { ok: true, state: await buildState() };
|
|
2030
|
-
}
|
|
2031
|
-
async function planGeneration(body) {
|
|
2032
|
-
const config = await loadConfig();
|
|
2033
|
-
const plan = resolveGenerationPlan(config, {
|
|
2034
|
-
prompt: String(body.prompt ?? "").trim(),
|
|
2035
|
-
presetName: asOptionalString(body.preset),
|
|
2036
|
-
providerName: asOptionalString(body.provider),
|
|
2037
|
-
modeName: asOptionalString(body.mode),
|
|
2038
|
-
model: asOptionalString(body.model),
|
|
2039
|
-
outputDirectory: asOptionalString(body.output_directory)
|
|
2040
|
-
});
|
|
2041
|
-
return {
|
|
2042
|
-
ok: true,
|
|
2043
|
-
dry_run: true,
|
|
2044
|
-
provider_called: false,
|
|
2045
|
-
plan: toPlanOutput(plan)
|
|
2046
|
-
};
|
|
2047
|
-
}
|
|
2048
|
-
async function generate(body) {
|
|
2049
|
-
const config = await loadConfig();
|
|
2050
|
-
const plan = resolveGenerationPlan(config, {
|
|
2051
|
-
prompt: String(body.prompt ?? "").trim(),
|
|
2052
|
-
presetName: asOptionalString(body.preset),
|
|
2053
|
-
providerName: asOptionalString(body.provider),
|
|
2054
|
-
modeName: asOptionalString(body.mode),
|
|
2055
|
-
model: asOptionalString(body.model),
|
|
2056
|
-
outputDirectory: asOptionalString(body.output_directory)
|
|
2057
|
-
});
|
|
2058
|
-
const run = await createGenerationRun(plan);
|
|
2059
|
-
const runtimePlan = { ...plan, outputDirectory: run.outputDirectory };
|
|
2060
|
-
const runtimePlanOutput = toPlanOutput(runtimePlan);
|
|
2061
|
-
await writeGenerationMetadata(run, {
|
|
2062
|
-
plan: runtimePlanOutput,
|
|
2063
|
-
run: {
|
|
2064
|
-
id: run.id,
|
|
2065
|
-
output_directory: run.outputDirectory,
|
|
2066
|
-
metadata_path: run.metadataPath,
|
|
2067
|
-
prompt_path: run.promptPath
|
|
2068
|
-
}
|
|
2069
|
-
});
|
|
2070
|
-
const adapter = getAdapter(plan.provider.protocol);
|
|
2071
|
-
try {
|
|
2072
|
-
const result = await adapter.generate(runtimePlan, run);
|
|
2073
|
-
await writeGenerationMetadata(run, {
|
|
2074
|
-
plan: runtimePlanOutput,
|
|
2075
|
-
run: {
|
|
2076
|
-
id: run.id,
|
|
2077
|
-
output_directory: run.outputDirectory,
|
|
2078
|
-
metadata_path: run.metadataPath,
|
|
2079
|
-
prompt_path: run.promptPath
|
|
2080
|
-
},
|
|
2081
|
-
provider_response: result.provider_response,
|
|
2082
|
-
images: result.images
|
|
2083
|
-
});
|
|
2084
|
-
return {
|
|
2085
|
-
ok: true,
|
|
2086
|
-
output_dir: run.outputDirectory,
|
|
2087
|
-
metadata_path: run.metadataPath,
|
|
2088
|
-
images: result.images
|
|
2089
|
-
};
|
|
2090
|
-
} catch (error) {
|
|
2091
|
-
await writeGenerationMetadata(run, {
|
|
2092
|
-
plan: runtimePlanOutput,
|
|
2093
|
-
run: {
|
|
2094
|
-
id: run.id,
|
|
2095
|
-
output_directory: run.outputDirectory,
|
|
2096
|
-
metadata_path: run.metadataPath,
|
|
2097
|
-
prompt_path: run.promptPath
|
|
2098
|
-
},
|
|
2099
|
-
error: {
|
|
2100
|
-
message: error instanceof Error ? error.message : String(error),
|
|
2101
|
-
name: error instanceof Error ? error.name : void 0
|
|
2102
|
-
}
|
|
2103
|
-
});
|
|
2104
|
-
throw error;
|
|
2105
|
-
}
|
|
2106
|
-
}
|
|
2107
|
-
async function listHistory() {
|
|
2108
|
-
const baseDir = resolve3(cwd2(), "outputs", "picgen");
|
|
2109
|
-
const dates = await safeReaddir(baseDir);
|
|
2110
|
-
const runs = [];
|
|
2111
|
-
for (const date of dates) {
|
|
2112
|
-
const datePath = join6(baseDir, date);
|
|
2113
|
-
if (!await isDirectory(datePath)) continue;
|
|
2114
|
-
for (const runName of await safeReaddir(datePath)) {
|
|
2115
|
-
const runPath = join6(datePath, runName);
|
|
2116
|
-
if (!await isDirectory(runPath)) continue;
|
|
2117
|
-
const metadataPath = join6(runPath, "metadata.json");
|
|
2118
|
-
const promptPath = join6(runPath, "prompt.txt");
|
|
2119
|
-
const metadata = await readJsonFile(metadataPath);
|
|
2120
|
-
const prompt = await readTextFile(promptPath);
|
|
2121
|
-
const images = (metadata?.images ?? []).filter((image) => image.path).map((image) => ({
|
|
2122
|
-
...image,
|
|
2123
|
-
url: `/api/file?path=${encodeURIComponent(image.path)}`
|
|
2124
|
-
}));
|
|
2125
|
-
const info = await stat2(runPath);
|
|
2126
|
-
runs.push({
|
|
2127
|
-
id: runName,
|
|
2128
|
-
date,
|
|
2129
|
-
created_at: info.mtime.toISOString(),
|
|
2130
|
-
output_dir: runPath,
|
|
2131
|
-
metadata_path: metadataPath,
|
|
2132
|
-
prompt,
|
|
2133
|
-
plan: metadata?.plan,
|
|
2134
|
-
images
|
|
2135
|
-
});
|
|
2136
|
-
}
|
|
2137
|
-
}
|
|
2138
|
-
return runs.sort((a, b) => String(b.created_at).localeCompare(String(a.created_at))).slice(0, 60);
|
|
2139
|
-
}
|
|
2140
|
-
async function sendOutputFile(response, path) {
|
|
2141
|
-
if (!path) {
|
|
2142
|
-
sendJson(response, 400, { ok: false, error: "Missing file path." });
|
|
2143
|
-
return;
|
|
2144
|
-
}
|
|
2145
|
-
const resolved = resolve3(path);
|
|
2146
|
-
const allowedRoot = resolve3(cwd2(), "outputs", "picgen");
|
|
2147
|
-
if (!resolved.startsWith(`${allowedRoot}/`)) {
|
|
2148
|
-
sendJson(response, 403, { ok: false, error: "File is outside PicGen outputs." });
|
|
2149
|
-
return;
|
|
2150
|
-
}
|
|
2151
|
-
const extension = extname3(resolved).toLowerCase();
|
|
2152
|
-
const mimeType = extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".webp" ? "image/webp" : extension === ".png" ? "image/png" : "application/octet-stream";
|
|
2153
|
-
response.writeHead(200, {
|
|
2154
|
-
"Content-Type": mimeType,
|
|
2155
|
-
"Cache-Control": "no-store"
|
|
2156
|
-
});
|
|
2157
|
-
createReadStream(resolved).pipe(response);
|
|
2158
|
-
}
|
|
2159
|
-
function defaultProviderTemplate(protocol, channel) {
|
|
2160
|
-
if (protocol === "gemini") {
|
|
2161
|
-
return {
|
|
2162
|
-
name: channel === "official" ? "gemini_official" : "gemini_proxy",
|
|
2163
|
-
base_url: channel === "official" ? "https://generativelanguage.googleapis.com" : "https://www.pandai.vip",
|
|
2164
|
-
api_key_env: channel === "official" ? "GEMINI_API_KEY" : "PICGEN_GEMINI_PROXY_KEY",
|
|
2165
|
-
models: ["gemini-3.1-flash-image-preview", "gemini-3-pro-image-preview"]
|
|
2166
|
-
};
|
|
2167
|
-
}
|
|
2168
|
-
return {
|
|
2169
|
-
name: channel === "official" ? "openai_official" : "openai_proxy",
|
|
2170
|
-
base_url: channel === "official" ? "https://api.openai.com" : "https://www.pandai.vip",
|
|
2171
|
-
api_key_env: channel === "official" ? "OPENAI_API_KEY" : "PICGEN_OPENAI_PROXY_KEY",
|
|
2172
|
-
models: ["gpt-image-2"]
|
|
2173
|
-
};
|
|
2174
|
-
}
|
|
2175
|
-
function parseModels2(raw) {
|
|
2176
|
-
return raw.split(",").map((item) => item.trim()).filter(Boolean);
|
|
2177
|
-
}
|
|
2178
|
-
function asOptionalString(value) {
|
|
2179
|
-
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
2180
|
-
}
|
|
2181
|
-
async function readJson(request) {
|
|
2182
|
-
const chunks = [];
|
|
2183
|
-
for await (const chunk of request) {
|
|
2184
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
2185
|
-
}
|
|
2186
|
-
const text = Buffer.concat(chunks).toString("utf8");
|
|
2187
|
-
if (!text.trim()) return {};
|
|
2188
|
-
return JSON.parse(text);
|
|
2189
|
-
}
|
|
2190
|
-
async function safeReaddir(path) {
|
|
2191
|
-
try {
|
|
2192
|
-
return await readdir(path);
|
|
2193
|
-
} catch {
|
|
2194
|
-
return [];
|
|
2195
|
-
}
|
|
2196
|
-
}
|
|
2197
|
-
async function isDirectory(path) {
|
|
2198
|
-
try {
|
|
2199
|
-
return (await stat2(path)).isDirectory();
|
|
2200
|
-
} catch {
|
|
2201
|
-
return false;
|
|
2202
|
-
}
|
|
2203
|
-
}
|
|
2204
|
-
async function readJsonFile(path) {
|
|
2205
|
-
try {
|
|
2206
|
-
return JSON.parse(await readFile5(path, "utf8"));
|
|
2207
|
-
} catch {
|
|
2208
|
-
return void 0;
|
|
2209
|
-
}
|
|
2210
|
-
}
|
|
2211
|
-
async function readTextFile(path) {
|
|
2212
|
-
try {
|
|
2213
|
-
return await readFile5(path, "utf8");
|
|
2214
|
-
} catch {
|
|
2215
|
-
return void 0;
|
|
2216
|
-
}
|
|
2217
|
-
}
|
|
2218
|
-
function sendJson(response, status, value) {
|
|
2219
|
-
response.writeHead(status, {
|
|
2220
|
-
"Content-Type": "application/json; charset=utf-8",
|
|
2221
|
-
"Cache-Control": "no-store"
|
|
2222
|
-
});
|
|
2223
|
-
response.end(JSON.stringify(value, null, 2));
|
|
2224
|
-
}
|
|
2225
|
-
function sendHtml(response, value) {
|
|
2226
|
-
response.writeHead(200, {
|
|
2227
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
2228
|
-
"Cache-Control": "no-store"
|
|
2229
|
-
});
|
|
2230
|
-
response.end(value);
|
|
2231
|
-
}
|
|
2232
|
-
function sendText(response, status, value) {
|
|
2233
|
-
response.writeHead(status, {
|
|
2234
|
-
"Content-Type": "text/plain; charset=utf-8",
|
|
2235
|
-
"Cache-Control": "no-store"
|
|
2236
|
-
});
|
|
2237
|
-
response.end(value);
|
|
2238
|
-
}
|
|
2239
|
-
async function openBrowser(url) {
|
|
2240
|
-
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
2241
|
-
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
2242
|
-
await new Promise((resolveOpen) => {
|
|
2243
|
-
execFile2(command, args, () => resolveOpen());
|
|
2244
|
-
});
|
|
2245
|
-
}
|
|
2246
|
-
var appHtml = `<!doctype html>
|
|
2247
|
-
<html lang="zh-CN">
|
|
2248
|
-
<head>
|
|
2249
|
-
<meta charset="utf-8">
|
|
2250
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2251
|
-
<title>PicGen</title>
|
|
2252
|
-
<style>
|
|
2253
|
-
:root{--bg:#f6f7f9;--panel:#fff;--text:#17202a;--muted:#667085;--border:#d9dee7;--soft:#eef2f6;--accent:#2563eb;--accent2:#0f766e;--danger:#b42318;--ok:#087443;--warn:#b54708;--radius:8px}
|
|
2254
|
-
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}button,input,select,textarea{font:inherit}button{border:1px solid var(--border);background:#fff;color:var(--text);border-radius:7px;padding:7px 10px;cursor:pointer}button.primary{background:var(--accent);border-color:var(--accent);color:#fff}button.ghost{background:transparent}button.danger{color:var(--danger)}button:disabled{opacity:.55;cursor:not-allowed}input,select,textarea{width:100%;border:1px solid var(--border);border-radius:7px;padding:8px 9px;background:#fff;color:var(--text)}textarea{min-height:120px;resize:vertical}.wrap{max-width:1120px;margin:0 auto;padding:22px}.top{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:12px}.brand h1{font-size:22px;margin:0 0 3px}.brand p,.muted{color:var(--muted);margin:0}.notice{border:1px solid #bfdbfe;background:#eff6ff;color:#1e3a8a;border-radius:var(--radius);padding:10px 12px;margin-bottom:16px}.tabs{display:flex;gap:6px;margin-bottom:16px;border-bottom:1px solid var(--border)}.tab{border:0;border-radius:7px 7px 0 0;background:transparent;padding:9px 12px}.tab.active{background:#fff;border:1px solid var(--border);border-bottom-color:#fff;margin-bottom:-1px}.grid{display:grid;gap:14px}.cols{display:grid;grid-template-columns:1fr 360px;gap:14px}.panel{background:var(--panel);border:1px solid var(--border);border-radius:var(--radius);padding:14px}.panel h2{font-size:15px;margin:0 0 12px}.row{display:grid;grid-template-columns:150px 1fr;gap:10px;align-items:center;margin:9px 0}.actions{display:flex;gap:8px;flex-wrap:wrap}.provider{display:grid;grid-template-columns:1fr auto;gap:12px;border:1px solid var(--border);border-radius:var(--radius);padding:12px;background:#fff}.provider+.provider{margin-top:10px}.title{font-weight:650}.badges{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}.badge{display:inline-flex;align-items:center;border-radius:999px;background:var(--soft);color:#344054;font-size:12px;padding:2px 8px}.badge.ok{background:#dcfae6;color:var(--ok)}.badge.warn{background:#fef0c7;color:var(--warn)}.badge.default{background:#dbeafe;color:#1d4ed8}.keyline{margin-top:8px;color:var(--muted)}.keyline code,.path{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background:#f2f4f7;border-radius:5px;padding:2px 5px}.source{display:inline-flex;align-items:center;border-radius:5px;padding:2px 6px;background:#ecfdf3;color:#067647;font-size:12px;margin-left:4px}.source.project{background:#fff7ed;color:#9a3412}.source.shell{background:#eef4ff;color:#3538cd}.source.missing{background:#fef3f2;color:#b42318}.hint{margin-top:8px;color:var(--muted);font-size:12px}.warning{margin-top:8px;color:var(--warn);font-size:12px}.icon-btn{display:inline-flex;align-items:center;justify-content:center;width:31px;height:31px;padding:0;vertical-align:middle}.icon-btn svg{width:16px;height:16px;stroke:currentColor}.formgrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.full{grid-column:1/-1}.plan{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;white-space:pre-wrap;background:#f8fafc;border:1px solid var(--border);border-radius:var(--radius);padding:10px}.gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px}.run img,.result img{width:100%;height:150px;object-fit:contain;background:#f8fafc;border:1px solid var(--border);border-radius:var(--radius)}.run{background:#fff;border:1px solid var(--border);border-radius:var(--radius);padding:10px}.hidden{display:none}.toast{position:fixed;right:18px;bottom:18px;background:#111827;color:#fff;border-radius:8px;padding:10px 12px;max-width:360px}.small{font-size:12px}.split{display:flex;justify-content:space-between;gap:8px;align-items:center}@media(max-width:860px){.cols{grid-template-columns:1fr}.row{grid-template-columns:1fr}.formgrid{grid-template-columns:1fr}.provider{grid-template-columns:1fr}.top{display:block}.wrap{padding:14px}}
|
|
2255
|
-
</style>
|
|
2256
|
-
</head>
|
|
2257
|
-
<body>
|
|
2258
|
-
<main class="wrap">
|
|
2259
|
-
<div class="top">
|
|
2260
|
-
<div class="brand"><h1>PicGen</h1><p>\u672C\u5730\u751F\u56FE\u5DE5\u4F5C\u53F0</p></div>
|
|
2261
|
-
<div class="actions"><button id="refresh">\u5237\u65B0</button></div>
|
|
2262
|
-
</div>
|
|
2263
|
-
<div class="notice">\u8FD9\u662F\u672C\u673A\u4E34\u65F6\u9875\u9762\uFF0C\u53EA\u7ED1\u5B9A 127.0.0.1\u3002\u4F7F\u7528\u5B8C\u53EF\u4EE5\u5173\u95ED\u9875\u9762\uFF0C\u5E76\u5728\u542F\u52A8 PicGen \u7684\u7EC8\u7AEF\u6309 Ctrl+C \u9000\u51FA\u670D\u52A1\u3002</div>
|
|
2264
|
-
<nav class="tabs">
|
|
2265
|
-
<button class="tab active" data-tab="settings">\u914D\u7F6E</button>
|
|
2266
|
-
<button class="tab" data-tab="generate">\u751F\u6210</button>
|
|
2267
|
-
<button class="tab" data-tab="history">\u5386\u53F2</button>
|
|
2268
|
-
</nav>
|
|
2269
|
-
<section id="settings" class="grid"></section>
|
|
2270
|
-
<section id="generate" class="hidden"></section>
|
|
2271
|
-
<section id="history" class="hidden grid"></section>
|
|
2272
|
-
</main>
|
|
2273
|
-
<div id="toast" class="toast hidden"></div>
|
|
2274
|
-
<script>
|
|
2275
|
-
const token = new URLSearchParams(location.search).get('token');
|
|
2276
|
-
const headers = {'Content-Type':'application/json','x-picgen-token':token};
|
|
2277
|
-
const state = {data:null,plan:null};
|
|
2278
|
-
const $ = (s,p=document)=>p.querySelector(s);
|
|
2279
|
-
const esc = v => String(v ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
2280
|
-
function toast(msg){const el=$('#toast');el.textContent=msg;el.classList.remove('hidden');setTimeout(()=>el.classList.add('hidden'),3500)}
|
|
2281
|
-
async function api(path, opts={}){const res=await fetch(path,{...opts,headers:{...headers,...opts.headers}});const data=await res.json().catch(()=>({ok:false,error:'Invalid JSON'}));if(!res.ok||data.ok===false)throw new Error(data.error||data.message||res.statusText);return data}
|
|
2282
|
-
async function load(){state.data=await api('/api/state');renderSettings();renderGenerate();await renderHistory()}
|
|
2283
|
-
function tab(name){document.querySelectorAll('.tab').forEach(b=>b.classList.toggle('active',b.dataset.tab===name));['settings','generate','history'].forEach(id=>$('#'+id).classList.toggle('hidden',id!==name))}
|
|
2284
|
-
document.querySelectorAll('.tab').forEach(b=>b.onclick=()=>tab(b.dataset.tab));$('#refresh').onclick=()=>load().then(()=>toast('\u5DF2\u5237\u65B0')).catch(e=>toast(e.message));
|
|
2285
|
-
const labels={default:'\u9ED8\u8BA4',fallback:'\u5907\u7528',manual:'\u624B\u52A8',official:'\u5B98\u65B9',third_party:'\u7B2C\u4E09\u65B9','openai-images':'OpenAI \u517C\u5BB9',gemini:'Gemini',enabled:'\u5DF2\u542F\u7528',disabled:'\u5DF2\u505C\u7528',shell:'\u7EC8\u7AEF\u73AF\u5883\u53D8\u91CF',project:'\u5F53\u524D\u9879\u76EE .env',managed:'PicGen \u7BA1\u7406\u6587\u4EF6',missing:'\u672A\u914D\u7F6E'};
|
|
2286
|
-
const eyeIcon='<svg viewBox="0 0 24 24" fill="none" stroke-width="2"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12Z"/><circle cx="12" cy="12" r="3"/></svg>';
|
|
2287
|
-
const eyeOffIcon='<svg viewBox="0 0 24 24" fill="none" stroke-width="2"><path d="m2 2 20 20"/><path d="M10.6 10.6A3 3 0 0 0 13.4 13.4"/><path d="M9.9 4.2A10.4 10.4 0 0 1 12 4.0c6.5 0 10 8 10 8a17.8 17.8 0 0 1-2.3 3.5"/><path d="M6.6 6.6C3.6 8.7 2 12 2 12s3.5 8 10 8c1.6 0 3-.3 4.2-.9"/></svg>';
|
|
2288
|
-
function label(v){return labels[v]||v}
|
|
2289
|
-
function keySource(key){return key?.set?label(key.source):'\u672A\u914D\u7F6E'}
|
|
2290
|
-
function templateDefaults(template){return {gemini_proxy:{protocol:'gemini',channel:'third_party',name:'gemini_proxy',host:'https://www.pandai.vip',models:'gemini-3.1-flash-image-preview, gemini-3-pro-image-preview'},openai_proxy:{protocol:'openai-images',channel:'third_party',name:'openai_proxy',host:'https://www.pandai.vip',models:'gpt-image-2'},gemini_official:{protocol:'gemini',channel:'official',name:'gemini_official',host:'https://generativelanguage.googleapis.com',models:'gemini-3.1-flash-image-preview, gemini-3-pro-image-preview'},openai_official:{protocol:'openai-images',channel:'official',name:'openai_official',host:'https://api.openai.com',models:'gpt-image-2'}}[template]}
|
|
2291
|
-
function renderSettings(){const s=state.data;$('#settings').innerHTML=\`
|
|
2292
|
-
<div class="panel"><h2>\u8DEF\u5F84</h2><div class="row"><div>\u914D\u7F6E\u6587\u4EF6</div><div class="path">\${esc(s.config_path)}</div></div><div class="row"><div>PicGen \u5BC6\u94A5\u6587\u4EF6</div><div class="path">\${esc(s.key_file_path)}</div></div><p class="hint">key \u8BFB\u53D6\u4F18\u5148\u7EA7\uFF1A\u7EC8\u7AEF\u73AF\u5883\u53D8\u91CF > \u5F53\u524D\u9879\u76EE .env > PicGen \u7BA1\u7406\u6587\u4EF6\u3002</p></div>
|
|
2293
|
-
<div class="panel"><div class="split"><h2>\u6E20\u9053</h2><button class="primary" id="addProviderBtn">\u6DFB\u52A0\u6E20\u9053</button></div>
|
|
2294
|
-
<div class="hidden" id="providerForm" style="margin:12px 0 14px"><h2>\u6DFB\u52A0\u6E20\u9053</h2>
|
|
2295
|
-
<div class="formgrid">
|
|
2296
|
-
<label class="full">\u6E20\u9053\u7C7B\u578B<select id="newTemplate"><option value="gemini_proxy">\u7B2C\u4E09\u65B9 Gemini</option><option value="openai_proxy">\u7B2C\u4E09\u65B9 OpenAI \u517C\u5BB9</option><option value="gemini_official">\u5B98\u65B9 Gemini</option><option value="openai_official">\u5B98\u65B9 OpenAI</option></select></label>
|
|
2297
|
-
<label>\u540D\u79F0<input id="newName" placeholder="\u81EA\u52A8"></label>
|
|
2298
|
-
<label>Host<input id="newHost" value="https://www.pandai.vip"></label>
|
|
2299
|
-
<label class="full">API key<input id="newKey" type="password" placeholder="\u4FDD\u5B58\u5728\u672C\u673A\uFF0C\u4E0D\u5199\u5165\u804A\u5929"></label>
|
|
2300
|
-
<p class="hint full">\u6BCF\u4E2A\u65B0\u6E20\u9053\u4F1A\u81EA\u52A8\u5206\u914D\u72EC\u7ACB\u7684 key \u540D\u79F0\uFF0C\u907F\u514D\u591A\u4E2A\u6E20\u9053\u4E92\u76F8\u8986\u76D6\u3002</p>
|
|
2301
|
-
<label class="full">\u6A21\u578B\u5217\u8868<input id="newModels" placeholder="\u4F7F\u7528\u63A8\u8350\u9ED8\u8BA4\u503C"></label>
|
|
2302
|
-
<label><input id="newPrefer" type="checkbox" checked style="width:auto"> \u8BBE\u4E3A\u9ED8\u8BA4\u6E20\u9053</label>
|
|
2303
|
-
<div class="actions"><button class="primary" id="saveProvider">\u4FDD\u5B58\u6E20\u9053</button><button id="cancelProvider">\u53D6\u6D88</button></div>
|
|
2304
|
-
</div>
|
|
2305
|
-
</div><div id="providers"></div></div>\`;
|
|
2306
|
-
$('#addProviderBtn').onclick=()=>{const form=$('#providerForm');form.classList.remove('hidden');form.scrollIntoView({block:'nearest'});};$('#cancelProvider').onclick=()=>$('#providerForm').classList.add('hidden');$('#saveProvider').onclick=saveProvider;$('#newTemplate').onchange=applyProviderTemplate;applyProviderTemplate();
|
|
2307
|
-
$('#providers').innerHTML=s.providers.map(providerCard).join('') || '<p class="muted">\u8FD8\u6CA1\u6709\u914D\u7F6E\u6E20\u9053\u3002</p>';
|
|
2308
|
-
bindProviderActions();
|
|
2309
|
-
}
|
|
2310
|
-
function providerCard(p){const key=p.key||{set:false};const source=key.set?\`<span class="source \${esc(key.source)}">\${esc(keySource(key))}</span>\`:'<span class="source missing">\u672A\u914D\u7F6E</span>';const sourcePath=key.path?\`<div class="keyline small">\u6765\u6E90\u8DEF\u5F84 <span class="path">\${esc(key.path)}</span></div>\`:'';const officialHint=p.channel==='official'?'<div class="warning">\u5B98\u65B9\u6E20\u9053\u901A\u5E38\u9700\u8981\u5B98\u65B9 API key\uFF1B\u5982\u679C\u8FD9\u91CC\u653E\u7684\u662F\u7B2C\u4E09\u65B9\u6E20\u9053 key\uFF0C\u6D4B\u8BD5\u53EF\u80FD\u5931\u8D25\u3002</div>':'';return \`<div class="provider"><div><div class="title">\${esc(p.name)}</div><div class="badges"><span class="badge \${p.preference==='default'?'default':''}">\${esc(label(p.preference))}</span><span class="badge">\${esc(label(p.protocol))}</span><span class="badge">\${esc(label(p.channel))}</span><span class="badge \${p.enabled?'ok':'warn'}">\${p.enabled?'\u5DF2\u542F\u7528':'\u5DF2\u505C\u7528'}</span></div><div class="keyline">\${esc(p.base_url)}</div><div class="keyline">Key <code>\${esc(p.api_key_env)}</code>: \${key.set?\`<code data-key-preview="\${esc(p.api_key_env)}">\${esc(key.preview)}</code> <button class="icon-btn" title="\u663E\u793A\u5B8C\u6574 key" aria-label="\u663E\u793A\u5B8C\u6574 key" data-reveal="\${esc(p.api_key_env)}" data-visible="false">\${eyeIcon}</button>\`:'\u672A\u914D\u7F6E'} \${source} \${key.fingerprint?'<span class="small">fingerprint '+esc(key.fingerprint)+'</span>':''}</div>\${sourcePath}\${officialHint}</div><div class="actions"><button data-test="\${esc(p.name)}">\u6D4B\u8BD5</button><button data-default="\${esc(p.name)}">\u8BBE\u4E3A\u9ED8\u8BA4</button><button data-toggle="\${esc(p.name)}">\${p.enabled?'\u505C\u7528':'\u542F\u7528'}</button><button class="danger" data-delete="\${esc(p.name)}">\u79FB\u9664</button></div></div>\`}
|
|
2311
|
-
function bindProviderActions(){document.querySelectorAll('[data-test]').forEach(b=>b.onclick=()=>testProvider(b.dataset.test));document.querySelectorAll('[data-default]').forEach(b=>b.onclick=()=>post('/api/providers/'+encodeURIComponent(b.dataset.default)+'/default',{}));document.querySelectorAll('[data-toggle]').forEach(b=>{const p=state.data.providers.find(x=>x.name===b.dataset.toggle);b.onclick=()=>patch('/api/providers/'+encodeURIComponent(p.name),{enabled:!p.enabled})});document.querySelectorAll('[data-delete]').forEach(b=>b.onclick=()=>confirm('\u786E\u8BA4\u79FB\u9664\u8FD9\u4E2A\u6E20\u9053\uFF1F')&&del('/api/providers/'+encodeURIComponent(b.dataset.delete)));document.querySelectorAll('[data-reveal]').forEach(b=>b.onclick=()=>toggleKey(b.dataset.reveal,b))}
|
|
2312
|
-
function applyProviderTemplate(){const t=templateDefaults($('#newTemplate').value);$('#newName').placeholder=t.name;$('#newHost').value=t.host;$('#newModels').placeholder=t.models}
|
|
2313
|
-
async function saveProvider(){const t=templateDefaults($('#newTemplate').value);await post('/api/providers',{protocol:t.protocol,channel:t.channel,name:$('#newName').value,base_url:$('#newHost').value,api_key:$('#newKey').value,models:$('#newModels').value,prefer:$('#newPrefer').checked});$('#providerForm').classList.add('hidden');toast('\u6E20\u9053\u5DF2\u4FDD\u5B58')}
|
|
2314
|
-
async function testProvider(name){const r=await api('/api/providers/'+encodeURIComponent(name)+'/test',{method:'POST'});toast((r.ok?'\u6D4B\u8BD5\u901A\u8FC7\uFF1A':'\u6D4B\u8BD5\u5931\u8D25\uFF1A')+r.message)}
|
|
2315
|
-
async function toggleKey(name,btn){const slot=document.querySelector('[data-key-preview="'+CSS.escape(name)+'"]');if(btn.dataset.visible==='true'){const key=state.data.providers.find(p=>p.api_key_env===name)?.key;slot.textContent=key?.preview||'';btn.dataset.visible='false';btn.title='\u663E\u793A\u5B8C\u6574 key';btn.setAttribute('aria-label','\u663E\u793A\u5B8C\u6574 key');btn.innerHTML=eyeIcon;return}const r=await api('/api/key/'+encodeURIComponent(name));slot.textContent=r.value||'';btn.dataset.visible='true';btn.title='\u9690\u85CF key';btn.setAttribute('aria-label','\u9690\u85CF key');btn.innerHTML=eyeOffIcon}
|
|
2316
|
-
async function post(path,body){await api(path,{method:'POST',body:JSON.stringify(body)});await load()}
|
|
2317
|
-
async function patch(path,body){await api(path,{method:'PATCH',body:JSON.stringify(body)});await load()}
|
|
2318
|
-
async function del(path){await api(path,{method:'DELETE'});await load()}
|
|
2319
|
-
function renderGenerate(){const s=state.data;const prefs=JSON.parse(localStorage.getItem('picgen:prefs')||'{}');$('#generate').innerHTML=\`<div class="cols"><div class="panel"><h2>\u751F\u6210\u56FE\u7247</h2><label>\u63D0\u793A\u8BCD<textarea id="prompt">\${esc(prefs.prompt||'\u4E00\u5F20\u7B80\u6D01\u7684 PicGen \u6D4B\u8BD5\u56FE\uFF0C\u767D\u8272\u80CC\u666F\uFF0C\u5C11\u91CF\u84DD\u7EFF\u8272\u79D1\u6280\u611F\u70B9\u7F00')}</textarea></label><div class="formgrid"><label>\u6E20\u9053<select id="genProvider"><option value="">\u81EA\u52A8\u9009\u62E9</option>\${s.providers.map(p=>'<option value="'+esc(p.name)+'" '+(prefs.provider===p.name?'selected':'')+'>'+esc(p.name)+'</option>').join('')}</select></label><label>\u9884\u8BBE<select id="genPreset">\${Object.keys(s.presets).map(p=>'<option '+((prefs.preset||'fast-draft')===p?'selected':'')+'>'+esc(p)+'</option>').join('')}</select></label><label>\u6A21\u578B<input id="genModel" value="\${esc(prefs.model||'')}" placeholder="\u53EF\u9009"></label><label>\u6A21\u5F0F<input id="genMode" value="\${esc(prefs.mode||'')}" placeholder="\u53EF\u9009"></label></div><div class="actions" style="margin-top:12px"><button id="preview" class="primary">\u9884\u89C8\u65B9\u6848</button><button id="generateBtn" disabled>\u5F00\u59CB\u751F\u6210</button></div></div><div class="panel"><h2>\u65B9\u6848 / \u7ED3\u679C</h2><div id="plan" class="plan">\u8FD8\u6CA1\u6709\u9884\u89C8\u3002</div><div id="result" class="result" style="margin-top:12px"></div></div></div>\`;$('#preview').onclick=preview;$('#generateBtn').onclick=generateNow}
|
|
2320
|
-
function genBody(){const body={prompt:$('#prompt').value,preset:$('#genPreset').value,provider:$('#genProvider').value,model:$('#genModel').value,mode:$('#genMode').value};localStorage.setItem('picgen:prefs',JSON.stringify(body));return body}
|
|
2321
|
-
async function preview(){const r=await api('/api/plan',{method:'POST',body:JSON.stringify(genBody())});state.plan=r.plan;$('#plan').textContent=JSON.stringify(r.plan,null,2);$('#generateBtn').disabled=false}
|
|
2322
|
-
async function generateNow(){if(!state.plan&&!confirm('\u8FD8\u6CA1\u6709\u9884\u89C8\u65B9\u6848\uFF0C\u786E\u5B9A\u76F4\u63A5\u751F\u6210\uFF1F'))return;$('#generateBtn').disabled=true;$('#result').innerHTML='<p class="muted">\u6B63\u5728\u751F\u6210...</p>';try{const r=await api('/api/generate',{method:'POST',body:JSON.stringify(genBody())});$('#result').innerHTML='<p>\u5DF2\u4FDD\u5B58\u5230 <span class="path">'+esc(r.output_dir)+'</span></p>'+r.images.map(img=>'<img src="/api/file?path='+encodeURIComponent(img.path)+'&token='+token+'"><div class="path">'+esc(img.path)+'</div>').join('');await renderHistory()}catch(e){$('#result').innerHTML='<p style="color:var(--danger)">'+esc(e.message)+'</p>'}finally{$('#generateBtn').disabled=false}}
|
|
2323
|
-
async function renderHistory(){const h=$('#history');if(!state.data)return;const r=await api('/api/history');h.innerHTML='<div class="panel"><h2>\u5386\u53F2\u8BB0\u5F55</h2><p class="muted">\u627E\u56DE\u6700\u8FD1\u751F\u6210\u7684\u56FE\u7247\u548C\u672C\u5730\u4FDD\u5B58\u8DEF\u5F84\u3002</p></div><div class="gallery">'+r.runs.map(runCard).join('')+'</div>'}
|
|
2324
|
-
function runCard(r){const img=(r.images&&r.images[0])?'<img src="'+r.images[0].url+'&token='+token+'">':'';const plan=r.plan||{};return '<div class="run">'+img+'<div class="title">'+esc((r.prompt||'\u672A\u547D\u540D').slice(0,60))+'</div><div class="muted small">'+esc(new Date(r.created_at).toLocaleString())+' \xB7 '+esc(plan.provider||'')+' \xB7 '+esc(plan.preset||'')+'</div><div class="path small">'+esc(r.output_dir)+'</div><div class="actions" style="margin-top:8px"><button onclick="navigator.clipboard.writeText(\\''+esc(String(r.output_dir)).replaceAll("'","\\\\'")+'\\')">\u590D\u5236\u6587\u4EF6\u5939</button></div></div>'}
|
|
2325
|
-
load().catch(e=>toast(e.message));
|
|
2326
|
-
</script>
|
|
2327
|
-
</body>
|
|
2328
|
-
</html>`;
|
|
2329
1500
|
|
|
2330
1501
|
// src/commands/preferences.ts
|
|
2331
1502
|
async function preferMode(name) {
|
|
@@ -2351,39 +1522,26 @@ function formatQuickstart() {
|
|
|
2351
1522
|
"PicGen quickstart",
|
|
2352
1523
|
"",
|
|
2353
1524
|
"Install:",
|
|
2354
|
-
" node -v",
|
|
2355
|
-
" npm -v",
|
|
2356
1525
|
" npm install -g @ai-agent-tools/picgen",
|
|
2357
|
-
" npx -y skills add ai-agent-tools/picgen --skill picgen -g -y --copy",
|
|
2358
|
-
" picgen skill install codex",
|
|
2359
|
-
"",
|
|
2360
|
-
"Open local web UI:",
|
|
2361
|
-
" picgen open",
|
|
2362
1526
|
"",
|
|
2363
1527
|
"Configure:",
|
|
2364
|
-
" picgen setup
|
|
2365
|
-
" picgen provider quick-add gemini-proxy --host https://www.pandai.vip --prefer",
|
|
2366
|
-
" picgen key set PICGEN_GEMINI_PROXY_KEY --clipboard",
|
|
2367
|
-
" picgen key set PICGEN_GEMINI_PROXY_KEY --stdin",
|
|
2368
|
-
" picgen key list --json",
|
|
1528
|
+
" picgen setup",
|
|
2369
1529
|
" picgen doctor --json",
|
|
2370
1530
|
"",
|
|
2371
1531
|
"Preview before spending quota:",
|
|
2372
|
-
' picgen create --dry-run
|
|
1532
|
+
' picgen create --dry-run "\u4E00\u5F20\u6781\u7B80\u79D1\u6280\u611F\u4EA7\u54C1\u6D77\u62A5"',
|
|
2373
1533
|
"",
|
|
2374
1534
|
"Generate after confirmation:",
|
|
2375
|
-
' picgen create --yes
|
|
1535
|
+
' picgen create --yes "\u4E00\u5F20\u6781\u7B80\u79D1\u6280\u611F\u4EA7\u54C1\u6D77\u62A5"',
|
|
2376
1536
|
"",
|
|
2377
1537
|
"Use a reference image:",
|
|
2378
1538
|
' picgen create --dry-run --reference ./reference.png "\u57FA\u4E8E\u53C2\u8003\u56FE\u751F\u6210\u4E00\u5F20\u54C1\u724C\u6D77\u62A5"',
|
|
2379
1539
|
"",
|
|
2380
1540
|
"Agent prompt:",
|
|
2381
|
-
" \u8BF7\
|
|
1541
|
+
" \u8BF7\u5B89\u88C5\u5E76\u4F53\u9A8C @ai-agent-tools/picgen\uFF1A\u5168\u5C40\u5B89\u88C5 npm install -g @ai-agent-tools/picgen\uFF0C\u8FD0\u884C picgen setup \u914D\u7F6E\uFF0C\u7136\u540E\u5148 dry-run \u9884\u89C8\uFF0C\u518D\u786E\u8BA4\u751F\u6210\u4E00\u5F20\u6D4B\u8BD5\u56FE\u3002\u5982\u679C\u6211\u8981\u7528\u53C2\u8003\u56FE\uFF0C\u8BF7\u4F7F\u7528 --reference <\u56FE\u7247\u8DEF\u5F84>\u3002",
|
|
2382
1542
|
"",
|
|
2383
1543
|
"Notes:",
|
|
2384
1544
|
" - Provider host URLs should not include /v1 or /v1beta.",
|
|
2385
|
-
" - picgen setup can store API keys in ~/.picgen/.env.",
|
|
2386
|
-
" - picgen open starts a foreground local web UI on 127.0.0.1:8188.",
|
|
2387
1545
|
" - Agent workflows should dry-run before real generation.",
|
|
2388
1546
|
" - Generated images are saved locally; do not paste base64 into chat.",
|
|
2389
1547
|
" - First-user rollout checklist: docs/release-alpha.md"
|
|
@@ -2391,7 +1549,7 @@ function formatQuickstart() {
|
|
|
2391
1549
|
}
|
|
2392
1550
|
|
|
2393
1551
|
// src/commands/setup.ts
|
|
2394
|
-
import { confirm as confirm2, input as input2,
|
|
1552
|
+
import { confirm as confirm2, input as input2, select as select2 } from "@inquirer/prompts";
|
|
2395
1553
|
async function runSetup() {
|
|
2396
1554
|
await ensureConfig();
|
|
2397
1555
|
console.log(`PicGen config: ${getConfigPath()}`);
|
|
@@ -2406,20 +1564,17 @@ async function runSetup() {
|
|
|
2406
1564
|
{ name: "Quick add a common provider/channel", value: "quick-add" },
|
|
2407
1565
|
{ name: "Choose default provider/channel", value: "provider" },
|
|
2408
1566
|
{ name: "Choose generation preference", value: "mode" },
|
|
2409
|
-
{ name: "Configure API key", value: "key" },
|
|
2410
1567
|
{ name: "Test a provider", value: "test" },
|
|
2411
1568
|
{ name: "Advanced: add a custom provider/channel", value: "add" },
|
|
2412
1569
|
{ name: "Finish setup", value: "done" }
|
|
2413
1570
|
]
|
|
2414
1571
|
});
|
|
2415
1572
|
if (action === "quick-add") {
|
|
2416
|
-
await
|
|
1573
|
+
await quickAddProvider();
|
|
2417
1574
|
} else if (action === "provider") {
|
|
2418
1575
|
await chooseDefaultProvider();
|
|
2419
1576
|
} else if (action === "mode") {
|
|
2420
1577
|
await chooseDefaultMode();
|
|
2421
|
-
} else if (action === "key") {
|
|
2422
|
-
await chooseProviderKeyToConfigure();
|
|
2423
1578
|
} else if (action === "test") {
|
|
2424
1579
|
await chooseProviderToTest();
|
|
2425
1580
|
} else if (action === "add") {
|
|
@@ -2438,7 +1593,7 @@ async function printSetupSummary() {
|
|
|
2438
1593
|
for (const [providerName, provider2] of Object.entries(config.providers)) {
|
|
2439
1594
|
const preference = providerName === config.routing.default_provider ? "default" : config.routing.fallback_providers.includes(providerName) ? "fallback" : "manual";
|
|
2440
1595
|
console.log(
|
|
2441
|
-
`- ${providerName}: ${provider2.enabled ? "enabled" : "disabled"}, ${preference}, ${providerLabel(provider2)},
|
|
1596
|
+
`- ${providerName}: ${provider2.enabled ? "enabled" : "disabled"}, ${preference}, ${providerLabel(provider2)}, capabilities=${provider2.capabilities.join(",")}`
|
|
2442
1597
|
);
|
|
2443
1598
|
}
|
|
2444
1599
|
}
|
|
@@ -2486,19 +1641,7 @@ async function chooseProviderToTest() {
|
|
|
2486
1641
|
if (result.model) console.log(`Model: ${result.model}`);
|
|
2487
1642
|
if (result.http_status) console.log(`HTTP status: ${result.http_status}`);
|
|
2488
1643
|
}
|
|
2489
|
-
async function
|
|
2490
|
-
const config = await loadConfig();
|
|
2491
|
-
const name = await select2({
|
|
2492
|
-
message: "Choose the provider key to configure",
|
|
2493
|
-
default: config.routing.default_provider,
|
|
2494
|
-
choices: Object.entries(config.providers).map(([providerName, provider2]) => ({
|
|
2495
|
-
name: `${providerName} (${provider2.api_key_env}${process.env[provider2.api_key_env] ? ", currently set" : ", missing"})`,
|
|
2496
|
-
value: providerName
|
|
2497
|
-
}))
|
|
2498
|
-
});
|
|
2499
|
-
await configureProviderApiKey(config.providers[name]);
|
|
2500
|
-
}
|
|
2501
|
-
async function quickAddProvider2() {
|
|
1644
|
+
async function quickAddProvider() {
|
|
2502
1645
|
const config = await loadConfig();
|
|
2503
1646
|
const template = await select2({
|
|
2504
1647
|
message: "Choose the provider/channel you want to add",
|
|
@@ -2532,7 +1675,7 @@ async function quickAddProvider2() {
|
|
|
2532
1675
|
});
|
|
2533
1676
|
const apiKeyEnv = await input2({
|
|
2534
1677
|
message: "API key environment variable",
|
|
2535
|
-
default:
|
|
1678
|
+
default: defaults.api_key_env
|
|
2536
1679
|
});
|
|
2537
1680
|
const modelsRaw = await input2({
|
|
2538
1681
|
message: "Models (comma separated, press Enter for recommended defaults)",
|
|
@@ -2544,7 +1687,7 @@ async function quickAddProvider2() {
|
|
|
2544
1687
|
channel: defaults.channel,
|
|
2545
1688
|
base_url: normalizeProviderBaseUrl(baseUrl),
|
|
2546
1689
|
api_key_env: apiKeyEnv,
|
|
2547
|
-
models:
|
|
1690
|
+
models: parseModels(modelsRaw),
|
|
2548
1691
|
capabilities: defaultCapabilitiesForProtocol2(defaults.protocol)
|
|
2549
1692
|
};
|
|
2550
1693
|
addProviderToConfig(config, name, provider2);
|
|
@@ -2557,27 +1700,7 @@ async function quickAddProvider2() {
|
|
|
2557
1700
|
}
|
|
2558
1701
|
await saveConfig(config);
|
|
2559
1702
|
console.log(`Added provider: ${name}`);
|
|
2560
|
-
|
|
2561
|
-
}
|
|
2562
|
-
async function configureProviderApiKey(provider2) {
|
|
2563
|
-
if (process.env[provider2.api_key_env]) {
|
|
2564
|
-
const replace = await confirm2({
|
|
2565
|
-
message: `${provider2.api_key_env} is already available. Replace the saved PicGen key?`,
|
|
2566
|
-
default: false
|
|
2567
|
-
});
|
|
2568
|
-
if (!replace) return;
|
|
2569
|
-
}
|
|
2570
|
-
const value = await password2({
|
|
2571
|
-
message: `Paste API key for ${provider2.api_key_env} (leave empty to skip)`,
|
|
2572
|
-
mask: "*"
|
|
2573
|
-
});
|
|
2574
|
-
if (!value.trim()) {
|
|
2575
|
-
console.log(`Skipped API key. You can configure it later with picgen setup.`);
|
|
2576
|
-
return;
|
|
2577
|
-
}
|
|
2578
|
-
const path = await saveManagedEnvVar(provider2.api_key_env, value.trim());
|
|
2579
|
-
console.log(`Saved ${provider2.api_key_env} to ${path}`);
|
|
2580
|
-
console.log(`PicGen loads this file automatically. Advanced users can override it with shell env vars or a project .env.`);
|
|
1703
|
+
console.log(`Set ${apiKeyEnv} in your shell or .env before testing this provider.`);
|
|
2581
1704
|
}
|
|
2582
1705
|
function quickProviderDefaults(template) {
|
|
2583
1706
|
switch (template) {
|
|
@@ -2587,7 +1710,7 @@ function quickProviderDefaults(template) {
|
|
|
2587
1710
|
protocol: "openai-images",
|
|
2588
1711
|
channel: "third_party",
|
|
2589
1712
|
base_url: "https://www.pandai.vip",
|
|
2590
|
-
api_key_env: "
|
|
1713
|
+
api_key_env: "OPENAI_API_KEY",
|
|
2591
1714
|
models: ["gpt-image-2"]
|
|
2592
1715
|
};
|
|
2593
1716
|
case "gemini_proxy":
|
|
@@ -2596,7 +1719,7 @@ function quickProviderDefaults(template) {
|
|
|
2596
1719
|
protocol: "gemini",
|
|
2597
1720
|
channel: "third_party",
|
|
2598
1721
|
base_url: "https://www.pandai.vip",
|
|
2599
|
-
api_key_env: "
|
|
1722
|
+
api_key_env: "GEMINI_API_KEY",
|
|
2600
1723
|
models: ["gemini-3.1-flash-image-preview", "gemini-3-pro-image-preview"]
|
|
2601
1724
|
};
|
|
2602
1725
|
case "openai_official":
|
|
@@ -2619,7 +1742,7 @@ function quickProviderDefaults(template) {
|
|
|
2619
1742
|
};
|
|
2620
1743
|
}
|
|
2621
1744
|
}
|
|
2622
|
-
function
|
|
1745
|
+
function parseModels(raw) {
|
|
2623
1746
|
return raw.split(",").map((model) => model.trim()).filter(Boolean);
|
|
2624
1747
|
}
|
|
2625
1748
|
function providerLabel(provider2) {
|
|
@@ -2641,54 +1764,11 @@ function modeLabel(modeName) {
|
|
|
2641
1764
|
}
|
|
2642
1765
|
}
|
|
2643
1766
|
|
|
2644
|
-
// src/commands/skill.ts
|
|
2645
|
-
import { access, cp, mkdir as mkdir5 } from "fs/promises";
|
|
2646
|
-
import { constants } from "fs";
|
|
2647
|
-
import { dirname as dirname4, join as join7, resolve as resolve4 } from "path";
|
|
2648
|
-
import { homedir as homedir4 } from "os";
|
|
2649
|
-
import { fileURLToPath } from "url";
|
|
2650
|
-
async function installSkill(target, options) {
|
|
2651
|
-
if (target !== "codex") {
|
|
2652
|
-
throw new Error(`Unsupported skill target: ${target}. Supported target: codex.`);
|
|
2653
|
-
}
|
|
2654
|
-
const source = await findBundledPicgenSkill();
|
|
2655
|
-
const destination = join7(getCodexHome(), "skills", "picgen");
|
|
2656
|
-
await mkdir5(dirname4(destination), { recursive: true });
|
|
2657
|
-
await cp(source, destination, {
|
|
2658
|
-
recursive: true,
|
|
2659
|
-
force: options.force ?? false,
|
|
2660
|
-
errorOnExist: !(options.force ?? false)
|
|
2661
|
-
});
|
|
2662
|
-
console.log(`Installed PicGen skill for Codex: ${destination}`);
|
|
2663
|
-
console.log("Restart Codex or start a new Codex session if the skill is not visible yet.");
|
|
2664
|
-
}
|
|
2665
|
-
function getCodexHome() {
|
|
2666
|
-
return process.env.CODEX_HOME ?? join7(homedir4(), ".codex");
|
|
2667
|
-
}
|
|
2668
|
-
async function findBundledPicgenSkill() {
|
|
2669
|
-
const here = dirname4(fileURLToPath(import.meta.url));
|
|
2670
|
-
const candidates = [
|
|
2671
|
-
resolve4(here, "../skills/picgen"),
|
|
2672
|
-
resolve4(here, "../../skills/picgen"),
|
|
2673
|
-
resolve4(process.cwd(), "skills/picgen")
|
|
2674
|
-
];
|
|
2675
|
-
for (const candidate of candidates) {
|
|
2676
|
-
try {
|
|
2677
|
-
await access(join7(candidate, "SKILL.md"), constants.R_OK);
|
|
2678
|
-
return candidate;
|
|
2679
|
-
} catch {
|
|
2680
|
-
}
|
|
2681
|
-
}
|
|
2682
|
-
throw new Error("Bundled PicGen skill not found. Reinstall @ai-agent-tools/picgen and try again.");
|
|
2683
|
-
}
|
|
2684
|
-
|
|
2685
1767
|
// src/cli.ts
|
|
2686
|
-
await loadPicgenEnv();
|
|
2687
1768
|
var program = new Command();
|
|
2688
1769
|
program.name("picgen").description("Lightweight image generation connector for AI agents.").version(VERSION);
|
|
2689
1770
|
program.command("setup").description("Run the interactive PicGen setup wizard.").action(runSetup);
|
|
2690
1771
|
program.command("quickstart").description("Print install and first-run guidance.").action(runQuickstart);
|
|
2691
|
-
program.command("open").description("Open the local PicGen web interface.").option("--port <port>", "Preferred local port. Defaults to 8188.").option("--no-open", "Print the URL without opening the browser.").action(runOpen);
|
|
2692
1772
|
program.command("doctor").description("Inspect PicGen configuration and provider readiness.").option("--json", "Print machine-readable JSON.").action(runDoctor);
|
|
2693
1773
|
program.command("create").description("Create an image generation plan or generate images.").argument("<prompt...>", "Prompt text.").option("--dry-run", "Plan generation without calling a provider.").option("--preset <name>", "Preset name.").option("--provider <name>", "Provider name.").option("--mode <name>", "Mode name.").option("--model <name>", "Model name.").option("--out-dir <path>", "Output directory.").option(
|
|
2694
1774
|
"--reference <path>",
|
|
@@ -2699,18 +1779,12 @@ program.command("create").description("Create an image generation plan or genera
|
|
|
2699
1779
|
var provider = program.command("provider").description("Manage providers/channels.");
|
|
2700
1780
|
provider.command("list").description("List providers.").action(listProviders);
|
|
2701
1781
|
provider.command("add").description("Add a provider.").action(addProvider);
|
|
2702
|
-
provider.command("quick-add").argument("<template>", "openai-proxy, gemini-proxy, openai-official, or gemini-official").description("Add a common provider/channel without interactive prompts.").option("--name <name>", "Provider name.").option("--host <url>", "Provider host URL. Do not include /v1 or /v1beta.").option("--key-env <name>", "API key environment variable.").option("--models <models>", "Comma-separated model list.").option("--prefer", "Use this provider as the default.").action(quickAddProvider);
|
|
2703
1782
|
provider.command("edit").argument("<name>").description("Edit a provider.").action(editProvider);
|
|
2704
1783
|
provider.command("test").argument("<name>").description("Test provider connectivity without generating an image.").option("--json", "Print machine-readable JSON.").action(runProviderTest);
|
|
2705
1784
|
provider.command("prefer").argument("<name>").description("Set the default provider preference.").action(preferProvider);
|
|
2706
1785
|
provider.command("enable").argument("<name>").description("Enable a provider.").action((name) => setProviderEnabled(name, true));
|
|
2707
1786
|
provider.command("disable").argument("<name>").description("Disable a provider.").action((name) => setProviderEnabled(name, false));
|
|
2708
1787
|
provider.command("remove").argument("<name>").description("Remove a provider.").action(removeProvider);
|
|
2709
|
-
var key = program.command("key").description("Manage PicGen API keys.");
|
|
2710
|
-
key.command("set").argument("<env-name>", "Environment variable name, such as PICGEN_GEMINI_PROXY_KEY.").description("Save an API key to PicGen's managed env file.").option("--stdin", "Read the key value from stdin.").option("--clipboard", "Read the key value from the macOS clipboard.").option("--value <value>", "Set the key value directly. Prefer --stdin for agent workflows.").action(setApiKey);
|
|
2711
|
-
key.command("list").description("List configured provider keys without revealing secret values.").option("--json", "Print machine-readable JSON.").action(listApiKeys);
|
|
2712
|
-
key.command("show").argument("<env-name>", "Environment variable name.").description("Show one configured key without revealing the secret value.").option("--json", "Print machine-readable JSON.").action(showApiKey);
|
|
2713
|
-
program.command("skill").description("Install PicGen agent skills.").command("install").argument("<target>", "Skill target. Currently supported: codex.").description("Install the bundled PicGen skill into an agent skill directory.").option("--force", "Overwrite an existing installed skill.").action(installSkill);
|
|
2714
1788
|
program.command("mode").description("Manage generation mode preferences.").command("prefer").argument("<name>").description("Set the default mode preference.").action(preferMode);
|
|
2715
1789
|
program.command("preset").description("Manage generation preset preferences.").command("prefer").argument("<name>").description("Set the default preset preference.").action(preferPreset);
|
|
2716
1790
|
program.command("update").description("Manage PicGen updates.").command("check").description("Check whether a newer PicGen version is available.").option("--json", "Print machine-readable JSON.").action(runUpdateCheck);
|