@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.
- package/README.md +40 -4
- package/bin/aglit.cjs +171 -72
- package/package.json +2 -1
- package/src/installation.cjs +120 -81
- package/src/launcher.cjs +30 -0
- package/src/ui.cjs +81 -0
- package/src/update-worker.cjs +37 -0
- package/src/updates.cjs +530 -0
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
|
+
}
|
package/src/ui.cjs
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const clean = value => String(value ?? '').replace(/[\x00-\x1f\x7f-\x9f]/g, '');
|
|
4
|
+
function teamsFromRoles(value) {
|
|
5
|
+
const teams = new Map();
|
|
6
|
+
for (const company of value?.companies || []) {
|
|
7
|
+
for (const team of [...(company.teams || []), ...(company.organizations || []).flatMap(organization => organization.teams || [])]) {
|
|
8
|
+
if (typeof team.teamId === 'string') teams.set(team.teamId, { id: team.teamId, roles: team.roles || [] });
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
for (const role of value?.roles || []) {
|
|
12
|
+
const id = role.teamId || (role.scopeType === 'team' ? role.scopeId : null);
|
|
13
|
+
if (typeof id === 'string' && !teams.has(id)) teams.set(id, { id, roles: [role.roleLabel || role.role].filter(Boolean) });
|
|
14
|
+
}
|
|
15
|
+
return [...teams.values()];
|
|
16
|
+
}
|
|
17
|
+
async function createUi(interactive) {
|
|
18
|
+
if (!interactive) return {
|
|
19
|
+
interactive: false,
|
|
20
|
+
step: (_label, action) => action(),
|
|
21
|
+
result: value => process.stdout.write(JSON.stringify(value, null, 2) + '\n'),
|
|
22
|
+
info: message => process.stderr.write(message + '\n'),
|
|
23
|
+
finish() {},
|
|
24
|
+
};
|
|
25
|
+
const p = await import('@clack/prompts');
|
|
26
|
+
p.intro('Aglit Computer Use');
|
|
27
|
+
async function answer(promise) {
|
|
28
|
+
const value = await promise;
|
|
29
|
+
if (p.isCancel(value)) {
|
|
30
|
+
p.cancel('Setup cancelled.');
|
|
31
|
+
throw Object.assign(Error('Setup cancelled'), { cancelled: true });
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
interactive: true,
|
|
37
|
+
name: initialValue => answer(p.text({ message: 'Name this computer', placeholder: initialValue, defaultValue: initialValue,
|
|
38
|
+
validate: value => value && (!value.trim() || value.length > 100 || /[\x00-\x1f\x7f-\x9f]/.test(value)) ? 'Use 1–100 printable characters.' : undefined })),
|
|
39
|
+
mode: () => answer(p.select({ message: 'How should Aglit run?', initialValue: 'headless', options: [
|
|
40
|
+
{ value: 'headless', label: 'Headless', hint: 'default · background' },
|
|
41
|
+
{ value: 'headed', label: 'Open Desktop window', hint: 'headed' },
|
|
42
|
+
] })),
|
|
43
|
+
provider: (providers, preferred) => answer(p.select({ message: 'Choose your AI provider', initialValue: preferred,
|
|
44
|
+
options: providers.map(value => ({ value, label: value === 'codex' ? 'Codex' : 'Claude' })) })),
|
|
45
|
+
team: teams => answer(p.select({ message: 'Team for invitations', initialValue: '', options: [
|
|
46
|
+
{ value: '', label: 'Personal computer', hint: 'access stays with your account' },
|
|
47
|
+
...teams.map(team => ({ value: team.id, label: clean(team.id), hint: team.roles.map(clean).join(', ') })),
|
|
48
|
+
] })),
|
|
49
|
+
teams(value) {
|
|
50
|
+
const teams = teamsFromRoles(value);
|
|
51
|
+
p.note(teams.length ? teams.map(team => `${clean(team.id)} ${team.roles.map(clean).join(', ')}`).join('\n') : 'No team memberships.', 'Your teams');
|
|
52
|
+
return teams;
|
|
53
|
+
},
|
|
54
|
+
pipelines(value) {
|
|
55
|
+
p.note((value.devices.length ? value.devices.map(device => `${clean(device.name)}${device.platform ? ' · ' + clean(device.platform) : ''}`).join('\n') + '\n\n' : 'No authorized pipeline devices.\n\n') +
|
|
56
|
+
'https://pipelines.aglit.ai/\nOpen the encrypted viewer with aglit pipelines --open.', 'Pipeline devices');
|
|
57
|
+
},
|
|
58
|
+
confirmAccount: email => answer(p.confirm({ message: `Register with ${clean(email)}?`, initialValue: true })),
|
|
59
|
+
async step(label, action) {
|
|
60
|
+
const spinner = p.spinner(); spinner.start(label);
|
|
61
|
+
try { const value = await action(); spinner.stop(label); return value; }
|
|
62
|
+
catch (error) { spinner.stop('Setup paused'); throw error; }
|
|
63
|
+
},
|
|
64
|
+
result(value) {
|
|
65
|
+
if (value.product === 'Aglit Computer Use') {
|
|
66
|
+
p.note([
|
|
67
|
+
value.name && `Computer ${clean(value.name)}`,
|
|
68
|
+
value.email && `Account ${clean(value.email)}`,
|
|
69
|
+
`Runtime ${value.running ? 'running' : value.installed ? 'stopped' : 'not installed'}`,
|
|
70
|
+
value.mode && `Window ${value.windowVisible ? 'open' : 'hidden'}`,
|
|
71
|
+
`Access ${value.connected ? 'connected' : value.enabled ? 'connecting' : 'disabled'}`,
|
|
72
|
+
value.provider && `Provider ${clean(value.provider)}`,
|
|
73
|
+
].filter(Boolean).join('\n'), 'This computer');
|
|
74
|
+
} else p.log.info(JSON.stringify(value, null, 2));
|
|
75
|
+
},
|
|
76
|
+
info: message => p.log.info(clean(message)),
|
|
77
|
+
finish: message => p.outro(message),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { createUi, teamsFromRoles };
|
|
@@ -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; });
|