wizards 0.6.2 → 0.7.1

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 (88) hide show
  1. package/README.md +6 -2
  2. package/apps/runtime/dist/agents/architect.js +7 -29
  3. package/apps/runtime/dist/agents/studio-assistant.js +7 -24
  4. package/apps/runtime/dist/cli/autostart.js +58 -12
  5. package/apps/runtime/dist/cli/autoupdate.js +221 -0
  6. package/apps/runtime/dist/cli/connect.js +6 -2
  7. package/apps/runtime/dist/cli/environment.js +5 -0
  8. package/apps/runtime/dist/cli/machine.js +7 -3
  9. package/apps/runtime/dist/cli/main.js +10 -3
  10. package/apps/runtime/dist/cli/menu.js +27 -8
  11. package/apps/runtime/dist/cli/processes.js +72 -0
  12. package/apps/runtime/dist/cli/setup.js +26 -26
  13. package/apps/runtime/dist/cli/start.js +18 -1
  14. package/apps/runtime/dist/cli/status.js +51 -17
  15. package/apps/runtime/dist/cli/uninstall-main.js +20 -0
  16. package/apps/runtime/dist/cli/uninstall.js +231 -0
  17. package/apps/runtime/dist/cli/update.js +25 -2
  18. package/apps/runtime/dist/cli/versions.js +106 -0
  19. package/apps/runtime/dist/docs/user/command-line.md +3 -2
  20. package/apps/runtime/dist/docs/user/data.md +1 -1
  21. package/apps/runtime/dist/docs/user/install.md +29 -6
  22. package/apps/runtime/dist/engine/input.js +4 -5
  23. package/apps/runtime/dist/engine/steps.js +3 -3
  24. package/apps/runtime/dist/engine/template.js +8 -13
  25. package/apps/runtime/dist/env.js +3 -20
  26. package/apps/runtime/dist/features.js +2 -2
  27. package/apps/runtime/dist/index.js +3 -1
  28. package/apps/runtime/dist/manage.js +7 -0
  29. package/apps/runtime/dist/models.js +12 -0
  30. package/apps/runtime/dist/sandbox/docker.js +0 -2
  31. package/apps/runtime/dist/sandbox/index.js +3 -10
  32. package/apps/runtime/dist/services/space-categories.js +3 -25
  33. package/apps/runtime/dist/tools/browser.js +2 -14
  34. package/apps/runtime/dist/tools/index.js +1 -19
  35. package/apps/runtime/dist/tools/mail.js +1 -4
  36. package/apps/runtime/dist/tools/shared.js +30 -3
  37. package/apps/runtime/dist/update.js +10 -19
  38. package/apps/web/dist/assets/AboutDialog-BJALish1.js +1 -0
  39. package/apps/web/dist/assets/{ChangelogDialog-BRiSYyIn.js → ChangelogDialog-CxmHNvq1.js} +1 -1
  40. package/apps/web/dist/assets/ChatView-C9ASPVJj.js +10 -0
  41. package/apps/web/dist/assets/EditorPage-BHhNEiKa.js +3 -0
  42. package/apps/web/dist/assets/{FlowPreview-CUOEzoeJ.js → FlowPreview-DvpjuQdC.js} +1 -1
  43. package/apps/web/dist/assets/{Harness-D3kTVsF5.js → Harness-Duq0wpfg.js} +2 -2
  44. package/apps/web/dist/assets/{NewWizardChannels-C3PGe_Zd.js → NewWizardChannels-CGFy67jk.js} +1 -1
  45. package/apps/web/dist/assets/NewWizardPage-DwBcRWJj.js +7 -0
  46. package/apps/web/dist/assets/{OnboardingPage-pOPB5TVZ.js → OnboardingPage-YwKzupPI.js} +1 -1
  47. package/apps/web/dist/assets/ProjectPage-DP1xMjbe.js +1 -0
  48. package/apps/web/dist/assets/PublicRunner-CECAuP0g.js +1 -0
  49. package/apps/web/dist/assets/{ReadOnly-CCxZvYrT.js → ReadOnly-BnnGcLWU.js} +1 -1
  50. package/apps/web/dist/assets/RunDrawer-C0vf8QAc.js +4 -0
  51. package/apps/web/dist/assets/{RunnerView-pKik3sdE.js → RunnerView-CBrZpjt5.js} +1 -1
  52. package/apps/web/dist/assets/SettingsPage-C0-mVcs5.js +1 -0
  53. package/apps/web/dist/assets/SetupPage-BGlYQREU.js +1 -0
  54. package/apps/web/dist/assets/{SharePage-CZQqp5JM.js → SharePage-IwxlV6WC.js} +1 -1
  55. package/apps/web/dist/assets/changelog-CPNPseoQ.js +1 -0
  56. package/apps/web/dist/assets/{esm-D4pSRQgT.js → esm-6TNBQNCD.js} +1 -1
  57. package/apps/web/dist/assets/index-DU-Onx-c.js +14 -0
  58. package/apps/web/dist/assets/{lock-DYnHCyRg.js → lock-HLZib0u9.js} +1 -1
  59. package/apps/web/dist/assets/{phone-DJawY1RF.js → phone-DsxbIcWi.js} +1 -1
  60. package/apps/web/dist/assets/settings-menu-BMMkIVP1.js +1 -0
  61. package/apps/web/dist/assets/ui-Brg2qM1A.js +137 -0
  62. package/apps/web/dist/assets/{upload-DL6NRa4h.js → upload-BaGy0ppG.js} +1 -1
  63. package/apps/web/dist/index.html +2 -2
  64. package/apps/web/dist/install.sh +30 -12
  65. package/apps/web/dist/mcp/flow.html +11 -11
  66. package/bin/wizards.mjs +17 -3
  67. package/node_modules/@engenty-wizards/shared/dist/definition.js +6 -17
  68. package/node_modules/@engenty-wizards/shared/dist/features.d.ts +6 -0
  69. package/node_modules/@engenty-wizards/shared/dist/features.js +12 -0
  70. package/node_modules/@engenty-wizards/shared/dist/numbers.d.ts +24 -0
  71. package/node_modules/@engenty-wizards/shared/dist/numbers.js +54 -0
  72. package/node_modules/@engenty-wizards/shared/dist/store.d.ts +7 -0
  73. package/node_modules/@engenty-wizards/shared/dist/store.js +28 -0
  74. package/package.json +1 -1
  75. package/apps/runtime/dist/sandbox/agentos.js +0 -191
  76. package/apps/web/dist/assets/AboutDialog-BrfbtjmR.js +0 -1
  77. package/apps/web/dist/assets/ChatView-_dYvuuUY.js +0 -10
  78. package/apps/web/dist/assets/EditorPage-CkRjTRMg.js +0 -3
  79. package/apps/web/dist/assets/NewWizardPage-B3ol4K43.js +0 -7
  80. package/apps/web/dist/assets/ProjectPage-Boyptzz_.js +0 -1
  81. package/apps/web/dist/assets/PublicRunner-DcETj9_E.js +0 -1
  82. package/apps/web/dist/assets/RunDrawer-hwruVTDG.js +0 -4
  83. package/apps/web/dist/assets/SettingsPage-BOe6NgFg.js +0 -1
  84. package/apps/web/dist/assets/SetupPage-81QxPnfE.js +0 -1
  85. package/apps/web/dist/assets/changelog-CtyrKHAc.js +0 -1
  86. package/apps/web/dist/assets/index-DnIvQFIg.js +0 -14
  87. package/apps/web/dist/assets/settings-menu-B_-_EAmK.js +0 -1
  88. package/apps/web/dist/assets/ui-rh8TaWvT.js +0 -137
