10 Commits

18 changed files with 372 additions and 183 deletions

12
.editorconfig Normal file
View File

@@ -0,0 +1,12 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

View File

@@ -1,22 +0,0 @@
---
description: Instructions for using the JMESPath Testing Tool repository.
applyTo: "**/*.md,**/.js"
---
# AI Agent Instructions for JMESPath Testing Tool
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.
Coding Guidelines:
1. Use React, Vite and JavaScript/TypeScript for development.
2. Check the current date to establish context for choosing versions and dependencies.
3. Use Node.js 24 or higher LTS version.
4. When asked, answer the question and provide explanations. Do not guess nor infer missing information. Report lack of information instead.
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.
6. Do not try to manage the files directly. Instead always use Git mv, rm, etc. commands to ensure proper tracking.
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.
8. When working with MUI components, use the latest stable version and leverage the tools from the MCP server (`mui-mcp`).
9. Do not hardcode color values. Use MUI theme palette colors instead.
10. Do not use emojis in code comments, program output, or log messages.
11. Suggest code commits, but never create them without consent.
12. Never push changes.

3
.gitignore vendored
View File

@@ -32,3 +32,6 @@ yarn-error.log*
# OS # OS
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Don't store AGENTS.md in git
AGENTS.md

View File

@@ -8,6 +8,9 @@ ARG IS_RELEASE="false"
# Set working directory # Set working directory
WORKDIR /app WORKDIR /app
# Install git for version generation
RUN apk add --no-cache git
# Copy package files # Copy package files
COPY package*.json ./ COPY package*.json ./

View File

@@ -71,8 +71,8 @@ container run -p 3000:3000 jmespath-playground
1. **Enter a JMESPath expression** in the top input field (e.g., `people[*].name`) 1. **Enter a JMESPath expression** in the top input field (e.g., `people[*].name`)
2. **Add JSON data** using one of these methods: 2. **Add JSON data** using one of these methods:
- **Load an Object**: Click "📄 Load an Object" to upload standard JSON files (.json) - **Load an Object**: Click "Load an Object" to upload standard JSON files (.json)
- **Load a Log File**: Click "📋 Load a Log File" to upload JSON Lines files (.log) - each line converted to array - **Load a Log File**: Click "Load a Log File" to upload JSON Lines files (.log) - each line converted to array
- **Paste or type**: Enter JSON data directly in the bottom-left textarea - **Paste or type**: Enter JSON data directly in the bottom-left textarea
- **Load sample**: Use the "Load Sample" button for quick testing - **Load sample**: Use the "Load Sample" button for quick testing
3. **View the results** in the bottom-right output area 3. **View the results** in the bottom-right output area
@@ -87,7 +87,7 @@ container run -p 3000:3000 jmespath-playground
The application includes a REST API for uploading sample data remotely: The application includes a REST API for uploading sample data remotely:
1. **Access API Key**: Click the key-lock button (🔒) to view your unique API key 1. **Access API Key**: Click the key-lock button to view your unique API key
2. **Upload Data**: Use curl or any HTTP client to upload JSON data: 2. **Upload Data**: Use curl or any HTTP client to upload JSON data:
```bash ```bash
curl -X POST \ curl -X POST \

View File

