@vizzly-testing/cli 0.38.1 → 0.39.1
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 -195
- package/dist/cli.js +55 -54
- package/dist/commands/init.js +154 -483
- 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 +140 -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 +30 -61
- package/dist/reporter-ssr/ssr-entry.js +3 -6
- package/dist/server/handlers/tdd-handler.js +33 -2
- 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/cli-update.js +105 -0
- 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/project-binding.js +96 -0
- package/dist/utils/project-link-store.js +13 -7
- package/dist/utils/update-check.js +2 -0
- package/dist/utils/update-notifications.js +175 -0
- package/package.json +16 -16
- 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,184 @@
|
|
|
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}`;
|
|
86
|
-
});
|
|
87
|
-
return `[\n${items.join(',\n')}\n${indent}]`;
|
|
88
|
-
}
|
|
89
|
-
if (typeof value === 'object') {
|
|
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}`;
|
|
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'
|
|
95
15
|
});
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
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}).`));
|
|
176
19
|
});
|
|
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
21
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
let
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
return section;
|
|
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
|
-
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
payload.agentSkill = agentSkillResult;
|
|
389
|
-
}
|
|
390
|
-
if (agentGuidanceResult) {
|
|
391
|
-
payload.agentGuidance = agentGuidanceResult;
|
|
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.');
|
|
392
95
|
}
|
|
393
|
-
return payload;
|
|
394
96
|
}
|
|
395
|
-
async function
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
let
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
return {
|
|
459
|
-
status: 'skipped',
|
|
460
|
-
configPath
|
|
117
|
+
let mode = quick ? 'quick' : 'guided';
|
|
118
|
+
let plan;
|
|
119
|
+
let prompts;
|
|
120
|
+
try {
|
|
121
|
+
if (initialized) {
|
|
122
|
+
plan = await planAgentSetup(cwd, agentOptions(options));
|
|
123
|
+
} else if (quick) {
|
|
124
|
+
let configChange = planConfig(config);
|
|
125
|
+
let agents = await planAgentSetup(cwd, agentOptions(options));
|
|
126
|
+
plan = {
|
|
127
|
+
...agents,
|
|
128
|
+
changes: [configChange, ...agents.changes]
|
|
461
129
|
};
|
|
130
|
+
} else {
|
|
131
|
+
output.header('init');
|
|
132
|
+
prompts = createPrompts();
|
|
133
|
+
plan = await guidedPlan(cwd, config, prompts);
|
|
462
134
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
writeAgentSkillOutput(resolvedDeps.output, agentSkillResult);
|
|
467
|
-
writeAgentGuidanceOutput(resolvedDeps.output, agentGuidanceResult);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (error.code !== 'INIT_CANCELLED') throw error;
|
|
137
|
+
output.hint('Setup cancelled; no files changed.');
|
|
468
138
|
return {
|
|
469
|
-
status: '
|
|
470
|
-
|
|
139
|
+
status: 'cancelled',
|
|
140
|
+
mode,
|
|
141
|
+
exitCode: 130
|
|
471
142
|
};
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
})
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
143
|
+
} finally {
|
|
144
|
+
prompts?.close();
|
|
145
|
+
}
|
|
146
|
+
let changes = await applyChanges(plan.changes);
|
|
147
|
+
let result = {
|
|
148
|
+
status: initialized ? 'skipped' : 'created',
|
|
149
|
+
...(initialized ? {
|
|
150
|
+
reason: 'config_exists'
|
|
151
|
+
} : {}),
|
|
152
|
+
configPath: config.configPath,
|
|
153
|
+
mode,
|
|
154
|
+
plugins: [],
|
|
155
|
+
changes,
|
|
156
|
+
cloud: {
|
|
157
|
+
status: 'not_requested'
|
|
158
|
+
},
|
|
159
|
+
...(plan.agentSkill ? {
|
|
160
|
+
agentSkill: plan.agentSkill
|
|
161
|
+
} : {}),
|
|
162
|
+
...(plan.agentGuidance ? {
|
|
163
|
+
agentGuidance: plan.agentGuidance
|
|
164
|
+
} : {}),
|
|
165
|
+
exitCode: 0
|
|
166
|
+
};
|
|
167
|
+
if (plan.cloudProject) {
|
|
168
|
+
try {
|
|
169
|
+
result.cloud = await connectProject(plan.cloudProject, options);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
result.cloud = {
|
|
172
|
+
status: 'failed',
|
|
173
|
+
project: plan.cloudProject,
|
|
174
|
+
error: error.message
|
|
496
175
|
};
|
|
497
176
|
}
|
|
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
|
-
});
|
|
177
|
+
if (result.cloud.status !== 'linked') result.exitCode = 1;
|
|
512
178
|
}
|
|
179
|
+
output.configure({
|
|
180
|
+
json: options.json || false
|
|
181
|
+
});
|
|
182
|
+
showResult(result, options.json);
|
|
183
|
+
return result;
|
|
513
184
|
}
|