@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.
- package/LICENSE +190 -0
- package/README.md +388 -0
- package/package.json +64 -0
- package/src/commands/auth.js +259 -0
- package/src/commands/chrome.js +140 -0
- package/src/commands/ci-run.js +123 -0
- package/src/commands/ci-setup.js +288 -0
- package/src/commands/drifts.js +423 -0
- package/src/commands/import-tests.js +309 -0
- package/src/commands/ingest.js +458 -0
- package/src/commands/init.js +633 -0
- package/src/commands/publish.js +1721 -0
- package/src/commands/pull.js +303 -0
- package/src/commands/record.js +94 -0
- package/src/commands/run.js +476 -0
- package/src/commands/setup-wizard.js +740 -0
- package/src/commands/setup.js +137 -0
- package/src/commands/status.js +275 -0
- package/src/commands/sync.js +621 -0
- package/src/commands/ui.js +248 -0
- package/src/commands/validate-docs.js +529 -0
- package/src/index.js +462 -0
- package/src/lib/api-client.js +815 -0
- package/src/lib/capture-engine.js +1623 -0
- package/src/lib/capture-script-runner.js +3120 -0
- package/src/lib/ci-detect.js +137 -0
- package/src/lib/config.js +1240 -0
- package/src/lib/diff-engine.js +642 -0
- package/src/lib/hash.js +74 -0
- package/src/lib/image-crop.js +396 -0
- package/src/lib/matrix.js +89 -0
- package/src/lib/output-path-template.js +318 -0
- package/src/lib/playwright-runner.js +252 -0
- package/src/lib/polished-clip.js +553 -0
- package/src/lib/privacy-engine.js +408 -0
- package/src/lib/progress-tracker.js +142 -0
- package/src/lib/record-browser-injection.js +654 -0
- package/src/lib/record-cdp.js +612 -0
- package/src/lib/record-clip.js +343 -0
- package/src/lib/record-config.js +623 -0
- package/src/lib/record-screenshot.js +360 -0
- package/src/lib/record-terminal.js +123 -0
- package/src/lib/recorder-service.js +781 -0
- package/src/lib/secrets.js +51 -0
- package/src/lib/selector-strategies.js +859 -0
- package/src/lib/standalone-mode.js +400 -0
- package/src/lib/storage-providers.js +569 -0
- package/src/lib/style-engine.js +684 -0
- package/src/lib/ui-api.js +4677 -0
- package/src/lib/ui-assets.js +373 -0
- package/src/lib/ui-executor.js +587 -0
- package/src/lib/variant-injector.js +591 -0
- package/src/lib/viewport-presets.js +454 -0
- package/src/lib/worker-pool.js +118 -0
- package/web/cropper/index.html +436 -0
- package/web/manager/dist/assets/index--ZgioErz.js +507 -0
- package/web/manager/dist/assets/index-n468W0Wr.css +1 -0
- package/web/manager/dist/index.html +27 -0
- 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;
|