@aglit-ai/cli 0.1.2 → 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.
- package/README.md +16 -0
- package/bin/aglit.cjs +51 -11
- package/package.json +1 -1
- package/src/installation.cjs +120 -81
- package/src/launcher.cjs +30 -0
- package/src/update-worker.cjs +37 -0
- package/src/updates.cjs +530 -0
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] [--
|
|
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 > (
|
|
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
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
package/src/installation.cjs
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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 };
|
package/src/launcher.cjs
ADDED
|
@@ -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; });
|
package/src/updates.cjs
ADDED
|
@@ -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};
|