@scrymore/scry-deployer 0.0.2
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/LICENSE +21 -0
- package/README.md +889 -0
- package/bin/cli.js +351 -0
- package/lib/analysis.js +445 -0
- package/lib/apiClient.js +130 -0
- package/lib/archive.js +31 -0
- package/lib/archiveUtils.js +95 -0
- package/lib/config-store.js +47 -0
- package/lib/config.js +217 -0
- package/lib/errors.js +49 -0
- package/lib/init.js +478 -0
- package/lib/logger.js +48 -0
- package/lib/screencap.js +55 -0
- package/lib/templates.js +226 -0
- package/package.json +61 -0
- package/scripts/postinstall.js +7 -0
package/lib/init.js
ADDED
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const { createLogger } = require('./logger');
|
|
5
|
+
const { getApiClient } = require('./apiClient');
|
|
6
|
+
const { generateMainWorkflow, generatePRWorkflow } = require('./templates');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Run the initialization wizard
|
|
10
|
+
*/
|
|
11
|
+
async function runInit(argv) {
|
|
12
|
+
const logger = createLogger({ verbose: true });
|
|
13
|
+
|
|
14
|
+
logger.info('🚀 Scry Storybook Deployer - Setup Wizard\n');
|
|
15
|
+
logger.info('━'.repeat(50) + '\n');
|
|
16
|
+
|
|
17
|
+
const totalStartTime = Date.now();
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// Step 1: Validate credentials
|
|
21
|
+
const step1Start = Date.now();
|
|
22
|
+
logger.info('1/8: Validating credentials...');
|
|
23
|
+
await validateCredentials(argv.apiUrl, argv.apiKey, argv.project);
|
|
24
|
+
const step1Duration = Date.now() - step1Start;
|
|
25
|
+
logger.success(`✅ Credentials validated [${step1Duration}ms]\n`);
|
|
26
|
+
|
|
27
|
+
// Step 2: Check prerequisites
|
|
28
|
+
const step2Start = Date.now();
|
|
29
|
+
logger.info('2/8: Checking prerequisites...');
|
|
30
|
+
const envInfo = await checkEnvironment();
|
|
31
|
+
const step2Duration = Date.now() - step2Start;
|
|
32
|
+
|
|
33
|
+
if (!envInfo.isGit) {
|
|
34
|
+
throw new Error('Not a git repository. Please run "git init" first.');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!envInfo.githubRemote) {
|
|
38
|
+
logger.error('⚠️ Warning: No GitHub remote found. You\'ll need to add one before pushing.');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
logger.success(`✅ Environment detected [${step2Duration}ms]:
|
|
42
|
+
• Git: ${envInfo.isGit ? '✓' : '✗'}
|
|
43
|
+
• GitHub: ${envInfo.githubRemote || 'Not configured'}
|
|
44
|
+
• Package Manager: ${envInfo.packageManager}
|
|
45
|
+
• Build Command: ${envInfo.storybookBuildCmd || 'build-storybook'}\n`);
|
|
46
|
+
|
|
47
|
+
// Step 3: Create config file
|
|
48
|
+
const step3Start = Date.now();
|
|
49
|
+
logger.info('3/8: Creating configuration file...');
|
|
50
|
+
createConfigFile(argv.project, argv.apiKey, argv.apiUrl, envInfo);
|
|
51
|
+
const step3Duration = Date.now() - step3Start;
|
|
52
|
+
logger.success(`✅ Created .storybook-deployer.json [${step3Duration}ms]\n`);
|
|
53
|
+
|
|
54
|
+
// Step 4: Add to gitignore (optional - keep API key out of git if user prefers)
|
|
55
|
+
const step4Start = Date.now();
|
|
56
|
+
if (!argv.commitApiKey) {
|
|
57
|
+
logger.info('4/8: Updating .gitignore...');
|
|
58
|
+
updateGitignore();
|
|
59
|
+
const step4Duration = Date.now() - step4Start;
|
|
60
|
+
logger.success(`✅ Updated .gitignore (API key will use env vars in CI) [${step4Duration}ms]\n`);
|
|
61
|
+
} else {
|
|
62
|
+
logger.info('4/8: Skipping .gitignore update (--commit-api-key flag set)\n');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Step 5: Generate workflow files
|
|
66
|
+
const step5Start = Date.now();
|
|
67
|
+
logger.info('5/8: Generating GitHub Actions workflows...');
|
|
68
|
+
const workflowFiles = generateWorkflows(argv.project, argv.apiUrl, envInfo);
|
|
69
|
+
const step5Duration = Date.now() - step5Start;
|
|
70
|
+
logger.success(`✅ Created .github/workflows/deploy-storybook.yml [${step5Duration}ms]`);
|
|
71
|
+
logger.success('✅ Created .github/workflows/deploy-pr-preview.yml\n');
|
|
72
|
+
|
|
73
|
+
// Step 6: Setup GitHub variables (if gh CLI available)
|
|
74
|
+
const step6Start = Date.now();
|
|
75
|
+
if (!argv.skipGhSetup && isGhCliAvailable()) {
|
|
76
|
+
logger.info('6/8: Setting up GitHub repository variables...');
|
|
77
|
+
try {
|
|
78
|
+
await setupGitHubVariables(argv.project, argv.apiKey, argv.apiUrl, logger);
|
|
79
|
+
const step6Duration = Date.now() - step6Start;
|
|
80
|
+
logger.success(`✅ GitHub variables configured [${step6Duration}ms]\n`);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
const step6Duration = Date.now() - step6Start;
|
|
83
|
+
logger.error(`⚠️ GitHub setup failed: ${error.message} [${step6Duration}ms]`);
|
|
84
|
+
logger.info('You can set these up manually later.\n');
|
|
85
|
+
showManualSetupInstructions(argv.project, argv.apiKey, argv.apiUrl);
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
logger.info('6/8: Skipping GitHub CLI setup\n');
|
|
89
|
+
if (!argv.skipGhSetup) {
|
|
90
|
+
showManualSetupInstructions(argv.project, argv.apiKey, argv.apiUrl);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Step 7: Git commit
|
|
95
|
+
const step7Start = Date.now();
|
|
96
|
+
logger.info('7/8: Committing changes...');
|
|
97
|
+
const commitResult = gitCommit(argv.commitApiKey, logger);
|
|
98
|
+
const step7Duration = Date.now() - step7Start;
|
|
99
|
+
if (commitResult.success) {
|
|
100
|
+
logger.success(`✅ Changes committed: ${commitResult.sha} [${step7Duration}ms]\n`);
|
|
101
|
+
} else {
|
|
102
|
+
logger.error(`⚠️ Could not commit automatically. Please commit manually. [${step7Duration}ms]\n`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Step 8: Git push
|
|
106
|
+
const step8Start = Date.now();
|
|
107
|
+
logger.info('8/8: Pushing to GitHub...');
|
|
108
|
+
const pushResult = gitPush(logger);
|
|
109
|
+
const step8Duration = Date.now() - step8Start;
|
|
110
|
+
if (pushResult.success) {
|
|
111
|
+
logger.success(`✅ Pushed to ${pushResult.branch} [${step8Duration}ms]\n`);
|
|
112
|
+
} else {
|
|
113
|
+
logger.error(`⚠️ Could not push automatically. Please push manually with:\n git push [${step8Duration}ms]\n`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Calculate total time
|
|
117
|
+
const totalDuration = Date.now() - totalStartTime;
|
|
118
|
+
logger.info('━'.repeat(50));
|
|
119
|
+
logger.info(`[TIMING] Total setup time: ${totalDuration}ms (${(totalDuration / 1000).toFixed(2)}s)\n`);
|
|
120
|
+
|
|
121
|
+
showSuccessMessage(argv.project, envInfo, argv.apiUrl, pushResult.success);
|
|
122
|
+
|
|
123
|
+
} catch (error) {
|
|
124
|
+
logger.error(`\n❌ Setup failed: ${error.message}`);
|
|
125
|
+
if (error.response) {
|
|
126
|
+
logger.error(`API Error: ${error.response.status} - ${error.response.data || error.response.statusText}`);
|
|
127
|
+
}
|
|
128
|
+
if (error.stack && argv.verbose) {
|
|
129
|
+
logger.debug(error.stack);
|
|
130
|
+
}
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Validate that the API credentials work
|
|
137
|
+
*/
|
|
138
|
+
async function validateCredentials(apiUrl, apiKey, projectId) {
|
|
139
|
+
// For now, just verify the parameters are provided
|
|
140
|
+
// In the future, we could ping a /validate endpoint
|
|
141
|
+
if (!projectId || !apiKey) {
|
|
142
|
+
throw new Error('Both --project-id and --api-key are required');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Basic format validation
|
|
146
|
+
if (projectId.length < 3) {
|
|
147
|
+
throw new Error('Project ID seems too short. Please check your credentials.');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Could add API validation call here if endpoint exists
|
|
151
|
+
// const apiClient = getApiClient(apiUrl, apiKey);
|
|
152
|
+
// await apiClient.get('/validate');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Check the local environment and detect settings
|
|
157
|
+
*/
|
|
158
|
+
async function checkEnvironment() {
|
|
159
|
+
const envInfo = {
|
|
160
|
+
isGit: false,
|
|
161
|
+
githubRemote: null,
|
|
162
|
+
githubRepo: null,
|
|
163
|
+
packageManager: 'npm',
|
|
164
|
+
storybookBuildCmd: null,
|
|
165
|
+
currentBranch: null
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// Check if git repo
|
|
169
|
+
try {
|
|
170
|
+
execSync('git rev-parse --git-dir', { stdio: 'ignore' });
|
|
171
|
+
envInfo.isGit = true;
|
|
172
|
+
|
|
173
|
+
// Get current branch
|
|
174
|
+
try {
|
|
175
|
+
envInfo.currentBranch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim();
|
|
176
|
+
} catch (e) {
|
|
177
|
+
// Ignore
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Get GitHub remote
|
|
181
|
+
try {
|
|
182
|
+
const remote = execSync('git remote get-url origin', { encoding: 'utf8' }).trim();
|
|
183
|
+
envInfo.githubRemote = remote;
|
|
184
|
+
envInfo.githubRepo = parseGitHubRemote(remote);
|
|
185
|
+
} catch (e) {
|
|
186
|
+
// No remote configured
|
|
187
|
+
}
|
|
188
|
+
} catch (e) {
|
|
189
|
+
// Not a git repo
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Detect package manager
|
|
193
|
+
if (fs.existsSync('pnpm-lock.yaml')) {
|
|
194
|
+
envInfo.packageManager = 'pnpm';
|
|
195
|
+
} else if (fs.existsSync('yarn.lock')) {
|
|
196
|
+
envInfo.packageManager = 'yarn';
|
|
197
|
+
} else if (fs.existsSync('bun.lockb')) {
|
|
198
|
+
envInfo.packageManager = 'bun';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Detect Storybook build command from package.json
|
|
202
|
+
try {
|
|
203
|
+
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
|
204
|
+
if (pkg.scripts) {
|
|
205
|
+
if (pkg.scripts['build-storybook']) {
|
|
206
|
+
envInfo.storybookBuildCmd = 'build-storybook';
|
|
207
|
+
} else if (pkg.scripts['storybook:build']) {
|
|
208
|
+
envInfo.storybookBuildCmd = 'storybook:build';
|
|
209
|
+
} else if (pkg.scripts['build:storybook']) {
|
|
210
|
+
envInfo.storybookBuildCmd = 'build:storybook';
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
} catch (e) {
|
|
214
|
+
// No package.json or can't read
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return envInfo;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Parse GitHub remote URL to extract owner/repo
|
|
222
|
+
*/
|
|
223
|
+
function parseGitHubRemote(remote) {
|
|
224
|
+
// Handle both HTTPS and SSH formats
|
|
225
|
+
// HTTPS: https://github.com/owner/repo.git
|
|
226
|
+
// SSH: git@github.com:owner/repo.git
|
|
227
|
+
|
|
228
|
+
const httpsMatch = remote.match(/github\.com[/:]([\w-]+)\/([\w-]+)/);
|
|
229
|
+
if (httpsMatch) {
|
|
230
|
+
return `${httpsMatch[1]}/${httpsMatch[2].replace('.git', '')}`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Create the configuration file
|
|
238
|
+
*/
|
|
239
|
+
function createConfigFile(projectId, apiKey, apiUrl, envInfo) {
|
|
240
|
+
const config = {
|
|
241
|
+
apiUrl: apiUrl,
|
|
242
|
+
project: projectId,
|
|
243
|
+
dir: "./storybook-static",
|
|
244
|
+
version: "latest",
|
|
245
|
+
verbose: false
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// Only include apiKey in config if user wants it committed
|
|
249
|
+
// Otherwise it should be set via environment variable
|
|
250
|
+
// For now, we'll include it but note in .gitignore
|
|
251
|
+
config.apiKey = apiKey;
|
|
252
|
+
|
|
253
|
+
const configPath = '.storybook-deployer.json';
|
|
254
|
+
fs.writeFileSync(
|
|
255
|
+
configPath,
|
|
256
|
+
JSON.stringify(config, null, 2) + '\n',
|
|
257
|
+
'utf8'
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Update .gitignore to exclude sensitive files (optional)
|
|
263
|
+
*/
|
|
264
|
+
function updateGitignore() {
|
|
265
|
+
const gitignorePath = '.gitignore';
|
|
266
|
+
const entries = [
|
|
267
|
+
'# Scry Storybook Deployer',
|
|
268
|
+
'.storybook-deployer.json # Contains API key'
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
let gitignoreContent = '';
|
|
272
|
+
if (fs.existsSync(gitignorePath)) {
|
|
273
|
+
gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Check if already added
|
|
277
|
+
if (!gitignoreContent.includes('.storybook-deployer.json')) {
|
|
278
|
+
gitignoreContent += '\n' + entries.join('\n') + '\n';
|
|
279
|
+
fs.writeFileSync(gitignorePath, gitignoreContent, 'utf8');
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Generate workflow files
|
|
285
|
+
*/
|
|
286
|
+
function generateWorkflows(projectId, apiUrl, envInfo) {
|
|
287
|
+
// Create .github/workflows directory
|
|
288
|
+
const workflowsDir = '.github/workflows';
|
|
289
|
+
fs.mkdirSync(workflowsDir, { recursive: true });
|
|
290
|
+
|
|
291
|
+
const buildCmd = envInfo.storybookBuildCmd || 'build-storybook';
|
|
292
|
+
|
|
293
|
+
// Generate main deployment workflow
|
|
294
|
+
const mainWorkflow = generateMainWorkflow(projectId, apiUrl, envInfo.packageManager, buildCmd);
|
|
295
|
+
const mainWorkflowPath = path.join(workflowsDir, 'deploy-storybook.yml');
|
|
296
|
+
fs.writeFileSync(mainWorkflowPath, mainWorkflow, 'utf8');
|
|
297
|
+
|
|
298
|
+
// Generate PR preview workflow
|
|
299
|
+
const prWorkflow = generatePRWorkflow(projectId, apiUrl, envInfo.packageManager, buildCmd);
|
|
300
|
+
const prWorkflowPath = path.join(workflowsDir, 'deploy-pr-preview.yml');
|
|
301
|
+
fs.writeFileSync(prWorkflowPath, prWorkflow, 'utf8');
|
|
302
|
+
|
|
303
|
+
return [mainWorkflowPath, prWorkflowPath];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Check if GitHub CLI is available
|
|
308
|
+
*/
|
|
309
|
+
function isGhCliAvailable() {
|
|
310
|
+
try {
|
|
311
|
+
execSync('gh --version', { stdio: 'ignore' });
|
|
312
|
+
return true;
|
|
313
|
+
} catch (e) {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Setup GitHub variables using gh CLI
|
|
320
|
+
*/
|
|
321
|
+
async function setupGitHubVariables(projectId, apiKey, apiUrl, logger) {
|
|
322
|
+
try {
|
|
323
|
+
// Set variables
|
|
324
|
+
execSync(`gh variable set SCRY_PROJECT_ID --body "${projectId}"`, { stdio: 'pipe' });
|
|
325
|
+
logger.debug(' ✓ Set SCRY_PROJECT_ID');
|
|
326
|
+
|
|
327
|
+
execSync(`gh variable set SCRY_API_URL --body "${apiUrl}"`, { stdio: 'pipe' });
|
|
328
|
+
logger.debug(' ✓ Set SCRY_API_URL');
|
|
329
|
+
|
|
330
|
+
// Set secret (API key)
|
|
331
|
+
execSync(`gh secret set SCRY_API_KEY --body "${apiKey}"`, { stdio: 'pipe' });
|
|
332
|
+
logger.debug(' ✓ Set SCRY_API_KEY');
|
|
333
|
+
|
|
334
|
+
} catch (error) {
|
|
335
|
+
throw new Error(`Failed to set GitHub variables: ${error.message}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Show manual setup instructions
|
|
341
|
+
*/
|
|
342
|
+
function showManualSetupInstructions(projectId, apiKey, apiUrl) {
|
|
343
|
+
console.log(`
|
|
344
|
+
📋 Manual GitHub Setup (Optional):
|
|
345
|
+
|
|
346
|
+
If you haven't already, set up these repository variables:
|
|
347
|
+
|
|
348
|
+
1. Go to your GitHub repository Settings
|
|
349
|
+
2. Navigate to: Settings → Secrets and variables → Actions
|
|
350
|
+
|
|
351
|
+
3. Add these Variables (Variables tab):
|
|
352
|
+
• SCRY_PROJECT_ID = ${projectId}
|
|
353
|
+
• SCRY_API_URL = ${apiUrl}
|
|
354
|
+
|
|
355
|
+
4. Add this Secret (Secrets tab):
|
|
356
|
+
• SCRY_API_KEY = ${apiKey}
|
|
357
|
+
|
|
358
|
+
Or install GitHub CLI and run:
|
|
359
|
+
gh variable set SCRY_PROJECT_ID --body "${projectId}"
|
|
360
|
+
gh variable set SCRY_API_URL --body "${apiUrl}"
|
|
361
|
+
gh secret set SCRY_API_KEY --body "${apiKey}"
|
|
362
|
+
`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Commit the changes to git
|
|
367
|
+
*/
|
|
368
|
+
function gitCommit(commitApiKey, logger) {
|
|
369
|
+
try {
|
|
370
|
+
// Check if there are changes to commit
|
|
371
|
+
const status = execSync('git status --porcelain', { encoding: 'utf8' });
|
|
372
|
+
if (!status.trim()) {
|
|
373
|
+
return { success: false, message: 'No changes to commit' };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Add files
|
|
377
|
+
const filesToAdd = [
|
|
378
|
+
'.github/workflows/deploy-storybook.yml',
|
|
379
|
+
'.github/workflows/deploy-pr-preview.yml',
|
|
380
|
+
'.storybook-deployer.json'
|
|
381
|
+
];
|
|
382
|
+
|
|
383
|
+
if (!commitApiKey) {
|
|
384
|
+
filesToAdd.push('.gitignore');
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
for (const file of filesToAdd) {
|
|
388
|
+
if (fs.existsSync(file)) {
|
|
389
|
+
execSync(`git add "${file}"`, { stdio: 'pipe' });
|
|
390
|
+
logger.debug(` ✓ Added ${file}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Commit
|
|
395
|
+
const commitMessage = 'chore: add Scry Storybook deployment workflows';
|
|
396
|
+
execSync(`git commit -m "${commitMessage}"`, { stdio: 'pipe' });
|
|
397
|
+
|
|
398
|
+
// Get commit SHA
|
|
399
|
+
const sha = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim();
|
|
400
|
+
|
|
401
|
+
return { success: true, sha };
|
|
402
|
+
} catch (error) {
|
|
403
|
+
return { success: false, error: error.message };
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Push changes to remote
|
|
409
|
+
*/
|
|
410
|
+
function gitPush(logger) {
|
|
411
|
+
try {
|
|
412
|
+
// Get current branch
|
|
413
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim();
|
|
414
|
+
|
|
415
|
+
// Check if remote is configured
|
|
416
|
+
try {
|
|
417
|
+
execSync('git remote get-url origin', { stdio: 'ignore' });
|
|
418
|
+
} catch (e) {
|
|
419
|
+
return { success: false, error: 'No remote configured', branch };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Push
|
|
423
|
+
execSync(`git push -u origin ${branch}`, { stdio: 'pipe' });
|
|
424
|
+
|
|
425
|
+
return { success: true, branch };
|
|
426
|
+
} catch (error) {
|
|
427
|
+
// Check if it's an authentication error
|
|
428
|
+
if (error.message.includes('Authentication') || error.message.includes('permission')) {
|
|
429
|
+
return {
|
|
430
|
+
success: false,
|
|
431
|
+
error: 'Authentication failed. Please check your git credentials.',
|
|
432
|
+
branch: null
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return { success: false, error: error.message, branch: null };
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Show success message
|
|
442
|
+
*/
|
|
443
|
+
function showSuccessMessage(projectId, envInfo, apiUrl, pushed) {
|
|
444
|
+
console.log(`
|
|
445
|
+
🎉 ${pushed ? 'Setup Complete and Deployed!' : 'Setup Complete!'}
|
|
446
|
+
|
|
447
|
+
Your Storybook deployment is configured and ready to go.
|
|
448
|
+
|
|
449
|
+
📦 What was set up:
|
|
450
|
+
✅ Configuration file (.storybook-deployer.json)
|
|
451
|
+
✅ GitHub Actions workflows (.github/workflows/)
|
|
452
|
+
✅ Repository variables (SCRY_PROJECT_ID, SCRY_API_URL)
|
|
453
|
+
✅ Repository secret (SCRY_API_KEY)
|
|
454
|
+
${pushed ? '✅ Changes committed and pushed' : '⚠️ Manual push required'}
|
|
455
|
+
|
|
456
|
+
${!pushed ? `
|
|
457
|
+
📌 Next Step:
|
|
458
|
+
Push your changes to GitHub:
|
|
459
|
+
git push
|
|
460
|
+
` : ''}
|
|
461
|
+
|
|
462
|
+
🚀 Deployment:
|
|
463
|
+
Your Storybook will deploy automatically on:
|
|
464
|
+
• Every push to ${envInfo.currentBranch || 'main'} branch
|
|
465
|
+
• Every pull request (as a preview)
|
|
466
|
+
|
|
467
|
+
🌐 Deployment URLs:
|
|
468
|
+
• Production: ${apiUrl}/${projectId}/latest
|
|
469
|
+
• PR Previews: ${apiUrl}/${projectId}/pr-{number}
|
|
470
|
+
|
|
471
|
+
📖 Learn more: https://github.com/epinnock/scry-node
|
|
472
|
+
💬 Need help? Open an issue on GitHub
|
|
473
|
+
|
|
474
|
+
Happy deploying! ✨
|
|
475
|
+
`);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
module.exports = { runInit };
|
package/lib/logger.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a logger instance.
|
|
5
|
+
* The logger's behavior is controlled by the arguments passed to the CLI.
|
|
6
|
+
* @param {object} argv The arguments object from yargs.
|
|
7
|
+
* @param {boolean} argv.verbose Whether to enable verbose (debug) logging.
|
|
8
|
+
* @returns {{info: Function, error: Function, debug: Function, success: Function}}
|
|
9
|
+
*/
|
|
10
|
+
function createLogger({ verbose = false }) {
|
|
11
|
+
return {
|
|
12
|
+
/**
|
|
13
|
+
* Logs an informational message.
|
|
14
|
+
* @param {string} message The message to log.
|
|
15
|
+
*/
|
|
16
|
+
info: (message) => {
|
|
17
|
+
console.log(message);
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Logs a success message, typically at the end of a process.
|
|
22
|
+
* @param {string} message The message to log.
|
|
23
|
+
*/
|
|
24
|
+
success: (message) => {
|
|
25
|
+
console.log(chalk.green(message));
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Logs an error message.
|
|
30
|
+
* @param {string} message The message to log.
|
|
31
|
+
*/
|
|
32
|
+
error: (message) => {
|
|
33
|
+
console.error(chalk.red(message));
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Logs a debug message. Only logs if verbose mode is enabled.
|
|
38
|
+
* @param {string} message The message to log.
|
|
39
|
+
*/
|
|
40
|
+
debug: (message) => {
|
|
41
|
+
if (verbose) {
|
|
42
|
+
console.log(chalk.dim(`[debug] ${message}`));
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { createLogger };
|
package/lib/screencap.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const { execSync } = require('child_process');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Captures screenshots from a Storybook URL using storycap
|
|
5
|
+
* @param {string} storybookUrl - URL of the deployed Storybook
|
|
6
|
+
* @param {Object} options - Storycap options
|
|
7
|
+
* @param {string} options.chromiumPath - Path to Chromium executable (optional)
|
|
8
|
+
* @param {string} options.outDir - Output directory for screenshots (default: ./__screenshots__)
|
|
9
|
+
* @param {number} options.parallel - Number of parallel browser instances (optional)
|
|
10
|
+
* @param {number} options.delay - Delay between screenshots in ms (optional)
|
|
11
|
+
* @param {string} options.include - Include stories matching pattern (optional)
|
|
12
|
+
* @param {string} options.exclude - Exclude stories matching pattern (optional)
|
|
13
|
+
* @param {boolean} options.omitBackground - Omit background (default: true)
|
|
14
|
+
* @returns {Promise<void>}
|
|
15
|
+
*/
|
|
16
|
+
async function captureScreenshots(storybookUrl, options = {}) {
|
|
17
|
+
// Build storycap command
|
|
18
|
+
let command = `npx storycap "${storybookUrl}"`;
|
|
19
|
+
|
|
20
|
+
if (options.chromiumPath) {
|
|
21
|
+
command += ` --chromiumPath "${options.chromiumPath}"`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (options.omitBackground !== false) {
|
|
25
|
+
command += ` --omitBackground true`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (options.outDir) {
|
|
29
|
+
command += ` --outDir "${options.outDir}"`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (options.parallel) {
|
|
33
|
+
command += ` --parallel ${options.parallel}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (options.delay) {
|
|
37
|
+
command += ` --delay ${options.delay}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (options.include) {
|
|
41
|
+
command += ` --include "${options.include}"`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (options.exclude) {
|
|
45
|
+
command += ` --exclude "${options.exclude}"`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
execSync(command, { stdio: 'inherit' });
|
|
50
|
+
} catch (error) {
|
|
51
|
+
throw new Error(`Failed to capture screenshots: ${error.message}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { captureScreenshots };
|