@@ -1,9 +1,11 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { join } from "node:path";
1
3
  import * as p from "@clack/prompts";
2
4
  import { updateStatus } from "../update.js";
3
5
  import { accountMenu, accountState, describeAccount } from "./account.js";
4
- import { autostartState, setAutostart } from "./autostart.js";
6
+ import { autostartForStatus, setAutostart } from "./autostart.js";
5
7
  import { connectCommand } from "./connect.js";
6
- import { command, packageVersion, readInstallNote } from "./home.js";
8
+ import { command, packageRoot, packageVersion, readInstallNote } from "./home.js";
7
9
  import { setup } from "./setup.js";
8
10
  import { open, runningRuntime, settings, start } from "./start.js";
9
11
  import { restart, status, stop } from "./status.js";
@@ -13,7 +15,7 @@ async function look(paths) {
13
15
  const { dataDir } = settings(paths);
14
16
  const [running, atLogin, release, account] = await Promise.all([
15
17
  runningRuntime(dataDir),
16
- autostartState(paths),
18
+ autostartForStatus(paths),
17
19
  updateStatus().catch(() => null),
18
20
  accountState(),
19
21
  ]);
@@ -45,7 +47,7 @@ function choices({ running, atLogin, release, account }) {
45
47
  ]
46
48
  : [
47
49
  { value: "start", label: "Start in the background", hint: "and open the studio" },
48
- { value: "foreground", label: "Start here", hint: "in this terminal (Ctrl-C stops it)" },
50
+ { value: "foreground", label: "Start in this terminal", hint: "Ctrl-C stops it" },
49
51
  ];
50
52
  options.push({
51
53
  value: "update",
@@ -62,14 +64,18 @@ function choices({ running, atLogin, release, account }) {
62
64
  options.push({
63
65
  value: "connect",
64
66
  label: "Connect AI apps",
65
- hint: "use your wizards in Claude, Cursor, Codex…",
67
+ hint: "add your wizards as an MCP server in Claude, Cursor, Codex…",
66
68
  }, {
67
69
  value: "account",
68
70
  label: "Account",
69
71
  hint: account.kind === "linked"
70
72
  ? `signed in as ${describeAccount(account)}`
71
73
  : describeAccount(account),
72
- }, { value: "status", label: "Status", hint: "what’s installed, and any problems" }, { value: "setup", label: "Setup", hint: "account, AI client, ffmpeg, start at login" }, { value: "help", label: "Help", hint: "every command" }, { value: "quit", label: "Quit", hint: running ? "engenty wizards keeps running" : undefined });
74
+ }, { value: "status", label: "Status", hint: "what’s installed, and any problems" }, { value: "setup", label: "Setup", hint: "account, AI client, ffmpeg, start at login" }, {
75
+ value: "uninstall",
76
+ label: "Uninstall",
77
+ hint: "remove it from this computer; asks about your data first",
78
+ }, { value: "help", label: "Help", hint: "every command" }, { value: "quit", label: "Quit", hint: running ? "engenty wizards keeps running" : undefined });
73
79
  return options;
74
80
  }
75
81
  /** Whether it starts at login. Esc goes back to the menu. */
@@ -81,7 +87,11 @@ async function serviceSettings(paths, atLogin) {
81
87
  message: "Start at login?",
82
88
  initialValue: atLogin.on,
83
89
  options: [
84
- { value: true, label: "Yes", hint: "runs in the background whenever you log in" },
90
+ {
91
+ value: true,
92
+ label: "Yes",
93
+ hint: "runs in the background whenever you log in to this computer",
94
+ },
85
95
  { value: false, label: "No", hint: "only when you start it" },
86
96
  ],
87
97
  });
