@engenty/wizards-runtime 0.7.9 → 0.7.11

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.
Files changed (63) hide show
  1. package/LICENSE-FIGURES +37 -0
  2. package/README.md +5 -2
  3. package/TRADEMARKS.md +47 -0
  4. package/apps/runtime/dist/app.js +2 -2
  5. package/apps/runtime/dist/cli/autostart.js +90 -7
  6. package/apps/runtime/dist/cli/autoupdate.js +18 -5
  7. package/apps/runtime/dist/cli/clients.js +25 -6
  8. package/apps/runtime/dist/cli/connect.js +7 -2
  9. package/apps/runtime/dist/cli/environment.js +33 -2
  10. package/apps/runtime/dist/cli/exec.js +38 -1
  11. package/apps/runtime/dist/cli/home.js +15 -0
  12. package/apps/runtime/dist/cli/machine.js +18 -8
  13. package/apps/runtime/dist/cli/processes.js +23 -3
  14. package/apps/runtime/dist/cli/setup.js +11 -1
  15. package/apps/runtime/dist/cli/start.js +33 -7
  16. package/apps/runtime/dist/cli/uninstall.js +100 -6
  17. package/apps/runtime/dist/cli/update.js +28 -12
  18. package/apps/runtime/dist/cli/versions.js +16 -4
  19. package/apps/runtime/dist/docs/user/install.md +18 -8
  20. package/apps/runtime/dist/harness/env.js +41 -8
  21. package/apps/runtime/dist/harness/index.js +10 -1
  22. package/apps/runtime/dist/harness/model.js +1 -1
  23. package/apps/runtime/dist/render/browser-path.js +43 -11
  24. package/apps/runtime/dist/services/engenty-shapes.js +3 -2
  25. package/apps/runtime/dist/update.js +4 -0
  26. package/apps/web/dist/assets/{AboutDialog-CShc0mRo.js → AboutDialog-CPRAlD2H.js} +1 -1
  27. package/apps/web/dist/assets/{ChangelogDialog-C4dVSwtN.js → ChangelogDialog-CZrd-MgC.js} +1 -1
  28. package/apps/web/dist/assets/{ChatView-KkOboyOo.js → ChatView-DVeERQdi.js} +1 -1
  29. package/apps/web/dist/assets/{EditorPage-lQ3YgsFT.js → EditorPage-DhUoKcss.js} +1 -1
  30. package/apps/web/dist/assets/{Harness-D6mgLWTb.js → Harness-DiB8CBdY.js} +1 -1
  31. package/apps/web/dist/assets/{NewWizardChannels-CPlh_37J.js → NewWizardChannels-Ba3XGpro.js} +1 -1
  32. package/apps/web/dist/assets/{NewWizardPage-D0FVJtXK.js → NewWizardPage-C2Oh2sMM.js} +1 -1
  33. package/apps/web/dist/assets/{OnboardingPage-BXPOMrQR.js → OnboardingPage-D_4ShWrL.js} +1 -1
  34. package/apps/web/dist/assets/{ProjectPage-DroTprqs.js → ProjectPage-C58rq50b.js} +1 -1
  35. package/apps/web/dist/assets/{PublicRunner-j3qoKjQQ.js → PublicRunner-CE0ohzyd.js} +1 -1
  36. package/apps/web/dist/assets/{ReadOnly-CKgSQ2z1.js → ReadOnly-CD5YWvrW.js} +1 -1
  37. package/apps/web/dist/assets/{RunDrawer-CJZ_ISAd.js → RunDrawer-Cd8-qHV7.js} +1 -1
  38. package/apps/web/dist/assets/{SettingsPage-BHZ0Z-og.js → SettingsPage-ChYLv-MG.js} +1 -1
  39. package/apps/web/dist/assets/{SetupPage-CDkYkjJX.js → SetupPage-hX7deF0g.js} +1 -1
  40. package/apps/web/dist/assets/{SharePage-BlVjVjHG.js → SharePage-B6u9kS12.js} +1 -1
  41. package/apps/web/dist/assets/changelog-Dn9LT4MS.js +1 -0
  42. package/apps/web/dist/assets/engenty-Cko4qGFV.js +1 -0
  43. package/apps/web/dist/assets/fluffy-engenty-Db7agOjV.js +520 -0
  44. package/apps/web/dist/assets/index-4jH2mkp7.js +14 -0
  45. package/apps/web/dist/assets/index-BwW652E-.css +1 -0
  46. package/apps/web/dist/assets/{oss-credits-qGhcbqiM.js → oss-credits-Bc05Qv5H.js} +1 -1
  47. package/apps/web/dist/favicon.svg +2 -1
  48. package/apps/web/dist/icons/apple-touch-icon.png +0 -0
  49. package/apps/web/dist/icons/icon-192.png +0 -0
  50. package/apps/web/dist/icons/icon-512.png +0 -0
  51. package/apps/web/dist/icons/icon-maskable-512.png +0 -0
  52. package/apps/web/dist/index.html +3 -3
  53. package/apps/web/dist/install.ps1 +307 -0
  54. package/apps/web/dist/mcp/flow.html +33 -88
  55. package/bin/wizards.mjs +59 -35
  56. package/package.json +6 -7
  57. package/plugin/.claude-plugin/plugin.json +1 -1
  58. package/plugin/hooks/engenty.ts +3 -2
  59. package/apps/web/dist/assets/changelog-Be0i5Jjw.js +0 -1
  60. package/apps/web/dist/assets/engenty-WpMi3xwq.js +0 -1
  61. package/apps/web/dist/assets/fluffy-engenty-DPNHytrz.js +0 -575
  62. package/apps/web/dist/assets/index-C4aHMcg9.css +0 -1
  63. package/apps/web/dist/assets/index-C61HAltK.js +0 -14
