launchpd 0.1.2 → 0.1.5

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.

Potentially problematic release.


This version of launchpd might be problematic. Click here for more details.

@@ -2,11 +2,11 @@ import { existsSync, statSync } from 'node:fs';
2
2
  import { readdir } from 'node:fs/promises';
3
3
  import { resolve, basename, join } from 'node:path';
4
4
  import { generateSubdomain } from '../utils/id.js';
5
- import { uploadFolder } from '../utils/upload.js';
6
- import { recordDeployment as recordMetadata, getNextVersion, setActiveVersion } from '../utils/metadata.js';
5
+ import { uploadFolder, finalizeUpload } from '../utils/upload.js';
6
+ import { getNextVersion } from '../utils/metadata.js';
7
7
  import { saveLocalDeployment } from '../utils/localConfig.js';
8
- import { recordDeployment as recordToAPI, getNextVersionFromAPI } from '../utils/api.js';
9
- import { success, error, info, warning } from '../utils/logger.js';
8
+ import { getNextVersionFromAPI } from '../utils/api.js';
9
+ import { success, errorWithSuggestions, info, warning, spinner, formatSize } from '../utils/logger.js';
10
10
  import { calculateExpiresAt, formatTimeRemaining } from '../utils/expiration.js';
11
11
  import { checkQuota, displayQuotaWarnings } from '../utils/quota.js';
12
12
  import { getCredentials } from '../utils/credentials.js';
