6 Commits

8 changed files with 148 additions and 347 deletions

View File

@@ -4,104 +4,19 @@ applyTo: "**/*.md,**/.js"
--- ---
# AI Agent Instructions for JMESPath Testing Tool # AI Agent Instructions for JMESPath Testing Tool
The tool in this repository is designed to help users validate and test JMESPath expressions against JSON data. It is a React-based web application that provides an interactive interface for entering JMESPath queries and viewing the results. This repository contains a React-based web application that allows users to test JMESPath expressions against JSON data. The application includes both a frontend and a backend server.
The main application page is divided into three sections: Coding Guidelines:
- Top section: Title and description of the tool. 1. Use React, Vite and JavaScript/TypeScript for development.
- Theme control buttons (auto/light/dark) 2. Check the current date to establish context for choosing versions and dependencies.
- Key-lock button that switches to the second application page. 3. Use Node.js 24 or higher LTS version.
- Middle section: 4. When asked, answer the question and provide explanations. Do not guess nor infer missing information. Report lack of information instead.
- The label "JMESPath Expression" with a right allinged row of action buttons: 5. When requested to make changes, do not modify unrelated parts of the code nor apply unapproved changes. Always present a change plan first, wait for approval, then implement the changes.
- Load an Object 6. Do not try to manage the files directly. Instead always use Git mv, rm, etc. commands to ensure proper tracking.
- Load a Log File 7. Do not run the development server(s) unless explicitly instructed to do so. Report the need to run the server for testing purposes and wait for approval.
- Load Sample 8. When working with MUI components, use the latest stable version and leverage the tools from the MCP server (`mui-mcp`).
- Format JSON 9. Do not hardcode color values. Use MUI theme palette colors instead.
- Clear All 10. Do not use emojis in code comments, program output, or log messages.
- Input area for JMESPath expressions 11. Suggest code commits, but never create them without consent.
- Message area for errors related to JMESPath expression syntax 12. Never push changes.
- Lower Middle left section: Input area for JSON data
- Lower Middle right section: Output are for JMESPath query results
- Bottom section: Footer with author and license information
The Middle section also contains a toolbar with buttons to load data from disk, load sample data, format JSON input, and clear all inputs.
The second page of the application contains:
- Top section: that is the same as the main page
- Middle section:
- API key display area with a button to regenerate the API key. The API key is 32 characters long cryptograghically secure random string.
- Instructions on how to use the API to upload sample data remotely with a code block displaying example curl command.
- Bottom section: Footer with author and license information.
The sample code block:
```bash
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "X-API-Key: {{API_KEY}}" \
--data @{{JSON_FILE_NAME}} \
"{{API_URL}}/api/v1/upload"
```
Placeholders `{{API_KEY}}` and `{{API_URL}}` should be replaced with the actual API key and the URL of the deployed application respectively. The `{{JSON_FILE_NAME}}` placeholder should be shown as is to indicate the file containing the JSON data to be uploaded.
The server code is only used as a bridge between the UI app and the external tools that may upload the sample data. The server does not perform any JMESPath evaluation or JSON parsing; all such logic is handled in the React application.
The server exposes a REST API to allow external tools to upload sample data that users can load into the application. The API key is required to upload sample data.
The API key is used for:
- encrypting the sample data
- authenticating download requests
Session id is a hash of the API key.
The server keeps two pieces of information in memory for each session:
1. The sample data itself.
2. A state variable (a GUID) that changes whenever new sample data is uploaded.
The maximum number of sessions to keep in memory set at the server startup using `MAX_SESSIONS` environment variable that defaults to 100. The maximum size of the sample data is set using `MAX_SAMPLE_SIZE` environment variable that defaults to 1 MB. Maximum session age is controlled using `MAX_SESSION_TTL` environment variable that defaults to 1 hour. After reaching the maximum number of sessions, the server rejects new uploads until some sessions expire. Sessions older than the maximum session age are automatically purged.
The UI generates an API key at startup then load the sample data at startup and periodically checks the state variable to see if new sample data is available. If state variable changes, the React app displays a button beneath the expression input area to reload the sample data. The reload is performed only when the user clicks the button.
---
The main components of the application are located in the `src` directory and target Node 24 LTS environment.
- Material UI v7 for building the user interface.
- React for building the component logic.
- JavaScript (ES6+) for scripting.
- Express.js for serving the application and handling API requests.
### API
The application exposes a REST API for remotly uploading sample data. The API endpoints are as follows:
- `POST /api/v1/upload`: The sample data is sent in the request body as JSON. The request must include an `x-api-key` header with the API key. If the upload is successful, the server responds with status 200 OK.
The server stores the sample data in memory and generates a new value for its state variable (a guid).
- `GET /api/v1/sample`: Returns the currently stored sample data as JSON. The request must include an `x-api-key` header with the API key. If the API key is invalid or the header is missing, the server responds with status 403 Forbidden.
- `GET /api/v1/state`: Returns the current value of the state variable (a guid) as a string. The request must include an `x-api-key` header with the API key. If the API key is invalid or the header is missing, the server responds with status 403 Forbidden.
## Containerization
The application should be prepared for deployment using containerization. It should extend minimal Node 24 LTS container image.
## Updates
Always use `scripts/new-version.js` script to make a new release.
Correct procedure to make a new release:
- Review the code changes and ensure everything is working.
- Run `npm run build` to build the React application.
- Run `npm test` to execute the test suite and ensure all tests pass.
- Prepare a commit message describing the changes made.
- Use `scripts/new-version.js` to create a new version and commit the changes. Use `--force` option if repository is not clean.
- Don't push the changes without approval.
- Don't build docker image without approval.

