@aglit-ai/cli 0.1.1 → 0.1.4

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.
@@ -0,0 +1,530 @@
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) { const selected = channel || state().channel; need(channels.includes(selected), 'Invalid update channel'); return {...await stage(await manifest('@aglit-ai/cli', selected), os.tmpdir()), channel: selected}; }
232
+ function adoptBootstrap(value) {
233
+ const parent = path.join(installation.root, 'cli'); fs.mkdirSync(parent, {recursive: true, mode: 0o700});
234
+ const target = path.join(parent, value.cli.version + '-' + crypto.randomUUID());
235
+ 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}); }
236
+ const prior = state(); saveSettings({channel: value.channel}); savePointer({...prior, current: pointer(target, value.cli, value.cli.dist.integrity), previous: prior.current}); writeStartup();
237
+ }
238
+ function live(pid) { try { process.kill(pid, 0); return true; } catch (error) { if (error.code === 'ESRCH') return false; throw error; } }
239
+ function lock(file) {
240
+ if (fs.existsSync(file)) {
241
+ const value = privateJson(file); need(Number.isSafeInteger(value.pid) && value.pid > 0, 'Invalid updater lock');
242
+ if (live(value.pid)) return null; fs.unlinkSync(file);
243
+ }
244
+ const nonce = crypto.randomUUID();
245
+ try { fs.writeFileSync(file, JSON.stringify({pid: process.pid, nonce}) + '\n', {flag: 'wx', mode: 0o600}); }
246
+ catch (error) { if (error.code === 'EEXIST') return null; throw error; }
247
+ return () => { if (fs.existsSync(file) && privateJson(file).nonce === nonce) fs.unlinkSync(file); };
248
+ }
249
+ async function offers(selected) {
250
+ const cli = await manifest('@aglit-ai/cli', selected);
251
+ need(cli.bin?.aglit === 'bin/aglit.cjs' && !/-runtime\.(?:darwin-arm64|win32-x64)/.test(cli.version), 'Selected channel does not identify the CLI launcher');
252
+ const runtime = await manifest('@aglit-ai/cli', runtimeTag(selected));
253
+ runtimeVersion(runtime);
254
+ return {cli, runtime};
255
+ }
256
+ async function status() {
257
+ const value = state(), latest = await offers(value.channel);
258
+ const version = value.current?.version || identity.version;
259
+ const installedRuntimeVersion = fs.existsSync(path.join(installation.root, 'installation.json')) ? installation.readInstallation().version : null;
260
+ return {channel: value.channel, automatic: value.automatic, version, availableVersion: latest.cli.version,
261
+ runtimeVersion: installedRuntimeVersion, availableRuntimeVersion: runtimeVersion(latest.runtime),
262
+ updateAvailable: compareVersions(latest.cli.version, version) > 0 || installedRuntimeVersion !== null && compareVersions(runtimeVersion(latest.runtime), installedRuntimeVersion) > 0};
263
+ }
264
+ function runtimeStopped(installed) {
265
+ installation.canonical(installed.executable);
266
+ if (process.platform === 'darwin') {
267
+ const output = execFileSync('/bin/ps', ['-axo', 'pid=,uid=,comm='], {encoding: 'utf8', timeout: 15000, maxBuffer: 8 * 1024 ** 2});
268
+ const lines = output.trim().split('\n'); need(lines.length > 1, 'Native process inventory unavailable');
269
+ return !lines.some(line => {
270
+ const found = /^\s*(\d+)\s+(\d+)\s+(.+)$/.exec(line); need(found, 'Native process inventory is incomplete');
271
+ return found[3].trim() === installed.executable;
272
+ });
273
+ }
274
+ if (process.platform === 'win32') {
275
+ const powershell = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32/WindowsPowerShell/v1.0/powershell.exe');
276
+ 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';
277
+ 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}});
278
+ return JSON.parse(output).stopped === true;
279
+ }
280
+ throw Error('Native process ownership proof unavailable');
281
+ }
282
+ function rememberStatus(value) {
283
+ need(value?.running && Number.isSafeInteger(value.pid) && typeof value.deviceId === 'string' && value.deviceId.length > 0 &&
284
+ (value.email === null || typeof value.email === 'string') && typeof value.enabled === 'boolean', 'Current runtime identity is incomplete');
285
+ const saved = {kind: 'aglit-cli-runtime-status-v1', version: value.version, deviceId: value.deviceId, email: value.email,
286
+ enabled: value.enabled, provider: value.provider, mode: value.mode, pid: value.pid};
287
+ writeJson(path.join(installation.root, 'last-runtime-status.json'), saved); return saved;
288
+ }
289
+ function offlineStatus(installed) {
290
+ const previous = path.join(installation.root, 'last-runtime-status.json');
291
+ 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; }
292
+ // A never-enrolled legacy installation can migrate without reading any auth
293
+ // files. Existing registered profiles require an authenticated prior status.
294
+ const file = path.join(installation.profile, 'system-settings.json');
295
+ const value = privateJson(file).settings;
296
+ need(value && value.enableAnywhereAccess === false && typeof value.remoteAccessDeviceId === 'string' && !value.providerSettings?.chatProvider,
297
+ 'Start the existing Desktop and run aglit status, then quit it normally before the first npm update');
298
+ return {deviceId: value.remoteAccessDeviceId, email: null, enabled: false, provider: null, mode: 'headless'};
299
+ }
300
+ function continuity(value, original, runtime) {
301
+ return value?.running && value.version === runtime.version && value.deviceId === original.deviceId && value.email === original.email &&
302
+ value.enabled === original.enabled && value.provider === original.provider;
303
+ }
304
+ function activationHold(runtime, original) {
305
+ return {kind: 'aglit-computer-use-update-activation-v1', root: installation.root, profile: installation.profile,
306
+ ...runtime, deviceId: original.deviceId, email: original.email, enabled: original.enabled, provider: original.provider};
307
+ }
308
+ function removeHold(runtime) {
309
+ const file = path.join(installation.root, 'update-activation.json'); if (!fs.existsSync(file)) return;
310
+ const hold = privateJson(file);
311
+ need(hold.kind === 'aglit-computer-use-update-activation-v1' && hold.executable === runtime.executable && hold.version === runtime.version &&
312
+ hold.sha256 === runtime.sha256 && hold.sourceCommit === runtime.sourceCommit && hold.root === installation.root && hold.profile === installation.profile, 'Candidate activation hold changed');
313
+ fs.unlinkSync(file);
314
+ }
315
+ async function acceptCandidate(config, pid, version) {
316
+ // The native contract makes this action idempotent for one running occurrence.
317
+ // Retry only this explicit acknowledgement, never general mutation commands.
318
+ let result;
319
+ for (let attempt = 0; attempt < 2; attempt++) {
320
+ try { result = await control(config, 'update-accept'); break; }
321
+ catch (error) { if (attempt === 1) throw error; await delay(250); }
322
+ }
323
+ need(result?.accepted === true && result.pid === pid && result.version === version, 'Updated runtime did not accept its continuity hold');
324
+ return result;
325
+ }
326
+ async function recoverActivation(ownedLock = false) {
327
+ const file = path.join(installation.root, 'update-transaction.json'); if (!fs.existsSync(file)) return {recovered: false};
328
+ const unlock = ownedLock ? () => {} : lock(path.join(installation.root, 'updating.lock')); if (!unlock) return {recovered: false, updating: true};
329
+ try {
330
+ const tx = privateJson(file), current = installation.readInstallation();
331
+ need(tx.kind === 'aglit-cli-activation-recovery-v1' && tx.previous.root === installation.root && tx.previous.profile === installation.profile &&
332
+ tx.runtime.executable.startsWith(path.join(installation.root, 'runtimes') + path.sep), 'Interrupted update recovery binding differs');
333
+ validatePointer(tx.candidate); if (tx.previousCli.current) validatePointer(tx.previousCli.current);
334
+ const holdFile = path.join(installation.root, 'update-activation.json');
335
+ if (current.sha256 === tx.runtime.sha256 && current.executable === tx.runtime.executable) {
336
+ const policy = settings();
337
+ const cancelled = tx.policy && (policy.channel !== tx.policy.channel || policy.revision !== tx.policy.revision && !policy.automatic);
338
+ let launched = null, measured = null;
339
+ if (!cancelled && runtimeStopped(current)) { writeJson(holdFile, activationHold(tx.runtime, tx.original)); launched = installation.start(); }
340
+ const deadline = Date.now() + 120000, config = path.join(installation.profile, 'ipc/sdk-ipc.json');
341
+ if (!cancelled) do {
342
+ try { const value = await control(config, 'status'); if (continuity(value, tx.original, tx.runtime) && (!launched || value.pid === launched.pid)) { measured = value; break; } }
343
+ catch { /* Account/provider restore may still be in flight. */ }
344
+ await delay(1000);
345
+ } while (Date.now() < deadline && !runtimeStopped(current));
346
+ if (measured) {
347
+ if (fs.existsSync(holdFile)) { await acceptCandidate(config, measured.pid, tx.runtime.version); }
348
+ fs.unlinkSync(file); const pending = path.join(installation.root, 'pending-update.json'); if (fs.existsSync(pending)) fs.unlinkSync(pending);
349
+ return {recovered: true, accepted: true, runtimePid: measured.pid};
350
+ }
351
+ if (!runtimeStopped(current)) {
352
+ const result = await control(config, 'update-prepare'); need(result.quitting && Number.isSafeInteger(result.pid), 'Candidate cannot safely retire for recovery');
353
+ const deadline = Date.now() + 30000; while (live(result.pid) && Date.now() < deadline) await delay(250);
354
+ }
355
+ need(runtimeStopped(current), 'Candidate process remains active; recovery preserved');
356
+ } else {
357
+ need(current.sha256 === tx.previous.sha256 && current.executable === tx.previous.executable, 'Installation changed outside interrupted update');
358
+ if (!fs.existsSync(holdFile)) {
359
+ if (tx.retirementRequested) {
360
+ if (!runtimeStopped(tx.previous)) {
361
+ try { await control(path.join(installation.profile, 'ipc/sdk-ipc.json'), 'status'); }
362
+ catch {
363
+ const deadline = Date.now() + 30000; while (!runtimeStopped(tx.previous) && Date.now() < deadline) await delay(500);
364
+ need(runtimeStopped(tx.previous), 'Previous runtime is still retiring; profile preserved');
365
+ }
366
+ }
367
+ if (runtimeStopped(tx.previous)) installation.start();
368
+ }
369
+ fs.unlinkSync(file); return {recovered: true, activated: false};
370
+ }
371
+ need(runtimeStopped({...current, ...tx.runtime}), 'Interrupted candidate process is still active');
372
+ }
373
+ removeHold(tx.runtime);
374
+ writeJson(path.join(installation.root, 'installation.json'), tx.previous); savePointer(tx.previousCli); writeStartup();
375
+ if (runtimeStopped(tx.previous)) installation.start(); fs.unlinkSync(file);
376
+ return {recovered: true, rolledBack: true};
377
+ } finally { unlock(); }
378
+ }
379
+ async function update({check = false, channel} = {}) {
380
+ need(channel === undefined || channels.includes(channel), 'Update channel must be latest, canary or embedded');
381
+ if (check) {
382
+ const current = state(), latest = await offers(channel || current.channel);
383
+ const installed = fs.existsSync(path.join(installation.root, 'installation.json')) ? installation.readInstallation() : null;
384
+ return {channel: channel || current.channel, version: current.current?.version || identity.version, availableVersion: latest.cli.version,
385
+ runtimeVersion: installed?.version || null, availableRuntimeVersion: runtimeVersion(latest.runtime),
386
+ updateAvailable: compareVersions(latest.cli.version, current.current?.version || identity.version) > 0 || Boolean(installed && compareVersions(runtimeVersion(latest.runtime), installed.version) > 0), automatic: current.automatic};
387
+ }
388
+ installation.readInstallation();
389
+ const unlock = lock(path.join(installation.root, 'updating.lock')); if (!unlock) return {updated: false, updating: true};
390
+ try {
391
+ await recoverActivation(true);
392
+ initialize(channel);
393
+ const value = state(), policy = settings(), installed = installation.readInstallation();
394
+ const selected = value.channel, latest = await offers(selected), currentVersion = value.current?.version || identity.version;
395
+ const cancelled = () => {
396
+ const current = settings();
397
+ need(!fs.existsSync(path.join(installation.root, 'updater-stop.json')) && current.channel === selected &&
398
+ !(current.revision !== policy.revision && !current.automatic), 'Update cancelled before activation; current profile preserved');
399
+ };
400
+ cancelled();
401
+ const cliAvailable = compareVersions(latest.cli.version, currentVersion) > 0;
402
+ const runtimeAvailable = compareVersions(runtimeVersion(latest.runtime), installed.version) > 0;
403
+ if (!cliAvailable && !runtimeAvailable) return {updated: false, version: currentVersion, runtimeVersion: installed.version};
404
+ const cli = cliAvailable || latest.cli.version === currentVersion ? latest.cli : await manifest('@aglit-ai/cli', currentVersion);
405
+ if (!runtimeAvailable) {
406
+ const pending = await stage(cli, path.join(installation.root, 'cli'), null, true); cancelled();
407
+ savePointer({...state(), current: pointer(pending.directory, cli, cli.dist.integrity), previous: state().current}); writeStartup();
408
+ return {updated: true, version: cli.version, runtimeUpdated: false};
409
+ }
410
+ const pendingFile = path.join(installation.root, 'pending-update.json');
411
+ let prepared = fs.existsSync(pendingFile) ? privateJson(pendingFile) : null;
412
+ if (prepared && (prepared.kind !== 'aglit-cli-pending-update-v1' || prepared.cliVersion !== cli.version || prepared.runtimeVersion !== runtimeVersion(latest.runtime) || prepared.previousArchive !== installed.sha256)) prepared = null;
413
+ if (!prepared) {
414
+ const pending = await stage(cli, path.join(installation.root, 'cli'), latest.runtime); cancelled();
415
+ const candidate = pointer(pending.directory, cli, cli.dist.integrity);
416
+ const bundle = installation.embeddedRelease(process.platform === 'darwin' ? 'mac' : 'win', pending.packageDirectory, pending.runtimeDirectory);
417
+ need(bundle && compareVersions(bundle.release.version, installed.version) > 0, 'Desktop update would not advance this computer');
418
+ const generations = path.join(installation.root, 'runtimes'); fs.mkdirSync(generations, {recursive: true, mode: 0o700});
419
+ const generation = fs.mkdtempSync(path.join(generations, bundle.release.version + '-')); fs.chmodSync(generation, 0o700);
420
+ const runtime = await installation.stageRuntime(bundle, path.join(generation, 'runtime')); cancelled();
421
+ prepared = {kind: 'aglit-cli-pending-update-v1', cliVersion: cli.version, runtimeVersion: runtime.version, previousArchive: installed.sha256, candidate, runtime};
422
+ writeJson(pendingFile, prepared);
423
+ }
424
+ const {candidate, runtime} = prepared; validatePointer(candidate);
425
+ need(runtime.executable.startsWith(path.join(installation.root, 'runtimes') + path.sep), 'Invalid staged Desktop generation');
426
+ installation.canonical(runtime.executable); installation.verifyRuntime(runtime.executable, runtime); cancelled();
427
+ const config = path.join(installation.profile, 'ipc/sdk-ipc.json');
428
+ let original, retiring = null;
429
+ try { original = rememberStatus(await control(config, 'status')); }
430
+ catch { need(runtimeStopped(installed), 'Desktop is running but unavailable; stop it normally before updating'); original = offlineStatus(installed); }
431
+ if (original.enabled) need(original.email, 'Restore the current Desktop sign-in before applying an update');
432
+ const transaction = path.join(installation.root, 'update-transaction.json');
433
+ const activation = path.join(installation.root, 'update-activation.json');
434
+ 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}};
435
+ writeJson(transaction, before);
436
+ let launched = null, switched = false, oldRetired = false, accepted = false;
437
+ try {
438
+ cancelled();
439
+ if (!runtimeStopped(installed)) {
440
+ let ready; try { ready = await control(config, 'update-status'); }
441
+ catch { return {updated: false, deferred: true, reason: 'legacy-runtime-needs-normal-exit', availableVersion: cli.version}; }
442
+ if (!ready?.supported || ready.busy) return {updated: false, deferred: true, reason: 'active-work', availableVersion: cli.version};
443
+ cancelled(); writeJson(transaction, {...before, retirementRequested: true}); retiring = await control(config, 'update-prepare');
444
+ if (!retiring?.quitting || retiring.busy) return {updated: false, deferred: true, reason: 'active-work', availableVersion: cli.version};
445
+ need(Number.isSafeInteger(retiring.pid) && retiring.pid > 0 && retiring.pid === original.pid, 'Runtime retirement acknowledgement required');
446
+ const deadline = Date.now() + 30000;
447
+ while (live(retiring.pid) && Date.now() < deadline) await delay(250);
448
+ need(!live(retiring.pid), 'Runtime is still closing; update remains staged');
449
+ }
450
+ oldRetired = true; cancelled();
451
+ need(runtimeStopped(installed), 'A legacy Desktop started during activation; update remains staged');
452
+ const authoritative = installation.readInstallation(); need(authoritative.sha256 === installed.sha256 && authoritative.executable === installed.executable, 'Installation changed during update');
453
+ writeJson(path.join(installation.root, 'installation.previous.json'), installed);
454
+ writeJson(activation, activationHold(runtime, original));
455
+ writeJson(path.join(installation.root, 'installation.json'), {...installed, ...runtime}); switched = true;
456
+ savePointer({...state(), current: candidate, previous: before.previousCli.current}); writeStartup();
457
+ cancelled(); launched = installation.start();
458
+ let replacement; const deadline = Date.now() + 120000;
459
+ do {
460
+ await delay(1000);
461
+ try { const current = await control(config, 'status');
462
+ if (current.pid === launched.pid && continuity(current, original, runtime)) { replacement = current; break; }
463
+ } catch { /* Allow the replacement account/provider settings to restore. */ }
464
+ } while (live(launched.pid) && Date.now() < deadline);
465
+ need(replacement, 'Updated Desktop continuity did not match before its readiness deadline'); cancelled();
466
+ await acceptCandidate(config, launched.pid, runtime.version);
467
+ accepted = true;
468
+ let windowRestored = true;
469
+ if ((retiring?.mode || original.mode) === 'headed') { try { await control(config, 'window', {mode: 'headed'}); } catch { windowRestored = false; } }
470
+ try { fs.unlinkSync(pendingFile); fs.unlinkSync(transaction); } catch { /* Accepted runtime stays active; next start settles only metadata. */ }
471
+ return {updated: true, version: cli.version, runtimeUpdated: true, runtimeVersion: runtime.version, runtimePid: replacement.pid, windowRestored};
472
+ } catch (error) {
473
+ if (accepted) return {updated: true, version: cli.version, runtimeUpdated: true, runtimeVersion: runtime.version, runtimePid: launched.pid, recoveryMetadataPending: true};
474
+ // A candidate can be retired only through its own authenticated idle
475
+ // handoff. Never kill an unproven process or start two profile owners.
476
+ let candidateStopped = !launched || !live(launched.pid);
477
+ if (launched && !candidateStopped) {
478
+ try { const closing = await control(config, 'update-prepare'); if (closing.quitting && closing.pid === launched.pid) {
479
+ const deadline = Date.now() + 30000; while (live(launched.pid) && Date.now() < deadline) await delay(250); candidateStopped = !live(launched.pid);
480
+ } } catch { /* Preserve any busy or uncertain candidate for recovery. */ }
481
+ }
482
+ let restored = false;
483
+ if (candidateStopped && (oldRetired || switched || retiring?.quitting)) {
484
+ try {
485
+ removeHold(runtime);
486
+ writeJson(path.join(installation.root, 'installation.json'), installed); savePointer(before.previousCli); writeStartup();
487
+ if (runtimeStopped(installed)) installation.start();
488
+ restored = true; if (fs.existsSync(transaction)) fs.unlinkSync(transaction);
489
+ } catch { /* Durable recovery record retains original receipt and CLI. */ }
490
+ }
491
+ writeJson(path.join(installation.root, 'update-recovery.json'), {...before, rolledBack: restored});
492
+ throw Error(restored ? 'Updated Desktop failed; previous runtime restored with the original profile' : 'Update stopped; previous runtime and recovery receipt preserved');
493
+ }
494
+ } finally { unlock(); }
495
+ }
496
+ async function stopWorker() {
497
+ if (!fs.existsSync(installation.root)) return;
498
+ installation.privateRoot(); const marker = path.join(installation.root, 'updater-stop.json');
499
+ writeJson(marker, {kind: 'aglit-cli-updater-stop-v1'});
500
+ const file = path.join(installation.root, 'updater-worker.json');
501
+ const deadline = Date.now() + 30000;
502
+ while (fs.existsSync(file) && Date.now() < deadline) {
503
+ const worker = privateJson(file);
504
+ if (!Number.isSafeInteger(worker.pid) || !live(worker.pid)) { fs.unlinkSync(file); break; }
505
+ await delay(250);
506
+ }
507
+ need(!fs.existsSync(file), 'Updater is closing; installation preserved. Retry aglit uninstall.');
508
+ if (fs.existsSync(marker)) fs.unlinkSync(marker);
509
+ }
510
+ async function automatic(enabled) {
511
+ need(typeof enabled === 'boolean', 'Automatic update setting required'); installation.readInstallation(); saveSettings({automatic: enabled});
512
+ if (!enabled) await stopWorker();
513
+ 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. */ } }
514
+ return {automatic: enabled, channel: state().channel};
515
+ }
516
+ async function prepareUninstall() {
517
+ await stopWorker(); const unlock = lock(path.join(installation.root, 'updating.lock'));
518
+ need(unlock, 'An update is still active; installation preserved. Retry uninstall after it finishes.'); return unlock;
519
+ }
520
+ function ensureWorker(runtimePid) {
521
+ const value = initialize(); if (!value.automatic) return;
522
+ const stop = path.join(installation.root, 'updater-stop.json'); if (fs.existsSync(stop)) { privateJson(stop); fs.unlinkSync(stop); }
523
+ const file = path.join(installation.root, 'updater-worker.json');
524
+ if (fs.existsSync(file)) { const previous = privateJson(file); if (Number.isSafeInteger(previous.pid) && live(previous.pid)) return; fs.unlinkSync(file); }
525
+ const script = path.join(packageDirectory(value.current.directory), 'src/update-worker.cjs'); installation.canonical(script);
526
+ const log = fs.openSync(path.join(installation.root, 'updater.log'), 'a', 0o600);
527
+ 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];
528
+ 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);
529
+ }
530
+ 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};