@webority/ensemble 0.5.11 → 0.5.12

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 (2) hide show
  1. package/lib/core.js +64 -15
  2. package/package.json +6 -6
package/lib/core.js CHANGED
@@ -14,8 +14,9 @@ const DEFAULT_API = process.env.ENSEMBLE_API || 'https://api.ensemble.host';
14
14
  // Public host for the prebuilt binaries (runner + bus). Branded CDN in front of the blob
15
15
  // (Cloudflare Worker → ensembledl blob; the raw blob URL still works as a fallback). Overridable.
16
16
  const DL_BASE = process.env.ENSEMBLE_DL_BASE || 'https://storage.ensemble.host/cli';
17
- // RIDs we currently publish a prebuilt runner + bus for on DL_BASE. Expand as more are uploaded
18
- // (osx-x64 for Intel Macs, linux-arm64, win-arm64). Keep in sync with what the build pipeline ships.
17
+ // RIDs we publish a prebuilt runner + runtime for (npm platform packages + the DL_BASE fallback).
18
+ // Keep in sync with the build pipeline (publish-cli.yml). win-arm64 is not built yet ARM Windows
19
+ // runs the win-x64 build under emulation, since platform() maps every win32 host to win-x64.
19
20
  const SUPPORTED_PLATFORMS = ['win-x64', 'osx-arm64', 'osx-x64', 'linux-x64', 'linux-arm64'];
20
21
 
21
22
  function log(msg) { process.stdout.write(msg + '\n'); }
@@ -171,8 +172,13 @@ function restartRunner(pf, runnerPath) {
171
172
  if (pf.os === 'windows') {
172
173
  spawnSync('taskkill', ['/IM', 'Ensemble.Runner.exe', '/F'], { stdio: 'ignore' });
173
174
  startRunner(runnerPath);
175
+ } else if (pf.os === 'mac') {
176
+ installAutostart(runnerPath); // launchctl unload+load kills + respawns onto the new binary
174
177
  } else {
175
- installAutostart(runnerPath); // unload+load (launchd) / enable --now (systemd) picks up new binary
178
+ // systemd: `enable --now` is a no-op on an already-running unit, so the old process keeps its
179
+ // old inode after the binary is swapped — force an explicit restart onto the new build.
180
+ installAutostart(runnerPath);
181
+ spawnSync('systemctl', ['--user', 'restart', 'ensemble-runner.service'], { stdio: 'ignore' });
176
182
  }
177
183
  } catch (e) { /* best-effort — the new binary is on disk and runs on next launch */ }
178
184
  }
@@ -317,9 +323,24 @@ async function ensureBinaries(opts) {
317
323
  if (pf.os !== 'windows') {
318
324
  fs.chmodSync(t.dest, 0o755);
319
325
  // macOS requires at least an ad-hoc signature to run an unsigned binary.
320
- if (pf.os === 'mac') spawnSync('codesign', ['-s', '-', '-f', t.dest], { stdio: 'ignore' });
326
+ if (pf.os === 'mac') {
327
+ const sign = spawnSync('codesign', ['-s', '-', '-f', t.dest], { stdio: 'ignore' });
328
+ if (sign.status !== 0) {
329
+ warn('codesign failed for ' + path.basename(t.dest) + ' — macOS may refuse to run it (' +
330
+ (sign.error ? sign.error.message : 'exit ' + sign.status) + '). Install Xcode Command Line Tools.');
331
+ }
332
+ }
321
333
  }
322
334
  }
335
+ // A ≥1MB size check (isUsableBinary) does NOT prove the runtime matches this CPU — a wrong-arch copy
336
+ // passes it and then every hook silently dies with "bad CPU type". Prove it actually executes before
337
+ // we trust it, so a bad download / wrong platform package / stale CDN blob fails the install loudly.
338
+ const smoke = spawnSync(runtimePath, ['--help'], { stdio: 'ignore', timeout: 5000 });
339
+ if (smoke.error || smoke.status == null) {
340
+ throw new Error('the Ensemble runtime at ' + runtimePath + ' does not run on this machine (' + pf.key +
341
+ ') — likely a wrong-arch or corrupt binary' + (smoke.error ? ': ' + smoke.error.message : '') +
342
+ '. Delete ~/.ensemble/bin and re-run `ensemble install`.');
343
+ }
323
344
  removeLegacyBusArtifacts();
324
345
  writeInstalledVersion();
