@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.
- package/LICENSE-FIGURES +37 -0
- package/README.md +5 -2
- package/TRADEMARKS.md +47 -0
- 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/services/engenty-shapes.js +3 -2
- package/apps/runtime/dist/update.js +4 -0
- package/apps/web/dist/assets/{AboutDialog-CShc0mRo.js → AboutDialog-CPRAlD2H.js} +1 -1
- package/apps/web/dist/assets/{ChangelogDialog-C4dVSwtN.js → ChangelogDialog-CZrd-MgC.js} +1 -1
- package/apps/web/dist/assets/{ChatView-KkOboyOo.js → ChatView-DVeERQdi.js} +1 -1
- package/apps/web/dist/assets/{EditorPage-lQ3YgsFT.js → EditorPage-DhUoKcss.js} +1 -1
- package/apps/web/dist/assets/{Harness-D6mgLWTb.js → Harness-DiB8CBdY.js} +1 -1
- package/apps/web/dist/assets/{NewWizardChannels-CPlh_37J.js → NewWizardChannels-Ba3XGpro.js} +1 -1
- package/apps/web/dist/assets/{NewWizardPage-D0FVJtXK.js → NewWizardPage-C2Oh2sMM.js} +1 -1
- package/apps/web/dist/assets/{OnboardingPage-BXPOMrQR.js → OnboardingPage-D_4ShWrL.js} +1 -1
- package/apps/web/dist/assets/{ProjectPage-DroTprqs.js → ProjectPage-C58rq50b.js} +1 -1
- package/apps/web/dist/assets/{PublicRunner-j3qoKjQQ.js → PublicRunner-CE0ohzyd.js} +1 -1
- package/apps/web/dist/assets/{ReadOnly-CKgSQ2z1.js → ReadOnly-CD5YWvrW.js} +1 -1
- package/apps/web/dist/assets/{RunDrawer-CJZ_ISAd.js → RunDrawer-Cd8-qHV7.js} +1 -1
- package/apps/web/dist/assets/{SettingsPage-BHZ0Z-og.js → SettingsPage-ChYLv-MG.js} +1 -1
- package/apps/web/dist/assets/{SetupPage-CDkYkjJX.js → SetupPage-hX7deF0g.js} +1 -1
- package/apps/web/dist/assets/{SharePage-BlVjVjHG.js → SharePage-B6u9kS12.js} +1 -1
- package/apps/web/dist/assets/changelog-Dn9LT4MS.js +1 -0
- package/apps/web/dist/assets/engenty-Cko4qGFV.js +1 -0
- package/apps/web/dist/assets/fluffy-engenty-Db7agOjV.js +520 -0
- package/apps/web/dist/assets/index-4jH2mkp7.js +14 -0
- package/apps/web/dist/assets/index-BwW652E-.css +1 -0
- package/apps/web/dist/assets/{oss-credits-qGhcbqiM.js → oss-credits-Bc05Qv5H.js} +1 -1
- package/apps/web/dist/favicon.svg +2 -1
- package/apps/web/dist/icons/apple-touch-icon.png +0 -0
- package/apps/web/dist/icons/icon-192.png +0 -0
- package/apps/web/dist/icons/icon-512.png +0 -0
- package/apps/web/dist/icons/icon-maskable-512.png +0 -0
- package/apps/web/dist/index.html +3 -3
- package/apps/web/dist/install.ps1 +307 -0
- package/apps/web/dist/mcp/flow.html +33 -88
- package/bin/wizards.mjs +59 -35
- package/package.json +6 -7
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/hooks/engenty.ts +3 -2
- package/apps/web/dist/assets/changelog-Be0i5Jjw.js +0 -1
- package/apps/web/dist/assets/engenty-WpMi3xwq.js +0 -1
- package/apps/web/dist/assets/fluffy-engenty-DPNHytrz.js +0 -575
- package/apps/web/dist/assets/index-C4aHMcg9.css +0 -1
- package/apps/web/dist/assets/index-C61HAltK.js +0 -14
package/LICENSE-FIGURES
ADDED
|
@@ -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
|
|
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.
|
|
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.
|
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;
|