jpm-pkg 1.0.3
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/LICENSE.md +21 -0
- package/README.md +148 -0
- package/bin/jpm.js +252 -0
- package/package.json +52 -0
- package/src/commands/audit.js +56 -0
- package/src/commands/config.js +59 -0
- package/src/commands/info.js +78 -0
- package/src/commands/init.js +88 -0
- package/src/commands/install.js +139 -0
- package/src/commands/list.js +103 -0
- package/src/commands/publish.js +148 -0
- package/src/commands/run.js +72 -0
- package/src/commands/search.js +41 -0
- package/src/commands/uninstall.js +48 -0
- package/src/commands/update.js +63 -0
- package/src/commands/x.js +136 -0
- package/src/core/cache.js +117 -0
- package/src/core/installer.js +316 -0
- package/src/core/lockfile.js +128 -0
- package/src/core/package-json.js +133 -0
- package/src/core/registry.js +166 -0
- package/src/core/resolver.js +248 -0
- package/src/security/audit.js +100 -0
- package/src/security/integrity.js +70 -0
- package/src/utils/config.js +138 -0
- package/src/utils/env.js +31 -0
- package/src/utils/fs.js +154 -0
- package/src/utils/http.js +232 -0
- package/src/utils/logger.js +128 -0
- package/src/utils/lru-cache.js +66 -0
- package/src/utils/progress.js +142 -0
- package/src/utils/semver.js +279 -0
- package/src/utils/system.js +39 -0
- package/src/workspace/workspace.js +126 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Full SemVer implementation — no external dep
|
|
4
|
+
// Supports: X.Y.Z, X.Y.Z-pre+build, ranges: ^, ~, >, >=, <, <=, =, *, x, ||, ranges
|
|
5
|
+
|
|
6
|
+
const RE_VERSION = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-.]+))?(?:\+([0-9A-Za-z-.]+))?$/;
|
|
7
|
+
const RE_RANGE_PART = /^\s*([\^~>=<!*]*)([0-9x*]+(?:\.[0-9x*]+(?:\.[0-9x*]+)?)?(?:-[0-9A-Za-z-.]+)?)\s*$/;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Represents a parsed Semantic Version (SemVer).
|
|
11
|
+
*/
|
|
12
|
+
class Version {
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} str - The version string to parse
|
|
15
|
+
*/
|
|
16
|
+
constructor(str) {
|
|
17
|
+
const m = RE_VERSION.exec(str.trim().replace(/^v/, ''));
|
|
18
|
+
if (!m) throw new Error(`Invalid version: ${str}`);
|
|
19
|
+
this.major = parseInt(m[1], 10);
|
|
20
|
+
this.minor = parseInt(m[2], 10);
|
|
21
|
+
this.patch = parseInt(m[3], 10);
|
|
22
|
+
this.pre = m[4] ? m[4].split('.') : [];
|
|
23
|
+
this.build = m[5] ? m[5].split('.') : [];
|
|
24
|
+
this.raw = str;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Reconstructs the canonical version string.
|
|
29
|
+
* @returns {string}
|
|
30
|
+
*/
|
|
31
|
+
toString() {
|
|
32
|
+
let s = `${this.major}.${this.minor}.${this.patch}`;
|
|
33
|
+
if (this.pre.length) s += `-${this.pre.join('.')}`;
|
|
34
|
+
if (this.build.length) s += `+${this.build.join('.')}`;
|
|
35
|
+
return s;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Compares two pre-release identifier arrays according to SemVer rules.
|
|
41
|
+
*
|
|
42
|
+
* @param {string[]} a - First pre-release array
|
|
43
|
+
* @param {string[]} b - Second pre-release array
|
|
44
|
+
* @returns {number} -1 if a < b, 1 if a > b, 0 if equal
|
|
45
|
+
* @private
|
|
46
|
+
*/
|
|
47
|
+
function comparePre(a, b) {
|
|
48
|
+
if (!a.length && !b.length) return 0;
|
|
49
|
+
if (!a.length) return 1;
|
|
50
|
+
if (!b.length) return -1;
|
|
51
|
+
const len = Math.max(a.length, b.length);
|
|
52
|
+
for (let i = 0; i < len; i++) {
|
|
53
|
+
if (a[i] === b[i]) continue;
|
|
54
|
+
if (a[i] === undefined) return -1;
|
|
55
|
+
if (b[i] === undefined) return 1;
|
|
56
|
+
const na = parseInt(a[i], 10), nb = parseInt(b[i], 10);
|
|
57
|
+
if (!isNaN(na) && !isNaN(nb)) return na < nb ? -1 : 1;
|
|
58
|
+
if (!isNaN(na)) return -1;
|
|
59
|
+
if (!isNaN(nb)) return 1;
|
|
60
|
+
return a[i] < b[i] ? -1 : 1;
|
|
61
|
+
}
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Compares two versions.
|
|
67
|
+
*
|
|
68
|
+
* @param {string|Version} a
|
|
69
|
+
* @param {string|Version} b
|
|
70
|
+
* @returns {number} -1 if a < b, 1 if a > b, 0 if equal
|
|
71
|
+
*/
|
|
72
|
+
function compare(a, b) {
|
|
73
|
+
const va = a instanceof Version ? a : new Version(a);
|
|
74
|
+
const vb = b instanceof Version ? b : new Version(b);
|
|
75
|
+
for (const k of ['major', 'minor', 'patch']) {
|
|
76
|
+
if (va[k] !== vb[k]) return va[k] < vb[k] ? -1 : 1;
|
|
77
|
+
}
|
|
78
|
+
return comparePre(va.pre, vb.pre);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** @returns {boolean} True if a is greater than b */
|
|
82
|
+
function gt(a, b) { return compare(a, b) > 0; }
|
|
83
|
+
/** @returns {boolean} True if a is less than b */
|
|
84
|
+
function lt(a, b) { return compare(a, b) < 0; }
|
|
85
|
+
/** @returns {boolean} True if a is greater than or equal to b */
|
|
86
|
+
function gte(a, b) { return compare(a, b) >= 0; }
|
|
87
|
+
/** @returns {boolean} True if a is less than or equal to b */
|
|
88
|
+
function lte(a, b) { return compare(a, b) <= 0; }
|
|
89
|
+
/** @returns {boolean} True if versions are logically equal */
|
|
90
|
+
function eq(a, b) { return compare(a, b) === 0; }
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Parses a single semver range token (e.g., "^1.2.3") into a predicate.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} token
|
|
96
|
+
* @returns {function(string|Version): boolean}
|
|
97
|
+
* @private
|
|
98
|
+
*/
|
|
99
|
+
function parseSimpleRange(token) {
|
|
100
|
+
token = token.trim();
|
|
101
|
+
if (!token || token === '*' || token === 'latest') return () => true;
|
|
102
|
+
|
|
103
|
+
const hyphen = token.match(/^(.+?)\s+-\s+(.+)$/);
|
|
104
|
+
if (hyphen) {
|
|
105
|
+
const lo = hyphen[1].trim(), hi = hyphen[2].trim();
|
|
106
|
+
return (v) => gte(v, lo) && lte(v, hi);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const m = RE_RANGE_PART.exec(token);
|
|
110
|
+
if (!m) return () => false;
|
|
111
|
+
|
|
112
|
+
const [, op, verStr] = m;
|
|
113
|
+
const hasPre = verStr.includes('-');
|
|
114
|
+
|
|
115
|
+
const baseVer = verStr.split(/[-+]/)[0];
|
|
116
|
+
const parts = baseVer.split('.').map(p => (p === 'x' || p === '*' ? null : parseInt(p, 10)));
|
|
117
|
+
const [maj, min, pat] = parts;
|
|
118
|
+
|
|
119
|
+
let lo, hi;
|
|
120
|
+
|
|
121
|
+
if (op === '^') {
|
|
122
|
+
lo = verStr;
|
|
123
|
+
if (maj !== null && maj > 0) hi = `${maj + 1}.0.0-0`;
|
|
124
|
+
else if (min !== null && min > 0) hi = `0.${min + 1}.0-0`;
|
|
125
|
+
else hi = `0.0.${(pat ?? 0) + 1}-0`;
|
|
126
|
+
} else if (op === '~') {
|
|
127
|
+
lo = verStr;
|
|
128
|
+
if (min !== null) hi = `${maj}.${min + 1}.0-0`;
|
|
129
|
+
else hi = `${maj + 1}.0.0-0`;
|
|
130
|
+
} else if (op === '>') {
|
|
131
|
+
return (v) => gt(v, verStr);
|
|
132
|
+
} else if (op === '<') {
|
|
133
|
+
return (v) => lt(v, verStr);
|
|
134
|
+
} else if (op === '>=') {
|
|
135
|
+
return (v) => gte(v, verStr);
|
|
136
|
+
} else if (op === '<=') {
|
|
137
|
+
return (v) => lte(v, verStr);
|
|
138
|
+
} else if (op === '=') {
|
|
139
|
+
return (v) => eq(v, verStr);
|
|
140
|
+
} else {
|
|
141
|
+
if (maj === null || isNaN(maj)) return () => true;
|
|
142
|
+
if (min === null || isNaN(min)) {
|
|
143
|
+
lo = `${maj}.0.0`;
|
|
144
|
+
hi = `${maj + 1}.0.0-0`;
|
|
145
|
+
} else if (pat === null || isNaN(pat)) {
|
|
146
|
+
lo = `${maj}.${min}.0`;
|
|
147
|
+
hi = `${maj}.${min + 1}.0-0`;
|
|
148
|
+
} else {
|
|
149
|
+
return (v) => eq(v, verStr);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return (v) => {
|
|
154
|
+
const ver = v instanceof Version ? v : new Version(v);
|
|
155
|
+
if (!gte(ver, lo)) return false;
|
|
156
|
+
if (hi && !lt(ver, hi)) return false;
|
|
157
|
+
|
|
158
|
+
if (ver.pre.length > 0) {
|
|
159
|
+
if (hasPre) {
|
|
160
|
+
const rangeV = new Version(verStr);
|
|
161
|
+
return ver.major === rangeV.major && ver.minor === rangeV.minor && ver.patch === rangeV.patch;
|
|
162
|
+
}
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
return true;
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Checks if a version satisfies a given semver range.
|
|
171
|
+
*
|
|
172
|
+
* @param {string} version
|
|
173
|
+
* @param {string} range
|
|
174
|
+
* @returns {boolean}
|
|
175
|
+
*/
|
|
176
|
+
function satisfies(version, range) {
|
|
177
|
+
if (!range || range === '*' || range === 'latest') return true;
|
|
178
|
+
try {
|
|
179
|
+
const orGroups = range.split('||');
|
|
180
|
+
return orGroups.some(group => {
|
|
181
|
+
const parts = group.trim().split(/\s+(?=[\^~><=!])/);
|
|
182
|
+
return parts.every(part => parseSimpleRange(part)(version));
|
|
183
|
+
});
|
|
184
|
+
} catch {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** @returns {boolean} True if the range string is valid */
|
|
190
|
+
function validRange(range) {
|
|
191
|
+
try { parseSimpleRange(range); return true; } catch { return false; }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** @returns {Version|null} Parsed version or null if invalid */
|
|
195
|
+
function parse(str) {
|
|
196
|
+
try { return new Version(str); } catch { return null; }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** @returns {string|null} Validated version string or null */
|
|
200
|
+
function valid(str) {
|
|
201
|
+
return parse(str) ? str.trim().replace(/^v/, '') : null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Coerces a dirty string into a valid major.minor.patch version string.
|
|
206
|
+
* @param {string} str
|
|
207
|
+
* @returns {string|null}
|
|
208
|
+
*/
|
|
209
|
+
function coerce(str) {
|
|
210
|
+
const m = str.match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
|
|
211
|
+
if (!m) return null;
|
|
212
|
+
return `${m[1] ?? 0}.${m[2] ?? 0}.${m[3] ?? 0}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Finds the highest version in an array that satisfies a range.
|
|
217
|
+
*
|
|
218
|
+
* @param {string[]} versions
|
|
219
|
+
* @param {string} range
|
|
220
|
+
* @returns {string|null}
|
|
221
|
+
*/
|
|
222
|
+
function maxSatisfying(versions, range) {
|
|
223
|
+
return versions
|
|
224
|
+
.filter(v => { try { return satisfies(v, range); } catch { return false; } })
|
|
225
|
+
.sort((a, b) => compare(b, a))[0] ?? null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Finds the lowest version in an array that satisfies a range.
|
|
230
|
+
*
|
|
231
|
+
* @param {string[]} versions
|
|
232
|
+
* @param {string} range
|
|
233
|
+
* @returns {string|null}
|
|
234
|
+
*/
|
|
235
|
+
function minSatisfying(versions, range) {
|
|
236
|
+
return versions
|
|
237
|
+
.filter(v => { try { return satisfies(v, range); } catch { return false; } })
|
|
238
|
+
.sort((a, b) => compare(a, b))[0] ?? null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Increments a version according to release type.
|
|
243
|
+
*
|
|
244
|
+
* @param {string} version
|
|
245
|
+
* @param {'major'|'minor'|'patch'|'prerelease'} release
|
|
246
|
+
* @returns {string|null}
|
|
247
|
+
*/
|
|
248
|
+
function inc(version, release) {
|
|
249
|
+
const v = new Version(version);
|
|
250
|
+
if (release === 'major') return `${v.major + 1}.0.0`;
|
|
251
|
+
if (release === 'minor') return `${v.major}.${v.minor + 1}.0`;
|
|
252
|
+
if (release === 'patch') return `${v.major}.${v.minor}.${v.patch + 1}`;
|
|
253
|
+
if (release === 'prerelease') {
|
|
254
|
+
if (v.pre.length) {
|
|
255
|
+
const last = parseInt(v.pre[v.pre.length - 1], 10);
|
|
256
|
+
if (!isNaN(last)) {
|
|
257
|
+
return `${v.major}.${v.minor}.${v.patch}-${v.pre.slice(0, -1).concat(last + 1).join('.')}`;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return `${v.major}.${v.minor}.${v.patch + 1}-0`;
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Sorts an array of version strings */
|
|
266
|
+
function sort(versions) {
|
|
267
|
+
return [...versions].sort((a, b) => compare(a, b));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Sorts an array of version strings in reverse */
|
|
271
|
+
function rsort(versions) {
|
|
272
|
+
return [...versions].sort((a, b) => compare(b, a));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
module.exports = {
|
|
276
|
+
Version, compare, gt, lt, gte, lte, eq,
|
|
277
|
+
satisfies, validRange, parse, valid, coerce,
|
|
278
|
+
maxSatisfying, minSatisfying, inc, sort, rsort,
|
|
279
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* System capabilities and environment detection.
|
|
5
|
+
*/
|
|
6
|
+
const system = {
|
|
7
|
+
platform: process.platform, // 'win32', 'linux', 'darwin', etc.
|
|
8
|
+
arch: process.arch, // 'x64', 'arm64', etc.
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Checks if a package's OS/CPU requirements match the current system.
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} meta - Package metadata (must contain 'os' and/or 'cpu' arrays)
|
|
14
|
+
* @returns {boolean} True if the package is compatible or no requirements defined
|
|
15
|
+
*/
|
|
16
|
+
isCompatible(meta) {
|
|
17
|
+
// OS check
|
|
18
|
+
if (meta.os && Array.isArray(meta.os)) {
|
|
19
|
+
const isMatch = meta.os.some(o => {
|
|
20
|
+
if (o.startsWith('!')) return system.platform !== o.slice(1);
|
|
21
|
+
return system.platform === o;
|
|
22
|
+
});
|
|
23
|
+
if (!isMatch) return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// CPU check
|
|
27
|
+
if (meta.cpu && Array.isArray(meta.cpu)) {
|
|
28
|
+
const isMatch = meta.cpu.some(c => {
|
|
29
|
+
if (c.startsWith('!')) return system.arch !== c.slice(1);
|
|
30
|
+
return system.arch === c;
|
|
31
|
+
});
|
|
32
|
+
if (!isMatch) return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
module.exports = system;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const PackageJSON = require('../core/package-json');
|
|
6
|
+
const { mkdirp, symlink } = require('../utils/fs');
|
|
7
|
+
const logger = require('../utils/logger');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Discovers and manages workspaces in a monorepo.
|
|
11
|
+
* Supports glob-like patterns: "packages/*", "apps/*"
|
|
12
|
+
*/
|
|
13
|
+
class Workspace {
|
|
14
|
+
constructor(rootDir) {
|
|
15
|
+
this.rootDir = rootDir;
|
|
16
|
+
this.rootPkg = PackageJSON.fromDir(rootDir);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Returns array of { name, version, dir, pkg } for all workspace packages */
|
|
20
|
+
getPackages() {
|
|
21
|
+
const patterns = this.rootPkg.workspaces;
|
|
22
|
+
if (!patterns || !patterns.length) return [];
|
|
23
|
+
|
|
24
|
+
const packages = [];
|
|
25
|
+
for (const pattern of patterns) {
|
|
26
|
+
const matchedDirs = this._glob(pattern);
|
|
27
|
+
for (const dir of matchedDirs) {
|
|
28
|
+
const pkgFile = path.join(dir, 'package.json');
|
|
29
|
+
if (!fs.existsSync(pkgFile)) continue;
|
|
30
|
+
const pkg = PackageJSON.fromDir(dir);
|
|
31
|
+
packages.push({
|
|
32
|
+
name: pkg.name,
|
|
33
|
+
version: pkg.version,
|
|
34
|
+
dir,
|
|
35
|
+
pkg,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return packages;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Link workspace packages into each other's node_modules
|
|
45
|
+
* so inter-workspace imports work without publishing.
|
|
46
|
+
*/
|
|
47
|
+
async link() {
|
|
48
|
+
const packages = this.getPackages();
|
|
49
|
+
const byName = new Map(packages.map(p => [p.name, p]));
|
|
50
|
+
|
|
51
|
+
for (const ws of packages) {
|
|
52
|
+
const allDeps = {
|
|
53
|
+
...ws.pkg.dependencies,
|
|
54
|
+
...ws.pkg.devDependencies,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
for (const depName of Object.keys(allDeps)) {
|
|
58
|
+
if (!byName.has(depName)) continue;
|
|
59
|
+
const depWs = byName.get(depName);
|
|
60
|
+
|
|
61
|
+
// Create symlink: ws/node_modules/depName → depWs.dir
|
|
62
|
+
const linkPath = path.join(ws.dir, 'node_modules', depName);
|
|
63
|
+
mkdirp(path.dirname(linkPath));
|
|
64
|
+
try {
|
|
65
|
+
symlink(depWs.dir, linkPath);
|
|
66
|
+
logger.verbose(`linked ${depName} → ${depWs.dir} in ${ws.name}`);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
logger.warn(`Could not link ${depName} in ${ws.name}: ${err.message}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
logger.success(`Linked ${packages.length} workspace packages`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Run a script across all (or filtered) workspaces */
|
|
77
|
+
async runScript(scriptName, { filter } = {}) {
|
|
78
|
+
const { spawnSync } = require('node:child_process');
|
|
79
|
+
const packages = this.getPackages().filter(ws =>
|
|
80
|
+
!filter || ws.name.includes(filter)
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
for (const ws of packages) {
|
|
84
|
+
if (!ws.pkg.scripts[scriptName]) {
|
|
85
|
+
logger.verbose(`[${ws.name}] no script "${scriptName}" — skipping`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
logger.section(`▶ ${ws.name} — ${scriptName}`);
|
|
89
|
+
const result = spawnSync(ws.pkg.scripts[scriptName], {
|
|
90
|
+
cwd: ws.dir,
|
|
91
|
+
shell: true,
|
|
92
|
+
stdio: 'inherit',
|
|
93
|
+
});
|
|
94
|
+
if (result.status !== 0) {
|
|
95
|
+
logger.error(`[${ws.name}] script "${scriptName}" failed with exit code ${result.status}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Simple glob: supports "packages/*" — one level wildcard only */
|
|
101
|
+
_glob(pattern) {
|
|
102
|
+
const parts = pattern.split('/');
|
|
103
|
+
let dirs = [this.rootDir];
|
|
104
|
+
|
|
105
|
+
for (const part of parts) {
|
|
106
|
+
const next = [];
|
|
107
|
+
for (const base of dirs) {
|
|
108
|
+
if (part === '*' || part === '**') {
|
|
109
|
+
try {
|
|
110
|
+
for (const entry of fs.readdirSync(base, { withFileTypes: true })) {
|
|
111
|
+
if (entry.isDirectory()) next.push(path.join(base, entry.name));
|
|
112
|
+
}
|
|
113
|
+
} catch { }
|
|
114
|
+
} else {
|
|
115
|
+
const candidate = path.join(base, part);
|
|
116
|
+
if (fs.existsSync(candidate)) next.push(candidate);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
dirs = next;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return dirs;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = Workspace;
|