@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/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();
|