@vizzly-testing/cli 0.39.0 → 0.39.2

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 (195) hide show
  1. package/README.md +12 -3
  2. package/bin/vizzly.js +1 -1
  3. package/dist/{cli.js → adapters/cli/cli.js} +50 -35
  4. package/dist/adapters/cli/setup.js +34 -0
  5. package/dist/{client → adapters/client}/index.js +6 -7
  6. package/dist/adapters/http/cloud-proxy.js +29 -0
  7. package/dist/{server → adapters/http}/http-server.js +12 -13
  8. package/dist/{server-manager/operations.js → adapters/http/server-lifecycle.js} +10 -13
  9. package/dist/{server-manager/index.js → adapters/http/server-manager.js} +23 -27
  10. package/dist/{plugin-api.js → adapters/plugins/plugin-api.js} +1 -1
  11. package/dist/{plugin-loader.js → adapters/plugins/plugin-loader.js} +1 -1
  12. package/dist/{services/index.js → adapters/plugins/services.js} +4 -9
  13. package/dist/adapters/sdk/index.d.ts +89 -0
  14. package/dist/{sdk → adapters/sdk}/index.js +22 -19
  15. package/dist/{commands/api.js → domains/api/api.commands.js} +3 -3
  16. package/dist/{auth/client.js → domains/auth/auth-client.js} +4 -3
  17. package/dist/domains/auth/auth-errors.js +50 -0
  18. package/dist/domains/auth/auth-headers.js +41 -0
  19. package/dist/domains/auth/auth.normalizers.js +50 -0
  20. package/dist/{server/routers/auth.js → domains/auth/auth.routes.js} +9 -9
  21. package/dist/domains/auth/auth.rules.js +19 -0
  22. package/dist/domains/auth/auth.serializers.js +32 -0
  23. package/dist/{auth/operations.js → domains/auth/auth.service.js} +115 -17
  24. package/dist/domains/auth/auth.validations.js +21 -0
  25. package/dist/{api/client.js → domains/auth/cloud-client.js} +11 -18
  26. package/dist/{commands/login.js → domains/auth/login.commands.js} +11 -8
  27. package/dist/{commands/logout.js → domains/auth/logout.commands.js} +6 -4
  28. package/dist/domains/auth/token-store.js +81 -0
  29. package/dist/{commands/whoami.js → domains/auth/whoami.commands.js} +6 -3
  30. package/dist/domains/baselines/baseline-download.js +192 -0
  31. package/dist/domains/baselines/baseline-write.js +22 -0
  32. package/dist/{server/routers/baseline.js → domains/baselines/baseline.routes.js} +3 -3
  33. package/dist/{tdd/services/baseline-manager.js → domains/baselines/baseline.service.js} +1 -39
  34. package/dist/{commands/baselines.js → domains/baselines/baselines.commands.js} +3 -3
  35. package/dist/domains/baselines/cloud-baselines.js +9 -0
  36. package/dist/domains/baselines/cloud-baselines.routes.js +42 -0
  37. package/dist/domains/builds/build.normalizers.js +35 -0
  38. package/dist/domains/builds/build.serializers.js +164 -0
  39. package/dist/domains/builds/build.service.js +119 -0
  40. package/dist/domains/builds/build.validations.js +39 -0
  41. package/dist/{commands/builds.js → domains/builds/builds.commands.js} +6 -74
  42. package/dist/domains/builds/cloud-builds.routes.js +31 -0
  43. package/dist/{commands/finalize.js → domains/builds/finalize.commands.js} +5 -17
  44. package/dist/{commands/status.js → domains/builds/status.commands.js} +9 -120
  45. package/dist/domains/comparisons/cloud-comparisons.js +43 -0
  46. package/dist/{tdd/services/comparison-service.js → domains/comparisons/comparison.service.js} +1 -1
  47. package/dist/{commands/comparisons.js → domains/comparisons/comparisons.commands.js} +7 -5
  48. package/dist/domains/config/config-defaults.js +27 -0
  49. package/dist/{utils → domains/config}/config-discovery.js +1 -9
  50. package/dist/{utils → domains/config}/config-loader.js +10 -9
  51. package/dist/{commands/config-cmd.js → domains/config/config.commands.js} +4 -4
  52. package/dist/{types → domains/config}/config.d.ts +3 -3
  53. package/dist/domains/config/config.normalizers.js +126 -0
  54. package/dist/{server/routers/config.js → domains/config/config.routes.js} +13 -13
  55. package/dist/domains/config/config.rules.js +47 -0
  56. package/dist/domains/config/config.serializers.js +202 -0
  57. package/dist/domains/config/config.service.js +625 -0
  58. package/dist/{utils/config-schema.js → domains/config/config.validations.js} +9 -9
  59. package/dist/{utils/environment-config.js → domains/config/environment.js} +1 -8
  60. package/dist/domains/context/compact-context.validations.js +60 -0
  61. package/dist/domains/context/context-presentation.js +368 -0
  62. package/dist/domains/context/context.commands.js +385 -0
  63. package/dist/domains/context/context.serializers.js +157 -0
  64. package/dist/domains/context/context.service.js +76 -0
  65. package/dist/domains/context/context.validations.js +100 -0
  66. package/dist/{context → domains/context}/local-workspace-provider.js +1 -1
  67. package/dist/{commands/doctor.js → domains/diagnostics/doctor.commands.js} +6 -5
  68. package/dist/{server/routers/health.js → domains/diagnostics/health.routes.js} +11 -20
  69. package/dist/{commands/preview.js → domains/previews/preview.commands.js} +9 -7
  70. package/dist/domains/previews/preview.service.js +37 -0
  71. package/dist/domains/projects/cloud-projects.routes.js +28 -0
  72. package/dist/{commands/orgs.js → domains/projects/orgs.commands.js} +6 -8
  73. package/dist/{utils → domains/projects}/project-binding.js +3 -3
  74. package/dist/domains/projects/project-errors.js +61 -0
  75. package/dist/{utils → domains/projects}/project-link-store.js +2 -2
  76. package/dist/domains/projects/project-requests.js +61 -0
  77. package/dist/{commands/project.js → domains/projects/project.commands.js} +9 -41
  78. package/dist/domains/projects/project.normalizers.js +69 -0
  79. package/dist/{project/operations.js → domains/projects/project.service.js} +89 -16
  80. package/dist/domains/projects/project.validations.js +31 -0
  81. package/dist/{commands/projects.js → domains/projects/projects.commands.js} +6 -8
  82. package/dist/{server/routers/projects.js → domains/projects/projects.routes.js} +5 -5
  83. package/dist/domains/reports/dashboard.routes.js +96 -0
  84. package/dist/{server/routers/events.js → domains/reports/events.routes.js} +14 -36
  85. package/dist/domains/reports/report-assets.routes.js +81 -0
  86. package/dist/domains/reports/report-state.js +51 -0
  87. package/dist/domains/reports/report-store.js +85 -0
  88. package/dist/domains/reports/report.serializers.js +99 -0
  89. package/dist/{services/static-report-generator.js → domains/reports/report.service.js} +11 -33
  90. package/dist/{commands/review.js → domains/review/review.commands.js} +4 -4
  91. package/dist/domains/runs/run-environment.js +69 -0
  92. package/dist/{services/test-runner.js → domains/runs/run-events.js} +11 -9
  93. package/dist/{commands/run.js → domains/runs/run.commands.js} +16 -13
  94. package/dist/domains/runs/run.normalizers.js +8 -0
  95. package/dist/domains/runs/run.rules.js +56 -0
  96. package/dist/domains/runs/run.serializers.js +101 -0
  97. package/dist/{test-runner/operations.js → domains/runs/run.service.js} +35 -7
  98. package/dist/domains/runs/run.validations.js +17 -0
  99. package/dist/{utils/session.js → domains/runs/session-store.js} +3 -2
  100. package/dist/{server/handlers/api-handler.js → domains/screenshots/cloud-handler.js} +4 -4
  101. package/dist/domains/screenshots/cloud-screenshots.js +109 -0
  102. package/dist/{utils/file-helpers.js → domains/screenshots/image-files.js} +1 -1
  103. package/dist/domains/screenshots/screenshot-hash.js +53 -0
  104. package/dist/domains/screenshots/screenshot-server-options.js +28 -0
  105. package/dist/{screenshot-server/operations.js → domains/screenshots/screenshot-server.js} +14 -1
  106. package/dist/domains/screenshots/screenshot.normalizers.js +40 -0
  107. package/dist/{server/routers/screenshot.js → domains/screenshots/screenshot.routes.js} +4 -4
  108. package/dist/domains/screenshots/screenshot.serializers.js +146 -0
  109. package/dist/{utils/security.js → domains/screenshots/screenshot.validations.js} +20 -70
  110. package/dist/{init/agents.js → domains/setup/agent-setup.js} +54 -14
  111. package/dist/{commands/init.js → domains/setup/init.commands.js} +11 -44
  112. package/dist/{server/handlers/tdd-handler.js → domains/tdd/screenshot-handler.js} +46 -281
  113. package/dist/{commands/tdd-daemon.js → domains/tdd/tdd-daemon.commands.js} +6 -6
  114. package/dist/{commands/tdd-screenshots.js → domains/tdd/tdd-screenshots.commands.js} +5 -5
  115. package/dist/{commands/tdd.js → domains/tdd/tdd.commands.js} +16 -15
  116. package/dist/{tdd/tdd-service.js → domains/tdd/tdd.service.js} +81 -405
  117. package/dist/domains/updates/cli-update.js +103 -0
  118. package/dist/domains/updates/update-check.js +2 -0
  119. package/dist/domains/updates/update-notifications.js +175 -0
  120. package/dist/domains/uploads/upload-defaults.js +4 -0
  121. package/dist/domains/uploads/upload-files.js +32 -0
  122. package/dist/domains/uploads/upload-settings.js +21 -0
  123. package/dist/domains/uploads/upload-timing.js +18 -0
  124. package/dist/{commands/upload.js → domains/uploads/upload.commands.js} +11 -9
  125. package/dist/domains/uploads/upload.normalizers.js +60 -0
  126. package/dist/domains/uploads/upload.serializers.js +179 -0
  127. package/dist/{uploader/operations.js → domains/uploads/upload.service.js} +85 -28
  128. package/dist/domains/uploads/upload.validations.js +77 -0
  129. package/dist/index.js +12 -12
  130. package/dist/reporter/reporter-bundle.iife.js +86 -86
  131. package/dist/reporter-ssr/ssr-entry.js +3 -6
  132. package/dist/shared/cloud-protocol.js +171 -0
  133. package/dist/{utils → shared}/file-changes.js +1 -1
  134. package/dist/shared/files.js +9 -0
  135. package/dist/{utils → shared}/global-config.js +7 -78
  136. package/dist/shared/log-level.js +7 -0
  137. package/dist/{utils → shared}/output.js +1 -1
  138. package/dist/{utils → shared}/package-info.js +5 -4
  139. package/dist/shared/paths.js +57 -0
  140. package/package.json +37 -36
  141. package/dist/api/core.js +0 -382
  142. package/dist/api/endpoints.js +0 -421
  143. package/dist/api/index.js +0 -19
  144. package/dist/auth/core.js +0 -237
  145. package/dist/auth/index.js +0 -31
  146. package/dist/commands/context.js +0 -1066
  147. package/dist/config/core.js +0 -439
  148. package/dist/config/operations.js +0 -327
  149. package/dist/project/core.js +0 -211
  150. package/dist/screenshot-server/core.js +0 -161
  151. package/dist/screenshot-server/index.js +0 -11
  152. package/dist/server/routers/assets.js +0 -95
  153. package/dist/server/routers/cloud-proxy.js +0 -146
  154. package/dist/server/routers/dashboard.js +0 -172
  155. package/dist/services/auth-service.js +0 -117
  156. package/dist/services/config-service.js +0 -313
  157. package/dist/services/project-service.js +0 -88
  158. package/dist/test-runner/core.js +0 -293
  159. package/dist/test-runner/index.js +0 -13
  160. package/dist/types/sdk.d.ts +0 -186
  161. package/dist/uploader/core.js +0 -432
  162. package/dist/uploader/index.js +0 -88
  163. /package/dist/{types/client.d.ts → adapters/client/index.d.ts} +0 -0
  164. /package/dist/{server → adapters/http}/middleware/cors.js +0 -0
  165. /package/dist/{server → adapters/http}/middleware/json-parser.js +0 -0
  166. /package/dist/{server → adapters/http}/middleware/response.js +0 -0
  167. /package/dist/{server-manager/core.js → adapters/http/server-options.js} +0 -0
  168. /package/dist/{utils/login-poll-wait.js → domains/auth/login-wait.js} +0 -0
  169. /package/dist/{tdd/metadata → domains/baselines}/baseline-metadata.js +0 -0
  170. /package/dist/{services/build-manager.js → domains/builds/build-info.js} +0 -0
  171. /package/dist/{tdd/services/result-service.js → domains/comparisons/comparison-results.js} +0 -0
  172. /package/dist/{init/core.js → domains/config/config-plan.js} +0 -0
  173. /package/dist/{utils/config-helpers.js → domains/config/config.js} +0 -0
  174. /package/dist/{utils/visual-context-normalizers.js → domains/context/context.normalizers.js} +0 -0
  175. /package/dist/{context → domains/context}/provider-resolver.js +0 -0
  176. /package/dist/{utils/context.js → domains/diagnostics/environment-context.js} +0 -0
  177. /package/dist/{utils/report-data.js → domains/reports/report.normalizers.js} +0 -0
  178. /package/dist/{utils/image-input-detector.js → domains/screenshots/image-input.js} +0 -0
  179. /package/dist/{tdd/core/signature.js → domains/screenshots/screenshot-identity.js} +0 -0
  180. /package/dist/{utils → domains/screenshots}/screenshot-options.js +0 -0
  181. /package/dist/{init/prompts.js → domains/setup/setup-prompts.js} +0 -0
  182. /package/dist/{tdd → domains/tdd}/runtime-context.js +0 -0
  183. /package/dist/{tdd → domains/tdd}/server-registry.js +0 -0
  184. /package/dist/{commands/tdd-daemon-process.js → domains/tdd/tdd-daemon-process.commands.js} +0 -0
  185. /package/dist/{types/index.d.ts → index.d.ts} +0 -0
  186. /package/dist/{utils → shared}/api-url.js +0 -0
  187. /package/dist/{utils → shared}/async-utils.js +0 -0
  188. /package/dist/{utils → shared}/browser.js +0 -0
  189. /package/dist/{utils → shared}/ci-env.js +0 -0
  190. /package/dist/{utils → shared}/colors.js +0 -0
  191. /package/dist/{utils → shared}/fetch-utils.js +0 -0
  192. /package/dist/{utils → shared}/git.js +0 -0
  193. /package/dist/{utils → shared}/patterns.js +0 -0
  194. /package/dist/{utils → shared}/secret-store.js +0 -0
  195. /package/dist/{errors → shared}/vizzly-error.js +0 -0
