@baseplate-dev/project-builder-cli 0.3.3 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function addDevServerCommand(program: Command): void;
@@ -0,0 +1,24 @@
1
+ import path from 'node:path';
2
+ import { createServiceActionContext } from '#src/utils/create-service-action-context.js';
3
+ import { logger } from '../services/logger.js';
4
+ export function addDevServerCommand(program) {
5
+ program
6
+ .command('dev-server')
7
+ .description('Start the development server with MCP integration')
8
+ .option('-c, --current-dir <path>', 'Current directory (default: process.cwd())')
9
+ .action(async (options) => {
10
+ const cwd = options.currentDir
11
+ ? path.resolve(options.currentDir)
12
+ : process.cwd();
13
+ logger.info('Starting Baseplate development server...', {
14
+ cwd,
15
+ });
16
+ const context = await createServiceActionContext();
17
+ const { DevServer } = await import('@baseplate-dev/project-builder-server/dev-server');
18
+ const devServer = new DevServer({
19
+ cwd,
20
+ context,
21
+ });
22
+ await devServer.start();
23
+ });
24
+ }
@@ -1,35 +1,23 @@
1
- import { createSchemaParserContext } from '#src/services/schema-parser-context.js';
2
- import { getUserConfig } from '#src/services/user-config.js';
3
- import { expandPathWithTilde } from '#src/utils/path.js';
4
- import { logger } from '../services/logger.js';
1
+ import { diffProjectAction, invokeServiceActionAsCli, } from '@baseplate-dev/project-builder-server/actions';
2
+ import { createServiceActionContext } from '#src/utils/create-service-action-context.js';
3
+ import { resolveProject } from '#src/utils/list-projects.js';
5
4
  /**
6
5
  * Adds a diff command to the program.
7
6
  * @param program - The program to add the command to.
8
7
  */
