@awak-app/simy-cli 0.5.20 → 0.5.21

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awak-app/simy-cli",
3
- "version": "0.5.20",
3
+ "version": "0.5.21",
4
4
  "description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,7 @@ import {
13
13
  } from "./durable-local-task-steps-contract.js";
14
14
  import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
15
15
 
16
- export const CLI_VERSION = "0.5.20";
16
+ export const CLI_VERSION = "0.5.21";
17
17
  export const CLI_API_CONTRACT_VERSION = 10;
18
18
 
19
19
  export function withCliContract(
@@ -1,3 +1,4 @@
1
+ import { createWindowsDesktopUpdater } from "./windows-desktop-updater.js";
1
2
  import { spawn, execFile } from "node:child_process";
2
3
  import { promisify } from "node:util";
3
4
  import { createInterface } from "node:readline";
@@ -70,7 +71,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
70
71
  if (!candidate) {
71
72
  await (dependencies.assertNoLegacyDaemon || assertNoLegacyCliDaemon)(webOrigin);
72
73
  }
73
- updater = await (dependencies.createUpdater || createCliAutoUpdater)({
74
+ updater = await (dependencies.createUpdater || (process.platform === "win32" ? (() => createWindowsDesktopUpdater({ version: packageInfo.version, disabled: env.SIMY_AUTO_UPDATE === "0" })) : createCliAutoUpdater))({
74
75
  daemon: true, interactive: true, webOrigin,
75
76
  disabled: env.SIMY_AUTO_UPDATE === "0",
76
77
  dependencies: { packageInfo, logger() {} },
@@ -194,6 +195,13 @@ function validCandidateStatus(status, version, webOrigin) {
194
195
  async function openCodexLogin(root) {
195
196
  const codex = await inspectBackendExecutable("codex");
196
197
  if (!codex.available || !codex.executable) throw new Error("Codex is not installed.");
198
+ if (process.platform === "win32") {
199
+ const child = spawn(join(process.env.SystemRoot || "C:\\Windows", "System32/WindowsPowerShell/v1.0/powershell.exe"),
200
+ ["-NoProfile", "-NoExit", "-Command", "& $env:SIMY_CODEX_LOGIN login"],
201
+ { detached: true, stdio: "ignore", windowsHide: false, env: { ...process.env, SIMY_CODEX_LOGIN: codex.executable } });
202
+ await new Promise((resolve, reject) => { child.once("spawn", resolve); child.once("error", reject); });
203
+ child.unref(); return;
204
+ }
197
205
  const directory = await mkdtemp(join(root, "codex-login-"));
198
206
  const script = join(directory, "Sign in to Codex.command");
199
207
  const quote = (s) => `'${s.replaceAll("'", "'\\''")}'`;
@@ -1,5 +1,6 @@
1
1
  import { mkdir, readFile, realpath, rm, writeFile, stat } from "node:fs/promises";
2
- import { basename, dirname, join } from "node:path";
2
+ import { dirname, join } from "node:path";
3
+ import { windowsProcessStart, windowsUserProcesses } from "./windows-processes.js";
3
4
  import { randomBytes } from "node:crypto";
4
5
  import { execFile } from "node:child_process";
5
6
  import { promisify } from "node:util";
@@ -161,8 +162,8 @@ export async function stopConfirmedCliConflicts(webOrigin, approved, { force = f
161
162
 
162
163
  export function parseLegacyAgentArguments(args) {
163
164
  const argv = Array.isArray(args) ? args : recoverProcessArguments(args);
164
- if (argv.length < 2 || !["node", "nodejs"].includes(basename(argv[0]))) return null;
165
- const entryIndex = argv.findIndex((value, index) => index > 0 && /(?:^|\/)src\/index\.js$/.test(value));
165
+ if (argv.length < 2 || !["node", "nodejs", "node.exe", "nodejs.exe"].includes(argv[0].replaceAll("\\", "/").split("/").at(-1).toLowerCase())) return null;
166
+ const entryIndex = argv.findIndex((value, index) => index > 0 && /(?:^|\/)src\/index\.js$/.test(value.replaceAll("\\", "/")));
166
167
  if (entryIndex < 1) return null;
167
168
  const agentArgs = argv.slice(entryIndex + 1);
168
169
  if (agentArgs.includes("--help") || agentArgs.includes("-h")) return null;
@@ -224,6 +225,7 @@ export async function validateDesktopSupervisor(root, nonce) {
224
225
  }
225
226
 
226
227
  async function processStart(pid) {
228
+ if (process.platform === "win32") return windowsProcessStart(pid);
227
229
  const { stdout } = await promisify(execFile)("/bin/ps", ["-p", String(pid), "-o", "lstart="], { timeout: 2_000 });
228
230
  return stdout.trim();
229
231
  }
@@ -233,6 +235,7 @@ function processAlive(pid) {
233
235
  }
234
236
 
235
237
  async function listSameUserProcesses() {
238
+ if (process.platform === "win32") return windowsUserProcesses();
236
239
  const uid = process.getuid?.();
237
240
  if (!Number.isSafeInteger(uid)) return [];
238
241
  const { stdout } = await promisify(execFile)("/bin/ps", ["-ww", "-axo", "pid=,uid=,command="], {
@@ -1,5 +1,6 @@
1
1
  import { fileURLToPath } from 'node:url';
2
2
  import path from 'node:path';
3
+ import { existsSync } from 'node:fs';
3
4
  import { spawn } from 'node:child_process';
4
5
  import { maintainWindowsUpdate, selectedWindowsRoot } from './windows-update.js';
5
6
  const installation = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
@@ -18,8 +19,11 @@ try {
18
19
  if (updating) {
19
20
  if (!process.exitCode) console.log(`SIMY Windows installation ready: ${root}`);
20
21
  } else {
21
- const child = spawn(path.join(root, 'runtime/node.exe'), [path.join(root, 'cli/src/index.js'), ...argv], {
22
- stdio: 'inherit', env: { ...process.env, SIMY_AUTO_UPDATE: '0' },
22
+ const desktop = argv[0] === '--desktop';
23
+ if (desktop && !existsSync(path.join(root, 'simy-desktop.exe'))) root = installation;
24
+ const child = spawn(path.join(root, desktop ? 'simy-desktop.exe' : 'runtime/node.exe'),
25
+ desktop ? ['--selected'] : [path.join(root, 'cli/src/index.js'), ...argv], {
26
+ stdio: 'inherit', env: { ...process.env, ...(desktop ? {} : { SIMY_AUTO_UPDATE: '0' }) },
23
27
  });
24
28
  // Windows console signals are already delivered to both processes.
25
29
  process.on('SIGINT', () => {});
@@ -0,0 +1,39 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { WINDOWS_MANIFEST_URL, validateWindowsRelease, compareWindowsVersions } from './windows-update.js';
3
+ import { openAuthorizationUrl } from './browser.js';
4
+
5
+ // Windows selects verified immutable releases at startup. Never replace a live
6
+ // desktop/agent during a task; the UI offers the installer for immediate upgrades.
7
+ export function createWindowsDesktopUpdater({ version, fetchImpl = fetch, open = openAuthorizationUrl, disabled = false }) {
8
+ const events = new EventEmitter();
9
+ let timer, initial, release;
10
+ let state = { state: disabled ? 'disabled' : 'idle', current_version: version, automatic: false, install_mode: 'windows_installer',
11
+ message: 'Updates are applied when SIMY starts. Check now to download the latest installer.' };
12
+ const update = values => { state = { ...state, ...values }; events.emit('change'); };
13
+ let pending;
14
+ events.snapshot = () => structuredClone(state);
15
+ events.start = () => {
16
+ if (disabled || timer) return;
17
+ initial = setTimeout(() => void events.checkNow(), 15000); initial.unref?.();
18
+ timer = setInterval(() => void events.checkNow(), 3600000); timer.unref?.();
19
+ };
20
+ events.stop = () => { clearTimeout(initial); clearInterval(timer); timer = null; };
21
+ events.checkNow = () => pending ||= (async () => {
22
+ update({ state: 'checking', message: 'Checking for updates…' });
23
+ try {
24
+ const response = await fetchImpl(WINDOWS_MANIFEST_URL, { signal: AbortSignal.timeout(15000), redirect: 'error' });
25
+ if (!response.ok) throw new Error('Update service unavailable');
26
+ release = validateWindowsRelease(await response.json());
27
+ const newer = compareWindowsVersions(release.version, version) > 0;
28
+ update({ state: newer ? 'update_available' : 'up_to_date', target_version: newer ? release.version : null,
29
+ message: newer ? `SIMY ${release.version} is available. Install it after your tasks finish.` : 'SIMY is up to date.',
30
+ action: newer ? { url: release.artifacts[0].url } : null });
31
+ } catch { update({ state: 'failed', message: 'Could not check for updates. Try again.', action: null }); }
32
+ return events.snapshot();
33
+ })().finally(() => { pending = null; });
34
+ events.performAction = async () => { if (state.action?.url) await open(state.action.url); else await events.checkNow(); };
35
+ events.maybeApply = events.performAction;
36
+ events.beginWork = () => () => {};
37
+ events.acceptsNewWork = () => true;
38
+ return events;
39
+ }
@@ -0,0 +1,37 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import path from 'node:path';
4
+ const run = promisify(execFile);
5
+ export async function windowsProcessStart(pid) {
6
+ if (!Number.isSafeInteger(pid) || pid <= 0) throw new Error('Invalid process');
7
+ const { stdout } = await powershell('(Get-Process -Id ([int]$env:SIMY_INSPECT_PID) -ErrorAction Stop).StartTime.ToUniversalTime().ToString("o")', { SIMY_INSPECT_PID: String(pid) });
8
+ return stdout.trim();
9
+ }
10
+ export async function windowsUserProcesses() {
11
+ const { stdout } = await powershell(`$ErrorActionPreference='Stop';$sid=[Security.Principal.WindowsIdentity]::GetCurrent().User.Value;$rows=@(Get-CimInstance Win32_Process -Filter "Name='node.exe' OR Name='nodejs.exe'" | ForEach-Object {$owner=Invoke-CimMethod -InputObject $_ -MethodName GetOwnerSid;if($owner.Sid -eq $sid){@{pid=$_.ProcessId;command=$_.CommandLine}}});ConvertTo-Json -InputObject $rows -Compress`);
12
+ return JSON.parse(stdout || '[]').map(row => ({ pid: Number(row.pid), args: splitWindowsArguments(row.command || '') }));
13
+ }
14
+ function powershell(script, env = {}) {
15
+ return run(path.join(process.env.SystemRoot || 'C:\\Windows', 'System32/WindowsPowerShell/v1.0/powershell.exe'),
16
+ ['-NoProfile', '-NonInteractive', '-Command', script], { timeout: 15000, windowsHide: true, maxBuffer: 2*1024*1024, env: { ...process.env, ...env } });
17
+ }
18
+ // CommandLineToArgvW backslash/quote rules; never shell-unescape Windows paths.
19
+ export function splitWindowsArguments(command) {
20
+ const result = []; let i = 0;
21
+ while (i < command.length) {
22
+ while (/\s/.test(command[i] || '') && i < command.length) i++;
23
+ if (i === command.length) break;
24
+ let value = '', quoted = false;
25
+ while (i < command.length && (quoted || !/\s/.test(command[i]))) {
26
+ let slashes = 0;
27
+ while (command[i] === '\\') { slashes++; i++; }
28
+ if (command[i] === '"') {
29
+ value += '\\'.repeat(Math.floor(slashes / 2));
30
+ if (slashes % 2) value += '"'; else quoted = !quoted;
31
+ i++;
32
+ } else { value += '\\'.repeat(slashes); if (i < command.length) value += command[i++]; }
33
+ }
34
+ result.push(value);
35
+ }
36
+ return result;
37
+ }
@@ -83,7 +83,7 @@ async function verifyWindowsCandidate(candidate, marker) {
83
83
  if (marker?.signed) {
84
84
  if (!/^[A-Fa-f0-9]{40}$/.test(marker.thumbprint || '')) throw new Error('Missing trusted signer');
85
85
  // Paths are passed through environment values, never inserted into shell source.
86
- await exec(powershell(), ['-NoProfile', '-NonInteractive', '-Command', "$ErrorActionPreference='Stop';foreach($file in @('simy.exe','runtime/node.exe')){$s=Get-AuthenticodeSignature (Join-Path $env:SIMY_CANDIDATE $file);if($s.Status -ne 'Valid'){throw 'Invalid signature'};if($file -eq 'simy.exe' -and $s.SignerCertificate.Thumbprint -ne $env:SIMY_SIGNER){throw 'Unexpected signer'}}"], { env: { ...process.env, SIMY_CANDIDATE: candidate, SIMY_SIGNER: marker.thumbprint }, timeout: 30_000, windowsHide: true });
86
+ await exec(powershell(), ['-NoProfile', '-NonInteractive', '-Command', "$ErrorActionPreference='Stop';foreach($file in @('simy.exe','simy-desktop.exe','runtime/node.exe')){$s=Get-AuthenticodeSignature (Join-Path $env:SIMY_CANDIDATE $file);if($s.Status -ne 'Valid'){throw 'Invalid signature'};if($file -ne 'runtime/node.exe' -and $s.SignerCertificate.Thumbprint -ne $env:SIMY_SIGNER){throw 'Unexpected signer'}}"], { env: { ...process.env, SIMY_CANDIDATE: candidate, SIMY_SIGNER: marker.thumbprint }, timeout: 30_000, windowsHide: true });
87
87
  }
88
88
  const probeHome = path.join(candidate, '.update-probe');
89
89
  try {