@@ -112,7 +122,7 @@ function commands() {
112
122
  }
113
123
  /**
114
124
  * `wizards` on a terminal: what runs, and a menu of what the commands do. The first time, the
115
- * setup runs first. Returns the exit code; "Start here" ends it with the runtime in the
125
+ * setup runs first. Returns the exit code; "Start in this terminal" ends it with the runtime in the
116
126
  * foreground.
117
127
  */
118
128
  export async function menu(paths, setupOptions, help) {
@@ -175,6 +185,15 @@ export async function menu(paths, setupOptions, help) {
175
185
  case "setup":
176
186
  await setup(paths, setupOptions);
177
187
  break;
188
+ case "uninstall": {
189
+ // Its own process, as `wizards uninstall` is: this one's modules write to the install.
190
+ const { status: code } = spawnSync(process.execPath, [join(packageRoot, "bin", "wizards.mjs"), "uninstall"], { stdio: "inherit" });
191
+ // 130: the person said no, so the menu goes on.
192
+ if (code !== 130) {
193
+ return code ?? 1;
194
+ }
195
+ break;
196
+ }
178
197
  case "help":
179
198
  console.log(help);
180
199
  break;
@@ -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 { autostartState, setAutostart } from "./autostart.js";
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";
@@ -41,17 +41,16 @@ async function clientsStep(paths, options, interactive) {
41
41
  ? `Found AI clients: ${installed.map((s) => `${s.client.name} ${s.version}`).join(", ")}.`
42
42
  : "No AI client on this computer yet.");
43
43
  if (!installed.length) {
44
- p.log.message(dim("engenty wizards uses an AI client to think, on a subscription you already have."));
44
+ p.log.message(dim("engenty wizards runs its AI through an AI client, like Claude Code or Codex, on a subscription you already have."));
45
45
  }
46
46
  let wanted = missing.filter((s) => options.clients.includes(s.client.id));
47
- if (interactive && missing.length && !wanted.length) {
47
+ // With a client already here, setup asks nothing more; another one is a `--client` away.
48
+ if (interactive && !installed.length && missing.length && !wanted.length) {
48
49
  const suggested = suggestedClient(missing);
49
50
  // A client whose app is on this Mac comes first: its subscription is most likely paid for.
50
51
  const offered = [...missing].sort((a, b) => Number(!vendorApp(a.client)) - Number(!vendorApp(b.client)));
51
52
  const answer = await p.multiselect({
52
- message: installed.length
53
- ? "Install another AI client? (space to choose, enter to continue)"
54
- : "Which AI client should be installed?",
53
+ message: "Which AI client should be installed? (space to choose, enter to continue)",
55
54
  options: offered.map(({ client }) => {
56
55
  const app = vendorApp(client);
57
56
  return {
@@ -60,13 +59,13 @@ async function clientsStep(paths, options, interactive) {
60
59
  hint: [
61
60
  `${client.account} subscription`,
62
61
  app ? `the ${app} app is on this Mac` : "",
63
- !installed.length && client.id === suggested?.client.id ? "recommended" : "",
62
+ client.id === suggested?.client.id ? "recommended" : "",
64
63
  ]
65
64
  .filter(Boolean)
66
65
  .join(", "),
67
66
  };
68
67
  }),
69
- initialValues: installed.length || !suggested ? [] : [suggested.client.id],
68
+ initialValues: suggested ? [suggested.client.id] : [],
70
69
  required: false,
71
70
  });
72
71
  if (cancelled(answer)) {
@@ -89,7 +88,7 @@ async function clientsStep(paths, options, interactive) {
89
88
  async function toolsStep(interactive) {
90
89
  const chrome = findChrome();
91
90
  if (chrome) {
92
- p.log.success(`Browser for PDF and PNG: ${dim(tilde(chrome))}`);
91
+ p.log.success(`Browser for PDF and PNG exports: ${dim(tilde(chrome))}`);
93
92
  }
94
93
  else {
95
94
  p.log.warn("No Chrome, Edge or Chromium found. PDF and PNG exports and browser steps need one.\nhttps://www.google.com/chrome");
@@ -129,20 +128,20 @@ async function toolsStep(interactive) {
129
128
  * service does. Asked, never assumed; `autostart off` undoes it.
130
129
  */
131
130
  async function autostartStep(paths, interactive) {
132
- const state = await autostartState(paths);
131
+ const state = await autostartForStatus(paths);
133
132
  if ("unavailable" in state) {
134
133
  return;
135
134
  }
136
135
  if (state.on) {
137
- p.log.success(`Starts at login: ${dim(state.how)}`);
136
+ p.log.success(`Starts when you log in to this computer: ${dim(state.how)}`);
138
137
  return;
139
138
  }
140
139
  if (!interactive) {
141
- p.log.info(dim(`To start it when you log in: ${cmd("autostart on")}`));
140
+ p.log.info(dim(`To start it when you log in to this computer: ${cmd("autostart on")}`));
142
141
  return;
143
142
  }
144
143
  const answer = await p.confirm({
145
- message: "Start engenty wizards when you log in? It keeps running in the background, so links and your AI apps can always reach it.",
144
+ message: "Start engenty wizards when you log in to this computer? It runs as a background process, without a terminal window, so links and your AI apps can always reach it.",
146
145
  initialValue: true,
147
146
  });
148
147
  if (cancelled(answer)) {
@@ -155,14 +154,14 @@ async function autostartStep(paths, interactive) {
155
154
  try {
156
155
  const after = await setAutostart(paths, true);
157
156
  if ("on" in after && after.on) {
158
- p.log.success(`Starts at login: ${dim(after.how)}`);
157
+ p.log.success(`Starts when you log in to this computer: ${dim(after.how)}`);
159
158
  }
160
159
  else {
161
- p.log.warn("Couldn’t turn on start at login.");
160
+ p.log.warn("Couldn’t set it to start when you log in.");
162
161
  }
163
162
  }
164
163
  catch (error) {
165
- p.log.warn(`Couldn’t turn on start at login. ${error.message}`);
164
+ p.log.warn(`Couldn’t set it to start when you log in. ${error.message}`);
166
165
  }
167
166
  }
168
167
  /**
@@ -174,7 +173,7 @@ async function appsStep(paths, interactive) {
174
173
  const connected = states.filter((s) => s.connected);
175
174
  const offered = states.filter((s) => s.installed && !s.connected);
176
175
  if (connected.length) {
177
- p.log.success(`In your AI apps: ${connected.map((s) => s.name).join(", ")}`);
176
+ p.log.success(`Already in your AI apps: ${connected.map((s) => s.name).join(", ")}`);
178
177
  }
179
178
  if (!offered.length) {
180
179
  return;
@@ -184,12 +183,8 @@ async function appsStep(paths, interactive) {
184
183
  return;
185
184
  }
186
185
  const answer = await p.multiselect({
187
- message: "Use your wizards in these AI apps? (space to choose, enter to continue)",
188
- options: offered.map((s) => ({
189
- value: s.id,
190
- label: s.name,
191
- hint: s.widgets ? "shows runs as a widget" : "shows runs as text",
192
- })),
186
+ message: "Add your wizards to these AI apps, as an MCP server in each app’s settings? (space to choose, enter to continue)",
187
+ options: offered.map((s) => ({ value: s.id, label: s.name })),
193
188
  initialValues: offered.map((s) => s.id),
194
189
  required: false,
195
190
  });
@@ -261,7 +256,7 @@ async function pathStep(interactive) {
261
256
  async function howToUse(paths) {
262
257
  const [running, atLogin] = await Promise.all([
263
258
  runningRuntime(settings(paths).dataDir),
264
- autostartState(paths),
259
+ autostartForStatus(paths),
265
260
  ]);
266
261
  const wizards = command();
267
262
  const row = (what, cmd, note = "") => `${what.padEnd(9)}${cyan(cmd.padEnd(wizards.length + 12))}${note ? dim(note) : ""}`;
@@ -271,7 +266,7 @@ async function howToUse(paths) {
271
266
  : "engenty wizards isn’t running yet.",
272
267
  "unavailable" in atLogin || !atLogin.on
273
268
  ? ""
274
- : dim("It starts automatically when you log in."),
269
+ : dim("It starts automatically when you log in to this computer."),
275
270
  ]
276
271
  .filter(Boolean)
277
272
  .join("\n"));
@@ -322,7 +317,12 @@ export async function setup(paths, options) {
322
317
  if ((await pathStep(interactive)) === "cancelled") {
323
318
  return false;
324
319
  }
325
- writeInstallNote({ ...readInstallNote(paths), setupAt: new Date().toISOString(), version }, paths);
320
+ writeInstallNote({
321
+ ...readInstallNote(paths),
322
+ setupAt: new Date().toISOString(),
323
+ version,
324
+ unattended: !process.stdout.isTTY,
325
+ }, paths);
326
326
  p.log.info(CHROME_APP);
327
327
  await howToUse(paths);
328
328
  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
- autostartState(paths),
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
- /** Stops the runtime that runs on this install's data folder. */
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
- if (!running) {
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
- process.kill(running.pid, "SIGTERM");
96
- if (await ended(running.pid, 8000)) {
97
- console.log(`Stopped (pid ${running.pid}). Your data is kept.`);
98
- return 0;
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
- console.error(`engenty wizards (pid ${running.pid}) didn’t stop within 8 seconds.`);
101
- return 1;
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 = await autostartState(paths);
149
- if (value && !("unavailable" in state)) {
150
- try {
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
- catch (error) {
154
- console.error(`Couldn’t turn start at login ${value}: ${error.message}`);
155
- return 1;
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
+ }