@aglit-ai/cli 0.1.2 → 0.1.5

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
@@ -87,3 +87,19 @@ identity. It never adopts a managed guest's key or changes its registration.
87
87
 
88
88
  See [architecture and acceptance](../../docs/computer-use.md). The source CLI can
89
89
  be inspected without installation using `node packages/cli/bin/aglit.cjs --help`.
90
+
91
+ ## Updates
92
+
93
+ One CLI installation includes the correct signed Desktop runtime for this
94
+ computer. The runtime assets are bundled internally through npm; no separate
95
+ runtime package installation is needed.
96
+ Downloads verify the npm package integrities, every runtime part, the complete
97
+ Desktop archive and the native publisher before launch. GitHub delta downloads
98
+ are not used by this installation.
99
+
100
+ Automatic update checks run while Desktop is active. They preserve the computer's
101
+ account, device identity and profile, and wait for an idle runtime before a native
102
+ restart. Use `aglit update --check` to inspect available updates, `aglit update`
103
+ to apply one, and `aglit updates off` or `aglit updates on` to change automatic
104
+ checks. New installations follow `latest`; experimental installations can save
105
+ an explicit channel with `aglit init --channel embedded`.
package/bin/aglit.cjs CHANGED
@@ -5,6 +5,7 @@ const path = require('node:path');
5
5
  const os = require('node:os');
6
6
  const { parseArgs } = require('node:util');
7
7
  const installation = require('../src/installation.cjs');
8
+ const updates = require('../src/updates.cjs');
8
9
  const { createUi, teamsFromRoles } = require('../src/ui.cjs');
9
10
  const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
10
11
  const help = `Aglit Computer Use
@@ -12,10 +13,13 @@ const help = `Aglit Computer Use
12
13
  aglit Interactive setup
13
14
  aglit init Install and register this computer
14
15
  [--name "Computer name"] [--provider codex|claude|auto]
15
- [--account EMAIL] [--headed | --headless] [--team TEAM_ID] [--invite EMAIL]
16
+ [--account EMAIL] [--headed | --headless] [--channel latest|canary|embedded]
17
+ [--team TEAM_ID] [--invite EMAIL]
16
18
  aglit install Install the signed Desktop runtime
17
19
  aglit start [--headed | --headless]
18
20
  aglit status [--json]
21
+ aglit update [--check] [--channel latest|canary|embedded] [--json]
22
+ aglit updates on|off|status [--json]
19
23
  aglit enable | disable
20
24
  aglit teams
21
25
  aglit pipelines [--open]
@@ -44,19 +48,22 @@ async function finishUninstall(receipt) {
44
48
  }
45
49
 
46
50
  async function main() {
51
+ const delegated = updates.delegate(__filename); if (delegated !== null) { process.exitCode = delegated; return; }
47
52
  const { values, positionals } = parseArgs({ options: {
48
53
  name: { type: 'string' }, provider: { type: 'string' }, account: { type: 'string' },
49
54
  team: { type: 'string' }, invite: { type: 'string' }, headed: { type: 'boolean' },
50
- headless: { type: 'boolean' }, open: { type: 'boolean' }, json: { type: 'boolean' }, help: { type: 'boolean', short: 'h' },
55
+ headless: { type: 'boolean' }, check: { type: 'boolean' }, channel: { type: 'string' }, open: { type: 'boolean' }, json: { type: 'boolean' }, help: { type: 'boolean', short: 'h' },
51
56
  }, allowPositionals: true, strict: true });
52
57
  const interactive = Boolean(process.stdin.isTTY && process.stderr.isTTY && !values.json && !process.env.CI);
53
58
  if (values.help || !positionals.length && !interactive) { process.stdout.write(help); return; }
54
59
  const [command = 'init', email] = positionals;
55
60
  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: [],
61
+ install: ['json', 'channel'], init: ['name', 'provider', 'account', 'team', 'invite', 'headed', 'headless', 'json', 'channel'],
62
+ start: ['headed', 'headless', 'json', 'channel'], status: ['json'], update: ['check', 'channel', 'json'], updates: ['json'], enable: [], disable: [], teams: ['json'], pipelines: ['open', 'json'], providers: ['json'], invite: ['team'], uninstall: [],
58
63
  };
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.');
64
+ if (!Object.hasOwn(options, command) || Object.keys(values).some(key => !options[command].includes(key)) || positionals.length > (['invite', 'updates'].includes(command) ? 2 : 1)) throw Error('Invalid command or options. Run aglit --help.');
65
+ if (command === 'updates' && !['on', 'off', 'status'].includes(email)) throw Error('Use aglit updates on|off|status');
66
+ if (values.channel !== undefined && !['latest', 'canary', 'embedded'].includes(values.channel)) throw Error('Update channel must be latest, canary or embedded');
60
67
  if (values.headed && values.headless) throw Error('Choose either --headed or --headless');
61
68
  if (values.team && !/^[A-Za-z0-9._:-]{1,128}$/.test(values.team)) throw Error('Invalid team ID');
62
69
  if (values.provider && !['auto', 'codex', 'claude'].includes(values.provider)) throw Error('Provider must be auto, codex or claude');
