@sundaysf/cli-v3 0.0.1 → 0.0.2

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 (3) hide show
  1. package/README.md +54 -6
  2. package/dist/cli.js +40 -16
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -3,12 +3,56 @@
3
3
  CLI that scaffolds a production-ready REST API (Express 5 · TypeScript · Knex · PostgreSQL ·
4
4
  Zod · pino · Jest) and generates complete entity verticals inside it.
5
5
 
6
+ ## Commands
7
+
6
8
  ```bash
7
- npx @sundaysf/cli-v3 new my-api --with-auth
8
- cd my-api && docker compose up -d && npm run db:migrate && npm run start:dev
9
+ # Create a project ---------------------------------------------------------------
10
+ npx @sundaysf/cli-v3 new my-api # new folder ./my-api
11
+ npx @sundaysf/cli-v3 new my-api --with-auth # + user/auth tables, register/login/me, JWT
12
+ npx @sundaysf/cli-v3 new . # scaffold INTO the current (empty) folder
13
+ npx @sundaysf/cli-v3 new . --name my-api # same, choosing the package name
14
+ npx @sundaysf/cli-v3 new my-api -y # no prompts, defaults (no auth, port 3005, npm)
15
+
16
+ # Options for `new`
17
+ # --with-auth | --no-auth include / skip auth without asking
18
+ # --port <n> HTTP port (default 3005)
19
+ # --pm npm|pnpm package manager (auto-detected)
20
+ # --no-install skip dependency installation
21
+ # --no-git skip git init and the initial commit
22
+ # --name <name> project name when using "."
23
+ # -y, --yes accept every default, never prompt
24
+
25
+ # Work inside the project --------------------------------------------------------
26
+ docker compose up -d # local PostgreSQL 16
27
+ npm run db:migrate # apply migrations
28
+ npm run start:dev # http://localhost:3005/api/health
29
+ npm test # jest + coverage (needs Postgres)
30
+ npm run test:unit # jest without database suites
31
+
32
+ # Generate an entity (run inside the project) ------------------------------------
9
33
  sundaysf generate entity product name:string:unique price:decimal isActive:boolean=true
34
+ sundaysf g entity order userId:user.id total:decimal 'notes:text?' status:string=pending
35
+ sundaysf g entity tag --dry-run # print, write nothing (asks for fields on a TTY)
36
+
37
+ # Options for `generate entity`
38
+ # --fields "<spec>" fields as one string instead of positional args
39
+ # --no-tests skip unit + route tests
40
+ # --no-migration skip the migration
41
+ # --dry-run print everything, write nothing
42
+ # --force overwrite existing code files (never migrations)
43
+
44
+ # After generating
45
+ npm run db:migrate && npm run typecheck && npm test
46
+
47
+ # Global install instead of npx ----------------------------------------------------
48
+ npm install -g @sundaysf/cli-v3 # then: sundaysf new ..., sundaysf g entity ...
49
+ sundaysf --help | sundaysf new --help | sundaysf generate entity --help
10
50
  ```
11
51
 
