@simple-auth-kit/cli 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -7
- package/lib/copy.ts +17 -0
- package/package.json +2 -1
- package/simple-auth-kit.ts +220 -78
package/README.md
CHANGED
|
@@ -21,7 +21,9 @@ npx @simple-auth-kit/cli init # fresh setup: guided "what
|
|
|
21
21
|
npx @simple-auth-kit/cli add <combo> [--workspaces] # install a specific combo
|
|
22
22
|
npx @simple-auth-kit/cli add # guided picker, same flow as init (no config write)
|
|
23
23
|
npx @simple-auth-kit/cli update [--check] # re-installs whatever add last recorded — no args needed
|
|
24
|
-
npx @simple-auth-kit/cli
|
|
24
|
+
npx @simple-auth-kit/cli diff # shows the actual content diff for every file that differs from the registry (api combos only)
|
|
25
|
+
npx @simple-auth-kit/cli # bare, in a real terminal: same guided picker as `add`
|
|
26
|
+
npx @simple-auth-kit/cli --help # full command/flag reference + available combos
|
|
25
27
|
```
|
|
26
28
|
|
|
27
29
|
`--into <path>` targets any directory (defaults to the current one). In a real terminal,
|
|
@@ -29,15 +31,37 @@ without `--force` or `--check`, a file that's changed locally since install prom
|
|
|
29
31
|
per file — before it's overwritten; outside a TTY (CI/scripts) it's left alone by default and
|
|
30
32
|
reported at the end, same as `--force` always applying and `--check` never writing anything.
|
|
31
33
|
|
|
34
|
+
**Dependencies are installed for you**, shadcn-`add`-style — no separate `npm install` step.
|
|
35
|
+
The package manager is auto-detected from a lockfile already in the target directory (npm if
|
|
36
|
+
none found); override it with `--pm <npm|pnpm|yarn|bun>`, or skip the install entirely with
|
|
37
|
+
`--skip-install` if you'd rather review `package.json` first.
|
|
38
|
+
|
|
32
39
|
## Available combos
|
|
33
40
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
Add `--workspaces` to any of these for the workspaces variant.
|
|
42
|
+
|
|
43
|
+
**`api`** — merged into `src/lib/auth` of an existing project (the Prisma/Drizzle config +
|
|
44
|
+
schema land at the project root instead — see below):
|
|
45
|
+
```bash
|
|
46
|
+
npx @simple-auth-kit/cli add nestjs-prisma --into .
|
|
47
|
+
npx @simple-auth-kit/cli add nestjs-drizzle --into .
|
|
48
|
+
npx @simple-auth-kit/cli add express-prisma --into .
|
|
49
|
+
npx @simple-auth-kit/cli add express-drizzle --into .
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**`admin`** — a whole new standalone app, scaffolded at the target (refuses a non-empty target
|
|
53
|
+
unless `--force`):
|
|
54
|
+
```bash
|
|
55
|
+
npx @simple-auth-kit/cli add admin-nextjs --into ./admin
|
|
56
|
+
npx @simple-auth-kit/cli add admin-react --into ./admin
|
|
57
|
+
```
|
|
39
58
|
|
|
40
|
-
|
|
59
|
+
**`mobile`** — a whole new standalone app, scaffolded at the target (same non-empty-target
|
|
60
|
+
rule):
|
|
61
|
+
```bash
|
|
62
|
+
npx @simple-auth-kit/cli add mobile-expo --into ./mobile
|
|
63
|
+
npx @simple-auth-kit/cli add mobile-bare-rn --into ./mobile
|
|
64
|
+
```
|
|
41
65
|
|
|
42
66
|
## Your own database models are safe
|
|
43
67
|
|
package/lib/copy.ts
CHANGED
|
@@ -48,6 +48,22 @@ export interface CopyOptions {
|
|
|
48
48
|
* deleting anything on disk — for a "what would change" check before actually applying it.
|
|
49
49
|
*/
|
|
50
50
|
dryRun?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* When set, every file whose on-disk content differs from what's about to be written gets a
|
|
53
|
+
* `{path, oldContent, newContent}` entry pushed onto this array — for rendering an actual
|
|
54
|
+
* diff (see `cmdDiff`), not just a filename list. Typically paired with `force: true` and
|
|
55
|
+
* `dryRun: true` so a locally-modified file's difference is captured too, not silently
|
|
56
|
+
* skipped the way a real install would.
|
|
57
|
+
*/
|
|
58
|
+
collectDiffs?: DiffEntry[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface DiffEntry {
|
|
62
|
+
/** Path relative to destRoot, POSIX separators — same form as CopyResult's arrays. */
|
|
63
|
+
path: string;
|
|
64
|
+
/** null when the file doesn't exist on disk yet (a brand-new file). */
|
|
65
|
+
oldContent: string | null;
|
|
66
|
+
newContent: string;
|
|
51
67
|
}
|
|
52
68
|
|
|
53
69
|
export interface CopyResult {
|
|
@@ -111,6 +127,7 @@ export async function copyOneFile(srcPath: string, destPath: string, destRoot: s
|
|
|
111
127
|
|
|
112
128
|
result.updated.push(rel);
|
|
113
129
|
result.manifest[rel] = newHash;
|
|
130
|
+
opts.collectDiffs?.push({ path: rel, oldContent: existing, newContent: content });
|
|
114
131
|
if (opts.dryRun) return;
|
|
115
132
|
|
|
116
133
|
await mkdir(dirname(destPath), { recursive: true });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simple-auth-kit/cli",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
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": {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"prepack": "node scripts/bundle-registry.mjs"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
+
"diff": "^9.0.0",
|
|
23
24
|
"prompts": "^2.4.2",
|
|
24
25
|
"tsx": "^4.23.11"
|
|
25
26
|
},
|
package/simple-auth-kit.ts
CHANGED
|
@@ -16,11 +16,13 @@
|
|
|
16
16
|
// workspaces support — or reads the same choices from --kind/--framework/--workspaces for
|
|
17
17
|
// non-interactive/scripted use.
|
|
18
18
|
import { existsSync } from "node:fs";
|
|
19
|
+
import { spawnSync } from "node:child_process";
|
|
19
20
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
20
21
|
import { access, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
21
22
|
import { fileURLToPath } from "node:url";
|
|
23
|
+
import { createTwoFilesPatch } from "diff";
|
|
22
24
|
import prompts from "prompts";
|
|
23
|
-
import { copyDir, copyOneFile, CopyResult, NEVER_COPY, pruneRemovedFiles, SCAFFOLD_NEVER_COPY, sha256, toPosix } from "./lib/copy.js";
|
|
25
|
+
import { copyDir, copyOneFile, CopyOptions, CopyResult, DiffEntry, NEVER_COPY, pruneRemovedFiles, SCAFFOLD_NEVER_COPY, sha256, toPosix } from "./lib/copy.js";
|
|
24
26
|
import { reconcileManifest, renameNative, type NativeIdentity } from "./lib/rename-native.js";
|
|
25
27
|
|
|
26
28
|
const CLI_DIR = dirname(fileURLToPath(import.meta.url));
|
|
@@ -41,7 +43,7 @@ const KIND_LABELS: Record<Kind, string> = { api: "API (backend)", admin: "Admin
|
|
|
41
43
|
const KIND_FRAMEWORK_NOUN: Record<Kind, string> = { api: "API stack", admin: "admin framework", mobile: "mobile framework" };
|
|
42
44
|
|
|
43
45
|
/** Flags that take no value. Everything else consumes the next argv entry. */
|
|
44
|
-
const BOOLEAN_FLAGS = new Set(["workspaces", "force", "check", "config-only"]);
|
|
46
|
+
const BOOLEAN_FLAGS = new Set(["workspaces", "force", "check", "config-only", "skip-install", "help"]);
|
|
45
47
|
|
|
46
48
|
interface SimpleAuthKitConfig {
|
|
47
49
|
path: string;
|
|
@@ -178,6 +180,47 @@ async function resolveForcePaths(runDry: () => Promise<{ result: CopyResult }>,
|
|
|
178
180
|
return new Set(picked ?? []);
|
|
179
181
|
}
|
|
180
182
|
|
|
183
|
+
const PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"] as const;
|
|
184
|
+
type PackageManager = (typeof PACKAGE_MANAGERS)[number];
|
|
185
|
+
|
|
186
|
+
/** Detected from whichever lockfile is already sitting in targetRoot — npm if none match,
|
|
187
|
+
* matching what a fresh `npm install`-first project would have. Overridden by `--pm` when
|
|
188
|
+
* given, so e.g. a first-ever install (no lockfile yet to detect from) can still pick pnpm. */
|
|
189
|
+
function detectPackageManager(targetRoot: string, flags: Record<string, string | true>): PackageManager {
|
|
190
|
+
const requested = flagString(flags.pm);
|
|
191
|
+
if (requested) {
|
|
192
|
+
if ((PACKAGE_MANAGERS as readonly string[]).includes(requested)) return requested as PackageManager;
|
|
193
|
+
console.error(`--pm "${requested}" isn't one of ${PACKAGE_MANAGERS.join(", ")} — falling back to auto-detection.`);
|
|
194
|
+
}
|
|
195
|
+
if (existsSync(join(targetRoot, "pnpm-lock.yaml"))) return "pnpm";
|
|
196
|
+
if (existsSync(join(targetRoot, "yarn.lock"))) return "yarn";
|
|
197
|
+
if (existsSync(join(targetRoot, "bun.lockb")) || existsSync(join(targetRoot, "bun.lock"))) return "bun";
|
|
198
|
+
return "npm";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Actually runs the dependency install, shadcn-`add`-style, instead of just printing the
|
|
203
|
+
* command and leaving it to the consumer. `deps` empty means "just install whatever's in
|
|
204
|
+
* package.json" (the scaffold-mode case — dependencies are already declared, nothing to name).
|
|
205
|
+
* Skipped entirely under `--skip-install`, or when `deps` is non-empty but there's nothing new
|
|
206
|
+
* to add (an update that touched no files has nothing worth re-installing for). `--pm
|
|
207
|
+
* <npm|pnpm|yarn|bun>` picks the tool explicitly instead of auto-detecting it from a lockfile.
|
|
208
|
+
*/
|
|
209
|
+
function installDependencies(targetRoot: string, deps: string[], flags: Record<string, string | true>): void {
|
|
210
|
+
if (flags["skip-install"] === true) return;
|
|
211
|
+
|
|
212
|
+
const pm = detectPackageManager(targetRoot, flags);
|
|
213
|
+
// "install everything in package.json" (zero deps named) is the same bare verb across all
|
|
214
|
+
// four; naming specific packages is "install <pkgs>" for npm, "add <pkgs>" for the others.
|
|
215
|
+
const args = deps.length ? [pm === "npm" ? "install" : "add", ...deps] : ["install"];
|
|
216
|
+
|
|
217
|
+
console.log(`\nInstalling dependencies (${pm})...`);
|
|
218
|
+
const result = spawnSync(pm, args, { cwd: targetRoot, stdio: "inherit" });
|
|
219
|
+
if (result.status !== 0) {
|
|
220
|
+
console.error(`\n${pm} install exited with an error — run it yourself: cd ${targetRoot} && ${pm} ${args.join(" ")}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
181
224
|
/**
|
|
182
225
|
* The fresh-setup entry point: writes .simple-auth-kit.json (so merge-mode combos have an
|
|
183
226
|
* install path/alias to anchor to), then launches the same guided "what do you need"
|
|
@@ -264,28 +307,28 @@ const ORM_LAYOUTS: { configFile: string; dataDir: string; generatedClientImport?
|
|
|
264
307
|
{ configFile: "drizzle.config.ts", dataDir: "drizzle" },
|
|
265
308
|
];
|
|
266
309
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
310
|
+
interface MergePlan {
|
|
311
|
+
destRoot: string;
|
|
312
|
+
installPath: string;
|
|
313
|
+
alias: string;
|
|
314
|
+
sharedDir: string;
|
|
315
|
+
variantDir: string;
|
|
316
|
+
orm: (typeof ORM_LAYOUTS)[number] | null;
|
|
317
|
+
skipFromShared: Set<string>;
|
|
318
|
+
skipFromVariant: Set<string>;
|
|
319
|
+
extraRewrites: { from: string; to: string }[] | undefined;
|
|
320
|
+
config: SimpleAuthKitConfig;
|
|
321
|
+
previous: Record<string, string>;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Everything about a merge-mode install that doesn't depend on force/dryRun/forcePaths —
|
|
325
|
+
* computed once, shared by installMerge's real run and cmdDiff's read-only one. */
|
|
326
|
+
async function buildMergePlan(combo: ComboEntry, variant: string, targetRoot: string, flags: Record<string, string | true>, previous: Record<string, string>): Promise<MergePlan> {
|
|
279
327
|
const config = await loadConfig(targetRoot);
|
|
280
328
|
const installPath = flagString(flags.path) ?? config.path;
|
|
281
329
|
const alias = flagString(flags.alias) ?? config.alias;
|
|
282
330
|
const destRoot = resolve(targetRoot, installPath);
|
|
283
331
|
|
|
284
|
-
const lock = await loadLock(targetRoot);
|
|
285
|
-
const previous = lock.files ?? {};
|
|
286
|
-
const force = flags.force === true;
|
|
287
|
-
const checkOnly = flags.check === true;
|
|
288
|
-
|
|
289
332
|
const comboDir = join(REGISTRY_ROOT, combo.dir);
|
|
290
333
|
const sharedDir = join(comboDir, combo.sharedDir ?? "shared");
|
|
291
334
|
const variantDir = join(comboDir, combo.variantsDir ?? "variants", variant);
|
|
@@ -308,37 +351,66 @@ async function installMerge(comboName: string, combo: ComboEntry, variant: strin
|
|
|
308
351
|
]
|
|
309
352
|
: undefined;
|
|
310
353
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const copyOpts = {
|
|
314
|
-
aliasFrom: "@/lib/auth/core",
|
|
315
|
-
aliasTo: `${alias}/core`,
|
|
316
|
-
extraRewrites,
|
|
317
|
-
previous,
|
|
318
|
-
force: opts.force,
|
|
319
|
-
forcePaths: opts.forcePaths,
|
|
320
|
-
ignore: config.ignore,
|
|
321
|
-
neverCopy: NEVER_COPY,
|
|
322
|
-
dryRun: opts.dryRun,
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
await copyDir(join(REGISTRY_ROOT, registry.core.dir), join(destRoot, "core"), copyOpts, destRoot, result);
|
|
326
|
-
await copyDir(sharedDir, destRoot, { ...copyOpts, neverCopy: skipFromShared }, destRoot, result);
|
|
327
|
-
await copyDir(variantDir, destRoot, { ...copyOpts, neverCopy: skipFromVariant }, destRoot, result);
|
|
328
|
-
|
|
329
|
-
if (orm) {
|
|
330
|
-
await copyOneFile(join(sharedDir, orm.configFile), join(targetRoot, orm.configFile), destRoot, copyOpts, result);
|
|
331
|
-
await copyDir(join(variantDir, orm.dataDir), join(targetRoot, orm.dataDir), copyOpts, destRoot, result);
|
|
332
|
-
}
|
|
354
|
+
return { destRoot, installPath, alias, sharedDir, variantDir, orm, skipFromShared, skipFromVariant, extraRewrites, config, previous };
|
|
355
|
+
}
|
|
333
356
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
357
|
+
/** The actual copy pass for a merge-mode install — shared by installMerge (writes for real, or
|
|
358
|
+
* dry-runs for --check/the interactive-prompt pre-check) and cmdDiff (always a dry run, always
|
|
359
|
+
* force:true so a locally-modified file's difference is captured too instead of silently
|
|
360
|
+
* skipped). One exception to "everything merges into destRoot": an ORM combo's config file and
|
|
361
|
+
* data directory (see ORM_LAYOUTS) land at the *project root* instead — e.g. the Prisma combos'
|
|
362
|
+
* `prisma.config.ts` + `prisma/`, or the Drizzle combos' `drizzle.config.ts` + `drizzle/`.
|
|
363
|
+
*
|
|
364
|
+
* Both are still copied with destRoot (not targetRoot) as the manifest-key root, which makes
|
|
365
|
+
* copyOneFile compute "../"-relative keys for them — deliberate, not an oversight: those keys
|
|
366
|
+
* can never collide with a real destRoot-relative key, so pruneRemovedFiles below correctly
|
|
367
|
+
* removes an ORM folder a pre-migration install left nested inside destRoot, without disturbing
|
|
368
|
+
* anything else's keys. */
|
|
369
|
+
async function runMergeCopy(
|
|
370
|
+
plan: MergePlan,
|
|
371
|
+
registry: Registry,
|
|
372
|
+
targetRoot: string,
|
|
373
|
+
opts: { force: boolean; forcePaths: Set<string>; dryRun: boolean; collectDiffs?: DiffEntry[] },
|
|
374
|
+
): Promise<{ result: CopyResult; pruned: { removed: string[]; keptModified: string[] } }> {
|
|
375
|
+
const result: CopyResult = { manifest: {}, skipped: [], ignored: [], updated: [] };
|
|
376
|
+
const copyOpts: CopyOptions = {
|
|
377
|
+
aliasFrom: "@/lib/auth/core",
|
|
378
|
+
aliasTo: `${plan.alias}/core`,
|
|
379
|
+
extraRewrites: plan.extraRewrites,
|
|
380
|
+
previous: plan.previous,
|
|
381
|
+
force: opts.force,
|
|
382
|
+
forcePaths: opts.forcePaths,
|
|
383
|
+
ignore: plan.config.ignore,
|
|
384
|
+
neverCopy: NEVER_COPY,
|
|
385
|
+
dryRun: opts.dryRun,
|
|
386
|
+
collectDiffs: opts.collectDiffs,
|
|
337
387
|
};
|
|
338
388
|
|
|
389
|
+
await copyDir(join(REGISTRY_ROOT, registry.core.dir), join(plan.destRoot, "core"), copyOpts, plan.destRoot, result);
|
|
390
|
+
await copyDir(plan.sharedDir, plan.destRoot, { ...copyOpts, neverCopy: plan.skipFromShared }, plan.destRoot, result);
|
|
391
|
+
await copyDir(plan.variantDir, plan.destRoot, { ...copyOpts, neverCopy: plan.skipFromVariant }, plan.destRoot, result);
|
|
392
|
+
|
|
393
|
+
if (plan.orm) {
|
|
394
|
+
await copyOneFile(join(plan.sharedDir, plan.orm.configFile), join(targetRoot, plan.orm.configFile), plan.destRoot, copyOpts, result);
|
|
395
|
+
await copyDir(join(plan.variantDir, plan.orm.dataDir), join(targetRoot, plan.orm.dataDir), copyOpts, plan.destRoot, result);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Switching variants has to remove the old variant's files, or the project ends up with both wired in.
|
|
399
|
+
const pruned = await pruneRemovedFiles(plan.destRoot, plan.previous, result, { force: opts.force, ignore: plan.config.ignore, dryRun: opts.dryRun });
|
|
400
|
+
return { result, pruned };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async function installMerge(comboName: string, combo: ComboEntry, variant: string, registry: Registry, targetRoot: string, flags: Record<string, string | true>) {
|
|
404
|
+
const lock = await loadLock(targetRoot);
|
|
405
|
+
const previous = lock.files ?? {};
|
|
406
|
+
const force = flags.force === true;
|
|
407
|
+
const checkOnly = flags.check === true;
|
|
408
|
+
|
|
409
|
+
const plan = await buildMergePlan(combo, variant, targetRoot, flags, previous);
|
|
410
|
+
|
|
339
411
|
if (checkOnly) {
|
|
340
|
-
const { result, pruned } = await
|
|
341
|
-
console.log(`\nCheck only — nothing written. "${comboName}" (${variant} variant) in ${installPath} (alias ${alias}):`);
|
|
412
|
+
const { result, pruned } = await runMergeCopy(plan, registry, targetRoot, { force, forcePaths: new Set(), dryRun: true });
|
|
413
|
+
console.log(`\nCheck only — nothing written. "${comboName}" (${variant} variant) in ${plan.installPath} (alias ${plan.alias}):`);
|
|
342
414
|
printInstallSummary(result, pruned);
|
|
343
415
|
const silent = !result.updated.length && !result.skipped.length && !pruned.removed.length && !pruned.keptModified.length;
|
|
344
416
|
if (silent) console.log(`\nUp to date — nothing would change.`);
|
|
@@ -348,8 +420,8 @@ async function installMerge(comboName: string, combo: ComboEntry, variant: strin
|
|
|
348
420
|
// Interactive + no --force: find locally-modified conflicts first (a dry run, nothing written),
|
|
349
421
|
// and let the user pick which — if any — to overwrite anyway, shadcn-"this file already exists"
|
|
350
422
|
// style, instead of the non-interactive default of silently leaving all of them alone.
|
|
351
|
-
const forcePaths = await resolveForcePaths(() =>
|
|
352
|
-
const { result, pruned } = await
|
|
423
|
+
const forcePaths = await resolveForcePaths(() => runMergeCopy(plan, registry, targetRoot, { force: false, forcePaths: new Set(), dryRun: true }), flags);
|
|
424
|
+
const { result, pruned } = await runMergeCopy(plan, registry, targetRoot, { force, forcePaths, dryRun: false });
|
|
353
425
|
|
|
354
426
|
await writeFile(
|
|
355
427
|
join(targetRoot, "auth.lock.json"),
|
|
@@ -357,11 +429,16 @@ async function installMerge(comboName: string, combo: ComboEntry, variant: strin
|
|
|
357
429
|
"utf8",
|
|
358
430
|
);
|
|
359
431
|
|
|
360
|
-
console.log(`\nInstalled "${comboName}" (${variant} variant) into ${installPath} (alias ${alias})`);
|
|
432
|
+
console.log(`\nInstalled "${comboName}" (${variant} variant) into ${plan.installPath} (alias ${plan.alias})`);
|
|
361
433
|
printInstallSummary(result, pruned);
|
|
362
434
|
|
|
363
435
|
const peerDeps = [...new Set([...registry.core.peerDependencies, ...combo.peerDependencies])];
|
|
364
|
-
console.log(`\
|
|
436
|
+
console.log(`\nPeer dependencies: ${peerDeps.join(" ")}`);
|
|
437
|
+
if (result.updated.length) {
|
|
438
|
+
installDependencies(targetRoot, peerDeps, flags);
|
|
439
|
+
} else if (flags["skip-install"] !== true) {
|
|
440
|
+
console.log(`(nothing changed this run — skipping install)`);
|
|
441
|
+
}
|
|
365
442
|
printPostInstallNotes(combo, variant);
|
|
366
443
|
}
|
|
367
444
|
|
|
@@ -427,7 +504,13 @@ async function installScaffold(comboName: string, combo: ComboEntry, variant: st
|
|
|
427
504
|
|
|
428
505
|
console.log(`\nGenerated "${appName}" (${comboName}, ${variant} variant) into ${targetRoot}`);
|
|
429
506
|
printInstallSummary(result, pruned);
|
|
430
|
-
|
|
507
|
+
if (result.updated.length) {
|
|
508
|
+
// Dependencies are already declared in package.json — no specific packages to name, just
|
|
509
|
+
// "install whatever's there" (installDependencies with an empty list does exactly that).
|
|
510
|
+
installDependencies(targetRoot, [], flags);
|
|
511
|
+
} else if (flags["skip-install"] !== true) {
|
|
512
|
+
console.log(`\n(nothing changed this run — skipping install)`);
|
|
513
|
+
}
|
|
431
514
|
printPostInstallNotes(combo, variant);
|
|
432
515
|
}
|
|
433
516
|
|
|
@@ -649,21 +732,100 @@ async function cmdUpdate(targetRoot: string, flags: Record<string, string | true
|
|
|
649
732
|
await installCombo(lock.combo, combo, variant, registry, targetRoot, flags);
|
|
650
733
|
}
|
|
651
734
|
|
|
735
|
+
/**
|
|
736
|
+
* Shows the actual content diff between what's on disk and what the current registry would
|
|
737
|
+
* produce — for every tracked file that differs, whether that's because the registry changed
|
|
738
|
+
* upstream or because you hand-edited the file yourself (both are worth seeing before deciding
|
|
739
|
+
* what to do). Read-only: never writes anything, unlike `update`.
|
|
740
|
+
*/
|
|
652
741
|
async function cmdDiff(targetRoot: string) {
|
|
653
742
|
const lock = await loadLock(targetRoot);
|
|
654
|
-
if (!lock.
|
|
655
|
-
console.error(`No auth.lock.json
|
|
743
|
+
if (!lock.combo || !lock.variant) {
|
|
744
|
+
console.error(`No auth.lock.json (or it's missing combo/variant) in ${targetRoot} — nothing installed to diff.`);
|
|
745
|
+
process.exitCode = 1;
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const registry = await loadRegistry();
|
|
750
|
+
const combo = registry.combos[lock.combo];
|
|
751
|
+
if (!combo) {
|
|
752
|
+
console.error(`auth.lock.json names combo "${lock.combo}", which no longer exists in this registry.`);
|
|
753
|
+
process.exitCode = 1;
|
|
656
754
|
return;
|
|
657
755
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
756
|
+
|
|
757
|
+
if (comboInstallMode(combo) === "scaffold") {
|
|
758
|
+
console.error(`diff isn't supported yet for "${lock.combo}" (a scaffold-mode combo) — only merge-mode (api) combos support it.`);
|
|
759
|
+
process.exitCode = 1;
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const variant = resolveVariant(combo, lock.combo, lock.variant);
|
|
764
|
+
if (!variant) {
|
|
765
|
+
process.exitCode = 1;
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const plan = await buildMergePlan(combo, variant, targetRoot, {}, lock.files ?? {});
|
|
770
|
+
const diffs: DiffEntry[] = [];
|
|
771
|
+
// force:true so a locally-modified file's difference is captured too — diff wants to show
|
|
772
|
+
// everything that differs, not just what a real (non---force) install would apply; dryRun:true
|
|
773
|
+
// so nothing is written.
|
|
774
|
+
await runMergeCopy(plan, registry, targetRoot, { force: true, forcePaths: new Set(), dryRun: true, collectDiffs: diffs });
|
|
775
|
+
|
|
776
|
+
if (!diffs.length) {
|
|
777
|
+
console.log(`No differences — every tracked file in "${lock.combo}" (${variant} variant) matches the current registry.`);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
console.log(`${diffs.length} file(s) differ from the current registry:`);
|
|
782
|
+
for (const d of diffs) {
|
|
783
|
+
const patch = createTwoFilesPatch(d.path, d.path, d.oldContent ?? "", d.newContent, "installed", "registry");
|
|
784
|
+
process.stdout.write(`\n${patch}`);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
async function printUsage(exitCode: number) {
|
|
789
|
+
const registry = await loadRegistry();
|
|
790
|
+
console.log("Usage: simple-auth-kit <init|add|update|diff> [...]");
|
|
791
|
+
console.log(` init [--config-only] [--kind ...] [--into <path>] (fresh setup: guided "what do you need" picker, like bare "add")`);
|
|
792
|
+
console.log(` --config-only: just write .simple-auth-kit.json and stop, no install`);
|
|
793
|
+
console.log(` add <combo> [--workspaces] [--force] [--skip-install] [--into <path>] [--path <dir>] [--alias <alias>]`);
|
|
794
|
+
console.log(` add [--kind api,admin,mobile] [--framework <name>,...] [--workspaces] [--into <path>] [--name <appName>]`);
|
|
795
|
+
console.log(` (bare "add", or "add" with --kind but no combo, launches a guided prompt for whatever's missing)`);
|
|
796
|
+
console.log(` update [--check] [--force] [--skip-install] [--into <path>] (re-installs whatever combo+variant auth.lock.json already records)`);
|
|
797
|
+
console.log(` --check: report what would change, without writing anything (merge-mode combos only)`);
|
|
798
|
+
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)`);
|
|
799
|
+
console.log(` In a TTY, without --force or --check: a file changed locally since install prompts to overwrite, per file.`);
|
|
800
|
+
console.log(` --skip-install: don't run the package manager after copying files — the default is to install for you, shadcn-\`add\`-style.`);
|
|
801
|
+
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.`);
|
|
802
|
+
console.log(`\nAvailable combos:`);
|
|
803
|
+
for (const kind of ["api", "admin", "mobile"] as Kind[]) {
|
|
804
|
+
const names = combosByKind(registry, kind).map(([name]) => name);
|
|
805
|
+
if (names.length) console.log(` ${KIND_LABELS[kind]}: ${names.join(", ")}`);
|
|
806
|
+
}
|
|
807
|
+
console.log(`\nVariants (choose one at install time):`);
|
|
808
|
+
for (const [name, variant] of Object.entries(registry.variants)) {
|
|
809
|
+
console.log(` ${name}${variant.flag ? ` (${variant.flag})` : " (default)"} — ${variant.description}`);
|
|
810
|
+
}
|
|
811
|
+
process.exitCode = exitCode;
|
|
661
812
|
}
|
|
662
813
|
|
|
663
814
|
async function main() {
|
|
664
815
|
const { command, positional, flags } = parseArgs(process.argv.slice(2));
|
|
665
816
|
const targetRoot = resolve(process.cwd(), flagString(flags.into) ?? ".");
|
|
666
817
|
|
|
818
|
+
if (command === "--help" || command === "-h" || command === "help" || flags.help === true) {
|
|
819
|
+
await printUsage(0);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
if (!command && isTTY()) {
|
|
823
|
+
// Bare `npx @simple-auth-kit/cli` in a real terminal — go straight to the guided picker
|
|
824
|
+
// (same flow as `add` with no combo) instead of just printing help text.
|
|
825
|
+
await cmdCreate(targetRoot, flags);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
|
|
667
829
|
switch (command) {
|
|
668
830
|
case "init":
|
|
669
831
|
await cmdInit(targetRoot, flags);
|
|
@@ -684,28 +846,8 @@ async function main() {
|
|
|
684
846
|
case "diff":
|
|
685
847
|
await cmdDiff(targetRoot);
|
|
686
848
|
break;
|
|
687
|
-
default:
|
|
688
|
-
|
|
689
|
-
console.log("Usage: simple-auth-kit <init|add|update|diff> [...]");
|
|
690
|
-
console.log(` init [--config-only] [--kind ...] [--into <path>] (fresh setup: guided "what do you need" picker, like bare "add")`);
|
|
691
|
-
console.log(` --config-only: just write .simple-auth-kit.json and stop, no install`);
|
|
692
|
-
console.log(` add <combo> [--workspaces] [--force] [--into <path>] [--path <dir>] [--alias <alias>]`);
|
|
693
|
-
console.log(` add [--kind api,admin,mobile] [--framework <name>,...] [--workspaces] [--into <path>] [--name <appName>]`);
|
|
694
|
-
console.log(` (bare "add", or "add" with --kind but no combo, launches a guided prompt for whatever's missing)`);
|
|
695
|
-
console.log(` update [--check] [--force] [--into <path>] (re-installs whatever combo+variant auth.lock.json already records)`);
|
|
696
|
-
console.log(` --check: report what would change, without writing anything (merge-mode combos only)`);
|
|
697
|
-
console.log(` In a TTY, without --force or --check: a file changed locally since install prompts to overwrite, per file.`);
|
|
698
|
-
console.log(`\nAvailable combos:`);
|
|
699
|
-
for (const kind of ["api", "admin", "mobile"] as Kind[]) {
|
|
700
|
-
const names = combosByKind(registry, kind).map(([name]) => name);
|
|
701
|
-
if (names.length) console.log(` ${KIND_LABELS[kind]}: ${names.join(", ")}`);
|
|
702
|
-
}
|
|
703
|
-
console.log(`\nVariants (choose one at install time):`);
|
|
704
|
-
for (const [name, variant] of Object.entries(registry.variants)) {
|
|
705
|
-
console.log(` ${name}${variant.flag ? ` (${variant.flag})` : " (default)"} — ${variant.description}`);
|
|
706
|
-
}
|
|
707
|
-
process.exitCode = command ? 1 : 0;
|
|
708
|
-
}
|
|
849
|
+
default:
|
|
850
|
+
await printUsage(command ? 1 : 0);
|
|
709
851
|
}
|
|
710
852
|
}
|
|
711
853
|
|