@tapestry-mud/cli 0.1.0
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/bin/tapestry.js +288 -0
- package/package.json +26 -0
- package/src/commands/create-pack.js +66 -0
- package/src/commands/disable.js +16 -0
- package/src/commands/enable.js +16 -0
- package/src/commands/engine.js +25 -0
- package/src/commands/info.js +51 -0
- package/src/commands/init.js +63 -0
- package/src/commands/install.js +100 -0
- package/src/commands/list.js +50 -0
- package/src/commands/login.js +40 -0
- package/src/commands/outdated.js +43 -0
- package/src/commands/pack.js +24 -0
- package/src/commands/publish.js +63 -0
- package/src/commands/register.js +41 -0
- package/src/commands/search.js +41 -0
- package/src/commands/start.js +9 -0
- package/src/commands/stop.js +9 -0
- package/src/commands/uninstall.js +46 -0
- package/src/commands/update.js +80 -0
- package/src/commands/validate.js +29 -0
- package/src/lib/auth.js +34 -0
- package/src/lib/boot.js +112 -0
- package/src/lib/engine-manager.js +294 -0
- package/src/lib/lock-file.js +21 -0
- package/src/lib/process-tracker.js +28 -0
- package/src/lib/registry-client.js +46 -0
- package/src/lib/semver-resolver.js +75 -0
- package/src/lib/tarball-builder.js +35 -0
- package/src/lib/tarball.js +26 -0
- package/src/scaffold/templates.js +415 -0
- package/src/schema/manifest.js +74 -0
- package/src/util/prompt.js +28 -0
- package/src/util/yaml.js +18 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync, spawn } = require('child_process');
|
|
6
|
+
const { readYaml } = require('../util/yaml');
|
|
7
|
+
const { writePid, readPid, clearPid } = require('./process-tracker');
|
|
8
|
+
|
|
9
|
+
const ENGINE_REPO = 'https://github.com/tapestry-mud/tapestry.git';
|
|
10
|
+
const DEFAULT_IMAGE = 'ghcr.io/tapestry-mud/tapestry';
|
|
11
|
+
const PLATFORM_MAP = { linux: 'linux', darwin: 'osx', win32: 'windows' };
|
|
12
|
+
|
|
13
|
+
// ── Docker helpers ──────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
function dockerPull(image, version) {
|
|
16
|
+
console.log(`Pulling ${image}:${version}...`);
|
|
17
|
+
const result = spawnSync('docker', ['pull', `${image}:${version}`], { stdio: 'inherit' });
|
|
18
|
+
if (result.status !== 0) {
|
|
19
|
+
console.log(`Tag ${version} not found, falling back to ${image}:latest...`);
|
|
20
|
+
const fallback = spawnSync('docker', ['pull', `${image}:latest`], { stdio: 'inherit' });
|
|
21
|
+
if (fallback.status !== 0) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`docker pull failed. Is Docker running and is ${image} a valid image?`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
spawnSync('docker', ['tag', `${image}:latest`, `${image}:${version}`], { stdio: 'inherit' });
|
|
27
|
+
console.log(`Engine image ready: ${image}:${version} (from latest)`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
console.log(`Engine image ready: ${image}:${version}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function dockerStart(projectName, image, version, packsDir, serverYamlPath) {
|
|
34
|
+
const containerName = `tapestry-${projectName}`;
|
|
35
|
+
const result = spawnSync('docker', [
|
|
36
|
+
'run', '--detach',
|
|
37
|
+
'--name', containerName,
|
|
38
|
+
'-p', '4000:4000',
|
|
39
|
+
'-p', '4001:4001',
|
|
40
|
+
'-v', `${packsDir}:/app/packs`,
|
|
41
|
+
'-v', `${serverYamlPath}:/app/server.yaml`,
|
|
42
|
+
`${image}:${version}`,
|
|
43
|
+
], { stdio: 'inherit' });
|
|
44
|
+
if (result.status !== 0) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`docker run failed. Ensure the image exists and no container named '${containerName}' is already running.`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
console.log(`Engine started. Container: ${containerName}`);
|
|
50
|
+
console.log(' Telnet: telnet localhost 4000');
|
|
51
|
+
console.log(' WebSocket: ws://localhost:4001');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function dockerStop(projectName) {
|
|
55
|
+
const containerName = `tapestry-${projectName}`;
|
|
56
|
+
const result = spawnSync('docker', ['stop', containerName], { stdio: 'inherit' });
|
|
57
|
+
if (result.status !== 0) {
|
|
58
|
+
throw new Error(`Failed to stop container '${containerName}'. Is it running?`);
|
|
59
|
+
}
|
|
60
|
+
const rmResult = spawnSync('docker', ['rm', containerName], { stdio: 'inherit' });
|
|
61
|
+
if (rmResult.status !== 0) {
|
|
62
|
+
throw new Error(`Failed to remove container '${containerName}'.`);
|
|
63
|
+
}
|
|
64
|
+
console.log('Engine stopped.');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function dockerInfo(image, version) {
|
|
68
|
+
return { mode: 'docker', version, image: `${image}:${version}` };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Binary helpers ──────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
function binaryExecName() {
|
|
74
|
+
return process.platform === 'win32' ? 'Tapestry.exe' : 'Tapestry';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function binaryInstall(version, installDir) {
|
|
78
|
+
const platform = PLATFORM_MAP[process.platform] || 'linux';
|
|
79
|
+
const url =
|
|
80
|
+
`https://github.com/tapestry-mud/tapestry/releases/download/v${version}/tapestry-${platform}.tar.gz`;
|
|
81
|
+
const binDir = path.join(installDir, 'binary', version);
|
|
82
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
83
|
+
const tarPath = path.join(binDir, 'tapestry.tar.gz');
|
|
84
|
+
|
|
85
|
+
console.log(`Downloading Tapestry engine v${version} for ${platform}...`);
|
|
86
|
+
const dlResult = spawnSync('curl', ['-L', '-o', tarPath, url], { stdio: 'inherit' });
|
|
87
|
+
if (dlResult.status !== 0) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
'Failed to download engine binary. Ensure curl is installed and the version exists on GitHub releases.'
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const exResult = spawnSync('tar', ['-xzf', tarPath, '-C', binDir], { stdio: 'inherit' });
|
|
94
|
+
if (exResult.status !== 0) {
|
|
95
|
+
if (fs.existsSync(tarPath)) {
|
|
96
|
+
fs.unlinkSync(tarPath);
|
|
97
|
+
}
|
|
98
|
+
throw new Error('Failed to extract engine binary.');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (fs.existsSync(tarPath)) {
|
|
102
|
+
fs.unlinkSync(tarPath);
|
|
103
|
+
}
|
|
104
|
+
console.log(`Engine installed to ${binDir}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function binaryStart(version, installDir, packsDir, serverYamlPath, cwd) {
|
|
108
|
+
const binDir = path.join(installDir, 'binary', version);
|
|
109
|
+
const execPath = path.join(binDir, binaryExecName());
|
|
110
|
+
if (!fs.existsSync(execPath)) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`Engine binary not found at ${execPath}. Run tapestry engine install first.`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
const child = spawn(execPath, ['--packs', packsDir, '--config', serverYamlPath], {
|
|
116
|
+
detached: true,
|
|
117
|
+
stdio: 'ignore',
|
|
118
|
+
});
|
|
119
|
+
child.unref();
|
|
120
|
+
writePid(cwd, child.pid);
|
|
121
|
+
console.log(`Engine started (PID ${child.pid}).`);
|
|
122
|
+
console.log(' Telnet: telnet localhost 4000');
|
|
123
|
+
console.log(' WebSocket: ws://localhost:4001');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function binaryInfo(version, installDir) {
|
|
127
|
+
const binDir = path.join(installDir, 'binary', version);
|
|
128
|
+
return { mode: 'binary', version, path: binDir, installed: fs.existsSync(binDir) };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Source helpers ──────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
function sourceInstall(installDir) {
|
|
134
|
+
const sourceDir = path.join(installDir, 'source');
|
|
135
|
+
if (fs.existsSync(sourceDir)) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`Engine source already exists at ${sourceDir}. Run tapestry engine update to pull changes.`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
console.log('Cloning Tapestry engine source...');
|
|
141
|
+
const result = spawnSync('git', ['clone', ENGINE_REPO, sourceDir], { stdio: 'inherit' });
|
|
142
|
+
if (result.status !== 0) {
|
|
143
|
+
throw new Error('git clone failed. Check your network connection.');
|
|
144
|
+
}
|
|
145
|
+
console.log(`Engine source cloned to ${sourceDir}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function sourcePull(installDir) {
|
|
149
|
+
const sourceDir = path.join(installDir, 'source');
|
|
150
|
+
if (!fs.existsSync(sourceDir)) {
|
|
151
|
+
throw new Error('Engine source not found. Run tapestry engine install first.');
|
|
152
|
+
}
|
|
153
|
+
console.log('Pulling engine source updates...');
|
|
154
|
+
const result = spawnSync('git', ['-C', sourceDir, 'pull'], { stdio: 'inherit' });
|
|
155
|
+
if (result.status !== 0) {
|
|
156
|
+
throw new Error('git pull failed. Check your network connection.');
|
|
157
|
+
}
|
|
158
|
+
console.log('Engine source updated.');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function sourceStart(installDir, packsDir, serverYamlPath, cwd) {
|
|
162
|
+
const sourceDir = path.join(installDir, 'source');
|
|
163
|
+
if (!fs.existsSync(sourceDir)) {
|
|
164
|
+
throw new Error('Engine source not found. Run tapestry engine install first.');
|
|
165
|
+
}
|
|
166
|
+
const child = spawn(
|
|
167
|
+
'dotnet',
|
|
168
|
+
['run', '--', '--packs', packsDir, '--config', serverYamlPath],
|
|
169
|
+
{ cwd: sourceDir, detached: true, stdio: 'ignore' }
|
|
170
|
+
);
|
|
171
|
+
child.unref();
|
|
172
|
+
writePid(cwd, child.pid);
|
|
173
|
+
console.log(`Engine started via dotnet run (PID ${child.pid}).`);
|
|
174
|
+
console.log(' Telnet: telnet localhost 4000');
|
|
175
|
+
console.log(' WebSocket: ws://localhost:4001');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function sourceInfo(version, installDir) {
|
|
179
|
+
const sourceDir = path.join(installDir, 'source');
|
|
180
|
+
return { mode: 'source', version, path: sourceDir, installed: fs.existsSync(sourceDir) };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Shared process helpers ──────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
function processStop(cwd) {
|
|
186
|
+
const pid = readPid(cwd);
|
|
187
|
+
if (!pid) {
|
|
188
|
+
throw new Error('Engine is not running (no .tapestry.pid found).');
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
process.kill(pid, 'SIGTERM');
|
|
192
|
+
} catch (_e) {
|
|
193
|
+
// process already gone — clear the pid file and report success
|
|
194
|
+
}
|
|
195
|
+
clearPid(cwd);
|
|
196
|
+
console.log('Engine stopped.');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── Config reader ───────────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
function readEngineConfig(cwd) {
|
|
202
|
+
const manifestPath = path.join(cwd, 'tapestry.yaml');
|
|
203
|
+
if (!fs.existsSync(manifestPath)) {
|
|
204
|
+
throw new Error('No tapestry.yaml found. Run tapestry init first.');
|
|
205
|
+
}
|
|
206
|
+
const manifest = readYaml(manifestPath);
|
|
207
|
+
const engine = manifest.engine;
|
|
208
|
+
if (!engine || typeof engine !== 'object') {
|
|
209
|
+
throw new Error(
|
|
210
|
+
'engine must be configured as an object in tapestry.yaml:\n' +
|
|
211
|
+
' engine:\n version: "3.1.0"\n mode: "docker"'
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
if (!engine.version) {
|
|
215
|
+
throw new Error('engine.version is required in tapestry.yaml');
|
|
216
|
+
}
|
|
217
|
+
if (!['docker', 'binary', 'source'].includes(engine.mode)) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
`engine.mode must be docker, binary, or source. Got: ${engine.mode}`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
version: engine.version,
|
|
224
|
+
mode: engine.mode,
|
|
225
|
+
image: engine.image || DEFAULT_IMAGE,
|
|
226
|
+
installDir: path.join(cwd, '.tapestry-engine'),
|
|
227
|
+
projectName: (manifest.name || 'tapestry').toLowerCase().replace(/[^a-z0-9-]+/g, '-'),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
async function installEngine(cwd) {
|
|
234
|
+
const config = readEngineConfig(cwd);
|
|
235
|
+
if (config.mode === 'docker') {
|
|
236
|
+
dockerPull(config.image, config.version);
|
|
237
|
+
} else if (config.mode === 'binary') {
|
|
238
|
+
binaryInstall(config.version, config.installDir);
|
|
239
|
+
} else {
|
|
240
|
+
sourceInstall(config.installDir);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function updateEngine(cwd) {
|
|
245
|
+
const config = readEngineConfig(cwd);
|
|
246
|
+
if (config.mode === 'docker') {
|
|
247
|
+
dockerPull(config.image, config.version);
|
|
248
|
+
} else if (config.mode === 'binary') {
|
|
249
|
+
binaryInstall(config.version, config.installDir);
|
|
250
|
+
} else {
|
|
251
|
+
sourcePull(config.installDir);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function getEngineInfo(cwd) {
|
|
256
|
+
const config = readEngineConfig(cwd);
|
|
257
|
+
if (config.mode === 'docker') {
|
|
258
|
+
return dockerInfo(config.image, config.version);
|
|
259
|
+
}
|
|
260
|
+
if (config.mode === 'binary') {
|
|
261
|
+
return binaryInfo(config.version, config.installDir);
|
|
262
|
+
}
|
|
263
|
+
return sourceInfo(config.version, config.installDir);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function startEngine(cwd) {
|
|
267
|
+
const config = readEngineConfig(cwd);
|
|
268
|
+
const packsDir = path.resolve(cwd, 'packs');
|
|
269
|
+
const serverYamlPath = path.resolve(cwd, 'server.yaml');
|
|
270
|
+
if (!fs.existsSync(packsDir)) {
|
|
271
|
+
throw new Error('packs/ directory not found. Run tapestry install first.');
|
|
272
|
+
}
|
|
273
|
+
if (!fs.existsSync(serverYamlPath)) {
|
|
274
|
+
throw new Error('server.yaml not found in the current directory.');
|
|
275
|
+
}
|
|
276
|
+
if (config.mode === 'docker') {
|
|
277
|
+
dockerStart(config.projectName, config.image, config.version, packsDir, serverYamlPath);
|
|
278
|
+
} else if (config.mode === 'binary') {
|
|
279
|
+
binaryStart(config.version, config.installDir, packsDir, serverYamlPath, cwd);
|
|
280
|
+
} else {
|
|
281
|
+
sourceStart(config.installDir, packsDir, serverYamlPath, cwd);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function stopEngine(cwd) {
|
|
286
|
+
const config = readEngineConfig(cwd);
|
|
287
|
+
if (config.mode === 'docker') {
|
|
288
|
+
dockerStop(config.projectName);
|
|
289
|
+
} else {
|
|
290
|
+
processStop(cwd);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
module.exports = { installEngine, updateEngine, getEngineInfo, startEngine, stopEngine };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { readYaml, writeYaml } = require('../util/yaml');
|
|
6
|
+
|
|
7
|
+
const LOCK_FILE = 'tapestry-lock.yaml';
|
|
8
|
+
|
|
9
|
+
function readLock(cwd) {
|
|
10
|
+
const lockPath = path.join(cwd, LOCK_FILE);
|
|
11
|
+
if (!fs.existsSync(lockPath)) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return readYaml(lockPath);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function writeLock(cwd, lock) {
|
|
18
|
+
writeYaml(path.join(cwd, LOCK_FILE), lock);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { readLock, writeLock };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const PID_FILE = '.tapestry.pid';
|
|
7
|
+
|
|
8
|
+
function writePid(cwd, pid) {
|
|
9
|
+
fs.writeFileSync(path.join(cwd, PID_FILE), String(pid), 'utf8');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function readPid(cwd) {
|
|
13
|
+
const pidFile = path.join(cwd, PID_FILE);
|
|
14
|
+
if (!fs.existsSync(pidFile)) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
|
|
18
|
+
return isNaN(pid) || pid <= 0 ? null : pid;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function clearPid(cwd) {
|
|
22
|
+
const pidFile = path.join(cwd, PID_FILE);
|
|
23
|
+
if (fs.existsSync(pidFile)) {
|
|
24
|
+
fs.unlinkSync(pidFile);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = { writePid, readPid, clearPid };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fetch = require('node-fetch');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_REGISTRY = process.env.TAPESTRY_REGISTRY || 'https://registry.tapestryengine.com';
|
|
6
|
+
|
|
7
|
+
function validatePackageName(name) {
|
|
8
|
+
if (!name || typeof name !== 'string') {
|
|
9
|
+
throw new Error('Package name must be a non-empty string');
|
|
10
|
+
}
|
|
11
|
+
if (!/^@[a-z0-9-]+\/[a-z0-9-]+/.test(name)) {
|
|
12
|
+
throw new Error(`Invalid package name: ${name}. Expected @scope/name format`);
|
|
13
|
+
}
|
|
14
|
+
if (name.includes('..') || name.includes('//')) {
|
|
15
|
+
throw new Error(`Invalid package name: ${name}. Path traversal not allowed`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function fetchPackageMetadata(name, registryUrl = DEFAULT_REGISTRY) {
|
|
20
|
+
validatePackageName(name);
|
|
21
|
+
const url = `${registryUrl}/v1/packages/${name}`;
|
|
22
|
+
const res = await fetch(url);
|
|
23
|
+
if (!res.ok) {
|
|
24
|
+
if (res.status === 404) {
|
|
25
|
+
throw new Error(`Package ${name} not found in registry`);
|
|
26
|
+
}
|
|
27
|
+
const body = await res.text();
|
|
28
|
+
throw new Error(`Registry error ${res.status}: ${body}`);
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
return await res.json();
|
|
32
|
+
} catch (e) {
|
|
33
|
+
throw new Error(`Invalid JSON response from registry for ${name}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function fetchTarball(url) {
|
|
38
|
+
const res = await fetch(url);
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
const body = await res.text();
|
|
41
|
+
throw new Error(`Tarball download failed: ${res.status}: ${body}`);
|
|
42
|
+
}
|
|
43
|
+
return res.buffer();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { fetchPackageMetadata, fetchTarball, DEFAULT_REGISTRY };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const semver = require('semver');
|
|
4
|
+
const { fetchPackageMetadata } = require('./registry-client');
|
|
5
|
+
|
|
6
|
+
async function resolve(dependencies, registryUrl) {
|
|
7
|
+
if (!dependencies || Object.keys(dependencies).length === 0) {
|
|
8
|
+
return {};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const baseUrl = registryUrl.replace(/\/$/, '');
|
|
12
|
+
|
|
13
|
+
const resolved = {};
|
|
14
|
+
const resolvedBy = {};
|
|
15
|
+
const queue = Object.entries(dependencies).map(([name, range]) => ({
|
|
16
|
+
name,
|
|
17
|
+
range,
|
|
18
|
+
requiredBy: 'root',
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
while (queue.length > 0) {
|
|
22
|
+
const { name, range, requiredBy } = queue.shift();
|
|
23
|
+
|
|
24
|
+
if (resolved[name]) {
|
|
25
|
+
if (!semver.satisfies(resolved[name].version, range)) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`CONFLICT: ${resolvedBy[name].requiredBy} requires ${name}@${resolvedBy[name].range}\n` +
|
|
28
|
+
` ${requiredBy} requires ${name}@${range}\n` +
|
|
29
|
+
` No version satisfies both ranges.`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
resolvedBy[name] = { range, requiredBy };
|
|
36
|
+
|
|
37
|
+
const meta = await fetchPackageMetadata(name, baseUrl);
|
|
38
|
+
const versions = meta.versions.map((v) => v.version);
|
|
39
|
+
const best = semver.maxSatisfying(versions, range);
|
|
40
|
+
|
|
41
|
+
if (!best) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`No version of ${name} satisfies ${range}. Available: ${versions.join(', ') || 'none'}`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const versionData = meta.versions.find((v) => v.version === best);
|
|
48
|
+
const manifest =
|
|
49
|
+
typeof versionData.manifest === 'string'
|
|
50
|
+
? JSON.parse(versionData.manifest)
|
|
51
|
+
: versionData.manifest;
|
|
52
|
+
|
|
53
|
+
resolved[name] = {
|
|
54
|
+
version: best,
|
|
55
|
+
integrity: versionData.integrity,
|
|
56
|
+
tarball: `${baseUrl}/v1/packages/${name}/${best}.tgz`,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const transDeps = manifest.dependencies || {};
|
|
60
|
+
for (const [depName, depRange] of Object.entries(transDeps)) {
|
|
61
|
+
queue.push({ name: depName, range: depRange, requiredBy: `${name}@${best}` });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const peerDeps = manifest.peerDependencies || {};
|
|
65
|
+
for (const [peerName, peerRange] of Object.entries(peerDeps)) {
|
|
66
|
+
if (!resolved[peerName] && !dependencies[peerName]) {
|
|
67
|
+
console.warn(` warn: optional peer ${peerName}@${peerRange} not installed`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return resolved;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { resolve };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const tar = require('tar');
|
|
6
|
+
|
|
7
|
+
const EXCLUDE = new Set(['.git', 'node_modules', '.DS_Store']);
|
|
8
|
+
|
|
9
|
+
function shouldInclude(filePath) {
|
|
10
|
+
if (filePath.endsWith('.tgz')) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
return !filePath.split('/').some((part) => EXCLUDE.has(part));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function buildTarball(packDir, outputPath) {
|
|
17
|
+
await tar.create(
|
|
18
|
+
{
|
|
19
|
+
file: outputPath,
|
|
20
|
+
gzip: true,
|
|
21
|
+
cwd: packDir,
|
|
22
|
+
prefix: 'package',
|
|
23
|
+
filter: (p) => shouldInclude(p),
|
|
24
|
+
},
|
|
25
|
+
['.']
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function computeIntegrity(filePath) {
|
|
30
|
+
const data = fs.readFileSync(filePath);
|
|
31
|
+
const hash = crypto.createHash('sha256').update(data).digest('base64');
|
|
32
|
+
return `sha256-${hash}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { buildTarball, computeIntegrity };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const tar = require('tar');
|
|
6
|
+
|
|
7
|
+
function verifyIntegrity(buffer, expected) {
|
|
8
|
+
const hash = crypto.createHash('sha256').update(buffer).digest('base64');
|
|
9
|
+
const computed = `sha256-${hash}`;
|
|
10
|
+
if (computed !== expected) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
`Integrity check failed\n expected: ${expected}\n got: ${computed}`
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function saveTarball(buffer, destPath) {
|
|
18
|
+
fs.writeFileSync(destPath, buffer);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function extractTarball(tarballPath, destDir) {
|
|
22
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
23
|
+
await tar.extract({ file: tarballPath, cwd: destDir, strip: 1 });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = { verifyIntegrity, saveTarball, extractTarball };
|