@@ -0,0 +1,100 @@
1
+ function buildSourceErrorMessage() {
2
+ return '--source must be one of: auto, cloud, local';
3
+ }
4
+ function buildIncludeErrorMessage() {
5
+ return '--include must contain only: diffs';
6
+ }
7
+ function validateLimitRange(value, flagName, {
8
+ min = 1,
9
+ max
10
+ }) {
11
+ if (value == null) {
12
+ return [];
13
+ }
14
+ if (!Number.isInteger(value) || value < min || value > max) {
15
+ return [`${flagName} must be an integer between ${min} and ${max}`];
16
+ }
17
+ return [];
18
+ }
19
+ function validateCursor(value) {
20
+ if (value == null) {
21
+ return [];
22
+ }
23
+ if (typeof value !== 'string' || value.trim().length === 0) {
24
+ return ['--cursor must be a non-empty opaque cursor'];
25
+ }
26
+ return [];
27
+ }
28
+ function validateOffset(value) {
29
+ if (value == null) return [];
30
+ if (!Number.isInteger(value) || value < 0) {
31
+ return ['--offset must be a non-negative integer'];
32
+ }
33
+ return [];
34
+ }
35
+ function validateSourceOption(value) {
36
+ if (value == null) {
37
+ return [];
38
+ }
39
+ if (!['auto', 'cloud', 'local'].includes(value)) {
40
+ return [buildSourceErrorMessage()];
41
+ }
42
+ return [];
43
+ }
44
+ export function parseIncludeOption(value) {
45
+ if (!value) {
46
+ return [];
47
+ }
48
+ let rawItems = Array.isArray(value) ? value : String(value).split(',');
49
+ return rawItems.map(item => item.trim()).filter(Boolean);
50
+ }
51
+ function validateIncludeOption(value) {
52
+ let allowed = new Set(['diffs']);
53
+ let invalid = parseIncludeOption(value).filter(item => !allowed.has(item));
54
+ return invalid.length > 0 ? [buildIncludeErrorMessage()] : [];
55
+ }
56
+ function validateScopedProjectOptions(options = {}) {
57
+ let errors = [];
58
+ if (options.org && !options.project) {
59
+ errors.push('--org requires --project');
60
+ }
61
+ return errors;
62
+ }
63
+ export function validateContextBuildOptions(_options = {}) {
64
+ let errors = validateSourceOption(_options.source);
65
+ errors.push(...validateIncludeOption(_options.include));
66
+ errors.push(...validateCursor(_options.cursor));
67
+ return errors;
68
+ }
69
+ export function validateContextComparisonOptions(options = {}) {
70
+ let errors = [];
71
+ errors.push(...validateSourceOption(options.source));
72
+ errors.push(...validateIncludeOption(options.include));
73
+ errors.push(...validateCursor(options.cursor));
74
+ return errors;
75
+ }
76
+ export function validateContextScreenshotOptions(options = {}) {
77
+ let errors = validateScopedProjectOptions(options);
78
+ errors.push(...validateSourceOption(options.source));
79
+ errors.push(...validateLimitRange(options.recentLimit, '--recent-limit', {
80
+ max: 50
81
+ }));
82
+ return errors;
83
+ }
84
+ export function validateContextSimilarOptions(options = {}) {
85
+ let errors = validateScopedProjectOptions(options);
86
+ errors.push(...validateSourceOption(options.source));
87
+ errors.push(...validateLimitRange(options.limit, '--limit', {
88
+ max: 50
89
+ }));
90
+ return errors;
91
+ }
92
+ export function validateContextReviewQueueOptions(options = {}) {
93
+ let errors = validateScopedProjectOptions(options);
94
+ errors.push(...validateSourceOption(options.source));
95
+ errors.push(...validateLimitRange(options.limit, '--limit', {
96
+ max: 100
97
+ }));
98
+ errors.push(...validateOffset(options.offset));
99
+ return errors;
100
+ }
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { existsSync as defaultExistsSync, readFileSync } from 'node:fs';
3
3
  import { basename, isAbsolute, join } from 'node:path';
