@openfairygui/cli 0.1.0 → 0.2.0-alpha.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.
Files changed (3) hide show
  1. package/dist/cli.mjs +3222 -2988
  2. package/package.json +4 -1
  3. package/src/cli.ts +216 -108
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openfairygui/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0-alpha.0",
4
4
  "description": "FairyGUI Headless Authoring SDK — command-line interface.",
5
5
  "author": "OpenFairyGUI Contributors",
6
6
  "license": "MIT",
@@ -39,6 +39,9 @@
39
39
  "@openfairygui/core": "workspace:*",
40
40
  "@openfairygui/functions": "workspace:*"
41
41
  },
42
+ "dependencies": {
43
+ "@openfairygui/backend": "workspace:*"
44
+ },
42
45
  "optionalDependencies": {
43
46
  "sharp": ">=0.33.0"
44
47
  }
package/src/cli.ts CHANGED
@@ -1,16 +1,25 @@
1
+ import { BackendRuntime } from '@openfairygui/backend';
1
2
  import {
2
3
  NodeIO,
3
4
  ProjectType,
5
+ } from '@openfairygui/core';
6
+ import {
7
+ inspect,
8
+ publish,
9
+ resolvePublishOptions,
10
+ restore,
11
+ type InspectReport,
12
+ type PublishOptions,
13
+ type RestoreFileSystem,
4
14
  type RestoreImageCropInput,
5
15
  type RestoreImageCropper,
6
16
  type RestoreImageExtractInput,
7
17
  type RestoreImageExtractor,
8
- } from '@openfairygui/core';
9
- import { inspect, publish, resolvePublishOptions, type InspectReport, type PublishOptions } from '@openfairygui/functions';
18
+ } from '@openfairygui/functions';
10
19
  import fs from 'node:fs/promises';
11
20
  import path from 'node:path';
12
21
  import { parseArgs } from 'node:util';
