@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kntic/kntic",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
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",
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
- commands.update().catch((err) => {
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
  });
@@ -182,7 +182,53 @@ function extractLibOnly(tarball, destDir) {
182
182
  );
183
183
  }
184
184
 
185
- async function update() {
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
- console.log("Updating .kntic/lib …");
196
- extractLibOnly(tmpFile, ".");
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
- console.log("Done. .kntic/lib updated successfully.");
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
 
@@ -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 only");
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