@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.
- package/CHANGELOG.md +31 -0
- package/dist/codemods/additions/additions.js +20 -7
- package/dist/codemods/additions/prompts/docs-content.md +119 -0
- package/dist/codemods/additions/prompts/example-prompt.md +56 -0
- package/dist/codemods/additions/scripts/docs-content.js +30 -0
- package/dist/codemods/additions/scripts/docs.js +250 -0
- package/dist/codemods/additions/scripts/experimental-app-sdk.js +39 -33
- package/dist/codemods/agentic/definitions.js +31 -0
- package/dist/codemods/agentic/detect.js +57 -0
- package/dist/codemods/agentic/handoff.js +35 -0
- package/dist/codemods/agentic/index.js +143 -0
- package/dist/codemods/agentic/prompts.js +115 -0
- package/dist/codemods/agentic/resolve.js +120 -0
- package/dist/codemods/agentic/runner.js +79 -0
- package/dist/codemods/agentic/verify.js +55 -0
- package/dist/codemods/migrations/migrations.js +12 -0
- package/dist/codemods/migrations/scripts/017-jest-30.js +32 -0
- package/dist/codemods/migrations/scripts/018-replace-eslint-webpack-plugin.js +115 -0
- package/dist/codemods/utils.agents.js +64 -0
- package/dist/codemods/utils.js +51 -8
- package/dist/codemods/utils.workflows.js +94 -0
- package/dist/commands/add.command.js +74 -12
- package/dist/utils/utils.cli.js +7 -2
- package/dist/utils/utils.console.js +10 -1
- package/dist/utils/utils.git.js +10 -1
- package/dist/utils/utils.handlebars.js +9 -2
- package/dist/utils/utils.packageManager.js +2 -2
- package/dist/utils/utils.templates.js +38 -24
- package/package.json +1 -1
- package/src/codemods/AGENTS.md +25 -0
- package/src/codemods/additions/additions.test.ts +69 -4
- package/src/codemods/additions/additions.ts +50 -8
- package/src/codemods/additions/prompts/docs-content.md +119 -0
- package/src/codemods/additions/prompts/example-prompt.md +56 -0
- package/src/codemods/additions/scripts/docs-content.test.ts +94 -0
- package/src/codemods/additions/scripts/docs-content.ts +32 -0
- package/src/codemods/additions/scripts/docs.test.ts +1037 -0
- package/src/codemods/additions/scripts/docs.ts +353 -0
- package/src/codemods/additions/scripts/experimental-app-sdk.test.ts +40 -80
- package/src/codemods/additions/scripts/experimental-app-sdk.ts +42 -34
- package/src/codemods/agentic/definitions.test.ts +67 -0
- package/src/codemods/agentic/definitions.ts +33 -0
- package/src/codemods/agentic/detect.test.ts +136 -0
- package/src/codemods/agentic/detect.ts +75 -0
- package/src/codemods/agentic/handoff.test.ts +102 -0
- package/src/codemods/agentic/handoff.ts +45 -0
- package/src/codemods/agentic/index.test.ts +342 -0
- package/src/codemods/agentic/index.ts +221 -0
- package/src/codemods/agentic/prompts.test.ts +233 -0
- package/src/codemods/agentic/prompts.ts +166 -0
- package/src/codemods/agentic/resolve.test.ts +220 -0
- package/src/codemods/agentic/resolve.ts +165 -0
- package/src/codemods/agentic/runner.test.ts +146 -0
- package/src/codemods/agentic/runner.ts +103 -0
- package/src/codemods/agentic/types.ts +50 -0
- package/src/codemods/agentic/verify.test.ts +106 -0
- package/src/codemods/agentic/verify.ts +93 -0
- package/src/codemods/migrations/migrations.ts +14 -0
- package/src/codemods/migrations/scripts/004-eslint9-flat-config.ts +1 -3
- package/src/codemods/migrations/scripts/017-jest-30.test.ts +72 -0
- package/src/codemods/migrations/scripts/017-jest-30.ts +39 -0
- package/src/codemods/migrations/scripts/018-replace-eslint-webpack-plugin.test.ts +174 -0
- package/src/codemods/migrations/scripts/018-replace-eslint-webpack-plugin.ts +141 -0
- package/src/codemods/utils.agents.test.ts +140 -0
- package/src/codemods/utils.agents.ts +81 -0
- package/src/codemods/utils.test.ts +131 -1
- package/src/codemods/utils.ts +79 -8
- package/src/codemods/utils.workflows.test.ts +153 -0
- package/src/codemods/utils.workflows.ts +140 -0
- package/src/commands/add.command.ts +92 -9
- package/src/utils/utils.cli.ts +5 -0
- package/src/utils/utils.git.test.ts +57 -0
- package/src/utils/utils.git.ts +25 -0
- package/src/utils/utils.handlebars.ts +9 -0
- package/src/utils/utils.packageManager.test.ts +23 -0
- package/src/utils/utils.packageManager.ts +3 -2
- package/src/utils/utils.templates.ts +50 -25
- package/templates/app-sdk/.config/app-sdk/generate-kinds.mjs +1 -1
- package/templates/common/.config/webpack/webpack.config.ts +4 -5
- package/templates/common/_package.json +9 -8
- package/templates/docs/common/workflows/validate-docs.yml +52 -0
- package/templates/docs/panel/docs/AGENTS.md +107 -0
- package/templates/docs/panel/docs/CLAUDE.md +1 -0
- package/templates/docs/panel/docs/README.md +20 -0
- package/templates/docs/panel/docs/data-formats.md +27 -0
- package/templates/docs/panel/docs/examples.md +35 -0
- package/templates/docs/panel/docs/index.md +26 -0
- package/templates/docs/panel/docs/options.md +53 -0
- package/templates/docs/panel/docs/troubleshooting.md +35 -0
- package/templates/github/workflows/bundle-stats.yml +1 -1
- package/templates/github/workflows/ci.yml +7 -7
- package/templates/github/workflows/is-compatible.yml +3 -3
- package/templates/github/workflows/release.yml +1 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { parseHandoff } from './handoff.js';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_POLL_INTERVAL_MS = 500;
|
|
6
|
+
const DEFAULT_KILL_GRACE_MS = 5e3;
|
|
7
|
+
function runAgentSession(options) {
|
|
8
|
+
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
9
|
+
const killGraceMs = options.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
const child = spawn(options.binaryPath, options.args, {
|
|
12
|
+
stdio: "inherit",
|
|
13
|
+
cwd: options.cwd,
|
|
14
|
+
env: { ...process.env, ...options.env }
|
|
15
|
+
});
|
|
16
|
+
let settled = false;
|
|
17
|
+
let pendingHandoff;
|
|
18
|
+
let killTimer;
|
|
19
|
+
const pollTimer = setInterval(async () => {
|
|
20
|
+
const handoff = await readHandoff(options.handoffPath);
|
|
21
|
+
if (!handoff || settled || pendingHandoff) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
pendingHandoff = handoff;
|
|
25
|
+
clearInterval(pollTimer);
|
|
26
|
+
child.kill("SIGINT");
|
|
27
|
+
killTimer = setTimeout(() => child.kill("SIGKILL"), killGraceMs);
|
|
28
|
+
}, pollIntervalMs);
|
|
29
|
+
function settle(result) {
|
|
30
|
+
if (settled) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
settled = true;
|
|
34
|
+
clearInterval(pollTimer);
|
|
35
|
+
if (killTimer) {
|
|
36
|
+
clearTimeout(killTimer);
|
|
37
|
+
}
|
|
38
|
+
resetTerminal();
|
|
39
|
+
resolve(result);
|
|
40
|
+
}
|
|
41
|
+
child.on("error", () => {
|
|
42
|
+
settle({ outcome: "ambiguous-exit", exitCode: null, signal: null });
|
|
43
|
+
});
|
|
44
|
+
child.on("exit", async (exitCode, signal) => {
|
|
45
|
+
if (pendingHandoff) {
|
|
46
|
+
settle({ outcome: "handoff", handoff: pendingHandoff });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const handoff = await readHandoff(options.handoffPath);
|
|
50
|
+
if (handoff) {
|
|
51
|
+
settle({ outcome: "handoff", handoff });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (isUserAbort(exitCode, signal)) {
|
|
55
|
+
settle({ outcome: "user-aborted" });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
settle({ outcome: "ambiguous-exit", exitCode, signal });
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async function readHandoff(handoffPath) {
|
|
63
|
+
try {
|
|
64
|
+
const raw = await readFile(handoffPath, "utf-8");
|
|
65
|
+
return parseHandoff(raw);
|
|
66
|
+
} catch {
|
|
67
|
+
return void 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function isUserAbort(exitCode, signal) {
|
|
71
|
+
return exitCode === 130 || exitCode === 143 || signal === "SIGINT" || signal === "SIGTERM";
|
|
72
|
+
}
|
|
73
|
+
function resetTerminal() {
|
|
74
|
+
if (process.stdout.isTTY) {
|
|
75
|
+
process.stdout.write("\x1B[0m\n");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export { runAgentSession };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
function runVerifyScripts(options) {
|
|
6
|
+
const { scripts, cwd, packageManagerName, runScript = runPackageScript } = options;
|
|
7
|
+
const availableScripts = readPackageScripts(cwd);
|
|
8
|
+
const results = [];
|
|
9
|
+
for (const script of scripts) {
|
|
10
|
+
if (!availableScripts.has(script)) {
|
|
11
|
+
results.push({ script, outcome: "missing" });
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
const hasPassed = runScript(packageManagerName, script, cwd);
|
|
15
|
+
results.push({ script, outcome: hasPassed ? "passed" : "failed" });
|
|
16
|
+
}
|
|
17
|
+
return results;
|
|
18
|
+
}
|
|
19
|
+
function buildVerifyWarning(additionName, packageManagerName, results) {
|
|
20
|
+
const failedResults = results.filter((result) => result.outcome !== "passed");
|
|
21
|
+
if (failedResults.length === 0) {
|
|
22
|
+
return void 0;
|
|
23
|
+
}
|
|
24
|
+
const failedCount = failedResults.filter((result) => result.outcome === "failed").length;
|
|
25
|
+
const title = failedCount > 0 ? `${additionName} was applied, but ${failedCount} of its checks failed.` : `${additionName} was applied, but some of its checks could not run.`;
|
|
26
|
+
return {
|
|
27
|
+
title,
|
|
28
|
+
body: [
|
|
29
|
+
...failedResults.map(
|
|
30
|
+
({ script, outcome }) => outcome === "failed" ? `${packageManagerName} run ${script} failed` : `${packageManagerName} run ${script} skipped: package.json has no "${script}" script`
|
|
31
|
+
),
|
|
32
|
+
"Nothing was committed. Review the changes with git diff before you commit them."
|
|
33
|
+
]
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function readPackageScripts(cwd) {
|
|
37
|
+
try {
|
|
38
|
+
const packageJson = JSON.parse(
|
|
39
|
+
readFileSync(join(cwd, "package.json"), "utf-8")
|
|
40
|
+
);
|
|
41
|
+
return new Set(Object.keys(packageJson.scripts ?? {}));
|
|
42
|
+
} catch {
|
|
43
|
+
return /* @__PURE__ */ new Set();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function runPackageScript(packageManagerName, script, cwd) {
|
|
47
|
+
try {
|
|
48
|
+
execSync(`${packageManagerName} run ${script}`, { cwd, stdio: "inherit" });
|
|
49
|
+
return true;
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { buildVerifyWarning, runVerifyScripts };
|
|
@@ -96,6 +96,18 @@ var defaultMigrations = [
|
|
|
96
96
|
version: "7.12.2",
|
|
97
97
|
description: "Fix @grafana/ui Combobox dropdown crashing in Jest: add an IntersectionObserver polyfill stub so ScrollIndicators (mounted when the dropdown opens) no longer throws ReferenceError in jsdom.",
|
|
98
98
|
scriptPath: import.meta.resolve("./scripts/016-jest-intersection-observer.js")
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "017-jest-30",
|
|
102
|
+
version: "7.13.0",
|
|
103
|
+
description: "Update Jest to v30, which replaces micromatch with picomatch and drops the braces dependency flagged by vulnerability scanners.",
|
|
104
|
+
scriptPath: import.meta.resolve("./scripts/017-jest-30.js")
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: "018-replace-eslint-webpack-plugin",
|
|
108
|
+
version: "7.13.0",
|
|
109
|
+
description: "Replace eslint-webpack-plugin with diagnostics-webpack-plugin in the webpack config. eslint-webpack-plugin depends on micromatch, which pulls in the braces dependency flagged by vulnerability scanners.",
|
|
110
|
+
scriptPath: import.meta.resolve("./scripts/018-replace-eslint-webpack-plugin.js")
|
|
99
111
|
}
|
|
100
112
|
// Do not use LEGACY_UPDATE_CUTOFF_VERSION for new migrations. It is only used above to force migrations to run
|
|
101
113
|
// for those written before the switch to updates as migrations.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { readJsonFile, addDependenciesToPackageJson } from '../../utils.js';
|
|
2
|
+
import { output } from '../../../utils/utils.console.js';
|
|
3
|
+
|
|
4
|
+
const JEST_30_DEPENDENCIES = {
|
|
5
|
+
jest: "^30.5.0",
|
|
6
|
+
"jest-environment-jsdom": "^30.5.0",
|
|
7
|
+
"@types/jest": "^30.0.0"
|
|
8
|
+
};
|
|
9
|
+
function migrate(context) {
|
|
10
|
+
if (!context.doesFileExist("package.json")) {
|
|
11
|
+
return context;
|
|
12
|
+
}
|
|
13
|
+
const packageJson = readJsonFile(context, "package.json");
|
|
14
|
+
const installed = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
15
|
+
const devDependencies = Object.fromEntries(
|
|
16
|
+
Object.entries(JEST_30_DEPENDENCIES).filter(([name]) => Boolean(installed[name]))
|
|
17
|
+
);
|
|
18
|
+
const before = context.getFile("package.json");
|
|
19
|
+
addDependenciesToPackageJson(context, {}, devDependencies);
|
|
20
|
+
if (context.getFile("package.json") !== before) {
|
|
21
|
+
output.warning({
|
|
22
|
+
title: "Jest was updated to v30",
|
|
23
|
+
body: [
|
|
24
|
+
"Jest 30 is a major release, so existing tests may need small fixes (jsdom 26, removed matcher aliases like `toBeCalled`).",
|
|
25
|
+
"See https://jestjs.io/docs/upgrading-to-jest30 if `npm run test:ci` fails after the update."
|
|
26
|
+
]
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return context;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { migrate as default };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import * as recast from 'recast';
|
|
3
|
+
import { addDependenciesToPackageJson, removeDependenciesFromPackageJson, migrationsDebug } from '../../utils.js';
|
|
4
|
+
import { parseAsTypescript } from '../../utils.ast.js';
|
|
5
|
+
import { output } from '../../../utils/utils.console.js';
|
|
6
|
+
|
|
7
|
+
const OLD_PACKAGE_NAME = "eslint-webpack-plugin";
|
|
8
|
+
const NEW_PACKAGE_NAME = "diagnostics-webpack-plugin";
|
|
9
|
+
const WEBPACK_CONFIG_PATH = join(".config", "webpack", "webpack.config.ts");
|
|
10
|
+
const CONFIG_PATHS = [
|
|
11
|
+
WEBPACK_CONFIG_PATH,
|
|
12
|
+
join(".config", "rspack", "rspack.config.ts"),
|
|
13
|
+
"webpack.config.ts",
|
|
14
|
+
"rspack.config.ts"
|
|
15
|
+
];
|
|
16
|
+
const SCAFFOLDED_OPTIONS = [
|
|
17
|
+
"{extensions:['.ts','.tsx'],lintDirtyModulesOnly:Boolean(env.development)}",
|
|
18
|
+
"{extensions:['.ts','.tsx'],lintDirtyModulesOnly:Boolean(env.development),failOnError:Boolean(env.production)}"
|
|
19
|
+
];
|
|
20
|
+
const NEW_IMPORT = `import DiagnosticsPlugin from '${NEW_PACKAGE_NAME}';`;
|
|
21
|
+
const NEW_PLUGIN = `new DiagnosticsPlugin({
|
|
22
|
+
lintOnStart: false, // don't lint on start, only lint changed files
|
|
23
|
+
checks: [{ use: 'eslint', extensions: ['ts', 'tsx'] }],
|
|
24
|
+
})`;
|
|
25
|
+
function migrate(context) {
|
|
26
|
+
if (!replaceESLintPlugin(context)) {
|
|
27
|
+
return context;
|
|
28
|
+
}
|
|
29
|
+
if (context.doesFileExist("package.json")) {
|
|
30
|
+
addDependenciesToPackageJson(context, {}, { [NEW_PACKAGE_NAME]: "^1.0.1", webpack: "^5.106.0" });
|
|
31
|
+
const isStillUsed = CONFIG_PATHS.some(
|
|
32
|
+
(path) => context.doesFileExist(path) && (context.getFile(path) ?? "").includes(OLD_PACKAGE_NAME)
|
|
33
|
+
);
|
|
34
|
+
if (!isStillUsed) {
|
|
35
|
+
removeDependenciesFromPackageJson(context, [OLD_PACKAGE_NAME], [OLD_PACKAGE_NAME]);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return context;
|
|
39
|
+
}
|
|
40
|
+
function replaceESLintPlugin(context) {
|
|
41
|
+
if (!context.doesFileExist(WEBPACK_CONFIG_PATH)) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const source = context.getFile(WEBPACK_CONFIG_PATH);
|
|
45
|
+
if (!source || !source.includes(OLD_PACKAGE_NAME)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
const parsed = parseAsTypescript(source);
|
|
49
|
+
if (!parsed.success) {
|
|
50
|
+
migrationsDebug(`Failed to parse ${WEBPACK_CONFIG_PATH}. Error: ${parsed.error.message}`);
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
const lineOffsets = [0];
|
|
54
|
+
for (const line of source.split("\n")) {
|
|
55
|
+
lineOffsets.push(lineOffsets[lineOffsets.length - 1] + line.length + 1);
|
|
56
|
+
}
|
|
57
|
+
const toOffset = ({ line, column }) => lineOffsets[line - 1] + column;
|
|
58
|
+
const localNames = /* @__PURE__ */ new Set();
|
|
59
|
+
const replacements = [];
|
|
60
|
+
let hasCustomOptions = false;
|
|
61
|
+
recast.visit(parsed.ast, {
|
|
62
|
+
visitImportDeclaration(path) {
|
|
63
|
+
if (path.node.source.value === OLD_PACKAGE_NAME) {
|
|
64
|
+
for (const specifier of path.node.specifiers ?? []) {
|
|
65
|
+
if (specifier.local?.name) {
|
|
66
|
+
localNames.add(String(specifier.local.name));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
replacements.push({ node: path.node, text: NEW_IMPORT });
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return this.traverse(path);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
recast.visit(parsed.ast, {
|
|
76
|
+
visitNewExpression(path) {
|
|
77
|
+
const { node } = path;
|
|
78
|
+
if (node.callee.type === "Identifier" && localNames.has(node.callee.name)) {
|
|
79
|
+
const [options] = node.arguments;
|
|
80
|
+
const optionsText = options?.loc ? source.slice(toOffset(options.loc.start), toOffset(options.loc.end)) : "";
|
|
81
|
+
const normalized = optionsText.replace(/\/\/.*$/gm, "").replace(/\s+/g, "").replace(/,}/g, "}");
|
|
82
|
+
if (node.arguments.length !== 1 || !SCAFFOLDED_OPTIONS.includes(normalized)) {
|
|
83
|
+
hasCustomOptions = true;
|
|
84
|
+
}
|
|
85
|
+
replacements.push({ node, text: NEW_PLUGIN });
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
return this.traverse(path);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
if (hasCustomOptions) {
|
|
92
|
+
output.warning({
|
|
93
|
+
title: `Skipping the ${OLD_PACKAGE_NAME} replacement in ${WEBPACK_CONFIG_PATH}`,
|
|
94
|
+
body: [
|
|
95
|
+
`The ${OLD_PACKAGE_NAME} options differ from the scaffolded defaults, so the config was left as is.`,
|
|
96
|
+
`${OLD_PACKAGE_NAME} depends on micromatch, which pulls in braces. To drop it, switch to ${NEW_PACKAGE_NAME} by hand.`
|
|
97
|
+
]
|
|
98
|
+
});
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
let updated = source;
|
|
102
|
+
const sorted = replacements.filter(({ node }) => node.loc).sort(
|
|
103
|
+
(a, b) => b.node.loc.start.line - a.node.loc.start.line || b.node.loc.start.column - a.node.loc.start.column
|
|
104
|
+
);
|
|
105
|
+
for (const { node, text } of sorted) {
|
|
106
|
+
const { start, end } = node.loc;
|
|
107
|
+
const indent = " ".repeat(start.column);
|
|
108
|
+
updated = updated.slice(0, toOffset(start)) + text.replaceAll("\n", `
|
|
109
|
+
${indent}`) + updated.slice(toOffset(end));
|
|
110
|
+
}
|
|
111
|
+
context.updateFile(WEBPACK_CONFIG_PATH, updated);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export { migrate as default };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { addTemplateFolder, codemodsDebug } from './utils.js';
|
|
4
|
+
|
|
5
|
+
const AGENT_INSTRUCTIONS_PATH = ".config/AGENTS/instructions.md";
|
|
6
|
+
const ROOT_AGENT_FILES = ["AGENTS.md", "CLAUDE.md", "GEMINI.md"];
|
|
7
|
+
const INSTRUCTIONS_POINTER = `## Project knowledge
|
|
8
|
+
This repository contains a **Grafana plugin**. You must Read @./${AGENT_INSTRUCTIONS_PATH} before doing changes.
|
|
9
|
+
`;
|
|
10
|
+
const IMPORTS_INSTRUCTIONS = /@(\.\/)?\.config\/AGENTS\/instructions\.md/;
|
|
11
|
+
const IMPORTS_AGENTS_MD = /(^|\s)@(\.\/)?AGENTS\.md\b/m;
|
|
12
|
+
function ensureRootAgentFiles(context, templateDir) {
|
|
13
|
+
const existing = ROOT_AGENT_FILES.filter((file) => context.doesFileExist(file));
|
|
14
|
+
for (const file of ROOT_AGENT_FILES) {
|
|
15
|
+
if (!existing.includes(file)) {
|
|
16
|
+
context.addFile(file, readFileSync(join(templateDir, file), "utf-8"));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const reachesInstructions = (file) => {
|
|
20
|
+
const content = context.getFile(file) ?? "";
|
|
21
|
+
if (file === "AGENTS.md") {
|
|
22
|
+
return content.includes(AGENT_INSTRUCTIONS_PATH);
|
|
23
|
+
}
|
|
24
|
+
return IMPORTS_INSTRUCTIONS.test(content) || IMPORTS_AGENTS_MD.test(content) && reachesInstructions("AGENTS.md");
|
|
25
|
+
};
|
|
26
|
+
const linked = [];
|
|
27
|
+
for (const file of existing) {
|
|
28
|
+
if (reachesInstructions(file)) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const content = context.getFile(file) ?? "";
|
|
32
|
+
context.updateFile(file, `${content.trimEnd()}
|
|
33
|
+
|
|
34
|
+
${INSTRUCTIONS_POINTER}`);
|
|
35
|
+
linked.push(file);
|
|
36
|
+
}
|
|
37
|
+
return linked;
|
|
38
|
+
}
|
|
39
|
+
function ensureConfigAgentFiles(context, templateDir, files, data) {
|
|
40
|
+
if (context.doesFileExist(AGENT_INSTRUCTIONS_PATH)) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
return addTemplateFolder(context, {
|
|
44
|
+
folderPath: join(templateDir, ".config", "AGENTS"),
|
|
45
|
+
targetPath: ".config/AGENTS",
|
|
46
|
+
data,
|
|
47
|
+
include: (file) => files.includes(file)
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function appendToAgentInstructions(context, marker, text, separator = "\n\n") {
|
|
51
|
+
const existing = context.getFile(AGENT_INSTRUCTIONS_PATH);
|
|
52
|
+
if (existing === void 0) {
|
|
53
|
+
codemodsDebug(`Could not find ${AGENT_INSTRUCTIONS_PATH}, skipping`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (existing.includes(marker)) {
|
|
57
|
+
codemodsDebug(`${AGENT_INSTRUCTIONS_PATH} already contains ${marker}, skipping`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const kept = existing.trimEnd();
|
|
61
|
+
context.updateFile(AGENT_INSTRUCTIONS_PATH, kept ? `${kept}${separator}${text}` : text);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { AGENT_INSTRUCTIONS_PATH, INSTRUCTIONS_POINTER, appendToAgentInstructions, ensureConfigAgentFiles, ensureRootAgentFiles };
|
package/dist/codemods/utils.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { join, dirname } from 'node:path';
|
|
1
|
+
import { join, dirname, posix } from 'node:path';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
import { mkdirSync, writeFileSync, rmSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { mkdirSync, writeFileSync, rmSync, existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { glob } from 'glob';
|
|
4
5
|
import { styleText } from 'node:util';
|
|
5
6
|
import { output } from '../utils/utils.console.js';
|
|
6
7
|
import { getPackageManagerWithFallback, getPackageManagerSilentInstallCmd } from '../utils/utils.packageManager.js';
|
|
@@ -9,7 +10,9 @@ import which from 'which';
|
|
|
9
10
|
import { gte, gt, clean, coerce } from 'semver';
|
|
10
11
|
import { debug } from '../utils/utils.cli.js';
|
|
11
12
|
import { renderHandlebarsTemplate } from '../utils/utils.handlebars.js';
|
|
12
|
-
import { getTemplateData } from '../utils/utils.templates.js';
|
|
13
|
+
import { buildTemplateData, getTemplateData } from '../utils/utils.templates.js';
|
|
14
|
+
import { getConfig } from '../utils/utils.config.js';
|
|
15
|
+
import { getExportFileName } from '../utils/utils.files.js';
|
|
13
16
|
|
|
14
17
|
function printChanges(context, key, description) {
|
|
15
18
|
const changes = context.listChanges();
|
|
@@ -131,11 +134,16 @@ function readJsonFile(context, path) {
|
|
|
131
134
|
if (!context.doesFileExist(path)) {
|
|
132
135
|
throw new Error(`Cannot find ${path}`);
|
|
133
136
|
}
|
|
137
|
+
let parsed;
|
|
134
138
|
try {
|
|
135
|
-
|
|
139
|
+
parsed = JSON.parse(context.getFile(path) || "");
|
|
136
140
|
} catch (e) {
|
|
137
141
|
throw new Error(`Cannot parse ${path}: ${e}`);
|
|
138
142
|
}
|
|
143
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
144
|
+
throw new Error(`${path} does not contain a JSON object.`);
|
|
145
|
+
}
|
|
146
|
+
return parsed;
|
|
139
147
|
}
|
|
140
148
|
function addDependenciesToPackageJson(context, dependencies, devDependencies = {}, packageJsonPath = "package.json") {
|
|
141
149
|
const currentPackageJson = readJsonFile(context, packageJsonPath);
|
|
@@ -232,9 +240,9 @@ function sortObjectByKeys(obj) {
|
|
|
232
240
|
}
|
|
233
241
|
const migrationsDebug = debug.extend("migrations");
|
|
234
242
|
const additionsDebug = debug.extend("additions");
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
let rendered = renderHandlebarsTemplate(readFileSync(templatePath, "utf-8"),
|
|
243
|
+
const codemodsDebug = debug.extend("codemods");
|
|
244
|
+
function renderTemplate(templatePath, includeWarning = false, data = getTemplateData()) {
|
|
245
|
+
let rendered = renderHandlebarsTemplate(readFileSync(templatePath, "utf-8"), data);
|
|
238
246
|
const regex = /DO NOT EDIT THIS FILE DIRECTLY\./g;
|
|
239
247
|
if (includeWarning && !regex.test(rendered)) {
|
|
240
248
|
rendered = `${warningComment}
|
|
@@ -243,9 +251,44 @@ ${rendered}`;
|
|
|
243
251
|
}
|
|
244
252
|
return rendered;
|
|
245
253
|
}
|
|
254
|
+
function getTemplateDataFromContext(context) {
|
|
255
|
+
return buildTemplateData({
|
|
256
|
+
pluginJson: readJsonFile(context, "src/plugin.json"),
|
|
257
|
+
packageManager: getPackageManagerWithFallback(),
|
|
258
|
+
features: getConfig(context.basePath).features
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
function addTemplateFolder(context, {
|
|
262
|
+
folderPath,
|
|
263
|
+
targetPath,
|
|
264
|
+
data,
|
|
265
|
+
include = () => true
|
|
266
|
+
}) {
|
|
267
|
+
if (!existsSync(folderPath)) {
|
|
268
|
+
throw new Error(
|
|
269
|
+
`Cannot find templates at ${folderPath}. This is a bug in @grafana/create-plugin - please report it.`
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
const added = [];
|
|
273
|
+
for (const relativeFile of glob.sync("**", { cwd: folderPath, dot: true, nodir: true, posix: true })) {
|
|
274
|
+
const file = join(folderPath, relativeFile);
|
|
275
|
+
const outputFile = posix.join(posix.dirname(relativeFile), getExportFileName(file));
|
|
276
|
+
if (!include(outputFile)) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const filePath = join(targetPath, outputFile);
|
|
280
|
+
if (context.doesFileExist(filePath)) {
|
|
281
|
+
codemodsDebug(`${filePath} already exists, skipping`);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
context.addFile(filePath, renderHandlebarsTemplate(readFileSync(file, "utf-8"), data));
|
|
285
|
+
added.push(filePath);
|
|
286
|
+
}
|
|
287
|
+
return added;
|
|
288
|
+
}
|
|
246
289
|
const warningComment = `/*
|
|
247
290
|
* \u26A0\uFE0F\u26A0\uFE0F\u26A0\uFE0F THIS FILE WAS SCAFFOLDED BY \`@grafana/create-plugin\`. DO NOT EDIT THIS FILE DIRECTLY. \u26A0\uFE0F\u26A0\uFE0F\u26A0\uFE0F
|
|
248
291
|
*
|
|
249
292
|
*/`;
|
|
250
293
|
|
|
251
|
-
export { addDependenciesToPackageJson, additionsDebug, flushChanges, formatFiles, installNPMDependencies, isVersionGreater, migrationsDebug, printChanges, readJsonFile, removeDependenciesFromPackageJson, renderTemplate, runGoModTidy };
|
|
294
|
+
export { addDependenciesToPackageJson, addTemplateFolder, additionsDebug, codemodsDebug, flushChanges, formatFiles, getTemplateDataFromContext, installNPMDependencies, isVersionGreater, migrationsDebug, printChanges, readJsonFile, removeDependenciesFromPackageJson, renderTemplate, runGoModTidy };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { parseDocument, visit, isScalar } from 'yaml';
|
|
2
|
+
import { codemodsDebug } from './utils.js';
|
|
3
|
+
|
|
4
|
+
function bumpActionRefs(context, workflowPath, minimumRefs) {
|
|
5
|
+
const minimums = new Map(
|
|
6
|
+
Object.entries(minimumRefs).map(([action, minimumRef]) => {
|
|
7
|
+
const minimum = parseTag(action, minimumRef);
|
|
8
|
+
if (!minimum) {
|
|
9
|
+
throw new Error(`The minimum ref '${minimumRef}' for ${action} is not a version tag.`);
|
|
10
|
+
}
|
|
11
|
+
return [action, minimum];
|
|
12
|
+
})
|
|
13
|
+
);
|
|
14
|
+
const content = context.getFile(workflowPath);
|
|
15
|
+
if (!content) {
|
|
16
|
+
codemodsDebug(`Could not find ${workflowPath}. Skipping action bumps.`);
|
|
17
|
+
return { found: [], unrecognized: [] };
|
|
18
|
+
}
|
|
19
|
+
const doc = parseDocument(content);
|
|
20
|
+
if (doc.errors.length > 0) {
|
|
21
|
+
codemodsDebug(`Failed to parse ${workflowPath}: ${doc.errors[0].message}. Skipping action bumps.`);
|
|
22
|
+
return { found: [], unrecognized: [] };
|
|
23
|
+
}
|
|
24
|
+
const found = /* @__PURE__ */ new Set();
|
|
25
|
+
const unrecognized = /* @__PURE__ */ new Set();
|
|
26
|
+
const edits = [];
|
|
27
|
+
visit(doc, {
|
|
28
|
+
Pair: (_, pair) => {
|
|
29
|
+
if (!isScalar(pair.key) || pair.key.value !== "uses" || !isScalar(pair.value)) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const uses = pair.value;
|
|
33
|
+
if (typeof uses.value !== "string" || !uses.value.includes("@")) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const separator = uses.value.lastIndexOf("@");
|
|
37
|
+
const action = uses.value.slice(0, separator);
|
|
38
|
+
const ref = uses.value.slice(separator + 1);
|
|
39
|
+
const minimum = minimums.get(action);
|
|
40
|
+
if (!minimum) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
found.add(action);
|
|
44
|
+
if (ref === "main") {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const tag = parseTag(action, ref);
|
|
48
|
+
if (!tag) {
|
|
49
|
+
codemodsDebug(`${workflowPath} pins ${action}@${ref}, which is not a version tag. Leaving it as-is.`);
|
|
50
|
+
unrecognized.add(action);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (meetsMinimum(tag.version, minimum.version)) {
|
|
54
|
+
codemodsDebug(`${workflowPath} already pins ${action}@${ref}. Skipping.`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (!uses.range) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const [start, end] = uses.range;
|
|
61
|
+
edits.push({ start, end, text: content.slice(start, end).replace(`@${ref}`, `@${raisedRef(tag, minimum)}`) });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
if (edits.length > 0) {
|
|
65
|
+
const updated = edits.sort((a, b) => b.start - a.start).reduce((acc, { start, end, text }) => `${acc.slice(0, start)}${text}${acc.slice(end)}`, content);
|
|
66
|
+
context.updateFile(workflowPath, updated);
|
|
67
|
+
}
|
|
68
|
+
return { found: [...found], unrecognized: [...unrecognized] };
|
|
69
|
+
}
|
|
70
|
+
function parseTag(action, ref) {
|
|
71
|
+
const match = /^(?:([^/]+)\/)?v(\d+)(?:\.(\d+))?(?:\.(\d+))?$/.exec(ref);
|
|
72
|
+
if (!match || match[1] !== void 0 && match[1] !== action.split("/").at(-1)) {
|
|
73
|
+
return void 0;
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
prefix: match[1] === void 0 ? "" : `${match[1]}/`,
|
|
77
|
+
version: match.slice(2).filter((part) => part !== void 0).map(Number)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function meetsMinimum(version, minimum) {
|
|
81
|
+
for (const [i, part] of version.entries()) {
|
|
82
|
+
const minimumPart = minimum[i] ?? 0;
|
|
83
|
+
if (part !== minimumPart) {
|
|
84
|
+
return part > minimumPart;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
function raisedRef(tag, minimum) {
|
|
90
|
+
const version = Array.from({ length: tag.version.length }, (_, i) => minimum.version[i] ?? 0);
|
|
91
|
+
return `${minimum.prefix}v${version.join(".")}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export { bumpActionRefs };
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import
|
|
1
|
+
import additions, { isScriptAddition, hasPromptStep } from '../codemods/additions/additions.js';
|
|
2
|
+
import { prepareAgenticAddition, runAgenticStep, buildAdditionTodosWarning } from '../codemods/agentic/index.js';
|
|
3
|
+
import { getPromptPath, buildDirectiveBlock, buildNextStepsLine } from '../codemods/agentic/prompts.js';
|
|
4
|
+
import { buildVerifyWarning, runVerifyScripts } from '../codemods/agentic/verify.js';
|
|
2
5
|
import { runCodemod } from '../codemods/runner.js';
|
|
3
|
-
import { getPackageManagerFromUserAgent, getPackageManagerExecCmd } from '../utils/utils.packageManager.js';
|
|
6
|
+
import { getPackageManagerWithFallback, getPackageManagerFromUserAgent, getPackageManagerExecCmd } from '../utils/utils.packageManager.js';
|
|
4
7
|
import { performPreCodemodChecks } from '../utils/utils.checks.js';
|
|
8
|
+
import { isGitDirectoryClean, findAdditionTodos } from '../utils/utils.git.js';
|
|
5
9
|
import { output } from '../utils/utils.console.js';
|
|
6
10
|
|
|
11
|
+
const ADD_FEATURES_DOCS_URL = "https://grafana.com/developers/plugin-tools/how-to-guides/add-features-to-a-plugin";
|
|
7
12
|
const add = async (argv) => {
|
|
8
13
|
const subCommand = argv._[1];
|
|
9
14
|
if (!subCommand) {
|
|
@@ -12,23 +17,78 @@ const add = async (argv) => {
|
|
|
12
17
|
}
|
|
13
18
|
await performPreCodemodChecks(argv);
|
|
14
19
|
try {
|
|
15
|
-
const addition =
|
|
20
|
+
const addition = additions.find((addition2) => addition2.name === subCommand);
|
|
16
21
|
if (!addition) {
|
|
17
|
-
const additionsList =
|
|
22
|
+
const additionsList = additions.map((addition2) => addition2.name);
|
|
18
23
|
throw new Error(`Unknown addition: ${subCommand}
|
|
19
24
|
|
|
20
25
|
Available additions: ${additionsList.join(", ")}`);
|
|
21
26
|
}
|
|
22
|
-
const { _, $0, ...codemodOptions } = argv;
|
|
23
|
-
const
|
|
24
|
-
const
|
|
27
|
+
const { _, $0, agent: agentFlag, ...codemodOptions } = argv;
|
|
28
|
+
const resolution = await prepareAgenticAddition(addition, agentFlag);
|
|
29
|
+
const treeWasDirty = resolution?.mode === "enabled" ? !await isGitDirectoryClean() : false;
|
|
30
|
+
let context;
|
|
31
|
+
if (isScriptAddition(addition)) {
|
|
32
|
+
context = await runCodemod(addition, codemodOptions);
|
|
33
|
+
}
|
|
34
|
+
const message = context?.getMessage();
|
|
25
35
|
if (message) {
|
|
26
36
|
output[message.level]({ title: message.title, body: message.body });
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
}
|
|
38
|
+
if (!resolution || !hasPromptStep(addition)) {
|
|
39
|
+
if (!message) {
|
|
40
|
+
output.success({
|
|
41
|
+
title: `Successfully added ${addition.name} to your plugin.`
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (resolution.mode === "enabled") {
|
|
47
|
+
const result = await runAgenticStep({
|
|
48
|
+
addition,
|
|
49
|
+
agent: resolution.agent,
|
|
50
|
+
context,
|
|
51
|
+
basePath: process.cwd(),
|
|
52
|
+
treeWasDirty
|
|
30
53
|
});
|
|
54
|
+
const summaryBody = result.kind === "applied" ? [result.summary] : [];
|
|
55
|
+
const { packageManagerName } = getPackageManagerWithFallback();
|
|
56
|
+
const verifyWarning = buildVerifyWarning(
|
|
57
|
+
addition.name,
|
|
58
|
+
packageManagerName,
|
|
59
|
+
runVerifyScripts({ scripts: addition.verify ?? [], cwd: process.cwd(), packageManagerName })
|
|
60
|
+
);
|
|
61
|
+
if (verifyWarning) {
|
|
62
|
+
output.warning({ title: verifyWarning.title, body: [...summaryBody, ...verifyWarning.body] });
|
|
63
|
+
} else {
|
|
64
|
+
output.success({
|
|
65
|
+
title: `Successfully added ${addition.name} to your plugin.`,
|
|
66
|
+
body: summaryBody.length > 0 ? summaryBody : void 0
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const todosWarning = buildAdditionTodosWarning(
|
|
70
|
+
addition.name,
|
|
71
|
+
await findAdditionTodos(addition.name, process.cwd())
|
|
72
|
+
);
|
|
73
|
+
if (todosWarning) {
|
|
74
|
+
output.warning(todosWarning);
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
31
77
|
}
|
|
78
|
+
const deferredAddition = {
|
|
79
|
+
name: addition.name,
|
|
80
|
+
description: addition.description,
|
|
81
|
+
instructionsPath: getPromptPath(addition.prompt),
|
|
82
|
+
verifyScripts: addition.verify ?? []
|
|
83
|
+
};
|
|
84
|
+
if (resolution.mode === "inside-agent") {
|
|
85
|
+
output.logSingleLine(buildDirectiveBlock(deferredAddition));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
output.warning({
|
|
89
|
+
title: `${addition.name} includes AI-agent instructions that were not applied.`,
|
|
90
|
+
body: [buildNextStepsLine(deferredAddition)]
|
|
91
|
+
});
|
|
32
92
|
} catch (error) {
|
|
33
93
|
if (error instanceof Error) {
|
|
34
94
|
output.error({
|
|
@@ -40,7 +100,7 @@ Available additions: ${additionsList.join(", ")}`);
|
|
|
40
100
|
}
|
|
41
101
|
};
|
|
42
102
|
async function showAdditionsHelp() {
|
|
43
|
-
const additionsList =
|
|
103
|
+
const additionsList = additions.map((addition) => addition.name);
|
|
44
104
|
const { packageManagerName, packageManagerVersion } = getPackageManagerFromUserAgent();
|
|
45
105
|
output.error({
|
|
46
106
|
title: "No addition specified",
|
|
@@ -48,7 +108,9 @@ async function showAdditionsHelp() {
|
|
|
48
108
|
`Usage: ${getPackageManagerExecCmd(packageManagerName, packageManagerVersion)} add <addition-name> [options]`,
|
|
49
109
|
"",
|
|
50
110
|
"Available additions:",
|
|
51
|
-
...output.bulletList(additionsList)
|
|
111
|
+
...output.bulletList(additionsList),
|
|
112
|
+
"",
|
|
113
|
+
`Learn more: ${output.formatUrl(ADD_FEATURES_DOCS_URL)}`
|
|
52
114
|
]
|
|
53
115
|
});
|
|
54
116
|
}
|