@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/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 };