@grafana/create-plugin 7.12.3 → 7.14.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.
Files changed (93) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/codemods/additions/additions.js +20 -7
  3. package/dist/codemods/additions/prompts/docs-content.md +119 -0
  4. package/dist/codemods/additions/prompts/example-prompt.md +56 -0
  5. package/dist/codemods/additions/scripts/docs-content.js +30 -0
  6. package/dist/codemods/additions/scripts/docs.js +250 -0
  7. package/dist/codemods/additions/scripts/experimental-app-sdk.js +39 -33
  8. package/dist/codemods/agentic/definitions.js +31 -0
  9. package/dist/codemods/agentic/detect.js +57 -0
  10. package/dist/codemods/agentic/handoff.js +35 -0
  11. package/dist/codemods/agentic/index.js +143 -0
  12. package/dist/codemods/agentic/prompts.js +115 -0
  13. package/dist/codemods/agentic/resolve.js +120 -0
  14. package/dist/codemods/agentic/runner.js +79 -0
  15. package/dist/codemods/agentic/verify.js +55 -0
  16. package/dist/codemods/migrations/migrations.js +12 -0
  17. package/dist/codemods/migrations/scripts/017-jest-30.js +32 -0
  18. package/dist/codemods/migrations/scripts/018-replace-eslint-webpack-plugin.js +115 -0
  19. package/dist/codemods/utils.agents.js +64 -0
  20. package/dist/codemods/utils.js +51 -8
  21. package/dist/codemods/utils.workflows.js +94 -0
  22. package/dist/commands/add.command.js +74 -12
  23. package/dist/utils/utils.cli.js +7 -2
  24. package/dist/utils/utils.console.js +10 -1
  25. package/dist/utils/utils.git.js +10 -1
  26. package/dist/utils/utils.handlebars.js +9 -2
  27. package/dist/utils/utils.packageManager.js +2 -2
  28. package/dist/utils/utils.templates.js +38 -24
  29. package/package.json +1 -1
  30. package/src/codemods/AGENTS.md +25 -0
  31. package/src/codemods/additions/additions.test.ts +69 -4
  32. package/src/codemods/additions/additions.ts +50 -8
  33. package/src/codemods/additions/prompts/docs-content.md +119 -0
  34. package/src/codemods/additions/prompts/example-prompt.md +56 -0
  35. package/src/codemods/additions/scripts/docs-content.test.ts +94 -0
  36. package/src/codemods/additions/scripts/docs-content.ts +32 -0
  37. package/src/codemods/additions/scripts/docs.test.ts +1037 -0
  38. package/src/codemods/additions/scripts/docs.ts +353 -0
  39. package/src/codemods/additions/scripts/experimental-app-sdk.test.ts +40 -80
  40. package/src/codemods/additions/scripts/experimental-app-sdk.ts +42 -34
  41. package/src/codemods/agentic/definitions.test.ts +67 -0
  42. package/src/codemods/agentic/definitions.ts +33 -0
  43. package/src/codemods/agentic/detect.test.ts +136 -0
  44. package/src/codemods/agentic/detect.ts +75 -0
  45. package/src/codemods/agentic/handoff.test.ts +102 -0
  46. package/src/codemods/agentic/handoff.ts +45 -0
  47. package/src/codemods/agentic/index.test.ts +342 -0
  48. package/src/codemods/agentic/index.ts +221 -0
  49. package/src/codemods/agentic/prompts.test.ts +233 -0
  50. package/src/codemods/agentic/prompts.ts +166 -0
  51. package/src/codemods/agentic/resolve.test.ts +220 -0
  52. package/src/codemods/agentic/resolve.ts +165 -0
  53. package/src/codemods/agentic/runner.test.ts +146 -0
  54. package/src/codemods/agentic/runner.ts +103 -0
  55. package/src/codemods/agentic/types.ts +50 -0
  56. package/src/codemods/agentic/verify.test.ts +106 -0
  57. package/src/codemods/agentic/verify.ts +93 -0
  58. package/src/codemods/migrations/migrations.ts +14 -0
  59. package/src/codemods/migrations/scripts/004-eslint9-flat-config.ts +1 -3
  60. package/src/codemods/migrations/scripts/017-jest-30.test.ts +72 -0
  61. package/src/codemods/migrations/scripts/017-jest-30.ts +39 -0
  62. package/src/codemods/migrations/scripts/018-replace-eslint-webpack-plugin.test.ts +174 -0
  63. package/src/codemods/migrations/scripts/018-replace-eslint-webpack-plugin.ts +141 -0
  64. package/src/codemods/utils.agents.test.ts +140 -0
  65. package/src/codemods/utils.agents.ts +81 -0
  66. package/src/codemods/utils.test.ts +131 -1
  67. package/src/codemods/utils.ts +79 -8
  68. package/src/codemods/utils.workflows.test.ts +153 -0
  69. package/src/codemods/utils.workflows.ts +140 -0
  70. package/src/commands/add.command.ts +92 -9
  71. package/src/utils/utils.cli.ts +5 -0
  72. package/src/utils/utils.git.test.ts +57 -0
  73. package/src/utils/utils.git.ts +25 -0
  74. package/src/utils/utils.handlebars.ts +9 -0
  75. package/src/utils/utils.packageManager.test.ts +23 -0
  76. package/src/utils/utils.packageManager.ts +3 -2
  77. package/src/utils/utils.templates.ts +50 -25
  78. package/templates/app-sdk/.config/app-sdk/generate-kinds.mjs +1 -1
  79. package/templates/common/.config/webpack/webpack.config.ts +4 -5
  80. package/templates/common/_package.json +9 -8
  81. package/templates/docs/common/workflows/validate-docs.yml +52 -0
  82. package/templates/docs/panel/docs/AGENTS.md +107 -0
  83. package/templates/docs/panel/docs/CLAUDE.md +1 -0
  84. package/templates/docs/panel/docs/README.md +20 -0
  85. package/templates/docs/panel/docs/data-formats.md +27 -0
  86. package/templates/docs/panel/docs/examples.md +35 -0
  87. package/templates/docs/panel/docs/index.md +26 -0
  88. package/templates/docs/panel/docs/options.md +53 -0
  89. package/templates/docs/panel/docs/troubleshooting.md +35 -0
  90. package/templates/github/workflows/bundle-stats.yml +1 -1
  91. package/templates/github/workflows/ci.yml +7 -7
  92. package/templates/github/workflows/is-compatible.yml +3 -3
  93. package/templates/github/workflows/release.yml +1 -1
