mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 00:21:21 +00:00
Add pre-commit hooks with husky and lint staged
This commit is contained in:
24
.editorconfig
Normal file
24
.editorconfig
Normal file
@@ -0,0 +1,24 @@
|
||||
# EditorConfig helps maintain consistent coding styles across editors
|
||||
# https://editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{ts,tsx,js,jsx,mjs,cjs}]
|
||||
indent_style = tab
|
||||
indent_size = 2
|
||||
|
||||
[*.{json,yml,yaml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -33,7 +33,11 @@ scratch/
|
||||
Thumbs.db
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
# .vscode/ is tracked for shared settings (settings.json, extensions.json)
|
||||
# But ignore personal/local files
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/extensions.json
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
4
.husky/pre-commit
Normal file
4
.husky/pre-commit
Normal file
@@ -0,0 +1,4 @@
|
||||
# Run lint-staged for formatting and linting on staged files only
|
||||
npx lint-staged
|
||||
|
||||
# Everything else (tests, type checking) should run in CI
|
||||
7
.vscode/extensions.json
vendored
Normal file
7
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"editorconfig.editorconfig"
|
||||
]
|
||||
}
|
||||
47
.vscode/settings.json
vendored
Normal file
47
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
// Format on save with Prettier
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
|
||||
// Use tabs (matches .prettierrc and .editorconfig)
|
||||
"editor.tabSize": 2,
|
||||
"editor.insertSpaces": false,
|
||||
"editor.detectIndentation": false,
|
||||
|
||||
// ESLint configuration
|
||||
"eslint.enable": true,
|
||||
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
|
||||
|
||||
// Don't let ESLint format - let Prettier handle it
|
||||
"eslint.format.enable": false,
|
||||
|
||||
// File-specific formatters
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
|
||||
// Recommended extensions
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
|
||||
// Files to exclude from search/watch
|
||||
"files.exclude": {
|
||||
"dist": true,
|
||||
"release": true,
|
||||
"node_modules": true
|
||||
},
|
||||
|
||||
// TypeScript settings
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import eslint from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import reactPlugin from 'eslint-plugin-react';
|
||||
import reactHooksPlugin from 'eslint-plugin-react-hooks';
|
||||
import prettierConfig from 'eslint-config-prettier';
|
||||
import globals from 'globals';
|
||||
|
||||
export default tseslint.config(
|
||||
@@ -28,6 +29,9 @@ export default tseslint.config(
|
||||
// TypeScript ESLint recommended rules
|
||||
...tseslint.configs.recommended,
|
||||
|
||||
// Prettier config - disables ESLint rules that conflict with Prettier
|
||||
prettierConfig,
|
||||
|
||||
// Main configuration for all TypeScript files
|
||||
{
|
||||
files: ['src/**/*.ts', 'src/**/*.tsx'],
|
||||
|
||||
17
package-lock.json
generated
17
package-lock.json
generated
@@ -87,6 +87,7 @@
|
||||
"electron-rebuild": "^3.2.9",
|
||||
"esbuild": "^0.24.2",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"globals": "^16.5.0",
|
||||
@@ -9103,6 +9104,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-config-prettier": {
|
||||
"version": "10.1.8",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz",
|
||||
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"eslint-config-prettier": "bin/cli.js"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint-config-prettier"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-react": {
|
||||
"version": "7.37.5",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz",
|
||||
|
||||
13
package.json
13
package.json
@@ -35,6 +35,7 @@
|
||||
"package:linux": "node scripts/set-version.mjs npm run build && node scripts/set-version.mjs electron-builder --linux",
|
||||
"start": "electron .",
|
||||
"clean": "rm -rf dist release node_modules/.vite",
|
||||
"prepare": "husky",
|
||||
"postinstall": "electron-rebuild -f -w node-pty,better-sqlite3",
|
||||
"lint": "tsc -p tsconfig.lint.json && tsc -p tsconfig.main.json --noEmit && tsc -p tsconfig.cli.json --noEmit",
|
||||
"lint:eslint": "eslint src/",
|
||||
@@ -281,10 +282,13 @@
|
||||
"electron-rebuild": "^3.2.9",
|
||||
"esbuild": "^0.24.2",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"globals": "^16.5.0",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^27.2.0",
|
||||
"lint-staged": "^16.2.7",
|
||||
"lucide-react": "^0.303.0",
|
||||
"playwright": "^1.57.0",
|
||||
"postcss": "^8.4.33",
|
||||
@@ -300,5 +304,14 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
"src/**/*.{ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --fix"
|
||||
],
|
||||
"src/**/*.{json,css,md}": [
|
||||
"prettier --write"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,91 +26,92 @@ export class SshCommandRunner {
|
||||
sshConfig: SshRemoteConfig,
|
||||
shellEnvVars?: Record<string, string>
|
||||
): Promise<CommandResult> {
|
||||
return new Promise(async (resolve) => {
|
||||
// Build SSH arguments
|
||||
const sshArgs: string[] = [];
|
||||
// Build SSH arguments
|
||||
const sshArgs: string[] = [];
|
||||
|
||||
// Force disable TTY allocation
|
||||
sshArgs.push('-T');
|
||||
// Force disable TTY allocation
|
||||
sshArgs.push('-T');
|
||||
|
||||
// Add identity file
|
||||
if (sshConfig.useSshConfig) {
|
||||
// Only specify identity file if explicitly provided (override SSH config)
|
||||
if (sshConfig.privateKeyPath && sshConfig.privateKeyPath.trim()) {
|
||||
sshArgs.push('-i', expandTilde(sshConfig.privateKeyPath));
|
||||
}
|
||||
} else {
|
||||
// Direct connection: require private key
|
||||
// Add identity file
|
||||
if (sshConfig.useSshConfig) {
|
||||
// Only specify identity file if explicitly provided (override SSH config)
|
||||
if (sshConfig.privateKeyPath && sshConfig.privateKeyPath.trim()) {
|
||||
sshArgs.push('-i', expandTilde(sshConfig.privateKeyPath));
|
||||
}
|
||||
} else {
|
||||
// Direct connection: require private key
|
||||
sshArgs.push('-i', expandTilde(sshConfig.privateKeyPath));
|
||||
}
|
||||
|
||||
// Default SSH options for non-interactive operation
|
||||
const sshOptions: Record<string, string> = {
|
||||
BatchMode: 'yes',
|
||||
StrictHostKeyChecking: 'accept-new',
|
||||
ConnectTimeout: '10',
|
||||
ClearAllForwardings: 'yes',
|
||||
RequestTTY: 'no',
|
||||
};
|
||||
for (const [key, value] of Object.entries(sshOptions)) {
|
||||
sshArgs.push('-o', `${key}=${value}`);
|
||||
}
|
||||
// Default SSH options for non-interactive operation
|
||||
const sshOptions: Record<string, string> = {
|
||||
BatchMode: 'yes',
|
||||
StrictHostKeyChecking: 'accept-new',
|
||||
ConnectTimeout: '10',
|
||||
ClearAllForwardings: 'yes',
|
||||
RequestTTY: 'no',
|
||||
};
|
||||
for (const [key, value] of Object.entries(sshOptions)) {
|
||||
sshArgs.push('-o', `${key}=${value}`);
|
||||
}
|
||||
|
||||
// Port specification
|
||||
if (!sshConfig.useSshConfig || sshConfig.port !== 22) {
|
||||
sshArgs.push('-p', sshConfig.port.toString());
|
||||
}
|
||||
// Port specification
|
||||
if (!sshConfig.useSshConfig || sshConfig.port !== 22) {
|
||||
sshArgs.push('-p', sshConfig.port.toString());
|
||||
}
|
||||
|
||||
// Build destination (user@host or just host for SSH config)
|
||||
if (sshConfig.useSshConfig) {
|
||||
if (sshConfig.username && sshConfig.username.trim()) {
|
||||
sshArgs.push(`${sshConfig.username}@${sshConfig.host}`);
|
||||
} else {
|
||||
sshArgs.push(sshConfig.host);
|
||||
}
|
||||
} else {
|
||||
// Build destination (user@host or just host for SSH config)
|
||||
if (sshConfig.useSshConfig) {
|
||||
if (sshConfig.username && sshConfig.username.trim()) {
|
||||
sshArgs.push(`${sshConfig.username}@${sshConfig.host}`);
|
||||
}
|
||||
|
||||
// Determine the working directory on the remote
|
||||
const remoteCwd = cwd || '~';
|
||||
|
||||
// Merge environment variables: SSH config's remoteEnv + shell env vars
|
||||
const mergedEnv: Record<string, string> = {
|
||||
...(sshConfig.remoteEnv || {}),
|
||||
...(shellEnvVars || {}),
|
||||
};
|
||||
|
||||
// Build the remote command with cd and env vars
|
||||
const envExports = Object.entries(mergedEnv)
|
||||
.filter(([key]) => /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key))
|
||||
.map(([key, value]) => `${key}='${value.replace(/'/g, "'\\''")}'`)
|
||||
.join(' ');
|
||||
|
||||
// Escape the user's command for the remote shell
|
||||
const escapedCommand = shellEscapeForDoubleQuotes(command);
|
||||
let remoteCommand: string;
|
||||
if (envExports) {
|
||||
remoteCommand = `cd '${remoteCwd.replace(/'/g, "'\\''")}' && ${envExports} $SHELL -lc "${escapedCommand}"`;
|
||||
} else {
|
||||
remoteCommand = `cd '${remoteCwd.replace(/'/g, "'\\''")}' && $SHELL -lc "${escapedCommand}"`;
|
||||
sshArgs.push(sshConfig.host);
|
||||
}
|
||||
} else {
|
||||
sshArgs.push(`${sshConfig.username}@${sshConfig.host}`);
|
||||
}
|
||||
|
||||
// Wrap the entire thing for SSH
|
||||
const wrappedForSsh = `$SHELL -c "${shellEscapeForDoubleQuotes(remoteCommand)}"`;
|
||||
sshArgs.push(wrappedForSsh);
|
||||
// Determine the working directory on the remote
|
||||
const remoteCwd = cwd || '~';
|
||||
|
||||
logger.info('[ProcessManager] runCommandViaSsh spawning', 'ProcessManager', {
|
||||
sessionId,
|
||||
sshHost: sshConfig.host,
|
||||
remoteCwd,
|
||||
command,
|
||||
fullSshCommand: `ssh ${sshArgs.join(' ')}`,
|
||||
});
|
||||
// Merge environment variables: SSH config's remoteEnv + shell env vars
|
||||
const mergedEnv: Record<string, string> = {
|
||||
...(sshConfig.remoteEnv || {}),
|
||||
...(shellEnvVars || {}),
|
||||
};
|
||||
|
||||
// Spawn the SSH process
|
||||
const sshPath = await resolveSshPath();
|
||||
const expandedEnv = getExpandedEnv();
|
||||
// Build the remote command with cd and env vars
|
||||
const envExports = Object.entries(mergedEnv)
|
||||
.filter(([key]) => /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key))
|
||||
.map(([key, value]) => `${key}='${value.replace(/'/g, "'\\''")}'`)
|
||||
.join(' ');
|
||||
|
||||
// Escape the user's command for the remote shell
|
||||
const escapedCommand = shellEscapeForDoubleQuotes(command);
|
||||
let remoteCommand: string;
|
||||
if (envExports) {
|
||||
remoteCommand = `cd '${remoteCwd.replace(/'/g, "'\\''")}' && ${envExports} $SHELL -lc "${escapedCommand}"`;
|
||||
} else {
|
||||
remoteCommand = `cd '${remoteCwd.replace(/'/g, "'\\''")}' && $SHELL -lc "${escapedCommand}"`;
|
||||
}
|
||||
|
||||
// Wrap the entire thing for SSH
|
||||
const wrappedForSsh = `$SHELL -c "${shellEscapeForDoubleQuotes(remoteCommand)}"`;
|
||||
sshArgs.push(wrappedForSsh);
|
||||
|
||||
logger.info('[ProcessManager] runCommandViaSsh spawning', 'ProcessManager', {
|
||||
sessionId,
|
||||
sshHost: sshConfig.host,
|
||||
remoteCwd,
|
||||
command,
|
||||
fullSshCommand: `ssh ${sshArgs.join(' ')}`,
|
||||
});
|
||||
|
||||
// Resolve SSH path before entering the Promise
|
||||
const sshPath = await resolveSshPath();
|
||||
const expandedEnv = getExpandedEnv();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const childProcess = spawn(sshPath, sshArgs, {
|
||||
env: {
|
||||
...expandedEnv,
|
||||
|
||||
Reference in New Issue
Block a user