@localctl/cli 0.2.0 → 0.4.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/LICENSE +21 -21
- package/README.md +398 -431
- package/assets/manifests/namespace-addons.yaml +4 -4
- package/assets/manifests/tls-store.yaml +8 -8
- package/bin/localctl.js +2 -2
- package/package.json +61 -54
- package/src/commands/addons.js +56 -56
- package/src/commands/doctor.js +44 -43
- package/src/commands/down.js +35 -35
- package/src/commands/exec.js +15 -15
- package/src/commands/generateManifests.js +12 -12
- package/src/commands/hosts.js +23 -23
- package/src/commands/logs.js +45 -45
- package/src/commands/profiles.js +180 -180
- package/src/commands/prune.js +49 -49
- package/src/commands/registryInfo.js +18 -18
- package/src/commands/reload.js +44 -44
- package/src/commands/secrets.js +75 -75
- package/src/commands/setup.js +36 -36
- package/src/commands/status.js +136 -136
- package/src/commands/up.js +123 -123
- package/src/index.js +38 -36
- package/src/lib/addonDirs.js +43 -43
- package/src/lib/addonLoader.js +190 -190
- package/src/lib/addons/cluster/logging/addon.yaml +46 -46
- package/src/lib/addons/cluster/logging/config/all-logs.json +21 -21
- package/src/lib/addons/cluster/monitoring/addon.yaml +33 -33
- package/src/lib/addons/mongo/addon.yaml +18 -18
- package/src/lib/addons/postgres/addon.yaml +39 -39
- package/src/lib/addons/redis/addon.yaml +29 -29
- package/src/lib/appSecrets.js +60 -60
- package/src/lib/clusterAddons.js +59 -59
- package/src/lib/clusterProvision.js +171 -170
- package/src/lib/configSchema.js +103 -103
- package/src/lib/constants.js +16 -16
- package/src/lib/engine.js +36 -20
- package/src/lib/exec.js +77 -77
- package/src/lib/hostsFile.js +79 -79
- package/src/lib/loadConfig.js +51 -51
- package/src/lib/logger.js +9 -9
- package/src/lib/manifestGen.js +210 -210
- package/src/lib/paths.js +41 -41
- package/src/lib/podman.js +72 -72
- package/src/lib/profileStore.js +134 -134
- package/src/lib/projectContext.js +32 -32
- package/src/lib/prompt.js +97 -97
- package/src/lib/toolInstall.js +30 -30
- package/src/lib/ui.js +50 -50
- package/src/lib/updateCheck.js +84 -0
- package/src/lib/updateCheckWorker.js +46 -0
- package/src/lib/workloadReadiness.js +35 -35
- package/templates/Tiltfile.tmpl +35 -35
package/src/commands/up.js
CHANGED
|
@@ -1,123 +1,123 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const { loadConfig } = require('../lib/loadConfig');
|
|
3
|
-
const { syncHosts, registerApp } = require('../lib/hostsFile');
|
|
4
|
-
const { run, capture, sleepSync, spawnDetached, getFreePort, isRunning } = require('../lib/exec');
|
|
5
|
-
const { detectEngine } = require('../lib/engine');
|
|
6
|
-
const { ensureClusterUp } = require('../lib/clusterProvision');
|
|
7
|
-
const { portsFor } = require('../lib/profileStore');
|
|
8
|
-
const { listKeys } = require('../lib/appSecrets');
|
|
9
|
-
const { runFiles } = require('../lib/paths');
|
|
10
|
-
const { DOMAIN, DEFAULT_HTTPS_PORT } = require('../lib/constants');
|
|
11
|
-
const ui = require('../lib/ui');
|
|
12
|
-
const logger = require('../lib/logger');
|
|
13
|
-
|
|
14
|
-
function podmanBuildEnv() {
|
|
15
|
-
const env = { ...process.env };
|
|
16
|
-
const detected = detectEngine();
|
|
17
|
-
if (detected && detected.engine === 'podman') {
|
|
18
|
-
// Podman's Docker-API-compat socket doesn't implement BuildKit's gRPC build protocol
|
|
19
|
-
// (Tilt's docker_build fails with "unable to upgrade to h2c" otherwise).
|
|
20
|
-
env.DOCKER_BUILDKIT = '0';
|
|
21
|
-
logger.info('Podman detected: disabling BuildKit for this build.');
|
|
22
|
-
}
|
|
23
|
-
return env;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function printEndpoints(cfg) {
|
|
27
|
-
const { httpsPort } = portsFor(cfg.profile);
|
|
28
|
-
const suffix = httpsPort === DEFAULT_HTTPS_PORT ? '' : `:${httpsPort}`;
|
|
29
|
-
const url = `https://${cfg.subdomain}.${DOMAIN}${suffix}`;
|
|
30
|
-
console.log('');
|
|
31
|
-
console.log(`${ui.check} ${ui.bold(cfg.name)} -> ${ui.url(url)}`);
|
|
32
|
-
if (cfg.debug.enabled && cfg.debug.port) {
|
|
33
|
-
console.log(` debug: localhost:${cfg.debug.port}`);
|
|
34
|
-
}
|
|
35
|
-
console.log(` logs: localctl app logs ${cfg.name} -f`);
|
|
36
|
-
console.log(` status: localctl app status`);
|
|
37
|
-
console.log(` stop: localctl app down`);
|
|
38
|
-
console.log('');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function tailLog(logFile, lines) {
|
|
42
|
-
if (!fs.existsSync(logFile)) return null;
|
|
43
|
-
return fs.readFileSync(logFile, 'utf8').trim().split('\n').slice(-lines).join('\n');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
module.exports = (program) => {
|
|
47
|
-
program
|
|
48
|
-
.command('up')
|
|
49
|
-
.option('-f, --foreground', 'run in the foreground with the interactive Tilt UI (default: detached, survives closing this terminal)')
|
|
50
|
-
.description('Deploy/dev-loop the app in the current directory')
|
|
51
|
-
.action(async (opts) => {
|
|
52
|
-
const cfg = loadConfig();
|
|
53
|
-
ensureClusterUp(cfg.clusterName, cfg.profile);
|
|
54
|
-
|
|
55
|
-
if (cfg.secrets.length > 0) {
|
|
56
|
-
const set = new Set(listKeys(cfg));
|
|
57
|
-
const missing = cfg.secrets.filter((key) => !set.has(key));
|
|
58
|
-
if (missing.length > 0) {
|
|
59
|
-
throw new Error(
|
|
60
|
-
`Missing secret value${missing.length > 1 ? 's' : ''} for: ${missing.join(', ')}. Set ` +
|
|
61
|
-
`${missing.length > 1 ? 'them' : 'it'} first:\n` +
|
|
62
|
-
missing.map((key) => ` localctl app secrets set ${key}`).join('\n'),
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
registerApp(cfg.subdomain);
|
|
68
|
-
syncHosts();
|
|
69
|
-
const env = podmanBuildEnv();
|
|
70
|
-
|
|
71
|
-
// Each app gets its own Tilt web UI port. Tilt's default (10350) is shared/fixed, so two
|
|
72
|
-
// apps running `localctl up` at once - the whole point of detached mode - would otherwise
|
|
73
|
-
// collide outright, and Tilt's resulting error doesn't explain why to someone who never
|
|
74
|
-
// asked to think about Tilt's own dashboard port in the first place.
|
|
75
|
-
const tiltPort = await getFreePort();
|
|
76
|
-
|
|
77
|
-
if (opts.foreground) {
|
|
78
|
-
logger.step(`Starting Tilt for "${cfg.name}" -> https://${cfg.subdomain}.${DOMAIN}`);
|
|
79
|
-
run('tilt', ['up', '--port', String(tiltPort)], { env });
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const { pidFile, portFile, logFile } = runFiles(cfg.name);
|
|
84
|
-
const pid = spawnDetached('tilt', ['up', '--port', String(tiltPort)], { cwd: process.cwd(), env, logFile });
|
|
85
|
-
fs.writeFileSync(pidFile, String(pid));
|
|
86
|
-
fs.writeFileSync(portFile, String(tiltPort));
|
|
87
|
-
logger.step(`Starting Tilt for "${cfg.name}" in the background (pid ${pid})...`);
|
|
88
|
-
logger.info(`Full Tilt output: tail -f ${logFile}`);
|
|
89
|
-
|
|
90
|
-
logger.step('Waiting for the deployment to become ready...');
|
|
91
|
-
// Tilt needs a moment to apply the generated manifests - "rollout status" errors outright
|
|
92
|
-
// on a Deployment that doesn't exist yet, it doesn't wait for one to appear.
|
|
93
|
-
let exists = false;
|
|
94
|
-
for (let i = 0; i < 30; i += 1) {
|
|
95
|
-
if (capture('kubectl', ['get', 'deployment', cfg.name, '-n', cfg.namespace]).status === 0) {
|
|
96
|
-
exists = true;
|
|
97
|
-
break;
|
|
98
|
-
}
|
|
99
|
-
if (!isRunning(pid)) break; // Tilt already died - no point waiting out the rest of the poll
|
|
100
|
-
sleepSync(1000);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const rollout = exists
|
|
104
|
-
? run(
|
|
105
|
-
'kubectl',
|
|
106
|
-
['rollout', 'status', `deployment/${cfg.name}`, '-n', cfg.namespace, '--timeout=180s'],
|
|
107
|
-
{ allowFail: true },
|
|
108
|
-
)
|
|
109
|
-
: { status: 1 };
|
|
110
|
-
|
|
111
|
-
if (rollout.status === 0) {
|
|
112
|
-
printEndpoints(cfg);
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (!isRunning(pid)) {
|
|
117
|
-
logger.warn(`Tilt exited before the deployment came up. Last lines of ${logFile}:`);
|
|
118
|
-
console.log(ui.dim(tailLog(logFile, 15) || '(log file is empty)'));
|
|
119
|
-
} else {
|
|
120
|
-
logger.warn(`Still starting (or something's wrong) - check "localctl app logs ${cfg.name} -f" or "localctl app status".`);
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
};
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const { loadConfig } = require('../lib/loadConfig');
|
|
3
|
+
const { syncHosts, registerApp } = require('../lib/hostsFile');
|
|
4
|
+
const { run, capture, sleepSync, spawnDetached, getFreePort, isRunning } = require('../lib/exec');
|
|
5
|
+
const { detectEngine } = require('../lib/engine');
|
|
6
|
+
const { ensureClusterUp } = require('../lib/clusterProvision');
|
|
7
|
+
const { portsFor } = require('../lib/profileStore');
|
|
8
|
+
const { listKeys } = require('../lib/appSecrets');
|
|
9
|
+
const { runFiles } = require('../lib/paths');
|
|
10
|
+
const { DOMAIN, DEFAULT_HTTPS_PORT } = require('../lib/constants');
|
|
11
|
+
const ui = require('../lib/ui');
|
|
12
|
+
const logger = require('../lib/logger');
|
|
13
|
+
|
|
14
|
+
function podmanBuildEnv() {
|
|
15
|
+
const env = { ...process.env };
|
|
16
|
+
const detected = detectEngine();
|
|
17
|
+
if (detected && detected.engine === 'podman') {
|
|
18
|
+
// Podman's Docker-API-compat socket doesn't implement BuildKit's gRPC build protocol
|
|
19
|
+
// (Tilt's docker_build fails with "unable to upgrade to h2c" otherwise).
|
|
20
|
+
env.DOCKER_BUILDKIT = '0';
|
|
21
|
+
logger.info('Podman detected: disabling BuildKit for this build.');
|
|
22
|
+
}
|
|
23
|
+
return env;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function printEndpoints(cfg) {
|
|
27
|
+
const { httpsPort } = portsFor(cfg.profile);
|
|
28
|
+
const suffix = httpsPort === DEFAULT_HTTPS_PORT ? '' : `:${httpsPort}`;
|
|
29
|
+
const url = `https://${cfg.subdomain}.${DOMAIN}${suffix}`;
|
|
30
|
+
console.log('');
|
|
31
|
+
console.log(`${ui.check} ${ui.bold(cfg.name)} -> ${ui.url(url)}`);
|
|
32
|
+
if (cfg.debug.enabled && cfg.debug.port) {
|
|
33
|
+
console.log(` debug: localhost:${cfg.debug.port}`);
|
|
34
|
+
}
|
|
35
|
+
console.log(` logs: localctl app logs ${cfg.name} -f`);
|
|
36
|
+
console.log(` status: localctl app status`);
|
|
37
|
+
console.log(` stop: localctl app down`);
|
|
38
|
+
console.log('');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function tailLog(logFile, lines) {
|
|
42
|
+
if (!fs.existsSync(logFile)) return null;
|
|
43
|
+
return fs.readFileSync(logFile, 'utf8').trim().split('\n').slice(-lines).join('\n');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = (program) => {
|
|
47
|
+
program
|
|
48
|
+
.command('up')
|
|
49
|
+
.option('-f, --foreground', 'run in the foreground with the interactive Tilt UI (default: detached, survives closing this terminal)')
|
|
50
|
+
.description('Deploy/dev-loop the app in the current directory')
|
|
51
|
+
.action(async (opts) => {
|
|
52
|
+
const cfg = loadConfig();
|
|
53
|
+
ensureClusterUp(cfg.clusterName, cfg.profile);
|
|
54
|
+
|
|
55
|
+
if (cfg.secrets.length > 0) {
|
|
56
|
+
const set = new Set(listKeys(cfg));
|
|
57
|
+
const missing = cfg.secrets.filter((key) => !set.has(key));
|
|
58
|
+
if (missing.length > 0) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Missing secret value${missing.length > 1 ? 's' : ''} for: ${missing.join(', ')}. Set ` +
|
|
61
|
+
`${missing.length > 1 ? 'them' : 'it'} first:\n` +
|
|
62
|
+
missing.map((key) => ` localctl app secrets set ${key}`).join('\n'),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
registerApp(cfg.subdomain);
|
|
68
|
+
syncHosts();
|
|
69
|
+
const env = podmanBuildEnv();
|
|
70
|
+
|
|
71
|
+
// Each app gets its own Tilt web UI port. Tilt's default (10350) is shared/fixed, so two
|
|
72
|
+
// apps running `localctl up` at once - the whole point of detached mode - would otherwise
|
|
73
|
+
// collide outright, and Tilt's resulting error doesn't explain why to someone who never
|
|
74
|
+
// asked to think about Tilt's own dashboard port in the first place.
|
|
75
|
+
const tiltPort = await getFreePort();
|
|
76
|
+
|
|
77
|
+
if (opts.foreground) {
|
|
78
|
+
logger.step(`Starting Tilt for "${cfg.name}" -> https://${cfg.subdomain}.${DOMAIN}`);
|
|
79
|
+
run('tilt', ['up', '--port', String(tiltPort)], { env });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const { pidFile, portFile, logFile } = runFiles(cfg.name);
|
|
84
|
+
const pid = spawnDetached('tilt', ['up', '--port', String(tiltPort)], { cwd: process.cwd(), env, logFile });
|
|
85
|
+
fs.writeFileSync(pidFile, String(pid));
|
|
86
|
+
fs.writeFileSync(portFile, String(tiltPort));
|
|
87
|
+
logger.step(`Starting Tilt for "${cfg.name}" in the background (pid ${pid})...`);
|
|
88
|
+
logger.info(`Full Tilt output: tail -f ${logFile}`);
|
|
89
|
+
|
|
90
|
+
logger.step('Waiting for the deployment to become ready...');
|
|
91
|
+
// Tilt needs a moment to apply the generated manifests - "rollout status" errors outright
|
|
92
|
+
// on a Deployment that doesn't exist yet, it doesn't wait for one to appear.
|
|
93
|
+
let exists = false;
|
|
94
|
+
for (let i = 0; i < 30; i += 1) {
|
|
95
|
+
if (capture('kubectl', ['get', 'deployment', cfg.name, '-n', cfg.namespace]).status === 0) {
|
|
96
|
+
exists = true;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
if (!isRunning(pid)) break; // Tilt already died - no point waiting out the rest of the poll
|
|
100
|
+
sleepSync(1000);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const rollout = exists
|
|
104
|
+
? run(
|
|
105
|
+
'kubectl',
|
|
106
|
+
['rollout', 'status', `deployment/${cfg.name}`, '-n', cfg.namespace, '--timeout=180s'],
|
|
107
|
+
{ allowFail: true },
|
|
108
|
+
)
|
|
109
|
+
: { status: 1 };
|
|
110
|
+
|
|
111
|
+
if (rollout.status === 0) {
|
|
112
|
+
printEndpoints(cfg);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!isRunning(pid)) {
|
|
117
|
+
logger.warn(`Tilt exited before the deployment came up. Last lines of ${logFile}:`);
|
|
118
|
+
console.log(ui.dim(tailLog(logFile, 15) || '(log file is empty)'));
|
|
119
|
+
} else {
|
|
120
|
+
logger.warn(`Still starting (or something's wrong) - check "localctl app logs ${cfg.name} -f" or "localctl app status".`);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
};
|
package/src/index.js
CHANGED
|
@@ -1,36 +1,38 @@
|
|
|
1
|
-
const { Command } = require('commander');
|
|
2
|
-
const { version } = require('../package.json');
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
require('./commands/
|
|
14
|
-
require('./commands/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
require('./commands/
|
|
21
|
-
require('./commands/
|
|
22
|
-
require('./commands/
|
|
23
|
-
require('./commands/
|
|
24
|
-
require('./commands/
|
|
25
|
-
require('./commands/
|
|
26
|
-
require('./commands/
|
|
27
|
-
require('./commands/
|
|
28
|
-
require('./commands/
|
|
29
|
-
|
|
30
|
-
require('./commands/
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const { version } = require('../package.json');
|
|
3
|
+
|
|
4
|
+
require('./lib/updateCheck').start(version);
|
|
5
|
+
|
|
6
|
+
const program = new Command();
|
|
7
|
+
program
|
|
8
|
+
.name('localctl')
|
|
9
|
+
.description('Local Kubernetes dev platform CLI')
|
|
10
|
+
.version(version);
|
|
11
|
+
|
|
12
|
+
// Global/machine-level - not tied to any one app or addon.
|
|
13
|
+
require('./commands/setup')(program);
|
|
14
|
+
require('./commands/uninstall')(program);
|
|
15
|
+
require('./commands/doctor')(program);
|
|
16
|
+
require('./commands/hosts')(program);
|
|
17
|
+
|
|
18
|
+
// Feature-level command groups.
|
|
19
|
+
const app = program.command('app').description('Manage a single app: scaffold, deploy, monitor');
|
|
20
|
+
require('./commands/new')(app);
|
|
21
|
+
require('./commands/up')(app);
|
|
22
|
+
require('./commands/down')(app);
|
|
23
|
+
require('./commands/status')(app);
|
|
24
|
+
require('./commands/logs')(app);
|
|
25
|
+
require('./commands/reload')(app);
|
|
26
|
+
require('./commands/generateManifests')(app);
|
|
27
|
+
require('./commands/registryInfo')(app);
|
|
28
|
+
require('./commands/exec')(app);
|
|
29
|
+
require('./commands/prune')(app);
|
|
30
|
+
require('./commands/secrets')(app); // its own subgroup: set/unset/list/show
|
|
31
|
+
|
|
32
|
+
require('./commands/addons')(program); // already its own group: list/enable/disable/status
|
|
33
|
+
require('./commands/profiles')(program); // already its own group: new/list/switch/status
|
|
34
|
+
|
|
35
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
36
|
+
console.error(err && err.message ? err.message : err);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
});
|
package/src/lib/addonDirs.js
CHANGED
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const os = require('os');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
|
|
5
|
-
// Addons are searched in two places, in precedence order:
|
|
6
|
-
// 1. USER_DIR - yours, never touched by a `localctl` update/reinstall. Defaults to
|
|
7
|
-
// ~/.localctl/addons, overridable via LOCALCTL_ADDONS_DIR (e.g. to point at a
|
|
8
|
-
// folder checked into your own infra repo instead).
|
|
9
|
-
// 2. BUILTIN_DIR - the addons shipped with this CLI (postgres/redis/mongo/cluster/logging).
|
|
10
|
-
// A user-dir addon of the same name replaces the built-in one; a new name just adds to the list.
|
|
11
|
-
// This is what lets someone install this tool without inheriting its author's addon set, and
|
|
12
|
-
// without forking the tool to add their own - see docs/adding-tools.md.
|
|
13
|
-
const BUILTIN_DIR = path.join(__dirname, 'addons');
|
|
14
|
-
const USER_DIR = process.env.LOCALCTL_ADDONS_DIR || path.join(os.homedir(), '.localctl', 'addons');
|
|
15
|
-
|
|
16
|
-
function roots(...segments) {
|
|
17
|
-
return [path.join(USER_DIR, ...segments), path.join(BUILTIN_DIR, ...segments)];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// First matching addon.yaml wins - user dir searched before built-in.
|
|
21
|
-
function findAddonDir(name, ...segments) {
|
|
22
|
-
for (const root of roots(...segments)) {
|
|
23
|
-
const dir = path.join(root, name);
|
|
24
|
-
if (fs.existsSync(path.join(dir, 'addon.yaml'))) return dir;
|
|
25
|
-
}
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Every addon name available across both locations, user-dir entries deduplicated against
|
|
30
|
-
// built-ins of the same name (a listing, not a "which one wins" answer - see findAddonDir for that).
|
|
31
|
-
function listAddonNames(...segments) {
|
|
32
|
-
const names = new Set();
|
|
33
|
-
for (const root of roots(...segments)) {
|
|
34
|
-
if (!fs.existsSync(root)) continue;
|
|
35
|
-
fs.readdirSync(root, { withFileTypes: true })
|
|
36
|
-
.filter((e) => e.isDirectory())
|
|
37
|
-
.filter((e) => fs.existsSync(path.join(root, e.name, 'addon.yaml')))
|
|
38
|
-
.forEach((e) => names.add(e.name));
|
|
39
|
-
}
|
|
40
|
-
return [...names];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
module.exports = { BUILTIN_DIR, USER_DIR, findAddonDir, listAddonNames };
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
// Addons are searched in two places, in precedence order:
|
|
6
|
+
// 1. USER_DIR - yours, never touched by a `localctl` update/reinstall. Defaults to
|
|
7
|
+
// ~/.localctl/addons, overridable via LOCALCTL_ADDONS_DIR (e.g. to point at a
|
|
8
|
+
// folder checked into your own infra repo instead).
|
|
9
|
+
// 2. BUILTIN_DIR - the addons shipped with this CLI (postgres/redis/mongo/cluster/logging).
|
|
10
|
+
// A user-dir addon of the same name replaces the built-in one; a new name just adds to the list.
|
|
11
|
+
// This is what lets someone install this tool without inheriting its author's addon set, and
|
|
12
|
+
// without forking the tool to add their own - see docs/adding-tools.md.
|
|
13
|
+
const BUILTIN_DIR = path.join(__dirname, 'addons');
|
|
14
|
+
const USER_DIR = process.env.LOCALCTL_ADDONS_DIR || path.join(os.homedir(), '.localctl', 'addons');
|
|
15
|
+
|
|
16
|
+
function roots(...segments) {
|
|
17
|
+
return [path.join(USER_DIR, ...segments), path.join(BUILTIN_DIR, ...segments)];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// First matching addon.yaml wins - user dir searched before built-in.
|
|
21
|
+
function findAddonDir(name, ...segments) {
|
|
22
|
+
for (const root of roots(...segments)) {
|
|
23
|
+
const dir = path.join(root, name);
|
|
24
|
+
if (fs.existsSync(path.join(dir, 'addon.yaml'))) return dir;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Every addon name available across both locations, user-dir entries deduplicated against
|
|
30
|
+
// built-ins of the same name (a listing, not a "which one wins" answer - see findAddonDir for that).
|
|
31
|
+
function listAddonNames(...segments) {
|
|
32
|
+
const names = new Set();
|
|
33
|
+
for (const root of roots(...segments)) {
|
|
34
|
+
if (!fs.existsSync(root)) continue;
|
|
35
|
+
fs.readdirSync(root, { withFileTypes: true })
|
|
36
|
+
.filter((e) => e.isDirectory())
|
|
37
|
+
.filter((e) => fs.existsSync(path.join(root, e.name, 'addon.yaml')))
|
|
38
|
+
.forEach((e) => names.add(e.name));
|
|
39
|
+
}
|
|
40
|
+
return [...names];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { BUILTIN_DIR, USER_DIR, findAddonDir, listAddonNames };
|