wyvrnpm 1.3.2 → 2.0.2
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 +704 -413
- package/bin/wyvrn.js +158 -105
- package/package.json +1 -1
- package/src/commands/clean.js +40 -40
- package/src/commands/configure.js +80 -80
- package/src/commands/init.js +33 -33
- package/src/commands/install.js +78 -31
- package/src/commands/link.js +316 -316
- package/src/commands/profile.js +171 -0
- package/src/commands/publish.js +116 -64
- package/src/config.js +8 -6
- package/src/download.js +252 -89
- package/src/index.js +5 -4
- package/src/link-utils.js +95 -95
- package/src/manifest.js +58 -2
- package/src/profile.js +377 -0
- package/src/providers/base.js +89 -12
- package/src/providers/file.js +123 -13
- package/src/providers/http.js +217 -67
- package/src/providers/index.js +43 -43
- package/src/providers/s3.js +207 -70
- package/src/resolve.js +19 -5
package/src/download.js
CHANGED
|
@@ -1,63 +1,34 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const fs
|
|
3
|
+
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { pipeline } = require('stream/promises');
|
|
6
6
|
const { Readable } = require('stream');
|
|
7
7
|
const StreamZip = require('node-stream-zip');
|
|
8
|
+
|
|
8
9
|
const { isLink, getLinkTarget } = require('./link-utils');
|
|
10
|
+
const { sha256Of } = require('./profile');
|
|
11
|
+
const { getProvider } = require('./providers');
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// v1 (legacy) download helpers
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
9
16
|
|
|
10
|
-
/**
|
|
11
|
-
* Downloads a single URL to a local file path, respecting the given timeout.
|
|
12
|
-
*
|
|
13
|
-
* @param {string} url - Remote URL to download.
|
|
14
|
-
* @param {string} destPath - Local path to write the response body to.
|
|
15
|
-
* @param {Function} httpClient - fetch-compatible function.
|
|
16
|
-
* @param {number} timeoutMs - Abort timeout in milliseconds.
|
|
17
|
-
* @returns {Promise<void>}
|
|
18
|
-
* @throws {Error} On non-OK HTTP status or network / timeout error.
|
|
19
|
-
*/
|
|
20
17
|
async function downloadFile(url, destPath, httpClient, timeoutMs) {
|
|
21
18
|
const controller = new AbortController();
|
|
22
19
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
23
|
-
|
|
24
20
|
let response;
|
|
25
21
|
try {
|
|
26
22
|
response = await httpClient(url, { signal: controller.signal });
|
|
27
23
|
} finally {
|
|
28
24
|
clearTimeout(timer);
|
|
29
25
|
}
|
|
30
|
-
|
|
31
|
-
if (!response.ok) {
|
|
32
|
-
throw new Error(`HTTP ${response.status} for ${url}`);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Stream the response body directly to disk — avoids loading large files into RAM.
|
|
26
|
+
if (!response.ok) throw new Error(`HTTP ${response.status} for ${url}`);
|
|
36
27
|
const fileStream = fs.createWriteStream(destPath);
|
|
37
28
|
await pipeline(Readable.fromWeb(response.body), fileStream);
|
|
38
29
|
}
|
|
39
30
|
|
|
40
|
-
|
|
41
|
-
* Resolves which source URL contains a given package by probing each one in order.
|
|
42
|
-
*
|
|
43
|
-
* @param {string} name
|
|
44
|
-
* @param {string} version
|
|
45
|
-
* @param {string[]} packageSources
|
|
46
|
-
* @param {string} platform
|
|
47
|
-
* @param {Function} httpClient
|
|
48
|
-
* @param {number} timeoutMs
|
|
49
|
-
* @param {string} destZipPath - Where to save the downloaded zip.
|
|
50
|
-
* @returns {Promise<boolean>} True if successfully downloaded, false otherwise.
|
|
51
|
-
*/
|
|
52
|
-
async function tryDownloadFromSources(
|
|
53
|
-
name,
|
|
54
|
-
version,
|
|
55
|
-
packageSources,
|
|
56
|
-
platform,
|
|
57
|
-
httpClient,
|
|
58
|
-
timeoutMs,
|
|
59
|
-
destZipPath,
|
|
60
|
-
) {
|
|
31
|
+
async function tryDownloadFromSources(name, version, packageSources, platform, httpClient, timeoutMs, destZipPath) {
|
|
61
32
|
for (const source of packageSources) {
|
|
62
33
|
const base = source.replace(/\/$/, '');
|
|
63
34
|
const candidates = [
|
|
@@ -78,95 +49,287 @@ async function tryDownloadFromSources(
|
|
|
78
49
|
return false;
|
|
79
50
|
}
|
|
80
51
|
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// v2 download helpers
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
81
56
|
/**
|
|
82
|
-
*
|
|
57
|
+
* Try to download a v2 prebuilt binary for a package.
|
|
83
58
|
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* @param {
|
|
90
|
-
*
|
|
59
|
+
* Resolution order:
|
|
60
|
+
* 1. Exact profile hash match
|
|
61
|
+
* 2. Compatible build from versions.json (same OS + arch, any compiler)
|
|
62
|
+
* 3. Return null (caller should fall back to v1)
|
|
63
|
+
*
|
|
64
|
+
* @param {{
|
|
65
|
+
* name: string,
|
|
66
|
+
* version: string,
|
|
67
|
+
* profileHash: string,
|
|
68
|
+
* profile: import('./profile').WyvrnProfile,
|
|
69
|
+
* }} dep
|
|
70
|
+
* @param {string[]} packageSources
|
|
71
|
+
* @param {string} destZipPath
|
|
72
|
+
* @param {number} timeoutMs
|
|
73
|
+
* @param {object} authOptions - { awsProfile?, token? }
|
|
74
|
+
* @returns {Promise<{
|
|
75
|
+
* found: boolean,
|
|
76
|
+
* contentSha256: string|null,
|
|
77
|
+
* gitSha: string|null,
|
|
78
|
+
* gitRepo: string|null,
|
|
79
|
+
* profileHash: string,
|
|
80
|
+
* source: string, // which source URL was used
|
|
81
|
+
* }|null>} null = not found in v2
|
|
91
82
|
*/
|
|
92
|
-
async function
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
83
|
+
async function tryDownloadV2(dep, packageSources, destZipPath, timeoutMs, authOptions = {}) {
|
|
84
|
+
const { name, version, profileHash, profile } = dep;
|
|
85
|
+
const { awsProfile, token } = authOptions;
|
|
86
|
+
|
|
87
|
+
for (const source of packageSources) {
|
|
88
|
+
let provider;
|
|
89
|
+
try { provider = getProvider(source); } catch { continue; }
|
|
90
|
+
|
|
91
|
+
// 1) Exact profile hash
|
|
92
|
+
const base = source.replace(/\/$/, '');
|
|
93
|
+
const metaUrl = `${base}/v2/${name}/${version}/${profileHash}/wyvrn.json`;
|
|
94
|
+
console.log(`[wyvrn] Trying v2: ${metaUrl}`);
|
|
95
|
+
const exactMeta = await provider.v2GetMeta({ source, name, version, profileHash, awsProfile, token });
|
|
96
|
+
if (exactMeta) {
|
|
97
|
+
const ok = await provider.v2DownloadZip({ source, name, version, profileHash, awsProfile, token }, destZipPath, timeoutMs);
|
|
98
|
+
if (ok) {
|
|
99
|
+
return {
|
|
100
|
+
found: true,
|
|
101
|
+
contentSha256: exactMeta.contentSha256 ?? null,
|
|
102
|
+
gitSha: exactMeta.gitSha ?? null,
|
|
103
|
+
gitRepo: exactMeta.gitRepo ?? null,
|
|
104
|
+
profileHash,
|
|
105
|
+
source,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
console.log(`[wyvrn] v2 miss (no exact profile match): ${metaUrl}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 2) Fallback: scan versions.json for a compatible build (same OS + arch)
|
|
113
|
+
const versionsUrl = `${base}/v2/${name}/versions.json`;
|
|
114
|
+
console.log(`[wyvrn] Trying v2 versions index: ${versionsUrl}`);
|
|
115
|
+
const idx = await provider.v2GetVersionsIndex({ source, name, awsProfile, token });
|
|
116
|
+
if (!idx?.versions?.[version]?.profiles) {
|
|
117
|
+
console.log(`[wyvrn] v2 miss (no versions index or no entry for ${version}): ${versionsUrl}`);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const versionEntry = idx.versions[version];
|
|
122
|
+
for (const [ph, buildEntry] of Object.entries(versionEntry.profiles)) {
|
|
123
|
+
// buildSettings is the copy of the profile stored for debugging at publish time
|
|
124
|
+
const bp = buildEntry.buildSettings ?? buildEntry.profile;
|
|
125
|
+
if (!bp) continue;
|
|
126
|
+
if (bp.os !== profile.os) continue;
|
|
127
|
+
if (bp.arch !== profile.arch) continue;
|
|
128
|
+
// Same OS + arch — accept as a compatible fallback
|
|
129
|
+
console.warn(
|
|
130
|
+
`[wyvrn] No exact profile match for ${name}@${version}; using compatible build [${ph}]` +
|
|
131
|
+
` (${bp.compiler} ${bp['compiler.version']})`,
|
|
132
|
+
);
|
|
133
|
+
const ok = await provider.v2DownloadZip({ source, name, version, profileHash: ph, awsProfile, token }, destZipPath, timeoutMs);
|
|
134
|
+
if (ok) {
|
|
135
|
+
return {
|
|
136
|
+
found: true,
|
|
137
|
+
contentSha256: buildEntry.contentSha256 ?? null,
|
|
138
|
+
gitSha: versionEntry.source?.gitSha ?? null,
|
|
139
|
+
gitRepo: versionEntry.source?.gitRepo ?? null,
|
|
140
|
+
profileHash: ph,
|
|
141
|
+
source,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Extraction helper
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
async function extractZip(destZipPath, extractDir) {
|
|
155
|
+
const zip = new StreamZip.async({ file: destZipPath });
|
|
156
|
+
await zip.extract(null, extractDir);
|
|
157
|
+
await zip.close();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// Main downloadDependencies (v2-aware)
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Downloads and extracts all resolved dependencies.
|
|
166
|
+
* Tries v2 first (profile-aware + SHA256 verification), falls back to v1.
|
|
167
|
+
*
|
|
168
|
+
* @param {Map<string, string | {version:string, profileHash:string|null, profile:import('./profile').WyvrnProfile|null}>} deps
|
|
169
|
+
* Either a plain `Map<name, version>` (legacy callers / tests) or the enriched map
|
|
170
|
+
* produced by install.js: `Map<name, { version, profileHash, profile }>`.
|
|
171
|
+
* @param {string[]} packageSources
|
|
172
|
+
* @param {string} platform - v1 legacy platform string
|
|
173
|
+
* @param {string} razerDir
|
|
174
|
+
* @param {Function} httpClient - fetch-compatible
|
|
175
|
+
* @param {number} timeout - seconds
|
|
176
|
+
* @param {object} [authOptions] - { awsProfile?, token? }
|
|
177
|
+
* @returns {Promise<Map<string, {version:string, profileHash?:string, contentSha256?:string, gitSha?:string, resolvedFrom:string}>>}
|
|
178
|
+
*/
|
|
179
|
+
async function downloadDependencies(deps, packageSources, platform, razerDir, httpClient, timeout, authOptions = {}) {
|
|
100
180
|
const timeoutMs = timeout * 1000;
|
|
181
|
+
const { awsProfile, token } = authOptions;
|
|
101
182
|
|
|
102
183
|
if (!fs.existsSync(razerDir)) {
|
|
103
184
|
fs.mkdirSync(razerDir, { recursive: true });
|
|
104
185
|
}
|
|
105
186
|
|
|
106
|
-
|
|
107
|
-
|
|
187
|
+
/** @type {Map<string, object>} */
|
|
188
|
+
const lockEntries = new Map();
|
|
189
|
+
|
|
190
|
+
for (const [name, depInfoOrVersion] of deps) {
|
|
191
|
+
// Normalise: accept both plain version string (legacy) and enriched object
|
|
192
|
+
const isLegacyEntry = typeof depInfoOrVersion === 'string';
|
|
193
|
+
const version = isLegacyEntry ? depInfoOrVersion : depInfoOrVersion.version;
|
|
194
|
+
const profileHash = isLegacyEntry ? null : (depInfoOrVersion.profileHash ?? null);
|
|
195
|
+
const profile = isLegacyEntry ? null : (depInfoOrVersion.profile ?? null);
|
|
196
|
+
|
|
197
|
+
const extractDir = path.join(razerDir, name);
|
|
108
198
|
const versionFile = path.join(extractDir, '.wyvrn-version');
|
|
199
|
+
const metaFile = path.join(extractDir, '.wyvrn-meta.json');
|
|
109
200
|
|
|
110
|
-
// Skip linked packages
|
|
201
|
+
// Skip linked packages
|
|
111
202
|
if (isLink(extractDir)) {
|
|
112
203
|
const linkTarget = getLinkTarget(extractDir);
|
|
113
204
|
console.log(`[wyvrn] Linked: ${name} → ${linkTarget} (skipping download)`);
|
|
205
|
+
lockEntries.set(name, { version, resolvedFrom: 'link' });
|
|
114
206
|
continue;
|
|
115
207
|
}
|
|
116
208
|
|
|
209
|
+
// Skip if already at correct version + profile hash
|
|
117
210
|
if (fs.existsSync(extractDir)) {
|
|
118
211
|
const installedVersion = fs.existsSync(versionFile)
|
|
119
212
|
? fs.readFileSync(versionFile, 'utf8').trim()
|
|
120
213
|
: null;
|
|
121
|
-
|
|
122
|
-
|
|
214
|
+
const installedMeta = fs.existsSync(metaFile)
|
|
215
|
+
? JSON.parse(fs.readFileSync(metaFile, 'utf8'))
|
|
216
|
+
: null;
|
|
217
|
+
|
|
218
|
+
const sameVersion = installedVersion === version;
|
|
219
|
+
const sameProfile = !profileHash || installedMeta?.profileHash === profileHash;
|
|
220
|
+
|
|
221
|
+
if (sameVersion && sameProfile) {
|
|
222
|
+
console.log(`[wyvrn] Already present: ${name}@${version}${profileHash ? ` [${installedMeta?.profileHash}]` : ''} — skipping`);
|
|
223
|
+
lockEntries.set(name, {
|
|
224
|
+
version,
|
|
225
|
+
profileHash: installedMeta?.profileHash ?? null,
|
|
226
|
+
contentSha256: installedMeta?.contentSha256 ?? null,
|
|
227
|
+
gitSha: installedMeta?.gitSha ?? null,
|
|
228
|
+
resolvedFrom: installedMeta?.resolvedFrom ?? 'cached',
|
|
229
|
+
});
|
|
123
230
|
continue;
|
|
124
231
|
}
|
|
125
|
-
|
|
232
|
+
if (!sameVersion) {
|
|
233
|
+
console.log(`[wyvrn] Version changed: ${name} ${installedVersion} → ${version}, re-downloading`);
|
|
234
|
+
} else {
|
|
235
|
+
console.log(`[wyvrn] Profile changed for ${name}@${version}, re-downloading`);
|
|
236
|
+
}
|
|
126
237
|
fs.rmSync(extractDir, { recursive: true, force: true });
|
|
127
238
|
}
|
|
128
239
|
|
|
129
240
|
console.log(`[wyvrn] Downloading: ${name}@${version}`);
|
|
130
|
-
|
|
131
241
|
const destZipPath = path.join(razerDir, `${name}.zip`);
|
|
132
242
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
console.warn(
|
|
145
|
-
`[wyvrn] Warning: package ${name}@${version} was not found in any configured source`,
|
|
243
|
+
let downloadResult = null;
|
|
244
|
+
let resolvedFrom = 'v1';
|
|
245
|
+
|
|
246
|
+
// ── Try v2 first ────────────────────────────────────────────────────────
|
|
247
|
+
if (profileHash && profile) {
|
|
248
|
+
downloadResult = await tryDownloadV2(
|
|
249
|
+
{ name, version, profileHash, profile },
|
|
250
|
+
packageSources,
|
|
251
|
+
destZipPath,
|
|
252
|
+
timeoutMs,
|
|
253
|
+
{ awsProfile, token },
|
|
146
254
|
);
|
|
147
|
-
|
|
255
|
+
if (downloadResult) resolvedFrom = 'v2';
|
|
148
256
|
}
|
|
149
257
|
|
|
258
|
+
// ── Fall back to v1 ─────────────────────────────────────────────────────
|
|
259
|
+
if (!downloadResult) {
|
|
260
|
+
const ok = await tryDownloadFromSources(name, version, packageSources, platform, httpClient, timeoutMs, destZipPath);
|
|
261
|
+
if (!ok) {
|
|
262
|
+
console.warn(`[wyvrn] Warning: package ${name}@${version} was not found in any configured source`);
|
|
263
|
+
lockEntries.set(name, { version, resolvedFrom: 'not-found' });
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ── SHA256 integrity check (v2 only) ────────────────────────────────────
|
|
269
|
+
if (downloadResult?.contentSha256) {
|
|
270
|
+
const actual = sha256Of(destZipPath);
|
|
271
|
+
if (actual !== downloadResult.contentSha256) {
|
|
272
|
+
console.error(
|
|
273
|
+
`[wyvrn] Error: SHA256 mismatch for ${name}@${version}\n` +
|
|
274
|
+
` expected: ${downloadResult.contentSha256}\n` +
|
|
275
|
+
` actual: ${actual}`,
|
|
276
|
+
);
|
|
277
|
+
try { fs.unlinkSync(destZipPath); } catch { /* ignore */ }
|
|
278
|
+
lockEntries.set(name, { version, resolvedFrom: 'sha256-mismatch' });
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
console.log(`[wyvrn] SHA256 verified: ${actual.slice(0, 16)}...`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ── Extract ─────────────────────────────────────────────────────────────
|
|
150
285
|
try {
|
|
151
286
|
fs.mkdirSync(extractDir, { recursive: true });
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
287
|
+
await extractZip(destZipPath, extractDir);
|
|
288
|
+
|
|
289
|
+
// Write .wyvrn-version (v1 compat)
|
|
155
290
|
fs.writeFileSync(versionFile, version, 'utf8');
|
|
156
|
-
|
|
291
|
+
|
|
292
|
+
// Write .wyvrn-meta.json (v2 extended metadata)
|
|
293
|
+
const meta = {
|
|
294
|
+
version,
|
|
295
|
+
resolvedFrom,
|
|
296
|
+
profileHash: downloadResult?.profileHash ?? null,
|
|
297
|
+
contentSha256: downloadResult?.contentSha256 ?? null,
|
|
298
|
+
gitSha: downloadResult?.gitSha ?? null,
|
|
299
|
+
gitRepo: downloadResult?.gitRepo ?? null,
|
|
300
|
+
installedAt: new Date().toISOString(),
|
|
301
|
+
};
|
|
302
|
+
fs.writeFileSync(metaFile, JSON.stringify(meta, null, 2) + '\n', 'utf8');
|
|
303
|
+
|
|
304
|
+
console.log(
|
|
305
|
+
`[wyvrn] Extracted: ${name}@${version}` +
|
|
306
|
+
(downloadResult?.profileHash ? ` [${downloadResult.profileHash}]` : '') +
|
|
307
|
+
` (${resolvedFrom}) → ${extractDir}`,
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
if (downloadResult?.gitSha) {
|
|
311
|
+
console.log(`[wyvrn] Source commit : ${downloadResult.gitSha}`);
|
|
312
|
+
if (downloadResult?.gitRepo) {
|
|
313
|
+
console.log(`[wyvrn] Source repo : ${downloadResult.gitRepo}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
lockEntries.set(name, {
|
|
318
|
+
version,
|
|
319
|
+
resolvedFrom,
|
|
320
|
+
profileHash: downloadResult?.profileHash ?? null,
|
|
321
|
+
contentSha256: downloadResult?.contentSha256 ?? null,
|
|
322
|
+
gitSha: downloadResult?.gitSha ?? null,
|
|
323
|
+
});
|
|
157
324
|
} catch (err) {
|
|
158
325
|
console.warn(`[wyvrn] Failed to extract ${name}@${version}: ${err.message}`);
|
|
326
|
+
lockEntries.set(name, { version, resolvedFrom: 'extract-failed' });
|
|
159
327
|
} finally {
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
fs.unlinkSync(destZipPath);
|
|
163
|
-
} catch {
|
|
164
|
-
// Ignore cleanup errors.
|
|
165
|
-
}
|
|
328
|
+
try { fs.unlinkSync(destZipPath); } catch { /* ignore */ }
|
|
166
329
|
}
|
|
167
330
|
}
|
|
331
|
+
|
|
332
|
+
return lockEntries;
|
|
168
333
|
}
|
|
169
334
|
|
|
170
|
-
module.exports = {
|
|
171
|
-
downloadDependencies,
|
|
172
|
-
};
|
|
335
|
+
module.exports = { downloadDependencies };
|
package/src/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
module.exports = {
|
|
4
|
-
init:
|
|
5
|
-
install:
|
|
6
|
-
clean:
|
|
7
|
-
publish:
|
|
4
|
+
init: require('./commands/init'),
|
|
5
|
+
install: require('./commands/install'),
|
|
6
|
+
clean: require('./commands/clean'),
|
|
7
|
+
publish: require('./commands/publish'),
|
|
8
8
|
configure: require('./commands/configure'),
|
|
9
|
+
profile: require('./commands/profile'),
|
|
9
10
|
};
|
package/src/link-utils.js
CHANGED
|
@@ -1,95 +1,95 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Creates a symlink/junction from linkPath pointing to targetPath.
|
|
8
|
-
* Uses junctions on Windows (no admin rights required), symlinks on Unix.
|
|
9
|
-
*
|
|
10
|
-
* @param {string} targetPath - The actual directory to link to.
|
|
11
|
-
* @param {string} linkPath - The path where the link will be created.
|
|
12
|
-
*/
|
|
13
|
-
function createLink(targetPath, linkPath) {
|
|
14
|
-
const absoluteTarget = path.resolve(targetPath);
|
|
15
|
-
const absoluteLink = path.resolve(linkPath);
|
|
16
|
-
|
|
17
|
-
// Ensure parent directory exists
|
|
18
|
-
const parentDir = path.dirname(absoluteLink);
|
|
19
|
-
if (!fs.existsSync(parentDir)) {
|
|
20
|
-
fs.mkdirSync(parentDir, { recursive: true });
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Remove existing link/directory if present
|
|
24
|
-
if (fs.existsSync(absoluteLink)) {
|
|
25
|
-
const stats = fs.lstatSync(absoluteLink);
|
|
26
|
-
if (stats.isSymbolicLink()) {
|
|
27
|
-
fs.unlinkSync(absoluteLink);
|
|
28
|
-
} else {
|
|
29
|
-
fs.rmSync(absoluteLink, { recursive: true, force: true });
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Use 'junction' on Windows (no admin required), 'dir' on Unix
|
|
34
|
-
const linkType = process.platform === 'win32' ? 'junction' : 'dir';
|
|
35
|
-
fs.symlinkSync(absoluteTarget, absoluteLink, linkType);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Checks if a path is a symlink or junction.
|
|
40
|
-
*
|
|
41
|
-
* @param {string} linkPath - Path to check.
|
|
42
|
-
* @returns {boolean} True if it's a symlink/junction.
|
|
43
|
-
*/
|
|
44
|
-
function isLink(linkPath) {
|
|
45
|
-
try {
|
|
46
|
-
const stats = fs.lstatSync(linkPath);
|
|
47
|
-
return stats.isSymbolicLink();
|
|
48
|
-
} catch {
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Removes a symlink/junction without affecting the target directory.
|
|
55
|
-
*
|
|
56
|
-
* @param {string} linkPath - Path to the symlink/junction to remove.
|
|
57
|
-
* @returns {boolean} True if removed, false if it didn't exist or wasn't a link.
|
|
58
|
-
*/
|
|
59
|
-
function removeLink(linkPath) {
|
|
60
|
-
try {
|
|
61
|
-
const stats = fs.lstatSync(linkPath);
|
|
62
|
-
if (stats.isSymbolicLink()) {
|
|
63
|
-
fs.unlinkSync(linkPath);
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
return false;
|
|
67
|
-
} catch {
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Gets the target path of a symlink/junction.
|
|
74
|
-
*
|
|
75
|
-
* @param {string} linkPath - Path to the symlink/junction.
|
|
76
|
-
* @returns {string|null} The target path, or null if not a link.
|
|
77
|
-
*/
|
|
78
|
-
function getLinkTarget(linkPath) {
|
|
79
|
-
try {
|
|
80
|
-
const stats = fs.lstatSync(linkPath);
|
|
81
|
-
if (stats.isSymbolicLink()) {
|
|
82
|
-
return fs.readlinkSync(linkPath);
|
|
83
|
-
}
|
|
84
|
-
return null;
|
|
85
|
-
} catch {
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
module.exports = {
|
|
91
|
-
createLink,
|
|
92
|
-
isLink,
|
|
93
|
-
removeLink,
|
|
94
|
-
getLinkTarget,
|
|
95
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates a symlink/junction from linkPath pointing to targetPath.
|
|
8
|
+
* Uses junctions on Windows (no admin rights required), symlinks on Unix.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} targetPath - The actual directory to link to.
|
|
11
|
+
* @param {string} linkPath - The path where the link will be created.
|
|
12
|
+
*/
|
|
13
|
+
function createLink(targetPath, linkPath) {
|
|
14
|
+
const absoluteTarget = path.resolve(targetPath);
|
|
15
|
+
const absoluteLink = path.resolve(linkPath);
|
|
16
|
+
|
|
17
|
+
// Ensure parent directory exists
|
|
18
|
+
const parentDir = path.dirname(absoluteLink);
|
|
19
|
+
if (!fs.existsSync(parentDir)) {
|
|
20
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Remove existing link/directory if present
|
|
24
|
+
if (fs.existsSync(absoluteLink)) {
|
|
25
|
+
const stats = fs.lstatSync(absoluteLink);
|
|
26
|
+
if (stats.isSymbolicLink()) {
|
|
27
|
+
fs.unlinkSync(absoluteLink);
|
|
28
|
+
} else {
|
|
29
|
+
fs.rmSync(absoluteLink, { recursive: true, force: true });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Use 'junction' on Windows (no admin required), 'dir' on Unix
|
|
34
|
+
const linkType = process.platform === 'win32' ? 'junction' : 'dir';
|
|
35
|
+
fs.symlinkSync(absoluteTarget, absoluteLink, linkType);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Checks if a path is a symlink or junction.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} linkPath - Path to check.
|
|
42
|
+
* @returns {boolean} True if it's a symlink/junction.
|
|
43
|
+
*/
|
|
44
|
+
function isLink(linkPath) {
|
|
45
|
+
try {
|
|
46
|
+
const stats = fs.lstatSync(linkPath);
|
|
47
|
+
return stats.isSymbolicLink();
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Removes a symlink/junction without affecting the target directory.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} linkPath - Path to the symlink/junction to remove.
|
|
57
|
+
* @returns {boolean} True if removed, false if it didn't exist or wasn't a link.
|
|
58
|
+
*/
|
|
59
|
+
function removeLink(linkPath) {
|
|
60
|
+
try {
|
|
61
|
+
const stats = fs.lstatSync(linkPath);
|
|
62
|
+
if (stats.isSymbolicLink()) {
|
|
63
|
+
fs.unlinkSync(linkPath);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Gets the target path of a symlink/junction.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} linkPath - Path to the symlink/junction.
|
|
76
|
+
* @returns {string|null} The target path, or null if not a link.
|
|
77
|
+
*/
|
|
78
|
+
function getLinkTarget(linkPath) {
|
|
79
|
+
try {
|
|
80
|
+
const stats = fs.lstatSync(linkPath);
|
|
81
|
+
if (stats.isSymbolicLink()) {
|
|
82
|
+
return fs.readlinkSync(linkPath);
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
createLink,
|
|
92
|
+
isLink,
|
|
93
|
+
removeLink,
|
|
94
|
+
getLinkTarget,
|
|
95
|
+
};
|