laxy-verify 1.2.1 → 1.2.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.
package/README.md CHANGED
@@ -63,23 +63,18 @@ This is most useful if you ship frontend apps and want a practical gate before:
63
63
  - QA handoff
64
64
  - production release
65
65
 
66
- ## vs other tools
67
-
68
- No single competitor covers this combination. Here is where each tool stops:
69
-
70
- | | laxy-verify | LHCI | Checkly | Percy / Argos | Unlighthouse | QA Wolf |
71
- |--|--|--|--|--|--|--|
72
- | Build failure detection | yes | no | no | no | no | no |
73
- | Auto-generated E2E | yes | no | write your own | no | no | yes |
74
- | Security audit | yes | no | no | no | no | no |
75
- | Lighthouse (multi-run avg) | yes | yes | no | no | yes | no |
76
- | Visual diff | yes | no | no | yes | no | no |
77
- | Multi-viewport checks | yes | no | separate config | no | yes | no |
78
- | Ship/hold release decision | yes | score only | no | no | no | no |
79
- | Zero config to start | yes | no | no | no | partial | no |
80
- | Free full coverage | yes | yes | limited | limited | yes | enterprise |
81
-
82
- LHCI gives you Lighthouse. Percy gives you visual diffs. Checkly watches your production uptime. None of them produce a merge or release decision from one command.
66
+ ## Why not just LHCI?
67
+
68
+ | | laxy-verify | LHCI | Checkly | Percy |
69
+ |--|--|--|--|--|
70
+ | Production build failure detection | Yes | No | No | No |
71
+ | User-flow E2E verification | Yes | No | Manual setup | No |
72
+ | Lighthouse scoring | Yes | Yes | No | No |
73
+ | Visual regression check | Yes | No | No | Yes |
74
+ | Release decision (`hold` / `client-ready`) | Yes | No, score only | No | No |
75
+ | Zero-config local start | Yes | No | No | No |
76
+
77
+ LHCI measures Lighthouse. `laxy-verify` is for deciding whether this frontend is actually safe to ship.
83
78
 
84
79
  ## The failures it is meant to catch
85
80
 
package/dist/cli.js CHANGED
@@ -159,6 +159,7 @@ function parseArgs() {
159
159
  crawl: flags.crawl !== undefined,
160
160
  planOverride: flags["plan-override"],
161
161
  port: flags.port !== undefined ? Number(flags.port) : undefined,
162
+ share: flags.share !== undefined,
162
163
  help: flags.help !== undefined || flags.h !== undefined,
163
164
  };
164
165
  }
@@ -296,6 +297,9 @@ function consoleOutput(result) {
296
297
  if (result.markdownReportPath) {
297
298
  console.log(` Report: ${path.basename(result.markdownReportPath)}`);
298
299
  }
300
+ if (result.share?.url) {
301
+ console.log(` Share: ${result.share.url}`);
302
+ }
299
303
  console.log(` Exit code: ${result.exitCode}`);
300
304
  }
