@openfairygui/cli 0.2.0-alpha.8 → 0.2.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openfairygui/cli",
3
- "version": "0.2.0-alpha.8",
3
+ "version": "0.2.0",
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.0-alpha.8",
37
- "@openfairygui/functions": "0.2.0-alpha.8"
36
+ "@openfairygui/core": "0.2.0",
37
+ "@openfairygui/functions": "0.2.0"
38
38
  },
39
39
  "dependencies": {
40
- "@openfairygui/backend": "0.2.0-alpha.8"
40
+ "commander": "^14.0.2",
41
+ "jiti": "^2.7.0",
42
+ "@openfairygui/backend": "0.2.0"
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 --env.PACKAGE_VERSION=$npm_package_version"
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 { createNodeBackendRuntime } from '@openfairygui/backend/node';
2
- import { ProjectType } from '@openfairygui/core';
3
- import { NodeIO } from '@openfairygui/core/node';
4
- import {
5
- inspect,
6
- publish,
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
- /** Resolve input to a .fairy file path. Accepts a directory or a .fairy file. */
79
- async function resolveFairyPath(input: string): Promise<string> {
80
- const resolved = path.resolve(input);
81
- const stat = await fs.stat(resolved);
82
-
83
- if (stat.isFile() && resolved.endsWith('.fairy')) {
84
- return resolved;
85
- }
86
-
87
- if (stat.isDirectory()) {
88
- // Scan for *.fairy in the directory
89
- const entries = await fs.readdir(resolved);
90
- const fairyFiles = entries.filter((e) => e.endsWith('.fairy'));
91
- if (fairyFiles.length === 1) {
92
- return path.join(resolved, fairyFiles[0]);
93
- }
94
- if (fairyFiles.length > 1) {
95
- throw new Error(
96
- `Multiple .fairy files found in ${resolved}: ${fairyFiles.join(', ')}. Please specify one.`,
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
- console.log(`\nDone! Output: ${outputDir}`);
32
+ return program;
506
33
  }
507
34
 
508
- async function cmdBackendCapabilities(args: string[]): Promise<void> {
509
- if (args.length === 0) {
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
+ }