@kntic/kntic 0.4.2 → 0.4.4
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/update.js +60 -4
- package/src/commands/update.test.js +186 -1
- package/src/commands/usage.js +2 -1
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -30,7 +30,8 @@ if (!subcommand || subcommand === "usage") {
|
|
|
30
30
|
process.exit(1);
|
|
31
31
|
}
|
|
32
32
|
} else if (subcommand === "update") {
|
|
33
|
-
|
|
33
|
+
const updateOpts = { libOnly: args.includes("--lib-only") };
|
|
34
|
+
commands.update(updateOpts).catch((err) => {
|
|
34
35
|
console.error(`Error: ${err.message}`);
|
|
35
36
|
process.exit(1);
|
|
36
37
|
});
|
package/src/commands/update.js
CHANGED
|
@@ -182,7 +182,53 @@ function extractLibOnly(tarball, destDir) {
|
|
|
182
182
|
);
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
|
|
185
|
+
/**
|
|
186
|
+
* Extract .kntic/lib/**, .kntic/adrs/**, and .kntic/hooks/gdr/** from a tarball
|
|
187
|
+
* into the destination directory.
|
|
188
|
+
*
|
|
189
|
+
* - .kntic/lib and .kntic/adrs are **replaced** (cleared first, then extracted).
|
|
190
|
+
* - .kntic/hooks/gdr is **updated** (existing files are overwritten or new files
|
|
191
|
+
* added, but files not present in the archive are preserved).
|
|
192
|
+
*
|
|
193
|
+
* @param {string} tarball – path to the .tar.gz file
|
|
194
|
+
* @param {string} destDir – target directory (usually ".")
|
|
195
|
+
*/
|
|
196
|
+
function extractUpdate(tarball, destDir) {
|
|
197
|
+
const libDir = path.join(destDir, ".kntic", "lib");
|
|
198
|
+
const adrsDir = path.join(destDir, ".kntic", "adrs");
|
|
199
|
+
const gdrDir = path.join(destDir, ".kntic", "hooks", "gdr");
|
|
200
|
+
|
|
201
|
+
// Ensure target directories exist
|
|
202
|
+
fs.mkdirSync(libDir, { recursive: true });
|
|
203
|
+
fs.mkdirSync(adrsDir, { recursive: true });
|
|
204
|
+
fs.mkdirSync(gdrDir, { recursive: true });
|
|
205
|
+
|
|
206
|
+
// Clear lib and adrs (full replacement)
|
|
207
|
+
clearDirectory(libDir);
|
|
208
|
+
clearDirectory(adrsDir);
|
|
209
|
+
|
|
210
|
+
// Note: gdr is NOT cleared — update semantics (overwrite/add, preserve others)
|
|
211
|
+
|
|
212
|
+
// Extract .kntic/lib/ and .kntic/adrs/ (guaranteed to be in the archive)
|
|
213
|
+
execSync(
|
|
214
|
+
`tar xzf "${tarball}" -C "${destDir}" "./.kntic/lib/" "./.kntic/adrs/"`,
|
|
215
|
+
{ stdio: "pipe" }
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
// Extract .kntic/hooks/gdr/ separately — archive may not contain it
|
|
219
|
+
try {
|
|
220
|
+
execSync(
|
|
221
|
+
`tar xzf "${tarball}" -C "${destDir}" "./.kntic/hooks/gdr/"`,
|
|
222
|
+
{ stdio: "pipe" }
|
|
223
|
+
);
|
|
224
|
+
} catch (_) {
|
|
225
|
+
// No .kntic/hooks/gdr/ in the archive — that's fine
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function update(options = {}) {
|
|
230
|
+
const libOnly = options.libOnly || false;
|
|
231
|
+
|
|
186
232
|
// Resolve current version from the artifact metadata file
|
|
187
233
|
const artifactFilename = await fetchText(BOOTSTRAP_ARTIFACT_URL);
|
|
188
234
|
const version = extractVersion(artifactFilename);
|
|
@@ -192,8 +238,13 @@ async function update() {
|
|
|
192
238
|
console.log(`Downloading KNTIC bootstrap archive… (v${version})`);
|
|
193
239
|
await download(BOOTSTRAP_URL, tmpFile);
|
|
194
240
|
|
|
195
|
-
|
|
196
|
-
|
|
241
|
+
if (libOnly) {
|
|
242
|
+
console.log("Updating .kntic/lib …");
|
|
243
|
+
extractLibOnly(tmpFile, ".");
|
|
244
|
+
} else {
|
|
245
|
+
console.log("Updating .kntic/lib, .kntic/adrs, and .kntic/hooks/gdr …");
|
|
246
|
+
extractUpdate(tmpFile, ".");
|
|
247
|
+
}
|
|
197
248
|
|
|
198
249
|
// Update KNTIC_VERSION in .kntic.env
|
|
199
250
|
updateEnvVersion(version);
|
|
@@ -202,11 +253,16 @@ async function update() {
|
|
|
202
253
|
fs.unlinkSync(tmpFile);
|
|
203
254
|
|
|
204
255
|
console.log(`@kntic/kntic@${version}`);
|
|
205
|
-
|
|
256
|
+
if (libOnly) {
|
|
257
|
+
console.log("Done. .kntic/lib updated successfully.");
|
|
258
|
+
} else {
|
|
259
|
+
console.log("Done. .kntic/lib, .kntic/adrs, and .kntic/hooks/gdr updated successfully.");
|
|
260
|
+
}
|
|
206
261
|
}
|
|
207
262
|
|
|
208
263
|
module.exports = update;
|
|
209
264
|
module.exports.extractLibOnly = extractLibOnly;
|
|
265
|
+
module.exports.extractUpdate = extractUpdate;
|
|
210
266
|
module.exports.extractVersion = extractVersion;
|
|
211
267
|
module.exports.clearDirectory = clearDirectory;
|
|
212
268
|
module.exports.updateEnvVersion = updateEnvVersion;
|
|
@@ -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 { extractLibOnly, extractVersion, clearDirectory, updateEnvVersion } = require("./update");
|
|
10
|
+
const { extractLibOnly, extractUpdate, extractVersion, clearDirectory, updateEnvVersion } = require("./update");
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Helper — create a tar.gz archive in `tmpDir` containing the given files.
|
|
@@ -202,6 +202,191 @@ describe("extractLibOnly", () => {
|
|
|
202
202
|
});
|
|
203
203
|
});
|
|
204
204
|
|
|
205
|
+
describe("extractUpdate", () => {
|
|
206
|
+
let tmpDir;
|
|
207
|
+
let destDir;
|
|
208
|
+
|
|
209
|
+
beforeEach(() => {
|
|
210
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "kntic-test-extractupdate-"));
|
|
211
|
+
destDir = path.join(tmpDir, "dest");
|
|
212
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
afterEach(() => {
|
|
216
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("extracts .kntic/lib, .kntic/adrs, and .kntic/hooks/gdr from the archive", () => {
|
|
220
|
+
const tarball = createTarball(tmpDir, {
|
|
221
|
+
".kntic/lib/orchestrator.py": "# orchestrator\n",
|
|
222
|
+
".kntic/lib/skills/validator.py": "# validator\n",
|
|
223
|
+
".kntic/adrs/ADR-001.md": "# ADR 001\n",
|
|
224
|
+
".kntic/adrs/ADR-002.md": "# ADR 002\n",
|
|
225
|
+
".kntic/hooks/gdr/check.sh": "#!/bin/sh\necho check\n",
|
|
226
|
+
".kntic/MEMORY.MD": "# Memory\n",
|
|
227
|
+
"README.md": "# Hello\n",
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
extractUpdate(tarball, destDir);
|
|
231
|
+
|
|
232
|
+
// lib files must be extracted
|
|
233
|
+
assert.equal(
|
|
234
|
+
fs.readFileSync(path.join(destDir, ".kntic", "lib", "orchestrator.py"), "utf8"),
|
|
235
|
+
"# orchestrator\n"
|
|
236
|
+
);
|
|
237
|
+
assert.equal(
|
|
238
|
+
fs.readFileSync(path.join(destDir, ".kntic", "lib", "skills", "validator.py"), "utf8"),
|
|
239
|
+
"# validator\n"
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// adrs files must be extracted
|
|
243
|
+
assert.equal(
|
|
244
|
+
fs.readFileSync(path.join(destDir, ".kntic", "adrs", "ADR-001.md"), "utf8"),
|
|
245
|
+
"# ADR 001\n"
|
|
246
|
+
);
|
|
247
|
+
assert.equal(
|
|
248
|
+
fs.readFileSync(path.join(destDir, ".kntic", "adrs", "ADR-002.md"), "utf8"),
|
|
249
|
+
"# ADR 002\n"
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
// hooks/gdr files must be extracted
|
|
253
|
+
assert.equal(
|
|
254
|
+
fs.readFileSync(path.join(destDir, ".kntic", "hooks", "gdr", "check.sh"), "utf8"),
|
|
255
|
+
"#!/bin/sh\necho check\n"
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
// Files outside lib/adrs/hooks/gdr must NOT be extracted
|
|
259
|
+
assert.ok(
|
|
260
|
+
!fs.existsSync(path.join(destDir, ".kntic", "MEMORY.MD")),
|
|
261
|
+
"MEMORY.MD must not be extracted"
|
|
262
|
+
);
|
|
263
|
+
assert.ok(
|
|
264
|
+
!fs.existsSync(path.join(destDir, "README.md")),
|
|
265
|
+
"README.md must not be extracted"
|
|
266
|
+
);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("replaces existing .kntic/lib and .kntic/adrs content", () => {
|
|
270
|
+
const libDir = path.join(destDir, ".kntic", "lib");
|
|
271
|
+
const adrsDir = path.join(destDir, ".kntic", "adrs");
|
|
272
|
+
fs.mkdirSync(libDir, { recursive: true });
|
|
273
|
+
fs.mkdirSync(adrsDir, { recursive: true });
|
|
274
|
+
fs.writeFileSync(path.join(libDir, "old_lib.py"), "# old lib\n");
|
|
275
|
+
fs.writeFileSync(path.join(adrsDir, "ADR-OLD.md"), "# old adr\n");
|
|
276
|
+
|
|
277
|
+
const tarball = createTarball(tmpDir, {
|
|
278
|
+
".kntic/lib/new_lib.py": "# new lib\n",
|
|
279
|
+
".kntic/adrs/ADR-NEW.md": "# new adr\n",
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
extractUpdate(tarball, destDir);
|
|
283
|
+
|
|
284
|
+
// Old files must be gone
|
|
285
|
+
assert.ok(!fs.existsSync(path.join(libDir, "old_lib.py")), "old lib file must be removed");
|
|
286
|
+
assert.ok(!fs.existsSync(path.join(adrsDir, "ADR-OLD.md")), "old adr file must be removed");
|
|
287
|
+
|
|
288
|
+
// New files must be present
|
|
289
|
+
assert.equal(
|
|
290
|
+
fs.readFileSync(path.join(libDir, "new_lib.py"), "utf8"),
|
|
291
|
+
"# new lib\n"
|
|
292
|
+
);
|
|
293
|
+
assert.equal(
|
|
294
|
+
fs.readFileSync(path.join(adrsDir, "ADR-NEW.md"), "utf8"),
|
|
295
|
+
"# new adr\n"
|
|
296
|
+
);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("updates .kntic/hooks/gdr without clearing existing files (update semantics)", () => {
|
|
300
|
+
const gdrDir = path.join(destDir, ".kntic", "hooks", "gdr");
|
|
301
|
+
fs.mkdirSync(gdrDir, { recursive: true });
|
|
302
|
+
fs.writeFileSync(path.join(gdrDir, "existing-hook.sh"), "#!/bin/sh\necho existing\n");
|
|
303
|
+
fs.writeFileSync(path.join(gdrDir, "shared-hook.sh"), "#!/bin/sh\necho old\n");
|
|
304
|
+
|
|
305
|
+
const tarball = createTarball(tmpDir, {
|
|
306
|
+
".kntic/lib/orchestrator.py": "# orchestrator\n",
|
|
307
|
+
".kntic/adrs/ADR-001.md": "# ADR 001\n",
|
|
308
|
+
".kntic/hooks/gdr/shared-hook.sh": "#!/bin/sh\necho new\n",
|
|
309
|
+
".kntic/hooks/gdr/new-hook.sh": "#!/bin/sh\necho added\n",
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
extractUpdate(tarball, destDir);
|
|
313
|
+
|
|
314
|
+
// Existing file NOT in archive must be preserved (update semantics, not replace)
|
|
315
|
+
assert.equal(
|
|
316
|
+
fs.readFileSync(path.join(gdrDir, "existing-hook.sh"), "utf8"),
|
|
317
|
+
"#!/bin/sh\necho existing\n",
|
|
318
|
+
"existing hook not in archive must be preserved"
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
// File in both archive and disk must be overwritten
|
|
322
|
+
assert.equal(
|
|
323
|
+
fs.readFileSync(path.join(gdrDir, "shared-hook.sh"), "utf8"),
|
|
324
|
+
"#!/bin/sh\necho new\n",
|
|
325
|
+
"shared hook must be overwritten with archive version"
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
// New file from archive must be added
|
|
329
|
+
assert.equal(
|
|
330
|
+
fs.readFileSync(path.join(gdrDir, "new-hook.sh"), "utf8"),
|
|
331
|
+
"#!/bin/sh\necho added\n",
|
|
332
|
+
"new hook from archive must be added"
|
|
333
|
+
);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("works when archive has no .kntic/hooks/gdr directory", () => {
|
|
337
|
+
const gdrDir = path.join(destDir, ".kntic", "hooks", "gdr");
|
|
338
|
+
fs.mkdirSync(gdrDir, { recursive: true });
|
|
339
|
+
fs.writeFileSync(path.join(gdrDir, "my-hook.sh"), "#!/bin/sh\necho mine\n");
|
|
340
|
+
|
|
341
|
+
const tarball = createTarball(tmpDir, {
|
|
342
|
+
".kntic/lib/orchestrator.py": "# orchestrator\n",
|
|
343
|
+
".kntic/adrs/ADR-001.md": "# ADR 001\n",
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// Should not throw
|
|
347
|
+
extractUpdate(tarball, destDir);
|
|
348
|
+
|
|
349
|
+
// Existing gdr hook must be preserved
|
|
350
|
+
assert.equal(
|
|
351
|
+
fs.readFileSync(path.join(gdrDir, "my-hook.sh"), "utf8"),
|
|
352
|
+
"#!/bin/sh\necho mine\n",
|
|
353
|
+
"existing gdr hook must be preserved when archive has no gdr"
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
// lib and adrs must still be extracted
|
|
357
|
+
assert.ok(fs.existsSync(path.join(destDir, ".kntic", "lib", "orchestrator.py")));
|
|
358
|
+
assert.ok(fs.existsSync(path.join(destDir, ".kntic", "adrs", "ADR-001.md")));
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("preserves files outside .kntic/lib, .kntic/adrs, and .kntic/hooks/gdr", () => {
|
|
362
|
+
const knticDir = path.join(destDir, ".kntic");
|
|
363
|
+
fs.mkdirSync(knticDir, { recursive: true });
|
|
364
|
+
fs.writeFileSync(path.join(knticDir, "MEMORY.MD"), "# My memory\n");
|
|
365
|
+
fs.writeFileSync(path.join(destDir, "README.md"), "# My readme\n");
|
|
366
|
+
|
|
367
|
+
const tarball = createTarball(tmpDir, {
|
|
368
|
+
".kntic/lib/orchestrator.py": "# orchestrator\n",
|
|
369
|
+
".kntic/adrs/ADR-001.md": "# ADR 001\n",
|
|
370
|
+
".kntic/hooks/gdr/check.sh": "#!/bin/sh\n",
|
|
371
|
+
".kntic/MEMORY.MD": "# Archive memory\n",
|
|
372
|
+
"README.md": "# Archive readme\n",
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
extractUpdate(tarball, destDir);
|
|
376
|
+
|
|
377
|
+
assert.equal(
|
|
378
|
+
fs.readFileSync(path.join(knticDir, "MEMORY.MD"), "utf8"),
|
|
379
|
+
"# My memory\n",
|
|
380
|
+
"MEMORY.MD must not be overwritten"
|
|
381
|
+
);
|
|
382
|
+
assert.equal(
|
|
383
|
+
fs.readFileSync(path.join(destDir, "README.md"), "utf8"),
|
|
384
|
+
"# My readme\n",
|
|
385
|
+
"README.md must not be overwritten"
|
|
386
|
+
);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
205
390
|
describe("updateEnvVersion", () => {
|
|
206
391
|
let tmpDir;
|
|
207
392
|
|
package/src/commands/usage.js
CHANGED
|
@@ -9,7 +9,8 @@ function usage() {
|
|
|
9
9
|
console.log(" start Build and start KNTIC services via docker compose (uses kntic.yml + .kntic.env)");
|
|
10
10
|
console.log(" --screen Run inside a GNU screen session");
|
|
11
11
|
console.log(" stop Stop KNTIC services via docker compose");
|
|
12
|
-
console.log(" update Download the latest KNTIC bootstrap and update .kntic/lib
|
|
12
|
+
console.log(" update Download the latest KNTIC bootstrap and update .kntic/lib, .kntic/adrs, and .kntic/hooks/gdr");
|
|
13
|
+
console.log(" --lib-only Update only .kntic/lib (skip .kntic/adrs and .kntic/hooks/gdr)");
|
|
13
14
|
console.log("");
|
|
14
15
|
}
|
|
15
16
|
|