diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..eb94608 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,30 @@ +module.exports = { + root: true, + ignorePatterns: ["dist/**", "node_modules/**"], + overrides: [ + { + files: ["*.cjs", "scripts/**/*.js"], + env: { + es2022: true, + node: true, + }, + parserOptions: { + ecmaVersion: "latest", + sourceType: "script", + }, + extends: ["eslint:recommended"], + }, + { + files: ["tests/**/*.mjs"], + env: { + es2022: true, + node: true, + }, + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + }, + extends: ["eslint:recommended"], + }, + ], +}; diff --git a/package.json b/package.json index 903a7d9..125b0d4 100644 --- a/package.json +++ b/package.json @@ -11,16 +11,16 @@ "access": "public" }, "scripts": { - "build": "tsc && node scripts/strip-sourcemap-refs.js", + "build": "node scripts/validate-repo.js && node scripts/strip-sourcemap-refs.js", "dev": "node scripts/dev-watch.js", "postinstall": "node scripts/postinstall.js", "prepack": "node scripts/strip-sourcemap-refs.js", "test": "vitest", "test:run": "vitest --run", - "lint": "eslint src tests --ext .ts", - "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"", + "lint": "eslint .eslintrc.cjs scripts tests --ext .js,.mjs", + "format": "prettier --write \".eslintrc.cjs\" \"scripts/**/*.js\" \"tests/**/*.mjs\"", "index:rebuild": "node dist/tools/build-index.js", - "validate": "node dist/cli/commands/validate.js", + "validate": "node scripts/validate-repo.js", "release:cut": "node scripts/release-cut.js", "release:cut:patch": "node scripts/release-cut.js patch", "release:cut:minor": "node scripts/release-cut.js minor", @@ -63,7 +63,6 @@ "SKILL.md", "skill.yaml", "README.md", - "README.zh-CN.md", "LICENSE" ], "license": "MIT", diff --git a/scripts/dev-watch.js b/scripts/dev-watch.js index 96fa257..ad03f80 100644 --- a/scripts/dev-watch.js +++ b/scripts/dev-watch.js @@ -1,63 +1,135 @@ #!/usr/bin/env node -const { spawn } = require('child_process'); -const { stripDistSourceMapReferences } = require('./strip-sourcemap-refs.js'); +const fs = require("fs"); +const path = require("path"); +const { spawn } = require("child_process"); -const WATCH_READY_PATTERN = /Watching for file changes\./; -const WATCH_ARGS = ['--watch', ...process.argv.slice(2)]; -const TSC_BIN = require.resolve('typescript/bin/tsc'); +const rootDir = path.resolve(__dirname, ".."); +const validateScriptPath = path.join(__dirname, "validate-repo.js"); +const stripSourceMapScriptPath = path.join( + __dirname, + "strip-sourcemap-refs.js", +); +const watchTargets = [ + ".eslintrc.cjs", + "README.md", + "SKILL.md", + "package.json", + "skill.yaml", + "scripts", + "tests", +]; -let outputBuffer = ''; -let stripTimer = null; +let runTimer = null; +let runInProgress = false; +let rerunRequested = false; +const watchers = []; -function scheduleStrip() { - if (stripTimer) { - clearTimeout(stripTimer); - } +function runNodeScript(scriptPath) { + return new Promise((resolve) => { + const child = spawn(process.execPath, [scriptPath], { + cwd: rootDir, + stdio: "inherit", + }); - stripTimer = setTimeout(() => { - stripTimer = null; - try { - stripDistSourceMapReferences(); - } catch (error) { - console.error(`[ospec] failed to strip sourceMappingURL during watch: ${error.message}`); - } - }, 50); + child.on("error", (error) => { + console.error( + `[ospec] failed to run ${path.basename(scriptPath)}: ${error.message}`, + ); + resolve(1); + }); + + child.on("exit", (code) => { + resolve(code ?? 1); + }); + }); } -function handleCompilerOutput(targetStream, chunk) { - const text = chunk.toString(); - targetStream.write(text); - outputBuffer = `${outputBuffer}${text}`.slice(-512); - if (WATCH_READY_PATTERN.test(outputBuffer)) { - outputBuffer = ''; - scheduleStrip(); +async function runMaintenance() { + if (runInProgress) { + rerunRequested = true; + return; } -} -const child = spawn(process.execPath, [TSC_BIN, ...WATCH_ARGS], { - cwd: process.cwd(), - stdio: ['inherit', 'pipe', 'pipe'], -}); + runInProgress = true; + console.log("[ospec] running repository maintenance checks"); -child.stdout.on('data', chunk => handleCompilerOutput(process.stdout, chunk)); -child.stderr.on('data', chunk => handleCompilerOutput(process.stderr, chunk)); + const validateCode = await runNodeScript(validateScriptPath); + if (validateCode === 0) { + await runNodeScript(stripSourceMapScriptPath); + } -child.on('exit', (code, signal) => { - if (stripTimer) { - clearTimeout(stripTimer); + runInProgress = false; + if (rerunRequested) { + rerunRequested = false; + scheduleRun("queued change"); } - if (signal) { - process.kill(process.pid, signal); +} + +function scheduleRun(reason) { + if (runTimer) { + clearTimeout(runTimer); + } + + console.log(`[ospec] change detected: ${reason}`); + runTimer = setTimeout(() => { + runTimer = null; + void runMaintenance(); + }, 100); +} + +function shouldIgnore(relativePath) { + return ( + relativePath.startsWith(".git/") || relativePath.startsWith("node_modules/") + ); +} + +function watchPath(relativePath) { + const absolutePath = path.join(rootDir, relativePath); + if (!fs.existsSync(absolutePath)) { return; } - process.exit(code ?? 0); -}); + const watcher = fs.watch(absolutePath, (_eventType, filename) => { + const nextPath = filename + ? path.join(relativePath, filename.toString()).replace(/\\/g, "/") + : relativePath; + if (shouldIgnore(nextPath)) { + return; + } + scheduleRun(nextPath); + }); + + watcher.on("error", (error) => { + console.warn( + `[ospec] watch disabled for ${relativePath}: ${error.message}`, + ); + try { + watcher.close(); + } catch (_closeError) { + // Ignore watcher close failures during teardown. + } + }); + + watchers.push(watcher); +} + +function initializeWatchers() { + for (const relativePath of watchTargets) { + watchPath(relativePath); + } +} + +initializeWatchers(); +void runMaintenance(); -for (const event of ['SIGINT', 'SIGTERM']) { +for (const event of ["SIGINT", "SIGTERM"]) { process.on(event, () => { - if (!child.killed) { - child.kill(event); + if (runTimer) { + clearTimeout(runTimer); + } + for (const watcher of watchers) { + watcher.close(); } + process.exit(0); }); } diff --git a/scripts/validate-repo.js b/scripts/validate-repo.js new file mode 100644 index 0000000..da8562d --- /dev/null +++ b/scripts/validate-repo.js @@ -0,0 +1,98 @@ +#!/usr/bin/env node + +const fs = require("fs"); +const path = require("path"); + +const rootDir = path.resolve(__dirname, ".."); +const packageJsonPath = path.join(rootDir, "package.json"); +const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); +const GLOB_META_PATTERN = /[*?[{!]/; + +const requiredRepoPaths = [ + ".eslintrc.cjs", + "README.md", + "SKILL.md", + "agents/openai.yaml", + "dist/cli.js", + "dist/index.js", + "dist/tools/build-index.js", + "scripts/postinstall.js", + "scripts/strip-sourcemap-refs.js", + "tests", +]; + +function resolveEntryCheckPath(entry) { + const normalized = entry.replace(/\\/g, "/"); + if (!GLOB_META_PATTERN.test(normalized)) { + return normalized; + } + + const concreteSegments = []; + for (const segment of normalized.split("/")) { + if (GLOB_META_PATTERN.test(segment)) { + break; + } + concreteSegments.push(segment); + } + + return concreteSegments.join("/"); +} + +function pathExists(relativePath) { + return fs.existsSync(path.join(rootDir, relativePath)); +} + +function collectMissingPaths(pathsToCheck, label) { + const missing = []; + for (const relativePath of pathsToCheck) { + if (!relativePath) { + continue; + } + if (!pathExists(relativePath)) { + missing.push(`${label}: ${relativePath}`); + } + } + return missing; +} + +function validatePackageFiles() { + const fileEntries = Array.isArray(packageJson.files) ? packageJson.files : []; + const fileTargets = fileEntries.map(resolveEntryCheckPath); + return collectMissingPaths(fileTargets, "package.json files entry"); +} + +function validateRepoPaths() { + return collectMissingPaths(requiredRepoPaths, "required repository path"); +} + +function validateRepository() { + return [...validateRepoPaths(), ...validatePackageFiles()]; +} + +function printValidationResult(missing) { + if (missing.length > 0) { + console.error("[ospec] repository validation failed"); + for (const item of missing) { + console.error(`- ${item}`); + } + return 1; + } + + console.log("[ospec] repository validation passed"); + return 0; +} + +function main() { + const missing = validateRepository(); + + return printValidationResult(missing); +} + +if (require.main === module) { + process.exitCode = main(); +} + +module.exports = { + printValidationResult, + validateRepository, +};