@webority/ensemble 0.5.0 → 0.5.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/bin/ensemble.js CHANGED
@@ -26,41 +26,54 @@ function reportSetup(wired) {
26
26
  }
27
27
 
28
28
  // Browser sign-in (device flow) — no token to copy. The recommended path.
29
+ // Order matters: AUTHENTICATE FIRST (browser approval), and only download the runtime once the
30
+ // machine is approved — so a cancelled or denied login never pulls ~90 MB for nothing.
29
31
  async function login(args) {
30
32
  const api = args.api || core.DEFAULT_API;
31
33
  const name = args.name || os.hostname();
32
- core.log('Ensemble — signing in "' + name + '" via ' + api);
33
- core.log(' fetching runner + bus binaries');
34
- const { runnerPath } = await core.ensureBinaries();
34
+ core.assertSupported();
35
+ core.log('Ensemble sign in for "' + name + '" via ' + api);
35
36
  const grant = await core.startDevice(api, name);
36
- core.log('\n Approve this machine in your browser:\n ' + grant.verificationUriComplete +
37
- '\n Confirmation code: ' + grant.userCode + '\n');
37
+ core.log('\n 1) Approve this machine in your browser:\n ' + grant.verificationUriComplete +
38
+ '\n Confirmation code: ' + grant.userCode + '\n');
38
39
  core.openBrowser(grant.verificationUriComplete);
39
- core.log('• waiting for approval …');
40
+ core.log('• waiting for your approval …');
41
+ const token = await waitForApproval(api, grant);
42
+ core.log('\n✓ Approved. Connecting this machine …');
43
+ const { runnerPath } = core.requireRuntime();
44
+ reportSetup(core.finishSetup(api, token, runnerPath));
45
+ }
46
+
47
+ async function waitForApproval(api, grant) {
40
48
  const deadline = Date.now() + grant.expiresIn * 1000;
41
- let token = null;
42
49
  while (Date.now() < deadline) {
43
50
  await core.sleep((grant.interval || 5) * 1000);
44
51
  const r = await core.pollDevice(api, grant.deviceCode);
45
- if (r.status === 'approved') { token = r.token; break; }
46
- if (r.status === 'denied') core.die('approval was denied in the browser.');
52
+ if (r.status === 'approved') return r.token;
53
+ if (r.status === 'denied') core.die('approval was denied in the browser — nothing was installed.');
47
54
  if (r.status === 'expired') core.die('the login request expired — run `ensemble login` again.');
48
55
  }
49
- if (!token) core.die('timed out waiting for approval — run `ensemble login` again.');
50
- reportSetup(core.finishSetup(api, token, runnerPath));
56
+ core.die('timed out waiting for approval — nothing was installed. Run `ensemble login` again.');
57
+ }
58
+
59
+ // Explicit (re)install of the runner + bus. Normally runs automatically at `npm install`.
60
+ async function install() {
61
+ core.assertSupported();
62
+ await core.ensureBinaries();
63
+ core.log('✓ Ensemble runtime installed. Next: ensemble login');
51
64
  }
52
65
 
53
- // Headless / CI path: enrol with a copy-pasted enrollment token.
66
+ // Headless / CI path: enrol with a copy-pasted enrollment token. Auth only — the runtime is
67
+ // installed at `npm install` (or `ensemble install`).
54
68
  async function enroll(args) {
55
69
  const token = args.token || process.env.ENSEMBLE_ENROLL_TOKEN;
56
70
  if (!token) core.die('missing --token <enrollment-token> — get it from your Ensemble portal → Connect Runner, or use `ensemble login` for browser sign-in');
57
71
  const api = args.api || core.DEFAULT_API;
58
72
  const name = args.name || os.hostname();
59
73
  core.log('Ensemble — enrolling "' + name + '" with ' + api);
60
- core.log('• fetching runner + bus binaries');
61
- const { runnerPath } = await core.ensureBinaries();
62
- core.log('• exchanging enrollment token for a per-machine token');
74
+ core.log('• exchanging the enrollment token for a per-machine token');
63
75
  const machineToken = await core.enrollMachine(api, token, name);
76
+ const { runnerPath } = core.requireRuntime();
64
77
  reportSetup(core.finishSetup(api, machineToken, runnerPath));
65
78
  }
66
79
 
