#!/usr/bin/env node const fs = require('fs'); const os = require('os'); const path = require('path'); const { spawnSync } = require('child_process'); const SOURCE_ROOT = path.resolve(__dirname, '..'); const WORKSPACE_ROOT = process.cwd(); const DEFAULT_TARGET = path.join(os.homedir(), '.claude', 'plugins', 'measurement-marker-check'); const WORKSPACE_TARGET = path.join(WORKSPACE_ROOT, '.claude', 'plugins', 'measurement-marker-check'); const WORKSPACE_PLUGINS_ROOT = path.join(WORKSPACE_ROOT, '.claude', 'plugins'); const COPY_ENTRIES = [ '.claude-plugin', 'README.md', 'scripts', 'skill-package-manifest.json', 'skills', 'test-fixtures', 'tools', 'package.json' ]; function expandHome(value) { return String(value || '').replace(/^~(?=$|[\\/])/, os.homedir()); } function parseArgs(argv) { const first = argv[0] && !argv[0].startsWith('--') ? argv[0] : 'install'; const args = { command: first, target: DEFAULT_TARGET, smoke: false, force: false, help: false }; if (first === 'workspace' || first === 'install-workspace') { args.command = 'install'; args.target = WORKSPACE_TARGET; } for (let i = first === argv[0] ? 1 : 0; i < argv.length; i++) { const token = argv[i]; if (token === '--target' && argv[i + 1]) { args.target = argv[++i]; } else if (token.startsWith('--target=')) { args.target = token.slice('--target='.length); } else if (token === '--workspace') { args.target = WORKSPACE_TARGET; } else if (token === '--smoke') { args.smoke = true; } else if (token === '--force') { args.force = true; } else if (token === '--help' || token === '-h') { args.help = true; } } args.target = path.resolve(expandHome(args.target)); return args; } function printHelp() { console.log([ 'Claude measurement marker skill package installer', '', 'Usage:', ' claude-measure-marker install [--target ] [--smoke]', ' claude-measure-marker workspace [--smoke]', ' claude-measure-marker install --workspace [--smoke]', ' claude-measure-marker check', ' claude-measure-marker smoke', ' claude-measure-marker path', '', 'npx:', ' npx @gangvy/claude-code-measurement-marker-check install', ' npx @gangvy/claude-code-measurement-marker-check workspace --smoke', '', 'Options:', ' --workspace Install into ./.claude/plugins/measurement-marker-check', ' --target Install into a custom directory', ' --force Allow overwriting an existing custom target', ' --smoke Run sample smoke checks after install', ' --help, -h Show help' ].join('\n')); } function ensureDir(dirPath) { fs.mkdirSync(dirPath, { recursive: true }); } function isInside(parentDir, childDir) { const relative = path.relative(path.resolve(parentDir), path.resolve(childDir)); return relative === '' || (!!relative && !relative.startsWith('..') && !path.isAbsolute(relative)); } function isSafeDefaultTarget(targetDir) { return path.resolve(targetDir) === path.resolve(DEFAULT_TARGET); } function isSafeWorkspaceTarget(targetDir) { return isInside(WORKSPACE_PLUGINS_ROOT, targetDir); } function canOverwriteTarget(targetDir, force) { return force || isSafeDefaultTarget(targetDir) || isSafeWorkspaceTarget(targetDir); } function copyDirRecursive(source, destination) { const stat = fs.statSync(source); if (stat.isDirectory()) { ensureDir(destination); for (const child of fs.readdirSync(source)) { if (child === 'node_modules' || child === 'outputs' || child === '.git') continue; copyDirRecursive(path.join(source, child), path.join(destination, child)); } return; } ensureDir(path.dirname(destination)); fs.copyFileSync(source, destination); } function copyPlugin(target, force) { if (fs.existsSync(target)) { if (!canOverwriteTarget(target, force)) { throw new Error(`Refusing to overwrite custom target without --force: ${target}`); } fs.rmSync(target, { recursive: true, force: true }); } ensureDir(target); for (const entry of COPY_ENTRIES) { const source = path.join(SOURCE_ROOT, entry); if (!fs.existsSync(source)) continue; copyDirRecursive(source, path.join(target, entry)); } } function checkPlugin(target) { const required = [ '.claude-plugin/plugin.json', 'skills/measurement-marker-check/SKILL.md', 'tools/measurement-marker-check-run.js', 'scripts/smoke.js', 'package.json' ]; const missing = required.filter(entry => !fs.existsSync(path.join(target, entry))); if (missing.length) { throw new Error(`Install target is missing required files: ${missing.join(', ')}`); } return { status: 'ok', target, required }; } function asForwardSlash(filePath) { return path.resolve(filePath).replace(/\\/g, '/'); } function quotedPath(filePath) { return `"${asForwardSlash(filePath)}"`; } function workspaceSkillText(target) { const runner = path.join(target, 'tools', 'measurement-marker-check-run.js'); return [ '---', 'name: measurement-marker-check', 'description: Check 装修量尺图、手绘量尺单、户型草图或现场测量图片中是否存在疑似未标记位置,输出缺标记清单、待复核区域和下一步补标建议。Use when the user asks for 实战二、装修量尺、量尺图识别、尺寸图缺标记、哪里没标、未标注检查 or measurement drawing review.', 'allowed-tools: Read Write Bash(node *)', '---', '', '# 装修量尺图缺标记检查官', '', '你是面向设计/量尺团队的量尺图缺标记检查官。用户要的是业务复核清单,不是技术日志。', '', '## 工作流', '', '1. 如果用户没有上传图片或给出图片路径,先请用户上传量尺图、手绘量尺单、户型草图或现场测量截图。', '2. 如果用户上传了图片,先直接观察图片内容,整理已标记内容、疑似缺标位置、待人工复核区域。', '3. 把观察结果写成结构化 JSON,保存到 `outputs/measurement-marker-check/input.json`。', '4. 调用本地工具生成稳定报告。', '5. 优先把工具返回的 `assistantMessage` 正文发给用户,文件路径只放末尾作为归档信息。', '', '## 检查口径', '', '- 空间名称:客厅、卧室、厨房、卫生间、阳台等。', '- 关键尺寸:长、宽、高、门洞、窗洞、墙面、柜体位置等。', '- 门窗洞口:宽高、离地高度、开启方向、门套或窗台限制。', '- 梁柱/墙体限制:梁高、柱位、墙厚、承重/非承重提示。', '- 水电/管道/烟道:下水、地漏、插座、开关、燃气、烟道、排水管。', '- 安装避让备注:需避让、不可拆、需复核、现场限制、手写备注。', '', '## 工具输入 JSON 结构', '', '```json', '{', ' "project": "项目或样本名称",', ' "image": "图片文件名或路径",', ' "detectedMarkers": [', ' { "area": "厨房右侧墙面", "markerType": "关键尺寸", "value": "3200mm", "evidence": "墙面横向尺寸已标注", "confidence": 0.9 }', ' ],', ' "missingMarkers": [', ' { "area": "窗户下沿", "missingType": "窗台高度", "risk": "可能影响台面、吊柜或窗帘盒设计", "evidence": "窗户位置已画出,但没有离地高度标注", "confidence": 0.78 }', ' ],', ' "uncertainAreas": [', ' { "area": "厨房右上角", "reason": "疑似烟道或立管,但文字不清晰", "action": "请现场照片或原始量尺单复核", "confidence": 0.58 }', ' ],', ' "overallJudgement": "存在关键缺标记,建议人工复核后再进入标准尺寸表。"', '}', '```', '', '## 生成报告命令', '', '```bash', `node ${quotedPath(runner)} --input "outputs/measurement-marker-check/input.json" --output "outputs/measurement-marker-check" --result-prefix MEASURE_MARKER_RESULT`, '```', '', '## 演示样例命令', '', '```bash', `node ${quotedPath(runner)} --sample --output "outputs/measurement-marker-check-sample" --result-prefix MEASURE_MARKER_RESULT`, '```', '', '## 输出规则', '', '- 先给结论:是否存在疑似缺标记。', '- 必须明确“区域 + 缺失类型 + 风险 + 图像依据 + 置信度”。', '- 看不清的地方归入“待人工复核区域”,不要强行判断。', '- 不要把 P0 结果说成最终工程结论;它是辅助复核清单。', '- 不要只返回文件路径。', '', '## 推荐启动话术', '', '```text', '帮我检查这张装修量尺图有没有哪里未标记。', '重点看空间名称、门窗洞口、长宽高、管道、插座/烟道和现场限制备注。', '先给我缺标记清单和人工复核建议。', '```', '' ].join('\n'); } function writeWorkspaceSkillEntry(target) { if (!isSafeWorkspaceTarget(target)) return; const skillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'measurement-marker-check'); ensureDir(skillDir); fs.writeFileSync(path.join(skillDir, 'SKILL.md'), workspaceSkillText(target), 'utf8'); console.log('Wrote workspace skill entry: .\\.claude\\skills\\measurement-marker-check\\SKILL.md'); } function runSmoke(target) { const result = spawnSync(process.execPath, ['scripts/smoke.js'], { cwd: target, stdio: 'inherit', shell: false }); if (result.status !== 0) { throw new Error('smoke failed'); } } function printNextSteps(target) { const workspaceMode = isSafeWorkspaceTarget(target); console.log(''); console.log('Install complete.'); console.log(''); if (workspaceMode) { console.log('VSCode workspace mode is ready.'); console.log(''); console.log('Generated project files:'); console.log(' .\\.claude\\skills\\measurement-marker-check\\SKILL.md'); console.log(' .\\.claude\\plugins\\measurement-marker-check'); console.log(''); console.log('Restart the VSCode Claude Code session if it was already open.'); } else { console.log('Claude Code CLI launch command:'); console.log(` claude --plugin-dir "${target}"`); } console.log(''); console.log('Try this prompt in Claude Code:'); console.log(' 帮我检查这张装修量尺图有没有哪里未标记。'); console.log(' 重点看空间名称、门窗洞口、长宽高、管道、插座/烟道和现场限制备注。'); console.log(' 先给我缺标记清单和人工复核建议。'); console.log(''); console.log(`Install target: ${target}`); } function main() { const args = parseArgs(process.argv.slice(2)); if (args.help || args.command === 'help') { printHelp(); return; } if (args.command === 'path') { console.log(args.target); return; } if (args.command === 'install') { copyPlugin(args.target, args.force); const check = checkPlugin(args.target); console.log(JSON.stringify(check, null, 2)); writeWorkspaceSkillEntry(args.target); if (args.smoke) runSmoke(args.target); printNextSteps(args.target); return; } if (args.command === 'check') { console.log(JSON.stringify(checkPlugin(args.target), null, 2)); return; } if (args.command === 'smoke') { runSmoke(SOURCE_ROOT); return; } printHelp(); process.exitCode = 1; } try { main(); } catch (error) { console.error(`claude-measure-marker failed: ${error.message}`); process.exit(1); }