@openfairygui/cli 0.2.0-alpha.1 → 0.2.0-alpha.11

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.
@@ -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
+ }
@@ -0,0 +1,108 @@
1
+ import type { Command } from 'commander';
2
+ import { NodeIO } from '@openfairygui/core/node';
3
+ import { publish, resolvePublishOptions, type PublishOptions } from '@openfairygui/functions';
4
+ import fs from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ import { resolveFairyPath } from '../utils/project-input.js';
7
+ import { parseProjectType } from '../utils/project-type.js';
8
+
9
+ type PublishCommandOptions = {
10
+ output?: string;
11
+ compressed?: boolean;
12
+ packages?: string;
13
+ branch?: string;
14
+ projectType?: string;
15
+ };
16
+
17
+ export function registerPublishCommand(program: Command): void {
18
+ program
19
+ .command('publish')
20
+ .description('Publish project to binary outputs and configured generated code')
21
+ .argument('<project-dir>', 'Project root directory or .fairy file')
22
+ .option('-o, --output <dir>', 'Override project or package publish output directory')
23
+ .option('-c, --compressed', 'Compress binary data (overrides project setting)')
24
+ .option('-p, --packages <a,b,c>', 'Only publish specific packages (comma-separated)')
25
+ .option('-b, --branch <name>', 'Active branch used by "主干合并活跃分支"; omit for main branch')
26
+ .option('-t, --project-type <name|id>', 'Override project type (for example: unity, layabox, cocoscreator, 0, 4, 3)')
27
+ .action(async (projectDir: string, options: PublishCommandOptions) => {
28
+ const fairyPath = await resolveFairyPath(projectDir);
29
+ const projectRootDir = path.dirname(fairyPath);
30
+ const outputDir = options.output ? path.resolve(options.output) : undefined;
31
+
32
+ console.log(`Reading project: ${fairyPath}`);
33
+ const io = new NodeIO();
34
+ const doc = await io.readProject(fairyPath);
35
+ const projectType = parseProjectType(options.projectType);
36
+ if (projectType !== undefined) {
37
+ doc.getRoot().setProjectType(projectType);
38
+ }
39
+
40
+ const pkgFilter = options.packages?.split(',').map((value) => value.trim());
41
+ const resolved = resolvePublishOptions(doc, {
42
+ compressed: options.compressed,
43
+ packages: pkgFilter,
44
+ });
45
+
46
+ console.log(`Settings: ext=${resolved.fileExtension}, compressed=${resolved.compressed}`);
47
+ if (options.branch) {
48
+ console.log(`Active branch: ${options.branch}`);
49
+ }
50
+
51
+ const atlasConfig: NonNullable<PublishOptions['atlas']> = {
52
+ ...resolved.atlas,
53
+ readFileRaw: async (filePath: string) => {
54
+ const buf = await fs.readFile(filePath);
55
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
56
+ },
57
+ };
58
+
59
+ let encoder: PublishOptions['encoder'];
60
+ try {
61
+ const sharp = await import('sharp');
62
+ encoder = sharp.default ?? sharp;
63
+ console.log('Sharp loaded — atlas PNGs will be generated.');
64
+ } catch {
65
+ console.log('Sharp not available — atlas PNGs will NOT be generated (layout only).');
66
+ console.log(' Install sharp to enable: pnpm add sharp');
67
+ }
68
+
69
+ const publishFs: NonNullable<PublishOptions['fs']> = {
70
+ async readFileRaw(filePath: string): Promise<Uint8Array> {
71
+ const buf = await fs.readFile(filePath);
72
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
73
+ },
74
+ async writeFileRaw(filePath: string, data: Uint8Array): Promise<void> {
75
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
76
+ await fs.writeFile(filePath, data);
77
+ },
78
+ async mkdir(dirPath: string): Promise<void> {
79
+ await fs.mkdir(dirPath, { recursive: true });
80
+ },
81
+ async readdir(dirPath: string): Promise<string[]> {
82
+ return fs.readdir(dirPath);
83
+ },
84
+ async deleteFile(filePath: string): Promise<void> {
85
+ await fs.rm(filePath, { force: true });
86
+ },
87
+ join(...paths: string[]): string {
88
+ return path.join(...paths);
89
+ },
90
+ };
91
+
92
+ await doc.transform(
93
+ publish({
94
+ output: outputDir,
95
+ compressed: resolved.compressed,
96
+ fileExtension: resolved.fileExtension,
97
+ packages: resolved.packages,
98
+ fs: publishFs,
99
+ encoder,
100
+ basePath: path.join(projectRootDir, 'assets'),
101
+ atlas: atlasConfig,
102
+ branch: options.branch,
103
+ }),
104
+ );
105
+
106
+ console.log(`\nDone!${outputDir ? ` Output override: ${outputDir}` : ''}`);
107
+ });
108
+ }
@@ -0,0 +1,209 @@
1
+ import type { Command } from 'commander';
2
+ import {
3
+ restore,
4
+ type RestoreFileSystem,
5
+ type RestoreImageCropInput,
6
+ type RestoreImageCropper,
7
+ type RestoreImageExtractInput,
8
+ type RestoreImageExtractor,
9
+ } from '@openfairygui/functions';
10
+ import fs from 'node:fs/promises';
11
+ import path from 'node:path';
12
+ import { parseProjectType } from '../utils/project-type.js';
13
+
14
+ type RestoreCommandOptions = {
15
+ output: string;
16
+ packages?: string;
17
+ force?: boolean;
18
+ projectType?: string;
19
+ };
20
+
21
+ interface RestoreImageProcessors {
22
+ cropImage: RestoreImageCropper;
23
+ extractImage: RestoreImageExtractor;
24
+ }
25
+
26
+ export function registerRestoreCommand(program: Command): void {
27
+ program
28
+ .command('restore')
29
+ .description('Restore a FairyGUI project from published binaries')
30
+ .argument('<release-dir>', 'Published release directory')
31
+ .requiredOption('-o, --output <dir>', 'Output project directory')
32
+ .option('-p, --packages <a,b,c>', 'Only restore specific packages (comma-separated)')
33
+ .option('-f, --force', 'Overwrite a non-empty output directory')
34
+ .option('-t, --project-type <name|id>', 'Override restored project type; default is unity')
35
+ .action(async (releaseDir: string, options: RestoreCommandOptions) => {
36
+ const inputDir = path.resolve(releaseDir);
37
+ const outputDir = path.resolve(options.output);
38
+ const pkgFilter = options.packages
39
+ ? options.packages
40
+ .split(',')
41
+ .map((value) => value.trim())
42
+ .filter(Boolean)
43
+ : undefined;
44
+ const projectType = parseProjectType(options.projectType);
45
+ const { cropImage, extractImage } = await createRestoreImageProcessors();
46
+
47
+ console.log(`Restoring published FairyGUI project: ${inputDir}`);
48
+ const result = await restore({
49
+ inputDir,
50
+ output: outputDir,
51
+ fs: createNodeRestoreFs(),
52
+ packages: pkgFilter,
53
+ force: options.force,
54
+ projectType,
55
+ cropImage,
56
+ extractImage,
57
+ });
58
+
59
+ const packages = result.document.getRoot().listPackages();
60
+ console.log(`\nDone! Output: ${result.projectPath}`);
61
+ console.log(`Packages: ${packages.map((pkg) => pkg.getName()).join(', ')}`);
62
+ for (const warning of result.warnings) {
63
+ console.warn(`Warning: ${warning}`);
64
+ }
65
+ });
66
+ }
67
+
68
+ function createNodeRestoreFs(): RestoreFileSystem {
69
+ return {
70
+ async readFile(filePath: string): Promise<string> {
71
+ return fs.readFile(filePath, 'utf-8');
72
+ },
73
+ async readFileRaw(filePath: string): Promise<Uint8Array> {
74
+ const buf = await fs.readFile(filePath);
75
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
76
+ },
77
+ async writeFile(filePath: string, content: string): Promise<void> {
78
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
79
+ await fs.writeFile(filePath, content, 'utf-8');
80
+ },
81
+ async writeFileRaw(filePath: string, data: Uint8Array): Promise<void> {
82
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
83
+ await fs.writeFile(filePath, data);
84
+ },
85
+ async mkdir(dirPath: string): Promise<void> {
86
+ await fs.mkdir(dirPath, { recursive: true });
87
+ },
88
+ async readdir(dirPath: string): Promise<string[]> {
89
+ return fs.readdir(dirPath);
90
+ },
91
+ async exists(filePath: string): Promise<boolean> {
92
+ try {
93
+ await fs.access(filePath);
94
+ return true;
95
+ } catch {
96
+ return false;
97
+ }
98
+ },
99
+ async isFile(filePath: string): Promise<boolean> {
100
+ try {
101
+ return (await fs.stat(filePath)).isFile();
102
+ } catch {
103
+ return false;
104
+ }
105
+ },
106
+ async resolvePath(filePath: string): Promise<string> {
107
+ try {
108
+ return await fs.realpath(filePath);
109
+ } catch {
110
+ return path.resolve(filePath);
111
+ }
112
+ },
113
+ async rm(targetPath: string, options?: { recursive?: boolean; force?: boolean }): Promise<void> {
114
+ await fs.rm(targetPath, { recursive: options?.recursive ?? false, force: options?.force ?? false });
115
+ },
116
+ join(...paths: string[]): string {
117
+ return path.join(...paths);
118
+ },
119
+ dirname(filePath: string): string {
120
+ return path.dirname(filePath);
121
+ },
122
+ };
123
+ }
124
+
125
+ async function createRestoreImageProcessors(): Promise<RestoreImageProcessors> {
126
+ let sharp: any;
127
+ try {
128
+ const mod = await import('sharp');
129
+ sharp = mod.default ?? mod;
130
+ } catch {
131
+ throw new Error('restore: sharp is required to crop atlas images. Install it with: pnpm add sharp');
132
+ }
133
+
134
+ async function extractImage(input: RestoreImageExtractInput): Promise<Uint8Array> {
135
+ const targetPath = (input as RestoreImageCropInput).outputPath ?? input.sourcePath;
136
+ let image = sharp(input.sourcePath).extract({
137
+ left: input.left,
138
+ top: input.top,
139
+ width: input.width,
140
+ height: input.height,
141
+ });
142
+ if (input.rotated) image = image.rotate(90);
143
+ const { data, info } = await image.png().toBuffer({ resolveWithObject: true });
144
+ const needsOriginalCanvas =
145
+ input.expectedWidth > 0 &&
146
+ input.expectedHeight > 0 &&
147
+ (input.offsetX !== 0 ||
148
+ input.offsetY !== 0 ||
149
+ info.width !== input.expectedWidth ||
150
+ info.height !== input.expectedHeight);
151
+
152
+ if (needsOriginalCanvas) {
153
+ if (
154
+ input.offsetX < 0 ||
155
+ input.offsetY < 0 ||
156
+ input.offsetX + info.width > input.expectedWidth ||
157
+ input.offsetY + info.height > input.expectedHeight
158
+ ) {
159
+ throw new Error(
160
+ `restore: Cropped image does not fit original canvas for ${targetPath}: ` +
161
+ `crop ${info.width}x${info.height} at ${input.offsetX},${input.offsetY}, ` +
162
+ `canvas ${input.expectedWidth}x${input.expectedHeight}`,
163
+ );
164
+ }
165
+ const composed = await sharp({
166
+ create: {
167
+ width: input.expectedWidth,
168
+ height: input.expectedHeight,
169
+ channels: 4,
170
+ background: { r: 0, g: 0, b: 0, alpha: 0 },
171
+ },
172
+ })
173
+ .composite([{ input: data, left: input.offsetX, top: input.offsetY }])
174
+ .png()
175
+ .toBuffer({ resolveWithObject: true });
176
+ if (
177
+ input.expectedWidth > 0 &&
178
+ input.expectedHeight > 0 &&
179
+ (composed.info.width !== input.expectedWidth || composed.info.height !== input.expectedHeight)
180
+ ) {
181
+ throw new Error(
182
+ `restore: Cropped image size mismatch for ${targetPath}: ` +
183
+ `expected ${input.expectedWidth}x${input.expectedHeight}, got ${composed.info.width}x${composed.info.height}`,
184
+ );
185
+ }
186
+ return composed.data;
187
+ }
188
+
189
+ if (
190
+ input.expectedWidth > 0 &&
191
+ input.expectedHeight > 0 &&
192
+ (info.width !== input.expectedWidth || info.height !== input.expectedHeight)
193
+ ) {
194
+ throw new Error(
195
+ `restore: Cropped image size mismatch for ${targetPath}: ` +
196
+ `expected ${input.expectedWidth}x${input.expectedHeight}, got ${info.width}x${info.height}`,
197
+ );
198
+ }
199
+ return data;
200
+ }
201
+
202
+ return {
203
+ extractImage,
204
+ cropImage: async (input: RestoreImageCropInput): Promise<void> => {
205
+ await fs.mkdir(path.dirname(input.outputPath), { recursive: true });
206
+ await fs.writeFile(input.outputPath, await extractImage(input));
207
+ },
208
+ };
209
+ }
@@ -0,0 +1,22 @@
1
+ import { createRequire } from 'node:module';
2
+
3
+ const require = createRequire(import.meta.url);
4
+
5
+ function getInjectedPackageVersion(): string | null {
6
+ const version = (import.meta as ImportMeta & { env?: { PACKAGE_VERSION?: string } }).env?.PACKAGE_VERSION;
7
+ return typeof version === 'string' && version.length > 0 ? version : null;
8
+ }
9
+
10
+ export function readPackageVersion(): string {
11
+ const injectedVersion = getInjectedPackageVersion();
12
+ if (injectedVersion) return injectedVersion;
13
+ try {
14
+ const pkg = require('../../package.json') as { version?: unknown };
15
+ if (typeof pkg.version === 'string' && pkg.version.length > 0) {
16
+ return pkg.version;
17
+ }
18
+ } catch {
19
+ // Keep the CLI usable when executed from a bundled artifact missing package.json.
20
+ }
21
+ return '0.0.0-dev';
22
+ }
@@ -0,0 +1,26 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ /** Resolve input to a .fairy file path. Accepts a directory or a .fairy file. */
5
+ export async function resolveFairyPath(input: string): Promise<string> {
6
+ const resolved = path.resolve(input);
7
+ const stat = await fs.stat(resolved);
8
+
9
+ if (stat.isFile() && resolved.endsWith('.fairy')) {
10
+ return resolved;
11
+ }
12
+
13
+ if (stat.isDirectory()) {
14
+ const entries = await fs.readdir(resolved);
15
+ const fairyFiles = entries.filter((entry) => entry.endsWith('.fairy'));
16
+ if (fairyFiles.length === 1) {
17
+ return path.join(resolved, fairyFiles[0]);
18
+ }
19
+ if (fairyFiles.length > 1) {
20
+ throw new Error(`Multiple .fairy files found in ${resolved}: ${fairyFiles.join(', ')}. Please specify one.`);
21
+ }
22
+ throw new Error(`No .fairy file found in ${resolved}`);
23
+ }
24
+
25
+ throw new Error(`Input is not a .fairy file or directory: ${resolved}`);
26
+ }
@@ -0,0 +1,31 @@
1
+ import { ProjectType } from '@openfairygui/core';
2
+
3
+ export function parseProjectType(value: string | undefined): number | undefined {
4
+ if (!value) return undefined;
5
+ const trimmed = value.trim();
6
+ if (trimmed === '') return undefined;
7
+ if (/^\d+$/u.test(trimmed)) return Number(trimmed);
8
+ const normalized = trimmed.toLowerCase();
9
+ const map: Record<string, number> = {
10
+ unity: ProjectType.Unity,
11
+ flash: ProjectType.Flash,
12
+ starling: ProjectType.Starling,
13
+ cocoscreator: ProjectType.CocosCreator,
14
+ cocos: ProjectType.CocosCreator,
15
+ layabox: ProjectType.LayaBox,
16
+ laya: ProjectType.LayaBox,
17
+ egret: ProjectType.Egret,
18
+ haxe: ProjectType.Haxe,
19
+ pixi: ProjectType.Pixi,
20
+ libgdx: ProjectType.LibGDX,
21
+ unreal: ProjectType.Unreal,
22
+ cryengine: ProjectType.CryEngine,
23
+ monogame: ProjectType.MonoGame,
24
+ vision: ProjectType.Vision,
25
+ };
26
+ const resolved = map[normalized];
27
+ if (resolved === undefined) {
28
+ throw new Error(`Unknown project type: ${value}. Use a numeric id or one of: ${Object.keys(map).join(', ')}`);
29
+ }
30
+ return resolved;
31
+ }