Skip to content
Merged
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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@autor3search/javascript",
"version": "0.2.2",
"version": "0.2.3",
"description": "Autonomous AI-driven performance optimization for any JavaScript repository",
"type": "module",
"license": "MIT",
Expand Down
13 changes: 7 additions & 6 deletions src/cli/cmd-baseline.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,14 @@ import {
import { expandSingleDashFlags, loadRepoConfig, resolveRepo } from './context.js'

export async function runBaseline(args, io) {
const options = {
C: { type: 'string', default: '.' },
tag: { type: 'string', default: defaultTag() },
force: { type: 'boolean', default: false },
}
const { values } = parseArgs({
args: expandSingleDashFlags(args, ['tag']),
options: {
C: { type: 'string', default: '.' },
tag: { type: 'string', default: defaultTag() },
force: { type: 'boolean', default: false },
},
args: expandSingleDashFlags(args, options),
options,
allowPositionals: false,
})

Expand Down
15 changes: 8 additions & 7 deletions src/cli/cmd-eval.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@ const FORCE_POLL_MS = 500
* @returns {Promise<number>}
*/
export async function runEval(args, io) {
const options = {
C: { type: 'string', default: '.' },
json: { type: 'boolean', default: false },
desc: { type: 'string', default: '' },
'no-log': { type: 'boolean', default: false },
}
const { values } = parseArgs({
args: expandSingleDashFlags(args, ['desc']),
options: {
C: { type: 'string', default: '.' },
json: { type: 'boolean', default: false },
desc: { type: 'string', default: '' },
'no-log': { type: 'boolean', default: false },
},
args: expandSingleDashFlags(args, options),
options,
allowPositionals: false,
})

Expand Down
5 changes: 3 additions & 2 deletions src/cli/cmd-status.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ import { expandSingleDashFlags, resolveRun } from './context.js'
* @returns {Promise<number>}
*/
export async function runStatus(args, io) {
const options = { C: { type: 'string', default: '.' }, tag: { type: 'string' } }
const { values } = parseArgs({
args: expandSingleDashFlags(args, ['tag']),
options: { C: { type: 'string', default: '.' }, tag: { type: 'string' } },
args: expandSingleDashFlags(args, options),
options,
allowPositionals: false,
})

Expand Down
15 changes: 8 additions & 7 deletions src/cli/cmd-stop.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,15 @@ import { expandSingleDashFlags, resolveRun } from './context.js'
* @returns {Promise<number>}
*/
export async function runStop(args, io) {
const options = {
C: { type: 'string', default: '.' },
tag: { type: 'string' },
clear: { type: 'boolean', default: false },
force: { type: 'boolean', default: false },
}
const { values } = parseArgs({
args: expandSingleDashFlags(args, ['tag']),
options: {
C: { type: 'string', default: '.' },
tag: { type: 'string' },
clear: { type: 'boolean', default: false },
force: { type: 'boolean', default: false },
},
args: expandSingleDashFlags(args, options),
options,
allowPositionals: false,
})

Expand Down
27 changes: 22 additions & 5 deletions src/cli/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,37 @@ import { join } from 'node:path'
* Only an exact whole-token match is rewritten, so a VALUE that happens to
* look like a flag (`-desc "-tag is confusing"`) is left alone.
*
* The parseArgs OPTIONS are the input, not a list of names, and that is
* load-bearing. A hand-kept list is free to fall out of step with the
* declaration beside it, and did: `stop` accepted `-tag` but not the `-force`
* the README documents, so `stop -force` failed with "Unknown option '-f'"
* until this was derived instead. Deriving it means a flag cannot be
* documented, declared, and still unparseable.
*
* Whether a flag consumes the token after it comes from its declared `type`.
* A boolean takes no value, so the next token is another flag and must still
* be expanded — skipping it is what made `-force -tag t` die on "Unknown
* option '-t'". Single-character names are left alone because parseArgs
* already accepts them in single-dash form; `-C` must stay `-C`.
*
* @param {string[]} args
* @param {string[]} names long option names to accept in single-dash form
* @param {Record<string, {type: string}>} options the same parseArgs options the caller parses with
* @returns {string[]}
*/
export function expandSingleDashFlags(args, names) {
const single = new Set(names.map((name) => `-${name}`))
export function expandSingleDashFlags(args, options) {
const expandable = new Map(
Object.entries(options)
.filter(([name]) => name.length > 1)
.map(([name, opt]) => [`-${name}`, opt.type !== 'boolean']),
)
let expectingValue = false
return args.map((token) => {
if (expectingValue) {
expectingValue = false
return token
}
if (single.has(token)) {
expectingValue = true
if (expandable.has(token)) {
expectingValue = expandable.get(token)
return `--${token.slice(1)}`
}
return token
Expand Down
36 changes: 32 additions & 4 deletions test/cli/context.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,20 +84,48 @@ describe('loadRepoConfig', () => {
})

describe('expandSingleDashFlags', () => {
// The real `stop` declaration: one string option, two booleans, and the
// single-character -C. Passing the parseArgs options themselves is the
// point — a hand-kept list of names is what let -force go unexpanded.
const STOP_OPTIONS = {
C: { type: 'string' },
tag: { type: 'string' },
clear: { type: 'boolean' },
force: { type: 'boolean' },
}

it('rewrites a documented single-dash flag into the form parseArgs accepts', () => {
expect(expandSingleDashFlags(['-tag', 'sep7'], ['tag'])).toEqual(['--tag', 'sep7'])
expect(expandSingleDashFlags(['-tag', 'sep7'], STOP_OPTIONS)).toEqual(['--tag', 'sep7'])
})

it('leaves single-character flags and long forms alone', () => {
expect(expandSingleDashFlags(['-C', '/tmp', '--tag', 'x'], ['tag'])).toEqual(['-C', '/tmp', '--tag', 'x'])
expect(expandSingleDashFlags(['-C', '/tmp', '--tag', 'x'], STOP_OPTIONS)).toEqual(['-C', '/tmp', '--tag', 'x'])
})

it('does not rewrite a VALUE that looks like a flag', () => {
// `-desc "-tag is confusing"` must keep its value verbatim.
expect(expandSingleDashFlags(['-desc', '-tag'], ['desc', 'tag'])).toEqual(['--desc', '-tag'])
const options = { desc: { type: 'string' }, tag: { type: 'string' } }
expect(expandSingleDashFlags(['-desc', '-tag'], options)).toEqual(['--desc', '-tag'])
})

it('leaves an unrelated token untouched', () => {
expect(expandSingleDashFlags(['-nope', 'x'], ['tag'])).toEqual(['-nope', 'x'])
expect(expandSingleDashFlags(['-nope', 'x'], STOP_OPTIONS)).toEqual(['-nope', 'x'])
})

it('rewrites a single-dash BOOLEAN flag, which README documents for stop', () => {
expect(expandSingleDashFlags(['-force'], STOP_OPTIONS)).toEqual(['--force'])
expect(expandSingleDashFlags(['-clear'], STOP_OPTIONS)).toEqual(['--clear'])
})

it('does not swallow the token after a boolean flag', () => {
// A boolean takes no value, so the token after it is another flag and must
// still be expanded. Treating it as a value is what made the first attempt
// at this fix wrong: `-force -tag t` became `['--force', '-tag', 't']` and
// died on "Unknown option '-t'" — one bug traded for a subtler one.
expect(expandSingleDashFlags(['-force', '-tag', 't'], STOP_OPTIONS)).toEqual(['--force', '--tag', 't'])
})

it('still treats a string option as taking a value, in either order', () => {
expect(expandSingleDashFlags(['-tag', 't', '-force'], STOP_OPTIONS)).toEqual(['--tag', 't', '--force'])
})
})
22 changes: 22 additions & 0 deletions test/cli/stop.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,28 @@ describe('stop', () => {
expect(await stopRequested(await stateDir(dir, 't'))).toBe(true)
})

it('accepts the single-dash -force the README documents', async () => {
// README's agent prompt tells the human "I may run `stop -force` myself".
// It used to fail with "Unknown option '-f'": parseArgs only takes a single
// dash for a single-character name, and -force was never expanded. The
// unit test in context.test.js covers the expansion; this covers the
// spelling a human actually types.
const dir = await ready()
const { code } = await runCli(['stop', '-C', dir, '-force'])
expect(code).toBe(0)
expect(await forceRequested(await stateDir(dir, 't'))).toBe(true)
})

it('accepts -force and -tag together, in either order', async () => {
// A boolean consumes no value, so the flag after it must still be expanded.
const dir = await ready()
expect((await runCli(['stop', '-C', dir, '-force', '-tag', 't'])).code).toBe(0)
expect(await forceRequested(await stateDir(dir, 't'))).toBe(true)
await runCli(['stop', '-C', dir, '--clear'])
expect((await runCli(['stop', '-C', dir, '-tag', 't', '-force'])).code).toBe(0)
expect(await forceRequested(await stateDir(dir, 't'))).toBe(true)
})

it('warns a later plain stop that a forced stop from earlier is still pending', async () => {
// stop --force issued while nothing was running leaves the marker sticky
// — it aborts the NEXT eval, not this session's. A human who later runs
Expand Down