@@ -1,6 +1,6 @@
{ {
"name": "jmespath-playground", "name": "jmespath-playground",
"version": "1.4.0", "version": "1.4.3",
"description": "A React-based web application for testing JMESPath expressions against JSON data", "description": "A React-based web application for testing JMESPath expressions against JSON data",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
@@ -11,7 +11,7 @@
"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": "vite build && node scripts/build-image.js" "build-image": "vite build && node scripts/build-image.mjs"
}, },
"engines": { "engines": {
"node": ">=24.0.0" "node": ">=24.0.0"

View File

@@ -1,10 +1,13 @@
#!/usr/bin/env node #!/usr/bin/env node
const { execSync } = require('child_process'); import { execSync } from 'node:child_process';
const fs = require('fs'); import fs from 'node:fs';
const path = require('path'); import path from 'node:path';
const { pathToFileURL } = require('url'); import { fileURLToPath, pathToFileURL } from 'node:url';
const { parseArgs } = require('util'); import { parseArgs } from 'node:util';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
function execCommand(command, description) { function execCommand(command, description) {
try { try {
@@ -33,6 +36,15 @@ function getContainerTool() {
} }
} }
function isGitRepo() {
try {
execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
return true;
} catch (error) {
return false;
}
}
async function generateVersionFile() { async function generateVersionFile() {
const versionModuleUrl = pathToFileURL(path.join(__dirname, 'version.mjs')).href; const versionModuleUrl = pathToFileURL(path.join(__dirname, 'version.mjs')).href;
const { generateVersionFile: generate } = await import(versionModuleUrl); const { generateVersionFile: generate } = await import(versionModuleUrl);
@@ -41,6 +53,26 @@ async function generateVersionFile() {
return versionFilePath; return versionFilePath;
} }
function writeVersionFile(version, isRelease) {
const versionFilePath = path.join(__dirname, '..', 'src', 'version.js');
const contents = [
`export const VERSION = '${version}';`,
`export const IS_RELEASE = ${isRelease};`,
''
].join('\n');
fs.writeFileSync(versionFilePath, contents, 'utf8');
return versionFilePath;
}
function readPackageJsonVersion() {
const packagePath = path.join(__dirname, '..', 'package.json');
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
if (!pkg.version) {
throw new Error('package.json does not contain a version');
}
return pkg.version;
}
function readVersionFile(versionFilePath) { function readVersionFile(versionFilePath) {
const contents = fs.readFileSync(versionFilePath, 'utf8'); const contents = fs.readFileSync(versionFilePath, 'utf8');
const versionMatch = contents.match(/export const VERSION = '([^']+)';/); const versionMatch = contents.match(/export const VERSION = '([^']+)';/);
@@ -71,19 +103,21 @@ function showHelp() {
console.log(`Build multi-architecture container images for JMESPath Playground console.log(`Build multi-architecture container images for JMESPath Playground
Usage: Usage:
build-image.js [OPTIONS] build-image.mjs [OPTIONS]
Options: Options:
--all-arch Build for both arm64 and amd64 (default: build for host architecture only) --all-arch Build for both arm64 and amd64 (default: build for host architecture only)
--arch <arch> Target architecture (arm64 or amd64). Can be specified multiple times. --arch <arch> Target architecture (arm64 or amd64). Can be specified multiple times.
--registry <id> Image registry (default: docker.io). Can also set JMESPATH_REGISTRY.
--help, -h Show this help message and exit --help, -h Show this help message and exit
Examples: Examples:
build-image.js # Builds for ${hostArch} only (host architecture) build-image.mjs # Builds for ${hostArch} only (host architecture)
build-image.js --all-arch # Builds for both arm64 and amd64 build-image.mjs --all-arch # Builds for both arm64 and amd64
build-image.js --arch arm64 # Builds for arm64 only build-image.mjs --arch arm64 # Builds for arm64 only
build-image.js --arch arm64 --arch amd64 # Explicitly specify both build-image.mjs --arch arm64 --arch amd64 # Explicitly specify both
build-image.js -h # Show help`); build-image.mjs --registry docker.io # Use Docker Hub registry explicitly
build-image.mjs -h # Show help`);
} }
async function main() { async function main() {
@@ -102,6 +136,10 @@ async function main() {
type: 'string', type: 'string',
multiple: true, multiple: true,
description: 'Target architecture (arm64 or amd64)' description: 'Target architecture (arm64 or amd64)'
},
registry: {
type: 'string',
description: 'Image registry (default: docker.io)'
} }
}, },
strict: true, strict: true,
@@ -114,8 +152,19 @@ async function main() {
} }
const containerTool = getContainerTool(); const containerTool = getContainerTool();
let version;
let isRelease;
if (isGitRepo()) {
const versionFilePath = await generateVersionFile(); const versionFilePath = await generateVersionFile();
const { version, isRelease } = readVersionFile(versionFilePath); const versionInfo = readVersionFile(versionFilePath);
version = versionInfo.version;
isRelease = versionInfo.isRelease;
} else {
version = readPackageJsonVersion();
isRelease = true;
writeVersionFile(version, isRelease);
}
let architectures; let architectures;
if (values['all-arch']) { if (values['all-arch']) {
@@ -130,14 +179,16 @@ async function main() {
console.log(`Target architectures: ${architectures.join(', ')}`); console.log(`Target architectures: ${architectures.join(', ')}`);
// Build container image // Build container image
const registry = values.registry || process.env.JMESPATH_REGISTRY || 'docker.io';
const imageName = `${registry.replace(/\/$/, '')}/skoszewski/jmespath-playground`;
const tags = isRelease const tags = isRelease
? [ ? [
`-t skoszewski/jmespath-playground:${version}`, `-t ${imageName}:${version}`,
`-t skoszewski/jmespath-playground:latest` `-t ${imageName}:latest`
].join(' ') ].join(' ')
: [ : [
`-t skoszewski/jmespath-playground:dev`, `-t ${imageName}:dev`,
`-t skoszewski/jmespath-playground:latest` `-t ${imageName}:latest`
].join(' '); ].join(' ');
const archFlags = architectures.map(arch => `--arch ${arch}`).join(' '); const archFlags = architectures.map(arch => `--arch ${arch}`).join(' ');
@@ -150,26 +201,29 @@ async function main() {
// Show usage instructions // Show usage instructions
console.log(`\nUsage examples:`); console.log(`\nUsage examples:`);
console.log(` build-image.js # Builds for host architecture only`); console.log(` build-image.mjs # Builds for host architecture only`);
console.log(` build-image.js --all-arch # Builds for both arm64 and amd64`); console.log(` build-image.mjs --all-arch # Builds for both arm64 and amd64`);
console.log(` build-image.js --arch arm64 # Builds for arm64 only`); console.log(` build-image.mjs --arch arm64 # Builds for arm64 only`);
console.log(` build-image.js --arch arm64 --arch amd64 # Explicitly specify both`); console.log(` build-image.mjs --arch arm64 --arch amd64 # Explicitly specify both`);
if (isRelease) { if (isRelease) {
console.log(`\nTo run the container:`); console.log(`\nTo run the container:`);
console.log(` ${containerTool} run --arch arm64 --name jmespathpg -p 3000:3000 skoszewski/jmespath-playground:${version}`); console.log(` ${containerTool} run --arch arm64 --name jmespathpg -p 3000:3000 ${imageName}:${version}`);
if (containerTool === 'docker') { if (containerTool === 'docker') {
console.log(`\nTo push to Docker Hub:`); console.log(`\nTo push to Docker Hub:`);
console.log(` docker push skoszewski/jmespath-playground:${version}`); console.log(` docker push ${imageName}:${version}`);
console.log(` docker push skoszewski/jmespath-playground:latest`); console.log(` docker push ${imageName}:latest`);
} }
} else { } else {
console.log(`\nTo run the container:`); console.log(`\nTo run the container:`);
console.log(` ${containerTool} run --arch arm64 --name jmespathpg -p 3000:3000 skoszewski/jmespath-playground:dev`); console.log(` ${containerTool} run --arch arm64 --name jmespathpg -p 3000:3000 ${imageName}:dev`);
} }
} }
if (require.main === module) { const isDirectRun = process.argv[1]
&& fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
if (isDirectRun) {
main().catch((error) => { main().catch((error) => {
console.error(`Error: ${error.message}`); console.error(`Error: ${error.message}`);
process.exit(1); process.exit(1);

View File

@@ -1,13 +1,46 @@
#!/usr/bin/env node #!/usr/bin/env node
const fs = require('fs'); import { execSync } from 'node:child_process';
const { execSync } = require('child_process'); import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import semver from 'semver';
function tagMatchesVersion(tag, version) {
if (!tag) {
return false;
}
if (tag === version) {
return true;
}
if (tag.startsWith('v')) {
return tag.slice(1) === version;
}
return false;
}
function hasMatchingTag(tagsOutput, version) {
return tagsOutput
.split('\n')
.map(tag => tag.trim())
.filter(Boolean)
.some(tag => tagMatchesVersion(tag, version));
}
function findMatchingTag(tagsOutput, version) {
return tagsOutput
.split('\n')
.map(tag => tag.trim())
.filter(Boolean)
.find(tag => tagMatchesVersion(tag, version)) || null;
}
function showUsage() { function showUsage() {
console.log('Usage: node scripts/new-version.js <version> [--force] [-m|--message "commit message"]'); console.log('Usage: node scripts/new-version.mjs <version> [--force] [-m|--message "commit message"]');
console.log(' node scripts/new-version.js --check <version>'); console.log(' node scripts/new-version.mjs --check <version>');
console.log(''); console.log('');
console.log('Creates a new version by tagging the current commit.'); console.log('Creates a new version by tagging the current commit.');
console.log('Version must be valid semver (e.g., 1.2.3).');
console.log(''); console.log('');
console.log('Options:'); console.log('Options:');
console.log(' --force Force version creation even with dirty repo or package.json mismatch'); console.log(' --force Force version creation even with dirty repo or package.json mismatch');
@@ -15,14 +48,14 @@ function showUsage() {
console.log(' -m, --message TEXT Custom commit message (only used when commit is needed)'); console.log(' -m, --message TEXT Custom commit message (only used when commit is needed)');
console.log(''); console.log('');
console.log('Example:'); console.log('Example:');
console.log(' node scripts/new-version.js 1.2.0'); console.log(' node scripts/new-version.mjs 1.2.0');
console.log(' node scripts/new-version.js 1.2.0 --force'); console.log(' node scripts/new-version.mjs 1.2.0 --force');
console.log(' node scripts/new-version.js 1.2.0 -m "Add new feature XYZ"'); console.log(' node scripts/new-version.mjs 1.2.0 -m "Add new feature XYZ"');
console.log(' node scripts/new-version.js --check 1.3.0'); console.log(' node scripts/new-version.mjs --check 1.3.0');
} }
function performCheck(targetVersion) { function performCheck(targetVersion) {
console.log('🔍 Repository Analysis Report'); console.log('Repository Analysis Report');
console.log('============================'); console.log('============================');
try { try {
@@ -31,7 +64,7 @@ function performCheck(targetVersion) {
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8')); const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
const currentVersion = pkg.version; const currentVersion = pkg.version;
console.log(`📦 Package.json version: ${currentVersion}`); console.log(`Package.json version: ${currentVersion}`);
// Check repository status // Check repository status
let isRepoDirty = false; let isRepoDirty = false;
@@ -41,71 +74,71 @@ function performCheck(targetVersion) {
isRepoDirty = status.trim() !== ''; isRepoDirty = status.trim() !== '';
dirtyFiles = status.trim(); dirtyFiles = status.trim();
} catch (error) { } catch (error) {
console.log('⚠️ Cannot determine git status'); console.log('Warning: Cannot determine git status');
} }
if (isRepoDirty) { if (isRepoDirty) {
console.log('🔄 Repository status: DIRTY'); console.log('Repository status: DIRTY');
console.log(' Uncommitted changes:'); console.log(' Uncommitted changes:');
dirtyFiles.split('\n').forEach(line => { dirtyFiles.split('\n').forEach(line => {
if (line.trim()) console.log(` ${line}`); if (line.trim()) console.log(` ${line}`);
}); });
} else { } else {
console.log('Repository status: CLEAN'); console.log('Repository status: CLEAN');
} }
// Check current commit info // Check current commit info
try { try {
const currentCommit = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim(); const currentCommit = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim();
const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim(); const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim();
console.log(`🌟 Current commit: ${currentCommit.substring(0, 7)} (${currentBranch})`); console.log(`Current commit: ${currentCommit.substring(0, 7)} (${currentBranch})`);
// Check if current commit is tagged // Check if current commit is tagged
const tagsOnHead = execSync('git tag --points-at HEAD', { encoding: 'utf8' }).trim(); const tagsOnHead = execSync('git tag --points-at HEAD', { encoding: 'utf8' }).trim();
if (tagsOnHead) { if (tagsOnHead) {
console.log(`🏷️ Current commit tags: ${tagsOnHead.split('\n').join(', ')}`); console.log(`Current commit tags: ${tagsOnHead.split('\n').join(', ')}`);
} else { } else {
console.log('🏷️ Current commit: No tags'); console.log('Current commit: No tags');
} }
} catch (error) { } catch (error) {
console.log('⚠️ Cannot determine commit info'); console.log('Warning: Cannot determine commit info');
} }
// List recent tags // List recent tags
try { try {
const recentTags = execSync('git tag --sort=-version:refname | head -5', { encoding: 'utf8' }).trim(); const recentTags = execSync('git tag --sort=-version:refname | head -5', { encoding: 'utf8' }).trim();
if (recentTags) { if (recentTags) {
console.log('📋 Recent tags:'); console.log('Recent tags:');
recentTags.split('\n').forEach(tag => { recentTags.split('\n').forEach(tag => {
if (tag.trim()) console.log(` ${tag}`); if (tag.trim()) console.log(` ${tag}`);
}); });
} else { } else {
console.log('📋 No tags found in repository'); console.log('No tags found in repository');
} }
} catch (error) { } catch (error) {
console.log('⚠️ Cannot list tags'); console.log('Warning: Cannot list tags');
} }
console.log(''); console.log('');
// Analysis for target version (if provided) // Analysis for target version (if provided)
if (targetVersion) { if (targetVersion) {
const tagName = `v${targetVersion}`; const tagName = targetVersion;
console.log(`🎯 Analysis for version ${targetVersion}:`); console.log(`Analysis for version ${targetVersion}:`);
console.log('====================================='); console.log('=====================================');
// Check if target tag exists // Check if target tag exists
try { try {
const existingTags = execSync('git tag -l', { encoding: 'utf8' }); const existingTags = execSync('git tag -l', { encoding: 'utf8' });
const tagExists = existingTags.split('\n').includes(tagName); const matchingTag = findMatchingTag(existingTags, targetVersion);
if (tagExists) { if (matchingTag) {
console.log(` Tag '${tagName}' already exists - CANNOT CREATE`); console.log(`Error: Tag '${matchingTag}' already exists - CANNOT CREATE`);
return; return;
} }
console.log(`Tag '${tagName}' available`); console.log(`Tag '${tagName}' available`);
} catch (error) { } catch (error) {
console.log('⚠️ Cannot check tag availability'); console.log('Warning: Cannot check tag availability');
return; return;
} }
@@ -114,46 +147,46 @@ function performCheck(targetVersion) {
const needsPackageUpdate = !packageJsonMatches; const needsPackageUpdate = !packageJsonMatches;
const needsCommit = isRepoDirty || needsPackageUpdate; const needsCommit = isRepoDirty || needsPackageUpdate;
console.log(`📝 Package.json: ${packageJsonMatches ? 'MATCHES' : `NEEDS UPDATE (${currentVersion} ${targetVersion})`}`); console.log(`Package.json: ${packageJsonMatches ? 'MATCHES' : `NEEDS UPDATE (${currentVersion} -> ${targetVersion})`}`);
if (needsCommit) { if (needsCommit) {
console.log('Actions needed:'); console.log('Actions needed:');
if (needsPackageUpdate) { if (needsPackageUpdate) {
console.log(' Update package.json'); console.log(' - Update package.json');
} }
if (isRepoDirty) { if (isRepoDirty) {
console.log(' Stage uncommitted changes'); console.log(' - Stage uncommitted changes');
} }
console.log(' Create commit'); console.log(' - Create commit');
console.log(` Create tag ${tagName}`); console.log(` - Create tag ${tagName}`);
console.log(''); console.log('');
console.log('📋 Commands that would work:'); console.log('Commands that would work:');
if (isRepoDirty || needsPackageUpdate) { if (isRepoDirty || needsPackageUpdate) {
console.log(` node scripts/new-version.js ${targetVersion} --force`); console.log(` node scripts/new-version.mjs ${targetVersion} --force`);
} else { } else {
console.log(` node scripts/new-version.js ${targetVersion}`); console.log(` node scripts/new-version.mjs ${targetVersion}`);
console.log(` node scripts/new-version.js ${targetVersion} --force`); console.log(` node scripts/new-version.mjs ${targetVersion} --force`);
} }
} else { } else {
console.log('Actions needed:'); console.log('Actions needed:');
console.log(` Create tag ${tagName} (no commit needed)`); console.log(` - Create tag ${tagName} (no commit needed)`);
console.log(''); console.log('');
console.log('📋 Commands that would work:'); console.log('Commands that would work:');
console.log(` node scripts/new-version.js ${targetVersion}`); console.log(` node scripts/new-version.mjs ${targetVersion}`);
console.log(` node scripts/new-version.js ${targetVersion} --force`); console.log(` node scripts/new-version.mjs ${targetVersion} --force`);
} }
console.log(''); console.log('');
console.log('🚦 Default mode requirements:'); console.log('Default mode requirements:');
if (isRepoDirty) { if (isRepoDirty) {
console.log(' Repository must be clean (currently dirty)'); console.log(' Repository must be clean (currently dirty)');
} else { } else {
console.log(' Repository is clean'); console.log(' Repository is clean');
} }
if (!packageJsonMatches) { if (!packageJsonMatches) {
console.log(` Package.json must match version (currently ${currentVersion})`); console.log(` Package.json must match version (currently ${currentVersion})`);
} else { } else {
console.log(' Package.json version matches'); console.log(' Package.json version matches');
} }
} else { } else {
@@ -163,7 +196,7 @@ function performCheck(targetVersion) {
} }
} catch (error) { } catch (error) {
console.error('Error during analysis:', error.message); console.error('Error during analysis:', error.message);
process.exit(1); process.exit(1);
} }
} }
@@ -200,28 +233,43 @@ function main() {
// For normal operation, version is required // For normal operation, version is required
newVersion = args.find(arg => !arg.startsWith('--') && arg !== '-m' && arg !== customMessage); newVersion = args.find(arg => !arg.startsWith('--') && arg !== '-m' && arg !== customMessage);
if (!newVersion) { if (!newVersion) {
console.error('Error: Version argument required');
showUsage();
process.exit(1);
} }
} }
if (newVersion && newVersion.startsWith('v')) {
console.error('Error: Version must not start with "v". Use plain semver like 1.2.3.');
process.exit(1);
}
const normalizedVersion = newVersion;
if (!semver.valid(normalizedVersion)) {
console.error('Error: Version must be valid semver (e.g., 1.2.3)');
process.exit(1);
}
if (isCheck) { if (isCheck) {
performCheck(newVersion); performCheck(normalizedVersion);
return; return;
} }
const tagName = `v${newVersion}`; const tagName = normalizedVersion;
console.log(`🏷️ Creating new version: ${newVersion}${isForce ? ' (forced)' : ''}`); console.log(`Creating new version: ${normalizedVersion}${isForce ? ' (forced)' : ''}`);
try { try {
// 1. Check if tag already exists - Always ERROR // 1. Check if tag already exists - Always ERROR
try { try {
const existingTags = execSync('git tag -l', { encoding: 'utf8' }); const existingTags = execSync('git tag -l', { encoding: 'utf8' });
if (existingTags.split('\n').includes(tagName)) { const matchingTag = findMatchingTag(existingTags, normalizedVersion);
console.error(`❌ Error: Tag '${tagName}' already exists`); if (matchingTag) {
console.error(`Error: Tag '${matchingTag}' already exists`);
process.exit(1); process.exit(1);
} }
} catch (error) { } catch (error) {
console.error('Error: Failed to check existing tags'); console.error('Error: Failed to check existing tags');
process.exit(1); process.exit(1);
} }
@@ -231,7 +279,7 @@ function main() {
const status = execSync('git status --porcelain', { encoding: 'utf8' }); const status = execSync('git status --porcelain', { encoding: 'utf8' });
isRepoDirty = status.trim() !== ''; isRepoDirty = status.trim() !== '';
} catch (error) { } catch (error) {
console.error('Error: Failed to check git status'); console.error('Error: Failed to check git status');
process.exit(1); process.exit(1);
} }
@@ -239,7 +287,7 @@ function main() {
const packagePath = './package.json'; const packagePath = './package.json';
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8')); const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
const currentVersion = pkg.version; const currentVersion = pkg.version;
const packageJsonMatches = currentVersion === newVersion; const packageJsonMatches = currentVersion === normalizedVersion;
// 4. Determine what action is needed // 4. Determine what action is needed
const needsPackageUpdate = !packageJsonMatches; const needsPackageUpdate = !packageJsonMatches;
@@ -248,12 +296,12 @@ function main() {
// 5. Check if force is required // 5. Check if force is required
if (!isForce) { if (!isForce) {
if (isRepoDirty) { if (isRepoDirty) {
console.error('Error: Working directory has uncommitted changes'); console.error('Error: Working directory has uncommitted changes');
console.error('Please commit your changes first or use --force'); console.error('Please commit your changes first or use --force');
process.exit(1); process.exit(1);
} }
if (needsPackageUpdate) { if (needsPackageUpdate) {
console.error(`Error: Package.json version is ${currentVersion}, requested ${newVersion}`); console.error(`Error: Package.json version is ${currentVersion}, requested ${normalizedVersion}`);
console.error('Use --force to update package.json'); console.error('Use --force to update package.json');
process.exit(1); process.exit(1);
} }
@@ -261,40 +309,45 @@ function main() {
// 6. Execute the versioning // 6. Execute the versioning
if (needsCommit) { if (needsCommit) {
console.log(`📦 Needs commit: ${needsPackageUpdate ? 'package.json update' : ''}${needsPackageUpdate && isRepoDirty ? ' + ' : ''}${isRepoDirty ? 'uncommitted changes' : ''}`); console.log(`Needs commit: ${needsPackageUpdate ? 'package.json update' : ''}${needsPackageUpdate && isRepoDirty ? ' + ' : ''}${isRepoDirty ? 'uncommitted changes' : ''}`);
// Update package.json if needed // Update package.json if needed
if (needsPackageUpdate) { if (needsPackageUpdate) {
pkg.version = newVersion; pkg.version = normalizedVersion;
fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + '\n'); fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + '\n');
console.log(`📝 Updated package.json: ${currentVersion} ${newVersion}`); console.log(`Updated package.json: ${currentVersion} -> ${normalizedVersion}`);
} }
// Stage all changes // Stage all changes
execSync('git add .', { stdio: 'inherit' }); execSync('git add .', { stdio: 'inherit' });
// Commit // Commit
const commitMessage = customMessage || (needsPackageUpdate ? `Version ${newVersion}` : `Prepare for version ${newVersion}`); const commitMessage = customMessage || (needsPackageUpdate ? `Version ${normalizedVersion}` : `Prepare for version ${normalizedVersion}`);
execSync(`git commit -m "${commitMessage}"`, { stdio: 'inherit' }); execSync(`git commit -m "${commitMessage}"`, { stdio: 'inherit' });
console.log(`Committed changes`); console.log('Committed changes');
} else { } else {
console.log(`Repository clean, package.json matches - tagging current commit`); console.log('Repository clean, package.json matches - tagging current commit');
} }
// 7. Tag the commit // 7. Tag the commit
execSync(`git tag ${tagName}`, { stdio: 'inherit' }); execSync(`git tag ${tagName}`, { stdio: 'inherit' });
console.log(`🏷️ Created tag: ${tagName}`); console.log(`Created tag: ${tagName}`);
console.log(''); console.log('');
console.log('🎉 Version created successfully!'); console.log('Version created successfully!');
console.log(''); console.log('');
console.log('Next steps:'); console.log('Next steps:');
console.log(` git push origin main --tags # Push the commit and tag`); console.log(` git push origin main --tags # Push the commit and tag`);
} catch (error) { } catch (error) {
console.error('Error during version creation:', error.message); console.error('Error during version creation:', error.message);
process.exit(1); process.exit(1);
} }
} }
main(); const isDirectRun = process.argv[1]
&& fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
if (isDirectRun) {
main();
}

View File

@@ -1,30 +0,0 @@
{
"users": [
{
"id": 1,
"name": "Alice Johnson",
"email": "alice@example.com",
"role": "admin",
"skills": ["JavaScript", "Python", "SQL"]
},
{
"id": 2,
"name": "Bob Wilson",
"email": "bob@example.com",
"role": "developer",
"skills": ["Java", "Spring", "React"]
},
{
"id": 3,
"name": "Carol Davis",
"email": "carol@example.com",
"role": "designer",
"skills": ["Figma", "Photoshop", "CSS"]
}
],
"metadata": {
"total": 3,
"created": "2026-01-21",
"version": "1.0"
}
}

View File

@@ -2,6 +2,24 @@ import { readFileSync, write, writeFileSync } from "fs";
import { execSync } from "child_process"; import { execSync } from "child_process";
import semver from "semver"; import semver from "semver";
export function isGitAvailable() {
try {
execSync("git --version", { stdio: "ignore" });
return true;
} catch (e) {
return false;
}
}
export function isGitRepo() {
try {
execSync("git rev-parse --is-inside-work-tree", { stdio: "ignore" });
return true;
} catch (e) {
return false;
}
}
export function getGitVersion() { export function getGitVersion() {
let rawGitVersion; let rawGitVersion;
let gitVersion; let gitVersion;
@@ -28,13 +46,22 @@ export function getGitVersion() {
} }
export function generateVersionFile(versionFilePath) { export function generateVersionFile(versionFilePath) {
if (!isGitAvailable()) {
throw new Error("Git is required to generate version info.");
}
// Read package.json version // Read package.json version
const packageVersion = JSON.parse( const packageVersion = JSON.parse(
readFileSync("package.json", { encoding: "utf-8" }), readFileSync("package.json", { encoding: "utf-8" }),
).version; ).version;
let gitVersion = packageVersion;
let gitBaseVersion = packageVersion;
let isRelease = true;
if (isGitRepo()) {
// Get version from git repository // Get version from git repository
const gitVersion = getGitVersion(); gitVersion = getGitVersion();
const gitBaseVersion = semver.coerce(gitVersion)?.version; gitBaseVersion = semver.coerce(gitVersion)?.version;
// if git returned malformed version, throw error // if git returned malformed version, throw error
if (!gitBaseVersion || gitBaseVersion === "0.0.0") { if (!gitBaseVersion || gitBaseVersion === "0.0.0") {
@@ -50,6 +77,9 @@ export function generateVersionFile(versionFilePath) {
); );
} }
isRelease = gitVersion === packageVersion;
}
// Generate version file // Generate version file
const buildDate = new Date().toISOString(); const buildDate = new Date().toISOString();
writeFileSync( writeFileSync(
@@ -58,7 +88,7 @@ export function generateVersionFile(versionFilePath) {
// Generated at: ${buildDate} // Generated at: ${buildDate}
export const VERSION = '${packageVersion}'; export const VERSION = '${packageVersion}';
export const IS_RELEASE = ${gitVersion === packageVersion}; export const IS_RELEASE = ${isRelease};
export const BUILD_TIME = '${buildDate}'; export const BUILD_TIME = '${buildDate}';
`, `,
); );

View File

@@ -29,7 +29,7 @@ function encrypt(data, key) {
tag: authTag.toString("hex"), tag: authTag.toString("hex"),
}; };
} catch (error) { } catch (error) {
console.error("⚠️ Encryption exception:", { console.error("Encryption exception:", {
message: error.message, message: error.message,
algorithm: "aes-256-gcm", algorithm: "aes-256-gcm",
keyLength: key ? key.length : "undefined", keyLength: key ? key.length : "undefined",
@@ -56,7 +56,7 @@ function decrypt(encryptedObj, key) {
return JSON.parse(decrypted); return JSON.parse(decrypted);
} catch (error) { } catch (error) {
console.error("⚠️ Decryption exception:", { console.error("Decryption exception:", {
message: error.message, message: error.message,
algorithm: "aes-256-gcm", algorithm: "aes-256-gcm",
keyLength: key ? key.length : "undefined", keyLength: key ? key.length : "undefined",
@@ -100,7 +100,7 @@ function createApp(devMode = false) {
if (devMode) { if (devMode) {
app.use((req, res, next) => { app.use((req, res, next) => {
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
console.log(`📨 [${timestamp}] ${req.method} ${req.path}`); console.log(`[${timestamp}] ${req.method} ${req.path}`);
if (req.method !== "GET" && Object.keys(req.body).length > 0) { if (req.method !== "GET" && Object.keys(req.body).length > 0) {
const bodySize = Buffer.byteLength(JSON.stringify(req.body), "utf8"); const bodySize = Buffer.byteLength(JSON.stringify(req.body), "utf8");
console.log(` Request body size: ${(bodySize / 1024).toFixed(2)}KB`); console.log(` Request body size: ${(bodySize / 1024).toFixed(2)}KB`);
@@ -108,7 +108,7 @@ function createApp(devMode = false) {
const originalJson = res.json; const originalJson = res.json;
res.json = function (data) { res.json = function (data) {
console.log(` Response: ${res.statusCode}`); console.log(` Response: ${res.statusCode}`);
return originalJson.call(this, data); return originalJson.call(this, data);
}; };
next(); next();
@@ -125,7 +125,7 @@ function createApp(devMode = false) {
if (now - session.createdAt > MAX_SESSION_TTL) { if (now - session.createdAt > MAX_SESSION_TTL) {
sessions.delete(sessionId); sessions.delete(sessionId);
console.log( console.log(
`🧹 Cleaned up expired session: ${sessionId.substring(0, 8)}...`, `Cleaned up expired session: ${sessionId.substring(0, 8)}...`,
); );
} }
} }
@@ -192,12 +192,12 @@ function createApp(devMode = false) {
}); });
console.log( console.log(
`📁 Session created: ${sessionId.substring(0, 8)}... (${sessions.size}/${MAX_SESSIONS})`, `Session created: ${sessionId.substring(0, 8)}... (${sessions.size}/${MAX_SESSIONS})`,
); );
res.json({ message: "OK" }); res.json({ message: "OK" });
} catch (error) { } catch (error) {
console.error("⚠️ Upload endpoint exception occurred:", { console.error("Upload endpoint exception occurred:", {
message: error.message, message: error.message,
stack: error.stack, stack: error.stack,
sessionCount: sessions.size, sessionCount: sessions.size,
@@ -257,12 +257,12 @@ function createApp(devMode = false) {
// Remove session after first access (one-time use) // Remove session after first access (one-time use)
sessions.delete(sessionId); sessions.delete(sessionId);
console.log( console.log(
`📤 Sample data retrieved and session cleared: ${sessionId.substring(0, 8)}...`, `Sample data retrieved and session cleared: ${sessionId.substring(0, 8)}...`,
); );
res.json(decryptedData); res.json(decryptedData);
} catch (error) { } catch (error) {
console.error("⚠️ Sample retrieval exception occurred:", { console.error("Sample retrieval exception occurred:", {
message: error.message, message: error.message,
stack: error.stack, stack: error.stack,
sessionCount: sessions.size, sessionCount: sessions.size,
@@ -318,7 +318,7 @@ function createApp(devMode = false) {
res.json({ state: session.state }); res.json({ state: session.state });
} catch (error) { } catch (error) {
console.error("⚠️ State retrieval exception occurred:", { console.error("State retrieval exception occurred:", {
message: error.message, message: error.message,
stack: error.stack, stack: error.stack,
sessionCount: sessions.size, sessionCount: sessions.size,
@@ -402,7 +402,7 @@ if (require.main === module) {
app.listen(PORT, HOST, () => { app.listen(PORT, HOST, () => {
console.log(`JMESPath Playground Server running`); console.log(`JMESPath Playground Server running`);
if (DEV_MODE) { if (DEV_MODE) {
console.log(` 🔧 Development Mode Enabled`); console.log(" Development Mode Enabled");
} }
// Show actual accessible URLs // Show actual accessible URLs

View File

@@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import { Box, Typography, Container, Link, Grid } from "@mui/material"; import { Box, Typography, Container, Link, Grid } from "@mui/material";
import { VERSION } from "../version"; import { IS_RELEASE, VERSION } from "../version";
function Footer() { function Footer() {
return ( return (
@@ -19,7 +19,7 @@ function Footer() {
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
<strong>JMESPath Testing Tool</strong>{" "} <strong>JMESPath Testing Tool</strong>{" "}
{VERSION === "unknown" ? VERSION : `v${VERSION}`} - Created for {IS_RELEASE ? VERSION : `${VERSION}-dev`} - Created for
testing and validating JMESPath expressions testing and validating JMESPath expressions
</Typography> </Typography>
</Grid> </Grid>

View File

@@ -214,7 +214,6 @@ function MainPage({
<Paper <Paper
sx={{ sx={{
mb: 1,
flexShrink: 0, flexShrink: 0,
bgcolor: "background.paper", bgcolor: "background.paper",
border: 1, border: 1,

7
terraform/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
# Terraform
*.tfstate
*.tfstate.*
.terraform/
.terraform.lock.hcl
.terraform.lock.hcl.backup
*tfplan

41
terraform/main.tf Normal file
View File

@@ -0,0 +1,41 @@
# Create Cloud Run service
resource "google_cloud_run_v2_service" "jppg" {
name = "jmespath-playground"
location = var.default_region
invoker_iam_disabled = true
template {
containers {
image = "skoszewski/jmespath-playground:${var.image_version}"
name = "jmespath-playground-1"
ports {
container_port = 3000
}
}
}
scaling {
max_instance_count = 1
}
deletion_protection = var.deletion_protection
lifecycle {
ignore_changes = [
client
]
}
}
resource "google_cloud_run_domain_mapping" "jppg" {
name = "jmespath-playground.gcp-lab.koszewscy.waw.pl"
location = var.default_region
spec {
route_name = google_cloud_run_v2_service.jppg.name
}
metadata {
namespace = var.project_id
}
}

13
terraform/providers.tf Normal file
View File

@@ -0,0 +1,13 @@
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "~>7.17.0"
}
}
}
provider "google" {
project = var.project_id
region = var.default_region
}

View File

@@ -0,0 +1,5 @@
{
"project_id": "dom-lab",
"image_version": "1.4.3",
"deletion_protection": true
}

21
terraform/variables.tf Normal file
View File

@@ -0,0 +1,21 @@
variable "project_id" {
description = "Google project id"
type = string
}
variable "default_region" {
description = "Default Google Cloud region"
type = string
default = "europe-west1" # Belgium
}
variable "image_version" {
description = "Version of the Docker image"
type = string
}
variable "deletion_protection" {
type = bool
description = "Protect resources from deletion using terraform destroy and apply."
default = true
}