zudojs-cli 1.2.1 → 2.0.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.
Files changed (109) hide show
  1. package/README.md +35 -1
  2. package/dist/src/adapters/frontend/flutter.adapter.d.ts +9 -0
  3. package/dist/src/adapters/frontend/flutter.adapter.js +16 -1
  4. package/dist/src/bin/zudojs.js +16 -5
  5. package/dist/src/cliApplication/cliApplication.builtins.d.ts +4 -0
  6. package/dist/src/cliApplication/cliApplication.builtins.js +28 -9
  7. package/dist/src/cliApplication/cliApplication.core.d.ts +8 -0
  8. package/dist/src/cliApplication/cliApplication.core.js +45 -5
  9. package/dist/src/cliApplication/cliApplication.help.d.ts +17 -0
  10. package/dist/src/cliApplication/cliApplication.help.js +94 -0
  11. package/dist/src/cliApplication/cliApplication.logger.js +4 -4
  12. package/dist/src/cliCommand/cliCommand.validator.js +22 -0
  13. package/dist/src/cliConstant/cliConstant.value.d.ts +18 -12
  14. package/dist/src/cliConstant/cliConstant.value.js +25 -19
  15. package/dist/src/cliError/cliError.argument.js +2 -2
  16. package/dist/src/cliError/cliError.base.js +28 -5
  17. package/dist/src/cliError/cliError.command.js +5 -2
  18. package/dist/src/cliError/cliError.execution.js +3 -3
  19. package/dist/src/cliParser/cliParser.core.js +11 -5
  20. package/dist/src/cliParser/cliParser.helper.d.ts +11 -0
  21. package/dist/src/cliParser/cliParser.helper.js +19 -0
  22. package/dist/src/cliParser/cliParser.longOption.js +4 -4
  23. package/dist/src/cliParser/cliParser.shortOption.js +7 -4
  24. package/dist/src/cliVersion/cliVersion.update.js +5 -3
  25. package/dist/src/commands/add.command.js +17 -2
  26. package/dist/src/commands/commandError.helper.d.ts +15 -0
  27. package/dist/src/commands/commandError.helper.js +28 -0
  28. package/dist/src/commands/create.command.js +284 -46
  29. package/dist/src/commands/doctor.command.d.ts +8 -0
  30. package/dist/src/commands/doctor.command.js +38 -5
  31. package/dist/src/commands/generate.command.js +71 -32
  32. package/dist/src/constants/index.d.ts +40 -68
  33. package/dist/src/constants/index.js +73 -23
  34. package/dist/src/generators/frontend/frontendPipeline.js +16 -4
  35. package/dist/src/generators/integration/integrationGenerator.core.js +10 -6
  36. package/dist/src/generators/project/project.generator.js +5 -1
  37. package/dist/src/index.d.ts +4 -4
  38. package/dist/src/index.js +3 -3
  39. package/dist/src/manifest/index.d.ts +1 -0
  40. package/dist/src/manifest/index.js +1 -0
  41. package/dist/src/manifest/manifestFile.helper.d.ts +42 -0
  42. package/dist/src/manifest/manifestFile.helper.js +141 -0
  43. package/dist/src/manifest/manifestManager.core.d.ts +16 -0
  44. package/dist/src/manifest/manifestManager.core.js +65 -35
  45. package/dist/src/prompts/backend/api-style.prompt.js +2 -5
  46. package/dist/src/prompts/backend/backend-architecture.prompt.js +5 -22
  47. package/dist/src/prompts/backend/database.prompt.js +5 -23
  48. package/dist/src/prompts/backend/services.prompt.js +12 -10
  49. package/dist/src/prompts/cancel.prompt.d.ts +18 -0
  50. package/dist/src/prompts/cancel.prompt.js +26 -0
  51. package/dist/src/prompts/capabilities/capabilities.prompt.js +2 -5
  52. package/dist/src/prompts/frontend/framework.prompt.js +2 -5
  53. package/dist/src/prompts/frontend/frontend-architecture.prompt.js +2 -5
  54. package/dist/src/prompts/index.d.ts +1 -0
  55. package/dist/src/prompts/index.js +1 -0
  56. package/dist/src/prompts/project/confirmation.prompt.js +2 -5
  57. package/dist/src/prompts/project/project-name.prompt.js +6 -7
  58. package/dist/src/prompts/project/project-type.prompt.js +2 -5
  59. package/dist/src/prompts/workspace/package-manager.prompt.js +5 -12
  60. package/dist/src/registries/adapter/frontendAdapterRegistry.core.d.ts +6 -0
  61. package/dist/src/registries/adapter/frontendAdapterRegistry.core.js +12 -0
  62. package/dist/src/registries/adapter/packageManagerRegistry.core.d.ts +5 -0
  63. package/dist/src/registries/adapter/packageManagerRegistry.core.js +11 -0
  64. package/dist/src/registries/dependency/dependencyRegistry.core.d.ts +9 -0
  65. package/dist/src/registries/dependency/dependencyRegistry.core.js +16 -0
  66. package/dist/src/registries/generator/generatorRegistry.core.d.ts +8 -0
  67. package/dist/src/registries/generator/generatorRegistry.core.js +14 -0
  68. package/dist/src/resolvers/capability/capabilityResolver.core.d.ts +1 -1
  69. package/dist/src/resolvers/capability/capabilityResolver.core.js +0 -2
  70. package/dist/src/resolvers/layout/projectLayout.core.js +1 -6
  71. package/dist/src/resolvers/project.resolver.d.ts +9 -3
  72. package/dist/src/resolvers/project.resolver.js +11 -7
  73. package/dist/src/rollback/index.d.ts +1 -1
  74. package/dist/src/rollback/index.js +1 -1
  75. package/dist/src/rollback/rollbackManager.core.d.ts +19 -1
  76. package/dist/src/rollback/rollbackManager.core.js +21 -2
  77. package/dist/src/runners/package-manager/index.d.ts +1 -1
  78. package/dist/src/runners/package-manager/index.js +1 -1
  79. package/dist/src/runners/package-manager/packageManagerRunner.core.d.ts +13 -1
  80. package/dist/src/runners/package-manager/packageManagerRunner.core.js +19 -1
  81. package/dist/src/runners/process/processRunner.core.d.ts +11 -0
  82. package/dist/src/runners/process/processRunner.core.js +41 -6
  83. package/dist/src/runners/task/index.d.ts +1 -1
  84. package/dist/src/runners/task/taskRunner.core.d.ts +22 -1
  85. package/dist/src/runners/task/taskRunner.core.js +35 -6
  86. package/dist/src/scaffolders/framework.scaffolder.js +3 -0
  87. package/dist/src/scaffolders/scaffolder.helper.d.ts +10 -0
  88. package/dist/src/scaffolders/scaffolder.helper.js +25 -2
  89. package/dist/src/templates/microservice/index.d.ts +1 -1
  90. package/dist/src/templates/microservice/index.js +1 -1
  91. package/dist/src/templates/microservice/microservice.template.d.ts +8 -4
  92. package/dist/src/templates/microservice/microservice.template.js +30 -19
  93. package/dist/src/templates/modular-monolith/modularMonolith.template.js +27 -8
  94. package/dist/src/templates/monolith/monolith.template.js +11 -17
  95. package/dist/src/templates/shared/capability.template.d.ts +25 -0
  96. package/dist/src/templates/shared/capability.template.js +46 -0
  97. package/dist/src/templates/shared/index.d.ts +1 -0
  98. package/dist/src/templates/shared/index.js +1 -0
  99. package/dist/src/utils/utils.exec.d.ts +22 -0
  100. package/dist/src/utils/utils.exec.js +51 -4
  101. package/dist/src/utils/utils.fileSystem.js +46 -2
  102. package/dist/src/utils/utils.name.d.ts +9 -0
  103. package/dist/src/utils/utils.name.js +29 -3
  104. package/dist/src/validators/environment/environmentValidator.core.d.ts +11 -1
  105. package/dist/src/validators/environment/environmentValidator.core.js +40 -73
  106. package/dist/src/validators/project/index.d.ts +1 -1
  107. package/dist/src/validators/project/projectValidator.core.d.ts +11 -1
  108. package/dist/src/validators/project/projectValidator.core.js +20 -4
  109. package/package.json +1 -1
