wizards 0.2.1 → 0.2.3
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 +4 -1
- package/apps/runtime/dist/app.js +44 -0
- package/apps/runtime/dist/auth/account.js +16 -2
- package/apps/runtime/dist/auth/index.js +14 -1
- package/apps/runtime/dist/connectors/external.js +5 -0
- package/apps/runtime/dist/db/control-schema.js +12 -3
- package/apps/runtime/dist/db/migrations/control/0005_chilly_oracle.sql +1 -0
- package/apps/runtime/dist/db/migrations/control/0006_sleepy_mojo.sql +1 -0
- package/apps/runtime/dist/db/migrations/control/meta/0005_snapshot.json +358 -0
- package/apps/runtime/dist/db/migrations/control/meta/0006_snapshot.json +365 -0
- package/apps/runtime/dist/db/migrations/control/meta/_journal.json +14 -0
- package/apps/runtime/dist/db/migrations/tenant/0005_reflective_forgotten_one.sql +2 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0005_snapshot.json +1659 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/_journal.json +7 -0
- package/apps/runtime/dist/db/schema.js +6 -0
- package/apps/runtime/dist/engine/asks.js +2 -0
- package/apps/runtime/dist/engine/push.js +58 -0
- package/apps/runtime/dist/engine/runner.js +6 -0
- package/apps/runtime/dist/env.js +14 -1
- package/apps/runtime/dist/limits.js +21 -0
- package/apps/runtime/dist/manage.js +16 -3
- package/apps/runtime/dist/mcp/tools.js +4 -2
- package/apps/runtime/dist/plugins/loader.js +23 -1
- package/apps/runtime/dist/routes/api.js +96 -6
- package/apps/runtime/dist/routes/runs.js +68 -2
- package/apps/runtime/dist/routes/studio.js +79 -11
- package/apps/runtime/dist/services/access.js +62 -0
- package/apps/runtime/dist/services/architect.js +3 -2
- package/apps/runtime/dist/services/cloud.js +241 -39
- package/apps/runtime/dist/services/errors.js +1 -0
- package/apps/runtime/dist/services/files.js +3 -3
- package/apps/runtime/dist/services/project-files.js +6 -0
- package/apps/runtime/dist/services/projects.js +38 -14
- package/apps/runtime/dist/services/spaces.js +400 -0
- package/apps/runtime/dist/services/wizards.js +19 -8
- package/apps/runtime/dist/tenants/control.js +47 -0
- package/apps/web/dist/assets/{ChangelogDialog-Co_9fs61.js → ChangelogDialog-CbZBUMRu.js} +1 -1
- package/apps/web/dist/assets/ChatPanel-zurtfj6A.js +1 -0
- package/apps/web/dist/assets/{CreditsDialog-D3fZRqXH.js → CreditsDialog-BewZHZTR.js} +1 -1
- package/apps/web/dist/assets/EditorPage-B7ZOoW5h.js +2 -0
- package/apps/web/dist/assets/{FlowPreview-CjUK5046.js → FlowPreview-DHrhPps0.js} +1 -1
- package/apps/web/dist/assets/HomePage-CBvWxXsq.js +1 -0
- package/apps/web/dist/assets/ImportWizard-CKjVJMaG.js +1 -0
- package/apps/web/dist/assets/{LocalRuntime-CTyLGC98.js → LocalRuntime-Brkvbzzg.js} +10 -10
- package/apps/web/dist/assets/NewWizardPage-B3XePMlp.js +2 -0
- package/apps/web/dist/assets/PublicRunner-Bn1g6HG9.js +1 -0
- package/apps/web/dist/assets/RunnerView-CO1Nk3jn.js +2 -0
- package/apps/web/dist/assets/SettingsPage-BIY4kHZl.js +2 -0
- package/apps/web/dist/assets/{SetupPage-CfXTOaIm.js → SetupPage-BvLwZH9i.js} +1 -1
- package/apps/web/dist/assets/{SharePage-CgByqrAF.js → SharePage-CDESJ11B.js} +1 -1
- package/apps/web/dist/assets/{api-KMi35npD.js → api-9sHuNTkp.js} +1 -1
- package/apps/web/dist/assets/changelog-Cto0j5-V.js +1 -0
- package/apps/web/dist/assets/index-CIwJmOjo.css +1 -0
- package/apps/web/dist/assets/{index-BmpnE7IO.js → index-VsRgvimv.js} +3 -3
- package/apps/web/dist/assets/{meta-4R1Lnr25.js → meta-B4mZqEso.js} +1 -1
- package/apps/web/dist/assets/oss-credits-Hu4LieLA.js +1 -0
- package/apps/web/dist/assets/outputs-boPx5ngv.js +69 -0
- package/apps/web/dist/assets/{session-B6-6VomQ.js → session-CJGuo1Tv.js} +1 -1
- package/apps/web/dist/assets/type-BMJpS6_D.js +1 -0
- package/apps/web/dist/assets/ui-iTYHqwvY.js +1 -0
- package/apps/web/dist/index.html +5 -5
- package/apps/web/dist/mcp/flow.html +54 -54
- package/modules/README.md +1 -1
- package/node_modules/@engenty-wizards/plugin-sdk/dist/index.d.ts +24 -1
- package/node_modules/@engenty-wizards/plugin-sdk/dist/studio.d.ts +1 -1
- package/package.json +1 -1
- package/apps/web/dist/assets/ChatPanel-Br3nYkUc.js +0 -1
- package/apps/web/dist/assets/EditorPage-BSjQC6FE.js +0 -2
- package/apps/web/dist/assets/HomePage-8v4HqIbP.js +0 -1
- package/apps/web/dist/assets/ImportWizard-DiV62Uno.js +0 -1
- package/apps/web/dist/assets/NewWizardPage-BBL2yenw.js +0 -2
- package/apps/web/dist/assets/PublicRunner-BwtoEVlE.js +0 -1
- package/apps/web/dist/assets/RunnerView-DOizlTad.js +0 -2
- package/apps/web/dist/assets/SettingsPage-CBDv04YS.js +0 -2
- package/apps/web/dist/assets/changelog-03JgixQA.js +0 -1
- package/apps/web/dist/assets/index-B9YVjvVY.css +0 -1
- package/apps/web/dist/assets/oss-credits-roB62QJu.js +0 -1
- package/apps/web/dist/assets/outputs-COE_nvAw.js +0 -69
- package/apps/web/dist/assets/type-Bj9H3eOK.js +0 -1
- package/apps/web/dist/assets/ui-Cs6lpsmj.js +0 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { FACT_TYPES, PROJECT_LIMITS } from "@engenty-wizards/shared/projects";
|
|
2
|
-
import { and, count, eq } from "drizzle-orm";
|
|
2
|
+
import { and, count, eq, isNull } from "drizzle-orm";
|
|
3
3
|
import { nanoid } from "nanoid";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { db, schema } from "../db/client.js";
|
|
6
6
|
import { env } from "../env.js";
|
|
7
7
|
import { managed, tenantInfo } from "../manage.js";
|
|
8
8
|
import { currentTenant } from "../tenants/tenant.js";
|
|
9
|
+
import { mayBuild, requireBuild, requireWritable } from "./access.js";
|
|
9
10
|
import { notFound, ServiceError } from "./errors.js";
|
|
10
11
|
import { forgetWizardLinks } from "./links.js";
|
|
11
12
|
import { removeProjectFiles } from "./project-files.js";
|
|
@@ -61,22 +62,33 @@ export async function ownedProject(_userId, projectId) {
|
|
|
61
62
|
}
|
|
62
63
|
return p;
|
|
63
64
|
}
|
|
64
|
-
/**
|
|
65
|
+
/** The tenant's projects that were made here, not synced from a local install. */
|
|
66
|
+
const madeHere = () => and(eq(schema.project.tenantId, currentTenant()), isNull(schema.project.origin));
|
|
67
|
+
/** The tenant's projects a local install synced. */
|
|
68
|
+
const synced = () => and(eq(schema.project.tenantId, currentTenant()), eq(schema.project.origin, "local"));
|
|
69
|
+
/**
|
|
70
|
+
* A tenant that builds here always has a project of its own: the first one is made when its
|
|
71
|
+
* studio is first opened.
|
|
72
|
+
*/
|
|
65
73
|
async function ensureProject() {
|
|
66
|
-
const any = await db.query.project.findFirst({
|
|
67
|
-
where: eq(schema.project.tenantId, currentTenant()),
|
|
68
|
-
});
|
|
74
|
+
const any = await db.query.project.findFirst({ where: madeHere() });
|
|
69
75
|
if (!any) {
|
|
70
76
|
await db
|
|
71
77
|
.insert(schema.project)
|
|
72
78
|
.values({ id: nanoid(12), tenantId: currentTenant(), name: "Meine Wizards" });
|
|
73
79
|
}
|
|
74
80
|
}
|
|
75
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* The project a caller means when it names none: the oldest one made here, where a wizard
|
|
83
|
+
* lands; for a tenant that builds nothing here, the one its local install synced.
|
|
84
|
+
*/
|
|
76
85
|
export async function defaultProject(_userId) {
|
|
77
|
-
await
|
|
86
|
+
const build = await mayBuild();
|
|
87
|
+
if (build) {
|
|
88
|
+
await ensureProject();
|
|
89
|
+
}
|
|
78
90
|
const p = await db.query.project.findFirst({
|
|
79
|
-
where:
|
|
91
|
+
where: build ? madeHere() : synced(),
|
|
80
92
|
orderBy: [schema.project.createdAt],
|
|
81
93
|
});
|
|
82
94
|
if (!p) {
|
|
@@ -84,11 +96,18 @@ export async function defaultProject(_userId) {
|
|
|
84
96
|
}
|
|
85
97
|
return p;
|
|
86
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* The projects the studio shows. A tenant that builds here: its own and the synced ones, up to
|
|
101
|
+
* its number. One that does not: only what its local install synced — possibly nothing yet.
|
|
102
|
+
*/
|
|
87
103
|
export async function listProjects(_userId) {
|
|
88
|
-
await
|
|
104
|
+
const build = await mayBuild();
|
|
105
|
+
if (build) {
|
|
106
|
+
await ensureProject();
|
|
107
|
+
}
|
|
89
108
|
// The oldest come first: with a limit of one, that is the project everything lands in.
|
|
90
109
|
const projects = await db.query.project.findMany({
|
|
91
|
-
where: eq(schema.project.tenantId, currentTenant()),
|
|
110
|
+
where: build ? eq(schema.project.tenantId, currentTenant()) : synced(),
|
|
92
111
|
orderBy: [schema.project.createdAt],
|
|
93
112
|
limit: await projectLimit(),
|
|
94
113
|
});
|
|
@@ -112,6 +131,7 @@ export function maskedServers(p) {
|
|
|
112
131
|
}));
|
|
113
132
|
}
|
|
114
133
|
export async function createProject(_userId, name) {
|
|
134
|
+
await requireBuild();
|
|
115
135
|
const [{ n }] = await db
|
|
116
136
|
.select({ n: count() })
|
|
117
137
|
.from(schema.project)
|
|
@@ -128,6 +148,7 @@ export async function createProject(_userId, name) {
|
|
|
128
148
|
}
|
|
129
149
|
export async function updateProject(userId, projectId, patch) {
|
|
130
150
|
const p = await ownedProject(userId, projectId);
|
|
151
|
+
await requireWritable(p);
|
|
131
152
|
const mcpServers = patch.mcpServers?.map((s) => {
|
|
132
153
|
const before = p.mcpServers.find((x) => x.id === s.id);
|
|
133
154
|
const headers = s.headers
|
|
@@ -160,13 +181,16 @@ export function projectProfile(p) {
|
|
|
160
181
|
facts: p.facts.filter((f) => f.value.trim()),
|
|
161
182
|
};
|
|
162
183
|
}
|
|
163
|
-
export async function deleteProject(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
});
|
|
184
|
+
export async function deleteProject(userId, projectId) {
|
|
185
|
+
await requireWritable(await ownedProject(userId, projectId));
|
|
186
|
+
const all = await db.query.project.findMany({ where: madeHere() });
|
|
167
187
|
if (all.length <= 1) {
|
|
168
188
|
throw new ServiceError("refused", "Das letzte Projekt bleibt.");
|
|
169
189
|
}
|
|
190
|
+
await removeProject(projectId);
|
|
191
|
+
}
|
|
192
|
+
/** Takes a project away with its wizards' links and its files. */
|
|
193
|
+
export async function removeProject(projectId) {
|
|
170
194
|
const wizards = await db
|
|
171
195
|
.select({ id: schema.wizard.id })
|
|
172
196
|
.from(schema.wizard)
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { PROJECT_LIMITS } from "@engenty-wizards/shared/projects";
|
|
3
|
+
import { WORKSPACE_LIMITS } from "@engenty-wizards/shared/workspace";
|
|
4
|
+
import { and, count, eq, inArray, sql } from "drizzle-orm";
|
|
5
|
+
import { nanoid } from "nanoid";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { db, schema } from "../db/client.js";
|
|
8
|
+
import { missingModels, unavoidableSteps } from "../engine/requirements.js";
|
|
9
|
+
import { env } from "../env.js";
|
|
10
|
+
import { codeOf, putLink } from "../tenants/control.js";
|
|
11
|
+
import { currentTenant } from "../tenants/tenant.js";
|
|
12
|
+
import { asSync, mayBuild } from "./access.js";
|
|
13
|
+
import { emitDraftChanged } from "./draft-events.js";
|
|
14
|
+
import { notFound, ServiceError } from "./errors.js";
|
|
15
|
+
import { draftFiles, writeFile } from "./files.js";
|
|
16
|
+
import { addProjectFile, projectFiles, removeProjectFile } from "./project-files.js";
|
|
17
|
+
import { brandColorSchema, projectFactSchema, projectLimit, removeProject, } from "./projects.js";
|
|
18
|
+
import { deleteWizard, draftIssues, parseDraft, shareUrl } from "./wizards.js";
|
|
19
|
+
/**
|
|
20
|
+
* A local install's project on this runtime (docs/manage-contract.md, "Spaces of a local
|
|
21
|
+
* install"). The install sends a wizard when it publishes it: the published version and its
|
|
22
|
+
* workspace, under the ids they have there. Here the project is shown and its wizards run; it
|
|
23
|
+
* is changed only by the next sync. The share link is this runtime's own.
|
|
24
|
+
*/
|
|
25
|
+
/** Ids as the local install makes them (nanoid); nothing else is taken as a key. */
|
|
26
|
+
const syncedId = z.string().regex(/^[A-Za-z0-9_-]{8,40}$/);
|
|
27
|
+
const spaceSchema = z.object({
|
|
28
|
+
name: z.string().min(1).max(80),
|
|
29
|
+
brand: z
|
|
30
|
+
.object({
|
|
31
|
+
name: z.string().max(120).optional(),
|
|
32
|
+
about: z.string().max(8000).optional(),
|
|
33
|
+
colors: z.array(brandColorSchema).max(PROJECT_LIMITS.colors).optional(),
|
|
34
|
+
})
|
|
35
|
+
.default({}),
|
|
36
|
+
facts: z.array(projectFactSchema).max(PROJECT_LIMITS.facts).default([]),
|
|
37
|
+
/** The logo end users see; null for none. */
|
|
38
|
+
logo: z
|
|
39
|
+
.object({
|
|
40
|
+
name: z.string().min(1).max(120),
|
|
41
|
+
mime: z.string().max(100),
|
|
42
|
+
description: z.string().max(600).default(""),
|
|
43
|
+
data: z.string(),
|
|
44
|
+
})
|
|
45
|
+
.nullable()
|
|
46
|
+
.default(null),
|
|
47
|
+
});
|
|
48
|
+
export const syncWizardSchema = z.object({
|
|
49
|
+
space: spaceSchema,
|
|
50
|
+
/** The version the install published, as it counts them. */
|
|
51
|
+
version: z.number().int().min(1).max(1_000_000),
|
|
52
|
+
definition: z.unknown(),
|
|
53
|
+
files: z
|
|
54
|
+
.array(z.object({
|
|
55
|
+
path: z.string().max(300),
|
|
56
|
+
mime: z.string().max(100).optional(),
|
|
57
|
+
data: z.string(),
|
|
58
|
+
}))
|
|
59
|
+
.max(WORKSPACE_LIMITS.files)
|
|
60
|
+
.default([]),
|
|
61
|
+
shareEnabled: z.boolean().default(true),
|
|
62
|
+
dailyRunLimit: z.number().int().min(1).max(10_000).optional(),
|
|
63
|
+
});
|
|
64
|
+
export const syncSettingsSchema = z.object({
|
|
65
|
+
shareEnabled: z.boolean().optional(),
|
|
66
|
+
dailyRunLimit: z.number().int().min(1).max(10_000).optional(),
|
|
67
|
+
});
|
|
68
|
+
export const checkSchema = z.object({
|
|
69
|
+
definition: z.unknown(),
|
|
70
|
+
/** Paths of the wizard's workspace files. */
|
|
71
|
+
files: z.array(z.string().max(300)).max(WORKSPACE_LIMITS.files).default([]),
|
|
72
|
+
});
|
|
73
|
+
const stepsWith = (def, has) => def.steps.filter(has).map((s) => ({ id: s.id, title: s.title }));
|
|
74
|
+
/**
|
|
75
|
+
* What of a wizard does not work on this runtime: what its validator says here (a connector
|
|
76
|
+
* the project lacks, a plugin's tool), model classes nothing is bound to, the sandbox where
|
|
77
|
+
* there is none, MCP servers the project does not know.
|
|
78
|
+
*/
|
|
79
|
+
export async function serverProblems(def, issues, mcpServers) {
|
|
80
|
+
const problems = issues.map((issue) => ({
|
|
81
|
+
code: /connector/i.test(issue.message) ? "connector" : "invalid",
|
|
82
|
+
blocking: true,
|
|
83
|
+
steps: stepsWith(def, (s) => s.id === issue.stepId),
|
|
84
|
+
detail: issue.message,
|
|
85
|
+
}));
|
|
86
|
+
for (const missing of await missingModels(def)) {
|
|
87
|
+
problems.push({
|
|
88
|
+
code: "model",
|
|
89
|
+
blocking: missing.blocking,
|
|
90
|
+
steps: missing.steps,
|
|
91
|
+
detail: `${missing.cls}: ${missing.problem}`,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (env.sandbox === "off") {
|
|
95
|
+
const steps = stepsWith(def, (s) => s.type === "agent" && s.tools.includes("sandbox"));
|
|
96
|
+
if (steps.length) {
|
|
97
|
+
problems.push({ code: "sandbox", blocking: false, steps, detail: "" });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const unknown = new Set();
|
|
101
|
+
const steps = stepsWith(def, (s) => {
|
|
102
|
+
const lacking = s.type === "agent" ? (s.mcp ?? []).filter((id) => !mcpServers.includes(id)) : [];
|
|
103
|
+
for (const id of lacking) {
|
|
104
|
+
unknown.add(id);
|
|
105
|
+
}
|
|
106
|
+
return lacking.length > 0;
|
|
107
|
+
});
|
|
108
|
+
if (steps.length) {
|
|
109
|
+
const unavoidable = unavoidableSteps(def);
|
|
110
|
+
problems.push({
|
|
111
|
+
code: "mcp",
|
|
112
|
+
blocking: steps.some((s) => unavoidable.has(s.id)),
|
|
113
|
+
steps,
|
|
114
|
+
detail: [...unknown].join(", "),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return problems;
|
|
118
|
+
}
|
|
119
|
+
/** What a wizard would lack here, asked before it is sent: no project is touched. */
|
|
120
|
+
export async function checkOnServer(input) {
|
|
121
|
+
const { draft, issues } = parseDraft(input.definition, input.files);
|
|
122
|
+
// Connectors are a project's; a project that is not here yet has none.
|
|
123
|
+
const connectors = (draft.connections ?? [])
|
|
124
|
+
.filter((c) => c.connector)
|
|
125
|
+
.map((c) => ({
|
|
126
|
+
message: `Connection "${c.id}" uses connector "${c.connector}", which this project has not imported.`,
|
|
127
|
+
}));
|
|
128
|
+
return serverProblems(draft, [...issues, ...connectors], []);
|
|
129
|
+
}
|
|
130
|
+
async function syncedProject(spaceId) {
|
|
131
|
+
const p = await db.query.project.findFirst({
|
|
132
|
+
where: and(eq(schema.project.id, spaceId), eq(schema.project.tenantId, currentTenant())),
|
|
133
|
+
});
|
|
134
|
+
return p ?? null;
|
|
135
|
+
}
|
|
136
|
+
/** Refuses a project beyond the tenant's number; a synced one counts like one made here. */
|
|
137
|
+
async function requireRoom(spaceId) {
|
|
138
|
+
const mine = eq(schema.project.tenantId, currentTenant());
|
|
139
|
+
// What a tenant that builds nothing here once made here is not shown, so it takes no place.
|
|
140
|
+
const counted = (await mayBuild()) ? mine : and(mine, eq(schema.project.origin, "local"));
|
|
141
|
+
const others = await db
|
|
142
|
+
.select({ id: schema.project.id, name: schema.project.name, origin: schema.project.origin })
|
|
143
|
+
.from(schema.project)
|
|
144
|
+
.where(and(counted, sql `${schema.project.id} <> ${spaceId}`));
|
|
145
|
+
const limit = await projectLimit();
|
|
146
|
+
if (others.length >= limit) {
|
|
147
|
+
throw new ServiceError("refused", limit === 1
|
|
148
|
+
? "Dieses Konto hat hier schon ein Projekt."
|
|
149
|
+
: `Dieses Konto hat hier schon ${limit} Projekte.`, {
|
|
150
|
+
reason: "space_limit",
|
|
151
|
+
limit,
|
|
152
|
+
spaces: others.filter((p) => p.origin === "local").map((p) => ({ id: p.id, name: p.name })),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/** A synced project holds so many wizards: each brings up to 25 MB of files to keep. */
|
|
157
|
+
async function requireWizardRoom(spaceId) {
|
|
158
|
+
const [row] = await db
|
|
159
|
+
.select({ n: count() })
|
|
160
|
+
.from(schema.wizard)
|
|
161
|
+
.where(eq(schema.wizard.projectId, spaceId));
|
|
162
|
+
if ((row?.n ?? 0) >= env.limits.syncedWizards) {
|
|
163
|
+
throw new ServiceError("refused", `Hier haben höchstens ${env.limits.syncedWizards} veröffentlichte Wizards Platz.`, { reason: "wizard_limit", limit: env.limits.syncedWizards });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const hashOf = (data) => createHash("sha256").update(data).digest("hex");
|
|
167
|
+
/** The project's logo follows the install's: kept when it is the same picture, else replaced. */
|
|
168
|
+
async function syncLogo(projectId, logo) {
|
|
169
|
+
const here = await projectFiles(projectId, "logo");
|
|
170
|
+
const data = logo ? Buffer.from(logo.data, "base64") : null;
|
|
171
|
+
const mark = data ? `sync:${hashOf(data)}` : null;
|
|
172
|
+
if (mark && here.length === 1 && here[0].source === mark) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
for (const old of here) {
|
|
176
|
+
await removeProjectFile(projectId, old.id);
|
|
177
|
+
}
|
|
178
|
+
if (logo && data && mark) {
|
|
179
|
+
await addProjectFile(projectId, {
|
|
180
|
+
kind: "logo",
|
|
181
|
+
name: logo.name,
|
|
182
|
+
mime: logo.mime,
|
|
183
|
+
data,
|
|
184
|
+
// With words of its own no model is asked to describe it.
|
|
185
|
+
description: logo.description || logo.name,
|
|
186
|
+
source: mark,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/** Makes or updates the synced project: its name, what it says about itself, its logo. */
|
|
191
|
+
async function syncSpace(spaceId, space) {
|
|
192
|
+
const existing = await syncedProject(spaceId);
|
|
193
|
+
if (existing && existing.origin !== "local") {
|
|
194
|
+
throw new ServiceError("refused", "Ein Projekt mit dieser Kennung wurde hier angelegt.", {
|
|
195
|
+
reason: "id_taken",
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (!existing) {
|
|
199
|
+
await requireRoom(spaceId);
|
|
200
|
+
}
|
|
201
|
+
const now = new Date();
|
|
202
|
+
const values = { name: space.name, brand: space.brand, facts: space.facts, syncedAt: now };
|
|
203
|
+
if (existing) {
|
|
204
|
+
await db
|
|
205
|
+
.update(schema.project)
|
|
206
|
+
.set({ ...values, updatedAt: now })
|
|
207
|
+
.where(eq(schema.project.id, spaceId));
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
await db.insert(schema.project).values({
|
|
211
|
+
id: spaceId,
|
|
212
|
+
tenantId: currentTenant(),
|
|
213
|
+
origin: "local",
|
|
214
|
+
mcpServers: [],
|
|
215
|
+
...values,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
await syncLogo(spaceId, space.logo);
|
|
219
|
+
const project = await syncedProject(spaceId);
|
|
220
|
+
if (!project) {
|
|
221
|
+
throw notFound();
|
|
222
|
+
}
|
|
223
|
+
return project;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Takes a wizard a local install published: the project it is in, the wizard under its id, its
|
|
227
|
+
* workspace, and the version — published here when a run of it can finish. Sending the same
|
|
228
|
+
* wizard again updates the copy; its link stays.
|
|
229
|
+
*/
|
|
230
|
+
export async function syncWizard(userId, rawSpaceId, rawWizardId, input) {
|
|
231
|
+
const spaceId = syncedId.parse(rawSpaceId);
|
|
232
|
+
const wizardId = syncedId.parse(rawWizardId);
|
|
233
|
+
const paths = input.files.map((f) => f.path);
|
|
234
|
+
// The shape is checked before anything is written; a wrong one leaves nothing behind.
|
|
235
|
+
const { draft } = parseDraft(input.definition, paths);
|
|
236
|
+
return asSync(async () => {
|
|
237
|
+
const project = await syncSpace(spaceId, input.space);
|
|
238
|
+
const existing = await db.query.wizard.findFirst({
|
|
239
|
+
where: and(eq(schema.wizard.id, wizardId), eq(schema.wizard.tenantId, currentTenant())),
|
|
240
|
+
});
|
|
241
|
+
if (existing && existing.projectId !== spaceId) {
|
|
242
|
+
throw new ServiceError("refused", "Ein Wizard mit dieser Kennung gehört hier woanders hin.", {
|
|
243
|
+
reason: "id_taken",
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
const settings = {
|
|
247
|
+
title: draft.title,
|
|
248
|
+
draft,
|
|
249
|
+
shareEnabled: input.shareEnabled,
|
|
250
|
+
...(input.dailyRunLimit ? { dailyRunLimit: input.dailyRunLimit } : {}),
|
|
251
|
+
};
|
|
252
|
+
if (!existing) {
|
|
253
|
+
await requireWizardRoom(spaceId);
|
|
254
|
+
}
|
|
255
|
+
let shareToken = existing?.shareToken;
|
|
256
|
+
if (existing) {
|
|
257
|
+
await db
|
|
258
|
+
.update(schema.wizard)
|
|
259
|
+
.set({ ...settings, revision: sql `${schema.wizard.revision} + 1`, updatedAt: new Date() })
|
|
260
|
+
.where(eq(schema.wizard.id, wizardId));
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
// The link is made here: a token an install brought could be one another tenant holds.
|
|
264
|
+
shareToken = nanoid(14);
|
|
265
|
+
await db.insert(schema.wizard).values({
|
|
266
|
+
id: wizardId,
|
|
267
|
+
projectId: spaceId,
|
|
268
|
+
tenantId: currentTenant(),
|
|
269
|
+
shareToken,
|
|
270
|
+
dailyRunLimit: env.limits.defaultDailyRuns,
|
|
271
|
+
...settings,
|
|
272
|
+
});
|
|
273
|
+
await putLink(shareToken, "wizard", wizardId);
|
|
274
|
+
}
|
|
275
|
+
// The workspace becomes what was sent: files that are gone there go here.
|
|
276
|
+
const before = await draftFiles(wizardId);
|
|
277
|
+
const gone = before.filter((f) => !paths.includes(f.path)).map((f) => f.path);
|
|
278
|
+
if (gone.length) {
|
|
279
|
+
await db
|
|
280
|
+
.delete(schema.wizardFile)
|
|
281
|
+
.where(and(eq(schema.wizardFile.wizardId, wizardId), inArray(schema.wizardFile.path, gone)));
|
|
282
|
+
}
|
|
283
|
+
for (const file of input.files) {
|
|
284
|
+
await writeFile(userId, wizardId, file.path, Buffer.from(file.data, "base64"), file.mime);
|
|
285
|
+
}
|
|
286
|
+
const row = await db.query.wizard.findFirst({ where: eq(schema.wizard.id, wizardId) });
|
|
287
|
+
if (!row) {
|
|
288
|
+
throw notFound();
|
|
289
|
+
}
|
|
290
|
+
const problems = await serverProblems(draft, await draftIssues(row), project.mcpServers.map((s) => s.id));
|
|
291
|
+
const runnable = !problems.some((p) => p.blocking);
|
|
292
|
+
let publishedVersion = row.publishedVersion;
|
|
293
|
+
if (runnable) {
|
|
294
|
+
const files = await draftFiles(wizardId);
|
|
295
|
+
await db
|
|
296
|
+
.insert(schema.wizardVersion)
|
|
297
|
+
.values({ id: nanoid(12), wizardId, version: input.version, definition: draft, files })
|
|
298
|
+
.onConflictDoUpdate({
|
|
299
|
+
target: [schema.wizardVersion.wizardId, schema.wizardVersion.version],
|
|
300
|
+
set: { definition: draft, files },
|
|
301
|
+
});
|
|
302
|
+
await db
|
|
303
|
+
.update(schema.wizard)
|
|
304
|
+
.set({ publishedVersion: input.version })
|
|
305
|
+
.where(eq(schema.wizard.id, wizardId));
|
|
306
|
+
publishedVersion = input.version;
|
|
307
|
+
}
|
|
308
|
+
emitDraftChanged({
|
|
309
|
+
wizardId,
|
|
310
|
+
kind: "draft",
|
|
311
|
+
revision: row.revision,
|
|
312
|
+
source: "mcp",
|
|
313
|
+
client: "sync",
|
|
314
|
+
note: null,
|
|
315
|
+
touched: [],
|
|
316
|
+
});
|
|
317
|
+
return {
|
|
318
|
+
wizardId,
|
|
319
|
+
shareUrl: shareUrl(shareToken ?? row.shareToken),
|
|
320
|
+
code: await codeOf(shareToken ?? row.shareToken),
|
|
321
|
+
shareEnabled: row.shareEnabled,
|
|
322
|
+
publishedVersion,
|
|
323
|
+
runnable,
|
|
324
|
+
problems,
|
|
325
|
+
};
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
async function syncedWizard(rawSpaceId, rawWizardId) {
|
|
329
|
+
const spaceId = syncedId.parse(rawSpaceId);
|
|
330
|
+
const wizardId = syncedId.parse(rawWizardId);
|
|
331
|
+
const project = await syncedProject(spaceId);
|
|
332
|
+
const w = project?.origin === "local"
|
|
333
|
+
? await db.query.wizard.findFirst({
|
|
334
|
+
where: and(eq(schema.wizard.id, wizardId), eq(schema.wizard.projectId, spaceId)),
|
|
335
|
+
})
|
|
336
|
+
: null;
|
|
337
|
+
if (!w) {
|
|
338
|
+
throw notFound();
|
|
339
|
+
}
|
|
340
|
+
return w;
|
|
341
|
+
}
|
|
342
|
+
/** The install changed how a synced wizard is shared: its link on or off, its runs a day. */
|
|
343
|
+
export async function patchSyncedWizard(spaceId, wizardId, patch) {
|
|
344
|
+
const w = await syncedWizard(spaceId, wizardId);
|
|
345
|
+
await db
|
|
346
|
+
.update(schema.wizard)
|
|
347
|
+
.set({ ...patch, updatedAt: new Date() })
|
|
348
|
+
.where(eq(schema.wizard.id, w.id));
|
|
349
|
+
return { ok: true };
|
|
350
|
+
}
|
|
351
|
+
/** The install deleted a wizard: its copy here goes, with its link and its runs. */
|
|
352
|
+
export async function removeSyncedWizard(userId, spaceId, wizardId) {
|
|
353
|
+
const w = await syncedWizard(spaceId, wizardId);
|
|
354
|
+
await asSync(() => deleteWizard(userId, w.id));
|
|
355
|
+
return { ok: true };
|
|
356
|
+
}
|
|
357
|
+
/** The projects local installs synced, with their wizards: what an install compares itself to. */
|
|
358
|
+
export async function listSyncedSpaces() {
|
|
359
|
+
const projects = await db.query.project.findMany({
|
|
360
|
+
where: and(eq(schema.project.tenantId, currentTenant()), eq(schema.project.origin, "local")),
|
|
361
|
+
orderBy: [schema.project.createdAt],
|
|
362
|
+
});
|
|
363
|
+
const wizards = projects.length
|
|
364
|
+
? await db
|
|
365
|
+
.select({
|
|
366
|
+
id: schema.wizard.id,
|
|
367
|
+
projectId: schema.wizard.projectId,
|
|
368
|
+
title: schema.wizard.title,
|
|
369
|
+
publishedVersion: schema.wizard.publishedVersion,
|
|
370
|
+
shareToken: schema.wizard.shareToken,
|
|
371
|
+
shareEnabled: schema.wizard.shareEnabled,
|
|
372
|
+
})
|
|
373
|
+
.from(schema.wizard)
|
|
374
|
+
.where(inArray(schema.wizard.projectId, projects.map((p) => p.id)))
|
|
375
|
+
: [];
|
|
376
|
+
return projects.map((p) => ({
|
|
377
|
+
id: p.id,
|
|
378
|
+
name: p.name,
|
|
379
|
+
syncedAt: p.syncedAt?.toISOString() ?? null,
|
|
380
|
+
wizards: wizards
|
|
381
|
+
.filter((w) => w.projectId === p.id)
|
|
382
|
+
.map((w) => ({
|
|
383
|
+
id: w.id,
|
|
384
|
+
title: w.title,
|
|
385
|
+
publishedVersion: w.publishedVersion,
|
|
386
|
+
shareUrl: shareUrl(w.shareToken),
|
|
387
|
+
shareEnabled: w.shareEnabled,
|
|
388
|
+
})),
|
|
389
|
+
}));
|
|
390
|
+
}
|
|
391
|
+
/** Takes a synced project away from this runtime: its wizards, their links and runs, its files. */
|
|
392
|
+
export async function removeSyncedSpace(rawSpaceId) {
|
|
393
|
+
const spaceId = syncedId.parse(rawSpaceId);
|
|
394
|
+
const project = await syncedProject(spaceId);
|
|
395
|
+
if (project?.origin !== "local") {
|
|
396
|
+
throw notFound();
|
|
397
|
+
}
|
|
398
|
+
await asSync(() => removeProject(spaceId));
|
|
399
|
+
return { ok: true };
|
|
400
|
+
}
|
|
@@ -8,6 +8,7 @@ import { env } from "../env.js";
|
|
|
8
8
|
import { pluginToolsOf } from "../plugins/registry.js";
|
|
9
9
|
import { dropLinks, putLink } from "../tenants/control.js";
|
|
10
10
|
import { currentTenant } from "../tenants/tenant.js";
|
|
11
|
+
import { requireBuild, requireWritable, requireWritableProject } from "./access.js";
|
|
11
12
|
import { changedSteps, emitDraftChanged } from "./draft-events.js";
|
|
12
13
|
import { notFound, ServiceError } from "./errors.js";
|
|
13
14
|
import { copyFiles, draftFiles, sameFiles, seedFiles } from "./files.js";
|
|
@@ -41,6 +42,12 @@ export async function ownedWizard(_userId, wizardId) {
|
|
|
41
42
|
}
|
|
42
43
|
return w;
|
|
43
44
|
}
|
|
45
|
+
/** The wizard, for a change: refused where its project is a local install's or nothing is built here. */
|
|
46
|
+
export async function writableWizard(userId, wizardId) {
|
|
47
|
+
const w = await ownedWizard(userId, wizardId);
|
|
48
|
+
await requireWritableProject(w.projectId);
|
|
49
|
+
return w;
|
|
50
|
+
}
|
|
44
51
|
export function wizardSummary(w) {
|
|
45
52
|
return {
|
|
46
53
|
id: w.id,
|
|
@@ -193,9 +200,12 @@ export function shapeIssues(error, raw) {
|
|
|
193
200
|
});
|
|
194
201
|
}
|
|
195
202
|
export async function createWizard(userId, input, writer = { source: "studio" }) {
|
|
203
|
+
// Asked first: a tenant that builds nothing here has no project of its own to name.
|
|
204
|
+
await requireBuild();
|
|
196
205
|
const project = input.projectId
|
|
197
206
|
? await ownedProject(userId, input.projectId)
|
|
198
207
|
: await defaultProject(userId);
|
|
208
|
+
await requireWritable(project);
|
|
199
209
|
const starter = input.starterId
|
|
200
210
|
? await marketplaceWizard(input.starterId, asLang(input.lang))
|
|
201
211
|
: undefined;
|
|
@@ -243,7 +253,7 @@ export async function createWizard(userId, input, writer = { source: "studio" })
|
|
|
243
253
|
* architect, MCP — goes through here, so the revision counts all of them.
|
|
244
254
|
*/
|
|
245
255
|
export async function writeDraft(userId, wizardId, input, writer = { source: "studio" }) {
|
|
246
|
-
const w = await
|
|
256
|
+
const w = await writableWizard(userId, wizardId);
|
|
247
257
|
const files = await draftFiles(w.id);
|
|
248
258
|
const paths = files.map((f) => f.path);
|
|
249
259
|
const { draft } = parseDraft(input.definition, paths);
|
|
@@ -279,7 +289,7 @@ export async function writeDraft(userId, wizardId, input, writer = { source: "st
|
|
|
279
289
|
return { revision: row.revision, issues, draft };
|
|
280
290
|
}
|
|
281
291
|
export async function editWizard(userId, wizardId, input, writer) {
|
|
282
|
-
const w = await
|
|
292
|
+
const w = await writableWizard(userId, wizardId);
|
|
283
293
|
if (w.revision !== input.baseRevision) {
|
|
284
294
|
throw new ServiceError("revision_conflict", "Der Wizard wurde inzwischen geändert.", {
|
|
285
295
|
revision: w.revision,
|
|
@@ -298,9 +308,9 @@ export async function editWizard(userId, wizardId, input, writer) {
|
|
|
298
308
|
return writeDraft(userId, wizardId, { definition, baseRevision: input.baseRevision, note: input.note }, writer);
|
|
299
309
|
}
|
|
300
310
|
export async function updateWizardSettings(userId, wizardId, patch) {
|
|
301
|
-
const w = await
|
|
311
|
+
const w = await writableWizard(userId, wizardId);
|
|
302
312
|
if (patch.projectId) {
|
|
303
|
-
await ownedProject(userId, patch.projectId);
|
|
313
|
+
await requireWritable(await ownedProject(userId, patch.projectId));
|
|
304
314
|
}
|
|
305
315
|
await db
|
|
306
316
|
.update(schema.wizard)
|
|
@@ -309,7 +319,7 @@ export async function updateWizardSettings(userId, wizardId, patch) {
|
|
|
309
319
|
return wizardSummary(await ownedWizard(userId, wizardId));
|
|
310
320
|
}
|
|
311
321
|
export async function rotateShareLink(userId, wizardId) {
|
|
312
|
-
const w = await
|
|
322
|
+
const w = await writableWizard(userId, wizardId);
|
|
313
323
|
const token = newShareToken();
|
|
314
324
|
await db
|
|
315
325
|
.update(schema.wizard)
|
|
@@ -320,7 +330,7 @@ export async function rotateShareLink(userId, wizardId) {
|
|
|
320
330
|
return { shareToken: token, shareUrl: shareUrl(token) };
|
|
321
331
|
}
|
|
322
332
|
export async function duplicateWizard(userId, wizardId) {
|
|
323
|
-
const w = await
|
|
333
|
+
const w = await writableWizard(userId, wizardId);
|
|
324
334
|
const id = nanoid(12);
|
|
325
335
|
const draft = { ...w.draft, title: `${w.draft.title} (Kopie)` };
|
|
326
336
|
const shareToken = newShareToken();
|
|
@@ -338,7 +348,8 @@ export async function duplicateWizard(userId, wizardId) {
|
|
|
338
348
|
await copyFiles(w.id, id);
|
|
339
349
|
return { id };
|
|
340
350
|
}
|
|
341
|
-
export async function deleteWizard(
|
|
351
|
+
export async function deleteWizard(userId, wizardId) {
|
|
352
|
+
await writableWizard(userId, wizardId);
|
|
342
353
|
await forgetWizardLinks(wizardId);
|
|
343
354
|
await db
|
|
344
355
|
.delete(schema.wizard)
|
|
@@ -354,7 +365,7 @@ export async function requireClean(w) {
|
|
|
354
365
|
return { definition: w.draft, files };
|
|
355
366
|
}
|
|
356
367
|
export async function publishWizard(userId, wizardId) {
|
|
357
|
-
const w = await
|
|
368
|
+
const w = await writableWizard(userId, wizardId);
|
|
358
369
|
const { definition, files } = await requireClean(w);
|
|
359
370
|
const version = (w.publishedVersion ?? 0) + 1;
|
|
360
371
|
await db
|
|
@@ -12,10 +12,57 @@ export async function putLink(token, kind, ref) {
|
|
|
12
12
|
set: { tenantId: currentTenant(), kind, ref },
|
|
13
13
|
});
|
|
14
14
|
}
|
|
15
|
+
/** A token's link goes, and with a wizard's share token its ID. */
|
|
15
16
|
export async function dropLinks(tokens) {
|
|
16
17
|
if (tokens.length) {
|
|
17
18
|
await controlDb.delete(control.link).where(inArray(control.link.token, tokens));
|
|
19
|
+
await controlDb
|
|
20
|
+
.delete(control.link)
|
|
21
|
+
.where(and(eq(control.link.kind, "code"), inArray(control.link.ref, tokens)));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// --- wizard IDs ----------------------------------------------------------------------
|
|
25
|
+
// The share token is 14 characters of both cases, `_` and `-`: nobody types that on a phone.
|
|
26
|
+
// The ID is 8 capitals and digits without look-alikes (no 0 O 1 I), made the first time someone
|
|
27
|
+
// asks for it and gone when the share token rotates.
|
|
28
|
+
const CODE_ALPHABET = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
|
|
29
|
+
const CODE_LENGTH = 8;
|
|
30
|
+
function newCode() {
|
|
31
|
+
const bytes = crypto.getRandomValues(new Uint8Array(CODE_LENGTH));
|
|
32
|
+
return Array.from(bytes, (b) => CODE_ALPHABET[b % CODE_ALPHABET.length]).join("");
|
|
33
|
+
}
|
|
34
|
+
/** What a person typed, as stored: capitals, no spaces or dashes. */
|
|
35
|
+
export const normalizeCode = (input) => input.toUpperCase().replace(/[\s-]/g, "");
|
|
36
|
+
/** The ID of a wizard's share token; made on the first ask. */
|
|
37
|
+
export async function codeOf(shareToken) {
|
|
38
|
+
const row = await controlDb.query.link.findFirst({
|
|
39
|
+
where: and(eq(control.link.kind, "code"), eq(control.link.ref, shareToken)),
|
|
40
|
+
});
|
|
41
|
+
if (row) {
|
|
42
|
+
return row.token;
|
|
18
43
|
}
|
|
44
|
+
for (;;) {
|
|
45
|
+
const code = newCode();
|
|
46
|
+
const made = await controlDb
|
|
47
|
+
.insert(control.link)
|
|
48
|
+
.values({ token: code, tenantId: currentTenant(), kind: "code", ref: shareToken })
|
|
49
|
+
.onConflictDoNothing()
|
|
50
|
+
.returning({ token: control.link.token });
|
|
51
|
+
if (made.length) {
|
|
52
|
+
return code;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** The share token behind an ID, and its tenant. */
|
|
57
|
+
export async function tokenOfCode(input) {
|
|
58
|
+
const code = normalizeCode(input);
|
|
59
|
+
if (code.length !== CODE_LENGTH) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
const row = await controlDb.query.link.findFirst({
|
|
63
|
+
where: and(eq(control.link.kind, "code"), eq(control.link.token, code)),
|
|
64
|
+
});
|
|
65
|
+
return row ? { token: row.ref, tenantId: row.tenantId } : null;
|
|
19
66
|
}
|
|
20
67
|
export async function tenantOfLink(token, kind) {
|
|
21
68
|
const row = await controlDb.query.link.findFirst({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{a as e,n as t,t as n}from"./jsx-runtime-D3jfb0Ew.js";import{t as r}from"./preload-helper-CNEI7N0N.js";import{a as i,d as a,h as o,i as s,s as c,y as l}from"./ui-
|
|
1
|
+
import{a as e,n as t,t as n}from"./jsx-runtime-D3jfb0Ew.js";import{t as r}from"./preload-helper-CNEI7N0N.js";import{a as i,d as a,h as o,i as s,s as c,y as l}from"./ui-iTYHqwvY.js";var u=e(t(),1),d=e(n(),1);function f(e){switch(e.group){case`Added`:return{label:`NEW`,dot:`bg-moss`};case`Fixed`:return{label:`FIXED`,dot:`bg-amber`};case`Changed`:return{label:`CHANGED`,dot:`bg-cobalt`};case`Performance`:return{label:`PERFORMANCE`,dot:`bg-ember`};case`Deploy`:return{label:`DEPLOY`,dot:`bg-ink-4`};case`Docs`:return{label:`DOCS`,dot:`bg-ink-4`};default:return{label:`OTHER`,dot:`bg-ink-4`}}}var p=e=>e.version?e.version.replace(/^v/,``):l(`about.unreleased`),m=e=>e.timestamp?new Date(e.timestamp*1e3).toISOString().slice(0,10):``,h=e=>e.charAt(0).toUpperCase()+e.slice(1);function g(e,t){let n=t.trim().toLowerCase();if(!n)return e;let r=[];for(let t of e){if(`${p(t)} ${m(t)}`.toLowerCase().includes(n)){r.push(t);continue}let e=t.commits.filter(e=>`${f(e).label} ${e.scope??``} ${e.message}`.toLowerCase().includes(n));e.length>0&&r.push({...t,commits:e})}return r}function _({open:e,onClose:t}){let[n,_]=(0,u.useState)(null),[v,y]=(0,u.useState)(``);(0,u.useEffect)(()=>{if(!e||n)return;let t=!1;return r(()=>import(`./changelog-Cto0j5-V.js`).then(e=>{t||_(e.default)}),[]),()=>{t=!0}},[e,n]),(0,u.useEffect)(()=>{e||y(``)},[e]);let b=n?g(n,v):[];return(0,d.jsxs)(s,{open:e,onClose:t,title:l(`about.changelog`),wide:!0,children:[(0,d.jsx)(`p`,{className:`-mt-3 mb-4 text-[14px] text-ink-3`,children:l(`about.changelogHint`)}),n===null?(0,d.jsx)(`div`,{className:`flex justify-center py-10 text-ink-3`,children:(0,d.jsx)(a,{})}):n.length===0?(0,d.jsx)(i,{children:l(`about.changelogEmpty`)}):(0,d.jsxs)(d.Fragment,{children:[(0,d.jsx)(c,{type:`search`,value:v,placeholder:l(`about.search`),onChange:e=>y(e.target.value)}),b.length===0?(0,d.jsx)(i,{children:l(`about.noMatches`)}):null,b.map(e=>(0,d.jsxs)(`section`,{className:`mt-6`,children:[(0,d.jsxs)(`h3`,{className:`flex items-baseline gap-3 border-border-soft border-b pb-2 font-display font-semibold text-[16px]`,children:[p(e),(0,d.jsx)(`span`,{className:`font-normal font-sans text-[13px] text-ink-3 tabular-nums`,children:m(e)})]}),(0,d.jsx)(`ul`,{className:`mt-3 flex flex-col gap-2`,children:e.commits.map(e=>{let t=f(e);return(0,d.jsxs)(`li`,{className:`flex items-start gap-2.5 text-[14px]`,children:[(0,d.jsx)(`span`,{className:o(`mt-[7px] size-1.5 shrink-0 rounded-full`,t.dot)}),(0,d.jsxs)(`span`,{className:`min-w-0 text-ink-2`,children:[(0,d.jsxs)(`span`,{className:`font-semibold text-[12px] text-ink tracking-wide`,children:[t.label,e.scope?` [${e.scope}]`:``]}),` `,h(e.message),e.breaking?(0,d.jsx)(`span`,{className:`ml-1.5 rounded-full bg-rose-tint px-1.5 py-0.5 font-medium text-[12px] text-rose`,children:l(`about.breaking`)}):null]})]},e.id)})})]},e.version??`unreleased`))]})]})}export{_ as ChangelogDialog};
|