pumuki 6.3.26 → 6.3.28
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 +3 -1
- package/bin/pumuki-mcp-enterprise-stdio.js +5 -0
- package/bin/pumuki-mcp-evidence-stdio.js +5 -0
- package/core/gate/conditionMatches.ts +1 -21
- package/core/gate/evaluateGate.js +5 -0
- package/core/gate/evaluateRules.js +5 -0
- package/core/gate/evaluateRules.ts +1 -24
- package/core/gate/scopeMatcher.ts +84 -0
- package/docs/EXECUTION_BOARD.md +749 -376
- package/docs/MCP_SERVERS.md +41 -2
- package/docs/README.md +6 -2
- package/docs/REFRACTOR_PROGRESS.md +374 -6
- package/docs/validation/README.md +11 -1
- package/docs/validation/p9-ruralgo-bug-registry.md +607 -0
- package/docs/validation/p9-ruralgo-fork-validation-tracking.md +904 -0
- package/docs/validation/real-repo-manual-e2e-ruralgo-fork.md +372 -0
- package/integrations/config/skillsCompliance.ts +212 -0
- package/integrations/evidence/integrity.ts +352 -0
- package/integrations/evidence/rulesCoverage.ts +94 -0
- package/integrations/evidence/schema.test.ts +16 -0
- package/integrations/evidence/schema.ts +41 -0
- package/integrations/evidence/writeEvidence.test.ts +68 -0
- package/integrations/evidence/writeEvidence.ts +23 -2
- package/integrations/gate/evaluateAiGate.ts +382 -15
- package/integrations/gate/stagePolicies.ts +70 -15
- package/integrations/gate/waivers.ts +209 -0
- package/integrations/git/findingTraceability.ts +3 -23
- package/integrations/git/index.js +5 -0
- package/integrations/git/runCliCommand.ts +16 -0
- package/integrations/git/runPlatformGate.ts +53 -1
- package/integrations/git/runPlatformGateEvaluation.ts +13 -0
- package/integrations/git/stageRunners.ts +168 -5
- package/integrations/lifecycle/adapter.templates.json +72 -5
- package/integrations/lifecycle/adapter.ts +78 -4
- package/integrations/lifecycle/cli.ts +384 -14
- package/integrations/lifecycle/doctor.ts +534 -0
- package/integrations/lifecycle/hookBlock.ts +2 -1
- package/integrations/lifecycle/index.js +5 -0
- package/integrations/lifecycle/install.ts +115 -3
- package/integrations/lifecycle/openSpecBootstrap.ts +68 -8
- package/integrations/lifecycle/preWriteAutomation.ts +142 -0
- package/integrations/mcp/aiGateCheck.ts +6 -0
- package/integrations/mcp/aiGateReceipt.ts +188 -0
- package/integrations/mcp/enterpriseServer.ts +14 -1
- package/integrations/mcp/enterpriseStdioServer.cli.ts +315 -0
- package/integrations/mcp/evidenceStdioServer.cli.ts +342 -0
- package/integrations/mcp/index.js +5 -0
- package/integrations/sdd/index.js +5 -0
- package/integrations/sdd/index.ts +2 -0
- package/integrations/sdd/policy.ts +191 -2
- package/integrations/sdd/sessionStore.ts +139 -19
- package/integrations/sdd/syncDocs.ts +180 -0
- package/integrations/sdd/types.ts +4 -1
- package/integrations/telemetry/structuredTelemetry.ts +197 -0
- package/package.json +27 -8
- package/scripts/build-p9-validation-manifests.ts +53 -0
- package/scripts/check-p9-ruralgo-baseline-clean.ts +200 -0
- package/scripts/check-p9-ruralgo-baseline-versioned.ts +198 -0
- package/scripts/check-p9-ruralgo-branch-ready.ts +215 -0
- package/scripts/check-p9-ruralgo-install-health.ts +288 -0
- package/scripts/check-p9-ruralgo-runtime-ready.ts +188 -0
- package/scripts/check-package-manifest.ts +49 -0
- package/scripts/check-tracking-single-active.sh +40 -0
- package/scripts/framework-menu-consumer-preflight-lib.ts +31 -0
- package/scripts/framework-menu-consumer-runtime-lib.ts +3 -3
- package/scripts/framework-menu-legacy-audit-lib.ts +35 -7
- package/scripts/framework-menu-matrix-evidence-lib.ts +6 -2
- package/scripts/manage-library.sh +1 -1
- package/scripts/p9-ruralgo-baseline-clean-lib.ts +117 -0
- package/scripts/p9-ruralgo-baseline-versioned-lib.ts +119 -0
- package/scripts/p9-ruralgo-branch-ready-lib.ts +128 -0
- package/scripts/p9-ruralgo-install-health-lib.ts +121 -0
- package/scripts/p9-ruralgo-runtime-ready-lib.ts +149 -0
- package/scripts/p9-validation-manifests-lib.ts +366 -0
- package/scripts/package-manifest-lib.ts +9 -0
- package/skills.lock.json +1 -1
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
import {
|
|
6
|
+
evaluateP9RuralgoBaselineVersioned,
|
|
7
|
+
type P9RuralgoBaselineVersionedPolicy,
|
|
8
|
+
} from './p9-ruralgo-baseline-versioned-lib';
|
|
9
|
+
|
|
10
|
+
type CliOptions = {
|
|
11
|
+
repoPath: string;
|
|
12
|
+
json: boolean;
|
|
13
|
+
policy: P9RuralgoBaselineVersionedPolicy;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type GitRun = {
|
|
17
|
+
ok: boolean;
|
|
18
|
+
status: number;
|
|
19
|
+
stdout: string;
|
|
20
|
+
stderr: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const DEFAULT_REPO_PATH =
|
|
24
|
+
'/Users/juancarlosmerlosalbarracin/Developer/Projects/ruralgo-fork';
|
|
25
|
+
|
|
26
|
+
const OPENSPEC_PROJECT = 'openspec/project.md';
|
|
27
|
+
const OPENSPEC_ARCHIVE_GITKEEP = 'openspec/changes/archive/.gitkeep';
|
|
28
|
+
const OPENSPEC_SPECS_GITKEEP = 'openspec/specs/.gitkeep';
|
|
29
|
+
const AI_EVIDENCE_FILE = '.ai_evidence.json';
|
|
30
|
+
|
|
31
|
+
const printHelp = (): void => {
|
|
32
|
+
process.stdout.write(
|
|
33
|
+
[
|
|
34
|
+
'Uso:',
|
|
35
|
+
' node --import tsx scripts/check-p9-ruralgo-baseline-versioned.ts [opciones]',
|
|
36
|
+
'',
|
|
37
|
+
'Opciones:',
|
|
38
|
+
' --repo=<path> Ruta del repo ruralgo-fork (default: ~/Developer/Projects/ruralgo-fork)',
|
|
39
|
+
' --allow-missing Permite ausencia de artifacts baseline',
|
|
40
|
+
' --allow-untracked Permite artifacts sin trackear',
|
|
41
|
+
' --json Salida JSON',
|
|
42
|
+
' --help Muestra ayuda',
|
|
43
|
+
'',
|
|
44
|
+
'Exit code:',
|
|
45
|
+
' 0 => baseline versionado para F1.T4',
|
|
46
|
+
' 1 => baseline no versionado',
|
|
47
|
+
'',
|
|
48
|
+
].join('\n'),
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const parseArgs = (argv: ReadonlyArray<string>): CliOptions => {
|
|
53
|
+
const options: CliOptions = {
|
|
54
|
+
repoPath: DEFAULT_REPO_PATH,
|
|
55
|
+
json: false,
|
|
56
|
+
policy: {
|
|
57
|
+
requireExists: true,
|
|
58
|
+
requireTracked: true,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
for (const arg of argv) {
|
|
63
|
+
if (arg === '--json') {
|
|
64
|
+
options.json = true;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (arg === '--allow-missing') {
|
|
68
|
+
options.policy.requireExists = false;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (arg === '--allow-untracked') {
|
|
72
|
+
options.policy.requireTracked = false;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (arg.startsWith('--repo=')) {
|
|
76
|
+
options.repoPath = arg.slice('--repo='.length);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
throw new Error(`Argumento no soportado: ${arg}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return options;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const runGit = (repoPath: string, args: ReadonlyArray<string>): GitRun => {
|
|
86
|
+
try {
|
|
87
|
+
const stdout = execFileSync('git', ['-C', repoPath, ...args], {
|
|
88
|
+
encoding: 'utf8',
|
|
89
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
90
|
+
}).trim();
|
|
91
|
+
return { ok: true, status: 0, stdout, stderr: '' };
|
|
92
|
+
} catch (error) {
|
|
93
|
+
const err = error as {
|
|
94
|
+
status?: number;
|
|
95
|
+
stdout?: string | Buffer;
|
|
96
|
+
stderr?: string | Buffer;
|
|
97
|
+
};
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
status: typeof err.status === 'number' ? err.status : 1,
|
|
101
|
+
stdout: String(err.stdout ?? '').trim(),
|
|
102
|
+
stderr: String(err.stderr ?? '').trim(),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const isTrackedByGit = (repoPath: string, relativePath: string): boolean =>
|
|
108
|
+
runGit(repoPath, ['ls-files', '--error-unmatch', '--', relativePath]).ok;
|
|
109
|
+
|
|
110
|
+
const main = (): number => {
|
|
111
|
+
const argv = process.argv.slice(2);
|
|
112
|
+
if (argv.includes('--help')) {
|
|
113
|
+
printHelp();
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let options: CliOptions;
|
|
118
|
+
try {
|
|
119
|
+
options = parseArgs(argv);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
process.stderr.write(`[p9][baseline-versioned] ${(error as Error).message}\n`);
|
|
122
|
+
process.stderr.write('Usa --help para ver opciones.\n');
|
|
123
|
+
return 1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const isGitRepo = runGit(options.repoPath, ['rev-parse', '--is-inside-work-tree']).stdout === 'true';
|
|
127
|
+
|
|
128
|
+
const openspecProjectExists = existsSync(join(options.repoPath, OPENSPEC_PROJECT));
|
|
129
|
+
const openspecArchiveGitkeepExists = existsSync(join(options.repoPath, OPENSPEC_ARCHIVE_GITKEEP));
|
|
130
|
+
const openspecSpecsGitkeepExists = existsSync(join(options.repoPath, OPENSPEC_SPECS_GITKEEP));
|
|
131
|
+
const aiEvidenceExists = existsSync(join(options.repoPath, AI_EVIDENCE_FILE));
|
|
132
|
+
|
|
133
|
+
const result = evaluateP9RuralgoBaselineVersioned(
|
|
134
|
+
{
|
|
135
|
+
isGitRepo,
|
|
136
|
+
openspecProjectExists,
|
|
137
|
+
openspecArchiveGitkeepExists,
|
|
138
|
+
openspecSpecsGitkeepExists,
|
|
139
|
+
aiEvidenceExists,
|
|
140
|
+
openspecProjectTracked: isGitRepo ? isTrackedByGit(options.repoPath, OPENSPEC_PROJECT) : false,
|
|
141
|
+
openspecArchiveGitkeepTracked: isGitRepo
|
|
142
|
+
? isTrackedByGit(options.repoPath, OPENSPEC_ARCHIVE_GITKEEP)
|
|
143
|
+
: false,
|
|
144
|
+
openspecSpecsGitkeepTracked: isGitRepo
|
|
145
|
+
? isTrackedByGit(options.repoPath, OPENSPEC_SPECS_GITKEEP)
|
|
146
|
+
: false,
|
|
147
|
+
aiEvidenceTracked: isGitRepo ? isTrackedByGit(options.repoPath, AI_EVIDENCE_FILE) : false,
|
|
148
|
+
},
|
|
149
|
+
options.policy,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const payload = {
|
|
153
|
+
ready: result.ready,
|
|
154
|
+
repoPath: options.repoPath,
|
|
155
|
+
policy: options.policy,
|
|
156
|
+
snapshot: {
|
|
157
|
+
isGitRepo,
|
|
158
|
+
openspecProjectExists,
|
|
159
|
+
openspecArchiveGitkeepExists,
|
|
160
|
+
openspecSpecsGitkeepExists,
|
|
161
|
+
aiEvidenceExists,
|
|
162
|
+
openspecProjectTracked: isGitRepo ? isTrackedByGit(options.repoPath, OPENSPEC_PROJECT) : false,
|
|
163
|
+
openspecArchiveGitkeepTracked: isGitRepo
|
|
164
|
+
? isTrackedByGit(options.repoPath, OPENSPEC_ARCHIVE_GITKEEP)
|
|
165
|
+
: false,
|
|
166
|
+
openspecSpecsGitkeepTracked: isGitRepo
|
|
167
|
+
? isTrackedByGit(options.repoPath, OPENSPEC_SPECS_GITKEEP)
|
|
168
|
+
: false,
|
|
169
|
+
aiEvidenceTracked: isGitRepo ? isTrackedByGit(options.repoPath, AI_EVIDENCE_FILE) : false,
|
|
170
|
+
},
|
|
171
|
+
issues: result.issues,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
if (options.json) {
|
|
175
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
176
|
+
} else {
|
|
177
|
+
process.stdout.write('[p9][baseline-versioned] PRECHECK\n');
|
|
178
|
+
process.stdout.write(`[p9][baseline-versioned] repo=${payload.repoPath}\n`);
|
|
179
|
+
process.stdout.write(
|
|
180
|
+
`[p9][baseline-versioned] openspec_exists(project/archive/specs)=${payload.snapshot.openspecProjectExists}/${payload.snapshot.openspecArchiveGitkeepExists}/${payload.snapshot.openspecSpecsGitkeepExists}\n`,
|
|
181
|
+
);
|
|
182
|
+
process.stdout.write(
|
|
183
|
+
`[p9][baseline-versioned] tracked(project/archive/specs/ai_evidence)=${payload.snapshot.openspecProjectTracked}/${payload.snapshot.openspecArchiveGitkeepTracked}/${payload.snapshot.openspecSpecsGitkeepTracked}/${payload.snapshot.aiEvidenceTracked}\n`,
|
|
184
|
+
);
|
|
185
|
+
process.stdout.write(
|
|
186
|
+
`[p9][baseline-versioned] status=${payload.ready ? 'READY' : 'NOT_READY'} issues=${payload.issues.length}\n`,
|
|
187
|
+
);
|
|
188
|
+
for (const issue of payload.issues) {
|
|
189
|
+
process.stdout.write(
|
|
190
|
+
`[p9][baseline-versioned][${issue.severity}] ${issue.code}: ${issue.message}\n`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return payload.ready ? 0 : 1;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
process.exitCode = main();
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import {
|
|
4
|
+
evaluateP9RuralgoBranchReadiness,
|
|
5
|
+
type P9RuralgoBranchReadinessPolicy,
|
|
6
|
+
} from './p9-ruralgo-branch-ready-lib';
|
|
7
|
+
|
|
8
|
+
type CliOptions = {
|
|
9
|
+
repoPath: string;
|
|
10
|
+
expectedBranch: string;
|
|
11
|
+
baseRef: string;
|
|
12
|
+
json: boolean;
|
|
13
|
+
policy: P9RuralgoBranchReadinessPolicy;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type GitRun = {
|
|
17
|
+
ok: boolean;
|
|
18
|
+
status: number;
|
|
19
|
+
stdout: string;
|
|
20
|
+
stderr: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const DEFAULT_REPO_PATH =
|
|
24
|
+
'/Users/juancarlosmerlosalbarracin/Developer/Projects/ruralgo-fork';
|
|
25
|
+
|
|
26
|
+
const printHelp = (): void => {
|
|
27
|
+
process.stdout.write(
|
|
28
|
+
[
|
|
29
|
+
'Uso:',
|
|
30
|
+
' node --import tsx scripts/check-p9-ruralgo-branch-ready.ts [opciones]',
|
|
31
|
+
'',
|
|
32
|
+
'Opciones:',
|
|
33
|
+
' --repo=<path> Ruta del repo ruralgo-fork (default: ~/Developer/Projects/ruralgo-fork)',
|
|
34
|
+
' --expected-branch=<name> Rama esperada (default: feature/p9-api-contact-contract)',
|
|
35
|
+
' --base-ref=<git-ref> Base esperada (default: origin/develop)',
|
|
36
|
+
' --allow-upstream Permite upstream ya configurado',
|
|
37
|
+
' --allow-dirty Permite worktree con cambios',
|
|
38
|
+
' --allow-nonfeature-name Permite nombres fuera de feature/<kebab-case>',
|
|
39
|
+
' --json Salida JSON',
|
|
40
|
+
' --help Muestra ayuda',
|
|
41
|
+
'',
|
|
42
|
+
'Exit code:',
|
|
43
|
+
' 0 => rama lista para P9.F0.T3.ST2',
|
|
44
|
+
' 1 => no lista (revisar issues)',
|
|
45
|
+
'',
|
|
46
|
+
].join('\n'),
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const parseArgs = (argv: ReadonlyArray<string>): CliOptions => {
|
|
51
|
+
const options: CliOptions = {
|
|
52
|
+
repoPath: DEFAULT_REPO_PATH,
|
|
53
|
+
expectedBranch: 'feature/p9-api-contact-contract',
|
|
54
|
+
baseRef: 'origin/develop',
|
|
55
|
+
json: false,
|
|
56
|
+
policy: {
|
|
57
|
+
requireNoUpstream: true,
|
|
58
|
+
requireCleanWorktree: true,
|
|
59
|
+
requireFeatureNaming: true,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
for (const arg of argv) {
|
|
64
|
+
if (arg === '--json') {
|
|
65
|
+
options.json = true;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (arg === '--allow-upstream') {
|
|
69
|
+
options.policy.requireNoUpstream = false;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (arg === '--allow-dirty') {
|
|
73
|
+
options.policy.requireCleanWorktree = false;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (arg === '--allow-nonfeature-name') {
|
|
77
|
+
options.policy.requireFeatureNaming = false;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (arg.startsWith('--repo=')) {
|
|
81
|
+
options.repoPath = arg.slice('--repo='.length);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (arg.startsWith('--expected-branch=')) {
|
|
85
|
+
options.expectedBranch = arg.slice('--expected-branch='.length);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (arg.startsWith('--base-ref=')) {
|
|
89
|
+
options.baseRef = arg.slice('--base-ref='.length);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
throw new Error(`Argumento no soportado: ${arg}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return options;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const runGit = (repoPath: string, args: ReadonlyArray<string>): GitRun => {
|
|
99
|
+
try {
|
|
100
|
+
const stdout = execFileSync('git', ['-C', repoPath, ...args], {
|
|
101
|
+
encoding: 'utf8',
|
|
102
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
103
|
+
}).trim();
|
|
104
|
+
return { ok: true, status: 0, stdout, stderr: '' };
|
|
105
|
+
} catch (error) {
|
|
106
|
+
const err = error as {
|
|
107
|
+
status?: number;
|
|
108
|
+
stdout?: string | Buffer;
|
|
109
|
+
stderr?: string | Buffer;
|
|
110
|
+
};
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
status: typeof err.status === 'number' ? err.status : 1,
|
|
114
|
+
stdout: String(err.stdout ?? '').trim(),
|
|
115
|
+
stderr: String(err.stderr ?? '').trim(),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const main = (): number => {
|
|
121
|
+
const argv = process.argv.slice(2);
|
|
122
|
+
if (argv.includes('--help')) {
|
|
123
|
+
printHelp();
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let options: CliOptions;
|
|
128
|
+
try {
|
|
129
|
+
options = parseArgs(argv);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
process.stderr.write(`[p9][branch-ready] ${(error as Error).message}\n`);
|
|
132
|
+
process.stderr.write('Usa --help para ver opciones.\n');
|
|
133
|
+
return 1;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const isGitRepoCheck = runGit(options.repoPath, ['rev-parse', '--is-inside-work-tree']);
|
|
137
|
+
const isGitRepo = isGitRepoCheck.ok && isGitRepoCheck.stdout === 'true';
|
|
138
|
+
const currentBranch = isGitRepo
|
|
139
|
+
? runGit(options.repoPath, ['rev-parse', '--abbrev-ref', 'HEAD']).stdout || null
|
|
140
|
+
: null;
|
|
141
|
+
|
|
142
|
+
const upstreamCheck = isGitRepo
|
|
143
|
+
? runGit(
|
|
144
|
+
options.repoPath,
|
|
145
|
+
['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'],
|
|
146
|
+
)
|
|
147
|
+
: { ok: false, status: 1, stdout: '', stderr: '' };
|
|
148
|
+
const upstreamRef = upstreamCheck.ok ? upstreamCheck.stdout : null;
|
|
149
|
+
|
|
150
|
+
const statusCheck = isGitRepo ? runGit(options.repoPath, ['status', '--porcelain']) : null;
|
|
151
|
+
const worktreeDirty = Boolean(statusCheck?.stdout);
|
|
152
|
+
|
|
153
|
+
const baseCheck = isGitRepo
|
|
154
|
+
? runGit(options.repoPath, ['merge-base', '--is-ancestor', options.baseRef, 'HEAD'])
|
|
155
|
+
: { ok: false, status: 2, stdout: '', stderr: '' };
|
|
156
|
+
const baseReachable =
|
|
157
|
+
baseCheck.status === 0 ? true : baseCheck.status === 1 ? false : null;
|
|
158
|
+
|
|
159
|
+
const result = evaluateP9RuralgoBranchReadiness(
|
|
160
|
+
{
|
|
161
|
+
repoPath: options.repoPath,
|
|
162
|
+
expectedBranch: options.expectedBranch,
|
|
163
|
+
currentBranch,
|
|
164
|
+
upstreamRef,
|
|
165
|
+
baseRef: options.baseRef,
|
|
166
|
+
baseReachable,
|
|
167
|
+
worktreeDirty,
|
|
168
|
+
isGitRepo,
|
|
169
|
+
},
|
|
170
|
+
options.policy,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const payload = {
|
|
174
|
+
ready: result.ready,
|
|
175
|
+
repoPath: options.repoPath,
|
|
176
|
+
expectedBranch: options.expectedBranch,
|
|
177
|
+
currentBranch,
|
|
178
|
+
upstreamRef,
|
|
179
|
+
baseRef: options.baseRef,
|
|
180
|
+
baseReachable,
|
|
181
|
+
worktreeDirty,
|
|
182
|
+
policy: options.policy,
|
|
183
|
+
issues: result.issues,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
if (options.json) {
|
|
187
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
188
|
+
} else {
|
|
189
|
+
process.stdout.write('[p9][branch-ready] PRECHECK\n');
|
|
190
|
+
process.stdout.write(`[p9][branch-ready] repo=${payload.repoPath}\n`);
|
|
191
|
+
process.stdout.write(`[p9][branch-ready] expected_branch=${payload.expectedBranch}\n`);
|
|
192
|
+
process.stdout.write(`[p9][branch-ready] current_branch=${payload.currentBranch ?? 'n/a'}\n`);
|
|
193
|
+
process.stdout.write(`[p9][branch-ready] upstream=${payload.upstreamRef ?? 'none'}\n`);
|
|
194
|
+
process.stdout.write(`[p9][branch-ready] base_ref=${payload.baseRef}\n`);
|
|
195
|
+
process.stdout.write(
|
|
196
|
+
`[p9][branch-ready] base_reachable=${String(payload.baseReachable)} worktree_dirty=${String(
|
|
197
|
+
payload.worktreeDirty,
|
|
198
|
+
)}\n`,
|
|
199
|
+
);
|
|
200
|
+
process.stdout.write(
|
|
201
|
+
`[p9][branch-ready] status=${payload.ready ? 'READY' : 'NOT_READY'} issues=${
|
|
202
|
+
payload.issues.length
|
|
203
|
+
}\n`,
|
|
204
|
+
);
|
|
205
|
+
for (const issue of payload.issues) {
|
|
206
|
+
process.stdout.write(
|
|
207
|
+
`[p9][branch-ready][${issue.severity}] ${issue.code}: ${issue.message}\n`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return payload.ready ? 0 : 1;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
process.exitCode = main();
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
import {
|
|
6
|
+
evaluateP9RuralgoInstallHealth,
|
|
7
|
+
type P9RuralgoInstallHealthPolicy,
|
|
8
|
+
} from './p9-ruralgo-install-health-lib';
|
|
9
|
+
|
|
10
|
+
type CliOptions = {
|
|
11
|
+
repoPath: string;
|
|
12
|
+
json: boolean;
|
|
13
|
+
policy: P9RuralgoInstallHealthPolicy;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type CommandResult = {
|
|
17
|
+
ok: boolean;
|
|
18
|
+
status: number;
|
|
19
|
+
stdout: string;
|
|
20
|
+
stderr: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type PumukiStatusPayload = {
|
|
24
|
+
lifecycleState?: {
|
|
25
|
+
installed?: string | boolean;
|
|
26
|
+
};
|
|
27
|
+
hookStatus?: {
|
|
28
|
+
'pre-commit'?: { exists?: boolean; managedBlockPresent?: boolean };
|
|
29
|
+
'pre-push'?: { exists?: boolean; managedBlockPresent?: boolean };
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const DEFAULT_REPO_PATH =
|
|
34
|
+
'/Users/juancarlosmerlosalbarracin/Developer/Projects/ruralgo-fork';
|
|
35
|
+
|
|
36
|
+
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
|
37
|
+
const localPumukiCliPath = resolve(scriptDir, '../bin/pumuki.js');
|
|
38
|
+
|
|
39
|
+
const printHelp = (): void => {
|
|
40
|
+
process.stdout.write(
|
|
41
|
+
[
|
|
42
|
+
'Uso:',
|
|
43
|
+
' node --import tsx scripts/check-p9-ruralgo-install-health.ts [opciones]',
|
|
44
|
+
'',
|
|
45
|
+
'Opciones:',
|
|
46
|
+
' --repo=<path> Ruta del repo ruralgo-fork (default: ~/Developer/Projects/ruralgo-fork)',
|
|
47
|
+
' --allow-not-installed Permite lifecycle no instalado',
|
|
48
|
+
' --allow-unmanaged-hooks Permite hooks no gestionados',
|
|
49
|
+
' --allow-doctor-fail No exige doctor PASS',
|
|
50
|
+
' --json Salida JSON',
|
|
51
|
+
' --help Muestra ayuda',
|
|
52
|
+
'',
|
|
53
|
+
'Exit code:',
|
|
54
|
+
' 0 => instalación saludable para F1.T3',
|
|
55
|
+
' 1 => instalación no saludable',
|
|
56
|
+
'',
|
|
57
|
+
].join('\n'),
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const parseArgs = (argv: ReadonlyArray<string>): CliOptions => {
|
|
62
|
+
const options: CliOptions = {
|
|
63
|
+
repoPath: DEFAULT_REPO_PATH,
|
|
64
|
+
json: false,
|
|
65
|
+
policy: {
|
|
66
|
+
requireInstalled: true,
|
|
67
|
+
requireManagedHooks: true,
|
|
68
|
+
requireDoctorPass: true,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
for (const arg of argv) {
|
|
73
|
+
if (arg === '--json') {
|
|
74
|
+
options.json = true;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (arg === '--allow-not-installed') {
|
|
78
|
+
options.policy.requireInstalled = false;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (arg === '--allow-unmanaged-hooks') {
|
|
82
|
+
options.policy.requireManagedHooks = false;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (arg === '--allow-doctor-fail') {
|
|
86
|
+
options.policy.requireDoctorPass = false;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (arg.startsWith('--repo=')) {
|
|
90
|
+
options.repoPath = arg.slice('--repo='.length);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
throw new Error(`Argumento no soportado: ${arg}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return options;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const runCommand = (
|
|
100
|
+
command: string,
|
|
101
|
+
args: ReadonlyArray<string>,
|
|
102
|
+
cwd: string,
|
|
103
|
+
): CommandResult => {
|
|
104
|
+
try {
|
|
105
|
+
const stdout = execFileSync(command, args, {
|
|
106
|
+
cwd,
|
|
107
|
+
encoding: 'utf8',
|
|
108
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
109
|
+
});
|
|
110
|
+
return {
|
|
111
|
+
ok: true,
|
|
112
|
+
status: 0,
|
|
113
|
+
stdout: stdout.trim(),
|
|
114
|
+
stderr: '',
|
|
115
|
+
};
|
|
116
|
+
} catch (error) {
|
|
117
|
+
const err = error as {
|
|
118
|
+
status?: number;
|
|
119
|
+
stdout?: string | Buffer;
|
|
120
|
+
stderr?: string | Buffer;
|
|
121
|
+
};
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
status: typeof err.status === 'number' ? err.status : 1,
|
|
125
|
+
stdout: String(err.stdout ?? '').trim(),
|
|
126
|
+
stderr: String(err.stderr ?? '').trim(),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const parseLifecycleInstalled = (value: string | boolean | undefined): boolean | null => {
|
|
132
|
+
if (typeof value === 'boolean') {
|
|
133
|
+
return value;
|
|
134
|
+
}
|
|
135
|
+
if (typeof value === 'string') {
|
|
136
|
+
if (value === 'true') {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
if (value === 'false') {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const parseDoctorVerdictPass = (stdout: string): boolean | null => {
|
|
147
|
+
if (!stdout) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
const maybeJson = stdout.trim();
|
|
151
|
+
if (maybeJson.startsWith('{')) {
|
|
152
|
+
try {
|
|
153
|
+
const parsed = JSON.parse(maybeJson) as { verdict?: string; issues?: unknown[] };
|
|
154
|
+
if (typeof parsed.verdict === 'string') {
|
|
155
|
+
return parsed.verdict.toUpperCase() === 'PASS';
|
|
156
|
+
}
|
|
157
|
+
if (Array.isArray(parsed.issues)) {
|
|
158
|
+
return parsed.issues.length === 0;
|
|
159
|
+
}
|
|
160
|
+
} catch {
|
|
161
|
+
// fallback to regex text parsing
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const match = stdout.match(/doctor verdict:\s*([A-Z]+)/i);
|
|
165
|
+
if (!match) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
return match[1]?.toUpperCase() === 'PASS';
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const main = (): number => {
|
|
172
|
+
const argv = process.argv.slice(2);
|
|
173
|
+
if (argv.includes('--help')) {
|
|
174
|
+
printHelp();
|
|
175
|
+
return 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let options: CliOptions;
|
|
179
|
+
try {
|
|
180
|
+
options = parseArgs(argv);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
process.stderr.write(`[p9][install-health] ${(error as Error).message}\n`);
|
|
183
|
+
process.stderr.write('Usa --help para ver opciones.\n');
|
|
184
|
+
return 1;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const isGitRepoCheck = runCommand('git', ['rev-parse', '--is-inside-work-tree'], options.repoPath);
|
|
188
|
+
const isGitRepo = isGitRepoCheck.ok && isGitRepoCheck.stdout === 'true';
|
|
189
|
+
|
|
190
|
+
const statusCommand = isGitRepo
|
|
191
|
+
? runCommand('node', [localPumukiCliPath, 'status', '--json'], options.repoPath)
|
|
192
|
+
: { ok: false, status: 1, stdout: '', stderr: '' };
|
|
193
|
+
|
|
194
|
+
let statusJsonValid = false;
|
|
195
|
+
let statusPayload: PumukiStatusPayload = {};
|
|
196
|
+
if (statusCommand.ok) {
|
|
197
|
+
try {
|
|
198
|
+
statusPayload = JSON.parse(statusCommand.stdout) as PumukiStatusPayload;
|
|
199
|
+
statusJsonValid = true;
|
|
200
|
+
} catch {
|
|
201
|
+
statusJsonValid = false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const doctorCommand = isGitRepo
|
|
206
|
+
? runCommand('node', [localPumukiCliPath, 'doctor', '--json'], options.repoPath)
|
|
207
|
+
: { ok: false, status: 1, stdout: '', stderr: '' };
|
|
208
|
+
|
|
209
|
+
const lifecycleInstalled = statusJsonValid
|
|
210
|
+
? parseLifecycleInstalled(statusPayload.lifecycleState?.installed)
|
|
211
|
+
: null;
|
|
212
|
+
const preCommitManaged = statusJsonValid
|
|
213
|
+
? statusPayload.hookStatus?.['pre-commit']?.managedBlockPresent === true
|
|
214
|
+
: null;
|
|
215
|
+
const prePushManaged = statusJsonValid
|
|
216
|
+
? statusPayload.hookStatus?.['pre-push']?.managedBlockPresent === true
|
|
217
|
+
: null;
|
|
218
|
+
const doctorVerdictPass = doctorCommand.ok
|
|
219
|
+
? parseDoctorVerdictPass(doctorCommand.stdout)
|
|
220
|
+
: null;
|
|
221
|
+
|
|
222
|
+
const result = evaluateP9RuralgoInstallHealth(
|
|
223
|
+
{
|
|
224
|
+
isGitRepo,
|
|
225
|
+
statusCommandOk: statusCommand.ok,
|
|
226
|
+
statusJsonValid,
|
|
227
|
+
lifecycleInstalled,
|
|
228
|
+
preCommitManaged,
|
|
229
|
+
prePushManaged,
|
|
230
|
+
doctorCommandOk: doctorCommand.ok,
|
|
231
|
+
doctorVerdictPass,
|
|
232
|
+
},
|
|
233
|
+
options.policy,
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const payload = {
|
|
237
|
+
ready: result.ready,
|
|
238
|
+
repoPath: options.repoPath,
|
|
239
|
+
policy: options.policy,
|
|
240
|
+
snapshot: {
|
|
241
|
+
isGitRepo,
|
|
242
|
+
statusCommandOk: statusCommand.ok,
|
|
243
|
+
statusJsonValid,
|
|
244
|
+
lifecycleInstalled,
|
|
245
|
+
preCommitManaged,
|
|
246
|
+
prePushManaged,
|
|
247
|
+
doctorCommandOk: doctorCommand.ok,
|
|
248
|
+
doctorVerdictPass,
|
|
249
|
+
},
|
|
250
|
+
raw: {
|
|
251
|
+
status: {
|
|
252
|
+
exitCode: statusCommand.status,
|
|
253
|
+
stdout: statusCommand.stdout,
|
|
254
|
+
stderr: statusCommand.stderr,
|
|
255
|
+
},
|
|
256
|
+
doctor: {
|
|
257
|
+
exitCode: doctorCommand.status,
|
|
258
|
+
stdout: doctorCommand.stdout,
|
|
259
|
+
stderr: doctorCommand.stderr,
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
issues: result.issues,
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
if (options.json) {
|
|
266
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
267
|
+
} else {
|
|
268
|
+
process.stdout.write('[p9][install-health] PRECHECK\n');
|
|
269
|
+
process.stdout.write(`[p9][install-health] repo=${payload.repoPath}\n`);
|
|
270
|
+
process.stdout.write(
|
|
271
|
+
`[p9][install-health] lifecycle_installed=${String(payload.snapshot.lifecycleInstalled)} managed_hooks(pre-commit/pre-push)=${String(
|
|
272
|
+
payload.snapshot.preCommitManaged,
|
|
273
|
+
)}/${String(payload.snapshot.prePushManaged)}\n`,
|
|
274
|
+
);
|
|
275
|
+
process.stdout.write(
|
|
276
|
+
`[p9][install-health] doctor_pass=${String(payload.snapshot.doctorVerdictPass)} status=${payload.ready ? 'READY' : 'NOT_READY'} issues=${payload.issues.length}\n`,
|
|
277
|
+
);
|
|
278
|
+
for (const issue of payload.issues) {
|
|
279
|
+
process.stdout.write(
|
|
280
|
+
`[p9][install-health][${issue.severity}] ${issue.code}: ${issue.message}\n`,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return payload.ready ? 0 : 1;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
process.exitCode = main();
|