agentplane 0.1.8 → 0.1.9
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/assets/AGENTS.md +13 -2
- package/dist/backends/task-backend.d.ts +12 -0
- package/dist/backends/task-backend.d.ts.map +1 -1
- package/dist/backends/task-backend.js +41 -4
- package/dist/cli/command-guide.d.ts.map +1 -1
- package/dist/cli/command-guide.js +6 -7
- package/dist/cli/run-cli.d.ts.map +1 -1
- package/dist/cli/run-cli.js +57 -2
- package/dist/commands/guard/index.d.ts +24 -3
- package/dist/commands/guard/index.d.ts.map +1 -1
- package/dist/commands/guard/index.js +175 -61
- package/dist/commands/hooks/index.d.ts.map +1 -1
- package/dist/commands/hooks/index.js +39 -29
- package/dist/commands/recipes.d.ts +75 -6
- package/dist/commands/recipes.d.ts.map +1 -1
- package/dist/commands/recipes.js +15 -521
- package/dist/commands/scenario.d.ts +7 -0
- package/dist/commands/scenario.d.ts.map +1 -0
- package/dist/commands/scenario.js +501 -0
- package/dist/commands/shared/task-backend.d.ts +19 -3
- package/dist/commands/shared/task-backend.d.ts.map +1 -1
- package/dist/commands/shared/task-backend.js +13 -5
- package/dist/commands/task/block.d.ts.map +1 -1
- package/dist/commands/task/block.js +22 -16
- package/dist/commands/task/comment.d.ts.map +1 -1
- package/dist/commands/task/comment.js +9 -2
- package/dist/commands/task/finish.d.ts.map +1 -1
- package/dist/commands/task/finish.js +36 -26
- package/dist/commands/task/set-status.d.ts.map +1 -1
- package/dist/commands/task/set-status.js +18 -4
- package/dist/commands/task/shared.d.ts +3 -2
- package/dist/commands/task/shared.d.ts.map +1 -1
- package/dist/commands/task/shared.js +18 -28
- package/dist/commands/task/start.d.ts.map +1 -1
- package/dist/commands/task/start.js +24 -18
- package/dist/commands/task/verify-record.d.ts.map +1 -1
- package/dist/commands/task/verify-record.js +8 -1
- package/dist/shared/git-log.d.ts +5 -0
- package/dist/shared/git-log.d.ts.map +1 -0
- package/dist/shared/git-log.js +14 -0
- package/dist/shared/git-path.d.ts +3 -0
- package/dist/shared/git-path.d.ts.map +1 -0
- package/dist/shared/git-path.js +30 -0
- package/dist/shared/guards.d.ts +2 -0
- package/dist/shared/guards.d.ts.map +1 -0
- package/dist/shared/guards.js +3 -0
- package/dist/shared/protected-paths.d.ts +12 -0
- package/dist/shared/protected-paths.d.ts.map +1 -0
- package/dist/shared/protected-paths.js +51 -0
- package/dist/shared/strings.d.ts +2 -0
- package/dist/shared/strings.d.ts.map +1 -0
- package/dist/shared/strings.js +14 -0
- package/package.json +2 -2
package/dist/commands/recipes.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
1
|
import { createPublicKey, verify } from "node:crypto";
|
|
3
|
-
import { cp, mkdir, mkdtemp, readdir, readFile,
|
|
2
|
+
import { cp, mkdir, mkdtemp, readdir, readFile, rename, rm } from "node:fs/promises";
|
|
4
3
|
import os from "node:os";
|
|
5
4
|
import path from "node:path";
|
|
6
|
-
import { promisify } from "node:util";
|
|
7
5
|
import { atomicWriteFile, defaultConfig, loadConfig, resolveProject } from "@agentplaneorg/core";
|
|
8
6
|
import { extractArchive } from "../cli/archive.js";
|
|
9
7
|
import { sha256File } from "../cli/checksum.js";
|
|
@@ -11,13 +9,15 @@ import { mapCoreError } from "../cli/error-map.js";
|
|
|
11
9
|
import { fileExists, getPathKind } from "../cli/fs-utils.js";
|
|
12
10
|
import { downloadToFile, fetchJson, fetchText } from "../cli/http.js";
|
|
13
11
|
import { emptyStateMessage, infoMessage, invalidFieldMessage, invalidPathMessage, missingValueMessage, missingFileMessage, requiredFieldMessage, successMessage, usageMessage, } from "../cli/output.js";
|
|
12
|
+
import { isRecord } from "../shared/guards.js";
|
|
14
13
|
import { CliError } from "../shared/errors.js";
|
|
14
|
+
import { dedupeStrings } from "../shared/strings.js";
|
|
15
15
|
import { ensureNetworkApproved } from "./shared/network-approval.js";
|
|
16
|
-
|
|
16
|
+
import { resolvePathFallback } from "./shared/path.js";
|
|
17
17
|
const INSTALLED_RECIPES_NAME = "recipes.json";
|
|
18
|
-
const RECIPES_DIR_NAME = "recipes";
|
|
19
|
-
const RECIPES_SCENARIOS_DIR_NAME = "scenarios";
|
|
20
|
-
const RECIPES_SCENARIOS_INDEX_NAME = "scenarios.json";
|
|
18
|
+
export const RECIPES_DIR_NAME = "recipes";
|
|
19
|
+
export const RECIPES_SCENARIOS_DIR_NAME = "scenarios";
|
|
20
|
+
export const RECIPES_SCENARIOS_INDEX_NAME = "scenarios.json";
|
|
21
21
|
const RECIPES_REMOTE_INDEX_NAME = "recipes-index.json";
|
|
22
22
|
const RECIPES_REMOTE_INDEX_SIG_NAME = "recipes-index.json.sig";
|
|
23
23
|
const RECIPE_USAGE = "Usage: agentplane recipes <list|info|explain|install|remove|list-remote|cache> [args]";
|
|
@@ -47,128 +47,12 @@ const RECIPE_CONFLICT_MODES = ["fail", "rename", "overwrite"];
|
|
|
47
47
|
const AGENTPLANE_HOME_ENV = "AGENTPLANE_HOME";
|
|
48
48
|
const GLOBAL_RECIPES_DIR_NAME = "recipes";
|
|
49
49
|
const PROJECT_RECIPES_CACHE_DIR_NAME = "recipes-cache";
|
|
50
|
-
const SCENARIO_USAGE = "Usage: agentplane scenario <list|info|run> [args]";
|
|
51
|
-
const SCENARIO_USAGE_EXAMPLE = "agentplane scenario list";
|
|
52
|
-
const SCENARIO_INFO_USAGE = "Usage: agentplane scenario info <recipe:scenario>";
|
|
53
|
-
const SCENARIO_INFO_USAGE_EXAMPLE = "agentplane scenario info viewer:demo";
|
|
54
|
-
const SCENARIO_RUN_USAGE = "Usage: agentplane scenario run <recipe:scenario>";
|
|
55
|
-
const SCENARIO_RUN_USAGE_EXAMPLE = "agentplane scenario run viewer:demo";
|
|
56
|
-
const SCENARIO_REPORT_NAME = "report.json";
|
|
57
|
-
const SENSITIVE_ARG_FLAGS = new Set([
|
|
58
|
-
"--token",
|
|
59
|
-
"--secret",
|
|
60
|
-
"--password",
|
|
61
|
-
"--api-key",
|
|
62
|
-
"--apikey",
|
|
63
|
-
"--access-key",
|
|
64
|
-
"--client-secret",
|
|
65
|
-
"--auth",
|
|
66
|
-
"--authorization",
|
|
67
|
-
"--bearer",
|
|
68
|
-
]);
|
|
69
|
-
function isRecord(value) {
|
|
70
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
71
|
-
}
|
|
72
|
-
function redactArgs(args) {
|
|
73
|
-
const out = [...args];
|
|
74
|
-
for (let i = 0; i < out.length; i++) {
|
|
75
|
-
const arg = out[i];
|
|
76
|
-
if (!arg)
|
|
77
|
-
continue;
|
|
78
|
-
const eqIndex = arg.indexOf("=");
|
|
79
|
-
const flag = eqIndex === -1 ? arg : arg.slice(0, eqIndex);
|
|
80
|
-
if (!SENSITIVE_ARG_FLAGS.has(flag))
|
|
81
|
-
continue;
|
|
82
|
-
if (eqIndex !== -1) {
|
|
83
|
-
out[i] = `${flag}=<redacted>`;
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
out[i] = flag;
|
|
87
|
-
if (i + 1 < out.length && !out[i + 1]?.startsWith("-")) {
|
|
88
|
-
out[i + 1] = "<redacted>";
|
|
89
|
-
i += 1;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return out;
|
|
93
|
-
}
|
|
94
|
-
function dedupeStrings(items) {
|
|
95
|
-
const seen = new Set();
|
|
96
|
-
const out = [];
|
|
97
|
-
for (const item of items) {
|
|
98
|
-
const trimmed = item.trim();
|
|
99
|
-
if (!trimmed)
|
|
100
|
-
continue;
|
|
101
|
-
if (seen.has(trimmed))
|
|
102
|
-
continue;
|
|
103
|
-
seen.add(trimmed);
|
|
104
|
-
out.push(trimmed);
|
|
105
|
-
}
|
|
106
|
-
return out;
|
|
107
|
-
}
|
|
108
|
-
async function resolvePathFallback(filePath) {
|
|
109
|
-
try {
|
|
110
|
-
return await realpath(filePath);
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
return path.resolve(filePath);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
50
|
function isNotGitRepoError(err) {
|
|
117
51
|
if (err instanceof Error) {
|
|
118
52
|
return err.message.startsWith("Not a git repository");
|
|
119
53
|
}
|
|
120
54
|
return false;
|
|
121
55
|
}
|
|
122
|
-
async function getGitDiffSummary(cwd) {
|
|
123
|
-
try {
|
|
124
|
-
const [diff, staged, status] = await Promise.all([
|
|
125
|
-
execFileAsync("git", ["diff", "--stat"], { cwd }),
|
|
126
|
-
execFileAsync("git", ["diff", "--stat", "--staged"], { cwd }),
|
|
127
|
-
execFileAsync("git", ["status", "--porcelain"], { cwd }),
|
|
128
|
-
]);
|
|
129
|
-
const diffStat = diff.stdout.trim();
|
|
130
|
-
const stagedStat = staged.stdout.trim();
|
|
131
|
-
const statusLines = status.stdout.trim();
|
|
132
|
-
return {
|
|
133
|
-
diff_stat: diffStat || undefined,
|
|
134
|
-
staged_stat: stagedStat || undefined,
|
|
135
|
-
status: statusLines
|
|
136
|
-
? statusLines
|
|
137
|
-
.split("\n")
|
|
138
|
-
.map((line) => line.trim())
|
|
139
|
-
.filter(Boolean)
|
|
140
|
-
: [],
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
catch {
|
|
144
|
-
return undefined;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
function collectScenarioEnvKeys(stepEnv) {
|
|
148
|
-
return dedupeStrings([
|
|
149
|
-
...Object.keys(stepEnv ?? {}),
|
|
150
|
-
"AGENTPLANE_RUN_DIR",
|
|
151
|
-
"AGENTPLANE_STEP_DIR",
|
|
152
|
-
"AGENTPLANE_RECIPES_CACHE_DIR",
|
|
153
|
-
"AGENTPLANE_RECIPE_ID",
|
|
154
|
-
"AGENTPLANE_SCENARIO_ID",
|
|
155
|
-
"AGENTPLANE_TOOL_ID",
|
|
156
|
-
]);
|
|
157
|
-
}
|
|
158
|
-
async function writeScenarioReport(opts) {
|
|
159
|
-
const report = {
|
|
160
|
-
schema_version: 1,
|
|
161
|
-
recipe: opts.recipeId,
|
|
162
|
-
scenario: opts.scenarioId,
|
|
163
|
-
run_id: opts.runId,
|
|
164
|
-
started_at: opts.startedAt,
|
|
165
|
-
ended_at: new Date().toISOString(),
|
|
166
|
-
status: opts.status,
|
|
167
|
-
steps: opts.steps,
|
|
168
|
-
git: opts.gitSummary,
|
|
169
|
-
};
|
|
170
|
-
await atomicWriteFile(path.join(opts.runDir, SCENARIO_REPORT_NAME), `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
|
171
|
-
}
|
|
172
56
|
async function maybeResolveProject(opts) {
|
|
173
57
|
try {
|
|
174
58
|
return await resolveProject({
|
|
@@ -461,11 +345,11 @@ function validateScenarioDefinition(raw, sourcePath) {
|
|
|
461
345
|
steps: raw.steps,
|
|
462
346
|
};
|
|
463
347
|
}
|
|
464
|
-
async function readScenarioDefinition(filePath) {
|
|
348
|
+
export async function readScenarioDefinition(filePath) {
|
|
465
349
|
const raw = JSON.parse(await readFile(filePath, "utf8"));
|
|
466
350
|
return validateScenarioDefinition(raw, filePath);
|
|
467
351
|
}
|
|
468
|
-
async function readScenarioIndex(filePath) {
|
|
352
|
+
export async function readScenarioIndex(filePath) {
|
|
469
353
|
const raw = JSON.parse(await readFile(filePath, "utf8"));
|
|
470
354
|
if (!isRecord(raw))
|
|
471
355
|
throw new Error(invalidFieldMessage("scenarios index", "object"));
|
|
@@ -536,7 +420,7 @@ async function collectRecipeScenarioDetails(recipeDir, manifest) {
|
|
|
536
420
|
}
|
|
537
421
|
return [];
|
|
538
422
|
}
|
|
539
|
-
function normalizeScenarioToolStep(raw, sourcePath) {
|
|
423
|
+
export function normalizeScenarioToolStep(raw, sourcePath) {
|
|
540
424
|
if (!isRecord(raw)) {
|
|
541
425
|
throw new Error(invalidFieldMessage("scenario step", "object", sourcePath));
|
|
542
426
|
}
|
|
@@ -562,7 +446,7 @@ function normalizeScenarioToolStep(raw, sourcePath) {
|
|
|
562
446
|
}
|
|
563
447
|
return { tool, args, env };
|
|
564
448
|
}
|
|
565
|
-
async function readInstalledRecipesFile(filePath) {
|
|
449
|
+
export async function readInstalledRecipesFile(filePath) {
|
|
566
450
|
try {
|
|
567
451
|
const raw = JSON.parse(await readFile(filePath, "utf8"));
|
|
568
452
|
return sortInstalledRecipes(validateInstalledRecipesFile(raw));
|
|
@@ -582,7 +466,7 @@ async function writeInstalledRecipesFile(filePath, file) {
|
|
|
582
466
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
583
467
|
await atomicWriteFile(filePath, `${JSON.stringify(sorted, null, 2)}\n`, "utf8");
|
|
584
468
|
}
|
|
585
|
-
async function readRecipeManifest(manifestPath) {
|
|
469
|
+
export async function readRecipeManifest(manifestPath) {
|
|
586
470
|
const raw = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
587
471
|
return validateRecipeManifest(raw);
|
|
588
472
|
}
|
|
@@ -610,16 +494,16 @@ function resolveAgentplaneHome() {
|
|
|
610
494
|
function resolveGlobalRecipesDir() {
|
|
611
495
|
return path.join(resolveAgentplaneHome(), GLOBAL_RECIPES_DIR_NAME);
|
|
612
496
|
}
|
|
613
|
-
function resolveInstalledRecipesPath() {
|
|
497
|
+
export function resolveInstalledRecipesPath() {
|
|
614
498
|
return path.join(resolveAgentplaneHome(), INSTALLED_RECIPES_NAME);
|
|
615
499
|
}
|
|
616
500
|
function resolveRecipesIndexCachePath() {
|
|
617
501
|
return path.join(resolveAgentplaneHome(), RECIPES_REMOTE_INDEX_NAME);
|
|
618
502
|
}
|
|
619
|
-
function resolveInstalledRecipeDir(entry) {
|
|
503
|
+
export function resolveInstalledRecipeDir(entry) {
|
|
620
504
|
return path.join(resolveGlobalRecipesDir(), entry.id, entry.version);
|
|
621
505
|
}
|
|
622
|
-
function resolveProjectRecipesCacheDir(resolved) {
|
|
506
|
+
export function resolveProjectRecipesCacheDir(resolved) {
|
|
623
507
|
return path.join(resolved.agentplaneDir, PROJECT_RECIPES_CACHE_DIR_NAME);
|
|
624
508
|
}
|
|
625
509
|
function parseRecipeInstallArgs(args) {
|
|
@@ -1015,343 +899,6 @@ async function cmdRecipeListRemote(opts) {
|
|
|
1015
899
|
throw mapCoreError(err, { command: "recipes list-remote", root: opts.rootOverride ?? null });
|
|
1016
900
|
}
|
|
1017
901
|
}
|
|
1018
|
-
async function cmdScenarioList(opts) {
|
|
1019
|
-
try {
|
|
1020
|
-
const installed = await readInstalledRecipesFile(resolveInstalledRecipesPath());
|
|
1021
|
-
const entries = [];
|
|
1022
|
-
for (const recipe of installed.recipes) {
|
|
1023
|
-
const recipeDir = resolveInstalledRecipeDir(recipe);
|
|
1024
|
-
const scenariosDir = path.join(recipeDir, RECIPES_SCENARIOS_DIR_NAME);
|
|
1025
|
-
if ((await getPathKind(scenariosDir)) === "dir") {
|
|
1026
|
-
const files = await readdir(scenariosDir);
|
|
1027
|
-
const jsonFiles = files.filter((entry) => entry.toLowerCase().endsWith(".json")).toSorted();
|
|
1028
|
-
for (const file of jsonFiles) {
|
|
1029
|
-
const scenario = await readScenarioDefinition(path.join(scenariosDir, file));
|
|
1030
|
-
entries.push({ recipeId: recipe.id, scenarioId: scenario.id, summary: scenario.summary });
|
|
1031
|
-
}
|
|
1032
|
-
continue;
|
|
1033
|
-
}
|
|
1034
|
-
const scenariosIndexPath = path.join(recipeDir, RECIPES_SCENARIOS_INDEX_NAME);
|
|
1035
|
-
if (await fileExists(scenariosIndexPath)) {
|
|
1036
|
-
const index = await readScenarioIndex(scenariosIndexPath);
|
|
1037
|
-
for (const scenario of index.scenarios) {
|
|
1038
|
-
entries.push({
|
|
1039
|
-
recipeId: recipe.id,
|
|
1040
|
-
scenarioId: scenario.id,
|
|
1041
|
-
summary: scenario.summary,
|
|
1042
|
-
});
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
if (entries.length === 0) {
|
|
1047
|
-
process.stdout.write(`${emptyStateMessage("scenarios", "Install a recipe to add scenarios.")}\n`);
|
|
1048
|
-
return 0;
|
|
1049
|
-
}
|
|
1050
|
-
const sorted = entries.toSorted((a, b) => {
|
|
1051
|
-
const byRecipe = a.recipeId.localeCompare(b.recipeId);
|
|
1052
|
-
if (byRecipe !== 0)
|
|
1053
|
-
return byRecipe;
|
|
1054
|
-
return a.scenarioId.localeCompare(b.scenarioId);
|
|
1055
|
-
});
|
|
1056
|
-
for (const entry of sorted) {
|
|
1057
|
-
process.stdout.write(`${entry.recipeId}:${entry.scenarioId} - ${entry.summary ?? "No summary"}\n`);
|
|
1058
|
-
}
|
|
1059
|
-
return 0;
|
|
1060
|
-
}
|
|
1061
|
-
catch (err) {
|
|
1062
|
-
if (err instanceof CliError)
|
|
1063
|
-
throw err;
|
|
1064
|
-
throw mapCoreError(err, { command: "scenario list", root: opts.rootOverride ?? null });
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
async function cmdScenarioInfo(opts) {
|
|
1068
|
-
try {
|
|
1069
|
-
const [recipeId, scenarioId] = opts.id.split(":");
|
|
1070
|
-
if (!recipeId || !scenarioId) {
|
|
1071
|
-
throw new CliError({
|
|
1072
|
-
exitCode: 2,
|
|
1073
|
-
code: "E_USAGE",
|
|
1074
|
-
message: usageMessage(SCENARIO_INFO_USAGE, SCENARIO_INFO_USAGE_EXAMPLE),
|
|
1075
|
-
});
|
|
1076
|
-
}
|
|
1077
|
-
const installed = await readInstalledRecipesFile(resolveInstalledRecipesPath());
|
|
1078
|
-
const entry = installed.recipes.find((recipe) => recipe.id === recipeId);
|
|
1079
|
-
if (!entry) {
|
|
1080
|
-
throw new CliError({
|
|
1081
|
-
exitCode: 5,
|
|
1082
|
-
code: "E_IO",
|
|
1083
|
-
message: `Recipe not installed: ${recipeId}`,
|
|
1084
|
-
});
|
|
1085
|
-
}
|
|
1086
|
-
const recipeDir = resolveInstalledRecipeDir(entry);
|
|
1087
|
-
const scenariosDir = path.join(recipeDir, RECIPES_SCENARIOS_DIR_NAME);
|
|
1088
|
-
let scenario = null;
|
|
1089
|
-
if ((await getPathKind(scenariosDir)) === "dir") {
|
|
1090
|
-
const files = await readdir(scenariosDir);
|
|
1091
|
-
const jsonFiles = files.filter((file) => file.toLowerCase().endsWith(".json")).toSorted();
|
|
1092
|
-
for (const file of jsonFiles) {
|
|
1093
|
-
const candidate = await readScenarioDefinition(path.join(scenariosDir, file));
|
|
1094
|
-
if (candidate.id === scenarioId) {
|
|
1095
|
-
scenario = candidate;
|
|
1096
|
-
break;
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
let summary;
|
|
1101
|
-
if (!scenario) {
|
|
1102
|
-
const scenariosIndexPath = path.join(recipeDir, RECIPES_SCENARIOS_INDEX_NAME);
|
|
1103
|
-
if (await fileExists(scenariosIndexPath)) {
|
|
1104
|
-
const index = await readScenarioIndex(scenariosIndexPath);
|
|
1105
|
-
const entrySummary = index.scenarios.find((item) => item.id === scenarioId);
|
|
1106
|
-
summary = entrySummary?.summary;
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
if (!scenario && !summary) {
|
|
1110
|
-
throw new CliError({
|
|
1111
|
-
exitCode: 5,
|
|
1112
|
-
code: "E_IO",
|
|
1113
|
-
message: `Scenario not found: ${recipeId}:${scenarioId}`,
|
|
1114
|
-
});
|
|
1115
|
-
}
|
|
1116
|
-
process.stdout.write(`Scenario: ${recipeId}:${scenarioId}\n`);
|
|
1117
|
-
if (summary)
|
|
1118
|
-
process.stdout.write(`Summary: ${summary}\n`);
|
|
1119
|
-
if (!scenario) {
|
|
1120
|
-
process.stdout.write("Details: Scenario definition not found in recipe.\n");
|
|
1121
|
-
return 0;
|
|
1122
|
-
}
|
|
1123
|
-
if (scenario.summary)
|
|
1124
|
-
process.stdout.write(`Summary: ${scenario.summary}\n`);
|
|
1125
|
-
if (scenario.description)
|
|
1126
|
-
process.stdout.write(`Description: ${scenario.description}\n`);
|
|
1127
|
-
process.stdout.write(`Goal: ${scenario.goal}\n`);
|
|
1128
|
-
process.stdout.write(`Inputs: ${JSON.stringify(scenario.inputs, null, 2)}\n`);
|
|
1129
|
-
process.stdout.write(`Outputs: ${JSON.stringify(scenario.outputs, null, 2)}\n`);
|
|
1130
|
-
process.stdout.write("Steps:\n");
|
|
1131
|
-
let stepIndex = 1;
|
|
1132
|
-
for (const step of scenario.steps) {
|
|
1133
|
-
process.stdout.write(` ${stepIndex}. ${JSON.stringify(step)}\n`);
|
|
1134
|
-
stepIndex += 1;
|
|
1135
|
-
}
|
|
1136
|
-
return 0;
|
|
1137
|
-
}
|
|
1138
|
-
catch (err) {
|
|
1139
|
-
if (err instanceof CliError)
|
|
1140
|
-
throw err;
|
|
1141
|
-
throw mapCoreError(err, { command: "scenario info", root: opts.rootOverride ?? null });
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
async function executeRecipeTool(opts) {
|
|
1145
|
-
try {
|
|
1146
|
-
const command = opts.runtime === "node" ? "node" : "bash";
|
|
1147
|
-
const { stdout, stderr } = await execFileAsync(command, [opts.entrypoint, ...opts.args], {
|
|
1148
|
-
cwd: opts.cwd,
|
|
1149
|
-
env: opts.env,
|
|
1150
|
-
});
|
|
1151
|
-
return { exitCode: 0, stdout: String(stdout), stderr: String(stderr) };
|
|
1152
|
-
}
|
|
1153
|
-
catch (err) {
|
|
1154
|
-
let execErr = null;
|
|
1155
|
-
if (err && typeof err === "object") {
|
|
1156
|
-
execErr = err;
|
|
1157
|
-
}
|
|
1158
|
-
const exitCode = typeof execErr?.code === "number" ? execErr.code : 1;
|
|
1159
|
-
return {
|
|
1160
|
-
exitCode,
|
|
1161
|
-
stdout: String(execErr?.stdout ?? ""),
|
|
1162
|
-
stderr: String(execErr?.stderr ?? ""),
|
|
1163
|
-
};
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
function sanitizeRunId(value) {
|
|
1167
|
-
return value.replaceAll(/[^a-zA-Z0-9._-]/g, "_");
|
|
1168
|
-
}
|
|
1169
|
-
async function cmdScenarioRun(opts) {
|
|
1170
|
-
try {
|
|
1171
|
-
const resolved = await resolveProject({
|
|
1172
|
-
cwd: opts.cwd,
|
|
1173
|
-
rootOverride: opts.rootOverride ?? null,
|
|
1174
|
-
});
|
|
1175
|
-
const [recipeId, scenarioId] = opts.id.split(":");
|
|
1176
|
-
if (!recipeId || !scenarioId) {
|
|
1177
|
-
throw new CliError({
|
|
1178
|
-
exitCode: 2,
|
|
1179
|
-
code: "E_USAGE",
|
|
1180
|
-
message: usageMessage(SCENARIO_RUN_USAGE, SCENARIO_RUN_USAGE_EXAMPLE),
|
|
1181
|
-
});
|
|
1182
|
-
}
|
|
1183
|
-
const installed = await readInstalledRecipesFile(resolveInstalledRecipesPath());
|
|
1184
|
-
const entry = installed.recipes.find((recipe) => recipe.id === recipeId);
|
|
1185
|
-
if (!entry) {
|
|
1186
|
-
throw new CliError({
|
|
1187
|
-
exitCode: 5,
|
|
1188
|
-
code: "E_IO",
|
|
1189
|
-
message: `Recipe not installed: ${recipeId}`,
|
|
1190
|
-
});
|
|
1191
|
-
}
|
|
1192
|
-
const recipeDir = resolveInstalledRecipeDir(entry);
|
|
1193
|
-
const manifestPath = path.join(recipeDir, "manifest.json");
|
|
1194
|
-
const manifest = await readRecipeManifest(manifestPath);
|
|
1195
|
-
const scenariosDir = path.join(recipeDir, RECIPES_SCENARIOS_DIR_NAME);
|
|
1196
|
-
if ((await getPathKind(scenariosDir)) !== "dir") {
|
|
1197
|
-
throw new CliError({
|
|
1198
|
-
exitCode: 5,
|
|
1199
|
-
code: "E_IO",
|
|
1200
|
-
message: `Scenario definitions not found for recipe: ${recipeId}`,
|
|
1201
|
-
});
|
|
1202
|
-
}
|
|
1203
|
-
let scenario = null;
|
|
1204
|
-
const files = await readdir(scenariosDir);
|
|
1205
|
-
const jsonFiles = files.filter((file) => file.toLowerCase().endsWith(".json")).toSorted();
|
|
1206
|
-
for (const file of jsonFiles) {
|
|
1207
|
-
const candidate = await readScenarioDefinition(path.join(scenariosDir, file));
|
|
1208
|
-
if (candidate.id === scenarioId) {
|
|
1209
|
-
scenario = candidate;
|
|
1210
|
-
break;
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
if (!scenario) {
|
|
1214
|
-
throw new CliError({
|
|
1215
|
-
exitCode: 5,
|
|
1216
|
-
code: "E_IO",
|
|
1217
|
-
message: `Scenario not found: ${recipeId}:${scenarioId}`,
|
|
1218
|
-
});
|
|
1219
|
-
}
|
|
1220
|
-
const runsRoot = path.join(resolved.agentplaneDir, RECIPES_DIR_NAME, recipeId, "runs");
|
|
1221
|
-
await mkdir(runsRoot, { recursive: true });
|
|
1222
|
-
const recipesCacheDir = resolveProjectRecipesCacheDir(resolved);
|
|
1223
|
-
await mkdir(recipesCacheDir, { recursive: true });
|
|
1224
|
-
const runStartedAt = new Date().toISOString();
|
|
1225
|
-
const runId = `${new Date()
|
|
1226
|
-
.toISOString()
|
|
1227
|
-
.replaceAll(":", "-")
|
|
1228
|
-
.replaceAll(".", "-")}-${sanitizeRunId(scenarioId)}`;
|
|
1229
|
-
const runDir = path.join(runsRoot, runId);
|
|
1230
|
-
await mkdir(runDir, { recursive: true });
|
|
1231
|
-
const stepsMeta = [];
|
|
1232
|
-
const stepsReport = [];
|
|
1233
|
-
for (let index = 0; index < scenario.steps.length; index++) {
|
|
1234
|
-
const step = normalizeScenarioToolStep(scenario.steps[index], `${recipeId}:${scenarioId}`);
|
|
1235
|
-
const toolEntry = manifest.tools?.find((tool) => tool?.id === step.tool);
|
|
1236
|
-
if (!toolEntry) {
|
|
1237
|
-
throw new CliError({
|
|
1238
|
-
exitCode: 5,
|
|
1239
|
-
code: "E_IO",
|
|
1240
|
-
message: `Tool not found in recipe manifest: ${step.tool}`,
|
|
1241
|
-
});
|
|
1242
|
-
}
|
|
1243
|
-
const runtime = toolEntry.runtime === "node" || toolEntry.runtime === "bash" ? toolEntry.runtime : "";
|
|
1244
|
-
const entrypoint = typeof toolEntry.entrypoint === "string" ? toolEntry.entrypoint : "";
|
|
1245
|
-
if (!runtime || !entrypoint) {
|
|
1246
|
-
throw new CliError({
|
|
1247
|
-
exitCode: 3,
|
|
1248
|
-
code: "E_VALIDATION",
|
|
1249
|
-
message: `Tool entry is missing runtime/entrypoint: ${step.tool}`,
|
|
1250
|
-
});
|
|
1251
|
-
}
|
|
1252
|
-
if (Array.isArray(toolEntry.permissions) && toolEntry.permissions.length > 0) {
|
|
1253
|
-
process.stdout.write(`Warning: tool ${toolEntry.id} declares permissions: ${toolEntry.permissions.join(", ")}\n`);
|
|
1254
|
-
}
|
|
1255
|
-
const entrypointPath = path.join(recipeDir, entrypoint);
|
|
1256
|
-
if (!(await fileExists(entrypointPath))) {
|
|
1257
|
-
throw new CliError({
|
|
1258
|
-
exitCode: 5,
|
|
1259
|
-
code: "E_IO",
|
|
1260
|
-
message: `Tool entrypoint not found: ${entrypoint}`,
|
|
1261
|
-
});
|
|
1262
|
-
}
|
|
1263
|
-
const stepDir = path.join(runDir, `step-${index + 1}-${sanitizeRunId(step.tool)}`);
|
|
1264
|
-
await mkdir(stepDir, { recursive: true });
|
|
1265
|
-
const stepEnvKeys = collectScenarioEnvKeys(step.env);
|
|
1266
|
-
const env = {
|
|
1267
|
-
...process.env,
|
|
1268
|
-
...step.env,
|
|
1269
|
-
AGENTPLANE_RUN_DIR: runDir,
|
|
1270
|
-
AGENTPLANE_STEP_DIR: stepDir,
|
|
1271
|
-
AGENTPLANE_RECIPES_CACHE_DIR: recipesCacheDir,
|
|
1272
|
-
AGENTPLANE_RECIPE_ID: recipeId,
|
|
1273
|
-
AGENTPLANE_SCENARIO_ID: scenarioId,
|
|
1274
|
-
AGENTPLANE_TOOL_ID: step.tool,
|
|
1275
|
-
};
|
|
1276
|
-
const startedAt = Date.now();
|
|
1277
|
-
const result = await executeRecipeTool({
|
|
1278
|
-
runtime,
|
|
1279
|
-
entrypoint: entrypointPath,
|
|
1280
|
-
args: step.args,
|
|
1281
|
-
cwd: recipeDir,
|
|
1282
|
-
env,
|
|
1283
|
-
});
|
|
1284
|
-
const durationMs = Date.now() - startedAt;
|
|
1285
|
-
await atomicWriteFile(path.join(stepDir, "stdout.log"), result.stdout, "utf8");
|
|
1286
|
-
await atomicWriteFile(path.join(stepDir, "stderr.log"), result.stderr, "utf8");
|
|
1287
|
-
stepsMeta.push({
|
|
1288
|
-
tool: step.tool,
|
|
1289
|
-
runtime,
|
|
1290
|
-
entrypoint,
|
|
1291
|
-
exitCode: result.exitCode,
|
|
1292
|
-
duration_ms: durationMs,
|
|
1293
|
-
});
|
|
1294
|
-
stepsReport.push({
|
|
1295
|
-
step: index + 1,
|
|
1296
|
-
tool: step.tool,
|
|
1297
|
-
runtime,
|
|
1298
|
-
entrypoint,
|
|
1299
|
-
args: redactArgs(step.args),
|
|
1300
|
-
env_keys: stepEnvKeys,
|
|
1301
|
-
exit_code: result.exitCode,
|
|
1302
|
-
duration_ms: durationMs,
|
|
1303
|
-
});
|
|
1304
|
-
if (result.exitCode !== 0) {
|
|
1305
|
-
const gitSummary = await getGitDiffSummary(resolved.gitRoot);
|
|
1306
|
-
await writeScenarioReport({
|
|
1307
|
-
runDir,
|
|
1308
|
-
recipeId,
|
|
1309
|
-
scenarioId,
|
|
1310
|
-
runId,
|
|
1311
|
-
startedAt: runStartedAt,
|
|
1312
|
-
status: "failed",
|
|
1313
|
-
steps: stepsReport,
|
|
1314
|
-
gitSummary,
|
|
1315
|
-
});
|
|
1316
|
-
await atomicWriteFile(path.join(runDir, "meta.json"), `${JSON.stringify({
|
|
1317
|
-
recipe: recipeId,
|
|
1318
|
-
scenario: scenarioId,
|
|
1319
|
-
run_id: runId,
|
|
1320
|
-
steps: stepsMeta,
|
|
1321
|
-
}, null, 2)}\n`, "utf8");
|
|
1322
|
-
throw new CliError({
|
|
1323
|
-
exitCode: result.exitCode,
|
|
1324
|
-
code: "E_INTERNAL",
|
|
1325
|
-
message: `Scenario step failed: ${step.tool}`,
|
|
1326
|
-
});
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
const gitSummary = await getGitDiffSummary(resolved.gitRoot);
|
|
1330
|
-
await writeScenarioReport({
|
|
1331
|
-
runDir,
|
|
1332
|
-
recipeId,
|
|
1333
|
-
scenarioId,
|
|
1334
|
-
runId,
|
|
1335
|
-
startedAt: runStartedAt,
|
|
1336
|
-
status: "success",
|
|
1337
|
-
steps: stepsReport,
|
|
1338
|
-
gitSummary,
|
|
1339
|
-
});
|
|
1340
|
-
await atomicWriteFile(path.join(runDir, "meta.json"), `${JSON.stringify({
|
|
1341
|
-
recipe: recipeId,
|
|
1342
|
-
scenario: scenarioId,
|
|
1343
|
-
run_id: runId,
|
|
1344
|
-
steps: stepsMeta,
|
|
1345
|
-
}, null, 2)}\n`, "utf8");
|
|
1346
|
-
process.stdout.write(`Run artifacts: ${path.relative(resolved.gitRoot, runDir)}\n`);
|
|
1347
|
-
return 0;
|
|
1348
|
-
}
|
|
1349
|
-
catch (err) {
|
|
1350
|
-
if (err instanceof CliError)
|
|
1351
|
-
throw err;
|
|
1352
|
-
throw mapCoreError(err, { command: "scenario run", root: opts.rootOverride ?? null });
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
902
|
async function cmdRecipeInfo(opts) {
|
|
1356
903
|
try {
|
|
1357
904
|
const installed = await readInstalledRecipesFile(resolveInstalledRecipesPath());
|
|
@@ -1908,56 +1455,3 @@ export async function cmdRecipes(opts) {
|
|
|
1908
1455
|
message: usageMessage(RECIPE_USAGE, RECIPE_USAGE_EXAMPLE),
|
|
1909
1456
|
});
|
|
1910
1457
|
}
|
|
1911
|
-
export async function cmdScenario(opts) {
|
|
1912
|
-
const subcommand = opts.command;
|
|
1913
|
-
if (!subcommand) {
|
|
1914
|
-
throw new CliError({
|
|
1915
|
-
exitCode: 2,
|
|
1916
|
-
code: "E_USAGE",
|
|
1917
|
-
message: usageMessage(SCENARIO_USAGE, SCENARIO_USAGE_EXAMPLE),
|
|
1918
|
-
});
|
|
1919
|
-
}
|
|
1920
|
-
if (subcommand === "list") {
|
|
1921
|
-
if (opts.args.length > 0) {
|
|
1922
|
-
throw new CliError({
|
|
1923
|
-
exitCode: 2,
|
|
1924
|
-
code: "E_USAGE",
|
|
1925
|
-
message: usageMessage(SCENARIO_USAGE, SCENARIO_USAGE_EXAMPLE),
|
|
1926
|
-
});
|
|
1927
|
-
}
|
|
1928
|
-
return await cmdScenarioList({ cwd: opts.cwd, rootOverride: opts.rootOverride });
|
|
1929
|
-
}
|
|
1930
|
-
if (subcommand === "info") {
|
|
1931
|
-
if (opts.args.length !== 1) {
|
|
1932
|
-
throw new CliError({
|
|
1933
|
-
exitCode: 2,
|
|
1934
|
-
code: "E_USAGE",
|
|
1935
|
-
message: usageMessage(SCENARIO_INFO_USAGE, SCENARIO_INFO_USAGE_EXAMPLE),
|
|
1936
|
-
});
|
|
1937
|
-
}
|
|
1938
|
-
return await cmdScenarioInfo({
|
|
1939
|
-
cwd: opts.cwd,
|
|
1940
|
-
rootOverride: opts.rootOverride,
|
|
1941
|
-
id: opts.args[0],
|
|
1942
|
-
});
|
|
1943
|
-
}
|
|
1944
|
-
if (subcommand === "run") {
|
|
1945
|
-
if (opts.args.length !== 1) {
|
|
1946
|
-
throw new CliError({
|
|
1947
|
-
exitCode: 2,
|
|
1948
|
-
code: "E_USAGE",
|
|
1949
|
-
message: usageMessage(SCENARIO_RUN_USAGE, SCENARIO_RUN_USAGE_EXAMPLE),
|
|
1950
|
-
});
|
|
1951
|
-
}
|
|
1952
|
-
return await cmdScenarioRun({
|
|
1953
|
-
cwd: opts.cwd,
|
|
1954
|
-
rootOverride: opts.rootOverride,
|
|
1955
|
-
id: opts.args[0],
|
|
1956
|
-
});
|
|
1957
|
-
}
|
|
1958
|
-
throw new CliError({
|
|
1959
|
-
exitCode: 2,
|
|
1960
|
-
code: "E_USAGE",
|
|
1961
|
-
message: usageMessage(SCENARIO_USAGE, SCENARIO_USAGE_EXAMPLE),
|
|
1962
|
-
});
|
|
1963
|
-
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scenario.d.ts","sourceRoot":"","sources":["../../src/commands/scenario.ts"],"names":[],"mappings":"AAojBA,wBAAsB,WAAW,CAAC,IAAI,EAAE;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB,GAAG,OAAO,CAAC,MAAM,CAAC,CAqDlB"}
|