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
@@ -0,0 +1,141 @@
1
+ /**
2
+ * zudojs-cli — Manifest File Helpers
3
+ *
4
+ * Parsing, validation, atomic writes and locking for
5
+ * `.zudojs/manifest.json`. Kept apart from the manager so the manager stays
6
+ * a thin read/modify/write API over them.
7
+ */
8
+ import { open, rename, rm, stat, writeFile } from "node:fs/promises";
9
+ const LOCK_TIMEOUT_MS = 5_000;
10
+ const LOCK_STALE_MS = 30_000;
11
+ const LOCK_RETRY_MS = 20;
12
+ /**
13
+ * Parses manifest text, filling in the fields an older CLI (or a hand edit)
14
+ * may have left out.
15
+ *
16
+ * `capabilities` used to be read straight off an unvalidated cast, so a
17
+ * manifest without it threw a TypeError from inside `addCapability`.
18
+ */
19
+ export function parseManifest(content) {
20
+ let parsed;
21
+ try {
22
+ parsed = JSON.parse(content);
23
+ }
24
+ catch (error) {
25
+ return {
26
+ status: "invalid",
27
+ manifest: null,
28
+ reason: `not valid JSON (${error instanceof Error ? error.message : String(error)})`,
29
+ };
30
+ }
31
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
32
+ return {
33
+ status: "invalid",
34
+ manifest: null,
35
+ reason: "the top level is not a JSON object",
36
+ };
37
+ }
38
+ const raw = parsed;
39
+ if (raw.capabilities !== undefined && !Array.isArray(raw.capabilities)) {
40
+ return {
41
+ status: "invalid",
42
+ manifest: null,
43
+ reason: '"capabilities" is not an array',
44
+ };
45
+ }
46
+ const capabilities = Array.isArray(raw.capabilities)
47
+ ? raw.capabilities.filter((item) => typeof item === "string")
48
+ : [];
49
+ return {
50
+ status: "ok",
51
+ manifest: {
52
+ ...raw,
53
+ version: typeof raw.version === "string" ? raw.version : "",
54
+ architecture: typeof raw.architecture === "string" ? raw.architecture : "monolith",
55
+ capabilities,
56
+ generatedAt: typeof raw.generatedAt === "string" ? raw.generatedAt : "",
57
+ updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : "",
58
+ },
59
+ };
60
+ }
61
+ /**
62
+ * Writes the manifest through a sibling temp file and `rename`.
63
+ *
64
+ * Writing in place truncated the real file first, so an interrupted or
65
+ * failing write left an empty or half-written manifest behind.
66
+ */
67
+ export async function writeManifestFile(manifestPath, manifest) {
68
+ const tempPath = `${manifestPath}.${process.pid}.${Math.random()
69
+ .toString(36)
70
+ .slice(2)}.tmp`;
71
+ await writeFile(tempPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf-8");
72
+ try {
73
+ await rename(tempPath, manifestPath);
74
+ }
75
+ catch (error) {
76
+ await rm(tempPath, { force: true });
77
+ throw error;
78
+ }
79
+ }
80
+ const queues = new Map();
81
+ /**
82
+ * Serializes a read-modify-write of one manifest.
83
+ *
84
+ * Concurrent `zudojs add` runs lost each other's updates: each read the
85
+ * manifest, added its own capability and wrote the whole file back. The
86
+ * queue orders calls inside one process and the lock file orders them
87
+ * across processes.
88
+ */
89
+ export async function withManifestLock(manifestPath, operation) {
90
+ const previous = queues.get(manifestPath) ?? Promise.resolve();
91
+ const run = previous.then(async () => {
92
+ const release = await acquireLock(`${manifestPath}.lock`);
93
+ try {
94
+ return await operation();
95
+ }
96
+ finally {
97
+ await release();
98
+ }
99
+ });
100
+ queues.set(manifestPath, run.then(() => undefined, () => undefined));
101
+ return run;
102
+ }
103
+ async function acquireLock(lockPath) {
104
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
105
+ const noop = async () => { };
106
+ while (true) {
107
+ try {
108
+ const handle = await open(lockPath, "wx");
109
+ await handle.close();
110
+ return async () => {
111
+ await rm(lockPath, { force: true }).catch(() => undefined);
112
+ };
113
+ }
114
+ catch (error) {
115
+ if (error.code !== "EEXIST") {
116
+ // The lock itself cannot be created (read-only or missing
117
+ // directory). The write is still attempted; failing here would be
118
+ // worse than an unserialized write.
119
+ return noop;
120
+ }
121
+ if (await isStale(lockPath)) {
122
+ await rm(lockPath, { force: true }).catch(() => undefined);
123
+ continue;
124
+ }
125
+ if (Date.now() >= deadline) {
126
+ return noop;
127
+ }
128
+ await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS));
129
+ }
130
+ }
131
+ }
132
+ async function isStale(lockPath) {
133
+ try {
134
+ const stats = await stat(lockPath);
135
+ return Date.now() - stats.mtimeMs > LOCK_STALE_MS;
136
+ }
137
+ catch {
138
+ return false;
139
+ }
140
+ }
141
+ //# sourceMappingURL=manifestFile.helper.js.map
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Machine-managed project manifest for Zudojs projects.
5
5
  */