41
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "jmespath-playground", "name": "jmespath-playground",
"version": "1.3.1", "version": "1.4.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "jmespath-playground", "name": "jmespath-playground",
"version": "1.3.1", "version": "1.4.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
@@ -20,6 +20,7 @@
"jmespath": "^0.16.0", "jmespath": "^0.16.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"semver": "^7.7.3",
"uuid": "^9.0.0" "uuid": "^9.0.0"
}, },
"devDependencies": { "devDependencies": {
@@ -158,6 +159,16 @@
"url": "https://opencollective.com/babel" "url": "https://opencollective.com/babel"
} }
}, },
"node_modules/@babel/core/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/@babel/generator": { "node_modules/@babel/generator": {
"version": "7.28.6", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz",
@@ -191,6 +202,16 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/helper-compilation-targets/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/@babel/helper-globals": { "node_modules/@babel/helper-globals": {
"version": "7.28.0", "version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
@@ -4122,9 +4143,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash": { "node_modules/lodash": {
"version": "4.17.21", "version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@@ -4891,13 +4912,15 @@
} }
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "6.3.1", "version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
} }
}, },
"node_modules/send": { "node_modules/send": {

View File

@@ -5,13 +5,13 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"start": "vite", "start": "vite",
"prebuild": "node scripts/version-check.js", "prebuild": "node scripts/version.mjs",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"test": "vitest", "test": "vitest",
"server": "node server.js --dev", "server": "node server.js --dev",
"dev": "concurrently \"npm start\" \"node --watch server.js --dev\"", "dev": "concurrently \"npm start\" \"node --watch server.js --dev\"",
"build-image": "node scripts/build-image.js" "build-image": "vite build && node scripts/build-image.js"
}, },
"engines": { "engines": {
"node": ">=24.0.0" "node": ">=24.0.0"
@@ -28,6 +28,7 @@
"jmespath": "^0.16.0", "jmespath": "^0.16.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"semver": "^7.7.3",
"uuid": "^9.0.0" "uuid": "^9.0.0"
}, },
"browserslist": { "browserslist": {

View File

@@ -2,6 +2,8 @@
const { execSync } = require('child_process'); const { execSync } = require('child_process');
const fs = require('fs'); const fs = require('fs');
const path = require('path');
const { pathToFileURL } = require('url');
const { parseArgs } = require('util'); const { parseArgs } = require('util');
function execCommand(command, description) { function execCommand(command, description) {
@@ -31,20 +33,27 @@ function getContainerTool() {
} }
} }
function getVersion() { async function generateVersionFile() {
try { const versionModuleUrl = pathToFileURL(path.join(__dirname, 'version.mjs')).href;
// Try to get version from git tag const { generateVersionFile: generate } = await import(versionModuleUrl);
const gitTag = execSync('git tag --points-at HEAD', { encoding: 'utf8' }).trim(); const versionFilePath = path.join(__dirname, '..', 'src', 'version.js');
if (gitTag) { generate(versionFilePath);
return { version: gitTag.replace(/^v/, ''), isRelease: true }; return versionFilePath;
}
} catch (error) {
// Git command failed, ignore
} }
// Development build - use package.json version with -dev suffix function readVersionFile(versionFilePath) {
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')); const contents = fs.readFileSync(versionFilePath, 'utf8');
return { version: `${packageJson.version}-dev`, isRelease: false }; const versionMatch = contents.match(/export const VERSION = '([^']+)';/);
const releaseMatch = contents.match(/export const IS_RELEASE = (true|false);/);
if (!versionMatch || !releaseMatch) {
throw new Error(`Could not parse version file at ${versionFilePath}`);
}
return {
version: versionMatch[1],
isRelease: releaseMatch[1] === 'true'
};
} }
function getHostArchitecture() { function getHostArchitecture() {
@@ -77,7 +86,7 @@ Examples:
build-image.js -h # Show help`); build-image.js -h # Show help`);
} }
function main() { async function main() {
const { values } = parseArgs({ const { values } = parseArgs({
options: { options: {
help: { help: {
@@ -105,7 +114,8 @@ function main() {
} }
const containerTool = getContainerTool(); const containerTool = getContainerTool();
const { version, isRelease } = getVersion(); const versionFilePath = await generateVersionFile();
const { version, isRelease } = readVersionFile(versionFilePath);
let architectures; let architectures;
if (values['all-arch']) { if (values['all-arch']) {
@@ -160,5 +170,8 @@ function main() {
} }
if (require.main === module) { if (require.main === module) {
main(); main().catch((error) => {
console.error(`Error: ${error.message}`);
process.exit(1);
});
} }

View File

@@ -1,172 +0,0 @@
#!/usr/bin/env node
/**
* JMESPath Playground Upload Script (JavaScript)
* Usage: node upload.js [-u URL] [-k API_KEY] "json_file.json"
*/
const fs = require('fs');
const path = require('path');
const https = require('https');
const http = require('http');
const { URL } = require('url');
const { parseArgs } = require('util');
function showUsage() {
const scriptName = path.basename(process.argv[1]);
console.log(`Usage: node ${scriptName} [-u|--url URL] [-k|--key API_KEY] <json_file>`);
console.log('');
console.log('Options:');
console.log(' -u, --url URL API URL (default: http://localhost:3000)');
console.log(' -k, --key API_KEY API key (not required for localhost)');
console.log(' -h, --help Show this help message');
console.log('');
console.log('Examples:');
console.log(` node ${scriptName} data.json`);
console.log(` node ${scriptName} -u http://example.com:3000 -k your-api-key data.json`);
}
function getArguments() {
const { values, positionals } = parseArgs({
args: process.argv.slice(2),
options: {
url: { type: 'string', short: 'u', default: 'http://localhost:3000' },
key: { type: 'string', short: 'k' },
help: { type: 'boolean', short: 'h' }
},
allowPositionals: true
});
if (values.help) {
showUsage();
process.exit(0);
}
if (positionals.length !== 1) {
console.error('Error: JSON file required');
showUsage();
process.exit(1);
}
return {
apiUrl: values.url,
apiKey: values.key || '',
jsonFile: positionals[0]
};
}
async function validateJsonFile(jsonFile) {
// Check if file exists
if (!fs.existsSync(jsonFile)) {
console.error(`Error: JSON file '${jsonFile}' not found`);
process.exit(1);
}
// Validate JSON content
try {
const content = fs.readFileSync(jsonFile, 'utf8');
JSON.parse(content);
return content;
} catch (error) {
console.error(`Error: '${jsonFile}' contains invalid JSON`);
console.error(error.message);
process.exit(1);
}
}
function isLocalhost(url) {
try {
const parsed = new URL(url);
const hostname = parsed.hostname;
return hostname === 'localhost' ||
hostname === '127.0.0.1' ||
hostname.startsWith('127.') ||
hostname === '::1';
} catch {
return false;
}
}
function makeRequest(url, options) {
return new Promise((resolve, reject) => {
const parsedUrl = new URL(url);
const isHttps = parsedUrl.protocol === 'https:';
const client = isHttps ? https : http;
const requestOptions = {
hostname: parsedUrl.hostname,
port: parsedUrl.port,
path: parsedUrl.pathname,
method: options.method || 'GET',
headers: options.headers || {}
};
const req = client.request(requestOptions, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
resolve({
ok: res.statusCode >= 200 && res.statusCode < 300,
status: res.statusCode,
json: () => Promise.resolve(JSON.parse(data))
});
});
});
req.on('error', reject);
if (options.body) {
req.write(options.body);
}
req.end();
});
}
async function uploadData(apiUrl, apiKey, jsonFile, jsonData) {
try {
const headers = {
'Content-Type': 'application/json',
};
// Only send API key for non-localhost requests
const isLocal = isLocalhost(apiUrl);
if (!isLocal && apiKey) {
headers['X-API-Key'] = apiKey;
} else if (!isLocal && !apiKey) {
console.error('Error: API key required for non-localhost URLs');
console.error('Use -k/--key option to specify API key');
process.exit(1);
}
const response = await makeRequest(`${apiUrl}/api/v1/upload`, {
method: 'POST',
headers: headers,
body: jsonData
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(`HTTP ${response.status}: ${errorData.error || 'Upload failed'}`);
}
const result = await response.json();
console.log(JSON.stringify(result));
} catch (error) {
console.error('Error uploading data:', error.message);
process.exit(1);
}
}
async function main() {
const { apiUrl, apiKey, jsonFile } = getArguments();
const jsonData = await validateJsonFile(jsonFile);
await uploadData(apiUrl, apiKey, jsonFile, jsonData);
}
// Run the script
main().catch((error) => {
console.error('Unexpected error:', error);
process.exit(1);
});

View File

@@ -1,48 +0,0 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// Read package.json for base version
const packagePath = './package.json';
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
let version = pkg.version;
let isRelease = false;
try {
// Check if current commit is tagged
const gitTag = execSync('git tag --points-at HEAD', { encoding: 'utf8' }).trim();
if (gitTag) {
// We're at a tagged commit - extract version from tag
const tagVersion = gitTag.replace(/^v/, ''); // Remove 'v' prefix if present
version = tagVersion;
console.log(`✅ Building release version ${version} (tagged: ${gitTag})`);
isRelease = true;
} else {
// We're not at a tagged commit - use unknown version
version = 'unknown';
console.log(`📦 Building development version with unknown version`);
isRelease = false;
}
} catch (error) {
// Git command failed (maybe not in a git repo)
version = 'unknown';
console.log(`⚠️ Cannot determine git status, using unknown version`);
isRelease = false;
}
// Generate version.js file
const versionFile = path.join('./src', 'version.js');
const versionContent = `// Auto-generated version file - do not edit manually
// Generated at: ${new Date().toISOString()}
export const VERSION = '${version}';
export const IS_RELEASE = ${isRelease};
export const BUILD_TIME = '${new Date().toISOString()}';
`;
fs.writeFileSync(versionFile, versionContent);
console.log(`📝 Generated ${versionFile} with version ${version}`);

69
scripts/version.mjs Normal file
View File

@@ -0,0 +1,69 @@
import { readFileSync, write, writeFileSync } from "fs";
import { execSync } from "child_process";
import semver from "semver";
export function getGitVersion() {
let rawGitVersion;
let gitVersion;
try {
rawGitVersion = execSync("git describe --tags --dirty").toString().trim();
gitVersion = semver.coerce(rawGitVersion) || semver.coerce("0.0.0");
} catch (e) {
return "0.0.0";
}
// Git describe may return versions like v1.2.3-4-gabcdef
// or v1.2.3-dirty or v1.2.3 or v1.2.3-4-gabcdef-dirty.
// We need to return either a clean version or
// append -dev for modified versions and
// -dirty for dirty working tree.
if (rawGitVersion.endsWith("-dirty")) {
return gitVersion.version + "-dirty";
} else if (rawGitVersion.includes("-")) {
return gitVersion.version + "-dev";
} else {
return gitVersion.version || "0.0.0";
}
}
export function generateVersionFile(versionFilePath) {
// Read package.json version
const packageVersion = JSON.parse(
readFileSync("package.json", { encoding: "utf-8" }),
).version;
// Get version from git repository
const gitVersion = getGitVersion();
const gitBaseVersion = semver.coerce(gitVersion)?.version;
// if git returned malformed version, throw error
if (!gitBaseVersion || gitBaseVersion === "0.0.0") {
throw new Error(
"Cannot determine git version. Make sure the script is run in a git repository with tags.",
);
}
// Compare git version with package.json version
if (semver.neq(gitBaseVersion, packageVersion)) {
throw new Error(
`Version mismatch: package.json version is ${packageVersion}, but git version is ${gitBaseVersion}`,
);
}
// Generate version file
const buildDate = new Date().toISOString();
writeFileSync(
versionFilePath,
`// Auto-generated version file - do not edit manually
// Generated at: ${buildDate}
export const VERSION = '${packageVersion}';
export const IS_RELEASE = ${gitVersion === packageVersion};
export const BUILD_TIME = '${buildDate}';
`,
);
}
if (import.meta.url === `file://${process.argv[1]}`) {
generateVersionFile("src/version.js");
}