@brewnet/cli 0.0.1 → 0.0.2

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 (33) hide show
  1. package/dist/admin-server-UODBPGWR.js +16 -0
  2. package/dist/app-manager-FIHPVUP7.js +51 -0
  3. package/dist/{boilerplate-manager-P6QYUU7Q.js → boilerplate-manager-WEFTHL2O.js} +3 -3
  4. package/dist/{chunk-DH2VK3YI.js → chunk-54WFZCU6.js} +2 -2
  5. package/dist/{chunk-4TJMJZMO.js → chunk-AXSHZEB3.js} +63 -1
  6. package/dist/{chunk-4TJMJZMO.js.map → chunk-AXSHZEB3.js.map} +1 -1
  7. package/dist/chunk-FZZ3HP2G.js +1151 -0
  8. package/dist/chunk-FZZ3HP2G.js.map +1 -0
  9. package/dist/chunk-JIPAYMOA.js +58 -0
  10. package/dist/chunk-JIPAYMOA.js.map +1 -0
  11. package/dist/{chunk-SIXBB6JU.js → chunk-Q6UUZR2V.js} +238 -1171
  12. package/dist/chunk-Q6UUZR2V.js.map +1 -0
  13. package/dist/{chunk-2VWMDHGI.js → chunk-YAYXULLO.js} +9 -61
  14. package/dist/chunk-YAYXULLO.js.map +1 -0
  15. package/dist/{chunk-JFPHGZ6Z.js → chunk-YXFDB5YX.js} +17 -2
  16. package/dist/chunk-YXFDB5YX.js.map +1 -0
  17. package/dist/{cloudflare-client-TFT6VCXF.js → cloudflare-client-F2TGQXGS.js} +2 -2
  18. package/dist/{compose-generator-O7GSIJ2S.js → compose-generator-OFJ2YWMB.js} +4 -2
  19. package/dist/compose-generator-OFJ2YWMB.js.map +1 -0
  20. package/dist/index.js +122 -168
  21. package/dist/index.js.map +1 -1
  22. package/dist/services/admin-daemon.js +6 -4
  23. package/dist/services/admin-daemon.js.map +1 -1
  24. package/package.json +1 -1
  25. package/dist/admin-server-DQVIEHV3.js +0 -14
  26. package/dist/chunk-2VWMDHGI.js.map +0 -1
  27. package/dist/chunk-JFPHGZ6Z.js.map +0 -1
  28. package/dist/chunk-SIXBB6JU.js.map +0 -1
  29. /package/dist/{admin-server-DQVIEHV3.js.map → admin-server-UODBPGWR.js.map} +0 -0
  30. /package/dist/{boilerplate-manager-P6QYUU7Q.js.map → app-manager-FIHPVUP7.js.map} +0 -0
  31. /package/dist/{cloudflare-client-TFT6VCXF.js.map → boilerplate-manager-WEFTHL2O.js.map} +0 -0
  32. /package/dist/{chunk-DH2VK3YI.js.map → chunk-54WFZCU6.js.map} +0 -0
  33. /package/dist/{compose-generator-O7GSIJ2S.js.map → cloudflare-client-F2TGQXGS.js.map} +0 -0
@@ -1,26 +1,38 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ createApp,
4
+ deployApp,
5
+ detectBasePath,
6
+ getAppBranches,
7
+ getAppDir,
8
+ getAppGitInfo,
9
+ getDeployHistory,
10
+ getDeploySettings,
11
+ getJobStatus,
12
+ listApps,
13
+ listGiteaRepos,
14
+ removeApp,
15
+ rollbackApp,
16
+ startApp,
17
+ stopApp,
18
+ updateDeploySettings
19
+ } from "./chunk-FZZ3HP2G.js";
2
20
  import {
3
21
  DomainManager,
4
- addApp,
5
22
  addService,
6
- appendDeployHistory,
7
23
  createBackup,
8
24
  getLogStats,
9
25
  listBackups,
10
26
  queryLogs,
11
- readApps,
12
- readDeployHistory,
13
- removeApp,
14
- removeService,
15
- updateApp
16
- } from "./chunk-2VWMDHGI.js";
27
+ removeService
28
+ } from "./chunk-YAYXULLO.js";
17
29
  import {
18
30
  verifyToken
19
- } from "./chunk-JFPHGZ6Z.js";
31
+ } from "./chunk-YXFDB5YX.js";
20
32
  import {
21
33
  SERVICE_REGISTRY,
22
34
  getServiceDefinition
23
- } from "./chunk-4TJMJZMO.js";
35
+ } from "./chunk-AXSHZEB3.js";
24
36
  import {
25
37
  getLastProject,
26
38
  loadState,
@@ -33,1099 +45,13 @@ import {
33
45
  // src/services/admin-server.ts
34
46
  import { createServer } from "http";
35
47
  import { createConnection } from "net";
36
- import { join as join2, resolve, extname } from "path";
37
- import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, statSync, createReadStream } from "fs";
48
+ import { join, resolve, extname } from "path";
49
+ import { existsSync, readFileSync, writeFileSync, statSync, createReadStream } from "fs";
38
50
  import { fileURLToPath } from "url";
39
- import { homedir as homedir2 } from "os";
40
- import Dockerode from "dockerode";
41
-
42
- // src/services/app-manager.ts
43
- import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, readdirSync } from "fs";
44
- import { join } from "path";
45
51
  import { homedir } from "os";
