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.
- 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/cli/autostart.js +58 -12
- package/apps/runtime/dist/cli/autoupdate.js +221 -0
- package/apps/runtime/dist/cli/connect.js +6 -2
- package/apps/runtime/dist/cli/environment.js +5 -0
- package/apps/runtime/dist/cli/machine.js +7 -3
- package/apps/runtime/dist/cli/main.js +10 -3
- package/apps/runtime/dist/cli/menu.js +27 -8
- package/apps/runtime/dist/cli/processes.js +72 -0
- package/apps/runtime/dist/cli/setup.js +26 -26
- 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/user/command-line.md +3 -2
- 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/steps.js +3 -3
- package/apps/runtime/dist/engine/template.js +8 -13
- 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/models.js +12 -0
- package/apps/runtime/dist/sandbox/docker.js +0 -2
- package/apps/runtime/dist/sandbox/index.js +3 -10
- package/apps/runtime/dist/services/space-categories.js +3 -25
- 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-BJALish1.js +1 -0
- package/apps/web/dist/assets/{ChangelogDialog-BRiSYyIn.js → ChangelogDialog-CxmHNvq1.js} +1 -1
- package/apps/web/dist/assets/ChatView-C9ASPVJj.js +10 -0
- package/apps/web/dist/assets/EditorPage-BHhNEiKa.js +3 -0
- package/apps/web/dist/assets/{FlowPreview-CUOEzoeJ.js → FlowPreview-DvpjuQdC.js} +1 -1
- package/apps/web/dist/assets/{Harness-D3kTVsF5.js → Harness-Duq0wpfg.js} +2 -2
- package/apps/web/dist/assets/{NewWizardChannels-C3PGe_Zd.js → NewWizardChannels-CGFy67jk.js} +1 -1
- package/apps/web/dist/assets/NewWizardPage-DwBcRWJj.js +7 -0
- package/apps/web/dist/assets/{OnboardingPage-pOPB5TVZ.js → OnboardingPage-YwKzupPI.js} +1 -1
- package/apps/web/dist/assets/ProjectPage-DP1xMjbe.js +1 -0
- package/apps/web/dist/assets/PublicRunner-CECAuP0g.js +1 -0
- package/apps/web/dist/assets/{ReadOnly-CCxZvYrT.js → ReadOnly-BnnGcLWU.js} +1 -1
- package/apps/web/dist/assets/RunDrawer-C0vf8QAc.js +4 -0
- package/apps/web/dist/assets/{RunnerView-pKik3sdE.js → RunnerView-CBrZpjt5.js} +1 -1
- package/apps/web/dist/assets/SettingsPage-C0-mVcs5.js +1 -0
- package/apps/web/dist/assets/SetupPage-BGlYQREU.js +1 -0
- package/apps/web/dist/assets/{SharePage-CZQqp5JM.js → SharePage-IwxlV6WC.js} +1 -1
- package/apps/web/dist/assets/changelog-CPNPseoQ.js +1 -0
- package/apps/web/dist/assets/{esm-D4pSRQgT.js → esm-6TNBQNCD.js} +1 -1
- package/apps/web/dist/assets/index-DU-Onx-c.js +14 -0
- package/apps/web/dist/assets/{lock-DYnHCyRg.js → lock-HLZib0u9.js} +1 -1
- package/apps/web/dist/assets/{phone-DJawY1RF.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-DL6NRa4h.js → upload-BaGy0ppG.js} +1 -1
- package/apps/web/dist/index.html +2 -2
- package/apps/web/dist/install.sh +30 -12
- package/apps/web/dist/mcp/flow.html +11 -11
- package/bin/wizards.mjs +17 -3
- package/node_modules/@engenty-wizards/shared/dist/definition.js +6 -17
- 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/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-BrfbtjmR.js +0 -1
- package/apps/web/dist/assets/ChatView-_dYvuuUY.js +0 -10
- package/apps/web/dist/assets/EditorPage-CkRjTRMg.js +0 -3
- package/apps/web/dist/assets/NewWizardPage-B3ol4K43.js +0 -7
- package/apps/web/dist/assets/ProjectPage-Boyptzz_.js +0 -1
- package/apps/web/dist/assets/PublicRunner-DcETj9_E.js +0 -1
- package/apps/web/dist/assets/RunDrawer-hwruVTDG.js +0 -4
- package/apps/web/dist/assets/SettingsPage-BOe6NgFg.js +0 -1
- package/apps/web/dist/assets/SetupPage-81QxPnfE.js +0 -1
- package/apps/web/dist/assets/changelog-CtyrKHAc.js +0 -1
- package/apps/web/dist/assets/index-DnIvQFIg.js +0 -14
- package/apps/web/dist/assets/settings-menu-B_-_EAmK.js +0 -1
- package/apps/web/dist/assets/ui-rh8TaWvT.js +0 -137
package/README.md
CHANGED
|
@@ -159,8 +159,12 @@ when a newer release is out and can run the same update from there or from its A
|
|
|
159
159
|
From source: `git pull`, `pnpm install`, `pnpm build`. Your data is brought up to date at the
|
|
160
160
|
next start.
|
|
161
161
|
|
|
162
|
-
To remove engenty wizards, run `wizards
|
|
163
|
-
|
|
162
|
+
To remove engenty wizards, run `wizards uninstall` (or `npx wizards uninstall` if the command no
|
|
163
|
+
longer runs). It shows what it will remove first: your AI apps' entries, the login item, every
|
|
164
|
+
running process, `~/.engenty/wizards` and `~/.local/bin/wizards`. Your data in
|
|
165
|
+
`~/.engenty/wizards/data` is kept unless you choose to delete it, and a reinstall picks it up
|
|
166
|
+
again. `--dry-run` only shows the plan, `--yes` runs it without questions and keeps your data,
|
|
167
|
+
and `--yes --delete-data` deletes your data too.
|
|
164
168
|
|
|
165
169
|
## Run it on a server
|
|
166
170
|
|
|
@@ -7,16 +7,16 @@ import { z } from "zod";
|
|
|
7
7
|
import { connectorsLine, exampleWizard, mcpServersLine, PRINCIPLES, pluginToolsLine, SCHEMA_DOC, WIDGET_GUIDE, } from "../authoring/guide.js";
|
|
8
8
|
import { wizardOpSchema } from "../authoring/ops.js";
|
|
9
9
|
import { importFromRegistry, listConnectors, searchRegistry } from "../connectors/external.js";
|
|
10
|
-
import { attachTools, costOf, gatewayTools, textModel } from "../models.js";
|
|
10
|
+
import { agentModel, attachTools, costOf, gatewayTools, textModel, totalUsage, } from "../models.js";
|
|
11
11
|
import { pluginToolsOf } from "../plugins/registry.js";
|
|
12
12
|
import { htmlToMarkdown } from "../render/convert.js";
|
|
13
|
-
import { ServiceError } from "../services/errors.js";
|
|
14
13
|
import { deleteFile, listFiles, readFileText, writeFile } from "../services/files.js";
|
|
15
14
|
import { checkDraftWidget } from "../services/widgets.js";
|
|
16
15
|
import { editWizard, ownedWizard, writeDraft } from "../services/wizards.js";
|
|
17
16
|
import { currentTenant } from "../tenants/tenant.js";
|
|
18
17
|
import { serverText } from "../texts.js";
|
|
19
18
|
import { assertPublicUrl, safeFetch } from "../tools/net-guard.js";
|
|
19
|
+
import { attempt as attemptTool, imageOutput } from "../tools/shared.js";
|
|
20
20
|
import { runScript } from "../widgets/script.js";
|
|
21
21
|
import { thoughtLine } from "./thought.js";
|
|
22
22
|
const MAX_STEPS = 45;
|
|
@@ -93,18 +93,8 @@ function activity(tool, args, lang = "en") {
|
|
|
93
93
|
return null;
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
-
/** A tool failure the model can read and fix, instead of a crashed turn. */
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
return await run();
|
|
100
|
-
}
|
|
101
|
-
catch (err) {
|
|
102
|
-
if (err instanceof ServiceError) {
|
|
103
|
-
return { error: err.message, ...err.data };
|
|
104
|
-
}
|
|
105
|
-
return { error: err.message };
|
|
106
|
-
}
|
|
107
|
-
}
|
|
96
|
+
/** A tool failure the model can read and fix, instead of a crashed turn: the whole message. */
|
|
97
|
+
const attempt = (run) => attemptTool(run, { limit: Number.POSITIVE_INFINITY });
|
|
108
98
|
function buildTools(input, resolved, wrote) {
|
|
109
99
|
const { userId, wizardId } = input;
|
|
110
100
|
const signal = input.signal;
|
|
@@ -314,19 +304,7 @@ function buildTools(input, resolved, wrote) {
|
|
|
314
304
|
screenshot: Buffer.from(r.png).toString("base64"),
|
|
315
305
|
};
|
|
316
306
|
}),
|
|
317
|
-
toModelOutput: (
|
|
318
|
-
if (!out || out.error || !out.screenshot) {
|
|
319
|
-
return { type: "json", value: out };
|
|
320
|
-
}
|
|
321
|
-
const { screenshot, ...rest } = out;
|
|
322
|
-
return {
|
|
323
|
-
type: "content",
|
|
324
|
-
value: [
|
|
325
|
-
{ type: "text", text: JSON.stringify(rest) },
|
|
326
|
-
{ type: "image-data", data: screenshot, mediaType: "image/png" },
|
|
327
|
-
],
|
|
328
|
-
};
|
|
329
|
-
},
|
|
307
|
+
toModelOutput: imageOutput("screenshot"),
|
|
330
308
|
}),
|
|
331
309
|
};
|
|
332
310
|
if (resolved.vendor === "anthropic") {
|
|
@@ -357,7 +335,7 @@ export async function runArchitect(input) {
|
|
|
357
335
|
content: systemPrompt(input.mcpServers, input.connectors, await pluginToolsOf(currentTenant())),
|
|
358
336
|
providerOptions: { anthropic: { cacheControl: { type: "ephemeral" } } },
|
|
359
337
|
},
|
|
360
|
-
model: resolved
|
|
338
|
+
model: agentModel(resolved),
|
|
361
339
|
tools,
|
|
362
340
|
});
|
|
363
341
|
const context = [
|
|
@@ -456,7 +434,7 @@ export async function runArchitect(input) {
|
|
|
456
434
|
throw payload.error ?? new Error("architect stream error");
|
|
457
435
|
}
|
|
458
436
|
}
|
|
459
|
-
const usage =
|
|
437
|
+
const usage = await totalUsage(stream);
|
|
460
438
|
return {
|
|
461
439
|
reply: reply.trim(),
|
|
462
440
|
changed,
|
|
@@ -7,8 +7,7 @@ import { z } from "zod";
|
|
|
7
7
|
import { knowsTools } from "../assistant/knows.js";
|
|
8
8
|
import { PRIMER } from "../assistant/primer.js";
|
|
9
9
|
import { currentFeatures } from "../features.js";
|
|
10
|
-
import { attachTools, contextWindowOf, costOf, gatewayTools, textModel, usageOf, } from "../models.js";
|
|
11
|
-
import { ServiceError } from "../services/errors.js";
|
|
10
|
+
import { agentModel, attachTools, contextWindowOf, costOf, gatewayTools, textModel, totalUsage, usageOf, } from "../models.js";
|
|
12
11
|
import { knowledgeCounts } from "../services/knowledge.js";
|
|
13
12
|
import { addProjectFileFromUrl, projectFileContent, projectFiles, updateProjectFile, } from "../services/project-files.js";
|
|
14
13
|
import { documentText } from "../services/project-index.js";
|
|
@@ -17,6 +16,7 @@ import { categoryRows } from "../services/space-categories.js";
|
|
|
17
16
|
import { currentTenant } from "../tenants/tenant.js";
|
|
18
17
|
import { serverText } from "../texts.js";
|
|
19
18
|
import { assistantPageOf, assistantToolsOf } from "../tools/plugin.js";
|
|
19
|
+
import { attempt as attemptTool, imageOutput } from "../tools/shared.js";
|
|
20
20
|
import { readWebsite } from "./website.js";
|
|
21
21
|
/** How a message written meanwhile reaches the model. */
|
|
22
22
|
export function signalText(taken) {
|
|
@@ -97,14 +97,8 @@ function activity(tool, args, lang) {
|
|
|
97
97
|
: tool;
|
|
98
98
|
return LABELS[key] ? serverText(lang, LABELS[key]) : null;
|
|
99
99
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return await run();
|
|
103
|
-
}
|
|
104
|
-
catch (err) {
|
|
105
|
-
return { error: err instanceof ServiceError ? err.message : err.message };
|
|
106
|
-
}
|
|
107
|
-
}
|
|
100
|
+
/** A tool failure the model can read and fix, instead of a crashed turn: the whole message. */
|
|
101
|
+
const attempt = (run) => attemptTool(run, { limit: Number.POSITIVE_INFINITY });
|
|
108
102
|
async function projectState(userId, projectId) {
|
|
109
103
|
const p = await ownedProject(userId, projectId);
|
|
110
104
|
const files = await projectFiles(projectId);
|
|
@@ -587,18 +581,7 @@ function spaceTools(input, project, wrote, on) {
|
|
|
587
581
|
}
|
|
588
582
|
return { name: row.name, image: data.toString("base64"), mime: row.mime };
|
|
589
583
|
}),
|
|
590
|
-
toModelOutput: (
|
|
591
|
-
if (!out?.image) {
|
|
592
|
-
return { type: "json", value: out };
|
|
593
|
-
}
|
|
594
|
-
return {
|
|
595
|
-
type: "content",
|
|
596
|
-
value: [
|
|
597
|
-
{ type: "text", text: out.name },
|
|
598
|
-
{ type: "image-data", data: out.image, mediaType: out.mime },
|
|
599
|
-
],
|
|
600
|
-
};
|
|
601
|
-
},
|
|
584
|
+
toModelOutput: imageOutput("image", (rest) => String(rest.name)),
|
|
602
585
|
}),
|
|
603
586
|
};
|
|
604
587
|
if (!on.brandSections) {
|
|
@@ -765,7 +748,7 @@ export async function runStudioAssistant(input) {
|
|
|
765
748
|
content: SYSTEM,
|
|
766
749
|
providerOptions: { anthropic: { cacheControl: { type: "ephemeral" } } },
|
|
767
750
|
},
|
|
768
|
-
model: resolved
|
|
751
|
+
model: agentModel(resolved),
|
|
769
752
|
tools,
|
|
770
753
|
});
|
|
771
754
|
const page = input.page;
|
|
@@ -895,6 +878,6 @@ export async function runStudioAssistant(input) {
|
|
|
895
878
|
throw payload.error ?? new Error("assistant stream error");
|
|
896
879
|
}
|
|
897
880
|
}
|
|
898
|
-
const usage =
|
|
881
|
+
const usage = await totalUsage(stream);
|
|
899
882
|
return { ...cut(), costUsd: costOf(resolved, usage) };
|
|
900
883
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
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";
|
|
@@ -12,7 +12,7 @@ const run = promisify(execFile);
|
|
|
12
12
|
* LaunchAgent, on Linux from a systemd user service. Only for an install made by install.sh,
|
|
13
13
|
* whose command stays where it is.
|
|
14
14
|
*/
|
|
15
|
-
const LOGIN_LABEL = "com.engenty.wizards.login";
|
|
15
|
+
export const LOGIN_LABEL = "com.engenty.wizards.login";
|
|
16
16
|
/** The LaunchAgent. */
|
|
17
17
|
export const LOGIN_ITEM = join(homedir(), "Library", "LaunchAgents", `${LOGIN_LABEL}.plist`);
|
|
18
18
|
const xml = (text) => text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
@@ -45,7 +45,7 @@ ${program.map((arg) => ` <string>${xml(arg)}</string>\n`).join("")} </array>
|
|
|
45
45
|
</plist>
|
|
46
46
|
`;
|
|
47
47
|
}
|
|
48
|
-
const UNIT = "engenty-wizards.service";
|
|
48
|
+
export const UNIT = "engenty-wizards.service";
|
|
49
49
|
export function unitFile(env = process.env) {
|
|
50
50
|
return join(env.XDG_CONFIG_HOME || join(homedir(), ".config"), "systemd", "user", UNIT);
|
|
51
51
|
}
|
|
@@ -80,27 +80,71 @@ async function systemdHere() {
|
|
|
80
80
|
return false;
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Whether a file is there. Unlike existsSync, a folder it may not look into (a sandbox, its
|
|
85
|
+
* permissions) is an error, not "no file": `autostart off` must not say it's off then.
|
|
86
|
+
*/
|
|
87
|
+
export function present(file) {
|
|
88
|
+
try {
|
|
89
|
+
lstatSync(file);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (error.code === "ENOENT") {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
throw new Error(`Couldn’t check ${file}: ${error.message}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Whether it starts at login, read from the login item itself: a LaunchAgent or unit file that
|
|
101
|
+
* is there counts, whichever copy of the command wrote it. Only an install made by install.sh can
|
|
102
|
+
* write one.
|
|
103
|
+
*/
|
|
83
104
|
export async function autostartState(paths) {
|
|
84
105
|
if (process.platform !== "darwin" && process.platform !== "linux") {
|
|
85
106
|
return {
|
|
86
107
|
unavailable: process.platform === "win32" ? "not on Windows" : `not on ${process.platform}`,
|
|
87
108
|
};
|
|
88
109
|
}
|
|
89
|
-
|
|
110
|
+
const file = process.platform === "darwin" ? LOGIN_ITEM : unitFile();
|
|
111
|
+
const there = present(file);
|
|
112
|
+
if (!there && !installedByScript(paths)) {
|
|
90
113
|
return { unavailable: "only for installs made with the install script" };
|
|
91
114
|
}
|
|
92
115
|
if (process.platform === "darwin") {
|
|
93
|
-
return { on:
|
|
116
|
+
return { on: there, how: MAC_HOW };
|
|
94
117
|
}
|
|
95
118
|
if (!(await systemdHere())) {
|
|
96
|
-
return
|
|
119
|
+
return there
|
|
120
|
+
? { on: true, how: LINUX_HOW }
|
|
121
|
+
: { unavailable: "this system has no systemd user session" };
|
|
97
122
|
}
|
|
98
123
|
try {
|
|
99
124
|
await run("systemctl", ["--user", "is-enabled", "--quiet", UNIT], { timeout: 10_000 });
|
|
100
125
|
return { on: true, how: LINUX_HOW };
|
|
101
126
|
}
|
|
102
127
|
catch {
|
|
103
|
-
|
|
128
|
+
// A unit file that is there but not enabled still counts: `off` takes it away.
|
|
129
|
+
return { on: there, how: LINUX_HOW };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** The LaunchAgent unloaded from this login session, if launchd has it; errors are ignored. */
|
|
133
|
+
export async function unloadLoginItem() {
|
|
134
|
+
const uid = process.getuid?.();
|
|
135
|
+
if (process.platform !== "darwin" || uid === undefined) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
await run("launchctl", ["bootout", `gui/${uid}/${LOGIN_LABEL}`], { timeout: 15_000 }).catch(() => undefined);
|
|
139
|
+
}
|
|
140
|
+
/** The systemd user service disabled, stopped and its file gone. */
|
|
141
|
+
export async function removeUnit(file = unitFile()) {
|
|
142
|
+
if (findOnPath("systemctl", process.env.PATH ?? "")) {
|
|
143
|
+
await run("systemctl", ["--user", "disable", "--now", UNIT]).catch(() => undefined);
|
|
144
|
+
}
|
|
145
|
+
rmSync(file, { force: true });
|
|
146
|
+
if (findOnPath("systemctl", process.env.PATH ?? "")) {
|
|
147
|
+
await run("systemctl", ["--user", "daemon-reload"]).catch(() => undefined);
|
|
104
148
|
}
|
|
105
149
|
}
|
|
106
150
|
/** Switches it on or off; what it is afterwards, or an error the person can read. */
|
|
@@ -110,7 +154,7 @@ export async function setAutostart(paths, on) {
|
|
|
110
154
|
return state;
|
|
111
155
|
}
|
|
112
156
|
if (process.platform === "darwin") {
|
|
113
|
-
// Takes effect at the next login; nothing is started now.
|
|
157
|
+
// Takes effect at the next login; nothing is started or stopped now.
|
|
114
158
|
if (on) {
|
|
115
159
|
mkdirSync(dirname(LOGIN_ITEM), { recursive: true });
|
|
116
160
|
writeFileSync(LOGIN_ITEM, loginItemText(join(paths.home, "bin", "wizards"), join(paths.home, "logs", "autostart.log")));
|
|
@@ -118,7 +162,7 @@ export async function setAutostart(paths, on) {
|
|
|
118
162
|
else {
|
|
119
163
|
rmSync(LOGIN_ITEM, { force: true });
|
|
120
164
|
}
|
|
121
|
-
return { on:
|
|
165
|
+
return { on: present(LOGIN_ITEM), how: MAC_HOW };
|
|
122
166
|
}
|
|
123
167
|
const file = unitFile();
|
|
124
168
|
if (on) {
|
|
@@ -128,9 +172,11 @@ export async function setAutostart(paths, on) {
|
|
|
128
172
|
await run("systemctl", ["--user", "enable", "--now", UNIT]);
|
|
129
173
|
}
|
|
130
174
|
else {
|
|
131
|
-
await
|
|
132
|
-
rmSync(file, { force: true });
|
|
133
|
-
await run("systemctl", ["--user", "daemon-reload"]).catch(() => undefined);
|
|
175
|
+
await removeUnit(file);
|
|
134
176
|
}
|
|
135
177
|
return autostartState(paths);
|
|
136
178
|
}
|
|
179
|
+
/** The state for a status line: a login item that can't be checked reads as unavailable. */
|
|
180
|
+
export function autostartForStatus(paths) {
|
|
181
|
+
return autostartState(paths).catch((error) => ({ unavailable: error.message }));
|
|
182
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdirSync, mkdtempSync, openSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { readEnvFile, readInstallNote } from "./home.js";
|
|
5
|
+
import { currentVersion, promote, prune, runtimeDir, switchTo, versionIn } from "./versions.js";
|
|
6
|
+
/**
|
|
7
|
+
* Updates that come by themselves, for an install made by install.sh (never `npx wizards` or a
|
|
8
|
+
* checkout). The runtime asks the npm registry for the newest `wizards` when it starts and once
|
|
9
|
+
* a day; npm installs a newer one, checking its integrity, beside the one that runs
|
|
10
|
+
* (versions.ts). Nothing switches then: runs cost credits and are never cut off. The next
|
|
11
|
+
* `wizards start` (a restart, the next login) makes it `current` before any runtime runs.
|
|
12
|
+
*
|
|
13
|
+
* Off with `wizards autoupdate off` or WIZARDS_DISABLE_AUTOUPDATE=1 (terminal or the install's
|
|
14
|
+
* .env); off by default in CI and for an install set up without a terminal.
|
|
15
|
+
*/
|
|
16
|
+
const LATEST_URL = "https://registry.npmjs.org/wizards/latest";
|
|
17
|
+
const HOUR = 60 * 60_000;
|
|
18
|
+
/** How long the status and the studio keep saying what came with an update. */
|
|
19
|
+
const NOTICE_MS = 14 * 24 * HOUR;
|
|
20
|
+
export const notesUrl = (version) => `https://github.com/engenty/wizards/releases/tag/v${version}`;
|
|
21
|
+
const stateFile = (paths) => join(paths.home, "update.json");
|
|
22
|
+
export function readUpdateState(paths) {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(readFileSync(stateFile(paths), "utf8"));
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function writeUpdateState(paths, state) {
|
|
31
|
+
mkdirSync(paths.home, { recursive: true });
|
|
32
|
+
const tmp = `${stateFile(paths)}.${process.pid}.tmp`;
|
|
33
|
+
writeFileSync(tmp, `${JSON.stringify(state, null, 2)}\n`);
|
|
34
|
+
renameSync(tmp, stateFile(paths));
|
|
35
|
+
}
|
|
36
|
+
/** Whether `a` is a newer version than `b`; a `v` and a pre-release suffix are ignored. */
|
|
37
|
+
export function isNewer(a, b) {
|
|
38
|
+
const parts = (v) => v.replace(/^v/, "").split("-")[0].split(".").map(Number);
|
|
39
|
+
const [x, y] = [parts(a), parts(b)];
|
|
40
|
+
for (let i = 0; i < Math.max(x.length, y.length); i++) {
|
|
41
|
+
if ((x[i] || 0) !== (y[i] || 0)) {
|
|
42
|
+
return (x[i] || 0) > (y[i] || 0);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
// --- whether it is on ----------------------------------------------------------------------
|
|
48
|
+
const truthy = (value) => value !== undefined && !["", "0", "false", "no", "off"].includes(value.trim().toLowerCase());
|
|
49
|
+
/** A build machine. The runtime gets ENGENTY_WIZARDS_CI from the command, which sees CI. */
|
|
50
|
+
export const isCI = (env) => truthy(env.CI) || truthy(env.ENGENTY_WIZARDS_CI);
|
|
51
|
+
/**
|
|
52
|
+
* Whether this install updates itself. The variable switches it off whatever else says;
|
|
53
|
+
* `wizards autoupdate on|off` decides next; without either it is on, except in CI and for an
|
|
54
|
+
* install set up without a terminal.
|
|
55
|
+
*/
|
|
56
|
+
export function autoUpdatePolicy(paths, options) {
|
|
57
|
+
const env = options.env ?? process.env;
|
|
58
|
+
if (!options.installed) {
|
|
59
|
+
return { on: false, reason: "only an install made by install.sh updates itself" };
|
|
60
|
+
}
|
|
61
|
+
if (truthy(env.WIZARDS_DISABLE_AUTOUPDATE ?? readEnvFile(paths.envFile).WIZARDS_DISABLE_AUTOUPDATE)) {
|
|
62
|
+
return { on: false, reason: "WIZARDS_DISABLE_AUTOUPDATE is set" };
|
|
63
|
+
}
|
|
64
|
+
const { autoUpdate, unattended } = options.note ?? readInstallNote(paths);
|
|
65
|
+
if (autoUpdate !== undefined) {
|
|
66
|
+
return { on: autoUpdate, reason: autoUpdate ? "turned on" : "turned off" };
|
|
67
|
+
}
|
|
68
|
+
if (isCI(env)) {
|
|
69
|
+
return { on: false, reason: "off in CI" };
|
|
70
|
+
}
|
|
71
|
+
if (unattended) {
|
|
72
|
+
return { on: false, reason: "off: set up without a terminal" };
|
|
73
|
+
}
|
|
74
|
+
return { on: true, reason: "on" };
|
|
75
|
+
}
|
|
76
|
+
export const systemDeps = {
|
|
77
|
+
fetch: (...args) => fetch(...args),
|
|
78
|
+
npmInstall,
|
|
79
|
+
now: () => new Date(),
|
|
80
|
+
};
|
|
81
|
+
/** The version npm's `latest` tag names; null when it names none. */
|
|
82
|
+
async function latestVersion(deps, env) {
|
|
83
|
+
const response = await deps.fetch(env.ENGENTY_WIZARDS_REGISTRY_URL?.trim() || LATEST_URL, {
|
|
84
|
+
headers: { accept: "application/json" },
|
|
85
|
+
signal: AbortSignal.timeout(10_000),
|
|
86
|
+
});
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
throw new Error(`The npm registry answered ${response.status} for the newest version.`);
|
|
89
|
+
}
|
|
90
|
+
const { version } = (await response.json());
|
|
91
|
+
return typeof version === "string" && /^\d+\.\d+\.\d+$/.test(version) ? version : null;
|
|
92
|
+
}
|
|
93
|
+
/** npm of the install's own Node, as install.sh runs it; its output goes to logs/update.log. */
|
|
94
|
+
async function npmInstall(paths, prefix, spec) {
|
|
95
|
+
const bin = join(paths.tools, "node", "bin");
|
|
96
|
+
mkdirSync(paths.logs, { recursive: true });
|
|
97
|
+
const log = openSync(join(paths.logs, "update.log"), "a");
|
|
98
|
+
const args = "install --omit=dev --ignore-scripts --legacy-peer-deps --no-audit --no-fund --loglevel=error";
|
|
99
|
+
const code = await new Promise((done) => {
|
|
100
|
+
const child = spawn(join(bin, "npm"), [...args.split(" "), "--prefix", prefix, spec], {
|
|
101
|
+
stdio: ["ignore", log, log],
|
|
102
|
+
env: { ...process.env, PATH: `${bin}:${process.env.PATH ?? ""}` },
|
|
103
|
+
});
|
|
104
|
+
child.on("error", () => done(127));
|
|
105
|
+
child.on("exit", (exit) => done(exit ?? 1));
|
|
106
|
+
});
|
|
107
|
+
if (code !== 0) {
|
|
108
|
+
throw new Error(`npm install of the new version failed (exit ${code}); see logs/update.log.`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Asks npm for the newest version and, when it is newer than the current one, installs it into
|
|
113
|
+
* its version folder (npm checks the package against the registry's integrity hash). It switches nothing. `force` asks even
|
|
114
|
+
* within the hour since the last time.
|
|
115
|
+
*/
|
|
116
|
+
export async function checkForUpdate(paths, options, deps = systemDeps) {
|
|
117
|
+
if (!options.policy.on) {
|
|
118
|
+
return { kind: "off", reason: options.policy.reason };
|
|
119
|
+
}
|
|
120
|
+
const state = readUpdateState(paths);
|
|
121
|
+
const now = deps.now();
|
|
122
|
+
const current = currentVersion(paths);
|
|
123
|
+
if (!current) {
|
|
124
|
+
return { kind: "skipped", reason: "no runtime installed" };
|
|
125
|
+
}
|
|
126
|
+
if (!options.force && now.getTime() - Date.parse(state.checkedAt ?? "0") < HOUR) {
|
|
127
|
+
return { kind: "skipped", reason: "asked within the hour" };
|
|
128
|
+
}
|
|
129
|
+
state.checkedAt = now.toISOString();
|
|
130
|
+
delete state.error;
|
|
131
|
+
const tmp = mkdtempSync(join(paths.home, "update-"));
|
|
132
|
+
try {
|
|
133
|
+
const version = await latestVersion(deps, options.env ?? process.env);
|
|
134
|
+
if (!version || !isNewer(version, current)) {
|
|
135
|
+
return { kind: "current", version: current };
|
|
136
|
+
}
|
|
137
|
+
if (state.pending?.version !== version || !versionIn(runtimeDir(paths, version))) {
|
|
138
|
+
await deps.npmInstall(paths, tmp, `wizards@${version}`);
|
|
139
|
+
promote(paths, tmp, version);
|
|
140
|
+
state.pending = { version, notes: notesUrl(version) };
|
|
141
|
+
prune(paths, keepOf(state));
|
|
142
|
+
}
|
|
143
|
+
return { kind: "downloaded", version };
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
state.error = error.message;
|
|
147
|
+
return { kind: "failed", reason: state.error };
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
writeUpdateState(paths, state);
|
|
151
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/** The versions kept besides the current one: the one waiting and the one before. */
|
|
155
|
+
const keepOf = (state) => [state.pending?.version, state.updated?.from].filter((v) => Boolean(v));
|
|
156
|
+
// --- switching -------------------------------------------------------------------------------
|
|
157
|
+
/**
|
|
158
|
+
* Makes a downloaded version `current`. Only called where no runtime runs (`wizards start`
|
|
159
|
+
* before it starts one), so no run is cut off; a version that is no longer newer is dropped.
|
|
160
|
+
*/
|
|
161
|
+
export function applyPendingUpdate(paths, now = new Date()) {
|
|
162
|
+
const state = readUpdateState(paths);
|
|
163
|
+
const { pending } = state;
|
|
164
|
+
const from = currentVersion(paths);
|
|
165
|
+
if (!pending || !from) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
delete state.pending;
|
|
169
|
+
try {
|
|
170
|
+
if (!isNewer(pending.version, from)) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
switchTo(paths, pending.version);
|
|
174
|
+
state.updated = { from, to: pending.version, at: now.toISOString(), notes: pending.notes };
|
|
175
|
+
prune(paths, [from]);
|
|
176
|
+
return { from, to: pending.version };
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
state.error = error.message;
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
writeUpdateState(paths, state);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/** Notes a switch somebody made by hand (`wizards update`), so the notice says it too. */
|
|
187
|
+
export function noteUpdated(paths, from, to, now = new Date()) {
|
|
188
|
+
if (from !== to) {
|
|
189
|
+
const state = readUpdateState(paths);
|
|
190
|
+
delete state.pending;
|
|
191
|
+
state.updated = { from, to, at: now.toISOString(), notes: notesUrl(to) };
|
|
192
|
+
writeUpdateState(paths, state);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/** What the status and the studio say: a fresh update, or one waiting for the next start. */
|
|
196
|
+
export function updateNotice(paths, running, now = new Date()) {
|
|
197
|
+
const { updated, pending } = readUpdateState(paths);
|
|
198
|
+
return {
|
|
199
|
+
updated: updated?.to === running && now.getTime() - Date.parse(updated.at) < NOTICE_MS
|
|
200
|
+
? updated
|
|
201
|
+
: null,
|
|
202
|
+
pending: pending && isNewer(pending.version, running) ? pending : null,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/** The runtime's part: a check soon after it starts, then once a day. */
|
|
206
|
+
export function scheduleUpdateChecks(paths, policy, deps = systemDeps) {
|
|
207
|
+
if (!policy().on) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const check = async (force) => {
|
|
211
|
+
const result = await checkForUpdate(paths, { policy: policy(), force }, deps);
|
|
212
|
+
if (result.kind === "downloaded") {
|
|
213
|
+
console.log(`[update] ${result.version} is ready; it runs from the next start.`);
|
|
214
|
+
}
|
|
215
|
+
else if (result.kind === "failed") {
|
|
216
|
+
console.error(`[update] ${result.reason}`);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
setTimeout(() => void check(false), 30_000).unref();
|
|
220
|
+
setInterval(() => void check(true), 24 * HOUR).unref();
|
|
221
|
+
}
|
|
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { delimiter, dirname, join } from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import { installedByScript, packageRoot } from "./home.js";
|
|
7
|
-
import { findOnPath } from "./machine.js";
|
|
7
|
+
import { findOnPath, macAppPresent } from "./machine.js";
|
|
8
8
|
import { bad, cmd, dim, ok, tilde } from "./ui.js";
|
|
9
9
|
const run = promisify(execFile);
|
|
10
10
|
/**
|
|
@@ -225,7 +225,8 @@ function jsonApp(def) {
|
|
|
225
225
|
name: def.name,
|
|
226
226
|
widgets: def.widgets,
|
|
227
227
|
where: def.file,
|
|
228
|
-
installed: () => existsSync(def.marker)
|
|
228
|
+
installed: () => existsSync(def.marker) &&
|
|
229
|
+
(process.platform !== "darwin" || !def.macApp || macAppPresent(def.macApp)),
|
|
229
230
|
connected: async () => jsonHasServer(read(def.file) ?? "", def.at),
|
|
230
231
|
async connect(server) {
|
|
231
232
|
const busy = running();
|
|
@@ -423,6 +424,7 @@ export function apps(paths) {
|
|
|
423
424
|
widgets: true,
|
|
424
425
|
file: join(appSupport("Claude"), "claude_desktop_config.json"),
|
|
425
426
|
marker: appSupport("Claude"),
|
|
427
|
+
macApp: "Claude",
|
|
426
428
|
at: ["mcpServers"],
|
|
427
429
|
entry: stdioEntry,
|
|
428
430
|
ownsFileWhileRunning: () => processRunning(CLAUDE_DESKTOP_PROCESS),
|
|
@@ -444,6 +446,7 @@ export function apps(paths) {
|
|
|
444
446
|
widgets: true,
|
|
445
447
|
file: join(vscodeUser, "mcp.json"),
|
|
446
448
|
marker: vscodeUser,
|
|
449
|
+
macApp: "Visual Studio Code",
|
|
447
450
|
at: ["servers"],
|
|
448
451
|
entry: (server) => ({ type: "stdio", ...stdioEntry(server) }),
|
|
449
452
|
}),
|
|
@@ -453,6 +456,7 @@ export function apps(paths) {
|
|
|
453
456
|
widgets: false,
|
|
454
457
|
file: join(home, ".codeium", "windsurf", "mcp_config.json"),
|
|
455
458
|
marker: join(home, ".codeium", "windsurf"),
|
|
459
|
+
macApp: "Windsurf",
|
|
456
460
|
at: ["mcpServers"],
|
|
457
461
|
entry: stdioEntry,
|
|
458
462
|
}),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { basename, delimiter, dirname, join } from "node:path";
|
|
2
|
+
import { isCI } from "./autoupdate.js";
|
|
2
3
|
/**
|
|
3
4
|
* Variables of the terminal the runtime inherits. Everything else it gets comes from the
|
|
4
5
|
* install's `.env` or is set by the command line. A key or a database address that the shell
|
|
@@ -40,6 +41,8 @@ const INHERITED = new Set([
|
|
|
40
41
|
"ACCOUNT_URL",
|
|
41
42
|
"ACCOUNT_GATEWAY_URL",
|
|
42
43
|
"CLOUD_URL",
|
|
44
|
+
// Off for this start (autoupdate.ts).
|
|
45
|
+
"WIZARDS_DISABLE_AUTOUPDATE",
|
|
43
46
|
]);
|
|
44
47
|
export function inherited(env) {
|
|
45
48
|
const out = {};
|
|
@@ -82,6 +85,8 @@ export function runtimeEnv(paths, start, env = process.env, fileEnv = {}, platfo
|
|
|
82
85
|
LOCAL_ACCESS_KEY: start.accessKey,
|
|
83
86
|
// The install the runtime belongs to: the studio connects AI apps to its command.
|
|
84
87
|
...(basename(paths.home) === "wizards" ? { ENGENTY_HOME: dirname(paths.home) } : {}),
|
|
88
|
+
// The runtime sees no CI variables; it is told so, and checks for no updates (autoupdate.ts).
|
|
89
|
+
...(isCI(env) ? { ENGENTY_WIZARDS_CI: "1" } : {}),
|
|
85
90
|
// Keys entered in the settings live in the Keychain, as with the desktop app.
|
|
86
91
|
...(platform === "darwin" && !secretsGiven ? { SECRETS: "keychain" } : {}),
|
|
87
92
|
};
|
|
@@ -42,13 +42,17 @@ export const CLIENTS = [
|
|
|
42
42
|
app: "Cursor",
|
|
43
43
|
},
|
|
44
44
|
];
|
|
45
|
+
const APP_ROOTS = ["/Applications", join(homedir(), "Applications")];
|
|
46
|
+
/** Whether `<name>.app` is in one of the Mac's Applications folders. */
|
|
47
|
+
export function macAppPresent(name, roots = APP_ROOTS) {
|
|
48
|
+
return roots.some((root) => existsSync(join(root, `${name}.app`)));
|
|
49
|
+
}
|
|
45
50
|
/** The client's desktop app, when it is on this Mac: its name, e.g. "Claude". */
|
|
46
|
-
export function vendorApp(client, roots =
|
|
51
|
+
export function vendorApp(client, roots = APP_ROOTS) {
|
|
47
52
|
if (process.platform !== "darwin" || !client.app) {
|
|
48
53
|
return null;
|
|
49
54
|
}
|
|
50
|
-
|
|
51
|
-
return roots.some((root) => existsSync(join(root, `${app}.app`))) ? app : null;
|
|
55
|
+
return macAppPresent(client.app, roots) ? client.app : null;
|
|
52
56
|
}
|
|
53
57
|
/**
|
|
54
58
|
* The client the setup suggests among those not installed: the one whose app is on this Mac —
|
|
@@ -12,7 +12,7 @@ import { setup } from "./setup.js";
|
|
|
12
12
|
import { open, start, studio } from "./start.js";
|
|
13
13
|
import { autostart, restart, status, stop } from "./status.js";
|
|
14
14
|
import { cmd, cyan, tilde } from "./ui.js";
|
|
15
|
-
import { update } from "./update.js";
|
|
15
|
+
import { autoUpdateCommand, update } from "./update.js";
|
|
16
16
|
import { validateCommand } from "./validate.js";
|
|
17
17
|
/** One line of the help: the name padded, then coloured, so the columns stay straight. */
|
|
18
18
|
const line = (name, what) => ` ${cyan(name.padEnd(command().length + 12))}${what}`;
|
|
@@ -38,7 +38,9 @@ ${line(`${command()} report`, "send the status, model settings and last log line
|
|
|
38
38
|
${line(`${command()} stop`, "stop it (your data is kept)")}
|
|
39
39
|
${line(`${command()} restart`, "restart it, e.g. to switch to a newly installed version")}
|
|
40
40
|
${line(`${command()} update`, "install the latest version")}
|
|
41
|
-
${line(`${command()}
|
|
41
|
+
${line(`${command()} autoupdate`, "updates by themselves: add on or off (WIZARDS_DISABLE_AUTOUPDATE=1 is off too)")}
|
|
42
|
+
${more("(downloaded in the background, used from the next start; never during a run)")}
|
|
43
|
+
${line(`${command()} autostart`, "start it when you log in to this computer: add on or off")}
|
|
42
44
|
${more("(macOS: a LaunchAgent; Linux: a systemd user service)")}
|
|
43
45
|
${line(`${command()} connect`, "add your wizards to your AI apps: every app found, or the ones you name:")}
|
|
44
46
|
${more(apps(layout())
|
|
@@ -46,6 +48,8 @@ ${more(apps(layout())
|
|
|
46
48
|
.join(", "))}
|
|
47
49
|
${line(`${command()} disconnect`, "remove your wizards from those apps (same names)")}
|
|
48
50
|
${line(`${command()} mcp`, "run the MCP server over stdio (AI apps start this)")}
|
|
51
|
+
${line(`${command()} uninstall`, "take it off this computer: shows the plan, asks about your data (kept by default)")}
|
|
52
|
+
${more("--dry-run only shows it; --yes asks nothing and keeps your data; --yes --delete-data")}
|
|
49
53
|
${line(`${command()} validate`, "check wizards without the studio: .wizard files, unpacked packages, wizard JSON")}
|
|
50
54
|
${more("--json prints the issues as JSON. A space’s connectors and plugins are checked on import.")}
|
|
51
55
|
|
|
@@ -112,7 +116,8 @@ function parse(argv) {
|
|
|
112
116
|
else if (arg.startsWith("-")) {
|
|
113
117
|
return `Unknown option ${arg}.`;
|
|
114
118
|
}
|
|
115
|
-
else if (["autostart", "logs", "account"].includes(args.command) &&
|
|
119
|
+
else if (["autostart", "autoupdate", "logs", "account"].includes(args.command) &&
|
|
120
|
+
!args.value) {
|
|
116
121
|
args.value = arg;
|
|
117
122
|
}
|
|
118
123
|
else if (["connect", "disconnect", "features"].includes(args.command)) {
|
|
@@ -190,6 +195,8 @@ async function main(argv) {
|
|
|
190
195
|
return update(paths);
|
|
191
196
|
case "autostart":
|
|
192
197
|
return autostart(paths, args.value);
|
|
198
|
+
case "autoupdate":
|
|
199
|
+
return autoUpdateCommand(paths, args.value);
|
|
193
200
|
case "account":
|
|
194
201
|
return accountCommand(paths, args.value, { yes: args.yes });
|
|
195
202
|
case "connect":
|