@vizzly-testing/cli 0.30.0 → 0.32.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 (79) hide show
  1. package/README.md +9 -9
  2. package/dist/api/client.js +6 -3
  3. package/dist/cli.js +58 -29
  4. package/dist/client/index.js +21 -29
  5. package/dist/commands/api.js +145 -90
  6. package/dist/commands/baselines.js +78 -47
  7. package/dist/commands/builds.js +9 -8
  8. package/dist/commands/comparisons.js +7 -8
  9. package/dist/commands/config-cmd.js +15 -7
  10. package/dist/commands/context.js +329 -18
  11. package/dist/commands/doctor.js +116 -81
  12. package/dist/commands/init.js +202 -201
  13. package/dist/commands/login.js +134 -65
  14. package/dist/commands/logout.js +17 -6
  15. package/dist/commands/preview.js +29 -57
  16. package/dist/commands/project.js +120 -0
  17. package/dist/commands/projects.js +9 -2
  18. package/dist/commands/review.js +236 -202
  19. package/dist/commands/run.js +75 -72
  20. package/dist/commands/status.js +228 -150
  21. package/dist/commands/tdd-daemon-process.js +98 -0
  22. package/dist/commands/tdd-daemon.js +318 -212
  23. package/dist/commands/tdd.js +23 -8
  24. package/dist/commands/upload.js +10 -9
  25. package/dist/commands/whoami.js +63 -25
  26. package/dist/config/core.js +36 -35
  27. package/dist/context/local-workspace-provider.js +132 -12
  28. package/dist/context/provider-resolver.js +5 -1
  29. package/dist/index.js +5 -4
  30. package/dist/plugin-loader.js +55 -49
  31. package/dist/project/core.js +2 -2
  32. package/dist/project/operations.js +6 -3
  33. package/dist/reporter/reporter-bundle.css +1 -1
  34. package/dist/reporter/reporter-bundle.iife.js +7 -7
  35. package/dist/sdk/index.js +162 -107
  36. package/dist/server/handlers/tdd-handler.js +30 -54
  37. package/dist/server/routers/baseline.js +195 -178
  38. package/dist/server/routers/events.js +212 -174
  39. package/dist/server/routers/projects.js +14 -5
  40. package/dist/server/routers/screenshot.js +1 -31
  41. package/dist/services/build-manager.js +19 -97
  42. package/dist/services/config-service.js +26 -34
  43. package/dist/services/index.js +2 -4
  44. package/dist/services/static-report-generator.js +11 -12
  45. package/dist/services/test-runner.js +21 -27
  46. package/dist/tdd/core/region-coverage.js +109 -21
  47. package/dist/tdd/index.js +1 -1
  48. package/dist/tdd/metadata/region-metadata.js +0 -37
  49. package/dist/tdd/runtime-context.js +78 -0
  50. package/dist/tdd/server-registry.js +38 -17
  51. package/dist/tdd/services/baseline-manager.js +23 -0
  52. package/dist/tdd/services/comparison-service.js +10 -1
  53. package/dist/tdd/services/hotspot-service.js +0 -13
  54. package/dist/tdd/tdd-service.js +144 -138
  55. package/dist/types/client.d.ts +36 -1
  56. package/dist/types/index.d.ts +114 -22
  57. package/dist/types/sdk.d.ts +40 -0
  58. package/dist/uploader/core.js +5 -4
  59. package/dist/uploader/index.js +79 -2
  60. package/dist/uploader/operations.js +40 -5
  61. package/dist/utils/api-url.js +14 -0
  62. package/dist/utils/async-utils.js +26 -0
  63. package/dist/utils/ci-env.js +19 -12
  64. package/dist/utils/config-loader.js +39 -65
  65. package/dist/utils/config-schema.js +19 -55
  66. package/dist/utils/fetch-utils.js +12 -5
  67. package/dist/utils/git.js +35 -72
  68. package/dist/utils/global-config.js +20 -1
  69. package/dist/utils/output.js +19 -7
  70. package/dist/utils/patterns.js +14 -0
  71. package/dist/utils/project-link-store.js +105 -0
  72. package/dist/utils/screenshot-options.js +28 -0
  73. package/dist/utils/secret-store.js +65 -0
  74. package/package.json +14 -11
  75. package/dist/services/screenshot-server.js +0 -65
  76. package/dist/services/server-manager.js +0 -81
  77. package/dist/services/uploader.js +0 -100
  78. package/dist/tdd/services/region-service.js +0 -75
  79. package/dist/utils/build-history.js +0 -103
