Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -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"],
},
],
};
9 changes: 4 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -63,7 +63,6 @@
"SKILL.md",
"skill.yaml",
"README.md",
"README.zh-CN.md",
"LICENSE"
],
"license": "MIT",
Expand Down
158 changes: 115 additions & 43 deletions scripts/dev-watch.js
Original file line number Diff line number Diff line change
@@ -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);
});
}
98 changes: 98 additions & 0 deletions scripts/validate-repo.js
Original file line number Diff line number Diff line change
@@ -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,
};