dokku-compose 0.3.5 → 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 +658 -500
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@
4
4
  import { Command } from "commander";
5
5
  import * as fs3 from "fs";
6
6
  import * as yaml3 from "js-yaml";
7
+ import { createRequire } from "module";
7
8
 
8
9
  // src/core/config.ts
9
10
  import * as fs from "fs";
@@ -26,6 +27,9 @@ var ChecksSchema = z.union([
26
27
  skipped: z.array(z.string()).optional()
27
28
  }).catchall(z.union([z.string(), z.number(), z.boolean()]))
28
29
  ]);
30
+ var GitSchema = z.object({
31
+ deploy_branch: z.string().optional()
32
+ });
29
33
  var AppSchema = z.object({
30
34
  domains: z.union([z.array(z.string()), z.literal(false)]).optional(),
31
35
  links: z.array(z.string()).optional(),
@@ -60,12 +64,26 @@ var AppSchema = z.object({
60
64
  build: z.array(z.string()).optional(),
61
65
  deploy: z.array(z.string()).optional(),
62
66
  run: z.array(z.string()).optional()
63
- }).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
64
81
  });
65
82
  var ServiceSchema = z.object({
66
83
  plugin: z.string(),
67
84
  version: z.string().optional(),
68
- image: z.string().optional()
85
+ image: z.string().optional(),
86
+ backup: ServiceBackupSchema.optional()
69
87
  });
