Skip to content

Commit ccd6b7b

Browse files
committed
More refactoring.
1 parent ac0d9a1 commit ccd6b7b

3 files changed

Lines changed: 159 additions & 277 deletions

File tree

tools/tsp-client/.prettierignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules
2+
dist

tools/tsp-client/src/index.ts

Lines changed: 157 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,36 @@ import { hideBin } from "yargs/helpers";
33
import { checkDebugLogging, Logger, printBanner, usageText } from "./log.js";
44
import { joinPaths, normalizePath, resolvePath } from "@typespec/compiler";
55
import { addSpecFiles, checkoutCommit, cloneRepo, getRepoRoot, sparseCheckout } from "./git.js";
6-
import { createTempDirectory, getEmitterFromRepoConfig, readTspLocation, removeDirectory } from "./fs.js";
6+
import {
7+
createTempDirectory,
8+
getEmitterFromRepoConfig,
9+
readTspLocation,
10+
removeDirectory,
11+
} from "./fs.js";
712
import { cp, mkdir, readFile, rename, stat, unlink, writeFile } from "fs/promises";
813
import { npmCommand, npxCommand } from "./npm.js";
914
import { compileTsp, discoverMainFile, resolveTspConfigUrl, TspLocation } from "./typespec.js";
10-
import { formatAdditionalDirectories, getAdditionalDirectoryName, getServiceDir, makeSparseSpecDir } from "./utils.js";
15+
import {
16+
formatAdditionalDirectories,
17+
getAdditionalDirectoryName,
18+
getServiceDir,
19+
makeSparseSpecDir,
20+
} from "./utils.js";
1121
import { parse as parseYaml } from "yaml";
1222
import { config as dotenvConfig } from "dotenv";
1323
import { resolve } from "node:path";
1424
import { doesFileExist } from "./network.js";
25+
import PromptSync from "prompt-sync";
1526

1627
function commandPreamble(argv: any) {
17-
checkDebugLogging(argv);
28+
checkDebugLogging(argv);
1829
printBanner();
1930
yargs().showVersion();
2031
}
2132

