@kntic/kntic 0.5.0 → 0.6.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/package.json +1 -1
- package/src/cli.js +2 -1
- package/src/commands/init.js +138 -1
- package/src/commands/init.test.js +134 -1
- package/src/commands/usage.js +2 -0
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,121 @@ 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
|
+
async function init(options = {}) {
|
|
159
274
|
// Resolve current version from the artifact metadata file
|
|
160
275
|
const artifactFilename = await fetchText(BOOTSTRAP_ARTIFACT_URL);
|
|
161
276
|
const version = extractVersion(artifactFilename);
|
|
@@ -174,9 +289,31 @@ async function init() {
|
|
|
174
289
|
// Clean up
|
|
175
290
|
fs.unlinkSync(tmpFile);
|
|
176
291
|
|
|
292
|
+
// Auto-detect git remote
|
|
293
|
+
const gitInfo = parseGitRemote();
|
|
294
|
+
|
|
295
|
+
// Auto-fill git values (both modes)
|
|
296
|
+
const autoValues = {};
|
|
297
|
+
if (gitInfo) {
|
|
298
|
+
autoValues.GIT_HOST = gitInfo.host;
|
|
299
|
+
autoValues.GIT_REPO_PATH = gitInfo.repoPath;
|
|
300
|
+
if (gitInfo.gitlabToken) {
|
|
301
|
+
autoValues.GITLAB_TOKEN = gitInfo.gitlabToken;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (Object.keys(autoValues).length > 0) {
|
|
305
|
+
fillEnvValues('.kntic.env', autoValues);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (options.interactive) {
|
|
309
|
+
await interactiveEnvSetup('.kntic.env');
|
|
310
|
+
}
|
|
311
|
+
|
|
177
312
|
console.log("Done. KNTIC project bootstrapped successfully.");
|
|
178
313
|
}
|
|
179
314
|
|
|
180
315
|
module.exports = init;
|
|
181
316
|
module.exports.extractArchive = extractArchive;
|
|
182
317
|
module.exports.extractVersion = extractVersion;
|
|
318
|
+
module.exports.parseGitRemote = parseGitRemote;
|
|
319
|
+
module.exports.fillEnvValues = fillEnvValues;
|
|
@@ -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 } = require("./init");
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Helper — create a tar.gz archive in `tmpDir` containing the given files.
|
|
@@ -126,3 +126,136 @@ describe("extractVersion", () => {
|
|
|
126
126
|
assert.throws(() => extractVersion("no-version-here.tar.gz"));
|
|
127
127
|
});
|
|
128
128
|
});
|
|
129
|
+
|
|
130
|
+
describe("parseGitRemote", () => {
|
|
131
|
+
let tmpDir;
|
|
132
|
+
let originalCwd;
|
|
133
|
+
|
|
134
|
+
beforeEach(() => {
|
|
135
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "kntic-git-test-"));
|
|
136
|
+
originalCwd = process.cwd();
|
|
137
|
+
process.chdir(tmpDir);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
afterEach(() => {
|
|
141
|
+
process.chdir(originalCwd);
|
|
142
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("parses SSH remote", () => {
|
|
146
|
+
execSync("git init", { stdio: "pipe" });
|
|
147
|
+
execSync("git remote add origin git@gitlab.kommune7.wien:kntic-ai/orchestrator/control.git", { stdio: "pipe" });
|
|
148
|
+
const result = parseGitRemote();
|
|
149
|
+
assert.deepStrictEqual(result, {
|
|
150
|
+
host: "gitlab.kommune7.wien",
|
|
151
|
+
repoPath: "kntic-ai/orchestrator/control.git",
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("parses HTTPS remote", () => {
|
|
156
|
+
execSync("git init", { stdio: "pipe" });
|
|
157
|
+
execSync("git remote add origin https://github.com/org/repo.git", { stdio: "pipe" });
|
|
158
|
+
const result = parseGitRemote();
|
|
159
|
+
assert.deepStrictEqual(result, {
|
|
160
|
+
host: "github.com",
|
|
161
|
+
repoPath: "org/repo.git",
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("returns null when no .git dir", () => {
|
|
166
|
+
const result = parseGitRemote();
|
|
167
|
+
assert.equal(result, null);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("returns null when no origin remote", () => {
|
|
171
|
+
execSync("git init", { stdio: "pipe" });
|
|
172
|
+
const result = parseGitRemote();
|
|
173
|
+
assert.equal(result, null);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("parses HTTPS remote with embedded glpat token", () => {
|
|
177
|
+
execSync("git init", { stdio: "pipe" });
|
|
178
|
+
execSync("git remote add origin https://oauth2:glpat-abc123@gitlab.kommune7.wien/org/repo.git", { stdio: "pipe" });
|
|
179
|
+
const result = parseGitRemote();
|
|
180
|
+
assert.deepStrictEqual(result, {
|
|
181
|
+
host: "gitlab.kommune7.wien",
|
|
182
|
+
repoPath: "org/repo.git",
|
|
183
|
+
gitlabToken: "glpat-abc123",
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("HTTPS without credentials returns no gitlabToken", () => {
|
|
188
|
+
execSync("git init", { stdio: "pipe" });
|
|
189
|
+
execSync("git remote add origin https://github.com/org/repo.git", { stdio: "pipe" });
|
|
190
|
+
const result = parseGitRemote();
|
|
191
|
+
assert.equal(result.host, "github.com");
|
|
192
|
+
assert.equal(result.repoPath, "org/repo.git");
|
|
193
|
+
assert.equal(result.gitlabToken, undefined);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("HTTPS with non-glpat credentials returns no gitlabToken", () => {
|
|
197
|
+
execSync("git init", { stdio: "pipe" });
|
|
198
|
+
execSync("git remote add origin https://user:password@github.com/org/repo.git", { stdio: "pipe" });
|
|
199
|
+
const result = parseGitRemote();
|
|
200
|
+
assert.equal(result.host, "github.com");
|
|
201
|
+
assert.equal(result.repoPath, "org/repo.git");
|
|
202
|
+
assert.equal(result.gitlabToken, undefined);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("fillEnvValues", () => {
|
|
207
|
+
let tmpDir;
|
|
208
|
+
let envPath;
|
|
209
|
+
|
|
210
|
+
beforeEach(() => {
|
|
211
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "kntic-env-test-"));
|
|
212
|
+
envPath = path.join(tmpDir, ".kntic.env");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
afterEach(() => {
|
|
216
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("replaces existing key", () => {
|
|
220
|
+
fs.writeFileSync(envPath, "GIT_HOST=\nGIT_REPO_PATH=\n");
|
|
221
|
+
fillEnvValues(envPath, { GIT_HOST: "gitlab.kommune7.wien" });
|
|
222
|
+
const content = fs.readFileSync(envPath, "utf8");
|
|
223
|
+
assert.ok(content.includes("GIT_HOST=gitlab.kommune7.wien"));
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("appends missing key", () => {
|
|
227
|
+
fs.writeFileSync(envPath, "GIT_REPO_PATH=some/path.git\n");
|
|
228
|
+
fillEnvValues(envPath, { GIT_HOST: "gitlab.kommune7.wien" });
|
|
229
|
+
const content = fs.readFileSync(envPath, "utf8");
|
|
230
|
+
assert.ok(content.includes("GIT_HOST=gitlab.kommune7.wien"));
|
|
231
|
+
assert.ok(content.includes("GIT_REPO_PATH=some/path.git"));
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("preserves other lines and comments", () => {
|
|
235
|
+
fs.writeFileSync(envPath, "# This is a comment\nAPI_KEY=abc123\nGIT_HOST=\n# Another comment\n");
|
|
236
|
+
fillEnvValues(envPath, { GIT_HOST: "example.com" });
|
|
237
|
+
const content = fs.readFileSync(envPath, "utf8");
|
|
238
|
+
assert.ok(content.includes("# This is a comment"));
|
|
239
|
+
assert.ok(content.includes("API_KEY=abc123"));
|
|
240
|
+
assert.ok(content.includes("GIT_HOST=example.com"));
|
|
241
|
+
assert.ok(content.includes("# Another comment"));
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("handles multiple replacements", () => {
|
|
245
|
+
fs.writeFileSync(envPath, "GIT_HOST=\nGIT_REPO_PATH=\n");
|
|
246
|
+
fillEnvValues(envPath, {
|
|
247
|
+
GIT_HOST: "gitlab.kommune7.wien",
|
|
248
|
+
GIT_REPO_PATH: "kntic-ai/orchestrator/control.git",
|
|
249
|
+
});
|
|
250
|
+
const content = fs.readFileSync(envPath, "utf8");
|
|
251
|
+
assert.ok(content.includes("GIT_HOST=gitlab.kommune7.wien"));
|
|
252
|
+
assert.ok(content.includes("GIT_REPO_PATH=kntic-ai/orchestrator/control.git"));
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("fills GITLAB_TOKEN from git remote", () => {
|
|
256
|
+
fs.writeFileSync(envPath, "GITLAB_TOKEN=***\nGIT_HOST=\n");
|
|
257
|
+
fillEnvValues(envPath, { GITLAB_TOKEN: "glpat-abc123" });
|
|
258
|
+
const content = fs.readFileSync(envPath, "utf8");
|
|
259
|
+
assert.ok(content.includes("GITLAB_TOKEN=glpat-abc123"));
|
|
260
|
+
});
|
|
261
|
+
});
|
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");
|