@tapestry-mud/cli 0.3.11 → 0.5.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');
@@ -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',
@@ -255,7 +256,7 @@ program
255
256
 
256
257
  program
257
258
  .command('validate')
258
- .description('Validate tapestry.yaml in the current directory')
259
+ .description('Validate pack.yaml in the current directory')
259
260
  .action(() => {
260
261
  try {
261
262
  validate();
@@ -337,6 +338,35 @@ program
337
338
  }
338
339
  });
339
340
 
341
+ program
342
+ .command('link [path]')
343
+ .description('Attach a local pack working copy to this project (use --list to show links)')
344
+ .option('--list', 'List active links instead of creating one')
345
+ .action(async (linkPath, options) => {
346
+ try {
347
+ if (options.list || !linkPath) {
348
+ await linkList();
349
+ } else {
350
+ await link(linkPath);
351
+ }
352
+ } catch (e) {
353
+ console.error(`error: ${e.message}`);
354
+ process.exit(1);
355
+ }
356
+ });
357
+
358
+ program
359
+ .command('unlink <name>')
360
+ .description('Detach a linked pack and restore the registry copy on next install')
361
+ .action(async (name) => {
362
+ try {
363
+ await unlink(name);
364
+ } catch (e) {
365
+ console.error(`error: ${e.message}`);
366
+ process.exit(1);
367
+ }
368
+ });
369
+
340
370
  const engineCmd = program.command('engine').description('Manage the Tapestry engine');
341
371
 
342
372
  engineCmd
package/package.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "@tapestry-mud/cli",
3
- "version": "0.3.11",
3
+ "version": "0.5.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
  }
@@ -3,6 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { generatePackFiles } = require('../scaffold/templates');
6
+ const { PACK_MANIFEST } = require('../lib/manifest');
6
7
 
7
8
  function parseName(name) {
8
9
  const scopedMatch = name.match(/^@([a-z0-9-]+)\/([a-z0-9-]+)$/);
@@ -59,7 +60,7 @@ function createPack(name, cwd) {
59
60
  console.log(` ${file.path}`);
60
61
  }
61
62
  }
62
- console.log('\nEdit tapestry.yaml, then run: tapestry validate');
63
+ console.log(`\nEdit ${PACK_MANIFEST}, then run: tapestry validate`);
63
64
  }
64
65
  }
65
66
 
@@ -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('');
@@ -10,6 +10,8 @@ const { fetchTarball, DEFAULT_REGISTRY } = require('../lib/registry-client');
10
10
  const { verifyIntegrity, saveTarball, extractTarball } = require('../lib/tarball');
11
11
  const { addPackageToBoot } = require('../lib/boot');
12
12
  const { loadToken } = require('../lib/auth');
13
+ const { PACK_MANIFEST } = require('../lib/manifest');
14
+ const { readLinks } = require('../lib/links');
13
15
 