2233
async function initCommand(argv: any) {
34+
let outputDir = resolveOutputDir(argv);
2335
let tspConfig = argv["tsp-config"];
24-
let outputDir = argv["output-dir"] ?? ".";
2536
const skipSyncAndGenerate = argv["skip-sync-and-generate"];
2637
const localSpecRepo = argv["local-spec-repo"];
2738
const saveInputs = argv["save-inputs"];
@@ -41,7 +52,18 @@ async function initCommand(argv: any) {
4152
throw new Error("Couldn't find emitter-package.json in the repo");
4253
}
4354

44-
if (isUrl) {
55+
let isFolder = true;
56+
if (await doesFileExist(tspConfig)) {
57+
isFolder = false;
58+
}
59+
if (!isFolder) {
60+
if (!commit || !repo) {
61+
Logger.error("--commit and --repo are required when --tsp-config is a local directory");
62+
process.exit(1);
63+
}
64+
}
65+
66+
if (isFolder) {
4567
// URL scenario
4668
const resolvedConfigUrl = resolveTspConfigUrl(tspConfig);
4769
const cloneDir = await makeSparseSpecDir(repoRoot);
@@ -124,7 +146,7 @@ async function initCommand(argv: any) {
124146
}
125147

126148
Logger.info(`SDK initialized in ${outputDir}`);
127-
149+
128150
if (!skipSyncAndGenerate) {
129151
// FIXME: Call into the syncCommand and the generateCommand
130152
await syncCommand(outputDir, localSpecRepo);
@@ -137,11 +159,11 @@ async function initCommand(argv: any) {
137159
}
138160

139161
async function syncCommand(argv: any) {
140-
let outputDir = argv["output-dir"] ?? ".";
162+
let outputDir = resolveOutputDir(argv);
141163
const localSpecRepo = argv["local-spec-repo"];
142164

143165
commandPreamble(argv);
144-
166+
145167
const tempRoot = await createTempDirectory(outputDir);
146168
const repoRoot = await getRepoRoot(outputDir);
147169
Logger.debug(`Repo root is ${repoRoot}`);
@@ -227,8 +249,7 @@ async function syncCommand(argv: any) {
227249
}
228250

229251
async function generateCommand(argv: any) {
230-
231-
let outputDir = argv["output-dir"] ?? ".";
252+
let outputDir = resolveOutputDir(argv);
232253
const emitterOptions = argv["emitter-options"];
233254
const saveInputs = argv["save-inputs"];
234255

@@ -286,8 +307,7 @@ async function generateCommand(argv: any) {
286307
}
287308

288309
async function updateCommand(argv: any) {
289-
290-
const outputDir = argv["output-dir"] ?? ".";
310+
const outputDir = resolveOutputDir(argv);
291311
const repo = argv["repo"];
292312
const commit = argv["commit"];
293313
let tspConfig = argv["tsp-config"];
@@ -331,10 +351,9 @@ async function updateCommand(argv: any) {
331351
}
332352

333353
async function convertCommand(argv: any): Promise<void> {
334-
354+
const outputDir = resolveOutputDir(argv);
335355
const swaggerReadme = argv["swagger-readme"];
336356
const arm = argv["arm"];
337-
const outputDir = argv["output-dir"] ?? ".";
338357
let rootUrl = resolvePath(outputDir);
339358

340359
commandPreamble(argv);
@@ -394,7 +413,7 @@ async function convertCommand(argv: any): Promise<void> {
394413
}
395414

396415
async function generateLockFileCommand(argv: any) {
397-
const outputDir = argv["output-dir"] ?? ".";
416+
const outputDir = resolveOutputDir(argv);
398417
let rootUrl = resolvePath(outputDir);
399418
const repoRoot = await getRepoRoot(rootUrl);
400419

@@ -419,6 +438,33 @@ async function generateLockFileCommand(argv: any) {
419438
Logger.info(`Lock file generated in ${joinPaths(repoRoot, "eng", "emitter-package-lock.json")}`);
420439
}
421440

441+
/** Ensure the output directory exists and allow interactive users to confirm or override the value. */
442+
function resolveOutputDir(argv: any): string {
443+
let outputDir = resolvePath(process.cwd(), argv["output-dir"]);
444+
const noPrompt = argv["no-prompt"];
445+
446+
let useOutputDir;
447+
if (process.stdin.isTTY && !noPrompt) {
448+
// Ask user is this is the correct output directory
449+
const prompt = PromptSync();
450+
useOutputDir = prompt(`Use output directory '${outputDir}'? (y/n)`, "y");
451+
} else {
452+
// There is no user to ask, so assume yes
453+
useOutputDir = "y";
454+
}
455+
456+
if (useOutputDir.toLowerCase() === "n") {
457+
const newOutputDir = prompt("Enter output directory: ");
458+
if (!newOutputDir) {
459+
Logger.error("Output directory is required");
460+
process.exit(1);
461+
}
462+
outputDir = resolvePath(normalizePath(newOutputDir));
463+
}
464+
Logger.info(`Using output directory '${outputDir}'`);
465+
return outputDir;
466+
}
467+
422468
const parser = yargs(hideBin(process.argv))
423469
.scriptName("")
424470
.usage(usageText)
@@ -430,7 +476,14 @@ const parser = yargs(hideBin(process.argv))
430476
.option("output-dir", {
431477
alias: "o",
432478
type: "string",
433-
description: "Specify an alternate output directory for the generated files. Default is your current directory",
479+
description:
480+
"Specify an alternate output directory for the generated files. Default is your current directory.",
481+
default: ".",
482+
})
483+
.option("no-prompt", {
484+
alias: "y",
485+
type: "boolean",
486+
description: "Skip any interactive prompts.",
434487
})
435488
.command(
436489
"init",
@@ -466,11 +519,11 @@ const parser = yargs(hideBin(process.argv))
466519
.option("repo", {
467520
type: "string",
468521
description: "Repository where the project is defined",
469-
})
522+
});
470523
},
471524
async (argv) => {
472525
await initCommand(argv);
473-
}
526+
},
474527
)
475528
.command(
476529
"sync",
@@ -480,90 +533,97 @@ const parser = yargs(hideBin(process.argv))
480533
type: "string",
481534
description: "Path to local spec repo",
482535
});
483-
}, async (argv) => {
484-
await syncCommand(argv);
485-
})
486-
.command("generate", "Generate from a TypeSpec project", (yargs) => {
487-
return yargs
488-
.options("emitter-options", {
489-
type: "string",
490-
description: "The options to pass to the emitter",
491-
})
492-
.options("save-inputs", {
493-
type: "boolean",
494-
description: "Don't clean up the temp directory after generation",
495-
});
496-
}, async (argv) => {
497-
await generateCommand(argv);
498-
})
499-
.command("update", "Sync and generate from a TypeSpec project", (yargs) => {
500-
return yargs
501-
.option("repo", {
502-
type: "string",
503-
description: "Repository where the project is defined",
504-
})
505-
.option("commit", {
506-
type: "string",
507-
description: "Commit hash to be used",
508-
})
509-
.option("tsp-config", {
510-
type: "string",
511-
description: "Path to tspconfig.yaml",
512-
})
513-
.option("local-spec-repo", {
514-
type: "string",
515-
description: "Path to local spec repo",
516-
})
517-
.option("emitter-options", {
518-
type: "string",
519-
description: "The options to pass to the emitter",
520-
})
521-
.option("save-inputs", {
522-
type: "boolean",
523-
description: "Don't clean up the temp directory after generation",
524-
})
525-
}, async (argv) => {
526-
await updateCommand(argv);
527-
})
528-
.command("convert", "Convert a swagger specification to TypeSpec", (yargs) => {
529-
return yargs.option("swagger-readme", {
530-
type: "string",
531-
description: "Path to the swagger readme file",
532-
demandOption: true,
533-
})
534-
.option("arm", {
535-
type: "boolean",
536-
description: "Convert swagger to ARM TypeSpec",
537-
});
538-
}, async (argv) => {
539-
await convertCommand(argv);
540-
})
541-
.command("generate-lock-file", "Generate a lock file under the eng/ directory from an existing emitter-package.json", {}, async (argv) => {
542-
await generateLockFileCommand(argv);
543-
})
536+
},
537+
async (argv) => {
538+
await syncCommand(argv);
539+
},
540+
)
541+
.command(
542+
"generate",
543+
"Generate from a TypeSpec project",
544+
(yargs) => {
545+
return yargs
546+
.options("emitter-options", {
547+
type: "string",
548+
description: "The options to pass to the emitter",
549+
})
550+
.options("save-inputs", {
551+
type: "boolean",
552+
description: "Don't clean up the temp directory after generation",
553+
});
554+
},
555+
async (argv) => {
556+
await generateCommand(argv);
557+
},
558+
)
559+
.command(
560+
"update",
561+
"Sync and generate from a TypeSpec project",
562+
(yargs) => {
563+
return yargs
564+
.option("repo", {
565+
type: "string",
566+
description: "Repository where the project is defined",
567+
})
568+
.option("commit", {
569+
type: "string",
570+
description: "Commit hash to be used",
571+
})
572+
.option("tsp-config", {
573+
type: "string",
574+
description: "Path to tspconfig.yaml",
575+
})
576+
.option("local-spec-repo", {
577+
type: "string",
578+
description: "Path to local spec repo",
579+
})
580+
.option("emitter-options", {
581+
type: "string",
582+
description: "The options to pass to the emitter",
583+
})
584+
.option("save-inputs", {
585+
type: "boolean",
586+
description: "Don't clean up the temp directory after generation",
587+
});
588+
},
589+
async (argv) => {
590+
await updateCommand(argv);
591+
},
592+
)
593+
.command(
594+
"convert",
595+
"Convert a swagger specification to TypeSpec",
596+
(yargs) => {
597+
return yargs
598+
.option("swagger-readme", {
599+
type: "string",
600+
description: "Path to the swagger readme file",
601+
demandOption: true,
602+
})
603+
.option("arm", {
604+
type: "boolean",
605+
description: "Convert swagger to ARM TypeSpec",
606+
});
607+
},
608+
async (argv) => {
609+
await convertCommand(argv);
610+
},
611+
)
612+
.command(
613+
"generate-lock-file",
614+
"Generate a lock file under the eng/ directory from an existing emitter-package.json",
615+
{},
616+
async (argv) => {
617+
await generateLockFileCommand(argv);
618+
},
619+
)
544620
.demandCommand(1, "Please provide a command.")
545621
.help()
622+
.showHelpOnFail(true);
546623

547624
try {
548625
await parser.parse();
549-
} catch(err: any) {
626+
} catch (err: any) {
550627
Logger.error(err);
551628
process.exit(1);
552629
}
553-
554-
555-
// Options:
556-
// --arm Convert ARM swagger specification to TypeSpec [boolean]
557-
// -c, --tsp-config The tspconfig.yaml file to use [string]
558-
// --commit Commit to be used for project init or update [string]
559-
// --emitter-options The options to pass to the emitter [string]
560-
// --generate-lock-file Generate a lock file under the eng/ directory from
561-
// an existing emitter-package.json [boolean]
562-
// --no-prompt Skip prompting for output directory confirmation [boolean]
563-
// --save-inputs Don't clean up the temp directory after generation [boolean]
564-
// --skip-sync-and-generate Skip sync and generate during project init [boolean]
565-
// --swagger-readme Path or url to swagger readme file [string]
566-
// -o, --output-dir Specify an alternate output directory for the
567-
// generated files. Default is your current directory [string]
568-
// --repo Repository where the project is defined for init
569-
// or update [string]

0 commit comments

Comments
 (0)