@playcraft/cli 0.0.36 → 0.0.37
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/dist/commands/login.js +138 -0
- package/dist/commands/prefab.js +10 -2
- package/dist/commands/tools.js +36 -0
- package/dist/config.js +31 -4
- package/dist/index.js +9 -0
- package/package.json +3 -3
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import { exec } from 'child_process';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import { saveGlobalConfig, loadGlobalConfig } from '../config.js';
|
|
5
|
+
const CALLBACK_PORT = 3456;
|
|
6
|
+
const TIMEOUT_MS = 5 * 60 * 1000;
|
|
7
|
+
function escapeHtml(text) {
|
|
8
|
+
return text
|
|
9
|
+
.replace(/&/g, '&')
|
|
10
|
+
.replace(/</g, '<')
|
|
11
|
+
.replace(/>/g, '>')
|
|
12
|
+
.replace(/"/g, '"');
|
|
13
|
+
}
|
|
14
|
+
function openBrowser(url) {
|
|
15
|
+
const cmd = process.platform === 'darwin'
|
|
16
|
+
? 'open'
|
|
17
|
+
: process.platform === 'win32'
|
|
18
|
+
? 'start'
|
|
19
|
+
: 'xdg-open';
|
|
20
|
+
try {
|
|
21
|
+
exec(`${cmd} "${url}"`, (err, _stdout, stderr) => {
|
|
22
|
+
if (err) {
|
|
23
|
+
console.error('Failed to open browser automatically:', err.message);
|
|
24
|
+
if (stderr?.trim()) {
|
|
25
|
+
console.error(stderr.trim());
|
|
26
|
+
}
|
|
27
|
+
console.error('Please use the URL printed above to complete login.');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
console.error('Failed to start browser launcher:', e instanceof Error ? e.message : e);
|
|
34
|
+
console.error('Please use the URL printed above to complete login.');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function loginCommand(options) {
|
|
38
|
+
const globalConfig = loadGlobalConfig();
|
|
39
|
+
const backendUrl = options.url ||
|
|
40
|
+
globalConfig.backendUrl ||
|
|
41
|
+
'https://playcraft.woa.com';
|
|
42
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const server = http.createServer((req, res) => {
|
|
45
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
46
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
|
47
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
48
|
+
if (req.method === 'OPTIONS') {
|
|
49
|
+
res.writeHead(204);
|
|
50
|
+
res.end();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (req.url === '/callback' && req.method === 'POST') {
|
|
54
|
+
let body = '';
|
|
55
|
+
req.on('data', (chunk) => (body += chunk));
|
|
56
|
+
req.on('end', () => {
|
|
57
|
+
try {
|
|
58
|
+
const data = JSON.parse(body);
|
|
59
|
+
if (data.state !== state) {
|
|
60
|
+
res.writeHead(403, { 'Content-Type': 'text/html' });
|
|
61
|
+
res.end('<html><body><h1>Invalid state</h1></body></html>');
|
|
62
|
+
server.close(() => {
|
|
63
|
+
reject(new Error('State mismatch - possible CSRF attack'));
|
|
64
|
+
});
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
saveGlobalConfig({ token: data.token, backendUrl });
|
|
68
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
69
|
+
res.end('<html>' +
|
|
70
|
+
'<head><meta charset="utf-8"><title>Login Success</title></head>' +
|
|
71
|
+
'<body style="font-family:system-ui;text-align:center;padding:40px">' +
|
|
72
|
+
'<h1>CLI Login Successful</h1>' +
|
|
73
|
+
'<p>You can close this window and return to your terminal.</p>' +
|
|
74
|
+
'</body></html>');
|
|
75
|
+
server.close();
|
|
76
|
+
console.log('Login successful! Token saved to ~/.playcraft/config.json');
|
|
77
|
+
resolve();
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
const cause = err instanceof Error ? err : new Error(String(err));
|
|
81
|
+
console.error('[playcraft login] Callback handling failed:', cause.message);
|
|
82
|
+
if (cause.stack) {
|
|
83
|
+
console.error(cause.stack);
|
|
84
|
+
}
|
|
85
|
+
const userFacing = cause instanceof SyntaxError
|
|
86
|
+
? 'Invalid JSON in login callback.'
|
|
87
|
+
: `Login callback failed: ${cause.message}`;
|
|
88
|
+
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
89
|
+
res.end('<html><head><meta charset="utf-8"/><title>Error</title></head>' +
|
|
90
|
+
'<body style="font-family:system-ui;padding:24px">' +
|
|
91
|
+
`<h1>Error</h1><p>${escapeHtml(userFacing)}</p>` +
|
|
92
|
+
'</body></html>');
|
|
93
|
+
server.close(() => {
|
|
94
|
+
reject(new Error(userFacing));
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
// Bind failures emit 'error' (e.g. EADDRINUSE); synchronously thrown errors from listen() are caught below.
|
|
101
|
+
server.on('error', (err) => {
|
|
102
|
+
server.close(() => {
|
|
103
|
+
if (err.code === 'EADDRINUSE') {
|
|
104
|
+
reject(new Error(`Port ${CALLBACK_PORT} is already in use. ` +
|
|
105
|
+
'Please close any other playcraft login process and try again.'));
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
reject(err);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
try {
|
|
113
|
+
server.listen(CALLBACK_PORT, () => {
|
|
114
|
+
const callbackUrl = `http://localhost:${CALLBACK_PORT}/callback`;
|
|
115
|
+
const authUrl = `${backendUrl}/cli-auth?callback=${encodeURIComponent(callbackUrl)}&state=${state}`;
|
|
116
|
+
console.log('Opening browser for login...');
|
|
117
|
+
console.log(`If the browser does not open, visit this URL manually:\n ${authUrl}\n`);
|
|
118
|
+
openBrowser(authUrl);
|
|
119
|
+
console.log(`Waiting for authorization (timeout: ${TIMEOUT_MS / 1000}s)...`);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
server.close(() => {
|
|
124
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// Timeout handler
|
|
129
|
+
const timeout = setTimeout(() => {
|
|
130
|
+
server.close(() => {
|
|
131
|
+
reject(new Error('Login timed out after 5 minutes. Please try again.'));
|
|
132
|
+
});
|
|
133
|
+
}, TIMEOUT_MS);
|
|
134
|
+
server.on('close', () => {
|
|
135
|
+
clearTimeout(timeout);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
package/dist/commands/prefab.js
CHANGED
|
@@ -1265,8 +1265,16 @@ export function registerPrefabCommands(program) {
|
|
|
1265
1265
|
console.error(`Error: 场景 "${variant}" 不存在。可用: ${scenes.map((s) => `${s.name}(${s.id})`).join(', ')}`);
|
|
1266
1266
|
process.exit(1);
|
|
1267
1267
|
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1268
|
+
let manifestJson;
|
|
1269
|
+
try {
|
|
1270
|
+
const manifestRaw = readLocalFile(projectDir, MANIFEST_PATH);
|
|
1271
|
+
manifestJson = JSON.parse(manifestRaw);
|
|
1272
|
+
}
|
|
1273
|
+
catch (e) {
|
|
1274
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1275
|
+
console.error(`Error: 无法读取或解析 ${MANIFEST_PATH}: ${msg}`);
|
|
1276
|
+
process.exit(1);
|
|
1277
|
+
}
|
|
1270
1278
|
const reordered = reorderManifestDefaultScene(manifestJson, found.id);
|
|
1271
1279
|
if (reordered && reordered !== manifestJson) {
|
|
1272
1280
|
writeFileSync(join(projectDir, MANIFEST_PATH), JSON.stringify(reordered, null, 2), 'utf-8');
|
package/dist/commands/tools.js
CHANGED
|
@@ -144,6 +144,40 @@ export function registerToolsCommands(program) {
|
|
|
144
144
|
const tools = program
|
|
145
145
|
.command('tools')
|
|
146
146
|
.description('后端 /api/agent/tools;需 PLAYCRAFT_API_URL + PLAYCRAFT_SANDBOX_TOKEN 或 .playcraft.json');
|
|
147
|
+
// ─── Image Models ────────────────────────────────────────────
|
|
148
|
+
tools.command('list-image-models')
|
|
149
|
+
.description('列出后端已配置的可用生图模型(modelKind=image),输出可直接作为 --image-model 的值')
|
|
150
|
+
.option('--json', '以 JSON 格式输出(默认为人类可读表格)')
|
|
151
|
+
.action(async (opts) => {
|
|
152
|
+
try {
|
|
153
|
+
const client = new AgentApiClient();
|
|
154
|
+
const models = await client.get('/image-models');
|
|
155
|
+
if (!models.length) {
|
|
156
|
+
console.log('暂无已配置的生图模型。请前往 Admin > AI Settings 添加 modelKind=image 的配置。');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (opts.json) {
|
|
160
|
+
console.log(JSON.stringify(models, null, 2));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
// 人类可读表格
|
|
164
|
+
const defaultMark = (m) => (m.isDefault ? ' (default)' : '');
|
|
165
|
+
const maxRefLen = Math.max(...models.map((m) => m.ref.length), 'MODEL REF'.length);
|
|
166
|
+
console.log('');
|
|
167
|
+
console.log(`${'MODEL REF'.padEnd(maxRefLen)} PROVIDER MODEL`);
|
|
168
|
+
console.log(`${'-'.repeat(maxRefLen)} ---------------- --------------------------------`);
|
|
169
|
+
for (const m of models) {
|
|
170
|
+
console.log(`${m.ref.padEnd(maxRefLen)} ${m.provider.padEnd(16)} ${m.model}${defaultMark(m)}`);
|
|
171
|
+
}
|
|
172
|
+
console.log('');
|
|
173
|
+
console.log(`使用方式:playcraft tools generate-image --prompt "..." --output out.png --image-model <MODEL REF>`);
|
|
174
|
+
console.log('');
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
console.error('获取生图模型列表失败:', err instanceof Error ? err.message : String(err));
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
147
181
|
// ─── Generation ─────────────────────────────────────────────
|
|
148
182
|
tools.command('generate-image')
|
|
149
183
|
.description('AI 生成图片(支持多张参考图图生图)')
|
|
@@ -152,6 +186,7 @@ export function registerToolsCommands(program) {
|
|
|
152
186
|
.requiredOption('--output <path>', '保存路径')
|
|
153
187
|
.option('--image-size <size>', '图片尺寸 (1K|2K|4K)')
|
|
154
188
|
.option('--reference-image <path>', '参考图路径(可重复多次,最多 8 张),支持 PNG/JPG/WEBP', collectReferenceImagePaths, [])
|
|
189
|
+
.option('--image-model <ref>', '生图模型,格式 provider/model-id(如 iegg-litellm/gpt-image-1 或 google/gemini-2.0-flash-preview-image-generation)')
|
|
155
190
|
.action(async (opts) => {
|
|
156
191
|
try {
|
|
157
192
|
const paths = opts.referenceImage ?? [];
|
|
@@ -165,6 +200,7 @@ export function registerToolsCommands(program) {
|
|
|
165
200
|
aspectRatio: opts.aspectRatio,
|
|
166
201
|
imageSize: opts.imageSize,
|
|
167
202
|
referenceImages,
|
|
203
|
+
...(opts.imageModel ? { imageModelRef: opts.imageModel } : {}),
|
|
168
204
|
});
|
|
169
205
|
const buf = Buffer.from(result.imageBase64, 'base64');
|
|
170
206
|
const sniffed = sniffImageExtension(buf);
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { cosmiconfig } from 'cosmiconfig';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import path from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import { mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
4
6
|
import 'dotenv/config';
|
|
5
7
|
export const ConfigSchema = z.object({
|
|
6
8
|
projectId: z.string().optional(),
|
|
@@ -13,19 +15,44 @@ export const ConfigSchema = z.object({
|
|
|
13
15
|
const explorer = cosmiconfig('playcraft', {
|
|
14
16
|
searchPlaces: ['playcraft.config.json', 'playcraft.agent.config.json'],
|
|
15
17
|
});
|
|
18
|
+
const GLOBAL_CONFIG_DIR = path.join(homedir(), '.playcraft');
|
|
19
|
+
const GLOBAL_CONFIG_FILE = path.join(GLOBAL_CONFIG_DIR, 'config.json');
|
|
20
|
+
export function loadGlobalConfig() {
|
|
21
|
+
try {
|
|
22
|
+
const content = readFileSync(GLOBAL_CONFIG_FILE, 'utf-8');
|
|
23
|
+
return JSON.parse(content);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function saveGlobalConfig(partial) {
|
|
30
|
+
mkdirSync(GLOBAL_CONFIG_DIR, { recursive: true });
|
|
31
|
+
let existing = {};
|
|
32
|
+
try {
|
|
33
|
+
existing = JSON.parse(readFileSync(GLOBAL_CONFIG_FILE, 'utf-8'));
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// ignore
|
|
37
|
+
}
|
|
38
|
+
const merged = { ...existing, ...partial };
|
|
39
|
+
writeFileSync(GLOBAL_CONFIG_FILE, JSON.stringify(merged, null, 2), 'utf-8');
|
|
40
|
+
}
|
|
16
41
|
export async function loadConfig(cliOptions) {
|
|
17
42
|
const result = await explorer.search(cliOptions.dir || process.cwd());
|
|
18
43
|
const fileConfig = result?.config || {};
|
|
19
44
|
const agentConfig = (fileConfig && typeof fileConfig === 'object' && 'agent' in fileConfig)
|
|
20
45
|
? (fileConfig.agent || {})
|
|
21
46
|
: fileConfig;
|
|
47
|
+
const globalConfig = loadGlobalConfig();
|
|
22
48
|
const config = ConfigSchema.parse({
|
|
49
|
+
...globalConfig,
|
|
23
50
|
...agentConfig,
|
|
24
51
|
...cliOptions,
|
|
25
|
-
projectId: cliOptions.projectId || process.env.PLAYCRAFT_PROJECT_ID || agentConfig.projectId,
|
|
26
|
-
token: cliOptions.token || process.env.PLAYCRAFT_TOKEN || agentConfig.token,
|
|
27
|
-
port: cliOptions.port || (process.env.PLAYCRAFT_PORT ? parseInt(process.env.PLAYCRAFT_PORT) : undefined) || agentConfig.port,
|
|
28
|
-
mode: cliOptions.mode || process.env.PLAYCRAFT_MODE || agentConfig.mode,
|
|
52
|
+
projectId: cliOptions.projectId || process.env.PLAYCRAFT_PROJECT_ID || agentConfig.projectId || globalConfig.projectId,
|
|
53
|
+
token: cliOptions.token || process.env.PLAYCRAFT_TOKEN || agentConfig.token || globalConfig.token,
|
|
54
|
+
port: cliOptions.port || (process.env.PLAYCRAFT_PORT ? parseInt(process.env.PLAYCRAFT_PORT) : undefined) || agentConfig.port || globalConfig.port,
|
|
55
|
+
mode: cliOptions.mode || process.env.PLAYCRAFT_MODE || agentConfig.mode || globalConfig.mode,
|
|
29
56
|
});
|
|
30
57
|
// Resolve absolute path for dir
|
|
31
58
|
config.dir = path.resolve(process.cwd(), config.dir);
|
package/dist/index.js
CHANGED
|
@@ -43,6 +43,15 @@ program
|
|
|
43
43
|
.action(async (options) => {
|
|
44
44
|
await initCommand(options.dir);
|
|
45
45
|
});
|
|
46
|
+
// login 命令
|
|
47
|
+
program
|
|
48
|
+
.command('login')
|
|
49
|
+
.description('登录 PlayCraft 账号并获取 CLI token')
|
|
50
|
+
.option('--url <url>', '后端地址', 'https://playcraft.woa.com')
|
|
51
|
+
.action(async (options) => {
|
|
52
|
+
const { loginCommand } = await import('./commands/login.js');
|
|
53
|
+
await loginCommand(options);
|
|
54
|
+
});
|
|
46
55
|
// start 命令
|
|
47
56
|
program
|
|
48
57
|
.command('start')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcraft/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.37",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"release": "node scripts/release.js"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@playcraft/build": "^0.0.
|
|
27
|
-
"@playcraft/common": "^0.0.
|
|
26
|
+
"@playcraft/build": "^0.0.37",
|
|
27
|
+
"@playcraft/common": "^0.0.25",
|
|
28
28
|
"chokidar": "^4.0.3",
|
|
29
29
|
"commander": "^13.1.0",
|
|
30
30
|
"cors": "^2.8.6",
|