@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/bin/cli.js ADDED
@@ -0,0 +1,351 @@
1
+ #!/usr/bin/env node
2
+
3
+ const yargs = require('yargs/yargs');
4
+ const { hideBin } = require('yargs/helpers');
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+ const { zipDirectory } = require('../lib/archive.js');
9
+ const { createMasterZip } = require('../lib/archiveUtils.js');
10
+ const { getApiClient, uploadFileDirectly } = require('../lib/apiClient.js');
11
+ const { createLogger } = require('../lib/logger.js');
12
+ const { AppError, ApiError } = require('../lib/errors.js');
13
+ const { loadConfig } = require('../lib/config.js');
14
+ const { captureScreenshots } = require('../lib/screencap.js');
15
+ const { analyzeStorybook } = require('../lib/analysis.js');
16
+ const { runInit } = require('../lib/init.js');
17
+
18
+ async function runAnalysis(argv) {
19
+ const logger = createLogger(argv);
20
+ logger.info('๐Ÿ“Š Starting Storybook analysis...');
21
+ logger.debug(`Received arguments: ${JSON.stringify(argv)}`);
22
+
23
+ const outPath = path.join(os.tmpdir(), `storybook-analysis-${Date.now()}.zip`);
24
+
25
+ try {
26
+ // 1. Capture screenshots if storybook URL provided
27
+ if (argv.storybookUrl) {
28
+ logger.info(`1/4: Capturing screenshots from '${argv.storybookUrl}'...`);
29
+ await captureScreenshots(argv.storybookUrl, argv.storycapOptions || {});
30
+ logger.success('โœ… Screenshots captured');
31
+ } else {
32
+ logger.info('1/4: Skipping screenshot capture (no Storybook URL provided)');
33
+ }
34
+
35
+ // 2. Analyze stories and map screenshots
36
+ logger.info('2/4: Analyzing stories and mapping screenshots...');
37
+ const analysisResults = analyzeStorybook({
38
+ storiesDir: argv.storiesDir,
39
+ screenshotsDir: argv.screenshotsDir,
40
+ project: argv.project,
41
+ version: argv.version
42
+ });
43
+ logger.success(`โœ… Found ${analysisResults.summary.totalStories} stories (${analysisResults.summary.withScreenshots} with screenshots)`);
44
+ logger.debug(`Analysis complete: ${JSON.stringify(analysisResults.summary)}`);
45
+
46
+ // 3. Create master ZIP
47
+ logger.info('3/4: Creating master archive...');
48
+ await createMasterZip({
49
+ outPath: outPath,
50
+ staticsiteDir: null, // No static site for analyze-only
51
+ screenshotsDir: argv.screenshotsDir,
52
+ metadata: analysisResults
53
+ });
54
+ logger.success(`โœ… Master archive created: ${outPath}`);
55
+ logger.debug(`Archive size: ${fs.statSync(outPath).size} bytes`);
56
+
57
+ // 4. Upload archive
58
+ logger.info('4/4: Uploading to deployment service...');
59
+ const apiClient = getApiClient(argv.apiUrl, argv.apiKey);
60
+ const uploadResult = await uploadFileDirectly(apiClient, {
61
+ project: argv.project,
62
+ version: argv.version,
63
+ }, outPath);
64
+ logger.success('โœ… Archive uploaded.');
65
+ logger.debug(`Upload result: ${JSON.stringify(uploadResult)}`);
66
+
67
+ logger.success('\n๐ŸŽ‰ Analysis complete! ๐ŸŽ‰');
68
+
69
+ } finally {
70
+ // Clean up the local archive
71
+ if (fs.existsSync(outPath)) {
72
+ fs.unlinkSync(outPath);
73
+ logger.info(`๐Ÿงน Cleaned up temporary file: ${outPath}`);
74
+ }
75
+ }
76
+ }
77
+
78
+ async function runDeployment(argv) {
79
+ const logger = createLogger(argv);
80
+ logger.info('๐Ÿš€ Starting deployment...');
81
+ logger.debug(`Received arguments: ${JSON.stringify(argv)}`);
82
+
83
+ const outPath = path.join(os.tmpdir(), `storybook-deployment-${Date.now()}.zip`);
84
+
85
+ try {
86
+ if (argv.withAnalysis) {
87
+ // Full deployment with analysis
88
+ logger.info('Running deployment with analysis...');
89
+
90
+ // 1. Capture screenshots if storybook URL provided
91
+ if (argv.storybookUrl) {
92
+ logger.info(`1/5: Capturing screenshots from '${argv.storybookUrl}'...`);
93
+ await captureScreenshots(argv.storybookUrl, argv.storycapOptions || {});
94
+ logger.success('โœ… Screenshots captured');
95
+ } else {
96
+ logger.info('1/5: Skipping screenshot capture (no Storybook URL provided)');
97
+ }
98
+
99
+ // 2. Analyze stories and map screenshots
100
+ logger.info('2/5: Analyzing stories and mapping screenshots...');
101
+ const analysisResults = analyzeStorybook({
102
+ storiesDir: argv.storiesDir,
103
+ screenshotsDir: argv.screenshotsDir,
104
+ project: argv.project,
105
+ version: argv.version
106
+ });
107
+ logger.success(`โœ… Found ${analysisResults.summary.totalStories} stories (${analysisResults.summary.withScreenshots} with screenshots)`);
108
+
109
+ // 3. Create master ZIP with staticsite, images, and metadata
110
+ logger.info('3/5: Creating master archive with static site, images, and metadata...');
111
+ await createMasterZip({
112
+ outPath: outPath,
113
+ staticsiteDir: argv.dir,
114
+ screenshotsDir: argv.screenshotsDir,
115
+ metadata: analysisResults
116
+ });
117
+ logger.success(`โœ… Master archive created: ${outPath}`);
118
+ logger.debug(`Archive size: ${fs.statSync(outPath).size} bytes`);
119
+
120
+ // 4. Upload archive
121
+ logger.info('4/5: Uploading to deployment service...');
122
+ const apiClient = getApiClient(argv.apiUrl, argv.apiKey);
123
+ const uploadResult = await uploadFileDirectly(apiClient, {
124
+ project: argv.project,
125
+ version: argv.version,
126
+ }, outPath);
127
+ logger.success('โœ… Archive uploaded.');
128
+ logger.debug(`Upload result: ${JSON.stringify(uploadResult)}`);
129
+
130
+ logger.success('\n๐ŸŽ‰ Deployment with analysis successful! ๐ŸŽ‰');
131
+
132
+ } else {
133
+ // Simple deployment without analysis
134
+ // 1. Archive the directory
135
+ logger.info(`1/3: Zipping directory '${argv.dir}'...`);
136
+ await zipDirectory(argv.dir, outPath);
137
+ logger.success(`โœ… Archive created: ${outPath}`);
138
+ logger.debug(`Archive size: ${fs.statSync(outPath).size} bytes`);
139
+
140
+ // 2. Authenticate and upload directly
141
+ logger.info('2/3: Uploading to deployment service...');
142
+ const apiClient = getApiClient(argv.apiUrl, argv.apiKey);
143
+ const uploadResult = await uploadFileDirectly(apiClient, {
144
+ project: argv.project,
145
+ version: argv.version,
146
+ }, outPath);
147
+ logger.success('โœ… Archive uploaded.');
148
+ logger.debug(`Upload result: ${JSON.stringify(uploadResult)}`);
149
+
150
+ logger.success('\n๐ŸŽ‰ Deployment successful! ๐ŸŽ‰');
151
+ }
152
+
153
+ } finally {
154
+ // 4. Clean up the local archive
155
+ if (fs.existsSync(outPath)) {
156
+ fs.unlinkSync(outPath);
157
+ logger.info(`๐Ÿงน Cleaned up temporary file: ${outPath}`);
158
+ }
159
+ }
160
+ }
161
+
162
+ function handleError(error, argv) {
163
+ const logger = createLogger(argv || {});
164
+ logger.error(`\nโŒ Error: ${error.message}`);
165
+
166
+ if (error instanceof ApiError) {
167
+ if (error.statusCode === 401) {
168
+ logger.error('Suggestion: Check that your API key is correct and has not expired.');
169
+ } else if (error.statusCode >= 500) {
170
+ logger.error('Suggestion: This seems to be a server-side issue. Please try again later or contact support.');
171
+ }
172
+ }
173
+
174
+ if (argv && argv.verbose && error.stack) {
175
+ logger.debug(error.stack);
176
+ }
177
+
178
+ process.exit(1);
179
+ }
180
+
181
+ async function main() {
182
+ let config;
183
+ try {
184
+ const args = await yargs(hideBin(process.argv))
185
+ .command('$0', 'Deploy Storybook static build', (yargs) => {
186
+ return yargs
187
+ .option('dir', {
188
+ describe: 'Path to the built Storybook directory (e.g., storybook-static)',
189
+ type: 'string',
190
+ })
191
+ .option('api-key', {
192
+ describe: 'API key for the deployment service',
193
+ type: 'string',
194
+ })
195
+ .option('api-url', {
196
+ describe: 'Base URL for the deployment service API',
197
+ type: 'string',
198
+ })
199
+ .option('project', {
200
+ describe: 'Project name/identifier',
201
+ type: 'string',
202
+ })
203
+ .option('deploy-version', {
204
+ alias: 'v',
205
+ describe: 'Version identifier for the deployment',
206
+ type: 'string',
207
+ })
208
+ .option('with-analysis', {
209
+ describe: 'Include Storybook analysis (screenshots, metadata)',
210
+ type: 'boolean',
211
+ })
212
+ .option('storybook-url', {
213
+ describe: 'URL of the Storybook for screenshot capture',
214
+ type: 'string',
215
+ })
216
+ .option('stories-dir', {
217
+ describe: 'Directory containing story files',
218
+ type: 'string',
219
+ })
220
+ .option('screenshots-dir', {
221
+ describe: 'Directory for screenshots',
222
+ type: 'string',
223
+ })
224
+ .option('verbose', {
225
+ describe: 'Enable verbose logging',
226
+ type: 'boolean',
227
+ });
228
+ }, async (argv) => {
229
+ // Load and merge configuration
230
+ config = loadConfig(argv);
231
+
232
+ // Validate required fields
233
+ if (!config.dir) {
234
+ throw new Error('--dir is required. You can provide it via CLI arguments, config file, or environment variables.');
235
+ }
236
+
237
+ // Validate directory exists and is valid
238
+ if (!fs.existsSync(config.dir)) {
239
+ throw new Error(`Directory not found at path: ${config.dir}`);
240
+ }
241
+ if (!fs.lstatSync(config.dir).isDirectory()) {
242
+ throw new Error(`Path is not a directory: ${config.dir}`);
243
+ }
244
+
245
+ await runDeployment(config);
246
+ })
247
+ .command('analyze', 'Analyze Storybook stories and generate metadata', (yargs) => {
248
+ return yargs
249
+ .option('project', {
250
+ describe: 'Project name/identifier',
251
+ type: 'string',
252
+ demandOption: true,
253
+ })
254
+ .option('deploy-version', {
255
+ alias: 'v',
256
+ describe: 'Version identifier',
257
+ type: 'string',
258
+ demandOption: true,
259
+ })
260
+ .option('api-key', {
261
+ describe: 'API key for the deployment service',
262
+ type: 'string',
263
+ })
264
+ .option('api-url', {
265
+ describe: 'Base URL for the deployment service API',
266
+ type: 'string',
267
+ })
268
+ .option('storybook-url', {
269
+ describe: 'URL of the Storybook for screenshot capture',
270
+ type: 'string',
271
+ })
272
+ .option('stories-dir', {
273
+ describe: 'Directory containing story files',
274
+ type: 'string',
275
+ })
276
+ .option('screenshots-dir', {
277
+ describe: 'Directory for screenshots',
278
+ type: 'string',
279
+ })
280
+ .option('verbose', {
281
+ describe: 'Enable verbose logging',
282
+ type: 'boolean',
283
+ });
284
+ }, async (argv) => {
285
+ // Load and merge configuration
286
+ config = loadConfig(argv);
287
+
288
+ await runAnalysis(config);
289
+ })
290
+ .command('init', 'Setup GitHub Actions workflows for automatic deployment', (yargs) => {
291
+ return yargs
292
+ .option('project-id', {
293
+ describe: 'Project ID from Scry dashboard',
294
+ type: 'string',
295
+ demandOption: true,
296
+ alias: 'projectId'
297
+ })
298
+ .option('api-key', {
299
+ describe: 'API key from Scry dashboard',
300
+ type: 'string',
301
+ demandOption: true,
302
+ alias: 'apiKey'
303
+ })
304
+ .option('api-url', {
305
+ describe: 'Scry API URL',
306
+ type: 'string',
307
+ default: 'https://storybook-deployment-service.epinnock.workers.dev',
308
+ alias: 'apiUrl'
309
+ })
310
+ .option('skip-gh-setup', {
311
+ describe: 'Skip GitHub CLI variable setup',
312
+ type: 'boolean',
313
+ default: false,
314
+ alias: 'skipGhSetup'
315
+ })
316
+ .option('commit-api-key', {
317
+ describe: 'Commit API key in config file (not recommended)',
318
+ type: 'boolean',
319
+ default: true,
320
+ alias: 'commitApiKey'
321
+ })
322
+ .option('verbose', {
323
+ describe: 'Enable verbose logging',
324
+ type: 'boolean',
325
+ default: false
326
+ });
327
+ }, async (argv) => {
328
+ // Map projectId/apiKey to project/apiKey for consistency
329
+ const initConfig = {
330
+ project: argv.projectId,
331
+ apiKey: argv.apiKey,
332
+ apiUrl: argv.apiUrl,
333
+ skipGhSetup: argv.skipGhSetup,
334
+ commitApiKey: argv.commitApiKey,
335
+ verbose: argv.verbose
336
+ };
337
+
338
+ await runInit(initConfig);
339
+ })
340
+ .env('STORYBOOK_DEPLOYER')
341
+ .help()
342
+ .alias('help', 'h')
343
+ .version(false) // Disable built-in version since we use -v for deploy-version
344
+ .parse();
345
+
346
+ } catch (error) {
347
+ handleError(error, config);
348
+ }
349
+ }
350
+
351
+ main();