dokku-compose 0.3.6 → 0.4.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 +607 -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,326 @@ 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/lists.ts
288
+ function splitWords(raw) {
289
+ return raw.split(/\s+/).map((s) => s.trim()).filter(Boolean);
208
290
  }
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);
291
+ function splitLines(raw) {
292
+ return raw.split("\n").map((s) => s.trim()).filter(Boolean);
293
+ }
294
+ var Ports = {
295
+ key: "ports",
296
+ read: async (ctx, target) => {
297
+ const raw = await ctx.query("ports:report", target, "--ports-map");
298
+ return splitWords(raw);
299
+ },
300
+ onChange: async (ctx, target, change) => {
301
+ await ctx.run("ports:set", target, ...change.after);
302
+ }
303
+ };
304
+ var Domains = {
305
+ key: "domains",
306
+ read: async (ctx, target) => {
307
+ const raw = await ctx.query("domains:report", target, "--domains-app-vhosts");
308
+ return splitLines(raw);
309
+ },
310
+ onChange: async (ctx, target, { added, removed }) => {
311
+ for (const d of removed) await ctx.run("domains:remove", target, d);
312
+ for (const d of added) await ctx.run("domains:add", target, d);
313
+ }
314
+ };
315
+ var Storage = {
316
+ key: "storage",
317
+ read: async (ctx, target) => {
318
+ const raw = await ctx.query("storage:report", target, "--storage-mounts");
319
+ return splitLines(raw);
320
+ },
321
+ onChange: async (ctx, target, { added, removed }) => {
322
+ for (const m of removed) await ctx.run("storage:unmount", target, m);
323
+ for (const m of added) await ctx.run("storage:mount", target, m);
324
+ }
325
+ };
326
+
327
+ // src/resources/parsers.ts
328
+ function parseReport(raw, namespace) {
329
+ const result = {};
330
+ const prefix = new RegExp(`^${namespace}\\s+`, "i");
331
+ for (const line of raw.split("\n")) {
332
+ if (line.trimStart().startsWith("=====>")) continue;
333
+ const colonIdx = line.indexOf(":");
334
+ if (colonIdx === -1) continue;
335
+ const rawKey = line.slice(0, colonIdx).trim();
336
+ if (!rawKey) continue;
337
+ const value = line.slice(colonIdx + 1).trim();
338
+ const stripped = rawKey.replace(prefix, "");
339
+ const key = stripped.toLowerCase().replace(/\s+/g, "-");
340
+ if (key.startsWith("computed-") || key.startsWith("global-") || key === "last-visited-at") continue;
341
+ if (!value) continue;
342
+ result[key] = value;
215
343
  }
216
- logDone();
344
+ return result;
217
345
  }
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;
346
+
347
+ // src/resources/properties.ts
348
+ function propertyResource(opts) {
349
+ return {
350
+ key: opts.key,
351
+ async read(ctx, target) {
352
+ const raw = await ctx.query(`${opts.namespace}:report`, target);
353
+ return parseReport(raw, opts.namespace);
354
+ },
355
+ async onChange(ctx, target, change) {
356
+ for (const [key, value] of Object.entries({ ...change.added, ...change.modified })) {
357
+ await ctx.run(opts.setCmd, target, key, String(value));
358
+ }
359
+ if (opts.afterChange) {
360
+ for (const cmd of opts.afterChange) {
361
+ await ctx.run(cmd, target);
362
+ }
363
+ }
364
+ }
365
+ };
225
366
  }
