@kntic/kntic 0.8.0 → 0.9.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.
package/README.md CHANGED
@@ -42,13 +42,13 @@ kntic usage
42
42
  Download and extract the KNTIC bootstrap template into the current directory. Sets up the `.kntic/` directory structure, `kntic.yml`, and `.kntic.env`.
43
43
 
44
44
  ```bash
45
- kntic init [--quick | --interactive | -i]
45
+ kntic init [--quick | -q | --interactive | -i]
46
46
  ```
47
47
 
48
48
  | Option | Description |
49
49
  |--------|-------------|
50
- | `--quick` | **Default.** Non-interactive mode. Auto-detects `GIT_HOST` and `GIT_REPO_PATH` from the git origin remote (SSH and HTTPS). Extracts `GITLAB_TOKEN` from HTTPS credentials if available (`glpat-*` tokens). |
51
- | `--interactive`, `-i` | Walks through all `.kntic.env` values interactively, prompting for each variable. Auto-detected values are pre-filled as defaults. Skips `KNTIC_VERSION` and already-detected `GITLAB_TOKEN`. |
50
+ | `--interactive`, `-i` | **Default.** Walks through all `.kntic.env` values interactively, prompting for each variable. Auto-detected values are pre-filled as defaults. Skips `KNTIC_VERSION` and already-detected `GITLAB_TOKEN`. |
51
+ | `--quick`, `-q` | Non-interactive mode. Auto-detects `GIT_HOST` and `GIT_REPO_PATH` from the git origin remote (SSH and HTTPS). Extracts `GITLAB_TOKEN` from HTTPS credentials if available (`glpat-*` tokens). |
52
52
 
53
53
  **What it does:**
