feat: migrate lobster to TypeScript and oxlint

This commit is contained in:
Vignesh Natarajan
2026-01-21 21:42:06 -08:00
parent d748672293
commit 5f8a64e695
52 changed files with 189 additions and 43 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
"es2022": true
},
"rules": {
"eslint/no-unused-vars": "error",
"eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "caughtErrorsIgnorePattern": "^_", "varsIgnorePattern": "^_" }],
"eslint/no-undef": "error",
"typescript/no-explicit-any": "off"
}
+1 -1
View File
@@ -18,7 +18,7 @@
"typecheck": "tsc -p tsconfig.json --noEmit",
"lint": "oxlint --tsconfig tsconfig.json src test",
"fmt": "oxlint --tsconfig tsconfig.json --fix src test",
"test": "pnpm build && node --test dist/test"
"test": "pnpm build && node --test dist/test/*.test.js"
},
"devDependencies": {
"@types/node": "^22.0.0",
+122
View File
@@ -0,0 +1,122 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
'@types/node':
specifier: ^22.0.0
version: 22.19.7
oxlint:
specifier: ^0.15.0
version: 0.15.15
typescript:
specifier: ^5.7.0
version: 5.9.3
packages:
'@oxlint/darwin-arm64@0.15.15':
resolution: {integrity: sha512-7GOyGM6D36lUhsOvavAVpF72SycPVG0Enunx0bzv8g0+9TklzOSFN3FJlZjLst14VPdZWujZMLgkQC7tOp+Rwg==}
cpu: [arm64]
os: [darwin]
'@oxlint/darwin-x64@0.15.15':
resolution: {integrity: sha512-pbrnYFwMn/fuX0z3IeQ05Nvo/b1zGxjmmWgkrQSDwYHxBxP6NT41hk1pmqkcA+v53xk9wvOa/6vBBI/U30F8Ow==}
cpu: [x64]
os: [darwin]
'@oxlint/linux-arm64-gnu@0.15.15':
resolution: {integrity: sha512-QWjG3YVsDlIvDTBUPmtPiyqP34ZQpFJqQh2JO94pBih11lFxQ0IGVMEXDhmW3WdiSFPZSJsZGzWynalM9eg+RA==}
cpu: [arm64]
os: [linux]
'@oxlint/linux-arm64-musl@0.15.15':
resolution: {integrity: sha512-4W0YsmMSbNzzExOWhk+6zNfmJEmKFqSjFIn8CKLtYFvH8kF6KjoW4/0HNsDNYW5Fz+KOut/2JgkvxAiKH+r0zA==}
cpu: [arm64]
os: [linux]
'@oxlint/linux-x64-gnu@0.15.15':
resolution: {integrity: sha512-agP3e+eQ6tE5tqN6VI4Uukx2yvjwYFjtrDMcB19J7PmGOaFRwuMuT0sNWK/9guvhuS9aCINNZTi3kEhMy9Qgng==}
cpu: [x64]
os: [linux]
'@oxlint/linux-x64-musl@0.15.15':
resolution: {integrity: sha512-L2qE9NhhUafsJOO4pofLx/0hW5IB0sfJa6bS85q0j+ySaI0f3CxMaAadrZLFSuqHWB3oF18B5yvzaPWsc2ohbQ==}
cpu: [x64]
os: [linux]
'@oxlint/win32-arm64@0.15.15':
resolution: {integrity: sha512-B7f4VAS/E78n8zy6XZlNeyYOtWTel4BJn/22Ap2yEAlNzO34ot8dGfpLk6MqTUWJrRnARwVBVmc3wRVrsOT5yg==}
cpu: [arm64]
os: [win32]
'@oxlint/win32-x64@0.15.15':
resolution: {integrity: sha512-ZM9T3/OpaQ3qvrk/VuHO2EQmhNH4cOZdr/b/Ju9VKwBr+ahhqMn3W5srrplWQWxfsb0yd1yBj7iD0jdAps2iLg==}
cpu: [x64]
os: [win32]
'@types/node@22.19.7':
resolution: {integrity: sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==}
oxlint@0.15.15:
resolution: {integrity: sha512-oQNc1mAHrrbKiXyKJMGs9VCZfwGfLy7YiQKa4qupi71X/u4xyWqOh36YKXqWOXnmm2y7vfWFpGZlhJPAa9tMqA==}
engines: {node: '>=8.*'}
hasBin: true
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
snapshots:
'@oxlint/darwin-arm64@0.15.15':
optional: true
'@oxlint/darwin-x64@0.15.15':
optional: true
'@oxlint/linux-arm64-gnu@0.15.15':
optional: true
'@oxlint/linux-arm64-musl@0.15.15':
optional: true
'@oxlint/linux-x64-gnu@0.15.15':
optional: true
'@oxlint/linux-x64-musl@0.15.15':
optional: true
'@oxlint/win32-arm64@0.15.15':
optional: true
'@oxlint/win32-x64@0.15.15':
optional: true
'@types/node@22.19.7':
dependencies:
undici-types: 6.21.0
oxlint@0.15.15:
optionalDependencies:
'@oxlint/darwin-arm64': 0.15.15
'@oxlint/darwin-x64': 0.15.15
'@oxlint/linux-arm64-gnu': 0.15.15
'@oxlint/linux-arm64-musl': 0.15.15
'@oxlint/linux-x64-gnu': 0.15.15
'@oxlint/linux-x64-musl': 0.15.15
'@oxlint/win32-arm64': 0.15.15
'@oxlint/win32-x64': 0.15.15
typescript@5.9.3: {}
undici-types@6.21.0: {}
+9 -4
View File
@@ -74,6 +74,7 @@ async function handleRun({ argv, registry }) {
const output = await runPipeline({
pipeline,
registry,
input: [],
stdin: process.stdin,
stdout: process.stdout,
stderr: process.stderr,
@@ -223,7 +224,7 @@ async function readVersion() {
const { dirname, join } = await import('node:path');
const here = dirname(fileURLToPath(import.meta.url));
const pkgPath = join(here, '..', 'package.json');
const pkgPath = join(here, '..', '..', 'package.json');
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
return pkg.version ?? '0.0.0';
}
@@ -231,25 +232,29 @@ async function readVersion() {
async function handleDoctor({ argv, registry }) {
const mode = 'tool';
const pipeline = "exec --json --shell 'echo [1]'";
const output = await (async () => {
const output: any = await (async () => {
try {
const parsed = parsePipeline(pipeline);
return await runPipeline({
pipeline: parsed,
registry,
input: [],
stdin: process.stdin,
stdout: process.stdout,
stderr: process.stderr,
env: process.env,
mode,
});
} catch (err) {
} catch (err: any) {
return { error: err };
}
})();
if (output?.error) {
writeToolEnvelope({ ok: false, error: { type: 'doctor_error', message: output.error?.message ?? String(output.error) } });
writeToolEnvelope({
ok: false,
error: { type: 'doctor_error', message: output.error?.message ?? String(output.error) },
});
process.exitCode = 1;
return;
}
@@ -14,7 +14,7 @@ export const clawdInvokeCommand = {
},
async run({ input, args, ctx }) {
// Drain input: for now we don't stream input into clawd calls.
for await (const _ of input) {
for await (const _item of input) {
// no-op
}
@@ -31,7 +31,7 @@ export const clawdInvokeCommand = {
if (args['args-json']) {
try {
toolArgs = JSON.parse(String(args['args-json']));
} catch (err) {
} catch (_err) {
throw new Error('clawd.invoke --args-json must be valid JSON');
}
}
@@ -63,7 +63,7 @@ export const clawdInvokeCommand = {
let parsed;
try {
parsed = text ? JSON.parse(text) : null;
} catch (err) {
} catch (_err) {
throw new Error('clawd.invoke expected JSON response');
}
@@ -43,7 +43,7 @@ export const execCommand = {
};
function runProcess(command, argv, { env, cwd }) {
return new Promise((resolve, reject) => {
return new Promise<any>((resolve, reject) => {
const child = spawn(command, argv, {
env,
cwd,
@@ -1,5 +1,5 @@
function parsePredicate(expr) {
const m = expr.match(/^([a-zA-Z0-9_\.]+)\s*(==|=|!=|<=|>=|<|>)\s*(.+)$/);
const m = expr.match(/^([a-zA-Z0-9_.]+)\s*(==|=|!=|<=|>=|<|>)\s*(.+)$/);
if (!m) throw new Error(`Invalid where expression: ${expr}`);
const [, path, op, rawValue] = m;
@@ -7,7 +7,7 @@ export const workflowsListCommand = {
},
async run({ input }) {
// Drain input.
for await (const _ of input) {
for await (const _item of input) {
// no-op
}
@@ -17,7 +17,7 @@ export const workflowsRunCommand = {
},
async run({ input, args, ctx }) {
// Drain input.
for await (const _ of input) {
for await (const _item of input) {
// no-op
}
View File
@@ -19,7 +19,7 @@ import { spawn } from 'node:child_process';
* @returns {Promise<{stdout: string, stderr: string}>}
*/
function runGh(argv, { env, cwd }) {
return new Promise((resolve, reject) => {
return new Promise<any>((resolve, reject) => {
const child = spawn('gh', argv, {
env,
cwd,
@@ -35,7 +35,7 @@ function runGh(argv, { env, cwd }) {
child.stdout.on('data', (d) => { stdout += d; });
child.stderr.on('data', (d) => { stderr += d; });
child.on('error', (err) => {
child.on('error', (err: any) => {
if (err?.code === 'ENOENT') {
reject(new Error('gh not found on PATH (install GitHub CLI)'));
return;
@@ -80,7 +80,7 @@ export function ghPrView(options) {
async run({ input, ctx }) {
// Drain input
for await (const _ of input) {
for await (const _item of input) {
// no-op
}
@@ -91,7 +91,7 @@ export function ghPrView(options) {
'--json', fields.join(','),
];
const { stdout } = await runGh(argv, { env: ctx.env, cwd: process.cwd() });
const { stdout } = (await runGh(argv, { env: ctx.env, cwd: process.cwd() })) as any;
let parsed;
try {
@@ -4,14 +4,21 @@
import { prMonitor, prMonitorNotify } from "./github/pr-monitor.js";
const recipes = {
const recipes: Record<string, any> = {
"github.pr.monitor": prMonitor,
"github.pr.monitor.notify": prMonitorNotify,
};
export function registerRecipe(fn) {
const meta = fn?.meta ?? {};
const name = meta.name;
if (!name) throw new Error("Recipe is missing meta.name");
recipes[name] = fn;
}
export function listRecipes() {
return Object.entries(recipes).map(([name, fn]) => {
const meta = fn.meta ?? {};
const meta: any = (fn as any).meta ?? {};
return {
name,
description: meta.description ?? "",
View File
View File
+3 -3
View File
@@ -32,8 +32,8 @@ export class Lobster {
/** @type {Array<Function|Object>} */
#stages = [];
/** @type {LobsterOptions} */
#options = {};
/** @type {any} */
#options: any = {} as any;
/** @type {Object|null} */
#meta = null;
@@ -42,7 +42,7 @@ export class Lobster {
* Create a new Lobster workflow builder
* @param {LobsterOptions} [options]
*/
constructor(options = {}) {
constructor(options: any = {}) {
this.#options = {
env: options.env ?? process.env,
stateDir: options.stateDir,
@@ -18,7 +18,7 @@
* @param {boolean} [options.preview=true] - Include items in approval request
* @returns {Object} Stage object with run method
*/
export function approve(options = {}) {
export function approve(options: any = {}) {
const prompt = options.prompt ?? 'Approve?';
const preview = options.preview !== false;
@@ -26,7 +26,7 @@ export function approve(options = {}) {
type: 'approve',
prompt,
async run({ input, ctx }) {
async run({ input, ctx: _ctx }) {
// Collect all items
const items = [];
for await (const item of input) {
@@ -72,7 +72,7 @@ function stableStringify(value) {
* @param {boolean} [options.changesOnly=false] - If true, suppress output when unchanged
* @returns {Object} Stage object with run method
*/
export function diffLast(key, options = {}) {
export function diffLast(key, options: any = {}) {
if (!key) throw new Error('diffLast requires a key');
const changesOnly = options.changesOnly === true;
@@ -19,7 +19,7 @@ import { spawn } from 'node:child_process';
* @returns {Promise<{stdout: string, stderr: string}>}
*/
function runProcess(command, argv, { env, cwd }) {
return new Promise((resolve, reject) => {
return new Promise<any>((resolve, reject) => {
const child = spawn(command, argv, {
env,
cwd,
@@ -112,7 +112,7 @@ function parseCommand(cmdString) {
* @param {string} [options.cwd] - Working directory
* @returns {Object} Stage object with run method
*/
export function exec(cmdString, options = {}) {
export function exec(cmdString, options: any = {}) {
const parseJson = options.json !== false;
const useShell = options.shell === true;
const cwd = options.cwd ?? process.cwd();
@@ -123,7 +123,7 @@ export function exec(cmdString, options = {}) {
async run({ input, ctx }) {
// Drain input (exec doesn't use input stream)
for await (const _ of input) {
for await (const _item of input) {
// no-op
}
@@ -63,7 +63,7 @@ export function stateGet(key) {
async run({ input, ctx }) {
// Drain input
for await (const _ of input) {
for await (const _item of input) {
// no-op
}
+1 -1
View File
@@ -9,7 +9,7 @@ export function decodeToken(token) {
try {
const json = Buffer.from(String(token), 'base64url').toString('utf8');
return JSON.parse(json);
} catch (err) {
} catch (_err) {
throw new Error('Invalid token');
}
}
@@ -13,7 +13,7 @@ function runProcess(command, argv, { env, cwd }) {
child.stdout.on('data', (d) => { stdout += d; });
child.stderr.on('data', (d) => { stderr += d; });
child.on('error', (err) => {
child.on('error', (err: any) => {
if (err?.code === 'ENOENT') {
reject(new Error('gh not found on PATH (install GitHub CLI)'));
return;
@@ -97,7 +97,7 @@ export async function runGithubPrMonitorWorkflow({ args, ctx }) {
'number,title,url,state,isDraft,mergeable,reviewDecision,author,baseRefName,headRefName,updatedAt',
];
const { stdout } = await runProcess('gh', argv, { env: ctx.env, cwd: process.cwd() });
const { stdout } = (await runProcess('gh', argv, { env: ctx.env, cwd: process.cwd() })) as any;
let current;
try {
@@ -112,7 +112,7 @@ export async function runGithubPrMonitorWorkflow({ args, ctx }) {
return {
kind: 'github.pr.monitor',
repo,
pr: Number(pr),
prNumber: Number(pr),
key,
changed: false,
suppressed: true,
@@ -125,7 +125,7 @@ export async function runGithubPrMonitorWorkflow({ args, ctx }) {
return {
kind: 'github.pr.monitor',
repo,
pr: Number(pr),
prNumber: Number(pr),
key,
changed,
summary,
@@ -142,7 +142,7 @@ export async function runGithubPrMonitorWorkflow({ args, ctx }) {
return {
kind: 'github.pr.monitor',
repo,
pr: Number(pr),
prNumber: Number(pr),
key,
changed,
summary,
@@ -171,7 +171,7 @@ export async function runGithubPrMonitorNotifyWorkflow({ args, ctx }) {
kind: 'github.pr.monitor.notify',
changed: Boolean(base.changed),
repo: args.repo,
pr: Number(args.pr),
prNumber: Number(args.pr),
message: formatPrChangeMessage({
repo: args.repo,
pr: Number(args.pr),
@@ -29,8 +29,9 @@ test('clawd.invoke posts to /tools/invoke and returns JSON', async () => {
});
});
await new Promise((resolve) => server.listen(0, resolve));
const port = server.address().port;
await new Promise<void>((resolve) => server.listen(0, () => resolve()));
const addr = server.address();
const port = typeof addr === "string" || addr == null ? 0 : addr.port;
try {
const registry = createDefaultRegistry();
@@ -29,8 +29,9 @@ test('clawd.invoke accepts legacy raw JSON response', async () => {
});
});
await new Promise((resolve) => server.listen(0, resolve));
const port = server.address().port;
await new Promise<void>((resolve) => server.listen(0, () => resolve()));
const addr = server.address();
const port = typeof addr === "string" || addr == null ? 0 : addr.port;
try {
const registry = createDefaultRegistry();
+4 -1
View File
@@ -5,7 +5,10 @@ import path from 'node:path';
test('doctor returns tool-mode ok with version', () => {
const bin = path.join(process.cwd(), 'bin', 'lobster.js');
const res = spawnSync('node', [bin, 'doctor'], { encoding: 'utf8' });
const res = spawnSync('node', [bin, 'doctor'], {
encoding: 'utf8',
env: { ...process.env, LOBSTER_STATE_DIR: path.join(process.cwd(), '.tmp-test-state') },
});
assert.equal(res.status, 0);
const out = JSON.parse(res.stdout);
assert.equal(out.ok, true);
@@ -2,6 +2,8 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import { buildPrChangeSummary } from '../src/workflows/github_pr_monitor.js';
const build = buildPrChangeSummary as any;
test('buildPrChangeSummary reports all fields on first snapshot', () => {
const after = {
number: 1,
@@ -16,7 +18,7 @@ test('buildPrChangeSummary reports all fields on first snapshot', () => {
headRefName: 'feat',
};
const res = buildPrChangeSummary(null, after);
const res = build(null, after);
assert.ok(res.changedFields.length > 0);
assert.equal(res.changes.title.to, 'A');
});
@@ -36,7 +38,7 @@ test('buildPrChangeSummary only includes changed fields', () => {
};
const after = { ...before, title: 'B', updatedAt: 't2' };
const res = buildPrChangeSummary(before, after);
const res = build(before, after);
assert.deepEqual(res.changedFields.sort(), ['title', 'updatedAt'].sort());
assert.equal(res.changes.title.from, 'A');
assert.equal(res.changes.title.to, 'B');
@@ -21,6 +21,7 @@ test('resume token roundtrip and resume pipeline continues', async () => {
const first = await runPipeline({
pipeline,
registry,
input: [],
stdin: process.stdin,
stdout: process.stdout,
stderr: process.stderr,
@@ -47,6 +47,7 @@ test('state.get returns null for missing key', async () => {
const output = await runPipeline({
pipeline: [{ name: 'state.get', args: { _: ['missing'] }, raw: 'state.get missing' }],
registry,
input: [],
stdin: process.stdin,
stdout: process.stdout,
stderr: process.stderr,
@@ -19,6 +19,7 @@ test('approve halts pipeline in tool mode', async () => {
const output = await runPipeline({
pipeline,
registry,
input: [],
stdin: process.stdin,
stdout: process.stdout,
stderr: process.stderr,
+2
View File
@@ -10,6 +10,8 @@
"declaration": false,
"sourceMap": true,
"strict": false,
"useUnknownInCatchVariables": false,
"noImplicitAny": false,
"skipLibCheck": true,
"noEmitOnError": true
},