@reshotdev/screenshot 0.0.1-beta.0

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.
Files changed (59) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +388 -0
  3. package/package.json +64 -0
  4. package/src/commands/auth.js +259 -0
  5. package/src/commands/chrome.js +140 -0
  6. package/src/commands/ci-run.js +123 -0
  7. package/src/commands/ci-setup.js +288 -0
  8. package/src/commands/drifts.js +423 -0
  9. package/src/commands/import-tests.js +309 -0
  10. package/src/commands/ingest.js +458 -0
  11. package/src/commands/init.js +633 -0
  12. package/src/commands/publish.js +1721 -0
  13. package/src/commands/pull.js +303 -0
  14. package/src/commands/record.js +94 -0
  15. package/src/commands/run.js +476 -0
  16. package/src/commands/setup-wizard.js +740 -0
  17. package/src/commands/setup.js +137 -0
  18. package/src/commands/status.js +275 -0
  19. package/src/commands/sync.js +621 -0
  20. package/src/commands/ui.js +248 -0
  21. package/src/commands/validate-docs.js +529 -0
  22. package/src/index.js +462 -0
  23. package/src/lib/api-client.js +815 -0
  24. package/src/lib/capture-engine.js +1623 -0
  25. package/src/lib/capture-script-runner.js +3120 -0
  26. package/src/lib/ci-detect.js +137 -0
  27. package/src/lib/config.js +1240 -0
  28. package/src/lib/diff-engine.js +642 -0
  29. package/src/lib/hash.js +74 -0
  30. package/src/lib/image-crop.js +396 -0
  31. package/src/lib/matrix.js +89 -0
  32. package/src/lib/output-path-template.js +318 -0
  33. package/src/lib/playwright-runner.js +252 -0
  34. package/src/lib/polished-clip.js +553 -0
  35. package/src/lib/privacy-engine.js +408 -0
  36. package/src/lib/progress-tracker.js +142 -0
  37. package/src/lib/record-browser-injection.js +654 -0
  38. package/src/lib/record-cdp.js +612 -0
  39. package/src/lib/record-clip.js +343 -0
  40. package/src/lib/record-config.js +623 -0
  41. package/src/lib/record-screenshot.js +360 -0
  42. package/src/lib/record-terminal.js +123 -0
  43. package/src/lib/recorder-service.js +781 -0
  44. package/src/lib/secrets.js +51 -0
  45. package/src/lib/selector-strategies.js +859 -0
  46. package/src/lib/standalone-mode.js +400 -0
  47. package/src/lib/storage-providers.js +569 -0
  48. package/src/lib/style-engine.js +684 -0
  49. package/src/lib/ui-api.js +4677 -0
  50. package/src/lib/ui-assets.js +373 -0
  51. package/src/lib/ui-executor.js +587 -0
  52. package/src/lib/variant-injector.js +591 -0
  53. package/src/lib/viewport-presets.js +454 -0
  54. package/src/lib/worker-pool.js +118 -0
  55. package/web/cropper/index.html +436 -0
  56. package/web/manager/dist/assets/index--ZgioErz.js +507 -0
  57. package/web/manager/dist/assets/index-n468W0Wr.css +1 -0
  58. package/web/manager/dist/index.html +27 -0
  59. package/web/subtitle-editor/index.html +295 -0
