wizards 0.4.4 → 0.4.6

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 (129) hide show
  1. package/apps/runtime/dist/agents/architect.js +32 -13
  2. package/apps/runtime/dist/agents/subscription.js +12 -12
  3. package/apps/runtime/dist/app.js +9 -5
  4. package/apps/runtime/dist/assistant/knows.js +2 -2
  5. package/apps/runtime/dist/authoring/example.js +21 -20
  6. package/apps/runtime/dist/authoring/guide.js +4 -2
  7. package/apps/runtime/dist/connectors/imap.js +4 -4
  8. package/apps/runtime/dist/connectors/index.js +40 -15
  9. package/apps/runtime/dist/connectors/plugin-connections.js +3 -2
  10. package/apps/runtime/dist/connectors/via-account.js +8 -6
  11. package/apps/runtime/dist/docs/dev/plugins/connections.md +1 -1
  12. package/apps/runtime/dist/docs/dev/plugins/reference.md +3 -2
  13. package/apps/runtime/dist/docs/dev/plugins/runners.md +38 -2
  14. package/apps/runtime/dist/docs/dev/plugins/server.md +3 -2
  15. package/apps/runtime/dist/docs/dev/plugins/starters.md +1 -1
  16. package/apps/runtime/dist/docs/dev/plugins/studio.md +2 -1
  17. package/apps/runtime/dist/documents/parse.js +5 -3
  18. package/apps/runtime/dist/engine/events.js +1 -1
  19. package/apps/runtime/dist/engine/input.js +28 -8
  20. package/apps/runtime/dist/engine/prepare.js +2 -2
  21. package/apps/runtime/dist/engine/requirements.js +12 -3
  22. package/apps/runtime/dist/engine/runner.js +34 -23
  23. package/apps/runtime/dist/engine/steps.js +6 -5
  24. package/apps/runtime/dist/engine/surface.js +2 -1
  25. package/apps/runtime/dist/engine/template.js +1 -1
  26. package/apps/runtime/dist/engine/types.js +2 -1
  27. package/apps/runtime/dist/film/project.js +1 -1
  28. package/apps/runtime/dist/film/skills.js +2 -2
  29. package/apps/runtime/dist/film/sounds.js +1 -1
  30. package/apps/runtime/dist/film/step.js +7 -4
  31. package/apps/runtime/dist/film/tools.js +1 -1
  32. package/apps/runtime/dist/geocode.js +1 -1
  33. package/apps/runtime/dist/harness/apple.js +0 -5
  34. package/apps/runtime/dist/harness/claude.js +2 -1
  35. package/apps/runtime/dist/harness/codex.js +2 -1
  36. package/apps/runtime/dist/harness/model.js +4 -3
  37. package/apps/runtime/dist/harness/terminal.js +4 -3
  38. package/apps/runtime/dist/model-errors.js +8 -3
  39. package/apps/runtime/dist/models.js +58 -38
  40. package/apps/runtime/dist/plugins/channels.js +1 -0
  41. package/apps/runtime/dist/plugins/runs.js +2 -1
  42. package/apps/runtime/dist/routes/api.js +16 -5
  43. package/apps/runtime/dist/routes/lang.js +13 -0
  44. package/apps/runtime/dist/routes/runs.js +12 -19
  45. package/apps/runtime/dist/routes/studio.js +40 -28
  46. package/apps/runtime/dist/services/access.js +4 -3
  47. package/apps/runtime/dist/services/architect.js +15 -4
  48. package/apps/runtime/dist/services/assistant-chats.js +2 -1
  49. package/apps/runtime/dist/services/brand.js +3 -2
  50. package/apps/runtime/dist/services/errors.js +7 -5
  51. package/apps/runtime/dist/services/knowledge-model.js +3 -2
  52. package/apps/runtime/dist/services/marketplace.js +6 -5
  53. package/apps/runtime/dist/services/package.js +10 -3
  54. package/apps/runtime/dist/services/profile.js +12 -0
  55. package/apps/runtime/dist/services/project-files.js +13 -12
  56. package/apps/runtime/dist/services/projects.js +11 -6
  57. package/apps/runtime/dist/services/runners.js +17 -2
  58. package/apps/runtime/dist/services/runs.js +18 -11
  59. package/apps/runtime/dist/services/shares.js +2 -1
  60. package/apps/runtime/dist/services/site.js +21 -10
  61. package/apps/runtime/dist/services/space-categories.js +7 -6
  62. package/apps/runtime/dist/services/space-data.js +19 -16
  63. package/apps/runtime/dist/services/spaces.js +8 -7
  64. package/apps/runtime/dist/services/wizards.js +41 -20
  65. package/apps/runtime/dist/store/errors.js +2 -1
  66. package/apps/runtime/dist/store/index.js +21 -4
  67. package/apps/runtime/dist/store/shared-lists.js +2 -3
  68. package/apps/runtime/dist/tools/index.js +17 -2
  69. package/apps/runtime/dist/tools/plugin.js +3 -2
  70. package/apps/runtime/dist/widgets/render.js +1 -1
  71. package/apps/runtime/dist/widgets/step.js +2 -1
  72. package/apps/web/dist/assets/{AboutDialog-nLn3vZAN.js → AboutDialog-i-qD_Nea.js} +1 -1
  73. package/apps/web/dist/assets/{ChangelogDialog-Cbx3mN2p.js → ChangelogDialog-BYuVcmKR.js} +1 -1
  74. package/apps/web/dist/assets/ChatView-DMEYEsA7.js +10 -0
  75. package/apps/web/dist/assets/EditorPage-Bry35WHx.js +3 -0
  76. package/apps/web/dist/assets/{FlowPreview-Bvj3R8g4.js → FlowPreview-BsfjDVv8.js} +1 -1
  77. package/apps/web/dist/assets/{Harness-D-Z3MbYq.js → Harness-BxQDTGJk.js} +2 -2
  78. package/apps/web/dist/assets/NewWizardChannels-DPnLT-md.js +1 -0
  79. package/apps/web/dist/assets/NewWizardPage-BSh_bUIJ.js +7 -0
  80. package/apps/web/dist/assets/{OnboardingPage-CdoAfnI2.js → OnboardingPage-BZrRdVwr.js} +1 -1
  81. package/apps/web/dist/assets/ProjectPage-_3L9R3SZ.js +1 -0
  82. package/apps/web/dist/assets/PublicRunner-DXbh8uhe.js +1 -0
  83. package/apps/web/dist/assets/{ReadOnly-DQPHmvS0.js → ReadOnly-DFysMEMF.js} +1 -1
  84. package/apps/web/dist/assets/RunDrawer-DpUakYb9.js +4 -0
  85. package/apps/web/dist/assets/RunnerView-DQ-NgcCa.js +1 -0
  86. package/apps/web/dist/assets/SettingsPage-DyUCIRjo.js +1 -0
  87. package/apps/web/dist/assets/SetupPage-BgVHvN6j.js +1 -0
  88. package/apps/web/dist/assets/SharePage-BI6vkc9c.js +1 -0
  89. package/apps/web/dist/assets/changelog-Cnv7ZZEL.js +1 -0
  90. package/apps/web/dist/assets/{circle-alert-DCBV7n5k.js → circle-alert-BVj6idzu.js} +1 -1
  91. package/apps/web/dist/assets/esm-hSq-jeOG.js +7 -0
  92. package/apps/web/dist/assets/index-Ca1841qE.js +14 -0
  93. package/apps/web/dist/assets/index-GSl2Rcjy.css +1 -0
  94. package/apps/web/dist/assets/{lock-DlzTfwxP.js → lock-BoWS-JhN.js} +1 -1
  95. package/apps/web/dist/assets/{phone-BFD815nc.js → phone-CeqyvwK-.js} +1 -1
  96. package/apps/web/dist/assets/settings-menu-BK8WpcGv.js +1 -0
  97. package/apps/web/dist/assets/ui-BbIh6SOJ.js +134 -0
  98. package/apps/web/dist/assets/{upload-BEf5-d6X.js → upload-BQrAlHIG.js} +1 -1
  99. package/apps/web/dist/index.html +4 -4
  100. package/apps/web/dist/mcp/flow.html +76 -76
  101. package/node_modules/@engenty-wizards/plugin-sdk/dist/index.d.ts +28 -4
  102. package/node_modules/@engenty-wizards/plugin-sdk/dist/studio.d.ts +52 -1
  103. package/node_modules/@engenty-wizards/plugin-sdk/dist/thread.js +6 -1
  104. package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +6 -2
  105. package/node_modules/@engenty-wizards/shared/dist/definition.js +13 -5
  106. package/node_modules/@engenty-wizards/shared/dist/knowledge.js +1 -1
  107. package/node_modules/@engenty-wizards/shared/dist/run.d.ts +11 -1
  108. package/node_modules/@engenty-wizards/shared/dist/run.js +11 -1
  109. package/node_modules/@engenty-wizards/shared/dist/runners.d.ts +12 -0
  110. package/node_modules/@engenty-wizards/shared/dist/store.d.ts +8 -0
  111. package/node_modules/@engenty-wizards/shared/dist/store.js +8 -0
  112. package/package.json +1 -1
  113. package/apps/web/dist/assets/ChatView-1VuHE2Q7.js +0 -10
  114. package/apps/web/dist/assets/EditorPage-Cp3jqQsk.js +0 -3
  115. package/apps/web/dist/assets/NewWizardChannels-DCF7JoL0.js +0 -1
  116. package/apps/web/dist/assets/NewWizardPage-BTlgq7yN.js +0 -7
  117. package/apps/web/dist/assets/ProjectPage-CoPSdDO9.js +0 -1
  118. package/apps/web/dist/assets/PublicRunner-CHwK3X75.js +0 -1
  119. package/apps/web/dist/assets/RunDrawer-CmGuqp0p.js +0 -4
  120. package/apps/web/dist/assets/RunnerView-DiaQiDbw.js +0 -1
  121. package/apps/web/dist/assets/SettingsPage-NzmFrJXb.js +0 -1
  122. package/apps/web/dist/assets/SetupPage-8mBuezdY.js +0 -1
  123. package/apps/web/dist/assets/SharePage-C-T5fkp1.js +0 -1
  124. package/apps/web/dist/assets/changelog-CsxPWKJ-.js +0 -1
  125. package/apps/web/dist/assets/esm-CnR8rDPi.js +0 -7
  126. package/apps/web/dist/assets/index-CmxVazOf.css +0 -1
  127. package/apps/web/dist/assets/index-D57rKyGN.js +0 -14
  128. package/apps/web/dist/assets/settings-menu-D-UqnL1p.js +0 -1
  129. package/apps/web/dist/assets/ui-DTuixH4y.js +0 -134