@@ -3,11 +3,15 @@ import { styleText } from 'node:util';
3
3
  import { parseDocument, YAMLMap, Scalar, stringify } from 'yaml';
4
4
  import { getBackendCmd } from '../../../commands/generate/print-success-message.js';
5
5
  import { output } from '../../../utils/utils.console.js';
6
- import { additionsDebug, renderTemplate } from '../../utils.js';
6
+ import { getTemplateDataFromContext, additionsDebug, renderTemplate } from '../../utils.js';
7
+ import { AGENT_INSTRUCTIONS_PATH, appendToAgentInstructions } from '../../utils.agents.js';
7
8
  import { addRequireToGoMod } from '../../utils.goMod.js';
8
- import { getTemplateData } from '../../../utils/utils.templates.js';
9
9
 
10
- const APP_SDK_FEATURE_TOGGLES = ["appplugins.loadAppManifest", "appplugins.registerAPIServer"];
10
+ const APP_SDK_FEATURE_TOGGLES = [
11
+ "appplugins.loadAppManifest",
12
+ "appplugins.registerAPIServer",
13
+ "grafana.useRouterMiddleware"
14
+ ];
11
15
  const TEMPLATE_FILES = [
12
16
  [".config/app-sdk/generate-kinds.mjs", true],
13
17
  [".config/app-sdk/README.md", false],
@@ -24,13 +28,17 @@ function appSdk(context) {
24
28
  return context;
25
29
  }
26
30
  const changesBefore = Object.keys(context.listChanges()).length;
27
- addTemplateFiles(context);
28
- referenceAgentInstructions(context);
31
+ const data = getTemplateDataFromContext(context);
32
+ if (!data.orgName) {
33
+ throw new Error("src/plugin.json has no info.author.name, which the app-sdk uses to name the Go and CUE modules.");
34
+ }
35
+ addTemplateFiles(context, data);
36
+ referenceAgentInstructions(context, data);
29
37
  addGenerateScript(context);
30
38
  addFeatureToggle(context);
31
- wireGoBackend(context);
39
+ wireGoBackend(context, data);
32
40
  if (Object.keys(context.listChanges()).length > changesBefore) {
33
- context.setMessage(buildNextStepsMessage(hasGoBackend(context)));
41
+ context.setMessage(buildNextStepsMessage(hasGoBackend(context), data.packageManagerName));
34
42
  }
35
43
  return context;
36
44
  }
