@vizzly-testing/cli 0.38.0 → 0.39.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/README.md +222 -183
- package/dist/auth/client.js +4 -1
- package/dist/auth/core.js +64 -3
- package/dist/auth/operations.js +21 -11
- package/dist/cli.js +45 -53
- package/dist/commands/init.js +156 -483
- package/dist/commands/login.js +71 -137
- package/dist/commands/project.js +128 -44
- package/dist/commands/run.js +2 -2
- package/dist/commands/tdd.js +89 -38
- package/dist/commands/upload.js +2 -2
- package/dist/init/agents.js +99 -0
- package/dist/init/core.js +31 -0
- package/dist/init/prompts.js +44 -0
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +31 -127
- package/dist/server/handlers/tdd-handler.js +33 -2
- package/dist/server/routers/auth.js +32 -42
- package/dist/services/index.js +8 -2
- package/dist/tdd/services/comparison-service.js +4 -1
- package/dist/tdd/tdd-service.js +14 -2
- package/dist/test-runner/core.js +28 -3
- package/dist/test-runner/operations.js +41 -20
- package/dist/types/index.d.ts +8 -0
- package/dist/uploader/core.js +1 -0
- package/dist/utils/browser.js +7 -1
- package/dist/utils/config-discovery.js +73 -0
- package/dist/utils/config-loader.js +29 -8
- package/dist/utils/config-schema.js +5 -0
- package/dist/utils/file-changes.js +105 -0
- package/dist/utils/login-poll-wait.js +50 -0
- package/dist/utils/project-binding.js +96 -0
- package/dist/utils/project-link-store.js +13 -7
- package/package.json +27 -27
- package/skills/vizzly/SKILL.md +4 -3
- package/skills/vizzly/references/ci.md +33 -0
- package/skills/vizzly/references/cli-context.md +81 -71
- package/skills/vizzly/references/dynamic-content.md +16 -17
- package/skills/vizzly/references/sdks.md +22 -22
- package/skills/vizzly/references/setup-ci.md +0 -44
package/dist/commands/init.js
CHANGED
|
@@ -1,513 +1,186 @@
|
|
|
1
|
-
|
|
2
|
-
import fs from 'node:fs/promises';
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
3
2
|
import path from 'node:path';
|
|
4
|
-
import { createInterface } from 'node:readline/promises';
|
|
5
3
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
4
|
+
import { planAgentSetup } from '../init/agents.js';
|
|
5
|
+
import { planConfig } from '../init/core.js';
|
|
6
|
+
import { createPrompts } from '../init/prompts.js';
|
|
7
|
+
import { discoverConfig } from '../utils/config-discovery.js';
|
|
8
|
+
import { applyChanges } from '../utils/file-changes.js';
|
|
10
9
|
import * as output from '../utils/output.js';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
access: deps.access || fs.access,
|
|
17
|
-
copy: deps.copy || fs.cp,
|
|
18
|
-
cwd: deps.cwd || (() => process.cwd()),
|
|
19
|
-
isInteractive: deps.isInteractive || (() => Boolean(process.stdin.isTTY && process.stdout.isTTY)),
|
|
20
|
-
loadConfig: deps.loadConfig || loadConfig,
|
|
21
|
-
loadPlugins: deps.loadPlugins || loadPlugins,
|
|
22
|
-
mkdir: deps.mkdir || fs.mkdir,
|
|
23
|
-
output: deps.output || output,
|
|
24
|
-
promptAgentSkill: deps.promptAgentSkill || promptAgentSkill,
|
|
25
|
-
readFile: deps.readFile || fs.readFile,
|
|
26
|
-
remove: deps.remove || fs.rm,
|
|
27
|
-
skillSourcePath: deps.skillSourcePath || getPackagedAgentSkillPath(),
|
|
28
|
-
writeFile: deps.writeFile || fs.writeFile
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
function configureOutput(output, options) {
|
|
32
|
-
output.configure({
|
|
33
|
-
json: options.json || false,
|
|
34
|
-
verbose: options.verbose || false,
|
|
35
|
-
color: options.color !== false
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
export function getInitConfigPath(cwd = process.cwd()) {
|
|
39
|
-
return path.join(cwd, 'vizzly.config.js');
|
|
40
|
-
}
|
|
41
|
-
function getPackagedAgentSkillPath(baseDir = commandDir) {
|
|
42
|
-
return path.resolve(baseDir, '..', '..', 'skills', 'vizzly');
|
|
43
|
-
}
|
|
44
|
-
export function getProjectAgentSkillPath(cwd = process.cwd()) {
|
|
45
|
-
return path.join(cwd, '.agents', 'skills', 'vizzly');
|
|
46
|
-
}
|
|
47
|
-
export function getProjectAgentsPath(cwd = process.cwd()) {
|
|
48
|
-
return path.join(cwd, 'AGENTS.md');
|
|
49
|
-
}
|
|
50
|
-
export async function fileExists(filePath, access = fs.access) {
|
|
51
|
-
try {
|
|
52
|
-
await access(filePath);
|
|
53
|
-
return true;
|
|
54
|
-
} catch {
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
function getPluginsWithConfig(plugins = []) {
|
|
59
|
-
return plugins.filter(plugin => plugin.configSchema);
|
|
60
|
-
}
|
|
61
|
-
function getPluginConfigNames(plugins = []) {
|
|
62
|
-
return getPluginsWithConfig(plugins).map(plugin => plugin.name);
|
|
63
|
-
}
|
|
64
|
-
function formatObjectKey(key) {
|
|
65
|
-
if (/^[A-Za-z_$][\w$]*$/.test(key)) {
|
|
66
|
-
return key;
|
|
67
|
-
}
|
|
68
|
-
return formatValue(key);
|
|
69
|
-
}
|
|
70
|
-
export function formatValue(value, depth = 0) {
|
|
71
|
-
let indent = ' '.repeat(depth);
|
|
72
|
-
let nextIndent = ' '.repeat(depth + 1);
|
|
73
|
-
if (value === null) return 'null';
|
|
74
|
-
if (value === undefined) return 'undefined';
|
|
75
|
-
if (typeof value === 'string') {
|
|
76
|
-
return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n')}'`;
|
|
77
|
-
}
|
|
78
|
-
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
79
|
-
return String(value);
|
|
80
|
-
}
|
|
81
|
-
if (Array.isArray(value)) {
|
|
82
|
-
if (value.length === 0) return '[]';
|
|
83
|
-
let items = value.map(item => {
|
|
84
|
-
let formatted = formatValue(item, depth + 1);
|
|
85
|
-
return `${nextIndent}${formatted}`;
|
|
10
|
+
import { projectLinkCommand, validateProjectLinkOptions } from './project.js';
|
|
11
|
+
function runProcess(command, args) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
let child = spawn(command, args, {
|
|
14
|
+
stdio: 'inherit'
|
|
86
15
|
});
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
let entries = Object.entries(value);
|
|
91
|
-
if (entries.length === 0) return '{}';
|
|
92
|
-
let props = entries.map(([key, entryValue]) => {
|
|
93
|
-
let formatted = formatValue(entryValue, depth + 1);
|
|
94
|
-
return `${nextIndent}${formatObjectKey(key)}: ${formatted}`;
|
|
16
|
+
child.once('error', reject);
|
|
17
|
+
child.once('close', (code, signal) => {
|
|
18
|
+
if (code === 0) resolve();else reject(new Error(`${command} did not finish successfully (${signal || code}).`));
|
|
95
19
|
});
|
|
96
|
-
return `{\n${props.join(',\n')}\n${indent}}`;
|
|
97
|
-
}
|
|
98
|
-
return String(value);
|
|
99
|
-
}
|
|
100
|
-
export function formatPluginConfig(plugin, output = null) {
|
|
101
|
-
try {
|
|
102
|
-
configSchemaValidator.parse(plugin.configSchema);
|
|
103
|
-
let configEntries = [];
|
|
104
|
-
for (let [key, value] of Object.entries(plugin.configSchema)) {
|
|
105
|
-
let formattedValue = formatValue(value, 1);
|
|
106
|
-
configEntries.push(` // ${plugin.name} plugin configuration\n ${formatObjectKey(key)}: ${formattedValue}`);
|
|
107
|
-
}
|
|
108
|
-
return configEntries.join(',\n\n');
|
|
109
|
-
} catch (error) {
|
|
110
|
-
if (error instanceof z.ZodError) {
|
|
111
|
-
let messages = error.issues.map(zodError => `${zodError.path.join('.')}: ${zodError.message}`);
|
|
112
|
-
output?.warn(`Invalid config schema for plugin ${plugin.name}: ${messages.join(', ')}`);
|
|
113
|
-
} else {
|
|
114
|
-
output?.warn(`Failed to format config for plugin ${plugin.name}: ${error.message}`);
|
|
115
|
-
}
|
|
116
|
-
return '';
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
function generatePluginConfigs(plugins = [], output = null) {
|
|
120
|
-
let sections = [];
|
|
121
|
-
for (let plugin of plugins) {
|
|
122
|
-
if (plugin.configSchema) {
|
|
123
|
-
let configStr = formatPluginConfig(plugin, output);
|
|
124
|
-
if (configStr) {
|
|
125
|
-
sections.push(configStr);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return sections.length > 0 ? sections.join(',\n\n') : '';
|
|
130
|
-
}
|
|
131
|
-
export function createConfigContent(plugins = [], output = null) {
|
|
132
|
-
let coreConfig = `export default {
|
|
133
|
-
// Server configuration (for run command)
|
|
134
|
-
server: {
|
|
135
|
-
port: 47392,
|
|
136
|
-
timeout: 30000
|
|
137
|
-
},
|
|
138
|
-
|
|
139
|
-
// Build configuration
|
|
140
|
-
build: {
|
|
141
|
-
name: 'Build {timestamp}',
|
|
142
|
-
environment: 'test'
|
|
143
|
-
},
|
|
144
|
-
|
|
145
|
-
// Upload configuration (for upload command)
|
|
146
|
-
upload: {
|
|
147
|
-
screenshotsDir: './screenshots',
|
|
148
|
-
batchSize: 10,
|
|
149
|
-
timeout: 30000
|
|
150
|
-
},
|
|
151
|
-
|
|
152
|
-
// Comparison configuration (CIEDE2000 Delta E: 0=exact, 1=JND, 2=recommended)
|
|
153
|
-
comparison: {
|
|
154
|
-
threshold: 2.0,
|
|
155
|
-
minClusterSize: 2
|
|
156
|
-
},
|
|
157
|
-
|
|
158
|
-
// TDD configuration
|
|
159
|
-
tdd: {
|
|
160
|
-
openReport: false // Whether to auto-open HTML report in browser
|
|
161
|
-
}`;
|
|
162
|
-
let pluginConfigs = generatePluginConfigs(plugins, output);
|
|
163
|
-
if (pluginConfigs) {
|
|
164
|
-
coreConfig += `,\n\n${pluginConfigs}`;
|
|
165
|
-
}
|
|
166
|
-
return `${coreConfig}\n};\n`;
|
|
167
|
-
}
|
|
168
|
-
function writeHumanCreatedOutput(output, plugins) {
|
|
169
|
-
output.header('init');
|
|
170
|
-
output.complete('Created vizzly.config.js');
|
|
171
|
-
let pluginsWithConfig = getPluginsWithConfig(plugins);
|
|
172
|
-
if (pluginsWithConfig.length > 0) {
|
|
173
|
-
output.hint(`Added config for ${pluginsWithConfig.length} plugin(s):`);
|
|
174
|
-
output.list(pluginsWithConfig.map(plugin => plugin.name), {
|
|
175
|
-
indent: 4
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
function showNextSteps(output) {
|
|
180
|
-
output.blank();
|
|
181
|
-
output.labelValue('Next steps', '');
|
|
182
|
-
output.list(['Set your API token: export VIZZLY_TOKEN="your-api-key"', 'Run your tests with Vizzly: pnpm exec vizzly run "pnpm test"', 'Upload screenshots: pnpm exec vizzly upload ./screenshots']);
|
|
183
|
-
}
|
|
184
|
-
async function promptAgentSkill({
|
|
185
|
-
stdin = process.stdin,
|
|
186
|
-
stdout = process.stdout
|
|
187
|
-
} = {}) {
|
|
188
|
-
let rl = createInterface({
|
|
189
|
-
input: stdin,
|
|
190
|
-
output: stdout
|
|
191
20
|
});
|
|
192
|
-
try {
|
|
193
|
-
let answer = await rl.question('Add the Vizzly agent skill to this repo? [Y/n] ');
|
|
194
|
-
return !['n', 'no'].includes(answer.trim().toLowerCase());
|
|
195
|
-
} finally {
|
|
196
|
-
rl.close();
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
async function shouldInstallAgentSkill(options, deps) {
|
|
200
|
-
if (options.agentGuidance) {
|
|
201
|
-
return true;
|
|
202
|
-
}
|
|
203
|
-
if (options.skipAgentSkill) {
|
|
204
|
-
return false;
|
|
205
|
-
}
|
|
206
|
-
if (options.agentSkill) {
|
|
207
|
-
return true;
|
|
208
|
-
}
|
|
209
|
-
if (options.json || !deps.isInteractive()) {
|
|
210
|
-
return false;
|
|
211
|
-
}
|
|
212
|
-
return await deps.promptAgentSkill();
|
|
213
|
-
}
|
|
214
|
-
let agentGuidanceStart = '<!-- vizzly-agent-guidance -->';
|
|
215
|
-
let agentGuidanceEnd = '<!-- /vizzly-agent-guidance -->';
|
|
216
|
-
function createAgentGuidanceContent() {
|
|
217
|
-
let skillPath = '.agents/skills/vizzly/SKILL.md';
|
|
218
|
-
return `${agentGuidanceStart}
|
|
219
|
-
## Visual Testing With Vizzly
|
|
220
|
-
|
|
221
|
-
For user-facing changes, use the repo-local Vizzly skill at
|
|
222
|
-
\`${skillPath}\`.
|
|
223
|
-
|
|
224
|
-
- Inspect existing visual evidence before and after the change.
|
|
225
|
-
- Use the repository's established Vizzly command and owning user workflow.
|
|
226
|
-
- For cloud reviews, discover supported requests with
|
|
227
|
-
\`vizzly api schema --json\` and inspect baseline, current, and diff images
|
|
228
|
-
together.
|
|
229
|
-
- For local evidence, use \`vizzly context ... --json\`.
|
|
230
|
-
|
|
231
|
-
Treat Vizzly diffs as review evidence. Do not approve, reject, or replace
|
|
232
|
-
evidence unless the task explicitly asks for that mutation.
|
|
233
|
-
${agentGuidanceEnd}
|
|
234
|
-
`;
|
|
235
21
|
}
|
|
236
|
-
function
|
|
237
|
-
|
|
238
|
-
|
|
22
|
+
async function guidedPlan(cwd, config, prompts) {
|
|
23
|
+
output.hint(`Project: ${cwd}`);
|
|
24
|
+
let guidance = await prompts.confirm('Add the Vizzly skill and AGENTS.md instructions?');
|
|
25
|
+
let cloudProject = null;
|
|
26
|
+
if (await prompts.confirm('Connect this repository to a Vizzly Cloud project?')) {
|
|
27
|
+
cloudProject = await prompts.ask('Project (organization/project)');
|
|
28
|
+
let errors = validateProjectLinkOptions(cloudProject);
|
|
29
|
+
if (errors.length) throw new Error(errors.join(' '));
|
|
30
|
+
}
|
|
31
|
+
let agents = await planAgentSetup(cwd, {
|
|
32
|
+
skill: guidance,
|
|
33
|
+
guidance
|
|
34
|
+
});
|
|
35
|
+
let changes = [planConfig(config), ...agents.changes];
|
|
36
|
+
output.labelValue('Proposed changes', '');
|
|
37
|
+
output.list(changes.map(change => path.relative(cwd, change.path)));
|
|
38
|
+
if (cloudProject) {
|
|
39
|
+
output.hint(`Connect project: ${cloudProject}`);
|
|
40
|
+
output.hint('Local setup is saved first. If cloud linking fails, your local files stay in place.');
|
|
41
|
+
}
|
|
42
|
+
if (!(await prompts.confirm('Apply this setup?', true))) {
|
|
43
|
+
throw Object.assign(new Error('Setup cancelled.'), {
|
|
44
|
+
code: 'INIT_CANCELLED'
|
|
45
|
+
});
|
|
239
46
|
}
|
|
240
|
-
return
|
|
47
|
+
return {
|
|
48
|
+
...agents,
|
|
49
|
+
changes,
|
|
50
|
+
cloudProject
|
|
51
|
+
};
|
|
241
52
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
access = fs.access,
|
|
245
|
-
readFile = fs.readFile,
|
|
246
|
-
writeFile = fs.writeFile
|
|
247
|
-
}) {
|
|
248
|
-
let agentsPath = getProjectAgentsPath(cwd);
|
|
249
|
-
let guidance = createAgentGuidanceContent();
|
|
250
|
-
if (!(await fileExists(agentsPath, access))) {
|
|
251
|
-
await writeFile(agentsPath, guidance, 'utf8');
|
|
252
|
-
return {
|
|
253
|
-
status: 'created',
|
|
254
|
-
agentsPath
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
let existingContent = await readFile(agentsPath, 'utf8');
|
|
258
|
-
let startIndex = existingContent.indexOf(agentGuidanceStart);
|
|
259
|
-
let endIndex = existingContent.indexOf(agentGuidanceEnd);
|
|
260
|
-
let startCount = existingContent.split(agentGuidanceStart).length - 1;
|
|
261
|
-
let endCount = existingContent.split(agentGuidanceEnd).length - 1;
|
|
262
|
-
if (startCount !== endCount || startCount > 1 || startCount === 1 && endIndex < startIndex) {
|
|
263
|
-
return {
|
|
264
|
-
status: 'malformed',
|
|
265
|
-
agentsPath
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
if (startIndex >= 0 && endIndex >= startIndex) {
|
|
269
|
-
let blockEnd = endIndex + agentGuidanceEnd.length;
|
|
270
|
-
let refreshedContent = `${existingContent.slice(0, startIndex)}${guidance.trimEnd()}${existingContent.slice(blockEnd)}`;
|
|
271
|
-
if (refreshedContent === existingContent) {
|
|
272
|
-
return {
|
|
273
|
-
status: 'exists',
|
|
274
|
-
agentsPath
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
await writeFile(agentsPath, refreshedContent, 'utf8');
|
|
278
|
-
return {
|
|
279
|
-
status: 'refreshed',
|
|
280
|
-
agentsPath
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
await writeFile(agentsPath, appendSection(existingContent, guidance), 'utf8');
|
|
53
|
+
function agentOptions(options) {
|
|
54
|
+
let guidance = Boolean(options.agentGuidance);
|
|
284
55
|
return {
|
|
285
|
-
|
|
286
|
-
|
|
56
|
+
guidance,
|
|
57
|
+
skill: guidance || !options.skipAgentSkill && Boolean(options.agentSkill)
|
|
287
58
|
};
|
|
288
59
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
mkdir = fs.mkdir,
|
|
296
|
-
remove = fs.rm
|
|
297
|
-
}) {
|
|
298
|
-
let targetPath = getProjectAgentSkillPath(cwd);
|
|
299
|
-
if (!(await fileExists(path.join(sourcePath, 'SKILL.md'), access))) {
|
|
300
|
-
return {
|
|
301
|
-
status: 'missing-source',
|
|
302
|
-
sourcePath,
|
|
303
|
-
targetPath
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
let targetExists = await fileExists(targetPath, access);
|
|
307
|
-
if (targetExists && !refresh) {
|
|
308
|
-
return {
|
|
309
|
-
status: 'exists',
|
|
310
|
-
sourcePath,
|
|
311
|
-
targetPath
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
await mkdir(path.dirname(targetPath), {
|
|
315
|
-
recursive: true
|
|
316
|
-
});
|
|
317
|
-
await copy(sourcePath, targetPath, {
|
|
318
|
-
recursive: true,
|
|
319
|
-
errorOnExist: !targetExists,
|
|
320
|
-
force: targetExists
|
|
60
|
+
async function connectProject(project, options) {
|
|
61
|
+
let exitCode = 0;
|
|
62
|
+
let link = () => projectLinkCommand(project, {}, options, {
|
|
63
|
+
exit: code => {
|
|
64
|
+
exitCode = code;
|
|
65
|
+
}
|
|
321
66
|
});
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
67
|
+
let result = await link();
|
|
68
|
+
if (result?.reason === 'auth_required') {
|
|
69
|
+
let cli = fileURLToPath(new URL('../cli.js', import.meta.url));
|
|
70
|
+
await runProcess(process.execPath, [cli, 'login']);
|
|
71
|
+
result = await link();
|
|
72
|
+
}
|
|
73
|
+
let status = result?.linked ? 'linked' : 'failed';
|
|
74
|
+
if (!result?.linked && exitCode === 1) status = 'auth_required';
|
|
327
75
|
return {
|
|
328
|
-
status
|
|
329
|
-
|
|
330
|
-
targetPath
|
|
76
|
+
status,
|
|
77
|
+
project
|
|
331
78
|
};
|
|
332
79
|
}
|
|
333
|
-
function
|
|
334
|
-
if (
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
if (result.status === 'installed') {
|
|
338
|
-
output.complete('Added Vizzly agent skill');
|
|
339
|
-
output.hint(`Installed at ${result.targetPath}`);
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
if (result.status === 'refreshed') {
|
|
343
|
-
output.complete('Refreshed Vizzly agent skill');
|
|
344
|
-
output.hint(`Updated ${result.targetPath}`);
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
if (result.status === 'exists') {
|
|
348
|
-
output.hint('Vizzly agent skill already exists in this repo');
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
if (result.status === 'missing-source') {
|
|
352
|
-
output.warn('Vizzly agent skill was not found in this CLI package');
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
function writeAgentGuidanceOutput(output, result) {
|
|
356
|
-
if (!result) {
|
|
357
|
-
return;
|
|
358
|
-
}
|
|
359
|
-
if (result.status === 'created') {
|
|
360
|
-
output.complete('Created AGENTS.md with Vizzly guidance');
|
|
361
|
-
output.hint(`Wrote ${result.agentsPath}`);
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
if (result.status === 'updated') {
|
|
365
|
-
output.complete('Added Vizzly guidance to AGENTS.md');
|
|
366
|
-
output.hint(`Updated ${result.agentsPath}`);
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
if (result.status === 'refreshed') {
|
|
370
|
-
output.complete('Refreshed Vizzly guidance in AGENTS.md');
|
|
371
|
-
output.hint(`Updated ${result.agentsPath}`);
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
if (result.status === 'exists') {
|
|
375
|
-
output.hint('Vizzly guidance already exists in AGENTS.md');
|
|
80
|
+
function showResult(result, json) {
|
|
81
|
+
if (json) {
|
|
82
|
+
output.data(result);
|
|
376
83
|
return;
|
|
377
84
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
85
|
+
output.complete(result.status === 'created' ? 'Config created' : 'Existing config preserved');
|
|
86
|
+
output.hint(result.configPath);
|
|
87
|
+
if (result.agentSkill) output.hint(`Vizzly agent skill: ${result.agentSkill.status}`);
|
|
88
|
+
if (result.agentGuidance) output.hint(`AGENTS.md guidance: ${result.agentGuidance.status}`);
|
|
89
|
+
if (result.cloud.error) output.warn(`Cloud linking failed: ${result.cloud.error}`);
|
|
90
|
+
if (result.status === 'skipped') {
|
|
91
|
+
output.hint('Edit this config to change your setup. Use --force to replace it.');
|
|
92
|
+
} else {
|
|
93
|
+
output.hint('Edit this config for your project.');
|
|
94
|
+
output.hint('Use a Vizzly client to add screenshot capture.');
|
|
381
95
|
}
|
|
382
96
|
}
|
|
383
|
-
function
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
}) {
|
|
402
|
-
let coreConfig = createConfigContent(plugins, output);
|
|
403
|
-
await writeFile(configPath, coreConfig, 'utf8');
|
|
404
|
-
if (!options.json) {
|
|
405
|
-
writeHumanCreatedOutput(output, plugins);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
async function loadInitPlugins(options, deps) {
|
|
409
|
-
if (options.plugins) {
|
|
410
|
-
return options.plugins;
|
|
411
|
-
}
|
|
412
|
-
try {
|
|
413
|
-
let config = await deps.loadConfig(options.config, {});
|
|
414
|
-
return await deps.loadPlugins(options.config, config);
|
|
415
|
-
} catch {
|
|
416
|
-
return [];
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
export async function init(options = {}, deps = {}) {
|
|
420
|
-
let resolvedDeps = createInitDeps(deps);
|
|
421
|
-
configureOutput(resolvedDeps.output, options);
|
|
422
|
-
let plugins = await loadInitPlugins(options, resolvedDeps);
|
|
423
|
-
let configPath = getInitConfigPath(resolvedDeps.cwd());
|
|
424
|
-
let hasConfig = await fileExists(configPath, resolvedDeps.access);
|
|
425
|
-
let agentSkillResult = null;
|
|
426
|
-
let agentGuidanceResult = null;
|
|
427
|
-
if (await shouldInstallAgentSkill(options, resolvedDeps)) {
|
|
428
|
-
agentSkillResult = await installProjectAgentSkill({
|
|
429
|
-
cwd: resolvedDeps.cwd(),
|
|
430
|
-
sourcePath: resolvedDeps.skillSourcePath,
|
|
431
|
-
access: resolvedDeps.access,
|
|
432
|
-
copy: resolvedDeps.copy,
|
|
433
|
-
mkdir: resolvedDeps.mkdir,
|
|
434
|
-
refresh: options.agentGuidance || options.agentSkill,
|
|
435
|
-
remove: resolvedDeps.remove
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
if (options.agentGuidance) {
|
|
439
|
-
agentGuidanceResult = await upsertProjectAgentGuidance({
|
|
440
|
-
cwd: resolvedDeps.cwd(),
|
|
441
|
-
access: resolvedDeps.access,
|
|
442
|
-
readFile: resolvedDeps.readFile,
|
|
443
|
-
writeFile: resolvedDeps.writeFile
|
|
97
|
+
export async function init(options = {}) {
|
|
98
|
+
output.configure({
|
|
99
|
+
json: options.json || false,
|
|
100
|
+
verbose: options.verbose,
|
|
101
|
+
color: options.color
|
|
102
|
+
});
|
|
103
|
+
let cwd = process.cwd();
|
|
104
|
+
let explicitAgentOption = options.agentSkill || options.agentGuidance || options.skipAgentSkill;
|
|
105
|
+
let quick = Boolean(options.quick || options.json || explicitAgentOption);
|
|
106
|
+
let config = await discoverConfig({
|
|
107
|
+
cwd,
|
|
108
|
+
configPath: options.config,
|
|
109
|
+
allowInvalid: options.force
|
|
110
|
+
});
|
|
111
|
+
let initialized = config.exists && !options.force;
|
|
112
|
+
if (!initialized && !quick && (!process.stdin.isTTY || !process.stdout.isTTY || process.env.CI)) {
|
|
113
|
+
throw Object.assign(new Error('Use vizzly init --quick to create config without a terminal, or run vizzly init interactively.'), {
|
|
114
|
+
code: 'INIT_INPUT_REQUIRED'
|
|
444
115
|
});
|
|
445
116
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
configPath
|
|
117
|
+
let mode = quick ? 'quick' : 'guided';
|
|
118
|
+
let plan;
|
|
119
|
+
let prompts;
|
|
120
|
+
try {
|
|
121
|
+
if (initialized) {
|
|
122
|
+
plan = {
|
|
123
|
+
changes: []
|
|
124
|
+
};
|
|
125
|
+
} else if (quick) {
|
|
126
|
+
let configChange = planConfig(config);
|
|
127
|
+
let agents = await planAgentSetup(cwd, agentOptions(options));
|
|
128
|
+
plan = {
|
|
129
|
+
...agents,
|
|
130
|
+
changes: [configChange, ...agents.changes]
|
|
461
131
|
};
|
|
132
|
+
} else {
|
|
133
|
+
output.header('init');
|
|
134
|
+
prompts = createPrompts();
|
|
135
|
+
plan = await guidedPlan(cwd, config, prompts);
|
|
462
136
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
writeAgentSkillOutput(resolvedDeps.output, agentSkillResult);
|
|
467
|
-
writeAgentGuidanceOutput(resolvedDeps.output, agentGuidanceResult);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
if (error.code !== 'INIT_CANCELLED') throw error;
|
|
139
|
+
output.hint('Setup cancelled; no files changed.');
|
|
468
140
|
return {
|
|
469
|
-
status: '
|
|
470
|
-
|
|
141
|
+
status: 'cancelled',
|
|
142
|
+
mode,
|
|
143
|
+
exitCode: 130
|
|
471
144
|
};
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
})
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
145
|
+
} finally {
|
|
146
|
+
prompts?.close();
|
|
147
|
+
}
|
|
148
|
+
let changes = await applyChanges(plan.changes);
|
|
149
|
+
let result = {
|
|
150
|
+
status: initialized ? 'skipped' : 'created',
|
|
151
|
+
...(initialized ? {
|
|
152
|
+
reason: 'config_exists'
|
|
153
|
+
} : {}),
|
|
154
|
+
configPath: config.configPath,
|
|
155
|
+
mode,
|
|
156
|
+
plugins: [],
|
|
157
|
+
changes,
|
|
158
|
+
cloud: {
|
|
159
|
+
status: 'not_requested'
|
|
160
|
+
},
|
|
161
|
+
...(plan.agentSkill ? {
|
|
162
|
+
agentSkill: plan.agentSkill
|
|
163
|
+
} : {}),
|
|
164
|
+
...(plan.agentGuidance ? {
|
|
165
|
+
agentGuidance: plan.agentGuidance
|
|
166
|
+
} : {}),
|
|
167
|
+
exitCode: 0
|
|
168
|
+
};
|
|
169
|
+
if (plan.cloudProject) {
|
|
170
|
+
try {
|
|
171
|
+
result.cloud = await connectProject(plan.cloudProject, options);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
result.cloud = {
|
|
174
|
+
status: 'failed',
|
|
175
|
+
project: plan.cloudProject,
|
|
176
|
+
error: error.message
|
|
496
177
|
};
|
|
497
178
|
}
|
|
498
|
-
|
|
499
|
-
writeAgentSkillOutput(resolvedDeps.output, agentSkillResult);
|
|
500
|
-
writeAgentGuidanceOutput(resolvedDeps.output, agentGuidanceResult);
|
|
501
|
-
resolvedDeps.output.blank();
|
|
502
|
-
resolvedDeps.output.complete('Vizzly CLI setup complete');
|
|
503
|
-
return {
|
|
504
|
-
status: 'created',
|
|
505
|
-
configPath,
|
|
506
|
-
plugins: pluginNames
|
|
507
|
-
};
|
|
508
|
-
} catch (error) {
|
|
509
|
-
throw new VizzlyError('Failed to initialize Vizzly configuration', 'INIT_FAILED', {
|
|
510
|
-
error: error.message
|
|
511
|
-
});
|
|
179
|
+
if (result.cloud.status !== 'linked') result.exitCode = 1;
|
|
512
180
|
}
|
|
181
|
+
output.configure({
|
|
182
|
+
json: options.json || false
|
|
183
|
+
});
|
|
184
|
+
showResult(result, options.json);
|
|
185
|
+
return result;
|
|
513
186
|
}
|