52
+ Field syntax: `name:type[?][:unique][=default]` with `string text integer decimal boolean date
53
+ datetime json uuid <entity>.id`. Quote fields containing `?` in zsh (`'notes:text?'`).
54
+ Full reference: [`sundaysf generate entity`](#sundaysf-generate-entity).
55
+
12
56
  - [What changed since v2](#what-changed-since-v2)
13
57
  - [Install](#install)
14
58
  - [`sundaysf new`](#sundaysf-new)
@@ -52,8 +96,10 @@ the local database (optional).
52
96
  ## `sundaysf new`
53
97
 
54
98
  ```
55
- sundaysf new [name] [options]
99
+ sundaysf new <name|.> [options]
56
100
 
101
+ . scaffold into the current directory (must be empty; an existing .git is kept)
102
+ --name <name> project/package name when using "." (default: the folder name, slugified)
57
103
  --with-auth include auth (user/auth tables, register/login/me, JWT, bcrypt, middleware, tests)
58
104
  --no-auth skip auth without asking
59
105
  --port <port> HTTP port (default 3005)
@@ -68,14 +114,16 @@ are: no auth, port 3005, install, git.
68
114
 
69
115
  What it does:
70
116
 
71
- 1. Validates the name (`^[a-z0-9][a-z0-9-]{0,63}$`) and that the target folder is empty.
117
+ 1. Validates the name (`^[a-z0-9][a-z0-9-]{0,63}$`) and that the target folder is empty. With
118
+ `.` the folder is the current directory: only `.git` and editor folders may already exist,
119
+ and the name comes from `--name` or the folder name (`My API v2` → `my-api-v2`).
72
120
  2. Copies the `api` template, applies the `api-auth` overlay when requested, replaces the
73
121
  `__SF_*__` tokens (project name, port, database name, CLI version), renames `_gitignore` and
74
122
  `_package.json`, and refuses to finish if any token is left or a `.npmrc` sneaked in.
75
123
  3. Writes `.env` from `.env.example`. With auth, `.env` gets a random 64-char `JWT_SECRET`
76
124
  (`.env.example` keeps a placeholder).
77
- 4. `git init -b main`, `npm install`, `npm run format`, and an initial commit
78
- (`chore: scaffold <name> with @sundaysf/cli-v3 <version>`).
125
+ 4. `git init -b main` (skipped when `.git` already exists), `npm install`, `npm run format`,
126
+ and an initial commit (`chore: scaffold <name> with @sundaysf/cli-v3 <version>`).
79
127
  5. Prints the next steps. A failing install or commit is reported but never deletes the files.
80
128
 
81
129
  Generated tree (auth files marked `*`):
package/dist/cli.js CHANGED
@@ -298,6 +298,7 @@ var detectPackageManager = () => {
298
298
  const ua = process.env.npm_config_user_agent ?? "";
299
299
  return ua.startsWith("pnpm") ? "pnpm" : "npm";
300
300
  };
301
+ var slugifyProjectName = (raw) => raw.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
301
302
  var validateProjectName = (name) => {
302
303
  if (!PROJECT_NAME_RE.test(name)) {
303
304
  return "Use lowercase letters, numbers and dashes, starting with a letter or number (max 64 chars)";
@@ -340,25 +341,46 @@ var scaffoldProject = async (dir, name, port, withAuth) => {
340
341
  await writeDotEnv(dir, withAuth);
341
342
  return { dir, name, port, withAuth };
342
343
  };
343
- var isEmptyDir = async (dir) => {
344
- if (!await fs5.pathExists(dir)) return true;
344
+ var HARMLESS_ENTRIES = /* @__PURE__ */ new Set([".git", ".DS_Store", ".idea", ".vscode"]);
345
+ var listBlockingEntries = async (dir) => {
346
+ if (!await fs5.pathExists(dir)) return [];
345
347
  const entries = await fs5.readdir(dir);
346
- return entries.length === 0;
348
+ return entries.filter((e) => !HARMLESS_ENTRIES.has(e));
349
+ };
350
+ var resolveTarget = async (rawName, cwd, explicitName) => {
351
+ const inPlace = rawName === ".";
352
+ const dir = inPlace ? path4.resolve(cwd) : path4.resolve(cwd, rawName);
353
+ const name = explicitName ?? (inPlace ? slugifyProjectName(path4.basename(dir)) : rawName);
354
+ const nameError = validateProjectName(name);
355
+ if (nameError) {
356
+ throw new Error(
357
+ inPlace ? `Cannot derive a project name from folder "${path4.basename(dir)}": ${nameError}. Use --name <name>.` : `Invalid project name "${name}": ${nameError}`
358
+ );
359
+ }
360
+ const blocking = await listBlockingEntries(dir);
361
+ if (blocking.length) {
362
+ throw new Error(
363
+ inPlace ? `The current directory is not empty (${blocking.slice(0, 5).join(", ")}${blocking.length > 5 ? ", ..." : ""}). Run \`sundaysf new .\` in an empty folder (a .git directory is fine).` : `Directory "${rawName}" already exists and is not empty.`
364
+ );
365
+ }
366
+ return { dir, name, inPlace };
347
367
  };
348
368
  var runNew = async (rawName, opts) => {
349
369
  const interactive = isInteractive() && !opts.yes;
350
370
  p2.intro(pc2.bgCyan(pc2.black(` sundaysf v${cliVersion()} `)) + " " + pc2.dim("new API project"));
351
- let name = rawName?.trim();
352
- if (!name) {
353
- if (!interactive) throw new Error("Project name is required: sundaysf new <name>");
354
- name = await text2("Project name", { placeholder: "my-api", validate: validateProjectName });
355
- }
356
- const nameError = validateProjectName(name);
357
- if (nameError) throw new Error(`Invalid project name "${name}": ${nameError}`);
358
- const dir = path4.resolve(process.cwd(), name);
359
- if (!await isEmptyDir(dir)) {
360
- throw new Error(`Directory "${name}" already exists and is not empty.`);
371
+ let target = rawName?.trim();
372
+ if (!target) {
373
+ if (!interactive)
374
+ throw new Error(
375
+ 'Project name is required: sundaysf new <name> (or "." for the current directory)'
376
+ );
377
+ target = await text2('Project name ("." for the current directory)', {
378
+ placeholder: "my-api",
379
+ validate: (v) => v === "." ? void 0 : validateProjectName(v)
380
+ });
361
381
  }
382
+ const { dir, name, inPlace } = await resolveTarget(target, process.cwd(), opts.name);
383
+ if (inPlace) p2.log.info(`Scaffolding "${name}" into ${dir}`);
362
384
  let withAuth = opts.withAuth;
363
385
  if (withAuth === void 0) {
364
386
  withAuth = interactive ? await confirm2("Include auth (user/auth tables, register/login/me, JWT)?", false) : false;
@@ -378,7 +400,8 @@ var runNew = async (rawName, opts) => {
378
400
  });
379
401
  let gitOk = false;
380
402
  if (doGit) {
381
- gitOk = await step("git init", async () => {
403
+ const hasGit = await fs5.pathExists(path4.join(dir, ".git"));
404
+ gitOk = hasGit ? true : await step("git init", async () => {
382
405
  await run("git", ["init", "-b", "main"], { cwd: dir });
383
406
  });
384
407
  }
@@ -404,7 +427,7 @@ var runNew = async (rawName, opts) => {
404
427
  });
405
428
  }
406
429
  const next = [
407
- `cd ${name}`,
430
+ ...inPlace ? [] : [`cd ${name}`],
408
431
  ...doInstall && installOk ? [] : [`${pm} install`],
409
432
  "docker compose up -d # local Postgres",
410
433
  `${pm} run db:migrate`,
@@ -415,8 +438,9 @@ var runNew = async (rawName, opts) => {
415
438
  p2.outro(pc2.green(`Project "${name}" is ready.`) + (withAuth ? pc2.dim(" (with auth)") : ""));
416
439
  };
417
440
  var registerNewCommand = (program2) => {
418
- program2.command("new").description("Create a new API project (Express 5 + Knex + Postgres) ready to work on").argument("[name]", "project name (lowercase, dashes allowed)").option("--with-auth", "include auth: user/auth tables, register/login/me, JWT").option("--no-auth", "explicitly skip auth (no prompt)").option("--port <port>", "HTTP port for the API", void 0).option("--no-install", "skip dependency installation").option("--no-git", "skip git init and initial commit").option("--pm <pm>", "package manager: npm | pnpm").option("-y, --yes", "accept all defaults, no prompts").action(async (name, raw) => {
441
+ program2.command("new").description("Create a new API project (Express 5 + Knex + Postgres) ready to work on").argument("[name]", 'project name (lowercase, dashes allowed) or "." for the current directory').option("--name <name>", 'project name when scaffolding with "." (default: folder name)').option("--with-auth", "include auth: user/auth tables, register/login/me, JWT").option("--no-auth", "explicitly skip auth (no prompt)").option("--port <port>", "HTTP port for the API", void 0).option("--no-install", "skip dependency installation").option("--no-git", "skip git init and initial commit").option("--pm <pm>", "package manager: npm | pnpm").option("-y, --yes", "accept all defaults, no prompts").action(async (name, raw) => {
419
442
  const opts = {
443
+ name: raw.name,
420
444
  withAuth: raw.withAuth === true ? true : raw.auth === false ? false : void 0,
421
445
  port: raw.port,
422
446
  install: raw.install,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sundaysf/cli-v3",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Sundays Framework v3 CLI - scaffolds an Express 5 + Knex + Postgres API and generates entity verticals",
5
5
  "type": "module",
6
6
  "bin": {