@openfairygui/cli 0.2.0-alpha.14 → 0.2.0-alpha.16
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 +3 -0
- package/dist/cli.mjs +233 -97
- package/package.json +4 -4
- package/src/commands/restore.ts +9 -6
package/README.md
CHANGED
|
@@ -14,9 +14,12 @@ npm install --global @openfairygui/cli
|
|
|
14
14
|
ofgui --help
|
|
15
15
|
ofgui inspect ./MyProject
|
|
16
16
|
ofgui publish ./MyProject --output ./release
|
|
17
|
+
# Trusted-local recovery only; this is not a normal authoring workflow.
|
|
17
18
|
ofgui restore ./release --output ./restored-project
|
|
18
19
|
```
|
|
19
20
|
|
|
21
|
+
`restore` accepts a publish directory and writes a new project directory. It validates artifact paths and completes a staged write before `--force` replaces an existing output; it does not make untrusted artifacts safe or recover the original source project.
|
|
22
|
+
|
|
20
23
|
The package also keeps `openfairygui` as a compatibility alias for the CLI command.
|
|
21
24
|
|
|
22
25
|
Repository:
|
package/dist/cli.mjs
CHANGED
|
@@ -12582,16 +12582,26 @@ var ProjectWriter = class {
|
|
|
12582
12582
|
return attrs;
|
|
12583
12583
|
});
|
|
12584
12584
|
if (publishAtlases.length > 0) publishAttrs.atlas = publishAtlases;
|
|
12585
|
-
|
|
12586
|
-
|
|
12585
|
+
const packageDescriptorPath = fs.join(pkgDir, "package.xml");
|
|
12586
|
+
await fs.writeFile(packageDescriptorPath, this._renderPackageDescriptionXml(packageDescriptionAttrs, mainResources, publishAttrs));
|
|
12587
|
+
currentSourceFilePaths.add(packageDescriptorPath);
|
|
12588
|
+
for (const comp of mainResources.filter((resource) => resource.propertyType === "Component")) {
|
|
12589
|
+
currentSourceFilePaths.add(fs.join(pkgDir, this._componentSourceRelativePath(comp)));
|
|
12590
|
+
await this._writeComponent(comp, pkgDir);
|
|
12591
|
+
}
|
|
12587
12592
|
await this._writeResourceSourceFiles(mainResources, pkgDir, currentSourceFilePaths);
|
|
12588
12593
|
for (const [branchName, branchResources] of resourcesByBranch) {
|
|
12589
12594
|
if (!branchName) continue;
|
|
12590
12595
|
this._assertSafePathSegment(branchName, "branch name");
|
|
12591
12596
|
const branchPkgDir = fs.join(basePath, `assets_${branchName}`, pkg.getName());
|
|
12592
12597
|
await fs.mkdir(branchPkgDir);
|
|
12593
|
-
|
|
12594
|
-
|
|
12598
|
+
const branchDescriptorPath = fs.join(branchPkgDir, "package_branch.xml");
|
|
12599
|
+
await fs.writeFile(branchDescriptorPath, this._renderBranchDescriptionXml(branchResources));
|
|
12600
|
+
currentSourceFilePaths.add(branchDescriptorPath);
|
|
12601
|
+
for (const comp of branchResources.filter((resource) => resource.propertyType === "Component")) {
|
|
12602
|
+
currentSourceFilePaths.add(fs.join(branchPkgDir, this._componentSourceRelativePath(comp)));
|
|
12603
|
+
await this._writeComponent(comp, branchPkgDir);
|
|
12604
|
+
}
|
|
12595
12605
|
await this._writeResourceSourceFiles(branchResources, branchPkgDir, currentSourceFilePaths);
|
|
12596
12606
|
}
|
|
12597
12607
|
}
|
|
@@ -21921,7 +21931,8 @@ const ATLAS_DEFAULTS = {
|
|
|
21921
21931
|
preserveInputOrderOnTie: false,
|
|
21922
21932
|
directSingleImageOutput: false,
|
|
21923
21933
|
extractAlpha: false,
|
|
21924
|
-
separatedAtlasForBranch: false
|
|
21934
|
+
separatedAtlasForBranch: false,
|
|
21935
|
+
strictOutput: false
|
|
21925
21936
|
};
|
|
21926
21937
|
function getPublishedItemId(resource) {
|
|
21927
21938
|
return (resource.getExtras() ?? {})._publishedId ?? resource.getId();
|
|
@@ -22078,8 +22089,9 @@ function atlas(_options = {}) {
|
|
|
22078
22089
|
const packageFilter = options.packages ? new Set(options.packages) : null;
|
|
22079
22090
|
for (const pkg of root.listPackages()) {
|
|
22080
22091
|
if (packageFilter && !packageFilter.has(pkg.getName())) continue;
|
|
22081
|
-
const
|
|
22082
|
-
const
|
|
22092
|
+
const publishedResourceIds = pkg.getExtras()?.publishedResourceIds;
|
|
22093
|
+
const selectedPublishIds = new Set(publishedResourceIds);
|
|
22094
|
+
const allResources = publishedResourceIds !== void 0 && (options.strictOutput || selectedPublishIds.size > 0) ? pkg.listResources().filter((resource) => selectedPublishIds.has(resource.getId())) : pkg.listResources();
|
|
22083
22095
|
const skeletonDependencyImageIds = getSelectedSkeletonDependencyImageIds(allResources);
|
|
22084
22096
|
const orderedResources = await resolveEditorCompatibleResourceOrder(pkg, allResources, options);
|
|
22085
22097
|
const orderedAllResources = sortResourcesByOrder(allResources, new Map(orderedResources.map((resource, index) => [resource.getId(), index])), new Map(allResources.map((resource, index) => [resource.getId(), index])));
|
|
@@ -22168,6 +22180,7 @@ function atlas(_options = {}) {
|
|
|
22168
22180
|
await _collectFontTexture(doc, res, pkg, options);
|
|
22169
22181
|
}
|
|
22170
22182
|
if (inputs.length === 0) continue;
|
|
22183
|
+
if (options.strictOutput && (!encoder || !options.basePath || !options.outputPath)) throw new Error(`atlas: Package "${pkg.getName()}" requires encoder, basePath, and outputPath for complete raster output.`);
|
|
22171
22184
|
let totalPageCount = 0;
|
|
22172
22185
|
let usedDirectOutput = false;
|
|
22173
22186
|
const { autoInputs, fixedPageGroups, standaloneGroups, reservedPageIndexes } = groupStandaloneInputs(doc, inputs, options);
|
|
@@ -22265,7 +22278,7 @@ function reserveAutoPageStart(branchPageOffsets, branchOrdinal, reservedPageInde
|
|
|
22265
22278
|
async function emitPagedAtlasGroup(doc, pkg, allResources, inputs, context) {
|
|
22266
22279
|
if (inputs.length === 0) return 0;
|
|
22267
22280
|
const pages = packAtlasPages(inputs, context.options, context.forceSinglePage === true);
|
|
22268
|
-
|
|
22281
|
+
assertPackedInputCoverage(pages, inputs.length, `package "${pkg.getName()}"`);
|
|
22269
22282
|
for (let pageOffset = 0; pageOffset < pages.length; pageOffset += 1) {
|
|
22270
22283
|
const page = pages[pageOffset];
|
|
22271
22284
|
const pageIndex = context.pageStart + pageOffset;
|
|
@@ -22291,7 +22304,7 @@ async function emitStandaloneAtlasGroup(doc, pkg, group, context) {
|
|
|
22291
22304
|
multipleOfFour: true,
|
|
22292
22305
|
square: false
|
|
22293
22306
|
} : void 0);
|
|
22294
|
-
|
|
22307
|
+
assertPackedInputCoverage(pages, group.inputs.length, `standalone texture in package "${pkg.getName()}"`);
|
|
22295
22308
|
for (let pageOffset = 0; pageOffset < pages.length; pageOffset += 1) {
|
|
22296
22309
|
const page = pages[pageOffset];
|
|
22297
22310
|
const baseFileName = resolveStandaloneAtlasOutputFileName(pkg, group.resource, group.branchName);
|
|
@@ -22334,6 +22347,12 @@ function packAtlasPages(inputs, options, forceSinglePage, sizeOverrides) {
|
|
|
22334
22347
|
preserveInputOrderOnTie: options.preserveInputOrderOnTie
|
|
22335
22348
|
}).pack(inputs.map((input, index) => inputToCompatRect(input, index))) ?? [];
|
|
22336
22349
|
}
|
|
22350
|
+
function assertPackedInputCoverage(pages, inputCount, label) {
|
|
22351
|
+
const packedIndexes = /* @__PURE__ */ new Set();
|
|
22352
|
+
for (const page of pages) for (const outputRect of page.outputRects) packedIndexes.add(outputRect.index);
|
|
22353
|
+
const hasEveryInput = Array.from({ length: inputCount }, (_, index) => packedIndexes.has(index)).every(Boolean);
|
|
22354
|
+
if (packedIndexes.size !== inputCount || !hasEveryInput) throw new Error(`atlas: Could not pack every input for ${label}.`);
|
|
22355
|
+
}
|
|
22337
22356
|
function attachSpritesToAtlas(doc, allResources, inputs, outputRects, atlasNode) {
|
|
22338
22357
|
for (const packedRect of outputRects) {
|
|
22339
22358
|
const input = inputs[packedRect.index];
|
|
@@ -22393,7 +22412,9 @@ async function writeAtlasPageImage(pkg, inputs, page, atlasFileName, encoder, op
|
|
|
22393
22412
|
} else if (input.rasterizedBuffer) imageBuffer = input.rasterizedBuffer;
|
|
22394
22413
|
else {
|
|
22395
22414
|
if (!isImageResource$1(input.resource)) {
|
|
22396
|
-
|
|
22415
|
+
const message = `atlas: Non-image input "${input.id}" is missing inline buffer.`;
|
|
22416
|
+
if (options.strictOutput) throw new Error(message);
|
|
22417
|
+
logger.warn(`${message} Skipping compositing.`);
|
|
22397
22418
|
continue;
|
|
22398
22419
|
}
|
|
22399
22420
|
imageBuffer = await encoder(_resolveImagePath(input.resource, pkg, options.basePath)).toBuffer();
|
|
@@ -22405,7 +22426,9 @@ async function writeAtlasPageImage(pkg, inputs, page, atlasFileName, encoder, op
|
|
|
22405
22426
|
top: packedRect.y
|
|
22406
22427
|
});
|
|
22407
22428
|
} catch {
|
|
22408
|
-
|
|
22429
|
+
const message = `atlas: Could not read image "${input.id}" for compositing.`;
|
|
22430
|
+
if (options.strictOutput) throw new Error(message);
|
|
22431
|
+
logger.warn(message);
|
|
22409
22432
|
}
|
|
22410
22433
|
}
|
|
22411
22434
|
const outputFile = `${options.outputPath}/${atlasFileName}`;
|
|
@@ -22522,7 +22545,9 @@ async function emitDirectImageOutput(doc, pkg, input, encoder, options, logger,
|
|
|
22522
22545
|
}]).png().toFile(outputFile);
|
|
22523
22546
|
}
|
|
22524
22547
|
} catch {
|
|
22525
|
-
|
|
22548
|
+
const message = `atlas: Could not write direct-output atlas "${atlasFileName}".`;
|
|
22549
|
+
if (options.strictOutput) throw new Error(message);
|
|
22550
|
+
logger.warn(message);
|
|
22526
22551
|
}
|
|
22527
22552
|
}
|
|
22528
22553
|
function getInputBranchName(input) {
|
|
@@ -22748,6 +22773,7 @@ async function _collectImage(resource, pkg, inputs, encoder, options, doTrim, lo
|
|
|
22748
22773
|
sourceHasAlpha = true;
|
|
22749
22774
|
}
|
|
22750
22775
|
} catch {
|
|
22776
|
+
if (options.strictOutput) throw new Error(`atlas: Could not read image "${filePath}".`);
|
|
22751
22777
|
if (origW === 0 || origH === 0) {
|
|
22752
22778
|
logger.warn(`atlas: Could not read image "${filePath}", skipping.`);
|
|
22753
22779
|
return;
|
|
@@ -22786,7 +22812,11 @@ async function _collectImage(resource, pkg, inputs, encoder, options, doTrim, lo
|
|
|
22786
22812
|
}
|
|
22787
22813
|
/** Collect MovieClip frame textures from a .jta file into the inputs array. */
|
|
22788
22814
|
async function _collectMovieClipFrames(doc, resource, pkg, inputs, encoder, options, logger) {
|
|
22789
|
-
if (!options.basePath || !options.readFileRaw)
|
|
22815
|
+
if (!options.basePath || !options.readFileRaw) {
|
|
22816
|
+
if (options.strictOutput) throw new Error(`atlas: MovieClip "${resource.getId()}" requires basePath and readFileRaw for complete raster output.`);
|
|
22817
|
+
return;
|
|
22818
|
+
}
|
|
22819
|
+
if (!encoder && options.strictOutput) throw new Error(`atlas: MovieClip "${resource.getId()}" requires an encoder for complete raster output.`);
|
|
22790
22820
|
const mcId = resource.getId();
|
|
22791
22821
|
const mcName = resource.getName() + ".jta";
|
|
22792
22822
|
const mcPath = resource.getPath() ?? "/";
|
|
@@ -22809,7 +22839,7 @@ async function _collectMovieClipFrames(doc, resource, pkg, inputs, encoder, opti
|
|
|
22809
22839
|
const exportFrameIndex = firstFrameIndexByTextureIndex.get(textureIndex);
|
|
22810
22840
|
if (exportFrameIndex === void 0) continue;
|
|
22811
22841
|
const itemId = `${mcId}_${exportFrameIndex}`;
|
|
22812
|
-
const input = await _createMovieClipFrameInput(jta.frames[textureIndex], itemId, resource, encoder);
|
|
22842
|
+
const input = await _createMovieClipFrameInput(jta.frames[textureIndex], itemId, resource, encoder, options.strictOutput);
|
|
22813
22843
|
if (!input) continue;
|
|
22814
22844
|
inputs.push(input);
|
|
22815
22845
|
spriteIdByTextureIndex.set(textureIndex, itemId);
|
|
@@ -22823,7 +22853,7 @@ async function _collectMovieClipFrames(doc, resource, pkg, inputs, encoder, opti
|
|
|
22823
22853
|
}
|
|
22824
22854
|
} else for (let frameIndex = 0; frameIndex < jta.frames.length; frameIndex += 1) {
|
|
22825
22855
|
const itemId = `${mcId}_${frameIndex}`;
|
|
22826
|
-
const input = await _createMovieClipFrameInput(jta.frames[frameIndex], itemId, resource, encoder);
|
|
22856
|
+
const input = await _createMovieClipFrameInput(jta.frames[frameIndex], itemId, resource, encoder, options.strictOutput);
|
|
22827
22857
|
if (!input) continue;
|
|
22828
22858
|
inputs.push(input);
|
|
22829
22859
|
const frame = doc.createMovieFrame(itemId);
|
|
@@ -22835,10 +22865,12 @@ async function _collectMovieClipFrames(doc, resource, pkg, inputs, encoder, opti
|
|
|
22835
22865
|
resource.setHeight(jta.meta?.height ?? 0);
|
|
22836
22866
|
}
|
|
22837
22867
|
} catch {
|
|
22838
|
-
|
|
22868
|
+
const message = `atlas: Could not parse MovieClip "${filePath}".`;
|
|
22869
|
+
if (options.strictOutput) throw new Error(message);
|
|
22870
|
+
logger.warn(`${message} Skipping frames.`);
|
|
22839
22871
|
}
|
|
22840
22872
|
}
|
|
22841
|
-
async function _createMovieClipFrameInput(buffer, itemId, resource, encoder) {
|
|
22873
|
+
async function _createMovieClipFrameInput(buffer, itemId, resource, encoder, strictOutput) {
|
|
22842
22874
|
if (!encoder || buffer.length === 0) return null;
|
|
22843
22875
|
try {
|
|
22844
22876
|
const meta = await encoder(buffer).metadata();
|
|
@@ -22858,6 +22890,7 @@ async function _createMovieClipFrameInput(buffer, itemId, resource, encoder) {
|
|
|
22858
22890
|
sourceKind: "movieclip-frame"
|
|
22859
22891
|
};
|
|
22860
22892
|
} catch {
|
|
22893
|
+
if (strictOutput) throw new Error(`atlas: Could not decode MovieClip frame "${itemId}".`);
|
|
22861
22894
|
return null;
|
|
22862
22895
|
}
|
|
22863
22896
|
}
|
|
@@ -23540,9 +23573,9 @@ function resolveProjectBasePath(basePath) {
|
|
|
23540
23573
|
const normalized = trimTrailingSlashes$2(basePath);
|
|
23541
23574
|
const assetsMatch = normalized.match(/^(.*)[/\\]assets(?:_[^/\\]+)?$/i);
|
|
23542
23575
|
if (assetsMatch?.[1]) return assetsMatch[1];
|
|
23543
|
-
return dirname$
|
|
23576
|
+
return dirname$1(normalized);
|
|
23544
23577
|
}
|
|
23545
|
-
function dirname$
|
|
23578
|
+
function dirname$1(filePath) {
|
|
23546
23579
|
return trimTrailingSlashes$2(filePath).match(/^(.*)[/\\][^/\\]+$/)?.[1] ?? "";
|
|
23547
23580
|
}
|
|
23548
23581
|
function trimTrailingSlashes$2(value) {
|
|
@@ -23687,10 +23720,16 @@ const TRANSPARENT_PNG_1X1 = Uint8Array.from([
|
|
|
23687
23720
|
96,
|
|
23688
23721
|
130
|
|
23689
23722
|
]);
|
|
23723
|
+
function assertSafeRestoreSegment(value, label) {
|
|
23724
|
+
if (!value || value === "." || value === ".." || value.includes("\0") || /[\\/:]/u.test(value)) throw new Error(`restore: Invalid ${label} "${value}".`);
|
|
23725
|
+
}
|
|
23690
23726
|
function normalizeVirtualPath(path) {
|
|
23691
|
-
const
|
|
23692
|
-
if (!
|
|
23693
|
-
|
|
23727
|
+
const raw = (path ?? "").trim();
|
|
23728
|
+
if (!raw || raw === "/") return "";
|
|
23729
|
+
if (raw.includes("\0") || raw.startsWith("\\") || raw.startsWith("//") || /^[a-z]:/iu.test(raw)) throw new Error(`restore: Invalid resource path "${raw}".`);
|
|
23730
|
+
const segments = raw.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
23731
|
+
if (segments.some((segment) => segment === "." || segment === ".." || segment.includes(":"))) throw new Error(`restore: Invalid resource path "${raw}".`);
|
|
23732
|
+
return segments.join("/");
|
|
23694
23733
|
}
|
|
23695
23734
|
function resourceFileName(resource) {
|
|
23696
23735
|
return resource.getFileName?.() || resource.getFile?.() || resource.getName?.() || "";
|
|
@@ -23874,44 +23913,40 @@ function normalizeComparablePath(value) {
|
|
|
23874
23913
|
const joined = segments.join("/");
|
|
23875
23914
|
return (drivePrefix ? `${drivePrefix}/${joined}`.replace(/\/$/, "") : hasRoot ? `/${joined}`.replace(/\/$/, "") : joined || ".").toLowerCase();
|
|
23876
23915
|
}
|
|
23877
|
-
function
|
|
23878
|
-
|
|
23916
|
+
function isPathWithin(root, candidate) {
|
|
23917
|
+
const normalizedRoot = normalizeComparablePath(root);
|
|
23918
|
+
return normalizeComparablePath(candidate).startsWith(`${normalizedRoot}/`);
|
|
23879
23919
|
}
|
|
23880
23920
|
function basename(filePath) {
|
|
23881
23921
|
return trimTrailingSlashes$1(filePath).match(/([^/\\]+)$/)?.[1] ?? "";
|
|
23882
23922
|
}
|
|
23883
|
-
function
|
|
23884
|
-
|
|
23885
|
-
const
|
|
23886
|
-
|
|
23887
|
-
return
|
|
23888
|
-
}
|
|
23889
|
-
|
|
23890
|
-
|
|
23891
|
-
|
|
23892
|
-
|
|
23893
|
-
|
|
23894
|
-
|
|
23895
|
-
|
|
23896
|
-
|
|
23897
|
-
|
|
23898
|
-
|
|
23899
|
-
|
|
23900
|
-
|
|
23901
|
-
|
|
23902
|
-
|
|
23903
|
-
|
|
23904
|
-
|
|
23905
|
-
|
|
23906
|
-
|
|
23907
|
-
|
|
23908
|
-
|
|
23909
|
-
|
|
23910
|
-
}
|
|
23911
|
-
if (!await fs.exists(outputDir)) {
|
|
23912
|
-
await fs.mkdir(outputDir);
|
|
23913
|
-
return;
|
|
23914
|
-
}
|
|
23923
|
+
function normalizeRestoreOutputDir(output) {
|
|
23924
|
+
const normalized = trimTrailingSlashes$1(output);
|
|
23925
|
+
const name = basename(normalized);
|
|
23926
|
+
if (!normalized || /\.fairy$/i.test(normalized) || !name || name === "." || name === ".." || /^[a-z]:$/iu.test(name)) throw new Error("restore: Output must be a non-root project directory, not a .fairy file.");
|
|
23927
|
+
return normalized;
|
|
23928
|
+
}
|
|
23929
|
+
function resolveOutputProjectPath(outputDir, fs) {
|
|
23930
|
+
return fs.join(outputDir, `${basename(outputDir)}.fairy`);
|
|
23931
|
+
}
|
|
23932
|
+
async function resolvePathForContainment(filePath, fs) {
|
|
23933
|
+
const missingSegments = [];
|
|
23934
|
+
let existingPath = filePath;
|
|
23935
|
+
while (!await fs.exists(existingPath)) {
|
|
23936
|
+
const parentPath = fs.dirname(existingPath);
|
|
23937
|
+
if (!parentPath || parentPath === existingPath) return Promise.resolve(fs.resolvePath(filePath));
|
|
23938
|
+
missingSegments.unshift(basename(existingPath));
|
|
23939
|
+
existingPath = parentPath;
|
|
23940
|
+
}
|
|
23941
|
+
const resolvedExistingPath = await Promise.resolve(fs.resolvePath(existingPath));
|
|
23942
|
+
return missingSegments.reduce((resolvedPath, segment) => fs.join(resolvedPath, segment), resolvedExistingPath);
|
|
23943
|
+
}
|
|
23944
|
+
async function assertRestoreOutputDir(inputDir, outputDir, fs, force) {
|
|
23945
|
+
const [resolvedInputDir, resolvedOutputDir] = await Promise.all([resolvePathForContainment(inputDir, fs), resolvePathForContainment(outputDir, fs)]);
|
|
23946
|
+
const normalizedInputDir = normalizeComparablePath(resolvedInputDir);
|
|
23947
|
+
const normalizedOutputDir = normalizeComparablePath(resolvedOutputDir);
|
|
23948
|
+
if (normalizedInputDir === normalizedOutputDir || isPathWithin(normalizedInputDir, normalizedOutputDir) || isPathWithin(normalizedOutputDir, normalizedInputDir)) throw new Error("Restore output directory must be independent from the published input directory.");
|
|
23949
|
+
if (!await fs.exists(outputDir)) return;
|
|
23915
23950
|
let entries;
|
|
23916
23951
|
try {
|
|
23917
23952
|
entries = await fs.readdir(outputDir);
|
|
@@ -23920,23 +23955,63 @@ async function prepareRestoreOutputDir(inputDir, outputDir, outputProjectPath, f
|
|
|
23920
23955
|
}
|
|
23921
23956
|
if (entries.length === 0) return;
|
|
23922
23957
|
if (!force) throw new Error(`Restore output directory is not empty: ${outputDir}. Use --force to overwrite it.`);
|
|
23923
|
-
|
|
23924
|
-
|
|
23925
|
-
|
|
23926
|
-
|
|
23927
|
-
|
|
23928
|
-
|
|
23958
|
+
}
|
|
23959
|
+
async function createRestoreStagingDir(outputDir, fs) {
|
|
23960
|
+
const parentDir = fs.dirname(outputDir) || ".";
|
|
23961
|
+
await fs.mkdir(parentDir);
|
|
23962
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
23963
|
+
const stagingDir = fs.join(parentDir, `.${basename(outputDir)}.restore-${generateId()}`);
|
|
23964
|
+
if (await fs.exists(stagingDir)) continue;
|
|
23965
|
+
await fs.mkdir(stagingDir);
|
|
23966
|
+
return stagingDir;
|
|
23967
|
+
}
|
|
23968
|
+
throw new Error(`restore: Could not allocate a staging directory beside ${outputDir}.`);
|
|
23969
|
+
}
|
|
23970
|
+
async function commitRestoreOutput(stagingDir, outputDir, fs) {
|
|
23971
|
+
if (!await fs.exists(outputDir)) {
|
|
23972
|
+
await fs.rename(stagingDir, outputDir);
|
|
23973
|
+
return null;
|
|
23974
|
+
}
|
|
23975
|
+
const parentDir = fs.dirname(outputDir) || ".";
|
|
23976
|
+
let backupDir = "";
|
|
23977
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
23978
|
+
const candidate = fs.join(parentDir, `.${basename(outputDir)}.restore-backup-${generateId()}`);
|
|
23979
|
+
if (!await fs.exists(candidate)) {
|
|
23980
|
+
backupDir = candidate;
|
|
23981
|
+
break;
|
|
23982
|
+
}
|
|
23983
|
+
}
|
|
23984
|
+
if (!backupDir) throw new Error(`restore: Could not allocate a backup directory beside ${outputDir}.`);
|
|
23985
|
+
await fs.rename(outputDir, backupDir);
|
|
23986
|
+
try {
|
|
23987
|
+
await fs.rename(stagingDir, outputDir);
|
|
23988
|
+
} catch (error) {
|
|
23989
|
+
await fs.rename(backupDir, outputDir);
|
|
23990
|
+
throw error;
|
|
23991
|
+
}
|
|
23992
|
+
try {
|
|
23993
|
+
await fs.rm(backupDir, {
|
|
23994
|
+
recursive: true,
|
|
23995
|
+
force: true
|
|
23996
|
+
});
|
|
23997
|
+
return null;
|
|
23998
|
+
} catch {
|
|
23999
|
+
return `restore: Previous output retained at ${backupDir}; remove it after checking the restored project.`;
|
|
24000
|
+
}
|
|
23929
24001
|
}
|
|
23930
24002
|
async function restore(options) {
|
|
23931
24003
|
const sourceDir = trimTrailingSlashes$1(options.inputDir);
|
|
23932
|
-
const
|
|
23933
|
-
const outputProjectPath = resolveOutputProjectPath(
|
|
23934
|
-
await
|
|
24004
|
+
const outputDir = normalizeRestoreOutputDir(options.output);
|
|
24005
|
+
const outputProjectPath = resolveOutputProjectPath(outputDir, options.fs);
|
|
24006
|
+
await assertRestoreOutputDir(sourceDir, outputDir, options.fs, options.force === true);
|
|
23935
24007
|
const packageFilter = options.packages?.length ? new Set(options.packages) : null;
|
|
23936
|
-
const
|
|
24008
|
+
const binaryNames = (await options.fs.readdir(sourceDir)).filter((name) => isPublishedBinaryFile(name)).filter((name) => !packageFilter || packageFilter.has(inferPackageName(name)));
|
|
24009
|
+
for (const binaryName of binaryNames) assertSafeRestoreSegment(binaryName, "published binary file name");
|
|
24010
|
+
const candidateBinaryPaths = binaryNames.map((name) => options.fs.join(sourceDir, name)).sort((left, right) => left.localeCompare(right));
|
|
23937
24011
|
const binaryPaths = (await Promise.all(candidateBinaryPaths.map(async (filePath) => await options.fs.isFile(filePath) ? filePath : null))).filter((filePath) => !!filePath).sort((left, right) => left.localeCompare(right));
|
|
23938
24012
|
if (binaryPaths.length === 0) throw new Error(`No FairyGUI published binary files found in ${sourceDir}.`);
|
|
23939
|
-
|
|
24013
|
+
const restorer = new RestoreWorkflow(options.fs);
|
|
24014
|
+
const document = await restorer.prepare({
|
|
23940
24015
|
binaryPaths,
|
|
23941
24016
|
sourceDir,
|
|
23942
24017
|
outputProjectPath,
|
|
@@ -23944,18 +24019,45 @@ async function restore(options) {
|
|
|
23944
24019
|
cropImage: options.cropImage,
|
|
23945
24020
|
extractImage: options.extractImage
|
|
23946
24021
|
});
|
|
24022
|
+
const stagingDir = await createRestoreStagingDir(outputDir, options.fs);
|
|
24023
|
+
const stagingProjectPath = options.fs.join(stagingDir, basename(outputProjectPath));
|
|
24024
|
+
const warnings = [];
|
|
24025
|
+
try {
|
|
24026
|
+
await restorer.write(document, {
|
|
24027
|
+
binaryPaths,
|
|
24028
|
+
sourceDir,
|
|
24029
|
+
outputProjectPath: stagingProjectPath,
|
|
24030
|
+
projectType: options.projectType,
|
|
24031
|
+
cropImage: options.cropImage,
|
|
24032
|
+
extractImage: options.extractImage
|
|
24033
|
+
}, warnings);
|
|
24034
|
+
const cleanupWarning = await commitRestoreOutput(stagingDir, outputDir, options.fs);
|
|
24035
|
+
if (cleanupWarning) warnings.push(cleanupWarning);
|
|
24036
|
+
} catch (error) {
|
|
24037
|
+
await options.fs.rm(stagingDir, {
|
|
24038
|
+
recursive: true,
|
|
24039
|
+
force: true
|
|
24040
|
+
}).catch(() => void 0);
|
|
24041
|
+
throw error;
|
|
24042
|
+
}
|
|
24043
|
+
return {
|
|
24044
|
+
document,
|
|
24045
|
+
projectPath: outputProjectPath,
|
|
24046
|
+
warnings
|
|
24047
|
+
};
|
|
23947
24048
|
}
|
|
23948
24049
|
var RestoreWorkflow = class {
|
|
23949
24050
|
_fs;
|
|
23950
24051
|
constructor(fs) {
|
|
23951
24052
|
this._fs = fs;
|
|
23952
24053
|
}
|
|
23953
|
-
async
|
|
23954
|
-
const warnings = [];
|
|
24054
|
+
async prepare(options) {
|
|
23955
24055
|
const doc = await new BinaryReader(this._fs).readMany(options.binaryPaths);
|
|
24056
|
+
this._assertDocumentPaths(doc);
|
|
23956
24057
|
this._initializeProjectDefaults(doc, options.projectType);
|
|
23957
24058
|
this._initializeImageFileNames(doc);
|
|
23958
24059
|
this._initializeLooseResourceFileNames(doc);
|
|
24060
|
+
this._assertDocumentPaths(doc);
|
|
23959
24061
|
await this._synthesizeLooseSkeletonResources(doc, options.sourceDir);
|
|
23960
24062
|
this._initializeRestoredResourceRelations(doc);
|
|
23961
24063
|
this._initializePublishedFontTextureIds(doc);
|
|
@@ -23964,13 +24066,27 @@ var RestoreWorkflow = class {
|
|
|
23964
24066
|
this._initializePublishedTextFontResources(doc);
|
|
23965
24067
|
this._initializeDisplayObjectFileNames(doc);
|
|
23966
24068
|
this._initializePublishedFontDefaults(doc);
|
|
24069
|
+
this._assertDocumentPaths(doc);
|
|
24070
|
+
return doc;
|
|
24071
|
+
}
|
|
24072
|
+
async write(doc, options, warnings) {
|
|
23967
24073
|
await new ProjectWriter(this._fs).write(doc, options.outputProjectPath);
|
|
23968
24074
|
await this._restoreAssets(doc, options, warnings);
|
|
23969
|
-
|
|
23970
|
-
|
|
23971
|
-
|
|
23972
|
-
|
|
23973
|
-
|
|
24075
|
+
}
|
|
24076
|
+
_assertDocumentPaths(doc) {
|
|
24077
|
+
for (const pkg of doc.getRoot().listPackages()) {
|
|
24078
|
+
assertSafeRestoreSegment(pkg.getName(), "package name");
|
|
24079
|
+
assertSafeRestoreSegment(pkg.getPublishName() || pkg.getName(), "package publish name");
|
|
24080
|
+
for (const resource of pkg.listResources()) {
|
|
24081
|
+
normalizeVirtualPath(resource.getPath?.());
|
|
24082
|
+
const branch = resource.getBranch?.() ?? "";
|
|
24083
|
+
if (branch) assertSafeRestoreSegment(branch, "branch name");
|
|
24084
|
+
const fileName = resourceFileName(resource);
|
|
24085
|
+
if (fileName) assertSafeRestoreSegment(fileName, "resource file name");
|
|
24086
|
+
const publishedFileName = resourcePublishedFileName(resource);
|
|
24087
|
+
if (publishedFileName) assertSafeRestoreSegment(publishedFileName, "published resource file name");
|
|
24088
|
+
}
|
|
24089
|
+
}
|
|
23974
24090
|
}
|
|
23975
24091
|
_initializeProjectDefaults(doc, projectType) {
|
|
23976
24092
|
doc.getRoot().setProjectId(generateId()).setProjectType(projectType ?? ProjectType.Unity).setVersion("3.0").setSettings({
|
|
@@ -24404,27 +24520,40 @@ var RestoreWorkflow = class {
|
|
|
24404
24520
|
}
|
|
24405
24521
|
_sourceFileCandidates(pkg, fileName, outputFileName = fileName) {
|
|
24406
24522
|
const publishName = pkg.getPublishName() || pkg.getName();
|
|
24407
|
-
|
|
24523
|
+
assertSafeRestoreSegment(publishName, "package publish name");
|
|
24524
|
+
assertSafeRestoreSegment(fileName, "published source file name");
|
|
24525
|
+
assertSafeRestoreSegment(outputFileName, "published source file name");
|
|
24526
|
+
const candidates = Array.from(new Set([
|
|
24408
24527
|
`${publishName}_${fileName}`,
|
|
24409
24528
|
fileName,
|
|
24410
24529
|
`${publishName}_${outputFileName}`,
|
|
24411
24530
|
outputFileName
|
|
24412
24531
|
]));
|
|
24532
|
+
for (const candidate of candidates) assertSafeRestoreSegment(candidate, "published source file name");
|
|
24533
|
+
return candidates;
|
|
24413
24534
|
}
|
|
24414
24535
|
async _resolveLooseSourceFile(pkg, sourceDir, outputFileName) {
|
|
24415
24536
|
const candidates = outputFileName.endsWith(".atlas") ? this._sourceFileCandidates(pkg, `${outputFileName}.txt`, outputFileName) : outputFileName.endsWith(".skel") ? this._sourceFileCandidates(pkg, `${outputFileName}.bytes`, outputFileName) : this._sourceFileCandidates(pkg, outputFileName);
|
|
24416
24537
|
return this._resolveSourceFile(sourceDir, candidates);
|
|
24417
24538
|
}
|
|
24418
24539
|
async _resolveSourceFile(sourceDir, candidates) {
|
|
24540
|
+
const resolvedSourceDir = await Promise.resolve(this._fs.resolvePath(sourceDir));
|
|
24419
24541
|
for (const candidate of candidates) {
|
|
24542
|
+
assertSafeRestoreSegment(candidate, "published source file name");
|
|
24420
24543
|
const sourcePath = this._fs.join(sourceDir, candidate);
|
|
24421
|
-
if (await this._fs.isFile(sourcePath))
|
|
24544
|
+
if (!await this._fs.isFile(sourcePath)) continue;
|
|
24545
|
+
const resolvedSourcePath = await Promise.resolve(this._fs.resolvePath(sourcePath));
|
|
24546
|
+
if (!isPathWithin(resolvedSourceDir, resolvedSourcePath)) throw new Error(`restore: Published source file resolves outside the input directory: ${candidate}.`);
|
|
24547
|
+
return resolvedSourcePath;
|
|
24422
24548
|
}
|
|
24423
24549
|
return null;
|
|
24424
24550
|
}
|
|
24425
24551
|
_resourceOutputPath(outputProjectPath, pkg, resource, fileName) {
|
|
24426
24552
|
const basePath = this._fs.dirname(outputProjectPath);
|
|
24427
24553
|
const branch = resource.getBranch?.() ?? "";
|
|
24554
|
+
assertSafeRestoreSegment(pkg.getName(), "package name");
|
|
24555
|
+
if (branch) assertSafeRestoreSegment(branch, "branch name");
|
|
24556
|
+
assertSafeRestoreSegment(fileName, "resource file name");
|
|
24428
24557
|
const assetsDir = branch ? `assets_${branch}` : "assets";
|
|
24429
24558
|
const virtualPath = normalizeVirtualPath(resource.getPath?.());
|
|
24430
24559
|
const pkgDir = this._fs.join(basePath, assetsDir, pkg.getName());
|
|
@@ -24991,13 +25120,13 @@ function getPublishedSkeletonDependencyImageIds(pkg, publishedResourceIds) {
|
|
|
24991
25120
|
}
|
|
24992
25121
|
return imageIds;
|
|
24993
25122
|
}
|
|
24994
|
-
async function exportPackageSounds(pkg, outputDir, basePath, fs, readFileRaw
|
|
25123
|
+
async function exportPackageSounds(pkg, outputDir, basePath, fs, readFileRaw) {
|
|
24995
25124
|
const publishedResourceIds = getAnnotatedPublishedResourceIds(pkg);
|
|
24996
25125
|
if (publishedResourceIds.size === 0) return;
|
|
24997
25126
|
if (!basePath || !readFileRaw) {
|
|
24998
25127
|
if (pkg.listResources().some((resource) => {
|
|
24999
25128
|
return isSoundResource(resource) && publishedResourceIds.has(resource.getId());
|
|
25000
|
-
}))
|
|
25129
|
+
})) throw new Error(`publish: Sound resources in package "${pkg.getName()}" require basePath and readFileRaw for output.`);
|
|
25001
25130
|
return;
|
|
25002
25131
|
}
|
|
25003
25132
|
for (const resource of pkg.listResources()) {
|
|
@@ -25010,18 +25139,18 @@ async function exportPackageSounds(pkg, outputDir, basePath, fs, readFileRaw, lo
|
|
|
25010
25139
|
const data = await readFileRaw(sourcePath);
|
|
25011
25140
|
await fs.writeFileRaw(targetPath, data);
|
|
25012
25141
|
} catch {
|
|
25013
|
-
|
|
25142
|
+
throw new Error(`publish: Could not export sound "${resource.getId()}" from package "${pkg.getName()}".`);
|
|
25014
25143
|
}
|
|
25015
25144
|
}
|
|
25016
25145
|
}
|
|
25017
|
-
async function exportPackageExternalResources(pkg, outputDir, basePath, fs, readFileRaw
|
|
25146
|
+
async function exportPackageExternalResources(pkg, outputDir, basePath, fs, readFileRaw) {
|
|
25018
25147
|
const exportedResourceIds = getAnnotatedExportedResourceIds(pkg);
|
|
25019
25148
|
const skeletonDependencyImageIds = getPublishedSkeletonDependencyImageIds(pkg, exportedResourceIds);
|
|
25020
25149
|
if (exportedResourceIds.size === 0) return;
|
|
25021
25150
|
if (!basePath || !readFileRaw) {
|
|
25022
25151
|
if (pkg.listResources().some((resource) => {
|
|
25023
25152
|
return (isMiscResource(resource) || isSkeletonResource(resource)) && exportedResourceIds.has(resource.getId()) || skeletonDependencyImageIds.has(resource.getId());
|
|
25024
|
-
}))
|
|
25153
|
+
})) throw new Error(`publish: External resources in package "${pkg.getName()}" require basePath and readFileRaw for output.`);
|
|
25025
25154
|
return;
|
|
25026
25155
|
}
|
|
25027
25156
|
for (const resource of pkg.listResources()) {
|
|
@@ -25043,7 +25172,7 @@ async function exportPackageExternalResources(pkg, outputDir, basePath, fs, read
|
|
|
25043
25172
|
const data = await readFileRaw(sourcePath);
|
|
25044
25173
|
await fs.writeFileRaw(targetPath, data);
|
|
25045
25174
|
} catch {
|
|
25046
|
-
|
|
25175
|
+
throw new Error(`publish: Could not export external resource "${resource.getId()}" from package "${pkg.getName()}".`);
|
|
25047
25176
|
}
|
|
25048
25177
|
}
|
|
25049
25178
|
}
|
|
@@ -25059,18 +25188,17 @@ async function exportPackageExternalResources(pkg, outputDir, basePath, fs, read
|
|
|
25059
25188
|
* `publishNode()` or `publishBrowser()` through their dedicated entries.
|
|
25060
25189
|
*
|
|
25061
25190
|
* ```ts
|
|
25062
|
-
* import
|
|
25063
|
-
*
|
|
25064
|
-
* const doc = await
|
|
25191
|
+
* import { NodeIO } from '@openfairygui/core/node';
|
|
25192
|
+
* import { publishNode } from '@openfairygui/functions/node';
|
|
25193
|
+
* const doc = await new NodeIO().readProject('./project.fairy');
|
|
25065
25194
|
*
|
|
25066
|
-
* await
|
|
25195
|
+
* await publishNode({
|
|
25196
|
+
* document: doc,
|
|
25067
25197
|
* output: './release/',
|
|
25068
25198
|
* compressed: true,
|
|
25069
|
-
*
|
|
25070
|
-
* basePath: './assets/',
|
|
25199
|
+
* assetsPath: './assets/',
|
|
25071
25200
|
* fileExtension: 'bytes',
|
|
25072
|
-
*
|
|
25073
|
-
* }));
|
|
25201
|
+
* });
|
|
25074
25202
|
* ```
|
|
25075
25203
|
*/
|
|
25076
25204
|
function publish(options) {
|
|
@@ -25138,6 +25266,12 @@ function publish(options) {
|
|
|
25138
25266
|
}
|
|
25139
25267
|
});
|
|
25140
25268
|
const publishPackage = async (plan, writerFs, packageIndex) => {
|
|
25269
|
+
if (options.fs && !plan.outputDir) throw new Error("publish: no output directory resolved. Provide --output, or configure global publish.path / package publishPath.");
|
|
25270
|
+
if (options.fs) {
|
|
25271
|
+
await options.fs.mkdir(plan.outputDir);
|
|
25272
|
+
await exportPackageSounds(plan.pkg, plan.outputDir, options.basePath, options.fs, options.atlas?.readFileRaw ?? options.fs.readFileRaw);
|
|
25273
|
+
await exportPackageExternalResources(plan.pkg, plan.outputDir, options.basePath, options.fs, options.atlas?.readFileRaw ?? options.fs.readFileRaw);
|
|
25274
|
+
}
|
|
25141
25275
|
const atlasRuntimeOptions = resolvePublishAtlasRuntimeOptions(plan.fileExtension);
|
|
25142
25276
|
await atlas({
|
|
25143
25277
|
...plan.atlas,
|
|
@@ -25148,20 +25282,17 @@ function publish(options) {
|
|
|
25148
25282
|
outputPath: options.fs ? plan.outputDir : void 0,
|
|
25149
25283
|
mkdir: options.fs ? options.fs.mkdir : void 0,
|
|
25150
25284
|
readFileRaw: options.atlas?.readFileRaw ?? options.fs?.readFileRaw,
|
|
25285
|
+
strictOutput: options.fs !== void 0,
|
|
25151
25286
|
packages: [plan.pkg.getName()],
|
|
25152
25287
|
...atlasRuntimeOptions
|
|
25153
25288
|
})(doc);
|
|
25154
25289
|
if (!options.fs) return;
|
|
25155
|
-
if (!plan.outputDir) throw new Error("publish: no output directory resolved. Provide --output, or configure global publish.path / package publishPath.");
|
|
25156
|
-
await options.fs.mkdir(plan.outputDir);
|
|
25157
25290
|
const filePath = options.fs.join(plan.outputDir, plan.fileName);
|
|
25158
25291
|
const bwOptions = {
|
|
25159
25292
|
compressed: plan.compressed,
|
|
25160
25293
|
packageIndex
|
|
25161
25294
|
};
|
|
25162
25295
|
await new BinaryWriter(writerFs).write(doc, filePath, bwOptions);
|
|
25163
|
-
await exportPackageSounds(plan.pkg, plan.outputDir, options.basePath, options.fs, options.atlas?.readFileRaw ?? options.fs.readFileRaw, logger);
|
|
25164
|
-
await exportPackageExternalResources(plan.pkg, plan.outputDir, options.basePath, options.fs, options.atlas?.readFileRaw ?? options.fs.readFileRaw, logger);
|
|
25165
25296
|
logger.info(`publish: Written ${plan.fileName}`);
|
|
25166
25297
|
};
|
|
25167
25298
|
const root = doc.getRoot();
|
|
@@ -25194,7 +25325,9 @@ function publish(options) {
|
|
|
25194
25325
|
}
|
|
25195
25326
|
const plans = allPackages.map((pkg) => resolvePackagePublishPlan(pkg, resolved, projectBasePath));
|
|
25196
25327
|
if (!options.fs) {
|
|
25197
|
-
|
|
25328
|
+
const outputPlan = plans.find((plan) => !!plan.outputDir);
|
|
25329
|
+
if (outputPlan) throw new Error(`publish: Output for package "${outputPlan.pkg.getName()}" requires a filesystem. Omit output and publish paths to run a layout-only transform.`);
|
|
25330
|
+
logger.info(`publish: Layout computed for ${allPackages.length} package(s); no output directory was requested.`);
|
|
25198
25331
|
const noopWriterFs = toBinaryWriterFileSystem(createNoopPublishFs());
|
|
25199
25332
|
for (const plan of plans) await publishPackage(plan, noopWriterFs, allDocPackages.indexOf(plan.pkg));
|
|
25200
25333
|
await runPublishPluginHook(plugins, "onPublishEnd", doc, options);
|
|
@@ -25548,7 +25681,7 @@ async function publishNode(options) {
|
|
|
25548
25681
|
const { document, assetsPath: configuredAssetsPath, atlas, encoder: configuredEncoder, plugins: configuredPlugins, ...publishOptions } = options;
|
|
25549
25682
|
const [fileSystem, assetsPath] = await Promise.all([createNodePublishFileSystem(), resolveNodeAssetsPath(document, configuredAssetsPath)]);
|
|
25550
25683
|
const [encoder, plugins] = await Promise.all([configuredEncoder === void 0 ? loadSharpBackend() : Promise.resolve(configuredEncoder), configuredPlugins === void 0 ? loadNodePublishPlugins(document, assetsPath) : Promise.resolve(configuredPlugins)]);
|
|
25551
|
-
if (!encoder)
|
|
25684
|
+
if (!encoder) throw new Error("publishNode: Sharp is required for a complete publish. Install sharp or provide an encoder.");
|
|
25552
25685
|
await document.transform(publish({
|
|
25553
25686
|
...publishOptions,
|
|
25554
25687
|
basePath: assetsPath,
|
|
@@ -25624,7 +25757,7 @@ function registerPublishCommand(program) {
|
|
|
25624
25757
|
//#endregion
|
|
25625
25758
|
//#region src/commands/restore.ts
|
|
25626
25759
|
function registerRestoreCommand(program) {
|
|
25627
|
-
program.command("restore").description("
|
|
25760
|
+
program.command("restore").description("Recover a project directory from trusted local published artifacts").argument("<release-dir>", "Published release directory").requiredOption("-o, --output <dir>", "Output project directory").option("-p, --packages <a,b,c>", "Only restore specific packages (comma-separated)").option("-f, --force", "Replace a non-empty output directory only after a complete staged restore").option("-t, --project-type <name|id>", "Override restored project type; default is unity").action(async (releaseDir, options) => {
|
|
25628
25761
|
const inputDir = path.resolve(releaseDir);
|
|
25629
25762
|
const outputDir = path.resolve(options.output);
|
|
25630
25763
|
const pkgFilter = options.packages ? options.packages.split(",").map((value) => value.trim()).filter(Boolean) : void 0;
|
|
@@ -25698,6 +25831,9 @@ function createNodeRestoreFs() {
|
|
|
25698
25831
|
force: options?.force ?? false
|
|
25699
25832
|
});
|
|
25700
25833
|
},
|
|
25834
|
+
async rename(from, to) {
|
|
25835
|
+
await fs.rename(from, to);
|
|
25836
|
+
},
|
|
25701
25837
|
join(...paths) {
|
|
25702
25838
|
return path.join(...paths);
|
|
25703
25839
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openfairygui/cli",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.16",
|
|
4
4
|
"description": "FairyGUI Headless Authoring SDK — command-line interface.",
|
|
5
5
|
"author": "OpenFairyGUI Contributors",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,13 +33,13 @@
|
|
|
33
33
|
"restore"
|
|
34
34
|
],
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@openfairygui/functions": "0.2.0-alpha.
|
|
37
|
-
"@openfairygui/core": "0.2.0-alpha.
|
|
36
|
+
"@openfairygui/functions": "0.2.0-alpha.16",
|
|
37
|
+
"@openfairygui/core": "0.2.0-alpha.16"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"commander": "^14.0.2",
|
|
41
41
|
"jiti": "^2.7.0",
|
|
42
|
-
"@openfairygui/backend": "0.2.0-alpha.
|
|
42
|
+
"@openfairygui/backend": "0.2.0-alpha.16"
|
|
43
43
|
},
|
|
44
44
|
"optionalDependencies": {
|
|
45
45
|
"sharp": ">=0.33.0"
|
package/src/commands/restore.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import {
|
|
3
|
-
restore,
|
|
4
4
|
type RestoreFileSystem,
|
|
5
5
|
type RestoreImageCropInput,
|
|
6
6
|
type RestoreImageCropper,
|
|
7
7
|
type RestoreImageExtractInput,
|
|
8
8
|
type RestoreImageExtractor,
|
|
9
|
+
restore,
|
|
9
10
|
} from '@openfairygui/functions';
|
|
10
|
-
import
|
|
11
|
-
import path from 'node:path';
|
|
11
|
+
import type { Command } from 'commander';
|
|
12
12
|
import { parseProjectType } from '../utils/project-type.js';
|
|
13
13
|
|
|
14
14
|
type RestoreCommandOptions = {
|
|
@@ -26,11 +26,11 @@ interface RestoreImageProcessors {
|
|
|
26
26
|
export function registerRestoreCommand(program: Command): void {
|
|
27
27
|
program
|
|
28
28
|
.command('restore')
|
|
29
|
-
.description('
|
|
29
|
+
.description('Recover a project directory from trusted local published artifacts')
|
|
30
30
|
.argument('<release-dir>', 'Published release directory')
|
|
31
31
|
.requiredOption('-o, --output <dir>', 'Output project directory')
|
|
32
32
|
.option('-p, --packages <a,b,c>', 'Only restore specific packages (comma-separated)')
|
|
33
|
-
.option('-f, --force', '
|
|
33
|
+
.option('-f, --force', 'Replace a non-empty output directory only after a complete staged restore')
|
|
34
34
|
.option('-t, --project-type <name|id>', 'Override restored project type; default is unity')
|
|
35
35
|
.action(async (releaseDir: string, options: RestoreCommandOptions) => {
|
|
36
36
|
const inputDir = path.resolve(releaseDir);
|
|
@@ -113,6 +113,9 @@ function createNodeRestoreFs(): RestoreFileSystem {
|
|
|
113
113
|
async rm(targetPath: string, options?: { recursive?: boolean; force?: boolean }): Promise<void> {
|
|
114
114
|
await fs.rm(targetPath, { recursive: options?.recursive ?? false, force: options?.force ?? false });
|
|
115
115
|
},
|
|
116
|
+
async rename(from: string, to: string): Promise<void> {
|
|
117
|
+
await fs.rename(from, to);
|
|
118
|
+
},
|
|
116
119
|
join(...paths: string[]): string {
|
|
117
120
|
return path.join(...paths);
|
|
118
121
|
},
|