@openfairygui/cli 0.2.0-alpha.14 → 0.2.0-alpha.15
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 +219 -93
- 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
|
@@ -21921,7 +21921,8 @@ const ATLAS_DEFAULTS = {
|
|
|
21921
21921
|
preserveInputOrderOnTie: false,
|
|
21922
21922
|
directSingleImageOutput: false,
|
|
21923
21923
|
extractAlpha: false,
|
|
21924
|
-
separatedAtlasForBranch: false
|
|
21924
|
+
separatedAtlasForBranch: false,
|
|
21925
|
+
strictOutput: false
|
|
21925
21926
|
};
|
|
21926
21927
|
function getPublishedItemId(resource) {
|
|
21927
21928
|
return (resource.getExtras() ?? {})._publishedId ?? resource.getId();
|
|
@@ -22078,8 +22079,9 @@ function atlas(_options = {}) {
|
|
|
22078
22079
|
const packageFilter = options.packages ? new Set(options.packages) : null;
|
|
22079
22080
|
for (const pkg of root.listPackages()) {
|
|
22080
22081
|
if (packageFilter && !packageFilter.has(pkg.getName())) continue;
|
|
22081
|
-
const
|
|
22082
|
-
const
|
|
22082
|
+
const publishedResourceIds = pkg.getExtras()?.publishedResourceIds;
|
|
22083
|
+
const selectedPublishIds = new Set(publishedResourceIds);
|
|
22084
|
+
const allResources = publishedResourceIds !== void 0 && (options.strictOutput || selectedPublishIds.size > 0) ? pkg.listResources().filter((resource) => selectedPublishIds.has(resource.getId())) : pkg.listResources();
|
|
22083
22085
|
const skeletonDependencyImageIds = getSelectedSkeletonDependencyImageIds(allResources);
|
|
22084
22086
|
const orderedResources = await resolveEditorCompatibleResourceOrder(pkg, allResources, options);
|
|
22085
22087
|
const orderedAllResources = sortResourcesByOrder(allResources, new Map(orderedResources.map((resource, index) => [resource.getId(), index])), new Map(allResources.map((resource, index) => [resource.getId(), index])));
|
|
@@ -22168,6 +22170,7 @@ function atlas(_options = {}) {
|
|
|
22168
22170
|
await _collectFontTexture(doc, res, pkg, options);
|
|
22169
22171
|
}
|
|
22170
22172
|
if (inputs.length === 0) continue;
|
|
22173
|
+
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
22174
|
let totalPageCount = 0;
|
|
22172
22175
|
let usedDirectOutput = false;
|
|
22173
22176
|
const { autoInputs, fixedPageGroups, standaloneGroups, reservedPageIndexes } = groupStandaloneInputs(doc, inputs, options);
|
|
@@ -22265,7 +22268,7 @@ function reserveAutoPageStart(branchPageOffsets, branchOrdinal, reservedPageInde
|
|
|
22265
22268
|
async function emitPagedAtlasGroup(doc, pkg, allResources, inputs, context) {
|
|
22266
22269
|
if (inputs.length === 0) return 0;
|
|
22267
22270
|
const pages = packAtlasPages(inputs, context.options, context.forceSinglePage === true);
|
|
22268
|
-
|
|
22271
|
+
assertPackedInputCoverage(pages, inputs.length, `package "${pkg.getName()}"`);
|
|
22269
22272
|
for (let pageOffset = 0; pageOffset < pages.length; pageOffset += 1) {
|
|
22270
22273
|
const page = pages[pageOffset];
|
|
22271
22274
|
const pageIndex = context.pageStart + pageOffset;
|
|
@@ -22291,7 +22294,7 @@ async function emitStandaloneAtlasGroup(doc, pkg, group, context) {
|
|
|
22291
22294
|
multipleOfFour: true,
|
|
22292
22295
|
square: false
|
|
22293
22296
|
} : void 0);
|
|
22294
|
-
|
|
22297
|
+
assertPackedInputCoverage(pages, group.inputs.length, `standalone texture in package "${pkg.getName()}"`);
|
|
22295
22298
|
for (let pageOffset = 0; pageOffset < pages.length; pageOffset += 1) {
|
|
22296
22299
|
const page = pages[pageOffset];
|
|
22297
22300
|
const baseFileName = resolveStandaloneAtlasOutputFileName(pkg, group.resource, group.branchName);
|
|
@@ -22334,6 +22337,12 @@ function packAtlasPages(inputs, options, forceSinglePage, sizeOverrides) {
|
|
|
22334
22337
|
preserveInputOrderOnTie: options.preserveInputOrderOnTie
|
|
22335
22338
|
}).pack(inputs.map((input, index) => inputToCompatRect(input, index))) ?? [];
|
|
22336
22339
|
}
|
|
22340
|
+
function assertPackedInputCoverage(pages, inputCount, label) {
|
|
22341
|
+
const packedIndexes = /* @__PURE__ */ new Set();
|
|
22342
|
+
for (const page of pages) for (const outputRect of page.outputRects) packedIndexes.add(outputRect.index);
|
|
22343
|
+
const hasEveryInput = Array.from({ length: inputCount }, (_, index) => packedIndexes.has(index)).every(Boolean);
|
|
22344
|
+
if (packedIndexes.size !== inputCount || !hasEveryInput) throw new Error(`atlas: Could not pack every input for ${label}.`);
|
|
22345
|
+
}
|
|
22337
22346
|
function attachSpritesToAtlas(doc, allResources, inputs, outputRects, atlasNode) {
|
|
22338
22347
|
for (const packedRect of outputRects) {
|
|
22339
22348
|
const input = inputs[packedRect.index];
|
|
@@ -22393,7 +22402,9 @@ async function writeAtlasPageImage(pkg, inputs, page, atlasFileName, encoder, op
|
|
|
22393
22402
|
} else if (input.rasterizedBuffer) imageBuffer = input.rasterizedBuffer;
|
|
22394
22403
|
else {
|
|
22395
22404
|
if (!isImageResource$1(input.resource)) {
|
|
22396
|
-
|
|
22405
|
+
const message = `atlas: Non-image input "${input.id}" is missing inline buffer.`;
|
|
22406
|
+
if (options.strictOutput) throw new Error(message);
|
|
22407
|
+
logger.warn(`${message} Skipping compositing.`);
|
|
22397
22408
|
continue;
|
|
22398
22409
|
}
|
|
22399
22410
|
imageBuffer = await encoder(_resolveImagePath(input.resource, pkg, options.basePath)).toBuffer();
|
|
@@ -22405,7 +22416,9 @@ async function writeAtlasPageImage(pkg, inputs, page, atlasFileName, encoder, op
|
|
|
22405
22416
|
top: packedRect.y
|
|
22406
22417
|
});
|
|
22407
22418
|
} catch {
|
|
22408
|
-
|
|
22419
|
+
const message = `atlas: Could not read image "${input.id}" for compositing.`;
|
|
22420
|
+
if (options.strictOutput) throw new Error(message);
|
|
22421
|
+
logger.warn(message);
|
|
22409
22422
|
}
|
|
22410
22423
|
}
|
|
22411
22424
|
const outputFile = `${options.outputPath}/${atlasFileName}`;
|
|
@@ -22522,7 +22535,9 @@ async function emitDirectImageOutput(doc, pkg, input, encoder, options, logger,
|
|
|
22522
22535
|
}]).png().toFile(outputFile);
|
|
22523
22536
|
}
|
|
22524
22537
|
} catch {
|
|
22525
|
-
|
|
22538
|
+
const message = `atlas: Could not write direct-output atlas "${atlasFileName}".`;
|
|
22539
|
+
if (options.strictOutput) throw new Error(message);
|
|
22540
|
+
logger.warn(message);
|
|
22526
22541
|
}
|
|
22527
22542
|
}
|
|
22528
22543
|
function getInputBranchName(input) {
|
|
@@ -22748,6 +22763,7 @@ async function _collectImage(resource, pkg, inputs, encoder, options, doTrim, lo
|
|
|
22748
22763
|
sourceHasAlpha = true;
|
|
22749
22764
|
}
|
|
22750
22765
|
} catch {
|
|
22766
|
+
if (options.strictOutput) throw new Error(`atlas: Could not read image "${filePath}".`);
|
|
22751
22767
|
if (origW === 0 || origH === 0) {
|
|
22752
22768
|
logger.warn(`atlas: Could not read image "${filePath}", skipping.`);
|
|
22753
22769
|
return;
|
|
@@ -22786,7 +22802,11 @@ async function _collectImage(resource, pkg, inputs, encoder, options, doTrim, lo
|
|
|
22786
22802
|
}
|
|
22787
22803
|
/** Collect MovieClip frame textures from a .jta file into the inputs array. */
|
|
22788
22804
|
async function _collectMovieClipFrames(doc, resource, pkg, inputs, encoder, options, logger) {
|
|
22789
|
-
if (!options.basePath || !options.readFileRaw)
|
|
22805
|
+
if (!options.basePath || !options.readFileRaw) {
|
|
22806
|
+
if (options.strictOutput) throw new Error(`atlas: MovieClip "${resource.getId()}" requires basePath and readFileRaw for complete raster output.`);
|
|
22807
|
+
return;
|
|
22808
|
+
}
|
|
22809
|
+
if (!encoder && options.strictOutput) throw new Error(`atlas: MovieClip "${resource.getId()}" requires an encoder for complete raster output.`);
|
|
22790
22810
|
const mcId = resource.getId();
|
|
22791
22811
|
const mcName = resource.getName() + ".jta";
|
|
22792
22812
|
const mcPath = resource.getPath() ?? "/";
|
|
@@ -22809,7 +22829,7 @@ async function _collectMovieClipFrames(doc, resource, pkg, inputs, encoder, opti
|
|
|
22809
22829
|
const exportFrameIndex = firstFrameIndexByTextureIndex.get(textureIndex);
|
|
22810
22830
|
if (exportFrameIndex === void 0) continue;
|
|
22811
22831
|
const itemId = `${mcId}_${exportFrameIndex}`;
|
|
22812
|
-
const input = await _createMovieClipFrameInput(jta.frames[textureIndex], itemId, resource, encoder);
|
|
22832
|
+
const input = await _createMovieClipFrameInput(jta.frames[textureIndex], itemId, resource, encoder, options.strictOutput);
|
|
22813
22833
|
if (!input) continue;
|
|
22814
22834
|
inputs.push(input);
|
|
22815
22835
|
spriteIdByTextureIndex.set(textureIndex, itemId);
|
|
@@ -22823,7 +22843,7 @@ async function _collectMovieClipFrames(doc, resource, pkg, inputs, encoder, opti
|
|
|
22823
22843
|
}
|
|
22824
22844
|
} else for (let frameIndex = 0; frameIndex < jta.frames.length; frameIndex += 1) {
|
|
22825
22845
|
const itemId = `${mcId}_${frameIndex}`;
|
|
22826
|
-
const input = await _createMovieClipFrameInput(jta.frames[frameIndex], itemId, resource, encoder);
|
|
22846
|
+
const input = await _createMovieClipFrameInput(jta.frames[frameIndex], itemId, resource, encoder, options.strictOutput);
|
|
22827
22847
|
if (!input) continue;
|
|
22828
22848
|
inputs.push(input);
|
|
22829
22849
|
const frame = doc.createMovieFrame(itemId);
|
|
@@ -22835,10 +22855,12 @@ async function _collectMovieClipFrames(doc, resource, pkg, inputs, encoder, opti
|
|
|
22835
22855
|
resource.setHeight(jta.meta?.height ?? 0);
|
|
22836
22856
|
}
|
|
22837
22857
|
} catch {
|
|
22838
|
-
|
|
22858
|
+
const message = `atlas: Could not parse MovieClip "${filePath}".`;
|
|
22859
|
+
if (options.strictOutput) throw new Error(message);
|
|
22860
|
+
logger.warn(`${message} Skipping frames.`);
|
|
22839
22861
|
}
|
|
22840
22862
|
}
|
|
22841
|
-
async function _createMovieClipFrameInput(buffer, itemId, resource, encoder) {
|
|
22863
|
+
async function _createMovieClipFrameInput(buffer, itemId, resource, encoder, strictOutput) {
|
|
22842
22864
|
if (!encoder || buffer.length === 0) return null;
|
|
22843
22865
|
try {
|
|
22844
22866
|
const meta = await encoder(buffer).metadata();
|
|
@@ -22858,6 +22880,7 @@ async function _createMovieClipFrameInput(buffer, itemId, resource, encoder) {
|
|
|
22858
22880
|
sourceKind: "movieclip-frame"
|
|
22859
22881
|
};
|
|
22860
22882
|
} catch {
|
|
22883
|
+
if (strictOutput) throw new Error(`atlas: Could not decode MovieClip frame "${itemId}".`);
|
|
22861
22884
|
return null;
|
|
22862
22885
|
}
|
|
22863
22886
|
}
|
|
@@ -23540,9 +23563,9 @@ function resolveProjectBasePath(basePath) {
|
|
|
23540
23563
|
const normalized = trimTrailingSlashes$2(basePath);
|
|
23541
23564
|
const assetsMatch = normalized.match(/^(.*)[/\\]assets(?:_[^/\\]+)?$/i);
|
|
23542
23565
|
if (assetsMatch?.[1]) return assetsMatch[1];
|
|
23543
|
-
return dirname$
|
|
23566
|
+
return dirname$1(normalized);
|
|
23544
23567
|
}
|
|
23545
|
-
function dirname$
|
|
23568
|
+
function dirname$1(filePath) {
|
|
23546
23569
|
return trimTrailingSlashes$2(filePath).match(/^(.*)[/\\][^/\\]+$/)?.[1] ?? "";
|
|
23547
23570
|
}
|
|
23548
23571
|
function trimTrailingSlashes$2(value) {
|
|
@@ -23687,10 +23710,16 @@ const TRANSPARENT_PNG_1X1 = Uint8Array.from([
|
|
|
23687
23710
|
96,
|
|
23688
23711
|
130
|
|
23689
23712
|
]);
|
|
23713
|
+
function assertSafeRestoreSegment(value, label) {
|
|
23714
|
+
if (!value || value === "." || value === ".." || value.includes("\0") || /[\\/:]/u.test(value)) throw new Error(`restore: Invalid ${label} "${value}".`);
|
|
23715
|
+
}
|
|
23690
23716
|
function normalizeVirtualPath(path) {
|
|
23691
|
-
const
|
|
23692
|
-
if (!
|
|
23693
|
-
|
|
23717
|
+
const raw = (path ?? "").trim();
|
|
23718
|
+
if (!raw || raw === "/") return "";
|
|
23719
|
+
if (raw.includes("\0") || raw.startsWith("\\") || raw.startsWith("//") || /^[a-z]:/iu.test(raw)) throw new Error(`restore: Invalid resource path "${raw}".`);
|
|
23720
|
+
const segments = raw.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
23721
|
+
if (segments.some((segment) => segment === "." || segment === ".." || segment.includes(":"))) throw new Error(`restore: Invalid resource path "${raw}".`);
|
|
23722
|
+
return segments.join("/");
|
|
23694
23723
|
}
|
|
23695
23724
|
function resourceFileName(resource) {
|
|
23696
23725
|
return resource.getFileName?.() || resource.getFile?.() || resource.getName?.() || "";
|
|
@@ -23874,44 +23903,40 @@ function normalizeComparablePath(value) {
|
|
|
23874
23903
|
const joined = segments.join("/");
|
|
23875
23904
|
return (drivePrefix ? `${drivePrefix}/${joined}`.replace(/\/$/, "") : hasRoot ? `/${joined}`.replace(/\/$/, "") : joined || ".").toLowerCase();
|
|
23876
23905
|
}
|
|
23877
|
-
function
|
|
23878
|
-
|
|
23906
|
+
function isPathWithin(root, candidate) {
|
|
23907
|
+
const normalizedRoot = normalizeComparablePath(root);
|
|
23908
|
+
return normalizeComparablePath(candidate).startsWith(`${normalizedRoot}/`);
|
|
23879
23909
|
}
|
|
23880
23910
|
function basename(filePath) {
|
|
23881
23911
|
return trimTrailingSlashes$1(filePath).match(/([^/\\]+)$/)?.[1] ?? "";
|
|
23882
23912
|
}
|
|
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
|
-
}
|
|
23913
|
+
function normalizeRestoreOutputDir(output) {
|
|
23914
|
+
const normalized = trimTrailingSlashes$1(output);
|
|
23915
|
+
const name = basename(normalized);
|
|
23916
|
+
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.");
|
|
23917
|
+
return normalized;
|
|
23918
|
+
}
|
|
23919
|
+
function resolveOutputProjectPath(outputDir, fs) {
|
|
23920
|
+
return fs.join(outputDir, `${basename(outputDir)}.fairy`);
|
|
23921
|
+
}
|
|
23922
|
+
async function resolvePathForContainment(filePath, fs) {
|
|
23923
|
+
const missingSegments = [];
|
|
23924
|
+
let existingPath = filePath;
|
|
23925
|
+
while (!await fs.exists(existingPath)) {
|
|
23926
|
+
const parentPath = fs.dirname(existingPath);
|
|
23927
|
+
if (!parentPath || parentPath === existingPath) return Promise.resolve(fs.resolvePath(filePath));
|
|
23928
|
+
missingSegments.unshift(basename(existingPath));
|
|
23929
|
+
existingPath = parentPath;
|
|
23930
|
+
}
|
|
23931
|
+
const resolvedExistingPath = await Promise.resolve(fs.resolvePath(existingPath));
|
|
23932
|
+
return missingSegments.reduce((resolvedPath, segment) => fs.join(resolvedPath, segment), resolvedExistingPath);
|
|
23933
|
+
}
|
|
23934
|
+
async function assertRestoreOutputDir(inputDir, outputDir, fs, force) {
|
|
23935
|
+
const [resolvedInputDir, resolvedOutputDir] = await Promise.all([resolvePathForContainment(inputDir, fs), resolvePathForContainment(outputDir, fs)]);
|
|
23936
|
+
const normalizedInputDir = normalizeComparablePath(resolvedInputDir);
|
|
23937
|
+
const normalizedOutputDir = normalizeComparablePath(resolvedOutputDir);
|
|
23938
|
+
if (normalizedInputDir === normalizedOutputDir || isPathWithin(normalizedInputDir, normalizedOutputDir) || isPathWithin(normalizedOutputDir, normalizedInputDir)) throw new Error("Restore output directory must be independent from the published input directory.");
|
|
23939
|
+
if (!await fs.exists(outputDir)) return;
|
|
23915
23940
|
let entries;
|
|
23916
23941
|
try {
|
|
23917
23942
|
entries = await fs.readdir(outputDir);
|
|
@@ -23920,23 +23945,63 @@ async function prepareRestoreOutputDir(inputDir, outputDir, outputProjectPath, f
|
|
|
23920
23945
|
}
|
|
23921
23946
|
if (entries.length === 0) return;
|
|
23922
23947
|
if (!force) throw new Error(`Restore output directory is not empty: ${outputDir}. Use --force to overwrite it.`);
|
|
23923
|
-
|
|
23924
|
-
|
|
23925
|
-
|
|
23926
|
-
|
|
23927
|
-
|
|
23928
|
-
|
|
23948
|
+
}
|
|
23949
|
+
async function createRestoreStagingDir(outputDir, fs) {
|
|
23950
|
+
const parentDir = fs.dirname(outputDir) || ".";
|
|
23951
|
+
await fs.mkdir(parentDir);
|
|
23952
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
23953
|
+
const stagingDir = fs.join(parentDir, `.${basename(outputDir)}.restore-${generateId()}`);
|
|
23954
|
+
if (await fs.exists(stagingDir)) continue;
|
|
23955
|
+
await fs.mkdir(stagingDir);
|
|
23956
|
+
return stagingDir;
|
|
23957
|
+
}
|
|
23958
|
+
throw new Error(`restore: Could not allocate a staging directory beside ${outputDir}.`);
|
|
23959
|
+
}
|
|
23960
|
+
async function commitRestoreOutput(stagingDir, outputDir, fs) {
|
|
23961
|
+
if (!await fs.exists(outputDir)) {
|
|
23962
|
+
await fs.rename(stagingDir, outputDir);
|
|
23963
|
+
return null;
|
|
23964
|
+
}
|
|
23965
|
+
const parentDir = fs.dirname(outputDir) || ".";
|
|
23966
|
+
let backupDir = "";
|
|
23967
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
23968
|
+
const candidate = fs.join(parentDir, `.${basename(outputDir)}.restore-backup-${generateId()}`);
|
|
23969
|
+
if (!await fs.exists(candidate)) {
|
|
23970
|
+
backupDir = candidate;
|
|
23971
|
+
break;
|
|
23972
|
+
}
|
|
23973
|
+
}
|
|
23974
|
+
if (!backupDir) throw new Error(`restore: Could not allocate a backup directory beside ${outputDir}.`);
|
|
23975
|
+
await fs.rename(outputDir, backupDir);
|
|
23976
|
+
try {
|
|
23977
|
+
await fs.rename(stagingDir, outputDir);
|
|
23978
|
+
} catch (error) {
|
|
23979
|
+
await fs.rename(backupDir, outputDir);
|
|
23980
|
+
throw error;
|
|
23981
|
+
}
|
|
23982
|
+
try {
|
|
23983
|
+
await fs.rm(backupDir, {
|
|
23984
|
+
recursive: true,
|
|
23985
|
+
force: true
|
|
23986
|
+
});
|
|
23987
|
+
return null;
|
|
23988
|
+
} catch {
|
|
23989
|
+
return `restore: Previous output retained at ${backupDir}; remove it after checking the restored project.`;
|
|
23990
|
+
}
|
|
23929
23991
|
}
|
|
23930
23992
|
async function restore(options) {
|
|
23931
23993
|
const sourceDir = trimTrailingSlashes$1(options.inputDir);
|
|
23932
|
-
const
|
|
23933
|
-
const outputProjectPath = resolveOutputProjectPath(
|
|
23934
|
-
await
|
|
23994
|
+
const outputDir = normalizeRestoreOutputDir(options.output);
|
|
23995
|
+
const outputProjectPath = resolveOutputProjectPath(outputDir, options.fs);
|
|
23996
|
+
await assertRestoreOutputDir(sourceDir, outputDir, options.fs, options.force === true);
|
|
23935
23997
|
const packageFilter = options.packages?.length ? new Set(options.packages) : null;
|
|
23936
|
-
const
|
|
23998
|
+
const binaryNames = (await options.fs.readdir(sourceDir)).filter((name) => isPublishedBinaryFile(name)).filter((name) => !packageFilter || packageFilter.has(inferPackageName(name)));
|
|
23999
|
+
for (const binaryName of binaryNames) assertSafeRestoreSegment(binaryName, "published binary file name");
|
|
24000
|
+
const candidateBinaryPaths = binaryNames.map((name) => options.fs.join(sourceDir, name)).sort((left, right) => left.localeCompare(right));
|
|
23937
24001
|
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
24002
|
if (binaryPaths.length === 0) throw new Error(`No FairyGUI published binary files found in ${sourceDir}.`);
|
|
23939
|
-
|
|
24003
|
+
const restorer = new RestoreWorkflow(options.fs);
|
|
24004
|
+
const document = await restorer.prepare({
|
|
23940
24005
|
binaryPaths,
|
|
23941
24006
|
sourceDir,
|
|
23942
24007
|
outputProjectPath,
|
|
@@ -23944,18 +24009,45 @@ async function restore(options) {
|
|
|
23944
24009
|
cropImage: options.cropImage,
|
|
23945
24010
|
extractImage: options.extractImage
|
|
23946
24011
|
});
|
|
24012
|
+
const stagingDir = await createRestoreStagingDir(outputDir, options.fs);
|
|
24013
|
+
const stagingProjectPath = options.fs.join(stagingDir, basename(outputProjectPath));
|
|
24014
|
+
const warnings = [];
|
|
24015
|
+
try {
|
|
24016
|
+
await restorer.write(document, {
|
|
24017
|
+
binaryPaths,
|
|
24018
|
+
sourceDir,
|
|
24019
|
+
outputProjectPath: stagingProjectPath,
|
|
24020
|
+
projectType: options.projectType,
|
|
24021
|
+
cropImage: options.cropImage,
|
|
24022
|
+
extractImage: options.extractImage
|
|
24023
|
+
}, warnings);
|
|
24024
|
+
const cleanupWarning = await commitRestoreOutput(stagingDir, outputDir, options.fs);
|
|
24025
|
+
if (cleanupWarning) warnings.push(cleanupWarning);
|
|
24026
|
+
} catch (error) {
|
|
24027
|
+
await options.fs.rm(stagingDir, {
|
|
24028
|
+
recursive: true,
|
|
24029
|
+
force: true
|
|
24030
|
+
}).catch(() => void 0);
|
|
24031
|
+
throw error;
|
|
24032
|
+
}
|
|
24033
|
+
return {
|
|
24034
|
+
document,
|
|
24035
|
+
projectPath: outputProjectPath,
|
|
24036
|
+
warnings
|
|
24037
|
+
};
|
|
23947
24038
|
}
|
|
23948
24039
|
var RestoreWorkflow = class {
|
|
23949
24040
|
_fs;
|
|
23950
24041
|
constructor(fs) {
|
|
23951
24042
|
this._fs = fs;
|
|
23952
24043
|
}
|
|
23953
|
-
async
|
|
23954
|
-
const warnings = [];
|
|
24044
|
+
async prepare(options) {
|
|
23955
24045
|
const doc = await new BinaryReader(this._fs).readMany(options.binaryPaths);
|
|
24046
|
+
this._assertDocumentPaths(doc);
|
|
23956
24047
|
this._initializeProjectDefaults(doc, options.projectType);
|
|
23957
24048
|
this._initializeImageFileNames(doc);
|
|
23958
24049
|
this._initializeLooseResourceFileNames(doc);
|
|
24050
|
+
this._assertDocumentPaths(doc);
|
|
23959
24051
|
await this._synthesizeLooseSkeletonResources(doc, options.sourceDir);
|
|
23960
24052
|
this._initializeRestoredResourceRelations(doc);
|
|
23961
24053
|
this._initializePublishedFontTextureIds(doc);
|
|
@@ -23964,13 +24056,27 @@ var RestoreWorkflow = class {
|
|
|
23964
24056
|
this._initializePublishedTextFontResources(doc);
|
|
23965
24057
|
this._initializeDisplayObjectFileNames(doc);
|
|
23966
24058
|
this._initializePublishedFontDefaults(doc);
|
|
24059
|
+
this._assertDocumentPaths(doc);
|
|
24060
|
+
return doc;
|
|
24061
|
+
}
|
|
24062
|
+
async write(doc, options, warnings) {
|
|
23967
24063
|
await new ProjectWriter(this._fs).write(doc, options.outputProjectPath);
|
|
23968
24064
|
await this._restoreAssets(doc, options, warnings);
|
|
23969
|
-
|
|
23970
|
-
|
|
23971
|
-
|
|
23972
|
-
|
|
23973
|
-
|
|
24065
|
+
}
|
|
24066
|
+
_assertDocumentPaths(doc) {
|
|
24067
|
+
for (const pkg of doc.getRoot().listPackages()) {
|
|
24068
|
+
assertSafeRestoreSegment(pkg.getName(), "package name");
|
|
24069
|
+
assertSafeRestoreSegment(pkg.getPublishName() || pkg.getName(), "package publish name");
|
|
24070
|
+
for (const resource of pkg.listResources()) {
|
|
24071
|
+
normalizeVirtualPath(resource.getPath?.());
|
|
24072
|
+
const branch = resource.getBranch?.() ?? "";
|
|
24073
|
+
if (branch) assertSafeRestoreSegment(branch, "branch name");
|
|
24074
|
+
const fileName = resourceFileName(resource);
|
|
24075
|
+
if (fileName) assertSafeRestoreSegment(fileName, "resource file name");
|
|
24076
|
+
const publishedFileName = resourcePublishedFileName(resource);
|
|
24077
|
+
if (publishedFileName) assertSafeRestoreSegment(publishedFileName, "published resource file name");
|
|
24078
|
+
}
|
|
24079
|
+
}
|
|
23974
24080
|
}
|
|
23975
24081
|
_initializeProjectDefaults(doc, projectType) {
|
|
23976
24082
|
doc.getRoot().setProjectId(generateId()).setProjectType(projectType ?? ProjectType.Unity).setVersion("3.0").setSettings({
|
|
@@ -24404,27 +24510,40 @@ var RestoreWorkflow = class {
|
|
|
24404
24510
|
}
|
|
24405
24511
|
_sourceFileCandidates(pkg, fileName, outputFileName = fileName) {
|
|
24406
24512
|
const publishName = pkg.getPublishName() || pkg.getName();
|
|
24407
|
-
|
|
24513
|
+
assertSafeRestoreSegment(publishName, "package publish name");
|
|
24514
|
+
assertSafeRestoreSegment(fileName, "published source file name");
|
|
24515
|
+
assertSafeRestoreSegment(outputFileName, "published source file name");
|
|
24516
|
+
const candidates = Array.from(new Set([
|
|
24408
24517
|
`${publishName}_${fileName}`,
|
|
24409
24518
|
fileName,
|
|
24410
24519
|
`${publishName}_${outputFileName}`,
|
|
24411
24520
|
outputFileName
|
|
24412
24521
|
]));
|
|
24522
|
+
for (const candidate of candidates) assertSafeRestoreSegment(candidate, "published source file name");
|
|
24523
|
+
return candidates;
|
|
24413
24524
|
}
|
|
24414
24525
|
async _resolveLooseSourceFile(pkg, sourceDir, outputFileName) {
|
|
24415
24526
|
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
24527
|
return this._resolveSourceFile(sourceDir, candidates);
|
|
24417
24528
|
}
|
|
24418
24529
|
async _resolveSourceFile(sourceDir, candidates) {
|
|
24530
|
+
const resolvedSourceDir = await Promise.resolve(this._fs.resolvePath(sourceDir));
|
|
24419
24531
|
for (const candidate of candidates) {
|
|
24532
|
+
assertSafeRestoreSegment(candidate, "published source file name");
|
|
24420
24533
|
const sourcePath = this._fs.join(sourceDir, candidate);
|
|
24421
|
-
if (await this._fs.isFile(sourcePath))
|
|
24534
|
+
if (!await this._fs.isFile(sourcePath)) continue;
|
|
24535
|
+
const resolvedSourcePath = await Promise.resolve(this._fs.resolvePath(sourcePath));
|
|
24536
|
+
if (!isPathWithin(resolvedSourceDir, resolvedSourcePath)) throw new Error(`restore: Published source file resolves outside the input directory: ${candidate}.`);
|
|
24537
|
+
return resolvedSourcePath;
|
|
24422
24538
|
}
|
|
24423
24539
|
return null;
|
|
24424
24540
|
}
|
|
24425
24541
|
_resourceOutputPath(outputProjectPath, pkg, resource, fileName) {
|
|
24426
24542
|
const basePath = this._fs.dirname(outputProjectPath);
|
|
24427
24543
|
const branch = resource.getBranch?.() ?? "";
|
|
24544
|
+
assertSafeRestoreSegment(pkg.getName(), "package name");
|
|
24545
|
+
if (branch) assertSafeRestoreSegment(branch, "branch name");
|
|
24546
|
+
assertSafeRestoreSegment(fileName, "resource file name");
|
|
24428
24547
|
const assetsDir = branch ? `assets_${branch}` : "assets";
|
|
24429
24548
|
const virtualPath = normalizeVirtualPath(resource.getPath?.());
|
|
24430
24549
|
const pkgDir = this._fs.join(basePath, assetsDir, pkg.getName());
|
|
@@ -24991,13 +25110,13 @@ function getPublishedSkeletonDependencyImageIds(pkg, publishedResourceIds) {
|
|
|
24991
25110
|
}
|
|
24992
25111
|
return imageIds;
|
|
24993
25112
|
}
|
|
24994
|
-
async function exportPackageSounds(pkg, outputDir, basePath, fs, readFileRaw
|
|
25113
|
+
async function exportPackageSounds(pkg, outputDir, basePath, fs, readFileRaw) {
|
|
24995
25114
|
const publishedResourceIds = getAnnotatedPublishedResourceIds(pkg);
|
|
24996
25115
|
if (publishedResourceIds.size === 0) return;
|
|
24997
25116
|
if (!basePath || !readFileRaw) {
|
|
24998
25117
|
if (pkg.listResources().some((resource) => {
|
|
24999
25118
|
return isSoundResource(resource) && publishedResourceIds.has(resource.getId());
|
|
25000
|
-
}))
|
|
25119
|
+
})) throw new Error(`publish: Sound resources in package "${pkg.getName()}" require basePath and readFileRaw for output.`);
|
|
25001
25120
|
return;
|
|
25002
25121
|
}
|
|
25003
25122
|
for (const resource of pkg.listResources()) {
|
|
@@ -25010,18 +25129,18 @@ async function exportPackageSounds(pkg, outputDir, basePath, fs, readFileRaw, lo
|
|
|
25010
25129
|
const data = await readFileRaw(sourcePath);
|
|
25011
25130
|
await fs.writeFileRaw(targetPath, data);
|
|
25012
25131
|
} catch {
|
|
25013
|
-
|
|
25132
|
+
throw new Error(`publish: Could not export sound "${resource.getId()}" from package "${pkg.getName()}".`);
|
|
25014
25133
|
}
|
|
25015
25134
|
}
|
|
25016
25135
|
}
|
|
25017
|
-
async function exportPackageExternalResources(pkg, outputDir, basePath, fs, readFileRaw
|
|
25136
|
+
async function exportPackageExternalResources(pkg, outputDir, basePath, fs, readFileRaw) {
|
|
25018
25137
|
const exportedResourceIds = getAnnotatedExportedResourceIds(pkg);
|
|
25019
25138
|
const skeletonDependencyImageIds = getPublishedSkeletonDependencyImageIds(pkg, exportedResourceIds);
|
|
25020
25139
|
if (exportedResourceIds.size === 0) return;
|
|
25021
25140
|
if (!basePath || !readFileRaw) {
|
|
25022
25141
|
if (pkg.listResources().some((resource) => {
|
|
25023
25142
|
return (isMiscResource(resource) || isSkeletonResource(resource)) && exportedResourceIds.has(resource.getId()) || skeletonDependencyImageIds.has(resource.getId());
|
|
25024
|
-
}))
|
|
25143
|
+
})) throw new Error(`publish: External resources in package "${pkg.getName()}" require basePath and readFileRaw for output.`);
|
|
25025
25144
|
return;
|
|
25026
25145
|
}
|
|
25027
25146
|
for (const resource of pkg.listResources()) {
|
|
@@ -25043,7 +25162,7 @@ async function exportPackageExternalResources(pkg, outputDir, basePath, fs, read
|
|
|
25043
25162
|
const data = await readFileRaw(sourcePath);
|
|
25044
25163
|
await fs.writeFileRaw(targetPath, data);
|
|
25045
25164
|
} catch {
|
|
25046
|
-
|
|
25165
|
+
throw new Error(`publish: Could not export external resource "${resource.getId()}" from package "${pkg.getName()}".`);
|
|
25047
25166
|
}
|
|
25048
25167
|
}
|
|
25049
25168
|
}
|
|
@@ -25059,18 +25178,17 @@ async function exportPackageExternalResources(pkg, outputDir, basePath, fs, read
|
|
|
25059
25178
|
* `publishNode()` or `publishBrowser()` through their dedicated entries.
|
|
25060
25179
|
*
|
|
25061
25180
|
* ```ts
|
|
25062
|
-
* import
|
|
25063
|
-
*
|
|
25064
|
-
* const doc = await
|
|
25181
|
+
* import { NodeIO } from '@openfairygui/core/node';
|
|
25182
|
+
* import { publishNode } from '@openfairygui/functions/node';
|
|
25183
|
+
* const doc = await new NodeIO().readProject('./project.fairy');
|
|
25065
25184
|
*
|
|
25066
|
-
* await
|
|
25185
|
+
* await publishNode({
|
|
25186
|
+
* document: doc,
|
|
25067
25187
|
* output: './release/',
|
|
25068
25188
|
* compressed: true,
|
|
25069
|
-
*
|
|
25070
|
-
* basePath: './assets/',
|
|
25189
|
+
* assetsPath: './assets/',
|
|
25071
25190
|
* fileExtension: 'bytes',
|
|
25072
|
-
*
|
|
25073
|
-
* }));
|
|
25191
|
+
* });
|
|
25074
25192
|
* ```
|
|
25075
25193
|
*/
|
|
25076
25194
|
function publish(options) {
|
|
@@ -25138,6 +25256,12 @@ function publish(options) {
|
|
|
25138
25256
|
}
|
|
25139
25257
|
});
|
|
25140
25258
|
const publishPackage = async (plan, writerFs, packageIndex) => {
|
|
25259
|
+
if (options.fs && !plan.outputDir) throw new Error("publish: no output directory resolved. Provide --output, or configure global publish.path / package publishPath.");
|
|
25260
|
+
if (options.fs) {
|
|
25261
|
+
await options.fs.mkdir(plan.outputDir);
|
|
25262
|
+
await exportPackageSounds(plan.pkg, plan.outputDir, options.basePath, options.fs, options.atlas?.readFileRaw ?? options.fs.readFileRaw);
|
|
25263
|
+
await exportPackageExternalResources(plan.pkg, plan.outputDir, options.basePath, options.fs, options.atlas?.readFileRaw ?? options.fs.readFileRaw);
|
|
25264
|
+
}
|
|
25141
25265
|
const atlasRuntimeOptions = resolvePublishAtlasRuntimeOptions(plan.fileExtension);
|
|
25142
25266
|
await atlas({
|
|
25143
25267
|
...plan.atlas,
|
|
@@ -25148,20 +25272,17 @@ function publish(options) {
|
|
|
25148
25272
|
outputPath: options.fs ? plan.outputDir : void 0,
|
|
25149
25273
|
mkdir: options.fs ? options.fs.mkdir : void 0,
|
|
25150
25274
|
readFileRaw: options.atlas?.readFileRaw ?? options.fs?.readFileRaw,
|
|
25275
|
+
strictOutput: options.fs !== void 0,
|
|
25151
25276
|
packages: [plan.pkg.getName()],
|
|
25152
25277
|
...atlasRuntimeOptions
|
|
25153
25278
|
})(doc);
|
|
25154
25279
|
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
25280
|
const filePath = options.fs.join(plan.outputDir, plan.fileName);
|
|
25158
25281
|
const bwOptions = {
|
|
25159
25282
|
compressed: plan.compressed,
|
|
25160
25283
|
packageIndex
|
|
25161
25284
|
};
|
|
25162
25285
|
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
25286
|
logger.info(`publish: Written ${plan.fileName}`);
|
|
25166
25287
|
};
|
|
25167
25288
|
const root = doc.getRoot();
|
|
@@ -25194,7 +25315,9 @@ function publish(options) {
|
|
|
25194
25315
|
}
|
|
25195
25316
|
const plans = allPackages.map((pkg) => resolvePackagePublishPlan(pkg, resolved, projectBasePath));
|
|
25196
25317
|
if (!options.fs) {
|
|
25197
|
-
|
|
25318
|
+
const outputPlan = plans.find((plan) => !!plan.outputDir);
|
|
25319
|
+
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.`);
|
|
25320
|
+
logger.info(`publish: Layout computed for ${allPackages.length} package(s); no output directory was requested.`);
|
|
25198
25321
|
const noopWriterFs = toBinaryWriterFileSystem(createNoopPublishFs());
|
|
25199
25322
|
for (const plan of plans) await publishPackage(plan, noopWriterFs, allDocPackages.indexOf(plan.pkg));
|
|
25200
25323
|
await runPublishPluginHook(plugins, "onPublishEnd", doc, options);
|
|
@@ -25548,7 +25671,7 @@ async function publishNode(options) {
|
|
|
25548
25671
|
const { document, assetsPath: configuredAssetsPath, atlas, encoder: configuredEncoder, plugins: configuredPlugins, ...publishOptions } = options;
|
|
25549
25672
|
const [fileSystem, assetsPath] = await Promise.all([createNodePublishFileSystem(), resolveNodeAssetsPath(document, configuredAssetsPath)]);
|
|
25550
25673
|
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)
|
|
25674
|
+
if (!encoder) throw new Error("publishNode: Sharp is required for a complete publish. Install sharp or provide an encoder.");
|
|
25552
25675
|
await document.transform(publish({
|
|
25553
25676
|
...publishOptions,
|
|
25554
25677
|
basePath: assetsPath,
|
|
@@ -25624,7 +25747,7 @@ function registerPublishCommand(program) {
|
|
|
25624
25747
|
//#endregion
|
|
25625
25748
|
//#region src/commands/restore.ts
|
|
25626
25749
|
function registerRestoreCommand(program) {
|
|
25627
|
-
program.command("restore").description("
|
|
25750
|
+
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
25751
|
const inputDir = path.resolve(releaseDir);
|
|
25629
25752
|
const outputDir = path.resolve(options.output);
|
|
25630
25753
|
const pkgFilter = options.packages ? options.packages.split(",").map((value) => value.trim()).filter(Boolean) : void 0;
|
|
@@ -25698,6 +25821,9 @@ function createNodeRestoreFs() {
|
|
|
25698
25821
|
force: options?.force ?? false
|
|
25699
25822
|
});
|
|
25700
25823
|
},
|
|
25824
|
+
async rename(from, to) {
|
|
25825
|
+
await fs.rename(from, to);
|
|
25826
|
+
},
|
|
25701
25827
|
join(...paths) {
|
|
25702
25828
|
return path.join(...paths);
|
|
25703
25829
|
},
|
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.15",
|
|
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.15",
|
|
37
|
+
"@openfairygui/core": "0.2.0-alpha.15"
|
|
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.15"
|
|
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
|
},
|