@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.
- package/README.md +54 -6
- package/dist/cli.js +40 -16
- 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
|
-
|
|
8
|
-
|
|
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
|
|
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
|
|
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
|
|
344
|
-
|
|
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.
|
|
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
|
|
352
|
-
if (!
|
|
353
|
-
if (!interactive)
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
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]",
|
|
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,
|