dokku-compose 0.3.6 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +711 -487
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -27,6 +27,9 @@ var ChecksSchema = z.union([
27
27
  skipped: z.array(z.string()).optional()
28
28
  }).catchall(z.union([z.string(), z.number(), z.boolean()]))
29
29
  ]);
30
+ var GitSchema = z.object({
31
+ deploy_branch: z.string().optional()
32
+ });
30
33
  var AppSchema = z.object({
31
34
  domains: z.union([z.array(z.string()), z.literal(false)]).optional(),
32
35
  links: z.array(z.string()).optional(),
@@ -61,12 +64,26 @@ var AppSchema = z.object({
61
64
  build: z.array(z.string()).optional(),
62
65
  deploy: z.array(z.string()).optional(),
63
66
  run: z.array(z.string()).optional()
64
- }).optional()
67
+ }).optional(),
68
+ git: GitSchema.optional()
69
+ });
70
+ var ServiceBackupAuthSchema = z.object({
71
+ access_key_id: z.string(),
72
+ secret_access_key: z.string(),
73
+ region: z.string(),
74
+ signature_version: z.string(),
75
+ endpoint: z.string()
76
+ });
77
+ var ServiceBackupSchema = z.object({
78
+ schedule: z.string(),
79
+ bucket: z.string(),
80
+ auth: ServiceBackupAuthSchema
65
81
  });
66
82
  var ServiceSchema = z.object({
67
83
  plugin: z.string(),
68
84
  version: z.string().optional(),
69
- image: z.string().optional()
85
+ image: z.string().optional(),
86
+ backup: ServiceBackupSchema.optional()
70
87
  });
71
88
  var PluginSchema = z.object({
72
89
  url: z.string().url(),
@@ -83,18 +100,23 @@ var ConfigSchema = z.object({
83
100
  domains: z.union([z.array(z.string()), z.literal(false)]).optional(),
84
101
  env: EnvMapSchema.optional(),
85
102
  nginx: z.record(z.string(), z.union([z.string(), z.number()])).optional(),
86
- logs: z.record(z.string(), z.union([z.string(), z.number()])).optional()
103
+ logs: z.record(z.string(), z.union([z.string(), z.number()])).optional(),
104
+ git: GitSchema.optional()
87
105
  });
88
106
  function parseConfig(raw) {
89
107
  return ConfigSchema.parse(raw);
90
108
  }
91
109
 
92
110
  // src/core/config.ts
111
+ function interpolateEnvVars(content) {
112
+ return content.replace(/\$\{([^}]+)\}/g, (_, name) => process.env[name] ?? "");
113
+ }
93
114
  function loadConfig(filePath) {
94
115
  if (!fs.existsSync(filePath)) {
95
116
  throw new Error(`Config file not found: ${filePath}`);
96
117
  }
97
- const raw = yaml.load(fs.readFileSync(filePath, "utf8"));
118
+ const content = fs.readFileSync(filePath, "utf8");
119
+ const raw = yaml.load(interpolateEnvVars(content));
98
120
  return parseConfig(raw);
99
121
  }
100
122
 
@@ -153,6 +175,69 @@ function createRunner(opts = {}) {
153
175
  };
154
176
  }
155
177
 
178
+ // src/core/context.ts
179
+ function createContext(runner) {
180
+ const cache = /* @__PURE__ */ new Map();
181
+ const commands = [];
182
+ return {
183
+ commands,
184
+ runner,
185
+ query(...args) {
186
+ const key = args.join("\0");
187
+ if (!cache.has(key)) {
188
+ cache.set(key, runner.query(...args));
189
+ }
190
+ return cache.get(key);
191
+ },
192
+ check(...args) {
193
+ return runner.check(...args);
194
+ },
195
+ async run(...args) {
196
+ commands.push(args);
197
+ await runner.run(...args);
198
+ },
199
+ close() {
200
+ return runner.close();
201
+ }
202
+ };
203
+ }
204
+
205
+ // src/core/change.ts
206
+ function computeChange(before, after) {
207
+ if (before === null || before === void 0 || after === null || after === void 0) {
208
+ return { before, after, changed: before !== after };
209
+ }
210
+ if (Array.isArray(before) && Array.isArray(after)) {
211
+ const beforeSet = new Set(before);
212
+ const afterSet = new Set(after);
213
+ const added = after.filter((x) => !beforeSet.has(x));
214
+ const removed = before.filter((x) => !afterSet.has(x));
215
+ return {
216
+ before,
217
+ after,
218
+ changed: added.length > 0 || removed.length > 0,
219
+ added,
220
+ removed
221
+ };
222
+ }
223
+ if (typeof before === "object" && typeof after === "object") {
224
+ const b = before;
225
+ const a = after;
226
+ const allKeys = /* @__PURE__ */ new Set([...Object.keys(b), ...Object.keys(a)]);
227
+ const added = {};
228
+ const removed = [];
229
+ const modified = {};
230
+ for (const key of allKeys) {
231
+ if (!(key in b)) added[key] = a[key];
232
+ else if (!(key in a)) removed.push(key);
233
+ else if (String(b[key]) !== String(a[key])) modified[key] = a[key];
234
+ }
235
+ const changed = Object.keys(added).length > 0 || removed.length > 0 || Object.keys(modified).length > 0;
236
+ return { before, after, changed, added, removed, modified };
237
+ }
238
+ return { before, after, changed: before !== after };
239
+ }
240
+
156
241
  // src/core/logger.ts
157
242
  import chalk from "chalk";