@@ -1,4 +1,5 @@
1
1
  import { COLOR_SECTION_KINDS, cleanAssetSections, cleanLogoSections, FACT_TYPES, FONT_USES, FONT_WEIGHTS, GRADIENT_TYPES, orderColors, PROJECT_LIMITS, RULE_MARKS, } from "@engenty-wizards/shared/projects";
2
+ import { bilingual } from "@engenty-wizards/shared/run";
2
3
  import { and, count, eq, isNull } from "drizzle-orm";
3
4
  import { nanoid } from "nanoid";
4
5
  import { z } from "zod";
@@ -154,9 +155,11 @@ async function ensureProject() {
154
155
  const any = await db.query.project.findFirst({ where: madeHere() });
155
156
  if (!any) {
156
157
  const id = nanoid(12);
157
- await db
158
- .insert(schema.project)
159
- .values({ id, tenantId: currentTenant(), name: "Meine Wizards" });
158
+ await db.insert(schema.project).values({
159
+ id,
160
+ tenantId: currentTenant(),
161
+ name: env.manage.url ? "Meine Wizards" : "Local Space",
162
+ });
160
163
  spaceChanged("space.created", id);
161
164
  }
162
165
  }
@@ -222,7 +225,9 @@ export async function createProject(_userId, name) {
222
225
  .where(eq(schema.project.tenantId, currentTenant()));
223
226
  const limit = await projectLimit();
224
227
  if (n >= limit) {
225
- throw new ServiceError("refused", limit === 1 ? "Hier gibt es ein Projekt." : `Höchstens ${limit} Projekte.`);
228
+ throw new ServiceError("refused", limit === 1
229
+ ? bilingual("Hier gibt es ein Projekt.", "There is one project here.")
230
+ : bilingual(`Höchstens ${limit} Projekte.`, `At most ${limit} projects.`));
226
231
  }
227
232
  const id = nanoid(12);
228
233
  await db
@@ -246,7 +251,7 @@ export async function updateProject(userId, projectId, patch) {
246
251
  return { ...s, headers };
247
252
  });
