@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
package/src/lib/ui.js CHANGED
@@ -1,50 +1,50 @@
1
- const chalk = require('chalk');
2
-
3
- // eslint-disable-next-line no-control-regex
4
- const ANSI_RE = /\x1b\[[0-9;]*m/g;
5
-
6
- function visibleLength(str) {
7
- return String(str).replace(ANSI_RE, '').length;
8
- }
9
-
10
- function padVisible(str, width) {
11
- const pad = Math.max(0, width - visibleLength(str));
12
- return str + ' '.repeat(pad);
13
- }
14
-
15
- // Column widths computed from visible (non-ANSI) length, so colored cells still line up.
16
- function printTable(headers, rows) {
17
- const widths = headers.map((h, i) =>
18
- Math.max(visibleLength(h), ...rows.map((r) => visibleLength(r[i])), 0),
19
- );
20
- console.log(headers.map((h, i) => padVisible(chalk.bold(h), widths[i])).join(' ').trimEnd());
21
- rows.forEach((row) => {
22
- console.log(row.map((cell, i) => padVisible(String(cell), widths[i])).join(' ').trimEnd());
23
- });
24
- }
25
-
26
- const ok = (s) => chalk.green(s);
27
- const warn = (s) => chalk.yellow(s);
28
- const dim = (s) => chalk.gray(s);
29
-
30
- // `ready`/`desired` replica counts -> a colored "x/y" string. Used anywhere a workload's
31
- // readiness is summarized (app status, addon status) so the coloring rule stays in one place.
32
- function readyLabel(ready, desired) {
33
- const text = `${ready}/${desired}`;
34
- return ready >= desired && desired > 0 ? ok(text) : warn(text);
35
- }
36
-
37
- module.exports = {
38
- printTable,
39
- visibleLength,
40
- padVisible,
41
- readyLabel,
42
- ok,
43
- warn,
44
- err: (s) => chalk.red(s),
45
- dim,
46
- bold: (s) => chalk.bold(s),
47
- url: (s) => chalk.cyan.underline(s),
48
- check: chalk.green('✔'),
49
- cross: chalk.red('✖'),
50
- };
1
+ const chalk = require('chalk');
2
+
3
+ // eslint-disable-next-line no-control-regex
4
+ const ANSI_RE = /\x1b\[[0-9;]*m/g;
5
+
6
+ function visibleLength(str) {
7
+ return String(str).replace(ANSI_RE, '').length;
8
+ }
9
+
10
+ function padVisible(str, width) {
11
+ const pad = Math.max(0, width - visibleLength(str));
12
+ return str + ' '.repeat(pad);
13
+ }
14
+
15
+ // Column widths computed from visible (non-ANSI) length, so colored cells still line up.
16
+ function printTable(headers, rows) {
17
+ const widths = headers.map((h, i) =>
18
+ Math.max(visibleLength(h), ...rows.map((r) => visibleLength(r[i])), 0),
19
+ );
20
+ console.log(headers.map((h, i) => padVisible(chalk.bold(h), widths[i])).join(' ').trimEnd());
21
+ rows.forEach((row) => {
22
+ console.log(row.map((cell, i) => padVisible(String(cell), widths[i])).join(' ').trimEnd());
23
+ });
24
+ }
25
+
26
+ const ok = (s) => chalk.green(s);
27
+ const warn = (s) => chalk.yellow(s);
28
+ const dim = (s) => chalk.gray(s);
29
+
30
+ // `ready`/`desired` replica counts -> a colored "x/y" string. Used anywhere a workload's
31
+ // readiness is summarized (app status, addon status) so the coloring rule stays in one place.
32
+ function readyLabel(ready, desired) {
33
+ const text = `${ready}/${desired}`;
34
+ return ready >= desired && desired > 0 ? ok(text) : warn(text);
35
+ }
36
+
37
+ module.exports = {
38
+ printTable,
39
+ visibleLength,
40
+ padVisible,
41
+ readyLabel,
42
+ ok,
43
+ warn,
44
+ err: (s) => chalk.red(s),
45
+ dim,
46
+ bold: (s) => chalk.bold(s),
47
+ url: (s) => chalk.cyan.underline(s),
48
+ check: chalk.green('✔'),
49
+ cross: chalk.red('✖'),
50
+ };
@@ -0,0 +1,84 @@
1
+ // Tells the user when a newer @localctl/cli is on npm. Never slows a command down: the notice is
2
+ // printed from a cached result, and the cache is refreshed (at most once a day) by a detached
3
+ // background process that outlives this one. Silent in CI, when stderr isn't a terminal, when
4
+ // running from a source checkout, or with LOCALCTL_NO_UPDATE_CHECK=1.
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { spawn } = require('child_process');
8
+ const chalk = require('chalk');
9
+ const { HOME_STATE_DIR } = require('./paths');
10
+
11
+ const PACKAGE_NAME = '@localctl/cli';
12
+ const CACHE_PATH = path.join(HOME_STATE_DIR, 'update-check.json');
13
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
14
+ const DEV_VERSION = '0.0.0-development';
15
+
16
+ function parse(version) {
17
+ const [core, pre] = String(version).split('-');
18
+ return { nums: core.split('.').map((n) => parseInt(n, 10) || 0), pre: pre || null };
19
+ }
20
+
21
+ // Plain x.y.z comparison; a pre-release is older than the same x.y.z without one. Good enough to
22
+ // decide "is there something newer" without pulling in a semver dependency.
23
+ function isNewer(candidate, current) {
24
+ const a = parse(candidate);
25
+ const b = parse(current);
26
+ for (let i = 0; i < 3; i += 1) {
27
+ if (a.nums[i] !== b.nums[i]) return a.nums[i] > b.nums[i];
28
+ }
29
+ if (a.pre === b.pre) return false;
30
+ if (!a.pre) return true;
31
+ if (!b.pre) return false;
32
+ return a.pre.localeCompare(b.pre, undefined, { numeric: true }) > 0;
33
+ }
34
+
35
+ function readCache() {
36
+ try {
37
+ return JSON.parse(fs.readFileSync(CACHE_PATH, 'utf8'));
38
+ } catch {
39
+ return {};
40
+ }
41
+ }
42
+
43
+ function enabled(currentVersion) {
44
+ return !process.env.LOCALCTL_NO_UPDATE_CHECK
45
+ && !process.env.CI
46
+ && process.stderr.isTTY
47
+ && currentVersion !== DEV_VERSION;
48
+ }
49
+
50
+ function start(currentVersion) {
51
+ if (!enabled(currentVersion)) return;
52
+ const cache = readCache();
53
+
54
+ // Users on a pre-release also get told about newer pre-releases on their own channel.
55
+ const { pre } = parse(currentVersion);
56
+ const channel = pre ? pre.split('.')[0] : null;
57
+ const candidates = [cache.distTags?.latest, channel && cache.distTags?.[channel]].filter(Boolean);
58
+ const newest = candidates.filter((v) => isNewer(v, currentVersion))
59
+ .sort((x, y) => (isNewer(x, y) ? -1 : 1))[0];
60
+
61
+ if (newest) {
62
+ const tag = newest.includes('-') ? `@${channel}` : '';
63
+ process.on('exit', () => {
64
+ console.error();
65
+ console.error(`${chalk.yellow('Update available:')} ${currentVersion} -> ${chalk.green(newest)}`);
66
+ console.error(`Run ${chalk.cyan(`npm install -g ${PACKAGE_NAME}${tag}`)} to update.`);
67
+ });
68
+ }
69
+
70
+ if (!cache.checkedAt || Date.now() - cache.checkedAt > CHECK_INTERVAL_MS) {
71
+ try {
72
+ const child = spawn(process.execPath, [path.join(__dirname, 'updateCheckWorker.js'), CACHE_PATH, PACKAGE_NAME], {
73
+ detached: true,
74
+ stdio: 'ignore',
75
+ windowsHide: true,
76
+ });
77
+ child.unref();
78
+ } catch {
79
+ // Best effort - a failed check must never affect the command the user actually ran.
80
+ }
81
+ }
82
+ }
83
+
84
+ module.exports = { start, isNewer };
@@ -0,0 +1,46 @@
1
+ // Background half of updateCheck.js: fetches the package's npm dist-tags and caches them. Runs
2
+ // detached, so it may outlive the command that started it; every failure is swallowed.
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const https = require('https');
6
+
7
+ const [cachePath, packageName] = process.argv.slice(2);
8
+
9
+ function write(data) {
10
+ try {
11
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
12
+ fs.writeFileSync(cachePath, JSON.stringify(data));
13
+ } catch {
14
+ // Nothing useful to do - the next command simply tries again.
15
+ }
16
+ }
17
+
18
+ let previous = {};
19
+ try {
20
+ previous = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
21
+ } catch {
22
+ // No cache yet.
23
+ }
24
+
25
+ // Record the attempt up front, so a slow or failing registry is retried tomorrow, not on every
26
+ // command in between.
27
+ write({ ...previous, checkedAt: Date.now() });
28
+
29
+ const req = https.get(`https://registry.npmjs.org/-/package/${packageName}/dist-tags`, { timeout: 5000 }, (res) => {
30
+ if (res.statusCode !== 200) {
31
+ res.resume();
32
+ return;
33
+ }
34
+ let body = '';
35
+ res.setEncoding('utf8');
36
+ res.on('data', (chunk) => { body += chunk; });
37
+ res.on('end', () => {
38
+ try {
39
+ write({ checkedAt: Date.now(), distTags: JSON.parse(body) });
40
+ } catch {
41
+ // Malformed response - keep the previous cache.
42
+ }
43
+ });
44
+ });
45
+ req.on('timeout', () => req.destroy());
46
+ req.on('error', () => {});
@@ -1,35 +1,35 @@
1
- const { capture } = require('./exec');
2
-
3
- function getList(args) {
4
- const res = capture('kubectl', [...args, '-o', 'json']);
5
- if (res.status !== 0) return [];
6
- try {
7
- return JSON.parse(res.stdout).items || [];
8
- } catch (e) {
9
- return [];
10
- }
11
- }
12
-
13
- // Deployment/StatefulSet/DaemonSet all expose ready/desired counts, just under slightly
14
- // different field names depending on kind.
15
- function readyCounts(workload) {
16
- const status = workload.status || {};
17
- const ready = status.readyReplicas ?? status.numberReady ?? 0;
18
- const desired = (workload.spec || {}).replicas ?? status.desiredNumberScheduled ?? 0;
19
- return { ready, desired };
20
- }
21
-
22
- // Every workload (any kind) in a namespace whose name starts with `prefix`, with ready/desired
23
- // counts - used to summarize a release's health without needing to know in advance which
24
- // workload kind(s) the underlying Helm chart happens to use (this is exactly the fragility that
25
- // bit `localctl status`'s dependency listing once already: Bitnami's Postgres/Redis charts use a
26
- // StatefulSet, not the Deployment a hand-rolled addon would have used).
27
- function workloadsWithPrefix(namespace, prefix) {
28
- const kinds = ['deployment', 'statefulset', 'daemonset'];
29
- return kinds
30
- .flatMap((kind) => getList(['get', kind, '-n', namespace]))
31
- .filter((w) => w.metadata.name.startsWith(prefix))
32
- .map((w) => ({ name: w.metadata.name, ...readyCounts(w) }));
33
- }
34
-
35
- module.exports = { getList, readyCounts, workloadsWithPrefix };
1
+ const { capture } = require('./exec');
2
+
3
+ function getList(args) {
4
+ const res = capture('kubectl', [...args, '-o', 'json']);
5
+ if (res.status !== 0) return [];
6
+ try {
7
+ return JSON.parse(res.stdout).items || [];
8
+ } catch (e) {
9
+ return [];
10
+ }
11
+ }
12
+
13
+ // Deployment/StatefulSet/DaemonSet all expose ready/desired counts, just under slightly
14
+ // different field names depending on kind.
15
+ function readyCounts(workload) {
16
+ const status = workload.status || {};
17
+ const ready = status.readyReplicas ?? status.numberReady ?? 0;
18
+ const desired = (workload.spec || {}).replicas ?? status.desiredNumberScheduled ?? 0;
19
+ return { ready, desired };
20
+ }
21
+
22
+ // Every workload (any kind) in a namespace whose name starts with `prefix`, with ready/desired
23
+ // counts - used to summarize a release's health without needing to know in advance which
24
+ // workload kind(s) the underlying Helm chart happens to use (this is exactly the fragility that
25
+ // bit `localctl status`'s dependency listing once already: Bitnami's Postgres/Redis charts use a
26
+ // StatefulSet, not the Deployment a hand-rolled addon would have used).
27
+ function workloadsWithPrefix(namespace, prefix) {
28
+ const kinds = ['deployment', 'statefulset', 'daemonset'];
29
+ return kinds
30
+ .flatMap((kind) => getList(['get', kind, '-n', namespace]))
31
+ .filter((w) => w.metadata.name.startsWith(prefix))
32
+ .map((w) => ({ name: w.metadata.name, ...readyCounts(w) }));
33
+ }
34
+
35
+ module.exports = { getList, readyCounts, workloadsWithPrefix };
@@ -1,35 +1,35 @@
1
- # Generated by `localctl app new`.
2
- # Most configuration lives in .local/config.json - edit that instead of this file
3
- # unless you need custom Tilt behavior (extra resources, manual k8s_yaml, etc).
4
-
5
- config_json = read_json('.local/config.json')
6
-
7
- # Images are built and pushed from your machine to this app's project registry (localhost:5050
8
- # for the default project - port 5050, not 5000, to dodge macOS Control Center's claim on 5000 -
9
- # or a project-specific port for another Main project, see docs/architecture.md), but the cluster
10
- # pulls them back using the registry's in-network name. Resolved per app via `localctl app
11
- # registry-info` instead of hardcoded, since a project on its own cluster has its own registry.
12
- registry_info = decode_json(str(local(['localctl', 'app', 'registry-info'], quiet=True)))
13
- default_registry(registry_info['pushHost'], host_from_cluster=registry_info['clusterHost'])
14
-
15
- docker_build(
16
- config_json['name'],
17
- config_json['build'].get('context', '.'),
18
- dockerfile=config_json['build'].get('dockerfile', 'Dockerfile'),
19
- live_update=[
20
- sync(s['localPath'], s['remotePath'])
21
- for s in config_json.get('sync', [])
22
- ],
23
- )
24
-
25
- # Manifests are generated by the localctl CLI from .local/config.json so there is a single
26
- # source of truth for the app's k8s definition.
27
- k8s_yaml(local(['localctl', 'app', 'generate-manifests']))
28
-
29
- port_forwards = ['%s:%s' % (config_json['port'], config_json['port'])]
30
- debug_cfg = config_json.get('debug', {})
31
- if debug_cfg.get('enabled'):
32
- dp = debug_cfg['port']
33
- port_forwards.append('%s:%s' % (dp, dp))
34
-
35
- k8s_resource(config_json['name'], port_forwards=port_forwards)
1
+ # Generated by `localctl app new`.
2
+ # Most configuration lives in .local/config.json - edit that instead of this file
3
+ # unless you need custom Tilt behavior (extra resources, manual k8s_yaml, etc).
4
+
5
+ config_json = read_json('.local/config.json')
6
+
7
+ # Images are built and pushed from your machine to this app's project registry (localhost:5050
8
+ # for the default project - port 5050, not 5000, to dodge macOS Control Center's claim on 5000 -
9
+ # or a project-specific port for another Main project, see docs/architecture.md), but the cluster
10
+ # pulls them back using the registry's in-network name. Resolved per app via `localctl app
11
+ # registry-info` instead of hardcoded, since a project on its own cluster has its own registry.
12
+ registry_info = decode_json(str(local(['localctl', 'app', 'registry-info'], quiet=True)))
13
+ default_registry(registry_info['pushHost'], host_from_cluster=registry_info['clusterHost'])
14
+
15
+ docker_build(
16
+ config_json['name'],
17
+ config_json['build'].get('context', '.'),
18
+ dockerfile=config_json['build'].get('dockerfile', 'Dockerfile'),
19
+ live_update=[
20
+ sync(s['localPath'], s['remotePath'])
21
+ for s in config_json.get('sync', [])
22
+ ],
23
+ )
24
+
25
+ # Manifests are generated by the localctl CLI from .local/config.json so there is a single
26
+ # source of truth for the app's k8s definition.
27
+ k8s_yaml(local(['localctl', 'app', 'generate-manifests']))
28
+
29
+ port_forwards = ['%s:%s' % (config_json['port'], config_json['port'])]
30
+ debug_cfg = config_json.get('debug', {})
31
+ if debug_cfg.get('enabled'):
32
+ dp = debug_cfg['port']
33
+ port_forwards.append('%s:%s' % (dp, dp))
34
+
35
+ k8s_resource(config_json['name'], port_forwards=port_forwards)