pumuki 6.3.9 → 6.3.11
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/README.md +2 -0
- package/VERSION +1 -1
- package/docs/REFRACTOR_PROGRESS.md +10 -1
- package/integrations/lifecycle/consumerPackage.ts +22 -0
- package/integrations/lifecycle/remove.ts +171 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -118,6 +118,8 @@ The `pumuki` binary provides repository lifecycle operations:
|
|
|
118
118
|
| `pumuki doctor` | Safety checks (hook drift, tracked `node_modules`, lifecycle state) |
|
|
119
119
|
| `pumuki status` | Current lifecycle snapshot |
|
|
120
120
|
|
|
121
|
+
`pumuki remove` is dependency-safe by design: it never deletes non-Pumuki third-party dependencies and preserves pre-existing third-party empty directories.
|
|
122
|
+
|
|
121
123
|
## Gate Commands
|
|
122
124
|
|
|
123
125
|
Dedicated gate binaries are available:
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
v6.3.
|
|
1
|
+
v6.3.11
|
|
@@ -153,7 +153,16 @@ Estado consolidado del refactor con seguimiento de tareas y evidencia del avance
|
|
|
153
153
|
- ✅ Corregir `Quick Start` del `README.md` para consumo real por npm (`pumuki`) y comandos ejecutables de lifecycle/gates.
|
|
154
154
|
- ✅ Auditar `README.md` con criterios enterprise (profesionalismo, claridad, estructura y completitud) y generar backlog de mejoras priorizado.
|
|
155
155
|
- ✅ Reescribir `README.md` de forma integral con estándar enterprise (audiencia consumer/framework separada, comandos reales y estructura consistente).
|
|
156
|
-
-
|
|
156
|
+
- ✅ Publicar `pumuki@6.3.9` en npm (tags `latest` y `next`) para reflejar la documentación enterprise reescrita.
|
|
157
|
+
- ✅ Ejecutar matriz E2E completa en `pumuki-mock-consumer` (`install -> pre-commit/pre-push/ci -> remove`) sobre escenarios `clean`, `violations` y `mixed`.
|
|
158
|
+
- ✅ Endurecer `pumuki-mock-consumer` con fixtures multiarchivo por plataforma y runner único `npm run pumuki:matrix`.
|
|
159
|
+
- ✅ Endurecer `pumuki remove` para podar residuos vacíos de `node_modules` sin borrar dependencias reales de terceros.
|
|
160
|
+
- ✅ Restringir poda de vacíos en `node_modules` a repos sin dependencias externas declaradas (seguridad enterprise reforzada).
|
|
161
|
+
- ✅ Publicar `pumuki@6.3.10` con hardening de desinstalación (`latest` y `next`).
|
|
162
|
+
- ✅ Refinar `pumuki remove` para eliminar vacíos nuevos tras uninstall manteniendo vacíos preexistentes de terceros.
|
|
163
|
+
- ✅ Endurecer `pumuki remove` para limpiar trazas del árbol de dependencias de Pumuki sin borrar dependencias ajenas (incluyendo vacíos no relacionados).
|
|
164
|
+
- 🚧 Publicar `pumuki@6.3.11` con la limpieza estricta de trazas y revalidar ciclo install/remove en consumidor mock.
|
|
165
|
+
- 🚧 Integrar MCP en `pumuki-mock-consumer` y validar consumo real de `ai_evidence` desde cliente MCP externo.
|
|
157
166
|
|
|
158
167
|
## Notas
|
|
159
168
|
- Estrategia obligatoria: commits atómicos por tarea.
|
|
@@ -7,6 +7,8 @@ export type ConsumerDependencySource = 'dependencies' | 'devDependencies' | 'non
|
|
|
7
7
|
type ConsumerPackageJson = {
|
|
8
8
|
dependencies?: Record<string, string>;
|
|
9
9
|
devDependencies?: Record<string, string>;
|
|
10
|
+
optionalDependencies?: Record<string, string>;
|
|
11
|
+
peerDependencies?: Record<string, string>;
|
|
10
12
|
};
|
|
11
13
|
|
|
12
14
|
const readPackageJson = (repoRoot: string): ConsumerPackageJson => {
|
|
@@ -46,3 +48,23 @@ export const resolveCurrentPumukiDependency = (repoRoot: string): {
|
|
|
46
48
|
source: 'none',
|
|
47
49
|
};
|
|
48
50
|
};
|
|
51
|
+
|
|
52
|
+
export const hasDeclaredDependenciesBeyondPumuki = (repoRoot: string): boolean => {
|
|
53
|
+
const pkg = readPackageJson(repoRoot);
|
|
54
|
+
const pumukiPackage = getCurrentPumukiPackageName();
|
|
55
|
+
const ignoredPackages = new Set([pumukiPackage, 'pumuki-ast-hooks']);
|
|
56
|
+
|
|
57
|
+
const hasExternalDependency = (section?: Record<string, string>): boolean => {
|
|
58
|
+
if (!section) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return Object.keys(section).some((dependencyName) => !ignoredPackages.has(dependencyName));
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
hasExternalDependency(pkg.dependencies) ||
|
|
66
|
+
hasExternalDependency(pkg.devDependencies) ||
|
|
67
|
+
hasExternalDependency(pkg.optionalDependencies) ||
|
|
68
|
+
hasExternalDependency(pkg.peerDependencies)
|
|
69
|
+
);
|
|
70
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync, readdirSync, rmSync, unlinkSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, rmSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
3
|
import { resolveCurrentPumukiDependency } from './consumerPackage';
|
|
4
4
|
import { LifecycleGitService, type ILifecycleGitService } from './gitService';
|
|
5
5
|
import { LifecycleNpmService, type ILifecycleNpmService } from './npmService';
|
|
@@ -13,6 +13,138 @@ export type LifecycleRemoveResult = {
|
|
|
13
13
|
removedArtifacts: ReadonlyArray<string>;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
type NodePackageManifest = {
|
|
17
|
+
dependencies?: Record<string, string>;
|
|
18
|
+
optionalDependencies?: Record<string, string>;
|
|
19
|
+
peerDependencies?: Record<string, string>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const readManifestDependencyNames = (packageDirectory: string): ReadonlyArray<string> => {
|
|
23
|
+
const packageJsonPath = join(packageDirectory, 'package.json');
|
|
24
|
+
if (!existsSync(packageJsonPath)) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const manifest = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as NodePackageManifest;
|
|
30
|
+
const dependencyNames = new Set<string>();
|
|
31
|
+
const sections = [manifest.dependencies, manifest.optionalDependencies, manifest.peerDependencies];
|
|
32
|
+
for (const section of sections) {
|
|
33
|
+
if (!section) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
for (const dependencyName of Object.keys(section)) {
|
|
37
|
+
dependencyNames.add(dependencyName);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return Array.from(dependencyNames);
|
|
41
|
+
} catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const resolveInstalledDependencyDirectory = (params: {
|
|
47
|
+
dependencyName: string;
|
|
48
|
+
fromPackageDirectory: string;
|
|
49
|
+
nodeModulesPath: string;
|
|
50
|
+
}): string | undefined => {
|
|
51
|
+
const repoRoot = dirname(params.nodeModulesPath);
|
|
52
|
+
let currentDirectory = params.fromPackageDirectory;
|
|
53
|
+
|
|
54
|
+
while (currentDirectory.startsWith(repoRoot)) {
|
|
55
|
+
const candidate = join(currentDirectory, 'node_modules', params.dependencyName);
|
|
56
|
+
if (existsSync(join(candidate, 'package.json'))) {
|
|
57
|
+
return candidate;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (currentDirectory === repoRoot) {
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const parentDirectory = dirname(currentDirectory);
|
|
65
|
+
if (parentDirectory === currentDirectory) {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
currentDirectory = parentDirectory;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const topLevelCandidate = join(params.nodeModulesPath, params.dependencyName);
|
|
72
|
+
if (existsSync(join(topLevelCandidate, 'package.json'))) {
|
|
73
|
+
return topLevelCandidate;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return undefined;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const collectPumukiTraceDirectories = (params: {
|
|
80
|
+
repoRoot: string;
|
|
81
|
+
packageName: string;
|
|
82
|
+
}): ReadonlySet<string> => {
|
|
83
|
+
const nodeModulesPath = join(params.repoRoot, 'node_modules');
|
|
84
|
+
const rootPackageDirectory = join(nodeModulesPath, params.packageName);
|
|
85
|
+
if (!existsSync(join(rootPackageDirectory, 'package.json'))) {
|
|
86
|
+
return new Set<string>();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const traceDirectories = new Set<string>();
|
|
90
|
+
const pending = [rootPackageDirectory];
|
|
91
|
+
|
|
92
|
+
while (pending.length > 0) {
|
|
93
|
+
const packageDirectory = pending.pop();
|
|
94
|
+
if (!packageDirectory || traceDirectories.has(packageDirectory)) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
traceDirectories.add(packageDirectory);
|
|
98
|
+
|
|
99
|
+
const dependencyNames = readManifestDependencyNames(packageDirectory);
|
|
100
|
+
for (const dependencyName of dependencyNames) {
|
|
101
|
+
const resolvedDependencyDirectory = resolveInstalledDependencyDirectory({
|
|
102
|
+
dependencyName,
|
|
103
|
+
fromPackageDirectory: packageDirectory,
|
|
104
|
+
nodeModulesPath,
|
|
105
|
+
});
|
|
106
|
+
if (!resolvedDependencyDirectory) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
pending.push(resolvedDependencyDirectory);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return traceDirectories;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
type EmptyDirectoryCleanupResult = 'removed' | 'missing' | 'not-empty';
|
|
117
|
+
|
|
118
|
+
const removeDirectoryIfEmpty = (directoryPath: string): EmptyDirectoryCleanupResult => {
|
|
119
|
+
if (!existsSync(directoryPath)) {
|
|
120
|
+
return 'missing';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (readdirSync(directoryPath).length > 0) {
|
|
124
|
+
return 'not-empty';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
rmSync(directoryPath, { recursive: true, force: true });
|
|
128
|
+
return 'removed';
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const cleanupTraceAncestors = (params: {
|
|
132
|
+
tracePath: string;
|
|
133
|
+
nodeModulesPath: string;
|
|
134
|
+
}): void => {
|
|
135
|
+
let currentDirectory = dirname(params.tracePath);
|
|
136
|
+
while (
|
|
137
|
+
currentDirectory.startsWith(params.nodeModulesPath) &&
|
|
138
|
+
currentDirectory !== params.nodeModulesPath
|
|
139
|
+
) {
|
|
140
|
+
const cleanupResult = removeDirectoryIfEmpty(currentDirectory);
|
|
141
|
+
if (cleanupResult === 'not-empty') {
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
currentDirectory = dirname(currentDirectory);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
16
148
|
const cleanupNodeModulesIfOnlyLockfile = (repoRoot: string): void => {
|
|
17
149
|
const nodeModulesPath = join(repoRoot, 'node_modules');
|
|
18
150
|
if (!existsSync(nodeModulesPath)) {
|
|
@@ -39,8 +171,7 @@ const cleanupNodeModulesIfOnlyLockfile = (repoRoot: string): void => {
|
|
|
39
171
|
const binEntry = entries.find((entry) => entry.name === '.bin' && entry.isDirectory());
|
|
40
172
|
if (binEntry) {
|
|
41
173
|
const binPath = join(nodeModulesPath, '.bin');
|
|
42
|
-
|
|
43
|
-
if (binEntries.length > 0) {
|
|
174
|
+
if (readdirSync(binPath).length > 0) {
|
|
44
175
|
return;
|
|
45
176
|
}
|
|
46
177
|
rmSync(binPath, { recursive: true, force: true });
|
|
@@ -55,6 +186,30 @@ const cleanupNodeModulesIfOnlyLockfile = (repoRoot: string): void => {
|
|
|
55
186
|
}
|
|
56
187
|
};
|
|
57
188
|
|
|
189
|
+
const cleanupPumukiTraceDirectories = (params: {
|
|
190
|
+
repoRoot: string;
|
|
191
|
+
traceDirectories: ReadonlySet<string>;
|
|
192
|
+
}): void => {
|
|
193
|
+
const nodeModulesPath = join(params.repoRoot, 'node_modules');
|
|
194
|
+
if (!existsSync(nodeModulesPath)) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const orderedTraceDirectories = Array.from(params.traceDirectories).sort(
|
|
199
|
+
(left, right) => right.length - left.length
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
for (const traceDirectory of orderedTraceDirectories) {
|
|
203
|
+
removeDirectoryIfEmpty(traceDirectory);
|
|
204
|
+
cleanupTraceAncestors({
|
|
205
|
+
tracePath: traceDirectory,
|
|
206
|
+
nodeModulesPath,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
cleanupNodeModulesIfOnlyLockfile(params.repoRoot);
|
|
211
|
+
};
|
|
212
|
+
|
|
58
213
|
export const runLifecycleRemove = (params?: {
|
|
59
214
|
cwd?: string;
|
|
60
215
|
git?: ILifecycleGitService;
|
|
@@ -72,9 +227,16 @@ export const runLifecycleRemove = (params?: {
|
|
|
72
227
|
|
|
73
228
|
const currentDependency = resolveCurrentPumukiDependency(repoRoot);
|
|
74
229
|
const packageName = getCurrentPumukiPackageName();
|
|
230
|
+
const pumukiTraceDirectories = collectPumukiTraceDirectories({
|
|
231
|
+
repoRoot,
|
|
232
|
+
packageName,
|
|
233
|
+
});
|
|
75
234
|
|
|
76
235
|
if (currentDependency.source === 'none') {
|
|
77
|
-
|
|
236
|
+
cleanupPumukiTraceDirectories({
|
|
237
|
+
repoRoot,
|
|
238
|
+
traceDirectories: pumukiTraceDirectories,
|
|
239
|
+
});
|
|
78
240
|
return {
|
|
79
241
|
repoRoot,
|
|
80
242
|
packageRemoved: false,
|
|
@@ -84,7 +246,10 @@ export const runLifecycleRemove = (params?: {
|
|
|
84
246
|
}
|
|
85
247
|
|
|
86
248
|
npm.runNpm(['uninstall', packageName], repoRoot);
|
|
87
|
-
|
|
249
|
+
cleanupPumukiTraceDirectories({
|
|
250
|
+
repoRoot,
|
|
251
|
+
traceDirectories: pumukiTraceDirectories,
|
|
252
|
+
});
|
|
88
253
|
|
|
89
254
|
return {
|
|
90
255
|
repoRoot,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.11",
|
|
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": {
|