@nakedev/go-scaffold 0.4.0 → 0.5.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 +598 -306
- package/dist/commands/auth.js +65 -23
- package/dist/commands/check.js +281 -0
- package/dist/commands/config.js +50 -0
- package/dist/commands/create.js +33 -2
- package/dist/commands/generate.js +29 -3
- package/dist/commands/method.js +74 -63
- package/dist/commands/migration.js +2 -2
- package/dist/commands/observability.js +4 -53
- package/dist/commands/rbac.js +21 -10
- package/dist/commands/undo.js +11 -3
- package/dist/commands/worker.js +15 -5
- package/dist/index.js +198 -59
- package/dist/prompts/auth-wizard.js +40 -6
- package/dist/prompts/create-wizard.js +42 -1
- package/dist/prompts/generate-wizard.js +89 -9
- package/dist/templates/auth-manifest.js +50 -19
- package/dist/templates/create-manifest.js +8 -0
- package/dist/templates/module-manifest.js +84 -26
- package/dist/templates/rbac-manifest.js +16 -11
- package/dist/templates/worker-manifest.js +4 -1
- package/dist/types.js +8 -0
- package/dist/utils/auth-patcher.js +124 -33
- package/dist/utils/config.js +167 -4
- package/dist/utils/docs-patcher.js +68 -0
- package/dist/utils/hexagonal-method-patcher.js +334 -0
- package/dist/utils/main-patcher.js +32 -30
- package/dist/utils/marker-patch.js +7 -1
- package/dist/utils/module-location.js +17 -11
- package/dist/utils/module-profile.js +32 -0
- package/dist/utils/platform-patcher.js +56 -7
- package/dist/utils/rbac-patcher.js +89 -210
- package/package.json +7 -2
- package/templates/add/auth/cmd/seed/main.go.hbs +15 -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/adapters/inbound/http/browser_policy.go.hbs +98 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/dto.go.hbs +159 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler.go.hbs +228 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_local.go.hbs +76 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_mfa.go.hbs +83 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_oauth.go.hbs +70 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_recovery.go.hbs +49 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_test.go.hbs +311 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_user.go.hbs +41 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/session_cookie.go.hbs +35 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/password/bcrypt.go.hbs +35 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/password/bcrypt_test.go.hbs +20 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/mfa_store.go.hbs +129 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/mfa_store_test.go.hbs +174 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/model.go.hbs +84 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/repository.go.hbs +211 -0
- package/templates/add/auth/internal/app/user/{repository_test.go.hbs → adapters/outbound/postgres/repository_test.go.hbs} +18 -19
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/tokenstore_pg.go.hbs +213 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/tokenstore_pg_test.go.hbs +103 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/tokenstore_recovery.go.hbs +84 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/redis/tokenstore.go.hbs +228 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/redis/tokenstore_test.go.hbs +196 -0
- package/templates/add/auth/internal/app/user/application/contracts.go.hbs +52 -0
- package/templates/add/auth/internal/app/user/application/dto.go.hbs +75 -0
- package/templates/add/auth/internal/app/user/application/errors.go.hbs +62 -0
- package/templates/add/auth/internal/app/user/application/external_login.go.hbs +198 -0
- package/templates/add/auth/internal/app/user/application/jwt.go.hbs +58 -0
- package/templates/add/auth/internal/app/user/application/local_auth.go.hbs +96 -0
- package/templates/add/auth/internal/app/user/application/mfa_service.go.hbs +449 -0
- package/templates/add/auth/internal/app/user/application/mfa_service_test.go.hbs +200 -0
- package/templates/add/auth/internal/app/user/application/oauth.go.hbs +132 -0
- package/templates/add/auth/internal/app/user/application/provider_test.go.hbs +285 -0
- package/templates/add/auth/internal/app/user/application/recovery.go.hbs +82 -0
- package/templates/add/auth/internal/app/user/application/recovery_service.go.hbs +112 -0
- package/templates/add/auth/internal/app/user/application/service.go.hbs +145 -0
- package/templates/add/auth/internal/app/user/application/service_test.go.hbs +891 -0
- package/templates/add/auth/internal/app/user/application/sessions.go.hbs +99 -0
- package/templates/add/auth/internal/app/user/application/tokenstore_ports.go.hbs +14 -0
- package/templates/add/auth/internal/app/user/application/user_query.go.hbs +65 -0
- package/templates/add/auth/internal/app/user/composition.go.hbs +168 -0
- package/templates/add/auth/internal/app/user/domain/entity.go.hbs +41 -0
- package/templates/add/auth/internal/app/user/domain/errors.go.hbs +32 -0
- package/templates/add/auth/internal/app/user/ports/password.go.hbs +9 -0
- package/templates/add/auth/internal/app/user/ports/repository.go.hbs +90 -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 +10 -5
- package/templates/add/auth/migrations/create_identities.up.sql.hbs +1 -1
- package/templates/add/auth/migrations/create_login_throttle.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 +4 -3
- package/templates/add/rbac/internal/app/role/adapters/inbound/http/handler.go.hbs +142 -0
- package/templates/add/rbac/internal/app/role/adapters/inbound/http/handler_test.go.hbs +19 -0
- package/templates/add/rbac/internal/app/role/adapters/outbound/postgres/model.go.hbs +48 -0
- package/templates/add/rbac/internal/app/role/adapters/outbound/postgres/repository.go.hbs +127 -0
- package/templates/add/rbac/internal/app/role/{repository_test.go.hbs → adapters/outbound/postgres/repository_test.go.hbs} +8 -8
- package/templates/add/rbac/internal/app/role/application/dto.go.hbs +47 -0
- package/templates/add/rbac/internal/app/role/application/errors.go.hbs +19 -0
- package/templates/add/rbac/internal/app/role/application/service.go.hbs +157 -0
- package/templates/add/rbac/internal/app/role/{service_test.go.hbs → application/service_test.go.hbs} +26 -19
- package/templates/add/rbac/internal/app/role/composition.go.hbs +48 -0
- package/templates/add/rbac/internal/app/role/domain/entity.go.hbs +23 -0
- package/templates/add/rbac/internal/app/role/domain/errors.go.hbs +26 -0
- package/templates/add/rbac/internal/app/role/ports/repository.go.hbs +25 -0
- package/templates/add/rbac/migrations/add_roles.down.sql.hbs +3 -11
- package/templates/add/rbac/migrations/add_roles.up.sql.hbs +17 -6
- package/templates/add/worker/internal/platform/queue/river_test.go.hbs +84 -0
- package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +358 -121
- package/templates/create/base/.env.example.hbs +0 -1
- package/templates/create/base/.golangci.yml.hbs +2 -2
- package/templates/create/base/AGENTS.md.hbs +279 -67
- package/templates/create/base/Makefile.hbs +2 -1
- package/templates/create/base/README.md.hbs +115 -32
- package/templates/create/base/cmd/api/wiring.go.hbs +13 -9
- package/templates/create/base/internal/composition/doc.go.hbs +7 -0
- 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 +92 -32
- package/templates/create/features/docs/patterns.md.hbs +137 -91
- package/templates/create/features/docs/techstack.md.hbs +18 -3
- package/templates/generate/module/hexagonal/adapters/inbound/http/dto.go.hbs +45 -0
- package/templates/generate/module/hexagonal/adapters/inbound/http/dto.minimal.go.hbs +28 -0
- package/templates/generate/module/hexagonal/adapters/inbound/http/handler.go.hbs +182 -0
- package/templates/generate/module/hexagonal/adapters/inbound/http/handler.minimal.go.hbs +83 -0
- package/templates/generate/module/hexagonal/adapters/inbound/http/handler_crud_test.go.hbs +18 -0
- package/templates/generate/module/hexagonal/adapters/inbound/http/handler_test.go.hbs +30 -0
- package/templates/generate/module/hexagonal/adapters/outbound/postgres/model.go.hbs +37 -0
- package/templates/generate/module/hexagonal/adapters/outbound/postgres/repository.go.hbs +95 -0
- package/templates/generate/module/{repository_test.go.hbs → hexagonal/adapters/outbound/postgres/repository_test.go.hbs} +8 -8
- package/templates/generate/module/hexagonal/application/commands.crud.go.hbs +54 -0
- package/templates/generate/module/hexagonal/application/commands.go.hbs +25 -0
- package/templates/generate/module/hexagonal/application/cqrs_test.go.hbs +66 -0
- package/templates/generate/module/hexagonal/application/dto.go.hbs +35 -0
- package/templates/generate/module/hexagonal/application/dto.minimal.go.hbs +25 -0
- package/templates/generate/module/hexagonal/application/queries.crud.go.hbs +33 -0
- package/templates/generate/module/hexagonal/application/queries.go.hbs +25 -0
- package/templates/generate/module/hexagonal/application/service.crud.go.hbs +73 -0
- package/templates/generate/module/hexagonal/application/service.go.hbs +29 -0
- package/templates/generate/module/hexagonal/application/service_test.go.hbs +62 -0
- package/templates/generate/module/hexagonal/composition.go.hbs +27 -0
- package/templates/generate/module/hexagonal/domain/entity.go.hbs +20 -0
- package/templates/generate/module/hexagonal/domain/errors.go.hbs +11 -0
- package/templates/generate/module/hexagonal/ports/repository.go.hbs +38 -0
- package/templates/generate/module/migration.up.sql.hbs +1 -1
- package/dist/utils/method-patcher.js +0 -357
- package/templates/add/auth/docs/google-callback.yaml.hbs +0 -22
- package/templates/add/auth/docs/google-login.yaml.hbs +0 -7
- package/templates/add/auth/internal/app/user/dto.go.hbs +0 -77
- package/templates/add/auth/internal/app/user/errors.go.hbs +0 -43
- package/templates/add/auth/internal/app/user/handler.go.hbs +0 -276
- package/templates/add/auth/internal/app/user/jwt.go.hbs +0 -108
- package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +0 -39
- package/templates/add/auth/internal/app/user/model/identity.go.hbs +0 -31
- package/templates/add/auth/internal/app/user/model/loginthrottle.go.hbs +0 -26
- package/templates/add/auth/internal/app/user/model/user.go.hbs +0 -30
- package/templates/add/auth/internal/app/user/repository.go.hbs +0 -137
- package/templates/add/auth/internal/app/user/service.go.hbs +0 -531
- package/templates/add/auth/internal/app/user/service_test.go.hbs +0 -316
- package/templates/add/auth/internal/app/user/tokenstore.go.hbs +0 -30
- package/templates/add/auth/internal/app/user/tokenstore_pg.go.hbs +0 -144
- package/templates/add/auth/internal/app/user/tokenstore_redis.go.hbs +0 -147
- package/templates/add/rbac/internal/app/role/dto.go.hbs +0 -45
- package/templates/add/rbac/internal/app/role/errors.go.hbs +0 -39
- package/templates/add/rbac/internal/app/role/handler.go.hbs +0 -104
- package/templates/add/rbac/internal/app/role/model/permission.go.hbs +0 -12
- package/templates/add/rbac/internal/app/role/model/role.go.hbs +0 -22
- package/templates/add/rbac/internal/app/role/model/role_permission.go.hbs +0 -11
- package/templates/add/rbac/internal/app/role/repository.go.hbs +0 -97
- package/templates/add/rbac/internal/app/role/service.go.hbs +0 -217
- package/templates/generate/module/dto.go.hbs +0 -36
- package/templates/generate/module/errors.go.hbs +0 -33
- package/templates/generate/module/handler.go.hbs +0 -134
- package/templates/generate/module/handler_test.go.hbs +0 -174
- package/templates/generate/module/minimal/dto.go.hbs +0 -28
- package/templates/generate/module/minimal/handler.go.hbs +0 -48
- package/templates/generate/module/minimal/handler_test.go.hbs +0 -10
- package/templates/generate/module/minimal/service.go.hbs +0 -45
- package/templates/generate/module/minimal/service_test.go.hbs +0 -77
- package/templates/generate/module/model/model.go.hbs +0 -36
- package/templates/generate/module/repository.go.hbs +0 -103
- package/templates/generate/module/service.go.hbs +0 -108
- package/templates/generate/module/service_test.go.hbs +0 -161
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.patchConfigForAuth = patchConfigForAuth;
|
|
7
7
|
exports.authWiringLines = authWiringLines;
|
|
8
|
+
exports.authHandlerLineFor = authHandlerLineFor;
|
|
8
9
|
exports.patchMainGoForAuth = patchMainGoForAuth;
|
|
9
10
|
exports.upgradeMailerToQueue = upgradeMailerToQueue;
|
|
10
11
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
@@ -17,8 +18,7 @@ const PLATFORM_INIT_MARKER = "// go-scaffold:platform-init";
|
|
|
17
18
|
const SCHEMA_MARKER = "// go-scaffold:schemas";
|
|
18
19
|
const MODEL_MARKER = "// go-scaffold:models";
|
|
19
20
|
const ROUTE_MARKER = "// go-scaffold:routes";
|
|
20
|
-
|
|
21
|
-
// patchConfigForAuth adds JWT/cookie/password-reset/Google OAuth fields to
|
|
21
|
+
// patchConfigForAuth adds JWT/cookie/password-reset/browser OAuth fields to
|
|
22
22
|
// Config, the same marker-based text insertion patchConfigForWorker uses
|
|
23
23
|
// (config.go.hbs is only rendered once, at `create` — everything after that
|
|
24
24
|
// is a real file a human may have already edited).
|
|
@@ -28,8 +28,11 @@ function patchConfigForAuth(configGoPath) {
|
|
|
28
28
|
"JWTSecret string",
|
|
29
29
|
"JWTAccessTTL time.Duration",
|
|
30
30
|
"JWTRefreshTTL time.Duration",
|
|
31
|
+
"JWTRefreshMaxTTL time.Duration",
|
|
32
|
+
"OAuthStateTTL time.Duration",
|
|
31
33
|
"CookieSecure bool",
|
|
32
34
|
"CookieSameSite string",
|
|
35
|
+
"AuthBrowserTopology string",
|
|
33
36
|
"",
|
|
34
37
|
"PasswordResetTTL time.Duration",
|
|
35
38
|
"PasswordResetURL string",
|
|
@@ -39,15 +42,25 @@ function patchConfigForAuth(configGoPath) {
|
|
|
39
42
|
"",
|
|
40
43
|
"GoogleClientID string",
|
|
41
44
|
"GoogleClientSecret string",
|
|
42
|
-
"
|
|
45
|
+
"GoogleOAuthRedirectURI string",
|
|
46
|
+
"",
|
|
47
|
+
"AuthMFAEnabled bool",
|
|
48
|
+
"MFAIssuer string",
|
|
49
|
+
"MFAEncryptionKey string",
|
|
50
|
+
"MFAChallengeTTL time.Duration",
|
|
51
|
+
"MFATOTPWindow int",
|
|
52
|
+
"MFARecoveryCodeCount int",
|
|
43
53
|
].join("\n");
|
|
44
54
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, CONFIG_FIELDS_MARKER, fieldsBlock, "JWTSecret");
|
|
45
55
|
const loadBlock = [
|
|
46
56
|
'JWTSecret: env("JWT_SECRET", "dev-secret-change-me"),',
|
|
47
57
|
'JWTAccessTTL: time.Duration(envInt("JWT_ACCESS_TTL_MIN", 15)) * time.Minute,',
|
|
48
58
|
'JWTRefreshTTL: time.Duration(envInt("JWT_REFRESH_TTL_MIN", 43200)) * time.Minute,',
|
|
59
|
+
'JWTRefreshMaxTTL: time.Duration(envInt("JWT_REFRESH_MAX_TTL_MIN", 43200)) * time.Minute,',
|
|
60
|
+
'OAuthStateTTL: time.Duration(envInt("OAUTH_STATE_TTL_MIN", 10)) * time.Minute,',
|
|
49
61
|
'CookieSecure: env("COOKIE_SECURE", "false") == "true",',
|
|
50
62
|
'CookieSameSite: env("COOKIE_SAMESITE", "strict"),',
|
|
63
|
+
'AuthBrowserTopology: env("AUTH_BROWSER_TOPOLOGY", "same-site"),',
|
|
51
64
|
"",
|
|
52
65
|
'PasswordResetTTL: time.Duration(envInt("PASSWORD_RESET_TTL_MIN", 30)) * time.Minute,',
|
|
53
66
|
'PasswordResetURL: env("PASSWORD_RESET_URL", "http://localhost:3000/reset-password"),',
|
|
@@ -57,7 +70,14 @@ function patchConfigForAuth(configGoPath) {
|
|
|
57
70
|
"",
|
|
58
71
|
'GoogleClientID: env("GOOGLE_CLIENT_ID", ""),',
|
|
59
72
|
'GoogleClientSecret: env("GOOGLE_CLIENT_SECRET", ""),',
|
|
60
|
-
'
|
|
73
|
+
'GoogleOAuthRedirectURI: env("GOOGLE_OAUTH_REDIRECT_URI", ""),',
|
|
74
|
+
"",
|
|
75
|
+
'AuthMFAEnabled: env("AUTH_MFA_ENABLED", "false") == "true",',
|
|
76
|
+
'MFAIssuer: env("MFA_ISSUER", "go-scaffold"),',
|
|
77
|
+
'MFAEncryptionKey: env("MFA_ENCRYPTION_KEY", ""),',
|
|
78
|
+
'MFAChallengeTTL: time.Duration(envInt("MFA_CHALLENGE_TTL_MIN", 5)) * time.Minute,',
|
|
79
|
+
'MFATOTPWindow: envInt("MFA_TOTP_WINDOW", 1),',
|
|
80
|
+
'MFARecoveryCodeCount: envInt("MFA_RECOVERY_CODE_COUNT", 10),',
|
|
61
81
|
].join("\n");
|
|
62
82
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, CONFIG_LOAD_MARKER, loadBlock, 'env("JWT_SECRET"');
|
|
63
83
|
fs_extra_1.default.writeFileSync(configGoPath, content);
|
|
@@ -68,26 +88,36 @@ function patchConfigForAuth(configGoPath) {
|
|
|
68
88
|
function authWiringLines(w) {
|
|
69
89
|
const postgres = w.store === "postgres";
|
|
70
90
|
return {
|
|
71
|
-
tokenStore: postgres ? "user.NewPgTokenStore(db)" : "user.NewRedisTokenStore(rdb)",
|
|
91
|
+
tokenStore: postgres ? "user.NewPgTokenStore(db)" : "user.NewRedisTokenStore(rdb, db)",
|
|
72
92
|
limiter: postgres ? "middleware.NewMemoryLimiter()" : "middleware.NewRedisLimiter(rdb)",
|
|
73
93
|
// with a queue the mail is enqueued and the request returns immediately;
|
|
74
94
|
// without one it goes out inline, which is the cost of not running a worker
|
|
75
95
|
mailer: w.worker ? "mail.NewAsyncClient(q)" : "mail.NewSyncClient(mail.Open(cfg))",
|
|
76
96
|
};
|
|
77
97
|
}
|
|
98
|
+
// authHandlerLineFor is the one root-level auth route shape. The feature owns
|
|
99
|
+
// construction; wiring.go only hands it shared infrastructure and, when RBAC
|
|
100
|
+
// is present, the role feature's public capabilities.
|
|
101
|
+
function authHandlerLineFor(w, roleDependencies = ["nil", "nil"]) {
|
|
102
|
+
const args = ["db", "cfg"];
|
|
103
|
+
if (w.store === "redis")
|
|
104
|
+
args.push("rdb");
|
|
105
|
+
if (w.worker)
|
|
106
|
+
args.push("q");
|
|
107
|
+
args.push(...roleDependencies);
|
|
108
|
+
return `user.NewHandlerFromDB(${args.join(", ")}).Register(api)`;
|
|
109
|
+
}
|
|
78
110
|
function patchMainGoForAuth(mainGoPath, w) {
|
|
79
111
|
const { goModule, queueBackend, store } = w;
|
|
80
112
|
let content = fs_extra_1.default.readFileSync(mainGoPath, "utf8");
|
|
81
113
|
const importLine = `"${goModule}/internal/app/user"`;
|
|
82
114
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, importLine, importLine);
|
|
83
|
-
const modelImportLine = `usermodel "${goModule}/internal/app/user/
|
|
115
|
+
const modelImportLine = `usermodel "${goModule}/internal/app/user/adapters/outbound/postgres"`;
|
|
84
116
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, modelImportLine, modelImportLine);
|
|
85
117
|
if (w.worker) {
|
|
86
118
|
const queueImportLine = `"${goModule}/internal/platform/queue"`;
|
|
87
119
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, queueImportLine, queueImportLine);
|
|
88
120
|
}
|
|
89
|
-
const mailImportLine = `"${goModule}/internal/platform/mail"`;
|
|
90
|
-
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, mailImportLine, mailImportLine);
|
|
91
121
|
const checkBlock = [
|
|
92
122
|
'if cfg.IsProd() && cfg.JWTSecret == "dev-secret-change-me" {',
|
|
93
123
|
'\treturn errors.New("JWT_SECRET is still the dev default — set a real secret before deploying with APP_ENV=production")',
|
|
@@ -105,6 +135,21 @@ function patchMainGoForAuth(mainGoPath, w) {
|
|
|
105
135
|
"}",
|
|
106
136
|
].join("\n");
|
|
107
137
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, CONFIG_CHECKS_MARKER, smtpCheckBlock, "SMTP_HOST is unset");
|
|
138
|
+
const mfaCheckBlock = [
|
|
139
|
+
"if cfg.AuthMFAEnabled {",
|
|
140
|
+
"\tif err := user.ValidateMFASettings(user.MFASettings{",
|
|
141
|
+
"\t\tEnabled: cfg.AuthMFAEnabled,",
|
|
142
|
+
"\t\tIssuer: cfg.MFAIssuer,",
|
|
143
|
+
"\t\tEncryptionKey: cfg.MFAEncryptionKey,",
|
|
144
|
+
"\t\tChallengeTTL: cfg.MFAChallengeTTL,",
|
|
145
|
+
"\t\tTOTPWindow: cfg.MFATOTPWindow,",
|
|
146
|
+
"\t\tRecoveryCodeCount: cfg.MFARecoveryCodeCount,",
|
|
147
|
+
"\t}); err != nil {",
|
|
148
|
+
"\t\treturn fmt.Errorf(\"invalid MFA configuration: %w\", err)",
|
|
149
|
+
"\t}",
|
|
150
|
+
"}",
|
|
151
|
+
].join("\n");
|
|
152
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, CONFIG_CHECKS_MARKER, mfaCheckBlock, "invalid MFA configuration");
|
|
108
153
|
// The enqueuer is built from whatever backend `add worker` chose — the
|
|
109
154
|
// constructor differs, everything downstream of it (mail.NewAsyncClient)
|
|
110
155
|
// only sees the queue.Enqueuer interface and doesn't change.
|
|
@@ -120,24 +165,27 @@ function patchMainGoForAuth(mainGoPath, w) {
|
|
|
120
165
|
].join("\n");
|
|
121
166
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, SCHEMA_MARKER, schemaBlock, "CREATE SCHEMA IF NOT EXISTS user_svc");
|
|
122
167
|
const migrateLines = ["&usermodel.User{},", "&usermodel.Identity{},", "&usermodel.LoginThrottle{},"];
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
168
|
+
// Recovery tokens are always durable in Postgres, even when refresh tokens
|
|
169
|
+
// live in Redis, so reset/verification can share the user transaction.
|
|
170
|
+
migrateLines.push("&usermodel.AuthToken{},");
|
|
171
|
+
migrateLines.push("&usermodel.MFAEnrollment{},", "&usermodel.MFAChallenge{},", "&usermodel.MFARecoveryCode{},");
|
|
126
172
|
for (const line of migrateLines) {
|
|
127
173
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, MODEL_MARKER, line, line);
|
|
128
174
|
}
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
const { tokenStore, limiter, mailer } = authWiringLines(w);
|
|
133
|
-
const svcLine = `userSvc := user.NewService(user.NewRepository(db), ${tokenStore}, ${mailer}, cfg)`;
|
|
134
|
-
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, ROUTE_MARKER, svcLine, "userSvc :=");
|
|
135
|
-
const routeLine = `user.NewHandler(userSvc, cfg.JWTSecret, cfg.JWTRefreshTTL, cfg.CookieSecure, cfg.CookieSameSite, ${limiter}).Register(api)`;
|
|
175
|
+
// Keep auth construction inside the feature package. The root only chooses
|
|
176
|
+
// shared infrastructure and registers the resulting handler.
|
|
177
|
+
const routeLine = authHandlerLineFor(w);
|
|
136
178
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, ROUTE_MARKER, routeLine, routeLine);
|
|
137
179
|
content = content.replace(/\n\t_ = api \/\/ dropped once `generate module` registers the first route\n/, "\n");
|
|
138
180
|
if (w.worker) {
|
|
139
|
-
const
|
|
140
|
-
|
|
181
|
+
const cleanupBlock = [
|
|
182
|
+
"defer func() {",
|
|
183
|
+
'\tif err := q.Close(); err != nil {',
|
|
184
|
+
'\t\tlogger.Error("close queue", "error", err)',
|
|
185
|
+
"\t}",
|
|
186
|
+
"}()",
|
|
187
|
+
].join("\n");
|
|
188
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, PLATFORM_INIT_MARKER, cleanupBlock, "defer func() {\n\tif err := q.Close()");
|
|
141
189
|
}
|
|
142
190
|
fs_extra_1.default.writeFileSync(mainGoPath, content);
|
|
143
191
|
}
|
|
@@ -153,19 +201,62 @@ function patchMainGoForAuth(mainGoPath, w) {
|
|
|
153
201
|
// at all — both simply don't contain the line it looks for. Returns whether
|
|
154
202
|
// it actually upgraded something, so the caller's printed summary can say
|
|
155
203
|
// which happened instead of always assuming "no auth yet".
|
|
156
|
-
function upgradeMailerToQueue(mainGoPath, goModule, queueBackend) {
|
|
204
|
+
function upgradeMailerToQueue(mainGoPath, goModule, queueBackend, compositionGoPath) {
|
|
157
205
|
let content = fs_extra_1.default.readFileSync(mainGoPath, "utf8");
|
|
158
206
|
const syncMailer = "mail.NewSyncClient(mail.Open(cfg))";
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
207
|
+
let upgradedComposition = false;
|
|
208
|
+
// Auth now keeps mailer construction in internal/app/user. A worker added
|
|
209
|
+
// later must upgrade that feature-local composition and thread q into the
|
|
210
|
+
// existing root registration, rather than moving construction back to the
|
|
211
|
+
// composition root.
|
|
212
|
+
if (compositionGoPath && fs_extra_1.default.existsSync(compositionGoPath)) {
|
|
213
|
+
let composition = fs_extra_1.default.readFileSync(compositionGoPath, "utf8");
|
|
214
|
+
if (composition.includes(syncMailer)) {
|
|
215
|
+
composition = (0, marker_patch_1.ensureImport)(composition, `${goModule}/internal/platform/queue`);
|
|
216
|
+
if (!composition.includes("q queue.Enqueuer")) {
|
|
217
|
+
const configParam = "\tcfg config.Config,\n";
|
|
218
|
+
const redisParam = "\trdb *redis.Client,\n";
|
|
219
|
+
const queueAnchor = composition.includes(redisParam) ? redisParam : configParam;
|
|
220
|
+
if (!composition.includes(queueAnchor)) {
|
|
221
|
+
throw new Error(`${compositionGoPath} is missing the cfg parameter needed to upgrade auth mail to a queue`);
|
|
222
|
+
}
|
|
223
|
+
composition = composition.replace(queueAnchor, `${queueAnchor}\tq queue.Enqueuer,\n`);
|
|
224
|
+
}
|
|
225
|
+
composition = composition.replace(syncMailer, "mail.NewAsyncClient(q)");
|
|
226
|
+
fs_extra_1.default.writeFileSync(compositionGoPath, composition);
|
|
227
|
+
upgradedComposition = true;
|
|
228
|
+
const routeMatch = content.match(/user\.NewHandlerFromDB\(db, cfg, ([^)]*)\)\.Register\(api\)/);
|
|
229
|
+
if (!routeMatch) {
|
|
230
|
+
throw new Error("cmd/api/wiring.go is missing auth's feature-local NewHandlerFromDB route while adding the worker");
|
|
231
|
+
}
|
|
232
|
+
const args = routeMatch[1].split(",").map((arg) => arg.trim());
|
|
233
|
+
if (!args.includes("q")) {
|
|
234
|
+
if (args.length < 2)
|
|
235
|
+
throw new Error("auth's NewHandlerFromDB route has no role/authz dependency slots");
|
|
236
|
+
args.splice(args.length - 2, 0, "q");
|
|
237
|
+
content = content.replace(routeMatch[0], `user.NewHandlerFromDB(db, cfg, ${args.join(", ")}).Register(api)`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (upgradedComposition) {
|
|
242
|
+
const queueImportLine = `"${goModule}/internal/platform/queue"`;
|
|
243
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, queueImportLine, queueImportLine);
|
|
244
|
+
const queueCtor = queueBackend === "river" ? "queue.NewRiverEnqueuer(db)" : "queue.NewAsynqEnqueuer(cfg.RedisURL)";
|
|
245
|
+
const queueInitBlock = [`q, err := ${queueCtor}`, "if err != nil {", '\treturn fmt.Errorf("open queue: %w", err)', "}"].join("\n");
|
|
246
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, PLATFORM_INIT_MARKER, queueInitBlock, "q, err := queue.New");
|
|
247
|
+
const cleanupBlock = [
|
|
248
|
+
"defer func() {",
|
|
249
|
+
'\tif err := q.Close(); err != nil {',
|
|
250
|
+
'\t\tlogger.Error("close queue", "error", err)',
|
|
251
|
+
"\t}",
|
|
252
|
+
"}()",
|
|
253
|
+
].join("\n");
|
|
254
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, PLATFORM_INIT_MARKER, cleanupBlock, "defer func() {\n\tif err := q.Close()");
|
|
255
|
+
fs_extra_1.default.writeFileSync(mainGoPath, content);
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
if (content.includes(syncMailer)) {
|
|
259
|
+
throw new Error("auth mailer is not owned by internal/app/user/composition.go; regenerate auth with the canonical split layout before adding a worker");
|
|
260
|
+
}
|
|
261
|
+
return false;
|
|
171
262
|
}
|
package/dist/utils/config.js
CHANGED
|
@@ -3,18 +3,45 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.CONFIG_SCHEMA_VERSION = void 0;
|
|
6
7
|
exports.configPath = configPath;
|
|
8
|
+
exports.parseModuleSurface = parseModuleSurface;
|
|
9
|
+
exports.parseApplicationStyle = parseApplicationStyle;
|
|
10
|
+
exports.parseModuleProfile = parseModuleProfile;
|
|
7
11
|
exports.writeConfig = writeConfig;
|
|
8
12
|
exports.readConfig = readConfig;
|
|
9
13
|
exports.isProjectDir = isProjectDir;
|
|
10
14
|
const path_1 = __importDefault(require("path"));
|
|
11
15
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
16
|
+
const types_1 = require("../types");
|
|
12
17
|
const CONFIG_FILE = "go-scaffold.config.json";
|
|
18
|
+
exports.CONFIG_SCHEMA_VERSION = 2;
|
|
13
19
|
function configPath(projectDir) {
|
|
14
20
|
return path_1.default.join(projectDir, CONFIG_FILE);
|
|
15
21
|
}
|
|
22
|
+
function parseModuleSurface(value) {
|
|
23
|
+
if (value === undefined)
|
|
24
|
+
return undefined;
|
|
25
|
+
const normalized = value.trim().toLowerCase();
|
|
26
|
+
assertOneOf(normalized, "--module-surface", ["minimal", "crud"]);
|
|
27
|
+
return normalized;
|
|
28
|
+
}
|
|
29
|
+
function parseApplicationStyle(value) {
|
|
30
|
+
if (value === undefined)
|
|
31
|
+
return undefined;
|
|
32
|
+
const normalized = value.trim().toLowerCase();
|
|
33
|
+
assertOneOf(normalized, "--application-style", ["service", "cqrs"]);
|
|
34
|
+
return normalized;
|
|
35
|
+
}
|
|
36
|
+
function parseModuleProfile(value, flag = "--profile") {
|
|
37
|
+
if (value === undefined)
|
|
38
|
+
return undefined;
|
|
39
|
+
const normalized = value.trim().toLowerCase();
|
|
40
|
+
assertOneOf(normalized, flag, ["lean", "crud", "cqrs"]);
|
|
41
|
+
return normalized;
|
|
42
|
+
}
|
|
16
43
|
function writeConfig(projectDir, config) {
|
|
17
|
-
fs_extra_1.default.writeJsonSync(configPath(projectDir), config, { spaces: 2 });
|
|
44
|
+
fs_extra_1.default.writeJsonSync(configPath(projectDir), normalizeProjectConfig(config, projectDir), { spaces: 2 });
|
|
18
45
|
}
|
|
19
46
|
// readConfig falls back to detecting from go.mod when the config file is
|
|
20
47
|
// missing (e.g. a project scaffolded before this file existed).
|
|
@@ -35,13 +62,142 @@ function readConfig(projectDir) {
|
|
|
35
62
|
if (!fs_extra_1.default.existsSync(file))
|
|
36
63
|
return detectConfig(projectDir);
|
|
37
64
|
const config = fs_extra_1.default.readJsonSync(file);
|
|
65
|
+
if (!isRecord(config)) {
|
|
66
|
+
throw new Error(`${CONFIG_FILE} must contain a JSON object`);
|
|
67
|
+
}
|
|
38
68
|
const detected = detectFeatures(projectDir);
|
|
39
|
-
const features = { ...config.features };
|
|
69
|
+
const features = { ...(isRecord(config.features) ? config.features : {}) };
|
|
40
70
|
for (const key of Object.keys(detected)) {
|
|
41
71
|
if (features[key] === undefined)
|
|
42
72
|
features[key] = detected[key];
|
|
43
73
|
}
|
|
44
|
-
return { ...config, features };
|
|
74
|
+
return normalizeProjectConfig({ ...config, features: features }, projectDir);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Validate and fill defaults for the project manifest. Projects that omit the
|
|
78
|
+
* architecture/modules keys can still be resolved safely; an explicit schema
|
|
79
|
+
* from before the split-layout contract is rejected so commands never write a
|
|
80
|
+
* mixed old/new tree.
|
|
81
|
+
*/
|
|
82
|
+
function normalizeProjectConfig(raw, projectDir) {
|
|
83
|
+
const projectName = requiredString(raw.projectName, "projectName");
|
|
84
|
+
const goModule = requiredString(raw.goModule, "goModule");
|
|
85
|
+
const apiPrefix = requiredString(raw.apiPrefix ?? "", "apiPrefix");
|
|
86
|
+
const schemaVersion = raw.schemaVersion ?? exports.CONFIG_SCHEMA_VERSION;
|
|
87
|
+
if (!Number.isInteger(schemaVersion) || schemaVersion < 1) {
|
|
88
|
+
throw new Error(`${CONFIG_FILE} has invalid schemaVersion "${String(schemaVersion)}" — expected a positive integer`);
|
|
89
|
+
}
|
|
90
|
+
if (schemaVersion < exports.CONFIG_SCHEMA_VERSION) {
|
|
91
|
+
throw new Error(`${CONFIG_FILE} uses legacy schemaVersion ${schemaVersion}; go-scaffold ${exports.CONFIG_SCHEMA_VERSION} requires the hexagonal split layout. ` +
|
|
92
|
+
`Keep using go-scaffold 0.4.x for that project, or migrate its modules to internal/app/<module>/{domain,application,ports,adapters} before upgrading.`);
|
|
93
|
+
}
|
|
94
|
+
if (schemaVersion > exports.CONFIG_SCHEMA_VERSION) {
|
|
95
|
+
throw new Error(`${CONFIG_FILE} uses schemaVersion ${schemaVersion}, but this CLI supports up to ${exports.CONFIG_SCHEMA_VERSION} — upgrade go-scaffold first`);
|
|
96
|
+
}
|
|
97
|
+
const detected = detectFeatures(projectDir);
|
|
98
|
+
const features = { ...(isRecord(raw.features) ? raw.features : {}) };
|
|
99
|
+
for (const key of Object.keys(detected)) {
|
|
100
|
+
if (features[key] === undefined)
|
|
101
|
+
features[key] = detected[key];
|
|
102
|
+
}
|
|
103
|
+
validateFeatures(features);
|
|
104
|
+
const architecture = normalizeArchitecture(raw.architecture);
|
|
105
|
+
const modules = normalizeModules(raw.modules);
|
|
106
|
+
return {
|
|
107
|
+
schemaVersion,
|
|
108
|
+
projectName,
|
|
109
|
+
goModule,
|
|
110
|
+
apiPrefix,
|
|
111
|
+
features: features,
|
|
112
|
+
architecture,
|
|
113
|
+
modules,
|
|
114
|
+
...(raw.scaffoldVersion ? { scaffoldVersion: raw.scaffoldVersion } : {}),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function normalizeArchitecture(raw) {
|
|
118
|
+
const value = raw === undefined ? {} : raw;
|
|
119
|
+
if (!isRecord(value)) {
|
|
120
|
+
throw new Error(`${CONFIG_FILE}.architecture must be a JSON object`);
|
|
121
|
+
}
|
|
122
|
+
const architecture = {
|
|
123
|
+
...types_1.DEFAULT_ARCHITECTURE_CONFIG,
|
|
124
|
+
...value,
|
|
125
|
+
};
|
|
126
|
+
assertOneOf(architecture.style, "architecture.style", ["modular-monolith"]);
|
|
127
|
+
assertOneOf(architecture.boundary, "architecture.boundary", ["hexagonal"]);
|
|
128
|
+
assertOneOf(architecture.packageLayout, "architecture.packageLayout", ["split"]);
|
|
129
|
+
assertOneOf(architecture.defaultModuleSurface, "architecture.defaultModuleSurface", ["minimal", "crud"]);
|
|
130
|
+
assertOneOf(architecture.defaultApplicationStyle, "architecture.defaultApplicationStyle", ["service", "cqrs"]);
|
|
131
|
+
return {
|
|
132
|
+
style: architecture.style,
|
|
133
|
+
boundary: architecture.boundary,
|
|
134
|
+
packageLayout: architecture.packageLayout,
|
|
135
|
+
defaultModuleSurface: architecture.defaultModuleSurface,
|
|
136
|
+
defaultApplicationStyle: architecture.defaultApplicationStyle,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function normalizeModules(raw) {
|
|
140
|
+
if (raw === undefined)
|
|
141
|
+
return {};
|
|
142
|
+
if (!isRecord(raw))
|
|
143
|
+
throw new Error(`${CONFIG_FILE}.modules must be a JSON object`);
|
|
144
|
+
const modules = {};
|
|
145
|
+
for (const [name, entry] of Object.entries(raw)) {
|
|
146
|
+
if (!name || name.includes("/") || name.includes("\\")) {
|
|
147
|
+
throw new Error(`${CONFIG_FILE}.modules has invalid module key "${name}" — use the module's Go package name`);
|
|
148
|
+
}
|
|
149
|
+
if (!isRecord(entry))
|
|
150
|
+
throw new Error(`${CONFIG_FILE}.modules.${name} must be a JSON object`);
|
|
151
|
+
const boundary = entry.boundary ?? types_1.DEFAULT_ARCHITECTURE_CONFIG.boundary;
|
|
152
|
+
const packageLayout = entry.packageLayout ?? types_1.DEFAULT_ARCHITECTURE_CONFIG.packageLayout;
|
|
153
|
+
assertOneOf(entry.surface, `modules.${name}.surface`, ["minimal", "crud"]);
|
|
154
|
+
assertOneOf(entry.applicationStyle, `modules.${name}.applicationStyle`, ["service", "cqrs"]);
|
|
155
|
+
assertOneOf(boundary, `modules.${name}.boundary`, ["hexagonal"]);
|
|
156
|
+
assertOneOf(packageLayout, `modules.${name}.packageLayout`, ["split"]);
|
|
157
|
+
modules[name] = {
|
|
158
|
+
surface: entry.surface,
|
|
159
|
+
applicationStyle: entry.applicationStyle,
|
|
160
|
+
boundary: boundary,
|
|
161
|
+
packageLayout: packageLayout,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return modules;
|
|
165
|
+
}
|
|
166
|
+
function validateFeatures(features) {
|
|
167
|
+
if (typeof features.docker !== "boolean")
|
|
168
|
+
throw new Error(`${CONFIG_FILE}.features.docker must be true or false`);
|
|
169
|
+
if (typeof features.openapiDocs !== "boolean")
|
|
170
|
+
throw new Error(`${CONFIG_FILE}.features.openapiDocs must be true or false`);
|
|
171
|
+
if (features.worker !== undefined && typeof features.worker !== "boolean") {
|
|
172
|
+
throw new Error(`${CONFIG_FILE}.features.worker must be true or false`);
|
|
173
|
+
}
|
|
174
|
+
if (features.queue !== undefined)
|
|
175
|
+
assertOneOf(features.queue, "features.queue", ["river", "asynq"]);
|
|
176
|
+
if (features.auth !== undefined && typeof features.auth !== "boolean") {
|
|
177
|
+
throw new Error(`${CONFIG_FILE}.features.auth must be true or false`);
|
|
178
|
+
}
|
|
179
|
+
if (features.authStore !== undefined)
|
|
180
|
+
assertOneOf(features.authStore, "features.authStore", ["postgres", "redis"]);
|
|
181
|
+
if (features.rbac !== undefined && typeof features.rbac !== "boolean") {
|
|
182
|
+
throw new Error(`${CONFIG_FILE}.features.rbac must be true or false`);
|
|
183
|
+
}
|
|
184
|
+
if (features.observability !== undefined && typeof features.observability !== "boolean") {
|
|
185
|
+
throw new Error(`${CONFIG_FILE}.features.observability must be true or false`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function requiredString(value, field) {
|
|
189
|
+
if (typeof value !== "string")
|
|
190
|
+
throw new Error(`${CONFIG_FILE}.${field} must be a string`);
|
|
191
|
+
return value;
|
|
192
|
+
}
|
|
193
|
+
function assertOneOf(value, field, allowed) {
|
|
194
|
+
if (typeof value !== "string" || !allowed.includes(value)) {
|
|
195
|
+
const label = field.startsWith("--") ? field : `${CONFIG_FILE}.${field}`;
|
|
196
|
+
throw new Error(`${label} must be one of: ${allowed.join(", ")} (got "${String(value)}")`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function isRecord(value) {
|
|
200
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
45
201
|
}
|
|
46
202
|
// detectFeatures answers "what is actually installed here" from the tree
|
|
47
203
|
// alone. Every `add` command leaves a directory or a file behind that nothing
|
|
@@ -66,7 +222,11 @@ function detectFeatures(projectDir) {
|
|
|
66
222
|
// file it wrote — same trick as the queue adapter above. Projects from
|
|
67
223
|
// before the option existed have neither name and read as "redis",
|
|
68
224
|
// which is what they in fact are.
|
|
69
|
-
authStore: !auth
|
|
225
|
+
authStore: !auth
|
|
226
|
+
? undefined
|
|
227
|
+
: has("internal", "app", "user", "adapters", "outbound", "postgres", "tokenstore_pg.go")
|
|
228
|
+
? "postgres"
|
|
229
|
+
: "redis",
|
|
70
230
|
rbac: has("internal", "app", "role") && has("internal", "shared", "middleware", "authz.go"),
|
|
71
231
|
observability: has("internal", "platform", "telemetry"),
|
|
72
232
|
};
|
|
@@ -107,10 +267,13 @@ function detectConfig(projectDir) {
|
|
|
107
267
|
// already exists"), with no way out. Worse, the first `add` to succeed then
|
|
108
268
|
// wrote a config that recorded the undetected features as absent.
|
|
109
269
|
return {
|
|
270
|
+
schemaVersion: exports.CONFIG_SCHEMA_VERSION,
|
|
110
271
|
projectName: path_1.default.basename(projectDir),
|
|
111
272
|
goModule,
|
|
112
273
|
apiPrefix,
|
|
113
274
|
features: detectFeatures(projectDir),
|
|
275
|
+
architecture: { ...types_1.DEFAULT_ARCHITECTURE_CONFIG },
|
|
276
|
+
modules: {},
|
|
114
277
|
};
|
|
115
278
|
}
|
|
116
279
|
// isProjectDir answers "would readConfig succeed here?" without throwing, so
|
|
@@ -0,0 +1,68 @@
|
|
|
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.refreshProjectDocs = refreshProjectDocs;
|
|
7
|
+
exports.docsRefreshWarning = docsRefreshWarning;
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
10
|
+
const naming_1 = require("./naming");
|
|
11
|
+
const template_renderer_1 = require("./template-renderer");
|
|
12
|
+
const PROJECT_DOCS = [
|
|
13
|
+
{ template: "create/base/README.md.hbs", output: "README.md" },
|
|
14
|
+
{ template: "create/features/docs/architecture.md.hbs", output: path_1.default.join("docs", "architect", "architecture.md") },
|
|
15
|
+
{ template: "create/features/docs/techstack.md.hbs", output: path_1.default.join("docs", "architect", "techstack.md") },
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* Refresh generated project docs after an incremental feature is installed.
|
|
19
|
+
*
|
|
20
|
+
* A generated doc is only safe to rewrite when its bytes still match the
|
|
21
|
+
* version rendered from the project's current config. This keeps feature
|
|
22
|
+
* commands from destroying a maintainer's edits while still preventing the
|
|
23
|
+
* common stale-doc state where config says a feature is enabled but README or
|
|
24
|
+
* architect docs still describe the base skeleton.
|
|
25
|
+
*/
|
|
26
|
+
function refreshProjectDocs(projectDir, config, featureOverrides) {
|
|
27
|
+
const beforeFeatures = { ...config.features };
|
|
28
|
+
const afterFeatures = { ...beforeFeatures, ...featureOverrides };
|
|
29
|
+
const beforeContext = {
|
|
30
|
+
projectName: config.projectName,
|
|
31
|
+
dbName: (0, naming_1.toDbName)(config.projectName),
|
|
32
|
+
apiPrefix: config.apiPrefix,
|
|
33
|
+
...config.architecture,
|
|
34
|
+
...beforeFeatures,
|
|
35
|
+
};
|
|
36
|
+
const afterContext = {
|
|
37
|
+
projectName: config.projectName,
|
|
38
|
+
dbName: (0, naming_1.toDbName)(config.projectName),
|
|
39
|
+
apiPrefix: config.apiPrefix,
|
|
40
|
+
...config.architecture,
|
|
41
|
+
...afterFeatures,
|
|
42
|
+
};
|
|
43
|
+
const root = (0, template_renderer_1.getTemplatesRoot)();
|
|
44
|
+
const skipped = [];
|
|
45
|
+
for (const doc of PROJECT_DOCS) {
|
|
46
|
+
const outputPath = path_1.default.join(projectDir, doc.output);
|
|
47
|
+
if (!fs_extra_1.default.existsSync(outputPath))
|
|
48
|
+
continue;
|
|
49
|
+
const source = fs_extra_1.default.readFileSync(path_1.default.join(root, doc.template), "utf8");
|
|
50
|
+
const onDisk = normalizeLineEndings(fs_extra_1.default.readFileSync(outputPath, "utf8"));
|
|
51
|
+
const expectedBefore = normalizeLineEndings((0, template_renderer_1.renderString)(source, beforeContext));
|
|
52
|
+
if (onDisk !== expectedBefore) {
|
|
53
|
+
skipped.push(doc.output);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
fs_extra_1.default.writeFileSync(outputPath, (0, template_renderer_1.renderString)(source, afterContext));
|
|
57
|
+
}
|
|
58
|
+
return skipped;
|
|
59
|
+
}
|
|
60
|
+
function docsRefreshWarning(skipped, feature) {
|
|
61
|
+
if (skipped.length === 0)
|
|
62
|
+
return "";
|
|
63
|
+
const files = skipped.join(", ");
|
|
64
|
+
return `\nwarning: ${files} did not match the generated version, so ${feature} left them untouched — update them by hand`;
|
|
65
|
+
}
|
|
66
|
+
function normalizeLineEndings(text) {
|
|
67
|
+
return text.replace(/\r\n/g, "\n");
|
|
68
|
+
}
|