@aglit-ai/cli 0.1.1 → 0.1.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
@@ -7,15 +7,23 @@ After the CLI and a compatible signed desktop release have been published:
7
7
 
8
8
  ```sh
9
9
  pnpm add -g @aglit-ai/cli
10
- aglit install
11
- aglit init
10
+ aglit
11
+ aglit start --headed
12
+ aglit start --headless
12
13
  aglit status
13
14
  aglit disable
14
15
  aglit enable
15
16
  aglit uninstall
16
17
  ```
17
18
 
18
- `init` opens Aglit's Google login in the browser, asks for a computer name,
19
+ Run `aglit` in an interactive terminal to open the guided setup. `aglit init`
20
+ also runs this flow and installs the signed runtime if it is missing. Setup asks
21
+ for a computer name and whether to keep the app in the background or open its
22
+ Desktop window. **Headless is the default**, including login startup. Headed
23
+ mode is an explicit choice; it shows the same app and profile. `aglit start
24
+ --headless` hides its window without disabling its connection.
25
+
26
+ Setup opens Aglit's Google login in the browser,
19
27
  detects existing Codex/Claude logins, and uses a configured authenticated provider
20
28
  or the sole available provider. Multiple usable providers prompt for a choice.
21
29
  Provider login detection is not an inference acceptance check. Original provider
@@ -25,11 +33,23 @@ accounts remain owned by the user; uninstall does not delete their original logi
25
33
  aglit init --name "Office Mac" --provider codex
26
34
  aglit providers
27
35
  aglit teams
36
+ aglit pipelines
37
+ aglit pipelines --open
28
38
  aglit init --name "Office Mac" --provider auto --team TEAM_ID
29
39
  aglit invite USER_EMAIL --team TEAM_ID
30
40
  ```
31
41
 
32
- For unattended setup, provide `--name` and an explicit `--provider` when detection
42
+ The terminal shows the current account's team memberships and authorized
43
+ pipeline devices. `aglit pipelines --open` opens the existing encrypted pipeline
44
+ viewer for pipeline contents and controls. A device listed in the catalog is not
45
+ proof that its pipelines are connected. Team selection remains invitation
46
+ context; it does not share the newly registered computer.
47
+
48
+ For unattended setup, use `aglit init --json --name "Office Mac" --provider codex
49
+ --account YOUR_EMAIL --headless`. `--account` checks the actual signed-in
50
+ account before configuration or remote access is enabled; it never selects or
51
+ impersonates an account. JSON mode and redirected input skip interactive prompts.
52
+ Provide an explicit `--provider` when detection
33
53
  cannot select a single provider. Initial account login and OS consent still need
34
54
  the user. On macOS, Screen Recording and Accessibility must be granted before
35
55
  `init` enables access. The native tray offers status, enable/disable, Console and
package/bin/aglit.cjs CHANGED
@@ -4,24 +4,30 @@ const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
  const os = require('node:os');
6
6
  const { parseArgs } = require('node:util');
7
- const { createInterface } = require('node:readline/promises');
8
7
  const installation = require('../src/installation.cjs');
8
+ const { createUi, teamsFromRoles } = require('../src/ui.cjs');
9
9
  const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
10
10
  const help = `Aglit Computer Use
11
11
 
12
- aglit install
13
- aglit init [--name "Computer name"] [--provider codex|claude|auto] [--team TEAM_ID] [--invite EMAIL]
12
+ aglit Interactive setup
13
+ aglit init Install and register this computer
14
+ [--name "Computer name"] [--provider codex|claude|auto]
15
+ [--account EMAIL] [--headed | --headless] [--team TEAM_ID] [--invite EMAIL]
16
+ aglit install Install the signed Desktop runtime
17
+ aglit start [--headed | --headless]
14
18
  aglit status [--json]
15
19
  aglit enable | disable
16
20
  aglit teams
21
+ aglit pipelines [--open]
17
22
  aglit providers
18
23
  aglit invite EMAIL --team TEAM_ID
19
24
  aglit uninstall
20
25
 