14
16
  function packInstallPath(cwd, packageName) {
15
17
  const parts = packageName.split('/');
@@ -33,11 +35,16 @@ function isLockCurrent(manifestDeps, lock) {
33
35
  }
34
36
 
35
37
  async function installResolved(cwd, resolved, token) {
38
+ const { links } = readLinks(cwd);
36
39
  for (const [packageName, info] of Object.entries(resolved)) {
40
+ if (packageName in links) {
41
+ console.log(` skipping ${packageName} (linked)`);
42
+ continue;
43
+ }
37
44
  const destDir = packInstallPath(cwd, packageName);
38
45
 
39
46
  if (fs.existsSync(destDir)) {
40
- const installedManifestPath = path.join(destDir, 'tapestry.yaml');
47
+ const installedManifestPath = path.join(destDir, PACK_MANIFEST);
41
48
  if (fs.existsSync(installedManifestPath)) {
42
49
  const installed = readYaml(installedManifestPath);
43
50
  if (installed.version === info.version) {
@@ -67,7 +74,7 @@ async function installResolved(cwd, resolved, token) {
67
74
  }
68
75
  }
69
76
 
70
- const packManifest = readYaml(path.join(destDir, 'tapestry.yaml'));
77
+ const packManifest = readYaml(path.join(destDir, PACK_MANIFEST));
71
78
  addPackageToBoot(cwd, packageName, packManifest);
72
79
  }
73
80
  }
@@ -97,13 +104,22 @@ async function install(packageArg, { cwd = process.cwd(), registryUrl = DEFAULT_
97
104
 
98
105
  writeYaml(manifestPath, manifest);
99
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
+ );
100
116
  const lock = readLock(cwd);
101
- if (lock && isLockCurrent(manifest.dependencies || {}, lock)) {
117
+ if (lock && isLockCurrent(depsToResolve, lock)) {
102
118
  console.log('Installing from lock file...');
103
119
  resolved = lock.resolved;
104
120
  } else {
105
121
  console.log('Resolving dependencies...');
106
- resolved = await resolve(manifest.dependencies || {}, registryUrl, token);
122
+ resolved = await resolve(depsToResolve, registryUrl, token);
107
123
  }
108
124
  }
109
125
 
@@ -0,0 +1,79 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const {
6
+ readLinks, addLink, removeLink, readPackManifest,
7
+ removeMaterializedLink, checkMissingDeps,
8
+ } = require('../lib/links');
9
+ const { addPackageToBoot, removePackageFromBoot } = require('../lib/boot');
10
+
11
+ function requireProject(cwd) {
12
+ if (!fs.existsSync(path.join(cwd, 'tapestry.yaml'))) {
13
+ throw new Error('No tapestry.yaml found. Run `tapestry init` first.');
14
+ }
15
+ }
16
+
17
+ function ensureGitignore(cwd) {
18
+ const gi = path.join(cwd, '.gitignore');
19
+ if (!fs.existsSync(gi)) {
20
+ return;
21
+ }
22
+ const body = fs.readFileSync(gi, 'utf8');
23
+ if (!body.split(/\r?\n/).includes('tapestry-links.yaml')) {
24
+ fs.appendFileSync(gi, `${body.endsWith('\n') ? '' : '\n'}tapestry-links.yaml\n`);
25
+ }
26
+ }
27
+
28
+ async function link(targetPath, { cwd = process.cwd() } = {}) {
29
+ requireProject(cwd);
30
+ const absPath = path.resolve(cwd, targetPath);
31
+ if (!fs.existsSync(absPath)) {
32
+ throw new Error(`Path not found: ${absPath}`);
33
+ }
34
+ const manifest = readPackManifest(absPath);
35
+ const name = manifest.name;
36
+ if (!name) {
37
+ throw new Error(`Pack at ${absPath} has no 'name' in its manifest`);
38
+ }
39
+
40
+ addLink(cwd, name, absPath);
41
+ addPackageToBoot(cwd, name, manifest);
42
+ ensureGitignore(cwd);
43
+
44
+ console.log(`linked ${name} -> ${absPath}`);
45
+
46
+ if (manifest.active === false) {
47
+ console.warn(` warning: ${name} is marked active: false; it will not load until activated`);
48
+ }
49
+ for (const dep of checkMissingDeps(cwd, manifest)) {
50
+ const range = manifest.dependencies[dep];
51
+ console.warn(` warning: missing dependency ${dep} (${range}) -- run: tapestry install ${dep}`);
52
+ }
53
+ }
54
+
55
+ async function unlink(name, { cwd = process.cwd() } = {}) {
56
+ requireProject(cwd);
57
+ if (!removeLink(cwd, name)) {
58
+ throw new Error(`${name} is not linked`);
59
+ }
60
+ removeMaterializedLink(cwd, name);
61
+ removePackageFromBoot(cwd, name);
62
+ console.log(`unlinked ${name}`);
63
+ console.log(` run 'tapestry install' to restore the registry copy`);
64
+ }
65
+
66
+ async function linkList({ cwd = process.cwd() } = {}) {
67
+ const { links } = readLinks(cwd);
68
+ const entries = Object.entries(links);
69
+ if (entries.length === 0) {
70
+ console.log('No linked packs.');
71
+ return;
72
+ }
73
+ for (const [name, absPath] of entries) {
74
+ const flag = fs.existsSync(absPath) ? '' : ' (MISSING)';
75
+ console.log(`${name} -> ${absPath}${flag}`);
76
+ }
77
+ }
78
+
79
+ module.exports = { link, unlink, linkList };
@@ -5,6 +5,8 @@ const path = require('path');
5
5
  const { readLock } = require('../lib/lock-file');
6
6
  const { readBoot } = require('../lib/boot');
7
7
  const { readYaml } = require('../util/yaml');
8
+ const { PACK_MANIFEST } = require('../lib/manifest');
9
+ const { readLinks } = require('../lib/links');
8
10
 
9
11
  function packInstallPath(cwd, packageName) {
10
12
  const parts = packageName.split('/');
@@ -13,37 +15,46 @@ function packInstallPath(cwd, packageName) {
13
15
 
14
16
  async function list({ cwd = process.cwd() } = {}) {
15
17
  const lock = readLock(cwd);
16
- if (!lock || !lock.resolved || !Object.keys(lock.resolved).length) {
17
- console.log('No packages installed.');
18
- return;
19
- }
20
-
21
- const boot = readBoot(cwd);
22
- 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);
23
21
 
24
- const nameWidth = Math.max(7, ...packages.map(([n]) => n.length));
25
- 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));
26
24
 
27
- console.log(
28
- `${'PACKAGE'.padEnd(nameWidth)} ${'VERSION'.padEnd(verWidth)} TYPE STATUS`
29
- );
30
-
31
- for (const [pkgName, resolved] of packages) {
32
- const enabled = boot.packs[pkgName]?.enabled !== false ? 'enabled' : 'disabled';
25
+ console.log(
26
+ `${'PACKAGE'.padEnd(nameWidth)} ${'VERSION'.padEnd(verWidth)} TYPE STATUS`
27
+ );
33
28
 
34
- let type = '';
35
- const packManifestPath = path.join(packInstallPath(cwd, pkgName), 'tapestry.yaml');
36
- if (fs.existsSync(packManifestPath)) {
37
- try {
38
- type = readYaml(packManifestPath).type || '';
39
- } catch {
40
- //
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
+ }
41
40
  }
41
+
42
+ console.log(
43
+ `${pkgName.padEnd(nameWidth)} ${resolved.version.padEnd(verWidth)} ${type.padEnd(8)} ${enabled}`
44
+ );
42
45
  }
46
+ } else {
47
+ console.log('No packages installed.');
48
+ }
43
49
 
44
- console.log(
45
- `${pkgName.padEnd(nameWidth)} ${resolved.version.padEnd(verWidth)} ${type.padEnd(8)} ${enabled}`
46
- );
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
+ }
47
58
  }
48
59
  }