@@ -12,8 +12,14 @@ export declare class FrontendAdapterRegistry {
12
12
  constructor();
13
13
  /**
14
14
  * Registers a new frontend adapter.
15
+ *
16
+ * A duplicate name used to overwrite the adapter already registered,
17
+ * silently, so the winner depended on registration order. Use
18
+ * {@link replace} to do that on purpose.
15
19
  */
16
20
  register(adapter: FrontendAdapter): void;
21
+ /** Registers an adapter, replacing any adapter of the same name. */
22
+ replace(adapter: FrontendAdapter): void;
17
23
  /**
18
24
  * Gets an adapter by name.
19
25
  */
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * @module registries/adapter
5
5
  */
6
+ import { CLIValidationError } from "../../errors/index.js";
6
7
  import { ReactAdapter } from "../../adapters/frontend/react.adapter.js";
7
8
  import { NextAdapter } from "../../adapters/frontend/next.adapter.js";
8
9
  import { VueAdapter } from "../../adapters/frontend/vue.adapter.js";
@@ -34,8 +35,19 @@ export class FrontendAdapterRegistry {
34
35
  }
35
36
  /**
36
37
  * Registers a new frontend adapter.
38
+ *
39
+ * A duplicate name used to overwrite the adapter already registered,
40
+ * silently, so the winner depended on registration order. Use
41
+ * {@link replace} to do that on purpose.
37
42
  */
38
43
  register(adapter) {
44
+ if (this.adapters.has(adapter.name)) {
45
+ throw new CLIValidationError(`A frontend adapter named "${adapter.name}" is already registered. Use replace() to override it.`);
46
+ }
47
+ this.adapters.set(adapter.name, adapter);
48
+ }
49
+ /** Registers an adapter, replacing any adapter of the same name. */
50
+ replace(adapter) {
39
51
  this.adapters.set(adapter.name, adapter);
40
52
  }
41
53
  /**
@@ -12,8 +12,13 @@ export declare class PackageManagerRegistry {
12
12
  constructor();
13
13
  /**
14
14
  * Registers a new package manager adapter.
15
+ *
16
+ * Registering the same name twice used to overwrite the first adapter
17
+ * without a word; {@link replace} is the explicit way to do that.
15
18
  */
16
19
  register(adapter: PackageManager): void;
20
+ /** Registers an adapter, replacing any adapter of the same name. */
21
+ replace(adapter: PackageManager): void;
17
22
  /**
18
23
  * Gets an adapter by name.
19
24
  */
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * @module registries/adapter/packageManager
5
5
  */
6
+ import { CLIValidationError } from "../../errors/index.js";
6
7
  import { PnpmAdapter } from "../../adapters/package-managers/pnpm.adapter.js";
7
8
  import { NpmAdapter } from "../../adapters/package-managers/npm.adapter.js";
8
9
  import { YarnAdapter } from "../../adapters/package-managers/yarn.adapter.js";
@@ -20,8 +21,18 @@ export class PackageManagerRegistry {
20
21
  }
21
22
  /**
22
23
  * Registers a new package manager adapter.
24
+ *
25
+ * Registering the same name twice used to overwrite the first adapter
26
+ * without a word; {@link replace} is the explicit way to do that.
23
27
  */
24
28
  register(adapter) {
29
+ if (this.adapters.has(adapter.name)) {
30
+ throw new CLIValidationError(`A package manager adapter named "${adapter.name}" is already registered. Use replace() to override it.`);
31
+ }
32
+ this.adapters.set(adapter.name, adapter);
33
+ }
34
+ /** Registers an adapter, replacing any adapter of the same name. */
35
+ replace(adapter) {
25
36
  this.adapters.set(adapter.name, adapter);
26
37
  }
27
38
  /**
@@ -11,7 +11,16 @@ export interface DependencyRecord {
11
11
  }
12
12
  export declare class DependencyRegistry {
13
13
  private readonly dependencies;
14
+ /**
15
+ * Records a dependency.
16
+ *
17
+ * A second record for the same package used to overwrite the first one
18
+ * silently, which hid version disagreements between two callers. Use
19
+ * {@link replace} when overwriting is the intent.
20
+ */
14
21
  add(record: DependencyRecord): void;
22
+ /** Records a dependency, replacing any record of the same name. */
23
+ replace(record: DependencyRecord): void;
15
24
  addMany(records: readonly DependencyRecord[]): void;
16
25
  get(name: string): DependencyRecord | undefined;
17
26
  getAll(): readonly DependencyRecord[];
@@ -3,9 +3,25 @@
3
3
  *
4
4
  * Registry for tracking and managing project dependencies.
5
5
  */
6
+ import { CLIValidationError } from "../../errors/index.js";
6
7
  export class DependencyRegistry {
7
8
  dependencies = new Map();
9
+ /**
10
+ * Records a dependency.
11
+ *
12
+ * A second record for the same package used to overwrite the first one
13
+ * silently, which hid version disagreements between two callers. Use
14
+ * {@link replace} when overwriting is the intent.
15
+ */
8
16
  add(record) {
17
+ const existing = this.dependencies.get(record.name);
18
+ if (existing !== undefined) {
19
+ throw new CLIValidationError(`Dependency "${record.name}" is already registered as ${existing.version} (${existing.source}). Use replace() to override it.`);
20
+ }
21
+ this.dependencies.set(record.name, record);
22
+ }
23
+ /** Records a dependency, replacing any record of the same name. */
24
+ replace(record) {
9
25
  this.dependencies.set(record.name, record);
10
26
  }
11
27
  addMany(records) {
@@ -11,7 +11,15 @@ export interface GeneratorRegistryEntry {
11
11
  export declare class GeneratorRegistry {
12
12
  private readonly generators;
13
13
  constructor();
14
+ /**
15
+ * Registers a generator.
16
+ *
17
+ * A duplicate name used to replace the previous entry silently, so which
18
+ * generator ran depended on registration order.
19
+ */
14
20
  register(entry: GeneratorRegistryEntry): void;
21
+ /** Registers a generator, replacing any entry of the same name. */
22
+ replace(entry: GeneratorRegistryEntry): void;
15
23
  get(name: string): GeneratorRegistryEntry | undefined;
16
24
  getByCapability(capability: string): GeneratorRegistryEntry[];
17
25
  getNames(): readonly string[];
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Registry for project generators with capability-based lookup.
5
5
  */
6
+ import { CLIValidationError } from "../../errors/index.js";
6
7
  import { BackendGenerator } from "../../generators/backend/backend.generator.js";
7
8
  import { FrontendGenerator } from "../../generators/frontend/frontendGenerator.core.js";
8
9
  import { FullstackComposer } from "../../generators/fullstack/fullstackComposer.core.js";
@@ -49,7 +50,20 @@ export class GeneratorRegistry {
49
50
  capabilities: ["docker", "database", "microservice"],
50
51
  });
51
52
  }
53
+ /**
54
+ * Registers a generator.
55
+ *
56
+ * A duplicate name used to replace the previous entry silently, so which
57
+ * generator ran depended on registration order.
58
+ */
52
59
  register(entry) {
60
+ if (this.generators.has(entry.name)) {
61
+ throw new CLIValidationError(`A generator named "${entry.name}" is already registered. Use replace() to override it.`);
62
+ }
63
+ this.generators.set(entry.name, entry);
64
+ }
65
+ /** Registers a generator, replacing any entry of the same name. */
66
+ replace(entry) {
53
67
  this.generators.set(entry.name, entry);
54
68
  }
55
69
  get(name) {
@@ -9,8 +9,8 @@ export interface CapabilityDependency {
9
9
  }
10
10
  export interface CapabilityResolutionResult {
11
11
  readonly capabilities: readonly string[];
12
+ /** Framework packages the selected capabilities build on. */
12
13
  readonly dependencies: readonly string[];
13
- readonly conflicts: readonly string[];
14
14
  }
15
15
  export declare class CapabilityResolver {
16
16
  private readonly dependencyGraph;
@@ -37,7 +37,6 @@ export class CapabilityResolver {
37
37
  resolve(capabilities) {
38
38
  const resolved = new Set();
39
39
  const dependencies = new Set();
40
- const conflicts = [];
41
40
  for (const capability of capabilities) {
42
41
  if (resolved.has(capability))
43
42
  continue;
@@ -52,7 +51,6 @@ export class CapabilityResolver {
52
51
  return {
53
52
  capabilities: Array.from(resolved),
54
53
  dependencies: Array.from(dependencies),
55
- conflicts,
56
54
  };
57
55
  }
58
56
  getCapabilities() {
@@ -19,6 +19,7 @@
19
19
  *
20
20
  * @module resolvers/layout
21
21
  */
22
+ import { PACKAGE_MANAGERS } from "../../constants/index.js";
22
23
  import { existsSync, readFileSync, readdirSync } from "node:fs";
23
24
  import { join } from "node:path";
24
25
  import { SAFE_PATH_SEGMENT } from "../../utils/utils.name.js";
@@ -27,12 +28,6 @@ const PROJECT_TYPES = [
27
28
  "frontend",
28
29
  "fullstack",
29
30
  ];
30
- const PACKAGE_MANAGERS = [
31
- "pnpm",
32
- "npm",
33
- "yarn",
34
- "bun",
35
- ];
36
31
  function readJson(path) {
37
32
  try {
38
33
  const parsed = JSON.parse(readFileSync(path, "utf-8"));
@@ -1,7 +1,13 @@
1
1
  /**
2
- * Walks upward from `startDir` to the nearest directory that looks like a
3
- * project root: one holding a `.zudojs/manifest.json`, a legacy
4
- * `zudojs.config.ts`, or a `package.json`.
2
+ * Walks upward from `startDir` to the nearest directory that really is a
3
+ * Zudojs project — one `resolveProjectLayout` recognizes: a
4
+ * `.zudojs/manifest.json`, a legacy `zudojs.config.ts`/`.js`, or a `zudojs`
5
+ * block in `package.json`.
6
+ *
7
+ * A bare `package.json` used to be accepted too, which made this a
8
+ * "any JavaScript project" detector rather than a Zudojs one: run from a
9
+ * directory with no Zudojs project anywhere, `zudojs build` climbed to an
10
+ * unrelated ancestor `package.json` and ran its `scripts.build`.
5
11
  */
6
12
  export declare function findProjectRoot(startDir?: string): string | null;
7
13
  export declare function resolveProjectPath(cwd: string, name: string): string;
@@ -1,16 +1,20 @@
1
- import { existsSync } from "node:fs";
2
1
  import { dirname, join } from "node:path";
2
+ import { resolveProjectLayout } from "./layout/projectLayout.core.js";
3
3
  /**
4
- * Walks upward from `startDir` to the nearest directory that looks like a
5
- * project root: one holding a `.zudojs/manifest.json`, a legacy
6
- * `zudojs.config.ts`, or a `package.json`.
4
+ * Walks upward from `startDir` to the nearest directory that really is a
5
+ * Zudojs project — one `resolveProjectLayout` recognizes: a
6
+ * `.zudojs/manifest.json`, a legacy `zudojs.config.ts`/`.js`, or a `zudojs`
7
+ * block in `package.json`.
8
+ *
9
+ * A bare `package.json` used to be accepted too, which made this a
10
+ * "any JavaScript project" detector rather than a Zudojs one: run from a
11
+ * directory with no Zudojs project anywhere, `zudojs build` climbed to an
12
+ * unrelated ancestor `package.json` and ran its `scripts.build`.
7
13
  */
8
14
  export function findProjectRoot(startDir = process.cwd()) {
9
15
  let dir = startDir;
10
16
  while (true) {
11
- if (existsSync(join(dir, ".zudojs", "manifest.json")) ||
12
- existsSync(join(dir, "zudojs.config.ts")) ||
13
- existsSync(join(dir, "package.json"))) {
17
+ if (resolveProjectLayout(dir) !== null) {
14
18
  return dir;
15
19
  }
16
20
  const parent = dirname(dir);
@@ -3,5 +3,5 @@
3
3
  *
4
4
  * Rollback system barrel exports.
5
5
  */
6
- export { RollbackManager, type RollbackEntry } from "./rollbackManager.core.js";
6
+ export { RollbackManager, type RollbackEntry, type RollbackFailure, type RollbackResult, } from "./rollbackManager.core.js";
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -3,5 +3,5 @@
3
3
  *
4
4
  * Rollback system barrel exports.
5
5
  */
6
- export { RollbackManager } from "./rollbackManager.core.js";
6
+ export { RollbackManager, } from "./rollbackManager.core.js";
7
7
  //# sourceMappingURL=index.js.map
@@ -8,11 +8,29 @@ export interface RollbackEntry {
8
8
  readonly path: string;
9
9
  readonly timestamp: number;
10
10
  }
11
+ /** One entry that could not be removed, with the reason. */
12
+ export interface RollbackFailure {
13
+ readonly path: string;
14
+ readonly reason: string;
15
+ }
16
+ /** What a rollback actually managed to undo. */
17
+ export interface RollbackResult {
18
+ readonly removed: readonly string[];
19
+ readonly failures: readonly RollbackFailure[];
20
+ }
11
21
  export declare class RollbackManager {
12
22
  private readonly entries;
13
23
  trackFile(path: string): void;
14
24
  trackDirectory(path: string): void;
15
- rollback(): Promise<void>;
25
+ /**
26
+ * Removes every tracked path, newest first.
27
+ *
28
+ * Failures used to be swallowed and the entry list cleared regardless, so
29
+ * a half-created project stayed on disk with nothing left to retry and
30
+ * nothing said about it. Entries that could not be removed are kept, and
31
+ * the caller is handed the paths so it can name them.
32
+ */
33
+ rollback(): Promise<RollbackResult>;
16
34
  get entriesCount(): number;
17
35
  clear(): void;
18
36
  }
@@ -21,21 +21,40 @@ export class RollbackManager {
21
21
  timestamp: Date.now(),
22
22
  });
23
23
  }
24
+ /**
25
+ * Removes every tracked path, newest first.
26
+ *
27
+ * Failures used to be swallowed and the entry list cleared regardless, so
28
+ * a half-created project stayed on disk with nothing left to retry and
29
+ * nothing said about it. Entries that could not be removed are kept, and
30
+ * the caller is handed the paths so it can name them.
31
+ */
24
32
  async rollback() {
33
+ const removed = [];
34
+ const failures = [];
35
+ const remaining = [];
25
36
  for (const entry of [...this.entries].reverse()) {
26
37
  try {
27
38
  if (entry.type === "file" && existsSync(entry.path)) {
28
39
  await rm(entry.path);
40
+ removed.push(entry.path);
29
41
  }
30
42
  else if (entry.type === "directory" && existsSync(entry.path)) {
31
43
  await rm(entry.path, { recursive: true, force: true });
44
+ removed.push(entry.path);
32
45
  }
33
46
  }
34
- catch {
35
- // Best-effort rollback
47
+ catch (error) {
48
+ failures.push({
49
+ path: entry.path,
50
+ reason: error instanceof Error ? error.message : String(error),
51
+ });
52
+ remaining.push(entry);
36
53
  }
37
54
  }
38
55
  this.entries.length = 0;
56
+ this.entries.push(...remaining);
57
+ return { removed, failures };
39
58
  }
40
59
  get entriesCount() {
41
60
  return this.entries.length;
@@ -3,5 +3,5 @@
3
3
  *
4
4
  * Runners for package manager operations.
5
5
  */
6
- export { PackageManagerRunner, type PackageManagerRunOptions, } from "./packageManagerRunner.core.js";
6
+ export { PackageManagerRunner, assertPackageManager, type PackageManagerRunOptions, } from "./packageManagerRunner.core.js";
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -3,5 +3,5 @@
3
3
  *
4
4
  * Runners for package manager operations.
5
5
  */
6
- export { PackageManagerRunner, } from "./packageManagerRunner.core.js";
6
+ export { PackageManagerRunner, assertPackageManager, } from "./packageManagerRunner.core.js";
7
7
  //# sourceMappingURL=index.js.map
@@ -3,13 +3,25 @@
3
3
  *
4
4
  * Runner for package manager operations (install, add, remove, run).
5
5
  */
6
+ import type { PackageManager } from "../../types/index.js";
6
7
  export interface PackageManagerRunOptions {
7
8
  readonly cwd: string;
8
9
  readonly args: readonly string[];
9
10
  }
11
+ /** The package managers this runner will put in the executable position. */
12
+ /**
13
+ * Narrows an untrusted value to a supported package manager.
14
+ *
15
+ * The constructor argument ends up as the executable name of a spawned
16
+ * process, so it is checked at runtime as well as at compile time: this is
17
+ * public API and a JavaScript caller gets no type checking at all.
18
+ *
19
+ * @throws {CLIValidationError} If the value is not a known package manager.
20
+ */
21
+ export declare function assertPackageManager(value: string): PackageManager;
10
22
  export declare class PackageManagerRunner {
11
23
  private readonly manager;
12
- constructor(manager: string);
24
+ constructor(manager: PackageManager);
13
25
  install(cwd: string): Promise<void>;
14
26
  add(packages: readonly string[], options: PackageManagerRunOptions): Promise<void>;
15
27
  addDev(packages: readonly string[], options: PackageManagerRunOptions): Promise<void>;
@@ -3,11 +3,29 @@
3
3
  *
4
4
  * Runner for package manager operations (install, add, remove, run).
5
5
  */
6
+ import { PACKAGE_MANAGERS } from "../../constants/index.js";
7
+ import { CLIValidationError } from "../../errors/index.js";
6
8
  import { execCommand } from "../../utils/utils.exec.js";
9
+ /** The package managers this runner will put in the executable position. */
10
+ /**
11
+ * Narrows an untrusted value to a supported package manager.
12
+ *
13
+ * The constructor argument ends up as the executable name of a spawned
14
+ * process, so it is checked at runtime as well as at compile time: this is
15
+ * public API and a JavaScript caller gets no type checking at all.
16
+ *
17
+ * @throws {CLIValidationError} If the value is not a known package manager.
18
+ */
19
+ export function assertPackageManager(value) {
20
+ if (!PACKAGE_MANAGERS.includes(value)) {
21
+ throw new CLIValidationError(`Unsupported package manager: "${value}". Expected one of ${PACKAGE_MANAGERS.join(", ")}.`);
22
+ }
23
+ return value;
24
+ }
7
25
  export class PackageManagerRunner {
8
26
  manager;
9
27
  constructor(manager) {
10
- this.manager = manager;
28
+ this.manager = assertPackageManager(manager);
11
29
  }
12
30
  async install(cwd) {
13
31
  const args = this.manager === "pnpm"
@@ -18,6 +18,17 @@ export declare class ProcessRunner {
18
18
  stderr: string;
19
19
  exitCode: number;
20
20
  }>;
21
+ /**
22
+ * Starts a process and resolves once it has actually spawned.
23
+ *
24
+ * A raw `spawn` with no `'error'` listener turns an ENOENT into an
25
+ * unhandled `'error'` event, which terminates the CLI itself — and the
26
+ * old implementation had already returned `{ pid: 0 }`, so no caller
27
+ * could catch it. The failure is now a rejection, and the command goes
28
+ * through {@link resolveSpawnTarget} so Windows `.cmd` shims work.
29
+ *
30
+ * @throws {Error} If the process cannot be started.
31
+ */
21
32
  runBackground(command: string, args: readonly string[], options: ProcessOptions): Promise<{
22
33
  pid: number;
23
34
  }>;
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Runner for executing system processes.
5
5
  */
6
- import { execCommand } from "../../utils/utils.exec.js";
6
+ import { execCommand, resolveChildEnv, resolveSpawnTarget, } from "../../utils/utils.exec.js";
7
7
  export class ProcessRunner {
8
8
  async run(command, args, options) {
9
9
  try {
@@ -29,14 +29,49 @@ export class ProcessRunner {
29
29
  return { stdout, stderr, exitCode };
30
30
  }
31
31
  }
32
+ /**
33
+ * Starts a process and resolves once it has actually spawned.
34
+ *
35
+ * A raw `spawn` with no `'error'` listener turns an ENOENT into an
36
+ * unhandled `'error'` event, which terminates the CLI itself — and the
37
+ * old implementation had already returned `{ pid: 0 }`, so no caller
38
+ * could catch it. The failure is now a rejection, and the command goes
39
+ * through {@link resolveSpawnTarget} so Windows `.cmd` shims work.
40
+ *
41
+ * @throws {Error} If the process cannot be started.
42
+ */
32
43
  async runBackground(command, args, options) {
33
44
  const { spawn } = await import("node:child_process");
34
- const child = spawn(command, Array.from(args), {
35
- cwd: options.cwd,
36
- env: { ...process.env, ...options.env },
37
- stdio: options.stdio ?? "ignore",
45
+ const target = resolveSpawnTarget(command, Array.from(args));
46
+ const env = resolveChildEnv(target.shell, {
47
+ ...process.env,
48
+ ...options.env,
49
+ });
50
+ return await new Promise((resolve, reject) => {
51
+ const child = spawn(target.file, target.args, {
52
+ cwd: options.cwd,
53
+ env,
54
+ stdio: options.stdio ?? "ignore",
55
+ shell: target.shell,
56
+ });
57
+ let settled = false;
58
+ child.on("error", (error) => {
59
+ if (settled)
60
+ return;
61
+ settled = true;
62
+ if (error.code === "ENOENT") {
63
+ reject(new Error(`Command "${command}" was not found. Install it and make sure it is on your PATH.`));
64
+ return;
65
+ }
66
+ reject(error);
67
+ });
68
+ child.on("spawn", () => {
69
+ if (settled)
70
+ return;
71
+ settled = true;
72
+ resolve({ pid: child.pid ?? 0 });
73
+ });
38
74
  });
39
- return { pid: child.pid ?? 0 };
40
75
  }
41
76
  }
42
77
  //# sourceMappingURL=processRunner.core.js.map
@@ -3,5 +3,5 @@
3
3
  *
4
4
  * Runners for executing development tasks.
5
5
  */
6
- export { TaskRunner, type TaskDefinition, type TaskResult, } from "./taskRunner.core.js";
6
+ export { TaskRunner, type TaskDefinition, type TaskResult, type TaskRunOptions, } from "./taskRunner.core.js";
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -8,16 +8,37 @@ export interface TaskDefinition {
8
8
  readonly command: string;
9
9
  readonly args: readonly string[];
10
10
  readonly cwd: string;
11
+ /**
12
+ * When true, a failure of this task cancels the other tasks of the same
13
+ * {@link TaskRunner.runParallel} batch. Defaults to false.
14
+ */
11
15
  readonly required?: boolean;
12
16
  }
13
17
  export interface TaskResult {
14
18
  readonly task: string;
15
19
  readonly success: boolean;
20
+ /** The child's exit code, or -1 when it never exited normally. */
16
21
  readonly exitCode: number;
22
+ /** The child's stdout. */
17
23
  readonly output: string;
24
+ /** The child's stderr, or the reason it could not be run. */
25
+ readonly stderr: string;
26
+ /** True when the task was cancelled because a required task failed. */
27
+ readonly cancelled: boolean;
28
+ }
29
+ export interface TaskRunOptions {
30
+ /** Aborting the signal terminates the task's child process. */
31
+ readonly signal?: AbortSignal;
18
32
  }
19
33
  export declare class TaskRunner {
20
- run(task: TaskDefinition): Promise<TaskResult>;
34
+ run(task: TaskDefinition, options?: TaskRunOptions): Promise<TaskResult>;
35
+ /**
36
+ * Runs every task concurrently.
37
+ *
38
+ * A task marked `required` that fails aborts the tasks still running:
39
+ * there is no point building the rest of a project once a prerequisite
40
+ * step has failed. Those tasks come back with `cancelled: true`.
41
+ */
21
42
  runParallel(tasks: readonly TaskDefinition[]): Promise<readonly TaskResult[]>;
22
43
  }
23
44
  //# sourceMappingURL=taskRunner.core.d.ts.map
@@ -4,29 +4,58 @@
4
4
  * Runner for executing development tasks (dev servers, builds, tests).
5
5
  */
6
6
  import { execCommand } from "../../utils/utils.exec.js";
7
+ /** Exit code reported for a child that never exited with a status. */
8
+ const NO_EXIT_CODE = -1;
9
+ function readString(source, key) {
10
+ const value = source?.[key];
11
+ return typeof value === "string" ? value : "";
12
+ }
7
13
  export class TaskRunner {
8
- async run(task) {
14
+ async run(task, options = {}) {
9
15
  try {
10
- await execCommand(task.command, Array.from(task.args), task.cwd);
16
+ const result = await execCommand(task.command, Array.from(task.args), task.cwd, { ...(options.signal ? { signal: options.signal } : {}) });
11
17
  return {
12
18
  task: task.name,
13
19
  success: true,
14
20
  exitCode: 0,
15
- output: "",
21
+ output: result.stdout,
22
+ stderr: result.stderr,
23
+ cancelled: false,
16
24
  };
17
25
  }
18
26
  catch (error) {
19
27
  const message = error instanceof Error ? error.message : String(error);
28
+ const cancelled = error instanceof Error &&
29
+ error.name === "AbortError" &&
30
+ options.signal?.aborted === true;
31
+ const code = error.code;
32
+ const stderr = readString(error, "stderr");
20
33
  return {
21
34
  task: task.name,
22
35
  success: false,
23
- exitCode: 1,
24
- output: message,
36
+ exitCode: typeof code === "number" ? code : NO_EXIT_CODE,
37
+ output: readString(error, "stdout"),
38
+ stderr: stderr !== "" ? stderr : message,
39
+ cancelled,
25
40
  };
26
41
  }
27
42
  }
43
+ /**
44
+ * Runs every task concurrently.
45
+ *
46
+ * A task marked `required` that fails aborts the tasks still running:
47
+ * there is no point building the rest of a project once a prerequisite
48
+ * step has failed. Those tasks come back with `cancelled: true`.
49
+ */
28
50
  async runParallel(tasks) {
29
- const results = await Promise.all(tasks.map((task) => this.run(task)));
51
+ const controller = new AbortController();
52
+ const results = await Promise.all(tasks.map(async (task) => {
53
+ const result = await this.run(task, { signal: controller.signal });
54
+ if (!result.success && task.required === true) {
55
+ controller.abort();
56
+ }
57
+ return result;
58
+ }));
30
59
  return results;
31
60
  }
32
61
  }
@@ -4,12 +4,15 @@
4
4
  * Invokes official framework scaffolders to create projects.
5
5
  */
6
6
  import { execCommand } from "../utils/utils.exec.js";
7
+ import { SCAFFOLD_TIMEOUT_MS } from "./scaffolder.helper.js";
7
8
  export class FrameworkScaffolder {
8
9
  async scaffold(targetPath) {
9
10
  try {
10
11
  await execCommand(this.command, this.args, targetPath, {
11
12
  // Suppress interactive prompts from create-* tools.
12
13
  env: { ...process.env, CI: "1" },
14
+ // Registry downloads outrun the default 2-minute probe timeout.
15
+ timeout: SCAFFOLD_TIMEOUT_MS,
13
16
  });
14
17
  return { success: true, path: targetPath };
15
18
  }