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.
Files changed (53) hide show
  1. package/assets/AGENTS.md +13 -2
  2. package/dist/backends/task-backend.d.ts +12 -0
  3. package/dist/backends/task-backend.d.ts.map +1 -1
  4. package/dist/backends/task-backend.js +41 -4
  5. package/dist/cli/command-guide.d.ts.map +1 -1
  6. package/dist/cli/command-guide.js +6 -7
  7. package/dist/cli/run-cli.d.ts.map +1 -1
  8. package/dist/cli/run-cli.js +57 -2
  9. package/dist/commands/guard/index.d.ts +24 -3
  10. package/dist/commands/guard/index.d.ts.map +1 -1
  11. package/dist/commands/guard/index.js +175 -61
  12. package/dist/commands/hooks/index.d.ts.map +1 -1
  13. package/dist/commands/hooks/index.js +39 -29
  14. package/dist/commands/recipes.d.ts +75 -6
  15. package/dist/commands/recipes.d.ts.map +1 -1
  16. package/dist/commands/recipes.js +15 -521
  17. package/dist/commands/scenario.d.ts +7 -0
  18. package/dist/commands/scenario.d.ts.map +1 -0
  19. package/dist/commands/scenario.js +501 -0
  20. package/dist/commands/shared/task-backend.d.ts +19 -3
  21. package/dist/commands/shared/task-backend.d.ts.map +1 -1
  22. package/dist/commands/shared/task-backend.js +13 -5
  23. package/dist/commands/task/block.d.ts.map +1 -1
  24. package/dist/commands/task/block.js +22 -16
  25. package/dist/commands/task/comment.d.ts.map +1 -1
  26. package/dist/commands/task/comment.js +9 -2
  27. package/dist/commands/task/finish.d.ts.map +1 -1
  28. package/dist/commands/task/finish.js +36 -26
  29. package/dist/commands/task/set-status.d.ts.map +1 -1
  30. package/dist/commands/task/set-status.js +18 -4
  31. package/dist/commands/task/shared.d.ts +3 -2
  32. package/dist/commands/task/shared.d.ts.map +1 -1
  33. package/dist/commands/task/shared.js +18 -28
  34. package/dist/commands/task/start.d.ts.map +1 -1
  35. package/dist/commands/task/start.js +24 -18
  36. package/dist/commands/task/verify-record.d.ts.map +1 -1
  37. package/dist/commands/task/verify-record.js +8 -1
  38. package/dist/shared/git-log.d.ts +5 -0
  39. package/dist/shared/git-log.d.ts.map +1 -0
  40. package/dist/shared/git-log.js +14 -0
  41. package/dist/shared/git-path.d.ts +3 -0
  42. package/dist/shared/git-path.d.ts.map +1 -0
  43. package/dist/shared/git-path.js +30 -0
  44. package/dist/shared/guards.d.ts +2 -0
  45. package/dist/shared/guards.d.ts.map +1 -0
  46. package/dist/shared/guards.js +3 -0
  47. package/dist/shared/protected-paths.d.ts +12 -0
  48. package/dist/shared/protected-paths.d.ts.map +1 -0
  49. package/dist/shared/protected-paths.js +51 -0
  50. package/dist/shared/strings.d.ts +2 -0
  51. package/dist/shared/strings.d.ts.map +1 -0
  52. package/dist/shared/strings.js +14 -0
  53. package/package.json +2 -2
@@ -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, realpath, rename, rm } from "node:fs/promises";
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
- const execFileAsync = promisify(execFile);
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,7 @@
1
+ export declare function cmdScenario(opts: {
2
+ cwd: string;
3
+ rootOverride?: string;
4
+ command?: string;
5
+ args: string[];
6
+ }): Promise<number>;
7
+ //# sourceMappingURL=scenario.d.ts.map
@@ -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"}