@launchframe/cli 0.1.6
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/README.md +59 -0
- package/package.json +45 -0
- package/src/commands/deploy-configure.js +219 -0
- package/src/commands/deploy-init.js +277 -0
- package/src/commands/deploy-set-env.js +232 -0
- package/src/commands/deploy-up.js +144 -0
- package/src/commands/docker-build.js +44 -0
- package/src/commands/docker-destroy.js +93 -0
- package/src/commands/docker-down.js +44 -0
- package/src/commands/docker-logs.js +69 -0
- package/src/commands/docker-up.js +73 -0
- package/src/commands/doctor.js +20 -0
- package/src/commands/help.js +79 -0
- package/src/commands/init.js +126 -0
- package/src/commands/service.js +569 -0
- package/src/commands/waitlist-deploy.js +231 -0
- package/src/commands/waitlist-down.js +50 -0
- package/src/commands/waitlist-logs.js +55 -0
- package/src/commands/waitlist-up.js +95 -0
- package/src/generator.js +190 -0
- package/src/index.js +158 -0
- package/src/prompts.js +200 -0
- package/src/services/registry.js +48 -0
- package/src/services/variant-config.js +349 -0
- package/src/utils/docker-helper.js +237 -0
- package/src/utils/env-generator.js +88 -0
- package/src/utils/env-validator.js +75 -0
- package/src/utils/file-ops.js +87 -0
- package/src/utils/project-helpers.js +104 -0
- package/src/utils/section-replacer.js +71 -0
- package/src/utils/ssh-helper.js +220 -0
- package/src/utils/variable-replacer.js +95 -0
- package/src/utils/variant-processor.js +313 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { isLaunchFrameProject } = require('./utils/project-helpers');
|
|
5
|
+
|
|
6
|
+
// Import commands
|
|
7
|
+
const { init } = require('./commands/init');
|
|
8
|
+
const { deployConfigure } = require('./commands/deploy-configure');
|
|
9
|
+
const { deploySetEnv } = require('./commands/deploy-set-env');
|
|
10
|
+
const { deployInit } = require('./commands/deploy-init');
|
|
11
|
+
const { deployUp } = require('./commands/deploy-up');
|
|
12
|
+
const { waitlistDeploy } = require('./commands/waitlist-deploy');
|
|
13
|
+
const { waitlistUp } = require('./commands/waitlist-up');
|
|
14
|
+
const { waitlistDown } = require('./commands/waitlist-down');
|
|
15
|
+
const { waitlistLogs } = require('./commands/waitlist-logs');
|
|
16
|
+
const { dockerBuild } = require('./commands/docker-build');
|
|
17
|
+
const { dockerUp } = require('./commands/docker-up');
|
|
18
|
+
const { dockerDown } = require('./commands/docker-down');
|
|
19
|
+
const { dockerLogs } = require('./commands/docker-logs');
|
|
20
|
+
const { dockerDestroy } = require('./commands/docker-destroy');
|
|
21
|
+
const { doctor } = require('./commands/doctor');
|
|
22
|
+
const { help } = require('./commands/help');
|
|
23
|
+
const {
|
|
24
|
+
serviceAdd,
|
|
25
|
+
serviceList,
|
|
26
|
+
serviceRemove
|
|
27
|
+
} = require('./commands/service');
|
|
28
|
+
|
|
29
|
+
// Get command and arguments
|
|
30
|
+
const command = process.argv[2];
|
|
31
|
+
const args = process.argv.slice(2);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse flags from command line arguments
|
|
35
|
+
* @param {string[]} args - Command line arguments
|
|
36
|
+
* @returns {Object} Parsed flags
|
|
37
|
+
*/
|
|
38
|
+
function parseFlags(args) {
|
|
39
|
+
const flags = {};
|
|
40
|
+
for (let i = 1; i < args.length; i++) {
|
|
41
|
+
const arg = args[i];
|
|
42
|
+
if (arg.startsWith('--')) {
|
|
43
|
+
const flagName = arg.substring(2);
|
|
44
|
+
const nextArg = args[i + 1];
|
|
45
|
+
// Check if next arg is a value (not a flag)
|
|
46
|
+
if (nextArg && !nextArg.startsWith('-')) {
|
|
47
|
+
flags[flagName] = nextArg;
|
|
48
|
+
i++; // Skip next arg since we consumed it
|
|
49
|
+
} else {
|
|
50
|
+
flags[flagName] = true; // Boolean flag
|
|
51
|
+
}
|
|
52
|
+
} else if (arg.startsWith('-') && arg.length === 2) {
|
|
53
|
+
const flagName = arg.substring(1);
|
|
54
|
+
flags[flagName] = true; // Short flag (always boolean)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return flags;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Main CLI router
|
|
62
|
+
*/
|
|
63
|
+
async function main() {
|
|
64
|
+
const inProject = isLaunchFrameProject();
|
|
65
|
+
const flags = parseFlags(args);
|
|
66
|
+
|
|
67
|
+
// No command provided
|
|
68
|
+
if (!command) {
|
|
69
|
+
if (inProject) {
|
|
70
|
+
console.error(chalk.red('\n❌ Error: No command specified'));
|
|
71
|
+
help();
|
|
72
|
+
process.exit(1);
|
|
73
|
+
} else {
|
|
74
|
+
// Outside project, default to init
|
|
75
|
+
await init(flags);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Route commands
|
|
81
|
+
switch (command) {
|
|
82
|
+
case 'init':
|
|
83
|
+
await init({ projectName: flags['project-name'] });
|
|
84
|
+
break;
|
|
85
|
+
case 'deploy:configure':
|
|
86
|
+
await deployConfigure();
|
|
87
|
+
break;
|
|
88
|
+
case 'deploy:set-env':
|
|
89
|
+
await deploySetEnv();
|
|
90
|
+
break;
|
|
91
|
+
case 'deploy:init':
|
|
92
|
+
await deployInit();
|
|
93
|
+
break;
|
|
94
|
+
case 'deploy:up':
|
|
95
|
+
await deployUp();
|
|
96
|
+
break;
|
|
97
|
+
case 'waitlist:deploy':
|
|
98
|
+
await waitlistDeploy();
|
|
99
|
+
break;
|
|
100
|
+
case 'waitlist:up':
|
|
101
|
+
await waitlistUp();
|
|
102
|
+
break;
|
|
103
|
+
case 'waitlist:down':
|
|
104
|
+
await waitlistDown();
|
|
105
|
+
break;
|
|
106
|
+
case 'waitlist:logs':
|
|
107
|
+
await waitlistLogs();
|
|
108
|
+
break;
|
|
109
|
+
case 'docker:build':
|
|
110
|
+
await dockerBuild();
|
|
111
|
+
break;
|
|
112
|
+
case 'docker:up':
|
|
113
|
+
await dockerUp(args[1]); // Pass optional service name
|
|
114
|
+
break;
|
|
115
|
+
case 'docker:down':
|
|
116
|
+
await dockerDown();
|
|
117
|
+
break;
|
|
118
|
+
case 'docker:logs':
|
|
119
|
+
await dockerLogs();
|
|
120
|
+
break;
|
|
121
|
+
case 'docker:destroy':
|
|
122
|
+
await dockerDestroy({ force: flags.force || flags.f });
|
|
123
|
+
break;
|
|
124
|
+
case 'doctor':
|
|
125
|
+
await doctor();
|
|
126
|
+
break;
|
|
127
|
+
case 'service:add':
|
|
128
|
+
if (!args[1]) {
|
|
129
|
+
console.error(chalk.red('Error: Service name required'));
|
|
130
|
+
console.log('Usage: launchframe service:add <service-name>');
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
await serviceAdd(args[1]);
|
|
134
|
+
break;
|
|
135
|
+
case 'service:list':
|
|
136
|
+
await serviceList();
|
|
137
|
+
break;
|
|
138
|
+
case 'service:remove':
|
|
139
|
+
if (!args[1]) {
|
|
140
|
+
console.error(chalk.red('Error: Service name required'));
|
|
141
|
+
console.log('Usage: launchframe service:remove <service-name>');
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
await serviceRemove(args[1]);
|
|
145
|
+
break;
|
|
146
|
+
case 'help':
|
|
147
|
+
case '--help':
|
|
148
|
+
case '-h':
|
|
149
|
+
help();
|
|
150
|
+
break;
|
|
151
|
+
default:
|
|
152
|
+
console.error(chalk.red(`\n❌ Unknown command: ${command}\n`));
|
|
153
|
+
help();
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
main();
|
package/src/prompts.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
const inquirer = require('inquirer');
|
|
2
|
+
const { getVariantPrompts } = require('./services/variant-config');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Prompts for initial project setup (local development only)
|
|
6
|
+
*/
|
|
7
|
+
async function runInitPrompts() {
|
|
8
|
+
const answers = await inquirer.prompt([
|
|
9
|
+
{
|
|
10
|
+
type: 'input',
|
|
11
|
+
name: 'projectName',
|
|
12
|
+
message: 'Project name (slug):',
|
|
13
|
+
default: 'my-saas',
|
|
14
|
+
validate: (input) => {
|
|
15
|
+
if (/^[a-z0-9-]+$/.test(input)) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
return 'Project name must contain only lowercase letters, numbers, and hyphens';
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
type: 'input',
|
|
23
|
+
name: 'projectDisplayName',
|
|
24
|
+
message: 'Project display name:',
|
|
25
|
+
default: (answers) => {
|
|
26
|
+
// Convert kebab-case to Title Case
|
|
27
|
+
return answers.projectName
|
|
28
|
+
.split('-')
|
|
29
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
30
|
+
.join(' ');
|
|
31
|
+
},
|
|
32
|
+
validate: (input) => {
|
|
33
|
+
if (input.trim().length > 0) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
return 'Project display name is required';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
// Transform answers
|
|
42
|
+
return {
|
|
43
|
+
...answers,
|
|
44
|
+
projectNameUpper: answers.projectName.toUpperCase().replace(/-/g, '_'),
|
|
45
|
+
projectNameCamel: answers.projectName.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Prompts for deployment configuration
|
|
51
|
+
* @param {string} projectName - The project name from .launchframe config
|
|
52
|
+
*/
|
|
53
|
+
async function runDeployPrompts(projectName = 'launchframe') {
|
|
54
|
+
const answers = await inquirer.prompt([
|
|
55
|
+
{
|
|
56
|
+
type: 'input',
|
|
57
|
+
name: 'primaryDomain',
|
|
58
|
+
message: 'Primary domain (e.g., example.com):',
|
|
59
|
+
validate: (input) => {
|
|
60
|
+
if (/^[a-z0-9-]+\.[a-z]{2,}$/.test(input)) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
return 'Please enter a valid domain (e.g., example.com)';
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
type: 'input',
|
|
68
|
+
name: 'adminEmail',
|
|
69
|
+
message: 'Admin email (for Let\'s Encrypt SSL):',
|
|
70
|
+
validate: (input) => {
|
|
71
|
+
if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input)) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
return 'Please enter a valid email address';
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
type: 'input',
|
|
79
|
+
name: 'githubOrg',
|
|
80
|
+
message: 'GitHub organization/username:',
|
|
81
|
+
validate: (input) => {
|
|
82
|
+
if (input.trim().length > 0) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
return 'GitHub org/username is required';
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
type: 'input',
|
|
90
|
+
name: 'vpsHost',
|
|
91
|
+
message: 'VPS hostname or IP:',
|
|
92
|
+
validate: (input) => {
|
|
93
|
+
if (input.trim().length > 0) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
return 'VPS host is required';
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
type: 'input',
|
|
101
|
+
name: 'vpsUser',
|
|
102
|
+
message: 'VPS SSH username:',
|
|
103
|
+
default: 'root',
|
|
104
|
+
validate: (input) => {
|
|
105
|
+
if (input.trim().length > 0) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
return 'VPS user is required';
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
type: 'input',
|
|
113
|
+
name: 'vpsAppFolder',
|
|
114
|
+
message: 'VPS deployment folder path:',
|
|
115
|
+
default: `/opt/${projectName}`,
|
|
116
|
+
validate: (input) => {
|
|
117
|
+
if (!input.startsWith('/')) {
|
|
118
|
+
return 'Must be an absolute path starting with /';
|
|
119
|
+
}
|
|
120
|
+
if (input.endsWith('/')) {
|
|
121
|
+
return 'Must not end with a trailing slash';
|
|
122
|
+
}
|
|
123
|
+
if (input.trim().length === 0) {
|
|
124
|
+
return 'VPS app folder is required';
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
type: 'password',
|
|
131
|
+
name: 'ghcrToken',
|
|
132
|
+
message: 'GitHub Personal Access Token (for Docker images):',
|
|
133
|
+
mask: '*',
|
|
134
|
+
validate: (input) => {
|
|
135
|
+
if (!input || input.trim().length === 0) {
|
|
136
|
+
return 'GHCR token is required for pushing Docker images';
|
|
137
|
+
}
|
|
138
|
+
if (!input.startsWith('ghp_') && !input.startsWith('github_pat_')) {
|
|
139
|
+
return 'Invalid GitHub token format (should start with ghp_ or github_pat_)';
|
|
140
|
+
}
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
return answers;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Prompts for variant selection (multi-tenancy, B2B vs B2B2C)
|
|
151
|
+
*/
|
|
152
|
+
async function runVariantPrompts() {
|
|
153
|
+
const prompts = getVariantPrompts();
|
|
154
|
+
|
|
155
|
+
console.log('\n📦 Configure Your Application\n');
|
|
156
|
+
|
|
157
|
+
// Prompt for user model
|
|
158
|
+
const userModelAnswer = await inquirer.prompt([
|
|
159
|
+
{
|
|
160
|
+
type: 'list',
|
|
161
|
+
name: 'userModel',
|
|
162
|
+
message: prompts.userModel.message,
|
|
163
|
+
choices: prompts.userModel.choices.map(choice => ({
|
|
164
|
+
name: choice.name,
|
|
165
|
+
value: choice.value,
|
|
166
|
+
short: choice.value
|
|
167
|
+
})),
|
|
168
|
+
default: prompts.userModel.default
|
|
169
|
+
}
|
|
170
|
+
]);
|
|
171
|
+
|
|
172
|
+
// Prompt for tenancy
|
|
173
|
+
const tenancyAnswer = await inquirer.prompt([
|
|
174
|
+
{
|
|
175
|
+
type: 'list',
|
|
176
|
+
name: 'tenancy',
|
|
177
|
+
message: prompts.tenancy.message,
|
|
178
|
+
choices: prompts.tenancy.choices.map(choice => ({
|
|
179
|
+
name: choice.name,
|
|
180
|
+
value: choice.value,
|
|
181
|
+
short: choice.value
|
|
182
|
+
})),
|
|
183
|
+
default: prompts.tenancy.default
|
|
184
|
+
}
|
|
185
|
+
]);
|
|
186
|
+
|
|
187
|
+
const variantChoices = {
|
|
188
|
+
tenancy: tenancyAnswer.tenancy,
|
|
189
|
+
userModel: userModelAnswer.userModel
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Show summary
|
|
193
|
+
console.log('\n✅ Configuration:');
|
|
194
|
+
console.log(` User Model: ${variantChoices.userModel}`);
|
|
195
|
+
console.log(` Tenancy: ${variantChoices.tenancy}\n`);
|
|
196
|
+
|
|
197
|
+
return variantChoices;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.exports = { runInitPrompts, runDeployPrompts, runVariantPrompts };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Service registry - available optional services for LaunchFrame
|
|
2
|
+
const SERVICE_REGISTRY = {
|
|
3
|
+
waitlist: {
|
|
4
|
+
name: 'waitlist',
|
|
5
|
+
displayName: 'Waitlist Landing Page',
|
|
6
|
+
description: 'Coming soon page with email collection via Airtable',
|
|
7
|
+
repoUrl: 'https://github.com/{{GITHUB_ORG}}/launchframe-waitlist',
|
|
8
|
+
techStack: 'Next.js (standalone)',
|
|
9
|
+
dependencies: ['Airtable API'],
|
|
10
|
+
standalone: true, // Has own docker-compose files, not part of infrastructure/
|
|
11
|
+
envVars: {
|
|
12
|
+
AIRTABLE_PERSONAL_ACCESS_TOKEN: 'Your Airtable personal access token (create at https://airtable.com/create/tokens)',
|
|
13
|
+
AIRTABLE_BASE_ID: 'Your Airtable base ID (e.g., appHhPeD0hVeiE7dS - found in your base URL: airtable.com/appXXXXXX/...)',
|
|
14
|
+
AIRTABLE_TABLE_NAME: 'Your Airtable table name (e.g., "Waitlist Signups" - the exact name of your table, not the table ID)'
|
|
15
|
+
},
|
|
16
|
+
installPath: 'waitlist',
|
|
17
|
+
devPort: 3002,
|
|
18
|
+
version: '1.0.0'
|
|
19
|
+
},
|
|
20
|
+
docs: {
|
|
21
|
+
name: 'docs',
|
|
22
|
+
displayName: 'Documentation Site',
|
|
23
|
+
description: 'VitePress documentation site with LaunchFrame philosophy',
|
|
24
|
+
repoUrl: 'https://github.com/{{GITHUB_ORG}}/launchframe-docs',
|
|
25
|
+
techStack: 'VitePress + sirv-cli',
|
|
26
|
+
dependencies: ['None'],
|
|
27
|
+
standalone: false, // Integrated into main infrastructure/
|
|
28
|
+
envVars: {}, // No env vars needed for static docs
|
|
29
|
+
installPath: 'docs',
|
|
30
|
+
devPort: 5173,
|
|
31
|
+
version: '1.0.0'
|
|
32
|
+
},
|
|
33
|
+
'customers-frontend': {
|
|
34
|
+
name: 'customers-frontend',
|
|
35
|
+
displayName: 'Customer Frontend',
|
|
36
|
+
description: 'Customer-facing portal for B2B2C (React + Vite + Zustand)',
|
|
37
|
+
repoUrl: 'https://github.com/{{GITHUB_ORG}}/launchframe-customers-frontend',
|
|
38
|
+
techStack: 'React + Vite + Zustand + TanStack Query',
|
|
39
|
+
dependencies: ['backend'],
|
|
40
|
+
standalone: false, // Integrated into main infrastructure/
|
|
41
|
+
envVars: {}, // Uses standard env vars from infrastructure/.env
|
|
42
|
+
installPath: 'customers-frontend',
|
|
43
|
+
devPort: 3000,
|
|
44
|
+
version: '1.0.0'
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
module.exports = { SERVICE_REGISTRY };
|