wizards 0.3.7 → 0.4.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 +28 -14
- package/apps/runtime/dist/agents/studio-assistant.js +79 -7
- package/apps/runtime/dist/assistant/runs.js +24 -0
- package/apps/runtime/dist/auth/account.js +36 -4
- package/apps/runtime/dist/auth/device.js +101 -0
- package/apps/runtime/dist/auth/index.js +4 -4
- package/apps/runtime/dist/cli/account.js +289 -0
- package/apps/runtime/dist/cli/install-env.js +33 -0
- package/apps/runtime/dist/cli/main.js +17 -3
- package/apps/runtime/dist/cli/menu.js +19 -5
- package/apps/runtime/dist/cli/setup.js +9 -16
- package/apps/runtime/dist/cli/status.js +4 -1
- package/apps/runtime/dist/cli/ui.js +14 -0
- package/apps/runtime/dist/docs/user/assistant.md +25 -7
- package/apps/runtime/dist/docs/user/command-line.md +3 -2
- package/apps/runtime/dist/docs/user/create-account.md +47 -0
- package/apps/runtime/dist/docs/user/first-start.md +9 -1
- package/apps/runtime/dist/docs/user/index.md +16 -12
- package/apps/runtime/dist/docs/user/install.md +3 -1
- package/apps/runtime/dist/docs/user/local-only.md +38 -0
- package/apps/runtime/dist/docs/user/meta.json +3 -1
- package/apps/runtime/dist/docs/user/models.md +3 -2
- package/apps/runtime/dist/env.js +5 -0
- package/apps/runtime/dist/manage.js +16 -4
- package/apps/runtime/dist/models.js +6 -0
- package/apps/runtime/dist/plugins/pro.js +16 -16
- package/apps/runtime/dist/routes/studio.js +140 -40
- package/apps/runtime/dist/services/assistant-chats.js +49 -6
- package/apps/runtime/dist/services/cloud.js +10 -10
- package/apps/runtime/dist/services/onboarding.js +152 -0
- package/apps/web/dist/assets/{AboutDialog-C0dN86zd.js → AboutDialog-CPQiN7H9.js} +1 -1
- package/apps/web/dist/assets/{ChangelogDialog-Cv81t7tj.js → ChangelogDialog-DynvQuuR.js} +1 -1
- package/apps/web/dist/assets/ChatView-CKo_9IZD.js +10 -0
- package/apps/web/dist/assets/EditorPage-CDIJXC4T.js +3 -0
- package/apps/web/dist/assets/{FlowPreview-D4dp1MWS.js → FlowPreview-BZLnG7GV.js} +1 -1
- package/apps/web/dist/assets/{Harness-v7r0e6vK.js → Harness-CNh3nbB_.js} +2 -2
- package/apps/web/dist/assets/{NewWizardChannels-Be-7VhBi.js → NewWizardChannels-C1Ss9UMY.js} +1 -1
- package/apps/web/dist/assets/{NewWizardPage-C8hPvrK8.js → NewWizardPage-2qLAbvDL.js} +2 -2
- package/apps/web/dist/assets/OnboardingPage-C1FJwEsT.js +1 -0
- package/apps/web/dist/assets/ProjectPage-CGuLUuOs.js +1 -0
- package/apps/web/dist/assets/{PublicRunner-DeCpVfZP.js → PublicRunner-BKw1lOW4.js} +1 -1
- package/apps/web/dist/assets/{ReadOnly-B-UCgnqW.js → ReadOnly-DTjv2Jzm.js} +1 -1
- package/apps/web/dist/assets/RunDrawer-D1I5xqVf.js +4 -0
- package/apps/web/dist/assets/RunnerView-BPPmlv9z.js +1 -0
- package/apps/web/dist/assets/SettingsPage-zbomQfGy.js +1 -0
- package/apps/web/dist/assets/SetupPage-D3F7M6Pz.js +1 -0
- package/apps/web/dist/assets/{SharePage-B778xUFo.js → SharePage-BY49IJgT.js} +1 -1
- package/apps/web/dist/assets/{changelog-DCJo2mbx.js → changelog-DYZC2E6q.js} +1 -1
- package/apps/web/dist/assets/{circle-alert-DLvC9ePT.js → circle-alert-CVOH4wAv.js} +1 -1
- package/apps/web/dist/assets/{esm-BJskogLD.js → esm-BfXdeWaI.js} +1 -1
- package/apps/web/dist/assets/index-B9xVkr2X.css +1 -0
- package/apps/web/dist/assets/index-BH0PYgov.js +15 -0
- package/apps/web/dist/assets/{lock-DR7NakPn.js → lock-DHBh62LA.js} +1 -1
- package/apps/web/dist/assets/{phone-BORh3jfv.js → phone-Bt1cyWpY.js} +1 -1
- package/apps/web/dist/assets/{settings-menu-DV5D91TD.js → settings-menu-ace1vI3i.js} +1 -1
- package/apps/web/dist/assets/{ui-BOCXzzHi.js → ui-D2SqIZeQ.js} +2 -2
- package/apps/web/dist/assets/{upload-B3yv8gsk.js → upload-CCEq88l4.js} +1 -1
- package/apps/web/dist/index.html +3 -3
- package/apps/web/dist/install.sh +17 -7
- package/apps/web/dist/mcp/flow.html +3 -3
- package/node_modules/@engenty-wizards/shared/dist/assistant.d.ts +62 -1
- package/node_modules/@engenty-wizards/shared/dist/assistant.js +10 -0
- package/package.json +1 -1
- package/apps/web/dist/assets/ChatView-uogJ3nht.js +0 -10
- package/apps/web/dist/assets/EditorPage-n8YuTh6j.js +0 -3
- package/apps/web/dist/assets/ProjectPage-B686ArYy.js +0 -1
- package/apps/web/dist/assets/RunDrawer-C6pzi8AJ.js +0 -4
- package/apps/web/dist/assets/RunnerView-Deenymq0.js +0 -1
- package/apps/web/dist/assets/SettingsPage-CK6L0yEe.js +0 -1
- package/apps/web/dist/assets/SetupPage-9CH3qXdH.js +0 -1
- package/apps/web/dist/assets/index-D6hUM8NU.css +0 -1
- package/apps/web/dist/assets/index-Df6UxElH.js +0 -12
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import { accountOverview, knownTeam, linkedAccount, unlink, } from "../auth/account.js";
|
|
3
|
+
import { DeviceSignInError, requestDeviceCode, waitForDeviceSignIn } from "../auth/device.js";
|
|
4
|
+
import { migrateControlDb } from "../db/client.js";
|
|
5
|
+
import { env } from "../env.js";
|
|
6
|
+
import { featuresFile, fileFeatures, writeFeature } from "../features.js";
|
|
7
|
+
import { command, readInstallNote, writeInstallNote } from "./home.js";
|
|
8
|
+
import { openBrowser, runningRuntime, settings } from "./start.js";
|
|
9
|
+
import { bold, busy, cyan, dim, tilde } from "./ui.js";
|
|
10
|
+
/**
|
|
11
|
+
* The account of this install, from the terminal. `wizards account` says where it stands,
|
|
12
|
+
* `sign-in` runs the device flow (auth/device.ts), `sign-out` ends the link the way the
|
|
13
|
+
* studio does. The setup's first step is the same sign-in. `--local` keeps an install off the
|
|
14
|
+
* account altogether: `account=off` in the install's `.features` hides every account surface
|
|
15
|
+
* (packages/shared/src/feature-flags.ts), and a note in install.json says so.
|
|
16
|
+
*
|
|
17
|
+
* Everything here reads and writes the install's settings and vault through the runtime's own
|
|
18
|
+
* modules, with or without a runtime running (install-env.ts puts this process where the
|
|
19
|
+
* runtime is). A runtime that runs reads the account from the settings as it goes; what it
|
|
20
|
+
* holds from its start is brought in line by a restart, which is offered.
|
|
21
|
+
*/
|
|
22
|
+
export const INTRO = "Your team, its space and your wizards live in the account. This computer joins the space.";
|
|
23
|
+
let prepared = null;
|
|
24
|
+
/** The control database as the runtime keeps it: a fresh install has none until something opens it. */
|
|
25
|
+
function ready() {
|
|
26
|
+
prepared ??= migrateControlDb();
|
|
27
|
+
return prepared;
|
|
28
|
+
}
|
|
29
|
+
/** Set up with `--local`: the install's `.features` says `account=off`. */
|
|
30
|
+
export function isLocalOnly(file = featuresFile()) {
|
|
31
|
+
return fileFeatures(file).account === false;
|
|
32
|
+
}
|
|
33
|
+
/** `--local`: the switch into the file, the note into install.json. */
|
|
34
|
+
export function localOnly(paths, file = featuresFile()) {
|
|
35
|
+
writeFeature("account", false, file);
|
|
36
|
+
writeInstallNote({ ...readInstallNote(paths), local: true }, paths);
|
|
37
|
+
}
|
|
38
|
+
/** A sign-in ends local-only: the switch follows the default again, the note goes. */
|
|
39
|
+
export function joinedAccount(paths, file = featuresFile()) {
|
|
40
|
+
if (isLocalOnly(file)) {
|
|
41
|
+
writeFeature("account", null, file);
|
|
42
|
+
}
|
|
43
|
+
const note = readInstallNote(paths);
|
|
44
|
+
if (note.local) {
|
|
45
|
+
const { local: _, ...rest } = note;
|
|
46
|
+
writeInstallNote(rest, paths);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Where the account stands, from this machine alone: no round trip. */
|
|
50
|
+
export async function accountState() {
|
|
51
|
+
if (isLocalOnly()) {
|
|
52
|
+
return { kind: "local" };
|
|
53
|
+
}
|
|
54
|
+
await ready();
|
|
55
|
+
const account = await linkedAccount();
|
|
56
|
+
if (!account) {
|
|
57
|
+
return { kind: "none" };
|
|
58
|
+
}
|
|
59
|
+
return { kind: "linked", account, team: await knownTeam() };
|
|
60
|
+
}
|
|
61
|
+
/** "Matthias · team Platzer Fenster & Türen · 120 credits", as far as it is known. */
|
|
62
|
+
export function described(account, team, credits = true) {
|
|
63
|
+
const parts = [account.name || account.email];
|
|
64
|
+
if (team) {
|
|
65
|
+
parts.push(`team ${team.name}`);
|
|
66
|
+
if (credits) {
|
|
67
|
+
parts.push(`${team.balanceCredits} credits`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return parts.join(" · ");
|
|
71
|
+
}
|
|
72
|
+
/** One line for the menu and `wizards status`, after the symbol. */
|
|
73
|
+
export function describeAccount(state) {
|
|
74
|
+
switch (state.kind) {
|
|
75
|
+
case "local":
|
|
76
|
+
return "local only";
|
|
77
|
+
case "none":
|
|
78
|
+
return "not signed in";
|
|
79
|
+
default:
|
|
80
|
+
return described(state.account, state.team, false);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** The account's site, as people know it: engenty.ai for account.engenty.ai. */
|
|
84
|
+
const host = () => new URL(env.local.accountUrl).host.replace(/^account\./, "");
|
|
85
|
+
/** `wizards account`: where it stands, with the Manage-App's word when it answers. */
|
|
86
|
+
async function status() {
|
|
87
|
+
const state = await accountState();
|
|
88
|
+
if (state.kind === "local") {
|
|
89
|
+
console.log(`Local only (no account). ${dim(`\`${command()} account sign-in\` connects one.`)}`);
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
if (state.kind === "none") {
|
|
93
|
+
console.log(`Not signed in. ${dim(`\`${command()} account sign-in\``)}`);
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
const overview = await accountOverview();
|
|
97
|
+
const team = overview?.tenant ?? state.team;
|
|
98
|
+
console.log(`Signed in as ${described(state.account, team)} · ${host()}${overview ? "" : dim(` (${host()} did not answer${team ? "; this is from last time" : ""})`)}`);
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The device flow on the terminal: the link and the code, the browser opened where there is
|
|
103
|
+
* one, then a wait while the person allows it. Null when it ended without an account.
|
|
104
|
+
*/
|
|
105
|
+
async function deviceSignIn() {
|
|
106
|
+
let code;
|
|
107
|
+
try {
|
|
108
|
+
code = await requestDeviceCode();
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
p.log.error(err.message);
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const link = code.verificationUriComplete ?? code.verificationUri;
|
|
115
|
+
p.log.message(`Open ${cyan(link)} and type ${bold(code.userCode)}`);
|
|
116
|
+
if (openBrowser(link)) {
|
|
117
|
+
p.log.message(dim("The page opens in your browser."));
|
|
118
|
+
}
|
|
119
|
+
const spinner = busy("Waiting for you to allow it in the browser");
|
|
120
|
+
try {
|
|
121
|
+
const account = await waitForDeviceSignIn(code);
|
|
122
|
+
const overview = await accountOverview();
|
|
123
|
+
spinner.stop(`Signed in as ${described(account, overview?.tenant ?? null)}`);
|
|
124
|
+
return account;
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
spinner.fail(err instanceof DeviceSignInError
|
|
128
|
+
? err.message
|
|
129
|
+
: `The sign-in did not work. ${err.message}`);
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* A runtime that runs reads the account from the settings as it goes, but holds a few things
|
|
135
|
+
* from its start: the Pro modules it loaded, which source the models run on, what was sent to
|
|
136
|
+
* the cloud. A restart brings it in line; the person is asked, or told how.
|
|
137
|
+
*/
|
|
138
|
+
async function pickUp(paths, interactive, why) {
|
|
139
|
+
if (!(await runningRuntime(settings(paths).dataDir))) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const how = `${command()} restart`;
|
|
143
|
+
if (!interactive) {
|
|
144
|
+
p.log.info(`The studio that runs ${why} after ${cyan(how)}.`);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const answer = await p.confirm({
|
|
148
|
+
message: `Restart the studio that runs, so it ${why}?`,
|
|
149
|
+
initialValue: true,
|
|
150
|
+
});
|
|
151
|
+
if (p.isCancel(answer) || !answer) {
|
|
152
|
+
p.log.info(dim(`Later: ${how}`));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
// status.ts shows the account; loaded here when needed, so neither module waits on the other.
|
|
156
|
+
const { restart } = await import("./status.js");
|
|
157
|
+
await restart(paths);
|
|
158
|
+
}
|
|
159
|
+
const SIGNED_IN = "fetches the Pro modules of your plan and sends what is published to the cloud";
|
|
160
|
+
async function signIn(paths, interactive) {
|
|
161
|
+
await ready();
|
|
162
|
+
const linked = await linkedAccount();
|
|
163
|
+
if (linked) {
|
|
164
|
+
p.log.info(`Already signed in as ${described(linked, await knownTeam(), false)}. ${dim(`To switch: \`${command()} account sign-out\` first.`)}`);
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
const account = await deviceSignIn();
|
|
168
|
+
if (!account) {
|
|
169
|
+
return 1;
|
|
170
|
+
}
|
|
171
|
+
joinedAccount(paths);
|
|
172
|
+
p.log.message(dim("What you publish here runs in the cloud too."));
|
|
173
|
+
await pickUp(paths, interactive, SIGNED_IN);
|
|
174
|
+
return 0;
|
|
175
|
+
}
|
|
176
|
+
async function signOut(paths, interactive) {
|
|
177
|
+
const state = await accountState();
|
|
178
|
+
if (state.kind !== "linked") {
|
|
179
|
+
console.log(state.kind === "local" ? "Local only: there is no account to sign out." : "Not signed in.");
|
|
180
|
+
return 0;
|
|
181
|
+
}
|
|
182
|
+
const spinner = busy(`Signing out ${state.account.name || state.account.email}`);
|
|
183
|
+
await unlink();
|
|
184
|
+
// The same as the studio's sign-out (DELETE /api/studio/account): what was sent to the cloud
|
|
185
|
+
// is forgotten, the entitlement goes with the account's plan, and models that ran on its
|
|
186
|
+
// credits run on own keys again. This process never loaded Pro modules or models, so only the
|
|
187
|
+
// settings change here; a runtime that runs lets go of its modules on restart.
|
|
188
|
+
const [{ forgetCloud }, { refreshPro }, models] = await Promise.all([
|
|
189
|
+
import("../services/cloud.js"),
|
|
190
|
+
import("../plugins/pro.js"),
|
|
191
|
+
import("../models.js"),
|
|
192
|
+
]);
|
|
193
|
+
await forgetCloud();
|
|
194
|
+
await refreshPro();
|
|
195
|
+
await models.loadLocalModels();
|
|
196
|
+
if (models.localModelSettings().source === "account") {
|
|
197
|
+
await models.saveLocalModels({ source: "own" });
|
|
198
|
+
}
|
|
199
|
+
spinner.stop("Signed out. Your wizards and results stay on this computer.");
|
|
200
|
+
await pickUp(paths, interactive, "lets go of the account");
|
|
201
|
+
return 0;
|
|
202
|
+
}
|
|
203
|
+
/** `wizards account [sign-in|sign-out]`. */
|
|
204
|
+
export async function accountCommand(paths, value, options) {
|
|
205
|
+
const interactive = !options.yes && Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
206
|
+
switch (value) {
|
|
207
|
+
case "":
|
|
208
|
+
return status();
|
|
209
|
+
case "sign-in":
|
|
210
|
+
case "signin":
|
|
211
|
+
case "login":
|
|
212
|
+
return signIn(paths, interactive);
|
|
213
|
+
case "sign-out":
|
|
214
|
+
case "signout":
|
|
215
|
+
case "logout":
|
|
216
|
+
return signOut(paths, interactive);
|
|
217
|
+
default:
|
|
218
|
+
console.error(`${command()} account [sign-in|sign-out]`);
|
|
219
|
+
return 2;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/** The menu's "Account": where it stands, and the way in or out. */
|
|
223
|
+
export async function accountMenu(paths) {
|
|
224
|
+
await status();
|
|
225
|
+
const state = await accountState();
|
|
226
|
+
const action = await p.select({
|
|
227
|
+
message: "Account",
|
|
228
|
+
options: [
|
|
229
|
+
state.kind === "linked"
|
|
230
|
+
? { value: "sign-out", label: "Sign out", hint: "your wizards and results stay here" }
|
|
231
|
+
: { value: "sign-in", label: "Sign in", hint: "with a code, in your browser" },
|
|
232
|
+
{ value: "back", label: "Back" },
|
|
233
|
+
],
|
|
234
|
+
});
|
|
235
|
+
if (p.isCancel(action) || action === "back") {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (action === "sign-in") {
|
|
239
|
+
await signIn(paths, true);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
await signOut(paths, true);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* The setup's first step. `--local` keeps the install off the account; else the person is
|
|
247
|
+
* asked to sign in, unless the setup runs without a terminal (`--yes`), which says how to do
|
|
248
|
+
* it later.
|
|
249
|
+
*/
|
|
250
|
+
export async function accountStep(paths, options, interactive) {
|
|
251
|
+
if (options.local) {
|
|
252
|
+
localOnly(paths);
|
|
253
|
+
p.log.warn(`Local only. No account, nothing leaves this computer. Wizards, the space and results stay in ${tilde(settings(paths).dataDir)}. Links answer on this computer only.`);
|
|
254
|
+
p.log.message(dim(`Connect an account any time: ${command()} account`));
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const state = await accountState();
|
|
258
|
+
if (state.kind === "local") {
|
|
259
|
+
p.log.info(`Local only (no account). ${dim(`\`${command()} account sign-in\` connects one.`)}`);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (state.kind === "linked") {
|
|
263
|
+
const overview = await accountOverview();
|
|
264
|
+
p.log.success(`Signed in as ${described(state.account, overview?.tenant ?? state.team)}`);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
p.note(INTRO, "Your engenty account");
|
|
268
|
+
if (!interactive) {
|
|
269
|
+
p.log.info(dim(`Sign in later: ${command()} account sign-in`));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const answer = await p.confirm({ message: "Sign in now?", initialValue: true });
|
|
273
|
+
if (p.isCancel(answer)) {
|
|
274
|
+
p.cancel(`Setup stopped. Run it again with \`${command()} setup\`.`);
|
|
275
|
+
return "cancelled";
|
|
276
|
+
}
|
|
277
|
+
if (!answer) {
|
|
278
|
+
p.log.info(dim(`Later: ${command()} account`));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const account = await deviceSignIn();
|
|
282
|
+
if (!account) {
|
|
283
|
+
p.log.info(dim(`Later: ${command()} account sign-in`));
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
joinedAccount(paths);
|
|
287
|
+
p.log.message(dim("What you publish here runs in the cloud too."));
|
|
288
|
+
await pickUp(paths, interactive, SIGNED_IN);
|
|
289
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { layout, readEnvFile } from "./home.js";
|
|
4
|
+
/**
|
|
5
|
+
* Imported first by the command: before the runtime's modules load, this process sees the
|
|
6
|
+
* install the way its runtime does. env.ts reads the process once, when it is first imported,
|
|
7
|
+
* and `.env` from the working directory; the runtime starts in the install's folder (start.ts)
|
|
8
|
+
* and so reads the install's `.env`, with its data folder beside it. The command goes there
|
|
9
|
+
* too — else `wizards account` or `wizards status` from another folder would open a `data/`
|
|
10
|
+
* of its own there, with a secret of its own, and a vault the runtime cannot read.
|
|
11
|
+
*
|
|
12
|
+
* What the terminal exports for other projects never reaches the runtime (environment.ts); a
|
|
13
|
+
* few keys that would make this process another runtime are taken from the install's `.env`
|
|
14
|
+
* alone. The rest the runtime inherits, so the terminal's word stands.
|
|
15
|
+
*/
|
|
16
|
+
const paths = layout();
|
|
17
|
+
const fileEnv = readEnvFile(paths.envFile);
|
|
18
|
+
/** The install's data folder, as start.ts hands it to the runtime: the terminal's word first. */
|
|
19
|
+
const given = process.env.DATA_DIR?.trim() || fileEnv.DATA_DIR?.trim();
|
|
20
|
+
process.env.DATA_DIR = given ? resolve(paths.home, given) : paths.data;
|
|
21
|
+
for (const key of ["MANAGE_URL", "APP_SECRET", "DATABASE_URL", "DATABASE_AUTH_TOKEN"]) {
|
|
22
|
+
process.env[key] = fileEnv[key] ?? "";
|
|
23
|
+
}
|
|
24
|
+
// Keys and the account's sign-in live in the Keychain on a Mac, as the runtime keeps them.
|
|
25
|
+
if (process.platform === "darwin" &&
|
|
26
|
+
process.env.SECRETS === undefined &&
|
|
27
|
+
fileEnv.SECRETS === undefined) {
|
|
28
|
+
process.env.SECRETS = "keychain";
|
|
29
|
+
}
|
|
30
|
+
/** Where the command was typed: `wizards validate ./my.wizard` means a path from there. */
|
|
31
|
+
export const startedIn = process.cwd();
|
|
32
|
+
mkdirSync(paths.home, { recursive: true });
|
|
33
|
+
process.chdir(paths.home);
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import "./install-env.js";
|
|
2
|
+
import { accountCommand } from "./account.js";
|
|
1
3
|
import { apps, connectCommand } from "./connect.js";
|
|
2
4
|
import { featuresCommand } from "./features.js";
|
|
3
5
|
import { command, layout, packageVersion, readInstallNote } from "./home.js";
|
|
6
|
+
import { startedIn } from "./install-env.js";
|
|
4
7
|
import { CLIENTS, nodeIsCurrent } from "./machine.js";
|
|
5
8
|
import { mcp } from "./mcp.js";
|
|
6
9
|
import { menu } from "./menu.js";
|
|
@@ -16,7 +19,9 @@ const HELP = `engenty wizards ${packageVersion()}
|
|
|
16
19
|
(needs a terminal)
|
|
17
20
|
${command()} start start it and open the studio; it runs in this terminal until Ctrl-C
|
|
18
21
|
${command()} studio open the studio; when it does not run, it starts as a service first
|
|
19
|
-
${command()} setup guided setup: an AI client to think with, ffmpeg,
|
|
22
|
+
${command()} setup guided setup: your engenty account, an AI client to think with, ffmpeg,
|
|
23
|
+
start at login
|
|
24
|
+
${command()} account your engenty account: status, sign-in, sign-out
|
|
20
25
|
${command()} open let this browser into the running studio (--print: only show the link)
|
|
21
26
|
${command()} status what is installed and what runs
|
|
22
27
|
${command()} doctor status, and what is wrong
|
|
@@ -37,6 +42,7 @@ const HELP = `engenty wizards ${packageVersion()}
|
|
|
37
42
|
checked on import
|
|
38
43
|
|
|
39
44
|
--yes, -y ask nothing and install nothing optional
|
|
45
|
+
--local setup without an account: nothing leaves this computer
|
|
40
46
|
--client <name> install this AI client without asking: ${CLIENTS.map((c) => c.id).join(", ")}
|
|
41
47
|
--no-open start without opening the browser
|
|
42
48
|
--service, -s start as a service, detached from this terminal; \`${command()} stop\` stops it
|
|
@@ -52,6 +58,7 @@ function parse(argv) {
|
|
|
52
58
|
paths: [],
|
|
53
59
|
json: false,
|
|
54
60
|
yes: false,
|
|
61
|
+
local: false,
|
|
55
62
|
open: true,
|
|
56
63
|
service: false,
|
|
57
64
|
print: false,
|
|
@@ -62,6 +69,9 @@ function parse(argv) {
|
|
|
62
69
|
if (arg === "--yes" || arg === "-y") {
|
|
63
70
|
args.yes = true;
|
|
64
71
|
}
|
|
72
|
+
else if (arg === "--local") {
|
|
73
|
+
args.local = true;
|
|
74
|
+
}
|
|
65
75
|
else if (arg === "--no-open") {
|
|
66
76
|
args.open = false;
|
|
67
77
|
}
|
|
@@ -90,7 +100,7 @@ function parse(argv) {
|
|
|
90
100
|
else if (arg.startsWith("-")) {
|
|
91
101
|
return `Unknown option ${arg}.`;
|
|
92
102
|
}
|
|
93
|
-
else if (
|
|
103
|
+
else if (["autostart", "logs", "account"].includes(args.command) && !args.value) {
|
|
94
104
|
args.value = arg;
|
|
95
105
|
}
|
|
96
106
|
else if (["connect", "disconnect", "features"].includes(args.command)) {
|
|
@@ -115,7 +125,7 @@ async function main(argv) {
|
|
|
115
125
|
return 2;
|
|
116
126
|
}
|
|
117
127
|
const paths = layout();
|
|
118
|
-
const setupOptions = { yes: args.yes, clients: args.clients };
|
|
128
|
+
const setupOptions = { yes: args.yes, clients: args.clients, local: args.local };
|
|
119
129
|
if (!args.command) {
|
|
120
130
|
// Without a command: the menu, which needs a terminal. Starting is \`start\`.
|
|
121
131
|
if (args.service || !args.open || args.print) {
|
|
@@ -168,6 +178,8 @@ async function main(argv) {
|
|
|
168
178
|
return update(paths);
|
|
169
179
|
case "autostart":
|
|
170
180
|
return autostart(paths, args.value);
|
|
181
|
+
case "account":
|
|
182
|
+
return accountCommand(paths, args.value, { yes: args.yes });
|
|
171
183
|
case "connect":
|
|
172
184
|
case "disconnect":
|
|
173
185
|
return connectCommand(paths, args.apps, args.command === "disconnect");
|
|
@@ -176,6 +188,8 @@ async function main(argv) {
|
|
|
176
188
|
case "features":
|
|
177
189
|
return featuresCommand(args.apps);
|
|
178
190
|
case "validate":
|
|
191
|
+
// The paths are the person's, from where they typed the command (install-env.ts).
|
|
192
|
+
process.chdir(startedIn);
|
|
179
193
|
return validateCommand(args.paths, args.json);
|
|
180
194
|
default:
|
|
181
195
|
console.error(`Unknown command ${args.command}.\n\n${HELP}`);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
2
|
import { updateStatus } from "../update.js";
|
|
3
|
+
import { accountMenu, accountState, describeAccount } from "./account.js";
|
|
3
4
|
import { autostartState, setAutostart } from "./autostart.js";
|
|
4
5
|
import { connectCommand } from "./connect.js";
|
|
5
6
|
import { command, packageVersion, readInstallNote } from "./home.js";
|
|
@@ -10,14 +11,15 @@ import { badge, cyan, dim, no, ok } from "./ui.js";
|
|
|
10
11
|
import { update } from "./update.js";
|
|
11
12
|
async function look(paths) {
|
|
12
13
|
const { dataDir } = settings(paths);
|
|
13
|
-
const [running, atLogin, release] = await Promise.all([
|
|
14
|
+
const [running, atLogin, release, account] = await Promise.all([
|
|
14
15
|
runningRuntime(dataDir),
|
|
15
16
|
autostartState(paths),
|
|
16
17
|
updateStatus().catch(() => null),
|
|
18
|
+
accountState(),
|
|
17
19
|
]);
|
|
18
|
-
return { running, atLogin, release };
|
|
20
|
+
return { running, atLogin, release, account };
|
|
19
21
|
}
|
|
20
|
-
function report({ running, atLogin, release }) {
|
|
22
|
+
function report({ running, atLogin, release, account }) {
|
|
21
23
|
const lines = [
|
|
22
24
|
running
|
|
23
25
|
? `${ok} running ${cyan(running.url)} ${dim(`pid ${running.pid}, since ${new Date(running.startedAt).toLocaleString()}`)}`
|
|
@@ -26,12 +28,15 @@ function report({ running, atLogin, release }) {
|
|
|
26
28
|
if (!("unavailable" in atLogin)) {
|
|
27
29
|
lines.push(atLogin.on ? `${ok} starts at login` : `${no} does not start at login`);
|
|
28
30
|
}
|
|
31
|
+
lines.push(account.kind === "linked"
|
|
32
|
+
? `${ok} signed in as ${describeAccount(account)}`
|
|
33
|
+
: `${no} ${describeAccount(account)}`);
|
|
29
34
|
if (release?.newer) {
|
|
30
35
|
lines.push(`${cyan("↑")} ${release.latest} is out ${dim(`(you have ${release.current})`)}`);
|
|
31
36
|
}
|
|
32
37
|
return lines.join("\n");
|
|
33
38
|
}
|
|
34
|
-
function choices({ running, atLogin, release }) {
|
|
39
|
+
function choices({ running, atLogin, release, account }) {
|
|
35
40
|
const options = running
|
|
36
41
|
? [
|
|
37
42
|
{ value: "open", label: "Open the studio", hint: "in your browser" },
|
|
@@ -58,7 +63,13 @@ function choices({ running, atLogin, release }) {
|
|
|
58
63
|
hint: atLogin.on ? "starts at login" : "does not start at login",
|
|
59
64
|
});
|
|
60
65
|
}
|
|
61
|
-
options.push({ value: "connect", label: "Connect AI apps", hint: "your wizards in Claude, Cursor, Codex …" }, {
|
|
66
|
+
options.push({ value: "connect", label: "Connect AI apps", hint: "your wizards in Claude, Cursor, Codex …" }, {
|
|
67
|
+
value: "account",
|
|
68
|
+
label: "Account",
|
|
69
|
+
hint: account.kind === "linked"
|
|
70
|
+
? `signed in as ${describeAccount(account)}`
|
|
71
|
+
: describeAccount(account),
|
|
72
|
+
}, { value: "status", label: "Status", hint: "what is installed, and what is wrong" }, { 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 ? "it keeps running" : undefined });
|
|
62
73
|
return options;
|
|
63
74
|
}
|
|
64
75
|
/** How the service runs: whether it starts at login. Esc goes back to the menu. */
|
|
@@ -153,6 +164,9 @@ export async function menu(paths, setupOptions, help) {
|
|
|
153
164
|
case "connect":
|
|
154
165
|
await connectCommand(paths, [], false);
|
|
155
166
|
break;
|
|
167
|
+
case "account":
|
|
168
|
+
await accountMenu(paths);
|
|
169
|
+
break;
|
|
156
170
|
case "status":
|
|
157
171
|
await status(paths, true);
|
|
158
172
|
break;
|
|
@@ -2,6 +2,7 @@ import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { delimiter, join } from "node:path";
|
|
4
4
|
import * as p from "@clack/prompts";
|
|
5
|
+
import { accountStep } from "./account.js";
|
|
5
6
|
import { autostartState, setAutostart } from "./autostart.js";
|
|
6
7
|
import { installClient, installCommand } from "./clients.js";
|
|
7
8
|
import { appStates, connectApps } from "./connect.js";
|
|
@@ -9,21 +10,9 @@ import { runLogged } from "./exec.js";
|
|
|
9
10
|
import { command, installedByScript, isCheckout, packageVersion, readInstallNote, writeInstallNote, } from "./home.js";
|
|
10
11
|
import { CLIENTS, detectClients, findChrome, findFfmpeg, findOnPath, suggestedClient, vendorApp, } from "./machine.js";
|
|
11
12
|
import { runningRuntime, settings } from "./start.js";
|
|
12
|
-
import { badge, cyan, dim, tilde } from "./ui.js";
|
|
13
|
+
import { badge, busy, cyan, dim, tilde } from "./ui.js";
|
|
13
14
|
/** The studio as an app of its own: Chrome (or Edge) installs it from the page. */
|
|
14
15
|
const CHROME_APP = "Tip: in Chrome or Edge the studio installs as an app, in a window of its own with its own icon. The studio offers it at the top; or use the install icon in the address bar.";
|
|
15
|
-
/** A spinner on a terminal; where nobody watches, only what came of it. */
|
|
16
|
-
function busy(message) {
|
|
17
|
-
if (!process.stdout.isTTY) {
|
|
18
|
-
return { stop: (text) => p.log.step(text), fail: (text) => p.log.warn(text) };
|
|
19
|
-
}
|
|
20
|
-
const spinner = p.spinner();
|
|
21
|
-
spinner.start(message);
|
|
22
|
-
return {
|
|
23
|
-
stop: (text) => spinner.stop(text),
|
|
24
|
-
fail: (text) => spinner.error(text),
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
16
|
function cancelled(value) {
|
|
28
17
|
if (p.isCancel(value)) {
|
|
29
18
|
p.cancel(`Setup stopped. Run it again with \`${command()} setup\`.`);
|
|
@@ -298,9 +287,10 @@ async function howToUse(paths) {
|
|
|
298
287
|
].join("\n"));
|
|
299
288
|
}
|
|
300
289
|
/**
|
|
301
|
-
* The guided setup:
|
|
302
|
-
* to think with, ffmpeg, starting at login. It
|
|
303
|
-
* start, stop and check it. Returns false
|
|
290
|
+
* The guided setup: the account this computer joins, then what this machine has, what is
|
|
291
|
+
* missing, and what to install — an AI client to think with, ffmpeg, starting at login. It
|
|
292
|
+
* starts nothing; it ends with what runs and how to start, stop and check it. Returns false
|
|
293
|
+
* when it was stopped.
|
|
304
294
|
*/
|
|
305
295
|
export async function setup(paths, options) {
|
|
306
296
|
const interactive = !options.yes && Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
@@ -310,6 +300,9 @@ export async function setup(paths, options) {
|
|
|
310
300
|
`Install ${tilde(paths.home)}${installedByScript() ? "" : dim(` (data; the code runs from ${isCheckout() ? "this checkout" : "npm"})`)}`,
|
|
311
301
|
`Node ${process.version} ${dim(tilde(process.execPath))}`,
|
|
312
302
|
].join("\n"));
|
|
303
|
+
if ((await accountStep(paths, options, interactive)) === "cancelled") {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
313
306
|
if ((await clientsStep(paths, options, interactive)) === "cancelled") {
|
|
314
307
|
return false;
|
|
315
308
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { accessSync, constants, existsSync, mkdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { accountState, describeAccount } from "./account.js";
|
|
3
4
|
import { autostartState, setAutostart } from "./autostart.js";
|
|
4
5
|
import { installedByScript, isCheckout, packageRoot, packageVersion } from "./home.js";
|
|
5
6
|
import { detectClients, findChrome, findFfmpeg, nodeIsCurrent } from "./machine.js";
|
|
@@ -11,11 +12,12 @@ import { bad, badge, cyan, dim, no, ok, tilde } from "./ui.js";
|
|
|
11
12
|
*/
|
|
12
13
|
export async function status(paths, checks) {
|
|
13
14
|
const { dataDir } = settings(paths);
|
|
14
|
-
const [running, clients, ffmpeg, atLogin] = await Promise.all([
|
|
15
|
+
const [running, clients, ffmpeg, atLogin, account] = await Promise.all([
|
|
15
16
|
runningRuntime(dataDir),
|
|
16
17
|
detectClients(paths),
|
|
17
18
|
findFfmpeg(),
|
|
18
19
|
autostartState(paths),
|
|
20
|
+
accountState(),
|
|
19
21
|
]);
|
|
20
22
|
const chrome = findChrome();
|
|
21
23
|
const row = (label, value) => console.log(` ${label.padEnd(9)} ${value}`);
|
|
@@ -31,6 +33,7 @@ export async function status(paths, checks) {
|
|
|
31
33
|
if (!("unavailable" in atLogin)) {
|
|
32
34
|
row("At login", atLogin.on ? `${ok} starts ${dim(`(${atLogin.how})`)}` : `${no} does not start`);
|
|
33
35
|
}
|
|
36
|
+
row("Account", `${account.kind === "linked" ? ok : no} ${describeAccount(account)}`);
|
|
34
37
|
for (const { client, path, version } of clients) {
|
|
35
38
|
row(client.name.split(" ")[0], path ? `${ok} ${version} ${dim(tilde(path))}` : `${no} not installed`);
|
|
36
39
|
}
|
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { styleText } from "node:util";
|
|
3
|
+
import * as p from "@clack/prompts";
|
|
3
4
|
const colours = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
4
5
|
/** The name on engenty's orange. */
|
|
5
6
|
export const badge = (text) => colours ? `\x1b[48;2;224;83;27m\x1b[97m\x1b[1m ${text} \x1b[0m` : text;
|
|
6
7
|
export const dim = (text) => styleText("dim", text);
|
|
8
|
+
export const bold = (text) => styleText("bold", text);
|
|
7
9
|
export const cyan = (text) => styleText("cyan", text);
|
|
8
10
|
export const ok = styleText("green", "✓");
|
|
9
11
|
export const no = styleText("yellow", "–");
|
|
10
12
|
export const bad = styleText("red", "✗");
|
|
11
13
|
/** A path as the person would write it. */
|
|
12
14
|
export const tilde = (path) => path.startsWith(homedir()) ? `~${path.slice(homedir().length)}` : path;
|
|
15
|
+
/** A spinner on a terminal; where nobody watches, only what came of it. */
|
|
16
|
+
export function busy(message) {
|
|
17
|
+
if (!process.stdout.isTTY) {
|
|
18
|
+
return { stop: (text) => p.log.step(text), fail: (text) => p.log.warn(text) };
|
|
19
|
+
}
|
|
20
|
+
const spinner = p.spinner();
|
|
21
|
+
spinner.start(message);
|
|
22
|
+
return {
|
|
23
|
+
stop: (text) => spinner.stop(text),
|
|
24
|
+
fail: (text) => spinner.error(text),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
|
-
title:
|
|
2
|
+
title: Goose, the assistant
|
|
3
3
|
description: The chat at the bottom of every studio page. It knows the app, sees the page you are on, shows you where things are and fills the space in.
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.7
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Goose, the assistant, stands at the bottom of every page of the studio but the editor, which has a
|
|
8
8
|
conversation of its own. It thinks on the High class of Text & reasoning: your subscription,
|
|
9
9
|
your keys or credits ([Models and account](./models.md)).
|
|
10
10
|
|
|
@@ -26,20 +26,38 @@ a value into a field for you; you check it and save. It does not press buttons.
|
|
|
26
26
|
| | |
|
|
27
27
|
|---|---|
|
|
28
28
|
| At the bottom | The default. Click the field or the engenty, and the chat rises above it |
|
|
29
|
-
|
|
|
29
|
+
| Closed | ✕ in the header: the engenty at the lower right; a click opens the chat where it last stood. Data and Results start closed |
|
|
30
30
|
| Sidebar | Beside the page, on a window 1,520 pixels wide or more |
|
|
31
31
|
|
|
32
32
|
The buttons in the chat's header switch between them; the studio remembers your choice.
|
|
33
33
|
|
|
34
|
+
At the bottom, the chat moves out of the way of what it covers: drag it by the grip before its
|
|
35
|
+
name (or the engenty, while it is folded). It stays where you put it. Near the bottom it snaps to
|
|
36
|
+
the middle or the right; near the right side, higher up, it becomes the sidebar, and the
|
|
37
|
+
sidebar's grip pulls it out again. A dashed outline shows where it will snap. The handle on top of
|
|
38
|
+
the chat sets how tall the thread is; a click on it folds the chat away.
|
|
39
|
+
|
|
40
|
+
## Writing while it works
|
|
41
|
+
|
|
42
|
+
You can write on while Goose answers. What you send waits above the field:
|
|
43
|
+
|
|
44
|
+
| | |
|
|
45
|
+
|---|---|
|
|
46
|
+
| Enter | Goose reads it before its next step, and keeps what you asked before unless the message changes it |
|
|
47
|
+
| ⌘↩ (Ctrl+↩) | It goes out as the next question once the answer is done |
|
|
48
|
+
| Send now | Stops the answer and sends it at once |
|
|
49
|
+
|
|
50
|
+
The stop button stands where Send does while the field is empty.
|
|
51
|
+
|
|
34
52
|
## One chat
|
|
35
53
|
|
|
36
54
|
You have one chat, the same on every page and in every space, after a reload and on another
|
|
37
55
|
device. Nobody else in the team sees it.
|
|
38
56
|
|
|
39
|
-
- **New chat** (the menu at the chat's name)
|
|
40
|
-
|
|
57
|
+
- **New chat** (the pen in the header, or the menu at the chat's name) clears the chat at once and
|
|
58
|
+
sums the old one up meanwhile. Goose keeps the summaries of your last chats in mind.
|
|
41
59
|
- **Earlier chats** lists the closed ones to read. Ask about something you discussed before, and
|
|
42
|
-
|
|
60
|
+
Goose searches them.
|
|
43
61
|
|
|
44
62
|
## Live chat
|
|
45
63
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Command line
|
|
3
3
|
description: The wizards command — start, setup, status, update — and the install's settings file.
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.4
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## Commands
|
|
@@ -11,7 +11,8 @@ version: 0.3.2
|
|
|
11
11
|
| `wizards` | What runs, and a menu: open the studio, restart and stop while it runs, "Start as service" or "Start here" (in this terminal) while it does not; update, which restarts what runs into the new version; "Service settings" (start at login); connect AI apps, status, setup, help. The first time, the setup runs first. Needs a terminal |
|
|
12
12
|
| `wizards start` | Starts it and opens the studio. It runs in that terminal; Ctrl-C stops it. The first time, the setup runs first |
|
|
13
13
|
| `wizards studio` | Opens the studio in your browser. When it does not run, it starts as a service first |
|
|
14
|
-
| `wizards setup` | The guided setup again: an AI client to think with, the browser and ffmpeg, start at login, your AI apps |
|
|
14
|
+
| `wizards setup` | The guided setup again: your account, an AI client to think with, the browser and ffmpeg, start at login, your AI apps. `--local` leaves the account out ([Local only](./local-only.md)) |
|
|
15
|
+
| `wizards account` | How the install stands to your account: signed in as whom, which team, the credits. `sign-in` shows a code and opens the browser; `sign-out` disconnects the install |
|
|
15
16
|
| `wizards open` | Lets this browser into the running studio. `--print` only shows the link; it works once, within a minute |
|
|
16
17
|
| `wizards status` | What is installed and what runs, with the browser the app uses for PDF, PNG and websites |
|
|
17
18
|
| `wizards doctor` | Status, and what is wrong |
|