| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334 |
- #!/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 <dir>] [--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 <dir> 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);
- }
|