@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.
Files changed (40) hide show
  1. package/README.md +222 -183
  2. package/dist/auth/client.js +4 -1
  3. package/dist/auth/core.js +64 -3
  4. package/dist/auth/operations.js +21 -11
  5. package/dist/cli.js +45 -53
  6. package/dist/commands/init.js +156 -483
  7. package/dist/commands/login.js +71 -137
  8. package/dist/commands/project.js +128 -44
  9. package/dist/commands/run.js +2 -2
  10. package/dist/commands/tdd.js +89 -38
  11. package/dist/commands/upload.js +2 -2
  12. package/dist/init/agents.js +99 -0
  13. package/dist/init/core.js +31 -0
  14. package/dist/init/prompts.js +44 -0
  15. package/dist/reporter/reporter-bundle.css +1 -1
  16. package/dist/reporter/reporter-bundle.iife.js +31 -127
  17. package/dist/server/handlers/tdd-handler.js +33 -2
  18. package/dist/server/routers/auth.js +32 -42
  19. package/dist/services/index.js +8 -2
  20. package/dist/tdd/services/comparison-service.js +4 -1
  21. package/dist/tdd/tdd-service.js +14 -2
  22. package/dist/test-runner/core.js +28 -3
  23. package/dist/test-runner/operations.js +41 -20
  24. package/dist/types/index.d.ts +8 -0
  25. package/dist/uploader/core.js +1 -0
  26. package/dist/utils/browser.js +7 -1
  27. package/dist/utils/config-discovery.js +73 -0
  28. package/dist/utils/config-loader.js +29 -8
  29. package/dist/utils/config-schema.js +5 -0
  30. package/dist/utils/file-changes.js +105 -0
  31. package/dist/utils/login-poll-wait.js +50 -0
  32. package/dist/utils/project-binding.js +96 -0
  33. package/dist/utils/project-link-store.js +13 -7
  34. package/package.json +27 -27
  35. package/skills/vizzly/SKILL.md +4 -3
  36. package/skills/vizzly/references/ci.md +33 -0
  37. package/skills/vizzly/references/cli-context.md +81 -71
  38. package/skills/vizzly/references/dynamic-content.md +16 -17
  39. package/skills/vizzly/references/sdks.md +22 -22
  40. package/skills/vizzly/references/setup-ci.md +0 -44
@@ -1,513 +1,186 @@
1
- #!/usr/bin/env node
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 { z } from 'zod';
7
- import { VizzlyError } from '../errors/vizzly-error.js';
8
- import { loadPlugins } from '../plugin-loader.js';
9
- import { loadConfig } from '../utils/config-loader.js';
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
- let commandDir = path.dirname(fileURLToPath(import.meta.url));
12
- let configValueSchema = z.lazy(() => z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(configValueSchema), z.record(z.string(), configValueSchema)]));
13
- let configSchemaValidator = z.record(z.string(), configValueSchema);
14
- function createInitDeps(deps = {}) {
15
- return {
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
- 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}`;
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 appendSection(content, section) {
237
- if (!content.trim()) {
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 `${content.replace(/\s*$/, '')}\n\n${section}`;
47
+ return {
48
+ ...agents,
49
+ changes,
50
+ cloudProject
51
+ };
241
52
  }
242
- async function upsertProjectAgentGuidance({
243
- cwd,
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
- status: 'updated',
286
- agentsPath
56
+ guidance,
57
+ skill: guidance || !options.skipAgentSkill && Boolean(options.agentSkill)
287
58
  };
288
59
  }
289
- export async function installProjectAgentSkill({
290
- cwd,
291
- sourcePath,
292
- refresh = false,
293
- access = fs.access,
294
- copy = fs.cp,
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
- if (targetExists) {
323
- await remove(path.join(targetPath, 'agents', 'openai.yaml'), {
324
- force: true
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: targetExists ? 'refreshed' : 'installed',
329
- sourcePath,
330
- targetPath
76
+ status,
77
+ project
331
78
  };
332
79
  }
333
- function writeAgentSkillOutput(output, result) {
334
- if (!result) {
335
- return;
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
- if (result.status === 'malformed') {
379
- output.warn('Vizzly guidance markers in AGENTS.md are incomplete');
380
- output.hint(`Repair the managed block in ${result.agentsPath}`);
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 withAgentSetupResults(payload, {
384
- agentSkillResult,
385
- agentGuidanceResult
386
- }) {
387
- if (agentSkillResult) {
388
- payload.agentSkill = agentSkillResult;
389
- }
390
- if (agentGuidanceResult) {
391
- payload.agentGuidance = agentGuidanceResult;
392
- }
393
- return payload;
394
- }
395
- async function writeConfigFile({
396
- configPath,
397
- plugins,
398
- options,
399
- output,
400
- writeFile
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
- if (hasConfig && !options.force) {
447
- if (options.json) {
448
- let payload = withAgentSetupResults({
449
- status: 'skipped',
450
- reason: 'config_exists',
451
- configPath,
452
- message: 'A vizzly.config.js file already exists. Use --force to overwrite.'
453
- }, {
454
- agentSkillResult,
455
- agentGuidanceResult
456
- });
457
- resolvedDeps.output.data(payload);
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 = {
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
- resolvedDeps.output.header('init');
464
- resolvedDeps.output.warn('A vizzly.config.js file already exists');
465
- resolvedDeps.output.hint('Use --force to overwrite');
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: 'skipped',
470
- configPath
141
+ status: 'cancelled',
142
+ mode,
143
+ exitCode: 130
471
144
  };
472
- }
473
- try {
474
- await writeConfigFile({
475
- configPath,
476
- plugins,
477
- options,
478
- output: resolvedDeps.output,
479
- writeFile: resolvedDeps.writeFile
480
- });
481
- let pluginNames = getPluginConfigNames(plugins);
482
- if (options.json) {
483
- let payload = withAgentSetupResults({
484
- status: 'created',
485
- configPath,
486
- plugins: pluginNames
487
- }, {
488
- agentSkillResult,
489
- agentGuidanceResult
490
- });
491
- resolvedDeps.output.data(payload);
492
- return {
493
- status: 'created',
494
- configPath,
495
- plugins: pluginNames
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
- showNextSteps(resolvedDeps.output);
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
  }