szpont 0.90.0 → 0.91.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/README.md CHANGED
@@ -26,7 +26,7 @@ is already in the repository:
26
26
  | get it | `git clone` into `~/.diplomat/checkout`, or fast-forward it | same |
27
27
  | build it | `install/build-app.sh` → `Diplomat.app` | `install/build-core.sh` when the prompt binary is missing |
28
28
  | set it up | - | a venv at `~/.diplomat/venv` with `requirements.txt` in it |
29
- | start it | `open Diplomat.app` | the package's own `./diplomat`, on the venv's interpreter |
29
+ | start it | `open -n Diplomat.app` | the package's own `./diplomat`, on the venv's interpreter |
30
30
 
31
31
  Nothing is rebuilt that does not need to be: a venv is left alone until
32
32
  `requirements.txt` changes, and the Swift prompt binary is only built when the
@@ -49,8 +49,8 @@ may have work in it. `DIPLOMAT_REPO_URL` clones from a fork.
49
49
  [`pip install szpont`](https://github.com/latekvo/Diplomat/tree/main/packages/szpont)
50
50
  installs the same launcher and nothing else. The two implementations are separate
51
51
  files in separate languages, so what keeps them one command is
52
- `test/parity-with-python.mjs`: every machine shape in `test/scenarios.mjs` goes
53
- through both planners, and the two answers have to be identical.
52
+ `test/parity-with-python.mjs`: every machine in `test/scenarios.mjs` is probed by
53
+ both and every fact set planned by both, and the two answers have to be identical.
54
54
 
55
55
  ## Its own tests
56
56
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "szpont",
3
- "version": "0.90.0",
3
+ "version": "0.91.0",
4
4
  "type": "module",
5
5
  "description": "Install-and-run for Diplomat (Szpont Yon): fetches the checkout, builds it, and starts the applet.",
6
6
  "keywords": ["diplomat", "szpont", "szpontnet", "launcher", "menu-bar", "tray"],
package/src/launcher.js CHANGED
@@ -1,10 +1,11 @@
1
1
  // The npm twin of `packages/szpont/szpont_launcher.py`, line for line where it
2
2
  // matters: `probe()` reads the machine, `plan()` turns those facts into the steps
3
3
  // that will run, and `run()` runs them. Splitting it that way is what lets
4
- // `test/parity-with-python.mjs` hand both implementations one set of synthetic
5
- // facts and demand the same plan back — the two are published under the same name
6
- // to the two indexes, and a `szpont` that meant different things depending on
7
- // which one you installed from would be the whole failure mode.
4
+ // `test/parity-with-python.mjs` hand both implementations one machine on disk and
5
+ // demand the same facts back, then one set of facts and demand the same plan —
6
+ // the two are published under the same name to the two indexes, and a `szpont`
7
+ // that meant different things depending on which one you installed from would be
8
+ // the whole failure mode.
8
9
  //
9
10
  // Why a bootstrapper at all, rather than a package holding Diplomat: see the
10
11
  // Python module's header. Short version — Diplomat is built out of the checkout it
@@ -16,7 +17,7 @@ import path from 'node:path';
16
17
  import { createHash } from 'node:crypto';
17
18
  import { spawnSync } from 'node:child_process';
18
19
 
19
- export const VERSION = '0.90.0';
20
+ export const VERSION = '0.91.0';
20
21
  export const DEFAULT_REPO_URL = 'https://github.com/latekvo/Diplomat.git';
21
22
  export const SUPPORTED = ['darwin', 'linux'];
22
23
  export const MIN_PYTHON = [3, 10];
@@ -25,16 +26,48 @@ export const MIN_PYTHON = [3, 10];
25
26
  // already owns, so uninstalling is one `rm -rf ~/.diplomat` rather than a hunt.
26
27
  const STATE_DIR = '.diplomat';
27
28
 
29
+ // `getconf PATH`, where shutil.which looks when neither the machine nor the
30
+ // process has a PATH (glibc and musl agree on the Linux value). Bound once: the
31
+ // libc's answer does not move with the platform probe() is told it is on.
32
+ const DEFAULT_PATH = process.platform === 'darwin' ? '/usr/bin:/bin:/usr/sbin:/sbin' : '/bin:/usr/bin';
33
+
34
+ // The executable's path, or null - `shutil.which` in the Python twin: anything
35
+ // executable that is not a directory, and an empty PATH entry is the working
36
+ // directory, as `sh` reads it.
28
37
  function which(name, env) {
29
- const dirs = (env.PATH || '').split(path.delimiter).filter(Boolean);
30
- return dirs.some((dir) => {
38
+ const searchPath = env.PATH ?? process.env.PATH ?? DEFAULT_PATH;
39
+ if (!searchPath) return null;
40
+ for (const dir of searchPath.split(path.delimiter)) {
41
+ const candidate = path.join(dir, name);
31
42
  try {
32
- fs.accessSync(path.join(dir, name), fs.constants.X_OK);
33
- return true;
43
+ fs.accessSync(candidate, fs.constants.X_OK);
44
+ if (!fs.statSync(candidate).isDirectory()) return candidate;
34
45
  } catch {
35
- return false;
46
+ // not here
36
47
  }
37
- });
48
+ }
49
+ return null;
50
+ }
51
+
52
+ // Whether `name` is on PATH and would run. Every Mac has a /usr/bin/git and a
53
+ // /usr/bin/swift: xcrun shims that run the Command Line Tools when those are
54
+ // installed and pop the graphical installer when they are not. Found there, a tool
55
+ // is only as present as the toolchain, which `xcode-select -p` reports without
56
+ // going through a shim.
57
+ function tool(name, env, platform) {
58
+ const found = which(name, env);
59
+ if (!found) return false;
60
+ // realpath: a symlink to it, or a directory symlinked onto /usr/bin - what runs
61
+ // is still the shim.
62
+ if (platform === 'darwin' && fs.realpathSync(found) === `/usr/bin/${name}`) return toolchainPresent(env);
63
+ return true;
64
+ }
65
+
66
+ function toolchainPresent(env) {
67
+ const xcodeSelect = which('xcode-select', env);
68
+ if (!xcodeSelect) return false;
69
+ const r = spawnSync(xcodeSelect, ['-p'], { encoding: 'utf8', timeout: 30_000 });
70
+ return r.status === 0 && isDir((r.stdout || '').trim());
38
71
  }
39
72
 
40
73
  // `python3 --version`, or null when there is no python3 to ask. Only ever called
@@ -113,13 +146,16 @@ export function probe(appArgs = [], { update = true, env = process.env } = {}) {
113
146
  managed: !explicit,
114
147
  repo_url: env.DIPLOMAT_REPO_URL || DEFAULT_REPO_URL,
115
148
  update: Boolean(update),
116
- git: which('git', env),
117
- swift: which('swift', env),
149
+ git: tool('git', env, platform),
150
+ swift: tool('swift', env, platform),
118
151
  // Linux-only questions, and asked only there — see python3Version.
119
152
  python3: platform === 'linux' ? python3Version(env) : null,
120
153
  core_bin: platform === 'linux' ? coreBinPresent(home, env) : null,
121
154
  venv,
122
- venv_python: fs.existsSync(path.join(venv, 'bin', 'python')),
155
+ // Both, because a venv is only useful with something to install into it:
156
+ // Debian without python3-venv leaves bin/python behind and stops before pip,
157
+ // and running `venv` again over that directory is what completes it.
158
+ venv_python: fs.existsSync(path.join(venv, 'bin', 'python')) && fs.existsSync(path.join(venv, 'bin', 'pip')),
123
159
  venv_current: digest !== null && readText(path.join(venv, '.szpont-requirements')) === digest,
124
160
  args: [...appArgs],
125
161
  };
@@ -225,8 +261,10 @@ export function plan(facts) {
225
261
  steps.push(step('build', [path.join(macos, 'install', 'build-app.sh')], { cwd: macos, needs: 'swift' }));
226
262
  // build-app.sh rebuilds unconditionally, so the bundle always matches the
227
263
  // source it was just cloned or pulled from; `open` then detaches it from this
228
- // terminal, which is where a menu-bar app belongs.
229
- const launch = ['open', path.join(macos, 'Diplomat.app')];
264
+ // terminal, which is where a menu-bar app belongs. `-n` starts that bundle
265
+ // even while an instance is up, and the app's newest-wins singleton retires
266
+ // the old one - the swap its own updater relies on.
267
+ const launch = ['open', '-n', path.join(macos, 'Diplomat.app')];
230
268
  if (args.length) launch.push('--args', ...args);
231
269
  steps.push(step('launch', launch, { cwd: macos }));
232
270
  } else if (facts.platform === 'linux') {
@@ -290,7 +328,7 @@ export function run(steps, { checkout, venv }) {
290
328
  try {
291
329
  fs.writeFileSync(path.join(venv, '.szpont-requirements'), digest || '');
292
330
  } catch {
293
- // a re-install next time is the whole cost of not recording it
331
+ // re-installed next time; linux/diplomat passes the venv over until then
294
332
  }
295
333
  }
296
334
  }