@kaupang/cli 0.1.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 (3) hide show
  1. package/README.md +83 -0
  2. package/dist/cli.js +668 -0
  3. package/package.json +39 -0
package/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # @kaupang/cli
2
+
3
+ [![npm](https://img.shields.io/npm/v/@kaupang/cli.svg)](https://www.npmjs.com/package/@kaupang/cli)
4
+ [![CI](https://github.com/kaupang-dev/kaupang/actions/workflows/ci.yml/badge.svg)](https://github.com/kaupang-dev/kaupang/actions/workflows/ci.yml)
5
+ [![license](https://img.shields.io/npm/l/@kaupang/cli.svg)](https://github.com/kaupang-dev/kaupang/blob/main/LICENSE)
6
+
7
+ > The **`kaupang`** CLI — spin up environments on Docker Compose, Docker Swarm, or
8
+ > Kubernetes from a single config, the same command on your laptop and in CI.
9
+
10
+ kaupang is an imperative, push-based deploy tool — an *environment maker*, not a
11
+ reconciler. You run it, it makes a target match your config, records exactly what it
12
+ did, and lets you replay or roll back. No control loop, no cluster agent.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm install -g @kaupang/cli # installs the `kaupang` command
18
+ # or run without installing:
19
+ npx @kaupang/cli up api --target staging
20
+ ```
21
+
22
+ ## Quick start
23
+
24
+ Create a `kaupang.config.ts` at your repo root and an `environments/` folder:
25
+
26
+ ```ts
27
+ // kaupang.config.ts
28
+ import { defineConfig } from "@kaupang/core";
29
+
30
+ export default defineConfig({
31
+ environments: "./environments",
32
+ project: "shop",
33
+ dockerRepository: "ghcr.io/acme",
34
+ });
35
+ ```
36
+
37
+ ```ts
38
+ // environments/api.ts
39
+ import { defineEnvironment } from "@kaupang/core";
40
+
41
+ export default defineEnvironment({
42
+ services: {
43
+ web: { image: "shop-api", ports: ["8080:3000"] },
44
+ },
45
+ });
46
+ ```
47
+
48
+ Then:
49
+
50
+ ```bash
51
+ kaupang up api # resolve images → pin digests → deploy → record
52
+ kaupang up api --dry-run # show the dependency graph + commands, run nothing
53
+ kaupang down api # tear it down
54
+ kaupang rollback api # re-apply the previous good deployment
55
+ ```
56
+
57
+ The authoring helpers (`defineConfig`, `defineEnvironment`, `secret`, `use`, …) come
58
+ from [`@kaupang/core`](https://www.npmjs.com/package/@kaupang/core), which this package
59
+ depends on.
60
+
61
+ ## Commands
62
+
63
+ | Command | What it does |
64
+ | --- | --- |
65
+ | `kaupang up <env \| --solution \| --bundle>` | Deploy to a target |
66
+ | `kaupang down <env>` | Tear down (optionally `--with-deps`) |
67
+ | `kaupang build <env> [--push]` | Build (and push) images |
68
+ | `kaupang bundle <solution> [--push oci://…]` | Pack a portable, pinned bundle |
69
+ | `kaupang rollback <env> [--to <id>]` | Re-apply a recorded deployment |
70
+ | `kaupang run <pipeline>` | Run a pipeline (run / up / down / build / wait steps) |
71
+
72
+ Add `--dry-run` to preview without touching anything, and `-v` / `--verbose` to print the
73
+ underlying `docker`/`kubectl` commands.
74
+
75
+ ## Docs
76
+
77
+ Full documentation — config, catalog presets, targets, solutions & bundles, pipelines,
78
+ secrets, backends, Azure DevOps — lives in the
79
+ [project README](https://github.com/kaupang-dev/kaupang#readme).
80
+
81
+ ## License
82
+
83
+ MIT © Andreas Quist Batista
package/dist/cli.js ADDED
@@ -0,0 +1,668 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { defineCommand as defineCommand7, runMain } from "citty";
5
+
6
+ // src/commands/build.ts
7
+ import { mkdirSync, writeFileSync } from "node:fs";
8
+ import { dirname } from "node:path";
9
+ import { defineCommand } from "citty";
10
+ import { consola } from "consola";
11
+ import { backendNames, getBackend } from "@kaupang/core/internal";
12
+ import { loadConfig } from "@kaupang/core/internal";
13
+ import { makeContext } from "@kaupang/core/internal";
14
+ import { resolvePlan } from "@kaupang/core/internal";
15
+ import { run } from "@kaupang/core/internal";
16
+
17
+ // src/commands/shared.ts
18
+ import { setVerbose } from "@kaupang/core/internal";
19
+ var verboseArg = {
20
+ verbose: {
21
+ type: "boolean",
22
+ alias: "v",
23
+ description: "Print each underlying command ($ docker \u2026) as it runs."
24
+ }
25
+ };
26
+ function applyVerbose(args) {
27
+ setVerbose(Boolean(args.verbose));
28
+ }
29
+
30
+ // src/commands/build.ts
31
+ var buildCommand = defineCommand({
32
+ meta: {
33
+ name: "build",
34
+ description: "Build images for services that declare a build context."
35
+ },
36
+ args: {
37
+ environment: {
38
+ type: "positional",
39
+ description: "Environment whose services to build.",
40
+ required: true
41
+ },
42
+ backend: { type: "string", alias: "b", description: backendNames.join(" | ") },
43
+ push: { type: "boolean", description: "Push built images to the registry after building." },
44
+ "dry-run": { type: "boolean", description: "Print build commands without running them." },
45
+ ...verboseArg,
46
+ cwd: { type: "string", description: "Directory to resolve the config from." }
47
+ },
48
+ async run({ args }) {
49
+ applyVerbose(args);
50
+ const loaded = await loadConfig(args.cwd);
51
+ const backendName = args.backend ?? loaded.config.defaultBackend ?? "compose";
52
+ if (backendName === "kubernetes") {
53
+ consola.warn(
54
+ "The kubernetes backend does not build images \u2014 build + push with your CI, then deploy by image."
55
+ );
56
+ return;
57
+ }
58
+ const plan = resolvePlan(args.environment, loaded.environments, backendName);
59
+ const ctx = makeContext(loaded);
60
+ const backend = getBackend(backendName);
61
+ const env = plan.environments.find((e) => e.name === plan.target);
62
+ const hasBuild = Object.values(env.services).some((s) => s.build);
63
+ if (!hasBuild) {
64
+ consola.info(`No services in "${env.name}" declare a build context \u2014 nothing to build.`);
65
+ return;
66
+ }
67
+ const m = backend.materialize(env, ctx);
68
+ if (!args["dry-run"]) {
69
+ for (const f of m.files) {
70
+ mkdirSync(dirname(f.path), { recursive: true });
71
+ writeFileSync(f.path, f.content);
72
+ }
73
+ }
74
+ consola.start(`\u2692\uFE0F forging ${env.name}`);
75
+ for (const action of m.build) {
76
+ await run(action.file, action.args, { cwd: ctx.rootDir, dryRun: args["dry-run"] });
77
+ }
78
+ if (args.push) {
79
+ const pushActions = m.push ?? [];
80
+ if (pushActions.length === 0) {
81
+ consola.warn(`The ${backendName} backend can't push \u2014 build + push with your CI instead.`);
82
+ } else {
83
+ consola.start(`\u26F5 shipping ${env.name}`);
84
+ for (const action of pushActions) {
85
+ await run(action.file, action.args, { cwd: ctx.rootDir, dryRun: args["dry-run"] });
86
+ }
87
+ }
88
+ }
89
+ consola.success(`\u2692\uFE0F Forged "${env.name}"${args.push ? " and shipped" : ""}.`);
90
+ }
91
+ });
92
+
93
+ // src/commands/bundle.ts
94
+ import { basename, join, resolve } from "node:path";
95
+ import { mkdirSync as mkdirSync2 } from "node:fs";
96
+ import { defineCommand as defineCommand2 } from "citty";
97
+ import { consola as consola2 } from "consola";
98
+ import { getBackend as getBackend2 } from "@kaupang/core/internal";
99
+ import { loadConfig as loadConfig2 } from "@kaupang/core/internal";
100
+ import { makeContext as makeContext2 } from "@kaupang/core/internal";
101
+ import { resolveMultiPlan } from "@kaupang/core/internal";
102
+ import { resolveEnvironmentImages } from "@kaupang/core/internal";
103
+ import { applyPins, resolveSolution } from "@kaupang/core/internal";
104
+ import {
105
+ pushBundle,
106
+ rewriteActionPath,
107
+ writeBundle
108
+ } from "@kaupang/core/internal";
109
+ import { resolveTarget } from "@kaupang/core/internal";
110
+ import { stackName } from "@kaupang/core/internal";
111
+ import { mergeEnv } from "@kaupang/core/internal";
112
+ import { hasBinary, run as run2 } from "@kaupang/core/internal";
113
+ var bundleCommand = defineCommand2({
114
+ meta: {
115
+ name: "bundle",
116
+ description: "Materialize a solution into a portable, pinned bundle."
117
+ },
118
+ args: {
119
+ solution: { type: "positional", description: "Solution name.", required: true },
120
+ target: { type: "string", description: 'Target the bundle is built for (default "local").' },
121
+ output: { type: "string", alias: "o", description: "Output directory for the bundle." },
122
+ "with-images": {
123
+ type: "boolean",
124
+ description: "docker save each pinned image into the bundle (for airgapped transfer)."
125
+ },
126
+ push: {
127
+ type: "string",
128
+ description: "Also push the bundle to an OCI registry, e.g. oci://acmeregistry.azurecr.io/bundles/x:1."
129
+ },
130
+ resolve: {
131
+ type: "boolean",
132
+ default: true,
133
+ description: "Resolve images to digests (--no-resolve to keep tags as-is)."
134
+ },
135
+ cwd: { type: "string", description: "Directory to resolve the config from." }
136
+ },
137
+ async run({ args }) {
138
+ const loaded = await loadConfig2(args.cwd);
139
+ const sol = await resolveSolution(loaded.config, loaded.catalog, args.solution);
140
+ const targetName = args.target ?? sol.target ?? "local";
141
+ const rt = resolveTarget(loaded.config, targetName, loaded.rootDir);
142
+ const backendName = rt.backend ?? loaded.config.defaultBackend ?? "compose";
143
+ const plan = resolveMultiPlan(sol.environments, sol.name, loaded.environments, backendName);
144
+ applyPins(plan, sol.pins);
145
+ const ctx = makeContext2(loaded, { targetEnv: mergeEnv(rt.env, sol.env) });
146
+ const backend = getBackend2(backendName);
147
+ const outDir = resolve(loaded.rootDir, args.output ?? `${sol.name}-bundle`);
148
+ const tars = [];
149
+ const environments = [];
150
+ consola2.info(
151
+ `\u{1F4E6} Packing cargo "${sol.name}"${sol.version ? `@${sol.version}` : ""} for ${targetName} (${backendName})`
152
+ );
153
+ for (const env of plan.environments) {
154
+ let deployEnv = env;
155
+ let images = [];
156
+ if (args.resolve) {
157
+ const r = await resolveEnvironmentImages(env);
158
+ deployEnv = r.env;
159
+ images = r.images;
160
+ }
161
+ const m = backend.materialize(deployEnv, ctx);
162
+ const file = m.files[0];
163
+ const relPath = join("artifacts", stackName(loaded.project, env.name), basename(file.path));
164
+ environments.push({
165
+ name: env.name,
166
+ backend: backendName,
167
+ images,
168
+ artifact: { relPath, content: file.content },
169
+ up: m.up.map((a) => rewriteActionPath(a, file.path, relPath)),
170
+ down: m.down.map((a) => rewriteActionPath(a, file.path, relPath))
171
+ });
172
+ }
173
+ if (args["with-images"]) {
174
+ if (!await hasBinary("docker")) {
175
+ consola2.warn("docker not found \u2014 skipping image export (bundle has no tars).");
176
+ } else {
177
+ mkdirSync2(join(outDir, "images"), { recursive: true });
178
+ const unique = [...new Set(environments.flatMap((e) => e.images.map((i) => i.pinned)))];
179
+ for (const ref of unique) {
180
+ const tar = join("images", `${ref.replace(/[^a-zA-Z0-9]+/g, "_")}.tar`);
181
+ consola2.start(`\u{1F4E6} stowing image ${ref}`);
182
+ await run2("docker", ["save", "-o", join(outDir, tar), ref], { cwd: loaded.rootDir });
183
+ tars.push(tar);
184
+ }
185
+ }
186
+ }
187
+ const manifest = {
188
+ kaupang: "0.1.0",
189
+ solution: sol.name,
190
+ version: sol.version,
191
+ target: targetName,
192
+ project: loaded.project,
193
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
194
+ images: { included: tars.length > 0, tars },
195
+ environments
196
+ };
197
+ writeBundle(outDir, manifest);
198
+ consola2.success(
199
+ `\u{1F4E6} Cargo packed at ${outDir} (${environments.length} environment(s)${manifest.images.included ? `, ${tars.length} image tar(s)` : ", no images"}).`
200
+ );
201
+ if (args.push) {
202
+ consola2.start(`\u26F5 ferrying cargo \u2192 ${args.push}`);
203
+ await pushBundle(outDir, args.push);
204
+ consola2.success(`cargo delivered to ${args.push}`);
205
+ }
206
+ }
207
+ });
208
+
209
+ // src/commands/down.ts
210
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "node:fs";
211
+ import { dirname as dirname2 } from "node:path";
212
+ import { defineCommand as defineCommand3 } from "citty";
213
+ import { consola as consola3 } from "consola";
214
+ import { backendNames as backendNames2, getBackend as getBackend3 } from "@kaupang/core/internal";
215
+ import { loadConfig as loadConfig3 } from "@kaupang/core/internal";
216
+ import { makeContext as makeContext3 } from "@kaupang/core/internal";
217
+ import { resolvePlan as resolvePlan2 } from "@kaupang/core/internal";
218
+ import { latestSuccessful } from "@kaupang/core/internal";
219
+ import { applyTarget, resolveTarget as resolveTarget2 } from "@kaupang/core/internal";
220
+ import { run as run3 } from "@kaupang/core/internal";
221
+ import { runHooks } from "@kaupang/core/internal";
222
+ var downCommand = defineCommand3({
223
+ meta: {
224
+ name: "down",
225
+ description: "Tear down an environment (optionally its dependencies too)."
226
+ },
227
+ args: {
228
+ environment: {
229
+ type: "positional",
230
+ description: "Environment to tear down.",
231
+ required: true
232
+ },
233
+ backend: {
234
+ type: "string",
235
+ alias: "b",
236
+ description: `Backend: ${backendNames2.join(" | ")}. Defaults to the cached run's backend.`
237
+ },
238
+ "with-deps": {
239
+ type: "boolean",
240
+ description: "Also tear down dependencies (reverse start order). Off by default."
241
+ },
242
+ target: { type: "string", description: 'Deployment target (default "local").' },
243
+ "dry-run": {
244
+ type: "boolean",
245
+ description: "Print the teardown commands without running them."
246
+ },
247
+ ...verboseArg,
248
+ cwd: { type: "string", description: "Directory to resolve the config from." }
249
+ },
250
+ async run({ args }) {
251
+ applyVerbose(args);
252
+ const loaded = await loadConfig3(args.cwd);
253
+ const target = args.target ?? process.env.KAUPANG_TARGET ?? "local";
254
+ const rt = resolveTarget2(loaded.config, target, loaded.rootDir);
255
+ const cached = latestSuccessful(loaded.cacheDir, args.environment, target);
256
+ const backendName = args.backend ?? cached?.backend ?? rt.backend ?? loaded.config.defaultBackend ?? "compose";
257
+ const plan = resolvePlan2(args.environment, loaded.environments, backendName);
258
+ const ctx = makeContext3(loaded, { targetEnv: rt.env });
259
+ const backend = getBackend3(backendName);
260
+ const targets = args["with-deps"] ? [...plan.environments].reverse() : plan.environments.filter((e) => e.name === plan.target);
261
+ consola3.info(
262
+ `\u{1F525} Striking camp: ${targets.map((e) => e.name).join(", ")} via ${backendName}` + (args["dry-run"] ? " (dry-run)" : "")
263
+ );
264
+ for (const env of targets) {
265
+ const m = backend.materialize(env, ctx);
266
+ consola3.start(`\u{1F525} striking ${env.name}`);
267
+ await runHooks(env.hooks?.beforeDown, {
268
+ rootDir: ctx.rootDir,
269
+ dryRun: args["dry-run"]
270
+ });
271
+ if (!args["dry-run"]) {
272
+ for (const f of m.files) {
273
+ mkdirSync3(dirname2(f.path), { recursive: true });
274
+ writeFileSync2(f.path, f.content);
275
+ }
276
+ }
277
+ for (const action of m.down) {
278
+ const a = applyTarget(action, rt);
279
+ await run3(a.file, a.args, {
280
+ cwd: ctx.rootDir,
281
+ dryRun: args["dry-run"],
282
+ input: a.input,
283
+ env: a.env
284
+ });
285
+ }
286
+ await runHooks(env.hooks?.afterDown, {
287
+ rootDir: ctx.rootDir,
288
+ dryRun: args["dry-run"]
289
+ });
290
+ consola3.success(env.name);
291
+ }
292
+ }
293
+ });
294
+
295
+ // src/commands/rollback.ts
296
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync3 } from "node:fs";
297
+ import { dirname as dirname3 } from "node:path";
298
+ import { defineCommand as defineCommand4 } from "citty";
299
+ import { consola as consola4 } from "consola";
300
+ import { loadConfig as loadConfig4 } from "@kaupang/core/internal";
301
+ import {
302
+ appendDeployment,
303
+ history,
304
+ newDeploymentId,
305
+ rollbackTarget
306
+ } from "@kaupang/core/internal";
307
+ import { applyTarget as applyTarget2, resolveTarget as resolveTarget3 } from "@kaupang/core/internal";
308
+ import { run as run4 } from "@kaupang/core/internal";
309
+ var rollbackCommand = defineCommand4({
310
+ meta: {
311
+ name: "rollback",
312
+ description: "Re-apply a previously recorded deployment of an environment."
313
+ },
314
+ args: {
315
+ environment: {
316
+ type: "positional",
317
+ description: "Environment to roll back.",
318
+ required: true
319
+ },
320
+ to: {
321
+ type: "string",
322
+ description: "Deployment id to roll back to (see --list). Defaults to the previous one."
323
+ },
324
+ target: { type: "string", description: 'Deployment target (default "local").' },
325
+ list: { type: "boolean", description: "Show deployment history and exit." },
326
+ "dry-run": {
327
+ type: "boolean",
328
+ description: "Print the commands that would replay, without running them."
329
+ },
330
+ ...verboseArg,
331
+ cwd: { type: "string", description: "Directory to resolve the config from." }
332
+ },
333
+ async run({ args }) {
334
+ applyVerbose(args);
335
+ const loaded = await loadConfig4(args.cwd);
336
+ const target = args.target ?? process.env.KAUPANG_TARGET ?? "local";
337
+ const rt = resolveTarget3(loaded.config, target, loaded.rootDir);
338
+ const records = history(loaded.cacheDir, args.environment, target);
339
+ if (records.length === 0) {
340
+ consola4.warn(`No deployment history for "${args.environment}@${target}".`);
341
+ return;
342
+ }
343
+ if (args.list) {
344
+ consola4.log(`
345
+ Deployments for ${args.environment}@${target} (newest first):
346
+ `);
347
+ for (const d of [...records].reverse()) {
348
+ const imgs = d.images.map((i) => `${i.service}=${i.digest.slice(0, 19)}\u2026`).join(" ");
349
+ const tag = d.rollbackOf ? ` (rollback of ${d.rollbackOf})` : "";
350
+ consola4.log(
351
+ ` ${d.id} ${pad(d.status, 9)} ${d.backend} ${d.ranAt}${tag}
352
+ ` + (imgs ? ` ${imgs}
353
+ ` : "")
354
+ );
355
+ }
356
+ return;
357
+ }
358
+ const restore = rollbackTarget(loaded.cacheDir, args.environment, target, args.to);
359
+ if (!restore) {
360
+ consola4.error(
361
+ args.to ? `No successful deployment with id "${args.to}".` : "No previous successful deployment to roll back to (need at least two)."
362
+ );
363
+ return;
364
+ }
365
+ consola4.info(
366
+ `\u21A9\uFE0F Sailing back "${args.environment}@${target}" to ${restore.id} (${restore.ranAt})`
367
+ );
368
+ for (const i of restore.images) {
369
+ consola4.log(` ${i.service} \u2190 ${i.digest.slice(0, 19)}\u2026`);
370
+ }
371
+ if (!args["dry-run"]) {
372
+ for (const f of restore.files) {
373
+ mkdirSync4(dirname3(f.path), { recursive: true });
374
+ writeFileSync3(f.path, f.content);
375
+ }
376
+ }
377
+ for (const action of restore.up) {
378
+ const a = applyTarget2(action, rt);
379
+ await run4(a.file, a.args, {
380
+ cwd: loaded.rootDir,
381
+ dryRun: args["dry-run"],
382
+ input: a.input,
383
+ env: a.env
384
+ });
385
+ }
386
+ if (!args["dry-run"]) {
387
+ const record = {
388
+ ...restore,
389
+ id: newDeploymentId(),
390
+ ranAt: (/* @__PURE__ */ new Date()).toISOString(),
391
+ status: "succeeded",
392
+ rollbackOf: restore.id
393
+ };
394
+ appendDeployment(loaded.cacheDir, record);
395
+ }
396
+ consola4.success(`\u21A9\uFE0F Sailed back "${args.environment}" to ${restore.id}.`);
397
+ }
398
+ });
399
+ function pad(s, n) {
400
+ return s.length >= n ? s : s + " ".repeat(n - s.length);
401
+ }
402
+
403
+ // src/commands/run.ts
404
+ import { defineCommand as defineCommand5 } from "citty";
405
+ import { loadConfig as loadConfig5 } from "@kaupang/core/internal";
406
+ import { runPipeline } from "@kaupang/core/internal";
407
+ var runCommand = defineCommand5({
408
+ meta: {
409
+ name: "run",
410
+ description: "Run a named pipeline (ordered run / up / down / build / wait steps)."
411
+ },
412
+ args: {
413
+ pipeline: { type: "positional", description: "Pipeline name.", required: true },
414
+ target: { type: "string", description: 'Default target for up/down/build steps (default "local").' },
415
+ "dry-run": { type: "boolean", description: "Print the step graph without running anything." },
416
+ ...verboseArg,
417
+ cwd: { type: "string", description: "Directory to resolve the config from." }
418
+ },
419
+ async run({ args }) {
420
+ applyVerbose(args);
421
+ const loaded = await loadConfig5(args.cwd);
422
+ await runPipeline(loaded, args.pipeline, {
423
+ target: args.target,
424
+ dryRun: Boolean(args["dry-run"])
425
+ });
426
+ }
427
+ });
428
+
429
+ // src/commands/up.ts
430
+ import { join as join2, resolve as resolvePath } from "node:path";
431
+ import { defineCommand as defineCommand6 } from "citty";
432
+ import { consola as consola5 } from "consola";
433
+ import { backendNames as backendNames3 } from "@kaupang/core/internal";
434
+ import { loadConfig as loadConfig6 } from "@kaupang/core/internal";
435
+ import { makeContext as makeContext4 } from "@kaupang/core/internal";
436
+ import { resolveMultiPlan as resolveMultiPlan2, resolvePlan as resolvePlan3 } from "@kaupang/core/internal";
437
+ import { appendDeployment as appendDeployment2, newDeploymentId as newDeploymentId2 } from "@kaupang/core/internal";
438
+ import { renderPlan } from "@kaupang/core/internal";
439
+ import { deployPlan } from "@kaupang/core/internal";
440
+ import { applyPins as applyPins2, resolveSolution as resolveSolution2 } from "@kaupang/core/internal";
441
+ import { isOciRef, pullBundle, readBundle } from "@kaupang/core/internal";
442
+ import { applyTarget as applyTarget3, resolveTarget as resolveTarget4 } from "@kaupang/core/internal";
443
+ import { mergeEnv as mergeEnv2 } from "@kaupang/core/internal";
444
+ import { run as run5 } from "@kaupang/core/internal";
445
+ var PULL_POLICIES = ["always", "missing", "never"];
446
+ var upCommand = defineCommand6({
447
+ meta: {
448
+ name: "up",
449
+ description: "Deploy an environment, a solution, or a pre-built bundle to a target."
450
+ },
451
+ args: {
452
+ environment: { type: "positional", description: "Environment to bring up.", required: false },
453
+ solution: { type: "string", description: "Deploy a solution (composed environments) instead." },
454
+ bundle: { type: "string", description: "Deploy a pre-built bundle directory (offline)." },
455
+ target: { type: "string", description: 'Deployment target (default "local").' },
456
+ backend: { type: "string", alias: "b", description: `Backend: ${backendNames3.join(" | ")}.` },
457
+ pull: { type: "string", description: `Force pull policy: ${PULL_POLICIES.join(" | ")}.` },
458
+ resolve: {
459
+ type: "boolean",
460
+ default: true,
461
+ description: "Resolve image references to digests (pass --no-resolve to skip)."
462
+ },
463
+ "dry-run": { type: "boolean", description: "Print the plan and commands without running anything." },
464
+ output: { type: "string", description: 'Output format: "text" (default) or "json".' },
465
+ ...verboseArg,
466
+ cwd: { type: "string", description: "Directory to resolve the config from." }
467
+ },
468
+ async run({ args }) {
469
+ applyVerbose(args);
470
+ const json = parseOutput(args.output);
471
+ if (json) consola5.level = -999;
472
+ const loaded = await loadConfig6(args.cwd);
473
+ if (args.bundle) {
474
+ await deployBundle(loaded, args.bundle, {
475
+ target: args.target,
476
+ dryRun: Boolean(args["dry-run"]),
477
+ json
478
+ });
479
+ return;
480
+ }
481
+ const targetName = args.target ?? process.env.KAUPANG_TARGET ?? "local";
482
+ const rt = resolveTarget4(loaded.config, targetName, loaded.rootDir);
483
+ let plan;
484
+ let targetEnv = rt.env;
485
+ if (args.solution) {
486
+ const sol = await resolveSolution2(loaded.config, loaded.catalog, args.solution);
487
+ const backendName2 = resolveBackend(args.backend, rt.backend, loaded.config.defaultBackend);
488
+ plan = resolveMultiPlan2(sol.environments, sol.name, loaded.environments, backendName2);
489
+ applyPins2(plan, sol.pins);
490
+ targetEnv = mergeEnv2(rt.env, sol.env);
491
+ } else if (args.environment) {
492
+ const backendName2 = resolveBackend(args.backend, rt.backend, loaded.config.defaultBackend);
493
+ plan = resolvePlan3(args.environment, loaded.environments, backendName2);
494
+ } else {
495
+ throw new Error("Specify an environment, --solution <name>, or --bundle <dir>.");
496
+ }
497
+ const backendName = plan.backend;
498
+ const pull = resolvePull(args.pull) ?? rt.pull ?? loaded.config.defaultPull;
499
+ const ctx = makeContext4(loaded, { pull, targetEnv });
500
+ if (args["dry-run"]) {
501
+ if (json) {
502
+ emitJson({
503
+ dryRun: true,
504
+ project: loaded.project,
505
+ target: targetName,
506
+ backend: backendName,
507
+ willResolve: Boolean(args.resolve),
508
+ environments: plan.environments.map((e) => ({
509
+ name: e.name,
510
+ services: Object.entries(e.services).map(([service, s]) => ({
511
+ service,
512
+ image: s.image ?? null
513
+ }))
514
+ }))
515
+ });
516
+ return;
517
+ }
518
+ renderPlan(plan, ctx, rt);
519
+ if (args.resolve) {
520
+ consola5.info("Images will be resolved to digests at deploy time (--no-resolve to skip).");
521
+ }
522
+ return;
523
+ }
524
+ const records = await deployPlan(loaded, plan, rt, {
525
+ targetName,
526
+ targetEnv,
527
+ pull,
528
+ resolve: Boolean(args.resolve)
529
+ });
530
+ if (json) {
531
+ emitJson({
532
+ project: loaded.project,
533
+ target: targetName,
534
+ backend: backendName,
535
+ deployments: records.map((r) => ({
536
+ environment: r.environment,
537
+ deploymentId: r.id,
538
+ ranAt: r.ranAt,
539
+ images: r.images.map((i) => ({
540
+ service: i.service,
541
+ ref: i.ref,
542
+ digest: i.digest ?? null,
543
+ pinned: i.pinned
544
+ }))
545
+ }))
546
+ });
547
+ return;
548
+ }
549
+ }
550
+ });
551
+ function parseOutput(value) {
552
+ if (value === void 0 || value === "text") return false;
553
+ if (value === "json") return true;
554
+ throw new Error(`Unknown --output "${value}" (use "text" or "json").`);
555
+ }
556
+ function emitJson(value) {
557
+ process.stdout.write(`${JSON.stringify(value, null, 2)}
558
+ `);
559
+ }
560
+ async function deployBundle(loaded, bundleArg, opts) {
561
+ let dir;
562
+ if (isOciRef(bundleArg)) {
563
+ consola5.start(`\u26F5 ferrying cargo \u2190 ${bundleArg}`);
564
+ dir = await pullBundle(bundleArg, join2(loaded.cacheDir, "pulled"));
565
+ consola5.success(`cargo ashore at ${dir}`);
566
+ } else {
567
+ dir = resolvePath(loaded.rootDir, bundleArg);
568
+ }
569
+ const manifest = readBundle(dir);
570
+ const targetName = opts.target ?? manifest.target ?? "local";
571
+ const rt = resolveTarget4(loaded.config, targetName, loaded.rootDir);
572
+ consola5.info(
573
+ `\u{1F4E6} Unpacking cargo "${manifest.solution}"${manifest.version ? `@${manifest.version}` : ""} \u2192 ${targetName} (${manifest.environments.map((e) => e.name).join(" \u2192 ")})`
574
+ );
575
+ if (manifest.images.included && manifest.images.tars.length) {
576
+ for (const tar of manifest.images.tars) {
577
+ await run5("docker", ["load", "-i", join2(dir, tar)], { cwd: dir, dryRun: opts.dryRun });
578
+ }
579
+ }
580
+ const deployed = [];
581
+ for (const env of manifest.environments) {
582
+ consola5.start(`\u{1FA93} raising ${env.name}`);
583
+ const absArtifact = join2(dir, env.artifact.relPath);
584
+ for (const action of env.up) {
585
+ const abs = {
586
+ ...action,
587
+ args: action.args.map(
588
+ (x, i, arr) => x === env.artifact.relPath ? absArtifact : arr[i - 1] === "--project-directory" ? dir : x
589
+ )
590
+ };
591
+ const a = applyTarget3(abs, rt);
592
+ await run5(a.file, a.args, { cwd: dir, dryRun: opts.dryRun, input: a.input, env: a.env });
593
+ }
594
+ if (!opts.dryRun) {
595
+ const id = newDeploymentId2();
596
+ const ranAt = (/* @__PURE__ */ new Date()).toISOString();
597
+ appendDeployment2(loaded.cacheDir, {
598
+ id,
599
+ environment: env.name,
600
+ target: targetName,
601
+ backend: env.backend,
602
+ project: manifest.project,
603
+ ranAt,
604
+ status: "succeeded",
605
+ images: env.images,
606
+ files: [{ path: absArtifact, content: env.artifact.content }],
607
+ up: env.up,
608
+ down: env.down
609
+ });
610
+ deployed.push({ environment: env.name, deploymentId: id, ranAt, images: env.images });
611
+ }
612
+ consola5.success(env.name);
613
+ }
614
+ if (opts.json) {
615
+ emitJson({
616
+ project: manifest.project,
617
+ solution: manifest.solution,
618
+ version: manifest.version ?? null,
619
+ target: targetName,
620
+ bundle: dir,
621
+ deployments: deployed.map((d) => ({
622
+ environment: d.environment,
623
+ deploymentId: d.deploymentId,
624
+ ranAt: d.ranAt,
625
+ images: d.images.map((i) => ({
626
+ service: i.service,
627
+ ref: i.ref,
628
+ digest: i.digest ?? null,
629
+ pinned: i.pinned
630
+ }))
631
+ }))
632
+ });
633
+ return;
634
+ }
635
+ consola5.success(`\u{1F6D6} Cargo "${manifest.solution}" landed on ${targetName}.`);
636
+ }
637
+ function resolveBackend(flag, fromTarget, fallback) {
638
+ const name = flag ?? fromTarget ?? fallback ?? "compose";
639
+ if (!backendNames3.includes(name)) {
640
+ throw new Error(`Unknown backend "${name}". Use one of: ${backendNames3.join(", ")}.`);
641
+ }
642
+ return name;
643
+ }
644
+ function resolvePull(flag) {
645
+ if (!flag) return void 0;
646
+ if (!PULL_POLICIES.includes(flag)) {
647
+ throw new Error(`Unknown pull policy "${flag}". Use one of: ${PULL_POLICIES.join(", ")}.`);
648
+ }
649
+ return flag;
650
+ }
651
+
652
+ // src/cli.ts
653
+ var main = defineCommand7({
654
+ meta: {
655
+ name: "kaupang",
656
+ version: "0.1.0",
657
+ description: "The trading hub for your deploys \u2014 gather your services, pin them into portable cargo, and ship them to any target (Compose, Swarm, or Kubernetes) from one config."
658
+ },
659
+ subCommands: {
660
+ up: upCommand,
661
+ down: downCommand,
662
+ build: buildCommand,
663
+ bundle: bundleCommand,
664
+ rollback: rollbackCommand,
665
+ run: runCommand
666
+ }
667
+ });
668
+ runMain(main);
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@kaupang/cli",
3
+ "version": "0.1.0",
4
+ "description": "The kaupang CLI — the `kaupang` binary. Deploy the same definitions to Docker Compose, Swarm, or Kubernetes from one config.",
5
+ "license": "MIT",
6
+ "author": "Andreas Quist Batista",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/kaupang-dev/kaupang.git",
10
+ "directory": "packages/cli"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/kaupang-dev/kaupang/issues"
14
+ },
15
+ "homepage": "https://github.com/kaupang-dev/kaupang#readme",
16
+ "type": "module",
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "bin": {
21
+ "kaupang": "./dist/cli.js"
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsup",
28
+ "typecheck": "tsc --noEmit",
29
+ "prepublishOnly": "npm run build"
30
+ },
31
+ "dependencies": {
32
+ "@kaupang/core": "^0.1.0",
33
+ "citty": "0.1.6",
34
+ "consola": "3.4.0"
35
+ },
36
+ "engines": {
37
+ "node": ">=18"
38
+ }
39
+ }