feat(cli): add interactive area build menu

This commit is contained in:
2026-08-11 14:08:22 +08:00
parent d26921c6d1
commit b68be063ad
7 changed files with 517 additions and 55 deletions

View File

@@ -37,12 +37,14 @@ npm install
## 主流程 ## 主流程
默认构建南台子湖创新谷样例 交互式选择区域和构建阶段
```bash ```bash
npm run build npm run build
``` ```
使用方向键移动、空格多选阶段、回车开始构建;`intermediates``reimport` 不能同时选择。
指定区域配置: 指定区域配置:
```bash ```bash
@@ -310,7 +312,7 @@ npm run compress:glb -- \
南台子湖创新谷当前可直接使用下面这条命令: 南台子湖创新谷当前可直接使用下面这条命令:
```bash ```bash
npm run build -- --config config/areas/nantaizi-lake-innovation-valley.json --stages reimport,blender,cesium npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages reimport,blender,cesium
``` ```
`reimport` 阶段(`scripts/reimport-gpkg.js`)做两件事: `reimport` 阶段(`scripts/reimport-gpkg.js`)做两件事:

411
package-lock.json generated
View File

@@ -8,14 +8,425 @@
"name": "osm-asset-pipeline", "name": "osm-asset-pipeline",
"version": "0.3.0", "version": "0.3.0",
"dependencies": { "dependencies": {
"@inquirer/prompts": "^8.5.2",
"osm2streets-js-node": "0.1.4" "osm2streets-js-node": "0.1.4"
} }
}, },
"node_modules/@inquirer/ansi": {
"version": "2.0.7",
"resolved": "http://172.16.1.86:4873/@inquirer/ansi/-/ansi-2.0.7.tgz",
"integrity": "sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==",
"license": "MIT",
"engines": {
"node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
}
},
"node_modules/@inquirer/checkbox": {
"version": "5.2.1",
"resolved": "http://172.16.1.86:4873/@inquirer/checkbox/-/checkbox-5.2.1.tgz",
"integrity": "sha512-b6xmA/VlTe0ZgDQHDui+Nav470u7u49nRd8/iuhOcQPO9Ch7lGuogydhi2VOmNlZ+zXcM8IcPuNSwQcdJaF/kw==",
"license": "MIT",
"dependencies": {
"@inquirer/ansi": "^2.0.7",
"@inquirer/core": "^11.2.1",
"@inquirer/figures": "^2.0.7",
"@inquirer/type": "^4.0.7"
},
"engines": {
"node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/confirm": {
"version": "6.1.1",
"resolved": "http://172.16.1.86:4873/@inquirer/confirm/-/confirm-6.1.1.tgz",
"integrity": "sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ==",
"license": "MIT",
"dependencies": {
"@inquirer/core": "^11.2.1",
"@inquirer/type": "^4.0.7"
},
"engines": {
"node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/core": {
"version": "11.2.1",
"resolved": "http://172.16.1.86:4873/@inquirer/core/-/core-11.2.1.tgz",
"integrity": "sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==",
"license": "MIT",
"dependencies": {
"@inquirer/ansi": "^2.0.7",
"@inquirer/figures": "^2.0.7",
"@inquirer/type": "^4.0.7",
"cli-width": "^4.1.0",
"fast-wrap-ansi": "^0.2.0",
"mute-stream": "^3.0.0",
"signal-exit": "^4.1.0"
},
"engines": {
"node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/editor": {
"version": "5.2.2",
"resolved": "http://172.16.1.86:4873/@inquirer/editor/-/editor-5.2.2.tgz",
"integrity": "sha512-ZRVd/oD+sYsUd5zVm0NflqEzlqfYCyHNsqkHl2oWXEUHs12tCbcSFi+wVFEvD8+LGRaMUsVrE7qeo6lSG/S1Vg==",
"license": "MIT",
"dependencies": {
"@inquirer/core": "^11.2.1",
"@inquirer/external-editor": "^3.0.3",
"@inquirer/type": "^4.0.7"
},
"engines": {
"node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/expand": {
"version": "5.1.1",
"resolved": "http://172.16.1.86:4873/@inquirer/expand/-/expand-5.1.1.tgz",
"integrity": "sha512-YmQpenjbFSHAK3sOd44puHh3V1KXXr+JiNpUztoSQ4drLh2rTVzTap/YtlAVu/5xavifIlBfNEzJ/neZJ1a/1g==",
"license": "MIT",
"dependencies": {
"@inquirer/core": "^11.2.1",
"@inquirer/type": "^4.0.7"
},
"engines": {
"node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/external-editor": {
"version": "3.0.3",
"resolved": "http://172.16.1.86:4873/@inquirer/external-editor/-/external-editor-3.0.3.tgz",
"integrity": "sha512-6thf5I8q7lZwzGLAxPaaGEREEkZ3nyePPDQ1oyobblxmEE8mqTLguScP7pDjUTAibiyb4hfXl+qjUEJ+di/aNA==",
"license": "MIT",
"dependencies": {
"chardet": "^2.1.1",
"iconv-lite": "^0.7.2"
},
"engines": {
"node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/figures": {
"version": "2.0.7",
"resolved": "http://172.16.1.86:4873/@inquirer/figures/-/figures-2.0.7.tgz",
"integrity": "sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==",
"license": "MIT",
"engines": {
"node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
}
},
"node_modules/@inquirer/input": {
"version": "5.1.2",
"resolved": "http://172.16.1.86:4873/@inquirer/input/-/input-5.1.2.tgz",
"integrity": "sha512-9K/DDBSQpOyZSkt6sOVP9Vo0TR7atX2kuILsUu0x3wVcVbe97lJwIJKMLdMw25tDYuXl/qp6erT0Xs1rfmcfZg==",
"license": "MIT",
"dependencies": {
"@inquirer/core": "^11.2.1",
"@inquirer/type": "^4.0.7"
},
"engines": {
"node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/number": {
"version": "4.1.1",
"resolved": "http://172.16.1.86:4873/@inquirer/number/-/number-4.1.1.tgz",
"integrity": "sha512-XF4IXAbPnGPgw0wsbC/i2tPcyfdZgDpUlhsqU0SfT4IRIGWha6Xm9VRgN5yYxJq+jnyXlfXI/nQ3ulfk0iEICA==",
"license": "MIT",
"dependencies": {
"@inquirer/core": "^11.2.1",
"@inquirer/type": "^4.0.7"
},
"engines": {
"node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/password": {
"version": "5.1.1",
"resolved": "http://172.16.1.86:4873/@inquirer/password/-/password-5.1.1.tgz",
"integrity": "sha512-3XBfF7DAsp5qeDsvN5Rd1HmbNokVvEQoUM0QLrRcybC9nX96w3Pbmu7qUsb3IT3J3jBvs2+mTXaKHOUsgHMLzg==",
"license": "MIT",
"dependencies": {
"@inquirer/ansi": "^2.0.7",
"@inquirer/core": "^11.2.1",
"@inquirer/type": "^4.0.7"
},
"engines": {
"node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/prompts": {
"version": "8.5.2",
"resolved": "http://172.16.1.86:4873/@inquirer/prompts/-/prompts-8.5.2.tgz",
"integrity": "sha512-IYR/3C/paEVVQYQvdDlFZVjRCJVYHHON0XXMH91KO9GSxs0TdKYWlUdvfQl2EfAHDxUaN3IBffkE/BDTh5nJ6g==",
"license": "MIT",
"dependencies": {
"@inquirer/checkbox": "^5.2.1",
"@inquirer/confirm": "^6.1.1",
"@inquirer/editor": "^5.2.2",
"@inquirer/expand": "^5.1.1",
"@inquirer/input": "^5.1.2",
"@inquirer/number": "^4.1.1",
"@inquirer/password": "^5.1.1",
"@inquirer/rawlist": "^5.3.1",
"@inquirer/search": "^4.2.1",
"@inquirer/select": "^5.2.1"
},
"engines": {
"node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/rawlist": {
"version": "5.3.1",
"resolved": "http://172.16.1.86:4873/@inquirer/rawlist/-/rawlist-5.3.1.tgz",
"integrity": "sha512-QqdTqQddL3qPX/PPrjobpsO25NZ4dWXgTLenrR445L2ptLEYE6Z+PD5c5CNDJNx4ugRgELAIpSIJxZaO2jJ2Og==",
"license": "MIT",
"dependencies": {
"@inquirer/core": "^11.2.1",
"@inquirer/type": "^4.0.7"
},
"engines": {
"node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/search": {
"version": "4.2.1",
"resolved": "http://172.16.1.86:4873/@inquirer/search/-/search-4.2.1.tgz",
"integrity": "sha512-xJj8QWKRSrfKoBIITLZK61dD3zwo0Rz11fgDImku30/Oe81zMdIdGgrLY2h6RkJ+KZ/GhNYIRMKnH/62qBTA5g==",
"license": "MIT",
"dependencies": {
"@inquirer/core": "^11.2.1",
"@inquirer/figures": "^2.0.7",
"@inquirer/type": "^4.0.7"
},
"engines": {
"node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/select": {
"version": "5.2.1",
"resolved": "http://172.16.1.86:4873/@inquirer/select/-/select-5.2.1.tgz",
"integrity": "sha512-FlDndEUww8m7BfukO2nJa25vhD+H5jxxCv4oGioKqzyWz3nPHhhw4LKdYRSlXuAx7DsdWia7iyaBPKKS95Evfw==",
"license": "MIT",
"dependencies": {
"@inquirer/ansi": "^2.0.7",
"@inquirer/core": "^11.2.1",
"@inquirer/figures": "^2.0.7",
"@inquirer/type": "^4.0.7"
},
"engines": {
"node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/type": {
"version": "4.0.7",
"resolved": "http://172.16.1.86:4873/@inquirer/type/-/type-4.0.7.tgz",
"integrity": "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==",
"license": "MIT",
"engines": {
"node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/chardet": {
"version": "2.2.0",
"resolved": "http://172.16.1.86:4873/chardet/-/chardet-2.2.0.tgz",
"integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==",
"license": "MIT"
},
"node_modules/cli-width": {
"version": "4.1.0",
"resolved": "http://172.16.1.86:4873/cli-width/-/cli-width-4.1.0.tgz",
"integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
"license": "ISC",
"engines": {
"node": ">= 12"
}
},
"node_modules/fast-string-truncated-width": {
"version": "3.0.3",
"resolved": "http://172.16.1.86:4873/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz",
"integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==",
"license": "MIT"
},
"node_modules/fast-string-width": {
"version": "3.0.2",
"resolved": "http://172.16.1.86:4873/fast-string-width/-/fast-string-width-3.0.2.tgz",
"integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==",
"license": "MIT",
"dependencies": {
"fast-string-truncated-width": "^3.0.2"
}
},
"node_modules/fast-wrap-ansi": {
"version": "0.2.2",
"resolved": "http://172.16.1.86:4873/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz",
"integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==",
"license": "MIT",
"dependencies": {
"fast-string-width": "^3.0.2"
}
},
"node_modules/iconv-lite": {
"version": "0.7.3",
"resolved": "http://172.16.1.86:4873/iconv-lite/-/iconv-lite-0.7.3.tgz",
"integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/mute-stream": {
"version": "3.0.0",
"resolved": "http://172.16.1.86:4873/mute-stream/-/mute-stream-3.0.0.tgz",
"integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==",
"license": "ISC",
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/osm2streets-js-node": { "node_modules/osm2streets-js-node": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/osm2streets-js-node/-/osm2streets-js-node-0.1.4.tgz", "resolved": "https://registry.npmjs.org/osm2streets-js-node/-/osm2streets-js-node-0.1.4.tgz",
"integrity": "sha512-JjS6qJJjrrlEKEZxqV4s/N4qU0YfvdMUasdvfoyYCmZxEpNKAYjpuZeWJWeLEcGOqhu+YuV3YDIk9jgvMU0kKQ==", "integrity": "sha512-JjS6qJJjrrlEKEZxqV4s/N4qU0YfvdMUasdvfoyYCmZxEpNKAYjpuZeWJWeLEcGOqhu+YuV3YDIk9jgvMU0kKQ==",
"license": "Apache-2.0" "license": "Apache-2.0"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "http://172.16.1.86:4873/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "http://172.16.1.86:4873/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"license": "ISC",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
} }
} }
} }

View File

@@ -4,7 +4,7 @@
"private": true, "private": true,
"type": "commonjs", "type": "commonjs",
"scripts": { "scripts": {
"build": "node scripts/build-area.js", "build": "node scripts/interactive-build.js",
"build:area": "node scripts/build-area.js", "build:area": "node scripts/build-area.js",
"build:qgis": "node scripts/build-osm2streets-qgis.js", "build:qgis": "node scripts/build-osm2streets-qgis.js",
"check:area": "node scripts/check-area.js", "check:area": "node scripts/check-area.js",
@@ -12,6 +12,7 @@
"diagnose:area": "node scripts/diagnose-area.js", "diagnose:area": "node scripts/diagnose-area.js",
"preflight:area": "node scripts/preflight-area.js", "preflight:area": "node scripts/preflight-area.js",
"test:preflight": "node scripts/test-area-preflight.js", "test:preflight": "node scripts/test-area-preflight.js",
"test:build-stages": "node scripts/test-build-stages.js",
"test:budgets": "node scripts/test-asset-budgets.js", "test:budgets": "node scripts/test-asset-budgets.js",
"test:preview-assets": "node scripts/test-preview-assets.js", "test:preview-assets": "node scripts/test-preview-assets.js",
"test:compress-glb": "node scripts/test-compress-glb.js", "test:compress-glb": "node scripts/test-compress-glb.js",
@@ -20,6 +21,7 @@
"render:turn-lane-arrow-samples": "node scripts/render-turn-lane-arrow-samples.js" "render:turn-lane-arrow-samples": "node scripts/render-turn-lane-arrow-samples.js"
}, },
"dependencies": { "dependencies": {
"@inquirer/prompts": "^8.5.2",
"osm2streets-js-node": "0.1.4" "osm2streets-js-node": "0.1.4"
} }
} }

View File

@@ -4,6 +4,7 @@ const fs = require("fs");
const path = require("path"); const path = require("path");
const { spawnSync } = require("child_process"); const { spawnSync } = require("child_process");
const { readAreaConfig } = require("./lib/area-config"); const { readAreaConfig } = require("./lib/area-config");
const { resolveStages } = require("./lib/build-stages");
const { blenderExecutable: resolveBlenderExecutable } = require("./lib/tool-paths"); const { blenderExecutable: resolveBlenderExecutable } = require("./lib/tool-paths");
const { const {
SCENE_LAYERS, SCENE_LAYERS,
@@ -41,15 +42,6 @@ const requestedStages = args.stages
: null; : null;
const stages = resolveStages(area.stages, requestedStages); const stages = resolveStages(area.stages, requestedStages);
// intermediates deletes and rebuilds the GeoPackage from OSM, which is exactly
// the manual work reimport exists to recover. Refuse the combination instead of
// silently letting one undo the other.
if (stages.intermediates && stages.reimport) {
throw new Error(
"Stages 'intermediates' and 'reimport' are mutually exclusive: " +
"intermediates rebuilds the GeoPackage from OSM and would discard the QGIS edits reimport reads back.",
);
}
console.log(`Area: ${area.id}`); console.log(`Area: ${area.id}`);
console.log(`Config: ${configPath}`); console.log(`Config: ${configPath}`);
@@ -100,49 +92,6 @@ function splitList(value) {
.filter(Boolean); .filter(Boolean);
} }
function resolveStages(defaults, requested) {
if (!requested) return defaults;
// 'reimport' is deliberately absent from 'all': it is a recovery step for
// hand-edited GeoPackages, never part of a full build.
// 'compress' is also absent from 'all': it creates an alternate Cesium GLB,
// not the baseline asset.
const aliases = {
all: ["intermediates", "blender", "cesium"],
qgis: ["intermediates"],
osm2streets: ["intermediates"],
geojson: ["intermediates"],
intermediate: ["intermediates"],
intermediates: ["intermediates"],
reimport: ["reimport"],
gpkg: ["reimport"],
blender: ["blender"],
scene: ["blender"],
cesium: ["cesium"],
glb: ["cesium"],
preview: ["preview"],
html: ["preview"],
cesiumPreview: ["preview"],
compress: ["compress"],
compression: ["compress"],
compressedCesium: ["compress"],
};
const out = {
intermediates: false,
reimport: false,
blender: false,
cesium: false,
preview: false,
compress: false,
};
for (const stage of requested) {
const mapped = aliases[stage];
if (!mapped) {
throw new Error(`Unknown stage '${stage}'. Use intermediates, reimport, blender, cesium, preview, compress, or all.`);
}
for (const key of mapped) out[key] = true;
}
return out;
}
function writeDerivedConfig(area) { function writeDerivedConfig(area) {
fs.mkdirSync(area.outputs.pipelineDir, { recursive: true }); fs.mkdirSync(area.outputs.pipelineDir, { recursive: true });

View File

@@ -0,0 +1,49 @@
#!/usr/bin/env node
"use strict";
const fs = require("fs");
const path = require("path");
const { spawnSync } = require("child_process");
const { STAGES, resolveStages, canonicalStages } = require("./lib/build-stages");
const repoRoot = path.resolve(__dirname, "..");
function areaConfigs() {
return fs.readdirSync(path.join(repoRoot, "config", "areas"))
.filter((file) => file.endsWith(".json"))
.sort()
.map((file) => path.join(repoRoot, "config", "areas", file));
}
function requireTty(input = process.stdin, output = process.stdout) {
if (!input.isTTY || !output.isTTY) throw new Error("Interactive build requires a TTY. Use npm run build:area -- --config <area> --stages <stages> instead.");
}
async function main() {
requireTty();
const configs = areaConfigs();
if (!configs.length) throw new Error("No area configs found in config/areas.");
const { select, checkbox } = await import("@inquirer/prompts");
const config = await select({
message: "Choose an area",
choices: configs.map((file) => ({ name: path.basename(file, ".json"), value: file })),
});
const chosen = await checkbox({
message: "Choose build stages",
choices: STAGES.map((stage) => ({ name: `${stage.id} - ${stage.description}`, value: stage.id })),
required: true,
});
if (!chosen.length) throw new Error("Choose at least one build stage.");
const stages = canonicalStages(resolveStages({}, chosen));
console.log(`Area: ${path.basename(config)}`);
console.log(`Stages: ${stages.join(", ")}`);
const result = spawnSync(process.execPath, [path.join(__dirname, "build-area.js"), "--config", config, "--stages", stages.join(",")], { stdio: "inherit" });
if (result.error) throw result.error;
process.exitCode = result.status || 0;
}
if (require.main === module) main().catch((error) => {
if (error.name !== "ExitPromptError") console.error(`Error: ${error.message}`);
process.exitCode = error.name === "ExitPromptError" ? 0 : 1;
});
module.exports = { areaConfigs, requireTty };

View File

@@ -0,0 +1,41 @@
"use strict";
const STAGES = [
{ id: "intermediates", label: "OSM / QGIS intermediates", description: "Rebuild GeoJSON and GeoPackage from OSM" },
{ id: "reimport", label: "Reimport QGIS edits", description: "Copy GeoPackage layers back to GeoJSON" },
{ id: "blender", label: "Blender scene", description: "Generate the editable scene and render" },
{ id: "cesium", label: "Cesium export", description: "Export GLB, metadata, and preview" },
{ id: "preview", label: "Preview refresh", description: "Regenerate preview HTML and vehicles" },
{ id: "compress", label: "Compressed preview", description: "Create optional compressed GLB artifacts" },
];
const ALIASES = {
all: ["intermediates", "blender", "cesium"],
qgis: ["intermediates"], osm2streets: ["intermediates"], geojson: ["intermediates"], intermediate: ["intermediates"],
intermediates: ["intermediates"], reimport: ["reimport"], gpkg: ["reimport"], blender: ["blender"], scene: ["blender"],
cesium: ["cesium"], glb: ["cesium"], preview: ["preview"], html: ["preview"], cesiumPreview: ["preview"],
compress: ["compress"], compression: ["compress"], compressedCesium: ["compress"],
};
function resolveStages(defaults, requested) {
const selected = requested || Object.keys(defaults).filter((key) => defaults[key]);
const result = Object.fromEntries(STAGES.map(({ id }) => [id, false]));
for (const stage of selected) {
const mapped = ALIASES[stage];
if (!mapped) throw new Error(`Unknown stage '${stage}'. Use ${stageNames().join(", ")}, or all.`);
for (const id of mapped) result[id] = true;
}
assertCompatible(result);
return result;
}
function assertCompatible(stages) {
if (stages.intermediates && stages.reimport) {
throw new Error("Stages 'intermediates' and 'reimport' are mutually exclusive: intermediates rebuilds the GeoPackage from OSM and would discard the QGIS edits reimport reads back.");
}
}
function stageNames() { return STAGES.map(({ id }) => id); }
function canonicalStages(stages) { return stageNames().filter((id) => stages[id]); }
module.exports = { STAGES, resolveStages, canonicalStages, stageNames };

View File

@@ -0,0 +1,8 @@
#!/usr/bin/env node
"use strict";
const assert = require("assert");
const { resolveStages, canonicalStages } = require("./lib/build-stages");
assert.deepEqual(canonicalStages(resolveStages({}, ["compress", "blender", "preview"])), ["blender", "preview", "compress"]);
assert.deepEqual(canonicalStages(resolveStages({}, ["all"])), ["intermediates", "blender", "cesium"]);
assert.throws(() => resolveStages({}, ["intermediates", "reimport"]), /mutually exclusive/);
console.log("Build stage tests passed.");