feat(cli): add interactive area build menu
This commit is contained in:
@@ -4,6 +4,7 @@ const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { spawnSync } = require("child_process");
|
||||
const { readAreaConfig } = require("./lib/area-config");
|
||||
const { resolveStages } = require("./lib/build-stages");
|
||||
const { blenderExecutable: resolveBlenderExecutable } = require("./lib/tool-paths");
|
||||
const {
|
||||
SCENE_LAYERS,
|
||||
@@ -41,15 +42,6 @@ const requestedStages = args.stages
|
||||
: null;
|
||||
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(`Config: ${configPath}`);
|
||||
@@ -100,49 +92,6 @@ function splitList(value) {
|
||||
.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) {
|
||||
fs.mkdirSync(area.outputs.pipelineDir, { recursive: true });
|
||||
|
||||
49
scripts/interactive-build.js
Normal file
49
scripts/interactive-build.js
Normal 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 };
|
||||
41
scripts/lib/build-stages.js
Normal file
41
scripts/lib/build-stages.js
Normal 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 };
|
||||
8
scripts/test-build-stages.js
Normal file
8
scripts/test-build-stages.js
Normal 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.");
|
||||
Reference in New Issue
Block a user