21
- Uninstall revokes remote access and purges this installation, including login,
22
- settings, owned bundled plugins/helpers and local data. Mini Computer and VMs
23
- are separate. Team invitations require backend authorization; pending invites
24
- grant no access. A team selector never grants access to a personal computer.
26
+ Aglit runs in the background by default. --headed opens the Desktop window.
27
+ Use --json with install, init, start or status for non-interactive JSON output.
28
+ Uninstall revokes access and purges only this installation. Mini Computer,
29
+ VMs, shared browser profiles and original provider logins remain separate.
30
+ Team selection does not grant access to this personal computer.
25
31
  `;
26
32
 
27
33
  async function finishUninstall(receipt) {
@@ -31,108 +37,161 @@ async function finishUninstall(receipt) {
31
37
  try { process.kill(receipt.pid, 0); } catch (error) {
32
38
  if (error.code !== 'ESRCH') throw error;
33
39
  installation.purge();
34
- process.stdout.write(JSON.stringify({ uninstalled: true, purged: true }) + '\n'); return;
40
+ return { uninstalled: true, purged: true };
35
41
  }
36
42
  }
37
43
  throw Error('Runtime has not exited; local files preserved. Retry aglit uninstall after it quits.');
38
44
  }
39
45
 
40
46
  async function main() {
41
- const { values, positionals } = parseArgs({ options: { name: { type: 'string' }, provider: { type: 'string' }, team: { type: 'string' }, invite: { type: 'string' }, json: { type: 'boolean' }, help: { type: 'boolean', short: 'h' } }, allowPositionals: true, strict: true });
42
- if (values.help || !positionals.length) { process.stdout.write(help); return; }
43
- const [command, email] = positionals;
44
- const options = { install: [], init: ['name', 'provider', 'team', 'invite'], status: ['json'], enable: [], disable: [], teams: [], providers: [], invite: ['team'], uninstall: [] };
47
+ const { values, positionals } = parseArgs({ options: {
48
+ name: { type: 'string' }, provider: { type: 'string' }, account: { type: 'string' },
49
+ team: { type: 'string' }, invite: { type: 'string' }, headed: { type: 'boolean' },
50
+ headless: { type: 'boolean' }, open: { type: 'boolean' }, json: { type: 'boolean' }, help: { type: 'boolean', short: 'h' },
51
+ }, allowPositionals: true, strict: true });
52
+ const interactive = Boolean(process.stdin.isTTY && process.stderr.isTTY && !values.json && !process.env.CI);
53
+ if (values.help || !positionals.length && !interactive) { process.stdout.write(help); return; }
54
+ const [command = 'init', email] = positionals;
55
+ const options = {
56
+ install: ['json'], init: ['name', 'provider', 'account', 'team', 'invite', 'headed', 'headless', 'json'],
57
+ start: ['headed', 'headless', 'json'], status: ['json'], enable: [], disable: [], teams: ['json'], pipelines: ['open', 'json'], providers: ['json'], invite: ['team'], uninstall: [],
58
+ };
45
59
  if (!Object.hasOwn(options, command) || Object.keys(values).some(key => !options[command].includes(key)) || positionals.length > (command === 'invite' ? 2 : 1)) throw Error('Invalid command or options. Run aglit --help.');
60
+ if (values.headed && values.headless) throw Error('Choose either --headed or --headless');
46
61
  if (values.team && !/^[A-Za-z0-9._:-]{1,128}$/.test(values.team)) throw Error('Invalid team ID');
47
62
  if (values.provider && !['auto', 'codex', 'claude'].includes(values.provider)) throw Error('Provider must be auto, codex or claude');
63
+ const validEmail = value => value.length <= 254 && /^[^\s@*]+@[^\s@*]+\.[^\s@*]+$/.test(value);
48
64
  const recipient = command === 'invite' ? email : values.invite;
49
- if (recipient && (recipient.length > 254 || !/^[^\s@*]+@[^\s@*]+\.[^\s@*]+$/.test(recipient))) throw Error('An individual invitation email address is required');
65
+ if (recipient && !validEmail(recipient)) throw Error('An individual invitation email address is required');
66
+ if (values.account !== undefined && !validEmail(values.account)) throw Error('An individual account email address is required');
50
67
  if ((command === 'invite' || values.invite) && (!recipient || !values.team)) throw Error('Invitations require an email address and --team TEAM_ID');
51
- if (values.name !== undefined && (!values.name.trim() || values.name.length > 100 || /[\x00-\x1f\x7f]/.test(values.name))) throw Error('Computer name must contain 1–100 printable characters');
52
- const print = value => process.stdout.write(JSON.stringify(value, null, 2) + '\n');
53
- if (command === 'install') { print(await installation.install()); return; }
54
- if (command === 'status' && !fs.existsSync(path.join(installation.root, 'installation.json'))) {
55
- print({ product: 'Aglit Computer Use', installed: false, running: false, enabled: false }); return;
68
+ if (values.name !== undefined && (!values.name.trim() || values.name.length > 100 || /[\x00-\x1f\x7f-\x9f]/.test(values.name))) throw Error('Computer name must contain 1–100 printable characters');
69
+ const ui = await createUi(interactive);
70
+ const installed = () => fs.existsSync(path.join(installation.root, 'installation.json'));
71
+ if (command === 'install') {
72
+ const receipt = await ui.step('Download and verify Desktop', () => installation.install());
73
+ if (interactive) ui.info('Desktop installed. Run aglit init to register this computer.');
74
+ else ui.result(receipt);
75
+ ui.finish('Ready for setup'); return;
56
76
  }
57
- if (command === 'uninstall' && !fs.existsSync(path.join(installation.root, 'installation.json'))) {
77
+ if (command === 'status' && !installed()) {
78
+ ui.result({ product: 'Aglit Computer Use', installed: false, running: false, enabled: false });
79
+ ui.finish('Run aglit to set up this computer'); return;
80
+ }
81
+ if (command === 'uninstall' && !installed()) {
58
82
  if (fs.existsSync(installation.root)) installation.purgeIncomplete();
59
- print({ uninstalled: true, purged: true }); return;
83
+ ui.result({ uninstalled: true, purged: true }); ui.finish('Uninstalled'); return;
60
84
  }
61
- if (command === 'uninstall' || command === 'status') {
85
+ if ((command === 'uninstall' || command === 'status') && installed()) {
62
86
  const pending = installation.readUninstall();
63
87
  if (pending) {
64
- if (command === 'uninstall') await finishUninstall(pending);
65
- else print({ installed: true, uninstalling: true, enabled: false });
66
- return;
88
+ ui.result(command === 'uninstall' ? await finishUninstall(pending) : { installed: true, uninstalling: true, enabled: false });
89
+ ui.finish('Uninstall cleanup'); return;
90
+ }
91
+ }
92
+ let name = values.name;
93
+ let mode = values.headed ? 'headed' : 'headless';
94
+ if (command === 'init') {
95
+ if (!name && interactive && !installed()) name = await ui.name(os.hostname());
96
+ if (interactive && values.headed === undefined && values.headless === undefined) mode = await ui.mode();
97
+ if (!installed()) {
98
+ if (!interactive && !name) throw Error('Provide --name for unattended setup');
99
+ await ui.step('Download and verify Desktop', () => installation.install());
67
100
  }
68
101
  }
102
+ if (!installed()) throw Error('Run aglit init to install and register this computer first.');
69
103
  installation.readInstallation();
70
104
  const configPath = path.join(installation.profile, 'ipc/sdk-ipc.json');
71
105
  const control = (action, fields = {}) => require('../src/control.cjs').control(configPath, action, fields);
72
106
  let state;
73
107
  try { state = await control('status'); } catch (error) {
74
- if (command === 'status') { print({ installed: true, running: false, enabled: null, error: error.message }); process.exitCode = 1; return; }
75
- installation.start();
76
- const deadline = Date.now() + 60000;
77
- while (Date.now() < deadline) {
78
- await delay(1000);
79
- try { state = await control('status'); break; } catch { /* Wait for this installed runtime's native socket. */ }
80
- }
81
- if (!state) throw Error('Computer Use did not become ready. See runtime.log in its installation directory.');
108
+ if (command === 'status') { ui.result({ product: 'Aglit Computer Use', installed: true, running: false, enabled: null, error: error.message }); ui.finish('Runtime stopped'); process.exitCode = 1; return; }
109
+ state = await ui.step('Start Desktop in the background', async () => {
110
+ installation.start();
111
+ const deadline = Date.now() + 60000;
112
+ while (Date.now() < deadline) {
113
+ await delay(1000);
114
+ try { return await control('status'); } catch { /* Wait for this installation's authenticated native socket. */ }
115
+ }
116
+ throw Error('Computer Use did not become ready. See runtime.log in its installation directory.');
117
+ });
118
+ }
119
+ if (command === 'status') { ui.result(state); ui.finish('Computer status'); return; }
120
+ if (command === 'teams' || command === 'pipelines') {
121
+ const result = await ui.step(command === 'teams' ? 'Load your teams' : 'Load authorized pipeline devices', () => control(command));
122
+ if (interactive) ui[command](result); else ui.result(result);
123
+ if (command === 'pipelines' && values.open) await control('open-pipelines');
124
+ ui.finish(command === 'teams' ? 'Your teams' : 'Your pipelines'); return;
125
+ }
126
+ if (command === 'start' || command === 'init') {
127
+ const window = await control('window', { mode });
128
+ state = { ...state, ...window };
129
+ if (command === 'start') { ui.result(state); ui.finish(mode === 'headed' ? 'Desktop window opened' : 'Running in the background'); return; }
82
130
  }
83
- if (command === 'status') { print(state); return; }
84
131
  if (command === 'init') {
85
132
  if (!state.email) {
86
- await control('login');
87
- process.stderr.write('Complete sign-in in your browser.\n');
88
- const deadline = Date.now() + 5 * 60000;
89
- while (Date.now() < deadline) {
90
- await delay(1500); state = await control('status'); if (state.email) break;
91
- }
92
- if (!state.email) throw Error('Sign-in timed out. Run aglit init to resume.');
133
+ state = await ui.step('Complete sign-in in your browser', async () => {
134
+ await control('login');
135
+ const deadline = Date.now() + 5 * 60000;
136
+ while (Date.now() < deadline) {
137
+ await delay(1500); const current = await control('status'); if (current.email) return current;
138
+ }
139
+ throw Error('Sign-in timed out. Run aglit init to resume.');
140
+ });
93
141
  }
94
- let name = values.name || state.name;
95
- if (!name && process.stdin.isTTY) {
96
- const prompt = createInterface({ input: process.stdin, output: process.stderr });
97
- try { name = (await prompt.question(`Computer name [${os.hostname()}]: `)).trim() || os.hostname(); } finally { prompt.close(); }
142
+ if (values.account && state.email?.toLowerCase() !== values.account.toLowerCase())
143
+ throw Error(`Signed in as ${state.email}; expected ${values.account}. Switch the browser sign-in account and rerun aglit init.`);
144
+ if (interactive && !values.account && !await ui.confirmAccount(state.email))
145
+ throw Object.assign(Error('Setup cancelled before enabling remote access.'), { cancelled: true });
146
+ ui.info(`Signed in as ${state.email}`);
147
+ let teamId = values.team;
148
+ if (interactive) {
149
+ const discovery = await ui.step('Load your teams and pipelines', () => Promise.allSettled([control('teams'), control('pipelines')]));
150
+ let teams = [];
151
+ if (discovery[0].status === 'fulfilled') { ui.teams(discovery[0].value); teams = teamsFromRoles(discovery[0].value); }
152
+ else ui.info('Teams are unavailable. You can retry with aglit teams.');
153
+ if (discovery[1].status === 'fulfilled') ui.pipelines(discovery[1].value);
154
+ else ui.info('Pipeline devices are unavailable. You can retry with aglit pipelines.');
155
+ if (!teamId && teams.length) teamId = await ui.team(teams);
98
156
  }
157
+ name ||= state.name;
158
+ if (!name && interactive) name = await ui.name(os.hostname());
99
159
  if (!name) throw Error('Provide --name for unattended setup');
100
- const detected = await control('providers');
160
+ const detected = await ui.step('Find signed-in AI providers', () => control('providers'));
101
161
  const available = detected.providers.filter(provider => provider.authenticated).map(provider => provider.provider);
102
162
  let provider = values.provider && values.provider !== 'auto' ? values.provider :
103
163
  available.includes(detected.preferred) ? detected.preferred : available.length === 1 ? available[0] : null;
104
- if (!provider && available.length && process.stdin.isTTY) {
105
- const prompt = createInterface({ input: process.stdin, output: process.stderr });
106
- try { provider = (await prompt.question(`Provider (${available.join(', ')}): `)).trim(); } finally { prompt.close(); }
107
- }
108
- if (!provider || !available.includes(provider)) throw Error('No provider selected with a usable login. Sign in to Codex or Claude on this computer, then rerun aglit init --provider codex|claude.');
109
- await control('configure', { name, provider, ...(values.team ? { teamId: values.team } : {}) });
110
- const permissions = await control('permissions');
111
- print(permissions);
164
+ if (!provider && available.length && interactive) provider = await ui.provider(available, detected.preferred);
165
+ if (!provider || !available.includes(provider)) throw Error('Sign in to Codex or Claude on this computer, then rerun aglit init --provider codex|claude.');
166
+ await ui.step('Save computer settings', () => control('configure', { name, provider, ...(teamId ? { teamId } : {}) }));
167
+ const permissions = await ui.step('Check screen recording and input permissions', () => control('permissions'));
168
+ if (!interactive) ui.result(permissions);
112
169
  if (permissions.permissions?.supported && (permissions.permissions.screenRecording !== 'granted' || permissions.permissions.accessibility !== 'granted'))
113
170
  throw Error('Grant Screen Recording and Accessibility in System Settings, then rerun aglit init. Anywhere Access has not been enabled.');
114
- // An enable receipt is configuration, not proof that capture/input work.
115
- await control('enable');
116
- const connectionDeadline = Date.now() + 30000;
117
- do {
118
- state = await control('status');
119
- if (state.connected) break;
120
- await delay(1000);
121
- } while (Date.now() < connectionDeadline);
122
- print(state);
123
- if (!state.connected) throw Error('Anywhere Access is enabled but its connection is not ready. Check aglit status before retrying.');
124
- if (values.invite) print(await control('invite', { teamId: values.team, email: values.invite }));
125
- return;
171
+ state = await ui.step('Connect this computer to your account', async () => {
172
+ await control('enable');
173
+ const deadline = Date.now() + 30000;
174
+ do {
175
+ const current = await control('status');
176
+ if (current.connected) return current;
177
+ await delay(1000);
178
+ } while (Date.now() < deadline);
179
+ throw Error('Anywhere Access is enabled but its connection is not ready. Check aglit status before retrying.');
180
+ });
181
+ if (values.invite) ui.result(await control('invite', { teamId: values.team, email: values.invite }));
182
+ ui.result(state); ui.finish('Computer ready · https://console.aglit.ai/'); return;
126
183
  }
127
- if (command === 'invite') { print(await control('invite', { teamId: values.team, email })); return; }
184
+ if (command === 'invite') { ui.result(await control('invite', { teamId: values.team, email })); ui.finish('Invitation sent'); return; }
128
185
  if (command === 'uninstall') {
129
186
  const result = await control('uninstall');
130
187
  if (!result?.revoked || !result?.quitting || !Number.isSafeInteger(result.pid) || result.pid <= 0) throw Error('Revocation was not confirmed; installation preserved');
131
- await finishUninstall(result); return;
188
+ ui.result(await ui.step('Remove this installation', () => finishUninstall(result)));
189
+ ui.finish('Uninstalled'); return;
132
190
  }
133
- print(await control(command));
191
+ ui.result(await control(command)); ui.finish('Done');
134
192
  }
135
193
 
136
194
  main().then(() => process.exit(process.exitCode || 0), error => {
137
- process.stderr.write(`aglit: ${error.message}\n`); process.exit(1);
195
+ if (!error.cancelled) process.stderr.write(`aglit: ${error.message}\n`);
196
+ process.exit(error.cancelled ? 130 : 1);
138
197
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglit-ai/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Install and manage Aglit Computer Use",
5
5
  "license": "UNLICENSED",
6
6
  "type": "commonjs",
@@ -16,6 +16,7 @@
16
16
  "node": ">=22"
17
17
  },
18
18
  "dependencies": {
19
+ "@clack/prompts": "1.8.1",
19
20
  "yauzl": "3.4.0"
20
21
  },
21
22
  "publishConfig": {
package/src/ui.cjs ADDED
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+
3
+ const clean = value => String(value ?? '').replace(/[\x00-\x1f\x7f-\x9f]/g, '');
4
+ function teamsFromRoles(value) {
5
+ const teams = new Map();
6
+ for (const company of value?.companies || []) {
7
+ for (const team of [...(company.teams || []), ...(company.organizations || []).flatMap(organization => organization.teams || [])]) {
8
+ if (typeof team.teamId === 'string') teams.set(team.teamId, { id: team.teamId, roles: team.roles || [] });
9
+ }
10
+ }
11
+ for (const role of value?.roles || []) {
12
+ const id = role.teamId || (role.scopeType === 'team' ? role.scopeId : null);
13
+ if (typeof id === 'string' && !teams.has(id)) teams.set(id, { id, roles: [role.roleLabel || role.role].filter(Boolean) });
14
+ }
15
+ return [...teams.values()];
16
+ }
17
+ async function createUi(interactive) {
18
+ if (!interactive) return {
19
+ interactive: false,
20
+ step: (_label, action) => action(),
21
+ result: value => process.stdout.write(JSON.stringify(value, null, 2) + '\n'),
22
+ info: message => process.stderr.write(message + '\n'),
23
+ finish() {},
24
+ };
25
+ const p = await import('@clack/prompts');
26
+ p.intro('Aglit Computer Use');
27
+ async function answer(promise) {
28
+ const value = await promise;
29
+ if (p.isCancel(value)) {
30
+ p.cancel('Setup cancelled.');
31
+ throw Object.assign(Error('Setup cancelled'), { cancelled: true });
32
+ }
33
+ return value;
34
+ }
35
+ return {
36
+ interactive: true,
37
+ name: initialValue => answer(p.text({ message: 'Name this computer', placeholder: initialValue, defaultValue: initialValue,
38
+ validate: value => value && (!value.trim() || value.length > 100 || /[\x00-\x1f\x7f-\x9f]/.test(value)) ? 'Use 1–100 printable characters.' : undefined })),
39
+ mode: () => answer(p.select({ message: 'How should Aglit run?', initialValue: 'headless', options: [
40
+ { value: 'headless', label: 'Headless', hint: 'default · background' },
41
+ { value: 'headed', label: 'Open Desktop window', hint: 'headed' },
42
+ ] })),
43
+ provider: (providers, preferred) => answer(p.select({ message: 'Choose your AI provider', initialValue: preferred,
44
+ options: providers.map(value => ({ value, label: value === 'codex' ? 'Codex' : 'Claude' })) })),
45
+ team: teams => answer(p.select({ message: 'Team for invitations', initialValue: '', options: [
46
+ { value: '', label: 'Personal computer', hint: 'access stays with your account' },
47
+ ...teams.map(team => ({ value: team.id, label: clean(team.id), hint: team.roles.map(clean).join(', ') })),
48
+ ] })),
49
+ teams(value) {
50
+ const teams = teamsFromRoles(value);
51
+ p.note(teams.length ? teams.map(team => `${clean(team.id)} ${team.roles.map(clean).join(', ')}`).join('\n') : 'No team memberships.', 'Your teams');
52
+ return teams;
53
+ },
54
+ pipelines(value) {
55
+ p.note((value.devices.length ? value.devices.map(device => `${clean(device.name)}${device.platform ? ' · ' + clean(device.platform) : ''}`).join('\n') + '\n\n' : 'No authorized pipeline devices.\n\n') +
56
+ 'https://pipelines.aglit.ai/\nOpen the encrypted viewer with aglit pipelines --open.', 'Pipeline devices');
57
+ },
58
+ confirmAccount: email => answer(p.confirm({ message: `Register with ${clean(email)}?`, initialValue: true })),
59
+ async step(label, action) {
60
+ const spinner = p.spinner(); spinner.start(label);
61
+ try { const value = await action(); spinner.stop(label); return value; }
62
+ catch (error) { spinner.stop('Setup paused'); throw error; }
63
+ },
64
+ result(value) {
65
+ if (value.product === 'Aglit Computer Use') {
66
+ p.note([
67
+ value.name && `Computer ${clean(value.name)}`,
68
+ value.email && `Account ${clean(value.email)}`,
69
+ `Runtime ${value.running ? 'running' : value.installed ? 'stopped' : 'not installed'}`,
70
+ value.mode && `Window ${value.windowVisible ? 'open' : 'hidden'}`,
71
+ `Access ${value.connected ? 'connected' : value.enabled ? 'connecting' : 'disabled'}`,
72
+ value.provider && `Provider ${clean(value.provider)}`,
73
+ ].filter(Boolean).join('\n'), 'This computer');
74
+ } else p.log.info(JSON.stringify(value, null, 2));
75
+ },
76
+ info: message => p.log.info(clean(message)),
77
+ finish: message => p.outro(message),
78
+ };
79
+ }
80
+
81
+ module.exports = { createUi, teamsFromRoles };