@nakedev/go-scaffold 0.1.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 +223 -0
- package/bin/go-scaffold.js +2 -0
- package/dist/commands/create.js +57 -0
- package/dist/commands/generate.js +97 -0
- package/dist/commands/method.js +70 -0
- package/dist/commands/remove.js +72 -0
- package/dist/index.js +138 -0
- package/dist/prompts/create-wizard.js +43 -0
- package/dist/prompts/generate-wizard.js +68 -0
- package/dist/templates/create-manifest.js +110 -0
- package/dist/templates/module-manifest.js +28 -0
- package/dist/types.js +2 -0
- package/dist/utils/config.js +53 -0
- package/dist/utils/main-patcher.js +59 -0
- package/dist/utils/marker-patch.js +63 -0
- package/dist/utils/method-patcher.js +271 -0
- package/dist/utils/migrations.js +17 -0
- package/dist/utils/module-paths.js +33 -0
- package/dist/utils/naming.js +160 -0
- package/dist/utils/openapi-patcher.js +47 -0
- package/dist/utils/template-renderer.js +51 -0
- package/package.json +49 -0
- package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +83 -0
- package/templates/create/base/.env.example.hbs +7 -0
- package/templates/create/base/.github/workflows/ci.yml.hbs +46 -0
- package/templates/create/base/.gitignore.hbs +5 -0
- package/templates/create/base/.golangci.yml.hbs +32 -0
- package/templates/create/base/.vscode/settings.json.hbs +11 -0
- package/templates/create/base/AGENTS.md.hbs +68 -0
- package/templates/create/base/CLAUDE.md.hbs +1 -0
- package/templates/create/base/Makefile.hbs +93 -0
- package/templates/create/base/README.md.hbs +143 -0
- package/templates/create/base/cmd/api/main.go.hbs +116 -0
- package/templates/create/base/go.mod.hbs +11 -0
- package/templates/create/base/internal/platform/database/database.go.hbs +28 -0
- package/templates/create/base/internal/shared/apperror/apperror.go.hbs +36 -0
- package/templates/create/base/internal/shared/config/config.go.hbs +46 -0
- package/templates/create/base/internal/shared/dberr/dberr.go.hbs +28 -0
- package/templates/create/base/internal/shared/httpx/httpx.go.hbs +37 -0
- package/templates/create/base/internal/shared/id/id.go.hbs +16 -0
- package/templates/create/base/internal/shared/middleware/error.go.hbs +33 -0
- package/templates/create/base/internal/shared/middleware/logger.go.hbs +23 -0
- package/templates/create/base/internal/shared/middleware/requestid.go.hbs +36 -0
- package/templates/create/base/internal/shared/pagination/pagination.go.hbs +39 -0
- package/templates/create/base/migrations/.gitkeep.hbs +0 -0
- package/templates/create/features/docker-compose.yml.hbs +14 -0
- package/templates/create/features/docs/architecture.md.hbs +99 -0
- package/templates/create/features/docs/common/parameters.yaml.hbs +13 -0
- package/templates/create/features/docs/common/responses.yaml.hbs +20 -0
- package/templates/create/features/docs/common/schemas.yaml.hbs +23 -0
- package/templates/create/features/docs/health/health-livez.yaml.hbs +13 -0
- package/templates/create/features/docs/health/health-readyz.yaml.hbs +21 -0
- package/templates/create/features/docs/openapi.yaml.hbs +33 -0
- package/templates/create/features/docs/patterns.md.hbs +119 -0
- package/templates/create/features/docs/techstack.md.hbs +38 -0
- package/templates/generate/module/docs/collection.yaml.hbs +36 -0
- package/templates/generate/module/docs/item.yaml.hbs +37 -0
- package/templates/generate/module/docs/schemas.yaml.hbs +13 -0
- package/templates/generate/module/dto.go.hbs +29 -0
- package/templates/generate/module/errors.go.hbs +28 -0
- package/templates/generate/module/handler.go.hbs +103 -0
- package/templates/generate/module/handler_test.go.hbs +105 -0
- package/templates/generate/module/migration.down.sql.hbs +1 -0
- package/templates/generate/module/migration.up.sql.hbs +5 -0
- package/templates/generate/module/minimal/dto.go.hbs +26 -0
- package/templates/generate/module/minimal/handler.go.hbs +24 -0
- package/templates/generate/module/minimal/handler_test.go.hbs +70 -0
- package/templates/generate/module/minimal/service.go.hbs +45 -0
- package/templates/generate/module/minimal/service_test.go.hbs +54 -0
- package/templates/generate/module/model/model.go.hbs +20 -0
- package/templates/generate/module/repository.go.hbs +49 -0
- package/templates/generate/module/service.go.hbs +97 -0
- package/templates/generate/module/service_test.go.hbs +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# @nakedev/go-scaffold
|
|
2
|
+
|
|
3
|
+
A CLI that scaffolds a Gin + GORM + PostgreSQL Go backend, then keeps
|
|
4
|
+
generating consistent domain modules into that project as it grows — the Go
|
|
5
|
+
counterpart to [nest-scaffold](../nest-scaffold).
|
|
6
|
+
|
|
7
|
+
You don't hand-wire a new domain into `cmd/api/main.go`, write the
|
|
8
|
+
handler/service/repository boilerplate, or decide error-handling conventions
|
|
9
|
+
each time — the CLI does that, and every module it generates follows the
|
|
10
|
+
same shape as the last one.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pnpm install
|
|
16
|
+
pnpm run build
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`npm link` / `pnpm link --global` may not put the binary on your `PATH`
|
|
20
|
+
depending on your machine's npm/pnpm global-bin config — rather than fight
|
|
21
|
+
that, run the CLI directly, or add a shell alias once:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
node bin/go-scaffold.js create my-api --defaults
|
|
25
|
+
# or: alias go-scaffold="node $(pwd)/bin/go-scaffold.js"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The rest of this README uses the bare `go-scaffold ...` form for brevity —
|
|
29
|
+
substitute the `node bin/go-scaffold.js ...` form or your alias if you
|
|
30
|
+
haven't linked it.
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
go-scaffold create my-api
|
|
36
|
+
cd my-api
|
|
37
|
+
make docker-up # if you kept Docker + PostgreSQL
|
|
38
|
+
go mod tidy
|
|
39
|
+
make run
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Then grow the project without leaving the CLI:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
go-scaffold generate module orders
|
|
46
|
+
go-scaffold generate method orders approve --type patch
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Commands
|
|
50
|
+
|
|
51
|
+
### `create <name>` — scaffold a new project
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
go-scaffold create my-api # interactive wizard
|
|
55
|
+
go-scaffold create my-api --defaults # no prompts, CI-friendly
|
|
56
|
+
go-scaffold create my-api --defaults --no-docker --api-prefix beta
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Produces a **bare skeleton only** — `cmd/api`, the shared platform packages
|
|
60
|
+
(config/apperror/dberr/httpx/id/middleware/pagination), Docker+Postgres,
|
|
61
|
+
migrations folder, and the standards docs (`docs/architect/`, `AGENTS.md`,
|
|
62
|
+
`CLAUDE.md`, `.claude/skills/go-scaffold/`). No domain modules — add those
|
|
63
|
+
with `generate module`.
|
|
64
|
+
|
|
65
|
+
| Option | Effect |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `--defaults` | Skip the wizard, use defaults (Docker on, OpenAPI docs on, prefix `v1`) |
|
|
68
|
+
| `--no-docker` | Skip `docker-compose.yml` (with `--defaults`) |
|
|
69
|
+
| `--no-openapi-docs` | Skip `docs/openapi.yaml` (with `--defaults`) |
|
|
70
|
+
| `--api-prefix <prefix>` | URL prefix every route is grouped under (with `--defaults`; default `v1`, `""` for none, `/`-separated segments like `api/v1` are fine) |
|
|
71
|
+
|
|
72
|
+
Without `--defaults`, an interactive wizard asks the same three questions.
|
|
73
|
+
The prefix is a single project-wide choice made once at `create` time —
|
|
74
|
+
there's no per-domain versioning (a domain that needs a real breaking change
|
|
75
|
+
gets a new domain package or a new DTO field, not a duplicated model pointed
|
|
76
|
+
at the same table under a different URL — see "Why no per-domain versioning"
|
|
77
|
+
below).
|
|
78
|
+
|
|
79
|
+
**Config file** — every `create` writes `go-scaffold.config.json` to the
|
|
80
|
+
project root; `generate` reads it back (or auto-detects from `go.mod` /
|
|
81
|
+
directory layout if missing).
|
|
82
|
+
|
|
83
|
+
### `generate module <name>` (alias `m`) — add a domain module
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
go-scaffold generate module orders # full CRUD (default)
|
|
87
|
+
go-scaffold generate module orders --no-full # minimal skeleton — add endpoints with `generate method`
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Full CRUD (default) scaffolds:
|
|
91
|
+
|
|
92
|
+
```text
|
|
93
|
+
internal/app/orders/
|
|
94
|
+
├── model/model.go # domain model + GORM table (id/created_at/updated_at — add real fields yourself; a folder so multi-table domains can add more files)
|
|
95
|
+
├── dto.go # request/response structs (empty stubs — add real fields yourself)
|
|
96
|
+
├── errors.go # ORDERS_NOT_FOUND / ORDERS_CONFLICT / ORDERS_HAS_REFERENCES
|
|
97
|
+
├── repository.go # GORM data access
|
|
98
|
+
├── service.go # business logic + repository interface (mockable)
|
|
99
|
+
├── handler.go # Gin routes, registered under the project's API prefix
|
|
100
|
+
├── service_test.go # unit test, fake repo
|
|
101
|
+
└── handler_test.go # integration test, real Postgres, tx rollback
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`--no-full` scaffolds the same `model`/`errors`/`repository` (so `generate
|
|
105
|
+
method` always has a full data-access surface to call), but `dto`/`service`/
|
|
106
|
+
`handler` start empty — no default CRUD, no routes, just the plumbing
|
|
107
|
+
(`Register()`, the `repository` interface, `wrapFindErr`) that `generate
|
|
108
|
+
method` patches into. Use it when a domain doesn't need the full REST
|
|
109
|
+
surface, or you'd rather add endpoints one at a time.
|
|
110
|
+
|
|
111
|
+
Both modes also:
|
|
112
|
+
|
|
113
|
+
- Register the module in `cmd/api/main.go` (via marker comments — see
|
|
114
|
+
`// go-scaffold:*` in that file) — full wires an actual route, minimal
|
|
115
|
+
wires an empty route group
|
|
116
|
+
- Add the model to the `AutoMigrate(...)` call
|
|
117
|
+
- Append `migrations/<seq>_create_<plural>.{up,down}.sql`
|
|
118
|
+
|
|
119
|
+
What it does **not** do: invent your fields or wire foreign keys between
|
|
120
|
+
domains — see `docs/architect/patterns.md` in the generated project for the
|
|
121
|
+
conventions to follow by hand.
|
|
122
|
+
|
|
123
|
+
### `generate method <module> <name>` (alias `me`) — add one endpoint
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
go-scaffold generate method orders approve --type patch
|
|
127
|
+
go-scaffold generate method orders findByStatus --type get --get-mode one --field status
|
|
128
|
+
go-scaffold g me orders findOverdue --type get --get-mode all
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Patches an *existing* module's `handler.go`/`service.go` in place via the
|
|
132
|
+
same marker-comment approach as `main.go` — never a whole new module. Never
|
|
133
|
+
overwrites a method with the same name; picks a different one or errors.
|
|
134
|
+
|
|
135
|
+
| Option | Effect |
|
|
136
|
+
|---|---|
|
|
137
|
+
| `--type <get\|post\|put\|patch\|delete>` | HTTP verb |
|
|
138
|
+
| `--get-mode <all\|one>` | For `get` only — list-style vs. single-record lookup |
|
|
139
|
+
| `--field <name>` | For `get --get-mode one` — the lookup field (e.g. `email`, `status`); can't be `id` |
|
|
140
|
+
|
|
141
|
+
| `--type` | Route | What's generated |
|
|
142
|
+
|---|---|---|
|
|
143
|
+
| `get --get-mode all` | `GET /<plural>/<kebab-name>` | reuses `FindAll` — TODO to add real filtering |
|
|
144
|
+
| `get --get-mode one --field <f>` | `GET /<plural>/<f>/:<f>` | a real `FindBy<F>` query added to the repository (+ its interface + `fakeRepo` test stub) |
|
|
145
|
+
| `post` | `POST /<plural>/<kebab-name>` | adds a body DTO; service is a TODO stub |
|
|
146
|
+
| `put` / `patch` | `<VERB> /<plural>/:id/<kebab-name>` | finds by id, TODO before saving (safe no-op until implemented) |
|
|
147
|
+
| `delete` | `DELETE /<plural>/:id/<kebab-name>` | TODO stub |
|
|
148
|
+
|
|
149
|
+
Business logic is always left as a `TODO`-marked stub that compiles and
|
|
150
|
+
returns a clean `500` rather than inventing behavior — see
|
|
151
|
+
`docs/architect/patterns.md` in the generated project.
|
|
152
|
+
|
|
153
|
+
`generate method` prints the route it added but does **not** touch
|
|
154
|
+
`docs/openapi.yaml` — endpoint-specific spec entries stay hand-written.
|
|
155
|
+
|
|
156
|
+
### `remove module <name>` (alias `rm m`) — drop a domain
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
go-scaffold remove module orders # confirms first
|
|
160
|
+
go-scaffold rm m orders --yes # skip the confirm
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The inverse of `generate module`: deletes `internal/app/<name>/` and reverses
|
|
164
|
+
everything that was wired up — the import/AutoMigrate/route in `main.go`, the
|
|
165
|
+
paths/schemas in `docs/openapi.yaml`, the per-module docs folder, and the
|
|
166
|
+
`create_<plural>` migration. Restores the `_ = api` placeholder if it was the
|
|
167
|
+
last module, so the project still builds. Use this instead of hand-deleting
|
|
168
|
+
the folder — a partial hand-delete leaves stale wiring that duplicates on the
|
|
169
|
+
next `generate module` (which would panic gin at startup).
|
|
170
|
+
|
|
171
|
+
## Why no per-domain versioning
|
|
172
|
+
|
|
173
|
+
Earlier versions of this CLI let a domain live in a `v1/`/`v2/` folder with
|
|
174
|
+
its own route group and import alias, so the same domain name could exist
|
|
175
|
+
twice with different behavior. It was cut: the migration (and usually the
|
|
176
|
+
DB table) is shared between "versions" of the same domain, but each version
|
|
177
|
+
got its own physically-copied `model.go` — nothing stopped the two structs
|
|
178
|
+
from drifting apart. Verified against a real Postgres instance:
|
|
179
|
+
`AutoMigrate` silently accepted a column typed `int` in one version's model
|
|
180
|
+
and `float64` in the other for the *same* column, converging it to
|
|
181
|
+
`numeric` with no error — the two versions would then read/write the same
|
|
182
|
+
data with different, silently incompatible interpretations.
|
|
183
|
+
|
|
184
|
+
Instead, every route in a project is grouped under a single project-wide
|
|
185
|
+
`--api-prefix` (default `v1`) chosen once at `create` time. A domain that
|
|
186
|
+
needs a real breaking change gets a new domain package, or a new field on
|
|
187
|
+
the existing DTO — not a duplicated model pointed at a table it can drift
|
|
188
|
+
out of sync with.
|
|
189
|
+
|
|
190
|
+
## Project structure produced by `create`
|
|
191
|
+
|
|
192
|
+
```text
|
|
193
|
+
cmd/api/main.go
|
|
194
|
+
internal/
|
|
195
|
+
├── platform/database/
|
|
196
|
+
├── shared/{config,apperror,dberr,httpx,id,middleware,pagination}/
|
|
197
|
+
└── app/ # empty until you `generate module`
|
|
198
|
+
docs/
|
|
199
|
+
├── architect/{architecture,patterns,techstack}.md
|
|
200
|
+
└── openapi.yaml + common/ + health/ # if openapi docs enabled
|
|
201
|
+
migrations/
|
|
202
|
+
.github/workflows/ci.yml # build, vet, gofmt check, golangci-lint, go test (with a Postgres service)
|
|
203
|
+
Makefile
|
|
204
|
+
.golangci.yml
|
|
205
|
+
.vscode/settings.json
|
|
206
|
+
AGENTS.md
|
|
207
|
+
CLAUDE.md
|
|
208
|
+
.claude/skills/go-scaffold/SKILL.md
|
|
209
|
+
go-scaffold.config.json
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Supported stack
|
|
213
|
+
|
|
214
|
+
| Package | Version |
|
|
215
|
+
|---|---|
|
|
216
|
+
| Gin | v1.10.0 |
|
|
217
|
+
| GORM + postgres driver | v1.25.12 / v1.5.9 |
|
|
218
|
+
| validator/v10 | v10.20.0 |
|
|
219
|
+
| google/uuid | v1.6.0 |
|
|
220
|
+
|
|
221
|
+
## License
|
|
222
|
+
|
|
223
|
+
MIT
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createProject = createProject;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
10
|
+
const template_renderer_1 = require("../utils/template-renderer");
|
|
11
|
+
const create_manifest_1 = require("../templates/create-manifest");
|
|
12
|
+
const config_1 = require("../utils/config");
|
|
13
|
+
const naming_1 = require("../utils/naming");
|
|
14
|
+
const create_wizard_1 = require("../prompts/create-wizard");
|
|
15
|
+
async function createProject(rawName, opts) {
|
|
16
|
+
const trimmed = (rawName ?? (await (0, create_wizard_1.promptProjectName)())).trim();
|
|
17
|
+
if (!trimmed)
|
|
18
|
+
throw new Error("project name is required");
|
|
19
|
+
(0, naming_1.assertValidGoModulePath)(trimmed);
|
|
20
|
+
const goModule = trimmed;
|
|
21
|
+
const projectName = trimmed.includes("/") ? trimmed.split("/").pop() : trimmed;
|
|
22
|
+
const projectDir = path_1.default.resolve(process.cwd(), projectName);
|
|
23
|
+
if (fs_extra_1.default.existsSync(projectDir) && fs_extra_1.default.readdirSync(projectDir).length > 0) {
|
|
24
|
+
throw new Error(`${projectDir} already exists and is not empty`);
|
|
25
|
+
}
|
|
26
|
+
let features;
|
|
27
|
+
let apiPrefix;
|
|
28
|
+
if (opts.defaults) {
|
|
29
|
+
features = { docker: opts.docker ?? true, openapiDocs: opts.openapiDocs ?? true };
|
|
30
|
+
apiPrefix = (0, naming_1.normalizeApiPrefix)(opts.apiPrefix ?? "v1");
|
|
31
|
+
const check = (0, naming_1.validateApiPrefix)(apiPrefix);
|
|
32
|
+
if (check !== true)
|
|
33
|
+
throw new Error(check);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
({ features, apiPrefix } = await (0, create_wizard_1.runCreateWizard)());
|
|
37
|
+
}
|
|
38
|
+
const context = {
|
|
39
|
+
projectName,
|
|
40
|
+
goModule,
|
|
41
|
+
dbName: (0, naming_1.toDbName)(projectName),
|
|
42
|
+
apiPrefix,
|
|
43
|
+
...features,
|
|
44
|
+
};
|
|
45
|
+
await fs_extra_1.default.ensureDir(projectDir);
|
|
46
|
+
await (0, template_renderer_1.applyTemplateEntries)(projectDir, create_manifest_1.CREATE_MANIFEST, context);
|
|
47
|
+
(0, template_renderer_1.gofmtTree)(projectDir);
|
|
48
|
+
(0, config_1.writeConfig)(projectDir, { projectName, goModule, apiPrefix, features });
|
|
49
|
+
console.log(picocolors_1.default.green(`\ncreated ${projectName}/`));
|
|
50
|
+
console.log(`\ncd ${projectName}`);
|
|
51
|
+
if (features.docker)
|
|
52
|
+
console.log(`make docker-up`);
|
|
53
|
+
console.log(`make db-create`);
|
|
54
|
+
console.log(`go mod tidy`);
|
|
55
|
+
console.log(`make run`);
|
|
56
|
+
console.log(picocolors_1.default.dim(`\nadd your first domain: go-scaffold generate module <name>`));
|
|
57
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.generateModule = generateModule;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
10
|
+
const config_1 = require("../utils/config");
|
|
11
|
+
const naming_1 = require("../utils/naming");
|
|
12
|
+
const template_renderer_1 = require("../utils/template-renderer");
|
|
13
|
+
const module_manifest_1 = require("../templates/module-manifest");
|
|
14
|
+
const main_patcher_1 = require("../utils/main-patcher");
|
|
15
|
+
const openapi_patcher_1 = require("../utils/openapi-patcher");
|
|
16
|
+
const migrations_1 = require("../utils/migrations");
|
|
17
|
+
const generate_wizard_1 = require("../prompts/generate-wizard");
|
|
18
|
+
async function generateModule(rawName, opts, projectDir = process.cwd()) {
|
|
19
|
+
const config = (0, config_1.readConfig)(projectDir);
|
|
20
|
+
const naming = (0, naming_1.resolveModuleNaming)(rawName ?? (await (0, generate_wizard_1.promptModuleName)()));
|
|
21
|
+
const modulePath = naming.pkg;
|
|
22
|
+
const moduleDir = path_1.default.join(projectDir, "internal", "app", modulePath);
|
|
23
|
+
if (fs_extra_1.default.existsSync(moduleDir) && fs_extra_1.default.readdirSync(moduleDir).length > 0) {
|
|
24
|
+
throw new Error(`${moduleDir} already exists — pick a different name or delete it first`);
|
|
25
|
+
}
|
|
26
|
+
const context = {
|
|
27
|
+
...naming,
|
|
28
|
+
goModule: config.goModule,
|
|
29
|
+
dbName: (0, naming_1.toDbName)(config.projectName),
|
|
30
|
+
modulePath,
|
|
31
|
+
};
|
|
32
|
+
const moduleFiles = opts.full ? module_manifest_1.MODULE_FILES : module_manifest_1.MODULE_FILES_MINIMAL;
|
|
33
|
+
const moduleEntries = moduleFiles.map((f) => ({
|
|
34
|
+
template: f.template,
|
|
35
|
+
output: path_1.default.join("internal", "app", modulePath, f.output),
|
|
36
|
+
}));
|
|
37
|
+
await (0, template_renderer_1.applyTemplateEntries)(projectDir, moduleEntries, context);
|
|
38
|
+
// skip if a create_<plural> migration already exists — re-running after
|
|
39
|
+
// only the module folder was deleted shouldn't leave a duplicate migration.
|
|
40
|
+
const migrationsDir = path_1.default.join(projectDir, "migrations");
|
|
41
|
+
const migrationExists = fs_extra_1.default.existsSync(migrationsDir) &&
|
|
42
|
+
fs_extra_1.default.readdirSync(migrationsDir).some((f) => f.endsWith(`_create_${naming.plural}.up.sql`));
|
|
43
|
+
let seq = "";
|
|
44
|
+
if (!migrationExists) {
|
|
45
|
+
seq = (0, migrations_1.nextMigrationSeq)(migrationsDir);
|
|
46
|
+
const migrationEntries = [
|
|
47
|
+
{
|
|
48
|
+
template: "generate/module/migration.up.sql.hbs",
|
|
49
|
+
output: path_1.default.join("migrations", `${seq}_create_${naming.plural}.up.sql`),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
template: "generate/module/migration.down.sql.hbs",
|
|
53
|
+
output: path_1.default.join("migrations", `${seq}_create_${naming.plural}.down.sql`),
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
await (0, template_renderer_1.applyTemplateEntries)(projectDir, migrationEntries, context);
|
|
57
|
+
}
|
|
58
|
+
const mainGoPath = path_1.default.join(projectDir, "cmd", "api", "main.go");
|
|
59
|
+
(0, main_patcher_1.patchMainGo)(mainGoPath, {
|
|
60
|
+
goModule: config.goModule,
|
|
61
|
+
modulePath,
|
|
62
|
+
pkg: naming.pkg,
|
|
63
|
+
pascalName: naming.pascalName,
|
|
64
|
+
});
|
|
65
|
+
let docsMessage = "";
|
|
66
|
+
const openapiPath = path_1.default.join(projectDir, "docs", "openapi.yaml");
|
|
67
|
+
if (opts.full && config.features.openapiDocs && fs_extra_1.default.existsSync(openapiPath)) {
|
|
68
|
+
const docsEntries = [
|
|
69
|
+
{ template: "generate/module/docs/collection.yaml.hbs", output: path_1.default.join("docs", naming.plural, "collection.yaml") },
|
|
70
|
+
{ template: "generate/module/docs/item.yaml.hbs", output: path_1.default.join("docs", naming.plural, "item.yaml") },
|
|
71
|
+
{ template: "generate/module/docs/schemas.yaml.hbs", output: path_1.default.join("docs", naming.plural, "schemas.yaml") },
|
|
72
|
+
];
|
|
73
|
+
await (0, template_renderer_1.applyTemplateEntries)(projectDir, docsEntries, context);
|
|
74
|
+
(0, openapi_patcher_1.patchOpenapiIndex)(openapiPath, naming, config.apiPrefix);
|
|
75
|
+
docsMessage = `\ndocs: docs/${naming.plural}/{collection,item,schemas}.yaml, wired into docs/openapi.yaml`;
|
|
76
|
+
}
|
|
77
|
+
(0, template_renderer_1.gofmtTree)(projectDir);
|
|
78
|
+
const routePath = config.apiPrefix ? `/${config.apiPrefix}/${naming.plural}` : `/${naming.plural}`;
|
|
79
|
+
console.log(picocolors_1.default.green(`\ngenerated internal/app/${modulePath}/`));
|
|
80
|
+
if (opts.full) {
|
|
81
|
+
console.log(`registered route ${routePath} in cmd/api/main.go`);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
console.log(`registered empty route group ${routePath} in cmd/api/main.go — ` +
|
|
85
|
+
`add endpoints with \`go-scaffold generate method ${naming.pkg} <name> --type ...\``);
|
|
86
|
+
}
|
|
87
|
+
if (seq) {
|
|
88
|
+
console.log(`migration: migrations/${seq}_create_${naming.plural}.{up,down}.sql`);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
console.log(`migration: reused existing migrations/*_create_${naming.plural}.{up,down}.sql`);
|
|
92
|
+
}
|
|
93
|
+
if (docsMessage)
|
|
94
|
+
console.log(docsMessage);
|
|
95
|
+
console.log(picocolors_1.default.dim(`\nnext: add real fields to model.go/dto.go, run \`go build ./...\`, then apply the migration ` +
|
|
96
|
+
`(AUTO_MIGRATE=true handles it in dev, or \`migrate -path migrations -database "$DB_DSN" up\`)`));
|
|
97
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.generateMethod = generateMethod;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
10
|
+
const config_1 = require("../utils/config");
|
|
11
|
+
const naming_1 = require("../utils/naming");
|
|
12
|
+
const method_patcher_1 = require("../utils/method-patcher");
|
|
13
|
+
const template_renderer_1 = require("../utils/template-renderer");
|
|
14
|
+
const generate_wizard_1 = require("../prompts/generate-wizard");
|
|
15
|
+
// the actual URL the new route answers on — printed so the user can add the
|
|
16
|
+
// matching openapi.yaml entry by hand (methods are deliberately not wired into
|
|
17
|
+
// the spec; see the note in docs/openapi.yaml). Mirrors the paths registered
|
|
18
|
+
// in method-patcher.ts.
|
|
19
|
+
function routeHint(naming, method, type, apiPrefix, getMode, field) {
|
|
20
|
+
const base = apiPrefix ? `/${apiPrefix}/${naming.plural}` : `/${naming.plural}`;
|
|
21
|
+
if (type === "get" && getMode === "all")
|
|
22
|
+
return `GET ${base}/${method.pathSegment}`;
|
|
23
|
+
if (type === "get")
|
|
24
|
+
return `GET ${base}/${(0, naming_1.toDbName)(field ?? "")}/{${(0, naming_1.toCamelCase)(field ?? "")}}`;
|
|
25
|
+
if (type === "post")
|
|
26
|
+
return `POST ${base}/${method.pathSegment}`;
|
|
27
|
+
if (type === "delete")
|
|
28
|
+
return `DELETE ${base}/{id}/${method.pathSegment}`;
|
|
29
|
+
return `${type.toUpperCase()} ${base}/{id}/${method.pathSegment}`;
|
|
30
|
+
}
|
|
31
|
+
async function generateMethod(moduleNameArg, methodNameArg, opts, projectDir = process.cwd()) {
|
|
32
|
+
const config = (0, config_1.readConfig)(projectDir);
|
|
33
|
+
const naming = (0, naming_1.resolveModuleNaming)(moduleNameArg ?? (await (0, generate_wizard_1.promptModuleName)()));
|
|
34
|
+
const modulePath = naming.pkg;
|
|
35
|
+
const moduleDir = path_1.default.join(projectDir, "internal", "app", modulePath);
|
|
36
|
+
const paths = {
|
|
37
|
+
dtoPath: path_1.default.join(moduleDir, "dto.go"),
|
|
38
|
+
repositoryPath: path_1.default.join(moduleDir, "repository.go"),
|
|
39
|
+
servicePath: path_1.default.join(moduleDir, "service.go"),
|
|
40
|
+
handlerPath: path_1.default.join(moduleDir, "handler.go"),
|
|
41
|
+
serviceTestPath: path_1.default.join(moduleDir, "service_test.go"),
|
|
42
|
+
};
|
|
43
|
+
for (const p of Object.values(paths)) {
|
|
44
|
+
if (!fs_extra_1.default.existsSync(p)) {
|
|
45
|
+
throw new Error(`module "${naming.pkg}" not found at ${moduleDir} (missing ${path_1.default.basename(p)}) — ` +
|
|
46
|
+
`run \`go-scaffold generate module ${naming.pkg}\` first`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const type = opts.type ?? (await (0, generate_wizard_1.promptMethodType)());
|
|
50
|
+
if (opts.getMode && type !== "get") {
|
|
51
|
+
throw new Error("--get-mode can only be used with --type get");
|
|
52
|
+
}
|
|
53
|
+
if (opts.field && !(type === "get" && opts.getMode === "one")) {
|
|
54
|
+
throw new Error("--field can only be used with --type get --get-mode one");
|
|
55
|
+
}
|
|
56
|
+
const getMode = type === "get" ? opts.getMode ?? (await (0, generate_wizard_1.promptGetMode)()) : undefined;
|
|
57
|
+
const field = type === "get" && getMode === "one" ? opts.field ?? (await (0, generate_wizard_1.promptLookupField)()) : undefined;
|
|
58
|
+
// field becomes a Go param name (`func (...)(ctx, <field> string)`)
|
|
59
|
+
if (field)
|
|
60
|
+
(0, naming_1.assertNotGoKeyword)((0, naming_1.toCamelCase)(field), "lookup field");
|
|
61
|
+
const method = (0, naming_1.resolveMethodNaming)(methodNameArg ?? (await (0, generate_wizard_1.promptMethodName)()));
|
|
62
|
+
(0, method_patcher_1.patchMethod)(paths, naming, method, { type, getMode, field }, config.goModule);
|
|
63
|
+
(0, template_renderer_1.gofmtTree)(projectDir);
|
|
64
|
+
console.log(picocolors_1.default.green(`\nadded "${method.name}" to internal/app/${modulePath}/`));
|
|
65
|
+
console.log(`route: ${routeHint(naming, method, type, config.apiPrefix, getMode, field)}`);
|
|
66
|
+
if (config.features.openapiDocs) {
|
|
67
|
+
console.log(picocolors_1.default.yellow(`docs: add this route to docs/openapi.yaml by hand — \`generate method\` doesn't touch the spec`));
|
|
68
|
+
}
|
|
69
|
+
console.log(picocolors_1.default.dim(`\nnext: fill in the TODO in service.go, then \`go build ./...\` / \`go test ./...\``));
|
|
70
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.removeModule = removeModule;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
10
|
+
const prompts_1 = require("@inquirer/prompts");
|
|
11
|
+
const config_1 = require("../utils/config");
|
|
12
|
+
const naming_1 = require("../utils/naming");
|
|
13
|
+
const main_patcher_1 = require("../utils/main-patcher");
|
|
14
|
+
const openapi_patcher_1 = require("../utils/openapi-patcher");
|
|
15
|
+
const template_renderer_1 = require("../utils/template-renderer");
|
|
16
|
+
const generate_wizard_1 = require("../prompts/generate-wizard");
|
|
17
|
+
// removeModule is the inverse of generateModule: deletes the domain package and
|
|
18
|
+
// pulls its wiring back out of main.go / openapi.yaml / migrations, so dropping
|
|
19
|
+
// a domain is one command instead of hand-editing 3+ files (the error-prone
|
|
20
|
+
// path that produced the duplicate-registration bug in the first place).
|
|
21
|
+
async function removeModule(rawName, opts, projectDir = process.cwd()) {
|
|
22
|
+
const config = (0, config_1.readConfig)(projectDir);
|
|
23
|
+
const naming = (0, naming_1.resolveModuleNaming)(rawName ?? (await (0, generate_wizard_1.promptModuleName)()));
|
|
24
|
+
const modulePath = naming.pkg;
|
|
25
|
+
const moduleDir = path_1.default.join(projectDir, "internal", "app", modulePath);
|
|
26
|
+
if (!fs_extra_1.default.existsSync(moduleDir)) {
|
|
27
|
+
throw new Error(`module "${naming.pkg}" not found at internal/app/${modulePath} — nothing to remove`);
|
|
28
|
+
}
|
|
29
|
+
if (!opts.yes) {
|
|
30
|
+
const ok = await (0, prompts_1.confirm)({
|
|
31
|
+
message: `Remove module "${naming.pkg}"? Deletes internal/app/${modulePath}/, its migration, and un-wires main.go/openapi.yaml`,
|
|
32
|
+
default: false,
|
|
33
|
+
});
|
|
34
|
+
if (!ok)
|
|
35
|
+
throw new Error("removal cancelled");
|
|
36
|
+
}
|
|
37
|
+
// 1. the domain package
|
|
38
|
+
fs_extra_1.default.removeSync(moduleDir);
|
|
39
|
+
// 2. main.go wiring
|
|
40
|
+
(0, main_patcher_1.unpatchMainGo)(path_1.default.join(projectDir, "cmd", "api", "main.go"), {
|
|
41
|
+
goModule: config.goModule,
|
|
42
|
+
modulePath,
|
|
43
|
+
pkg: naming.pkg,
|
|
44
|
+
pascalName: naming.pascalName,
|
|
45
|
+
});
|
|
46
|
+
// 3. openapi index + per-module docs
|
|
47
|
+
const openapiPath = path_1.default.join(projectDir, "docs", "openapi.yaml");
|
|
48
|
+
if (fs_extra_1.default.existsSync(openapiPath)) {
|
|
49
|
+
(0, openapi_patcher_1.unpatchOpenapiIndex)(openapiPath, naming, config.apiPrefix);
|
|
50
|
+
fs_extra_1.default.removeSync(path_1.default.join(projectDir, "docs", naming.plural));
|
|
51
|
+
}
|
|
52
|
+
// 4. migration files (up + down)
|
|
53
|
+
const migrationsDir = path_1.default.join(projectDir, "migrations");
|
|
54
|
+
const removedMigrations = [];
|
|
55
|
+
if (fs_extra_1.default.existsSync(migrationsDir)) {
|
|
56
|
+
for (const f of fs_extra_1.default.readdirSync(migrationsDir)) {
|
|
57
|
+
if (f.endsWith(`_create_${naming.plural}.up.sql`) || f.endsWith(`_create_${naming.plural}.down.sql`)) {
|
|
58
|
+
fs_extra_1.default.removeSync(path_1.default.join(migrationsDir, f));
|
|
59
|
+
removedMigrations.push(f);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
(0, template_renderer_1.gofmtTree)(projectDir);
|
|
64
|
+
console.log(picocolors_1.default.green(`\nremoved module "${naming.pkg}"`));
|
|
65
|
+
console.log(` deleted internal/app/${modulePath}/`);
|
|
66
|
+
console.log(` un-wired cmd/api/main.go`);
|
|
67
|
+
if (fs_extra_1.default.existsSync(openapiPath))
|
|
68
|
+
console.log(` un-wired docs/openapi.yaml + deleted docs/${naming.plural}/`);
|
|
69
|
+
if (removedMigrations.length)
|
|
70
|
+
console.log(` deleted ${removedMigrations.join(", ")}`);
|
|
71
|
+
console.log(picocolors_1.default.yellow(`\nnote: the ${naming.plural} table (if migrated) is untouched — drop it yourself, or add a down migration`));
|
|
72
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const prompts_1 = require("@inquirer/prompts");
|
|
9
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
10
|
+
const create_1 = require("./commands/create");
|
|
11
|
+
const generate_1 = require("./commands/generate");
|
|
12
|
+
const method_1 = require("./commands/method");
|
|
13
|
+
const remove_1 = require("./commands/remove");
|
|
14
|
+
const program = new commander_1.Command();
|
|
15
|
+
program
|
|
16
|
+
.name("go-scaffold")
|
|
17
|
+
.description("Scaffold Gin + GORM + Postgres Go backend projects with a consistent domain-module standard")
|
|
18
|
+
.version("0.1.0");
|
|
19
|
+
program
|
|
20
|
+
.command("create [name]")
|
|
21
|
+
.alias("c")
|
|
22
|
+
.description("scaffold a new project (bare skeleton — add domains with `generate module`)")
|
|
23
|
+
.option("--defaults", "skip the wizard, use defaults (for CI/scripting)")
|
|
24
|
+
.option("--no-docker", "skip docker-compose.yml (only applies with --defaults)")
|
|
25
|
+
.option("--no-openapi-docs", "skip docs/openapi.yaml (only applies with --defaults)")
|
|
26
|
+
.option("--api-prefix <prefix>", 'URL prefix every route is grouped under (default "v1"; pass "" for none)')
|
|
27
|
+
.action(async (name, opts) => {
|
|
28
|
+
try {
|
|
29
|
+
await (0, create_1.createProject)(name, {
|
|
30
|
+
defaults: opts.defaults,
|
|
31
|
+
docker: opts.docker,
|
|
32
|
+
openapiDocs: opts.openapiDocs,
|
|
33
|
+
apiPrefix: opts.apiPrefix,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
console.error(picocolors_1.default.red(err.message));
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
const generate = program
|
|
42
|
+
.command("generate")
|
|
43
|
+
.alias("g")
|
|
44
|
+
.description("add to an existing go-scaffold project")
|
|
45
|
+
.action(async () => {
|
|
46
|
+
// bare `generate`/`g` — ask which target, then delegate (each subcommand
|
|
47
|
+
// still prompts for anything else it's missing, e.g. the name).
|
|
48
|
+
try {
|
|
49
|
+
const target = await (0, prompts_1.select)({
|
|
50
|
+
message: "What do you want to generate?",
|
|
51
|
+
choices: [
|
|
52
|
+
{ name: "Module (full CRUD domain)", value: "module" },
|
|
53
|
+
{ name: "Method (add one endpoint to an existing module)", value: "method" },
|
|
54
|
+
],
|
|
55
|
+
});
|
|
56
|
+
if (target === "module") {
|
|
57
|
+
await (0, generate_1.generateModule)(undefined, { full: true });
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
await (0, method_1.generateMethod)(undefined, undefined, {});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
console.error(picocolors_1.default.red(err.message));
|
|
65
|
+
process.exitCode = 1;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
generate
|
|
69
|
+
.command("module [name]")
|
|
70
|
+
.alias("m")
|
|
71
|
+
.description("scaffold a domain module — full CRUD by default, or a bare skeleton with --no-full")
|
|
72
|
+
.option("--no-full", "minimal skeleton (model/errors/repository, no default CRUD) — add endpoints one at a time with `generate method`")
|
|
73
|
+
.action(async (name, opts) => {
|
|
74
|
+
try {
|
|
75
|
+
await (0, generate_1.generateModule)(name, { full: opts.full });
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
console.error(picocolors_1.default.red(err.message));
|
|
79
|
+
process.exitCode = 1;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
generate
|
|
83
|
+
.command("method [module] [name]")
|
|
84
|
+
.alias("me")
|
|
85
|
+
.description("add one endpoint to an existing module (patches handler/service in place)")
|
|
86
|
+
.option("--type <type>", "get|post|put|patch|delete")
|
|
87
|
+
.option("--get-mode <mode>", "for --type get only: all|one")
|
|
88
|
+
.option("--field <name>", "for --type get --get-mode one: the lookup field (e.g. email, status)")
|
|
89
|
+
.action(async (moduleName, methodName, opts) => {
|
|
90
|
+
try {
|
|
91
|
+
const type = opts.type;
|
|
92
|
+
if (type && !["get", "post", "put", "patch", "delete"].includes(type)) {
|
|
93
|
+
throw new Error(`--type must be one of: get, post, put, patch, delete (got "${type}")`);
|
|
94
|
+
}
|
|
95
|
+
const getMode = opts.getMode;
|
|
96
|
+
if (getMode && !["all", "one"].includes(getMode)) {
|
|
97
|
+
throw new Error(`--get-mode must be "all" or "one" (got "${getMode}")`);
|
|
98
|
+
}
|
|
99
|
+
await (0, method_1.generateMethod)(moduleName, methodName, {
|
|
100
|
+
type,
|
|
101
|
+
getMode,
|
|
102
|
+
field: opts.field,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
console.error(picocolors_1.default.red(err.message));
|
|
107
|
+
process.exitCode = 1;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
const remove = program
|
|
111
|
+
.command("remove")
|
|
112
|
+
.alias("rm")
|
|
113
|
+
.description("remove a domain module (deletes the package + un-wires main.go/openapi.yaml/migration)")
|
|
114
|
+
.action(async () => {
|
|
115
|
+
// bare `remove`/`rm` — module is the only target, so prompt for the name
|
|
116
|
+
try {
|
|
117
|
+
await (0, remove_1.removeModule)(undefined, {});
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
console.error(picocolors_1.default.red(err.message));
|
|
121
|
+
process.exitCode = 1;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
remove
|
|
125
|
+
.command("module [name]")
|
|
126
|
+
.alias("m")
|
|
127
|
+
.description("delete a domain module and reverse everything `generate module` wired up")
|
|
128
|
+
.option("-y, --yes", "skip the confirmation prompt")
|
|
129
|
+
.action(async (name, opts) => {
|
|
130
|
+
try {
|
|
131
|
+
await (0, remove_1.removeModule)(name, { yes: opts.yes });
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
console.error(picocolors_1.default.red(err.message));
|
|
135
|
+
process.exitCode = 1;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
program.parseAsync(process.argv);
|