@tapestry-mud/cli 0.4.0 → 0.6.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/SECURITY.md ADDED
@@ -0,0 +1,93 @@
1
+ # Security Policy
2
+
3
+ We take the security of Tapestry and its surrounding tooling seriously. This
4
+ document explains how to report a vulnerability and what we do to keep the
5
+ project and its supply chain safe.
6
+
7
+ ## Reporting a Vulnerability
8
+
9
+ **Please do not open a public issue or pull request for security problems.**
10
+ Public reports tip off attackers before a fix is available.
11
+
12
+ Instead, report privately through GitHub:
13
+
14
+ 1. Go to this repository's **Security** tab.
15
+ 2. Click **Report a vulnerability** (GitHub Private Vulnerability Reporting).
16
+ 3. Describe the issue, the affected version or commit, and steps to reproduce.
17
+
18
+ If you can include a proof of concept, impact assessment, or suggested fix, that
19
+ helps us triage faster — but it isn't required to file a report.
20
+
21
+ ### What to expect
22
+
23
+ - **A best-effort acknowledgement within 72 hours** of your report.
24
+ - An assessment and, where confirmed, a plan and rough timeline for a fix.
25
+ - **Coordinated disclosure:** we develop and release the fix before publishing
26
+ details, then credit you in the advisory unless you'd prefer to remain
27
+ anonymous.
28
+
29
+ This is a small, fast-moving project, so timelines are best-effort — but we will
30
+ keep you informed.
31
+
32
+ ## Supported Versions
33
+
34
+ Tapestry is pre-1.0 and moves quickly. Security fixes are applied to the latest
35
+ release and the `master` branch only.
36
+
37
+ | Version | Supported |
38
+ | ------------------ | ------------------ |
39
+ | Latest release | :white_check_mark: |
40
+ | `master` (default) | :white_check_mark: |
41
+ | Older releases | :x: |
42
+
43
+ If you're running an older tagged release, upgrade to the latest before
44
+ reporting — the issue may already be fixed.
45
+
46
+ ## How We Protect the Supply Chain
47
+
48
+ Supply-chain attacks (compromised dependencies, malicious package updates,
49
+ hijacked CI) are a primary threat for any project that publishes artifacts. Our
50
+ standing measures:
51
+
52
+ - **Exact-pinned dependencies.** Every dependency is pinned to an exact version
53
+ — no `^` or `~` ranges — so a malicious upstream release can't be pulled in
54
+ silently by a version range.
55
+ - **Install cooldown for new packages.** Package installs enforce a minimum
56
+ release age, so freshly published versions are not installed immediately. This
57
+ blunts worm-style compromises that rely on rapid propagation in the hours
58
+ after a malicious release.
59
+ - **CI actions pinned to commit SHAs.** GitHub Actions are referenced by full
60
+ commit SHA rather than mutable tags, so a retagged or hijacked action can't
61
+ alter our builds.
62
+ - **Least-privilege CI.** Workflows declare scoped `permissions:` blocks and use
63
+ short-lived, narrowly scoped credentials — the built-in `GITHUB_TOKEN`,
64
+ OpenID Connect (OIDC) tokens, and scoped GitHub App tokens — instead of
65
+ long-lived personal access tokens or stored secrets.
66
+ - **Protected default branch.** Changes to `master` require a pull request, at
67
+ least one review, and passing status checks before merge.
68
+
69
+ ## For Contributors and Pack Authors
70
+
71
+ - **Don't introduce unpinned dependencies.** Match the existing exact-version
72
+ pinning. PRs that add `^`/`~` ranges will be asked to pin.
73
+ - **Report suspicious dependency behavior.** If a dependency starts doing
74
+ something unexpected — unfamiliar network calls, new lifecycle scripts,
75
+ surprising postinstall steps — report it via the process above.
76
+ - **Treat third-party content packs as untrusted.** Packs execute JavaScript
77
+ (via Jint) inside the engine. Only run packs you trust, and review pack
78
+ scripts before loading them into a server you care about.
79
+
80
+ ## Scope
81
+
82
+ **In scope:** the Tapestry engine, registry, CLI, web client, and the official
83
+ content-pack tooling maintained under the `tapestry-mud` organization.
84
+
85
+ **Out of scope:**
86
+
87
+ - Misconfiguration of your own self-hosted deployment (firewall rules, exposed
88
+ admin ports, weak operator passwords, etc.).
89
+ - Third-party or community content packs not authored by this project.
90
+ - Vulnerabilities in upstream dependencies — report those upstream, though we
91
+ appreciate a heads-up so we can pin around them.
92
+
93
+ Thank you for helping keep Tapestry and its users safe.
package/bin/tapestry.js CHANGED
@@ -18,6 +18,7 @@ const { publish } = require('../src/commands/publish');
18
18
  const { search } = require('../src/commands/search');