301
305
  async function run() {
@@ -324,6 +328,7 @@ async function run() {
324
328
  --fail-on unverified | bronze | silver | gold
325
329
  --skip-lighthouse Skip Lighthouse but still run build and E2E
326
330
  --port <port> Use an already-running dev server on this port (skip build & server start)
331
+ --share Create a public share link for this verification result (Pro)
327
332
  --plan-override free | pro | team (testing metadata only)
328
333
  --multi-viewport Lighthouse on desktop/tablet/mobile
329
334
  --crawl Crawl the app to discover routes before E2E
@@ -341,6 +346,7 @@ async function run() {
341
346
  npx laxy-verify . --ci # CI mode
342
347
  npx laxy-verify . --fail-on silver # Block Bronze or worse
343
348
  npx laxy-verify . --port 3001 # Use existing dev server on port 3001
349
+ npx laxy-verify . --share # Save and share a public verification link
344
350
 
345
351
  Docs: https://github.com/SUNgm24/Laxy/tree/main/laxy-verify
346
352
  `);
@@ -417,6 +423,11 @@ async function run() {
417
423
  exitGracefully(0);
418
424
  return;
419
425
  }
426
+ // 팀 공통 임계값을 서버에서 로드 (토큰 없음/네트워크 오류 시 null)
427
+ const teamThresholds = await (0, config_js_1.fetchTeamThresholds)();
428
+ if (teamThresholds && args.format !== "json") {
429
+ console.log(" Team thresholds loaded from laxy.dev");
430
+ }
420
431
  let config;
421
432
  try {
422
433
  config = (0, config_js_1.loadConfig)({
@@ -427,6 +438,7 @@ async function run() {
427
438
  failOn: args.failOn,
428
439
  skipLighthouse: args.skipLighthouse,
429
440
  },
441
+ teamThresholds,
430
442
  });
431
443
  }
432
444
  catch (err) {
@@ -465,7 +477,9 @@ async function run() {
465
477
  exitGracefully(2);
466
478
  return;
467
479
  }
468
- console.log(`Using existing dev server on port ${port} (HTTP ${status})`);
480
+ if (args.format !== "json") {
481
+ console.log(`Using existing dev server on port ${port} (HTTP ${status})`);
482
+ }
469
483
  }
470
484
  let buildResult;
471
485
  if (useExistingServer) {
@@ -814,6 +828,9 @@ async function run() {
814
828
  // Pro 이상: 결과를 서버에 저장 (배지, 공유 링크용)
815
829
  const token = (0, auth_js_1.loadToken)();
816
830
  const isPro = effectiveFeatures.plan === "pro" || effectiveFeatures.plan === "team";
831
+ if (args.share && (!token || !isPro)) {
832
+ console.warn(" [warn] --share requires a logged-in Pro or Team account.");
833
+ }
817
834
  if (token && isPro) {
818
835
  try {
819
836
  const repoId = (0, auth_js_1.getOrCreateRepoId)(args.projectDir);
@@ -828,7 +845,10 @@ async function run() {
828
845
  repo_id: repoId,
829
846
  project_name: path.basename(path.resolve(args.projectDir)),
830
847
  grade: unifiedGrade,
831
- verdict: verificationReport.verdict,
848
+ verdict: verificationReport.verdict === "client-ready" || verificationReport.verdict === "release-ready"
849
+ ? "client-ready"
850
+ : "hold",
851
+ share: args.share,
832
852
  scores: {
833
853
  performance: scores?.performance,
834
854
  accessibility: scores?.accessibility,
@@ -840,18 +860,36 @@ async function run() {
840
860
  console_error_count: e2eConsoleErrors.length,
841
861
  broken_link_count: brokenLinksResult?.brokenLinks.length,
842
862
  },
843
- full_result: { framework: detected.framework },
863
+ full_result: {
864
+ framework: detected.framework,
865
+ build: { success: buildResult.success },
866
+ e2e: e2eResult ? { passed: e2eResult.passed, total: e2eResult.total } : null,
867
+ lighthouse: lighthouseResult,
868
+ verification: {
869
+ report: verificationReport,
870
+ },
871
+ },
844
872
  }),
845
873
  });
846
874
  if (!saveRes.ok) {
847
875
  const errBody = await saveRes.json().catch(() => ({}));
848
876
  console.warn(` [warn] Result save failed (${saveRes.status}): ${errBody.error ?? "unknown error"}`);
849
877
  }
878
+ else {
879
+ const saveData = await saveRes.json().catch(() => ({}));
880
+ if (saveData.share_id && saveData.share_url) {
881
+ resultObj.share = {
882
+ id: saveData.share_id,
883
+ url: saveData.share_url,
884
+ };
885
+ }
886
+ }
850
887
  }
851
888
  catch {
852
889
  // 네트워크 오류는 검증 결과에 영향 없음
853
890
  }
854
891
  }
892
+ writeResultFile(args.projectDir, resultObj);
855
893
  if (args.format === "json") {
856
894
  console.log(JSON.stringify(resultObj, null, 2));
857
895
  }
package/dist/config.d.ts CHANGED
@@ -38,6 +38,18 @@ export interface LaxyConfig {
38
38
  export declare class ConfigParseError extends Error {
39
39
  constructor(msg: string);
40
40
  }
41
+ export interface TeamThresholdsConfig {
42
+ performance: number;
43
+ accessibility: number;
44
+ seo: number;
45
+ best_practices: number;
46
+ fail_on: FailOn;
47
+ }
48
+ /**
49
+ * 로그인된 CLI 토큰으로 팀 공통 임계값을 서버에서 가져온다.
50
+ * 토큰 없음 / 팀 없음 / 네트워크 오류 시 null 반환 (graceful degradation).
51
+ */
52
+ export declare function fetchTeamThresholds(): Promise<TeamThresholdsConfig | null>;
41
53
  export interface LoadConfigOptions {
42
54
  dir: string;
43
55
  configPath?: string;
@@ -46,6 +58,7 @@ export interface LoadConfigOptions {
46
58
  skipLighthouse?: boolean;
47
59
  };
48
60
  ciMode: boolean;
61
+ teamThresholds?: TeamThresholdsConfig | null;
49
62
  }
50
63
  export declare function loadConfig(options: LoadConfigOptions): LaxyConfig & {
51
64
  ciMode: boolean;
package/dist/config.js CHANGED
@@ -34,10 +34,12 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.ConfigParseError = void 0;
37
+ exports.fetchTeamThresholds = fetchTeamThresholds;
37
38
  exports.loadConfig = loadConfig;
38
39
  const fs = __importStar(require("node:fs"));
39
40
  const path = __importStar(require("node:path"));
40
41
  const yaml = __importStar(require("js-yaml"));
42
+ const auth_js_1 = require("./auth.js");
41
43
  const DEFAULT_CONFIG = {
42
44
  framework: "auto",
43
45
  build_command: "",
@@ -164,12 +166,46 @@ function parseYaml(filePath) {
164
166
  }
165
167
  return result;
166
168
  }
169
+ /**
170
+ * 로그인된 CLI 토큰으로 팀 공통 임계값을 서버에서 가져온다.
171
+ * 토큰 없음 / 팀 없음 / 네트워크 오류 시 null 반환 (graceful degradation).
172
+ */
173
+ async function fetchTeamThresholds() {
174
+ const token = (0, auth_js_1.loadToken)();
175
+ if (!token)
176
+ return null;
177
+ try {
178
+ const res = await fetch(`${auth_js_1.LAXY_API_URL}/api/v1/team-thresholds`, {
179
+ headers: { Authorization: `Bearer ${token}` },
180
+ signal: AbortSignal.timeout(5000),
181
+ });
182
+ if (!res.ok)
183
+ return null;
184
+ const data = (await res.json());
185
+ return data.thresholds ?? null;
186
+ }
187
+ catch {
188
+ return null;
189
+ }
190
+ }
167
191
  function loadConfig(options) {
168
192
  const configPath = options.configPath ?? path.join(options.dir, ".laxy.yml");
169
193
  let base = {};
170
- if (fs.existsSync(configPath)) {
194
+ const hasLocalConfig = fs.existsSync(configPath);
195
+ if (hasLocalConfig) {
171
196
  base = parseYaml(configPath);
172
197
  }
198
+ // 팀 임계값: 로컬 .laxy.yml에 thresholds가 없을 때만 적용
199
+ const team = options.teamThresholds ?? null;
200
+ const teamThresholdFallback = (!hasLocalConfig || !base.thresholds) && team
201
+ ? {
202
+ performance: team.performance,
203
+ accessibility: team.accessibility,
204
+ seo: team.seo,
205
+ bestPractices: team.best_practices,
206
+ }
207
+ : {};
208
+ const teamFailOnFallback = (!hasLocalConfig || !base.fail_on) && team ? team.fail_on : undefined;
173
209
  const config = {
174
210
  ...DEFAULT_CONFIG,
175
211
  framework: base.framework ?? DEFAULT_CONFIG.framework,
@@ -180,14 +216,18 @@ function loadConfig(options) {
180
216
  build_timeout: base.build_timeout ?? DEFAULT_CONFIG.build_timeout,
181
217
  dev_timeout: base.dev_timeout ?? DEFAULT_CONFIG.dev_timeout,
182
218
  lighthouse_runs: base.lighthouse_runs ?? DEFAULT_CONFIG.lighthouse_runs,
183
- fail_on: base.fail_on ?? DEFAULT_CONFIG.fail_on,
219
+ fail_on: base.fail_on ?? teamFailOnFallback ?? DEFAULT_CONFIG.fail_on,
184
220
  scenarios: base.scenarios,
185
221
  crawl: base.crawl ?? DEFAULT_CONFIG.crawl,
186
222
  max_crawl_depth: base.max_crawl_depth ?? DEFAULT_CONFIG.max_crawl_depth,
187
223
  max_crawl_pages: base.max_crawl_pages ?? DEFAULT_CONFIG.max_crawl_pages,
188
224
  browsers: base.browsers ?? DEFAULT_CONFIG.browsers,
189
225
  };
190
- config.thresholds = { ...DEFAULT_CONFIG.thresholds, ...(base.thresholds ?? {}) };
226
+ config.thresholds = {
227
+ ...DEFAULT_CONFIG.thresholds,
228
+ ...teamThresholdFallback,
229
+ ...(base.thresholds ?? {}),
230
+ };
191
231
  // CLI flag overrides
192
232
  if (options.cliFlags?.failOn !== undefined) {
193
233
  if (!VALID_FAIL_ON.includes(options.cliFlags.failOn)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "laxy-verify",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "Frontend verification CLI for build checks, Lighthouse, E2E, and release readiness",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",