@@ -0,0 +1,37 @@
1
+ # engenty figures
2
+
3
+ SPDX-License-Identifier: CC-BY-4.0
4
+
5
+ Copyright 2026 - Thru The Heart Creative GmbH - Matthias Platzer
6
+
7
+ The engenty figures — the one-eyed characters of engenty, in every kind and rendering, with
8
+ their shapes, colours, eyes and accessories — are licensed under the Creative Commons
9
+ Attribution 4.0 International licence (CC BY 4.0):
10
+ https://creativecommons.org/licenses/by/4.0/legalcode
11
+
12
+ You may use, copy, change and share them, commercially too, free of charge and without
13
+ royalties, as long as you give credit, for example:
14
+
15
+ engenty figures © Thru The Heart Creative GmbH - Matthias Platzer, CC BY 4.0, engenty.ai
16
+
17
+ These files describe what the figures look like and are under CC BY 4.0:
18
+
19
+ - apps/web/src/engenty/engenty.tsx, colors.ts, forms.ts, helmet.tsx, logo.tsx,
20
+ goose-helmet-fit.ts, assets/goose-helmet.bin, assets/goose-helmet-livery.webp
21
+ - apps/runtime/src/services/engenty-shapes.ts, plugin/hooks/engenty.ts
22
+ - assets/engenty/: every flat figure as a 512 px PNG, plain and, for round, drop, dome, flame,
23
+ oval, sprout and tower, with the helmet
24
+ - apps/web/public/favicon.svg, apps/web/public/icons/, apps/docs/public/favicon.svg
25
+ - docs/assets/engenty.svg
26
+
27
+ The figures are under CC BY 4.0 wherever else they appear in this repository too: in the
28
+ screenshots docs/assets/setup.png, studio.png and wizard.png, and as the pixel engenty that
29
+ apps/web/public/install.sh draws. The rest of those files is under the Functional Source License.
30
+
31
+ The code that renders, animates and deforms the figures (the fur and jelly renderers and
32
+ their shaders, the helmet's geometry and shaders, gaze and motion) stays under the Functional
33
+ Source License (LICENSE).
34
+
35
+ CC BY 4.0 licenses copyright and similar rights, not trademark or patent rights (section
36
+ 2(b)(2)). Using the figures or the name engenty as your own brand, logo or mascot, or in a way
37
+ that suggests engenty endorses you, is governed by TRADEMARKS.md.
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 the machine, `npx wizards` does the same.
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
@@ -211,5 +212,7 @@ and is a site of its own: `pnpm dev:docs`.
211
212
 
212
213
  [FSL-1.1-MIT](LICENSE): use it for yourself and your company, run it, change it – just don’t
213
214
  offer it to others as a competing product or service. Each release becomes MIT two years
214
- after it is published. [COPYRIGHTS.md](COPYRIGHTS.md) names the owner, the marks, what is not ours and the
215
+ after it is published. The engenty figures are free artwork under CC BY 4.0
216
+ ([LICENSE-FIGURES](LICENSE-FIGURES)); the name and the figures as a brand are trademarks
217
+ ([TRADEMARKS.md](TRADEMARKS.md)). [COPYRIGHTS.md](COPYRIGHTS.md) names the owner, the marks, what is not ours and the
215
218
  third-party software with its licences.
package/TRADEMARKS.md ADDED
@@ -0,0 +1,47 @@
1
+ # Trademarks
2
+
3
+ "engenty", "engenty.wizards" (also written "engenty wizards"), the engenty wordmark and the
4
+ engenty figures are trademarks of Thru The Heart Creative GmbH. The word "wizards" on its own is
5
+ not one of them: it names the package, the repository and the command.
6
+
7
+ engenty comes under several licences. In this repository: most of the code under the Functional
8
+ Source License ([LICENSE](LICENSE)); the files of `packages/shared` that define the wizard
9
+ format also under MIT, at your choice (`FSL-1.1-MIT OR MIT`,
10
+ [packages/shared/LICENSE-MIT](packages/shared/LICENSE-MIT)); the runner package under MIT
11
+ ([packages/runner/LICENSE](packages/runner/LICENSE)); the figures as artwork under CC BY 4.0
12
+ ([LICENSE-FIGURES](LICENSE-FIGURES)). Outside it: the WordPress plugins under GPL-2.0-or-later,
13
+ and the closed parts (engenty.ai, the mobile and desktop apps, the Pro plugins, also when a
14
+ local install receives them) under no public licence. No licence of any part grants trademark
15
+ rights. These rules say how the name and the figures may be used as signs, whichever licence the
16
+ part you use is under.
17
+
18
+ ## Fine without asking
19
+
20
+ - Talking about engenty, truthfully: posts, reports, reviews, tutorials, courses, videos,
21
+ streams, podcasts, articles and talks, and saying that your wizard, plugin, connector or app
22
+ works with, runs on or is about engenty.
23
+ - Showing the figures in that content, thumbnails and preview images included, and in
24
+ documentation, screenshots and the wizards and apps built with engenty, with the credit
25
+ CC BY 4.0 asks for (for a video or a post, its description is enough).
26
+ - Changing the figures, drawing new poses, making fan art.
27
+ - Running and sharing engenty.wizards and your own changed copy of it, as the licences allow,
28
+ with its engenties inside.
29
+
30
+ ## Needs our written permission
31
+
32
+ - Using an engenty, a figure made from one or one that looks like one, as your own logo, mascot,
33
+ app icon, avatar or profile picture, or as the recurring character of your channel or brand.
34
+ - Using "engenty" or a figure in the name of your product, service, company, domain or social
35
+ media account.
36
+ - Advertising your own product or service (other than content about engenty) with an engenty as
37
+ its face, or anything else that suggests engenty made, endorses, sponsors or is part of what
38
+ you offer.
39
+ - Merchandise: selling or giving away goods (stickers, shirts, toys, prints) with the figures.
40
+ - Registering "engenty", the figures or anything like them as a trademark, design or domain.
41
+
42
+ A changed copy of engenty.wizards that you offer to others must carry its own name and logo; it
43
+ may keep the engenties inside the software, but not on its name, logo or advertising.
44
+
45
+ ## Ask
46
+
47
+ contact@engenty.ai — say what you plan, we usually answer quickly and often say yes.
@@ -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 installer, the embed script for websites, and the service worker of public wizards. 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. Only for an install made by install.sh,
13
- * whose command stays where it is.
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 (process.platform !== "darwin" && process.platform !== "linux") {
106
- return {
107
- unavailable: process.platform === "win32" ? "not on Windows" : `not on ${process.platform}`,
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
- const bin = join(paths.tools, "node", "bin");
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 = "install --omit=dev --ignore-scripts --legacy-peer-deps --no-audit --no-fund --loglevel=error";
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(join(bin, "npm"), [...args.split(" "), "--prefix", prefix, spec], {
112
+ const child = spawn(command, all, {
101
113
  stdio: ["ignore", log, log],
102
- env: { ...process.env, PATH: `${bin}:${process.env.PATH ?? ""}` },
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
- /** The command the setup runs for a client, as the person would type it. */
9
- export function installCommand(client, paths) {
10
- return client.install.kind === "npm"
11
- ? `npm install --global --prefix ${shellWord(paths.clients)} ${client.install.pkg}`
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 = { ...process.env, PATH: runtimePath(paths, process.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: join(paths.bin, "wizards"), args: ["mcp"], ...env };
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: { kind: "script", url: "https://claude.ai/install.sh" },
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: { kind: "script", url: "https://antigravity.google/cli/install.sh" },
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: { kind: "script", url: "https://cursor.com/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, PATH: path, TERM: "dumb" },
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; empty where there is no `ps` (Windows). */
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;