@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.
Files changed (52) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +398 -431
  3. package/assets/manifests/namespace-addons.yaml +4 -4
  4. package/assets/manifests/tls-store.yaml +8 -8
  5. package/bin/localctl.js +2 -2
  6. package/package.json +61 -54
  7. package/src/commands/addons.js +56 -56
  8. package/src/commands/doctor.js +44 -43
  9. package/src/commands/down.js +35 -35
  10. package/src/commands/exec.js +15 -15
  11. package/src/commands/generateManifests.js +12 -12
  12. package/src/commands/hosts.js +23 -23
  13. package/src/commands/logs.js +45 -45
  14. package/src/commands/profiles.js +180 -180
  15. package/src/commands/prune.js +49 -49
  16. package/src/commands/registryInfo.js +18 -18
  17. package/src/commands/reload.js +44 -44
  18. package/src/commands/secrets.js +75 -75
  19. package/src/commands/setup.js +36 -36
  20. package/src/commands/status.js +136 -136
  21. package/src/commands/up.js +123 -123
  22. package/src/index.js +38 -36
  23. package/src/lib/addonDirs.js +43 -43
  24. package/src/lib/addonLoader.js +190 -190
  25. package/src/lib/addons/cluster/logging/addon.yaml +46 -46
  26. package/src/lib/addons/cluster/logging/config/all-logs.json +21 -21
  27. package/src/lib/addons/cluster/monitoring/addon.yaml +33 -33
  28. package/src/lib/addons/mongo/addon.yaml +18 -18
  29. package/src/lib/addons/postgres/addon.yaml +39 -39
  30. package/src/lib/addons/redis/addon.yaml +29 -29
  31. package/src/lib/appSecrets.js +60 -60
  32. package/src/lib/clusterAddons.js +59 -59
  33. package/src/lib/clusterProvision.js +171 -170
  34. package/src/lib/configSchema.js +103 -103
  35. package/src/lib/constants.js +16 -16
  36. package/src/lib/engine.js +36 -20
  37. package/src/lib/exec.js +77 -77
  38. package/src/lib/hostsFile.js +79 -79
  39. package/src/lib/loadConfig.js +51 -51
  40. package/src/lib/logger.js +9 -9
  41. package/src/lib/manifestGen.js +210 -210
  42. package/src/lib/paths.js +41 -41
  43. package/src/lib/podman.js +72 -72
  44. package/src/lib/profileStore.js +134 -134
  45. package/src/lib/projectContext.js +32 -32
  46. package/src/lib/prompt.js +97 -97
  47. package/src/lib/toolInstall.js +30 -30
  48. package/src/lib/ui.js +50 -50
  49. package/src/lib/updateCheck.js +84 -0
  50. package/src/lib/updateCheckWorker.js +46 -0
  51. package/src/lib/workloadReadiness.js +35 -35
  52. package/templates/Tiltfile.tmpl +35 -35
@@ -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
- const program = new Command();
5
- program
6
- .name('localctl')
7
- .description('Local Kubernetes dev platform CLI')
8
- .version(version);
9
-
10
- // Global/machine-level - not tied to any one app or addon.
11
- require('./commands/setup')(program);
12
- require('./commands/uninstall')(program);
13
- require('./commands/doctor')(program);
14
- require('./commands/hosts')(program);
15
-
16
- // Feature-level command groups.
17
- const app = program.command('app').description('Manage a single app: scaffold, deploy, monitor');
18
- require('./commands/new')(app);
19
- require('./commands/up')(app);
20
- require('./commands/down')(app);
21
- require('./commands/status')(app);
22
- require('./commands/logs')(app);
23
- require('./commands/reload')(app);
24
- require('./commands/generateManifests')(app);
25
- require('./commands/registryInfo')(app);
26
- require('./commands/exec')(app);
27
- require('./commands/prune')(app);
28
- require('./commands/secrets')(app); // its own subgroup: set/unset/list/show
29
-
30
- require('./commands/addons')(program); // already its own group: list/enable/disable/status
31
- require('./commands/profiles')(program); // already its own group: new/list/switch/status
32
-
33
- program.parseAsync(process.argv).catch((err) => {
34
- console.error(err && err.message ? err.message : err);
35
- process.exit(1);
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
+ });
@@ -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 };