@zitadel/cli 0.1.0-alpha.0 → 0.1.0-alpha.10
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 +227 -18
- package/SKILLS.md +104 -11
- package/dist/commands/apply.mjs +4 -4
- package/dist/commands/apply.mjs.map +1 -1
- package/dist/commands/doctor.mjs +291 -17
- package/dist/commands/doctor.mjs.map +1 -1
- package/dist/commands/eject.mjs +14 -6
- package/dist/commands/eject.mjs.map +1 -1
- package/dist/commands/logs.mjs +58 -0
- package/dist/commands/logs.mjs.map +1 -0
- package/dist/commands/plan.mjs +4 -4
- package/dist/commands/plan.mjs.map +1 -1
- package/dist/commands/reset.mjs +79 -0
- package/dist/commands/reset.mjs.map +1 -0
- package/dist/commands/setup.mjs +267 -105
- package/dist/commands/setup.mjs.map +1 -1
- package/dist/commands/start.mjs +288 -0
- package/dist/commands/start.mjs.map +1 -0
- package/dist/commands/status.mjs +92 -27
- package/dist/commands/status.mjs.map +1 -1
- package/dist/commands/stop.mjs +105 -0
- package/dist/commands/stop.mjs.map +1 -0
- package/dist/docker-CnGQK3ZK.mjs +432 -0
- package/dist/docker-CnGQK3ZK.mjs.map +1 -0
- package/dist/docker-guidance-ypN3IM3o.mjs +21 -0
- package/dist/docker-guidance-ypN3IM3o.mjs.map +1 -0
- package/dist/{project-C3pSfbao.mjs → oclif-B7lBzh3R.mjs} +339 -119
- package/dist/oclif-B7lBzh3R.mjs.map +1 -0
- package/dist/orca-BoTFU8SI.mjs +2581 -0
- package/dist/orca-BoTFU8SI.mjs.map +1 -0
- package/dist/ports-B09RjuHx.mjs +111 -0
- package/dist/ports-B09RjuHx.mjs.map +1 -0
- package/dist/processes-Cw8TO1SY.mjs +120 -0
- package/dist/processes-Cw8TO1SY.mjs.map +1 -0
- package/dist/project-Cd0L3PtM.mjs +87 -0
- package/dist/project-Cd0L3PtM.mjs.map +1 -0
- package/dist/{sync-Cuyh-X1J.mjs → sync-BojoQm2P.mjs} +4 -4
- package/dist/{sync-Cuyh-X1J.mjs.map → sync-BojoQm2P.mjs.map} +1 -1
- package/oclif.manifest.json +399 -7
- package/package.json +8 -41
- package/dist/orca-COsUnVoz.mjs +0 -1006
- package/dist/orca-COsUnVoz.mjs.map +0 -1
- package/dist/project-C3pSfbao.mjs.map +0 -1
package/dist/orca-COsUnVoz.mjs
DELETED
|
@@ -1,1006 +0,0 @@
|
|
|
1
|
-
import { c as MANAGED_MARKER, d as parseJsonObject, f as stableStringify, l as DEFAULT_SERVER, p as ZitadelError, u as isObject } from "./project-C3pSfbao.mjs";
|
|
2
|
-
import { chmod, mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
import { spawnSync } from "node:child_process";
|
|
5
|
-
//#region src/lib/orca/detectors/package-json.ts
|
|
6
|
-
/**
|
|
7
|
-
* Reads and parses the `package.json` at `cwd`. Rejects if the file is absent
|
|
8
|
-
* or malformed; callers that treat those as "not a project" are expected to
|
|
9
|
-
* catch and fall back rather than have detection swallow the error here.
|
|
10
|
-
*/
|
|
11
|
-
async function readPackageJson(cwd) {
|
|
12
|
-
const contents = await readFile(join(cwd, "package.json"), "utf8");
|
|
13
|
-
return JSON.parse(contents);
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Reports whether `name` appears in either `dependencies` or `devDependencies`.
|
|
17
|
-
* Both are checked because framework packages may legitimately live in either.
|
|
18
|
-
*/
|
|
19
|
-
function hasDependency(pkg, name) {
|
|
20
|
-
return Boolean(pkg.dependencies?.[name] ?? pkg.devDependencies?.[name]);
|
|
21
|
-
}
|
|
22
|
-
//#endregion
|
|
23
|
-
//#region src/lib/orca/detectors/port.ts
|
|
24
|
-
/**
|
|
25
|
-
* Port assumed when no explicit dev port can be discovered. Matches Next.js's
|
|
26
|
-
* own default so the inferred issuer URL lines up with `next dev`.
|
|
27
|
-
*/
|
|
28
|
-
const DEFAULT_DEV_PORT = 3e3;
|
|
29
|
-
/**
|
|
30
|
-
* Determines the local dev-server port for `cwd`. The `dev` script is the most
|
|
31
|
-
* authoritative source, then a `PORT` declaration in an env file, falling back
|
|
32
|
-
* to {@link DEFAULT_DEV_PORT}. Used to derive the local issuer URL.
|
|
33
|
-
*/
|
|
34
|
-
async function detectDevPort(cwd, pkg) {
|
|
35
|
-
const dev = pkg.scripts?.dev;
|
|
36
|
-
const fromScript = typeof dev === "string" ? extractPort(dev) : void 0;
|
|
37
|
-
if (fromScript) return fromScript;
|
|
38
|
-
const fromEnvFile = await portFromEnvFile(cwd);
|
|
39
|
-
if (fromEnvFile) return fromEnvFile;
|
|
40
|
-
return DEFAULT_DEV_PORT;
|
|
41
|
-
}
|
|
42
|
-
async function portFromEnvFile(cwd) {
|
|
43
|
-
for (const candidate of [".env.local", ".env"]) try {
|
|
44
|
-
const rawPort = (await readFile(join(cwd, candidate), "utf8")).match(/^\s*PORT\s*=\s*(\d+)/m)?.[1];
|
|
45
|
-
if (rawPort) return Number.parseInt(rawPort, 10);
|
|
46
|
-
} catch {
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Parses a dev port out of an npm `dev` script string, recognizing both flag
|
|
52
|
-
* forms (`-p`/`--port`) and a leading `PORT=` env assignment. Returns
|
|
53
|
-
* `undefined` when no valid positive port is present so callers can fall back.
|
|
54
|
-
*/
|
|
55
|
-
function extractPort(script) {
|
|
56
|
-
const inline = script.match(/-p\s+(\d+)|--port[=\s]+(\d+)/);
|
|
57
|
-
if (inline) {
|
|
58
|
-
const raw = inline[1] ?? inline[2];
|
|
59
|
-
if (!raw) return;
|
|
60
|
-
const value = Number.parseInt(raw, 10);
|
|
61
|
-
if (Number.isFinite(value) && value > 0) return value;
|
|
62
|
-
}
|
|
63
|
-
const rawEnvPort = script.match(/(?:^|\s)PORT=(\d+)/)?.[1];
|
|
64
|
-
if (rawEnvPort) return Number.parseInt(rawEnvPort, 10);
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Builds the local OIDC issuer URL for a given dev port. Centralized so the
|
|
68
|
-
* `localhost` origin convention is defined in exactly one place.
|
|
69
|
-
*/
|
|
70
|
-
function issuerFromPort(port) {
|
|
71
|
-
return `http://localhost:${port}`;
|
|
72
|
-
}
|
|
73
|
-
//#endregion
|
|
74
|
-
//#region src/lib/orca/detectors/next.ts
|
|
75
|
-
/**
|
|
76
|
-
* Detects a Next.js App Router project and extracts its facts: the App Router
|
|
77
|
-
* directory (`app` vs `src/app`), the dev-server port (parsed from the `dev`
|
|
78
|
-
* script / env file, else 3000), and the derived local issuer URL. Owns every
|
|
79
|
-
* Next-specific assumption so the orchestrator and commands stay generic.
|
|
80
|
-
*/
|
|
81
|
-
var NextDetector = class {
|
|
82
|
-
framework = "next";
|
|
83
|
-
/**
|
|
84
|
-
* Returns `null` when `cwd` is not a Next.js project (no `next` dependency),
|
|
85
|
-
* so the orchestrator can try other detectors. Throws
|
|
86
|
-
* `E_UNSUPPORTED_PROJECT_SHAPE` when it is Next.js but lacks an App Router
|
|
87
|
-
* directory (e.g. a Pages Router project), which is a hard error rather than
|
|
88
|
-
* an empty directory to scaffold.
|
|
89
|
-
*/
|
|
90
|
-
async detect(cwd) {
|
|
91
|
-
const pkg = await readPackageJson(cwd).catch(() => void 0);
|
|
92
|
-
if (!pkg || !hasDependency(pkg, "next")) return null;
|
|
93
|
-
const appDir = await dirExists(join(cwd, "app")) ? "app" : await dirExists(join(cwd, "src/app")) ? "src/app" : void 0;
|
|
94
|
-
if (!appDir) throw new ZitadelError("E_UNSUPPORTED_PROJECT_SHAPE", "Next.js Pages Router projects are not supported in v1", { hint: "Create an App Router project with an app/ or src/app/ directory." });
|
|
95
|
-
const devPort = await detectDevPort(cwd, pkg);
|
|
96
|
-
return {
|
|
97
|
-
id: "next",
|
|
98
|
-
appDir,
|
|
99
|
-
devPort,
|
|
100
|
-
url: issuerFromPort(devPort)
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
async function dirExists(path) {
|
|
105
|
-
try {
|
|
106
|
-
return (await stat(path)).isDirectory();
|
|
107
|
-
} catch (error) {
|
|
108
|
-
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return false;
|
|
109
|
-
throw error;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
//#endregion
|
|
113
|
-
//#region src/lib/orca/detectors/index.ts
|
|
114
|
-
/**
|
|
115
|
-
* Active detectors, in probe order. The orchestrator tries each until one
|
|
116
|
-
* recognises the project. Add a framework by appending its detector here — no
|
|
117
|
-
* orchestrator changes needed.
|
|
118
|
-
*/
|
|
119
|
-
const detectors = [new NextDetector()];
|
|
120
|
-
//#endregion
|
|
121
|
-
//#region src/lib/orca/patchers/rule/file-writer/index.ts
|
|
122
|
-
/**
|
|
123
|
-
* Applies a {@link ScaffoldPlan} to disk, executing its operations in order.
|
|
124
|
-
*
|
|
125
|
-
* Operations are idempotent: writes whose target already matches the desired
|
|
126
|
-
* contents are recorded as skipped rather than rewritten, so re-running setup
|
|
127
|
-
* is safe. With `dryRun` no filesystem changes are made but the result still
|
|
128
|
-
* reflects what would have been written. Existing files are only overwritten
|
|
129
|
-
* when `force` is set; otherwise an `E_CONFLICT` is thrown to protect
|
|
130
|
-
* user-authored content. Paths in the plan are resolved relative to `cwd`.
|
|
131
|
-
*/
|
|
132
|
-
async function scaffold(plan, opts) {
|
|
133
|
-
const result = {
|
|
134
|
-
dryRun: opts.dryRun,
|
|
135
|
-
filesWritten: [],
|
|
136
|
-
filesSkipped: [],
|
|
137
|
-
depsAdded: []
|
|
138
|
-
};
|
|
139
|
-
for (const op of plan.ops) await applyOp(op, opts, result);
|
|
140
|
-
return result;
|
|
141
|
-
}
|
|
142
|
-
async function applyOp(op, opts, result) {
|
|
143
|
-
switch (op.kind) {
|
|
144
|
-
case "mkdir":
|
|
145
|
-
await ensureDir(abs(opts.cwd, op.path), op.mode, opts.dryRun, result);
|
|
146
|
-
break;
|
|
147
|
-
case "write":
|
|
148
|
-
await writeText(abs(opts.cwd, op.path), op.contents, {
|
|
149
|
-
mode: op.mode,
|
|
150
|
-
force: opts.force,
|
|
151
|
-
dryRun: opts.dryRun
|
|
152
|
-
}, result);
|
|
153
|
-
break;
|
|
154
|
-
case "append":
|
|
155
|
-
await appendText(abs(opts.cwd, op.path), op.contents, op.ifMissing, opts.dryRun, result);
|
|
156
|
-
break;
|
|
157
|
-
case "merge-env":
|
|
158
|
-
await mergeEnv(abs(opts.cwd, op.path), op.entries, opts.dryRun, result);
|
|
159
|
-
break;
|
|
160
|
-
case "merge-json":
|
|
161
|
-
await mergeJson(abs(opts.cwd, op.path), op.patch, opts.dryRun, result);
|
|
162
|
-
break;
|
|
163
|
-
case "append-gitignore":
|
|
164
|
-
await appendGitignore(abs(opts.cwd, ".gitignore"), op.entries, opts.dryRun, result);
|
|
165
|
-
break;
|
|
166
|
-
case "add-dep":
|
|
167
|
-
await addDependency(abs(opts.cwd, "package.json"), op, opts.dryRun, result);
|
|
168
|
-
break;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
async function ensureDir(path, mode, dryRun, result) {
|
|
172
|
-
if (dryRun) {
|
|
173
|
-
result.filesWritten.push(path);
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
await mkdir(path, {
|
|
177
|
-
recursive: true,
|
|
178
|
-
mode
|
|
179
|
-
});
|
|
180
|
-
if (mode) await chmod(path, mode).catch(() => void 0);
|
|
181
|
-
result.filesWritten.push(path);
|
|
182
|
-
}
|
|
183
|
-
async function writeText(path, contents, opts, result) {
|
|
184
|
-
const existing = await readIfExists(path);
|
|
185
|
-
if (existing === contents) {
|
|
186
|
-
result.filesSkipped.push(path);
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
if (existing !== void 0 && !opts.force) throw new ZitadelError("E_CONFLICT", `Refusing to overwrite ${path}`, {
|
|
190
|
-
hint: "Re-run with --force if you want the CLI to replace this file.",
|
|
191
|
-
details: { path }
|
|
192
|
-
});
|
|
193
|
-
if (opts.dryRun) {
|
|
194
|
-
result.filesWritten.push(path);
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
await mkdir(dirname(path), { recursive: true });
|
|
198
|
-
const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
199
|
-
await writeFile(tmp, contents, { mode: opts.mode });
|
|
200
|
-
if (opts.mode) await chmod(tmp, opts.mode).catch(() => void 0);
|
|
201
|
-
await rename(tmp, path);
|
|
202
|
-
result.filesWritten.push(path);
|
|
203
|
-
}
|
|
204
|
-
async function appendText(path, contents, ifMissing, dryRun, result) {
|
|
205
|
-
const existing = await readIfExists(path) ?? "";
|
|
206
|
-
if (ifMissing && existing.includes(ifMissing)) {
|
|
207
|
-
result.filesSkipped.push(path);
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
const next = `${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}${contents}`;
|
|
211
|
-
if (next === existing) {
|
|
212
|
-
result.filesSkipped.push(path);
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
if (dryRun) {
|
|
216
|
-
result.filesWritten.push(path);
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
await mkdir(dirname(path), { recursive: true });
|
|
220
|
-
await writeFile(path, next);
|
|
221
|
-
result.filesWritten.push(path);
|
|
222
|
-
}
|
|
223
|
-
async function mergeEnv(path, entries, dryRun, result) {
|
|
224
|
-
const existing = await readIfExists(path) ?? "";
|
|
225
|
-
const present = new Set(existing.split(/\r?\n/g).map((line) => line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)=/)?.[1]).filter((value) => Boolean(value)));
|
|
226
|
-
const additions = Object.entries(entries).filter(([key]) => !present.has(key));
|
|
227
|
-
if (additions.length === 0) {
|
|
228
|
-
result.filesSkipped.push(path);
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
const block = additions.map(([key, value]) => `${key}=${value}`).join("\n");
|
|
232
|
-
const next = `${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}${block}\n`;
|
|
233
|
-
if (dryRun) {
|
|
234
|
-
result.filesWritten.push(path);
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
await mkdir(dirname(path), { recursive: true });
|
|
238
|
-
await writeFile(path, next);
|
|
239
|
-
result.filesWritten.push(path);
|
|
240
|
-
}
|
|
241
|
-
async function mergeJson(path, patch, dryRun, result) {
|
|
242
|
-
const existing = await readIfExists(path);
|
|
243
|
-
const contents = `${stableStringify(deepMerge(existing ? parseJsonObject(existing, path) : {}, patch))}\n`;
|
|
244
|
-
if (existing === contents) {
|
|
245
|
-
result.filesSkipped.push(path);
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
if (dryRun) {
|
|
249
|
-
result.filesWritten.push(path);
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
await mkdir(dirname(path), { recursive: true });
|
|
253
|
-
await writeFile(path, contents);
|
|
254
|
-
result.filesWritten.push(path);
|
|
255
|
-
}
|
|
256
|
-
async function appendGitignore(path, entries, dryRun, result) {
|
|
257
|
-
const existing = await readIfExists(path) ?? "";
|
|
258
|
-
const lines = new Set(existing.split(/\r?\n/g).map((line) => line.trim()));
|
|
259
|
-
const missing = entries.filter((entry) => !lines.has(entry));
|
|
260
|
-
if (missing.length === 0) {
|
|
261
|
-
result.filesSkipped.push(path);
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
const next = `${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}${missing.join("\n")}\n`;
|
|
265
|
-
if (dryRun) {
|
|
266
|
-
result.filesWritten.push(path);
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
await writeFile(path, next);
|
|
270
|
-
result.filesWritten.push(path);
|
|
271
|
-
}
|
|
272
|
-
async function addDependency(path, op, dryRun, result) {
|
|
273
|
-
const existing = await readIfExists(path);
|
|
274
|
-
if (!existing) throw new ZitadelError("E_VALIDATION", "package.json is required to add Zitadel dependencies");
|
|
275
|
-
const current = parseJsonObject(existing, path);
|
|
276
|
-
const key = op.dev ? "devDependencies" : "dependencies";
|
|
277
|
-
const deps = isObject(current[key]) ? current[key] : {};
|
|
278
|
-
if (deps[op.name] === op.version) {
|
|
279
|
-
result.filesSkipped.push(path);
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
current[key] = {
|
|
283
|
-
...deps,
|
|
284
|
-
[op.name]: op.version
|
|
285
|
-
};
|
|
286
|
-
const contents = `${stableStringify(current)}\n`;
|
|
287
|
-
if (dryRun) {
|
|
288
|
-
result.filesWritten.push(path);
|
|
289
|
-
result.depsAdded.push(op.name);
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
await writeFile(path, contents);
|
|
293
|
-
result.filesWritten.push(path);
|
|
294
|
-
result.depsAdded.push(op.name);
|
|
295
|
-
}
|
|
296
|
-
async function readIfExists(path) {
|
|
297
|
-
try {
|
|
298
|
-
return await readFile(path, "utf8");
|
|
299
|
-
} catch (error) {
|
|
300
|
-
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return;
|
|
301
|
-
throw error;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
function abs(cwd, path) {
|
|
305
|
-
return join(cwd, path);
|
|
306
|
-
}
|
|
307
|
-
function deepMerge(target, patch) {
|
|
308
|
-
const out = { ...target };
|
|
309
|
-
for (const [key, value] of Object.entries(patch)) if (isObject(value) && isObject(out[key])) out[key] = deepMerge(out[key], value);
|
|
310
|
-
else out[key] = value;
|
|
311
|
-
return out;
|
|
312
|
-
}
|
|
313
|
-
//#endregion
|
|
314
|
-
//#region src/lib/orca/patchers/rule/reclaim.ts
|
|
315
|
-
/**
|
|
316
|
-
* The subset of a patcher plan's operations that `doctor --fix` re-applies:
|
|
317
|
-
* env merges, gitignore entries, dependency additions, and marker-bearing
|
|
318
|
-
* managed files (framework routes/middleware). Deliberately excludes the
|
|
319
|
-
* unmarked `.zitadel/` resource writes and `zitadel.json` — those are
|
|
320
|
-
* user-editable and synced by `apply`, so `--fix` must not clobber them.
|
|
321
|
-
*
|
|
322
|
-
* Pure: filters a freshly-allocated list; the input plan is not mutated.
|
|
323
|
-
*/
|
|
324
|
-
function reclaimableOps(plan) {
|
|
325
|
-
return plan.ops.filter((op) => op.kind === "merge-env" || op.kind === "append-gitignore" || op.kind === "add-dep" || op.kind === "write" && op.contents.includes("// zitadel-cli: managed-file v1"));
|
|
326
|
-
}
|
|
327
|
-
//#endregion
|
|
328
|
-
//#region src/lib/orca/patchers/rule/base.ts
|
|
329
|
-
/**
|
|
330
|
-
* Base for rule-based (deterministic, template-driven) patchers, as opposed to
|
|
331
|
-
* a future LLM-driven family. It applies the integration by building a
|
|
332
|
-
* file-operation plan and running the file-writer — that strategy stays
|
|
333
|
-
* entirely inside this family, so callers only ever see the family-neutral
|
|
334
|
-
* {@link Patcher} surface. Owns the framework-agnostic `.zitadel/` base files
|
|
335
|
-
* and the shared eject classification; subclasses contribute only their
|
|
336
|
-
* framework-specific routes/middleware.
|
|
337
|
-
*/
|
|
338
|
-
var AbstractRulePatcher = class {
|
|
339
|
-
/** Apply the full plan (base `.zitadel/` files + framework routes). */
|
|
340
|
-
async patch(ctx, opts) {
|
|
341
|
-
return scaffold(this.plan(ctx), opts);
|
|
342
|
-
}
|
|
343
|
-
/**
|
|
344
|
-
* Re-apply only the reclaimable subset — env files, gitignore, the SDK
|
|
345
|
-
* dependency, and marker-bearing routes — leaving the user-editable
|
|
346
|
-
* `.zitadel/` resources untouched. Backs `doctor --fix`.
|
|
347
|
-
*/
|
|
348
|
-
async repair(ctx, opts) {
|
|
349
|
-
const plan = this.plan(ctx);
|
|
350
|
-
return scaffold({
|
|
351
|
-
ops: reclaimableOps(plan),
|
|
352
|
-
summary: plan.summary
|
|
353
|
-
}, opts);
|
|
354
|
-
}
|
|
355
|
-
/** Shared base artifacts plus the subclass's marker-bearing route files. */
|
|
356
|
-
artifacts(view) {
|
|
357
|
-
return {
|
|
358
|
-
markedFiles: this.routeFiles(view),
|
|
359
|
-
rootConfigFiles: ["zitadel.json"],
|
|
360
|
-
directories: [".zitadel"],
|
|
361
|
-
envBackups: [".env.local"],
|
|
362
|
-
dependencies: this.routeDeps(view)
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
/**
|
|
366
|
-
* The full file-operation plan this patcher would apply. Public so rule-family
|
|
367
|
-
* unit tests can assert the planned ops directly; the generic {@link Patcher}
|
|
368
|
-
* interface deliberately does not expose it (an LLM patcher has no such plan).
|
|
369
|
-
*/
|
|
370
|
-
plan(ctx) {
|
|
371
|
-
return {
|
|
372
|
-
ops: [...this.baseOps(ctx), ...this.routeOps(ctx)],
|
|
373
|
-
summary: [this.summary(ctx)]
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
/**
|
|
377
|
-
* The framework-agnostic `.zitadel/` base files every rule patcher writes:
|
|
378
|
-
* the project secret, `zitadel.json`, env templates, and an empty sync
|
|
379
|
-
* state. The `schemas/` and `flows/` directories are created empty — the
|
|
380
|
-
* server provisions the default user schema and flow definition when the
|
|
381
|
-
* project is created, so nothing is scaffolded into them here. Pure: no
|
|
382
|
-
* filesystem or network.
|
|
383
|
-
*/
|
|
384
|
-
baseOps(ctx) {
|
|
385
|
-
return [
|
|
386
|
-
{
|
|
387
|
-
kind: "mkdir",
|
|
388
|
-
path: ".zitadel",
|
|
389
|
-
mode: 448
|
|
390
|
-
},
|
|
391
|
-
{
|
|
392
|
-
kind: "mkdir",
|
|
393
|
-
path: ".zitadel/flows"
|
|
394
|
-
},
|
|
395
|
-
{
|
|
396
|
-
kind: "mkdir",
|
|
397
|
-
path: ".zitadel/schemas"
|
|
398
|
-
},
|
|
399
|
-
{
|
|
400
|
-
kind: "append-gitignore",
|
|
401
|
-
entries: [
|
|
402
|
-
".zitadel/secret",
|
|
403
|
-
".env*",
|
|
404
|
-
"!.env.example"
|
|
405
|
-
]
|
|
406
|
-
},
|
|
407
|
-
{
|
|
408
|
-
kind: "write",
|
|
409
|
-
path: ".zitadel/secret",
|
|
410
|
-
mode: 384,
|
|
411
|
-
contents: `${stableStringify({
|
|
412
|
-
project_id: ctx.project.id,
|
|
413
|
-
project_secret: ctx.project.projectSecret,
|
|
414
|
-
preview_secret: ctx.project.previewSecret,
|
|
415
|
-
preview_origins: ctx.project.previewOrigins,
|
|
416
|
-
created_at: ctx.project.createdAt
|
|
417
|
-
})}\n`
|
|
418
|
-
},
|
|
419
|
-
{
|
|
420
|
-
kind: "write",
|
|
421
|
-
path: "zitadel.json",
|
|
422
|
-
contents: `${stableStringify(projectConfig(ctx))}\n`
|
|
423
|
-
},
|
|
424
|
-
{
|
|
425
|
-
kind: "merge-env",
|
|
426
|
-
path: ".env.example",
|
|
427
|
-
entries: {
|
|
428
|
-
ZITADEL_PROJECT_ID: "",
|
|
429
|
-
ZITADEL_ENVIRONMENT: "",
|
|
430
|
-
ZITADEL_ISSUER: "",
|
|
431
|
-
ZITADEL_URL: "",
|
|
432
|
-
NEXT_PUBLIC_ZITADEL_PROJECT_ID: ""
|
|
433
|
-
}
|
|
434
|
-
},
|
|
435
|
-
{
|
|
436
|
-
kind: "merge-env",
|
|
437
|
-
path: ".env.local",
|
|
438
|
-
entries: {
|
|
439
|
-
ZITADEL_PROJECT_ID: ctx.project.id,
|
|
440
|
-
ZITADEL_ENVIRONMENT: "development",
|
|
441
|
-
ZITADEL_ISSUER: ctx.issuer,
|
|
442
|
-
ZITADEL_URL: ctx.server,
|
|
443
|
-
NEXT_PUBLIC_ZITADEL_PROJECT_ID: ctx.project.id
|
|
444
|
-
}
|
|
445
|
-
},
|
|
446
|
-
{
|
|
447
|
-
kind: "write",
|
|
448
|
-
path: ".zitadel/state.json",
|
|
449
|
-
contents: `${stableStringify({
|
|
450
|
-
framework: ctx.framework.id,
|
|
451
|
-
resources: {}
|
|
452
|
-
})}\n`
|
|
453
|
-
}
|
|
454
|
-
];
|
|
455
|
-
}
|
|
456
|
-
};
|
|
457
|
-
/** Builds the `zitadel.json` body persisted at the project root. */
|
|
458
|
-
function projectConfig(ctx) {
|
|
459
|
-
const environments = { development: { issuer: ctx.issuer } };
|
|
460
|
-
if (ctx.project.previewOrigins.length > 0) environments.preview = { issuer_pattern: ctx.project.previewOrigins.map((origin) => `https://${origin}`) };
|
|
461
|
-
return {
|
|
462
|
-
$schema: "https://schemas.zitadel.com/v2/project.schema.json",
|
|
463
|
-
project: ctx.project.id,
|
|
464
|
-
server: resolveServerOrigin(ctx.server),
|
|
465
|
-
framework: { id: ctx.framework.id },
|
|
466
|
-
branding: {
|
|
467
|
-
renderer: ctx.rendererId,
|
|
468
|
-
attribution: "visible"
|
|
469
|
-
},
|
|
470
|
-
environments
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
/** Normalizes a server URL to its origin, falling back to {@link DEFAULT_SERVER}. */
|
|
474
|
-
function resolveServerOrigin(source) {
|
|
475
|
-
try {
|
|
476
|
-
return new URL(source).origin;
|
|
477
|
-
} catch {
|
|
478
|
-
return DEFAULT_SERVER;
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
//#endregion
|
|
482
|
-
//#region src/lib/orca/patchers/rule/next/renderers/lit/index.ts
|
|
483
|
-
/**
|
|
484
|
-
* Placeholder renderer for the `<zitadel-flow>` Lit web component. Declared
|
|
485
|
-
* so the `web-component` renderer id resolves and surfaces a clear
|
|
486
|
-
* "not yet published" error, while reserving the integration shape for when
|
|
487
|
-
* `@zitadel/ui-lit` ships. The `authPage` template emits an illustrative
|
|
488
|
-
* page only; this renderer is never selected for real scaffolding because
|
|
489
|
-
* `getRenderer` rejects any `status: "not-implemented"` spec.
|
|
490
|
-
*/
|
|
491
|
-
const litRenderer = {
|
|
492
|
-
id: "web-component",
|
|
493
|
-
displayName: "Web component (<zitadel-flow>)",
|
|
494
|
-
status: "not-implemented",
|
|
495
|
-
frameworks: [
|
|
496
|
-
"next",
|
|
497
|
-
"astro",
|
|
498
|
-
"remix",
|
|
499
|
-
"sveltekit",
|
|
500
|
-
"nuxt",
|
|
501
|
-
"vanilla"
|
|
502
|
-
],
|
|
503
|
-
dependency: {
|
|
504
|
-
name: "@zitadel/ui-lit",
|
|
505
|
-
version: "workspace:*"
|
|
506
|
-
},
|
|
507
|
-
templates: { authPage(mode) {
|
|
508
|
-
return {
|
|
509
|
-
mode,
|
|
510
|
-
contents: `${MANAGED_MARKER}
|
|
511
|
-
// The web component renderer ships a <zitadel-flow> element. Until
|
|
512
|
-
// @zitadel/ui-lit is published, this template only declares the
|
|
513
|
-
// intended integration point. See docs/design/cli/bdui-renderer.md.
|
|
514
|
-
import "@zitadel/ui-lit";
|
|
515
|
-
|
|
516
|
-
const environment =
|
|
517
|
-
process.env.ZITADEL_ENVIRONMENT ??
|
|
518
|
-
(process.env.NODE_ENV === "production" ? "production" : "development");
|
|
519
|
-
|
|
520
|
-
export default function ${mode === "login" ? "LoginPage" : "RegisterPage"}() {
|
|
521
|
-
return (
|
|
522
|
-
<zitadel-flow
|
|
523
|
-
purpose="${mode === "login" ? "login" : "register"}"
|
|
524
|
-
project-id={process.env.ZITADEL_PROJECT_ID}
|
|
525
|
-
issuer={process.env.ZITADEL_ISSUER}
|
|
526
|
-
environment={environment}
|
|
527
|
-
/>
|
|
528
|
-
);
|
|
529
|
-
}
|
|
530
|
-
`
|
|
531
|
-
};
|
|
532
|
-
} }
|
|
533
|
-
};
|
|
534
|
-
//#endregion
|
|
535
|
-
//#region src/lib/orca/patchers/rule/next/renderers/react/index.ts
|
|
536
|
-
/**
|
|
537
|
-
* The Next.js App Router renderer scaffolds `/login`, `/register`, and
|
|
538
|
-
* `/profile` pages that drive the `<zitadel-login>` and `<zitadel-logout>`
|
|
539
|
-
* Lit web components.
|
|
540
|
-
*
|
|
541
|
-
* Each page is a single client component (`"use client"`) that, inside a
|
|
542
|
-
* `next/dynamic({ ssr: false })` loader, builds the SDK project handle with
|
|
543
|
-
* `configureZitadel({ projectId, proxyPath: "/__nextgen" })` and passes it to
|
|
544
|
-
* the widget via `project={...}`. It also imports
|
|
545
|
-
* `@zitadel/sdk-next/client` for its `customElements.define`
|
|
546
|
-
* side-effect — importing `@zitadel/components` directly would fail on
|
|
547
|
-
* strict-resolution package managers (pnpm, yarn PnP) because the app only
|
|
548
|
-
* declares `sdk-next` as a direct dep. SSR is disabled because Lit's element
|
|
549
|
-
* registration needs a browser.
|
|
550
|
-
*
|
|
551
|
-
* The handle is passed as the `project` DOM property, which relies on React
|
|
552
|
-
* 19's custom-element property binding (the scaffold targets the latest Next /
|
|
553
|
-
* React). The backend URL never reaches the browser: the client talks to the
|
|
554
|
-
* same-origin `/__nextgen` proxy path, and the scaffolded `middleware.ts`
|
|
555
|
-
* forwards it to `ZITADEL_URL` server-side. `NEXT_PUBLIC_ZITADEL_PROJECT_ID` is
|
|
556
|
-
* public — the project id is not sensitive and the widget needs it to start a
|
|
557
|
-
* flow.
|
|
558
|
-
*/
|
|
559
|
-
const reactRenderer = {
|
|
560
|
-
id: "react",
|
|
561
|
-
displayName: "React (Next.js App Router)",
|
|
562
|
-
status: "available",
|
|
563
|
-
frameworks: ["next"],
|
|
564
|
-
dependency: {
|
|
565
|
-
name: "@zitadel/sdk-next",
|
|
566
|
-
version: "latest"
|
|
567
|
-
},
|
|
568
|
-
templates: {
|
|
569
|
-
authPage(mode) {
|
|
570
|
-
const componentName = mode === "login" ? "LoginPage" : "RegisterPage";
|
|
571
|
-
const elementName = mode === "login" ? "ZitadelLogin" : "ZitadelRegister";
|
|
572
|
-
return {
|
|
573
|
-
mode,
|
|
574
|
-
contents: `${MANAGED_MARKER}
|
|
575
|
-
"use client";
|
|
576
|
-
|
|
577
|
-
import dynamic from "next/dynamic";
|
|
578
|
-
|
|
579
|
-
const ${elementName} = dynamic(
|
|
580
|
-
async () => {
|
|
581
|
-
const { configureZitadel } = await import("@zitadel/sdk-next/client");
|
|
582
|
-
// Build the SDK project handle and pass it to the component via the
|
|
583
|
-
// \`project\` prop. The component reads config from this prop directly, so
|
|
584
|
-
// it works regardless of how the SDK packages are bundled. The backend URL
|
|
585
|
-
// stays server-side — requests go through the proxy path "/__nextgen",
|
|
586
|
-
// which the scaffolded middleware forwards to the Zitadel server.
|
|
587
|
-
const project = configureZitadel({
|
|
588
|
-
projectId: process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID ?? "",
|
|
589
|
-
proxyPath: "/__nextgen",
|
|
590
|
-
});
|
|
591
|
-
return function ${elementName}Element() {
|
|
592
|
-
return (
|
|
593
|
-
<zitadel-login
|
|
594
|
-
project={project}
|
|
595
|
-
purpose="${mode}"
|
|
596
|
-
post-sign-in-url="/profile"
|
|
597
|
-
/>
|
|
598
|
-
);
|
|
599
|
-
};
|
|
600
|
-
},
|
|
601
|
-
{ ssr: false },
|
|
602
|
-
);
|
|
603
|
-
|
|
604
|
-
export default function ${componentName}() {
|
|
605
|
-
return (
|
|
606
|
-
<main style={{ minHeight: "100vh", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
|
607
|
-
<${elementName} />
|
|
608
|
-
</main>
|
|
609
|
-
);
|
|
610
|
-
}
|
|
611
|
-
`
|
|
612
|
-
};
|
|
613
|
-
},
|
|
614
|
-
profilePage() {
|
|
615
|
-
return { contents: `${MANAGED_MARKER}
|
|
616
|
-
"use client";
|
|
617
|
-
|
|
618
|
-
import dynamic from "next/dynamic";
|
|
619
|
-
|
|
620
|
-
const ZitadelLogout = dynamic(
|
|
621
|
-
async () => {
|
|
622
|
-
const { configureZitadel } = await import("@zitadel/sdk-next/client");
|
|
623
|
-
const project = configureZitadel({
|
|
624
|
-
projectId: process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID ?? "",
|
|
625
|
-
proxyPath: "/__nextgen",
|
|
626
|
-
});
|
|
627
|
-
return function ZitadelLogoutElement() {
|
|
628
|
-
return (
|
|
629
|
-
<zitadel-logout
|
|
630
|
-
project={project}
|
|
631
|
-
post-sign-out-url="/login"
|
|
632
|
-
/>
|
|
633
|
-
);
|
|
634
|
-
};
|
|
635
|
-
},
|
|
636
|
-
{ ssr: false },
|
|
637
|
-
);
|
|
638
|
-
|
|
639
|
-
export default function ProfilePage() {
|
|
640
|
-
return (
|
|
641
|
-
<main style={{ padding: "48px", maxWidth: "600px", margin: "0 auto" }}>
|
|
642
|
-
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "24px" }}>
|
|
643
|
-
<h1 style={{ fontSize: "24px", fontWeight: 700, margin: 0 }}>Signed in</h1>
|
|
644
|
-
<ZitadelLogout />
|
|
645
|
-
</div>
|
|
646
|
-
<p style={{ color: "#6b7280" }}>You are signed in. Use the button above to log out.</p>
|
|
647
|
-
</main>
|
|
648
|
-
);
|
|
649
|
-
}
|
|
650
|
-
` };
|
|
651
|
-
},
|
|
652
|
-
customElementsDts() {
|
|
653
|
-
return { contents: `${MANAGED_MARKER}
|
|
654
|
-
import type React from "react";
|
|
655
|
-
import type { ZitadelProject } from "@zitadel/sdk-next/client";
|
|
656
|
-
|
|
657
|
-
declare module "react" {
|
|
658
|
-
namespace JSX {
|
|
659
|
-
interface IntrinsicElements {
|
|
660
|
-
"zitadel-login": React.HTMLAttributes<HTMLElement> & {
|
|
661
|
-
project?: ZitadelProject;
|
|
662
|
-
"session-exchange-path"?: string;
|
|
663
|
-
"post-sign-in-url"?: string;
|
|
664
|
-
purpose?: string;
|
|
665
|
-
};
|
|
666
|
-
"zitadel-logout": React.HTMLAttributes<HTMLElement> & {
|
|
667
|
-
project?: ZitadelProject;
|
|
668
|
-
"post-sign-out-url"?: string;
|
|
669
|
-
};
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
` };
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
};
|
|
677
|
-
//#endregion
|
|
678
|
-
//#region src/lib/orca/patchers/rule/next/renderers/registry.ts
|
|
679
|
-
/**
|
|
680
|
-
* Runtime mirror of the {@link RendererId} union, used by {@link isRendererId}
|
|
681
|
-
* to validate untrusted strings (a TS union has no runtime presence). Must stay
|
|
682
|
-
* in sync with the {@link RendererId} type.
|
|
683
|
-
*/
|
|
684
|
-
const RENDERER_IDS = ["react", "web-component"];
|
|
685
|
-
/**
|
|
686
|
-
* Type guard narrowing an arbitrary value to a {@link RendererId}, used to
|
|
687
|
-
* validate renderer ids read from config before indexing {@link RENDERERS}.
|
|
688
|
-
*/
|
|
689
|
-
function isRendererId(value) {
|
|
690
|
-
return typeof value === "string" && RENDERER_IDS.includes(value);
|
|
691
|
-
}
|
|
692
|
-
/**
|
|
693
|
-
* The single source of truth mapping each {@link RendererId} to its spec.
|
|
694
|
-
* Keyed by id so {@link getRenderer} can look up and validate a renderer
|
|
695
|
-
* chosen from persisted config (an arbitrary string) at runtime.
|
|
696
|
-
*/
|
|
697
|
-
const RENDERERS = {
|
|
698
|
-
react: reactRenderer,
|
|
699
|
-
"web-component": litRenderer
|
|
700
|
-
};
|
|
701
|
-
/**
|
|
702
|
-
* Resolves a renderer id (an untrusted string from config) to its spec,
|
|
703
|
-
* throwing a typed {@link ZitadelError} rather than returning `undefined`
|
|
704
|
-
* so callers get an actionable message. Rejects ids that are unknown
|
|
705
|
-
* (`E_VALIDATION`) or declared-but-unpublished (`E_NOT_IMPLEMENTED`),
|
|
706
|
-
* guaranteeing the returned spec is safe to scaffold from.
|
|
707
|
-
*/
|
|
708
|
-
function getRenderer(id) {
|
|
709
|
-
if (!isRendererId(id)) throw new ZitadelError("E_VALIDATION", `Unknown renderer "${id}"`, { hint: `Available renderers: ${Object.keys(RENDERERS).join(", ")}` });
|
|
710
|
-
const renderer = RENDERERS[id];
|
|
711
|
-
if (renderer.status === "not-implemented") throw new ZitadelError("E_NOT_IMPLEMENTED", `Renderer "${id}" is declared but not yet published`, { hint: "Use --renderer react for now; the <zitadel-flow> web component ships in a later package." });
|
|
712
|
-
return renderer;
|
|
713
|
-
}
|
|
714
|
-
//#endregion
|
|
715
|
-
//#region src/lib/orca/patchers/rule/next/index.ts
|
|
716
|
-
/**
|
|
717
|
-
* Next.js `middleware.ts` at the project root. Wires `nextgenMiddleware` so the
|
|
718
|
-
* scaffolded `<zitadel-login api-base="/__nextgen">` requests are same-origin
|
|
719
|
-
* proxied to `ZITADEL_URL` and `/profile` is gated. The `middleware`
|
|
720
|
-
* form (not the Next 16 `proxy` rename) works on every supported Next major.
|
|
721
|
-
* Carries the managed marker so `doctor --fix` reclaims it and `eject` removes it.
|
|
722
|
-
*/
|
|
723
|
-
const middlewareTemplate = `${MANAGED_MARKER}
|
|
724
|
-
import { nextgenMiddleware } from "@zitadel/sdk-next/middleware";
|
|
725
|
-
import type { NextRequest } from "next/server";
|
|
726
|
-
|
|
727
|
-
export function middleware(req: NextRequest) {
|
|
728
|
-
return nextgenMiddleware(req, {
|
|
729
|
-
url: process.env.ZITADEL_URL,
|
|
730
|
-
protectedRoutes: ["/profile"],
|
|
731
|
-
loginPath: "/login",
|
|
732
|
-
});
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
export const config = {
|
|
736
|
-
matcher: ["/__nextgen/:path*", "/profile/:path*"],
|
|
737
|
-
};
|
|
738
|
-
`;
|
|
739
|
-
/**
|
|
740
|
-
* Rule-based patcher for the Next.js App Router. Inherits the shared
|
|
741
|
-
* `.zitadel/` base files from {@link AbstractRulePatcher} and contributes the
|
|
742
|
-
* Next routes/middleware whose templates come from the chosen renderer.
|
|
743
|
-
*/
|
|
744
|
-
var NextPatcher = class extends AbstractRulePatcher {
|
|
745
|
-
/** Returns true for Next.js projects. */
|
|
746
|
-
canPatch(framework) {
|
|
747
|
-
return framework === "next";
|
|
748
|
-
}
|
|
749
|
-
routeOps(ctx) {
|
|
750
|
-
return nextCodeOps(ctx, getRenderer(ctx.rendererId));
|
|
751
|
-
}
|
|
752
|
-
routeFiles(view) {
|
|
753
|
-
return nextCodeFilePaths(view.framework.appDir, getRenderer(view.rendererId));
|
|
754
|
-
}
|
|
755
|
-
routeDeps(view) {
|
|
756
|
-
return [getRenderer(view.rendererId).dependency.name];
|
|
757
|
-
}
|
|
758
|
-
summary(ctx) {
|
|
759
|
-
return {
|
|
760
|
-
title: "Next.js integration",
|
|
761
|
-
detail: `Scaffolded login/register/profile routes with renderer "${ctx.rendererId}".`
|
|
762
|
-
};
|
|
763
|
-
}
|
|
764
|
-
};
|
|
765
|
-
/**
|
|
766
|
-
* Ordered paths of the framework code files the patcher writes. All carry the
|
|
767
|
-
* managed marker. Shared by {@link NextPatcher.routeOps} (which adds contents)
|
|
768
|
-
* and {@link NextPatcher.routeFiles} (which only needs the paths) so the two
|
|
769
|
-
* cannot drift.
|
|
770
|
-
*/
|
|
771
|
-
function nextCodeFilePaths(appDir, renderer) {
|
|
772
|
-
const paths = [join(appDir, "login/page.tsx"), join(appDir, "register/page.tsx")];
|
|
773
|
-
if (renderer.templates.profilePage) paths.push(join(appDir, "profile/page.tsx"));
|
|
774
|
-
paths.push(join(appDir, "../middleware.ts"));
|
|
775
|
-
if (renderer.templates.provider) paths.push(join(appDir, renderer.templates.provider.filename));
|
|
776
|
-
if (renderer.templates.customElementsDts) paths.push(join(appDir, "../custom-elements.d.ts"));
|
|
777
|
-
return paths;
|
|
778
|
-
}
|
|
779
|
-
/** The Next route/middleware write ops plus the SDK dependency. */
|
|
780
|
-
function nextCodeOps(ctx, renderer) {
|
|
781
|
-
const appDir = ctx.framework.appDir;
|
|
782
|
-
const ops = [{
|
|
783
|
-
kind: "write",
|
|
784
|
-
path: join(appDir, "login/page.tsx"),
|
|
785
|
-
contents: renderer.templates.authPage("login").contents
|
|
786
|
-
}, {
|
|
787
|
-
kind: "write",
|
|
788
|
-
path: join(appDir, "register/page.tsx"),
|
|
789
|
-
contents: renderer.templates.authPage("register").contents
|
|
790
|
-
}];
|
|
791
|
-
const profile = renderer.templates.profilePage?.();
|
|
792
|
-
if (profile) ops.push({
|
|
793
|
-
kind: "write",
|
|
794
|
-
path: join(appDir, "profile/page.tsx"),
|
|
795
|
-
contents: profile.contents
|
|
796
|
-
});
|
|
797
|
-
ops.push({
|
|
798
|
-
kind: "write",
|
|
799
|
-
path: join(appDir, "../middleware.ts"),
|
|
800
|
-
contents: middlewareTemplate
|
|
801
|
-
});
|
|
802
|
-
const provider = renderer.templates.provider;
|
|
803
|
-
if (provider) ops.push({
|
|
804
|
-
kind: "write",
|
|
805
|
-
path: join(appDir, provider.filename),
|
|
806
|
-
contents: provider.contents
|
|
807
|
-
});
|
|
808
|
-
const dts = renderer.templates.customElementsDts?.();
|
|
809
|
-
if (dts) ops.push({
|
|
810
|
-
kind: "write",
|
|
811
|
-
path: join(appDir, "../custom-elements.d.ts"),
|
|
812
|
-
contents: dts.contents
|
|
813
|
-
});
|
|
814
|
-
ops.push({
|
|
815
|
-
kind: "add-dep",
|
|
816
|
-
name: renderer.dependency.name,
|
|
817
|
-
version: renderer.dependency.version
|
|
818
|
-
});
|
|
819
|
-
return ops;
|
|
820
|
-
}
|
|
821
|
-
//#endregion
|
|
822
|
-
//#region src/lib/orca/patchers/index.ts
|
|
823
|
-
/**
|
|
824
|
-
* Active patchers, in priority order; the first whose `canPatch` matches wins.
|
|
825
|
-
*
|
|
826
|
-
* Patchers are grouped by family under subdirectories: `rule/` holds the
|
|
827
|
-
* deterministic, template-driven patchers (extending
|
|
828
|
-
* {@link import("./rule/base").AbstractRulePatcher}). A future LLM-driven
|
|
829
|
-
* family lives under `llm/` and registers its concrete patchers here — no
|
|
830
|
-
* orchestrator or command changes needed. Only Next.js is supported today.
|
|
831
|
-
*/
|
|
832
|
-
const patchers = [new NextPatcher()];
|
|
833
|
-
//#endregion
|
|
834
|
-
//#region src/lib/orca/scaffolders/cli.ts
|
|
835
|
-
/**
|
|
836
|
-
* Base for scaffolders that delegate to an external CLI (e.g. create-next-app).
|
|
837
|
-
* Subclasses implement {@link scaffold} and call {@link runCommand}.
|
|
838
|
-
*/
|
|
839
|
-
var AbstractCLIScaffolder = class {
|
|
840
|
-
/** True when the requested framework is in {@link supportedFrameworks}. */
|
|
841
|
-
canScaffold(framework) {
|
|
842
|
-
return this.supportedFrameworks.includes(framework);
|
|
843
|
-
}
|
|
844
|
-
/**
|
|
845
|
-
* Runs an external command in `cwd`, throwing a typed {@link ZitadelError} on
|
|
846
|
-
* failure so the cause surfaces as a categorized CLI error. Distinguishes
|
|
847
|
-
* "binary not on PATH" (`ENOENT` from the spawn itself) from "binary ran but
|
|
848
|
-
* exited non-zero" — the former previously got masked as a generic
|
|
849
|
-
* `exited with status 1`, leaving users to guess. Tests stub
|
|
850
|
-
* `node:child_process` to assert the command without spawning.
|
|
851
|
-
*/
|
|
852
|
-
runCommand(command, args, cwd) {
|
|
853
|
-
const result = spawnSync(command, [...args], {
|
|
854
|
-
cwd,
|
|
855
|
-
encoding: "utf8"
|
|
856
|
-
});
|
|
857
|
-
if (result.error) {
|
|
858
|
-
const err = result.error;
|
|
859
|
-
const notFound = err.code === "ENOENT";
|
|
860
|
-
throw new ZitadelError("E_VALIDATION", notFound ? `Command not found: ${command}` : `Failed to spawn "${command}": ${err.message}`, {
|
|
861
|
-
hint: notFound ? `Ensure '${command}' is installed and on PATH.` : void 0,
|
|
862
|
-
details: {
|
|
863
|
-
command,
|
|
864
|
-
args: [...args],
|
|
865
|
-
code: err.code
|
|
866
|
-
}
|
|
867
|
-
});
|
|
868
|
-
}
|
|
869
|
-
const status = result.status ?? 1;
|
|
870
|
-
if (status !== 0) throw new ZitadelError("E_VALIDATION", `Command "${command} ${args.join(" ")}" exited with status ${String(status)}`, { details: { stderr: result.stderr ?? "" } });
|
|
871
|
-
}
|
|
872
|
-
};
|
|
873
|
-
//#endregion
|
|
874
|
-
//#region src/lib/orca/scaffolders/next.ts
|
|
875
|
-
/** Scaffolds a new Next.js App Router project with `create-next-app`. */
|
|
876
|
-
var NextScaffolder = class extends AbstractCLIScaffolder {
|
|
877
|
-
displayName = "Next.js";
|
|
878
|
-
supportedFrameworks = ["next"];
|
|
879
|
-
/**
|
|
880
|
-
* Runs `npx create-next-app@latest . --ts --app --no-git --yes` in `cwd`,
|
|
881
|
-
* creating a TypeScript App Router project in place. `--yes` accepts all
|
|
882
|
-
* defaults so the command runs unattended.
|
|
883
|
-
*/
|
|
884
|
-
async scaffold(cwd, _framework) {
|
|
885
|
-
this.runCommand("npx", [
|
|
886
|
-
"create-next-app@latest",
|
|
887
|
-
".",
|
|
888
|
-
"--ts",
|
|
889
|
-
"--app",
|
|
890
|
-
"--no-git",
|
|
891
|
-
"--yes"
|
|
892
|
-
], cwd);
|
|
893
|
-
}
|
|
894
|
-
};
|
|
895
|
-
//#endregion
|
|
896
|
-
//#region src/lib/orca/scaffolders/index.ts
|
|
897
|
-
/**
|
|
898
|
-
* Active scaffolders, in priority order. The framework picker derives its
|
|
899
|
-
* choices from this list. Add a new framework by appending its scaffolder
|
|
900
|
-
* here — no orchestrator changes needed.
|
|
901
|
-
*/
|
|
902
|
-
const scaffolders = [new NextScaffolder()];
|
|
903
|
-
//#endregion
|
|
904
|
-
//#region src/lib/orca/index.ts
|
|
905
|
-
/**
|
|
906
|
-
* Orchestrates the three per-framework strategies — detectors (recognise an
|
|
907
|
-
* existing project and extract its facts), scaffolders (create a project), and
|
|
908
|
-
* patchers (integrate Zitadel) — over their respective registries. It resolves
|
|
909
|
-
* the right strategy for a framework and drives the detect/scaffold lifecycle;
|
|
910
|
-
* how a patcher applies its work (file operations vs an LLM agent) stays
|
|
911
|
-
* internal to that patcher. Registries are injected so tests can supply fakes.
|
|
912
|
-
*/
|
|
913
|
-
var Orca = class {
|
|
914
|
-
constructor(detectors, scaffolders, patchers) {
|
|
915
|
-
this.detectors = detectors;
|
|
916
|
-
this.scaffolders = scaffolders;
|
|
917
|
-
this.patchers = patchers;
|
|
918
|
-
}
|
|
919
|
-
/**
|
|
920
|
-
* Detects the framework in `cwd` and extracts its {@link FrameworkFacts},
|
|
921
|
-
* honouring an explicit `requested` framework. Throws
|
|
922
|
-
* `E_FRAMEWORK_NOT_DETECTED` when nothing matches; a detector's
|
|
923
|
-
* `E_UNSUPPORTED_PROJECT_SHAPE` (recognised but unsupported) propagates.
|
|
924
|
-
*/
|
|
925
|
-
async detect(cwd, requested) {
|
|
926
|
-
const candidates = requested ? this.detectors.filter((detector) => detector.framework === requested) : this.detectors;
|
|
927
|
-
if (requested && candidates.length === 0) throw new ZitadelError("E_FRAMEWORK_NOT_DETECTED", `Unsupported framework "${requested}"`, { hint: `Supported frameworks: ${this.frameworkIds().join(", ")}.` });
|
|
928
|
-
for (const detector of candidates) {
|
|
929
|
-
const facts = await detector.detect(cwd);
|
|
930
|
-
if (facts) return facts;
|
|
931
|
-
}
|
|
932
|
-
throw new ZitadelError("E_FRAMEWORK_NOT_DETECTED", "Could not detect a supported framework", { hint: `Run this from a supported project (${this.frameworkIds().join(", ")}) or pass --cwd <path>.` });
|
|
933
|
-
}
|
|
934
|
-
/**
|
|
935
|
-
* Non-throwing detection: returns `undefined` instead of raising for a
|
|
936
|
-
* project that is absent, unrecognised, or recognised-but-unsupported, so
|
|
937
|
-
* callers (e.g. `eject`) can probe and degrade gracefully.
|
|
938
|
-
*/
|
|
939
|
-
async tryDetect(cwd) {
|
|
940
|
-
try {
|
|
941
|
-
return await this.detect(cwd);
|
|
942
|
-
} catch (error) {
|
|
943
|
-
if (error instanceof ZitadelError && (error.code === "E_FRAMEWORK_NOT_DETECTED" || error.code === "E_UNSUPPORTED_PROJECT_SHAPE")) return;
|
|
944
|
-
throw error;
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
/**
|
|
948
|
-
* Whether `cwd` has no `package.json`, i.e. it is empty (or non-Node) and
|
|
949
|
-
* should be scaffolded from scratch rather than detected/patched.
|
|
950
|
-
*/
|
|
951
|
-
async isEmpty(cwd) {
|
|
952
|
-
try {
|
|
953
|
-
await readFile(join(cwd, "package.json"), "utf8");
|
|
954
|
-
return false;
|
|
955
|
-
} catch {
|
|
956
|
-
return true;
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
/**
|
|
960
|
-
* Creates a new `framework` project in `cwd`, then re-detects it to return
|
|
961
|
-
* the resulting {@link FrameworkFacts}. Throws `E_CONFLICT` when the directory
|
|
962
|
-
* already contains a project ("already scaffolded") and `E_VALIDATION` when no
|
|
963
|
-
* scaffolder supports the framework.
|
|
964
|
-
*/
|
|
965
|
-
async scaffold(cwd, framework) {
|
|
966
|
-
if (!await this.isEmpty(cwd)) throw new ZitadelError("E_CONFLICT", `Cannot scaffold: ${cwd} already contains a project`, { hint: "Run setup in an empty directory, or integrate the existing project instead." });
|
|
967
|
-
await this.scaffolderFor(framework).scaffold(cwd, framework);
|
|
968
|
-
return this.detect(cwd, framework);
|
|
969
|
-
}
|
|
970
|
-
/**
|
|
971
|
-
* Resolves the scaffolder for a framework, throwing `E_VALIDATION` (with the
|
|
972
|
-
* available list) when none matches.
|
|
973
|
-
*/
|
|
974
|
-
scaffolderFor(framework) {
|
|
975
|
-
const scaffolder = this.scaffolders.find((candidate) => candidate.canScaffold(framework));
|
|
976
|
-
if (!scaffolder) throw new ZitadelError("E_VALIDATION", `No scaffolder supports "${framework}"`, { hint: `Available frameworks: ${this.availableFrameworks().map((f) => f.id).join(", ")}.` });
|
|
977
|
-
return scaffolder;
|
|
978
|
-
}
|
|
979
|
-
/**
|
|
980
|
-
* Resolves the patcher for a framework, throwing `E_VALIDATION` when none
|
|
981
|
-
* matches (e.g. a framework that can be scaffolded but not yet integrated).
|
|
982
|
-
*/
|
|
983
|
-
patcherFor(framework) {
|
|
984
|
-
const patcher = this.patchers.find((candidate) => candidate.canPatch(framework));
|
|
985
|
-
if (!patcher) throw new ZitadelError("E_VALIDATION", `No patcher supports "${framework}"`, { hint: "Zitadel integration currently supports Next.js." });
|
|
986
|
-
return patcher;
|
|
987
|
-
}
|
|
988
|
-
/** The frameworks that can be scaffolded, derived from the scaffolder registry. */
|
|
989
|
-
availableFrameworks() {
|
|
990
|
-
return this.scaffolders.map((scaffolder) => ({
|
|
991
|
-
id: scaffolder.supportedFrameworks[0] ?? scaffolder.displayName,
|
|
992
|
-
displayName: scaffolder.displayName
|
|
993
|
-
}));
|
|
994
|
-
}
|
|
995
|
-
frameworkIds() {
|
|
996
|
-
return this.detectors.map((detector) => detector.framework);
|
|
997
|
-
}
|
|
998
|
-
};
|
|
999
|
-
/** {@link Orca} wired with the default detector, scaffolder, and patcher registries. */
|
|
1000
|
-
function createOrca() {
|
|
1001
|
-
return new Orca(detectors, scaffolders, patchers);
|
|
1002
|
-
}
|
|
1003
|
-
//#endregion
|
|
1004
|
-
export { RENDERER_IDS as n, issuerFromPort as r, createOrca as t };
|
|
1005
|
-
|
|
1006
|
-
//# sourceMappingURL=orca-COsUnVoz.mjs.map
|