@localctl/cli 0.4.0 โ†’ 0.4.2

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/README.md CHANGED
@@ -75,17 +75,19 @@ JSON file per app.
75
75
  |---|---|
76
76
  | **macOS** or **Windows** | Windows commands run in PowerShell. |
77
77
  | **Node.js 18+** | Needed to install the CLI from npm. |
78
- | **Container engine** | [Podman Desktop](https://podman-desktop.io/), running. See [supported container engines](#supported-container-engines). |
78
+ | **Container engine** | [Podman Desktop](https://podman-desktop.io/) (or Docker Desktop / Rancher Desktop), running. See [supported container engines](#supported-container-engines). |
79
79
  | **Homebrew** or **winget** | Used once by `localctl setup` to install missing tools. |
80
80
 
81
81
  `localctl setup` installs `k3d`, `kubectl`, `tilt`, `mkcert` and `helm` for you if they're missing.
82
+ On Windows, Tilt isn't on winget, so it's downloaded from its GitHub release into
83
+ `~.localctl ools` and added to your user PATH.
82
84
 
83
85
  ### Supported container engines
84
86
 
85
87
  | Engine | Status |
86
88
  |---|---|
87
89
  | [Podman Desktop](https://podman-desktop.io/) | โœ… Tested |
88
- | [Docker Desktop](https://www.docker.com/products/docker-desktop/) | ๐Ÿงช Experimental โ€” not yet tested |
90
+ | [Docker Desktop](https://www.docker.com/products/docker-desktop/) | ๐Ÿงช Experimental โ€” setup and app deploy verified on Windows |
89
91
  | [Rancher Desktop](https://rancherdesktop.io/) | ๐Ÿงช Experimental โ€” not yet tested |
90
92
 
91
93
  Experimental engines are detected and should work, since k3d only needs a Docker-compatible API,
@@ -106,7 +108,9 @@ the local image registry, trusts the `*.local.test` certificate, and updates you
106
108
  is idempotent: run it again any time to repair your environment.
107
109
 
108
110
  You'll see up to two one-time prompts during setup: **mkcert** asking to trust its local
109
- certificate authority, and **sudo/Administrator** to write the `*.local.test` hosts entries.
111
+ certificate authority, and **admin rights** to write the `*.local.test` hosts entries (your
112
+ `sudo` password on macOS, a UAC prompt on Windows). Approve both: without the hosts entries, apps
113
+ run but their addresses don't resolve.
110
114
 
111
115
  **Pre-releases** are published under the `beta` tag: `npm install -g @localctl/cli@beta`.
112
116
 
@@ -120,7 +124,8 @@ Update available: 0.2.0 -> 0.3.0
120
124
  Run npm install -g @localctl/cli to update.
121
125
  ```
122
126
 
123
- Update with `npm install -g @localctl/cli`. The check never slows a command down, is skipped in
127
+ Update with `npm install -g @localctl/cli`. On a pre-release you're also told about newer
128
+ versions on your own channel (e.g. `beta`). The check never slows a command down, is skipped in
124
129
  CI, and can be turned off with `LOCALCTL_NO_UPDATE_CHECK=1`.
125
130
 
126
131
  <details>
@@ -204,6 +209,16 @@ localctl app down
204
209
 
205
210
  This removes only that app. The shared cluster keeps running for everything else.
206
211
 
212
+ <details>
213
+ <summary><b>App is ready but <code>https://&lt;app&gt;.local.test</code> doesn't open?</b></summary>
214
+
215
+ If `localctl app status` shows `1/1` but the browser can't reach the address, the hosts file
216
+ entry is missing: the `sudo` or UAC prompt was declined or missed. Run `localctl hosts sync` and
217
+ approve the prompt. More cases, with cause and fix:
218
+ [**Troubleshooting**](https://psilvmoreira.github.io/localctl/troubleshooting/).
219
+
220
+ </details>
221
+
207
222
  > **Tip:** no app of your own yet? Clone the repo and try a ready-made example:
208
223
  > [`node-app`](https://github.com/psilvmoreira/localctl/tree/master/examples/node-app) (Node.js + Postgres),
209
224
  > [`python-app`](https://github.com/psilvmoreira/localctl/tree/master/examples/python-app) (Python + Redis), or
@@ -211,7 +226,8 @@ This removes only that app. The shared cluster keeps running for everything else
211
226
 
212
227
  ## Commands
213
228
 
214
- Run `localctl <command> --help` for every option.
229
+ Run `localctl <command> --help` for every option, or see the full
230
+ [**CLI reference**](https://psilvmoreira.github.io/localctl/cli-reference/).
215
231
 
216
232
  <details open>
217
233
  <summary><b>Machine setup</b></summary>
@@ -220,7 +236,7 @@ Run `localctl <command> --help` for every option.
220
236
  |---|---|
221
237
  | `localctl setup` | Install or repair the infrastructure: container engine, cluster, TLS, hosts. Idempotent. |
222
238
  | `localctl doctor` | Health check of the active project's environment. |
223
- | `localctl hosts sync` / `list` | Manage the `*.local.test` entries in your hosts file. |
239
+ | `localctl hosts sync` / `list` | Write or list the `*.local.test` entries in your hosts file. Asks for admin rights when needed. |
224
240
  | `localctl uninstall [-y]` | Remove every cluster, registry, hosts entry, certificate and state file `localctl` created. |
225
241
 
226
242
  </details>
@@ -235,7 +251,7 @@ Run `localctl <command> --help` for every option.
235
251
  | `localctl app down` | Stop the dev loop and remove the app. Keeps its data. |
236
252
  | `localctl app reload [-a <name>]` | Force a rebuild and redeploy without waiting for a file change. |
237
253
  | `localctl app status [-a <name>] [-A]` | One app's detail inside its repository, or a table of every app. |
238
- | `localctl app logs <name> [-f]` | Show or follow an app's logs. |
254
+ | `localctl app logs [name] [-f]` | Show or follow an app's logs. Inside its repository the name is optional. |
239
255
  | `localctl app exec [cmd...]` | Open a shell (default `sh`) in the app's pod. |
240
256
  | `localctl app prune [-y]` | Delete leftover data (volumes, secrets) of apps already taken down. |
241
257
  | `localctl app secrets set <KEY> [value]` | Store a secret as a Kubernetes Secret, never in `config.json`. |
@@ -318,11 +334,13 @@ The full documentation lives at **[psilvmoreira.github.io/localctl](https://psil
318
334
  |---|---|
319
335
  | [Getting started](https://psilvmoreira.github.io/localctl/getting-started/) | Install and first deploy, step by step |
320
336
  | [Architecture](https://psilvmoreira.github.io/localctl/architecture/) | Every component, why it was chosen, multi-project support |
337
+ | [CLI commands](https://psilvmoreira.github.io/localctl/cli-reference/) | Every command and option |
321
338
  | [Config schema](https://psilvmoreira.github.io/localctl/config-schema/) | Every `.local/config.json` field |
322
339
  | [Debugging](https://psilvmoreira.github.io/localctl/debugging/) | Attach a debugger in Node, Python, Go, Java and .NET |
323
340
  | [Adding tools](https://psilvmoreira.github.io/localctl/adding-tools/) | Write your own per-app or cluster-wide addon |
324
341
  | [Troubleshooting](https://psilvmoreira.github.io/localctl/troubleshooting/) | Known issues, with cause and fix |
325
342
  | [Uninstalling](https://psilvmoreira.github.io/localctl/uninstalling/) | Full teardown reference |
343
+ | [Contributing](https://psilvmoreira.github.io/localctl/contributing/) | Local setup, tests, pull requests, dependency policy, docs site |
326
344
  | [Releasing](https://psilvmoreira.github.io/localctl/releasing/) | How versions are cut and published |
327
345
 
328
346
  ## Releases and versioning
@@ -333,15 +351,21 @@ The full documentation lives at **[psilvmoreira.github.io/localctl](https://psil
333
351
  with notes and the package tarball.
334
352
  - Versions follow [Semantic Versioning](https://semver.org). Before `1.0.0`, minor versions may
335
353
  contain breaking changes.
336
- - npm releases are published from GitHub Actions with
354
+ - Every pull request shows a **release preview**: the version that merging it would publish,
355
+ including any unreleased commits already on `master`.
356
+ - npm releases are published from GitHub Actions through
357
+ [trusted publishing](https://docs.npmjs.com/trusted-publishers) (no npm tokens) with
337
358
  [provenance](https://docs.npmjs.com/generating-provenance-statements). The npm package page links
338
359
  each version to the exact commit and workflow run that built it.
360
+ - Dependencies and GitHub Actions are kept up to date by Dependabot, with every action pinned to a
361
+ commit SHA.
339
362
 
340
363
  ## Contributing
341
364
 
342
- Issues and pull requests are welcome.
365
+ Issues and pull requests are welcome. The full guide (constraints, dependency policy, working on
366
+ the docs site) is in [**Contributing**](https://psilvmoreira.github.io/localctl/contributing/).
343
367
 
344
- 1. Fork the repository and create a branch.
368
+ 1. Fork the repository and create a branch from an up-to-date `master`.
345
369
  2. Make your change and run the tests:
346
370
  ```sh
347
371
  cd cli
@@ -350,15 +374,19 @@ Issues and pull requests are welcome.
350
374
  npm run test:pack # the published package installs and runs
351
375
  ```
352
376
  3. Open a pull request with a [Conventional Commits](https://www.conventionalcommits.org/) title.
353
- The title decides the next version:
377
+ PRs are squash-merged, so the title decides the next version. GitHub pre-fills it from the
378
+ branch name, which fails the title check: edit it.
354
379
 
355
380
  | Title | Release |
356
381
  |---|---|
357
- | `fix: ...` | Patch (`0.2.0` โ†’ `0.2.1`) |
358
- | `feat: ...` | Minor (`0.2.1` โ†’ `0.3.0`) |
359
- | `feat!: ...` | Major (`0.3.0` โ†’ `1.0.0`) |
382
+ | `fix: ...`, `perf: ...` | Patch (`0.4.0` โ†’ `0.4.1`) |
383
+ | `feat: ...` | Minor (`0.4.1` โ†’ `0.5.0`) |
384
+ | `feat!: ...` | Major (`0.5.0` โ†’ `1.0.0`) |
360
385
  | `docs:` `chore:` `ci:` `refactor:` `test:` | No release |
361
386
 
387
+ 4. Check the **release preview** on the `pr-title` check and wait for every check to pass
388
+ (title, tests on Linux/macOS/Windows ร— Node 18/22, docs build, non-empty diff).
389
+
362
390
  <details>
363
391
  <summary><b>Repository layout</b></summary>
364
392
 
@@ -374,8 +402,10 @@ localctl/
374
402
  โ”œโ”€โ”€ cluster/manifests/ cluster-level manifests (TLS store, addon namespace)
375
403
  โ”œโ”€โ”€ schema/ JSON Schema for .local/config.json
376
404
  โ”œโ”€โ”€ examples/ ready-to-run sample apps
377
- โ”œโ”€โ”€ docs/ documentation site (MkDocs Material, deployed to GitHub Pages)
378
- โ””โ”€โ”€ bin/ bootstrap and uninstall scripts for source installs
405
+ โ”œโ”€โ”€ docs/ documentation pages (MkDocs Material, deployed to GitHub Pages)
406
+ โ”œโ”€โ”€ overrides/ docs site template overrides (landing page)
407
+ โ”œโ”€โ”€ bin/ bootstrap and uninstall scripts for source installs
408
+ โ””โ”€โ”€ .github/ CI, release and docs workflows, Dependabot config
379
409
  ```
380
410
 
381
411
  </details>
@@ -388,8 +418,10 @@ npm uninstall -g @localctl/cli # the CLI itself
388
418
  ```
389
419
 
390
420
  `localctl uninstall` asks for confirmation (skip with `-y`) and is safe to re-run. It leaves the
391
- shared tools (`k3d`, `kubectl`, `tilt`, `mkcert`, `node`) and mkcert's root CA installed, since
392
- other projects may use them. It prints the exact commands to remove them too. Installed from
421
+ shared tools (`k3d`, `kubectl`, `tilt`, `mkcert`, `helm`, `node`) and mkcert's root CA installed,
422
+ since other projects may use them. It prints the exact commands to remove them for your OS
423
+ (`brew uninstall ...` on macOS; `winget uninstall ...` plus the `~.localctl ools` folder for
424
+ Tilt on Windows). Installed from
393
425
  source with the bootstrap script? Use `./bin/uninstall.sh` or `./bin/uninstall.ps1` instead.
394
426
  Details: [**Uninstalling**](https://psilvmoreira.github.io/localctl/uninstalling/).
395
427
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@localctl/cli",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "localctl - local Kubernetes dev platform CLI",
5
5
  "keywords": [
6
6
  "kubernetes",
@@ -91,7 +91,12 @@ async function uninstall(opts) {
91
91
  logger.success('Uninstall complete.');
92
92
  console.log('');
93
93
  console.log('Left in place (remove yourself if you want them gone too):');
94
- console.log(' brew uninstall k3d kubectl tilt mkcert node # only if nothing else needs them');
94
+ if (process.platform === 'win32') {
95
+ console.log(' winget uninstall k3d.k3d Kubernetes.kubectl FiloSottile.mkcert Helm.Helm # only if nothing else needs them');
96
+ console.log(' Remove-Item -Recurse -Force ~\\.localctl\\tools # tilt, if localctl installed it');
97
+ } else {
98
+ console.log(' brew uninstall k3d kubectl tilt mkcert helm # only if nothing else needs them');
99
+ }
95
100
  console.log(" mkcert -uninstall # removes the root CA from trust stores - affects EVERY");
96
101
  console.log(' # mkcert-issued cert on this machine, not just *.local.test');
97
102
  }
package/src/index.js CHANGED
@@ -2,6 +2,9 @@ const { Command } = require('commander');
2
2
  const { version } = require('../package.json');
3
3
 
4
4
  require('./lib/updateCheck').start(version);
5
+ // Tools localctl downloaded itself (e.g. tilt on Windows) work even in a terminal opened before
6
+ // they were added to the user's PATH.
7
+ require('./lib/toolInstall').addToolsDirToPath();
5
8
 
6
9
  const program = new Command();
7
10
  program
@@ -4,7 +4,7 @@ const path = require('path');
4
4
  const yaml = require('js-yaml');
5
5
  const { run, capture, sleepSync, commandExists } = require('./exec');
6
6
  const { detectEngine, warnIfExperimental } = require('./engine');
7
- const { installIfMissing } = require('./toolInstall');
7
+ const { installIfMissing, installTiltWindows } = require('./toolInstall');
8
8
  const { ensureInsecureRegistry } = require('./podman');
9
9
  const { DOMAIN } = require('./constants');
10
10
  const logger = require('./logger');
@@ -49,9 +49,9 @@ function provisionCluster({ clusterName, httpPort, httpsPort, registryPort }) {
49
49
  ensureInsecureRegistry(registryPort);
50
50
  }
51
51
 
52
- installIfMissing('k3d', 'k3d', 'k3d-io.k3d');
52
+ installIfMissing('k3d', 'k3d', 'k3d.k3d');
53
53
  installIfMissing('kubectl', 'kubernetes-cli', 'Kubernetes.kubectl');
54
- installIfMissing('tilt', 'tilt', 'Tilt.dev.tilt');
54
+ installIfMissing('tilt', 'tilt', installTiltWindows);
55
55
  installIfMissing('mkcert', 'mkcert', 'FiloSottile.mkcert');
56
56
  installIfMissing('helm', 'helm', 'Helm.Helm');
57
57
 
@@ -24,6 +24,20 @@ function listApps() {
24
24
  return Object.keys(registry);
25
25
  }
26
26
 
27
+ // The Windows counterpart of `sudo cp`: copies src over the hosts file from an elevated
28
+ // PowerShell, which raises a UAC prompt. Returns false if the prompt is declined or the copy
29
+ // fails. The inner command goes through -EncodedCommand so paths need no nested quoting.
30
+ function copyElevatedWindows(src) {
31
+ const quote = (s) => `'${s.replace(/'/g, "''")}'`;
32
+ const inner = `Copy-Item -LiteralPath ${quote(src)} -Destination ${quote(HOSTS_PATH)} -Force`;
33
+ const encoded = Buffer.from(inner, 'utf16le').toString('base64');
34
+ const res = spawnSync('powershell', [
35
+ '-NoProfile', '-NonInteractive', '-Command',
36
+ `Start-Process powershell -Verb RunAs -Wait -WindowStyle Hidden -ArgumentList '-NoProfile','-NonInteractive','-EncodedCommand','${encoded}'`,
37
+ ], { stdio: 'ignore' });
38
+ return res.status === 0;
39
+ }
40
+
27
41
  function writeHosts(next, current) {
28
42
  if (next === current) {
29
43
  logger.info('Hosts file already up to date.');
@@ -38,8 +52,13 @@ function writeHosts(next, current) {
38
52
  const tmp = path.join(os.tmpdir(), 'localctl-hosts-new');
39
53
  fs.writeFileSync(tmp, next);
40
54
  if (process.platform === 'win32') {
41
- logger.warn('Need Administrator rights to update the hosts file. Run in an elevated PowerShell:');
42
- console.log(` Copy-Item "${tmp}" "${HOSTS_PATH}" -Force`);
55
+ logger.warn('Need Administrator rights to update the hosts file. Approve the Windows prompt to continue.');
56
+ if (copyElevatedWindows(tmp) && fs.readFileSync(HOSTS_PATH, 'utf8') === next) {
57
+ logger.success(`Updated ${HOSTS_PATH}`);
58
+ } else {
59
+ logger.warn('Hosts file not updated, so *.local.test addresses won\'t resolve. Run in an elevated PowerShell:');
60
+ console.log(` Copy-Item "${tmp}" "${HOSTS_PATH}" -Force`);
61
+ }
43
62
  } else {
44
63
  logger.warn('Need sudo to update the hosts file.');
45
64
  const res = spawnSync('sudo', ['cp', tmp, HOSTS_PATH], { stdio: 'inherit' });
@@ -1,7 +1,72 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
1
3
  const { commandExists, run } = require('./exec');
2
4
  const logger = require('./logger');
5
+ const { HOME_STATE_DIR } = require('./paths');
3
6
 
4
- // macId: Homebrew formula name. winId: winget package id.
7
+ // Tools that have no package-manager package on a platform get downloaded here instead. The
8
+ // directory is added to the user's PATH for new terminals, and to this process's PATH by
9
+ // addToolsDirToPath() on every run, so the current terminal needs no restart.
10
+ const TOOLS_DIR = path.join(HOME_STATE_DIR, 'tools');
11
+
12
+ function prependToPath(dir) {
13
+ if (!fs.existsSync(dir)) return;
14
+ const entries = (process.env.PATH || '').split(path.delimiter);
15
+ if (!entries.includes(dir)) process.env.PATH = [dir, ...entries].join(path.delimiter);
16
+ }
17
+
18
+ function addToolsDirToPath() {
19
+ prependToPath(TOOLS_DIR);
20
+ }
21
+
22
+ // winget puts portable packages (k3d, kubectl, helm, mkcert) behind links in this directory and
23
+ // adds it to PATH for new terminals only. Adding it here lets the rest of `localctl setup` use a
24
+ // tool winget just installed.
25
+ function addWingetLinksToPath() {
26
+ if (process.env.LOCALAPPDATA) prependToPath(path.join(process.env.LOCALAPPDATA, 'Microsoft', 'WinGet', 'Links'));
27
+ }
28
+
29
+ function powershell(script) {
30
+ run('powershell', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', script]);
31
+ }
32
+
33
+ // Tilt isn't published to winget. Same steps as Tilt's own install.ps1 (latest release zip from
34
+ // GitHub), but into TOOLS_DIR and with the directory added to the user's PATH, which install.ps1
35
+ // leaves to you.
36
+ function installTiltWindows() {
37
+ logger.step('Installing tilt from its GitHub release (not available on winget)...');
38
+ powershell(`
39
+ $ErrorActionPreference = 'Stop'
40
+ $ProgressPreference = 'SilentlyContinue'
41
+ $dir = '${TOOLS_DIR.replace(/'/g, "''")}'
42
+ $version = (Invoke-RestMethod 'https://api.github.com/repos/tilt-dev/tilt/releases/latest').tag_name.TrimStart('v')
43
+ $url = "https://github.com/tilt-dev/tilt/releases/download/v$version/tilt.$version.windows.x86_64.zip"
44
+ $tmp = Join-Path $env:TEMP "localctl-tilt-$version"
45
+ if (Test-Path $tmp) { Remove-Item -Recurse -Force $tmp }
46
+ New-Item -ItemType Directory -Force -Path $tmp, $dir | Out-Null
47
+ Write-Host "Downloading $url"
48
+ Invoke-WebRequest $url -OutFile "$tmp\\tilt.zip"
49
+ Expand-Archive "$tmp\\tilt.zip" -DestinationPath $tmp
50
+ Move-Item -Force "$tmp\\tilt.exe" "$dir\\tilt.exe"
51
+ Remove-Item -Recurse -Force $tmp
52
+ # Raw registry value, so entries like %USERPROFILE%\\bin stay unexpanded (REG_EXPAND_SZ).
53
+ $key = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $true)
54
+ $userPath = [string]$key.GetValue('Path', '', 'DoNotExpandEnvironmentNames')
55
+ if ((($userPath -split ';') | Where-Object { $_ -eq $dir }).Count -eq 0) {
56
+ $key.SetValue('Path', (($userPath.TrimEnd(';'), $dir) -join ';').TrimStart(';'), 'ExpandString')
57
+ # Setting any user variable through .NET broadcasts WM_SETTINGCHANGE, so terminals opened
58
+ # from now on pick up the new PATH without signing out.
59
+ [Environment]::SetEnvironmentVariable('LOCALCTL_PATH_REFRESH', '1', 'User')
60
+ [Environment]::SetEnvironmentVariable('LOCALCTL_PATH_REFRESH', $null, 'User')
61
+ }
62
+ $key.Close()
63
+ `);
64
+ addToolsDirToPath();
65
+ logger.success(`tilt installed in ${TOOLS_DIR} (added to your PATH for new terminals)`);
66
+ }
67
+
68
+ // macId: Homebrew formula name. winId: winget package id, or a function that installs the tool
69
+ // when winget doesn't have it.
5
70
  function installIfMissing(bin, macId, winId) {
6
71
  if (commandExists(bin)) {
7
72
  logger.success(`${bin} already installed`);
@@ -14,6 +79,10 @@ function installIfMissing(bin, macId, winId) {
14
79
  logger.step(`Installing ${macId} via brew...`);
15
80
  run('brew', ['install', macId]);
16
81
  } else if (process.platform === 'win32') {
82
+ if (typeof winId === 'function') {
83
+ winId();
84
+ return;
85
+ }
17
86
  if (!commandExists('winget')) {
18
87
  throw new Error('winget is required to install missing tools (ships with modern Windows / App Installer).');
19
88
  }
@@ -22,9 +91,10 @@ function installIfMissing(bin, macId, winId) {
22
91
  'install', '--id', winId, '-e', '--source', 'winget',
23
92
  '--accept-package-agreements', '--accept-source-agreements',
24
93
  ]);
94
+ addWingetLinksToPath();
25
95
  } else {
26
96
  throw new Error(`"${bin}" is missing and automatic install isn't supported on this platform. Install it manually.`);
27
97
  }
28
98
  }
29
99
 
30
- module.exports = { installIfMissing };
100
+ module.exports = { installIfMissing, installTiltWindows, addToolsDirToPath, TOOLS_DIR };