@dailephd/my-dev-kit-lab 0.2.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/README.md +272 -0
- package/benchmarks/contracts/benchmark-project-profiles.json +1199 -0
- package/benchmarks/contracts/todo-behavior.md +70 -0
- package/benchmarks/contracts/todo-benchmark-case.json +227 -0
- package/benchmarks/projects/README.md +34 -0
- package/benchmarks/projects/task-analytics-large-mixed/README.md +1 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/task_analytics/__init__.py +3 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/task_analytics/fixtures.py +6 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/task_analytics/metrics.py +29 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/task_analytics/models.py +21 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/task_analytics/parser.py +16 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/task_analytics/pipeline.py +9 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/task_analytics/quality.py +8 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/task_analytics/reporting.py +11 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/tests/test_metrics.py +19 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/tests/test_parser.py +15 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/tests/test_quality.py +19 -0
- package/benchmarks/projects/task-analytics-large-mixed/py/tests/test_reporting.py +15 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/package.json +12 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/index.ts +11 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/models/analyticsSnapshot.ts +20 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/models/project.ts +5 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/models/task.ts +10 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/reporting/buildProjectLeaderboard.ts +7 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/reporting/formatTaskHealthReport.ts +13 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/services/buildAnalyticsSnapshot.ts +39 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/services/completeTask.ts +10 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/services/createTask.ts +21 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/services/listTasksByProject.ts +6 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/store/projectStore.ts +20 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/store/taskStore.ts +44 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/validation/projectValidation.ts +12 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/src/validation/taskValidation.ts +18 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/tests/buildAnalyticsSnapshot.test.ts +48 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/tests/completeTask.test.ts +21 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/tests/createTask.test.ts +31 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/tests/listTasksByProject.test.ts +18 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/tests/reporting.test.ts +19 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/tsconfig.json +12 -0
- package/benchmarks/projects/task-analytics-large-mixed/ts/vitest.config.ts +5 -0
- package/benchmarks/projects/task-workflow-medium-ts/README.md +1 -0
- package/benchmarks/projects/task-workflow-medium-ts/package.json +12 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/index.ts +9 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/models/project.ts +6 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/models/task.ts +39 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/services/completeTask.ts +15 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/services/createTask.ts +26 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/services/filterTasks.ts +17 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/services/importTasks.ts +33 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/services/summarizeTasks.ts +30 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/store/taskStore.ts +76 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/utils/deterministicId.ts +3 -0
- package/benchmarks/projects/task-workflow-medium-ts/src/validation/taskValidation.ts +45 -0
- package/benchmarks/projects/task-workflow-medium-ts/tests/completeTask.test.ts +16 -0
- package/benchmarks/projects/task-workflow-medium-ts/tests/createTask.test.ts +21 -0
- package/benchmarks/projects/task-workflow-medium-ts/tests/filterTasks.test.ts +18 -0
- package/benchmarks/projects/task-workflow-medium-ts/tests/importTasks.test.ts +22 -0
- package/benchmarks/projects/task-workflow-medium-ts/tests/summarizeTasks.test.ts +29 -0
- package/benchmarks/projects/task-workflow-medium-ts/tsconfig.json +12 -0
- package/benchmarks/projects/task-workflow-medium-ts/vitest.config.ts +5 -0
- package/benchmarks/projects/todo-js/README.md +3 -0
- package/benchmarks/projects/todo-js/package.json +11 -0
- package/benchmarks/projects/todo-js/src/index.js +2 -0
- package/benchmarks/projects/todo-js/src/taskService.js +37 -0
- package/benchmarks/projects/todo-js/src/taskStore.js +28 -0
- package/benchmarks/projects/todo-js/tests/taskService.test.js +45 -0
- package/benchmarks/projects/todo-js/vitest.config.js +5 -0
- package/benchmarks/projects/todo-mixed-ts-py/README.md +3 -0
- package/benchmarks/projects/todo-mixed-ts-py/package.json +13 -0
- package/benchmarks/projects/todo-mixed-ts-py/python/task_service.py +76 -0
- package/benchmarks/projects/todo-mixed-ts-py/src/taskCli.ts +38 -0
- package/benchmarks/projects/todo-mixed-ts-py/tests/mixedBoundary.test.ts +18 -0
- package/benchmarks/projects/todo-mixed-ts-py/tsconfig.json +12 -0
- package/benchmarks/projects/todo-mixed-ts-py/vitest.config.ts +5 -0
- package/benchmarks/projects/todo-python/README.md +3 -0
- package/benchmarks/projects/todo-python/src/__init__.py +4 -0
- package/benchmarks/projects/todo-python/src/task_service.py +32 -0
- package/benchmarks/projects/todo-python/src/task_store.py +28 -0
- package/benchmarks/projects/todo-python/tests/test_task_service.py +52 -0
- package/benchmarks/projects/todo-ts/README.md +3 -0
- package/benchmarks/projects/todo-ts/package.json +12 -0
- package/benchmarks/projects/todo-ts/src/index.ts +2 -0
- package/benchmarks/projects/todo-ts/src/taskService.ts +41 -0
- package/benchmarks/projects/todo-ts/src/taskStore.ts +34 -0
- package/benchmarks/projects/todo-ts/tests/taskService.test.ts +45 -0
- package/benchmarks/projects/todo-ts/tsconfig.json +12 -0
- package/benchmarks/projects/todo-ts/vitest.config.ts +5 -0
- package/dist/scripts/build-gallery.js +3 -0
- package/dist/scripts/capture-demo-report.js +3 -0
- package/dist/scripts/evaluate-token-savings.js +2 -0
- package/dist/scripts/experiments/describeExperiment.js +143 -0
- package/dist/scripts/experiments/listExperiments.js +44 -0
- package/dist/scripts/experiments/runExperiment.js +199 -0
- package/dist/scripts/generate-experiment-plots.js +3 -0
- package/dist/scripts/generate-prompt-variants.js +2 -0
- package/dist/scripts/render-experiment-report.js +2 -0
- package/dist/scripts/run-agent-prompt.js +2 -0
- package/dist/scripts/run-controlled-experiment.js +2 -0
- package/dist/scripts/run-final-demo.js +3 -0
- package/dist/scripts/run-lab-demo.js +5 -0
- package/dist/scripts/run-visualization-demos.js +3 -0
- package/dist/scripts/security/runCodeql.js +57 -0
- package/dist/scripts/security/runDependencyChecks.js +57 -0
- package/dist/scripts/security/runFuzzSmoke.js +29 -0
- package/dist/scripts/security/runPackageChecks.js +56 -0
- package/dist/scripts/security/runSemgrep.js +63 -0
- package/dist/scripts/security/validate.js +117 -0
- package/dist/scripts/verify-benchmarks.js +202 -0
- package/dist/src/agents/adapters/claudeAdapter.js +37 -0
- package/dist/src/agents/adapters/codexAdapter.js +110 -0
- package/dist/src/agents/adapters/fakeAgentAdapter.js +101 -0
- package/dist/src/agents/agentRegistry.js +21 -0
- package/dist/src/agents/index.js +7 -0
- package/dist/src/agents/parseAgentTokenUsage.js +137 -0
- package/dist/src/agents/runAgentPrompt.js +38 -0
- package/dist/src/agents/types.js +1 -0
- package/dist/src/commands/buildGalleryCommand.js +56 -0
- package/dist/src/commands/captureDemoReport.js +116 -0
- package/dist/src/commands/evaluateTokenSavings.js +175 -0
- package/dist/src/commands/generateExperimentPlotsCommand.js +38 -0
- package/dist/src/commands/generatePromptVariants.js +67 -0
- package/dist/src/commands/renderExperimentReportCommand.js +131 -0
- package/dist/src/commands/runAgentPromptCommand.js +132 -0
- package/dist/src/commands/runControlledExperimentCommand.js +174 -0
- package/dist/src/commands/runFinalDemoCommand.js +123 -0
- package/dist/src/commands/runLabDemo.js +62 -0
- package/dist/src/commands/runVisualizationDemosCommand.js +67 -0
- package/dist/src/core/commandLine.js +59 -0
- package/dist/src/core/countTokens.js +8 -0
- package/dist/src/core/fileGlobs.js +100 -0
- package/dist/src/core/localProjectTarget.js +75 -0
- package/dist/src/core/pathSafety.js +19 -0
- package/dist/src/core/pythonCommand.js +30 -0
- package/dist/src/core/resolveCommand.js +110 -0
- package/dist/src/core/runMeasuredCommand.js +143 -0
- package/dist/src/evaluation/benchmarkMetadata.js +207 -0
- package/dist/src/evaluation/buildExperimentMatrix.js +75 -0
- package/dist/src/evaluation/classifyAgentRunOutcome.js +40 -0
- package/dist/src/evaluation/compareExperimentRuns.js +79 -0
- package/dist/src/evaluation/compareTokenSavings.js +47 -0
- package/dist/src/evaluation/controlledExperimentTypes.js +1 -0
- package/dist/src/evaluation/index.js +18 -0
- package/dist/src/evaluation/parseAgentAnswer.js +230 -0
- package/dist/src/evaluation/projectComplexity.js +126 -0
- package/dist/src/evaluation/projectFileTree.js +83 -0
- package/dist/src/evaluation/readEvaluationCases.js +59 -0
- package/dist/src/evaluation/renderTokenSavingsReportInput.js +55 -0
- package/dist/src/evaluation/runControlledExperiment.js +158 -0
- package/dist/src/evaluation/runMyDevKitRetrieval.js +197 -0
- package/dist/src/evaluation/runRawFullFileBaseline.js +31 -0
- package/dist/src/evaluation/scoreCorrectness.js +127 -0
- package/dist/src/evaluation/types.js +1 -0
- package/dist/src/evaluation/writeExperimentArtifacts.js +104 -0
- package/dist/src/evaluation/writeTokenSavingsArtifacts.js +57 -0
- package/dist/src/experiments/config.js +24 -0
- package/dist/src/experiments/defaultRegistry.js +7 -0
- package/dist/src/experiments/errors.js +18 -0
- package/dist/src/experiments/index.js +9 -0
- package/dist/src/experiments/outputPaths.js +25 -0
- package/dist/src/experiments/plugins/contextStrategyComparison/config.js +37 -0
- package/dist/src/experiments/plugins/contextStrategyComparison/index.js +3 -0
- package/dist/src/experiments/plugins/contextStrategyComparison/plugin.js +83 -0
- package/dist/src/experiments/plugins/contextStrategyComparison/resultMapping.js +260 -0
- package/dist/src/experiments/plugins/index.js +1 -0
- package/dist/src/experiments/registry.js +43 -0
- package/dist/src/experiments/results.js +48 -0
- package/dist/src/experiments/runner.js +181 -0
- package/dist/src/experiments/target.js +8 -0
- package/dist/src/experiments/types.js +1 -0
- package/dist/src/gallery/index.js +2 -0
- package/dist/src/gallery/types.js +1 -0
- package/dist/src/gallery/writeGalleryManifest.js +214 -0
- package/dist/src/index.js +12 -0
- package/dist/src/plots/buildExperimentPlotData.js +137 -0
- package/dist/src/plots/index.js +4 -0
- package/dist/src/plots/renderSvgChart.js +82 -0
- package/dist/src/plots/types.js +1 -0
- package/dist/src/plots/writePlotArtifacts.js +46 -0
- package/dist/src/prompts/buildPromptContext.js +68 -0
- package/dist/src/prompts/generateMyDevKitPrompt.js +106 -0
- package/dist/src/prompts/generatePromptVariants.js +36 -0
- package/dist/src/prompts/generateRawFullFilePrompt.js +97 -0
- package/dist/src/prompts/index.js +7 -0
- package/dist/src/prompts/measurePromptComplexity.js +41 -0
- package/dist/src/prompts/types.js +1 -0
- package/dist/src/prompts/writePromptArtifacts.js +43 -0
- package/dist/src/report/buildExperimentReportInput.js +339 -0
- package/dist/src/report/experimentReportTypes.js +1 -0
- package/dist/src/report/experiments/buildPluginExperimentReport.js +153 -0
- package/dist/src/report/experiments/experimentReportModel.js +1 -0
- package/dist/src/report/experiments/index.js +4 -0
- package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +133 -0
- package/dist/src/report/experiments/writePluginExperimentReports.js +30 -0
- package/dist/src/report/index.js +8 -0
- package/dist/src/report/renderExperimentHtmlReport.js +354 -0
- package/dist/src/report/renderHtmlReport.js +103 -0
- package/dist/src/report/types.js +10 -0
- package/dist/src/report/writeExperimentReportArtifacts.js +38 -0
- package/dist/src/report/writeReportArtifacts.js +39 -0
- package/dist/src/screenshot/captureReportScreenshot.js +75 -0
- package/dist/src/screenshot/index.js +2 -0
- package/dist/src/screenshot/types.js +1 -0
- package/dist/src/securityValidation/artifacts.js +15 -0
- package/dist/src/securityValidation/cliAdversarial/adversarialCliConfig.js +38 -0
- package/dist/src/securityValidation/cliAdversarial/dataVolumeChecks.js +194 -0
- package/dist/src/securityValidation/cliAdversarial/jsonStdoutChecks.js +359 -0
- package/dist/src/securityValidation/cliAdversarial/malformedArtifactChecks.js +284 -0
- package/dist/src/securityValidation/cliAdversarial/malformedArtifactFixtures.js +79 -0
- package/dist/src/securityValidation/cliAdversarial/pathBoundaryChecks.js +431 -0
- package/dist/src/securityValidation/cliAdversarial/pathCases.js +144 -0
- package/dist/src/securityValidation/cliAdversarial/readOnlyBoundaryChecks.js +294 -0
- package/dist/src/securityValidation/cliAdversarial/runAdversarialCheck.js +149 -0
- package/dist/src/securityValidation/cliAdversarial/subprocessSafetyChecks.js +214 -0
- package/dist/src/securityValidation/cliAdversarial/tempWorkspace.js +160 -0
- package/dist/src/securityValidation/commandRunner.js +136 -0
- package/dist/src/securityValidation/config.js +39 -0
- package/dist/src/securityValidation/dependencies/parseNpmAudit.js +115 -0
- package/dist/src/securityValidation/dependencies/parseNpmLs.js +71 -0
- package/dist/src/securityValidation/dependencies/parseNpmOutdated.js +41 -0
- package/dist/src/securityValidation/dependencies/runDependencyChecks.js +239 -0
- package/dist/src/securityValidation/dependencies/runOsvScanner.js +43 -0
- package/dist/src/securityValidation/fuzz/fuzzHarness.js +61 -0
- package/dist/src/securityValidation/fuzz/fuzzTargets.js +204 -0
- package/dist/src/securityValidation/fuzz/randomInput.js +0 -0
- package/dist/src/securityValidation/index.js +34 -0
- package/dist/src/securityValidation/packageChecks/forbiddenPackageContents.js +67 -0
- package/dist/src/securityValidation/packageChecks/parseNpmPackDryRun.js +56 -0
- package/dist/src/securityValidation/packageChecks/runPackageChecks.js +88 -0
- package/dist/src/securityValidation/report/renderSecurityReport.js +248 -0
- package/dist/src/securityValidation/report/securityReportTypes.js +1 -0
- package/dist/src/securityValidation/staticScans/codeql.js +66 -0
- package/dist/src/securityValidation/staticScans/semgrep.js +180 -0
- package/dist/src/securityValidation/testMatrix.js +535 -0
- package/dist/src/securityValidation/types.js +34 -0
- package/dist/src/securityValidation/validate/resolveTarget.js +32 -0
- package/dist/src/securityValidation/validate/runSecurityValidation.js +169 -0
- package/dist/src/securityValidation/validate/verdict.js +73 -0
- package/dist/src/visualizationDemos/buildMyDevKitVisualizationCommands.js +59 -0
- package/dist/src/visualizationDemos/index.js +4 -0
- package/dist/src/visualizationDemos/runVisualizationDemos.js +82 -0
- package/dist/src/visualizationDemos/types.js +1 -0
- package/dist/src/visualizationDemos/writeVisualizationDemoArtifacts.js +25 -0
- package/docs/METRICS.md +286 -0
- package/examples/demo-report-input.json +78 -0
- package/examples/lab-demo-cases.json +35 -0
- package/examples/real-agent-campaign-cases.json +118 -0
- package/examples/token-savings-cases.json +122 -0
- package/package.json +91 -0
- package/tests/fixtures/fake-adversarial-cli.js +152 -0
- package/tests/fixtures/fake-my-dev-kit-cli.js +83 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { runVisualizationDemos } from "../visualizationDemos/index.js";
|
|
3
|
+
export function parseRunVisualizationDemosArgs(argv) {
|
|
4
|
+
let projectPath = "";
|
|
5
|
+
let kitCommand = "";
|
|
6
|
+
let outDir = "";
|
|
7
|
+
let query;
|
|
8
|
+
let nodeId;
|
|
9
|
+
let requireAll = false;
|
|
10
|
+
let timeoutMs;
|
|
11
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
12
|
+
const arg = argv[index];
|
|
13
|
+
if (arg === "--project") {
|
|
14
|
+
projectPath = argv[index + 1] ?? "";
|
|
15
|
+
index += 1;
|
|
16
|
+
}
|
|
17
|
+
else if (arg === "--kit-command") {
|
|
18
|
+
kitCommand = argv[index + 1] ?? "";
|
|
19
|
+
index += 1;
|
|
20
|
+
}
|
|
21
|
+
else if (arg === "--out") {
|
|
22
|
+
outDir = argv[index + 1] ?? "";
|
|
23
|
+
index += 1;
|
|
24
|
+
}
|
|
25
|
+
else if (arg === "--query") {
|
|
26
|
+
query = argv[index + 1] ?? "";
|
|
27
|
+
index += 1;
|
|
28
|
+
}
|
|
29
|
+
else if (arg === "--node") {
|
|
30
|
+
nodeId = argv[index + 1] ?? "";
|
|
31
|
+
index += 1;
|
|
32
|
+
}
|
|
33
|
+
else if (arg === "--require-all") {
|
|
34
|
+
requireAll = true;
|
|
35
|
+
}
|
|
36
|
+
else if (arg === "--timeout-ms") {
|
|
37
|
+
timeoutMs = Number(argv[index + 1]);
|
|
38
|
+
index += 1;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (!projectPath || !kitCommand || !outDir)
|
|
42
|
+
throw new Error("Usage: --project <dir> --kit-command <command> --out <dir>");
|
|
43
|
+
return { projectPath, kitCommand, outDir, query, nodeId, requireAll, timeoutMs };
|
|
44
|
+
}
|
|
45
|
+
export async function runVisualizationDemosFromArgs(args, repoRoot = process.cwd()) {
|
|
46
|
+
return runVisualizationDemos({
|
|
47
|
+
projectPath: path.resolve(repoRoot, args.projectPath),
|
|
48
|
+
kitCommand: args.kitCommand,
|
|
49
|
+
outDir: path.resolve(repoRoot, args.outDir),
|
|
50
|
+
query: args.query,
|
|
51
|
+
nodeId: args.nodeId,
|
|
52
|
+
requireAll: args.requireAll,
|
|
53
|
+
timeoutMs: args.timeoutMs
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
export async function runVisualizationDemosCommand(argv) {
|
|
57
|
+
try {
|
|
58
|
+
const args = parseRunVisualizationDemosArgs(argv);
|
|
59
|
+
const artifacts = await runVisualizationDemosFromArgs(args);
|
|
60
|
+
console.log([`Runs: ${artifacts.summary.totalRuns}`, `Completed: ${artifacts.summary.completedRuns}`, `Failed: ${artifacts.summary.failedRuns}`, `Output: ${path.resolve(args.outDir)}`].join("\n"));
|
|
61
|
+
return args.requireAll && artifacts.summary.failedRuns > 0 ? 1 : 0;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export function parseCommandString(command) {
|
|
2
|
+
const parts = splitCommandString(command);
|
|
3
|
+
if (parts.length === 0) {
|
|
4
|
+
throw new Error("Command string is empty.");
|
|
5
|
+
}
|
|
6
|
+
return {
|
|
7
|
+
executable: parts[0],
|
|
8
|
+
args: parts.slice(1)
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function splitCommandString(command) {
|
|
12
|
+
const parts = [];
|
|
13
|
+
let current = "";
|
|
14
|
+
let quote = null;
|
|
15
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
16
|
+
const char = command[index];
|
|
17
|
+
const next = command[index + 1];
|
|
18
|
+
if (char === "\\" && quote !== null && next === quote) {
|
|
19
|
+
current += next;
|
|
20
|
+
index += 1;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if ((char === "\"" || char === "'") && quote === null) {
|
|
24
|
+
quote = char;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (char === quote) {
|
|
28
|
+
quote = null;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (/\s/.test(char) && quote === null) {
|
|
32
|
+
if (current.length > 0) {
|
|
33
|
+
parts.push(current);
|
|
34
|
+
current = "";
|
|
35
|
+
}
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
current += char;
|
|
39
|
+
}
|
|
40
|
+
if (quote !== null) {
|
|
41
|
+
throw new Error(`Command string has an unmatched ${quote} quote.`);
|
|
42
|
+
}
|
|
43
|
+
if (current.length > 0) {
|
|
44
|
+
parts.push(current);
|
|
45
|
+
}
|
|
46
|
+
return parts;
|
|
47
|
+
}
|
|
48
|
+
export function serializeCommand(parts) {
|
|
49
|
+
return parts.map(quoteCommandPart).join(" ");
|
|
50
|
+
}
|
|
51
|
+
export function quoteCommandPart(part) {
|
|
52
|
+
if (part.length === 0) {
|
|
53
|
+
return "\"\"";
|
|
54
|
+
}
|
|
55
|
+
if (!/[\s"'\\]/.test(part)) {
|
|
56
|
+
return part;
|
|
57
|
+
}
|
|
58
|
+
return `"${part.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
59
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { readdirSync, statSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { relativeWithinRoot, resolveWithinRoot } from "./pathSafety.js";
|
|
4
|
+
const excludedDirNames = new Set([
|
|
5
|
+
"node_modules",
|
|
6
|
+
"dist",
|
|
7
|
+
"build",
|
|
8
|
+
"coverage",
|
|
9
|
+
".git",
|
|
10
|
+
"lab-output",
|
|
11
|
+
".my-dev-kit",
|
|
12
|
+
".my-dev-kit-v1",
|
|
13
|
+
".my-dev-kit-lab",
|
|
14
|
+
"__pycache__"
|
|
15
|
+
]);
|
|
16
|
+
function isTempFile(relPath) {
|
|
17
|
+
return (relPath.endsWith(".tmp") ||
|
|
18
|
+
relPath.endsWith(".temp") ||
|
|
19
|
+
relPath.endsWith(".log") ||
|
|
20
|
+
relPath.endsWith(".pyc") ||
|
|
21
|
+
relPath.endsWith("~"));
|
|
22
|
+
}
|
|
23
|
+
function walkFiles(dir, root) {
|
|
24
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
25
|
+
const files = [];
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
const fullPath = path.join(dir, entry.name);
|
|
28
|
+
const relPath = relativeWithinRoot(root, fullPath);
|
|
29
|
+
if (entry.isDirectory()) {
|
|
30
|
+
if (excludedDirNames.has(entry.name)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
files.push(...walkFiles(fullPath, root));
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (!isTempFile(relPath)) {
|
|
37
|
+
files.push(fullPath);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return files;
|
|
41
|
+
}
|
|
42
|
+
function baseDirectoryFromGlob(globPattern) {
|
|
43
|
+
const normalized = globPattern.replace(/\\/g, "/");
|
|
44
|
+
const wildcardIndex = normalized.search(/[*?]/);
|
|
45
|
+
if (wildcardIndex === -1) {
|
|
46
|
+
return normalized;
|
|
47
|
+
}
|
|
48
|
+
const prefix = normalized.slice(0, wildcardIndex);
|
|
49
|
+
const trimmed = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
50
|
+
return trimmed || ".";
|
|
51
|
+
}
|
|
52
|
+
function matchesGlob(relPath, globPattern) {
|
|
53
|
+
const normalizedPath = relPath.replace(/\\/g, "/");
|
|
54
|
+
const normalizedGlob = globPattern.replace(/\\/g, "/");
|
|
55
|
+
if (normalizedGlob === "**/*") {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
const baseDir = baseDirectoryFromGlob(normalizedGlob);
|
|
59
|
+
if (normalizedGlob.endsWith("/**/*")) {
|
|
60
|
+
const prefix = baseDir === "." ? "" : `${baseDir}/`;
|
|
61
|
+
return normalizedPath.startsWith(prefix);
|
|
62
|
+
}
|
|
63
|
+
if (normalizedGlob.includes("*")) {
|
|
64
|
+
const placeholder = "__DOUBLE_STAR__";
|
|
65
|
+
const escaped = normalizedGlob
|
|
66
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
67
|
+
.replace(/\*\*/g, placeholder)
|
|
68
|
+
.replace(/\*/g, "[^/]*")
|
|
69
|
+
.replaceAll(placeholder, ".*");
|
|
70
|
+
return new RegExp(`^${escaped}$`).test(normalizedPath);
|
|
71
|
+
}
|
|
72
|
+
return normalizedPath === normalizedGlob;
|
|
73
|
+
}
|
|
74
|
+
export function collectFilesForGlobs(targetRoot, globs) {
|
|
75
|
+
const resolvedRoot = path.resolve(targetRoot);
|
|
76
|
+
const fileMap = new Map();
|
|
77
|
+
for (const globPattern of globs) {
|
|
78
|
+
if (!globPattern || typeof globPattern !== "string") {
|
|
79
|
+
throw new Error("Invalid glob pattern.");
|
|
80
|
+
}
|
|
81
|
+
const baseDir = resolveWithinRoot(resolvedRoot, baseDirectoryFromGlob(globPattern));
|
|
82
|
+
let baseStats;
|
|
83
|
+
try {
|
|
84
|
+
baseStats = statSync(baseDir);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
throw new Error(`Glob base directory does not exist: ${globPattern}`);
|
|
88
|
+
}
|
|
89
|
+
const candidateFiles = baseStats.isDirectory() ? walkFiles(baseDir, resolvedRoot) : [baseDir];
|
|
90
|
+
for (const candidate of candidateFiles) {
|
|
91
|
+
const relPath = relativeWithinRoot(resolvedRoot, candidate);
|
|
92
|
+
if (matchesGlob(relPath, globPattern)) {
|
|
93
|
+
fileMap.set(relPath, candidate);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return [...fileMap.entries()]
|
|
98
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
99
|
+
.map(([relativePath, absolutePath]) => ({ relativePath, absolutePath }));
|
|
100
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
export function resolveLocalProjectTarget(targetPathArg, toolRoot) {
|
|
5
|
+
let targetRoot;
|
|
6
|
+
if (!targetPathArg) {
|
|
7
|
+
targetRoot = toolRoot;
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
targetRoot = path.isAbsolute(targetPathArg)
|
|
11
|
+
? targetPathArg
|
|
12
|
+
: path.resolve(process.cwd(), targetPathArg);
|
|
13
|
+
if (!fs.existsSync(targetRoot)) {
|
|
14
|
+
throw new Error(`Target path does not exist: ${targetRoot}`);
|
|
15
|
+
}
|
|
16
|
+
const stat = fs.statSync(targetRoot);
|
|
17
|
+
if (!stat.isDirectory()) {
|
|
18
|
+
throw new Error(`Target path is not a directory: ${targetRoot}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const resolvedTargetRoot = path.resolve(targetRoot);
|
|
22
|
+
const resolvedToolRoot = path.resolve(toolRoot);
|
|
23
|
+
const packageMetadata = readPackageMetadata(resolvedTargetRoot);
|
|
24
|
+
return {
|
|
25
|
+
targetRoot: resolvedTargetRoot,
|
|
26
|
+
toolRoot: resolvedToolRoot,
|
|
27
|
+
packageName: packageMetadata.packageName,
|
|
28
|
+
packageVersion: packageMetadata.packageVersion,
|
|
29
|
+
hasPackageJson: packageMetadata.hasPackageJson,
|
|
30
|
+
hasLockfile: hasLockfile(resolvedTargetRoot),
|
|
31
|
+
...readGitMetadata(resolvedTargetRoot),
|
|
32
|
+
isSelf: resolvedTargetRoot === resolvedToolRoot,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function readPackageMetadata(targetRoot) {
|
|
36
|
+
try {
|
|
37
|
+
const pkgRaw = fs.readFileSync(path.join(targetRoot, "package.json"), "utf8");
|
|
38
|
+
const pkg = JSON.parse(pkgRaw);
|
|
39
|
+
return {
|
|
40
|
+
hasPackageJson: true,
|
|
41
|
+
packageName: typeof pkg.name === "string" ? pkg.name : null,
|
|
42
|
+
packageVersion: typeof pkg.version === "string" ? pkg.version : null,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return {
|
|
47
|
+
hasPackageJson: false,
|
|
48
|
+
packageName: null,
|
|
49
|
+
packageVersion: null,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function hasLockfile(targetRoot) {
|
|
54
|
+
return (fs.existsSync(path.join(targetRoot, "package-lock.json")) ||
|
|
55
|
+
fs.existsSync(path.join(targetRoot, "yarn.lock")) ||
|
|
56
|
+
fs.existsSync(path.join(targetRoot, "pnpm-lock.yaml")));
|
|
57
|
+
}
|
|
58
|
+
function readGitMetadata(targetRoot) {
|
|
59
|
+
try {
|
|
60
|
+
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
61
|
+
cwd: targetRoot,
|
|
62
|
+
encoding: "utf8",
|
|
63
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
64
|
+
}).trim();
|
|
65
|
+
const commit = execSync("git rev-parse --short HEAD", {
|
|
66
|
+
cwd: targetRoot,
|
|
67
|
+
encoding: "utf8",
|
|
68
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
69
|
+
}).trim();
|
|
70
|
+
return { branch, commit, hasGit: true };
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return { branch: null, commit: null, hasGit: false };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
export function resolveWithinRoot(root, relativeOrAbsolutePath) {
|
|
3
|
+
const resolvedRoot = path.resolve(root);
|
|
4
|
+
const resolvedPath = path.resolve(resolvedRoot, relativeOrAbsolutePath);
|
|
5
|
+
const relative = path.relative(resolvedRoot, resolvedPath);
|
|
6
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
7
|
+
throw new Error(`Resolved path escapes target root: ${relativeOrAbsolutePath}`);
|
|
8
|
+
}
|
|
9
|
+
return resolvedPath;
|
|
10
|
+
}
|
|
11
|
+
export function relativeWithinRoot(root, filePath) {
|
|
12
|
+
const resolvedRoot = path.resolve(root);
|
|
13
|
+
const resolvedFile = path.resolve(filePath);
|
|
14
|
+
const relative = path.relative(resolvedRoot, resolvedFile);
|
|
15
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
16
|
+
throw new Error(`File path escapes target root: ${filePath}`);
|
|
17
|
+
}
|
|
18
|
+
return relative.replace(/\\/g, "/");
|
|
19
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
const WINDOWS_PYTHON_CANDIDATES = [
|
|
3
|
+
{ command: "py", argsPrefix: ["-3"] },
|
|
4
|
+
{ command: "python", argsPrefix: [] }
|
|
5
|
+
];
|
|
6
|
+
const POSIX_PYTHON_CANDIDATES = [
|
|
7
|
+
{ command: "python3", argsPrefix: [] },
|
|
8
|
+
{ command: "python", argsPrefix: [] }
|
|
9
|
+
];
|
|
10
|
+
export function resolvePythonCommand(options = {}) {
|
|
11
|
+
const candidates = (options.platform ?? process.platform) === "win32"
|
|
12
|
+
? WINDOWS_PYTHON_CANDIDATES
|
|
13
|
+
: POSIX_PYTHON_CANDIDATES;
|
|
14
|
+
const probe = options.probeAvailability ?? ((candidate) => canRunPython(candidate, options.timeoutMs ?? 5_000));
|
|
15
|
+
for (const candidate of candidates) {
|
|
16
|
+
if (probe(candidate)) {
|
|
17
|
+
return candidate;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return candidates[0];
|
|
21
|
+
}
|
|
22
|
+
function canRunPython(candidate, timeoutMs) {
|
|
23
|
+
const result = spawnSync(candidate.command, [...candidate.argsPrefix, "--version"], {
|
|
24
|
+
encoding: "utf8",
|
|
25
|
+
shell: false,
|
|
26
|
+
timeout: timeoutMs,
|
|
27
|
+
windowsHide: true
|
|
28
|
+
});
|
|
29
|
+
return !result.error && result.status === 0;
|
|
30
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const windowsExtensionPreference = [".cmd", ".exe", ".bat", ".ps1", ""];
|
|
4
|
+
export function resolveCommand(command, options = {}) {
|
|
5
|
+
const platform = options.platform ?? process.platform;
|
|
6
|
+
const env = options.env ?? process.env;
|
|
7
|
+
if (platform !== "win32") {
|
|
8
|
+
return {
|
|
9
|
+
originalCommand: command,
|
|
10
|
+
command,
|
|
11
|
+
argsPrefix: [],
|
|
12
|
+
resolutionKind: "direct",
|
|
13
|
+
resolvedPath: command,
|
|
14
|
+
warnings: []
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const cwd = options.cwd ?? process.cwd();
|
|
18
|
+
const candidate = findWindowsCommandCandidate(command, env, cwd);
|
|
19
|
+
if (!candidate) {
|
|
20
|
+
return {
|
|
21
|
+
originalCommand: command,
|
|
22
|
+
command,
|
|
23
|
+
argsPrefix: [],
|
|
24
|
+
resolutionKind: "unavailable",
|
|
25
|
+
warnings: [`Command was not found on PATH: ${command}`]
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return commandForWindowsCandidate(command, candidate, env, options.allowPowerShellShim ?? true);
|
|
29
|
+
}
|
|
30
|
+
function commandForWindowsCandidate(originalCommand, resolvedPath, env, allowPowerShellShim) {
|
|
31
|
+
const extension = path.extname(resolvedPath).toLowerCase();
|
|
32
|
+
if (extension === ".ps1") {
|
|
33
|
+
if (!allowPowerShellShim) {
|
|
34
|
+
return {
|
|
35
|
+
originalCommand,
|
|
36
|
+
command: resolvedPath,
|
|
37
|
+
argsPrefix: [],
|
|
38
|
+
resolutionKind: "unavailable",
|
|
39
|
+
resolvedPath,
|
|
40
|
+
warnings: [`PowerShell shim execution is disabled for command: ${resolvedPath}`]
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
originalCommand,
|
|
45
|
+
command: "powershell.exe",
|
|
46
|
+
argsPrefix: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", resolvedPath],
|
|
47
|
+
resolutionKind: "windows-powershell-shim",
|
|
48
|
+
resolvedPath,
|
|
49
|
+
warnings: []
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (extension === ".cmd" || extension === ".bat") {
|
|
53
|
+
return {
|
|
54
|
+
originalCommand,
|
|
55
|
+
command: env.ComSpec ?? env.COMSPEC ?? process.env.ComSpec ?? process.env.COMSPEC ?? "cmd.exe",
|
|
56
|
+
argsPrefix: ["/d", "/s", "/c", "call"],
|
|
57
|
+
resolutionKind: "windows-cmd-shim",
|
|
58
|
+
resolvedPath,
|
|
59
|
+
warnings: []
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
originalCommand,
|
|
64
|
+
command: resolvedPath,
|
|
65
|
+
argsPrefix: [],
|
|
66
|
+
resolutionKind: path.extname(originalCommand) ? "direct" : "path-extension",
|
|
67
|
+
resolvedPath,
|
|
68
|
+
warnings: []
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function findWindowsCommandCandidate(command, env, cwd) {
|
|
72
|
+
if (hasPathSeparator(command) || path.isAbsolute(command)) {
|
|
73
|
+
return findCandidateInDirectory(path.resolve(cwd, command));
|
|
74
|
+
}
|
|
75
|
+
for (const searchDir of getPathEntries(env)) {
|
|
76
|
+
const candidate = findCandidateInDirectory(path.join(searchDir, command));
|
|
77
|
+
if (candidate) {
|
|
78
|
+
return candidate;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
function findCandidateInDirectory(basePath) {
|
|
84
|
+
const extension = path.extname(basePath);
|
|
85
|
+
if (extension) {
|
|
86
|
+
return isExecutableFile(basePath) ? basePath : undefined;
|
|
87
|
+
}
|
|
88
|
+
for (const candidateExtension of windowsExtensionPreference) {
|
|
89
|
+
const candidatePath = `${basePath}${candidateExtension}`;
|
|
90
|
+
if (isExecutableFile(candidatePath)) {
|
|
91
|
+
return candidatePath;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
function getPathEntries(env) {
|
|
97
|
+
const pathValue = env.Path ?? env.PATH ?? "";
|
|
98
|
+
return pathValue.split(path.delimiter).filter(Boolean);
|
|
99
|
+
}
|
|
100
|
+
function hasPathSeparator(command) {
|
|
101
|
+
return command.includes("/") || command.includes("\\");
|
|
102
|
+
}
|
|
103
|
+
function isExecutableFile(candidatePath) {
|
|
104
|
+
try {
|
|
105
|
+
return fs.statSync(candidatePath).isFile();
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { parseCommandString } from "./commandLine.js";
|
|
5
|
+
import { resolveCommand } from "./resolveCommand.js";
|
|
6
|
+
export { parseCommandString } from "./commandLine.js";
|
|
7
|
+
export async function runMeasuredCommand(options) {
|
|
8
|
+
await mkdir(options.outDir, { recursive: true });
|
|
9
|
+
const parsed = parseCommandString(options.commandString);
|
|
10
|
+
const resolution = options.resolveCommand === false
|
|
11
|
+
? {
|
|
12
|
+
originalCommand: parsed.executable,
|
|
13
|
+
command: parsed.executable,
|
|
14
|
+
argsPrefix: [],
|
|
15
|
+
resolutionKind: "direct",
|
|
16
|
+
resolvedPath: parsed.executable,
|
|
17
|
+
warnings: []
|
|
18
|
+
}
|
|
19
|
+
: resolveCommand(parsed.executable, {
|
|
20
|
+
cwd: options.cwd,
|
|
21
|
+
env: { ...process.env, ...options.env },
|
|
22
|
+
allowPowerShellShim: options.allowPowerShellShim
|
|
23
|
+
});
|
|
24
|
+
const executable = resolution.command;
|
|
25
|
+
const trailingArgs = [...parsed.args, ...(options.extraArgs ?? [])];
|
|
26
|
+
const args = resolution.resolutionKind === "windows-cmd-shim" && resolution.resolvedPath
|
|
27
|
+
? [...resolution.argsPrefix, resolution.resolvedPath, ...trailingArgs]
|
|
28
|
+
: [...resolution.argsPrefix, ...trailingArgs];
|
|
29
|
+
const stdoutPath = path.join(options.outDir, `${options.commandId}.stdout.txt`);
|
|
30
|
+
const stderrPath = path.join(options.outDir, `${options.commandId}.stderr.txt`);
|
|
31
|
+
const telemetryPath = path.join(options.outDir, `${options.commandId}.telemetry.json`);
|
|
32
|
+
const startedAt = new Date().toISOString();
|
|
33
|
+
const started = Date.now();
|
|
34
|
+
const result = await new Promise((resolve) => {
|
|
35
|
+
let stdout = "";
|
|
36
|
+
let stderr = "";
|
|
37
|
+
let spawnError;
|
|
38
|
+
let timedOut = false;
|
|
39
|
+
let timeout;
|
|
40
|
+
let child;
|
|
41
|
+
try {
|
|
42
|
+
child = spawn(executable, args, {
|
|
43
|
+
cwd: options.cwd,
|
|
44
|
+
env: { ...process.env, ...options.env },
|
|
45
|
+
shell: false,
|
|
46
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
const endedAt = new Date().toISOString();
|
|
51
|
+
const durationMs = Date.now() - started;
|
|
52
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
53
|
+
const measured = {
|
|
54
|
+
commandId: options.commandId,
|
|
55
|
+
commandString: options.commandString,
|
|
56
|
+
executable,
|
|
57
|
+
args,
|
|
58
|
+
cwd: options.cwd,
|
|
59
|
+
startedAt,
|
|
60
|
+
endedAt,
|
|
61
|
+
durationMs,
|
|
62
|
+
exitCode: null,
|
|
63
|
+
stdout,
|
|
64
|
+
stderr,
|
|
65
|
+
stdoutPath,
|
|
66
|
+
stderrPath,
|
|
67
|
+
telemetryPath,
|
|
68
|
+
ok: false,
|
|
69
|
+
error: message,
|
|
70
|
+
resolvedCommand: resolution
|
|
71
|
+
};
|
|
72
|
+
void Promise.all([
|
|
73
|
+
writeFile(stdoutPath, stdout, "utf8"),
|
|
74
|
+
writeFile(stderrPath, stderr, "utf8"),
|
|
75
|
+
writeFile(telemetryPath, JSON.stringify(measured, null, 2), "utf8")
|
|
76
|
+
]).then(() => resolve(measured));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
child.stdout.on("data", (chunk) => {
|
|
80
|
+
stdout += String(chunk);
|
|
81
|
+
});
|
|
82
|
+
child.stderr.on("data", (chunk) => {
|
|
83
|
+
stderr += String(chunk);
|
|
84
|
+
});
|
|
85
|
+
child.on("error", (error) => {
|
|
86
|
+
spawnError = error.message;
|
|
87
|
+
});
|
|
88
|
+
if (options.timeoutMs !== undefined) {
|
|
89
|
+
timeout = setTimeout(() => {
|
|
90
|
+
timedOut = true;
|
|
91
|
+
spawnError = `Command timed out after ${options.timeoutMs}ms.`;
|
|
92
|
+
killProcessTree(child.pid);
|
|
93
|
+
}, options.timeoutMs);
|
|
94
|
+
}
|
|
95
|
+
child.on("close", async (exitCode) => {
|
|
96
|
+
if (timeout) {
|
|
97
|
+
clearTimeout(timeout);
|
|
98
|
+
}
|
|
99
|
+
const endedAt = new Date().toISOString();
|
|
100
|
+
const durationMs = Date.now() - started;
|
|
101
|
+
await writeFile(stdoutPath, stdout, "utf8");
|
|
102
|
+
await writeFile(stderrPath, stderr, "utf8");
|
|
103
|
+
const measured = {
|
|
104
|
+
commandId: options.commandId,
|
|
105
|
+
commandString: options.commandString,
|
|
106
|
+
executable,
|
|
107
|
+
args,
|
|
108
|
+
cwd: options.cwd,
|
|
109
|
+
startedAt,
|
|
110
|
+
endedAt,
|
|
111
|
+
durationMs,
|
|
112
|
+
exitCode,
|
|
113
|
+
stdout,
|
|
114
|
+
stderr,
|
|
115
|
+
stdoutPath,
|
|
116
|
+
stderrPath,
|
|
117
|
+
telemetryPath,
|
|
118
|
+
ok: exitCode === 0 && !spawnError && !timedOut,
|
|
119
|
+
error: spawnError,
|
|
120
|
+
resolvedCommand: resolution
|
|
121
|
+
};
|
|
122
|
+
await writeFile(telemetryPath, JSON.stringify(measured, null, 2), "utf8");
|
|
123
|
+
resolve(measured);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
function killProcessTree(pid) {
|
|
129
|
+
if (!pid) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (process.platform === "win32") {
|
|
133
|
+
const killer = spawn("taskkill", ["/pid", String(pid), "/T", "/F"], { shell: false, stdio: "ignore" });
|
|
134
|
+
killer.on("error", () => undefined);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
process.kill(pid, "SIGTERM");
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// The child may have exited between timeout scheduling and kill.
|
|
142
|
+
}
|
|
143
|
+
}
|