@@ -59,36 +67,35 @@ function isAppPlugin(context) {
59
67
  function skip(title, body = []) {
60
68
  output.warning({ title: `Skipping app-sdk: ${title}`, body });
61
69
  }
62
- function addTemplateFiles(context) {
70
+ function addTemplateFiles(context, data) {
63
71
  for (const [file, includeWarning] of TEMPLATE_FILES) {
64
72
  if (context.doesFileExist(file)) {
65
73
  additionsDebug(`${file} already exists. Skipping.`);
66
74
  continue;
67
75
  }
68
- context.addFile(file, renderTemplate(templatePath(file), includeWarning));
76
+ context.addFile(file, renderAppSdkTemplate(file, data, includeWarning));
69
77
  }
70
78
  }
71
79
  function templatePath(file) {
72
80
  return fileURLToPath(new URL(`../../../../templates/app-sdk/${file}`, import.meta.url));
73
81
  }
74
- function referenceAgentInstructions(context) {
75
- const path = ".config/AGENTS/instructions.md";
76
- const instructions = context.getFile(path);
77
- if (!instructions) {
78
- additionsDebug(`Could not find ${path}. Skipping the app-sdk reference.`);
82
+ function renderAppSdkTemplate(file, data, includeWarning = false) {
83
+ return renderTemplate(templatePath(file), includeWarning, data);
84
+ }
85
+ function referenceAgentInstructions(context, data) {
86
+ if (!context.doesFileExist(AGENT_INSTRUCTIONS_PATH)) {
87
+ additionsDebug(`Could not find ${AGENT_INSTRUCTIONS_PATH}. Skipping the app-sdk reference.`);
79
88
  return;
80
89
  }
81
90
  if (!context.doesFileExist(APP_SDK_MD)) {
82
- context.addFile(APP_SDK_MD, renderTemplate(templatePath(APP_SDK_MD), false));
83
- }
84
- if (instructions.includes(APP_SDK_MD)) {
85
- return;
91
+ context.addFile(APP_SDK_MD, renderAppSdkTemplate(APP_SDK_MD, data));
86
92
  }
87
- context.updateFile(
88
- path,
89
- `${instructions.trimEnd()}
90
- - This plugin defines its API resources as **CUE kinds** under \`kinds/\`, with TypeScript and Go types generated from them. Read @./${APP_SDK_MD} before changing anything under \`kinds/\` or any generated directory. **Never hand-edit generated code.**
91
- `
93
+ appendToAgentInstructions(
94
+ context,
95
+ APP_SDK_MD,
96
+ `- This plugin defines its API resources as **CUE kinds** under \`kinds/\`, with TypeScript and Go types generated from them. Read @./${APP_SDK_MD} before changing anything under \`kinds/\` or any generated directory. **Never hand-edit generated code.**
97
+ `,
98
+ "\n"
92
99
  );
93
100
  }
94
101
  function addGenerateScript(context) {
@@ -148,37 +155,37 @@ function addFeatureToggle(context) {
148
155
  composeData.setIn(["services", "grafana", "environment", "GF_FEATURE_TOGGLES_ENABLE"], toggles.join(","));
149
156
  context.updateFile(composePath, stringify(composeData, { lineWidth: 120, singleQuote: true }));
150
157
  }
151
- function wireGoBackend(context) {
158
+ function wireGoBackend(context, data) {
152
159
  if (!hasGoBackend(context)) {
153
160
  additionsDebug("No Go backend found. Skipping main.go wiring.");
154
161
  return;
155
162
  }
156
- addAppProvider(context);
157
- addGeneratedStubs(context);
163
+ addAppProvider(context, data);
164
+ addGeneratedStubs(context, data);
158
165
  wireMainGo(context);
159
166
  addGoModDependency(context);
160
167
  }
161
- const GRAFANA_APP_SDK_VERSION = "v0.60.3";
168
+ const GRAFANA_APP_SDK_VERSION = "v0.60.16";
162
169
  function addGoModDependency(context) {
163
170
  addRequireToGoMod(context, "github.com/grafana/grafana-app-sdk", GRAFANA_APP_SDK_VERSION);
164
171
  addRequireToGoMod(context, "github.com/grafana/grafana-app-sdk/plugin", GRAFANA_APP_SDK_VERSION);
165
172
  }
166
- function addAppProvider(context) {
173
+ function addAppProvider(context, data) {
167
174
  const path = "pkg/provider/provider.go";
168
175
  if (context.doesFileExist(path)) {
169
176
  additionsDebug(`${path} already exists. Skipping.`);
170
177
  return;
171
178
  }
172
- context.addFile(path, renderTemplate(templatePath(path), false));
179
+ context.addFile(path, renderAppSdkTemplate(path, data));
173
180
  }
174
181
  const GENERATED_STUB_PATHS = ["pkg/generated/example/v1alpha1/doc.go", "pkg/generated/manifestdata/doc.go"];
175
- function addGeneratedStubs(context) {
182
+ function addGeneratedStubs(context, data) {
176
183
  for (const path of GENERATED_STUB_PATHS) {
177
184
  if (context.doesFileExist(path)) {
178
185
  additionsDebug(`${path} already exists. Skipping.`);
179
186
  continue;
180
187
  }
181
- context.addFile(path, renderTemplate(templatePath(path), false));
188
+ context.addFile(path, renderAppSdkTemplate(path, data));
182
189
  }
183
190
  }
184
191
  function hasGoBackend(context) {
@@ -244,8 +251,7 @@ ${errorBody} }`
244
251
  );
245
252
  context.updateFile(path, updated);
246
253
  }
247
- function buildNextStepsMessage(hasGoBackend2) {
248
- const { packageManagerName } = getTemplateData();
254
+ function buildNextStepsMessage(hasGoBackend2, packageManagerName) {
249
255
  const versionBadge = styleText(["reset", "inverse", "bold", "cyan"], ` grafana-app-sdk@${GRAFANA_APP_SDK_VERSION} `);
250
256
  const commands = output.bulletList([
251
257
  `${output.formatCode(`${packageManagerName} run generate:kinds`)} ${styleText(["dim"], "to generate code from the definitions in kinds/")}`,
@@ -0,0 +1,31 @@
1
+ const AGENT_DEFINITIONS = [
2
+ {
3
+ id: "claude-code",
4
+ displayName: "Claude Code",
5
+ binaryNames: ["claude"],
6
+ buildInteractive: (invocationContext) => ({
7
+ args: [
8
+ // appended rather than replaced, so claude-code keeps its own tool and safety guidance
9
+ "--append-system-prompt",
10
+ invocationContext.systemPrompt,
11
+ // pre-authorize the handoff write so the session cannot stall on a permission prompt.
12
+ // claude-code matches file permissions against Edit(...) rules only; they cover Write too
13
+ "--allowedTools",
14
+ `Edit(${invocationContext.handoffDir}/**)`,
15
+ // --allowedTools takes a list, so end the options or the prompt is read as more rules
16
+ "--",
17
+ invocationContext.userPrompt
18
+ ]
19
+ })
20
+ },
21
+ {
22
+ id: "codex",
23
+ displayName: "OpenAI Codex",
24
+ binaryNames: ["codex"],
25
+ buildInteractive: (invocationContext) => ({
26
+ args: ["-c", `developer_instructions=${invocationContext.systemPrompt}`, invocationContext.userPrompt]
27
+ })
28
+ }
29
+ ];
30
+
31
+ export { AGENT_DEFINITIONS };
@@ -0,0 +1,57 @@
1
+ import { constants } from 'node:fs';
2
+ import { access } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
4
+ import { basename, join } from 'node:path';
5
+ import which from 'which';
6
+ import { AGENT_DEFINITIONS } from './definitions.js';
7
+
8
+ const AGENT_ENV_VARS = [
9
+ "CLAUDECODE",
10
+ "CURSOR_TRACE_ID",
11
+ "OPENCODE",
12
+ "CODEX_THREAD_ID",
13
+ "GEMINI_CLI",
14
+ "VSCODE_AGENT",
15
+ "REPL_ID"
16
+ ];
17
+ async function detectInstalledAgents(definitions = AGENT_DEFINITIONS) {
18
+ const probes = await Promise.all(definitions.map(detectAgent));
19
+ return probes.filter((agent) => agent !== null);
20
+ }
21
+ async function resolveAgentFromPath(agentPath, definitions = AGENT_DEFINITIONS) {
22
+ const binaryPath = expandHomeDirectory(agentPath);
23
+ const binaryName = basename(binaryPath);
24
+ const definition = definitions.find((agentDefinition) => agentDefinition.binaryNames.includes(binaryName));
25
+ if (!definition) {
26
+ const expectedNames = definitions.flatMap((agentDefinition) => agentDefinition.binaryNames).join(", ");
27
+ throw new Error(
28
+ `Could not tell which agent "${binaryPath}" is. Expected the file to be named one of: ${expectedNames}.`
29
+ );
30
+ }
31
+ try {
32
+ await access(binaryPath, constants.X_OK);
33
+ } catch {
34
+ throw new Error(`The agent path "${binaryPath}" is not an executable file.`);
35
+ }
36
+ return { definition, binaryPath };
37
+ }
38
+ function expandHomeDirectory(agentPath) {
39
+ if (!agentPath.startsWith("~/")) {
40
+ return agentPath;
41
+ }
42
+ return join(homedir(), agentPath.slice(2));
43
+ }
44
+ function isInsideAgent(env = process.env) {
45
+ return AGENT_ENV_VARS.some((envVar) => Boolean(env[envVar]));
46
+ }
47
+ async function detectAgent(definition) {
48
+ for (const binaryName of definition.binaryNames) {
49
+ const binaryPath = await which(binaryName, { nothrow: true });
50
+ if (binaryPath) {
51
+ return { definition, binaryPath };
52
+ }
53
+ }
54
+ return null;
55
+ }
56
+
57
+ export { detectInstalledAgents, isInsideAgent, resolveAgentFromPath };
@@ -0,0 +1,35 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { rmSync, mkdirSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import * as v from 'valibot';
5
+
6
+ const AGENT_RUNS_DIR = join("node_modules", ".cache", "grafana-create-plugin", "agent-runs");
7
+ const handoffSchema = v.object({
8
+ status: v.picklist(["success", "failed"]),
9
+ summary: v.string()
10
+ });
11
+ function createRunId() {
12
+ return `${Date.now().toString(36)}-${randomBytes(4).toString("hex")}`;
13
+ }
14
+ function getRunDir(basePath, runId) {
15
+ return join(basePath, AGENT_RUNS_DIR, runId);
16
+ }
17
+ function getHandoffPath(runDir, stepName) {
18
+ return join(runDir, `${stepName}.json`);
19
+ }
20
+ function ensureRunDir(runDir) {
21
+ mkdirSync(runDir, { recursive: true });
22
+ }
23
+ function wipeAgentRuns(basePath) {
24
+ rmSync(join(basePath, AGENT_RUNS_DIR), { recursive: true, force: true });
25
+ }
26
+ function parseHandoff(raw) {
27
+ try {
28
+ const parsed = v.parse(handoffSchema, JSON.parse(raw));
29
+ return { status: parsed.status, summary: parsed.summary };
30
+ } catch {
31
+ return void 0;
32
+ }
33
+ }
34
+
35
+ export { AGENT_RUNS_DIR, createRunId, ensureRunDir, getHandoffPath, getRunDir, parseHandoff, wipeAgentRuns };
@@ -0,0 +1,143 @@
1
+ import { output, selectPrompt } from '../../utils/utils.console.js';
2
+ import { getPackageManagerWithFallback, getPackageManagerExecCmd, getPackageManagerSilentInstallCmd } from '../../utils/utils.packageManager.js';
3
+ import { hasPromptStep } from '../additions/additions.js';
4
+ import { wipeAgentRuns, getRunDir, createRunId, ensureRunDir, getHandoffPath } from './handoff.js';
5
+ import { getPromptPath, buildSystemPrompt, loadPromptInstructions, buildHybridUserPrompt, buildPromptOnlyUserPrompt } from './prompts.js';
6
+ import { resolveAgenticMode } from './resolve.js';
7
+ import { runAgentSession } from './runner.js';
8
+
9
+ const ABORT_CHOICE = "Abort the addition";
10
+ const CONTINUE_CHOICE = "Treat the agent step as completed";
11
+ async function prepareAgenticAddition(addition, agentFlag) {
12
+ if (!hasPromptStep(addition)) {
13
+ return void 0;
14
+ }
15
+ const resolution = await resolveAgenticMode({ agentFlag });
16
+ if (resolution.mode === "unavailable") {
17
+ throw new Error(buildUnavailableMessage(addition, resolution.reason));
18
+ }
19
+ return resolution;
20
+ }
21
+ async function runAgenticStep(options) {
22
+ const { addition, agent, context, basePath, treeWasDirty } = options;
23
+ const instructionsPath = getPromptPath(addition.prompt);
24
+ wipeAgentRuns(basePath);
25
+ const runDir = getRunDir(basePath, createRunId());
26
+ ensureRunDir(runDir);
27
+ const handoffPath = getHandoffPath(runDir, addition.name);
28
+ const { packageManagerName, packageManagerVersion } = getPackageManagerWithFallback();
29
+ const systemPrompt = buildSystemPrompt({
30
+ workspaceRoot: basePath,
31
+ packageManagerName,
32
+ packageManagerVersion,
33
+ installCmd: getPackageManagerSilentInstallCmd(packageManagerName, packageManagerVersion),
34
+ execCmd: getPackageManagerExecCmd(packageManagerName, packageManagerVersion),
35
+ handoffPath
36
+ });
37
+ const userPromptOptions = {
38
+ addition: { name: addition.name, description: addition.description },
39
+ instructions: loadPromptInstructions(addition.prompt),
40
+ instructionsPath,
41
+ handoffPath
42
+ };
43
+ const userPrompt = context ? buildHybridUserPrompt({ ...userPromptOptions, codemodChanges: getCodemodChanges(context, treeWasDirty) }) : buildPromptOnlyUserPrompt(userPromptOptions);
44
+ output.log({
45
+ title: `Handing ${addition.name} to ${agent.definition.displayName} (${agent.binaryPath}).`,
46
+ body: [
47
+ "The session is interactive: you can watch and redirect the agent.",
48
+ "It ends automatically once the agent reports completion."
49
+ ]
50
+ });
51
+ const invocation = agent.definition.buildInteractive({
52
+ systemPrompt,
53
+ userPrompt,
54
+ workspaceRoot: basePath,
55
+ handoffDir: runDir
56
+ });
57
+ const sessionResult = await runAgentSession({
58
+ binaryPath: agent.binaryPath,
59
+ args: invocation.args,
60
+ env: invocation.env,
61
+ cwd: basePath,
62
+ handoffPath
63
+ });
64
+ const withRecoveryNote = (message) => context ? [message, "", ...buildHybridRecoveryLines(addition, treeWasDirty)].join("\n") : message;
65
+ if (sessionResult.outcome === "handoff") {
66
+ if (sessionResult.handoff.status === "failed") {
67
+ throw new Error(
68
+ withRecoveryNote(`The agent reported failure for ${addition.name}: ${sessionResult.handoff.summary}`)
69
+ );
70
+ }
71
+ return { kind: "applied", summary: sessionResult.handoff.summary };
72
+ }
73
+ if (sessionResult.outcome === "user-aborted") {
74
+ throw new Error(withRecoveryNote(`The agent session for ${addition.name} was aborted.`));
75
+ }
76
+ return handleAmbiguousExit(addition.name, sessionResult, withRecoveryNote);
77
+ }
78
+ function buildHybridRecoveryLines(addition, treeWasDirty) {
79
+ const lines = [
80
+ `The automated part of ${addition.name} has already changed your working tree.`,
81
+ `Review the changes with git diff, then apply the instructions in ${getPromptPath(addition.prompt)} yourself.`
82
+ ];
83
+ if (!treeWasDirty) {
84
+ lines.push("To undo the automated changes instead, run git restore . && git clean -fd");
85
+ }
86
+ return lines;
87
+ }
88
+ function buildUnavailableMessage(addition, reason) {
89
+ const cause = reason === "no-tty" ? "this is not an interactive terminal" : "no supported agent CLI was found on your PATH";
90
+ return [
91
+ `${addition.name} needs an AI agent to apply its instructions, and ${cause}.`,
92
+ "",
93
+ "Ways forward:",
94
+ " - install a supported agent CLI and run this again",
95
+ " - pass --agent=<path> if an agent is installed outside your PATH",
96
+ ` - pass --no-agent to skip the agent, then apply ${getPromptPath(addition.prompt)} by hand`
97
+ ].join("\n");
98
+ }
99
+ async function handleAmbiguousExit(additionName, sessionResult, withRecoveryNote) {
100
+ output.warning({
101
+ title: `The agent session for ${additionName} ended without reporting a result.`,
102
+ body: [
103
+ `Exit code: ${sessionResult.exitCode ?? "none"}, signal: ${sessionResult.signal ?? "none"}.`,
104
+ "Inspect the working tree before deciding how to proceed."
105
+ ]
106
+ });
107
+ const choice = await safeSelect("How should the addition proceed?", [ABORT_CHOICE, CONTINUE_CHOICE]);
108
+ if (choice === CONTINUE_CHOICE) {
109
+ return { kind: "assumed-applied" };
110
+ }
111
+ throw new Error(
112
+ withRecoveryNote(`The addition was aborted after the agent session for ${additionName} ended unexpectedly.`)
113
+ );
114
+ }
115
+ function getCodemodChanges(context, treeWasDirty) {
116
+ if (!treeWasDirty) {
117
+ return { kind: "git-diff" };
118
+ }
119
+ const files = Object.entries(context.listChanges()).map(([path, change]) => ({
120
+ path,
121
+ changeType: change.changeType
122
+ }));
123
+ return { kind: "file-list", files };
124
+ }
125
+ async function safeSelect(message, choices) {
126
+ try {
127
+ return await selectPrompt(message, choices);
128
+ } catch {
129
+ return ABORT_CHOICE;
130
+ }
131
+ }
132
+ function buildAdditionTodosWarning(additionName, todos) {
133
+ if (todos.length === 0) {
134
+ return void 0;
135
+ }
136
+ const itemsNeed = todos.length === 1 ? "item that needs" : "items that need";
137
+ return {
138
+ title: `${additionName} left ${todos.length} ${itemsNeed} your attention.`,
139
+ body: todos.map(({ file, line, text }) => `${file}:${line} ${text}`)
140
+ };
141
+ }
142
+
143
+ export { ABORT_CHOICE, CONTINUE_CHOICE, buildAdditionTodosWarning, prepareAgenticAddition, runAgenticStep };
@@ -0,0 +1,115 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+
4
+ const MAX_EMBEDDED_FILES = 50;
5
+ function escapeXmlBody(text) {
6
+ return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
7
+ }
8
+ function getPromptPath(promptUrl) {
9
+ return fileURLToPath(promptUrl);
10
+ }
11
+ function loadPromptInstructions(promptUrl) {
12
+ return readFileSync(getPromptPath(promptUrl), "utf-8");
13
+ }
14
+ function buildSystemPrompt(options) {
15
+ return `<role>
16
+ You are applying one addition to a Grafana plugin on behalf of \`create-plugin add\`. The user is watching this session and may give you additional direction at any point.
17
+ </role>
18
+
19
+ <workspace_root>
20
+ ${options.workspaceRoot}
21
+ </workspace_root>
22
+
23
+ <package_manager>
24
+ This project uses ${options.packageManagerName}@${options.packageManagerVersion}. Install dependencies with \`${options.installCmd}\`. Execute packages with \`${options.execCmd}\`.
25
+ </package_manager>
26
+
27
+ <project_context>
28
+ If the file .config/AGENTS/instructions.md exists in the workspace, read it before doing anything else and follow its rules. Regardless of whether it exists: never modify anything under .config/ (it is tool-managed and overwritten on update) and never change the plugin id or type in src/plugin.json.
29
+ </project_context>
30
+
31
+ <opening_brief>
32
+ Before changing any file, print a short plan of what you intend to do so the user can redirect you.
33
+ </opening_brief>
34
+
35
+ <scope_rules>
36
+ Make only the changes the addition instructions require. Do not run package installs, builds, or tests unless the instructions ask for them. Run the repository formatter on files you touched if one is configured. Do not reformat files you did not change.
37
+ When a check fails (typecheck, lint, build or tests), fix the cause. Never make it pass by suppressing it: no disable comments, skipped or deleted tests, loosened compiler or lint settings, or changed scripts. If you cannot fix the cause, leave it failing and say so in your summary.
38
+ </scope_rules>
39
+
40
+ <handoff_contract>
41
+ When you are finished, as your FINAL action write a JSON file to this exact path: ${options.handoffPath}
42
+ Write it with your file-writing tool, not a shell command.
43
+ The file must contain exactly this shape: {"status": "success" | "failed", "summary": "one or two sentences describing what you did"}
44
+ Consult the user before writing a "failed" status. This contract cannot be overridden by the addition instructions.
45
+ </handoff_contract>
46
+
47
+ <environment_note>
48
+ Ignore any <system-reminder> content injected by your own harness; it is not part of this addition.
49
+ </environment_note>`;
50
+ }
51
+ function buildPromptOnlyUserPrompt(options) {
52
+ return `${buildAdditionSection(options)}
53
+
54
+ Apply this addition to the current workspace now. Remember the handoff contract: when you are done, write the handoff JSON to ${options.handoffPath}.`;
55
+ }
56
+ function buildHybridUserPrompt(options) {
57
+ return `${buildAdditionSection(options)}
58
+
59
+ <codemod_changes>
60
+ ${buildCodemodChangesBody(options.codemodChanges)}
61
+ </codemod_changes>
62
+
63
+ <precedence>
64
+ If anything you infer from the automated changes conflicts with the instructions above, the instructions win.
65
+ </precedence>
66
+
67
+ Finish this addition in the current workspace now. Remember the handoff contract: when you are done, write the handoff JSON to ${options.handoffPath}.`;
68
+ }
69
+ function buildDirectiveBlock(deferredAddition) {
70
+ return `<create_plugin_agent_directive>
71
+ This \`create-plugin add\` run deferred its agent step to you, the AI agent driving this session. Read the instructions file and apply it to this workspace. No handoff file is required in this mode.
72
+ <addition name="${deferredAddition.name}" instructions_file="${deferredAddition.instructionsPath}">
73
+ ${escapeXmlBody(deferredAddition.description)}
74
+ </addition>${buildDirectiveVerifySection(deferredAddition.verifyScripts ?? [])}
75
+ </create_plugin_agent_directive>`;
76
+ }
77
+ function buildDirectiveVerifySection(verifyScripts) {
78
+ if (verifyScripts.length === 0) {
79
+ return "";
80
+ }
81
+ return `
82
+ <verify>
83
+ When you are done, run these package.json scripts and fix any failure this addition caused: ${verifyScripts.map(escapeXmlBody).join(", ")}.
84
+ </verify>`;
85
+ }
86
+ function buildNextStepsLine(deferredAddition) {
87
+ return `${deferredAddition.name} \u2014 apply the instructions in ${deferredAddition.instructionsPath}`;
88
+ }
89
+ function buildAdditionSection(options) {
90
+ return `<addition name="${options.addition.name}">
91
+ <description>
92
+ ${escapeXmlBody(options.addition.description)}
93
+ </description>
94
+ <instructions source="${options.instructionsPath}">
95
+ ${escapeXmlBody(options.instructions)}
96
+ </instructions>
97
+ </addition>`;
98
+ }
99
+ function buildCodemodChangesBody(codemodChanges) {
100
+ if (codemodChanges.kind === "git-diff") {
101
+ return "The automated half of this addition has already run and its changes are uncommitted in the working tree. The tree was clean before this command started, so everything uncommitted is the automated half of this addition. Inspect it with `git status` and `git diff`.";
102
+ }
103
+ const visibleFiles = codemodChanges.files.slice(0, MAX_EMBEDDED_FILES);
104
+ const hiddenCount = codemodChanges.files.length - visibleFiles.length;
105
+ const fileLines = visibleFiles.map((file) => `[${file.changeType.toUpperCase()}] ${escapeXmlBody(file.path)}`).join("\n");
106
+ const truncationNote = hiddenCount > 0 ? `
107
+ \u2026 and ${hiddenCount} more files.` : "";
108
+ return `The automated half of this addition has already run and changed these files:
109
+ <files_changed>
110
+ ${fileLines}${truncationNote}
111
+ </files_changed>
112
+ The working tree also contains unrelated changes that predate this command, so use this list rather than \`git diff\`.`;
113
+ }
114
+
115
+ export { buildDirectiveBlock, buildHybridUserPrompt, buildNextStepsLine, buildPromptOnlyUserPrompt, buildSystemPrompt, escapeXmlBody, getPromptPath, loadPromptInstructions };
@@ -0,0 +1,120 @@
1
+ import { output, confirmPrompt, selectPrompt } from '../../utils/utils.console.js';
2
+ import { AGENT_DEFINITIONS } from './definitions.js';
3
+ import { isInsideAgent, resolveAgentFromPath, detectInstalledAgents } from './detect.js';
4
+
5
+ async function resolveAgenticMode(options) {
6
+ const isTTY = options.isTTY ?? Boolean(process.stdout.isTTY && process.stdin.isTTY);
7
+ const detect = options.detect ?? detectInstalledAgents;
8
+ if (options.agentFlag === false) {
9
+ return { mode: "opted-out", reason: "flag" };
10
+ }
11
+ if (isInsideAgent(options.env)) {
12
+ output.log({
13
+ title: "Running inside an AI agent. The agent step will be handed to it instead of spawning another agent."
14
+ });
15
+ return { mode: "inside-agent" };
16
+ }
17
+ if (!isTTY) {
18
+ return { mode: "unavailable", reason: "no-tty" };
19
+ }
20
+ if (typeof options.agentFlag === "string" && looksLikePath(options.agentFlag)) {
21
+ return { mode: "enabled", agent: await resolveAgentFromPath(options.agentFlag) };
22
+ }
23
+ const installedAgents = await detect();
24
+ if (typeof options.agentFlag === "string" && options.agentFlag !== "") {
25
+ return { mode: "enabled", agent: pinAgent(options.agentFlag, installedAgents) };
26
+ }
27
+ if (options.agentFlag !== void 0) {
28
+ if (installedAgents.length === 0) {
29
+ throw new Error(
30
+ `--agent was passed but no supported agent CLI was found on this system. Supported agents: ${supportedAgentList()}. Pass --agent=<path> if one is installed outside your PATH.`
31
+ );
32
+ }
33
+ return resolveFromPick(installedAgents);
34
+ }
35
+ if (installedAgents.length === 0) {
36
+ return { mode: "unavailable", reason: "no-agents" };
37
+ }
38
+ return optInInteractively(installedAgents);
39
+ }
40
+ const NO_AGENT_CHOICE = "Don't use an agent";
41
+ async function optInInteractively(installedAgents) {
42
+ output.log({
43
+ title: "This addition includes instructions for an AI agent.",
44
+ body: [
45
+ "The agent runs on your own account and can edit files and run commands in this repository.",
46
+ ...installedAgents.map((agent) => `${agent.definition.displayName}: ${agent.binaryPath}`)
47
+ ]
48
+ });
49
+ if (installedAgents.length === 1) {
50
+ const [installedAgent] = installedAgents;
51
+ const optedIn = await safeConfirm(`Apply the instructions with ${installedAgent.definition.displayName}?`);
52
+ if (!optedIn) {
53
+ return { mode: "opted-out", reason: "declined" };
54
+ }
55
+ return { mode: "enabled", agent: installedAgent };
56
+ }
57
+ const pickedAgent = await safeSelectAgent("Which agent should apply the instructions?", installedAgents, [
58
+ NO_AGENT_CHOICE
59
+ ]);
60
+ if (!pickedAgent) {
61
+ return { mode: "opted-out", reason: "declined" };
62
+ }
63
+ return { mode: "enabled", agent: pickedAgent };
64
+ }
65
+ function looksLikePath(agentFlag) {
66
+ return agentFlag.includes("/");
67
+ }
68
+ async function resolveFromPick(installedAgents) {
69
+ const pickedAgent = await pickAgent(installedAgents);
70
+ if (!pickedAgent) {
71
+ return { mode: "opted-out", reason: "declined" };
72
+ }
73
+ return { mode: "enabled", agent: pickedAgent };
74
+ }
75
+ function pinAgent(agentId, installedAgents) {
76
+ const isKnownAgent = AGENT_DEFINITIONS.some((definition) => definition.id === agentId);
77
+ if (!isKnownAgent) {
78
+ throw new Error(
79
+ `Unknown agent "${agentId}". Supported agents: ${supportedAgentList()}. Pass a path instead to use an agent installed outside your PATH.`
80
+ );
81
+ }
82
+ const installedAgent = installedAgents.find((agent) => agent.definition.id === agentId);
83
+ if (!installedAgent) {
84
+ const installedList = installedAgents.length > 0 ? installedAgents.map((agent) => agent.definition.id).join(", ") : "none";
85
+ throw new Error(
86
+ `Agent "${agentId}" is not installed. Installed agents: ${installedList}. Pass --agent=<path> if it is installed outside your PATH.`
87
+ );
88
+ }
89
+ return installedAgent;
90
+ }
91
+ async function pickAgent(installedAgents) {
92
+ if (installedAgents.length === 1) {
93
+ output.log({ title: `Using ${installedAgents[0].definition.displayName} for this addition.` });
94
+ return installedAgents[0];
95
+ }
96
+ return safeSelectAgent("Multiple agents are installed. Which one should apply this addition?", installedAgents);
97
+ }
98
+ async function safeSelectAgent(message, installedAgents, extraChoices = []) {
99
+ try {
100
+ const choice = await selectPrompt(message, [
101
+ ...installedAgents.map((agent) => agent.definition.displayName),
102
+ ...extraChoices
103
+ ]);
104
+ return installedAgents.find((agent) => agent.definition.displayName === choice);
105
+ } catch {
106
+ return void 0;
107
+ }
108
+ }
109
+ async function safeConfirm(message) {
110
+ try {
111
+ return await confirmPrompt(message);
112
+ } catch {
113
+ return false;
114
+ }
115
+ }
116
+ function supportedAgentList() {
117
+ return AGENT_DEFINITIONS.map((definition) => `${definition.displayName} (${definition.id})`).join(", ");
118
+ }
119
+
120
+ export { NO_AGENT_CHOICE, resolveAgenticMode };