@simple-auth-kit/cli 1.4.2 → 1.5.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 (133) hide show
  1. package/package.json +1 -1
  2. package/registry/combos/express-drizzle/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
  3. package/registry/combos/express-drizzle/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +23 -7
  4. package/registry/combos/{express-prisma/shared/src → express-drizzle/shared/src/middleware}/response-envelope.middleware.ts +15 -3
  5. package/registry/combos/express-drizzle/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +32 -9
  6. package/registry/combos/express-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
  7. package/registry/combos/express-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
  8. package/registry/combos/express-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +20 -6
  9. package/registry/combos/express-drizzle/shared/src/request-context.ts +1 -1
  10. package/registry/combos/express-drizzle/shared/src/routers/auth.router.ts +395 -0
  11. package/registry/combos/express-drizzle/variants/base/src/create-auth-app.ts +52 -19
  12. package/registry/combos/express-drizzle/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
  13. package/registry/combos/express-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +28 -10
  14. package/registry/combos/express-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +239 -48
  15. package/registry/combos/express-drizzle/variants/base/src/routers/admin.router.ts +497 -0
  16. package/registry/combos/express-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +480 -109
  17. package/registry/combos/express-drizzle/variants/workspaces/src/create-auth-app.ts +69 -23
  18. package/registry/combos/express-drizzle/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
  19. package/registry/combos/express-drizzle/variants/workspaces/src/openapi-workspace.ts +147 -28
  20. package/registry/combos/express-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +30 -11
  21. package/registry/combos/express-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +319 -70
  22. package/registry/combos/express-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +127 -30
  23. package/registry/combos/express-drizzle/variants/workspaces/src/routers/admin.router.ts +534 -0
  24. package/registry/combos/express-drizzle/variants/workspaces/src/routers/workspace.router.ts +194 -0
  25. package/registry/combos/express-drizzle/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +544 -122
  26. package/registry/combos/express-prisma/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
  27. package/registry/combos/express-prisma/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +19 -6
  28. package/registry/combos/{express-drizzle/shared/src → express-prisma/shared/src/middleware}/response-envelope.middleware.ts +15 -3
  29. package/registry/combos/express-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +27 -9
  30. package/registry/combos/express-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
  31. package/registry/combos/express-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
  32. package/registry/combos/express-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
  33. package/registry/combos/express-prisma/shared/src/request-context.ts +1 -1
  34. package/registry/combos/express-prisma/shared/src/routers/auth.router.ts +398 -0
  35. package/registry/combos/express-prisma/variants/base/src/create-auth-app.ts +57 -22
  36. package/registry/combos/express-prisma/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
  37. package/registry/combos/express-prisma/variants/base/src/rbac.defaults.ts +67 -19
  38. package/registry/combos/express-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  39. package/registry/combos/express-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +199 -48
  40. package/registry/combos/express-prisma/variants/base/src/routers/admin.router.ts +508 -0
  41. package/registry/combos/express-prisma/variants/base/src/seed.ts +78 -20
  42. package/registry/combos/express-prisma/variants/base/src/{auth.service.ts → services/auth.service.ts} +454 -105
  43. package/registry/combos/express-prisma/variants/workspaces/src/create-auth-app.ts +81 -26
  44. package/registry/combos/express-prisma/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
  45. package/registry/combos/express-prisma/variants/workspaces/src/openapi-workspace.ts +147 -28
  46. package/registry/combos/express-prisma/variants/workspaces/src/rbac.defaults.ts +73 -21
  47. package/registry/combos/express-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  48. package/registry/combos/express-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +272 -64
  49. package/registry/combos/express-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +86 -23
  50. package/registry/combos/express-prisma/variants/workspaces/src/routers/admin.router.ts +545 -0
  51. package/registry/combos/express-prisma/variants/workspaces/src/routers/workspace.router.ts +194 -0
  52. package/registry/combos/express-prisma/variants/workspaces/src/seed.ts +109 -29
  53. package/registry/combos/express-prisma/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +518 -119
  54. package/registry/combos/nestjs-drizzle/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
  55. package/registry/combos/nestjs-drizzle/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
  56. package/registry/combos/nestjs-drizzle/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
  57. package/registry/combos/nestjs-drizzle/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
  58. package/registry/combos/{nestjs-prisma/shared/src → nestjs-drizzle/shared/src/interceptors}/response.interceptor.ts +10 -2
  59. package/registry/combos/nestjs-drizzle/shared/src/repositories/oauth.repository.ts +63 -0
  60. package/registry/combos/nestjs-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
  61. package/registry/combos/nestjs-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
  62. package/registry/combos/nestjs-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +21 -7
  63. package/registry/combos/nestjs-drizzle/shared/src/request-context.ts +1 -1
  64. package/registry/combos/nestjs-drizzle/test/prove-cycle.ts +1097 -307
  65. package/registry/combos/nestjs-drizzle/variants/base/src/auth.module.ts +44 -20
  66. package/registry/combos/nestjs-drizzle/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +228 -62
  67. package/registry/combos/nestjs-drizzle/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
  68. package/registry/combos/nestjs-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +27 -10
  69. package/registry/combos/nestjs-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +249 -51
  70. package/registry/combos/nestjs-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +499 -116
  71. package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.module.ts +48 -22
  72. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +269 -70
  73. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
  74. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
  75. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +29 -11
  76. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +323 -71
  77. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +131 -30
  78. package/registry/combos/nestjs-drizzle/variants/workspaces/src/services/auth.service.ts +988 -0
  79. package/registry/combos/nestjs-prisma/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
  80. package/registry/combos/nestjs-prisma/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
  81. package/registry/combos/nestjs-prisma/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
  82. package/registry/combos/nestjs-prisma/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
  83. package/registry/combos/{nestjs-drizzle/shared/src → nestjs-prisma/shared/src/interceptors}/response.interceptor.ts +10 -2
  84. package/registry/combos/nestjs-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +25 -8
  85. package/registry/combos/nestjs-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
  86. package/registry/combos/nestjs-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
  87. package/registry/combos/nestjs-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
  88. package/registry/combos/nestjs-prisma/shared/src/request-context.ts +1 -1
  89. package/registry/combos/nestjs-prisma/test/prove-cycle.ts +1097 -307
  90. package/registry/combos/nestjs-prisma/variants/base/src/auth.module.ts +59 -28
  91. package/registry/combos/nestjs-prisma/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +469 -110
  92. package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.gateway.ts → gateways/audit-log.gateway.ts} +22 -6
  93. package/registry/combos/nestjs-prisma/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
  94. package/registry/combos/nestjs-prisma/variants/base/src/rbac.defaults.ts +98 -27
  95. package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  96. package/registry/combos/nestjs-prisma/variants/base/src/{country.repository.ts → repositories/country.repository.ts} +98 -25
  97. package/registry/combos/nestjs-prisma/variants/base/src/{customer.repository.ts → repositories/customer.repository.ts} +106 -27
  98. package/registry/combos/nestjs-prisma/variants/base/src/{language.repository.ts → repositories/language.repository.ts} +93 -25
  99. package/registry/combos/nestjs-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +229 -55
  100. package/registry/combos/nestjs-prisma/variants/base/src/seed.ts +78 -20
  101. package/registry/combos/nestjs-prisma/variants/base/src/services/auth.service.ts +1064 -0
  102. package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.module.ts +55 -27
  103. package/registry/combos/nestjs-prisma/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +544 -128
  104. package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
  105. package/registry/combos/nestjs-prisma/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
  106. package/registry/combos/nestjs-prisma/variants/workspaces/src/rbac.defaults.ts +104 -29
  107. package/registry/combos/nestjs-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  108. package/registry/combos/nestjs-prisma/variants/workspaces/src/{country.repository.ts → repositories/country.repository.ts} +127 -26
  109. package/registry/combos/nestjs-prisma/variants/workspaces/src/{customer.repository.ts → repositories/customer.repository.ts} +137 -28
  110. package/registry/combos/nestjs-prisma/variants/workspaces/src/repositories/language.repository.ts +243 -0
  111. package/registry/combos/nestjs-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +314 -74
  112. package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +94 -24
  113. package/registry/combos/nestjs-prisma/variants/workspaces/src/seed.ts +109 -29
  114. package/registry/combos/nestjs-prisma/variants/workspaces/src/services/auth.service.ts +961 -0
  115. package/registry.json +6 -4
  116. package/simple-auth-kit.ts +582 -124
  117. package/registry/combos/express-drizzle/shared/src/auth.router.ts +0 -266
  118. package/registry/combos/express-drizzle/shared/src/password-reset.repository.ts +0 -35
  119. package/registry/combos/express-drizzle/variants/base/src/admin.router.ts +0 -307
  120. package/registry/combos/express-drizzle/variants/workspaces/src/admin.router.ts +0 -323
  121. package/registry/combos/express-drizzle/variants/workspaces/src/workspace.router.ts +0 -123
  122. package/registry/combos/express-prisma/shared/src/auth.router.ts +0 -269
  123. package/registry/combos/express-prisma/shared/src/password-reset.repository.ts +0 -29
  124. package/registry/combos/express-prisma/variants/base/src/admin.router.ts +0 -314
  125. package/registry/combos/express-prisma/variants/workspaces/src/admin.router.ts +0 -330
  126. package/registry/combos/express-prisma/variants/workspaces/src/workspace.router.ts +0 -123
  127. package/registry/combos/nestjs-drizzle/shared/src/oauth.repository.ts +0 -36
  128. package/registry/combos/nestjs-drizzle/shared/src/password-reset.repository.ts +0 -29
  129. package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.service.ts +0 -553
  130. package/registry/combos/nestjs-prisma/shared/src/password-reset.repository.ts +0 -29
  131. package/registry/combos/nestjs-prisma/variants/base/src/auth.service.ts +0 -618
  132. package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.service.ts +0 -509
  133. package/registry/combos/nestjs-prisma/variants/workspaces/src/language.repository.ts +0 -143
