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 result = runSpawnSync('npm', args, {
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(`npm ${args.join(' ')} failed: ${result.error.message}`);
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(`npm ${args.join(' ')} failed with exit code ${result.status ?? 'unknown'}`);
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.88",
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": {