@kntic/kntic 0.4.7 → 0.4.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kntic/kntic",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
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",
@@ -189,6 +189,8 @@ function extractLibOnly(tarball, destDir) {
189
189
  * - .kntic/lib and .kntic/adrs are **replaced** (cleared first, then extracted).
190
190
  * - .kntic/hooks/gia/internal is **updated** (existing files are overwritten or new files
191
191
  * added, but files not present in the archive are preserved).
192
+ * - .kntic/hooks/gia/specific is **extracted only if the directory does not already exist**
193
+ * (one-time bootstrap; user customizations are never overwritten).
192
194
  * - .kntic/gia/weights.json is **replaced** if present in the archive.
193
195
  *
194
196
  * @param {string} tarball – path to the .tar.gz file
@@ -198,6 +200,7 @@ function extractUpdate(tarball, destDir) {
198
200
  const libDir = path.join(destDir, ".kntic", "lib");
199
201
  const adrsDir = path.join(destDir, ".kntic", "adrs");
200
202
  const giaInternalDir = path.join(destDir, ".kntic", "hooks", "gia", "internal");
203
+ const giaSpecificDir = path.join(destDir, ".kntic", "hooks", "gia", "specific");
201
204
  const giaDir = path.join(destDir, ".kntic", "gia");
202
205
 
203
206
  // Ensure target directories exist
@@ -211,6 +214,7 @@ function extractUpdate(tarball, destDir) {
211
214
  clearDirectory(adrsDir);
212
215
 
213
216
  // Note: gia/internal hooks are NOT cleared — update semantics (overwrite/add, preserve others)
217
+ // Note: gia/specific hooks are only extracted if the directory does NOT already exist
214
218
  // Note: gia is NOT cleared — only weights.json is replaced
215
219
 
216
220
  // Extract .kntic/lib/ and .kntic/adrs/ (guaranteed to be in the archive)
@@ -229,6 +233,20 @@ function extractUpdate(tarball, destDir) {
229
233
  // No .kntic/hooks/gia/internal/ in the archive — that's fine
230
234
  }
231
235
 
236
+ // Extract .kntic/hooks/gia/specific/ only if it does not already exist on disk.
237
+ // This is a one-time bootstrap — once the user has specific hooks, they are never
238
+ // overwritten by update.
239
+ if (!fs.existsSync(giaSpecificDir)) {
240
+ try {
241
+ execSync(
242
+ `tar xzf "${tarball}" -C "${destDir}" "./.kntic/hooks/gia/specific/"`,
243
+ { stdio: "pipe" }
244
+ );
245
+ } catch (_) {
246
+ // No .kntic/hooks/gia/specific/ in the archive — that's fine
247
+ }
248
+ }
249
+
232
250
  // Extract .kntic/gia/weights.json separately — archive may not contain it
233
251
  try {
234
252
  execSync(
@@ -256,7 +274,7 @@ async function update(options = {}) {
256
274
  console.log("Updating .kntic/lib …");
257
275
  extractLibOnly(tmpFile, ".");
258
276
  } else {
259
- console.log("Updating .kntic/lib, .kntic/adrs, .kntic/hooks/gia/internal, and .kntic/gia/weights.json …");
277
+ console.log("Updating .kntic/lib, .kntic/adrs, .kntic/hooks/gia/internal, .kntic/hooks/gia/specific (if new), and .kntic/gia/weights.json …");
260
278
  extractUpdate(tmpFile, ".");
261
279
  }
262
280
 
@@ -270,7 +288,7 @@ async function update(options = {}) {
270
288
  if (libOnly) {
271
289
  console.log("Done. .kntic/lib updated successfully.");
272
290
  } else {
273
- console.log("Done. .kntic/lib, .kntic/adrs, .kntic/hooks/gia/internal, and .kntic/gia/weights.json updated successfully.");
291
+ console.log("Done. .kntic/lib, .kntic/adrs, .kntic/hooks/gia/internal, .kntic/hooks/gia/specific (if new), and .kntic/gia/weights.json updated successfully.");
274
292
  }
275
293
  }
276
294
 
@@ -408,6 +408,75 @@ describe("extractUpdate", () => {
408
408
  );
409
409
  });
410
410
 
411
+ it("extracts .kntic/hooks/gia/specific when the directory does not exist", () => {
412
+ const tarball = createTarball(tmpDir, {
413
+ ".kntic/lib/orchestrator.py": "# orchestrator\n",
414
+ ".kntic/adrs/ADR-001.md": "# ADR 001\n",
415
+ ".kntic/hooks/gia/specific/my-hook.sh": "#!/bin/sh\necho specific\n",
416
+ ".kntic/hooks/gia/specific/another.sh": "#!/bin/sh\necho another\n",
417
+ });
418
+
419
+ // specific dir does not exist yet
420
+ const specificDir = path.join(destDir, ".kntic", "hooks", "gia", "specific");
421
+ assert.ok(!fs.existsSync(specificDir), "specific dir must not exist before extraction");
422
+
423
+ extractUpdate(tarball, destDir);
424
+
425
+ // specific hooks must be extracted
426
+ assert.equal(
427
+ fs.readFileSync(path.join(specificDir, "my-hook.sh"), "utf8"),
428
+ "#!/bin/sh\necho specific\n"
429
+ );
430
+ assert.equal(
431
+ fs.readFileSync(path.join(specificDir, "another.sh"), "utf8"),
432
+ "#!/bin/sh\necho another\n"
433
+ );
434
+ });
435
+
436
+ it("does NOT overwrite .kntic/hooks/gia/specific when the directory already exists", () => {
437
+ const specificDir = path.join(destDir, ".kntic", "hooks", "gia", "specific");
438
+ fs.mkdirSync(specificDir, { recursive: true });
439
+ fs.writeFileSync(path.join(specificDir, "custom.sh"), "#!/bin/sh\necho custom\n");
440
+
441
+ const tarball = createTarball(tmpDir, {
442
+ ".kntic/lib/orchestrator.py": "# orchestrator\n",
443
+ ".kntic/adrs/ADR-001.md": "# ADR 001\n",
444
+ ".kntic/hooks/gia/specific/my-hook.sh": "#!/bin/sh\necho from-archive\n",
445
+ });
446
+
447
+ extractUpdate(tarball, destDir);
448
+
449
+ // User's custom hook must be preserved
450
+ assert.equal(
451
+ fs.readFileSync(path.join(specificDir, "custom.sh"), "utf8"),
452
+ "#!/bin/sh\necho custom\n",
453
+ "user's custom specific hook must be preserved"
454
+ );
455
+
456
+ // Archive's specific hook must NOT be extracted
457
+ assert.ok(
458
+ !fs.existsSync(path.join(specificDir, "my-hook.sh")),
459
+ "archive specific hook must not be extracted when directory already exists"
460
+ );
461
+ });
462
+
463
+ it("works when archive has no .kntic/hooks/gia/specific and directory does not exist", () => {
464
+ const tarball = createTarball(tmpDir, {
465
+ ".kntic/lib/orchestrator.py": "# orchestrator\n",
466
+ ".kntic/adrs/ADR-001.md": "# ADR 001\n",
467
+ });
468
+
469
+ // Should not throw
470
+ extractUpdate(tarball, destDir);
471
+
472
+ // specific dir should not be created
473
+ const specificDir = path.join(destDir, ".kntic", "hooks", "gia", "specific");
474
+ assert.ok(
475
+ !fs.existsSync(specificDir),
476
+ "specific dir must not be created when not in archive"
477
+ );
478
+ });
479
+
411
480
  it("preserves files outside .kntic/lib, .kntic/adrs, and .kntic/hooks/gia/internal", () => {
412
481
  const knticDir = path.join(destDir, ".kntic");
413
482
  fs.mkdirSync(knticDir, { recursive: true });
@@ -9,8 +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, .kntic/adrs, .kntic/hooks/gia/internal, and .kntic/gia/weights.json");
13
- console.log(" --lib-only Update only .kntic/lib (skip .kntic/adrs, .kntic/hooks/gia/internal, and .kntic/gia/weights.json)");
12
+ console.log(" update Download the latest KNTIC bootstrap and update .kntic/lib, .kntic/adrs, .kntic/hooks/gia/internal, .kntic/hooks/gia/specific (if new), and .kntic/gia/weights.json");
13
+ console.log(" --lib-only Update only .kntic/lib (skip adrs, hooks, and weights)");
14
14
  console.log("");
15
15
  }
16
16