@@ -0,0 +1,288 @@
1
+ // ci-setup.js - Set up CI/CD integration
2
+ const inquirer = require('inquirer');
3
+ const chalk = require('chalk');
4
+ const fs = require('fs-extra');
5
+ const path = require('path');
6
+
7
+ const GITHUB_ACTIONS_WORKFLOW = (secretNames) => `name: Reshot Visual Documentation
8
+
9
+ on:
10
+ push:
11
+ branches:
12
+ - main
13
+ - master
14
+
15
+ jobs:
16
+ docs:
17
+ runs-on: ubuntu-latest
18
+
19
+ steps:
20
+ - name: Checkout code
21
+ uses: actions/checkout@v3
22
+
23
+ - name: Set up Node.js
24
+ uses: actions/setup-node@v3
25
+ with:
26
+ node-version: '18'
27
+
28
+ - name: Install dependencies
29
+ run: npm install
30
+
31
+ - name: Install Reshot CLI
32
+ run: npm install -g @reshot/cli
33
+
34
+ - name: Install Playwright browsers
35
+ run: npx playwright install chromium
36
+
37
+ - name: Install ffmpeg
38
+ run: sudo apt-get update && sudo apt-get install -y ffmpeg
39
+
40
+ # Generate visual assets from docsync.config.json blueprint
41
+ # Only runs if features.visuals is enabled for the project
42
+ - name: Run Reshot scenarios
43
+ env:
44
+ RESHOT_API_KEY: \${{ secrets.${secretNames.apiKey} }}
45
+ RESHOT_PROJECT_ID: \${{ secrets.${secretNames.projectId} }}
46
+ RESHOT_API_BASE_URL: \${{ secrets.${secretNames.baseUrl} }}
47
+ TEST_PASSWORD: \${{ secrets.${secretNames.testPassword} }}
48
+ run: reshot run
49
+
50
+ # Publish three streams to Reshot (respects project feature toggles):
51
+ # Stream A: Visual assets (screenshots/videos) - always enabled
52
+ # Stream B: Documentation files (if docs feature enabled + configured)
53
+ # Stream C: Changelog drafts from commit messages (if changelog feature enabled)
54
+ - name: Publish assets to Reshot
55
+ env:
56
+ RESHOT_API_KEY: \${{ secrets.${secretNames.apiKey} }}
57
+ RESHOT_PROJECT_ID: \${{ secrets.${secretNames.projectId} }}
58
+ RESHOT_API_BASE_URL: \${{ secrets.${secretNames.baseUrl} }}
59
+ TEST_PASSWORD: \${{ secrets.${secretNames.testPassword} }}
60
+ run: reshot publish
61
+
62
+ - name: Upload artifacts (optional)
63
+ uses: actions/upload-artifact@v3
64
+ if: always()
65
+ with:
66
+ name: reshot-output
67
+ path: .reshot/output/
68
+ `;
69
+
70
+ const CIRCLECI_CONFIG = (secretNames) => `version: 2.1
71
+
72
+ jobs:
73
+ docs:
74
+ docker:
75
+ - image: cimg/node:18.0-browsers
76
+ environment:
77
+ RESHOT_API_KEY: \${${secretNames.apiKey}}
78
+ RESHOT_PROJECT_ID: \${${secretNames.projectId}}
79
+ RESHOT_API_BASE_URL: \${${secretNames.baseUrl}}
80
+ TEST_PASSWORD: \${${secretNames.testPassword}}
81
+ steps:
82
+ - checkout
83
+
84
+ - run:
85
+ name: Install dependencies
86
+ command: |
87
+ npm install
88
+ npm install -g @reshot/cli
89
+
90
+ - run:
91
+ name: Install Playwright browsers
92
+ command: npx playwright install chromium
93
+
94
+ - run:
95
+ name: Install ffmpeg
96
+ command: sudo apt-get update && sudo apt-get install -y ffmpeg
97
+
98
+ - run:
99
+ name: Run Reshot scenarios
100
+ command: reshot run
101
+
102
+ - run:
103
+ name: Publish assets to Reshot
104
+ command: reshot publish
105
+
106
+ - store_artifacts:
107
+ path: .reshot/output
108
+
109
+ workflows:
110
+ version: 2
111
+ docs:
112
+ jobs:
113
+ - docs
114
+ `;
115
+
116
+ const GITLAB_CI_CONFIG = (secretNames) => `stages:
117
+ - docs
118
+
119
+ docs:
120
+ stage: docs
121
+ image: node:18
122
+
123
+ variables:
124
+ RESHOT_API_KEY: \$${secretNames.apiKey}
125
+ RESHOT_PROJECT_ID: \$${secretNames.projectId}
126
+ RESHOT_API_BASE_URL: \$${secretNames.baseUrl}
127
+ TEST_PASSWORD: \$${secretNames.testPassword}
128
+
129
+ before_script:
130
+ - apt-get update && apt-get install -y ffmpeg
131
+ - npm install
132
+ - npx playwright install chromium
133
+ - npm install -g @reshot/cli
134
+
135
+ script:
136
+ - reshot run
137
+ - reshot publish
138
+
139
+ artifacts:
140
+ paths:
141
+ - .reshot/output/
142
+ expire_in: 1 week
143
+
144
+ only:
145
+ - main
146
+ - master
147
+ `;
148
+
149
+ async function ciSetupCommand() {
150
+ console.log(chalk.cyan('šŸ”§ Setting up CI/CD integration...\n'));
151
+
152
+ // Prompt for CI provider
153
+ const { provider } = await inquirer.prompt([
154
+ {
155
+ type: 'list',
156
+ name: 'provider',
157
+ message: 'Select your CI/CD provider:',
158
+ choices: [
159
+ { name: 'GitHub Actions', value: 'github' },
160
+ { name: 'CircleCI', value: 'circleci' },
161
+ { name: 'GitLab CI', value: 'gitlab' }
162
+ ]
163
+ }
164
+ ]);
165
+
166
+ // Prompt for secret names
167
+ console.log(chalk.cyan('\nEnter the names for your CI secrets:'));
168
+ const { apiKeySecret, projectIdSecret, baseUrlSecret, testPasswordSecret } = await inquirer.prompt([
169
+ {
170
+ type: 'input',
171
+ name: 'apiKeySecret',
172
+ message: 'API Key secret name:',
173
+ default: 'RESHOT_API_KEY'
174
+ },
175
+ {
176
+ type: 'input',
177
+ name: 'projectIdSecret',
178
+ message: 'Project ID secret name:',
179
+ default: 'RESHOT_PROJECT_ID'
180
+ },
181
+ {
182
+ type: 'input',
183
+ name: 'baseUrlSecret',
184
+ message: 'Base URL secret name:',
185
+ default: 'RESHOT_API_BASE_URL'
186
+ },
187
+ {
188
+ type: 'input',
189
+ name: 'testPasswordSecret',
190
+ message: 'Test password secret name:',
191
+ default: 'TEST_PASSWORD'
192
+ }
193
+ ]);
194
+
195
+ const secretNames = {
196
+ apiKey: apiKeySecret,
197
+ projectId: projectIdSecret,
198
+ baseUrl: baseUrlSecret,
199
+ testPassword: testPasswordSecret
200
+ };
201
+
202
+ // Generate workflow file based on provider
203
+ let workflowPath;
204
+ let workflowContent;
205
+
206
+ switch (provider) {
207
+ case 'github':
208
+ workflowPath = path.join(process.cwd(), '.github', 'workflows', 'docsync.yml');
209
+ workflowContent = GITHUB_ACTIONS_WORKFLOW(secretNames);
210
+ break;
211
+
212
+ case 'circleci':
213
+ workflowPath = path.join(process.cwd(), '.circleci', 'config.yml');
214
+ workflowContent = CIRCLECI_CONFIG(secretNames);
215
+ break;
216
+
217
+ case 'gitlab':
218
+ workflowPath = path.join(process.cwd(), '.gitlab-ci.yml');
219
+ workflowContent = GITLAB_CI_CONFIG(secretNames);
220
+ break;
221
+ }
222
+
223
+ // Check if file already exists
224
+ if (fs.existsSync(workflowPath)) {
225
+ const { overwrite } = await inquirer.prompt([
226
+ {
227
+ type: 'confirm',
228
+ name: 'overwrite',
229
+ message: `${workflowPath} already exists. Overwrite?`,
230
+ default: false
231
+ }
232
+ ]);
233
+
234
+ if (!overwrite) {
235
+ console.log(chalk.yellow('\n⚠ Setup cancelled. Existing workflow file was not modified.'));
236
+ return;
237
+ }
238
+ }
239
+
240
+ // Write workflow file
241
+ fs.ensureDirSync(path.dirname(workflowPath));
242
+ fs.writeFileSync(workflowPath, workflowContent);
243
+
244
+ console.log(chalk.green(`\nāœ” Workflow file created: ${workflowPath}`));
245
+
246
+ // Print instructions
247
+ console.log(chalk.cyan('\nšŸ“‹ Next Steps:\n'));
248
+
249
+ switch (provider) {
250
+ case 'github':
251
+ console.log('1. Go to your GitHub repository settings');
252
+ console.log('2. Navigate to ' + chalk.bold('Secrets and variables > Actions'));
253
+ console.log('3. Add the following secrets:\n');
254
+ console.log(` ${chalk.bold(secretNames.apiKey)} (Project API key for CLI uploads)`);
255
+ console.log(` ${chalk.bold(secretNames.projectId)} (Project ID for metadata)`);
256
+ console.log(` ${chalk.bold(secretNames.baseUrl)} (API base URL, e.g. https://api.reshot.dev/api)`);
257
+ console.log(` ${chalk.bold(secretNames.testPassword)} (Any additional test credential you reference)`);
258
+ console.log('\n4. Commit and push the workflow file to trigger the action');
259
+ break;
260
+
261
+ case 'circleci':
262
+ console.log('1. Go to your CircleCI project settings');
263
+ console.log('2. Navigate to ' + chalk.bold('Environment Variables'));
264
+ console.log('3. Add the following variables:\n');
265
+ console.log(` ${chalk.bold(secretNames.apiKey)}`);
266
+ console.log(` ${chalk.bold(secretNames.projectId)}`);
267
+ console.log(` ${chalk.bold(secretNames.baseUrl)}`);
268
+ console.log(` ${chalk.bold(secretNames.testPassword)}`);
269
+ console.log('\n4. Commit and push the config file to trigger the pipeline');
270
+ break;
271
+
272
+ case 'gitlab':
273
+ console.log('1. Go to your GitLab project settings');
274
+ console.log('2. Navigate to ' + chalk.bold('CI/CD > Variables'));
275
+ console.log('3. Add the following variables:\n');
276
+ console.log(` ${chalk.bold(secretNames.apiKey)}`);
277
+ console.log(` ${chalk.bold(secretNames.projectId)}`);
278
+ console.log(` ${chalk.bold(secretNames.baseUrl)}`);
279
+ console.log(` ${chalk.bold(secretNames.testPassword)}`);
280
+ console.log('\n4. Commit and push the config file to trigger the pipeline');
281
+ break;
282
+ }
283
+
284
+ console.log();
285
+ }
286
+
287
+ module.exports = ciSetupCommand;
288
+
@@ -0,0 +1,423 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * drifts - View and resolve documentation drifts
4
+ *
5
+ * Commands:
6
+ * - reshot drifts List pending drifts
7
+ * - reshot drifts list List drifts with filters
8
+ * - reshot drifts show <id> Show drift details
9
+ * - reshot drifts approve <id> Approve a drift
10
+ * - reshot drifts reject <id> Reject a drift
11
+ * - reshot drifts ignore <id> Mark drift as ignored
12
+ * - reshot drifts sync <id> Mark as manually synced (external_host)
13
+ * - reshot drifts approve-all Approve all pending drifts
14
+ * - reshot drifts reject-all Reject all pending drifts
15
+ * - reshot drifts validate Validate journey bindings against visual inventory
16
+ */
17
+
18
+ const chalk = require("chalk");
19
+ const config = require("../lib/config");
20
+ const apiClient = require("../lib/api-client");
21
+
22
+ /**
23
+ * Format drift for display
24
+ */
25
+ function formatDrift(drift, verbose = false) {
26
+ const typeIcon = {
27
+ VISUAL: "šŸ–¼",
28
+ SEMANTIC: "šŸ“",
29
+ BOTH: "⚔",
30
+ }[drift.driftType] || "?";
31
+
32
+ const statusColor = {
33
+ PENDING: chalk.yellow,
34
+ APPROVED: chalk.green,
35
+ REJECTED: chalk.red,
36
+ IGNORED: chalk.gray,
37
+ }[drift.status] || chalk.white;
38
+
39
+ let output = `${typeIcon} ${statusColor(drift.status.padEnd(8))} `;
40
+ output += chalk.white(drift.journeyKey || drift.docPath || "Unknown");
41
+ output += chalk.gray(` (${Math.round(drift.confidenceScore * 100)}%)`);
42
+
43
+ if (verbose && drift.docPath) {
44
+ output += `\n ${chalk.gray("File:")} ${drift.docPath}`;
45
+ }
46
+ if (verbose && drift.id) {
47
+ output += `\n ${chalk.gray("ID:")} ${drift.id}`;
48
+ }
49
+
50
+ return output;
51
+ }
52
+
53
+ /**
54
+ * List drifts with optional filters
55
+ */
56
+ async function listDrifts(apiKey, projectId, options = {}) {
57
+ console.log(chalk.blue("\nšŸ”„ Documentation Drifts\n"));
58
+
59
+ try {
60
+ const response = await apiClient.getDrifts(apiKey, projectId, {
61
+ status: options.status,
62
+ journeyKey: options.journey,
63
+ });
64
+
65
+ const drifts = response.drifts || [];
66
+ const stats = response.stats || {};
67
+
68
+ // Show stats
69
+ console.log(
70
+ chalk.gray("Total: ") +
71
+ chalk.white(stats.total || drifts.length) +
72
+ chalk.gray(" │ ") +
73
+ chalk.yellow(`Pending: ${stats.pending || 0}`) +
74
+ chalk.gray(" │ ") +
75
+ chalk.green(`Approved: ${stats.approved || 0}`) +
76
+ chalk.gray(" │ ") +
77
+ chalk.red(`Rejected: ${stats.rejected || 0}`)
78
+ );
79
+ console.log();
80
+
81
+ if (drifts.length === 0) {
82
+ if (options.status) {
83
+ console.log(chalk.gray(` No drifts with status: ${options.status}`));
84
+ } else {
85
+ console.log(chalk.green(" āœ“ No pending drifts!"));
86
+ }
87
+ return;
88
+ }
89
+
90
+ // List drifts
91
+ for (const drift of drifts) {
92
+ console.log(" " + formatDrift(drift, options.verbose));
93
+ }
94
+
95
+ console.log();
96
+ console.log(
97
+ chalk.gray(" Use ") +
98
+ chalk.white("reshot drifts show <id>") +
99
+ chalk.gray(" to see details")
100
+ );
101
+ console.log(
102
+ chalk.gray(" Use ") +
103
+ chalk.white("reshot drifts approve <id>") +
104
+ chalk.gray(" to approve changes")
105
+ );
106
+ } catch (error) {
107
+ console.error(chalk.red("Error:"), error.message);
108
+ process.exit(1);
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Show drift details
114
+ */
115
+ async function showDrift(apiKey, projectId, driftId) {
116
+ console.log(chalk.blue("\nšŸ“‹ Drift Details\n"));
117
+
118
+ try {
119
+ const response = await apiClient.post(
120
+ `/v1/projects/${projectId}/drifts/${driftId}`,
121
+ {},
122
+ { headers: { Authorization: `Bearer ${apiKey}` } }
123
+ ).catch(async () => {
124
+ // Fallback: get from list
125
+ const listResp = await apiClient.getDrifts(apiKey, projectId);
126
+ const drift = listResp.drifts?.find((d) => d.id === driftId);
127
+ if (drift) return { drift };
128
+ throw new Error(`Drift ${driftId} not found`);
129
+ });
130
+
131
+ const drift = response.drift;
132
+ if (!drift) {
133
+ console.error(chalk.red("Error:"), `Drift ${driftId} not found`);
134
+ process.exit(1);
135
+ }
136
+
137
+ // Display drift details
138
+ console.log(chalk.gray(" ID: ") + chalk.white(drift.id));
139
+ console.log(chalk.gray(" Status: ") + chalk.white(drift.status));
140
+ console.log(chalk.gray(" Type: ") + chalk.white(drift.driftType));
141
+ console.log(chalk.gray(" Journey: ") + chalk.white(drift.journeyKey || "N/A"));
142
+ console.log(chalk.gray(" File: ") + chalk.white(drift.docPath || "N/A"));
143
+ console.log(chalk.gray(" Confidence: ") + chalk.white(`${Math.round(drift.confidenceScore * 100)}%`));
144
+
145
+ // Visual diff info
146
+ if (drift.hasVisualDrift) {
147
+ console.log();
148
+ console.log(chalk.cyan(" šŸ–¼ Visual Changes Detected"));
149
+ if (drift.visualDiff?.diffPercentage) {
150
+ console.log(chalk.gray(` Diff: ${drift.visualDiff.diffPercentage.toFixed(2)}% changed`));
151
+ }
152
+ if (drift.newScreenshotUrl) {
153
+ console.log(chalk.gray(` New: ${drift.newScreenshotUrl}`));
154
+ }
155
+ }
156
+
157
+ // Semantic diff info
158
+ if (drift.hasSemanticDrift) {
159
+ console.log();
160
+ console.log(chalk.magenta(" šŸ“ Text Changes Detected"));
161
+
162
+ if (drift.originalContent) {
163
+ console.log(chalk.gray("\n Current Content:"));
164
+ console.log(chalk.red(" - " + drift.originalContent.slice(0, 200).replace(/\n/g, "\n - ")));
165
+ }
166
+
167
+ if (drift.proposedContent) {
168
+ console.log(chalk.gray("\n Proposed Content:"));
169
+ console.log(chalk.green(" + " + drift.proposedContent.slice(0, 200).replace(/\n/g, "\n + ")));
170
+ }
171
+ }
172
+
173
+ console.log();
174
+ console.log(chalk.gray(" Actions:"));
175
+ console.log(chalk.white(` reshot drifts approve ${driftId}`));
176
+ console.log(chalk.white(` reshot drifts reject ${driftId}`));
177
+ console.log(chalk.white(` reshot drifts ignore ${driftId}`));
178
+ } catch (error) {
179
+ console.error(chalk.red("Error:"), error.message);
180
+ process.exit(1);
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Perform action on a drift
186
+ */
187
+ async function performDriftAction(apiKey, projectId, driftId, action) {
188
+ const actionMap = {
189
+ approve: { label: "Approving", status: "APPROVED", emoji: "āœ“", apiAction: "approve" },
190
+ reject: { label: "Rejecting", status: "REJECTED", emoji: "āœ—", apiAction: "reject" },
191
+ ignore: { label: "Ignoring", status: "IGNORED", emoji: "ā—‹", apiAction: "ignore" },
192
+ sync: { label: "Marking as synced", status: "APPROVED", emoji: "āœ“", apiAction: "mark_synced" },
193
+ };
194
+
195
+ const actionInfo = actionMap[action];
196
+ if (!actionInfo) {
197
+ console.error(chalk.red("Error:"), `Unknown action: ${action}`);
198
+ process.exit(1);
199
+ }
200
+
201
+ console.log(chalk.blue(`\n${actionInfo.emoji} ${actionInfo.label} drift...\n`));
202
+
203
+ try {
204
+ const response = await apiClient.driftAction(apiKey, projectId, driftId, actionInfo.apiAction);
205
+
206
+ if (response.success) {
207
+ console.log(chalk.green(` āœ“ Drift ${driftId} ${action}ed successfully`));
208
+
209
+ if (response.prUrl) {
210
+ console.log(chalk.gray("\n Pull Request created:"));
211
+ console.log(chalk.blue(` ${response.prUrl}`));
212
+ }
213
+
214
+ if (action === "sync") {
215
+ console.log(chalk.gray("\n The visual assets have been promoted to 'approved' status."));
216
+ }
217
+ } else {
218
+ console.log(chalk.yellow(` ⚠ Action completed with warnings: ${response.message || "Unknown"}`));
219
+ }
220
+ } catch (error) {
221
+ console.error(chalk.red("Error:"), error.message);
222
+ process.exit(1);
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Batch approve or reject all pending drifts
228
+ */
229
+ async function batchDriftAction(apiKey, projectId, action, options = {}) {
230
+ const actionLabel = action === "approve" ? "Approving" : "Rejecting";
231
+ const actionVerb = action === "approve" ? "approved" : "rejected";
232
+
233
+ console.log(chalk.blue(`\nšŸ”„ ${actionLabel} all pending drifts...\n`));
234
+
235
+ try {
236
+ // Fetch all pending drifts
237
+ const response = await apiClient.getDrifts(apiKey, projectId, { status: "PENDING" });
238
+ const drifts = response.drifts || [];
239
+
240
+ if (drifts.length === 0) {
241
+ console.log(chalk.green(" āœ“ No pending drifts to process"));
242
+ return;
243
+ }
244
+
245
+ console.log(chalk.gray(` Found ${drifts.length} pending drift(s)\n`));
246
+
247
+ let succeeded = 0;
248
+ let failed = 0;
249
+ const errors = [];
250
+
251
+ for (const drift of drifts) {
252
+ try {
253
+ const apiAction = action === "approve" ? "approve" : "reject";
254
+ await apiClient.driftAction(apiKey, projectId, drift.id, apiAction);
255
+ succeeded++;
256
+ console.log(chalk.green(` āœ“ ${drift.journeyKey || drift.id} ${actionVerb}`));
257
+ } catch (err) {
258
+ failed++;
259
+ errors.push({ drift, error: err.message });
260
+ console.log(chalk.red(` āœ— ${drift.journeyKey || drift.id} failed: ${err.message}`));
261
+ }
262
+ }
263
+
264
+ console.log();
265
+ console.log(chalk.gray("─".repeat(40)));
266
+ console.log(chalk.green(` ${succeeded} drift(s) ${actionVerb}`));
267
+ if (failed > 0) {
268
+ console.log(chalk.red(` ${failed} drift(s) failed`));
269
+ }
270
+ } catch (error) {
271
+ console.error(chalk.red("Error:"), error.message);
272
+ process.exit(1);
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Validate journey bindings against visual inventory
278
+ */
279
+ async function validateBindings(apiKey, projectId, options = {}) {
280
+ console.log(chalk.blue("\nšŸ” Validating Journey Bindings\n"));
281
+
282
+ try {
283
+ // Fetch visual keys from server
284
+ const visualKeys = await apiClient.getVisualKeys(projectId, apiKey);
285
+ console.log(chalk.green(` āœ“ Fetched ${visualKeys.size} visual keys from project`));
286
+
287
+ // Read docsync config
288
+ const docSyncConfig = config.readDocSyncConfig();
289
+ const docConfig = docSyncConfig.documentation;
290
+
291
+ if (!docConfig || !docConfig.root) {
292
+ console.log(chalk.yellow(" ⚠ No documentation.root configured"));
293
+ return;
294
+ }
295
+
296
+ // Use the validate-docs module for full validation
297
+ const validateDocs = require("./validate-docs");
298
+ const result = await validateDocs.validateDocSync({
299
+ strict: options.strict,
300
+ verbose: options.verbose,
301
+ });
302
+
303
+ // Additional server-side validation: check if any journey keys in the project
304
+ // are not bound to any documentation
305
+ const mappings = docConfig.mappings || {};
306
+ const boundKeys = new Set(Object.values(mappings));
307
+
308
+ const orphanedVisuals = [];
309
+ for (const key of visualKeys) {
310
+ if (!boundKeys.has(key)) {
311
+ orphanedVisuals.push(key);
312
+ }
313
+ }
314
+
315
+ if (orphanedVisuals.length > 0) {
316
+ console.log(chalk.yellow(`\n ⚠ ${orphanedVisuals.length} visual(s) not bound to documentation:`));
317
+ if (options.verbose) {
318
+ orphanedVisuals.slice(0, 10).forEach((key) => {
319
+ console.log(chalk.gray(` - ${key}`));
320
+ });
321
+ if (orphanedVisuals.length > 10) {
322
+ console.log(chalk.gray(` ... and ${orphanedVisuals.length - 10} more`));
323
+ }
324
+ }
325
+ }
326
+
327
+ if (!result.valid) {
328
+ process.exit(1);
329
+ }
330
+ } catch (error) {
331
+ console.error(chalk.red("Error:"), error.message);
332
+ process.exit(1);
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Main drifts command handler
338
+ */
339
+ async function driftsCommand(subcommand, args = [], options = {}) {
340
+ // Read configuration
341
+ let docSyncConfig;
342
+ try {
343
+ docSyncConfig = config.readDocSyncConfig();
344
+ } catch (error) {
345
+ console.error(chalk.red("Error:"), "docsync.config.json not found. Run `reshot init` first.");
346
+ process.exit(1);
347
+ }
348
+
349
+ // Get API key and project ID
350
+ const settings = config.readSettings();
351
+ const apiKey = process.env.RESHOT_API_KEY || settings?.apiKey;
352
+ const projectId =
353
+ process.env.RESHOT_PROJECT_ID ||
354
+ settings?.projectId ||
355
+ docSyncConfig._metadata?.projectId;
356
+
357
+ if (!apiKey) {
358
+ console.error(chalk.red("Error:"), "API key not found. Set RESHOT_API_KEY or run `reshot auth`.");
359
+ process.exit(1);
360
+ }
361
+
362
+ if (!projectId) {
363
+ console.error(chalk.red("Error:"), "Project ID not found. Set RESHOT_PROJECT_ID or run `reshot init`.");
364
+ process.exit(1);
365
+ }
366
+
367
+ // Route to appropriate handler
368
+ switch (subcommand) {
369
+ case undefined:
370
+ case "list":
371
+ await listDrifts(apiKey, projectId, options);
372
+ break;
373
+
374
+ case "show":
375
+ if (!args[0]) {
376
+ console.error(chalk.red("Error:"), "Drift ID required. Usage: reshot drifts show <id>");
377
+ process.exit(1);
378
+ }
379
+ await showDrift(apiKey, projectId, args[0]);
380
+ break;
381
+
382
+ case "approve":
383
+ case "reject":
384
+ case "ignore":
385
+ case "sync":
386
+ if (!args[0]) {
387
+ console.error(chalk.red("Error:"), `Drift ID required. Usage: reshot drifts ${subcommand} <id>`);
388
+ process.exit(1);
389
+ }
390
+ await performDriftAction(apiKey, projectId, args[0], subcommand);
391
+ break;
392
+
393
+ case "approve-all":
394
+ await batchDriftAction(apiKey, projectId, "approve", options);
395
+ break;
396
+
397
+ case "reject-all":
398
+ await batchDriftAction(apiKey, projectId, "reject", options);
399
+ break;
400
+
401
+ case "validate":
402
+ await validateBindings(apiKey, projectId, options);
403
+ break;
404
+
405
+ default:
406
+ console.error(chalk.red("Error:"), `Unknown subcommand: ${subcommand}`);
407
+ console.log(chalk.gray("\nAvailable subcommands:"));
408
+ console.log(chalk.white(" list ") + chalk.gray("List drifts (default)"));
409
+ console.log(chalk.white(" show ") + chalk.gray("Show drift details"));
410
+ console.log(chalk.white(" approve ") + chalk.gray("Approve a drift"));
411
+ console.log(chalk.white(" reject ") + chalk.gray("Reject a drift"));
412
+ console.log(chalk.white(" ignore ") + chalk.gray("Ignore a drift"));
413
+ console.log(chalk.white(" sync ") + chalk.gray("Mark as manually synced"));
414
+ console.log(chalk.white(" approve-all ") + chalk.gray("Approve all pending drifts"));
415
+ console.log(chalk.white(" reject-all ") + chalk.gray("Reject all pending drifts"));
416
+ console.log(chalk.white(" validate ") + chalk.gray("Validate journey bindings"));
417
+ process.exit(1);
418
+ }
419
+
420
+ console.log();
421
+ }
422
+
423
+ module.exports = driftsCommand;