@@ -68,8 +75,18 @@ async function main() {
68
75
  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
76
  const ui = await createUi(interactive);
70
77
  const installed = () => fs.existsSync(path.join(installation.root, 'installation.json'));
78
+ if (command === 'update') {
79
+ ui.result(await ui.step(values.check ? 'Check npm updates' : 'Update Aglit from npm', () => updates.update({check: Boolean(values.check), channel: values.channel})));
80
+ ui.finish('Update check complete'); return;
81
+ }
82
+ if (command === 'updates') {
83
+ if (!installed()) throw Error('Run aglit init before configuring automatic updates');
84
+ ui.result(email === 'status' ? await updates.status() : await updates.automatic(email === 'on'));
85
+ ui.finish('Automatic updates'); return;
86
+ }
71
87
  if (command === 'install') {
72
- const receipt = await ui.step('Download and verify Desktop', () => installation.install());
88
+ const receipt = await ui.step('Download and verify Desktop', () => installation.install(values.channel));
89
+ updates.initialize(values.channel);
73
90
  if (interactive) ui.info('Desktop installed. Run aglit init to register this computer.');
74
91
  else ui.result(receipt);
75
92
  ui.finish('Ready for setup'); return;
@@ -85,6 +102,7 @@ async function main() {
85
102
  if ((command === 'uninstall' || command === 'status') && installed()) {
86
103
  const pending = installation.readUninstall();
87
104
  if (pending) {
105
+ if (command === 'uninstall') await updates.stopWorker();
88
106
  ui.result(command === 'uninstall' ? await finishUninstall(pending) : { installed: true, uninstalling: true, enabled: false });
89
107
  ui.finish('Uninstall cleanup'); return;
90
108
  }
@@ -96,17 +114,33 @@ async function main() {
96
114
  if (interactive && values.headed === undefined && values.headless === undefined) mode = await ui.mode();
97
115
  if (!installed()) {
98
116
  if (!interactive && !name) throw Error('Provide --name for unattended setup');
99
- await ui.step('Download and verify Desktop', () => installation.install());
117
+ await ui.step('Download and verify Desktop', () => installation.install(values.channel));
100
118
  }
101
119
  }
102
120
  if (!installed()) throw Error('Run aglit init to install and register this computer first.');
103
121
  installation.readInstallation();
122
+ if (['start', 'init'].includes(command)) {
123
+ const recovered = await updates.recoverActivation();
124
+ if (recovered.updating) { ui.result(recovered); ui.finish('Update in progress'); return; }
125
+ }
126
+ if (['start', 'init'].includes(command)) updates.initialize(values.channel);
104
127
  const configPath = path.join(installation.profile, 'ipc/sdk-ipc.json');
105
128
  const control = (action, fields = {}) => require('../src/control.cjs').control(configPath, action, fields);
106
129
  let state;
107
130
  try { state = await control('status'); } catch (error) {
108
131
  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
132
  state = await ui.step('Start Desktop in the background', async () => {
133
+ // A stopped legacy runtime can make its first npm migration here without
134
+ // terminating active work or relying on handoff commands it predates.
135
+ if (updates.state().automatic && updates.runtimeStopped(installation.readInstallation())) {
136
+ try { await updates.update(); } catch {
137
+ if (fs.existsSync(path.join(installation.root, 'update-transaction.json')) || fs.existsSync(path.join(installation.root, 'update-activation.json'))) {
138
+ const recovered = await updates.recoverActivation(); if (recovered.updating) throw Error('An update is still active; retry aglit start after it finishes');
139
+ }
140
+ ui.info('Automatic update deferred; starting the installed Desktop.');
141
+ }
142
+ try { const current = await control('status'); if (current.running) return current; } catch { /* No update started Desktop. */ }
143
+ }
110
144
  installation.start();
111
145
  const deadline = Date.now() + 60000;
112
146
  while (Date.now() < deadline) {
@@ -116,6 +150,8 @@ async function main() {
116
150
  throw Error('Computer Use did not become ready. See runtime.log in its installation directory.');
117
151
  });
118
152
  }
153
+ if (state.running && ['status', 'start', 'init'].includes(command)) updates.rememberStatus(state);
154
+ if (['start', 'init'].includes(command) && state.running) updates.ensureWorker(state.pid);
119
155
  if (command === 'status') { ui.result(state); ui.finish('Computer status'); return; }
120
156
  if (command === 'teams' || command === 'pipelines') {
121
157
  const result = await ui.step(command === 'teams' ? 'Load your teams' : 'Load authorized pipeline devices', () => control(command));
@@ -179,14 +215,18 @@ async function main() {
179
215
  throw Error('Anywhere Access is enabled but its connection is not ready. Check aglit status before retrying.');
180
216
  });
181
217
  if (values.invite) ui.result(await control('invite', { teamId: values.team, email: values.invite }));
218
+ updates.rememberStatus(state);
182
219
  ui.result(state); ui.finish('Computer ready · https://console.aglit.ai/'); return;
183
220
  }
184
221
  if (command === 'invite') { ui.result(await control('invite', { teamId: values.team, email })); ui.finish('Invitation sent'); return; }
185
222
  if (command === 'uninstall') {
186
- const result = await control('uninstall');
187
- if (!result?.revoked || !result?.quitting || !Number.isSafeInteger(result.pid) || result.pid <= 0) throw Error('Revocation was not confirmed; installation preserved');
188
- ui.result(await ui.step('Remove this installation', () => finishUninstall(result)));
189
- ui.finish('Uninstalled'); return;
223
+ const unlock = await updates.prepareUninstall();
224
+ try {
225
+ const result = await control('uninstall');
226
+ if (!result?.revoked || !result?.quitting || !Number.isSafeInteger(result.pid) || result.pid <= 0) throw Error('Revocation was not confirmed; installation preserved');
227
+ ui.result(await ui.step('Remove this installation', () => finishUninstall(result)));
228
+ ui.finish('Uninstalled'); return;
229
+ } finally { unlock(); }
190
230
  }
191
231
  ui.result(await control(command)); ui.finish('Done');
192
232
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglit-ai/cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.5",
4
4
  "description": "Install and manage Aglit Computer Use",
5
5
  "license": "UNLICENSED",
6
6
  "type": "commonjs",
@@ -5,7 +5,8 @@ const os = require('node:os');
5
5
  const crypto = require('node:crypto');
6
6
  const { spawn, execFileSync } = require('node:child_process');
7
7
  const { pipeline } = require('node:stream/promises');
8
- const { Readable, Transform } = require('node:stream');
8
+ const { once } = require('node:events');
9
+ const { createRequire } = require('node:module');
9
10
 
10
11
  const root = path.join(process.platform === 'darwin' ? path.join(os.homedir(), 'Library/Application Support') :
11
12
  process.platform === 'win32' ? process.env.APPDATA || path.join(os.homedir(), 'AppData/Roaming') : path.join(os.homedir(), '.config'), 'Aglit Computer Use');
@@ -56,48 +57,6 @@ function readInstallation(allowPartialCleanup = false) {
56
57
  if (!allowPartialCleanup) { canonical(value.executable); canonical(value.profile); }
57
58
  return value;
58
59
  }
59
- async function download(url) {
60
- for (let hop = 0; hop < 6; hop++) {
61
- const target = new URL(url);
62
- if (target.protocol !== 'https:' || !['github.com', 'release-assets.githubusercontent.com', 'objects.githubusercontent.com'].includes(target.hostname)) throw Error('Untrusted release URL');
63
- const response = await fetch(target, { redirect: 'manual', signal: AbortSignal.timeout(30 * 60 * 1000) });
64
- if ([301, 302, 303, 307, 308].includes(response.status)) {
65
- await response.body?.cancel();
66
- url = new URL(response.headers.get('location'), target).href;
67
- continue;
68
- }
69
- if (!response.ok || !response.body) throw Error(`Release download failed (${response.status})`);
70
- return response;
71
- }
72
- throw Error('Too many release redirects');
73
- }
74
- async function json(url) {
75
- const response = await download(url);
76
- let size = 0;
77
- const chunks = [];
78
- for await (const chunk of response.body) {
79
- size += chunk.length;
80
- if (size > 65536) throw Error('Release metadata too large');
81
- chunks.push(chunk);
82
- }
83
- return JSON.parse(Buffer.concat(chunks).toString('utf8'));
84
- }
85
- function parseRelease(value, platform) {
86
- const fields = ['kind', 'package', 'platform', 'version', 'sourceCommit', 'assets', 'windowsSigningPolicy'];
87
- if (!value || Object.keys(value).some(key => !fields.includes(key)) || value.kind !== 'aglit-github-release-v1' ||
88
- value.package !== 'desktop-app' || value.platform !== platform || !/^\d+\.\d+\.\d+$/.test(value.version) ||
89
- !/^[a-f0-9]{40}$/.test(value.sourceCommit) || !Array.isArray(value.assets) || value.assets.length !== 3 ||
90
- value.windowsSigningPolicy !== undefined && (platform !== 'win' || value.windowsSigningPolicy !== 'unsigned-user-accepted')) throw Error('Invalid desktop release receipt');
91
- const stem = `Aglit.AI-${value.version}-${platform === 'mac' ? 'mac-arm64' : 'win-x64'}`;
92
- const names = platform === 'mac' ? [stem + '.dmg', stem + '.zip', stem + '.zip.blockmap'] : [stem + '.exe', stem + '.exe.blockmap', stem + '.zip'];
93
- for (let index = 0; index < 3; index++) {
94
- const asset = value.assets[index];
95
- if (!asset || Object.keys(asset).sort().join(',') !== 'name,sha256,sha512,size' || asset.name !== names[index] ||
96
- !Number.isSafeInteger(asset.size) || asset.size <= 0 || asset.size > 2 * 1024 ** 3 ||
97
- !/^[a-f0-9]{64}$/.test(asset.sha256) || !/^[A-Za-z0-9+/]{86}==$/.test(asset.sha512)) throw Error('Invalid release asset');
98
- }
99
- return value;
100
- }
101
60
  function verifyPublisher(executable) {
102
61
  if (process.platform === 'darwin') {
103
62
  const bundle = path.resolve(executable, '../../..');
@@ -108,53 +67,131 @@ function verifyPublisher(executable) {
108
67
  run(powershell, ['-NoLogo', '-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-Command', script], { env: { ...process.env, AGLIT_VERIFY_EXECUTABLE: executable } });
109
68
  }
110
69
  }
111
- async function install() {
70
+ function embeddedRelease(platform, packageDirectory = path.resolve(__dirname, '..'), runtimeDirectory) {
71
+ const packageName = '@aglit-ai/desktop-runtime-' + (platform === 'mac' ? 'darwin-arm64' : 'win32-x64');
72
+ let directory = path.join(runtimeDirectory || packageDirectory, 'runtime');
73
+ if (!fs.existsSync(path.join(directory, 'desktop-runtime.json'))) {
74
+ try { directory = path.join(path.dirname(createRequire(path.join(packageDirectory, 'package.json')).resolve(packageName + '/package.json')), 'runtime'); }
75
+ catch (error) { if (error.code === 'MODULE_NOT_FOUND') return null; throw error; }
76
+ }
77
+ const metadata = path.join(directory, 'desktop-runtime.json');
78
+ canonical(metadata);
79
+ const stat = fs.statSync(metadata);
80
+ if (!stat.isFile() || stat.size > 65536) throw Error('Invalid bundled Desktop metadata');
81
+ const value = JSON.parse(fs.readFileSync(metadata, 'utf8'));
82
+ const aliased = value?.kind === 'aglit-cli-desktop-runtime-v3';
83
+ const chunked = aliased || value?.kind === 'aglit-cli-desktop-runtime-v2';
84
+ const keys = chunked ? 'archive,chunks,kind,platform,sourceCommit,version' : 'archive,kind,platform,sourceCommit,version';
85
+ if (!value || Object.keys(value).sort().join(',') !== keys ||
86
+ !['aglit-cli-desktop-runtime-v1', 'aglit-cli-desktop-runtime-v2', 'aglit-cli-desktop-runtime-v3'].includes(value.kind) || value.platform !== platform ||
87
+ !/^\d+\.\d+\.\d+$/.test(value.version) || !/^[a-f0-9]{40}$/.test(value.sourceCommit))
88
+ throw Error('Bundled Desktop identity differs');
89
+ const validAsset = asset => asset && Number.isSafeInteger(asset.size) && asset.size > 0 && asset.size <= 2 * 1024 ** 3 &&
90
+ /^[a-f0-9]{64}$/.test(asset.sha256) && /^[A-Za-z0-9+/]{86}==$/.test(asset.sha512);
91
+ const asset = value.archive;
92
+ if (!validAsset(asset) || Object.keys(asset).sort().join(',') !== 'name,sha256,sha512,size' || asset.name !== 'desktop.zip')
93
+ throw Error('Invalid bundled Desktop archive');
94
+ const chunks = [];
95
+ if (aliased) {
96
+ const descriptor = JSON.parse(fs.readFileSync(path.join(path.dirname(directory), 'package.json'), 'utf8'));
97
+ const descriptorVersion = value.version + '-runtime.' + (platform === 'mac' ? 'darwin-arm64' : 'win32-x64');
98
+ if (descriptor.name !== '@aglit-ai/cli' || descriptor.version !== descriptorVersion || !descriptor.dependencies ||
99
+ Object.keys(descriptor.dependencies).length !== value.chunks?.length) throw Error('Desktop descriptor package differs');
100
+ for (const part of value.chunks || []) if (descriptor.dependencies[part.installName] !== 'npm:@aglit-ai/cli@' + part.version)
101
+ throw Error('Desktop part dependency alias differs');
102
+ }
103
+ if (chunked) {
104
+ if (!Array.isArray(value.chunks) || !value.chunks.length || value.chunks.length > 64) throw Error('Invalid Desktop parts');
105
+ const resolver = createRequire(path.join(path.dirname(directory), 'package.json'));
106
+ for (let index = 0; index < value.chunks.length; index++) {
107
+ const part = value.chunks[index];
108
+ const name = packageName + '-part-' + String(index).padStart(2, '0');
109
+ const internalVersion = value.version + '-runtime.' + (platform === 'mac' ? 'darwin-arm64' : 'win32-x64') + '.part-' + String(index).padStart(2, '0');
110
+ const expectedFields = aliased ? 'file,installName,package,sha256,sha512,size,version' : 'file,package,sha256,sha512,size,version';
111
+ if (!validAsset(part) || Object.keys(part).sort().join(',') !== expectedFields ||
112
+ part.package !== (aliased ? '@aglit-ai/cli' : name) || part.version !== (aliased ? internalVersion : value.version) ||
113
+ aliased && part.installName !== name || part.file !== 'runtime/part.bin' || part.size > 64 * 1024 ** 2)
114
+ throw Error('Invalid Desktop part identity');
115
+ const manifest = resolver.resolve((aliased ? part.installName : part.package) + '/package.json'); canonical(manifest);
116
+ const identity = JSON.parse(fs.readFileSync(manifest, 'utf8'));
117
+ if (identity.name !== part.package || identity.version !== part.version) throw Error('Desktop part package differs');
118
+ const archive = path.join(path.dirname(manifest), part.file); canonical(archive);
119
+ if (!fs.statSync(archive).isFile() || fs.statSync(archive).size !== part.size) throw Error('Desktop part size differs');
120
+ chunks.push({ ...part, archive });
121
+ }
122
+ if (chunks.reduce((sum, part) => sum + part.size, 0) !== asset.size) throw Error('Desktop parts do not cover archive');
123
+ } else {
124
+ const archive = path.join(directory, asset.name); canonical(archive);
125
+ if (!fs.statSync(archive).isFile() || fs.statSync(archive).size !== asset.size) throw Error('Bundled Desktop archive size differs');
126
+ chunks.push({ ...asset, archive });
127
+ }
128
+ return { release: value, asset, chunks };
129
+ }
130
+ async function copyArchive(bundle, archive) {
131
+ const output = fs.createWriteStream(archive, { flags: 'wx', mode: 0o600 });
132
+ const sha256 = crypto.createHash('sha256'), sha512 = crypto.createHash('sha512');
133
+ let received = 0;
134
+ try {
135
+ for (const part of bundle.chunks) {
136
+ const before = fs.statSync(part.archive);
137
+ const part256 = crypto.createHash('sha256'), part512 = crypto.createHash('sha512'); let size = 0;
138
+ for await (const chunk of fs.createReadStream(part.archive)) {
139
+ size += chunk.length; received += chunk.length;
140
+ if (size > part.size || received > bundle.asset.size) throw Error('Desktop archive exceeds declared size');
141
+ part256.update(chunk); part512.update(chunk); sha256.update(chunk); sha512.update(chunk);
142
+ if (!output.write(chunk)) await once(output, 'drain');
143
+ }
144
+ const after = fs.statSync(part.archive);
145
+ if (size !== part.size || part256.digest('hex') !== part.sha256 || part512.digest('base64') !== part.sha512 ||
146
+ ['dev', 'ino', 'size', 'mtimeMs', 'ctimeMs'].some(key => before[key] !== after[key])) throw Error('Desktop part checksum differs; archive was not executed');
147
+ }
148
+ output.end(); await once(output, 'finish');
149
+ if (received !== bundle.asset.size || sha256.digest('hex') !== bundle.asset.sha256 || sha512.digest('base64') !== bundle.asset.sha512)
150
+ throw Error('Desktop archive checksum differs; archive was not executed');
151
+ } catch (error) { output.destroy(); throw error; }
152
+ }
153
+ async function stageRuntime(bundle, runtime) {
154
+ const archive = path.join(path.dirname(runtime), 'runtime.zip');
155
+ await copyArchive(bundle, archive);
156
+ fs.mkdirSync(runtime, { mode: 0o700 });
157
+ await require('./extract.cjs').extract(archive, runtime);
158
+ const executable = process.platform === 'darwin' ? path.join(runtime, 'Aglit AI.app/Contents/MacOS/Aglit AI') : path.join(runtime, 'Aglit AI.exe');
159
+ verifyRuntime(executable, bundle.release);
160
+ fs.unlinkSync(archive);
161
+ return { executable, version: bundle.release.version, sourceCommit: bundle.release.sourceCommit, sha256: bundle.asset.sha256 };
162
+ }
163
+ function verifyRuntime(executable, release) {
164
+ canonical(executable); verifyPublisher(executable);
165
+ const resources = process.platform === 'darwin' ? path.resolve(path.dirname(executable), '../Resources') : path.join(path.dirname(executable), 'resources');
166
+ const buildInfoPath = path.join(resources, 'app.asar.unpacked/dist-electron/desktop-app/build-info.json'); canonical(buildInfoPath);
167
+ if (fs.statSync(buildInfoPath).size > 16 * 1024 * 1024) throw Error('Build metadata too large');
168
+ const info = JSON.parse(fs.readFileSync(buildInfoPath, 'utf8'));
169
+ if (info.version !== release.version || info.gitCommit !== release.sourceCommit || info.dirty !== false || info.runtimeMinified !== true || info.computerUseControlVersion !== 1)
170
+ throw Error('A published obfuscated desktop release with Computer Use CLI support is required; this archive was not launched');
171
+ return {version: info.version, sourceCommit: info.gitCommit};
172
+ }
173
+ async function install(channel) {
174
+ if (channel !== undefined && !['latest', 'canary', 'embedded'].includes(channel)) throw Error('Invalid npm channel');
112
175
  if (fs.existsSync(path.join(root, 'uninstalling.json'))) throw Error('Finish aglit uninstall before reinstalling');
113
176
  if (!(process.platform === 'darwin' && process.arch === 'arm64') && !(process.platform === 'win32' && process.arch === 'x64')) throw Error('Published Computer Use releases currently support Apple silicon Macs and Windows x64');
114
177
  if (fs.existsSync(receiptPath)) return { installed: true, ...readInstallation() };
115
178
  if (fs.existsSync(root)) throw Error(`Existing unowned installation directory preserved: ${root}`);
116
179
  const platform = process.platform === 'darwin' ? 'mac' : 'win';
117
- const base = 'https://github.com/Aglit-AI/desktop-app-release/releases/download/';
118
- const desired = parseRelease(await json(base + `updates-${platform}/github-release-${platform}.json`), platform);
119
- const release = parseRelease(await json(base + `v${desired.version}/github-release-${platform}.json`), platform);
120
- if (JSON.stringify(desired) !== JSON.stringify(release)) throw Error('Release pointer differs from immutable release');
121
- // The product's unsigned Windows policy is not silently inherited by the CLI.
122
- if (release.windowsSigningPolicy) throw Error('This Windows release is unsigned; Computer Use installation requires a signed release');
180
+ let bundled = embeddedRelease(platform), bootstrap;
181
+ if (!bundled) {
182
+ bootstrap = await require('./updates.cjs').bootstrap(channel);
183
+ bundled = embeddedRelease(platform, bootstrap.packageDirectory);
184
+ if (!bundled) throw Error('Published npm CLI has no runtime for this platform');
185
+ }
123
186
  canonical(path.dirname(root));
124
- fs.mkdirSync(root, { mode: 0o700 });
125
- privateRoot(true);
187
+ fs.mkdirSync(root, { mode: 0o700 }); privateRoot(true);
126
188
  fs.writeFileSync(path.join(root, 'installation-attempt.json'), JSON.stringify({ kind: 'aglit-computer-use-attempt-v1', root }) + '\n', { flag: 'wx', mode: 0o600 });
127
- const asset = release.assets.find(item => item.name.endsWith('.zip'));
128
- const archive = path.join(root, 'runtime.zip');
129
- let received = 0;
130
- const sha256 = crypto.createHash('sha256'), sha512 = crypto.createHash('sha512');
131
- const meter = new Transform({ transform(chunk, _encoding, done) {
132
- received += chunk.length;
133
- if (received > asset.size) return done(Error('Release exceeds declared size'));
134
- sha256.update(chunk); sha512.update(chunk); done(null, chunk);
135
- } });
136
- const response = await download(base + `v${release.version}/${asset.name}`);
137
- await pipeline(Readable.fromWeb(response.body), meter, fs.createWriteStream(archive, { flags: 'wx', mode: 0o600 }));
138
- if (received !== asset.size || sha256.digest('hex') !== asset.sha256 || sha512.digest('base64') !== asset.sha512) throw Error('Release checksum differs; archive was not executed');
139
- const runtime = path.join(root, 'runtime');
140
- fs.mkdirSync(runtime, { mode: 0o700 });
141
- await require('./extract.cjs').extract(archive, runtime);
142
- const executable = process.platform === 'darwin' ? path.join(runtime, 'Aglit AI.app/Contents/MacOS/Aglit AI') : path.join(runtime, 'Aglit AI.exe');
143
- canonical(executable);
144
- verifyPublisher(executable);
145
- const resources = process.platform === 'darwin' ? path.join(runtime, 'Aglit AI.app/Contents/Resources') : path.join(runtime, 'resources');
146
- const buildInfoPath = path.join(resources, 'app.asar.unpacked/dist-electron/desktop-app/build-info.json');
147
- canonical(buildInfoPath);
148
- if (fs.statSync(buildInfoPath).size > 16 * 1024 * 1024) throw Error('Build metadata too large');
149
- const buildInfo = JSON.parse(fs.readFileSync(buildInfoPath, 'utf8'));
150
- if (buildInfo.gitCommit !== release.sourceCommit || buildInfo.dirty !== false ||
151
- buildInfo.runtimeMinified !== true || buildInfo.computerUseControlVersion !== 1)
152
- throw Error('A published obfuscated desktop release with Computer Use CLI support is required; this archive was not launched');
189
+ const staged = await stageRuntime(bundled, path.join(root, 'runtime'));
153
190
  fs.mkdirSync(profile, { mode: 0o700 });
154
191
  fs.writeFileSync(path.join(profile, 'system-settings.json'), JSON.stringify({ settings: { enableAnywhereAccess: false, remoteAccessDeviceId: crypto.randomUUID() } }) + '\n', { flag: 'wx', mode: 0o600 });
155
- const receipt = { kind: 'aglit-computer-use-installation-v1', root, profile, executable, version: release.version, sourceCommit: release.sourceCommit, sha256: asset.sha256 };
192
+ const receipt = { kind: 'aglit-computer-use-installation-v1', root, profile, ...staged };
156
193
  fs.writeFileSync(receiptPath, JSON.stringify(receipt) + '\n', { flag: 'wx', mode: 0o600 });
157
- fs.unlinkSync(archive);
194
+ if (bootstrap) require('./updates.cjs').adoptBootstrap(bootstrap);
158
195
  return { installed: true, ...receipt };
159
196
  }
160
197
  function start() {
@@ -171,6 +208,8 @@ function start() {
171
208
  const child = spawn(installed.executable, ['--computer-use'], { detached: true, windowsHide: true, stdio: ['ignore', log, log], env });
172
209
  child.on('error', error => { process.stderr.write(`Computer Use failed to start: ${error.message}\n`); });
173
210
  child.unref(); fs.closeSync(log);
211
+ if (!Number.isSafeInteger(child.pid)) throw Error('Desktop process did not start');
212
+ return { pid: child.pid };
174
213
  }
175
214
  function purge() {
176
215
  readInstallation(true);
@@ -211,4 +250,4 @@ function readUninstall() {
211
250
  !Number.isSafeInteger(receipt.pid) || receipt.pid <= 0) throw Error('Invalid uninstall receipt');
212
251
  return receipt;
213
252
  }
214
- module.exports = { root, profile, readInstallation, install, start, purge, purgeIncomplete, readUninstall };
253
+ module.exports = { root, profile, readInstallation, install, start, purge, purgeIncomplete, readUninstall, privateRoot, canonical, verifyPublisher, embeddedRelease, copyArchive, stageRuntime, verifyRuntime };
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+ // Copied to the private installation root. It has no npm dependencies and its
3
+ // filename remains fixed while versioned CLI/runtime generations are replaced.
4
+ const fs = require('node:fs'), path = require('node:path'), crypto = require('node:crypto');
5
+ const {spawnSync} = require('node:child_process');
6
+ try {
7
+ const root = __dirname, record = path.join(root, 'cli-installation.json');
8
+ const stat = fs.lstatSync(record);
9
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 65536 || stat.nlink !== 1 || fs.realpathSync(root) !== root ||
10
+ process.platform !== 'win32' && (stat.uid !== process.getuid() || stat.mode & 0o077)) throw Error();
11
+ const updating = path.join(root, 'updating.lock');
12
+ if (fs.existsSync(updating)) {
13
+ const lock = fs.lstatSync(updating);
14
+ if (!lock.isFile() || lock.isSymbolicLink() || lock.size > 65536 || process.platform !== 'win32' && (lock.uid !== process.getuid() || lock.mode & 0o077)) throw Error();
15
+ const owner = JSON.parse(fs.readFileSync(updating, 'utf8'));
16
+ if (!Number.isSafeInteger(owner.pid) || owner.pid <= 0) throw Error();
17
+ try { process.kill(owner.pid, 0); process.stdout.write('Aglit update is active; startup is deferred.\n'); process.exit(0); } catch (error) { if (error.code !== 'ESRCH') throw error; }
18
+ }
19
+ const value = JSON.parse(fs.readFileSync(record, 'utf8')), current = value.current;
20
+ if (value.kind !== 'aglit-cli-installation-v1' || !current || typeof current.directory !== 'string' ||
21
+ !current.directory.startsWith(path.join(root, 'cli') + path.sep) || !/^[a-f0-9]{64}$/.test(current.sha256)) throw Error();
22
+ const entry = path.join(current.directory, 'node_modules/@aglit-ai/cli/bin/aglit.cjs');
23
+ const file = fs.lstatSync(entry);
24
+ if (!file.isFile() || file.isSymbolicLink() || fs.realpathSync(entry) !== entry || file.size > 1024 * 1024 ||
25
+ crypto.createHash('sha256').update(fs.readFileSync(entry)).digest('hex') !== current.sha256) throw Error();
26
+ const result = spawnSync(process.execPath, [entry, ...process.argv.slice(2)], {stdio: 'inherit', windowsHide: true});
27
+ process.exitCode = result.status ?? 1;
28
+ } catch {
29
+ process.stderr.write('Aglit private CLI launcher is unavailable. Reinstall @aglit-ai/cli to repair it.\n'); process.exitCode = 1;
30
+ }
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+ const fs = require('node:fs'), path = require('node:path');
3
+ const updates = require('./updates.cjs'), installation = require('./installation.cjs');
4
+ const {control} = require('./control.cjs');
5
+ const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
6
+ const config = path.join(installation.profile, 'ipc/sdk-ipc.json');
7
+ async function main() {
8
+ const expectedPid = Number(process.argv[2]);
9
+ if (process.argv.length !== 3 || !Number.isSafeInteger(expectedPid) || expectedPid <= 0) throw Error('Invalid updater runtime');
10
+ installation.readInstallation(); const unlock = updates.lock(path.join(installation.root, 'updater-worker.json'));
11
+ if (!unlock) return;
12
+ let replacement = null;
13
+ try {
14
+ let nextCheck = 0;
15
+ for (;;) {
16
+ if (fs.existsSync(path.join(installation.root, 'uninstalling.json')) || fs.existsSync(path.join(installation.root, 'updater-stop.json')) || !updates.state().automatic || !updates.live(expectedPid)) return;
17
+ let runtime; try { runtime = await control(config, 'status'); } catch { return; }
18
+ if (!runtime?.running || runtime.pid !== expectedPid) return;
19
+ updates.rememberStatus(runtime);
20
+ if (Date.now() >= nextCheck) {
21
+ nextCheck = Date.now() + 15 * 60000;
22
+ try {
23
+ const result = await updates.update();
24
+ if (result.runtimePid && result.runtimePid !== expectedPid) { replacement = result.runtimePid; return; }
25
+ if (result.updated) { replacement = expectedPid; return; }
26
+ if (result.deferred) nextCheck = Date.now() + 60000;
27
+ } catch {
28
+ process.stderr.write('Aglit automatic update deferred; current runtime and profile preserved.\n');
29
+ }
30
+ }
31
+ for (let i = 0; i < 30 && !fs.existsSync(path.join(installation.root, 'updater-stop.json')); i++) await delay(1000);
32
+ }
33
+ } finally {
34
+ unlock(); if (replacement && !fs.existsSync(path.join(installation.root, 'updater-stop.json'))) updates.ensureWorker(replacement);
35
+ }
36
+ }
37
+ main().catch(() => { process.stderr.write('Aglit updater stopped; run aglit update to inspect or retry.\n'); process.exitCode = 1; });
@@ -0,0 +1,535 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const os = require('node:os');
5
+ const crypto = require('node:crypto');
6
+ const { createRequire } = require('node:module');
7
+ const { spawn, spawnSync, execFileSync } = require('node:child_process');
8
+ const installation = require('./installation.cjs');
9
+ const { control } = require('./control.cjs');
10
+ const ownPackage = path.resolve(__dirname, '..');
11
+ const identity = JSON.parse(fs.readFileSync(path.join(ownPackage, 'package.json'), 'utf8'));
12
+ const channels = ['latest', 'canary', 'embedded'];
13
+ const registry = 'https://registry.npmjs.org/';
14
+ const stateFile = path.join(installation.root, 'cli-installation.json');
15
+ const settingsFile = path.join(installation.root, 'cli-update-settings.json');
16
+ const need = (value, message) => { if (!value) throw Error(message); };
17
+ const sha = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
18
+ const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
19
+ function compareVersions(left, right) {
20
+ const parse = value => {
21
+ need(typeof value === 'string' && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[a-z0-9]+(?:[.-][a-z0-9]+)*)?$/.test(value), 'Invalid npm version');
22
+ const [core, ...suffix] = value.split('-'); const numbers = core.split('.').map(Number);
23
+ need(numbers.every(Number.isSafeInteger), 'Invalid npm version'); return { numbers, suffix: suffix.join('-') };
24
+ };
25
+ const a = parse(left), b = parse(right);
26
+ for (let index = 0; index < 3; index++) if (a.numbers[index] !== b.numbers[index]) return Math.sign(a.numbers[index] - b.numbers[index]);
27
+ if (!a.suffix || !b.suffix) return a.suffix === b.suffix ? 0 : a.suffix ? -1 : 1;
28
+ const x = a.suffix.split('.'), y = b.suffix.split('.');
29
+ for (let index = 0; index < Math.max(x.length, y.length); index++) {
30
+ if (x[index] === y[index]) continue;
31
+ if (x[index] === undefined || y[index] === undefined) return x[index] === undefined ? -1 : 1;
32
+ const xn = /^\d+$/.test(x[index]), yn = /^\d+$/.test(y[index]);
33
+ if (xn && yn) return BigInt(x[index]) > BigInt(y[index]) ? 1 : -1;
34
+ if (xn !== yn) return xn ? -1 : 1;
35
+ return x[index] > y[index] ? 1 : -1;
36
+ }
37
+ return 0;
38
+ }
39
+ function privateJson(file) {
40
+ installation.canonical(file); const stat = fs.statSync(file);
41
+ need(stat.isFile() && stat.nlink === 1 && stat.size <= 65536 &&
42
+ (process.platform === 'win32' || stat.uid === process.getuid() && !(stat.mode & 0o077)), 'Private updater record required');
43
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
44
+ }
45
+ function writeJson(file, value) {
46
+ const stage = file + '.' + crypto.randomUUID();
47
+ fs.writeFileSync(stage, JSON.stringify(value) + '\n', {flag: 'wx', mode: 0o600});
48
+ try { fs.renameSync(stage, file); } finally { if (fs.existsSync(stage)) fs.unlinkSync(stage); }
49
+ }
50
+ function packageDirectory(prefix) { return path.join(prefix, 'node_modules', '@aglit-ai', 'cli'); }
51
+ function pointer(prefix, manifest, integrity = null) {
52
+ const entry = path.join(packageDirectory(prefix), 'bin/aglit.cjs'); installation.canonical(entry);
53
+ return { directory: prefix, version: manifest.version, integrity, sha256: sha(fs.readFileSync(entry)) };
54
+ }
55
+ function validatePointer(value) {
56
+ need(value && Object.keys(value).sort().join(',') === 'directory,integrity,sha256,version' &&
57
+ value.directory.startsWith(path.join(installation.root, 'cli') + path.sep) && /^[a-f0-9]{64}$/.test(value.sha256) &&
58
+ (value.integrity === null || /^sha512-[A-Za-z0-9+/]{86}==$/.test(value.integrity)), 'Invalid private CLI installation');
59
+ compareVersions(value.version, value.version);
60
+ const entry = path.join(packageDirectory(value.directory), 'bin/aglit.cjs'); installation.canonical(entry);
61
+ need(sha(fs.readFileSync(entry)) === value.sha256, 'Installed CLI entry changed'); return value;
62
+ }
63
+ function settings() {
64
+ if (!fs.existsSync(settingsFile)) return {channel: 'latest', automatic: true, revision: 0};
65
+ const value = privateJson(settingsFile);
66
+ need(value.kind === 'aglit-cli-update-settings-v1' && Object.keys(value).sort().join(',') === 'automatic,channel,kind,revision' && channels.includes(value.channel) &&
67
+ typeof value.automatic === 'boolean' && Number.isSafeInteger(value.revision) && value.revision >= 0, 'Invalid CLI update settings');
68
+ return {channel: value.channel, automatic: value.automatic, revision: value.revision};
69
+ }
70
+ function saveSettings(patch) {
71
+ const unlock = lock(path.join(installation.root, 'updater-settings.lock')); need(unlock, 'Update settings are busy; retry the command');
72
+ try { const value = {...settings(), ...patch}; writeJson(settingsFile, {kind: 'aglit-cli-update-settings-v1', ...value, revision: value.revision + 1}); }
73
+ finally { unlock(); }
74
+ }
75
+ function savePointer(value) { writeJson(stateFile, {kind: 'aglit-cli-installation-v1', current: value.current, previous: value.previous}); }
76
+ function state() {
77
+ const policy = settings();
78
+ if (!fs.existsSync(stateFile)) return {kind: 'aglit-cli-installation-v1', ...policy, current: null, previous: null};
79
+ installation.privateRoot(); const value = privateJson(stateFile);
80
+ need(value && Object.keys(value).sort().join(',') === 'current,kind,previous' && value.kind === 'aglit-cli-installation-v1', 'Invalid private CLI state');
81
+ if (value.current) validatePointer(value.current);
82
+ if (value.previous) validatePointer(value.previous);
83
+ return {...value, ...policy};
84
+ }
85
+ function resolvePackage(name, from) {
86
+ const requireFrom = createRequire(path.join(from, 'package.json')); let entry;
87
+ try { return path.dirname(requireFrom.resolve(name + '/package.json')); } catch (error) { if (!['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) throw error; }
88
+ entry = requireFrom.resolve(name);
89
+ for (let directory = path.dirname(entry), count = 0; count < 20; directory = path.dirname(directory), count++) {
90
+ const file = path.join(directory, 'package.json');
91
+ if (fs.existsSync(file) && JSON.parse(fs.readFileSync(file)).name === name) return directory;
92
+ if (path.dirname(directory) === directory) break;
93
+ }
94
+ throw Error('Installed CLI dependency is unavailable');
95
+ }
96
+ function copyPackage(source, target, cli = false, ancestors = []) {
97
+ const manifest = JSON.parse(fs.readFileSync(path.join(source, 'package.json')));
98
+ const key = manifest.name + '@' + manifest.version;
99
+ need(!ancestors.includes(key) && ancestors.length < 16, 'CLI dependency graph cannot be copied safely');
100
+ fs.mkdirSync(target, {recursive: true, mode: 0o700});
101
+ const names = cli ? ['package.json', 'README.md', 'bin', 'src'] : fs.readdirSync(source).filter(name => name !== 'node_modules');
102
+ for (const name of names) fs.cpSync(path.join(source, name), path.join(target, name), {recursive: true, dereference: false, errorOnExist: true, force: false});
103
+ for (const name of Object.keys(manifest.dependencies || {})) {
104
+ need(/^(@[a-z0-9-]+\/)?[a-z0-9][a-z0-9._-]*$/.test(name), 'Invalid CLI dependency');
105
+ copyPackage(resolvePackage(name, source), path.join(target, 'node_modules', name), false, [...ancestors, key]);
106
+ }
107
+ }
108
+ function writeStartup() {
109
+ const launcher = path.join(installation.root, 'cli-launcher.cjs');
110
+ const source = fs.readFileSync(path.join(__dirname, 'launcher.cjs'));
111
+ const staged = launcher + '.' + crypto.randomUUID(); fs.writeFileSync(staged, source, {flag: 'wx', mode: 0o600}); fs.renameSync(staged, launcher);
112
+ writeJson(path.join(installation.root, 'cli-startup.json'), {kind: 'aglit-computer-use-cli-startup-v1', nodeExecutable: fs.realpathSync(process.execPath), launcher});
113
+ }
114
+ function initialize(channel) {
115
+ need(channel === undefined || channels.includes(channel), 'Invalid update channel');
116
+ installation.readInstallation(); let value = state();
117
+ if (channel && channel !== value.channel) { saveSettings({channel}); value = state(); }
118
+ const updating = path.join(installation.root, 'updating.lock');
119
+ if (fs.existsSync(updating)) { const owner = privateJson(updating); if (owner.pid !== process.pid && live(owner.pid)) return value; }
120
+ if (!value.current || compareVersions(identity.version, value.current.version) > 0) {
121
+ const parent = path.join(installation.root, 'cli'); fs.mkdirSync(parent, {recursive: true, mode: 0o700});
122
+ const prefix = path.join(parent, identity.version + '-' + crypto.randomUUID());
123
+ copyPackage(ownPackage, packageDirectory(prefix), true);
124
+ const current = pointer(prefix, identity); value = {...value, previous: value.current, current}; savePointer(value);
125
+ }
126
+ writeStartup(); return value;
127
+ }
128
+ function delegate(entry) {
129
+ if (!fs.existsSync(stateFile)) return null;
130
+ const current = state().current;
131
+ if (!current || compareVersions(current.version, identity.version) <= 0) return null;
132
+ const target = path.join(packageDirectory(current.directory), 'bin/aglit.cjs');
133
+ if (fs.realpathSync(entry) === fs.realpathSync(target)) return null;
134
+ const result = spawnSync(process.execPath, [target, ...process.argv.slice(2)], {stdio: 'inherit', windowsHide: true});
135
+ if (result.error) throw Error('Updated CLI could not start'); return result.status ?? 1;
136
+ }
137
+ async function manifest(name, selector) {
138
+ need(/^@aglit-ai\/(?:cli|desktop-runtime-(?:darwin-arm64|win32-x64)(?:-part-\d{2})?)$/.test(name), 'Unexpected npm package');
139
+ if (!channels.includes(selector) && !/^runtime-(?:darwin-arm64|win32-x64)-(?:latest|canary|embedded)$/.test(selector)) compareVersions(selector, selector);
140
+ const url = new URL(encodeURIComponent(name) + '/' + encodeURIComponent(selector), registry);
141
+ const response = await fetch(url, {redirect: 'error', signal: AbortSignal.timeout(30000)});
142
+ if (response.status === 404) {
143
+ if (/^runtime-(?:darwin-arm64|win32-x64)-/.test(selector)) throw Error('No published signed Desktop runtime is available for ' + process.platform + ' ' + process.arch + ' in npm channel ' + selector);
144
+ if (name === '@aglit-ai/cli') throw Error('The Aglit CLI npm release ' + selector + ' has not been published yet');
145
+ if (name === runtimePackage()) throw Error('No published signed Desktop runtime is available for ' + process.platform + ' ' + process.arch + ' in npm channel/version ' + selector);
146
+ throw Error('A required verified Desktop runtime part is not published yet');
147
+ }
148
+ need(response.ok && response.body, 'Published npm release is unavailable');
149
+ let size = 0; const chunks = [];
150
+ for await (const chunk of response.body) { size += chunk.length; need(size <= 2 * 1024 ** 2, 'npm metadata is too large'); chunks.push(chunk); }
151
+ const value = JSON.parse(Buffer.concat(chunks).toString('utf8'));
152
+ need(value.name === name && value._npmUser?.name === 'aglit-ai-dev' && value._npmUser?.email === 'developers@aglit.ai' &&
153
+ /^sha512-[A-Za-z0-9+/]{86}==$/.test(value.dist?.integrity), 'Company npm publisher and integrity required');
154
+ compareVersions(value.version, value.version);
155
+ const tarball = new URL(value.dist.tarball); need(tarball.protocol === 'https:' && tarball.hostname === 'registry.npmjs.org' && !tarball.username && !tarball.password, 'Untrusted npm archive');
156
+ return value;
157
+ }
158
+ function runtimePackage() {
159
+ need(process.platform === 'darwin' && process.arch === 'arm64' || process.platform === 'win32' && process.arch === 'x64', 'Published runtime does not support this platform');
160
+ return '@aglit-ai/desktop-runtime-' + process.platform + '-' + process.arch;
161
+ }
162
+ function runtimeVersion(value) {
163
+ if (value.name === runtimePackage()) { compareVersions(value.version, value.version); return value.version; }
164
+ const expected = process.platform + '-' + process.arch;
165
+ const match = /^(\d+\.\d+\.\d+)-runtime\.(darwin-arm64|win32-x64)$/.exec(value.version);
166
+ need(value.name === '@aglit-ai/cli' && match && match[2] === expected, 'Runtime npm version does not match this platform');
167
+ return match[1];
168
+ }
169
+ function runtimeTag(channel) { runtimePackage(); need(channels.includes(channel), 'Invalid update channel'); return 'runtime-' + process.platform + '-' + process.arch + '-' + channel; }
170
+ async function releaseManifests(cli, offeredRuntime) {
171
+ const result = [{...cli, installName: '@aglit-ai/cli'}], name = runtimePackage();
172
+ const pin = cli.optionalDependencies?.[name];
173
+ let runtime = offeredRuntime;
174
+ if (!runtime) {
175
+ need(typeof pin === 'string', 'Published CLI has no runtime for this platform');
176
+ const alias = /^npm:@aglit-ai\/cli@(\d+\.\d+\.\d+-runtime\.(?:darwin-arm64|win32-x64))$/.exec(pin);
177
+ if (alias) runtime = await manifest('@aglit-ai/cli', alias[1]);
178
+ else { need(/^\d+\.\d+\.\d+$/.test(pin), 'Runtime bootstrap pin differs'); runtime = await manifest(name, pin); }
179
+ }
180
+ const nativeVersion = runtimeVersion(runtime), aliased = runtime.name === '@aglit-ai/cli';
181
+ result.push({...runtime, installName: name});
182
+ const entries = Object.entries(runtime.dependencies || {});
183
+ need(entries.length > 0 && entries.length <= 64, 'Runtime part dependencies are unavailable');
184
+ for (let index = 0; index < entries.length; index++) {
185
+ const installName = name + '-part-' + String(index).padStart(2, '0');
186
+ const version = aliased ? runtime.version + '.part-' + String(index).padStart(2, '0') : nativeVersion;
187
+ need(runtime.dependencies[installName] === (aliased ? 'npm:@aglit-ai/cli@' : '') + version, 'Runtime part alias or pin differs');
188
+ const part = await manifest(aliased ? '@aglit-ai/cli' : installName, version);
189
+ need(part.version === version && !Object.keys(part.dependencies || {}).length, 'Runtime part package differs');
190
+ result.push({...part, installName});
191
+ }
192
+ return result;
193
+ }
194
+ function npmEntry() {
195
+ const node = fs.realpathSync(process.execPath), directory = path.dirname(node);
196
+ const candidates = [path.join(directory, 'node_modules/npm/bin/npm-cli.js'), path.resolve(directory, '../lib/node_modules/npm/bin/npm-cli.js')];
197
+ for (const file of candidates) if (fs.existsSync(file)) return fs.realpathSync(file);
198
+ const nearby = path.join(path.dirname(process.execPath), process.platform === 'win32' ? 'npm.cmd' : 'npm');
199
+ if (process.platform !== 'win32' && fs.existsSync(nearby)) return fs.realpathSync(nearby);
200
+ throw Error('npm must be installed with Node.js to update Aglit');
201
+ }
202
+ async function stage(cli, parent, offeredRuntime, cliOnly = false) {
203
+ need(cli.name === '@aglit-ai/cli' && cli.bin?.aglit === 'bin/aglit.cjs' && !/-runtime\.(?:darwin-arm64|win32-x64)/.test(cli.version), 'Selected npm version is not the Aglit CLI launcher');
204
+ const manifests = cliOnly ? [{...cli, installName: '@aglit-ai/cli'}] : await releaseManifests(cli, offeredRuntime);
205
+ const runtime = manifests[1];
206
+ const directory = fs.mkdtempSync(path.join(parent, 'aglit-cli-')); fs.chmodSync(directory, 0o700);
207
+ fs.writeFileSync(path.join(directory, 'package.json'), '{"private":true}\n', {mode: 0o600});
208
+ const config = path.join(directory, 'npmrc'); fs.writeFileSync(config, 'registry=' + registry + '\n@aglit-ai:registry=' + registry + '\n', {mode: 0o600});
209
+ const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !/^npm_config_|^node_auth_token$|^npm_token$|^node_options$|^node_path$/i.test(key)));
210
+ await new Promise((resolve, reject) => {
211
+ const child = spawn(process.execPath, [npmEntry(), 'install', '--ignore-scripts', '--omit=optional', '--no-audit', '--no-fund', '--save-exact', '--package-lock=true', '--registry=' + registry,
212
+ '--userconfig=' + config, '--globalconfig=' + (process.platform === 'win32' ? 'NUL' : '/dev/null'), cli.name + '@' + cli.version, ...(runtime ? [runtime.installName + '@' + (runtime.name === runtime.installName ? runtime.version : 'npm:' + runtime.name + '@' + runtime.version)] : [])], {cwd: directory, env, windowsHide: true, stdio: ['ignore', 'ignore', 'ignore']});
213
+ const timeout = setTimeout(() => { child.kill(); reject(Error('npm update download timed out; current runtime preserved')); }, 30 * 60000);
214
+ const cancellation = setInterval(() => { if (fs.existsSync(path.join(installation.root, 'updater-stop.json'))) child.kill(); }, 1000);
215
+ child.on('error', () => { clearInterval(cancellation); clearTimeout(timeout); reject(Error('npm update could not start')); });
216
+ child.on('exit', code => { clearInterval(cancellation); clearTimeout(timeout); code === 0 ? resolve() : reject(Error('npm update download failed; current runtime preserved')); });
217
+ });
218
+ validateInstalledGraph(directory, manifests);
219
+ fs.unlinkSync(config); return {directory, packageDirectory: packageDirectory(directory), runtimeDirectory: runtime ? path.join(directory, 'node_modules', runtime.installName) : null, cli};
220
+ }
221
+ function validateInstalledGraph(directory, manifests) {
222
+ const lock = JSON.parse(fs.readFileSync(path.join(directory, 'package-lock.json'), 'utf8'));
223
+ for (const expected of manifests) {
224
+ const entry = lock.packages?.['node_modules/' + expected.installName];
225
+ need(entry?.version === expected.version && entry.integrity === expected.dist.integrity, 'Downloaded npm package integrity differs');
226
+ const installed = JSON.parse(fs.readFileSync(path.join(directory, 'node_modules', expected.installName, 'package.json')));
227
+ need(installed.name === expected.name && installed.version === expected.version, 'Installed npm package identity differs');
228
+ }
229
+ return true;
230
+ }
231
+ async function bootstrap(channel) {
232
+ const selected = channel || state().channel;
233
+ need(channels.includes(selected), 'Invalid update channel');
234
+ const offered = await offers(selected);
235
+ return {...await stage(offered.cli, fs.realpathSync(os.tmpdir()), offered.runtime), channel: selected};
236
+ }
237
+ function adoptBootstrap(value) {
238
+ const parent = path.join(installation.root, 'cli'); fs.mkdirSync(parent, {recursive: true, mode: 0o700});
239
+ const target = path.join(parent, value.cli.version + '-' + crypto.randomUUID());
240
+ try { fs.renameSync(value.directory, target); } catch (error) { if (error.code !== 'EXDEV') throw error; fs.cpSync(value.directory, target, {recursive: true}); fs.rmSync(value.directory, {recursive: true}); }
241
+ const prior = state(); saveSettings({channel: value.channel}); savePointer({...prior, current: pointer(target, value.cli, value.cli.dist.integrity), previous: prior.current}); writeStartup();
242
+ }
243
+ function live(pid) { try { process.kill(pid, 0); return true; } catch (error) { if (error.code === 'ESRCH') return false; throw error; } }
244
+ function lock(file) {
245
+ if (fs.existsSync(file)) {
246
+ const value = privateJson(file); need(Number.isSafeInteger(value.pid) && value.pid > 0, 'Invalid updater lock');
247
+ if (live(value.pid)) return null; fs.unlinkSync(file);
248
+ }
249
+ const nonce = crypto.randomUUID();
250
+ try { fs.writeFileSync(file, JSON.stringify({pid: process.pid, nonce}) + '\n', {flag: 'wx', mode: 0o600}); }
251
+ catch (error) { if (error.code === 'EEXIST') return null; throw error; }
252
+ return () => { if (fs.existsSync(file) && privateJson(file).nonce === nonce) fs.unlinkSync(file); };
253
+ }
254
+ async function offers(selected) {
255
+ const cli = await manifest('@aglit-ai/cli', selected);
256
+ need(cli.bin?.aglit === 'bin/aglit.cjs' && !/-runtime\.(?:darwin-arm64|win32-x64)/.test(cli.version), 'Selected channel does not identify the CLI launcher');
257
+ const runtime = await manifest('@aglit-ai/cli', runtimeTag(selected));
258
+ runtimeVersion(runtime);
259
+ return {cli, runtime};
260
+ }
261
+ async function status() {
262
+ const value = state(), latest = await offers(value.channel);
263
+ const version = value.current?.version || identity.version;
264
+ const installedRuntimeVersion = fs.existsSync(path.join(installation.root, 'installation.json')) ? installation.readInstallation().version : null;
265
+ return {channel: value.channel, automatic: value.automatic, version, availableVersion: latest.cli.version,
266
+ runtimeVersion: installedRuntimeVersion, availableRuntimeVersion: runtimeVersion(latest.runtime),
267
+ updateAvailable: compareVersions(latest.cli.version, version) > 0 || installedRuntimeVersion !== null && compareVersions(runtimeVersion(latest.runtime), installedRuntimeVersion) > 0};
268
+ }
269
+ function runtimeStopped(installed) {
270
+ installation.canonical(installed.executable);
271
+ if (process.platform === 'darwin') {
272
+ const output = execFileSync('/bin/ps', ['-axo', 'pid=,uid=,comm='], {encoding: 'utf8', timeout: 15000, maxBuffer: 8 * 1024 ** 2});
273
+ const lines = output.trim().split('\n'); need(lines.length > 1, 'Native process inventory unavailable');
274
+ return !lines.some(line => {
275
+ const found = /^\s*(\d+)\s+(-?\d+)\s+(.+)$/.exec(line); need(found, 'Native process inventory is incomplete');
276
+ return found[3].trim() === installed.executable;
277
+ });
278
+ }
279
+ if (process.platform === 'win32') {
280
+ const powershell = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32/WindowsPowerShell/v1.0/powershell.exe');
281
+ const script = '$ErrorActionPreference="Stop";$rows=@(Get-CimInstance Win32_Process -Filter "Name=\'Aglit AI.exe\'");$unknown=@($rows|Where-Object{![bool]$_.ExecutablePath});$matches=@($rows|Where-Object{$_.ExecutablePath -eq $env:AGLIT_EXPECTED_RUNTIME});@{stopped=($unknown.Count -eq 0 -and $matches.Count -eq 0)}|ConvertTo-Json -Compress';
282
+ const output = execFileSync(powershell, ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', script], {encoding: 'utf8', timeout: 15000, maxBuffer: 65536, windowsHide: true, env: {...process.env, AGLIT_EXPECTED_RUNTIME: installed.executable}});
283
+ return JSON.parse(output).stopped === true;
284
+ }
285
+ throw Error('Native process ownership proof unavailable');
286
+ }
287
+ function rememberStatus(value) {
288
+ need(value?.running && Number.isSafeInteger(value.pid) && typeof value.deviceId === 'string' && value.deviceId.length > 0 &&
289
+ (value.email === null || typeof value.email === 'string') && typeof value.enabled === 'boolean', 'Current runtime identity is incomplete');
290
+ const saved = {kind: 'aglit-cli-runtime-status-v1', version: value.version, deviceId: value.deviceId, email: value.email,
291
+ enabled: value.enabled, provider: value.provider, mode: value.mode, pid: value.pid};
292
+ writeJson(path.join(installation.root, 'last-runtime-status.json'), saved); return saved;
293
+ }
294
+ function offlineStatus(installed) {
295
+ const previous = path.join(installation.root, 'last-runtime-status.json');
296
+ if (fs.existsSync(previous)) { const value = privateJson(previous); need(value.kind === 'aglit-cli-runtime-status-v1' && value.version === installed.version, 'Previous runtime identity is unavailable'); return value; }
297
+ // A never-enrolled legacy installation can migrate without reading any auth
298
+ // files. Existing registered profiles require an authenticated prior status.
299
+ const file = path.join(installation.profile, 'system-settings.json');
300
+ const value = privateJson(file).settings;
301
+ need(value && value.enableAnywhereAccess === false && typeof value.remoteAccessDeviceId === 'string' && !value.providerSettings?.chatProvider,
302
+ 'Start the existing Desktop and run aglit status, then quit it normally before the first npm update');
303
+ return {deviceId: value.remoteAccessDeviceId, email: null, enabled: false, provider: null, mode: 'headless'};
304
+ }
305
+ function continuity(value, original, runtime) {
306
+ return value?.running && value.version === runtime.version && value.deviceId === original.deviceId && value.email === original.email &&
307
+ value.enabled === original.enabled && value.provider === original.provider;
308
+ }
309
+ function activationHold(runtime, original) {
310
+ return {kind: 'aglit-computer-use-update-activation-v1', root: installation.root, profile: installation.profile,
311
+ ...runtime, deviceId: original.deviceId, email: original.email, enabled: original.enabled, provider: original.provider};
312
+ }
313
+ function removeHold(runtime) {
314
+ const file = path.join(installation.root, 'update-activation.json'); if (!fs.existsSync(file)) return;
315
+ const hold = privateJson(file);
316
+ need(hold.kind === 'aglit-computer-use-update-activation-v1' && hold.executable === runtime.executable && hold.version === runtime.version &&
317
+ hold.sha256 === runtime.sha256 && hold.sourceCommit === runtime.sourceCommit && hold.root === installation.root && hold.profile === installation.profile, 'Candidate activation hold changed');
318
+ fs.unlinkSync(file);
319
+ }
320
+ async function acceptCandidate(config, pid, version) {
321
+ // The native contract makes this action idempotent for one running occurrence.
322
+ // Retry only this explicit acknowledgement, never general mutation commands.
323
+ let result;
324
+ for (let attempt = 0; attempt < 2; attempt++) {
325
+ try { result = await control(config, 'update-accept'); break; }
326
+ catch (error) { if (attempt === 1) throw error; await delay(250); }
327
+ }
328
+ need(result?.accepted === true && result.pid === pid && result.version === version, 'Updated runtime did not accept its continuity hold');
329
+ return result;
330
+ }
331
+ async function recoverActivation(ownedLock = false) {
332
+ const file = path.join(installation.root, 'update-transaction.json'); if (!fs.existsSync(file)) return {recovered: false};
333
+ const unlock = ownedLock ? () => {} : lock(path.join(installation.root, 'updating.lock')); if (!unlock) return {recovered: false, updating: true};
334
+ try {
335
+ const tx = privateJson(file), current = installation.readInstallation();
336
+ need(tx.kind === 'aglit-cli-activation-recovery-v1' && tx.previous.root === installation.root && tx.previous.profile === installation.profile &&
337
+ tx.runtime.executable.startsWith(path.join(installation.root, 'runtimes') + path.sep), 'Interrupted update recovery binding differs');
338
+ validatePointer(tx.candidate); if (tx.previousCli.current) validatePointer(tx.previousCli.current);
339
+ const holdFile = path.join(installation.root, 'update-activation.json');
340
+ if (current.sha256 === tx.runtime.sha256 && current.executable === tx.runtime.executable) {
341
+ const policy = settings();
342
+ const cancelled = tx.policy && (policy.channel !== tx.policy.channel || policy.revision !== tx.policy.revision && !policy.automatic);
343
+ let launched = null, measured = null;
344
+ if (!cancelled && runtimeStopped(current)) { writeJson(holdFile, activationHold(tx.runtime, tx.original)); launched = installation.start(); }
345
+ const deadline = Date.now() + 120000, config = path.join(installation.profile, 'ipc/sdk-ipc.json');
346
+ if (!cancelled) do {
347
+ try { const value = await control(config, 'status'); if (continuity(value, tx.original, tx.runtime) && (!launched || value.pid === launched.pid)) { measured = value; break; } }
348
+ catch { /* Account/provider restore may still be in flight. */ }
349
+ await delay(1000);
350
+ } while (Date.now() < deadline && !runtimeStopped(current));
351
+ if (measured) {
352
+ if (fs.existsSync(holdFile)) { await acceptCandidate(config, measured.pid, tx.runtime.version); }
353
+ fs.unlinkSync(file); const pending = path.join(installation.root, 'pending-update.json'); if (fs.existsSync(pending)) fs.unlinkSync(pending);
354
+ return {recovered: true, accepted: true, runtimePid: measured.pid};
355
+ }
356
+ if (!runtimeStopped(current)) {
357
+ const result = await control(config, 'update-prepare'); need(result.quitting && Number.isSafeInteger(result.pid), 'Candidate cannot safely retire for recovery');
358
+ const deadline = Date.now() + 30000; while (live(result.pid) && Date.now() < deadline) await delay(250);
359
+ }
360
+ need(runtimeStopped(current), 'Candidate process remains active; recovery preserved');
361
+ } else {
362
+ need(current.sha256 === tx.previous.sha256 && current.executable === tx.previous.executable, 'Installation changed outside interrupted update');
363
+ if (!fs.existsSync(holdFile)) {
364
+ if (tx.retirementRequested) {
365
+ if (!runtimeStopped(tx.previous)) {
366
+ try { await control(path.join(installation.profile, 'ipc/sdk-ipc.json'), 'status'); }
367
+ catch {
368
+ const deadline = Date.now() + 30000; while (!runtimeStopped(tx.previous) && Date.now() < deadline) await delay(500);
369
+ need(runtimeStopped(tx.previous), 'Previous runtime is still retiring; profile preserved');
370
+ }
371
+ }
372
+ if (runtimeStopped(tx.previous)) installation.start();
373
+ }
374
+ fs.unlinkSync(file); return {recovered: true, activated: false};
375
+ }
376
+ need(runtimeStopped({...current, ...tx.runtime}), 'Interrupted candidate process is still active');
377
+ }
378
+ removeHold(tx.runtime);
379
+ writeJson(path.join(installation.root, 'installation.json'), tx.previous); savePointer(tx.previousCli); writeStartup();
380
+ if (runtimeStopped(tx.previous)) installation.start(); fs.unlinkSync(file);
381
+ return {recovered: true, rolledBack: true};
382
+ } finally { unlock(); }
383
+ }
384
+ async function update({check = false, channel} = {}) {
385
+ need(channel === undefined || channels.includes(channel), 'Update channel must be latest, canary or embedded');
386
+ if (check) {
387
+ const current = state(), latest = await offers(channel || current.channel);
388
+ const installed = fs.existsSync(path.join(installation.root, 'installation.json')) ? installation.readInstallation() : null;
389
+ return {channel: channel || current.channel, version: current.current?.version || identity.version, availableVersion: latest.cli.version,
390
+ runtimeVersion: installed?.version || null, availableRuntimeVersion: runtimeVersion(latest.runtime),
391
+ updateAvailable: compareVersions(latest.cli.version, current.current?.version || identity.version) > 0 || Boolean(installed && compareVersions(runtimeVersion(latest.runtime), installed.version) > 0), automatic: current.automatic};
392
+ }
393
+ installation.readInstallation();
394
+ const unlock = lock(path.join(installation.root, 'updating.lock')); if (!unlock) return {updated: false, updating: true};
395
+ try {
396
+ await recoverActivation(true);
397
+ initialize(channel);
398
+ const value = state(), policy = settings(), installed = installation.readInstallation();
399
+ const selected = value.channel, latest = await offers(selected), currentVersion = value.current?.version || identity.version;
400
+ const cancelled = () => {
401
+ const current = settings();
402
+ need(!fs.existsSync(path.join(installation.root, 'updater-stop.json')) && current.channel === selected &&
403
+ !(current.revision !== policy.revision && !current.automatic), 'Update cancelled before activation; current profile preserved');
404
+ };
405
+ cancelled();
406
+ const cliAvailable = compareVersions(latest.cli.version, currentVersion) > 0;
407
+ const runtimeAvailable = compareVersions(runtimeVersion(latest.runtime), installed.version) > 0;
408
+ if (!cliAvailable && !runtimeAvailable) return {updated: false, version: currentVersion, runtimeVersion: installed.version};
409
+ const cli = cliAvailable || latest.cli.version === currentVersion ? latest.cli : await manifest('@aglit-ai/cli', currentVersion);
410
+ if (!runtimeAvailable) {
411
+ const pending = await stage(cli, path.join(installation.root, 'cli'), null, true); cancelled();
412
+ savePointer({...state(), current: pointer(pending.directory, cli, cli.dist.integrity), previous: state().current}); writeStartup();
413
+ return {updated: true, version: cli.version, runtimeUpdated: false};
414
+ }
415
+ const pendingFile = path.join(installation.root, 'pending-update.json');
416
+ let prepared = fs.existsSync(pendingFile) ? privateJson(pendingFile) : null;
417
+ if (prepared && (prepared.kind !== 'aglit-cli-pending-update-v1' || prepared.cliVersion !== cli.version || prepared.runtimeVersion !== runtimeVersion(latest.runtime) || prepared.previousArchive !== installed.sha256)) prepared = null;
418
+ if (!prepared) {
419
+ const pending = await stage(cli, path.join(installation.root, 'cli'), latest.runtime); cancelled();
420
+ const candidate = pointer(pending.directory, cli, cli.dist.integrity);
421
+ const bundle = installation.embeddedRelease(process.platform === 'darwin' ? 'mac' : 'win', pending.packageDirectory, pending.runtimeDirectory);
422
+ need(bundle && compareVersions(bundle.release.version, installed.version) > 0, 'Desktop update would not advance this computer');
423
+ const generations = path.join(installation.root, 'runtimes'); fs.mkdirSync(generations, {recursive: true, mode: 0o700});
424
+ const generation = fs.mkdtempSync(path.join(generations, bundle.release.version + '-')); fs.chmodSync(generation, 0o700);
425
+ const runtime = await installation.stageRuntime(bundle, path.join(generation, 'runtime')); cancelled();
426
+ prepared = {kind: 'aglit-cli-pending-update-v1', cliVersion: cli.version, runtimeVersion: runtime.version, previousArchive: installed.sha256, candidate, runtime};
427
+ writeJson(pendingFile, prepared);
428
+ }
429
+ const {candidate, runtime} = prepared; validatePointer(candidate);
430
+ need(runtime.executable.startsWith(path.join(installation.root, 'runtimes') + path.sep), 'Invalid staged Desktop generation');
431
+ installation.canonical(runtime.executable); installation.verifyRuntime(runtime.executable, runtime); cancelled();
432
+ const config = path.join(installation.profile, 'ipc/sdk-ipc.json');
433
+ let original, retiring = null;
434
+ try { original = rememberStatus(await control(config, 'status')); }
435
+ catch { need(runtimeStopped(installed), 'Desktop is running but unavailable; stop it normally before updating'); original = offlineStatus(installed); }
436
+ if (original.enabled) need(original.email, 'Restore the current Desktop sign-in before applying an update');
437
+ const transaction = path.join(installation.root, 'update-transaction.json');
438
+ const activation = path.join(installation.root, 'update-activation.json');
439
+ const before = {kind: 'aglit-cli-activation-recovery-v1', previous: installed, previousCli: {current: state().current, previous: state().previous}, candidate, runtime, original, policy: {channel: selected, revision: policy.revision, automatic: policy.automatic}};
440
+ writeJson(transaction, before);
441
+ let launched = null, switched = false, oldRetired = false, accepted = false;
442
+ try {
443
+ cancelled();
444
+ if (!runtimeStopped(installed)) {
445
+ let ready; try { ready = await control(config, 'update-status'); }
446
+ catch { return {updated: false, deferred: true, reason: 'legacy-runtime-needs-normal-exit', availableVersion: cli.version}; }
447
+ if (!ready?.supported || ready.busy) return {updated: false, deferred: true, reason: 'active-work', availableVersion: cli.version};
448
+ cancelled(); writeJson(transaction, {...before, retirementRequested: true}); retiring = await control(config, 'update-prepare');
449
+ if (!retiring?.quitting || retiring.busy) return {updated: false, deferred: true, reason: 'active-work', availableVersion: cli.version};
450
+ need(Number.isSafeInteger(retiring.pid) && retiring.pid > 0 && retiring.pid === original.pid, 'Runtime retirement acknowledgement required');
451
+ const deadline = Date.now() + 30000;
452
+ while (live(retiring.pid) && Date.now() < deadline) await delay(250);
453
+ need(!live(retiring.pid), 'Runtime is still closing; update remains staged');
454
+ }
455
+ oldRetired = true; cancelled();
456
+ need(runtimeStopped(installed), 'A legacy Desktop started during activation; update remains staged');
457
+ const authoritative = installation.readInstallation(); need(authoritative.sha256 === installed.sha256 && authoritative.executable === installed.executable, 'Installation changed during update');
458
+ writeJson(path.join(installation.root, 'installation.previous.json'), installed);
459
+ writeJson(activation, activationHold(runtime, original));
460
+ writeJson(path.join(installation.root, 'installation.json'), {...installed, ...runtime}); switched = true;
461
+ savePointer({...state(), current: candidate, previous: before.previousCli.current}); writeStartup();
462
+ cancelled(); launched = installation.start();
463
+ let replacement; const deadline = Date.now() + 120000;
464
+ do {
465
+ await delay(1000);
466
+ try { const current = await control(config, 'status');
467
+ if (current.pid === launched.pid && continuity(current, original, runtime)) { replacement = current; break; }
468
+ } catch { /* Allow the replacement account/provider settings to restore. */ }
469
+ } while (live(launched.pid) && Date.now() < deadline);
470
+ need(replacement, 'Updated Desktop continuity did not match before its readiness deadline'); cancelled();
471
+ await acceptCandidate(config, launched.pid, runtime.version);
472
+ accepted = true;
473
+ let windowRestored = true;
474
+ if ((retiring?.mode || original.mode) === 'headed') { try { await control(config, 'window', {mode: 'headed'}); } catch { windowRestored = false; } }
475
+ try { fs.unlinkSync(pendingFile); fs.unlinkSync(transaction); } catch { /* Accepted runtime stays active; next start settles only metadata. */ }
476
+ return {updated: true, version: cli.version, runtimeUpdated: true, runtimeVersion: runtime.version, runtimePid: replacement.pid, windowRestored};
477
+ } catch (error) {
478
+ if (accepted) return {updated: true, version: cli.version, runtimeUpdated: true, runtimeVersion: runtime.version, runtimePid: launched.pid, recoveryMetadataPending: true};
479
+ // A candidate can be retired only through its own authenticated idle
480
+ // handoff. Never kill an unproven process or start two profile owners.
481
+ let candidateStopped = !launched || !live(launched.pid);
482
+ if (launched && !candidateStopped) {
483
+ try { const closing = await control(config, 'update-prepare'); if (closing.quitting && closing.pid === launched.pid) {
484
+ const deadline = Date.now() + 30000; while (live(launched.pid) && Date.now() < deadline) await delay(250); candidateStopped = !live(launched.pid);
485
+ } } catch { /* Preserve any busy or uncertain candidate for recovery. */ }
486
+ }
487
+ let restored = false;
488
+ if (candidateStopped && (oldRetired || switched || retiring?.quitting)) {
489
+ try {
490
+ removeHold(runtime);
491
+ writeJson(path.join(installation.root, 'installation.json'), installed); savePointer(before.previousCli); writeStartup();
492
+ if (runtimeStopped(installed)) installation.start();
493
+ restored = true; if (fs.existsSync(transaction)) fs.unlinkSync(transaction);
494
+ } catch { /* Durable recovery record retains original receipt and CLI. */ }
495
+ }
496
+ writeJson(path.join(installation.root, 'update-recovery.json'), {...before, rolledBack: restored});
497
+ throw Error(restored ? 'Updated Desktop failed; previous runtime restored with the original profile' : 'Update stopped; previous runtime and recovery receipt preserved');
498
+ }
499
+ } finally { unlock(); }
500
+ }
501
+ async function stopWorker() {
502
+ if (!fs.existsSync(installation.root)) return;
503
+ installation.privateRoot(); const marker = path.join(installation.root, 'updater-stop.json');
504
+ writeJson(marker, {kind: 'aglit-cli-updater-stop-v1'});
505
+ const file = path.join(installation.root, 'updater-worker.json');
506
+ const deadline = Date.now() + 30000;
507
+ while (fs.existsSync(file) && Date.now() < deadline) {
508
+ const worker = privateJson(file);
509
+ if (!Number.isSafeInteger(worker.pid) || !live(worker.pid)) { fs.unlinkSync(file); break; }
510
+ await delay(250);
511
+ }
512
+ need(!fs.existsSync(file), 'Updater is closing; installation preserved. Retry aglit uninstall.');
513
+ if (fs.existsSync(marker)) fs.unlinkSync(marker);
514
+ }
515
+ async function automatic(enabled) {
516
+ need(typeof enabled === 'boolean', 'Automatic update setting required'); installation.readInstallation(); saveSettings({automatic: enabled});
517
+ if (!enabled) await stopWorker();
518
+ else { try { const running = await control(path.join(installation.profile, 'ipc/sdk-ipc.json'), 'status'); if (running.running) ensureWorker(running.pid); } catch { /* Next start resumes automatic updates. */ } }
519
+ return {automatic: enabled, channel: state().channel};
520
+ }
521
+ async function prepareUninstall() {
522
+ await stopWorker(); const unlock = lock(path.join(installation.root, 'updating.lock'));
523
+ need(unlock, 'An update is still active; installation preserved. Retry uninstall after it finishes.'); return unlock;
524
+ }
525
+ function ensureWorker(runtimePid) {
526
+ const value = initialize(); if (!value.automatic) return;
527
+ const stop = path.join(installation.root, 'updater-stop.json'); if (fs.existsSync(stop)) { privateJson(stop); fs.unlinkSync(stop); }
528
+ const file = path.join(installation.root, 'updater-worker.json');
529
+ if (fs.existsSync(file)) { const previous = privateJson(file); if (Number.isSafeInteger(previous.pid) && live(previous.pid)) return; fs.unlinkSync(file); }
530
+ const script = path.join(packageDirectory(value.current.directory), 'src/update-worker.cjs'); installation.canonical(script);
531
+ const log = fs.openSync(path.join(installation.root, 'updater.log'), 'a', 0o600);
532
+ const env = {...process.env}; for (const key of Object.keys(env)) if (/^AGLIT_|^VITE_|^NODE_OPTIONS$|^NODE_PATH$|^ELECTRON_RUN_AS_NODE$/.test(key)) delete env[key];
533
+ const child = spawn(process.execPath, [script, String(runtimePid)], {detached: true, windowsHide: true, env, stdio: ['ignore', log, log]}); child.on('error', () => {}); child.unref(); fs.closeSync(log);
534
+ }
535
+ module.exports = {compareVersions, state, initialize, delegate, manifest, bootstrap, adoptBootstrap, status, update, automatic, ensureWorker, stopWorker, prepareUninstall, lock, writeJson, privateJson, live, stage, settings, runtimeStopped, rememberStatus, recoverActivation, runtimeVersion, runtimeTag, releaseManifests, validateInstalledGraph};