@planu/cli 4.9.0 → 4.10.0
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/CHANGELOG.md +13 -0
- package/dist/config/license-plans.json +5 -0
- package/dist/config/registries/hosts/codex.json +3 -4
- package/dist/engine/evidence-index/index-builder.js +38 -12
- package/dist/storage/feedback-remote.d.ts +5 -1
- package/dist/storage/feedback-remote.js +75 -16
- package/dist/storage/feedback-store.d.ts +3 -1
- package/dist/storage/feedback-store.js +107 -10
- package/dist/tools/feedback-handler.d.ts +3 -1
- package/dist/tools/feedback-handler.js +47 -1
- package/dist/tools/register-sdd-tools.d.ts +1 -1
- package/dist/tools/register-sdd-tools.js +5 -0
- package/dist/tools/status-handler.js +11 -11
- package/dist/tools/tool-registry/core-tools.js +97 -0
- package/dist/tools/validate-assurance.d.ts +6 -0
- package/dist/tools/validate-assurance.js +55 -0
- package/dist/tools/validate-graph-coverage.d.ts +8 -0
- package/dist/tools/validate-graph-coverage.js +30 -0
- package/dist/tools/validate-helpers.d.ts +9 -0
- package/dist/tools/validate-helpers.js +47 -0
- package/dist/tools/validate-lint.d.ts +3 -0
- package/dist/tools/validate-lint.js +67 -0
- package/dist/tools/validate-minimality.d.ts +16 -0
- package/dist/tools/validate-minimality.js +22 -0
- package/dist/tools/validate-runtime.d.ts +14 -0
- package/dist/tools/validate-runtime.js +85 -0
- package/dist/tools/validate.js +5 -222
- package/dist/types/evidence-index.d.ts +1 -0
- package/dist/types/feedback.d.ts +37 -0
- package/package.json +12 -12
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
|
@@ -37,6 +37,7 @@ import { handleIdeConfig } from '../ide-config-handler.js';
|
|
|
37
37
|
import { IdeTargetEnum, IdeConfigScopeEnum } from '../schemas/ide-config.js';
|
|
38
38
|
import { handleReconcileSpecLiving } from '../reconcile-spec-living-handler.js';
|
|
39
39
|
import { handleSecurityReport } from '../security-report-handler.js';
|
|
40
|
+
import { handleFeedbackStatus, handleResolveFeedback, handleSubmitFeedback, handleSyncFeedback, handleTriageFeedback, } from '../feedback-handler.js';
|
|
40
41
|
import { SecurityReportTimeRangeEnum, SecurityReportFormatEnum, } from '../schemas/runtime-security.js';
|
|
41
42
|
import { RenderSpecForProviderInputSchema, handleRenderSpecForProvider, } from '../render-spec-for-provider.js';
|
|
42
43
|
import { handleTriageRequest, TriageRequestInputSchema } from '../triage-request.js';
|
|
@@ -750,6 +751,102 @@ const coreToolsRegistry = [
|
|
|
750
751
|
wrap: 'tracked',
|
|
751
752
|
group: 'core',
|
|
752
753
|
},
|
|
754
|
+
// ── feedback hub (SPEC-188, SPEC-1100) ───────────────────────────────────
|
|
755
|
+
{
|
|
756
|
+
name: 'submit_feedback',
|
|
757
|
+
description: 'Submit user feedback or a bug report to Planu Feedback Hub. Always persists locally first, then attempts privacy-bounded remote registration with the central feedback intake.',
|
|
758
|
+
schema: {
|
|
759
|
+
type: z
|
|
760
|
+
.enum(['bug', 'feature', 'suggestion', 'dx'])
|
|
761
|
+
.describe('Feedback type: bug | feature | suggestion | dx'),
|
|
762
|
+
title: z.string().min(1).max(200).describe('Short feedback title'),
|
|
763
|
+
description: z.string().min(1).max(5000).describe('Detailed feedback description'),
|
|
764
|
+
context: z
|
|
765
|
+
.object({
|
|
766
|
+
projectPath: z.string().max(4096).optional(),
|
|
767
|
+
currentFile: z.string().max(4096).optional(),
|
|
768
|
+
errorMessage: z.string().max(2000).optional(),
|
|
769
|
+
stack: z.string().max(8000).optional(),
|
|
770
|
+
})
|
|
771
|
+
.optional()
|
|
772
|
+
.describe('Optional local-only context; sensitive path fields are not sent remotely.'),
|
|
773
|
+
},
|
|
774
|
+
handler: async (args) => handleSubmitFeedback(args),
|
|
775
|
+
annotations: {
|
|
776
|
+
title: 'Submit Feedback',
|
|
777
|
+
readOnlyHint: false,
|
|
778
|
+
destructiveHint: false,
|
|
779
|
+
openWorldHint: true,
|
|
780
|
+
},
|
|
781
|
+
wrap: 'tracked',
|
|
782
|
+
group: 'feedback',
|
|
783
|
+
},
|
|
784
|
+
{
|
|
785
|
+
name: 'triage_feedback',
|
|
786
|
+
description: 'Group pending local feedback by similarity so maintainers can decide whether to create specs, merge duplicates, or dismiss items.',
|
|
787
|
+
schema: {
|
|
788
|
+
filter: z
|
|
789
|
+
.enum(['bug', 'feature', 'suggestion', 'dx'])
|
|
790
|
+
.optional()
|
|
791
|
+
.describe('Optional feedback type filter'),
|
|
792
|
+
},
|
|
793
|
+
handler: async (args) => Promise.resolve(handleTriageFeedback(args)),
|
|
794
|
+
annotations: { title: 'Triage Feedback', readOnlyHint: true },
|
|
795
|
+
wrap: 'tracked',
|
|
796
|
+
group: 'feedback',
|
|
797
|
+
},
|
|
798
|
+
{
|
|
799
|
+
name: 'resolve_feedback',
|
|
800
|
+
description: 'Mark a local feedback item as resolved and optionally link it to a Planu spec, PR URL, and resolution notes.',
|
|
801
|
+
schema: {
|
|
802
|
+
feedbackId: z.string().min(1).max(50).describe('Local feedback ID, e.g. FB-001'),
|
|
803
|
+
specId: z.string().max(50).optional().describe('Optional linked Planu spec ID'),
|
|
804
|
+
prUrl: z.url().optional().describe('Optional pull request URL'),
|
|
805
|
+
notes: z.string().max(2000).optional().describe('Optional resolution notes'),
|
|
806
|
+
},
|
|
807
|
+
handler: async (args) => handleResolveFeedback(args),
|
|
808
|
+
annotations: { title: 'Resolve Feedback', readOnlyHint: false, destructiveHint: false },
|
|
809
|
+
wrap: 'tracked',
|
|
810
|
+
group: 'feedback',
|
|
811
|
+
},
|
|
812
|
+
{
|
|
813
|
+
name: 'sync_feedback',
|
|
814
|
+
description: 'Retry remote Supabase registration for local feedback entries whose delivery is pending or failed. Does not accept remote row IDs or perform direct SQL/Data API writes.',
|
|
815
|
+
schema: {
|
|
816
|
+
projectPath: z.string().max(4096).optional().describe('Absolute path to project root'),
|
|
817
|
+
includeSynced: z
|
|
818
|
+
.boolean()
|
|
819
|
+
.optional()
|
|
820
|
+
.describe('When true, also reprocess entries already marked synced. Default false.'),
|
|
821
|
+
limit: z
|
|
822
|
+
.number()
|
|
823
|
+
.int()
|
|
824
|
+
.positive()
|
|
825
|
+
.max(500)
|
|
826
|
+
.optional()
|
|
827
|
+
.describe('Maximum number of local entries to process.'),
|
|
828
|
+
},
|
|
829
|
+
handler: async (args) => handleSyncFeedback(args),
|
|
830
|
+
annotations: {
|
|
831
|
+
title: 'Sync Feedback',
|
|
832
|
+
readOnlyHint: false,
|
|
833
|
+
destructiveHint: false,
|
|
834
|
+
openWorldHint: true,
|
|
835
|
+
},
|
|
836
|
+
wrap: 'tracked',
|
|
837
|
+
group: 'feedback',
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
name: 'feedback_status',
|
|
841
|
+
description: 'Show aggregate local feedback totals and remote delivery health without exposing descriptions, file paths, user identifiers, or secrets.',
|
|
842
|
+
schema: {
|
|
843
|
+
projectPath: z.string().max(4096).optional().describe('Absolute path to project root'),
|
|
844
|
+
},
|
|
845
|
+
handler: async (args) => Promise.resolve(handleFeedbackStatus(args)),
|
|
846
|
+
annotations: { title: 'Feedback Status', readOnlyHint: true },
|
|
847
|
+
wrap: 'tracked',
|
|
848
|
+
group: 'feedback',
|
|
849
|
+
},
|
|
753
850
|
];
|
|
754
851
|
export default coreToolsRegistry;
|
|
755
852
|
//# sourceMappingURL=core-tools.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type NewCodeGateResult } from '../engine/ai-assurance/index.js';
|
|
2
|
+
export declare function runAssuranceGates(projectPath: string): {
|
|
3
|
+
newCode: NewCodeGateResult;
|
|
4
|
+
};
|
|
5
|
+
export declare function checkPlanuUncommittedChanges(projectPath: string): boolean;
|
|
6
|
+
//# sourceMappingURL=validate-assurance.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// tools/validate-assurance.ts — Validate assurance and local git helpers.
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { evaluateNewCodeGate } from '../engine/ai-assurance/index.js';
|
|
5
|
+
export function runAssuranceGates(projectPath) {
|
|
6
|
+
return {
|
|
7
|
+
newCode: evaluateNewCodeGate(readChangedSpecLogicFiles(projectPath)),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function readChangedSpecLogicFiles(projectPath) {
|
|
11
|
+
let changedFiles;
|
|
12
|
+
try {
|
|
13
|
+
const output = execFileSync('git', [
|
|
14
|
+
'-C',
|
|
15
|
+
projectPath,
|
|
16
|
+
'diff',
|
|
17
|
+
'--name-only',
|
|
18
|
+
'HEAD',
|
|
19
|
+
'--',
|
|
20
|
+
'src/tools/create-spec',
|
|
21
|
+
'src/tools/validate.ts',
|
|
22
|
+
'src/engine',
|
|
23
|
+
], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
24
|
+
changedFiles = output
|
|
25
|
+
.split('\n')
|
|
26
|
+
.map((line) => line.trim())
|
|
27
|
+
.filter((line) => line.endsWith('.ts'));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
return changedFiles.flatMap((path) => {
|
|
33
|
+
try {
|
|
34
|
+
return [{ path, content: readFileSync(`${projectPath}/${path}`, 'utf-8') }];
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
export function checkPlanuUncommittedChanges(projectPath) {
|
|
42
|
+
try {
|
|
43
|
+
const output = execFileSync('git', ['status', '--porcelain', 'planu/'], {
|
|
44
|
+
cwd: projectPath,
|
|
45
|
+
encoding: 'utf-8',
|
|
46
|
+
timeout: 5_000,
|
|
47
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
48
|
+
});
|
|
49
|
+
return output.trim().length > 0;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=validate-assurance.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { GraphCoverageReport } from '../types/project-knowledge-graph.js';
|
|
2
|
+
export declare function buildGraphCoverageReport(args: {
|
|
3
|
+
projectId: string;
|
|
4
|
+
projectPath: string;
|
|
5
|
+
specId: string;
|
|
6
|
+
}): Promise<GraphCoverageReport>;
|
|
7
|
+
export declare function formatGraphCoverageText(report: GraphCoverageReport): string;
|
|
8
|
+
//# sourceMappingURL=validate-graph-coverage.d.ts.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// tools/validate-graph-coverage.ts — Graph coverage helpers for validate.
|
|
2
|
+
import { queryProjectGraphSlice } from '../engine/project-graph/index.js';
|
|
3
|
+
function graphCoverageGaps(slice) {
|
|
4
|
+
if (slice === null) {
|
|
5
|
+
return [];
|
|
6
|
+
}
|
|
7
|
+
const criteria = slice.nodes.filter((node) => node.type === 'criterion');
|
|
8
|
+
return criteria
|
|
9
|
+
.filter((criterion) => {
|
|
10
|
+
const outgoing = slice.edges.filter((edge) => edge.from === criterion.id);
|
|
11
|
+
return !outgoing.some((edge) => edge.type === 'implements' || edge.type === 'tests');
|
|
12
|
+
})
|
|
13
|
+
.map((criterion) => criterion.label)
|
|
14
|
+
.slice(0, 10);
|
|
15
|
+
}
|
|
16
|
+
export async function buildGraphCoverageReport(args) {
|
|
17
|
+
const graphSlice = await queryProjectGraphSlice(args).catch(() => null);
|
|
18
|
+
return {
|
|
19
|
+
freshness: graphSlice?.freshness,
|
|
20
|
+
gaps: graphCoverageGaps(graphSlice),
|
|
21
|
+
compactNodes: graphSlice?.nodes.length ?? 0,
|
|
22
|
+
compactEdges: graphSlice?.edges.length ?? 0,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function formatGraphCoverageText(report) {
|
|
26
|
+
return report.gaps.length > 0
|
|
27
|
+
? `\nGRAPH ${String(report.gaps.length)} graph-backed coverage gap(s)`
|
|
28
|
+
: '';
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=validate-graph-coverage.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { InteractiveQuestion } from '../types/clarification.js';
|
|
2
|
+
import type { ToolResult } from '../types/index.js';
|
|
3
|
+
export declare function validateStrictLayoutOrError(args: {
|
|
4
|
+
projectPath: string;
|
|
5
|
+
specId: string;
|
|
6
|
+
specPath?: string;
|
|
7
|
+
}): Promise<ToolResult | null>;
|
|
8
|
+
export declare function buildValidateFailureFallback(failedCount: number): InteractiveQuestion;
|
|
9
|
+
//# sourceMappingURL=validate-helpers.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export async function validateStrictLayoutOrError(args) {
|
|
2
|
+
try {
|
|
3
|
+
const { validateStrictPlanuLayout } = await import('../engine/spec-migrator/index.js');
|
|
4
|
+
const layout = await validateStrictPlanuLayout(args.projectPath, {
|
|
5
|
+
specPath: args.specPath,
|
|
6
|
+
includeRoot: false,
|
|
7
|
+
});
|
|
8
|
+
if (layout.ok) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
content: [
|
|
13
|
+
{
|
|
14
|
+
type: 'text',
|
|
15
|
+
text: `Strict Planu layout validation failed for ${args.specId}.\n\n` +
|
|
16
|
+
`Non-canonical paths:\n${layout.offenders.map((p) => `- ${p}`).join('\n')}\n\n` +
|
|
17
|
+
`Canonical contract:\n${layout.contract}`,
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
isError: true,
|
|
21
|
+
structuredContent: {
|
|
22
|
+
error: 'strict_planu_layout_violation',
|
|
23
|
+
offenders: layout.offenders,
|
|
24
|
+
contract: layout.contract,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function buildValidateFailureFallback(failedCount) {
|
|
33
|
+
return {
|
|
34
|
+
header: 'Failures',
|
|
35
|
+
question: `${failedCount} criteria failed. How do you want to proceed?`,
|
|
36
|
+
multiSelect: false,
|
|
37
|
+
options: [
|
|
38
|
+
{
|
|
39
|
+
label: 're-implement',
|
|
40
|
+
description: 'Fix the failing criteria and run validate again (Recommended)',
|
|
41
|
+
},
|
|
42
|
+
{ label: 'ignore', description: 'Ignore failures and proceed (not recommended)' },
|
|
43
|
+
{ label: 'mark-manual', description: 'Mark as manually reviewed — will bypass auto-check' },
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=validate-helpers.js.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { formatLintFailureDiagnostics, resolveProjectCommandPlan, runProjectCommandPlan, } from './validate-runtime.js';
|
|
2
|
+
/** Allowlist: alphanumerics, spaces, and safe shell chars. Rejects ; | $ ` & < > */
|
|
3
|
+
const SAFE_COMMAND_RE = /^[\w\s./:@~=-]+$/;
|
|
4
|
+
const LINT_CHECK_TIMEOUT_MS = 50_000;
|
|
5
|
+
function isSafeCommand(cmd) {
|
|
6
|
+
return SAFE_COMMAND_RE.test(cmd);
|
|
7
|
+
}
|
|
8
|
+
function isCommandTimeout(err) {
|
|
9
|
+
if (!(err instanceof Error)) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
const errorWithProcessFields = err;
|
|
13
|
+
return (errorWithProcessFields.signal === 'SIGTERM' ||
|
|
14
|
+
errorWithProcessFields.killed === true ||
|
|
15
|
+
errorWithProcessFields.code === 'ETIMEDOUT' ||
|
|
16
|
+
/timed out|timeout/i.test(err.message));
|
|
17
|
+
}
|
|
18
|
+
function commandOutput(err) {
|
|
19
|
+
if (!(err instanceof Error)) {
|
|
20
|
+
return String(err);
|
|
21
|
+
}
|
|
22
|
+
const errorWithOutput = err;
|
|
23
|
+
return [errorWithOutput.stdout, errorWithOutput.stderr]
|
|
24
|
+
.filter((chunk) => chunk !== undefined)
|
|
25
|
+
.map((chunk) => String(chunk))
|
|
26
|
+
.join('\n')
|
|
27
|
+
.trim();
|
|
28
|
+
}
|
|
29
|
+
function parseLintIssueCount(output) {
|
|
30
|
+
const m = /(\d+)\s+problem/i.exec(output);
|
|
31
|
+
if (m) {
|
|
32
|
+
return parseInt(m[1] ?? '0', 10);
|
|
33
|
+
}
|
|
34
|
+
return output.split('\n').filter((l) => l.trim().length > 0).length;
|
|
35
|
+
}
|
|
36
|
+
export function runLintCheck(projectPath, lintCommand) {
|
|
37
|
+
if (lintCommand !== null && !isSafeCommand(lintCommand)) {
|
|
38
|
+
console.warn(`[Planu] validate: lintCommand contains unsafe characters — skipping execution`);
|
|
39
|
+
return {
|
|
40
|
+
passed: true,
|
|
41
|
+
command: lintCommand,
|
|
42
|
+
issueCount: 0,
|
|
43
|
+
output: '(skipped — unsafe command)',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const command = lintCommand ?? 'pnpm lint';
|
|
47
|
+
const plan = resolveProjectCommandPlan(projectPath, command);
|
|
48
|
+
try {
|
|
49
|
+
runProjectCommandPlan(plan, projectPath, LINT_CHECK_TIMEOUT_MS);
|
|
50
|
+
return { passed: true, command, issueCount: 0, output: '' };
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
if (isCommandTimeout(err)) {
|
|
54
|
+
const seconds = Math.round(LINT_CHECK_TIMEOUT_MS / 1000);
|
|
55
|
+
const output = `Lint command timed out after ${String(seconds)}s and was skipped so validate can complete within MCP request limits. ` +
|
|
56
|
+
`Run \`${plan.executedCommand}\` manually before marking the spec done.`;
|
|
57
|
+
console.warn(`[Planu] lintCheck timed out: command="${plan.executedCommand}" cwd="${projectPath}" timeoutMs=${String(LINT_CHECK_TIMEOUT_MS)}`);
|
|
58
|
+
return { passed: true, command, issueCount: 0, output };
|
|
59
|
+
}
|
|
60
|
+
const raw = commandOutput(err);
|
|
61
|
+
const output = formatLintFailureDiagnostics(plan, raw);
|
|
62
|
+
const issueCount = parseLintIssueCount(raw.trim());
|
|
63
|
+
console.warn(`[Planu] lintCheck failed: command="${plan.executedCommand}" cwd="${projectPath}" issues=${String(issueCount)}`);
|
|
64
|
+
return { passed: false, command, issueCount, output };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=validate-lint.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { MinimalImplementationReport } from '../types/minimal-implementation-gate.js';
|
|
2
|
+
export declare function buildMinimalityReport(args: {
|
|
3
|
+
projectPath: string;
|
|
4
|
+
specId: string;
|
|
5
|
+
risk?: string;
|
|
6
|
+
title?: string;
|
|
7
|
+
missing: string[];
|
|
8
|
+
extra: string[];
|
|
9
|
+
qualityIssues: {
|
|
10
|
+
file: string;
|
|
11
|
+
rule: string;
|
|
12
|
+
message: string;
|
|
13
|
+
suggestion?: string;
|
|
14
|
+
}[];
|
|
15
|
+
}): Promise<MinimalImplementationReport | null>;
|
|
16
|
+
//# sourceMappingURL=validate-minimality.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// tools/validate-minimality.ts — Minimal implementation report helper for validate.
|
|
2
|
+
import { analyzeMinimalImplementation, loadMinimalImplementationPolicy, } from '../engine/minimality/index.js';
|
|
3
|
+
export async function buildMinimalityReport(args) {
|
|
4
|
+
try {
|
|
5
|
+
const { policy, evidence } = await loadMinimalImplementationPolicy(args.projectPath);
|
|
6
|
+
return analyzeMinimalImplementation({
|
|
7
|
+
policy,
|
|
8
|
+
specId: args.specId,
|
|
9
|
+
specRisk: args.risk,
|
|
10
|
+
specText: [args.title ?? '', ...args.missing, ...args.extra].join('\n'),
|
|
11
|
+
files: args.qualityIssues.map((issue) => ({
|
|
12
|
+
path: issue.file,
|
|
13
|
+
content: [issue.rule, issue.message, issue.suggestion ?? ''].join('\n'),
|
|
14
|
+
})),
|
|
15
|
+
evidence,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=validate-minimality.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ProjectCommandPlan {
|
|
2
|
+
requestedCommand: string;
|
|
3
|
+
executedCommand: string;
|
|
4
|
+
executable: string;
|
|
5
|
+
args: string[];
|
|
6
|
+
projectPackageManager: string | null;
|
|
7
|
+
source: 'project-package-manager' | 'host-path';
|
|
8
|
+
runtimeNode: string;
|
|
9
|
+
hint?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function resolveProjectCommandPlan(projectPath: string, requestedCommand: string, corepackAvailable?: boolean): ProjectCommandPlan;
|
|
12
|
+
export declare function formatLintFailureDiagnostics(plan: ProjectCommandPlan, output: string): string;
|
|
13
|
+
export declare function runProjectCommandPlan(plan: ProjectCommandPlan, projectPath: string, timeoutMs: number): void;
|
|
14
|
+
//# sourceMappingURL=validate-runtime.d.ts.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// tools/validate-runtime.ts — Project-aware command execution helpers for validate.
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
function readProjectPackageManager(projectPath) {
|
|
5
|
+
try {
|
|
6
|
+
const raw = readFileSync(`${projectPath}/package.json`, 'utf-8');
|
|
7
|
+
const parsed = JSON.parse(raw);
|
|
8
|
+
return typeof parsed.packageManager === 'string' ? parsed.packageManager : null;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function shellTokens(command) {
|
|
15
|
+
return command.split(/\s+/).filter((token) => token.length > 0);
|
|
16
|
+
}
|
|
17
|
+
function canRunCorepack(projectPath) {
|
|
18
|
+
try {
|
|
19
|
+
execFileSync('corepack', ['--version'], {
|
|
20
|
+
cwd: projectPath,
|
|
21
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
22
|
+
timeout: 5_000,
|
|
23
|
+
});
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function resolveProjectCommandPlan(projectPath, requestedCommand, corepackAvailable = canRunCorepack(projectPath)) {
|
|
31
|
+
const projectPackageManager = readProjectPackageManager(projectPath);
|
|
32
|
+
const tokens = shellTokens(requestedCommand);
|
|
33
|
+
const usesPnpm = tokens[0] === 'pnpm';
|
|
34
|
+
const projectPinsPnpm = projectPackageManager?.startsWith('pnpm@') ?? false;
|
|
35
|
+
if (usesPnpm && projectPinsPnpm && corepackAvailable) {
|
|
36
|
+
const args = ['pnpm', ...tokens.slice(1)];
|
|
37
|
+
return {
|
|
38
|
+
requestedCommand,
|
|
39
|
+
executedCommand: ['corepack', ...args].join(' '),
|
|
40
|
+
executable: 'corepack',
|
|
41
|
+
args,
|
|
42
|
+
projectPackageManager,
|
|
43
|
+
source: 'project-package-manager',
|
|
44
|
+
runtimeNode: process.version,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
requestedCommand,
|
|
49
|
+
executedCommand: requestedCommand,
|
|
50
|
+
executable: tokens[0] ?? requestedCommand,
|
|
51
|
+
args: tokens.slice(1),
|
|
52
|
+
projectPackageManager,
|
|
53
|
+
source: 'host-path',
|
|
54
|
+
runtimeNode: process.version,
|
|
55
|
+
...(usesPnpm && projectPinsPnpm
|
|
56
|
+
? {
|
|
57
|
+
hint: 'Project pins pnpm via packageManager but Corepack was unavailable; falling back to host PATH. Enable Corepack or run `corepack pnpm ...` to match the project runtime.',
|
|
58
|
+
}
|
|
59
|
+
: {}),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export function formatLintFailureDiagnostics(plan, output) {
|
|
63
|
+
const lines = [
|
|
64
|
+
output.trim(),
|
|
65
|
+
'',
|
|
66
|
+
'Planu validate runtime diagnostics:',
|
|
67
|
+
`- requestedCommand: ${plan.requestedCommand}`,
|
|
68
|
+
`- executedCommand: ${plan.executedCommand}`,
|
|
69
|
+
`- projectPackageManager: ${plan.projectPackageManager ?? 'unknown'}`,
|
|
70
|
+
`- commandSource: ${plan.source}`,
|
|
71
|
+
`- runtimeNode: ${plan.runtimeNode}`,
|
|
72
|
+
];
|
|
73
|
+
if (plan.hint !== undefined) {
|
|
74
|
+
lines.push(`- hint: ${plan.hint}`);
|
|
75
|
+
}
|
|
76
|
+
return lines.filter((line, index) => index !== 0 || line.length > 0).join('\n');
|
|
77
|
+
}
|
|
78
|
+
export function runProjectCommandPlan(plan, projectPath, timeoutMs) {
|
|
79
|
+
execFileSync(plan.executable, plan.args, {
|
|
80
|
+
cwd: projectPath,
|
|
81
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
82
|
+
timeout: timeoutMs,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=validate-runtime.js.map
|