@tinycloud/cli 1.0.1-beta.0 → 1.0.1-beta.1
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/dist/index.js +125 -43
- package/dist/index.js.map +1 -1
- package/dist/legacy-entry.js +134 -58
- package/dist/legacy-entry.js.map +1 -1
- package/package.json +2 -2
- package/skills/tc-cli/AUTH.md +1 -0
package/dist/legacy-entry.js
CHANGED
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
tinycloudConfigPath,
|
|
38
38
|
tinycloudHomePath
|
|
39
39
|
} from "@tinycloud/operations/state";
|
|
40
|
-
var CONFIG_DIR, PROFILES_DIR, CONFIG_FILE, DEFAULT_HOST, DEFAULT_OPENKEY_HOST, DEFAULT_OPENKEY_DEVICE_API_HOST, DEFAULT_SHARE_ORIGIN, DEFAULT_PROFILE, DEFAULT_CHAIN_ID, ExitCode;
|
|
40
|
+
var CONFIG_DIR, PROFILES_DIR, CONFIG_FILE, DEFAULT_HOST, DEFAULT_OPENKEY_HOST, DEFAULT_OPENKEY_DEVICE_API_HOST, DEFAULT_SHARE_ORIGIN, DEFAULT_PROFILE, DEFAULT_CHAIN_ID, PROFILE_COMMIT_LOCK_TIMEOUT_MS, ExitCode;
|
|
41
41
|
var init_constants = __esm({
|
|
42
42
|
"src/config/constants.ts"() {
|
|
43
43
|
"use strict";
|
|
@@ -50,6 +50,7 @@ var init_constants = __esm({
|
|
|
50
50
|
DEFAULT_SHARE_ORIGIN = "https://share.tinycloud.xyz";
|
|
51
51
|
DEFAULT_PROFILE = "default";
|
|
52
52
|
DEFAULT_CHAIN_ID = 1;
|
|
53
|
+
PROFILE_COMMIT_LOCK_TIMEOUT_MS = 45e3;
|
|
53
54
|
ExitCode = {
|
|
54
55
|
SUCCESS: 0,
|
|
55
56
|
ERROR: 1,
|
|
@@ -104,9 +105,6 @@ async function fileExists(filePath) {
|
|
|
104
105
|
async function ensureDir(dirPath) {
|
|
105
106
|
await mkdir(dirPath, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
106
107
|
}
|
|
107
|
-
async function removeDir(dirPath) {
|
|
108
|
-
await rm(dirPath, { recursive: true, force: true });
|
|
109
|
-
}
|
|
110
108
|
async function listDirs(dirPath) {
|
|
111
109
|
try {
|
|
112
110
|
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
@@ -641,9 +639,10 @@ var init_host = __esm({
|
|
|
641
639
|
});
|
|
642
640
|
|
|
643
641
|
// src/config/profiles.ts
|
|
644
|
-
import { chmod, rm as rm2 } from "fs/promises";
|
|
642
|
+
import { chmod, lstat, readdir as readdir2, rm as rm2, rmdir } from "fs/promises";
|
|
645
643
|
import { join as join3 } from "path";
|
|
646
644
|
import {
|
|
645
|
+
profilePath,
|
|
647
646
|
readSession,
|
|
648
647
|
removeSession,
|
|
649
648
|
withProfileLock,
|
|
@@ -719,8 +718,8 @@ var init_profiles = __esm({
|
|
|
719
718
|
* Throws CLIError if the profile doesn't exist.
|
|
720
719
|
*/
|
|
721
720
|
static async getProfile(name) {
|
|
722
|
-
const
|
|
723
|
-
const profile = await readJson(
|
|
721
|
+
const profilePath2 = join3(PROFILES_DIR, name, "profile.json");
|
|
722
|
+
const profile = await readJson(profilePath2);
|
|
724
723
|
if (!profile) {
|
|
725
724
|
throw new CLIError(
|
|
726
725
|
"PROFILE_NOT_FOUND",
|
|
@@ -755,10 +754,27 @@ var init_profiles = __esm({
|
|
|
755
754
|
return listDirs(PROFILES_DIR);
|
|
756
755
|
}
|
|
757
756
|
/**
|
|
758
|
-
* Deletes a profile
|
|
759
|
-
*
|
|
757
|
+
* Deletes a profile. Its key, session, settings, stores and cache are
|
|
758
|
+
* removed while holding the profile lock, so another writer's critical
|
|
759
|
+
* section never sees them vanish midway: session and key first, settings
|
|
760
|
+
* last, so a crash midway never leaves a session or key without its
|
|
761
|
+
* profile. `.lock` itself is left to the lock's release; the then-empty
|
|
762
|
+
* directory is removed afterwards unless another writer took the lock (or
|
|
763
|
+
* wrote) meanwhile. A profile directory that is a symlink is unlinked, its
|
|
764
|
+
* target left alone.
|
|
765
|
+
* Throws if the name is not one path segment or names the default profile.
|
|
760
766
|
*/
|
|
761
767
|
static async deleteProfile(name) {
|
|
768
|
+
let profileDir;
|
|
769
|
+
try {
|
|
770
|
+
profileDir = profilePath(name);
|
|
771
|
+
} catch {
|
|
772
|
+
throw new CLIError(
|
|
773
|
+
"INVALID_PROFILE_NAME",
|
|
774
|
+
`Invalid profile name "${name}": a profile name is one path segment (no "/", "\\", "." or "..").`,
|
|
775
|
+
ExitCode.USAGE_ERROR
|
|
776
|
+
);
|
|
777
|
+
}
|
|
762
778
|
const config = await _ProfileManager.getConfig();
|
|
763
779
|
if (config.defaultProfile === name) {
|
|
764
780
|
throw new CLIError(
|
|
@@ -766,8 +782,24 @@ var init_profiles = __esm({
|
|
|
766
782
|
`Cannot delete the default profile "${name}". Change the default first with \`tc profile default <other>\`.`
|
|
767
783
|
);
|
|
768
784
|
}
|
|
769
|
-
const
|
|
770
|
-
|
|
785
|
+
const isLink = await lstat(profileDir).then((stats) => stats.isSymbolicLink(), (error) => {
|
|
786
|
+
if (error.code === "ENOENT") return false;
|
|
787
|
+
throw error;
|
|
788
|
+
});
|
|
789
|
+
if (isLink) {
|
|
790
|
+
await rm2(profileDir, { force: true });
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
await _ProfileManager.withLock(name, async () => {
|
|
794
|
+
for (const file of ["session.json", "key.json"]) await rm2(join3(profileDir, file), { force: true });
|
|
795
|
+
for (const entry of await readdir2(profileDir)) {
|
|
796
|
+
if (entry !== ".lock" && entry !== "profile.json") await rm2(join3(profileDir, entry), { recursive: true, force: true });
|
|
797
|
+
}
|
|
798
|
+
await rm2(join3(profileDir, "profile.json"), { force: true });
|
|
799
|
+
});
|
|
800
|
+
await rmdir(profileDir).catch((error) => {
|
|
801
|
+
if (error.code !== "ENOENT" && error.code !== "ENOTEMPTY" && error.code !== "EEXIST") throw error;
|
|
802
|
+
});
|
|
771
803
|
}
|
|
772
804
|
// ── Key management ──────────────────────────────────────────────────
|
|
773
805
|
/**
|
|
@@ -13493,39 +13525,89 @@ async function createSDKInstance(ctx, options) {
|
|
|
13493
13525
|
return node;
|
|
13494
13526
|
}
|
|
13495
13527
|
async function bootstrapDelegatedSession(ctx, delegation) {
|
|
13496
|
-
const
|
|
13497
|
-
|
|
13498
|
-
|
|
13499
|
-
|
|
13500
|
-
|
|
13501
|
-
|
|
13502
|
-
|
|
13503
|
-
|
|
13504
|
-
|
|
13505
|
-
|
|
13506
|
-
|
|
13507
|
-
|
|
13508
|
-
|
|
13509
|
-
|
|
13510
|
-
|
|
13511
|
-
|
|
13512
|
-
|
|
13513
|
-
|
|
13514
|
-
|
|
13515
|
-
|
|
13528
|
+
const written = await ProfileManager.withLock(ctx.profile, async () => {
|
|
13529
|
+
const previousProfile = await ProfileManager.getProfile(ctx.profile);
|
|
13530
|
+
if (resolveProfilePosture(previousProfile) !== "delegate-session") {
|
|
13531
|
+
throw new CLIError(
|
|
13532
|
+
"AUTH_REQUIRED",
|
|
13533
|
+
`Profile "${ctx.profile}" is not a delegate-session profile.`,
|
|
13534
|
+
ExitCode.AUTH_REQUIRED
|
|
13535
|
+
);
|
|
13536
|
+
}
|
|
13537
|
+
const sessionDid = previousProfile.sessionDid ?? previousProfile.did;
|
|
13538
|
+
if (delegation.delegateDID.split("#", 1)[0] !== sessionDid.split("#", 1)[0]) {
|
|
13539
|
+
throw new CLIError(
|
|
13540
|
+
"DELEGATION_AUDIENCE_MISMATCH",
|
|
13541
|
+
`Delegation targets ${delegation.delegateDID}, but profile "${ctx.profile}" uses ${sessionDid}.`,
|
|
13542
|
+
ExitCode.PERMISSION_DENIED
|
|
13543
|
+
);
|
|
13544
|
+
}
|
|
13545
|
+
if (await ProfileManager.getSession(ctx.profile) !== null) {
|
|
13546
|
+
throw new CLIError(
|
|
13547
|
+
"PROFILE_CHANGED_DURING_IMPORT",
|
|
13548
|
+
`Profile "${ctx.profile}" gained a session (another login or import) after this import checked it. Nothing was saved; run the import again.`,
|
|
13549
|
+
ExitCode.ERROR
|
|
13550
|
+
);
|
|
13551
|
+
}
|
|
13552
|
+
const jwk = signerJwkForProfile(ctx.profile, void 0, await ProfileManager.getKey(ctx.profile));
|
|
13553
|
+
const session = {
|
|
13516
13554
|
delegationHeader: delegation.delegationHeader,
|
|
13517
13555
|
delegationCid: delegation.cid,
|
|
13518
13556
|
spaceId: delegation.spaceId,
|
|
13519
13557
|
jwk,
|
|
13520
13558
|
verificationMethod: sessionDid
|
|
13521
|
-
}
|
|
13522
|
-
|
|
13523
|
-
|
|
13524
|
-
|
|
13525
|
-
|
|
13526
|
-
})
|
|
13559
|
+
};
|
|
13560
|
+
const profile = { ...previousProfile, sessionDid, spaceId: delegation.spaceId };
|
|
13561
|
+
try {
|
|
13562
|
+
await ProfileManager.setSession(ctx.profile, session);
|
|
13563
|
+
await ProfileManager.setProfile(ctx.profile, profile);
|
|
13564
|
+
} catch (error) {
|
|
13565
|
+
throw annotate(error, await restoreBeforeBootstrap(ctx.profile, previousProfile));
|
|
13566
|
+
}
|
|
13567
|
+
return { previousProfile, profile, session };
|
|
13527
13568
|
});
|
|
13528
|
-
|
|
13569
|
+
const abandon = async (cause) => {
|
|
13570
|
+
const note = await ProfileManager.withLock(ctx.profile, async () => {
|
|
13571
|
+
const profile = await ProfileManager.getProfile(ctx.profile).catch((error) => {
|
|
13572
|
+
if (error instanceof CLIError && error.code === "PROFILE_NOT_FOUND") return null;
|
|
13573
|
+
throw error;
|
|
13574
|
+
});
|
|
13575
|
+
const session = await ProfileManager.getSession(ctx.profile);
|
|
13576
|
+
if (JSON.stringify(profile) !== JSON.stringify(written.profile) || JSON.stringify(session) !== JSON.stringify(written.session)) {
|
|
13577
|
+
return `Profile "${ctx.profile}" changed while the import was pending (another login, logout or profile update), so its newer state was kept and the provisional session was not rolled back.`;
|
|
13578
|
+
}
|
|
13579
|
+
return restoreBeforeBootstrap(ctx.profile, written.previousProfile);
|
|
13580
|
+
}, { timeoutMs: PROFILE_COMMIT_LOCK_TIMEOUT_MS }).catch((error) => (
|
|
13581
|
+
// The lock or a read failed: the import's error stays the one reported.
|
|
13582
|
+
`Rolling back the provisional session of profile "${ctx.profile}" could not run (${error instanceof Error ? error.message : String(error)}); check \`tc --profile ${ctx.profile} context\`.`
|
|
13583
|
+
));
|
|
13584
|
+
throw annotate(cause, note);
|
|
13585
|
+
};
|
|
13586
|
+
let node;
|
|
13587
|
+
try {
|
|
13588
|
+
node = await createSDKInstance(ctx);
|
|
13589
|
+
} catch (error) {
|
|
13590
|
+
return abandon(error);
|
|
13591
|
+
}
|
|
13592
|
+
return { node, abandon };
|
|
13593
|
+
}
|
|
13594
|
+
async function restoreBeforeBootstrap(profileName, previousProfile) {
|
|
13595
|
+
const failures = [];
|
|
13596
|
+
for (const write of [
|
|
13597
|
+
() => ProfileManager.clearSession(profileName),
|
|
13598
|
+
() => ProfileManager.setProfile(profileName, previousProfile)
|
|
13599
|
+
]) {
|
|
13600
|
+
await write().catch((error) => {
|
|
13601
|
+
failures.push(error instanceof Error ? error.message : String(error));
|
|
13602
|
+
});
|
|
13603
|
+
}
|
|
13604
|
+
if (failures.length === 0) return void 0;
|
|
13605
|
+
return `Rolling back the provisional session of profile "${profileName}" failed too (${failures.join("; ")}); check \`tc --profile ${profileName} context\`.`;
|
|
13606
|
+
}
|
|
13607
|
+
function annotate(error, note) {
|
|
13608
|
+
if (note === void 0) return error;
|
|
13609
|
+
const cause = wrapError(error);
|
|
13610
|
+
return new CLIError(cause.code, `${cause.message} ${note}`, cause.exitCode, cause.metadata);
|
|
13529
13611
|
}
|
|
13530
13612
|
async function ensureAuthenticated(ctx, options) {
|
|
13531
13613
|
if (options?.privateKey) {
|
|
@@ -14590,7 +14672,6 @@ function assertSessionReplaceable(profileName, snapshot, ownerDid, scope, newExp
|
|
|
14590
14672
|
ExitCode.USAGE_ERROR
|
|
14591
14673
|
);
|
|
14592
14674
|
}
|
|
14593
|
-
var COMMIT_LOCK_TIMEOUT_MS = 45e3;
|
|
14594
14675
|
async function restore(profileName, state) {
|
|
14595
14676
|
const writes = [
|
|
14596
14677
|
() => state.key === null ? ProfileManager.removeKey(profileName) : ProfileManager.setKey(profileName, state.key),
|
|
@@ -14616,7 +14697,7 @@ async function commitLogin(profileName, snapshot, commit) {
|
|
|
14616
14697
|
if (canonicalJson(current.profile) !== canonicalJson(snapshot.profile) || canonicalJson(current.key) !== canonicalJson(snapshot.key) || canonicalJson(current.session) !== canonicalJson(snapshot.session)) {
|
|
14617
14698
|
throw new CLIError(
|
|
14618
14699
|
"PROFILE_CHANGED_DURING_LOGIN",
|
|
14619
|
-
`Profile "${profileName}" changed while
|
|
14700
|
+
`Profile "${profileName}" changed while this login was in progress (another login, key rotation or logout). Nothing was saved; check \`tc --profile ${profileName} context\` and run the login again if it is still needed.`,
|
|
14620
14701
|
ExitCode.ERROR
|
|
14621
14702
|
);
|
|
14622
14703
|
}
|
|
@@ -14637,11 +14718,11 @@ async function commitLogin(profileName, snapshot, commit) {
|
|
|
14637
14718
|
ExitCode.ERROR
|
|
14638
14719
|
);
|
|
14639
14720
|
}
|
|
14640
|
-
}, { timeoutMs:
|
|
14721
|
+
}, { timeoutMs: PROFILE_COMMIT_LOCK_TIMEOUT_MS }).catch((error) => {
|
|
14641
14722
|
if (!(error instanceof ProfileLockTimeoutError2)) throw error;
|
|
14642
14723
|
throw new CLIError(
|
|
14643
14724
|
"PROFILE_LOCK_TIMEOUT",
|
|
14644
|
-
`Another tc process kept profile "${profileName}" locked for ${
|
|
14725
|
+
`Another tc process kept profile "${profileName}" locked for ${PROFILE_COMMIT_LOCK_TIMEOUT_MS / 1e3} s, so the approved login was not saved. Wait for it to finish (a crashed process's lock is reclaimed after 30 s) and run the login again.`,
|
|
14645
14726
|
ExitCode.ERROR
|
|
14646
14727
|
);
|
|
14647
14728
|
});
|
|
@@ -15432,7 +15513,7 @@ function registerAuthCommand(program) {
|
|
|
15432
15513
|
const profile = await ProfileManager.getProfile(ctx.profile);
|
|
15433
15514
|
const session = await ProfileManager.getSession(ctx.profile);
|
|
15434
15515
|
if (session || resolveProfilePosture(profile) !== "delegate-session") throw error;
|
|
15435
|
-
node = await bootstrapDelegatedSession(ctx, imported.delegation);
|
|
15516
|
+
node = (await bootstrapDelegatedSession(ctx, imported.delegation)).node;
|
|
15436
15517
|
}
|
|
15437
15518
|
await appendAdditionalDelegation(ctx.profile, storedAdditionalDelegation(
|
|
15438
15519
|
imported.delegation,
|
|
@@ -15757,15 +15838,11 @@ async function importRequestBoundDelegationWithBootstrap(ctx, artifact) {
|
|
|
15757
15838
|
}
|
|
15758
15839
|
const candidate = artifact;
|
|
15759
15840
|
const delegation = normalizePortableDelegation(candidate.delegation);
|
|
15841
|
+
const bootstrap = await bootstrapDelegatedSession(ctx, delegation);
|
|
15760
15842
|
try {
|
|
15761
|
-
await bootstrapDelegatedSession(ctx, delegation);
|
|
15762
15843
|
await importRequestBoundDelegation(ctx, artifact);
|
|
15763
15844
|
} catch (error) {
|
|
15764
|
-
await
|
|
15765
|
-
await ProfileManager.clearSession(ctx.profile);
|
|
15766
|
-
await ProfileManager.setProfile(ctx.profile, profile);
|
|
15767
|
-
});
|
|
15768
|
-
throw error;
|
|
15845
|
+
await bootstrap.abandon(error);
|
|
15769
15846
|
}
|
|
15770
15847
|
}
|
|
15771
15848
|
async function importRequestBoundDelegation(ctx, artifact) {
|
|
@@ -16079,7 +16156,6 @@ async function rotateAuthKey(profileName, host, options = {}) {
|
|
|
16079
16156
|
ExitCode.AUTH_REQUIRED
|
|
16080
16157
|
);
|
|
16081
16158
|
}
|
|
16082
|
-
await ProfileManager.clearSession(profileName);
|
|
16083
16159
|
const result2 = await handleLocalAuth(profileName, host, {
|
|
16084
16160
|
emitOutput: false,
|
|
16085
16161
|
forceSessionKey: true
|
|
@@ -17643,7 +17719,7 @@ init_errors();
|
|
|
17643
17719
|
init_constants();
|
|
17644
17720
|
init_storage();
|
|
17645
17721
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
17646
|
-
import { lstat, open as open2, readFile as readFile8, rename as rename2, rm as rm3 } from "fs/promises";
|
|
17722
|
+
import { lstat as lstat2, open as open2, readFile as readFile8, rename as rename2, rm as rm3 } from "fs/promises";
|
|
17647
17723
|
import { basename as basename2, dirname as dirname3, join as join5 } from "path";
|
|
17648
17724
|
import { homedir } from "os";
|
|
17649
17725
|
import { invokeOperation as invokeOperation2 } from "@tinycloud/operations";
|
|
@@ -17817,7 +17893,7 @@ function scopedSecretLoginHint(profileName) {
|
|
|
17817
17893
|
}
|
|
17818
17894
|
async function validateSecretOutput(path) {
|
|
17819
17895
|
try {
|
|
17820
|
-
const destination = await
|
|
17896
|
+
const destination = await lstat2(path);
|
|
17821
17897
|
if (!destination.isFile()) {
|
|
17822
17898
|
throw new CLIError("INVALID_ARGUMENT", `Secret output "${path}" must be a regular file, not a symlink, directory, or device.`, ExitCode.USAGE_ERROR);
|
|
17823
17899
|
}
|
|
@@ -17825,7 +17901,7 @@ async function validateSecretOutput(path) {
|
|
|
17825
17901
|
if (!isMissingFileError(error)) throw error;
|
|
17826
17902
|
}
|
|
17827
17903
|
try {
|
|
17828
|
-
const parent = await
|
|
17904
|
+
const parent = await lstat2(dirname3(path));
|
|
17829
17905
|
if (!parent.isDirectory()) {
|
|
17830
17906
|
throw new CLIError("INVALID_ARGUMENT", `Secret output "${path}" requires an existing directory parent.`, ExitCode.USAGE_ERROR);
|
|
17831
17907
|
}
|
|
@@ -27477,7 +27553,7 @@ function receiveJson(result, path) {
|
|
|
27477
27553
|
|
|
27478
27554
|
// src/share/io.ts
|
|
27479
27555
|
import { constants } from "fs";
|
|
27480
|
-
import { lstat as
|
|
27556
|
+
import { lstat as lstat3, mkdir as mkdir3, mkdtemp, open as open4, readFile as readFile9, realpath, stat as stat2, link, rename as rename3, rm as rm4, unlink } from "fs/promises";
|
|
27481
27557
|
import { randomBytes as randomBytes4 } from "crypto";
|
|
27482
27558
|
import { basename as basename3, join as join6, resolve as resolve2, sep } from "path";
|
|
27483
27559
|
init_errors();
|
|
@@ -27535,7 +27611,7 @@ async function assertDirectory(path) {
|
|
|
27535
27611
|
for (const segment of segments) {
|
|
27536
27612
|
current = current === sep ? join6(current, segment) : join6(current, segment);
|
|
27537
27613
|
try {
|
|
27538
|
-
const info = await
|
|
27614
|
+
const info = await lstat3(current);
|
|
27539
27615
|
if (info.isSymbolicLink()) {
|
|
27540
27616
|
const canonical = await realpath(current);
|
|
27541
27617
|
if (current !== "/tmp" && current !== "/var") throw new Error("OUTPUT_EXISTS");
|
|
@@ -27544,7 +27620,7 @@ async function assertDirectory(path) {
|
|
|
27544
27620
|
} catch (error) {
|
|
27545
27621
|
if (error.code !== "ENOENT") throw error;
|
|
27546
27622
|
await mkdir3(current, { mode: 448 });
|
|
27547
|
-
const created = await
|
|
27623
|
+
const created = await lstat3(current);
|
|
27548
27624
|
if (created.isSymbolicLink() || !created.isDirectory()) throw new Error("OUTPUT_EXISTS");
|
|
27549
27625
|
}
|
|
27550
27626
|
}
|
|
@@ -27563,7 +27639,7 @@ async function writeShareOutput(directory, filename, bytes, force) {
|
|
|
27563
27639
|
};
|
|
27564
27640
|
await assertStableDirectory();
|
|
27565
27641
|
const stagingDirectory = await mkdtemp(join6(stableDirectory, ".tinycloud-share-stage-"));
|
|
27566
|
-
const stagingInfo = await
|
|
27642
|
+
const stagingInfo = await lstat3(stagingDirectory);
|
|
27567
27643
|
if (!stagingInfo.isDirectory() || (stagingInfo.mode & 511) !== 448) throw new Error("OUTPUT_EXISTS");
|
|
27568
27644
|
const stagingPath = join6(stagingDirectory, `.tinycloud-share-${randomBytes4(16).toString("hex")}.tmp`);
|
|
27569
27645
|
let temporaryPath;
|
|
@@ -27571,7 +27647,7 @@ async function writeShareOutput(directory, filename, bytes, force) {
|
|
|
27571
27647
|
try {
|
|
27572
27648
|
await assertStableDirectory();
|
|
27573
27649
|
try {
|
|
27574
|
-
const existing = await
|
|
27650
|
+
const existing = await lstat3(outputPath);
|
|
27575
27651
|
if (existing.isSymbolicLink()) throw new Error("UNSAFE_FILENAME");
|
|
27576
27652
|
} catch (error) {
|
|
27577
27653
|
if (error.code !== "ENOENT") throw error;
|