@kntic/kntic 0.5.0 → 0.7.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 +125 -56
- package/package.json +1 -1
- package/src/cli.js +2 -1
- package/src/commands/init.js +178 -1
- package/src/commands/init.test.js +196 -1
- package/src/commands/usage.js +2 -0
package/README.md
CHANGED
|
@@ -1,93 +1,162 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @kntic/kntic
|
|
2
2
|
|
|
3
|
+
KNTIC CLI — bootstrap and manage KNTIC AI-orchestrated projects.
|
|
3
4
|
|
|
5
|
+
## Requirements
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
- **Node.js** >= 18.0.0
|
|
8
|
+
- **Docker** (with `docker compose` v2) — required for `kntic start` / `kntic stop`
|
|
9
|
+
- **GNU screen** _(optional)_ — needed for `kntic start --screen`
|
|
10
|
+
- **Git** _(optional)_ — enables auto-detection of `GIT_HOST`, `GIT_REPO_PATH`, and `GITLAB_TOKEN` during `kntic init`
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install -g @kntic/kntic
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Preflight Checks
|
|
8
19
|
|
|
9
|
-
|
|
20
|
+
When running `kntic init`, the CLI performs automatic preflight checks before downloading the bootstrap archive. All checks are **warnings only** — they never block execution.
|
|
10
21
|
|
|
11
|
-
|
|
22
|
+
| Check | Condition | Warning |
|
|
23
|
+
|-------|-----------|---------|
|
|
24
|
+
| **Platform** | `process.platform !== "linux"` | `⚠ Non-Linux detected (<platform>) — KNTIC is designed for Linux, other platforms may have issues` |
|
|
25
|
+
| **Docker** | `docker` binary not found in `$PATH` | `⚠ docker not found — required for \`kntic start\`` |
|
|
26
|
+
| **Screen** | `screen` binary not found in `$PATH` | `⚠ screen not found — optional, needed for \`kntic start --screen\`` |
|
|
12
27
|
|
|
13
|
-
|
|
14
|
-
* [Add files using the command line](https://docs.gitlab.com/topics/git/add_files/#add-files-to-a-git-repository) or push an existing Git repository with the following command:
|
|
28
|
+
## Commands
|
|
15
29
|
|
|
30
|
+
### `kntic usage`
|
|
31
|
+
|
|
32
|
+
List all available sub-commands.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
kntic usage
|
|
16
36
|
```
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
### `kntic init`
|
|
41
|
+
|
|
42
|
+
Download and extract the KNTIC bootstrap template into the current directory. Sets up the `.kntic/` directory structure, `kntic.yml`, and `.kntic.env`.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
kntic init [--quick | --interactive | -i]
|
|
21
46
|
```
|
|
22
47
|
|
|
23
|
-
|
|
48
|
+
| Option | Description |
|
|
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`. |
|
|
24
52
|
|
|
25
|
-
|
|
53
|
+
**What it does:**
|
|
26
54
|
|
|
27
|
-
|
|
55
|
+
1. Runs [preflight checks](#preflight-checks)
|
|
56
|
+
2. Fetches version metadata from the bootstrap artifact URL
|
|
57
|
+
3. Downloads and extracts the bootstrap archive (merges `.gitignore` if one already exists)
|
|
58
|
+
4. Appends `KNTIC_VERSION=<version>` to `.kntic.env`
|
|
59
|
+
5. Auto-detects git remote information and fills `GIT_HOST`, `GIT_REPO_PATH`, and `GITLAB_TOKEN`
|
|
60
|
+
6. _(Interactive mode only)_ Prompts for remaining `.kntic.env` values
|
|
28
61
|
|
|
29
|
-
|
|
30
|
-
* [Create a new merge request](https://docs.gitlab.com/user/project/merge_requests/creating_merge_requests/)
|
|
31
|
-
* [Automatically close issues from merge requests](https://docs.gitlab.com/user/project/issues/managing_issues/#closing-issues-automatically)
|
|
32
|
-
* [Enable merge request approvals](https://docs.gitlab.com/user/project/merge_requests/approvals/)
|
|
33
|
-
* [Set auto-merge](https://docs.gitlab.com/user/project/merge_requests/auto_merge/)
|
|
62
|
+
---
|
|
34
63
|
|
|
35
|
-
|
|
64
|
+
### `kntic start`
|
|
36
65
|
|
|
37
|
-
|
|
66
|
+
Build and start KNTIC services via Docker Compose.
|
|
38
67
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
* [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/user/clusters/agent/)
|
|
43
|
-
* [Set up protected environments](https://docs.gitlab.com/ci/environments/protected_environments/)
|
|
68
|
+
```bash
|
|
69
|
+
kntic start [--screen]
|
|
70
|
+
```
|
|
44
71
|
|
|
45
|
-
|
|
72
|
+
| Option | Description |
|
|
73
|
+
|--------|-------------|
|
|
74
|
+
| `--screen` | Wrap the Docker Compose process in a GNU `screen` session. The session name is read from `KNTIC_PRJ_PREFIX` in `.kntic.env`, falling back to the current directory name. Skipped if already inside a screen session or if `screen` is not available. |
|
|
46
75
|
|
|
47
|
-
|
|
76
|
+
Runs:
|
|
77
|
+
```bash
|
|
78
|
+
docker compose -f kntic.yml --env-file .kntic.env up --build
|
|
79
|
+
```
|
|
48
80
|
|
|
49
|
-
|
|
81
|
+
---
|
|
50
82
|
|
|
51
|
-
|
|
83
|
+
### `kntic stop`
|
|
52
84
|
|
|
53
|
-
|
|
85
|
+
Stop KNTIC services via Docker Compose.
|
|
54
86
|
|
|
55
|
-
|
|
56
|
-
|
|
87
|
+
```bash
|
|
88
|
+
kntic stop
|
|
89
|
+
```
|
|
57
90
|
|
|
58
|
-
|
|
59
|
-
|
|
91
|
+
Runs:
|
|
92
|
+
```bash
|
|
93
|
+
docker compose -f kntic.yml --env-file .kntic.env stop
|
|
94
|
+
```
|
|
60
95
|
|
|
61
|
-
|
|
62
|
-
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
|
|
96
|
+
---
|
|
63
97
|
|
|
64
|
-
|
|
65
|
-
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
|
|
98
|
+
### `kntic update`
|
|
66
99
|
|
|
67
|
-
|
|
68
|
-
|
|
100
|
+
Download the latest KNTIC bootstrap archive and update managed files.
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
kntic update [--lib-only] [--compose]
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
| Option | Description |
|
|
107
|
+
|--------|-------------|
|
|
108
|
+
| `--lib-only` | Update only `.kntic/lib/` (skip ADRs, hooks, and weights). |
|
|
109
|
+
| `--compose` | Also replace `kntic.yml` from the bootstrap template. Creates a backup at `kntic.yml.bak` before overwriting. |
|
|
110
|
+
|
|
111
|
+
**Default update scope** (without `--lib-only`):
|
|
69
112
|
|
|
70
|
-
|
|
71
|
-
|
|
113
|
+
| Path | Strategy |
|
|
114
|
+
|------|----------|
|
|
115
|
+
| `.kntic/lib/` | **Replaced** — cleared and re-extracted |
|
|
116
|
+
| `.kntic/adrs/` | **Replaced** — cleared and re-extracted |
|
|
117
|
+
| `.kntic/hooks/gia/internal/` | **Updated** — existing files overwritten, new files added, unlisted files preserved |
|
|
118
|
+
| `.kntic/hooks/gia/specific/` | **Bootstrap only** — extracted only if the directory does not already exist (user customizations are never overwritten) |
|
|
119
|
+
| `.kntic/gia/weights.json` | **Replaced** if present in the archive |
|
|
120
|
+
| `.kntic.env` | **Merged** — new variables from the template are appended with their comments; existing values are never overwritten |
|
|
121
|
+
| `KNTIC_VERSION` | Updated in `.kntic.env` to the latest version |
|
|
72
122
|
|
|
73
|
-
##
|
|
74
|
-
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
|
|
123
|
+
## Environment Variables
|
|
75
124
|
|
|
76
|
-
|
|
77
|
-
If you have ideas for releases in the future, it is a good idea to list them in the README.
|
|
125
|
+
The `.kntic.env` file contains project configuration:
|
|
78
126
|
|
|
79
|
-
|
|
80
|
-
|
|
127
|
+
| Variable | Description |
|
|
128
|
+
|----------|-------------|
|
|
129
|
+
| `ANTHROPIC_API_KEY` | API key for Anthropic (used by the orchestrator engine) |
|
|
130
|
+
| `UID` | Host user ID for container user mapping |
|
|
131
|
+
| `GID` | Host group ID for container user mapping |
|
|
132
|
+
| `GITLAB_TOKEN` | GitLab personal access token (`glpat-*`) |
|
|
133
|
+
| `GIT_HOST` | Git server hostname (auto-detected from origin remote) |
|
|
134
|
+
| `GIT_REPO_PATH` | Repository path on the git server (auto-detected from origin remote) |
|
|
135
|
+
| `KNTIC_VERSION` | Bootstrap version (set automatically by `kntic init` / `kntic update`) |
|
|
136
|
+
| `KNTIC_PRJ_PREFIX` | Project prefix used for screen session naming in `kntic start --screen` |
|
|
81
137
|
|
|
82
|
-
|
|
138
|
+
## Services
|
|
83
139
|
|
|
84
|
-
|
|
140
|
+
Defined in `kntic.yml`:
|
|
85
141
|
|
|
86
|
-
|
|
87
|
-
|
|
142
|
+
| Service | Container | Image | Port |
|
|
143
|
+
|---------|-----------|-------|------|
|
|
144
|
+
| **Dashboard** | `control-dashboard` | `kntic/dashboard:latest` | `8002` |
|
|
145
|
+
| **Orchestrator** | `control-engine` | `nexus.kommune7.wien/kntic/kntic-engine:latest` | — |
|
|
146
|
+
|
|
147
|
+
Both services run as `${UID}:${GID}` (non-root) and use `.kntic.env` for environment configuration.
|
|
148
|
+
|
|
149
|
+
## Testing
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
npm test
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Runs tests using the Node.js built-in test runner:
|
|
156
|
+
```bash
|
|
157
|
+
node --test src/**/*.test.js
|
|
158
|
+
```
|
|
88
159
|
|
|
89
160
|
## License
|
|
90
|
-
For open source projects, say how it is licensed.
|
|
91
161
|
|
|
92
|
-
|
|
93
|
-
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
|
|
162
|
+
[MIT](LICENSE)
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -10,7 +10,8 @@ const subcommand = args[0];
|
|
|
10
10
|
if (!subcommand || subcommand === "usage") {
|
|
11
11
|
commands.usage();
|
|
12
12
|
} else if (subcommand === "init") {
|
|
13
|
-
|
|
13
|
+
const initOpts = { interactive: args.includes("--interactive") || args.includes("-i") };
|
|
14
|
+
commands.init(initOpts).catch((err) => {
|
|
14
15
|
console.error(`Error: ${err.message}`);
|
|
15
16
|
process.exit(1);
|
|
16
17
|
});
|
package/src/commands/init.js
CHANGED
|
@@ -6,6 +6,7 @@ const { execSync } = require("child_process");
|
|
|
6
6
|
const fs = require("fs");
|
|
7
7
|
const path = require("path");
|
|
8
8
|
const os = require("os");
|
|
9
|
+
const readline = require("readline");
|
|
9
10
|
|
|
10
11
|
const BOOTSTRAP_ARTIFACT_URL =
|
|
11
12
|
"https://minio.kommune7.wien/public/kntic-bootstrap-latest.artifact";
|
|
@@ -155,7 +156,160 @@ function extractArchive(tarball, destDir) {
|
|
|
155
156
|
}
|
|
156
157
|
}
|
|
157
158
|
|
|
158
|
-
|
|
159
|
+
/**
|
|
160
|
+
* Parse the git origin remote URL to extract host and repo path.
|
|
161
|
+
* Returns { host, repoPath } or null if not available.
|
|
162
|
+
*/
|
|
163
|
+
function parseGitRemote() {
|
|
164
|
+
try {
|
|
165
|
+
if (!fs.existsSync('.git')) return null;
|
|
166
|
+
const output = execSync('git remote -v', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
167
|
+
const fetchLine = output.split('\n').find(l => l.startsWith('origin') && l.includes('(fetch)'));
|
|
168
|
+
if (!fetchLine) return null;
|
|
169
|
+
const url = fetchLine.split(/\s+/)[1];
|
|
170
|
+
// SSH format: git@host:path
|
|
171
|
+
const sshMatch = url.match(/^git@([^:]+):(.+?)(?:\.git)?$/);
|
|
172
|
+
if (sshMatch) return { host: sshMatch[1], repoPath: sshMatch[2] + '.git' };
|
|
173
|
+
// HTTPS with credentials: https://user:token@host/path.git
|
|
174
|
+
const httpsMatch = url.match(/^https?:\/\/(?:([^@]+)@)?([^/]+)\/(.+?)(?:\.git)?$/);
|
|
175
|
+
if (httpsMatch) {
|
|
176
|
+
const result = { host: httpsMatch[2], repoPath: httpsMatch[3] + '.git' };
|
|
177
|
+
// Extract glpat token from credentials (format: oauth2:glpat-xxx or just glpat-xxx)
|
|
178
|
+
if (httpsMatch[1]) {
|
|
179
|
+
const credParts = httpsMatch[1].split(':');
|
|
180
|
+
const token = credParts.find(p => p.startsWith('glpat-'));
|
|
181
|
+
if (token) result.gitlabToken = token;
|
|
182
|
+
}
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
} catch { return null; }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Read an env file, replace values for given keys (or append if missing),
|
|
191
|
+
* and write back.
|
|
192
|
+
*/
|
|
193
|
+
function fillEnvValues(envPath, values) {
|
|
194
|
+
let content = fs.readFileSync(envPath, 'utf8');
|
|
195
|
+
for (const [key, val] of Object.entries(values)) {
|
|
196
|
+
const regex = new RegExp(`^(${key}=).*$`, 'm');
|
|
197
|
+
if (regex.test(content)) {
|
|
198
|
+
content = content.replace(regex, `$1${val}`);
|
|
199
|
+
} else {
|
|
200
|
+
// Key missing from file — append it
|
|
201
|
+
content = content.trimEnd() + `\n${key}=${val}\n`;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
fs.writeFileSync(envPath, content);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Interactively prompt the user for each env variable in the file.
|
|
209
|
+
* Skips KNTIC_VERSION. Shows current value as default.
|
|
210
|
+
*/
|
|
211
|
+
function interactiveEnvSetup(envPath) {
|
|
212
|
+
return new Promise((resolve) => {
|
|
213
|
+
const content = fs.readFileSync(envPath, 'utf8');
|
|
214
|
+
const lines = content.split('\n');
|
|
215
|
+
|
|
216
|
+
// Collect KEY=VALUE entries to prompt for
|
|
217
|
+
const entries = [];
|
|
218
|
+
for (let i = 0; i < lines.length; i++) {
|
|
219
|
+
const match = lines[i].match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
220
|
+
if (match) {
|
|
221
|
+
const key = match[1];
|
|
222
|
+
if (key === 'KNTIC_VERSION') continue;
|
|
223
|
+
// Also skip GITLAB_TOKEN if already auto-filled with a real token
|
|
224
|
+
if (key === 'GITLAB_TOKEN' && match[2].startsWith('glpat-')) continue;
|
|
225
|
+
entries.push({ index: i, key, value: match[2] });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (entries.length === 0) {
|
|
230
|
+
resolve();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const rl = readline.createInterface({
|
|
235
|
+
input: process.stdin,
|
|
236
|
+
output: process.stdout,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const updatedValues = {};
|
|
240
|
+
let idx = 0;
|
|
241
|
+
|
|
242
|
+
function promptNext() {
|
|
243
|
+
if (idx >= entries.length) {
|
|
244
|
+
rl.close();
|
|
245
|
+
// Write updated values back
|
|
246
|
+
const newLines = [...lines];
|
|
247
|
+
for (const entry of entries) {
|
|
248
|
+
const finalVal = updatedValues[entry.key] !== undefined
|
|
249
|
+
? updatedValues[entry.key]
|
|
250
|
+
: entry.value;
|
|
251
|
+
newLines[entry.index] = `${entry.key}=${finalVal}`;
|
|
252
|
+
}
|
|
253
|
+
fs.writeFileSync(envPath, newLines.join('\n'));
|
|
254
|
+
resolve();
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const entry = entries[idx];
|
|
259
|
+
const displayVal = entry.value || '';
|
|
260
|
+
rl.question(` ${entry.key} [${displayVal}]: `, (answer) => {
|
|
261
|
+
if (answer.length > 0) {
|
|
262
|
+
updatedValues[entry.key] = answer;
|
|
263
|
+
}
|
|
264
|
+
idx++;
|
|
265
|
+
promptNext();
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
promptNext();
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Run preflight checks before downloading the bootstrap archive.
|
|
275
|
+
* All checks are warnings only — they never throw or exit.
|
|
276
|
+
* Returns an array of warning strings (empty if all checks pass).
|
|
277
|
+
*/
|
|
278
|
+
function preflightChecks() {
|
|
279
|
+
const warnings = [];
|
|
280
|
+
|
|
281
|
+
// 1. Linux platform check
|
|
282
|
+
if (process.platform !== "linux") {
|
|
283
|
+
warnings.push(
|
|
284
|
+
`⚠ Non-Linux detected (${process.platform}) — KNTIC is designed for Linux, other platforms may have issues`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// 2. docker binary check
|
|
289
|
+
try {
|
|
290
|
+
execSync("which docker", { stdio: "ignore" });
|
|
291
|
+
} catch {
|
|
292
|
+
warnings.push("⚠ docker not found — required for `kntic start`");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 3. screen binary check
|
|
296
|
+
try {
|
|
297
|
+
execSync("which screen", { stdio: "ignore" });
|
|
298
|
+
} catch {
|
|
299
|
+
warnings.push("⚠ screen not found — optional, needed for `kntic start --screen`");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
for (const w of warnings) {
|
|
303
|
+
console.log(w);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return warnings;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function init(options = {}) {
|
|
310
|
+
// Run preflight checks before anything else
|
|
311
|
+
preflightChecks();
|
|
312
|
+
|
|
159
313
|
// Resolve current version from the artifact metadata file
|
|
160
314
|
const artifactFilename = await fetchText(BOOTSTRAP_ARTIFACT_URL);
|
|
161
315
|
const version = extractVersion(artifactFilename);
|
|
@@ -174,9 +328,32 @@ async function init() {
|
|
|
174
328
|
// Clean up
|
|
175
329
|
fs.unlinkSync(tmpFile);
|
|
176
330
|
|
|
331
|
+
// Auto-detect git remote
|
|
332
|
+
const gitInfo = parseGitRemote();
|
|
333
|
+
|
|
334
|
+
// Auto-fill git values (both modes)
|
|
335
|
+
const autoValues = {};
|
|
336
|
+
if (gitInfo) {
|
|
337
|
+
autoValues.GIT_HOST = gitInfo.host;
|
|
338
|
+
autoValues.GIT_REPO_PATH = gitInfo.repoPath;
|
|
339
|
+
if (gitInfo.gitlabToken) {
|
|
340
|
+
autoValues.GITLAB_TOKEN = gitInfo.gitlabToken;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (Object.keys(autoValues).length > 0) {
|
|
344
|
+
fillEnvValues('.kntic.env', autoValues);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (options.interactive) {
|
|
348
|
+
await interactiveEnvSetup('.kntic.env');
|
|
349
|
+
}
|
|
350
|
+
|
|
177
351
|
console.log("Done. KNTIC project bootstrapped successfully.");
|
|
178
352
|
}
|
|
179
353
|
|
|
180
354
|
module.exports = init;
|
|
181
355
|
module.exports.extractArchive = extractArchive;
|
|
182
356
|
module.exports.extractVersion = extractVersion;
|
|
357
|
+
module.exports.parseGitRemote = parseGitRemote;
|
|
358
|
+
module.exports.fillEnvValues = fillEnvValues;
|
|
359
|
+
module.exports.preflightChecks = preflightChecks;
|
|
@@ -7,7 +7,7 @@ const path = require("path");
|
|
|
7
7
|
const os = require("os");
|
|
8
8
|
const { execSync } = require("child_process");
|
|
9
9
|
|
|
10
|
-
const { extractArchive, extractVersion } = require("./init");
|
|
10
|
+
const { extractArchive, extractVersion, parseGitRemote, fillEnvValues, preflightChecks } = require("./init");
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Helper — create a tar.gz archive in `tmpDir` containing the given files.
|
|
@@ -28,6 +28,68 @@ function createTarball(tmpDir, files) {
|
|
|
28
28
|
return tarball;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
describe("preflightChecks", () => {
|
|
32
|
+
let originalPlatform;
|
|
33
|
+
let logMessages;
|
|
34
|
+
let originalLog;
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
originalPlatform = Object.getOwnPropertyDescriptor(process, "platform");
|
|
38
|
+
originalLog = console.log;
|
|
39
|
+
logMessages = [];
|
|
40
|
+
console.log = (...args) => logMessages.push(args.join(" "));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
Object.defineProperty(process, "platform", originalPlatform);
|
|
45
|
+
console.log = originalLog;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("warns when platform is not linux", () => {
|
|
49
|
+
Object.defineProperty(process, "platform", { value: "darwin", configurable: true });
|
|
50
|
+
const warnings = preflightChecks();
|
|
51
|
+
const platformWarning = warnings.find((w) => w.includes("Non-Linux detected"));
|
|
52
|
+
assert.ok(platformWarning, "should warn about non-linux platform");
|
|
53
|
+
assert.ok(platformWarning.includes("darwin"));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("does not warn about platform on linux", () => {
|
|
57
|
+
Object.defineProperty(process, "platform", { value: "linux", configurable: true });
|
|
58
|
+
const warnings = preflightChecks();
|
|
59
|
+
const platformWarning = warnings.find((w) => w.includes("Non-Linux detected"));
|
|
60
|
+
assert.equal(platformWarning, undefined, "should not warn on linux");
|
|
61
|
+
});
|
|
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) {
|
|
69
|
+
assert.equal(typeof w, "string");
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("each warning is independent (all checks run)", () => {
|
|
74
|
+
Object.defineProperty(process, "platform", { value: "win32", configurable: true });
|
|
75
|
+
const warnings = preflightChecks();
|
|
76
|
+
// Platform warning must be present regardless of binary check results
|
|
77
|
+
const platformWarning = warnings.find((w) => w.includes("Non-Linux detected"));
|
|
78
|
+
assert.ok(platformWarning, "platform warning must be present");
|
|
79
|
+
assert.ok(platformWarning.includes("win32"));
|
|
80
|
+
// All warnings are logged to console
|
|
81
|
+
assert.equal(logMessages.length, warnings.length, "all warnings must be logged");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("prints warnings to console.log", () => {
|
|
85
|
+
Object.defineProperty(process, "platform", { value: "freebsd", configurable: true });
|
|
86
|
+
const warnings = preflightChecks();
|
|
87
|
+
// At least the platform warning should be logged
|
|
88
|
+
const platformLog = logMessages.find((m) => m.includes("Non-Linux detected"));
|
|
89
|
+
assert.ok(platformLog, "platform warning must be printed via console.log");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
31
93
|
describe("extractArchive", () => {
|
|
32
94
|
let tmpDir;
|
|
33
95
|
let destDir;
|
|
@@ -126,3 +188,136 @@ describe("extractVersion", () => {
|
|
|
126
188
|
assert.throws(() => extractVersion("no-version-here.tar.gz"));
|
|
127
189
|
});
|
|
128
190
|
});
|
|
191
|
+
|
|
192
|
+
describe("parseGitRemote", () => {
|
|
193
|
+
let tmpDir;
|
|
194
|
+
let originalCwd;
|
|
195
|
+
|
|
196
|
+
beforeEach(() => {
|
|
197
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "kntic-git-test-"));
|
|
198
|
+
originalCwd = process.cwd();
|
|
199
|
+
process.chdir(tmpDir);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
afterEach(() => {
|
|
203
|
+
process.chdir(originalCwd);
|
|
204
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("parses SSH remote", () => {
|
|
208
|
+
execSync("git init", { stdio: "pipe" });
|
|
209
|
+
execSync("git remote add origin git@gitlab.kommune7.wien:kntic-ai/orchestrator/control.git", { stdio: "pipe" });
|
|
210
|
+
const result = parseGitRemote();
|
|
211
|
+
assert.deepStrictEqual(result, {
|
|
212
|
+
host: "gitlab.kommune7.wien",
|
|
213
|
+
repoPath: "kntic-ai/orchestrator/control.git",
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("parses HTTPS remote", () => {
|
|
218
|
+
execSync("git init", { stdio: "pipe" });
|
|
219
|
+
execSync("git remote add origin https://github.com/org/repo.git", { stdio: "pipe" });
|
|
220
|
+
const result = parseGitRemote();
|
|
221
|
+
assert.deepStrictEqual(result, {
|
|
222
|
+
host: "github.com",
|
|
223
|
+
repoPath: "org/repo.git",
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("returns null when no .git dir", () => {
|
|
228
|
+
const result = parseGitRemote();
|
|
229
|
+
assert.equal(result, null);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("returns null when no origin remote", () => {
|
|
233
|
+
execSync("git init", { stdio: "pipe" });
|
|
234
|
+
const result = parseGitRemote();
|
|
235
|
+
assert.equal(result, null);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("parses HTTPS remote with embedded glpat token", () => {
|
|
239
|
+
execSync("git init", { stdio: "pipe" });
|
|
240
|
+
execSync("git remote add origin https://oauth2:glpat-abc123@gitlab.kommune7.wien/org/repo.git", { stdio: "pipe" });
|
|
241
|
+
const result = parseGitRemote();
|
|
242
|
+
assert.deepStrictEqual(result, {
|
|
243
|
+
host: "gitlab.kommune7.wien",
|
|
244
|
+
repoPath: "org/repo.git",
|
|
245
|
+
gitlabToken: "glpat-abc123",
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("HTTPS without credentials returns no gitlabToken", () => {
|
|
250
|
+
execSync("git init", { stdio: "pipe" });
|
|
251
|
+
execSync("git remote add origin https://github.com/org/repo.git", { stdio: "pipe" });
|
|
252
|
+
const result = parseGitRemote();
|
|
253
|
+
assert.equal(result.host, "github.com");
|
|
254
|
+
assert.equal(result.repoPath, "org/repo.git");
|
|
255
|
+
assert.equal(result.gitlabToken, undefined);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("HTTPS with non-glpat credentials returns no gitlabToken", () => {
|
|
259
|
+
execSync("git init", { stdio: "pipe" });
|
|
260
|
+
execSync("git remote add origin https://user:password@github.com/org/repo.git", { stdio: "pipe" });
|
|
261
|
+
const result = parseGitRemote();
|
|
262
|
+
assert.equal(result.host, "github.com");
|
|
263
|
+
assert.equal(result.repoPath, "org/repo.git");
|
|
264
|
+
assert.equal(result.gitlabToken, undefined);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
describe("fillEnvValues", () => {
|
|
269
|
+
let tmpDir;
|
|
270
|
+
let envPath;
|
|
271
|
+
|
|
272
|
+
beforeEach(() => {
|
|
273
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "kntic-env-test-"));
|
|
274
|
+
envPath = path.join(tmpDir, ".kntic.env");
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
afterEach(() => {
|
|
278
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("replaces existing key", () => {
|
|
282
|
+
fs.writeFileSync(envPath, "GIT_HOST=\nGIT_REPO_PATH=\n");
|
|
283
|
+
fillEnvValues(envPath, { GIT_HOST: "gitlab.kommune7.wien" });
|
|
284
|
+
const content = fs.readFileSync(envPath, "utf8");
|
|
285
|
+
assert.ok(content.includes("GIT_HOST=gitlab.kommune7.wien"));
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("appends missing key", () => {
|
|
289
|
+
fs.writeFileSync(envPath, "GIT_REPO_PATH=some/path.git\n");
|
|
290
|
+
fillEnvValues(envPath, { GIT_HOST: "gitlab.kommune7.wien" });
|
|
291
|
+
const content = fs.readFileSync(envPath, "utf8");
|
|
292
|
+
assert.ok(content.includes("GIT_HOST=gitlab.kommune7.wien"));
|
|
293
|
+
assert.ok(content.includes("GIT_REPO_PATH=some/path.git"));
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("preserves other lines and comments", () => {
|
|
297
|
+
fs.writeFileSync(envPath, "# This is a comment\nAPI_KEY=abc123\nGIT_HOST=\n# Another comment\n");
|
|
298
|
+
fillEnvValues(envPath, { GIT_HOST: "example.com" });
|
|
299
|
+
const content = fs.readFileSync(envPath, "utf8");
|
|
300
|
+
assert.ok(content.includes("# This is a comment"));
|
|
301
|
+
assert.ok(content.includes("API_KEY=abc123"));
|
|
302
|
+
assert.ok(content.includes("GIT_HOST=example.com"));
|
|
303
|
+
assert.ok(content.includes("# Another comment"));
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("handles multiple replacements", () => {
|
|
307
|
+
fs.writeFileSync(envPath, "GIT_HOST=\nGIT_REPO_PATH=\n");
|
|
308
|
+
fillEnvValues(envPath, {
|
|
309
|
+
GIT_HOST: "gitlab.kommune7.wien",
|
|
310
|
+
GIT_REPO_PATH: "kntic-ai/orchestrator/control.git",
|
|
311
|
+
});
|
|
312
|
+
const content = fs.readFileSync(envPath, "utf8");
|
|
313
|
+
assert.ok(content.includes("GIT_HOST=gitlab.kommune7.wien"));
|
|
314
|
+
assert.ok(content.includes("GIT_REPO_PATH=kntic-ai/orchestrator/control.git"));
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("fills GITLAB_TOKEN from git remote", () => {
|
|
318
|
+
fs.writeFileSync(envPath, "GITLAB_TOKEN=***\nGIT_HOST=\n");
|
|
319
|
+
fillEnvValues(envPath, { GITLAB_TOKEN: "glpat-abc123" });
|
|
320
|
+
const content = fs.readFileSync(envPath, "utf8");
|
|
321
|
+
assert.ok(content.includes("GITLAB_TOKEN=glpat-abc123"));
|
|
322
|
+
});
|
|
323
|
+
});
|
package/src/commands/usage.js
CHANGED
|
@@ -6,6 +6,8 @@ function usage() {
|
|
|
6
6
|
console.log("Available commands:\n");
|
|
7
7
|
console.log(" usage List all available sub-commands");
|
|
8
8
|
console.log(" init Download and extract the KNTIC bootstrap template into the current directory");
|
|
9
|
+
console.log(" --quick Default mode — non-interactive, auto-detects git remote");
|
|
10
|
+
console.log(" --interactive Walk through .kntic.env values interactively (-i)");
|
|
9
11
|
console.log(" start Build and start KNTIC services via docker compose (uses kntic.yml + .kntic.env)");
|
|
10
12
|
console.log(" --screen Run inside a GNU screen session");
|
|
11
13
|
console.log(" stop Stop KNTIC services via docker compose");
|