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 CHANGED
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import path9 from "path";
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 path4 from "path";
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: path4.join(findPackageRoot(), "templates", "node"),
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 readFile4, rename, writeFile as writeFile4 } from "fs/promises";
328
- import path6 from "path";
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 readFile3, writeFile as writeFile3 } from "fs/promises";
335
- import path5 from "path";
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 readFile3(path5.join(dir, relativePath), "utf8");
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 = path5.join(".clispark", "manifest.json");
817
+ var MANIFEST_RELATIVE_PATH = path10.join(".clispark", "manifest.json");
366
818
  async function writeManifest(targetDir, manifest) {
367
- const manifestPath = path5.join(targetDir, MANIFEST_RELATIVE_PATH);
368
- await mkdir(path5.dirname(manifestPath), { recursive: true });
369
- await writeFile3(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
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 readFile3(path5.join(targetDir, MANIFEST_RELATIVE_PATH), "utf8");
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 = path5.dirname(fileURLToPath2(import.meta.url));
841
+ let dir = path10.dirname(fileURLToPath2(import.meta.url));
390
842
  while (true) {
391
- const pkgPath = path5.join(dir, "package.json");
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 = path5.dirname(dir);
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 readdir(targetDir);
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 readdir(dir, { withFileTypes: true });
872
+ const entries = await readdir3(dir, { withFileTypes: true });
421
873
  const files = await Promise.all(
422
874
  entries.map((entry) => {
423
- const fullPath = path6.join(dir, entry.name);
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 readFile4(filePath, "utf8");
885
+ const content = await readFile7(filePath, "utf8");
434
886
  if (content.includes("{{projectName}}")) {
435
- await writeFile4(filePath, applyPlaceholders(content, projectName));
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(path6.join(targetDir, "gitignore"), path6.join(targetDir, ".gitignore"));
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 writeFile4(path6.join(targetDir, ".npmrc"), `registry=${registryUrl}
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 path7 from "path";
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 = path7.join(logDir, SWEEP_MARKER_FILE);
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 = path7.join(logDir, file);
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 = path7.join(logDir, buildLogFileName(commandName));
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 mkdir2, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
580
- import path8 from "path";
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 FETCH_TIMEOUT_MS2 = 5e3;
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(FETCH_TIMEOUT_MS2) });
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 readFile5(path8.join(templateDir, adapter.manifestFileName), "utf8"),
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 readFile5(path8.join(templateDir, adapter.templateSourcePath(relativePath)), "utf8"),
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 readFile5(path8.join(targetDir, relativePath), "utf8"));
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: path8.join(targetDir, relativePath), content: newContent });
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 readFile5(path8.join(targetDir, relativePath), "utf8");
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 mkdir2(path8.dirname(write.targetPath), { recursive: true });
723
- await writeFile5(write.targetPath, write.content);
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 FETCH_TIMEOUT_MS3 = 3e3;
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(FETCH_TIMEOUT_MS3) });
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 = path9.join(process.cwd(), answers.projectName);
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.");