@webority/ensemble 0.2.3 → 0.3.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/bin/ensemble.js CHANGED
@@ -18,9 +18,42 @@ function parseArgs(argv) {
18
18
  return args;
19
19
  }
20
20
 
21
+ function reportSetup(wired) {
22
+ core.log('• wired session hooks: ' + (wired.join(', ') || '(no supported coding CLI found — run `ensemble hooks` after installing claude/codex/grok)'));
23
+ core.log('• installed auto-start + launched the runner');
24
+ core.log('\n✓ Done. This machine runs your Ensemble runner and its coding sessions now share the bus (scoped to your org).');
25
+ core.log(' Open a NEW terminal (so ENSEMBLE_MACHINE_TOKEN loads), then start Claude/Codex/Grok as usual.');
26
+ }
27
+
28
+ // Browser sign-in (device flow) — no token to copy. The recommended path.
29
+ async function login(args) {
30
+ const api = args.api || core.DEFAULT_API;
31
+ 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();
35
+ 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');
38
+ core.openBrowser(grant.verificationUriComplete);
39
+ core.log('• waiting for approval …');
40
+ const deadline = Date.now() + grant.expiresIn * 1000;
41
+ let token = null;
42
+ while (Date.now() < deadline) {
43
+ await core.sleep((grant.interval || 5) * 1000);
44
+ 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.');
47
+ if (r.status === 'expired') core.die('the login request expired — run `ensemble login` again.');
48
+ }
49
+ if (!token) core.die('timed out waiting for approval — run `ensemble login` again.');
50
+ reportSetup(core.finishSetup(api, token, runnerPath));
51
+ }
52
+
53
+ // Headless / CI path: enrol with a copy-pasted enrollment token.
21
54
  async function enroll(args) {
22
55
  const token = args.token || process.env.ENSEMBLE_ENROLL_TOKEN;
23
- if (!token) core.die('missing --token <enrollment-token> — get it from your Ensemble portal → Connect Runner');
56
+ if (!token) core.die('missing --token <enrollment-token> — get it from your Ensemble portal → Connect Runner, or use `ensemble login` for browser sign-in');
24
57
  const api = args.api || core.DEFAULT_API;
25
58
  const name = args.name || os.hostname();
26
59
  core.log('Ensemble — enrolling "' + name + '" with ' + api);
@@ -28,14 +61,7 @@ async function enroll(args) {
28
61
  const { runnerPath } = await core.ensureBinaries();
29
62
  core.log('• exchanging enrollment token for a per-machine token');
30
63
  const machineToken = await core.enrollMachine(api, token, name);
31
- core.writeRunnerConfig(api, machineToken);
32
- const wired = core.wireHooks();
33
- core.log('• wired session hooks: ' + (wired.join(', ') || '(no supported coding CLI found — run `ensemble hooks` after installing claude/codex/grok)'));
34
- core.log('• installing auto-start + launching the runner');
35
- core.installAutostart(runnerPath);
36
- core.startRunner(runnerPath);
37
- core.log('\n✓ Enrolled. This machine runs your Ensemble runner and its coding sessions now share the bus (scoped to your org).');
38
- core.log(' Open a NEW terminal (so ENSEMBLE_MACHINE_TOKEN loads), then start Claude/Codex/Grok as usual.');
64
+ reportSetup(core.finishSetup(api, machineToken, runnerPath));
39
65
  }
40
66
 
41
67
  async function status(args) {
@@ -57,13 +83,15 @@ function usage() {
57
83
  core.log(
58
84
  `ensemble — connect this machine to your Ensemble control plane
59
85
 
86
+ ensemble login [--api <url>] [--name <machine>]
87
+ sign in via your browser (recommended) — approve the machine, no token to copy
60
88
  ensemble enroll --token <enrollment-token> [--api <url>] [--name <machine>]
61
- download + enrol the runner, wire session hooks, and start it
89
+ headless / CI: enrol with an enrollment token from the portal
62
90
  ensemble status show connection + your org's live session mailboxes
63
91
  ensemble hooks (re)wire ensemble-bus into installed coding CLIs
64
92
  ensemble version
65
93
 
66
- Get your enrollment token from the Ensemble portal Connect Runner.`);
94
+ Run \`ensemble login\` and approve the machine in your browser.`);
67
95
  }
68
96
 
69
97
  (async () => {
@@ -71,7 +99,8 @@ Get your enrollment token from the Ensemble portal → Connect Runner.`);
71
99
  const cmd = argv[0];
72
100
  const args = parseArgs(argv.slice(1));
73
101
  try {
74
- if (cmd === 'enroll') await enroll(args);
102
+ if (cmd === 'login') await login(args);
103
+ else if (cmd === 'enroll') await enroll(args);
75
104
  else if (cmd === 'status') await status(args);
76
105
  else if (cmd === 'hooks') { await core.ensureBinaries(); core.log('wired: ' + (core.wireHooks().join(', ') || 'none')); }
77
106
  else if (cmd === 'version' || cmd === '--version' || cmd === '-v') core.log(require('../package.json').version);
package/lib/core.js CHANGED
@@ -238,7 +238,43 @@ function startRunner(runnerPath) {
238
238
  child.unref();
239
239
  }
240
240
 
241
+ function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
242
+
243
+ function openBrowser(url) {
244
+ const pf = platform();
245
+ try {
246
+ if (pf.os === 'windows') spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore' }).unref();
247
+ else if (pf.os === 'mac') spawn('open', [url], { detached: true, stdio: 'ignore' }).unref();
248
+ else spawn('xdg-open', [url], { detached: true, stdio: 'ignore' }).unref();
249
+ } catch (e) { /* fall back to the printed URL */ }
250
+ }
251
+
252
+ // --- device-authorization login (RFC 8628): browser approve instead of a copy-pasted token ---
253
+ async function startDevice(api, machineName) {
254
+ const res = await request('POST', api.replace(/\/$/, '') + '/api/device/start',
255
+ { 'Content-Type': 'application/json' }, JSON.stringify({ machineName }));
256
+ if (res.status !== 200) throw new Error('could not start login: HTTP ' + res.status + ' ' + res.body);
257
+ return JSON.parse(res.body);
258
+ }
259
+
260
+ async function pollDevice(api, deviceCode) {
261
+ const res = await request('POST', api.replace(/\/$/, '') + '/api/device/poll',
262
+ { 'Content-Type': 'application/json' }, JSON.stringify({ deviceCode }));
263
+ if (res.status !== 200) throw new Error('poll failed: HTTP ' + res.status);
264
+ return JSON.parse(res.body);
265
+ }
266
+
267
+ // Shared post-token setup used by both `login` and `enroll`: write config, wire hooks, auto-start, run.
268
+ function finishSetup(api, machineToken, runnerPath) {
269
+ writeRunnerConfig(api, machineToken);
270
+ const wired = wireHooks();
271
+ installAutostart(runnerPath);
272
+ startRunner(runnerPath);
273
+ return wired;
274
+ }
275
+
241
276
  module.exports = {
242
277
  log, warn, die, platform, DEFAULT_API, ENSEMBLE_DIR, RUNNER_DIR, BIN_DIR,
243
278
  ensureBinaries, enrollMachine, writeRunnerConfig, wireHooks, installAutostart, startRunner, request,
279
+ sleep, openBrowser, startDevice, pollDevice, finishSetup,
244
280
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webority/ensemble",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
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"