universal-dev-standards 6.12.0 → 6.13.0-beta.2

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.
@@ -1,8 +1,168 @@
1
1
  import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
2
- import { join } from 'path';
2
+ import { join, basename } from 'path';
3
+ import {
4
+ collectHookConfigs, standardsSourceDir, hooksSourceDir,
5
+ CODEX_HOOK_SCRIPT, GEMINI_HOOK_SCRIPT,
6
+ } from '../installers/hooks-installer.js';
3
7
 
4
8
  /**
5
- * Remove UDS-related lines from .husky/pre-commit
9
+ * Every UDS-installed hook entry — in .claude/settings.json, .codex/hooks.json
10
+ * and .gemini/settings.json alike — runs a command of the shape
11
+ * `node scripts/hooks/<script>` (see installHooks/installCodexHooks/
12
+ * installGeminiHooks in ../installers/hooks-installer.js). That path alone is
13
+ * NOT a safe signature, though: `scripts/hooks/` is a directory UDS itself
14
+ * scaffolds inside the adopter's project, so an adopter's own hook script can
15
+ * live at the exact same path and be indistinguishable by path alone
16
+ * (verified: an earlier version of this file matched on path only, and a
17
+ * test placing a user hook at `scripts/hooks/my-own-hook.mjs` lost it).
18
+ * The safe signature is path *and* a script basename UDS is actually known to
19
+ * ship right now — the same set collectHookConfigs() derives for install,
20
+ * plus the two fixed Codex/Gemini script names.
21
+ *
22
+ * 🔴 Until this file added the three functions below, uninstall only ever
23
+ * touched .husky/pre-commit and .git/hooks/pre-commit — the settings.json /
24
+ * hooks.json files installHooks() etc. actually write were never cleaned up,
25
+ * so `uds uninstall` silently left every enforcement hook running (Claude
26
+ * Code included, not just Codex/Gemini).
27
+ */
28
+ const UDS_HOOK_COMMAND_PATTERN = /^node scripts\/hooks\//;
29
+
30
+ /** The command string a hook entry runs, mirroring hooks-installer.js's own commandOf(). */
31
+ function commandOf(entry) {
32
+ const h = entry && entry.hooks && entry.hooks[0];
33
+ if (typeof h === 'string') return h;
34
+ return h && typeof h.command === 'string' ? h.command : undefined;
35
+ }
36
+
37
+ /** Script basenames UDS currently ships and would install a hook entry for. */
38
+ function knownUdsHookScripts() {
39
+ const { scripts } = collectHookConfigs(standardsSourceDir(), hooksSourceDir());
40
+ return new Set([...scripts.map((s) => basename(s)), CODEX_HOOK_SCRIPT, GEMINI_HOOK_SCRIPT]);
41
+ }
42
+
43
+ function isUdsHookEntry(entry, knownScripts) {
44
+ const cmd = commandOf(entry);
45
+ if (typeof cmd !== 'string' || !UDS_HOOK_COMMAND_PATTERN.test(cmd)) return false;
46
+ return knownScripts.has(basename(cmd));
47
+ }
48
+
49
+ /**
50
+ * Strip UDS-installed entries out of a `{ event: entry[] }` map, dropping any
51
+ * event key left with zero entries. An adopter's own hooks on the same event
52
+ * (or even the same array, or the same `scripts/hooks/` directory) are never
53
+ * touched — only entries whose command names a script UDS is known to ship
54
+ * are removed.
55
+ */
56
+ function stripUdsEntries(hooksMap) {
57
+ const knownScripts = knownUdsHookScripts();
58
+ let removedCount = 0;
59
+ const nextMap = {};
60
+ for (const [event, entries] of Object.entries(hooksMap || {})) {
61
+ const kept = (entries || []).filter((entry) => {
62
+ if (isUdsHookEntry(entry, knownScripts)) {
63
+ removedCount += 1;
64
+ return false;
65
+ }
66
+ return true;
67
+ });
68
+ if (kept.length > 0) nextMap[event] = kept;
69
+ }
70
+ return { nextMap, removedCount };
71
+ }
72
+
73
+ /** Put a stripped hooks map back onto a config object, dropping the `hooks` key entirely if empty. */
74
+ function applyHooksMap(config, nextMap) {
75
+ const updated = { ...config };
76
+ if (Object.keys(nextMap).length > 0) {
77
+ updated.hooks = nextMap;
78
+ } else {
79
+ delete updated.hooks;
80
+ }
81
+ return updated;
82
+ }
83
+
84
+ /**
85
+ * Shared machinery for the three "remove UDS entries from a hooks config
86
+ * file" functions below. Handles the missing-file, invalid-JSON, and
87
+ * nothing-to-remove cases identically for all three, so their only
88
+ * difference is which file and which key they look at.
89
+ */
90
+ function uninstallHookConfigFile({ configPath, label, dryRun, deleteEmptyFile }) {
91
+ const result = { removed: [], skipped: [], errors: [] };
92
+ if (!existsSync(configPath)) return result; // nothing installed here — nothing to report
93
+
94
+ let config;
95
+ try {
96
+ config = JSON.parse(readFileSync(configPath, 'utf-8'));
97
+ } catch (error) {
98
+ result.errors.push(`${label} — could not parse (${error.message}); left untouched`);
99
+ return result;
100
+ }
101
+
102
+ const { nextMap, removedCount } = stripUdsEntries(config.hooks);
103
+ if (removedCount === 0) {
104
+ result.skipped.push(`${label} (no UDS hook entries found)`);
105
+ return result;
106
+ }
107
+
108
+ const entryLabel = `${label} (${removedCount} UDS hook ${removedCount === 1 ? 'entry' : 'entries'})`;
109
+ if (dryRun) {
110
+ result.removed.push(entryLabel);
111
+ return result;
112
+ }
113
+
114
+ const updatedConfig = applyHooksMap(config, nextMap);
115
+ const isNowEmpty = Object.keys(updatedConfig).length === 0;
116
+
117
+ try {
118
+ if (deleteEmptyFile && isNowEmpty) {
119
+ unlinkSync(configPath);
120
+ result.removed.push(`${entryLabel}, file removed — created by UDS, now empty`);
121
+ } else {
122
+ writeFileSync(configPath, JSON.stringify(updatedConfig, null, 2) + '\n');
123
+ result.removed.push(entryLabel);
124
+ }
125
+ } catch (error) {
126
+ result.errors.push(`${label} — ${error.message}`);
127
+ }
128
+
129
+ return result;
130
+ }
131
+
132
+ /** Remove UDS's Stop/PreToolUse/etc. entries from .claude/settings.json (installHooks()'s output). */
133
+ export function uninstallClaudeCodeHooks(projectPath, options = {}) {
134
+ return uninstallHookConfigFile({
135
+ configPath: join(projectPath, '.claude', 'settings.json'),
136
+ label: '.claude/settings.json',
137
+ dryRun: options.dryRun || false,
138
+ deleteEmptyFile: true,
139
+ });
140
+ }
141
+
142
+ /** Remove UDS's Stop entry from .codex/hooks.json (installCodexHooks()'s output). */
143
+ export function uninstallCodexHooks(projectPath, options = {}) {
144
+ return uninstallHookConfigFile({
145
+ configPath: join(projectPath, '.codex', 'hooks.json'),
146
+ label: '.codex/hooks.json',
147
+ dryRun: options.dryRun || false,
148
+ deleteEmptyFile: true,
149
+ });
150
+ }
151
+
152
+ /** Remove UDS's AfterAgent entry from .gemini/settings.json (installGeminiHooks()'s output). */
153
+ export function uninstallGeminiHooks(projectPath, options = {}) {
154
+ return uninstallHookConfigFile({
155
+ configPath: join(projectPath, '.gemini', 'settings.json'),
156
+ label: '.gemini/settings.json',
157
+ dryRun: options.dryRun || false,
158
+ deleteEmptyFile: true,
159
+ });
160
+ }
161
+
162
+ /**
163
+ * Remove UDS-related lines from .husky/pre-commit, the native
164
+ * .git/hooks/pre-commit fallback, and the enforcement-hook entries UDS wrote
165
+ * into .claude/settings.json, .codex/hooks.json and .gemini/settings.json.
6
166
  * @param {string} projectPath - Project root path
7
167
  * @param {Object} options - { dryRun: boolean }
8
168
  * @returns {Object} { removed: string[], skipped: string[], errors: string[] }
@@ -14,29 +174,24 @@ export function uninstallHook(projectPath, options = {}) {
14
174
 
15
175
  if (!existsSync(hookPath)) {
16
176
  result.skipped.push('.husky/pre-commit (not found)');
17
- return result;
18
- }
19
-
20
- try {
21
- const content = readFileSync(hookPath, 'utf-8');
22
- const lines = content.split('\n');
23
- const udsPattern = /uds\s+check|checkin-standards/;
24
- const filteredLines = lines.filter(line => !udsPattern.test(line));
25
-
26
- if (filteredLines.length === lines.length) {
27
- result.skipped.push('.husky/pre-commit (no UDS lines found)');
28
- return result;
29
- }
177
+ } else {
178
+ try {
179
+ const content = readFileSync(hookPath, 'utf-8');
180
+ const lines = content.split('\n');
181
+ const udsPattern = /uds\s+check|checkin-standards/;
182
+ const filteredLines = lines.filter(line => !udsPattern.test(line));
30
183
 
31
- if (dryRun) {
32
- result.removed.push('.husky/pre-commit (UDS check lines)');
33
- return result;
184
+ if (filteredLines.length === lines.length) {
185
+ result.skipped.push('.husky/pre-commit (no UDS lines found)');
186
+ } else if (dryRun) {
187
+ result.removed.push('.husky/pre-commit (UDS check lines)');
188
+ } else {
189
+ writeFileSync(hookPath, filteredLines.join('\n'), 'utf-8');
190
+ result.removed.push('.husky/pre-commit (UDS check lines)');
191
+ }
192
+ } catch (error) {
193
+ result.errors.push(`.husky/pre-commit — ${error.message}`);
34
194
  }
35
-
36
- writeFileSync(hookPath, filteredLines.join('\n'), 'utf-8');
37
- result.removed.push('.husky/pre-commit (UDS check lines)');
38
- } catch (error) {
39
- result.errors.push(`.husky/pre-commit — ${error.message}`);
40
195
  }
41
196
 
42
197
  // Also handle native .git/hooks/pre-commit (installed by uds init for non-Node projects)
@@ -70,5 +225,17 @@ export function uninstallHook(projectPath, options = {}) {
70
225
  }
71
226
  }
72
227
 
228
+ // Enforcement hooks written by installHooks()/installCodexHooks()/installGeminiHooks()
229
+ // — the gap this function used to have entirely (see module doc comment above).
230
+ for (const sub of [
231
+ uninstallClaudeCodeHooks(projectPath, { dryRun }),
232
+ uninstallCodexHooks(projectPath, { dryRun }),
233
+ uninstallGeminiHooks(projectPath, { dryRun }),
234
+ ]) {
235
+ result.removed.push(...sub.removed);
236
+ result.skipped.push(...sub.skipped);
237
+ result.errors.push(...sub.errors);
238
+ }
239
+
73
240
  return result;
74
241
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "version": "6.12.0",
3
+ "version": "6.13.0-beta.2",
4
4
  "lastUpdated": "2026-05-13",
5
5
  "description": "Standards registry for universal-dev-standards with integrated skills and AI-optimized formats",
6
6
  "formats": {
@@ -58,14 +58,14 @@
58
58
  "standards": {
59
59
  "name": "universal-dev-standards",
60
60
  "url": "https://github.com/AsiaOstrich/universal-dev-standards",
61
- "version": "6.12.0"
61
+ "version": "6.13.0-beta.2"
62
62
  },
63
63
  "skills": {
64
64
  "name": "universal-dev-standards",
65
65
  "url": "https://github.com/AsiaOstrich/universal-dev-standards",
66
66
  "localPath": "skills",
67
67
  "rawUrl": "https://raw.githubusercontent.com/AsiaOstrich/universal-dev-standards/main/skills",
68
- "version": "6.12.0",
68
+ "version": "6.13.0-beta.2",
69
69
  "note": "Skills are now included in the main repository under skills/"
70
70
  }
71
71
  },
@@ -2295,7 +2295,7 @@
2295
2295
  "id": "license-compliance",
2296
2296
  "name": "License Compliance Standards",
2297
2297
  "nameZh": "授權合規標準",
2298
- "version": "6.12.0",
2298
+ "version": "6.13.0-beta.2",
2299
2299
  "source": {
2300
2300
  "human": "core/license-compliance.md",
2301
2301
  "ai": "ai/standards/license-compliance.ai.yaml"
@@ -2307,7 +2307,7 @@
2307
2307
  "id": "verification-oracle",
2308
2308
  "name": "Verification Oracle Standards",
2309
2309
  "nameZh": "驗證 Oracle 標準",
2310
- "version": "6.12.0",
2310
+ "version": "6.13.0-beta.2",
2311
2311
  "source": {
2312
2312
  "human": "core/verification-oracle.md",
2313
2313
  "ai": "ai/standards/verification-oracle.ai.yaml"
@@ -2319,7 +2319,7 @@
2319
2319
  "id": "model-provenance",
2320
2320
  "name": "Model Provenance Policy Standards",
2321
2321
  "nameZh": "模型來源政策標準",
2322
- "version": "6.12.0",
2322
+ "version": "6.13.0-beta.2",
2323
2323
  "source": {
2324
2324
  "human": "core/model-provenance.md",
2325
2325
  "ai": "ai/standards/model-provenance.ai.yaml"
@@ -2331,7 +2331,7 @@
2331
2331
  "id": "resource-cost-boundary",
2332
2332
  "name": "Resource / Cost Boundary Declaration Standards",
2333
2333
  "nameZh": "資源/成本邊界宣告標準",
2334
- "version": "6.12.0",
2334
+ "version": "6.13.0-beta.2",
2335
2335
  "source": {
2336
2336
  "human": "core/resource-cost-boundary.md",
2337
2337
  "ai": "ai/standards/resource-cost-boundary.ai.yaml"