@playcraft/cli 0.0.34 → 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 +15 -7
- 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
|
@@ -2,7 +2,7 @@ import { inspect } from 'node:util';
|
|
|
2
2
|
import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from 'fs';
|
|
3
3
|
import { join, resolve } from 'path';
|
|
4
4
|
import JSON5 from 'json5';
|
|
5
|
-
import { THEME_SCHEMA_PATH, THEME_INDEX_PATH, THEME_DIR, DEFAULT_GAME_PATH, MANIFEST_PATH, ASSETS_JSON_PATH, LITE_CREATOR_EXTENSIONS_DIR, themeDataPath, scenePath, defaultValueFromJsonSchemaProperty, buildThemeIndexTs, parseThemeIndexImportPath, parseThemePrefabs, mergeThemeDataKey, deleteThemeDataKey, parseExtensionMetaData, parseGameConfig, buildExtensionPrefabs, updateGameConfigValue, updateGameConfigExtensions, findConfigKeyCaseInsensitive, parseConfigKey, listScenesFromManifest, listScenesWithGameConfig, resolveGameConfigPath,
|
|
5
|
+
import { THEME_SCHEMA_PATH, THEME_INDEX_PATH, THEME_DIR, DEFAULT_GAME_PATH, MANIFEST_PATH, ASSETS_JSON_PATH, LITE_CREATOR_EXTENSIONS_DIR, themeDataPath, scenePath, defaultValueFromJsonSchemaProperty, buildThemeIndexTs, parseThemeIndexImportPath, parseThemePrefabs, mergeThemeDataKey, deleteThemeDataKey, parseExtensionMetaData, parseGameConfig, buildExtensionPrefabs, updateGameConfigValue, updateGameConfigExtensions, findConfigKeyCaseInsensitive, parseConfigKey, listScenesFromManifest, listScenesWithGameConfig, resolveGameConfigPath, reorderManifestDefaultScene, validateThemeValue, validateConfigValue, describeJsonSchemaFields, describeConfigSchemaFields, getPrefabFieldDescriptors, getPrefabFieldDiffs, prefabHasSchemaDiff, } from '@playcraft/common/prefab';
|
|
6
6
|
const DEFAULT_PAGE_LIMIT = 50;
|
|
7
7
|
/** 场景在 manifest / CLI 中列出但解析不到 GameConfig 时的人读标签 */
|
|
8
8
|
const NO_SCENE_GAMECONFIG_LABEL = '(无场景级 GameConfig)';
|
|
@@ -243,16 +243,16 @@ function resolveThemeId(projectDir, opts) {
|
|
|
243
243
|
}
|
|
244
244
|
// ─── PlayCanvas / LiteCreator helpers ────────────────────────────────────────
|
|
245
245
|
/**
|
|
246
|
-
* 当前活跃场景 ID = manifest.scenes[0]
|
|
247
|
-
*
|
|
248
|
-
* 与 Remix 编辑器和后端 `setDefaultScene`(将目标场景移到首位)的 SSOT 一致。
|
|
246
|
+
* 当前活跃场景 ID = manifest.scenes[0](与 `@playcraft/common` 中 `getActiveSceneIdFromManifest` 一致)。
|
|
247
|
+
* 用 `listScenesFromManifest` 组合实现,避免依赖较新 common 包才提供的命名导出。
|
|
249
248
|
*/
|
|
250
249
|
function getMainSceneId(projectDir) {
|
|
251
250
|
if (!fileExists(projectDir, MANIFEST_PATH))
|
|
252
251
|
return null;
|
|
253
252
|
try {
|
|
254
253
|
const manifestJson = JSON.parse(readLocalFile(projectDir, MANIFEST_PATH));
|
|
255
|
-
|
|
254
|
+
const scenes = listScenesFromManifest(manifestJson);
|
|
255
|
+
return scenes.length > 0 ? scenes[0].id : null;
|
|
256
256
|
}
|
|
257
257
|
catch {
|
|
258
258
|
return null;
|
|
@@ -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",
|