49
60
 
@@ -3,12 +3,13 @@
3
3
  const path = require('path');
4
4
  const { readYaml } = require('../util/yaml');
5
5
  const { buildTarball, computeIntegrity } = require('../lib/tarball-builder');
6
+ const { PACK_MANIFEST } = require('../lib/manifest');
6
7
  const { validate } = require('./validate');
7
8
 
8
9
  async function pack({ cwd = process.cwd() } = {}) {
9
10
  validate({ cwd });
10
11
 
11
- const manifest = readYaml(path.join(cwd, 'tapestry.yaml'));
12
+ const manifest = readYaml(path.join(cwd, PACK_MANIFEST));
12
13
  const shortName = manifest.name.split('/')[1];
13
14
  const outputPath = path.join(cwd, `${shortName}-${manifest.version}.tgz`);
14
15
 
@@ -8,13 +8,14 @@ const FormData = require('form-data');
8
8
  const { readYaml } = require('../util/yaml');
9
9
  const { validate } = require('./validate');
10
10
  const { buildTarball, computeIntegrity } = require('../lib/tarball-builder');
11
+ const { PACK_MANIFEST } = require('../lib/manifest');
11
12
  const { requireToken } = require('../lib/auth');
12
13
  const { DEFAULT_REGISTRY, throwIfError } = require('../lib/registry-client');
13
14
 
14
15
  async function publish({ cwd = process.cwd(), registryUrl = DEFAULT_REGISTRY } = {}) {
15
16
  validate({ cwd });
16
17
 
17
- const manifest = readYaml(path.join(cwd, 'tapestry.yaml'));
18
+ const manifest = readYaml(path.join(cwd, PACK_MANIFEST));
18
19
  const token = requireToken();
19
20
 
20
21
  const shortName = manifest.name.split('/')[1];
@@ -9,6 +9,7 @@ const { readLock, writeLock } = require('../lib/lock-file');
9
9
  const { fetchTarball, DEFAULT_REGISTRY } = require('../lib/registry-client');
10
10
  const { verifyIntegrity, saveTarball, extractTarball } = require('../lib/tarball');
11
11
  const { addPackageToBoot } = require('../lib/boot');
12
+ const { PACK_MANIFEST } = require('../lib/manifest');
12
13
 
13
14
  function packInstallPath(cwd, packageName) {
14
15
  const parts = packageName.split('/');
@@ -69,7 +70,7 @@ async function update(packageArg, { cwd = process.cwd(), registryUrl = DEFAULT_R
69
70
  }
70
71
  }
71
72
 
72
- const packManifest = readYaml(path.join(destDir, 'tapestry.yaml'));
73
+ const packManifest = readYaml(path.join(destDir, PACK_MANIFEST));
73
74
  addPackageToBoot(cwd, packageName, packManifest);
74
75
  }
75
76
 
@@ -4,11 +4,20 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { readYaml } = require('../util/yaml');
6
6
  const { validatePackageManifest } = require('../schema/manifest');
7
+ const { PACK_MANIFEST } = require('../lib/manifest');
7
8
 
8
9
  function validate({ cwd = process.cwd() } = {}) {
9
- const manifestPath = path.join(cwd, 'tapestry.yaml');
10
+ const manifestPath = path.join(cwd, PACK_MANIFEST);
10
11
  if (!fs.existsSync(manifestPath)) {
11
- throw new Error('No tapestry.yaml found in current directory');
12
+ const serverPath = path.join(cwd, 'tapestry.yaml');
13
+ if (fs.existsSync(serverPath)) {
14
+ throw new Error(
15
+ `No ${PACK_MANIFEST} found in current directory. ` +
16
+ `The tapestry.yaml here is a server manifest. ` +
17
+ `Pack validation requires ${PACK_MANIFEST}.`
18
+ );
19
+ }
20
+ throw new Error(`No ${PACK_MANIFEST} found in current directory`);
12
21
  }
13
22
 
14
23
  const data = readYaml(manifestPath);
@@ -18,7 +27,11 @@ function validate({ cwd = process.cwd() } = {}) {
18
27
  if (!result.success) {
19
28
  for (const issue of result.error.issues) {
20
29
  const fieldPath = issue.path.join('.') || 'root';
21
- console.log(` error: ${fieldPath} - ${issue.message}`);
30
+ let message = issue.message;
31
+ if (fieldPath === 'engine' && data.engine && typeof data.engine === 'object') {
32
+ message += `. engine must be a version constraint string in pack manifests (e.g. '>=0.0.1'). Object format is for server manifests (tapestry.yaml).`;
33
+ }
34
+ console.log(` error: ${fieldPath} - ${message}`);
22
35
  }
23
36
  throw new Error(`${result.error.issues.length} validation error(s)`);
24
37
  }
@@ -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,120 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const { readYaml, writeYaml } = require('../util/yaml');
6
+ const { PACK_MANIFEST } = require('./manifest');
7
+
8
+ const LINKS_FILE = 'tapestry-links.yaml';
9
+
10
+ function readLinks(cwd) {
11
+ const p = path.join(cwd, LINKS_FILE);
12
+ if (!fs.existsSync(p)) {
13
+ return { version: 1, links: {} };
14
+ }
15
+ const data = readYaml(p) || {};
16
+ return { version: data.version || 1, links: data.links || {} };
17
+ }
18
+
19
+ function writeLinks(cwd, data) {
20
+ writeYaml(path.join(cwd, LINKS_FILE), { version: 1, links: data.links || {} });
21
+ }
22
+
23
+ function addLink(cwd, name, absPath) {
24
+ const data = readLinks(cwd);
25
+ data.links[name] = absPath;
26
+ writeLinks(cwd, data);
27
+ }
28
+
29
+ function removeLink(cwd, name) {
30
+ const data = readLinks(cwd);
31
+ if (!(name in data.links)) {
32
+ return false;
33
+ }
34
+ delete data.links[name];
35
+ writeLinks(cwd, data);
36
+ return true;
37
+ }
38
+
39
+ function readPackManifest(packDir) {
40
+ let p = path.join(packDir, PACK_MANIFEST);
41
+ if (!fs.existsSync(p)) {
42
+ p = path.join(packDir, 'tapestry.yaml');
43
+ }
44
+ if (!fs.existsSync(p)) {
45
+ throw new Error(`${packDir} is not a pack (no pack.yaml or tapestry.yaml)`);
46
+ }
47
+ return readYaml(p) || {};
48
+ }
49
+
50
+ function packLinkPath(cwd, name) {
51
+ return path.join(cwd, 'packs', ...name.split('/'));
52
+ }
53
+
54
+ function containerPackTarget(name) {
55
+ return `/app/packs/${name}`;
56
+ }
57
+
58
+ function dockerLinkMounts(cwd) {
59
+ const { links } = readLinks(cwd);
60
+ const args = [];
61
+ for (const [name, absPath] of Object.entries(links)) {
62
+ args.push('-v', `${absPath}:${containerPackTarget(name)}`);
63
+ }
64
+ return args;
65
+ }
66
+
67
+ function lexists(p) {
68
+ try {
69
+ fs.lstatSync(p);
70
+ return true;
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+
76
+ function symlinkType() {
77
+ return process.platform === 'win32' ? 'junction' : 'dir';
78
+ }
79
+
80
+ function materializeLinks(cwd) {
81
+ const { links } = readLinks(cwd);
82
+ for (const [name, absPath] of Object.entries(links)) {
83
+ if (!fs.existsSync(absPath)) {
84
+ throw new Error(`Linked pack '${name}' points to ${absPath}, which no longer exists. Run 'tapestry unlink ${name}' or restore the path.`);
85
+ }
86
+ const linkPath = packLinkPath(cwd, name);
87
+ if (lexists(linkPath)) {
88
+ fs.rmSync(linkPath, { recursive: true, force: true });
89
+ }
90
+ fs.mkdirSync(path.dirname(linkPath), { recursive: true });
91
+ fs.symlinkSync(absPath, linkPath, symlinkType());
92
+ }
93
+ }
94
+
95
+ function removeMaterializedLink(cwd, name) {
96
+ const linkPath = packLinkPath(cwd, name);
97
+ if (lexists(linkPath)) {
98
+ fs.rmSync(linkPath, { recursive: true, force: true });
99
+ }
100
+ }
101
+
102
+ function checkMissingDeps(cwd, manifest) {
103
+ const deps = (manifest && manifest.dependencies) || {};
104
+ const { links } = readLinks(cwd);
105
+ const missing = [];
106
+ for (const depName of Object.keys(deps)) {
107
+ const installed = fs.existsSync(packLinkPath(cwd, depName));
108
+ const linked = depName in links;
109
+ if (!installed && !linked) {
110
+ missing.push(depName);
111
+ }
112
+ }
113
+ return missing;
114
+ }
115
+
116
+ module.exports = {
117
+ LINKS_FILE, readLinks, writeLinks, addLink, removeLink,
118
+ readPackManifest, packLinkPath, containerPackTarget, dockerLinkMounts,
119
+ materializeLinks, removeMaterializedLink, checkMissingDeps,
120
+ };
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+
3
+ const PACK_MANIFEST = 'pack.yaml';
4
+
5
+ module.exports = { PACK_MANIFEST };
@@ -404,7 +404,7 @@ see_also: [help, commands]
404
404
 
405
405
  function generatePackFiles({ scopedName, shortName }) {
406
406
  return [
407
- { path: 'tapestry.yaml', content: manifestTemplate(scopedName) },
407
+ { path: 'pack.yaml', content: manifestTemplate(scopedName) },
408
408
  { path: 'tags.yml', content: tagsTemplate() },
409
409
  { path: 'areas/example-area/area.yaml', content: areaTemplate() },
410
410
  { path: 'areas/example-area/rooms/town-square.yaml', content: roomTemplate(shortName) },