smoke.js 4.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
  1. #!/usr/bin/env node
  2. // Copyright (c) 未来飞马
  3. //
  4. // This Source Code Form is subject to the terms of the Mozilla Public
  5. // License, v. 2.0. If a copy of the MPL was not distributed with this
  6. // file, You can obtain one at https://mozilla.org/MPL/2.0/.
  7. //
  8. // Trademark Notice:
  9. // The MPL-2.0 license grants copyright permissions for source code only.
  10. // It does NOT grant any rights to use trademarks including "未来飞马",
  11. // "Harness Loop", "RSI", and associated slogan "让AI进化提前发生,让AI落地快人一步".
  12. // Any use of these trademarks requires separate written permission.
  13. import fs from 'node:fs';
  14. import path from 'node:path';
  15. import { fileURLToPath, pathToFileURL } from 'node:url';
  16. const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
  17. const SKILL_DIR = path.join(ROOT, 'skills', 'skill-product-lab');
  18. function fail(msg) { console.error('SMOKE FAIL: ' + msg); process.exit(1); }
  19. // 1. 包结构
  20. if (!fs.existsSync(path.join(SKILL_DIR, 'SKILL.md'))) fail('missing skills/skill-product-lab/SKILL.md');
  21. for (const rel of ['bin/skill-product-lab.mjs', 'lib/skill-product-lab.mjs', 'skill-package-manifest.json', '.claude-plugin/plugin.json']) {
  22. if (!fs.existsSync(path.join(ROOT, rel))) fail('missing ' + rel);
  23. }
  24. // 2. 元数据一致性:包名/版本/bin 三处对齐
  25. const readJSON = rel => JSON.parse(fs.readFileSync(path.join(ROOT, rel), 'utf8'));
  26. const pkg = readJSON('package.json');
  27. const manifest = readJSON('skill-package-manifest.json');
  28. const plugin = readJSON('.claude-plugin/plugin.json');
  29. if (pkg.name !== 'skill-product-lab') fail('package.json name should be skill-product-lab, got ' + pkg.name);
  30. if (manifest.name !== 'skill-product-lab') fail('manifest name should be skill-product-lab, got ' + manifest.name);
  31. if (plugin.name !== 'skill-product-lab') fail('plugin.json name should be skill-product-lab, got ' + plugin.name);
  32. if (manifest.version !== pkg.version) fail(`version mismatch: manifest ${manifest.version} vs package ${pkg.version}`);
  33. if (plugin.version !== pkg.version) fail(`version mismatch: plugin ${plugin.version} vs package ${pkg.version}`);
  34. if (!pkg.bin || !pkg.bin['skill-product-lab']) fail('package.json bin should expose skill-product-lab');
  35. // 3. 无残留旧名(模式由片段拼出,避免本文件自匹配)
  36. // 版本历史里 "更名 old → new" 这类记录是有意保留的,不计为残留。
  37. const OLD_NAME = ['fmode', 'product', 'lab'].join('-');
  38. const OLD_RE = new RegExp(OLD_NAME);
  39. const RENAME_NOTE = /更名|renamed/i;
  40. const stale = [];
  41. (function walk(dir) {
  42. for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
  43. if (entry.name === '.git' || entry.name === 'node_modules') continue;
  44. const full = path.join(dir, entry.name);
  45. if (entry.isDirectory()) { walk(full); continue; }
  46. if (!/\.(js|mjs|json|md)$/.test(entry.name)) continue;
  47. const lines = fs.readFileSync(full, 'utf8').split('\n');
  48. if (lines.some(l => OLD_RE.test(l) && !RENAME_NOTE.test(l))) stale.push(path.relative(ROOT, full));
  49. }
  50. })(ROOT);
  51. if (stale.length) fail(`stale "${OLD_NAME}" references in: ` + stale.join(', '));
  52. // 4. 模块导出(SKILL.md 承诺的组件式调用)
  53. const mod = await import(pathToFileURL(path.join(ROOT, 'lib', 'skill-product-lab.mjs')).href);
  54. for (const fn of ['analyze', 'renderHTML']) {
  55. if (typeof mod[fn] !== 'function') fail('export ' + fn + ' is not a function');
  56. }
  57. // 5. analyze() 输出契约:九个板块齐全
  58. const d = mod.analyze('面向25-35岁白领的抗氧化胶原蛋白软糖,含VC,电商渠道');
  59. for (const key of ['raw', 'voc', 'marketShare', 'priceBand', 'competition', 'reposition', 'categoryPos', 'views', 'recommendation']) {
  60. if (d[key] === undefined) fail('analyze() result missing ' + key);
  61. }
  62. if (!d.recommendation || !d.recommendation.name) fail('analyze() recommendation has no name');
  63. for (const view of ['market', 'user', 'supply', 'process', 'regulation']) {
  64. if (!d.views[view]) fail('analyze() views missing ' + view);
  65. }
  66. // 6. renderHTML() 产出可用的完整 HTML
  67. const html = mod.renderHTML(d);
  68. if (typeof html !== 'string' || html.length < 500) fail('renderHTML() returned too little content');
  69. if (!/^<!DOCTYPE html>/i.test(html.trim())) fail('renderHTML() output is not a full HTML document');
  70. if (!html.includes(d.recommendation.name)) fail('renderHTML() does not mention the recommended product');
  71. console.log('SMOKE OK: skill-product-lab package structure + metadata + exports + analyze contract + HTML render verified');