248
253
  if (patch.facts && new Set(patch.facts.map((f) => f.key)).size !== patch.facts.length) {
249
- throw new ServiceError("invalid", "Zwei Einträge haben denselben Schlüssel.");
254
+ throw new ServiceError("invalid", bilingual("Zwei Einträge haben denselben Schlüssel.", "Two entries have the same key."));
250
255
  }
251
256
  await db
252
257
  .update(schema.project)
@@ -360,7 +365,7 @@ export async function deleteProject(userId, projectId) {
360
365
  await requireAdmin();
361
366
  const all = await db.query.project.findMany({ where: madeHere() });
362
367
  if (all.length <= 1) {
363
- throw new ServiceError("refused", "Das letzte Projekt bleibt.");
368
+ throw new ServiceError("refused", bilingual("Das letzte Projekt bleibt.", "The last project stays."));
364
369
  }
365
370
  await removeProject(projectId);
366
371
  }
@@ -22,17 +22,32 @@ export async function runnersOf(tenantId = currentTenant()) {
22
22
  // A plugin's runner says per tenant whether it is ready (a number connected, a key set).
23
23
  const ofPlugins = await Promise.all(plugins.flatMap((p) => [...p.runners.values()].map(async (r) => ({
24
24
  ...r.info,
25
- problem: r.problem
25
+ ...problemOf(r.problem
26
26
  ? await Promise.resolve()
27
27
  .then(() => r.problem?.() ?? null)
28
28
  .catch((err) => err.message || "The runner is not ready.")
29
- : null,
29
+ : null),
30
30
  }))));
31
31
  return [
32
32
  ...BUILT_IN_RUNNERS.map((r) => CONVERSATION_RUNNERS.includes(r.id) ? { ...r, problem: talk } : r),
33
33
  ...ofPlugins,
34
34
  ];
35
35
  }
36
+ /** A runner's answer to `problem`, as the studio lists it. */
37
+ function problemOf(answer) {
38
+ if (!answer) {
39
+ return { problem: null };
40
+ }
41
+ if (typeof answer === "string") {
42
+ return { problem: answer, problemKind: "setup" };
43
+ }
44
+ return {
45
+ problem: answer.text || "The runner is not ready.",
46
+ problemKind: answer.kind ?? "setup",
47
+ problemNote: answer.note ?? null,
48
+ problemPlan: answer.kind === "plan" ? (answer.plan ?? null) : null,
49
+ };
50
+ }
36
51
  /**
37
52
  * How a wizard is offered, on the runners there are: a runner switched on that is gone (a
38
53
  * plugin the tenant lost) is left out, and the link opens the first one left.
@@ -1,11 +1,13 @@
1
1
  import { FIELD_WAYS, formatsFor, isDecidedField, shownFields, slotText, wizardLang, } from "@engenty-wizards/shared/definition";
2
+ import { bilingual } from "@engenty-wizards/shared/run";
2
3
  import { and, count, desc, eq, gte, inArray, max, or, sql } from "drizzle-orm";
3
4
  import { canSpend, MICROS_PER_CREDIT } from "../credits/credits.js";
4
5
  import { db, schema } from "../db/client.js";
5
6
  import { answerAsk } from "../engine/asks.js";
6
7
  import { emitEvent, recentEvents, watchRun } from "../engine/events.js";
8
+ import { fieldErrorsIn } from "../engine/input.js";
7
9
  import { cancel, createRun, goBack, NoCreditsError, pageContext, RunConflict, RunInputError, retry, reviewStep, submitPage, } from "../engine/runner.js";
8
- import { ModelUnavailableError } from "../model-errors.js";
10
+ import { ModelUnavailableError, saidOf } from "../model-errors.js";
9
11
  import { signedUrl } from "../secrets/signing.js";
10
12
  import { currentTenant } from "../tenants/tenant.js";
11
13
  import { notFound, ServiceError } from "./errors.js";
@@ -16,7 +18,7 @@ export async function startTestRun(userId, wizardId, autopilot) {
16
18
  const w = await ownedWizard(userId, wizardId);
17
19
  const { definition, files } = await requireClean(w);
18
20
  if (!(await canSpend())) {
19
- throw new ServiceError("no_credits", "Dein Guthaben ist aufgebraucht.");
21
+ throw new ServiceError("no_credits", bilingual("Dein Guthaben ist aufgebraucht.", "Your credits are used up."));
20
22
  }
21
23
  const runId = await createRun({
22
24
  wizardId: w.id,
@@ -27,7 +29,7 @@ export async function startTestRun(userId, wizardId, autopilot) {
27
29
  userId,
28
30
  unattended: Boolean(autopilot),
29
31
  }).catch((err) => {
30
- throw err instanceof ModelUnavailableError ? new ServiceError("refused", err.message) : err;
32
+ throw err instanceof ModelUnavailableError ? new ServiceError("refused", saidOf(err)) : err;
31
33
  });
32
34
  if (autopilot) {
33
35
  fly(runId, autopilot.answers, autopilot.acceptReviews);
@@ -78,9 +80,14 @@ function fly(runId, answers, acceptReviews) {
78
80
  } while (again);
79
81
  }
80
82
  catch (err) {
83
+ const answers = (lang) => err instanceof RunInputError
84
+ ? fieldErrorsIn(err.errors, lang)
85
+ .map((e) => `${e.field} – ${e.message}`)
86
+ .join("; ")
87
+ : "";
81
88
  const message = err instanceof RunInputError
82
- ? `Antworten fehlen oder passen nicht: ${err.errors.map((e) => `${e.field} – ${e.message}`).join("; ")}`
83
- : err.message;
89
+ ? bilingual(`Antworten fehlen oder passen nicht: ${answers("de")}`, `Answers are missing or do not fit: ${answers("en")}`)
90
+ : saidOf(err);
84
91
  await emitEvent(runId, null, "error", message).catch(() => undefined);
85
92
  land();
86
93
  }
@@ -347,10 +354,10 @@ export const testRunReport = (userId, runId, waitSeconds = 0) => runReport(userI
347
354
  /** A command the run refused, as a tool error the client can act on. */