@@ -84,9 +97,10 @@ function usage() {
84
97
  `ensemble — connect this machine to your Ensemble control plane
85
98
 
86
99
  ensemble login [--api <url>] [--name <machine>]
87
- sign in via your browser (recommended) approve the machine, no token to copy
100
+ sign in via your browser + connect this machine (runtime is installed at npm install)
88
101
  ensemble enroll --token <enrollment-token> [--api <url>] [--name <machine>]
89
102
  headless / CI: enrol with an enrollment token from the portal
103
+ ensemble install (re)download the runner + bus for this platform
90
104
  ensemble status show connection + your org's live session mailboxes
91
105
  ensemble hooks (re)wire ensemble-bus into installed coding CLIs
92
106
  ensemble version
@@ -101,8 +115,9 @@ Run \`ensemble login\` and approve the machine in your browser.`);
101
115
  try {
102
116
  if (cmd === 'login') await login(args);
103
117
  else if (cmd === 'enroll') await enroll(args);
118
+ else if (cmd === 'install') await install();
104
119
  else if (cmd === 'status') await status(args);
105
- else if (cmd === 'hooks') { await core.ensureBinaries(); core.log('wired: ' + (core.wireHooks().join(', ') || 'none')); }
120
+ else if (cmd === 'hooks') { core.log('wired: ' + (core.wireHooks().join(', ') || 'none')); }
106
121
  else if (cmd === 'version' || cmd === '--version' || cmd === '-v') core.log(require('../package.json').version);
107
122
  else usage();
108
123
  } catch (e) {
package/lib/core.js CHANGED
@@ -11,8 +11,12 @@ const ENSEMBLE_DIR = path.join(HOME, '.ensemble');
11
11
  const BIN_DIR = path.join(ENSEMBLE_DIR, 'bin');
12
12
  const RUNNER_DIR = path.join(ENSEMBLE_DIR, 'runner');
13
13
  const DEFAULT_API = process.env.ENSEMBLE_API || 'https://api.ensemble.host';
14
- // Public host for the prebuilt binaries (runner + bus). Overridable for testing.
15
- const DL_BASE = process.env.ENSEMBLE_DL_BASE || 'https://ensembledl.blob.core.windows.net/cli';
14
+ // Public host for the prebuilt binaries (runner + bus). Branded CDN in front of the blob
15
+ // (Cloudflare Worker ensembledl blob; the raw blob URL still works as a fallback). Overridable.
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.
19
+ const SUPPORTED_PLATFORMS = ['win-x64', 'osx-arm64', 'osx-x64', 'linux-x64', 'linux-arm64'];
16
20
 
17
21
  function log(msg) { process.stdout.write(msg + '\n'); }
18
22
  function warn(msg) { process.stderr.write(msg + '\n'); }
@@ -64,9 +68,43 @@ function downloadTo(url, dest) {
64
68
  });
65
69
  }
66
70
 
71
+ // Throws (not die) so the npm postinstall can catch and not fail the whole install on an
72
+ // unsupported OS; the CLI dispatcher catches the throw and prints it cleanly.
73
+ function assertSupported() {
74
+ const pf = platform();
75
+ if (!SUPPORTED_PLATFORMS.includes(pf.key)) {
76
+ throw new Error(`no prebuilt Ensemble runtime for "${pf.key}" (${process.platform}/${process.arch}) — ` +
77
+ `supported: ${SUPPORTED_PLATFORMS.join(', ')}. Ask us to add ${pf.key}, or build from source.`);
78
+ }
79
+ }
80
+
81
+ function runtimePaths() {
82
+ const pf = platform();
83
+ return {
84
+ pf,
85
+ busPath: path.join(BIN_DIR, 'ensemble-bus' + pf.exe),
86
+ runnerPath: path.join(RUNNER_DIR, 'Ensemble.Runner' + pf.exe),
87
+ };
88
+ }
89
+
90
+ // The runner + bus are downloaded at `npm install` (postinstall) or `ensemble install` — NOT at login.
91
+ function runtimeInstalled() {
92
+ const p = runtimePaths();
93
+ const ok = (f) => fs.existsSync(f) && fs.statSync(f).size > 0;
94
+ return ok(p.busPath) && ok(p.runnerPath);
95
+ }
96
+
97
+ function requireRuntime() {
98
+ if (!runtimeInstalled()) {
99
+ die('the Ensemble runtime is not installed — run `ensemble install` (or reinstall @webority/ensemble).');
100
+ }
101
+ return runtimePaths();
102
+ }
103
+
67
104
  async function ensureBinaries() {
68
105
  ensureDirs();
69
106
  const pf = platform();
107
+ assertSupported();
70
108
  const busPath = path.join(BIN_DIR, 'ensemble-bus' + pf.exe);
71
109
  const runnerPath = path.join(RUNNER_DIR, 'Ensemble.Runner' + pf.exe);
72
110
  const targets = [
@@ -276,5 +314,6 @@ function finishSetup(api, machineToken, runnerPath) {
276
314
  module.exports = {
277
315
  log, warn, die, platform, DEFAULT_API, ENSEMBLE_DIR, RUNNER_DIR, BIN_DIR,
278
316
  ensureBinaries, enrollMachine, writeRunnerConfig, wireHooks, installAutostart, startRunner, request,
279
- sleep, openBrowser, startDevice, pollDevice, finishSetup,
317
+ sleep, openBrowser, startDevice, pollDevice, finishSetup, assertSupported,
318
+ runtimePaths, runtimeInstalled, requireRuntime,
280
319
  };
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+ // Runs at `npm install` (postinstall). Downloads the runner + bus for this platform so that
3
+ // `ensemble login` is auth-only. Resilient by design: any failure (offline, unsupported OS,
4
+ // --ignore-scripts) warns and exits 0 so it never breaks `npm install` — `ensemble install`
5
+ // (or the next install) can retry.
6
+ const core = require('./core');
7
+
8
+ (async () => {
9
+ try {
10
+ if (core.runtimeInstalled()) {
11
+ return; // already present (e.g. reinstall of the same version)
12
+ }
13
+ core.assertSupported();
14
+ core.log('Ensemble: installing the runner + bus for this machine …');
15
+ await core.ensureBinaries();
16
+ core.log('Ensemble: runtime installed. Next: ensemble login');
17
+ } catch (e) {
18
+ core.warn('Ensemble: could not install the runtime now (' + (e && e.message ? e.message : e) + ').');
19
+ core.warn(' Run ensemble install when ready, then ensemble login.');
20
+ // exit 0 — never fail `npm install`.
21
+ }
22
+ })();
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@webority/ensemble",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Connect this machine to your Ensemble control plane — runs the local agent runner and wires the ensemble-bus session bus so your coding sessions talk (per-org, isolated).",
5
5
  "bin": {
6
6
  "ensemble": "bin/ensemble.js"
7
7
  },
8
+ "scripts": {
9
+ "postinstall": "node lib/postinstall.js"
10
+ },
8
11
  "engines": {
9
12
  "node": ">=18"
10
13
  },