13
-
22
+
14
23
  const HELP = `
15
24
  ofgui — FairyGUI Headless Authoring CLI
16
25
 
@@ -21,6 +30,7 @@ Commands:
21
30
  inspect <project-dir> Show project contents report
22
31
  publish <project-dir> --output <dir> [options] Publish project to binary outputs and configured generated code
23
32
  restore <release-dir> --output <dir> [options] Restore a FairyGUI project from published binaries
33
+ backend-capabilities <project-dir> Open a backend session, print runtime capabilities, then close it
24
34
 
25
35
  Publish options:
26
36
  --output, -o <dir> Output directory (required)
@@ -36,69 +46,72 @@ Restore options:
36
46
  --project-type <name|id> Override restored project type; default is unity
37
47
 
38
48
  Options:
39
- --help, -h Show this help
40
- --version, -v Show version
41
-
42
- Input can be a .fairy file or a project root directory (auto-discovers .fairy file).
43
- File extension and binary format are read from project settings.
44
- `;
45
-
46
- /** Resolve input to a .fairy file path. Accepts a directory or a .fairy file. */
47
- async function resolveFairyPath(input: string): Promise<string> {
48
- const resolved = path.resolve(input);
49
- const stat = await fs.stat(resolved);
50
-
51
- if (stat.isFile() && resolved.endsWith('.fairy')) {
52
- return resolved;
53
- }
54
-
55
- if (stat.isDirectory()) {
56
- // Scan for *.fairy in the directory
57
- const entries = await fs.readdir(resolved);
58
- const fairyFiles = entries.filter((e) => e.endsWith('.fairy'));
59
- if (fairyFiles.length === 1) {
60
- return path.join(resolved, fairyFiles[0]);
61
- }
62
- if (fairyFiles.length > 1) {
63
- throw new Error(`Multiple .fairy files found in ${resolved}: ${fairyFiles.join(', ')}. Please specify one.`);
64
- }
65
- throw new Error(`No .fairy file found in ${resolved}`);
66
- }
67
-
68
- throw new Error(`Input is not a .fairy file or directory: ${resolved}`);
69
- }
70
-
49
+ --help, -h Show this help
50
+ --version, -v Show version
51
+
52
+ Input can be a .fairy file or a project root directory (auto-discovers .fairy file).
53
+ File extension and binary format are read from project settings.
54
+ `;
55
+
56
+ /** Resolve input to a .fairy file path. Accepts a directory or a .fairy file. */
57
+ async function resolveFairyPath(input: string): Promise<string> {
58
+ const resolved = path.resolve(input);
59
+ const stat = await fs.stat(resolved);
60
+
61
+ if (stat.isFile() && resolved.endsWith('.fairy')) {
62
+ return resolved;
63
+ }
64
+
65
+ if (stat.isDirectory()) {
66
+ // Scan for *.fairy in the directory
67
+ const entries = await fs.readdir(resolved);
68
+ const fairyFiles = entries.filter((e) => e.endsWith('.fairy'));
69
+ if (fairyFiles.length === 1) {
70
+ return path.join(resolved, fairyFiles[0]);
71
+ }
72
+ if (fairyFiles.length > 1) {
73
+ throw new Error(`Multiple .fairy files found in ${resolved}: ${fairyFiles.join(', ')}. Please specify one.`);
74
+ }
75
+ throw new Error(`No .fairy file found in ${resolved}`);
76
+ }
77
+
78
+ throw new Error(`Input is not a .fairy file or directory: ${resolved}`);
79
+ }
80
+
71
81
  async function main(): Promise<void> {
72
- const args = process.argv.slice(2);
73
-
74
- if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
75
- console.log(HELP);
76
- return;
77
- }
78
-
79
- if (args.includes('--version') || args.includes('-v')) {
80
- console.log('0.1.0');
81
- return;
82
- }
83
-
84
- const command = args[0];
85
- const rest = args.slice(1);
86
-
87
- switch (command) {
88
- case 'inspect':
89
- await cmdInspect(rest);
90
- break;
82
+ const args = process.argv.slice(2);
83
+
84
+ if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
85
+ console.log(HELP);
86
+ return;
87
+ }
88
+
89
+ if (args.includes('--version') || args.includes('-v')) {
90
+ console.log('0.1.0');
91
+ return;
92
+ }
93
+
94
+ const command = args[0];
95
+ const rest = args.slice(1);
96
+
97
+ switch (command) {
98
+ case 'inspect':
99
+ await cmdInspect(rest);
100
+ break;
91
101
  case 'publish':
92
102
  await cmdPublish(rest);
93
103
  break;
94
104
  case 'restore':
95
105
  await cmdRestore(rest);
96
106
  break;
107
+ case 'backend-capabilities':
108
+ await cmdBackendCapabilities(rest);
109
+ break;
97
110
  default:
98
- console.error(`Unknown command: ${command}\n`);
99
- console.log(HELP);
100
- process.exit(1);
101
- }
111
+ console.error(`Unknown command: ${command}\n`);
112
+ console.log(HELP);
113
+ process.exit(1);
114
+ }
102
115
  }
103
116
 
104
117
  interface RestoreImageProcessors {
@@ -106,6 +119,63 @@ interface RestoreImageProcessors {
106
119
  extractImage: RestoreImageExtractor;
107
120
  }
108
121
 
122
+ function createNodeRestoreFs(): RestoreFileSystem {
123
+ return {
124
+ async readFile(filePath: string): Promise<string> {
125
+ return fs.readFile(filePath, 'utf-8');
126
+ },
127
+ async readFileRaw(filePath: string): Promise<Uint8Array> {
128
+ const buf = await fs.readFile(filePath);
129
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
130
+ },
131
+ async writeFile(filePath: string, content: string): Promise<void> {
132
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
133
+ await fs.writeFile(filePath, content, 'utf-8');
134
+ },
135
+ async writeFileRaw(filePath: string, data: Uint8Array): Promise<void> {
136
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
137
+ await fs.writeFile(filePath, data);
138
+ },
139
+ async mkdir(dirPath: string): Promise<void> {
140
+ await fs.mkdir(dirPath, { recursive: true });
141
+ },
142
+ async readdir(dirPath: string): Promise<string[]> {
143
+ return fs.readdir(dirPath);
144
+ },
145
+ async exists(filePath: string): Promise<boolean> {
146
+ try {
147
+ await fs.access(filePath);
148
+ return true;
149
+ } catch {
150
+ return false;
151
+ }
152
+ },
153
+ async isFile(filePath: string): Promise<boolean> {
154
+ try {
155
+ return (await fs.stat(filePath)).isFile();
156
+ } catch {
157
+ return false;
158
+ }
159
+ },
160
+ async resolvePath(filePath: string): Promise<string> {
161
+ try {
162
+ return await fs.realpath(filePath);
163
+ } catch {
164
+ return path.resolve(filePath);
165
+ }
166
+ },
167
+ async rm(targetPath: string, options?: { recursive?: boolean; force?: boolean }): Promise<void> {
168
+ await fs.rm(targetPath, { recursive: options?.recursive ?? false, force: options?.force ?? false });
169
+ },
170
+ join(...paths: string[]): string {
171
+ return path.join(...paths);
172
+ },
173
+ dirname(filePath: string): string {
174
+ return path.dirname(filePath);
175
+ },
176
+ };
177
+ }
178
+
109
179
  async function createRestoreImageProcessors(): Promise<RestoreImageProcessors> {
110
180
  let sharp: any;
111
181
  try {
@@ -140,7 +210,7 @@ async function createRestoreImageProcessors(): Promise<RestoreImageProcessors> {
140
210
  || input.offsetY + info.height > input.expectedHeight
141
211
  ) {
142
212
  throw new Error(
143
- `restore: Cropped image does not fit original canvas for ${input.outputPath}: `
213
+ `restore: Cropped image does not fit original canvas for ${targetPath}: `
144
214
  + `crop ${info.width}x${info.height} at ${input.offsetX},${input.offsetY}, `
145
215
  + `canvas ${input.expectedWidth}x${input.expectedHeight}`,
146
216
  );
@@ -190,41 +260,41 @@ async function createRestoreImageProcessors(): Promise<RestoreImageProcessors> {
190
260
  },
191
261
  };
192
262
  }
193
-
263
+
194
264
  async function cmdInspect(args: string[]): Promise<void> {
195
265
  if (args.length === 0) {
196
266
  console.error('Usage: ofgui inspect <project-dir>');
197
267
  process.exit(1);
198
268
  }
199
-
200
- const fairyPath = await resolveFairyPath(args[0]);
201
- console.log(`Project: ${fairyPath}\n`);
202
-
203
- const io = new NodeIO();
204
- const doc = await io.readProject(fairyPath);
205
- const report = inspect(doc);
206
-
207
- printReport(report);
208
- }
209
-
210
- function printReport(report: InspectReport): void {
211
- console.log(`ID: ${report.projectId}`);
212
- console.log(`Type: ${report.projectType}, Version: ${report.version}`);
213
- console.log(`\nPackages: ${report.totals.packages}`);
214
- console.log(` Images: ${report.totals.images}`);
215
- console.log(` Sounds: ${report.totals.sounds}`);
216
- console.log(` Fonts: ${report.totals.fonts}`);
217
- console.log(` MovieClips: ${report.totals.movieClips}`);
218
- console.log(` Components: ${report.totals.components}`);
219
- console.log(` DisplayObjs: ${report.totals.displayObjects}`);
220
- console.log(` Gears: ${report.totals.gears}`);
221
- console.log(` Controllers: ${report.totals.controllers}`);
222
- console.log(` Transitions: ${report.totals.transitions}`);
223
-
224
- console.log('\nPackage details:');
225
- for (const pkg of report.packages) {
226
- const res = pkg.resources;
227
- console.log(` ${pkg.name} (${pkg.id}): ${res.images.count} img, ${res.sounds.count} snd, ${res.fonts.count} font, ${res.components.count} comp`);
269
+
270
+ const fairyPath = await resolveFairyPath(args[0]);
271
+ console.log(`Project: ${fairyPath}\n`);
272
+
273
+ const io = new NodeIO();
274
+ const doc = await io.readProject(fairyPath);
275
+ const report = inspect(doc);
276
+
277
+ printReport(report);
278
+ }
279
+
280
+ function printReport(report: InspectReport): void {
281
+ console.log(`ID: ${report.projectId}`);
282
+ console.log(`Type: ${report.projectType}, Version: ${report.version}`);
283
+ console.log(`\nPackages: ${report.totals.packages}`);
284
+ console.log(` Images: ${report.totals.images}`);
285
+ console.log(` Sounds: ${report.totals.sounds}`);
286
+ console.log(` Fonts: ${report.totals.fonts}`);
287
+ console.log(` MovieClips: ${report.totals.movieClips}`);
288
+ console.log(` Components: ${report.totals.components}`);
289
+ console.log(` DisplayObjs: ${report.totals.displayObjects}`);
290
+ console.log(` Gears: ${report.totals.gears}`);
291
+ console.log(` Controllers: ${report.totals.controllers}`);
292
+ console.log(` Transitions: ${report.totals.transitions}`);
293
+
294
+ console.log('\nPackage details:');
295
+ for (const pkg of report.packages) {
296
+ const res = pkg.resources;
297
+ console.log(` ${pkg.name} (${pkg.id}): ${res.images.count} img, ${res.sounds.count} snd, ${res.fonts.count} font, ${res.components.count} comp`);
228
298
  }
229
299
  }
230
300
 
@@ -282,8 +352,10 @@ async function cmdRestore(args: string[]): Promise<void> {
282
352
  const { cropImage, extractImage } = await createRestoreImageProcessors();
283
353
 
284
354
  console.log(`Restoring published FairyGUI project: ${releaseDir}`);
285
- const io = new NodeIO();
286
- const result = await io.restorePublishedProject(releaseDir, outputDir, {
355
+ const result = await restore({
356
+ inputDir: releaseDir,
357
+ output: outputDir,
358
+ fs: createNodeRestoreFs(),
287
359
  packages: pkgFilter,
288
360
  force: values.force,
289
361
  projectType,
@@ -311,16 +383,16 @@ async function cmdPublish(args: string[]): Promise<void> {
311
383
  },
312
384
  allowPositionals: true,
313
385
  });
314
-
386
+
315
387
  if (positionals.length === 0 || !values.output) {
316
388
  console.error('Usage: ofgui publish <project-dir> --output <dir> [--compressed] [--packages a,b,c] [--branch name]');
317
389
  process.exit(1);
318
390
  }
319
-
320
- const fairyPath = await resolveFairyPath(positionals[0]);
321
- const projectDir = path.dirname(fairyPath);
322
- const outputDir = path.resolve(values.output);
323
-
391
+
392
+ const fairyPath = await resolveFairyPath(positionals[0]);
393
+ const projectDir = path.dirname(fairyPath);
394
+ const outputDir = path.resolve(values.output);
395
+
324
396
  console.log(`Reading project: ${fairyPath}`);
325
397
  const io = new NodeIO();
326
398
  const doc = await io.readProject(fairyPath);
@@ -353,7 +425,7 @@ async function cmdPublish(args: string[]): Promise<void> {
353
425
  try {
354
426
  const sharp = await import('sharp');
355
427
  encoder = sharp.default ?? sharp;
356
- console.log('Sharp loaded — atlas PNGs will be generated.');
428
+ console.log('Sharp loaded — atlas PNGs will be generated.');
357
429
  } catch {
358
430
  console.log('Sharp not available — atlas PNGs will NOT be generated (layout only).');
359
431
  console.log(' Install sharp to enable: pnpm add sharp');
@@ -367,7 +439,7 @@ async function cmdPublish(args: string[]): Promise<void> {
367
439
  async writeFileRaw(filePath: string, data: Uint8Array): Promise<void> {
368
440
  await fs.mkdir(path.dirname(filePath), { recursive: true });
369
441
  await fs.writeFile(filePath, data);
370
- },
442
+ },
371
443
  async mkdir(dirPath: string): Promise<void> {
372
444
  await fs.mkdir(dirPath, { recursive: true });
373
445
  },
@@ -381,7 +453,7 @@ async function cmdPublish(args: string[]): Promise<void> {
381
453
  return path.join(...paths);
382
454
  },
383
455
  };
384
-
456
+
385
457
  await doc.transform(publish({
386
458
  output: outputDir,
387
459
  compressed: resolved.compressed,
@@ -393,11 +465,47 @@ async function cmdPublish(args: string[]): Promise<void> {
393
465
  atlas: atlasConfig,
394
466
  branch: values.branch,
395
467
  }));
396
-
397
- console.log(`\nDone! Output: ${outputDir}`);
398
- }
399
-
400
- main().catch((err) => {
401
- console.error(err);
402
- process.exit(1);
403
- });
468
+
469
+ console.log(`\nDone! Output: ${outputDir}`);
470
+ }
471
+
472
+ async function cmdBackendCapabilities(args: string[]): Promise<void> {
473
+ if (args.length === 0) {
474
+ console.error('Usage: ofgui backend-capabilities <project-dir>');
475
+ process.exit(1);
476
+ }
477
+
478
+ const runtime = new BackendRuntime();
479
+ const opened = await runtime.openSession({ projectPath: path.resolve(args[0]) });
480
+ if (!opened.ok) {
481
+ const failure = opened as Extract<typeof opened, { ok: false }>;
482
+ console.error(`backend-capabilities: ${failure.error.message}`);
483
+ process.exit(1);
484
+ }
485
+
486
+ const capabilities = runtime.getCapabilities();
487
+ if (!capabilities.ok) {
488
+ console.error('backend-capabilities: failed to read capabilities');
489
+ await runtime.closeSession({ sessionId: opened.data.sessionId });
490
+ process.exit(1);
491
+ }
492
+
493
+ console.log(`Session: ${opened.data.sessionId}`);
494
+ console.log(`Project: ${opened.data.canonicalProjectPath}`);
495
+ console.log(`Revision: ${opened.data.revision}`);
496
+ console.log(`Runtime owner: ${capabilities.data.runtimeOwner}`);
497
+ console.log(`Transaction owner: ${capabilities.data.transactionKernelOwner}`);
498
+ console.log(`App seam owner: ${capabilities.data.appSeamOwner}`);
499
+
500
+ const closed = await runtime.closeSession({ sessionId: opened.data.sessionId });
501
+ if (!closed.ok) {
502
+ const failure = closed as Extract<typeof closed, { ok: false }>;
503
+ console.error(`backend-capabilities: ${failure.error.message}`);
504
+ process.exit(1);
505
+ }
506
+ }
507
+
508
+ main().catch((err) => {
509
+ console.error(err);
510
+ process.exit(1);
511
+ });