@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,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fetch = require('node-fetch');
|
|
4
|
+
const { saveToken } = require('../lib/auth');
|
|
5
|
+
const { DEFAULT_REGISTRY } = require('../lib/registry-client');
|
|
6
|
+
const { createInterface, ask, askPassword } = require('../util/prompt');
|
|
7
|
+
|
|
8
|
+
async function promptCredentials() {
|
|
9
|
+
const rl = createInterface();
|
|
10
|
+
try {
|
|
11
|
+
const email = await ask(rl, 'Email: ');
|
|
12
|
+
const password = await askPassword(rl, 'Password: ');
|
|
13
|
+
return { email, password };
|
|
14
|
+
} finally {
|
|
15
|
+
rl.close();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function login({ email, password } = {}, { registryUrl = DEFAULT_REGISTRY } = {}) {
|
|
20
|
+
if (!email || !password) {
|
|
21
|
+
({ email, password } = await promptCredentials());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const res = await fetch(`${registryUrl}/v1/auth/login`, {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: { 'Content-Type': 'application/json' },
|
|
27
|
+
body: JSON.stringify({ email, password }),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (!res.ok) {
|
|
31
|
+
const body = await res.json().catch(() => ({}));
|
|
32
|
+
throw new Error(body.error || `Login failed (${res.status})`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { token } = await res.json();
|
|
36
|
+
saveToken(token);
|
|
37
|
+
console.log('Logged in.');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { login };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const semver = require('semver');
|
|
4
|
+
const { readLock } = require('../lib/lock-file');
|
|
5
|
+
const { fetchPackageMetadata, DEFAULT_REGISTRY } = require('../lib/registry-client');
|
|
6
|
+
|
|
7
|
+
async function outdated({ cwd = process.cwd(), registryUrl = DEFAULT_REGISTRY } = {}) {
|
|
8
|
+
const lock = readLock(cwd);
|
|
9
|
+
if (!lock || !lock.resolved || !Object.keys(lock.resolved).length) {
|
|
10
|
+
console.log('No packages installed.');
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const packages = Object.entries(lock.resolved);
|
|
15
|
+
const stale = [];
|
|
16
|
+
|
|
17
|
+
for (const [pkgName, resolved] of packages) {
|
|
18
|
+
let latestVersion;
|
|
19
|
+
try {
|
|
20
|
+
const data = await fetchPackageMetadata(pkgName, registryUrl);
|
|
21
|
+
latestVersion = data.versions?.[0]?.version;
|
|
22
|
+
} catch {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (latestVersion && semver.gt(latestVersion, resolved.version)) {
|
|
26
|
+
stale.push({ name: pkgName, current: resolved.version, latest: latestVersion });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!stale.length) {
|
|
31
|
+
console.log('All packages are up to date.');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const nameWidth = Math.max(7, ...stale.map((s) => s.name.length));
|
|
36
|
+
const curWidth = Math.max(7, ...stale.map((s) => s.current.length));
|
|
37
|
+
console.log(`${'PACKAGE'.padEnd(nameWidth)} ${'CURRENT'.padEnd(curWidth)} LATEST`);
|
|
38
|
+
for (const s of stale) {
|
|
39
|
+
console.log(`${s.name.padEnd(nameWidth)} ${s.current.padEnd(curWidth)} ${s.latest}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { outdated };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { readYaml } = require('../util/yaml');
|
|
5
|
+
const { buildTarball, computeIntegrity } = require('../lib/tarball-builder');
|
|
6
|
+
const { validate } = require('./validate');
|
|
7
|
+
|
|
8
|
+
async function pack({ cwd = process.cwd() } = {}) {
|
|
9
|
+
validate({ cwd });
|
|
10
|
+
|
|
11
|
+
const manifest = readYaml(path.join(cwd, 'tapestry.yaml'));
|
|
12
|
+
const shortName = manifest.name.split('/')[1];
|
|
13
|
+
const outputPath = path.join(cwd, `${shortName}-${manifest.version}.tgz`);
|
|
14
|
+
|
|
15
|
+
console.log(`Packing ${manifest.name}@${manifest.version}...`);
|
|
16
|
+
await buildTarball(cwd, outputPath);
|
|
17
|
+
|
|
18
|
+
const integrity = computeIntegrity(outputPath);
|
|
19
|
+
console.log(` ${path.basename(outputPath)}`);
|
|
20
|
+
console.log(` integrity: ${integrity}`);
|
|
21
|
+
console.log('Done.');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { pack };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const fetch = require('node-fetch');
|
|
7
|
+
const FormData = require('form-data');
|
|
8
|
+
const { readYaml } = require('../util/yaml');
|
|
9
|
+
const { validate } = require('./validate');
|
|
10
|
+
const { buildTarball, computeIntegrity } = require('../lib/tarball-builder');
|
|
11
|
+
const { requireToken } = require('../lib/auth');
|
|
12
|
+
const { DEFAULT_REGISTRY } = require('../lib/registry-client');
|
|
13
|
+
|
|
14
|
+
async function publish({ cwd = process.cwd(), registryUrl = DEFAULT_REGISTRY } = {}) {
|
|
15
|
+
validate({ cwd });
|
|
16
|
+
|
|
17
|
+
const manifest = readYaml(path.join(cwd, 'tapestry.yaml'));
|
|
18
|
+
const token = requireToken();
|
|
19
|
+
|
|
20
|
+
const shortName = manifest.name.split('/')[1];
|
|
21
|
+
const tmpPath = path.join(
|
|
22
|
+
os.tmpdir(),
|
|
23
|
+
`tapestry-publish-${shortName}-${manifest.version}.tgz`
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
console.log(`Publishing ${manifest.name}@${manifest.version}...`);
|
|
28
|
+
await buildTarball(cwd, tmpPath);
|
|
29
|
+
|
|
30
|
+
const integrity = computeIntegrity(tmpPath);
|
|
31
|
+
|
|
32
|
+
const form = new FormData();
|
|
33
|
+
form.append('tarball', fs.createReadStream(tmpPath), {
|
|
34
|
+
filename: `${manifest.version}.tgz`,
|
|
35
|
+
contentType: 'application/gzip',
|
|
36
|
+
});
|
|
37
|
+
form.append('metadata', JSON.stringify({ ...manifest, integrity }));
|
|
38
|
+
|
|
39
|
+
const res = await fetch(`${registryUrl}/v1/publish`, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: {
|
|
42
|
+
...form.getHeaders(),
|
|
43
|
+
Authorization: `Bearer ${token}`,
|
|
44
|
+
},
|
|
45
|
+
body: form,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (!res.ok) {
|
|
49
|
+
const body = await res.json().catch(() => ({}));
|
|
50
|
+
throw new Error(body.error || `Publish failed (${res.status})`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const result = await res.json();
|
|
54
|
+
console.log(` Published ${result.name}@${result.version}`);
|
|
55
|
+
console.log('Done.');
|
|
56
|
+
} finally {
|
|
57
|
+
if (fs.existsSync(tmpPath)) {
|
|
58
|
+
fs.unlinkSync(tmpPath);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { publish };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fetch = require('node-fetch');
|
|
4
|
+
const { saveToken } = require('../lib/auth');
|
|
5
|
+
const { DEFAULT_REGISTRY } = require('../lib/registry-client');
|
|
6
|
+
const { createInterface, ask, askPassword } = require('../util/prompt');
|
|
7
|
+
|
|
8
|
+
async function promptRegistration() {
|
|
9
|
+
const rl = createInterface();
|
|
10
|
+
try {
|
|
11
|
+
const handle = await ask(rl, 'Handle (lowercase, e.g. mallek): ');
|
|
12
|
+
const email = await ask(rl, 'Email: ');
|
|
13
|
+
const password = await askPassword(rl, 'Password: ');
|
|
14
|
+
return { handle, email, password };
|
|
15
|
+
} finally {
|
|
16
|
+
rl.close();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function register({ handle, email, password } = {}, { registryUrl = DEFAULT_REGISTRY } = {}) {
|
|
21
|
+
if (!handle || !email || !password) {
|
|
22
|
+
({ handle, email, password } = await promptRegistration());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const res = await fetch(`${registryUrl}/v1/auth/register`, {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: { 'Content-Type': 'application/json' },
|
|
28
|
+
body: JSON.stringify({ handle, email, password }),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
const body = await res.json().catch(() => ({}));
|
|
33
|
+
throw new Error(body.error || `Registration failed (${res.status})`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { token } = await res.json();
|
|
37
|
+
saveToken(token);
|
|
38
|
+
console.log(`Registered as ${handle}. Logged in.`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { register };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fetch = require('node-fetch');
|
|
4
|
+
const { DEFAULT_REGISTRY } = require('../lib/registry-client');
|
|
5
|
+
|
|
6
|
+
async function search(query, { registryUrl = DEFAULT_REGISTRY } = {}) {
|
|
7
|
+
if (!query || !query.trim()) {
|
|
8
|
+
throw new Error('Usage: tapestry search <query>');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const res = await fetch(
|
|
12
|
+
`${registryUrl}/v1/search?q=${encodeURIComponent(query.trim())}`,
|
|
13
|
+
undefined
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
if (!res.ok) {
|
|
17
|
+
const body = await res.text();
|
|
18
|
+
throw new Error(`Search failed (${res.status}): ${body}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { results } = await res.json();
|
|
22
|
+
|
|
23
|
+
if (!results.length) {
|
|
24
|
+
console.log('No results found.');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const nameWidth = Math.max(4, ...results.map((r) => r.name.length));
|
|
29
|
+
const verWidth = Math.max(7, ...results.map((r) => (r.version || '').length));
|
|
30
|
+
|
|
31
|
+
console.log(
|
|
32
|
+
`${'NAME'.padEnd(nameWidth)} ${'VERSION'.padEnd(verWidth)} DESCRIPTION`
|
|
33
|
+
);
|
|
34
|
+
for (const r of results) {
|
|
35
|
+
console.log(
|
|
36
|
+
`${r.name.padEnd(nameWidth)} ${(r.version || '').padEnd(verWidth)} ${r.description || ''}`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { search };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { readYaml, writeYaml } = require('../util/yaml');
|
|
6
|
+
const { readLock, writeLock } = require('../lib/lock-file');
|
|
7
|
+
const { removePackageFromBoot } = require('../lib/boot');
|
|
8
|
+
|
|
9
|
+
function packInstallPath(cwd, packageName) {
|
|
10
|
+
const parts = packageName.split('/');
|
|
11
|
+
return path.join(cwd, 'packs', ...parts);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function uninstall(packageName, { cwd = process.cwd() } = {}) {
|
|
15
|
+
const manifestPath = path.join(cwd, 'tapestry.yaml');
|
|
16
|
+
if (!fs.existsSync(manifestPath)) {
|
|
17
|
+
throw new Error('No tapestry.yaml found. Run `tapestry init` first.');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const manifest = readYaml(manifestPath);
|
|
21
|
+
if (!manifest.dependencies || !manifest.dependencies[packageName]) {
|
|
22
|
+
throw new Error(`${packageName} is not in tapestry.yaml`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const destDir = packInstallPath(cwd, packageName);
|
|
26
|
+
if (fs.existsSync(destDir)) {
|
|
27
|
+
fs.rmSync(destDir, { recursive: true });
|
|
28
|
+
console.log(` removed ${packageName}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
delete manifest.dependencies[packageName];
|
|
32
|
+
writeYaml(manifestPath, manifest);
|
|
33
|
+
|
|
34
|
+
const lock = readLock(cwd);
|
|
35
|
+
if (lock && lock.resolved) {
|
|
36
|
+
delete lock.resolved[packageName];
|
|
37
|
+
writeLock(cwd, lock);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
removePackageFromBoot(cwd, packageName);
|
|
41
|
+
|
|
42
|
+
console.log(`Uninstalled ${packageName}.`);
|
|
43
|
+
console.log('Note: transitive dependencies are not automatically removed. Run `tapestry install` to refresh.');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { uninstall };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { readYaml } = require('../util/yaml');
|
|
7
|
+
const { resolve } = require('../lib/semver-resolver');
|
|
8
|
+
const { readLock, writeLock } = require('../lib/lock-file');
|
|
9
|
+
const { fetchTarball, DEFAULT_REGISTRY } = require('../lib/registry-client');
|
|
10
|
+
const { verifyIntegrity, saveTarball, extractTarball } = require('../lib/tarball');
|
|
11
|
+
const { addPackageToBoot } = require('../lib/boot');
|
|
12
|
+
|
|
13
|
+
function packInstallPath(cwd, packageName) {
|
|
14
|
+
const parts = packageName.split('/');
|
|
15
|
+
return path.join(cwd, 'packs', ...parts);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function update(packageArg, { cwd = process.cwd(), registryUrl = DEFAULT_REGISTRY } = {}) {
|
|
19
|
+
const manifestPath = path.join(cwd, 'tapestry.yaml');
|
|
20
|
+
if (!fs.existsSync(manifestPath)) {
|
|
21
|
+
throw new Error('No tapestry.yaml found. Run `tapestry init` first.');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const manifest = readYaml(manifestPath);
|
|
25
|
+
const allDeps = manifest.dependencies || {};
|
|
26
|
+
|
|
27
|
+
let depsToResolve;
|
|
28
|
+
if (packageArg) {
|
|
29
|
+
if (!allDeps[packageArg]) {
|
|
30
|
+
throw new Error(`${packageArg} is not in tapestry.yaml`);
|
|
31
|
+
}
|
|
32
|
+
depsToResolve = { [packageArg]: allDeps[packageArg] };
|
|
33
|
+
} else {
|
|
34
|
+
depsToResolve = allDeps;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log('Resolving latest compatible versions...');
|
|
38
|
+
const freshResolved = await resolve(depsToResolve, registryUrl);
|
|
39
|
+
|
|
40
|
+
const existingLock = readLock(cwd);
|
|
41
|
+
const existingResolved = (existingLock && existingLock.resolved) || {};
|
|
42
|
+
const mergedResolved = Object.assign({}, existingResolved, freshResolved);
|
|
43
|
+
|
|
44
|
+
for (const [packageName, info] of Object.entries(freshResolved)) {
|
|
45
|
+
const currentVersion = existingResolved[packageName] && existingResolved[packageName].version;
|
|
46
|
+
if (currentVersion === info.version) {
|
|
47
|
+
console.log(` up to date ${packageName}@${info.version}`);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log(` updating ${packageName} ${currentVersion || 'not installed'} -> ${info.version}`);
|
|
52
|
+
|
|
53
|
+
const destDir = packInstallPath(cwd, packageName);
|
|
54
|
+
if (fs.existsSync(destDir)) {
|
|
55
|
+
fs.rmSync(destDir, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const safeId = packageName.replace('@', '').replace('/', '-');
|
|
59
|
+
const tmpPath = path.join(os.tmpdir(), `tapestry-${safeId}-${info.version}.tgz`);
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const buffer = await fetchTarball(info.tarball);
|
|
63
|
+
verifyIntegrity(buffer, info.integrity);
|
|
64
|
+
saveTarball(buffer, tmpPath);
|
|
65
|
+
await extractTarball(tmpPath, destDir);
|
|
66
|
+
} finally {
|
|
67
|
+
if (fs.existsSync(tmpPath)) {
|
|
68
|
+
fs.unlinkSync(tmpPath);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const packManifest = readYaml(path.join(destDir, 'tapestry.yaml'));
|
|
73
|
+
addPackageToBoot(cwd, packageName, packManifest);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
writeLock(cwd, { lockfile_version: 1, resolved: mergedResolved });
|
|
77
|
+
console.log('Done.');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = { update };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { readYaml } = require('../util/yaml');
|
|
6
|
+
const { validatePackageManifest } = require('../schema/manifest');
|
|
7
|
+
|
|
8
|
+
function validate({ cwd = process.cwd() } = {}) {
|
|
9
|
+
const manifestPath = path.join(cwd, 'tapestry.yaml');
|
|
10
|
+
if (!fs.existsSync(manifestPath)) {
|
|
11
|
+
throw new Error('No tapestry.yaml found in current directory');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const data = readYaml(manifestPath);
|
|
15
|
+
console.log(`Validating ${manifestPath}...`);
|
|
16
|
+
|
|
17
|
+
const result = validatePackageManifest(data);
|
|
18
|
+
if (!result.success) {
|
|
19
|
+
for (const issue of result.error.issues) {
|
|
20
|
+
const fieldPath = issue.path.join('.') || 'root';
|
|
21
|
+
console.log(` error: ${fieldPath} - ${issue.message}`);
|
|
22
|
+
}
|
|
23
|
+
throw new Error(`${result.error.issues.length} validation error(s)`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
console.log(` OK ${data.name} v${data.version}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = { validate };
|
package/src/lib/auth.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const yaml = require('js-yaml');
|
|
7
|
+
|
|
8
|
+
const RC_PATH = path.join(os.homedir(), '.tapestryrc');
|
|
9
|
+
|
|
10
|
+
function loadToken() {
|
|
11
|
+
if (!fs.existsSync(RC_PATH)) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const data = yaml.load(fs.readFileSync(RC_PATH, 'utf8'));
|
|
16
|
+
return data?.token ?? null;
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function saveToken(token) {
|
|
23
|
+
fs.writeFileSync(RC_PATH, yaml.dump({ token }, { lineWidth: -1 }), { mode: 0o600 });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function requireToken() {
|
|
27
|
+
const token = loadToken();
|
|
28
|
+
if (!token) {
|
|
29
|
+
throw new Error('Not logged in. Run: tapestry login');
|
|
30
|
+
}
|
|
31
|
+
return token;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { RC_PATH, loadToken, saveToken, requireToken };
|
package/src/lib/boot.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { readYaml, writeYaml } = require('../util/yaml');
|
|
6
|
+
|
|
7
|
+
const BOOT_FILE = 'tapestry-boot.yaml';
|
|
8
|
+
|
|
9
|
+
function readBoot(cwd) {
|
|
10
|
+
const bootPath = path.join(cwd, BOOT_FILE);
|
|
11
|
+
if (!fs.existsSync(bootPath)) {
|
|
12
|
+
return { modules: [], packs: {} };
|
|
13
|
+
}
|
|
14
|
+
const data = readYaml(bootPath);
|
|
15
|
+
return { modules: data.modules || [], packs: data.packs || {} };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function writeBoot(cwd, boot) {
|
|
19
|
+
writeYaml(path.join(cwd, BOOT_FILE), boot);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function addPackageToBoot(cwd, packageName, manifest) {
|
|
23
|
+
const boot = readBoot(cwd);
|
|
24
|
+
|
|
25
|
+
if (!boot.packs[packageName]) {
|
|
26
|
+
boot.packs[packageName] = { enabled: true };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (manifest.module && manifest.module.class) {
|
|
30
|
+
const alreadyPresent = boot.modules.some((m) => m.package === packageName);
|
|
31
|
+
if (!alreadyPresent) {
|
|
32
|
+
boot.modules.push({
|
|
33
|
+
class: manifest.module.class,
|
|
34
|
+
package: packageName,
|
|
35
|
+
enabled: true,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
boot.modules = topoSort(boot.modules);
|
|
41
|
+
writeBoot(cwd, boot);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function removePackageFromBoot(cwd, packageName) {
|
|
45
|
+
const boot = readBoot(cwd);
|
|
46
|
+
delete boot.packs[packageName];
|
|
47
|
+
boot.modules = boot.modules.filter((m) => m.package !== packageName);
|
|
48
|
+
writeBoot(cwd, boot);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function enablePackage(cwd, packageName) {
|
|
52
|
+
const boot = readBoot(cwd);
|
|
53
|
+
if (!boot.packs[packageName]) {
|
|
54
|
+
throw new Error(`${packageName} is not installed`);
|
|
55
|
+
}
|
|
56
|
+
boot.packs[packageName].enabled = true;
|
|
57
|
+
for (const mod of boot.modules) {
|
|
58
|
+
if (mod.package === packageName) {
|
|
59
|
+
mod.enabled = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
writeBoot(cwd, boot);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function disablePackage(cwd, packageName) {
|
|
66
|
+
const boot = readBoot(cwd);
|
|
67
|
+
if (!boot.packs[packageName]) {
|
|
68
|
+
throw new Error(`${packageName} is not installed`);
|
|
69
|
+
}
|
|
70
|
+
boot.packs[packageName].enabled = false;
|
|
71
|
+
for (const mod of boot.modules) {
|
|
72
|
+
if (mod.package === packageName) {
|
|
73
|
+
mod.enabled = false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
writeBoot(cwd, boot);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function topoSort(modules) {
|
|
80
|
+
const byClass = {};
|
|
81
|
+
for (const mod of modules) {
|
|
82
|
+
byClass[mod.class] = mod;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const visited = new Set();
|
|
86
|
+
const inStack = new Set();
|
|
87
|
+
const result = [];
|
|
88
|
+
|
|
89
|
+
function visit(mod) {
|
|
90
|
+
if (inStack.has(mod.class)) {
|
|
91
|
+
throw new Error(`Circular dependency in .NET module boot order involving: ${mod.class}`);
|
|
92
|
+
}
|
|
93
|
+
if (visited.has(mod.class)) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
inStack.add(mod.class);
|
|
97
|
+
if (mod.after && byClass[mod.after]) {
|
|
98
|
+
visit(byClass[mod.after]);
|
|
99
|
+
}
|
|
100
|
+
inStack.delete(mod.class);
|
|
101
|
+
visited.add(mod.class);
|
|
102
|
+
result.push(mod);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const mod of modules) {
|
|
106
|
+
visit(mod);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = { readBoot, writeBoot, addPackageToBoot, removePackageFromBoot, enablePackage, disablePackage, topoSort };
|