diff --git a/packages/void/src/cli/domain-args.ts b/packages/void/src/cli/domain-args.ts index ab90c3241..c33bc8732 100644 --- a/packages/void/src/cli/domain-args.ts +++ b/packages/void/src/cli/domain-args.ts @@ -32,6 +32,8 @@ export function parseDomainArgs(args: Array): DomainArgs { projectSlug = rest[++i]; } else if (rest[i] === '--verbose') { verbose = true; + } else if (rest[i].startsWith('-')) { + throw new Error(`domain: Unknown flag '${rest[i]}'.`); } else if (!hostname) { hostname = rest[i]; } diff --git a/packages/void/src/cli/gen-args.ts b/packages/void/src/cli/gen-args.ts index 0879289dd..e763d4322 100644 --- a/packages/void/src/cli/gen-args.ts +++ b/packages/void/src/cli/gen-args.ts @@ -57,12 +57,16 @@ const COLUMN_TYPES = new Set(['string', 'text', 'datetime', 'integer', 'boolean' const VALID_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete']); +// Generated-file name validators (hoisted once); *_HYPHENS variant also allows hyphens (cron/queue/middleware). +const NAME_RE = /^[a-z_][a-z0-9_]*$/; +const NAME_WITH_HYPHENS_RE = /^[a-z_][a-z0-9_-]*$/; + function parseModelArgs(args: Array): GenArgs { const [name, ...rest] = args; if (!name) { throw new Error('gen: Usage: void gen model [columns...].'); } - if (!/^[a-z_][a-z0-9_]*$/.test(name)) { + if (!NAME_RE.test(name)) { throw new Error( 'gen: Model name must be lowercase alphanumeric with underscores and no leading digits.', ); @@ -94,7 +98,7 @@ function parseMigrationArgs(args: Array): GenArgs { if (extra) { throw new Error(`gen: Unexpected argument '${extra}'.`); } - if (!/^[a-z_][a-z0-9_]*$/.test(name)) { + if (!NAME_RE.test(name)) { throw new Error( 'gen: Migration name must be lowercase alphanumeric with underscores and no leading digits.', ); @@ -145,6 +149,11 @@ function parseMiddlewareArgs(args: Array): GenArgs { if (extra) { throw new Error(`gen: Unexpected argument '${extra}'.`); } + if (!NAME_WITH_HYPHENS_RE.test(name)) { + throw new Error( + 'gen: Middleware name must be lowercase alphanumeric with underscores or hyphens and no leading digits.', + ); + } return { subcommand: 'middleware', name }; } @@ -181,7 +190,7 @@ function parseCronArgs(args: Array): GenArgs { if (extra) { throw new Error(`gen: Unexpected argument '${extra}'.`); } - if (!/^[a-z_][a-z0-9_-]*$/.test(name)) { + if (!NAME_WITH_HYPHENS_RE.test(name)) { throw new Error( 'gen: Cron name must be lowercase alphanumeric with underscores or hyphens and no leading digits.', ); @@ -197,7 +206,7 @@ function parseQueueArgs(args: Array): GenArgs { if (extra) { throw new Error(`gen: Unexpected argument '${extra}'.`); } - if (!/^[a-z_][a-z0-9_-]*$/.test(name)) { + if (!NAME_WITH_HYPHENS_RE.test(name)) { throw new Error( 'gen: Queue name must be lowercase alphanumeric with underscores or hyphens and no leading digits.', ); diff --git a/packages/void/src/cli/project-args.ts b/packages/void/src/cli/project-args.ts index 4336e2bf6..3688b1753 100644 --- a/packages/void/src/cli/project-args.ts +++ b/packages/void/src/cli/project-args.ts @@ -38,6 +38,9 @@ export function parseProjectArgs(args: Array): ProjectArgs { if (subcommand === 'status') { const [name, extra] = rest; + if (name?.startsWith('-')) { + throw new Error(`project: Unknown flag '${name}'.`); + } if (extra) { throw new Error(`project: Unexpected argument '${extra}'.`); } @@ -46,6 +49,9 @@ export function parseProjectArgs(args: Array): ProjectArgs { if (subcommand === 'link') { const [name, extra] = rest; + if (name?.startsWith('-')) { + throw new Error(`project: Unknown flag '${name}'.`); + } if (extra) { throw new Error(`project: Unexpected argument '${extra}'.`); } @@ -88,6 +94,9 @@ export function parseProjectArgs(args: Array): ProjectArgs { if (subcommand === 'rollback') { const [deployId, extra] = rest; + if (deployId?.startsWith('-')) { + throw new Error(`project: Unknown flag '${deployId}'.`); + } if (extra) { throw new Error(`project: Unexpected argument '${extra}'.`); } @@ -96,6 +105,9 @@ export function parseProjectArgs(args: Array): ProjectArgs { if (subcommand === 'cancel') { const [deployId, extra] = rest; + if (deployId?.startsWith('-')) { + throw new Error(`project: Unknown flag '${deployId}'.`); + } if (extra) { throw new Error(`project: Unexpected argument '${extra}'.`); } diff --git a/packages/void/test/unit/domain-args.test.ts b/packages/void/test/unit/domain-args.test.ts new file mode 100644 index 000000000..331f8c5f1 --- /dev/null +++ b/packages/void/test/unit/domain-args.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vite-plus/test'; +import { parseDomainArgs } from '../../src/cli/domain-args.ts'; + +describe('parseDomainArgs', () => { + it('parses a hostname positional', () => { + expect(parseDomainArgs(['add', 'example.com'])).toEqual({ + subcommand: 'add', + hostname: 'example.com', + projectSlug: undefined, + verbose: false, + }); + }); + + it('parses --project and --verbose', () => { + expect(parseDomainArgs(['status', 'example.com', '--project', 'my-app', '--verbose'])).toEqual({ + subcommand: 'status', + hostname: 'example.com', + projectSlug: 'my-app', + verbose: true, + }); + }); + + it('rejects an unknown flag instead of capturing it as the hostname', () => { + expect(() => parseDomainArgs(['add', '--foo', 'example.com'])).toThrow( + "domain: Unknown flag '--foo'.", + ); + }); + + it('rejects an unknown flag even when a hostname is already present', () => { + expect(() => parseDomainArgs(['status', 'example.com', '--foo'])).toThrow( + "domain: Unknown flag '--foo'.", + ); + }); +}); diff --git a/packages/void/test/unit/gen-args.test.ts b/packages/void/test/unit/gen-args.test.ts index c6ee0b31d..4bf75fd50 100644 --- a/packages/void/test/unit/gen-args.test.ts +++ b/packages/void/test/unit/gen-args.test.ts @@ -174,6 +174,19 @@ describe('parseGenArgs', () => { "gen: Unexpected argument 'extra'.", ); }); + + it('rejects an invalid name (e.g. a flag) instead of scaffolding a bogus file', () => { + expect(() => parseGenArgs(['middleware', '--foo'])).toThrow( + 'gen: Middleware name must be lowercase alphanumeric with underscores or hyphens and no leading digits.', + ); + }); + + it('allows hyphenated middleware names', () => { + expect(parseGenArgs(['middleware', 'rate-limit'])).toEqual({ + subcommand: 'middleware', + name: 'rate-limit', + }); + }); }); describe('ssr', () => { diff --git a/packages/void/test/unit/project-args.test.ts b/packages/void/test/unit/project-args.test.ts index 4e36833ff..81bda5a26 100644 --- a/packages/void/test/unit/project-args.test.ts +++ b/packages/void/test/unit/project-args.test.ts @@ -101,4 +101,32 @@ describe('parseProjectArgs', () => { ); }); }); + + describe('unknown-flag rejection', () => { + // status/link/rollback/cancel previously captured a leading flag as the positional id. + it('rejects an unknown flag for status instead of capturing it as the name', () => { + expect(() => parseProjectArgs(['status', '--foo'])).toThrow("project: Unknown flag '--foo'."); + }); + + it('rejects an unknown flag for link', () => { + expect(() => parseProjectArgs(['link', '--foo'])).toThrow("project: Unknown flag '--foo'."); + }); + + it('rejects an unknown flag for rollback', () => { + expect(() => parseProjectArgs(['rollback', '--foo'])).toThrow( + "project: Unknown flag '--foo'.", + ); + }); + + it('rejects an unknown flag for cancel', () => { + expect(() => parseProjectArgs(['cancel', '--foo'])).toThrow("project: Unknown flag '--foo'."); + }); + + it('still accepts a valid positional id', () => { + expect(parseProjectArgs(['status', 'my-app'])).toEqual({ + subcommand: 'status', + name: 'my-app', + }); + }); + }); });