6
+ import { type ManifestReadResult } from "./manifestFile.helper.js";
6
7
  export interface ZudojsManifest {
7
8
  readonly version: string;
8
9
  readonly projectType?: string;
@@ -30,10 +31,25 @@ export interface ZudojsManifest {
30
31
  export declare class ManifestManager {
31
32
  private readonly manifestPath;
32
33
  constructor(cwd: string);
34
+ /** Absolute path of the manifest this manager owns. */
35
+ get path(): string;
33
36
  create(manifest: Omit<ZudojsManifest, "generatedAt" | "updatedAt">): Promise<void>;
37
+ /**
38
+ * Reads the manifest, keeping "there is none" and "there is one but it is
39
+ * corrupt" apart. A blanket catch used to report both as missing.
40
+ */
41
+ readResult(): Promise<ManifestReadResult>;
34
42
  read(): Promise<ZudojsManifest | null>;
43
+ /**
44
+ * Applies `updates` to the manifest on disk.
45
+ *
46
+ * Throws when there is no usable manifest: returning silently made
47
+ * `zudojs add` report success while nothing had been recorded.
48
+ */
35
49
  update(updates: Partial<ZudojsManifest>): Promise<void>;
50
+ /** Adds `capability` to the manifest if it is not already recorded. */
36
51
  addCapability(capability: string): Promise<void>;
52
+ private require;
37
53
  private write;
38
54
  }
39
55
  //# sourceMappingURL=manifestManager.core.d.ts.map
@@ -3,61 +3,91 @@
3
3
  *
4
4
  * Machine-managed project manifest for Zudojs projects.
5
5
  */
6
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
7
- import { join } from "node:path";
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { mkdir } from "node:fs/promises";
8
+ import { dirname, join } from "node:path";
9
+ import { CLIGenerationError } from "../errors/index.js";
10
+ import { parseManifest, withManifestLock, writeManifestFile, } from "./manifestFile.helper.js";
8
11
  export class ManifestManager {
9
12
  manifestPath;
10
13
  constructor(cwd) {
11
14
  this.manifestPath = join(cwd, ".zudojs", "manifest.json");
12
15
  }
16
+ /** Absolute path of the manifest this manager owns. */
17
+ get path() {
18
+ return this.manifestPath;
19
+ }
13
20
  async create(manifest) {
14
21
  const now = new Date().toISOString();
15
- const fullManifest = {
16
- ...manifest,
17
- generatedAt: now,
18
- updatedAt: now,
19
- };
20
- await this.write(fullManifest);
22
+ await withManifestLock(this.manifestPath, async () => {
23
+ await this.write({ ...manifest, generatedAt: now, updatedAt: now });
24
+ });
21
25
  }
22
- async read() {
26
+ /**
27
+ * Reads the manifest, keeping "there is none" and "there is one but it is
28
+ * corrupt" apart. A blanket catch used to report both as missing.
29
+ */
30
+ async readResult() {
23
31
  if (!existsSync(this.manifestPath)) {
24
- return null;
32
+ return { status: "missing", manifest: null };
25
33
  }
34
+ let content;
26
35
  try {
27
- const content = readFileSync(this.manifestPath, "utf-8");
28
- return JSON.parse(content);
36
+ content = readFileSync(this.manifestPath, "utf-8");
29
37
  }
30
- catch {
31
- return null;
38
+ catch (error) {
39
+ return {
40
+ status: "invalid",
41
+ manifest: null,
42
+ reason: `could not be read (${error instanceof Error ? error.message : String(error)})`,
43
+ };
32
44
  }
45
+ return parseManifest(content);
46
+ }
47
+ async read() {
48
+ return (await this.readResult()).manifest;
33
49
  }
50
+ /**
51
+ * Applies `updates` to the manifest on disk.
52
+ *
53
+ * Throws when there is no usable manifest: returning silently made
54
+ * `zudojs add` report success while nothing had been recorded.
55
+ */
34
56
  async update(updates) {
35
- const existing = await this.read();
36
- if (!existing) {
37
- return;
38
- }
39
- const updated = {
40
- ...existing,
41
- ...updates,
42
- updatedAt: new Date().toISOString(),
43
- };
44
- await this.write(updated);
57
+ await withManifestLock(this.manifestPath, async () => {
58
+ const existing = this.require(await this.readResult());
59
+ await this.write({
60
+ ...existing,
61
+ ...updates,
62
+ updatedAt: new Date().toISOString(),
63
+ });
64
+ });
45
65
  }
66
+ /** Adds `capability` to the manifest if it is not already recorded. */
46
67
  async addCapability(capability) {
47
- const existing = await this.read();
48
- if (!existing) {
49
- return;
68
+ await withManifestLock(this.manifestPath, async () => {
69
+ const existing = this.require(await this.readResult());
70
+ if (existing.capabilities.includes(capability)) {
71
+ return;
72
+ }
73
+ await this.write({
74
+ ...existing,
75
+ capabilities: [...existing.capabilities, capability],
76
+ updatedAt: new Date().toISOString(),
77
+ });
78
+ });
79
+ }
80
+ require(result) {
81
+ if (result.status === "ok" && result.manifest) {
82
+ return result.manifest;
50
83
  }
51
- const capabilities = existing.capabilities.includes(capability)
52
- ? existing.capabilities
53
- : [...existing.capabilities, capability];
54
- await this.update({ capabilities });
84
+ throw new CLIGenerationError(result.status === "missing"
85
+ ? `No project manifest at ${this.manifestPath}.`
86
+ : `The project manifest at ${this.manifestPath} is unusable: ${result.reason ?? "unknown reason"}.`);
55
87
  }
56
88
  async write(manifest) {
57
- const dir = join(this.manifestPath, "..");
58
- const { mkdir } = await import("node:fs/promises");
59
- await mkdir(dir, { recursive: true });
60
- writeFileSync(this.manifestPath, JSON.stringify(manifest, null, 2));
89
+ await mkdir(dirname(this.manifestPath), { recursive: true });
90
+ await writeManifestFile(this.manifestPath, manifest);
61
91
  }
62
92
  }
63
93
  //# sourceMappingURL=manifestManager.core.js.map
@@ -4,6 +4,7 @@
4
4
  * Prompts for API style selection.
5
5
  */
6
6
  import * as p from "@clack/prompts";
7
+ import { cancelled } from "../cancel.prompt.js";
7
8
  export async function promptApiStyle(overrides) {
8
9
  const value = overrides ??
9
10
  (await p.select({
@@ -27,10 +28,6 @@ export async function promptApiStyle(overrides) {
27
28
  ],
28
29
  initialValue: "rest",
29
30
  }));
30
- if (p.isCancel(value)) {
31
- p.cancel("Operation cancelled.");
32
- process.exit(0);
33
- }
34
- return value;
31
+ return cancelled(value);
35
32
  }
36
33
  //# sourceMappingURL=api-style.prompt.js.map
@@ -4,32 +4,15 @@
4
4
  * Prompts for backend architecture selection.
5
5
  */
6
6
  import * as p from "@clack/prompts";
7
+ import { cancelled } from "../cancel.prompt.js";
8
+ import { ARCHITECTURE_CHOICES, DEFAULT_ARCHITECTURE, } from "../../constants/index.js";
7
9
  export async function promptBackendArchitecture(overrides) {
8
10
  const value = overrides ??
9
11
  (await p.select({
10
12
  message: "Select backend architecture",
11
- options: [
12
- {
13
- value: "monolith",
14
- label: "Monolith",
15
- hint: "Single application",
16
- },
17
- {
18
- value: "modular-monolith",
19
- label: "Modular Monolith",
20
- hint: "Modular single application",
21
- },
22
- {
23
- value: "microservice",
24
- label: "Microservices",
25
- hint: "Independent services",
26
- },
27
- ],
13
+ options: ARCHITECTURE_CHOICES.map((choice) => ({ ...choice })),
14
+ initialValue: DEFAULT_ARCHITECTURE,
28
15
  }));
29
- if (p.isCancel(value)) {
30
- p.cancel("Operation cancelled.");
31
- process.exit(0);
32
- }
33
- return value;
16
+ return cancelled(value);
34
17
  }
35
18
  //# sourceMappingURL=backend-architecture.prompt.js.map
@@ -4,33 +4,15 @@
4
4
  * Prompts for database selection.
5
5
  */
6
6
  import * as p from "@clack/prompts";
7
+ import { cancelled } from "../cancel.prompt.js";
8
+ import { DATABASE_CHOICES, DEFAULT_DATABASE } from "../../constants/index.js";
7
9
  export async function promptDatabase(overrides) {
8
10
  const value = overrides ??
9
11
  (await p.select({
10
12
  message: "Select database",
11
- options: [
12
- {
13
- value: "postgresql",
14
- label: "PostgreSQL",
15
- hint: "Recommended for production",
16
- },
17
- {
18
- value: "mysql",
19
- label: "MySQL",
20
- hint: "Widely used relational database",
21
- },
22
- {
23
- value: "sqlite",
24
- label: "SQLite",
25
- hint: "Lightweight, file-based",
26
- },
27
- ],
28
- initialValue: "postgresql",
13
+ options: DATABASE_CHOICES.map((choice) => ({ ...choice })),
14
+ initialValue: DEFAULT_DATABASE,
29
15
  }));
30
- if (p.isCancel(value)) {
31
- p.cancel("Operation cancelled.");
32
- process.exit(0);
33
- }
34
- return value;
16
+ return cancelled(value);
35
17
  }
36
18
  //# sourceMappingURL=database.prompt.js.map
@@ -4,6 +4,7 @@
4
4
  * Prompts for microservice service names (comma-separated).
5
5
  */
6
6
  import * as p from "@clack/prompts";
7
+ import { cancelled } from "../cancel.prompt.js";
7
8
  const SERVICE_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
8
9
  function parseServices(value) {
9
10
  return value
@@ -16,13 +17,18 @@ export async function promptServices(overrides) {
16
17
  return [...overrides];
17
18
  }
18
19
  const value = await p.text({
19
- message: "Service names (comma-separated)",
20
- placeholder: "identity,billing,notification",
20
+ message: "Service names (comma-separated, leave blank for none)",
21
+ placeholder: "leave blank, or e.g. billing,search",
22
+ /**
23
+ * Blank is a valid answer and yields a gateway-only project.
24
+ *
25
+ * This used to demand at least one name, and an empty list elsewhere was
26
+ * substituted with four example services, so every project arrived with
27
+ * domains nobody had asked for. Services are created when they are named,
28
+ * here or later with `zudojs generate service <name>`.
29
+ */
21
30
  validate(input) {
22
31
  const services = parseServices(input ?? "");
23
- if (services.length === 0) {
24
- return "At least one service name is required.";
25
- }
26
32
  for (const service of services) {
27
33
  if (!SERVICE_NAME_PATTERN.test(service)) {
28
34
  return `Invalid service name "${service}". Only alphanumeric characters, hyphens, and underscores are allowed.`;
@@ -30,10 +36,6 @@ export async function promptServices(overrides) {
30
36
  }
31
37
  },
32
38
  });
33
- if (p.isCancel(value)) {
34
- p.cancel("Operation cancelled.");
35
- process.exit(0);
36
- }
37
- return parseServices(value);
39
+ return parseServices(cancelled(value));
38
40
  }
39
41
  //# sourceMappingURL=services.prompt.js.map
@@ -0,0 +1,18 @@
1
+ /**
2
+ * zudojs-cli — Prompt Cancellation
3
+ *
4
+ * The single place where an aborted prompt ends the process.
5
+ */
6
+ /**
7
+ * Returns a prompt answer, or ends the process when the prompt was cancelled.
8
+ *
9
+ * Every prompt used to exit `0` on Ctrl-C, so `zudojs create my-api && cd
10
+ * my-api` ran the `cd` against a directory that was never created, and in CI
11
+ * an abort was indistinguishable from a success. The exit status is now 130,
12
+ * the conventional "terminated by SIGINT" value.
13
+ *
14
+ * @param value - The value a `@clack/prompts` call resolved with.
15
+ * @returns The answer, once it is known not to be the cancel symbol.
16
+ */
17
+ export declare function cancelled<T>(value: T | symbol): T;
18
+ //# sourceMappingURL=cancel.prompt.d.ts.map
@@ -0,0 +1,26 @@
1
+ /**
2
+ * zudojs-cli — Prompt Cancellation
3
+ *
4
+ * The single place where an aborted prompt ends the process.
5
+ */
6
+ import * as p from "@clack/prompts";
7
+ import { CLI_EXIT_CODES } from "../cliConstant/cliConstant.value.js";
8
+ /**
9
+ * Returns a prompt answer, or ends the process when the prompt was cancelled.
10
+ *
11
+ * Every prompt used to exit `0` on Ctrl-C, so `zudojs create my-api && cd
12
+ * my-api` ran the `cd` against a directory that was never created, and in CI
13
+ * an abort was indistinguishable from a success. The exit status is now 130,
14
+ * the conventional "terminated by SIGINT" value.
15
+ *
16
+ * @param value - The value a `@clack/prompts` call resolved with.
17
+ * @returns The answer, once it is known not to be the cancel symbol.
18
+ */
19
+ export function cancelled(value) {
20
+ if (p.isCancel(value)) {
21
+ p.cancel("Operation cancelled.");
22
+ process.exit(CLI_EXIT_CODES.INTERRUPTED);
23
+ }
24
+ return value;
25
+ }
26
+ //# sourceMappingURL=cancel.prompt.js.map
@@ -4,6 +4,7 @@
4
4
  * Prompts for selecting Zudojs capabilities.
5
5
  */
6
6
  import * as p from "@clack/prompts";
7
+ import { cancelled } from "../cancel.prompt.js";
7
8
  const CAPABILITY_OPTIONS = [
8
9
  {
9
10
  value: "cqrs",
@@ -37,10 +38,6 @@ export async function promptCapabilities(selected = []) {
37
38
  required: false,
38
39
  initialValues: Array.from(selected),
39
40
  });
40
- if (p.isCancel(value)) {
41
- p.cancel("Operation cancelled.");
42
- process.exit(0);
43
- }
44
- return value;
41
+ return cancelled(value);
45
42
  }
46
43
  //# sourceMappingURL=capabilities.prompt.js.map
@@ -4,6 +4,7 @@
4
4
  * Prompts for frontend framework selection.
5
5
  */
6
6
  import * as p from "@clack/prompts";
7
+ import { cancelled } from "../cancel.prompt.js";
7
8
  const FRONTEND_OPTIONS = [
8
9
  { value: "react", label: "React", hint: "SPA with Vite" },
9
10
  { value: "next", label: "Next.js", hint: "Full React framework" },
@@ -33,10 +34,6 @@ export async function promptFramework(projectType, overrides) {
33
34
  hint: opt.hint,
34
35
  })),
35
36
  }));
36
- if (p.isCancel(value)) {
37
- p.cancel("Operation cancelled.");
38
- process.exit(0);
39
- }
40
- return value;
37
+ return cancelled(value);
41
38
  }
42
39
  //# sourceMappingURL=framework.prompt.js.map
@@ -4,6 +4,7 @@
4
4
  * Prompts for frontend project structure.
5
5
  */
6
6
  import * as p from "@clack/prompts";
7
+ import { cancelled } from "../cancel.prompt.js";
7
8
  export async function promptFrontendArchitecture(overrides) {
8
9
  const value = overrides ??
9
10
  (await p.select({
@@ -32,10 +33,6 @@ export async function promptFrontendArchitecture(overrides) {
32
33
  ],
33
34
  initialValue: "zudojs-standard",
34
35
  }));
35
- if (p.isCancel(value)) {
36
- p.cancel("Operation cancelled.");
37
- process.exit(0);
38
- }
39
- return value;
36
+ return cancelled(value);
40
37
  }
41
38
  //# sourceMappingURL=frontend-architecture.prompt.js.map
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * zudojs-cli — Prompts Index
3
3
  */
4
+ export { cancelled } from "./cancel.prompt.js";
4
5
  export { promptProjectName } from "./project/project-name.prompt.js";
5
6
  export { promptProjectType } from "./project/project-type.prompt.js";
6
7
  export { promptConfirmation } from "./project/confirmation.prompt.js";
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * zudojs-cli — Prompts Index
3
3
  */
4
+ export { cancelled } from "./cancel.prompt.js";
4
5
  export { promptProjectName } from "./project/project-name.prompt.js";
5
6
  export { promptProjectType } from "./project/project-type.prompt.js";
6
7
  export { promptConfirmation } from "./project/confirmation.prompt.js";
@@ -4,15 +4,12 @@
4
4
  * Prompts for confirmation before generation.
5
5
  */
6
6
  import * as p from "@clack/prompts";
7
+ import { cancelled } from "../cancel.prompt.js";
7
8
  export async function promptConfirmation(message, initialValue = true) {
8
9
  const value = await p.confirm({
9
10
  message,
10
11
  initialValue,
11
12
  });
12
- if (p.isCancel(value)) {
13
- p.cancel("Operation cancelled.");
14
- process.exit(0);
15
- }
16
- return value;
13
+ return cancelled(value);
17
14
  }
18
15
  //# sourceMappingURL=confirmation.prompt.js.map
@@ -4,6 +4,7 @@
4
4
  * Prompts for the project name.
5
5
  */
6
6
  import * as p from "@clack/prompts";
7
+ import { cancelled } from "../cancel.prompt.js";
7
8
  export async function promptProjectName(overrides) {
8
9
  const value = overrides ??
9
10
  (await p.text({
@@ -13,15 +14,13 @@ export async function promptProjectName(overrides) {
13
14
  if (!value || value.trim().length === 0) {
14
15
  return "Project name is required.";
15
16
  }
16
- if (!/^[a-zA-Z0-9_-]+$/.test(value)) {
17
- return "Only alphanumeric, hyphens, and underscores allowed.";
17
+ // A leading "-" made a directory that `cd -dash` cannot enter and
18
+ // `rm -rf -dash` cannot remove, so the first character is alphanumeric.
19
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(value)) {
20
+ return "Must start with a letter or digit; only alphanumeric characters, hyphens, and underscores are allowed.";
18
21
  }
19
22
  },
20
23
  }));
21
- if (p.isCancel(value)) {
22
- p.cancel("Operation cancelled.");
23
- process.exit(0);
24
- }
25
- return value;
24
+ return cancelled(value);
26
25
  }
27
26
  //# sourceMappingURL=project-name.prompt.js.map
@@ -4,6 +4,7 @@
4
4
  * Prompts for the project type.
5
5
  */
6
6
  import * as p from "@clack/prompts";
7
+ import { cancelled } from "../cancel.prompt.js";
7
8
  export async function promptProjectType(overrides) {
8
9
  const value = overrides ??
9
10
  (await p.select({
@@ -26,10 +27,6 @@ export async function promptProjectType(overrides) {
26
27
  },
27
28
  ],
28
29
  }));
29
- if (p.isCancel(value)) {
30
- p.cancel("Operation cancelled.");
31
- process.exit(0);
32
- }
33
- return value;
30
+ return cancelled(value);
34
31
  }
35
32
  //# sourceMappingURL=project-type.prompt.js.map
@@ -4,22 +4,15 @@
4
4
  * Prompts for package manager selection.
5
5
  */
6
6
  import * as p from "@clack/prompts";
7
+ import { cancelled } from "../cancel.prompt.js";
8
+ import { DEFAULT_PACKAGE_MANAGER, PACKAGE_MANAGER_CHOICES, } from "../../constants/index.js";
7
9
  export async function promptPackageManager(overrides) {
8
10
  const value = overrides ??
9
11
  (await p.select({
10
12
  message: "Select package manager",
11
- options: [
12
- { value: "pnpm", label: "pnpm" },
13
- { value: "npm", label: "npm" },
14
- { value: "yarn", label: "Yarn" },
15
- { value: "bun", label: "Bun" },
16
- ],
17
- initialValue: "pnpm",
13
+ options: PACKAGE_MANAGER_CHOICES.map((choice) => ({ ...choice })),
14
+ initialValue: DEFAULT_PACKAGE_MANAGER,
18
15
  }));
19
- if (p.isCancel(value)) {
20
- p.cancel("Operation cancelled.");
21
- process.exit(0);
22
- }
23
- return value;
16
+ return cancelled(value);
24
17
  }
25
18
  //# sourceMappingURL=package-manager.prompt.js.map