Skip to content

Commit 821c468

Browse files
committed
feat(create-tbk-app): add support for AI agents in CLI and prompts
- Introduced `--agents` option in CLI for specifying comma-separated AI agents/IDEs. - Enhanced prompts to collect agent preferences from users. - Updated configuration types to include agents. - Implemented parsing logic for agent input validation. - Added new templates and documentation for agent integration. - Refactored related functions to accommodate new agent features.
1 parent 37e1afc commit 821c468

File tree

27 files changed

+4152
-35
lines changed

27 files changed

+4152
-35
lines changed

packages/create-tbk-app/src/cli.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
checkDirectoryExists,
2424
} from './utils/validation.js';
2525
import type {
26+
AgentId,
2627
AuthType,
2728
CacheProvider,
2829
EmailProvider,
@@ -51,6 +52,7 @@ interface CliOptions {
5152
pm?: string;
5253
skipGit?: boolean;
5354
skipInstall?: boolean;
55+
agents?: string;
5456
yes?: boolean;
5557
force?: boolean;
5658
}
@@ -84,6 +86,7 @@ program
8486
.option('--pm <manager>', 'Package manager (pnpm, npm, yarn)')
8587
.option('--skip-git', 'Skip git initialization')
8688
.option('--skip-install', 'Skip dependency installation')
89+
.option('--agents <agents>', 'Comma-separated AI agents/IDEs (claude,cursor,other)')
8790
.option('-y, --yes', 'Skip prompts and accept defaults')
8891
.option('--force', 'Overwrite target directory without prompting')
8992
.action(async (projectName: string | undefined, options: CliOptions) => {
@@ -152,6 +155,7 @@ interface NormalizedOptions {
152155
packageManager?: PackageManager;
153156
skipGit?: boolean;
154157
skipInstall?: boolean;
158+
agents?: AgentId[];
155159
yes: boolean;
156160
force: boolean;
157161
}
@@ -195,6 +199,7 @@ function normalizeOptions(options: CliOptions): NormalizedOptions {
195199
packageManager: parseChoice(options.pm, ['pnpm', 'npm', 'yarn'], 'pm'),
196200
skipGit: getBooleanOption(options, 'skipGit'),
197201
skipInstall: getBooleanOption(options, 'skipInstall'),
202+
agents: parseAgents(options.agents),
198203
yes: options.yes ?? false,
199204
force: options.force ?? false,
200205
};
@@ -286,6 +291,7 @@ function mapOptionsToDefaults(
286291
realtime: options.realtime,
287292
admin: options.admin,
288293
observability: options.observability,
294+
agents: options.agents,
289295
packageManager: options.packageManager,
290296
skipGit: options.skipGit,
291297
skipInstall: options.skipInstall,
@@ -491,6 +497,7 @@ function buildConfigFromOptions(
491497
realtime,
492498
admin,
493499
observability,
500+
agents: options.agents ?? [],
494501
packageManager: options.packageManager ?? 'pnpm',
495502
skipGit: resolveBooleanOption(options.skipGit, false),
496503
skipInstall: resolveBooleanOption(options.skipInstall, false),
@@ -516,6 +523,35 @@ function parseChoice<T extends string>(
516523
return normalized;
517524
}
518525

526+
function parseAgents(value: string | undefined): AgentId[] | undefined {
527+
if (value === undefined) {
528+
return undefined;
529+
}
530+
531+
const allowed: AgentId[] = ['claude', 'cursor', 'other'];
532+
const selections = value
533+
.split(',')
534+
.map((item) => item.trim().toLowerCase())
535+
.filter((item) => item.length > 0);
536+
537+
if (selections.length === 0) {
538+
return [];
539+
}
540+
541+
const invalid = selections.filter(
542+
(item): item is string => !allowed.includes(item as AgentId),
543+
);
544+
545+
if (invalid.length > 0) {
546+
throw new Error(
547+
`Invalid value(s) "${invalid.join(', ')}" for --agents. Allowed values: ${allowed.join(', ')}`,
548+
);
549+
}
550+
551+
const unique = Array.from(new Set(selections)) as AgentId[];
552+
return unique;
553+
}
554+
519555
function getBooleanOption(options: CliOptions, key: keyof CliOptions) {
520556
const value = options[key];
521557
return typeof value === 'boolean' ? value : undefined;

packages/create-tbk-app/src/generators/project.generator.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,10 @@ Selected features: ${JSON.stringify(context, null, 2)}
173173
}
174174
}
175175
}
176+
177+
if (context.AGENT_CLAUDE) {
178+
await copyClaudeCommandsIfAvailable(targetDir, templateBaseDir);
179+
}
176180
}
177181

178182
async function copyAndRenderDirectory(
@@ -220,3 +224,25 @@ async function copyAndRenderDirectory(
220224
}
221225
}
222226
}
227+
228+
async function copyClaudeCommandsIfAvailable(
229+
targetDir: string,
230+
templateBaseDir: string,
231+
): Promise<void> {
232+
const targetCommandsDir = path.join(targetDir, '.claude', 'commands');
233+
const candidateSources = [
234+
path.resolve(templateBaseDir, '..', '..', '..', '.claude', 'commands'),
235+
path.resolve(process.cwd(), '.claude', 'commands'),
236+
];
237+
238+
for (const source of candidateSources) {
239+
if (!(await pathExists(source))) {
240+
continue;
241+
}
242+
243+
await fs.ensureDir(targetCommandsDir);
244+
await fs.emptyDir(targetCommandsDir);
245+
await fs.copy(source, targetCommandsDir, { overwrite: true, errorOnExist: false });
246+
return;
247+
}
248+
}

packages/create-tbk-app/src/prompts.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import {
66
select,
77
text,
88
cancel,
9+
multiselect,
910
} from '@clack/prompts';
1011
import type {
12+
AgentId,
1113
AuthType,
1214
CacheProvider,
1315
EmailProvider,
@@ -42,6 +44,7 @@ export interface PromptDefaults {
4244
packageManager?: PackageManager;
4345
skipGit?: BooleanLike;
4446
skipInstall?: BooleanLike;
47+
agents?: AgentId[];
4548
}
4649

4750
export async function collectProjectConfig(
@@ -84,6 +87,7 @@ export async function collectProjectConfig(
8487
}
8588

8689
const basicOptions = await collectBasicOptions(defaults);
90+
const agentOptions = await collectAgentOptions(defaults);
8791

8892
const finalConfig: ProjectConfig = {
8993
projectName,
@@ -99,6 +103,7 @@ export async function collectProjectConfig(
99103
realtime: customConfig.realtime ?? false,
100104
admin: customConfig.admin ?? false,
101105
observability: customConfig.observability!,
106+
agents: agentOptions,
102107
packageManager: basicOptions.packageManager,
103108
skipGit: basicOptions.skipGit,
104109
skipInstall: basicOptions.skipInstall,
@@ -123,6 +128,7 @@ export function renderSummary(config: ProjectConfig) {
123128
`Realtime: ${config.realtime ? 'enabled' : 'disabled'}`,
124129
`Admin: ${config.admin ? 'enabled' : 'disabled'}`,
125130
`Observability: ${config.observability}`,
131+
`Agents/IDEs: ${config.agents.length > 0 ? config.agents.join(', ') : 'none'}`,
126132
`Package manager: ${config.packageManager}`,
127133
`Initialize git repo: ${config.skipGit ? 'no' : 'yes'}`,
128134
`Install dependencies: ${config.skipInstall ? 'later' : 'now'}`,
@@ -274,6 +280,20 @@ async function collectBasicOptions(defaults: PromptDefaults) {
274280
} satisfies Pick<ProjectConfig, 'packageManager' | 'skipGit' | 'skipInstall'>;
275281
}
276282

283+
async function collectAgentOptions(defaults: PromptDefaults): Promise<AgentId[]> {
284+
const result = await promptMultiSelectValue<AgentId>({
285+
message: 'Which AI agents/IDEs do you use?',
286+
options: [
287+
{ label: 'Claude Code', value: 'claude', hint: 'Includes .claude commands' },
288+
{ label: 'Cursor', value: 'cursor', hint: 'Includes Cursor rules' },
289+
{ label: 'Other editor/agent', value: 'other', hint: 'Adds AGENTS.md guide' },
290+
],
291+
initialValue: defaults.agents ?? [],
292+
});
293+
294+
return result;
295+
}
296+
277297
async function promptSelectValue<T extends string>({
278298
message,
279299
options,
@@ -298,6 +318,31 @@ async function promptSelectValue<T extends string>({
298318
return ensureNotCancelled(result);
299319
}
300320

321+
async function promptMultiSelectValue<T extends string>({
322+
message,
323+
options,
324+
initialValue,
325+
}: {
326+
message: string;
327+
options: Choice<T>[];
328+
initialValue?: T[];
329+
}): Promise<T[]> {
330+
const multiselectPrompt = multiselect as unknown as (opts: {
331+
message: string;
332+
options: Choice<T>[];
333+
initialValue?: T[];
334+
}) => Promise<T[] | symbol>;
335+
336+
const result = await multiselectPrompt({
337+
message,
338+
options,
339+
initialValue,
340+
});
341+
342+
const values = ensureNotCancelled(result);
343+
return Array.isArray(values) ? values : [];
344+
}
345+
301346
async function promptConfirmValue(
302347
message: string,
303348
initialValue: boolean,

packages/create-tbk-app/src/types/config.types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
export type PresetType = 'minimal' | 'standard' | 'full' | 'custom';
22

3+
export type AgentId = 'claude' | 'cursor' | 'other';
4+
35
export type AuthType = 'none' | 'jwt' | 'jwt-sessions';
46

57
export type CacheProvider = 'none' | 'memory' | 'redis';
@@ -28,6 +30,7 @@ export interface ProjectConfig {
2830
admin: boolean;
2931
queueDashboard: boolean;
3032
observability: ObservabilityLevel;
33+
agents: AgentId[];
3134
packageManager: PackageManager;
3235
skipGit: boolean;
3336
skipInstall: boolean;
@@ -73,6 +76,10 @@ export interface TemplateContext {
7376
SECURITY: boolean;
7477

7578
PRESET: PresetType;
79+
80+
AGENT_CLAUDE: boolean;
81+
AGENT_CURSOR: boolean;
82+
AGENT_OTHER: boolean;
7683
}
7784

7885
export interface PresetConfig {

packages/create-tbk-app/src/utils/template.engine.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import type { ProjectConfig, TemplateContext } from '../types/config.types.js';
33
import { toKebabCase, toPascalCase } from './validation.js';
44

55
export function createTemplateContext(config: ProjectConfig): TemplateContext {
6+
const agents = config.agents ?? [];
7+
68
return {
79
PROJECT_NAME: config.projectName,
810
PROJECT_NAME_KEBAB: toKebabCase(config.projectName),
@@ -53,6 +55,11 @@ export function createTemplateContext(config: ProjectConfig): TemplateContext {
5355

5456
// Preset
5557
PRESET: config.preset,
58+
59+
// Agents/IDEs
60+
AGENT_CLAUDE: agents.includes('claude'),
61+
AGENT_CURSOR: agents.includes('cursor'),
62+
AGENT_OTHER: agents.includes('other'),
5663
};
5764
}
5865

@@ -125,6 +132,22 @@ export function shouldIncludeFile(filePath: string, context: TemplateContext): b
125132
if (!context.SECURITY) return false;
126133
}
127134

135+
if (fileName.includes('/.claude/') || fileName.includes('\\.claude\\')) {
136+
if (!context.AGENT_CLAUDE) return false;
137+
}
138+
139+
if (fileName.includes('/.cursor/') || fileName.includes('\\.cursor\\')) {
140+
if (!context.AGENT_CURSOR) return false;
141+
}
142+
143+
if (fileName.endsWith('/claude.md') || fileName.endsWith('\\claude.md')) {
144+
if (!context.AGENT_CLAUDE) return false;
145+
}
146+
147+
if (fileName.endsWith('/agents.md') || fileName.endsWith('\\agents.md')) {
148+
if (!context.AGENT_OTHER) return false;
149+
}
150+
128151
return true;
129152
}
130153

packages/create-tbk-app/templates/admin/src/plugins/admin/index.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,9 @@ export const adminDashboardPlugin: PluginFactory<AdminDashboardOptions> = (
3232
options,
3333

3434
register({ app, port }) {
35-
3635
app.use(express.json());
3736
app.use(express.urlencoded({ extended: true }));
38-
app.use(cookieParser())
37+
app.use(cookieParser());
3938

4039
app.get(`/admin/login`, (req, res) => {
4140
const loginPath = path.join(
@@ -84,13 +83,9 @@ export const adminDashboardPlugin: PluginFactory<AdminDashboardOptions> = (
8483
res.redirect(next);
8584
});
8685

87-
app.post(`/admin/logout`, (req, res) => {
86+
app.post(`/admin/logout`, (_req, res) => {
8887
clearAdminCookie(res);
89-
const acceptsJson = req.headers.accept?.includes('application/json');
90-
if (acceptsJson) {
91-
return res.json({ ok: true });
92-
}
93-
88+
return res.json({ ok: true });
9489
});
9590

9691
app.use(`/admin/api`, adminAuthGuardApi, adminApiRouter);
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
2+

0 commit comments

Comments
 (0)