@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 +93 -0
- package/bin/tapestry.js +32 -2
- package/package.json +14 -10
- package/src/commands/create-pack.js +2 -1
- package/src/commands/init.js +1 -1
- package/src/commands/install.js +20 -4
- package/src/commands/link.js +79 -0
- package/src/commands/list.js +36 -25
- package/src/commands/pack.js +2 -1
- package/src/commands/publish.js +2 -1
- package/src/commands/update.js +2 -1
- package/src/commands/validate.js +16 -3
- package/src/lib/engine-manager.js +6 -2
- package/src/lib/links.js +120 -0
- package/src/lib/manifest.js +5 -0
- package/src/scaffold/templates.js +1 -1
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
|
|
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
|
+
"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": "
|
|
16
|
-
"form-data": "
|
|
17
|
-
"inquirer": "
|
|
18
|
-
"js-yaml": "
|
|
19
|
-
"node-fetch": "
|
|
20
|
-
"semver": "
|
|
21
|
-
"tar": "
|
|
22
|
-
"zod": "
|
|
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
|
+
"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(
|
|
63
|
+
console.log(`\nEdit ${PACK_MANIFEST}, then run: tapestry validate`);
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
66
|
|
package/src/commands/init.js
CHANGED
|
@@ -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('');
|
package/src/commands/install.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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(
|
|
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(
|
|
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 };
|
package/src/commands/list.js
CHANGED
|
@@ -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 (
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
|
package/src/commands/pack.js
CHANGED
|
@@ -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,
|
|
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
|
|
package/src/commands/publish.js
CHANGED
|
@@ -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,
|
|
18
|
+
const manifest = readYaml(path.join(cwd, PACK_MANIFEST));
|
|
18
19
|
const token = requireToken();
|
|
19
20
|
|
|
20
21
|
const shortName = manifest.name.split('/')[1];
|
package/src/commands/update.js
CHANGED
|
@@ -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,
|
|
73
|
+
const packManifest = readYaml(path.join(destDir, PACK_MANIFEST));
|
|
73
74
|
addPackageToBoot(cwd, packageName, packManifest);
|
|
74
75
|
}
|
|
75
76
|
|
package/src/commands/validate.js
CHANGED
|
@@ -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,
|
|
10
|
+
const manifestPath = path.join(cwd, PACK_MANIFEST);
|
|
10
11
|
if (!fs.existsSync(manifestPath)) {
|
|
11
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/lib/links.js
ADDED
|
@@ -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
|
+
};
|
|
@@ -404,7 +404,7 @@ see_also: [help, commands]
|
|
|
404
404
|
|
|
405
405
|
function generatePackFiles({ scopedName, shortName }) {
|
|
406
406
|
return [
|
|
407
|
-
{ path: '
|
|
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) },
|