@openfairygui/cli 0.2.0-alpha.9 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/cli.mjs +17847 -13389
- package/package.json +7 -5
- package/src/cli.ts +30 -535
- package/src/commands/backend-capabilities.ts +37 -0
- package/src/commands/inspect.ts +44 -0
- package/src/commands/publish.ts +67 -0
- package/src/commands/restore.ts +49 -0
- package/src/utils/package-version.ts +23 -0
- package/src/utils/project-input.ts +26 -0
- package/src/utils/project-type.ts +31 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openfairygui/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "FairyGUI Headless Authoring SDK — command-line interface.",
|
|
5
5
|
"author": "OpenFairyGUI Contributors",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,16 +33,18 @@
|
|
|
33
33
|
"restore"
|
|
34
34
|
],
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@openfairygui/core": "0.2.
|
|
37
|
-
"@openfairygui/functions": "0.2.
|
|
36
|
+
"@openfairygui/core": "0.2.1",
|
|
37
|
+
"@openfairygui/functions": "0.2.1"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"
|
|
40
|
+
"commander": "^14.0.2",
|
|
41
|
+
"jiti": "^2.7.0",
|
|
42
|
+
"@openfairygui/backend": "0.2.1"
|
|
41
43
|
},
|
|
42
44
|
"optionalDependencies": {
|
|
43
45
|
"sharp": ">=0.33.0"
|
|
44
46
|
},
|
|
45
47
|
"scripts": {
|
|
46
|
-
"build": "tsdown src/cli.ts --format esm --platform node --no-dts --external sharp --
|
|
48
|
+
"build": "tsdown src/cli.ts --format esm --platform node --no-dts --external sharp --external jiti"
|
|
47
49
|
}
|
|
48
50
|
}
|
package/src/cli.ts
CHANGED
|
@@ -1,547 +1,42 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
resolvePublishOptions,
|
|
8
|
-
restore,
|
|
9
|
-
type InspectReport,
|
|
10
|
-
type PublishOptions,
|
|
11
|
-
type RestoreFileSystem,
|
|
12
|
-
type RestoreImageCropInput,
|
|
13
|
-
type RestoreImageCropper,
|
|
14
|
-
type RestoreImageExtractInput,
|
|
15
|
-
type RestoreImageExtractor,
|
|
16
|
-
} from '@openfairygui/functions';
|
|
17
|
-
import fs from 'node:fs/promises';
|
|
18
|
-
import { createRequire } from 'node:module';
|
|
19
|
-
import path from 'node:path';
|
|
20
|
-
import { parseArgs } from 'node:util';
|
|
21
|
-
|
|
22
|
-
const HELP = `
|
|
23
|
-
ofgui — FairyGUI Headless Authoring CLI
|
|
24
|
-
|
|
25
|
-
Alias:
|
|
26
|
-
openfairygui
|
|
27
|
-
|
|
28
|
-
Commands:
|
|
29
|
-
inspect <project-dir> Show project contents report
|
|
30
|
-
publish <project-dir> --output <dir> [options] Publish project to binary outputs and configured generated code
|
|
31
|
-
restore <release-dir> --output <dir> [options] Restore a FairyGUI project from published binaries
|
|
32
|
-
backend-capabilities <project-dir> Open a backend session, print runtime capabilities, then close it
|
|
33
|
-
|
|
34
|
-
Publish options:
|
|
35
|
-
--output, -o <dir> Output directory (required)
|
|
36
|
-
--compressed Compress binary data (overrides project setting)
|
|
37
|
-
--packages <a,b,c> Only publish specific packages (comma-separated)
|
|
38
|
-
--branch <name> Active branch used by "主干合并活跃分支"; omit for main branch
|
|
39
|
-
--project-type <name|id> Override project type (for example: unity, layabox, cocoscreator, 0, 4, 3)
|
|
40
|
-
|
|
41
|
-
Restore options:
|
|
42
|
-
--output, -o <dir> Output project directory (required)
|
|
43
|
-
--packages <a,b,c> Only restore specific packages (comma-separated)
|
|
44
|
-
--force Overwrite a non-empty output directory
|
|
45
|
-
--project-type <name|id> Override restored project type; default is unity
|
|
46
|
-
|
|
47
|
-
Options:
|
|
48
|
-
--help, -h Show this help
|
|
49
|
-
--version, -v Show version
|
|
50
|
-
|
|
51
|
-
Input can be a .fairy file or a project root directory (auto-discovers .fairy file).
|
|
52
|
-
File extension and binary format are read from project settings.
|
|
53
|
-
`;
|
|
54
|
-
|
|
55
|
-
const require = createRequire(import.meta.url);
|
|
56
|
-
|
|
57
|
-
function getInjectedPackageVersion(): string | null {
|
|
58
|
-
const version = (import.meta as ImportMeta & { env?: { PACKAGE_VERSION?: string } }).env?.PACKAGE_VERSION;
|
|
59
|
-
return typeof version === 'string' && version.length > 0 ? version : null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function readPackageVersion(): string {
|
|
63
|
-
const injectedVersion = getInjectedPackageVersion();
|
|
64
|
-
if (injectedVersion) return injectedVersion;
|
|
65
|
-
try {
|
|
66
|
-
const pkg = require('../package.json') as { version?: unknown };
|
|
67
|
-
if (typeof pkg.version === 'string' && pkg.version.length > 0) {
|
|
68
|
-
return pkg.version;
|
|
69
|
-
}
|
|
70
|
-
} catch {
|
|
71
|
-
// Keep the CLI usable when executed from a bundled artifact missing package.json.
|
|
72
|
-
}
|
|
73
|
-
return '0.0.0-dev';
|
|
74
|
-
}
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { registerBackendCapabilitiesCommand } from './commands/backend-capabilities.js';
|
|
3
|
+
import { registerInspectCommand } from './commands/inspect.js';
|
|
4
|
+
import { registerPublishCommand } from './commands/publish.js';
|
|
5
|
+
import { registerRestoreCommand } from './commands/restore.js';
|
|
6
|
+
import { readPackageVersion } from './utils/package-version.js';
|
|
75
7
|
|
|
76
8
|
const PACKAGE_VERSION = readPackageVersion();
|
|
77
9
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
throw new Error(`No .fairy file found in ${resolved}`);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
throw new Error(`Input is not a .fairy file or directory: ${resolved}`);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async function main(): Promise<void> {
|
|
106
|
-
const args = process.argv.slice(2);
|
|
107
|
-
|
|
108
|
-
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
109
|
-
console.log(HELP);
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (args.includes('--version') || args.includes('-v')) {
|
|
114
|
-
console.log(PACKAGE_VERSION);
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const command = args[0];
|
|
119
|
-
const rest = args.slice(1);
|
|
120
|
-
|
|
121
|
-
switch (command) {
|
|
122
|
-
case 'inspect':
|
|
123
|
-
await cmdInspect(rest);
|
|
124
|
-
break;
|
|
125
|
-
case 'publish':
|
|
126
|
-
await cmdPublish(rest);
|
|
127
|
-
break;
|
|
128
|
-
case 'restore':
|
|
129
|
-
await cmdRestore(rest);
|
|
130
|
-
break;
|
|
131
|
-
case 'backend-capabilities':
|
|
132
|
-
await cmdBackendCapabilities(rest);
|
|
133
|
-
break;
|
|
134
|
-
default:
|
|
135
|
-
console.error(`Unknown command: ${command}\n`);
|
|
136
|
-
console.log(HELP);
|
|
137
|
-
process.exit(1);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
interface RestoreImageProcessors {
|
|
142
|
-
cropImage: RestoreImageCropper;
|
|
143
|
-
extractImage: RestoreImageExtractor;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function createNodeRestoreFs(): RestoreFileSystem {
|
|
147
|
-
return {
|
|
148
|
-
async readFile(filePath: string): Promise<string> {
|
|
149
|
-
return fs.readFile(filePath, 'utf-8');
|
|
150
|
-
},
|
|
151
|
-
async readFileRaw(filePath: string): Promise<Uint8Array> {
|
|
152
|
-
const buf = await fs.readFile(filePath);
|
|
153
|
-
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
154
|
-
},
|
|
155
|
-
async writeFile(filePath: string, content: string): Promise<void> {
|
|
156
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
157
|
-
await fs.writeFile(filePath, content, 'utf-8');
|
|
158
|
-
},
|
|
159
|
-
async writeFileRaw(filePath: string, data: Uint8Array): Promise<void> {
|
|
160
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
161
|
-
await fs.writeFile(filePath, data);
|
|
162
|
-
},
|
|
163
|
-
async mkdir(dirPath: string): Promise<void> {
|
|
164
|
-
await fs.mkdir(dirPath, { recursive: true });
|
|
165
|
-
},
|
|
166
|
-
async readdir(dirPath: string): Promise<string[]> {
|
|
167
|
-
return fs.readdir(dirPath);
|
|
168
|
-
},
|
|
169
|
-
async exists(filePath: string): Promise<boolean> {
|
|
170
|
-
try {
|
|
171
|
-
await fs.access(filePath);
|
|
172
|
-
return true;
|
|
173
|
-
} catch {
|
|
174
|
-
return false;
|
|
175
|
-
}
|
|
176
|
-
},
|
|
177
|
-
async isFile(filePath: string): Promise<boolean> {
|
|
178
|
-
try {
|
|
179
|
-
return (await fs.stat(filePath)).isFile();
|
|
180
|
-
} catch {
|
|
181
|
-
return false;
|
|
182
|
-
}
|
|
183
|
-
},
|
|
184
|
-
async resolvePath(filePath: string): Promise<string> {
|
|
185
|
-
try {
|
|
186
|
-
return await fs.realpath(filePath);
|
|
187
|
-
} catch {
|
|
188
|
-
return path.resolve(filePath);
|
|
189
|
-
}
|
|
190
|
-
},
|
|
191
|
-
async rm(targetPath: string, options?: { recursive?: boolean; force?: boolean }): Promise<void> {
|
|
192
|
-
await fs.rm(targetPath, { recursive: options?.recursive ?? false, force: options?.force ?? false });
|
|
193
|
-
},
|
|
194
|
-
join(...paths: string[]): string {
|
|
195
|
-
return path.join(...paths);
|
|
196
|
-
},
|
|
197
|
-
dirname(filePath: string): string {
|
|
198
|
-
return path.dirname(filePath);
|
|
199
|
-
},
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
async function createRestoreImageProcessors(): Promise<RestoreImageProcessors> {
|
|
204
|
-
let sharp: any;
|
|
205
|
-
try {
|
|
206
|
-
const mod = await import('sharp');
|
|
207
|
-
sharp = mod.default ?? mod;
|
|
208
|
-
} catch {
|
|
209
|
-
throw new Error('restore: sharp is required to crop atlas images. Install it with: pnpm add sharp');
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
async function extractImage(input: RestoreImageExtractInput): Promise<Uint8Array> {
|
|
213
|
-
const targetPath = (input as RestoreImageCropInput).outputPath ?? input.sourcePath;
|
|
214
|
-
let image = sharp(input.sourcePath).extract({
|
|
215
|
-
left: input.left,
|
|
216
|
-
top: input.top,
|
|
217
|
-
width: input.width,
|
|
218
|
-
height: input.height,
|
|
219
|
-
});
|
|
220
|
-
if (input.rotated) image = image.rotate(90);
|
|
221
|
-
const { data, info } = await image.png().toBuffer({ resolveWithObject: true });
|
|
222
|
-
const needsOriginalCanvas =
|
|
223
|
-
input.expectedWidth > 0 &&
|
|
224
|
-
input.expectedHeight > 0 &&
|
|
225
|
-
(input.offsetX !== 0 ||
|
|
226
|
-
input.offsetY !== 0 ||
|
|
227
|
-
info.width !== input.expectedWidth ||
|
|
228
|
-
info.height !== input.expectedHeight);
|
|
229
|
-
|
|
230
|
-
if (needsOriginalCanvas) {
|
|
231
|
-
if (
|
|
232
|
-
input.offsetX < 0 ||
|
|
233
|
-
input.offsetY < 0 ||
|
|
234
|
-
input.offsetX + info.width > input.expectedWidth ||
|
|
235
|
-
input.offsetY + info.height > input.expectedHeight
|
|
236
|
-
) {
|
|
237
|
-
throw new Error(
|
|
238
|
-
`restore: Cropped image does not fit original canvas for ${targetPath}: ` +
|
|
239
|
-
`crop ${info.width}x${info.height} at ${input.offsetX},${input.offsetY}, ` +
|
|
240
|
-
`canvas ${input.expectedWidth}x${input.expectedHeight}`,
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
const composed = await sharp({
|
|
244
|
-
create: {
|
|
245
|
-
width: input.expectedWidth,
|
|
246
|
-
height: input.expectedHeight,
|
|
247
|
-
channels: 4,
|
|
248
|
-
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
249
|
-
},
|
|
250
|
-
})
|
|
251
|
-
.composite([{ input: data, left: input.offsetX, top: input.offsetY }])
|
|
252
|
-
.png()
|
|
253
|
-
.toBuffer({ resolveWithObject: true });
|
|
254
|
-
if (
|
|
255
|
-
input.expectedWidth > 0 &&
|
|
256
|
-
input.expectedHeight > 0 &&
|
|
257
|
-
(composed.info.width !== input.expectedWidth || composed.info.height !== input.expectedHeight)
|
|
258
|
-
) {
|
|
259
|
-
throw new Error(
|
|
260
|
-
`restore: Cropped image size mismatch for ${targetPath}: ` +
|
|
261
|
-
`expected ${input.expectedWidth}x${input.expectedHeight}, got ${composed.info.width}x${composed.info.height}`,
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
return composed.data;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (
|
|
268
|
-
input.expectedWidth > 0 &&
|
|
269
|
-
input.expectedHeight > 0 &&
|
|
270
|
-
(info.width !== input.expectedWidth || info.height !== input.expectedHeight)
|
|
271
|
-
) {
|
|
272
|
-
throw new Error(
|
|
273
|
-
`restore: Cropped image size mismatch for ${targetPath}: ` +
|
|
274
|
-
`expected ${input.expectedWidth}x${input.expectedHeight}, got ${info.width}x${info.height}`,
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
return data;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
return {
|
|
281
|
-
extractImage,
|
|
282
|
-
cropImage: async (input: RestoreImageCropInput): Promise<void> => {
|
|
283
|
-
await fs.mkdir(path.dirname(input.outputPath), { recursive: true });
|
|
284
|
-
await fs.writeFile(input.outputPath, await extractImage(input));
|
|
285
|
-
},
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
async function cmdInspect(args: string[]): Promise<void> {
|
|
290
|
-
if (args.length === 0) {
|
|
291
|
-
console.error('Usage: ofgui inspect <project-dir>');
|
|
292
|
-
process.exit(1);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const fairyPath = await resolveFairyPath(args[0]);
|
|
296
|
-
console.log(`Project: ${fairyPath}\n`);
|
|
297
|
-
|
|
298
|
-
const io = new NodeIO();
|
|
299
|
-
const doc = await io.readProject(fairyPath);
|
|
300
|
-
const report = inspect(doc);
|
|
301
|
-
|
|
302
|
-
printReport(report);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function printReport(report: InspectReport): void {
|
|
306
|
-
console.log(`ID: ${report.projectId}`);
|
|
307
|
-
console.log(`Type: ${report.projectType}, Version: ${report.version}`);
|
|
308
|
-
console.log(`\nPackages: ${report.totals.packages}`);
|
|
309
|
-
console.log(` Images: ${report.totals.images}`);
|
|
310
|
-
console.log(` Sounds: ${report.totals.sounds}`);
|
|
311
|
-
console.log(` Fonts: ${report.totals.fonts}`);
|
|
312
|
-
console.log(` MovieClips: ${report.totals.movieClips}`);
|
|
313
|
-
console.log(` Components: ${report.totals.components}`);
|
|
314
|
-
console.log(` DisplayObjs: ${report.totals.displayObjects}`);
|
|
315
|
-
console.log(` Gears: ${report.totals.gears}`);
|
|
316
|
-
console.log(` Controllers: ${report.totals.controllers}`);
|
|
317
|
-
console.log(` Transitions: ${report.totals.transitions}`);
|
|
318
|
-
|
|
319
|
-
console.log('\nPackage details:');
|
|
320
|
-
for (const pkg of report.packages) {
|
|
321
|
-
const res = pkg.resources;
|
|
322
|
-
console.log(
|
|
323
|
-
` ${pkg.name} (${pkg.id}): ${res.images.count} img, ${res.sounds.count} snd, ${res.fonts.count} font, ${res.components.count} comp`,
|
|
324
|
-
);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function parseProjectType(value: string | undefined): number | undefined {
|
|
329
|
-
if (!value) return undefined;
|
|
330
|
-
const trimmed = value.trim();
|
|
331
|
-
if (trimmed === '') return undefined;
|
|
332
|
-
if (/^\d+$/u.test(trimmed)) return Number(trimmed);
|
|
333
|
-
const normalized = trimmed.toLowerCase();
|
|
334
|
-
const map: Record<string, number> = {
|
|
335
|
-
unity: ProjectType.Unity,
|
|
336
|
-
flash: ProjectType.Flash,
|
|
337
|
-
starling: ProjectType.Starling,
|
|
338
|
-
cocoscreator: ProjectType.CocosCreator,
|
|
339
|
-
cocos: ProjectType.CocosCreator,
|
|
340
|
-
layabox: ProjectType.LayaBox,
|
|
341
|
-
laya: ProjectType.LayaBox,
|
|
342
|
-
egret: ProjectType.Egret,
|
|
343
|
-
haxe: ProjectType.Haxe,
|
|
344
|
-
pixi: ProjectType.Pixi,
|
|
345
|
-
libgdx: ProjectType.LibGDX,
|
|
346
|
-
unreal: ProjectType.Unreal,
|
|
347
|
-
cryengine: ProjectType.CryEngine,
|
|
348
|
-
monogame: ProjectType.MonoGame,
|
|
349
|
-
vision: ProjectType.Vision,
|
|
350
|
-
};
|
|
351
|
-
const resolved = map[normalized];
|
|
352
|
-
if (resolved === undefined) {
|
|
353
|
-
throw new Error(`Unknown project type: ${value}. Use a numeric id or one of: ${Object.keys(map).join(', ')}`);
|
|
354
|
-
}
|
|
355
|
-
return resolved;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
async function cmdRestore(args: string[]): Promise<void> {
|
|
359
|
-
const { values, positionals } = parseArgs({
|
|
360
|
-
args,
|
|
361
|
-
options: {
|
|
362
|
-
output: { type: 'string', short: 'o' },
|
|
363
|
-
packages: { type: 'string' },
|
|
364
|
-
force: { type: 'boolean' },
|
|
365
|
-
'project-type': { type: 'string' },
|
|
366
|
-
},
|
|
367
|
-
allowPositionals: true,
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
if (positionals.length === 0 || !values.output) {
|
|
371
|
-
console.error('Usage: ofgui restore <release-dir> --output <dir> [--packages a,b,c] [--force]');
|
|
372
|
-
process.exit(1);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
const releaseDir = path.resolve(positionals[0]);
|
|
376
|
-
const outputDir = path.resolve(values.output);
|
|
377
|
-
const pkgFilter = values.packages
|
|
378
|
-
? values.packages
|
|
379
|
-
.split(',')
|
|
380
|
-
.map((s) => s.trim())
|
|
381
|
-
.filter(Boolean)
|
|
382
|
-
: undefined;
|
|
383
|
-
const projectType = parseProjectType(values['project-type']);
|
|
384
|
-
const { cropImage, extractImage } = await createRestoreImageProcessors();
|
|
385
|
-
|
|
386
|
-
console.log(`Restoring published FairyGUI project: ${releaseDir}`);
|
|
387
|
-
const result = await restore({
|
|
388
|
-
inputDir: releaseDir,
|
|
389
|
-
output: outputDir,
|
|
390
|
-
fs: createNodeRestoreFs(),
|
|
391
|
-
packages: pkgFilter,
|
|
392
|
-
force: values.force,
|
|
393
|
-
projectType,
|
|
394
|
-
cropImage,
|
|
395
|
-
extractImage,
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
const packages = result.document.getRoot().listPackages();
|
|
399
|
-
console.log(`\nDone! Output: ${result.projectPath}`);
|
|
400
|
-
console.log(`Packages: ${packages.map((pkg) => pkg.getName()).join(', ')}`);
|
|
401
|
-
for (const warning of result.warnings) {
|
|
402
|
-
console.warn(`Warning: ${warning}`);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
async function cmdPublish(args: string[]): Promise<void> {
|
|
407
|
-
const { values, positionals } = parseArgs({
|
|
408
|
-
args,
|
|
409
|
-
options: {
|
|
410
|
-
output: { type: 'string', short: 'o' },
|
|
411
|
-
compressed: { type: 'boolean' },
|
|
412
|
-
packages: { type: 'string' },
|
|
413
|
-
branch: { type: 'string' },
|
|
414
|
-
'project-type': { type: 'string' },
|
|
415
|
-
},
|
|
416
|
-
allowPositionals: true,
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
if (positionals.length === 0 || !values.output) {
|
|
420
|
-
console.error(
|
|
421
|
-
'Usage: ofgui publish <project-dir> --output <dir> [--compressed] [--packages a,b,c] [--branch name]',
|
|
422
|
-
);
|
|
423
|
-
process.exit(1);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
const fairyPath = await resolveFairyPath(positionals[0]);
|
|
427
|
-
const projectDir = path.dirname(fairyPath);
|
|
428
|
-
const outputDir = path.resolve(values.output);
|
|
429
|
-
|
|
430
|
-
console.log(`Reading project: ${fairyPath}`);
|
|
431
|
-
const io = new NodeIO();
|
|
432
|
-
const doc = await io.readProject(fairyPath);
|
|
433
|
-
const projectType = parseProjectType(values['project-type']);
|
|
434
|
-
if (projectType !== undefined) {
|
|
435
|
-
doc.getRoot().setProjectType(projectType);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const pkgFilter = values.packages ? values.packages.split(',').map((s) => s.trim()) : undefined;
|
|
439
|
-
const resolved = resolvePublishOptions(doc, {
|
|
440
|
-
compressed: values.compressed,
|
|
441
|
-
packages: pkgFilter,
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
console.log(`Settings: ext=${resolved.fileExtension}, compressed=${resolved.compressed}`);
|
|
445
|
-
if (values.branch) {
|
|
446
|
-
console.log(`Active branch: ${values.branch}`);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
const atlasConfig: NonNullable<PublishOptions['atlas']> = {
|
|
450
|
-
...resolved.atlas,
|
|
451
|
-
readFileRaw: async (filePath: string) => {
|
|
452
|
-
const buf = await fs.readFile(filePath);
|
|
453
|
-
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
454
|
-
},
|
|
455
|
-
};
|
|
456
|
-
|
|
457
|
-
// Try to load sharp for atlas image compositing
|
|
458
|
-
let encoder: PublishOptions['encoder'];
|
|
459
|
-
try {
|
|
460
|
-
const sharp = await import('sharp');
|
|
461
|
-
encoder = sharp.default ?? sharp;
|
|
462
|
-
console.log('Sharp loaded — atlas PNGs will be generated.');
|
|
463
|
-
} catch {
|
|
464
|
-
console.log('Sharp not available — atlas PNGs will NOT be generated (layout only).');
|
|
465
|
-
console.log(' Install sharp to enable: pnpm add sharp');
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
const publishFs: NonNullable<PublishOptions['fs']> = {
|
|
469
|
-
async readFileRaw(filePath: string): Promise<Uint8Array> {
|
|
470
|
-
const buf = await fs.readFile(filePath);
|
|
471
|
-
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
472
|
-
},
|
|
473
|
-
async writeFileRaw(filePath: string, data: Uint8Array): Promise<void> {
|
|
474
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
475
|
-
await fs.writeFile(filePath, data);
|
|
476
|
-
},
|
|
477
|
-
async mkdir(dirPath: string): Promise<void> {
|
|
478
|
-
await fs.mkdir(dirPath, { recursive: true });
|
|
479
|
-
},
|
|
480
|
-
async readdir(dirPath: string): Promise<string[]> {
|
|
481
|
-
return fs.readdir(dirPath);
|
|
482
|
-
},
|
|
483
|
-
async deleteFile(filePath: string): Promise<void> {
|
|
484
|
-
await fs.rm(filePath, { force: true });
|
|
485
|
-
},
|
|
486
|
-
join(...paths: string[]): string {
|
|
487
|
-
return path.join(...paths);
|
|
488
|
-
},
|
|
489
|
-
};
|
|
490
|
-
|
|
491
|
-
await doc.transform(
|
|
492
|
-
publish({
|
|
493
|
-
output: outputDir,
|
|
494
|
-
compressed: resolved.compressed,
|
|
495
|
-
fileExtension: resolved.fileExtension,
|
|
496
|
-
packages: resolved.packages,
|
|
497
|
-
fs: publishFs,
|
|
498
|
-
encoder,
|
|
499
|
-
basePath: path.join(projectDir, 'assets'),
|
|
500
|
-
atlas: atlasConfig,
|
|
501
|
-
branch: values.branch,
|
|
502
|
-
}),
|
|
10
|
+
function createProgram(): Command {
|
|
11
|
+
const program = new Command('ofgui');
|
|
12
|
+
|
|
13
|
+
program.description('FairyGUI Headless Authoring CLI').version(PACKAGE_VERSION).showHelpAfterError();
|
|
14
|
+
|
|
15
|
+
registerInspectCommand(program);
|
|
16
|
+
registerPublishCommand(program);
|
|
17
|
+
registerRestoreCommand(program);
|
|
18
|
+
registerBackendCapabilitiesCommand(program);
|
|
19
|
+
|
|
20
|
+
program.addHelpText(
|
|
21
|
+
'after',
|
|
22
|
+
[
|
|
23
|
+
'',
|
|
24
|
+
'Alias:',
|
|
25
|
+
' openfairygui',
|
|
26
|
+
'',
|
|
27
|
+
'Input can be a .fairy file or a project root directory (auto-discovers .fairy file).',
|
|
28
|
+
'File extension and binary format are read from project settings.',
|
|
29
|
+
].join('\n'),
|
|
503
30
|
);
|
|
504
31
|
|
|
505
|
-
|
|
32
|
+
return program;
|
|
506
33
|
}
|
|
507
34
|
|
|
508
|
-
async function
|
|
509
|
-
|
|
510
|
-
console.error('Usage: ofgui backend-capabilities <project-dir>');
|
|
511
|
-
process.exit(1);
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
const runtime = createNodeBackendRuntime();
|
|
515
|
-
const opened = await runtime.openSession({ projectPath: path.resolve(args[0]) });
|
|
516
|
-
if (!opened.ok) {
|
|
517
|
-
const failure = opened as Extract<typeof opened, { ok: false }>;
|
|
518
|
-
console.error(`backend-capabilities: ${failure.error.message}`);
|
|
519
|
-
process.exit(1);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
const capabilities = runtime.getCapabilities();
|
|
523
|
-
if (!capabilities.ok) {
|
|
524
|
-
console.error('backend-capabilities: failed to read capabilities');
|
|
525
|
-
await runtime.closeSession({ sessionId: opened.data.sessionId });
|
|
526
|
-
process.exit(1);
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
console.log(`Session: ${opened.data.sessionId}`);
|
|
530
|
-
console.log(`Project: ${opened.data.canonicalProjectPath}`);
|
|
531
|
-
console.log(`Revision: ${opened.data.revision}`);
|
|
532
|
-
console.log(`Runtime owner: ${capabilities.data.runtimeOwner}`);
|
|
533
|
-
console.log(`Transaction owner: ${capabilities.data.transactionKernelOwner}`);
|
|
534
|
-
console.log(`App seam owner: ${capabilities.data.appSeamOwner}`);
|
|
535
|
-
|
|
536
|
-
const closed = await runtime.closeSession({ sessionId: opened.data.sessionId });
|
|
537
|
-
if (!closed.ok) {
|
|
538
|
-
const failure = closed as Extract<typeof closed, { ok: false }>;
|
|
539
|
-
console.error(`backend-capabilities: ${failure.error.message}`);
|
|
540
|
-
process.exit(1);
|
|
541
|
-
}
|
|
35
|
+
async function main(): Promise<void> {
|
|
36
|
+
await createProgram().parseAsync(process.argv);
|
|
542
37
|
}
|
|
543
38
|
|
|
544
39
|
main().catch((err) => {
|
|
545
|
-
console.error(err);
|
|
40
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
546
41
|
process.exit(1);
|
|
547
42
|
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createNodeBackendRuntime } from '@openfairygui/backend/node';
|
|
2
|
+
import type { Command } from 'commander';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
export function registerBackendCapabilitiesCommand(program: Command): void {
|
|
6
|
+
program
|
|
7
|
+
.command('backend-capabilities')
|
|
8
|
+
.description('Open a backend session, print runtime capabilities, then close it')
|
|
9
|
+
.argument('<project-dir>', 'Project root directory')
|
|
10
|
+
.action(async (projectDir: string) => {
|
|
11
|
+
const runtime = createNodeBackendRuntime();
|
|
12
|
+
const opened = await runtime.openSession({ projectPath: path.resolve(projectDir) });
|
|
13
|
+
if (!opened.ok) {
|
|
14
|
+
const failure = opened as Extract<typeof opened, { ok: false }>;
|
|
15
|
+
throw new Error(`backend-capabilities: ${failure.error.message}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const capabilities = runtime.getCapabilities();
|
|
19
|
+
if (!capabilities.ok) {
|
|
20
|
+
await runtime.closeSession({ sessionId: opened.data.sessionId });
|
|
21
|
+
throw new Error('backend-capabilities: failed to read capabilities');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
console.log(`Session: ${opened.data.sessionId}`);
|
|
25
|
+
console.log(`Project: ${opened.data.canonicalProjectPath}`);
|
|
26
|
+
console.log(`Revision: ${opened.data.revision}`);
|
|
27
|
+
console.log(`Runtime owner: ${capabilities.data.runtimeOwner}`);
|
|
28
|
+
console.log(`Transaction owner: ${capabilities.data.transactionKernelOwner}`);
|
|
29
|
+
console.log(`App seam owner: ${capabilities.data.appSeamOwner}`);
|
|
30
|
+
|
|
31
|
+
const closed = await runtime.closeSession({ sessionId: opened.data.sessionId });
|
|
32
|
+
if (!closed.ok) {
|
|
33
|
+
const failure = closed as Extract<typeof closed, { ok: false }>;
|
|
34
|
+
throw new Error(`backend-capabilities: ${failure.error.message}`);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import { inspect, type InspectReport } from '@openfairygui/functions';
|
|
3
|
+
import { NodeIO } from '@openfairygui/core/node';
|
|
4
|
+
import { resolveFairyPath } from '../utils/project-input.js';
|
|
5
|
+
|
|
6
|
+
export function registerInspectCommand(program: Command): void {
|
|
7
|
+
program
|
|
8
|
+
.command('inspect')
|
|
9
|
+
.description('Show project contents report')
|
|
10
|
+
.argument('<project-dir>', 'Project root directory or .fairy file')
|
|
11
|
+
.action(async (projectDir: string) => {
|
|
12
|
+
const fairyPath = await resolveFairyPath(projectDir);
|
|
13
|
+
console.log(`Project: ${fairyPath}\n`);
|
|
14
|
+
|
|
15
|
+
const io = new NodeIO();
|
|
16
|
+
const doc = await io.readProject(fairyPath);
|
|
17
|
+
const report = inspect(doc);
|
|
18
|
+
|
|
19
|
+
printReport(report);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function printReport(report: InspectReport): void {
|
|
24
|
+
console.log(`ID: ${report.projectId}`);
|
|
25
|
+
console.log(`Type: ${report.projectType}, Version: ${report.version}`);
|
|
26
|
+
console.log(`\nPackages: ${report.totals.packages}`);
|
|
27
|
+
console.log(` Images: ${report.totals.images}`);
|
|
28
|
+
console.log(` Sounds: ${report.totals.sounds}`);
|
|
29
|
+
console.log(` Fonts: ${report.totals.fonts}`);
|
|
30
|
+
console.log(` MovieClips: ${report.totals.movieClips}`);
|
|
31
|
+
console.log(` Components: ${report.totals.components}`);
|
|
32
|
+
console.log(` DisplayObjs: ${report.totals.displayObjects}`);
|
|
33
|
+
console.log(` Gears: ${report.totals.gears}`);
|
|
34
|
+
console.log(` Controllers: ${report.totals.controllers}`);
|
|
35
|
+
console.log(` Transitions: ${report.totals.transitions}`);
|
|
36
|
+
|
|
37
|
+
console.log('\nPackage details:');
|
|
38
|
+
for (const pkg of report.packages) {
|
|
39
|
+
const res = pkg.resources;
|
|
40
|
+
console.log(
|
|
41
|
+
` ${pkg.name} (${pkg.id}): ${res.images.count} img, ${res.sounds.count} snd, ${res.fonts.count} font, ${res.components.count} comp`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|