9
8
  export function addDiffCommand(program) {
10
9
  program
11
- .command('diff [directory]')
12
- .description('Show diff between generated output and current working directory')
10
+ .command('diff [project]')
11
+ .description('Show diff between generated output and project (name or directory)')
13
12
  .option('--compact', 'Show compact diff format instead of unified diff')
14
- .option('--app <apps...>', 'Filter by specific app names')
15
- .option('--glob <patterns...>', 'Filter files by glob patterns')
16
- .option('--no-ignore-file', 'Disable .baseplateignore file filtering')
17
- .action(async (directory, options) => {
18
- const { diffProject } = await import('@baseplate-dev/project-builder-server');
19
- const resolvedDirectory = directory
20
- ? expandPathWithTilde(directory)
21
- : process.cwd();
22
- const context = await createSchemaParserContext(resolvedDirectory);
23
- const userConfig = await getUserConfig();
24
- await diffProject({
25
- directory: resolvedDirectory,
26
- logger,
27
- context,
28
- userConfig,
29
- compact: options.compact ?? false,
30
- appFilter: options.app,
31
- globPatterns: options.glob,
32
- useIgnoreFile: options.ignoreFile ?? true,
33
- });
13
+ .option('--package <packages...>', 'Filter by specific package names')
14
+ .option('--include <patterns...>', 'Filter files by glob patterns')
15
+ .action(async (project, options) => {
16
+ const resolvedProject = await resolveProject(project);
17
+ const context = await createServiceActionContext(resolvedProject);
18
+ await invokeServiceActionAsCli(diffProjectAction, {
19
+ project: resolvedProject.name,
20
+ ...options,
21
+ }, context);
34
22
  });
35
23
  }
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function addMcpCommand(program: Command): void;
@@ -0,0 +1,11 @@
1
+ import { createServiceActionContext } from '#src/utils/create-service-action-context.js';
2
+ export function addMcpCommand(program) {
3
+ program
4
+ .command('mcp')
5
+ .description('Start the MCP server in stdio mode')
6
+ .action(async () => {
7
+ const context = await createServiceActionContext();
8
+ const { startMcpStdioServer } = await import('@baseplate-dev/project-builder-server/dev-server');
9
+ await startMcpStdioServer(context);
10
+ });
11
+ }
@@ -0,0 +1,6 @@
1
+ import type { Command } from 'commander';
2
+ /**
3
+ * Adds project management commands to the program.
4
+ * @param program - The program to add the commands to.
5
+ */
6
+ export declare function addProjectsCommand(program: Command): void;
@@ -0,0 +1,91 @@
1
+ import { resolveProjects } from '../utils/project-resolver.js';
2
+ /**
3
+ * Adds project management commands to the program.
4
+ * @param program - The program to add the commands to.
5
+ */
6
+ export function addProjectsCommand(program) {
7
+ const projectsCommand = program
8
+ .command('projects')
9
+ .description('Manage and discover projects');
10
+ // Projects list subcommand
11
+ projectsCommand
12
+ .command('list')
13
+ .description('List all available projects')
14
+ .option('--json', 'Output in JSON format', false)
15
+ .option('--include-examples', 'Include example projects (overrides INCLUDE_EXAMPLES env)', undefined)
16
+ .action(async (options) => {
17
+ await handleListProjects(options);
18
+ });
19
+ }
20
+ async function handleListProjects(options) {
21
+ try {
22
+ const projectMap = await resolveProjects({
23
+ includeExamples: options.includeExamples ?? process.env.INCLUDE_EXAMPLES === 'true',
24
+ defaultToCwd: true,
25
+ });
26
+ if (projectMap.size === 0) {
27
+ console.info('No projects found.');
28
+ console.info('Try setting PROJECT_DIRECTORIES or INCLUDE_EXAMPLES=true environment variables.');
29
+ return;
30
+ }
31
+ if (options.json) {
32
+ const projects = [...projectMap.values()].map((project) => ({
33
+ name: project.name,
34
+ path: project.path,
35
+ isExample: project.isExample,
36
+ description: project.packageJson.description ?? null,
37
+ version: project.packageJson.version ?? null,
38
+ }));
39
+ console.info(JSON.stringify(projects, null, 2));
40
+ }
41
+ else {
42
+ console.info(`Found ${projectMap.size} project(s):\n`);
43
+ // Group projects by type
44
+ const examples = [];
45
+ const regular = [];
46
+ for (const project of projectMap.values()) {
47
+ if (project.isExample) {
48
+ examples.push(project);
49
+ }
50
+ else {
51
+ regular.push(project);
52
+ }
53
+ }
54
+ // Display regular projects first
55
+ if (regular.length > 0) {
56
+ console.info('📦 Projects:');
57
+ for (const project of regular) {
58
+ console.info(` ${project.name}`);
59
+ if (typeof project.packageJson.description === 'string') {
60
+ console.info(` ${project.packageJson.description}`);
61
+ }
62
+ console.info(` Path: ${project.path}`);
63
+ console.info();
64
+ }
65
+ }
66
+ // Display examples
67
+ if (examples.length > 0) {
68
+ console.info('📚 Examples:');
69
+ for (const project of examples) {
70
+ console.info(` ${project.name}`);
71
+ if (typeof project.packageJson.description === 'string') {
72
+ console.info(` ${project.packageJson.description}`);
73
+ }
74
+ console.info(` Path: ${project.path}`);
75
+ console.info();
76
+ }
77
+ }
78
+ console.info('Usage examples:');
79
+ console.info(' pnpm baseplate serve <project-name>');
80
+ console.info(' pnpm baseplate diff <project-name>');
81
+ console.info(' pnpm baseplate templates extract <project-name> <app>');
82
+ console.info();
83
+ console.info('Tip: Set INCLUDE_EXAMPLES=true to automatically include example projects');
84
+ }
85
+ }
86
+ catch (error) {
87
+ const errorMessage = error instanceof Error ? error.message : String(error);
88
+ console.error(`Failed to list projects: ${errorMessage}`);
89
+ throw error;
90
+ }
91
+ }
@@ -1,9 +1,10 @@
1
- import { getDefaultPlugins } from '@baseplate-dev/project-builder-common';
1
+ import { discoverProjects } from '@baseplate-dev/project-builder-server/actions';
2
2
  import path from 'node:path';
3
3
  import { packageDirectory } from 'pkg-dir';
4
4
  import { getUserConfig } from '#src/services/user-config.js';
5
5
  import { getEnabledFeatureFlags } from '../services/feature-flags.js';
6
6
  import { logger } from '../services/logger.js';
7
+ import { createServiceActionContext } from '../utils/create-service-action-context.js';
7
8
  import { expandPathWithTilde } from '../utils/path.js';
8
9
  import { resolveModule } from '../utils/resolve.js';
9
10
  import { getPackageVersion } from '../utils/version.js';
@@ -12,25 +13,27 @@ export async function serveWebServer(directories, { browser, port, logger: overr
12
13
  const projectBuilderWebDir = await packageDirectory({
13
14
  cwd: resolveModule('@baseplate-dev/project-builder-web/package.json'),
14
15
  });
15
- const resolvedDirectories = directories.map((dir) => expandPathWithTilde(dir));
16
- const builtInPlugins = await getDefaultPlugins(logger);
17
16
  const version = await getPackageVersion();
18
17
  if (!projectBuilderWebDir) {
19
18
  throw new Error(`Unable to find project-builder-web package to host website`);
20
19
  }
21
20
  const userConfig = await getUserConfig();
21
+ const context = await createServiceActionContext();
22
22
  const serviceManager = new BuilderServiceManager({
23
- initialDirectories: resolvedDirectories,
24
23
  cliVersion: version,
25
- builtInPlugins,
26
- userConfig,
27
24
  skipCommands,
28
25
  cliFilePath: process.argv[1],
26
+ serviceActionContext: context,
29
27
  });
28
+ // Apply PORT_OFFSET if set
29
+ const portOffset = process.env.PORT_OFFSET
30
+ ? Number.parseInt(process.env.PORT_OFFSET, 10)
31
+ : 0;
32
+ const effectivePort = port ?? DEFAULT_SERVER_PORT + portOffset;
30
33
  const fastifyInstance = await startWebServer({
31
34
  serviceManager,
32
35
  browser,
33
- port: port ?? DEFAULT_SERVER_PORT,
36
+ port: effectivePort,
34
37
  cliVersion: version,
35
38
  projectBuilderStaticDir: path.join(projectBuilderWebDir, 'dist'),
36
39
  logger: overrideLogger ?? logger,
@@ -49,8 +52,24 @@ export function addServeCommand(program) {
49
52
  .option('--browser', 'Opens browser with project builder web service', !process.env.NO_BROWSER || process.env.NO_BROWSER === 'false')
50
53
  .option('--no-browser', 'Do not start browser')
51
54
  .option('--port <number>', 'Port to listen on', Number.parseInt, process.env.PORT ? Number.parseInt(process.env.PORT, 10) : undefined)
52
- .argument('[directories...]', 'Directories to serve', process.env.PROJECT_DIRECTORIES?.split(',') ?? ['.'])
53
- .action(async (directories, { browser, port }) => {
54
- await serveWebServer(directories, { browser, port });
55
+ .argument('[projects...]', 'Project names or directories to serve', process.env.PROJECT_DIRECTORIES?.split(',') ?? ['.'])
56
+ .action(async (projects, { browser, port }) => {
57
+ try {
58
+ // Resolve directories and discover projects
59
+ const resolvedDirectories = projects.length > 0
60
+ ? projects.map((dir) => expandPathWithTilde(dir))
61
+ : [process.cwd()];
62
+ const discoveredProjects = await discoverProjects(resolvedDirectories, logger);
63
+ const projectDirectories = discoveredProjects.map((p) => p.directory);
64
+ const projectNames = discoveredProjects.map((p) => p.name);
65
+ if (projectNames.length > 0) {
66
+ logger.info(`Serving ${projectNames.length} project(s): ${projectNames.join(', ')}`);
67
+ }
68
+ await serveWebServer(projectDirectories, { browser, port });
69
+ }
70
+ catch (error) {
71
+ logger.error(`Failed to resolve projects: ${error instanceof Error ? error.message : String(error)}`);
72
+ throw error;
73
+ }
55
74
  });
56
75
  }
@@ -2,6 +2,7 @@ import { confirm } from '@inquirer/prompts';
2
2
  import { createSchemaParserContext } from '#src/services/schema-parser-context.js';
3
3
  import { getUserConfig } from '#src/services/user-config.js';
4
4
  import { expandPathWithTilde } from '#src/utils/path.js';
5
+ import { resolveProject } from '#src/utils/project-resolver.js';
5
6
  import { logger } from '../services/logger.js';
6
7
  /**
7
8
  * Adds snapshot management commands to the program.
@@ -13,14 +14,24 @@ export function addSnapshotCommand(program) {
13
14
  .description('Manage project snapshots for persistent differences');
14
15
  // snapshot add command
15
16
  snapshotCommand
16
- .command('add <project-directory> <app> <files...>')
17
+ .command('add <project> <app> <files...>')
17
18
  .description('Add files to snapshot (use --deleted for intentionally deleted files)')
18
19
  .option('--deleted', 'Mark files as intentionally deleted in snapshot')
19
20
  .option('--snapshot-dir <directory>', 'Snapshot directory', '.baseplate-snapshot')
20
- .action(async (projectDirectory, app, files, options) => {
21
+ .action(async (project, app, files, options) => {
21
22
  try {
22
23
  const { addFilesToSnapshot } = await import('@baseplate-dev/project-builder-server');
23
- const resolvedDirectory = expandPathWithTilde(projectDirectory);
24
+ let resolvedDirectory;
25
+ if (project.includes('/') || project.includes('\\')) {
26
+ // It's a path, expand it
27
+ resolvedDirectory = expandPathWithTilde(project);
28
+ }
29
+ else {
30
+ // It's a project name, resolve it
31
+ const projectInfo = await resolveProject(project);
32
+ resolvedDirectory = projectInfo.path;
33
+ logger.info(`Running snapshot add for project: ${projectInfo.name}`);
34
+ }
24
35
  const context = await createSchemaParserContext(resolvedDirectory);
25
36
  await addFilesToSnapshot(files, !!options.deleted, {
26
37
  projectDirectory: resolvedDirectory,
@@ -37,13 +48,23 @@ export function addSnapshotCommand(program) {
37
48
  });
38
49
  // snapshot remove command
39
50
  snapshotCommand
40
- .command('remove <project-directory> <app> <files...>')
51
+ .command('remove <project> <app> <files...>')
41
52
  .description('Remove files from snapshot tracking')
42
53
  .option('--snapshot-dir <directory>', 'Snapshot directory', '.baseplate-snapshot')
43
- .action(async (projectDirectory, app, files, options) => {
54
+ .action(async (project, app, files, options) => {
44
55
  try {
45
56
  const { removeFilesFromSnapshot } = await import('@baseplate-dev/project-builder-server');
46
- const resolvedDirectory = expandPathWithTilde(projectDirectory);
57
+ let resolvedDirectory;
58
+ if (project.includes('/') || project.includes('\\')) {
59
+ // It's a path, expand it
60
+ resolvedDirectory = expandPathWithTilde(project);
61
+ }
62
+ else {
63
+ // It's a project name, resolve it
64
+ const projectInfo = await resolveProject(project);
65
+ resolvedDirectory = projectInfo.path;
66
+ logger.info(`Running snapshot remove for project: ${projectInfo.name}`);
67
+ }
47
68
  const context = await createSchemaParserContext(resolvedDirectory);
48
69
  await removeFilesFromSnapshot(files, {
49
70
  projectDirectory: resolvedDirectory,
@@ -60,13 +81,23 @@ export function addSnapshotCommand(program) {
60
81
  });
61
82
  // snapshot save command
62
83
  snapshotCommand
63
- .command('save <project-directory> <app>')
84
+ .command('save <project> <app>')
64
85
  .description('Save snapshot of current differences (overwrites existing snapshot)')
65
86
  .option('--snapshot-dir <directory>', 'Snapshot directory', '.baseplate-snapshot')
66
- .action(async (projectDirectory, app, options) => {
87
+ .action(async (project, app, options) => {
67
88
  try {
68
89
  const { createSnapshotForProject } = await import('@baseplate-dev/project-builder-server');
69
- const resolvedDirectory = expandPathWithTilde(projectDirectory);
90
+ let resolvedDirectory;
91
+ if (project.includes('/') || project.includes('\\')) {
92
+ // It's a path, expand it
93
+ resolvedDirectory = expandPathWithTilde(project);
94
+ }
95
+ else {
96
+ // It's a project name, resolve it
97
+ const projectInfo = await resolveProject(project);
98
+ resolvedDirectory = projectInfo.path;
99
+ logger.info(`Running snapshot save for project: ${projectInfo.name}`);
100
+ }
70
101
  const context = await createSchemaParserContext(resolvedDirectory);
71
102
  const userConfig = await getUserConfig();
72
103
  // Confirm with user before overwriting existing snapshot
@@ -97,13 +128,23 @@ export function addSnapshotCommand(program) {
97
128
  });
98
129
  // snapshot show command
99
130
  snapshotCommand
100
- .command('show <project-directory> <app>')
131
+ .command('show <project> <app>')
101
132
  .description('Show current snapshot contents')
102
133
  .option('--snapshot-dir <directory>', 'Snapshot directory', '.baseplate-snapshot')
103
- .action(async (projectDirectory, app, options) => {
134
+ .action(async (project, app, options) => {
104
135
  try {
105
136
  const { listSnapshotContents } = await import('@baseplate-dev/project-builder-server');
106
- const resolvedDirectory = expandPathWithTilde(projectDirectory);
137
+ let resolvedDirectory;
138
+ if (project.includes('/') || project.includes('\\')) {
139
+ // It's a path, expand it
140
+ resolvedDirectory = expandPathWithTilde(project);
141
+ }
142
+ else {
143
+ // It's a project name, resolve it
144
+ const projectInfo = await resolveProject(project);
145
+ resolvedDirectory = projectInfo.path;
146
+ logger.info(`Running snapshot show for project: ${projectInfo.name}`);
147
+ }
107
148
  const context = await createSchemaParserContext(resolvedDirectory);
108
149
  await listSnapshotContents({
109
150
  projectDirectory: resolvedDirectory,
@@ -1,46 +1,26 @@
1
- import { createSchemaParserContext } from '#src/services/schema-parser-context.js';
2
- import { getUserConfig } from '#src/services/user-config.js';
3
- import { expandPathWithTilde } from '#src/utils/path.js';
4
- import { logger } from '../services/logger.js';
1
+ import { invokeServiceActionAsCli, syncProjectAction, } from '@baseplate-dev/project-builder-server/actions';
2
+ import { createServiceActionContext } from '#src/utils/create-service-action-context.js';
5
3
  /**
6
4
  * Adds a sync command to the program.
7
5
  * @param program - The program to add the command to.
8
6
  */
9
7
  export function addSyncCommand(program) {
10
8
  program
11
- .command('sync [directory]')
9
+ .command('sync [project]')
12
10
  .description('Syncs project from project-definition.json in baseplate/ directory')
13
11
  .option('--overwrite', 'Force overwrite existing files and apply snapshots automatically')
14
12
  .option('--snapshot <directory>', 'Apply diffs from snapshot directory (requires --overwrite)')
15
- .action(async (directory, options) => {
16
- const { syncProject, SyncMetadataController } = await import('@baseplate-dev/project-builder-server');
17
- const resolvedDirectory = directory
18
- ? expandPathWithTilde(directory)
19
- : '.';
20
- // Validate that --snapshot requires --overwrite
21
- if (options.snapshot && !options.overwrite) {
22
- logger.error('Error: --snapshot option requires --overwrite flag');
23
- logger.error('Snapshots are only applied when overwriting files, not during normal merging.');
24
- throw new Error('--snapshot option requires --overwrite flag');
25
- }
26
- const context = await createSchemaParserContext(resolvedDirectory);
27
- const userConfig = await getUserConfig();
28
- const syncMetadataController = new SyncMetadataController(resolvedDirectory, logger);
29
- try {
30
- await syncProject({
31
- directory: resolvedDirectory,
32
- logger,
33
- context,
34
- userConfig,
35
- cliFilePath: process.argv[1],
36
- syncMetadataController,
37
- overwrite: options.overwrite ?? false,
38
- snapshotDirectory: options.overwrite ? options.snapshot : undefined,
39
- });
40
- }
41
- catch (error) {
42
- logger.error('Sync failed:', error);
43
- throw error;
13
+ .action(async (project, options) => {
14
+ const context = await createServiceActionContext();
15
+ const projectWithDefault = project ??
16
+ (context.projects.length > 0 ? context.projects[0].name : undefined);
17
+ if (!projectWithDefault) {
18
+ throw new Error('No project specified');
44
19
  }
20
+ await invokeServiceActionAsCli(syncProjectAction, {
21
+ project: projectWithDefault,
22
+ overwrite: options.overwrite,
23
+ snapshotDirectory: options.snapshot,
24
+ }, context);
45
25
  });
46
26
  }
@@ -2,6 +2,7 @@ import { getDefaultPlugins } from '@baseplate-dev/project-builder-common';
2
2
  import path from 'node:path';
3
3
  import { logger } from '#src/services/logger.js';
4
4
  import { expandPathWithTilde } from '#src/utils/path.js';
5
+ import { resolveProject } from '#src/utils/project-resolver.js';
5
6
  /**
6
7
  * Adds template management commands to the program.
7
8
  * @param program - The program to add the commands to.
@@ -29,12 +30,12 @@ export function addTemplatesCommand(program) {
29
30
  });
30
31
  // Templates extract subcommand
31
32
  templatesCommand
32
- .command('extract <directory> <app>')
33
- .description('Extracts templates from the specified directory and saves them to the templates directory')
33
+ .command('extract <project> <app>')
34
+ .description('Extracts templates from the specified project (name or directory) and saves them to the templates directory')
34
35
  .option('--auto-generate-extractor', 'Auto-generate extractor.json files', true)
35
36
  .option('--skip-clean', 'Skip cleaning the output directories (templates and generated)', false)
36
- .action(async (directory, app, options) => {
37
- await handleExtractTemplates(directory, app, options);
37
+ .action(async (project, app, options) => {
38
+ await handleExtractTemplates(project, app, options);
38
39
  });
39
40
  // Templates generate subcommand
40
41
  templatesCommand
@@ -107,14 +108,21 @@ async function handleDeleteTemplate(generatorName, templateName, options) {
107
108
  throw error;
108
109
  }
109
110
  }
110
- async function handleExtractTemplates(directory, app, options) {
111
- const { runTemplateExtractorsForProject } = await import('@baseplate-dev/project-builder-server/template-extractor');
112
- const resolvedDirectory = expandPathWithTilde(directory);
113
- const defaultPlugins = await getDefaultPlugins(logger);
114
- await runTemplateExtractorsForProject(resolvedDirectory, app, defaultPlugins, logger, {
115
- autoGenerateExtractor: options.autoGenerateExtractor,
116
- skipClean: options.skipClean,
117
- });
111
+ async function handleExtractTemplates(project, app, options) {
112
+ try {
113
+ const { runTemplateExtractorsForProject } = await import('@baseplate-dev/project-builder-server/template-extractor');
114
+ const projectInfo = await resolveProject(project);
115
+ const defaultPlugins = await getDefaultPlugins(logger);
116
+ logger.info(`Extracting templates from project: ${projectInfo.name}`);
117
+ await runTemplateExtractorsForProject(projectInfo.path, app, defaultPlugins, logger, {
118
+ autoGenerateExtractor: options.autoGenerateExtractor,
119
+ skipClean: options.skipClean,
120
+ });
121
+ }
122
+ catch (error) {
123
+ logger.error(`Failed to extract templates: ${error instanceof Error ? error.message : String(error)}`);
124
+ throw error;
125
+ }
118
126
  }
119
127
  async function handleGenerateTemplates(directory, options) {
120
128
  const { generateTypedTemplateFiles } = await import('@baseplate-dev/project-builder-server/template-extractor');
package/dist/index.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import { program } from 'commander';
2
2
  import { addConfigCommand } from './commands/config.js';
3
+ import { addDevServerCommand } from './commands/dev-server.js';
3
4
  import { addDiffCommand } from './commands/diff.js';
5
+ import { addMcpCommand } from './commands/mcp.js';
6
+ import { addProjectsCommand } from './commands/projects.js';
4
7
  import { addServeCommand } from './commands/server.js';
5
8
  import { addSnapshotCommand } from './commands/snapshot.js';
6
9
  import { addSyncCommand } from './commands/sync.js';
@@ -22,6 +25,9 @@ export async function runCli() {
22
25
  addSyncCommand(program);
23
26
  addDiffCommand(program);
24
27
  addServeCommand(program);
28
+ addDevServerCommand(program);
29
+ addProjectsCommand(program);
25
30
  addConfigCommand(program);
31
+ addMcpCommand(program);
26
32
  await program.parseAsync(process.argv);
27
33
  }
@@ -1,4 +1,4 @@
1
- import type { BaseplateUserConfig } from '@baseplate-dev/project-builder-server';
1
+ import type { BaseplateUserConfig } from '@baseplate-dev/project-builder-server/user-config';
2
2
  /**
3
3
  * Get the user config for the project builder.
4
4
  *
@@ -1,3 +1,4 @@
1
+ import { userConfigSchema } from '@baseplate-dev/project-builder-server/user-config';
1
2
  import { handleFileNotFoundError, readJsonWithSchema, writeStablePrettyJson, } from '@baseplate-dev/utils/node';
2
3
  import { mkdir } from 'node:fs/promises';
3
4
  import os from 'node:os';
@@ -12,7 +13,6 @@ function getConfigPath() {
12
13
  * @returns The user config for the project builder.
13
14
  */
14
15
  export async function getUserConfig() {
15
- const { userConfigSchema } = await import('@baseplate-dev/project-builder-server');
16
16
  const configPath = getConfigPath();
17
17
  const config = await readJsonWithSchema(configPath, userConfigSchema).catch(handleFileNotFoundError);
18
18
  return config ?? {};
@@ -0,0 +1,14 @@
1
+ import z from 'zod';
2
+ declare const envConfigSchema: z.ZodObject<{
3
+ PROJECT_DIRECTORIES: z.ZodOptional<z.ZodString>;
4
+ INCLUDE_EXAMPLES: z.ZodOptional<z.ZodBoolean>;
5
+ }, "strip", z.ZodTypeAny, {
6
+ PROJECT_DIRECTORIES?: string | undefined;
7
+ INCLUDE_EXAMPLES?: boolean | undefined;
8
+ }, {
9
+ PROJECT_DIRECTORIES?: string | undefined;
10
+ INCLUDE_EXAMPLES?: boolean | undefined;
11
+ }>;
12
+ type EnvConfig = z.infer<typeof envConfigSchema>;
13
+ export declare function getEnvConfig(): EnvConfig;
14
+ export {};
@@ -0,0 +1,8 @@
1
+ import z from 'zod';
2
+ const envConfigSchema = z.object({
3
+ PROJECT_DIRECTORIES: z.string().optional(),
4
+ INCLUDE_EXAMPLES: z.coerce.boolean().optional(),
5
+ });
6
+ export function getEnvConfig() {
7
+ return envConfigSchema.parse(process.env);
8
+ }
@@ -0,0 +1,2 @@
1
+ import type { ServiceActionContext, ServiceActionProject } from '@baseplate-dev/project-builder-server/actions';
2
+ export declare function createServiceActionContext(project?: ServiceActionProject): Promise<ServiceActionContext>;
@@ -0,0 +1,15 @@
1
+ import { getDefaultPlugins } from '@baseplate-dev/project-builder-common';
2
+ import { logger } from '#src/services/logger.js';
3
+ import { getUserConfig } from '#src/services/user-config.js';
4
+ import { listProjects } from './list-projects.js';
5
+ export async function createServiceActionContext(project) {
6
+ const projects = project ? [project] : await listProjects({});
7
+ const plugins = await getDefaultPlugins(logger);
8
+ const userConfig = await getUserConfig();
9
+ return {
10
+ projects,
11
+ logger,
12
+ userConfig,
13
+ plugins,
14
+ };
15
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Find all example directories in the Baseplate repository.
3
+ *
4
+ * This function locates the root of the Baseplate monorepo by finding the current
5
+ * package's package.json, then navigating up to find the root package.json with
6
+ * name "@baseplate-dev/root". It then looks for directories in the examples/ folder.
7
+ *
8
+ * @returns Array of absolute paths to example directories
9
+ * @throws Error if root package.json cannot be found or verified
10
+ */
11
+ export declare function findExamplesDirectories(): Promise<string[]>;
@@ -0,0 +1,49 @@
1
+ import { findNearestPackageJson, handleFileNotFoundError, } from '@baseplate-dev/utils/node';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ /**
6
+ * Find all example directories in the Baseplate repository.
7
+ *
8
+ * This function locates the root of the Baseplate monorepo by finding the current
9
+ * package's package.json, then navigating up to find the root package.json with
10
+ * name "@baseplate-dev/root". It then looks for directories in the examples/ folder.
11
+ *
12
+ * @returns Array of absolute paths to example directories
13
+ * @throws Error if root package.json cannot be found or verified
14
+ */
15
+ export async function findExamplesDirectories() {
16
+ // Find current package.json (project-builder-cli)
17
+ const currentPackageJson = await findNearestPackageJson({
18
+ cwd: fileURLToPath(import.meta.url),
19
+ });
20
+ if (!currentPackageJson) {
21
+ throw new Error(`Could not find current package.json of @baseplate-dev/project-builder-cli in ${fileURLToPath(import.meta.url)}`);
22
+ }
23
+ // Go up one level and find root package.json
24
+ const parentDir = path.dirname(path.dirname(currentPackageJson));
25
+ const rootPackageJson = await findNearestPackageJson({
26
+ cwd: parentDir,
27
+ });
28
+ if (!rootPackageJson) {
29
+ throw new Error(`Could not find root package.json of @baseplate-dev/root in ${parentDir}. Make sure we are running inside the monorepo.`);
30
+ }
31
+ // Verify it's the root package
32
+ const packageContent = await fs.readFile(rootPackageJson, 'utf-8');
33
+ const packageData = JSON.parse(packageContent);
34
+ if (packageData.name !== '@baseplate-dev/root') {
35
+ throw new Error(`Found package.json is not the root package (found: ${packageData.name})`);
36
+ }
37
+ // Look for examples directory
38
+ const rootDir = path.dirname(rootPackageJson);
39
+ const examplesDir = path.join(rootDir, 'examples');
40
+ const entries = await fs
41
+ .readdir(examplesDir, { withFileTypes: true })
42
+ .catch(handleFileNotFoundError);
43
+ if (!entries) {
44
+ throw new Error(`Could not find examples directory in ${examplesDir}. Make sure the examples directory exists.`);
45
+ }
46
+ return entries
47
+ .filter((entry) => entry.isDirectory())
48
+ .map((entry) => path.join(examplesDir, entry.name));
49
+ }
@@ -0,0 +1,58 @@
1
+ import type { ServiceActionProject } from '@baseplate-dev/project-builder-server/actions';
2
+ /**
3
+ * Lists all available projects by searching through configured directories.
4
+ *
5
+ * This function searches for projects in:
6
+ * - Directories specified in PROJECT_DIRECTORIES environment variable
7
+ * - Additional directories provided as parameters
8
+ * - Example directories (if INCLUDE_EXAMPLES is enabled)
9
+ * - Current working directory (as fallback)
10
+ *
11
+ * @param options - Configuration options for project discovery
12
+ * @param options.additionalDirectories - Optional array of additional directory paths to search for projects
13
+ * @returns Promise that resolves to an array of discovered projects
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * // List all projects from default directories
18
+ * const projects = await listProjects({});
19
+ *
20
+ * // List projects including additional custom directories
21
+ * const projects = await listProjects({
22
+ * additionalDirectories: ['/path/to/custom/projects', '~/my-projects']
23
+ * });
24
+ * ```
25
+ */
26
+ export declare function listProjects({ additionalDirectories, }: {
27
+ additionalDirectories?: string[];
28
+ }): Promise<ServiceActionProject[]>;
29
+ /**
30
+ * Resolves a project by name or directory path.
31
+ *
32
+ * This function can resolve projects in two ways:
33
+ * 1. **Path-based resolution**: If the input starts with '.', '/', or '~', it treats the input as a directory path
34
+ * 2. **Name-based resolution**: Otherwise, it searches through all available projects by name or ID
35
+ *
36
+ * @param projectNameOrDirectory - Project name, ID, or directory path to resolve
37
+ * @returns Promise that resolves to the found project
38
+ * @throws {Error} When the project cannot be found
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * // Resolve by project name
43
+ * const project = await resolveProject('my-project');
44
+ *
45
+ * // Resolve by project ID
46
+ * const project = await resolveProject('proj_123');
47
+ *
48
+ * // Resolve by relative path
49
+ * const project = await resolveProject('./my-project');
50
+ *
51
+ * // Resolve by absolute path
52
+ * const project = await resolveProject('/path/to/project');
53
+ *
54
+ * // Resolve by home directory path
55
+ * const project = await resolveProject('~/projects/my-project');
56
+ * ```
57
+ */
58
+ export declare function resolveProject(projectNameOrDirectory?: string | undefined): Promise<ServiceActionProject>;
@@ -0,0 +1,109 @@
1
+ import { discoverProjects, loadProjectFromDirectory, } from '@baseplate-dev/project-builder-server/actions';
2
+ import path from 'node:path';
3
+ import { logger } from '#src/services/logger.js';
4
+ import { getEnvConfig } from './config.js';
5
+ import { findExamplesDirectories } from './find-examples-directories.js';
6
+ import { expandPathWithTilde } from './path.js';
7
+ const config = getEnvConfig();
8
+ async function getSearchDirectories({ additionalDirectories, includeExamples, defaultToCwd, }) {
9
+ const allDirectories = new Set();
10
+ // Add directories from PROJECT_DIRECTORIES env var
11
+ const envDirectories = config.PROJECT_DIRECTORIES?.split(',') ?? [];
12
+ for (const dir of envDirectories) {
13
+ if (dir.trim()) {
14
+ allDirectories.add(expandPathWithTilde(dir.trim()));
15
+ }
16
+ }
17
+ // Add explicitly provided directories
18
+ for (const dir of additionalDirectories ?? []) {
19
+ allDirectories.add(expandPathWithTilde(dir));
20
+ }
21
+ // Add example directories if requested
22
+ if (includeExamples) {
23
+ const exampleDirs = await findExamplesDirectories();
24
+ for (const dir of exampleDirs) {
25
+ allDirectories.add(dir);
26
+ }
27
+ }
28
+ // Default to current working directory if no projects specified
29
+ if (allDirectories.size === 0 && defaultToCwd) {
30
+ allDirectories.add(process.cwd());
31
+ }
32
+ return [...allDirectories];
33
+ }
34
+ /**
35
+ * Lists all available projects by searching through configured directories.
36
+ *
37
+ * This function searches for projects in:
38
+ * - Directories specified in PROJECT_DIRECTORIES environment variable
39
+ * - Additional directories provided as parameters
40
+ * - Example directories (if INCLUDE_EXAMPLES is enabled)
41
+ * - Current working directory (as fallback)
42
+ *
43
+ * @param options - Configuration options for project discovery
44
+ * @param options.additionalDirectories - Optional array of additional directory paths to search for projects
45
+ * @returns Promise that resolves to an array of discovered projects
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * // List all projects from default directories
50
+ * const projects = await listProjects({});
51
+ *
52
+ * // List projects including additional custom directories
53
+ * const projects = await listProjects({
54
+ * additionalDirectories: ['/path/to/custom/projects', '~/my-projects']
55
+ * });
56
+ * ```
57
+ */
58
+ export async function listProjects({ additionalDirectories, }) {
59
+ const searchDirectories = await getSearchDirectories({
60
+ additionalDirectories,
61
+ includeExamples: config.INCLUDE_EXAMPLES ?? false,
62
+ defaultToCwd: true,
63
+ });
64
+ return discoverProjects(searchDirectories, logger);
65
+ }
66
+ /**
67
+ * Resolves a project by name or directory path.
68
+ *
69
+ * This function can resolve projects in two ways:
70
+ * 1. **Path-based resolution**: If the input starts with '.', '/', or '~', it treats the input as a directory path
71
+ * 2. **Name-based resolution**: Otherwise, it searches through all available projects by name or ID
72
+ *
73
+ * @param projectNameOrDirectory - Project name, ID, or directory path to resolve
74
+ * @returns Promise that resolves to the found project
75
+ * @throws {Error} When the project cannot be found
76
+ *
77
+ * @example
78
+ * ```typescript
79
+ * // Resolve by project name
80
+ * const project = await resolveProject('my-project');
81
+ *
82
+ * // Resolve by project ID
83
+ * const project = await resolveProject('proj_123');
84
+ *
85
+ * // Resolve by relative path
86
+ * const project = await resolveProject('./my-project');
87
+ *
88
+ * // Resolve by absolute path
89
+ * const project = await resolveProject('/path/to/project');
90
+ *
91
+ * // Resolve by home directory path
92
+ * const project = await resolveProject('~/projects/my-project');
93
+ * ```
94
+ */
95
+ export async function resolveProject(projectNameOrDirectory = process.cwd()) {
96
+ if (projectNameOrDirectory.startsWith('.') ||
97
+ path.isAbsolute(projectNameOrDirectory) ||
98
+ projectNameOrDirectory.startsWith('~')) {
99
+ const resolvedPath = expandPathWithTilde(projectNameOrDirectory);
100
+ const projectInfo = await loadProjectFromDirectory(resolvedPath);
101
+ return projectInfo;
102
+ }
103
+ const projects = await listProjects({});
104
+ const project = projects.find((p) => p.name === projectNameOrDirectory || p.id === projectNameOrDirectory);
105
+ if (!project) {
106
+ throw new Error(`Project ${projectNameOrDirectory} not found`);
107
+ }
108
+ return project;
109
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Information about a discovered project
3
+ */
4
+ export interface ProjectInfo {
5
+ /** Project name from package.json */
6
+ name: string;
7
+ /** Absolute path to the project directory */
8
+ path: string;
9
+ /** Parsed package.json content */
10
+ packageJson: Record<string, unknown>;
11
+ /** Whether this project is from the examples directory */
12
+ isExample: boolean;
13
+ }
14
+ /**
15
+ * Options for resolving projects
16
+ */
17
+ interface ResolveProjectsOptions {
18
+ /** Whether to include example projects (from INCLUDE_EXAMPLES env) */
19
+ includeExamples?: boolean;
20
+ /** Additional directories to include (from PROJECT_DIRECTORIES env or arguments) */
21
+ directories?: string[];
22
+ /** Whether to default to current working directory if no projects specified */
23
+ defaultToCwd?: boolean;
24
+ }
25
+ /**
26
+ * Resolves all available projects based on the provided options.
27
+ *
28
+ * @param options - Configuration for project resolution
29
+ * @returns Map of project name to ProjectInfo
30
+ * @throws Error if duplicate project names are found or if project loading fails
31
+ */
32
+ export declare function resolveProjects(options?: ResolveProjectsOptions): Promise<Map<string, ProjectInfo>>;
33
+ /**
34
+ * Resolves a single project by name or path.
35
+ *
36
+ * @param nameOrPath - Project name or directory path
37
+ * @returns ProjectInfo for the resolved project
38
+ * @throws Error if project cannot be resolved or loaded
39
+ */
40
+ export declare function resolveProject(nameOrPath: string): Promise<ProjectInfo>;
41
+ /**
42
+ * Gets project directories from resolved projects.
43
+ *
44
+ * @param projectMap - Map of resolved projects
45
+ * @returns Array of absolute directory paths
46
+ */
47
+ export declare function getProjectDirectories(projectMap: Map<string, ProjectInfo>): string[];
48
+ /**
49
+ * Gets project names from resolved projects.
50
+ *
51
+ * @param projectMap - Map of resolved projects
52
+ * @returns Array of project names
53
+ */
54
+ export declare function getProjectNames(projectMap: Map<string, ProjectInfo>): string[];
55
+ export {};
@@ -0,0 +1,137 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { findExamplesDirectories } from './find-examples-directories.js';
4
+ import { expandPathWithTilde } from './path.js';
5
+ /**
6
+ * Resolves all available projects based on the provided options.
7
+ *
8
+ * @param options - Configuration for project resolution
9
+ * @returns Map of project name to ProjectInfo
10
+ * @throws Error if duplicate project names are found or if project loading fails
11
+ */
12
+ export async function resolveProjects(options = {}) {
13
+ const { includeExamples = process.env.INCLUDE_EXAMPLES === 'true', directories = [], defaultToCwd = false, } = options;
14
+ const allDirectories = new Set();
15
+ // Add directories from PROJECT_DIRECTORIES env var
16
+ const envDirectories = process.env.PROJECT_DIRECTORIES?.split(',') ?? [];
17
+ for (const dir of envDirectories) {
18
+ if (dir.trim()) {
19
+ allDirectories.add(expandPathWithTilde(dir.trim()));
20
+ }
21
+ }
22
+ // Add explicitly provided directories
23
+ for (const dir of directories) {
24
+ allDirectories.add(expandPathWithTilde(dir));
25
+ }
26
+ // Add example directories if requested
27
+ if (includeExamples) {
28
+ const exampleDirs = await findExamplesDirectories();
29
+ for (const dir of exampleDirs) {
30
+ allDirectories.add(dir);
31
+ }
32
+ }
33
+ // Default to current working directory if no projects specified
34
+ if (allDirectories.size === 0 && defaultToCwd) {
35
+ allDirectories.add(process.cwd());
36
+ }
37
+ const projectMap = new Map();
38
+ const nameConflicts = [];
39
+ // Load project info for each directory
40
+ for (const directory of allDirectories) {
41
+ try {
42
+ const projectInfo = await loadProjectInfo(directory);
43
+ // Check for name conflicts
44
+ if (projectMap.has(projectInfo.name)) {
45
+ nameConflicts.push(projectInfo.name);
46
+ }
47
+ else {
48
+ projectMap.set(projectInfo.name, projectInfo);
49
+ }
50
+ }
51
+ catch (error) {
52
+ console.warn(`Warning: Could not load project from ${directory}: ${error instanceof Error ? error.message : String(error)}`);
53
+ }
54
+ }
55
+ // Handle name conflicts
56
+ if (nameConflicts.length > 0) {
57
+ throw new Error(`Duplicate project names found: ${nameConflicts.join(', ')}. ` +
58
+ 'Each project must have a unique name in its package.json.');
59
+ }
60
+ return projectMap;
61
+ }
62
+ /**
63
+ * Resolves a single project by name or path.
64
+ *
65
+ * @param nameOrPath - Project name or directory path
66
+ * @returns ProjectInfo for the resolved project
67
+ * @throws Error if project cannot be resolved or loaded
68
+ */
69
+ export async function resolveProject(nameOrPath) {
70
+ // If it looks like a path (contains separators), treat as path
71
+ if (nameOrPath.includes('/') || nameOrPath.includes('\\')) {
72
+ const resolvedPath = expandPathWithTilde(nameOrPath);
73
+ return await loadProjectInfo(resolvedPath);
74
+ }
75
+ // Otherwise, try to resolve by name
76
+ const projectMap = await resolveProjects({
77
+ includeExamples: process.env.INCLUDE_EXAMPLES === 'true',
78
+ defaultToCwd: false,
79
+ });
80
+ const projectInfo = projectMap.get(nameOrPath);
81
+ if (!projectInfo) {
82
+ const availableProjects = [...projectMap.keys()].sort();
83
+ throw new Error(`Project '${nameOrPath}' not found. Available projects: ${availableProjects.join(', ')}\n` +
84
+ 'You can also specify a directory path directly.');
85
+ }
86
+ return projectInfo;
87
+ }
88
+ /**
89
+ * Loads project information from a directory.
90
+ *
91
+ * @param directory - Absolute path to the project directory
92
+ * @returns ProjectInfo for the project
93
+ * @throws Error if directory doesn't exist or package.json is invalid
94
+ */
95
+ async function loadProjectInfo(directory) {
96
+ const packageJsonPath = path.join(directory, 'package.json');
97
+ try {
98
+ const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8');
99
+ const packageJson = JSON.parse(packageJsonContent);
100
+ const name = packageJson.name;
101
+ if (!name || typeof name !== 'string') {
102
+ throw new Error('package.json must have a valid "name" field');
103
+ }
104
+ // Determine if this is an example project by checking if it's in examples directory
105
+ const isExample = directory.includes('/examples/') || directory.includes('\\examples\\');
106
+ return {
107
+ name,
108
+ path: directory,
109
+ packageJson,
110
+ isExample,
111
+ };
112
+ }
113
+ catch (error) {
114
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
115
+ throw new Error(`No package.json found in ${directory}`);
116
+ }
117
+ throw error;
118
+ }
119
+ }
120
+ /**
121
+ * Gets project directories from resolved projects.
122
+ *
123
+ * @param projectMap - Map of resolved projects
124
+ * @returns Array of absolute directory paths
125
+ */
126
+ export function getProjectDirectories(projectMap) {
127
+ return [...projectMap.values()].map((project) => project.path);
128
+ }
129
+ /**
130
+ * Gets project names from resolved projects.
131
+ *
132
+ * @param projectMap - Map of resolved projects
133
+ * @returns Array of project names
134
+ */
135
+ export function getProjectNames(projectMap) {
136
+ return [...projectMap.keys()].sort();
137
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@baseplate-dev/project-builder-cli",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "Full-stack CLI builder using Baseplate generators",
5
5
  "keywords": [
6
6
  "cli",
@@ -40,28 +40,29 @@
40
40
  "bin/**/*"
41
41
  ],
42
42
  "dependencies": {
43
- "@inquirer/prompts": "7.2.1",
43
+ "@inquirer/prompts": "7.8.3",
44
44
  "commander": "^12.1.0",
45
45
  "pino": "9.5.0",
46
46
  "pino-pretty": "13.0.0",
47
47
  "pkg-dir": "^8.0.0",
48
- "zod": "3.24.1",
49
- "@baseplate-dev/project-builder-common": "0.3.3",
50
- "@baseplate-dev/project-builder-lib": "0.3.3",
51
- "@baseplate-dev/project-builder-server": "0.3.3",
52
- "@baseplate-dev/project-builder-web": "0.3.3",
53
- "@baseplate-dev/utils": "0.3.3"
48
+ "zod": "3.25.76",
49
+ "@baseplate-dev/project-builder-common": "0.3.4",
50
+ "@baseplate-dev/project-builder-lib": "0.3.4",
51
+ "@baseplate-dev/project-builder-server": "0.3.4",
52
+ "@baseplate-dev/project-builder-web": "0.3.4",
53
+ "@baseplate-dev/utils": "0.3.4"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@playwright/test": "1.51.0",
57
- "@types/node": "^22.0.0",
57
+ "@types/node": "^22.17.2",
58
58
  "eslint": "9.32.0",
59
59
  "fastify": "5.3.2",
60
+ "memfs": "4.15.1",
60
61
  "prettier": "3.6.2",
61
62
  "tsx": "4.19.3",
62
- "typescript": "5.7.3",
63
+ "typescript": "5.8.3",
63
64
  "vitest": "3.2.4",
64
- "@baseplate-dev/tools": "0.3.3"
65
+ "@baseplate-dev/tools": "0.3.4"
65
66
  },
66
67
  "engines": {
67
68
  "node": "^22.0.0"
@@ -76,13 +77,14 @@
76
77
  "scripts": {
77
78
  "build": "tsc -p tsconfig.build.json",
78
79
  "clean": "rm -rf ./dist",
79
- "dev": "tsx watch --tsconfig ./tsconfig.app.json --exclude /**/node_modules/** --env-file-if-exists=.env -C development ./src/cli.ts",
80
+ "dev": "tsx watch --tsconfig ./tsconfig.app.json --exclude /**/node_modules/** --env-file-if-exists=../../.env --env-file-if-exists=.env -C development ./src/cli.ts",
81
+ "dev:dev-server": "pnpm dev dev-server",
80
82
  "dev:serve": "pnpm dev serve",
81
83
  "lint": "eslint .",
82
84
  "prettier:check": "prettier --check .",
83
85
  "prettier:write": "prettier -w .",
84
86
  "project:generate": "pnpm start generate",
85
- "start": "tsx --tsconfig ./tsconfig.app.json --env-file-if-exists=.env -C development ./src/cli.ts",
87
+ "start": "tsx --tsconfig ./tsconfig.app.json --env-file-if-exists=../../.env --env-file-if-exists=.env -C development ./src/cli.ts",
86
88
  "templates:extract": "pnpm start templates extract",
87
89
  "templates:generate": "pnpm start templates generate",
88
90
  "test": "vitest",