laxy-verify 1.2.3 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/config.js CHANGED
@@ -1,360 +1,360 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.ConfigParseError = void 0;
37
- exports.fetchTeamThresholds = fetchTeamThresholds;
38
- exports.loadConfig = loadConfig;
39
- const fs = __importStar(require("node:fs"));
40
- const path = __importStar(require("node:path"));
41
- const yaml = __importStar(require("js-yaml"));
42
- const auth_js_1 = require("./auth.js");
43
- const DEFAULT_CONFIG = {
44
- framework: "auto",
45
- build_command: "",
46
- dev_command: "",
47
- package_manager: "auto",
48
- port: 3000,
49
- build_timeout: 300,
50
- dev_timeout: 60,
51
- lighthouse_runs: 3,
52
- thresholds: {
53
- performance: 70,
54
- accessibility: 85,
55
- seo: 80,
56
- bestPractices: 80,
57
- },
58
- fail_on: "bronze",
59
- crawl: false,
60
- max_crawl_depth: 3,
61
- max_crawl_pages: 10,
62
- browsers: ["chromium"],
63
- max_lighthouse_routes: 5,
64
- visual_diff: {
65
- pixelmatchThreshold: 0.1,
66
- warnThreshold: 30,
67
- rollbackThreshold: 60,
68
- ignoreSelectors: [],
69
- disableAnimations: true,
70
- },
71
- typecheck: false,
72
- secret_scan: false,
73
- secret_scan_ignore_paths: [],
74
- bundle_size: false,
75
- outdated_check: false,
76
- a11y_deep: false,
77
- seo_deep: false,
78
- vitals_budget: false,
79
- };
80
- const VALID_FAIL_ON = ["unverified", "bronze", "silver", "gold"];
81
- class ConfigParseError extends Error {
82
- constructor(msg) {
83
- super(msg);
84
- this.name = "ConfigParseError";
85
- }
86
- }
87
- exports.ConfigParseError = ConfigParseError;
88
- function parseYaml(filePath) {
89
- const content = fs.readFileSync(filePath, "utf-8");
90
- const raw = yaml.load(content);
91
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
92
- throw new ConfigParseError("Invalid YAML structure in .laxy.yml");
93
- }
94
- const result = {};
95
- if (typeof raw.framework === "string")
96
- result.framework = raw.framework;
97
- if (typeof raw.build_command === "string")
98
- result.build_command = raw.build_command;
99
- if (typeof raw.dev_command === "string")
100
- result.dev_command = raw.dev_command;
101
- if (typeof raw.package_manager === "string")
102
- result.package_manager = raw.package_manager;
103
- if (typeof raw.port === "number")
104
- result.port = raw.port;
105
- if (typeof raw.build_timeout === "number")
106
- result.build_timeout = raw.build_timeout;
107
- if (typeof raw.dev_timeout === "number")
108
- result.dev_timeout = raw.dev_timeout;
109
- if (typeof raw.lighthouse_runs === "number")
110
- result.lighthouse_runs = raw.lighthouse_runs;
111
- if (typeof raw.crawl === "boolean")
112
- result.crawl = raw.crawl;
113
- if (typeof raw.max_crawl_depth === "number")
114
- result.max_crawl_depth = raw.max_crawl_depth;
115
- if (typeof raw.max_crawl_pages === "number")
116
- result.max_crawl_pages = raw.max_crawl_pages;
117
- if (Array.isArray(raw.browsers)) {
118
- const validBrowsers = ["chromium", "firefox", "webkit"];
119
- const browsers = raw.browsers
120
- .filter((b) => typeof b === "string" && validBrowsers.includes(b));
121
- if (browsers.length > 0)
122
- result.browsers = browsers;
123
- }
124
- if (Array.isArray(raw.lighthouse_routes)) {
125
- const routes = raw.lighthouse_routes
126
- .filter((r) => typeof r === "string" && r.startsWith("/"))
127
- .slice(0, 20);
128
- if (routes.length > 0)
129
- result.lighthouse_routes = routes;
130
- }
131
- if (Array.isArray(raw.extra_routes)) {
132
- const routes = raw.extra_routes
133
- .filter((r) => typeof r === "string" && r.startsWith("/"))
134
- .slice(0, 20);
135
- if (routes.length > 0)
136
- result.extra_routes = routes;
137
- }
138
- if (typeof raw.max_lighthouse_routes === "number") {
139
- result.max_lighthouse_routes = Math.max(1, Math.min(20, raw.max_lighthouse_routes));
140
- }
141
- if (typeof raw.visual_diff === "object" &&
142
- raw.visual_diff !== null &&
143
- !Array.isArray(raw.visual_diff)) {
144
- const vd = raw.visual_diff;
145
- const visualDiff = {};
146
- if (typeof vd.pixelmatch_threshold === "number") {
147
- visualDiff.pixelmatchThreshold = Math.max(0, Math.min(1, vd.pixelmatch_threshold));
148
- }
149
- if (typeof vd.warn_threshold === "number") {
150
- visualDiff.warnThreshold = Math.max(0, Math.min(100, vd.warn_threshold));
151
- }
152
- if (typeof vd.rollback_threshold === "number") {
153
- visualDiff.rollbackThreshold = Math.max(0, Math.min(100, vd.rollback_threshold));
154
- }
155
- if (Array.isArray(vd.ignore_selectors)) {
156
- visualDiff.ignoreSelectors = vd.ignore_selectors
157
- .filter((selector) => typeof selector === "string" && selector.trim().length > 0)
158
- .slice(0, 30);
159
- }
160
- if (typeof vd.disable_animations === "boolean") {
161
- visualDiff.disableAnimations = vd.disable_animations;
162
- }
163
- result.visual_diff = visualDiff;
164
- }
165
- if (typeof raw.fail_on === "string") {
166
- const f = raw.fail_on;
167
- if (!VALID_FAIL_ON.includes(f)) {
168
- throw new ConfigParseError(`Invalid fail_on value: "${f}". Must be one of: ${VALID_FAIL_ON.join(", ")}`);
169
- }
170
- result.fail_on = f;
171
- }
172
- if (typeof raw.thresholds === "object" &&
173
- raw.thresholds !== null &&
174
- !Array.isArray(raw.thresholds)) {
175
- const t = raw.thresholds;
176
- const thr = {};
177
- if (typeof t.performance === "number")
178
- thr.performance = t.performance;
179
- if (typeof t.accessibility === "number")
180
- thr.accessibility = t.accessibility;
181
- if (typeof t.seo === "number")
182
- thr.seo = t.seo;
183
- if (typeof t.best_practices === "number")
184
- thr.bestPractices = t.best_practices;
185
- result.thresholds = thr;
186
- }
187
- if (Array.isArray(raw.scenarios)) {
188
- const scenarios = [];
189
- for (const item of raw.scenarios) {
190
- if (typeof item === "object" &&
191
- item !== null &&
192
- typeof item.name === "string" &&
193
- Array.isArray(item.steps)) {
194
- const rawScenario = item;
195
- const steps = [];
196
- for (const rawStep of rawScenario.steps) {
197
- if (typeof rawStep === "object" && rawStep !== null) {
198
- const s = rawStep;
199
- const step = {};
200
- if (typeof s.goto === "string")
201
- step.goto = s.goto;
202
- if (typeof s.fill === "string")
203
- step.fill = s.fill;
204
- if (typeof s.with === "string")
205
- step.with = s.with;
206
- if (typeof s.click === "string")
207
- step.click = s.click;
208
- if (typeof s.expect_visible === "string")
209
- step.expect_visible = s.expect_visible;
210
- if (typeof s.expect_text === "string")
211
- step.expect_text = s.expect_text;
212
- if (typeof s.wait === "number")
213
- step.wait = s.wait;
214
- steps.push(step);
215
- }
216
- }
217
- scenarios.push({ name: String(rawScenario.name), steps });
218
- }
219
- }
220
- if (scenarios.length > 0) {
221
- result.scenarios = scenarios;
222
- }
223
- }
224
- if (typeof raw.typecheck === "boolean")
225
- result.typecheck = raw.typecheck;
226
- if (typeof raw.secret_scan === "boolean")
227
- result.secret_scan = raw.secret_scan;
228
- if (Array.isArray(raw.secret_scan_ignore_paths)) {
229
- result.secret_scan_ignore_paths = raw.secret_scan_ignore_paths
230
- .filter((p) => typeof p === "string");
231
- }
232
- if (typeof raw.bundle_size === "boolean")
233
- result.bundle_size = raw.bundle_size;
234
- if (typeof raw.outdated_check === "boolean")
235
- result.outdated_check = raw.outdated_check;
236
- if (typeof raw.a11y_deep === "boolean")
237
- result.a11y_deep = raw.a11y_deep;
238
- if (typeof raw.seo_deep === "boolean")
239
- result.seo_deep = raw.seo_deep;
240
- if (typeof raw.vitals_budget === "boolean")
241
- result.vitals_budget = raw.vitals_budget;
242
- return result;
243
- }
244
- /**
245
- * 로그인된 CLI 토큰으로 팀 공통 임계값을 서버에서 가져온다.
246
- * 토큰 없음 / 팀 없음 / 네트워크 오류 시 null 반환 (graceful degradation).
247
- */
248
- async function fetchTeamThresholds() {
249
- const token = (0, auth_js_1.loadToken)();
250
- if (!token)
251
- return null;
252
- try {
253
- const res = await fetch(`${auth_js_1.LAXY_API_URL}/api/v1/team-thresholds`, {
254
- headers: { Authorization: `Bearer ${token}` },
255
- signal: AbortSignal.timeout(5000),
256
- });
257
- if (!res.ok)
258
- return null;
259
- const data = (await res.json());
260
- return data.thresholds ?? null;
261
- }
262
- catch {
263
- return null;
264
- }
265
- }
266
- function loadConfig(options) {
267
- const configPath = options.configPath ?? path.join(options.dir, ".laxy.yml");
268
- let base = {};
269
- const hasLocalConfig = fs.existsSync(configPath);
270
- if (hasLocalConfig) {
271
- base = parseYaml(configPath);
272
- }
273
- // 팀 임계값: 로컬 .laxy.yml에 thresholds가 없을 때만 적용
274
- const team = options.teamThresholds ?? null;
275
- const teamThresholdFallback = (!hasLocalConfig || !base.thresholds) && team
276
- ? {
277
- performance: team.performance,
278
- accessibility: team.accessibility,
279
- seo: team.seo,
280
- bestPractices: team.best_practices,
281
- }
282
- : {};
283
- const teamFailOnFallback = (!hasLocalConfig || !base.fail_on) && team ? team.fail_on : undefined;
284
- const config = {
285
- ...DEFAULT_CONFIG,
286
- framework: base.framework ?? DEFAULT_CONFIG.framework,
287
- build_command: base.build_command ?? DEFAULT_CONFIG.build_command,
288
- dev_command: base.dev_command ?? DEFAULT_CONFIG.dev_command,
289
- package_manager: base.package_manager ?? DEFAULT_CONFIG.package_manager,
290
- port: base.port ?? DEFAULT_CONFIG.port,
291
- build_timeout: base.build_timeout ?? DEFAULT_CONFIG.build_timeout,
292
- dev_timeout: base.dev_timeout ?? DEFAULT_CONFIG.dev_timeout,
293
- lighthouse_runs: base.lighthouse_runs ?? DEFAULT_CONFIG.lighthouse_runs,
294
- fail_on: base.fail_on ?? teamFailOnFallback ?? DEFAULT_CONFIG.fail_on,
295
- scenarios: base.scenarios,
296
- crawl: base.crawl ?? DEFAULT_CONFIG.crawl,
297
- max_crawl_depth: base.max_crawl_depth ?? DEFAULT_CONFIG.max_crawl_depth,
298
- max_crawl_pages: base.max_crawl_pages ?? DEFAULT_CONFIG.max_crawl_pages,
299
- browsers: base.browsers ?? DEFAULT_CONFIG.browsers,
300
- lighthouse_routes: base.lighthouse_routes,
301
- extra_routes: base.extra_routes,
302
- max_lighthouse_routes: base.max_lighthouse_routes ?? DEFAULT_CONFIG.max_lighthouse_routes,
303
- visual_diff: {
304
- ...DEFAULT_CONFIG.visual_diff,
305
- ...(base.visual_diff ?? {}),
306
- },
307
- typecheck: base.typecheck ?? DEFAULT_CONFIG.typecheck,
308
- secret_scan: base.secret_scan ?? DEFAULT_CONFIG.secret_scan,
309
- secret_scan_ignore_paths: base.secret_scan_ignore_paths ?? DEFAULT_CONFIG.secret_scan_ignore_paths,
310
- bundle_size: base.bundle_size ?? DEFAULT_CONFIG.bundle_size,
311
- outdated_check: base.outdated_check ?? DEFAULT_CONFIG.outdated_check,
312
- a11y_deep: base.a11y_deep ?? DEFAULT_CONFIG.a11y_deep,
313
- seo_deep: base.seo_deep ?? DEFAULT_CONFIG.seo_deep,
314
- vitals_budget: base.vitals_budget ?? DEFAULT_CONFIG.vitals_budget,
315
- };
316
- config.thresholds = {
317
- ...DEFAULT_CONFIG.thresholds,
318
- ...teamThresholdFallback,
319
- ...(base.thresholds ?? {}),
320
- };
321
- // CLI flag overrides
322
- if (options.cliFlags?.failOn !== undefined) {
323
- if (!VALID_FAIL_ON.includes(options.cliFlags.failOn)) {
324
- throw new ConfigParseError(`Invalid --fail-on value: "${options.cliFlags.failOn}". Must be one of: ${VALID_FAIL_ON.join(", ")}`);
325
- }
326
- config.fail_on = options.cliFlags.failOn;
327
- }
328
- // CI mode: apply CI defaults
329
- const ciMode = options.ciMode;
330
- if (ciMode) {
331
- // dev_timeout: 90s in CI
332
- if (!base.dev_timeout) {
333
- config.dev_timeout = 90;
334
- }
335
- // lighthouse_runs: default to 3 in CI, but explicit config file value wins
336
- if (!base.lighthouse_runs) {
337
- config.lighthouse_runs = 3;
338
- }
339
- }
340
- // Skip lighthouse: max grade is Bronze
341
- if (options.cliFlags?.skipLighthouse) {
342
- // Effectively disables Lighthouse grading
343
- }
344
- // CLI flag overrides for new checks
345
- if (options.cliFlags?.typecheck !== undefined)
346
- config.typecheck = options.cliFlags.typecheck;
347
- if (options.cliFlags?.secretScan !== undefined)
348
- config.secret_scan = options.cliFlags.secretScan;
349
- if (options.cliFlags?.bundleSize !== undefined)
350
- config.bundle_size = options.cliFlags.bundleSize;
351
- if (options.cliFlags?.outdatedCheck !== undefined)
352
- config.outdated_check = options.cliFlags.outdatedCheck;
353
- if (options.cliFlags?.a11yDeep !== undefined)
354
- config.a11y_deep = options.cliFlags.a11yDeep;
355
- if (options.cliFlags?.seoDeep !== undefined)
356
- config.seo_deep = options.cliFlags.seoDeep;
357
- if (options.cliFlags?.vitalsBudget !== undefined)
358
- config.vitals_budget = options.cliFlags.vitalsBudget;
359
- return { ...config, ciMode };
360
- }
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ConfigParseError = void 0;
37
+ exports.fetchTeamThresholds = fetchTeamThresholds;
38
+ exports.loadConfig = loadConfig;
39
+ const fs = __importStar(require("node:fs"));
40
+ const path = __importStar(require("node:path"));
41
+ const yaml = __importStar(require("js-yaml"));
42
+ const auth_js_1 = require("./auth.js");
43
+ const DEFAULT_CONFIG = {
44
+ framework: "auto",
45
+ build_command: "",
46
+ dev_command: "",
47
+ package_manager: "auto",
48
+ port: 3000,
49
+ build_timeout: 300,
50
+ dev_timeout: 60,
51
+ lighthouse_runs: 3,
52
+ thresholds: {
53
+ performance: 70,
54
+ accessibility: 85,
55
+ seo: 80,
56
+ bestPractices: 80,
57
+ },
58
+ fail_on: "bronze",
59
+ crawl: false,
60
+ max_crawl_depth: 3,
61
+ max_crawl_pages: 10,
62
+ browsers: ["chromium"],
63
+ max_lighthouse_routes: 5,
64
+ visual_diff: {
65
+ pixelmatchThreshold: 0.1,
66
+ warnThreshold: 30,
67
+ rollbackThreshold: 60,
68
+ ignoreSelectors: [],
69
+ disableAnimations: true,
70
+ },
71
+ typecheck: false,
72
+ secret_scan: false,
73
+ secret_scan_ignore_paths: [],
74
+ bundle_size: false,
75
+ outdated_check: false,
76
+ a11y_deep: false,
77
+ seo_deep: false,
78
+ vitals_budget: false,
79
+ };
80
+ const VALID_FAIL_ON = ["unverified", "bronze", "silver", "gold"];
81
+ class ConfigParseError extends Error {
82
+ constructor(msg) {
83
+ super(msg);
84
+ this.name = "ConfigParseError";
85
+ }
86
+ }
87
+ exports.ConfigParseError = ConfigParseError;
88
+ function parseYaml(filePath) {
89
+ const content = fs.readFileSync(filePath, "utf-8");
90
+ const raw = yaml.load(content);
91
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
92
+ throw new ConfigParseError("Invalid YAML structure in .laxy.yml");
93
+ }
94
+ const result = {};
95
+ if (typeof raw.framework === "string")
96
+ result.framework = raw.framework;
97
+ if (typeof raw.build_command === "string")
98
+ result.build_command = raw.build_command;
99
+ if (typeof raw.dev_command === "string")
100
+ result.dev_command = raw.dev_command;
101
+ if (typeof raw.package_manager === "string")
102
+ result.package_manager = raw.package_manager;
103
+ if (typeof raw.port === "number")
104
+ result.port = raw.port;
105
+ if (typeof raw.build_timeout === "number")
106
+ result.build_timeout = raw.build_timeout;
107
+ if (typeof raw.dev_timeout === "number")
108
+ result.dev_timeout = raw.dev_timeout;
109
+ if (typeof raw.lighthouse_runs === "number")
110
+ result.lighthouse_runs = raw.lighthouse_runs;
111
+ if (typeof raw.crawl === "boolean")
112
+ result.crawl = raw.crawl;
113
+ if (typeof raw.max_crawl_depth === "number")
114
+ result.max_crawl_depth = raw.max_crawl_depth;
115
+ if (typeof raw.max_crawl_pages === "number")
116
+ result.max_crawl_pages = raw.max_crawl_pages;
117
+ if (Array.isArray(raw.browsers)) {
118
+ const validBrowsers = ["chromium", "firefox", "webkit"];
119
+ const browsers = raw.browsers
120
+ .filter((b) => typeof b === "string" && validBrowsers.includes(b));
121
+ if (browsers.length > 0)
122
+ result.browsers = browsers;
123
+ }
124
+ if (Array.isArray(raw.lighthouse_routes)) {
125
+ const routes = raw.lighthouse_routes
126
+ .filter((r) => typeof r === "string" && r.startsWith("/"))
127
+ .slice(0, 20);
128
+ if (routes.length > 0)
129
+ result.lighthouse_routes = routes;
130
+ }
131
+ if (Array.isArray(raw.extra_routes)) {
132
+ const routes = raw.extra_routes
133
+ .filter((r) => typeof r === "string" && r.startsWith("/"))
134
+ .slice(0, 20);
135
+ if (routes.length > 0)
136
+ result.extra_routes = routes;
137
+ }
138
+ if (typeof raw.max_lighthouse_routes === "number") {
139
+ result.max_lighthouse_routes = Math.max(1, Math.min(20, raw.max_lighthouse_routes));
140
+ }
141
+ if (typeof raw.visual_diff === "object" &&
142
+ raw.visual_diff !== null &&
143
+ !Array.isArray(raw.visual_diff)) {
144
+ const vd = raw.visual_diff;
145
+ const visualDiff = {};
146
+ if (typeof vd.pixelmatch_threshold === "number") {
147
+ visualDiff.pixelmatchThreshold = Math.max(0, Math.min(1, vd.pixelmatch_threshold));
148
+ }
149
+ if (typeof vd.warn_threshold === "number") {
150
+ visualDiff.warnThreshold = Math.max(0, Math.min(100, vd.warn_threshold));
151
+ }
152
+ if (typeof vd.rollback_threshold === "number") {
153
+ visualDiff.rollbackThreshold = Math.max(0, Math.min(100, vd.rollback_threshold));
154
+ }
155
+ if (Array.isArray(vd.ignore_selectors)) {
156
+ visualDiff.ignoreSelectors = vd.ignore_selectors
157
+ .filter((selector) => typeof selector === "string" && selector.trim().length > 0)
158
+ .slice(0, 30);
159
+ }
160
+ if (typeof vd.disable_animations === "boolean") {
161
+ visualDiff.disableAnimations = vd.disable_animations;
162
+ }
163
+ result.visual_diff = visualDiff;
164
+ }
165
+ if (typeof raw.fail_on === "string") {
166
+ const f = raw.fail_on;
167
+ if (!VALID_FAIL_ON.includes(f)) {
168
+ throw new ConfigParseError(`Invalid fail_on value: "${f}". Must be one of: ${VALID_FAIL_ON.join(", ")}`);
169
+ }
170
+ result.fail_on = f;
171
+ }
172
+ if (typeof raw.thresholds === "object" &&
173
+ raw.thresholds !== null &&
174
+ !Array.isArray(raw.thresholds)) {
175
+ const t = raw.thresholds;
176
+ const thr = {};
177
+ if (typeof t.performance === "number")
178
+ thr.performance = t.performance;
179
+ if (typeof t.accessibility === "number")
180
+ thr.accessibility = t.accessibility;
181
+ if (typeof t.seo === "number")
182
+ thr.seo = t.seo;
183
+ if (typeof t.best_practices === "number")
184
+ thr.bestPractices = t.best_practices;
185
+ result.thresholds = thr;
186
+ }
187
+ if (Array.isArray(raw.scenarios)) {
188
+ const scenarios = [];
189
+ for (const item of raw.scenarios) {
190
+ if (typeof item === "object" &&
191
+ item !== null &&
192
+ typeof item.name === "string" &&
193
+ Array.isArray(item.steps)) {
194
+ const rawScenario = item;
195
+ const steps = [];
196
+ for (const rawStep of rawScenario.steps) {
197
+ if (typeof rawStep === "object" && rawStep !== null) {
198
+ const s = rawStep;
199
+ const step = {};
200
+ if (typeof s.goto === "string")
201
+ step.goto = s.goto;
202
+ if (typeof s.fill === "string")
203
+ step.fill = s.fill;
204
+ if (typeof s.with === "string")
205
+ step.with = s.with;
206
+ if (typeof s.click === "string")
207
+ step.click = s.click;
208
+ if (typeof s.expect_visible === "string")
209
+ step.expect_visible = s.expect_visible;
210
+ if (typeof s.expect_text === "string")
211
+ step.expect_text = s.expect_text;
212
+ if (typeof s.wait === "number")
213
+ step.wait = s.wait;
214
+ steps.push(step);
215
+ }
216
+ }
217
+ scenarios.push({ name: String(rawScenario.name), steps });
218
+ }
219
+ }
220
+ if (scenarios.length > 0) {
221
+ result.scenarios = scenarios;
222
+ }
223
+ }
224
+ if (typeof raw.typecheck === "boolean")
225
+ result.typecheck = raw.typecheck;
226
+ if (typeof raw.secret_scan === "boolean")
227
+ result.secret_scan = raw.secret_scan;
228
+ if (Array.isArray(raw.secret_scan_ignore_paths)) {
229
+ result.secret_scan_ignore_paths = raw.secret_scan_ignore_paths
230
+ .filter((p) => typeof p === "string");
231
+ }
232
+ if (typeof raw.bundle_size === "boolean")
233
+ result.bundle_size = raw.bundle_size;
234
+ if (typeof raw.outdated_check === "boolean")
235
+ result.outdated_check = raw.outdated_check;
236
+ if (typeof raw.a11y_deep === "boolean")
237
+ result.a11y_deep = raw.a11y_deep;
238
+ if (typeof raw.seo_deep === "boolean")
239
+ result.seo_deep = raw.seo_deep;
240
+ if (typeof raw.vitals_budget === "boolean")
241
+ result.vitals_budget = raw.vitals_budget;
242
+ return result;
243
+ }
244
+ /**
245
+ * 로그인된 CLI 토큰으로 팀 공통 임계값을 서버에서 가져온다.
246
+ * 토큰 없음 / 팀 없음 / 네트워크 오류 시 null 반환 (graceful degradation).
247
+ */
248
+ async function fetchTeamThresholds() {
249
+ const token = (0, auth_js_1.loadToken)();
250
+ if (!token)
251
+ return null;
252
+ try {
253
+ const res = await fetch(`${auth_js_1.LAXY_API_URL}/api/v1/team-thresholds`, {
254
+ headers: { Authorization: `Bearer ${token}` },
255
+ signal: AbortSignal.timeout(5000),
256
+ });
257
+ if (!res.ok)
258
+ return null;
259
+ const data = (await res.json());
260
+ return data.thresholds ?? null;
261
+ }
262
+ catch {
263
+ return null;
264
+ }
265
+ }
266
+ function loadConfig(options) {
267
+ const configPath = options.configPath ?? path.join(options.dir, ".laxy.yml");
268
+ let base = {};
269
+ const hasLocalConfig = fs.existsSync(configPath);
270
+ if (hasLocalConfig) {
271
+ base = parseYaml(configPath);
272
+ }
273
+ // 팀 임계값: 로컬 .laxy.yml에 thresholds가 없을 때만 적용
274
+ const team = options.teamThresholds ?? null;
275
+ const teamThresholdFallback = (!hasLocalConfig || !base.thresholds) && team
276
+ ? {
277
+ performance: team.performance,
278
+ accessibility: team.accessibility,
279
+ seo: team.seo,
280
+ bestPractices: team.best_practices,
281
+ }
282
+ : {};
283
+ const teamFailOnFallback = (!hasLocalConfig || !base.fail_on) && team ? team.fail_on : undefined;
284
+ const config = {
285
+ ...DEFAULT_CONFIG,
286
+ framework: base.framework ?? DEFAULT_CONFIG.framework,
287
+ build_command: base.build_command ?? DEFAULT_CONFIG.build_command,
288
+ dev_command: base.dev_command ?? DEFAULT_CONFIG.dev_command,
289
+ package_manager: base.package_manager ?? DEFAULT_CONFIG.package_manager,
290
+ port: base.port ?? DEFAULT_CONFIG.port,
291
+ build_timeout: base.build_timeout ?? DEFAULT_CONFIG.build_timeout,
292
+ dev_timeout: base.dev_timeout ?? DEFAULT_CONFIG.dev_timeout,
293
+ lighthouse_runs: base.lighthouse_runs ?? DEFAULT_CONFIG.lighthouse_runs,
294
+ fail_on: base.fail_on ?? teamFailOnFallback ?? DEFAULT_CONFIG.fail_on,
295
+ scenarios: base.scenarios,
296
+ crawl: base.crawl ?? DEFAULT_CONFIG.crawl,
297
+ max_crawl_depth: base.max_crawl_depth ?? DEFAULT_CONFIG.max_crawl_depth,
298
+ max_crawl_pages: base.max_crawl_pages ?? DEFAULT_CONFIG.max_crawl_pages,
299
+ browsers: base.browsers ?? DEFAULT_CONFIG.browsers,
300
+ lighthouse_routes: base.lighthouse_routes,
301
+ extra_routes: base.extra_routes,
302
+ max_lighthouse_routes: base.max_lighthouse_routes ?? DEFAULT_CONFIG.max_lighthouse_routes,
303
+ visual_diff: {
304
+ ...DEFAULT_CONFIG.visual_diff,
305
+ ...(base.visual_diff ?? {}),
306
+ },
307
+ typecheck: base.typecheck ?? DEFAULT_CONFIG.typecheck,
308
+ secret_scan: base.secret_scan ?? DEFAULT_CONFIG.secret_scan,
309
+ secret_scan_ignore_paths: base.secret_scan_ignore_paths ?? DEFAULT_CONFIG.secret_scan_ignore_paths,
310
+ bundle_size: base.bundle_size ?? DEFAULT_CONFIG.bundle_size,
311
+ outdated_check: base.outdated_check ?? DEFAULT_CONFIG.outdated_check,
312
+ a11y_deep: base.a11y_deep ?? DEFAULT_CONFIG.a11y_deep,
313
+ seo_deep: base.seo_deep ?? DEFAULT_CONFIG.seo_deep,
314
+ vitals_budget: base.vitals_budget ?? DEFAULT_CONFIG.vitals_budget,
315
+ };
316
+ config.thresholds = {
317
+ ...DEFAULT_CONFIG.thresholds,
318
+ ...teamThresholdFallback,
319
+ ...(base.thresholds ?? {}),
320
+ };
321
+ // CLI flag overrides
322
+ if (options.cliFlags?.failOn !== undefined) {
323
+ if (!VALID_FAIL_ON.includes(options.cliFlags.failOn)) {
324
+ throw new ConfigParseError(`Invalid --fail-on value: "${options.cliFlags.failOn}". Must be one of: ${VALID_FAIL_ON.join(", ")}`);
325
+ }
326
+ config.fail_on = options.cliFlags.failOn;
327
+ }
328
+ // CI mode: apply CI defaults
329
+ const ciMode = options.ciMode;
330
+ if (ciMode) {
331
+ // dev_timeout: 90s in CI
332
+ if (!base.dev_timeout) {
333
+ config.dev_timeout = 90;
334
+ }
335
+ // lighthouse_runs: default to 3 in CI, but explicit config file value wins
336
+ if (!base.lighthouse_runs) {
337
+ config.lighthouse_runs = 3;
338
+ }
339
+ }
340
+ // Skip lighthouse: max grade is Bronze
341
+ if (options.cliFlags?.skipLighthouse) {
342
+ // Effectively disables Lighthouse grading
343
+ }
344
+ // CLI flag overrides for new checks
345
+ if (options.cliFlags?.typecheck !== undefined)
346
+ config.typecheck = options.cliFlags.typecheck;
347
+ if (options.cliFlags?.secretScan !== undefined)
348
+ config.secret_scan = options.cliFlags.secretScan;
349
+ if (options.cliFlags?.bundleSize !== undefined)
350
+ config.bundle_size = options.cliFlags.bundleSize;
351
+ if (options.cliFlags?.outdatedCheck !== undefined)
352
+ config.outdated_check = options.cliFlags.outdatedCheck;
353
+ if (options.cliFlags?.a11yDeep !== undefined)
354
+ config.a11y_deep = options.cliFlags.a11yDeep;
355
+ if (options.cliFlags?.seoDeep !== undefined)
356
+ config.seo_deep = options.cliFlags.seoDeep;
357
+ if (options.cliFlags?.vitalsBudget !== undefined)
358
+ config.vitals_budget = options.cliFlags.vitalsBudget;
359
+ return { ...config, ciMode };
360
+ }