-
Notifications
You must be signed in to change notification settings - Fork 0
feat: clean add-steps API + serve readiness signal #54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -70,7 +70,9 @@ program | |
| .description('Start the OpenHop API server (:8787) and web UI (:8788)') | ||
| .option('-p, --port <port>', 'API port', '8787') | ||
| .option('--no-web', 'Start API only, skip the web UI dev server') | ||
| .action((opts) => { | ||
| .option('--no-wait-ready', "Don't probe /health and don't print the ready line on stdout") | ||
| .option('--ready-timeout <seconds>', 'How long to wait for readiness before giving up', '60') | ||
| .action(async (opts) => { | ||
| const cliDir = resolve(import.meta.dirname, '..', '..') | ||
| const serverEntry = resolve(cliDir, 'server', 'src', 'index.ts') | ||
| const webDir = resolve(cliDir, 'web') | ||
|
|
@@ -119,6 +121,37 @@ program | |
| web?.kill('SIGTERM') | ||
| process.exit(code ?? ExitCode.SUCCESS) | ||
| }) | ||
|
|
||
| // Readiness probe. Default-on: poll /health until it returns 200, then | ||
| // emit a stable, machine-parseable line on stdout. This is the only | ||
| // thing `serve` puts on stdout, so callers can do: | ||
| // openhop serve & wait_for=$(grep -m1 '^openhop: ready ' fd 1) | ||
| // Without --no-wait-ready, downstream scripts have to poll /health | ||
| // themselves to know when they can push. | ||
| if (opts.waitReady !== false) { | ||
| const timeoutSec = Number.parseInt(opts.readyTimeout, 10) || 60 | ||
| const apiUrl = `http://localhost:${opts.port}` | ||
| const webPart = opts.web !== false ? ` web=http://localhost:8788` : '' | ||
| const t0 = Date.now() | ||
| const deadline = t0 + timeoutSec * 1000 | ||
| while (Date.now() < deadline) { | ||
| try { | ||
| const r = await fetch(`${apiUrl}/health`) | ||
| if (r.ok) { | ||
| const elapsed = Math.round((Date.now() - t0) / 100) / 10 | ||
| process.stdout.write(`openhop: ready api=${apiUrl}${webPart} elapsed=${elapsed}s\n`) | ||
| break | ||
| } | ||
| } catch { | ||
| // Not ready yet — back off and retry. | ||
| } | ||
| await new Promise((r) => setTimeout(r, 500)) | ||
| } | ||
|
Comment on lines
+132
to
+149
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bound each At Line 139, 🔧 Suggested fix const t0 = Date.now()
const deadline = t0 + timeoutSec * 1000
while (Date.now() < deadline) {
+ const remainingMs = deadline - Date.now()
+ if (remainingMs <= 0) break
+ const controller = new AbortController()
+ const probeTimeoutMs = Math.min(1500, remainingMs)
+ const timer = setTimeout(() => controller.abort(), probeTimeoutMs)
try {
- const r = await fetch(`${apiUrl}/health`)
+ const r = await fetch(`${apiUrl}/health`, { signal: controller.signal })
if (r.ok) {
const elapsed = Math.round((Date.now() - t0) / 100) / 10
process.stdout.write(`openhop: ready api=${apiUrl}${webPart} elapsed=${elapsed}s\n`)
break
}
} catch {
// Not ready yet — back off and retry.
+ } finally {
+ clearTimeout(timer)
}
await new Promise((r) => setTimeout(r, 500))
}🤖 Prompt for AI Agents |
||
| if (Date.now() >= deadline) { | ||
| errStderr(red(`✗ API did not become ready within ${timeoutSec}s. Check logs above.`)) | ||
| // Don't exit — the children may still come up. Just warn. | ||
| } | ||
| } | ||
| }) | ||
|
|
||
| // --- push --- | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -216,13 +216,13 @@ describe('applyPatch', () => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| describe('add-steps', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('inserts a step at the beginning (after=-1)', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('inserts at the beginning (index: 0)', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const root = makeRoot() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = applyPatch(root, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| operations: [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| op: 'add-steps', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| after: -1, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| index: 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| steps: [{ from: 'b', to: 'a', data: 'response' }], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -236,13 +236,15 @@ describe('applyPatch', () => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('inserts a step after a given index', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('inserts at a middle index (existing step gets pushed right)', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // makeRoot has 1 step (a→b). Insert at index 1 → becomes the 2nd step, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // existing step stays at index 0. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+239
to
+241
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test name/comment claims “middle insertion,” but this case is append-at-end. At Line 240, ✅ Suggested test adjustment- it('inserts at a middle index (existing step gets pushed right)', () => {
- // makeRoot has 1 step (a→b). Insert at index 1 → becomes the 2nd step,
- // existing step stays at index 0.
- const root = makeRoot()
+ it('inserts at a middle index (existing step gets pushed right)', () => {
+ const root = makeRoot({
+ flow: {
+ nodes: [
+ { id: 'a', label: 'Node A' },
+ { id: 'b', label: 'Node B' },
+ ],
+ steps: [
+ { from: 'a', to: 'b', data: 's1' },
+ { from: 'b', to: 'a', data: 's2' },
+ ],
+ },
+ })
const result = applyPatch(root, {
operations: [
{
op: 'add-steps',
index: 1,
steps: [{ from: 'b', to: 'a', data: 'response' }],
},
],
})
expect(result.success).toBe(true)
- expect(result.data!.flow.steps).toHaveLength(2)
+ expect(result.data!.flow.steps).toHaveLength(3)
expect(result.data!.flow.steps![1]).toMatchObject({
from: 'b',
to: 'a',
})
})Also applies to: 247-257 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const root = makeRoot() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = applyPatch(root, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| operations: [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| op: 'add-steps', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| after: 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| index: 1, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| steps: [{ from: 'b', to: 'a', data: 'response' }], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -307,7 +309,7 @@ describe('applyPatch', () => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { op: 'remove-steps', indices: [0] }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| op: 'add-steps', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| after: -1, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| index: 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| steps: [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { from: 'a', to: 'c', data: 'new step' }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { from: 'c', to: 'b', data: 'another' }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -330,7 +332,7 @@ describe('applyPatch', () => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| operations: [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| op: 'add-steps', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| after: 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| index: 1, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| steps: [{ from: 'a', to: 'ghost', data: 'bad' }], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -351,18 +353,60 @@ describe('applyPatch', () => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| describe('edge cases', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('add-steps reports out-of-range insertion position', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const root = makeRoot() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const root = makeRoot() // 1 step → valid range is 0..1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = applyPatch(root, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| operations: [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| op: 'add-steps', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| after: 99, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| index: 99, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| steps: [{ from: 'a', to: 'b', data: 'x' }], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result.success).toBe(false) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result.errors[0].message).toContain('out of range') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Error should teach the recovery: the valid range and the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // "omit to append" shortcut. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result.errors[0].message).toContain('omit to append') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('add-steps appends when index is omitted (no need to know steps.length)', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const root = makeRoot() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const initialCount = root.flow.steps?.length ?? 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = applyPatch(root, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| operations: [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| op: 'add-steps', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| steps: [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { from: 'a', to: 'b', data: 'appended-1' }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { from: 'b', to: 'a', data: 'appended-2' }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result.success).toBe(true) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const steps = result.data!.flow.steps! | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(steps).toHaveLength(initialCount + 2) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // The two new steps must be at the very end, in order. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(steps[steps.length - 2]).toMatchObject({ data: 'appended-1' }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(steps[steps.length - 1]).toMatchObject({ data: 'appended-2' }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('add-steps with explicit index === steps.length also appends', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const root = makeRoot() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const initialCount = root.flow.steps?.length ?? 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = applyPatch(root, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| operations: [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| op: 'add-steps', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| index: initialCount, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| steps: [{ from: 'a', to: 'b', data: 'appended' }], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result.success).toBe(true) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const steps = result.data!.flow.steps! | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(steps[steps.length - 1]).toMatchObject({ data: 'appended' }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+395
to
410
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Append-at-length test should also assert step count increases. This test checks last element content but not that insertion actually grows the array. Add a length assertion to lock behavior. ✅ Suggested assertion it('add-steps with explicit index === steps.length also appends', () => {
const root = makeRoot()
const initialCount = root.flow.steps?.length ?? 0
const result = applyPatch(root, {
@@
})
expect(result.success).toBe(true)
const steps = result.data!.flow.steps!
+ expect(steps).toHaveLength(initialCount + 1)
expect(steps[steps.length - 1]).toMatchObject({ data: 'appended' })
})📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('add-steps initializes the steps array when absent', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -379,7 +423,7 @@ describe('applyPatch', () => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| operations: [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| op: 'add-steps', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| after: -1, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Omit index → append to a freshly-created empty array. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| steps: [{ from: 'a', to: 'b', data: 'first' }], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -467,7 +511,7 @@ describe('applyPatch', () => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Now adds a step referencing the removed 'b' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| op: 'add-steps', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| after: -1, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| index: 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| steps: [{ from: 'a', to: 'b', data: 'orphan' }], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: naorsabag/OpenHop
Length of output: 3322
🏁 Script executed:
Repository: naorsabag/OpenHop
Length of output: 3890
🏁 Script executed:
Repository: naorsabag/OpenHop
Length of output: 43
🏁 Script executed:
Repository: naorsabag/OpenHop
Length of output: 176
🏁 Script executed:
Repository: naorsabag/OpenHop
Length of output: 543
🏁 Script executed:
Repository: naorsabag/OpenHop
Length of output: 402
🏁 Script executed:
Repository: naorsabag/OpenHop
Length of output: 43
🏁 Script executed:
Repository: naorsabag/OpenHop
Length of output: 1033
🏁 Script executed:
Repository: naorsabag/OpenHop
Length of output: 637
🏁 Script executed:
# Verify Commander v12 API - check what validation methods are available web_search Commander.js v12 option validation InvalidArgumentErrorRepository: naorsabag/OpenHop
Length of output: 112
🌐 Web query:
Commander.js v12 option validation InvalidArgumentError API💡 Result:
In Commander.js v12, option validation primarily occurs through built-in mechanisms and custom processing. For custom validation in option processors (argParser), throw a commander.InvalidArgumentError(message) from the processor function to trigger Commander's error handling with a formatted message like "error: option '--port ' argument 'abc' is invalid: Not a number." Example from official npm docs and README: function myParseInt(value, dummyPrevious) { const parsedValue = parseInt(value, 10); if (isNaN(parsedValue)) { throw new commander.InvalidArgumentError('Not a number.'); } return parsedValue; } program.option('-p, --port ', 'port number', myParseInt); Commander catches this during parsing and displays: "error: option '-p, --port ' argument 'abc' is invalid: Not a number." Other validation: - Use .choices(['a', 'b']) on Option for predefined values (throws InvalidArgumentError automatically). - Required options: .requiredOption or .makeOptionMandatory. - Unknown options: Error by default; suppress with .allowUnknownOption. - No major API changes for this in v12 changelog; Node 18+ required. InvalidArgumentError extends CommanderError and is used consistently for argument/option validation failures across versions (renamed from older specific errors pre-v8). Official sources: npm package page, GitHub README, jsDocs.io.
Citations:
Validate
--ready-timeoutinstead of silently coercing it.Number.parseInt(opts.readyTimeout, 10) || 60accepts partial parses like10fooas10, converts0to60, and permits negative values. Add a custom option parser using Commander'sInvalidArgumentErrorto validate input upfront.Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents