Skip to content

Commit 35fbf83

Browse files
committed
feat: add parseFlags for non-interactive CLI mode
1 parent 031441b commit 35fbf83

2 files changed

Lines changed: 124 additions & 1 deletion

File tree

src/cli.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,77 @@ describe('dispatchSubcommand', () => {
7070
expect(await dispatchExists(join(dispatchTmpDir, 'src/app-extensions/panel/index.ts'))).toBe(true);
7171
});
7272
});
73+
74+
describe('parseFlags', () => {
75+
it('returns empty partial when no flags are present', async () => {
76+
const { parseFlags } = await import('./cli.js');
77+
expect(parseFlags(['node', 'cli.js'])).toEqual({});
78+
});
79+
80+
it('parses --project-name', async () => {
81+
const { parseFlags } = await import('./cli.js');
82+
expect(parseFlags(['node', 'cli.js', '--project-name', 'my-app'])).toMatchObject({ nameOrPath: 'my-app' });
83+
});
84+
85+
it('parses --database postgres', async () => {
86+
const { parseFlags } = await import('./cli.js');
87+
expect(parseFlags(['node', 'cli.js', '--database', 'postgres'])).toMatchObject({ database: 'postgres' });
88+
});
89+
90+
it('parses --database mysql', async () => {
91+
const { parseFlags } = await import('./cli.js');
92+
expect(parseFlags(['node', 'cli.js', '--database', 'mysql'])).toMatchObject({ database: 'mysql' });
93+
});
94+
95+
it('parses --database sqlite', async () => {
96+
const { parseFlags } = await import('./cli.js');
97+
expect(parseFlags(['node', 'cli.js', '--database', 'sqlite'])).toMatchObject({ database: 'sqlite' });
98+
});
99+
100+
it('throws on invalid --database', async () => {
101+
const { parseFlags } = await import('./cli.js');
102+
expect(() => parseFlags(['node', 'cli.js', '--database', 'oracle'])).toThrow(
103+
'Invalid database "oracle". Choose one of: postgres, mysql, sqlite.',
104+
);
105+
});
106+
107+
it('parses --app-extensions none as empty array', async () => {
108+
const { parseFlags } = await import('./cli.js');
109+
expect(parseFlags(['node', 'cli.js', '--app-extensions', 'none'])).toMatchObject({ appExtensions: [] });
110+
});
111+
112+
it('parses --app-extensions custom-panel', async () => {
113+
const { parseFlags } = await import('./cli.js');
114+
expect(parseFlags(['node', 'cli.js', '--app-extensions', 'custom-panel'])).toMatchObject({
115+
appExtensions: ['custom-panel'],
116+
});
117+
});
118+
119+
it('parses --app-extensions custom-panel,custom-modal', async () => {
120+
const { parseFlags } = await import('./cli.js');
121+
expect(parseFlags(['node', 'cli.js', '--app-extensions', 'custom-panel,custom-modal'])).toMatchObject({
122+
appExtensions: ['custom-panel', 'custom-modal'],
123+
});
124+
});
125+
126+
it('throws on invalid --app-extensions value', async () => {
127+
const { parseFlags } = await import('./cli.js');
128+
expect(() => parseFlags(['node', 'cli.js', '--app-extensions', 'custom-widget'])).toThrow(
129+
'Invalid app extension type "custom-widget". Choose from: custom-panel, custom-modal.',
130+
);
131+
});
132+
133+
it('throws on --project-name with empty value', async () => {
134+
const { parseFlags } = await import('./cli.js');
135+
expect(() => parseFlags(['node', 'cli.js', '--project-name', ' '])).toThrow(
136+
'--project-name cannot be empty.',
137+
);
138+
});
139+
140+
it('parses all three flags together', async () => {
141+
const { parseFlags } = await import('./cli.js');
142+
expect(
143+
parseFlags(['node', 'cli.js', '--project-name', 'my-app', '--database', 'sqlite', '--app-extensions', 'none']),
144+
).toEqual({ nameOrPath: 'my-app', database: 'sqlite', appExtensions: [] });
145+
});
146+
});

src/cli.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#!/usr/bin/env node
12
import * as clack from '@clack/prompts';
23
import { realpathSync } from 'node:fs';
34
import { basename, resolve } from 'node:path';
@@ -7,7 +8,7 @@ import { promptDatabase } from './prompts/database.js';
78
import { promptProjectName } from './prompts/projectName.js';
89
import { nodeGenerator } from './generators/node/index.js';
910
import { addAppExtension } from './subcommands/addAppExtension.js';
10-
import type { AppExtensionType } from './generators/interface.js';
11+
import type { AppExtensionType, Database } from './generators/interface.js';
1112
import { isAppExtensionType } from './generators/interface.js';
1213

1314
interface NextStepOptions {
@@ -47,6 +48,54 @@ export function isCliEntrypoint(
4748
}
4849
}
4950

51+
interface ParsedFlags {
52+
nameOrPath?: string;
53+
database?: Database;
54+
appExtensions?: AppExtensionType[];
55+
}
56+
57+
export function parseFlags(argv: string[]): ParsedFlags {
58+
const result: ParsedFlags = {};
59+
60+
const nameIdx = argv.indexOf('--project-name');
61+
if (nameIdx !== -1) {
62+
const value = argv[nameIdx + 1];
63+
if (!value || value.startsWith('--')) throw new Error('--project-name requires a value.');
64+
if (!value.trim()) throw new Error('--project-name cannot be empty.');
65+
result.nameOrPath = value.trim();
66+
}
67+
68+
const dbIdx = argv.indexOf('--database');
69+
if (dbIdx !== -1) {
70+
const value = argv[dbIdx + 1];
71+
if (!value || value.startsWith('--')) throw new Error('--database requires a value.');
72+
const valid: Database[] = ['postgres', 'mysql', 'sqlite'];
73+
if (!valid.includes(value as Database)) {
74+
throw new Error(`Invalid database "${value}". Choose one of: postgres, mysql, sqlite.`);
75+
}
76+
result.database = value as Database;
77+
}
78+
79+
const extIdx = argv.indexOf('--app-extensions');
80+
if (extIdx !== -1) {
81+
const value = argv[extIdx + 1];
82+
if (!value || value.startsWith('--')) throw new Error('--app-extensions requires a value.');
83+
if (value === 'none') {
84+
result.appExtensions = [];
85+
} else {
86+
const types = value.split(',');
87+
for (const type of types) {
88+
if (!isAppExtensionType(type)) {
89+
throw new Error(`Invalid app extension type "${type}". Choose from: custom-panel, custom-modal.`);
90+
}
91+
}
92+
result.appExtensions = types as AppExtensionType[];
93+
}
94+
}
95+
96+
return result;
97+
}
98+
5099
export async function dispatchSubcommand(argv: string[]): Promise<boolean> {
51100
const subcommand = argv[2];
52101
const outputDirIdx = argv.indexOf('--output-dir');

0 commit comments

Comments
 (0)