Skip to content

Commit 6f05e4f

Browse files
committed
Fix CLI export errors and locale selection
1 parent b890a5b commit 6f05e4f

7 files changed

Lines changed: 310 additions & 31 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"release:create-apiops:publish": "npm publish --workspace packages/create-apiops --access public",
3434
"check:packaging:skills": "node scripts/check-packaging-skills.mjs",
3535
"check:package:contents": "node scripts/check-package-contents.mjs",
36-
"test:create-apiops": "node scripts/test-create-apiops-scaffold.mjs"
36+
"test:create-apiops": "node scripts/test-create-apiops-scaffold.mjs && node scripts/test-method-cli.mjs"
3737
},
3838
"devDependencies": {
3939
"@changesets/cli": "^2.29.7",

packages/create-apiops/bin/create-apiops-project.js

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,32 @@ function replaceInFile(filePath, replacements) {
9797
fs.writeFileSync(filePath, content);
9898
}
9999

100+
function resolveNpmCommand() {
101+
const npmCliPath = path.resolve(path.dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js");
102+
if (fs.existsSync(npmCliPath)) {
103+
return {
104+
command: process.execPath,
105+
args: [npmCliPath]
106+
};
107+
}
108+
109+
return {
110+
command: process.platform === "win32" ? "npm.cmd" : "npm",
111+
args: []
112+
};
113+
}
114+
115+
function runCommand(command, args, options = {}) {
116+
return spawnSync(command, args, {
117+
shell: false,
118+
...options
119+
});
120+
}
121+
100122
function getScripts(locale, apiStyle) {
101123
const base = {
102124
"method": "node ./node_modules/apiops-cycles-method-data/packages/create-apiops/bin/method-cli.js",
103-
"method:start": `node ./node_modules/apiops-cycles-method-data/packages/create-apiops/bin/method-cli.js start --locale ${locale}`,
125+
"method:start": `node ./node_modules/apiops-cycles-method-data/packages/create-apiops/bin/method-cli.js start --default-locale ${locale}`,
104126
"method:resources:strategy": `node ./node_modules/apiops-cycles-method-data/packages/create-apiops/bin/method-cli.js resources --station api-product-strategy --locale ${locale}`,
105127
"method:canvases:new-api": `node ./node_modules/apiops-cycles-method-data/packages/create-apiops/bin/method-cli.js generate-canvases --preset new-api --style "${apiStyle}" --locale ${locale} --output ./specs/canvases`,
106128
"method:stations": `node ./node_modules/apiops-cycles-method-data/skills/new-api-guide/scripts/get-core-stations.cjs ${locale}`,
@@ -200,18 +222,18 @@ async function main() {
200222
console.log(`Created ${projectName}`);
201223

202224
if (installNow) {
203-
const result = spawnSync("npm", ["install"], {
225+
const npmCommand = resolveNpmCommand();
226+
const result = runCommand(npmCommand.command, [...npmCommand.args, "install"], {
204227
cwd: targetDir,
205-
stdio: "inherit",
206-
shell: true
228+
stdio: "inherit"
207229
});
208230
if (result.status !== 0) {
209231
console.error("npm install failed");
210232
process.exit(result.status || 1);
211233
}
212234

213-
const canvasInit = spawnSync(
214-
"node",
235+
const canvasInit = runCommand(
236+
process.execPath,
215237
[
216238
"./node_modules/apiops-cycles-method-data/packages/create-apiops/bin/method-cli.js",
217239
"generate-canvases",
@@ -222,8 +244,7 @@ async function main() {
222244
],
223245
{
224246
cwd: targetDir,
225-
stdio: "inherit",
226-
shell: true
247+
stdio: "inherit"
227248
}
228249
);
229250

packages/create-apiops/bin/method-cli.js

Lines changed: 125 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@ const STATION_SCRIPT_HINTS = {
1818

1919
function printUsage() {
2020
console.log(`Usage:
21-
node packages/create-apiops/bin/method-cli.js start [--locale <locale>] [--json] [--list] [--answers <yes,no,...>] [--next-action <resources|canvases|exit>]
21+
node packages/create-apiops/bin/method-cli.js start [--locale <locale>] [--default-locale <locale>] [--json] [--list] [--answers <yes,no,...>] [--next-action <resources|canvases|exit>]
2222
node packages/create-apiops/bin/method-cli.js resources --station <station-id> [--locale <locale>] [--style <style>] [--json] [--list] [--step-actions <details,next,...>]
2323
node packages/create-apiops/bin/method-cli.js generate-canvases [--station <station-id> | --stations <ids> | --preset new-api] [--locale <locale>] [--style <style>] [--output <dir>] [--force] [--json]
2424
2525
Examples:
2626
node packages/create-apiops/bin/method-cli.js start --locale en
27+
node packages/create-apiops/bin/method-cli.js start --default-locale en
2728
node packages/create-apiops/bin/method-cli.js start --locale en --answers yes,no,yes --next-action resources
2829
node packages/create-apiops/bin/method-cli.js resources --station api-product-strategy --locale en
2930
node packages/create-apiops/bin/method-cli.js generate-canvases --preset new-api --style REST --output ./specs/canvases
@@ -71,6 +72,14 @@ function readJson(filePath) {
7172
return JSON.parse(fs.readFileSync(filePath, "utf8"));
7273
}
7374

75+
function runCommand(command, args, options = {}) {
76+
return spawnSync(command, args, {
77+
encoding: "utf8",
78+
shell: false,
79+
...options
80+
});
81+
}
82+
7483
function getNextStepHints(stationId) {
7584
const hints = STATION_SCRIPT_HINTS[stationId];
7685
if (hints) {
@@ -155,44 +164,137 @@ async function fillCanvasSectionsInteractive(stationId, step, locale, output, rl
155164
}
156165

157166
function resolveCanvasExportCommand() {
158-
const binPath = path.resolve(
159-
process.cwd(),
160-
"node_modules",
161-
".bin",
162-
process.platform === "win32" ? "canvascreator-export.cmd" : "canvascreator-export"
163-
);
164-
if (fs.existsSync(binPath)) {
167+
const packageRoot = path.resolve(process.cwd(), "node_modules", "canvascreator");
168+
if (!fs.existsSync(packageRoot)) {
165169
return {
166-
command: binPath,
167-
args: []
170+
ok: false,
171+
reason: "SVG export requires CanvasCreator export support, which is not installed in this project.",
172+
help: "Run `npm install` to install project dependencies, or `npm install canvascreator` to add CanvasCreator manually."
168173
};
169174
}
170175

171-
const scriptPath = path.resolve(process.cwd(), "node_modules", "canvascreator", "scripts", "export.js");
176+
const packageJsonPath = path.join(packageRoot, "package.json");
177+
if (fs.existsSync(packageJsonPath)) {
178+
const packageJson = readJson(packageJsonPath);
179+
const binEntry = typeof packageJson.bin === "string"
180+
? packageJson.bin
181+
: packageJson.bin?.["canvascreator-export"];
182+
183+
if (binEntry) {
184+
const binPath = path.resolve(packageRoot, binEntry);
185+
if (fs.existsSync(binPath)) {
186+
return {
187+
ok: true,
188+
command: process.execPath,
189+
args: [binPath]
190+
};
191+
}
192+
}
193+
}
194+
195+
const scriptPath = path.resolve(packageRoot, "scripts", "export.js");
172196
if (fs.existsSync(scriptPath)) {
173197
return {
198+
ok: true,
174199
command: process.execPath,
175200
args: [scriptPath]
176201
};
177202
}
178203

179-
return null;
204+
return {
205+
ok: false,
206+
reason: "CanvasCreator is installed, but its export CLI could not be found.",
207+
help: "Reinstall `canvascreator` or use the CanvasCreator web app to export SVG from the JSON file."
208+
};
209+
}
210+
211+
function resolveStartLocale(options = {}) {
212+
const explicitLocale = options.locale;
213+
const defaultLocale = options["default-locale"] || methodEngine.DEFAULT_LOCALE;
214+
if (explicitLocale) {
215+
return explicitLocale;
216+
}
217+
218+
return methodEngine.getSupportedMethodLocales().includes(defaultLocale)
219+
? defaultLocale
220+
: methodEngine.DEFAULT_LOCALE;
221+
}
222+
223+
async function maybePromptForStartLocale(options = {}) {
224+
const selectedLocale = resolveStartLocale(options);
225+
if (
226+
options.locale ||
227+
options.json ||
228+
options.list ||
229+
options.answers ||
230+
!process.stdin.isTTY
231+
) {
232+
return selectedLocale;
233+
}
234+
235+
const supportedLocales = methodEngine.getSupportedMethodLocales();
236+
const rl = readline.createInterface({
237+
input: process.stdin,
238+
output: process.stdout
239+
});
240+
241+
try {
242+
console.log(`Default method language: ${selectedLocale}`);
243+
console.log(`Available languages: ${supportedLocales.join(", ")}`);
244+
const answer = (await rl.question("Press Enter to keep the default, or type another locale for this run: "))
245+
.trim()
246+
.toLowerCase();
247+
if (!answer) {
248+
console.log("");
249+
return selectedLocale;
250+
}
251+
252+
if (supportedLocales.includes(answer)) {
253+
console.log("");
254+
return answer;
255+
}
256+
257+
console.log(`Unknown locale "${answer}", using ${selectedLocale}.`);
258+
console.log("");
259+
return selectedLocale;
260+
} finally {
261+
rl.close();
262+
}
263+
}
264+
265+
function formatCommandFailure(result) {
266+
if (result.error?.message) {
267+
return result.error.message;
268+
}
269+
270+
const stderr = String(result.stderr || "").trim();
271+
if (stderr) {
272+
return stderr;
273+
}
274+
275+
const stdout = String(result.stdout || "").trim();
276+
if (stdout) {
277+
return stdout;
278+
}
279+
280+
return "Canvas export failed.";
180281
}
181282

182283
function exportCanvasSvgForResource(stationId, step, locale, output) {
183284
const jsonPath = methodEngine.generateCanvasForStationResource(stationId, step.resourceId, locale, output);
184285
const exportCommand = resolveCanvasExportCommand();
185-
if (!exportCommand) {
286+
if (!exportCommand.ok) {
186287
return {
187288
ok: false,
188-
reason: "CanvasCreator export CLI is not installed in this project yet.",
289+
reason: exportCommand.reason,
290+
help: exportCommand.help,
189291
jsonPath
190292
};
191293
}
192294

193295
const outputDir = path.dirname(jsonPath);
194296
const outputFile = path.join(outputDir, `${step.resourceId}.svg`);
195-
const result = spawnSync(
297+
const result = runCommand(
196298
exportCommand.command,
197299
[
198300
...exportCommand.args,
@@ -201,16 +303,14 @@ function exportCanvasSvgForResource(stationId, step, locale, output) {
201303
"--output", outputFile
202304
],
203305
{
204-
cwd: process.cwd(),
205-
encoding: "utf8",
206-
shell: process.platform === "win32"
306+
cwd: process.cwd()
207307
}
208308
);
209309

210310
if (result.status !== 0) {
211311
return {
212312
ok: false,
213-
reason: result.stderr || result.stdout || "Canvas export failed.",
313+
reason: formatCommandFailure(result),
214314
jsonPath
215315
};
216316
}
@@ -562,6 +662,9 @@ async function runInteractiveResources(options) {
562662
console.log(`Source JSON: ${exportResult.jsonPath}`);
563663
} else {
564664
console.log(`SVG export unavailable: ${exportResult.reason}`);
665+
if (exportResult.help) {
666+
console.log(exportResult.help);
667+
}
565668
console.log(`Canvas JSON is still available at: ${exportResult.jsonPath}`);
566669
console.log(`CanvasCreator URL: ${methodEngine.getCanvasCreatorUrl(step.canvasId, locale)}`);
567670
}
@@ -627,7 +730,8 @@ async function main() {
627730
}
628731

629732
if (command === "start") {
630-
const data = methodEngine.buildStartData(options.locale || methodEngine.DEFAULT_LOCALE);
733+
const locale = await maybePromptForStartLocale(options);
734+
const data = methodEngine.buildStartData(locale);
631735
if (options.json) {
632736
console.log(JSON.stringify(data, null, 2));
633737
return;
@@ -638,7 +742,7 @@ async function main() {
638742
return;
639743
}
640744

641-
await runInteractiveStart(data, options);
745+
await runInteractiveStart(data, { ...options, locale });
642746
return;
643747
}
644748

packages/create-apiops/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "create-apiops",
3-
"version": "0.1.0",
3+
"version": "1.0.0",
44
"description": "Scaffold a new APIOps project with an OpenAPI spec, audit scaffolding, and APIOps documentation templates.",
55
"license": "Apache-2.0",
66
"type": "module",

scripts/test-create-apiops-scaffold.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ try {
5959
assert(pkg.name === projectName, "Scaffolded package name was not set correctly.");
6060
assert(pkg.scripts?.method, "Expected generic method script in scaffolded package.");
6161
assert(pkg.scripts?.["method:start"], "Expected method:start script in scaffolded package.");
62+
assert(
63+
pkg.scripts["method:start"].includes("--default-locale en"),
64+
"Expected method:start to pass the project default locale without forcing the runtime locale."
65+
);
6266
assert(pkg.scripts?.["method:resources:strategy"], "Expected method:resources:strategy script in scaffolded package.");
6367
assert(pkg.scripts?.["method:canvases:new-api"], "Expected method:canvases:new-api script in scaffolded package.");
6468
assert(pkg.scripts?.["method:stations"], "Expected method:stations script in scaffolded package.");

0 commit comments

Comments
 (0)