@nakedev/go-scaffold 0.3.3 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +288 -50
- package/dist/commands/auth.js +53 -22
- package/dist/commands/config.js +50 -0
- package/dist/commands/create.js +32 -2
- package/dist/commands/generate.js +25 -2
- package/dist/commands/method.js +22 -7
- package/dist/commands/migration.js +2 -2
- package/dist/commands/observability.js +3 -3
- package/dist/commands/rbac.js +3 -3
- package/dist/commands/undo.js +5 -0
- package/dist/commands/worker.js +1 -1
- package/dist/index.js +186 -59
- package/dist/prompts/auth-wizard.js +40 -6
- package/dist/prompts/create-wizard.js +43 -2
- package/dist/prompts/generate-wizard.js +89 -9
- package/dist/templates/auth-manifest.js +31 -1
- package/dist/templates/create-manifest.js +4 -0
- package/dist/templates/module-manifest.js +37 -1
- package/dist/templates/rbac-manifest.js +1 -0
- package/dist/types.js +6 -0
- package/dist/utils/auth-patcher.js +115 -24
- package/dist/utils/config.js +147 -3
- package/dist/utils/main-patcher.js +29 -27
- package/dist/utils/marker-patch.js +7 -1
- package/dist/utils/method-patcher.js +261 -81
- package/dist/utils/module-profile.js +32 -0
- package/dist/utils/observability-patcher.js +2 -2
- package/dist/utils/platform-patcher.js +29 -7
- package/dist/utils/rbac-patcher.js +97 -75
- package/package.json +7 -2
- package/templates/add/auth/cmd/seed/main.go.hbs +13 -3
- package/templates/add/auth/docs/login.yaml.hbs +11 -1
- package/templates/add/auth/docs/mfa-verify.yaml.hbs +19 -0
- package/templates/add/auth/docs/provider-exchange.yaml.hbs +40 -0
- package/templates/add/auth/docs/provider-login.yaml.hbs +31 -0
- package/templates/add/auth/docs/refresh.yaml.hbs +7 -0
- package/templates/add/auth/docs/register.yaml.hbs +7 -0
- package/templates/add/auth/docs/reset-password.yaml.hbs +1 -1
- package/templates/add/auth/docs/schemas.yaml.hbs +59 -1
- package/templates/add/auth/docs/users-me-mfa-confirm.yaml.hbs +19 -0
- package/templates/add/auth/docs/users-me-mfa-disable.yaml.hbs +15 -0
- package/templates/add/auth/docs/users-me-mfa-setup.yaml.hbs +14 -0
- package/templates/add/auth/docs/users-me-mfa.yaml.hbs +12 -0
- package/templates/add/auth/internal/app/user/application/oauth.go.hbs +132 -0
- package/templates/add/auth/internal/app/user/application/recovery.go.hbs +113 -0
- package/templates/add/auth/internal/app/user/browser_policy.go.hbs +98 -0
- package/templates/add/auth/internal/app/user/composition.go.hbs +165 -0
- package/templates/add/auth/internal/app/user/contracts.go.hbs +88 -0
- package/templates/add/auth/internal/app/user/dto.go.hbs +57 -0
- package/templates/add/auth/internal/app/user/errors.go.hbs +25 -0
- package/templates/add/auth/internal/app/user/external_login.go.hbs +208 -0
- package/templates/add/auth/internal/app/user/handler.go.hbs +60 -203
- package/templates/add/auth/internal/app/user/handler_local.go.hbs +75 -0
- package/templates/add/auth/internal/app/user/handler_mfa.go.hbs +83 -0
- package/templates/add/auth/internal/app/user/handler_oauth.go.hbs +70 -0
- package/templates/add/auth/internal/app/user/handler_recovery.go.hbs +49 -0
- package/templates/add/auth/internal/app/user/handler_test.go.hbs +290 -0
- package/templates/add/auth/internal/app/user/handler_user.go.hbs +41 -0
- package/templates/add/auth/internal/app/user/jwt.go.hbs +6 -59
- package/templates/add/auth/internal/app/user/local_auth.go.hbs +98 -0
- package/templates/add/auth/internal/app/user/mfa_service.go.hbs +450 -0
- package/templates/add/auth/internal/app/user/mfa_service_test.go.hbs +199 -0
- package/templates/add/auth/internal/app/user/mfa_store.go.hbs +127 -0
- package/templates/add/auth/internal/app/user/mfa_store_test.go.hbs +174 -0
- package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +8 -2
- package/templates/add/auth/internal/app/user/model/identity.go.hbs +4 -3
- package/templates/add/auth/internal/app/user/model/mfa_challenge.go.hbs +17 -0
- package/templates/add/auth/internal/app/user/model/mfa_enrollment.go.hbs +20 -0
- package/templates/add/auth/internal/app/user/model/mfa_recovery_code.go.hbs +17 -0
- package/templates/add/auth/internal/app/user/model/user.go.hbs +3 -2
- package/templates/add/auth/internal/app/user/provider_test.go.hbs +286 -0
- package/templates/add/auth/internal/app/user/recovery_service.go.hbs +114 -0
- package/templates/add/auth/internal/app/user/repository.go.hbs +2 -0
- package/templates/add/auth/internal/app/user/service.go.hbs +82 -478
- package/templates/add/auth/internal/app/user/service_test.go.hbs +601 -45
- package/templates/add/auth/internal/app/user/session_cookie.go.hbs +33 -0
- package/templates/add/auth/internal/app/user/sessions.go.hbs +99 -0
- package/templates/add/auth/internal/app/user/tokenstore.go.hbs +42 -14
- package/templates/add/auth/internal/app/user/tokenstore_pg.go.hbs +105 -40
- package/templates/add/auth/internal/app/user/tokenstore_pg_test.go.hbs +96 -0
- package/templates/add/auth/internal/app/user/tokenstore_recovery.go.hbs +58 -0
- package/templates/add/auth/internal/app/user/tokenstore_redis.go.hbs +144 -70
- package/templates/add/auth/internal/app/user/tokenstore_redis_test.go.hbs +185 -0
- package/templates/add/auth/internal/app/user/user_query.go.hbs +65 -0
- package/templates/add/auth/internal/platform/authprovider/google/google.go.hbs +389 -0
- package/templates/add/auth/internal/platform/authprovider/google/google_test.go.hbs +312 -0
- package/templates/add/auth/migrations/create_auth_tokens.up.sql.hbs +9 -4
- package/templates/add/auth/migrations/create_identities.up.sql.hbs +1 -1
- package/templates/add/auth/migrations/create_mfa.down.sql.hbs +3 -0
- package/templates/add/auth/migrations/create_mfa.up.sql.hbs +29 -0
- package/templates/add/auth/migrations/create_users.up.sql.hbs +2 -2
- package/templates/add/rbac/internal/app/role/composition.go.hbs +35 -0
- package/templates/add/rbac/internal/app/role/service.go.hbs +12 -12
- package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +340 -121
- package/templates/create/base/.env.example.hbs +0 -1
- package/templates/create/base/AGENTS.md.hbs +255 -67
- package/templates/create/base/Makefile.hbs +2 -1
- package/templates/create/base/README.md.hbs +45 -17
- package/templates/create/base/cmd/api/wiring.go.hbs +18 -25
- package/templates/create/base/internal/platform/database/database.go.hbs +3 -3
- package/templates/create/base/internal/shared/apperror/apperror.go.hbs +15 -2
- package/templates/create/base/internal/shared/config/config.go.hbs +0 -8
- package/templates/create/base/internal/shared/middleware/cors_test.go.hbs +40 -0
- package/templates/create/base/internal/shared/middleware/error.go.hbs +15 -5
- package/templates/create/features/docs/architecture.md.hbs +38 -16
- package/templates/create/features/docs/patterns.md.hbs +40 -21
- package/templates/create/features/docs/techstack.md.hbs +3 -3
- package/templates/generate/module/commands.go.hbs +95 -0
- package/templates/generate/module/composition.go.hbs +23 -0
- package/templates/generate/module/cqrs_test.go.hbs +7 -0
- package/templates/generate/module/handler.go.hbs +50 -5
- package/templates/generate/module/minimal/commands.go.hbs +34 -0
- package/templates/generate/module/minimal/handler.go.hbs +34 -0
- package/templates/generate/module/minimal/queries.go.hbs +45 -0
- package/templates/generate/module/minimal/service.go.hbs +27 -1
- package/templates/generate/module/queries.go.hbs +62 -0
- package/templates/generate/module/service.go.hbs +61 -5
- package/templates/add/auth/docs/google-callback.yaml.hbs +0 -22
- package/templates/add/auth/docs/google-login.yaml.hbs +0 -7
package/dist/index.js
CHANGED
|
@@ -20,7 +20,9 @@ const observability_1 = require("./commands/observability");
|
|
|
20
20
|
const worker_wizard_1 = require("./prompts/worker-wizard");
|
|
21
21
|
const auth_wizard_1 = require("./prompts/auth-wizard");
|
|
22
22
|
const generate_wizard_1 = require("./prompts/generate-wizard");
|
|
23
|
-
const config_1 = require("./
|
|
23
|
+
const config_1 = require("./commands/config");
|
|
24
|
+
const config_2 = require("./utils/config");
|
|
25
|
+
const module_profile_1 = require("./utils/module-profile");
|
|
24
26
|
// fail is every command's catch: one place so the two non-obvious cases stay
|
|
25
27
|
// consistent. @inquirer/prompts throws ExitPromptError on Ctrl-C, and its raw
|
|
26
28
|
// message ("User force closed the prompt with 0 null") tells a user nothing.
|
|
@@ -41,17 +43,20 @@ const program = new commander_1.Command();
|
|
|
41
43
|
program
|
|
42
44
|
.name("go-scaffold")
|
|
43
45
|
.description("Scaffold Gin + GORM + Postgres Go backend projects with a consistent domain-module standard\n\n" +
|
|
44
|
-
"Run `go-scaffold` with no arguments to pick what to do from a menu.
|
|
46
|
+
"Run `go-scaffold` with no arguments to pick what to do from a menu. Interactive commands ask for values you omit; read-only commands (`config show`, `config validate`) print or check state without prompts.")
|
|
45
47
|
.version((0, version_1.cliVersion)());
|
|
46
48
|
program
|
|
47
49
|
.command("create [name]")
|
|
48
50
|
.alias("c")
|
|
49
51
|
.description("scaffold a new project (bare skeleton — add domains with `generate module`)")
|
|
50
|
-
.option("--defaults", "skip
|
|
51
|
-
.option("--no-docker", "
|
|
52
|
-
.option("--no-openapi-docs", "
|
|
53
|
-
.option("--observability", "
|
|
54
|
-
.option("--api-prefix <prefix>", '
|
|
52
|
+
.option("--defaults", "skip settings prompts; use Docker/OpenAPI on, no prefix, and Lean module defaults (still asks for name if omitted)")
|
|
53
|
+
.option("--no-docker", "do not create docker-compose.yml or include a local Postgres service")
|
|
54
|
+
.option("--no-openapi-docs", "do not create docs/openapi.yaml or per-module OpenAPI files")
|
|
55
|
+
.option("--observability", "include Prometheus /metrics and OpenTelemetry tracing; off unless this flag is passed")
|
|
56
|
+
.option("--api-prefix <prefix>", 'group every API route under a prefix such as "v1" or "api/v1"; omit for no prefix')
|
|
57
|
+
.option("--module-profile <profile>", "default profile for future modules: lean, crud, or cqrs; replaces the two profile questions")
|
|
58
|
+
.option("--module-surface <surface>", "legacy axis flag for future modules: minimal or crud; wizard asks when omitted")
|
|
59
|
+
.option("--application-style <style>", "legacy axis flag for future modules: service or cqrs; wizard asks when omitted")
|
|
55
60
|
.action(async (name, opts) => {
|
|
56
61
|
try {
|
|
57
62
|
await (0, create_1.createProject)(name, {
|
|
@@ -60,37 +65,96 @@ program
|
|
|
60
65
|
openapiDocs: opts.openapiDocs,
|
|
61
66
|
observability: opts.observability,
|
|
62
67
|
apiPrefix: opts.apiPrefix,
|
|
68
|
+
moduleProfile: opts.moduleProfile,
|
|
69
|
+
moduleSurface: opts.moduleSurface,
|
|
70
|
+
applicationStyle: opts.applicationStyle,
|
|
63
71
|
});
|
|
64
72
|
}
|
|
65
73
|
catch (err) {
|
|
66
74
|
fail(err);
|
|
67
75
|
}
|
|
68
76
|
});
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// defaults
|
|
77
|
+
function assertModuleWizardInputs(name, opts, config) {
|
|
78
|
+
if (process.stdin.isTTY)
|
|
79
|
+
return;
|
|
80
|
+
const missing = [];
|
|
81
|
+
if (name === undefined)
|
|
82
|
+
missing.push("<name>");
|
|
83
|
+
// --defaults answers every optional wizard question. The name is still
|
|
84
|
+
// required because there is no safe module name to invent.
|
|
85
|
+
if (!opts.defaults) {
|
|
86
|
+
if (opts.profile === undefined && opts.full === undefined)
|
|
87
|
+
missing.push("--profile or --full/--no-full");
|
|
88
|
+
if (opts.profile === undefined && opts.cqrs === undefined)
|
|
89
|
+
missing.push("--profile or --cqrs");
|
|
90
|
+
if (config.features.auth && opts.auth === undefined) {
|
|
91
|
+
missing.push("--auth (or --defaults for no auth)");
|
|
92
|
+
}
|
|
93
|
+
if (opts.auth && config.features.rbac && opts.permission === undefined) {
|
|
94
|
+
missing.push("--permission <code> (or --defaults for no permission)");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (missing.length > 0) {
|
|
98
|
+
throw new Error(`no interactive terminal to prompt on — \`generate module\` is missing: ${missing.join(", ")}. ` +
|
|
99
|
+
"Pass the missing choices as flags, or add --defaults.");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
76
102
|
async function runModuleWizard(name, opts) {
|
|
77
|
-
let { full, auth, permission } = opts;
|
|
103
|
+
let { full, cqrs, auth, permission } = opts;
|
|
104
|
+
// Read before checking the prompt contract so the error can name the
|
|
105
|
+
// feature-dependent auth/RBAC choices, and so a non-project still fails
|
|
106
|
+
// with the useful project error rather than a list of flags.
|
|
107
|
+
const config = (0, config_2.readConfig)(process.cwd());
|
|
108
|
+
assertModuleWizardInputs(name, opts, config);
|
|
109
|
+
if (opts.profile) {
|
|
110
|
+
const architecture = (0, module_profile_1.architectureForModuleProfile)(opts.profile);
|
|
111
|
+
full = architecture.moduleSurface === "crud";
|
|
112
|
+
cqrs = architecture.applicationStyle === "cqrs";
|
|
113
|
+
}
|
|
78
114
|
if (!opts.defaults) {
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
// would present a choice whose only outcome is generateModule's error.
|
|
83
|
-
const config = (0, config_1.readConfig)(process.cwd());
|
|
115
|
+
// auth/permission questions are only asked when the project actually has
|
|
116
|
+
// the features they depend on — offering them otherwise would present a
|
|
117
|
+
// choice whose only outcome is generateModule's error.
|
|
84
118
|
if (name === undefined)
|
|
85
119
|
name = await (0, generate_wizard_1.promptModuleName)();
|
|
86
|
-
if (full === undefined)
|
|
87
|
-
|
|
120
|
+
if (opts.profile === undefined && full === undefined && cqrs === undefined) {
|
|
121
|
+
const profile = await (0, generate_wizard_1.promptModuleProfile)((0, module_profile_1.moduleProfileFor)(config.architecture.defaultModuleSurface, config.architecture.defaultApplicationStyle));
|
|
122
|
+
if (profile === "advanced") {
|
|
123
|
+
const architecture = await (0, generate_wizard_1.promptAdvancedModuleArchitecture)(config.architecture.defaultModuleSurface, config.architecture.defaultApplicationStyle);
|
|
124
|
+
full = architecture.moduleSurface === "crud";
|
|
125
|
+
cqrs = architecture.applicationStyle === "cqrs";
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
const architecture = (0, module_profile_1.architectureForModuleProfile)(profile);
|
|
129
|
+
full = architecture.moduleSurface === "crud";
|
|
130
|
+
cqrs = architecture.applicationStyle === "cqrs";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
// A legacy axis flag is still an explicit answer. Ask only for the
|
|
135
|
+
// other axis so `--full` never gets silently changed by the profile
|
|
136
|
+
// selector.
|
|
137
|
+
if (full === undefined) {
|
|
138
|
+
full = (await (0, generate_wizard_1.promptModuleSurface)(config.architecture.defaultModuleSurface)) === "crud";
|
|
139
|
+
}
|
|
140
|
+
if (cqrs === undefined) {
|
|
141
|
+
cqrs = (await (0, generate_wizard_1.promptApplicationStyle)(config.architecture.defaultApplicationStyle)) === "cqrs";
|
|
142
|
+
}
|
|
143
|
+
}
|
|
88
144
|
if (auth === undefined && config.features.auth)
|
|
89
145
|
auth = await (0, generate_wizard_1.promptModuleAuth)();
|
|
90
146
|
if (auth && permission === undefined && config.features.rbac)
|
|
91
147
|
permission = await (0, generate_wizard_1.promptModulePermission)();
|
|
92
148
|
}
|
|
93
|
-
|
|
149
|
+
else {
|
|
150
|
+
// --defaults means "use this project's recorded defaults". Fresh projects
|
|
151
|
+
// still resolve to the historical minimal/service behaviour, while a
|
|
152
|
+
// project config wizard can intentionally choose another default for new
|
|
153
|
+
// modules without making CI scripts interactive.
|
|
154
|
+
full ??= config.architecture.defaultModuleSurface === "crud";
|
|
155
|
+
cqrs ??= config.architecture.defaultApplicationStyle === "cqrs";
|
|
156
|
+
}
|
|
157
|
+
await (0, generate_1.generateModule)(name, { full: full ?? false, cqrs: cqrs ?? false, auth, permission });
|
|
94
158
|
}
|
|
95
159
|
// runGenerateWizard is `generate`/`g` run bare — asks which target, then
|
|
96
160
|
// delegates (each subcommand still prompts for anything else it's missing,
|
|
@@ -101,7 +165,7 @@ async function runGenerateWizard() {
|
|
|
101
165
|
const target = await (0, interactive_1.select)({
|
|
102
166
|
message: "What do you want to generate?",
|
|
103
167
|
choices: [
|
|
104
|
-
{ name: "Module (
|
|
168
|
+
{ name: "Module (choose Lean / CRUD / CQRS profile)", value: "module" },
|
|
105
169
|
{ name: "Method (add one endpoint to an existing module)", value: "method" },
|
|
106
170
|
{ name: "Migration (reserve a timestamped up/down SQL file pair)", value: "migration" },
|
|
107
171
|
],
|
|
@@ -119,7 +183,7 @@ async function runGenerateWizard() {
|
|
|
119
183
|
const generate = program
|
|
120
184
|
.command("generate")
|
|
121
185
|
.alias("g")
|
|
122
|
-
.description("add to an existing
|
|
186
|
+
.description("add a module, endpoint, or migration to an existing project; bare `generate` opens a target wizard")
|
|
123
187
|
.action(async () => {
|
|
124
188
|
try {
|
|
125
189
|
await runGenerateWizard();
|
|
@@ -131,21 +195,29 @@ const generate = program
|
|
|
131
195
|
generate
|
|
132
196
|
.command("module [name]")
|
|
133
197
|
.alias("m")
|
|
134
|
-
.description("scaffold a
|
|
135
|
-
.option("--
|
|
198
|
+
.description("scaffold a domain module; the wizard asks for a Lean, CRUD, CQRS, or Advanced profile")
|
|
199
|
+
.option("--profile <profile>", "choose the architecture preset: lean (minimal + service), crud (CRUD + service), or cqrs (minimal + CQRS); auth may still prompt")
|
|
200
|
+
.option("--full", "legacy alias for the CRUD profile: generate list/get/create/update/delete skeletons; fields and rules remain TODO")
|
|
201
|
+
.option("--cqrs", "legacy axis flag: split state-changing commands from read-only queries; combine with --full for CRUD + CQRS")
|
|
136
202
|
// kept working for muscle memory, hidden from help: minimal is already the
|
|
137
203
|
// default, so advertising a flag that asks for it is pure noise.
|
|
138
204
|
.addOption(new commander_1.Option("--no-full", "deprecated compatibility alias; minimal is already the default").hideHelp())
|
|
139
|
-
.option("--auth", "require a valid access token for this module's routes (needs `add auth
|
|
140
|
-
.option("--permission <code>", "also require this permission via authz.Require (needs `add rbac
|
|
141
|
-
.option("--defaults", "skip
|
|
205
|
+
.option("--auth", "require a valid access token for this module's routes (needs `add auth`; omitted means public)")
|
|
206
|
+
.option("--permission <code>", "also require this permission via authz.Require (needs `add rbac` and --auth; e.g. users:manage)")
|
|
207
|
+
.option("--defaults", "skip every wizard question; use recorded project defaults and keep the module public (for CI/scripting)")
|
|
142
208
|
.action(async (name, opts) => {
|
|
143
209
|
try {
|
|
210
|
+
const profile = (0, config_2.parseModuleProfile)(opts.profile);
|
|
211
|
+
if (profile && (opts.full !== undefined || opts.cqrs !== undefined)) {
|
|
212
|
+
throw new Error("--profile cannot be combined with --full, --no-full, or --cqrs — choose one configuration style");
|
|
213
|
+
}
|
|
144
214
|
// anything not passed as a flag gets asked for — declaring both --full
|
|
145
215
|
// and --no-full leaves opts.full undefined when neither is given, which
|
|
146
216
|
// is exactly the "not answered yet" signal runModuleWizard needs.
|
|
147
217
|
await runModuleWizard(name, {
|
|
218
|
+
profile,
|
|
148
219
|
full: opts.full,
|
|
220
|
+
cqrs: opts.cqrs,
|
|
149
221
|
auth: opts.auth,
|
|
150
222
|
permission: opts.permission,
|
|
151
223
|
defaults: opts.defaults,
|
|
@@ -155,13 +227,46 @@ generate
|
|
|
155
227
|
fail(err);
|
|
156
228
|
}
|
|
157
229
|
});
|
|
230
|
+
const configCommand = program
|
|
231
|
+
.command("config")
|
|
232
|
+
.description("configure future module defaults interactively, or inspect/validate the project config")
|
|
233
|
+
.action(async () => {
|
|
234
|
+
try {
|
|
235
|
+
await (0, config_1.configureProject)();
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
fail(err);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
configCommand
|
|
242
|
+
.command("show")
|
|
243
|
+
.description("print the resolved project config, including installed features and module choices; no wizard")
|
|
244
|
+
.action(() => {
|
|
245
|
+
try {
|
|
246
|
+
(0, config_1.showProjectConfig)();
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
fail(err);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
configCommand
|
|
253
|
+
.command("validate")
|
|
254
|
+
.description("validate project config and detected scaffold state without changing files; no wizard")
|
|
255
|
+
.action(() => {
|
|
256
|
+
try {
|
|
257
|
+
(0, config_1.validateProjectConfig)();
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
fail(err);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
158
263
|
generate
|
|
159
264
|
.command("method [module] [name]")
|
|
160
265
|
.alias("me")
|
|
161
|
-
.description("add one endpoint to an existing module
|
|
162
|
-
.option("--type <type>", "get
|
|
163
|
-
.option("--get-mode <mode>", "for
|
|
164
|
-
.option("--field <name>", "
|
|
266
|
+
.description("add one endpoint to an existing module; omitted module, name, and endpoint details are asked by the wizard")
|
|
267
|
+
.option("--type <type>", "HTTP verb: get, post, put, patch, or delete; omitted means the wizard asks")
|
|
268
|
+
.option("--get-mode <mode>", "GET only: all for a list endpoint or one for a lookup by another field")
|
|
269
|
+
.option("--field <name>", "GET one only: lookup column such as email/status/slug; id is already the built-in lookup")
|
|
165
270
|
.action(async (moduleName, methodName, opts) => {
|
|
166
271
|
try {
|
|
167
272
|
const type = opts.type;
|
|
@@ -185,7 +290,7 @@ generate
|
|
|
185
290
|
generate
|
|
186
291
|
.command("migration [name]")
|
|
187
292
|
.alias("mig")
|
|
188
|
-
.description("reserve a timestamped
|
|
293
|
+
.description("reserve a timestamped up/down SQL pair; omitted name opens a migration-name wizard and the SQL remains your responsibility")
|
|
189
294
|
.action(async (name) => {
|
|
190
295
|
try {
|
|
191
296
|
await (0, migration_1.generateMigration)(name);
|
|
@@ -224,7 +329,7 @@ async function runAddWizard() {
|
|
|
224
329
|
// read once, up front: every add command reads it anyway, the summary below
|
|
225
330
|
// needs to know what's already installed (e.g. whether auth's mail goes out
|
|
226
331
|
// inline or through an existing queue), and so does the menu itself.
|
|
227
|
-
const config = (0,
|
|
332
|
+
const config = (0, config_2.readConfig)(process.cwd());
|
|
228
333
|
// Each add is once-only, and rbac needs auth first. Both facts are already
|
|
229
334
|
// known here, so say them in the menu rather than letting someone walk three
|
|
230
335
|
// steps and a confirmation to reach "already been added".
|
|
@@ -265,7 +370,7 @@ async function runAddWizard() {
|
|
|
265
370
|
// all), so the menu has to ask it the same way the worker menu asks for
|
|
266
371
|
// its queue backend — a choice only reachable by knowing the flag name
|
|
267
372
|
// isn't a choice for anyone driving this from the menu.
|
|
268
|
-
await runAddAuth(await (0, auth_wizard_1.promptAuthStore)(), {});
|
|
373
|
+
await runAddAuth(await (0, auth_wizard_1.promptAuthStore)(), await (0, auth_wizard_1.promptBrowserTopology)(), {});
|
|
269
374
|
}
|
|
270
375
|
else if (target === "rbac") {
|
|
271
376
|
await runAddRbac({});
|
|
@@ -283,21 +388,33 @@ async function runAddWorker(backend, opts) {
|
|
|
283
388
|
], opts);
|
|
284
389
|
await (0, worker_1.addWorker)(backend);
|
|
285
390
|
}
|
|
286
|
-
async function runAddAuth(store, opts) {
|
|
287
|
-
const config = (0,
|
|
391
|
+
async function runAddAuth(store, browserTopology, opts) {
|
|
392
|
+
const config = (0, config_2.readConfig)(process.cwd());
|
|
288
393
|
await confirmAdd([
|
|
289
394
|
"add internal/app/user/, internal/shared/middleware/auth.go, and cmd/seed",
|
|
395
|
+
`browser OAuth: frontend-owned callback with server-side provider redirect URI (${browserTopology})`,
|
|
290
396
|
store === "postgres"
|
|
291
|
-
? "
|
|
292
|
-
: picocolors_1.default.yellow("tokens + rate-limit counters: Redis — requires
|
|
397
|
+
? "refresh + recovery tokens: Postgres (user_svc.auth_tokens), rate-limit counters in-process — no extra service"
|
|
398
|
+
: picocolors_1.default.yellow("refresh tokens + rate-limit counters: Redis; recovery tokens: Postgres — requires Redis"),
|
|
293
399
|
config.features.worker
|
|
294
400
|
? "verification/reset mail: queued through the worker already installed"
|
|
295
401
|
: picocolors_1.default.yellow("verification/reset mail: sent inline over SMTP (no worker yet) — /auth/register and /auth/forgot-password block until it's sent"),
|
|
296
402
|
], opts);
|
|
297
|
-
await (0, auth_1.addAuth)(store);
|
|
403
|
+
await (0, auth_1.addAuth)(store, process.cwd(), browserTopology);
|
|
404
|
+
}
|
|
405
|
+
// Explicit browser flags are intentionally resolved before the confirmation
|
|
406
|
+
// summary. `--defaults` is the stable non-TTY escape hatch; `--yes` with no
|
|
407
|
+
// browser flags also uses the local default so existing CI invocations such as
|
|
408
|
+
// `add auth --store postgres --yes` do not unexpectedly start prompting.
|
|
409
|
+
async function resolveBrowserTopology(opts) {
|
|
410
|
+
if (opts.browserTopology !== undefined)
|
|
411
|
+
return (0, auth_wizard_1.validateBrowserTopology)(opts.browserTopology);
|
|
412
|
+
if (opts.defaults || opts.yes)
|
|
413
|
+
return auth_wizard_1.DEFAULT_BROWSER_TOPOLOGY;
|
|
414
|
+
return (0, auth_wizard_1.promptBrowserTopology)();
|
|
298
415
|
}
|
|
299
416
|
async function runAddRbac(opts) {
|
|
300
|
-
const config = (0,
|
|
417
|
+
const config = (0, config_2.readConfig)(process.cwd());
|
|
301
418
|
if (!config.features.auth) {
|
|
302
419
|
throw new Error("`add rbac` requires `add auth` first — there's no Role claim to check permissions against otherwise");
|
|
303
420
|
}
|
|
@@ -305,7 +422,7 @@ async function runAddRbac(opts) {
|
|
|
305
422
|
await (0, rbac_1.addRbac)();
|
|
306
423
|
}
|
|
307
424
|
async function runAddObservability(opts) {
|
|
308
|
-
const config = (0,
|
|
425
|
+
const config = (0, config_2.readConfig)(process.cwd());
|
|
309
426
|
await confirmAdd([
|
|
310
427
|
"add Prometheus /metrics + OpenTelemetry tracing",
|
|
311
428
|
"patch cmd/api/wiring.go and internal/platform/database to wire it in — cmd/api only",
|
|
@@ -317,7 +434,7 @@ async function runAddObservability(opts) {
|
|
|
317
434
|
}
|
|
318
435
|
const add = program
|
|
319
436
|
.command("add")
|
|
320
|
-
.description("add opt-in infrastructure to an existing
|
|
437
|
+
.description("add opt-in infrastructure to an existing project; bare `add` opens a worker/auth/RBAC/observability wizard")
|
|
321
438
|
.action(async () => {
|
|
322
439
|
try {
|
|
323
440
|
await runAddWizard();
|
|
@@ -330,8 +447,8 @@ add
|
|
|
330
447
|
.command("worker")
|
|
331
448
|
.description("add a background job queue, SMTP mail, and cmd/worker (opt-in — most projects don't need this on day one)")
|
|
332
449
|
.option("--queue <backend>", "where jobs are stored: postgres (River, default) or redis (Asynq)")
|
|
333
|
-
.option("--defaults", "skip
|
|
334
|
-
.option("-y, --yes", "skip the confirmation summary")
|
|
450
|
+
.option("--defaults", "skip queue and confirmation prompts; use Postgres/River")
|
|
451
|
+
.option("-y, --yes", "skip only the confirmation summary; an omitted queue still opens the queue wizard")
|
|
335
452
|
.action(async (opts) => {
|
|
336
453
|
try {
|
|
337
454
|
await runAddWorker(await resolveQueueBackend(opts), { yes: opts.yes || opts.defaults });
|
|
@@ -367,8 +484,9 @@ add
|
|
|
367
484
|
.command("auth")
|
|
368
485
|
.description("add email/password auth: JWT access tokens, refresh token rotation, register/login/refresh/logout/me (no prerequisites — without `add worker` the verification/reset mail is sent inline)")
|
|
369
486
|
.option("--store <store>", 'where tokens and rate-limit counters live: "postgres" (default, no extra service) or "redis" (exact across replicas)')
|
|
370
|
-
.option("--
|
|
371
|
-
.option("
|
|
487
|
+
.option("--browser-topology <topology>", "browser deployment topology for cookie/CORS policy: same-origin, same-site (different origin), or cross-site (requires HTTPS deployment)")
|
|
488
|
+
.option("--defaults", "skip store, browser-topology, and confirmation prompts; use Postgres plus local same-site defaults")
|
|
489
|
+
.option("-y, --yes", "skip confirmation; omitted store/topology use local Postgres and same-site defaults")
|
|
372
490
|
.action(async (opts) => {
|
|
373
491
|
try {
|
|
374
492
|
// Same shape as `add worker`: an explicit flag wins, --defaults takes
|
|
@@ -388,7 +506,8 @@ add
|
|
|
388
506
|
else {
|
|
389
507
|
store = await (0, auth_wizard_1.promptAuthStore)();
|
|
390
508
|
}
|
|
391
|
-
|
|
509
|
+
const browserTopology = await resolveBrowserTopology(opts);
|
|
510
|
+
await runAddAuth(store, browserTopology, { yes: opts.yes || opts.defaults });
|
|
392
511
|
}
|
|
393
512
|
catch (err) {
|
|
394
513
|
fail(err);
|
|
@@ -396,8 +515,8 @@ add
|
|
|
396
515
|
});
|
|
397
516
|
add
|
|
398
517
|
.command("rbac")
|
|
399
|
-
.description("add role-based access control: roles/permissions admin API, cached Authz middleware, PATCH /users/:id/set-role (requires `add auth
|
|
400
|
-
.option("-y, --yes", "skip the confirmation summary")
|
|
518
|
+
.description("add role-based access control: roles/permissions admin API, cached Authz middleware, and PATCH /users/:id/set-role (requires `add auth`; direct command only needs confirmation)")
|
|
519
|
+
.option("-y, --yes", "skip the confirmation summary; the bare `add` wizard can select this target")
|
|
401
520
|
.action(async (opts) => {
|
|
402
521
|
try {
|
|
403
522
|
await runAddRbac({ yes: opts.yes });
|
|
@@ -408,8 +527,8 @@ add
|
|
|
408
527
|
});
|
|
409
528
|
add
|
|
410
529
|
.command("observability")
|
|
411
|
-
.description("add Prometheus /metrics + OpenTelemetry tracing for Gin + GORM (also available at
|
|
412
|
-
.option("-y, --yes", "skip the confirmation summary")
|
|
530
|
+
.description("add Prometheus /metrics + OpenTelemetry tracing for Gin + GORM (also available at create time via --observability; direct command only needs confirmation)")
|
|
531
|
+
.option("-y, --yes", "skip the confirmation summary; the bare `add` wizard can select this target")
|
|
413
532
|
.action(async (opts) => {
|
|
414
533
|
try {
|
|
415
534
|
await runAddObservability({ yes: opts.yes });
|
|
@@ -420,7 +539,7 @@ add
|
|
|
420
539
|
});
|
|
421
540
|
const undo = program
|
|
422
541
|
.command("undo")
|
|
423
|
-
.description("undo a
|
|
542
|
+
.description("undo a generated module and its wiring; bare `undo` opens a module selector and confirmation")
|
|
424
543
|
.action(async () => {
|
|
425
544
|
// bare `undo` — module is the only target, so prompt for the name
|
|
426
545
|
try {
|
|
@@ -433,8 +552,8 @@ const undo = program
|
|
|
433
552
|
undo
|
|
434
553
|
.command("module [name]")
|
|
435
554
|
.alias("m")
|
|
436
|
-
.description("delete a generated module and
|
|
437
|
-
.option("-y, --yes", "skip the confirmation prompt")
|
|
555
|
+
.description("delete a generated module, its owned migrations/docs, and the wiring it added; omitted name opens a selector")
|
|
556
|
+
.option("-y, --yes", "skip the destructive confirmation prompt (use only after reviewing the target)")
|
|
438
557
|
.action(async (name, opts) => {
|
|
439
558
|
try {
|
|
440
559
|
await (0, undo_1.undoModule)(name, { yes: opts.yes });
|
|
@@ -449,10 +568,14 @@ undo
|
|
|
449
568
|
// ran on every database created from then on. Kept as an alias rather than
|
|
450
569
|
// deleted outright: a muscle-memory `rm m` shouldn't be an unrecognised-command
|
|
451
570
|
// error, and the semantics only got safer.
|
|
452
|
-
const remove = program
|
|
571
|
+
const remove = program
|
|
572
|
+
.command("remove", { hidden: true })
|
|
573
|
+
.alias("rm")
|
|
574
|
+
.description("deprecated hidden alias for undo; kept for backwards compatibility");
|
|
453
575
|
remove
|
|
454
576
|
.command("module [name]")
|
|
455
577
|
.alias("m")
|
|
578
|
+
.description("deprecated hidden alias for `undo module`; kept for backwards compatibility")
|
|
456
579
|
.option("-y, --yes", "skip the confirmation prompt")
|
|
457
580
|
.action(async (name, opts) => {
|
|
458
581
|
console.error(picocolors_1.default.yellow("`remove module` is now `undo module` — running that instead."));
|
|
@@ -478,13 +601,14 @@ async function runTopMenu() {
|
|
|
478
601
|
// Every entry but "create" needs a project. Offering them outside one buys
|
|
479
602
|
// two selects and then the same error — so say it once, up front, and only
|
|
480
603
|
// offer what can actually run here.
|
|
481
|
-
const inProject = (0,
|
|
604
|
+
const inProject = (0, config_2.isProjectDir)(process.cwd());
|
|
482
605
|
const target = inProject
|
|
483
606
|
? await (0, interactive_1.select)({
|
|
484
607
|
message: "What do you want to do?",
|
|
485
608
|
choices: [
|
|
486
609
|
{ name: "Create a new project", value: "create" },
|
|
487
610
|
{ name: "Generate (module/method/migration in an existing project)", value: "generate" },
|
|
611
|
+
{ name: "Configure project generation defaults", value: "config" },
|
|
488
612
|
{ name: "Add (auth/worker/rbac/observability in an existing project)", value: "add" },
|
|
489
613
|
{ name: "Undo a generated module", value: "undo" },
|
|
490
614
|
],
|
|
@@ -496,6 +620,9 @@ async function runTopMenu() {
|
|
|
496
620
|
else if (target === "generate") {
|
|
497
621
|
await runGenerateWizard();
|
|
498
622
|
}
|
|
623
|
+
else if (target === "config") {
|
|
624
|
+
await (0, config_1.configureProject)();
|
|
625
|
+
}
|
|
499
626
|
else if (target === "add") {
|
|
500
627
|
await runAddWizard();
|
|
501
628
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_BROWSER_TOPOLOGY = void 0;
|
|
3
4
|
exports.promptAuthStore = promptAuthStore;
|
|
5
|
+
exports.validateBrowserTopology = validateBrowserTopology;
|
|
6
|
+
exports.promptBrowserTopology = promptBrowserTopology;
|
|
4
7
|
const interactive_1 = require("./interactive");
|
|
5
|
-
// The one decision `add auth` cannot make for you: where refresh tokens
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// and whether the auth_tokens table exists.
|
|
8
|
+
// The one decision `add auth` cannot make for you: where refresh tokens and
|
|
9
|
+
// rate-limit counters live. Recovery tokens always use the durable Postgres
|
|
10
|
+
// table so consumption can share a transaction with the user update.
|
|
9
11
|
//
|
|
10
12
|
// Mirrors promptQueueBackend: the choice exists as `--store` for scripting,
|
|
11
13
|
// but nobody should have to know the flag name to discover the option.
|
|
@@ -17,13 +19,45 @@ async function promptAuthStore() {
|
|
|
17
19
|
{
|
|
18
20
|
name: "Postgres (user_svc.auth_tokens)",
|
|
19
21
|
value: "postgres",
|
|
20
|
-
description: "no extra service to run;
|
|
22
|
+
description: "no extra service to run; refresh and recovery tokens use your database, and the rate limiter counts in-process",
|
|
21
23
|
},
|
|
22
24
|
{
|
|
23
25
|
name: "Redis",
|
|
24
26
|
value: "redis",
|
|
25
|
-
description: "TTL
|
|
27
|
+
description: "refresh TTL and rate-limit counters are exact across replicas; recovery stays transactional in Postgres; needs Redis",
|
|
26
28
|
},
|
|
27
29
|
],
|
|
28
30
|
});
|
|
29
31
|
}
|
|
32
|
+
exports.DEFAULT_BROWSER_TOPOLOGY = "same-site";
|
|
33
|
+
const TOPOLOGIES = [
|
|
34
|
+
{
|
|
35
|
+
name: "Same-origin",
|
|
36
|
+
value: "same-origin",
|
|
37
|
+
description: "frontend and API share scheme, host, and port; simplest cookie boundary",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "Same-site, different-origin",
|
|
41
|
+
value: "same-site",
|
|
42
|
+
description: "for example localhost:3000 + localhost:8080 or app.example.com + api.example.com",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "Cross-site",
|
|
46
|
+
value: "cross-site",
|
|
47
|
+
description: "different registrable sites; requires SameSite=None, Secure cookies, HTTPS, and exact CORS",
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
function validateBrowserTopology(raw) {
|
|
51
|
+
const value = raw.trim().toLowerCase();
|
|
52
|
+
if (!TOPOLOGIES.some((topology) => topology.value === value)) {
|
|
53
|
+
throw new Error(`Browser topology must be one of: ${TOPOLOGIES.map((topology) => topology.value).join(", ")} (got "${raw}")`);
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
async function promptBrowserTopology() {
|
|
58
|
+
return (0, interactive_1.select)({
|
|
59
|
+
message: "How are the browser frontend and API deployed?",
|
|
60
|
+
default: exports.DEFAULT_BROWSER_TOPOLOGY,
|
|
61
|
+
choices: TOPOLOGIES,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -4,6 +4,9 @@ exports.promptProjectName = promptProjectName;
|
|
|
4
4
|
exports.runCreateWizard = runCreateWizard;
|
|
5
5
|
const interactive_1 = require("./interactive");
|
|
6
6
|
const naming_1 = require("../utils/naming");
|
|
7
|
+
const module_profile_1 = require("../utils/module-profile");
|
|
8
|
+
const types_1 = require("../types");
|
|
9
|
+
const generate_wizard_1 = require("./generate-wizard");
|
|
7
10
|
async function promptProjectName() {
|
|
8
11
|
const name = await (0, interactive_1.input)({
|
|
9
12
|
message: "Project name:",
|
|
@@ -28,7 +31,7 @@ async function runCreateWizard(preset = {}) {
|
|
|
28
31
|
}));
|
|
29
32
|
const openapiDocs = preset.openapiDocs ??
|
|
30
33
|
(await (0, interactive_1.confirm)({
|
|
31
|
-
message: "Include hand-written OpenAPI docs (docs/openapi.yaml
|
|
34
|
+
message: "Include hand-written OpenAPI docs (docs/openapi.yaml — files only, never served over HTTP)?",
|
|
32
35
|
default: true,
|
|
33
36
|
}));
|
|
34
37
|
const observability = preset.observability ??
|
|
@@ -49,6 +52,31 @@ async function runCreateWizard(preset = {}) {
|
|
|
49
52
|
if (prefixCheck !== true)
|
|
50
53
|
throw new Error(prefixCheck);
|
|
51
54
|
const apiPrefix = (0, naming_1.normalizeApiPrefix)(apiPrefixRaw);
|
|
55
|
+
let moduleSurface;
|
|
56
|
+
let applicationStyle;
|
|
57
|
+
if (preset.moduleProfile) {
|
|
58
|
+
({ moduleSurface, applicationStyle } = (0, generate_wizard_1.moduleArchitectureForProfile)(preset.moduleProfile));
|
|
59
|
+
}
|
|
60
|
+
else if (preset.moduleSurface !== undefined || preset.applicationStyle !== undefined) {
|
|
61
|
+
// Keep the two old axis flags useful for partial scripted invocations. If
|
|
62
|
+
// only one was supplied, ask only for the other one instead of silently
|
|
63
|
+
// overriding the explicit flag with a profile choice.
|
|
64
|
+
moduleSurface =
|
|
65
|
+
preset.moduleSurface ??
|
|
66
|
+
(await (0, generate_wizard_1.promptModuleSurface)(types_1.DEFAULT_ARCHITECTURE_CONFIG.defaultModuleSurface));
|
|
67
|
+
applicationStyle =
|
|
68
|
+
preset.applicationStyle ??
|
|
69
|
+
(await (0, generate_wizard_1.promptApplicationStyle)(types_1.DEFAULT_ARCHITECTURE_CONFIG.defaultApplicationStyle));
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
const profile = await (0, generate_wizard_1.promptModuleProfile)((0, module_profile_1.moduleProfileFor)(types_1.DEFAULT_ARCHITECTURE_CONFIG.defaultModuleSurface, types_1.DEFAULT_ARCHITECTURE_CONFIG.defaultApplicationStyle));
|
|
73
|
+
if (profile === "advanced") {
|
|
74
|
+
({ moduleSurface, applicationStyle } = await (0, generate_wizard_1.promptAdvancedModuleArchitecture)());
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
({ moduleSurface, applicationStyle } = (0, generate_wizard_1.moduleArchitectureForProfile)(profile));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
52
80
|
// Everything is listed whether it was asked or passed, so a flag never
|
|
53
81
|
// reaches the project without the caller seeing the value it produced.
|
|
54
82
|
const fromFlag = (passed) => (passed !== undefined ? " (from flag)" : "");
|
|
@@ -57,9 +85,22 @@ async function runCreateWizard(preset = {}) {
|
|
|
57
85
|
console.log(` OpenAPI docs: ${openapiDocs ? "yes" : "no"}${fromFlag(preset.openapiDocs)}`);
|
|
58
86
|
console.log(` Metrics + tracing: ${observability ? "yes" : "no"}${fromFlag(preset.observability)}`);
|
|
59
87
|
console.log(` Route prefix: ${apiPrefix ? `/${apiPrefix}` : "(none)"}${fromFlag(preset.apiPrefix)}`);
|
|
88
|
+
const profile = (0, generate_wizard_1.moduleProfileDescription)(moduleSurface, applicationStyle);
|
|
89
|
+
const profileSource = preset.moduleProfile !== undefined ? " (from --module-profile)" : "";
|
|
90
|
+
console.log(` Default module profile: ${profile}${profileSource}`);
|
|
91
|
+
console.log(` Default module surface: ${moduleSurface}${fromFlag(preset.moduleSurface)}`);
|
|
92
|
+
console.log(` Default application style: ${applicationStyle}${fromFlag(preset.applicationStyle)}`);
|
|
60
93
|
const proceed = await (0, interactive_1.confirm)({ message: "\nCreate project with these settings?", default: true });
|
|
61
94
|
if (!proceed) {
|
|
62
95
|
throw new Error("project creation cancelled");
|
|
63
96
|
}
|
|
64
|
-
return {
|
|
97
|
+
return {
|
|
98
|
+
features: { docker, openapiDocs, observability },
|
|
99
|
+
apiPrefix,
|
|
100
|
+
architecture: {
|
|
101
|
+
...types_1.DEFAULT_ARCHITECTURE_CONFIG,
|
|
102
|
+
defaultModuleSurface: moduleSurface,
|
|
103
|
+
defaultApplicationStyle: applicationStyle,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
65
106
|
}
|