70
88
  var PluginSchema = z.object({
71
89
  url: z.string().url(),
@@ -82,29 +100,39 @@ var ConfigSchema = z.object({
82
100
  domains: z.union([z.array(z.string()), z.literal(false)]).optional(),
83
101
  env: EnvMapSchema.optional(),
84
102
  nginx: z.record(z.string(), z.union([z.string(), z.number()])).optional(),
85
- 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()
86
105
  });
87
106
  function parseConfig(raw) {
88
107
  return ConfigSchema.parse(raw);
89
108
  }
90
109
 
91
110
  // src/core/config.ts
111
+ function interpolateEnvVars(content) {
112
+ return content.replace(/\$\{([^}]+)\}/g, (_, name) => process.env[name] ?? "");
113
+ }
92
114
  function loadConfig(filePath) {
93
115
  if (!fs.existsSync(filePath)) {
94
116
  throw new Error(`Config file not found: ${filePath}`);
95
117
  }
96
- const raw = yaml.load(fs.readFileSync(filePath, "utf8"));
118
+ const content = fs.readFileSync(filePath, "utf8");
119
+ const raw = yaml.load(interpolateEnvVars(content));
97
120
  return parseConfig(raw);
98
121
  }
99
122
 
100
123
  // src/core/dokku.ts
101
124
  import { execa } from "execa";
125
+ import { createHash } from "crypto";
126
+ import * as os from "os";
127
+ import * as path from "path";
102
128
  function createRunner(opts = {}) {
103
129
  const log = [];
130
+ const controlPath = opts.host ? path.join(os.tmpdir(), `dc-${createHash("sha1").update(opts.host).digest("hex").slice(0, 16)}.sock`) : null;
131
+ const sshControlFlags = controlPath ? ["-o", "ControlMaster=auto", "-o", `ControlPath=${controlPath}`, "-o", "ControlPersist=60"] : [];
104
132
  async function execDokku(args) {
105
133
  if (opts.host) {
106
134
  try {
107
- const result = await execa("ssh", [`dokku@${opts.host}`, ...args]);
135
+ const result = await execa("ssh", [...sshControlFlags, `dokku@${opts.host}`, ...args]);
108
136
  return { stdout: result.stdout, ok: true };
109
137
  } catch (e) {
110
138
  return { stdout: e.stdout ?? "", ok: false };
@@ -136,10 +164,80 @@ function createRunner(opts = {}) {
136
164
  if (opts.dryRun) return false;
137
165
  const { ok } = await execDokku(args);
138
166
  return ok;
167
+ },
168
+ async close() {
169
+ if (!opts.host || !controlPath) return;
170
+ try {
171
+ await execa("ssh", ["-O", "exit", "-o", `ControlPath=${controlPath}`, `dokku@${opts.host}`]);
172
+ } catch {
173
+ }
139
174
  }
140
175
  };
141
176
  }
142
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
+
143
241
  // src/core/logger.ts
144
242
  import chalk from "chalk";
145
243
  function logAction(context, message) {
@@ -152,68 +250,326 @@ function logSkip() {
152
250
  console.log(`... ${chalk.yellow("already configured")}`);
153
251
  }
154
252
 
155
- // src/modules/apps.ts
156
- async function ensureApp(runner, app) {
157
- const exists = await runner.check("apps:exists", app);
158
- logAction(app, "Creating app");
159
- if (exists) {
160
- 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();
161
260
  return;
162
261
  }
163
- await runner.run("apps:create", app);
164
- logDone();
165
- }
166
- async function destroyApp(runner, app) {
167
- const exists = await runner.check("apps:exists", app);
168
- logAction(app, "Destroying app");
169
- if (!exists) {
262
+ const before = await resource.read(ctx, target);
263
+ const change = computeChange(before, desired);
264
+ if (!change.changed) {
170
265
  logSkip();
171
266
  return;
172
267
  }
173
- await runner.run("apps:destroy", app, "--force");
268
+ await resource.onChange(ctx, target, change);
174
269
  logDone();
175
270
  }
176
- async function exportApps(runner) {
177
- const output = await runner.query("apps:list");
178
- return output.split("\n").map((s) => s.trim()).filter(
179
- (s) => s && !s.startsWith("=====>")
180
- );
181
- }
182
271
 
183
- // src/modules/domains.ts
184
- async function ensureAppDomains(runner, app, domains) {
185
- if (domains === void 0) return;
186
- logAction(app, "Configuring domains");
187
- if (domains === false) {
188
- await runner.run("domains:disable", app);
189
- await runner.run("domains:clear", app);
190
- } else {
191
- await runner.run("domains:enable", app);
192
- 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
+ }
193
284
  }
194
- logDone();
285
+ };
286
+
287
+ // src/resources/lists.ts
288
+ function splitWords(raw) {
289
+ return raw.split(/\s+/).map((s) => s.trim()).filter(Boolean);
195
290
  }
196
- async function ensureGlobalDomains(runner, domains) {
197
- logAction("global", "Configuring domains");
198
- if (domains === false) {
199
- await runner.run("domains:clear-global");
200
- } else {
201
- 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;
202
343
  }
203
- logDone();
344
+ return result;
204
345
  }
205
- async function exportAppDomains(runner, app) {
206
- const enabledRaw = await runner.query("domains:report", app, "--domains-app-enabled");
207
- if (enabledRaw.trim() === "false") return false;
208
- const raw = await runner.query("domains:report", app, "--domains-app-vhosts");
209
- const vhosts = raw.split("\n").map((s) => s.trim()).filter(Boolean);
210
- if (vhosts.length === 0) return void 0;
211
- 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
+ };
212
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
+ };
213
569
 
214
570
  // src/modules/plugins.ts
215
- async function ensurePlugins(runner, plugins) {
216
- const listOutput = await runner.query("plugin:list");
571
+ async function ensurePlugins(ctx, plugins) {
572
+ const listOutput = await ctx.query("plugin:list");
217
573
  const installedNames = new Set(
218
574
  listOutput.split("\n").map((line) => line.trim().split(/\s+/)[0]).filter(Boolean)
219
575
  );
@@ -223,511 +579,304 @@ async function ensurePlugins(runner, plugins) {
223
579
  logSkip();
224
580
  continue;
225
581
  }
226
- await runner.run("plugin:install", config.url, "--name", name);
582
+ await ctx.run("plugin:install", config.url, "--name", name);
227
583
  logDone();
228
584
  }
229
585
  }
230
586
 
231
587
  // src/modules/network.ts
232
- async function ensureNetworks(runner, networks) {
588
+ async function ensureNetworks(ctx, networks) {
233
589
  for (const net of networks) {
234
590
  logAction("network", `Creating ${net}`);
235
- const exists = await runner.check("network:exists", net);
591
+ const exists = await ctx.check("network:exists", net);
236
592
  if (exists) {
237
593
  logSkip();
238
594
  continue;
239
595
  }
240
- await runner.run("network:create", net);
596
+ await ctx.run("network:create", net);
241
597
  logDone();
242
598
  }
243
599
  }
244
- async function ensureAppNetworks(runner, app, networks) {
245
- if (!networks || networks.length === 0) return;
246
- logAction(app, "Setting networks");
247
- await runner.run("network:set", app, "attach-post-deploy", ...networks);
248
- logDone();
249
- }
250
- async function ensureAppNetwork(runner, app, network) {
251
- if (!network) return;
252
- if (network.attach_post_create !== void 0 && network.attach_post_create !== false) {
253
- const nets = Array.isArray(network.attach_post_create) ? network.attach_post_create : [network.attach_post_create];
254
- await runner.run("network:set", app, "attach-post-create", ...nets);
255
- }
256
- if (network.initial_network !== void 0 && network.initial_network !== false) {
257
- await runner.run("network:set", app, "initial-network", network.initial_network);
258
- }
259
- if (network.bind_all_interfaces !== void 0) {
260
- await runner.run("network:set", app, "bind-all-interfaces", String(network.bind_all_interfaces));
261
- }
262
- if (network.tld !== void 0 && network.tld !== false) {
263
- await runner.run("network:set", app, "tld", network.tld);
264
- }
265
- }
266
600
  var DOCKER_BUILTIN_NETWORKS = /* @__PURE__ */ new Set(["bridge", "host", "none"]);
267
- async function exportNetworks(runner) {
268
- const output = await runner.query("network:list");
601
+ async function exportNetworks(ctx) {
602
+ const output = await ctx.query("network:list");
269
603
  return output.split("\n").map((s) => s.trim()).filter((s) => s && !s.startsWith("=====>") && !DOCKER_BUILTIN_NETWORKS.has(s));
270
604
  }
271
- async function exportAppNetwork(runner, app) {
272
- const output = await runner.query("network:report", app);
273
- if (!output) return void 0;
274
- const result = {};
275
- const lines = output.split("\n");
276
- for (const line of lines) {
277
- const [key, ...valueParts] = line.split(":").map((s) => s.trim());
278
- const value = valueParts.join(":").trim();
279
- if (!key || !value) continue;
280
- if (key === "Network attach post deploy") {
281
- result.networks = value.split(" ").filter(Boolean);
282
- }
283
- }
284
- return Object.keys(result).length > 0 ? result : void 0;
285
- }
286
605
 
287
606
  // src/modules/services.ts
288
- 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) {
289
615
  for (const [name, config] of Object.entries(services)) {
290
616
  logAction("services", `Ensuring ${name}`);
291
- const exists = await runner.check(`${config.plugin}:exists`, name);
617
+ const exists = await ctx.check(`${config.plugin}:exists`, name);
292
618
  if (exists) {
293
619
  logSkip();
294
620
  continue;
295
621
  }
296
- 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);
626
+ logDone();
627
+ }
628
+ }
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}`);
297
653
  logDone();
298
654
  }
299
655
  }
300
- async function ensureAppLinks(runner, app, desiredLinks, allServices) {
656
+ async function ensureAppLinks(ctx, app, desiredLinks, allServices) {
301
657
  const desiredSet = new Set(desiredLinks);
302
658
  for (const [serviceName, serviceConfig] of Object.entries(allServices)) {
303
- const isLinked = await runner.check(`${serviceConfig.plugin}:linked`, serviceName, app);
659
+ const isLinked = await ctx.check(`${serviceConfig.plugin}:linked`, serviceName, app);
304
660
  const isDesired = desiredSet.has(serviceName);
305
661
  if (isDesired && !isLinked) {
306
662
  logAction(app, `Linking ${serviceName}`);
307
- await runner.run(`${serviceConfig.plugin}:link`, serviceName, app, "--no-restart");
663
+ await ctx.run(`${serviceConfig.plugin}:link`, serviceName, app, "--no-restart");
308
664
  logDone();
309
665
  } else if (!isDesired && isLinked) {
310
666
  logAction(app, `Unlinking ${serviceName}`);
311
- await runner.run(`${serviceConfig.plugin}:unlink`, serviceName, app, "--no-restart");
667
+ await ctx.run(`${serviceConfig.plugin}:unlink`, serviceName, app, "--no-restart");
312
668
  logDone();
313
669
  }
314
670
  }
315
671
  }
316
- async function destroyAppLinks(runner, app, links, allServices) {
672
+ async function destroyAppLinks(ctx, app, links, allServices) {
317
673
  for (const serviceName of links) {
318
674
  const config = allServices[serviceName];
319
675
  if (!config) continue;
320
- const isLinked = await runner.check(`${config.plugin}:linked`, serviceName, app);
676
+ const isLinked = await ctx.check(`${config.plugin}:linked`, serviceName, app);
321
677
  if (isLinked) {
322
- await runner.run(`${config.plugin}:unlink`, serviceName, app, "--no-restart");
678
+ await ctx.run(`${config.plugin}:unlink`, serviceName, app, "--no-restart");
323
679
  }
324
680
  }
325
681
  }
326
- async function destroyServices(runner, services) {
682
+ async function destroyServices(ctx, services) {
327
683
  for (const [name, config] of Object.entries(services)) {
328
684
  logAction("services", `Destroying ${name}`);
329
- const exists = await runner.check(`${config.plugin}:exists`, name);
685
+ const exists = await ctx.check(`${config.plugin}:exists`, name);
330
686
  if (!exists) {
331
687
  logSkip();
332
688
  continue;
333
689
  }
334
- await runner.run(`${config.plugin}:destroy`, name, "--force");
690
+ await ctx.run(`${config.plugin}:destroy`, name, "--force");
335
691
  logDone();
336
692
  }
337
693
  }
338
- async function exportServices(_runner) {
694
+ async function exportServices(_ctx) {
339
695
  return {};
340
696
  }
341
- async function exportAppLinks(runner, app, services) {
697
+ async function exportAppLinks(ctx, app, services) {
342
698
  const linked = [];
343
699
  for (const [serviceName, config] of Object.entries(services)) {
344
- const isLinked = await runner.check(`${config.plugin}:linked`, serviceName, app);
700
+ const isLinked = await ctx.check(`${config.plugin}:linked`, serviceName, app);
345
701
  if (isLinked) linked.push(serviceName);
346
702
  }
347
703
  return linked;
348
704
  }
349
705
 
350
- // src/modules/proxy.ts
351
- async function ensureAppProxy(runner, app, enabled) {
352
- logAction(app, `Setting proxy enabled=${enabled}`);
353
- const currentRaw = await runner.query("proxy:report", app, "--proxy-enabled");
354
- const current = currentRaw.trim() === "true";
355
- if (current === enabled) {
356
- logSkip();
357
- return;
358
- }
359
- if (enabled) {
360
- 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");
361
711
  } else {
362
- await runner.run("proxy:disable", app);
712
+ await ctx.run("domains:set-global", ...domains);
363
713
  }
364
714
  logDone();
365
715
  }
366
- async function exportAppProxy(runner, app) {
367
- const raw = await runner.query("proxy:report", app, "--proxy-enabled");
368
- const enabled = raw.trim() === "true";
369
- if (enabled) return void 0;
370
- return { enabled };
371
- }
372
716
 
373
- // src/modules/ports.ts
374
- async function ensureAppPorts(runner, app, ports) {
375
- logAction(app, "Configuring ports");
376
- const currentRaw = await runner.query("ports:report", app, "--ports-map");
377
- const current = currentRaw.split(/\s+/).map((s) => s.trim()).filter(Boolean).sort();
378
- const desired = [...ports].sort();
379
- if (JSON.stringify(current) === JSON.stringify(desired)) {
380
- logSkip();
381
- return;
382
- }
383
- await runner.run("ports:set", app, ...ports);
384
- logDone();
385
- }
386
- async function exportAppPorts(runner, app) {
387
- const raw = await runner.query("ports:report", app, "--ports-map");
388
- const ports = raw.split(/\s+/).map((s) => s.trim()).filter(Boolean);
389
- if (ports.length === 0) return void 0;
390
- 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);
391
722
  }
392
723
 
393
- // src/modules/certs.ts
394
- async function ensureAppCerts(runner, app, ssl) {
395
- logAction(app, "Configuring SSL");
396
- const enabledRaw = await runner.query("certs:report", app, "--ssl-enabled");
397
- const enabled = enabledRaw.trim() === "true";
398
- if (ssl === false) {
399
- if (!enabled) {
400
- logSkip();
401
- return;
402
- }
403
- await runner.run("certs:remove", app);
404
- logDone();
405
- return;
406
- }
407
- if (ssl === true) {
408
- if (enabled) {
409
- logSkip();
410
- return;
411
- }
412
- logSkip();
413
- return;
414
- }
415
- if (enabled) {
416
- logSkip();
417
- return;
418
- }
419
- await runner.run("certs:add", app, ssl.certfile, ssl.keyfile);
420
- logDone();
421
- }
422
- async function exportAppCerts(runner, app) {
423
- const raw = await runner.query("certs:report", app, "--ssl-enabled");
424
- const enabled = raw.trim() === "true";
425
- if (!enabled) return void 0;
426
- return true;
427
- }
428
-
429
- // src/modules/storage.ts
430
- async function ensureAppStorage(runner, app, storage) {
431
- logAction(app, "Configuring storage");
432
- const currentRaw = await runner.query("storage:report", app, "--storage-mounts");
433
- const current = currentRaw.split("\n").map((s) => s.trim()).filter(Boolean);
434
- const desired = new Set(storage);
435
- const currentSet = new Set(current);
436
- const toUnmount = current.filter((m) => !desired.has(m));
437
- const toMount = storage.filter((m) => !currentSet.has(m));
438
- if (toUnmount.length === 0 && toMount.length === 0) {
439
- logSkip();
440
- return;
441
- }
442
- for (const mount of toUnmount) {
443
- await runner.run("storage:unmount", app, mount);
444
- }
445
- for (const mount of toMount) {
446
- 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));
447
728
  }
448
- logDone();
449
- }
450
- async function exportAppStorage(runner, app) {
451
- const raw = await runner.query("storage:report", app, "--storage-mounts");
452
- const mounts = raw.split("\n").map((s) => s.trim()).filter(Boolean);
453
- if (mounts.length === 0) return void 0;
454
- return mounts;
455
729
  }
456
730
 
457
731
  // src/modules/nginx.ts
458
- async function ensureAppNginx(runner, app, nginx) {
459
- for (const [key, value] of Object.entries(nginx)) {
460
- await runner.run("nginx:set", app, key, String(value));
461
- }
462
- }
463
- async function ensureGlobalNginx(runner, nginx) {
732
+ async function ensureGlobalNginx(ctx, nginx) {
464
733
  for (const [key, value] of Object.entries(nginx)) {
465
- await runner.run("nginx:set", "--global", key, String(value));
734
+ await ctx.run("nginx:set", "--global", key, String(value));
466
735
  }
467
736
  }
468
- async function exportAppNginx(runner, app) {
469
- const raw = await runner.query("nginx:report", app);
470
- if (!raw) return void 0;
471
- const result = {};
472
- for (const line of raw.split("\n")) {
473
- const match = line.match(/^\s*Nginx\s+(.+?):\s*(.+?)\s*$/);
474
- if (match) {
475
- const key = match[1].toLowerCase().replace(/\s+/g, "-");
476
- if (key.startsWith("computed-") || key.startsWith("global-") || key === "last-visited-at") continue;
477
- const value = match[2].trim();
478
- if (!value) continue;
479
- result[key] = value;
480
- }
481
- }
482
- return Object.keys(result).length > 0 ? result : void 0;
483
- }
484
-
485
- // src/modules/checks.ts
486
- async function ensureAppChecks(runner, app, checks) {
487
- logAction(app, "Configuring checks");
488
- if (checks === false) {
489
- await runner.run("checks:disable", app);
490
- logDone();
491
- return;
492
- }
493
- if (checks.disabled && checks.disabled.length > 0) {
494
- await runner.run("checks:disable", app, ...checks.disabled);
495
- }
496
- if (checks.skipped && checks.skipped.length > 0) {
497
- await runner.run("checks:skip", app, ...checks.skipped);
498
- }
499
- for (const [key, value] of Object.entries(checks)) {
500
- if (key === "disabled" || key === "skipped") continue;
501
- await runner.run("checks:set", app, key, String(value));
502
- }
503
- logDone();
504
- }
505
- async function exportAppChecks(runner, app) {
506
- const raw = await runner.query("checks:report", app);
507
- if (!raw) return void 0;
508
- return void 0;
509
- }
510
-
511
- // src/modules/logs.ts
512
- async function ensureAppLogs(runner, app, logs) {
513
- for (const [key, value] of Object.entries(logs)) {
514
- await runner.run("logs:set", app, key, String(value));
515
- }
516
- }
517
- async function ensureGlobalLogs(runner, logs) {
518
- for (const [key, value] of Object.entries(logs)) {
519
- await runner.run("logs:set", "--global", key, String(value));
520
- }
521
- }
522
- async function exportAppLogs(runner, app) {
523
- const raw = await runner.query("logs:report", app);
524
- if (!raw) return void 0;
525
- return void 0;
526
- }
527
737
 
528
- // src/modules/registry.ts
529
- async function ensureAppRegistry(runner, app, registry) {
530
- for (const [key, value] of Object.entries(registry)) {
531
- 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);
532
772
  }
533
773
  }
534
- async function exportAppRegistry(runner, app) {
535
- const raw = await runner.query("registry:report", app);
536
- if (!raw) return void 0;
537
- return void 0;
538
- }
539
774
 
540
- // src/modules/scheduler.ts
541
- async function ensureAppScheduler(runner, app, scheduler) {
542
- logAction(app, `Setting scheduler to ${scheduler}`);
543
- const current = await runner.query("scheduler:report", app, "--scheduler-selected");
544
- 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) {
545
780
  logSkip();
546
781
  return;
547
782
  }
548
- await runner.run("scheduler:set", app, "selected", scheduler);
783
+ await ctx.run("apps:destroy", app, "--force");
549
784
  logDone();
550
785
  }
551
- async function exportAppScheduler(runner, app) {
552
- const raw = await runner.query("scheduler:report", app, "--scheduler-selected");
553
- const scheduler = raw.trim();
554
- if (!scheduler || scheduler === "docker-local") return void 0;
555
- return scheduler;
556
- }
557
-
558
- // src/modules/config.ts
559
- var MANAGED_KEYS_VAR = "DOKKU_COMPOSE_MANAGED_KEYS";
560
- async function ensureAppConfig(runner, app, env) {
561
- logAction(app, "Configuring env vars");
562
- if (env === false) {
563
- logSkip();
564
- return;
565
- }
566
- const prevManagedRaw = await runner.query("config:get", app, MANAGED_KEYS_VAR);
567
- const prevManaged = prevManagedRaw.trim() ? prevManagedRaw.trim().split(",").filter(Boolean) : [];
568
- const desiredKeys = Object.keys(env);
569
- const toUnset = prevManaged.filter((k) => !desiredKeys.includes(k));
570
- if (toUnset.length > 0) {
571
- await runner.run("config:unset", "--no-restart", app, ...toUnset);
572
- }
573
- const pairs = Object.entries(env).map(([k, v]) => `${k}=${v}`);
574
- const newManagedKeys = desiredKeys.join(",");
575
- await runner.run(
576
- "config:set",
577
- "--no-restart",
578
- app,
579
- ...pairs,
580
- `${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("=====>")
581
790
  );
582
- logDone();
583
- }
584
- async function ensureGlobalConfig(runner, env) {
585
- if (env === false) return;
586
- const pairs = Object.entries(env).map(([k, v]) => `${k}=${v}`);
587
- await runner.run("config:set", "--global", ...pairs);
588
- }
589
- async function exportAppConfig(runner, app) {
590
- const raw = await runner.query("config:export", app, "--format", "shell");
591
- if (!raw) return void 0;
592
- const result = {};
593
- for (const line of raw.split("\n")) {
594
- const match = line.match(/^export\s+(\w+)=['"]?(.*?)['"]?$/);
595
- if (match && match[1] !== MANAGED_KEYS_VAR) {
596
- result[match[1]] = match[2];
597
- }
598
- }
599
- return Object.keys(result).length > 0 ? result : void 0;
600
- }
601
-
602
- // src/modules/builder.ts
603
- async function ensureAppBuilder(runner, app, build) {
604
- if (build.dockerfile) {
605
- await runner.run("builder-dockerfile:set", app, "dockerfile-path", build.dockerfile);
606
- }
607
- if (build.app_json) {
608
- await runner.run("app-json:set", app, "appjson-path", build.app_json);
609
- }
610
- if (build.context) {
611
- await runner.run("builder:set", app, "build-dir", build.context);
612
- }
613
- if (build.args && Object.keys(build.args).length > 0) {
614
- for (const [key, value] of Object.entries(build.args)) {
615
- await runner.run("docker-options:add", app, "build", `--build-arg ${key}=${value}`);
616
- }
617
- }
618
- }
619
-
620
- // src/modules/docker-options.ts
621
- async function ensureAppDockerOptions(runner, app, options) {
622
- const phases = ["build", "deploy", "run"];
623
- for (const phase of phases) {
624
- const opts = options[phase];
625
- if (!opts || opts.length === 0) continue;
626
- await runner.run("docker-options:clear", app, phase);
627
- for (const opt of opts) {
628
- await runner.run("docker-options:add", app, phase, opt);
629
- }
630
- }
631
- }
632
-
633
- // src/commands/up.ts
634
- async function runUp(runner, config, appFilter) {
635
- const apps = appFilter.length > 0 ? appFilter : Object.keys(config.apps);
636
- if (config.plugins) await ensurePlugins(runner, config.plugins);
637
- if (config.domains !== void 0) await ensureGlobalDomains(runner, config.domains);
638
- if (config.env !== void 0) await ensureGlobalConfig(runner, config.env);
639
- if (config.logs !== void 0) await ensureGlobalLogs(runner, config.logs);
640
- if (config.nginx !== void 0) await ensureGlobalNginx(runner, config.nginx);
641
- if (config.networks) await ensureNetworks(runner, config.networks);
642
- if (config.services) await ensureServices(runner, config.services);
643
- for (const app of apps) {
644
- const appConfig = config.apps[app];
645
- if (!appConfig) continue;
646
- await ensureApp(runner, app);
647
- await ensureAppDomains(runner, app, appConfig.domains);
648
- if (config.services) await ensureAppLinks(runner, app, appConfig.links ?? [], config.services);
649
- await ensureAppNetworks(runner, app, appConfig.networks);
650
- await ensureAppNetwork(runner, app, appConfig.network);
651
- if (appConfig.proxy) await ensureAppProxy(runner, app, appConfig.proxy.enabled);
652
- if (appConfig.ports) await ensureAppPorts(runner, app, appConfig.ports);
653
- if (appConfig.ssl !== void 0) await ensureAppCerts(runner, app, appConfig.ssl);
654
- if (appConfig.storage) await ensureAppStorage(runner, app, appConfig.storage);
655
- if (appConfig.nginx) await ensureAppNginx(runner, app, appConfig.nginx);
656
- if (appConfig.checks !== void 0) await ensureAppChecks(runner, app, appConfig.checks);
657
- if (appConfig.logs) await ensureAppLogs(runner, app, appConfig.logs);
658
- if (appConfig.registry) await ensureAppRegistry(runner, app, appConfig.registry);
659
- if (appConfig.scheduler) await ensureAppScheduler(runner, app, appConfig.scheduler);
660
- if (appConfig.env !== void 0) await ensureAppConfig(runner, app, appConfig.env);
661
- if (appConfig.build) await ensureAppBuilder(runner, app, appConfig.build);
662
- if (appConfig.docker_options) await ensureAppDockerOptions(runner, app, appConfig.docker_options);
663
- }
664
791
  }
665
792
 
666
793
  // src/commands/down.ts
667
- async function runDown(runner, config, appFilter, opts) {
794
+ async function runDown(ctx, config, appFilter, opts) {
668
795
  const apps = appFilter.length > 0 ? appFilter : Object.keys(config.apps);
669
796
  for (const app of apps) {
670
797
  const appConfig = config.apps[app];
671
798
  if (!appConfig) continue;
672
799
  if (config.services && appConfig.links) {
673
- await destroyAppLinks(runner, app, appConfig.links, config.services);
800
+ await destroyAppLinks(ctx, app, appConfig.links, config.services);
674
801
  }
675
- await destroyApp(runner, app);
802
+ await destroyApp(ctx, app);
676
803
  }
677
804
  if (config.services) {
678
- await destroyServices(runner, config.services);
805
+ await destroyServices(ctx, config.services);
679
806
  }
680
807
  if (config.networks) {
681
808
  for (const net of config.networks) {
682
809
  logAction("network", `Destroying ${net}`);
683
- const exists = await runner.check("network:exists", net);
810
+ const exists = await ctx.check("network:exists", net);
684
811
  if (!exists) {
685
812
  logSkip();
686
813
  continue;
687
814
  }
688
- await runner.run("network:destroy", net, "--force");
815
+ await ctx.run("network:destroy", net, "--force");
689
816
  logDone();
690
817
  }
691
818
  }
692
819
  }
693
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
+
694
850
  // src/commands/export.ts
695
- async function runExport(runner, opts) {
851
+ async function runExport(ctx, opts) {
696
852
  const config = { apps: {} };
697
- const apps = opts.appFilter?.length ? opts.appFilter : await exportApps(runner);
698
- const networks = await exportNetworks(runner);
853
+ const versionOutput = await ctx.query("version");
854
+ const versionMatch = versionOutput.match(/(\d+\.\d+\.\d+)/);
855
+ if (versionMatch) config.dokku = { version: versionMatch[1] };
856
+ const apps = opts.appFilter?.length ? opts.appFilter : await exportApps(ctx);
857
+ const networks = await exportNetworks(ctx);
699
858
  if (networks.length > 0) config.networks = networks;
700
- const services = await exportServices(runner);
859
+ const services = await exportServices(ctx);
701
860
  if (Object.keys(services).length > 0) config.services = services;
702
861
  for (const app of apps) {
703
862
  const appConfig = {};
704
- const domains = await exportAppDomains(runner, app);
705
- if (domains !== void 0) appConfig.domains = domains;
706
- const links = await exportAppLinks(runner, app, services);
707
- if (links.length > 0) appConfig.links = links;
708
- const ports = await exportAppPorts(runner, app);
709
- if (ports?.length) appConfig.ports = ports;
710
- const proxy = await exportAppProxy(runner, app);
711
- if (proxy !== void 0) appConfig.proxy = proxy;
712
- const ssl = await exportAppCerts(runner, app);
713
- if (ssl !== void 0) appConfig.ssl = ssl;
714
- const storage = await exportAppStorage(runner, app);
715
- if (storage?.length) appConfig.storage = storage;
716
- const nginx = await exportAppNginx(runner, app);
717
- if (nginx && Object.keys(nginx).length) appConfig.nginx = nginx;
718
- const checks = await exportAppChecks(runner, app);
719
- if (checks !== void 0) appConfig.checks = checks;
720
- const logs = await exportAppLogs(runner, app);
721
- if (logs && Object.keys(logs).length) appConfig.logs = logs;
722
- const registry = await exportAppRegistry(runner, app);
723
- if (registry && Object.keys(registry).length) appConfig.registry = registry;
724
- const scheduler = await exportAppScheduler(runner, app);
725
- if (scheduler) appConfig.scheduler = scheduler;
726
- const networkCfg = await exportAppNetwork(runner, app);
727
- if (networkCfg?.networks?.length) appConfig.networks = networkCfg.networks;
728
- if (networkCfg?.network) appConfig.network = networkCfg.network;
729
- const env = await exportAppConfig(runner, app);
730
- 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
+ }
731
880
  config.apps[app] = appConfig;
732
881
  }
733
882
  return config;
@@ -735,52 +884,38 @@ async function runExport(runner, opts) {
735
884
 
736
885
  // src/commands/diff.ts
737
886
  import chalk2 from "chalk";
738
- function computeDiff(desired, current) {
887
+ async function computeDiff(ctx, config) {
739
888
  const result = { apps: {}, services: {}, inSync: true };
740
- for (const [app, desiredApp] of Object.entries(desired.apps)) {
741
- const currentApp = current.apps[app] ?? {};
889
+ for (const [app, appConfig] of Object.entries(config.apps)) {
742
890
  const appDiff = {};
743
- const features = [
744
- "domains",
745
- "ports",
746
- "env",
747
- "ssl",
748
- "storage",
749
- "nginx",
750
- "logs",
751
- "registry",
752
- "scheduler",
753
- "checks",
754
- "networks",
755
- "proxy",
756
- "links"
757
- ];
758
- for (const feature of features) {
759
- const d = desiredApp[feature];
760
- const c = currentApp[feature];
761
- if (d === void 0) continue;
762
- const dStr = JSON.stringify(d);
763
- const cStr = JSON.stringify(c);
764
- if (c === void 0) {
765
- appDiff[feature] = { status: "missing", desired: d, current: void 0 };
766
- result.inSync = false;
767
- } else if (dStr !== cStr) {
768
- 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 };
769
907
  result.inSync = false;
770
908
  } else {
771
- appDiff[feature] = { status: "in-sync", desired: d, current: c };
909
+ appDiff[resource.key] = { status: "changed", desired, current };
910
+ result.inSync = false;
772
911
  }
773
912
  }
774
913
  result.apps[app] = appDiff;
775
914
  }
776
- for (const [svc] of Object.entries(desired.services ?? {})) {
777
- const exists = current.services?.[svc];
778
- if (!exists) {
779
- result.services[svc] = { status: "missing" };
780
- result.inSync = false;
781
- } else {
782
- result.services[svc] = { status: "in-sync" };
783
- }
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;
784
919
  }
785
920
  return result;
786
921
  }
@@ -893,7 +1028,9 @@ function validate(filePath) {
893
1028
  }
894
1029
 
895
1030
  // src/index.ts
896
- var program = new Command().name("dokku-compose").version("0.3.0");
1031
+ var require2 = createRequire(import.meta.url);
1032
+ var { version } = require2("../package.json");
1033
+ var program = new Command().name("dokku-compose").version(version);
897
1034
  function makeRunner(opts) {
898
1035
  return createRunner({
899
1036
  host: process.env.DOKKU_HOST,
@@ -903,10 +1040,15 @@ function makeRunner(opts) {
903
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) => {
904
1041
  const config = loadConfig(opts.file);
905
1042
  const runner = makeRunner(opts);
906
- await runUp(runner, config, apps);
907
- if (opts.dryRun) {
908
- console.log("\n# Commands that would run:");
909
- for (const cmd of runner.dryRunLog) console.log(`dokku ${cmd}`);
1043
+ const ctx = createContext(runner);
1044
+ try {
1045
+ await runUp(ctx, config, apps);
1046
+ if (opts.dryRun) {
1047
+ console.log("\n# Commands that would run:");
1048
+ for (const cmd of runner.dryRunLog) console.log(`dokku ${cmd}`);
1049
+ }
1050
+ } finally {
1051
+ await ctx.close();
910
1052
  }
911
1053
  });
912
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) => {
@@ -916,7 +1058,12 @@ program.command("down [apps...]").description("Destroy apps and services (requir
916
1058
  }
917
1059
  const config = loadConfig(opts.file);
918
1060
  const runner = makeRunner({});
919
- await runDown(runner, config, apps, { force: true });
1061
+ const ctx = createContext(runner);
1062
+ try {
1063
+ await runDown(ctx, config, apps, { force: true });
1064
+ } finally {
1065
+ await ctx.close();
1066
+ }
920
1067
  });
921
1068
  program.command("validate [file]").description("Validate dokku-compose.yml without touching the server").action((file = "dokku-compose.yml") => {
922
1069
  const result = validate(file);
@@ -936,33 +1083,44 @@ ${result.errors.length} error(s), ${result.warnings.length} warning(s)`);
936
1083
  });
937
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) => {
938
1085
  const runner = makeRunner({});
939
- const result = await runExport(runner, {
940
- appFilter: opts.app ? [opts.app] : void 0
941
- });
942
- const out = yaml3.dump(result, { lineWidth: 120 });
943
- if (opts.output) {
944
- fs3.writeFileSync(opts.output, out);
945
- console.error(`Written to ${opts.output}`);
946
- } else {
947
- process.stdout.write(out);
1086
+ const ctx = createContext(runner);
1087
+ try {
1088
+ const result = await runExport(ctx, {
1089
+ appFilter: opts.app ? [opts.app] : void 0
1090
+ });
1091
+ const out = yaml3.dump(result, { lineWidth: 120 });
1092
+ if (opts.output) {
1093
+ fs3.writeFileSync(opts.output, out);
1094
+ console.error(`Written to ${opts.output}`);
1095
+ } else {
1096
+ process.stdout.write(out);
1097
+ }
1098
+ } finally {
1099
+ await ctx.close();
948
1100
  }
949
1101
  });
950
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) => {
951
1103
  const desired = loadConfig(opts.file);
952
1104
  const runner = makeRunner({});
953
- const current = await runExport(runner, {
954
- appFilter: Object.keys(desired.apps)
955
- });
956
- const diff = computeDiff(desired, current);
957
- const output = opts.verbose ? formatVerbose(diff) : formatSummary(diff);
958
- process.stdout.write(output);
959
- process.exit(diff.inSync ? 0 : 1);
1105
+ const ctx = createContext(runner);
1106
+ try {
1107
+ const diff = await computeDiff(ctx, desired);
1108
+ const output = opts.verbose ? formatVerbose(diff) : formatSummary(diff);
1109
+ process.stdout.write(output);
1110
+ process.exit(diff.inSync ? 0 : 1);
1111
+ } finally {
1112
+ await ctx.close();
1113
+ }
960
1114
  });
961
1115
  program.command("ps [apps...]").description("Show status of configured apps").option("-f, --file <path>", "Config file", "dokku-compose.yml").action(async (apps, opts) => {
962
1116
  const config = loadConfig(opts.file);
963
1117
  const runner = makeRunner({});
964
- const { runPs } = await import("./ps-33II4UU3.js");
965
- await runPs(runner, config, apps);
1118
+ try {
1119
+ const { runPs } = await import("./ps-33II4UU3.js");
1120
+ await runPs(runner, config, apps);
1121
+ } finally {
1122
+ await runner.close();
1123
+ }
966
1124
  });
967
1125
  program.command("init [apps...]").description("Create a starter dokku-compose.yml").option("-f, --file <path>", "Config file", "dokku-compose.yml").action(async (apps, opts) => {
968
1126
  const { runInit } = await import("./init-GIXEVLNW.js");