Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 85a67867c9 | |||
| 25d4668661 | |||
| 62f7ec5a7c | |||
| 2d80a9dff1 | |||
| 3f0a7d352d | |||
| 3165432811 | |||
| fd22751e72 | |||
| 2218581e78 | |||
| c21c0f863e | |||
| bcc7983849 | |||
| fbb98b7f39 | |||
| d8bde75670 | |||
| 42e91f6ec1 |
12
.editorconfig
Normal file
12
.editorconfig
Normal 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
|
||||||
107
.github/copilot-instructions.md
vendored
107
.github/copilot-instructions.md
vendored
@@ -1,107 +0,0 @@
|
|||||||
---
|
|
||||||
description: Instructions for using the JMESPath Testing Tool repository.
|
|
||||||
applyTo: "**/*.md,**/.js"
|
|
||||||
---
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
The main application page is divided into three sections:
|
|
||||||
|
|
||||||
- Top section: Title and description of the tool.
|
|
||||||
- Theme control buttons (auto/light/dark)
|
|
||||||
- Key-lock button that switches to the second application page.
|
|
||||||
- Middle section:
|
|
||||||
- The label "JMESPath Expression" with a right allinged row of action buttons:
|
|
||||||
- Load an Object
|
|
||||||
- Load a Log File
|
|
||||||
- Load Sample
|
|
||||||
- Format JSON
|
|
||||||
- Clear All
|
|
||||||
- Input area for JMESPath expressions
|
|
||||||
- Message area for errors related to JMESPath expression syntax
|
|
||||||
- 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.
|
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -31,4 +31,7 @@ yarn-error.log*
|
|||||||
|
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
# Don't store AGENTS.md in git
|
||||||
|
AGENTS.md
|
||||||
|
|||||||
@@ -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 \
|
||||||
|
|||||||
41
package-lock.json
generated
41
package-lock.json
generated
@@ -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": {
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "jmespath-playground",
|
"name": "jmespath-playground",
|
||||||
"version": "1.4.0",
|
"version": "1.4.1",
|
||||||
"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": {
|
||||||
"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.mjs"
|
||||||
},
|
},
|
||||||
"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": {
|
||||||
|
|||||||
@@ -1,8 +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 { parseArgs } = require('util');
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||||
|
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 {
|
||||||
@@ -31,20 +36,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
|
function readVersionFile(versionFilePath) {
|
||||||
|
const contents = fs.readFileSync(versionFilePath, 'utf8');
|
||||||
|
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}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Development build - use package.json version with -dev suffix
|
return {
|
||||||
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
version: versionMatch[1],
|
||||||
return { version: `${packageJson.version}-dev`, isRelease: false };
|
isRelease: releaseMatch[1] === 'true'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHostArchitecture() {
|
function getHostArchitecture() {
|
||||||
@@ -62,7 +74,7 @@ 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)
|
||||||
@@ -70,14 +82,14 @@ Options:
|
|||||||
--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 -h # Show help`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function main() {
|
async function main() {
|
||||||
const { values } = parseArgs({
|
const { values } = parseArgs({
|
||||||
options: {
|
options: {
|
||||||
help: {
|
help: {
|
||||||
@@ -105,8 +117,9 @@ 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']) {
|
||||||
architectures = ['arm64', 'amd64'];
|
architectures = ['arm64', 'amd64'];
|
||||||
@@ -140,10 +153,10 @@ 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:`);
|
||||||
@@ -159,6 +172,12 @@ function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (require.main === module) {
|
const isDirectRun = process.argv[1]
|
||||||
main();
|
&& fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
||||||
}
|
|
||||||
|
if (isDirectRun) {
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(`Error: ${error.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
@@ -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
69
scripts/version.mjs
Normal 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");
|
||||||
|
}
|
||||||
22
server.js
22
server.js
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -55,4 +55,4 @@ function Footer() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Footer;
|
export default Footer;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user