325
346
  // A replaced runner binary only takes effect once the daemon restarts onto it.
@@ -346,9 +367,16 @@ function writeRunnerConfig(api, machineToken) {
346
367
  Logging: { LogLevel: { Default: 'Information', 'Microsoft.Hosting.Lifetime': 'Information' } },
347
368
  Ensemble: { HubUrl: hubUrl, MachineToken: machineToken },
348
369
  };
349
- fs.writeFileSync(path.join(RUNNER_DIR, 'appsettings.json'), JSON.stringify(cfg, null, 2));
370
+ // appsettings.json embeds the plaintext machine token (cfg.Ensemble.MachineToken) — write it 0600,
371
+ // same as the dotfile below; a default-umask write left it world-readable on shared machines.
372
+ const appsettingsPath = path.join(RUNNER_DIR, 'appsettings.json');
373
+ fs.writeFileSync(appsettingsPath, JSON.stringify(cfg, null, 2), { mode: 0o600 });
350
374
  // ensemble-runtime reads ENSEMBLE_MACHINE_TOKEN from env — persist it to the shell rc + a dotfile.
351
- fs.writeFileSync(path.join(ENSEMBLE_DIR, 'machine-token'), machineToken, { mode: 0o600 });
375
+ const tokenPath = path.join(ENSEMBLE_DIR, 'machine-token');
376
+ fs.writeFileSync(tokenPath, machineToken, { mode: 0o600 });
377
+ // `mode` only takes effect when the file is newly created — re-tighten on every enroll so a file an
378
+ // older build left at 0644 (or a manual edit widened) can't keep leaking the token.
379
+ try { fs.chmodSync(appsettingsPath, 0o600); fs.chmodSync(tokenPath, 0o600); } catch (e) { /* best-effort (Windows) */ }
352
380
  const pf = platform();
