@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.
- package/dist/commands/dev-server.d.ts +2 -0
- package/dist/commands/dev-server.js +24 -0
- package/dist/commands/diff.js +14 -26
- package/dist/commands/mcp.d.ts +2 -0
- package/dist/commands/mcp.js +11 -0
- package/dist/commands/projects.d.ts +6 -0
- package/dist/commands/projects.js +91 -0
- package/dist/commands/server.js +29 -10
- package/dist/commands/snapshot.js +53 -12
- package/dist/commands/sync.js +14 -34
- package/dist/commands/templates.js +20 -12
- package/dist/index.js +6 -0
- package/dist/services/user-config.d.ts +1 -1
- package/dist/services/user-config.js +1 -1
- package/dist/utils/config.d.ts +14 -0
- package/dist/utils/config.js +8 -0
- package/dist/utils/create-service-action-context.d.ts +2 -0
- package/dist/utils/create-service-action-context.js +15 -0
- package/dist/utils/find-examples-directories.d.ts +11 -0
- package/dist/utils/find-examples-directories.js +49 -0
- package/dist/utils/list-projects.d.ts +58 -0
- package/dist/utils/list-projects.js +109 -0
- package/dist/utils/project-resolver.d.ts +55 -0
- package/dist/utils/project-resolver.js +137 -0
- package/package.json +15 -13
|
@@ -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
|
+
}
|
package/dist/commands/diff.js
CHANGED
|
@@ -1,35 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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 [
|
|
12
|
-
.description('Show diff between generated output and
|
|
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('--
|
|
15
|
-
.option('--
|
|
16
|
-
.
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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,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,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
|
+
}
|
package/dist/commands/server.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
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:
|
|
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('[
|
|
53
|
-
.action(async (
|
|
54
|
-
|
|
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
|
|
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 (
|
|
21
|
+
.action(async (project, app, files, options) => {
|
|
21
22
|
try {
|
|
22
23
|
const { addFilesToSnapshot } = await import('@baseplate-dev/project-builder-server');
|
|
23
|
-
|
|
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
|
|
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 (
|
|
54
|
+
.action(async (project, app, files, options) => {
|
|
44
55
|
try {
|
|
45
56
|
const { removeFilesFromSnapshot } = await import('@baseplate-dev/project-builder-server');
|
|
46
|
-
|
|
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
|
|
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 (
|
|
87
|
+
.action(async (project, app, options) => {
|
|
67
88
|
try {
|
|
68
89
|
const { createSnapshotForProject } = await import('@baseplate-dev/project-builder-server');
|
|
69
|
-
|
|
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
|
|
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 (
|
|
134
|
+
.action(async (project, app, options) => {
|
|
104
135
|
try {
|
|
105
136
|
const { listSnapshotContents } = await import('@baseplate-dev/project-builder-server');
|
|
106
|
-
|
|
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,
|
package/dist/commands/sync.js
CHANGED
|
@@ -1,46 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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 [
|
|
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 (
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
?
|
|
19
|
-
|
|
20
|
-
|
|
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 <
|
|
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 (
|
|
37
|
-
await handleExtractTemplates(
|
|
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(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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,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,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
|
+
"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.
|
|
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.
|
|
49
|
-
"@baseplate-dev/project-builder-common": "0.3.
|
|
50
|
-
"@baseplate-dev/project-builder-lib": "0.3.
|
|
51
|
-
"@baseplate-dev/project-builder-server": "0.3.
|
|
52
|
-
"@baseplate-dev/project-builder-web": "0.3.
|
|
53
|
-
"@baseplate-dev/utils": "0.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.
|
|
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.
|
|
63
|
+
"typescript": "5.8.3",
|
|
63
64
|
"vitest": "3.2.4",
|
|
64
|
-
"@baseplate-dev/tools": "0.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",
|