pumuki 6.3.88 → 6.3.90
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,3 +1,8 @@
|
|
|
1
|
+
## 2026-04-20 (v6.3.89)
|
|
2
|
+
- **Aislamiento por worktree**: `pumuki install` detecta worktrees sin `core.hooksPath` explícito y fija un `core.hooksPath` local a `.pumuki/git-hooks`, evitando que los hooks gestionados se escriban en `.git/hooks` del checkout principal compartido.
|
|
3
|
+
- **Rollback limpio**: `pumuki uninstall` retira ese `core.hooksPath` solo si fue creado por Pumuki para el worktree.
|
|
4
|
+
- **Rollout recomendado**: publicar `pumuki@6.3.89`, repin inmediato en `Flux_training` y repetir la repro de worktree (`install`, `bootstrap-manifest`, `status --json`, checksums de hooks en checkout principal).
|
|
5
|
+
|
|
1
6
|
## 2026-04-20 (v6.3.87)
|
|
2
7
|
- Cierra el segundo tramo de PUM-026: si PRE_WRITE solo arregla el receipt MCP y el gate pasa a verde, fuerza un refresh de paridad contra PRE_COMMIT antes del veredicto final.
|
|
3
8
|
- Rollout recomendado: actualizar Flux_training y repetir la repro mínima de validate/pre-commit/.ai_evidence.
|
|
@@ -27,6 +27,81 @@ export type PumukiHooksDirectoryResolution = {
|
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
const HOOK_FILE_MODE = 0o755;
|
|
30
|
+
const PUMUKI_WORKTREE_HOOKS_PATH = '.pumuki/git-hooks';
|
|
31
|
+
|
|
32
|
+
const readLocalGitConfigValue = (repoRoot: string, key: string): string | null => {
|
|
33
|
+
try {
|
|
34
|
+
const value = execFileSync('git', ['config', '--local', '--get', key], {
|
|
35
|
+
cwd: repoRoot,
|
|
36
|
+
encoding: 'utf8',
|
|
37
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
38
|
+
}).trim();
|
|
39
|
+
return value.length > 0 ? value : null;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const writeLocalGitConfigValue = (repoRoot: string, key: string, value: string): void => {
|
|
46
|
+
execFileSync('git', ['config', '--local', key, value], {
|
|
47
|
+
cwd: repoRoot,
|
|
48
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const unsetLocalGitConfigValue = (repoRoot: string, key: string): void => {
|
|
53
|
+
try {
|
|
54
|
+
execFileSync('git', ['config', '--local', '--unset-all', key], {
|
|
55
|
+
cwd: repoRoot,
|
|
56
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
57
|
+
});
|
|
58
|
+
} catch {
|
|
59
|
+
// noop
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const isWorktreeCheckout = (repoRoot: string): boolean => {
|
|
64
|
+
try {
|
|
65
|
+
const gitPointer = readFileSync(join(repoRoot, '.git'), 'utf8').trim();
|
|
66
|
+
return /^gitdir:\s+/i.test(gitPointer);
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const isPumukiManagedWorktreeHooksPath = (repoRoot: string, hooksPath: string): boolean => {
|
|
73
|
+
const normalizedHooksPath = hooksPath.trim();
|
|
74
|
+
return (
|
|
75
|
+
normalizedHooksPath === PUMUKI_WORKTREE_HOOKS_PATH ||
|
|
76
|
+
normalizedHooksPath === resolve(repoRoot, PUMUKI_WORKTREE_HOOKS_PATH)
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const ensureWorktreeLocalHooksPath = (repoRoot: string): void => {
|
|
81
|
+
if (!isWorktreeCheckout(repoRoot)) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const currentHooksPath = readLocalGitConfigValue(repoRoot, 'core.hooksPath');
|
|
86
|
+
if (currentHooksPath) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
writeLocalGitConfigValue(repoRoot, 'core.hooksPath', PUMUKI_WORKTREE_HOOKS_PATH);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const clearWorktreeLocalHooksPath = (repoRoot: string): void => {
|
|
94
|
+
if (!isWorktreeCheckout(repoRoot)) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const currentHooksPath = readLocalGitConfigValue(repoRoot, 'core.hooksPath');
|
|
99
|
+
if (!currentHooksPath || !isPumukiManagedWorktreeHooksPath(repoRoot, currentHooksPath)) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
unsetLocalGitConfigValue(repoRoot, 'core.hooksPath');
|
|
104
|
+
};
|
|
30
105
|
|
|
31
106
|
const resolveGitPath = (repoRoot: string, gitPathTarget: string): string | null => {
|
|
32
107
|
try {
|
|
@@ -137,6 +212,7 @@ const ensureHooksDirectory = (repoRoot: string): void => {
|
|
|
137
212
|
};
|
|
138
213
|
|
|
139
214
|
export const installPumukiHooks = (repoRoot: string): HookInstallResult => {
|
|
215
|
+
ensureWorktreeLocalHooksPath(repoRoot);
|
|
140
216
|
ensureHooksDirectory(repoRoot);
|
|
141
217
|
const changedHooks: PumukiManagedHook[] = [];
|
|
142
218
|
|
|
@@ -180,6 +256,7 @@ export const uninstallPumukiHooks = (repoRoot: string): HookUninstallResult => {
|
|
|
180
256
|
changedHooks.push(hook);
|
|
181
257
|
}
|
|
182
258
|
|
|
259
|
+
clearWorktreeLocalHooksPath(repoRoot);
|
|
183
260
|
return { changedHooks };
|
|
184
261
|
};
|
|
185
262
|
|
|
@@ -1,21 +1,173 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
1
3
|
import { spawnSync as runSpawnSync } from 'node:child_process';
|
|
2
4
|
|
|
3
5
|
export interface ILifecycleNpmService {
|
|
4
6
|
runNpm(args: ReadonlyArray<string>, cwd: string): void;
|
|
5
7
|
}
|
|
6
8
|
|
|
9
|
+
type LifecyclePackageManager = 'npm' | 'pnpm' | 'yarn';
|
|
10
|
+
|
|
11
|
+
type PackageManagerManifest = {
|
|
12
|
+
packageManager?: string;
|
|
13
|
+
workspaces?: unknown;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const resolvePackageManagerFromManifest = (cwd: string): LifecyclePackageManager | undefined => {
|
|
17
|
+
const packageJsonPath = join(cwd, 'package.json');
|
|
18
|
+
if (!existsSync(packageJsonPath)) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const raw = readFileSync(packageJsonPath, 'utf8');
|
|
24
|
+
const manifest = JSON.parse(raw) as PackageManagerManifest;
|
|
25
|
+
const packageManager = manifest.packageManager?.trim().toLowerCase();
|
|
26
|
+
if (!packageManager) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
if (packageManager.startsWith('pnpm@')) {
|
|
30
|
+
return 'pnpm';
|
|
31
|
+
}
|
|
32
|
+
if (packageManager.startsWith('yarn@')) {
|
|
33
|
+
return 'yarn';
|
|
34
|
+
}
|
|
35
|
+
if (packageManager.startsWith('npm@')) {
|
|
36
|
+
return 'npm';
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return undefined;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const resolveLifecyclePackageManager = (cwd: string): LifecyclePackageManager => {
|
|
46
|
+
const fromManifest = resolvePackageManagerFromManifest(cwd);
|
|
47
|
+
if (fromManifest) {
|
|
48
|
+
return fromManifest;
|
|
49
|
+
}
|
|
50
|
+
if (existsSync(join(cwd, 'pnpm-lock.yaml'))) {
|
|
51
|
+
return 'pnpm';
|
|
52
|
+
}
|
|
53
|
+
if (existsSync(join(cwd, 'yarn.lock'))) {
|
|
54
|
+
return 'yarn';
|
|
55
|
+
}
|
|
56
|
+
return 'npm';
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const isWorkspaceRoot = (cwd: string): boolean => {
|
|
60
|
+
if (existsSync(join(cwd, 'pnpm-workspace.yaml'))) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
const packageJsonPath = join(cwd, 'package.json');
|
|
64
|
+
if (!existsSync(packageJsonPath)) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const raw = readFileSync(packageJsonPath, 'utf8');
|
|
69
|
+
const manifest = JSON.parse(raw) as PackageManagerManifest;
|
|
70
|
+
return Array.isArray(manifest.workspaces) || typeof manifest.workspaces === 'object';
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const translateInstallLikeArgs = (
|
|
77
|
+
packageManager: Exclude<LifecyclePackageManager, 'npm'>,
|
|
78
|
+
args: ReadonlyArray<string>,
|
|
79
|
+
cwd: string
|
|
80
|
+
): ReadonlyArray<string> => {
|
|
81
|
+
const packageSpecs = args.filter((arg) => !arg.startsWith('-')).slice(1);
|
|
82
|
+
if (packageSpecs.length === 0) {
|
|
83
|
+
return args;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const useExact = args.includes('--save-exact');
|
|
87
|
+
const useDev = args.includes('--save-dev');
|
|
88
|
+
|
|
89
|
+
if (packageManager === 'pnpm') {
|
|
90
|
+
return [
|
|
91
|
+
'add',
|
|
92
|
+
...(isWorkspaceRoot(cwd) ? ['-w'] : []),
|
|
93
|
+
...(useDev ? ['-D'] : []),
|
|
94
|
+
...(useExact ? ['-E'] : []),
|
|
95
|
+
...packageSpecs,
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return [
|
|
100
|
+
'add',
|
|
101
|
+
...(useDev ? ['--dev'] : []),
|
|
102
|
+
...(useExact ? ['--exact'] : []),
|
|
103
|
+
...packageSpecs,
|
|
104
|
+
];
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const translateUninstallLikeArgs = (
|
|
108
|
+
packageManager: Exclude<LifecyclePackageManager, 'npm'>,
|
|
109
|
+
args: ReadonlyArray<string>,
|
|
110
|
+
cwd: string
|
|
111
|
+
): ReadonlyArray<string> => {
|
|
112
|
+
const packageSpecs = args.filter((arg) => !arg.startsWith('-')).slice(1);
|
|
113
|
+
if (packageSpecs.length === 0) {
|
|
114
|
+
return args;
|
|
115
|
+
}
|
|
116
|
+
if (packageManager === 'pnpm') {
|
|
117
|
+
return ['remove', ...(isWorkspaceRoot(cwd) ? ['-w'] : []), ...packageSpecs];
|
|
118
|
+
}
|
|
119
|
+
return ['remove', ...packageSpecs];
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export const resolveLifecyclePackageManagerCommand = (
|
|
123
|
+
args: ReadonlyArray<string>,
|
|
124
|
+
cwd: string
|
|
125
|
+
): {
|
|
126
|
+
command: LifecyclePackageManager;
|
|
127
|
+
args: ReadonlyArray<string>;
|
|
128
|
+
} => {
|
|
129
|
+
const packageManager = resolveLifecyclePackageManager(cwd);
|
|
130
|
+
if (packageManager === 'npm') {
|
|
131
|
+
return {
|
|
132
|
+
command: 'npm',
|
|
133
|
+
args,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const primaryCommand = args[0];
|
|
138
|
+
if (primaryCommand === 'install') {
|
|
139
|
+
return {
|
|
140
|
+
command: packageManager,
|
|
141
|
+
args: translateInstallLikeArgs(packageManager, args, cwd),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (primaryCommand === 'uninstall') {
|
|
145
|
+
return {
|
|
146
|
+
command: packageManager,
|
|
147
|
+
args: translateUninstallLikeArgs(packageManager, args, cwd),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
command: packageManager,
|
|
153
|
+
args,
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
|
|
7
157
|
export class LifecycleNpmService implements ILifecycleNpmService {
|
|
8
158
|
runNpm(args: ReadonlyArray<string>, cwd: string): void {
|
|
9
|
-
const
|
|
159
|
+
const execution = resolveLifecyclePackageManagerCommand(args, cwd);
|
|
160
|
+
const renderedCommand = `${execution.command} ${execution.args.join(' ')}`.trim();
|
|
161
|
+
const result = runSpawnSync(execution.command, execution.args, {
|
|
10
162
|
cwd,
|
|
11
163
|
stdio: 'inherit',
|
|
12
164
|
env: process.env,
|
|
13
165
|
});
|
|
14
166
|
if (result.error) {
|
|
15
|
-
throw new Error(
|
|
167
|
+
throw new Error(`${renderedCommand} failed: ${result.error.message}`);
|
|
16
168
|
}
|
|
17
169
|
if (typeof result.status !== 'number' || result.status !== 0) {
|
|
18
|
-
throw new Error(
|
|
170
|
+
throw new Error(`${renderedCommand} failed with exit code ${result.status ?? 'unknown'}`);
|
|
19
171
|
}
|
|
20
172
|
}
|
|
21
173
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.90",
|
|
4
4
|
"description": "Enterprise-grade AST Intelligence System with multi-platform support (iOS, Android, Backend, Frontend) and Feature-First + DDD + Clean Architecture enforcement. Includes dynamic violations API for intelligent querying.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|