19
19
  const { info } = require('../src/commands/info');
20
20
  const { list } = require('../src/commands/list');
21
+ const { link, unlink, linkList } = require('../src/commands/link');
21
22
  const { outdated } = require('../src/commands/outdated');
22
23
  const { engineInstall, engineUpdate, engineInfo } = require('../src/commands/engine');
23
24
  const { engineVersions } = require('../src/commands/engine-versions');
@@ -26,7 +27,7 @@ const { stopCmd } = require('../src/commands/stop');
26
27
  const { changePassword } = require('../src/commands/change-password');
27
28
  const { unpublish } = require('../src/commands/unpublish');
28
29
  const { distTagSet, distTagList } = require('../src/commands/dist-tag');
29
- const { presetSet } = require('../src/commands/preset');
30
+ const { presetSet, presetDelete } = require('../src/commands/preset');
30
31
 
31
32
  const program = new Command();
32
33
 
@@ -40,7 +41,7 @@ program.configureHelp({
40
41
  const groups = [
41
42
  {
42
43
  title: 'Pack Management',
43
- commands: ['uninstall', 'update', 'list', 'enable', 'disable', 'outdated'],
44
+ commands: ['uninstall', 'update', 'list', 'enable', 'disable', 'outdated', 'link', 'unlink'],
44
45
  },
45
46
  {
46
47
  title: 'Engine',
@@ -168,6 +169,18 @@ presetCmd
168
169
  }
169
170
  });
170
171
 
172
+ presetCmd
173
+ .command('delete <name>')
174
+ .description('Delete a preset from the registry')
175
+ .action(async (name) => {
176
+ try {
177
+ await presetDelete(name);
178
+ } catch (e) {
179
+ console.error(`error: ${e.message}`);
180
+ process.exit(1);
181
+ }
182
+ });
183
+
171
184
  program
172
185
  .command('install [package]')
173
186
  .description('Install a package or all dependencies from tapestry.yaml')
@@ -337,6 +350,36 @@ program
337
350
  }
338
351
  });
339
352
 
353
+ program
354
+ .command('link [path]')
355
+ .description('Attach a local pack working copy to this project (use --list to show links)')
356
+ .option('--list', 'List active links instead of creating one')
357
+ .option('--skip-install', 'Skip dependency resolution; warn about missing deps instead')
358
+ .action(async (linkPath, options) => {
359
+ try {
360
+ if (options.list || !linkPath) {
361
+ await linkList();
362
+ } else {
363
+ await link(linkPath, { noInstall: !!options.skipInstall });
364
+ }
365
+ } catch (e) {
366
+ console.error(`error: ${e.message}`);
367
+ process.exit(1);
368
+ }
369
+ });
370
+
371
+ program
372
+ .command('unlink <name>')
373
+ .description('Detach a linked pack and restore the registry copy on next install')
374
+ .action(async (name) => {
375
+ try {
376
+ await unlink(name);
377
+ } catch (e) {
378
+ console.error(`error: ${e.message}`);
379
+ process.exit(1);
380
+ }
381
+ });
382
+
340
383
  const engineCmd = program.command('engine').description('Manage the Tapestry engine');
341
384
 
342
385
  engineCmd
package/package.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "@tapestry-mud/cli",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "CLI for the Tapestry MUD engine",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/tapestry-mud/tapestry-cli.git"
8
+ },
5
9
  "bin": {
6
10
  "tapestry": "./bin/tapestry.js"
7
11
  },
