@ngockhoale/ukit 2.3.22 → 2.3.23

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/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  All notable changes to UKit are documented here.
4
4
 
5
+ ## 2.3.23 - 2026-09-13
6
+
7
+ Self-install no longer clobbers UKit's own canonical docs. Running `ukit install` inside the UKit
8
+ source repository (e.g. the global CLI invoked from the repo) overwrote `docs/PROMPT_CACHING.md` —
9
+ the canonical 368-line ruleset — with the distilled 127-line copy shipped under `templates/docs/`,
10
+ because the manifest item is `overwrite_with_backup` and self-install went undetected.
11
+
12
+ - **Self-install detection.** `buildInstallPlan` now recognizes an install targeting the UKit repo
13
+ by two independent signals: the templates resolving inside the install target (the repo has a
14
+ `templates/` directory, the global CLI does not), or the target `package.json` naming
15
+ `@ngockhoale/ukit`. This covers the common case where the global CLI runs inside the repo, which
16
+ the path-containment check alone misses.
17
+ - **Docs downgraded to seed-only.** On a detected self-install, every `overwrite_with_backup`
18
+ entry whose target lives under `docs/` is downgraded to `skip`: a missing doc is seeded, but an
19
+ existing canonical doc is never overwritten. Downstream projects are unaffected — their docs
20
+ refresh on `ukit update` as before.
21
+ - **Verification.** New `buildInstallPlan` regressions cover the containment signal, the
22
+ package-name signal, and the unchanged downstream path; the full suite stays green.
23
+
5
24
  ## 2.3.22 - 2026-09-13
6
25
 
7
26
  Consultation-question routing fix — pure questions no longer fabricate mutation debt that the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "2.3.22",
3
+ "version": "2.3.23",
4
4
  "description": "Install/update an index-first AI workspace for Claude Code, OpenAI Codex, OpenCode, and omp (Oh My Pi).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { selectManifestItems } from '../manifest/selectItems.js';
4
4
  import { renderTemplateString } from '../render/renderTemplate.js';
5
+ import { UKIT_PACKAGE_NAME } from './update.js';
5
6
 
6
7
  const BINARY_TEMPLATE_EXTENSIONS = new Set([
7
8
  '.ttf',
@@ -44,6 +45,52 @@ function safeResolve(basePath, relativePath) {
44
45
  return resolvedPath;
45
46
  }
46
47
 
48
+ function isPathInside(parentPath, childPath) {
49
+ const relative = path.relative(parentPath, childPath);
50
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
51
+ }
52
+
53
+ function isDocsTarget(targetPath, projectRoot) {
54
+ const relative = path.relative(projectRoot, targetPath);
55
+ return relative === 'docs' || relative.startsWith(`docs${path.sep}`);
56
+ }
57
+
58
+ // Detect `ukit install` running against the UKit source repository itself. Two independent
59
+ // signals: the templates resolve inside the install target (the repo has a templates/ dir, the
60
+ // global CLI does not), or the target's package.json declares UKIT_PACKAGE_NAME (the path check
61
+ // misses the common case where the global CLI runs inside the repo).
62
+ async function isSelfInstall({ templatesRoot, projectRoot }) {
63
+ if (isPathInside(projectRoot, templatesRoot)) {
64
+ return true;
65
+ }
66
+ try {
67
+ const raw = await fs.readFile(path.join(projectRoot, 'package.json'), 'utf8');
68
+ return JSON.parse(raw)?.name === UKIT_PACKAGE_NAME;
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
73
+
74
+ // In the UKit source repo, docs/ holds canonical sources that deliberately diverge from the
75
+ // shipped copies under templates/ (docs/PROMPT_CACHING.md is the full ruleset while
76
+ // templates/docs/ ships a distilled version). The refresh-on-update strategy would silently
77
+ // clobber those canonical files, so downgrade docs overwrites to skip: seed a missing doc, but
78
+ // never overwrite one that already exists.
79
+ async function downgradeSelfInstallDocOverwrites(planEntries, { templatesRoot, projectRoot }) {
80
+ if (!(await isSelfInstall({ templatesRoot, projectRoot }))) {
81
+ return;
82
+ }
83
+
84
+ for (const entry of planEntries) {
85
+ if (entry.type === 'link' || entry.mergeStrategy !== 'overwrite_with_backup') {
86
+ continue;
87
+ }
88
+ if (isDocsTarget(entry.targetPath, projectRoot)) {
89
+ entry.mergeStrategy = 'skip';
90
+ }
91
+ }
92
+ }
93
+
47
94
  async function readTemplateFile(filePath, { binary = false } = {}) {
48
95
  if (binary) {
49
96
  return fs.readFile(filePath);
@@ -263,6 +310,8 @@ export async function buildInstallPlan({
263
310
  }
264
311
  }
265
312
 
313
+ await downgradeSelfInstallDocOverwrites(planEntries, { templatesRoot, projectRoot });
314
+
266
315
  return {
267
316
  selectedItems,
268
317
  entries: planEntries,