universal-dev-standards 6.14.0-beta.2 → 6.14.0-beta.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/bundled/ai/standards/ai-response-navigation.ai.yaml +43 -3
- package/bundled/ai/standards/checkin-standards.ai.yaml +25 -6
- package/bundled/ai/standards/open-work-tracking.ai.yaml +4 -1
- package/bundled/ai/standards/pipeline-security-gates.ai.yaml +5 -1
- package/bundled/core/ai-response-navigation.md +128 -12
- package/bundled/core/open-work-tracking.md +1 -1
- package/bundled/extensions/frameworks/fat-free-patterns.md +937 -0
- package/bundled/extensions/languages/csharp-style.md +464 -0
- package/bundled/extensions/languages/php/fat-free-patterns.md +915 -0
- package/bundled/extensions/languages/php/php-style.md +693 -0
- package/bundled/extensions/languages/php-style.md +700 -0
- package/bundled/extensions/locales/zh-cn.md +717 -0
- package/bundled/extensions/locales/zh-tw.md +717 -0
- package/bundled/locales/COVERAGE.md +5 -4
- package/bundled/locales/zh-CN/CHANGELOG.md +44 -3
- package/bundled/locales/zh-CN/README.md +2 -2
- package/bundled/locales/zh-CN/SECURITY.md +1 -1
- package/bundled/locales/zh-CN/core/ai-response-navigation.md +110 -12
- package/bundled/locales/zh-CN/skills/README.md +1 -0
- package/bundled/locales/zh-CN/skills/comprehension-ladder/SKILL.md +289 -0
- package/bundled/locales/zh-CN/skills/comprehension-ladder/eval-cases.md +261 -0
- package/bundled/locales/zh-TW/CHANGELOG.md +44 -3
- package/bundled/locales/zh-TW/README.md +2 -2
- package/bundled/locales/zh-TW/SECURITY.md +1 -1
- package/bundled/locales/zh-TW/core/ai-response-navigation.md +110 -12
- package/bundled/locales/zh-TW/core/open-work-tracking.md +3 -3
- package/bundled/locales/zh-TW/skills/README.md +1 -0
- package/bundled/locales/zh-TW/skills/comprehension-ladder/SKILL.md +289 -0
- package/bundled/locales/zh-TW/skills/comprehension-ladder/eval-cases.md +261 -0
- package/bundled/skills/README.md +1 -0
- package/bundled/skills/comprehension-ladder/SKILL.md +283 -0
- package/bundled/skills/comprehension-ladder/eval-cases.md +255 -0
- package/package.json +2 -2
- package/src/commands/check.js +9 -0
- package/src/commands/init.js +100 -27
- package/src/commands/uninstall.js +144 -30
- package/src/commands/update.js +62 -3
- package/src/core/install-records.js +191 -0
- package/src/i18n/messages.js +39 -6
- package/src/installers/hooks-installer.js +61 -30
- package/src/installers/integration-installer.js +5 -1
- package/src/installers/standards-installer.js +16 -23
- package/src/reconciler/plan-executor.js +10 -11
- package/src/uninstallers/hook-uninstaller.js +219 -33
- package/src/uninstallers/integration-uninstaller.js +35 -5
- package/src/utils/copier.js +57 -0
- package/src/utils/git-hooks.js +139 -7
- package/src/utils/hasher.js +36 -0
- package/src/utils/integration-generator.js +16 -6
- package/src/utils/legacy-hook-migration.js +112 -0
- package/src/utils/locale.js +19 -0
- package/src/utils/open-work-tracking.mjs +124 -23
- package/standards-registry.json +21 -7
package/src/commands/update.js
CHANGED
|
@@ -4,7 +4,7 @@ import { select, confirm as inquirerConfirm, checkbox, Separator } from '@inquir
|
|
|
4
4
|
import { execSync } from 'child_process';
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
|
|
6
6
|
import { join, basename, dirname, relative } from 'path';
|
|
7
|
-
import { readManifest, writeManifest, copyStandard, isInitialized, getRepoRoot } from '../utils/copier.js';
|
|
7
|
+
import { readManifest, writeManifest, copyStandard, copyExtension, isInitialized, getRepoRoot } from '../utils/copier.js';
|
|
8
8
|
import { getRepositoryInfo, getAllStandards, getShippableFilenames, getStandardSource } from '../utils/registry.js';
|
|
9
9
|
import { computeFileHash, planStandardsRemovals, refreshIntegrationBlockHashes, pruneIntegrationFileHashes } from '../utils/hasher.js';
|
|
10
10
|
import { AmbiguousMarkerError } from '../utils/marker-locator.js';
|
|
@@ -47,6 +47,8 @@ import {
|
|
|
47
47
|
import { getMarketplaceSkillsInfo } from '../utils/github.js';
|
|
48
48
|
import { detectAITools } from '../utils/detector.js';
|
|
49
49
|
import { HOOK_CAPABLE_TOOLS, resolveHookTools, installMissingHooks } from '../installers/hooks-installer.js';
|
|
50
|
+
import { persistRecorder, mergeRecorderInto } from '../core/install-records.js';
|
|
51
|
+
import { migrateLegacyHuskyHook } from '../utils/legacy-hook-migration.js';
|
|
50
52
|
import {
|
|
51
53
|
promptSkillsInstallLocation,
|
|
52
54
|
promptCommandsInstallation
|
|
@@ -498,6 +500,13 @@ export async function updateCommand(options) {
|
|
|
498
500
|
const scopedToSkills = !!options.skills;
|
|
499
501
|
const scopedToCommands = !!options.commands;
|
|
500
502
|
|
|
503
|
+
// A pre-commit line an older UDS wrote asks npm for the bare name `uds`, which
|
|
504
|
+
// is not this project. Fixed here, before any mode below can return early
|
|
505
|
+
// (an up-to-date adopter still has it). Narrowed runs stay narrowed.
|
|
506
|
+
if (!options.rollback && !scopedToSkills && !scopedToCommands && !options.standardsOnly) {
|
|
507
|
+
migrateLegacyPreCommitHook(projectPath, manifest, { plan: !!options.plan });
|
|
508
|
+
}
|
|
509
|
+
|
|
501
510
|
// Handle --rollback option (DSR). It restores a whole backup, so a scope
|
|
502
511
|
// flag cannot narrow it — say so rather than appearing to honour it.
|
|
503
512
|
if (options.rollback) {
|
|
@@ -729,7 +738,8 @@ export async function updateCommand(options) {
|
|
|
729
738
|
// Update extensions (skip non-string entries like custom-domain objects)
|
|
730
739
|
for (const ext of manifest.extensions) {
|
|
731
740
|
if (typeof ext !== 'string') continue;
|
|
732
|
-
|
|
741
|
+
// Package-only: an extension is never downloaded (XSPEC-452 R2).
|
|
742
|
+
const result = await copyExtension(ext, '.standards', projectPath);
|
|
733
743
|
if (result.success) {
|
|
734
744
|
results.updated.push(ext);
|
|
735
745
|
recordWritten(result);
|
|
@@ -1837,6 +1847,47 @@ async function switchClaudeTarget(projectPath, manifest, target, options) { // e
|
|
|
1837
1847
|
* @param {Object} manifest - Manifest object (will be mutated with updated hashes)
|
|
1838
1848
|
* @returns {{success: boolean, updated: string[], errors: string[]}}
|
|
1839
1849
|
*/
|
|
1850
|
+
/**
|
|
1851
|
+
* Swap the pre-commit line an older UDS wrote (`npx` + the bare name `uds`) for
|
|
1852
|
+
* the block current UDS writes, and say what was done or why not.
|
|
1853
|
+
*
|
|
1854
|
+
* Runs in every `uds update` mode that is not narrowed to something else, and in
|
|
1855
|
+
* `--with-hooks`, and it runs BEFORE the "already up to date" early return: an
|
|
1856
|
+
* adopter whose standards are current is exactly the one still carrying the old
|
|
1857
|
+
* line. Under `--plan` it reports and writes nothing.
|
|
1858
|
+
*
|
|
1859
|
+
* Only a line UDS can be shown to have written is changed (exact match under
|
|
1860
|
+
* UDS's marker comment — see legacy-hook-migration.js); any other bare-name line
|
|
1861
|
+
* is reported with its line number and left alone.
|
|
1862
|
+
*
|
|
1863
|
+
* @param {string} projectPath
|
|
1864
|
+
* @param {object} manifest - mutated only to carry a refreshed install record
|
|
1865
|
+
* @param {{plan?: boolean}} [opts]
|
|
1866
|
+
* @returns {ReturnType<typeof migrateLegacyHuskyHook>}
|
|
1867
|
+
*/
|
|
1868
|
+
export function migrateLegacyPreCommitHook(projectPath, manifest, { plan = false } = {}) {
|
|
1869
|
+
const msg = t().commands.update;
|
|
1870
|
+
const r = migrateLegacyHuskyHook(projectPath, { plan, manifest });
|
|
1871
|
+
if (r.state === 'error') {
|
|
1872
|
+
console.log(chalk.yellow(` ${msg.hookMigrateFailed.replace('{error}', r.error)}`));
|
|
1873
|
+
console.log();
|
|
1874
|
+
return r;
|
|
1875
|
+
}
|
|
1876
|
+
if (r.state === 'migrated') console.log(chalk.green(` ${msg.hookMigrated}`));
|
|
1877
|
+
if (r.state === 'would-migrate') console.log(chalk.cyan(` ${msg.hookWouldMigrate}`));
|
|
1878
|
+
for (const k of r.kept) {
|
|
1879
|
+
console.log(chalk.yellow(` ${msg.hookLegacyKept.replace('{line}', k.line).replace('{text}', k.text)}`));
|
|
1880
|
+
}
|
|
1881
|
+
if (r.state !== 'none') console.log();
|
|
1882
|
+
if (r.recorder) {
|
|
1883
|
+
persistRecorder(projectPath, r.recorder);
|
|
1884
|
+
// The manifest object is written again later in most update paths; without
|
|
1885
|
+
// this the record persisted just above would be overwritten by the older copy.
|
|
1886
|
+
Object.assign(manifest, mergeRecorderInto(manifest, r.recorder));
|
|
1887
|
+
}
|
|
1888
|
+
return r;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1840
1891
|
/**
|
|
1841
1892
|
* `uds update --with-hooks [--ai-tool <list>] [--plan] [--force]`
|
|
1842
1893
|
*
|
|
@@ -1858,6 +1909,9 @@ export async function updateHooksOnly(projectPath, manifest, options = {}) {
|
|
|
1858
1909
|
console.log();
|
|
1859
1910
|
}
|
|
1860
1911
|
|
|
1912
|
+
// The hook lines UDS wrote in the past belong to this command's subject too.
|
|
1913
|
+
migrateLegacyPreCommitHook(projectPath, manifest, { plan: !!options.plan });
|
|
1914
|
+
|
|
1861
1915
|
const capable = HOOK_CAPABLE_TOOLS.join(', ');
|
|
1862
1916
|
const resolved = resolveHookTools(projectPath, manifest, { aiTool: options.aiTool });
|
|
1863
1917
|
|
|
@@ -1887,11 +1941,16 @@ export async function updateHooksOnly(projectPath, manifest, options = {}) {
|
|
|
1887
1941
|
console.log(chalk.gray(` ${tool}: ${resolved.sources[tool].join(', ')}`));
|
|
1888
1942
|
}
|
|
1889
1943
|
|
|
1890
|
-
const { results, scripts } = installMissingHooks(projectPath, resolved.tools, {
|
|
1944
|
+
const { results, scripts, artifacts } = installMissingHooks(projectPath, resolved.tools, {
|
|
1891
1945
|
plan: !!options.plan,
|
|
1892
1946
|
overwriteScripts: !!options.force,
|
|
1893
1947
|
});
|
|
1894
1948
|
|
|
1949
|
+
// Record what was just written (hook scripts, and folders UDS had to create) in
|
|
1950
|
+
// the manifest, so `uds uninstall` can remove exactly that. Never on --plan: a
|
|
1951
|
+
// plan writes nothing, and the recorder is empty then anyway.
|
|
1952
|
+
if (!options.plan) persistRecorder(projectPath, artifacts);
|
|
1953
|
+
|
|
1895
1954
|
let failed = false;
|
|
1896
1955
|
for (const r of results) {
|
|
1897
1956
|
const rel = r.path ? relative(projectPath, r.path) || r.path : '';
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install records — the manifest's account of what UDS itself put on disk, kept
|
|
3
|
+
* so that `uds uninstall` removes exactly that and nothing else.
|
|
4
|
+
*
|
|
5
|
+
* Why a record and not a rule: `uds uninstall` used to leave four kinds of UDS
|
|
6
|
+
* output behind (hook scripts under scripts/hooks/, emptied `.codex/`-style
|
|
7
|
+
* folders, integration files whose generated header sits outside the UDS
|
|
8
|
+
* marker block, and the body of the native pre-commit hook). Each could be
|
|
9
|
+
* cleaned by pattern-matching on its content or path — and each of those
|
|
10
|
+
* patterns also matches files an adopter wrote themselves (`scripts/hooks/` is
|
|
11
|
+
* a directory UDS scaffolds INSIDE the adopter's project, where their own hook
|
|
12
|
+
* scripts may live). Origin is a fact about the moment of writing, so it is
|
|
13
|
+
* recorded then: a file is deleted only if a record says UDS wrote it AND its
|
|
14
|
+
* content still hashes to what UDS wrote. No record, or a changed hash, means
|
|
15
|
+
* the file is kept and the reason is printed.
|
|
16
|
+
*
|
|
17
|
+
* Deliberately NOT stored in `fileHashes` (`uds check` treats that as the list of
|
|
18
|
+
* standards files and would report these as modified/untracked) or in
|
|
19
|
+
* `provenance` (`uds update` prunes entries outside its expected set).
|
|
20
|
+
* `installedArtifacts` is read by nothing except uninstall.
|
|
21
|
+
*
|
|
22
|
+
* Shape:
|
|
23
|
+
* installedArtifacts: {
|
|
24
|
+
* files: { '<rel/path>': { kind, hash, size, installedAt } },
|
|
25
|
+
* createdDirs: ['<rel/dir>', ...] // directories UDS had to mkdir
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* `kind` selects what `hash` covers:
|
|
29
|
+
* 'hook-script', 'git-hook' → whole file (line endings normalized)
|
|
30
|
+
* 'integration-file' → everything outside the UDS marker block
|
|
31
|
+
*
|
|
32
|
+
* @module core/install-records
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { existsSync, mkdirSync, lstatSync, readFileSync, writeFileSync } from 'fs';
|
|
36
|
+
import { join, dirname, relative, isAbsolute } from 'path';
|
|
37
|
+
import { computeFileHash, computeOutsideBlockHash } from '../utils/hasher.js';
|
|
38
|
+
import { getManifestPath } from './manifest.js';
|
|
39
|
+
|
|
40
|
+
export const RECORDS_KEY = 'installedArtifacts';
|
|
41
|
+
|
|
42
|
+
export const RECORD_KINDS = Object.freeze({
|
|
43
|
+
HOOK_SCRIPT: 'hook-script',
|
|
44
|
+
GIT_HOOK: 'git-hook',
|
|
45
|
+
INTEGRATION_FILE: 'integration-file'
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const norm = (p) => String(p).replace(/\\/g, '/');
|
|
49
|
+
|
|
50
|
+
/** A fresh, empty recorder. Installers fill it; a caller persists it. */
|
|
51
|
+
export function newRecorder() {
|
|
52
|
+
return { files: {}, createdDirs: [] };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The hash a record of this kind holds for a file as it is on disk right now.
|
|
57
|
+
* Also what uninstall recomputes and compares — one function, so the two sides
|
|
58
|
+
* cannot drift apart.
|
|
59
|
+
* @returns {{hash: string, size: number}|null}
|
|
60
|
+
*/
|
|
61
|
+
export function currentHashFor(kind, absPath) {
|
|
62
|
+
if (kind === RECORD_KINDS.INTEGRATION_FILE) return computeOutsideBlockHash(absPath);
|
|
63
|
+
return computeFileHash(absPath);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Record a file UDS has just written. Call it AFTER the write, so the hash is of
|
|
68
|
+
* what is on disk. No-op (returns false) if the file cannot be hashed.
|
|
69
|
+
*/
|
|
70
|
+
export function recordFile(recorder, projectPath, relPath, kind) {
|
|
71
|
+
const rel = norm(relPath);
|
|
72
|
+
const h = currentHashFor(kind, join(projectPath, rel));
|
|
73
|
+
if (!h) return false;
|
|
74
|
+
recorder.files[rel] = { kind, hash: h.hash, size: h.size, installedAt: new Date().toISOString() };
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* `mkdir -p`, remembering every directory that did not exist before this call.
|
|
80
|
+
* "UDS created this directory" is only knowable at the moment of creation; after
|
|
81
|
+
* that an empty `.codex/` looks the same whether UDS or the adopter made it.
|
|
82
|
+
*/
|
|
83
|
+
export function mkdirTracked(recorder, projectPath, absDir) {
|
|
84
|
+
const missing = [];
|
|
85
|
+
let cur = absDir;
|
|
86
|
+
while (!existsSync(cur)) {
|
|
87
|
+
missing.push(cur);
|
|
88
|
+
const parent = dirname(cur);
|
|
89
|
+
if (parent === cur) break;
|
|
90
|
+
cur = parent;
|
|
91
|
+
}
|
|
92
|
+
if (missing.length === 0) return;
|
|
93
|
+
mkdirSync(absDir, { recursive: true });
|
|
94
|
+
for (const dir of missing.reverse()) {
|
|
95
|
+
const rel = norm(relative(projectPath, dir));
|
|
96
|
+
if (!rel || rel.startsWith('..') || isAbsolute(rel)) continue;
|
|
97
|
+
if (!recorder.createdDirs.includes(rel)) recorder.createdDirs.push(rel);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Fold a recorder into a manifest object (returned as a new object; input untouched). */
|
|
102
|
+
export function mergeRecorderInto(manifest, recorder) {
|
|
103
|
+
const prev = manifest?.[RECORDS_KEY] || {};
|
|
104
|
+
const dirs = new Set([...(prev.createdDirs || []), ...(recorder?.createdDirs || [])]);
|
|
105
|
+
return {
|
|
106
|
+
...manifest,
|
|
107
|
+
[RECORDS_KEY]: {
|
|
108
|
+
files: { ...(prev.files || {}), ...(recorder?.files || {}) },
|
|
109
|
+
createdDirs: [...dirs]
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Drop records for paths UDS has now removed. */
|
|
115
|
+
export function forgetRecords(manifest, relPaths) {
|
|
116
|
+
const prev = manifest?.[RECORDS_KEY];
|
|
117
|
+
if (!prev) return manifest;
|
|
118
|
+
const gone = new Set((relPaths || []).map(norm));
|
|
119
|
+
const files = Object.fromEntries(Object.entries(prev.files || {}).filter(([k]) => !gone.has(k)));
|
|
120
|
+
const createdDirs = (prev.createdDirs || []).filter((d) => !gone.has(d));
|
|
121
|
+
return { ...manifest, [RECORDS_KEY]: { files, createdDirs } };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The record for a path, or undefined. */
|
|
125
|
+
export function getFileRecord(manifest, relPath) {
|
|
126
|
+
return manifest?.[RECORDS_KEY]?.files?.[norm(relPath)];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** True when the recorder holds anything worth writing. */
|
|
130
|
+
export function hasRecords(recorder) {
|
|
131
|
+
return !!recorder && (Object.keys(recorder.files).length > 0 || recorder.createdDirs.length > 0);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Persist a recorder into the project's manifest on disk. For callers that run
|
|
136
|
+
* after the manifest was written (init's hook step, `uds update --with-hooks`).
|
|
137
|
+
* Returns false — and writes nothing — when there is nothing to record or no
|
|
138
|
+
* manifest to record it in.
|
|
139
|
+
*
|
|
140
|
+
* Reads and writes the manifest as RAW JSON, not through `readManifest`. That
|
|
141
|
+
* function migrates on read (it rewrites `standards` from paths to registry IDs,
|
|
142
|
+
* among other normalizations), so a read-modify-write through it would silently
|
|
143
|
+
* change every other field in the file as a side effect of recording one new
|
|
144
|
+
* one — measured: the e2e test "auto-restore missing files" reads `standards`
|
|
145
|
+
* straight from the file `uds init` wrote and started failing because init's
|
|
146
|
+
* manifest had come out in a different format.
|
|
147
|
+
*/
|
|
148
|
+
export function persistRecorder(projectPath, recorder) {
|
|
149
|
+
if (!hasRecords(recorder)) return false;
|
|
150
|
+
const manifestPath = getManifestPath(projectPath);
|
|
151
|
+
if (!existsSync(manifestPath)) return false;
|
|
152
|
+
let raw;
|
|
153
|
+
try {
|
|
154
|
+
raw = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
155
|
+
} catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
if (!raw || typeof raw !== 'object') return false;
|
|
159
|
+
writeFileSync(manifestPath, JSON.stringify(mergeRecorderInto(raw, recorder), null, 2));
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Is `absPath` a real directory (not a symlink to one)? Uninstall never follows links. */
|
|
164
|
+
export function isRealDirectory(absPath) {
|
|
165
|
+
try {
|
|
166
|
+
const st = lstatSync(absPath);
|
|
167
|
+
return st.isDirectory() && !st.isSymbolicLink();
|
|
168
|
+
} catch {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @returns {{ state: 'proven'|'no-record'|'changed', why: string }}
|
|
175
|
+
* proven a record exists and the file still hashes to it
|
|
176
|
+
* no-record nothing says UDS wrote it (older UDS, or written by hand)
|
|
177
|
+
* changed UDS wrote it, and it has been edited since
|
|
178
|
+
*/
|
|
179
|
+
export function proveUnchanged(manifest, projectPath, relPath) {
|
|
180
|
+
const rec = getFileRecord(manifest, relPath);
|
|
181
|
+
if (!rec) {
|
|
182
|
+
return {
|
|
183
|
+
state: 'no-record',
|
|
184
|
+
why: 'no install record — installed by an older UDS or not by UDS, so UDS cannot prove it wrote this'
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const now = currentHashFor(rec.kind, join(projectPath, relPath));
|
|
188
|
+
if (now && now.hash === rec.hash) return { state: 'proven', why: 'unchanged since UDS wrote it' };
|
|
189
|
+
return { state: 'changed', why: 'modified since UDS wrote it' };
|
|
190
|
+
}
|
|
191
|
+
|
package/src/i18n/messages.js
CHANGED
|
@@ -875,7 +875,7 @@ export const messages = {
|
|
|
875
875
|
// Pre-commit hook wiring (XSPEC: uds init writes a hook but never
|
|
876
876
|
// confirmed git would run it — this reports the gap, read-only)
|
|
877
877
|
hookNotWiredTitle: '⚠ [pre-commit] {file} was installed, but git will not run it.',
|
|
878
|
-
hookNotWiredOverride: ' git core.hooksPath is set to "{path}" — UDS will not override it. Confirm the hook there also runs `
|
|
878
|
+
hookNotWiredOverride: ' git core.hooksPath is set to "{path}" — UDS will not override it. Confirm the hook there also runs `universal-dev-standards check` (the installed CLI — not the short name "uds", which on the npm registry is an unrelated package), or switch it yourself: git config --local core.hooksPath .husky',
|
|
879
879
|
hookNotWiredUnwired: ' git core.hooksPath is not set, and {file} is not on git\'s default hook path.',
|
|
880
880
|
hookNotWiredFix: ' Fix (per-clone — not committed, teammates must repeat it): git config --local core.hooksPath .husky',
|
|
881
881
|
hookNotWiredFixNative: ' Fix: point core.hooksPath back to the default (git config --local --unset core.hooksPath), or add `uds check` under "{path}" instead.',
|
|
@@ -892,6 +892,8 @@ export const messages = {
|
|
|
892
892
|
// file or directory". Measured 2026-09-27 in CI (windows-latest).
|
|
893
893
|
hookMissingShebangTitle: '⚠ [pre-commit] {file} has no shebang line — git cannot run it on Windows.',
|
|
894
894
|
hookMissingShebangFix: ' Fix: add a `#!/bin/sh` shebang as the very first line of {file} (a husky-managed hook gets this automatically the next time `uds init` touches it), or re-run `uds init`.',
|
|
895
|
+
hookBareUdsTitle: '⚠ [pre-commit] {file} asks npm to run the bare name "uds", which on the npm registry is an unrelated package (not this project).',
|
|
896
|
+
hookBareUdsFix: ' Fix: run `uds update` — it replaces the line UDS itself wrote with one that only runs the installed UDS CLI. A line you wrote or edited yourself is left alone; change it to `universal-dev-standards check`.',
|
|
895
897
|
// Summary mode (--summary)
|
|
896
898
|
summary_mode: {
|
|
897
899
|
title: 'UDS Status Summary',
|
|
@@ -1038,6 +1040,11 @@ export const messages = {
|
|
|
1038
1040
|
currentVersion: 'Current version',
|
|
1039
1041
|
latestVersion: 'Latest version',
|
|
1040
1042
|
upToDate: '✓ Standards are up to date.',
|
|
1043
|
+
// Pre-commit hook migration (bare-name `uds` on the npm registry is not this project)
|
|
1044
|
+
hookMigrated: '✓ .husky/pre-commit: replaced the line an older UDS wrote (it asked npm to resolve the bare name "uds", which is not this project) with one that runs only the installed UDS CLI.',
|
|
1045
|
+
hookWouldMigrate: '+ .husky/pre-commit: would replace the line an older UDS wrote (it asks npm to resolve the bare name "uds", which is not this project) with one that runs only the installed UDS CLI.',
|
|
1046
|
+
hookLegacyKept: '⚠ .husky/pre-commit line {line} ("{text}") asks npm to resolve the bare name "uds", which on the npm registry is an unrelated package. UDS did not write this exact line, so it was left alone. Change it to "universal-dev-standards check" — it needs the UDS CLI installed in this project or on PATH.',
|
|
1047
|
+
hookMigrateFailed: '⚠ Could not rewrite .husky/pre-commit: {error}. The line that asks npm for the bare name "uds" is still there — change it to "universal-dev-standards check".',
|
|
1041
1048
|
newerVersion: '(You have a newer version than the registry: {version})',
|
|
1042
1049
|
updateAvailable: 'Update available: {current} → {latest}',
|
|
1043
1050
|
// Files
|
|
@@ -1264,7 +1271,11 @@ export const messages = {
|
|
|
1264
1271
|
uninstallPartial: '⚠ Uninstall completed with errors.',
|
|
1265
1272
|
removed: 'Removed',
|
|
1266
1273
|
skippedLabel: 'Skipped',
|
|
1267
|
-
errorsLabel: 'Errors'
|
|
1274
|
+
errorsLabel: 'Errors',
|
|
1275
|
+
cannotPrompt: 'Cannot ask for confirmation: nothing is attached to answer the prompt (non-interactive shell, CI, or a pipe).',
|
|
1276
|
+
cannotPromptHint: 'Nothing has been changed. Re-run with --yes to confirm up front, or with --dry-run to preview the changes.',
|
|
1277
|
+
promptClosed: 'The prompt was closed before an answer was given. Nothing has been changed.',
|
|
1278
|
+
promptClosedMidRun: 'A prompt was closed part-way through. Steps already completed stay done; run `uds uninstall` again to finish.'
|
|
1268
1279
|
}
|
|
1269
1280
|
}
|
|
1270
1281
|
},
|
|
@@ -2135,7 +2146,7 @@ export const messages = {
|
|
|
2135
2146
|
missingSkillsHint: '提示:執行 `uds update` 安裝缺少的 Skills/斜線命令',
|
|
2136
2147
|
// 提交前檢查是否真的會被 git 執行(uds init 寫了檢查檔,卻沒確認 git 會跑它——這裡只回報,不寫入)
|
|
2137
2148
|
hookNotWiredTitle: '⚠ [pre-commit] {file} 已安裝,但 git 實際不會執行它。',
|
|
2138
|
-
hookNotWiredOverride: ' git core.hooksPath 已設定為「{path}」——UDS 不會覆蓋它。請確認該路徑下的檔案也會執行 `
|
|
2149
|
+
hookNotWiredOverride: ' git core.hooksPath 已設定為「{path}」——UDS 不會覆蓋它。請確認該路徑下的檔案也會執行 `universal-dev-standards check`(要用已安裝的 CLI——不要用短名稱 uds,它在 npm registry 上是不相干的套件),或自行改用:git config --local core.hooksPath .husky',
|
|
2139
2150
|
hookNotWiredUnwired: ' git core.hooksPath 未設定,而 {file} 也不在 git 預設會讀取的路徑上。',
|
|
2140
2151
|
hookNotWiredFix: ' 修復方式(僅對此 clone 生效,不會進版控,其他人 clone 後要自己再跑一次):git config --local core.hooksPath .husky',
|
|
2141
2152
|
hookNotWiredFixNative: ' 修復方式:把 core.hooksPath 改回預設(git config --local --unset core.hooksPath),或改在「{path}」底下也加上 `uds check`。',
|
|
@@ -2151,6 +2162,8 @@ export const messages = {
|
|
|
2151
2162
|
// 2026-09-27 於 CI(windows-latest)實測。
|
|
2152
2163
|
hookMissingShebangTitle: '⚠ [pre-commit] {file} 沒有 shebang 行——在 Windows 上 git 無法執行它。',
|
|
2153
2164
|
hookMissingShebangFix: ' 修復方式:在 {file} 的第一行加上 shebang `#!/bin/sh`(由 husky 管理的 hook,下次 `uds init` 動到它時會自動處理),或重新執行 `uds init`。',
|
|
2165
|
+
hookBareUdsTitle: '⚠ [pre-commit] {file} 會請 npm 執行裸名稱 uds,而它在 npm registry 上是不相干的套件(不是本專案)。',
|
|
2166
|
+
hookBareUdsFix: ' 修復方式:執行 `uds update`——它會把 UDS 自己寫入的那一行換成只執行已安裝 UDS CLI 的寫法。你自己寫或改過的那一行不會被動,請自行改成 `universal-dev-standards check`。',
|
|
2154
2167
|
// Summary mode (--summary)
|
|
2155
2168
|
summary_mode: {
|
|
2156
2169
|
title: 'UDS 狀態摘要',
|
|
@@ -2294,6 +2307,11 @@ export const messages = {
|
|
|
2294
2307
|
currentVersion: '目前版本',
|
|
2295
2308
|
latestVersion: '最新版本',
|
|
2296
2309
|
upToDate: '✓ 標準已是最新版本。',
|
|
2310
|
+
// pre-commit hook 遷移(npm registry 上的裸名稱 uds 不是本專案)
|
|
2311
|
+
hookMigrated: '✓ .husky/pre-commit:已把舊版 UDS 寫入的那一行(它會請 npm 解析裸名稱 uds,而那不是本專案)換成只執行已安裝 UDS CLI 的寫法。',
|
|
2312
|
+
hookWouldMigrate: '+ .husky/pre-commit:將把舊版 UDS 寫入的那一行(它會請 npm 解析裸名稱 uds,而那不是本專案)換成只執行已安裝 UDS CLI 的寫法。',
|
|
2313
|
+
hookLegacyKept: '⚠ .husky/pre-commit 第 {line} 行(「{text}」)會請 npm 解析裸名稱 uds,而它在 npm registry 上是不相干的套件。這一行不是 UDS 寫入的原樣,所以沒有動它。請自行改成 universal-dev-standards check——需要在本專案或 PATH 上已安裝 UDS CLI。',
|
|
2314
|
+
hookMigrateFailed: '⚠ 無法改寫 .husky/pre-commit:{error}。請 npm 解析裸名稱 uds 的那一行仍在,請自行改成 universal-dev-standards check。',
|
|
2297
2315
|
newerVersion: '(你的版本比登錄庫更新:{version})',
|
|
2298
2316
|
updateAvailable: '有可用更新:{current} → {latest}',
|
|
2299
2317
|
// Files
|
|
@@ -2520,7 +2538,11 @@ export const messages = {
|
|
|
2520
2538
|
uninstallPartial: '⚠ 解除安裝完成但有錯誤。',
|
|
2521
2539
|
removed: '已移除',
|
|
2522
2540
|
skippedLabel: '已跳過',
|
|
2523
|
-
errorsLabel: '錯誤'
|
|
2541
|
+
errorsLabel: '錯誤',
|
|
2542
|
+
cannotPrompt: '無法詢問確認:目前沒有可回答提示的終端機(非互動 shell、CI 或管線)。',
|
|
2543
|
+
cannotPromptHint: '尚未變更任何檔案。請加上 --yes 事先確認,或加上 --dry-run 預覽變更。',
|
|
2544
|
+
promptClosed: '提示在回答前被關閉。尚未變更任何檔案。',
|
|
2545
|
+
promptClosedMidRun: '執行途中提示被關閉。已完成的步驟維持完成;請再執行一次 `uds uninstall` 完成其餘部分。'
|
|
2524
2546
|
}
|
|
2525
2547
|
}
|
|
2526
2548
|
},
|
|
@@ -3407,7 +3429,7 @@ export const messages = {
|
|
|
3407
3429
|
missingSkillsHint: '提示:执行 `uds update` 安装缺少的 Skills/斜线命令',
|
|
3408
3430
|
// 提交前检查是否真的会被 git 执行(uds init 写了检查文件,却没确认 git 会跑它——这里只回报,不写入)
|
|
3409
3431
|
hookNotWiredTitle: '⚠ [pre-commit] {file} 已安装,但 git 实际不会执行它。',
|
|
3410
|
-
hookNotWiredOverride: ' git core.hooksPath 已设定为“{path}”——UDS 不会覆盖它。请确认该路径下的文件也会执行 `
|
|
3432
|
+
hookNotWiredOverride: ' git core.hooksPath 已设定为“{path}”——UDS 不会覆盖它。请确认该路径下的文件也会执行 `universal-dev-standards check`(要用已安装的 CLI——不要用短名称 uds,它在 npm registry 上是不相干的包),或自行改用:git config --local core.hooksPath .husky',
|
|
3411
3433
|
hookNotWiredUnwired: ' git core.hooksPath 未设定,而 {file} 也不在 git 默认会读取的路径上。',
|
|
3412
3434
|
hookNotWiredFix: ' 修复方式(仅对此 clone 生效,不会进版控,其他人 clone 后要自己再跑一次):git config --local core.hooksPath .husky',
|
|
3413
3435
|
hookNotWiredFixNative: ' 修复方式:把 core.hooksPath 改回默认(git config --local --unset core.hooksPath),或改在“{path}”下也加上 `uds check`。',
|
|
@@ -3419,6 +3441,8 @@ export const messages = {
|
|
|
3419
3441
|
// 2026-09-27 于 CI(windows-latest)实测。
|
|
3420
3442
|
hookMissingShebangTitle: '⚠ [pre-commit] {file} 没有 shebang 行——在 Windows 上 git 无法执行它。',
|
|
3421
3443
|
hookMissingShebangFix: ' 修复方式:在 {file} 的第一行加上 shebang `#!/bin/sh`(由 husky 管理的 hook,下次 `uds init` 动到它时会自动处理),或重新运行 `uds init`。',
|
|
3444
|
+
hookBareUdsTitle: '⚠ [pre-commit] {file} 会请 npm 执行裸名称 uds,而它在 npm registry 上是不相干的包(不是本项目)。',
|
|
3445
|
+
hookBareUdsFix: ' 修复方式:运行 `uds update`——它会把 UDS 自己写入的那一行换成只执行已安装 UDS CLI 的写法。你自己写或改过的那一行不会被动,请自行改成 `universal-dev-standards check`。',
|
|
3422
3446
|
// Summary mode (--summary)
|
|
3423
3447
|
summary_mode: {
|
|
3424
3448
|
title: 'UDS 状态摘要',
|
|
@@ -3474,6 +3498,11 @@ export const messages = {
|
|
|
3474
3498
|
currentVersion: '当前版本',
|
|
3475
3499
|
latestVersion: '最新版本',
|
|
3476
3500
|
upToDate: '✓ 标准是最新的。',
|
|
3501
|
+
// pre-commit hook 迁移(npm registry 上的裸名称 uds 不是本项目)
|
|
3502
|
+
hookMigrated: '✓ .husky/pre-commit:已把旧版 UDS 写入的那一行(它会请 npm 解析裸名称 uds,而那不是本项目)换成只执行已安装 UDS CLI 的写法。',
|
|
3503
|
+
hookWouldMigrate: '+ .husky/pre-commit:将把旧版 UDS 写入的那一行(它会请 npm 解析裸名称 uds,而那不是本项目)换成只执行已安装 UDS CLI 的写法。',
|
|
3504
|
+
hookLegacyKept: '⚠ .husky/pre-commit 第 {line} 行(“{text}”)会请 npm 解析裸名称 uds,而它在 npm registry 上是不相干的包。这一行不是 UDS 写入的原样,所以没有动它。请自行改成 universal-dev-standards check——需要在本项目或 PATH 上已安装 UDS CLI。',
|
|
3505
|
+
hookMigrateFailed: '⚠ 无法改写 .husky/pre-commit:{error}。请 npm 解析裸名称 uds 的那一行仍在,请自行改成 universal-dev-standards check。',
|
|
3477
3506
|
newerVersion: '(您有比注册表更新的版本:{version})',
|
|
3478
3507
|
updateAvailable: '有可用更新:{current} → {latest}',
|
|
3479
3508
|
// Files
|
|
@@ -3700,7 +3729,11 @@ export const messages = {
|
|
|
3700
3729
|
uninstallPartial: '⚠ 卸载完成但有错误。',
|
|
3701
3730
|
removed: '已移除',
|
|
3702
3731
|
skippedLabel: '已跳过',
|
|
3703
|
-
errorsLabel: '错误'
|
|
3732
|
+
errorsLabel: '错误',
|
|
3733
|
+
cannotPrompt: '无法询问确认:当前没有可回答提示的终端(非交互 shell、CI 或管道)。',
|
|
3734
|
+
cannotPromptHint: '尚未更改任何文件。请加上 --yes 提前确认,或加上 --dry-run 预览变更。',
|
|
3735
|
+
promptClosed: '提示在回答前被关闭。尚未更改任何文件。',
|
|
3736
|
+
promptClosedMidRun: '执行途中提示被关闭。已完成的步骤保持完成;请再运行一次 `uds uninstall` 完成其余部分。'
|
|
3704
3737
|
}
|
|
3705
3738
|
}
|
|
3706
3739
|
}
|
|
@@ -31,12 +31,13 @@
|
|
|
31
31
|
* @see core/turn-completion-integrity.md
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
-
import { existsSync, readFileSync, writeFileSync,
|
|
35
|
-
import { join, dirname, basename } from 'path';
|
|
34
|
+
import { existsSync, readFileSync, writeFileSync, copyFileSync, readdirSync } from 'fs';
|
|
35
|
+
import { join, dirname, basename, relative } from 'path';
|
|
36
36
|
import { fileURLToPath } from 'url';
|
|
37
37
|
import { execFileSync } from 'child_process';
|
|
38
38
|
import { load as parseYaml } from 'js-yaml';
|
|
39
39
|
import { detectAITools } from '../utils/detector.js';
|
|
40
|
+
import { newRecorder, mkdirTracked, recordFile, RECORD_KINDS } from '../core/install-records.js';
|
|
40
41
|
|
|
41
42
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
42
43
|
|
|
@@ -172,7 +173,41 @@ function probeLanguageLimits(hooksDir, scripts) {
|
|
|
172
173
|
return out;
|
|
173
174
|
}
|
|
174
175
|
|
|
175
|
-
|
|
176
|
+
/**
|
|
177
|
+
* Copy the shipped hook scripts (recursively — a hook may ship a directory beside
|
|
178
|
+
* it: locale packs, fixtures) into `<project>/scripts/hooks`, and RECORD every
|
|
179
|
+
* file this call actually wrote and every directory it had to create.
|
|
180
|
+
*
|
|
181
|
+
* `cpSync` did the copy before and returns nothing, which is why `uds uninstall`
|
|
182
|
+
* had no way to know which of the files under `scripts/hooks/` were UDS's — a
|
|
183
|
+
* directory an adopter's own hook scripts may share. A file that already exists
|
|
184
|
+
* and is byte-identical is not recorded (this call did not write it, so it
|
|
185
|
+
* proves nothing about who did); a file that exists, differs, and is not
|
|
186
|
+
* overwritten is left alone and not recorded.
|
|
187
|
+
*
|
|
188
|
+
* @param {string} srcDir Shipped hooks directory
|
|
189
|
+
* @param {string} destDir Destination directory (absolute)
|
|
190
|
+
* @param {{ overwrite: boolean, recorder: object, projectPath: string }} ctx
|
|
191
|
+
*/
|
|
192
|
+
function copyTreeRecorded(srcDir, destDir, ctx) {
|
|
193
|
+
mkdirTracked(ctx.recorder, ctx.projectPath, destDir);
|
|
194
|
+
for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
|
|
195
|
+
const src = join(srcDir, entry.name);
|
|
196
|
+
const dest = join(destDir, entry.name);
|
|
197
|
+
if (entry.isDirectory()) {
|
|
198
|
+
copyTreeRecorded(src, dest, ctx);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (existsSync(dest)) {
|
|
202
|
+
if (!ctx.overwrite) continue;
|
|
203
|
+
if (readFileSync(src).equals(readFileSync(dest))) continue;
|
|
204
|
+
}
|
|
205
|
+
copyFileSync(src, dest);
|
|
206
|
+
recordFile(ctx.recorder, ctx.projectPath, relative(ctx.projectPath, dest), RECORD_KINDS.HOOK_SCRIPT);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function installHooks(projectPath, { overwriteScripts = true, recorder = newRecorder() } = {}) {
|
|
176
211
|
const claudeDir = join(projectPath, '.claude');
|
|
177
212
|
const settingsPath = join(claudeDir, 'settings.json');
|
|
178
213
|
const hooksDir = join(projectPath, 'scripts', 'hooks');
|
|
@@ -187,11 +222,8 @@ export function installHooks(projectPath, { overwriteScripts = true } = {}) {
|
|
|
187
222
|
return { installed: false, scriptsCount: 0, settingsPath, events, skipped };
|
|
188
223
|
}
|
|
189
224
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
// Recursive: a hook may ship a directory beside it (locale packs, fixtures).
|
|
194
|
-
cpSync(hookDir, hooksDir, { recursive: true, force: overwriteScripts });
|
|
225
|
+
mkdirTracked(recorder, projectPath, claudeDir);
|
|
226
|
+
copyTreeRecorded(hookDir, hooksDir, { overwrite: overwriteScripts, recorder, projectPath });
|
|
195
227
|
|
|
196
228
|
let settings = {};
|
|
197
229
|
if (existsSync(settingsPath)) {
|
|
@@ -217,6 +249,7 @@ export function installHooks(projectPath, { overwriteScripts = true } = {}) {
|
|
|
217
249
|
events,
|
|
218
250
|
skipped,
|
|
219
251
|
languageLimits: probeLanguageLimits(hooksDir, scripts),
|
|
252
|
+
artifacts: recorder,
|
|
220
253
|
};
|
|
221
254
|
}
|
|
222
255
|
|
|
@@ -264,9 +297,8 @@ export const AGY_HOOK_COMMAND_LEGACY = `node scripts/hooks/${AGY_HOOK_SCRIPT}`;
|
|
|
264
297
|
* `overwrite: false` keeps a script that already exists (an adopter may have
|
|
265
298
|
* edited it); it is what `uds update --with-hooks` uses unless `--force` is given.
|
|
266
299
|
*/
|
|
267
|
-
function copyHookScripts(hookDir, hooksDir, { overwrite = true } = {}) {
|
|
268
|
-
|
|
269
|
-
cpSync(hookDir, hooksDir, { recursive: true, force: overwrite });
|
|
300
|
+
function copyHookScripts(hookDir, hooksDir, { overwrite = true, recorder, projectPath } = {}) {
|
|
301
|
+
copyTreeRecorded(hookDir, hooksDir, { overwrite, recorder, projectPath });
|
|
270
302
|
}
|
|
271
303
|
|
|
272
304
|
/**
|
|
@@ -283,7 +315,7 @@ function copyHookScripts(hookDir, hooksDir, { overwrite = true } = {}) {
|
|
|
283
315
|
* @param {string} projectPath
|
|
284
316
|
* @returns {{ installed: boolean, settingsPath: string, event?: string, reason?: string }}
|
|
285
317
|
*/
|
|
286
|
-
export function installCodexHooks(projectPath, { overwriteScripts = true } = {}) {
|
|
318
|
+
export function installCodexHooks(projectPath, { overwriteScripts = true, recorder = newRecorder() } = {}) {
|
|
287
319
|
const hooksJsonPath = join(projectPath, '.codex', 'hooks.json');
|
|
288
320
|
const hookDir = hooksSourceDir();
|
|
289
321
|
|
|
@@ -291,9 +323,8 @@ export function installCodexHooks(projectPath, { overwriteScripts = true } = {})
|
|
|
291
323
|
return { installed: false, settingsPath: hooksJsonPath, reason: `hook script not found: ${CODEX_HOOK_SCRIPT}` };
|
|
292
324
|
}
|
|
293
325
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
copyHookScripts(hookDir, join(projectPath, 'scripts', 'hooks'), { overwrite: overwriteScripts });
|
|
326
|
+
mkdirTracked(recorder, projectPath, join(projectPath, '.codex'));
|
|
327
|
+
copyHookScripts(hookDir, join(projectPath, 'scripts', 'hooks'), { overwrite: overwriteScripts, recorder, projectPath });
|
|
297
328
|
|
|
298
329
|
let config = {};
|
|
299
330
|
if (existsSync(hooksJsonPath)) {
|
|
@@ -310,7 +341,7 @@ export function installCodexHooks(projectPath, { overwriteScripts = true } = {})
|
|
|
310
341
|
]);
|
|
311
342
|
|
|
312
343
|
writeFileSync(hooksJsonPath, JSON.stringify(config, null, 2) + '\n');
|
|
313
|
-
return { installed: true, settingsPath: hooksJsonPath, event: 'Stop' };
|
|
344
|
+
return { installed: true, settingsPath: hooksJsonPath, event: 'Stop', artifacts: recorder };
|
|
314
345
|
}
|
|
315
346
|
|
|
316
347
|
/**
|
|
@@ -323,7 +354,7 @@ export function installCodexHooks(projectPath, { overwriteScripts = true } = {})
|
|
|
323
354
|
* @param {string} projectPath
|
|
324
355
|
* @returns {{ installed: boolean, settingsPath: string, event?: string, reason?: string }}
|
|
325
356
|
*/
|
|
326
|
-
export function installGeminiHooks(projectPath, { overwriteScripts = true } = {}) {
|
|
357
|
+
export function installGeminiHooks(projectPath, { overwriteScripts = true, recorder = newRecorder() } = {}) {
|
|
327
358
|
const settingsPath = join(projectPath, '.gemini', 'settings.json');
|
|
328
359
|
const hookDir = hooksSourceDir();
|
|
329
360
|
|
|
@@ -331,9 +362,8 @@ export function installGeminiHooks(projectPath, { overwriteScripts = true } = {}
|
|
|
331
362
|
return { installed: false, settingsPath, reason: `hook script not found: ${GEMINI_HOOK_SCRIPT}` };
|
|
332
363
|
}
|
|
333
364
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
copyHookScripts(hookDir, join(projectPath, 'scripts', 'hooks'), { overwrite: overwriteScripts });
|
|
365
|
+
mkdirTracked(recorder, projectPath, join(projectPath, '.gemini'));
|
|
366
|
+
copyHookScripts(hookDir, join(projectPath, 'scripts', 'hooks'), { overwrite: overwriteScripts, recorder, projectPath });
|
|
337
367
|
|
|
338
368
|
let settings = {};
|
|
339
369
|
if (existsSync(settingsPath)) {
|
|
@@ -349,7 +379,7 @@ export function installGeminiHooks(projectPath, { overwriteScripts = true } = {}
|
|
|
349
379
|
]);
|
|
350
380
|
|
|
351
381
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
352
|
-
return { installed: true, settingsPath, event: 'AfterAgent' };
|
|
382
|
+
return { installed: true, settingsPath, event: 'AfterAgent', artifacts: recorder };
|
|
353
383
|
}
|
|
354
384
|
|
|
355
385
|
/**
|
|
@@ -377,7 +407,7 @@ export function installGeminiHooks(projectPath, { overwriteScripts = true } = {}
|
|
|
377
407
|
* @param {string} projectPath
|
|
378
408
|
* @returns {{ installed: boolean, settingsPath: string, event?: string, reason?: string }}
|
|
379
409
|
*/
|
|
380
|
-
export function installAgyHooks(projectPath, { overwriteScripts = true } = {}) {
|
|
410
|
+
export function installAgyHooks(projectPath, { overwriteScripts = true, recorder = newRecorder() } = {}) {
|
|
381
411
|
const hooksJsonPath = join(projectPath, '.agents', 'hooks.json');
|
|
382
412
|
const hookDir = hooksSourceDir();
|
|
383
413
|
|
|
@@ -397,9 +427,8 @@ export function installAgyHooks(projectPath, { overwriteScripts = true } = {}) {
|
|
|
397
427
|
}
|
|
398
428
|
}
|
|
399
429
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
copyHookScripts(hookDir, join(projectPath, 'scripts', 'hooks'), { overwrite: overwriteScripts });
|
|
430
|
+
mkdirTracked(recorder, projectPath, join(projectPath, '.agents'));
|
|
431
|
+
copyHookScripts(hookDir, join(projectPath, 'scripts', 'hooks'), { overwrite: overwriteScripts, recorder, projectPath });
|
|
403
432
|
|
|
404
433
|
const command = AGY_HOOK_COMMAND;
|
|
405
434
|
const entry = config[AGY_HOOK_NAME];
|
|
@@ -416,7 +445,7 @@ export function installAgyHooks(projectPath, { overwriteScripts = true } = {}) {
|
|
|
416
445
|
config[AGY_HOOK_NAME] = { ...mine, Stop: stop };
|
|
417
446
|
|
|
418
447
|
writeFileSync(hooksJsonPath, JSON.stringify(config, null, 2) + '\n');
|
|
419
|
-
return { installed: true, settingsPath: hooksJsonPath, event: 'Stop' };
|
|
448
|
+
return { installed: true, settingsPath: hooksJsonPath, event: 'Stop', artifacts: recorder };
|
|
420
449
|
}
|
|
421
450
|
|
|
422
451
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -574,9 +603,11 @@ const INSTALLERS = {
|
|
|
574
603
|
* @param {string} projectPath
|
|
575
604
|
* @param {string[]} tools
|
|
576
605
|
* @param {{ plan?: boolean, overwriteScripts?: boolean }} [opts]
|
|
577
|
-
* @returns {{ results: Array<{tool:string, outcome:'installed'|'unchanged'|'would-install'|'blocked'|'failed', path:string, why?:string, repaired?:boolean}>, scripts: {added:number, identical:number, kept:string[]} }}
|
|
606
|
+
* @returns {{ results: Array<{tool:string, outcome:'installed'|'unchanged'|'would-install'|'blocked'|'failed', path:string, why?:string, repaired?:boolean}>, scripts: {added:number, identical:number, kept:string[]}, artifacts: object }}
|
|
607
|
+
* `artifacts` is the recorder of what this call wrote (see core/install-records.js);
|
|
608
|
+
* the caller persists it into the manifest so `uds uninstall` can remove exactly that.
|
|
578
609
|
*/
|
|
579
|
-
export function installMissingHooks(projectPath, tools, { plan = false, overwriteScripts = false } = {}) {
|
|
610
|
+
export function installMissingHooks(projectPath, tools, { plan = false, overwriteScripts = false, recorder = newRecorder() } = {}) {
|
|
580
611
|
// Measured BEFORE anything is copied: afterwards every script would read "identical".
|
|
581
612
|
const scripts = scriptDiff(projectPath);
|
|
582
613
|
const results = [];
|
|
@@ -585,9 +616,9 @@ export function installMissingHooks(projectPath, tools, { plan = false, overwrit
|
|
|
585
616
|
if (st.status === 'present') { results.push({ tool, outcome: 'unchanged', path: st.path }); continue; }
|
|
586
617
|
if (st.status === 'blocked') { results.push({ tool, outcome: 'blocked', path: st.path, why: st.why }); continue; }
|
|
587
618
|
if (plan) { results.push({ tool, outcome: 'would-install', path: st.path, repaired: st.status === 'stale' }); continue; }
|
|
588
|
-
const r = INSTALLERS[tool](projectPath, { overwriteScripts });
|
|
619
|
+
const r = INSTALLERS[tool](projectPath, { overwriteScripts, recorder });
|
|
589
620
|
if (r.installed) results.push({ tool, outcome: 'installed', path: r.settingsPath ?? st.path, repaired: st.status === 'stale' });
|
|
590
621
|
else results.push({ tool, outcome: 'failed', path: r.settingsPath ?? st.path, why: r.reason ?? 'no standard produced a usable hook' });
|
|
591
622
|
}
|
|
592
|
-
return { results, scripts };
|
|
623
|
+
return { results, scripts, artifacts: recorder };
|
|
593
624
|
}
|