46
- import { randomBytes } from "crypto";
47
- import { execa } from "execa";
48
-
49
- // src/services/gitea-client.ts
50
- import { existsSync, readFileSync, writeFileSync, chmodSync, mkdirSync, unlinkSync } from "fs";
51
- import { dirname } from "path";
52
- import { execSync } from "child_process";
53
- var GiteaClient = class {
54
- config;
55
- constructor(config) {
56
- this.config = config;
57
- }
58
- // ---------------------------------------------------------------------------
59
- // Token management
60
- // ---------------------------------------------------------------------------
61
- /**
62
- * Create a Gitea API token via Basic Auth.
63
- * If the admin account has mustChangePassword=true (403), auto-fixes via docker exec and retries.
64
- * Saves the token to tokenPath on success.
65
- */
66
- async _createToken() {
67
- const { tokenPath, baseUrl, username, password } = this.config;
68
- const basic = Buffer.from(`${username}:${password}`).toString("base64");
69
- const makeRequest = () => fetch(`${baseUrl}/api/v1/users/${username}/tokens`, {
70
- method: "POST",
71
- headers: { Authorization: `Basic ${basic}`, "Content-Type": "application/json" },
72
- body: JSON.stringify({
73
- name: `brewnet-${Date.now()}`,
74
- scopes: ["write:repository", "read:repository", "write:user", "read:user"]
75
- }),
76
- signal: AbortSignal.timeout(8e3)
77
- });
78
- let res = await makeRequest();
79
- let wasFixed = false;
80
- if (!res.ok) {
81
- const body = await res.text();
82
- if (res.status === 403 && body.includes("must change")) {
83
- try {
84
- execSync(
85
- `docker exec -u git brewnet-gitea gitea admin user change-password --username ${username} --password ${password} --must-change-password=false`,
86
- { stdio: "pipe" }
87
- );
88
- } catch (e) {
89
- const stderr = e.stderr?.toString().trim() ?? String(e);
90
- throw new Error(
91
- `Gitea admin requires password change \u2014 auto-fix failed:
92
- ${stderr}
93
- Manual fix: docker exec -u git brewnet-gitea gitea admin user change-password --username ${username} --password <password> --must-change-password=false`
94
- );
95
- }
96
- wasFixed = true;
97
- res = await makeRequest();
98
- if (!res.ok) {
99
- throw new Error(
100
- `Gitea token creation failed after auto-fix: ${res.status} ${await res.text()}`
101
- );
102
- }
103
- } else {
104
- throw new Error(`Gitea token creation failed: ${res.status} ${body}`);
105
- }
106
- }
107
- const data = await res.json();
108
- mkdirSync(dirname(tokenPath), { recursive: true });
109
- writeFileSync(tokenPath, data.sha1, "utf-8");
110
- chmodSync(tokenPath, 384);
111
- return { wasFixed };
112
- }
113
- /**
114
- * Explicit setup step — call once before any API operations.
115
- * Validates any cached token; deletes and re-creates if stale (401).
116
- * Returns what happened so the caller can surface it in job step logs.
117
- */
118
- async prepare() {
119
- const { tokenPath, baseUrl } = this.config;
120
- if (existsSync(tokenPath)) {
121
- const token = readFileSync(tokenPath, "utf-8").trim();
122
- try {
123
- const check = await fetch(`${baseUrl}/api/v1/user`, {
124
- headers: { Authorization: `token ${token}` },
125
- signal: AbortSignal.timeout(8e3)
126
- });
127
- if (check.status !== 401) {
128
- return { autoFixed: false, message: "token cached" };
129
- }
130
- unlinkSync(tokenPath);
131
- } catch {
132
- return { autoFixed: false, message: "token cached (network check skipped)" };
133
- }
134
- }
135
- const { wasFixed } = await this._createToken();
136
- return {
137
- autoFixed: wasFixed,
138
- message: wasFixed ? "mustChangePassword was set \u2014 auto-fixed via docker exec; token created" : "token created"
139
- };
140
- }
141
- async ensureToken() {
142
- const { tokenPath } = this.config;
143
- if (existsSync(tokenPath)) {
144
- return readFileSync(tokenPath, "utf-8").trim();
145
- }
146
- await this._createToken();
147
- return readFileSync(tokenPath, "utf-8").trim();
148
- }
149
- async authHeaders() {
150
- return {
151
- Authorization: `token ${await this.ensureToken()}`,
152
- "Content-Type": "application/json"
153
- };
154
- }
155
- // ---------------------------------------------------------------------------
156
- // Repository operations
157
- // ---------------------------------------------------------------------------
158
- async repoExists(name) {
159
- const { baseUrl, username } = this.config;
160
- const res = await fetch(
161
- `${baseUrl}/api/v1/repos/${username}/${name}`,
162
- { headers: await this.authHeaders() }
163
- );
164
- return res.status === 200;
165
- }
166
- /** Returns true if the repo exists but has no commits (empty: true from Gitea API). */
167
- async repoIsEmpty(name) {
168
- const { baseUrl, username } = this.config;
169
- const res = await fetch(
170
- `${baseUrl}/api/v1/repos/${username}/${name}`,
171
- { headers: await this.authHeaders() }
172
- );
173
- if (res.status !== 200) return false;
174
- const data = await res.json();
175
- return data.empty === true;
176
- }
177
- /** Creates a private repo and returns the clone URL. */
178
- async createRepo(name, description = "") {
179
- const { baseUrl } = this.config;
180
- const res = await fetch(`${baseUrl}/api/v1/user/repos`, {
181
- method: "POST",
182
- headers: await this.authHeaders(),
183
- body: JSON.stringify({ name, description, private: false, auto_init: false })
184
- });
185
- if (!res.ok) {
186
- const body = await res.text();
187
- if (res.status === 409) {
188
- const existing = await fetch(`${baseUrl}/api/v1/repos/${this.config.username}/${name}`, {
189
- headers: await this.authHeaders()
190
- });
191
- if (existing.ok) {
192
- const data2 = await existing.json();
193
- return data2.clone_url;
194
- }
195
- }
196
- if (res.status === 500 && body.includes("files already exist")) {
197
- await this.deleteRepo(name).catch(() => {
198
- });
199
- const retry = await fetch(`${baseUrl}/api/v1/user/repos`, {
200
- method: "POST",
201
- headers: await this.authHeaders(),
202
- body: JSON.stringify({ name, description, private: false, auto_init: false })
203
- });
204
- if (!retry.ok) {
205
- throw new Error(`Gitea createRepo retry failed: ${retry.status} ${await retry.text()}`);
206
- }
207
- const retryData = await retry.json();
208
- return retryData.clone_url;
209
- }
210
- throw new Error(`Gitea createRepo failed: ${res.status} ${body}`);
211
- }
212
- const data = await res.json();
213
- return data.clone_url;
214
- }
215
- /** Patch a repo from private to public visibility. No-op if already public. */
216
- async makeRepoPublic(name) {
217
- const { baseUrl, username } = this.config;
218
- const res = await fetch(`${baseUrl}/api/v1/repos/${username}/${name}`, {
219
- method: "PATCH",
220
- headers: await this.authHeaders(),
221
- body: JSON.stringify({ private: false })
222
- });
223
- if (!res.ok) throw new Error(`Gitea makeRepoPublic failed: ${res.status} ${await res.text()}`);
224
- }
225
- async deleteRepo(name) {
226
- const { baseUrl, username } = this.config;
227
- await fetch(`${baseUrl}/api/v1/repos/${username}/${name}`, {
228
- method: "DELETE",
229
- headers: await this.authHeaders()
230
- });
231
- }
232
- /** Returns all repos accessible to the authenticated user. */
233
- async listRepos() {
234
- const { baseUrl } = this.config;
235
- const res = await fetch(`${baseUrl}/api/v1/user/repos`, {
236
- headers: await this.authHeaders(),
237
- signal: AbortSignal.timeout(8e3)
238
- });
239
- if (!res.ok) {
240
- throw new Error(`Gitea listRepos failed: ${res.status} ${await res.text()}`);
241
- }
242
- return await res.json();
243
- }
244
- /** Fetch a single repo's detail (includes default_branch, ssh_url). */
245
- async getRepo(name) {
246
- const { baseUrl, username } = this.config;
247
- const res = await fetch(`${baseUrl}/api/v1/repos/${username}/${name}`, {
248
- headers: await this.authHeaders()
249
- });
250
- if (!res.ok) throw new Error(`Gitea getRepo failed: ${res.status} ${await res.text()}`);
251
- return res.json();
252
- }
253
- /** Get the latest commit on a branch. Returns null for empty repos. */
254
- async getLatestCommit(repoName, branch) {
255
- const { baseUrl, username } = this.config;
256
- const res = await fetch(
257
- `${baseUrl}/api/v1/repos/${username}/${repoName}/commits?sha=${encodeURIComponent(branch)}&limit=1`,
258
- { headers: await this.authHeaders() }
259
- );
260
- if (!res.ok) return null;
261
- const commits = await res.json();
262
- if (!commits.length) return null;
263
- const c = commits[0];
264
- return {
265
- hash: c.sha,
266
- shortHash: c.sha.slice(0, 7),
267
- message: c.commit.message.split("\n")[0],
268
- date: c.commit.committer.date
269
- };
270
- }
271
- /** Register a push webhook on the repo. */
272
- async createWebhook(repoName, webhookUrl, secret) {
273
- const { baseUrl, username } = this.config;
274
- const res = await fetch(`${baseUrl}/api/v1/repos/${username}/${repoName}/hooks`, {
275
- method: "POST",
276
- headers: await this.authHeaders(),
277
- body: JSON.stringify({
278
- type: "gitea",
279
- config: { url: webhookUrl, content_type: "json", secret },
280
- events: ["push"],
281
- active: true
282
- })
283
- });
284
- if (!res.ok) throw new Error(`Gitea createWebhook failed: ${res.status} ${await res.text()}`);
285
- }
286
- /** Returns branch names for a repo. Falls back to empty array on error. */
287
- async listBranches(repoName) {
288
- const { baseUrl, username } = this.config;
289
- const res = await fetch(
290
- `${baseUrl}/api/v1/repos/${username}/${repoName}/branches?limit=50`,
291
- { headers: await this.authHeaders(), signal: AbortSignal.timeout(8e3) }
292
- );
293
- if (!res.ok) return [];
294
- const data = await res.json();
295
- return data.map((b) => b.name);
296
- }
297
- /** URL suitable for git remote add — includes credentials in URL (stored in .git/config which is chmod 600). */
298
- authedCloneUrl(cloneUrl) {
299
- const { username, password } = this.config;
300
- const encUser = encodeURIComponent(username);
301
- const encPass = encodeURIComponent(password);
302
- return cloneUrl.replace("http://", `http://${encUser}:${encPass}@`);
303
- }
304
- };
305
-
306
- // src/services/app-manager.ts
307
- var BREWNET_DIR = join(homedir(), ".brewnet");
308
- var GITEA_TOKEN_PATH = join(BREWNET_DIR, "gitea-token");
309
- var DEPLOY_HISTORY_PATH = join(BREWNET_DIR, "deploy-history.json");
310
- var jobs = /* @__PURE__ */ new Map();
311
- function resolveAppsJsonPath() {
312
- return join(BREWNET_DIR, "apps.json");
313
- }
314
- function readDotEnvValue(envPath, key) {
315
- if (!existsSync2(envPath)) return "";
316
- const lines = readFileSync2(envPath, "utf-8").split("\n");
317
- for (const line of lines) {
318
- const trimmed = line.trim();
319
- if (trimmed.startsWith(`${key}=`)) {
320
- return trimmed.slice(key.length + 1).trim();
321
- }
322
- }
323
- return "";
324
- }
325
- var _boilerplateRegistered = false;
326
- async function listApps() {
327
- const appsJson = resolveAppsJsonPath();
328
- const apps = readApps(appsJson);
329
- if (_boilerplateRegistered) return apps;
330
- _boilerplateRegistered = true;
331
- try {
332
- const ctx = resolveContext();
333
- const bpPath = join(ctx.projectPath, ".brewnet-boilerplate.json");
334
- if (existsSync2(bpPath)) {
335
- const raw = JSON.parse(readFileSync2(bpPath, "utf-8"));
336
- const bpMetas = Array.isArray(raw) ? raw : [raw];
337
- let changed = false;
338
- for (const bp of bpMetas) {
339
- if (!bp.stackId || !bp.appDir) continue;
340
- const exists = apps.some((a) => a.appDir === bp.appDir || a.stackId === bp.stackId);
341
- if (!exists) {
342
- const port = bp.backendUrl ? parseInt(new URL(bp.backendUrl).port || "8080", 10) : 8080;
343
- const entry = {
344
- name: bp.stackId,
345
- mode: "boilerplate",
346
- stackId: bp.stackId,
347
- appDir: bp.appDir,
348
- lang: bp.lang,
349
- framework: bp.frameworkId,
350
- port,
351
- status: bp.status || "running",
352
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
353
- };
354
- apps.push(entry);
355
- changed = true;
356
- }
357
- }
358
- if (changed) {
359
- writeFileSync2(appsJson, JSON.stringify(apps, null, 2), "utf-8");
360
- }
361
- }
362
- } catch {
363
- }
364
- return apps;
365
- }
366
- function getDeployHistory(appName) {
367
- const entries = readDeployHistory(DEPLOY_HISTORY_PATH);
368
- if (!appName) return entries;
369
- return entries.filter((e) => e.appName === appName);
370
- }
371
- async function listGiteaRepos() {
372
- const ctx = resolveContext();
373
- const gitea = new GiteaClient({
374
- baseUrl: ctx.giteaBaseUrl,
375
- username: ctx.giteaUser,
376
- password: ctx.giteaPassword,
377
- tokenPath: GITEA_TOKEN_PATH
378
- });
379
- await gitea.prepare();
380
- return gitea.listRepos();
381
- }
382
- function getDeploySettings(appName) {
383
- const apps = readApps(resolveAppsJsonPath());
384
- const app = apps.find((a) => a.name === appName);
385
- const settings = app?.deploySettings;
386
- return settings ?? { autoDeploy: false, deployBranch: "main" };
387
- }
388
- function updateDeploySettings(appName, settings) {
389
- const appsJson = resolveAppsJsonPath();
390
- const apps = readApps(appsJson);
391
- const app = apps.find((a) => a.name === appName);
392
- if (!app) throw new Error(`App "${appName}" not found`);
393
- const existing = app.deploySettings ?? { autoDeploy: false, deployBranch: "main" };
394
- app.deploySettings = { ...existing, ...settings };
395
- updateApp(appsJson, appName, app);
396
- }
397
- async function getAppGitInfo(appName) {
398
- const ctx = resolveContext();
399
- const apps = readApps(resolveAppsJsonPath());
400
- const app = apps.find((a) => a.name === appName);
401
- if (!app) throw new Error(`App "${appName}" not found`);
402
- const gitea = new GiteaClient({
403
- baseUrl: ctx.giteaBaseUrl,
404
- username: ctx.giteaUser,
405
- password: ctx.giteaPassword,
406
- tokenPath: GITEA_TOKEN_PATH
407
- });
408
- let branch = "main";
409
- let latestCommit = null;
410
- let cloneUrlSsh = `ssh://git@localhost:2222/${ctx.giteaUser}/${appName}.git`;
411
- try {
412
- const repo = await gitea.getRepo(appName);
413
- branch = repo.default_branch || "main";
414
- cloneUrlSsh = repo.ssh_url || cloneUrlSsh;
415
- if (repo.private) {
416
- await gitea.makeRepoPublic(appName).catch((e) => {
417
- console.warn(`[app-manager] makeRepoPublic failed for ${appName}: ${e instanceof Error ? e.message : String(e)}`);
418
- });
419
- }
420
- latestCommit = await gitea.getLatestCommit(appName, branch);
421
- } catch (e) {
422
- console.warn(`[app-manager] getAppGitInfo Gitea call failed (${appName}): ${e instanceof Error ? e.message : String(e)}`);
423
- }
424
- return {
425
- giteaUrl: `${ctx.giteaBaseUrl}/${ctx.giteaUser}/${appName}`,
426
- cloneUrlHttp: `${ctx.giteaBaseUrl}/${ctx.giteaUser}/${appName}.git`,
427
- cloneUrlSsh,
428
- localPath: app.appDir,
429
- branch,
430
- latestCommit
431
- };
432
- }
433
- async function rollbackApp(appName, commitHash) {
434
- const job = newJob(appName, ["Checkout", "Build & Start", "Health check"]);
435
- jobs.set(job.jobId, job);
436
- setImmediate(() => void _runRollback(job, appName, commitHash));
437
- return job.jobId;
438
- }
439
- async function _runRollback(job, appName, commitHash) {
440
- try {
441
- const apps = readApps(resolveAppsJsonPath());
442
- const app = apps.find((a) => a.name === appName);
443
- if (!app) throw new Error(`App "${appName}" not found`);
444
- const target = commitHash || "HEAD~1";
445
- setStep(job, 0, "running", `git checkout ${target.slice(0, 7)}`);
446
- await execa("git", ["checkout", target], { cwd: app.appDir });
447
- setStep(job, 0, "done");
448
- await _injectQuickTunnelIfNeeded(app.appDir, appName, app.port);
449
- setStep(job, 1, "running", "docker compose up --build");
450
- await _dockerComposeUp(app.appDir, job);
451
- setStep(job, 1, "done", "containers started");
452
- setStep(job, 2, "running");
453
- const healthUrl = _buildHealthUrl(app.appDir, app.port);
454
- setStep(job, 2, "running", `polling ${healthUrl}`);
455
- await _pollHealth(healthUrl, 12e4, job);
456
- setStep(job, 2, "done");
457
- updateApp(resolveAppsJsonPath(), appName, { status: "running" });
458
- appendDeployHistory(DEPLOY_HISTORY_PATH, {
459
- appName,
460
- commitHash,
461
- commitMessage: `Rollback to ${commitHash.slice(0, 7)}`,
462
- status: "success",
463
- deployedAt: (/* @__PURE__ */ new Date()).toISOString()
464
- });
465
- job.status = "done";
466
- } catch (err) {
467
- job.status = "failed";
468
- job.error = err instanceof Error ? err.message : String(err);
469
- for (const step of job.steps) {
470
- if (step.status === "running" || step.status === "pending") step.status = "failed";
471
- }
472
- appendDeployHistory(DEPLOY_HISTORY_PATH, {
473
- appName,
474
- commitHash,
475
- commitMessage: `Rollback to ${commitHash.slice(0, 7)}`,
476
- status: "failed",
477
- deployedAt: (/* @__PURE__ */ new Date()).toISOString()
478
- });
479
- }
480
- }
481
- async function getAppBranches(appName) {
482
- const ctx = resolveContext();
483
- const gitea = new GiteaClient({
484
- baseUrl: ctx.giteaBaseUrl,
485
- username: ctx.giteaUser,
486
- password: ctx.giteaPassword,
487
- tokenPath: GITEA_TOKEN_PATH
488
- });
489
- return gitea.listBranches(appName);
490
- }
491
- async function setupWebhook(appName, webhookUrl) {
492
- const ctx = resolveContext();
493
- const settings = getDeploySettings(appName);
494
- const secret = settings.webhookSecret ?? randomBytes(16).toString("hex");
495
- const gitea = new GiteaClient({
496
- baseUrl: ctx.giteaBaseUrl,
497
- username: ctx.giteaUser,
498
- password: ctx.giteaPassword,
499
- tokenPath: GITEA_TOKEN_PATH
500
- });
501
- await gitea.createWebhook(appName, webhookUrl, secret);
502
- updateDeploySettings(appName, { webhookSecret: secret });
503
- }
504
- async function deployApp(appName) {
505
- const job = newJob(appName, ["Pull", "Build & Start", "Health check"]);
506
- jobs.set(job.jobId, job);
507
- setImmediate(() => void _runDeploy(job, appName));
508
- return job.jobId;
509
- }
510
- async function _runDeploy(job, appName) {
511
- const apps = readApps(resolveAppsJsonPath());
512
- const app = apps.find((a) => a.name === appName);
513
- if (!app) {
514
- job.status = "failed";
515
- job.error = `App "${appName}" not found`;
516
- return;
517
- }
518
- try {
519
- const settings = getDeploySettings(appName);
520
- setStep(job, 0, "running");
521
- try {
522
- const ctx = resolveContext();
523
- const gitea = new GiteaClient({
524
- baseUrl: ctx.giteaBaseUrl,
525
- username: ctx.giteaUser,
526
- password: ctx.giteaPassword,
527
- tokenPath: GITEA_TOKEN_PATH
528
- });
529
- await gitea.prepare();
530
- const repoExists = await gitea.repoExists(appName);
531
- if (!repoExists) {
532
- appendLog(job, "[pull] Gitea repo not found \u2014 recreating and pushing local code");
533
- const cloneUrl = await gitea.createRepo(appName, `Brewnet app: ${appName}`);
534
- const authedUrl = gitea.authedCloneUrl(cloneUrl);
535
- await execa("git", ["remote", "add", "brewnet", authedUrl], { cwd: app.appDir }).catch(
536
- () => execa("git", ["remote", "set-url", "brewnet", authedUrl], { cwd: app.appDir })
537
- );
538
- await execa("git", ["push", "brewnet", "HEAD:main", "--force"], { cwd: app.appDir });
539
- appendLog(job, "[pull] Gitea repo recreated and code pushed \u2713");
540
- } else if (!existsSync2(app.appDir)) {
541
- appendLog(job, "[pull] appDir missing \u2014 cloning from Gitea");
542
- const authedUrl = gitea.authedCloneUrl(`${ctx.giteaBaseUrl}/${ctx.giteaUser}/${appName}.git`);
543
- await execa("git", ["clone", authedUrl, app.appDir]);
544
- appendLog(job, "[pull] re-cloned from Gitea \u2713");
545
- } else if (await gitea.repoIsEmpty(appName)) {
546
- appendLog(job, "[pull] Gitea repo is empty \u2014 pushing local code");
547
- const isShallow = await execa("git", ["rev-parse", "--is-shallow-repository"], { cwd: app.appDir }).then((r) => r.stdout.trim() === "true").catch(() => false);
548
- if (isShallow) {
549
- appendLog(job, "[pull] shallow clone detected \u2014 unshallowing");
550
- await execa("git", ["fetch", "--unshallow", "origin"], { cwd: app.appDir }).catch(async () => {
551
- const { reinitGit } = await import("./boilerplate-manager-P6QYUU7Q.js");
552
- await reinitGit(app.appDir);
553
- });
554
- }
555
- const authedUrl = gitea.authedCloneUrl(`${ctx.giteaBaseUrl}/${ctx.giteaUser}/${appName}.git`);
556
- await execa("git", ["remote", "add", "brewnet", authedUrl], { cwd: app.appDir }).catch(
557
- () => execa("git", ["remote", "set-url", "brewnet", authedUrl], { cwd: app.appDir })
558
- );
559
- await execa("git", ["push", "brewnet", "HEAD:main", "--force"], { cwd: app.appDir });
560
- appendLog(job, "[pull] code pushed to Gitea \u2713");
561
- } else {
562
- await execa("git", ["pull", "brewnet", settings.deployBranch], { cwd: app.appDir }).catch((e) => {
563
- appendLog(job, `[pull] git pull failed (non-critical): ${e instanceof Error ? e.message : String(e)}`);
564
- });
565
- }
566
- } catch (e) {
567
- appendLog(job, `[pull] Gitea sync failed (non-critical): ${e instanceof Error ? e.message : String(e)}`);
568
- }
569
- setStep(job, 0, "done");
570
- const hasCompose = existsSync2(join(app.appDir, "docker-compose.yml")) || existsSync2(join(app.appDir, "compose.yml"));
571
- if (!hasCompose) {
572
- const projectType = _detectProjectType(app.appDir);
573
- if (projectType) {
574
- appendLog(job, `[scaffold] Detected ${projectType} project \u2014 generating Docker config`);
575
- _scaffoldDockerConfig(app.appDir, appName, app.port, job, projectType);
576
- } else {
577
- throw new Error(
578
- "This project has no docker-compose.yml or Dockerfile. Add a Dockerfile and docker-compose.yml to deploy, or use a Brewnet boilerplate."
579
- );
580
- }
581
- }
582
- await _injectQuickTunnelIfNeeded(app.appDir, appName, app.port);
583
- setStep(job, 1, "running", "docker compose up --build");
584
- await _dockerComposeUp(app.appDir, job);
585
- setStep(job, 1, "done", "containers started");
586
- setStep(job, 2, "running");
587
- const healthUrlDeploy = _buildHealthUrl(app.appDir, app.port);
588
- setStep(job, 2, "running", `polling ${healthUrlDeploy}`);
589
- await _pollHealth(healthUrlDeploy, 12e4, job);
590
- setStep(job, 2, "done");
591
- updateApp(resolveAppsJsonPath(), appName, { status: "running" });
592
- const headHash = await execa("git", ["rev-parse", "HEAD"], { cwd: app.appDir }).then((r) => r.stdout.trim()).catch(() => "");
593
- const headMsg = await execa("git", ["log", "-1", "--format=%s"], { cwd: app.appDir }).then((r) => r.stdout.trim()).catch(() => "Manual deploy");
594
- appendDeployHistory(DEPLOY_HISTORY_PATH, {
595
- appName,
596
- commitHash: headHash,
597
- commitMessage: headMsg,
598
- status: "success",
599
- deployedAt: (/* @__PURE__ */ new Date()).toISOString()
600
- });
601
- job.status = "done";
602
- } catch (err) {
603
- job.status = "failed";
604
- job.error = err instanceof Error ? err.message : String(err);
605
- for (const step of job.steps) {
606
- if (step.status === "running" || step.status === "pending") step.status = "failed";
607
- }
608
- const headHashFail = await execa("git", ["rev-parse", "HEAD"], { cwd: app.appDir }).then((r) => r.stdout.trim()).catch(() => "");
609
- appendDeployHistory(DEPLOY_HISTORY_PATH, {
610
- appName,
611
- commitHash: headHashFail,
612
- commitMessage: "Manual deploy",
613
- status: "failed",
614
- deployedAt: (/* @__PURE__ */ new Date()).toISOString()
615
- });
616
- }
617
- }
618
- function getAppDir(appName) {
619
- const apps = readApps(resolveAppsJsonPath());
620
- return apps.find((a) => a.name === appName)?.appDir;
621
- }
622
- function getJobStatus(jobId) {
623
- return jobs.get(jobId);
624
- }
625
- function newJob(appName, stepLabels) {
626
- return {
627
- jobId: randomBytes(8).toString("hex"),
628
- appName,
629
- status: "running",
630
- steps: stepLabels.map((label) => ({ label, status: "pending" }))
631
- };
632
- }
633
- function setStep(job, index, status, message) {
634
- const step = job.steps[index];
635
- if (step) {
636
- step.status = status;
637
- if (message) step.message = message;
638
- }
639
- }
640
- function appendLog(job, line) {
641
- if (!job.logs) job.logs = [];
642
- job.logs.push(line);
643
- if (job.logs.length > 200) job.logs.splice(0, job.logs.length - 200);
644
- }
645
- function readBoilerplateMeta(projectPath) {
646
- const p = join(projectPath, ".brewnet-boilerplate.json");
647
- if (!existsSync2(p)) return [];
648
- try {
649
- const raw = JSON.parse(readFileSync2(p, "utf-8"));
650
- return Array.isArray(raw) ? raw : [raw];
651
- } catch {
652
- return [];
653
- }
654
- }
655
- function resolveContext() {
656
- const last = getLastProject();
657
- const state = loadState(last ?? "");
658
- const raw = state?.projectPath ?? process.cwd();
659
- const projectPath = raw.startsWith("~") ? join(homedir(), raw.slice(1)) : raw;
660
- const envPath = join(projectPath, ".env");
661
- const giteaUser = readDotEnvValue(envPath, "GITEA_ADMIN_USER") || state?.admin?.username || "admin";
662
- const secretsPath = join(projectPath, "secrets", "admin_password");
663
- const secretsPassword = existsSync2(secretsPath) ? readFileSync2(secretsPath, "utf-8").trim() : "";
664
- const giteaPassword = secretsPassword || readDotEnvValue(envPath, "GITEA_ADMIN_PASSWORD") || state?.admin?.password || "";
665
- const tunnelMode = state?.domain?.cloudflare?.tunnelMode ?? "";
666
- const gitPort = state?.servers?.gitServer?.port ?? 3e3;
667
- const giteaBaseUrl = tunnelMode === "quick" ? "http://localhost/git" : `http://localhost:${gitPort}`;
668
- return { projectPath, giteaBaseUrl, giteaUser, giteaPassword };
669
- }
670
- async function _injectQuickTunnelIfNeeded(appDir, appName, port) {
671
- try {
672
- const last = getLastProject();
673
- const state = loadState(last ?? "");
674
- if (state?.domain?.cloudflare?.tunnelMode !== "quick") return;
675
- const { injectTraefikForQuickTunnel } = await import("./boilerplate-manager-P6QYUU7Q.js");
676
- injectTraefikForQuickTunnel(appDir, appName, port);
677
- } catch (err) {
678
- console.error(`[Quick Tunnel] Failed to inject Traefik labels for ${appName}: ${err instanceof Error ? err.message : String(err)}`);
679
- }
680
- }
681
- function _detectProjectType(dir) {
682
- try {
683
- if (existsSync2(join(dir, "next.config.ts")) || existsSync2(join(dir, "next.config.mjs")) || existsSync2(join(dir, "next.config.js"))) return "nextjs";
684
- if (existsSync2(join(dir, "package.json"))) return "nodejs";
685
- if (existsSync2(join(dir, "requirements.txt")) || existsSync2(join(dir, "pyproject.toml"))) return "python";
686
- if (existsSync2(join(dir, "go.mod"))) return "go";
687
- if (existsSync2(join(dir, "Cargo.toml"))) return "rust";
688
- if (existsSync2(join(dir, "pom.xml")) || existsSync2(join(dir, "build.gradle")) || existsSync2(join(dir, "build.gradle.kts"))) return "java";
689
- if (existsSync2(join(dir, "index.html"))) return "static";
690
- if (readdirSync(dir).some((f) => f.endsWith(".html"))) return "static";
691
- } catch {
692
- }
693
- return null;
694
- }
695
- function _scaffoldDockerConfig(dir, _appName, port, job, detectedType) {
696
- const type = detectedType || _detectProjectType(dir);
697
- if (!type) throw new Error(`Cannot auto-detect project type in ${dir}. Add a Dockerfile and docker-compose.yml manually.`);
698
- if (job && !detectedType) appendLog(job, `[scaffold] Detected ${type} project \u2014 generating Docker config`);
699
- let dockerfile = "";
700
- switch (type) {
701
- case "nextjs":
702
- dockerfile = [
703
- "FROM node:22-alpine",
704
- "WORKDIR /app",
705
- "COPY package.json package-lock.json* pnpm-lock.yaml* yarn.lock* ./",
706
- "RUN npm install --legacy-peer-deps 2>/dev/null || yarn install 2>/dev/null || true",
707
- "COPY . .",
708
- "RUN npm run build",
709
- "ENV PORT=3000 HOSTNAME=0.0.0.0",
710
- "EXPOSE 3000",
711
- 'CMD ["npm", "start"]'
712
- ].join("\n");
713
- break;
714
- case "nodejs":
715
- dockerfile = [
716
- "FROM node:22-alpine",
717
- "WORKDIR /app",
718
- "COPY package.json package-lock.json* pnpm-lock.yaml* yarn.lock* ./",
719
- "RUN npm install --legacy-peer-deps || true",
720
- "COPY . .",
721
- "RUN npm run build 2>/dev/null || true",
722
- "EXPOSE " + port,
723
- 'CMD ["npm", "start"]'
724
- ].join("\n");
725
- break;
726
- case "python":
727
- dockerfile = [
728
- "FROM python:3.13-slim",
729
- "WORKDIR /app",
730
- "COPY requirements.txt* pyproject.toml* ./",
731
- "RUN pip install --no-cache-dir -r requirements.txt 2>/dev/null || pip install --no-cache-dir . 2>/dev/null || true",
732
- "COPY . .",
733
- "EXPOSE " + port,
734
- 'CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "' + port + '"]'
735
- ].join("\n");
736
- break;
737
- case "go":
738
- dockerfile = [
739
- "FROM golang:1.22-alpine AS builder",
740
- "WORKDIR /app",
741
- "COPY go.mod go.sum* ./",
742
- "RUN go mod download",
743
- "COPY . .",
744
- "RUN CGO_ENABLED=0 go build -o server .",
745
- "",
746
- "FROM alpine",
747
- "WORKDIR /app",
748
- "COPY --from=builder /app/server .",
749
- "EXPOSE " + port,
750
- 'CMD ["./server"]'
751
- ].join("\n");
752
- break;
753
- case "rust":
754
- dockerfile = [
755
- "FROM rust:1.88 AS builder",
756
- "WORKDIR /app",
757
- "COPY . .",
758
- "RUN cargo build --release",
759
- "",
760
- "FROM debian:bookworm-slim",
761
- "WORKDIR /app",
762
- "COPY --from=builder /app/target/release/* /app/ 2>/dev/null || true",
763
- "EXPOSE " + port,
764
- 'CMD ["./app"]'
765
- ].join("\n");
766
- break;
767
- case "java":
768
- dockerfile = [
769
- "FROM gradle:8.12-jdk21 AS builder",
770
- "WORKDIR /app",
771
- "COPY . .",
772
- "RUN gradle build -x test 2>/dev/null || ./gradlew build -x test 2>/dev/null || mvn package -DskipTests 2>/dev/null || true",
773
- "",
774
- "FROM eclipse-temurin:21-jre-alpine",
775
- "WORKDIR /app",
776
- "COPY --from=builder /app/build/libs/*.jar app.jar 2>/dev/null || true",
777
- "COPY --from=builder /app/target/*.jar app.jar 2>/dev/null || true",
778
- "EXPOSE " + port,
779
- 'CMD ["java", "-jar", "app.jar"]'
780
- ].join("\n");
781
- break;
782
- case "static":
783
- dockerfile = [
784
- "FROM nginx:1.27-alpine",
785
- "COPY . /usr/share/nginx/html/",
786
- "EXPOSE 80"
787
- ].join("\n");
788
- break;
789
- }
790
- const internalPort = type === "nextjs" ? 3e3 : type === "static" ? 80 : port;
791
- const compose = [
792
- "services:",
793
- " backend:",
794
- " build: .",
795
- " ports:",
796
- ` - "${port}:${internalPort}"`,
797
- " restart: unless-stopped",
798
- " healthcheck:",
799
- ` test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:${internalPort}/"]`,
800
- " interval: 10s",
801
- " timeout: 5s",
802
- " retries: 5"
803
- ].join("\n");
804
- if (!existsSync2(join(dir, "Dockerfile"))) {
805
- writeFileSync2(join(dir, "Dockerfile"), dockerfile, "utf-8");
806
- if (job) appendLog(job, "[scaffold] Generated Dockerfile");
807
- }
808
- writeFileSync2(join(dir, "docker-compose.yml"), compose, "utf-8");
809
- if (job) appendLog(job, "[scaffold] Generated docker-compose.yml");
810
- if (!existsSync2(join(dir, ".dockerignore"))) {
811
- writeFileSync2(join(dir, ".dockerignore"), "node_modules\n.next\n.git\n*.md\n", "utf-8");
812
- }
813
- }
814
- function ensureComposeFile(dir, appName, port, job) {
815
- if (existsSync2(join(dir, "docker-compose.yml")) || existsSync2(join(dir, "compose.yml"))) return;
816
- _scaffoldDockerConfig(dir, appName, port, job);
817
- }
818
- function _resolveBackendPort(appDir, fallbackPort) {
819
- const envPath = join(appDir, ".env");
820
- const val = readDotEnvValue(envPath, "BACKEND_PORT");
821
- const parsed = val ? parseInt(val, 10) : NaN;
822
- return isNaN(parsed) ? fallbackPort : parsed;
823
- }
824
- function detectBasePath(appDir) {
825
- for (const name of ["next.config.ts", "next.config.mjs", "next.config.js"]) {
826
- const p = join(appDir, name);
827
- if (existsSync2(p)) {
828
- const content = readFileSync2(p, "utf-8");
829
- const match = content.match(/basePath\s*:\s*['"`]([^'"`]+)['"`]/);
830
- if (match) return match[1];
831
- }
832
- }
833
- return "";
834
- }
835
- function _buildHealthUrl(appDir, fallbackPort) {
836
- const healthPort = _resolveBackendPort(appDir, fallbackPort);
837
- const basePath = detectBasePath(appDir);
838
- const isBoilerplate = existsSync2(join(appDir, ".env.example")) && readFileSync2(join(appDir, ".env.example"), "utf-8").includes("STACK_LANG");
839
- const hasHealthRoute = existsSync2(join(appDir, "src", "app", "health")) || existsSync2(join(appDir, "backend", "src"));
840
- const healthPath = isBoilerplate || hasHealthRoute ? "/health" : "/";
841
- return `http://127.0.0.1:${healthPort}${basePath}${healthPath}`;
842
- }
843
- async function _pollHealth(url, maxMs = 12e4, job) {
844
- const deadline = Date.now() + maxMs;
845
- let attempt = 0;
846
- while (Date.now() < deadline) {
847
- attempt++;
848
- try {
849
- const res = await fetch(url, { signal: AbortSignal.timeout(5e3) });
850
- if (res.ok) {
851
- if (job) appendLog(job, `[health] \u2713 ${url} \u2192 ${res.status} (attempt ${attempt})`);
852
- return;
853
- }
854
- if (job) appendLog(job, `[health] ${url} \u2192 ${res.status} (attempt ${attempt})`);
855
- } catch {
856
- if (job && attempt % 3 === 1) appendLog(job, `[health] waiting... ${url} (attempt ${attempt})`);
857
- }
858
- await new Promise((r) => setTimeout(r, 3e3));
859
- }
860
- if (job) appendLog(job, `[health] \u2717 timeout after ${maxMs / 1e3}s`);
861
- throw new Error(`Health check timed out after ${maxMs / 1e3}s: ${url}`);
862
- }
863
- async function _dockerComposeUp(cwd, job) {
864
- appendLog(job, `[docker] $ docker compose up -d --build`);
865
- appendLog(job, `[docker] cwd: ${cwd}`);
866
- const proc = execa("docker", ["compose", "up", "-d", "--build"], { cwd, reject: false });
867
- proc.stdout?.on("data", (chunk) => {
868
- for (const line of chunk.toString().split("\n").filter(Boolean)) {
869
- appendLog(job, `[docker] ${line}`);
870
- }
871
- });
872
- proc.stderr?.on("data", (chunk) => {
873
- for (const line of chunk.toString().split("\n").filter(Boolean)) {
874
- appendLog(job, `[docker] ${line}`);
875
- }
876
- });
877
- const result = await proc;
878
- if (result.exitCode !== 0) {
879
- appendLog(job, `[docker] \u2717 exit code ${result.exitCode}`);
880
- throw new Error(`Command failed with exit code ${result.exitCode}: docker compose up -d --build
881
- ${result.stderr}`);
882
- }
883
- appendLog(job, `[docker] \u2713 containers started`);
884
- }
885
- async function createApp(opts) {
886
- const job = newJob(opts.appName, ["Validating", "Gitea setup", "Gitea repo", "Git push", "Docker up", "Health check"]);
887
- jobs.set(job.jobId, job);
888
- setImmediate(() => void _runCreateApp(job, opts));
889
- return job.jobId;
890
- }
891
- async function _runCreateApp(job, opts) {
892
- try {
893
- const ctx = resolveContext();
894
- const appsJson = resolveAppsJsonPath();
895
- setStep(job, 0, "running");
896
- if (opts.mode === "boilerplate") {
897
- if (!opts.stackId) throw new Error("stackId is required for boilerplate mode");
898
- const metas = readBoilerplateMeta(ctx.projectPath);
899
- const meta = metas.find((m) => m.stackId === opts.stackId);
900
- if (meta) {
901
- opts._meta = meta;
902
- } else {
903
- appendLog(job, `[info] Stack "${opts.stackId}" not installed locally \u2014 cloning fresh from catalog`);
904
- opts._resolvedStackId = opts.stackId;
905
- }
906
- } else if (opts.mode === "git-clone") {
907
- if (!opts.gitUrl) throw new Error("gitUrl is required for Git Clone mode");
908
- } else if (opts.mode === "new-project") {
909
- const { resolveStackId } = await import("./frameworks-Z7VXDGP4.js");
910
- const stackId = resolveStackId(opts.language ?? "nodejs", opts.frameworkId ?? "express");
911
- if (!stackId) throw new Error(`Unknown stack: ${opts.language}/${opts.frameworkId}`);
912
- opts._resolvedStackId = stackId;
913
- }
914
- setStep(job, 0, "done");
915
- setStep(job, 1, "running");
916
- const gitea = new GiteaClient({
917
- baseUrl: ctx.giteaBaseUrl,
918
- username: ctx.giteaUser,
919
- password: ctx.giteaPassword,
920
- tokenPath: GITEA_TOKEN_PATH
921
- });
922
- const giteaPrep = await gitea.prepare();
923
- setStep(job, 1, "done", giteaPrep.message);
924
- if (opts.mode === "boilerplate" && opts._meta) {
925
- await _createModeA(job, opts, ctx, gitea, appsJson);
926
- } else if (opts.mode === "git-clone") {
927
- await _createModeB(job, opts, ctx, gitea, appsJson);
928
- } else {
929
- await _createModeC(job, opts, ctx, gitea, appsJson);
930
- }
931
- job.status = "done";
932
- } catch (err) {
933
- job.status = "failed";
934
- job.error = err instanceof Error ? err.message : String(err);
935
- for (const step of job.steps) {
936
- if (step.status === "running" || step.status === "pending") step.status = "failed";
937
- }
938
- }
939
- }
940
- async function _createModeA(job, opts, ctx, gitea, appsJson) {
941
- const meta = opts._meta;
942
- const port = opts.port ?? meta.port ?? parseInt(meta.backendUrl.split(":").pop() ?? "8080", 10);
943
- setStep(job, 2, "running", `checking ${ctx.giteaUser}/${opts.appName}`);
944
- const alreadyExists = await gitea.repoExists(opts.appName);
945
- let cloneUrl;
946
- if (!alreadyExists) {
947
- setStep(job, 2, "running", `creating ${ctx.giteaUser}/${opts.appName}`);
948
- cloneUrl = await gitea.createRepo(opts.appName, `Brewnet app: ${opts.appName}`);
949
- } else {
950
- cloneUrl = `${ctx.giteaBaseUrl}/${ctx.giteaUser}/${opts.appName}.git`;
951
- }
952
- setStep(job, 2, "done");
953
- setStep(job, 3, "running", `pushing HEAD:main \u2192 ${ctx.giteaUser}/${opts.appName}`);
954
- const shallowCheck = await execa("git", ["rev-parse", "--is-shallow-repository"], { cwd: meta.appDir }).catch(() => ({ stdout: "false" }));
955
- if (shallowCheck.stdout.trim() === "true") {
956
- await execa("git", ["fetch", "--unshallow", "origin"], { cwd: meta.appDir }).catch(async () => {
957
- const { reinitGit } = await import("./boilerplate-manager-P6QYUU7Q.js");
958
- await reinitGit(meta.appDir);
959
- });
960
- }
961
- const authedUrl = gitea.authedCloneUrl(cloneUrl);
962
- await execa("git", ["remote", "add", "brewnet", authedUrl], { cwd: meta.appDir }).catch(() => {
963
- return execa("git", ["remote", "set-url", "brewnet", authedUrl], { cwd: meta.appDir });
964
- });
965
- await execa("git", ["push", "brewnet", "HEAD:main", "--force"], { cwd: meta.appDir });
966
- setStep(job, 3, "done");
967
- setStep(job, 4, "running", "docker compose up --build");
968
- ensureComposeFile(meta.appDir, opts.appName, port, job);
969
- await _injectQuickTunnelIfNeeded(meta.appDir, opts.appName, port);
970
- await _dockerComposeUp(meta.appDir, job);
971
- setStep(job, 4, "done", "containers started");
972
- setStep(job, 5, "running");
973
- const healthUrlA = _buildHealthUrl(meta.appDir, port);
974
- setStep(job, 5, "running", `polling ${healthUrlA}`);
975
- await _pollHealth(healthUrlA, 12e4, job);
976
- setStep(job, 5, "done");
977
- addApp(appsJson, {
978
- name: opts.appName,
979
- mode: "boilerplate",
980
- stackId: opts.stackId,
981
- appDir: meta.appDir,
982
- lang: meta.lang,
983
- framework: meta.frameworkId,
984
- port,
985
- giteaRepoUrl: `${ctx.giteaBaseUrl}/${ctx.giteaUser}/${opts.appName}`,
986
- status: "running",
987
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
988
- });
989
- await setupWebhook(opts.appName, "http://localhost:8088/api/deploy/hook").catch((e) => {
990
- console.warn("[webhook] registration failed (non-critical):", e instanceof Error ? e.message : String(e));
991
- });
992
- }
993
- async function _createModeB(job, opts, ctx, gitea, appsJson) {
994
- const port = opts.port ?? 8080;
995
- const appDir = join(ctx.projectPath, "apps", opts.appName);
996
- setStep(job, 2, "running", "Cloning external repository...");
997
- const { reinitGit: reinitGitB } = await import("./boilerplate-manager-P6QYUU7Q.js");
998
- const { rmSync } = await import("fs");
999
- if (existsSync2(appDir)) {
1000
- rmSync(appDir, { recursive: true, force: true });
1001
- }
1002
- const cloneArgs = ["clone", "--depth", "1"];
1003
- if (opts.branch) cloneArgs.push("-b", opts.branch);
1004
- cloneArgs.push(opts.gitUrl, appDir);
1005
- await execa("git", cloneArgs);
1006
- const envExPath = join(appDir, ".env.example");
1007
- const envPath = join(appDir, ".env");
1008
- if (existsSync2(envExPath)) {
1009
- const { findFreePort: findFreePortB } = await import("./boilerplate-manager-P6QYUU7Q.js");
1010
- let envContent = readFileSync2(envExPath, "utf-8");
1011
- envContent = envContent.replace(/^BACKEND_PORT=.*/m, `BACKEND_PORT=${port}`);
1012
- const fePort = await findFreePortB(port + 1);
1013
- envContent = envContent.replace(/^FRONTEND_PORT=.*/m, `FRONTEND_PORT=${fePort}`);
1014
- writeFileSync2(envPath, envContent, "utf-8");
1015
- } else if (existsSync2(join(appDir, ".env"))) {
1016
- let envContent = readFileSync2(join(appDir, ".env"), "utf-8");
1017
- envContent = envContent.replace(/^BACKEND_PORT=.*/m, `BACKEND_PORT=${port}`);
1018
- writeFileSync2(join(appDir, ".env"), envContent, "utf-8");
1019
- }
1020
- await reinitGitB(appDir);
1021
- const alreadyExists = await gitea.repoExists(opts.appName);
1022
- const cloneUrl = alreadyExists ? `${ctx.giteaBaseUrl}/${ctx.giteaUser}/${opts.appName}.git` : await gitea.createRepo(opts.appName, `Brewnet app: ${opts.appName}`);
1023
- setStep(job, 2, "done");
1024
- setStep(job, 3, "running");
1025
- const authedUrl = gitea.authedCloneUrl(cloneUrl);
1026
- await execa("git", ["remote", "add", "brewnet", authedUrl], { cwd: appDir });
1027
- await execa("git", ["push", "brewnet", "HEAD:main", "--force"], { cwd: appDir });
1028
- setStep(job, 3, "done");
1029
- const hasCompose = existsSync2(join(appDir, "docker-compose.yml")) || existsSync2(join(appDir, "compose.yml"));
1030
- if (hasCompose) {
1031
- setStep(job, 4, "running", "docker compose up --build");
1032
- await _injectQuickTunnelIfNeeded(appDir, opts.appName, port);
1033
- await _dockerComposeUp(appDir, job);
1034
- setStep(job, 4, "done", "containers started");
1035
- setStep(job, 5, "running");
1036
- const healthUrlB = _buildHealthUrl(appDir, port);
1037
- setStep(job, 5, "running", `polling ${healthUrlB}`);
1038
- await _pollHealth(healthUrlB, 12e4, job);
1039
- setStep(job, 5, "done");
1040
- } else {
1041
- setStep(job, 4, "done", "skipped \u2014 no docker-compose.yml");
1042
- setStep(job, 5, "done", "skipped \u2014 deploy separately");
1043
- appendLog(job, "[clone] Gitea push completed \u2014 no docker-compose.yml, skipping Docker up");
1044
- }
1045
- addApp(appsJson, {
1046
- name: opts.appName,
1047
- mode: "git-clone",
1048
- sourceUrl: opts.gitUrl,
1049
- appDir,
1050
- port,
1051
- giteaRepoUrl: `${ctx.giteaBaseUrl}/${ctx.giteaUser}/${opts.appName}`,
1052
- status: hasCompose ? "running" : "stopped",
1053
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
1054
- });
1055
- }
1056
- async function _createModeC(job, opts, ctx, gitea, appsJson) {
1057
- const { cloneStack, generateEnv, reinitGit, findFreePort } = await import("./boilerplate-manager-P6QYUU7Q.js");
1058
- const { getStackById: getStackById2 } = await import("./stacks-M4FBTVO5.js");
1059
- const stackId = opts._resolvedStackId;
1060
- const requestedPort = opts.port ?? 8080;
1061
- const appDir = join(ctx.projectPath, "apps", opts.appName);
1062
- await cloneStack(stackId, appDir);
1063
- const port = await findFreePort(requestedPort);
1064
- const stackInfo = getStackById2(stackId);
1065
- const frontendPort = stackInfo && !stackInfo.isUnified ? await findFreePort(port + 1) : void 0;
1066
- generateEnv(appDir, stackId, "sqlite3", { hostPort: port, frontendPort });
1067
- await reinitGit(appDir);
1068
- setStep(job, 2, "running");
1069
- const cloneUrl = await gitea.createRepo(opts.appName, `Brewnet app: ${opts.appName}`);
1070
- setStep(job, 2, "done");
1071
- setStep(job, 3, "running");
1072
- const authedUrl = gitea.authedCloneUrl(cloneUrl);
1073
- await execa("git", ["remote", "add", "brewnet", authedUrl], { cwd: appDir });
1074
- await execa("git", ["push", "brewnet", "HEAD:main", "--force"], { cwd: appDir });
1075
- setStep(job, 3, "done");
1076
- setStep(job, 4, "running", "docker compose up --build");
1077
- ensureComposeFile(appDir, opts.appName, port, job);
1078
- await _injectQuickTunnelIfNeeded(appDir, opts.appName, port);
1079
- await _dockerComposeUp(appDir, job);
1080
- setStep(job, 4, "done", "containers started");
1081
- setStep(job, 5, "running");
1082
- const healthUrlC = _buildHealthUrl(appDir, port);
1083
- setStep(job, 5, "running", `polling ${healthUrlC}`);
1084
- await _pollHealth(healthUrlC, 12e4, job);
1085
- setStep(job, 5, "done");
1086
- addApp(appsJson, {
1087
- name: opts.appName,
1088
- mode: opts.mode === "boilerplate" ? "boilerplate" : "new-project",
1089
- stackId,
1090
- appDir,
1091
- lang: opts.language ?? stackInfo?.language,
1092
- framework: opts.frameworkId ?? stackInfo?.framework,
1093
- port,
1094
- giteaRepoUrl: `${ctx.giteaBaseUrl}/${ctx.giteaUser}/${opts.appName}`,
1095
- status: "running",
1096
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
1097
- });
1098
- }
1099
- async function startApp(appName) {
1100
- const appsJson = resolveAppsJsonPath();
1101
- const apps = readApps(appsJson);
1102
- const app = apps.find((a) => a.name === appName);
1103
- if (!app) throw new Error(`App "${appName}" not found`);
1104
- await execa("docker", ["compose", "up", "-d"], { cwd: app.appDir });
1105
- updateApp(appsJson, appName, { status: "running" });
1106
- }
1107
- async function stopApp(appName) {
1108
- const appsJson = resolveAppsJsonPath();
1109
- const apps = readApps(appsJson);
1110
- const app = apps.find((a) => a.name === appName);
1111
- if (!app) throw new Error(`App "${appName}" not found`);
1112
- await execa("docker", ["compose", "down"], { cwd: app.appDir });
1113
- updateApp(appsJson, appName, { status: "stopped" });
1114
- }
1115
- async function removeApp2(appName) {
1116
- const appsJson = resolveAppsJsonPath();
1117
- const apps = readApps(appsJson);
1118
- const app = apps.find((a) => a.name === appName);
1119
- if (!app) throw new Error(`App "${appName}" not found`);
1120
- await execa("docker", ["compose", "down", "--volumes"], { cwd: app.appDir }).catch((e) => {
1121
- console.warn("[removeApp] docker compose down failed:", e instanceof Error ? e.message : String(e));
1122
- });
1123
- removeApp(appsJson, appName);
1124
- }
1125
-
1126
- // src/services/admin-server.ts
1127
- var PKG_ROOT = join2(fileURLToPath(import.meta.url), "../../../..");
1128
- var ADMIN_UI_DIST = join2(PKG_ROOT, "packages/admin-ui/dist");
52
+ import Dockerode from "dockerode";
53
+ var PKG_ROOT = join(fileURLToPath(import.meta.url), "../../../..");
54
+ var ADMIN_UI_DIST = join(PKG_ROOT, "packages/admin-ui/dist");
1129
55
  var MIME_TYPES = {
1130
56
  ".html": "text/html; charset=utf-8",
1131
57
  ".js": "application/javascript; charset=utf-8",
@@ -1156,21 +82,21 @@ function serveStaticFile(filePath, res, statusCode = 200) {
1156
82
  }
1157
83
  var ICON_SVG = (() => {
1158
84
  const candidates = [
1159
- join2(PKG_ROOT, "public/images/icon.svg"),
1160
- join2(PKG_ROOT, "../public/images/icon.svg")
85
+ join(PKG_ROOT, "public/images/icon.svg"),
86
+ join(PKG_ROOT, "../public/images/icon.svg")
1161
87
  ];
1162
88
  for (const p of candidates) {
1163
- if (existsSync3(p)) return readFileSync3(p, "utf-8");
89
+ if (existsSync(p)) return readFileSync(p, "utf-8");
1164
90
  }
1165
91
  return `<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="4 6 38 38" fill="none" stroke="#f5a623" stroke-linecap="round" stroke-linejoin="round"><path d="M8 26H32V34C32 36.8 29.8 39 27 39H13C10.2 39 8 36.8 8 34V26Z" stroke-width="3.5" fill="none"/><path d="M32 28.5C35.5 28.5 37 30.5 37 32.5C37 34.5 35.5 36.5 32 36.5" stroke-width="3.5" fill="none"/><circle cx="20" cy="30" r="2.2" fill="#f5a623" stroke="none"/><path d="M16.5 20a5 5 0 0 1 7 0" stroke-width="3.5" fill="none"/><path d="M13.5 15.5a10 10 0 0 1 13 0" stroke-width="3.5" fill="none"/><path d="M10.5 11a15 15 0 0 1 19 0" stroke-width="3.5" fill="none"/></svg>`;
1166
92
  })();
1167
93
  var FAVICON_ICO = (() => {
1168
94
  const candidates = [
1169
- join2(PKG_ROOT, "public/images/favicon.ico"),
1170
- join2(PKG_ROOT, "../public/images/favicon.ico")
95
+ join(PKG_ROOT, "public/images/favicon.ico"),
96
+ join(PKG_ROOT, "../public/images/favicon.ico")
1171
97
  ];
1172
98
  for (const p of candidates) {
1173
- if (existsSync3(p)) return readFileSync3(p);
99
+ if (existsSync(p)) return readFileSync(p);
1174
100
  }
1175
101
  return null;
1176
102
  })();
@@ -1195,7 +121,8 @@ var SERVICE_DETAIL_MAP = {
1195
121
  "Remove --api.insecure=true in production and add BasicAuth or Authelia",
1196
122
  "Set exposedbydefault=false and explicitly enable each service with traefik.enable=true",
1197
123
  "Add --certificatesresolvers.le.acme.email=YOUR_EMAIL for Let's Encrypt"
1198
- ]
124
+ ],
125
+ securityNote: "\uBCF4\uC548\uC0C1 \uC678\uBD80 \uB3C4\uBA54\uC778\uC73C\uB85C \uB178\uCD9C\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uC11C\uBC84 \uB0B4\uBD80(localhost)\uC5D0\uC11C\uB9CC \uC811\uADFC \uAC00\uB2A5\uD569\uB2C8\uB2E4."
1199
126
  },
1200
127
  "Traefik Dashboard": {
1201
128
  description: "Built-in Traefik web UI for monitoring routes, services, and middleware",
@@ -1273,6 +200,12 @@ var SERVICE_DETAIL_MAP = {
1273
200
  summary: "Configured via POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB environment variables.",
1274
201
  command: "docker exec -it brewnet-postgresql psql -U brewnet -d brewnet_db"
1275
202
  },
203
+ connectionParams: [
204
+ { label: "host", value: "localhost" },
205
+ { label: "port", value: "5432" },
206
+ { label: "user", value: "brewnet" },
207
+ { label: "db", value: "brewnet_db" }
208
+ ],
1276
209
  tips: [
1277
210
  "Internal network only (brewnet-internal) \u2014 no host port exposed",
1278
211
  "Data persisted in named volume \u2014 safe across container restarts",
@@ -1294,6 +227,12 @@ var SERVICE_DETAIL_MAP = {
1294
227
  summary: "Configured via MYSQL_ROOT_PASSWORD, MYSQL_DATABASE, MYSQL_USER, MYSQL_PASSWORD environment variables.",
1295
228
  command: "docker exec -it brewnet-mysql mysql -u brewnet -p brewnet_db"
1296
229
  },
230
+ connectionParams: [
231
+ { label: "host", value: "localhost" },
232
+ { label: "port", value: "3306" },
233
+ { label: "user", value: "brewnet" },
234
+ { label: "db", value: "brewnet_db" }
235
+ ],
1297
236
  tips: [
1298
237
  "Internal network only (brewnet-internal) \u2014 no host port exposed",
1299
238
  "Root password required at first startup",
@@ -1376,6 +315,12 @@ var SERVICE_DETAIL_MAP = {
1376
315
  summary: "Uses admin username set in Pre-Step. Password auth enabled (PASSWORD_ACCESS=true); switch to key-only after setup.",
1377
316
  command: "ssh -p 2222 USER@localhost"
1378
317
  },
318
+ connectionParams: [
319
+ { label: "host", value: "localhost" },
320
+ { label: "port", value: "2222" },
321
+ { label: "user", value: "<admin-username>" },
322
+ { label: "protocol", value: "SSH / SFTP" }
323
+ ],
1379
324
  tips: [
1380
325
  "Switch to key-only auth after initial setup: set PASSWORD_ACCESS=false",
1381
326
  "Port 2222 avoids conflict with host SSH (port 22)",
@@ -1563,7 +508,15 @@ async function readBody(req) {
1563
508
  req.on("end", () => resolve2(body));
1564
509
  });
1565
510
  }
1566
- async function handleGetServices(_req, res, _parts, _body, _projectPath, urlMap = TRAEFIK_PATH_SERVICES, quickTunnelUrl = "", allowedDirs) {
511
+ async function handleGetServices(_req, res, _parts, _body, _projectPath, urlMap = TRAEFIK_PATH_SERVICES, quickTunnelUrl = "", allowedDirs, wizardState) {
512
+ const NAMED_SUBDOMAIN_MAP = {
513
+ gitea: "git",
514
+ nextcloud: "cloud",
515
+ jellyfin: "media",
516
+ filebrowser: "files",
517
+ pgadmin: "pgadmin",
518
+ minio: "minio"
519
+ };
1567
520
  try {
1568
521
  const allContainers = await docker.listContainers({ all: true });
1569
522
  const services = [];
@@ -1597,7 +550,21 @@ async function handleGetServices(_req, res, _parts, _body, _projectPath, urlMap
1597
550
  const localBasePath = traefikPath && !hasStripPrefix ? traefikPath : "";
1598
551
  let externalUrl = null;
1599
552
  const qtUrl = quickTunnelUrl;
1600
- if (qtUrl && traefikPath) {
553
+ const tunnelMode = wizardState?.domain?.cloudflare?.tunnelMode ?? "none";
554
+ const namedDomain = wizardState?.domain?.cloudflare?.zoneName || wizardState?.domain?.name || "";
555
+ if (tunnelMode === "named" && namedDomain) {
556
+ const sub = NAMED_SUBDOMAIN_MAP[composeService];
557
+ if (sub) {
558
+ externalUrl = `https://${sub}.${namedDomain}`;
559
+ }
560
+ if (!externalUrl) {
561
+ const appNameVariants = [composeService, composeService.replace(/^brewnet-/, "")];
562
+ const conn = (wizardState?.domainConnections ?? []).find(
563
+ (c2) => appNameVariants.includes(c2.appName)
564
+ );
565
+ if (conn) externalUrl = `https://${conn.hostname}`;
566
+ }
567
+ } else if (qtUrl && traefikPath) {
1601
568
  let extPath = traefikPath;
1602
569
  const stackLabel = labels["com.brewnet.stack"] ?? "";
1603
570
  if (stackLabel === "nodejs-nextjs" || composeService === "backend" && extPath.includes("nextjs-app")) {
@@ -1605,7 +572,7 @@ async function handleGetServices(_req, res, _parts, _body, _projectPath, urlMap
1605
572
  }
1606
573
  externalUrl = qtUrl.replace(/\/$/, "") + extPath;
1607
574
  }
1608
- if (!externalUrl && qtUrl) {
575
+ if (!externalUrl && qtUrl && tunnelMode !== "named") {
1609
576
  const EXT_PATH_MAP = {
1610
577
  traefik: "",
1611
578
  gitea: "/git",
@@ -1762,7 +729,7 @@ async function handleGetCatalog(_req, res, _parts, _body, _projectPath) {
1762
729
  }
1763
730
  }
1764
731
  async function handleBackup(req, res, _parts, _body, projectPath) {
1765
- const backupsDir = join2(homedir2(), ".brewnet", "backups");
732
+ const backupsDir = join(homedir(), ".brewnet", "backups");
1766
733
  if (req.method === "GET") {
1767
734
  try {
1768
735
  const backups = listBackups(backupsDir);
@@ -1803,7 +770,7 @@ function createAdminServer(options = {}) {
1803
770
  }
1804
771
  }
1805
772
  if (projectPath.startsWith("~/") || projectPath === "~") {
1806
- projectPath = join2(homedir2(), projectPath.slice(1));
773
+ projectPath = join(homedir(), projectPath.slice(1));
1807
774
  }
1808
775
  const username = wizardState?.admin?.username ?? "";
1809
776
  const password = wizardState?.admin?.password ?? "";
@@ -1812,17 +779,17 @@ function createAdminServer(options = {}) {
1812
779
  const allowedWorkingDirs = /* @__PURE__ */ new Set();
1813
780
  allowedWorkingDirs.add(projectPath);
1814
781
  try {
1815
- const bpMetaPath2 = join2(projectPath, ".brewnet-boilerplate.json");
1816
- if (existsSync3(bpMetaPath2)) {
1817
- const raw2 = JSON.parse(readFileSync3(bpMetaPath2, "utf-8"));
782
+ const bpMetaPath2 = join(projectPath, ".brewnet-boilerplate.json");
783
+ if (existsSync(bpMetaPath2)) {
784
+ const raw2 = JSON.parse(readFileSync(bpMetaPath2, "utf-8"));
1818
785
  const stacks2 = Array.isArray(raw2) ? raw2 : raw2.stackId ? [raw2] : [];
1819
786
  for (const s of stacks2) {
1820
787
  if (s.appDir) allowedWorkingDirs.add(s.appDir);
1821
788
  }
1822
789
  }
1823
- const appsJsonPath = join2(homedir2(), ".brewnet", "apps.json");
1824
- if (existsSync3(appsJsonPath)) {
1825
- const apps = JSON.parse(readFileSync3(appsJsonPath, "utf-8"));
790
+ const appsJsonPath = join(homedir(), ".brewnet", "apps.json");
791
+ if (existsSync(appsJsonPath)) {
792
+ const apps = JSON.parse(readFileSync(appsJsonPath, "utf-8"));
1826
793
  for (const app of apps) {
1827
794
  if (app.appDir) allowedWorkingDirs.add(app.appDir);
1828
795
  }
@@ -1926,7 +893,7 @@ function createAdminServer(options = {}) {
1926
893
  res.end("Forbidden");
1927
894
  return;
1928
895
  }
1929
- if (existsSync3(safePath) && statSync(safePath).isFile()) {
896
+ if (existsSync(safePath) && statSync(safePath).isFile()) {
1930
897
  serveStaticFile(safePath, res);
1931
898
  return;
1932
899
  }
@@ -1937,12 +904,12 @@ function createAdminServer(options = {}) {
1937
904
  if (req.method === "GET" && !url.startsWith("/api/")) {
1938
905
  const pathname = url.split("?")[0];
1939
906
  const exactPath = resolve(ADMIN_UI_DIST, "." + (pathname === "/" ? "/index.html" : pathname));
1940
- if (exactPath.startsWith(ADMIN_UI_DIST) && existsSync3(exactPath) && statSync(exactPath).isFile()) {
907
+ if (exactPath.startsWith(ADMIN_UI_DIST) && existsSync(exactPath) && statSync(exactPath).isFile()) {
1941
908
  serveStaticFile(exactPath, res);
1942
909
  return;
1943
910
  }
1944
- const indexPath = join2(ADMIN_UI_DIST, "index.html");
1945
- if (existsSync3(indexPath)) {
911
+ const indexPath = join(ADMIN_UI_DIST, "index.html");
912
+ if (existsSync(indexPath)) {
1946
913
  serveStaticFile(indexPath, res);
1947
914
  return;
1948
915
  }
@@ -1964,7 +931,21 @@ function createAdminServer(options = {}) {
1964
931
  return;
1965
932
  }
1966
933
  if (parts[1] === "services" && parts[2] === "catalog" && req.method === "GET") {
1967
- json(res, 200, { catalog: SERVICE_DETAIL_MAP, aliases: NAME_ALIASES });
934
+ const adminUser = wizardState?.admin?.username ?? "USER";
935
+ const catalog = { ...SERVICE_DETAIL_MAP };
936
+ if (catalog["SSH Server"]) {
937
+ catalog["SSH Server"] = {
938
+ ...catalog["SSH Server"],
939
+ credentials: {
940
+ ...catalog["SSH Server"].credentials,
941
+ command: `ssh -p 2222 ${adminUser}@localhost`
942
+ },
943
+ connectionParams: catalog["SSH Server"].connectionParams?.map(
944
+ (p) => p.label === "user" ? { ...p, value: adminUser } : p
945
+ )
946
+ };
947
+ }
948
+ json(res, 200, { catalog, aliases: NAME_ALIASES });
1968
949
  return;
1969
950
  }
1970
951
  if (parts[1] === "config" && req.method === "GET") {
@@ -1982,7 +963,7 @@ function createAdminServer(options = {}) {
1982
963
  }
1983
964
  if (parts[1] === "services") {
1984
965
  if (req.method === "GET" && parts.length === 2) {
1985
- await handleGetServices(req, res, parts, body, projectPath, runtimeUrlMap, dashConfig.quickTunnelUrl, allowedWorkingDirs);
966
+ await handleGetServices(req, res, parts, body, projectPath, runtimeUrlMap, dashConfig.quickTunnelUrl, allowedWorkingDirs, wizardState);
1986
967
  return;
1987
968
  }
1988
969
  if (req.method === "POST" && parts[2] === "install") {
@@ -2047,11 +1028,11 @@ function createAdminServer(options = {}) {
2047
1028
  for (const h of history) {
2048
1029
  if (h.status === "success") historyByApp.set(h.appName, h);
2049
1030
  }
2050
- const bpMetaPath = join2(projectPath, ".brewnet-boilerplate.json");
2051
- let bpMetaMap = /* @__PURE__ */ new Map();
2052
- if (existsSync3(bpMetaPath)) {
1031
+ const bpMetaPath = join(projectPath, ".brewnet-boilerplate.json");
1032
+ const bpMetaMap = /* @__PURE__ */ new Map();
1033
+ if (existsSync(bpMetaPath)) {
2053
1034
  try {
2054
- const raw = JSON.parse(readFileSync3(bpMetaPath, "utf-8"));
1035
+ const raw = JSON.parse(readFileSync(bpMetaPath, "utf-8"));
2055
1036
  const metas = Array.isArray(raw) ? raw : [raw];
2056
1037
  for (const m of metas) bpMetaMap.set(m.stackId, m);
2057
1038
  } catch {
@@ -2066,25 +1047,26 @@ function createAdminServer(options = {}) {
2066
1047
  let externalUrl;
2067
1048
  let backendLocalUrl = null;
2068
1049
  let backendExternalUrl = null;
1050
+ const domainConn = (wizardState?.domainConnections ?? []).find((c) => c.appName === a.name);
2069
1051
  if (isNonUnified) {
2070
1052
  let frontendPort = 3e3;
2071
- const feEnvPath = join2(a.appDir, ".env");
2072
- if (existsSync3(feEnvPath)) {
2073
- const feEnvContent = readFileSync3(feEnvPath, "utf-8");
1053
+ const feEnvPath = join(a.appDir, ".env");
1054
+ if (existsSync(feEnvPath)) {
1055
+ const feEnvContent = readFileSync(feEnvPath, "utf-8");
2074
1056
  const fePortMatch = feEnvContent.match(/^FRONTEND_PORT=(\d+)/m);
2075
1057
  if (fePortMatch) frontendPort = parseInt(fePortMatch[1], 10);
2076
1058
  }
2077
1059
  localUrl = `http://127.0.0.1:${frontendPort}`;
2078
- externalUrl = qt ? `${qt.replace(/\/$/, "")}/apps/${a.name}-ui` : null;
1060
+ externalUrl = domainConn ? `https://${domainConn.hostname}` : qt ? `${qt.replace(/\/$/, "")}/apps/${a.name}-ui` : null;
2079
1061
  backendLocalUrl = a.port ? `http://127.0.0.1:${a.port}` : null;
2080
- backendExternalUrl = qt ? `${qt.replace(/\/$/, "")}/apps/${a.name}` : null;
1062
+ backendExternalUrl = domainConn ? `https://${domainConn.hostname}` : qt ? `${qt.replace(/\/$/, "")}/apps/${a.name}` : null;
2081
1063
  } else {
2082
1064
  localUrl = a.port ? `http://localhost:${a.port}` : null;
2083
1065
  if (a.appDir) {
2084
1066
  const bp = detectBasePath(a.appDir);
2085
1067
  if (bp && localUrl) localUrl += bp;
2086
1068
  }
2087
- externalUrl = qt ? `${qt.replace(/\/$/, "")}/apps/${a.name}` : null;
1069
+ externalUrl = domainConn ? `https://${domainConn.hostname}` : qt ? `${qt.replace(/\/$/, "")}/apps/${a.name}` : null;
2088
1070
  }
2089
1071
  return { ...a, lastDeployedAt: lastDeploy?.deployedAt ?? null, localUrl, externalUrl, backendLocalUrl, backendExternalUrl };
2090
1072
  });
@@ -2093,10 +1075,10 @@ function createAdminServer(options = {}) {
2093
1075
  return;
2094
1076
  }
2095
1077
  if (req.method === "GET" && parts[2] === "boilerplates") {
2096
- const bpPath = join2(projectPath, ".brewnet-boilerplate.json");
1078
+ const bpPath = join(projectPath, ".brewnet-boilerplate.json");
2097
1079
  const metas = [];
2098
- if (existsSync3(bpPath)) {
2099
- const raw = JSON.parse(readFileSync3(bpPath, "utf-8"));
1080
+ if (existsSync(bpPath)) {
1081
+ const raw = JSON.parse(readFileSync(bpPath, "utf-8"));
2100
1082
  const wizardMetas = Array.isArray(raw) ? raw : [raw];
2101
1083
  metas.push(...wizardMetas);
2102
1084
  }
@@ -2104,13 +1086,13 @@ function createAdminServer(options = {}) {
2104
1086
  for (const app of allApps) {
2105
1087
  if (app.mode !== "boilerplate" || !app.stackId) continue;
2106
1088
  if (metas.some((m) => m.appDir && app.appDir && m.appDir === app.appDir)) continue;
2107
- const envPath = join2(app.appDir, ".env");
1089
+ const envPath = join(app.appDir, ".env");
2108
1090
  let frontendPort;
2109
1091
  let dbDriver;
2110
1092
  let dbUser;
2111
1093
  let dbName;
2112
- if (existsSync3(envPath)) {
2113
- const envContent = readFileSync3(envPath, "utf-8");
1094
+ if (existsSync(envPath)) {
1095
+ const envContent = readFileSync(envPath, "utf-8");
2114
1096
  const fpMatch = envContent.match(/^FRONTEND_PORT=(\d+)/m);
2115
1097
  if (fpMatch) frontendPort = parseInt(fpMatch[1], 10);
2116
1098
  const ddMatch = envContent.match(/^DB_DRIVER=(.+)/m);
@@ -2141,12 +1123,12 @@ function createAdminServer(options = {}) {
2141
1123
  if (req.method === "POST" && parts[2] === "boilerplates" && parts[3] && (parts[4] === "stop" || parts[4] === "start")) {
2142
1124
  const stackId = decodeURIComponent(parts[3]);
2143
1125
  const action = parts[4];
2144
- const bpPath = join2(projectPath, ".brewnet-boilerplate.json");
2145
- if (!existsSync3(bpPath)) {
1126
+ const bpPath = join(projectPath, ".brewnet-boilerplate.json");
1127
+ if (!existsSync(bpPath)) {
2146
1128
  json(res, 404, { error: "No boilerplates found" });
2147
1129
  return;
2148
1130
  }
2149
- const bpRaw = JSON.parse(readFileSync3(bpPath, "utf-8"));
1131
+ const bpRaw = JSON.parse(readFileSync(bpPath, "utf-8"));
2150
1132
  const bpMetas = Array.isArray(bpRaw) ? bpRaw : [bpRaw];
2151
1133
  const meta = bpMetas.find((m) => m.stackId === stackId);
2152
1134
  if (!meta) {
@@ -2161,8 +1143,8 @@ function createAdminServer(options = {}) {
2161
1143
  await execaBp("docker", ["compose", "up", "-d"], { cwd: meta.appDir });
2162
1144
  meta.status = "running";
2163
1145
  }
2164
- const { writeFileSync: writeFileSync4 } = await import("fs");
2165
- writeFileSync4(bpPath, JSON.stringify(bpMetas, null, 2), "utf-8");
1146
+ const { writeFileSync: writeFileSync2 } = await import("fs");
1147
+ writeFileSync2(bpPath, JSON.stringify(bpMetas, null, 2), "utf-8");
2166
1148
  json(res, 200, { success: true });
2167
1149
  return;
2168
1150
  }
@@ -2192,7 +1174,7 @@ function createAdminServer(options = {}) {
2192
1174
  return;
2193
1175
  }
2194
1176
  if (req.method === "DELETE" && parts[2]) {
2195
- await removeApp2(parts[2]);
1177
+ await removeApp(parts[2]);
2196
1178
  json(res, 200, { success: true });
2197
1179
  return;
2198
1180
  }
@@ -2207,10 +1189,10 @@ function createAdminServer(options = {}) {
2207
1189
  const lastDeploy = history.filter((h) => h.appName === found.name && h.status === "success").pop() ?? null;
2208
1190
  const qt = dashConfig.quickTunnelUrl;
2209
1191
  const bpMetaSingle = found.mode === "boilerplate" && found.stackId ? (() => {
2210
- const p = join2(projectPath, ".brewnet-boilerplate.json");
2211
- if (!existsSync3(p)) return void 0;
1192
+ const p = join(projectPath, ".brewnet-boilerplate.json");
1193
+ if (!existsSync(p)) return void 0;
2212
1194
  try {
2213
- const raw = JSON.parse(readFileSync3(p, "utf-8"));
1195
+ const raw = JSON.parse(readFileSync(p, "utf-8"));
2214
1196
  const list = Array.isArray(raw) ? raw : [raw];
2215
1197
  return list.find((m) => m.stackId === found.stackId);
2216
1198
  } catch {
@@ -2222,24 +1204,25 @@ function createAdminServer(options = {}) {
2222
1204
  let externalUrlSingle;
2223
1205
  let backendLocalUrlSingle = null;
2224
1206
  let backendExternalUrlSingle = null;
1207
+ const domainConnSingle = (wizardState?.domainConnections ?? []).find((c) => c.appName === found.name);
2225
1208
  if (isNonUnified) {
2226
1209
  let frontendPort = 3e3;
2227
- const feEnvPath = join2(found.appDir, ".env");
2228
- if (existsSync3(feEnvPath)) {
2229
- const m = readFileSync3(feEnvPath, "utf-8").match(/^FRONTEND_PORT=(\d+)/m);
1210
+ const feEnvPath = join(found.appDir, ".env");
1211
+ if (existsSync(feEnvPath)) {
1212
+ const m = readFileSync(feEnvPath, "utf-8").match(/^FRONTEND_PORT=(\d+)/m);
2230
1213
  if (m) frontendPort = parseInt(m[1], 10);
2231
1214
  }
2232
1215
  localUrlSingle = `http://127.0.0.1:${frontendPort}`;
2233
- externalUrlSingle = qt ? `${qt.replace(/\/$/, "")}/apps/${found.name}-ui` : null;
1216
+ externalUrlSingle = domainConnSingle ? `https://${domainConnSingle.hostname}` : qt ? `${qt.replace(/\/$/, "")}/apps/${found.name}-ui` : null;
2234
1217
  backendLocalUrlSingle = found.port ? `http://127.0.0.1:${found.port}` : null;
2235
- backendExternalUrlSingle = qt ? `${qt.replace(/\/$/, "")}/apps/${found.name}` : null;
1218
+ backendExternalUrlSingle = domainConnSingle ? `https://${domainConnSingle.hostname}` : qt ? `${qt.replace(/\/$/, "")}/apps/${found.name}` : null;
2236
1219
  } else {
2237
1220
  localUrlSingle = found.port ? `http://localhost:${found.port}` : null;
2238
1221
  if (found.appDir) {
2239
1222
  const bp = detectBasePath(found.appDir);
2240
1223
  if (bp && localUrlSingle) localUrlSingle += bp;
2241
1224
  }
2242
- externalUrlSingle = qt ? `${qt.replace(/\/$/, "")}/apps/${found.name}` : null;
1225
+ externalUrlSingle = domainConnSingle ? `https://${domainConnSingle.hostname}` : qt ? `${qt.replace(/\/$/, "")}/apps/${found.name}` : null;
2243
1226
  }
2244
1227
  const app = { ...found, lastDeployedAt: lastDeploy?.deployedAt ?? null, localUrl: localUrlSingle, externalUrl: externalUrlSingle, backendLocalUrl: backendLocalUrlSingle, backendExternalUrl: backendExternalUrlSingle };
2245
1228
  json(res, 200, { app });
@@ -2258,7 +1241,7 @@ function createAdminServer(options = {}) {
2258
1241
  try {
2259
1242
  const branches = await getAppBranches(decodeURIComponent(parts[2] ?? ""));
2260
1243
  json(res, 200, { branches });
2261
- } catch (err) {
1244
+ } catch (_err) {
2262
1245
  json(res, 200, { branches: [] });
2263
1246
  }
2264
1247
  return;
@@ -2446,8 +1429,8 @@ function createAdminServer(options = {}) {
2446
1429
  return;
2447
1430
  }
2448
1431
  try {
2449
- const appsPath = join2(homedir2(), ".brewnet", "apps.json");
2450
- let existing = existsSync3(appsPath) ? JSON.parse(readFileSync3(appsPath, "utf-8")) : [];
1432
+ const appsPath = join(homedir(), ".brewnet", "apps.json");
1433
+ let existing = existsSync(appsPath) ? JSON.parse(readFileSync(appsPath, "utf-8")) : [];
2451
1434
  const repos = await listGiteaRepos();
2452
1435
  const repo = repos.find((r) => r.name === repoName);
2453
1436
  if (!repo) {
@@ -2474,7 +1457,7 @@ function createAdminServer(options = {}) {
2474
1457
  app = {
2475
1458
  name: appName,
2476
1459
  mode: "boilerplate",
2477
- appDir: join2(projectPath, repoName),
1460
+ appDir: join(projectPath, repoName),
2478
1461
  lang,
2479
1462
  port: port2,
2480
1463
  giteaRepoUrl: repoUrl,
@@ -2489,7 +1472,7 @@ function createAdminServer(options = {}) {
2489
1472
  } else {
2490
1473
  app.giteaRepoUrl = repoUrl;
2491
1474
  }
2492
- writeFileSync3(appsPath, JSON.stringify(existing, null, 2));
1475
+ writeFileSync(appsPath, JSON.stringify(existing, null, 2));
2493
1476
  json(res, 200, { ok: true });
2494
1477
  } catch (err) {
2495
1478
  json(res, 500, { error: String(err) });
@@ -2631,7 +1614,7 @@ async function handleDomainList(res, state) {
2631
1614
  const cf = state.domain.cloudflare;
2632
1615
  if (cf.tunnelId && cf.apiToken && cf.accountId) {
2633
1616
  try {
2634
- const { getTunnelHealth } = await import("./cloudflare-client-TFT6VCXF.js");
1617
+ const { getTunnelHealth } = await import("./cloudflare-client-F2TGQXGS.js");
2635
1618
  const health = await getTunnelHealth(cf.apiToken, cf.accountId, cf.tunnelId);
2636
1619
  tunnel = { ...health, tunnelName: cf.tunnelName, tunnelId: cf.tunnelId };
2637
1620
  } catch {
@@ -2708,6 +1691,10 @@ async function handleDomainConnect(res, body, state) {
2708
1691
  json(res, statusCode, { success: false, error: result.error, message: result.error, steps: result.steps });
2709
1692
  return;
2710
1693
  }
1694
+ const freshStateAfterConnect = loadState(state.projectName);
1695
+ if (freshStateAfterConnect?.domainConnections) {
1696
+ state.domainConnections = freshStateAfterConnect.domainConnections;
1697
+ }
2711
1698
  json(res, 200, {
2712
1699
  success: true,
2713
1700
  hostname: result.hostname,
@@ -2731,6 +1718,10 @@ async function handleDomainDisconnect(res, appName, state) {
2731
1718
  json(res, statusCode, { success: false, error: result.error?.split(":")[0], message: result.error });
2732
1719
  return;
2733
1720
  }
1721
+ const freshStateAfterDisconnect = loadState(state.projectName);
1722
+ if (freshStateAfterDisconnect) {
1723
+ state.domainConnections = freshStateAfterDisconnect.domainConnections ?? [];
1724
+ }
2734
1725
  json(res, 200, {
2735
1726
  success: true,
2736
1727
  appName: result.appName,
@@ -2769,7 +1760,7 @@ async function handleCloudflareZones(res, state) {
2769
1760
  return;
2770
1761
  }
2771
1762
  try {
2772
- const { getZones } = await import("./cloudflare-client-TFT6VCXF.js");
1763
+ const { getZones } = await import("./cloudflare-client-F2TGQXGS.js");
2773
1764
  const zones = await getZones(apiToken);
2774
1765
  if (!state.domain.cloudflare.accountId && zones.length > 0) {
2775
1766
  const firstAccountId = zones[0]?.accountId;
@@ -2788,7 +1779,7 @@ async function handleCloudflareZones(res, state) {
2788
1779
  return;
2789
1780
  }
2790
1781
  json(res, 200, { success: true, zones });
2791
- } catch (err) {
1782
+ } catch (_err) {
2792
1783
  json(res, 400, {
2793
1784
  success: false,
2794
1785
  error: "TOKEN_INVALID",
@@ -2819,7 +1810,7 @@ async function handleCreateTunnel(res, body, state, projectPath) {
2819
1810
  return;
2820
1811
  }
2821
1812
  try {
2822
- const { createTunnel: cfCreateTunnel } = await import("./cloudflare-client-TFT6VCXF.js");
1813
+ const { createTunnel: cfCreateTunnel } = await import("./cloudflare-client-F2TGQXGS.js");
2823
1814
  const result = await cfCreateTunnel(cf.apiToken, cf.accountId, tunnelName.trim());
2824
1815
  const { saveState: save } = await import("./state-2SI3P4JG.js");
2825
1816
  state.domain.cloudflare.tunnelId = result.tunnelId;
@@ -2828,13 +1819,27 @@ async function handleCreateTunnel(res, body, state, projectPath) {
2828
1819
  state.domain.cloudflare.tunnelMode = "named";
2829
1820
  state.domain.cloudflare.enabled = true;
2830
1821
  save(state);
1822
+ if (cf.zoneName) {
1823
+ state.domain.name = cf.zoneName;
1824
+ save(state);
1825
+ }
1826
+ const zoneName = cf.zoneName;
1827
+ try {
1828
+ const { saveGiteaConfig } = await import("./app-manager-FIHPVUP7.js");
1829
+ const adminUsername = state.admin?.username ?? "admin";
1830
+ saveGiteaConfig("http://localhost/git", adminUsername);
1831
+ logger.info("tunnel", `[${tunnelName}] gitea-config.json normalized \u2192 http://localhost/git`);
1832
+ } catch (e) {
1833
+ logger.warn("tunnel", `[${tunnelName}] gitea-config.json update failed: ${e instanceof Error ? e.message : e}`);
1834
+ }
1835
+ const steps = [];
2831
1836
  let composeUpdated = false;
2832
1837
  let containerRestarted = false;
2833
- const composePath = join2(projectPath, "docker-compose.yml");
1838
+ const composePath = join(projectPath, "docker-compose.yml");
2834
1839
  const { existsSync: fsExists } = await import("fs");
2835
1840
  if (fsExists(composePath)) {
2836
1841
  try {
2837
- const { patchCloudflaredToNamedTunnel } = await import("./compose-generator-O7GSIJ2S.js");
1842
+ const { patchCloudflaredToNamedTunnel } = await import("./compose-generator-OFJ2YWMB.js");
2838
1843
  composeUpdated = patchCloudflaredToNamedTunnel(composePath, result.tunnelToken);
2839
1844
  logger.info("tunnel", `[${tunnelName}] compose patch: composeUpdated=${composeUpdated}`);
2840
1845
  } catch (e) {
@@ -2858,6 +1863,67 @@ async function handleCreateTunnel(res, body, state, projectPath) {
2858
1863
  logger.warn("tunnel", `[${tunnelName}] cloudflared recreate exception: ${e instanceof Error ? e.message : e}`);
2859
1864
  }
2860
1865
  }
1866
+ if (cf.apiToken && cf.accountId && cf.tunnelId && zoneName) {
1867
+ try {
1868
+ const { configureTunnelIngress, getActiveServiceRoutes } = await import("./cloudflare-client-F2TGQXGS.js");
1869
+ const routes = getActiveServiceRoutes(state).map((r) => ({ ...r, domain: zoneName }));
1870
+ await configureTunnelIngress(cf.apiToken, cf.accountId, cf.tunnelId, zoneName, routes);
1871
+ steps.push({ step: "ingress_configured", success: true, services: routes.map((r) => r.subdomain) });
1872
+ logger.info("tunnel", `[${tunnelName}] ingress configured for: ${routes.map((r) => r.subdomain).join(", ")}`);
1873
+ } catch (e) {
1874
+ const msg = e instanceof Error ? e.message : String(e);
1875
+ steps.push({ step: "ingress_configured", success: false, detail: msg });
1876
+ logger.warn("tunnel", `[${tunnelName}] ingress configure failed (non-fatal): ${msg}`);
1877
+ }
1878
+ const { createDnsRecord, getActiveServiceRoutes: getRoutes } = await import("./cloudflare-client-F2TGQXGS.js");
1879
+ const dnsRoutes = getRoutes(state);
1880
+ const dnsResults = [];
1881
+ const dnsFailed = [];
1882
+ for (const route of dnsRoutes) {
1883
+ try {
1884
+ await createDnsRecord(cf.apiToken, cf.zoneId, cf.tunnelId, route.subdomain, zoneName);
1885
+ dnsResults.push(route.subdomain);
1886
+ } catch (e) {
1887
+ const msg = e instanceof Error ? e.message : String(e);
1888
+ logger.warn("tunnel", `[${tunnelName}] DNS CNAME for ${route.subdomain} failed (non-fatal): ${msg}`);
1889
+ dnsFailed.push(route.subdomain);
1890
+ }
1891
+ }
1892
+ steps.push({ step: "dns_created", success: dnsFailed.length === 0, services: dnsResults, ...dnsFailed.length > 0 ? { detail: `skipped: ${dnsFailed.join(", ")}` } : {} });
1893
+ }
1894
+ if (zoneName) {
1895
+ try {
1896
+ const { patchBuiltinServicesForNamedTunnel } = await import("./compose-generator-OFJ2YWMB.js");
1897
+ const patchedServices = patchBuiltinServicesForNamedTunnel(composePath, zoneName);
1898
+ steps.push({ step: "services_env_patched", success: true, services: patchedServices });
1899
+ logger.info("tunnel", `[${tunnelName}] env patched for: ${patchedServices.join(", ") || "none"}`);
1900
+ if (patchedServices.length > 0) {
1901
+ try {
1902
+ const { execa: execaSvc } = await import("execa");
1903
+ const restart = await execaSvc(
1904
+ "docker",
1905
+ ["compose", "-f", composePath, "up", "-d", "--force-recreate", ...patchedServices],
1906
+ { cwd: projectPath, reject: false }
1907
+ );
1908
+ const restarted = restart.exitCode === 0;
1909
+ steps.push({ step: "services_restarted", success: restarted, services: patchedServices });
1910
+ if (!restarted) {
1911
+ logger.warn("tunnel", `[${tunnelName}] service restart failed (exit ${restart.exitCode}): ${restart.stderr}`);
1912
+ } else {
1913
+ logger.info("tunnel", `[${tunnelName}] restarted: ${patchedServices.join(", ")}`);
1914
+ }
1915
+ } catch (e) {
1916
+ const msg = e instanceof Error ? e.message : String(e);
1917
+ steps.push({ step: "services_restarted", success: false, detail: msg });
1918
+ logger.warn("tunnel", `[${tunnelName}] service restart exception: ${msg}`);
1919
+ }
1920
+ }
1921
+ } catch (e) {
1922
+ const msg = e instanceof Error ? e.message : String(e);
1923
+ steps.push({ step: "services_env_patched", success: false, detail: msg });
1924
+ logger.warn("tunnel", `[${tunnelName}] env patch failed (non-fatal): ${msg}`);
1925
+ }
1926
+ }
2861
1927
  } else {
2862
1928
  logger.warn("tunnel", `[${tunnelName}] compose file not found at ${composePath} \u2014 cloudflared must be updated manually`);
2863
1929
  }
@@ -2866,7 +1932,8 @@ async function handleCreateTunnel(res, body, state, projectPath) {
2866
1932
  tunnelId: result.tunnelId,
2867
1933
  tunnelName: tunnelName.trim(),
2868
1934
  composeUpdated,
2869
- containerRestarted
1935
+ containerRestarted,
1936
+ steps
2870
1937
  });
2871
1938
  } catch (err) {
2872
1939
  const msg = err instanceof Error ? err.message : String(err);
@@ -2935,13 +2002,13 @@ async function handleSettingsCloudflarePut(res, body, state) {
2935
2002
  }
2936
2003
  const { saveState: save } = await import("./state-2SI3P4JG.js");
2937
2004
  state.domain.cloudflare.apiToken = apiToken;
2938
- let resolvedAccountId = accountId || state.domain.cloudflare.accountId || await (await import("./cloudflare-client-TFT6VCXF.js")).getAccounts(apiToken).then((a) => a[0]?.id ?? "").catch(() => "");
2005
+ let resolvedAccountId = accountId || state.domain.cloudflare.accountId || await (await import("./cloudflare-client-F2TGQXGS.js")).getAccounts(apiToken).then((a) => a[0]?.id ?? "").catch(() => "");
2939
2006
  if (zoneId) state.domain.cloudflare.zoneId = zoneId;
2940
2007
  if (tunnelId) state.domain.cloudflare.tunnelId = tunnelId;
2941
2008
  let zoneName = state.domain.cloudflare.zoneName;
2942
2009
  if (zoneId) {
2943
2010
  try {
2944
- const { getZones } = await import("./cloudflare-client-TFT6VCXF.js");
2011
+ const { getZones } = await import("./cloudflare-client-F2TGQXGS.js");
2945
2012
  const zones = await getZones(apiToken);
2946
2013
  const found = zones.find((z) => z.id === zoneId);
2947
2014
  if (found) {
@@ -2970,4 +2037,4 @@ async function handleSettingsCloudflarePut(res, body, state) {
2970
2037
  export {
2971
2038
  createAdminServer
2972
2039
  };
2973
- //# sourceMappingURL=chunk-SIXBB6JU.js.map
2040
+ //# sourceMappingURL=chunk-Q6UUZR2V.js.map