wizards 0.6.1 → 0.7.0
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 +6 -2
- package/apps/runtime/dist/agents/architect.js +7 -29
- package/apps/runtime/dist/agents/studio-assistant.js +7 -24
- package/apps/runtime/dist/authoring/guide.js +1 -1
- package/apps/runtime/dist/cli/autostart.js +58 -12
- package/apps/runtime/dist/cli/autoupdate.js +221 -0
- package/apps/runtime/dist/cli/environment.js +5 -0
- package/apps/runtime/dist/cli/main.js +9 -2
- package/apps/runtime/dist/cli/menu.js +2 -2
- package/apps/runtime/dist/cli/processes.js +72 -0
- package/apps/runtime/dist/cli/setup.js +9 -4
- package/apps/runtime/dist/cli/start.js +18 -1
- package/apps/runtime/dist/cli/status.js +51 -17
- package/apps/runtime/dist/cli/uninstall-main.js +20 -0
- package/apps/runtime/dist/cli/uninstall.js +231 -0
- package/apps/runtime/dist/cli/update.js +25 -2
- package/apps/runtime/dist/cli/versions.js +106 -0
- package/apps/runtime/dist/docs/dev/wizard-format.md +6 -4
- package/apps/runtime/dist/docs/user/command-line.md +2 -1
- package/apps/runtime/dist/docs/user/data.md +1 -1
- package/apps/runtime/dist/docs/user/install.md +29 -6
- package/apps/runtime/dist/engine/input.js +4 -5
- package/apps/runtime/dist/engine/runner.js +5 -5
- package/apps/runtime/dist/engine/steps.js +5 -5
- package/apps/runtime/dist/engine/template.js +10 -15
- package/apps/runtime/dist/env.js +3 -20
- package/apps/runtime/dist/features.js +2 -2
- package/apps/runtime/dist/index.js +3 -1
- package/apps/runtime/dist/manage.js +7 -0
- package/apps/runtime/dist/media/conversation.js +2 -2
- package/apps/runtime/dist/models.js +12 -0
- package/apps/runtime/dist/plugins/runs.js +4 -4
- package/apps/runtime/dist/routes/runs.js +4 -2
- package/apps/runtime/dist/sandbox/docker.js +0 -2
- package/apps/runtime/dist/sandbox/index.js +3 -10
- package/apps/runtime/dist/services/api.js +2 -2
- package/apps/runtime/dist/services/runners.js +2 -2
- package/apps/runtime/dist/services/space-categories.js +3 -25
- package/apps/runtime/dist/services/translations.js +7 -4
- package/apps/runtime/dist/tools/browser.js +2 -14
- package/apps/runtime/dist/tools/index.js +1 -19
- package/apps/runtime/dist/tools/mail.js +1 -4
- package/apps/runtime/dist/tools/shared.js +30 -3
- package/apps/runtime/dist/update.js +10 -19
- package/apps/web/dist/assets/AboutDialog-CUVmdhzk.js +1 -0
- package/apps/web/dist/assets/{ChangelogDialog-jQk_bQpP.js → ChangelogDialog-DcQqFbiW.js} +1 -1
- package/apps/web/dist/assets/ChatView-DTPWjzWF.js +10 -0
- package/apps/web/dist/assets/EditorPage-SJHtft7P.js +3 -0
- package/apps/web/dist/assets/{FlowPreview-DA0sHtr7.js → FlowPreview-DvpjuQdC.js} +1 -1
- package/apps/web/dist/assets/{Harness-DmYkwU4x.js → Harness-D6fezoWO.js} +2 -2
- package/apps/web/dist/assets/{NewWizardChannels-BLS9sKhT.js → NewWizardChannels-BHeutZ7V.js} +1 -1
- package/apps/web/dist/assets/NewWizardPage-CzBxaPhH.js +7 -0
- package/apps/web/dist/assets/{OnboardingPage-CREvCCdX.js → OnboardingPage-DbR4N2Ms.js} +1 -1
- package/apps/web/dist/assets/ProjectPage-DG6_E7kA.js +1 -0
- package/apps/web/dist/assets/PublicRunner-19tV_8TR.js +1 -0
- package/apps/web/dist/assets/{ReadOnly-b7TjjRAk.js → ReadOnly-BMXFtKs0.js} +1 -1
- package/apps/web/dist/assets/RunDrawer-DqwLjFJb.js +4 -0
- package/apps/web/dist/assets/{RunnerView-CLfZzbKn.js → RunnerView-DRc4J-rg.js} +1 -1
- package/apps/web/dist/assets/SettingsPage-CCypPS9L.js +1 -0
- package/apps/web/dist/assets/SetupPage-D2Y2rev4.js +1 -0
- package/apps/web/dist/assets/{SharePage-B21QI97K.js → SharePage-DB5n6Oug.js} +1 -1
- package/apps/web/dist/assets/changelog-BDhd7tjO.js +1 -0
- package/apps/web/dist/assets/{esm-DqRvG-PU.js → esm-6TNBQNCD.js} +1 -1
- package/apps/web/dist/assets/index-mBRI7ZQp.js +14 -0
- package/apps/web/dist/assets/{lock-D_KjsGfU.js → lock-HLZib0u9.js} +1 -1
- package/apps/web/dist/assets/{phone-YRxkPQk6.js → phone-DsxbIcWi.js} +1 -1
- package/apps/web/dist/assets/settings-menu-BMMkIVP1.js +1 -0
- package/apps/web/dist/assets/ui-Brg2qM1A.js +137 -0
- package/apps/web/dist/assets/{upload-DDo14e2p.js → upload-BaGy0ppG.js} +1 -1
- package/apps/web/dist/index.html +2 -2
- package/apps/web/dist/install.sh +29 -11
- package/apps/web/dist/mcp/flow.html +11 -11
- package/bin/wizards.mjs +17 -3
- package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +18 -7
- package/node_modules/@engenty-wizards/shared/dist/definition.js +50 -21
- package/node_modules/@engenty-wizards/shared/dist/features.d.ts +6 -0
- package/node_modules/@engenty-wizards/shared/dist/features.js +12 -0
- package/node_modules/@engenty-wizards/shared/dist/numbers.d.ts +24 -0
- package/node_modules/@engenty-wizards/shared/dist/numbers.js +54 -0
- package/node_modules/@engenty-wizards/shared/dist/run.d.ts +4 -4
- package/node_modules/@engenty-wizards/shared/dist/store.d.ts +7 -0
- package/node_modules/@engenty-wizards/shared/dist/store.js +28 -0
- package/package.json +1 -1
- package/apps/runtime/dist/sandbox/agentos.js +0 -191
- package/apps/web/dist/assets/AboutDialog-CTuI-l3p.js +0 -1
- package/apps/web/dist/assets/ChatView-9TLi0B7K.js +0 -10
- package/apps/web/dist/assets/EditorPage-BVrzOfF3.js +0 -3
- package/apps/web/dist/assets/NewWizardPage-DWYZYiBF.js +0 -7
- package/apps/web/dist/assets/ProjectPage-BQkd55xR.js +0 -1
- package/apps/web/dist/assets/PublicRunner-sOlOC0zD.js +0 -1
- package/apps/web/dist/assets/RunDrawer-BPIVaan7.js +0 -4
- package/apps/web/dist/assets/SettingsPage-BO6TPmoj.js +0 -1
- package/apps/web/dist/assets/SetupPage-DlJHBYWH.js +0 -1
- package/apps/web/dist/assets/changelog-DeSoCsai.js +0 -1
- package/apps/web/dist/assets/index-CAUeJXCg.js +0 -14
- package/apps/web/dist/assets/settings-menu-CQgehwEX.js +0 -1
- package/apps/web/dist/assets/ui-DDUCc_4v.js +0 -137
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { sep } from "node:path";
|
|
3
|
+
/** Every process, as `ps` lists it; empty where there is no `ps` (Windows). */
|
|
4
|
+
function listProcesses() {
|
|
5
|
+
try {
|
|
6
|
+
return execFileSync("ps", ["-axww", "-o", "pid=,ppid=,command="], { encoding: "utf8" })
|
|
7
|
+
.split("\n")
|
|
8
|
+
.map((line) => line.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/))
|
|
9
|
+
.filter((match) => match !== null)
|
|
10
|
+
.map((match) => ({ pid: Number(match[1]), ppid: Number(match[2]), command: match[3] }));
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The processes in `list` that run from `home`, apart from `self` and the processes that started
|
|
18
|
+
* it (npx, a shell).
|
|
19
|
+
*/
|
|
20
|
+
export function wizardsProcesses(home, list, self = process.pid) {
|
|
21
|
+
const parents = new Map(list.map((entry) => [entry.pid, entry.ppid]));
|
|
22
|
+
const skip = new Set();
|
|
23
|
+
for (let at = self; at && !skip.has(at); at = parents.get(at)) {
|
|
24
|
+
skip.add(at);
|
|
25
|
+
}
|
|
26
|
+
return list
|
|
27
|
+
.filter((entry) => !skip.has(entry.pid) && entry.command.includes(home + sep))
|
|
28
|
+
.map(({ pid, command }) => {
|
|
29
|
+
const args = command.split(/\s+/);
|
|
30
|
+
const kind = /apps[\\/]runtime[\\/]dist[\\/]index\.js/.test(command)
|
|
31
|
+
? "runtime"
|
|
32
|
+
: args.includes("mcp")
|
|
33
|
+
? "mcp"
|
|
34
|
+
: args.includes("start")
|
|
35
|
+
? "start"
|
|
36
|
+
: "other";
|
|
37
|
+
return { pid, kind };
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export const findWizardsProcesses = (home) => wizardsProcesses(home, listProcesses());
|
|
41
|
+
const alive = (pid) => {
|
|
42
|
+
try {
|
|
43
|
+
process.kill(pid, 0);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
return error.code === "EPERM";
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
/** SIGTERM, then SIGKILL after 8 seconds; the processes still there after that. */
|
|
51
|
+
export async function stopProcesses(pids) {
|
|
52
|
+
const signal = (name) => {
|
|
53
|
+
for (const pid of pids.filter(alive)) {
|
|
54
|
+
try {
|
|
55
|
+
process.kill(pid, name);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// gone meanwhile, or not ours: the check below tells
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const wait = async (ms) => {
|
|
63
|
+
for (let waited = 0; waited < ms && pids.some(alive); waited += 100) {
|
|
64
|
+
await new Promise((done) => setTimeout(done, 100));
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
signal("SIGTERM");
|
|
68
|
+
await wait(8000);
|
|
69
|
+
signal("SIGKILL");
|
|
70
|
+
await wait(2000);
|
|
71
|
+
return pids.filter(alive);
|
|
72
|
+
}
|
|
@@ -3,7 +3,7 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { delimiter, join } from "node:path";
|
|
4
4
|
import * as p from "@clack/prompts";
|
|
5
5
|
import { accountStep } from "./account.js";
|
|
6
|
-
import {
|
|
6
|
+
import { autostartForStatus, setAutostart } from "./autostart.js";
|
|
7
7
|
import { installClient, installCommand } from "./clients.js";
|
|
8
8
|
import { appStates, connectApps } from "./connect.js";
|
|
9
9
|
import { runLogged } from "./exec.js";
|
|
@@ -129,7 +129,7 @@ async function toolsStep(interactive) {
|
|
|
129
129
|
* service does. Asked, never assumed; `autostart off` undoes it.
|
|
130
130
|
*/
|
|
131
131
|
async function autostartStep(paths, interactive) {
|
|
132
|
-
const state = await
|
|
132
|
+
const state = await autostartForStatus(paths);
|
|
133
133
|
if ("unavailable" in state) {
|
|
134
134
|
return;
|
|
135
135
|
}
|
|
@@ -261,7 +261,7 @@ async function pathStep(interactive) {
|
|
|
261
261
|
async function howToUse(paths) {
|
|
262
262
|
const [running, atLogin] = await Promise.all([
|
|
263
263
|
runningRuntime(settings(paths).dataDir),
|
|
264
|
-
|
|
264
|
+
autostartForStatus(paths),
|
|
265
265
|
]);
|
|
266
266
|
const wizards = command();
|
|
267
267
|
const row = (what, cmd, note = "") => `${what.padEnd(9)}${cyan(cmd.padEnd(wizards.length + 12))}${note ? dim(note) : ""}`;
|
|
@@ -322,7 +322,12 @@ export async function setup(paths, options) {
|
|
|
322
322
|
if ((await pathStep(interactive)) === "cancelled") {
|
|
323
323
|
return false;
|
|
324
324
|
}
|
|
325
|
-
writeInstallNote({
|
|
325
|
+
writeInstallNote({
|
|
326
|
+
...readInstallNote(paths),
|
|
327
|
+
setupAt: new Date().toISOString(),
|
|
328
|
+
version,
|
|
329
|
+
unattended: !process.stdout.isTTY,
|
|
330
|
+
}, paths);
|
|
326
331
|
p.log.info(CHROME_APP);
|
|
327
332
|
await howToUse(paths);
|
|
328
333
|
return true;
|
|
@@ -6,8 +6,9 @@ import { join, resolve } from "node:path";
|
|
|
6
6
|
import { mintLocalTicket } from "../auth/local-ticket.js";
|
|
7
7
|
import { readRunning } from "../running.js";
|
|
8
8
|
import { RESTART_EXIT } from "../update.js";
|
|
9
|
+
import { applyPendingUpdate, autoUpdatePolicy, notesUrl } from "./autoupdate.js";
|
|
9
10
|
import { runtimeEnv } from "./environment.js";
|
|
10
|
-
import { packageRoot, readEnvFile } from "./home.js";
|
|
11
|
+
import { installedByScript, packageRoot, readEnvFile } from "./home.js";
|
|
11
12
|
import { findOnPath } from "./machine.js";
|
|
12
13
|
import { cmd, cyan, dim, live, tilde } from "./ui.js";
|
|
13
14
|
const DEFAULT_PORT = 24368;
|
|
@@ -163,6 +164,22 @@ export async function start(paths, options) {
|
|
|
163
164
|
console.log(dim(` It was already running. To stop it: ${cmd("stop")}`));
|
|
164
165
|
return 0;
|
|
165
166
|
}
|
|
167
|
+
// Nothing runs, so no run is cut off: a version downloaded meanwhile becomes current and
|
|
168
|
+
// takes this process's place (the login item keeps waiting on the same pid). In the
|
|
169
|
+
// background, the `start` spawned below does it.
|
|
170
|
+
if (!options.background && installedByScript(paths) && process.execve) {
|
|
171
|
+
const switched = autoUpdatePolicy(paths, { installed: true }).on && applyPendingUpdate(paths);
|
|
172
|
+
if (switched) {
|
|
173
|
+
console.log(` ${live} Updated to ${switched.to}. What changed: ${notesUrl(switched.to)}`);
|
|
174
|
+
const wrapper = join(paths.bin, "wizards");
|
|
175
|
+
try {
|
|
176
|
+
process.execve(wrapper, [wrapper, "start", ...(options.open ? [] : ["--no-open"])]);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// This version starts once more; the next start is the new one.
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
166
183
|
if (options.background) {
|
|
167
184
|
const started = await startInBackground(paths);
|
|
168
185
|
if (!started) {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { accessSync, constants, existsSync, mkdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { accountState, describeAccount } from "./account.js";
|
|
4
|
-
import { autostartState, setAutostart } from "./autostart.js";
|
|
4
|
+
import { autostartForStatus, autostartState, setAutostart } from "./autostart.js";
|
|
5
|
+
import { autoUpdatePolicy, updateNotice } from "./autoupdate.js";
|
|
5
6
|
import { installedByScript, isCheckout, packageRoot, packageVersion } from "./home.js";
|
|
6
7
|
import { detectClients, findChrome, findFfmpeg, nodeIsCurrent } from "./machine.js";
|
|
8
|
+
import { findWizardsProcesses, stopProcesses } from "./processes.js";
|
|
7
9
|
import { entryUrl, runningRuntime, settings, startInBackground } from "./start.js";
|
|
8
10
|
import { bad, badge, cmd, cyan, dim, no, ok, tilde } from "./ui.js";
|
|
9
11
|
/**
|
|
@@ -16,7 +18,7 @@ export async function status(paths, checks) {
|
|
|
16
18
|
runningRuntime(dataDir),
|
|
17
19
|
detectClients(paths),
|
|
18
20
|
findFfmpeg(),
|
|
19
|
-
|
|
21
|
+
autostartForStatus(paths),
|
|
20
22
|
accountState(),
|
|
21
23
|
]);
|
|
22
24
|
const chrome = findChrome();
|
|
@@ -33,12 +35,23 @@ export async function status(paths, checks) {
|
|
|
33
35
|
if (!("unavailable" in atLogin)) {
|
|
34
36
|
row("At login", atLogin.on ? `${ok} starts ${dim(`(${atLogin.how})`)}` : `${no} doesn’t start`);
|
|
35
37
|
}
|
|
38
|
+
if (installedByScript(paths)) {
|
|
39
|
+
const policy = autoUpdatePolicy(paths, { installed: true });
|
|
40
|
+
row("Updates", policy.on ? `${ok} automatic` : `${no} ${policy.reason} ${dim(`(${cmd("autoupdate on")})`)}`);
|
|
41
|
+
}
|
|
36
42
|
row("Account", `${account.kind === "linked" ? ok : no} ${describeAccount(account)}`);
|
|
37
43
|
for (const { client, path, version } of clients) {
|
|
38
44
|
row(client.name.split(" ")[0], path ? `${ok} ${version} ${dim(tilde(path))}` : `${no} not installed`);
|
|
39
45
|
}
|
|
40
46
|
row("Browser", chrome ? `${ok} ${dim(tilde(chrome))}` : `${no} not found (needed for PDF and PNG)`);
|
|
41
47
|
row("ffmpeg", ffmpeg.path ? `${ok} ${ffmpeg.version ?? ""}` : `${no} not found (needed for MP4)`);
|
|
48
|
+
const { updated, pending } = updateNotice(paths, packageVersion());
|
|
49
|
+
if (updated) {
|
|
50
|
+
console.log(`\n ${ok} Updated to ${updated.to}. What changed: ${cyan(updated.notes)}`);
|
|
51
|
+
}
|
|
52
|
+
else if (pending) {
|
|
53
|
+
console.log(`\n ${ok} Version ${pending.version} is downloaded; it runs from the next start.`);
|
|
54
|
+
}
|
|
42
55
|
console.log("");
|
|
43
56
|
if (!checks) {
|
|
44
57
|
return 0;
|
|
@@ -84,21 +97,41 @@ async function ended(pid, ms) {
|
|
|
84
97
|
}
|
|
85
98
|
return false;
|
|
86
99
|
}
|
|
87
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* Stops engenty wizards: the runtime that runs on this install's data folder, and every other
|
|
102
|
+
* runtime or `start` of this install still about (an older `start --no-open` whose note another
|
|
103
|
+
* one overwrote). `wizards mcp` processes belong to the AI apps and stay.
|
|
104
|
+
*/
|
|
88
105
|
export async function stop(paths) {
|
|
89
106
|
const { dataDir } = settings(paths);
|
|
90
107
|
const running = await runningRuntime(dataDir);
|
|
91
|
-
|
|
108
|
+
const found = findWizardsProcesses(paths.home);
|
|
109
|
+
const pids = [
|
|
110
|
+
...new Set([
|
|
111
|
+
...(running ? [running.pid] : []),
|
|
112
|
+
...found.filter((p) => p.kind === "runtime" || p.kind === "start").map((p) => p.pid),
|
|
113
|
+
]),
|
|
114
|
+
];
|
|
115
|
+
if (!pids.length) {
|
|
92
116
|
console.log("engenty wizards isn’t running.");
|
|
93
117
|
return 0;
|
|
94
118
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
119
|
+
const left = await stopProcesses(pids);
|
|
120
|
+
const stopped = pids.length - left.length;
|
|
121
|
+
if (stopped) {
|
|
122
|
+
console.log(pids.length === 1
|
|
123
|
+
? `Stopped (pid ${pids[0]}). Your data is kept.`
|
|
124
|
+
: `Stopped ${stopped} processes (pid ${pids.filter((pid) => !left.includes(pid)).join(", ")}). Your data is kept.`);
|
|
99
125
|
}
|
|
100
|
-
|
|
101
|
-
|
|
126
|
+
const apps = found.filter((p) => p.kind === "mcp").length;
|
|
127
|
+
if (apps && !left.length) {
|
|
128
|
+
console.log(dim(`${apps === 1 ? "An AI app connection still runs" : `${apps} AI app connections still run`}. Using a wizard there starts engenty wizards again.`));
|
|
129
|
+
}
|
|
130
|
+
if (left.length) {
|
|
131
|
+
console.error(`${left.length === 1 ? "A process" : `${left.length} processes`} of engenty wizards didn’t stop (pid ${left.join(", ")}). To stop ${left.length === 1 ? "it" : "them"}: ${cyan(`kill -9 ${left.join(" ")}`)}`);
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
134
|
+
return 0;
|
|
102
135
|
}
|
|
103
136
|
/**
|
|
104
137
|
* Stops the runtime and starts it again, the version on disk now. One the command started (a
|
|
@@ -145,15 +178,16 @@ export async function autostart(paths, value) {
|
|
|
145
178
|
console.error(`Use ${cmd("autostart on")} or ${cmd("autostart off")}.`);
|
|
146
179
|
return 2;
|
|
147
180
|
}
|
|
148
|
-
let state
|
|
149
|
-
|
|
150
|
-
|
|
181
|
+
let state;
|
|
182
|
+
try {
|
|
183
|
+
state = await autostartState(paths);
|
|
184
|
+
if (value && !("unavailable" in state)) {
|
|
151
185
|
state = await setAutostart(paths, value === "on");
|
|
152
186
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
console.error(`Couldn’t ${value ? `turn start at login ${value}` : "check start at login"}: ${error.message}`);
|
|
190
|
+
return 1;
|
|
157
191
|
}
|
|
158
192
|
if ("unavailable" in state) {
|
|
159
193
|
console.error(`Start at login isn’t available here: ${state.unavailable}.`);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { layout } from "./home.js";
|
|
2
|
+
import { nodeIsCurrent } from "./machine.js";
|
|
3
|
+
import { parseUninstall, uninstall } from "./uninstall.js";
|
|
4
|
+
/**
|
|
5
|
+
* `wizards uninstall` on its own: bin/wizards.mjs starts it instead of main.ts, whose modules make
|
|
6
|
+
* the install folder, its data folder and a secret when they load (install-env.ts, env.ts,
|
|
7
|
+
* db/client.ts). An uninstall must find the computer as it is, and leave nothing behind.
|
|
8
|
+
*/
|
|
9
|
+
if (!nodeIsCurrent()) {
|
|
10
|
+
console.error(`engenty wizards needs Node.js 24.11 or newer. This is ${process.version}.`);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
const options = parseUninstall(process.argv.slice(2));
|
|
14
|
+
if (typeof options === "string") {
|
|
15
|
+
console.error(options);
|
|
16
|
+
process.exitCode = 2;
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
process.exitCode = await uninstall(layout(), options);
|
|
20
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, lstatSync, readdirSync, readlinkSync, rmdirSync, rmSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
5
|
+
import * as p from "@clack/prompts";
|
|
6
|
+
import { LOGIN_LABEL, present, removeUnit, UNIT, unloadLoginItem } from "./autostart.js";
|
|
7
|
+
import { apps as allApps } from "./connect.js";
|
|
8
|
+
import { findWizardsProcesses, stopProcesses } from "./processes.js";
|
|
9
|
+
import { settings } from "./start.js";
|
|
10
|
+
import { bad, cyan, dim, no, ok } from "./ui.js";
|
|
11
|
+
export const realSystem = () => ({
|
|
12
|
+
userHome: homedir(),
|
|
13
|
+
platform: process.platform,
|
|
14
|
+
env: process.env,
|
|
15
|
+
interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
16
|
+
apps: allApps,
|
|
17
|
+
processes: findWizardsProcesses,
|
|
18
|
+
stop: (pids) => stopProcesses(pids),
|
|
19
|
+
unloadLoginItem,
|
|
20
|
+
removeUnit,
|
|
21
|
+
});
|
|
22
|
+
/** Parts of the install folder that are the person's: kept or deleted with the data. */
|
|
23
|
+
const WITH_DATA = [".env", ".env.local", "plugins"];
|
|
24
|
+
function loginItemPath(system) {
|
|
25
|
+
if (system.platform === "darwin") {
|
|
26
|
+
return join(system.userHome, "Library", "LaunchAgents", `${LOGIN_LABEL}.plist`);
|
|
27
|
+
}
|
|
28
|
+
if (system.platform === "linux") {
|
|
29
|
+
const config = system.env.XDG_CONFIG_HOME || join(system.userHome, ".config");
|
|
30
|
+
return join(config, "systemd", "user", UNIT);
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
/** The links install.sh put into ~/.local/bin that point into the install. */
|
|
35
|
+
function ourLinks(home, system) {
|
|
36
|
+
return ["wizards", "engenty-wizards"]
|
|
37
|
+
.map((name) => join(system.userHome, ".local", "bin", name))
|
|
38
|
+
.filter((link) => {
|
|
39
|
+
try {
|
|
40
|
+
return (lstatSync(link).isSymbolicLink() &&
|
|
41
|
+
resolve(dirname(link), readlinkSync(link)).startsWith(home + sep));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/** Works out what an uninstall takes away, without changing anything. */
|
|
49
|
+
export async function makePlan(paths, system) {
|
|
50
|
+
const apps = [];
|
|
51
|
+
for (const app of system.apps(paths)) {
|
|
52
|
+
if (await app.connected().catch(() => false)) {
|
|
53
|
+
apps.push(app);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const item = loginItemPath(system);
|
|
57
|
+
let loginItem = null;
|
|
58
|
+
try {
|
|
59
|
+
loginItem = item && present(item) ? item : null;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Can't look: count it as there, so the uninstall tries and says so.
|
|
63
|
+
loginItem = item;
|
|
64
|
+
}
|
|
65
|
+
const { dataDir } = settings(paths, system.env);
|
|
66
|
+
const program = [];
|
|
67
|
+
const data = existsSync(dataDir) ? [dataDir] : [];
|
|
68
|
+
for (const name of existsSync(paths.home) ? readdirSync(paths.home) : []) {
|
|
69
|
+
const path = join(paths.home, name);
|
|
70
|
+
if (path === dataDir) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const empty = name === "plugins" && readdirSync(path).length === 0;
|
|
74
|
+
(WITH_DATA.includes(name) && !empty ? data : program).push(path);
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
home: paths.home,
|
|
78
|
+
apps,
|
|
79
|
+
loginItem,
|
|
80
|
+
processes: system.processes(paths.home),
|
|
81
|
+
program,
|
|
82
|
+
links: ourLinks(paths.home, system),
|
|
83
|
+
data,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** Size of a folder as `du` counts it, e.g. " (805 MB)"; "" where it can't tell. */
|
|
87
|
+
function size(path) {
|
|
88
|
+
const kb = Number(spawnSync("du", ["-sk", path], { encoding: "utf8" }).stdout?.split(/\s/)[0]);
|
|
89
|
+
return kb > 0 ? ` (${kb < 1024 ? `${kb} KB` : `${Math.round(kb / 1024)} MB`})` : "";
|
|
90
|
+
}
|
|
91
|
+
const processes = (n) => `${n} running process${n === 1 ? "" : "es"}`;
|
|
92
|
+
/** `wizards uninstall [--dry-run] [--yes] [--delete-data]`. */
|
|
93
|
+
export async function uninstall(paths, options, system = realSystem()) {
|
|
94
|
+
const t = (path) => path.startsWith(system.userHome + sep) ? `~${path.slice(system.userHome.length)}` : path;
|
|
95
|
+
if (!options.dryRun && !options.yes && !system.interactive) {
|
|
96
|
+
console.error("The uninstall asks before it deletes anything, and there is no terminal to ask in.\n" +
|
|
97
|
+
"Run it with --yes (your data is kept), --yes --delete-data, or --dry-run to only look.");
|
|
98
|
+
return 2;
|
|
99
|
+
}
|
|
100
|
+
const plan = await makePlan(paths, system);
|
|
101
|
+
const steps = [
|
|
102
|
+
...plan.apps.map((app) => `Disconnect ${app.name}`),
|
|
103
|
+
...(plan.loginItem ? [`Remove the login item ${t(plan.loginItem)}`] : []),
|
|
104
|
+
...(plan.processes.length ? [`Stop ${processes(plan.processes.length)}`] : []),
|
|
105
|
+
...(plan.program.length ? [`Delete the program in ${t(plan.home)}${size(plan.home)}`] : []),
|
|
106
|
+
...plan.links.map((link) => `Delete the command ${t(link)}`),
|
|
107
|
+
];
|
|
108
|
+
if (!steps.length && !plan.data.length) {
|
|
109
|
+
console.log(`${ok} engenty wizards isn’t installed here.`);
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
if (steps.length) {
|
|
113
|
+
console.log("\nThis removes engenty wizards:");
|
|
114
|
+
}
|
|
115
|
+
for (const step of steps) {
|
|
116
|
+
console.log(` ${no} ${step}`);
|
|
117
|
+
}
|
|
118
|
+
if (plan.data.length) {
|
|
119
|
+
console.log(`\nYour data: ${plan.data.map((path) => t(path) + size(path)).join(", ")}`);
|
|
120
|
+
console.log(dim(" Kept unless you choose to delete it. A reinstall picks it up again."));
|
|
121
|
+
}
|
|
122
|
+
console.log("");
|
|
123
|
+
if (options.dryRun) {
|
|
124
|
+
console.log(dim("Nothing was changed (--dry-run)."));
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
let deleteData = options.yes && options.deleteData;
|
|
128
|
+
if (!options.yes) {
|
|
129
|
+
if (plan.data.length) {
|
|
130
|
+
const answer = await p.confirm({
|
|
131
|
+
message: "Delete your data too? Your wizards, runs and results are gone for good.",
|
|
132
|
+
initialValue: false,
|
|
133
|
+
});
|
|
134
|
+
if (p.isCancel(answer)) {
|
|
135
|
+
return 130;
|
|
136
|
+
}
|
|
137
|
+
deleteData = answer;
|
|
138
|
+
}
|
|
139
|
+
const go = await p.confirm({
|
|
140
|
+
message: deleteData ? "Uninstall and delete your data now?" : "Uninstall now?",
|
|
141
|
+
});
|
|
142
|
+
if (p.isCancel(go) || !go) {
|
|
143
|
+
p.cancel("Nothing was changed.");
|
|
144
|
+
return 130;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const failed = (what, error) => console.log(`${bad} ${what}: ${error.message.split("\n")[0]}`);
|
|
148
|
+
for (const app of plan.apps) {
|
|
149
|
+
const result = await app
|
|
150
|
+
.disconnect()
|
|
151
|
+
.catch((error) => ({ ok: false, message: error.message }));
|
|
152
|
+
console.log(result.ok ? `${ok} Disconnected ${app.name}` : `${bad} ${app.name}: ${result.message}`);
|
|
153
|
+
}
|
|
154
|
+
if (plan.loginItem) {
|
|
155
|
+
try {
|
|
156
|
+
if (system.platform === "darwin") {
|
|
157
|
+
await system.unloadLoginItem();
|
|
158
|
+
rmSync(plan.loginItem, { force: true });
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
await system.removeUnit(plan.loginItem);
|
|
162
|
+
}
|
|
163
|
+
console.log(`${ok} Removed the login item`);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
failed("The login item", error);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Twice: an AI app's `wizards mcp` may start the studio again meanwhile.
|
|
170
|
+
for (let round = 0; round < 2; round++) {
|
|
171
|
+
const running = system.processes(paths.home);
|
|
172
|
+
if (running.length && !(await system.stop(running.map((proc) => proc.pid))).length) {
|
|
173
|
+
console.log(`${ok} Stopped ${processes(running.length)}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
for (const path of [...plan.program, ...plan.links, ...(deleteData ? plan.data : [])]) {
|
|
177
|
+
try {
|
|
178
|
+
rmSync(path, { recursive: true, force: true });
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
failed(`Couldn’t delete ${t(path)}`, error);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
for (const dir of [plan.home, join(system.userHome, ".engenty")]) {
|
|
185
|
+
try {
|
|
186
|
+
rmdirSync(dir);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// not empty, or not there
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// A second look: what is still there.
|
|
193
|
+
const after = await makePlan(paths, system);
|
|
194
|
+
const left = [
|
|
195
|
+
...after.apps.map((app) => `${app.name} still lists the wizards: ${cyan(`npx wizards disconnect ${app.id}`)}`),
|
|
196
|
+
...(after.loginItem ? [t(after.loginItem)] : []),
|
|
197
|
+
...after.processes.map((proc) => `process ${proc.pid}: ${cyan(`kill -9 ${proc.pid}`)}`),
|
|
198
|
+
...[...after.program, ...after.links, ...(deleteData ? after.data : [])].map(t),
|
|
199
|
+
];
|
|
200
|
+
if (!deleteData && after.data.length) {
|
|
201
|
+
console.log(`${ok} Kept your data: ${after.data.map(t).join(", ")}`);
|
|
202
|
+
}
|
|
203
|
+
if (left.length) {
|
|
204
|
+
console.log(`\n${bad} Still there:`);
|
|
205
|
+
for (const line of left) {
|
|
206
|
+
console.log(` ${line}`);
|
|
207
|
+
}
|
|
208
|
+
return 1;
|
|
209
|
+
}
|
|
210
|
+
console.log(`\n${ok} engenty wizards is uninstalled.\n`);
|
|
211
|
+
return 0;
|
|
212
|
+
}
|
|
213
|
+
/** The options of `uninstall` from its command line; a message when something is off. */
|
|
214
|
+
export function parseUninstall(argv) {
|
|
215
|
+
const options = { dryRun: false, yes: false, deleteData: false };
|
|
216
|
+
for (const arg of argv) {
|
|
217
|
+
if (arg === "--dry-run") {
|
|
218
|
+
options.dryRun = true;
|
|
219
|
+
}
|
|
220
|
+
else if (arg === "--yes" || arg === "-y") {
|
|
221
|
+
options.yes = true;
|
|
222
|
+
}
|
|
223
|
+
else if (arg === "--delete-data") {
|
|
224
|
+
options.deleteData = true;
|
|
225
|
+
}
|
|
226
|
+
else if (arg !== "uninstall") {
|
|
227
|
+
return `Unknown option ${arg}. The uninstall takes --dry-run, --yes and --delete-data.`;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return options;
|
|
231
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { autoUpdatePolicy, noteUpdated } from "./autoupdate.js";
|
|
5
|
+
import { installedByScript, isCheckout, readInstallNote, writeInstallNote, } from "./home.js";
|
|
5
6
|
import { runningRuntime, settings } from "./start.js";
|
|
6
7
|
import { restart } from "./status.js";
|
|
7
|
-
import { cmd, cyan } from "./ui.js";
|
|
8
|
+
import { cmd, cyan, no, ok } from "./ui.js";
|
|
9
|
+
import { currentVersion } from "./versions.js";
|
|
8
10
|
/** The installer this install was made with; it also updates one. */
|
|
9
11
|
const INSTALLER = "https://engenty.ai/install.sh";
|
|
10
12
|
async function installer(into) {
|
|
@@ -34,6 +36,7 @@ export async function update(paths, options = {}) {
|
|
|
34
36
|
: `This copy runs from npm. To update: ${cyan("npx wizards@latest")}`);
|
|
35
37
|
return 0;
|
|
36
38
|
}
|
|
39
|
+
const before = currentVersion(paths);
|
|
37
40
|
const tmp = mkdtempSync(join(paths.home, "update-"));
|
|
38
41
|
try {
|
|
39
42
|
const script = await installer(tmp);
|
|
@@ -53,6 +56,10 @@ export async function update(paths, options = {}) {
|
|
|
53
56
|
finally {
|
|
54
57
|
rmSync(tmp, { recursive: true, force: true });
|
|
55
58
|
}
|
|
59
|
+
const after = currentVersion(paths);
|
|
60
|
+
if (before && after) {
|
|
61
|
+
noteUpdated(paths, before, after);
|
|
62
|
+
}
|
|
56
63
|
if (await runningRuntime(settings(paths).dataDir)) {
|
|
57
64
|
if (options.restart) {
|
|
58
65
|
return restart(paths);
|
|
@@ -61,3 +68,19 @@ export async function update(paths, options = {}) {
|
|
|
61
68
|
}
|
|
62
69
|
return 0;
|
|
63
70
|
}
|
|
71
|
+
/** `wizards autoupdate [on|off]`: whether updates come by themselves (autoupdate.ts). */
|
|
72
|
+
export function autoUpdateCommand(paths, value) {
|
|
73
|
+
if (value && value !== "on" && value !== "off") {
|
|
74
|
+
console.error(`Use ${cmd("autoupdate on")} or ${cmd("autoupdate off")}.`);
|
|
75
|
+
return 2;
|
|
76
|
+
}
|
|
77
|
+
const installed = installedByScript(paths);
|
|
78
|
+
if (value && installed) {
|
|
79
|
+
writeInstallNote({ ...readInstallNote(paths), autoUpdate: value === "on" }, paths);
|
|
80
|
+
}
|
|
81
|
+
const policy = autoUpdatePolicy(paths, { installed });
|
|
82
|
+
console.log(policy.on
|
|
83
|
+
? `${ok} Updates come by themselves: downloaded in the background, used from the next start.`
|
|
84
|
+
: `${no} Updates don’t come by themselves: ${policy.reason}. To update now: ${cmd("update")}`);
|
|
85
|
+
return value && !installed ? 1 : 0;
|
|
86
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, renameSync, rmSync, symlinkSync, } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* The runtimes of an install made by install.sh, one folder per version, side by side:
|
|
5
|
+
*
|
|
6
|
+
* runtime/<version>/ an npm prefix: node_modules/wizards, package.json, package-lock.json
|
|
7
|
+
* runtime/current → <version>, the one `bin/wizards` starts
|
|
8
|
+
* runtime/node_modules → current/node_modules, for commands written before the versions
|
|
9
|
+
*
|
|
10
|
+
* Up to 0.6.2 `runtime/` itself was the prefix; `switchTo` moves such an install into its
|
|
11
|
+
* version folder first. A process keeps the files of the version it started with.
|
|
12
|
+
*/
|
|
13
|
+
const VERSION = /^\d+\.\d+\.\d+$/;
|
|
14
|
+
export const runtimeDir = (paths, version) => join(paths.runtime, version);
|
|
15
|
+
const currentLink = (paths) => join(paths.runtime, "current");
|
|
16
|
+
const modules = (paths) => join(paths.runtime, "node_modules");
|
|
17
|
+
/** The version of the package `wizards` in an npm prefix; null when there is none. */
|
|
18
|
+
export function versionIn(prefix) {
|
|
19
|
+
try {
|
|
20
|
+
const file = join(prefix, "node_modules", "wizards", "package.json");
|
|
21
|
+
const { version } = JSON.parse(readFileSync(file, "utf8"));
|
|
22
|
+
return typeof version === "string" ? version : null;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const isLink = (path) => {
|
|
29
|
+
try {
|
|
30
|
+
return lstatSync(path).isSymbolicLink();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
/** The version `bin/wizards` starts; null without a runtime. */
|
|
37
|
+
export function currentVersion(paths) {
|
|
38
|
+
if (isLink(currentLink(paths))) {
|
|
39
|
+
return versionIn(join(paths.runtime, readlinkSync(currentLink(paths))));
|
|
40
|
+
}
|
|
41
|
+
return versionIn(paths.runtime);
|
|
42
|
+
}
|
|
43
|
+
/** The version folders there are, each with a complete package in it. */
|
|
44
|
+
export function installedVersions(paths) {
|
|
45
|
+
try {
|
|
46
|
+
return readdirSync(paths.runtime)
|
|
47
|
+
.filter((name) => VERSION.test(name) && versionIn(join(paths.runtime, name)) === name)
|
|
48
|
+
.sort();
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Points `link` at `target` in one step: a new link renamed over the old one. */
|
|
55
|
+
function link(target, path) {
|
|
56
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
57
|
+
rmSync(tmp, { force: true });
|
|
58
|
+
symlinkSync(target, tmp);
|
|
59
|
+
renameSync(tmp, path);
|
|
60
|
+
}
|
|
61
|
+
/** Moves a runtime from before the version folders into `runtime/<version>/`. */
|
|
62
|
+
function adoptLegacy(paths) {
|
|
63
|
+
const version = versionIn(paths.runtime);
|
|
64
|
+
if (isLink(modules(paths)) || !version) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const into = runtimeDir(paths, version);
|
|
68
|
+
rmSync(into, { recursive: true, force: true });
|
|
69
|
+
mkdirSync(into, { recursive: true });
|
|
70
|
+
for (const name of ["node_modules", "package.json", "package-lock.json"]) {
|
|
71
|
+
if (existsSync(join(paths.runtime, name))) {
|
|
72
|
+
renameSync(join(paths.runtime, name), join(into, name));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
link(version, currentLink(paths));
|
|
76
|
+
}
|
|
77
|
+
/** Makes `version` the one `bin/wizards` starts from now on. */
|
|
78
|
+
export function switchTo(paths, version) {
|
|
79
|
+
if (versionIn(runtimeDir(paths, version)) !== version) {
|
|
80
|
+
throw new Error(`${runtimeDir(paths, version)} holds no complete runtime ${version}.`);
|
|
81
|
+
}
|
|
82
|
+
adoptLegacy(paths);
|
|
83
|
+
link(version, currentLink(paths));
|
|
84
|
+
link(join("current", "node_modules"), modules(paths));
|
|
85
|
+
}
|
|
86
|
+
/** Deletes the version folders other than `keep` and the current one; returns what went. */
|
|
87
|
+
export function prune(paths, keep) {
|
|
88
|
+
const current = currentVersion(paths);
|
|
89
|
+
const stale = installedVersions(paths).filter((v) => v !== current && !keep.includes(v));
|
|
90
|
+
for (const version of stale) {
|
|
91
|
+
rmSync(runtimeDir(paths, version), { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
return stale;
|
|
94
|
+
}
|
|
95
|
+
/** Puts an npm prefix installed elsewhere into its version folder (never the current one). */
|
|
96
|
+
export function promote(paths, staged, version) {
|
|
97
|
+
if (versionIn(staged) !== version) {
|
|
98
|
+
throw new Error(`The download holds version ${versionIn(staged) ?? "none"}, not ${version}.`);
|
|
99
|
+
}
|
|
100
|
+
if (currentVersion(paths) === version) {
|
|
101
|
+
rmSync(staged, { recursive: true, force: true });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
rmSync(runtimeDir(paths, version), { recursive: true, force: true });
|
|
105
|
+
renameSync(staged, runtimeDir(paths, version));
|
|
106
|
+
}
|