@@ -1,45 +1,15 @@
1
1
  import { resolve } from 'node:path';
2
2
  import { cosmiconfigSync } from 'cosmiconfig';
3
+ import { CONFIG_DEFAULTS, deepMerge } from '../config/core.js';
3
4
  import { validateVizzlyConfigWithDefaults } from './config-schema.js';
4
5
  import { getApiToken, getApiUrl, getBuildName, getParallelId } from './environment-config.js';
5
6
  import { getAccessToken } from './global-config.js';
6
7
  import * as output from './output.js';
7
- const DEFAULT_CONFIG = {
8
- // API Configuration
9
- apiKey: undefined,
10
- // Will be set from env, global config, or CLI overrides
11
- apiUrl: getApiUrl(),
12
- // Server Configuration (for run command)
13
- server: {
14
- port: 47392,
15
- timeout: 30000
16
- },
17
- // Build Configuration
18
- build: {
19
- name: 'Build {timestamp}',
20
- environment: 'test'
21
- },
22
- // Upload Configuration (for upload command)
23
- upload: {
24
- screenshotsDir: './screenshots',
25
- batchSize: 10,
26
- timeout: 30000
27
- },
28
- // Comparison Configuration (CIEDE2000 Delta E: 0=exact, 1=JND, 2=recommended)
29
- comparison: {
30
- threshold: 2.0
31
- },
32
- // TDD Configuration
33
- tdd: {
34
- openReport: false // Whether to auto-open HTML report in browser
35
- },
36
- // Plugins
37
- plugins: []
38
- };
8
+ import { getActiveProjectLink } from './project-link-store.js';
39
9
  export async function loadConfig(configPath = null, cliOverrides = {}) {
40
10
  // 1. Load from config file using cosmiconfig
41
- const explorer = cosmiconfigSync('vizzly');
42
- const result = configPath ? explorer.load(configPath) : explorer.search();
11
+ let explorer = cosmiconfigSync('vizzly');
12
+ let result = configPath ? explorer.load(configPath) : explorer.search();
43
13
  let fileConfig = {};
44
14
  if (result?.config) {
45
15
  // Handle ESM default export (cosmiconfig wraps it in { default: {...} })
@@ -47,28 +17,10 @@ export async function loadConfig(configPath = null, cliOverrides = {}) {
47
17
  }
48
18
 
49
19
  // 2. Validate config file using Zod schema
50
- const validatedFileConfig = validateVizzlyConfigWithDefaults(fileConfig);
20
+ let validatedFileConfig = validateVizzlyConfigWithDefaults(fileConfig);
51
21
 
52
22
  // Create a proper clone of the default config to avoid shared object references
53
- const config = {
54
- ...DEFAULT_CONFIG,
55
- server: {
56
- ...DEFAULT_CONFIG.server
57
- },
58
- build: {
59
- ...DEFAULT_CONFIG.build
60
- },
61
- upload: {
62
- ...DEFAULT_CONFIG.upload
63
- },
64
- comparison: {
65
- ...DEFAULT_CONFIG.comparison
66
- },
67
- tdd: {
68
- ...DEFAULT_CONFIG.tdd
69
- },
70
- plugins: [...DEFAULT_CONFIG.plugins]
71
- };
23
+ let config = deepMerge(CONFIG_DEFAULTS, {});
72
24
 
73
25
  // Merge validated file config
74
26
  mergeConfig(config, validatedFileConfig);
@@ -95,16 +47,35 @@ export async function loadConfig(configPath = null, cliOverrides = {}) {
95
47
  }
96
48
  applyCLIOverrides(config, cliOverrides);
97
49
 
98
- // 5. Fall back to user auth token if no other token found
99
- // This enables interactive commands (builds, comparisons, approve, etc.)
100
- // to work without a project token when the user is logged in
50
+ // 5. Fall back to a linked project token for cloud upload commands.
101
51
  if (!config.apiKey) {
102
- let userToken = await getAccessToken();
103
- if (userToken) {
104
- config.apiKey = userToken;
105
- output.debug('config', 'using token from user login');
52
+ let linkedProject = await getActiveProjectLink({
53
+ apiUrl: config.apiUrl
54
+ });
55
+ if (linkedProject?.token) {
56
+ config.apiKey = linkedProject.token;
57
+ config.linkedProject = {
58
+ account: linkedProject.account,
59
+ apiUrl: linkedProject.apiUrl,
60
+ organizationSlug: linkedProject.organizationSlug,
61
+ organizationName: linkedProject.organizationName,
62
+ projectSlug: linkedProject.projectSlug,
63
+ projectName: linkedProject.projectName,
64
+ tokenId: linkedProject.tokenId,
65
+ tokenPrefix: linkedProject.tokenPrefix,
66
+ expiresAt: linkedProject.expiresAt,
67
+ storage: linkedProject.storage
68
+ };
69
+ output.debug('config', 'using linked project token');
106
70
  }
107
71
  }
72
+
73
+ // 6. Keep user auth separate from upload credentials.
74
+ let userToken = await getAccessToken();
75
+ if (userToken) {
76
+ config.userToken = userToken;
77
+ output.debug('config', 'using user login for user-authenticated commands');
78
+ }
108
79
  return config;
109
80
  }
110
81
 
@@ -126,19 +97,22 @@ function applyCLIOverrides(config, cliOverrides = {}) {
126
97
  if (cliOverrides.parallelId) config.parallelId = cliOverrides.parallelId;
127
98
 
128
99
  // Server overrides
129
- if (cliOverrides.port) config.server.port = parseInt(cliOverrides.port, 10);
130
- if (cliOverrides.timeout) config.server.timeout = parseInt(cliOverrides.timeout, 10);
100
+ if (cliOverrides.port) config.server.port = Number(cliOverrides.port);
101
+ if (cliOverrides.timeout) config.server.timeout = Number(cliOverrides.timeout);
131
102
 
132
103
  // Upload overrides
133
104
  if (cliOverrides.batchSize !== undefined) {
134
- config.upload.batchSize = parseInt(cliOverrides.batchSize, 10);
105
+ config.upload.batchSize = Number(cliOverrides.batchSize);
135
106
  }
136
107
  if (cliOverrides.uploadTimeout !== undefined) {
137
- config.upload.timeout = parseInt(cliOverrides.uploadTimeout, 10);
108
+ config.upload.timeout = Number(cliOverrides.uploadTimeout);
138
109
  }
139
110
 
140
111
  // Comparison overrides
141
112
  if (cliOverrides.threshold !== undefined) config.comparison.threshold = cliOverrides.threshold;
113
+ if (cliOverrides.minClusterSize !== undefined) {
114
+ config.comparison.minClusterSize = Number(cliOverrides.minClusterSize);
115
+ }
142
116
 
143
117
  // Baseline overrides
144
118
  if (cliOverrides.baselineBuild) config.baselineBuildId = cliOverrides.baselineBuild;
@@ -4,21 +4,22 @@
4
4
  */
5
5
 
6
6
  import { z } from 'zod';
7
+ import { CONFIG_DEFAULTS } from '../config/core.js';
7
8
 
8
9
  /**
9
10
  * Server configuration schema
10
11
  */
11
12
  const serverSchema = z.object({
12
- port: z.number().int().positive().default(47392),
13
- timeout: z.number().int().positive().default(30000)
13
+ port: z.number().int().positive().default(CONFIG_DEFAULTS.server.port),
14
+ timeout: z.number().int().positive().default(CONFIG_DEFAULTS.server.timeout)
14
15
  });
15
16
 
16
17
  /**
17
18
  * Build configuration schema
18
19
  */
19
20
  const buildSchema = z.object({
20
- name: z.string().default('Build {timestamp}'),
21
- environment: z.string().default('test'),
21
+ name: z.string().default(CONFIG_DEFAULTS.build.name),
22
+ environment: z.string().default(CONFIG_DEFAULTS.build.environment),
22
23
  branch: z.string().optional(),
23
24
  commit: z.string().optional(),
24
25
  message: z.string().optional()
@@ -28,9 +29,9 @@ const buildSchema = z.object({
28
29
  * Upload configuration schema
29
30
  */
30
31
  const uploadSchema = z.object({
31
- screenshotsDir: z.union([z.string(), z.array(z.string())]).default('./screenshots'),
32
- batchSize: z.number().int().positive().default(10),
33
- timeout: z.number().int().positive().default(30000)
32
+ screenshotsDir: z.union([z.string(), z.array(z.string())]).default(CONFIG_DEFAULTS.upload.screenshotsDir),
33
+ batchSize: z.number().int().positive().default(CONFIG_DEFAULTS.upload.batchSize),
34
+ timeout: z.number().int().positive().default(CONFIG_DEFAULTS.upload.timeout)
34
35
  });
35
36
 
36
37
  /**
@@ -39,15 +40,15 @@ const uploadSchema = z.object({
39
40
  * minClusterSize: pixels (1 = exact)
40
41
  */
41
42
  const comparisonSchema = z.object({
42
- threshold: z.number().min(0).default(2.0),
43
- minClusterSize: z.int().min(1).default(2)
43
+ threshold: z.number().min(0).default(CONFIG_DEFAULTS.comparison.threshold),
44
+ minClusterSize: z.int().min(1).default(CONFIG_DEFAULTS.comparison.minClusterSize)
44
45
  });
45
46
 
46
47
  /**
47
48
  * TDD configuration schema
48
49
  */
49
50
  const tddSchema = z.object({
50
- openReport: z.boolean().default(false)
51
+ openReport: z.boolean().default(CONFIG_DEFAULTS.tdd.openReport)
51
52
  });
52
53
 
53
54
  /**
@@ -57,29 +58,14 @@ const tddSchema = z.object({
57
58
  export const vizzlyConfigSchema = z.object({
58
59
  // Core Vizzly config
59
60
  apiKey: z.string().optional(),
60
- apiUrl: z.string().url().optional(),
61
- server: serverSchema.default({
62
- port: 47392,
63
- timeout: 30000
64
- }),
65
- build: buildSchema.default({
66
- name: 'Build {timestamp}',
67
- environment: 'test'
68
- }),
69
- upload: uploadSchema.default({
70
- screenshotsDir: './screenshots',
71
- batchSize: 10,
72
- timeout: 30000
73
- }),
74
- comparison: comparisonSchema.default({
75
- threshold: 2.0,
76
- minClusterSize: 2
77
- }),
78
- tdd: tddSchema.default({
79
- openReport: false
80
- }),
61
+ apiUrl: z.string().url().default(CONFIG_DEFAULTS.apiUrl),
62
+ server: serverSchema.default(CONFIG_DEFAULTS.server),
63
+ build: buildSchema.default(CONFIG_DEFAULTS.build),
64
+ upload: uploadSchema.default(CONFIG_DEFAULTS.upload),
65
+ comparison: comparisonSchema.default(CONFIG_DEFAULTS.comparison),
66
+ tdd: tddSchema.default(CONFIG_DEFAULTS.tdd),
81
67
  signatureProperties: z.array(z.string()).default([]),
82
- plugins: z.array(z.string()).default([]),
68
+ plugins: z.array(z.string()).default(CONFIG_DEFAULTS.plugins),
83
69
  // Additional optional fields
84
70
  parallelId: z.string().optional(),
85
71
  baselineBuildId: z.string().optional(),
@@ -88,29 +74,7 @@ export const vizzlyConfigSchema = z.object({
88
74
  wait: z.boolean().optional(),
89
75
  allowNoToken: z.boolean().optional()
90
76
  }).passthrough() // Allow plugin-specific keys like `staticSite`, `storybook`, etc.
91
- .default({
92
- server: {
93
- port: 47392,
94
- timeout: 30000
95
- },
96
- build: {
97
- name: 'Build {timestamp}',
98
- environment: 'test'
99
- },
100
- upload: {
101
- screenshotsDir: './screenshots',
102
- batchSize: 10,
103
- timeout: 30000
104
- },
105
- comparison: {
106
- threshold: 2.0,
107
- minClusterSize: 2
108
- },
109
- tdd: {
110
- openReport: false
111
- },
112
- plugins: []
113
- });
77
+ .default(CONFIG_DEFAULTS);
114
78
 
115
79
  /**
116
80
  * Validate Vizzly configuration
@@ -1,9 +1,16 @@
1
- function fetchWithTimeout(url, opts = {}, ms = 300000) {
2
- const ctrl = new AbortController();
3
- const id = setTimeout(() => ctrl.abort(), ms);
4
- return fetch(url, {
1
+ let defaultTimers = {
2
+ setTimeout,
3
+ clearTimeout
4
+ };
5
+ function fetchWithTimeout(url, opts = {}, ms = 300000, deps = {}) {
6
+ let fetchFn = deps.fetch || fetch;
7
+ let AbortControllerClass = deps.AbortController || AbortController;
8
+ let timers = deps.timers || defaultTimers;
9
+ let ctrl = new AbortControllerClass();
10
+ let timeoutId = timers.setTimeout(() => ctrl.abort(), ms);
11
+ return fetchFn(url, {
5
12
  ...opts,
6
13
  signal: ctrl.signal
7
- }).finally(() => clearTimeout(id));
14
+ }).finally(() => timers.clearTimeout(timeoutId));
8
15
  }
9
16
  export { fetchWithTimeout };
package/dist/utils/git.js CHANGED
@@ -1,15 +1,18 @@
1
- import { exec } from 'node:child_process';
1
+ import { execFile } from 'node:child_process';
2
2
  import { promisify } from 'node:util';
3
3
  import { getBranch as getCIBranch, getCommit as getCICommit, getCommitMessage as getCICommitMessage, getPullRequestNumber } from './ci-env.js';
4
- const execAsync = promisify(exec);
4
+ let execFileAsync = promisify(execFile);
5
+ async function runGit(args, cwd = process.cwd()) {
6
+ let {
7
+ stdout
8
+ } = await execFileAsync('git', args, {
9
+ cwd
10
+ });
11
+ return stdout.trim();
12
+ }
5
13
  export async function getCommonAncestor(commit1, commit2, cwd = process.cwd()) {
6
14
  try {
7
- const {
8
- stdout
9
- } = await execAsync(`git merge-base ${commit1} ${commit2}`, {
10
- cwd
11
- });
12
- return stdout.trim();
15
+ return await runGit(['merge-base', commit1, commit2], cwd);
13
16
  } catch {
14
17
  // If merge-base fails (e.g., no common ancestor), return null
15
18
  return null;
@@ -17,24 +20,14 @@ export async function getCommonAncestor(commit1, commit2, cwd = process.cwd()) {
17
20
  }
18
21
  export async function getCurrentCommitSha(cwd = process.cwd()) {
19
22
  try {
20
- const {
21
- stdout
22
- } = await execAsync('git rev-parse HEAD', {
23
- cwd
24
- });
25
- return stdout.trim();
23
+ return await runGit(['rev-parse', 'HEAD'], cwd);
26
24
  } catch {
27
25
  return null;
28
26
  }
29
27
  }
30
28
  export async function getCurrentBranch(cwd = process.cwd()) {
31
29
  try {
32
- const {
33
- stdout
34
- } = await execAsync('git rev-parse --abbrev-ref HEAD', {
35
- cwd
36
- });
37
- return stdout.trim();
30
+ return await runGit(['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
38
31
  } catch {
39
32
  // Fallback strategy: use a simple, non-recursive approach
40
33
  // to avoid circular dependency with getDefaultBranch()
@@ -48,24 +41,18 @@ export async function getCurrentBranch(cwd = process.cwd()) {
48
41
  */
49
42
  async function getCurrentBranchFallback(cwd = process.cwd()) {
50
43
  // Try common default branches in order of likelihood
51
- const commonBranches = ['main', 'master', 'develop', 'dev'];
52
- for (const branch of commonBranches) {
44
+ let commonBranches = ['main', 'master', 'develop', 'dev'];
45
+ for (let branch of commonBranches) {
53
46
  try {
54
- await execAsync(`git show-ref --verify --quiet refs/heads/${branch}`, {
55
- cwd
56
- });
47
+ await runGit(['show-ref', '--verify', '--quiet', `refs/heads/${branch}`], cwd);
57
48
  return branch;
58
49
  } catch {}
59
50
  }
60
51
 
61
52
  // If none of the common branches exist, try to get any local branch
62
53
  try {
63
- const {
64
- stdout
65
- } = await execAsync('git branch --format="%(refname:short)"', {
66
- cwd
67
- });
68
- const branches = stdout.trim().split('\n').filter(b => b.trim());
54
+ let stdout = await runGit(['branch', '--format=%(refname:short)'], cwd);
55
+ let branches = stdout.trim().split('\n').filter(b => b.trim());
69
56
  if (branches.length > 0) {
70
57
  return branches[0]; // Return the first available branch
71
58
  }
@@ -80,35 +67,22 @@ async function getCurrentBranchFallback(cwd = process.cwd()) {
80
67
  export async function getDefaultBranch(cwd = process.cwd()) {
81
68
  try {
82
69
  // Try to get the default branch from remote origin
83
- const {
84
- stdout
85
- } = await execAsync('git symbolic-ref refs/remotes/origin/HEAD', {
86
- cwd
87
- });
88
- const defaultBranch = stdout.trim().replace('refs/remotes/origin/', '');
70
+ let stdout = await runGit(['symbolic-ref', 'refs/remotes/origin/HEAD'], cwd);
71
+ let defaultBranch = stdout.replace('refs/remotes/origin/', '');
89
72
  return defaultBranch;
90
73
  } catch {
91
74
  try {
92
75
  // Fallback: try to get default branch from git config
93
- const {
94
- stdout
95
- } = await execAsync('git config --get init.defaultBranch', {
96
- cwd
97
- });
98
- return stdout.trim();
76
+ return await runGit(['config', '--get', 'init.defaultBranch'], cwd);
99
77
  } catch {
100
78
  try {
101
79
  // Fallback: check if main exists
102
- await execAsync('git show-ref --verify --quiet refs/heads/main', {
103
- cwd
104
- });
80
+ await runGit(['show-ref', '--verify', '--quiet', 'refs/heads/main'], cwd);
105
81
  return 'main';
106
82
  } catch {
107
83
  try {
108
84
  // Fallback: check if master exists
109
- await execAsync('git show-ref --verify --quiet refs/heads/master', {
110
- cwd
111
- });
85
+ await runGit(['show-ref', '--verify', '--quiet', 'refs/heads/master'], cwd);
112
86
  return 'master';
113
87
  } catch {
114
88
  // If we're not in a git repo or no branches exist, return null
@@ -120,7 +94,7 @@ export async function getDefaultBranch(cwd = process.cwd()) {
120
94
  }
121
95
  }
122
96
  export function generateBuildName() {
123
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
97
+ let timestamp = new Date().toISOString().replace(/[:.]/g, '-');
124
98
  return `Build ${timestamp}`;
125
99
  }
126
100
 
@@ -131,12 +105,7 @@ export function generateBuildName() {
131
105
  */
132
106
  export async function getCommitMessage(cwd = process.cwd()) {
133
107
  try {
134
- const {
135
- stdout
136
- } = await execAsync('git log -1 --pretty=%B', {
137
- cwd
138
- });
139
- return stdout.trim();
108
+ return await runGit(['log', '-1', '--pretty=%B'], cwd);
140
109
  } catch {
141
110
  return null;
142
111
  }
@@ -152,7 +121,7 @@ export async function detectCommitMessage(override = null, cwd = process.cwd())
152
121
  if (override) return override;
153
122
 
154
123
  // Try CI environment variables first
155
- const ciCommitMessage = getCICommitMessage();
124
+ let ciCommitMessage = getCICommitMessage();
156
125
  if (ciCommitMessage) return ciCommitMessage;
157
126
 
158
127
  // Fallback to regular git log
@@ -166,9 +135,7 @@ export async function detectCommitMessage(override = null, cwd = process.cwd())
166
135
  */
167
136
  export async function isGitRepository(cwd = process.cwd()) {
168
137
  try {
169
- await execAsync('git rev-parse --git-dir', {
170
- cwd
171
- });
138
+ await runGit(['rev-parse', '--git-dir'], cwd);
172
139
  return true;
173
140
  } catch {
174
141
  return false;
@@ -182,12 +149,8 @@ export async function isGitRepository(cwd = process.cwd()) {
182
149
  */
183
150
  export async function getGitStatus(cwd = process.cwd()) {
184
151
  try {
185
- const {
186
- stdout
187
- } = await execAsync('git status --porcelain', {
188
- cwd
189
- });
190
- const changes = stdout.trim().split('\n').filter(line => line);
152
+ let stdout = await runGit(['status', '--porcelain'], cwd);
153
+ let changes = stdout.trim().split('\n').filter(line => line);
191
154
  return {
192
155
  hasChanges: changes.length > 0,
193
156
  changes: changes.map(line => ({
@@ -210,11 +173,11 @@ export async function detectBranch(override = null, cwd = process.cwd()) {
210
173
  if (override) return override;
211
174
 
212
175
  // Try CI environment variables first
213
- const ciBranch = getCIBranch();
176
+ let ciBranch = getCIBranch();
214
177
  if (ciBranch) return ciBranch;
215
178
 
216
179
  // Fallback to git command when no CI environment variables
217
- const currentBranch = await getCurrentBranch(cwd);
180
+ let currentBranch = await getCurrentBranch(cwd);
218
181
  return currentBranch || 'unknown';
219
182
  }
220
183
 
@@ -228,7 +191,7 @@ export async function detectCommit(override = null, cwd = process.cwd()) {
228
191
  if (override) return override;
229
192
 
230
193
  // Try CI environment variables first
231
- const ciCommit = getCICommit();
194
+ let ciCommit = getCICommit();
232
195
  if (ciCommit) return ciCommit;
233
196
 
234
197
  // Fallback to git command when no CI environment variables
@@ -243,10 +206,10 @@ export async function detectCommit(override = null, cwd = process.cwd()) {
243
206
  */
244
207
  export async function generateBuildNameWithGit(override = null, cwd = process.cwd()) {
245
208
  if (override) return override;
246
- const branch = await getCurrentBranch(cwd);
247
- const shortSha = await getCurrentCommitSha(cwd);
209
+ let branch = await getCurrentBranch(cwd);
210
+ let shortSha = await getCurrentCommitSha(cwd);
248
211
  if (branch && shortSha) {
249
- const shortCommit = shortSha.substring(0, 7);
212
+ let shortCommit = shortSha.substring(0, 7);
250
213
  return `${branch}-${shortCommit}`;
251
214
  }
252
215
  return generateBuildName();
@@ -8,6 +8,25 @@ import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
8
8
  import { homedir } from 'node:os';
9
9
  import { join } from 'node:path';
10
10
  import * as output from './output.js';
11
+ export function expandHomePath(path) {
12
+ if (!path) {
13
+ return path;
14
+ }
15
+ let home = homedir();
16
+ if (path === '$HOME') {
17
+ return home;
18
+ }
19
+ if (path.startsWith('$HOME/')) {
20
+ return join(home, path.slice('$HOME/'.length));
21
+ }
22
+ if (path === '~') {
23
+ return home;
24
+ }
25
+ if (path.startsWith('~/')) {
26
+ return join(home, path.slice(2));
27
+ }
28
+ return path;
29
+ }
11
30
 
12
31
  /**
13
32
  * Get the path to the global Vizzly directory
@@ -15,7 +34,7 @@ import * as output from './output.js';
15
34
  */
16
35
  export function getGlobalConfigDir() {
17
36
  if (process.env.VIZZLY_HOME) {
18
- return process.env.VIZZLY_HOME;
37
+ return expandHomePath(process.env.VIZZLY_HOME);
19
38
  }
20
39
  return join(homedir(), '.vizzly');
21
40
  }
@@ -603,15 +603,27 @@ function getComponentColor(component) {
603
603
  };
604
604
  return componentColors[component] || colors.brand.info;
605
605
  }
606
- export function debug(component, message, data = {}) {
607
- if (!shouldLog('debug')) return;
608
-
609
- // Handle legacy calls: debug('message') or debug('message', {data})
606
+ export function normalizeDebugArgs(component, message, data = {}) {
610
607
  if (typeof message === 'object' || message === undefined) {
611
- data = message || {};
612
- message = component;
613
- component = null;
608
+ return {
609
+ component: null,
610
+ message: component,
611
+ data: message || {}
612
+ };
614
613
  }
614
+ return {
615
+ component,
616
+ message,
617
+ data
618
+ };
619
+ }
620
+ export function debug(component, message, data = {}) {
621
+ if (!shouldLog('debug')) return;
622
+ ({
623
+ component,
624
+ message,
625
+ data
626
+ } = normalizeDebugArgs(component, message, data));
615
627
  let elapsed = getElapsedTime();
616
628
  if (config.json) {
617
629
  console.error(JSON.stringify({
@@ -0,0 +1,14 @@
1
+ export function escapeRegExp(value) {
2
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3
+ }
4
+ export function createWildcardMatcher(pattern, options = {}) {
5
+ let {
6
+ anchored = false
7
+ } = options;
8
+ let regexPattern = String(pattern).split('*').map(escapeRegExp).join('.*');
9
+ if (anchored) {
10
+ regexPattern = `^${regexPattern}$`;
11
+ }
12
+ let regex = new RegExp(regexPattern, 'i');
13
+ return value => regex.test(String(value));
14
+ }