clispark 1.14.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +654 -45
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/templates/dotnet/ARCHITECTURE.md +77 -0
- package/templates/dotnet/Cli.slnx +8 -0
- package/templates/dotnet/README.md +45 -0
- package/templates/dotnet/gitignore +4 -0
- package/templates/dotnet/src/Cli.csproj +24 -0
- package/templates/dotnet/src/CliUserException.cs +4 -0
- package/templates/dotnet/src/CommandDiscovery.cs +48 -0
- package/templates/dotnet/src/CommandPathAttribute.cs +12 -0
- package/templates/dotnet/src/Commands/HelloCommand.cs +28 -0
- package/templates/dotnet/src/Commands/TaskCommand.cs +28 -0
- package/templates/dotnet/src/Commands/TaskCompleteCommand.cs +30 -0
- package/templates/dotnet/src/Commands/TaskListCommand.cs +34 -0
- package/templates/dotnet/src/ICliCommand.cs +8 -0
- package/templates/dotnet/src/Logging/CliLoggerFactory.cs +70 -0
- package/templates/dotnet/src/Logging/SensitivePropertyEnricher.cs +19 -0
- package/templates/dotnet/src/Program.cs +35 -0
- package/templates/dotnet/tests/Cli.Tests.csproj +25 -0
- package/templates/dotnet/tests/HelloCommandTests.cs +28 -0
- package/templates/dotnet/tests/TaskCompleteCommandTests.cs +34 -0
package/dist/cli.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
4
|
+
import path14 from "path";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/wizard.ts
|
|
8
8
|
import { intro, outro, text, select, log, isCancel, cancel } from "@clack/prompts";
|
|
9
9
|
|
|
10
10
|
// src/languages/packs/node-oclif.ts
|
|
11
|
-
import
|
|
11
|
+
import path5 from "path";
|
|
12
12
|
|
|
13
13
|
// src/package-root.ts
|
|
14
14
|
import { existsSync, readFileSync } from "fs";
|
|
@@ -212,9 +212,135 @@ async function applyPrivateIntent(targetDir) {
|
|
|
212
212
|
pkg.private = true;
|
|
213
213
|
await writeFile2(packageJsonPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
214
214
|
}
|
|
215
|
+
async function applyRegistryUrl(targetDir, registryUrl) {
|
|
216
|
+
await writeFile2(path3.join(targetDir, ".npmrc"), `registry=${registryUrl}
|
|
217
|
+
`);
|
|
218
|
+
}
|
|
215
219
|
var npmRegistryChecker = {
|
|
216
220
|
checkNameAvailability,
|
|
217
|
-
applyPrivateIntent
|
|
221
|
+
applyPrivateIntent,
|
|
222
|
+
applyRegistryUrl
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// src/languages/command-generators/node-oclif.ts
|
|
226
|
+
import { mkdir, readdir, writeFile as writeFile3 } from "fs/promises";
|
|
227
|
+
import path4 from "path";
|
|
228
|
+
|
|
229
|
+
// src/languages/command-generator.ts
|
|
230
|
+
function buildCommandTree(paths2) {
|
|
231
|
+
const nodesByPath = /* @__PURE__ */ new Map();
|
|
232
|
+
const roots = [];
|
|
233
|
+
function getOrCreateNode(nodePath) {
|
|
234
|
+
let node = nodesByPath.get(nodePath);
|
|
235
|
+
if (!node) {
|
|
236
|
+
const segments = nodePath.split(" ");
|
|
237
|
+
node = { path: nodePath, displayLabel: segments.join(" > "), children: [] };
|
|
238
|
+
nodesByPath.set(nodePath, node);
|
|
239
|
+
if (segments.length === 1) {
|
|
240
|
+
roots.push(node);
|
|
241
|
+
} else {
|
|
242
|
+
const parent = getOrCreateNode(segments.slice(0, -1).join(" "));
|
|
243
|
+
parent.children.push(node);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return node;
|
|
247
|
+
}
|
|
248
|
+
for (const p of paths2) {
|
|
249
|
+
getOrCreateNode(p);
|
|
250
|
+
}
|
|
251
|
+
return roots;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/languages/command-generators/node-oclif.ts
|
|
255
|
+
async function collectCommandFiles(dir, baseDir = dir) {
|
|
256
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
257
|
+
const files = [];
|
|
258
|
+
for (const entry of entries) {
|
|
259
|
+
const fullPath = path4.join(dir, entry.name);
|
|
260
|
+
if (entry.isDirectory()) {
|
|
261
|
+
files.push(...await collectCommandFiles(fullPath, baseDir));
|
|
262
|
+
} else if (entry.name.endsWith(".ts") && !entry.name.endsWith(".test.ts")) {
|
|
263
|
+
files.push(path4.relative(baseDir, fullPath));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return files;
|
|
267
|
+
}
|
|
268
|
+
function toCommandPath(relativeFilePath) {
|
|
269
|
+
return relativeFilePath.replace(/\.ts$/, "").split(path4.sep).join(" ");
|
|
270
|
+
}
|
|
271
|
+
async function listExistingCommands(targetDir) {
|
|
272
|
+
const commandsDir = path4.join(targetDir, "src", "commands");
|
|
273
|
+
const files = await collectCommandFiles(commandsDir);
|
|
274
|
+
return buildCommandTree(files.map(toCommandPath));
|
|
275
|
+
}
|
|
276
|
+
function toClassName(pathSegments) {
|
|
277
|
+
return pathSegments.map((seg) => seg[0].toUpperCase() + seg.slice(1)).join("");
|
|
278
|
+
}
|
|
279
|
+
function argFor(param) {
|
|
280
|
+
const opts = [`required: ${param.required}`, `description: '${param.name}'`];
|
|
281
|
+
if (param.type === "enum") {
|
|
282
|
+
opts.push(`options: [${(param.allowedValues ?? []).map((v) => `'${v}'`).join(", ")}]`);
|
|
283
|
+
}
|
|
284
|
+
const argsMethod = param.type === "enum" ? "string" : param.type;
|
|
285
|
+
return `Args.${argsMethod}({ ${opts.join(", ")} })`;
|
|
286
|
+
}
|
|
287
|
+
function generateCommandFileContent(spec) {
|
|
288
|
+
const className = toClassName(spec.pathSegments);
|
|
289
|
+
const argsLines = spec.parameters.map((p) => ` ${p.name}: ${argFor(p)},`).join("\n");
|
|
290
|
+
const logExpr = spec.parameters.map((p) => `${p.name}=\${args.${p.name}}`).join(" ");
|
|
291
|
+
const depth = spec.pathSegments.length;
|
|
292
|
+
const baseCommandImportPath = "../".repeat(depth) + "base-command";
|
|
293
|
+
return `import { Args } from '@oclif/core';
|
|
294
|
+
import { BaseCommand } from '${baseCommandImportPath}';
|
|
295
|
+
|
|
296
|
+
export default class ${className} extends BaseCommand {
|
|
297
|
+
static description = '${spec.pathSegments[spec.pathSegments.length - 1]} command';
|
|
298
|
+
static args = {
|
|
299
|
+
${argsLines}
|
|
300
|
+
};
|
|
301
|
+
static flags = {};
|
|
302
|
+
|
|
303
|
+
async run(): Promise<void> {
|
|
304
|
+
const { args } = await this.parse(${className});
|
|
305
|
+
this.log(\`${logExpr}\`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
`;
|
|
309
|
+
}
|
|
310
|
+
function sampleArgValue(param) {
|
|
311
|
+
if (param.type === "enum") return param.allowedValues?.[0] ?? "";
|
|
312
|
+
if (param.type === "integer") return "1";
|
|
313
|
+
if (param.type === "boolean") return "true";
|
|
314
|
+
return "value";
|
|
315
|
+
}
|
|
316
|
+
function generateTestFileContent(spec) {
|
|
317
|
+
const commandInvocation = spec.pathSegments.join(" ");
|
|
318
|
+
const sampleArgs = spec.parameters.map(sampleArgValue).join(" ");
|
|
319
|
+
const fullInvocation = sampleArgs ? `${commandInvocation} ${sampleArgs}` : commandInvocation;
|
|
320
|
+
return `import { describe, it, expect } from 'vitest';
|
|
321
|
+
import { runCommand } from '@oclif/test';
|
|
322
|
+
|
|
323
|
+
describe('${commandInvocation}', () => {
|
|
324
|
+
it('runs successfully', async () => {
|
|
325
|
+
const { error } = await runCommand('${fullInvocation}');
|
|
326
|
+
expect(error).toBeUndefined();
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
`;
|
|
330
|
+
}
|
|
331
|
+
async function generateCommand(targetDir, spec) {
|
|
332
|
+
const relDir = path4.join("src", "commands", ...spec.pathSegments.slice(0, -1));
|
|
333
|
+
const fileName = spec.pathSegments[spec.pathSegments.length - 1];
|
|
334
|
+
const commandRelPath = path4.join(relDir, `${fileName}.ts`);
|
|
335
|
+
const testRelPath = path4.join(relDir, `${fileName}.test.ts`);
|
|
336
|
+
await mkdir(path4.join(targetDir, relDir), { recursive: true });
|
|
337
|
+
await writeFile3(path4.join(targetDir, commandRelPath), generateCommandFileContent(spec));
|
|
338
|
+
await writeFile3(path4.join(targetDir, testRelPath), generateTestFileContent(spec));
|
|
339
|
+
return { commandFile: commandRelPath, testFile: testRelPath };
|
|
340
|
+
}
|
|
341
|
+
var nodeOclifCommandGenerator = {
|
|
342
|
+
listExistingCommands,
|
|
343
|
+
generateCommand
|
|
218
344
|
};
|
|
219
345
|
|
|
220
346
|
// src/languages/packs/node-oclif.ts
|
|
@@ -228,7 +354,7 @@ function validateProjectName(value) {
|
|
|
228
354
|
var nodeOclifPack = {
|
|
229
355
|
id: "node",
|
|
230
356
|
displayName: "Node.js / TypeScript (oclif)",
|
|
231
|
-
templateDir:
|
|
357
|
+
templateDir: path5.join(findPackageRoot(), "templates", "node"),
|
|
232
358
|
scaffoldCommands: [
|
|
233
359
|
{ command: "npm", args: ["install"] },
|
|
234
360
|
{ command: "npm", args: ["run", "build"] }
|
|
@@ -239,13 +365,339 @@ var nodeOclifPack = {
|
|
|
239
365
|
defaultUrl: NPM_DEFAULT_REGISTRY_URL,
|
|
240
366
|
promptLabel: "Custom npm registry URL (leave empty for npmjs.org)",
|
|
241
367
|
checkNameAvailability: npmRegistryChecker.checkNameAvailability,
|
|
242
|
-
applyPrivateIntent: npmRegistryChecker.applyPrivateIntent
|
|
368
|
+
applyPrivateIntent: npmRegistryChecker.applyPrivateIntent,
|
|
369
|
+
applyRegistryUrl: npmRegistryChecker.applyRegistryUrl
|
|
370
|
+
},
|
|
371
|
+
commandGenerator: nodeOclifCommandGenerator
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
// src/languages/packs/dotnet.ts
|
|
375
|
+
import path9 from "path";
|
|
376
|
+
|
|
377
|
+
// src/update/adapters/dotnet.ts
|
|
378
|
+
import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
|
|
379
|
+
import path6 from "path";
|
|
380
|
+
var CORE_FILE_PATHS2 = [
|
|
381
|
+
"Cli.slnx",
|
|
382
|
+
"src/Program.cs",
|
|
383
|
+
"src/ICliCommand.cs",
|
|
384
|
+
"src/CommandPathAttribute.cs",
|
|
385
|
+
"src/CommandDiscovery.cs",
|
|
386
|
+
"src/CliUserException.cs",
|
|
387
|
+
"src/Logging/CliLoggerFactory.cs",
|
|
388
|
+
"src/Logging/SensitivePropertyEnricher.cs",
|
|
389
|
+
"tests/Cli.Tests.csproj",
|
|
390
|
+
"ARCHITECTURE.md",
|
|
391
|
+
".gitignore"
|
|
392
|
+
];
|
|
393
|
+
function escapeRegExp(value) {
|
|
394
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
395
|
+
}
|
|
396
|
+
function extractTag(content, tag) {
|
|
397
|
+
const match = content.match(new RegExp(`<${tag}>([^<]*)</${tag}>`));
|
|
398
|
+
if (!match) throw new Error(`.csproj is missing a <${tag}> tag`);
|
|
399
|
+
return match[1];
|
|
400
|
+
}
|
|
401
|
+
function extractPackageReferences(content) {
|
|
402
|
+
const refs = {};
|
|
403
|
+
const re = /<PackageReference\s+Include="([^"]+)"\s+Version="([^"]+)"\s*\/>/g;
|
|
404
|
+
let m;
|
|
405
|
+
while (m = re.exec(content)) {
|
|
406
|
+
refs[m[1]] = m[2];
|
|
407
|
+
}
|
|
408
|
+
return refs;
|
|
409
|
+
}
|
|
410
|
+
function setTag(content, tag, value) {
|
|
411
|
+
const escapedTag = escapeRegExp(tag);
|
|
412
|
+
return content.replace(new RegExp(`(<${escapedTag}>)[^<]*(</${escapedTag}>)`), `$1${value}$2`);
|
|
413
|
+
}
|
|
414
|
+
function setPackageReferenceVersion(content, name, version) {
|
|
415
|
+
const re = new RegExp(`(<PackageReference\\s+Include="${escapeRegExp(name)}"\\s+Version=")[^"]+(")`);
|
|
416
|
+
return content.replace(re, `$1${version}$2`);
|
|
417
|
+
}
|
|
418
|
+
function addPackageReference(content, name, version) {
|
|
419
|
+
const re = /^([ \t]*)<PackageReference[^\n]*\/>\n(?=[ \t]*<\/ItemGroup>)/m;
|
|
420
|
+
return content.replace(re, (match, indent) => `${match}${indent}<PackageReference Include="${name}" Version="${version}" />
|
|
421
|
+
`);
|
|
422
|
+
}
|
|
423
|
+
function parseManifestFile(rawContent) {
|
|
424
|
+
return {
|
|
425
|
+
raw: rawContent,
|
|
426
|
+
version: extractTag(rawContent, "Version"),
|
|
427
|
+
targetFramework: extractTag(rawContent, "TargetFramework"),
|
|
428
|
+
packageId: extractTag(rawContent, "PackageId"),
|
|
429
|
+
toolCommandName: extractTag(rawContent, "ToolCommandName"),
|
|
430
|
+
packageReferences: extractPackageReferences(rawContent)
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
function extractCoreFields2(manifestFile) {
|
|
434
|
+
return {
|
|
435
|
+
coreDependencies: manifestFile.packageReferences,
|
|
436
|
+
coreScripts: {},
|
|
437
|
+
coreFields: { TargetFramework: manifestFile.targetFramework }
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
function mergeManifestFile(current, oldManifest, newTemplate) {
|
|
441
|
+
let raw = current.raw;
|
|
442
|
+
let changed = false;
|
|
443
|
+
const dependencies = [];
|
|
444
|
+
const coreDependencies = {};
|
|
445
|
+
for (const name of Object.keys(newTemplate.packageReferences)) {
|
|
446
|
+
const newValue = newTemplate.packageReferences[name];
|
|
447
|
+
const currentValue = current.packageReferences[name];
|
|
448
|
+
const oldValue = oldManifest.coreDependencies[name];
|
|
449
|
+
const result = reconcileEntry(currentValue, oldValue, newValue, stringEquals);
|
|
450
|
+
dependencies.push({ key: name, outcome: result.outcome });
|
|
451
|
+
coreDependencies[name] = result.value;
|
|
452
|
+
if (result.outcome === "added") {
|
|
453
|
+
changed = true;
|
|
454
|
+
raw = addPackageReference(raw, name, result.value);
|
|
455
|
+
} else if (result.outcome !== "skipped" && result.value !== currentValue) {
|
|
456
|
+
changed = true;
|
|
457
|
+
raw = setPackageReferenceVersion(raw, name, result.value);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
const oldCoreFields = oldManifest.coreFields;
|
|
461
|
+
const fields = [];
|
|
462
|
+
const targetFrameworkResult = reconcileEntry(
|
|
463
|
+
current.targetFramework,
|
|
464
|
+
oldCoreFields.TargetFramework,
|
|
465
|
+
newTemplate.targetFramework,
|
|
466
|
+
stringEquals
|
|
467
|
+
);
|
|
468
|
+
fields.push({ key: "TargetFramework", outcome: targetFrameworkResult.outcome });
|
|
469
|
+
if (targetFrameworkResult.outcome !== "skipped" && targetFrameworkResult.value !== current.targetFramework) {
|
|
470
|
+
changed = true;
|
|
471
|
+
raw = setTag(raw, "TargetFramework", targetFrameworkResult.value);
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
updatedFile: { ...current, raw },
|
|
475
|
+
changed,
|
|
476
|
+
dependencies,
|
|
477
|
+
scripts: [],
|
|
478
|
+
fields,
|
|
479
|
+
coreDependencies,
|
|
480
|
+
coreScripts: {},
|
|
481
|
+
coreFields: { TargetFramework: targetFrameworkResult.value }
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
var dotnetAdapter = {
|
|
485
|
+
coreFilePaths: CORE_FILE_PATHS2,
|
|
486
|
+
templateSourcePath(relativePath) {
|
|
487
|
+
return relativePath === ".gitignore" ? "gitignore" : relativePath;
|
|
488
|
+
},
|
|
489
|
+
manifestFileName: "src/Cli.csproj",
|
|
490
|
+
async readManifestFile(dir) {
|
|
491
|
+
const content = await readFile3(path6.join(dir, "src", "Cli.csproj"), "utf8");
|
|
492
|
+
return parseManifestFile(content);
|
|
493
|
+
},
|
|
494
|
+
async writeManifestFile(dir, content) {
|
|
495
|
+
await writeFile4(path6.join(dir, "src", "Cli.csproj"), content.raw);
|
|
496
|
+
},
|
|
497
|
+
parseManifestFile,
|
|
498
|
+
readProjectName(manifestFile) {
|
|
499
|
+
return manifestFile.packageId;
|
|
500
|
+
},
|
|
501
|
+
extractCoreFields(manifestFile) {
|
|
502
|
+
return extractCoreFields2(manifestFile);
|
|
503
|
+
},
|
|
504
|
+
mergeManifestFile(current, oldManifest, newTemplate) {
|
|
505
|
+
return mergeManifestFile(current, oldManifest, newTemplate);
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
// src/languages/registry-checkers/nuget.ts
|
|
510
|
+
import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
|
|
511
|
+
import path7 from "path";
|
|
512
|
+
var NUGET_DEFAULT_REGISTRY_URL = "https://api.nuget.org/v3/index.json";
|
|
513
|
+
var NUGET_FLATCONTAINER_BASE = "https://api.nuget.org/v3-flatcontainer";
|
|
514
|
+
var FETCH_TIMEOUT_MS2 = 5e3;
|
|
515
|
+
async function checkNameAvailability2(name) {
|
|
516
|
+
const url = `${NUGET_FLATCONTAINER_BASE}/${encodeURIComponent(name.toLowerCase())}/index.json`;
|
|
517
|
+
try {
|
|
518
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS2) });
|
|
519
|
+
if (response.status === 404) return "available";
|
|
520
|
+
if (response.status === 200) return "taken";
|
|
521
|
+
return "unverified";
|
|
522
|
+
} catch {
|
|
523
|
+
return "unverified";
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
async function applyPrivateIntent2(targetDir) {
|
|
527
|
+
const csprojPath = path7.join(targetDir, "src", "Cli.csproj");
|
|
528
|
+
const content = await readFile4(csprojPath, "utf8");
|
|
529
|
+
const updated = content.replace(
|
|
530
|
+
/(<PropertyGroup>\s*\n)/,
|
|
531
|
+
"$1 <IsPackable>false</IsPackable>\n"
|
|
532
|
+
);
|
|
533
|
+
await writeFile5(csprojPath, updated);
|
|
534
|
+
}
|
|
535
|
+
async function applyRegistryUrl2(targetDir, registryUrl) {
|
|
536
|
+
const config = [
|
|
537
|
+
'<?xml version="1.0" encoding="utf-8"?>',
|
|
538
|
+
"<configuration>",
|
|
539
|
+
" <packageSources>",
|
|
540
|
+
" <clear />",
|
|
541
|
+
` <add key="custom" value="${registryUrl}" />`,
|
|
542
|
+
" </packageSources>",
|
|
543
|
+
"</configuration>",
|
|
544
|
+
""
|
|
545
|
+
].join("\n");
|
|
546
|
+
await mkdir2(targetDir, { recursive: true });
|
|
547
|
+
await writeFile5(path7.join(targetDir, "NuGet.config"), config);
|
|
548
|
+
}
|
|
549
|
+
var nugetRegistryChecker = {
|
|
550
|
+
checkNameAvailability: checkNameAvailability2,
|
|
551
|
+
applyPrivateIntent: applyPrivateIntent2,
|
|
552
|
+
applyRegistryUrl: applyRegistryUrl2
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
// src/languages/command-generators/dotnet.ts
|
|
556
|
+
import { mkdir as mkdir3, readFile as readFile5, readdir as readdir2, writeFile as writeFile6 } from "fs/promises";
|
|
557
|
+
import path8 from "path";
|
|
558
|
+
async function listExistingCommands2(targetDir) {
|
|
559
|
+
const commandsDir = path8.join(targetDir, "src", "Commands");
|
|
560
|
+
const files = await readdir2(commandsDir);
|
|
561
|
+
const paths2 = [];
|
|
562
|
+
for (const file of files) {
|
|
563
|
+
if (!file.endsWith(".cs")) continue;
|
|
564
|
+
const content = await readFile5(path8.join(commandsDir, file), "utf8");
|
|
565
|
+
const match = content.match(/\[CommandPath\("([^"]+)"\)\]/);
|
|
566
|
+
if (match) paths2.push(match[1]);
|
|
567
|
+
}
|
|
568
|
+
return buildCommandTree(paths2);
|
|
569
|
+
}
|
|
570
|
+
function toClassName2(pathSegments) {
|
|
571
|
+
return pathSegments.map((seg) => seg[0].toUpperCase() + seg.slice(1)).join("") + "Command";
|
|
572
|
+
}
|
|
573
|
+
function csharpDeclType(param) {
|
|
574
|
+
const baseType = param.type === "integer" ? "int" : param.type === "boolean" ? "bool" : "string";
|
|
575
|
+
if (param.required) return baseType;
|
|
576
|
+
return `${baseType}?`;
|
|
577
|
+
}
|
|
578
|
+
function argumentDeclaration(param) {
|
|
579
|
+
const varName = `${param.name}Argument`;
|
|
580
|
+
const declType = csharpDeclType(param);
|
|
581
|
+
const arityLine = param.required ? "" : "\n Arity = ArgumentArity.ZeroOrOne,";
|
|
582
|
+
const lines = [
|
|
583
|
+
` var ${varName} = new Argument<${declType}>("${param.name}")`,
|
|
584
|
+
` {`,
|
|
585
|
+
` Description = "${param.name}",${arityLine}`,
|
|
586
|
+
` };`
|
|
587
|
+
];
|
|
588
|
+
if (param.type === "enum") {
|
|
589
|
+
lines.push(` ${varName}.AcceptOnlyFromAmong(${(param.allowedValues ?? []).map((v) => `"${v}"`).join(", ")});`);
|
|
590
|
+
}
|
|
591
|
+
return lines.join("\n");
|
|
592
|
+
}
|
|
593
|
+
function generateCommandFileContent2(spec) {
|
|
594
|
+
const className = toClassName2(spec.pathSegments);
|
|
595
|
+
const commandPath = spec.pathSegments.join(" ");
|
|
596
|
+
const lastSegment = spec.pathSegments[spec.pathSegments.length - 1];
|
|
597
|
+
const argDecls = spec.parameters.map(argumentDeclaration).join("\n\n");
|
|
598
|
+
const addLines = spec.parameters.map((p) => ` command.Arguments.Add(${p.name}Argument);`).join("\n");
|
|
599
|
+
const getValueLines = spec.parameters.map((p) => ` var ${p.name} = parseResult.GetValue(${p.name}Argument);`).join("\n");
|
|
600
|
+
const interpolated = spec.parameters.map((p) => `${p.name}={${p.name}}`).join(" ");
|
|
601
|
+
return `using System.CommandLine;
|
|
602
|
+
|
|
603
|
+
namespace Cli.Commands;
|
|
604
|
+
|
|
605
|
+
[CommandPath("${commandPath}")]
|
|
606
|
+
public sealed class ${className} : ICliCommand
|
|
607
|
+
{
|
|
608
|
+
public Command Build()
|
|
609
|
+
{
|
|
610
|
+
${argDecls}
|
|
611
|
+
|
|
612
|
+
var command = new Command("${lastSegment}", "${lastSegment} command");
|
|
613
|
+
${addLines}
|
|
614
|
+
command.SetAction(parseResult =>
|
|
615
|
+
{
|
|
616
|
+
${getValueLines}
|
|
617
|
+
Console.WriteLine($"${interpolated}");
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
return command;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
`;
|
|
624
|
+
}
|
|
625
|
+
function sampleArgValue2(param) {
|
|
626
|
+
if (param.type === "enum") return param.allowedValues?.[0] ?? "";
|
|
627
|
+
if (param.type === "integer") return "1";
|
|
628
|
+
if (param.type === "boolean") return "true";
|
|
629
|
+
return "value";
|
|
630
|
+
}
|
|
631
|
+
function generateTestFileContent2(spec) {
|
|
632
|
+
const className = toClassName2(spec.pathSegments);
|
|
633
|
+
const sampleArgs = spec.parameters.map(sampleArgValue2);
|
|
634
|
+
return `using System.CommandLine;
|
|
635
|
+
using Cli.Commands;
|
|
636
|
+
|
|
637
|
+
namespace Cli.Tests;
|
|
638
|
+
|
|
639
|
+
public class ${className}Tests
|
|
640
|
+
{
|
|
641
|
+
[Fact]
|
|
642
|
+
public void RunsSuccessfully()
|
|
643
|
+
{
|
|
644
|
+
var command = new ${className}().Build();
|
|
645
|
+
var parseResult = command.Parse([${sampleArgs.map((a) => `"${a}"`).join(", ")}]);
|
|
646
|
+
|
|
647
|
+
var exitCode = parseResult.Invoke();
|
|
648
|
+
|
|
649
|
+
Assert.Equal(0, exitCode);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
`;
|
|
653
|
+
}
|
|
654
|
+
async function generateCommand2(targetDir, spec) {
|
|
655
|
+
const className = toClassName2(spec.pathSegments);
|
|
656
|
+
const commandRelPath = path8.join("src", "Commands", `${className}.cs`);
|
|
657
|
+
const testRelPath = path8.join("tests", `${className}Tests.cs`);
|
|
658
|
+
await mkdir3(path8.join(targetDir, "src", "Commands"), { recursive: true });
|
|
659
|
+
await mkdir3(path8.join(targetDir, "tests"), { recursive: true });
|
|
660
|
+
await writeFile6(path8.join(targetDir, commandRelPath), generateCommandFileContent2(spec));
|
|
661
|
+
await writeFile6(path8.join(targetDir, testRelPath), generateTestFileContent2(spec));
|
|
662
|
+
return { commandFile: commandRelPath, testFile: testRelPath };
|
|
663
|
+
}
|
|
664
|
+
var dotnetCommandGenerator = {
|
|
665
|
+
listExistingCommands: listExistingCommands2,
|
|
666
|
+
generateCommand: generateCommand2
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
// src/languages/packs/dotnet.ts
|
|
670
|
+
function validateProjectName2(value) {
|
|
671
|
+
if (!value || value.trim().length === 0) return "Project name is required.";
|
|
672
|
+
if (!/^[A-Z][A-Za-z0-9]*$/.test(value)) {
|
|
673
|
+
return "Use PascalCase, starting with an uppercase letter (e.g. MyTool).";
|
|
243
674
|
}
|
|
675
|
+
return void 0;
|
|
676
|
+
}
|
|
677
|
+
var dotnetPack = {
|
|
678
|
+
id: "dotnet",
|
|
679
|
+
displayName: ".NET / C# (System.CommandLine)",
|
|
680
|
+
templateDir: path9.join(findPackageRoot(), "templates", "dotnet"),
|
|
681
|
+
scaffoldCommands: [
|
|
682
|
+
{ command: "dotnet", args: ["restore"] },
|
|
683
|
+
{ command: "dotnet", args: ["build"] }
|
|
684
|
+
],
|
|
685
|
+
validateProjectName: validateProjectName2,
|
|
686
|
+
updateAdapter: dotnetAdapter,
|
|
687
|
+
registry: {
|
|
688
|
+
defaultUrl: NUGET_DEFAULT_REGISTRY_URL,
|
|
689
|
+
promptLabel: "Custom NuGet feed URL (leave empty for nuget.org)",
|
|
690
|
+
checkNameAvailability: nugetRegistryChecker.checkNameAvailability,
|
|
691
|
+
applyPrivateIntent: nugetRegistryChecker.applyPrivateIntent,
|
|
692
|
+
applyRegistryUrl: nugetRegistryChecker.applyRegistryUrl
|
|
693
|
+
},
|
|
694
|
+
commandGenerator: dotnetCommandGenerator
|
|
244
695
|
};
|
|
245
696
|
|
|
246
697
|
// src/languages/index.ts
|
|
247
698
|
var LANGUAGE_PACKS = {
|
|
248
|
-
[nodeOclifPack.id]: nodeOclifPack
|
|
699
|
+
[nodeOclifPack.id]: nodeOclifPack,
|
|
700
|
+
[dotnetPack.id]: dotnetPack
|
|
249
701
|
};
|
|
250
702
|
|
|
251
703
|
// src/wizard.ts
|
|
@@ -324,15 +776,15 @@ async function runWizard(deps = defaultDeps) {
|
|
|
324
776
|
}
|
|
325
777
|
|
|
326
778
|
// src/scaffold.ts
|
|
327
|
-
import { cp, readdir, readFile as
|
|
328
|
-
import
|
|
779
|
+
import { cp, readdir as readdir3, readFile as readFile7, rename, writeFile as writeFile8 } from "fs/promises";
|
|
780
|
+
import path11 from "path";
|
|
329
781
|
import spawn from "cross-spawn";
|
|
330
782
|
|
|
331
783
|
// src/update/manifest.ts
|
|
332
784
|
import { createHash } from "crypto";
|
|
333
785
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
334
|
-
import { mkdir, readFile as
|
|
335
|
-
import
|
|
786
|
+
import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile7 } from "fs/promises";
|
|
787
|
+
import path10 from "path";
|
|
336
788
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
337
789
|
|
|
338
790
|
// src/errors.ts
|
|
@@ -350,7 +802,7 @@ function hashContent(content) {
|
|
|
350
802
|
async function hashCoreFiles(dir, adapter) {
|
|
351
803
|
const entries = await Promise.all(
|
|
352
804
|
adapter.coreFilePaths.map(async (relativePath) => {
|
|
353
|
-
const content = await
|
|
805
|
+
const content = await readFile6(path10.join(dir, relativePath), "utf8");
|
|
354
806
|
return [relativePath, hashContent(content)];
|
|
355
807
|
})
|
|
356
808
|
);
|
|
@@ -362,15 +814,15 @@ async function buildManifest(targetDir, generatorVersion, language, adapter) {
|
|
|
362
814
|
const { coreDependencies, coreScripts, coreFields } = adapter.extractCoreFields(manifestFile);
|
|
363
815
|
return { generatorVersion, language, coreFiles, coreDependencies, coreScripts, coreFields };
|
|
364
816
|
}
|
|
365
|
-
var MANIFEST_RELATIVE_PATH =
|
|
817
|
+
var MANIFEST_RELATIVE_PATH = path10.join(".clispark", "manifest.json");
|
|
366
818
|
async function writeManifest(targetDir, manifest) {
|
|
367
|
-
const manifestPath =
|
|
368
|
-
await
|
|
369
|
-
await
|
|
819
|
+
const manifestPath = path10.join(targetDir, MANIFEST_RELATIVE_PATH);
|
|
820
|
+
await mkdir4(path10.dirname(manifestPath), { recursive: true });
|
|
821
|
+
await writeFile7(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
370
822
|
}
|
|
371
823
|
async function readManifest(targetDir) {
|
|
372
824
|
try {
|
|
373
|
-
const content = await
|
|
825
|
+
const content = await readFile6(path10.join(targetDir, MANIFEST_RELATIVE_PATH), "utf8");
|
|
374
826
|
return JSON.parse(content);
|
|
375
827
|
} catch {
|
|
376
828
|
return void 0;
|
|
@@ -386,14 +838,14 @@ async function requireManifest(targetDir) {
|
|
|
386
838
|
return manifest;
|
|
387
839
|
}
|
|
388
840
|
function getGeneratorVersion() {
|
|
389
|
-
let dir =
|
|
841
|
+
let dir = path10.dirname(fileURLToPath2(import.meta.url));
|
|
390
842
|
while (true) {
|
|
391
|
-
const pkgPath =
|
|
843
|
+
const pkgPath = path10.join(dir, "package.json");
|
|
392
844
|
if (existsSync2(pkgPath)) {
|
|
393
845
|
const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
|
|
394
846
|
if (pkg.name === "clispark") return pkg.version;
|
|
395
847
|
}
|
|
396
|
-
const parentDir =
|
|
848
|
+
const parentDir = path10.dirname(dir);
|
|
397
849
|
if (parentDir === dir) {
|
|
398
850
|
throw new Error("Could not locate clispark's own package.json.");
|
|
399
851
|
}
|
|
@@ -405,7 +857,7 @@ function getGeneratorVersion() {
|
|
|
405
857
|
async function assertTargetDirIsUsable(targetDir) {
|
|
406
858
|
let entries;
|
|
407
859
|
try {
|
|
408
|
-
entries = await
|
|
860
|
+
entries = await readdir3(targetDir);
|
|
409
861
|
} catch {
|
|
410
862
|
return;
|
|
411
863
|
}
|
|
@@ -417,10 +869,10 @@ function applyPlaceholders(content, projectName) {
|
|
|
417
869
|
return content.replaceAll("{{projectName}}", projectName);
|
|
418
870
|
}
|
|
419
871
|
async function collectFiles(dir) {
|
|
420
|
-
const entries = await
|
|
872
|
+
const entries = await readdir3(dir, { withFileTypes: true });
|
|
421
873
|
const files = await Promise.all(
|
|
422
874
|
entries.map((entry) => {
|
|
423
|
-
const fullPath =
|
|
875
|
+
const fullPath = path11.join(dir, entry.name);
|
|
424
876
|
return entry.isDirectory() ? collectFiles(fullPath) : Promise.resolve([fullPath]);
|
|
425
877
|
})
|
|
426
878
|
);
|
|
@@ -430,9 +882,9 @@ async function replacePlaceholdersInTree(targetDir, projectName) {
|
|
|
430
882
|
const files = await collectFiles(targetDir);
|
|
431
883
|
await Promise.all(
|
|
432
884
|
files.map(async (filePath) => {
|
|
433
|
-
const content = await
|
|
885
|
+
const content = await readFile7(filePath, "utf8");
|
|
434
886
|
if (content.includes("{{projectName}}")) {
|
|
435
|
-
await
|
|
887
|
+
await writeFile8(filePath, applyPlaceholders(content, projectName));
|
|
436
888
|
}
|
|
437
889
|
})
|
|
438
890
|
);
|
|
@@ -441,14 +893,13 @@ async function copyTemplate(options, pack) {
|
|
|
441
893
|
const { projectName, targetDir, registryUrl, publishIntent } = options;
|
|
442
894
|
await assertTargetDirIsUsable(targetDir);
|
|
443
895
|
await cp(pack.templateDir, targetDir, { recursive: true });
|
|
444
|
-
await rename(
|
|
896
|
+
await rename(path11.join(targetDir, "gitignore"), path11.join(targetDir, ".gitignore"));
|
|
445
897
|
await replacePlaceholdersInTree(targetDir, projectName);
|
|
446
898
|
if (publishIntent === false) {
|
|
447
899
|
await pack.registry.applyPrivateIntent(targetDir);
|
|
448
900
|
}
|
|
449
901
|
if (registryUrl && registryUrl !== pack.registry.defaultUrl) {
|
|
450
|
-
await
|
|
451
|
-
`);
|
|
902
|
+
await pack.registry.applyRegistryUrl(targetDir, registryUrl);
|
|
452
903
|
}
|
|
453
904
|
}
|
|
454
905
|
async function defaultRunCommand(command, args, cwd) {
|
|
@@ -481,7 +932,7 @@ async function scaffoldProject(options, pack, deps = defaultScaffoldDeps) {
|
|
|
481
932
|
// src/logger.ts
|
|
482
933
|
import { randomBytes } from "crypto";
|
|
483
934
|
import { mkdirSync, readdirSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
484
|
-
import
|
|
935
|
+
import path12 from "path";
|
|
485
936
|
import envPaths from "env-paths";
|
|
486
937
|
import pino from "pino";
|
|
487
938
|
var paths = envPaths("clispark", { suffix: "" });
|
|
@@ -517,7 +968,7 @@ var SWEEP_MARKER_FILE = ".last-sweep";
|
|
|
517
968
|
var SWEEP_THROTTLE_MS = 24 * 60 * 60 * 1e3;
|
|
518
969
|
function sweepOldLogs(logDir) {
|
|
519
970
|
safely(() => {
|
|
520
|
-
const markerPath =
|
|
971
|
+
const markerPath = path12.join(logDir, SWEEP_MARKER_FILE);
|
|
521
972
|
let shouldSweep = true;
|
|
522
973
|
try {
|
|
523
974
|
shouldSweep = Date.now() - statSync(markerPath).mtimeMs >= SWEEP_THROTTLE_MS;
|
|
@@ -527,7 +978,7 @@ function sweepOldLogs(logDir) {
|
|
|
527
978
|
const cutoffMs = Date.now() - getRetentionDays() * 24 * 60 * 60 * 1e3;
|
|
528
979
|
for (const file of readdirSync(logDir)) {
|
|
529
980
|
if (file === SWEEP_MARKER_FILE) continue;
|
|
530
|
-
const filePath =
|
|
981
|
+
const filePath = path12.join(logDir, file);
|
|
531
982
|
if (statSync(filePath).mtimeMs < cutoffMs) {
|
|
532
983
|
unlinkSync(filePath);
|
|
533
984
|
}
|
|
@@ -538,7 +989,7 @@ function sweepOldLogs(logDir) {
|
|
|
538
989
|
function createLogger(commandName, logDir = paths.log) {
|
|
539
990
|
mkdirSync(logDir, { recursive: true });
|
|
540
991
|
sweepOldLogs(logDir);
|
|
541
|
-
const logFilePath =
|
|
992
|
+
const logFilePath = path12.join(logDir, buildLogFileName(commandName));
|
|
542
993
|
const fileDestination = pino.destination({ dest: logFilePath, sync: true, mode: 384 });
|
|
543
994
|
const destination = process.env.DEBUG ? pino.multistream([{ stream: fileDestination }, { stream: process.stdout }]) : fileDestination;
|
|
544
995
|
const logger = pino({ redact: buildRedactPaths([...SENSITIVE_LOG_KEYS, "registryUrl"]) }, destination);
|
|
@@ -576,8 +1027,8 @@ function withLogging(commandName, action, logDir = paths.log, loggerFactory = cr
|
|
|
576
1027
|
}
|
|
577
1028
|
|
|
578
1029
|
// src/update/update.ts
|
|
579
|
-
import { mkdir as
|
|
580
|
-
import
|
|
1030
|
+
import { mkdir as mkdir5, readFile as readFile8, writeFile as writeFile9 } from "fs/promises";
|
|
1031
|
+
import path13 from "path";
|
|
581
1032
|
import spawn2 from "cross-spawn";
|
|
582
1033
|
|
|
583
1034
|
// src/update/releasenotes.ts
|
|
@@ -595,7 +1046,7 @@ function compareVersions(a, b) {
|
|
|
595
1046
|
return 0;
|
|
596
1047
|
}
|
|
597
1048
|
var RELEASES_URL = "https://api.github.com/repos/martinwichner/clispark/releases";
|
|
598
|
-
var
|
|
1049
|
+
var FETCH_TIMEOUT_MS3 = 5e3;
|
|
599
1050
|
async function fetchReleaseNotes(targetDir, fetchFn = fetch) {
|
|
600
1051
|
const manifest = await requireManifest(targetDir);
|
|
601
1052
|
const fromVersion = manifest.generatorVersion;
|
|
@@ -603,7 +1054,7 @@ async function fetchReleaseNotes(targetDir, fetchFn = fetch) {
|
|
|
603
1054
|
if (compareVersions(fromVersion, toVersion) >= 0) {
|
|
604
1055
|
return { status: "up-to-date", fromVersion, toVersion, releases: [] };
|
|
605
1056
|
}
|
|
606
|
-
const response = await fetchFn(RELEASES_URL, { signal: AbortSignal.timeout(
|
|
1057
|
+
const response = await fetchFn(RELEASES_URL, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS3) });
|
|
607
1058
|
if (!response.ok) {
|
|
608
1059
|
throw new Error(`Failed to fetch release notes: GitHub API responded with ${response.status}`);
|
|
609
1060
|
}
|
|
@@ -663,7 +1114,7 @@ async function updateProject(targetDir, adapter, templateDir, language, deps = d
|
|
|
663
1114
|
const currentManifestFile = await adapter.readManifestFile(targetDir);
|
|
664
1115
|
const projectName = adapter.readProjectName(currentManifestFile);
|
|
665
1116
|
const newTemplateRaw = applyPlaceholders(
|
|
666
|
-
await
|
|
1117
|
+
await readFile8(path13.join(templateDir, adapter.manifestFileName), "utf8"),
|
|
667
1118
|
projectName
|
|
668
1119
|
);
|
|
669
1120
|
const newTemplateManifestFile = adapter.parseManifestFile(newTemplateRaw);
|
|
@@ -673,13 +1124,13 @@ async function updateProject(targetDir, adapter, templateDir, language, deps = d
|
|
|
673
1124
|
const perFileResults = await Promise.all(
|
|
674
1125
|
adapter.coreFilePaths.map(async (relativePath) => {
|
|
675
1126
|
const newContent = applyPlaceholders(
|
|
676
|
-
await
|
|
1127
|
+
await readFile8(path13.join(templateDir, adapter.templateSourcePath(relativePath)), "utf8"),
|
|
677
1128
|
projectName
|
|
678
1129
|
);
|
|
679
1130
|
const newHash = hashContent(newContent);
|
|
680
1131
|
let currentHash;
|
|
681
1132
|
try {
|
|
682
|
-
currentHash = hashContent(await
|
|
1133
|
+
currentHash = hashContent(await readFile8(path13.join(targetDir, relativePath), "utf8"));
|
|
683
1134
|
} catch {
|
|
684
1135
|
currentHash = void 0;
|
|
685
1136
|
}
|
|
@@ -691,13 +1142,13 @@ async function updateProject(targetDir, adapter, templateDir, language, deps = d
|
|
|
691
1142
|
files.push({ path: relativePath, outcome: result.outcome });
|
|
692
1143
|
newCoreFiles[relativePath] = result.value;
|
|
693
1144
|
if (result.outcome === "added" || result.outcome === "replaced") {
|
|
694
|
-
fileWrites.push({ targetPath:
|
|
1145
|
+
fileWrites.push({ targetPath: path13.join(targetDir, relativePath), content: newContent });
|
|
695
1146
|
}
|
|
696
1147
|
}
|
|
697
1148
|
for (const relativePath of Object.keys(oldManifest.coreFiles)) {
|
|
698
1149
|
if (adapter.coreFilePaths.includes(relativePath)) continue;
|
|
699
1150
|
try {
|
|
700
|
-
await
|
|
1151
|
+
await readFile8(path13.join(targetDir, relativePath), "utf8");
|
|
701
1152
|
files.push({ path: relativePath, outcome: "no-longer-core" });
|
|
702
1153
|
} catch {
|
|
703
1154
|
}
|
|
@@ -719,8 +1170,8 @@ async function updateProject(targetDir, adapter, templateDir, language, deps = d
|
|
|
719
1170
|
};
|
|
720
1171
|
}
|
|
721
1172
|
for (const write of fileWrites) {
|
|
722
|
-
await
|
|
723
|
-
await
|
|
1173
|
+
await mkdir5(path13.dirname(write.targetPath), { recursive: true });
|
|
1174
|
+
await writeFile9(write.targetPath, write.content);
|
|
724
1175
|
}
|
|
725
1176
|
if (fileMerge.changed) {
|
|
726
1177
|
await adapter.writeManifestFile(targetDir, fileMerge.updatedFile);
|
|
@@ -782,10 +1233,157 @@ function formatUpdateSummary(result) {
|
|
|
782
1233
|
return lines.join("\n");
|
|
783
1234
|
}
|
|
784
1235
|
|
|
1236
|
+
// src/add-wizard.ts
|
|
1237
|
+
import { intro as intro2, outro as outro2, select as select2, text as text2, confirm, isCancel as isCancel2, cancel as cancel2, log as log2 } from "@clack/prompts";
|
|
1238
|
+
function exitIfCancelled2(value) {
|
|
1239
|
+
if (isCancel2(value)) {
|
|
1240
|
+
cancel2("Operation cancelled.");
|
|
1241
|
+
process.exit(1);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
var NEW_TOP_LEVEL = "__new_top_level__";
|
|
1245
|
+
var HERE = "__here__";
|
|
1246
|
+
function flattenForMenu(nodes) {
|
|
1247
|
+
const result = [];
|
|
1248
|
+
for (const node of nodes) {
|
|
1249
|
+
result.push({ value: node.path, label: node.displayLabel });
|
|
1250
|
+
result.push(...flattenForMenu(node.children));
|
|
1251
|
+
}
|
|
1252
|
+
return result;
|
|
1253
|
+
}
|
|
1254
|
+
function findNode(nodes, targetPath) {
|
|
1255
|
+
for (const node of nodes) {
|
|
1256
|
+
if (node.path === targetPath) return node;
|
|
1257
|
+
const found = findNode(node.children, targetPath);
|
|
1258
|
+
if (found) return found;
|
|
1259
|
+
}
|
|
1260
|
+
return void 0;
|
|
1261
|
+
}
|
|
1262
|
+
async function selectWithinNode(node) {
|
|
1263
|
+
if (node.children.length === 0) {
|
|
1264
|
+
return node.path.split(" ");
|
|
1265
|
+
}
|
|
1266
|
+
const options = [
|
|
1267
|
+
{ value: HERE, label: `Direct subcommand of "${node.path}"` },
|
|
1268
|
+
...node.children.map((child) => ({ value: child.path, label: `Under "${child.path}"` }))
|
|
1269
|
+
];
|
|
1270
|
+
const choice = await select2({ message: `Where under "${node.path}"?`, options });
|
|
1271
|
+
exitIfCancelled2(choice);
|
|
1272
|
+
if (choice === HERE) {
|
|
1273
|
+
return node.path.split(" ");
|
|
1274
|
+
}
|
|
1275
|
+
const childNode = node.children.find((c) => c.path === choice);
|
|
1276
|
+
return selectWithinNode(childNode);
|
|
1277
|
+
}
|
|
1278
|
+
async function selectPath(existing) {
|
|
1279
|
+
const options = [{ value: NEW_TOP_LEVEL, label: "New top-level command" }, ...flattenForMenu(existing)];
|
|
1280
|
+
const choice = await select2({ message: "Where should the new command go?", options });
|
|
1281
|
+
exitIfCancelled2(choice);
|
|
1282
|
+
if (choice === NEW_TOP_LEVEL) {
|
|
1283
|
+
return [];
|
|
1284
|
+
}
|
|
1285
|
+
const node = findNode(existing, choice);
|
|
1286
|
+
return selectWithinNode(node);
|
|
1287
|
+
}
|
|
1288
|
+
function flattenPaths(nodes) {
|
|
1289
|
+
return nodes.flatMap((node) => [node.path, ...flattenPaths(node.children)]);
|
|
1290
|
+
}
|
|
1291
|
+
async function collectParameters() {
|
|
1292
|
+
const parameters = [];
|
|
1293
|
+
let hasOptional = false;
|
|
1294
|
+
for (; ; ) {
|
|
1295
|
+
const addMore = await confirm({
|
|
1296
|
+
message: parameters.length === 0 ? "Add a parameter?" : "Add another parameter?"
|
|
1297
|
+
});
|
|
1298
|
+
exitIfCancelled2(addMore);
|
|
1299
|
+
if (!addMore) break;
|
|
1300
|
+
const nameValue = await text2({
|
|
1301
|
+
message: "Parameter name",
|
|
1302
|
+
validate: (value) => {
|
|
1303
|
+
if (!/^[a-z][a-zA-Z0-9]*$/.test(value)) return "Use a single word starting with a lowercase letter.";
|
|
1304
|
+
if (parameters.some((p) => p.name === value)) return "A parameter with this name already exists.";
|
|
1305
|
+
return void 0;
|
|
1306
|
+
}
|
|
1307
|
+
});
|
|
1308
|
+
exitIfCancelled2(nameValue);
|
|
1309
|
+
const name = nameValue;
|
|
1310
|
+
const typeValue = await select2({
|
|
1311
|
+
message: "Parameter type",
|
|
1312
|
+
options: [
|
|
1313
|
+
{ value: "string", label: "String" },
|
|
1314
|
+
{ value: "integer", label: "Integer" },
|
|
1315
|
+
{ value: "boolean", label: "Boolean" },
|
|
1316
|
+
{ value: "enum", label: "String with allowed values" }
|
|
1317
|
+
]
|
|
1318
|
+
});
|
|
1319
|
+
exitIfCancelled2(typeValue);
|
|
1320
|
+
const type = typeValue;
|
|
1321
|
+
let required = false;
|
|
1322
|
+
if (type !== "boolean") {
|
|
1323
|
+
const options = hasOptional ? [{ value: false, label: "Optional" }] : [
|
|
1324
|
+
{ value: true, label: "Required" },
|
|
1325
|
+
{ value: false, label: "Optional" }
|
|
1326
|
+
];
|
|
1327
|
+
const requiredValue = await select2({ message: "Required or optional?", options });
|
|
1328
|
+
exitIfCancelled2(requiredValue);
|
|
1329
|
+
required = requiredValue;
|
|
1330
|
+
}
|
|
1331
|
+
if (!required) hasOptional = true;
|
|
1332
|
+
let allowedValues;
|
|
1333
|
+
if (type === "enum") {
|
|
1334
|
+
const valuesInput = await text2({
|
|
1335
|
+
message: "Allowed values (comma-separated)",
|
|
1336
|
+
validate: (value) => {
|
|
1337
|
+
const values = value.split(",").map((v) => v.trim()).filter(Boolean);
|
|
1338
|
+
if (values.length < 2) return "Enter at least two comma-separated values.";
|
|
1339
|
+
for (const v of values) {
|
|
1340
|
+
if (!/^[A-Za-z0-9_-]+$/.test(v)) {
|
|
1341
|
+
return `Invalid value "${v}": use only letters, numbers, hyphens, and underscores.`;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
return void 0;
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
exitIfCancelled2(valuesInput);
|
|
1348
|
+
allowedValues = valuesInput.split(",").map((v) => v.trim()).filter(Boolean);
|
|
1349
|
+
}
|
|
1350
|
+
parameters.push({ name, type, required, allowedValues });
|
|
1351
|
+
}
|
|
1352
|
+
return parameters;
|
|
1353
|
+
}
|
|
1354
|
+
async function runAddWizard(targetDir, deps) {
|
|
1355
|
+
intro2("clispark add \u2014 add a new command");
|
|
1356
|
+
const existing = await deps.commandGenerator.listExistingCommands(targetDir);
|
|
1357
|
+
const pathSegments = await selectPath(existing);
|
|
1358
|
+
const existingPaths = new Set(flattenPaths(existing));
|
|
1359
|
+
const nameValue = await text2({
|
|
1360
|
+
message: "Command name",
|
|
1361
|
+
validate: (value) => {
|
|
1362
|
+
if (!/^[a-z][a-zA-Z0-9]*$/.test(value)) return "Use a single word starting with a lowercase letter.";
|
|
1363
|
+
const fullPath = [...pathSegments, value].join(" ");
|
|
1364
|
+
if (existingPaths.has(fullPath)) return `"${fullPath}" already exists.`;
|
|
1365
|
+
return void 0;
|
|
1366
|
+
}
|
|
1367
|
+
});
|
|
1368
|
+
exitIfCancelled2(nameValue);
|
|
1369
|
+
const fullPathSegments = [...pathSegments, nameValue];
|
|
1370
|
+
const parameters = await collectParameters();
|
|
1371
|
+
log2.info(`About to create "${fullPathSegments.join(" ")}" with ${parameters.length} parameter(s).`);
|
|
1372
|
+
const proceed = await confirm({ message: "Proceed?" });
|
|
1373
|
+
exitIfCancelled2(proceed);
|
|
1374
|
+
if (!proceed) {
|
|
1375
|
+
cancel2("Cancelled.");
|
|
1376
|
+
process.exit(1);
|
|
1377
|
+
}
|
|
1378
|
+
const spec = { pathSegments: fullPathSegments, parameters };
|
|
1379
|
+
const result = await deps.commandGenerator.generateCommand(targetDir, spec);
|
|
1380
|
+
outro2(`Created ${result.commandFile} and ${result.testFile}.`);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
785
1383
|
// src/whoami.ts
|
|
786
1384
|
import os from "os";
|
|
787
1385
|
var JOKE_API_URL = "https://v2.jokeapi.dev/joke/Programming,Miscellaneous";
|
|
788
|
-
var
|
|
1386
|
+
var FETCH_TIMEOUT_MS4 = 3e3;
|
|
789
1387
|
var SUPPORTED_JOKE_LANGUAGES = ["cs", "de", "en", "es", "fr", "pt"];
|
|
790
1388
|
var LOGO = String.raw`
|
|
791
1389
|
⚡ clispark
|
|
@@ -843,7 +1441,7 @@ function getRandomFunFact(osFacts = defaultOsFacts, randomFn = Math.random) {
|
|
|
843
1441
|
async function fetchJoke(language, fetchFn) {
|
|
844
1442
|
try {
|
|
845
1443
|
const url = `${JOKE_API_URL}?lang=${language}&safe-mode`;
|
|
846
|
-
const response = await fetchFn(url, { signal: AbortSignal.timeout(
|
|
1444
|
+
const response = await fetchFn(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS4) });
|
|
847
1445
|
if (!response.ok) return void 0;
|
|
848
1446
|
const data = await response.json();
|
|
849
1447
|
if (data.error) return void 0;
|
|
@@ -896,7 +1494,7 @@ function resolvePack(language) {
|
|
|
896
1494
|
program.action(
|
|
897
1495
|
(options) => withLogging("scaffold", async (logger) => {
|
|
898
1496
|
const answers = await runWizard();
|
|
899
|
-
const targetDir =
|
|
1497
|
+
const targetDir = path14.join(process.cwd(), answers.projectName);
|
|
900
1498
|
const pack = resolvePack(answers.language);
|
|
901
1499
|
logger.info({ projectName: answers.projectName, targetDir, language: pack.id }, "scaffold started");
|
|
902
1500
|
await scaffoldProject(
|
|
@@ -937,6 +1535,17 @@ program.command("releasenotes").description("Show what changed between this proj
|
|
|
937
1535
|
console.log(formatReleaseNotes(result));
|
|
938
1536
|
})
|
|
939
1537
|
);
|
|
1538
|
+
program.command("add").description("Add a new command to an already-scaffolded project").action(
|
|
1539
|
+
withLogging("add", async (logger) => {
|
|
1540
|
+
const targetDir = process.cwd();
|
|
1541
|
+
const manifest = await requireManifest(targetDir);
|
|
1542
|
+
const language = manifest.language ?? "node";
|
|
1543
|
+
const pack = resolvePack(language);
|
|
1544
|
+
logger.info({ targetDir, language }, "add started");
|
|
1545
|
+
await runAddWizard(targetDir, { commandGenerator: pack.commandGenerator });
|
|
1546
|
+
logger.info({}, "add completed");
|
|
1547
|
+
})
|
|
1548
|
+
);
|
|
940
1549
|
function resolveWhoamiMode(options) {
|
|
941
1550
|
if (options.joke && options.fact) {
|
|
942
1551
|
throw new UserError("Use either --joke or --fact, not both.");
|