@launchframe/cli 1.0.0 → 1.0.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@launchframe/cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Production-ready B2B SaaS boilerplate with subscriptions, credits, and multi-tenancy",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -3,24 +3,29 @@ const { clearCache, getCacheInfo } = require('../utils/service-cache');
3
3
 
4
4
  /**
5
5
  * Clear service cache
6
+ * @param {Object} flags - Optional flags
7
+ * @param {boolean} flags.yes - Skip confirmation prompt
8
+ * @param {boolean} flags.y - Skip confirmation prompt (short form)
6
9
  */
7
- async function cacheClear() {
10
+ async function cacheClear(flags = {}) {
8
11
  console.log(chalk.yellow('\n⚠️ This will delete all cached services'));
9
12
  console.log(chalk.gray('You will need to re-download on next init or service:add\n'));
10
-
11
- const inquirer = require('inquirer');
12
- const { confirmed } = await inquirer.prompt([{
13
- type: 'confirm',
14
- name: 'confirmed',
15
- message: 'Continue with cache clear?',
16
- default: false
17
- }]);
18
-
19
- if (!confirmed) {
20
- console.log('Cancelled');
21
- return;
13
+
14
+ if (!(flags.yes || flags.y)) {
15
+ const inquirer = require('inquirer');
16
+ const { confirmed } = await inquirer.prompt([{
17
+ type: 'confirm',
18
+ name: 'confirmed',
19
+ message: 'Continue with cache clear?',
20
+ default: false
21
+ }]);
22
+
23
+ if (!confirmed) {
24
+ console.log('Cancelled');
25
+ return;
26
+ }
22
27
  }
23
-
28
+
24
29
  await clearCache();
25
30
  }
26
31
 
@@ -5,7 +5,7 @@ const inquirer = require('inquirer');
5
5
  const { spawnSync } = require('child_process');
6
6
  const { requireProject, getProjectConfig } = require('../utils/project-helpers');
7
7
 
8
- async function databaseConsole({ remote = false } = {}) {
8
+ async function databaseConsole({ remote = false, query = null } = {}) {
9
9
  requireProject();
10
10
 
11
11
  const infrastructurePath = path.join(process.cwd(), 'infrastructure');
@@ -28,7 +28,24 @@ async function databaseConsole({ remote = false } = {}) {
28
28
 
29
29
  const { vpsUser, vpsHost, vpsAppFolder } = config.deployment;
30
30
 
31
- // 2. Warn before connecting to production
31
+ if (query) {
32
+ // Non-interactive query mode — pipe SQL via stdin, skip confirmation
33
+ const remoteCmd = `cd ${vpsAppFolder}/infrastructure && docker compose -f docker-compose.yml -f docker-compose.prod.yml exec -T database sh -c 'psql -U $POSTGRES_USER $POSTGRES_DB'`;
34
+ const result = spawnSync('ssh', [`${vpsUser}@${vpsHost}`, remoteCmd], {
35
+ input: query,
36
+ stdio: ['pipe', 'inherit', 'inherit'],
37
+ encoding: 'utf8'
38
+ });
39
+
40
+ if (result.status !== 0) {
41
+ console.error(chalk.red('\n❌ Could not execute query on the production database.'));
42
+ console.log(chalk.gray('Check that the VPS is reachable and services are running.\n'));
43
+ process.exit(1);
44
+ }
45
+ return;
46
+ }
47
+
48
+ // 2. Warn before connecting to production (interactive mode only)
32
49
  console.log(chalk.yellow.bold('\n⚠️ You are about to connect to the PRODUCTION database.\n'));
33
50
  console.log(chalk.gray(` Host: ${vpsHost}`));
34
51
  console.log(chalk.gray(` Folder: ${vpsAppFolder}\n`));
@@ -62,6 +79,29 @@ async function databaseConsole({ remote = false } = {}) {
62
79
  process.exit(1);
63
80
  }
64
81
  } else {
82
+ if (query) {
83
+ // Non-interactive local query mode — pipe SQL via stdin
84
+ const psqlCmd = [
85
+ 'compose', '-f', 'docker-compose.yml', '-f', 'docker-compose.dev.yml',
86
+ 'exec', '-T', 'database', 'sh', '-c', 'psql -U $POSTGRES_USER $POSTGRES_DB'
87
+ ];
88
+
89
+ const result = spawnSync('docker', psqlCmd, {
90
+ cwd: infrastructurePath,
91
+ input: query,
92
+ stdio: ['pipe', 'inherit', 'inherit'],
93
+ encoding: 'utf8'
94
+ });
95
+
96
+ if (result.status !== 0) {
97
+ console.error(chalk.red('\n❌ Could not execute query on the local database container.'));
98
+ console.log(chalk.gray('Make sure services are running:'));
99
+ console.log(chalk.white(' launchframe docker:up\n'));
100
+ process.exit(1);
101
+ }
102
+ return;
103
+ }
104
+
65
105
  console.log(chalk.blue.bold('\n🗄️ Opening local database console...\n'));
66
106
 
67
107
  // Let the shell inside the container expand $POSTGRES_USER / $POSTGRES_DB
@@ -34,7 +34,7 @@ function sqlBool(val) {
34
34
  return val ? 'true' : 'false';
35
35
  }
36
36
 
37
- async function deploySyncFeatures() {
37
+ async function deploySyncFeatures(flags = {}) {
38
38
  requireProject();
39
39
 
40
40
  // Step 1 — Project + infrastructure check
@@ -162,18 +162,20 @@ async function deploySyncFeatures() {
162
162
  console.log(chalk.gray(` Remote host: ${vpsHost}\n`));
163
163
  console.log(chalk.red('This will TRUNCATE subscription_plan_features (cascades to feature values).\n'));
164
164
 
165
- const { confirmed } = await inquirer.prompt([
166
- {
167
- type: 'confirm',
168
- name: 'confirmed',
169
- message: 'Are you sure you want to sync features to production?',
170
- default: false
165
+ if (!flags.yes) {
166
+ const { confirmed } = await inquirer.prompt([
167
+ {
168
+ type: 'confirm',
169
+ name: 'confirmed',
170
+ message: 'Are you sure you want to sync features to production?',
171
+ default: false
172
+ }
173
+ ]);
174
+
175
+ if (!confirmed) {
176
+ console.log(chalk.gray('\nAborted.\n'));
177
+ process.exit(0);
171
178
  }
172
- ]);
173
-
174
- if (!confirmed) {
175
- console.log(chalk.gray('\nAborted.\n'));
176
- process.exit(0);
177
179
  }
178
180
 
179
181
  // Step 9 — Build sync SQL transaction
@@ -30,12 +30,13 @@ function writeFile(filePath, data) {
30
30
 
31
31
  /**
32
32
  * Generate and inject logo/favicon assets across all relevant frontend services
33
+ * @param {string} svgPath - Optional path to SVG file (defaults to <projectRoot>/logo.svg)
33
34
  */
34
- async function devLogo() {
35
+ async function devLogo(svgPath) {
35
36
  requireProject();
36
37
 
37
38
  const cwd = process.cwd();
38
- const logoPath = path.join(cwd, 'logo.svg');
39
+ const logoPath = svgPath || path.join(cwd, 'logo.svg');
39
40
 
40
41
  if (!fs.existsSync(logoPath)) {
41
42
  console.error(chalk.red('\n❌ Error: logo.svg not found'));
@@ -1,13 +1,17 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const chalk = require('chalk');
4
- const { spawn } = require('child_process');
4
+ const { spawn, execSync } = require('child_process');
5
5
  const { requireProject } = require('../utils/project-helpers');
6
6
 
7
7
  /**
8
8
  * View logs from Docker services
9
+ * @param {string} service - Optional service name to filter logs
10
+ * @param {Object} flags - Optional flags
11
+ * @param {boolean} flags['no-follow'] - Snapshot mode: print lines and exit (non-interactive)
12
+ * @param {number} flags.tail - Number of lines to show (default 100, only used with --no-follow)
9
13
  */
10
- async function dockerLogs() {
14
+ async function dockerLogs(service, flags = {}) {
11
15
  requireProject();
12
16
 
13
17
  const infrastructurePath = path.join(process.cwd(), 'infrastructure');
@@ -18,9 +22,24 @@ async function dockerLogs() {
18
22
  process.exit(1);
19
23
  }
20
24
 
21
- // Get optional service name from args (e.g., launchframe docker:logs backend)
22
- const service = process.argv[3];
25
+ const noFollow = flags['no-follow'];
23
26
 
27
+ if (noFollow) {
28
+ // Snapshot mode — print tail and exit (non-interactive, suitable for MCP)
29
+ const tail = flags.tail || 100;
30
+ const args = ['-f', 'docker-compose.yml', '-f', 'docker-compose.dev.yml', 'logs', '--no-follow', '--tail', String(tail)];
31
+ if (service) args.push(service);
32
+
33
+ try {
34
+ execSync(`docker-compose ${args.join(' ')}`, { cwd: infrastructurePath, stdio: 'inherit' });
35
+ } catch (error) {
36
+ console.error(chalk.red('\n❌ Error viewing logs:'), error.message);
37
+ process.exit(1);
38
+ }
39
+ return;
40
+ }
41
+
42
+ // Streaming mode (interactive)
24
43
  console.log(chalk.blue.bold('\n📋 Docker Service Logs\n'));
25
44
 
26
45
  if (service) {
@@ -34,14 +53,14 @@ async function dockerLogs() {
34
53
 
35
54
  try {
36
55
  const logsCommand = 'docker-compose';
37
- const args = ['-f', 'docker-compose.yml', '-f', 'docker-compose.dev.yml', 'logs', '-f'];
56
+ const spawnArgs = ['-f', 'docker-compose.yml', '-f', 'docker-compose.dev.yml', 'logs', '-f'];
38
57
 
39
58
  if (service) {
40
- args.push(service);
59
+ spawnArgs.push(service);
41
60
  }
42
61
 
43
62
  // Use spawn to stream output in real-time
44
- const child = spawn(logsCommand, args, {
63
+ const child = spawn(logsCommand, spawnArgs, {
45
64
  cwd: infrastructurePath,
46
65
  stdio: 'inherit',
47
66
  shell: true
@@ -7,8 +7,10 @@ const { requireProject, getProjectConfig } = require('../utils/project-helpers')
7
7
  /**
8
8
  * Start Docker services (all or specific service)
9
9
  * @param {string} serviceName - Optional service name to start (e.g., 'docs', 'backend')
10
+ * @param {Object} flags - Optional flags
11
+ * @param {boolean} flags.detach - Run detached (docker-compose up -d) instead of watch mode
10
12
  */
11
- async function dockerUp(serviceName) {
13
+ async function dockerUp(serviceName, flags = {}) {
12
14
  requireProject();
13
15
 
14
16
  const infrastructurePath = path.join(process.cwd(), 'infrastructure');
@@ -19,14 +21,26 @@ async function dockerUp(serviceName) {
19
21
  process.exit(1);
20
22
  }
21
23
 
24
+ if (flags.detach) {
25
+ // Detached mode — start services in background (no watch, no blocking)
26
+ const upCommand = serviceName
27
+ ? `docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d ${serviceName}`
28
+ : 'docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d';
29
+
30
+ console.log(chalk.gray(`Running: ${upCommand}\n`));
31
+ execSync(upCommand, { cwd: infrastructurePath, stdio: 'inherit' });
32
+ console.log(chalk.green.bold('\n✅ Services started in detached mode.\n'));
33
+ return;
34
+ }
35
+
22
36
  // Check Docker Compose version for watch support
23
37
  try {
24
38
  const composeVersion = execSync('docker compose version', { encoding: 'utf8' });
25
39
  const versionMatch = composeVersion.match(/v?(\d+)\.(\d+)\.(\d+)/);
26
-
40
+
27
41
  if (versionMatch) {
28
42
  const [, major, minor] = versionMatch.map(Number);
29
-
43
+
30
44
  if (major < 2 || (major === 2 && minor < 22)) {
31
45
  console.error(chalk.red('\n❌ Error: Docker Compose v2.22+ is required for watch support'));
32
46
  console.log(chalk.yellow(`Current version: Docker Compose v${major}.${minor}`));
@@ -4,7 +4,7 @@ const chalk = require('chalk');
4
4
  const { execSync } = require('child_process');
5
5
  const { requireProject } = require('../utils/project-helpers');
6
6
 
7
- async function migrateCreate() {
7
+ async function migrateCreate(migrationName) {
8
8
  requireProject();
9
9
 
10
10
  const infrastructurePath = path.join(process.cwd(), 'infrastructure');
@@ -15,8 +15,6 @@ async function migrateCreate() {
15
15
  process.exit(1);
16
16
  }
17
17
 
18
- const migrationName = process.argv[3];
19
-
20
18
  if (!migrationName) {
21
19
  console.error(chalk.red('\n❌ Error: migration name is required'));
22
20
  console.log(chalk.gray('Usage: launchframe migrate:create <name>\n'));
@@ -41,7 +41,7 @@ async function moduleList() {
41
41
  console.log(' launchframe module:add <module-name>');
42
42
  }
43
43
 
44
- async function moduleAdd(moduleName) {
44
+ async function moduleAdd(moduleName, flags = {}) {
45
45
  requireProject();
46
46
 
47
47
  // Validate module exists in registry
@@ -97,16 +97,18 @@ async function moduleAdd(moduleName) {
97
97
  console.log(mod.description);
98
98
  console.log(`Affects services: ${mod.services.join(', ')}`);
99
99
 
100
- const { confirmed } = await inquirer.prompt([{
101
- type: 'confirm',
102
- name: 'confirmed',
103
- message: `Add module "${mod.displayName}" to your project?`,
104
- default: true
105
- }]);
106
-
107
- if (!confirmed) {
108
- console.log('Installation cancelled');
109
- process.exit(0);
100
+ if (!(flags.yes || flags.y)) {
101
+ const { confirmed } = await inquirer.prompt([{
102
+ type: 'confirm',
103
+ name: 'confirmed',
104
+ message: `Add module "${mod.displayName}" to your project?`,
105
+ default: true
106
+ }]);
107
+
108
+ if (!confirmed) {
109
+ console.log('Installation cancelled');
110
+ process.exit(0);
111
+ }
110
112
  }
111
113
 
112
114
  const affectedServices = [...new Set(Object.keys(MODULE_CONFIG[moduleName] || {}))].filter(s => s !== 'infrastructure');
@@ -139,8 +141,8 @@ async function moduleAdd(moduleName) {
139
141
  await dockerBuild(service);
140
142
  }
141
143
 
142
- // Restart the full stack in watch mode
143
- await dockerUp();
144
+ // Restart the full stack (detached when running non-interactively, watch otherwise)
145
+ await dockerUp(undefined, flags.yes || flags.y ? { detach: true } : {});
144
146
  }
145
147
 
146
148
  module.exports = { moduleAdd, moduleList };
@@ -10,7 +10,7 @@ const { updateEnvFile } = require('../utils/env-generator');
10
10
  const { checkGitHubAccess, showAccessDeniedMessage } = require('../utils/github-access');
11
11
  const { ensureCacheReady, getServicePath } = require('../utils/service-cache');
12
12
 
13
- async function serviceAdd(serviceName) {
13
+ async function serviceAdd(serviceName, flags = {}) {
14
14
  // STEP 1: Validation
15
15
  console.log(chalk.blue(`Installing ${serviceName} service...`));
16
16
 
@@ -49,16 +49,18 @@ async function serviceAdd(serviceName) {
49
49
  console.log(`Tech stack: ${service.techStack}`);
50
50
  console.log(`Dependencies: ${service.dependencies.join(', ')}`);
51
51
 
52
- const { confirmed } = await inquirer.prompt([{
53
- type: 'confirm',
54
- name: 'confirmed',
55
- message: 'Continue with installation?',
56
- default: true
57
- }]);
52
+ if (!(flags.yes || flags.y)) {
53
+ const { confirmed } = await inquirer.prompt([{
54
+ type: 'confirm',
55
+ name: 'confirmed',
56
+ message: 'Continue with installation?',
57
+ default: true
58
+ }]);
58
59
 
59
- if (!confirmed) {
60
- console.log('Installation cancelled');
61
- process.exit(0);
60
+ if (!confirmed) {
61
+ console.log('Installation cancelled');
62
+ process.exit(0);
63
+ }
62
64
  }
63
65
 
64
66
  // STEP 3: Get service files (from cache in production, local in dev)
@@ -124,7 +126,7 @@ async function serviceAdd(serviceName) {
124
126
 
125
127
  // STEP 4: Service-specific prompts (e.g., Airtable credentials)
126
128
  console.log(chalk.blue('\nConfiguring service...'));
127
- const envValues = await runServicePrompts(service);
129
+ const envValues = await runServicePrompts(service, flags);
128
130
 
129
131
  // STEP 5: Replace template variables
130
132
  console.log(chalk.blue('\nCustomizing service for your project...'));
@@ -265,9 +267,17 @@ async function serviceAdd(serviceName) {
265
267
  console.log(`\n📖 See README.md in ${serviceName}/ for more details.`);
266
268
  }
267
269
 
268
- async function runServicePrompts(service) {
270
+ async function runServicePrompts(service, flags = {}) {
269
271
  const envValues = {};
270
272
 
273
+ if (flags.yes || flags.y) {
274
+ // Non-interactive mode — use empty values; configure manually after install
275
+ for (const key of Object.keys(service.envVars)) {
276
+ envValues[key] = '';
277
+ }
278
+ return envValues;
279
+ }
280
+
271
281
  // Prompt for each required env var
272
282
  for (const [key, description] of Object.entries(service.envVars)) {
273
283
  const { value } = await inquirer.prompt([{
@@ -1,11 +1,14 @@
1
1
  const chalk = require('chalk');
2
- const { spawn } = require('child_process');
2
+ const { spawn, spawnSync } = require('child_process');
3
3
  const { requireProject, getProjectConfig, isWaitlistInstalled } = require('../utils/project-helpers');
4
4
 
5
5
  /**
6
- * View waitlist logs from VPS (streaming)
6
+ * View waitlist logs from VPS
7
+ * @param {Object} flags - Optional flags
8
+ * @param {boolean} flags['no-follow'] - Snapshot mode: print tail and exit (non-interactive)
9
+ * @param {number} flags.tail - Number of lines to show (default 100, used with --no-follow)
7
10
  */
8
- async function waitlistLogs() {
11
+ async function waitlistLogs(flags = {}) {
9
12
  requireProject();
10
13
 
11
14
  console.log(chalk.blue.bold('\n📋 Waitlist Logs\n'));
@@ -28,6 +31,20 @@ async function waitlistLogs() {
28
31
 
29
32
  const { vpsHost, vpsUser, vpsAppFolder } = config.deployment;
30
33
 
34
+ if (flags['no-follow']) {
35
+ // Snapshot mode — print tail and exit (non-interactive, suitable for MCP)
36
+ const tail = flags.tail || 100;
37
+ const sshCmd = `cd ${vpsAppFolder}/waitlist && docker-compose -f docker-compose.waitlist.yml logs --no-follow --tail=${tail}`;
38
+
39
+ const result = spawnSync('ssh', [`${vpsUser}@${vpsHost}`, sshCmd], { stdio: 'inherit' });
40
+
41
+ if (result.status !== 0 && result.status !== null) {
42
+ console.log(chalk.yellow(`\n⚠️ Process exited with code ${result.status}\n`));
43
+ }
44
+ return;
45
+ }
46
+
47
+ // Streaming mode (interactive)
31
48
  console.log(chalk.gray('Connecting to VPS and streaming logs...\n'));
32
49
  console.log(chalk.gray('Press Ctrl+C to exit\n'));
33
50
 
package/src/index.js CHANGED
@@ -132,7 +132,7 @@ async function main() {
132
132
  await deployBuild(args[1]); // Optional service name
133
133
  break;
134
134
  case 'deploy:sync-features':
135
- await deploySyncFeatures();
135
+ await deploySyncFeatures(flags);
136
136
  break;
137
137
  case 'waitlist:deploy':
138
138
  await waitlistDeploy();
@@ -144,19 +144,19 @@ async function main() {
144
144
  await waitlistDown();
145
145
  break;
146
146
  case 'waitlist:logs':
147
- await waitlistLogs();
147
+ await waitlistLogs(flags);
148
148
  break;
149
149
  case 'docker:build':
150
150
  await dockerBuild(args[1]); // Optional service name
151
151
  break;
152
152
  case 'docker:up':
153
- await dockerUp(args[1]); // Pass optional service name
153
+ await dockerUp(args[1] || flags.service, flags);
154
154
  break;
155
155
  case 'docker:down':
156
156
  await dockerDown();
157
157
  break;
158
158
  case 'docker:logs':
159
- await dockerLogs();
159
+ await dockerLogs(args[1] || flags.service, flags);
160
160
  break;
161
161
  case 'docker:destroy':
162
162
  await dockerDestroy({ force: flags.force || flags.f });
@@ -165,24 +165,24 @@ async function main() {
165
165
  await migrateRun();
166
166
  break;
167
167
  case 'migration:create':
168
- await migrateCreate();
168
+ await migrateCreate(args[1] || flags.name);
169
169
  break;
170
170
  case 'migration:revert':
171
171
  await migrateRevert();
172
172
  break;
173
173
  case 'database:console':
174
- await databaseConsole({ remote: flags.remote });
174
+ await databaseConsole({ remote: flags.remote, query: flags.query });
175
175
  break;
176
176
  case 'doctor':
177
177
  await doctor();
178
178
  break;
179
179
  case 'service:add':
180
- if (!args[1]) {
180
+ if (!args[1] && !flags.service) {
181
181
  console.error(chalk.red('Error: Service name required'));
182
182
  console.log('Usage: launchframe service:add <service-name>');
183
183
  process.exit(1);
184
184
  }
185
- await serviceAdd(args[1]);
185
+ await serviceAdd(args[1] || flags.service, flags);
186
186
  break;
187
187
  case 'service:list':
188
188
  await serviceList();
@@ -196,18 +196,18 @@ async function main() {
196
196
  await serviceRemove(args[1]);
197
197
  break;
198
198
  case 'module:add':
199
- if (!args[1]) {
199
+ if (!args[1] && !flags.name) {
200
200
  console.error(chalk.red('Error: Module name required'));
201
201
  console.log('Usage: launchframe module:add <module-name>');
202
202
  process.exit(1);
203
203
  }
204
- await moduleAdd(args[1]);
204
+ await moduleAdd(args[1] || flags.name, flags);
205
205
  break;
206
206
  case 'module:list':
207
207
  await moduleList();
208
208
  break;
209
209
  case 'cache:clear':
210
- await cacheClear();
210
+ await cacheClear(flags);
211
211
  break;
212
212
  case 'cache:info':
213
213
  await cacheInfo();
@@ -222,7 +222,7 @@ async function main() {
222
222
  await devQueue();
223
223
  break;
224
224
  case 'dev:logo':
225
- await devLogo();
225
+ await devLogo(args[1] || flags.svg);
226
226
  break;
227
227
  case 'dev:npm-install':
228
228
  if (!args[1]) {