353
381
  if (pf.os === 'windows') {
354
382
  spawnSync('setx', ['ENSEMBLE_MACHINE_TOKEN', machineToken], { stdio: 'ignore' });
@@ -356,7 +384,7 @@ function writeRunnerConfig(api, machineToken) {
356
384
  const rc = path.join(HOME, pf.os === 'mac' ? '.zshrc' : '.bashrc');
357
385
  try {
358
386
  let cur = fs.existsSync(rc) ? fs.readFileSync(rc, 'utf8') : '';
359
- cur = cur.replace(/\nexport ENSEMBLE_MACHINE_TOKEN=.*\n?/g, '\n');
387
+ cur = cur.replace(/(^|\n)export ENSEMBLE_MACHINE_TOKEN=.*\n?/g, '\n');
360
388
  if (!cur.endsWith('\n')) cur += '\n';
361
389
  cur += `export ENSEMBLE_MACHINE_TOKEN="${machineToken}"\n`;
362
390
  if (!/\.ensemble\/bin/.test(cur)) cur += `export PATH="$HOME/.ensemble/bin:$PATH"\n`;
@@ -739,7 +767,8 @@ function isEnsembleHook(h) {
739
767
  // our entry so re-runs don't stack, and migrate away the legacy lowercase keys. [BUG-06673-hooks]
740
768
  // Returns true when written, false when skipped (malformed JSON left untouched).
741
769
  function writeEngineHooks(file, runtimeInvoke, engine, windowsRuntime) {
742
- fs.mkdirSync(path.dirname(file), { recursive: true });
770
+ try { fs.mkdirSync(path.dirname(file), { recursive: true }); }
771
+ catch (e) { warn('could not create ' + engine + ' hooks dir for ' + file + ': ' + (e.message || e)); return false; }
743
772
  let existing = {};
744
773
  if (fs.existsSync(file)) {
745
774
  try { existing = JSON.parse(fs.readFileSync(file, 'utf8')); }
@@ -774,13 +803,19 @@ function writeEngineHooks(file, runtimeInvoke, engine, windowsRuntime) {
774
803
  hooks[evt] = kept;
775
804
  }
776
805
  existing.hooks = hooks;
777
- fs.writeFileSync(file, JSON.stringify(existing, null, 2) + '\n');
778
- return true;
806
+ try {
807
+ fs.writeFileSync(file, JSON.stringify(existing, null, 2) + '\n');
808
+ return true;
809
+ } catch (e) {
810
+ warn('could not write ' + engine + ' hooks config ' + file + ': ' + (e.message || e));
811
+ return false;
812
+ }
779
813
  }
780
814
 
781
815
  // Returns true when the settings were written, false when skipped (malformed JSON left untouched).
782
816
  function mergeClaudeHooks(file, runtimeInvoke) {
783
- fs.mkdirSync(path.dirname(file), { recursive: true });
817
+ try { fs.mkdirSync(path.dirname(file), { recursive: true }); }
818
+ catch (e) { warn('could not create Claude settings dir for ' + file + ': ' + (e.message || e)); return false; }
784
819
  let s = {};
785
820
  if (fs.existsSync(file)) {
786
821
  try { s = JSON.parse(fs.readFileSync(file, 'utf8')); }
@@ -800,8 +835,13 @@ function mergeClaudeHooks(file, runtimeInvoke) {
800
835
  kept.push({ hooks: [{ type: 'command', command }] });
801
836
  s.hooks[evt] = kept;
802
837
  }
803
- fs.writeFileSync(file, JSON.stringify(s, null, 2) + '\n');
804
- return true;
838
+ try {
839
+ fs.writeFileSync(file, JSON.stringify(s, null, 2) + '\n');
840
+ return true;
841
+ } catch (e) {
842
+ warn('could not write Claude settings ' + file + ': ' + (e.message || e));
843
+ return false;
844
+ }
805
845
  }
806
846
 
807
847
  // --- per-user auto-start of the runner (per OS), plus start it now ---
@@ -830,7 +870,11 @@ function installAutostart(runnerPath) {
830
870
  <key>StandardErrorPath</key><string>${path.join(RUNNER_DIR, 'runner.log')}</string>
831
871
  </dict></plist>`);
832
872
  spawnSync('launchctl', ['unload', plist], { stdio: 'ignore' });
833
- spawnSync('launchctl', ['load', plist], { stdio: 'ignore' });
873
+ const load = spawnSync('launchctl', ['load', plist], { stdio: 'ignore' });
874
+ if (load.error || load.status !== 0) {
875
+ warn('could not load the launchd runner agent (' +
876
+ (load.error ? load.error.message : 'exit ' + load.status) + ') — autostart may not be active.');
877
+ }
834
878
  } else {
835
879
  const unitDir = path.join(HOME, '.config', 'systemd', 'user');
836
880
  fs.mkdirSync(unitDir, { recursive: true });
@@ -846,7 +890,12 @@ RestartSec=5
846
890
  [Install]
847
891
  WantedBy=default.target`);
848
892
  spawnSync('systemctl', ['--user', 'daemon-reload'], { stdio: 'ignore' });
849
- spawnSync('systemctl', ['--user', 'enable', '--now', 'ensemble-runner.service'], { stdio: 'ignore' });
893
+ const enable = spawnSync('systemctl', ['--user', 'enable', '--now', 'ensemble-runner.service'], { stdio: 'ignore' });
894
+ if (enable.error || enable.status !== 0) {
895
+ warn('could not enable the systemd --user runner service (' +
896
+ (enable.error ? enable.error.message : 'exit ' + enable.status) +
897
+ ') — no systemd --user session? Autostart may not be active.');
898
+ }
850
899
  spawnSync('loginctl', ['enable-linger', os.userInfo().username], { stdio: 'ignore' });
851
900
  }
852
901
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webority/ensemble",
3
- "version": "0.5.11",
3
+ "version": "0.5.12",
4
4
  "description": "Connect this machine to Ensemble — runs the local agent runner and wires the ensemble session bus so your coding sessions talk (per-org, isolated).",
5
5
  "bin": {
6
6
  "ensemble": "bin/ensemble.js"
@@ -12,11 +12,11 @@
12
12
  "node": ">=18"
13
13
  },
14
14
  "optionalDependencies": {
15
- "@webority/ensemble-darwin-arm64": "0.5.11",
16
- "@webority/ensemble-darwin-x64": "0.5.11",
17
- "@webority/ensemble-linux-arm64": "0.5.11",
18
- "@webority/ensemble-linux-x64": "0.5.11",
19
- "@webority/ensemble-win-x64": "0.5.11"
15
+ "@webority/ensemble-darwin-arm64": "0.5.12",
16
+ "@webority/ensemble-darwin-x64": "0.5.12",
17
+ "@webority/ensemble-linux-arm64": "0.5.12",
18
+ "@webority/ensemble-linux-x64": "0.5.12",
19
+ "@webority/ensemble-win-x64": "0.5.12"
20
20
  },
21
21
  "files": [
22
22
  "bin",