@simple-auth-kit/cli 1.4.2 → 1.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/registry.json +2 -0
- package/simple-auth-kit.ts +555 -124
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simple-auth-kit/cli",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "simple-auth-kit add <combo> — copies registry source (backend combos, admin consoles, mobile apps) into a consumer repo, shadcn-CLI-style. Nothing is ever installed as a runtime dependency of the consumer.",
|
|
6
6
|
"bin": {
|
package/registry.json
CHANGED
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"postInstall": [
|
|
37
37
|
"Set DATABASE_URL in .env (see .env.example)",
|
|
38
38
|
"Set AUTH_JWT_SECRET in .env — a base64-encoded 256-bit (or longer) secret, e.g.: openssl rand -base64 32. The app refuses to start without it; there is no default and nothing is generated for you. Changing it invalidates every outstanding token.",
|
|
39
|
+
"Add \"@/prisma/*\": [\"./generated/prisma/*\"] to your tsconfig.json's compilerOptions.paths — the copied source imports the generated Prisma client via this alias, since prisma.config.ts/prisma/ (and the client it generates) install at the project root regardless of where --path put everything else",
|
|
39
40
|
"Run: npx prisma generate",
|
|
40
41
|
"Run: npx prisma migrate deploy",
|
|
41
42
|
"Seed RBAC: add \"seed\": \"tsx src/lib/auth/src/seed.ts\" to your package.json scripts (adjust the path if you installed elsewhere), then run: npm run seed — it is idempotent, and seeds the permission catalog and the default admin/member roles. Nothing is authorized until it has run at least once.",
|
|
@@ -102,6 +103,7 @@
|
|
|
102
103
|
"postInstall": [
|
|
103
104
|
"Set DATABASE_URL in .env (see .env.example)",
|
|
104
105
|
"Set AUTH_JWT_SECRET in .env — a base64-encoded 256-bit (or longer) secret, e.g.: openssl rand -base64 32. The app refuses to start without it; there is no default and nothing is generated for you. Changing it invalidates every outstanding token.",
|
|
106
|
+
"Add \"@/prisma/*\": [\"./generated/prisma/*\"] to your tsconfig.json's compilerOptions.paths — the copied source imports the generated Prisma client via this alias, since prisma.config.ts/prisma/ (and the client it generates) install at the project root regardless of where --path put everything else",
|
|
105
107
|
"Run: npx prisma generate",
|
|
106
108
|
"Run: npx prisma migrate deploy",
|
|
107
109
|
"Seed RBAC: add \"seed\": \"tsx src/lib/auth/src/seed.ts\" to your package.json scripts (adjust the path if you installed elsewhere), then run: npm run seed — it is idempotent, and seeds the permission catalog and the default admin/member roles. Nothing is authorized until it has run at least once.",
|
package/simple-auth-kit.ts
CHANGED
|
@@ -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,
|
|
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 {
|
|
26
|
-
|
|
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"))
|
|
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> = {
|
|
43
|
-
|
|
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([
|
|
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 = {
|
|
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 =>
|
|
90
|
-
|
|
91
|
-
|
|
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 =>
|
|
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"
|
|
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(
|
|
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(
|
|
167
|
-
|
|
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(
|
|
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:
|
|
177
|
-
|
|
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(
|
|
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 (
|
|
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(
|
|
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))
|
|
205
|
-
|
|
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(
|
|
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
|
|
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(
|
|
251
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
274
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
396
|
+
return flags.workspaces === true
|
|
397
|
+
? "workspaces"
|
|
398
|
+
: (flagString(flags.variant) ?? DEFAULT_VARIANT);
|
|
296
399
|
}
|
|
297
400
|
|
|
298
|
-
function printInstallSummary(
|
|
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(
|
|
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(
|
|
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 = [
|
|
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)
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
|
|
341
|
-
|
|
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(
|
|
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
|
|
378
|
-
|
|
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:
|
|
519
|
+
to: "@/prisma/client.js",
|
|
385
520
|
},
|
|
386
521
|
]
|
|
387
522
|
: undefined;
|
|
388
523
|
|
|
389
|
-
return {
|
|
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: {
|
|
409
|
-
|
|
410
|
-
|
|
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,90 @@ async function runMergeCopy(
|
|
|
421
581
|
collectDiffs: opts.collectDiffs,
|
|
422
582
|
};
|
|
423
583
|
|
|
424
|
-
await copyDir(
|
|
425
|
-
|
|
426
|
-
|
|
584
|
+
await copyDir(
|
|
585
|
+
join(REGISTRY_ROOT, registry.core.dir),
|
|
586
|
+
join(plan.destRoot, "core"),
|
|
587
|
+
copyOpts,
|
|
588
|
+
plan.destRoot,
|
|
589
|
+
result,
|
|
590
|
+
);
|
|
591
|
+
await copyDir(
|
|
592
|
+
plan.sharedDir,
|
|
593
|
+
plan.destRoot,
|
|
594
|
+
{ ...copyOpts, neverCopy: plan.skipFromShared },
|
|
595
|
+
plan.destRoot,
|
|
596
|
+
result,
|
|
597
|
+
);
|
|
598
|
+
await copyDir(
|
|
599
|
+
plan.variantDir,
|
|
600
|
+
plan.destRoot,
|
|
601
|
+
{ ...copyOpts, neverCopy: plan.skipFromVariant },
|
|
602
|
+
plan.destRoot,
|
|
603
|
+
result,
|
|
604
|
+
);
|
|
427
605
|
|
|
428
606
|
if (plan.orm) {
|
|
429
|
-
await copyOneFile(
|
|
430
|
-
|
|
607
|
+
await copyOneFile(
|
|
608
|
+
join(plan.sharedDir, plan.orm.configFile),
|
|
609
|
+
join(targetRoot, plan.orm.configFile),
|
|
610
|
+
plan.destRoot,
|
|
611
|
+
copyOpts,
|
|
612
|
+
result,
|
|
613
|
+
);
|
|
614
|
+
await copyDir(
|
|
615
|
+
join(plan.variantDir, plan.orm.dataDir),
|
|
616
|
+
join(targetRoot, plan.orm.dataDir),
|
|
617
|
+
copyOpts,
|
|
618
|
+
plan.destRoot,
|
|
619
|
+
result,
|
|
620
|
+
);
|
|
431
621
|
}
|
|
432
622
|
|
|
433
623
|
// 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, {
|
|
624
|
+
const pruned = await pruneRemovedFiles(plan.destRoot, plan.previous, result, {
|
|
625
|
+
force: opts.force,
|
|
626
|
+
ignore: plan.config.ignore,
|
|
627
|
+
dryRun: opts.dryRun,
|
|
628
|
+
});
|
|
435
629
|
return { result, pruned };
|
|
436
630
|
}
|
|
437
631
|
|
|
438
|
-
async function installMerge(
|
|
632
|
+
async function installMerge(
|
|
633
|
+
comboName: string,
|
|
634
|
+
combo: ComboEntry,
|
|
635
|
+
variant: string,
|
|
636
|
+
registry: Registry,
|
|
637
|
+
targetRoot: string,
|
|
638
|
+
flags: Record<string, string | true>,
|
|
639
|
+
) {
|
|
439
640
|
const lock = await loadLock(targetRoot);
|
|
440
641
|
const previous = lock.files ?? {};
|
|
441
642
|
const force = flags.force === true;
|
|
442
643
|
const checkOnly = flags.check === true;
|
|
443
644
|
|
|
444
|
-
const plan = await buildMergePlan(
|
|
645
|
+
const plan = await buildMergePlan(
|
|
646
|
+
combo,
|
|
647
|
+
variant,
|
|
648
|
+
targetRoot,
|
|
649
|
+
flags,
|
|
650
|
+
previous,
|
|
651
|
+
);
|
|
445
652
|
|
|
446
653
|
if (checkOnly) {
|
|
447
|
-
const { result, pruned } = await runMergeCopy(plan, registry, targetRoot, {
|
|
448
|
-
|
|
654
|
+
const { result, pruned } = await runMergeCopy(plan, registry, targetRoot, {
|
|
655
|
+
force,
|
|
656
|
+
forcePaths: new Set(),
|
|
657
|
+
dryRun: true,
|
|
658
|
+
});
|
|
659
|
+
console.log(
|
|
660
|
+
`\nCheck only — nothing written. "${comboName}" (${variant} variant) in ${plan.installPath} (alias ${plan.alias}):`,
|
|
661
|
+
);
|
|
449
662
|
printInstallSummary(result, pruned);
|
|
450
|
-
const silent =
|
|
663
|
+
const silent =
|
|
664
|
+
!result.updated.length &&
|
|
665
|
+
!result.skipped.length &&
|
|
666
|
+
!pruned.removed.length &&
|
|
667
|
+
!pruned.keptModified.length;
|
|
451
668
|
if (silent) console.log(`\nUp to date — nothing would change.`);
|
|
452
669
|
return;
|
|
453
670
|
}
|
|
@@ -455,19 +672,45 @@ async function installMerge(comboName: string, combo: ComboEntry, variant: strin
|
|
|
455
672
|
// Interactive + no --force: find locally-modified conflicts first (a dry run, nothing written),
|
|
456
673
|
// and let the user pick which — if any — to overwrite anyway, shadcn-"this file already exists"
|
|
457
674
|
// style, instead of the non-interactive default of silently leaving all of them alone.
|
|
458
|
-
const forcePaths = await resolveForcePaths(
|
|
459
|
-
|
|
675
|
+
const forcePaths = await resolveForcePaths(
|
|
676
|
+
() =>
|
|
677
|
+
runMergeCopy(plan, registry, targetRoot, {
|
|
678
|
+
force: false,
|
|
679
|
+
forcePaths: new Set(),
|
|
680
|
+
dryRun: true,
|
|
681
|
+
}),
|
|
682
|
+
flags,
|
|
683
|
+
);
|
|
684
|
+
const { result, pruned } = await runMergeCopy(plan, registry, targetRoot, {
|
|
685
|
+
force,
|
|
686
|
+
forcePaths,
|
|
687
|
+
dryRun: false,
|
|
688
|
+
});
|
|
460
689
|
|
|
461
690
|
await writeFile(
|
|
462
691
|
join(targetRoot, "auth.lock.json"),
|
|
463
|
-
JSON.stringify(
|
|
692
|
+
JSON.stringify(
|
|
693
|
+
{
|
|
694
|
+
...lock,
|
|
695
|
+
combo: comboName,
|
|
696
|
+
variant,
|
|
697
|
+
installedAt: new Date().toISOString(),
|
|
698
|
+
files: result.manifest,
|
|
699
|
+
},
|
|
700
|
+
null,
|
|
701
|
+
2,
|
|
702
|
+
) + "\n",
|
|
464
703
|
"utf8",
|
|
465
704
|
);
|
|
466
705
|
|
|
467
|
-
console.log(
|
|
706
|
+
console.log(
|
|
707
|
+
`\nInstalled "${comboName}" (${variant} variant) into ${plan.installPath} (alias ${plan.alias})`,
|
|
708
|
+
);
|
|
468
709
|
printInstallSummary(result, pruned);
|
|
469
710
|
|
|
470
|
-
const peerDeps = [
|
|
711
|
+
const peerDeps = [
|
|
712
|
+
...new Set([...registry.core.peerDependencies, ...combo.peerDependencies]),
|
|
713
|
+
];
|
|
471
714
|
console.log(`\nPeer dependencies: ${peerDeps.join(" ")}`);
|
|
472
715
|
if (result.updated.length) {
|
|
473
716
|
await installDependencies(targetRoot, peerDeps, flags);
|
|
@@ -480,7 +723,13 @@ async function installMerge(comboName: string, combo: ComboEntry, variant: strin
|
|
|
480
723
|
/** "scaffold" install — admin/mobile apps materialized as a whole standalone project directly
|
|
481
724
|
* into targetRoot: no core layer, no alias rewrite, package.json is real content (copied and
|
|
482
725
|
* name/description-templated), not the registry's own dev wiring. */
|
|
483
|
-
async function installScaffold(
|
|
726
|
+
async function installScaffold(
|
|
727
|
+
comboName: string,
|
|
728
|
+
combo: ComboEntry,
|
|
729
|
+
variant: string,
|
|
730
|
+
targetRoot: string,
|
|
731
|
+
flags: Record<string, string | true>,
|
|
732
|
+
) {
|
|
484
733
|
const lock = await loadLock(targetRoot);
|
|
485
734
|
const previous = lock.files ?? {};
|
|
486
735
|
const force = flags.force === true;
|
|
@@ -498,24 +747,65 @@ async function installScaffold(comboName: string, combo: ComboEntry, variant: st
|
|
|
498
747
|
|
|
499
748
|
const comboDir = join(REGISTRY_ROOT, combo.dir);
|
|
500
749
|
|
|
501
|
-
const runCopy = async (opts: {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
const
|
|
750
|
+
const runCopy = async (opts: {
|
|
751
|
+
force: boolean;
|
|
752
|
+
forcePaths: Set<string>;
|
|
753
|
+
dryRun: boolean;
|
|
754
|
+
}) => {
|
|
755
|
+
const result: CopyResult = {
|
|
756
|
+
manifest: {},
|
|
757
|
+
skipped: [],
|
|
758
|
+
ignored: [],
|
|
759
|
+
updated: [],
|
|
760
|
+
};
|
|
761
|
+
const copyOpts = {
|
|
762
|
+
previous,
|
|
763
|
+
force: opts.force,
|
|
764
|
+
forcePaths: opts.forcePaths,
|
|
765
|
+
neverCopy: SCAFFOLD_NEVER_COPY,
|
|
766
|
+
dryRun: opts.dryRun,
|
|
767
|
+
};
|
|
768
|
+
await copyDir(
|
|
769
|
+
join(comboDir, combo.sharedDir ?? "shared"),
|
|
770
|
+
targetRoot,
|
|
771
|
+
copyOpts,
|
|
772
|
+
targetRoot,
|
|
773
|
+
result,
|
|
774
|
+
);
|
|
775
|
+
await copyDir(
|
|
776
|
+
join(comboDir, combo.variantsDir ?? "variants", variant),
|
|
777
|
+
targetRoot,
|
|
778
|
+
copyOpts,
|
|
779
|
+
targetRoot,
|
|
780
|
+
result,
|
|
781
|
+
);
|
|
782
|
+
const pruned = await pruneRemovedFiles(targetRoot, previous, result, {
|
|
783
|
+
force: opts.force,
|
|
784
|
+
dryRun: opts.dryRun,
|
|
785
|
+
});
|
|
507
786
|
return { result, pruned };
|
|
508
787
|
};
|
|
509
788
|
|
|
510
789
|
// Same shadcn-style "this file already exists, overwrite?" prompt as installMerge — see
|
|
511
790
|
// resolveForcePaths. A no-op dry run on a genuinely fresh install (nothing to conflict with).
|
|
512
|
-
const forcePaths = await resolveForcePaths(
|
|
513
|
-
|
|
791
|
+
const forcePaths = await resolveForcePaths(
|
|
792
|
+
() => runCopy({ force: false, forcePaths: new Set(), dryRun: true }),
|
|
793
|
+
flags,
|
|
794
|
+
);
|
|
795
|
+
const { result, pruned } = await runCopy({
|
|
796
|
+
force,
|
|
797
|
+
forcePaths,
|
|
798
|
+
dryRun: false,
|
|
799
|
+
});
|
|
514
800
|
|
|
515
801
|
const appName = flagString(flags.name) ?? basename(targetRoot);
|
|
516
802
|
|
|
517
803
|
if (combo.nativeIdentity) {
|
|
518
|
-
const renamed = await renameNative(
|
|
804
|
+
const renamed = await renameNative(
|
|
805
|
+
targetRoot,
|
|
806
|
+
combo.nativeIdentity,
|
|
807
|
+
appName,
|
|
808
|
+
);
|
|
519
809
|
result.manifest = reconcileManifest(result.manifest, renamed);
|
|
520
810
|
}
|
|
521
811
|
|
|
@@ -533,11 +823,23 @@ async function installScaffold(comboName: string, combo: ComboEntry, variant: st
|
|
|
533
823
|
|
|
534
824
|
await writeFile(
|
|
535
825
|
join(targetRoot, "auth.lock.json"),
|
|
536
|
-
JSON.stringify(
|
|
826
|
+
JSON.stringify(
|
|
827
|
+
{
|
|
828
|
+
...lock,
|
|
829
|
+
combo: comboName,
|
|
830
|
+
variant,
|
|
831
|
+
installedAt: new Date().toISOString(),
|
|
832
|
+
files: result.manifest,
|
|
833
|
+
},
|
|
834
|
+
null,
|
|
835
|
+
2,
|
|
836
|
+
) + "\n",
|
|
537
837
|
"utf8",
|
|
538
838
|
);
|
|
539
839
|
|
|
540
|
-
console.log(
|
|
840
|
+
console.log(
|
|
841
|
+
`\nGenerated "${appName}" (${comboName}, ${variant} variant) into ${targetRoot}`,
|
|
842
|
+
);
|
|
541
843
|
printInstallSummary(result, pruned);
|
|
542
844
|
if (result.updated.length) {
|
|
543
845
|
// Dependencies are already declared in package.json — no specific packages to name, just
|
|
@@ -549,7 +851,14 @@ async function installScaffold(comboName: string, combo: ComboEntry, variant: st
|
|
|
549
851
|
printPostInstallNotes(combo, variant);
|
|
550
852
|
}
|
|
551
853
|
|
|
552
|
-
async function installCombo(
|
|
854
|
+
async function installCombo(
|
|
855
|
+
comboName: string,
|
|
856
|
+
combo: ComboEntry,
|
|
857
|
+
variant: string,
|
|
858
|
+
registry: Registry,
|
|
859
|
+
targetRoot: string,
|
|
860
|
+
flags: Record<string, string | true>,
|
|
861
|
+
) {
|
|
553
862
|
if (comboInstallMode(combo) === "scaffold") {
|
|
554
863
|
await installScaffold(comboName, combo, variant, targetRoot, flags);
|
|
555
864
|
} else {
|
|
@@ -557,11 +866,17 @@ async function installCombo(comboName: string, combo: ComboEntry, variant: strin
|
|
|
557
866
|
}
|
|
558
867
|
}
|
|
559
868
|
|
|
560
|
-
async function cmdAdd(
|
|
869
|
+
async function cmdAdd(
|
|
870
|
+
comboName: string,
|
|
871
|
+
targetRoot: string,
|
|
872
|
+
flags: Record<string, string | true>,
|
|
873
|
+
) {
|
|
561
874
|
const registry = await loadRegistry();
|
|
562
875
|
const combo = registry.combos[comboName];
|
|
563
876
|
if (!combo) {
|
|
564
|
-
console.error(
|
|
877
|
+
console.error(
|
|
878
|
+
`Unknown combo "${comboName}". Available: ${Object.keys(registry.combos).join(", ")}`,
|
|
879
|
+
);
|
|
565
880
|
process.exitCode = 1;
|
|
566
881
|
return;
|
|
567
882
|
}
|
|
@@ -575,15 +890,22 @@ async function cmdAdd(comboName: string, targetRoot: string, flags: Record<strin
|
|
|
575
890
|
await installCombo(comboName, combo, variant, registry, targetRoot, flags);
|
|
576
891
|
}
|
|
577
892
|
|
|
578
|
-
function combosByKind(
|
|
579
|
-
|
|
893
|
+
function combosByKind(
|
|
894
|
+
registry: Registry,
|
|
895
|
+
kind: Kind,
|
|
896
|
+
): Array<[string, ComboEntry]> {
|
|
897
|
+
return Object.entries(registry.combos).filter(
|
|
898
|
+
([, combo]) => comboKind(combo) === kind,
|
|
899
|
+
);
|
|
580
900
|
}
|
|
581
901
|
|
|
582
902
|
const isTTY = () => Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
583
903
|
|
|
584
904
|
/** Wraps a single `prompts()` call so a cancel (Ctrl+C) surfaces as `null` instead of `undefined`,
|
|
585
905
|
* which prompts itself doesn't distinguish from "field asked but left empty". */
|
|
586
|
-
async function ask<T>(
|
|
906
|
+
async function ask<T>(
|
|
907
|
+
question: Parameters<typeof prompts>[0] & { name: string },
|
|
908
|
+
): Promise<T | null> {
|
|
587
909
|
const res = await prompts(question as never);
|
|
588
910
|
const value = (res as Record<string, unknown>)[question.name as string];
|
|
589
911
|
return value === undefined ? null : (value as T);
|
|
@@ -595,7 +917,14 @@ async function ask<T>(question: Parameters<typeof prompts>[0] & { name: string }
|
|
|
595
917
|
* choice applies to every kind picked this run, matching how `variants` is already a single
|
|
596
918
|
* cross-cutting concept rather than per-combo.
|
|
597
919
|
*/
|
|
598
|
-
async function resolvePlan(
|
|
920
|
+
async function resolvePlan(
|
|
921
|
+
registry: Registry,
|
|
922
|
+
flags: Record<string, string | true>,
|
|
923
|
+
): Promise<Array<{
|
|
924
|
+
comboName: string;
|
|
925
|
+
combo: ComboEntry;
|
|
926
|
+
variant: string;
|
|
927
|
+
}> | null> {
|
|
599
928
|
const flagKinds = flagList(flags.kind) as Kind[] | undefined;
|
|
600
929
|
const flagFrameworks = flagList(flags.framework);
|
|
601
930
|
const interactive = isTTY();
|
|
@@ -608,7 +937,10 @@ async function resolvePlan(registry: Registry, flags: Record<string, string | tr
|
|
|
608
937
|
type: "multiselect",
|
|
609
938
|
name: "kinds",
|
|
610
939
|
message: "What would you like to generate?",
|
|
611
|
-
choices: (Object.keys(KIND_LABELS) as Kind[]).map((value) => ({
|
|
940
|
+
choices: (Object.keys(KIND_LABELS) as Kind[]).map((value) => ({
|
|
941
|
+
title: KIND_LABELS[value],
|
|
942
|
+
value,
|
|
943
|
+
})),
|
|
612
944
|
min: 1,
|
|
613
945
|
instructions: false,
|
|
614
946
|
});
|
|
@@ -618,7 +950,9 @@ async function resolvePlan(registry: Registry, flags: Record<string, string | tr
|
|
|
618
950
|
}
|
|
619
951
|
kinds = picked;
|
|
620
952
|
} else {
|
|
621
|
-
console.error(
|
|
953
|
+
console.error(
|
|
954
|
+
"Non-interactive session: pass --kind api,admin,mobile (comma-separated), or a specific combo name (simple-auth-kit add <combo>).",
|
|
955
|
+
);
|
|
622
956
|
return null;
|
|
623
957
|
}
|
|
624
958
|
|
|
@@ -642,7 +976,9 @@ async function resolvePlan(registry: Registry, flags: Record<string, string | tr
|
|
|
642
976
|
|
|
643
977
|
let comboName = flagFrameworks?.[i];
|
|
644
978
|
if (comboName && !available.some(([name]) => name === comboName)) {
|
|
645
|
-
const match = available.find(
|
|
979
|
+
const match = available.find(
|
|
980
|
+
([name]) => name === comboName || name.endsWith(`-${comboName}`),
|
|
981
|
+
);
|
|
646
982
|
comboName = match?.[0];
|
|
647
983
|
}
|
|
648
984
|
|
|
@@ -662,7 +998,9 @@ async function resolvePlan(registry: Registry, flags: Record<string, string | tr
|
|
|
662
998
|
}
|
|
663
999
|
comboName = picked;
|
|
664
1000
|
} else {
|
|
665
|
-
console.error(
|
|
1001
|
+
console.error(
|
|
1002
|
+
`Multiple "${kind}" combos available (${available.map(([name]) => name).join(", ")}) — pass --framework to disambiguate.`,
|
|
1003
|
+
);
|
|
666
1004
|
return null;
|
|
667
1005
|
}
|
|
668
1006
|
}
|
|
@@ -682,7 +1020,12 @@ async function resolvePlan(registry: Registry, flags: Record<string, string | tr
|
|
|
682
1020
|
} else if (requestedVariantFlag === "base") {
|
|
683
1021
|
wantsWorkspaces = false;
|
|
684
1022
|
} else if (interactive) {
|
|
685
|
-
const picked = await ask<boolean>({
|
|
1023
|
+
const picked = await ask<boolean>({
|
|
1024
|
+
type: "confirm",
|
|
1025
|
+
name: "workspaces",
|
|
1026
|
+
message: "Include workspaces support?",
|
|
1027
|
+
initial: false,
|
|
1028
|
+
});
|
|
686
1029
|
if (picked === null) {
|
|
687
1030
|
console.log("Cancelled.");
|
|
688
1031
|
return null;
|
|
@@ -692,16 +1035,27 @@ async function resolvePlan(registry: Registry, flags: Record<string, string | tr
|
|
|
692
1035
|
wantsWorkspaces = false;
|
|
693
1036
|
}
|
|
694
1037
|
|
|
695
|
-
const selections: Array<{
|
|
1038
|
+
const selections: Array<{
|
|
1039
|
+
comboName: string;
|
|
1040
|
+
combo: ComboEntry;
|
|
1041
|
+
variant: string;
|
|
1042
|
+
}> = [];
|
|
696
1043
|
for (const pick of picks) {
|
|
697
|
-
const variant = resolveVariant(
|
|
1044
|
+
const variant = resolveVariant(
|
|
1045
|
+
pick.combo,
|
|
1046
|
+
pick.comboName,
|
|
1047
|
+
wantsWorkspaces ? "workspaces" : DEFAULT_VARIANT,
|
|
1048
|
+
);
|
|
698
1049
|
if (!variant) return null;
|
|
699
1050
|
selections.push({ ...pick, variant });
|
|
700
1051
|
}
|
|
701
1052
|
return selections;
|
|
702
1053
|
}
|
|
703
1054
|
|
|
704
|
-
async function cmdCreate(
|
|
1055
|
+
async function cmdCreate(
|
|
1056
|
+
targetRoot: string,
|
|
1057
|
+
flags: Record<string, string | true>,
|
|
1058
|
+
) {
|
|
705
1059
|
const registry = await loadRegistry();
|
|
706
1060
|
const selections = await resolvePlan(registry, flags);
|
|
707
1061
|
if (!selections) {
|
|
@@ -723,8 +1077,18 @@ async function cmdCreate(targetRoot: string, flags: Record<string, string | true
|
|
|
723
1077
|
// (they don't have their own package.json identity), but two scaffold apps both literally
|
|
724
1078
|
// named e.g. "combo-test" would collide if anything ever treats them as sibling packages
|
|
725
1079
|
// (a pnpm/npm workspace, for one). Suffix with the combo name once there's more than one.
|
|
726
|
-
const installFlags =
|
|
727
|
-
|
|
1080
|
+
const installFlags =
|
|
1081
|
+
namespaced && flagString(flags.name)
|
|
1082
|
+
? { ...flags, name: `${flagString(flags.name)}-${comboName}` }
|
|
1083
|
+
: flags;
|
|
1084
|
+
await installCombo(
|
|
1085
|
+
comboName,
|
|
1086
|
+
combo,
|
|
1087
|
+
variant,
|
|
1088
|
+
registry,
|
|
1089
|
+
installRoot,
|
|
1090
|
+
installFlags,
|
|
1091
|
+
);
|
|
728
1092
|
}
|
|
729
1093
|
}
|
|
730
1094
|
|
|
@@ -735,10 +1099,15 @@ async function cmdCreate(targetRoot: string, flags: Record<string, string | true
|
|
|
735
1099
|
* anything you have is left alone and reported, exactly like a first install. Pass `--force`
|
|
736
1100
|
* yourself if you really do want to overwrite local edits.
|
|
737
1101
|
*/
|
|
738
|
-
async function cmdUpdate(
|
|
1102
|
+
async function cmdUpdate(
|
|
1103
|
+
targetRoot: string,
|
|
1104
|
+
flags: Record<string, string | true>,
|
|
1105
|
+
) {
|
|
739
1106
|
const lock = await loadLock(targetRoot);
|
|
740
1107
|
if (!lock.combo || !lock.variant) {
|
|
741
|
-
console.error(
|
|
1108
|
+
console.error(
|
|
1109
|
+
`No auth.lock.json (or it's missing combo/variant) in ${targetRoot} — nothing to update. Use "add <combo>" for a first install.`,
|
|
1110
|
+
);
|
|
742
1111
|
process.exitCode = 1;
|
|
743
1112
|
return;
|
|
744
1113
|
}
|
|
@@ -746,7 +1115,9 @@ async function cmdUpdate(targetRoot: string, flags: Record<string, string | true
|
|
|
746
1115
|
const registry = await loadRegistry();
|
|
747
1116
|
const combo = registry.combos[lock.combo];
|
|
748
1117
|
if (!combo) {
|
|
749
|
-
console.error(
|
|
1118
|
+
console.error(
|
|
1119
|
+
`auth.lock.json names combo "${lock.combo}", which no longer exists in this registry.`,
|
|
1120
|
+
);
|
|
750
1121
|
process.exitCode = 1;
|
|
751
1122
|
return;
|
|
752
1123
|
}
|
|
@@ -758,12 +1129,16 @@ async function cmdUpdate(targetRoot: string, flags: Record<string, string | true
|
|
|
758
1129
|
}
|
|
759
1130
|
|
|
760
1131
|
if (flags.check === true && comboInstallMode(combo) === "scaffold") {
|
|
761
|
-
console.error(
|
|
1132
|
+
console.error(
|
|
1133
|
+
`--check isn't supported for "${lock.combo}" (a scaffold-mode combo) — only merge-mode (api) combos support a dry run.`,
|
|
1134
|
+
);
|
|
762
1135
|
process.exitCode = 1;
|
|
763
1136
|
return;
|
|
764
1137
|
}
|
|
765
1138
|
|
|
766
|
-
console.log(
|
|
1139
|
+
console.log(
|
|
1140
|
+
`Updating "${lock.combo}" (${variant} variant) in ${targetRoot}${flags.force === true ? " — --force: local edits will be overwritten" : ""}`,
|
|
1141
|
+
);
|
|
767
1142
|
await installCombo(lock.combo, combo, variant, registry, targetRoot, flags);
|
|
768
1143
|
}
|
|
769
1144
|
|
|
@@ -776,7 +1151,9 @@ async function cmdUpdate(targetRoot: string, flags: Record<string, string | true
|
|
|
776
1151
|
async function cmdDiff(targetRoot: string) {
|
|
777
1152
|
const lock = await loadLock(targetRoot);
|
|
778
1153
|
if (!lock.combo || !lock.variant) {
|
|
779
|
-
console.error(
|
|
1154
|
+
console.error(
|
|
1155
|
+
`No auth.lock.json (or it's missing combo/variant) in ${targetRoot} — nothing installed to diff.`,
|
|
1156
|
+
);
|
|
780
1157
|
process.exitCode = 1;
|
|
781
1158
|
return;
|
|
782
1159
|
}
|
|
@@ -784,13 +1161,17 @@ async function cmdDiff(targetRoot: string) {
|
|
|
784
1161
|
const registry = await loadRegistry();
|
|
785
1162
|
const combo = registry.combos[lock.combo];
|
|
786
1163
|
if (!combo) {
|
|
787
|
-
console.error(
|
|
1164
|
+
console.error(
|
|
1165
|
+
`auth.lock.json names combo "${lock.combo}", which no longer exists in this registry.`,
|
|
1166
|
+
);
|
|
788
1167
|
process.exitCode = 1;
|
|
789
1168
|
return;
|
|
790
1169
|
}
|
|
791
1170
|
|
|
792
1171
|
if (comboInstallMode(combo) === "scaffold") {
|
|
793
|
-
console.error(
|
|
1172
|
+
console.error(
|
|
1173
|
+
`diff isn't supported yet for "${lock.combo}" (a scaffold-mode combo) — only merge-mode (api) combos support it.`,
|
|
1174
|
+
);
|
|
794
1175
|
process.exitCode = 1;
|
|
795
1176
|
return;
|
|
796
1177
|
}
|
|
@@ -801,21 +1182,41 @@ async function cmdDiff(targetRoot: string) {
|
|
|
801
1182
|
return;
|
|
802
1183
|
}
|
|
803
1184
|
|
|
804
|
-
const plan = await buildMergePlan(
|
|
1185
|
+
const plan = await buildMergePlan(
|
|
1186
|
+
combo,
|
|
1187
|
+
variant,
|
|
1188
|
+
targetRoot,
|
|
1189
|
+
{},
|
|
1190
|
+
lock.files ?? {},
|
|
1191
|
+
);
|
|
805
1192
|
const diffs: DiffEntry[] = [];
|
|
806
1193
|
// force:true so a locally-modified file's difference is captured too — diff wants to show
|
|
807
1194
|
// everything that differs, not just what a real (non---force) install would apply; dryRun:true
|
|
808
1195
|
// so nothing is written.
|
|
809
|
-
await runMergeCopy(plan, registry, targetRoot, {
|
|
1196
|
+
await runMergeCopy(plan, registry, targetRoot, {
|
|
1197
|
+
force: true,
|
|
1198
|
+
forcePaths: new Set(),
|
|
1199
|
+
dryRun: true,
|
|
1200
|
+
collectDiffs: diffs,
|
|
1201
|
+
});
|
|
810
1202
|
|
|
811
1203
|
if (!diffs.length) {
|
|
812
|
-
console.log(
|
|
1204
|
+
console.log(
|
|
1205
|
+
`No differences — every tracked file in "${lock.combo}" (${variant} variant) matches the current registry.`,
|
|
1206
|
+
);
|
|
813
1207
|
return;
|
|
814
1208
|
}
|
|
815
1209
|
|
|
816
1210
|
console.log(`${diffs.length} file(s) differ from the current registry:`);
|
|
817
1211
|
for (const d of diffs) {
|
|
818
|
-
const patch = createTwoFilesPatch(
|
|
1212
|
+
const patch = createTwoFilesPatch(
|
|
1213
|
+
d.path,
|
|
1214
|
+
d.path,
|
|
1215
|
+
d.oldContent ?? "",
|
|
1216
|
+
d.newContent,
|
|
1217
|
+
"installed",
|
|
1218
|
+
"registry",
|
|
1219
|
+
);
|
|
819
1220
|
process.stdout.write(`\n${patch}`);
|
|
820
1221
|
}
|
|
821
1222
|
}
|
|
@@ -823,25 +1224,50 @@ async function cmdDiff(targetRoot: string) {
|
|
|
823
1224
|
async function printUsage(exitCode: number) {
|
|
824
1225
|
const registry = await loadRegistry();
|
|
825
1226
|
console.log("Usage: simple-auth-kit <init|add|update|diff> [...]");
|
|
826
|
-
console.log(
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
console.log(
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
console.log(
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
console.log(
|
|
836
|
-
|
|
1227
|
+
console.log(
|
|
1228
|
+
` init [--config-only] [--kind ...] [--into <path>] (fresh setup: guided "what do you need" picker, like bare "add")`,
|
|
1229
|
+
);
|
|
1230
|
+
console.log(
|
|
1231
|
+
` --config-only: just write .simple-auth-kit.json and stop, no install`,
|
|
1232
|
+
);
|
|
1233
|
+
console.log(
|
|
1234
|
+
` add <combo> [--workspaces] [--force] [--skip-install] [--into <path>] [--path <dir>] [--alias <alias>]`,
|
|
1235
|
+
);
|
|
1236
|
+
console.log(
|
|
1237
|
+
` add [--kind api,admin,mobile] [--framework <name>,...] [--workspaces] [--into <path>] [--name <appName>]`,
|
|
1238
|
+
);
|
|
1239
|
+
console.log(
|
|
1240
|
+
` (bare "add", or "add" with --kind but no combo, launches a guided prompt for whatever's missing)`,
|
|
1241
|
+
);
|
|
1242
|
+
console.log(
|
|
1243
|
+
` update [--check] [--force] [--skip-install] [--into <path>] (re-installs whatever combo+variant auth.lock.json already records)`,
|
|
1244
|
+
);
|
|
1245
|
+
console.log(
|
|
1246
|
+
` --check: report what would change, without writing anything (merge-mode combos only)`,
|
|
1247
|
+
);
|
|
1248
|
+
console.log(
|
|
1249
|
+
` diff [--into <path>] (shows the actual content diff for every tracked file that differs from the current registry — read-only; merge-mode combos only)`,
|
|
1250
|
+
);
|
|
1251
|
+
console.log(
|
|
1252
|
+
` In a TTY, without --force or --check: a file changed locally since install prompts to overwrite, per file.`,
|
|
1253
|
+
);
|
|
1254
|
+
console.log(
|
|
1255
|
+
` --skip-install: don't run the package manager after copying files — the default is to install for you, shadcn-\`add\`-style.`,
|
|
1256
|
+
);
|
|
1257
|
+
console.log(
|
|
1258
|
+
` --pm <npm|pnpm|yarn|bun>: which package manager to install with — default: detected from a lockfile in the target directory, npm if none found.`,
|
|
1259
|
+
);
|
|
837
1260
|
console.log(`\nAvailable combos:`);
|
|
838
1261
|
for (const kind of ["api", "admin", "mobile"] as Kind[]) {
|
|
839
1262
|
const names = combosByKind(registry, kind).map(([name]) => name);
|
|
840
|
-
if (names.length)
|
|
1263
|
+
if (names.length)
|
|
1264
|
+
console.log(` ${KIND_LABELS[kind]}: ${names.join(", ")}`);
|
|
841
1265
|
}
|
|
842
1266
|
console.log(`\nVariants (choose one at install time):`);
|
|
843
1267
|
for (const [name, variant] of Object.entries(registry.variants)) {
|
|
844
|
-
console.log(
|
|
1268
|
+
console.log(
|
|
1269
|
+
` ${name}${variant.flag ? ` (${variant.flag})` : " (default)"} — ${variant.description}`,
|
|
1270
|
+
);
|
|
845
1271
|
}
|
|
846
1272
|
process.exitCode = exitCode;
|
|
847
1273
|
}
|
|
@@ -850,7 +1276,12 @@ async function main() {
|
|
|
850
1276
|
const { command, positional, flags } = parseArgs(process.argv.slice(2));
|
|
851
1277
|
const targetRoot = resolve(process.cwd(), flagString(flags.into) ?? ".");
|
|
852
1278
|
|
|
853
|
-
if (
|
|
1279
|
+
if (
|
|
1280
|
+
command === "--help" ||
|
|
1281
|
+
command === "-h" ||
|
|
1282
|
+
command === "help" ||
|
|
1283
|
+
flags.help === true
|
|
1284
|
+
) {
|
|
854
1285
|
await printUsage(0);
|
|
855
1286
|
return;
|
|
856
1287
|
}
|