@@ -42,9 +42,11 @@ async function calculateFolderSize(folderPath) {
42
42
  * @param {boolean} options.dryRun - Skip actual upload
43
43
  * @param {string} options.name - Custom subdomain
44
44
  * @param {string} options.expires - Expiration time (e.g., "30m", "2h", "1d")
45
+ * @param {boolean} options.verbose - Show verbose error details
45
46
  */
46
47
  export async function deploy(folder, options) {
47
48
  const folderPath = resolve(folder);
49
+ const verbose = options.verbose || false;
48
50
 
49
51
  // Parse expiration if provided
50
52
  let expiresAt = null;
@@ -52,41 +54,58 @@ export async function deploy(folder, options) {
52
54
  try {
53
55
  expiresAt = calculateExpiresAt(options.expires);
54
56
  } catch (err) {
55
- error(err.message);
57
+ errorWithSuggestions(err.message, [
58
+ 'Use format like: 30m, 2h, 1d, 7d',
59
+ 'Minimum expiration is 30 minutes',
60
+ 'Examples: --expires 1h, --expires 2d',
61
+ ], { verbose, cause: err });
56
62
  process.exit(1);
57
63
  }
58
64
  }
59
65
 
60
66
  // Validate folder exists
61
67
  if (!existsSync(folderPath)) {
62
- error(`Folder not found: ${folderPath}`);
68
+ errorWithSuggestions(`Folder not found: ${folderPath}`, [
69
+ 'Check the path is correct',
70
+ 'Use an absolute path or path relative to current directory',
71
+ `Current directory: ${process.cwd()}`,
72
+ ], { verbose });
63
73
  process.exit(1);
64
74
  }
65
75
 
66
76
  // Check folder is not empty
77
+ const scanSpinner = spinner('Scanning folder...');
67
78
  const files = await readdir(folderPath, { recursive: true, withFileTypes: true });
68
79
  const fileCount = files.filter(f => f.isFile()).length;
69
80
 
70
81
  if (fileCount === 0) {
71
- error('Folder is empty. Nothing to deploy.');
82
+ scanSpinner.fail('Folder is empty');
83
+ errorWithSuggestions('Nothing to deploy.', [
84
+ 'Add some files to your folder',
85
+ 'Make sure index.html exists for static sites',
86
+ ], { verbose });
72
87
  process.exit(1);
73
88
  }
89
+ scanSpinner.succeed(`Found ${fileCount} file(s)`);
74
90
 
75
91
  // Generate or use provided subdomain
76
92
  const subdomain = options.name || generateSubdomain();
77
93
  const url = `https://${subdomain}.launchpd.cloud`;
78
94
 
79
95
  // Calculate estimated upload size
96
+ const sizeSpinner = spinner('Calculating folder size...');
80
97
  const estimatedBytes = await calculateFolderSize(folderPath);
98
+ sizeSpinner.succeed(`Size: ${formatSize(estimatedBytes)}`);
81
99
 
82
100
  // Check quota before deploying
83
- info('Checking quota...');
101
+ const quotaSpinner = spinner('Checking quota...');
84
102
  const quotaCheck = await checkQuota(subdomain, estimatedBytes);
85
103
 
86
104
  if (!quotaCheck.allowed) {
87
- error('Deployment blocked due to quota limits');
105
+ quotaSpinner.fail('Deployment blocked due to quota limits');
88
106
  process.exit(1);
89
107
  }
108
+ quotaSpinner.succeed('Quota check passed');
90
109
 
91
110
  // Display any warnings
92
111
  displayQuotaWarnings(quotaCheck.warnings);
@@ -101,7 +120,6 @@ export async function deploy(folder, options) {
101
120
 
102
121
  info(`Deploying ${fileCount} file(s) from ${folderPath}`);
103
122
  info(`Target: ${url}`);
104
- info(`Size: ${(estimatedBytes / 1024 / 1024).toFixed(2)}MB`);
105
123
 
106
124
  if (options.dryRun) {
107
125
  warning('Dry run mode - skipping upload');
@@ -127,7 +145,7 @@ export async function deploy(folder, options) {
127
145
  ? (quotaCheck.quota.usage?.siteCount || 0) + 1
128
146
  : quotaCheck.quota.usage?.siteCount || 0;
129
147
  console.log(` Sites: ${sitesAfter}/${quotaCheck.quota.limits.maxSites}`);
130
- console.log(` Storage: ${(storageAfter / 1024 / 1024).toFixed(1)}MB/${quotaCheck.quota.limits.maxStorageMB}MB`);
148
+ console.log(` Storage: ${formatSize(storageAfter)}/${quotaCheck.quota.limits.maxStorageMB}MB`);
131
149
  console.log('');
132
150
  }
133
151
  return;
@@ -135,42 +153,37 @@ export async function deploy(folder, options) {
135
153
 
136
154
  // Perform actual upload
137
155
  try {
138
- // Get next version number for this subdomain (try API first, fallback to R2)
156
+ // Get next version number for this subdomain (try API first, fallback to local)
157
+ const versionSpinner = spinner('Fetching version info...');
139
158
  let version = await getNextVersionFromAPI(subdomain);
140
159
  if (version === null) {
141
160
  version = await getNextVersion(subdomain);
142
161
  }
143
- info(`Deploying as version ${version}...`);
162
+ versionSpinner.succeed(`Deploying as version ${version}`);
144
163
 
145
- const { totalBytes } = await uploadFolder(folderPath, subdomain, version);
164
+ // Upload all files via API proxy
165
+ const folderName = basename(folderPath);
166
+ const uploadSpinner = spinner(`Uploading files... 0/${fileCount}`);
146
167
 
147
- // Set this version as active
148
- await setActiveVersion(subdomain, version);
168
+ const { totalBytes } = await uploadFolder(folderPath, subdomain, version, (uploaded, total, fileName) => {
169
+ uploadSpinner.update(`Uploading files... ${uploaded}/${total} (${fileName})`);
170
+ });
149
171
 
150
- // Record deployment metadata
151
- info('Recording deployment metadata...');
172
+ uploadSpinner.succeed(`Uploaded ${fileCount} files (${formatSize(totalBytes)})`);
152
173
 
153
- // Try API first for centralized storage
154
- const folderName = basename(folderPath);
155
- const apiResult = await recordToAPI({
174
+ // Finalize upload: set active version and record metadata
175
+ const finalizeSpinner = spinner('Finalizing deployment...');
176
+ await finalizeUpload(
156
177
  subdomain,
157
- folderName,
178
+ version,
158
179
  fileCount,
159
180
  totalBytes,
160
- version,
161
- expiresAt: expiresAt?.toISOString() || null,
162
- });
163
-
164
- if (apiResult) {
165
- // API succeeded - deployment is centrally tracked
166
- info('Deployment recorded to central API');
167
- } else {
168
- // API unavailable - fallback to R2 metadata
169
- warning('Central API unavailable, using local fallback');
170
- await recordMetadata(subdomain, folderPath, fileCount, totalBytes, version, expiresAt);
171
- }
181
+ folderName,
182
+ expiresAt?.toISOString() || null
183
+ );
184
+ finalizeSpinner.succeed('Deployment finalized');
172
185
 
173
- // Always save locally for quick access
186
+ // Save locally for quick access
174
187
  await saveLocalDeployment({
175
188
  subdomain,
176
189
  folderName,
@@ -182,13 +195,33 @@ export async function deploy(folder, options) {
182
195
  });
183
196
 
184
197
  success(`Deployed successfully! (v${version})`);
185
- console.log(`\n 🚀 ${url}`);
198
+ console.log(`\n${url}`);
186
199
  if (expiresAt) {
187
- warning(`Expires: ${formatTimeRemaining(expiresAt)}`);
200
+ warning(`Expires: ${formatTimeRemaining(expiresAt)}`);
188
201
  }
189
202
  console.log('');
190
203
  } catch (err) {
191
- error(`Upload failed: ${err.message}`);
204
+ const suggestions = [];
205
+
206
+ // Provide context-specific suggestions
207
+ if (err.message.includes('fetch failed') || err.message.includes('ENOTFOUND')) {
208
+ suggestions.push('Check your internet connection');
209
+ suggestions.push('The API server may be temporarily unavailable');
210
+ } else if (err.message.includes('401') || err.message.includes('Unauthorized')) {
211
+ suggestions.push('Run "launchpd login" to authenticate');
212
+ suggestions.push('Your API key may have expired');
213
+ } else if (err.message.includes('413') || err.message.includes('too large')) {
214
+ suggestions.push('Try deploying fewer or smaller files');
215
+ suggestions.push('Check your storage quota with "launchpd quota"');
216
+ } else if (err.message.includes('429') || err.message.includes('rate limit')) {
217
+ suggestions.push('Wait a few minutes and try again');
218
+ suggestions.push('You may be deploying too frequently');
219
+ } else {
220
+ suggestions.push('Try running with --verbose for more details');
221
+ suggestions.push('Check https://status.launchpd.cloud for service status');
222
+ }
223
+
224
+ errorWithSuggestions(`Upload failed: ${err.message}`, suggestions, { verbose, cause: err });
192
225
  process.exit(1);
193
226
  }
194
227
  }
@@ -1,6 +1,6 @@
1
1
  import { getLocalDeployments } from '../utils/localConfig.js';
2
2
  import { listDeployments as listFromAPI } from '../utils/api.js';
3
- import { error, info, warning } from '../utils/logger.js';
3
+ import { errorWithSuggestions, info, spinner, formatSize } from '../utils/logger.js';
4
4
  import { formatTimeRemaining, isExpired } from '../utils/expiration.js';
5
5
  import chalk from 'chalk';
6
6
 
@@ -9,12 +9,17 @@ import chalk from 'chalk';
9
9
  * @param {object} options - Command options
10
10
  * @param {boolean} options.json - Output as JSON
11
11
  * @param {boolean} options.local - Only show local deployments
12
+ * @param {boolean} options.verbose - Show verbose error details
12
13
  */
13
14
  export async function list(options) {
15
+ const verbose = options.verbose || false;
16
+
14
17
  try {
15
18
  let deployments = [];
16
19
  let source = 'local';
17
20
 
21
+ const fetchSpinner = spinner('Fetching deployments...');
22
+
18
23
  // Try API first unless --local flag is set
19
24
  if (!options.local) {
20
25
  const apiResult = await listFromAPI();
@@ -40,11 +45,13 @@ export async function list(options) {
40
45
  }
41
46
 
42
47
  if (deployments.length === 0) {
43
- warning('No deployments found.');
44
- info('Deploy a folder with: launchpd deploy ./my-folder');
48
+ fetchSpinner.warn('No deployments found');
49
+ info('Deploy a folder with: ' + chalk.cyan('launchpd deploy ./my-folder'));
45
50
  return;
46
51
  }
47
52
 
53
+ fetchSpinner.succeed(`Found ${deployments.length} deployment(s)`);
54
+
48
55
  if (options.json) {
49
56
  console.log(JSON.stringify(deployments, null, 2));
50
57
  return;
@@ -53,7 +60,7 @@ export async function list(options) {
53
60
  // Display as table
54
61
  console.log('');
55
62
  console.log(chalk.bold('Your Deployments:'));
56
- console.log(chalk.gray('─'.repeat(95)));
63
+ console.log(chalk.gray('─'.repeat(100)));
57
64
 
58
65
  // Header
59
66
  console.log(
@@ -61,43 +68,58 @@ export async function list(options) {
61
68
  padRight('URL', 40) +
62
69
  padRight('Folder', 15) +
63
70
  padRight('Files', 7) +
71
+ padRight('Size', 12) +
64
72
  padRight('Date', 12) +
65
73
  'Status'
66
74
  )
67
75
  );
68
- console.log(chalk.gray('─'.repeat(95)));
76
+ console.log(chalk.gray('─'.repeat(100)));
69
77
 
70
78
  // Rows (most recent first)
71
79
  const sorted = [...deployments].reverse();
72
80
  for (const dep of sorted) {
73
81
  const url = `https://${dep.subdomain}.launchpd.cloud`;
74
82
  const date = new Date(dep.timestamp).toLocaleDateString();
83
+ const size = dep.totalBytes ? formatSize(dep.totalBytes) : '-';
75
84
 
76
- // Determine status
77
- let status = chalk.green('active');
85
+ // Determine status with colors
86
+ let status;
78
87
  if (dep.expiresAt) {
79
88
  if (isExpired(dep.expiresAt)) {
80
- status = chalk.red('expired');
89
+ status = chalk.red.bold('expired');
81
90
  } else {
82
- status = chalk.yellow(formatTimeRemaining(dep.expiresAt));
91
+ status = chalk.yellow(`⏱ ${formatTimeRemaining(dep.expiresAt)}`);
83
92
  }
93
+ } else {
94
+ status = chalk.green.bold('● active');
84
95
  }
85
96
 
97
+ // Version badge
98
+ const versionBadge = chalk.magenta(`v${dep.version || 1}`);
99
+
86
100
  console.log(
87
101
  chalk.cyan(padRight(url, 40)) +
88
- padRight(dep.folderName || '-', 15) +
89
- padRight(String(dep.fileCount), 7) +
102
+ chalk.white(padRight(dep.folderName || '-', 15)) +
103
+ chalk.white(padRight(String(dep.fileCount), 7)) +
104
+ chalk.white(padRight(size, 12)) +
90
105
  chalk.gray(padRight(date, 12)) +
91
- status
106
+ status + ' ' + versionBadge
92
107
  );
93
108
  }
94
109
 
95
- console.log(chalk.gray('─'.repeat(95)));
96
- console.log(chalk.gray(`Total: ${deployments.length} deployment(s)`) + (source === 'api' ? chalk.green(' (synced)') : chalk.yellow(' (local only)')));
110
+ console.log(chalk.gray('─'.repeat(100)));
111
+ const syncStatus = source === 'api'
112
+ ? chalk.green(' ✓ synced')
113
+ : chalk.yellow(' ⚠ local only');
114
+ console.log(chalk.gray(`Total: ${deployments.length} deployment(s)`) + syncStatus);
97
115
  console.log('');
98
116
 
99
117
  } catch (err) {
100
- error(`Failed to list deployments: ${err.message}`);
118
+ errorWithSuggestions(`Failed to list deployments: ${err.message}`, [
119
+ 'Check your internet connection',
120
+ 'Use --local flag to show local deployments only',
121
+ 'Try running with --verbose for more details',
122
+ ], { verbose, cause: err });
101
123
  process.exit(1);
102
124
  }
103
125
  }
@@ -1,16 +1,20 @@
1
1
  import { getVersionsForSubdomain, setActiveVersion, getActiveVersion } from '../utils/metadata.js';
2
2
  import { getVersions as getVersionsFromAPI, rollbackVersion as rollbackViaAPI } from '../utils/api.js';
3
- import { success, error, info, warning } from '../utils/logger.js';
3
+ import { error, errorWithSuggestions, info, warning, spinner } from '../utils/logger.js';
4
+ import chalk from 'chalk';
4
5
 
5
6
  /**
6
7
  * Rollback a subdomain to a previous version
7
8
  * @param {string} subdomain - Subdomain to rollback
8
9
  * @param {object} options - Command options
9
10
  * @param {number} options.to - Specific version to rollback to (optional)
11
+ * @param {boolean} options.verbose - Show verbose error details
10
12
  */
11
13
  export async function rollback(subdomain, options) {
14
+ const verbose = options.verbose || false;
15
+
12
16
  try {
13
- info(`Checking versions for ${subdomain}...`);
17
+ const fetchSpinner = spinner(`Checking versions for ${subdomain}...`);
14
18
 
15
19
  // Get all versions for this subdomain (try API first)
16
20
  let versions = [];
@@ -33,16 +37,22 @@ export async function rollback(subdomain, options) {
33
37
  }
34
38
 
35
39
  if (versions.length === 0) {
36
- error(`No deployments found for subdomain: ${subdomain}`);
40
+ fetchSpinner.fail('No deployments found');
41
+ errorWithSuggestions(`No deployments found for subdomain: ${subdomain}`, [
42
+ 'Check the subdomain name is correct',
43
+ 'Run "launchpd list" to see your deployments',
44
+ ], { verbose });
37
45
  process.exit(1);
38
46
  }
39
47
 
40
48
  if (versions.length === 1) {
41
- warning('Only one version exists. Nothing to rollback to.');
49
+ fetchSpinner.warn('Only one version exists');
50
+ warning('Nothing to rollback to.');
42
51
  process.exit(1);
43
52
  }
44
53
 
45
- info(`Current active version: v${currentActive}`);
54
+ fetchSpinner.succeed(`Found ${versions.length} versions`);
55
+ info(`Current active version: ${chalk.cyan(`v${currentActive}`)}`);
46
56
 
47
57
  // Determine target version
48
58
  let targetVersion;
@@ -51,9 +61,12 @@ export async function rollback(subdomain, options) {
51
61
  const versionExists = versions.some(v => v.version === targetVersion);
52
62
  if (!versionExists) {
53
63
  error(`Version ${targetVersion} does not exist.`);
64
+ console.log('');
54
65
  info('Available versions:');
55
66
  versions.forEach(v => {
56
- info(` v${v.version} - ${v.timestamp}`);
67
+ const isActive = v.version === currentActive;
68
+ const marker = isActive ? chalk.green(' (active)') : '';
69
+ console.log(` ${chalk.cyan(`v${v.version}`)} - ${chalk.gray(v.timestamp)}${marker}`);
57
70
  });
58
71
  process.exit(1);
59
72
  }
@@ -69,18 +82,18 @@ export async function rollback(subdomain, options) {
69
82
  }
70
83
 
71
84
  if (targetVersion === currentActive) {
72
- warning(`Version ${targetVersion} is already active.`);
85
+ warning(`Version ${chalk.cyan(`v${targetVersion}`)} is already active.`);
73
86
  process.exit(0);
74
87
  }
75
88
 
76
- info(`Rolling back from v${currentActive} to v${targetVersion}...`);
89
+ const rollbackSpinner = spinner(`Rolling back from v${currentActive} to v${targetVersion}...`);
77
90
 
78
91
  // Set the target version as active
79
92
  if (useAPI) {
80
93
  // Use API for centralized rollback (updates both D1 and R2)
81
94
  const result = await rollbackViaAPI(subdomain, targetVersion);
82
95
  if (!result) {
83
- warning('API unavailable, falling back to local rollback');
96
+ rollbackSpinner.warn('API unavailable, using local rollback');
84
97
  await setActiveVersion(subdomain, targetVersion);
85
98
  }
86
99
  } else {
@@ -90,12 +103,16 @@ export async function rollback(subdomain, options) {
90
103
  // Find the target version's deployment record for file count
91
104
  const targetDeployment = versions.find(v => v.version === targetVersion);
92
105
 
93
- success(`Rolled back to v${targetVersion} successfully!`);
106
+ rollbackSpinner.succeed(`Rolled back to ${chalk.cyan(`v${targetVersion}`)}`);
94
107
  console.log(`\n 🔄 https://${subdomain}.launchpd.cloud\n`);
95
- info(`Restored deployment from: ${targetDeployment?.timestamp || 'unknown'}`);
108
+ info(`Restored deployment from: ${chalk.gray(targetDeployment?.timestamp || 'unknown')}`);
96
109
 
97
110
  } catch (err) {
98
- error(`Rollback failed: ${err.message}`);
111
+ errorWithSuggestions(`Rollback failed: ${err.message}`, [
112
+ 'Check your internet connection',
113
+ 'Verify the subdomain and version exist',
114
+ 'Run "launchpd versions <subdomain>" to see available versions',
115
+ ], { verbose, cause: err });
99
116
  process.exit(1);
100
117
  }
101
118
  }
@@ -1,16 +1,20 @@
1
1
  import { getVersionsForSubdomain, getActiveVersion } from '../utils/metadata.js';
2
2
  import { getVersions as getVersionsFromAPI } from '../utils/api.js';
3
- import { success, error, info } from '../utils/logger.js';
3
+ import { success, errorWithSuggestions, info, spinner, formatSize } from '../utils/logger.js';
4
+ import chalk from 'chalk';
4
5
 
5
6
  /**
6
7
  * List all versions for a subdomain
7
8
  * @param {string} subdomain - Subdomain to list versions for
8
9
  * @param {object} options - Command options
9
10
  * @param {boolean} options.json - Output as JSON
11
+ * @param {boolean} options.verbose - Show verbose error details
10
12
  */
11
13
  export async function versions(subdomain, options) {
14
+ const verbose = options.verbose || false;
15
+
12
16
  try {
13
- info(`Fetching versions for ${subdomain}...`);
17
+ const fetchSpinner = spinner(`Fetching versions for ${subdomain}...`);
14
18
 
15
19
  let versionList = [];
16
20
  let activeVersion = 1;
@@ -32,10 +36,17 @@ export async function versions(subdomain, options) {
32
36
  }
33
37
 
34
38
  if (versionList.length === 0) {
35
- error(`No deployments found for subdomain: ${subdomain}`);
39
+ fetchSpinner.fail(`No deployments found for: ${subdomain}`);
40
+ errorWithSuggestions(`No deployments found for subdomain: ${subdomain}`, [
41
+ 'Check the subdomain name is correct',
42
+ 'Run "launchpd list" to see your deployments',
43
+ 'Deploy a new site with "launchpd deploy ./folder"',
44
+ ], { verbose });
36
45
  process.exit(1);
37
46
  }
38
47
 
48
+ fetchSpinner.succeed(`Found ${versionList.length} version(s)`);
49
+
39
50
  if (options.json) {
40
51
  console.log(JSON.stringify({
41
52
  subdomain,
@@ -52,24 +63,37 @@ export async function versions(subdomain, options) {
52
63
  }
53
64
 
54
65
  console.log('');
55
- success(`Versions for ${subdomain}.launchpd.cloud:`);
66
+ success(`Versions for ${chalk.cyan(subdomain)}.launchpd.cloud:`);
56
67
  console.log('');
57
68
 
69
+ // Table header
70
+ console.log(chalk.gray(' Version Date Files Size Status'));
71
+ console.log(chalk.gray(' ' + '─'.repeat(70)));
72
+
58
73
  for (const v of versionList) {
59
74
  const isActive = v.version === activeVersion;
60
- const activeMarker = isActive ? ' ← active' : '';
61
- const sizeKB = v.totalBytes ? `${(v.totalBytes / 1024).toFixed(1)} KB` : 'unknown size';
62
- const date = new Date(v.timestamp).toLocaleString();
75
+ const versionStr = chalk.bold.cyan(`v${v.version}`);
76
+ const date = chalk.gray(new Date(v.timestamp).toLocaleString());
77
+ const files = chalk.white(`${v.fileCount} files`);
78
+ const size = v.totalBytes ? chalk.white(formatSize(v.totalBytes)) : chalk.gray('unknown');
79
+ const status = isActive
80
+ ? chalk.green.bold('● active')
81
+ : chalk.gray('○ inactive');
63
82
 
64
- console.log(` v${v.version}${date}${v.fileCount} files │ ${sizeKB}${activeMarker}`);
83
+ console.log(` ${versionStr.padEnd(18)}${date.padEnd(30)}${files.padEnd(12)}${size.padEnd(14)}${status}`);
65
84
  }
66
85
 
86
+ console.log(chalk.gray(' ' + '─'.repeat(70)));
67
87
  console.log('');
68
- info(`Use 'launchpd rollback ${subdomain} --to <n>' to restore a version.`);
88
+ info(`Use ${chalk.cyan(`launchpd rollback ${subdomain} --to <n>`)} to restore a version.`);
69
89
  console.log('');
70
90
 
71
91
  } catch (err) {
72
- error(`Failed to list versions: ${err.message}`);
92
+ errorWithSuggestions(`Failed to list versions: ${err.message}`, [
93
+ 'Check your internet connection',
94
+ 'Verify the subdomain exists',
95
+ 'Try running with --verbose for more details',
96
+ ], { verbose, cause: err });
73
97
  process.exit(1);
74
98
  }
75
99
  }
package/src/config.js CHANGED
@@ -1,36 +1,14 @@
1
- import 'dotenv/config';
2
-
3
1
  /**
4
- * Application configuration loaded from environment variables
2
+ * Application configuration for Launchpd CLI
3
+ * No credentials needed - uploads go through the API proxy
5
4
  */
6
5
  export const config = {
7
- r2: {
8
- accountId: process.env.R2_ACCOUNT_ID || '',
9
- accessKeyId: process.env.R2_ACCESS_KEY_ID || '',
10
- secretAccessKey: process.env.R2_SECRET_ACCESS_KEY || '',
11
- bucketName: process.env.R2_BUCKET_NAME || 'launchpd',
12
- },
13
-
14
6
  // Base domain for deployments
15
- domain: process.env.STATICLAUNCH_DOMAIN || 'launchpd.cloud',
16
- };
17
-
18
- /**
19
- * Validate required configuration
20
- * @returns {boolean} true if all required config is present
21
- */
22
- export function validateConfig() {
23
- const required = [
24
- 'R2_ACCOUNT_ID',
25
- 'R2_ACCESS_KEY_ID',
26
- 'R2_SECRET_ACCESS_KEY',
27
- ];
7
+ domain: 'launchpd.cloud',
28
8
 
29
- const missing = required.filter(key => !process.env[key]);
9
+ // API endpoint
10
+ apiUrl: 'https://api.launchpd.cloud',
30
11
 
31
- if (missing.length > 0) {
32
- return { valid: false, missing };
33
- }
34
-
35
- return { valid: true, missing: [] };
36
- }
12
+ // CLI version
13
+ version: '0.1.1',
14
+ };