@kntic/kntic 0.2.1 → 0.3.1
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 +5 -0
- package/src/commands/index.js +2 -1
- package/src/commands/init.js +47 -1
- package/src/commands/init.test.js +128 -0
- package/src/commands/update.js +212 -0
- package/src/commands/update.test.js +271 -0
- package/src/commands/usage.js +1 -0
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -21,6 +21,11 @@ if (!subcommand || subcommand === "usage") {
|
|
|
21
21
|
console.error(`Error: ${err.message}`);
|
|
22
22
|
process.exit(1);
|
|
23
23
|
}
|
|
24
|
+
} else if (subcommand === "update") {
|
|
25
|
+
commands.update().catch((err) => {
|
|
26
|
+
console.error(`Error: ${err.message}`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
});
|
|
24
29
|
} else {
|
|
25
30
|
console.error(`Unknown command: "${subcommand}"\n`);
|
|
26
31
|
commands.usage();
|
package/src/commands/index.js
CHANGED
package/src/commands/init.js
CHANGED
|
@@ -111,6 +111,50 @@ function download(url, dest, maxRedirects = 5) {
|
|
|
111
111
|
});
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Extract a tarball into `destDir`, merging `.gitignore` instead of
|
|
116
|
+
* overwriting it. If `.gitignore` already exists in `destDir`, the
|
|
117
|
+
* archive's version is appended (with a separator comment); otherwise
|
|
118
|
+
* the file is extracted normally alongside everything else.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} tarball – path to the .tar.gz file
|
|
121
|
+
* @param {string} destDir – target directory (usually ".")
|
|
122
|
+
*/
|
|
123
|
+
function extractArchive(tarball, destDir) {
|
|
124
|
+
const gitignorePath = path.join(destDir, ".gitignore");
|
|
125
|
+
const gitignoreExists = fs.existsSync(gitignorePath);
|
|
126
|
+
|
|
127
|
+
if (gitignoreExists) {
|
|
128
|
+
// 1. Extract everything *except* .gitignore
|
|
129
|
+
execSync(
|
|
130
|
+
`tar xzf "${tarball}" -C "${destDir}" --exclude=".gitignore"`,
|
|
131
|
+
{ stdio: "inherit" }
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// 2. Read .gitignore content from the archive (may not exist in archive)
|
|
135
|
+
let archiveGitignore;
|
|
136
|
+
try {
|
|
137
|
+
archiveGitignore = execSync(
|
|
138
|
+
`tar xzf "${tarball}" --to-stdout .gitignore 2>/dev/null || tar xzf "${tarball}" --to-stdout "./.gitignore" 2>/dev/null`,
|
|
139
|
+
{ encoding: "utf8" }
|
|
140
|
+
);
|
|
141
|
+
} catch {
|
|
142
|
+
// Archive does not contain a .gitignore — nothing to merge
|
|
143
|
+
archiveGitignore = null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (archiveGitignore && archiveGitignore.trim().length > 0) {
|
|
147
|
+
const existing = fs.readFileSync(gitignorePath, "utf8");
|
|
148
|
+
const separator = "\n# --- kntic bootstrap ---\n";
|
|
149
|
+
const merged = existing.trimEnd() + separator + archiveGitignore.trimEnd() + "\n";
|
|
150
|
+
fs.writeFileSync(gitignorePath, merged);
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
// No existing .gitignore — plain extraction
|
|
154
|
+
execSync(`tar xzf "${tarball}" -C "${destDir}"`, { stdio: "inherit" });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
114
158
|
async function init() {
|
|
115
159
|
// Resolve current version from the artifact metadata file
|
|
116
160
|
const artifactFilename = await fetchText(BOOTSTRAP_ARTIFACT_URL);
|
|
@@ -122,7 +166,7 @@ async function init() {
|
|
|
122
166
|
await download(BOOTSTRAP_URL, tmpFile);
|
|
123
167
|
|
|
124
168
|
console.log("Extracting into current directory…");
|
|
125
|
-
|
|
169
|
+
extractArchive(tmpFile, ".");
|
|
126
170
|
|
|
127
171
|
// Append version to .kntic.env (project root) — do not replace existing content
|
|
128
172
|
fs.appendFileSync(".kntic.env", `KNTIC_VERSION=${version}\n`);
|
|
@@ -134,3 +178,5 @@ async function init() {
|
|
|
134
178
|
}
|
|
135
179
|
|
|
136
180
|
module.exports = init;
|
|
181
|
+
module.exports.extractArchive = extractArchive;
|
|
182
|
+
module.exports.extractVersion = extractVersion;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { describe, it, beforeEach, afterEach } = require("node:test");
|
|
4
|
+
const assert = require("node:assert/strict");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const { execSync } = require("child_process");
|
|
9
|
+
|
|
10
|
+
const { extractArchive, extractVersion } = require("./init");
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Helper — create a tar.gz archive in `tmpDir` containing the given files.
|
|
14
|
+
* `files` is an object like { ".gitignore": "node_modules/\n", "README.md": "# Hi" }
|
|
15
|
+
*/
|
|
16
|
+
function createTarball(tmpDir, files) {
|
|
17
|
+
const contentDir = path.join(tmpDir, "_archive_content");
|
|
18
|
+
fs.mkdirSync(contentDir, { recursive: true });
|
|
19
|
+
|
|
20
|
+
for (const [name, content] of Object.entries(files)) {
|
|
21
|
+
const filePath = path.join(contentDir, name);
|
|
22
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
23
|
+
fs.writeFileSync(filePath, content);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const tarball = path.join(tmpDir, "test.tar.gz");
|
|
27
|
+
execSync(`tar czf "${tarball}" -C "${contentDir}" .`, { stdio: "pipe" });
|
|
28
|
+
return tarball;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("extractArchive", () => {
|
|
32
|
+
let tmpDir;
|
|
33
|
+
let destDir;
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "kntic-test-"));
|
|
37
|
+
destDir = path.join(tmpDir, "dest");
|
|
38
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("extracts all files when .gitignore does not exist", () => {
|
|
46
|
+
const tarball = createTarball(tmpDir, {
|
|
47
|
+
".gitignore": "node_modules/\n",
|
|
48
|
+
"README.md": "# Hello\n",
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
extractArchive(tarball, destDir);
|
|
52
|
+
|
|
53
|
+
assert.equal(
|
|
54
|
+
fs.readFileSync(path.join(destDir, ".gitignore"), "utf8"),
|
|
55
|
+
"node_modules/\n"
|
|
56
|
+
);
|
|
57
|
+
assert.equal(
|
|
58
|
+
fs.readFileSync(path.join(destDir, "README.md"), "utf8"),
|
|
59
|
+
"# Hello\n"
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("appends archive .gitignore to existing one", () => {
|
|
64
|
+
// Pre-existing .gitignore
|
|
65
|
+
fs.writeFileSync(
|
|
66
|
+
path.join(destDir, ".gitignore"),
|
|
67
|
+
"*.log\n.env\n"
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const tarball = createTarball(tmpDir, {
|
|
71
|
+
".gitignore": "node_modules/\ndist/\n",
|
|
72
|
+
"README.md": "# Hello\n",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
extractArchive(tarball, destDir);
|
|
76
|
+
|
|
77
|
+
const result = fs.readFileSync(path.join(destDir, ".gitignore"), "utf8");
|
|
78
|
+
|
|
79
|
+
// Original content must still be present
|
|
80
|
+
assert.ok(result.includes("*.log"), "original entry *.log must be preserved");
|
|
81
|
+
assert.ok(result.includes(".env"), "original entry .env must be preserved");
|
|
82
|
+
|
|
83
|
+
// Archive content must be appended
|
|
84
|
+
assert.ok(result.includes("node_modules/"), "archive entry node_modules/ must be appended");
|
|
85
|
+
assert.ok(result.includes("dist/"), "archive entry dist/ must be appended");
|
|
86
|
+
|
|
87
|
+
// Separator comment must be present
|
|
88
|
+
assert.ok(
|
|
89
|
+
result.includes("# --- kntic bootstrap ---"),
|
|
90
|
+
"separator comment must be present between original and appended content"
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// Other files still extracted
|
|
94
|
+
assert.equal(
|
|
95
|
+
fs.readFileSync(path.join(destDir, "README.md"), "utf8"),
|
|
96
|
+
"# Hello\n"
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("leaves existing .gitignore unchanged when archive has none", () => {
|
|
101
|
+
fs.writeFileSync(
|
|
102
|
+
path.join(destDir, ".gitignore"),
|
|
103
|
+
"*.log\n"
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const tarball = createTarball(tmpDir, {
|
|
107
|
+
"README.md": "# Hello\n",
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
extractArchive(tarball, destDir);
|
|
111
|
+
|
|
112
|
+
assert.equal(
|
|
113
|
+
fs.readFileSync(path.join(destDir, ".gitignore"), "utf8"),
|
|
114
|
+
"*.log\n"
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("extractVersion", () => {
|
|
120
|
+
it("parses version from artifact filename", () => {
|
|
121
|
+
assert.equal(extractVersion("kntic-bootstrap-v0.0.10.tar.gz"), "0.0.10");
|
|
122
|
+
assert.equal(extractVersion("kntic-bootstrap-v1.2.3.tar.gz"), "1.2.3");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("throws on invalid filename", () => {
|
|
126
|
+
assert.throws(() => extractVersion("no-version-here.tar.gz"));
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const https = require("https");
|
|
4
|
+
const http = require("http");
|
|
5
|
+
const { execSync } = require("child_process");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const os = require("os");
|
|
9
|
+
|
|
10
|
+
const BOOTSTRAP_ARTIFACT_URL =
|
|
11
|
+
"https://minio.kommune7.wien/public/kntic-bootstrap-latest.artifact";
|
|
12
|
+
const BOOTSTRAP_URL =
|
|
13
|
+
"https://minio.kommune7.wien/public/kntic-bootstrap-latest.tar.gz";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Fetch a small text resource from a URL (follows redirects).
|
|
17
|
+
* Returns the response body as a trimmed string.
|
|
18
|
+
*/
|
|
19
|
+
function fetchText(url, maxRedirects = 5) {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
if (maxRedirects <= 0) {
|
|
22
|
+
return reject(new Error("Too many redirects"));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const client = url.startsWith("https") ? https : http;
|
|
26
|
+
|
|
27
|
+
client
|
|
28
|
+
.get(url, (res) => {
|
|
29
|
+
if ([301, 302, 303, 307, 308].includes(res.statusCode)) {
|
|
30
|
+
const location = res.headers.location;
|
|
31
|
+
if (!location) {
|
|
32
|
+
return reject(new Error("Redirect with no Location header"));
|
|
33
|
+
}
|
|
34
|
+
res.resume();
|
|
35
|
+
return resolve(fetchText(location, maxRedirects - 1));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (res.statusCode !== 200) {
|
|
39
|
+
res.resume();
|
|
40
|
+
return reject(
|
|
41
|
+
new Error(`Fetch failed — HTTP ${res.statusCode}`)
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const chunks = [];
|
|
46
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
47
|
+
res.on("end", () => resolve(Buffer.concat(chunks).toString("utf8").trim()));
|
|
48
|
+
res.on("error", reject);
|
|
49
|
+
})
|
|
50
|
+
.on("error", reject);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Follow redirects and download a URL to a local file path.
|
|
56
|
+
*/
|
|
57
|
+
function download(url, dest, maxRedirects = 5) {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
if (maxRedirects <= 0) {
|
|
60
|
+
return reject(new Error("Too many redirects"));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const client = url.startsWith("https") ? https : http;
|
|
64
|
+
|
|
65
|
+
client
|
|
66
|
+
.get(url, (res) => {
|
|
67
|
+
if ([301, 302, 303, 307, 308].includes(res.statusCode)) {
|
|
68
|
+
const location = res.headers.location;
|
|
69
|
+
if (!location) {
|
|
70
|
+
return reject(new Error("Redirect with no Location header"));
|
|
71
|
+
}
|
|
72
|
+
res.resume();
|
|
73
|
+
return resolve(download(location, dest, maxRedirects - 1));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (res.statusCode !== 200) {
|
|
77
|
+
res.resume();
|
|
78
|
+
return reject(
|
|
79
|
+
new Error(`Download failed — HTTP ${res.statusCode}`)
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const file = fs.createWriteStream(dest);
|
|
84
|
+
res.pipe(file);
|
|
85
|
+
file.on("finish", () => file.close(resolve));
|
|
86
|
+
file.on("error", (err) => {
|
|
87
|
+
fs.unlink(dest, () => {});
|
|
88
|
+
reject(err);
|
|
89
|
+
});
|
|
90
|
+
})
|
|
91
|
+
.on("error", (err) => {
|
|
92
|
+
fs.unlink(dest, () => {});
|
|
93
|
+
reject(err);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Extract semantic version from an artifact filename.
|
|
100
|
+
* E.g. "kntic-bootstrap-v0.0.10.tar.gz" → "0.0.10"
|
|
101
|
+
*/
|
|
102
|
+
function extractVersion(artifactFilename) {
|
|
103
|
+
const match = artifactFilename.match(/v(\d+\.\d+\.\d+)/);
|
|
104
|
+
if (!match) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Unable to extract version from artifact filename: ${artifactFilename}`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return match[1];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Update or create the KNTIC_VERSION variable in a .kntic.env file.
|
|
114
|
+
* If the file exists and contains a KNTIC_VERSION line, that line is replaced.
|
|
115
|
+
* If the file exists but has no KNTIC_VERSION, it is appended.
|
|
116
|
+
* If the file does not exist, it is created with the variable.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} version – semantic version string (e.g. "0.0.10")
|
|
119
|
+
* @param {string} envPath – path to .kntic.env (default: ".kntic.env")
|
|
120
|
+
*/
|
|
121
|
+
function updateEnvVersion(version, envPath = ".kntic.env") {
|
|
122
|
+
const line = `KNTIC_VERSION=${version}`;
|
|
123
|
+
|
|
124
|
+
if (!fs.existsSync(envPath)) {
|
|
125
|
+
fs.writeFileSync(envPath, line + "\n");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const content = fs.readFileSync(envPath, "utf8");
|
|
130
|
+
const regex = /^KNTIC_VERSION=.*$/m;
|
|
131
|
+
|
|
132
|
+
if (regex.test(content)) {
|
|
133
|
+
const updated = content.replace(regex, line);
|
|
134
|
+
fs.writeFileSync(envPath, updated);
|
|
135
|
+
} else {
|
|
136
|
+
const separator = content.endsWith("\n") ? "" : "\n";
|
|
137
|
+
fs.writeFileSync(envPath, content + separator + line + "\n");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Recursively remove all contents of a directory (but not the directory itself).
|
|
143
|
+
*/
|
|
144
|
+
function clearDirectory(dirPath) {
|
|
145
|
+
if (!fs.existsSync(dirPath)) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
149
|
+
for (const entry of entries) {
|
|
150
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
151
|
+
if (entry.isDirectory()) {
|
|
152
|
+
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
153
|
+
} else {
|
|
154
|
+
fs.unlinkSync(fullPath);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Extract only .kntic/lib/** from a tarball into the destination directory.
|
|
161
|
+
* Clears the existing .kntic/lib directory first, then extracts fresh content.
|
|
162
|
+
*
|
|
163
|
+
* @param {string} tarball – path to the .tar.gz file
|
|
164
|
+
* @param {string} destDir – target directory (usually ".")
|
|
165
|
+
*/
|
|
166
|
+
function extractLibOnly(tarball, destDir) {
|
|
167
|
+
const libDir = path.join(destDir, ".kntic", "lib");
|
|
168
|
+
|
|
169
|
+
// Ensure the target directory exists
|
|
170
|
+
fs.mkdirSync(libDir, { recursive: true });
|
|
171
|
+
|
|
172
|
+
// Clear existing lib contents
|
|
173
|
+
clearDirectory(libDir);
|
|
174
|
+
|
|
175
|
+
// Extract only .kntic/lib/ from the archive.
|
|
176
|
+
// Tar entries are prefixed with ./ (created with `tar -C dir .`), so we
|
|
177
|
+
// pass the exact member path `./.kntic/lib/` which matches the directory
|
|
178
|
+
// and all its descendants.
|
|
179
|
+
execSync(
|
|
180
|
+
`tar xzf "${tarball}" -C "${destDir}" "./.kntic/lib/"`,
|
|
181
|
+
{ stdio: "pipe" }
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function update() {
|
|
186
|
+
// Resolve current version from the artifact metadata file
|
|
187
|
+
const artifactFilename = await fetchText(BOOTSTRAP_ARTIFACT_URL);
|
|
188
|
+
const version = extractVersion(artifactFilename);
|
|
189
|
+
|
|
190
|
+
const tmpFile = path.join(os.tmpdir(), `kntic-bootstrap-${Date.now()}.tar.gz`);
|
|
191
|
+
|
|
192
|
+
console.log(`Downloading KNTIC bootstrap archive… (v${version})`);
|
|
193
|
+
await download(BOOTSTRAP_URL, tmpFile);
|
|
194
|
+
|
|
195
|
+
console.log("Updating .kntic/lib …");
|
|
196
|
+
extractLibOnly(tmpFile, ".");
|
|
197
|
+
|
|
198
|
+
// Update KNTIC_VERSION in .kntic.env
|
|
199
|
+
updateEnvVersion(version);
|
|
200
|
+
|
|
201
|
+
// Clean up
|
|
202
|
+
fs.unlinkSync(tmpFile);
|
|
203
|
+
|
|
204
|
+
console.log(`@kntic/kntic@${version}`);
|
|
205
|
+
console.log("Done. .kntic/lib updated successfully.");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = update;
|
|
209
|
+
module.exports.extractLibOnly = extractLibOnly;
|
|
210
|
+
module.exports.extractVersion = extractVersion;
|
|
211
|
+
module.exports.clearDirectory = clearDirectory;
|
|
212
|
+
module.exports.updateEnvVersion = updateEnvVersion;
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { describe, it, beforeEach, afterEach } = require("node:test");
|
|
4
|
+
const assert = require("node:assert/strict");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const { execSync } = require("child_process");
|
|
9
|
+
|
|
10
|
+
const { extractLibOnly, extractVersion, clearDirectory, updateEnvVersion } = require("./update");
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Helper — create a tar.gz archive in `tmpDir` containing the given files.
|
|
14
|
+
* `files` is an object like { ".kntic/lib/foo.py": "content", "README.md": "# Hi" }
|
|
15
|
+
*/
|
|
16
|
+
function createTarball(tmpDir, files) {
|
|
17
|
+
const contentDir = path.join(tmpDir, "_archive_content");
|
|
18
|
+
fs.mkdirSync(contentDir, { recursive: true });
|
|
19
|
+
|
|
20
|
+
for (const [name, content] of Object.entries(files)) {
|
|
21
|
+
const filePath = path.join(contentDir, name);
|
|
22
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
23
|
+
fs.writeFileSync(filePath, content);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const tarball = path.join(tmpDir, "test.tar.gz");
|
|
27
|
+
execSync(`tar czf "${tarball}" -C "${contentDir}" .`, { stdio: "pipe" });
|
|
28
|
+
return tarball;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("clearDirectory", () => {
|
|
32
|
+
let tmpDir;
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "kntic-test-clear-"));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("removes all files and subdirectories but keeps the directory itself", () => {
|
|
43
|
+
const dir = path.join(tmpDir, "target");
|
|
44
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
45
|
+
fs.writeFileSync(path.join(dir, "a.txt"), "hello");
|
|
46
|
+
fs.mkdirSync(path.join(dir, "sub"), { recursive: true });
|
|
47
|
+
fs.writeFileSync(path.join(dir, "sub", "b.txt"), "world");
|
|
48
|
+
|
|
49
|
+
clearDirectory(dir);
|
|
50
|
+
|
|
51
|
+
assert.ok(fs.existsSync(dir), "directory itself must still exist");
|
|
52
|
+
assert.deepEqual(fs.readdirSync(dir), [], "directory must be empty");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("does nothing if directory does not exist", () => {
|
|
56
|
+
// Should not throw
|
|
57
|
+
clearDirectory(path.join(tmpDir, "nonexistent"));
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("extractLibOnly", () => {
|
|
62
|
+
let tmpDir;
|
|
63
|
+
let destDir;
|
|
64
|
+
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "kntic-test-update-"));
|
|
67
|
+
destDir = path.join(tmpDir, "dest");
|
|
68
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("extracts only .kntic/lib files from the archive", () => {
|
|
76
|
+
const tarball = createTarball(tmpDir, {
|
|
77
|
+
".kntic/lib/orchestrator.py": "# orchestrator\n",
|
|
78
|
+
".kntic/lib/skills/validator.py": "# validator\n",
|
|
79
|
+
".kntic/MEMORY.MD": "# Memory\n",
|
|
80
|
+
".kntic/adrs/ADR-001.md": "# ADR\n",
|
|
81
|
+
"README.md": "# Hello\n",
|
|
82
|
+
"package.json": '{"name":"test"}\n',
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
extractLibOnly(tarball, destDir);
|
|
86
|
+
|
|
87
|
+
// .kntic/lib files must be extracted
|
|
88
|
+
assert.equal(
|
|
89
|
+
fs.readFileSync(path.join(destDir, ".kntic", "lib", "orchestrator.py"), "utf8"),
|
|
90
|
+
"# orchestrator\n"
|
|
91
|
+
);
|
|
92
|
+
assert.equal(
|
|
93
|
+
fs.readFileSync(path.join(destDir, ".kntic", "lib", "skills", "validator.py"), "utf8"),
|
|
94
|
+
"# validator\n"
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// Files outside .kntic/lib must NOT be extracted
|
|
98
|
+
assert.ok(
|
|
99
|
+
!fs.existsSync(path.join(destDir, ".kntic", "MEMORY.MD")),
|
|
100
|
+
"MEMORY.MD must not be extracted"
|
|
101
|
+
);
|
|
102
|
+
assert.ok(
|
|
103
|
+
!fs.existsSync(path.join(destDir, ".kntic", "adrs", "ADR-001.md")),
|
|
104
|
+
"ADR files must not be extracted"
|
|
105
|
+
);
|
|
106
|
+
assert.ok(
|
|
107
|
+
!fs.existsSync(path.join(destDir, "README.md")),
|
|
108
|
+
"README.md must not be extracted"
|
|
109
|
+
);
|
|
110
|
+
assert.ok(
|
|
111
|
+
!fs.existsSync(path.join(destDir, "package.json")),
|
|
112
|
+
"package.json must not be extracted"
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("replaces existing .kntic/lib content", () => {
|
|
117
|
+
// Pre-existing lib content
|
|
118
|
+
const libDir = path.join(destDir, ".kntic", "lib");
|
|
119
|
+
fs.mkdirSync(libDir, { recursive: true });
|
|
120
|
+
fs.writeFileSync(path.join(libDir, "old_file.py"), "# old\n");
|
|
121
|
+
fs.writeFileSync(path.join(libDir, "orchestrator.py"), "# old orchestrator\n");
|
|
122
|
+
|
|
123
|
+
const tarball = createTarball(tmpDir, {
|
|
124
|
+
".kntic/lib/orchestrator.py": "# new orchestrator\n",
|
|
125
|
+
".kntic/lib/new_file.py": "# new\n",
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
extractLibOnly(tarball, destDir);
|
|
129
|
+
|
|
130
|
+
// Old file must be gone
|
|
131
|
+
assert.ok(
|
|
132
|
+
!fs.existsSync(path.join(libDir, "old_file.py")),
|
|
133
|
+
"old_file.py must be removed"
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// New files must be present
|
|
137
|
+
assert.equal(
|
|
138
|
+
fs.readFileSync(path.join(libDir, "orchestrator.py"), "utf8"),
|
|
139
|
+
"# new orchestrator\n"
|
|
140
|
+
);
|
|
141
|
+
assert.equal(
|
|
142
|
+
fs.readFileSync(path.join(libDir, "new_file.py"), "utf8"),
|
|
143
|
+
"# new\n"
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("preserves files outside .kntic/lib that already exist on disk", () => {
|
|
148
|
+
// Pre-existing files outside lib
|
|
149
|
+
const knticDir = path.join(destDir, ".kntic");
|
|
150
|
+
fs.mkdirSync(knticDir, { recursive: true });
|
|
151
|
+
fs.writeFileSync(path.join(knticDir, "MEMORY.MD"), "# My memory\n");
|
|
152
|
+
fs.writeFileSync(path.join(destDir, "README.md"), "# My readme\n");
|
|
153
|
+
|
|
154
|
+
const tarball = createTarball(tmpDir, {
|
|
155
|
+
".kntic/lib/orchestrator.py": "# orchestrator\n",
|
|
156
|
+
".kntic/MEMORY.MD": "# Archive memory\n",
|
|
157
|
+
"README.md": "# Archive readme\n",
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
extractLibOnly(tarball, destDir);
|
|
161
|
+
|
|
162
|
+
// Existing files outside lib must be untouched
|
|
163
|
+
assert.equal(
|
|
164
|
+
fs.readFileSync(path.join(knticDir, "MEMORY.MD"), "utf8"),
|
|
165
|
+
"# My memory\n",
|
|
166
|
+
"MEMORY.MD must not be overwritten"
|
|
167
|
+
);
|
|
168
|
+
assert.equal(
|
|
169
|
+
fs.readFileSync(path.join(destDir, "README.md"), "utf8"),
|
|
170
|
+
"# My readme\n",
|
|
171
|
+
"README.md must not be overwritten"
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// Lib files must be extracted
|
|
175
|
+
assert.equal(
|
|
176
|
+
fs.readFileSync(path.join(knticDir, "lib", "orchestrator.py"), "utf8"),
|
|
177
|
+
"# orchestrator\n"
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("handles nested subdirectories in .kntic/lib", () => {
|
|
182
|
+
const tarball = createTarball(tmpDir, {
|
|
183
|
+
".kntic/lib/skills/validator.py": "# validator\n",
|
|
184
|
+
".kntic/lib/skills/navigation.py": "# navigation\n",
|
|
185
|
+
".kntic/lib/agent_runner.py": "# agent_runner\n",
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
extractLibOnly(tarball, destDir);
|
|
189
|
+
|
|
190
|
+
assert.equal(
|
|
191
|
+
fs.readFileSync(path.join(destDir, ".kntic", "lib", "skills", "validator.py"), "utf8"),
|
|
192
|
+
"# validator\n"
|
|
193
|
+
);
|
|
194
|
+
assert.equal(
|
|
195
|
+
fs.readFileSync(path.join(destDir, ".kntic", "lib", "skills", "navigation.py"), "utf8"),
|
|
196
|
+
"# navigation\n"
|
|
197
|
+
);
|
|
198
|
+
assert.equal(
|
|
199
|
+
fs.readFileSync(path.join(destDir, ".kntic", "lib", "agent_runner.py"), "utf8"),
|
|
200
|
+
"# agent_runner\n"
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe("updateEnvVersion", () => {
|
|
206
|
+
let tmpDir;
|
|
207
|
+
|
|
208
|
+
beforeEach(() => {
|
|
209
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "kntic-test-env-"));
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
afterEach(() => {
|
|
213
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("creates the file with KNTIC_VERSION if it does not exist", () => {
|
|
217
|
+
const envPath = path.join(tmpDir, ".kntic.env");
|
|
218
|
+
updateEnvVersion("1.2.3", envPath);
|
|
219
|
+
|
|
220
|
+
assert.equal(fs.readFileSync(envPath, "utf8"), "KNTIC_VERSION=1.2.3\n");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("appends KNTIC_VERSION if file exists but variable is missing", () => {
|
|
224
|
+
const envPath = path.join(tmpDir, ".kntic.env");
|
|
225
|
+
fs.writeFileSync(envPath, "UID=1000\nGID=1000\n");
|
|
226
|
+
|
|
227
|
+
updateEnvVersion("0.0.10", envPath);
|
|
228
|
+
|
|
229
|
+
const content = fs.readFileSync(envPath, "utf8");
|
|
230
|
+
assert.ok(content.includes("UID=1000"), "existing content must be preserved");
|
|
231
|
+
assert.ok(content.includes("GID=1000"), "existing content must be preserved");
|
|
232
|
+
assert.ok(content.includes("KNTIC_VERSION=0.0.10"), "version must be appended");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("replaces existing KNTIC_VERSION in-place", () => {
|
|
236
|
+
const envPath = path.join(tmpDir, ".kntic.env");
|
|
237
|
+
fs.writeFileSync(envPath, "UID=1000\nKNTIC_VERSION=0.0.1\nGID=1000\n");
|
|
238
|
+
|
|
239
|
+
updateEnvVersion("2.0.0", envPath);
|
|
240
|
+
|
|
241
|
+
const content = fs.readFileSync(envPath, "utf8");
|
|
242
|
+
assert.ok(content.includes("KNTIC_VERSION=2.0.0"), "version must be updated");
|
|
243
|
+
assert.ok(!content.includes("KNTIC_VERSION=0.0.1"), "old version must be gone");
|
|
244
|
+
assert.ok(content.includes("UID=1000"), "other vars must be preserved");
|
|
245
|
+
assert.ok(content.includes("GID=1000"), "other vars must be preserved");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("handles file without trailing newline when appending", () => {
|
|
249
|
+
const envPath = path.join(tmpDir, ".kntic.env");
|
|
250
|
+
fs.writeFileSync(envPath, "UID=1000");
|
|
251
|
+
|
|
252
|
+
updateEnvVersion("0.1.0", envPath);
|
|
253
|
+
|
|
254
|
+
const content = fs.readFileSync(envPath, "utf8");
|
|
255
|
+
assert.ok(content.includes("UID=1000"), "existing content must be preserved");
|
|
256
|
+
assert.ok(content.includes("KNTIC_VERSION=0.1.0"), "version must be appended");
|
|
257
|
+
// Ensure they're on separate lines
|
|
258
|
+
assert.ok(content.includes("UID=1000\nKNTIC_VERSION=0.1.0"), "must be on separate lines");
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe("extractVersion (update module)", () => {
|
|
263
|
+
it("parses version from artifact filename", () => {
|
|
264
|
+
assert.equal(extractVersion("kntic-bootstrap-v0.0.10.tar.gz"), "0.0.10");
|
|
265
|
+
assert.equal(extractVersion("kntic-bootstrap-v1.2.3.tar.gz"), "1.2.3");
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("throws on invalid filename", () => {
|
|
269
|
+
assert.throws(() => extractVersion("no-version-here.tar.gz"));
|
|
270
|
+
});
|
|
271
|
+
});
|
package/src/commands/usage.js
CHANGED
|
@@ -7,6 +7,7 @@ function usage() {
|
|
|
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
9
|
console.log(" start Build and start KNTIC services via docker compose (uses kntic.yml + .kntic.env)");
|
|
10
|
+
console.log(" update Download the latest KNTIC bootstrap and update .kntic/lib only");
|
|
10
11
|
console.log("");
|
|
11
12
|
}
|
|
12
13
|
|