@@ -17,13 +17,27 @@
17
17
  // non-interactive/scripted use.
18
18
  import { existsSync } from "node:fs";
19
19
  import { spawnSync } from "node:child_process";
20
- import { basename, dirname, join, relative, resolve } from "node:path";
20
+ import { basename, dirname, join, resolve } from "node:path";
21
21
  import { access, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
22
22
  import { fileURLToPath } from "node:url";
23
23
  import { createTwoFilesPatch } from "diff";
24
24
  import prompts from "prompts";
25
- import { copyDir, copyOneFile, CopyOptions, CopyResult, DiffEntry, NEVER_COPY, pruneRemovedFiles, SCAFFOLD_NEVER_COPY, sha256, toPosix } from "./lib/copy.js";
26
- import { reconcileManifest, renameNative, type NativeIdentity } from "./lib/rename-native.js";
25
+ import {
26
+ copyDir,
27
+ copyOneFile,
28
+ CopyOptions,
29
+ CopyResult,
30
+ DiffEntry,
31
+ NEVER_COPY,
32
+ pruneRemovedFiles,
33
+ SCAFFOLD_NEVER_COPY,
34
+ sha256,
35
+ } from "./lib/copy.js";
36
+ import {
37
+ reconcileManifest,
38
+ renameNative,
39
+ type NativeIdentity,
40
+ } from "./lib/rename-native.js";
27
41
 
28
42
  const CLI_DIR = dirname(fileURLToPath(import.meta.url));
29
43
  // Two contexts, and they disagree about where the registry is:
@@ -32,18 +46,35 @@ const CLI_DIR = dirname(fileURLToPath(import.meta.url));
32
46
  // materialized right before `npm publish`/`npm pack`).
33
47
  // - monorepo dev checkout: this file lives at packages/cli/, so the real registry/ is two
34
48
  // levels up, at the repo root.
35
- const REGISTRY_ROOT = existsSync(join(CLI_DIR, "registry")) ? join(CLI_DIR, "registry") : resolve(CLI_DIR, "..", "..", "registry");
49
+ const REGISTRY_ROOT = existsSync(join(CLI_DIR, "registry"))
50
+ ? join(CLI_DIR, "registry")
51
+ : resolve(CLI_DIR, "..", "..", "registry");
36
52
  const CONFIG_FILENAME = ".simple-auth-kit.json";
37
53
  const DEFAULT_VARIANT = "base";
38
54
 
39
55
  type Kind = "api" | "admin" | "mobile";
40
56
  type InstallMode = "merge" | "scaffold";
41
57
 
42
- const KIND_LABELS: Record<Kind, string> = { api: "API (backend)", admin: "Admin console", mobile: "Mobile app" };
43
- const KIND_FRAMEWORK_NOUN: Record<Kind, string> = { api: "API stack", admin: "admin framework", mobile: "mobile framework" };
58
+ const KIND_LABELS: Record<Kind, string> = {
59
+ api: "API (backend)",
60
+ admin: "Admin console",
61
+ mobile: "Mobile app",
62
+ };
63
+ const KIND_FRAMEWORK_NOUN: Record<Kind, string> = {
64
+ api: "API stack",
65
+ admin: "admin framework",
66
+ mobile: "mobile framework",
67
+ };
44
68
 
45
69
  /** Flags that take no value. Everything else consumes the next argv entry. */
46
- const BOOLEAN_FLAGS = new Set(["workspaces", "force", "check", "config-only", "skip-install", "help"]);
70
+ const BOOLEAN_FLAGS = new Set([
71
+ "workspaces",
72
+ "force",
73
+ "check",
74
+ "config-only",
75
+ "skip-install",
76
+ "help",
77
+ ]);
47
78
 
48
79
  interface SimpleAuthKitConfig {
49
80
  path: string;
@@ -83,12 +114,21 @@ interface AuthLock {
83
114
  files?: Record<string, string>;
84
115
  }
85
116
 
86
- const DEFAULT_CONFIG: SimpleAuthKitConfig = { path: "src/lib/auth", alias: "@/lib/auth", ignore: [] };
117
+ const DEFAULT_CONFIG: SimpleAuthKitConfig = {
118
+ path: "src/lib/auth",
119
+ alias: "@/lib/auth",
120
+ ignore: [],
121
+ };
87
122
 
88
123
  const comboKind = (combo: ComboEntry): Kind => combo.kind ?? "api";
89
- const comboInstallMode = (combo: ComboEntry): InstallMode => combo.installMode ?? (comboKind(combo) === "api" ? "merge" : "scaffold");
90
-
91
- function parseArgs(argv: string[]): { command?: string; positional: string[]; flags: Record<string, string | true> } {
124
+ const comboInstallMode = (combo: ComboEntry): InstallMode =>
125
+ combo.installMode ?? (comboKind(combo) === "api" ? "merge" : "scaffold");
126
+
127
+ function parseArgs(argv: string[]): {
128
+ command?: string;
129
+ positional: string[];
130
+ flags: Record<string, string | true>;
131
+ } {
92
132
  const [command, ...rest] = argv;
93
133
  const positional: string[] = [];
94
134
  const flags: Record<string, string | true> = {};
@@ -109,9 +149,15 @@ function parseArgs(argv: string[]): { command?: string; positional: string[]; fl
109
149
  return { command, positional, flags };
110
150
  }
111
151
 
112
- const flagString = (value: string | true | undefined): string | undefined => (typeof value === "string" ? value : undefined);
152
+ const flagString = (value: string | true | undefined): string | undefined =>
153
+ typeof value === "string" ? value : undefined;
113
154
  const flagList = (value: string | true | undefined): string[] | undefined =>
114
- typeof value === "string" ? value.split(",").map((s) => s.trim()).filter(Boolean) : undefined;
155
+ typeof value === "string"
156
+ ? value
157
+ .split(",")
158
+ .map((s) => s.trim())
159
+ .filter(Boolean)
160
+ : undefined;
115
161
 
116
162
  async function loadRegistry(): Promise<Registry> {
117
163
  return JSON.parse(await readFile(join(CLI_DIR, "registry.json"), "utf8"));
@@ -128,7 +174,9 @@ async function loadConfig(targetRoot: string): Promise<SimpleAuthKitConfig> {
128
174
 
129
175
  async function loadLock(targetRoot: string): Promise<AuthLock> {
130
176
  try {
131
- return JSON.parse(await readFile(join(targetRoot, "auth.lock.json"), "utf8"));
177
+ return JSON.parse(
178
+ await readFile(join(targetRoot, "auth.lock.json"), "utf8"),
179
+ );
132
180
  } catch {
133
181
  return {};
134
182
  }
@@ -163,18 +211,29 @@ async function isEmptyDir(dir: string): Promise<boolean> {
163
211
  * reported at the end by printInstallSummary) outside a TTY or under --check, since there's
164
212
  * nothing useful to prompt into either case.
165
213
  */
166
- async function resolveForcePaths(runDry: () => Promise<{ result: CopyResult }>, flags: Record<string, string | true>): Promise<Set<string>> {
167
- if (flags.force === true || flags.check === true || !isTTY()) return new Set();
214
+ async function resolveForcePaths(
215
+ runDry: () => Promise<{ result: CopyResult }>,
216
+ flags: Record<string, string | true>,
217
+ ): Promise<Set<string>> {
218
+ if (flags.force === true || flags.check === true || !isTTY())
219
+ return new Set();
168
220
 
169
221
  const { result } = await runDry();
170
222
  if (!result.skipped.length) return new Set();
171
223
 
172
- console.log(`\n${result.skipped.length} file(s) have changed locally since the last install:`);
224
+ console.log(
225
+ `\n${result.skipped.length} file(s) have changed locally since the last install:`,
226
+ );
173
227
  const picked = await ask<string[]>({
174
228
  type: "multiselect",
175
229
  name: "paths",
176
- message: "Overwrite any of these with the latest version? (unselected ones are left alone)",
177
- choices: result.skipped.map((file) => ({ title: file, value: file, selected: false })),
230
+ message:
231
+ "Overwrite any of these with the latest version? (unselected ones are left alone)",
232
+ choices: result.skipped.map((file) => ({
233
+ title: file,
234
+ value: file,
235
+ selected: false,
236
+ })),
178
237
  instructions: false,
179
238
  });
180
239
  return new Set(picked ?? []);
@@ -186,10 +245,16 @@ type PackageManager = (typeof PACKAGE_MANAGERS)[number];
186
245
  /** Detected from whichever lockfile is already sitting in targetRoot — unambiguous, so used
187
246
  * without asking. Returns null when nothing on disk says which tool to use and `--pm` wasn't
188
247
  * given either, leaving it to the caller to ask (interactive) or default (non-interactive). */
189
- function detectPackageManagerFromLockfile(targetRoot: string): PackageManager | null {
248
+ function detectPackageManagerFromLockfile(
249
+ targetRoot: string,
250
+ ): PackageManager | null {
190
251
  if (existsSync(join(targetRoot, "pnpm-lock.yaml"))) return "pnpm";
191
252
  if (existsSync(join(targetRoot, "yarn.lock"))) return "yarn";
192
- if (existsSync(join(targetRoot, "bun.lockb")) || existsSync(join(targetRoot, "bun.lock"))) return "bun";
253
+ if (
254
+ existsSync(join(targetRoot, "bun.lockb")) ||
255
+ existsSync(join(targetRoot, "bun.lock"))
256
+ )
257
+ return "bun";
193
258
  if (existsSync(join(targetRoot, "package-lock.json"))) return "npm";
194
259
  return null;
195
260
  }
@@ -198,11 +263,17 @@ function detectPackageManagerFromLockfile(targetRoot: string): PackageManager |
198
263
  * silently. Only when neither says anything — most commonly a scaffold install into an empty
199
264
  * directory — and this is a real terminal do we ask; a non-interactive run in that same situation
200
265
  * still falls back to npm rather than blocking. */
201
- async function resolvePackageManager(targetRoot: string, flags: Record<string, string | true>): Promise<PackageManager> {
266
+ async function resolvePackageManager(
267
+ targetRoot: string,
268
+ flags: Record<string, string | true>,
269
+ ): Promise<PackageManager> {
202
270
  const requested = flagString(flags.pm);
203
271
  if (requested) {
204
- if ((PACKAGE_MANAGERS as readonly string[]).includes(requested)) return requested as PackageManager;
205
- console.error(`--pm "${requested}" isn't one of ${PACKAGE_MANAGERS.join(", ")} — falling back to auto-detection.`);
272
+ if ((PACKAGE_MANAGERS as readonly string[]).includes(requested))
273
+ return requested as PackageManager;
274
+ console.error(
275
+ `--pm "${requested}" isn't one of ${PACKAGE_MANAGERS.join(", ")} — falling back to auto-detection.`,
276
+ );
206
277
  }
207
278
 
208
279
  const detected = detectPackageManagerFromLockfile(targetRoot);
@@ -229,13 +300,19 @@ async function resolvePackageManager(targetRoot: string, flags: Record<string, s
229
300
  * to add (an update that touched no files has nothing worth re-installing for). `--pm
230
301
  * <npm|pnpm|yarn|bun>` picks the tool explicitly instead of auto-detecting/asking.
231
302
  */
232
- async function installDependencies(targetRoot: string, deps: string[], flags: Record<string, string | true>): Promise<void> {
303
+ async function installDependencies(
304
+ targetRoot: string,
305
+ deps: string[],
306
+ flags: Record<string, string | true>,
307
+ ): Promise<void> {
233
308
  if (flags["skip-install"] === true) return;
234
309
 
235
310
  const pm = await resolvePackageManager(targetRoot, flags);
236
311
  // "install everything in package.json" (zero deps named) is the same bare verb across all
237
312
  // four; naming specific packages is "install <pkgs>" for npm, "add <pkgs>" for the others.
238
- const args = deps.length ? [pm === "npm" ? "install" : "add", ...deps] : ["install"];
313
+ const args = deps.length
314
+ ? [pm === "npm" ? "install" : "add", ...deps]
315
+ : ["install"];
239
316
 
240
317
  console.log(`\nInstalling dependencies (${pm})...`);
241
318
  let result = spawnSync(pm, args, { cwd: targetRoot, stdio: "inherit" });
@@ -247,12 +324,19 @@ async function installDependencies(targetRoot: string, deps: string[], flags: Re
247
324
  // a later `update` (a version bump re-flags a build), so approve unconditionally and retry
248
325
  // once rather than just telling the consumer to do it by hand each time — `approve-builds
249
326
  // --all` is a no-op when nothing is pending.
250
- console.log(`\n${pm} blocked some install scripts — running "pnpm approve-builds --all" and retrying...`);
251
- spawnSync(pm, ["approve-builds", "--all"], { cwd: targetRoot, stdio: "inherit" });
327
+ console.log(
328
+ `\n${pm} blocked some install scripts — running "pnpm approve-builds --all" and retrying...`,
329
+ );
330
+ spawnSync(pm, ["approve-builds", "--all"], {
331
+ cwd: targetRoot,
332
+ stdio: "inherit",
333
+ });
252
334
  result = spawnSync(pm, args, { cwd: targetRoot, stdio: "inherit" });
253
335
  }
254
336
  if (result.status !== 0) {
255
- console.error(`\n${pm} install exited with an error — run it yourself: cd ${targetRoot} && ${pm} ${args.join(" ")}`);
337
+ console.error(
338
+ `\n${pm} install exited with an error — run it yourself: cd ${targetRoot} && ${pm} ${args.join(" ")}`,
339
+ );
256
340
  }
257
341
  }
258
342
 
@@ -264,14 +348,23 @@ async function installDependencies(targetRoot: string, deps: string[], flags: Re
264
348
  * the fresh-project entry point, not just a config file. Pass --config-only to get the old
265
349
  * behavior back (write the config and stop there, no install).
266
350
  */
267
- async function cmdInit(targetRoot: string, flags: Record<string, string | true>) {
351
+ async function cmdInit(
352
+ targetRoot: string,
353
+ flags: Record<string, string | true>,
354
+ ) {
268
355
  const config: SimpleAuthKitConfig = {
269
356
  path: flagString(flags.path) ?? DEFAULT_CONFIG.path,
270
357
  alias: flagString(flags.alias) ?? DEFAULT_CONFIG.alias,
271
358
  ignore: [],
272
359
  };
273
- await writeFile(join(targetRoot, CONFIG_FILENAME), JSON.stringify(config, null, 2) + "\n", "utf8");
274
- console.log(`Wrote ${CONFIG_FILENAME} — combos will install into ${config.path} (import alias ${config.alias})`);
360
+ await writeFile(
361
+ join(targetRoot, CONFIG_FILENAME),
362
+ JSON.stringify(config, null, 2) + "\n",
363
+ "utf8",
364
+ );
365
+ console.log(
366
+ `Wrote ${CONFIG_FILENAME} — combos will install into ${config.path} (import alias ${config.alias})`,
367
+ );
275
368
 
276
369
  if (flags["config-only"] === true) return;
277
370
 
@@ -279,29 +372,44 @@ async function cmdInit(targetRoot: string, flags: Record<string, string | true>)
279
372
  await cmdCreate(targetRoot, flags);
280
373
  }
281
374
 
282
- function resolveVariant(combo: ComboEntry, comboName: string, requested: string): string | null {
375
+ function resolveVariant(
376
+ combo: ComboEntry,
377
+ comboName: string,
378
+ requested: string,
379
+ ): string | null {
283
380
  if (combo.variants.length === 0) {
284
- console.error(`Combo "${comboName}" has not been migrated to the variant layout yet (see registry/README.md) — nothing to install.`);
381
+ console.error(
382
+ `Combo "${comboName}" has not been migrated to the variant layout yet (see registry/README.md) — nothing to install.`,
383
+ );
285
384
  return null;
286
385
  }
287
386
  if (!combo.variants.includes(requested)) {
288
- console.error(`Combo "${comboName}" has no "${requested}" variant. Available: ${combo.variants.join(", ")}`);
387
+ console.error(
388
+ `Combo "${comboName}" has no "${requested}" variant. Available: ${combo.variants.join(", ")}`,
389
+ );
289
390
  return null;
290
391
  }
291
392
  return requested;
292
393
  }
293
394
 
294
395
  function requestedVariant(flags: Record<string, string | true>): string {
295
- return flags.workspaces === true ? "workspaces" : (flagString(flags.variant) ?? DEFAULT_VARIANT);
396
+ return flags.workspaces === true
397
+ ? "workspaces"
398
+ : (flagString(flags.variant) ?? DEFAULT_VARIANT);
296
399
  }
297
400
 
298
- function printInstallSummary(result: CopyResult, pruned: { removed: string[]; keptModified: string[] }) {
401
+ function printInstallSummary(
402
+ result: CopyResult,
403
+ pruned: { removed: string[]; keptModified: string[] },
404
+ ) {
299
405
  if (result.updated.length) {
300
406
  console.log(`\nUpdated (new or changed since last install):`);
301
407
  for (const file of result.updated) console.log(` ${file}`);
302
408
  }
303
409
  if (result.skipped.length) {
304
- console.log(`\nLeft alone — modified since the last install (re-run with --force to overwrite):`);
410
+ console.log(
411
+ `\nLeft alone — modified since the last install (re-run with --force to overwrite):`,
412
+ );
305
413
  for (const file of result.skipped) console.log(` ${file}`);
306
414
  }
307
415
  if (result.ignored.length) {
@@ -313,13 +421,18 @@ function printInstallSummary(result: CopyResult, pruned: { removed: string[]; ke
313
421
  for (const file of pruned.removed) console.log(` ${file}`);
314
422
  }
315
423
  if (pruned.keptModified.length) {
316
- console.log(`\nNo longer part of this install, but modified locally, so kept (delete by hand if you don't want them):`);
424
+ console.log(
425
+ `\nNo longer part of this install, but modified locally, so kept (delete by hand if you don't want them):`,
426
+ );
317
427
  for (const file of pruned.keptModified) console.log(` ${file}`);
318
428
  }
319
429
  }
320
430
 
321
431
  function printPostInstallNotes(combo: ComboEntry, variant: string) {
322
- const notes = [...combo.postInstall, ...(combo.variantPostInstall?.[variant] ?? [])];
432
+ const notes = [
433
+ ...combo.postInstall,
434
+ ...(combo.variantPostInstall?.[variant] ?? []),
435
+ ];
323
436
  if (notes.length) {
324
437
  console.log(`\nNext steps:`);
325
438
  for (const note of notes) console.log(` - ${note}`);
@@ -334,11 +447,23 @@ function printPostInstallNotes(combo: ComboEntry, variant: string) {
334
447
  * to schema.prisma's own location, so moving prisma.config.ts/prisma/ to the project root also
335
448
  * moves the generated client. The registry source's `src/*.ts` files import it as
336
449
  * `"../generated/prisma/client.js"` — correct for the registry's own dev/typecheck loop (where
337
- * nothing has moved), but wrong once copied into a real install, so installMerge rewrites that
338
- * literal string in every copied file to the new correct relative path. Drizzle has no equivalent
339
- * generated artifact to redirect. */
340
- const ORM_LAYOUTS: { configFile: string; dataDir: string; generatedClientImport?: string }[] = [
341
- { configFile: "prisma.config.ts", dataDir: "prisma", generatedClientImport: "../generated/prisma/client.js" },
450
+ * nothing has moved) and installMerge rewrites that literal string in every copied file to
451
+ * `@/prisma/client.js` instead of a computed relative path: the generated client always lands at
452
+ * the project root regardless of how deep the importing file sits (unlike `@/lib/auth/core`,
453
+ * whose depth actually varies with `--path`), so a fixed alias is both simpler and stable across
454
+ * installs. The consumer adds `"@/prisma/*": ["./generated/prisma/*"]` to their tsconfig `paths`
455
+ * once (see postInstall) — the CLI never writes tsconfig itself, same as `@/lib/auth`. Drizzle has
456
+ * no equivalent generated artifact to redirect. */
457
+ const ORM_LAYOUTS: {
458
+ configFile: string;
459
+ dataDir: string;
460
+ generatedClientImport?: string;
461
+ }[] = [
462
+ {
463
+ configFile: "prisma.config.ts",
464
+ dataDir: "prisma",
465
+ generatedClientImport: "../generated/prisma/client.js",
466
+ },
342
467
  { configFile: "drizzle.config.ts", dataDir: "drizzle" },
343
468
  ];
344
469
 
@@ -358,7 +483,13 @@ interface MergePlan {
358
483
 
359
484
  /** Everything about a merge-mode install that doesn't depend on force/dryRun/forcePaths —
360
485
  * computed once, shared by installMerge's real run and cmdDiff's read-only one. */
361
- async function buildMergePlan(combo: ComboEntry, variant: string, targetRoot: string, flags: Record<string, string | true>, previous: Record<string, string>): Promise<MergePlan> {
486
+ async function buildMergePlan(
487
+ combo: ComboEntry,
488
+ variant: string,
489
+ targetRoot: string,
490
+ flags: Record<string, string | true>,
491
+ previous: Record<string, string>,
492
+ ): Promise<MergePlan> {
362
493
  const config = await loadConfig(targetRoot);
363
494
  const installPath = flagString(flags.path) ?? config.path;
364
495
  const alias = flagString(flags.alias) ?? config.alias;
@@ -374,19 +505,35 @@ async function buildMergePlan(combo: ComboEntry, variant: string, targetRoot: st
374
505
  }
375
506
  return null;
376
507
  })();
377
- const skipFromShared = orm ? new Set([...NEVER_COPY, orm.configFile]) : NEVER_COPY;
378
- const skipFromVariant = orm ? new Set([...NEVER_COPY, orm.dataDir]) : NEVER_COPY;
508
+ const skipFromShared = orm
509
+ ? new Set([...NEVER_COPY, orm.configFile])
510
+ : NEVER_COPY;
511
+ const skipFromVariant = orm
512
+ ? new Set([...NEVER_COPY, orm.dataDir])
513
+ : NEVER_COPY;
379
514
 
380
515
  const extraRewrites = orm?.generatedClientImport
381
516
  ? [
382
517
  {
383
518
  from: orm.generatedClientImport,
384
- to: toPosix(relative(join(destRoot, "src"), join(targetRoot, "generated", "prisma", "client.js"))),
519
+ to: "@/prisma/client.js",
385
520
  },
386
521
  ]
387
522
  : undefined;
388
523
 
389
- return { destRoot, installPath, alias, sharedDir, variantDir, orm, skipFromShared, skipFromVariant, extraRewrites, config, previous };
524
+ return {
525
+ destRoot,
526
+ installPath,
527
+ alias,
528
+ sharedDir,
529
+ variantDir,
530
+ orm,
531
+ skipFromShared,
532
+ skipFromVariant,
533
+ extraRewrites,
534
+ config,
535
+ previous,
536
+ };
390
537
  }
391
538
 
392
539
  /** The actual copy pass for a merge-mode install — shared by installMerge (writes for real, or
@@ -405,9 +552,22 @@ async function runMergeCopy(
405
552
  plan: MergePlan,
406
553
  registry: Registry,
407
554
  targetRoot: string,
408
- opts: { force: boolean; forcePaths: Set<string>; dryRun: boolean; collectDiffs?: DiffEntry[] },
409
- ): Promise<{ result: CopyResult; pruned: { removed: string[]; keptModified: string[] } }> {
410
- const result: CopyResult = { manifest: {}, skipped: [], ignored: [], updated: [] };
555
+ opts: {
556
+ force: boolean;
557
+ forcePaths: Set<string>;
558
+ dryRun: boolean;
559
+ collectDiffs?: DiffEntry[];
560
+ },
561
+ ): Promise<{
562
+ result: CopyResult;
563
+ pruned: { removed: string[]; keptModified: string[] };
564
+ }> {
565
+ const result: CopyResult = {
566
+ manifest: {},
567
+ skipped: [],
568
+ ignored: [],
569
+ updated: [],
570
+ };
411
571
  const copyOpts: CopyOptions = {
412
572
  aliasFrom: "@/lib/auth/core",
413
573
  aliasTo: `${plan.alias}/core`,
@@ -421,33 +581,117 @@ async function runMergeCopy(
421
581
  collectDiffs: opts.collectDiffs,
422
582
  };
423
583
 
424
- await copyDir(join(REGISTRY_ROOT, registry.core.dir), join(plan.destRoot, "core"), copyOpts, plan.destRoot, result);
425
- await copyDir(plan.sharedDir, plan.destRoot, { ...copyOpts, neverCopy: plan.skipFromShared }, plan.destRoot, result);
426
- await copyDir(plan.variantDir, plan.destRoot, { ...copyOpts, neverCopy: plan.skipFromVariant }, plan.destRoot, result);
584
+ await copyDir(
585
+ join(REGISTRY_ROOT, registry.core.dir),
586
+ join(plan.destRoot, "core"),
587
+ copyOpts,
588
+ plan.destRoot,
589
+ result,
590
+ );
591
+
592
+ // shared/src/ and variants/<variant>/src/ are the registry's own internal source layout
593
+ // (parallel to core/, prisma.config.ts, etc.) — copied flat into destRoot rather than
594
+ // preserving that extra "src" level, so a consumer who installed at the default
595
+ // "src/lib/auth" doesn't end up with a redundant "src/lib/auth/src/...". Order matters:
596
+ // shared fully applied (its own src/, then everything else in shared/), then the variant
597
+ // overlaid the same way, so a variant file with the same name still wins.
598
+ const skipShared = new Set([...plan.skipFromShared, "src"]);
599
+ const skipVariant = new Set([...plan.skipFromVariant, "src"]);
600
+ if (await pathExists(join(plan.sharedDir, "src"))) {
601
+ await copyDir(
602
+ join(plan.sharedDir, "src"),
603
+ plan.destRoot,
604
+ { ...copyOpts, neverCopy: plan.skipFromShared },
605
+ plan.destRoot,
606
+ result,
607
+ );
608
+ }
609
+ await copyDir(
610
+ plan.sharedDir,
611
+ plan.destRoot,
612
+ { ...copyOpts, neverCopy: skipShared },
613
+ plan.destRoot,
614
+ result,
615
+ );
616
+ if (await pathExists(join(plan.variantDir, "src"))) {
617
+ await copyDir(
618
+ join(plan.variantDir, "src"),
619
+ plan.destRoot,
620
+ { ...copyOpts, neverCopy: plan.skipFromVariant },
621
+ plan.destRoot,
622
+ result,
623
+ );
624
+ }
625
+ await copyDir(
626
+ plan.variantDir,
627
+ plan.destRoot,
628
+ { ...copyOpts, neverCopy: skipVariant },
629
+ plan.destRoot,
630
+ result,
631
+ );
427
632
 
428
633
  if (plan.orm) {
429
- await copyOneFile(join(plan.sharedDir, plan.orm.configFile), join(targetRoot, plan.orm.configFile), plan.destRoot, copyOpts, result);
430
- await copyDir(join(plan.variantDir, plan.orm.dataDir), join(targetRoot, plan.orm.dataDir), copyOpts, plan.destRoot, result);
634
+ await copyOneFile(
635
+ join(plan.sharedDir, plan.orm.configFile),
636
+ join(targetRoot, plan.orm.configFile),
637
+ plan.destRoot,
638
+ copyOpts,
639
+ result,
640
+ );
641
+ await copyDir(
642
+ join(plan.variantDir, plan.orm.dataDir),
643
+ join(targetRoot, plan.orm.dataDir),
644
+ copyOpts,
645
+ plan.destRoot,
646
+ result,
647
+ );
431
648
  }
432
649
 
433
650
  // Switching variants has to remove the old variant's files, or the project ends up with both wired in.
434
- const pruned = await pruneRemovedFiles(plan.destRoot, plan.previous, result, { force: opts.force, ignore: plan.config.ignore, dryRun: opts.dryRun });
651
+ const pruned = await pruneRemovedFiles(plan.destRoot, plan.previous, result, {
652
+ force: opts.force,
653
+ ignore: plan.config.ignore,
654
+ dryRun: opts.dryRun,
655
+ });
435
656
  return { result, pruned };
436
657
  }
437
658
 
438
- async function installMerge(comboName: string, combo: ComboEntry, variant: string, registry: Registry, targetRoot: string, flags: Record<string, string | true>) {
659
+ async function installMerge(
660
+ comboName: string,
661
+ combo: ComboEntry,
662
+ variant: string,
663
+ registry: Registry,
664
+ targetRoot: string,
665
+ flags: Record<string, string | true>,
666
+ ) {
439
667
  const lock = await loadLock(targetRoot);
440
668
  const previous = lock.files ?? {};
441
669
  const force = flags.force === true;
442
670
  const checkOnly = flags.check === true;
443
671
 
444
- const plan = await buildMergePlan(combo, variant, targetRoot, flags, previous);
672
+ const plan = await buildMergePlan(
673
+ combo,
674
+ variant,
675
+ targetRoot,
676
+ flags,
677
+ previous,
678
+ );
445
679
 
446
680
  if (checkOnly) {
447
- const { result, pruned } = await runMergeCopy(plan, registry, targetRoot, { force, forcePaths: new Set(), dryRun: true });
448
- console.log(`\nCheck only — nothing written. "${comboName}" (${variant} variant) in ${plan.installPath} (alias ${plan.alias}):`);
681
+ const { result, pruned } = await runMergeCopy(plan, registry, targetRoot, {
682
+ force,
683
+ forcePaths: new Set(),
684
+ dryRun: true,
685
+ });
686
+ console.log(
687
+ `\nCheck only — nothing written. "${comboName}" (${variant} variant) in ${plan.installPath} (alias ${plan.alias}):`,
688
+ );
449
689
  printInstallSummary(result, pruned);
450
- const silent = !result.updated.length && !result.skipped.length && !pruned.removed.length && !pruned.keptModified.length;
690
+ const silent =
691
+ !result.updated.length &&
692
+ !result.skipped.length &&
693
+ !pruned.removed.length &&
694
+ !pruned.keptModified.length;
451
695
  if (silent) console.log(`\nUp to date — nothing would change.`);
452
696
  return;
453
697
  }
@@ -455,19 +699,45 @@ async function installMerge(comboName: string, combo: ComboEntry, variant: strin
455
699
  // Interactive + no --force: find locally-modified conflicts first (a dry run, nothing written),
456
700
  // and let the user pick which — if any — to overwrite anyway, shadcn-"this file already exists"
457
701
  // style, instead of the non-interactive default of silently leaving all of them alone.
458
- const forcePaths = await resolveForcePaths(() => runMergeCopy(plan, registry, targetRoot, { force: false, forcePaths: new Set(), dryRun: true }), flags);
459
- const { result, pruned } = await runMergeCopy(plan, registry, targetRoot, { force, forcePaths, dryRun: false });
702
+ const forcePaths = await resolveForcePaths(
703
+ () =>
704
+ runMergeCopy(plan, registry, targetRoot, {
705
+ force: false,
706
+ forcePaths: new Set(),
707
+ dryRun: true,
708
+ }),
709
+ flags,
710
+ );
711
+ const { result, pruned } = await runMergeCopy(plan, registry, targetRoot, {
712
+ force,
713
+ forcePaths,
714
+ dryRun: false,
715
+ });
460
716
 
461
717
  await writeFile(
462
718
  join(targetRoot, "auth.lock.json"),
463
- JSON.stringify({ ...lock, combo: comboName, variant, installedAt: new Date().toISOString(), files: result.manifest }, null, 2) + "\n",
719
+ JSON.stringify(
720
+ {
721
+ ...lock,
722
+ combo: comboName,
723
+ variant,
724
+ installedAt: new Date().toISOString(),
725
+ files: result.manifest,
726
+ },
727
+ null,
728
+ 2,
729
+ ) + "\n",
464
730
  "utf8",
465
731
  );
466
732
 
467
- console.log(`\nInstalled "${comboName}" (${variant} variant) into ${plan.installPath} (alias ${plan.alias})`);
733
+ console.log(
734
+ `\nInstalled "${comboName}" (${variant} variant) into ${plan.installPath} (alias ${plan.alias})`,
735
+ );
468
736
  printInstallSummary(result, pruned);
469
737
 
470
- const peerDeps = [...new Set([...registry.core.peerDependencies, ...combo.peerDependencies])];
738
+ const peerDeps = [
739
+ ...new Set([...registry.core.peerDependencies, ...combo.peerDependencies]),
740
+ ];
471
741
  console.log(`\nPeer dependencies: ${peerDeps.join(" ")}`);
472
742
  if (result.updated.length) {
473
743
  await installDependencies(targetRoot, peerDeps, flags);
@@ -480,7 +750,13 @@ async function installMerge(comboName: string, combo: ComboEntry, variant: strin
480
750
  /** "scaffold" install — admin/mobile apps materialized as a whole standalone project directly
481
751
  * into targetRoot: no core layer, no alias rewrite, package.json is real content (copied and
482
752
  * name/description-templated), not the registry's own dev wiring. */
483
- async function installScaffold(comboName: string, combo: ComboEntry, variant: string, targetRoot: string, flags: Record<string, string | true>) {
753
+ async function installScaffold(
754
+ comboName: string,
755
+ combo: ComboEntry,
756
+ variant: string,
757
+ targetRoot: string,
758
+ flags: Record<string, string | true>,
759
+ ) {
484
760
  const lock = await loadLock(targetRoot);
485
761
  const previous = lock.files ?? {};
486
762
  const force = flags.force === true;
@@ -498,24 +774,65 @@ async function installScaffold(comboName: string, combo: ComboEntry, variant: st
498
774
 
499
775
  const comboDir = join(REGISTRY_ROOT, combo.dir);
500
776
 
501
- const runCopy = async (opts: { force: boolean; forcePaths: Set<string>; dryRun: boolean }) => {
502
- const result: CopyResult = { manifest: {}, skipped: [], ignored: [], updated: [] };
503
- const copyOpts = { previous, force: opts.force, forcePaths: opts.forcePaths, neverCopy: SCAFFOLD_NEVER_COPY, dryRun: opts.dryRun };
504
- await copyDir(join(comboDir, combo.sharedDir ?? "shared"), targetRoot, copyOpts, targetRoot, result);
505
- await copyDir(join(comboDir, combo.variantsDir ?? "variants", variant), targetRoot, copyOpts, targetRoot, result);
506
- const pruned = await pruneRemovedFiles(targetRoot, previous, result, { force: opts.force, dryRun: opts.dryRun });
777
+ const runCopy = async (opts: {
778
+ force: boolean;
779
+ forcePaths: Set<string>;
780
+ dryRun: boolean;
781
+ }) => {
782
+ const result: CopyResult = {
783
+ manifest: {},
784
+ skipped: [],
785
+ ignored: [],
786
+ updated: [],
787
+ };
788
+ const copyOpts = {
789
+ previous,
790
+ force: opts.force,
791
+ forcePaths: opts.forcePaths,
792
+ neverCopy: SCAFFOLD_NEVER_COPY,
793
+ dryRun: opts.dryRun,
794
+ };
795
+ await copyDir(
796
+ join(comboDir, combo.sharedDir ?? "shared"),
797
+ targetRoot,
798
+ copyOpts,
799
+ targetRoot,
800
+ result,
801
+ );
802
+ await copyDir(
803
+ join(comboDir, combo.variantsDir ?? "variants", variant),
804
+ targetRoot,
805
+ copyOpts,
806
+ targetRoot,
807
+ result,
808
+ );
809
+ const pruned = await pruneRemovedFiles(targetRoot, previous, result, {
810
+ force: opts.force,
811
+ dryRun: opts.dryRun,
812
+ });
507
813
  return { result, pruned };
508
814
  };
509
815
 
510
816
  // Same shadcn-style "this file already exists, overwrite?" prompt as installMerge — see
511
817
  // resolveForcePaths. A no-op dry run on a genuinely fresh install (nothing to conflict with).
512
- const forcePaths = await resolveForcePaths(() => runCopy({ force: false, forcePaths: new Set(), dryRun: true }), flags);
513
- const { result, pruned } = await runCopy({ force, forcePaths, dryRun: false });
818
+ const forcePaths = await resolveForcePaths(
819
+ () => runCopy({ force: false, forcePaths: new Set(), dryRun: true }),
820
+ flags,
821
+ );
822
+ const { result, pruned } = await runCopy({
823
+ force,
824
+ forcePaths,
825
+ dryRun: false,
826
+ });
514
827
 
515
828
  const appName = flagString(flags.name) ?? basename(targetRoot);
516
829
 
517
830
  if (combo.nativeIdentity) {
518
- const renamed = await renameNative(targetRoot, combo.nativeIdentity, appName);
831
+ const renamed = await renameNative(
832
+ targetRoot,
833
+ combo.nativeIdentity,
834
+ appName,
835
+ );
519
836
  result.manifest = reconcileManifest(result.manifest, renamed);
520
837
  }
521
838
 
@@ -533,11 +850,23 @@ async function installScaffold(comboName: string, combo: ComboEntry, variant: st
533
850
 
534
851
  await writeFile(
535
852
  join(targetRoot, "auth.lock.json"),
536
- JSON.stringify({ ...lock, combo: comboName, variant, installedAt: new Date().toISOString(), files: result.manifest }, null, 2) + "\n",
853
+ JSON.stringify(
854
+ {
855
+ ...lock,
856
+ combo: comboName,
857
+ variant,
858
+ installedAt: new Date().toISOString(),
859
+ files: result.manifest,
860
+ },
861
+ null,
862
+ 2,
863
+ ) + "\n",
537
864
  "utf8",
538
865
  );
539
866
 
540
- console.log(`\nGenerated "${appName}" (${comboName}, ${variant} variant) into ${targetRoot}`);
867
+ console.log(
868
+ `\nGenerated "${appName}" (${comboName}, ${variant} variant) into ${targetRoot}`,
869
+ );
541
870
  printInstallSummary(result, pruned);
542
871
  if (result.updated.length) {
543
872
  // Dependencies are already declared in package.json — no specific packages to name, just
@@ -549,7 +878,14 @@ async function installScaffold(comboName: string, combo: ComboEntry, variant: st
549
878
  printPostInstallNotes(combo, variant);
550
879
  }
551
880
 
552
- async function installCombo(comboName: string, combo: ComboEntry, variant: string, registry: Registry, targetRoot: string, flags: Record<string, string | true>) {
881
+ async function installCombo(
882
+ comboName: string,
883
+ combo: ComboEntry,
884
+ variant: string,
885
+ registry: Registry,
886
+ targetRoot: string,
887
+ flags: Record<string, string | true>,
888
+ ) {
553
889
  if (comboInstallMode(combo) === "scaffold") {
554
890
  await installScaffold(comboName, combo, variant, targetRoot, flags);
555
891
  } else {
@@ -557,11 +893,17 @@ async function installCombo(comboName: string, combo: ComboEntry, variant: strin
557
893
  }
558
894
  }
559
895
 
560
- async function cmdAdd(comboName: string, targetRoot: string, flags: Record<string, string | true>) {
896
+ async function cmdAdd(
897
+ comboName: string,
898
+ targetRoot: string,
899
+ flags: Record<string, string | true>,
900
+ ) {
561
901
  const registry = await loadRegistry();
562
902
  const combo = registry.combos[comboName];
563
903
  if (!combo) {
564
- console.error(`Unknown combo "${comboName}". Available: ${Object.keys(registry.combos).join(", ")}`);
904
+ console.error(
905
+ `Unknown combo "${comboName}". Available: ${Object.keys(registry.combos).join(", ")}`,
906
+ );
565
907
  process.exitCode = 1;
566
908
  return;
567
909
  }
@@ -575,15 +917,22 @@ async function cmdAdd(comboName: string, targetRoot: string, flags: Record<strin
575
917
  await installCombo(comboName, combo, variant, registry, targetRoot, flags);
576
918
  }
577
919
 
578
- function combosByKind(registry: Registry, kind: Kind): Array<[string, ComboEntry]> {
579
- return Object.entries(registry.combos).filter(([, combo]) => comboKind(combo) === kind);
920
+ function combosByKind(
921
+ registry: Registry,
922
+ kind: Kind,
923
+ ): Array<[string, ComboEntry]> {
924
+ return Object.entries(registry.combos).filter(
925
+ ([, combo]) => comboKind(combo) === kind,
926
+ );
580
927
  }
581
928
 
582
929
  const isTTY = () => Boolean(process.stdin.isTTY && process.stdout.isTTY);
583
930
 
584
931
  /** Wraps a single `prompts()` call so a cancel (Ctrl+C) surfaces as `null` instead of `undefined`,
585
932
  * which prompts itself doesn't distinguish from "field asked but left empty". */
586
- async function ask<T>(question: Parameters<typeof prompts>[0] & { name: string }): Promise<T | null> {
933
+ async function ask<T>(
934
+ question: Parameters<typeof prompts>[0] & { name: string },
935
+ ): Promise<T | null> {
587
936
  const res = await prompts(question as never);
588
937
  const value = (res as Record<string, unknown>)[question.name as string];
589
938
  return value === undefined ? null : (value as T);
@@ -595,7 +944,14 @@ async function ask<T>(question: Parameters<typeof prompts>[0] & { name: string }
595
944
  * choice applies to every kind picked this run, matching how `variants` is already a single
596
945
  * cross-cutting concept rather than per-combo.
597
946
  */
598
- async function resolvePlan(registry: Registry, flags: Record<string, string | true>): Promise<Array<{ comboName: string; combo: ComboEntry; variant: string }> | null> {
947
+ async function resolvePlan(
948
+ registry: Registry,
949
+ flags: Record<string, string | true>,
950
+ ): Promise<Array<{
951
+ comboName: string;
952
+ combo: ComboEntry;
953
+ variant: string;
954
+ }> | null> {
599
955
  const flagKinds = flagList(flags.kind) as Kind[] | undefined;
600
956
  const flagFrameworks = flagList(flags.framework);
601
957
  const interactive = isTTY();
@@ -608,7 +964,10 @@ async function resolvePlan(registry: Registry, flags: Record<string, string | tr
608
964
  type: "multiselect",
609
965
  name: "kinds",
610
966
  message: "What would you like to generate?",
611
- choices: (Object.keys(KIND_LABELS) as Kind[]).map((value) => ({ title: KIND_LABELS[value], value })),
967
+ choices: (Object.keys(KIND_LABELS) as Kind[]).map((value) => ({
968
+ title: KIND_LABELS[value],
969
+ value,
970
+ })),
612
971
  min: 1,
613
972
  instructions: false,
614
973
  });
@@ -618,7 +977,9 @@ async function resolvePlan(registry: Registry, flags: Record<string, string | tr
618
977
  }
619
978
  kinds = picked;
620
979
  } else {
621
- console.error("Non-interactive session: pass --kind api,admin,mobile (comma-separated), or a specific combo name (simple-auth-kit add <combo>).");
980
+ console.error(
981
+ "Non-interactive session: pass --kind api,admin,mobile (comma-separated), or a specific combo name (simple-auth-kit add <combo>).",
982
+ );
622
983
  return null;
623
984
  }
624
985
 
@@ -642,7 +1003,9 @@ async function resolvePlan(registry: Registry, flags: Record<string, string | tr
642
1003
 
643
1004
  let comboName = flagFrameworks?.[i];
644
1005
  if (comboName && !available.some(([name]) => name === comboName)) {
645
- const match = available.find(([name]) => name === comboName || name.endsWith(`-${comboName}`));
1006
+ const match = available.find(
1007
+ ([name]) => name === comboName || name.endsWith(`-${comboName}`),
1008
+ );
646
1009
  comboName = match?.[0];
647
1010
  }
648
1011
 
@@ -662,7 +1025,9 @@ async function resolvePlan(registry: Registry, flags: Record<string, string | tr
662
1025
  }
663
1026
  comboName = picked;
664
1027
  } else {
665
- console.error(`Multiple "${kind}" combos available (${available.map(([name]) => name).join(", ")}) — pass --framework to disambiguate.`);
1028
+ console.error(
1029
+ `Multiple "${kind}" combos available (${available.map(([name]) => name).join(", ")}) — pass --framework to disambiguate.`,
1030
+ );
666
1031
  return null;
667
1032
  }
668
1033
  }
@@ -682,7 +1047,12 @@ async function resolvePlan(registry: Registry, flags: Record<string, string | tr
682
1047
  } else if (requestedVariantFlag === "base") {
683
1048
  wantsWorkspaces = false;
684
1049
  } else if (interactive) {
685
- const picked = await ask<boolean>({ type: "confirm", name: "workspaces", message: "Include workspaces support?", initial: false });
1050
+ const picked = await ask<boolean>({
1051
+ type: "confirm",
1052
+ name: "workspaces",
1053
+ message: "Include workspaces support?",
1054
+ initial: false,
1055
+ });
686
1056
  if (picked === null) {
687
1057
  console.log("Cancelled.");
688
1058
  return null;
@@ -692,16 +1062,27 @@ async function resolvePlan(registry: Registry, flags: Record<string, string | tr
692
1062
  wantsWorkspaces = false;
693
1063
  }
694
1064
 
695
- const selections: Array<{ comboName: string; combo: ComboEntry; variant: string }> = [];
1065
+ const selections: Array<{
1066
+ comboName: string;
1067
+ combo: ComboEntry;
1068
+ variant: string;
1069
+ }> = [];
696
1070
  for (const pick of picks) {
697
- const variant = resolveVariant(pick.combo, pick.comboName, wantsWorkspaces ? "workspaces" : DEFAULT_VARIANT);
1071
+ const variant = resolveVariant(
1072
+ pick.combo,
1073
+ pick.comboName,
1074
+ wantsWorkspaces ? "workspaces" : DEFAULT_VARIANT,
1075
+ );
698
1076
  if (!variant) return null;
699
1077
  selections.push({ ...pick, variant });
700
1078
  }
701
1079
  return selections;
702
1080
  }
703
1081
 
704
- async function cmdCreate(targetRoot: string, flags: Record<string, string | true>) {
1082
+ async function cmdCreate(
1083
+ targetRoot: string,
1084
+ flags: Record<string, string | true>,
1085
+ ) {
705
1086
  const registry = await loadRegistry();
706
1087
  const selections = await resolvePlan(registry, flags);
707
1088
  if (!selections) {
@@ -723,8 +1104,18 @@ async function cmdCreate(targetRoot: string, flags: Record<string, string | true
723
1104
  // (they don't have their own package.json identity), but two scaffold apps both literally
724
1105
  // named e.g. "combo-test" would collide if anything ever treats them as sibling packages
725
1106
  // (a pnpm/npm workspace, for one). Suffix with the combo name once there's more than one.
726
- const installFlags = namespaced && flagString(flags.name) ? { ...flags, name: `${flagString(flags.name)}-${comboName}` } : flags;
727
- await installCombo(comboName, combo, variant, registry, installRoot, installFlags);
1107
+ const installFlags =
1108
+ namespaced && flagString(flags.name)
1109
+ ? { ...flags, name: `${flagString(flags.name)}-${comboName}` }
1110
+ : flags;
1111
+ await installCombo(
1112
+ comboName,
1113
+ combo,
1114
+ variant,
1115
+ registry,
1116
+ installRoot,
1117
+ installFlags,
1118
+ );
728
1119
  }
729
1120
  }
730
1121
 
@@ -735,10 +1126,15 @@ async function cmdCreate(targetRoot: string, flags: Record<string, string | true
735
1126
  * anything you have is left alone and reported, exactly like a first install. Pass `--force`
736
1127
  * yourself if you really do want to overwrite local edits.
737
1128
  */
738
- async function cmdUpdate(targetRoot: string, flags: Record<string, string | true>) {
1129
+ async function cmdUpdate(
1130
+ targetRoot: string,
1131
+ flags: Record<string, string | true>,
1132
+ ) {
739
1133
  const lock = await loadLock(targetRoot);
740
1134
  if (!lock.combo || !lock.variant) {
741
- console.error(`No auth.lock.json (or it's missing combo/variant) in ${targetRoot} — nothing to update. Use "add <combo>" for a first install.`);
1135
+ console.error(
1136
+ `No auth.lock.json (or it's missing combo/variant) in ${targetRoot} — nothing to update. Use "add <combo>" for a first install.`,
1137
+ );
742
1138
  process.exitCode = 1;
743
1139
  return;
744
1140
  }
@@ -746,7 +1142,9 @@ async function cmdUpdate(targetRoot: string, flags: Record<string, string | true
746
1142
  const registry = await loadRegistry();
747
1143
  const combo = registry.combos[lock.combo];
748
1144
  if (!combo) {
749
- console.error(`auth.lock.json names combo "${lock.combo}", which no longer exists in this registry.`);
1145
+ console.error(
1146
+ `auth.lock.json names combo "${lock.combo}", which no longer exists in this registry.`,
1147
+ );
750
1148
  process.exitCode = 1;
751
1149
  return;
752
1150
  }
@@ -758,12 +1156,16 @@ async function cmdUpdate(targetRoot: string, flags: Record<string, string | true
758
1156
  }
759
1157
 
760
1158
  if (flags.check === true && comboInstallMode(combo) === "scaffold") {
761
- console.error(`--check isn't supported for "${lock.combo}" (a scaffold-mode combo) — only merge-mode (api) combos support a dry run.`);
1159
+ console.error(
1160
+ `--check isn't supported for "${lock.combo}" (a scaffold-mode combo) — only merge-mode (api) combos support a dry run.`,
1161
+ );
762
1162
  process.exitCode = 1;
763
1163
  return;
764
1164
  }
765
1165
 
766
- console.log(`Updating "${lock.combo}" (${variant} variant) in ${targetRoot}${flags.force === true ? " — --force: local edits will be overwritten" : ""}`);
1166
+ console.log(
1167
+ `Updating "${lock.combo}" (${variant} variant) in ${targetRoot}${flags.force === true ? " — --force: local edits will be overwritten" : ""}`,
1168
+ );
767
1169
  await installCombo(lock.combo, combo, variant, registry, targetRoot, flags);
768
1170
  }
769
1171
 
@@ -776,7 +1178,9 @@ async function cmdUpdate(targetRoot: string, flags: Record<string, string | true
776
1178
  async function cmdDiff(targetRoot: string) {
777
1179
  const lock = await loadLock(targetRoot);
778
1180
  if (!lock.combo || !lock.variant) {
779
- console.error(`No auth.lock.json (or it's missing combo/variant) in ${targetRoot} — nothing installed to diff.`);
1181
+ console.error(
1182
+ `No auth.lock.json (or it's missing combo/variant) in ${targetRoot} — nothing installed to diff.`,
1183
+ );
780
1184
  process.exitCode = 1;
781
1185
  return;
782
1186
  }
@@ -784,13 +1188,17 @@ async function cmdDiff(targetRoot: string) {
784
1188
  const registry = await loadRegistry();
785
1189
  const combo = registry.combos[lock.combo];
786
1190
  if (!combo) {
787
- console.error(`auth.lock.json names combo "${lock.combo}", which no longer exists in this registry.`);
1191
+ console.error(
1192
+ `auth.lock.json names combo "${lock.combo}", which no longer exists in this registry.`,
1193
+ );
788
1194
  process.exitCode = 1;
789
1195
  return;
790
1196
  }
791
1197
 
792
1198
  if (comboInstallMode(combo) === "scaffold") {
793
- console.error(`diff isn't supported yet for "${lock.combo}" (a scaffold-mode combo) — only merge-mode (api) combos support it.`);
1199
+ console.error(
1200
+ `diff isn't supported yet for "${lock.combo}" (a scaffold-mode combo) — only merge-mode (api) combos support it.`,
1201
+ );
794
1202
  process.exitCode = 1;
795
1203
  return;
796
1204
  }
@@ -801,21 +1209,41 @@ async function cmdDiff(targetRoot: string) {
801
1209
  return;
802
1210
  }
803
1211
 
804
- const plan = await buildMergePlan(combo, variant, targetRoot, {}, lock.files ?? {});
1212
+ const plan = await buildMergePlan(
1213
+ combo,
1214
+ variant,
1215
+ targetRoot,
1216
+ {},
1217
+ lock.files ?? {},
1218
+ );
805
1219
  const diffs: DiffEntry[] = [];
806
1220
  // force:true so a locally-modified file's difference is captured too — diff wants to show
807
1221
  // everything that differs, not just what a real (non---force) install would apply; dryRun:true
808
1222
  // so nothing is written.
809
- await runMergeCopy(plan, registry, targetRoot, { force: true, forcePaths: new Set(), dryRun: true, collectDiffs: diffs });
1223
+ await runMergeCopy(plan, registry, targetRoot, {
1224
+ force: true,
1225
+ forcePaths: new Set(),
1226
+ dryRun: true,
1227
+ collectDiffs: diffs,
1228
+ });
810
1229
 
811
1230
  if (!diffs.length) {
812
- console.log(`No differences — every tracked file in "${lock.combo}" (${variant} variant) matches the current registry.`);
1231
+ console.log(
1232
+ `No differences — every tracked file in "${lock.combo}" (${variant} variant) matches the current registry.`,
1233
+ );
813
1234
  return;
814
1235
  }
815
1236
 
816
1237
  console.log(`${diffs.length} file(s) differ from the current registry:`);
817
1238
  for (const d of diffs) {
818
- const patch = createTwoFilesPatch(d.path, d.path, d.oldContent ?? "", d.newContent, "installed", "registry");
1239
+ const patch = createTwoFilesPatch(
1240
+ d.path,
1241
+ d.path,
1242
+ d.oldContent ?? "",
1243
+ d.newContent,
1244
+ "installed",
1245
+ "registry",
1246
+ );
819
1247
  process.stdout.write(`\n${patch}`);
820
1248
  }
821
1249
  }
@@ -823,25 +1251,50 @@ async function cmdDiff(targetRoot: string) {
823
1251
  async function printUsage(exitCode: number) {
824
1252
  const registry = await loadRegistry();
825
1253
  console.log("Usage: simple-auth-kit <init|add|update|diff> [...]");
826
- console.log(` init [--config-only] [--kind ...] [--into <path>] (fresh setup: guided "what do you need" picker, like bare "add")`);
827
- console.log(` --config-only: just write .simple-auth-kit.json and stop, no install`);
828
- console.log(` add <combo> [--workspaces] [--force] [--skip-install] [--into <path>] [--path <dir>] [--alias <alias>]`);
829
- console.log(` add [--kind api,admin,mobile] [--framework <name>,...] [--workspaces] [--into <path>] [--name <appName>]`);
830
- console.log(` (bare "add", or "add" with --kind but no combo, launches a guided prompt for whatever's missing)`);
831
- console.log(` update [--check] [--force] [--skip-install] [--into <path>] (re-installs whatever combo+variant auth.lock.json already records)`);
832
- console.log(` --check: report what would change, without writing anything (merge-mode combos only)`);
833
- console.log(` diff [--into <path>] (shows the actual content diff for every tracked file that differs from the current registry — read-only; merge-mode combos only)`);
834
- console.log(` In a TTY, without --force or --check: a file changed locally since install prompts to overwrite, per file.`);
835
- console.log(` --skip-install: don't run the package manager after copying files — the default is to install for you, shadcn-\`add\`-style.`);
836
- console.log(` --pm <npm|pnpm|yarn|bun>: which package manager to install with — default: detected from a lockfile in the target directory, npm if none found.`);
1254
+ console.log(
1255
+ ` init [--config-only] [--kind ...] [--into <path>] (fresh setup: guided "what do you need" picker, like bare "add")`,
1256
+ );
1257
+ console.log(
1258
+ ` --config-only: just write .simple-auth-kit.json and stop, no install`,
1259
+ );
1260
+ console.log(
1261
+ ` add <combo> [--workspaces] [--force] [--skip-install] [--into <path>] [--path <dir>] [--alias <alias>]`,
1262
+ );
1263
+ console.log(
1264
+ ` add [--kind api,admin,mobile] [--framework <name>,...] [--workspaces] [--into <path>] [--name <appName>]`,
1265
+ );
1266
+ console.log(
1267
+ ` (bare "add", or "add" with --kind but no combo, launches a guided prompt for whatever's missing)`,
1268
+ );
1269
+ console.log(
1270
+ ` update [--check] [--force] [--skip-install] [--into <path>] (re-installs whatever combo+variant auth.lock.json already records)`,
1271
+ );
1272
+ console.log(
1273
+ ` --check: report what would change, without writing anything (merge-mode combos only)`,
1274
+ );
1275
+ console.log(
1276
+ ` diff [--into <path>] (shows the actual content diff for every tracked file that differs from the current registry — read-only; merge-mode combos only)`,
1277
+ );
1278
+ console.log(
1279
+ ` In a TTY, without --force or --check: a file changed locally since install prompts to overwrite, per file.`,
1280
+ );
1281
+ console.log(
1282
+ ` --skip-install: don't run the package manager after copying files — the default is to install for you, shadcn-\`add\`-style.`,
1283
+ );
1284
+ console.log(
1285
+ ` --pm <npm|pnpm|yarn|bun>: which package manager to install with — default: detected from a lockfile in the target directory, npm if none found.`,
1286
+ );
837
1287
  console.log(`\nAvailable combos:`);
838
1288
  for (const kind of ["api", "admin", "mobile"] as Kind[]) {
839
1289
  const names = combosByKind(registry, kind).map(([name]) => name);
840
- if (names.length) console.log(` ${KIND_LABELS[kind]}: ${names.join(", ")}`);
1290
+ if (names.length)
1291
+ console.log(` ${KIND_LABELS[kind]}: ${names.join(", ")}`);
841
1292
  }
842
1293
  console.log(`\nVariants (choose one at install time):`);
843
1294
  for (const [name, variant] of Object.entries(registry.variants)) {
844
- console.log(` ${name}${variant.flag ? ` (${variant.flag})` : " (default)"} — ${variant.description}`);
1295
+ console.log(
1296
+ ` ${name}${variant.flag ? ` (${variant.flag})` : " (default)"} — ${variant.description}`,
1297
+ );
845
1298
  }
846
1299
  process.exitCode = exitCode;
847
1300
  }
@@ -850,7 +1303,12 @@ async function main() {
850
1303
  const { command, positional, flags } = parseArgs(process.argv.slice(2));
851
1304
  const targetRoot = resolve(process.cwd(), flagString(flags.into) ?? ".");
852
1305
 
853
- if (command === "--help" || command === "-h" || command === "help" || flags.help === true) {
1306
+ if (
1307
+ command === "--help" ||
1308
+ command === "-h" ||
1309
+ command === "help" ||
1310
+ flags.help === true
1311
+ ) {
854
1312
  await printUsage(0);
855
1313
  return;
856
1314
  }