158
243
  function logAction(context, message) {
@@ -165,68 +250,416 @@ function logSkip() {
165
250
  console.log(`... ${chalk.yellow("already configured")}`);
166
251
  }
167
252
 
168
- // src/modules/apps.ts
169
- async function ensureApp(runner, app) {
170
- const exists = await runner.check("apps:exists", app);
171
- logAction(app, "Creating app");
172
- if (exists) {
173
- logSkip();
253
+ // src/core/reconcile.ts
254
+ async function reconcile(resource, ctx, target, desired) {
255
+ if (desired === void 0) return;
256
+ logAction(target, `${resource.key}`);
257
+ if (resource.forceApply) {
258
+ await resource.onChange(ctx, target, { before: void 0, after: desired, changed: true });
259
+ logDone();
174
260
  return;
175
261
  }
176
- await runner.run("apps:create", app);
177
- logDone();
178
- }
179
- async function destroyApp(runner, app) {
180
- const exists = await runner.check("apps:exists", app);
181
- logAction(app, "Destroying app");
182
- if (!exists) {
262
+ const before = await resource.read(ctx, target);
263
+ const change = computeChange(before, desired);
264
+ if (!change.changed) {
183
265
  logSkip();
184
266
  return;
185
267
  }
186
- await runner.run("apps:destroy", app, "--force");
268
+ await resource.onChange(ctx, target, change);
187
269
  logDone();
188
270
  }
189
- async function exportApps(runner) {
190
- const output = await runner.query("apps:list");
191
- return output.split("\n").map((s) => s.trim()).filter(
192
- (s) => s && !s.startsWith("=====>")
193
- );
194
- }
195
271
 
196
- // src/modules/domains.ts
197
- async function ensureAppDomains(runner, app, domains) {
198
- if (domains === void 0) return;
199
- logAction(app, "Configuring domains");
200
- if (domains === false) {
201
- await runner.run("domains:disable", app);
202
- await runner.run("domains:clear", app);
203
- } else {
204
- await runner.run("domains:enable", app);
205
- await runner.run("domains:set", app, ...domains);
272
+ // src/resources/lifecycle.ts
273
+ var Apps = {
274
+ key: "_app",
275
+ read: async (ctx, target) => {
276
+ return ctx.check("apps:exists", target);
277
+ },
278
+ onChange: async (ctx, target, { after }) => {
279
+ if (after) {
280
+ await ctx.run("apps:create", target);
281
+ } else {
282
+ await ctx.run("apps:destroy", target, "--force");
283
+ }
206
284
  }
207
- logDone();
285
+ };
286
+
287
+ // src/resources/parsers.ts
288
+ function parseReport(raw, namespace) {
289
+ const result = {};
290
+ const prefix = new RegExp(`^${namespace}\\s+`, "i");
291
+ for (const line of raw.split("\n")) {
292
+ if (line.trimStart().startsWith("=====>")) continue;
293
+ const colonIdx = line.indexOf(":");
294
+ if (colonIdx === -1) continue;
295
+ const rawKey = line.slice(0, colonIdx).trim();
296
+ if (!rawKey) continue;
297
+ const value = line.slice(colonIdx + 1).trim();
298
+ const stripped = rawKey.replace(prefix, "");
299
+ const key = stripped.toLowerCase().replace(/\s+/g, "-");
300
+ if (key.startsWith("computed-") || key.startsWith("global-") || key === "last-visited-at") continue;
301
+ if (!value) continue;
302
+ result[key] = value;
303
+ }
304
+ return result;
208
305
  }
209
- async function ensureGlobalDomains(runner, domains) {
210
- logAction("global", "Configuring domains");
211
- if (domains === false) {
212
- await runner.run("domains:clear-global");
213
- } else {
214
- await runner.run("domains:set-global", ...domains);
306
+ function parseBulkReport(raw, namespace) {
307
+ const result = /* @__PURE__ */ new Map();
308
+ const sections = raw.split(/(?=^=====> )/m).filter((s) => s.trim());
309
+ for (const section of sections) {
310
+ const headerEnd = section.indexOf("\n");
311
+ if (headerEnd === -1) continue;
312
+ const header = section.slice(0, headerEnd);
313
+ const match = header.match(/^=====> (.+?)\s+\S+\s+information/);
314
+ if (!match) continue;
315
+ const app = match[1];
316
+ result.set(app, parseReport(section, namespace));
215
317
  }
216
- logDone();
318
+ return result;
217
319
  }
218
- async function exportAppDomains(runner, app) {
219
- const enabledRaw = await runner.query("domains:report", app, "--domains-app-enabled");
220
- if (enabledRaw.trim() === "false") return false;
221
- const raw = await runner.query("domains:report", app, "--domains-app-vhosts");
222
- const vhosts = raw.split("\n").map((s) => s.trim()).filter(Boolean);
223
- if (vhosts.length === 0) return void 0;
224
- return vhosts;
320
+
321
+ // src/resources/lists.ts
322
+ function splitWords(raw) {
323
+ return raw.split(/\s+/).map((s) => s.trim()).filter(Boolean);
324
+ }
325
+ function splitLines(raw) {
326
+ return raw.split("\n").map((s) => s.trim()).filter(Boolean);
225
327
  }
328
+ var Ports = {
329
+ key: "ports",
330
+ read: async (ctx, target) => {
331
+ const raw = await ctx.query("ports:report", target, "--ports-map");
332
+ return splitWords(raw);
333
+ },
334
+ readAll: async (ctx) => {
335
+ const raw = await ctx.query("ports:report");
336
+ const bulk = parseBulkReport(raw, "ports");
337
+ const result = /* @__PURE__ */ new Map();
338
+ for (const [app, report] of bulk) {
339
+ result.set(app, report["map"] ? splitWords(report["map"]) : []);
340
+ }
341
+ return result;
342
+ },
343
+ onChange: async (ctx, target, change) => {
344
+ await ctx.run("ports:set", target, ...change.after);
345
+ }
346
+ };
347
+ var Domains = {
348
+ key: "domains",
349
+ read: async (ctx, target) => {
350
+ const raw = await ctx.query("domains:report", target, "--domains-app-vhosts");
351
+ return splitLines(raw);
352
+ },
353
+ readAll: async (ctx) => {
354
+ const raw = await ctx.query("domains:report");
355
+ const bulk = parseBulkReport(raw, "domains");
356
+ const result = /* @__PURE__ */ new Map();
357
+ for (const [app, report] of bulk) {
358
+ result.set(app, report["app-vhosts"] ? splitWords(report["app-vhosts"]) : []);
359
+ }
360
+ return result;
361
+ },
362
+ onChange: async (ctx, target, { added, removed }) => {
363
+ for (const d of removed) await ctx.run("domains:remove", target, d);
364
+ for (const d of added) await ctx.run("domains:add", target, d);
365
+ }
366
+ };
367
+ var Storage = {
368
+ key: "storage",
369
+ read: async (ctx, target) => {
370
+ const raw = await ctx.query("storage:report", target, "--storage-mounts");
371
+ return splitLines(raw);
372
+ },
373
+ readAll: async (ctx) => {
374
+ const raw = await ctx.query("storage:report");
375
+ const bulk = parseBulkReport(raw, "storage");
376
+ const result = /* @__PURE__ */ new Map();
377
+ for (const [app, report] of bulk) {
378
+ result.set(app, report["mounts"] ? splitLines(report["mounts"]) : []);
379
+ }
380
+ return result;
381
+ },
382
+ onChange: async (ctx, target, { added, removed }) => {
383
+ for (const m of removed) await ctx.run("storage:unmount", target, m);
384
+ for (const m of added) await ctx.run("storage:mount", target, m);
385
+ }
386
+ };
387
+
388
+ // src/resources/properties.ts
389
+ function propertyResource(opts) {
390
+ return {
391
+ key: opts.key,
392
+ async read(ctx, target) {
393
+ const raw = await ctx.query(`${opts.namespace}:report`, target);
394
+ return parseReport(raw, opts.namespace);
395
+ },
396
+ async readAll(ctx) {
397
+ const raw = await ctx.query(`${opts.namespace}:report`);
398
+ return parseBulkReport(raw, opts.namespace);
399
+ },
400
+ async onChange(ctx, target, change) {
401
+ for (const [key, value] of Object.entries({ ...change.added, ...change.modified })) {
402
+ await ctx.run(opts.setCmd, target, key, String(value));
403
+ }
404
+ if (opts.afterChange) {
405
+ for (const cmd of opts.afterChange) {
406
+ await ctx.run(cmd, target);
407
+ }
408
+ }
409
+ }
410
+ };
411
+ }
412
+ var Nginx = propertyResource({
413
+ key: "nginx",
414
+ namespace: "nginx",
415
+ setCmd: "nginx:set",
416
+ afterChange: ["proxy:build-config"]
417
+ });
418
+ var Logs = propertyResource({
419
+ key: "logs",
420
+ namespace: "logs",
421
+ setCmd: "logs:set"
422
+ });
423
+ var Registry = propertyResource({
424
+ key: "registry",
425
+ namespace: "registry",
426
+ setCmd: "registry:set"
427
+ });
428
+ var Scheduler = {
429
+ key: "scheduler",
430
+ async read(ctx, target) {
431
+ const raw = await ctx.query("scheduler:report", target);
432
+ const report = parseReport(raw, "scheduler");
433
+ return report["selected"] ?? "";
434
+ },
435
+ async readAll(ctx) {
436
+ const raw = await ctx.query("scheduler:report");
437
+ const bulk = parseBulkReport(raw, "scheduler");
438
+ const result = /* @__PURE__ */ new Map();
439
+ for (const [app, report] of bulk) {
440
+ result.set(app, report["selected"] ?? "");
441
+ }
442
+ return result;
443
+ },
444
+ async onChange(ctx, target, change) {
445
+ await ctx.run("scheduler:set", target, "selected", change.after);
446
+ }
447
+ };
448
+
449
+ // src/resources/toggle.ts
450
+ var Proxy = {
451
+ key: "proxy",
452
+ read: async (ctx, target) => {
453
+ const raw = await ctx.query("proxy:report", target, "--proxy-enabled");
454
+ return raw.trim() === "true";
455
+ },
456
+ readAll: async (ctx) => {
457
+ const raw = await ctx.query("proxy:report");
458
+ const bulk = parseBulkReport(raw, "proxy");
459
+ const result = /* @__PURE__ */ new Map();
460
+ for (const [app, report] of bulk) {
461
+ result.set(app, report["enabled"] === "true");
462
+ }
463
+ return result;
464
+ },
465
+ onChange: async (ctx, target, { after }) => {
466
+ await ctx.run(after ? "proxy:enable" : "proxy:disable", target);
467
+ }
468
+ };
469
+
470
+ // src/resources/config.ts
471
+ var MANAGED_KEYS_VAR = "DOKKU_COMPOSE_MANAGED_KEYS";
472
+ var Config = {
473
+ key: "env",
474
+ read: async (ctx, target) => {
475
+ const managedRaw = await ctx.query("config:get", target, MANAGED_KEYS_VAR);
476
+ const managedKeys = managedRaw.trim() ? managedRaw.trim().split(",").filter(Boolean) : [];
477
+ const result = {};
478
+ if (managedKeys.length > 0) {
479
+ const raw = await ctx.query("config:export", target, "--format", "shell");
480
+ for (const line of raw.split("\n")) {
481
+ const match = line.match(/^export\s+(\w+)=['"]?(.*?)['"]?$/);
482
+ if (match && managedKeys.includes(match[1])) {
483
+ result[match[1]] = match[2];
484
+ }
485
+ }
486
+ }
487
+ return result;
488
+ },
489
+ onChange: async (ctx, target, change) => {
490
+ const { added, removed, modified } = change;
491
+ if (removed.length > 0) {
492
+ await ctx.run("config:unset", "--no-restart", target, ...removed);
493
+ }
494
+ const toSet = { ...added, ...modified };
495
+ const allDesiredKeys = Object.keys(change.after);
496
+ const managedValue = allDesiredKeys.join(",");
497
+ if (Object.keys(toSet).length > 0 || removed.length > 0) {
498
+ const pairs = Object.entries(change.after).map(([k, v]) => `${k}=${v}`);
499
+ await ctx.run(
500
+ "config:set",
501
+ "--no-restart",
502
+ target,
503
+ ...pairs,
504
+ `${MANAGED_KEYS_VAR}=${managedValue}`
505
+ );
506
+ }
507
+ }
508
+ };
509
+
510
+ // src/resources/certs.ts
511
+ var Certs = {
512
+ key: "ssl",
513
+ read: async (ctx, target) => {
514
+ const raw = await ctx.query("certs:report", target, "--ssl-enabled");
515
+ return raw.trim() === "true";
516
+ },
517
+ readAll: async (ctx) => {
518
+ const raw = await ctx.query("certs:report");
519
+ const bulk = parseBulkReport(raw, "ssl");
520
+ const result = /* @__PURE__ */ new Map();
521
+ for (const [app, report] of bulk) {
522
+ result.set(app, report["enabled"] === "true");
523
+ }
524
+ return result;
525
+ },
526
+ onChange: async (ctx, target, { before, after }) => {
527
+ if (after === false && before) {
528
+ await ctx.run("certs:remove", target);
529
+ }
530
+ if (after && typeof after === "object") {
531
+ await ctx.run("certs:add", target, after.certfile, after.keyfile);
532
+ }
533
+ }
534
+ };
535
+
536
+ // src/resources/builder.ts
537
+ var Builder = {
538
+ key: "build",
539
+ forceApply: true,
540
+ read: async () => ({}),
541
+ onChange: async (ctx, target, { after }) => {
542
+ if (after.dockerfile)
543
+ await ctx.run("builder-dockerfile:set", target, "dockerfile-path", after.dockerfile);
544
+ if (after.app_json)
545
+ await ctx.run("app-json:set", target, "appjson-path", after.app_json);
546
+ if (after.context)
547
+ await ctx.run("builder:set", target, "build-dir", after.context);
548
+ if (after.args) {
549
+ for (const [key, value] of Object.entries(after.args)) {
550
+ await ctx.run("docker-options:add", target, "build", `--build-arg ${key}=${value}`);
551
+ }
552
+ }
553
+ }
554
+ };
555
+
556
+ // src/resources/docker-options.ts
557
+ var DockerOptions = {
558
+ key: "docker_options",
559
+ forceApply: true,
560
+ read: async () => ({}),
561
+ onChange: async (ctx, target, { after }) => {
562
+ for (const phase of ["build", "deploy", "run"]) {
563
+ const opts = after[phase];
564
+ if (!opts || opts.length === 0) continue;
565
+ await ctx.run("docker-options:clear", target, phase);
566
+ for (const opt of opts) {
567
+ await ctx.run("docker-options:add", target, phase, opt);
568
+ }
569
+ }
570
+ }
571
+ };
572
+
573
+ // src/resources/git.ts
574
+ var Git = {
575
+ key: "git",
576
+ read: async (ctx, target) => {
577
+ const report = await ctx.query("git:report", target, "--git-deploy-branch");
578
+ return { deploy_branch: report.trim() || void 0 };
579
+ },
580
+ readAll: async (ctx) => {
581
+ const raw = await ctx.query("git:report");
582
+ const bulk = parseBulkReport(raw, "git");
583
+ const result = /* @__PURE__ */ new Map();
584
+ for (const [app, report] of bulk) {
585
+ result.set(app, { deploy_branch: report["deploy-branch"] || void 0 });
586
+ }
587
+ return result;
588
+ },
589
+ onChange: async (ctx, target, { after }) => {
590
+ if (after.deploy_branch) {
591
+ await ctx.run("git:set", target, "deploy-branch", after.deploy_branch);
592
+ }
593
+ }
594
+ };
595
+
596
+ // src/resources/checks.ts
597
+ var Checks = {
598
+ key: "checks",
599
+ forceApply: true,
600
+ read: async () => ({}),
601
+ onChange: async (ctx, target, { after }) => {
602
+ if (after === false) {
603
+ await ctx.run("checks:disable", target);
604
+ return;
605
+ }
606
+ if (after.disabled && after.disabled.length > 0) {
607
+ await ctx.run("checks:disable", target, ...after.disabled);
608
+ }
609
+ if (after.skipped && after.skipped.length > 0) {
610
+ await ctx.run("checks:skip", target, ...after.skipped);
611
+ }
612
+ for (const [key, value] of Object.entries(after)) {
613
+ if (key === "disabled" || key === "skipped") continue;
614
+ await ctx.run("checks:set", target, key, String(value));
615
+ }
616
+ }
617
+ };
618
+
619
+ // src/resources/network.ts
620
+ var Networks = {
621
+ key: "networks",
622
+ read: async (ctx, target) => {
623
+ const raw = await ctx.query("network:report", target, "--network-attach-post-deploy");
624
+ return raw.trim() ? raw.trim().split(/\s+/) : [];
625
+ },
626
+ readAll: async (ctx) => {
627
+ const raw = await ctx.query("network:report");
628
+ const bulk = parseBulkReport(raw, "network");
629
+ const result = /* @__PURE__ */ new Map();
630
+ for (const [app, report] of bulk) {
631
+ result.set(app, report["attach-post-deploy"] ? report["attach-post-deploy"].split(/\s+/) : []);
632
+ }
633
+ return result;
634
+ },
635
+ onChange: async (ctx, target, { after }) => {
636
+ await ctx.run("network:set", target, "attach-post-deploy", ...after);
637
+ }
638
+ };
639
+ var NetworkProps = {
640
+ key: "network",
641
+ forceApply: true,
642
+ read: async () => ({}),
643
+ onChange: async (ctx, target, { after }) => {
644
+ if (after.attach_post_create !== void 0 && after.attach_post_create !== false) {
645
+ const nets = Array.isArray(after.attach_post_create) ? after.attach_post_create : [after.attach_post_create];
646
+ await ctx.run("network:set", target, "attach-post-create", ...nets);
647
+ }
648
+ if (after.initial_network !== void 0 && after.initial_network !== false) {
649
+ await ctx.run("network:set", target, "initial-network", after.initial_network);
650
+ }
651
+ if (after.bind_all_interfaces !== void 0) {
652
+ await ctx.run("network:set", target, "bind-all-interfaces", String(after.bind_all_interfaces));
653
+ }
654
+ if (after.tld !== void 0 && after.tld !== false) {
655
+ await ctx.run("network:set", target, "tld", after.tld);
656
+ }
657
+ }
658
+ };
226
659
 
227
660
  // src/modules/plugins.ts
228
- async function ensurePlugins(runner, plugins) {
229
- const listOutput = await runner.query("plugin:list");
661
+ async function ensurePlugins(ctx, plugins) {
662
+ const listOutput = await ctx.query("plugin:list");
230
663
  const installedNames = new Set(
231
664
  listOutput.split("\n").map((line) => line.trim().split(/\s+/)[0]).filter(Boolean)
232
665
  );
@@ -236,514 +669,311 @@ async function ensurePlugins(runner, plugins) {
236
669
  logSkip();
237
670
  continue;
238
671
  }
239
- await runner.run("plugin:install", config.url, "--name", name);
672
+ await ctx.run("plugin:install", config.url, "--name", name);
240
673
  logDone();
241
674
  }
242
675
  }
243
676
 
244
677
  // src/modules/network.ts
245
- async function ensureNetworks(runner, networks) {
678
+ async function ensureNetworks(ctx, networks) {
246
679
  for (const net of networks) {
247
680
  logAction("network", `Creating ${net}`);
248
- const exists = await runner.check("network:exists", net);
681
+ const exists = await ctx.check("network:exists", net);
249
682
  if (exists) {
250
683
  logSkip();
251
684
  continue;
252
685
  }
253
- await runner.run("network:create", net);
686
+ await ctx.run("network:create", net);
254
687
  logDone();
255
688
  }
256
689
  }
257
- async function ensureAppNetworks(runner, app, networks) {
258
- if (!networks || networks.length === 0) return;
259
- logAction(app, "Setting networks");
260
- await runner.run("network:set", app, "attach-post-deploy", ...networks);
261
- logDone();
262
- }
263
- async function ensureAppNetwork(runner, app, network) {
264
- if (!network) return;
265
- if (network.attach_post_create !== void 0 && network.attach_post_create !== false) {
266
- const nets = Array.isArray(network.attach_post_create) ? network.attach_post_create : [network.attach_post_create];
267
- await runner.run("network:set", app, "attach-post-create", ...nets);
268
- }
269
- if (network.initial_network !== void 0 && network.initial_network !== false) {
270
- await runner.run("network:set", app, "initial-network", network.initial_network);
271
- }
272
- if (network.bind_all_interfaces !== void 0) {
273
- await runner.run("network:set", app, "bind-all-interfaces", String(network.bind_all_interfaces));
274
- }
275
- if (network.tld !== void 0 && network.tld !== false) {
276
- await runner.run("network:set", app, "tld", network.tld);
277
- }
278
- }
279
690
  var DOCKER_BUILTIN_NETWORKS = /* @__PURE__ */ new Set(["bridge", "host", "none"]);
280
- async function exportNetworks(runner) {
281
- const output = await runner.query("network:list");
691
+ async function exportNetworks(ctx) {
692
+ const output = await ctx.query("network:list");
282
693
  return output.split("\n").map((s) => s.trim()).filter((s) => s && !s.startsWith("=====>") && !DOCKER_BUILTIN_NETWORKS.has(s));
283
694
  }
284
- async function exportAppNetwork(runner, app) {
285
- const output = await runner.query("network:report", app);
286
- if (!output) return void 0;
287
- const result = {};
288
- const lines = output.split("\n");
289
- for (const line of lines) {
290
- const [key, ...valueParts] = line.split(":").map((s) => s.trim());
291
- const value = valueParts.join(":").trim();
292
- if (!key || !value) continue;
293
- if (key === "Network attach post deploy") {
294
- result.networks = value.split(" ").filter(Boolean);
295
- }
296
- }
297
- return Object.keys(result).length > 0 ? result : void 0;
298
- }
299
695
 
300
696
  // src/modules/services.ts
301
- async function ensureServices(runner, services) {
697
+ import { createHash as createHash2 } from "crypto";
698
+ function backupHashKey(serviceName) {
699
+ return "DOKKU_COMPOSE_BACKUP_HASH_" + serviceName.toUpperCase().replace(/-/g, "_");
700
+ }
701
+ function computeBackupHash(backup) {
702
+ return createHash2("sha256").update(JSON.stringify(backup)).digest("hex");
703
+ }
704
+ async function ensureServices(ctx, services) {
302
705
  for (const [name, config] of Object.entries(services)) {
303
706
  logAction("services", `Ensuring ${name}`);
304
- const exists = await runner.check(`${config.plugin}:exists`, name);
707
+ const exists = await ctx.check(`${config.plugin}:exists`, name);
305
708
  if (exists) {
306
709
  logSkip();
307
710
  continue;
308
711
  }
309
- await runner.run(`${config.plugin}:create`, name);
712
+ const args = [`${config.plugin}:create`, name];
713
+ if (config.image) args.push("--image", config.image);
714
+ if (config.version) args.push("--image-version", config.version);
715
+ await ctx.run(...args);
716
+ logDone();
717
+ }
718
+ }
719
+ async function ensureServiceBackups(ctx, services) {
720
+ for (const [name, config] of Object.entries(services)) {
721
+ if (!config.backup) continue;
722
+ logAction("services", `Configuring backup for ${name}`);
723
+ const hashKey = backupHashKey(name);
724
+ const desiredHash = computeBackupHash(config.backup);
725
+ const storedHash = await ctx.query("config:get", "--global", hashKey);
726
+ if (storedHash === desiredHash) {
727
+ logSkip();
728
+ continue;
729
+ }
730
+ const { schedule, bucket, auth } = config.backup;
731
+ await ctx.run(`${config.plugin}:backup-deauth`, name);
732
+ await ctx.run(
733
+ `${config.plugin}:backup-auth`,
734
+ name,
735
+ auth.access_key_id,
736
+ auth.secret_access_key,
737
+ auth.region,
738
+ auth.signature_version,
739
+ auth.endpoint
740
+ );
741
+ await ctx.run(`${config.plugin}:backup-schedule`, name, schedule, bucket);
742
+ await ctx.run("config:set", "--global", `${hashKey}=${desiredHash}`);
310
743
  logDone();
311
744
  }
312
745
  }
313
- async function ensureAppLinks(runner, app, desiredLinks, allServices) {
746
+ async function ensureAppLinks(ctx, app, desiredLinks, allServices) {
314
747
  const desiredSet = new Set(desiredLinks);
315
748
  for (const [serviceName, serviceConfig] of Object.entries(allServices)) {
316
- const isLinked = await runner.check(`${serviceConfig.plugin}:linked`, serviceName, app);
749
+ const isLinked = await ctx.check(`${serviceConfig.plugin}:linked`, serviceName, app);
317
750
  const isDesired = desiredSet.has(serviceName);
318
751
  if (isDesired && !isLinked) {
319
752
  logAction(app, `Linking ${serviceName}`);
320
- await runner.run(`${serviceConfig.plugin}:link`, serviceName, app, "--no-restart");
753
+ await ctx.run(`${serviceConfig.plugin}:link`, serviceName, app, "--no-restart");
321
754
  logDone();
322
755
  } else if (!isDesired && isLinked) {
323
756
  logAction(app, `Unlinking ${serviceName}`);
324
- await runner.run(`${serviceConfig.plugin}:unlink`, serviceName, app, "--no-restart");
757
+ await ctx.run(`${serviceConfig.plugin}:unlink`, serviceName, app, "--no-restart");
325
758
  logDone();
326
759
  }
327
760
  }
328
761
  }
329
- async function destroyAppLinks(runner, app, links, allServices) {
762
+ async function destroyAppLinks(ctx, app, links, allServices) {
330
763
  for (const serviceName of links) {
331
764
  const config = allServices[serviceName];
332
765
  if (!config) continue;
333
- const isLinked = await runner.check(`${config.plugin}:linked`, serviceName, app);
766
+ const isLinked = await ctx.check(`${config.plugin}:linked`, serviceName, app);
334
767
  if (isLinked) {
335
- await runner.run(`${config.plugin}:unlink`, serviceName, app, "--no-restart");
768
+ await ctx.run(`${config.plugin}:unlink`, serviceName, app, "--no-restart");
336
769
  }
337
770
  }
338
771
  }
339
- async function destroyServices(runner, services) {
772
+ async function destroyServices(ctx, services) {
340
773
  for (const [name, config] of Object.entries(services)) {
341
774
  logAction("services", `Destroying ${name}`);
342
- const exists = await runner.check(`${config.plugin}:exists`, name);
775
+ const exists = await ctx.check(`${config.plugin}:exists`, name);
343
776
  if (!exists) {
344
777
  logSkip();
345
778
  continue;
346
779
  }
347
- await runner.run(`${config.plugin}:destroy`, name, "--force");
780
+ await ctx.run(`${config.plugin}:destroy`, name, "--force");
348
781
  logDone();
349
782
  }
350
783
  }
351
- async function exportServices(_runner) {
784
+ async function exportServices(_ctx) {
352
785
  return {};
353
786
  }
354
- async function exportAppLinks(runner, app, services) {
787
+ async function exportAppLinks(ctx, app, services) {
355
788
  const linked = [];
356
789
  for (const [serviceName, config] of Object.entries(services)) {
357
- const isLinked = await runner.check(`${config.plugin}:linked`, serviceName, app);
790
+ const isLinked = await ctx.check(`${config.plugin}:linked`, serviceName, app);
358
791
  if (isLinked) linked.push(serviceName);
359
792
  }
360
793
  return linked;
361
794
  }
362
795
 
363
- // src/modules/proxy.ts
364
- async function ensureAppProxy(runner, app, enabled) {
365
- logAction(app, `Setting proxy enabled=${enabled}`);
366
- const currentRaw = await runner.query("proxy:report", app, "--proxy-enabled");
367
- const current = currentRaw.trim() === "true";
368
- if (current === enabled) {
369
- logSkip();
370
- return;
371
- }
372
- if (enabled) {
373
- await runner.run("proxy:enable", app);
796
+ // src/modules/domains.ts
797
+ async function ensureGlobalDomains(ctx, domains) {
798
+ logAction("global", "Configuring domains");
799
+ if (domains === false) {
800
+ await ctx.run("domains:clear-global");
374
801
  } else {
375
- await runner.run("proxy:disable", app);
802
+ await ctx.run("domains:set-global", ...domains);
376
803
  }
377
804
  logDone();
378
805
  }
379
- async function exportAppProxy(runner, app) {
380
- const raw = await runner.query("proxy:report", app, "--proxy-enabled");
381
- const enabled = raw.trim() === "true";
382
- if (enabled) return void 0;
383
- return { enabled };
384
- }
385
806
 
386
- // src/modules/ports.ts
387
- async function ensureAppPorts(runner, app, ports) {
388
- logAction(app, "Configuring ports");
389
- const currentRaw = await runner.query("ports:report", app, "--ports-map");
390
- const current = currentRaw.split(/\s+/).map((s) => s.trim()).filter(Boolean).sort();
391
- const desired = [...ports].sort();
392
- if (JSON.stringify(current) === JSON.stringify(desired)) {
393
- logSkip();
394
- return;
395
- }
396
- await runner.run("ports:set", app, ...ports);
397
- logDone();
398
- }
399
- async function exportAppPorts(runner, app) {
400
- const raw = await runner.query("ports:report", app, "--ports-map");
401
- const ports = raw.split(/\s+/).map((s) => s.trim()).filter(Boolean);
402
- if (ports.length === 0) return void 0;
403
- return ports;
807
+ // src/modules/config.ts
808
+ async function ensureGlobalConfig(ctx, env) {
809
+ if (env === false) return;
810
+ const pairs = Object.entries(env).map(([k, v]) => `${k}=${v}`);
811
+ await ctx.run("config:set", "--global", ...pairs);
404
812
  }
405
813
 
406
- // src/modules/certs.ts
407
- async function ensureAppCerts(runner, app, ssl) {
408
- logAction(app, "Configuring SSL");
409
- const enabledRaw = await runner.query("certs:report", app, "--ssl-enabled");
410
- const enabled = enabledRaw.trim() === "true";
411
- if (ssl === false) {
412
- if (!enabled) {
413
- logSkip();
414
- return;
415
- }
416
- await runner.run("certs:remove", app);
417
- logDone();
418
- return;
419
- }
420
- if (ssl === true) {
421
- if (enabled) {
422
- logSkip();
423
- return;
424
- }
425
- logSkip();
426
- return;
427
- }
428
- if (enabled) {
429
- logSkip();
430
- return;
431
- }
432
- await runner.run("certs:add", app, ssl.certfile, ssl.keyfile);
433
- logDone();
434
- }
435
- async function exportAppCerts(runner, app) {
436
- const raw = await runner.query("certs:report", app, "--ssl-enabled");
437
- const enabled = raw.trim() === "true";
438
- if (!enabled) return void 0;
439
- return true;
440
- }
441
-
442
- // src/modules/storage.ts
443
- async function ensureAppStorage(runner, app, storage) {
444
- logAction(app, "Configuring storage");
445
- const currentRaw = await runner.query("storage:report", app, "--storage-mounts");
446
- const current = currentRaw.split("\n").map((s) => s.trim()).filter(Boolean);
447
- const desired = new Set(storage);
448
- const currentSet = new Set(current);
449
- const toUnmount = current.filter((m) => !desired.has(m));
450
- const toMount = storage.filter((m) => !currentSet.has(m));
451
- if (toUnmount.length === 0 && toMount.length === 0) {
452
- logSkip();
453
- return;
454
- }
455
- for (const mount of toUnmount) {
456
- await runner.run("storage:unmount", app, mount);
457
- }
458
- for (const mount of toMount) {
459
- await runner.run("storage:mount", app, mount);
814
+ // src/modules/logs.ts
815
+ async function ensureGlobalLogs(ctx, logs) {
816
+ for (const [key, value] of Object.entries(logs)) {
817
+ await ctx.run("logs:set", "--global", key, String(value));
460
818
  }
461
- logDone();
462
- }
463
- async function exportAppStorage(runner, app) {
464
- const raw = await runner.query("storage:report", app, "--storage-mounts");
465
- const mounts = raw.split("\n").map((s) => s.trim()).filter(Boolean);
466
- if (mounts.length === 0) return void 0;
467
- return mounts;
468
819
  }
469
820
 
470
821
  // src/modules/nginx.ts
471
- async function ensureAppNginx(runner, app, nginx) {
822
+ async function ensureGlobalNginx(ctx, nginx) {
472
823
  for (const [key, value] of Object.entries(nginx)) {
473
- await runner.run("nginx:set", app, key, String(value));
824
+ await ctx.run("nginx:set", "--global", key, String(value));
474
825
  }
475
826
  }
476
- async function ensureGlobalNginx(runner, nginx) {
477
- for (const [key, value] of Object.entries(nginx)) {
478
- await runner.run("nginx:set", "--global", key, String(value));
479
- }
480
- }
481
- async function exportAppNginx(runner, app) {
482
- const raw = await runner.query("nginx:report", app);
483
- if (!raw) return void 0;
484
- const result = {};
485
- for (const line of raw.split("\n")) {
486
- const match = line.match(/^\s*Nginx\s+(.+?):\s*(.+?)\s*$/);
487
- if (match) {
488
- const key = match[1].toLowerCase().replace(/\s+/g, "-");
489
- if (key.startsWith("computed-") || key.startsWith("global-") || key === "last-visited-at") continue;
490
- const value = match[2].trim();
491
- if (!value) continue;
492
- result[key] = value;
493
- }
494
- }
495
- return Object.keys(result).length > 0 ? result : void 0;
496
- }
497
827
 
498
- // src/modules/checks.ts
499
- async function ensureAppChecks(runner, app, checks) {
500
- logAction(app, "Configuring checks");
501
- if (checks === false) {
502
- await runner.run("checks:disable", app);
503
- logDone();
504
- return;
505
- }
506
- if (checks.disabled && checks.disabled.length > 0) {
507
- await runner.run("checks:disable", app, ...checks.disabled);
508
- }
509
- if (checks.skipped && checks.skipped.length > 0) {
510
- await runner.run("checks:skip", app, ...checks.skipped);
511
- }
512
- for (const [key, value] of Object.entries(checks)) {
513
- if (key === "disabled" || key === "skipped") continue;
514
- await runner.run("checks:set", app, key, String(value));
515
- }
516
- logDone();
517
- }
518
- async function exportAppChecks(runner, app) {
519
- const raw = await runner.query("checks:report", app);
520
- if (!raw) return void 0;
521
- return void 0;
522
- }
523
-
524
- // src/modules/logs.ts
525
- async function ensureAppLogs(runner, app, logs) {
526
- for (const [key, value] of Object.entries(logs)) {
527
- await runner.run("logs:set", app, key, String(value));
528
- }
529
- }
530
- async function ensureGlobalLogs(runner, logs) {
531
- for (const [key, value] of Object.entries(logs)) {
532
- await runner.run("logs:set", "--global", key, String(value));
533
- }
534
- }
535
- async function exportAppLogs(runner, app) {
536
- const raw = await runner.query("logs:report", app);
537
- if (!raw) return void 0;
538
- return void 0;
539
- }
540
-
541
- // src/modules/registry.ts
542
- async function ensureAppRegistry(runner, app, registry) {
543
- for (const [key, value] of Object.entries(registry)) {
544
- await runner.run("registry:set", app, key, String(value));
828
+ // src/commands/up.ts
829
+ async function runUp(ctx, config, appFilter) {
830
+ const apps = appFilter.length > 0 ? appFilter : Object.keys(config.apps);
831
+ if (config.plugins) await ensurePlugins(ctx, config.plugins);
832
+ if (config.domains !== void 0) await ensureGlobalDomains(ctx, config.domains);
833
+ if (config.env !== void 0) await ensureGlobalConfig(ctx, config.env);
834
+ if (config.logs !== void 0) await ensureGlobalLogs(ctx, config.logs);
835
+ if (config.nginx !== void 0) await ensureGlobalNginx(ctx, config.nginx);
836
+ if (config.networks) await ensureNetworks(ctx, config.networks);
837
+ if (config.services) await ensureServices(ctx, config.services);
838
+ if (config.services) await ensureServiceBackups(ctx, config.services);
839
+ for (const app of apps) {
840
+ const appConfig = config.apps[app];
841
+ if (!appConfig) continue;
842
+ await reconcile(Apps, ctx, app, true);
843
+ await reconcile(Domains, ctx, app, appConfig.domains);
844
+ await reconcile(Networks, ctx, app, appConfig.networks);
845
+ await reconcile(NetworkProps, ctx, app, appConfig.network);
846
+ await reconcile(Proxy, ctx, app, appConfig.proxy?.enabled);
847
+ await reconcile(Ports, ctx, app, appConfig.ports);
848
+ if (config.services) {
849
+ await ensureAppLinks(ctx, app, appConfig.links ?? [], config.services);
850
+ }
851
+ await reconcile(Certs, ctx, app, appConfig.ssl);
852
+ await reconcile(Storage, ctx, app, appConfig.storage);
853
+ await reconcile(Nginx, ctx, app, appConfig.nginx);
854
+ await reconcile(Checks, ctx, app, appConfig.checks);
855
+ await reconcile(Logs, ctx, app, appConfig.logs);
856
+ await reconcile(Registry, ctx, app, appConfig.registry);
857
+ await reconcile(Scheduler, ctx, app, appConfig.scheduler);
858
+ await reconcile(Config, ctx, app, appConfig.env);
859
+ await reconcile(Builder, ctx, app, appConfig.build);
860
+ await reconcile(Git, ctx, app, appConfig.git ?? config.git);
861
+ await reconcile(DockerOptions, ctx, app, appConfig.docker_options);
545
862
  }
546
863
  }
547
- async function exportAppRegistry(runner, app) {
548
- const raw = await runner.query("registry:report", app);
549
- if (!raw) return void 0;
550
- return void 0;
551
- }
552
864
 
553
- // src/modules/scheduler.ts
554
- async function ensureAppScheduler(runner, app, scheduler) {
555
- logAction(app, `Setting scheduler to ${scheduler}`);
556
- const current = await runner.query("scheduler:report", app, "--scheduler-selected");
557
- if (current.trim() === scheduler) {
865
+ // src/modules/apps.ts
866
+ async function destroyApp(ctx, app) {
867
+ const exists = await ctx.check("apps:exists", app);
868
+ logAction(app, "Destroying app");
869
+ if (!exists) {
558
870
  logSkip();
559
871
  return;
560
872
  }
561
- await runner.run("scheduler:set", app, "selected", scheduler);
873
+ await ctx.run("apps:destroy", app, "--force");
562
874
  logDone();
563
875
  }
564
- async function exportAppScheduler(runner, app) {
565
- const raw = await runner.query("scheduler:report", app, "--scheduler-selected");
566
- const scheduler = raw.trim();
567
- if (!scheduler || scheduler === "docker-local") return void 0;
568
- return scheduler;
569
- }
570
-
571
- // src/modules/config.ts
572
- var MANAGED_KEYS_VAR = "DOKKU_COMPOSE_MANAGED_KEYS";
573
- async function ensureAppConfig(runner, app, env) {
574
- logAction(app, "Configuring env vars");
575
- if (env === false) {
576
- logSkip();
577
- return;
578
- }
579
- const prevManagedRaw = await runner.query("config:get", app, MANAGED_KEYS_VAR);
580
- const prevManaged = prevManagedRaw.trim() ? prevManagedRaw.trim().split(",").filter(Boolean) : [];
581
- const desiredKeys = Object.keys(env);
582
- const toUnset = prevManaged.filter((k) => !desiredKeys.includes(k));
583
- if (toUnset.length > 0) {
584
- await runner.run("config:unset", "--no-restart", app, ...toUnset);
585
- }
586
- const pairs = Object.entries(env).map(([k, v]) => `${k}=${v}`);
587
- const newManagedKeys = desiredKeys.join(",");
588
- await runner.run(
589
- "config:set",
590
- "--no-restart",
591
- app,
592
- ...pairs,
593
- `${MANAGED_KEYS_VAR}=${newManagedKeys}`
876
+ async function exportApps(ctx) {
877
+ const output = await ctx.query("apps:list");
878
+ return output.split("\n").map((s) => s.trim()).filter(
879
+ (s) => s && !s.startsWith("=====>")
594
880
  );
595
- logDone();
596
- }
597
- async function ensureGlobalConfig(runner, env) {
598
- if (env === false) return;
599
- const pairs = Object.entries(env).map(([k, v]) => `${k}=${v}`);
600
- await runner.run("config:set", "--global", ...pairs);
601
- }
602
- async function exportAppConfig(runner, app) {
603
- const raw = await runner.query("config:export", app, "--format", "shell");
604
- if (!raw) return void 0;
605
- const result = {};
606
- for (const line of raw.split("\n")) {
607
- const match = line.match(/^export\s+(\w+)=['"]?(.*?)['"]?$/);
608
- if (match && match[1] !== MANAGED_KEYS_VAR) {
609
- result[match[1]] = match[2];
610
- }
611
- }
612
- return Object.keys(result).length > 0 ? result : void 0;
613
- }
614
-
615
- // src/modules/builder.ts
616
- async function ensureAppBuilder(runner, app, build) {
617
- if (build.dockerfile) {
618
- await runner.run("builder-dockerfile:set", app, "dockerfile-path", build.dockerfile);
619
- }
620
- if (build.app_json) {
621
- await runner.run("app-json:set", app, "appjson-path", build.app_json);
622
- }
623
- if (build.context) {
624
- await runner.run("builder:set", app, "build-dir", build.context);
625
- }
626
- if (build.args && Object.keys(build.args).length > 0) {
627
- for (const [key, value] of Object.entries(build.args)) {
628
- await runner.run("docker-options:add", app, "build", `--build-arg ${key}=${value}`);
629
- }
630
- }
631
- }
632
-
633
- // src/modules/docker-options.ts
634
- async function ensureAppDockerOptions(runner, app, options) {
635
- const phases = ["build", "deploy", "run"];
636
- for (const phase of phases) {
637
- const opts = options[phase];
638
- if (!opts || opts.length === 0) continue;
639
- await runner.run("docker-options:clear", app, phase);
640
- for (const opt of opts) {
641
- await runner.run("docker-options:add", app, phase, opt);
642
- }
643
- }
644
- }
645
-
646
- // src/commands/up.ts
647
- async function runUp(runner, config, appFilter) {
648
- const apps = appFilter.length > 0 ? appFilter : Object.keys(config.apps);
649
- if (config.plugins) await ensurePlugins(runner, config.plugins);
650
- if (config.domains !== void 0) await ensureGlobalDomains(runner, config.domains);
651
- if (config.env !== void 0) await ensureGlobalConfig(runner, config.env);
652
- if (config.logs !== void 0) await ensureGlobalLogs(runner, config.logs);
653
- if (config.nginx !== void 0) await ensureGlobalNginx(runner, config.nginx);
654
- if (config.networks) await ensureNetworks(runner, config.networks);
655
- if (config.services) await ensureServices(runner, config.services);
656
- for (const app of apps) {
657
- const appConfig = config.apps[app];
658
- if (!appConfig) continue;
659
- await ensureApp(runner, app);
660
- await ensureAppDomains(runner, app, appConfig.domains);
661
- if (config.services) await ensureAppLinks(runner, app, appConfig.links ?? [], config.services);
662
- await ensureAppNetworks(runner, app, appConfig.networks);
663
- await ensureAppNetwork(runner, app, appConfig.network);
664
- if (appConfig.proxy) await ensureAppProxy(runner, app, appConfig.proxy.enabled);
665
- if (appConfig.ports) await ensureAppPorts(runner, app, appConfig.ports);
666
- if (appConfig.ssl !== void 0) await ensureAppCerts(runner, app, appConfig.ssl);
667
- if (appConfig.storage) await ensureAppStorage(runner, app, appConfig.storage);
668
- if (appConfig.nginx) await ensureAppNginx(runner, app, appConfig.nginx);
669
- if (appConfig.checks !== void 0) await ensureAppChecks(runner, app, appConfig.checks);
670
- if (appConfig.logs) await ensureAppLogs(runner, app, appConfig.logs);
671
- if (appConfig.registry) await ensureAppRegistry(runner, app, appConfig.registry);
672
- if (appConfig.scheduler) await ensureAppScheduler(runner, app, appConfig.scheduler);
673
- if (appConfig.env !== void 0) await ensureAppConfig(runner, app, appConfig.env);
674
- if (appConfig.build) await ensureAppBuilder(runner, app, appConfig.build);
675
- if (appConfig.docker_options) await ensureAppDockerOptions(runner, app, appConfig.docker_options);
676
- }
677
881
  }
678
882
 
679
883
  // src/commands/down.ts
680
- async function runDown(runner, config, appFilter, opts) {
884
+ async function runDown(ctx, config, appFilter, opts) {
681
885
  const apps = appFilter.length > 0 ? appFilter : Object.keys(config.apps);
682
886
  for (const app of apps) {
683
887
  const appConfig = config.apps[app];
684
888
  if (!appConfig) continue;
685
889
  if (config.services && appConfig.links) {
686
- await destroyAppLinks(runner, app, appConfig.links, config.services);
890
+ await destroyAppLinks(ctx, app, appConfig.links, config.services);
687
891
  }
688
- await destroyApp(runner, app);
892
+ await destroyApp(ctx, app);
689
893
  }
690
894
  if (config.services) {
691
- await destroyServices(runner, config.services);
895
+ await destroyServices(ctx, config.services);
692
896
  }
693
897
  if (config.networks) {
694
898
  for (const net of config.networks) {
695
899
  logAction("network", `Destroying ${net}`);
696
- const exists = await runner.check("network:exists", net);
900
+ const exists = await ctx.check("network:exists", net);
697
901
  if (!exists) {
698
902
  logSkip();
699
903
  continue;
700
904
  }
701
- await runner.run("network:destroy", net, "--force");
905
+ await ctx.run("network:destroy", net, "--force");
702
906
  logDone();
703
907
  }
704
908
  }
705
909
  }
706
910
 
911
+ // src/resources/index.ts
912
+ var NETWORKING_RESOURCES = [
913
+ Domains,
914
+ Networks,
915
+ NetworkProps,
916
+ Proxy,
917
+ Ports
918
+ ];
919
+ var CONFIG_RESOURCES = [
920
+ Certs,
921
+ Storage,
922
+ Nginx,
923
+ Checks,
924
+ Logs,
925
+ Registry,
926
+ Scheduler,
927
+ Config
928
+ ];
929
+ var BUILD_RESOURCES = [
930
+ Builder,
931
+ Git,
932
+ DockerOptions
933
+ ];
934
+ var ALL_APP_RESOURCES = [
935
+ ...NETWORKING_RESOURCES,
936
+ ...CONFIG_RESOURCES,
937
+ ...BUILD_RESOURCES
938
+ ];
939
+
707
940
  // src/commands/export.ts
708
- async function runExport(runner, opts) {
941
+ async function runExport(ctx, opts) {
709
942
  const config = { apps: {} };
710
- const versionOutput = await runner.query("version");
943
+ const versionOutput = await ctx.query("version");
711
944
  const versionMatch = versionOutput.match(/(\d+\.\d+\.\d+)/);
712
945
  if (versionMatch) config.dokku = { version: versionMatch[1] };
713
- const apps = opts.appFilter?.length ? opts.appFilter : await exportApps(runner);
714
- const networks = await exportNetworks(runner);
946
+ const apps = opts.appFilter?.length ? opts.appFilter : await exportApps(ctx);
947
+ const networks = await exportNetworks(ctx);
715
948
  if (networks.length > 0) config.networks = networks;
716
- const services = await exportServices(runner);
949
+ const services = await exportServices(ctx);
717
950
  if (Object.keys(services).length > 0) config.services = services;
951
+ const prefetched = /* @__PURE__ */ new Map();
952
+ await Promise.all(
953
+ ALL_APP_RESOURCES.filter((r) => !r.forceApply && !r.key.startsWith("_") && r.readAll).map(async (r) => {
954
+ prefetched.set(r.key, await r.readAll(ctx));
955
+ })
956
+ );
718
957
  for (const app of apps) {
719
958
  const appConfig = {};
720
- const domains = await exportAppDomains(runner, app);
721
- if (domains !== void 0) appConfig.domains = domains;
722
- const links = await exportAppLinks(runner, app, services);
723
- if (links.length > 0) appConfig.links = links;
724
- const ports = await exportAppPorts(runner, app);
725
- if (ports?.length) appConfig.ports = ports;
726
- const proxy = await exportAppProxy(runner, app);
727
- if (proxy !== void 0) appConfig.proxy = proxy;
728
- const ssl = await exportAppCerts(runner, app);
729
- if (ssl !== void 0) appConfig.ssl = ssl;
730
- const storage = await exportAppStorage(runner, app);
731
- if (storage?.length) appConfig.storage = storage;
732
- const nginx = await exportAppNginx(runner, app);
733
- if (nginx && Object.keys(nginx).length) appConfig.nginx = nginx;
734
- const checks = await exportAppChecks(runner, app);
735
- if (checks !== void 0) appConfig.checks = checks;
736
- const logs = await exportAppLogs(runner, app);
737
- if (logs && Object.keys(logs).length) appConfig.logs = logs;
738
- const registry = await exportAppRegistry(runner, app);
739
- if (registry && Object.keys(registry).length) appConfig.registry = registry;
740
- const scheduler = await exportAppScheduler(runner, app);
741
- if (scheduler) appConfig.scheduler = scheduler;
742
- const networkCfg = await exportAppNetwork(runner, app);
743
- if (networkCfg?.networks?.length) appConfig.networks = networkCfg.networks;
744
- if (networkCfg?.network) appConfig.network = networkCfg.network;
745
- const env = await exportAppConfig(runner, app);
746
- if (env && Object.keys(env).length) appConfig.env = env;
959
+ for (const resource of ALL_APP_RESOURCES) {
960
+ if (resource.key.startsWith("_")) continue;
961
+ if (resource.forceApply) continue;
962
+ const bulk = prefetched.get(resource.key);
963
+ const value = bulk ? bulk.get(app) : await resource.read(ctx, app);
964
+ if (value === void 0 || value === null || value === "") continue;
965
+ if (Array.isArray(value) && value.length === 0) continue;
966
+ if (typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0) continue;
967
+ if (resource.key === "proxy") {
968
+ appConfig.proxy = { enabled: value };
969
+ } else {
970
+ appConfig[resource.key] = value;
971
+ }
972
+ }
973
+ if (Object.keys(services).length > 0) {
974
+ const links = await exportAppLinks(ctx, app, services);
975
+ if (links.length > 0) appConfig.links = links;
976
+ }
747
977
  config.apps[app] = appConfig;
748
978
  }
749
979
  return config;
@@ -751,52 +981,45 @@ async function runExport(runner, opts) {
751
981
 
752
982
  // src/commands/diff.ts
753
983
  import chalk2 from "chalk";
754
- function computeDiff(desired, current) {
984
+ async function computeDiff(ctx, config) {
755
985
  const result = { apps: {}, services: {}, inSync: true };
756
- for (const [app, desiredApp] of Object.entries(desired.apps)) {
757
- const currentApp = current.apps[app] ?? {};
986
+ const prefetched = /* @__PURE__ */ new Map();
987
+ await Promise.all(
988
+ ALL_APP_RESOURCES.filter((r) => !r.forceApply && !r.key.startsWith("_") && r.readAll).map(async (r) => {
989
+ prefetched.set(r.key, await r.readAll(ctx));
990
+ })
991
+ );
992
+ for (const [app, appConfig] of Object.entries(config.apps)) {
758
993
  const appDiff = {};
759
- const features = [
760
- "domains",
761
- "ports",
762
- "env",
763
- "ssl",
764
- "storage",
765
- "nginx",
766
- "logs",
767
- "registry",
768
- "scheduler",
769
- "checks",
770
- "networks",
771
- "proxy",
772
- "links"
773
- ];
774
- for (const feature of features) {
775
- const d = desiredApp[feature];
776
- const c = currentApp[feature];
777
- if (d === void 0) continue;
778
- const dStr = JSON.stringify(d);
779
- const cStr = JSON.stringify(c);
780
- if (c === void 0) {
781
- appDiff[feature] = { status: "missing", desired: d, current: void 0 };
782
- result.inSync = false;
783
- } else if (dStr !== cStr) {
784
- appDiff[feature] = { status: "changed", desired: d, current: c };
994
+ for (const resource of ALL_APP_RESOURCES) {
995
+ if (resource.key.startsWith("_")) continue;
996
+ if (resource.forceApply) continue;
997
+ let desired;
998
+ if (resource.key === "proxy") {
999
+ desired = appConfig.proxy?.enabled;
1000
+ } else {
1001
+ desired = appConfig[resource.key];
1002
+ }
1003
+ if (desired === void 0) continue;
1004
+ const bulk = prefetched.get(resource.key);
1005
+ const current = bulk ? bulk.get(app) : await resource.read(ctx, app);
1006
+ const change = computeChange(current, desired);
1007
+ if (!change.changed) {
1008
+ appDiff[resource.key] = { status: "in-sync", desired, current };
1009
+ } else if (current === null || current === void 0 || Array.isArray(current) && current.length === 0 || typeof current === "object" && Object.keys(current).length === 0) {
1010
+ appDiff[resource.key] = { status: "missing", desired, current };
785
1011
  result.inSync = false;
786
1012
  } else {
787
- appDiff[feature] = { status: "in-sync", desired: d, current: c };
1013
+ appDiff[resource.key] = { status: "changed", desired, current };
1014
+ result.inSync = false;
788
1015
  }
789
1016
  }
790
1017
  result.apps[app] = appDiff;
791
1018
  }
792
- for (const [svc] of Object.entries(desired.services ?? {})) {
793
- const exists = current.services?.[svc];
794
- if (!exists) {
795
- result.services[svc] = { status: "missing" };
796
- result.inSync = false;
797
- } else {
798
- result.services[svc] = { status: "in-sync" };
799
- }
1019
+ for (const [svc, svcConfig] of Object.entries(config.services ?? {})) {
1020
+ const exists = await ctx.check(`${svcConfig.plugin}:exists`, svc);
1021
+ result.services[svc] = { status: exists ? "in-sync" : "missing" };
1022
+ if (!exists) result.inSync = false;
800
1023
  }
801
1024
  return result;
802
1025
  }
@@ -921,14 +1144,15 @@ function makeRunner(opts) {
921
1144
  program.command("up [apps...]").description("Create/update apps and services to match config").option("-f, --file <path>", "Config file", "dokku-compose.yml").option("--dry-run", "Print commands without executing").option("--fail-fast", "Stop on first error").action(async (apps, opts) => {
922
1145
  const config = loadConfig(opts.file);
923
1146
  const runner = makeRunner(opts);
1147
+ const ctx = createContext(runner);
924
1148
  try {
925
- await runUp(runner, config, apps);
1149
+ await runUp(ctx, config, apps);
926
1150
  if (opts.dryRun) {
927
1151
  console.log("\n# Commands that would run:");
928
1152
  for (const cmd of runner.dryRunLog) console.log(`dokku ${cmd}`);
929
1153
  }
930
1154
  } finally {
931
- await runner.close();
1155
+ await ctx.close();
932
1156
  }
933
1157
  });
934
1158
  program.command("down [apps...]").description("Destroy apps and services (requires --force)").option("-f, --file <path>", "Config file", "dokku-compose.yml").option("--force", "Required to destroy apps").action(async (apps, opts) => {
@@ -938,10 +1162,11 @@ program.command("down [apps...]").description("Destroy apps and services (requir
938
1162
  }
939
1163
  const config = loadConfig(opts.file);
940
1164
  const runner = makeRunner({});
1165
+ const ctx = createContext(runner);
941
1166
  try {
942
- await runDown(runner, config, apps, { force: true });
1167
+ await runDown(ctx, config, apps, { force: true });
943
1168
  } finally {
944
- await runner.close();
1169
+ await ctx.close();
945
1170
  }
946
1171
  });
947
1172
  program.command("validate [file]").description("Validate dokku-compose.yml without touching the server").action((file = "dokku-compose.yml") => {
@@ -962,8 +1187,9 @@ ${result.errors.length} error(s), ${result.warnings.length} warning(s)`);
962
1187
  });
963
1188
  program.command("export").description("Export server state to dokku-compose.yml format").option("--app <app>", "Export only a specific app").option("-o, --output <path>", "Write to file instead of stdout").action(async (opts) => {
964
1189
  const runner = makeRunner({});
1190
+ const ctx = createContext(runner);
965
1191
  try {
966
- const result = await runExport(runner, {
1192
+ const result = await runExport(ctx, {
967
1193
  appFilter: opts.app ? [opts.app] : void 0
968
1194
  });
969
1195
  const out = yaml3.dump(result, { lineWidth: 120 });
@@ -974,22 +1200,20 @@ program.command("export").description("Export server state to dokku-compose.yml
974
1200
  process.stdout.write(out);
975
1201
  }
976
1202
  } finally {
977
- await runner.close();
1203
+ await ctx.close();
978
1204
  }
979
1205
  });
980
1206
  program.command("diff").description("Show what is out of sync between config and server").option("-f, --file <path>", "Config file", "dokku-compose.yml").option("--verbose", "Show git-style +/- diff").action(async (opts) => {
981
1207
  const desired = loadConfig(opts.file);
982
1208
  const runner = makeRunner({});
1209
+ const ctx = createContext(runner);
983
1210
  try {
984
- const current = await runExport(runner, {
985
- appFilter: Object.keys(desired.apps)
986
- });
987
- const diff = computeDiff(desired, current);
1211
+ const diff = await computeDiff(ctx, desired);
988
1212
  const output = opts.verbose ? formatVerbose(diff) : formatSummary(diff);
989
1213
  process.stdout.write(output);
990
1214
  process.exit(diff.inSync ? 0 : 1);
991
1215
  } finally {
992
- await runner.close();
1216
+ await ctx.close();
993
1217
  }
994
1218
  });
995
1219
  program.command("ps [apps...]").description("Show status of configured apps").option("-f, --file <path>", "Config file", "dokku-compose.yml").action(async (apps, opts) => {