54
54
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kntic/kntic",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "author": "Thomas Robak <contact@kntic.ai> (https://kntic.ai)",
5
5
  "description": "KNTIC CLI — bootstrap and manage KNTIC projects",
6
6
  "main": "src/index.js",
@@ -273,10 +273,11 @@ function interactiveEnvSetup(envPath) {
273
273
  /**
274
274
  * Run preflight checks before downloading the bootstrap archive.
275
275
  * All checks are warnings only — they never throw or exit.
276
- * Returns an array of warning strings (empty if all checks pass).
276
+ * Returns { warnings: string[], composeProvider: string|null }.
277
277
  */
278
278
  function preflightChecks() {
279
279
  const warnings = [];
280
+ let composeProvider = null;
280
281
 
281
282
  // 1. Linux platform check
282
283
  if (process.platform !== "linux") {
@@ -285,11 +286,31 @@ function preflightChecks() {
285
286
  );
286
287
  }
287
288
 
288
- // 2. docker binary check
289
+ // 2. Compose provider detection
290
+ let hasDocker = false;
291
+ let hasPodman = false;
289
292
  try {
290
293
  execSync("which docker", { stdio: "ignore" });
291
- } catch {
292
- warnings.push("⚠ docker not found — required for `kntic start`");
294
+ hasDocker = true;
295
+ } catch {}
296
+ if (!hasDocker) {
297
+ try {
298
+ execSync("which podman", { stdio: "ignore" });
299
+ hasPodman = true;
300
+ } catch {}
301
+ }
302
+
303
+ if (hasDocker) {
304
+ composeProvider = "docker compose";
305
+ } else if (hasPodman) {
306
+ composeProvider = "podman-compose";
307
+ try {
308
+ execSync("which podman-compose", { stdio: "ignore" });
309
+ } catch {
310
+ warnings.push("⚠ podman-compose not found — install it via `pip install podman-compose`");
311
+ }
312
+ } else {
313
+ warnings.push("⚠ No container runtime found (docker or podman) — required for `kntic start`");
293
314
  }
294
315
 
295
316
  // 3. screen binary check
@@ -303,12 +324,12 @@ function preflightChecks() {
303
324
  console.log(w);
304
325
  }
305
326
 
306
- return warnings;
327
+ return { warnings, composeProvider };
307
328
  }
308
329
 
309
330
  async function init(options = {}) {
310
331
  // Run preflight checks before anything else
311
- preflightChecks();
332
+ const { composeProvider } = preflightChecks();
312
333
 
313
334
  // Resolve current version from the artifact metadata file
314
335
  const artifactFilename = await fetchText(BOOTSTRAP_ARTIFACT_URL);
@@ -328,6 +349,11 @@ async function init(options = {}) {
328
349
  // Clean up
329
350
  fs.unlinkSync(tmpFile);
330
351
 
352
+ // Auto-fill compose provider
353
+ if (composeProvider) {
354
+ fillEnvValues('.kntic.env', { KNTIC_COMPOSE_PROVIDER: composeProvider });
355
+ }
356
+
331
357
  // Auto-detect git remote
332
358
  const gitInfo = parseGitRemote();
333
359
 
@@ -47,7 +47,7 @@ describe("preflightChecks", () => {
47
47
 
48
48
  it("warns when platform is not linux", () => {
49
49
  Object.defineProperty(process, "platform", { value: "darwin", configurable: true });
50
- const warnings = preflightChecks();
50
+ const { warnings } = preflightChecks();
51
51
  const platformWarning = warnings.find((w) => w.includes("Non-Linux detected"));
52
52
  assert.ok(platformWarning, "should warn about non-linux platform");
53
53
  assert.ok(platformWarning.includes("darwin"));
@@ -55,24 +55,76 @@ describe("preflightChecks", () => {
55
55
 
56
56
  it("does not warn about platform on linux", () => {
57
57
  Object.defineProperty(process, "platform", { value: "linux", configurable: true });
58
- const warnings = preflightChecks();
58
+ const { warnings } = preflightChecks();
59
59
  const platformWarning = warnings.find((w) => w.includes("Non-Linux detected"));
60
60
  assert.equal(platformWarning, undefined, "should not warn on linux");
61
61
  });
62
62
 
63
- it("warns when docker is not found", () => {
64
- // We test by checking the function handles missing binaries.
65
- // On the test system docker may or may not exist, so we check the structure.
66
- const warnings = preflightChecks();
67
- // Each warning should be a string
68
- for (const w of warnings) {
63
+ it("returns correct structure with warnings and composeProvider", () => {
64
+ const result = preflightChecks();
65
+ assert.ok(Array.isArray(result.warnings), "warnings must be an array");
66
+ // composeProvider is either a string or null
67
+ assert.ok(
68
+ result.composeProvider === null || typeof result.composeProvider === "string",
69
+ "composeProvider must be string or null"
70
+ );
71
+ for (const w of result.warnings) {
69
72
  assert.equal(typeof w, "string");
70
73
  }
71
74
  });
72
75
 
76
+ it("returns composeProvider 'docker compose' when docker is found", () => {
77
+ // On the test system docker may or may not exist. We verify the shape.
78
+ const { composeProvider } = preflightChecks();
79
+ // If docker is on PATH, provider should be 'docker compose'
80
+ let hasDocker = false;
81
+ try {
82
+ execSync("which docker", { stdio: "ignore" });
83
+ hasDocker = true;
84
+ } catch {}
85
+ if (hasDocker) {
86
+ assert.equal(composeProvider, "docker compose");
87
+ }
88
+ });
89
+
90
+ it("returns composeProvider 'podman-compose' when only podman is found", () => {
91
+ // This test verifies the detection logic structurally.
92
+ // On most CI systems docker is present, so we just verify the return shape.
93
+ const { composeProvider } = preflightChecks();
94
+ // If neither docker nor podman, composeProvider is null
95
+ let hasDocker = false;
96
+ let hasPodman = false;
97
+ try { execSync("which docker", { stdio: "ignore" }); hasDocker = true; } catch {}
98
+ if (!hasDocker) {
99
+ try { execSync("which podman", { stdio: "ignore" }); hasPodman = true; } catch {}
100
+ }
101
+ if (!hasDocker && hasPodman) {
102
+ assert.equal(composeProvider, "podman-compose");
103
+ }
104
+ if (!hasDocker && !hasPodman) {
105
+ assert.equal(composeProvider, null);
106
+ }
107
+ });
108
+
109
+ it("warns when neither docker nor podman found", () => {
110
+ // We verify the warning message format if it appears.
111
+ const { warnings, composeProvider } = preflightChecks();
112
+ let hasDocker = false;
113
+ let hasPodman = false;
114
+ try { execSync("which docker", { stdio: "ignore" }); hasDocker = true; } catch {}
115
+ if (!hasDocker) {
116
+ try { execSync("which podman", { stdio: "ignore" }); hasPodman = true; } catch {}
117
+ }
118
+ if (!hasDocker && !hasPodman) {
119
+ assert.equal(composeProvider, null);
120
+ const runtimeWarning = warnings.find((w) => w.includes("No container runtime found"));
121
+ assert.ok(runtimeWarning, "should warn about missing container runtime");
122
+ }
123
+ });
124
+
73
125
  it("each warning is independent (all checks run)", () => {
74
126
  Object.defineProperty(process, "platform", { value: "win32", configurable: true });
75
- const warnings = preflightChecks();
127
+ const { warnings } = preflightChecks();
76
128
  // Platform warning must be present regardless of binary check results
77
129
  const platformWarning = warnings.find((w) => w.includes("Non-Linux detected"));
78
130
  assert.ok(platformWarning, "platform warning must be present");
@@ -83,7 +135,7 @@ describe("preflightChecks", () => {
83
135
 
84
136
  it("prints warnings to console.log", () => {
85
137
  Object.defineProperty(process, "platform", { value: "freebsd", configurable: true });
86
- const warnings = preflightChecks();
138
+ const { warnings } = preflightChecks();
87
139
  // At least the platform warning should be logged
88
140
  const platformLog = logMessages.find((m) => m.includes("Non-Linux detected"));
89
141
  assert.ok(platformLog, "platform warning must be printed via console.log");
@@ -3,6 +3,7 @@
3
3
  const { execSync } = require("child_process");
4
4
  const { readFileSync } = require("fs");
5
5
  const { basename } = require("path");
6
+ const { getComposeProvider } = require("./utils");
6
7
 
7
8
  function isScreenAvailable() {
8
9
  try {
@@ -31,7 +32,8 @@ function getScreenName() {
31
32
  }
32
33
 
33
34
  function start(options = {}) {
34
- const composeCmd = "docker compose -f kntic.yml --env-file .kntic.env up --build";
35
+ const provider = getComposeProvider();
36
+ const composeCmd = `${provider} -f kntic.yml --env-file .kntic.env up --build`;
35
37
  const useScreen = !!options.screen;
36
38
 
37
39
  if (useScreen && isScreenAvailable() && !isInsideScreen()) {
@@ -57,6 +57,38 @@ describe("start — getScreenName resolution", () => {
57
57
  });
58
58
  });
59
59
 
60
+ describe("start — compose provider from .kntic.env", () => {
61
+ let tmpDir, origCwd;
62
+
63
+ beforeEach(() => {
64
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "kntic-start-compose-"));
65
+ origCwd = process.cwd();
66
+ process.chdir(tmpDir);
67
+ });
68
+
69
+ afterEach(() => {
70
+ process.chdir(origCwd);
71
+ fs.rmSync(tmpDir, { recursive: true, force: true });
72
+ });
73
+
74
+ it("reads KNTIC_COMPOSE_PROVIDER from .kntic.env", () => {
75
+ fs.writeFileSync(path.join(tmpDir, ".kntic.env"), "KNTIC_COMPOSE_PROVIDER=podman-compose\nUID=1000\n");
76
+ const { getComposeProvider } = require("./utils");
77
+ assert.equal(getComposeProvider(), "podman-compose");
78
+ });
79
+
80
+ it("defaults to 'docker compose' when KNTIC_COMPOSE_PROVIDER is missing", () => {
81
+ fs.writeFileSync(path.join(tmpDir, ".kntic.env"), "UID=1000\nGID=1000\n");
82
+ const { getComposeProvider } = require("./utils");
83
+ assert.equal(getComposeProvider(), "docker compose");
84
+ });
85
+
86
+ it("defaults to 'docker compose' when .kntic.env does not exist", () => {
87
+ const { getComposeProvider } = require("./utils");
88
+ assert.equal(getComposeProvider(), "docker compose");
89
+ });
90
+ });
91
+
60
92
  describe("start — isInsideScreen detection", () => {
61
93
  let origSTY;
62
94
 
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
 
3
3
  const { execSync } = require("child_process");
4
+ const { getComposeProvider } = require("./utils");
4
5
 
5
6
  function stop() {
6
- const composeCmd = "docker compose -f kntic.yml --env-file .kntic.env stop";
7
+ const provider = getComposeProvider();
8
+ const composeCmd = `${provider} -f kntic.yml --env-file .kntic.env stop`;
7
9
  console.log("Stopping KNTIC services…");
8
10
  execSync(composeCmd, { stdio: "inherit" });
9
11
  }
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
 
3
- const { describe, it } = require("node:test");
3
+ const { describe, it, beforeEach, afterEach } = require("node:test");
4
4
  const assert = require("node:assert/strict");
5
5
  const fs = require("fs");
6
+ const path = require("path");
7
+ const os = require("os");
6
8
 
7
9
  describe("stop — module structure", () => {
8
10
  it("exports a function", () => {
@@ -10,8 +12,41 @@ describe("stop — module structure", () => {
10
12
  assert.equal(typeof stop, "function");
11
13
  });
12
14
 
13
- it("source contains the correct docker compose stop command", () => {
15
+ it("source uses getComposeProvider for compose command", () => {
14
16
  const src = fs.readFileSync(require.resolve("./stop"), "utf8");
15
- assert.ok(src.includes("docker compose -f kntic.yml --env-file .kntic.env stop"));
17
+ assert.ok(src.includes("getComposeProvider"), "should use getComposeProvider helper");
18
+ assert.ok(src.includes("-f kntic.yml --env-file .kntic.env stop"), "should include compose flags");
19
+ });
20
+ });
21
+
22
+ describe("stop — compose provider from .kntic.env", () => {
23
+ let tmpDir, origCwd;
24
+
25
+ beforeEach(() => {
26
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "kntic-stop-compose-"));
27
+ origCwd = process.cwd();
28
+ process.chdir(tmpDir);
29
+ });
30
+
31
+ afterEach(() => {
32
+ process.chdir(origCwd);
33
+ fs.rmSync(tmpDir, { recursive: true, force: true });
34
+ });
35
+
36
+ it("reads KNTIC_COMPOSE_PROVIDER from .kntic.env", () => {
37
+ fs.writeFileSync(path.join(tmpDir, ".kntic.env"), "KNTIC_COMPOSE_PROVIDER=podman-compose\nUID=1000\n");
38
+ const { getComposeProvider } = require("./utils");
39
+ assert.equal(getComposeProvider(), "podman-compose");
40
+ });
41
+
42
+ it("defaults to 'docker compose' when KNTIC_COMPOSE_PROVIDER is missing", () => {
43
+ fs.writeFileSync(path.join(tmpDir, ".kntic.env"), "UID=1000\nGID=1000\n");
44
+ const { getComposeProvider } = require("./utils");
45
+ assert.equal(getComposeProvider(), "docker compose");
46
+ });
47
+
48
+ it("defaults to 'docker compose' when .kntic.env does not exist", () => {
49
+ const { getComposeProvider } = require("./utils");
50
+ assert.equal(getComposeProvider(), "docker compose");
16
51
  });
17
52
  });
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+
5
+ /**
6
+ * Read KNTIC_COMPOSE_PROVIDER from .kntic.env.
7
+ * Returns the value or 'docker compose' as fallback.
8
+ */
9
+ function getComposeProvider() {
10
+ try {
11
+ const content = fs.readFileSync(".kntic.env", "utf8");
12
+ const match = content.match(/^KNTIC_COMPOSE_PROVIDER=(.+)$/m);
13
+ if (match && match[1].trim()) {
14
+ return match[1].trim();
15
+ }
16
+ } catch {
17
+ // .kntic.env not found or unreadable — fall through
18
+ }
19
+ return "docker compose";
20
+ }
21
+
22
+ module.exports = { getComposeProvider };