@@ -12,16 +16,16 @@
12
16
  "access": "public"
13
17
  },
14
18
  "dependencies": {
15
- "commander": "^11.1.0",
16
- "form-data": "^4.0.5",
17
- "inquirer": "^8.2.6",
18
- "js-yaml": "^4.1.0",
19
- "node-fetch": "^2.7.0",
20
- "semver": "^7.6.2",
21
- "tar": "^7.5.15",
22
- "zod": "^3.22.4"
19
+ "commander": "11.1.0",
20
+ "form-data": "4.0.5",
21
+ "inquirer": "8.2.7",
22
+ "js-yaml": "4.1.1",
23
+ "node-fetch": "2.7.0",
24
+ "semver": "7.6.2",
25
+ "tar": "7.5.15",
26
+ "zod": "3.22.4"
23
27
  },
24
28
  "devDependencies": {
25
- "jest": "^29.7.0"
29
+ "jest": "29.7.0"
26
30
  }
27
31
  }
@@ -238,7 +238,7 @@ async function init(cwd, { registryUrl = DEFAULT_REGISTRY, yes = false, prompter
238
238
  fs.mkdirSync(path.join(cwd, 'packs'), { recursive: true });
239
239
  fs.writeFileSync(
240
240
  path.join(cwd, '.gitignore'),
241
- '# Installed packages (managed by tapestry install)\npacks/\n\n# Engine artifacts (managed by tapestry engine install)\n.tapestry-engine/\n\n# Game data (players, saves)\ndata/\n'
241
+ '# Installed packages (managed by tapestry install)\npacks/\n\n# Local pack links (managed by tapestry link)\ntapestry-links.yaml\n\n# Engine artifacts (managed by tapestry engine install)\n.tapestry-engine/\n\n# Game data (players, saves)\ndata/\n'
242
242
  );
243
243
 
244
244
  console.log('');
@@ -11,6 +11,7 @@ const { verifyIntegrity, saveTarball, extractTarball } = require('../lib/tarball
11
11
  const { addPackageToBoot } = require('../lib/boot');
12
12
  const { loadToken } = require('../lib/auth');
13
13
  const { PACK_MANIFEST } = require('../lib/manifest');
14
+ const { readLinks } = require('../lib/links');
14
15
 
15
16
  function packInstallPath(cwd, packageName) {
16
17
  const parts = packageName.split('/');
@@ -34,7 +35,12 @@ function isLockCurrent(manifestDeps, lock) {
34
35
  }
35
36
 
36
37
  async function installResolved(cwd, resolved, token) {
38
+ const { links } = readLinks(cwd);
37
39
  for (const [packageName, info] of Object.entries(resolved)) {
40
+ if (packageName in links) {
41
+ console.log(` skipping ${packageName} (linked)`);
42
+ continue;
43
+ }
38
44
  const destDir = packInstallPath(cwd, packageName);
39
45
 
40
46
  if (fs.existsSync(destDir)) {
@@ -98,13 +104,22 @@ async function install(packageArg, { cwd = process.cwd(), registryUrl = DEFAULT_
98
104
 
99
105
  writeYaml(manifestPath, manifest);
100
106
  } else {
107
+ const { links } = readLinks(cwd);
108
+ for (const name of Object.keys(links)) {
109
+ if ((manifest.dependencies || {})[name] !== undefined) {
110
+ console.log(` skipping ${name} (linked)`);
111
+ }
112
+ }
113
+ const depsToResolve = Object.fromEntries(
114
+ Object.entries(manifest.dependencies || {}).filter(([name]) => !(name in links))
115
+ );
101
116
  const lock = readLock(cwd);
102
- if (lock && isLockCurrent(manifest.dependencies || {}, lock)) {
117
+ if (lock && isLockCurrent(depsToResolve, lock)) {
103
118
  console.log('Installing from lock file...');
104
119
  resolved = lock.resolved;
105
120
  } else {
106
121
  console.log('Resolving dependencies...');
107
- resolved = await resolve(manifest.dependencies || {}, registryUrl, token);
122
+ resolved = await resolve(depsToResolve, registryUrl, token);
108
123
  }
109
124
  }
110
125
 
@@ -114,4 +129,4 @@ async function install(packageArg, { cwd = process.cwd(), registryUrl = DEFAULT_
114
129
  console.log('Done.');
115
130
  }
116
131
 
117
- module.exports = { install };
132
+ module.exports = { install, installResolved, packInstallPath };
@@ -0,0 +1,136 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const {
6
+ readLinks, addLink, removeLink, readPackManifest,
7
+ removeMaterializedLink, checkMissingDeps, partitionDeps,
8
+ } = require('../lib/links');
9
+ const { addPackageToBoot, removePackageFromBoot } = require('../lib/boot');
10
+ const { resolve } = require('../lib/semver-resolver');
11
+ const { installResolved, packInstallPath } = require('./install');
12
+ const { readLock, writeLock } = require('../lib/lock-file');
13
+ const { loadToken } = require('../lib/auth');
14
+ const { DEFAULT_REGISTRY } = require('../lib/registry-client');
15
+
16
+ function requireProject(cwd) {
17
+ if (!fs.existsSync(path.join(cwd, 'tapestry.yaml'))) {
18
+ throw new Error('No tapestry.yaml found. Run `tapestry init` first.');
19
+ }
20
+ }
21
+
22
+ function ensureGitignore(cwd) {
23
+ const gi = path.join(cwd, '.gitignore');
24
+ if (!fs.existsSync(gi)) {
25
+ return;
26
+ }
27
+ const body = fs.readFileSync(gi, 'utf8');
28
+ if (!body.split(/\r?\n/).includes('tapestry-links.yaml')) {
29
+ fs.appendFileSync(gi, `${body.endsWith('\n') ? '' : '\n'}tapestry-links.yaml\n`);
30
+ }
31
+ }
32
+
33
+ async function link(targetPath, { cwd = process.cwd(), noInstall = false, registryUrl = DEFAULT_REGISTRY } = {}) {
34
+ requireProject(cwd);
35
+ const absPath = path.resolve(cwd, targetPath);
36
+ if (!fs.existsSync(absPath)) {
37
+ throw new Error(`Path not found: ${absPath}`);
38
+ }
39
+ const manifest = readPackManifest(absPath);
40
+ const name = manifest.name;
41
+ if (!name) {
42
+ throw new Error(`Pack at ${absPath} has no 'name' in its manifest`);
43
+ }
44
+
45
+ addLink(cwd, name, absPath);
46
+ addPackageToBoot(cwd, name, manifest);
47
+ ensureGitignore(cwd);
48
+
49
+ // Warn if the pack is marked inactive — fires on all paths
50
+ if (manifest.active === false) {
51
+ console.warn(` warning: ${name} is marked active: false; it will not load until activated`);
52
+ }
53
+
54
+ if (noInstall) {
55
+ console.log(`linked ${name} -> ${absPath}`);
56
+ for (const dep of checkMissingDeps(cwd, manifest)) {
57
+ const range = manifest.dependencies[dep];
58
+ console.warn(` warning: missing dependency ${dep} (${range}) -- run: tapestry install ${dep}`);
59
+ }
60
+ return;
61
+ }
62
+
63
+ const { needsInstall } = partitionDeps(cwd, manifest);
64
+
65
+ if (Object.keys(needsInstall).length === 0) {
66
+ console.log(`linked ${name} -> ${absPath}`);
67
+ return;
68
+ }
69
+
70
+ let toRollback = [];
71
+ try {
72
+ const token = loadToken();
73
+ const resolved = await resolve(needsInstall, registryUrl, token);
74
+
75
+ // New installs: not on disk yet. Upgrade targets: in needsInstall AND on disk
76
+ // (installResolved deletes the old dir before downloading; track so rollback removes the boot entry)
77
+ toRollback = [
78
+ ...Object.keys(resolved).filter((n) => !fs.existsSync(packInstallPath(cwd, n))),
79
+ ...Object.keys(needsInstall).filter((n) => fs.existsSync(packInstallPath(cwd, n))),
80
+ ];
81
+
82
+ await installResolved(cwd, resolved, token);
83
+
84
+ const existingLock = readLock(cwd);
85
+ const mergedResolved = Object.assign({}, (existingLock && existingLock.resolved) || {}, resolved);
86
+ writeLock(cwd, {
87
+ lockfile_version: 1,
88
+ ...(existingLock && existingLock.deps_hash ? { deps_hash: existingLock.deps_hash } : {}),
89
+ resolved: mergedResolved,
90
+ });
91
+
92
+ console.log(`linked ${name} -> ${absPath}`);
93
+ for (const [pkgName, info] of Object.entries(resolved)) {
94
+ console.log(` installed ${pkgName}@${info.version} (dependency of ${name})`);
95
+ }
96
+ } catch (err) {
97
+ removeLink(cwd, name);
98
+ removePackageFromBoot(cwd, name);
99
+ for (const pkgName of toRollback) {
100
+ const installPath = packInstallPath(cwd, pkgName);
101
+ if (fs.existsSync(installPath)) {
102
+ fs.rmSync(installPath, { recursive: true });
103
+ }
104
+ removePackageFromBoot(cwd, pkgName);
105
+ }
106
+ throw new Error(
107
+ `Cannot resolve dependencies for ${name} — ${err.message}. Use --skip-install to link without dependency resolution.`
108
+ );
109
+ }
110
+ }
111
+
112
+ async function unlink(name, { cwd = process.cwd() } = {}) {
113
+ requireProject(cwd);
114
+ if (!removeLink(cwd, name)) {
115
+ throw new Error(`${name} is not linked`);
116
+ }
117
+ removeMaterializedLink(cwd, name);
118
+ removePackageFromBoot(cwd, name);
119
+ console.log(`unlinked ${name}`);
120
+ console.log(` run 'tapestry install' to restore the registry copy`);
121
+ }
122
+
123
+ async function linkList({ cwd = process.cwd() } = {}) {
124
+ const { links } = readLinks(cwd);
125
+ const entries = Object.entries(links);
126
+ if (entries.length === 0) {
127
+ console.log('No linked packs.');
128
+ return;
129
+ }
130
+ for (const [name, absPath] of entries) {
131
+ const flag = fs.existsSync(absPath) ? '' : ' (MISSING)';
132
+ console.log(`${name} -> ${absPath}${flag}`);
133
+ }
134
+ }
135
+
136
+ module.exports = { link, unlink, linkList };
@@ -6,6 +6,7 @@ const { readLock } = require('../lib/lock-file');
6
6
  const { readBoot } = require('../lib/boot');
7
7
  const { readYaml } = require('../util/yaml');
8
8
  const { PACK_MANIFEST } = require('../lib/manifest');
9
+ const { readLinks } = require('../lib/links');
9
10
 
10
11
  function packInstallPath(cwd, packageName) {
11
12
  const parts = packageName.split('/');
@@ -14,37 +15,46 @@ function packInstallPath(cwd, packageName) {
14
15
 
15
16
  async function list({ cwd = process.cwd() } = {}) {
16
17
  const lock = readLock(cwd);
17
- if (!lock || !lock.resolved || !Object.keys(lock.resolved).length) {
18
- console.log('No packages installed.');
19
- return;
20
- }
21
-
22
- const boot = readBoot(cwd);
23
- const packages = Object.entries(lock.resolved);
18
+ if (lock && lock.resolved && Object.keys(lock.resolved).length) {
19
+ const boot = readBoot(cwd);
20
+ const packages = Object.entries(lock.resolved);
24
21
 
25
- const nameWidth = Math.max(7, ...packages.map(([n]) => n.length));
26
- const verWidth = Math.max(7, ...packages.map(([, r]) => r.version.length));
22
+ const nameWidth = Math.max(7, ...packages.map(([n]) => n.length));
23
+ const verWidth = Math.max(7, ...packages.map(([, r]) => r.version.length));
27
24
 
28
- console.log(
29
- `${'PACKAGE'.padEnd(nameWidth)} ${'VERSION'.padEnd(verWidth)} TYPE STATUS`
30
- );
31
-
32
- for (const [pkgName, resolved] of packages) {
33
- const enabled = boot.packs[pkgName]?.enabled !== false ? 'enabled' : 'disabled';
25
+ console.log(
26
+ `${'PACKAGE'.padEnd(nameWidth)} ${'VERSION'.padEnd(verWidth)} TYPE STATUS`
27
+ );
34
28
 
35
- let type = '';
36
- const packManifestPath = path.join(packInstallPath(cwd, pkgName), PACK_MANIFEST);
37
- if (fs.existsSync(packManifestPath)) {
38
- try {
39
- type = readYaml(packManifestPath).type || '';
40
- } catch {
41
- //
29
+ for (const [pkgName, resolved] of packages) {
30
+ const enabled = boot.packs[pkgName]?.enabled !== false ? 'enabled' : 'disabled';
31
+
32
+ let type = '';
33
+ const packManifestPath = path.join(packInstallPath(cwd, pkgName), PACK_MANIFEST);
34
+ if (fs.existsSync(packManifestPath)) {
35
+ try {
36
+ type = readYaml(packManifestPath).type || '';
37
+ } catch {
38
+ //
39
+ }
42
40
  }
41
+
42
+ console.log(
43
+ `${pkgName.padEnd(nameWidth)} ${resolved.version.padEnd(verWidth)} ${type.padEnd(8)} ${enabled}`
44
+ );
43
45
  }
46
+ } else {
47
+ console.log('No packages installed.');
48
+ }
44
49
 
45
- console.log(
46
- `${pkgName.padEnd(nameWidth)} ${resolved.version.padEnd(verWidth)} ${type.padEnd(8)} ${enabled}`
47
- );
50
+ const { links } = readLinks(cwd);
51
+ const linkEntries = Object.entries(links);
52
+ if (linkEntries.length) {
53
+ console.log('\nLinked:');
54
+ for (const [name, absPath] of linkEntries) {
55
+ const flag = fs.existsSync(absPath) ? '' : ' (MISSING)';
56
+ console.log(` ${name} -> ${absPath}${flag}`);
57
+ }
48
58
  }
49
59
  }
50
60
 
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { requireToken } = require('../lib/auth');
4
- const { patchPreset, DEFAULT_REGISTRY } = require('../lib/registry-client');
4
+ const { patchPreset, deletePreset, DEFAULT_REGISTRY } = require('../lib/registry-client');
5
5
 
6
6
  async function presetSet(name, version, engineChannel, packs, { registryUrl = DEFAULT_REGISTRY } = {}) {
7
7
  const token = requireToken();
@@ -10,4 +10,11 @@ async function presetSet(name, version, engineChannel, packs, { registryUrl = DE
10
10
  console.log('Done.');
11
11
  }
12
12
 
13
- module.exports = { presetSet };
13
+ async function presetDelete(name, { registryUrl = DEFAULT_REGISTRY } = {}) {
14
+ const token = requireToken();
15
+ await deletePreset(name, token, registryUrl);
16
+ console.log(` Deleted preset '${name}'`);
17
+ console.log('Done.');
18
+ }
19
+
20
+ module.exports = { presetSet, presetDelete };
@@ -7,6 +7,7 @@ const { readYaml } = require('../util/yaml');
7
7
  const { writePid, readPid, clearPid } = require('./process-tracker');
8
8
  const fetch = require('node-fetch');
9
9
  const { DEFAULT_REGISTRY } = require('./registry-client');
10
+ const { dockerLinkMounts, materializeLinks } = require('./links');
10
11
 
11
12
  const NAMED_CHANNELS = ['nightly', 'stable'];
12
13
 
@@ -65,7 +66,7 @@ function dockerEnsureImage(image, version) {
65
66
  }
66
67
  }
67
68
 
68
- function dockerStart(projectName, image, version, packsDir, serverYamlPath, dataDir, network) {
69
+ function dockerStart(projectName, image, version, packsDir, serverYamlPath, dataDir, network, linkMounts = []) {
69
70
  const containerName = `tapestry-${projectName}`;
70
71
  dockerEnsureImage(image, version);
71
72
  spawnSync('docker', ['rm', '-f', containerName], { stdio: 'ignore' });
@@ -77,6 +78,7 @@ function dockerStart(projectName, image, version, packsDir, serverYamlPath, data
77
78
  '-v', `${packsDir}:/app/packs`,
78
79
  '-v', `${serverYamlPath}:/app/server.yaml`,
79
80
  '-v', `${dataDir}:/app/data`,
81
+ ...linkMounts,
80
82
  ];
81
83
  if (network) {
82
84
  args.push('--network', network);
@@ -322,10 +324,12 @@ async function startEngine(cwd) {
322
324
  fs.mkdirSync(dataDir, { recursive: true });
323
325
  if (config.mode === 'docker') {
324
326
  const tag = await resolveDockerTag(config);
325
- dockerStart(config.projectName, config.image, tag, packsDir, serverYamlPath, dataDir, config.network);
327
+ dockerStart(config.projectName, config.image, tag, packsDir, serverYamlPath, dataDir, config.network, dockerLinkMounts(cwd));
326
328
  } else if (config.mode === 'binary') {
329
+ materializeLinks(cwd);
327
330
  binaryStart(config.version, config.installDir, packsDir, serverYamlPath, cwd);
328
331
  } else {
332
+ materializeLinks(cwd);
329
333
  sourceStart(config.installDir, packsDir, serverYamlPath, cwd);
330
334
  }
331
335
  }
@@ -0,0 +1,147 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const semver = require('semver');
6
+ const { readYaml, writeYaml } = require('../util/yaml');
7
+ const { PACK_MANIFEST } = require('./manifest');
8
+
9
+ const LINKS_FILE = 'tapestry-links.yaml';
10
+
11
+ function readLinks(cwd) {
12
+ const p = path.join(cwd, LINKS_FILE);
13
+ if (!fs.existsSync(p)) {
14
+ return { version: 1, links: {} };
15
+ }
16
+ const data = readYaml(p) || {};
17
+ return { version: data.version || 1, links: data.links || {} };
18
+ }
19
+
20
+ function writeLinks(cwd, data) {
21
+ writeYaml(path.join(cwd, LINKS_FILE), { version: 1, links: data.links || {} });
22
+ }
23
+
24
+ function addLink(cwd, name, absPath) {
25
+ const data = readLinks(cwd);
26
+ data.links[name] = absPath;
27
+ writeLinks(cwd, data);
28
+ }
29
+
30
+ function removeLink(cwd, name) {
31
+ const data = readLinks(cwd);
32
+ if (!(name in data.links)) {
33
+ return false;
34
+ }
35
+ delete data.links[name];
36
+ writeLinks(cwd, data);
37
+ return true;
38
+ }
39
+
40
+ function readPackManifest(packDir) {
41
+ let p = path.join(packDir, PACK_MANIFEST);
42
+ if (!fs.existsSync(p)) {
43
+ p = path.join(packDir, 'tapestry.yaml');
44
+ }
45
+ if (!fs.existsSync(p)) {
46
+ throw new Error(`${packDir} is not a pack (no pack.yaml or tapestry.yaml)`);
47
+ }
48
+ return readYaml(p) || {};
49
+ }
50
+
51
+ function packLinkPath(cwd, name) {
52
+ return path.join(cwd, 'packs', ...name.split('/'));
53
+ }
54
+
55
+ function containerPackTarget(name) {
56
+ return `/app/packs/${name}`;
57
+ }
58
+
59
+ function dockerLinkMounts(cwd) {
60
+ const { links } = readLinks(cwd);
61
+ const args = [];
62
+ for (const [name, absPath] of Object.entries(links)) {
63
+ args.push('-v', `${absPath}:${containerPackTarget(name)}`);
64
+ }
65
+ return args;
66
+ }
67
+
68
+ function lexists(p) {
69
+ try {
70
+ fs.lstatSync(p);
71
+ return true;
72
+ } catch {
73
+ return false;
74
+ }
75
+ }
76
+
77
+ function symlinkType() {
78
+ return process.platform === 'win32' ? 'junction' : 'dir';
79
+ }
80
+
81
+ function materializeLinks(cwd) {
82
+ const { links } = readLinks(cwd);
83
+ for (const [name, absPath] of Object.entries(links)) {
84
+ if (!fs.existsSync(absPath)) {
85
+ throw new Error(`Linked pack '${name}' points to ${absPath}, which no longer exists. Run 'tapestry unlink ${name}' or restore the path.`);
86
+ }
87
+ const linkPath = packLinkPath(cwd, name);
88
+ if (lexists(linkPath)) {
89
+ fs.rmSync(linkPath, { recursive: true, force: true });
90
+ }
91
+ fs.mkdirSync(path.dirname(linkPath), { recursive: true });
92
+ fs.symlinkSync(absPath, linkPath, symlinkType());
93
+ }
94
+ }
95
+
96
+ function removeMaterializedLink(cwd, name) {
97
+ const linkPath = packLinkPath(cwd, name);
98
+ if (lexists(linkPath)) {
99
+ fs.rmSync(linkPath, { recursive: true, force: true });
100
+ }
101
+ }
102
+
103
+ function checkMissingDeps(cwd, manifest) {
104
+ const deps = (manifest && manifest.dependencies) || {};
105
+ const { links } = readLinks(cwd);
106
+ const missing = [];
107
+ for (const depName of Object.keys(deps)) {
108
+ const installed = fs.existsSync(packLinkPath(cwd, depName));
109
+ const linked = depName in links;
110
+ if (!installed && !linked) {
111
+ missing.push(depName);
112
+ }
113
+ }
114
+ return missing;
115
+ }
116
+
117
+ function partitionDeps(cwd, manifest) {
118
+ const deps = (manifest && manifest.dependencies) || {};
119
+ const needsInstall = {};
120
+ if (Object.keys(deps).length === 0) {
121
+ return { needsInstall };
122
+ }
123
+ const { links } = readLinks(cwd);
124
+ for (const [depName, range] of Object.entries(deps)) {
125
+ if (depName in links) {
126
+ continue;
127
+ }
128
+ const installPath = packLinkPath(cwd, depName);
129
+ if (fs.existsSync(installPath)) {
130
+ const manifestPath = path.join(installPath, PACK_MANIFEST);
131
+ if (fs.existsSync(manifestPath)) {
132
+ const installed = readYaml(manifestPath) || {};
133
+ if (installed.version && semver.satisfies(installed.version, range)) {
134
+ continue;
135
+ }
136
+ }
137
+ }
138
+ needsInstall[depName] = range;
139
+ }
140
+ return { needsInstall };
141
+ }
142
+
143
+ module.exports = {
144
+ LINKS_FILE, readLinks, writeLinks, addLink, removeLink,
145
+ readPackManifest, packLinkPath, containerPackTarget, dockerLinkMounts,
146
+ materializeLinks, removeMaterializedLink, checkMissingDeps, partitionDeps,
147
+ };
@@ -116,7 +116,19 @@ async function patchPreset(name, payload, token, registryUrl = DEFAULT_REGISTRY)
116
116
  return res.json();
117
117
  }
118
118
 
119
+ async function deletePreset(name, token, registryUrl = DEFAULT_REGISTRY) {
120
+ const url = `${registryUrl.replace(/\/$/, '')}/v1/admin/presets/${name}`;
121
+ const res = await fetch(url, {
122
+ method: 'DELETE',
123
+ headers: {
124
+ Authorization: `Bearer ${token}`,
125
+ },
126
+ });
127
+ await throwIfError(res, `Failed to delete preset '${name}'`);
128
+ return res.json();
129
+ }
130
+
119
131
  module.exports = {
120
132
  fetchPackageMetadata, fetchTarball, throwIfError, DEFAULT_REGISTRY,
121
- fetchPreset, fetchPresetList, patchDistTag, listDistTags, patchPreset,
133
+ fetchPreset, fetchPresetList, patchDistTag, listDistTags, patchPreset, deletePreset,
122
134
  };