348
355
  export function refusedInput(err) {
349
356
  if (err instanceof RunInputError) {
350
- throw new ServiceError("invalid", err.message, { fields: err.errors });
357
+ throw new ServiceError("invalid", saidOf(err), { fields: fieldErrorsIn(err.errors, "en") });
351
358
  }
352
359
  if (err instanceof RunConflict) {
353
- throw new ServiceError("refused", err.message);
360
+ throw new ServiceError("refused", saidOf(err));
354
361
  }
355
362
  throw err;
356
363
  }
@@ -370,7 +377,7 @@ export async function startRun(userId, wizardId, answers = {}) {
370
377
  throw notFound();
371
378
  }
372
379
  if (!(await canSpend())) {
373
- throw new ServiceError("no_credits", "Dein Guthaben ist aufgebraucht.");
380
+ throw new ServiceError("no_credits", bilingual("Dein Guthaben ist aufgebraucht.", "Your credits are used up."));
374
381
  }
375
382
  const runId = await createRun({
376
383
  wizardId: w.id,
@@ -381,10 +388,10 @@ export async function startRun(userId, wizardId, answers = {}) {
381
388
  userId,
382
389
  }).catch((err) => {
383
390
  if (err instanceof ModelUnavailableError) {
384
- throw new ServiceError("refused", err.message);
391
+ throw new ServiceError("refused", saidOf(err));
385
392
  }
386
393
  if (err instanceof NoCreditsError) {
387
- throw new ServiceError("no_credits", "Dein Guthaben ist aufgebraucht.");
394
+ throw new ServiceError("no_credits", bilingual("Dein Guthaben ist aufgebraucht.", "Your credits are used up."));
388
395
  }
389
396
  throw err;
390
397
  });
@@ -400,7 +407,7 @@ export async function startRun(userId, wizardId, answers = {}) {
400
407
  }
401
408
  catch (err) {
402
409
  if (err instanceof RunInputError) {
403
- return { runId, refused: err.errors };
410
+ return { runId, refused: fieldErrorsIn(err.errors, "en") };
404
411
  }
405
412
  throw err;
406
413
  }
@@ -1,3 +1,4 @@
1
+ import { bilingual } from "@engenty-wizards/shared/run";
1
2
  import { and, eq, isNotNull, lt } from "drizzle-orm";
2
3
  import { nanoid } from "nanoid";
3
4
  import { db, schema } from "../db/client.js";
@@ -12,7 +13,7 @@ export const shareUrlFor = (token) => `${env.publicUrl}/s/${token}`;
12
13
  /** Shares a finished run. Idempotent: the same run keeps the same link. */
13
14
  export async function shareRun(run) {
14
15
  if (run.status !== "done") {
15
- throw new ServiceError("refused", "Nur fertige Ergebnisse lassen sich teilen.");
16
+ throw new ServiceError("refused", bilingual("Nur fertige Ergebnisse lassen sich teilen.", "Only finished results can be shared."));
16
17
  }
17
18
  let token = run.shareToken;
18
19
  if (!token) {
@@ -1,8 +1,10 @@
1
1
  import { factKey, PROJECT_LIMITS } from "@engenty-wizards/shared/projects";
2
+ import { bilingual } from "@engenty-wizards/shared/run";
2
3
  import { nanoid } from "nanoid";
3
4
  import { z } from "zod";
4
5
  import { runStudioAssistant } from "../agents/studio-assistant.js";
5
6
  import { canSpend } from "../credits/credits.js";
7
+ import { errorText } from "../model-errors.js";
6
8
  import { currentTenant } from "../tenants/tenant.js";
7
9
  import { projectWritable, requireWritable } from "./access.js";
8
10
  import { apiSpace, apiSpaceDetails } from "./api.js";
@@ -55,6 +57,8 @@ export const siteSchema = z.object({
55
57
  /** Let the space assistant fill in about and facts; false writes only what the site states. */
56
58
  assist: z.boolean().default(true),
57
59
  });
60
+ /** The language the site's people read: German for a German site, else English. */
61
+ const siteLang = (input) => input.language?.toLowerCase().startsWith("de") ? "de" : "en";
58
62
  const jobs = new Map();
59
63
  const KEEP_MS = 60 * 60 * 1000;
60
64
  function forget() {
@@ -146,16 +150,16 @@ function assistMessage(input) {
146
150
  .slice(0, 6)
147
151
  .map((p) => `## ${p.title}${p.url ? ` (${p.url})` : ""}${p.role ? ` [${p.role}]` : ""}\n\n${p.markdown.slice(0, 6000)}`);
148
152
  return [
149
- `Das ist die WordPress-Website ${input.url}. Fülle den Space aus dem, was sie über sich sagt: Titel, eine Beschreibung (about), Fakten (Adresse, Telefon, E-Mail, UID, Firmenbuch, Öffnungszeiten, wer sie betreibt) und die Farben, falls noch keine da sind. Ihre Seiten stehen schon in Wissen; schreib sie nicht ab.`,
150
- "Die Angaben unten kommen direkt aus WordPress. Lies die Website mit read_website nur, wenn etwas Wichtiges fehlt. Erfinde nichts.",
151
- `Name: ${input.name || "—"}\nUntertitel: ${input.tagline || "—"}\nSprache: ${input.language ?? "—"}`,
153
+ `This is the WordPress website ${input.url}. Fill the space from what it says about itself: a title, a description (about), facts (address, phone, email, VAT id, company register, opening hours, who runs it) and the colours, if there are none yet. Its pages are already in Knowledge; do not copy them. Write in the website's own language.`,
154
+ "The details below come straight from WordPress. Read the website with read_website only when something important is missing. Invent nothing.",
155
+ `Name: ${input.name || "—"}\nTagline: ${input.tagline || "—"}\nLanguage: ${input.language ?? "—"}`,
152
156
  input.facts.length
153
- ? `Angaben der Website:\n${input.facts.map((f) => `- ${f.label}: ${f.value}`).join("\n")}`
157
+ ? `What the website states:\n${input.facts.map((f) => `- ${f.label}: ${f.value}`).join("\n")}`
154
158
  : "",
155
159
  input.colors.length
156
- ? `Farben des Themes: ${input.colors.map((c) => `${c.name} ${c.value}`).join(", ")}`
160
+ ? `The theme's colours: ${input.colors.map((c) => `${c.name} ${c.value}`).join(", ")}`
157
161
  : "",
158
- pages.length ? `Seiten:\n\n${pages.join("\n\n")}` : "",
162
+ pages.length ? `Pages:\n\n${pages.join("\n\n")}` : "",
159
163
  ]
160
164
  .filter(Boolean)
161
165
  .join("\n\n")
@@ -163,7 +167,10 @@ function assistMessage(input) {
163
167
  }
164
168
  async function work(job, projectId, input) {
165
169
  try {
166
- job.activity = "Übernimmt die Angaben der Website …";
170
+ job.activity =
171
+ siteLang(input) === "de"
172
+ ? "Übernimmt die Angaben der Website …"
173
+ : "Taking over what the website says …";
167
174
  await fillFromSite(job.userId, projectId, input);
168
175
  job.activity = "Schreibt die Seiten in Wissen …";
169
176
  job.pages = await writePages(projectId, input);
@@ -181,7 +188,7 @@ async function work(job, projectId, input) {
181
188
  message: assistMessage(input),
182
189
  page: null,
183
190
  browserTools: [],
184
- lang: input.language?.toLowerCase().startsWith("en") ? "en" : "de",
191
+ lang: siteLang(input),
185
192
  onActivity: (label) => {
186
193
  job.activity = label;
187
194
  },
@@ -201,7 +208,11 @@ async function work(job, projectId, input) {
201
208
  catch (err) {
202
209
  job.status = "failed";
203
210
  job.error =
204
- err instanceof ServiceError ? err.message : "Die Website ließ sich nicht übernehmen.";
211
+ err instanceof ServiceError
212
+ ? errorText(err, siteLang(input))
213
+ : siteLang(input) === "de"
214
+ ? "Die Website ließ sich nicht übernehmen."
215
+ : "The website could not be taken over.";
205
216
  console.error("[site]", err);
206
217
  }
207
218
  }
@@ -256,7 +267,7 @@ function siteJobView(job) {
256
267
  export function siteJob(jobId) {
257
268
  const job = jobs.get(jobId);
258
269
  if (!job || job.tenantId !== currentTenant()) {
259
- throw new ServiceError("not_found", "Diesen Vorgang gibt es nicht (mehr).");
270
+ throw new ServiceError("not_found", bilingual("Diesen Vorgang gibt es nicht (mehr).", "There is no such job (any more)."));
260
271
  }
261
272
  return siteJobView(job);
262
273
  }
@@ -1,4 +1,5 @@
1
1
  import { CATEGORY_TYPES, dayOf, } from "@engenty-wizards/shared/knowledge";
2
+ import { bilingual } from "@engenty-wizards/shared/run";
2
3
  import { and, asc, eq, inArray, isNotNull, or, sql } from "drizzle-orm";
3
4
  import { nanoid } from "nanoid";
4
5
  import { db, schema } from "../db/client.js";
@@ -272,10 +273,10 @@ const MAX_CATEGORIES = 60;
272
273
  function checkDraft(draft) {
273
274
  const name = draft.name.replace(/\s+/g, " ").trim().slice(0, 80);
274
275
  if (!name) {
275
- throw new ServiceError("invalid", "Eine Kategorie braucht einen Namen.");
276
+ throw new ServiceError("invalid", bilingual("Eine Kategorie braucht einen Namen.", "A category needs a name."));
276
277
  }
277
278
  if (!CATEGORY_TYPES.includes(draft.type)) {
278
- throw new ServiceError("invalid", `Unbekannter Typ „${draft.type}“.`);
279
+ throw new ServiceError("invalid", bilingual(`Unbekannter Typ „${draft.type}“.`, `Unknown type “${draft.type}”.`));
279
280
  }
280
281
  return name;
281
282
  }
@@ -329,7 +330,7 @@ export async function putCategory(projectId, draft, opts = {}) {
329
330
  return known;
330
331
  }
331
332
  if (rows.length >= MAX_CATEGORIES) {
332
- throw new ServiceError("refused", `Ein Space hat höchstens ${MAX_CATEGORIES} Kategorien.`);
333
+ throw new ServiceError("refused", bilingual(`Ein Space hat höchstens ${MAX_CATEGORIES} Kategorien.`, `A space has at most ${MAX_CATEGORIES} categories.`));
333
334
  }
334
335
  const id = nanoid(12);
335
336
  const [row] = await db
@@ -408,7 +409,7 @@ export async function updateCategory(categoryId, patch) {
408
409
  if (name !== row.name) {
409
410
  const clash = byName(await categoryRows(row.projectId), name);
410
411
  if (clash && clash.id !== row.id) {
411
- throw new ServiceError("invalid", `Die Kategorie „${name}“ gibt es schon.`);
412
+ throw new ServiceError("invalid", bilingual(`Die Kategorie „${name}“ gibt es schon.`, `The category “${name}” already exists.`));
412
413
  }
413
414
  }
414
415
  await db
@@ -458,7 +459,7 @@ export async function addCategoryValue(categoryId, value) {
458
459
  const category = await categoryRow(categoryId);
459
460
  await requireWritableProject(category.projectId);
460
461
  if (category.type !== "choice") {
461
- throw new ServiceError("invalid", "Nur eine Auswahl hat Werte.");
462
+ throw new ServiceError("invalid", bilingual("Nur eine Auswahl hat Werte.", "Only a choice has values."));
462
463
  }
463
464
  const ids = await addValues(category.id, [value]);
464
465
  return { id: [...ids.values()].at(-1) };
@@ -488,7 +489,7 @@ export async function mergeCategoryValues(fromId, intoId) {
488
489
  const from = await valueRow(fromId);
489
490
  const into = await valueRow(intoId);
490
491
  if (from.category.id !== into.category.id || fromId === intoId) {
491
- throw new ServiceError("invalid", "Nur Werte einer Kategorie lassen sich zusammenführen.");
492
+ throw new ServiceError("invalid", bilingual("Nur Werte einer Kategorie lassen sich zusammenführen.", "Only values of one category can be merged."));
492
493
  }
493
494
  await requireWritableProject(from.category.projectId);
494
495
  const t = schema.spaceItemCategory;
@@ -1,9 +1,11 @@
1
1
  import { TableColumnValueError, tableColumnsSchema, } from "@engenty-wizards/shared/engenty/data-tables";
2
+ import { bilingual } from "@engenty-wizards/shared/run";
2
3
  import { editCells, SPACE_DATA_LIMITS, } from "@engenty-wizards/shared/space-data";
3
4
  import { rowKey } from "@engenty-wizards/shared/store";
4
5
  import { and, asc, count, desc, eq, inArray, isNotNull, isNull, max } from "drizzle-orm";
5
6
  import { nanoid } from "nanoid";
6
7
  import { db, schema } from "../db/client.js";
8
+ import { saidOf } from "../model-errors.js";
7
9
  import { currentTenant } from "../tenants/tenant.js";
8
10
  import { projectWritable, requireWritableProject } from "./access.js";
9
11
  import { notFound, ServiceError } from "./errors.js";
@@ -21,20 +23,20 @@ async function ownerOf(projectId, wizardId) {
21
23
  columns: { id: true },
22
24
  });
23
25
  if (!w) {
24
- throw new ServiceError("invalid", "Diesen Wizard gibt es im Space nicht.");
26
+ throw new ServiceError("invalid", bilingual("Diesen Wizard gibt es im Space nicht.", "There is no such wizard in the space."));
25
27
  }
26
28
  return w.id;
27
29
  }
28
30
  function cellError(err) {
29
31
  if (err instanceof TableColumnValueError) {
30
- throw new ServiceError("invalid", err.message, { column: err.columnId });
32
+ throw new ServiceError("invalid", saidOf(err), { column: err.columnId });
31
33
  }
32
34
  throw err;
33
35
  }
34
36
  export function checkColumns(columns) {
35
37
  const parsed = tableColumnsSchema.safeParse(columns);
36
38
  if (!parsed.success) {
37
- throw new ServiceError("invalid", parsed.error.issues[0]?.message ?? "Ungültige Spalten.");
39
+ throw new ServiceError("invalid", parsed.error.issues[0]?.message ?? bilingual("Ungültige Spalten.", "Invalid columns."));
38
40
  }
39
41
  return parsed.data;
40
42
  }
@@ -102,10 +104,11 @@ async function tableCount(projectId) {
102
104
  .from(schema.spaceTable)
103
105
  .where(eq(schema.spaceTable.projectId, projectId));
104
106
  if (n >= SPACE_DATA_LIMITS.tables) {
105
- throw new ServiceError("refused", `Ein Space hat höchstens ${SPACE_DATA_LIMITS.tables} Tabellen.`);
107
+ throw new ServiceError("refused", bilingual(`Ein Space hat höchstens ${SPACE_DATA_LIMITS.tables} Tabellen.`, `A space has at most ${SPACE_DATA_LIMITS.tables} tables.`));
106
108
  }
107
109
  }
108
- export async function createTable(userId, projectId, input) {
110
+ export async function createTable(userId, projectId, input, lang = "en") {
111
+ const de = lang === "de";
109
112
  const project = await ownedProject(userId, projectId);
110
113
  await requireWritableProject(project.id);
111
114
  await tableCount(project.id);
@@ -113,10 +116,10 @@ export async function createTable(userId, projectId, input) {
113
116
  ? checkColumns(input.columns)
114
117
  : input.format === "faq"
115
118
  ? [
116
- { id: "frage", name: "Frage", type: "text" },
119
+ { id: "question", name: de ? "Frage" : "Question", type: "text" },
117
120
  {
118
- id: "antwort",
119
- name: "Antwort",
121
+ id: "answer",
122
+ name: de ? "Antwort" : "Answer",
120
123
  type: "text",
121
124
  format: { style: "multiline" },
122
125
  },
@@ -124,7 +127,7 @@ export async function createTable(userId, projectId, input) {
124
127
  : [{ id: "name", name: "Name", type: "text" }];
125
128
  const id = nanoid(12);
126
129
  const wizardId = await ownerOf(project.id, input.wizardId);
127
- const title = input.title.trim() || "Tabelle";
130
+ const title = input.title.trim() || (de ? "Tabelle" : "Table");
128
131
  await db.insert(schema.spaceTable).values({
129
132
  id,
130
133
  tenantId: currentTenant(),
@@ -184,7 +187,7 @@ export async function getTable(userId, tableId) {
184
187
  export async function updateTable(_userId, tableId, patch) {
185
188
  const table = await writableTable(tableId);
186
189
  if (table.list && (patch.columns !== undefined || patch.wizardId !== undefined)) {
187
- throw new ServiceError("refused", "Diese Tabelle gehört zu einer Liste des Wizards: ihre Spalten kommen aus dem Wizard.");
190
+ throw new ServiceError("refused", bilingual("Diese Tabelle gehört zu einer Liste des Wizards: ihre Spalten kommen aus dem Wizard.", "This table belongs to one of the wizard's lists: its columns come from the wizard."));
188
191
  }
189
192
  const title = patch.title !== undefined ? patch.title.trim() || table.title : table.title;
190
193
  const wizardId = patch.wizardId !== undefined ? await ownerOf(table.projectId, patch.wizardId) : table.wizardId;
@@ -247,7 +250,7 @@ async function refuseClash(tableId, key, rowId) {
247
250
  columns: { id: true },
248
251
  });
249
252
  if (other && other.id !== rowId) {
250
- throw new ServiceError("invalid", "Diesen Eintrag gibt es schon.");
253
+ throw new ServiceError("invalid", bilingual("Diesen Eintrag gibt es schon.", "This entry already exists."));
251
254
  }
252
255
  }
253
256
  /** After a row of a table of Wissen changed: its Kategorien from its columns, its unit. */
@@ -266,7 +269,7 @@ export async function addTableRow(_userId, tableId, cells = {}) {
266
269
  .from(schema.spaceTableRow)
267
270
  .where(eq(schema.spaceTableRow.tableId, table.id));
268
271
  if (n >= SPACE_DATA_LIMITS.rowsPerTable) {
269
- throw new ServiceError("refused", `Eine Tabelle hat höchstens ${SPACE_DATA_LIMITS.rowsPerTable} Zeilen.`);
272
+ throw new ServiceError("refused", bilingual(`Eine Tabelle hat höchstens ${SPACE_DATA_LIMITS.rowsPerTable} Zeilen.`, `A table has at most ${SPACE_DATA_LIMITS.rowsPerTable} rows.`));
270
273
  }
271
274
  let value;
272
275
  try {
@@ -331,7 +334,7 @@ async function pageRow(pageId) {
331
334
  }
332
335
  function checkMarkdown(markdown) {
333
336
  if (markdown.length > SPACE_DATA_LIMITS.pageChars) {
334
- throw new ServiceError("refused", `Eine Seite hat höchstens ${SPACE_DATA_LIMITS.pageChars.toLocaleString("de")} Zeichen.`);
337
+ throw new ServiceError("refused", bilingual(`Eine Seite hat höchstens ${SPACE_DATA_LIMITS.pageChars.toLocaleString("de")} Zeichen.`, `A page has at most ${SPACE_DATA_LIMITS.pageChars.toLocaleString("en")} characters.`));
335
338
  }
336
339
  return markdown;
337
340
  }
@@ -341,7 +344,7 @@ async function pageCount(projectId, more = 1) {
341
344
  .from(schema.spacePage)
342
345
  .where(eq(schema.spacePage.projectId, projectId));
343
346
  if (n + more > SPACE_DATA_LIMITS.pages) {
344
- throw new ServiceError("refused", `Ein Space hat höchstens ${SPACE_DATA_LIMITS.pages} Seiten.`);
347
+ throw new ServiceError("refused", bilingual(`Ein Space hat höchstens ${SPACE_DATA_LIMITS.pages} Seiten.`, `A space has at most ${SPACE_DATA_LIMITS.pages} pages.`));
345
348
  }
346
349
  }
347
350
  /** A page of the space's own that a sub-page may stand under: never one of its own sub-pages. */
@@ -354,11 +357,11 @@ async function parentOf(projectId, parentId, pageId) {
354
357
  columns: { id: true, wizardId: true, parentId: true },
355
358
  });
356
359
  if (!parent || parent.wizardId) {
357
- throw new ServiceError("invalid", "Unter diese Seite kann keine Seite.");
360
+ throw new ServiceError("invalid", bilingual("Unter diese Seite kann keine Seite.", "No page can go under this page."));
358
361
  }
359
362
  for (let at = parent.id; at && pageId;) {
360
363
  if (at === pageId) {
361
- throw new ServiceError("invalid", "Eine Seite kann nicht unter sich selbst stehen.");
364
+ throw new ServiceError("invalid", bilingual("Eine Seite kann nicht unter sich selbst stehen.", "A page cannot stand under itself."));
362
365
  }
363
366
  const up = await db.query.spacePage.findFirst({
364
367
  where: eq(schema.spacePage.id, at),
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { tableColumnsSchema } from "@engenty-wizards/shared/engenty/data-tables";
3
3
  import { CATEGORY_TYPES } from "@engenty-wizards/shared/knowledge";
4
4
  import { PROJECT_LIMITS } from "@engenty-wizards/shared/projects";
5
+ import { bilingual } from "@engenty-wizards/shared/run";
5
6
  import { SPACE_DATA_LIMITS } from "@engenty-wizards/shared/space-data";
6
7
  import { WORKSPACE_LIMITS } from "@engenty-wizards/shared/workspace";
7
8
  import { and, count, eq, inArray, isNull, notInArray, sql } from "drizzle-orm";
@@ -150,8 +151,8 @@ async function requireRoom(spaceId) {
150
151
  const limit = await projectLimit();
151
152
  if (others.length >= limit) {
152
153
  throw new ServiceError("refused", limit === 1
153
- ? "Dieses Konto hat hier schon ein Projekt."
154
- : `Dieses Konto hat hier schon ${limit} Projekte.`, {
154
+ ? bilingual("Dieses Konto hat hier schon ein Projekt.", "This account already has a project here.")
155
+ : bilingual(`Dieses Konto hat hier schon ${limit} Projekte.`, `This account already has ${limit} projects here.`), {
155
156
  reason: "space_limit",
156
157
  limit,
157
158
  spaces: others.filter((p) => p.origin === "local").map((p) => ({ id: p.id, name: p.name })),
@@ -165,7 +166,7 @@ async function requireWizardRoom(spaceId) {
165
166
  .from(schema.wizard)
166
167
  .where(eq(schema.wizard.projectId, spaceId));
167
168
  if ((row?.n ?? 0) >= env.limits.syncedWizards) {
168
- throw new ServiceError("refused", `Hier haben höchstens ${env.limits.syncedWizards} veröffentlichte Wizards Platz.`, { reason: "wizard_limit", limit: env.limits.syncedWizards });
169
+ throw new ServiceError("refused", bilingual(`Hier haben höchstens ${env.limits.syncedWizards} veröffentlichte Wizards Platz.`, `There is room for at most ${env.limits.syncedWizards} published wizards here.`), { reason: "wizard_limit", limit: env.limits.syncedWizards });
169
170
  }
170
171
  }
171
172
  const hashOf = (data) => createHash("sha256").update(data).digest("hex");
@@ -199,7 +200,7 @@ async function syncLogo(projectId, logo) {
199
200
  async function syncSpace(spaceId, space) {
200
201
  const existing = await syncedProject(spaceId);
201
202
  if (existing && existing.origin !== "local") {
202
- throw new ServiceError("refused", "Ein Projekt mit dieser Kennung wurde hier angelegt.", {
203
+ throw new ServiceError("refused", bilingual("Ein Projekt mit dieser Kennung wurde hier angelegt.", "A project with this id was created here."), {
203
204
  reason: "id_taken",
204
205
  });
205
206
  }
@@ -250,7 +251,7 @@ export async function syncWizard(rawSpaceId, rawWizardId, input) {
250
251
  where: and(eq(schema.wizard.id, wizardId), eq(schema.wizard.tenantId, currentTenant())),
251
252
  });
252
253
  if (existing && existing.projectId !== spaceId) {
253
- throw new ServiceError("refused", "Ein Wizard mit dieser Kennung gehört hier woanders hin.", {
254
+ throw new ServiceError("refused", bilingual("Ein Wizard mit dieser Kennung gehört hier woanders hin.", "A wizard with this id belongs elsewhere here."), {
254
255
  reason: "id_taken",
255
256
  });
256
257
  }
@@ -506,7 +507,7 @@ export async function syncSpaceData(rawSpaceId, input) {
506
507
  : [],
507
508
  ]);
508
509
  if ([...tablesHere, ...pagesHere].some((x) => x.projectId !== spaceId || x.wizardId !== null)) {
509
- throw new ServiceError("refused", "Eine Kennung ist hier schon vergeben.", {
510
+ throw new ServiceError("refused", bilingual("Eine Kennung ist hier schon vergeben.", "An id is already taken here."), {
510
511
  reason: "id_taken",
511
512
  });
512
513
  }
@@ -584,7 +585,7 @@ async function syncCategories(spaceId, categories, assignments) {
584
585
  })
585
586
  : [];
586
587
  if (taken.some((c) => c.projectId !== spaceId)) {
587
- throw new ServiceError("refused", "Eine Kennung ist hier schon vergeben.", {
588
+ throw new ServiceError("refused", bilingual("Eine Kennung ist hier schon vergeben.", "An id is already taken here."), {
588
589
  reason: "id_taken",
589
590
  });
590
591
  }
@@ -1,11 +1,13 @@
1
1
  import { versionChanges } from "@engenty-wizards/shared/changes";
2
2
  import { pluginToolOf, shapeIssues, validateWizard, wizardSchema, } from "@engenty-wizards/shared/definition";
3
+ import { bilingual } from "@engenty-wizards/shared/run";
3
4
  import { and, desc, eq, sql } from "drizzle-orm";
4
5
  import { nanoid } from "nanoid";
5
6
  import { applyOps, OpError } from "../authoring/ops.js";
6
7
  import { listConnectors } from "../connectors/external.js";
7
8
  import { db, schema } from "../db/client.js";
8
9
  import { env } from "../env.js";
10
+ import { saidOf } from "../model-errors.js";
9
11
  import { pluginToolsOf } from "../plugins/registry.js";
10
12
  import { dropLinks, putLink } from "../tenants/control.js";
11
13
  import { currentTenant } from "../tenants/tenant.js";
@@ -17,21 +19,33 @@ import { forgetWizardLinks } from "./links.js";
17
19
  import { asLang, countInstall, marketplaceWizard } from "./marketplace.js";
18
20
  import { defaultProject, ownedProject } from "./projects.js";
19
21
  import { checkRunnerSettings } from "./runners.js";
20
- const BLANK = {
21
- version: 1,
22
- title: "Neuer Wizard",
23
- description: "",
24
- avatar: "round",
25
- steps: [
26
- {
27
- id: "start",
28
- type: "page",
29
- title: "Los geht's",
30
- fields: [{ id: "input", label: "Worum geht es?", kind: "textarea", required: true }],
31
- },
32
- { id: "done", type: "result", title: "Fertig", deliverables: [] },
33
- ],
34
- };
22
+ /** The wizard a new one starts as, in the language of whoever makes it. */
23
+ function blankWizard(lang) {
24
+ const de = lang === "de";
25
+ return {
26
+ version: 1,
27
+ title: de ? "Neuer Wizard" : "New wizard",
28
+ description: "",
29
+ avatar: "round",
30
+ lang,
31
+ steps: [
32
+ {
33
+ id: "start",
34
+ type: "page",
35
+ title: de ? "Los geht's" : "Let's start",
36
+ fields: [
37
+ {
38
+ id: "input",
39
+ label: de ? "Worum geht es?" : "What is it about?",
40
+ kind: "textarea",
41
+ required: true,
42
+ },
43
+ ],
44
+ },
45
+ { id: "done", type: "result", title: de ? "Fertig" : "Done", deliverables: [] },
46
+ ],
47
+ };
48
+ }
35
49
  export const studioUrl = (wizardId) => `${env.appUrl}/studio/edit/${wizardId}`;
36
50
  export const shareUrl = (token) => `${env.publicUrl}/w/${token}`;
37
51
  const newShareToken = () => nanoid(14);
@@ -227,9 +241,16 @@ export async function createWizard(userId, input, writer = { source: "studio" })
227
241
  if (input.starterId && !starter) {
228
242
  throw new ServiceError("invalid", `There is no starter "${input.starterId}".`);
229
243
  }
244
+ const maker = input.lang?.toLowerCase().startsWith("de") ? "de" : "en";
230
245
  const { draft, issues } = input.definition !== undefined
231
246
  ? parseDraft(input.definition)
232
- : { draft: starter?.definition ?? structuredClone(BLANK), issues: [] };
247
+ : starter
248
+ ? // A starter names the language it is written in, where its definition does not.
249
+ {
250
+ draft: { ...starter.definition, lang: starter.definition.lang ?? starter.language },
251
+ issues: [],
252
+ }
253
+ : { draft: blankWizard(maker), issues: [] };
233
254
  const fromStarter = input.definition === undefined ? starter : undefined;
234
255
  const id = nanoid(12);
235
256
  const shareToken = newShareToken();
@@ -287,7 +308,7 @@ export async function writeDraft(userId, wizardId, input, writer = { source: "st
287
308
  .returning({ revision: schema.wizard.revision });
288
309
  if (!row) {
289
310
  const current = await ownedWizard(userId, wizardId);
290
- throw new ServiceError("revision_conflict", "Der Wizard wurde inzwischen geändert.", {
311
+ throw new ServiceError("revision_conflict", bilingual("Der Wizard wurde inzwischen geändert.", "The wizard has been changed in the meantime."), {
291
312
  revision: current.revision,
292
313
  });
293
314
  }
@@ -308,7 +329,7 @@ export async function writeDraft(userId, wizardId, input, writer = { source: "st
308
329
  export async function editWizard(userId, wizardId, input, writer) {
309
330
  const w = await writableWizard(userId, wizardId);
310
331
  if (w.revision !== input.baseRevision) {
311
- throw new ServiceError("revision_conflict", "Der Wizard wurde inzwischen geändert.", {
332
+ throw new ServiceError("revision_conflict", bilingual("Der Wizard wurde inzwischen geändert.", "The wizard has been changed in the meantime."), {
312
333
  revision: w.revision,
313
334
  });
314
335
  }
@@ -318,7 +339,7 @@ export async function editWizard(userId, wizardId, input, writer) {
318
339
  }
319
340
  catch (err) {
320
341
  if (err instanceof OpError) {
321
- throw new ServiceError("invalid", err.message);
342
+ throw new ServiceError("invalid", saidOf(err));
322
343
  }
323
344
  throw err;
324
345
  }
@@ -380,7 +401,7 @@ export async function requireClean(w) {
380
401
  const files = await draftFiles(w.id);
381
402
  const issues = await checkDraft(w.projectId, w.draft, files.map((f) => f.path));
382
403
  if (issues.length) {
383
- throw new ServiceError("has_issues", "Der Wizard hat noch Fehler.", { issues });
404
+ throw new ServiceError("has_issues", bilingual("Der Wizard hat noch Fehler.", "The wizard still has errors."), { issues });
384
405
  }
385
406
  return { definition: w.draft, files };
386
407
  }