367
+ var Nginx = propertyResource({
368
+ key: "nginx",
369
+ namespace: "nginx",
370
+ setCmd: "nginx:set",
371
+ afterChange: ["proxy:build-config"]
372
+ });
373
+ var Logs = propertyResource({
374
+ key: "logs",
375
+ namespace: "logs",
376
+ setCmd: "logs:set"
377
+ });
378
+ var Registry = propertyResource({
379
+ key: "registry",
380
+ namespace: "registry",
381
+ setCmd: "registry:set"
382
+ });
383
+ var Scheduler = {
384
+ key: "scheduler",
385
+ async read(ctx, target) {
386
+ const raw = await ctx.query("scheduler:report", target);
387
+ const report = parseReport(raw, "scheduler");
388
+ return report["selected"] ?? "";
389
+ },
390
+ async onChange(ctx, target, change) {
391
+ await ctx.run("scheduler:set", target, "selected", change.after);
392
+ }
393
+ };
394
+
395
+ // src/resources/toggle.ts
396
+ var Proxy = {
397
+ key: "proxy",
398
+ read: async (ctx, target) => {
399
+ const raw = await ctx.query("proxy:report", target, "--proxy-enabled");
400
+ return raw.trim() === "true";
401
+ },
402
+ onChange: async (ctx, target, { after }) => {
403
+ await ctx.run(after ? "proxy:enable" : "proxy:disable", target);
404
+ }
405
+ };
406
+
407
+ // src/resources/config.ts
408
+ var MANAGED_KEYS_VAR = "DOKKU_COMPOSE_MANAGED_KEYS";
409
+ var Config = {
410
+ key: "env",
411
+ read: async (ctx, target) => {
412
+ const managedRaw = await ctx.query("config:get", target, MANAGED_KEYS_VAR);
413
+ const managedKeys = managedRaw.trim() ? managedRaw.trim().split(",").filter(Boolean) : [];
414
+ const result = {};
415
+ if (managedKeys.length > 0) {
416
+ const raw = await ctx.query("config:export", target, "--format", "shell");
417
+ for (const line of raw.split("\n")) {
418
+ const match = line.match(/^export\s+(\w+)=['"]?(.*?)['"]?$/);
419
+ if (match && managedKeys.includes(match[1])) {
420
+ result[match[1]] = match[2];
421
+ }
422
+ }
423
+ }
424
+ return result;
425
+ },
426
+ onChange: async (ctx, target, change) => {
427
+ const { added, removed, modified } = change;
428
+ if (removed.length > 0) {
429
+ await ctx.run("config:unset", "--no-restart", target, ...removed);
430
+ }
431
+ const toSet = { ...added, ...modified };
432
+ const allDesiredKeys = Object.keys(change.after);
433
+ const managedValue = allDesiredKeys.join(",");
434
+ if (Object.keys(toSet).length > 0 || removed.length > 0) {
435
+ const pairs = Object.entries(change.after).map(([k, v]) => `${k}=${v}`);
436
+ await ctx.run(
437
+ "config:set",
438
+ "--no-restart",
439
+ target,
440
+ ...pairs,
441
+ `${MANAGED_KEYS_VAR}=${managedValue}`
442
+ );
443
+ }
444
+ }
445
+ };
446
+
447
+ // src/resources/certs.ts
448
+ var Certs = {
449
+ key: "ssl",
450
+ read: async (ctx, target) => {
451
+ const raw = await ctx.query("certs:report", target, "--ssl-enabled");
452
+ return raw.trim() === "true";
453
+ },
454
+ onChange: async (ctx, target, { before, after }) => {
455
+ if (after === false && before) {
456
+ await ctx.run("certs:remove", target);
457
+ }
458
+ if (after && typeof after === "object") {
459
+ await ctx.run("certs:add", target, after.certfile, after.keyfile);
460
+ }
461
+ }
462
+ };
463
+
464
+ // src/resources/builder.ts
465
+ var Builder = {
466
+ key: "build",
467
+ forceApply: true,
468
+ read: async () => ({}),
469
+ onChange: async (ctx, target, { after }) => {
470
+ if (after.dockerfile)
471
+ await ctx.run("builder-dockerfile:set", target, "dockerfile-path", after.dockerfile);
472
+ if (after.app_json)
473
+ await ctx.run("app-json:set", target, "appjson-path", after.app_json);
474
+ if (after.context)
475
+ await ctx.run("builder:set", target, "build-dir", after.context);
476
+ if (after.args) {
477
+ for (const [key, value] of Object.entries(after.args)) {
478
+ await ctx.run("docker-options:add", target, "build", `--build-arg ${key}=${value}`);
479
+ }
480
+ }
481
+ }
482
+ };
483
+
484
+ // src/resources/docker-options.ts
485
+ var DockerOptions = {
486
+ key: "docker_options",
487
+ forceApply: true,
488
+ read: async () => ({}),
489
+ onChange: async (ctx, target, { after }) => {
490
+ for (const phase of ["build", "deploy", "run"]) {
491
+ const opts = after[phase];
492
+ if (!opts || opts.length === 0) continue;
493
+ await ctx.run("docker-options:clear", target, phase);
494
+ for (const opt of opts) {
495
+ await ctx.run("docker-options:add", target, phase, opt);
496
+ }
497
+ }
498
+ }
499
+ };
500
+
501
+ // src/resources/git.ts
502
+ var Git = {
503
+ key: "git",
504
+ read: async (ctx, target) => {
505
+ const report = await ctx.query("git:report", target, "--git-deploy-branch");
506
+ return { deploy_branch: report.trim() || void 0 };
507
+ },
508
+ onChange: async (ctx, target, { after }) => {
509
+ if (after.deploy_branch) {
510
+ await ctx.run("git:set", target, "deploy-branch", after.deploy_branch);
511
+ }
512
+ }
513
+ };
514
+
515
+ // src/resources/checks.ts
516
+ var Checks = {
517
+ key: "checks",
518
+ forceApply: true,
519
+ read: async () => ({}),
520
+ onChange: async (ctx, target, { after }) => {
521
+ if (after === false) {
522
+ await ctx.run("checks:disable", target);
523
+ return;
524
+ }
525
+ if (after.disabled && after.disabled.length > 0) {
526
+ await ctx.run("checks:disable", target, ...after.disabled);
527
+ }
528
+ if (after.skipped && after.skipped.length > 0) {
529
+ await ctx.run("checks:skip", target, ...after.skipped);
530
+ }
531
+ for (const [key, value] of Object.entries(after)) {
532
+ if (key === "disabled" || key === "skipped") continue;
533
+ await ctx.run("checks:set", target, key, String(value));
534
+ }
535
+ }
536
+ };
537
+
538
+ // src/resources/network.ts
539
+ var Networks = {
540
+ key: "networks",
541
+ read: async (ctx, target) => {
542
+ const raw = await ctx.query("network:report", target, "--network-attach-post-deploy");
543
+ return raw.trim() ? raw.trim().split(/\s+/) : [];
544
+ },
545
+ onChange: async (ctx, target, { after }) => {
546
+ await ctx.run("network:set", target, "attach-post-deploy", ...after);
547
+ }
548
+ };
549
+ var NetworkProps = {
550
+ key: "network",
551
+ forceApply: true,
552
+ read: async () => ({}),
553
+ onChange: async (ctx, target, { after }) => {
554
+ if (after.attach_post_create !== void 0 && after.attach_post_create !== false) {
555
+ const nets = Array.isArray(after.attach_post_create) ? after.attach_post_create : [after.attach_post_create];
556
+ await ctx.run("network:set", target, "attach-post-create", ...nets);
557
+ }
558
+ if (after.initial_network !== void 0 && after.initial_network !== false) {
559
+ await ctx.run("network:set", target, "initial-network", after.initial_network);
560
+ }
561
+ if (after.bind_all_interfaces !== void 0) {
562
+ await ctx.run("network:set", target, "bind-all-interfaces", String(after.bind_all_interfaces));
563
+ }
564
+ if (after.tld !== void 0 && after.tld !== false) {
565
+ await ctx.run("network:set", target, "tld", after.tld);
566
+ }
567
+ }
568
+ };
226
569
 
227
570
  // src/modules/plugins.ts
228
- async function ensurePlugins(runner, plugins) {
229
- const listOutput = await runner.query("plugin:list");
571
+ async function ensurePlugins(ctx, plugins) {
572
+ const listOutput = await ctx.query("plugin:list");
230
573
  const installedNames = new Set(
231
574
  listOutput.split("\n").map((line) => line.trim().split(/\s+/)[0]).filter(Boolean)
232
575
  );
@@ -236,514 +579,304 @@ async function ensurePlugins(runner, plugins) {
236
579
  logSkip();
237
580
  continue;
238
581
  }
239
- await runner.run("plugin:install", config.url, "--name", name);
582
+ await ctx.run("plugin:install", config.url, "--name", name);
240
583
  logDone();
241
584
  }
242
585
  }
243
586
 
244
587
  // src/modules/network.ts
245
- async function ensureNetworks(runner, networks) {
588
+ async function ensureNetworks(ctx, networks) {
246
589
  for (const net of networks) {
247
590
  logAction("network", `Creating ${net}`);
248
- const exists = await runner.check("network:exists", net);
591
+ const exists = await ctx.check("network:exists", net);
249
592
  if (exists) {
250
593
  logSkip();
251
594
  continue;
252
595
  }
253
- await runner.run("network:create", net);
596
+ await ctx.run("network:create", net);
254
597
  logDone();
255
598
  }
256
599
  }
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
600
  var DOCKER_BUILTIN_NETWORKS = /* @__PURE__ */ new Set(["bridge", "host", "none"]);
280
- async function exportNetworks(runner) {
281
- const output = await runner.query("network:list");
601
+ async function exportNetworks(ctx) {
602
+ const output = await ctx.query("network:list");
282
603
  return output.split("\n").map((s) => s.trim()).filter((s) => s && !s.startsWith("=====>") && !DOCKER_BUILTIN_NETWORKS.has(s));
283
604
  }
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
605
 
300
606
  // src/modules/services.ts
301
- async function ensureServices(runner, services) {
607
+ import { createHash as createHash2 } from "crypto";
608
+ function backupHashKey(serviceName) {
609
+ return "DOKKU_COMPOSE_BACKUP_HASH_" + serviceName.toUpperCase().replace(/-/g, "_");
610
+ }
611
+ function computeBackupHash(backup) {
612
+ return createHash2("sha256").update(JSON.stringify(backup)).digest("hex");
613
+ }
614
+ async function ensureServices(ctx, services) {
302
615
  for (const [name, config] of Object.entries(services)) {
303
616
  logAction("services", `Ensuring ${name}`);
304
- const exists = await runner.check(`${config.plugin}:exists`, name);
617
+ const exists = await ctx.check(`${config.plugin}:exists`, name);
305
618
  if (exists) {
306
619
  logSkip();
307
620
  continue;
308
621
  }
309
- await runner.run(`${config.plugin}:create`, name);
622
+ const args = [`${config.plugin}:create`, name];
623
+ if (config.image) args.push("--image", config.image);
624
+ if (config.version) args.push("--image-version", config.version);
625
+ await ctx.run(...args);
310
626
  logDone();
311
627
  }
312
628
  }
313
- async function ensureAppLinks(runner, app, desiredLinks, allServices) {
629
+ async function ensureServiceBackups(ctx, services) {
630
+ for (const [name, config] of Object.entries(services)) {
631
+ if (!config.backup) continue;
632
+ logAction("services", `Configuring backup for ${name}`);
633
+ const hashKey = backupHashKey(name);
634
+ const desiredHash = computeBackupHash(config.backup);
635
+ const storedHash = await ctx.query("config:get", "--global", hashKey);
636
+ if (storedHash === desiredHash) {
637
+ logSkip();
638
+ continue;
639
+ }
640
+ const { schedule, bucket, auth } = config.backup;
641
+ await ctx.run(`${config.plugin}:backup-deauth`, name);
642
+ await ctx.run(
643
+ `${config.plugin}:backup-auth`,
644
+ name,
645
+ auth.access_key_id,
646
+ auth.secret_access_key,
647
+ auth.region,
648
+ auth.signature_version,
649
+ auth.endpoint
650
+ );
651
+ await ctx.run(`${config.plugin}:backup-schedule`, name, schedule, bucket);
652
+ await ctx.run("config:set", "--global", `${hashKey}=${desiredHash}`);
653
+ logDone();
654
+ }
655
+ }
656
+ async function ensureAppLinks(ctx, app, desiredLinks, allServices) {
314
657
  const desiredSet = new Set(desiredLinks);
315
658
  for (const [serviceName, serviceConfig] of Object.entries(allServices)) {
316
- const isLinked = await runner.check(`${serviceConfig.plugin}:linked`, serviceName, app);
659
+ const isLinked = await ctx.check(`${serviceConfig.plugin}:linked`, serviceName, app);
317
660
  const isDesired = desiredSet.has(serviceName);
318
661
  if (isDesired && !isLinked) {
319
662
  logAction(app, `Linking ${serviceName}`);
320
- await runner.run(`${serviceConfig.plugin}:link`, serviceName, app, "--no-restart");
663
+ await ctx.run(`${serviceConfig.plugin}:link`, serviceName, app, "--no-restart");
321
664
  logDone();
322
665
  } else if (!isDesired && isLinked) {
323
666
  logAction(app, `Unlinking ${serviceName}`);
324
- await runner.run(`${serviceConfig.plugin}:unlink`, serviceName, app, "--no-restart");
667
+ await ctx.run(`${serviceConfig.plugin}:unlink`, serviceName, app, "--no-restart");
325
668
  logDone();
326
669
  }
327
670
  }
328
671
  }
329
- async function destroyAppLinks(runner, app, links, allServices) {
672
+ async function destroyAppLinks(ctx, app, links, allServices) {
330
673
  for (const serviceName of links) {
331
674
  const config = allServices[serviceName];
332
675
  if (!config) continue;
333
- const isLinked = await runner.check(`${config.plugin}:linked`, serviceName, app);
676
+ const isLinked = await ctx.check(`${config.plugin}:linked`, serviceName, app);
334
677
  if (isLinked) {
335
- await runner.run(`${config.plugin}:unlink`, serviceName, app, "--no-restart");
678
+ await ctx.run(`${config.plugin}:unlink`, serviceName, app, "--no-restart");
336
679
  }
337
680
  }
338
681
  }
339
- async function destroyServices(runner, services) {
682
+ async function destroyServices(ctx, services) {
340
683
  for (const [name, config] of Object.entries(services)) {
341
684
  logAction("services", `Destroying ${name}`);
342
- const exists = await runner.check(`${config.plugin}:exists`, name);
685
+ const exists = await ctx.check(`${config.plugin}:exists`, name);
343
686
  if (!exists) {
344
687
  logSkip();
345
688
  continue;
346
689
  }
347
- await runner.run(`${config.plugin}:destroy`, name, "--force");
690
+ await ctx.run(`${config.plugin}:destroy`, name, "--force");
348
691
  logDone();
349
692
  }
350
693
  }
351
- async function exportServices(_runner) {
694
+ async function exportServices(_ctx) {
352
695
  return {};
353
696
  }
354
- async function exportAppLinks(runner, app, services) {
697
+ async function exportAppLinks(ctx, app, services) {
355
698
  const linked = [];
356
699
  for (const [serviceName, config] of Object.entries(services)) {
357
- const isLinked = await runner.check(`${config.plugin}:linked`, serviceName, app);
700
+ const isLinked = await ctx.check(`${config.plugin}:linked`, serviceName, app);
358
701
  if (isLinked) linked.push(serviceName);
359
702
  }
360
703
  return linked;
361
704
  }
362
705
 
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);
706
+ // src/modules/domains.ts
707
+ async function ensureGlobalDomains(ctx, domains) {
708
+ logAction("global", "Configuring domains");
709
+ if (domains === false) {
710
+ await ctx.run("domains:clear-global");
374
711
  } else {
375
- await runner.run("proxy:disable", app);
712
+ await ctx.run("domains:set-global", ...domains);
376
713
  }
377
714
  logDone();
378
715
  }
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
716
 
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;
717
+ // src/modules/config.ts
718
+ async function ensureGlobalConfig(ctx, env) {
719
+ if (env === false) return;
720
+ const pairs = Object.entries(env).map(([k, v]) => `${k}=${v}`);
721
+ await ctx.run("config:set", "--global", ...pairs);
404
722
  }
405
723
 
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);
724
+ // src/modules/logs.ts
725
+ async function ensureGlobalLogs(ctx, logs) {
726
+ for (const [key, value] of Object.entries(logs)) {
727
+ await ctx.run("logs:set", "--global", key, String(value));
460
728
  }
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
729
  }
469
730
 
470
731
  // src/modules/nginx.ts
471
- async function ensureAppNginx(runner, app, nginx) {
732
+ async function ensureGlobalNginx(ctx, nginx) {
472
733
  for (const [key, value] of Object.entries(nginx)) {
473
- await runner.run("nginx:set", app, key, String(value));
734
+ await ctx.run("nginx:set", "--global", key, String(value));
474
735
  }
475
736
  }
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
737
 
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));
738
+ // src/commands/up.ts
739
+ async function runUp(ctx, config, appFilter) {
740
+ const apps = appFilter.length > 0 ? appFilter : Object.keys(config.apps);
741
+ if (config.plugins) await ensurePlugins(ctx, config.plugins);
742
+ if (config.domains !== void 0) await ensureGlobalDomains(ctx, config.domains);
743
+ if (config.env !== void 0) await ensureGlobalConfig(ctx, config.env);
744
+ if (config.logs !== void 0) await ensureGlobalLogs(ctx, config.logs);
745
+ if (config.nginx !== void 0) await ensureGlobalNginx(ctx, config.nginx);
746
+ if (config.networks) await ensureNetworks(ctx, config.networks);
747
+ if (config.services) await ensureServices(ctx, config.services);
748
+ if (config.services) await ensureServiceBackups(ctx, config.services);
749
+ for (const app of apps) {
750
+ const appConfig = config.apps[app];
751
+ if (!appConfig) continue;
752
+ await reconcile(Apps, ctx, app, true);
753
+ await reconcile(Domains, ctx, app, appConfig.domains);
754
+ await reconcile(Networks, ctx, app, appConfig.networks);
755
+ await reconcile(NetworkProps, ctx, app, appConfig.network);
756
+ await reconcile(Proxy, ctx, app, appConfig.proxy?.enabled);
757
+ await reconcile(Ports, ctx, app, appConfig.ports);
758
+ if (config.services) {
759
+ await ensureAppLinks(ctx, app, appConfig.links ?? [], config.services);
760
+ }
761
+ await reconcile(Certs, ctx, app, appConfig.ssl);
762
+ await reconcile(Storage, ctx, app, appConfig.storage);
763
+ await reconcile(Nginx, ctx, app, appConfig.nginx);
764
+ await reconcile(Checks, ctx, app, appConfig.checks);
765
+ await reconcile(Logs, ctx, app, appConfig.logs);
766
+ await reconcile(Registry, ctx, app, appConfig.registry);
767
+ await reconcile(Scheduler, ctx, app, appConfig.scheduler);
768
+ await reconcile(Config, ctx, app, appConfig.env);
769
+ await reconcile(Builder, ctx, app, appConfig.build);
770
+ await reconcile(Git, ctx, app, appConfig.git ?? config.git);
771
+ await reconcile(DockerOptions, ctx, app, appConfig.docker_options);
545
772
  }
546
773
  }
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
774
 
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) {
775
+ // src/modules/apps.ts
776
+ async function destroyApp(ctx, app) {
777
+ const exists = await ctx.check("apps:exists", app);
778
+ logAction(app, "Destroying app");
779
+ if (!exists) {
558
780
  logSkip();
559
781
  return;
560
782
  }
561
- await runner.run("scheduler:set", app, "selected", scheduler);
783
+ await ctx.run("apps:destroy", app, "--force");
562
784
  logDone();
563
785
  }
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}`
786
+ async function exportApps(ctx) {
787
+ const output = await ctx.query("apps:list");
788
+ return output.split("\n").map((s) => s.trim()).filter(
789
+ (s) => s && !s.startsWith("=====>")
594
790
  );
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
791
  }
678
792
 
679
793
  // src/commands/down.ts
680
- async function runDown(runner, config, appFilter, opts) {
794
+ async function runDown(ctx, config, appFilter, opts) {
681
795
  const apps = appFilter.length > 0 ? appFilter : Object.keys(config.apps);
682
796
  for (const app of apps) {
683
797
  const appConfig = config.apps[app];
684
798
  if (!appConfig) continue;
685
799
  if (config.services && appConfig.links) {
686
- await destroyAppLinks(runner, app, appConfig.links, config.services);
800
+ await destroyAppLinks(ctx, app, appConfig.links, config.services);
687
801
  }
688
- await destroyApp(runner, app);
802
+ await destroyApp(ctx, app);
689
803
  }
690
804
  if (config.services) {
691
- await destroyServices(runner, config.services);
805
+ await destroyServices(ctx, config.services);
692
806
  }
693
807
  if (config.networks) {
694
808
  for (const net of config.networks) {
695
809
  logAction("network", `Destroying ${net}`);
696
- const exists = await runner.check("network:exists", net);
810
+ const exists = await ctx.check("network:exists", net);
697
811
  if (!exists) {
698
812
  logSkip();
699
813
  continue;
700
814
  }
701
- await runner.run("network:destroy", net, "--force");
815
+ await ctx.run("network:destroy", net, "--force");
702
816
  logDone();
703
817
  }
704
818
  }
705
819
  }
706
820
 
821
+ // src/resources/index.ts
822
+ var NETWORKING_RESOURCES = [
823
+ Domains,
824
+ Networks,
825
+ NetworkProps,
826
+ Proxy,
827
+ Ports
828
+ ];
829
+ var CONFIG_RESOURCES = [
830
+ Certs,
831
+ Storage,
832
+ Nginx,
833
+ Checks,
834
+ Logs,
835
+ Registry,
836
+ Scheduler,
837
+ Config
838
+ ];
839
+ var BUILD_RESOURCES = [
840
+ Builder,
841
+ Git,
842
+ DockerOptions
843
+ ];
844
+ var ALL_APP_RESOURCES = [
845
+ ...NETWORKING_RESOURCES,
846
+ ...CONFIG_RESOURCES,
847
+ ...BUILD_RESOURCES
848
+ ];
849
+
707
850
  // src/commands/export.ts
708
- async function runExport(runner, opts) {
851
+ async function runExport(ctx, opts) {
709
852
  const config = { apps: {} };
710
- const versionOutput = await runner.query("version");
853
+ const versionOutput = await ctx.query("version");
711
854
  const versionMatch = versionOutput.match(/(\d+\.\d+\.\d+)/);
712
855
  if (versionMatch) config.dokku = { version: versionMatch[1] };
713
- const apps = opts.appFilter?.length ? opts.appFilter : await exportApps(runner);
714
- const networks = await exportNetworks(runner);
856
+ const apps = opts.appFilter?.length ? opts.appFilter : await exportApps(ctx);
857
+ const networks = await exportNetworks(ctx);
715
858
  if (networks.length > 0) config.networks = networks;
716
- const services = await exportServices(runner);
859
+ const services = await exportServices(ctx);
717
860
  if (Object.keys(services).length > 0) config.services = services;
718
861
  for (const app of apps) {
719
862
  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;
863
+ for (const resource of ALL_APP_RESOURCES) {
864
+ if (resource.key.startsWith("_")) continue;
865
+ if (resource.forceApply) continue;
866
+ const value = await resource.read(ctx, app);
867
+ if (value === void 0 || value === null || value === "") continue;
868
+ if (Array.isArray(value) && value.length === 0) continue;
869
+ if (typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0) continue;
870
+ if (resource.key === "proxy") {
871
+ appConfig.proxy = { enabled: value };
872
+ } else {
873
+ appConfig[resource.key] = value;
874
+ }
875
+ }
876
+ if (Object.keys(services).length > 0) {
877
+ const links = await exportAppLinks(ctx, app, services);
878
+ if (links.length > 0) appConfig.links = links;
879
+ }
747
880
  config.apps[app] = appConfig;
748
881
  }
749
882
  return config;
@@ -751,52 +884,38 @@ async function runExport(runner, opts) {
751
884
 
752
885
  // src/commands/diff.ts
753
886
  import chalk2 from "chalk";
754
- function computeDiff(desired, current) {
887
+ async function computeDiff(ctx, config) {
755
888
  const result = { apps: {}, services: {}, inSync: true };
756
- for (const [app, desiredApp] of Object.entries(desired.apps)) {
757
- const currentApp = current.apps[app] ?? {};
889
+ for (const [app, appConfig] of Object.entries(config.apps)) {
758
890
  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 };
891
+ for (const resource of ALL_APP_RESOURCES) {
892
+ if (resource.key.startsWith("_")) continue;
893
+ if (resource.forceApply) continue;
894
+ let desired;
895
+ if (resource.key === "proxy") {
896
+ desired = appConfig.proxy?.enabled;
897
+ } else {
898
+ desired = appConfig[resource.key];
899
+ }
900
+ if (desired === void 0) continue;
901
+ const current = await resource.read(ctx, app);
902
+ const change = computeChange(current, desired);
903
+ if (!change.changed) {
904
+ appDiff[resource.key] = { status: "in-sync", desired, current };
905
+ } else if (current === null || current === void 0 || Array.isArray(current) && current.length === 0 || typeof current === "object" && Object.keys(current).length === 0) {
906
+ appDiff[resource.key] = { status: "missing", desired, current };
785
907
  result.inSync = false;
786
908
  } else {
787
- appDiff[feature] = { status: "in-sync", desired: d, current: c };
909
+ appDiff[resource.key] = { status: "changed", desired, current };
910
+ result.inSync = false;
788
911
  }
789
912
  }
790
913
  result.apps[app] = appDiff;
791
914
  }
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
- }
915
+ for (const [svc, svcConfig] of Object.entries(config.services ?? {})) {
916
+ const exists = await ctx.check(`${svcConfig.plugin}:exists`, svc);
917
+ result.services[svc] = { status: exists ? "in-sync" : "missing" };
918
+ if (!exists) result.inSync = false;
800
919
  }
801
920
  return result;
802
921
  }
@@ -921,14 +1040,15 @@ function makeRunner(opts) {
921
1040
  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
1041
  const config = loadConfig(opts.file);
923
1042
  const runner = makeRunner(opts);
1043
+ const ctx = createContext(runner);
924
1044
  try {
925
- await runUp(runner, config, apps);
1045
+ await runUp(ctx, config, apps);
926
1046
  if (opts.dryRun) {
927
1047
  console.log("\n# Commands that would run:");
928
1048
  for (const cmd of runner.dryRunLog) console.log(`dokku ${cmd}`);
929
1049
  }
930
1050
  } finally {
931
- await runner.close();
1051
+ await ctx.close();
932
1052
  }
933
1053
  });
934
1054
  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 +1058,11 @@ program.command("down [apps...]").description("Destroy apps and services (requir
938
1058
  }
939
1059
  const config = loadConfig(opts.file);
940
1060
  const runner = makeRunner({});
1061
+ const ctx = createContext(runner);
941
1062
  try {
942
- await runDown(runner, config, apps, { force: true });
1063
+ await runDown(ctx, config, apps, { force: true });
943
1064
  } finally {
944
- await runner.close();
1065
+ await ctx.close();
945
1066
  }
946
1067
  });
947
1068
  program.command("validate [file]").description("Validate dokku-compose.yml without touching the server").action((file = "dokku-compose.yml") => {
@@ -962,8 +1083,9 @@ ${result.errors.length} error(s), ${result.warnings.length} warning(s)`);
962
1083
  });
963
1084
  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
1085
  const runner = makeRunner({});
1086
+ const ctx = createContext(runner);
965
1087
  try {
966
- const result = await runExport(runner, {
1088
+ const result = await runExport(ctx, {
967
1089
  appFilter: opts.app ? [opts.app] : void 0
968
1090
  });
969
1091
  const out = yaml3.dump(result, { lineWidth: 120 });
@@ -974,22 +1096,20 @@ program.command("export").description("Export server state to dokku-compose.yml
974
1096
  process.stdout.write(out);
975
1097
  }
976
1098
  } finally {
977
- await runner.close();
1099
+ await ctx.close();
978
1100
  }
979
1101
  });
980
1102
  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
1103
  const desired = loadConfig(opts.file);
982
1104
  const runner = makeRunner({});
1105
+ const ctx = createContext(runner);
983
1106
  try {
984
- const current = await runExport(runner, {
985
- appFilter: Object.keys(desired.apps)
986
- });
987
- const diff = computeDiff(desired, current);
1107
+ const diff = await computeDiff(ctx, desired);
988
1108
  const output = opts.verbose ? formatVerbose(diff) : formatSummary(diff);
989
1109
  process.stdout.write(output);
990
1110
  process.exit(diff.inSync ? 0 : 1);
991
1111
  } finally {
992
- await runner.close();
1112
+ await ctx.close();
993
1113
  }
994
1114
  });
995
1115
  program.command("ps [apps...]").description("Show status of configured apps").option("-f, --file <path>", "Config file", "dokku-compose.yml").action(async (apps, opts) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dokku-compose",
3
- "version": "0.3.6",
3
+ "version": "0.4.0",
4
4
  "description": "Docker Compose for Dokku — declare your entire server in a single YAML file.",
5
5
  "main": "dist/index.js",
6
6
  "exports": "./dist/index.js",