feat: add CloudCLI computer use semantics, desktop helper packaging, and permission onboarding

This commit is contained in:
Simos Mikelatos
2026-06-19 12:09:55 +00:00
parent a35200f340
commit 1726705459
37 changed files with 3036 additions and 426 deletions

View File

@@ -0,0 +1,133 @@
#!/usr/bin/env node
import { spawn } from 'node:child_process';
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const rootDir = path.resolve(__dirname, '..');
const platform = process.env.CLOUDCLI_SEMANTICS_PLATFORM || process.platform;
const arch = process.env.CLOUDCLI_SEMANTICS_ARCH || process.arch;
const platformArch = `${platform}-${arch}`;
const semanticsRoot = path.join(rootDir, 'server', 'modules', 'computer-use', 'semantics');
const outDir = path.join(semanticsRoot, 'bin', platformArch);
const requireBuild = process.env.CLOUDCLI_SEMANTICS_BUILD_REQUIRED === '1';
function run(command, args, options = {}) {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
stdio: 'inherit',
shell: process.platform === 'win32',
...options,
});
child.once('error', reject);
child.once('exit', (code) => {
if (code === 0) resolve();
else reject(new Error(`${command} ${args.join(' ')} exited with code ${code}`));
});
});
}
function commandExists(command) {
return new Promise((resolve) => {
const child = spawn(command, ['--version'], {
stdio: 'ignore',
shell: process.platform === 'win32',
});
child.once('error', () => resolve(false));
child.once('exit', (code) => resolve(code === 0));
});
}
async function pathExists(filePath) {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
async function isUpToDate(output, inputs) {
if (!(await pathExists(output))) {
return false;
}
const outputStat = await fs.stat(output);
for (const input of inputs) {
const inputStat = await fs.stat(input);
if (inputStat.mtimeMs > outputStat.mtimeMs) {
return false;
}
}
return true;
}
async function ensureCommand(command, helpText) {
if (await commandExists(command)) {
return true;
}
const message = `${command} was not found. ${helpText}`;
if (requireBuild) {
throw new Error(message);
}
console.log(`Skipping semantic helper build: ${message}`);
return false;
}
if (platform === 'darwin') {
const source = path.join(semanticsRoot, 'helpers', 'macos', 'CloudCLISemantics.swift');
const output = path.join(outDir, 'CloudCLISemantics');
if (!(await ensureCommand('swiftc', 'Install Xcode Command Line Tools to compile the macOS helper.'))) {
process.exit(0);
}
if (await isUpToDate(output, [source])) {
console.log(`Semantic helper is up to date: ${path.relative(rootDir, output)}`);
process.exit(0);
}
await fs.mkdir(outDir, { recursive: true });
await run('swiftc', [
source,
'-o',
output,
'-framework',
'AppKit',
'-framework',
'ApplicationServices',
]);
await fs.chmod(output, 0o755);
console.log(`Built ${path.relative(rootDir, output)}`);
} else if (platform === 'win32') {
const project = path.join(semanticsRoot, 'helpers', 'windows', 'CloudCLISemantics.csproj');
const source = path.join(semanticsRoot, 'helpers', 'windows', 'Program.cs');
const output = path.join(outDir, 'CloudCLISemantics.exe');
if (!(await ensureCommand('dotnet', '.NET SDK is required to compile the Windows helper.'))) {
process.exit(0);
}
if (await isUpToDate(output, [project, source])) {
console.log(`Semantic helper is up to date: ${path.relative(rootDir, output)}`);
process.exit(0);
}
await fs.mkdir(outDir, { recursive: true });
await run('dotnet', [
'publish',
project,
'-c',
'Release',
'-r',
arch === 'arm64' ? 'win-arm64' : 'win-x64',
'--self-contained',
'false',
'-p:PublishSingleFile=true',
'-o',
outDir,
]);
console.log(`Built ${path.relative(rootDir, output)}`);
} else {
console.log(`Semantic helper build is not supported for ${platform}-${arch}.`);
}

View File

@@ -0,0 +1,24 @@
#!/usr/bin/env node
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const rootDir = path.resolve(__dirname, '..');
const sourceDir = path.join(rootDir, 'server', 'modules', 'computer-use', 'semantics', 'bin');
const targetDir = path.join(rootDir, 'dist-server', 'server', 'modules', 'computer-use', 'semantics', 'bin');
async function pathExists(filePath) {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
if (await pathExists(sourceDir)) {
await fs.mkdir(path.dirname(targetDir), { recursive: true });
await fs.cp(sourceDir, targetDir, { recursive: true });
console.log(`Copied Computer Use semantic helpers to ${path.relative(rootDir, targetDir)}`);
}

View File

@@ -108,8 +108,8 @@ function sha256(filePath) {
const platform = mapPlatform(process.env.CLOUDCLI_BUNDLE_PLATFORM || process.platform);
const arch = mapArch(process.env.CLOUDCLI_BUNDLE_ARCH || process.arch);
const version = packageJson.version;
const bundleName = `cloudcli-server-${version}-${platform}-${arch}.tar.gz`;
const bundleRoot = path.join(rootDir, 'release', 'server-bundles');
const bundleName = `cloudcli-local-server-${version}-${platform}-${arch}.tar.gz`;
const bundleRoot = path.join(rootDir, 'release', 'local-server');
const stageDir = path.join(bundleRoot, `.stage-${version}-${platform}-${arch}`);
const archivePath = path.join(bundleRoot, bundleName);

View File

@@ -85,7 +85,7 @@ function buildDesktopPackageJson(copiedOptionalDependencies) {
artifactName: packageJson.build.artifactName,
electronVersion: getElectronVersion(),
directories: {
output: '../../release',
output: '../../release/desktop',
},
extraMetadata: {
main: 'electron/main.js',
@@ -116,8 +116,7 @@ await copyRequired('public');
// not the full local server. Local CloudCLI is downloaded on demand.
await copyRequired('dist-server/server/computer-use-agent.js');
await copyIfExists('dist-server/server/computer-use-agent.js.map');
await copyRequired('dist-server/server/modules/computer-use/computer-executor.js');
await copyIfExists('dist-server/server/modules/computer-use/computer-executor.js.map');
await copyRequired('dist-server/server/modules/computer-use');
const copiedRuntimeDependencies = [];
if (await copyNodeModule('ws')) {