4
4
  import { pathToFileURL } from 'node:url';
5
- import { normalizeReportData } from '../utils/report-data.js';
5
+ import { normalizeReportData } from '../reports/report.normalizers.js';
6
6
  let LOCAL_CONTEXT_SOURCE = 'local_workspace';
7
7
  let DEFAULT_LOCAL_REVIEW_QUEUE_LIMIT = 50;
8
8
  function readJsonIfExists(path) {
@@ -1,9 +1,10 @@
1
1
  import { URL } from 'node:url';
2
- import { createApiClient as defaultCreateApiClient, getBuilds as defaultGetBuilds } from '../api/index.js';
3
- import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
4
- import { getContext as defaultGetContext } from '../utils/context.js';
5
- import { getApiToken as defaultGetApiToken } from '../utils/environment-config.js';
6
- import * as defaultOutput from '../utils/output.js';
2
+ import * as defaultOutput from '../../shared/output.js';
3
+ import { createApiClient as defaultCreateApiClient } from '../auth/cloud-client.js';
4
+ import { getBuilds as defaultGetBuilds } from '../builds/build.service.js';
5
+ import { loadConfig as defaultLoadConfig } from '../config/config-loader.js';
6
+ import { getApiToken as defaultGetApiToken } from '../config/environment.js';
7
+ import { getContext as defaultGetContext } from './environment-context.js';
7
8
  export let MIN_NODE_MAJOR = 22;
8
9
  export function createDoctorDiagnostics() {
9
10
  return {
@@ -3,43 +3,34 @@
3
3
  * Health check endpoint with diagnostics
4
4
  */
5
5
 
6
- import { existsSync, readFileSync } from 'node:fs';
7
- import { join } from 'node:path';
8
- import { sendSuccess } from '../middleware/response.js';
6
+ import { sendSuccess } from '../../adapters/http/middleware/response.js';
7
+ import { createReportStore } from '../reports/report-store.js';
9
8
 
10
9
  /**
11
10
  * Create health router
12
11
  * @param {Object} context - Router context
13
12
  * @param {number} context.port - Server port
14
13
  * @param {Object} context.screenshotHandler - Screenshot handler
14
+ * @param {string} [context.workingDir] - Workspace containing report artifacts
15
15
  * @returns {Function} Route handler
16
16
  */
17
17
  export function createHealthRouter({
18
18
  port,
19
- screenshotHandler
19
+ screenshotHandler,
20
+ workingDir = process.cwd()
20
21
  }) {
22
+ let reports = createReportStore(workingDir);
21
23
  return async function handleHealthRoute(req, res, pathname) {
22
24
  if (req.method !== 'GET' || pathname !== '/health') {
23
25
  return false;
24
26
  }
25
- const reportDataPath = join(process.cwd(), '.vizzly', 'report-data.json');
26
- const baselineMetadataPath = join(process.cwd(), '.vizzly', 'baselines', 'metadata.json');
27
27
  let reportData = null;
28
- let baselineInfo = null;
29
- if (existsSync(reportDataPath)) {
30
- try {
31
- reportData = JSON.parse(readFileSync(reportDataPath, 'utf8'));
32
- } catch {
33
- // Ignore read errors
34
- }
35
- }
36
- if (existsSync(baselineMetadataPath)) {
37
- try {
38
- baselineInfo = JSON.parse(readFileSync(baselineMetadataPath, 'utf8'));
39
- } catch {
40
- // Ignore read errors
41
- }
28
+ try {
29
+ reportData = reports.readReport();
30
+ } catch {
31
+ // Health remains available when report artifacts cannot be read.
42
32
  }
33
+ let baselineInfo = reports.readBaselineMetadata();
43
34
  sendSuccess(res, {
44
35
  status: 'ok',
45
36
  port,
@@ -12,13 +12,15 @@ import { readdir, readFile, realpath, stat, unlink } from 'node:fs/promises';
12
12
  import { tmpdir } from 'node:os';
13
13
  import { join, resolve } from 'node:path';
14
14
  import { promisify } from 'node:util';
15
- import { createApiClient as defaultCreateApiClient, getBuild as defaultGetBuild, uploadPreviewZip as defaultUploadPreviewZip } from '../api/index.js';
16
- import { openBrowser as defaultOpenBrowser } from '../utils/browser.js';
17
- import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
18
- import { detectBranch as defaultDetectBranch } from '../utils/git.js';
19
- import * as defaultOutput from '../utils/output.js';
20
- import { createWildcardMatcher } from '../utils/patterns.js';
21
- import { formatSessionAge as defaultFormatSessionAge, readSession as defaultReadSession } from '../utils/session.js';
15
+ import { openBrowser as defaultOpenBrowser } from '../../shared/browser.js';
16
+ import { detectBranch as defaultDetectBranch } from '../../shared/git.js';
17
+ import * as defaultOutput from '../../shared/output.js';
18
+ import { createWildcardMatcher } from '../../shared/patterns.js';
19
+ import { createApiClient as defaultCreateApiClient } from '../auth/cloud-client.js';
20
+ import { getBuild as defaultGetBuild } from '../builds/build.service.js';
21
+ import { loadConfig as defaultLoadConfig } from '../config/config-loader.js';
22
+ import { formatSessionAge as defaultFormatSessionAge, readSession as defaultReadSession } from '../runs/session-store.js';
23
+ import { uploadPreviewZip as defaultUploadPreviewZip } from './preview.service.js';
22
24
  let execFileAsync = promisify(execFile);
23
25
 
24
26
  // Maximum files to show in dry-run output (use --verbose for all)
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Upload preview ZIP file for a build
3
+ * @param {Object} client - API client
4
+ * @param {string} buildId - Build ID
5
+ * @param {Buffer} zipBuffer - ZIP file contents
6
+ * @returns {Promise<Object>} Upload result with preview URL
7
+ */
8
+ export async function uploadPreviewZip(client, buildId, zipBuffer) {
9
+ // Use native FormData (Node 18+) with Blob for proper fetch compatibility
10
+ let formData = new FormData();
11
+ let blob = new Blob([zipBuffer], {
12
+ type: 'application/zip'
13
+ });
14
+ formData.append('file', blob, 'preview.zip');
15
+ return client.request(`/api/sdk/builds/${buildId}/preview/upload-zip`, {
16
+ method: 'POST',
17
+ body: formData
18
+ // Let fetch set the Content-Type with boundary automatically
19
+ });
20
+ }
21
+
22
+ /**
23
+ * Get preview info for a build
24
+ * @param {Object} client - API client
25
+ * @param {string} buildId - Build ID
26
+ * @returns {Promise<Object>} Preview info or null if not found
27
+ */
28
+ export async function getPreviewInfo(client, buildId) {
29
+ try {
30
+ return await client.request(`/api/sdk/builds/${buildId}/preview`);
31
+ } catch (error) {
32
+ if (error.context?.status === 404) {
33
+ return null;
34
+ }
35
+ throw error;
36
+ }
37
+ }
@@ -0,0 +1,28 @@
1
+ import { sendError, sendSuccess } from '../../adapters/http/middleware/response.js';
2
+ import * as output from '../../shared/output.js';
3
+ export function createCloudProjectsRouter({
4
+ authService
5
+ }) {
6
+ return async function handleCloudProjectsRoute(req, res, pathname) {
7
+ if (req.method !== 'GET' || pathname !== '/api/cloud/projects') return false;
8
+ try {
9
+ let response = await authService.authenticatedRequest('/api/cli/projects', {
10
+ method: 'GET'
11
+ });
12
+ sendSuccess(res, {
13
+ projects: response.projects || []
14
+ });
15
+ } catch (error) {
16
+ output.error('Error fetching projects from cloud:', error);
17
+ if (error.message?.toLowerCase().includes('not authenticated') || error.code === 'AUTH_ERROR') {
18
+ sendSuccess(res, {
19
+ projects: [],
20
+ authenticated: false
21
+ });
22
+ } else {
23
+ sendError(res, 500, error.message);
24
+ }
25
+ }
26
+ return true;
27
+ };
28
+ }
@@ -1,13 +1,12 @@
1
+ import * as defaultOutput from '../../shared/output.js';
2
+ import { createApiClient as defaultCreateApiClient } from '../auth/cloud-client.js';
3
+ import { loadConfig as defaultLoadConfig } from '../config/config-loader.js';
4
+ import { getApiUrl as defaultGetApiUrl } from '../config/environment.js';
5
+
1
6
  /**
2
7
  * Organizations command - List organizations the user has access to
3
8
  */
4
9
 
5
- import { createApiClient as defaultCreateApiClient } from '../api/client.js';
6
- import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
7
- import { getApiUrl as defaultGetApiUrl } from '../utils/environment-config.js';
8
- import { getAccessToken as defaultGetAccessToken } from '../utils/global-config.js';
9
- import * as defaultOutput from '../utils/output.js';
10
-
11
10
  /**
12
11
  * Organizations command implementation
13
12
  * @param {Object} options - Command options
@@ -19,7 +18,6 @@ export async function orgsCommand(_options = {}, globalOptions = {}, deps = {})
19
18
  loadConfig = defaultLoadConfig,
20
19
  createApiClient = defaultCreateApiClient,
21
20
  getApiUrl = defaultGetApiUrl,
22
- getAccessToken = defaultGetAccessToken,
23
21
  output = defaultOutput,
24
22
  exit = code => process.exit(code)
25
23
  } = deps;
@@ -33,7 +31,7 @@ export async function orgsCommand(_options = {}, globalOptions = {}, deps = {})
33
31
 
34
32
  // Prefer user auth token for listing orgs (project tokens are org-scoped).
35
33
  // Falls back to config.apiKey which may be: VIZZLY_TOKEN, --token flag, or project token.
36
- let token = (await getAccessToken()) || config.apiKey;
34
+ let token = config.userToken || config.apiKey;
37
35
  if (!token) {
38
36
  output.error('API token required. Use --token, set VIZZLY_TOKEN, or run "vizzly login"');
39
37
  output.cleanup();
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
- import { VizzlyError } from '../errors/vizzly-error.js';
3
- import { discoverConfig } from './config-discovery.js';
4
- import { applyChanges } from './file-changes.js';
2
+ import { applyChanges } from '../../shared/file-changes.js';
3
+ import { VizzlyError } from '../../shared/vizzly-error.js';
4
+ import { discoverConfig } from '../config/config-discovery.js';
5
5
  function sameProject(left, right) {
6
6
  return left?.organizationSlug === right.organizationSlug && left?.projectSlug === right.projectSlug;
7
7
  }
@@ -0,0 +1,61 @@
1
+ import { VizzlyError } from '../../shared/vizzly-error.js';
2
+
3
+ /**
4
+ * Build error for project fetch failure
5
+ * @param {Error} originalError - Original error
6
+ * @returns {VizzlyError} Wrapped error
7
+ */
8
+ export function buildProjectFetchError(originalError) {
9
+ return new VizzlyError(`Failed to fetch project: ${originalError.message}`, 'PROJECT_FETCH_FAILED', {
10
+ originalError
11
+ });
12
+ }
13
+
14
+ /**
15
+ * Build error for no authentication available
16
+ * @returns {VizzlyError} No auth error
17
+ */
18
+ export function buildNoAuthError() {
19
+ return new VizzlyError('No authentication available', 'NO_AUTH_SERVICE');
20
+ }
21
+
22
+ /**
23
+ * Build error for no API service available
24
+ * @returns {VizzlyError} No API service error
25
+ */
26
+ export function buildNoApiServiceError() {
27
+ return new VizzlyError('API service not available', 'NO_API_SERVICE');
28
+ }
29
+
30
+ /**
31
+ * Build error for token creation failure
32
+ * @param {Error} originalError - Original error
33
+ * @returns {VizzlyError} Wrapped error
34
+ */
35
+ export function buildTokenCreateError(originalError) {
36
+ return new VizzlyError(`Failed to create project token: ${originalError.message}`, 'TOKEN_CREATE_FAILED', {
37
+ originalError
38
+ });
39
+ }
40
+
41
+ /**
42
+ * Build error for token fetch failure
43
+ * @param {Error} originalError - Original error
44
+ * @returns {VizzlyError} Wrapped error
45
+ */
46
+ export function buildTokensFetchError(originalError) {
47
+ return new VizzlyError(`Failed to fetch project tokens: ${originalError.message}`, 'TOKENS_FETCH_FAILED', {
48
+ originalError
49
+ });
50
+ }
51
+
52
+ /**
53
+ * Build error for token revoke failure
54
+ * @param {Error} originalError - Original error
55
+ * @returns {VizzlyError} Wrapped error
56
+ */
57
+ export function buildTokenRevokeError(originalError) {
58
+ return new VizzlyError(`Failed to revoke project token: ${originalError.message}`, 'TOKEN_REVOKE_FAILED', {
59
+ originalError
60
+ });
61
+ }
@@ -1,5 +1,5 @@
1
- import { loadGlobalConfig, saveGlobalConfig } from './global-config.js';
2
- import { deleteSecret as defaultDeleteSecret, getSecret as defaultGetSecret, saveSecret as defaultSaveSecret } from './secret-store.js';
1
+ import { loadGlobalConfig, saveGlobalConfig } from '../../shared/global-config.js';
2
+ import { deleteSecret as defaultDeleteSecret, getSecret as defaultGetSecret, saveSecret as defaultSaveSecret } from '../../shared/secret-store.js';
3
3
  export function buildProjectLinkAccount({
4
4
  apiUrl,
5
5
  organizationSlug,
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Build query params for builds API request
3
+ * @param {Object} options - Query options
4
+ * @param {number} [options.limit] - Number of builds to fetch
5
+ * @param {string} [options.branch] - Filter by branch
6
+ * @returns {string} Query string (empty string if no params)
7
+ */
8
+ export function buildBuildsQueryParams(options = {}) {
9
+ let params = new globalThis.URLSearchParams();
10
+ if (options.limit) {
11
+ params.append('limit', String(options.limit));
12
+ }
13
+ if (options.branch) {
14
+ params.append('branch', options.branch);
15
+ }
16
+ let query = params.toString();
17
+ return query ? `?${query}` : '';
18
+ }
19
+
20
+ /**
21
+ * Build organization header object
22
+ * @param {string} organizationSlug - Organization slug
23
+ * @returns {Object} Headers object with X-Organization header
24
+ */
25
+ export function buildOrgHeader(organizationSlug) {
26
+ return {
27
+ 'X-Organization': organizationSlug
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Build API URL for project endpoint
33
+ * @param {string} projectSlug - Project slug
34
+ * @returns {string} API URL path
35
+ */
36
+ export function buildProjectUrl(projectSlug) {
37
+ return `/api/project/${projectSlug}`;
38
+ }
39
+
40
+ /**
41
+ * Build API URL for builds endpoint
42
+ * @param {string} projectSlug - Project slug
43
+ * @param {Object} options - Query options
44
+ * @returns {string} Full API URL path with query params
45
+ */
46
+ export function buildBuildsUrl(projectSlug, options = {}) {
47
+ let queryString = buildBuildsQueryParams(options);
48
+ return `/api/build/${projectSlug}${queryString}`;
49
+ }
50
+
51
+ /**
52
+ * Build API URL for project tokens endpoint
53
+ * @param {string} organizationSlug - Organization slug
54
+ * @param {string} projectSlug - Project slug
55
+ * @param {string} [tokenId] - Optional token ID for specific token operations
56
+ * @returns {string} API URL path
57
+ */
58
+ export function buildTokensUrl(_organizationSlug, projectSlug, tokenId) {
59
+ let base = `/api/project/${projectSlug}/tokens`;
60
+ return tokenId ? `${base}/${tokenId}` : base;
61
+ }
@@ -1,46 +1,14 @@
1
- import { createApiClient as defaultCreateApiClient } from '../api/client.js';
2
- import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
3
- import { getApiUrl as defaultGetApiUrl } from '../utils/environment-config.js';
4
- import { preflightChanges } from '../utils/file-changes.js';
5
- import { getAccessToken as defaultGetAccessToken } from '../utils/global-config.js';
6
- import * as defaultOutput from '../utils/output.js';
7
- import { planProjectBinding as defaultPlanProjectBinding, writeProjectBinding as defaultWriteProjectBinding } from '../utils/project-binding.js';
8
- import { getProjectLink as defaultGetProjectLink, saveProjectLink as defaultSaveProjectLink } from '../utils/project-link-store.js';
9
- export function parseProjectSelector(selector, options = {}) {
10
- let organizationSlug = options.org || null;
11
- let projectSlug = options.project || null;
12
- if (selector) {
13
- let parts = selector.split('/');
14
- if (parts.length === 2) {
15
- organizationSlug = organizationSlug || parts[0];
16
- projectSlug = projectSlug || parts[1];
17
- } else if (parts.length === 1) {
18
- projectSlug = projectSlug || parts[0];
19
- }
20
- }
21
- return {
22
- organizationSlug,
23
- projectSlug
24
- };
25
- }
26
- export function validateProjectLinkOptions(selector, options = {}) {
27
- let errors = [];
28
- let {
29
- organizationSlug,
30
- projectSlug
31
- } = parseProjectSelector(selector, options);
32
- if (!organizationSlug) {
33
- errors.push('Organization is required. Use <org>/<project> or --org <slug>.');
34
- }
35
- if (!projectSlug) {
36
- errors.push('Project is required. Use <org>/<project> or --project <slug>.');
37
- }
38
- return errors;
39
- }
1
+ import { preflightChanges } from '../../shared/file-changes.js';
2
+ import * as defaultOutput from '../../shared/output.js';
3
+ import { createApiClient as defaultCreateApiClient } from '../auth/cloud-client.js';
4
+ import { loadConfig as defaultLoadConfig } from '../config/config-loader.js';
5
+ import { getApiUrl as defaultGetApiUrl } from '../config/environment.js';
6
+ import { parseProjectSelector } from './project.validations.js';
7
+ import { planProjectBinding as defaultPlanProjectBinding, writeProjectBinding as defaultWriteProjectBinding } from './project-binding.js';
8
+ import { getProjectLink as defaultGetProjectLink, saveProjectLink as defaultSaveProjectLink } from './project-link-store.js';
40
9
  export async function projectLinkCommand(selector, options = {}, globalOptions = {}, deps = {}) {
41
10
  let {
42
11
  createApiClient = defaultCreateApiClient,
43
- getAccessToken = defaultGetAccessToken,
44
12
  getApiUrl = defaultGetApiUrl,
45
13
  loadConfig = defaultLoadConfig,
46
14
  output = defaultOutput,
@@ -111,7 +79,7 @@ export async function projectLinkCommand(selector, options = {}, globalOptions =
111
79
  ...project
112
80
  }) : null;
113
81
  let reused = Boolean(linkedProject);
114
- let userToken = linkedProject ? null : config.userToken || globalOptions.token || (await getAccessToken());
82
+ let userToken = linkedProject ? null : config.userToken || globalOptions.token;
115
83
  if (!linkedProject && !userToken) {
116
84
  output.error('Login required before linking a project');
117
85
  output.hint('Run "vizzly login" first, then try project link again');
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Extract projects from API response
3
+ * @param {Object} response - API response
4
+ * @returns {Array} Array of projects
5
+ */
6
+ export function extractProjects(response) {
7
+ return response?.projects || [];
8
+ }
9
+
10
+ /**
11
+ * Extract project from API response
12
+ * @param {Object} response - API response
13
+ * @returns {Object} Project object
14
+ */
15
+ export function extractProject(response) {
16
+ return response?.project || response;
17
+ }
18
+
19
+ /**
20
+ * Extract builds from API response
21
+ * @param {Object} response - API response
22
+ * @returns {Array} Array of builds
23
+ */
24
+ export function extractBuilds(response) {
25
+ return response?.builds || [];
26
+ }
27
+
28
+ /**
29
+ * Extract token from API response
30
+ * @param {Object} response - API response
31
+ * @returns {Object} Token object
32
+ */
33
+ export function extractToken(response) {
34
+ return response?.token;
35
+ }
36
+
37
+ /**
38
+ * Extract tokens from API response
39
+ * @param {Object} response - API response
40
+ * @returns {Array} Array of tokens
41
+ */
42
+ export function extractTokens(response) {
43
+ return response?.tokens || [];
44
+ }
45
+
46
+ /**
47
+ * Enrich projects with organization info
48
+ * @param {Array} projects - Array of projects
49
+ * @param {Object} org - Organization object
50
+ * @param {string} org.slug - Organization slug
51
+ * @param {string} org.name - Organization name
52
+ * @returns {Array} Projects with organization info added
53
+ */
54
+ export function enrichProjectsWithOrg(projects, org) {
55
+ return projects.map(project => ({
56
+ ...project,
57
+ organizationSlug: org.slug,
58
+ organizationName: org.name
59
+ }));
60
+ }
61
+
62
+ /**
63
+ * Extract organizations from whoami response
64
+ * @param {Object} whoamiResponse - Whoami API response
65
+ * @returns {Array} Array of organizations
66
+ */
67
+ export function extractOrganizations(whoamiResponse) {
68
+ return whoamiResponse?.organizations || [];
69
+ }