@launchframe/cli 1.0.0-beta.9 ā 1.0.0
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/.claude/settings.local.json +12 -0
- package/CLAUDE.md +27 -0
- package/CODE_OF_CONDUCT.md +128 -0
- package/LICENSE +21 -0
- package/README.md +7 -1
- package/package.json +9 -6
- package/src/commands/database-console.js +84 -0
- package/src/commands/deploy-build.js +76 -0
- package/src/commands/deploy-configure.js +10 -3
- package/src/commands/deploy-init.js +24 -57
- package/src/commands/deploy-set-env.js +17 -7
- package/src/commands/deploy-sync-features.js +233 -0
- package/src/commands/deploy-up.js +4 -3
- package/src/commands/dev-add-user.js +165 -0
- package/src/commands/dev-logo.js +160 -0
- package/src/commands/dev-npm-install.js +33 -0
- package/src/commands/dev-queue.js +85 -0
- package/src/commands/docker-build.js +9 -6
- package/src/commands/help.js +33 -9
- package/src/commands/init.js +44 -52
- package/src/commands/migration-create.js +40 -0
- package/src/commands/migration-revert.js +32 -0
- package/src/commands/migration-run.js +32 -0
- package/src/commands/module.js +146 -0
- package/src/commands/waitlist-deploy.js +1 -0
- package/src/generator.js +41 -40
- package/src/index.js +109 -4
- package/src/services/module-config.js +25 -0
- package/src/services/module-registry.js +12 -0
- package/src/services/variant-config.js +24 -13
- package/src/utils/docker-helper.js +116 -2
- package/src/utils/env-generator.js +9 -6
- package/src/utils/env-validator.js +4 -2
- package/src/utils/github-access.js +15 -13
- package/src/utils/logger.js +93 -0
- package/src/utils/module-installer.js +58 -0
- package/src/utils/project-helpers.js +34 -1
- package/src/utils/service-cache.js +12 -18
- package/src/utils/ssh-helper.js +51 -1
- package/src/utils/telemetry.js +238 -0
- package/src/utils/variable-replacer.js +18 -23
- package/src/utils/variant-processor.js +35 -42
package/src/generator.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const fs = require('fs-extra');
|
|
3
3
|
const { execSync } = require('child_process');
|
|
4
|
+
const chalk = require('chalk');
|
|
4
5
|
const { replaceVariables } = require('./utils/variable-replacer');
|
|
5
6
|
const { copyDirectory } = require('./utils/file-ops');
|
|
6
7
|
const { generateEnvFile } = require('./utils/env-generator');
|
|
7
8
|
const { processServiceVariant } = require('./utils/variant-processor');
|
|
8
9
|
const { resolveVariantChoices } = require('./services/variant-config');
|
|
10
|
+
const logger = require('./utils/logger');
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Initialize git repository in a service directory
|
|
@@ -14,13 +16,13 @@ const { resolveVariantChoices } = require('./services/variant-config');
|
|
|
14
16
|
*/
|
|
15
17
|
function initGitRepo(servicePath, serviceName) {
|
|
16
18
|
try {
|
|
17
|
-
|
|
19
|
+
logger.detail(`Initializing git repository for ${serviceName}`);
|
|
18
20
|
execSync('git init', { cwd: servicePath, stdio: 'ignore' });
|
|
19
21
|
execSync('git add .', { cwd: servicePath, stdio: 'ignore' });
|
|
20
22
|
execSync('git commit -m "Initial commit"', { cwd: servicePath, stdio: 'ignore' });
|
|
21
|
-
|
|
23
|
+
logger.detail(`Git initialized: ${serviceName}`);
|
|
22
24
|
} catch (error) {
|
|
23
|
-
|
|
25
|
+
logger.warn(`Could not initialize git for ${serviceName}: ${error.message}`);
|
|
24
26
|
}
|
|
25
27
|
}
|
|
26
28
|
|
|
@@ -34,12 +36,11 @@ async function generateProject(answers, variantChoices, templateRoot) {
|
|
|
34
36
|
const { projectName } = answers;
|
|
35
37
|
|
|
36
38
|
// Define source (template) and destination paths
|
|
37
|
-
|
|
38
|
-
const projectRoot = path.resolve(__dirname, '../..'); // For root-level files like .github, README.md
|
|
39
|
+
const projectRoot = path.resolve(__dirname, '../..'); // For root-level files
|
|
39
40
|
const destinationRoot = path.resolve(process.cwd(), projectName);
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
logger.detail(`Template source: ${templateRoot}`);
|
|
43
|
+
logger.detail(`Destination: ${destinationRoot}`);
|
|
43
44
|
|
|
44
45
|
// Ensure destination directory exists
|
|
45
46
|
await fs.ensureDir(destinationRoot);
|
|
@@ -60,8 +61,8 @@ async function generateProject(answers, variantChoices, templateRoot) {
|
|
|
60
61
|
// Resolve variant choices for all services
|
|
61
62
|
const allServiceVariants = resolveVariantChoices(variantChoices);
|
|
62
63
|
|
|
63
|
-
//
|
|
64
|
-
console.log('
|
|
64
|
+
// Process backend
|
|
65
|
+
console.log(chalk.gray(' Processing backend...'));
|
|
65
66
|
await processServiceVariant(
|
|
66
67
|
'backend',
|
|
67
68
|
allServiceVariants.backend,
|
|
@@ -71,11 +72,10 @@ async function generateProject(answers, variantChoices, templateRoot) {
|
|
|
71
72
|
);
|
|
72
73
|
initGitRepo(path.join(destinationRoot, 'backend'), 'backend');
|
|
73
74
|
|
|
74
|
-
//
|
|
75
|
-
// Note: admin-portal folder might not exist yet in templates, skip if missing
|
|
75
|
+
// Process admin-portal
|
|
76
76
|
const adminPortalTemplatePath = path.join(templateRoot, 'admin-portal/base');
|
|
77
77
|
if (await fs.pathExists(adminPortalTemplatePath)) {
|
|
78
|
-
console.log('
|
|
78
|
+
console.log(chalk.gray(' Processing admin-portal...'));
|
|
79
79
|
await processServiceVariant(
|
|
80
80
|
'admin-portal',
|
|
81
81
|
allServiceVariants['admin-portal'],
|
|
@@ -85,8 +85,8 @@ async function generateProject(answers, variantChoices, templateRoot) {
|
|
|
85
85
|
);
|
|
86
86
|
initGitRepo(path.join(destinationRoot, 'admin-portal'), 'admin-portal');
|
|
87
87
|
} else {
|
|
88
|
-
// Fallback: Copy admin-portal directly without variants
|
|
89
|
-
console.log('
|
|
88
|
+
// Fallback: Copy admin-portal directly without variants
|
|
89
|
+
console.log(chalk.gray(' Copying admin-portal...'));
|
|
90
90
|
const adminPortalSource = path.join(templateRoot, 'admin-portal');
|
|
91
91
|
if (await fs.pathExists(adminPortalSource)) {
|
|
92
92
|
await copyDirectory(adminPortalSource, path.join(destinationRoot, 'admin-portal'));
|
|
@@ -95,12 +95,11 @@ async function generateProject(answers, variantChoices, templateRoot) {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
//
|
|
98
|
+
// Process customers-portal (only if B2B2C)
|
|
99
99
|
if (variantChoices.userModel === 'b2b2c') {
|
|
100
|
-
// Note: customers-portal folder might not exist yet in templates, skip if missing
|
|
101
100
|
const customersPortalTemplatePath = path.join(templateRoot, 'customers-portal/base');
|
|
102
101
|
if (await fs.pathExists(customersPortalTemplatePath)) {
|
|
103
|
-
console.log('
|
|
102
|
+
console.log(chalk.gray(' Processing customers-portal...'));
|
|
104
103
|
await processServiceVariant(
|
|
105
104
|
'customers-portal',
|
|
106
105
|
allServiceVariants['customers-portal'],
|
|
@@ -110,8 +109,7 @@ async function generateProject(answers, variantChoices, templateRoot) {
|
|
|
110
109
|
);
|
|
111
110
|
initGitRepo(path.join(destinationRoot, 'customers-portal'), 'customers-portal');
|
|
112
111
|
} else {
|
|
113
|
-
|
|
114
|
-
console.log('š Copying customers-portal service (B2B2C mode)...');
|
|
112
|
+
console.log(chalk.gray(' Copying customers-portal...'));
|
|
115
113
|
const customersPortalSource = path.join(templateRoot, 'customers-portal');
|
|
116
114
|
if (await fs.pathExists(customersPortalSource)) {
|
|
117
115
|
await copyDirectory(customersPortalSource, path.join(destinationRoot, 'customers-portal'));
|
|
@@ -120,11 +118,11 @@ async function generateProject(answers, variantChoices, templateRoot) {
|
|
|
120
118
|
}
|
|
121
119
|
}
|
|
122
120
|
} else {
|
|
123
|
-
|
|
121
|
+
logger.detail('Skipping customers-portal (B2B mode)');
|
|
124
122
|
}
|
|
125
123
|
|
|
126
|
-
//
|
|
127
|
-
console.log('
|
|
124
|
+
// Process infrastructure
|
|
125
|
+
console.log(chalk.gray(' Processing infrastructure...'));
|
|
128
126
|
await processServiceVariant(
|
|
129
127
|
'infrastructure',
|
|
130
128
|
allServiceVariants.infrastructure,
|
|
@@ -134,7 +132,8 @@ async function generateProject(answers, variantChoices, templateRoot) {
|
|
|
134
132
|
);
|
|
135
133
|
initGitRepo(path.join(destinationRoot, 'infrastructure'), 'infrastructure');
|
|
136
134
|
|
|
137
|
-
|
|
135
|
+
// Process website
|
|
136
|
+
console.log(chalk.gray(' Processing website...'));
|
|
138
137
|
await copyDirectory(
|
|
139
138
|
path.join(templateRoot, 'website'),
|
|
140
139
|
path.join(destinationRoot, 'website')
|
|
@@ -142,14 +141,9 @@ async function generateProject(answers, variantChoices, templateRoot) {
|
|
|
142
141
|
await replaceVariables(path.join(destinationRoot, 'website'), variables);
|
|
143
142
|
initGitRepo(path.join(destinationRoot, 'website'), 'website');
|
|
144
143
|
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
const additionalFiles = [
|
|
148
|
-
'.github',
|
|
149
|
-
'README.md',
|
|
150
|
-
'.gitignore',
|
|
151
|
-
'LICENSE'
|
|
152
|
-
];
|
|
144
|
+
// Copy additional files
|
|
145
|
+
logger.detail('Copying additional files...');
|
|
146
|
+
const additionalFiles = ['.github', 'README.md', '.gitignore', 'LICENSE'];
|
|
153
147
|
|
|
154
148
|
for (const file of additionalFiles) {
|
|
155
149
|
const sourcePath = path.join(projectRoot, file);
|
|
@@ -166,16 +160,26 @@ async function generateProject(answers, variantChoices, templateRoot) {
|
|
|
166
160
|
}
|
|
167
161
|
}
|
|
168
162
|
|
|
169
|
-
//
|
|
170
|
-
|
|
163
|
+
// Copy MCP configuration files from template root
|
|
164
|
+
logger.detail('Copying MCP configuration files...');
|
|
165
|
+
const mcpFiles = ['CLAUDE.md', '.mcp.json'];
|
|
166
|
+
for (const file of mcpFiles) {
|
|
167
|
+
const sourcePath = path.join(templateRoot, file);
|
|
168
|
+
const destPath = path.join(destinationRoot, file);
|
|
169
|
+
if (await fs.pathExists(sourcePath)) {
|
|
170
|
+
await fs.copy(sourcePath, destPath);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Generate .env file
|
|
175
|
+
console.log(chalk.gray(' Generating environment file...'));
|
|
171
176
|
const { envPath } = await generateEnvFile(destinationRoot, answers);
|
|
172
|
-
|
|
177
|
+
logger.detail(`Environment file: ${envPath}`);
|
|
173
178
|
|
|
174
|
-
//
|
|
175
|
-
|
|
179
|
+
// Create .launchframe marker file
|
|
180
|
+
logger.detail('Creating project marker file...');
|
|
176
181
|
const markerPath = path.join(destinationRoot, '.launchframe');
|
|
177
182
|
|
|
178
|
-
// Determine which services were installed
|
|
179
183
|
const installedServices = ['backend', 'admin-portal', 'infrastructure', 'website'];
|
|
180
184
|
if (variantChoices.userModel === 'b2b2c') {
|
|
181
185
|
installedServices.push('customers-portal');
|
|
@@ -188,12 +192,9 @@ async function generateProject(answers, variantChoices, templateRoot) {
|
|
|
188
192
|
projectDisplayName: answers.projectDisplayName,
|
|
189
193
|
deployConfigured: false,
|
|
190
194
|
installedServices: installedServices,
|
|
191
|
-
// Store variant choices for future reference
|
|
192
195
|
variants: variantChoices
|
|
193
196
|
};
|
|
194
197
|
await fs.writeJson(markerPath, markerContent, { spaces: 2 });
|
|
195
|
-
|
|
196
|
-
console.log('ā
Base project generated with variants applied');
|
|
197
198
|
}
|
|
198
199
|
|
|
199
200
|
module.exports = { generateProject };
|
package/src/index.js
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
const chalk = require('chalk');
|
|
4
4
|
const { isLaunchFrameProject } = require('./utils/project-helpers');
|
|
5
|
+
const logger = require('./utils/logger');
|
|
6
|
+
const { initTelemetry, trackEvent, sanitize, setTelemetryEnabled, showTelemetryStatus } = require('./utils/telemetry');
|
|
7
|
+
|
|
8
|
+
// Detect locally linked version: npm link installs to global node_modules
|
|
9
|
+
// as a symlink. When running from a real install, __dirname is inside the
|
|
10
|
+
// global node_modules folder. When linked, it resolves to the source directory.
|
|
11
|
+
const isDevMode = !__dirname.includes('node_modules');
|
|
12
|
+
if (isDevMode) {
|
|
13
|
+
const packageJson = require('../package.json');
|
|
14
|
+
console.log(chalk.yellow(`ā Running locally linked CLI v${packageJson.version} (${__dirname})`));
|
|
15
|
+
}
|
|
5
16
|
|
|
6
17
|
// Import commands
|
|
7
18
|
const { init } = require('./commands/init');
|
|
@@ -9,6 +20,7 @@ const { deployConfigure } = require('./commands/deploy-configure');
|
|
|
9
20
|
const { deploySetEnv } = require('./commands/deploy-set-env');
|
|
10
21
|
const { deployInit } = require('./commands/deploy-init');
|
|
11
22
|
const { deployUp } = require('./commands/deploy-up');
|
|
23
|
+
const { deployBuild } = require('./commands/deploy-build');
|
|
12
24
|
const { waitlistDeploy } = require('./commands/waitlist-deploy');
|
|
13
25
|
const { waitlistUp } = require('./commands/waitlist-up');
|
|
14
26
|
const { waitlistDown } = require('./commands/waitlist-down');
|
|
@@ -17,6 +29,10 @@ const { dockerBuild } = require('./commands/docker-build');
|
|
|
17
29
|
const { dockerUp } = require('./commands/docker-up');
|
|
18
30
|
const { dockerDown } = require('./commands/docker-down');
|
|
19
31
|
const { dockerLogs } = require('./commands/docker-logs');
|
|
32
|
+
const { migrateRun } = require('./commands/migration-run');
|
|
33
|
+
const { migrateCreate } = require('./commands/migration-create');
|
|
34
|
+
const { migrateRevert } = require('./commands/migration-revert');
|
|
35
|
+
const { databaseConsole } = require('./commands/database-console');
|
|
20
36
|
const { dockerDestroy } = require('./commands/docker-destroy');
|
|
21
37
|
const { doctor } = require('./commands/doctor');
|
|
22
38
|
const { help } = require('./commands/help');
|
|
@@ -25,7 +41,13 @@ const {
|
|
|
25
41
|
serviceList,
|
|
26
42
|
serviceRemove
|
|
27
43
|
} = require('./commands/service');
|
|
44
|
+
const { moduleAdd, moduleList } = require('./commands/module');
|
|
28
45
|
const { cacheClear, cacheInfo, cacheUpdate } = require('./commands/cache');
|
|
46
|
+
const { devAddUser } = require('./commands/dev-add-user');
|
|
47
|
+
const { devQueue } = require('./commands/dev-queue');
|
|
48
|
+
const { devLogo } = require('./commands/dev-logo');
|
|
49
|
+
const { devNpmInstall } = require('./commands/dev-npm-install');
|
|
50
|
+
const { deploySyncFeatures } = require('./commands/deploy-sync-features');
|
|
29
51
|
|
|
30
52
|
// Get command and arguments
|
|
31
53
|
const command = process.argv[2];
|
|
@@ -62,9 +84,23 @@ function parseFlags(args) {
|
|
|
62
84
|
* Main CLI router
|
|
63
85
|
*/
|
|
64
86
|
async function main() {
|
|
87
|
+
initTelemetry();
|
|
88
|
+
|
|
65
89
|
const inProject = isLaunchFrameProject();
|
|
66
90
|
const flags = parseFlags(args);
|
|
67
91
|
|
|
92
|
+
// Handle version flag (only as standalone command)
|
|
93
|
+
if (command === '--version') {
|
|
94
|
+
const packageJson = require('../package.json');
|
|
95
|
+
console.log(packageJson.version);
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Set verbose mode globally
|
|
100
|
+
if (flags.verbose || flags.v) {
|
|
101
|
+
logger.setVerbose(true);
|
|
102
|
+
}
|
|
103
|
+
|
|
68
104
|
// No command provided
|
|
69
105
|
if (!command) {
|
|
70
106
|
help();
|
|
@@ -74,7 +110,7 @@ async function main() {
|
|
|
74
110
|
// Route commands
|
|
75
111
|
switch (command) {
|
|
76
112
|
case 'init':
|
|
77
|
-
await init({
|
|
113
|
+
await init({
|
|
78
114
|
projectName: flags['project-name'],
|
|
79
115
|
tenancy: flags['tenancy'],
|
|
80
116
|
userModel: flags['user-model']
|
|
@@ -92,6 +128,12 @@ async function main() {
|
|
|
92
128
|
case 'deploy:up':
|
|
93
129
|
await deployUp();
|
|
94
130
|
break;
|
|
131
|
+
case 'deploy:build':
|
|
132
|
+
await deployBuild(args[1]); // Optional service name
|
|
133
|
+
break;
|
|
134
|
+
case 'deploy:sync-features':
|
|
135
|
+
await deploySyncFeatures();
|
|
136
|
+
break;
|
|
95
137
|
case 'waitlist:deploy':
|
|
96
138
|
await waitlistDeploy();
|
|
97
139
|
break;
|
|
@@ -105,7 +147,7 @@ async function main() {
|
|
|
105
147
|
await waitlistLogs();
|
|
106
148
|
break;
|
|
107
149
|
case 'docker:build':
|
|
108
|
-
await dockerBuild();
|
|
150
|
+
await dockerBuild(args[1]); // Optional service name
|
|
109
151
|
break;
|
|
110
152
|
case 'docker:up':
|
|
111
153
|
await dockerUp(args[1]); // Pass optional service name
|
|
@@ -119,6 +161,18 @@ async function main() {
|
|
|
119
161
|
case 'docker:destroy':
|
|
120
162
|
await dockerDestroy({ force: flags.force || flags.f });
|
|
121
163
|
break;
|
|
164
|
+
case 'migration:run':
|
|
165
|
+
await migrateRun();
|
|
166
|
+
break;
|
|
167
|
+
case 'migration:create':
|
|
168
|
+
await migrateCreate();
|
|
169
|
+
break;
|
|
170
|
+
case 'migration:revert':
|
|
171
|
+
await migrateRevert();
|
|
172
|
+
break;
|
|
173
|
+
case 'database:console':
|
|
174
|
+
await databaseConsole({ remote: flags.remote });
|
|
175
|
+
break;
|
|
122
176
|
case 'doctor':
|
|
123
177
|
await doctor();
|
|
124
178
|
break;
|
|
@@ -141,6 +195,17 @@ async function main() {
|
|
|
141
195
|
}
|
|
142
196
|
await serviceRemove(args[1]);
|
|
143
197
|
break;
|
|
198
|
+
case 'module:add':
|
|
199
|
+
if (!args[1]) {
|
|
200
|
+
console.error(chalk.red('Error: Module name required'));
|
|
201
|
+
console.log('Usage: launchframe module:add <module-name>');
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
await moduleAdd(args[1]);
|
|
205
|
+
break;
|
|
206
|
+
case 'module:list':
|
|
207
|
+
await moduleList();
|
|
208
|
+
break;
|
|
144
209
|
case 'cache:clear':
|
|
145
210
|
await cacheClear();
|
|
146
211
|
break;
|
|
@@ -150,16 +215,56 @@ async function main() {
|
|
|
150
215
|
case 'cache:update':
|
|
151
216
|
await cacheUpdate();
|
|
152
217
|
break;
|
|
218
|
+
case 'dev:add-user':
|
|
219
|
+
await devAddUser();
|
|
220
|
+
break;
|
|
221
|
+
case 'dev:queue':
|
|
222
|
+
await devQueue();
|
|
223
|
+
break;
|
|
224
|
+
case 'dev:logo':
|
|
225
|
+
await devLogo();
|
|
226
|
+
break;
|
|
227
|
+
case 'dev:npm-install':
|
|
228
|
+
if (!args[1]) {
|
|
229
|
+
console.error(chalk.red('Error: Service name required'));
|
|
230
|
+
console.log('Usage: launchframe dev:npm-install <service> [packages...]');
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
await devNpmInstall(args[1], args.slice(2));
|
|
234
|
+
break;
|
|
235
|
+
case 'telemetry':
|
|
236
|
+
if (flags.disable) {
|
|
237
|
+
setTelemetryEnabled(false);
|
|
238
|
+
} else if (flags.enable) {
|
|
239
|
+
setTelemetryEnabled(true);
|
|
240
|
+
} else {
|
|
241
|
+
showTelemetryStatus();
|
|
242
|
+
}
|
|
243
|
+
break;
|
|
153
244
|
case 'help':
|
|
154
245
|
case '--help':
|
|
155
246
|
case '-h':
|
|
156
247
|
help();
|
|
157
248
|
break;
|
|
158
249
|
default:
|
|
159
|
-
console.error(chalk.red(`\
|
|
250
|
+
console.error(chalk.red(`\nUnknown command: ${command}\n`));
|
|
160
251
|
help();
|
|
161
252
|
process.exit(1);
|
|
162
253
|
}
|
|
163
254
|
}
|
|
164
255
|
|
|
165
|
-
main()
|
|
256
|
+
main()
|
|
257
|
+
.then(() => {
|
|
258
|
+
if (command && command !== 'help' && command !== '--help' && command !== '-h' && command !== '--version') {
|
|
259
|
+
trackEvent('command_executed', { command, success: true });
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
.catch((error) => {
|
|
263
|
+
trackEvent('command_executed', {
|
|
264
|
+
command,
|
|
265
|
+
success: false,
|
|
266
|
+
error_message: sanitize(error.message)
|
|
267
|
+
});
|
|
268
|
+
console.error(chalk.red(error.message));
|
|
269
|
+
process.exit(1);
|
|
270
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Module configuration - defines files, sections, and dependencies for each module
|
|
2
|
+
const MODULE_CONFIG = {
|
|
3
|
+
blog: {
|
|
4
|
+
website: {
|
|
5
|
+
files: [
|
|
6
|
+
'src/lib/blog.ts',
|
|
7
|
+
'src/types/blog.ts',
|
|
8
|
+
'src/app/blog',
|
|
9
|
+
'src/components/blog',
|
|
10
|
+
'src/app/sitemap.ts',
|
|
11
|
+
'content/blog',
|
|
12
|
+
],
|
|
13
|
+
sections: {
|
|
14
|
+
'src/components/layout/Navbar.tsx': ['BLOG_NAV_LINK'],
|
|
15
|
+
'src/components/layout/Footer.tsx': ['BLOG_FOOTER_LINK'],
|
|
16
|
+
},
|
|
17
|
+
dependencies: {
|
|
18
|
+
'gray-matter': '^4.0.3',
|
|
19
|
+
'marked': '^12.0.0',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
module.exports = { MODULE_CONFIG };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Module registry - available modules for LaunchFrame services
|
|
2
|
+
const MODULE_REGISTRY = {
|
|
3
|
+
blog: {
|
|
4
|
+
name: 'blog',
|
|
5
|
+
displayName: 'Blog',
|
|
6
|
+
description: 'Markdown-based blog using local .md files with YAML front-matter ā no database required',
|
|
7
|
+
services: ['website'],
|
|
8
|
+
version: '1.0.0'
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
module.exports = { MODULE_REGISTRY };
|
|
@@ -21,24 +21,16 @@ const VARIANT_CONFIG = {
|
|
|
21
21
|
// Complete files/folders to copy
|
|
22
22
|
files: [
|
|
23
23
|
'src/modules/domain/projects', // Entire projects module
|
|
24
|
-
'src/modules/domain/ai/services/project-config.service.ts', // Project config service
|
|
25
24
|
'src/guards/project-ownership.guard.ts', // Project ownership guard (header-based)
|
|
26
25
|
'src/guards/project-param.guard.ts', // Project param guard (route-based)
|
|
27
|
-
'src/modules/auth/auth.service.ts', // Auth service with multi-tenant support
|
|
28
|
-
'src/modules/auth/auth.controller.ts', // Auth controller with multi-tenant support
|
|
29
26
|
'src/modules/users/users.service.ts', // Users service with multi-tenant support
|
|
30
27
|
'src/modules/users/users.controller.ts', // Users controller with multi-tenant support
|
|
31
28
|
'src/modules/users/create-user.dto.ts' // CreateUserDto with businessId
|
|
32
29
|
],
|
|
33
30
|
|
|
34
31
|
// Code sections to insert into base template files
|
|
32
|
+
// Note: main.ts uses PRIMARY_DOMAIN env var for dynamic CORS - no sections needed
|
|
35
33
|
sections: {
|
|
36
|
-
'src/main.ts': [
|
|
37
|
-
'PROJECT_IMPORTS', // Add project-related imports
|
|
38
|
-
'PROJECT_CUSTOM_DOMAINS', // Add custom domains query
|
|
39
|
-
'PROJECT_CUSTOM_DOMAINS_CORS', // Add custom domains to CORS
|
|
40
|
-
'PROJECT_GUARD' // Add ProjectOwnershipGuard registration
|
|
41
|
-
],
|
|
42
34
|
'src/modules/app/app.module.ts': [
|
|
43
35
|
'PROJECTS_MODULE_IMPORT', // Add ProjectsModule import
|
|
44
36
|
'PROJECTS_MODULE' // Add ProjectsModule to imports array
|
|
@@ -60,11 +52,15 @@ const VARIANT_CONFIG = {
|
|
|
60
52
|
}
|
|
61
53
|
},
|
|
62
54
|
|
|
63
|
-
// B2B2C variant: Adds regular_user support
|
|
55
|
+
// B2B2C variant: Adds regular_user support with separate customer auth
|
|
64
56
|
'b2b2c': {
|
|
65
57
|
// Complete files to copy
|
|
66
58
|
files: [
|
|
67
|
-
'src/modules/users/user-business.entity.ts',
|
|
59
|
+
'src/modules/users/user-business.entity.ts', // Business-to-user linking entity
|
|
60
|
+
'src/modules/auth/auth-customer.ts', // Customer auth config (regular_user, customer_ cookie)
|
|
61
|
+
'src/modules/auth/better-auth-customer.controller.ts', // Customer auth controller (/api/auth/customer)
|
|
62
|
+
'src/modules/auth/auth.module.ts', // Auth module with customer controller
|
|
63
|
+
'src/modules/auth/better-auth.guard.ts', // Guard handling both auth instances
|
|
68
64
|
],
|
|
69
65
|
|
|
70
66
|
// Code sections to insert
|
|
@@ -77,6 +73,9 @@ const VARIANT_CONFIG = {
|
|
|
77
73
|
'src/modules/users/users.module.ts': [
|
|
78
74
|
'B2B2C_IMPORTS', // Add UserBusiness import
|
|
79
75
|
'B2B2C_ENTITIES' // Add UserBusiness to TypeORM
|
|
76
|
+
],
|
|
77
|
+
'src/database/migrations/1764300000001-CreateSessionsTable.ts': [
|
|
78
|
+
'B2B2C_TENANT_COLUMN' // Add tenant_id column for session scoping
|
|
80
79
|
]
|
|
81
80
|
}
|
|
82
81
|
},
|
|
@@ -126,8 +125,7 @@ const VARIANT_CONFIG = {
|
|
|
126
125
|
// Complete files to copy (has both multi-tenant and B2B2C features)
|
|
127
126
|
files: [
|
|
128
127
|
'src/modules/users/user-business.entity.ts', // Business-to-user linking entity
|
|
129
|
-
'src/modules/auth/auth.
|
|
130
|
-
'src/modules/auth/auth.controller.ts', // Combined auth controller
|
|
128
|
+
'src/modules/auth/auth.ts', // Combined Better Auth config
|
|
131
129
|
'src/modules/users/users.service.ts', // Combined users service
|
|
132
130
|
'src/modules/users/users.controller.ts', // Combined users controller
|
|
133
131
|
'src/modules/domain/projects/projects.module.ts' // Projects module with UserBusiness
|
|
@@ -310,6 +308,14 @@ const VARIANT_CONFIG = {
|
|
|
310
308
|
'src/store/useProjectStore.ts' // Project state
|
|
311
309
|
],
|
|
312
310
|
sections: {}
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
'b2b2c': {
|
|
314
|
+
// B2B2C uses separate auth endpoint for customer sessions
|
|
315
|
+
files: [
|
|
316
|
+
'src/lib/auth-client.ts' // Auth client with /api/auth/customer basePath
|
|
317
|
+
],
|
|
318
|
+
sections: {}
|
|
313
319
|
}
|
|
314
320
|
},
|
|
315
321
|
|
|
@@ -395,6 +401,11 @@ function resolveVariantChoices(backendChoices) {
|
|
|
395
401
|
choices['admin-portal'].userModel = backendChoices.userModel;
|
|
396
402
|
}
|
|
397
403
|
|
|
404
|
+
// Special case: customers-portal inherits BOTH tenancy and userModel
|
|
405
|
+
if (choices['customers-portal']) {
|
|
406
|
+
choices['customers-portal'].userModel = backendChoices.userModel;
|
|
407
|
+
}
|
|
408
|
+
|
|
398
409
|
// Special case: infrastructure needs BOTH tenancy and userModel for proper variant resolution
|
|
399
410
|
if (choices['infrastructure']) {
|
|
400
411
|
choices['infrastructure'].tenancy = backendChoices.tenancy;
|
|
@@ -159,7 +159,6 @@ async function buildFullAppImages(projectRoot, projectName, githubOrg, envFilePa
|
|
|
159
159
|
`DOCS_URL=${envVars.DOCS_URL || ''}`,
|
|
160
160
|
`CONTACT_EMAIL=${envVars.CONTACT_EMAIL || ''}`,
|
|
161
161
|
`CTA_LINK=${envVars.CTA_LINK || ''}`,
|
|
162
|
-
`LIVE_DEMO_URL=${envVars.LIVE_DEMO_URL || ''}`,
|
|
163
162
|
`MIXPANEL_PROJECT_TOKEN=${envVars.MIXPANEL_PROJECT_TOKEN || ''}`,
|
|
164
163
|
`GOOGLE_ANALYTICS_ID=${envVars.GOOGLE_ANALYTICS_ID || ''}`
|
|
165
164
|
];
|
|
@@ -245,15 +244,130 @@ async function buildWaitlistImage(projectRoot, projectName, githubOrg) {
|
|
|
245
244
|
});
|
|
246
245
|
|
|
247
246
|
spinner.succeed(`waitlist built and pushed successfully`);
|
|
247
|
+
|
|
248
|
+
// Clean up local image after push
|
|
249
|
+
const cleanupSpinner = ora('Cleaning up local waitlist image...').start();
|
|
250
|
+
try {
|
|
251
|
+
await execAsync(`docker rmi ${imageName}`, { timeout: 30000 });
|
|
252
|
+
cleanupSpinner.succeed('Cleaned up local waitlist image');
|
|
253
|
+
} catch (error) {
|
|
254
|
+
cleanupSpinner.info('Could not remove local waitlist image (may be in use)');
|
|
255
|
+
}
|
|
248
256
|
} catch (error) {
|
|
249
257
|
spinner.fail(`Failed to build waitlist`);
|
|
250
258
|
throw new Error(`Build failed for waitlist: ${error.message}`);
|
|
251
259
|
}
|
|
252
260
|
}
|
|
253
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Clean up local Docker images after push
|
|
264
|
+
* @param {string} registry - Registry URL (e.g., 'ghcr.io/myorg')
|
|
265
|
+
* @param {string} projectName - Project name
|
|
266
|
+
* @param {string[]} services - List of services to clean up
|
|
267
|
+
* @returns {Promise<void>}
|
|
268
|
+
*/
|
|
269
|
+
async function cleanupLocalImages(registry, projectName, services) {
|
|
270
|
+
const spinner = ora('Cleaning up local Docker images...').start();
|
|
271
|
+
|
|
272
|
+
const imagesToRemove = services.map(service => `${registry}/${projectName}-${service}:latest`);
|
|
273
|
+
let removedCount = 0;
|
|
274
|
+
|
|
275
|
+
for (const imageName of imagesToRemove) {
|
|
276
|
+
try {
|
|
277
|
+
await execAsync(`docker rmi ${imageName}`, { timeout: 30000 });
|
|
278
|
+
removedCount++;
|
|
279
|
+
} catch (error) {
|
|
280
|
+
// Image might not exist or be in use, continue
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (removedCount > 0) {
|
|
285
|
+
spinner.succeed(`Cleaned up ${removedCount} local Docker image(s)`);
|
|
286
|
+
} else {
|
|
287
|
+
spinner.info('No local images to clean up');
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Complete build and push workflow - checks Docker, logs in to GHCR, builds and pushes images
|
|
293
|
+
* @param {Object} options - Workflow options
|
|
294
|
+
* @param {string} options.projectRoot - Project root directory
|
|
295
|
+
* @param {string} options.projectName - Project name
|
|
296
|
+
* @param {string} options.githubOrg - GitHub organization/username
|
|
297
|
+
* @param {string} options.ghcrToken - GitHub Container Registry token
|
|
298
|
+
* @param {string} options.envProdPath - Path to .env.prod file
|
|
299
|
+
* @param {string[]} options.installedServices - List of installed services
|
|
300
|
+
* @param {string} [options.serviceName] - Optional specific service to build (if not provided, builds all)
|
|
301
|
+
* @returns {Promise<void>}
|
|
302
|
+
*/
|
|
303
|
+
async function buildAndPushWorkflow(options) {
|
|
304
|
+
const {
|
|
305
|
+
projectRoot,
|
|
306
|
+
projectName,
|
|
307
|
+
githubOrg,
|
|
308
|
+
ghcrToken,
|
|
309
|
+
envProdPath,
|
|
310
|
+
installedServices,
|
|
311
|
+
serviceName
|
|
312
|
+
} = options;
|
|
313
|
+
|
|
314
|
+
// Step 1: Check Docker is running
|
|
315
|
+
const dockerSpinner = ora('Checking Docker...').start();
|
|
316
|
+
|
|
317
|
+
const dockerRunning = await checkDockerRunning();
|
|
318
|
+
if (!dockerRunning) {
|
|
319
|
+
dockerSpinner.fail('Docker is not running');
|
|
320
|
+
throw new Error('Docker is not running. Please start Docker and try again.');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
dockerSpinner.succeed('Docker is running');
|
|
324
|
+
|
|
325
|
+
// Step 2: Login to GHCR
|
|
326
|
+
if (!ghcrToken) {
|
|
327
|
+
throw new Error('GHCR token not found. Run deploy:configure to set up your GitHub token.');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
await loginToGHCR(githubOrg, ghcrToken);
|
|
331
|
+
|
|
332
|
+
// Step 3: Build and push images
|
|
333
|
+
console.log(chalk.yellow('\nš¦ Building and pushing images...\n'));
|
|
334
|
+
|
|
335
|
+
const registry = `ghcr.io/${githubOrg}`;
|
|
336
|
+
|
|
337
|
+
if (serviceName) {
|
|
338
|
+
// Build specific service
|
|
339
|
+
if (!installedServices.includes(serviceName)) {
|
|
340
|
+
throw new Error(`Service "${serviceName}" not found in installed services. Available: ${installedServices.join(', ')}`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
await buildAndPushImage(
|
|
344
|
+
serviceName,
|
|
345
|
+
path.join(projectRoot, serviceName),
|
|
346
|
+
registry,
|
|
347
|
+
projectName
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
// Clean up local image after push
|
|
351
|
+
await cleanupLocalImages(registry, projectName, [serviceName]);
|
|
352
|
+
|
|
353
|
+
console.log(chalk.green.bold(`\nā
${serviceName} built and pushed to GHCR!\n`));
|
|
354
|
+
} else {
|
|
355
|
+
// Build all services
|
|
356
|
+
await buildFullAppImages(projectRoot, projectName, githubOrg, envProdPath, installedServices);
|
|
357
|
+
|
|
358
|
+
// Clean up local images after push
|
|
359
|
+
await cleanupLocalImages(registry, projectName, installedServices);
|
|
360
|
+
|
|
361
|
+
console.log(chalk.green.bold('\nā
All images built and pushed to GHCR!\n'));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
254
365
|
module.exports = {
|
|
255
366
|
checkDockerRunning,
|
|
256
367
|
loginToGHCR,
|
|
368
|
+
buildAndPushImage,
|
|
257
369
|
buildFullAppImages,
|
|
258
|
-
buildWaitlistImage
|
|
370
|
+
buildWaitlistImage,
|
|
371
|
+
buildAndPushWorkflow,
|
|
372
|
+
cleanupLocalImages
|
|
259
373
|
};
|