@engenty/wizards-runtime 0.7.9 → 0.7.10
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 +2 -1
- package/apps/runtime/dist/app.js +2 -2
- package/apps/runtime/dist/cli/autostart.js +90 -7
- package/apps/runtime/dist/cli/autoupdate.js +18 -5
- package/apps/runtime/dist/cli/clients.js +25 -6
- package/apps/runtime/dist/cli/connect.js +7 -2
- package/apps/runtime/dist/cli/environment.js +33 -2
- package/apps/runtime/dist/cli/exec.js +38 -1
- package/apps/runtime/dist/cli/home.js +15 -0
- package/apps/runtime/dist/cli/machine.js +18 -8
- package/apps/runtime/dist/cli/processes.js +23 -3
- package/apps/runtime/dist/cli/setup.js +11 -1
- package/apps/runtime/dist/cli/start.js +33 -7
- package/apps/runtime/dist/cli/uninstall.js +100 -6
- package/apps/runtime/dist/cli/update.js +28 -12
- package/apps/runtime/dist/cli/versions.js +16 -4
- package/apps/runtime/dist/docs/user/install.md +18 -8
- package/apps/runtime/dist/harness/env.js +41 -8
- package/apps/runtime/dist/harness/index.js +10 -1
- package/apps/runtime/dist/harness/model.js +1 -1
- package/apps/runtime/dist/render/browser-path.js +43 -11
- package/apps/runtime/dist/update.js +4 -0
- package/apps/web/dist/assets/{AboutDialog-CShc0mRo.js → AboutDialog-B2LuZ_mC.js} +1 -1
- package/apps/web/dist/assets/{ChangelogDialog-C4dVSwtN.js → ChangelogDialog-B0lS0BdL.js} +1 -1
- package/apps/web/dist/assets/{ChatView-KkOboyOo.js → ChatView-BcPDcU2d.js} +1 -1
- package/apps/web/dist/assets/{EditorPage-lQ3YgsFT.js → EditorPage-erduYKNM.js} +1 -1
- package/apps/web/dist/assets/{Harness-D6mgLWTb.js → Harness-Dv9ln5xB.js} +1 -1
- package/apps/web/dist/assets/{NewWizardChannels-CPlh_37J.js → NewWizardChannels-JveYyh-U.js} +1 -1
- package/apps/web/dist/assets/{NewWizardPage-D0FVJtXK.js → NewWizardPage-Dfk-WOhX.js} +1 -1
- package/apps/web/dist/assets/{OnboardingPage-BXPOMrQR.js → OnboardingPage-CT9dL4ke.js} +1 -1
- package/apps/web/dist/assets/{ProjectPage-DroTprqs.js → ProjectPage-5_Wsk9Hv.js} +1 -1
- package/apps/web/dist/assets/{PublicRunner-j3qoKjQQ.js → PublicRunner-Cw1WzTze.js} +1 -1
- package/apps/web/dist/assets/{ReadOnly-CKgSQ2z1.js → ReadOnly-S9vGVQHr.js} +1 -1
- package/apps/web/dist/assets/{RunDrawer-CJZ_ISAd.js → RunDrawer-O4rEFCL8.js} +1 -1
- package/apps/web/dist/assets/{SettingsPage-BHZ0Z-og.js → SettingsPage-DmhN7OTy.js} +1 -1
- package/apps/web/dist/assets/{SetupPage-CDkYkjJX.js → SetupPage-BlWd7Uln.js} +1 -1
- package/apps/web/dist/assets/{SharePage-BlVjVjHG.js → SharePage-ktqm-jWI.js} +1 -1
- package/apps/web/dist/assets/changelog-FJJS0Tpt.js +1 -0
- package/apps/web/dist/assets/{index-C61HAltK.js → index-xH9uN8Nh.js} +3 -3
- package/apps/web/dist/assets/{oss-credits-qGhcbqiM.js → oss-credits-Bc05Qv5H.js} +1 -1
- package/apps/web/dist/index.html +1 -1
- package/apps/web/dist/install.ps1 +307 -0
- package/bin/wizards.mjs +59 -35
- package/package.json +2 -5
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/apps/web/dist/assets/changelog-Be0i5Jjw.js +0 -1
package/README.md
CHANGED
|
@@ -57,7 +57,8 @@ the cloud behind the same links. On macOS or Linux, in a terminal:
|
|
|
57
57
|
curl -fsSL https://engenty.ai/install.sh | bash
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
With Node.js 20 or newer on
|
|
60
|
+
On Windows, in PowerShell: `irm https://engenty.ai/install.ps1 | iex`. With Node.js 20 or newer on
|
|
61
|
+
the machine, `npx wizards` does the same.
|
|
61
62
|
|
|
62
63
|
It needs nothing but `curl` and `tar` and asks for no password. It brings its own Node.js (a
|
|
63
64
|
Node you already have is left alone), installs engenty wizards into `~/.engenty/wizards` and
|
package/apps/runtime/dist/app.js
CHANGED
|
@@ -305,13 +305,13 @@ async function previewFor(path) {
|
|
|
305
305
|
/**
|
|
306
306
|
* Production: the built app. Its files and the studio's pages sit below `/studio`; the public
|
|
307
307
|
* pages are `/w/<token>` and `/s/<token>`. Of the root only what others fetch there is served:
|
|
308
|
-
* the
|
|
308
|
+
* the installers, the embed script for websites, and the service worker of public wizards. The
|
|
309
309
|
* rest of the root belongs to others (the landing page); a runtime alone sends `/` to the studio.
|
|
310
310
|
* The app's files are served by both halves: a wizard's page loads its scripts from `/studio`
|
|
311
311
|
* too. Which pages a half answers follows its role.
|
|
312
312
|
*/
|
|
313
313
|
const webDir = resolve(dirname(fileURLToPath(import.meta.url)), "../../web/dist");
|
|
314
|
-
const STUDIO_FILES = ["/install.sh"];
|
|
314
|
+
const STUDIO_FILES = ["/install.sh", "/install.ps1"];
|
|
315
315
|
const DOORS_FILES = ["/embed.js", "/sw.js"];
|
|
316
316
|
const PAGES = new RegExp(`^/(${[...(serves.studio ? ["studio(/|$)"] : []), ...(serves.doors ? ["w/", "s/"] : [])].join("|")})`);
|
|
317
317
|
if (existsSync(webDir)) {
|
|
@@ -3,14 +3,15 @@ import { lstatSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
|
-
import { installedByScript } from "./home.js";
|
|
6
|
+
import { installedByScript, installedScript } from "./home.js";
|
|
7
7
|
import { findOnPath } from "./machine.js";
|
|
8
8
|
const run = promisify(execFile);
|
|
9
9
|
/**
|
|
10
10
|
* engenty wizards starting by itself when the person logs in, so that links and AI clients
|
|
11
11
|
* reach it without anybody starting it: `wizards start --no-open`, on a Mac from a
|
|
12
|
-
* LaunchAgent, on Linux from a systemd user service
|
|
13
|
-
*
|
|
12
|
+
* LaunchAgent, on Linux from a systemd user service, on Windows from a scheduled task (or, where
|
|
13
|
+
* Windows refuses one without an administrator, a shortcut in the Startup folder). Only for an
|
|
14
|
+
* install made by the installer, whose command stays where it is.
|
|
14
15
|
*/
|
|
15
16
|
export const LOGIN_LABEL = "com.engenty.wizards.login";
|
|
16
17
|
/** The LaunchAgent. */
|
|
@@ -80,6 +81,82 @@ async function systemdHere() {
|
|
|
80
81
|
return false;
|
|
81
82
|
}
|
|
82
83
|
}
|
|
84
|
+
const TASK = "engenty wizards";
|
|
85
|
+
const TASK_HOW = `a scheduled task, ${TASK}`;
|
|
86
|
+
const STARTUP_HOW = "a shortcut in the Startup folder";
|
|
87
|
+
/** The shortcut Windows starts at login when no task may be made. */
|
|
88
|
+
function startupShortcut(env = process.env) {
|
|
89
|
+
return join(env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "Microsoft", "Windows", "Start Menu", "Programs", "Startup", `${TASK}.lnk`);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* What Windows starts: the install's Node and script through conhost without a window — Node is
|
|
93
|
+
* a console program, and the task or shortcut would otherwise open a console at every login.
|
|
94
|
+
*/
|
|
95
|
+
export function windowsStart(paths) {
|
|
96
|
+
const { node, script } = installedScript(paths, "win32");
|
|
97
|
+
return {
|
|
98
|
+
file: join(process.env.SystemRoot ?? "C:\\Windows", "System32", "conhost.exe"),
|
|
99
|
+
args: ["--headless", node, script, "start", "--no-open"],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const quoted = (arg) => (/[\s"]/.test(arg) ? `"${arg.replaceAll('"', '\\"')}"` : arg);
|
|
103
|
+
async function windowsState() {
|
|
104
|
+
try {
|
|
105
|
+
await run("schtasks", ["/Query", "/TN", TASK], { timeout: 10_000 });
|
|
106
|
+
return { on: true, how: TASK_HOW };
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return present(startupShortcut())
|
|
110
|
+
? { on: true, how: STARTUP_HOW }
|
|
111
|
+
: { on: false, how: TASK_HOW };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** The Windows login item, as the uninstall names it; null without one. */
|
|
115
|
+
export async function windowsLoginItem() {
|
|
116
|
+
const state = await windowsState();
|
|
117
|
+
if (!("on" in state) || !state.on) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return state.how === STARTUP_HOW ? startupShortcut() : `the scheduled task “${TASK}”`;
|
|
121
|
+
}
|
|
122
|
+
/** Takes the scheduled task and the Startup shortcut away. */
|
|
123
|
+
export async function removeWindowsLoginItem() {
|
|
124
|
+
await run("schtasks", ["/Delete", "/F", "/TN", TASK], { timeout: 10_000 }).catch(() => undefined);
|
|
125
|
+
rmSync(startupShortcut(), { force: true });
|
|
126
|
+
}
|
|
127
|
+
async function setWindows(paths, on) {
|
|
128
|
+
await run("schtasks", ["/Delete", "/F", "/TN", TASK], { timeout: 10_000 }).catch(() => undefined);
|
|
129
|
+
rmSync(startupShortcut(), { force: true });
|
|
130
|
+
if (!on) {
|
|
131
|
+
return windowsState();
|
|
132
|
+
}
|
|
133
|
+
// Takes effect at the next login, as on a Mac; nothing is started now.
|
|
134
|
+
const { file, args } = windowsStart(paths);
|
|
135
|
+
const command = [file, ...args].map(quoted).join(" ");
|
|
136
|
+
try {
|
|
137
|
+
await run("schtasks", ["/Create", "/F", "/TN", TASK, "/SC", "ONLOGON", "/RL", "LIMITED", "/TR", command], {
|
|
138
|
+
timeout: 15_000,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// A task at logon may need an administrator; the Startup folder needs none.
|
|
143
|
+
const ps = (text) => `'${text.replaceAll("'", "''")}'`;
|
|
144
|
+
await run("powershell.exe", [
|
|
145
|
+
"-NoProfile",
|
|
146
|
+
"-Command",
|
|
147
|
+
[
|
|
148
|
+
"$s = (New-Object -ComObject WScript.Shell).CreateShortcut(" +
|
|
149
|
+
ps(startupShortcut()) +
|
|
150
|
+
")",
|
|
151
|
+
`$s.TargetPath = ${ps(file)}`,
|
|
152
|
+
`$s.Arguments = ${ps(args.map(quoted).join(" "))}`,
|
|
153
|
+
`$s.WorkingDirectory = ${ps(paths.home)}`,
|
|
154
|
+
"$s.Save()",
|
|
155
|
+
].join("; "),
|
|
156
|
+
], { timeout: 15_000 });
|
|
157
|
+
}
|
|
158
|
+
return windowsState();
|
|
159
|
+
}
|
|
83
160
|
/**
|
|
84
161
|
* Whether a file is there. Unlike existsSync, a folder it may not look into (a sandbox, its
|
|
85
162
|
* permissions) is an error, not "no file": `autostart off` must not say it's off then.
|
|
@@ -102,10 +179,13 @@ export function present(file) {
|
|
|
102
179
|
* write one.
|
|
103
180
|
*/
|
|
104
181
|
export async function autostartState(paths) {
|
|
105
|
-
if (
|
|
106
|
-
return {
|
|
107
|
-
|
|
108
|
-
|
|
182
|
+
if (!["darwin", "linux", "win32"].includes(process.platform)) {
|
|
183
|
+
return { unavailable: `not on ${process.platform}` };
|
|
184
|
+
}
|
|
185
|
+
if (process.platform === "win32") {
|
|
186
|
+
return installedByScript(paths)
|
|
187
|
+
? windowsState()
|
|
188
|
+
: { unavailable: "only for installs made with the install script" };
|
|
109
189
|
}
|
|
110
190
|
const file = process.platform === "darwin" ? LOGIN_ITEM : unitFile();
|
|
111
191
|
const there = present(file);
|
|
@@ -153,6 +233,9 @@ export async function setAutostart(paths, on) {
|
|
|
153
233
|
if ("unavailable" in state) {
|
|
154
234
|
return state;
|
|
155
235
|
}
|
|
236
|
+
if (process.platform === "win32") {
|
|
237
|
+
return setWindows(paths, on);
|
|
238
|
+
}
|
|
156
239
|
if (process.platform === "darwin") {
|
|
157
240
|
// Takes effect at the next login; nothing is started or stopped now.
|
|
158
241
|
if (on) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { mkdirSync, mkdtempSync, openSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { delimiter, join } from "node:path";
|
|
4
|
+
import { withPath } from "./environment.js";
|
|
4
5
|
import { readEnvFile, readInstallNote } from "./home.js";
|
|
5
6
|
import { currentVersion, promote, prune, RUNTIME_PACKAGE, runtimeDir, switchTo, versionIn, } from "./versions.js";
|
|
6
7
|
/**
|
|
@@ -92,14 +93,26 @@ async function latestVersion(deps, env) {
|
|
|
92
93
|
}
|
|
93
94
|
/** npm of the install's own Node, as install.sh runs it; its output goes to logs/update.log. */
|
|
94
95
|
async function npmInstall(paths, prefix, spec) {
|
|
95
|
-
|
|
96
|
+
// On Windows Node sits in the folder itself, and npm is its npm-cli.js (npm.cmd starts only
|
|
97
|
+
// through cmd.exe).
|
|
98
|
+
const windows = process.platform === "win32";
|
|
99
|
+
const bin = windows ? join(paths.tools, "node") : join(paths.tools, "node", "bin");
|
|
96
100
|
mkdirSync(paths.logs, { recursive: true });
|
|
97
101
|
const log = openSync(join(paths.logs, "update.log"), "a");
|
|
98
|
-
const args =
|
|
102
|
+
const args = [
|
|
103
|
+
..."install --omit=dev --ignore-scripts --legacy-peer-deps --no-audit --no-fund --loglevel=error".split(" "),
|
|
104
|
+
"--prefix",
|
|
105
|
+
prefix,
|
|
106
|
+
spec,
|
|
107
|
+
];
|
|
108
|
+
const [command, all] = windows
|
|
109
|
+
? [join(bin, "node.exe"), [join(bin, "node_modules", "npm", "bin", "npm-cli.js"), ...args]]
|
|
110
|
+
: [join(bin, "npm"), args];
|
|
99
111
|
const code = await new Promise((done) => {
|
|
100
|
-
const child = spawn(
|
|
112
|
+
const child = spawn(command, all, {
|
|
101
113
|
stdio: ["ignore", log, log],
|
|
102
|
-
env:
|
|
114
|
+
env: withPath(process.env, [bin, process.env.PATH ?? ""].join(delimiter)),
|
|
115
|
+
windowsHide: true,
|
|
103
116
|
});
|
|
104
117
|
child.on("error", () => done(127));
|
|
105
118
|
child.on("exit", (exit) => done(exit ?? 1));
|
|
@@ -1,16 +1,31 @@
|
|
|
1
1
|
import { mkdirSync } from "node:fs";
|
|
2
2
|
import { delimiter, dirname, join } from "node:path";
|
|
3
|
-
import { runtimePath } from "./environment.js";
|
|
3
|
+
import { runtimePath, withPath } from "./environment.js";
|
|
4
4
|
import { runLogged } from "./exec.js";
|
|
5
5
|
import { findOnPath } from "./machine.js";
|
|
6
6
|
/** A word for the shell: quoted when it holds anything but plain characters. */
|
|
7
7
|
const shellWord = (word) => /^[\w@%+=:,./-]+$/.test(word) ? word : `'${word.replaceAll("'", "'\\''")}'`;
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
/** A word for PowerShell: in single quotes, which take everything but a single quote as it is. */
|
|
9
|
+
const powershellWord = (word) => /^[\w@%+=:,./\\-]+$/.test(word) ? word : `'${word.replaceAll("'", "''")}'`;
|
|
10
|
+
/** The command the setup runs for a client, as the person would type it (PowerShell on Windows). */
|
|
11
|
+
export function installCommand(client, paths, platform = process.platform) {
|
|
12
|
+
const windows = platform === "win32";
|
|
13
|
+
if (client.install.kind === "npm") {
|
|
14
|
+
const prefix = windows ? powershellWord(paths.clients) : shellWord(paths.clients);
|
|
15
|
+
return `npm install --global --prefix ${prefix} ${client.install.pkg}`;
|
|
16
|
+
}
|
|
17
|
+
return windows
|
|
18
|
+
? `irm ${powershellWord(client.install.windows)} | iex`
|
|
12
19
|
: `curl -fsSL ${client.install.url} | bash`;
|
|
13
20
|
}
|
|
21
|
+
/** PowerShell, for a command written for it: without the person's profile, scripts allowed. */
|
|
22
|
+
export const POWERSHELL = [
|
|
23
|
+
"powershell.exe",
|
|
24
|
+
"-NoProfile",
|
|
25
|
+
"-ExecutionPolicy",
|
|
26
|
+
"Bypass",
|
|
27
|
+
"-Command",
|
|
28
|
+
];
|
|
14
29
|
/**
|
|
15
30
|
* Installs an AI client. One that comes from npm goes into the install's own prefix
|
|
16
31
|
* (`clients/`), with the npm of the Node that runs this command: nothing of the person's own
|
|
@@ -18,7 +33,11 @@ export function installCommand(client, paths) {
|
|
|
18
33
|
*/
|
|
19
34
|
export function installClient(client, paths, onLine) {
|
|
20
35
|
const logFile = join(paths.logs, "install.log");
|
|
21
|
-
const env =
|
|
36
|
+
const env = withPath(process.env, runtimePath(paths, process.env));
|
|
37
|
+
if (client.install.kind === "script" && process.platform === "win32") {
|
|
38
|
+
const [shell, ...args] = POWERSHELL;
|
|
39
|
+
return runLogged(shell, [...args, installCommand(client, paths)], { env, logFile, onLine });
|
|
40
|
+
}
|
|
22
41
|
if (client.install.kind === "script") {
|
|
23
42
|
return runLogged("bash", ["-c", `set -o pipefail; curl -fsSL ${client.install.url} | bash`], {
|
|
24
43
|
env,
|
|
@@ -3,7 +3,7 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { delimiter, dirname, join } from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
|
-
import { installedByScript, packageRoot } from "./home.js";
|
|
6
|
+
import { commandFile, installedByScript, installedScript, packageRoot, } from "./home.js";
|
|
7
7
|
import { findOnPath, macAppPresent } from "./machine.js";
|
|
8
8
|
import { bad, cmd, dim, ok, tilde } from "./ui.js";
|
|
9
9
|
const run = promisify(execFile);
|
|
@@ -23,8 +23,13 @@ export const SERVER_NAME = "engenty-wizards";
|
|
|
23
23
|
export function serverCommand(paths, script = join(packageRoot, "bin", "wizards.mjs")) {
|
|
24
24
|
const moved = dirname(paths.home) !== join(homedir(), ".engenty");
|
|
25
25
|
const env = moved ? { env: { ENGENTY_HOME: dirname(paths.home) } } : {};
|
|
26
|
+
if (installedByScript(paths) && process.platform === "win32") {
|
|
27
|
+
// Not wizards.cmd: an app starts a .cmd file only through cmd.exe, if at all.
|
|
28
|
+
const { node, script: installed } = installedScript(paths);
|
|
29
|
+
return { command: node, args: [installed, "mcp"], ...env };
|
|
30
|
+
}
|
|
26
31
|
if (installedByScript(paths)) {
|
|
27
|
-
return { command:
|
|
32
|
+
return { command: commandFile(paths), args: ["mcp"], ...env };
|
|
28
33
|
}
|
|
29
34
|
return { command: process.execPath, args: [script, "mcp"], ...env };
|
|
30
35
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { basename, delimiter, dirname, join } from "node:path";
|
|
1
|
+
import { basename, delimiter, dirname, join, win32 } from "node:path";
|
|
2
2
|
import { isCI } from "./autoupdate.js";
|
|
3
3
|
/**
|
|
4
4
|
* Variables of the terminal the runtime inherits. Everything else it gets comes from the
|
|
@@ -59,7 +59,24 @@ export function inherited(env) {
|
|
|
59
59
|
* The PATH of the runtime: AI clients the setup installed and the Node that runs this command
|
|
60
60
|
* come first — a client installed with npm is a script that asks for `node`.
|
|
61
61
|
*/
|
|
62
|
-
export function runtimePath(paths, env, node = process.execPath) {
|
|
62
|
+
export function runtimePath(paths, env, node = process.execPath, platform = process.platform) {
|
|
63
|
+
if (platform === "win32") {
|
|
64
|
+
// npm puts a prefix's commands in the prefix itself on Windows. Claude Code installs into
|
|
65
|
+
// ~\.local\bin, Cursor Agent into %LOCALAPPDATA%\cursor-agent and Antigravity into
|
|
66
|
+
// %LOCALAPPDATA%\agy\bin; each adds that to the user's PATH, which a process started before
|
|
67
|
+
// the install does not see.
|
|
68
|
+
const p = win32;
|
|
69
|
+
const home = env.USERPROFILE ?? "";
|
|
70
|
+
const entries = [
|
|
71
|
+
paths.clients,
|
|
72
|
+
p.dirname(node),
|
|
73
|
+
...(env.PATH ?? env.Path ?? "").split(";"),
|
|
74
|
+
home && p.join(home, ".local", "bin"),
|
|
75
|
+
env.LOCALAPPDATA && p.join(env.LOCALAPPDATA, "cursor-agent"),
|
|
76
|
+
env.LOCALAPPDATA && p.join(env.LOCALAPPDATA, "agy", "bin"),
|
|
77
|
+
];
|
|
78
|
+
return [...new Set(entries.filter(Boolean))].join(";");
|
|
79
|
+
}
|
|
63
80
|
const entries = [
|
|
64
81
|
join(paths.clients, "bin"),
|
|
65
82
|
dirname(node),
|
|
@@ -68,6 +85,20 @@ export function runtimePath(paths, env, node = process.execPath) {
|
|
|
68
85
|
];
|
|
69
86
|
return [...new Set(entries.filter(Boolean))].join(delimiter);
|
|
70
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* An environment with this PATH, once: Windows names it Path, and a copy holding both leaves to
|
|
90
|
+
* chance which one a program gets.
|
|
91
|
+
*/
|
|
92
|
+
export function withPath(env, path) {
|
|
93
|
+
const out = {};
|
|
94
|
+
for (const [key, value] of Object.entries(env)) {
|
|
95
|
+
if (key.toUpperCase() !== "PATH") {
|
|
96
|
+
out[key] = value;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
out.PATH = path;
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
71
102
|
/**
|
|
72
103
|
* The environment the command line starts the runtime with. `fileEnv` is the install's `.env`:
|
|
73
104
|
* the runtime reads it itself, it is only looked at here so that a default does not override it.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import { appendFileSync, mkdirSync } from "node:fs";
|
|
3
2
|
import { dirname } from "node:path";
|
|
3
|
+
// cross-spawn: on Windows a command from npm is a .cmd file, which only cmd.exe starts, with
|
|
4
|
+
// its arguments quoted the way cmd.exe reads them. Elsewhere it is child_process.spawn.
|
|
5
|
+
import spawn from "cross-spawn";
|
|
4
6
|
/**
|
|
5
7
|
* Runs a command; what it prints goes line by line to `onLine` and into the log file.
|
|
6
8
|
* Never rejects for a command that fails: the caller reads `code`.
|
|
@@ -43,3 +45,38 @@ export function runLogged(command, args, options = {}) {
|
|
|
43
45
|
child.on("close", (code) => resolve({ code: code ?? 1, tail: tail.trim() }));
|
|
44
46
|
});
|
|
45
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Runs a file and gives what it printed, like execFile: it rejects when the file is not there,
|
|
50
|
+
* ends with another code than 0, or takes longer than `timeout`.
|
|
51
|
+
*/
|
|
52
|
+
export function runFile(file, args, options = {}) {
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
const child = spawn(file, args, {
|
|
55
|
+
env: options.env,
|
|
56
|
+
cwd: options.cwd,
|
|
57
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
58
|
+
timeout: options.timeout,
|
|
59
|
+
});
|
|
60
|
+
let stdout = "";
|
|
61
|
+
let stderr = "";
|
|
62
|
+
child.stdout?.on("data", (chunk) => {
|
|
63
|
+
stdout += chunk.toString("utf8");
|
|
64
|
+
});
|
|
65
|
+
child.stderr?.on("data", (chunk) => {
|
|
66
|
+
stderr += chunk.toString("utf8");
|
|
67
|
+
});
|
|
68
|
+
child.on("error", reject);
|
|
69
|
+
child.on("close", (code, signal) => {
|
|
70
|
+
if (code === 0) {
|
|
71
|
+
resolve({ stdout, stderr });
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
reject(Object.assign(new Error(`${file} ended with ${signal ?? code}`), {
|
|
75
|
+
code,
|
|
76
|
+
stdout,
|
|
77
|
+
stderr,
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -43,6 +43,21 @@ export function packageVersion() {
|
|
|
43
43
|
}
|
|
44
44
|
return running ?? "0.0.0";
|
|
45
45
|
}
|
|
46
|
+
/** The `wizards` command install.sh (on Windows install.ps1) writes: `bin/wizards`, `bin\\wizards.cmd`. */
|
|
47
|
+
export function commandFile(paths, platform = process.platform) {
|
|
48
|
+
return join(paths.bin, platform === "win32" ? "wizards.cmd" : "wizards");
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The Node and the script of an install made by the installer: what its command runs. They stay
|
|
52
|
+
* where they are across updates (`tools/node` follows the Node of the version).
|
|
53
|
+
*/
|
|
54
|
+
export function installedScript(paths, platform = process.platform) {
|
|
55
|
+
const win = platform === "win32";
|
|
56
|
+
return {
|
|
57
|
+
node: join(paths.tools, "node", win ? "node.exe" : "bin/node"),
|
|
58
|
+
script: join(paths.runtime, "current", "node_modules", "@engenty", "wizards-runtime", "bin", "wizards.mjs"),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
46
61
|
/** How this copy got here: by install.sh into the home folder, or by npm/npx or a checkout. */
|
|
47
62
|
export function installedByScript(paths = layout()) {
|
|
48
63
|
try {
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
1
|
import { existsSync } from "node:fs";
|
|
3
2
|
import { homedir } from "node:os";
|
|
4
3
|
import { join } from "node:path";
|
|
5
|
-
import { promisify } from "node:util";
|
|
6
4
|
// The search for a browser is shared with env.ts, so the runtime uses the one the setup found.
|
|
7
5
|
import { findChrome, findOnPath } from "../render/browser-path.js";
|
|
8
|
-
import { runtimePath } from "./environment.js";
|
|
6
|
+
import { runtimePath, withPath } from "./environment.js";
|
|
7
|
+
import { runFile as run } from "./exec.js";
|
|
9
8
|
export { findChrome, findOnPath };
|
|
10
|
-
const run = promisify(execFile);
|
|
11
9
|
/** The AI clients the runtime can think with, in the order its own setup offers them. */
|
|
12
10
|
export const CLIENTS = [
|
|
13
11
|
{
|
|
@@ -23,7 +21,11 @@ export const CLIENTS = [
|
|
|
23
21
|
name: "Claude Code",
|
|
24
22
|
bin: "claude",
|
|
25
23
|
account: "Claude",
|
|
26
|
-
install: {
|
|
24
|
+
install: {
|
|
25
|
+
kind: "script",
|
|
26
|
+
url: "https://claude.ai/install.sh",
|
|
27
|
+
windows: "https://claude.ai/install.ps1",
|
|
28
|
+
},
|
|
27
29
|
app: "Claude",
|
|
28
30
|
},
|
|
29
31
|
{
|
|
@@ -31,7 +33,11 @@ export const CLIENTS = [
|
|
|
31
33
|
name: "Antigravity",
|
|
32
34
|
bin: "agy",
|
|
33
35
|
account: "Google AI",
|
|
34
|
-
install: {
|
|
36
|
+
install: {
|
|
37
|
+
kind: "script",
|
|
38
|
+
url: "https://antigravity.google/cli/install.sh",
|
|
39
|
+
windows: "https://antigravity.google/cli/install.ps1",
|
|
40
|
+
},
|
|
35
41
|
app: "Antigravity",
|
|
36
42
|
},
|
|
37
43
|
{
|
|
@@ -39,7 +45,11 @@ export const CLIENTS = [
|
|
|
39
45
|
name: "Cursor Agent",
|
|
40
46
|
bin: "cursor-agent",
|
|
41
47
|
account: "Cursor",
|
|
42
|
-
install: {
|
|
48
|
+
install: {
|
|
49
|
+
kind: "script",
|
|
50
|
+
url: "https://cursor.com/install",
|
|
51
|
+
windows: "https://cursor.com/install?win32=true",
|
|
52
|
+
},
|
|
43
53
|
app: "Cursor",
|
|
44
54
|
},
|
|
45
55
|
];
|
|
@@ -68,7 +78,7 @@ async function versionOf(file, path) {
|
|
|
68
78
|
try {
|
|
69
79
|
const { stdout } = await run(file, ["--version"], {
|
|
70
80
|
timeout: 10_000,
|
|
71
|
-
env: { ...process.env,
|
|
81
|
+
env: { ...withPath(process.env, path), TERM: "dumb" },
|
|
72
82
|
});
|
|
73
83
|
const line = stdout.trim().split("\n").pop()?.trim() ?? "";
|
|
74
84
|
return line.match(/\d+\.\d+[\w.+-]*/)?.[0] ?? (line || null);
|
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { sep } from "node:path";
|
|
3
|
-
/** Every process, as `ps` lists it
|
|
3
|
+
/** Every process, as `ps` lists it, on Windows as Windows' own process table does. */
|
|
4
4
|
function listProcesses() {
|
|
5
|
+
if (process.platform === "win32") {
|
|
6
|
+
try {
|
|
7
|
+
const json = execFileSync("powershell.exe", [
|
|
8
|
+
"-NoProfile",
|
|
9
|
+
"-Command",
|
|
10
|
+
"Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,CommandLine | ConvertTo-Json -Compress",
|
|
11
|
+
], { encoding: "utf8", windowsHide: true, maxBuffer: 32 * 1024 * 1024 });
|
|
12
|
+
const rows = JSON.parse(json);
|
|
13
|
+
return rows.map((row) => ({
|
|
14
|
+
pid: row.ProcessId,
|
|
15
|
+
ppid: row.ParentProcessId,
|
|
16
|
+
command: row.CommandLine ?? "",
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
5
23
|
try {
|
|
6
24
|
return execFileSync("ps", ["-axww", "-o", "pid=,ppid=,command="], { encoding: "utf8" })
|
|
7
25
|
.split("\n")
|
|
@@ -17,14 +35,16 @@ function listProcesses() {
|
|
|
17
35
|
* The processes in `list` that run from `home`, apart from `self` and the processes that started
|
|
18
36
|
* it (npx, a shell).
|
|
19
37
|
*/
|
|
20
|
-
export function wizardsProcesses(home, list, self = process.pid) {
|
|
38
|
+
export function wizardsProcesses(home, list, self = process.pid, platform = process.platform) {
|
|
39
|
+
// Windows paths are the same in any case.
|
|
40
|
+
const fold = (text) => (platform === "win32" ? text.toLowerCase() : text);
|
|
21
41
|
const parents = new Map(list.map((entry) => [entry.pid, entry.ppid]));
|
|
22
42
|
const skip = new Set();
|
|
23
43
|
for (let at = self; at && !skip.has(at); at = parents.get(at)) {
|
|
24
44
|
skip.add(at);
|
|
25
45
|
}
|
|
26
46
|
return list
|
|
27
|
-
.filter((entry) => !skip.has(entry.pid) && entry.command.includes(home + sep))
|
|
47
|
+
.filter((entry) => !skip.has(entry.pid) && fold(entry.command).includes(fold(home + sep)))
|
|
28
48
|
.map(({ pid, command }) => {
|
|
29
49
|
const args = command.split(/\s+/);
|
|
30
50
|
const kind = /apps[\\/]runtime[\\/]dist[\\/]index\.js/.test(command)
|
|
@@ -8,7 +8,7 @@ import { autostartForStatus, setAutostart } from "./autostart.js";
|
|
|
8
8
|
import { installClient, installCommand } from "./clients.js";
|
|
9
9
|
import { appStates, connectApps } from "./connect.js";
|
|
10
10
|
import { runLogged } from "./exec.js";
|
|
11
|
-
import { command, installedByScript, isCheckout, packageVersion, readInstallNote, writeInstallNote, } from "./home.js";
|
|
11
|
+
import { command, installedByScript, isCheckout, layout, packageVersion, readInstallNote, writeInstallNote, } from "./home.js";
|
|
12
12
|
import { CLIENTS, detectClients, findChrome, findFfmpeg, findOnPath, suggestedClient, vendorApp, } from "./machine.js";
|
|
13
13
|
import { appAsksForPassword, DECISION_MODEL_GB, decisionModelProblem, freeGb, installOllama, ollamaState, pullModel, servesDecisions, startOllama, } from "./ollama.js";
|
|
14
14
|
import { runningRuntime, settings } from "./start.js";
|
|
@@ -320,6 +320,16 @@ function pathLine() {
|
|
|
320
320
|
}
|
|
321
321
|
/** An install made by install.sh has its command in `~/.local/bin`: is that on the PATH? */
|
|
322
322
|
async function pathStep(interactive) {
|
|
323
|
+
if (process.platform === "win32") {
|
|
324
|
+
// install.ps1 put the command's folder on the user's PATH; a terminal opened before it
|
|
325
|
+
// does not see that.
|
|
326
|
+
const bin = layout().bin.toLowerCase();
|
|
327
|
+
const path = (process.env.PATH ?? "").toLowerCase().split(";");
|
|
328
|
+
if (installedByScript() && !path.includes(bin)) {
|
|
329
|
+
p.log.info(`Open a new terminal to use the ${cyan("wizards")} command.`);
|
|
330
|
+
}
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
323
333
|
const local = join(homedir(), ".local", "bin");
|
|
324
334
|
if (!installedByScript() || (process.env.PATH ?? "").split(delimiter).includes(local)) {
|
|
325
335
|
return;
|
|
@@ -8,11 +8,12 @@ import { readRunning } from "../running.js";
|
|
|
8
8
|
import { RESTART_EXIT } from "../update.js";
|
|
9
9
|
import { applyPendingUpdate, autoUpdatePolicy, notesUrl } from "./autoupdate.js";
|
|
10
10
|
import { runtimeEnv } from "./environment.js";
|
|
11
|
-
import { installedByScript, packageRoot, readEnvFile } from "./home.js";
|
|
11
|
+
import { commandFile, installedByScript, installedScript, packageRoot, readEnvFile, } from "./home.js";
|
|
12
12
|
import { findOnPath } from "./machine.js";
|
|
13
13
|
import { cmd, cyan, dim, live, tilde } from "./ui.js";
|
|
14
14
|
const DEFAULT_PORT = 24368;
|
|
15
|
-
|
|
15
|
+
// Windows on Arm runs the runtime's x64 Node emulated, and a first start there takes longer.
|
|
16
|
+
const START_TIMEOUT_MS = process.platform === "win32" ? 180_000 : 60_000;
|
|
16
17
|
/** The settings this install starts with: its `.env`, overridden by the terminal's variables. */
|
|
17
18
|
export function settings(paths, env = process.env) {
|
|
18
19
|
const fileEnv = readEnvFile(paths.envFile);
|
|
@@ -83,6 +84,21 @@ export function entryUrl(running, paths, env = process.env) {
|
|
|
83
84
|
}
|
|
84
85
|
/** Opens an address in the person's browser; false when this machine has none to open it with. */
|
|
85
86
|
export function openBrowser(url) {
|
|
87
|
+
if (process.platform === "win32") {
|
|
88
|
+
// The default browser, through the shell's own handler: no cmd.exe, which would read the
|
|
89
|
+
// link's & as the start of another command.
|
|
90
|
+
const rundll32 = join(process.env.SystemRoot || "C:\\Windows", "System32", "rundll32.exe");
|
|
91
|
+
try {
|
|
92
|
+
spawn(rundll32, ["url.dll,FileProtocolHandler", url], {
|
|
93
|
+
stdio: "ignore",
|
|
94
|
+
detached: true,
|
|
95
|
+
}).unref();
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
86
102
|
const opener = process.platform === "darwin"
|
|
87
103
|
? "open"
|
|
88
104
|
: process.env.DISPLAY || process.env.WAYLAND_DISPLAY
|
|
@@ -111,16 +127,26 @@ function show(url, entry, open) {
|
|
|
111
127
|
console.log(dim(" The link signs in one browser and works only once."));
|
|
112
128
|
}
|
|
113
129
|
}
|
|
114
|
-
/**
|
|
130
|
+
/**
|
|
131
|
+
* The command of this install, as it is on disk now: `wizards <args>`. On Windows its Node and
|
|
132
|
+
* script, which is what wizards.cmd runs, without a console window of its own.
|
|
133
|
+
*/
|
|
115
134
|
function spawnCommand(paths, args, options) {
|
|
116
|
-
const wrapper =
|
|
135
|
+
const wrapper = commandFile(paths);
|
|
136
|
+
if (existsSync(wrapper) && process.platform === "win32") {
|
|
137
|
+
const { node, script } = installedScript(paths);
|
|
138
|
+
return spawn(node, [script, ...args], { ...options, windowsHide: true });
|
|
139
|
+
}
|
|
117
140
|
return existsSync(wrapper)
|
|
118
141
|
? spawn(wrapper, args, options)
|
|
119
|
-
: spawn(process.execPath, [join(packageRoot, "bin/wizards.mjs"), ...args],
|
|
142
|
+
: spawn(process.execPath, [join(packageRoot, "bin/wizards.mjs"), ...args], {
|
|
143
|
+
...options,
|
|
144
|
+
windowsHide: true,
|
|
145
|
+
});
|
|
120
146
|
}
|
|
121
147
|
/**
|
|
122
148
|
* `wizards start --no-open` detached from this terminal, its output in logs/runtime.log; the
|
|
123
|
-
* runtime once it answers, or null when it ended or did not answer
|
|
149
|
+
* runtime once it answers, or null when it ended or did not answer in time.
|
|
124
150
|
*/
|
|
125
151
|
export async function startInBackground(paths) {
|
|
126
152
|
const { dataDir } = settings(paths);
|
|
@@ -226,7 +252,7 @@ export async function start(paths, options) {
|
|
|
226
252
|
while (exited === null && !(await healthy(port))) {
|
|
227
253
|
if (Date.now() - began > START_TIMEOUT_MS) {
|
|
228
254
|
child.kill("SIGTERM");
|
|
229
|
-
console.error(
|
|
255
|
+
console.error(`engenty wizards didn’t respond within ${START_TIMEOUT_MS / 1000} seconds.`);
|
|
230
256
|
return 1;
|
|
231
257
|
}
|
|
232
258
|
await new Promise((wait) => setTimeout(wait, 150));
|