rapidkit 0.25.4 → 0.25.6
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/dist/chunk-Q7ULIFQA.js +5 -0
- package/dist/chunk-VM2TOHNX.js +2 -0
- package/dist/chunk-ZAZJEYYT.js +11 -0
- package/dist/create-27NVMJAR.js +776 -0
- package/dist/demo-kit-63CFMCPD.js +141 -0
- package/dist/doctor-P57TTWEP.js +38 -0
- package/dist/gofiber-standard-NQHMZTFK.js +1572 -0
- package/dist/gogin-standard-2G2C3VQL.js +1656 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +167 -3568
- package/dist/package.json +1 -1
- package/dist/{workspace-KRZ3DWL4.js → workspace-VXNLNKCM.js} +5 -5
- package/package.json +1 -1
- package/dist/chunk-EDH5S5JF.js +0 -6
- package/dist/chunk-KAV65WZO.js +0 -786
- package/dist/create-PIVSRLDS.js +0 -1
package/dist/index.js
CHANGED
|
@@ -1,3569 +1,168 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {e
|
|
3
|
-
__pycache__/
|
|
4
|
-
*.py[cod]
|
|
5
|
-
*$py.class
|
|
6
|
-
*.so
|
|
7
|
-
.Python
|
|
8
|
-
build/
|
|
9
|
-
develop-eggs/
|
|
10
|
-
dist/
|
|
11
|
-
downloads/
|
|
12
|
-
eggs/
|
|
13
|
-
.eggs/
|
|
14
|
-
lib/
|
|
15
|
-
lib64/
|
|
16
|
-
parts/
|
|
17
|
-
sdist/
|
|
18
|
-
var/
|
|
19
|
-
wheels/
|
|
20
|
-
*.egg-info/
|
|
21
|
-
.installed.cfg
|
|
22
|
-
*.egg
|
|
23
|
-
|
|
24
|
-
# Virtual environments
|
|
25
|
-
.venv/
|
|
26
|
-
venv/
|
|
27
|
-
ENV/
|
|
28
|
-
env/
|
|
29
|
-
|
|
30
|
-
# IDEs
|
|
31
|
-
.vscode/
|
|
32
|
-
.idea/
|
|
33
|
-
*.swp
|
|
34
|
-
*.swo
|
|
35
|
-
*~
|
|
36
|
-
|
|
37
|
-
# OS
|
|
38
|
-
.DS_Store
|
|
39
|
-
Thumbs.db
|
|
40
|
-
|
|
41
|
-
# Project specific
|
|
42
|
-
.env
|
|
43
|
-
.env.local
|
|
44
|
-
`:`# Node artifacts
|
|
45
|
-
node_modules/
|
|
46
|
-
dist/
|
|
47
|
-
.tmp/
|
|
48
|
-
.env
|
|
49
|
-
.env.*
|
|
50
|
-
!.env.example
|
|
51
|
-
|
|
52
|
-
# Logs
|
|
53
|
-
logs/
|
|
54
|
-
*.log
|
|
55
|
-
npm-debug.log*
|
|
56
|
-
yarn-debug.log*
|
|
57
|
-
yarn-error.log*
|
|
58
|
-
pnpm-debug.log*
|
|
59
|
-
|
|
60
|
-
# OS
|
|
61
|
-
.DS_Store
|
|
62
|
-
Thumbs.db
|
|
63
|
-
|
|
64
|
-
# IDEs
|
|
65
|
-
.idea/
|
|
66
|
-
.vscode/
|
|
67
|
-
|
|
68
|
-
# Coverage
|
|
69
|
-
coverage/
|
|
70
|
-
`;if(await promises.writeFile(h.join(e,".gitignore"),w),n.succeed(`${i} project generated!`),!o.skipGit){let g=Ut("Initializing git repository...").start();try{await execa("git",["init"],{cwd:e}),await execa("git",["add","."],{cwd:e}),await execa("git",["commit","-m",`Initial commit: ${i} project via RapidKit`],{cwd:e}),g.succeed("Git repository initialized");}catch{g.warn("Could not initialize git repository");}}if(!r&&!o.skipInstall){let g=o.package_manager||"npm",R=Ut(`Installing dependencies with ${g}...`).start();try{await execa(g,g==="yarn"?["install"]:g==="pnpm"?["install"]:["install"],{cwd:e}),R.succeed("Dependencies installed");}catch{R.warn(`Could not install dependencies. Run '${g} install' manually.`);}}let f=h.basename(e);console.log(`
|
|
71
|
-
${l.yellow("\u26A0\uFE0F Limited offline mode:")} This project was created using basic templates.
|
|
72
|
-
${l.gray("For full kit features, install Python 3.10+ and rapidkit-core:")}
|
|
73
|
-
${l.cyan(" sudo apt install python3 python3-pip python3-venv")}
|
|
74
|
-
${l.cyan(" pip install rapidkit-core")}
|
|
75
|
-
`),console.log(r?`
|
|
76
|
-
${l.green("\u2728 FastAPI project created successfully!")}
|
|
77
|
-
|
|
78
|
-
${l.bold("\u{1F4C2} Project structure:")}
|
|
79
|
-
${e}/
|
|
80
|
-
\u251C\u2500\u2500 .rapidkit/ # RapidKit CLI module
|
|
81
|
-
\u251C\u2500\u2500 src/
|
|
82
|
-
\u2502 \u251C\u2500\u2500 main.py # FastAPI application
|
|
83
|
-
\u2502 \u251C\u2500\u2500 cli.py # CLI commands
|
|
84
|
-
\u2502 \u251C\u2500\u2500 routing/ # API routes
|
|
85
|
-
\u2502 \u2514\u2500\u2500 modules/ # Module system
|
|
86
|
-
\u251C\u2500\u2500 tests/ # Test suite
|
|
87
|
-
\u251C\u2500\u2500 pyproject.toml # Poetry configuration
|
|
88
|
-
\u2514\u2500\u2500 README.md
|
|
89
|
-
|
|
90
|
-
${l.bold("\u{1F680} Get started:")}
|
|
91
|
-
${l.cyan(`cd ${f}`)}
|
|
92
|
-
${l.cyan("npx rapidkit init")} ${l.gray("# Install dependencies")}
|
|
93
|
-
${l.cyan("npx rapidkit dev")} ${l.gray("# Start dev server")}
|
|
94
|
-
|
|
95
|
-
${l.bold("\u{1F4DA} Available commands:")}
|
|
96
|
-
npx rapidkit init # Install dependencies (poetry install)
|
|
97
|
-
npx rapidkit dev # Start dev server with hot reload
|
|
98
|
-
npx rapidkit start # Start production server
|
|
99
|
-
npx rapidkit test # Run tests
|
|
100
|
-
npx rapidkit lint # Lint code
|
|
101
|
-
npx rapidkit format # Format code
|
|
102
|
-
|
|
103
|
-
${l.gray("Alternative: make dev, ./rapidkit dev, poetry run dev")}
|
|
104
|
-
${l.gray("\u{1F4A1} Tip: Install globally (npm i -g rapidkit) to use without npx")}
|
|
105
|
-
`:`
|
|
106
|
-
${l.green("\u2728 NestJS project created successfully!")}
|
|
107
|
-
|
|
108
|
-
${l.bold("\u{1F4C2} Project structure:")}
|
|
109
|
-
${e}/
|
|
110
|
-
\u251C\u2500\u2500 .rapidkit/ # RapidKit CLI module
|
|
111
|
-
\u251C\u2500\u2500 src/
|
|
112
|
-
\u2502 \u251C\u2500\u2500 main.ts # Application entry point
|
|
113
|
-
\u2502 \u251C\u2500\u2500 app.module.ts # Root module
|
|
114
|
-
\u2502 \u251C\u2500\u2500 config/ # Configuration
|
|
115
|
-
\u2502 \u2514\u2500\u2500 examples/ # Example module
|
|
116
|
-
\u251C\u2500\u2500 test/ # Test files
|
|
117
|
-
\u251C\u2500\u2500 package.json # Dependencies
|
|
118
|
-
\u2514\u2500\u2500 README.md
|
|
119
|
-
|
|
120
|
-
${l.bold("\u{1F680} Get started:")}
|
|
121
|
-
${l.cyan(`cd ${f}`)}
|
|
122
|
-
${l.cyan("npx rapidkit init")} ${l.gray("# Install dependencies")}
|
|
123
|
-
${l.cyan("cp .env.example .env")}
|
|
124
|
-
${l.cyan("npx rapidkit dev")} ${l.gray("# Start dev server")}
|
|
125
|
-
|
|
126
|
-
${l.bold("\u{1F4DA} Available commands:")}
|
|
127
|
-
npx rapidkit init # Install dependencies
|
|
128
|
-
npx rapidkit dev # Start dev server with hot reload
|
|
129
|
-
npx rapidkit start # Start production server
|
|
130
|
-
npx rapidkit build # Build for production
|
|
131
|
-
npx rapidkit test # Run tests
|
|
132
|
-
npx rapidkit lint # Lint code
|
|
133
|
-
npx rapidkit format # Format code
|
|
134
|
-
|
|
135
|
-
${l.bold("\u{1F310} API endpoints:")}
|
|
136
|
-
http://localhost:8000/health # Health check
|
|
137
|
-
http://localhost:8000/docs # Swagger docs
|
|
138
|
-
http://localhost:8000/examples/notes # Example API
|
|
139
|
-
|
|
140
|
-
${l.gray("Alternative: npm run start:dev, ./rapidkit dev")}
|
|
141
|
-
${l.gray("\u{1F4A1} Tip: Install globally (npm i -g rapidkit) to use without npx")}
|
|
142
|
-
`);}catch(a){throw n.fail(`Failed to generate ${i} project`),a}}function bo(e){return e.split(/[-_\s]+/).map(o=>o.charAt(0).toUpperCase()+o.slice(1)).join("")}async function Qr(e,o){await promises.mkdir(h.dirname(e),{recursive:true}),await promises.writeFile(e,o,"utf8");}function Xr(e){return `package main
|
|
143
|
-
|
|
144
|
-
import (
|
|
145
|
-
"fmt"
|
|
146
|
-
"log/slog"
|
|
147
|
-
"os"
|
|
148
|
-
"os/signal"
|
|
149
|
-
"syscall"
|
|
150
|
-
"time"
|
|
151
|
-
|
|
152
|
-
_ "${e.module_path}/docs"
|
|
153
|
-
"${e.module_path}/internal/config"
|
|
154
|
-
"${e.module_path}/internal/server"
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
// Build-time variables \u2014 injected via -ldflags.
|
|
158
|
-
var (
|
|
159
|
-
version = "dev"
|
|
160
|
-
commit = "none"
|
|
161
|
-
date = "unknown"
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
func main() {
|
|
165
|
-
cfg := config.Load()
|
|
166
|
-
|
|
167
|
-
log := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
|
168
|
-
Level: config.ParseLogLevel(cfg.LogLevel),
|
|
169
|
-
}))
|
|
170
|
-
slog.SetDefault(log)
|
|
171
|
-
|
|
172
|
-
app := server.NewApp(cfg)
|
|
173
|
-
|
|
174
|
-
// Graceful shutdown on SIGINT / SIGTERM
|
|
175
|
-
quit := make(chan os.Signal, 1)
|
|
176
|
-
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
|
|
177
|
-
|
|
178
|
-
go func() {
|
|
179
|
-
slog.Info("starting", "port", cfg.Port, "version", version, "commit", commit, "date", date, "env", cfg.Env)
|
|
180
|
-
fmt.Printf("\\n\u{1F680} Server \u2192 http://127.0.0.1:%s\\n", cfg.Port)
|
|
181
|
-
fmt.Printf("\u{1F4D6} Docs \u2192 http://127.0.0.1:%s/docs\\n\\n", cfg.Port)
|
|
182
|
-
if err := app.Listen(":" + cfg.Port); err != nil {
|
|
183
|
-
slog.Error("server error", "err", err)
|
|
184
|
-
os.Exit(1)
|
|
185
|
-
}
|
|
186
|
-
}()
|
|
187
|
-
|
|
188
|
-
<-quit
|
|
189
|
-
slog.Info("shutting down\u2026")
|
|
190
|
-
if err := app.ShutdownWithTimeout(5 * time.Second); err != nil {
|
|
191
|
-
slog.Error("graceful shutdown failed", "err", err)
|
|
192
|
-
os.Exit(1)
|
|
193
|
-
}
|
|
194
|
-
slog.Info("server stopped")
|
|
195
|
-
}
|
|
196
|
-
`}function Zr(e){return `module ${e.module_path}
|
|
197
|
-
|
|
198
|
-
go ${e.go_version}
|
|
199
|
-
|
|
200
|
-
require (
|
|
201
|
-
github.com/gofiber/fiber/v2 v2.52.5
|
|
202
|
-
github.com/swaggo/fiber-swagger v1.3.0
|
|
203
|
-
github.com/swaggo/swag v1.16.3
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
require (
|
|
207
|
-
github.com/KyleBanks/depth v1.2.1 // indirect
|
|
208
|
-
github.com/andybalholm/brotli v1.1.0 // indirect
|
|
209
|
-
github.com/ghodss/yaml v1.0.0 // indirect
|
|
210
|
-
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
|
211
|
-
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
|
212
|
-
github.com/go-openapi/spec v0.21.0 // indirect
|
|
213
|
-
github.com/go-openapi/swag v0.23.0 // indirect
|
|
214
|
-
github.com/josharian/intern v1.0.0 // indirect
|
|
215
|
-
github.com/klauspost/compress v1.17.6 // indirect
|
|
216
|
-
github.com/mailru/easyjson v0.7.7 // indirect
|
|
217
|
-
github.com/mattn/go-colorable v0.1.13 // indirect
|
|
218
|
-
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
219
|
-
github.com/mattn/go-runewidth v0.0.15 // indirect
|
|
220
|
-
github.com/rivo/uniseg v0.2.0 // indirect
|
|
221
|
-
github.com/swaggo/files v1.0.1 // indirect
|
|
222
|
-
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
|
223
|
-
github.com/valyala/fasthttp v1.52.0 // indirect
|
|
224
|
-
github.com/valyala/tcplisten v1.0.0 // indirect
|
|
225
|
-
golang.org/x/net v0.25.0 // indirect
|
|
226
|
-
golang.org/x/sys v0.16.0 // indirect
|
|
227
|
-
golang.org/x/tools v0.21.0 // indirect
|
|
228
|
-
gopkg.in/yaml.v2 v2.4.0 // indirect
|
|
229
|
-
)
|
|
230
|
-
`}function ei(e){return `package config
|
|
231
|
-
|
|
232
|
-
import (
|
|
233
|
-
"log/slog"
|
|
234
|
-
"os"
|
|
235
|
-
"strings"
|
|
236
|
-
)
|
|
237
|
-
|
|
238
|
-
// Config holds application configuration loaded from environment variables.
|
|
239
|
-
type Config struct {
|
|
240
|
-
Port string
|
|
241
|
-
Env string
|
|
242
|
-
LogLevel string
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Load reads configuration from environment variables with sensible defaults.
|
|
246
|
-
func Load() *Config {
|
|
247
|
-
env := getEnv("APP_ENV", "development")
|
|
248
|
-
return &Config{
|
|
249
|
-
Port: getEnv("PORT", "${e.port}"),
|
|
250
|
-
Env: env,
|
|
251
|
-
LogLevel: getEnv("LOG_LEVEL", defaultLogLevel(env)),
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// ParseLogLevel maps a level string to the corresponding slog.Level.
|
|
256
|
-
// Falls back to Info for unrecognised values.
|
|
257
|
-
func ParseLogLevel(s string) slog.Level {
|
|
258
|
-
switch strings.ToLower(s) {
|
|
259
|
-
case "debug":
|
|
260
|
-
return slog.LevelDebug
|
|
261
|
-
case "warn", "warning":
|
|
262
|
-
return slog.LevelWarn
|
|
263
|
-
case "error":
|
|
264
|
-
return slog.LevelError
|
|
265
|
-
default:
|
|
266
|
-
return slog.LevelInfo
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
func defaultLogLevel(env string) string {
|
|
271
|
-
if env == "development" {
|
|
272
|
-
return "debug"
|
|
273
|
-
}
|
|
274
|
-
return "info"
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
func getEnv(key, fallback string) string {
|
|
278
|
-
if v, ok := os.LookupEnv(key); ok && v != "" {
|
|
279
|
-
return v
|
|
280
|
-
}
|
|
281
|
-
return fallback
|
|
282
|
-
}
|
|
283
|
-
`}function ti(e){return `package server
|
|
284
|
-
|
|
285
|
-
import (
|
|
286
|
-
"net/http"
|
|
287
|
-
"time"
|
|
288
|
-
|
|
289
|
-
"github.com/gofiber/fiber/v2"
|
|
290
|
-
"github.com/gofiber/fiber/v2/middleware/recover"
|
|
291
|
-
fiberSwagger "github.com/swaggo/fiber-swagger"
|
|
292
|
-
|
|
293
|
-
"${e.module_path}/internal/apierr"
|
|
294
|
-
"${e.module_path}/internal/config"
|
|
295
|
-
"${e.module_path}/internal/handlers"
|
|
296
|
-
"${e.module_path}/internal/middleware"
|
|
297
|
-
)
|
|
298
|
-
|
|
299
|
-
// NewApp creates and configures the Fiber application.
|
|
300
|
-
// Call this from main \u2014 or from tests via server.NewApp(cfg).
|
|
301
|
-
func NewApp(cfg *config.Config) *fiber.App {
|
|
302
|
-
app := fiber.New(fiber.Config{
|
|
303
|
-
AppName: "${e.project_name}",
|
|
304
|
-
ReadTimeout: 5 * time.Second,
|
|
305
|
-
WriteTimeout: 10 * time.Second,
|
|
306
|
-
IdleTimeout: 30 * time.Second,
|
|
307
|
-
// Override default error handler to always return JSON.
|
|
308
|
-
// The catch-all middleware returns fiber.ErrNotFound so all 404s
|
|
309
|
-
// are routed here, keeping error formatting in one place.
|
|
310
|
-
ErrorHandler: func(c *fiber.Ctx, err error) error {
|
|
311
|
-
code := fiber.StatusInternalServerError
|
|
312
|
-
if e, ok := err.(*fiber.Error); ok {
|
|
313
|
-
code = e.Code
|
|
314
|
-
}
|
|
315
|
-
if code == http.StatusNotFound {
|
|
316
|
-
return apierr.NotFound(c, "route not found")
|
|
317
|
-
}
|
|
318
|
-
if code == http.StatusMethodNotAllowed {
|
|
319
|
-
return apierr.MethodNotAllowed(c)
|
|
320
|
-
}
|
|
321
|
-
// Fallback for any unexpected error (e.g. panic-recovered 500).
|
|
322
|
-
return apierr.InternalError(c, err)
|
|
323
|
-
},
|
|
324
|
-
})
|
|
325
|
-
|
|
326
|
-
app.Use(recover.New())
|
|
327
|
-
app.Use(middleware.CORS())
|
|
328
|
-
app.Use(middleware.RequestID())
|
|
329
|
-
app.Use(middleware.RateLimit())
|
|
330
|
-
app.Use(middleware.Logger())
|
|
331
|
-
|
|
332
|
-
// Swagger UI \u2014 /docs redirects to /docs/index.html
|
|
333
|
-
app.Get("/docs", func(c *fiber.Ctx) error { return c.Redirect("/docs/index.html", fiber.StatusFound) })
|
|
334
|
-
app.Get("/docs/*", fiberSwagger.WrapHandler)
|
|
335
|
-
|
|
336
|
-
v1 := app.Group("/api/v1")
|
|
337
|
-
v1.Get("/health/live", handlers.Liveness)
|
|
338
|
-
v1.Get("/health/ready", handlers.Readiness)
|
|
339
|
-
v1.Get("/echo/:name", handlers.EchoParams)
|
|
340
|
-
|
|
341
|
-
// 404 catch-all: return fiber.ErrNotFound so it is processed by the
|
|
342
|
-
// custom ErrorHandler above, keeping all error formatting in one place.
|
|
343
|
-
app.Use(func(c *fiber.Ctx) error {
|
|
344
|
-
return fiber.ErrNotFound
|
|
345
|
-
})
|
|
346
|
-
|
|
347
|
-
return app
|
|
348
|
-
}
|
|
349
|
-
`}function oi(){return `package handlers
|
|
350
|
-
|
|
351
|
-
import (
|
|
352
|
-
"time"
|
|
353
|
-
|
|
354
|
-
"github.com/gofiber/fiber/v2"
|
|
355
|
-
)
|
|
356
|
-
|
|
357
|
-
// Liveness signals the process is alive (Kubernetes livenessProbe).
|
|
358
|
-
//
|
|
359
|
-
// @Summary Liveness probe
|
|
360
|
-
// @Description Returns 200 when the process is alive.
|
|
361
|
-
// @Tags health
|
|
362
|
-
// @Produce json
|
|
363
|
-
// @Success 200 {object} map[string]string
|
|
364
|
-
// @Router /api/v1/health/live [get]
|
|
365
|
-
func Liveness(c *fiber.Ctx) error {
|
|
366
|
-
return c.JSON(fiber.Map{
|
|
367
|
-
"status": "ok",
|
|
368
|
-
"time": time.Now().UTC().Format(time.RFC3339),
|
|
369
|
-
})
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// Readiness signals the service can accept traffic (Kubernetes readinessProbe).
|
|
373
|
-
// Extend this function to check database connectivity, caches, etc.
|
|
374
|
-
//
|
|
375
|
-
// @Summary Readiness probe
|
|
376
|
-
// @Description Returns 200 when the service is ready to accept traffic.
|
|
377
|
-
// @Tags health
|
|
378
|
-
// @Produce json
|
|
379
|
-
// @Success 200 {object} map[string]string
|
|
380
|
-
// @Router /api/v1/health/ready [get]
|
|
381
|
-
func Readiness(c *fiber.Ctx) error {
|
|
382
|
-
return c.JSON(fiber.Map{
|
|
383
|
-
"status": "ready",
|
|
384
|
-
"time": time.Now().UTC().Format(time.RFC3339),
|
|
385
|
-
})
|
|
386
|
-
}
|
|
387
|
-
`}function ri(e){return `package handlers_test
|
|
388
|
-
|
|
389
|
-
import (
|
|
390
|
-
"encoding/json"
|
|
391
|
-
"io"
|
|
392
|
-
"net/http"
|
|
393
|
-
"net/http/httptest"
|
|
394
|
-
"testing"
|
|
395
|
-
|
|
396
|
-
"${e.module_path}/internal/config"
|
|
397
|
-
"${e.module_path}/internal/server"
|
|
398
|
-
)
|
|
399
|
-
|
|
400
|
-
func TestLiveness(t *testing.T) {
|
|
401
|
-
app := server.NewApp(config.Load())
|
|
402
|
-
req := httptest.NewRequest(http.MethodGet, "/api/v1/health/live", nil)
|
|
403
|
-
resp, err := app.Test(req, -1)
|
|
404
|
-
if err != nil {
|
|
405
|
-
t.Fatalf("request error: %v", err)
|
|
406
|
-
}
|
|
407
|
-
defer resp.Body.Close()
|
|
408
|
-
|
|
409
|
-
if resp.StatusCode != http.StatusOK {
|
|
410
|
-
data, _ := io.ReadAll(resp.Body)
|
|
411
|
-
t.Fatalf("expected 200, got %d: %s", resp.StatusCode, data)
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
var body map[string]any
|
|
415
|
-
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
416
|
-
t.Fatalf("invalid JSON: %v", err)
|
|
417
|
-
}
|
|
418
|
-
if body["status"] != "ok" {
|
|
419
|
-
t.Fatalf("expected ok, got %v", body["status"])
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
func TestReadiness(t *testing.T) {
|
|
424
|
-
app := server.NewApp(config.Load())
|
|
425
|
-
req := httptest.NewRequest(http.MethodGet, "/api/v1/health/ready", nil)
|
|
426
|
-
resp, err := app.Test(req, -1)
|
|
427
|
-
if err != nil {
|
|
428
|
-
t.Fatalf("request error: %v", err)
|
|
429
|
-
}
|
|
430
|
-
defer resp.Body.Close()
|
|
431
|
-
|
|
432
|
-
if resp.StatusCode != http.StatusOK {
|
|
433
|
-
t.Fatalf("expected 200, got %d: %s", resp.StatusCode, resp.Status)
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
`}function ii(){return `# \u2500\u2500 Build stage \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
437
|
-
FROM golang:1.24-alpine AS builder
|
|
438
|
-
|
|
439
|
-
# Build-time version injection
|
|
440
|
-
ARG VERSION=dev
|
|
441
|
-
ARG COMMIT=none
|
|
442
|
-
ARG DATE=unknown
|
|
443
|
-
|
|
444
|
-
WORKDIR /app
|
|
445
|
-
COPY go.mod go.sum ./
|
|
446
|
-
RUN go mod download
|
|
447
|
-
|
|
448
|
-
COPY . .
|
|
449
|
-
RUN CGO_ENABLED=0 GOOS=linux go build \\
|
|
450
|
-
-ldflags="-s -w -X main.version=$\${VERSION} -X main.commit=$\${COMMIT} -X main.date=$\${DATE}" \\
|
|
451
|
-
-o server ./cmd/server
|
|
452
|
-
|
|
453
|
-
# \u2500\u2500 Runtime stage \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
454
|
-
# alpine includes busybox wget required for the HEALTHCHECK below.
|
|
455
|
-
FROM alpine:3.21
|
|
456
|
-
|
|
457
|
-
RUN addgroup -S app && adduser -S -G app app
|
|
458
|
-
COPY --from=builder /app/server /server
|
|
459
|
-
USER app
|
|
460
|
-
|
|
461
|
-
EXPOSE 3000
|
|
462
|
-
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
|
|
463
|
-
CMD wget -qO- http://localhost:3000/api/v1/health/live || exit 1
|
|
464
|
-
ENTRYPOINT ["/server"]
|
|
465
|
-
`}function ni(e){return `version: "3.9"
|
|
466
|
-
|
|
467
|
-
services:
|
|
468
|
-
api:
|
|
469
|
-
build: .
|
|
470
|
-
container_name: ${e.project_name}
|
|
471
|
-
ports:
|
|
472
|
-
- "${e.port}:${e.port}"
|
|
473
|
-
environment:
|
|
474
|
-
PORT: "${e.port}"
|
|
475
|
-
APP_ENV: development
|
|
476
|
-
LOG_LEVEL: info
|
|
477
|
-
CORS_ALLOW_ORIGINS: "*"
|
|
478
|
-
RATE_LIMIT_RPS: "100"
|
|
479
|
-
restart: unless-stopped
|
|
480
|
-
`}function si(e){return `.PHONY: dev run build test cover lint fmt tidy docs docker-up docker-down
|
|
481
|
-
|
|
482
|
-
# Build-time metadata
|
|
483
|
-
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
|
484
|
-
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo "none")
|
|
485
|
-
DATE ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
486
|
-
LDFLAGS = -ldflags "-s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(DATE)"
|
|
487
|
-
# Go tool binaries are installed to GOPATH/bin; include it so \`air\` and \`swag\` are found.
|
|
488
|
-
GOBIN ?= $(shell go env GOPATH)/bin
|
|
489
|
-
|
|
490
|
-
# Hot reload \u2014 installs air on first use
|
|
491
|
-
dev:
|
|
492
|
-
@test -x "$(GOBIN)/air" || go install github.com/air-verse/air@latest
|
|
493
|
-
$(GOBIN)/air
|
|
494
|
-
|
|
495
|
-
run:
|
|
496
|
-
go run $(LDFLAGS) ./cmd/server
|
|
497
|
-
|
|
498
|
-
build:
|
|
499
|
-
go build $(LDFLAGS) -o bin/${e.project_name} ./cmd/server
|
|
500
|
-
|
|
501
|
-
test:
|
|
502
|
-
go test ./... -v -race
|
|
503
|
-
|
|
504
|
-
cover:
|
|
505
|
-
go test ./... -race -coverprofile=coverage.out
|
|
506
|
-
go tool cover -html=coverage.out -o coverage.html
|
|
507
|
-
@echo "Coverage report: coverage.html"
|
|
508
|
-
|
|
509
|
-
# Generate Swagger docs \u2014 installs swag on first use
|
|
510
|
-
docs:
|
|
511
|
-
@test -x "$(GOBIN)/swag" || go install github.com/swaggo/swag/cmd/swag@latest
|
|
512
|
-
$(GOBIN)/swag init -g main.go -d cmd/server,internal/handlers,internal/apierr -o docs --parseDependency
|
|
513
|
-
tidy:
|
|
514
|
-
go mod tidy
|
|
515
|
-
|
|
516
|
-
docker-up:
|
|
517
|
-
go mod tidy
|
|
518
|
-
docker compose up --build \\
|
|
519
|
-
--build-arg VERSION=$(VERSION) \\
|
|
520
|
-
--build-arg COMMIT=$(COMMIT) \\
|
|
521
|
-
--build-arg DATE=$(DATE) \\
|
|
522
|
-
-d
|
|
523
|
-
|
|
524
|
-
docker-down:
|
|
525
|
-
docker compose down
|
|
526
|
-
`}function ai(e){return `# Application
|
|
527
|
-
PORT=${e.port}
|
|
528
|
-
APP_ENV=development
|
|
529
|
-
LOG_LEVEL=debug
|
|
530
|
-
|
|
531
|
-
# CORS \u2014 comma-separated list of allowed origins, or * to allow all
|
|
532
|
-
CORS_ALLOW_ORIGINS=*
|
|
533
|
-
|
|
534
|
-
# Rate limiting \u2014 max requests per IP per second
|
|
535
|
-
RATE_LIMIT_RPS=100
|
|
536
|
-
`}function ci(){return `# Binaries
|
|
537
|
-
bin/
|
|
538
|
-
*.exe
|
|
539
|
-
*.exe~
|
|
540
|
-
*.dll
|
|
541
|
-
*.so
|
|
542
|
-
*.dylib
|
|
543
|
-
|
|
544
|
-
# Test binary
|
|
545
|
-
*.test
|
|
546
|
-
|
|
547
|
-
# Output of go coverage tool
|
|
548
|
-
*.out
|
|
549
|
-
coverage.html
|
|
550
|
-
|
|
551
|
-
# Go workspace
|
|
552
|
-
go.work
|
|
553
|
-
go.work.sum
|
|
554
|
-
|
|
555
|
-
# Environment
|
|
556
|
-
.env
|
|
557
|
-
.env.local
|
|
558
|
-
|
|
559
|
-
# Hot reload (air)
|
|
560
|
-
tmp/
|
|
561
|
-
|
|
562
|
-
# Swagger \u2014 generated files (committed stub docs/doc.go; run \`make docs\` to regenerate)
|
|
563
|
-
docs/swagger.json
|
|
564
|
-
docs/swagger.yaml
|
|
565
|
-
docs/docs.go
|
|
566
|
-
|
|
567
|
-
# Editor
|
|
568
|
-
.idea/
|
|
569
|
-
.vscode/
|
|
570
|
-
*.swp
|
|
571
|
-
*.swo
|
|
572
|
-
|
|
573
|
-
# OS
|
|
574
|
-
.DS_Store
|
|
575
|
-
Thumbs.db
|
|
576
|
-
`}function li(e){return `name: CI
|
|
577
|
-
|
|
578
|
-
on:
|
|
579
|
-
push:
|
|
580
|
-
branches: [main, develop]
|
|
581
|
-
pull_request:
|
|
582
|
-
branches: [main]
|
|
583
|
-
|
|
584
|
-
jobs:
|
|
585
|
-
test:
|
|
586
|
-
name: Test
|
|
587
|
-
runs-on: ubuntu-latest
|
|
588
|
-
|
|
589
|
-
steps:
|
|
590
|
-
- uses: actions/checkout@v4
|
|
591
|
-
|
|
592
|
-
- name: Set up Go
|
|
593
|
-
uses: actions/setup-go@v5
|
|
594
|
-
with:
|
|
595
|
-
go-version: "${e.go_version}"
|
|
596
|
-
cache: true
|
|
597
|
-
|
|
598
|
-
- name: Tidy
|
|
599
|
-
run: go mod tidy
|
|
600
|
-
|
|
601
|
-
- name: Build
|
|
602
|
-
run: go build ./...
|
|
603
|
-
|
|
604
|
-
- name: Test
|
|
605
|
-
run: go test ./... -race -coverprofile=coverage.out
|
|
606
|
-
|
|
607
|
-
- name: Upload coverage
|
|
608
|
-
uses: actions/upload-artifact@v4
|
|
609
|
-
with:
|
|
610
|
-
name: coverage
|
|
611
|
-
path: coverage.out
|
|
612
|
-
|
|
613
|
-
lint:
|
|
614
|
-
name: Lint
|
|
615
|
-
runs-on: ubuntu-latest
|
|
616
|
-
|
|
617
|
-
steps:
|
|
618
|
-
- uses: actions/checkout@v4
|
|
619
|
-
|
|
620
|
-
- name: Set up Go
|
|
621
|
-
uses: actions/setup-go@v5
|
|
622
|
-
with:
|
|
623
|
-
go-version: "${e.go_version}"
|
|
624
|
-
cache: true
|
|
625
|
-
|
|
626
|
-
- name: golangci-lint
|
|
627
|
-
uses: golangci/golangci-lint-action@v6
|
|
628
|
-
with:
|
|
629
|
-
version: latest
|
|
630
|
-
`}function di(e){return `# ${bo(e.project_name)}
|
|
631
|
-
|
|
632
|
-
> ${e.description}
|
|
633
|
-
|
|
634
|
-
Built with [Go](https://go.dev/) + [Fiber v2](https://gofiber.io/) \xB7 Scaffolded by [RapidKit](https://getrapidkit.com)
|
|
635
|
-
|
|
636
|
-
## Quick start
|
|
637
|
-
|
|
638
|
-
\`\`\`bash
|
|
639
|
-
# Run locally (hot reload)
|
|
640
|
-
make dev
|
|
641
|
-
|
|
642
|
-
# Run tests
|
|
643
|
-
make test
|
|
644
|
-
|
|
645
|
-
# Build binary
|
|
646
|
-
make build
|
|
647
|
-
|
|
648
|
-
# Generate / refresh Swagger docs
|
|
649
|
-
make docs
|
|
650
|
-
|
|
651
|
-
# Docker
|
|
652
|
-
make docker-up
|
|
653
|
-
\`\`\`
|
|
654
|
-
|
|
655
|
-
## Swagger / OpenAPI
|
|
656
|
-
|
|
657
|
-
After running \`make docs\`, the interactive UI is available at:
|
|
658
|
-
|
|
659
|
-
\`\`\`
|
|
660
|
-
http://localhost:${e.port}/docs
|
|
661
|
-
\`\`\`
|
|
662
|
-
|
|
663
|
-
The raw OpenAPI spec is served at \`/docs/doc.json\`.
|
|
664
|
-
|
|
665
|
-
## Endpoints
|
|
666
|
-
|
|
667
|
-
| Method | Path | Description |
|
|
668
|
-
|--------|------|-------------|
|
|
669
|
-
| GET | /api/v1/health/live | Kubernetes livenessProbe |
|
|
670
|
-
| GET | /api/v1/health/ready | Kubernetes readinessProbe |
|
|
671
|
-
| GET | /api/v1/echo/:name | Example handler \u2014 remove in production |
|
|
672
|
-
| GET | /docs/* | Swagger UI (OpenAPI docs) |
|
|
673
|
-
|
|
674
|
-
## Configuration
|
|
675
|
-
|
|
676
|
-
All configuration is done through environment variables (see \`.env.example\`):
|
|
677
|
-
|
|
678
|
-
| Variable | Default | Description |
|
|
679
|
-
|----------|---------|-------------|
|
|
680
|
-
| \`PORT\` | \`${e.port}\` | HTTP listen port |
|
|
681
|
-
| \`APP_ENV\` | \`development\` | Application environment |
|
|
682
|
-
| \`LOG_LEVEL\` | \`debug\` / \`info\` | \`debug\` \\| \`info\` \\| \`warn\` \\| \`error\` |
|
|
683
|
-
| \`CORS_ALLOW_ORIGINS\` | \`*\` | Comma-separated list of allowed origins, or \`*\` |
|
|
684
|
-
| \`RATE_LIMIT_RPS\` | \`100\` | Max requests per IP per second |
|
|
685
|
-
|
|
686
|
-
## Project structure
|
|
687
|
-
|
|
688
|
-
\`\`\`
|
|
689
|
-
${e.project_name}/
|
|
690
|
-
\u251C\u2500\u2500 cmd/
|
|
691
|
-
\u2502 \u2514\u2500\u2500 server/
|
|
692
|
-
\u2502 \u2514\u2500\u2500 main.go # Graceful shutdown + version ldflags
|
|
693
|
-
\u251C\u2500\u2500 docs/ # Swagger generated files (\`make docs\`)
|
|
694
|
-
\u2502 \u2514\u2500\u2500 doc.go # Package-level OpenAPI annotations
|
|
695
|
-
\u251C\u2500\u2500 internal/
|
|
696
|
-
\u2502 \u251C\u2500\u2500 apierr/ # Consistent JSON error envelope
|
|
697
|
-
\u2502 \u2502 \u251C\u2500\u2500 apierr.go
|
|
698
|
-
\u2502 \u2502 \u2514\u2500\u2500 apierr_test.go
|
|
699
|
-
\u2502 \u251C\u2500\u2500 config/ # 12-factor configuration
|
|
700
|
-
\u2502 \u2502 \u251C\u2500\u2500 config.go
|
|
701
|
-
\u2502 \u2502 \u2514\u2500\u2500 config_test.go
|
|
702
|
-
\u2502 \u251C\u2500\u2500 handlers/ # HTTP handlers + tests
|
|
703
|
-
\u2502 \u2502 \u251C\u2500\u2500 health.go
|
|
704
|
-
\u2502 \u2502 \u251C\u2500\u2500 health_test.go
|
|
705
|
-
\u2502 \u2502 \u251C\u2500\u2500 example.go # EchoParams \u2014 replace with your own handlers
|
|
706
|
-
\u2502 \u2502 \u2514\u2500\u2500 example_test.go
|
|
707
|
-
\u2502 \u251C\u2500\u2500 middleware/
|
|
708
|
-
\u2502 \u2502 \u251C\u2500\u2500 requestid.go # X-Request-ID + structured logger
|
|
709
|
-
\u2502 \u2502 \u251C\u2500\u2500 requestid_test.go
|
|
710
|
-
\u2502 \u2502 \u251C\u2500\u2500 cors.go # CORS (CORS_ALLOW_ORIGINS)
|
|
711
|
-
\u2502 \u2502 \u251C\u2500\u2500 cors_test.go
|
|
712
|
-
\u2502 \u2502 \u251C\u2500\u2500 ratelimit.go # Per-IP limiter (RATE_LIMIT_RPS)
|
|
713
|
-
\u2502 \u2502 \u2514\u2500\u2500 ratelimit_test.go
|
|
714
|
-
\u2502 \u2514\u2500\u2500 server/
|
|
715
|
-
\u2502 \u251C\u2500\u2500 server.go
|
|
716
|
-
\u2502 \u2514\u2500\u2500 server_test.go
|
|
717
|
-
\u251C\u2500\u2500 .air.toml # Hot reload
|
|
718
|
-
\u251C\u2500\u2500 .github/workflows/ci.yml # CI: test + lint
|
|
719
|
-
\u251C\u2500\u2500 .golangci.yml
|
|
720
|
-
\u251C\u2500\u2500 Dockerfile # Multi-stage, alpine HEALTHCHECK
|
|
721
|
-
\u251C\u2500\u2500 docker-compose.yml
|
|
722
|
-
\u251C\u2500\u2500 Makefile
|
|
723
|
-
\u2514\u2500\u2500 README.md
|
|
724
|
-
\`\`\`
|
|
725
|
-
|
|
726
|
-
## Available commands
|
|
727
|
-
|
|
728
|
-
| Command | Description |
|
|
729
|
-
|---------|-------------|
|
|
730
|
-
| \`make dev\` | Hot reload via [air](https://github.com/air-verse/air) |
|
|
731
|
-
| \`make run\` | Run without hot reload |
|
|
732
|
-
| \`make build\` | Binary with version ldflags |
|
|
733
|
-
| \`make test\` | Run tests with race detector |
|
|
734
|
-
| \`make cover\` | HTML coverage report |
|
|
735
|
-
| \`make docs\` | Re-generate Swagger JSON (needs \`swag\`) |
|
|
736
|
-
| \`make lint\` | golangci-lint |
|
|
737
|
-
| \`make fmt\` | gofmt |
|
|
738
|
-
| \`make tidy\` | go mod tidy |
|
|
739
|
-
| \`make docker-up\` | Build & run via Docker Compose |
|
|
740
|
-
| \`make docker-down\` | Stop |
|
|
741
|
-
|
|
742
|
-
## License
|
|
743
|
-
|
|
744
|
-
${e.app_version} \xB7 ${e.author}
|
|
745
|
-
`}function pi(){return `package middleware
|
|
746
|
-
|
|
747
|
-
import (
|
|
748
|
-
"crypto/rand"
|
|
749
|
-
"encoding/hex"
|
|
750
|
-
"log/slog"
|
|
751
|
-
"time"
|
|
752
|
-
|
|
753
|
-
"github.com/gofiber/fiber/v2"
|
|
754
|
-
)
|
|
755
|
-
|
|
756
|
-
const headerRequestID = "X-Request-ID"
|
|
757
|
-
|
|
758
|
-
// RequestID injects a unique identifier into every request.
|
|
759
|
-
// If the caller sends an X-Request-ID header it is reused; otherwise a new one
|
|
760
|
-
// is generated and written back in the response.
|
|
761
|
-
func RequestID() fiber.Handler {
|
|
762
|
-
return func(c *fiber.Ctx) error {
|
|
763
|
-
id := c.Get(headerRequestID)
|
|
764
|
-
if id == "" {
|
|
765
|
-
id = newID()
|
|
766
|
-
}
|
|
767
|
-
c.Set(headerRequestID, id)
|
|
768
|
-
c.Locals("request_id", id)
|
|
769
|
-
return c.Next()
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
// Logger emits a structured JSON log line after each request.
|
|
774
|
-
func Logger() fiber.Handler {
|
|
775
|
-
return func(c *fiber.Ctx) error {
|
|
776
|
-
start := time.Now()
|
|
777
|
-
err := c.Next()
|
|
778
|
-
slog.Info("http",
|
|
779
|
-
"method", c.Method(),
|
|
780
|
-
"path", c.Path(),
|
|
781
|
-
"status", c.Response().StatusCode(),
|
|
782
|
-
"bytes", c.Response().Header.ContentLength(),
|
|
783
|
-
"latency_ms", time.Since(start).Milliseconds(),
|
|
784
|
-
"ip", c.IP(),
|
|
785
|
-
"request_id", c.Locals("request_id"),
|
|
786
|
-
)
|
|
787
|
-
return err
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
func newID() string {
|
|
792
|
-
b := make([]byte, 8)
|
|
793
|
-
if _, err := rand.Read(b); err != nil {
|
|
794
|
-
return "unknown"
|
|
795
|
-
}
|
|
796
|
-
return hex.EncodeToString(b)
|
|
797
|
-
}
|
|
798
|
-
`}function ui(e){return `package middleware_test
|
|
799
|
-
|
|
800
|
-
import (
|
|
801
|
-
"net/http"
|
|
802
|
-
"net/http/httptest"
|
|
803
|
-
"testing"
|
|
804
|
-
|
|
805
|
-
"github.com/gofiber/fiber/v2"
|
|
806
|
-
|
|
807
|
-
"${e.module_path}/internal/middleware"
|
|
808
|
-
)
|
|
809
|
-
|
|
810
|
-
func newTestApp() *fiber.App {
|
|
811
|
-
app := fiber.New()
|
|
812
|
-
app.Use(middleware.RequestID())
|
|
813
|
-
app.Use(middleware.Logger())
|
|
814
|
-
app.Get("/ping", func(c *fiber.Ctx) error {
|
|
815
|
-
return c.SendString("pong")
|
|
816
|
-
})
|
|
817
|
-
return app
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
func TestRequestID_IsGenerated(t *testing.T) {
|
|
821
|
-
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
|
|
822
|
-
resp, err := newTestApp().Test(req, -1)
|
|
823
|
-
if err != nil {
|
|
824
|
-
t.Fatalf("request error: %v", err)
|
|
825
|
-
}
|
|
826
|
-
defer resp.Body.Close()
|
|
827
|
-
|
|
828
|
-
id := resp.Header.Get("X-Request-ID")
|
|
829
|
-
if id == "" {
|
|
830
|
-
t.Fatal("expected X-Request-ID header to be set")
|
|
831
|
-
}
|
|
832
|
-
if len(id) != 16 { // 8 random bytes \u2192 16 hex chars
|
|
833
|
-
t.Fatalf("unexpected request ID length %d, want 16", len(id))
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
func TestRequestID_IsReused(t *testing.T) {
|
|
838
|
-
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
|
|
839
|
-
req.Header.Set("X-Request-ID", "my-trace-id")
|
|
840
|
-
resp, err := newTestApp().Test(req, -1)
|
|
841
|
-
if err != nil {
|
|
842
|
-
t.Fatalf("request error: %v", err)
|
|
843
|
-
}
|
|
844
|
-
defer resp.Body.Close()
|
|
845
|
-
|
|
846
|
-
id := resp.Header.Get("X-Request-ID")
|
|
847
|
-
if id != "my-trace-id" {
|
|
848
|
-
t.Fatalf("expected X-Request-ID to be reused, got %q", id)
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
`}function gi(){return `// Package apierr provides a consistent JSON error envelope for all API responses.
|
|
852
|
-
//
|
|
853
|
-
// Every error response looks like:
|
|
854
|
-
//
|
|
855
|
-
// {"error": "user not found", "code": "NOT_FOUND", "request_id": "a1b2c3d4..."}
|
|
856
|
-
package apierr
|
|
857
|
-
|
|
858
|
-
import (
|
|
859
|
-
"net/http"
|
|
860
|
-
|
|
861
|
-
"github.com/gofiber/fiber/v2"
|
|
862
|
-
)
|
|
863
|
-
|
|
864
|
-
// Response is the standard error envelope returned by all API endpoints.
|
|
865
|
-
type Response struct {
|
|
866
|
-
Error string \`json:"error"\`
|
|
867
|
-
Code string \`json:"code"\`
|
|
868
|
-
RequestID string \`json:"request_id,omitempty"\`
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
func reply(c *fiber.Ctx, status int, msg, code string) error {
|
|
872
|
-
rid, _ := c.Locals("request_id").(string)
|
|
873
|
-
return c.Status(status).JSON(Response{
|
|
874
|
-
Error: msg,
|
|
875
|
-
Code: code,
|
|
876
|
-
RequestID: rid,
|
|
877
|
-
})
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
// BadRequest responds with 400 and code "BAD_REQUEST".
|
|
881
|
-
func BadRequest(c *fiber.Ctx, msg string) error {
|
|
882
|
-
return reply(c, http.StatusBadRequest, msg, "BAD_REQUEST")
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
// NotFound responds with 404 and code "NOT_FOUND".
|
|
886
|
-
func NotFound(c *fiber.Ctx, msg string) error {
|
|
887
|
-
return reply(c, http.StatusNotFound, msg, "NOT_FOUND")
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
// Unauthorized responds with 401 and code "UNAUTHORIZED".
|
|
891
|
-
func Unauthorized(c *fiber.Ctx) error {
|
|
892
|
-
return reply(c, http.StatusUnauthorized, "authentication required", "UNAUTHORIZED")
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
// Forbidden responds with 403 and code "FORBIDDEN".
|
|
896
|
-
func Forbidden(c *fiber.Ctx) error {
|
|
897
|
-
return reply(c, http.StatusForbidden, "access denied", "FORBIDDEN")
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
// MethodNotAllowed responds with 405 and code "METHOD_NOT_ALLOWED".
|
|
901
|
-
func MethodNotAllowed(c *fiber.Ctx) error {
|
|
902
|
-
return reply(c, http.StatusMethodNotAllowed, "method not allowed", "METHOD_NOT_ALLOWED")
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
// InternalError responds with 500 and code "INTERNAL_ERROR".
|
|
906
|
-
// The original error is intentionally not exposed to the client.
|
|
907
|
-
func InternalError(c *fiber.Ctx, _ error) error {
|
|
908
|
-
return reply(c, http.StatusInternalServerError, "an internal error occurred", "INTERNAL_ERROR")
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
// TooManyRequests responds with 429 and code "TOO_MANY_REQUESTS".
|
|
912
|
-
func TooManyRequests(c *fiber.Ctx, msg string) error {
|
|
913
|
-
return reply(c, http.StatusTooManyRequests, msg, "TOO_MANY_REQUESTS")
|
|
914
|
-
}
|
|
915
|
-
`}function mi(e){return `package apierr_test
|
|
916
|
-
|
|
917
|
-
import (
|
|
918
|
-
"encoding/json"
|
|
919
|
-
"io"
|
|
920
|
-
"net/http"
|
|
921
|
-
"net/http/httptest"
|
|
922
|
-
"testing"
|
|
923
|
-
|
|
924
|
-
"github.com/gofiber/fiber/v2"
|
|
925
|
-
|
|
926
|
-
"${e.module_path}/internal/apierr"
|
|
927
|
-
)
|
|
928
|
-
|
|
929
|
-
func makeApp(fn func(*fiber.Ctx) error) *fiber.App {
|
|
930
|
-
app := fiber.New()
|
|
931
|
-
app.Get("/test", fn)
|
|
932
|
-
return app
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
func readJSON(t *testing.T, r io.Reader) apierr.Response {
|
|
936
|
-
t.Helper()
|
|
937
|
-
var out apierr.Response
|
|
938
|
-
if err := json.NewDecoder(r).Decode(&out); err != nil {
|
|
939
|
-
t.Fatalf("invalid JSON: %v", err)
|
|
940
|
-
}
|
|
941
|
-
return out
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
func TestBadRequest(t *testing.T) {
|
|
945
|
-
app := makeApp(func(c *fiber.Ctx) error { return apierr.BadRequest(c, "invalid email") })
|
|
946
|
-
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
947
|
-
resp, _ := app.Test(req, -1)
|
|
948
|
-
defer resp.Body.Close()
|
|
949
|
-
|
|
950
|
-
if resp.StatusCode != http.StatusBadRequest {
|
|
951
|
-
t.Fatalf("expected 400, got %d", resp.StatusCode)
|
|
952
|
-
}
|
|
953
|
-
body := readJSON(t, resp.Body)
|
|
954
|
-
if body.Code != "BAD_REQUEST" {
|
|
955
|
-
t.Fatalf("expected BAD_REQUEST, got %q", body.Code)
|
|
956
|
-
}
|
|
957
|
-
if body.Error != "invalid email" {
|
|
958
|
-
t.Fatalf("unexpected error message: %q", body.Error)
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
func TestNotFound(t *testing.T) {
|
|
963
|
-
app := makeApp(func(c *fiber.Ctx) error { return apierr.NotFound(c, "user not found") })
|
|
964
|
-
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
965
|
-
resp, _ := app.Test(req, -1)
|
|
966
|
-
defer resp.Body.Close()
|
|
967
|
-
|
|
968
|
-
if resp.StatusCode != http.StatusNotFound {
|
|
969
|
-
t.Fatalf("expected 404, got %d", resp.StatusCode)
|
|
970
|
-
}
|
|
971
|
-
body := readJSON(t, resp.Body)
|
|
972
|
-
if body.Code != "NOT_FOUND" {
|
|
973
|
-
t.Fatalf("expected NOT_FOUND, got %q", body.Code)
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
func TestUnauthorized(t *testing.T) {
|
|
978
|
-
app := makeApp(func(c *fiber.Ctx) error { return apierr.Unauthorized(c) })
|
|
979
|
-
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
980
|
-
resp, _ := app.Test(req, -1)
|
|
981
|
-
defer resp.Body.Close()
|
|
982
|
-
|
|
983
|
-
if resp.StatusCode != http.StatusUnauthorized {
|
|
984
|
-
t.Fatalf("expected 401, got %d", resp.StatusCode)
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
func TestForbidden(t *testing.T) {
|
|
989
|
-
app := makeApp(func(c *fiber.Ctx) error { return apierr.Forbidden(c) })
|
|
990
|
-
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
991
|
-
resp, _ := app.Test(req, -1)
|
|
992
|
-
defer resp.Body.Close()
|
|
993
|
-
|
|
994
|
-
if resp.StatusCode != http.StatusForbidden {
|
|
995
|
-
t.Fatalf("expected 403, got %d", resp.StatusCode)
|
|
996
|
-
}
|
|
997
|
-
body := readJSON(t, resp.Body)
|
|
998
|
-
if body.Code != "FORBIDDEN" {
|
|
999
|
-
t.Fatalf("expected FORBIDDEN, got %q", body.Code)
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
func TestMethodNotAllowed(t *testing.T) {
|
|
1004
|
-
app := makeApp(func(c *fiber.Ctx) error { return apierr.MethodNotAllowed(c) })
|
|
1005
|
-
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
1006
|
-
resp, _ := app.Test(req, -1)
|
|
1007
|
-
defer resp.Body.Close()
|
|
1008
|
-
|
|
1009
|
-
if resp.StatusCode != http.StatusMethodNotAllowed {
|
|
1010
|
-
t.Fatalf("expected 405, got %d", resp.StatusCode)
|
|
1011
|
-
}
|
|
1012
|
-
body := readJSON(t, resp.Body)
|
|
1013
|
-
if body.Code != "METHOD_NOT_ALLOWED" {
|
|
1014
|
-
t.Fatalf("expected METHOD_NOT_ALLOWED, got %q", body.Code)
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
func TestInternalError(t *testing.T) {
|
|
1019
|
-
app := makeApp(func(c *fiber.Ctx) error { return apierr.InternalError(c, nil) })
|
|
1020
|
-
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
1021
|
-
resp, _ := app.Test(req, -1)
|
|
1022
|
-
defer resp.Body.Close()
|
|
1023
|
-
|
|
1024
|
-
if resp.StatusCode != http.StatusInternalServerError {
|
|
1025
|
-
t.Fatalf("expected 500, got %d", resp.StatusCode)
|
|
1026
|
-
}
|
|
1027
|
-
body := readJSON(t, resp.Body)
|
|
1028
|
-
if body.Code != "INTERNAL_ERROR" {
|
|
1029
|
-
t.Fatalf("expected INTERNAL_ERROR, got %q", body.Code)
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
func TestTooManyRequests(t *testing.T) {
|
|
1034
|
-
app := makeApp(func(c *fiber.Ctx) error { return apierr.TooManyRequests(c, "slow down") })
|
|
1035
|
-
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
1036
|
-
resp, _ := app.Test(req, -1)
|
|
1037
|
-
defer resp.Body.Close()
|
|
1038
|
-
|
|
1039
|
-
if resp.StatusCode != http.StatusTooManyRequests {
|
|
1040
|
-
t.Fatalf("expected 429, got %d", resp.StatusCode)
|
|
1041
|
-
}
|
|
1042
|
-
body := readJSON(t, resp.Body)
|
|
1043
|
-
if body.Code != "TOO_MANY_REQUESTS" {
|
|
1044
|
-
t.Fatalf("expected TOO_MANY_REQUESTS, got %q", body.Code)
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
`}function fi(e){return `// Package docs provides the swaggo-generated OpenAPI specification.
|
|
1048
|
-
//
|
|
1049
|
-
// Run \`make docs\` to regenerate after changing handler annotations.
|
|
1050
|
-
//
|
|
1051
|
-
// @title ${bo(e.project_name)} API
|
|
1052
|
-
// @version ${e.app_version}
|
|
1053
|
-
// @description ${e.description}
|
|
1054
|
-
// @host localhost:${e.port}
|
|
1055
|
-
// @BasePath /
|
|
1056
|
-
// @schemes http https
|
|
1057
|
-
//
|
|
1058
|
-
// @contact.name ${e.author}
|
|
1059
|
-
// @license.name MIT
|
|
1060
|
-
package docs
|
|
1061
|
-
`}function hi(e){return `package handlers
|
|
1062
|
-
|
|
1063
|
-
import (
|
|
1064
|
-
"net/http"
|
|
1065
|
-
|
|
1066
|
-
"github.com/gofiber/fiber/v2"
|
|
1067
|
-
|
|
1068
|
-
"${e.module_path}/internal/apierr"
|
|
1069
|
-
)
|
|
1070
|
-
|
|
1071
|
-
// EchoResponse is the JSON body returned by EchoParams.
|
|
1072
|
-
type EchoResponse struct {
|
|
1073
|
-
Name string \`json:"name"\`
|
|
1074
|
-
RequestID string \`json:"request_id"\`
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
// EchoParams is an example handler demonstrating how to:
|
|
1078
|
-
// - read URL path parameters
|
|
1079
|
-
// - use apierr for consistent JSON error responses
|
|
1080
|
-
// - access the request ID injected by RequestID middleware
|
|
1081
|
-
//
|
|
1082
|
-
// Replace or remove this file once you add your own business logic.
|
|
1083
|
-
//
|
|
1084
|
-
// @Summary Echo path parameter
|
|
1085
|
-
// @Description Returns the :name path parameter together with the request ID.
|
|
1086
|
-
// @Tags example
|
|
1087
|
-
// @Produce json
|
|
1088
|
-
// @Param name path string true "Name to echo"
|
|
1089
|
-
// @Success 200 {object} handlers.EchoResponse
|
|
1090
|
-
// @Failure 400 {object} apierr.Response
|
|
1091
|
-
// @Router /api/v1/echo/{name} [get]
|
|
1092
|
-
func EchoParams(c *fiber.Ctx) error {
|
|
1093
|
-
name := c.Params("name")
|
|
1094
|
-
if name == "" {
|
|
1095
|
-
return apierr.BadRequest(c, "name parameter is required")
|
|
1096
|
-
}
|
|
1097
|
-
rid, _ := c.Locals("request_id").(string)
|
|
1098
|
-
return c.Status(http.StatusOK).JSON(EchoResponse{
|
|
1099
|
-
Name: name,
|
|
1100
|
-
RequestID: rid,
|
|
1101
|
-
})
|
|
1102
|
-
}
|
|
1103
|
-
`}function yi(e){return `package handlers_test
|
|
1104
|
-
|
|
1105
|
-
import (
|
|
1106
|
-
"encoding/json"
|
|
1107
|
-
"net/http"
|
|
1108
|
-
"net/http/httptest"
|
|
1109
|
-
"testing"
|
|
1110
|
-
|
|
1111
|
-
"github.com/gofiber/fiber/v2"
|
|
1112
|
-
|
|
1113
|
-
"${e.module_path}/internal/handlers"
|
|
1114
|
-
"${e.module_path}/internal/middleware"
|
|
1115
|
-
)
|
|
1116
|
-
|
|
1117
|
-
func newEchoApp() *fiber.App {
|
|
1118
|
-
app := fiber.New()
|
|
1119
|
-
app.Use(middleware.RequestID())
|
|
1120
|
-
app.Get("/echo/:name", handlers.EchoParams)
|
|
1121
|
-
return app
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
func TestEchoParams_Success(t *testing.T) {
|
|
1125
|
-
req := httptest.NewRequest(http.MethodGet, "/echo/alice", nil)
|
|
1126
|
-
resp, err := newEchoApp().Test(req, -1)
|
|
1127
|
-
if err != nil {
|
|
1128
|
-
t.Fatalf("request error: %v", err)
|
|
1129
|
-
}
|
|
1130
|
-
defer resp.Body.Close()
|
|
1131
|
-
|
|
1132
|
-
if resp.StatusCode != http.StatusOK {
|
|
1133
|
-
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
var body map[string]any
|
|
1137
|
-
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
1138
|
-
t.Fatalf("invalid JSON: %v", err)
|
|
1139
|
-
}
|
|
1140
|
-
if body["name"] != "alice" {
|
|
1141
|
-
t.Fatalf("expected name=alice, got %v", body["name"])
|
|
1142
|
-
}
|
|
1143
|
-
if body["request_id"] == nil || body["request_id"] == "" {
|
|
1144
|
-
t.Fatal("expected request_id to be set by RequestID middleware")
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
// TestEchoParams_EmptyName registers EchoParams on a param-free route so that
|
|
1149
|
-
// c.Params("name") returns "" and the 400 guard executes.
|
|
1150
|
-
func TestEchoParams_EmptyName(t *testing.T) {
|
|
1151
|
-
app := fiber.New()
|
|
1152
|
-
app.Get("/echo-bare", handlers.EchoParams)
|
|
1153
|
-
req := httptest.NewRequest(http.MethodGet, "/echo-bare", nil)
|
|
1154
|
-
resp, err := app.Test(req, -1)
|
|
1155
|
-
if err != nil {
|
|
1156
|
-
t.Fatalf("request error: %v", err)
|
|
1157
|
-
}
|
|
1158
|
-
defer resp.Body.Close()
|
|
1159
|
-
|
|
1160
|
-
if resp.StatusCode != http.StatusBadRequest {
|
|
1161
|
-
t.Fatalf("expected 400, got %d", resp.StatusCode)
|
|
1162
|
-
}
|
|
1163
|
-
var body map[string]any
|
|
1164
|
-
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
1165
|
-
t.Fatalf("invalid JSON: %v", err)
|
|
1166
|
-
}
|
|
1167
|
-
if body["code"] != "BAD_REQUEST" {
|
|
1168
|
-
t.Fatalf("expected code=BAD_REQUEST, got %v", body["code"])
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
`}function wi(e){return `package config_test
|
|
1172
|
-
|
|
1173
|
-
import (
|
|
1174
|
-
"log/slog"
|
|
1175
|
-
"testing"
|
|
1176
|
-
|
|
1177
|
-
"${e.module_path}/internal/config"
|
|
1178
|
-
)
|
|
1179
|
-
|
|
1180
|
-
func TestParseLogLevel(t *testing.T) {
|
|
1181
|
-
tests := []struct {
|
|
1182
|
-
input string
|
|
1183
|
-
want slog.Level
|
|
1184
|
-
}{
|
|
1185
|
-
{"debug", slog.LevelDebug},
|
|
1186
|
-
{"DEBUG", slog.LevelDebug},
|
|
1187
|
-
{"warn", slog.LevelWarn},
|
|
1188
|
-
{"warning", slog.LevelWarn},
|
|
1189
|
-
{"error", slog.LevelError},
|
|
1190
|
-
{"info", slog.LevelInfo},
|
|
1191
|
-
{"", slog.LevelInfo},
|
|
1192
|
-
{"unknown", slog.LevelInfo},
|
|
1193
|
-
}
|
|
1194
|
-
for _, tc := range tests {
|
|
1195
|
-
got := config.ParseLogLevel(tc.input)
|
|
1196
|
-
if got != tc.want {
|
|
1197
|
-
t.Errorf("ParseLogLevel(%q) = %v, want %v", tc.input, got, tc.want)
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
func TestLoad_EnvOverride(t *testing.T) {
|
|
1203
|
-
t.Setenv("PORT", "9090")
|
|
1204
|
-
t.Setenv("APP_ENV", "production")
|
|
1205
|
-
t.Setenv("LOG_LEVEL", "warn")
|
|
1206
|
-
|
|
1207
|
-
cfg := config.Load()
|
|
1208
|
-
|
|
1209
|
-
if cfg.Port != "9090" {
|
|
1210
|
-
t.Errorf("expected Port=9090, got %q", cfg.Port)
|
|
1211
|
-
}
|
|
1212
|
-
if cfg.Env != "production" {
|
|
1213
|
-
t.Errorf("expected Env=production, got %q", cfg.Env)
|
|
1214
|
-
}
|
|
1215
|
-
if cfg.LogLevel != "warn" {
|
|
1216
|
-
t.Errorf("expected LogLevel=warn, got %q", cfg.LogLevel)
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
func TestLoad_Defaults(t *testing.T) {
|
|
1221
|
-
// Empty string forces getEnv() to return the built-in fallback value.
|
|
1222
|
-
t.Setenv("PORT", "")
|
|
1223
|
-
t.Setenv("APP_ENV", "")
|
|
1224
|
-
t.Setenv("LOG_LEVEL", "")
|
|
1225
|
-
|
|
1226
|
-
cfg := config.Load()
|
|
1227
|
-
|
|
1228
|
-
if cfg.Port != "${e.port}" {
|
|
1229
|
-
t.Errorf("expected default Port=${e.port}, got %q", cfg.Port)
|
|
1230
|
-
}
|
|
1231
|
-
if cfg.Env != "development" {
|
|
1232
|
-
t.Errorf("expected default Env=development, got %q", cfg.Env)
|
|
1233
|
-
}
|
|
1234
|
-
// APP_ENV="" \u2192 fallback "development" \u2192 defaultLogLevel \u2192 "debug"
|
|
1235
|
-
if cfg.LogLevel != "debug" {
|
|
1236
|
-
t.Errorf("expected default LogLevel=debug (development env), got %q", cfg.LogLevel)
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
`}function vi(){return `package middleware
|
|
1240
|
-
|
|
1241
|
-
import (
|
|
1242
|
-
"os"
|
|
1243
|
-
|
|
1244
|
-
"github.com/gofiber/fiber/v2"
|
|
1245
|
-
"github.com/gofiber/fiber/v2/middleware/cors"
|
|
1246
|
-
)
|
|
1247
|
-
|
|
1248
|
-
// CORS returns a CORS middleware configured via CORS_ALLOW_ORIGINS env var.
|
|
1249
|
-
//
|
|
1250
|
-
// Set CORS_ALLOW_ORIGINS="*" for development (the default when unset).
|
|
1251
|
-
// In production supply a comma-separated list of allowed origins:
|
|
1252
|
-
//
|
|
1253
|
-
// CORS_ALLOW_ORIGINS=https://app.example.com,https://admin.example.com
|
|
1254
|
-
func CORS() fiber.Handler {
|
|
1255
|
-
origins := os.Getenv("CORS_ALLOW_ORIGINS")
|
|
1256
|
-
if origins == "" {
|
|
1257
|
-
origins = "*"
|
|
1258
|
-
}
|
|
1259
|
-
return cors.New(cors.Config{
|
|
1260
|
-
AllowOrigins: origins,
|
|
1261
|
-
AllowMethods: "GET,POST,PUT,PATCH,DELETE,OPTIONS",
|
|
1262
|
-
AllowHeaders: "Origin,Content-Type,Authorization,X-Request-ID",
|
|
1263
|
-
ExposeHeaders: "X-Request-ID",
|
|
1264
|
-
MaxAge: 600,
|
|
1265
|
-
})
|
|
1266
|
-
}
|
|
1267
|
-
`}function ki(e){return `package middleware_test
|
|
1268
|
-
|
|
1269
|
-
import (
|
|
1270
|
-
"net/http"
|
|
1271
|
-
"net/http/httptest"
|
|
1272
|
-
"testing"
|
|
1273
|
-
|
|
1274
|
-
"github.com/gofiber/fiber/v2"
|
|
1275
|
-
|
|
1276
|
-
"${e.module_path}/internal/middleware"
|
|
1277
|
-
)
|
|
1278
|
-
|
|
1279
|
-
func newCORSApp(t *testing.T) *fiber.App {
|
|
1280
|
-
t.Helper()
|
|
1281
|
-
app := fiber.New()
|
|
1282
|
-
app.Use(middleware.CORS())
|
|
1283
|
-
app.Get("/ping", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })
|
|
1284
|
-
return app
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
func TestCORS_Wildcard(t *testing.T) {
|
|
1288
|
-
t.Setenv("CORS_ALLOW_ORIGINS", "*")
|
|
1289
|
-
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
|
|
1290
|
-
req.Header.Set("Origin", "https://example.com")
|
|
1291
|
-
resp, _ := newCORSApp(t).Test(req, -1)
|
|
1292
|
-
defer resp.Body.Close()
|
|
1293
|
-
|
|
1294
|
-
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != "*" {
|
|
1295
|
-
t.Fatalf("expected ACAO=*, got %q", got)
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
func TestCORS_Preflight(t *testing.T) {
|
|
1300
|
-
t.Setenv("CORS_ALLOW_ORIGINS", "*")
|
|
1301
|
-
app := fiber.New()
|
|
1302
|
-
app.Use(middleware.CORS())
|
|
1303
|
-
|
|
1304
|
-
req := httptest.NewRequest(http.MethodOptions, "/ping", nil)
|
|
1305
|
-
req.Header.Set("Origin", "https://example.com")
|
|
1306
|
-
req.Header.Set("Access-Control-Request-Method", "POST")
|
|
1307
|
-
resp, _ := app.Test(req, -1)
|
|
1308
|
-
defer resp.Body.Close()
|
|
1309
|
-
|
|
1310
|
-
if resp.StatusCode != http.StatusNoContent {
|
|
1311
|
-
t.Fatalf("expected 204 preflight, got %d", resp.StatusCode)
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
func TestCORS_SpecificOrigin_Allowed(t *testing.T) {
|
|
1316
|
-
t.Setenv("CORS_ALLOW_ORIGINS", "https://app.example.com")
|
|
1317
|
-
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
|
|
1318
|
-
req.Header.Set("Origin", "https://app.example.com")
|
|
1319
|
-
resp, _ := newCORSApp(t).Test(req, -1)
|
|
1320
|
-
defer resp.Body.Close()
|
|
1321
|
-
|
|
1322
|
-
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != "https://app.example.com" {
|
|
1323
|
-
t.Fatalf("expected ACAO=https://app.example.com, got %q", got)
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
func TestCORS_Default_Origin(t *testing.T) {
|
|
1328
|
-
// When CORS_ALLOW_ORIGINS is unset, middleware must default to "*".
|
|
1329
|
-
t.Setenv("CORS_ALLOW_ORIGINS", "")
|
|
1330
|
-
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
|
|
1331
|
-
req.Header.Set("Origin", "https://anywhere.com")
|
|
1332
|
-
resp, _ := newCORSApp(t).Test(req, -1)
|
|
1333
|
-
defer resp.Body.Close()
|
|
1334
|
-
|
|
1335
|
-
if got := resp.Header.Get("Access-Control-Allow-Origin"); got == "" {
|
|
1336
|
-
t.Fatal("expected CORS header when origins defaulting to *")
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
`}function bi(e){return `package server_test
|
|
1340
|
-
|
|
1341
|
-
import (
|
|
1342
|
-
"encoding/json"
|
|
1343
|
-
"net/http"
|
|
1344
|
-
"net/http/httptest"
|
|
1345
|
-
"testing"
|
|
1346
|
-
|
|
1347
|
-
"${e.module_path}/internal/config"
|
|
1348
|
-
"${e.module_path}/internal/server"
|
|
1349
|
-
)
|
|
1350
|
-
|
|
1351
|
-
type serverAPIError struct {
|
|
1352
|
-
Code string \`json:"code"\`
|
|
1353
|
-
Message string \`json:"message"\`
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
func TestServer_NotFound_JSON(t *testing.T) {
|
|
1357
|
-
req := httptest.NewRequest(http.MethodGet, "/no-such-route", nil)
|
|
1358
|
-
resp, err := server.NewApp(config.Load()).Test(req, -1)
|
|
1359
|
-
if err != nil {
|
|
1360
|
-
t.Fatalf("request error: %v", err)
|
|
1361
|
-
}
|
|
1362
|
-
defer resp.Body.Close()
|
|
1363
|
-
|
|
1364
|
-
if resp.StatusCode != http.StatusNotFound {
|
|
1365
|
-
t.Fatalf("expected 404, got %d", resp.StatusCode)
|
|
1366
|
-
}
|
|
1367
|
-
var body serverAPIError
|
|
1368
|
-
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
1369
|
-
t.Fatalf("expected JSON error body: %v", err)
|
|
1370
|
-
}
|
|
1371
|
-
if body.Code != "NOT_FOUND" {
|
|
1372
|
-
t.Fatalf("expected code=NOT_FOUND, got %q", body.Code)
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
func TestServer_MethodNotAllowed_JSON(t *testing.T) {
|
|
1377
|
-
// Fiber v2 does not return 405 automatically \u2014 unmatched methods fall
|
|
1378
|
-
// through to the 404 catch-all, which is the expected behaviour.
|
|
1379
|
-
req := httptest.NewRequest(http.MethodPost, "/api/v1/health/live", nil)
|
|
1380
|
-
resp, err := server.NewApp(config.Load()).Test(req, -1)
|
|
1381
|
-
if err != nil {
|
|
1382
|
-
t.Fatalf("request error: %v", err)
|
|
1383
|
-
}
|
|
1384
|
-
defer resp.Body.Close()
|
|
1385
|
-
|
|
1386
|
-
if resp.StatusCode != http.StatusNotFound {
|
|
1387
|
-
t.Fatalf("expected 404 for unmatched method, got %d", resp.StatusCode)
|
|
1388
|
-
}
|
|
1389
|
-
var body serverAPIError
|
|
1390
|
-
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
1391
|
-
t.Fatalf("expected JSON error body: %v", err)
|
|
1392
|
-
}
|
|
1393
|
-
if body.Code != "NOT_FOUND" {
|
|
1394
|
-
t.Fatalf("expected code=NOT_FOUND, got %q", body.Code)
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
func TestServer_CORS_Header(t *testing.T) {
|
|
1399
|
-
t.Setenv("CORS_ALLOW_ORIGINS", "*")
|
|
1400
|
-
req := httptest.NewRequest(http.MethodGet, "/api/v1/health/live", nil)
|
|
1401
|
-
req.Header.Set("Origin", "https://example.com")
|
|
1402
|
-
resp, err := server.NewApp(config.Load()).Test(req, -1)
|
|
1403
|
-
if err != nil {
|
|
1404
|
-
t.Fatalf("request error: %v", err)
|
|
1405
|
-
}
|
|
1406
|
-
defer resp.Body.Close()
|
|
1407
|
-
|
|
1408
|
-
if got := resp.Header.Get("Access-Control-Allow-Origin"); got == "" {
|
|
1409
|
-
t.Fatal("expected Access-Control-Allow-Origin header to be set")
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
func TestServer_Docs_Redirect(t *testing.T) {
|
|
1414
|
-
req := httptest.NewRequest(http.MethodGet, "/docs", nil)
|
|
1415
|
-
resp, err := server.NewApp(config.Load()).Test(req, -1)
|
|
1416
|
-
if err != nil {
|
|
1417
|
-
t.Fatalf("request error: %v", err)
|
|
1418
|
-
}
|
|
1419
|
-
defer resp.Body.Close()
|
|
1420
|
-
|
|
1421
|
-
if resp.StatusCode != http.StatusFound {
|
|
1422
|
-
t.Fatalf("expected 302 redirect from /docs, got %d", resp.StatusCode)
|
|
1423
|
-
}
|
|
1424
|
-
if loc := resp.Header.Get("Location"); loc != "/docs/index.html" {
|
|
1425
|
-
t.Fatalf("expected Location=/docs/index.html, got %q", loc)
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
`}function Ri(e){return `package middleware
|
|
1429
|
-
|
|
1430
|
-
import (
|
|
1431
|
-
"os"
|
|
1432
|
-
"strconv"
|
|
1433
|
-
"time"
|
|
1434
|
-
|
|
1435
|
-
"github.com/gofiber/fiber/v2"
|
|
1436
|
-
"github.com/gofiber/fiber/v2/middleware/limiter"
|
|
1437
|
-
|
|
1438
|
-
"${e.module_path}/internal/apierr"
|
|
1439
|
-
)
|
|
1440
|
-
|
|
1441
|
-
// RateLimit returns a per-IP sliding-window rate limiter.
|
|
1442
|
-
// Configure the limit via RATE_LIMIT_RPS env var (requests per second, default 100).
|
|
1443
|
-
func RateLimit() fiber.Handler {
|
|
1444
|
-
rps := 100
|
|
1445
|
-
if raw := os.Getenv("RATE_LIMIT_RPS"); raw != "" {
|
|
1446
|
-
if n, err := strconv.Atoi(raw); err == nil && n > 0 {
|
|
1447
|
-
rps = n
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1450
|
-
return limiter.New(limiter.Config{
|
|
1451
|
-
Max: rps,
|
|
1452
|
-
Expiration: time.Second,
|
|
1453
|
-
KeyGenerator: func(c *fiber.Ctx) string {
|
|
1454
|
-
return c.IP()
|
|
1455
|
-
},
|
|
1456
|
-
LimitReached: func(c *fiber.Ctx) error {
|
|
1457
|
-
return apierr.TooManyRequests(c, "rate limit exceeded")
|
|
1458
|
-
},
|
|
1459
|
-
})
|
|
1460
|
-
}
|
|
1461
|
-
`}function _i(e){return `package middleware_test
|
|
1462
|
-
|
|
1463
|
-
import (
|
|
1464
|
-
"net/http"
|
|
1465
|
-
"net/http/httptest"
|
|
1466
|
-
"testing"
|
|
1467
|
-
|
|
1468
|
-
"github.com/gofiber/fiber/v2"
|
|
1469
|
-
|
|
1470
|
-
"${e.module_path}/internal/middleware"
|
|
1471
|
-
)
|
|
1472
|
-
|
|
1473
|
-
func newRateLimitApp(t *testing.T) *fiber.App {
|
|
1474
|
-
t.Helper()
|
|
1475
|
-
app := fiber.New()
|
|
1476
|
-
app.Use(middleware.RateLimit())
|
|
1477
|
-
app.Get("/", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })
|
|
1478
|
-
return app
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
func TestRateLimit_AllowsUnderLimit(t *testing.T) {
|
|
1482
|
-
t.Setenv("RATE_LIMIT_RPS", "3")
|
|
1483
|
-
app := newRateLimitApp(t)
|
|
1484
|
-
|
|
1485
|
-
for i := 0; i < 3; i++ {
|
|
1486
|
-
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
1487
|
-
resp, err := app.Test(req, -1)
|
|
1488
|
-
if err != nil {
|
|
1489
|
-
t.Fatalf("request %d: %v", i+1, err)
|
|
1490
|
-
}
|
|
1491
|
-
resp.Body.Close()
|
|
1492
|
-
if resp.StatusCode != http.StatusOK {
|
|
1493
|
-
t.Fatalf("request %d: expected 200, got %d", i+1, resp.StatusCode)
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
func TestRateLimit_Blocks_After_Limit(t *testing.T) {
|
|
1499
|
-
t.Setenv("RATE_LIMIT_RPS", "2")
|
|
1500
|
-
app := newRateLimitApp(t)
|
|
1501
|
-
|
|
1502
|
-
// Exhaust the limit.
|
|
1503
|
-
for i := 0; i < 2; i++ {
|
|
1504
|
-
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
1505
|
-
resp, _ := app.Test(req, -1)
|
|
1506
|
-
resp.Body.Close()
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
|
-
// Next request must be rejected.
|
|
1510
|
-
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
1511
|
-
resp, err := app.Test(req, -1)
|
|
1512
|
-
if err != nil {
|
|
1513
|
-
t.Fatalf("over-limit request: %v", err)
|
|
1514
|
-
}
|
|
1515
|
-
defer resp.Body.Close()
|
|
1516
|
-
|
|
1517
|
-
if resp.StatusCode != http.StatusTooManyRequests {
|
|
1518
|
-
t.Fatalf("expected 429, got %d", resp.StatusCode)
|
|
1519
|
-
}
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
func TestRateLimit_InvalidRPS(t *testing.T) {
|
|
1523
|
-
// Invalid value should fall back to default (100 rps) and allow normal requests.
|
|
1524
|
-
t.Setenv("RATE_LIMIT_RPS", "not-a-number")
|
|
1525
|
-
app := newRateLimitApp(t)
|
|
1526
|
-
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
1527
|
-
resp, err := app.Test(req, -1)
|
|
1528
|
-
if err != nil {
|
|
1529
|
-
t.Fatalf("request error: %v", err)
|
|
1530
|
-
}
|
|
1531
|
-
defer resp.Body.Close()
|
|
1532
|
-
|
|
1533
|
-
if resp.StatusCode != http.StatusOK {
|
|
1534
|
-
t.Fatalf("expected 200 with invalid RPS env, got %d", resp.StatusCode)
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1537
|
-
`}function Ci(e){return `# Air \u2014 live reload for Go projects
|
|
1538
|
-
# https://github.com/air-verse/air
|
|
1539
|
-
root = "."
|
|
1540
|
-
tmp_dir = "tmp"
|
|
1541
|
-
|
|
1542
|
-
[build]
|
|
1543
|
-
pre_cmd = ["$(go env GOPATH)/bin/swag init -g main.go -d cmd/server,internal/handlers,internal/apierr -o docs --parseDependency 2>/dev/null || true"]
|
|
1544
|
-
cmd = "go build -o ./tmp/server ./cmd/server"
|
|
1545
|
-
bin = "./tmp/server"
|
|
1546
|
-
include_ext = ["go", "yaml", "yml", "env"]
|
|
1547
|
-
exclude_dir = ["tmp", "vendor", ".git", "testdata", "docs"]
|
|
1548
|
-
delay = 500
|
|
1549
|
-
rerun_delay = 500
|
|
1550
|
-
send_interrupt = true
|
|
1551
|
-
kill_delay = "200ms"
|
|
1552
|
-
|
|
1553
|
-
[env]
|
|
1554
|
-
PORT = "${e.port}"
|
|
1555
|
-
|
|
1556
|
-
[misc]
|
|
1557
|
-
clean_on_exit = true
|
|
1558
|
-
|
|
1559
|
-
[log]
|
|
1560
|
-
time = false
|
|
1561
|
-
`}function Si(e){return `run:
|
|
1562
|
-
timeout: 5m
|
|
1563
|
-
|
|
1564
|
-
linters:
|
|
1565
|
-
enable:
|
|
1566
|
-
- bodyclose
|
|
1567
|
-
- durationcheck
|
|
1568
|
-
- errcheck
|
|
1569
|
-
- errname
|
|
1570
|
-
- errorlint
|
|
1571
|
-
- gci
|
|
1572
|
-
- goimports
|
|
1573
|
-
- gosimple
|
|
1574
|
-
- govet
|
|
1575
|
-
- ineffassign
|
|
1576
|
-
- misspell
|
|
1577
|
-
- noctx
|
|
1578
|
-
- nolintlint
|
|
1579
|
-
- prealloc
|
|
1580
|
-
- staticcheck
|
|
1581
|
-
- unconvert
|
|
1582
|
-
- unused
|
|
1583
|
-
- wrapcheck
|
|
1584
|
-
|
|
1585
|
-
linters-settings:
|
|
1586
|
-
gci:
|
|
1587
|
-
sections:
|
|
1588
|
-
- standard
|
|
1589
|
-
- default
|
|
1590
|
-
- prefix(${e})
|
|
1591
|
-
goimports:
|
|
1592
|
-
local-prefixes: "${e}"
|
|
1593
|
-
govet:
|
|
1594
|
-
enable:
|
|
1595
|
-
- shadow
|
|
1596
|
-
wrapcheck:
|
|
1597
|
-
ignorePackageGlobs:
|
|
1598
|
-
- "${e}/*"
|
|
1599
|
-
|
|
1600
|
-
issues:
|
|
1601
|
-
max-same-issues: 5
|
|
1602
|
-
exclude-rules:
|
|
1603
|
-
- path: _test.go
|
|
1604
|
-
linters:
|
|
1605
|
-
- errcheck
|
|
1606
|
-
- wrapcheck
|
|
1607
|
-
`}function xi(){return JSON.stringify({engine:"npm",runtime:"go"},null,2)}function Pi(e){return `#!/usr/bin/env sh
|
|
1608
|
-
# RapidKit Go/Fiber project launcher \u2014 generated by RapidKit CLI
|
|
1609
|
-
# https://getrapidkit.com
|
|
1610
|
-
|
|
1611
|
-
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
1612
|
-
CMD="\${1:-}"
|
|
1613
|
-
shift 2>/dev/null || true
|
|
1614
|
-
|
|
1615
|
-
case "$CMD" in
|
|
1616
|
-
init)
|
|
1617
|
-
cd "$SCRIPT_DIR"
|
|
1618
|
-
echo "\u{1F439} Initializing Go/Fiber project\u2026"
|
|
1619
|
-
GOBIN="$(go env GOPATH)/bin"
|
|
1620
|
-
echo " \u2192 installing air (hot reload)\u2026"
|
|
1621
|
-
go install github.com/air-verse/air@latest 2>/dev/null && echo " \u2713 air" || echo " \u26A0 air install failed (run: go install github.com/air-verse/air@latest)"
|
|
1622
|
-
echo " \u2192 installing swag (swagger)\u2026"
|
|
1623
|
-
go install github.com/swaggo/swag/cmd/swag@latest 2>/dev/null && echo " \u2713 swag" || echo " \u26A0 swag install failed (run: go install github.com/swaggo/swag/cmd/swag@latest)"
|
|
1624
|
-
if [ ! -f ".env" ] && [ -f ".env.example" ]; then
|
|
1625
|
-
cp .env.example .env && echo " \u2713 .env created from .env.example"
|
|
1626
|
-
fi
|
|
1627
|
-
go mod tidy && echo " \u2713 go mod tidy"
|
|
1628
|
-
echo " \u2192 generating swagger docs (first build)\u2026"
|
|
1629
|
-
"$(go env GOPATH)/bin/swag" init -g main.go -d cmd/server,internal/handlers,internal/apierr -o docs --parseDependency 2>/dev/null && echo " \u2713 swagger docs generated" || echo " \u26A0 swagger docs skipped (run: rapidkit docs)"
|
|
1630
|
-
echo "\u2705 Ready \u2014 run: rapidkit dev"
|
|
1631
|
-
;;
|
|
1632
|
-
dev)
|
|
1633
|
-
cd "$SCRIPT_DIR"
|
|
1634
|
-
echo "\u{1F4D6} Syncing swagger docs\u2026"
|
|
1635
|
-
"$(go env GOPATH)/bin/swag" init -g main.go -d cmd/server,internal/handlers,internal/apierr -o docs --parseDependency 2>/dev/null || true
|
|
1636
|
-
if [ -f "$SCRIPT_DIR/Makefile" ]; then
|
|
1637
|
-
exec make -C "$SCRIPT_DIR" dev "$@"
|
|
1638
|
-
else
|
|
1639
|
-
exec go run ./cmd/server "$@"
|
|
1640
|
-
fi
|
|
1641
|
-
;;
|
|
1642
|
-
start)
|
|
1643
|
-
BIN="$SCRIPT_DIR/bin/${e.project_name}"
|
|
1644
|
-
if [ ! -f "$BIN" ]; then
|
|
1645
|
-
make -C "$SCRIPT_DIR" build
|
|
1646
|
-
fi
|
|
1647
|
-
exec "$BIN" "$@"
|
|
1648
|
-
;;
|
|
1649
|
-
build)
|
|
1650
|
-
exec make -C "$SCRIPT_DIR" build "$@"
|
|
1651
|
-
;;
|
|
1652
|
-
test)
|
|
1653
|
-
exec make -C "$SCRIPT_DIR" test "$@"
|
|
1654
|
-
;;
|
|
1655
|
-
lint)
|
|
1656
|
-
exec make -C "$SCRIPT_DIR" lint "$@"
|
|
1657
|
-
;;
|
|
1658
|
-
format|fmt)
|
|
1659
|
-
exec make -C "$SCRIPT_DIR" fmt "$@"
|
|
1660
|
-
;;
|
|
1661
|
-
docs)
|
|
1662
|
-
exec make -C "$SCRIPT_DIR" docs "$@"
|
|
1663
|
-
;;
|
|
1664
|
-
help|--help|-h)
|
|
1665
|
-
echo "RapidKit \u2014 Go/Fiber project: ${e.project_name}"
|
|
1666
|
-
echo ""
|
|
1667
|
-
echo "Usage: rapidkit <command>"
|
|
1668
|
-
echo ""
|
|
1669
|
-
echo " init Install tools + create .env (air, swag, go mod tidy)"
|
|
1670
|
-
echo " dev Hot reload dev server (make dev \u2014 requires air)"
|
|
1671
|
-
echo " start Run compiled binary (make build + bin)"
|
|
1672
|
-
echo " build Build binary (make build)"
|
|
1673
|
-
echo " docs Generate Swagger docs (make docs \u2014 requires swag)"
|
|
1674
|
-
echo " test Run tests (make test)"
|
|
1675
|
-
echo " lint Run linter (make lint)"
|
|
1676
|
-
echo " format Format code (make fmt)"
|
|
1677
|
-
;;
|
|
1678
|
-
*)
|
|
1679
|
-
if [ -n "$CMD" ]; then
|
|
1680
|
-
echo "rapidkit: unknown command: $CMD" >&2
|
|
1681
|
-
fi
|
|
1682
|
-
echo "Available: init, dev, start, build, docs, test, lint, format" >&2
|
|
1683
|
-
exit 1
|
|
1684
|
-
;;
|
|
1685
|
-
esac
|
|
1686
|
-
`}function Ei(e){return `@echo off
|
|
1687
|
-
rem RapidKit Go/Fiber project launcher \u2014 Windows
|
|
1688
|
-
set CMD=%1
|
|
1689
|
-
if "%CMD%"=="" goto usage
|
|
1690
|
-
shift
|
|
1691
|
-
|
|
1692
|
-
if "%CMD%"=="init" (
|
|
1693
|
-
echo Initializing Go/Fiber project...
|
|
1694
|
-
go install github.com/air-verse/air@latest
|
|
1695
|
-
go install github.com/swaggo/swag/cmd/swag@latest
|
|
1696
|
-
if not exist .env if exist .env.example copy .env.example .env
|
|
1697
|
-
go mod tidy
|
|
1698
|
-
exit /b %ERRORLEVEL%
|
|
1699
|
-
)
|
|
1700
|
-
if "%CMD%"=="dev" ( make dev %* & exit /b %ERRORLEVEL% )
|
|
1701
|
-
if "%CMD%"=="build" ( make build %* & exit /b %ERRORLEVEL% )
|
|
1702
|
-
if "%CMD%"=="test" ( make test %* & exit /b %ERRORLEVEL% )
|
|
1703
|
-
if "%CMD%"=="lint" ( make lint %* & exit /b %ERRORLEVEL% )
|
|
1704
|
-
if "%CMD%"=="format" ( make fmt %* & exit /b %ERRORLEVEL% )
|
|
1705
|
-
if "%CMD%"=="docs" ( make docs %* & exit /b %ERRORLEVEL% )
|
|
1706
|
-
if "%CMD%"=="start" ( bin\\${e.project_name}.exe %* & exit /b %ERRORLEVEL% )
|
|
1707
|
-
|
|
1708
|
-
:usage
|
|
1709
|
-
echo Available: init, dev, start, build, docs, test, lint, format
|
|
1710
|
-
exit /b 1
|
|
1711
|
-
`}function Ii(e,o){return JSON.stringify({kit_name:"gofiber.standard",runtime:"go",module_support:false,project_name:e.project_name,module_path:e.module_path,app_version:e.app_version,created_by:"rapidkit-npm",rapidkit_version:o,created_at:new Date().toISOString()},null,2)}async function Vt(e,o){let t={project_name:o.project_name,module_path:o.module_path||o.project_name,author:o.author||"RapidKit User",description:o.description||`Go/Fiber REST API \u2014 ${o.project_name}`,go_version:o.go_version||"1.24",app_version:o.app_version||"0.1.0",port:o.port||"3000",skipGit:o.skipGit??false},r=c();try{await execa("go",["version"],{timeout:3e3});}catch{console.log(l.yellow("\n\u26A0 Go not found in PATH \u2014 project will be scaffolded, but `go mod tidy` requires Go 1.21+")),console.log(l.gray(` Install: https://go.dev/dl/
|
|
1712
|
-
`));}let i=Ut(`Generating Go/Fiber project: ${t.project_name}\u2026`).start();try{let n=(c,p)=>Qr(h.join(e,c),p),a=h.join(e,"rapidkit"),s=h.join(e,"rapidkit.cmd");await Promise.all([n("cmd/server/main.go",Xr(t)),n("go.mod",Zr(t)),n("internal/config/config.go",ei(t)),n("internal/server/server.go",ti(t)),n("internal/middleware/requestid.go",pi()),n("internal/middleware/requestid_test.go",ui(t)),n("internal/apierr/apierr.go",gi()),n("internal/apierr/apierr_test.go",mi(t)),n("internal/handlers/health.go",oi()),n("internal/handlers/health_test.go",ri(t)),n("internal/handlers/example.go",hi(t)),n("internal/handlers/example_test.go",yi(t)),n("internal/config/config_test.go",wi(t)),n("internal/middleware/cors.go",vi()),n("internal/middleware/cors_test.go",ki(t)),n("internal/middleware/ratelimit.go",Ri(t)),n("internal/middleware/ratelimit_test.go",_i(t)),n("internal/server/server_test.go",bi(t)),n("docs/doc.go",fi(t)),n(".air.toml",Ci(t)),n("Dockerfile",ii()),n("docker-compose.yml",ni(t)),n("Makefile",si(t)),n(".golangci.yml",Si(t.module_path)),n(".env.example",ai(t)),n(".gitignore",ci()),n(".github/workflows/ci.yml",li(t)),n("README.md",di(t)),n(".rapidkit/project.json",Ii(t,r)),n(".rapidkit/context.json",xi()),n("rapidkit",Pi(t)),n("rapidkit.cmd",Ei(t))]),await promises.chmod(a,493),await promises.chmod(s,493),i.succeed(l.green(`Project created at ${e}`));try{i.start("Fetching Go dependencies\u2026"),await execa("go",["mod","tidy"],{cwd:e,timeout:12e4}),i.succeed(l.gray("\u2713 go mod tidy completed"));}catch{i.warn(l.yellow("\u26A0 go mod tidy failed \u2014 run manually: go mod tidy"));}if(!t.skipGit)try{await execa("git",["init"],{cwd:e}),await execa("git",["add","-A"],{cwd:e}),await execa("git",["commit","-m","chore: initial scaffold (rapidkit gofiber.standard)"],{cwd:e}),console.log(l.gray("\u2713 git repository initialized"));}catch{console.log(l.gray("\u26A0 git init skipped (git not found or error)"));}console.log(""),console.log(l.bold("\u2705 Go/Fiber project ready!")),console.log(""),console.log(l.cyan("Next steps:")),console.log(l.white(` cd ${t.project_name}`)),console.log(l.white(" make run # start dev server")),console.log(l.white(" make test # run tests")),console.log(""),console.log(l.gray("Server will listen on port "+t.port)),console.log(l.gray(" http://localhost:"+t.port+"/api/v1/health/live")),console.log(l.gray(" http://localhost:"+t.port+"/api/v1/health/ready")),console.log(""),console.log(l.yellow("\u2139 RapidKit modules are not available for Go projects (module system uses Python/pip).")),console.log("");}catch(n){throw i.fail(l.red("Failed to generate Go/Fiber project")),n}}function Ro(e){return e.split(/[-_\s]+/).map(o=>o.charAt(0).toUpperCase()+o.slice(1)).join("")}async function Ai(e,o){await promises.mkdir(h.dirname(e),{recursive:true}),await promises.writeFile(e,o,"utf8");}function Oi(e){return `package main
|
|
1713
|
-
|
|
1714
|
-
import (
|
|
1715
|
-
"context"
|
|
1716
|
-
"errors"
|
|
1717
|
-
"fmt"
|
|
1718
|
-
"log/slog"
|
|
1719
|
-
"net/http"
|
|
1720
|
-
"os"
|
|
1721
|
-
"os/signal"
|
|
1722
|
-
"syscall"
|
|
1723
|
-
"time"
|
|
1724
|
-
|
|
1725
|
-
_ "${e.module_path}/docs"
|
|
1726
|
-
"${e.module_path}/internal/config"
|
|
1727
|
-
"${e.module_path}/internal/server"
|
|
1728
|
-
)
|
|
1729
|
-
|
|
1730
|
-
// Build-time variables \u2014 injected via -ldflags.
|
|
1731
|
-
var (
|
|
1732
|
-
version = "dev"
|
|
1733
|
-
commit = "none"
|
|
1734
|
-
date = "unknown"
|
|
1735
|
-
)
|
|
1736
|
-
|
|
1737
|
-
func main() {
|
|
1738
|
-
cfg := config.Load()
|
|
1739
|
-
|
|
1740
|
-
log := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
|
1741
|
-
Level: config.ParseLogLevel(cfg.LogLevel),
|
|
1742
|
-
}))
|
|
1743
|
-
slog.SetDefault(log)
|
|
1744
|
-
|
|
1745
|
-
r := server.NewRouter(cfg)
|
|
1746
|
-
|
|
1747
|
-
srv := &http.Server{
|
|
1748
|
-
Addr: ":" + cfg.Port,
|
|
1749
|
-
Handler: r,
|
|
1750
|
-
ReadTimeout: 5 * time.Second,
|
|
1751
|
-
WriteTimeout: 10 * time.Second,
|
|
1752
|
-
IdleTimeout: 30 * time.Second,
|
|
1753
|
-
}
|
|
1754
|
-
|
|
1755
|
-
go func() {
|
|
1756
|
-
slog.Info("starting", "port", cfg.Port, "version", version, "commit", commit, "date", date, "env", cfg.Env)
|
|
1757
|
-
fmt.Printf("\\n\u{1F680} Server \u2192 http://127.0.0.1:%s\\n", cfg.Port)
|
|
1758
|
-
fmt.Printf("\u{1F4D6} Docs \u2192 http://127.0.0.1:%s/docs\\n\\n", cfg.Port)
|
|
1759
|
-
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
1760
|
-
slog.Error("server error", "err", err)
|
|
1761
|
-
os.Exit(1)
|
|
1762
|
-
}
|
|
1763
|
-
}()
|
|
1764
|
-
|
|
1765
|
-
quit := make(chan os.Signal, 1)
|
|
1766
|
-
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
|
1767
|
-
<-quit
|
|
1768
|
-
|
|
1769
|
-
slog.Info("shutting down\u2026")
|
|
1770
|
-
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
1771
|
-
defer cancel()
|
|
1772
|
-
|
|
1773
|
-
if err := srv.Shutdown(ctx); err != nil {
|
|
1774
|
-
slog.Error("forced shutdown", "err", err)
|
|
1775
|
-
os.Exit(1)
|
|
1776
|
-
}
|
|
1777
|
-
slog.Info("server stopped")
|
|
1778
|
-
}
|
|
1779
|
-
`}function Ni(e){return `module ${e.module_path}
|
|
1780
|
-
|
|
1781
|
-
go ${e.go_version}
|
|
1782
|
-
|
|
1783
|
-
require (
|
|
1784
|
-
github.com/gin-gonic/gin v1.10.0
|
|
1785
|
-
github.com/swaggo/gin-swagger v1.6.0
|
|
1786
|
-
github.com/swaggo/swag v1.16.3
|
|
1787
|
-
)
|
|
1788
|
-
|
|
1789
|
-
require (
|
|
1790
|
-
github.com/KyleBanks/depth v1.2.1 // indirect
|
|
1791
|
-
github.com/bytedance/sonic v1.11.6 // indirect
|
|
1792
|
-
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
|
1793
|
-
github.com/cloudwego/base64x v0.1.4 // indirect
|
|
1794
|
-
github.com/cloudwego/iasm v0.2.0 // indirect
|
|
1795
|
-
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
|
1796
|
-
github.com/ghodss/yaml v1.0.0 // indirect
|
|
1797
|
-
github.com/gin-contrib/sse v0.1.0 // indirect
|
|
1798
|
-
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
|
1799
|
-
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
|
1800
|
-
github.com/go-openapi/spec v0.21.0 // indirect
|
|
1801
|
-
github.com/go-openapi/swag v0.23.0 // indirect
|
|
1802
|
-
github.com/go-playground/locales v0.14.1 // indirect
|
|
1803
|
-
github.com/go-playground/universal-translator v0.18.1 // indirect
|
|
1804
|
-
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
|
1805
|
-
github.com/goccy/go-json v0.10.2 // indirect
|
|
1806
|
-
github.com/josharian/intern v1.0.0 // indirect
|
|
1807
|
-
github.com/json-iterator/go v1.1.12 // indirect
|
|
1808
|
-
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
|
1809
|
-
github.com/leodido/go-urn v1.4.0 // indirect
|
|
1810
|
-
github.com/mailru/easyjson v0.7.7 // indirect
|
|
1811
|
-
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
1812
|
-
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
|
1813
|
-
github.com/modern-go/reflect2 v1.0.2 // indirect
|
|
1814
|
-
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
|
1815
|
-
github.com/swaggo/files v1.0.1 // indirect
|
|
1816
|
-
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
|
1817
|
-
github.com/ugorji/go/codec v1.2.12 // indirect
|
|
1818
|
-
golang.org/x/arch v0.8.0 // indirect
|
|
1819
|
-
golang.org/x/crypto v0.23.0 // indirect
|
|
1820
|
-
golang.org/x/net v0.25.0 // indirect
|
|
1821
|
-
golang.org/x/sys v0.20.0 // indirect
|
|
1822
|
-
golang.org/x/text v0.15.0 // indirect
|
|
1823
|
-
golang.org/x/tools v0.21.0 // indirect
|
|
1824
|
-
google.golang.org/protobuf v1.34.1 // indirect
|
|
1825
|
-
gopkg.in/yaml.v2 v2.4.0 // indirect
|
|
1826
|
-
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
1827
|
-
)
|
|
1828
|
-
`}function ji(e){return `package config
|
|
1829
|
-
|
|
1830
|
-
import (
|
|
1831
|
-
"log/slog"
|
|
1832
|
-
"os"
|
|
1833
|
-
"strings"
|
|
1834
|
-
)
|
|
1835
|
-
|
|
1836
|
-
// Config holds application configuration loaded from environment variables.
|
|
1837
|
-
type Config struct {
|
|
1838
|
-
Port string
|
|
1839
|
-
Env string
|
|
1840
|
-
GinMode string
|
|
1841
|
-
LogLevel string
|
|
1842
|
-
}
|
|
1843
|
-
|
|
1844
|
-
// Load reads configuration from environment variables with sensible defaults.
|
|
1845
|
-
func Load() *Config {
|
|
1846
|
-
env := getEnv("APP_ENV", "development")
|
|
1847
|
-
return &Config{
|
|
1848
|
-
Port: getEnv("PORT", "${e.port}"),
|
|
1849
|
-
Env: env,
|
|
1850
|
-
GinMode: getEnv("GIN_MODE", "debug"),
|
|
1851
|
-
LogLevel: getEnv("LOG_LEVEL", defaultLogLevel(env)),
|
|
1852
|
-
}
|
|
1853
|
-
}
|
|
1854
|
-
|
|
1855
|
-
// ParseLogLevel maps a level string to the corresponding slog.Level.
|
|
1856
|
-
// Falls back to Info for unrecognised values.
|
|
1857
|
-
func ParseLogLevel(s string) slog.Level {
|
|
1858
|
-
switch strings.ToLower(s) {
|
|
1859
|
-
case "debug":
|
|
1860
|
-
return slog.LevelDebug
|
|
1861
|
-
case "warn", "warning":
|
|
1862
|
-
return slog.LevelWarn
|
|
1863
|
-
case "error":
|
|
1864
|
-
return slog.LevelError
|
|
1865
|
-
default:
|
|
1866
|
-
return slog.LevelInfo
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
1869
|
-
|
|
1870
|
-
func defaultLogLevel(env string) string {
|
|
1871
|
-
if env == "development" {
|
|
1872
|
-
return "debug"
|
|
1873
|
-
}
|
|
1874
|
-
return "info"
|
|
1875
|
-
}
|
|
1876
|
-
|
|
1877
|
-
func getEnv(key, fallback string) string {
|
|
1878
|
-
if v, ok := os.LookupEnv(key); ok && v != "" {
|
|
1879
|
-
return v
|
|
1880
|
-
}
|
|
1881
|
-
return fallback
|
|
1882
|
-
}
|
|
1883
|
-
`}function $i(e){return `package server
|
|
1884
|
-
|
|
1885
|
-
import (
|
|
1886
|
-
"net/http"
|
|
1887
|
-
|
|
1888
|
-
"github.com/gin-gonic/gin"
|
|
1889
|
-
ginSwagger "github.com/swaggo/gin-swagger"
|
|
1890
|
-
swaggerFiles "github.com/swaggo/files"
|
|
1891
|
-
|
|
1892
|
-
"${e.module_path}/internal/apierr"
|
|
1893
|
-
"${e.module_path}/internal/config"
|
|
1894
|
-
"${e.module_path}/internal/handlers"
|
|
1895
|
-
"${e.module_path}/internal/middleware"
|
|
1896
|
-
)
|
|
1897
|
-
|
|
1898
|
-
// NewRouter assembles the Gin engine with all middleware and routes.
|
|
1899
|
-
// Call this from main \u2014 or from tests via server.NewRouter(cfg).
|
|
1900
|
-
func NewRouter(cfg *config.Config) *gin.Engine {
|
|
1901
|
-
if cfg.GinMode == "release" {
|
|
1902
|
-
gin.SetMode(gin.ReleaseMode)
|
|
1903
|
-
}
|
|
1904
|
-
|
|
1905
|
-
r := gin.New()
|
|
1906
|
-
r.Use(gin.Recovery())
|
|
1907
|
-
r.Use(middleware.CORS())
|
|
1908
|
-
r.Use(middleware.RequestID())
|
|
1909
|
-
r.Use(middleware.RateLimit())
|
|
1910
|
-
r.Use(middleware.Logger())
|
|
1911
|
-
|
|
1912
|
-
// Swagger UI \u2014 /docs redirects to /docs/index.html
|
|
1913
|
-
r.GET("/docs", func(c *gin.Context) { c.Redirect(http.StatusFound, "/docs/index.html") })
|
|
1914
|
-
r.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
|
1915
|
-
|
|
1916
|
-
v1 := r.Group("/api/v1")
|
|
1917
|
-
{
|
|
1918
|
-
v1.GET("/health/live", handlers.Liveness)
|
|
1919
|
-
v1.GET("/health/ready", handlers.Readiness)
|
|
1920
|
-
v1.GET("/echo/:name", handlers.EchoParams)
|
|
1921
|
-
}
|
|
1922
|
-
|
|
1923
|
-
// Return JSON for unknown routes/methods instead of Gin's default text responses.
|
|
1924
|
-
// HandleMethodNotAllowed must be true so NoMethod handler fires for 405 cases.
|
|
1925
|
-
r.HandleMethodNotAllowed = true
|
|
1926
|
-
r.NoRoute(func(c *gin.Context) {
|
|
1927
|
-
apierr.NotFound(c, "route not found")
|
|
1928
|
-
})
|
|
1929
|
-
r.NoMethod(func(c *gin.Context) {
|
|
1930
|
-
apierr.MethodNotAllowed(c)
|
|
1931
|
-
})
|
|
1932
|
-
|
|
1933
|
-
return r
|
|
1934
|
-
}
|
|
1935
|
-
`}function Di(){return `package handlers
|
|
1936
|
-
|
|
1937
|
-
import (
|
|
1938
|
-
"net/http"
|
|
1939
|
-
"time"
|
|
1940
|
-
|
|
1941
|
-
"github.com/gin-gonic/gin"
|
|
1942
|
-
)
|
|
1943
|
-
|
|
1944
|
-
// Liveness signals the process is alive (Kubernetes livenessProbe).
|
|
1945
|
-
//
|
|
1946
|
-
// @Summary Liveness probe
|
|
1947
|
-
// @Description Returns 200 when the process is alive.
|
|
1948
|
-
// @Tags health
|
|
1949
|
-
// @Produce json
|
|
1950
|
-
// @Success 200 {object} map[string]string
|
|
1951
|
-
// @Router /api/v1/health/live [get]
|
|
1952
|
-
func Liveness(c *gin.Context) {
|
|
1953
|
-
c.JSON(http.StatusOK, gin.H{
|
|
1954
|
-
"status": "ok",
|
|
1955
|
-
"time": time.Now().UTC().Format(time.RFC3339),
|
|
1956
|
-
})
|
|
1957
|
-
}
|
|
1958
|
-
|
|
1959
|
-
// Readiness signals the service can accept traffic (Kubernetes readinessProbe).
|
|
1960
|
-
// Extend this function to check database connectivity, caches, etc.
|
|
1961
|
-
//
|
|
1962
|
-
// @Summary Readiness probe
|
|
1963
|
-
// @Description Returns 200 when the service is ready to accept traffic.
|
|
1964
|
-
// @Tags health
|
|
1965
|
-
// @Produce json
|
|
1966
|
-
// @Success 200 {object} map[string]string
|
|
1967
|
-
// @Router /api/v1/health/ready [get]
|
|
1968
|
-
func Readiness(c *gin.Context) {
|
|
1969
|
-
c.JSON(http.StatusOK, gin.H{
|
|
1970
|
-
"status": "ready",
|
|
1971
|
-
"time": time.Now().UTC().Format(time.RFC3339),
|
|
1972
|
-
})
|
|
1973
|
-
}
|
|
1974
|
-
`}function Gi(e){return `package handlers_test
|
|
1975
|
-
|
|
1976
|
-
import (
|
|
1977
|
-
"encoding/json"
|
|
1978
|
-
"net/http"
|
|
1979
|
-
"net/http/httptest"
|
|
1980
|
-
"testing"
|
|
1981
|
-
|
|
1982
|
-
"github.com/gin-gonic/gin"
|
|
1983
|
-
|
|
1984
|
-
"${e.module_path}/internal/config"
|
|
1985
|
-
"${e.module_path}/internal/server"
|
|
1986
|
-
)
|
|
1987
|
-
|
|
1988
|
-
func init() { gin.SetMode(gin.TestMode) }
|
|
1989
|
-
|
|
1990
|
-
func newRouter() *gin.Engine { return server.NewRouter(config.Load()) }
|
|
1991
|
-
|
|
1992
|
-
func TestLiveness(t *testing.T) {
|
|
1993
|
-
w := httptest.NewRecorder()
|
|
1994
|
-
req, _ := http.NewRequest(http.MethodGet, "/api/v1/health/live", nil)
|
|
1995
|
-
newRouter().ServeHTTP(w, req)
|
|
1996
|
-
|
|
1997
|
-
if w.Code != http.StatusOK {
|
|
1998
|
-
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
1999
|
-
}
|
|
2000
|
-
|
|
2001
|
-
var body map[string]any
|
|
2002
|
-
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
|
|
2003
|
-
t.Fatalf("invalid JSON: %v", err)
|
|
2004
|
-
}
|
|
2005
|
-
if body["status"] != "ok" {
|
|
2006
|
-
t.Fatalf("expected ok, got %v", body["status"])
|
|
2007
|
-
}
|
|
2008
|
-
}
|
|
2009
|
-
|
|
2010
|
-
func TestReadiness(t *testing.T) {
|
|
2011
|
-
w := httptest.NewRecorder()
|
|
2012
|
-
req, _ := http.NewRequest(http.MethodGet, "/api/v1/health/ready", nil)
|
|
2013
|
-
newRouter().ServeHTTP(w, req)
|
|
2014
|
-
|
|
2015
|
-
if w.Code != http.StatusOK {
|
|
2016
|
-
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
2017
|
-
}
|
|
2018
|
-
}
|
|
2019
|
-
`}function Mi(){return `# \u2500\u2500 Build stage \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2020
|
-
FROM golang:1.24-alpine AS builder
|
|
2021
|
-
|
|
2022
|
-
# Build-time version injection
|
|
2023
|
-
ARG VERSION=dev
|
|
2024
|
-
ARG COMMIT=none
|
|
2025
|
-
ARG DATE=unknown
|
|
2026
|
-
|
|
2027
|
-
WORKDIR /app
|
|
2028
|
-
COPY go.mod go.sum ./
|
|
2029
|
-
RUN go mod download
|
|
2030
|
-
|
|
2031
|
-
COPY . .
|
|
2032
|
-
RUN CGO_ENABLED=0 GOOS=linux go build \\
|
|
2033
|
-
-ldflags="-s -w -X main.version=$\${VERSION} -X main.commit=$\${COMMIT} -X main.date=$\${DATE}" \\
|
|
2034
|
-
-o server ./cmd/server
|
|
2035
|
-
|
|
2036
|
-
# \u2500\u2500 Runtime stage \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2037
|
-
# alpine includes busybox wget required for the HEALTHCHECK below.
|
|
2038
|
-
FROM alpine:3.21
|
|
2039
|
-
|
|
2040
|
-
RUN addgroup -S app && adduser -S -G app app
|
|
2041
|
-
COPY --from=builder /app/server /server
|
|
2042
|
-
USER app
|
|
2043
|
-
|
|
2044
|
-
EXPOSE 8080
|
|
2045
|
-
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
|
|
2046
|
-
CMD wget -qO- http://localhost:8080/api/v1/health/live || exit 1
|
|
2047
|
-
ENTRYPOINT ["/server"]
|
|
2048
|
-
`}function Li(e){return `version: "3.9"
|
|
2049
|
-
|
|
2050
|
-
services:
|
|
2051
|
-
api:
|
|
2052
|
-
build: .
|
|
2053
|
-
container_name: ${e.project_name}
|
|
2054
|
-
ports:
|
|
2055
|
-
- "${e.port}:${e.port}"
|
|
2056
|
-
environment:
|
|
2057
|
-
PORT: "${e.port}"
|
|
2058
|
-
APP_ENV: development
|
|
2059
|
-
GIN_MODE: debug
|
|
2060
|
-
LOG_LEVEL: info
|
|
2061
|
-
CORS_ALLOW_ORIGINS: "*"
|
|
2062
|
-
RATE_LIMIT_RPS: "100"
|
|
2063
|
-
restart: unless-stopped
|
|
2064
|
-
`}function qi(e){return `.PHONY: dev run build test cover lint fmt tidy docs docker-up docker-down
|
|
2065
|
-
|
|
2066
|
-
# Build-time metadata
|
|
2067
|
-
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
|
2068
|
-
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo "none")
|
|
2069
|
-
DATE ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
2070
|
-
LDFLAGS = -ldflags "-s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(DATE)"
|
|
2071
|
-
# Go tool binaries are installed to GOPATH/bin; include it so \`air\` and \`swag\` are found.
|
|
2072
|
-
GOBIN ?= $(shell go env GOPATH)/bin
|
|
2073
|
-
|
|
2074
|
-
# Hot reload \u2014 installs air on first use
|
|
2075
|
-
dev:
|
|
2076
|
-
@test -x "$(GOBIN)/air" || go install github.com/air-verse/air@latest
|
|
2077
|
-
GIN_MODE=debug $(GOBIN)/air
|
|
2078
|
-
|
|
2079
|
-
run:
|
|
2080
|
-
GIN_MODE=debug go run $(LDFLAGS) ./cmd/server
|
|
2081
|
-
|
|
2082
|
-
build:
|
|
2083
|
-
go build $(LDFLAGS) -o bin/${e.project_name} ./cmd/server
|
|
2084
|
-
|
|
2085
|
-
test:
|
|
2086
|
-
GIN_MODE=test go test ./... -v -race
|
|
2087
|
-
|
|
2088
|
-
cover:
|
|
2089
|
-
GIN_MODE=test go test ./... -race -coverprofile=coverage.out
|
|
2090
|
-
go tool cover -html=coverage.out -o coverage.html
|
|
2091
|
-
@echo "Coverage report: coverage.html"
|
|
2092
|
-
|
|
2093
|
-
# Generate Swagger docs \u2014 installs swag on first use
|
|
2094
|
-
docs:
|
|
2095
|
-
@test -x "$(GOBIN)/swag" || go install github.com/swaggo/swag/cmd/swag@latest
|
|
2096
|
-
$(GOBIN)/swag init -g main.go -d cmd/server,internal/handlers,internal/apierr -o docs --parseDependency
|
|
2097
|
-
|
|
2098
|
-
lint:
|
|
2099
|
-
@command -v golangci-lint >/dev/null 2>&1 || (echo "golangci-lint not found. Install: https://golangci-lint.run/usage/install/" && exit 1)
|
|
2100
|
-
golangci-lint run ./...
|
|
2101
|
-
|
|
2102
|
-
fmt:
|
|
2103
|
-
gofmt -w .
|
|
2104
|
-
|
|
2105
|
-
tidy:
|
|
2106
|
-
go mod tidy
|
|
2107
|
-
|
|
2108
|
-
docker-up:
|
|
2109
|
-
go mod tidy
|
|
2110
|
-
docker compose up --build \\
|
|
2111
|
-
--build-arg VERSION=$(VERSION) \\
|
|
2112
|
-
--build-arg COMMIT=$(COMMIT) \\
|
|
2113
|
-
--build-arg DATE=$(DATE) \\
|
|
2114
|
-
-d
|
|
2115
|
-
|
|
2116
|
-
docker-down:
|
|
2117
|
-
docker compose down
|
|
2118
|
-
`}function Fi(e){return `# Application
|
|
2119
|
-
PORT=${e.port}
|
|
2120
|
-
APP_ENV=development
|
|
2121
|
-
GIN_MODE=debug
|
|
2122
|
-
LOG_LEVEL=debug
|
|
2123
|
-
|
|
2124
|
-
# CORS \u2014 comma-separated list of allowed origins, or * to allow all
|
|
2125
|
-
CORS_ALLOW_ORIGINS=*
|
|
2126
|
-
|
|
2127
|
-
# Rate limiting \u2014 max requests per IP per second
|
|
2128
|
-
RATE_LIMIT_RPS=100
|
|
2129
|
-
`}function Hi(){return `# Binaries
|
|
2130
|
-
bin/
|
|
2131
|
-
*.exe
|
|
2132
|
-
*.exe~
|
|
2133
|
-
*.dll
|
|
2134
|
-
*.so
|
|
2135
|
-
*.dylib
|
|
2136
|
-
|
|
2137
|
-
# Test binary
|
|
2138
|
-
*.test
|
|
2139
|
-
|
|
2140
|
-
# Output of go coverage tool
|
|
2141
|
-
*.out
|
|
2142
|
-
coverage.html
|
|
2143
|
-
|
|
2144
|
-
# Go workspace
|
|
2145
|
-
go.work
|
|
2146
|
-
go.work.sum
|
|
2147
|
-
|
|
2148
|
-
# Environment
|
|
2149
|
-
.env
|
|
2150
|
-
.env.local
|
|
2151
|
-
|
|
2152
|
-
# Hot reload (air)
|
|
2153
|
-
tmp/
|
|
2154
|
-
|
|
2155
|
-
# Swagger \u2014 generated files (committed stub docs/doc.go; run \`make docs\` to regenerate)
|
|
2156
|
-
docs/swagger.json
|
|
2157
|
-
docs/swagger.yaml
|
|
2158
|
-
docs/docs.go
|
|
2159
|
-
|
|
2160
|
-
# Editor
|
|
2161
|
-
.idea/
|
|
2162
|
-
.vscode/
|
|
2163
|
-
*.swp
|
|
2164
|
-
*.swo
|
|
2165
|
-
|
|
2166
|
-
# OS
|
|
2167
|
-
.DS_Store
|
|
2168
|
-
Thumbs.db
|
|
2169
|
-
`}function Ki(e){return `name: CI
|
|
2170
|
-
|
|
2171
|
-
on:
|
|
2172
|
-
push:
|
|
2173
|
-
branches: [main, develop]
|
|
2174
|
-
pull_request:
|
|
2175
|
-
branches: [main]
|
|
2176
|
-
|
|
2177
|
-
jobs:
|
|
2178
|
-
test:
|
|
2179
|
-
name: Test
|
|
2180
|
-
runs-on: ubuntu-latest
|
|
2181
|
-
|
|
2182
|
-
steps:
|
|
2183
|
-
- uses: actions/checkout@v4
|
|
2184
|
-
|
|
2185
|
-
- name: Set up Go
|
|
2186
|
-
uses: actions/setup-go@v5
|
|
2187
|
-
with:
|
|
2188
|
-
go-version: "${e.go_version}"
|
|
2189
|
-
cache: true
|
|
2190
|
-
|
|
2191
|
-
- name: Tidy
|
|
2192
|
-
run: go mod tidy
|
|
2193
|
-
|
|
2194
|
-
- name: Build
|
|
2195
|
-
run: go build ./...
|
|
2196
|
-
|
|
2197
|
-
- name: Test
|
|
2198
|
-
run: GIN_MODE=test go test ./... -race -coverprofile=coverage.out
|
|
2199
|
-
|
|
2200
|
-
- name: Upload coverage
|
|
2201
|
-
uses: actions/upload-artifact@v4
|
|
2202
|
-
with:
|
|
2203
|
-
name: coverage
|
|
2204
|
-
path: coverage.out
|
|
2205
|
-
|
|
2206
|
-
lint:
|
|
2207
|
-
name: Lint
|
|
2208
|
-
runs-on: ubuntu-latest
|
|
2209
|
-
|
|
2210
|
-
steps:
|
|
2211
|
-
- uses: actions/checkout@v4
|
|
2212
|
-
|
|
2213
|
-
- name: Set up Go
|
|
2214
|
-
uses: actions/setup-go@v5
|
|
2215
|
-
with:
|
|
2216
|
-
go-version: "${e.go_version}"
|
|
2217
|
-
cache: true
|
|
2218
|
-
|
|
2219
|
-
- name: golangci-lint
|
|
2220
|
-
uses: golangci/golangci-lint-action@v6
|
|
2221
|
-
with:
|
|
2222
|
-
version: latest
|
|
2223
|
-
`}function Wi(e){return `# ${Ro(e.project_name)}
|
|
2224
|
-
|
|
2225
|
-
> ${e.description}
|
|
2226
|
-
|
|
2227
|
-
Built with [Go](https://go.dev/) + [Gin](https://gin-gonic.com/) \xB7 Scaffolded by [RapidKit](https://getrapidkit.com)
|
|
2228
|
-
|
|
2229
|
-
## Quick start
|
|
2230
|
-
|
|
2231
|
-
\`\`\`bash
|
|
2232
|
-
# Run locally (hot reload)
|
|
2233
|
-
make dev
|
|
2234
|
-
|
|
2235
|
-
# Run tests
|
|
2236
|
-
make test
|
|
2237
|
-
|
|
2238
|
-
# Build binary
|
|
2239
|
-
make build
|
|
2240
|
-
|
|
2241
|
-
# Generate / refresh Swagger docs
|
|
2242
|
-
make docs
|
|
2243
|
-
|
|
2244
|
-
# Docker
|
|
2245
|
-
make docker-up
|
|
2246
|
-
\`\`\`
|
|
2247
|
-
|
|
2248
|
-
## Swagger / OpenAPI
|
|
2249
|
-
|
|
2250
|
-
After running \`make docs\`, the interactive UI is available at:
|
|
2251
|
-
|
|
2252
|
-
\`\`\`
|
|
2253
|
-
http://localhost:${e.port}/docs
|
|
2254
|
-
\`\`\`
|
|
2255
|
-
|
|
2256
|
-
The raw OpenAPI spec is served at \`/docs/doc.json\`.
|
|
2257
|
-
|
|
2258
|
-
## Endpoints
|
|
2259
|
-
|
|
2260
|
-
| Method | Path | Description |
|
|
2261
|
-
|--------|------|--------------|
|
|
2262
|
-
| GET | /api/v1/health/live | Kubernetes livenessProbe |
|
|
2263
|
-
| GET | /api/v1/health/ready | Kubernetes readinessProbe |
|
|
2264
|
-
| GET | /api/v1/echo/:name | Example handler \u2014 remove in production |
|
|
2265
|
-
| GET | /docs/* | Swagger UI (OpenAPI docs) |
|
|
2266
|
-
|
|
2267
|
-
## Configuration
|
|
2268
|
-
|
|
2269
|
-
All configuration is done through environment variables (see \`.env.example\`):
|
|
2270
|
-
|
|
2271
|
-
| Variable | Default | Description |
|
|
2272
|
-
|----------|---------|-------------|
|
|
2273
|
-
| \`PORT\` | \`${e.port}\` | HTTP listen port |
|
|
2274
|
-
| \`APP_ENV\` | \`development\` | Application environment |
|
|
2275
|
-
| \`GIN_MODE\` | \`debug\` | \`debug\` \\| \`release\` \\| \`test\` |
|
|
2276
|
-
| \`LOG_LEVEL\` | \`debug\` / \`info\` | \`debug\` \\| \`info\` \\| \`warn\` \\| \`error\` |
|
|
2277
|
-
| \`CORS_ALLOW_ORIGINS\` | \`*\` | Comma-separated list of allowed origins, or \`*\` |
|
|
2278
|
-
| \`RATE_LIMIT_RPS\` | \`100\` | Max requests per IP per second |
|
|
2279
|
-
|
|
2280
|
-
## Project structure
|
|
2281
|
-
|
|
2282
|
-
\`\`\`
|
|
2283
|
-
${e.project_name}/
|
|
2284
|
-
\u251C\u2500\u2500 cmd/
|
|
2285
|
-
\u2502 \u2514\u2500\u2500 server/
|
|
2286
|
-
\u2502 \u2514\u2500\u2500 main.go # Graceful shutdown + version ldflags
|
|
2287
|
-
\u251C\u2500\u2500 docs/ # Swagger generated files (\`make docs\`)
|
|
2288
|
-
\u2502 \u2514\u2500\u2500 doc.go # Package-level OpenAPI annotations
|
|
2289
|
-
\u251C\u2500\u2500 internal/
|
|
2290
|
-
\u2502 \u251C\u2500\u2500 apierr/ # Consistent JSON error envelope
|
|
2291
|
-
\u2502 \u2502 \u251C\u2500\u2500 apierr.go
|
|
2292
|
-
\u2502 \u2502 \u2514\u2500\u2500 apierr_test.go
|
|
2293
|
-
\u2502 \u251C\u2500\u2500 config/ # 12-factor configuration
|
|
2294
|
-
\u2502 \u2502 \u251C\u2500\u2500 config.go
|
|
2295
|
-
\u2502 \u2502 \u2514\u2500\u2500 config_test.go
|
|
2296
|
-
\u2502 \u251C\u2500\u2500 handlers/ # HTTP handlers + tests
|
|
2297
|
-
\u2502 \u2502 \u251C\u2500\u2500 health.go
|
|
2298
|
-
\u2502 \u2502 \u251C\u2500\u2500 health_test.go
|
|
2299
|
-
\u2502 \u2502 \u251C\u2500\u2500 example.go # EchoParams \u2014 replace with your own handlers
|
|
2300
|
-
\u2502 \u2502 \u2514\u2500\u2500 example_test.go
|
|
2301
|
-
\u2502 \u251C\u2500\u2500 middleware/
|
|
2302
|
-
\u2502 \u2502 \u251C\u2500\u2500 requestid.go # X-Request-ID + structured logger
|
|
2303
|
-
\u2502 \u2502 \u251C\u2500\u2500 requestid_test.go
|
|
2304
|
-
\u2502 \u2502 \u251C\u2500\u2500 cors.go # CORS (CORS_ALLOW_ORIGINS)
|
|
2305
|
-
\u2502 \u2502 \u251C\u2500\u2500 cors_test.go
|
|
2306
|
-
\u2502 \u2502 \u251C\u2500\u2500 ratelimit.go # Per-IP limiter (RATE_LIMIT_RPS)
|
|
2307
|
-
\u2502 \u2502 \u2514\u2500\u2500 ratelimit_test.go
|
|
2308
|
-
\u2502 \u2514\u2500\u2500 server/
|
|
2309
|
-
\u2502 \u251C\u2500\u2500 server.go
|
|
2310
|
-
\u2502 \u2514\u2500\u2500 server_test.go
|
|
2311
|
-
\u251C\u2500\u2500 .air.toml # Hot reload
|
|
2312
|
-
\u251C\u2500\u2500 .github/workflows/ci.yml # CI: test + lint
|
|
2313
|
-
\u251C\u2500\u2500 .golangci.yml
|
|
2314
|
-
\u251C\u2500\u2500 Dockerfile # Multi-stage, alpine HEALTHCHECK
|
|
2315
|
-
\u251C\u2500\u2500 docker-compose.yml
|
|
2316
|
-
\u251C\u2500\u2500 Makefile
|
|
2317
|
-
\u2514\u2500\u2500 README.md
|
|
2318
|
-
\`\`\`
|
|
2319
|
-
|
|
2320
|
-
## Available commands
|
|
2321
|
-
|
|
2322
|
-
| Command | Description |
|
|
2323
|
-
|---------|-------------|
|
|
2324
|
-
| \`make dev\` | Hot reload via [air](https://github.com/air-verse/air) |
|
|
2325
|
-
| \`make run\` | Run without hot reload |
|
|
2326
|
-
| \`make build\` | Binary with version ldflags |
|
|
2327
|
-
| \`make test\` | Run tests with race detector |
|
|
2328
|
-
| \`make cover\` | HTML coverage report |
|
|
2329
|
-
| \`make docs\` | Re-generate Swagger JSON (needs \`swag\`) |
|
|
2330
|
-
| \`make lint\` | golangci-lint |
|
|
2331
|
-
| \`make fmt\` | gofmt |
|
|
2332
|
-
| \`make tidy\` | go mod tidy |
|
|
2333
|
-
| \`make docker-up\` | Build & run via Docker Compose |
|
|
2334
|
-
| \`make docker-down\` | Stop |
|
|
2335
|
-
|
|
2336
|
-
## License
|
|
2337
|
-
|
|
2338
|
-
${e.app_version} \xB7 ${e.author}
|
|
2339
|
-
`}function Ui(){return `package middleware
|
|
2340
|
-
|
|
2341
|
-
import (
|
|
2342
|
-
"crypto/rand"
|
|
2343
|
-
"encoding/hex"
|
|
2344
|
-
"log/slog"
|
|
2345
|
-
"time"
|
|
2346
|
-
|
|
2347
|
-
"github.com/gin-gonic/gin"
|
|
2348
|
-
)
|
|
2349
|
-
|
|
2350
|
-
const headerRequestID = "X-Request-ID"
|
|
2351
|
-
|
|
2352
|
-
// RequestID injects a unique identifier into every request.
|
|
2353
|
-
// If the caller sends an X-Request-ID header it is reused; otherwise a new one
|
|
2354
|
-
// is generated and written back in the response.
|
|
2355
|
-
func RequestID() gin.HandlerFunc {
|
|
2356
|
-
return func(c *gin.Context) {
|
|
2357
|
-
id := c.GetHeader(headerRequestID)
|
|
2358
|
-
if id == "" {
|
|
2359
|
-
id = newID()
|
|
2360
|
-
}
|
|
2361
|
-
c.Set(headerRequestID, id)
|
|
2362
|
-
c.Header(headerRequestID, id)
|
|
2363
|
-
c.Next()
|
|
2364
|
-
}
|
|
2365
|
-
}
|
|
2366
|
-
|
|
2367
|
-
// Logger emits a structured JSON log line after each request.
|
|
2368
|
-
func Logger() gin.HandlerFunc {
|
|
2369
|
-
return func(c *gin.Context) {
|
|
2370
|
-
start := time.Now()
|
|
2371
|
-
c.Next()
|
|
2372
|
-
slog.Info("http",
|
|
2373
|
-
"method", c.Request.Method,
|
|
2374
|
-
"path", c.Request.URL.Path,
|
|
2375
|
-
"status", c.Writer.Status(),
|
|
2376
|
-
"bytes", c.Writer.Size(),
|
|
2377
|
-
"latency_ms", time.Since(start).Milliseconds(),
|
|
2378
|
-
"ip", c.ClientIP(),
|
|
2379
|
-
"request_id", c.GetString(headerRequestID),
|
|
2380
|
-
)
|
|
2381
|
-
}
|
|
2382
|
-
}
|
|
2383
|
-
|
|
2384
|
-
func newID() string {
|
|
2385
|
-
b := make([]byte, 8)
|
|
2386
|
-
if _, err := rand.Read(b); err != nil {
|
|
2387
|
-
return "unknown"
|
|
2388
|
-
}
|
|
2389
|
-
return hex.EncodeToString(b)
|
|
2390
|
-
}
|
|
2391
|
-
`}function Vi(e){return `package middleware_test
|
|
2392
|
-
|
|
2393
|
-
import (
|
|
2394
|
-
"net/http"
|
|
2395
|
-
"net/http/httptest"
|
|
2396
|
-
"testing"
|
|
2397
|
-
|
|
2398
|
-
"github.com/gin-gonic/gin"
|
|
2399
|
-
|
|
2400
|
-
"${e.module_path}/internal/middleware"
|
|
2401
|
-
)
|
|
2402
|
-
|
|
2403
|
-
func init() { gin.SetMode(gin.TestMode) }
|
|
2404
|
-
|
|
2405
|
-
func newTestRouter() *gin.Engine {
|
|
2406
|
-
r := gin.New()
|
|
2407
|
-
r.Use(middleware.RequestID())
|
|
2408
|
-
r.Use(middleware.Logger())
|
|
2409
|
-
r.GET("/ping", func(c *gin.Context) {
|
|
2410
|
-
c.String(http.StatusOK, "pong")
|
|
2411
|
-
})
|
|
2412
|
-
return r
|
|
2413
|
-
}
|
|
2414
|
-
|
|
2415
|
-
func TestRequestID_IsGenerated(t *testing.T) {
|
|
2416
|
-
w := httptest.NewRecorder()
|
|
2417
|
-
req, _ := http.NewRequest(http.MethodGet, "/ping", nil)
|
|
2418
|
-
newTestRouter().ServeHTTP(w, req)
|
|
2419
|
-
|
|
2420
|
-
id := w.Header().Get("X-Request-ID")
|
|
2421
|
-
if id == "" {
|
|
2422
|
-
t.Fatal("expected X-Request-ID header to be set")
|
|
2423
|
-
}
|
|
2424
|
-
if len(id) != 16 { // 8 random bytes \u2192 16 hex chars
|
|
2425
|
-
t.Fatalf("unexpected request ID length %d, want 16", len(id))
|
|
2426
|
-
}
|
|
2427
|
-
}
|
|
2428
|
-
|
|
2429
|
-
func TestRequestID_IsReused(t *testing.T) {
|
|
2430
|
-
w := httptest.NewRecorder()
|
|
2431
|
-
req, _ := http.NewRequest(http.MethodGet, "/ping", nil)
|
|
2432
|
-
req.Header.Set("X-Request-ID", "my-trace-id")
|
|
2433
|
-
newTestRouter().ServeHTTP(w, req)
|
|
2434
|
-
|
|
2435
|
-
id := w.Header().Get("X-Request-ID")
|
|
2436
|
-
if id != "my-trace-id" {
|
|
2437
|
-
t.Fatalf("expected X-Request-ID to be reused, got %q", id)
|
|
2438
|
-
}
|
|
2439
|
-
}
|
|
2440
|
-
`}function Bi(){return `// Package apierr provides a consistent JSON error envelope for all API responses.
|
|
2441
|
-
//
|
|
2442
|
-
// Every error response looks like:
|
|
2443
|
-
//
|
|
2444
|
-
// {"error": "user not found", "code": "NOT_FOUND", "request_id": "a1b2c3d4..."}
|
|
2445
|
-
package apierr
|
|
2446
|
-
|
|
2447
|
-
import (
|
|
2448
|
-
"net/http"
|
|
2449
|
-
|
|
2450
|
-
"github.com/gin-gonic/gin"
|
|
2451
|
-
)
|
|
2452
|
-
|
|
2453
|
-
// Response is the standard error envelope returned by all API endpoints.
|
|
2454
|
-
type Response struct {
|
|
2455
|
-
Error string \`json:"error"\`
|
|
2456
|
-
Code string \`json:"code"\`
|
|
2457
|
-
RequestID string \`json:"request_id,omitempty"\`
|
|
2458
|
-
}
|
|
2459
|
-
|
|
2460
|
-
func reply(c *gin.Context, status int, msg, code string) {
|
|
2461
|
-
c.AbortWithStatusJSON(status, Response{
|
|
2462
|
-
Error: msg,
|
|
2463
|
-
Code: code,
|
|
2464
|
-
RequestID: c.GetString("X-Request-ID"),
|
|
2465
|
-
})
|
|
2466
|
-
}
|
|
2467
|
-
|
|
2468
|
-
// BadRequest responds with 400 and code "BAD_REQUEST".
|
|
2469
|
-
func BadRequest(c *gin.Context, msg string) {
|
|
2470
|
-
reply(c, http.StatusBadRequest, msg, "BAD_REQUEST")
|
|
2471
|
-
}
|
|
2472
|
-
|
|
2473
|
-
// NotFound responds with 404 and code "NOT_FOUND".
|
|
2474
|
-
func NotFound(c *gin.Context, msg string) {
|
|
2475
|
-
reply(c, http.StatusNotFound, msg, "NOT_FOUND")
|
|
2476
|
-
}
|
|
2477
|
-
|
|
2478
|
-
// Unauthorized responds with 401 and code "UNAUTHORIZED".
|
|
2479
|
-
func Unauthorized(c *gin.Context) {
|
|
2480
|
-
reply(c, http.StatusUnauthorized, "authentication required", "UNAUTHORIZED")
|
|
2481
|
-
}
|
|
2482
|
-
|
|
2483
|
-
// Forbidden responds with 403 and code "FORBIDDEN".
|
|
2484
|
-
func Forbidden(c *gin.Context) {
|
|
2485
|
-
reply(c, http.StatusForbidden, "access denied", "FORBIDDEN")
|
|
2486
|
-
}
|
|
2487
|
-
|
|
2488
|
-
// MethodNotAllowed responds with 405 and code "METHOD_NOT_ALLOWED".
|
|
2489
|
-
func MethodNotAllowed(c *gin.Context) {
|
|
2490
|
-
reply(c, http.StatusMethodNotAllowed, "method not allowed", "METHOD_NOT_ALLOWED")
|
|
2491
|
-
}
|
|
2492
|
-
|
|
2493
|
-
// InternalError responds with 500 and code "INTERNAL_ERROR".
|
|
2494
|
-
// The original error is intentionally not exposed to the client.
|
|
2495
|
-
func InternalError(c *gin.Context, _ error) {
|
|
2496
|
-
reply(c, http.StatusInternalServerError, "an internal error occurred", "INTERNAL_ERROR")
|
|
2497
|
-
}
|
|
2498
|
-
|
|
2499
|
-
// TooManyRequests responds with 429 and code "TOO_MANY_REQUESTS".
|
|
2500
|
-
func TooManyRequests(c *gin.Context) {
|
|
2501
|
-
reply(c, http.StatusTooManyRequests, "rate limit exceeded", "TOO_MANY_REQUESTS")
|
|
2502
|
-
}
|
|
2503
|
-
`}function Ji(e){return `package apierr_test
|
|
2504
|
-
|
|
2505
|
-
import (
|
|
2506
|
-
"encoding/json"
|
|
2507
|
-
"net/http"
|
|
2508
|
-
"net/http/httptest"
|
|
2509
|
-
"testing"
|
|
2510
|
-
|
|
2511
|
-
"github.com/gin-gonic/gin"
|
|
2512
|
-
|
|
2513
|
-
"${e.module_path}/internal/apierr"
|
|
2514
|
-
)
|
|
2515
|
-
|
|
2516
|
-
func init() { gin.SetMode(gin.TestMode) }
|
|
2517
|
-
|
|
2518
|
-
func makeRouter(fn gin.HandlerFunc) *gin.Engine {
|
|
2519
|
-
r := gin.New()
|
|
2520
|
-
r.GET("/test", fn)
|
|
2521
|
-
return r
|
|
2522
|
-
}
|
|
2523
|
-
|
|
2524
|
-
func readJSON(t *testing.T, w *httptest.ResponseRecorder) apierr.Response {
|
|
2525
|
-
t.Helper()
|
|
2526
|
-
var out apierr.Response
|
|
2527
|
-
if err := json.NewDecoder(w.Body).Decode(&out); err != nil {
|
|
2528
|
-
t.Fatalf("invalid JSON: %v", err)
|
|
2529
|
-
}
|
|
2530
|
-
return out
|
|
2531
|
-
}
|
|
2532
|
-
|
|
2533
|
-
func TestBadRequest(t *testing.T) {
|
|
2534
|
-
w := httptest.NewRecorder()
|
|
2535
|
-
req, _ := http.NewRequest(http.MethodGet, "/test", nil)
|
|
2536
|
-
makeRouter(func(c *gin.Context) { apierr.BadRequest(c, "invalid email") }).ServeHTTP(w, req)
|
|
2537
|
-
|
|
2538
|
-
if w.Code != http.StatusBadRequest {
|
|
2539
|
-
t.Fatalf("expected 400, got %d", w.Code)
|
|
2540
|
-
}
|
|
2541
|
-
body := readJSON(t, w)
|
|
2542
|
-
if body.Code != "BAD_REQUEST" {
|
|
2543
|
-
t.Fatalf("expected BAD_REQUEST, got %q", body.Code)
|
|
2544
|
-
}
|
|
2545
|
-
if body.Error != "invalid email" {
|
|
2546
|
-
t.Fatalf("unexpected error message: %q", body.Error)
|
|
2547
|
-
}
|
|
2548
|
-
}
|
|
2549
|
-
|
|
2550
|
-
func TestNotFound(t *testing.T) {
|
|
2551
|
-
w := httptest.NewRecorder()
|
|
2552
|
-
req, _ := http.NewRequest(http.MethodGet, "/test", nil)
|
|
2553
|
-
makeRouter(func(c *gin.Context) { apierr.NotFound(c, "user not found") }).ServeHTTP(w, req)
|
|
2554
|
-
|
|
2555
|
-
if w.Code != http.StatusNotFound {
|
|
2556
|
-
t.Fatalf("expected 404, got %d", w.Code)
|
|
2557
|
-
}
|
|
2558
|
-
body := readJSON(t, w)
|
|
2559
|
-
if body.Code != "NOT_FOUND" {
|
|
2560
|
-
t.Fatalf("expected NOT_FOUND, got %q", body.Code)
|
|
2561
|
-
}
|
|
2562
|
-
}
|
|
2563
|
-
|
|
2564
|
-
func TestUnauthorized(t *testing.T) {
|
|
2565
|
-
w := httptest.NewRecorder()
|
|
2566
|
-
req, _ := http.NewRequest(http.MethodGet, "/test", nil)
|
|
2567
|
-
makeRouter(func(c *gin.Context) { apierr.Unauthorized(c) }).ServeHTTP(w, req)
|
|
2568
|
-
|
|
2569
|
-
if w.Code != http.StatusUnauthorized {
|
|
2570
|
-
t.Fatalf("expected 401, got %d", w.Code)
|
|
2571
|
-
}
|
|
2572
|
-
}
|
|
2573
|
-
|
|
2574
|
-
func TestForbidden(t *testing.T) {
|
|
2575
|
-
w := httptest.NewRecorder()
|
|
2576
|
-
req, _ := http.NewRequest(http.MethodGet, "/test", nil)
|
|
2577
|
-
makeRouter(func(c *gin.Context) { apierr.Forbidden(c) }).ServeHTTP(w, req)
|
|
2578
|
-
|
|
2579
|
-
if w.Code != http.StatusForbidden {
|
|
2580
|
-
t.Fatalf("expected 403, got %d", w.Code)
|
|
2581
|
-
}
|
|
2582
|
-
body := readJSON(t, w)
|
|
2583
|
-
if body.Code != "FORBIDDEN" {
|
|
2584
|
-
t.Fatalf("expected FORBIDDEN, got %q", body.Code)
|
|
2585
|
-
}
|
|
2586
|
-
}
|
|
2587
|
-
|
|
2588
|
-
func TestMethodNotAllowed(t *testing.T) {
|
|
2589
|
-
w := httptest.NewRecorder()
|
|
2590
|
-
req, _ := http.NewRequest(http.MethodGet, "/test", nil)
|
|
2591
|
-
makeRouter(func(c *gin.Context) { apierr.MethodNotAllowed(c) }).ServeHTTP(w, req)
|
|
2592
|
-
|
|
2593
|
-
if w.Code != http.StatusMethodNotAllowed {
|
|
2594
|
-
t.Fatalf("expected 405, got %d", w.Code)
|
|
2595
|
-
}
|
|
2596
|
-
body := readJSON(t, w)
|
|
2597
|
-
if body.Code != "METHOD_NOT_ALLOWED" {
|
|
2598
|
-
t.Fatalf("expected METHOD_NOT_ALLOWED, got %q", body.Code)
|
|
2599
|
-
}
|
|
2600
|
-
}
|
|
2601
|
-
|
|
2602
|
-
func TestInternalError(t *testing.T) {
|
|
2603
|
-
w := httptest.NewRecorder()
|
|
2604
|
-
req, _ := http.NewRequest(http.MethodGet, "/test", nil)
|
|
2605
|
-
makeRouter(func(c *gin.Context) { apierr.InternalError(c, nil) }).ServeHTTP(w, req)
|
|
2606
|
-
|
|
2607
|
-
if w.Code != http.StatusInternalServerError {
|
|
2608
|
-
t.Fatalf("expected 500, got %d", w.Code)
|
|
2609
|
-
}
|
|
2610
|
-
body := readJSON(t, w)
|
|
2611
|
-
if body.Code != "INTERNAL_ERROR" {
|
|
2612
|
-
t.Fatalf("expected INTERNAL_ERROR, got %q", body.Code)
|
|
2613
|
-
}
|
|
2614
|
-
}
|
|
2615
|
-
|
|
2616
|
-
func TestTooManyRequests(t *testing.T) {
|
|
2617
|
-
w := httptest.NewRecorder()
|
|
2618
|
-
req, _ := http.NewRequest(http.MethodGet, "/test", nil)
|
|
2619
|
-
makeRouter(func(c *gin.Context) { apierr.TooManyRequests(c) }).ServeHTTP(w, req)
|
|
2620
|
-
|
|
2621
|
-
if w.Code != http.StatusTooManyRequests {
|
|
2622
|
-
t.Fatalf("expected 429, got %d", w.Code)
|
|
2623
|
-
}
|
|
2624
|
-
body := readJSON(t, w)
|
|
2625
|
-
if body.Code != "TOO_MANY_REQUESTS" {
|
|
2626
|
-
t.Fatalf("expected TOO_MANY_REQUESTS, got %q", body.Code)
|
|
2627
|
-
}
|
|
2628
|
-
}
|
|
2629
|
-
`}function Yi(e){return `// Package docs provides the swaggo-generated OpenAPI specification.
|
|
2630
|
-
//
|
|
2631
|
-
// Run \`make docs\` to regenerate after changing handler annotations.
|
|
2632
|
-
//
|
|
2633
|
-
// @title ${Ro(e.project_name)} API
|
|
2634
|
-
// @version ${e.app_version}
|
|
2635
|
-
// @description ${e.description}
|
|
2636
|
-
// @host localhost:${e.port}
|
|
2637
|
-
// @BasePath /
|
|
2638
|
-
// @schemes http https
|
|
2639
|
-
//
|
|
2640
|
-
// @contact.name ${e.author}
|
|
2641
|
-
// @license.name MIT
|
|
2642
|
-
package docs
|
|
2643
|
-
`}function zi(e){return `package handlers
|
|
2644
|
-
|
|
2645
|
-
import (
|
|
2646
|
-
"net/http"
|
|
2647
|
-
|
|
2648
|
-
"github.com/gin-gonic/gin"
|
|
2649
|
-
|
|
2650
|
-
"${e.module_path}/internal/apierr"
|
|
2651
|
-
)
|
|
2652
|
-
|
|
2653
|
-
// EchoResponse is the JSON body returned by EchoParams.
|
|
2654
|
-
type EchoResponse struct {
|
|
2655
|
-
Name string \`json:"name"\`
|
|
2656
|
-
RequestID string \`json:"request_id"\`
|
|
2657
|
-
}
|
|
2658
|
-
|
|
2659
|
-
// EchoParams is an example handler demonstrating how to:
|
|
2660
|
-
// - read URL path parameters
|
|
2661
|
-
// - use apierr for consistent JSON error responses
|
|
2662
|
-
// - access the request ID injected by RequestID middleware
|
|
2663
|
-
//
|
|
2664
|
-
// Replace or remove this file once you add your own business logic.
|
|
2665
|
-
//
|
|
2666
|
-
// @Summary Echo path parameter
|
|
2667
|
-
// @Description Returns the :name path parameter together with the request ID.
|
|
2668
|
-
// @Tags example
|
|
2669
|
-
// @Produce json
|
|
2670
|
-
// @Param name path string true "Name to echo"
|
|
2671
|
-
// @Success 200 {object} handlers.EchoResponse
|
|
2672
|
-
// @Failure 400 {object} apierr.Response
|
|
2673
|
-
// @Router /api/v1/echo/{name} [get]
|
|
2674
|
-
func EchoParams(c *gin.Context) {
|
|
2675
|
-
name := c.Param("name")
|
|
2676
|
-
if name == "" {
|
|
2677
|
-
apierr.BadRequest(c, "name parameter is required")
|
|
2678
|
-
return
|
|
2679
|
-
}
|
|
2680
|
-
c.JSON(http.StatusOK, EchoResponse{
|
|
2681
|
-
Name: name,
|
|
2682
|
-
RequestID: c.GetString("X-Request-ID"),
|
|
2683
|
-
})
|
|
2684
|
-
}
|
|
2685
|
-
`}function Qi(e){return `package handlers_test
|
|
2686
|
-
|
|
2687
|
-
import (
|
|
2688
|
-
"encoding/json"
|
|
2689
|
-
"net/http"
|
|
2690
|
-
"net/http/httptest"
|
|
2691
|
-
"testing"
|
|
2692
|
-
|
|
2693
|
-
"github.com/gin-gonic/gin"
|
|
2694
|
-
|
|
2695
|
-
"${e.module_path}/internal/handlers"
|
|
2696
|
-
"${e.module_path}/internal/middleware"
|
|
2697
|
-
)
|
|
2698
|
-
|
|
2699
|
-
func newEchoRouter() *gin.Engine {
|
|
2700
|
-
r := gin.New()
|
|
2701
|
-
r.Use(middleware.RequestID())
|
|
2702
|
-
r.GET("/echo/:name", handlers.EchoParams)
|
|
2703
|
-
return r
|
|
2704
|
-
}
|
|
2705
|
-
|
|
2706
|
-
func TestEchoParams_Success(t *testing.T) {
|
|
2707
|
-
w := httptest.NewRecorder()
|
|
2708
|
-
req, _ := http.NewRequest(http.MethodGet, "/echo/alice", nil)
|
|
2709
|
-
newEchoRouter().ServeHTTP(w, req)
|
|
2710
|
-
|
|
2711
|
-
if w.Code != http.StatusOK {
|
|
2712
|
-
t.Fatalf("expected 200, got %d", w.Code)
|
|
2713
|
-
}
|
|
2714
|
-
|
|
2715
|
-
var body map[string]any
|
|
2716
|
-
if err := json.NewDecoder(w.Body).Decode(&body); err != nil {
|
|
2717
|
-
t.Fatalf("invalid JSON: %v", err)
|
|
2718
|
-
}
|
|
2719
|
-
if body["name"] != "alice" {
|
|
2720
|
-
t.Fatalf("expected name=alice, got %v", body["name"])
|
|
2721
|
-
}
|
|
2722
|
-
if body["request_id"] == nil || body["request_id"] == "" {
|
|
2723
|
-
t.Fatal("expected request_id to be set by RequestID middleware")
|
|
2724
|
-
}
|
|
2725
|
-
}
|
|
2726
|
-
|
|
2727
|
-
// TestEchoParams_EmptyName registers EchoParams on a param-free route so that
|
|
2728
|
-
// c.Param("name") returns "" and the 400 guard executes.
|
|
2729
|
-
func TestEchoParams_EmptyName(t *testing.T) {
|
|
2730
|
-
gin.SetMode(gin.TestMode)
|
|
2731
|
-
r := gin.New()
|
|
2732
|
-
r.Use(middleware.RequestID())
|
|
2733
|
-
r.GET("/echo-bare", handlers.EchoParams)
|
|
2734
|
-
|
|
2735
|
-
w := httptest.NewRecorder()
|
|
2736
|
-
req, _ := http.NewRequest(http.MethodGet, "/echo-bare", nil)
|
|
2737
|
-
r.ServeHTTP(w, req)
|
|
2738
|
-
|
|
2739
|
-
if w.Code != http.StatusBadRequest {
|
|
2740
|
-
t.Fatalf("expected 400, got %d", w.Code)
|
|
2741
|
-
}
|
|
2742
|
-
var body map[string]any
|
|
2743
|
-
if err := json.NewDecoder(w.Body).Decode(&body); err != nil {
|
|
2744
|
-
t.Fatalf("invalid JSON: %v", err)
|
|
2745
|
-
}
|
|
2746
|
-
if body["code"] != "BAD_REQUEST" {
|
|
2747
|
-
t.Fatalf("expected code=BAD_REQUEST, got %v", body["code"])
|
|
2748
|
-
}
|
|
2749
|
-
}
|
|
2750
|
-
`}function Xi(e){return `package config_test
|
|
2751
|
-
|
|
2752
|
-
import (
|
|
2753
|
-
"log/slog"
|
|
2754
|
-
"testing"
|
|
2755
|
-
|
|
2756
|
-
"${e.module_path}/internal/config"
|
|
2757
|
-
)
|
|
2758
|
-
|
|
2759
|
-
func TestParseLogLevel(t *testing.T) {
|
|
2760
|
-
tests := []struct {
|
|
2761
|
-
input string
|
|
2762
|
-
want slog.Level
|
|
2763
|
-
}{
|
|
2764
|
-
{"debug", slog.LevelDebug},
|
|
2765
|
-
{"DEBUG", slog.LevelDebug},
|
|
2766
|
-
{"warn", slog.LevelWarn},
|
|
2767
|
-
{"warning", slog.LevelWarn},
|
|
2768
|
-
{"error", slog.LevelError},
|
|
2769
|
-
{"info", slog.LevelInfo},
|
|
2770
|
-
{"", slog.LevelInfo},
|
|
2771
|
-
{"unknown", slog.LevelInfo},
|
|
2772
|
-
}
|
|
2773
|
-
for _, tc := range tests {
|
|
2774
|
-
got := config.ParseLogLevel(tc.input)
|
|
2775
|
-
if got != tc.want {
|
|
2776
|
-
t.Errorf("ParseLogLevel(%q) = %v, want %v", tc.input, got, tc.want)
|
|
2777
|
-
}
|
|
2778
|
-
}
|
|
2779
|
-
}
|
|
2780
|
-
|
|
2781
|
-
func TestLoad_EnvOverride(t *testing.T) {
|
|
2782
|
-
t.Setenv("PORT", "9090")
|
|
2783
|
-
t.Setenv("APP_ENV", "production")
|
|
2784
|
-
t.Setenv("LOG_LEVEL", "warn")
|
|
2785
|
-
t.Setenv("GIN_MODE", "release")
|
|
2786
|
-
|
|
2787
|
-
cfg := config.Load()
|
|
2788
|
-
|
|
2789
|
-
if cfg.Port != "9090" {
|
|
2790
|
-
t.Errorf("expected Port=9090, got %q", cfg.Port)
|
|
2791
|
-
}
|
|
2792
|
-
if cfg.Env != "production" {
|
|
2793
|
-
t.Errorf("expected Env=production, got %q", cfg.Env)
|
|
2794
|
-
}
|
|
2795
|
-
if cfg.LogLevel != "warn" {
|
|
2796
|
-
t.Errorf("expected LogLevel=warn, got %q", cfg.LogLevel)
|
|
2797
|
-
}
|
|
2798
|
-
if cfg.GinMode != "release" {
|
|
2799
|
-
t.Errorf("expected GinMode=release, got %q", cfg.GinMode)
|
|
2800
|
-
}
|
|
2801
|
-
}
|
|
2802
|
-
|
|
2803
|
-
func TestLoad_Defaults(t *testing.T) {
|
|
2804
|
-
// Empty string forces getEnv() to return the built-in fallback value.
|
|
2805
|
-
t.Setenv("PORT", "")
|
|
2806
|
-
t.Setenv("APP_ENV", "")
|
|
2807
|
-
t.Setenv("LOG_LEVEL", "")
|
|
2808
|
-
t.Setenv("GIN_MODE", "")
|
|
2809
|
-
|
|
2810
|
-
cfg := config.Load()
|
|
2811
|
-
|
|
2812
|
-
if cfg.Port != "${e.port}" {
|
|
2813
|
-
t.Errorf("expected default Port=${e.port}, got %q", cfg.Port)
|
|
2814
|
-
}
|
|
2815
|
-
if cfg.Env != "development" {
|
|
2816
|
-
t.Errorf("expected default Env=development, got %q", cfg.Env)
|
|
2817
|
-
}
|
|
2818
|
-
// APP_ENV="" \u2192 fallback "development" \u2192 defaultLogLevel \u2192 "debug"
|
|
2819
|
-
if cfg.LogLevel != "debug" {
|
|
2820
|
-
t.Errorf("expected default LogLevel=debug (development env), got %q", cfg.LogLevel)
|
|
2821
|
-
}
|
|
2822
|
-
if cfg.GinMode != "debug" {
|
|
2823
|
-
t.Errorf("expected default GinMode=debug, got %q", cfg.GinMode)
|
|
2824
|
-
}
|
|
2825
|
-
}
|
|
2826
|
-
`}function Zi(){return `package middleware
|
|
2827
|
-
|
|
2828
|
-
import (
|
|
2829
|
-
"net/http"
|
|
2830
|
-
"os"
|
|
2831
|
-
"strings"
|
|
2832
|
-
|
|
2833
|
-
"github.com/gin-gonic/gin"
|
|
2834
|
-
)
|
|
2835
|
-
|
|
2836
|
-
// CORS returns a Gin middleware configured via CORS_ALLOW_ORIGINS env var.
|
|
2837
|
-
//
|
|
2838
|
-
// Set CORS_ALLOW_ORIGINS="*" for development (the default when unset).
|
|
2839
|
-
// In production supply a comma-separated list of allowed origins:
|
|
2840
|
-
//
|
|
2841
|
-
// CORS_ALLOW_ORIGINS=https://app.example.com,https://admin.example.com
|
|
2842
|
-
func CORS() gin.HandlerFunc {
|
|
2843
|
-
allowed := os.Getenv("CORS_ALLOW_ORIGINS")
|
|
2844
|
-
if allowed == "" {
|
|
2845
|
-
allowed = "*"
|
|
2846
|
-
}
|
|
2847
|
-
allowAll := allowed == "*"
|
|
2848
|
-
|
|
2849
|
-
return func(c *gin.Context) {
|
|
2850
|
-
origin := c.Request.Header.Get("Origin")
|
|
2851
|
-
if allowAll {
|
|
2852
|
-
c.Header("Access-Control-Allow-Origin", "*")
|
|
2853
|
-
} else if origin != "" {
|
|
2854
|
-
for _, o := range strings.Split(allowed, ",") {
|
|
2855
|
-
if strings.TrimSpace(o) == origin {
|
|
2856
|
-
c.Header("Access-Control-Allow-Origin", origin)
|
|
2857
|
-
c.Header("Vary", "Origin")
|
|
2858
|
-
break
|
|
2859
|
-
}
|
|
2860
|
-
}
|
|
2861
|
-
}
|
|
2862
|
-
c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
|
|
2863
|
-
c.Header("Access-Control-Allow-Headers", "Origin,Content-Type,Authorization,X-Request-ID")
|
|
2864
|
-
c.Header("Access-Control-Expose-Headers", "X-Request-ID")
|
|
2865
|
-
c.Header("Access-Control-Max-Age", "600")
|
|
2866
|
-
|
|
2867
|
-
if c.Request.Method == http.MethodOptions {
|
|
2868
|
-
c.AbortWithStatus(http.StatusNoContent)
|
|
2869
|
-
return
|
|
2870
|
-
}
|
|
2871
|
-
c.Next()
|
|
2872
|
-
}
|
|
2873
|
-
}
|
|
2874
|
-
`}function en(e){return `package middleware_test
|
|
2875
|
-
|
|
2876
|
-
import (
|
|
2877
|
-
"net/http"
|
|
2878
|
-
"net/http/httptest"
|
|
2879
|
-
"testing"
|
|
2880
|
-
|
|
2881
|
-
"github.com/gin-gonic/gin"
|
|
2882
|
-
|
|
2883
|
-
"${e.module_path}/internal/middleware"
|
|
2884
|
-
)
|
|
2885
|
-
|
|
2886
|
-
func newCORSRouter(t *testing.T) *gin.Engine {
|
|
2887
|
-
t.Helper()
|
|
2888
|
-
gin.SetMode(gin.TestMode)
|
|
2889
|
-
r := gin.New()
|
|
2890
|
-
r.Use(middleware.CORS())
|
|
2891
|
-
r.GET("/ping", func(c *gin.Context) { c.Status(http.StatusOK) })
|
|
2892
|
-
return r
|
|
2893
|
-
}
|
|
2894
|
-
|
|
2895
|
-
func TestCORS_Wildcard(t *testing.T) {
|
|
2896
|
-
t.Setenv("CORS_ALLOW_ORIGINS", "*")
|
|
2897
|
-
w := httptest.NewRecorder()
|
|
2898
|
-
req, _ := http.NewRequest(http.MethodGet, "/ping", nil)
|
|
2899
|
-
req.Header.Set("Origin", "https://example.com")
|
|
2900
|
-
newCORSRouter(t).ServeHTTP(w, req)
|
|
2901
|
-
|
|
2902
|
-
if got := w.Header().Get("Access-Control-Allow-Origin"); got != "*" {
|
|
2903
|
-
t.Fatalf("expected ACAO=*, got %q", got)
|
|
2904
|
-
}
|
|
2905
|
-
}
|
|
2906
|
-
|
|
2907
|
-
func TestCORS_Preflight(t *testing.T) {
|
|
2908
|
-
t.Setenv("CORS_ALLOW_ORIGINS", "*")
|
|
2909
|
-
gin.SetMode(gin.TestMode)
|
|
2910
|
-
r := gin.New()
|
|
2911
|
-
r.Use(middleware.CORS())
|
|
2912
|
-
|
|
2913
|
-
w := httptest.NewRecorder()
|
|
2914
|
-
req, _ := http.NewRequest(http.MethodOptions, "/ping", nil)
|
|
2915
|
-
req.Header.Set("Origin", "https://example.com")
|
|
2916
|
-
req.Header.Set("Access-Control-Request-Method", "POST")
|
|
2917
|
-
r.ServeHTTP(w, req)
|
|
2918
|
-
|
|
2919
|
-
if w.Code != http.StatusNoContent {
|
|
2920
|
-
t.Fatalf("expected 204 preflight, got %d", w.Code)
|
|
2921
|
-
}
|
|
2922
|
-
}
|
|
2923
|
-
|
|
2924
|
-
func TestCORS_SpecificOrigin_Allowed(t *testing.T) {
|
|
2925
|
-
t.Setenv("CORS_ALLOW_ORIGINS", "https://app.example.com")
|
|
2926
|
-
w := httptest.NewRecorder()
|
|
2927
|
-
req, _ := http.NewRequest(http.MethodGet, "/ping", nil)
|
|
2928
|
-
req.Header.Set("Origin", "https://app.example.com")
|
|
2929
|
-
newCORSRouter(t).ServeHTTP(w, req)
|
|
2930
|
-
|
|
2931
|
-
if got := w.Header().Get("Access-Control-Allow-Origin"); got != "https://app.example.com" {
|
|
2932
|
-
t.Fatalf("expected ACAO=https://app.example.com, got %q", got)
|
|
2933
|
-
}
|
|
2934
|
-
}
|
|
2935
|
-
|
|
2936
|
-
func TestCORS_SpecificOrigin_Denied(t *testing.T) {
|
|
2937
|
-
t.Setenv("CORS_ALLOW_ORIGINS", "https://app.example.com")
|
|
2938
|
-
w := httptest.NewRecorder()
|
|
2939
|
-
req, _ := http.NewRequest(http.MethodGet, "/ping", nil)
|
|
2940
|
-
req.Header.Set("Origin", "https://evil.com")
|
|
2941
|
-
newCORSRouter(t).ServeHTTP(w, req)
|
|
2942
|
-
|
|
2943
|
-
if got := w.Header().Get("Access-Control-Allow-Origin"); got != "" {
|
|
2944
|
-
t.Fatalf("expected no ACAO header for denied origin, got %q", got)
|
|
2945
|
-
}
|
|
2946
|
-
}
|
|
2947
|
-
|
|
2948
|
-
func TestCORS_Default_Origin(t *testing.T) {
|
|
2949
|
-
// When CORS_ALLOW_ORIGINS is unset the middleware must default to "*".
|
|
2950
|
-
t.Setenv("CORS_ALLOW_ORIGINS", "")
|
|
2951
|
-
w := httptest.NewRecorder()
|
|
2952
|
-
req, _ := http.NewRequest(http.MethodGet, "/ping", nil)
|
|
2953
|
-
req.Header.Set("Origin", "https://anywhere.com")
|
|
2954
|
-
newCORSRouter(t).ServeHTTP(w, req)
|
|
2955
|
-
|
|
2956
|
-
if got := w.Header().Get("Access-Control-Allow-Origin"); got == "" {
|
|
2957
|
-
t.Fatal("expected CORS header when CORS_ALLOW_ORIGINS defaults to *")
|
|
2958
|
-
}
|
|
2959
|
-
}
|
|
2960
|
-
`}function tn(e){return `package server_test
|
|
2961
|
-
|
|
2962
|
-
import (
|
|
2963
|
-
"encoding/json"
|
|
2964
|
-
"net/http"
|
|
2965
|
-
"net/http/httptest"
|
|
2966
|
-
"testing"
|
|
2967
|
-
|
|
2968
|
-
"github.com/gin-gonic/gin"
|
|
2969
|
-
|
|
2970
|
-
"${e.module_path}/internal/config"
|
|
2971
|
-
"${e.module_path}/internal/server"
|
|
2972
|
-
)
|
|
2973
|
-
|
|
2974
|
-
func init() { gin.SetMode(gin.TestMode) }
|
|
2975
|
-
|
|
2976
|
-
type serverAPIError struct {
|
|
2977
|
-
Code string \`json:"code"\`
|
|
2978
|
-
Message string \`json:"message"\`
|
|
2979
|
-
}
|
|
2980
|
-
|
|
2981
|
-
func TestServer_NotFound_JSON(t *testing.T) {
|
|
2982
|
-
w := httptest.NewRecorder()
|
|
2983
|
-
req, _ := http.NewRequest(http.MethodGet, "/no-such-route", nil)
|
|
2984
|
-
server.NewRouter(config.Load()).ServeHTTP(w, req)
|
|
2985
|
-
|
|
2986
|
-
if w.Code != http.StatusNotFound {
|
|
2987
|
-
t.Fatalf("expected 404, got %d", w.Code)
|
|
2988
|
-
}
|
|
2989
|
-
var body serverAPIError
|
|
2990
|
-
if err := json.NewDecoder(w.Body).Decode(&body); err != nil {
|
|
2991
|
-
t.Fatalf("expected JSON error body: %v", err)
|
|
2992
|
-
}
|
|
2993
|
-
if body.Code != "NOT_FOUND" {
|
|
2994
|
-
t.Fatalf("expected code=NOT_FOUND, got %q", body.Code)
|
|
2995
|
-
}
|
|
2996
|
-
}
|
|
2997
|
-
|
|
2998
|
-
func TestServer_MethodNotAllowed_JSON(t *testing.T) {
|
|
2999
|
-
w := httptest.NewRecorder()
|
|
3000
|
-
req, _ := http.NewRequest(http.MethodPost, "/api/v1/health/live", nil)
|
|
3001
|
-
server.NewRouter(config.Load()).ServeHTTP(w, req)
|
|
3002
|
-
|
|
3003
|
-
if w.Code != http.StatusMethodNotAllowed {
|
|
3004
|
-
t.Fatalf("expected 405, got %d", w.Code)
|
|
3005
|
-
}
|
|
3006
|
-
var body serverAPIError
|
|
3007
|
-
if err := json.NewDecoder(w.Body).Decode(&body); err != nil {
|
|
3008
|
-
t.Fatalf("expected JSON error body: %v", err)
|
|
3009
|
-
}
|
|
3010
|
-
if body.Code != "METHOD_NOT_ALLOWED" {
|
|
3011
|
-
t.Fatalf("expected code=METHOD_NOT_ALLOWED, got %q", body.Code)
|
|
3012
|
-
}
|
|
3013
|
-
}
|
|
3014
|
-
|
|
3015
|
-
func TestServer_CORS_Header(t *testing.T) {
|
|
3016
|
-
t.Setenv("CORS_ALLOW_ORIGINS", "*")
|
|
3017
|
-
w := httptest.NewRecorder()
|
|
3018
|
-
req, _ := http.NewRequest(http.MethodGet, "/api/v1/health/live", nil)
|
|
3019
|
-
req.Header.Set("Origin", "https://example.com")
|
|
3020
|
-
server.NewRouter(config.Load()).ServeHTTP(w, req)
|
|
3021
|
-
|
|
3022
|
-
if got := w.Header().Get("Access-Control-Allow-Origin"); got == "" {
|
|
3023
|
-
t.Fatal("expected Access-Control-Allow-Origin header to be set")
|
|
3024
|
-
}
|
|
3025
|
-
}
|
|
3026
|
-
|
|
3027
|
-
func TestServer_HealthLive(t *testing.T) {
|
|
3028
|
-
w := httptest.NewRecorder()
|
|
3029
|
-
req, _ := http.NewRequest(http.MethodGet, "/api/v1/health/live", nil)
|
|
3030
|
-
server.NewRouter(config.Load()).ServeHTTP(w, req)
|
|
3031
|
-
|
|
3032
|
-
if w.Code != http.StatusOK {
|
|
3033
|
-
t.Fatalf("expected 200, got %d", w.Code)
|
|
3034
|
-
}
|
|
3035
|
-
var body map[string]any
|
|
3036
|
-
if err := json.NewDecoder(w.Body).Decode(&body); err != nil {
|
|
3037
|
-
t.Fatalf("invalid JSON: %v", err)
|
|
3038
|
-
}
|
|
3039
|
-
if body["status"] != "ok" {
|
|
3040
|
-
t.Fatalf("expected status=ok, got %v", body["status"])
|
|
3041
|
-
}
|
|
3042
|
-
}
|
|
3043
|
-
|
|
3044
|
-
func TestServer_Docs_Redirect(t *testing.T) {
|
|
3045
|
-
w := httptest.NewRecorder()
|
|
3046
|
-
req, _ := http.NewRequest(http.MethodGet, "/docs", nil)
|
|
3047
|
-
server.NewRouter(config.Load()).ServeHTTP(w, req)
|
|
3048
|
-
|
|
3049
|
-
if w.Code != http.StatusFound {
|
|
3050
|
-
t.Fatalf("expected 302 redirect from /docs, got %d", w.Code)
|
|
3051
|
-
}
|
|
3052
|
-
if loc := w.Header().Get("Location"); loc != "/docs/index.html" {
|
|
3053
|
-
t.Fatalf("expected Location=/docs/index.html, got %q", loc)
|
|
3054
|
-
}
|
|
3055
|
-
}
|
|
3056
|
-
|
|
3057
|
-
func TestServer_ReleaseMode(t *testing.T) {
|
|
3058
|
-
// Covers the gin.SetMode(gin.ReleaseMode) branch in NewRouter.
|
|
3059
|
-
// Restore test mode after so other tests are not affected.
|
|
3060
|
-
t.Cleanup(func() { gin.SetMode(gin.TestMode) })
|
|
3061
|
-
t.Setenv("GIN_MODE", "release")
|
|
3062
|
-
w := httptest.NewRecorder()
|
|
3063
|
-
req, _ := http.NewRequest(http.MethodGet, "/api/v1/health/live", nil)
|
|
3064
|
-
server.NewRouter(config.Load()).ServeHTTP(w, req)
|
|
3065
|
-
|
|
3066
|
-
if w.Code != http.StatusOK {
|
|
3067
|
-
t.Fatalf("expected 200 in release mode, got %d", w.Code)
|
|
3068
|
-
}
|
|
3069
|
-
}
|
|
3070
|
-
`}function on(e){return `package middleware
|
|
3071
|
-
|
|
3072
|
-
import (
|
|
3073
|
-
"os"
|
|
3074
|
-
"strconv"
|
|
3075
|
-
"sync"
|
|
3076
|
-
"time"
|
|
3077
|
-
|
|
3078
|
-
"github.com/gin-gonic/gin"
|
|
3079
|
-
|
|
3080
|
-
"${e.module_path}/internal/apierr"
|
|
3081
|
-
)
|
|
3082
|
-
|
|
3083
|
-
// ipBucket tracks the per-IP fixed-window request counter.
|
|
3084
|
-
type ipBucket struct {
|
|
3085
|
-
mu sync.Mutex
|
|
3086
|
-
count int
|
|
3087
|
-
windowStart time.Time
|
|
3088
|
-
}
|
|
3089
|
-
|
|
3090
|
-
// RateLimit returns a per-IP fixed-window rate limiter.
|
|
3091
|
-
// Configure the limit via RATE_LIMIT_RPS env var (requests per second, default 100).
|
|
3092
|
-
func RateLimit() gin.HandlerFunc {
|
|
3093
|
-
rps := 100
|
|
3094
|
-
if raw := os.Getenv("RATE_LIMIT_RPS"); raw != "" {
|
|
3095
|
-
if n, err := strconv.Atoi(raw); err == nil && n > 0 {
|
|
3096
|
-
rps = n
|
|
3097
|
-
}
|
|
3098
|
-
}
|
|
3099
|
-
var buckets sync.Map
|
|
3100
|
-
return func(c *gin.Context) {
|
|
3101
|
-
ip := c.ClientIP()
|
|
3102
|
-
now := time.Now()
|
|
3103
|
-
v, _ := buckets.LoadOrStore(ip, &ipBucket{windowStart: now})
|
|
3104
|
-
b := v.(*ipBucket)
|
|
3105
|
-
b.mu.Lock()
|
|
3106
|
-
if now.Sub(b.windowStart) >= time.Second {
|
|
3107
|
-
b.count = 0
|
|
3108
|
-
b.windowStart = now
|
|
3109
|
-
}
|
|
3110
|
-
b.count++
|
|
3111
|
-
count := b.count
|
|
3112
|
-
b.mu.Unlock()
|
|
3113
|
-
if count > rps {
|
|
3114
|
-
apierr.TooManyRequests(c)
|
|
3115
|
-
c.Header("Retry-After", "1")
|
|
3116
|
-
return
|
|
3117
|
-
}
|
|
3118
|
-
c.Next()
|
|
3119
|
-
}
|
|
3120
|
-
}
|
|
3121
|
-
`}function rn(e){return `package middleware_test
|
|
3122
|
-
|
|
3123
|
-
import (
|
|
3124
|
-
"net/http"
|
|
3125
|
-
"net/http/httptest"
|
|
3126
|
-
"testing"
|
|
3127
|
-
|
|
3128
|
-
"github.com/gin-gonic/gin"
|
|
3129
|
-
|
|
3130
|
-
"${e.module_path}/internal/middleware"
|
|
3131
|
-
)
|
|
3132
|
-
|
|
3133
|
-
func newRateLimitRouter(t *testing.T) *gin.Engine {
|
|
3134
|
-
t.Helper()
|
|
3135
|
-
gin.SetMode(gin.TestMode)
|
|
3136
|
-
r := gin.New()
|
|
3137
|
-
r.Use(middleware.RateLimit())
|
|
3138
|
-
r.GET("/", func(c *gin.Context) { c.Status(http.StatusOK) })
|
|
3139
|
-
return r
|
|
3140
|
-
}
|
|
3141
|
-
|
|
3142
|
-
func TestRateLimit_AllowsUnderLimit(t *testing.T) {
|
|
3143
|
-
t.Setenv("RATE_LIMIT_RPS", "3")
|
|
3144
|
-
r := newRateLimitRouter(t)
|
|
3145
|
-
|
|
3146
|
-
for i := 0; i < 3; i++ {
|
|
3147
|
-
w := httptest.NewRecorder()
|
|
3148
|
-
req, _ := http.NewRequest(http.MethodGet, "/", nil)
|
|
3149
|
-
r.ServeHTTP(w, req)
|
|
3150
|
-
if w.Code != http.StatusOK {
|
|
3151
|
-
t.Fatalf("request %d: expected 200, got %d", i+1, w.Code)
|
|
3152
|
-
}
|
|
3153
|
-
}
|
|
3154
|
-
}
|
|
3155
|
-
|
|
3156
|
-
func TestRateLimit_Blocks_After_Limit(t *testing.T) {
|
|
3157
|
-
t.Setenv("RATE_LIMIT_RPS", "2")
|
|
3158
|
-
r := newRateLimitRouter(t)
|
|
3159
|
-
|
|
3160
|
-
// Exhaust the limit.
|
|
3161
|
-
for i := 0; i < 2; i++ {
|
|
3162
|
-
w := httptest.NewRecorder()
|
|
3163
|
-
req, _ := http.NewRequest(http.MethodGet, "/", nil)
|
|
3164
|
-
r.ServeHTTP(w, req)
|
|
3165
|
-
}
|
|
3166
|
-
|
|
3167
|
-
// Next request must be rejected.
|
|
3168
|
-
w := httptest.NewRecorder()
|
|
3169
|
-
req, _ := http.NewRequest(http.MethodGet, "/", nil)
|
|
3170
|
-
r.ServeHTTP(w, req)
|
|
3171
|
-
|
|
3172
|
-
if w.Code != http.StatusTooManyRequests {
|
|
3173
|
-
t.Fatalf("expected 429 after limit, got %d", w.Code)
|
|
3174
|
-
}
|
|
3175
|
-
}
|
|
3176
|
-
|
|
3177
|
-
func TestRateLimit_InvalidRPS(t *testing.T) {
|
|
3178
|
-
// When RATE_LIMIT_RPS is not a valid positive integer, the middleware
|
|
3179
|
-
// must fall back to the default limit (100 rps) and allow normal requests.
|
|
3180
|
-
t.Setenv("RATE_LIMIT_RPS", "not-a-number")
|
|
3181
|
-
r := newRateLimitRouter(t)
|
|
3182
|
-
w := httptest.NewRecorder()
|
|
3183
|
-
req, _ := http.NewRequest(http.MethodGet, "/", nil)
|
|
3184
|
-
r.ServeHTTP(w, req)
|
|
3185
|
-
|
|
3186
|
-
if w.Code != http.StatusOK {
|
|
3187
|
-
t.Fatalf("expected 200 with invalid RPS env, got %d", w.Code)
|
|
3188
|
-
}
|
|
3189
|
-
}
|
|
3190
|
-
`}function nn(e){return `# Air \u2014 live reload for Go projects
|
|
3191
|
-
# https://github.com/air-verse/air
|
|
3192
|
-
root = "."
|
|
3193
|
-
tmp_dir = "tmp"
|
|
3194
|
-
|
|
3195
|
-
[build]
|
|
3196
|
-
pre_cmd = ["$(go env GOPATH)/bin/swag init -g main.go -d cmd/server,internal/handlers,internal/apierr -o docs --parseDependency 2>/dev/null || true"]
|
|
3197
|
-
cmd = "go build -o ./tmp/server ./cmd/server"
|
|
3198
|
-
bin = "./tmp/server"
|
|
3199
|
-
include_ext = ["go", "yaml", "yml", "env"]
|
|
3200
|
-
exclude_dir = ["tmp", "vendor", ".git", "testdata", "docs"]
|
|
3201
|
-
delay = 500
|
|
3202
|
-
rerun_delay = 500
|
|
3203
|
-
send_interrupt = true
|
|
3204
|
-
kill_delay = "200ms"
|
|
3205
|
-
|
|
3206
|
-
[env]
|
|
3207
|
-
GIN_MODE = "debug"
|
|
3208
|
-
PORT = "${e.port}"
|
|
3209
|
-
|
|
3210
|
-
[misc]
|
|
3211
|
-
clean_on_exit = true
|
|
3212
|
-
|
|
3213
|
-
[log]
|
|
3214
|
-
time = false
|
|
3215
|
-
`}function sn(e){return `run:
|
|
3216
|
-
timeout: 5m
|
|
3217
|
-
|
|
3218
|
-
linters:
|
|
3219
|
-
enable:
|
|
3220
|
-
- bodyclose
|
|
3221
|
-
- durationcheck
|
|
3222
|
-
- errcheck
|
|
3223
|
-
- errname
|
|
3224
|
-
- errorlint
|
|
3225
|
-
- gci
|
|
3226
|
-
- goimports
|
|
3227
|
-
- gosimple
|
|
3228
|
-
- govet
|
|
3229
|
-
- ineffassign
|
|
3230
|
-
- misspell
|
|
3231
|
-
- noctx
|
|
3232
|
-
- nolintlint
|
|
3233
|
-
- prealloc
|
|
3234
|
-
- staticcheck
|
|
3235
|
-
- unconvert
|
|
3236
|
-
- unused
|
|
3237
|
-
- wrapcheck
|
|
3238
|
-
|
|
3239
|
-
linters-settings:
|
|
3240
|
-
gci:
|
|
3241
|
-
sections:
|
|
3242
|
-
- standard
|
|
3243
|
-
- default
|
|
3244
|
-
- prefix(${e})
|
|
3245
|
-
goimports:
|
|
3246
|
-
local-prefixes: "${e}"
|
|
3247
|
-
govet:
|
|
3248
|
-
enable:
|
|
3249
|
-
- shadow
|
|
3250
|
-
wrapcheck:
|
|
3251
|
-
ignorePackageGlobs:
|
|
3252
|
-
- "${e}/*"
|
|
3253
|
-
|
|
3254
|
-
issues:
|
|
3255
|
-
max-same-issues: 5
|
|
3256
|
-
exclude-rules:
|
|
3257
|
-
- path: _test.go
|
|
3258
|
-
linters:
|
|
3259
|
-
- errcheck
|
|
3260
|
-
- wrapcheck
|
|
3261
|
-
`}function an(){return JSON.stringify({engine:"npm",runtime:"go"},null,2)}function cn(e,o){return JSON.stringify({kit_name:"gogin.standard",runtime:"go",module_support:false,project_name:e.project_name,module_path:e.module_path,app_version:e.app_version,created_by:"rapidkit-npm",rapidkit_version:o,created_at:new Date().toISOString()},null,2)}function ln(e){return `#!/usr/bin/env sh
|
|
3262
|
-
# RapidKit Go/Gin project launcher \u2014 generated by RapidKit CLI
|
|
3263
|
-
# https://getrapidkit.com
|
|
3264
|
-
|
|
3265
|
-
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
3266
|
-
CMD="\${1:-}"
|
|
3267
|
-
shift 2>/dev/null || true
|
|
3268
|
-
|
|
3269
|
-
case "$CMD" in
|
|
3270
|
-
init)
|
|
3271
|
-
cd "$SCRIPT_DIR"
|
|
3272
|
-
echo "\u{1F439} Initializing Go/Gin project\u2026"
|
|
3273
|
-
GOBIN="$(go env GOPATH)/bin"
|
|
3274
|
-
echo " \u2192 installing air (hot reload)\u2026"
|
|
3275
|
-
go install github.com/air-verse/air@latest 2>/dev/null && echo " \u2713 air" || echo " \u26A0 air install failed (run: go install github.com/air-verse/air@latest)"
|
|
3276
|
-
echo " \u2192 installing swag (swagger)\u2026"
|
|
3277
|
-
go install github.com/swaggo/swag/cmd/swag@latest 2>/dev/null && echo " \u2713 swag" || echo " \u26A0 swag install failed (run: go install github.com/swaggo/swag/cmd/swag@latest)"
|
|
3278
|
-
if [ ! -f ".env" ] && [ -f ".env.example" ]; then
|
|
3279
|
-
cp .env.example .env && echo " \u2713 .env created from .env.example"
|
|
3280
|
-
fi
|
|
3281
|
-
go mod tidy && echo " \u2713 go mod tidy"
|
|
3282
|
-
echo " \u2192 generating swagger docs (first build)\u2026"
|
|
3283
|
-
"$(go env GOPATH)/bin/swag" init -g main.go -d cmd/server,internal/handlers,internal/apierr -o docs --parseDependency 2>/dev/null && echo " \u2713 swagger docs generated" || echo " \u26A0 swagger docs skipped (run: rapidkit docs)"
|
|
3284
|
-
echo "\u2705 Ready \u2014 run: rapidkit dev"
|
|
3285
|
-
;;
|
|
3286
|
-
dev)
|
|
3287
|
-
cd "$SCRIPT_DIR"
|
|
3288
|
-
echo "\u{1F4D6} Syncing swagger docs\u2026"
|
|
3289
|
-
"$(go env GOPATH)/bin/swag" init -g main.go -d cmd/server,internal/handlers,internal/apierr -o docs --parseDependency 2>/dev/null || true
|
|
3290
|
-
if [ -f "$SCRIPT_DIR/Makefile" ]; then
|
|
3291
|
-
exec make -C "$SCRIPT_DIR" dev "$@"
|
|
3292
|
-
else
|
|
3293
|
-
cd "$SCRIPT_DIR" && GIN_MODE=debug exec go run ./cmd/server "$@"
|
|
3294
|
-
fi
|
|
3295
|
-
;;
|
|
3296
|
-
start)
|
|
3297
|
-
BIN="$SCRIPT_DIR/bin/${e.project_name}"
|
|
3298
|
-
if [ ! -f "$BIN" ]; then
|
|
3299
|
-
make -C "$SCRIPT_DIR" build
|
|
3300
|
-
fi
|
|
3301
|
-
exec "$BIN" "$@"
|
|
3302
|
-
;;
|
|
3303
|
-
build)
|
|
3304
|
-
exec make -C "$SCRIPT_DIR" build "$@"
|
|
3305
|
-
;;
|
|
3306
|
-
test)
|
|
3307
|
-
exec make -C "$SCRIPT_DIR" test "$@"
|
|
3308
|
-
;;
|
|
3309
|
-
lint)
|
|
3310
|
-
exec make -C "$SCRIPT_DIR" lint "$@"
|
|
3311
|
-
;;
|
|
3312
|
-
format|fmt)
|
|
3313
|
-
exec make -C "$SCRIPT_DIR" fmt "$@"
|
|
3314
|
-
;;
|
|
3315
|
-
docs)
|
|
3316
|
-
exec make -C "$SCRIPT_DIR" docs "$@"
|
|
3317
|
-
;;
|
|
3318
|
-
help|--help|-h)
|
|
3319
|
-
echo "RapidKit \u2014 Go/Gin project: ${e.project_name}"
|
|
3320
|
-
echo ""
|
|
3321
|
-
echo "Usage: rapidkit <command>"
|
|
3322
|
-
echo ""
|
|
3323
|
-
echo " init Install tools + create .env (air, swag, go mod tidy)"
|
|
3324
|
-
echo " dev Hot reload dev server (make dev \u2014 requires air)"
|
|
3325
|
-
echo " start Run compiled binary (make build + bin)"
|
|
3326
|
-
echo " build Build binary (make build)"
|
|
3327
|
-
echo " docs Generate Swagger docs (make docs \u2014 requires swag)"
|
|
3328
|
-
echo " test Run tests (make test)"
|
|
3329
|
-
echo " lint Run linter (make lint)"
|
|
3330
|
-
echo " format Format code (make fmt)"
|
|
3331
|
-
;;
|
|
3332
|
-
*)
|
|
3333
|
-
if [ -n "$CMD" ]; then
|
|
3334
|
-
echo "rapidkit: unknown command: $CMD" >&2
|
|
3335
|
-
fi
|
|
3336
|
-
echo "Available: init, dev, start, build, docs, test, lint, format" >&2
|
|
3337
|
-
exit 1
|
|
3338
|
-
;;
|
|
3339
|
-
esac
|
|
3340
|
-
`}function dn(e){return `@echo off
|
|
3341
|
-
rem RapidKit Go/Gin project launcher \u2014 Windows
|
|
3342
|
-
set CMD=%1
|
|
3343
|
-
if "%CMD%"=="" goto usage
|
|
3344
|
-
shift
|
|
3345
|
-
|
|
3346
|
-
if "%CMD%"=="init" (
|
|
3347
|
-
echo Initializing Go/Gin project...
|
|
3348
|
-
go install github.com/air-verse/air@latest
|
|
3349
|
-
go install github.com/swaggo/swag/cmd/swag@latest
|
|
3350
|
-
if not exist .env if exist .env.example copy .env.example .env
|
|
3351
|
-
go mod tidy
|
|
3352
|
-
exit /b %ERRORLEVEL%
|
|
3353
|
-
)
|
|
3354
|
-
if "%CMD%"=="dev" ( make dev %* & exit /b %ERRORLEVEL% )
|
|
3355
|
-
if "%CMD%"=="build" ( make build %* & exit /b %ERRORLEVEL% )
|
|
3356
|
-
if "%CMD%"=="test" ( make test %* & exit /b %ERRORLEVEL% )
|
|
3357
|
-
if "%CMD%"=="lint" ( make lint %* & exit /b %ERRORLEVEL% )
|
|
3358
|
-
if "%CMD%"=="format" ( make fmt %* & exit /b %ERRORLEVEL% )
|
|
3359
|
-
if "%CMD%"=="docs" ( make docs %* & exit /b %ERRORLEVEL% )
|
|
3360
|
-
if "%CMD%"=="start" ( bin\\${e.project_name}.exe %* & exit /b %ERRORLEVEL% )
|
|
3361
|
-
|
|
3362
|
-
:usage
|
|
3363
|
-
echo Available: init, dev, start, build, docs, test, lint, format
|
|
3364
|
-
exit /b 1
|
|
3365
|
-
`}async function Bt(e,o){let t={project_name:o.project_name,module_path:o.module_path||o.project_name,author:o.author||"RapidKit User",description:o.description||`Go/Gin REST API \u2014 ${o.project_name}`,go_version:o.go_version||"1.24",app_version:o.app_version||"0.1.0",port:o.port||"8080",skipGit:o.skipGit??false},r=c();try{await execa("go",["version"],{timeout:3e3});}catch{console.log(l.yellow("\n\u26A0 Go not found in PATH \u2014 project will be scaffolded, but `go mod tidy` requires Go 1.21+")),console.log(l.gray(` Install: https://go.dev/dl/
|
|
3366
|
-
`));}let i=Ut(`Generating Go/Gin project: ${t.project_name}\u2026`).start();try{let n=(c,p)=>Ai(h.join(e,c),p),a=h.join(e,"rapidkit"),s=h.join(e,"rapidkit.cmd");await Promise.all([n("cmd/server/main.go",Oi(t)),n("go.mod",Ni(t)),n("internal/config/config.go",ji(t)),n("internal/server/server.go",$i(t)),n("internal/middleware/requestid.go",Ui()),n("internal/middleware/requestid_test.go",Vi(t)),n("internal/apierr/apierr.go",Bi()),n("internal/apierr/apierr_test.go",Ji(t)),n("internal/handlers/health.go",Di()),n("internal/handlers/health_test.go",Gi(t)),n("internal/handlers/example.go",zi(t)),n("internal/handlers/example_test.go",Qi(t)),n("internal/config/config_test.go",Xi(t)),n("internal/middleware/cors.go",Zi()),n("internal/middleware/cors_test.go",en(t)),n("internal/middleware/ratelimit.go",on(t)),n("internal/middleware/ratelimit_test.go",rn(t)),n("internal/server/server_test.go",tn(t)),n("docs/doc.go",Yi(t)),n(".air.toml",nn(t)),n("Dockerfile",Mi()),n("docker-compose.yml",Li(t)),n("Makefile",qi(t)),n(".golangci.yml",sn(t.module_path)),n(".env.example",Fi(t)),n(".gitignore",Hi()),n(".github/workflows/ci.yml",Ki(t)),n("README.md",Wi(t)),n(".rapidkit/project.json",cn(t,r)),n(".rapidkit/context.json",an()),n("rapidkit",ln(t)),n("rapidkit.cmd",dn(t))]),await promises.chmod(a,493),await promises.chmod(s,493),i.succeed(l.green(`Project created at ${e}`));try{i.start("Fetching Go dependencies\u2026"),await execa("go",["mod","tidy"],{cwd:e,timeout:12e4}),i.succeed(l.gray("\u2713 go mod tidy completed"));}catch{i.warn(l.yellow("\u26A0 go mod tidy failed \u2014 run manually: go mod tidy"));}if(!t.skipGit)try{await execa("git",["init"],{cwd:e}),await execa("git",["add","-A"],{cwd:e}),await execa("git",["commit","-m","chore: initial scaffold (rapidkit gogin.standard)"],{cwd:e}),console.log(l.gray("\u2713 git repository initialized"));}catch{console.log(l.gray("\u26A0 git init skipped (git not found or error)"));}console.log(""),console.log(l.bold("\u2705 Go/Gin project ready!")),console.log(""),console.log(l.cyan("Next steps:")),console.log(l.white(` cd ${t.project_name}`)),console.log(l.white(" make run # start dev server")),console.log(l.white(" make test # run tests")),console.log(""),console.log(l.gray("Server will listen on port "+t.port)),console.log(l.gray(" http://localhost:"+t.port+"/api/v1/health/live")),console.log(l.gray(" http://localhost:"+t.port+"/api/v1/health/ready")),console.log(""),console.log(l.yellow("\u2139 RapidKit modules are not available for Go projects (module system uses Python/pip).")),console.log("");}catch(n){throw i.fail(l.red("Failed to generate Go/Gin project")),n}}function un(e){return [...new Set(e.filter(o=>o&&o.trim().length>0))]}function gn(){let e=k().map(r=>h.join(r,a()?"poetry.exe":"poetry")),o=a()?[h.join(process.env.APPDATA||"","Python","Scripts","poetry.exe"),h.join(process.env.USERPROFILE||"","AppData","Roaming","Python","Scripts","poetry.exe")]:[],t=a()?[]:["/usr/local/bin/poetry","/usr/bin/poetry"];return un([...e,...o,...t])}function mn(e){let o=k().map(c=>({location:"Global (user-local)",path:h.join(c,a()?"rapidkit.exe":"rapidkit")})),t=[{location:"Global (pipx)",path:h.join(e,".local","bin","rapidkit")},{location:"Global (pipx)",path:h.join(e,"AppData","Roaming","Python","Scripts","rapidkit.exe")},{location:"Global (pyenv)",path:h.join(e,".pyenv","shims","rapidkit")},{location:"Global (system)",path:"/usr/local/bin/rapidkit"},{location:"Global (system)",path:"/usr/bin/rapidkit"}],r=g(h.join(process.cwd(),".venv")),i=i$1(process.cwd()),n=[{location:"Workspace (.venv)",path:r},...i.map(c=>({location:"Workspace (launcher)",path:c}))],a$1=[...o,...t,...n],s=new Set;return a$1.filter(c=>s.has(c.path)?false:(s.add(c.path),true))}function fn(e){let o=new Map([["Workspace (.venv)",0],["Global (user-local)",1],["Global (pipx)",2],["Global (pyenv)",3],["Global (system)",4]]);return [...e].sort((t,r)=>{let i=o.get(t.location)??Number.MAX_SAFE_INTEGER,n=o.get(r.location)??Number.MAX_SAFE_INTEGER;return i!==n?i-n:t.path.localeCompare(r.path)})}function Je(e,o){return a()?`cd "${e}"; ${o}`:`cd ${e} && ${o}`}function _o(e){return a()?Je(e,"Copy-Item .env.example .env"):Je(e,"cp .env.example .env")}async function Co(e){try{let o=await b__default.stat(e);return `${h.basename(e)}:${o.isDirectory()?"d":"f"}:${o.size}:${o.mtimeMs}`}catch{return `${h.basename(e)}:missing`}}async function hn(e){try{let o=new Set([".git",".venv","node_modules",".rapidkit","dist","build","coverage","__pycache__"]),t=new Set;await Jt(e)&&t.add(e);let r=async(i,n)=>{if(n<0)return;let a=await To(i);for(let s of a){if(Ao(s,o))continue;let c=h.join(i,s);if(await Jt(c)){t.add(c);continue}n>0&&await r(c,n-1);}};return await r(e,1),t.size===0&&(await Pn(e,3,o)).forEach(n=>t.add(n)),Array.from(t).sort((i,n)=>i.localeCompare(n))}catch{return []}}async function yn(e,o){let t=[h.join(e,".rapidkit-workspace"),h.join(e,".rapidkit","workspace.json"),h.join(e,".rapidkit","policies.yml"),h.join(e,".rapidkit","toolchain.lock"),h.join(e,".rapidkit","cache-config.yml")],r=[".rapidkit/project.json",".rapidkit/context.json",".rapidkit/file-hashes.json","package.json","pyproject.toml","go.mod","go.sum","requirements.txt","Dockerfile","Makefile",".env",".env.example","src","modules","tests","test",".venv","node_modules"],i=await Promise.all(t.map(Co)),n=await Promise.all(o.map(async a=>{let s=await Promise.all(r.map(c=>Co(h.join(a,c))));return `${a}::${s.join("|")}`}));return [...i,...n].join("||")}async function wn(e,o){try{if(!await b__default.pathExists(e))return null;let t=await b__default.readJSON(e);return !t||t.signature!==o||!Array.isArray(t.projects)?null:t}catch{return null}}async function vn(e,o){try{await b__default.ensureDir(h.dirname(e)),await b__default.writeJSON(e,o,{spaces:2});}catch{}}async function kn(e,o,t){let r=h.join(e,".rapidkit","reports","doctor-last-run.json");try{return await b__default.ensureDir(h.dirname(r)),await b__default.writeJSON(r,{generatedAt:new Date().toISOString(),workspacePath:e,workspaceName:o.workspaceName,projectScanCached:o.projectScanCached??false,projectScanSignature:o.projectScanSignature,cachePath:t,healthScore:o.healthScore,system:{python:o.python,poetry:o.poetry,pipx:o.pipx,go:o.go,rapidkitCore:o.rapidkitCore,versions:{core:o.coreVersion,npm:o.npmVersion}},projects:o.projects,summary:{totalProjects:o.projects.length,totalIssues:o.projects.reduce((i,n)=>i+n.issues.length,0),hasSystemErrors:[o.python,o.rapidkitCore].some(i=>i.status==="error")}},{spaces:2}),r}catch{return}}async function Io(){let[e,o,t,r,i]=await Promise.all([bn(),Rn(),_n(),Cn(),Sn()]);return {python:e,poetry:o,pipx:t,go:r,rapidkitCore:i}}async function bn(){let e=d$2();for(let o of e)try{let{stdout:t}=await execa(o,["--version"],{timeout:3e3}),r=t.match(/Python (\d+\.\d+\.\d+)/);if(r){let i=r[1],[n,a]=i.split(".").map(Number);return n<3||n===3&&a<10?{status:"warn",message:`Python ${i} (requires 3.10+)`,details:`${o} found but version is below minimum requirement`}:{status:"ok",message:`Python ${i}`,details:`Using ${o}`}}}catch{continue}return {status:"error",message:"Python not found",details:"Install Python 3.10+ and ensure it's in PATH"}}async function Rn(){try{let{stdout:e}=await execa("poetry",["--version"],{timeout:3e3}),o=e.match(/Poetry .*version ([\d.]+)/);return o?{status:"ok",message:`Poetry ${o[1]}`,details:"Available for dependency management"}:{status:"warn",message:"Poetry version unknown"}}catch{let e=d$2().map(o=>({cmd:o,args:o==="py"?["-3","-m","poetry","--version"]:["-m","poetry","--version"]}));for(let o of e)try{let{stdout:t}=await execa(o.cmd,o.args,{timeout:3e3,shell:b$3()}),r=t.match(/Poetry .*version ([\d.]+)/)||t.match(/([\d.]+)/);return {status:"ok",message:r?.[1]?`Poetry ${r[1]}`:"Poetry detected",details:`Available via ${o.cmd} ${o.args.join(" ")}`}}catch{continue}for(let o of gn())try{if(!await b__default.pathExists(o))continue;let{stdout:t}=await execa(o,["--version"],{timeout:3e3,shell:b$3()}),r=t.match(/Poetry .*version ([\d.]+)/)||t.match(/([\d.]+)/);return {status:"ok",message:r?.[1]?`Poetry ${r[1]}`:"Poetry detected",details:`Available at ${o}`}}catch{continue}return {status:"warn",message:"Poetry not installed",details:"Optional: Install for better dependency management"}}}async function _n(){try{let{stdout:e}=await execa("pipx",["--version"],{timeout:3e3});return {status:"ok",message:`pipx ${e.trim()}`,details:"Available for global tool installation"}}catch{let e=d$2();for(let o of e)try{let t=o==="py"?["-3","-m","pipx","--version"]:["-m","pipx","--version"],{stdout:r}=await execa(o,t,{timeout:3e3,shell:b$3()});return {status:"ok",message:`pipx ${r.trim()}`,details:`Available via ${o} ${t.join(" ")}`}}catch{continue}return {status:"warn",message:"pipx not installed",details:"Optional: Install for isolated Python tools"}}}async function Cn(){try{let{stdout:e}=await execa("go",["version"],{timeout:3e3}),o=e.match(/go version go(\d+\.\d+(?:\.\d+)?)/);return o?{status:"ok",message:`Go ${o[1]}`,details:"Available for Go/Fiber and Go/Gin projects"}:{status:"ok",message:"Go (version unknown)",details:"go found in PATH"}}catch{return {status:"warn",message:"Go not installed",details:"Optional: Required only for gofiber.standard / gogin.standard projects \u2014 https://go.dev/dl/"}}}async function Sn(){let e=process.env.HOME||process.env.USERPROFILE||"",o=[],t=mn(e);for(let{location:i,path:n}of t)try{if(await b__default.pathExists(n)){let{stdout:a,exitCode:s}=await execa(n,["--version"],{timeout:3e3,reject:false});if(s===0&&(a.includes("RapidKit Version")||a.includes("RapidKit"))){let c=a.match(/v?([\d.]+(?:rc\d+)?(?:a\d+)?(?:b\d+)?)/);c&&o.push({location:i,path:n,version:c[1]});}}}catch{continue}if(o.length>0){let i=o.filter(a=>a.location!=="Workspace (launcher)");if(i.length>0){let a=fn(i);return {status:"ok",message:`RapidKit Core ${a[0].version}`,paths:a.map(c=>({location:c.location,path:c.path,version:c.version}))}}return {status:"ok",message:`RapidKit Core ${o[0].version}`,details:"Detected via workspace launcher"}}try{let{stdout:i,exitCode:n}=await execa("rapidkit",["--version"],{timeout:3e3,reject:false});if(n===0&&(i.includes("RapidKit Version")||i.includes("RapidKit"))){let a=i.match(/v?([\d.]+(?:rc\d+)?(?:a\d+)?(?:b\d+)?)/);if(a)return {status:"ok",message:`RapidKit Core ${a[1]}`,details:"Available via PATH"}}}catch{}try{let{stdout:i,exitCode:n}=await execa("poetry",["run","rapidkit","--version"],{timeout:3e3,reject:false});if(n===0&&(i.includes("RapidKit Version")||i.includes("RapidKit"))){let a=i.match(/v?([\d.]+(?:rc\d+)?(?:a\d+)?(?:b\d+)?)/);if(a)return {status:"ok",message:`RapidKit Core ${a[1]}`,details:"Available via Poetry"}}}catch{}let r=d$2();for(let i of r)try{let{stdout:n,exitCode:a}=await execa(i,["-c","import rapidkit_core; print(rapidkit_core.__version__)"],{timeout:3e3,reject:false});if(a===0&&n&&!n.includes("Traceback")&&!n.includes("ModuleNotFoundError")){let s=n.trim();if(s)return {status:"ok",message:`RapidKit Core ${s}`,details:`Available in ${i} environment`}}}catch{continue}return {status:"error",message:"RapidKit Core not installed",details:"Install with: pipx install rapidkit-core"}}async function Rt(e,o){let t=h.join(e,"Dockerfile");o.hasDocker=await b__default.pathExists(t);let r=h.join(e,"tests"),i=h.join(e,"test"),n=await b__default.pathExists(r)||await b__default.pathExists(i),a=false;if(o.framework==="Go/Fiber"||o.framework==="Go/Gin")try{let s=[{dir:e,depth:0}],c=4,p=new Set([".git",".venv","node_modules","dist","build","vendor"]);for(;s.length>0&&!a;){let d=s.shift();if(!d)break;let u=[];try{u=await b__default.readdir(d.dir);}catch{continue}for(let m of u){let w=h.join(d.dir,m),f;try{f=await b__default.stat(w);}catch{continue}if(f.isFile()&&m.endsWith("_test.go")){a=true;break}f.isDirectory()&&d.depth<c&&!p.has(m)&&!m.startsWith(".")&&s.push({dir:w,depth:d.depth+1});}}}catch{}if(o.hasTests=n||a,o.framework==="NestJS"){let s=h.join(e,".eslintrc.js"),c=h.join(e,".eslintrc.json");o.hasCodeQuality=await b__default.pathExists(s)||await b__default.pathExists(c);}else if(o.framework==="Go/Fiber"||o.framework==="Go/Gin"){let s=h.join(e,".golangci.yml"),c=h.join(e,".golangci.yaml"),p=h.join(e,"Makefile"),d=await b__default.pathExists(p)&&(await b__default.readFile(p,"utf8")).includes("golangci-lint");o.hasCodeQuality=await b__default.pathExists(s)||await b__default.pathExists(c)||d;}else if(o.framework==="FastAPI"){let s=h.join(e,"ruff.toml"),c=h.join(e,"pyproject.toml");if(await b__default.pathExists(c))try{let p=await b__default.readFile(c,"utf8");o.hasCodeQuality=p.includes("[tool.ruff]")||await b__default.pathExists(s);}catch{o.hasCodeQuality=await b__default.pathExists(s);}}try{if(o.framework==="NestJS"){let{stdout:s}=await execa("npm",["audit","--json"],{cwd:e,reject:false});if(s)try{let p=JSON.parse(s).metadata?.vulnerabilities;p&&(o.vulnerabilities=(p.high||0)+(p.critical||0)+(p.moderate||0));}catch{}}else if(o.framework==="FastAPI"){let s=h.join(e,".venv"),c=f$1(s);if(await b__default.pathExists(c))try{let{stdout:p}=await execa(c,["-m","pip","list","--format=json"],{timeout:5e3,reject:false});if(p){JSON.parse(p);o.vulnerabilities=0;}}catch{}}}catch{}}async function xn(e){let t={name:h.basename(e),path:e,venvActive:false,depsInstalled:false,coreInstalled:false,issues:[],fixCommands:[]},r=h.join(e,".rapidkit");if(!await b__default.pathExists(r))return t.issues.push("Not a valid RapidKit project (missing .rapidkit directory)"),t;try{let u=h.join(e,"registry.json");if(await b__default.pathExists(u)){let m=await b__default.readJson(u);m.installed_modules&&(t.stats={modules:m.installed_modules.length});}}catch{}let i=null;try{let u=h.join(r,"project.json");if(await b__default.pathExists(u)){i=await b__default.readJson(u);let m=i?.kit_name||i?.kit;m&&(t.kit=m);}}catch{}try{let u=h.join(e,".git");if(await b__default.pathExists(u)){let{stdout:m}=await execa("git",["log","-1","--format=%cr"],{cwd:e,reject:false});m&&(t.lastModified=m.trim());}else {let m=await b__default.stat(e),f=Date.now()-m.mtime.getTime(),g=Math.floor(f/(1e3*60*60*24));t.lastModified=g===0?"today":`${g} day${g>1?"s":""} ago`;}}catch{}let n=h.join(e,"package.json"),a=h.join(e,"pyproject.toml"),s=h.join(e,"go.mod");if(await b__default.pathExists(s)||i?.runtime==="go"||typeof i?.kit_name=="string"&&(i.kit_name.startsWith("gofiber")||i.kit_name.startsWith("gogin"))){let u=i?.kit_name??"";t.framework=u.startsWith("gogin")?"Go/Gin":"Go/Fiber",t.isGoProject=true,t.venvActive=true,t.coreInstalled=false;try{await execa("go",["version"],{timeout:3e3});}catch{t.issues.push("Go toolchain not found \u2014 install from https://go.dev/dl/"),t.fixCommands?.push("https://go.dev/dl/");}let m=h.join(e,"go.sum");return await b__default.pathExists(m)?t.depsInstalled=true:(t.depsInstalled=false,t.issues.push("Go dependencies not downloaded (go.sum missing)"),t.fixCommands?.push(Je(e,"go mod tidy"))),await Rt(e,t),t}let p=await b__default.pathExists(n),d=await b__default.pathExists(a);if(p){t.framework="NestJS",t.venvActive=true;let u=h.join(e,"node_modules");if(await b__default.pathExists(u))try{let g=(await b__default.readdir(u)).filter(R=>!R.startsWith(".")&&!R.startsWith("_"));t.depsInstalled=g.length>0;}catch{t.depsInstalled=false;}t.depsInstalled||(t.issues.push("Dependencies not installed (node_modules empty or missing)"),t.fixCommands?.push(Je(e,"rapidkit init"))),t.coreInstalled=false;let m=h.join(e,".env");if(t.hasEnvFile=await b__default.pathExists(m),!t.hasEnvFile){let f=h.join(e,".env.example");await b__default.pathExists(f)&&(t.issues.push("Environment file missing (found .env.example)"),t.fixCommands?.push(_o(e)));}let w=h.join(e,"src");if(t.modulesHealthy=true,t.missingModules=[],await b__default.pathExists(w))try{let f=await b__default.readdir(w);t.modulesHealthy=f.length>0;}catch{t.modulesHealthy=false;}return await Rt(e,t),t}if(d){t.framework="FastAPI";let u=h.join(e,".venv");if(await b__default.pathExists(u)){t.venvActive=true;let g=f$1(u);if(await b__default.pathExists(g)){try{let{stdout:R}=await execa(g,["-c","import rapidkit_core; print(rapidkit_core.__version__)"],{timeout:2e3});t.coreInstalled=true,t.coreVersion=R.trim();}catch{t.coreInstalled=false;}try{await execa(g,["-c","import fastapi"],{timeout:2e3}),t.depsInstalled=true;}catch{try{let R=h.join(u,"lib");if(await b__default.pathExists(R)){let T=(await b__default.readdir(R)).find(C=>C.startsWith("python"));if(T){let C=h.join(R,T,"site-packages");if(await b__default.pathExists(C)){let G=(await b__default.readdir(C)).filter(N=>!N.startsWith("_")&&!N.includes("dist-info")&&!["pip","setuptools","wheel","pkg_resources"].includes(N));t.depsInstalled=G.length>0;}}}t.depsInstalled||(t.issues.push("Dependencies not installed"),t.fixCommands?.push(Je(e,"rapidkit init")));}catch{t.issues.push("Could not verify dependency installation");}}}else t.issues.push("Virtual environment exists but Python executable not found");}else t.issues.push("Virtual environment not created"),t.fixCommands?.push(Je(e,"rapidkit init"));let m=h.join(e,".env");if(t.hasEnvFile=await b__default.pathExists(m),!t.hasEnvFile){let g=h.join(e,".env.example");await b__default.pathExists(g)&&(t.issues.push("Environment file missing (found .env.example)"),t.fixCommands?.push(_o(e)));}let w=h.join(e,"src"),f=h.join(e,"modules");if(t.modulesHealthy=true,t.missingModules=[],await b__default.pathExists(w)){let g=h.join(w,"__init__.py");await b__default.pathExists(g)||(t.modulesHealthy=false,t.missingModules.push("src/__init__.py"));}if(await b__default.pathExists(f))try{let g=await To(f);for(let R of g){let x=h.join(f,R,"__init__.py");await b__default.pathExists(x)||(t.modulesHealthy=false,t.missingModules.push(`modules/${R}/__init__.py`));}}catch{}return !t.modulesHealthy&&t.missingModules.length>0&&t.issues.push(`Missing module init files: ${t.missingModules.join(", ")}`),await Rt(e,t),t}return t.issues.push("Unknown project type (no package.json or pyproject.toml)"),await Rt(e,t),t}async function To(e){try{return (await b__default.readdir(e,{withFileTypes:true})).filter(t=>t.isDirectory()).map(t=>t.name)}catch{try{let o=await b__default.readdir(e),t=[];for(let r of o)try{(await b__default.stat(h.join(e,r))).isDirectory()&&t.push(r);}catch{continue}return t}catch{return []}}}async function Jt(e){let o=h.join(e,".rapidkit");if(!await b__default.pathExists(o))return false;let t=["project.json","context.json","file-hashes.json"];for(let r of t)if(await b__default.pathExists(h.join(o,r)))return true;return false}function Ao(e,o){if(o.has(e))return true;let t=e.toLowerCase();return !!(t==="dist"||t.startsWith("dist-")||t.startsWith("dist_")||t==="build"||t.startsWith("build-")||t.startsWith("build_"))}async function Pn(e,o,t){let r=new Set,i=[{dir:e,depth:0}];for(;i.length>0;){let n=i.shift();if(!n)break;try{let a=await b__default.readdir(n.dir);for(let s of a){if(Ao(s,t))continue;let c=h.join(n.dir,s),p;try{p=await b__default.stat(c);}catch{continue}if(p.isDirectory()){if(await Jt(c)){r.add(c);continue}n.depth<o&&i.push({dir:c,depth:n.depth+1});}}}catch{continue}}return Array.from(r)}async function So(e){let o=e,t=h.parse(o).root;for(;o!==t;){let r=[h.join(o,".rapidkit-workspace"),h.join(o,".rapidkit","workspace-marker.json"),h.join(o,".rapidkit","config.json")];for(let i of r)if(await b__default.pathExists(i))return o;o=h.dirname(o);}return null}function En(e,o){let t=0,r=0,i=0;return e.forEach(a=>{a.status==="ok"?t++:a.status==="warn"?r++:a.status==="error"&&i++;}),o.forEach(a=>{(a.isGoProject?a.issues.length===0&&a.depsInstalled:a.issues.length===0&&a.venvActive&&a.depsInstalled)?t++:a.issues.length>0&&r++;}),{total:t+r+i,passed:t,warnings:r,errors:i}}async function xo(e,o=true){let t=h.basename(e);try{let d=h.join(e,".rapidkit-workspace");await b__default.pathExists(d)&&(t=(await b__default.readJSON(d)).name||t);}catch{try{let d=h.join(e,".rapidkit","config.json");t=(await b__default.readJSON(d)).workspace_name||t;}catch{}}let[r,i]=await Promise.all([Io(),hn(e)]),n={workspacePath:e,workspaceName:t,python:r.python,poetry:r.poetry,pipx:r.pipx,go:r.go,rapidkitCore:r.rapidkitCore,projects:[]};a$1.debug(`Workspace scan found ${i.length} project(s)`);let a=await yn(e,i),s=h.join(e,".rapidkit","reports","doctor-workspace-cache.json"),c=o?await wn(s,a):null;if(c)n.projects=c.projects,n.projectScanCached=true,a$1.debug(`Workspace project health cache hit: ${s}`);else try{let d=await Promise.all(i.map(u=>xn(u)));n.projects=d,n.projectScanCached=false,await vn(s,{signature:a,generatedAt:new Date().toISOString(),projects:d}),a$1.debug(`Workspace project health cache refreshed: ${s}`);}catch(d){a$1.debug(`Failed to scan workspace projects: ${d}`);}n.projectScanSignature=a,n.projectScanCachePath=s;let p=[n.python,n.poetry,n.pipx,n.go,n.rapidkitCore];if(n.healthScore=En(p,n.projects),n.rapidkitCore.status==="ok"){let d=n.rapidkitCore.message.match(/([\d.]+(?:rc\d+)?(?:a\d+)?(?:b\d+)?)/);d&&(n.coreVersion=d[1]);}return n.evidencePath=await kn(e,n,c?s:null),n}function Ce(e,o){let t=e.status==="ok"?"\u2705":e.status==="warn"?"\u26A0\uFE0F":"\u274C",r=e.status==="ok"?l.green:e.status==="warn"?l.yellow:l.red;console.log(`${t} ${l.bold(o)}: ${r(e.message)}`),e.paths&&e.paths.length>0?e.paths.forEach(i=>{let n=i.version?l.cyan(` -> ${i.version}`):"";console.log(` ${l.cyan("\u2022")} ${l.gray(i.location)}: ${l.dim(i.path)}${n}`);}):e.details&&console.log(` ${l.gray(e.details)}`);}function In(e){let o=e.issues.length>0,t=o?"\u26A0\uFE0F":"\u2705",r=o?l.yellow:l.green;if(console.log(`
|
|
3367
|
-
${t} ${l.bold("Project")}: ${r(e.name)}`),e.framework){let c=e.framework==="FastAPI"?"\u{1F40D}":e.framework==="NestJS"?"\u{1F985}":e.framework==="Go/Fiber"||e.framework==="Go/Gin"?"\u{1F439}":"\u{1F4E6}";console.log(` ${c} Framework: ${l.cyan(e.framework)}${e.kit?l.gray(` (${e.kit})`):""}`);}console.log(` ${l.gray(`Path: ${e.path}`)}`);let i=e.isGoProject===true,n=!i&&e.venvActive&&!e.coreInstalled;if(!i&&!n&&(e.venvActive?console.log(` \u2705 Virtual environment: ${l.green("Active")}`):console.log(` \u274C Virtual environment: ${l.red("Not found")}`),e.coreInstalled?console.log(` ${l.dim("\u2139")} RapidKit Core: ${l.gray(e.coreVersion||"In venv")} ${l.dim("(optional)")}`):console.log(` ${l.dim("\u2139")} RapidKit Core: ${l.gray("Using global installation")} ${l.dim("(recommended)")}`)),e.depsInstalled?console.log(` \u2705 Dependencies: ${l.green("Installed")}`):console.log(` \u26A0\uFE0F Dependencies: ${l.yellow("Not installed")}`),e.hasEnvFile!==void 0&&(e.hasEnvFile?console.log(` \u2705 Environment: ${l.green(".env configured")}`):console.log(` \u26A0\uFE0F Environment: ${l.yellow(".env missing")}`)),e.modulesHealthy!==void 0&&(e.modulesHealthy?console.log(` \u2705 Modules: ${l.green("Healthy")}`):e.missingModules&&e.missingModules.length>0&&console.log(` \u26A0\uFE0F Modules: ${l.yellow(`Missing ${e.missingModules.length} init file(s)`)}`)),e.stats){let c=[];e.stats.modules!==void 0&&c.push(`${e.stats.modules} module${e.stats.modules!==1?"s":""}`),c.length>0&&console.log(` \u{1F4CA} Stats: ${l.cyan(c.join(" \u2022 "))}`);}e.lastModified&&console.log(` \u{1F552} Last Modified: ${l.gray(e.lastModified)}`);let s=[];if(e.hasTests!==void 0&&s.push(e.hasTests?"\u2705 Tests":l.dim("\u2298 No tests")),e.hasDocker!==void 0&&s.push(e.hasDocker?"\u2705 Docker":l.dim("\u2298 No Docker")),e.hasCodeQuality!==void 0){let c=e.framework==="NestJS"?"ESLint":e.framework==="Go/Fiber"||e.framework==="Go/Gin"?"golangci-lint":"Ruff";s.push(e.hasCodeQuality?`\u2705 ${c}`:l.dim(`\u2298 No ${c}`));}s.length>0&&console.log(` ${s.join(" \u2022 ")}`),e.vulnerabilities!==void 0&&e.vulnerabilities>0&&console.log(` \u26A0\uFE0F Security: ${l.yellow(`${e.vulnerabilities} vulnerability(ies) found`)}`),e.issues.length>0&&(console.log(` ${l.bold("Issues:")}`),e.issues.forEach(c=>{console.log(` \u2022 ${l.yellow(c)}`);}),e.fixCommands&&e.fixCommands.length>0&&(console.log(`
|
|
3368
|
-
${l.bold.cyan("\u{1F527} Quick Fix:")}`),e.fixCommands.forEach(c=>{console.log(` ${l.cyan("$")} ${l.white(c)}`);})));}async function Po(){try{return (await execa("go",["version"],{timeout:3e3,reject:false})).exitCode===0}catch{return false}}async function Eo(e,o=false){let t=e.filter(p=>p.fixCommands&&p.fixCommands.length>0),r=null;if(t.length===0){console.log(l.green(`
|
|
3369
|
-
\u2705 No fixes needed - all projects are healthy!`));return}console.log(l.bold.cyan(`
|
|
3370
|
-
\u{1F527} Available Fixes:
|
|
3371
|
-
`));for(let p of t)console.log(l.bold(`Project: ${l.yellow(p.name)}`)),p.fixCommands.forEach((d,u)=>{console.log(` ${u+1}. ${l.cyan(d)}`);}),console.log();let i=0;for(let p of t)for(let d of p.fixCommands){if(/^https?:\/\//i.test(d.trim()))continue;if(s(d,"cp\\s+\\.env\\.example\\s+\\.env")||s(d,"copy-item\\s+\\.env\\.example\\s+\\.env")||s(d,"rapidkit\\s+init")){i+=1;continue}if(s(d,"go\\s+mod\\s+tidy")){r===null&&(r=await Po()),r&&(i+=1);continue}i+=1;}if(i===0){console.log(l.gray("\u{1F4A1} No automatic fixes can be applied right now.")),r===false&&console.log(l.gray(" Install Go to enable go mod tidy fixes, then rerun `rapidkit doctor workspace --fix`."));return}if(!o){console.log(l.gray('\u{1F4A1} Run "npx rapidkit doctor workspace --fix" to apply fixes automatically'));return}let{confirm:n}=await xe.prompt([{type:"confirm",name:"confirm",message:`Apply ${t.reduce((p,d)=>p+d.fixCommands.length,0)} fix(es)?`,default:false}]);if(!n){console.log(l.yellow(`
|
|
3372
|
-
\u26A0\uFE0F Fixes cancelled by user`));return}console.log(l.bold.cyan(`
|
|
3373
|
-
\u{1F680} Applying fixes...
|
|
3374
|
-
`));let a=p=>/^https?:\/\//i.test(p.trim());function s(p,d){let u=[new RegExp(`^cd\\s+"([^"]+)"\\s*(?:&&|;)\\s*${d}\\s*$`,"i"),new RegExp(`^cd\\s+'([^']+)'\\s*(?:&&|;)\\s*${d}\\s*$`,"i"),new RegExp(`^cd\\s+(.+?)\\s*(?:&&|;)\\s*${d}\\s*$`,"i")];for(let m of u){let w=p.match(m);if(w?.[1])return {projectPath:w[1].trim()}}return null}function c(p){return s(p,"cp\\s+\\.env\\.example\\s+\\.env")||s(p,"copy-item\\s+\\.env\\.example\\s+\\.env")}for(let p of t){console.log(l.bold(`Fixing ${l.cyan(p.name)}...`));for(let d of p.fixCommands)try{if(console.log(l.gray(` $ ${d}`)),a(d)){console.log(l.yellow(` \u2139 Manual action required: open ${d}`)),console.log(l.green(` \u2705 Recorded as guidance
|
|
3375
|
-
`));continue}let u=c(d);if(u){let f=h.join(u.projectPath,".env.example"),g=h.join(u.projectPath,".env");if(!await b__default.pathExists(f))throw new Error(`.env.example not found at ${f}`);if(await b__default.pathExists(g)){console.log(l.green(` \u2705 .env already exists
|
|
3376
|
-
`));continue}await b__default.copy(f,g,{overwrite:false,errorOnExist:false}),console.log(l.green(` \u2705 Success
|
|
3377
|
-
`));continue}let m=s(d,"rapidkit\\s+init");if(m){await execa("rapidkit",["init"],{cwd:m.projectPath,shell:b$3(),stdio:"inherit"}),console.log(l.green(` \u2705 Success
|
|
3378
|
-
`));continue}let w=s(d,"go\\s+mod\\s+tidy");if(w){if(r===null&&(r=await Po()),!r){console.log(l.yellow(" \u26A0 Go toolchain is not installed \u2014 skipping go mod tidy; install Go to apply this fix.")),console.log(l.green(` \u2705 Recorded as guidance
|
|
3379
|
-
`));continue}await execa("go",["mod","tidy"],{cwd:w.projectPath,shell:b$3(),stdio:"inherit"}),console.log(l.green(` \u2705 Success
|
|
3380
|
-
`));continue}await execa(d,{shell:true,stdio:"inherit"}),console.log(l.green(` \u2705 Success
|
|
3381
|
-
`));}catch(u){console.log(l.red(` \u274C Failed: ${u instanceof Error?u.message:String(u)}
|
|
3382
|
-
`));}}console.log(l.bold.green(`
|
|
3383
|
-
\u2705 Fix process completed!`));}async function Oo(e={}){let o=!e.workspace&&e.fix?await So(process.cwd()):null,t=e.workspace||!!o;if(e.json||console.log(l.bold.cyan(`
|
|
3384
|
-
\u{1FA7A} RapidKit Health Check
|
|
3385
|
-
`)),t){let r=o??await So(process.cwd());r||(a$1.error("No RapidKit workspace found in current directory or parents"),a$1.info('Run this command from within a workspace, or use "rapidkit doctor" for system check'),process.exit(1)),e.json||(o&&console.log(l.gray("\u2139\uFE0F Detected workspace context; enabling workspace checks for --fix")),console.log(l.bold(`Workspace: ${l.cyan(h.basename(r))}`)),console.log(l.gray(`Path: ${r}`)));let i=await xo(r);if(e.json||(i.projectScanCached&&console.log(l.gray(`\u2139\uFE0F Reused cached project scan${i.projectScanCachePath?` (${h.basename(i.projectScanCachePath)})`:""}`)),i.evidencePath&&console.log(l.gray(`\u2139\uFE0F Evidence saved: ${i.evidencePath}`))),e.json){let s={workspace:{name:h.basename(r),path:r},cache:{projectScan:i.projectScanCached??false,projectScanPath:i.projectScanCachePath,evidencePath:i.evidencePath},healthScore:i.healthScore,system:{python:i.python,poetry:i.poetry,pipx:i.pipx,rapidkitCore:i.rapidkitCore,versions:{core:i.coreVersion,npm:i.npmVersion}},projects:i.projects.map(c=>({name:c.name,path:c.path,venvActive:c.venvActive,depsInstalled:c.depsInstalled,coreInstalled:c.coreInstalled,coreVersion:c.coreVersion,issues:c.issues,fixCommands:c.fixCommands})),summary:{totalProjects:i.projects.length,totalIssues:i.projects.reduce((c,p)=>c+p.issues.length,0),hasSystemErrors:[i.python,i.rapidkitCore].some(c=>c.status==="error")}};console.log(JSON.stringify(s,null,2));return}if(i.healthScore){let s=i.healthScore,c=Math.round(s.passed/s.total*100),p=c>=80?l.green:c>=50?l.yellow:l.red,d="\u2588".repeat(Math.floor(c/5))+"\u2591".repeat(20-Math.floor(c/5));console.log(l.bold(`
|
|
3386
|
-
\u{1F4CA} Health Score:`)),console.log(` ${p(`${c}%`)} ${l.gray(d)}`),console.log(` ${l.green(`\u2705 ${s.passed} passed`)} ${l.gray("|")} ${l.yellow(`\u26A0\uFE0F ${s.warnings} warnings`)} ${l.gray("|")} ${l.red(`\u274C ${s.errors} errors`)}`);}if(console.log(l.bold(`
|
|
3387
|
-
|
|
3388
|
-
System Tools:
|
|
3389
|
-
`)),Ce(i.python,"Python"),Ce(i.poetry,"Poetry"),Ce(i.pipx,"pipx"),Ce(i.go,"Go"),Ce(i.rapidkitCore,"RapidKit Core"),i.coreVersion&&i.npmVersion){let s=i.coreVersion.split(".")[1],c=i.npmVersion.split(".")[1];s!==c&&(console.log(l.yellow(`
|
|
3390
|
-
\u26A0\uFE0F Version mismatch: Core ${i.coreVersion} / CLI ${i.npmVersion}`)),console.log(l.gray(" Consider updating to matching versions for best compatibility")));}i.projects.length>0?(console.log(l.bold(`
|
|
3391
|
-
\u{1F4E6} Projects (${i.projects.length}):`)),i.projects.forEach(s=>In(s))):(console.log(l.bold(`
|
|
3392
|
-
\u{1F4E6} Projects:`)),console.log(l.gray(" No RapidKit projects found in workspace")));let n=i.projects.reduce((s,c)=>s+c.issues.length,0),a=[i.python,i.rapidkitCore].some(s=>s.status==="error");if(a||n>0)if(console.log(l.bold.yellow(`
|
|
3393
|
-
\u26A0\uFE0F Found ${n} project issue(s)`)),a&&console.log(l.bold.red("\u274C System requirements not met")),e.fix){if(await Eo(i.projects,true),!e.json){let s=await xo(r,false),c=s.projects.reduce((d,u)=>d+u.issues.length,0),p=[s.python,s.rapidkitCore].some(d=>d.status==="error");p||c>0?(console.log(l.bold.yellow(`
|
|
3394
|
-
\u26A0\uFE0F Post-fix verification found ${c} remaining issue(s)`)),p&&console.log(l.bold.red("\u274C System requirements still not met"))):console.log(l.bold.green(`
|
|
3395
|
-
\u2705 Post-fix verification passed. Workspace is healthy.`)),s.projectScanCached&&console.log(l.gray(`\u2139\uFE0F Reused cached project scan${s.projectScanCachePath?` (${h.basename(s.projectScanCachePath)})`:""}`)),s.evidencePath&&console.log(l.gray(`\u2139\uFE0F Evidence refreshed: ${s.evidencePath}`));}}else n>0&&await Eo(i.projects,false);else console.log(l.bold.green(`
|
|
3396
|
-
\u2705 All checks passed! Workspace is healthy.`));}else {console.log(l.bold(`System Tools:
|
|
3397
|
-
`));let r=await Io(),i=r.python,n=r.poetry,a=r.pipx,s=r.go,c=r.rapidkitCore;Ce(i,"Python"),Ce(n,"Poetry"),Ce(a,"pipx"),Ce(s,"Go"),Ce(c,"RapidKit Core"),[i,c].some(d=>d.status==="error")?(console.log(l.bold.red(`
|
|
3398
|
-
\u274C Some required tools are missing`)),e.fix&&console.log(l.gray(`
|
|
3399
|
-
Tip: Project auto-fix runs in workspace mode. Run from a workspace and use "rapidkit doctor workspace --fix"`)),console.log(l.gray(`
|
|
3400
|
-
Tip: Run "rapidkit doctor workspace" for detailed project checks`))):(console.log(l.bold.green(`
|
|
3401
|
-
\u2705 All required tools are installed!`)),e.fix&&console.log(l.gray(`
|
|
3402
|
-
Tip: Project auto-fix runs in workspace mode. Run from a workspace and use "rapidkit doctor workspace --fix"`)),console.log(l.gray(`
|
|
3403
|
-
Tip: Run "rapidkit doctor workspace" for detailed project checks`)));}console.log("");}var Yt=h.join(Tn.homedir(),".rapidkit"),_t=h.join(Yt,"config.json");function Ye(){try{if(!_.existsSync(_t))return {};let e=_.readFileSync(_t,"utf-8");return JSON.parse(e)}catch{return {}}}function Ct(e){let t={...Ye(),...e};_.existsSync(Yt)||_.mkdirSync(Yt,{recursive:true}),_.writeFileSync(_t,JSON.stringify(t,null,2),"utf-8");}function nt(){return process.env.OPENAI_API_KEY||Ye().openaiApiKey||null}function zt(){return Ye().aiEnabled!==false}function Qt(){return _t}async function jo(){return (await import('inquirer')).default}function $o(e){let o=e.command("config").description("Configure RapidKit settings");o.command("set-api-key").description("Set OpenAI API key for AI features").option("--key <key>","API key (or enter interactively)").action(async t=>{let r=t.key;r?r.startsWith("sk-")||(console.log(l.red(`
|
|
2
|
+
import {e as e$1,d,c as c$1,h as h$1,a as a$4}from'./chunk-UOGFCKQ5.js';import {a as a$2,b as b$1,c,e,j}from'./chunk-ZAZJEYYT.js';import {a,h,i,c as c$2,d as d$1,b as b$2,f as f$1}from'./chunk-Z5LKRG57.js';import {b,a as a$3}from'./chunk-Q7ULIFQA.js';import {a as a$1}from'./chunk-VM2TOHNX.js';import {Command,Option}from'commander';import s from'chalk';import he from'inquirer';import f from'path';import {fileURLToPath}from'url';import {exec,spawn}from'child_process';import zo from'validate-npm-package-name';import*as x from'fs-extra';import x__default from'fs-extra';import w,{promises,createWriteStream}from'fs';import {execa}from'execa';import rr from'os';import {promisify}from'util';import kr from'ora';import {createVerify,createHash,createHmac}from'crypto';import bo from'http';import Po from'https';function vt(t){let e=zo(t);if(!e.validForNewPackages){let r=e.errors||[],n=e.warnings||[],i=[...r,...n];throw new j(t,`NPM validation failed: ${i.join(", ")}`)}if(!/^[a-z][a-z0-9_-]*$/.test(t))throw new j(t,"Must start with a lowercase letter and contain only lowercase letters, numbers, hyphens, and underscores");if(["test","tests","src","dist","build","lib","python","pip","poetry","node","npm","rapidkit","rapidkit"].includes(t.toLowerCase()))throw new j(t,`"${t}" is a reserved name. Please choose a different name.`);if(t.length<2)throw new j(t,"Name must be at least 2 characters long");if(t.length>214)throw new j(t,"Name must be less than 214 characters");return true}function Zo(){return d$1()}function Xo(t,e){return t==="py"?["-3",...e]:e}function er(t){return typeof t=="object"&&t!==null}async function tr(t,e,o,r=8e3){try{let n=await execa(t,e,{cwd:o,timeout:r,reject:false,stdio:"pipe"});return {ok:n.exitCode===0,exitCode:n.exitCode,stdout:n.stdout,stderr:n.stderr}}catch(n){return {ok:false,exitCode:void 0,stdout:"",stderr:n instanceof Error?n.message:String(n)}}}async function or(t,e){let o=["-m","rapidkit",...t],r=Zo();for(let n of r){let i=await tr(n,Xo(n,o),e?.cwd,e?.timeoutMs);if(!i.ok)continue;let a=(i.stdout??"").trim();try{let c=JSON.parse(a);return er(c)?{ok:true,command:n,exitCode:i.exitCode,stdout:i.stdout,stderr:i.stderr,data:c}:{ok:false,command:n,exitCode:i.exitCode,stdout:i.stdout,stderr:i.stderr}}catch{return {ok:false,command:n,exitCode:i.exitCode,stdout:i.stdout,stderr:i.stderr}}}return {ok:false}}async function Ut(t,e){let o=await or(["project","detect","--path",t,"--json"],e);return !o.ok||!o.data||o.data.schema_version!==1?{ok:false,command:o.command,exitCode:o.exitCode,stdout:o.stdout,stderr:o.stderr}:o}var bt=f.join(rr.homedir(),".rapidkit"),et=f.join(bt,"config.json");function Te(){try{if(!w.existsSync(et))return {};let t=w.readFileSync(et,"utf-8");return JSON.parse(t)}catch{return {}}}function tt(t){let o={...Te(),...t};w.existsSync(bt)||w.mkdirSync(bt,{recursive:true}),w.writeFileSync(et,JSON.stringify(o,null,2),"utf-8");}function Fe(){return process.env.OPENAI_API_KEY||Te().openaiApiKey||null}function Pt(){return Te().aiEnabled!==false}function Ct(){return et}async function Jt(){return (await import('inquirer')).default}function Vt(t){let e=t.command("config").description("Configure RapidKit settings");e.command("set-api-key").description("Set OpenAI API key for AI features").option("--key <key>","API key (or enter interactively)").action(async o=>{let r=o.key;r?r.startsWith("sk-")||(console.log(s.red(`
|
|
3404
3
|
\u274C Invalid API key format (should start with sk-)
|
|
3405
|
-
`)),process.exit(1)):r=(await(await
|
|
4
|
+
`)),process.exit(1)):r=(await(await Jt()).prompt([{type:"password",name:"apiKey",message:"Enter your OpenAI API key:",validate:a=>a?a.startsWith("sk-")?a.length<20?"API key seems too short":true:"Invalid API key format (should start with sk-)":"API key is required"}])).apiKey,tt({openaiApiKey:r}),console.log(s.green(`
|
|
3406
5
|
\u2705 OpenAI API key saved successfully!
|
|
3407
|
-
`)),console.log(
|
|
3408
|
-
\u{1F389} You can now use AI features:`)),console.log(
|
|
3409
|
-
\u{1F4A1} To generate module embeddings (one-time):`)),console.log(
|
|
3410
|
-
`));}),
|
|
6
|
+
`)),console.log(s.gray(`Stored in: ${Ct()}`)),console.log(s.cyan(`
|
|
7
|
+
\u{1F389} You can now use AI features:`)),console.log(s.white(' rapidkit ai recommend "I need user authentication"')),console.log(s.gray(`
|
|
8
|
+
\u{1F4A1} To generate module embeddings (one-time):`)),console.log(s.white(" cd rapidkit-npm")),console.log(s.white(` npx tsx src/ai/generate-embeddings.ts
|
|
9
|
+
`));}),e.command("show").description("Show current configuration").action(()=>{let o=Te();if(console.log(s.bold(`
|
|
3411
10
|
\u2699\uFE0F RapidKit Configuration
|
|
3412
|
-
`)),
|
|
3413
|
-
\u{1F4C1} Config file: ${
|
|
3414
|
-
`));}),
|
|
11
|
+
`)),o.openaiApiKey){let r=o.openaiApiKey.substring(0,8)+"..."+o.openaiApiKey.slice(-4);console.log(s.cyan("OpenAI API Key:"),s.white(r));}else console.log(s.cyan("OpenAI API Key:"),s.red("Not set")),console.log(s.gray(" Set with: rapidkit config set-api-key"));console.log(s.cyan("AI Features:"),o.aiEnabled!==false?s.green("Enabled"):s.red("Disabled")),console.log(s.gray(`
|
|
12
|
+
\u{1F4C1} Config file: ${Ct()}
|
|
13
|
+
`));}),e.command("remove-api-key").description("Remove stored OpenAI API key").action(async()=>{if(!Te().openaiApiKey){console.log(s.yellow(`
|
|
3415
14
|
\u26A0\uFE0F No API key is currently stored
|
|
3416
|
-
`));return}(await(await
|
|
15
|
+
`));return}(await(await Jt()).prompt([{type:"confirm",name:"confirm",message:"Are you sure you want to remove your OpenAI API key?",default:false}])).confirm?(tt({openaiApiKey:void 0}),console.log(s.green(`
|
|
3417
16
|
\u2705 API key removed successfully
|
|
3418
|
-
`))):console.log(
|
|
17
|
+
`))):console.log(s.gray(`
|
|
3419
18
|
Cancelled
|
|
3420
|
-
`));}),
|
|
3421
|
-
\u274C Invalid action: ${
|
|
3422
|
-
`)),process.exit(1));let r=
|
|
19
|
+
`));}),e.command("ai <action>").description("Enable or disable AI features (enable|disable)").action(o=>{o!=="enable"&&o!=="disable"&&(console.log(s.red(`
|
|
20
|
+
\u274C Invalid action: ${o}`)),console.log(s.gray(`Use: rapidkit config ai enable|disable
|
|
21
|
+
`)),process.exit(1));let r=o==="enable";tt({aiEnabled:r}),console.log(s.green(`
|
|
3423
22
|
\u2705 AI features ${r?"enabled":"disabled"}
|
|
3424
|
-
`));});}var St=null,xt=false,Xt=null;async function An(){return Xt||(Xt=(await import('openai')).default),Xt}function Do(){xt=true;}function Go(e){let t=new Array(1536),r=0;for(let n=0;n<e.length;n++)r=(r<<5)-r+e.charCodeAt(n),r=r&r;for(let n=0;n<1536;n++)r=r*1664525+1013904223&4294967295,t[n]=r/4294967295*2-1;let i=Math.sqrt(t.reduce((n,a)=>n+a*a,0));return t.map(n=>n/i)}async function Pt(e){let o=await An();St=new o({apiKey:e});}function Mo(){if(!St)throw new Error("OpenAI client not initialized. Call initOpenAI() first with your API key.");return St}async function Lo(e){return xt?Go(e):(await Mo().embeddings.create({model:"text-embedding-3-small",input:e,encoding_format:"float"})).data[0].embedding}async function qo(e){return xt?e.map(Go):(await Mo().embeddings.create({model:"text-embedding-3-small",input:e,encoding_format:"float"})).data.map(r=>r.embedding)}function Fo(){return St!==null}function Ho(){return xt}var jn=promisify(exec),Wo=[{id:"authentication-core",name:"Authentication Core",category:"auth",description:"Complete authentication system with password hashing, JWT tokens, OAuth 2.0, and secure session management",longDescription:"Production-ready authentication with bcrypt password hashing, JWT access/refresh tokens, OAuth 2.0 providers (Google, GitHub, etc), rate limiting, and security best practices.",keywords:["auth","login","password","jwt","oauth","token","authentication","security","signin","signup"],framework:"both",dependencies:[],useCases:["User login and logout","Password reset flow","OAuth social login (Google, GitHub)","JWT authentication","Secure session management","Token refresh","Rate limiting"]},{id:"users-core",name:"Users Core",category:"auth",description:"User management system with profiles, roles, permissions, and user CRUD operations",longDescription:"Complete user management with user profiles, role-based access control (RBAC), permissions, user search, soft delete, and audit trails.",keywords:["user","profile","role","permission","rbac","management","admin","accounts"],framework:"both",dependencies:["authentication-core"],useCases:["User registration","User profile management","Role management (admin, user, etc)","Permission system","User administration dashboard","Soft delete users"]},{id:"session-management",name:"Session Management",category:"auth",description:"Secure session handling with Redis storage, session rotation, and device tracking",longDescription:"Advanced session management with Redis-backed storage, automatic session rotation, device fingerprinting, IP tracking, and session revocation.",keywords:["session","redis","cookie","storage","device","tracking"],framework:"both",dependencies:["authentication-core","redis-cache"],useCases:["User session management","Remember me functionality","Device tracking","Session security","Logout from all devices","Session expiration"]},{id:"db-postgres",name:"PostgreSQL",category:"database",description:"PostgreSQL integration with async SQLAlchemy, migrations, connection pooling, and query optimization",longDescription:"Production-ready PostgreSQL with async SQLAlchemy 2.0, Alembic migrations, connection pooling, query optimization, JSON support, and full-text search.",keywords:["postgres","postgresql","database","sql","sqlalchemy","migration","orm","relational"],framework:"both",dependencies:[],useCases:["Relational database","Complex SQL queries","Database transactions","Data integrity","Production-grade database","ACID compliance"]},{id:"db-mongodb",name:"MongoDB",category:"database",description:"MongoDB integration with Motor async driver, schema validation, and aggregation pipelines",longDescription:"Async MongoDB with Motor driver, Pydantic schema validation, aggregation pipelines, indexes, and Atlas integration.",keywords:["mongodb","mongo","nosql","document","database","motor"],framework:"both",dependencies:[],useCases:["Document storage","Flexible schema","Real-time data","JSON documents","Unstructured data","Analytics"]},{id:"stripe-payment",name:"Stripe Payment",category:"payment",description:"Stripe integration with payment intents, subscriptions, webhooks, and customer portal",longDescription:"Complete Stripe integration with Payment Intents API, subscription management, automatic webhooks, customer portal, refunds, and SCA compliance.",keywords:["stripe","payment","subscription","billing","checkout","webhook","credit card"],framework:"both",dependencies:[],useCases:["Accept credit card payments","Subscription billing","One-time payments","Checkout flow","Payment webhooks","Refunds and disputes"]},{id:"email",name:"Email",category:"communication",description:"Email sending with templates, SMTP/SendGrid/AWS SES support, and queue management",longDescription:"Production email system with Jinja2 templates, multiple providers (SMTP, SendGrid, AWS SES), queue management, retry logic, and bounce handling.",keywords:["email","mail","smtp","sendgrid","ses","template","notification"],framework:"both",dependencies:[],useCases:["Welcome emails","Password reset emails","Notifications","Marketing emails","Transactional emails","Email templates"]},{id:"sms",name:"SMS",category:"communication",description:"SMS sending with Twilio, verification codes, and delivery tracking",longDescription:"SMS integration with Twilio, verification codes, two-factor authentication, delivery tracking, and international support.",keywords:["sms","twilio","text","message","2fa","verification","otp"],framework:"both",dependencies:[],useCases:["2FA verification codes","SMS notifications","Phone verification","OTP generation","SMS alerts"]},{id:"redis-cache",name:"Redis Cache",category:"infrastructure",description:"Redis caching with decorators, TTL management, and cache invalidation patterns",longDescription:"Redis integration with async client, caching decorators, TTL management, cache invalidation, pub/sub, and rate limiting.",keywords:["redis","cache","memory","performance","speed","pubsub"],framework:"both",dependencies:[],useCases:["API response caching","Session storage","Rate limiting","Real-time features","Performance optimization","Pub/sub messaging"]},{id:"celery",name:"Celery",category:"infrastructure",description:"Background task processing with Celery, periodic tasks, and monitoring",longDescription:"Celery task queue with Redis/RabbitMQ backend, periodic tasks (cron), task monitoring, retry logic, and failure handling.",keywords:["celery","task","background","queue","async","worker","job","cron"],framework:"fastapi",dependencies:["redis-cache"],useCases:["Background email sending","Data processing","Report generation","Scheduled tasks","Long-running jobs"]},{id:"storage",name:"Storage",category:"infrastructure",description:"File storage with S3, local filesystem, and image processing",longDescription:"Unified storage interface for AWS S3, local files, image resizing, format conversion, CDN integration, and presigned URLs.",keywords:["storage","s3","file","upload","image","cdn","aws"],framework:"both",dependencies:[],useCases:["File uploads","Image storage","Document management","Profile pictures","Media files","CDN integration"]}],ze=null,Ko=0,$n=300*1e3;function Dn(e){return {id:e.name||e.id||e.module_id||"",name:e.display_name||e.name||"",category:Gn(e.category||"infrastructure"),description:e.description||e.summary||"",longDescription:e.long_description||e.description||"",keywords:e.keywords||e.tags||[],framework:Mn(e.framework),dependencies:e.dependencies||[],useCases:e.use_cases||e.useCases||[]}}function Gn(e){return {auth:"auth",authentication:"auth",database:"database",payment:"payment",billing:"payment",communication:"communication",infrastructure:"infrastructure",security:"security",analytics:"analytics"}[e.toLowerCase()]||"infrastructure"}function Mn(e){if(!e)return "both";if(typeof e=="string"){if(e.toLowerCase().includes("fastapi"))return "fastapi";if(e.toLowerCase().includes("nest"))return "nestjs"}return "both"}async function Ln(){try{let{stdout:e}=await jn("rapidkit modules list --json-schema 1",{timeout:1e4,maxBuffer:10485760}),o=e.match(/\{[\s\S]*\}/),t=o?o[0]:e,r=JSON.parse(t),i=[];return Array.isArray(r)?i=r:r.modules&&Array.isArray(r.modules)?i=r.modules:r.data&&Array.isArray(r.data)&&(i=r.data),i.map(Dn).filter(n=>n.id&&n.name)}catch(e){return e.code==="ENOENT"?console.warn("\u26A0\uFE0F RapidKit Python Core not found in PATH"):e.killed?console.warn("\u26A0\uFE0F Python Core command timed out"):console.warn("\u26A0\uFE0F Failed to fetch modules from Python Core:",e.message),console.warn(" Using fallback module catalog (11 modules)"),Wo}}async function Et(){let e=Date.now();return ze&&e-Ko<$n||(ze=await Ln(),Ko=e,ze.length===0&&(console.warn("\u26A0\uFE0F No modules found, using fallback catalog"),ze=Wo)),ze}var Fn=fileURLToPath(import.meta.url),Vo=h.dirname(Fn),st=null;function Hn(){if(st)return st;let e=[h.join(Vo,"../../data/modules-embeddings.json"),h.join(Vo,"../data/modules-embeddings.json"),h.join(process.cwd(),"data/modules-embeddings.json")],o=null;for(let i of e)if(_.existsSync(i)){o=i;break}if(!o)throw new Error("embeddings file not found");let t=_.readFileSync(o,"utf-8"),r=JSON.parse(t);return Array.isArray(r)?st={model:"mock-or-text-embedding-3-small",dimension:r[0]?.embedding?.length||1536,generated_at:new Date().toISOString(),modules:r}:st=r,st}function Kn(e,o){if(e.length!==o.length)throw new Error("Vectors must have the same length");let t=0,r=0,i=0;for(let a=0;a<e.length;a++)t+=e[a]*o[a],r+=e[a]*e[a],i+=o[a]*o[a];let n=Math.sqrt(r)*Math.sqrt(i);return n===0?0:t/n}function Wn(e,o){let t=o.toLowerCase(),r=e.keywords.filter(i=>t.includes(i)||i.includes(t));return r.length>0?`Matches: ${r.slice(0,3).join(", ")}`:`Relevant for: ${e.useCases[0]}`}async function Bo(e,o=5){let t=Hn(),r=await Et(),i=await Lo(e),n=t.modules.map(a=>{let s=r.find(p=>p.id===a.id);if(!s)return null;let c=Kn(i,a.embedding);return {module:s,score:c,reason:Wn(s,e)}}).filter(a=>a!==null);return n.sort((a,s)=>s.score-a.score),n.slice(0,o)}var Bn=fileURLToPath(import.meta.url),Jo=h.dirname(Bn);async function Yo(){return (await import('inquirer')).default}function Jn(){return [h.join(Jo,"../../data/modules-embeddings.json"),h.join(Jo,"../data/modules-embeddings.json"),h.join(process.cwd(),"data/modules-embeddings.json")]}function zo(){let e=Jn();for(let o of e)if(_.existsSync(o))try{let t=JSON.parse(_.readFileSync(o,"utf-8")),r=Array.isArray(t)?t:t.modules||[];return {exists:true,path:o,moduleCount:r.length,generatedAt:t.generated_at||null}}catch{continue}return {exists:false,path:null,moduleCount:0,generatedAt:null}}async function Tt(e=true,o){try{if(!Fo()&&!Ho())return console.log(l.red(`
|
|
3425
|
-
\u274C OpenAI not initialized`)),console.log(
|
|
3426
|
-
`)),false;console.log(
|
|
23
|
+
`));});}var ot=null,rt=false,_t=null;async function nr(){return _t||(_t=(await import('openai')).default),_t}function Bt(){rt=true;}function Yt(t){let o=new Array(1536),r=0;for(let i=0;i<t.length;i++)r=(r<<5)-r+t.charCodeAt(i),r=r&r;for(let i=0;i<1536;i++)r=r*1664525+1013904223&4294967295,o[i]=r/4294967295*2-1;let n=Math.sqrt(o.reduce((i,a)=>i+a*a,0));return o.map(i=>i/n)}async function nt(t){let e=await nr();ot=new e({apiKey:t});}function zt(){if(!ot)throw new Error("OpenAI client not initialized. Call initOpenAI() first with your API key.");return ot}async function Qt(t){return rt?Yt(t):(await zt().embeddings.create({model:"text-embedding-3-small",input:t,encoding_format:"float"})).data[0].embedding}async function Zt(t){return rt?t.map(Yt):(await zt().embeddings.create({model:"text-embedding-3-small",input:t,encoding_format:"float"})).data.map(r=>r.embedding)}function Xt(){return ot!==null}function eo(){return rt}var ar=promisify(exec),oo=[{id:"authentication-core",name:"Authentication Core",category:"auth",description:"Complete authentication system with password hashing, JWT tokens, OAuth 2.0, and secure session management",longDescription:"Production-ready authentication with bcrypt password hashing, JWT access/refresh tokens, OAuth 2.0 providers (Google, GitHub, etc), rate limiting, and security best practices.",keywords:["auth","login","password","jwt","oauth","token","authentication","security","signin","signup"],framework:"both",dependencies:[],useCases:["User login and logout","Password reset flow","OAuth social login (Google, GitHub)","JWT authentication","Secure session management","Token refresh","Rate limiting"]},{id:"users-core",name:"Users Core",category:"auth",description:"User management system with profiles, roles, permissions, and user CRUD operations",longDescription:"Complete user management with user profiles, role-based access control (RBAC), permissions, user search, soft delete, and audit trails.",keywords:["user","profile","role","permission","rbac","management","admin","accounts"],framework:"both",dependencies:["authentication-core"],useCases:["User registration","User profile management","Role management (admin, user, etc)","Permission system","User administration dashboard","Soft delete users"]},{id:"session-management",name:"Session Management",category:"auth",description:"Secure session handling with Redis storage, session rotation, and device tracking",longDescription:"Advanced session management with Redis-backed storage, automatic session rotation, device fingerprinting, IP tracking, and session revocation.",keywords:["session","redis","cookie","storage","device","tracking"],framework:"both",dependencies:["authentication-core","redis-cache"],useCases:["User session management","Remember me functionality","Device tracking","Session security","Logout from all devices","Session expiration"]},{id:"db-postgres",name:"PostgreSQL",category:"database",description:"PostgreSQL integration with async SQLAlchemy, migrations, connection pooling, and query optimization",longDescription:"Production-ready PostgreSQL with async SQLAlchemy 2.0, Alembic migrations, connection pooling, query optimization, JSON support, and full-text search.",keywords:["postgres","postgresql","database","sql","sqlalchemy","migration","orm","relational"],framework:"both",dependencies:[],useCases:["Relational database","Complex SQL queries","Database transactions","Data integrity","Production-grade database","ACID compliance"]},{id:"db-mongodb",name:"MongoDB",category:"database",description:"MongoDB integration with Motor async driver, schema validation, and aggregation pipelines",longDescription:"Async MongoDB with Motor driver, Pydantic schema validation, aggregation pipelines, indexes, and Atlas integration.",keywords:["mongodb","mongo","nosql","document","database","motor"],framework:"both",dependencies:[],useCases:["Document storage","Flexible schema","Real-time data","JSON documents","Unstructured data","Analytics"]},{id:"stripe-payment",name:"Stripe Payment",category:"payment",description:"Stripe integration with payment intents, subscriptions, webhooks, and customer portal",longDescription:"Complete Stripe integration with Payment Intents API, subscription management, automatic webhooks, customer portal, refunds, and SCA compliance.",keywords:["stripe","payment","subscription","billing","checkout","webhook","credit card"],framework:"both",dependencies:[],useCases:["Accept credit card payments","Subscription billing","One-time payments","Checkout flow","Payment webhooks","Refunds and disputes"]},{id:"email",name:"Email",category:"communication",description:"Email sending with templates, SMTP/SendGrid/AWS SES support, and queue management",longDescription:"Production email system with Jinja2 templates, multiple providers (SMTP, SendGrid, AWS SES), queue management, retry logic, and bounce handling.",keywords:["email","mail","smtp","sendgrid","ses","template","notification"],framework:"both",dependencies:[],useCases:["Welcome emails","Password reset emails","Notifications","Marketing emails","Transactional emails","Email templates"]},{id:"sms",name:"SMS",category:"communication",description:"SMS sending with Twilio, verification codes, and delivery tracking",longDescription:"SMS integration with Twilio, verification codes, two-factor authentication, delivery tracking, and international support.",keywords:["sms","twilio","text","message","2fa","verification","otp"],framework:"both",dependencies:[],useCases:["2FA verification codes","SMS notifications","Phone verification","OTP generation","SMS alerts"]},{id:"redis-cache",name:"Redis Cache",category:"infrastructure",description:"Redis caching with decorators, TTL management, and cache invalidation patterns",longDescription:"Redis integration with async client, caching decorators, TTL management, cache invalidation, pub/sub, and rate limiting.",keywords:["redis","cache","memory","performance","speed","pubsub"],framework:"both",dependencies:[],useCases:["API response caching","Session storage","Rate limiting","Real-time features","Performance optimization","Pub/sub messaging"]},{id:"celery",name:"Celery",category:"infrastructure",description:"Background task processing with Celery, periodic tasks, and monitoring",longDescription:"Celery task queue with Redis/RabbitMQ backend, periodic tasks (cron), task monitoring, retry logic, and failure handling.",keywords:["celery","task","background","queue","async","worker","job","cron"],framework:"fastapi",dependencies:["redis-cache"],useCases:["Background email sending","Data processing","Report generation","Scheduled tasks","Long-running jobs"]},{id:"storage",name:"Storage",category:"infrastructure",description:"File storage with S3, local filesystem, and image processing",longDescription:"Unified storage interface for AWS S3, local files, image resizing, format conversion, CDN integration, and presigned URLs.",keywords:["storage","s3","file","upload","image","cdn","aws"],framework:"both",dependencies:[],useCases:["File uploads","Image storage","Document management","Profile pictures","Media files","CDN integration"]}],Oe=null,to=0,cr=300*1e3;function lr(t){return {id:t.name||t.id||t.module_id||"",name:t.display_name||t.name||"",category:dr(t.category||"infrastructure"),description:t.description||t.summary||"",longDescription:t.long_description||t.description||"",keywords:t.keywords||t.tags||[],framework:pr(t.framework),dependencies:t.dependencies||[],useCases:t.use_cases||t.useCases||[]}}function dr(t){return {auth:"auth",authentication:"auth",database:"database",payment:"payment",billing:"payment",communication:"communication",infrastructure:"infrastructure",security:"security",analytics:"analytics"}[t.toLowerCase()]||"infrastructure"}function pr(t){if(!t)return "both";if(typeof t=="string"){if(t.toLowerCase().includes("fastapi"))return "fastapi";if(t.toLowerCase().includes("nest"))return "nestjs"}return "both"}async function ur(){try{let{stdout:t}=await ar("rapidkit modules list --json-schema 1",{timeout:1e4,maxBuffer:10485760}),e=t.match(/\{[\s\S]*\}/),o=e?e[0]:t,r=JSON.parse(o),n=[];return Array.isArray(r)?n=r:r.modules&&Array.isArray(r.modules)?n=r.modules:r.data&&Array.isArray(r.data)&&(n=r.data),n.map(lr).filter(i=>i.id&&i.name)}catch(t){return t.code==="ENOENT"?console.warn("\u26A0\uFE0F RapidKit Python Core not found in PATH"):t.killed?console.warn("\u26A0\uFE0F Python Core command timed out"):console.warn("\u26A0\uFE0F Failed to fetch modules from Python Core:",t.message),console.warn(" Using fallback module catalog (11 modules)"),oo}}async function it(){let t=Date.now();return Oe&&t-to<cr||(Oe=await ur(),to=t,Oe.length===0&&(console.warn("\u26A0\uFE0F No modules found, using fallback catalog"),Oe=oo)),Oe}var fr=fileURLToPath(import.meta.url),no=f.dirname(fr),We=null;function gr(){if(We)return We;let t=[f.join(no,"../../data/modules-embeddings.json"),f.join(no,"../data/modules-embeddings.json"),f.join(process.cwd(),"data/modules-embeddings.json")],e=null;for(let n of t)if(w.existsSync(n)){e=n;break}if(!e)throw new Error("embeddings file not found");let o=w.readFileSync(e,"utf-8"),r=JSON.parse(o);return Array.isArray(r)?We={model:"mock-or-text-embedding-3-small",dimension:r[0]?.embedding?.length||1536,generated_at:new Date().toISOString(),modules:r}:We=r,We}function yr(t,e){if(t.length!==e.length)throw new Error("Vectors must have the same length");let o=0,r=0,n=0;for(let a=0;a<t.length;a++)o+=t[a]*e[a],r+=t[a]*t[a],n+=e[a]*e[a];let i=Math.sqrt(r)*Math.sqrt(n);return i===0?0:o/i}function hr(t,e){let o=e.toLowerCase(),r=t.keywords.filter(n=>o.includes(n)||n.includes(o));return r.length>0?`Matches: ${r.slice(0,3).join(", ")}`:`Relevant for: ${t.useCases[0]}`}async function io(t,e=5){let o=gr(),r=await it(),n=await Qt(t),i=o.modules.map(a=>{let c=r.find(d=>d.id===a.id);if(!c)return null;let m=yr(n,a.embedding);return {module:c,score:m,reason:hr(c,t)}}).filter(a=>a!==null);return i.sort((a,c)=>c.score-a.score),i.slice(0,e)}var vr=fileURLToPath(import.meta.url),so=f.dirname(vr);async function ao(){return (await import('inquirer')).default}function br(){return [f.join(so,"../../data/modules-embeddings.json"),f.join(so,"../data/modules-embeddings.json"),f.join(process.cwd(),"data/modules-embeddings.json")]}function co(){let t=br();for(let e of t)if(w.existsSync(e))try{let o=JSON.parse(w.readFileSync(e,"utf-8")),r=Array.isArray(o)?o:o.modules||[];return {exists:true,path:e,moduleCount:r.length,generatedAt:o.generated_at||null}}catch{continue}return {exists:false,path:null,moduleCount:0,generatedAt:null}}async function at(t=true,e){try{if(!Xt()&&!eo())return console.log(s.red(`
|
|
24
|
+
\u274C OpenAI not initialized`)),console.log(s.yellow("Please set your API key:")),console.log(s.white(" rapidkit config set-api-key")),console.log(s.gray(` OR set: export OPENAI_API_KEY="sk-..."
|
|
25
|
+
`)),false;console.log(s.blue(`
|
|
3427
26
|
\u{1F916} Generating AI embeddings for RapidKit modules...
|
|
3428
|
-
`)),console.log(
|
|
3429
|
-
`));let r=
|
|
3430
|
-
`)),
|
|
27
|
+
`)),console.log(s.gray("\u{1F4E1} Fetching modules from RapidKit..."));let o=await it();console.log(s.green(`\u2713 Found ${o.length} modules
|
|
28
|
+
`));let r=o.length*50/1e6*.02;if(console.log(s.cyan(`\u{1F4B0} Estimated cost: ~$${r.toFixed(3)}`)),console.log(s.gray(` (Based on ${o.length} modules at $0.02/1M tokens)
|
|
29
|
+
`)),t){let a=await ao(),{confirm:c}=await a.prompt([{type:"confirm",name:"confirm",message:"Generate embeddings now?",default:true}]);if(!c)return console.log(s.yellow(`
|
|
3431
30
|
\u26A0\uFE0F Embeddings generation cancelled
|
|
3432
|
-
`)),false}let
|
|
3433
|
-
\u2705 Embeddings generated successfully!`)),console.log(
|
|
3434
|
-
`)),true}catch(a){return
|
|
3435
|
-
\u274C OpenAI API quota exceeded`)),console.log(
|
|
3436
|
-
`))):a.message?.includes("401")?(console.log(
|
|
3437
|
-
\u274C Invalid API key`)),console.log(
|
|
3438
|
-
`))):console.log(
|
|
31
|
+
`)),false}let n=o.map(a=>`${a.name}. ${a.description}. ${a.longDescription}. Keywords: ${a.keywords.join(", ")}. Use cases: ${a.useCases.join(", ")}.`),i=kr(`Generating embeddings for ${o.length} modules...`).start();try{let a=await Zt(n);i.succeed(`Generated embeddings for ${o.length} modules`);let c={model:"text-embedding-3-small",dimension:a[0].length,generated_at:new Date().toISOString(),modules:o.map((l,u)=>({id:l.id,name:l.name,embedding:a[u]}))},m=e||f.join(process.cwd(),"data","modules-embeddings.json"),d=f.dirname(m);return w.existsSync(d)||w.mkdirSync(d,{recursive:true}),w.writeFileSync(m,JSON.stringify(c,null,2)),console.log(s.green(`
|
|
32
|
+
\u2705 Embeddings generated successfully!`)),console.log(s.gray(`\u{1F4C1} Saved to: ${m}`)),console.log(s.gray(`\u{1F4CA} Size: ${o.length} modules, ${a[0].length} dimensions
|
|
33
|
+
`)),true}catch(a){return i.fail("Failed to generate embeddings"),a.message?.includes("429")?(console.log(s.red(`
|
|
34
|
+
\u274C OpenAI API quota exceeded`)),console.log(s.yellow(`Please check your billing: https://platform.openai.com/account/billing
|
|
35
|
+
`))):a.message?.includes("401")?(console.log(s.red(`
|
|
36
|
+
\u274C Invalid API key`)),console.log(s.yellow("Please set a valid API key:")),console.log(s.white(` rapidkit config set-api-key
|
|
37
|
+
`))):console.log(s.red(`
|
|
3439
38
|
\u274C Error: ${a.message}
|
|
3440
|
-
`)),false}}catch(
|
|
3441
|
-
\u274C Failed to generate embeddings: ${
|
|
3442
|
-
`)),false}}async function
|
|
3443
|
-
\u26A0\uFE0F Module embeddings not found`)),console.log(
|
|
3444
|
-
`)),!
|
|
3445
|
-
`)),false;let
|
|
39
|
+
`)),false}}catch(o){return console.log(s.red(`
|
|
40
|
+
\u274C Failed to generate embeddings: ${o.message}
|
|
41
|
+
`)),false}}async function lo(t=true){if(co().exists)return true;if(console.log(s.yellow(`
|
|
42
|
+
\u26A0\uFE0F Module embeddings not found`)),console.log(s.gray(`AI recommendations require embeddings to be generated.
|
|
43
|
+
`)),!t)return console.log(s.red("\u274C Cannot generate embeddings in non-interactive mode")),console.log(s.white(`Run: rapidkit ai generate-embeddings
|
|
44
|
+
`)),false;let o=await ao(),{action:r}=await o.prompt([{type:"list",name:"action",message:"What would you like to do?",choices:[{name:"\u{1F680} Generate embeddings now (requires OpenAI API key)",value:"generate"},{name:"\u{1F4DD} Show me how to generate them manually",value:"manual"},{name:"\u274C Cancel",value:"cancel"}]}]);return r==="generate"?await at(true):(r==="manual"&&(console.log(s.cyan(`
|
|
3446
45
|
\u{1F4DD} To generate embeddings manually:
|
|
3447
|
-
`)),console.log(
|
|
3448
|
-
`)),console.log(
|
|
3449
|
-
`)),console.log(
|
|
3450
|
-
`))),false)}async function
|
|
3451
|
-
\u{1F504} Updating embeddings...`)),console.log(
|
|
3452
|
-
`)),await
|
|
3453
|
-
\u26A0\uFE0F No existing embeddings found`)),console.log(
|
|
3454
|
-
`)),false)}async function
|
|
3455
|
-
\u26A0\uFE0F AI features are disabled`)),console.log(
|
|
3456
|
-
`)),process.exit(1));let
|
|
46
|
+
`)),console.log(s.white("1. Get OpenAI API key from: https://platform.openai.com/api-keys")),console.log(s.white("2. Set the API key:")),console.log(s.gray(" rapidkit config set-api-key")),console.log(s.gray(` OR: export OPENAI_API_KEY="sk-..."
|
|
47
|
+
`)),console.log(s.white("3. Generate embeddings:")),console.log(s.gray(` rapidkit ai generate-embeddings
|
|
48
|
+
`)),console.log(s.cyan(`\u{1F4B0} Cost: ~$0.50 one-time
|
|
49
|
+
`))),false)}async function po(){let t=co();return t.exists?(console.log(s.blue(`
|
|
50
|
+
\u{1F504} Updating embeddings...`)),console.log(s.gray(`Current: ${t.moduleCount} modules`)),console.log(s.gray(`Generated: ${t.generatedAt||"unknown"}
|
|
51
|
+
`)),await at(true,t.path)):(console.log(s.yellow(`
|
|
52
|
+
\u26A0\uFE0F No existing embeddings found`)),console.log(s.gray(`Use: rapidkit ai generate-embeddings
|
|
53
|
+
`)),false)}async function uo(){return (await import('inquirer')).default}function mo(t){let e=t.command("ai").description("AI-powered features");e.command("recommend").description("Get AI-powered module recommendations").argument("[query]",'What do you want to build? (e.g., "user authentication with email")').option("-n, --number <count>","Number of recommendations","5").option("--json","Output as JSON").action(async(o,r)=>{try{Pt()||(console.log(s.yellow(`
|
|
54
|
+
\u26A0\uFE0F AI features are disabled`)),console.log(s.gray(`Enable with: rapidkit config ai enable
|
|
55
|
+
`)),process.exit(1));let n=Fe();n?await nt(n):(console.log(s.yellow(`
|
|
3457
56
|
\u26A0\uFE0F OpenAI API key not configured - using MOCK MODE for testing
|
|
3458
|
-
`)),console.log(
|
|
3459
|
-
`)),console.log(
|
|
3460
|
-
`)),
|
|
57
|
+
`)),console.log(s.gray("\u{1F4DD} Note: Mock embeddings provide approximate results for testing.")),console.log(s.gray(` For production, configure your OpenAI API key:
|
|
58
|
+
`)),console.log(s.white(" 1. Get your key from: https://platform.openai.com/api-keys")),console.log(s.white(" 2. Configure it: rapidkit config set-api-key")),console.log(s.gray(` OR set: export OPENAI_API_KEY="sk-proj-..."
|
|
59
|
+
`)),Bt());let i=o;i||(i=(await(await uo()).prompt([{type:"input",name:"query",message:"\u{1F916} What do you want to build?",validate:p=>p.length===0?"Please enter a description":p.length<3?"Please be more specific (at least 3 characters)":true}])).query),r.json||console.log(s.blue(`
|
|
3461
60
|
\u{1F916} Analyzing your request...
|
|
3462
|
-
`)),await
|
|
61
|
+
`)),await lo(!r.json)||(console.log(s.yellow(`
|
|
3463
62
|
\u26A0\uFE0F Cannot proceed without embeddings
|
|
3464
|
-
`)),process.exit(1));let
|
|
63
|
+
`)),process.exit(1));let c=parseInt(r.number,10),m=await io(i,c);if(m.length===0||m[0].score<.3)if(console.log(s.yellow(`
|
|
3465
64
|
\u26A0\uFE0F No matching modules found in RapidKit registry.
|
|
3466
|
-
`)),console.log(
|
|
3467
|
-
`)),console.log(
|
|
3468
|
-
`)),console.log(
|
|
3469
|
-
`)),console.log(
|
|
3470
|
-
`)),
|
|
3471
|
-
`));else return;if(r.json){console.log(JSON.stringify({query:
|
|
3472
|
-
`)),
|
|
3473
|
-
`));let
|
|
3474
|
-
\u{1F4E6} Installing ${
|
|
3475
|
-
`)),console.log(
|
|
3476
|
-
\u26A0\uFE0F Note: Module installation not yet implemented`)),console.log(
|
|
3477
|
-
`))):console.log(
|
|
65
|
+
`)),console.log(s.cyan(`\u{1F4A1} Options:
|
|
66
|
+
`)),console.log(s.white("1. Create custom module:")),console.log(s.gray(" rapidkit modules scaffold <name> --category <category>")),console.log(s.gray(` Example: rapidkit modules scaffold blockchain-integration --category integrations
|
|
67
|
+
`)),console.log(s.white("2. Search with different keywords")),console.log(s.gray(` Try more general terms (e.g., "storage" instead of "blockchain")
|
|
68
|
+
`)),console.log(s.white("3. Request feature:")),console.log(s.gray(` https://github.com/getrapidkit/rapidkit/issues
|
|
69
|
+
`)),m.length>0)console.log(s.yellow(`\u26A0\uFE0F Low confidence matches found:
|
|
70
|
+
`));else return;if(r.json){console.log(JSON.stringify({query:i,recommendations:m},null,2));return}console.log(s.green.bold(`\u{1F4E6} Recommended Modules:
|
|
71
|
+
`)),m.forEach((y,v)=>{let p=(y.score*100).toFixed(1),g=y.score>.8?" \u2B50":"";console.log(s.bold(`${v+1}. ${y.module.name}${g}`)),console.log(s.gray(` ${y.module.description}`)),console.log(s.cyan(` Match: ${p}%`)+s.gray(` - ${y.reason}`)),console.log(s.yellow(` Category: ${y.module.category}`)),y.module.dependencies.length>0&&console.log(s.magenta(` Requires: ${y.module.dependencies.join(", ")}`)),console.log();});let d=m.slice(0,3).map(y=>y.module.id);console.log(s.cyan("\u{1F4A1} Quick install (top 3):")),console.log(s.white(` rapidkit add module ${d.join(" ")}
|
|
72
|
+
`));let l=await uo(),{shouldInstall:u}=await l.prompt([{type:"confirm",name:"shouldInstall",message:"Would you like to install these modules now?",default:false}]);if(u){let{selectedModules:y}=await l.prompt([{type:"checkbox",name:"selectedModules",message:"Select modules to install:",choices:m.map(v=>({name:`${v.module.name} - ${v.module.description}`,value:v.module.id,checked:v.score>.7}))}]);y.length>0?(console.log(s.blue(`
|
|
73
|
+
\u{1F4E6} Installing ${y.length} modules...
|
|
74
|
+
`)),console.log(s.gray(`Command: rapidkit add module ${y.join(" ")}`)),console.log(s.yellow(`
|
|
75
|
+
\u26A0\uFE0F Note: Module installation not yet implemented`)),console.log(s.gray(`Coming soon in next version!
|
|
76
|
+
`))):console.log(s.gray(`
|
|
3478
77
|
No modules selected
|
|
3479
|
-
`));}}catch(
|
|
3480
|
-
\u274C Error:`,
|
|
3481
|
-
\u{1F4A1} Your API key may be invalid or expired`)),console.log(
|
|
3482
|
-
`))):
|
|
3483
|
-
\u{1F4A1} Module embeddings not generated yet`)),console.log(
|
|
3484
|
-
`))),process.exit(1);}}),
|
|
78
|
+
`));}}catch(n){a$1.error(`
|
|
79
|
+
\u274C Error:`,n.message),n.code==="invalid_api_key"?(console.log(s.yellow(`
|
|
80
|
+
\u{1F4A1} Your API key may be invalid or expired`)),console.log(s.cyan(` Update it: rapidkit config set-api-key
|
|
81
|
+
`))):n.message.includes("embeddings file not found")&&(console.log(s.yellow(`
|
|
82
|
+
\u{1F4A1} Module embeddings not generated yet`)),console.log(s.cyan(" Generate them (one-time):")),console.log(s.white(" cd rapidkit-npm")),console.log(s.white(' export OPENAI_API_KEY="sk-proj-..."')),console.log(s.white(` npx tsx src/ai/generate-embeddings.ts
|
|
83
|
+
`))),process.exit(1);}}),e.command("info").description("Show AI features information").action(()=>{let o=Fe(),r=Pt();console.log(s.bold(`
|
|
3485
84
|
\u{1F916} RapidKit AI Features
|
|
3486
|
-
`)),console.log(
|
|
85
|
+
`)),console.log(s.cyan("Status:"),r?s.green("Enabled"):s.red("Disabled")),console.log(s.cyan("API Key:"),o?s.green("Configured \u2713"):s.red("Not configured \u2717")),console.log(s.bold(`
|
|
3487
86
|
\u{1F4E6} Available Features:
|
|
3488
|
-
`)),console.log(
|
|
87
|
+
`)),console.log(s.white("\u2022 Module Recommender")+s.gray(" - AI-powered module suggestions")),console.log(s.gray(' Usage: rapidkit ai recommend "I need authentication"')),console.log(s.bold(`
|
|
3489
88
|
\u{1F4B0} Pricing:
|
|
3490
|
-
`)),console.log(
|
|
89
|
+
`)),console.log(s.white("\u2022 Per query: ~$0.0002")+s.gray(" (practically free)")),console.log(s.white("\u2022 100 queries: ~$0.02")+s.gray(" (2 cents)")),console.log(s.white("\u2022 1000 queries: ~$0.20")+s.gray(" (20 cents)")),console.log(s.bold(`
|
|
3491
90
|
\u{1F680} Getting Started:
|
|
3492
|
-
`)),
|
|
91
|
+
`)),o?(console.log(s.green("\u2713 You're all set!")),console.log(s.white(' Try: rapidkit ai recommend "user authentication"'))):(console.log(s.white("1. Get OpenAI API key: https://platform.openai.com/api-keys")),console.log(s.white("2. Configure: rapidkit config set-api-key")),console.log(s.white('3. Try: rapidkit ai recommend "user authentication"'))),console.log();}),e.command("generate-embeddings").description("Generate AI embeddings for all modules (one-time setup)").option("--force","Force regeneration even if embeddings exist").action(async()=>{try{let o=Fe();o||(console.log(s.red(`
|
|
3493
92
|
\u274C OpenAI API key not configured
|
|
3494
|
-
`)),console.log(
|
|
3495
|
-
`)),console.log(
|
|
3496
|
-
OR set environment variable:`)),console.log(
|
|
3497
|
-
`)),process.exit(1)),
|
|
3498
|
-
`))),process.exit(r?0:1);}catch(
|
|
93
|
+
`)),console.log(s.cyan(`To generate embeddings, you need an OpenAI API key:
|
|
94
|
+
`)),console.log(s.white("1. Get your key from: https://platform.openai.com/api-keys")),console.log(s.white("2. Configure it: rapidkit config set-api-key")),console.log(s.gray(`
|
|
95
|
+
OR set environment variable:`)),console.log(s.white(` export OPENAI_API_KEY="sk-proj-..."
|
|
96
|
+
`)),process.exit(1)),nt(o);let r=await at(true);r&&(console.log(s.green("\u2705 Ready to use AI recommendations!")),console.log(s.cyan(`Try: rapidkit ai recommend "authentication"
|
|
97
|
+
`))),process.exit(r?0:1);}catch(o){a$1.error("Failed to generate embeddings:",o.message),process.exit(1);}}),e.command("update-embeddings").description("Update existing embeddings with latest modules").action(async()=>{try{let o=Fe();o||(console.log(s.red(`
|
|
3499
98
|
\u274C OpenAI API key not configured
|
|
3500
|
-
`)),console.log(
|
|
3501
|
-
`)),process.exit(1)),Pt(t);let r=await Xo();process.exit(r?0:1);}catch(t){a$1.error("Failed to update embeddings:",t.message),process.exit(1);}});}var At=class{constructor(o){this.runCommand=o;}runtime="go";async run(o,t,r){return {exitCode:await this.runCommand(o,t,r)}}async ensureGoInstalled(o){return (await this.run("go",["version"],o)).exitCode===0?null:{exitCode:1,message:"Go toolchain is not installed or not available on PATH. Install Go from https://go.dev/dl/ and retry."}}findWorkspaceRoot(o){let t=o;for(;;){if(_.existsSync(h.join(t,".rapidkit-workspace")))return t;let r=h.dirname(t);if(r===t)break;t=r;}return null}resolveDependencyMode(o){let t=process.env.RAPIDKIT_DEP_SHARING_MODE?.toLowerCase();if(t==="shared-runtime-caches"||t==="shared-node-deps"||t==="isolated")return t;let r=this.findWorkspaceRoot(o);if(!r)return "isolated";let i=h.join(r,".rapidkit","policies.yml");if(!_.existsSync(i))return "isolated";try{let s=_.readFileSync(i,"utf-8").match(/^\s*dependency_sharing_mode:\s*([a-zA-Z\-]+)\s*(?:#.*)?$/m)?.[1]?.toLowerCase();if(s==="shared-runtime-caches"||s==="shared-node-deps"||s==="isolated")return s}catch{}return "isolated"}withGoCacheEnv(o,t){let r=this.resolveDependencyMode(o),i=process.env.RAPIDKIT_WORKSPACE_PATH||this.findWorkspaceRoot(o),n=r==="shared-runtime-caches"?h.join(i||o,".rapidkit","cache","go"):h.join(o,".rapidkit","cache","go"),a=process.env.GOMODCACHE,s=process.env.GOCACHE;return process.env.GOMODCACHE=h.join(n,"mod"),process.env.GOCACHE=h.join(n,"build"),t().finally(()=>{typeof a>"u"?delete process.env.GOMODCACHE:process.env.GOMODCACHE=a,typeof s>"u"?delete process.env.GOCACHE:process.env.GOCACHE=s;})}async checkPrereqs(){return this.run("go",["version"],process.cwd())}async warmSetupCache(o){return this.withGoCacheEnv(o,async()=>{try{return process.env.GOMODCACHE&&_.mkdirSync(process.env.GOMODCACHE,{recursive:true}),process.env.GOCACHE&&_.mkdirSync(process.env.GOCACHE,{recursive:true}),{exitCode:0}}catch{return {exitCode:1,message:"Failed to prepare Go cache directories"}}})}async initProject(o){return this.withGoCacheEnv(o,async()=>{let t=await this.ensureGoInstalled(o);return t||this.run("go",["mod","tidy"],o)})}async runDev(o){return this.withGoCacheEnv(o,()=>(async()=>{let t=await this.ensureGoInstalled(o);if(t)return t;let r=h.join(o,"Makefile");return _.existsSync(r)?this.run("make",["run"],o):this.run("go",["run","./main.go"],o)})())}async runTest(o){return this.withGoCacheEnv(o,async()=>{let t=await this.ensureGoInstalled(o);return t||this.run("go",["test","./..."],o)})}async runBuild(o){return this.withGoCacheEnv(o,async()=>{let t=await this.ensureGoInstalled(o);return t||this.run("go",["build","./..."],o)})}async runStart(o){return this.withGoCacheEnv(o,async()=>{let r=(a()?[h.join(o,"server.exe"),h.join(o,"server")]:[h.join(o,"server")]).find(n=>_.existsSync(n));if(r)return this.run(r,[],o);let i=await this.ensureGoInstalled(o);return i||this.run("go",["run","./main.go"],o)})}async doctorHints(o){return ["Install Go from https://go.dev/dl/ if missing.","Run go mod tidy when dependencies are out of sync.","Use make run for hot-reload if Makefile exists."]}};var Ot=class{constructor(o){this.runCommand=o;}runtime="node";async run(o,t,r){return {exitCode:await this.runCommand(o,t,r)}}findWorkspaceRoot(o){let t=o;for(;;){if(_.existsSync(h.join(t,".rapidkit-workspace")))return t;let r=h.dirname(t);if(r===t)break;t=r;}return null}resolveDependencyMode(o){let t=process.env.RAPIDKIT_DEP_SHARING_MODE?.toLowerCase();if(t==="shared-runtime-caches"||t==="shared-node-deps"||t==="isolated")return t;let r=this.findWorkspaceRoot(o);if(!r)return "isolated";let i=h.join(r,".rapidkit","policies.yml");if(!_.existsSync(i))return "isolated";try{let s=_.readFileSync(i,"utf-8").match(/^\s*dependency_sharing_mode:\s*([a-zA-Z\-]+)\s*(?:#.*)?$/m)?.[1]?.toLowerCase();if(s==="shared-runtime-caches"||s==="shared-node-deps"||s==="isolated")return s}catch{}return "isolated"}withDependencyEnv(o,t,r){let i=this.resolveDependencyMode(o),n=process.env.RAPIDKIT_WORKSPACE_PATH||this.findWorkspaceRoot(o),a=i==="isolated"?h.join(o,".rapidkit","cache","node"):h.join(n||o,".rapidkit","cache","node"),s=process.env.npm_config_cache,c=process.env.npm_config_store_dir;return t==="pnpm"?(process.env.npm_config_store_dir=h.join(a,"pnpm-store"),process.env.npm_config_cache=h.join(a,"pnpm-cache")):t==="yarn"?process.env.npm_config_cache=h.join(a,"yarn-cache"):process.env.npm_config_cache=h.join(a,"npm-cache"),r().finally(()=>{typeof s>"u"?delete process.env.npm_config_cache:process.env.npm_config_cache=s,typeof c>"u"?delete process.env.npm_config_store_dir:process.env.npm_config_store_dir=c;})}detectPackageManager(o){return _.existsSync(h.join(o,"pnpm-lock.yaml"))?"pnpm":_.existsSync(h.join(o,"yarn.lock"))?"yarn":"npm"}scriptArgs(o,t){return o==="npm"?["run",t]:["run",t]}async checkPrereqs(){return this.run("node",["--version"],process.cwd())}async warmSetupCache(o){let t=this.detectPackageManager(o);return this.withDependencyEnv(o,t,async()=>{try{return process.env.npm_config_cache&&_.mkdirSync(process.env.npm_config_cache,{recursive:true}),t==="pnpm"&&process.env.npm_config_store_dir&&_.mkdirSync(process.env.npm_config_store_dir,{recursive:true}),{exitCode:0}}catch{return {exitCode:1,message:"Failed to prepare Node cache directories"}}})}async initProject(o){let t=this.detectPackageManager(o),r=this.resolveDependencyMode(o),i=r==="shared-runtime-caches"||r==="shared-node-deps"?["install","--prefer-offline"]:["install"];return this.withDependencyEnv(o,t,()=>this.run(t,i,o))}async runDev(o){let t=this.detectPackageManager(o);return this.withDependencyEnv(o,t,()=>this.run(t,this.scriptArgs(t,"dev"),o))}async runTest(o){let t=this.detectPackageManager(o);return this.withDependencyEnv(o,t,()=>this.run(t,this.scriptArgs(t,"test"),o))}async runBuild(o){let t=this.detectPackageManager(o);return this.withDependencyEnv(o,t,()=>this.run(t,this.scriptArgs(t,"build"),o))}async runStart(o){let t=this.detectPackageManager(o);return this.withDependencyEnv(o,t,()=>this.run(t,this.scriptArgs(t,"start"),o))}async doctorHints(o){return ["Install Node.js LTS and ensure node/npm are on PATH.","Use lockfiles (package-lock.json, pnpm-lock.yaml, yarn.lock) for deterministic installs.","Run install before dev/test/build if dependencies changed."]}};var Nt=class{constructor(o){this.runCore=o;}runtime="python";async run(o,t){return {exitCode:await this.withPythonCacheEnv(t,()=>this.runCore(o,t))}}findWorkspaceRoot(o){let t=o;for(;;){if(_.existsSync(h.join(t,".rapidkit-workspace")))return t;let r=h.dirname(t);if(r===t)break;t=r;}return null}resolveDependencyMode(o){let t=process.env.RAPIDKIT_DEP_SHARING_MODE?.toLowerCase();if(t==="shared-runtime-caches"||t==="shared-node-deps"||t==="isolated")return t;let r=this.findWorkspaceRoot(o);if(!r)return "isolated";let i=h.join(r,".rapidkit","policies.yml");if(!_.existsSync(i))return "isolated";try{let s=_.readFileSync(i,"utf-8").match(/^\s*dependency_sharing_mode:\s*([a-zA-Z\-]+)\s*(?:#.*)?$/m)?.[1]?.toLowerCase();if(s==="shared-runtime-caches"||s==="shared-node-deps"||s==="isolated")return s}catch{}return "isolated"}withPythonCacheEnv(o,t){let r=this.resolveDependencyMode(o),i=process.env.RAPIDKIT_WORKSPACE_PATH||this.findWorkspaceRoot(o),n=r==="shared-runtime-caches"?h.join(i||o,".rapidkit","cache","python"):h.join(o,".rapidkit","cache","python"),a=process.env.PIP_CACHE_DIR,s=process.env.POETRY_CACHE_DIR;return process.env.PIP_CACHE_DIR=h.join(n,"pip"),process.env.POETRY_CACHE_DIR=h.join(n,"poetry"),t().finally(()=>{typeof a>"u"?delete process.env.PIP_CACHE_DIR:process.env.PIP_CACHE_DIR=a,typeof s>"u"?delete process.env.POETRY_CACHE_DIR:process.env.POETRY_CACHE_DIR=s;})}async checkPrereqs(){let o=process.cwd(),t=await this.run(["doctor","check"],o);return t.exitCode===0?t:this.run(["doctor"],o)}async initProject(o){return this.run(["init"],o)}async runDev(o){return this.run(["dev"],o)}async runTest(o){return this.run(["test"],o)}async runBuild(o){return this.run(["build"],o)}async runStart(o){return this.run(["start"],o)}async doctorHints(o){return ['Run "npx rapidkit doctor workspace" for a full workspace scan.','Use "npx rapidkit init" after adding or changing modules.','Use workspace launcher "./rapidkit" to avoid environment drift.']}};function Yn(){let e={...process.env},o=e.PATH||"";if(o){let t=o.split(h.delimiter).filter(r=>!r.replace(/\\/g,"/").includes("/.pyenv/shims")).join(h.delimiter);e.PATH=t;}return e.PYENV_VERSION="system",e.POETRY_PYTHON=e.POETRY_PYTHON||c$3(),typeof e.RAPIDKIT_SKIP_LOCK_SYNC>"u"&&(e.RAPIDKIT_SKIP_LOCK_SYNC="1"),e}function He(e,o){return e==="go"?new At((t,r,i)=>o.runCommandInCwd(t,r,i)):e==="node"?new Ot((t,r,i)=>o.runCommandInCwd(t,r,i)):new Nt((t,r)=>o.runCoreRapidkit(t,{cwd:r,env:Yn()}))}var or=h.join(Tn.homedir(),".rapidkit","cache"),rr=1440*60*1e3;function eo(){let e=process.env.RAPIDKIT_CACHE_DIR?.trim();if(e)return e;let o=process.env.VITEST_WORKER_ID?.trim();return o?h.join(or,`vitest-${o}`):or}var $t=class e{static instance;memoryCache=new Map;constructor(){}static getInstance(){return e.instance||(e.instance=new e),e.instance}getCacheKey(o){return createHash("md5").update(o).digest("hex")}getCachePath(o){return h.join(eo(),`${this.getCacheKey(o)}.json`)}async get(o,t="1.0"){let r=this.memoryCache.get(o);if(r&&r.version===t&&Date.now()-r.timestamp<rr)return a$1.debug(`Cache hit (memory): ${o}`),r.data;try{let i=this.getCachePath(o),n=await promises.readFile(i,"utf-8"),a=JSON.parse(n);if(a.version===t&&Date.now()-a.timestamp<rr)return a$1.debug(`Cache hit (disk): ${o}`),this.memoryCache.set(o,a),a.data;await promises.unlink(i).catch(()=>{});}catch{a$1.debug(`Cache miss: ${o}`);}return null}async set(o,t,r="1.0"){let i={data:t,timestamp:Date.now(),version:r};this.memoryCache.set(o,i);try{await promises.mkdir(eo(),{recursive:true});let n=this.getCachePath(o);await promises.writeFile(n,JSON.stringify(i),"utf-8"),a$1.debug(`Cache set: ${o}`);}catch(n){a$1.debug(`Cache write failed: ${o}`,n);}}async invalidate(o){this.memoryCache.delete(o);try{let t=this.getCachePath(o);await promises.unlink(t),a$1.debug(`Cache invalidated: ${o}`);}catch{}}async clear(){this.memoryCache.clear();try{let o=eo(),t=await promises.readdir(o);await Promise.all(t.map(r=>promises.unlink(h.join(o,r)))),a$1.debug("Cache cleared");}catch{}}};function Se(e){let o=e;for(;;){let t=h.join(o,".rapidkit","project.json");if(_.existsSync(t))try{return JSON.parse(_.readFileSync(t,"utf8"))}catch{return null}let r=h.dirname(o);if(r===o)break;o=r;}return null}function pe(e,o){let t=e?.runtime?.toLowerCase(),r=e?.kit_name?.toLowerCase(),i=_.existsSync(h.join(o,"go.mod"));return t==="go"||(r?.startsWith("gofiber")??false)||(r?.startsWith("gogin")??false)||i}function ue(e,o){let t=e?.runtime?.toLowerCase(),r=e?.kit_name?.toLowerCase(),i=_.existsSync(h.join(o,"package.json"));return t==="node"||t==="typescript"||(r?.startsWith("nestjs")??false)||i}function me(e,o){let t=e?.runtime?.toLowerCase(),r=e?.kit_name?.toLowerCase(),i=_.existsSync(h.join(o,"pyproject.toml")),n=_.existsSync(h.join(o,"requirements.txt"))||_.existsSync(h.join(o,"requirements.in"));return t==="python"||(r?.startsWith("fastapi")??false)||i||n}async function ts(e){let o=await promises.readFile(e);return createHash("sha256").update(o).digest("hex")}async function ir(e,o){await b.outputFile(e,`${JSON.stringify(o,null,2)}
|
|
3502
|
-
`,"utf-8");}function
|
|
3503
|
-
`,"utf-8"),i}function as(e,o){if(!o?.enabled)return {headers:{}};let t=process.env[o.hmacKeyEnv];if(!t)return {headers:{},error:`Evidence signing key env is missing: ${o.hmacKeyEnv}`};let r=(o.algorithm||"sha256").toLowerCase(),i=o.headerName||"x-rapidkit-evidence-signature",n=createHmac(r,t).update(JSON.stringify(e)).digest("hex");return {headers:{[i]:n,"x-rapidkit-evidence-signature-alg":r}}}async function cs(e,o,t){let r=(t.algorithm||"sha256").toLowerCase(),i=be(e,t.publicKeyPath);if(!await b.pathExists(i))return {verified:false,algorithm:r,publicKeyPath:i,publicKeyFingerprint:"",signature:t.signature,message:`Public key not found: ${i}`};try{let n=await promises.readFile(i,"utf-8"),a=await promises.readFile(o),s=createVerify(r);s.update(a),s.end();let c=Buffer.from(t.signature,"base64"),p=s.verify(n,c),d=createHash("sha256").update(n).digest("hex");return {verified:p,algorithm:r,publicKeyPath:i,publicKeyFingerprint:d,signature:t.signature,message:p?"Attestation verified.":"Attestation signature verification failed."}}catch(n){return {verified:false,algorithm:r,publicKeyPath:i,publicKeyFingerprint:"",signature:t.signature,message:`Attestation verification error: ${n.message}`}}}async function ls(e,o,t,r){let i=process.env.RAPIDKIT_SIGSTORE_MOCK;if(i==="success")return {verified:true,tlogVerified:r.requireTransparencyLog,message:"Sigstore verification passed (mock).",identity:t.identity||null,issuer:t.issuer||null,rekorUrl:t.rekorUrl||null,bundlePath:t.bundlePath||null,certificatePath:t.certificatePath||null,signaturePath:t.signaturePath||null};if(i==="fail")return {verified:false,tlogVerified:false,message:"Sigstore verification failed (mock).",identity:t.identity||null,issuer:t.issuer||null,rekorUrl:t.rekorUrl||null,bundlePath:t.bundlePath||null,certificatePath:t.certificatePath||null,signaturePath:t.signaturePath||null};let n=t.signaturePath?be(e,t.signaturePath):null;if(!n||!await b.pathExists(n))return {verified:false,tlogVerified:false,message:"Sigstore signaturePath is missing or not found.",identity:t.identity||null,issuer:t.issuer||null,rekorUrl:t.rekorUrl||null,bundlePath:t.bundlePath||null,certificatePath:t.certificatePath||null,signaturePath:n};let a=["verify-blob",o,"--signature",n],s=t.certificatePath?be(e,t.certificatePath):null;s&&a.push("--certificate",s);let c=t.bundlePath?be(e,t.bundlePath):null;c&&a.push("--bundle",c);let p=t.keyPath?be(e,t.keyPath):null;p&&a.push("--key",p),t.identity&&a.push("--certificate-identity",t.identity),t.issuer&&a.push("--certificate-oidc-issuer",t.issuer),t.rekorUrl&&a.push("--rekor-url",t.rekorUrl),r.requireTransparencyLog||a.push("--insecure-ignore-tlog");try{let d=await execa("cosign",a,{reject:false});return d.exitCode===0?{verified:true,tlogVerified:r.requireTransparencyLog,message:"Sigstore verification passed.",identity:t.identity||null,issuer:t.issuer||null,rekorUrl:t.rekorUrl||null,bundlePath:c,certificatePath:s,signaturePath:n}:{verified:false,tlogVerified:false,message:`Sigstore verification failed: ${d.stderr||d.stdout||"unknown error"}`,identity:t.identity||null,issuer:t.issuer||null,rekorUrl:t.rekorUrl||null,bundlePath:c,certificatePath:s,signaturePath:n}}catch(d){return {verified:false,tlogVerified:false,message:`Sigstore verification error: ${d.message}`,identity:t.identity||null,issuer:t.issuer||null,rekorUrl:t.rekorUrl||null,bundlePath:c,certificatePath:s,signaturePath:n}}}async function ds(e,o){let t=o.algorithm||"sha256",r=be(e,o.policyPath),i=be(e,o.signaturePath),n=be(e,o.publicKeyPath);if(!await b.pathExists(r))return {verified:false,message:`Governance policy bundle not found: ${r}`,policies:null};if(!await b.pathExists(i))return {verified:false,message:`Governance policy signature not found: ${i}`,policies:null};if(!await b.pathExists(n))return {verified:false,message:`Governance policy public key not found: ${n}`,policies:null};try{let a=await promises.readFile(r,"utf-8"),s=(await promises.readFile(i,"utf-8")).trim(),c=await promises.readFile(n,"utf-8"),p=createVerify(t);return p.update(a),p.end(),p.verify(c,Buffer.from(s,"base64"))?{verified:true,message:"Governance policy bundle verified.",policies:JSON.parse(a).policies||{}}:{verified:false,message:"Governance policy bundle signature verification failed.",policies:null}}catch(a){return {verified:false,message:`Governance policy bundle verification error: ${a.message}`,policies:null}}}async function to(e,o){let t=[],r={syncedArtifacts:0,verifiedArtifacts:0,rotatedFiles:0,lockWritten:false,governanceBundleVerified:false,transparencyEvidenceWritten:false,transparencyEvidenceRecords:0,evidenceExported:false,evidenceExportTarget:null},i=h.join(e,".rapidkit"),n=h.join(i,"mirror-config.json"),a=h.join(i,"mirror.lock"),s=h.join(i,"mirror","artifacts"),c=h.join(i,"reports"),p=await rs(e);if(!await b.pathExists(n))return t.push({id:"mirror.lifecycle",status:"skipped",message:"Mirror lifecycle skipped: .rapidkit/mirror-config.json not found."}),{checks:t,details:r};let d={};try{d=JSON.parse(await promises.readFile(n,"utf-8"));}catch{return t.push({id:"mirror.lifecycle.config",status:"failed",message:"Mirror lifecycle failed: invalid JSON in mirror-config.json."}),{checks:t,details:r}}if(!(o.forceRun===true||o.ciMode||o.offlineMode||d.mode==="offline-only"))return t.push({id:"mirror.lifecycle",status:"skipped",message:"Mirror lifecycle skipped: not in ci/offline mode."}),{checks:t,details:r};await b.ensureDir(s);let m=Math.max(0,d.prefetch?.retries??2),w=Math.max(0,d.prefetch?.backoffMs??250),f=Math.max(1e3,d.prefetch?.timeoutMs??15e3),g=d.security?.requireAttestation===true,R=d.security?.requireSigstore===true,x=d.security?.requireTransparencyLog===true,T=d.security?.requireSignedGovernance===true,C=d.security?.evidenceExport,P=(process.env.RAPIDKIT_ENV||d.security?.governance?.environment||"dev").toLowerCase(),G=d.security?.governance?.policies||{};if(d.security?.governanceBundle){let z=await ds(e,d.security.governanceBundle);if(t.push({id:"governance.bundle.verify",status:z.verified?"passed":"failed",message:z.message}),z.verified&&z.policies)G=z.policies,r.governanceBundleVerified=true;else if(T)return {checks:t,details:r}}let N=G[P],$=x||N?.requireTransparencyLog===true,Re=[],Ze=Array.isArray(d.artifacts)?d.artifacts:[],we=[];for(let z=0;z<Ze.length;z+=1){let L=Ze[z],S=L.id||`artifact-${z+1}`,se=L.source?be(e,L.source):null,ae=os(L,S),H=h.join(s,ae),re=false,le={sourceType:"path",source:se||L.url||"unknown",host:null,fetchedAt:new Date().toISOString(),attempts:1,trusted:true};if(se&&await b.pathExists(se))await b.ensureDir(h.dirname(H)),await b.copyFile(se,H),r.syncedArtifacts+=1,re=true,le={sourceType:"path",source:se,host:null,fetchedAt:new Date().toISOString(),attempts:1,trusted:true},t.push({id:`mirror.sync.${S}`,status:"passed",message:`Mirrored artifact ${S} from source path.`});else if(L.url){let U="";try{U=new URL(L.url).hostname.toLowerCase();}catch{t.push({id:`mirror.prefetch.${S}`,status:"failed",message:`Invalid URL for ${S}: ${L.url}`});continue}if(!(process.env.RAPIDKIT_TRUSTED_SOURCES==="1"||p.has(U))){t.push({id:`mirror.prefetch.trust.${S}`,status:"failed",message:`Untrusted mirror host for ${S}: ${U}. Add host to .rapidkit/trusted-sources.lock or set RAPIDKIT_TRUSTED_SOURCES=1.`});continue}if(o.offlineMode&&(await b.pathExists(H)?(re=true,le={sourceType:"url",source:L.url,host:U,fetchedAt:new Date().toISOString(),attempts:0,trusted:true},t.push({id:`mirror.prefetch.${S}`,status:"passed",message:`Offline mode reused existing mirrored artifact ${S}.`})):t.push({id:`mirror.prefetch.${S}`,status:"failed",message:`Offline mode cannot prefetch remote artifact ${S} without an existing mirrored copy.`}),!re))continue;if(!re){let ie=null,Ae=0;for(let Ee=1;Ee<=m+1;Ee+=1){Ae=Ee;try{await is(L.url,H,f),r.syncedArtifacts+=1,re=true,le={sourceType:"url",source:L.url,host:U,fetchedAt:new Date().toISOString(),attempts:Ae,trusted:true},t.push({id:`mirror.prefetch.${S}`,status:"passed",message:Ae>1?`Prefetched artifact ${S} from ${U} after ${Ae} attempts.`:`Prefetched artifact ${S} from ${U}.`});break}catch(ut){if(ie=ut,Ee<=m){await nr(w*Ee);continue}}}if(!re){t.push({id:`mirror.prefetch.${S}`,status:"failed",message:`Failed to prefetch ${S} after ${m+1} attempt(s): ${ie?.message||"unknown error"}`});continue}}if(!re){t.push({id:`mirror.prefetch.${S}`,status:"failed",message:`Failed to prefetch ${S}.`});continue}}if(!re){L.required||o.offlineMode?t.push({id:`mirror.sync.${S}`,status:"failed",message:`Mirror source missing for ${S}${se?`: ${se}`:""}`}):t.push({id:`mirror.sync.${S}`,status:"skipped",message:`Mirror source not found for optional artifact ${S}.`});continue}let I=await ts(H);if(L.sha256&&L.sha256.toLowerCase()!==I.toLowerCase()){t.push({id:`mirror.verify.${S}`,status:"failed",message:`Checksum mismatch for ${S}.`});continue}r.verifiedArtifacts+=1,t.push({id:`mirror.verify.${S}`,status:"passed",message:`Checksum verified for ${S}.`});let O=L.attestation?await cs(e,H,L.attestation):null;if(L.attestation){if(t.push({id:`mirror.attest.${S}`,status:O?.verified?"passed":"failed",message:O?.message||"Attestation verification failed."}),!O?.verified)continue}else if(g){t.push({id:`mirror.attest.${S}`,status:"failed",message:`Attestation is required but missing for ${S}.`});continue}else t.push({id:`mirror.attest.${S}`,status:"skipped",message:`No attestation provided for ${S}.`});let M=L.attestation?.sigstore,j=M?await ls(e,H,M,{requireTransparencyLog:$}):null;if(M){if(t.push({id:`mirror.sigstore.${S}`,status:j?.verified?"passed":"failed",message:j?.message||"Sigstore verification failed."}),Re.push({artifactId:S,verified:!!j?.verified,tlogVerified:!!j?.tlogVerified,identity:j?.identity||null,issuer:j?.issuer||null,rekorUrl:j?.rekorUrl||null,timestamp:new Date().toISOString(),environment:P}),!j?.verified)continue}else if(R){t.push({id:`mirror.sigstore.${S}`,status:"failed",message:`Sigstore attestation is required but missing for ${S}.`});continue}else t.push({id:`mirror.sigstore.${S}`,status:"skipped",message:`No Sigstore attestation provided for ${S}.`});if(M&&j?.verified&&N){let U=N.allowedIdentities||[];if(U.length>0){let ie=!!j.identity&&U.includes(j.identity);if(t.push({id:`mirror.sigstore.policy.identity.${S}`,status:ie?"passed":"failed",message:ie?`Sigstore identity policy passed for ${S} in ${P}.`:`Sigstore identity policy failed for ${S} in ${P}.`}),!ie)continue}let Te=N.allowedIssuers||[];if(Te.length>0){let ie=!!j.issuer&&Te.includes(j.issuer);if(t.push({id:`mirror.sigstore.policy.issuer.${S}`,status:ie?"passed":"failed",message:ie?`Sigstore issuer policy passed for ${S} in ${P}.`:`Sigstore issuer policy failed for ${S} in ${P}.`}),!ie)continue}let Pe=N.allowedRekorUrls||[];if(Pe.length>0){let ie=!!j.rekorUrl&&Pe.includes(j.rekorUrl);if(t.push({id:`mirror.sigstore.policy.rekor.${S}`,status:ie?"passed":"failed",message:ie?`Sigstore Rekor policy passed for ${S} in ${P}.`:`Sigstore Rekor policy failed for ${S} in ${P}.`}),!ie)continue}}else N&&t.push({id:`mirror.sigstore.policy.${S}`,status:"skipped",message:`Sigstore governance policy configured for ${P} but no verified Sigstore attestation for ${S}.`});let ge=await promises.stat(H);we.push({id:S,path:h.relative(e,H),sha256:I,size:ge.size,provenance:le,attestation:{detached:{provided:!!L.attestation,verified:O?.verified||false,algorithm:O?.algorithm||null,publicKeyPath:O?.publicKeyPath||null,publicKeyFingerprint:O?.publicKeyFingerprint||null,signature:O?.signature||null,verifiedAt:O?.verified?new Date().toISOString():null},sigstore:{provided:!!M,verified:j?.verified||false,tlogVerified:j?.tlogVerified||false,identity:j?.identity||null,issuer:j?.issuer||null,rekorUrl:j?.rekorUrl||null,bundlePath:j?.bundlePath||null,certificatePath:j?.certificatePath||null,signaturePath:j?.signaturePath||null,verifiedAt:j?.verified?new Date().toISOString():null}}});}let fe=d.retention?.keepLast;if(typeof fe=="number"&&fe>0){let L=(await promises.readdir(s,{withFileTypes:true})).filter(S=>S.isFile()).map(S=>h.join(s,S.name));if(L.length>fe){let S=await Promise.all(L.map(async ae=>({filePath:ae,stat:await promises.stat(ae)})));S.sort((ae,H)=>H.stat.mtimeMs-ae.stat.mtimeMs);let se=S.slice(fe);for(let ae of se)await promises.unlink(ae.filePath),r.rotatedFiles+=1;}}t.push({id:"mirror.rotate",status:"passed",message:r.rotatedFiles>0?`Mirror retention rotation removed ${r.rotatedFiles} file(s).`:"Mirror retention rotation completed with no removals."});let Ve={schemaVersion:"1.0",generatedAt:new Date().toISOString(),mode:d.mode||null,environment:P,artifacts:we};if(await promises.writeFile(a,`${JSON.stringify(Ve,null,2)}
|
|
3504
|
-
`,"utf-8"),r.lockWritten=true,
|
|
3505
|
-
`,"utf-8"),r.evidenceExported=true,r.evidenceExportTarget=
|
|
3506
|
-
`),1;let r=
|
|
3507
|
-
`),1;await a.ensureDir(
|
|
3508
|
-
`),1}}async function
|
|
3509
|
-
`),1;let r=
|
|
3510
|
-
`),1;await a.ensureDir(
|
|
3511
|
-
`),1}}async function
|
|
3512
|
-
Reason: ${
|
|
99
|
+
`)),console.log(s.white(`Set your API key: rapidkit config set-api-key
|
|
100
|
+
`)),process.exit(1)),nt(o);let r=await po();process.exit(r?0:1);}catch(o){a$1.error("Failed to update embeddings:",o.message),process.exit(1);}});}var ct=class{constructor(e){this.runCommand=e;}runtime="go";async run(e,o,r){return {exitCode:await this.runCommand(e,o,r)}}async ensureGoInstalled(e){return (await this.run("go",["version"],e)).exitCode===0?null:{exitCode:1,message:"Go toolchain is not installed or not available on PATH. Install Go from https://go.dev/dl/ and retry."}}findWorkspaceRoot(e){let o=e;for(;;){if(w.existsSync(f.join(o,".rapidkit-workspace")))return o;let r=f.dirname(o);if(r===o)break;o=r;}return null}resolveDependencyMode(e){let o=process.env.RAPIDKIT_DEP_SHARING_MODE?.toLowerCase();if(o==="shared-runtime-caches"||o==="shared-node-deps"||o==="isolated")return o;let r=this.findWorkspaceRoot(e);if(!r)return "isolated";let n=f.join(r,".rapidkit","policies.yml");if(!w.existsSync(n))return "isolated";try{let c=w.readFileSync(n,"utf-8").match(/^\s*dependency_sharing_mode:\s*([a-zA-Z\-]+)\s*(?:#.*)?$/m)?.[1]?.toLowerCase();if(c==="shared-runtime-caches"||c==="shared-node-deps"||c==="isolated")return c}catch{}return "isolated"}withGoCacheEnv(e,o){let r=this.resolveDependencyMode(e),n=process.env.RAPIDKIT_WORKSPACE_PATH||this.findWorkspaceRoot(e),i=r==="shared-runtime-caches"?f.join(n||e,".rapidkit","cache","go"):f.join(e,".rapidkit","cache","go"),a=process.env.GOMODCACHE,c=process.env.GOCACHE;return process.env.GOMODCACHE=f.join(i,"mod"),process.env.GOCACHE=f.join(i,"build"),o().finally(()=>{typeof a>"u"?delete process.env.GOMODCACHE:process.env.GOMODCACHE=a,typeof c>"u"?delete process.env.GOCACHE:process.env.GOCACHE=c;})}async checkPrereqs(){return this.run("go",["version"],process.cwd())}async warmSetupCache(e){return this.withGoCacheEnv(e,async()=>{try{return process.env.GOMODCACHE&&w.mkdirSync(process.env.GOMODCACHE,{recursive:true}),process.env.GOCACHE&&w.mkdirSync(process.env.GOCACHE,{recursive:true}),{exitCode:0}}catch{return {exitCode:1,message:"Failed to prepare Go cache directories"}}})}async initProject(e){return this.withGoCacheEnv(e,async()=>{let o=await this.ensureGoInstalled(e);return o||this.run("go",["mod","tidy"],e)})}async runDev(e){return this.withGoCacheEnv(e,()=>(async()=>{let o=await this.ensureGoInstalled(e);if(o)return o;let r=f.join(e,"Makefile");return w.existsSync(r)?this.run("make",["run"],e):this.run("go",["run","./main.go"],e)})())}async runTest(e){return this.withGoCacheEnv(e,async()=>{let o=await this.ensureGoInstalled(e);return o||this.run("go",["test","./..."],e)})}async runBuild(e){return this.withGoCacheEnv(e,async()=>{let o=await this.ensureGoInstalled(e);return o||this.run("go",["build","./..."],e)})}async runStart(e){return this.withGoCacheEnv(e,async()=>{let r=(a()?[f.join(e,"server.exe"),f.join(e,"server")]:[f.join(e,"server")]).find(i=>w.existsSync(i));if(r)return this.run(r,[],e);let n=await this.ensureGoInstalled(e);return n||this.run("go",["run","./main.go"],e)})}async doctorHints(e){return ["Install Go from https://go.dev/dl/ if missing.","Run go mod tidy when dependencies are out of sync.","Use make run for hot-reload if Makefile exists."]}};var lt=class{constructor(e){this.runCommand=e;}runtime="node";async run(e,o,r){return {exitCode:await this.runCommand(e,o,r)}}findWorkspaceRoot(e){let o=e;for(;;){if(w.existsSync(f.join(o,".rapidkit-workspace")))return o;let r=f.dirname(o);if(r===o)break;o=r;}return null}resolveDependencyMode(e){let o=process.env.RAPIDKIT_DEP_SHARING_MODE?.toLowerCase();if(o==="shared-runtime-caches"||o==="shared-node-deps"||o==="isolated")return o;let r=this.findWorkspaceRoot(e);if(!r)return "isolated";let n=f.join(r,".rapidkit","policies.yml");if(!w.existsSync(n))return "isolated";try{let c=w.readFileSync(n,"utf-8").match(/^\s*dependency_sharing_mode:\s*([a-zA-Z\-]+)\s*(?:#.*)?$/m)?.[1]?.toLowerCase();if(c==="shared-runtime-caches"||c==="shared-node-deps"||c==="isolated")return c}catch{}return "isolated"}withDependencyEnv(e,o,r){let n=this.resolveDependencyMode(e),i=process.env.RAPIDKIT_WORKSPACE_PATH||this.findWorkspaceRoot(e),a=n==="isolated"?f.join(e,".rapidkit","cache","node"):f.join(i||e,".rapidkit","cache","node"),c=process.env.npm_config_cache,m=process.env.npm_config_store_dir;return o==="pnpm"?(process.env.npm_config_store_dir=f.join(a,"pnpm-store"),process.env.npm_config_cache=f.join(a,"pnpm-cache")):o==="yarn"?process.env.npm_config_cache=f.join(a,"yarn-cache"):process.env.npm_config_cache=f.join(a,"npm-cache"),r().finally(()=>{typeof c>"u"?delete process.env.npm_config_cache:process.env.npm_config_cache=c,typeof m>"u"?delete process.env.npm_config_store_dir:process.env.npm_config_store_dir=m;})}detectPackageManager(e){return w.existsSync(f.join(e,"pnpm-lock.yaml"))?"pnpm":w.existsSync(f.join(e,"yarn.lock"))?"yarn":"npm"}scriptArgs(e,o){return e==="npm"?["run",o]:["run",o]}async checkPrereqs(){return this.run("node",["--version"],process.cwd())}async warmSetupCache(e){let o=this.detectPackageManager(e);return this.withDependencyEnv(e,o,async()=>{try{return process.env.npm_config_cache&&w.mkdirSync(process.env.npm_config_cache,{recursive:true}),o==="pnpm"&&process.env.npm_config_store_dir&&w.mkdirSync(process.env.npm_config_store_dir,{recursive:true}),{exitCode:0}}catch{return {exitCode:1,message:"Failed to prepare Node cache directories"}}})}async initProject(e){let o=this.detectPackageManager(e),r=this.resolveDependencyMode(e),n=r==="shared-runtime-caches"||r==="shared-node-deps"?["install","--prefer-offline"]:["install"];return this.withDependencyEnv(e,o,()=>this.run(o,n,e))}async runDev(e){let o=this.detectPackageManager(e);return this.withDependencyEnv(e,o,()=>this.run(o,this.scriptArgs(o,"dev"),e))}async runTest(e){let o=this.detectPackageManager(e);return this.withDependencyEnv(e,o,()=>this.run(o,this.scriptArgs(o,"test"),e))}async runBuild(e){let o=this.detectPackageManager(e);return this.withDependencyEnv(e,o,()=>this.run(o,this.scriptArgs(o,"build"),e))}async runStart(e){let o=this.detectPackageManager(e);return this.withDependencyEnv(e,o,()=>this.run(o,this.scriptArgs(o,"start"),e))}async doctorHints(e){return ["Install Node.js LTS and ensure node/npm are on PATH.","Use lockfiles (package-lock.json, pnpm-lock.yaml, yarn.lock) for deterministic installs.","Run install before dev/test/build if dependencies changed."]}};var dt=class{constructor(e){this.runCore=e;}runtime="python";async run(e,o){return {exitCode:await this.withPythonCacheEnv(o,()=>this.runCore(e,o))}}findWorkspaceRoot(e){let o=e;for(;;){if(w.existsSync(f.join(o,".rapidkit-workspace")))return o;let r=f.dirname(o);if(r===o)break;o=r;}return null}resolveDependencyMode(e){let o=process.env.RAPIDKIT_DEP_SHARING_MODE?.toLowerCase();if(o==="shared-runtime-caches"||o==="shared-node-deps"||o==="isolated")return o;let r=this.findWorkspaceRoot(e);if(!r)return "isolated";let n=f.join(r,".rapidkit","policies.yml");if(!w.existsSync(n))return "isolated";try{let c=w.readFileSync(n,"utf-8").match(/^\s*dependency_sharing_mode:\s*([a-zA-Z\-]+)\s*(?:#.*)?$/m)?.[1]?.toLowerCase();if(c==="shared-runtime-caches"||c==="shared-node-deps"||c==="isolated")return c}catch{}return "isolated"}withPythonCacheEnv(e,o){let r=this.resolveDependencyMode(e),n=process.env.RAPIDKIT_WORKSPACE_PATH||this.findWorkspaceRoot(e),i=r==="shared-runtime-caches"?f.join(n||e,".rapidkit","cache","python"):f.join(e,".rapidkit","cache","python"),a=process.env.PIP_CACHE_DIR,c=process.env.POETRY_CACHE_DIR;return process.env.PIP_CACHE_DIR=f.join(i,"pip"),process.env.POETRY_CACHE_DIR=f.join(i,"poetry"),o().finally(()=>{typeof a>"u"?delete process.env.PIP_CACHE_DIR:process.env.PIP_CACHE_DIR=a,typeof c>"u"?delete process.env.POETRY_CACHE_DIR:process.env.POETRY_CACHE_DIR=c;})}async checkPrereqs(){let e=process.cwd(),o=await this.run(["doctor","check"],e);return o.exitCode===0?o:this.run(["doctor"],e)}async initProject(e){return this.run(["init"],e)}async runDev(e){return this.run(["dev"],e)}async runTest(e){return this.run(["test"],e)}async runBuild(e){return this.run(["build"],e)}async runStart(e){return this.run(["start"],e)}async doctorHints(e){return ['Run "npx rapidkit doctor workspace" for a full workspace scan.','Use "npx rapidkit init" after adding or changing modules.','Use workspace launcher "./rapidkit" to avoid environment drift.']}};function Pr(){let t={...process.env},e=t.PATH||"";if(e){let o=e.split(f.delimiter).filter(r=>!r.replace(/\\/g,"/").includes("/.pyenv/shims")).join(f.delimiter);t.PATH=o;}return t.PYENV_VERSION="system",t.POETRY_PYTHON=t.POETRY_PYTHON||c$2(),typeof t.RAPIDKIT_SKIP_LOCK_SYNC>"u"&&(t.RAPIDKIT_SKIP_LOCK_SYNC="1"),t}function Se(t,e){return t==="go"?new ct((o,r,n)=>e.runCommandInCwd(o,r,n)):t==="node"?new lt((o,r,n)=>e.runCommandInCwd(o,r,n)):new dt((o,r)=>e.runCoreRapidkit(o,{cwd:r,env:Pr()}))}var go=f.join(rr.homedir(),".rapidkit","cache"),yo=1440*60*1e3;function Rt(){let t=process.env.RAPIDKIT_CACHE_DIR?.trim();if(t)return t;let e=process.env.VITEST_WORKER_ID?.trim();return e?f.join(go,`vitest-${e}`):go}var ut=class t{static instance;memoryCache=new Map;constructor(){}static getInstance(){return t.instance||(t.instance=new t),t.instance}getCacheKey(e){return createHash("md5").update(e).digest("hex")}getCachePath(e){return f.join(Rt(),`${this.getCacheKey(e)}.json`)}async get(e,o="1.0"){let r=this.memoryCache.get(e);if(r&&r.version===o&&Date.now()-r.timestamp<yo)return a$1.debug(`Cache hit (memory): ${e}`),r.data;try{let n=this.getCachePath(e),i=await promises.readFile(n,"utf-8"),a=JSON.parse(i);if(a.version===o&&Date.now()-a.timestamp<yo)return a$1.debug(`Cache hit (disk): ${e}`),this.memoryCache.set(e,a),a.data;await promises.unlink(n).catch(()=>{});}catch{a$1.debug(`Cache miss: ${e}`);}return null}async set(e,o,r="1.0"){let n={data:o,timestamp:Date.now(),version:r};this.memoryCache.set(e,n);try{await promises.mkdir(Rt(),{recursive:true});let i=this.getCachePath(e);await promises.writeFile(i,JSON.stringify(n),"utf-8"),a$1.debug(`Cache set: ${e}`);}catch(i){a$1.debug(`Cache write failed: ${e}`,i);}}async invalidate(e){this.memoryCache.delete(e);try{let o=this.getCachePath(e);await promises.unlink(o),a$1.debug(`Cache invalidated: ${e}`);}catch{}}async clear(){this.memoryCache.clear();try{let e=Rt(),o=await promises.readdir(e);await Promise.all(o.map(r=>promises.unlink(f.join(e,r)))),a$1.debug("Cache cleared");}catch{}}};function ye(t){let e=t;for(;;){let o=f.join(e,".rapidkit","project.json");if(w.existsSync(o))try{return JSON.parse(w.readFileSync(o,"utf8"))}catch{return null}let r=f.dirname(e);if(r===e)break;e=r;}return null}function ne(t,e){let o=t?.runtime?.toLowerCase(),r=t?.kit_name?.toLowerCase(),n=w.existsSync(f.join(e,"go.mod"));return o==="go"||(r?.startsWith("gofiber")??false)||(r?.startsWith("gogin")??false)||n}function ie(t,e){let o=t?.runtime?.toLowerCase(),r=t?.kit_name?.toLowerCase(),n=w.existsSync(f.join(e,"package.json"));return o==="node"||o==="typescript"||(r?.startsWith("nestjs")??false)||n}function se(t,e){let o=t?.runtime?.toLowerCase(),r=t?.kit_name?.toLowerCase(),n=w.existsSync(f.join(e,"pyproject.toml")),i=w.existsSync(f.join(e,"requirements.txt"))||w.existsSync(f.join(e,"requirements.in"));return o==="python"||(r?.startsWith("fastapi")??false)||n||i}async function Er(t){let e=await promises.readFile(t);return createHash("sha256").update(e).digest("hex")}async function ho(t,e){await x.outputFile(t,`${JSON.stringify(e,null,2)}
|
|
101
|
+
`,"utf-8");}function me(t,e){return f.isAbsolute(e)?e:f.join(t,e)}function Ar(t,e){if(t.target)return t.target;if(t.source)return f.basename(t.source);if(t.url)try{let o=new URL(t.url).pathname,r=f.basename(o);if(r&&r!=="/")return r}catch{}return `${e}.artifact`}async function Ir(t){let e=f.join(t,".rapidkit","trusted-sources.lock"),o=new Set(["localhost","127.0.0.1"]);if(!await x.pathExists(e))return o;try{let n=(await promises.readFile(e,"utf-8")).split(/\r?\n/).map(i=>i.trim()).filter(i=>i.length>0&&!i.startsWith("#"));for(let i of n)o.add(i.toLowerCase());}catch{}return o}async function jr(t,e,o){await x.ensureDir(f.dirname(e)),await new Promise((r,n)=>{let a=(t.startsWith("https://")?Po:bo).get(t,c=>{if(!c.statusCode||c.statusCode<200||c.statusCode>=300){n(new Error(`HTTP ${c.statusCode||"unknown"}`)),c.resume();return}let m=createWriteStream(e);c.pipe(m),m.on("finish",()=>{m.close(),r();}),m.on("error",d=>{n(d);});});a.setTimeout(o,()=>{a.destroy(new Error(`Request timeout after ${o}ms`));}),a.on("error",c=>{n(c);});});}async function $r(t,e,o,r,n){let i=new URL(t),a=JSON.stringify(e),c=i.protocol==="https:"?Po:bo;await new Promise((m,d)=>{let l=c.request({method:"POST",hostname:i.hostname,port:i.port||(i.protocol==="https:"?443:80),path:`${i.pathname}${i.search}`,headers:{"Content-Type":"application/json","Content-Length":Buffer.byteLength(a),...r?{Authorization:`Bearer ${r}`}:{},...n||{}}},u=>{if(!u.statusCode||u.statusCode<200||u.statusCode>=300){d(new Error(`HTTP ${u.statusCode||"unknown"}`)),u.resume();return}u.resume(),m();});l.setTimeout(o,()=>{l.destroy(new Error(`Request timeout after ${o}ms`));}),l.on("error",u=>{d(u);}),l.write(a),l.end();});}function wo(t){return new Promise(e=>setTimeout(e,t))}async function Tr(t,e,o,r){let n=me(t,e||".rapidkit/reports/evidence-export-dead-letter.ndjson");return await x.ensureDir(f.dirname(n)),await promises.appendFile(n,`${JSON.stringify({timestamp:new Date().toISOString(),reason:r,payload:o})}
|
|
102
|
+
`,"utf-8"),n}function Or(t,e){if(!e?.enabled)return {headers:{}};let o=process.env[e.hmacKeyEnv];if(!o)return {headers:{},error:`Evidence signing key env is missing: ${e.hmacKeyEnv}`};let r=(e.algorithm||"sha256").toLowerCase(),n=e.headerName||"x-rapidkit-evidence-signature",i=createHmac(r,o).update(JSON.stringify(t)).digest("hex");return {headers:{[n]:i,"x-rapidkit-evidence-signature-alg":r}}}async function Mr(t,e,o){let r=(o.algorithm||"sha256").toLowerCase(),n=me(t,o.publicKeyPath);if(!await x.pathExists(n))return {verified:false,algorithm:r,publicKeyPath:n,publicKeyFingerprint:"",signature:o.signature,message:`Public key not found: ${n}`};try{let i=await promises.readFile(n,"utf-8"),a=await promises.readFile(e),c=createVerify(r);c.update(a),c.end();let m=Buffer.from(o.signature,"base64"),d=c.verify(i,m),l=createHash("sha256").update(i).digest("hex");return {verified:d,algorithm:r,publicKeyPath:n,publicKeyFingerprint:l,signature:o.signature,message:d?"Attestation verified.":"Attestation signature verification failed."}}catch(i){return {verified:false,algorithm:r,publicKeyPath:n,publicKeyFingerprint:"",signature:o.signature,message:`Attestation verification error: ${i.message}`}}}async function Dr(t,e,o,r){let n=process.env.RAPIDKIT_SIGSTORE_MOCK;if(n==="success")return {verified:true,tlogVerified:r.requireTransparencyLog,message:"Sigstore verification passed (mock).",identity:o.identity||null,issuer:o.issuer||null,rekorUrl:o.rekorUrl||null,bundlePath:o.bundlePath||null,certificatePath:o.certificatePath||null,signaturePath:o.signaturePath||null};if(n==="fail")return {verified:false,tlogVerified:false,message:"Sigstore verification failed (mock).",identity:o.identity||null,issuer:o.issuer||null,rekorUrl:o.rekorUrl||null,bundlePath:o.bundlePath||null,certificatePath:o.certificatePath||null,signaturePath:o.signaturePath||null};let i=o.signaturePath?me(t,o.signaturePath):null;if(!i||!await x.pathExists(i))return {verified:false,tlogVerified:false,message:"Sigstore signaturePath is missing or not found.",identity:o.identity||null,issuer:o.issuer||null,rekorUrl:o.rekorUrl||null,bundlePath:o.bundlePath||null,certificatePath:o.certificatePath||null,signaturePath:i};let a=["verify-blob",e,"--signature",i],c=o.certificatePath?me(t,o.certificatePath):null;c&&a.push("--certificate",c);let m=o.bundlePath?me(t,o.bundlePath):null;m&&a.push("--bundle",m);let d=o.keyPath?me(t,o.keyPath):null;d&&a.push("--key",d),o.identity&&a.push("--certificate-identity",o.identity),o.issuer&&a.push("--certificate-oidc-issuer",o.issuer),o.rekorUrl&&a.push("--rekor-url",o.rekorUrl),r.requireTransparencyLog||a.push("--insecure-ignore-tlog");try{let l=await execa("cosign",a,{reject:false});return l.exitCode===0?{verified:true,tlogVerified:r.requireTransparencyLog,message:"Sigstore verification passed.",identity:o.identity||null,issuer:o.issuer||null,rekorUrl:o.rekorUrl||null,bundlePath:m,certificatePath:c,signaturePath:i}:{verified:false,tlogVerified:false,message:`Sigstore verification failed: ${l.stderr||l.stdout||"unknown error"}`,identity:o.identity||null,issuer:o.issuer||null,rekorUrl:o.rekorUrl||null,bundlePath:m,certificatePath:c,signaturePath:i}}catch(l){return {verified:false,tlogVerified:false,message:`Sigstore verification error: ${l.message}`,identity:o.identity||null,issuer:o.issuer||null,rekorUrl:o.rekorUrl||null,bundlePath:m,certificatePath:c,signaturePath:i}}}async function Nr(t,e){let o=e.algorithm||"sha256",r=me(t,e.policyPath),n=me(t,e.signaturePath),i=me(t,e.publicKeyPath);if(!await x.pathExists(r))return {verified:false,message:`Governance policy bundle not found: ${r}`,policies:null};if(!await x.pathExists(n))return {verified:false,message:`Governance policy signature not found: ${n}`,policies:null};if(!await x.pathExists(i))return {verified:false,message:`Governance policy public key not found: ${i}`,policies:null};try{let a=await promises.readFile(r,"utf-8"),c=(await promises.readFile(n,"utf-8")).trim(),m=await promises.readFile(i,"utf-8"),d=createVerify(o);return d.update(a),d.end(),d.verify(m,Buffer.from(c,"base64"))?{verified:true,message:"Governance policy bundle verified.",policies:JSON.parse(a).policies||{}}:{verified:false,message:"Governance policy bundle signature verification failed.",policies:null}}catch(a){return {verified:false,message:`Governance policy bundle verification error: ${a.message}`,policies:null}}}async function St(t,e){let o=[],r={syncedArtifacts:0,verifiedArtifacts:0,rotatedFiles:0,lockWritten:false,governanceBundleVerified:false,transparencyEvidenceWritten:false,transparencyEvidenceRecords:0,evidenceExported:false,evidenceExportTarget:null},n=f.join(t,".rapidkit"),i=f.join(n,"mirror-config.json"),a=f.join(n,"mirror.lock"),c=f.join(n,"mirror","artifacts"),m=f.join(n,"reports"),d=await Ir(t);if(!await x.pathExists(i))return o.push({id:"mirror.lifecycle",status:"skipped",message:"Mirror lifecycle skipped: .rapidkit/mirror-config.json not found."}),{checks:o,details:r};let l={};try{l=JSON.parse(await promises.readFile(i,"utf-8"));}catch{return o.push({id:"mirror.lifecycle.config",status:"failed",message:"Mirror lifecycle failed: invalid JSON in mirror-config.json."}),{checks:o,details:r}}if(!(e.forceRun===true||e.ciMode||e.offlineMode||l.mode==="offline-only"))return o.push({id:"mirror.lifecycle",status:"skipped",message:"Mirror lifecycle skipped: not in ci/offline mode."}),{checks:o,details:r};await x.ensureDir(c);let y=Math.max(0,l.prefetch?.retries??2),v=Math.max(0,l.prefetch?.backoffMs??250),p=Math.max(1e3,l.prefetch?.timeoutMs??15e3),g=l.security?.requireAttestation===true,A=l.security?.requireSigstore===true,P=l.security?.requireTransparencyLog===true,S=l.security?.requireSignedGovernance===true,b=l.security?.evidenceExport,_=(process.env.RAPIDKIT_ENV||l.security?.governance?.environment||"dev").toLowerCase(),M=l.security?.governance?.policies||{};if(l.security?.governanceBundle){let U=await Nr(t,l.security.governanceBundle);if(o.push({id:"governance.bundle.verify",status:U.verified?"passed":"failed",message:U.message}),U.verified&&U.policies)M=U.policies,r.governanceBundleVerified=true;else if(S)return {checks:o,details:r}}let T=M[_],I=P||T?.requireTransparencyLog===true,fe=[],Ne=Array.isArray(l.artifacts)?l.artifacts:[],le=[];for(let U=0;U<Ne.length;U+=1){let D=Ne[U],k=D.id||`artifact-${U+1}`,Q=D.source?me(t,D.source):null,Z=Ar(D,k),K=f.join(c,Z),B=false,ee={sourceType:"path",source:Q||D.url||"unknown",host:null,fetchedAt:new Date().toISOString(),attempts:1,trusted:true};if(Q&&await x.pathExists(Q))await x.ensureDir(f.dirname(K)),await x.copyFile(Q,K),r.syncedArtifacts+=1,B=true,ee={sourceType:"path",source:Q,host:null,fetchedAt:new Date().toISOString(),attempts:1,trusted:true},o.push({id:`mirror.sync.${k}`,status:"passed",message:`Mirrored artifact ${k} from source path.`});else if(D.url){let H="";try{H=new URL(D.url).hostname.toLowerCase();}catch{o.push({id:`mirror.prefetch.${k}`,status:"failed",message:`Invalid URL for ${k}: ${D.url}`});continue}if(!(process.env.RAPIDKIT_TRUSTED_SOURCES==="1"||d.has(H))){o.push({id:`mirror.prefetch.trust.${k}`,status:"failed",message:`Untrusted mirror host for ${k}: ${H}. Add host to .rapidkit/trusted-sources.lock or set RAPIDKIT_TRUSTED_SOURCES=1.`});continue}if(e.offlineMode&&(await x.pathExists(K)?(B=true,ee={sourceType:"url",source:D.url,host:H,fetchedAt:new Date().toISOString(),attempts:0,trusted:true},o.push({id:`mirror.prefetch.${k}`,status:"passed",message:`Offline mode reused existing mirrored artifact ${k}.`})):o.push({id:`mirror.prefetch.${k}`,status:"failed",message:`Offline mode cannot prefetch remote artifact ${k} without an existing mirrored copy.`}),!B))continue;if(!B){let z=null,ve=0;for(let ke=1;ke<=y+1;ke+=1){ve=ke;try{await jr(D.url,K,p),r.syncedArtifacts+=1,B=true,ee={sourceType:"url",source:D.url,host:H,fetchedAt:new Date().toISOString(),attempts:ve,trusted:true},o.push({id:`mirror.prefetch.${k}`,status:"passed",message:ve>1?`Prefetched artifact ${k} from ${H} after ${ve} attempts.`:`Prefetched artifact ${k} from ${H}.`});break}catch(Ve){if(z=Ve,ke<=y){await wo(v*ke);continue}}}if(!B){o.push({id:`mirror.prefetch.${k}`,status:"failed",message:`Failed to prefetch ${k} after ${y+1} attempt(s): ${z?.message||"unknown error"}`});continue}}if(!B){o.push({id:`mirror.prefetch.${k}`,status:"failed",message:`Failed to prefetch ${k}.`});continue}}if(!B){D.required||e.offlineMode?o.push({id:`mirror.sync.${k}`,status:"failed",message:`Mirror source missing for ${k}${Q?`: ${Q}`:""}`}):o.push({id:`mirror.sync.${k}`,status:"skipped",message:`Mirror source not found for optional artifact ${k}.`});continue}let C=await Er(K);if(D.sha256&&D.sha256.toLowerCase()!==C.toLowerCase()){o.push({id:`mirror.verify.${k}`,status:"failed",message:`Checksum mismatch for ${k}.`});continue}r.verifiedArtifacts+=1,o.push({id:`mirror.verify.${k}`,status:"passed",message:`Checksum verified for ${k}.`});let R=D.attestation?await Mr(t,K,D.attestation):null;if(D.attestation){if(o.push({id:`mirror.attest.${k}`,status:R?.verified?"passed":"failed",message:R?.message||"Attestation verification failed."}),!R?.verified)continue}else if(g){o.push({id:`mirror.attest.${k}`,status:"failed",message:`Attestation is required but missing for ${k}.`});continue}else o.push({id:`mirror.attest.${k}`,status:"skipped",message:`No attestation provided for ${k}.`});let O=D.attestation?.sigstore,j=O?await Dr(t,K,O,{requireTransparencyLog:I}):null;if(O){if(o.push({id:`mirror.sigstore.${k}`,status:j?.verified?"passed":"failed",message:j?.message||"Sigstore verification failed."}),fe.push({artifactId:k,verified:!!j?.verified,tlogVerified:!!j?.tlogVerified,identity:j?.identity||null,issuer:j?.issuer||null,rekorUrl:j?.rekorUrl||null,timestamp:new Date().toISOString(),environment:_}),!j?.verified)continue}else if(A){o.push({id:`mirror.sigstore.${k}`,status:"failed",message:`Sigstore attestation is required but missing for ${k}.`});continue}else o.push({id:`mirror.sigstore.${k}`,status:"skipped",message:`No Sigstore attestation provided for ${k}.`});if(O&&j?.verified&&T){let H=T.allowedIdentities||[];if(H.length>0){let z=!!j.identity&&H.includes(j.identity);if(o.push({id:`mirror.sigstore.policy.identity.${k}`,status:z?"passed":"failed",message:z?`Sigstore identity policy passed for ${k} in ${_}.`:`Sigstore identity policy failed for ${k} in ${_}.`}),!z)continue}let de=T.allowedIssuers||[];if(de.length>0){let z=!!j.issuer&&de.includes(j.issuer);if(o.push({id:`mirror.sigstore.policy.issuer.${k}`,status:z?"passed":"failed",message:z?`Sigstore issuer policy passed for ${k} in ${_}.`:`Sigstore issuer policy failed for ${k} in ${_}.`}),!z)continue}let we=T.allowedRekorUrls||[];if(we.length>0){let z=!!j.rekorUrl&&we.includes(j.rekorUrl);if(o.push({id:`mirror.sigstore.policy.rekor.${k}`,status:z?"passed":"failed",message:z?`Sigstore Rekor policy passed for ${k} in ${_}.`:`Sigstore Rekor policy failed for ${k} in ${_}.`}),!z)continue}}else T&&o.push({id:`mirror.sigstore.policy.${k}`,status:"skipped",message:`Sigstore governance policy configured for ${_} but no verified Sigstore attestation for ${k}.`});let Y=await promises.stat(K);le.push({id:k,path:f.relative(t,K),sha256:C,size:Y.size,provenance:ee,attestation:{detached:{provided:!!D.attestation,verified:R?.verified||false,algorithm:R?.algorithm||null,publicKeyPath:R?.publicKeyPath||null,publicKeyFingerprint:R?.publicKeyFingerprint||null,signature:R?.signature||null,verifiedAt:R?.verified?new Date().toISOString():null},sigstore:{provided:!!O,verified:j?.verified||false,tlogVerified:j?.tlogVerified||false,identity:j?.identity||null,issuer:j?.issuer||null,rekorUrl:j?.rekorUrl||null,bundlePath:j?.bundlePath||null,certificatePath:j?.certificatePath||null,signaturePath:j?.signaturePath||null,verifiedAt:j?.verified?new Date().toISOString():null}}});}let ae=l.retention?.keepLast;if(typeof ae=="number"&&ae>0){let D=(await promises.readdir(c,{withFileTypes:true})).filter(k=>k.isFile()).map(k=>f.join(c,k.name));if(D.length>ae){let k=await Promise.all(D.map(async Z=>({filePath:Z,stat:await promises.stat(Z)})));k.sort((Z,K)=>K.stat.mtimeMs-Z.stat.mtimeMs);let Q=k.slice(ae);for(let Z of Q)await promises.unlink(Z.filePath),r.rotatedFiles+=1;}}o.push({id:"mirror.rotate",status:"passed",message:r.rotatedFiles>0?`Mirror retention rotation removed ${r.rotatedFiles} file(s).`:"Mirror retention rotation completed with no removals."});let je={schemaVersion:"1.0",generatedAt:new Date().toISOString(),mode:l.mode||null,environment:_,artifacts:le};if(await promises.writeFile(a,`${JSON.stringify(je,null,2)}
|
|
103
|
+
`,"utf-8"),r.lockWritten=true,o.push({id:"mirror.lock.write",status:"passed",message:`Mirror lock updated at ${f.relative(t,a)}.`}),r.transparencyEvidenceRecords=fe.length,fe.length>0){let U={schemaVersion:"1.0",generatedAt:new Date().toISOString(),environment:_,records:fe},D=new Date().toISOString().replace(/[:.]/g,"-"),k=f.join(m,`transparency-evidence-${D}.json`),Q=f.join(m,"transparency-evidence.latest.json");if(await x.ensureDir(m),await ho(k,U),await ho(Q,U),r.transparencyEvidenceWritten=true,o.push({id:"sigstore.evidence.write",status:"passed",message:`Transparency evidence written to ${f.relative(t,Q)}.`}),b?.enabled){let Z=Math.max(1e3,b.timeoutMs??1e4);if(b.target==="file")if(!b.filePath)o.push({id:"sigstore.evidence.export.file",status:"failed",message:"Evidence export target=file requires security.evidenceExport.filePath."});else try{let K=me(t,b.filePath);await x.ensureDir(f.dirname(K)),await promises.appendFile(K,`${JSON.stringify(U)}
|
|
104
|
+
`,"utf-8"),r.evidenceExported=true,r.evidenceExportTarget=K,o.push({id:"sigstore.evidence.export.file",status:"passed",message:`Transparency evidence exported to file sink ${K}.`});}catch(K){o.push({id:"sigstore.evidence.export.file",status:"failed",message:`Evidence file export failed: ${K.message}`});}else if(b.target==="http")if(!b.endpoint)o.push({id:"sigstore.evidence.export.http",status:"failed",message:"Evidence export target=http requires security.evidenceExport.endpoint."});else {let K=Math.max(0,b.retries??0),B=Math.max(0,b.backoffMs??500),ee=Or(U,b.signing);ee.error&&o.push({id:"sigstore.evidence.export.http",status:"failed",message:ee.error});try{let C=b.authTokenEnv?process.env[b.authTokenEnv]:void 0,R=false,O=null;for(let j=1;j<=K+1;j+=1)try{if(ee.error)throw new Error(ee.error);await $r(b.endpoint,U,Z,C,ee.headers),r.evidenceExported=true,r.evidenceExportTarget=b.endpoint,o.push({id:"sigstore.evidence.export.http",status:"passed",message:j>1?`Transparency evidence exported to HTTP endpoint ${b.endpoint} after ${j} attempts.`:`Transparency evidence exported to HTTP endpoint ${b.endpoint}.`}),R=true;break}catch(Y){O=Y,j<=K&&await wo(B*j);}if(!R)throw O||new Error("unknown evidence export error")}catch(C){let R=`Evidence HTTP export failed: ${C.message}`;o.push({id:"sigstore.evidence.export.http",status:"failed",message:R});try{let O=await Tr(t,b.deadLetterPath,U,R);o.push({id:"sigstore.evidence.export.deadletter",status:"passed",message:`Evidence export failure persisted to dead-letter sink ${O}.`});}catch(O){o.push({id:"sigstore.evidence.export.deadletter",status:"failed",message:`Evidence dead-letter write failed: ${O.message}`});}}}if(b.failOnError&&o.some(B=>B.status==="failed"&&(B.id==="sigstore.evidence.export.file"||B.id==="sigstore.evidence.export.http")))return {checks:o,details:r}}else o.push({id:"sigstore.evidence.export",status:"skipped",message:"Central evidence export not configured (security.evidenceExport.enabled=false)."});}else o.push({id:"sigstore.evidence.write",status:"skipped",message:"No Sigstore records available for transparency evidence output."}),o.push({id:"sigstore.evidence.export",status:"skipped",message:"Central evidence export skipped because no transparency evidence records exist."});return {checks:o,details:r}}function Et(t){if(!t||typeof t!="object")return null;let e=t.code;return e==="PYTHON_NOT_FOUND"||e==="BRIDGE_VENV_BOOTSTRAP_FAILED"?e:null}function Lr(t){let e=t.trim().toLowerCase();return e?e.startsWith("fastapi")?"fastapi":e.startsWith("nestjs")?"nestjs":null:null}function ht(t){let e=t.trim().toLowerCase();return e.startsWith("gofiber")||e==="go"||e==="go.standard"||e==="fiber"}function Ue(t){let e=t.trim().toLowerCase();return e.startsWith("gogin")||e==="gin"}function Je(t,e){let o=t.indexOf(e);if(o>=0&&o+1<t.length)return t[o+1];let r=t.find(n=>n.startsWith(`${e}=`));if(r)return r.slice(e.length+1)}function Ot(){return d$1()}function _o(){let t={...process.env},e=t.PATH||"";return e&&(t.PATH=e.split(f.delimiter).filter(o=>!o.replace(/\\/g,"/").includes("/.pyenv/shims")).join(f.delimiter)),t.PYENV_VERSION="system",t.POETRY_PYTHON||(t.POETRY_PYTHON=c$2()),t.RAPIDKIT_SKIP_LOCK_SYNC||(t.RAPIDKIT_SKIP_LOCK_SYNC="1"),t.POETRY_KEYRING_ENABLED||(t.POETRY_KEYRING_ENABLED="false"),t.PYTHON_KEYRING_BACKEND||(t.PYTHON_KEYRING_BACKEND="keyring.backends.null.Keyring"),t.POETRY_NO_INTERACTION||(t.POETRY_NO_INTERACTION="1"),t}function Fr(t){return f$1(f.join(t,".venv"))}async function Do(t,e){return await F(t,["--version"],e)===0}async function At(t){let e=f.join(t,"go.mod");if(await x__default.pathExists(e))return "go";let o=f.join(t,"package.json");if(await x__default.pathExists(o))return "node";let r=f.join(t,"pyproject.toml"),n=f.join(t,"requirements.txt"),i=f.join(t,"poetry.lock");return await x__default.pathExists(r)||await x__default.pathExists(n)||await x__default.pathExists(i)?"python":null}async function Wr(t){for(let e of Ot())if(await F(e,e==="py"?["-3","-m","venv",".venv"]:["-m","venv",".venv"],t)===0)return 0;return 1}async function No(t){for(let e of Ot())if(await F(e,e==="py"?["-3","-m","venv",".venv"]:["-m","venv",".venv"],t)===0)return 0;return 1}async function Gr(t){let e=f$1(f.join(t,".venv"));if(!await x__default.pathExists(e)){let i=await No(t);if(i!==0)return i}if(!await Do("poetry",t))return 0;let r=await F("poetry",["config","virtualenvs.in-project","true","--local"],t);if(r!==0)return r;let n=await F("poetry",["env","use",e],t);return n!==0?n:0}async function qr(t){let e=f$1(f.join(t,".venv"));if(!await x__default.pathExists(e)){let n=await No(t);if(n!==0)return n}await F(e,["-m","pip","install","--upgrade","pip","setuptools","wheel"],t);let o=f.join(t,"requirements.txt");if(await x__default.pathExists(o)&&await F(e,["-m","pip","install","-r","requirements.txt"],t)===0)return 0;let r=f.join(t,"pyproject.toml");return await x__default.pathExists(r)&&(await F(e,["-m","pip","install","-e","."],t)===0||await F(e,["-m","pip","install","."],t)===0)?0:1}async function mt(t,e){return await Gr(t)!==0&&console.log(s.yellow("\u26A0\uFE0F Could not fully configure Poetry local venv. Trying fallback installer...")),(await e.initProject(t)).exitCode===0&&await x__default.pathExists(f.join(t,".venv"))?0:(console.log(s.yellow("\u26A0\uFE0F Python init fallback: installing dependencies directly into project .venv")),await qr(t))}async function ft(t){let e=await He("init",t);if(e===0)return 0;let o=["npm","pnpm","yarn"];for(let r of o){if(!await Do(r,t))continue;if(await F(r,["install"],t)===0)return console.log(s.green(`\u2705 Node init fallback succeeded with ${r} install`)),0}return e}async function xo(t){if(t[0]!=="create"||t[1]!=="project")return 1;let e=t[2],o=t[3];if(!e||!o)return process.stderr.write(`Usage: rapidkit create project gofiber.standard <name> [--output <dir>]
|
|
105
|
+
`),1;let r=Je(t,"--output")||process.cwd(),n=f.resolve(r,o),i=t.includes("--skip-git")||t.includes("--no-git");try{let{default:a}=await import('fs-extra');if(await a.ensureDir(f.dirname(n)),await a.pathExists(n))return process.stderr.write(`\u274C Directory "${n}" already exists
|
|
106
|
+
`),1;await a.ensureDir(n);let{generateGoFiberKit:c}=await import('./gofiber-standard-NQHMZTFK.js');await c(n,{project_name:o,module_path:o,skipGit:i});let m=q(process.cwd());if(m){let{syncWorkspaceProjects:d}=await import('./workspace-VXNLNKCM.js');await d(m,true);}return 0}catch(a){return process.stderr.write(`RapidKit Go/Fiber generator failed: ${a?.message??a}
|
|
107
|
+
`),1}}async function Ro(t){if(t[0]!=="create"||t[1]!=="project")return 1;let e=t[2],o=t[3];if(!e||!o)return process.stderr.write(`Usage: rapidkit create project gogin.standard <name> [--output <dir>]
|
|
108
|
+
`),1;let r=Je(t,"--output")||process.cwd(),n=f.resolve(r,o),i=t.includes("--skip-git")||t.includes("--no-git");try{let{default:a}=await import('fs-extra');if(await a.ensureDir(f.dirname(n)),await a.pathExists(n))return process.stderr.write(`\u274C Directory "${n}" already exists
|
|
109
|
+
`),1;await a.ensureDir(n);let{generateGoGinKit:c}=await import('./gogin-standard-2G2C3VQL.js');await c(n,{project_name:o,module_path:o,skipGit:i});let m=q(process.cwd());if(m){let{syncWorkspaceProjects:d}=await import('./workspace-VXNLNKCM.js');await d(m,true);}return 0}catch(a){return process.stderr.write(`RapidKit Go/Gin generator failed: ${a?.message??a}
|
|
110
|
+
`),1}}async function It(t,e){if(t.includes("--json"))return process.stderr.write("RapidKit (npm) offline fallback does not support --json for `create` commands.\nInstall Python 3.10+ and retry the same command.\n"),1;if(t[0]!=="create")return 1;if(t[1]!=="project")return process.stderr.write(`RapidKit (npm) could not run the Python core engine for \`create\`.
|
|
111
|
+
Reason: ${e}.
|
|
3513
112
|
Install Python 3.10+ to use the interactive wizard and full kit catalog.
|
|
3514
|
-
`),1;let
|
|
113
|
+
`),1;let n=t[2],i=t[3];if(!n||!i)return process.stderr.write(`Usage: rapidkit create project <kit> <name> [--output <dir>]
|
|
3515
114
|
Tip: offline fallback supports only fastapi* and nestjs* kits.
|
|
3516
|
-
`),1;let a=
|
|
3517
|
-
Reason: ${
|
|
3518
|
-
Requested kit: ${
|
|
115
|
+
`),1;let a=Lr(n);if(!a)return process.stderr.write(`RapidKit (npm) could not run the Python core engine to create this kit.
|
|
116
|
+
Reason: ${e}.
|
|
117
|
+
Requested kit: ${n}
|
|
3519
118
|
Offline fallback only supports: fastapi.standard, nestjs.standard (and their shorthands).
|
|
3520
119
|
Install Python 3.10+ to access all kits.
|
|
3521
|
-
`),1;let
|
|
3522
|
-
`),1;let u="pip",
|
|
3523
|
-
`),1}}async function
|
|
3524
|
-
`),1;try{
|
|
3525
|
-
`),1;throw
|
|
3526
|
-
`),1;let u=await a$2(),
|
|
3527
|
-
`),1}try{if(
|
|
3528
|
-
`),1}if(!
|
|
120
|
+
`),1;let c=Je(t,"--output")||process.cwd(),m=f.resolve(c,i),d=t.includes("--skip-git")||t.includes("--no-git"),l=t.includes("--skip-install");try{if(await x__default.ensureDir(f.dirname(m)),await x__default.pathExists(m))return process.stderr.write(`\u274C Directory "${m}" already exists
|
|
121
|
+
`),1;let u="pip",y=q(process.cwd());if(y)try{let{readWorkspaceMarker:p}=await import('./workspace-marker-IOPQ42A7.js'),g=await p(y);g?.metadata?.npm?.installMethod&&(u=g.metadata.npm.installMethod,a$1.debug(`Detected workspace engine: ${u}`));}catch(p){a$1.debug("Failed to read workspace marker",p);}else a$1.debug("No workspace found, using default engine: pip");await x__default.ensureDir(m);let{generateDemoKit:v}=await import('./demo-kit-63CFMCPD.js');if(await v(m,{project_name:i,template:a,kit_name:n,skipGit:d,skipInstall:l,engine:u}),y){let{syncWorkspaceProjects:p}=await import('./workspace-VXNLNKCM.js');await p(y,true);}return 0}catch(u){return process.stderr.write(`RapidKit (npm) offline fallback failed: ${u?.message??u}
|
|
122
|
+
`),1}}async function Ko(t){let e$1=new Set(["--yes","-y","--skip-git","--skip-install","--debug","--dry-run","--no-update-check","--create-workspace","--no-workspace"]);if(t[0]==="create"&&(!t[1]||t[1].startsWith("-"))){let o=t.includes("--yes")||t.includes("-y"),r=t.slice(1),n;!process.stdin.isTTY||o?(n="workspace",process.stdin.isTTY&&console.log(s.gray("\u2139\uFE0F No subcommand provided for `create`; defaulting to `create workspace`."))):n=(await he.prompt([{type:"rawlist",name:"createTarget",message:"What do you want to create?",choices:[{name:"workspace",value:"workspace"},{name:"project",value:"project"}]}])).createTarget;let i=["create",n,...r];return await Ko(i)}if(t[0]==="create"&&t[1]==="workspace")try{let o=t.includes("--yes")||t.includes("-y"),r=t.includes("--skip-git")||t.includes("--no-git"),n=t[2]&&!t[2].startsWith("-")?t[2]:void 0,i=Je(t,"--install-method"),a=i==="poetry"||i==="venv"||i==="pipx"?i:void 0,c=Je(t,"--profile"),m=c==="minimal"||c==="go-only"||c==="python-only"||c==="node-only"||c==="polyglot"||c==="enterprise"?c:void 0,d=n||(o?"my-workspace":(await he.prompt([{type:"input",name:"workspaceName",message:"Workspace name:",default:"my-workspace"}])).workspaceName);if(!d||!d.trim())return process.stderr.write(`Workspace name is required.
|
|
123
|
+
`),1;try{vt(d);}catch(p){if(p instanceof e)return process.stderr.write(`${p.message}
|
|
124
|
+
`),1;throw p}let l=f.resolve(process.cwd(),d);if(await x__default.pathExists(l))return process.stderr.write(`\u274C Directory "${d}" already exists
|
|
125
|
+
`),1;let u=await a$2(),y=u.author||process.env.USER||"RapidKit User";if(!o){let p=await he.prompt([{type:"input",name:"author",message:"Author name:",default:y}]);p.author?.trim()&&(y=p.author.trim());}let{createProject:v}=await import('./create-27NVMJAR.js');return await v(d,{skipGit:r,yes:o,userConfig:{...u,author:y},installMethod:a,profile:m}),0}catch(o){return process.stderr.write(`RapidKit (npm) failed to create workspace: ${o?.message??o}
|
|
126
|
+
`),1}try{if(t[0]==="create"&&t[1]==="project"){if(t.includes("--help")||t.includes("-h"))try{return await c$1(),await d(["create","project","--help"],{cwd:process.cwd()})}catch(p){return process.stderr.write(`RapidKit (npm) failed to run the Python core engine: ${p?.message??p}
|
|
127
|
+
`),1}if(!t[2]||t[2].startsWith("-")){console.log(s.bold(`
|
|
3529
128
|
\u{1F680} RapidKit
|
|
3530
|
-
`));let{kitChoice:
|
|
3531
|
-
`),a$1.debug(`Synced Python version ${
|
|
3532
|
-
`),1)}}if(
|
|
3533
|
-
`),1)}return await c$
|
|
3534
|
-
`),1)}}var
|
|
3535
|
-
rules:`):`${
|
|
3536
|
-
`)?
|
|
129
|
+
`));let{kitChoice:p}=await he.prompt([{type:"rawlist",name:"kitChoice",message:"Select a kit to scaffold:",choices:[{name:"fastapi \u2014 FastAPI Standard Kit",value:"fastapi.standard"},{name:"fastapi \u2014 FastAPI DDD Kit",value:"fastapi.ddd"},{name:"nestjs \u2014 NestJS Standard Kit",value:"nestjs.standard"},{name:"go/fiber \u2014 Go Fiber Standard Kit",value:"gofiber.standard"},{name:"go/gin \u2014 Go Gin Standard Kit",value:"gogin.standard"}]}]);if(ht(p)||Ue(p)){let{projectName:A}=await he.prompt([{type:"input",name:"projectName",message:"Project name:",validate:S=>S.trim().length>0||"Project name is required"}]),P=t.slice(2).filter(S=>S.startsWith("-"));return Ue(p)?await Ro(["create","project",p,A.trim(),...P]):await xo(["create","project",p,A.trim(),...P])}let{projectName:g}=await he.prompt([{type:"input",name:"projectName",message:"Project name:",validate:A=>A.trim().length>0||"Project name is required"}]);t.splice(2,0,p,g.trim());}{let p=q(process.cwd()),g=(t[2]||"").toLowerCase();if(p&&g){let A=f.join(p,".rapidkit","workspace.json"),P=f.join(p,".rapidkit","policies.yml");try{let[S,b]=await Promise.all([x__default.pathExists(A).then(ae=>ae?w.promises.readFile(A,"utf-8"):"{}"),x__default.pathExists(P).then(ae=>ae?w.promises.readFile(P,"utf-8"):"")]),_=JSON.parse(S).profile,T=b.match(/^\s*mode:\s*(warn|strict)\s*(?:#.*)?$/m)?.[1]??"warn",I=ht(g)||Ue(g)||g.startsWith("go"),fe=["nestjs","react","vue","nextjs","next","vite","angular","svelte","express","koa","fastify"].some(ae=>g.includes(ae)),Ne=!I&&!fe,le=null;if(_==="python-only"&&!Ne?le=`Kit "${g}" is not a Python kit, but workspace profile is "python-only".`:_==="node-only"&&!fe?le=`Kit "${g}" is not a Node kit, but workspace profile is "node-only".`:_==="go-only"&&!I&&(le=`Kit "${g}" is not a Go kit, but workspace profile is "go-only".`),le){if(T==="strict")return console.log(s.red(`\u274C Profile violation (strict mode): ${le}`)),console.log(s.gray("\u{1F4A1} Change workspace profile or use --no-workspace to skip enforcement.")),1;console.log(s.yellow(`\u26A0\uFE0F Profile warning: ${le}`)),console.log(s.gray('\u{1F4A1} Consider using a "polyglot" workspace profile for multi-language projects.'));}}catch{}}}if(ht(t[2]||""))return await xo(t);if(Ue(t[2]||""))return await Ro(t);let r=t.includes("--create-workspace"),n=t.includes("--no-workspace"),i=t.includes("--yes")||t.includes("-y"),a=t.includes("--skip-git")||t.includes("--no-git");if(!!!$t(process.cwd())){let{registerWorkspaceAtPath:p}=await import('./create-27NVMJAR.js');if(r)await p(process.cwd(),{skipGit:a,yes:i,userConfig:await a$2()});else if(!n)if(i)await p(process.cwd(),{skipGit:a,yes:true,userConfig:await a$2()});else {let{createWs:g}=await he.prompt([{type:"confirm",name:"createWs",message:"This project will be created outside a RapidKit workspace. Create and register a workspace here?",default:true}]);g&&await p(process.cwd(),{skipGit:a,yes:false,userConfig:await a$2()});}}let d$1=[...t.filter(p=>{let g=p.split("=")[0];return !e$1.has(p)&&!e$1.has(g)})],l=q(process.cwd()),v=t.includes("--skip-install")||!!l?{...process.env,RAPIDKIT_SKIP_LOCKS:"1",RAPIDKIT_GENERATE_LOCKS:"0"}:void 0;try{await c$1();let p=await d(d$1,{cwd:process.cwd(),env:v});if(p===0&&l&&!t.includes("--skip-install")&&(console.log(s.gray("\u2139\uFE0F Fast create mode (workspace): dependencies were deferred.")),console.log(s.white(" Next: cd <project-name> && npx rapidkit init"))),p===0){let g=l||q(process.cwd());if(g){try{let P=t[3];if(P){let S=t.indexOf("--output"),b=S>=0?t[S+1]:".",_=f.resolve(process.cwd(),b,P),M=f.join(g,".python-version"),T=f.join(_,".python-version");if(w.existsSync(M)&&w.existsSync(_)){let I=w.readFileSync(M,"utf-8");w.writeFileSync(T,I.trim()+`
|
|
130
|
+
`),a$1.debug(`Synced Python version ${I.trim()} from workspace to ${P}`);}}}catch(P){a$1.debug("Could not sync Python version from workspace:",P);}let{syncWorkspaceProjects:A}=await import('./workspace-VXNLNKCM.js');await A(g,true);}}return p}catch(p){let g=Et(p);return g?await It(d$1,g):(process.stderr.write(`RapidKit (npm) failed to run the Python core engine: ${p?.message??p}
|
|
131
|
+
`),1)}}if(t[0]==="create"&&t[1]!=="project")try{await c$1();let o=await d(t,{cwd:process.cwd()});if(o===0){let r=q(process.cwd());if(r){let{syncWorkspaceProjects:n}=await import('./workspace-VXNLNKCM.js');await n(r,true);}}return o}catch(o){let r=Et(o);return r?await It(t,r):(process.stderr.write(`RapidKit (npm) failed to run the Python core engine: ${o?.message??o}
|
|
132
|
+
`),1)}return await c$1(),await d(t,{cwd:process.cwd()})}catch(o){let r=Et(o);return r?await It(t,r):(process.stderr.write(`RapidKit (npm) failed to run the Python core engine: ${o?.message??o}
|
|
133
|
+
`),1)}}var So=["init","dev","start","build","test","docs","lint","format","create","help","--help","-h"],Ur=["doctor","workspace","bootstrap","setup","cache","mirror","ai","config","shell"],Hr=["doctor","workspace","ai","config","shell"],Jr=["bootstrap","setup","cache","mirror"],Lo=["lint","format","docs"],Vr=["init"],Fo=["build","dev","start","test"],Eo=[...Fo,...Lo];function wt(t){return !!t&&Ur.includes(t)}function Ao(t){return !!t&&Hr.includes(t)}function Br(t){return !!t&&Jr.includes(t)}function Wo(t){return w.existsSync(f.join(t,".rapidkit-workspace"))||w.existsSync(f.join(t,".rapidkit","workspace.json"))}function Go(t){let e=t;for(;;){let o=f.join(e,".rapidkit","context.json");if(w.existsSync(o))return o;let r=f.dirname(e);if(r===e)break;e=r;}return null}function $t(t){let e=t;for(;;){let o=f.join(e,".rapidkit-workspace");if(w.existsSync(o))return o;let r=f.dirname(e);if(r===e)break;e=r;}return null}function q(t){let e=t;for(;;){let o=f.join(e,".rapidkit-workspace");if(w.existsSync(o))return e;let r=f.dirname(e);if(r===e)break;e=r;}return null}async function Yr(t,e=process.cwd(),o=process.platform){if(!(t.workspaceFlag||t.scope==="workspace"))return {detected:false};let n=i(e,o);for(let i of n)if(await x__default.pathExists(i)){if(a(o)){let a=i.toLowerCase();if(a.endsWith("rapidkit.cmd")||a.endsWith("rapidkit.exe"))return {detected:true,candidatePath:i,reason:"Found workspace-local rapidkit launcher on Windows."}}else if(f.basename(i)==="rapidkit")return {detected:true,candidatePath:i,reason:"Found workspace-local rapidkit bash launcher on Linux/macOS."}}return {detected:false}}function zr(t){let e=t;for(;;){let o=f.join(e,".rapidkit-workspace"),r=f.join(e,".rapidkit","workspace.json");if(!w.existsSync(o)&&w.existsSync(r))return e;let n=f.dirname(e);if(n===e)break;e=n;}return null}var qo={enforce_workspace_marker:true,enforce_toolchain_lock:false,disallow_untrusted_tool_sources:false,enforce_compatibility_matrix:false,require_mirror_lock_for_offline:true};function Qr(t){return t&&t.match(/^[\t ]*mode:\s*(warn|strict)\s*(?:#.*)?$/m)?.[1]==="strict"?"strict":"warn"}function qe(t,e){let o=t.match(new RegExp(`^[\\t ]*${e}:\\s*(true|false)\\s*(?:#.*)?$`,"m"));return o?o[1]==="true":qo[e]}function Io(t){let e=t??"";return {mode:Qr(e),dependency_sharing_mode:Uo(e),rules:{enforce_workspace_marker:qe(e,"enforce_workspace_marker"),enforce_toolchain_lock:qe(e,"enforce_toolchain_lock"),disallow_untrusted_tool_sources:qe(e,"disallow_untrusted_tool_sources"),enforce_compatibility_matrix:qe(e,"enforce_compatibility_matrix"),require_mirror_lock_for_offline:qe(e,"require_mirror_lock_for_offline")}}}function jo(t,e,o){let r=`${e}: ${o}`,n=e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),i=new RegExp(`^[\\t ]*${n}:\\s*.*$`,"m");if(i.test(t))return t.replace(i,r);let a=/^[\t ]*rules:\s*(?:#.*)?$/m;return a.test(t)?t.replace(a,`${r}
|
|
134
|
+
rules:`):`${t.endsWith(`
|
|
135
|
+
`)?t:`${t}
|
|
3537
136
|
`}${r}
|
|
3538
|
-
`}function
|
|
3539
|
-
${r}`):`${
|
|
3540
|
-
`)?
|
|
137
|
+
`}function Zr(t,e,o){let r=` ${e}: ${o?"true":"false"}`,n=e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),i=new RegExp(`^[\\t ]+${n}:\\s*.*$`,"m");if(i.test(t))return t.replace(i,r);let a=/^[\t ]*rules:\s*(?:#.*)?$/m;return a.test(t)?t.replace(a,`rules:
|
|
138
|
+
${r}`):`${t.endsWith(`
|
|
139
|
+
`)?t:`${t}
|
|
3541
140
|
`}rules:
|
|
3542
141
|
${r}
|
|
3543
|
-
`}function
|
|
3544
|
-
`)}async function
|
|
3545
|
-
`)?
|
|
3546
|
-
`;await
|
|
3547
|
-
`,"utf-8");}async function
|
|
3548
|
-
`,"utf-8");}catch{}let
|
|
3549
|
-
`):(console.log(
|
|
3550
|
-
`);else {let
|
|
3551
|
-
`,"utf-8"),console.log(
|
|
3552
|
-
Hints:`));for(let
|
|
3553
|
-
`)){let r=
|
|
3554
|
-
`,"utf-8"),console.log(
|
|
3555
|
-
`),0):(console.log(
|
|
3556
|
-
`),1;console.log(
|
|
3557
|
-
`),1;console.log(
|
|
3558
|
-
`),0):
|
|
3559
|
-
No projects yet \u2014 create one and then run init inside it:`)),console.log(
|
|
3560
|
-
\u{1F4A1} Go dependencies are managed per-project (go.mod / go mod tidy).`))):(console.log(
|
|
3561
|
-
No projects yet \u2014 create one to get started:`)),console.log(
|
|
3562
|
-
`).toLowerCase(),
|
|
3563
|
-
`);for(let
|
|
3564
|
-
`);process.exit(1);}}let
|
|
3565
|
-
`);for(let
|
|
3566
|
-
`);process.exit(1);}}if(g&&r&&
|
|
142
|
+
`}function Xr(){return ['version: "1.0"','mode: warn # "warn" or "strict"','dependency_sharing_mode: isolated # "isolated" or "shared-runtime-caches" or "shared-node-deps"',"# change profile (recommended): npx rapidkit bootstrap --profile polyglot","# change mode/dependency manually: edit this file and rerun npx rapidkit init","rules:"," enforce_workspace_marker: true"," enforce_toolchain_lock: false"," disallow_untrusted_tool_sources: false"," enforce_compatibility_matrix: false"," require_mirror_lock_for_offline: true",""].join(`
|
|
143
|
+
`)}async function $o(t){let e=f.join(t,".rapidkit","policies.yml");return await x__default.pathExists(e)?w.promises.readFile(e,"utf-8"):Xr()}async function en(t,e){let o=f.join(t,".rapidkit"),r=f.join(o,"policies.yml");await x__default.ensureDir(o);let n=e.endsWith(`
|
|
144
|
+
`)?e:`${e}
|
|
145
|
+
`;await w.promises.writeFile(r,n,"utf-8");}function tn(t){let e=t.trim().toLowerCase();return e==="true"||e==="1"||e==="on"?true:e==="false"||e==="0"||e==="off"?false:null}function Uo(t){if(!t)return "isolated";let o=t.match(/^\s*dependency_sharing_mode:\s*([a-zA-Z\-]+)\s*(?:#.*)?$/m)?.[1]?.toLowerCase();return o==="shared-runtime-caches"||o==="shared-node-deps"||o==="isolated"?o:"isolated"}function Ho(t){if(!t)return {mode:"isolated",status:"skipped",message:"No policies.yml found; dependency_sharing_mode defaults to isolated."};let e=t.match(/^\s*dependency_sharing_mode:\s*([a-zA-Z\-]+)\s*(?:#.*)?$/m);if(!e)return {mode:"isolated",status:"skipped",message:"dependency_sharing_mode is not set; defaulting to isolated."};let o=e[1].toLowerCase();return o==="isolated"||o==="shared-runtime-caches"||o==="shared-node-deps"?{mode:o,status:"passed",message:`dependency_sharing_mode is valid: ${o}.`}:{mode:"isolated",status:"failed",message:`Invalid dependency_sharing_mode: ${o}. Use one of: isolated, shared-runtime-caches, shared-node-deps.`}}async function Jo(t,e){let o=q(t),r=o?f.join(o,".rapidkit","policies.yml"):null,n="isolated";if(r&&await x__default.pathExists(r))try{let c=await w.promises.readFile(r,"utf-8"),m=Ho(c);if(m.status==="failed")return console.log(s.red(`\u274C ${m.message}`)),{ok:false,code:1};n=m.mode;}catch{return console.log(s.red("\u274C Failed to read workspace policy file (.rapidkit/policies.yml).")),{ok:false,code:1}}let i=process.env.RAPIDKIT_DEP_SHARING_MODE,a=process.env.RAPIDKIT_WORKSPACE_PATH;process.env.RAPIDKIT_DEP_SHARING_MODE=n,o&&(process.env.RAPIDKIT_WORKSPACE_PATH=o);try{return {ok:true,value:await e()}}finally{typeof i>"u"?delete process.env.RAPIDKIT_DEP_SHARING_MODE:process.env.RAPIDKIT_DEP_SHARING_MODE=i,typeof a>"u"?delete process.env.RAPIDKIT_WORKSPACE_PATH:process.env.RAPIDKIT_WORKSPACE_PATH=a;}}async function F(t,e,o){return await new Promise(r=>{let n=spawn(t,e,{stdio:"inherit",cwd:o,shell:b$2()});n.on("close",i=>r(i??1)),n.on("error",()=>r(1));})}async function De(t,e){await x__default.outputFile(t,`${JSON.stringify(e,null,2)}
|
|
146
|
+
`,"utf-8");}async function on(t){let e=new Set(["go-only"]);try{let r=f.join(t,".rapidkit","workspace.json"),n=JSON.parse(await w.promises.readFile(r,"utf-8"));if(e.has(n.profile??""))return 0}catch{}let o="poetry";try{let{readWorkspaceMarker:r}=await import('./workspace-marker-IOPQ42A7.js'),i=(await r(t))?.metadata?.npm?.installMethod;(i==="poetry"||i==="venv"||i==="pipx"||i==="pip")&&(o=i);}catch{}if(o==="poetry"||o==="venv"){let r=f.join(t,"pyproject.toml"),n=false;try{n=(await w.promises.readFile(r,"utf-8")).includes("rapidkit-core");}catch{n=false;}let i=process.env.RAPIDKIT_DEV_PATH,a=i?await x__default.pathExists(i):false;if(n){let c=Fr(t);if(!await x__default.pathExists(c)){let l=await Wr(t);if(l!==0)return l}let d=await F(c,a&&i?["-m","pip","install",i,"--quiet","--disable-pip-version-check"]:["-m","pip","install","rapidkit-core","--quiet","--disable-pip-version-check"],t);if(d!==0)return d}else {let c=await F("poetry",["install","--no-root"],t);if(c!==0)return c;let m=await F("poetry",["add","rapidkit-core"],t);if(m!==0)return m}try{let{writeWorkspaceLauncher:c}=await import('./create-27NVMJAR.js');await c(t,"poetry");}catch{}return 0}return 0}async function Vo(t){let e=await w.promises.readdir(t,{withFileTypes:true}),o=[];for(let r of e){if(!r.isDirectory()||r.name.startsWith("."))continue;let n=f.join(t,r.name),i=f.join(n,".rapidkit","context.json"),a=f.join(n,".rapidkit","project.json");(await x__default.pathExists(i)||await x__default.pathExists(a))&&o.push(n);}return o}function rn(t){let e="my-workspace",o=1;for(;;){let r=o===1?e:`${e}-${o}`,n=f.join(t,r);if(!w.existsSync(n))return {name:r,targetPath:n};o+=1;}}async function gt(t){let o=await Se("go",{runCommandInCwd:F,runCoreRapidkit:d}).initProject(t);return o.message&&console.log(s.red(`\u274C ${o.message}`)),o.exitCode}async function He(t,e){let o=Se("node",{runCommandInCwd:F,runCoreRapidkit:d});return t==="init"?(await o.initProject(e)).exitCode:t==="dev"?(await o.runDev(e)).exitCode:t==="test"?(await o.runTest(e)).exitCode:t==="build"?(await o.runBuild(e)).exitCode:(await o.runStart(e)).exitCode}async function nn(t,e=Tt){let o=process.env.RAPIDKIT_SKIP_LOCK_SYNC;typeof o>"u"&&(process.env.RAPIDKIT_SKIP_LOCK_SYNC="1");try{let i=function(C){if(!C)return null;let R=C.trim().toLowerCase();return R==="minimal"||R==="go-only"||R==="python-only"||R==="node-only"||R==="polyglot"||R==="enterprise"?R:null},a=function(C){let j=C.match(/^\s*mode:\s*([a-zA-Z]+)\s*(?:#.*)?$/m)?.[1]?.toLowerCase()==="strict"?"strict":"warn",Y=(H,de)=>{let we=C.match(new RegExp(`^\\s*${H}:\\s*(true|false)\\s*(?:#.*)?$`,"m"));return we?we[1].toLowerCase()==="true":de};return {mode:j,dependency_sharing_mode:Uo(C),rules:{enforce_workspace_marker:Y("enforce_workspace_marker",true),enforce_toolchain_lock:Y("enforce_toolchain_lock",false),disallow_untrusted_tool_sources:Y("disallow_untrusted_tool_sources",false),enforce_compatibility_matrix:Y("enforce_compatibility_matrix",false),require_mirror_lock_for_offline:Y("require_mirror_lock_for_offline",true)}}};let c=["init"],m,d=false,l=false,u=false;for(let C=1;C<t.length;C+=1){let R=t[C];if(R==="--ci"){d=true;continue}if(R==="--offline"){l=true;continue}if(R==="--json"){u=true;continue}if(R==="--profile"){let O=t[C+1];if(!O||O.startsWith("-"))return console.log(s.yellow("Usage: rapidkit bootstrap [path] [--profile <minimal|go-only|python-only|node-only|polyglot|enterprise>] [--ci] [--offline] [--json]")),1;m=O,C+=1;continue}if(R.startsWith("--profile=")){m=R.slice(10);continue}c.push(R);}let y=i(m);if(m&&!y)return console.log(s.red(`Invalid profile: ${m}. Use one of: minimal, go-only, python-only, node-only, polyglot, enterprise.`)),1;let v=process.cwd(),p=q(v);p||(p=zr(v));let g=[],A=null,P=null;if(p)try{let C=f.join(p,".rapidkit","workspace.json"),R=await w.promises.readFile(C,"utf-8"),O=JSON.parse(R);P=i(O.profile);}catch{P=null;}let S=["minimal","python-only","node-only","go-only","polyglot","enterprise"],b={minimal:"minimal \u2014 Foundation files only (fastest bootstrap, mixed projects)","python-only":"python-only \u2014 Python + Poetry (FastAPI, Django, ML pipelines)","node-only":"node-only \u2014 Node.js runtime (NestJS, Express, Next.js)","go-only":"go-only \u2014 Go runtime (Fiber, Gin, gRPC, microservices)",polyglot:"polyglot \u2014 Python + Node.js + Go multi-runtime workspace",enterprise:"enterprise \u2014 Polyglot + governance + Sigstore verification"},_=y;if(!!p&&!y&&!d&&!u&&!!process.stdin.isTTY&&!!process.stdout.isTTY){let C=P||"minimal",{chosenProfile:R}=await he.prompt([{type:"rawlist",name:"chosenProfile",message:`Select workspace profile for bootstrap (current: ${C})`,choices:S.map(O=>({name:O===C?`${b[O]} \u2190 current`:b[O],value:O})),default:S.indexOf(C)}]);_=R;}let T=_||P||"minimal";if(p)try{let R=T==="python-only"||T==="polyglot"||T==="enterprise"?"poetry":"venv",O;try{let de=(await w.promises.readFile(f.join(p,".python-version"),"utf-8")).trim();de&&(O=de);}catch{}let{syncWorkspaceFoundationFiles:j}=await import('./create-27NVMJAR.js'),Y=await j(p,{workspaceName:f.basename(p),installMethod:R,pythonVersion:O,profile:T,writeMarker:true,writeGitignore:true,onlyIfMissing:true});g.push({id:"workspace.legacy.sync",status:Y.length>0?"passed":"skipped",message:Y.length>0?`Legacy workspace foundation synchronized: ${Y.join(", ")}`:"Workspace foundation files are already up to date."});}catch(C){g.push({id:"workspace.legacy.sync",status:"failed",message:`Failed to synchronize legacy workspace foundation files: ${C.message}`});}if(p&&_&&_!==P)try{let C=f.join(p,".rapidkit","workspace.json"),R=await w.promises.readFile(C,"utf-8"),O=JSON.parse(R);O.profile=_,await w.promises.writeFile(C,JSON.stringify(O,null,2)+`
|
|
147
|
+
`,"utf-8");}catch{}let I={mode:"warn",dependency_sharing_mode:"isolated",rules:{enforce_workspace_marker:true,enforce_toolchain_lock:false,disallow_untrusted_tool_sources:false,enforce_compatibility_matrix:false,require_mirror_lock_for_offline:true}},fe=null;if(p)try{let C=await w.promises.readFile(f.join(p,".rapidkit","policies.yml"),"utf-8");fe=C,I=a(C);}catch{g.push({id:"policy.file",status:"skipped",message:"No workspace policy file found; using default bootstrap policy."});}else g.push({id:"workspace.detect",status:"skipped",message:"No workspace marker found; bootstrap runs in project/single-path mode."});if(p){let C=Ho(fe);I.dependency_sharing_mode=C.mode,g.push({id:"policy.schema.dependency_sharing_mode",status:C.status,message:C.message}),g.push({id:"policy.dependency_sharing_mode.effective",status:"passed",message:I.dependency_sharing_mode==="isolated"?"Effective dependency mode: isolated (default secure mode).":I.dependency_sharing_mode==="shared-node-deps"?"Effective dependency mode: shared-node-deps (Node projects share workspace-level caches).":"Effective dependency mode: shared-runtime-caches (Node/Python/Go share workspace-level caches)."});let R=w.existsSync(f.join(p,".rapidkit-workspace"));g.push({id:"policy.enforce_workspace_marker",status:!I.rules.enforce_workspace_marker||R?"passed":"failed",message:!I.rules.enforce_workspace_marker||R?"Workspace marker policy satisfied.":"Workspace marker policy failed: .rapidkit-workspace is missing."});let O=w.existsSync(f.join(p,".rapidkit","toolchain.lock"));g.push({id:"policy.enforce_toolchain_lock",status:!I.rules.enforce_toolchain_lock||O?"passed":"failed",message:!I.rules.enforce_toolchain_lock||O?"Toolchain lock policy satisfied.":"Toolchain lock policy failed: .rapidkit/toolchain.lock is missing."});let j=process.env.RAPIDKIT_TRUSTED_SOURCES==="1"||w.existsSync(f.join(p,".rapidkit","trusted-sources.lock"));g.push({id:"policy.disallow_untrusted_tool_sources",status:!I.rules.disallow_untrusted_tool_sources||j?"passed":"failed",message:!I.rules.disallow_untrusted_tool_sources||j?"Trusted tool sources policy satisfied.":"Trusted tool sources policy failed: set RAPIDKIT_TRUSTED_SOURCES=1 or provide .rapidkit/trusted-sources.lock."});let Y=f.join(p,".rapidkit","compatibility-matrix.json"),H=w.existsSync(Y),de=I.rules.enforce_compatibility_matrix;if(g.push({id:"policy.enforce_compatibility_matrix",status:!de||H?"passed":"failed",message:!de||H?"Compatibility matrix policy satisfied.":"Compatibility matrix policy failed: .rapidkit/compatibility-matrix.json is missing."}),H)try{let N=await w.promises.readFile(Y,"utf-8"),V=JSON.parse(N),Be=!!V&&typeof V=="object";g.push({id:"compatibility.matrix.parse",status:Be?"passed":"failed",message:Be?"Compatibility matrix parsed successfully.":"Compatibility matrix parse failed: invalid JSON object."});}catch{g.push({id:"compatibility.matrix.parse",status:"failed",message:"Compatibility matrix parse failed: invalid JSON."});}let we=f.join(p,".rapidkit","mirror-config.json"),z=f.join(p,".rapidkit","mirror.lock"),ve=w.existsSync(we),ke=w.existsSync(z),Ve={};if(ve)try{Ve=JSON.parse(await w.promises.readFile(we,"utf-8")),g.push({id:"mirror.config.parse",status:"passed",message:"Mirror configuration parsed successfully."});}catch{g.push({id:"mirror.config.parse",status:"failed",message:"Mirror configuration parse failed: invalid JSON in .rapidkit/mirror-config.json."});}let kt=await St(p,{ciMode:d,offlineMode:l});if(g.push(...kt.checks.map(N=>({id:N.id,status:N.status,message:N.message}))),A=kt.details,kt.details.lockWritten&&(ke=true),l){let N=process.env.RAPIDKIT_MIRROR_ENABLED==="1"||Ve.enabled===true;g.push({id:"offline.mirror.enabled",status:N?"passed":"failed",message:N?"Offline mode mirror is enabled.":'Offline mode requires mirror enablement (set RAPIDKIT_MIRROR_ENABLED=1 or .rapidkit/mirror-config.json {"enabled": true}).'});let V=I.rules.require_mirror_lock_for_offline;g.push({id:"offline.mirror.lock",status:!V||ke?"passed":"failed",message:!V||ke?"Offline mode mirror lock policy satisfied.":"Offline mode mirror lock policy failed: .rapidkit/mirror.lock is missing."});}else g.push({id:"offline.mirror.enabled",status:"skipped",message:"Offline mirror checks skipped (offline mode is disabled)."});let Yo=await Vo(p),te=new Set;for(let N of Yo){let V=ye(N);if(ne(V,N)){te.add("go");continue}if(ie(V,N)){te.add("node");continue}if(se(V,N)){te.add("python");continue}te.add("unknown");}if(T==="go-only"){let N=te.size===0||[...te].every(V=>V==="go");g.push({id:"profile.go-only",status:N?"passed":"failed",message:N?"go-only profile validated for discovered projects.":`go-only profile mismatch: detected runtimes [${[...te].join(", ")}].`});}else if(T==="python-only"){let N=te.size===0||[...te].every(V=>V==="python");g.push({id:"profile.python-only",status:N?"passed":"failed",message:N?"python-only profile validated for discovered projects.":`python-only profile mismatch: detected runtimes [${[...te].join(", ")}].`});}else if(T==="node-only"){let N=te.size===0||[...te].every(V=>V==="node");g.push({id:"profile.node-only",status:N?"passed":"failed",message:N?"node-only profile validated for discovered projects.":`node-only profile mismatch: detected runtimes [${[...te].join(", ")}].`});}else if(T==="minimal"){let N=[...te].filter(Be=>Be!=="unknown"),V=N.length<=1;g.push({id:"profile.minimal",status:V?"passed":"failed",message:V?"minimal profile is compatible with detected runtime mix.":`minimal profile mismatch: multiple runtimes detected [${N.join(", ")}].`});}else T==="enterprise"&&(g.push({id:"profile.enterprise.ci",status:d?"passed":"failed",message:d?"enterprise profile running with --ci.":"enterprise profile expects --ci for deterministic non-interactive mode."}),g.push({id:"profile.enterprise.compatibility-matrix",status:H?"passed":"failed",message:H?"enterprise profile has compatibility matrix.":"enterprise profile requires .rapidkit/compatibility-matrix.json."}),g.push({id:"profile.enterprise.mirror-config",status:ve?"passed":"failed",message:ve?"enterprise profile has mirror configuration.":"enterprise profile requires .rapidkit/mirror-config.json."}));}d&&(process.env.RAPIDKIT_BOOTSTRAP_CI="1"),l&&(process.env.RAPIDKIT_OFFLINE_MODE="1");let le=g.some(C=>C.id.startsWith("policy.schema.")&&C.status==="failed")||I.mode==="strict"&&g.some(C=>C.status==="failed"),ae=p||v,je=f.join(ae,".rapidkit","reports"),U=new Date().toISOString().replace(/[:.]/g,"-"),D=f.join(je,`bootstrap-compliance-${U}.json`),k=f.join(je,"bootstrap-compliance.latest.json"),Q={command:"bootstrap",timestamp:new Date().toISOString(),workspacePath:p,profile:T,options:{ci:d,offline:l,strict:I.mode==="strict"},policyMode:I.mode,policyRules:I.rules,mirrorLifecycle:A,checks:g};if(le){let C={...Q,result:"blocked",initExitCode:null};return await x__default.ensureDir(je),await De(D,C),await De(k,C),u?process.stdout.write(`${JSON.stringify(C,null,2)}
|
|
148
|
+
`):(console.log(s.red("\u274C Bootstrap blocked by strict policy checks.")),console.log(s.gray(`Compliance report: ${D}`))),1}let Z=0;u||(Z=await e(c));let K=g.filter(C=>C.status==="failed").length,B=Z!==0?"failed":K>0?"ok_with_warnings":"ok",ee={...Q,result:B,initExitCode:Z};if(await x__default.ensureDir(je),await De(D,ee),await De(k,ee),u)process.stdout.write(`${JSON.stringify(ee,null,2)}
|
|
149
|
+
`);else {let C=g.filter(R=>R.status==="failed").length;C>0&&console.log(s.yellow(`\u26A0\uFE0F Bootstrap completed with ${C} policy/profile warnings.`)),console.log(s.gray(`Compliance report: ${D}`));}return Z}finally{typeof o>"u"?delete process.env.RAPIDKIT_SKIP_LOCK_SYNC:process.env.RAPIDKIT_SKIP_LOCK_SYNC=o;}}async function sn(t){let e=(t[1]||"").toLowerCase(),o=t.includes("--warm-deps")||t.includes("--warm-dependencies");if(!e||!["python","node","go"].includes(e))return console.log(s.yellow("Usage: rapidkit setup <python|node|go> [--warm-deps]")),1;let r=async(d,l)=>{if(d==="node"){if(!w.existsSync(f.join(l,"package.json")))return {exitCode:0,message:"Node warm-up skipped: package.json not found in current directory."};let y=w.existsSync(f.join(l,"pnpm-lock.yaml")),v=w.existsSync(f.join(l,"yarn.lock"));return y?{exitCode:await F("pnpm",["install","--lockfile-only","--ignore-scripts"],l)}:v?{exitCode:await F("yarn",["install","--ignore-scripts"],l)}:{exitCode:await F("npm",["install","--package-lock-only","--ignore-scripts"],l)}}return d==="go"?w.existsSync(f.join(l,"go.mod"))?{exitCode:await F("go",["mod","download"],l)}:{exitCode:0,message:"Go warm-up skipped: go.mod not found in current directory."}:{exitCode:0,message:"Dependency warm-up currently applies to node/go runtimes."}},n=Se(e,{runCommandInCwd:F,runCoreRapidkit:(d$1,l)=>d(d$1,{...l,cwd:void 0})}),i=await n.checkPrereqs(),a=await n.doctorHints(process.cwd()),c=q(process.cwd()),m=c||process.cwd();if(i.exitCode===0){console.log(s.green(`\u2705 ${e} prerequisites look good.`));let d=["python","node","go"].filter(l=>l!==e).join("/");if(console.log(s.gray(` Scope: validated ${e} runtime only. ${d} checks are optional unless your workspace profile uses them.`)),e==="python"&&console.log(s.gray(" Note: Poetry is recommended, but venv/pipx-based flows are supported in workspace creation.")),n.warmSetupCache&&((await n.warmSetupCache(m)).exitCode===0?console.log(s.gray(` ${e} cache warm-up completed.`)):console.log(s.yellow(` ${e} cache warm-up skipped (non-fatal).`))),o){let l=await r(e,m),u=/skipped/i.test(l.message||"");l.message&&console.log(s.gray(` ${l.message}`)),l.exitCode===0&&!u?console.log(s.gray(` ${e} dependency warm-up completed (--warm-deps).`)):l.exitCode!==0&&console.log(s.yellow(` ${e} dependency warm-up failed (non-fatal).`));}if(c)try{let l=f.join(c,".rapidkit","toolchain.lock"),u={};try{u=JSON.parse(await w.promises.readFile(l,"utf-8"));}catch{}(!u.runtime||typeof u.runtime!="object")&&(u.runtime={});let y=u.runtime;if(e==="python"){let v=null;try{let{execa:p}=await import('execa');for(let g of Ot()){let P=await p(g,g==="py"?["-3","--version"]:["--version"],{cwd:c,stdio:"pipe",reject:false,timeout:3e3});if(P.exitCode===0){let b=(P.stdout||P.stderr||"").match(/Python\s+(\S+)/);if(v=b?b[1]:null,v)break}}}catch{}y.python={...y.python||{},version:v,last_setup:new Date().toISOString()};}else if(e==="node")y.node={...y.node||{},version:process.version,last_setup:new Date().toISOString()};else if(e==="go"){let v=null;try{let{execa:p}=await import('execa'),A=((await p("go",["version"],{cwd:c,stdio:"pipe"})).stdout||"").match(/go(\d+\.\d+(?:\.\d+)?)/i);v=A?A[1]:null;}catch{}y.go={...y.go||{},version:v,last_setup:new Date().toISOString()};}u.updated_at=new Date().toISOString(),await w.promises.writeFile(l,JSON.stringify(u,null,2)+`
|
|
150
|
+
`,"utf-8"),console.log(s.gray(" toolchain.lock updated (.rapidkit/toolchain.lock)"));}catch{}}else console.log(s.red(`\u274C ${e} prerequisites check failed.`));if(a.length>0){console.log(s.gray(`
|
|
151
|
+
Hints:`));for(let d of a)console.log(s.gray(`- ${d}`));}return i.exitCode}function an(t){let e={strategy:"shared",prune_on_bootstrap:false,self_heal:true,verify_integrity:false};for(let o of t.split(`
|
|
152
|
+
`)){let r=o.trim(),n=r.match(/^strategy:\s*(\S+)/);n&&(e.strategy=n[1].replace(/['"]]/g,""));let i=r.match(/^prune_on_bootstrap:\s*(true|false)/);i&&(e.prune_on_bootstrap=i[1]==="true");let a=r.match(/^self_heal:\s*(true|false)/);a&&(e.self_heal=a[1]==="true");let c=r.match(/^verify_integrity:\s*(true|false)/);c&&(e.verify_integrity=c[1]==="true");}return e}async function cn(t){let e=(t[1]||"status").toLowerCase(),o=ut.getInstance(),r=q(process.cwd()),n={strategy:"shared",prune_on_bootstrap:false,self_heal:true,verify_integrity:false};if(r)try{let i=await w.promises.readFile(f.join(r,".rapidkit","cache-config.yml"),"utf-8");n=an(i);}catch{}return e==="status"?(console.log(s.cyan("RapidKit cache is enabled")),console.log(s.cyan("RapidKit cache status")),r?(console.log(s.gray(` Workspace: ${r}`)),console.log(s.gray(` Strategy: ${n.strategy}`)),console.log(s.gray(` Self-heal: ${n.self_heal}`)),console.log(s.gray(` Prune on bootstrap:${n.prune_on_bootstrap}`)),console.log(s.gray(` Verify integrity: ${n.verify_integrity}`))):console.log(s.gray(" (not inside a workspace \u2014 showing in-memory cache only)")),console.log(s.gray(" In-memory cache: enabled")),console.log(s.gray(" Use: rapidkit cache clear|prune|repair")),0):e==="clear"?(await o.clear(),console.log(s.green("Cache clear completed")),console.log(s.green("\u2705 Cache cleared (all entries removed).")),0):e==="prune"?(await o.clear(),console.log(s.green("\u2705 Cache pruned (stale entries removed).")),n.prune_on_bootstrap||console.log(s.gray(" Tip: set prune_on_bootstrap: true in .rapidkit/cache-config.yml to auto-prune on every bootstrap.")),0):e==="repair"?n.self_heal?(await o.clear(),console.log(s.green("\u2705 Cache repaired (self-heal applied, stale entries evicted).")),n.verify_integrity&&console.log(s.gray(" Integrity verification is enabled in cache-config.yml.")),0):(console.log(s.yellow("\u26A0\uFE0F self_heal is disabled in .rapidkit/cache-config.yml \u2014 skipping repair.")),0):(console.log(s.yellow("Usage: rapidkit cache <status|clear|prune|repair>")),1)}async function ln(t,e,o,r){let n=(e||"show").toLowerCase(),i=f.join(t,".rapidkit","policies.yml");if(n==="show"||n==="status"||n==="get"){let l=await $o(t),u=Io(l);return console.log(s.cyan(`Policy file: ${i}`)),console.log(s.gray(` mode: ${u.mode}`)),console.log(s.gray(` dependency_sharing_mode: ${u.dependency_sharing_mode}`)),console.log(s.gray(" rules:")),console.log(s.gray(` enforce_workspace_marker: ${u.rules.enforce_workspace_marker}`)),console.log(s.gray(` enforce_toolchain_lock: ${u.rules.enforce_toolchain_lock}`)),console.log(s.gray(` disallow_untrusted_tool_sources: ${u.rules.disallow_untrusted_tool_sources}`)),console.log(s.gray(` enforce_compatibility_matrix: ${u.rules.enforce_compatibility_matrix}`)),console.log(s.gray(` require_mirror_lock_for_offline: ${u.rules.require_mirror_lock_for_offline}`)),console.log(s.gray("Examples:")),console.log(s.gray(" npx rapidkit workspace policy set mode strict")),console.log(s.gray(" npx rapidkit workspace policy set dependency_sharing_mode shared-runtime-caches")),console.log(s.gray(" npx rapidkit workspace policy set rules.enforce_toolchain_lock true")),0}if(n!=="set")return console.log(s.red(`Unknown workspace policy action: ${e||""}`)),console.log(s.gray("Available: show, set")),1;if(!o||typeof r>"u")return console.log(s.yellow("Usage: rapidkit workspace policy set <key> <value>")),console.log(s.gray("Allowed keys:")),console.log(s.gray(" mode (warn|strict)")),console.log(s.gray(" dependency_sharing_mode (isolated|shared-runtime-caches|shared-node-deps)")),console.log(s.gray(" rules.enforce_workspace_marker (true|false)")),console.log(s.gray(" rules.enforce_toolchain_lock (true|false)")),console.log(s.gray(" rules.disallow_untrusted_tool_sources (true|false)")),console.log(s.gray(" rules.enforce_compatibility_matrix (true|false)")),console.log(s.gray(" rules.require_mirror_lock_for_offline (true|false)")),1;let a=o.trim(),m=await $o(t);if(a==="mode"){let l=r.trim().toLowerCase();if(l!=="warn"&&l!=="strict")return console.log(s.red("\u274C Invalid mode. Use: warn | strict")),1;m=jo(m,"mode",`${l} # "warn" or "strict"`);}else if(a==="dependency_sharing_mode"){let l=r.trim().toLowerCase();if(l!=="isolated"&&l!=="shared-runtime-caches"&&l!=="shared-node-deps")return console.log(s.red("\u274C Invalid dependency_sharing_mode. Use: isolated | shared-runtime-caches | shared-node-deps")),1;m=jo(m,"dependency_sharing_mode",`${l} # "isolated" or "shared-runtime-caches" or "shared-node-deps"`);}else if(a.startsWith("rules.")){let l=a.slice(6);if(!(l in qo))return console.log(s.red(`\u274C Unknown policy rule: ${l}`)),1;let u=tn(r);if(u===null)return console.log(s.red("\u274C Rule values must be boolean: true | false")),1;m=Zr(m,l,u);}else return console.log(s.red(`\u274C Unknown policy key: ${a}`)),1;await en(t,m);let d=Io(m);return console.log(s.green(`\u2705 Updated ${a} in .rapidkit/policies.yml`)),console.log(s.gray(` mode: ${d.mode}`)),console.log(s.gray(` dependency_sharing_mode: ${d.dependency_sharing_mode}`)),console.log(s.gray(" Tip: run `npx rapidkit workspace policy show` to inspect all values.")),0}async function dn(t){let e=(t[1]||"status").toLowerCase(),o=t.includes("--json"),r=q(process.cwd());if(!r)return console.log(s.red("\u274C Not inside a RapidKit workspace")),console.log(s.gray("\u{1F4A1} Run this command from within a workspace directory")),1;let n=f.join(r,".rapidkit"),i=f.join(n,"mirror-config.json"),a=f.join(n,"mirror.lock"),c=f.join(n,"mirror","artifacts"),m=f.join(n,"reports");async function d(l){let u=new Date().toISOString().replace(/[:.]/g,"-"),y=f.join(m,`mirror-ops-${u}.json`),v=f.join(m,"mirror-ops.latest.json");await x__default.ensureDir(m),await De(y,l),await De(v,l);}if(e==="status"){if(!await x__default.pathExists(i))try{let A={schema_version:"1.0",enabled:false,strategy:"on-demand",artifacts:[],created_at:new Date().toISOString(),note:"Auto-generated by rapidkit mirror status. Set enabled: true and add artifact entries to activate mirroring."};await x__default.ensureDir(n),await w.promises.writeFile(i,JSON.stringify(A,null,2)+`
|
|
153
|
+
`,"utf-8"),console.log(s.gray(" mirror-config.json created with defaults (.rapidkit/mirror-config.json)"));}catch{}let u=await x__default.pathExists(i),y=await x__default.pathExists(c),v=await x__default.pathExists(a),p=y?(await w.promises.readdir(c,{withFileTypes:true})).filter(A=>A.isFile()).length:0,g={command:"mirror",action:e,result:"ok",timestamp:new Date().toISOString(),workspacePath:r,mirror:{configExists:u,lockExists:v,artifactsCount:p}};return await d(g),o?(process.stdout.write(`${JSON.stringify(g,null,2)}
|
|
154
|
+
`),0):(console.log(s.cyan("RapidKit mirror status")),console.log(s.gray(`Workspace: ${r}`)),console.log(s.gray(`Config: ${u?"present":"missing"} (${i})`)),console.log(s.gray(`Lock: ${v?"present":"missing"} (${a})`)),console.log(s.gray(`Artifacts: ${p}`)),0)}if(e==="sync"||e==="verify"||e==="rotate"){let l=await St(r,{ciMode:true,offlineMode:e==="verify",forceRun:true}),u=l.checks.filter(p=>p.status==="failed"),y=l.checks.some(p=>p.id.startsWith("mirror.verify.")&&p.status==="failed");if(e==="verify"&&y){let p={command:"mirror",action:e,result:"failed",timestamp:new Date().toISOString(),workspacePath:r,details:l.details,checks:l.checks};if(await d(p),o)return process.stdout.write(`${JSON.stringify(p,null,2)}
|
|
155
|
+
`),1;console.log(s.red("\u274C Mirror verify failed."));for(let g of l.checks.filter(A=>A.id.startsWith("mirror.verify.")))console.log(s.gray(`- ${g.id}: ${g.message}`));return 1}if(u.length>0){let p={command:"mirror",action:e,result:"failed",timestamp:new Date().toISOString(),workspacePath:r,details:l.details,checks:l.checks};if(await d(p),o)return process.stdout.write(`${JSON.stringify(p,null,2)}
|
|
156
|
+
`),1;console.log(s.yellow(`\u26A0\uFE0F Mirror ${e} completed with ${u.length} issue(s).`));for(let g of u)console.log(s.gray(`- ${g.id}: ${g.message}`));return 1}let v={command:"mirror",action:e,result:"ok",timestamp:new Date().toISOString(),workspacePath:r,details:l.details,checks:l.checks};return await d(v),o?(process.stdout.write(`${JSON.stringify(v,null,2)}
|
|
157
|
+
`),0):e==="rotate"?(console.log(s.green(`\u2705 Mirror rotate completed. Rotated files: ${l.details.rotatedFiles}.`)),0):e==="verify"?(console.log(s.green(`\u2705 Mirror verify completed. Verified artifacts: ${l.details.verifiedArtifacts}.`)),0):(console.log(s.green(`\u2705 Mirror sync completed. Synced artifacts: ${l.details.syncedArtifacts}.`)),0)}return console.log(s.yellow("Usage: rapidkit mirror <status|sync|verify|rotate> [--json]")),1}async function Tt(t){let e=process.env.RAPIDKIT_SKIP_LOCK_SYNC;typeof e>"u"&&(process.env.RAPIDKIT_SKIP_LOCK_SYNC="1");try{let o=process.cwd(),r=await Jo(o,async()=>{let n=q(o),i=Se("python",{runCommandInCwd:F,runCoreRapidkit:d});if(t.length>1){let y=f.resolve(o,t[1]),v=ye(y),p=await At(y);return ne(v,y)||p==="go"?await gt(y):ie(v,y)||p==="node"?await ft(y):se(v,y)||p==="python"?await mt(y,i):await d(t,{cwd:o})}let a=ye(o),c=!!q(o)&&o===q(o);if(!c&&ne(a,o))return await gt(o);let m=await At(o);if(!c&&(ie(a,o)||m==="node"))return await ft(o);if(!c&&(se(a,o)||m==="python"))return await mt(o,i);let d$1=n||q(o),l=Go(o),u=l?f.dirname(f.dirname(l)):null;if(u&&u!==d$1){let y=ye(u),v=await At(u);return ne(y,u)||v==="go"?await gt(u):ie(y,u)||v==="node"?await ft(u):se(y,u)||v==="python"?await mt(u,i):await d(["init"],{cwd:u})}if(d$1&&o===d$1){let y=await on(d$1);if(y!==0)return y;let v=await Vo(d$1);if(v.length===0){let p="minimal";try{p=JSON.parse(await w.promises.readFile(f.join(d$1,".rapidkit","workspace.json"),"utf-8")).profile??"minimal";}catch{}return p==="go-only"?(console.log(s.green("\u2714 Go workspace ready")),console.log(s.gray(`
|
|
158
|
+
No projects yet \u2014 create one and then run init inside it:`)),console.log(s.white(" npx rapidkit create project gofiber.standard my-api")),console.log(s.white(" cd my-api && npx rapidkit init")),console.log(s.gray(`
|
|
159
|
+
\u{1F4A1} Go dependencies are managed per-project (go.mod / go mod tidy).`))):(console.log(s.green("\u2714 Workspace ready")),console.log(s.gray(`
|
|
160
|
+
No projects yet \u2014 create one to get started:`)),console.log(s.white(" npx rapidkit create project"))),0}for(let p of v){let g=ye(p);if(ne(g,p)){let A=await gt(p);if(A!==0)return A}else {if(ie(g,p)){let P=await ft(p);if(P!==0)return P;continue}if(se(g,p)){let P=await mt(p,i);if(P!==0)return P;continue}let A=await d(["init"],{cwd:p});if(A!==0)return A}}return 0}if(!d$1){let y=await a$2(),{name:v}=rn(o),{createProject:p}=await import('./create-27NVMJAR.js');return await p(v,{yes:true,userConfig:y}),0}return await d(t,{cwd:o})});return r.ok?r.value:r.code}finally{typeof e>"u"?delete process.env.RAPIDKIT_SKIP_LOCK_SYNC:process.env.RAPIDKIT_SKIP_LOCK_SYNC=e;}}async function To(t){let e=q(t);if(!e)return [];let o="warn";try{(await w.promises.readFile(f.join(e,".rapidkit","policies.yml"),"utf-8")).match(/^\s*mode:\s*(warn|strict)\s*(?:#.*)?$/m)?.[1]==="strict"&&(o="strict");}catch{return []}if(o!=="strict")return [];let r=[],n=f.join(e,".rapidkit","toolchain.lock");if(!w.existsSync(n))return r.push("toolchain.lock is missing \u2014 run `rapidkit bootstrap` first (strict mode requires a reproducible toolchain)."),r;let i={};try{i=JSON.parse(await w.promises.readFile(n,"utf-8"));}catch{return []}let a=i.runtime??{},c=ye(t);ne(c,t)&&!a.go?.version?r.push("go.version is not pinned in toolchain.lock \u2014 run `rapidkit setup go` first."):ie(c,t)&&!a.node?.version?r.push("node.version is not pinned in toolchain.lock \u2014 run `rapidkit setup node` first."):se(c,t)&&!a.python?.version&&r.push("python.version is not pinned in toolchain.lock \u2014 run `rapidkit setup python` first.");try{let d=JSON.parse(await w.promises.readFile(f.join(e,".rapidkit","workspace.json"),"utf-8")).profile??"";d==="python-only"&&(ne(c,t)||ie(c,t))?r.push('Workspace profile is "python-only" but this project is not Python.'):d==="node-only"&&(ne(c,t)||se(c,t))?r.push('Workspace profile is "node-only" but this project is not Node.'):d==="go-only"&&(ie(c,t)||se(c,t))&&r.push('Workspace profile is "go-only" but this project is not Go.');}catch{}return r}async function pn(){let t=async P=>{if(!a()||!P.toLowerCase().endsWith(".cmd"))return false;try{let b=(await x__default.readFile(P,"utf8")).replace(/\r\n/g,`
|
|
161
|
+
`).toLowerCase(),_=b.includes("\\.rapidkit\\rapidkit"),M=b.includes("\\.rapidkit\\rapidkit.cmd")||b.includes("\\.rapidkit\\rapidkit.exe")||b.includes("\\.venv\\scripts\\rapidkit.exe");return _&&!M}catch{return false}},e=process.cwd(),o=process.argv.slice(2),r=o[0],n=r==="init",i$1=new Set(["dev","start","build","test"]),a$2=!r||r==="--help"||r==="-h"||r==="help",c=Wo(e),m=w.existsSync(f.join(e,".rapidkit","project.json")),d$1=ye(e),l=ne(d$1,e)||ie(d$1,e),u=!!r&&i$1.has(r)&&l;if(wt(o[0])||o[0]==="create"||o[0]==="init"&&c&&!m)return false;try{let P=r==="shell"&&o[1]==="activate",S=r==="create",b=await Ut(e,{cwd:e,timeoutMs:1200});if(b.ok&&b.data?.isRapidkitProject&&b.data.engine==="python"){let _=S||wt(r);if(!a$2&&!P&&!_&&!n&&!u){if(r&&Eo.includes(r)){let T=await To(e).catch(()=>[]);if(T.length>0){process.stderr.write(s.red("\u274C Strict policy violations prevent running this command:")+`
|
|
162
|
+
`);for(let I of T)process.stderr.write(s.red(` \u2022 ${I}`)+`
|
|
163
|
+
`);process.exit(1);}}let M=await d(process.argv.slice(2),{cwd:e});process.exit(M);}}}catch{}let y=Go(e),v=a(),p=i(e),g=null;for(let P of p)if(await x__default.pathExists(P)){if(await t(P)){a$1.warn(`Skipping legacy/broken Windows launcher candidate: ${P}. Falling back to core bridge.`);continue}g=P;break}let A=r==="create";if(r==="init"&&c&&!m)return false;if(r&&Eo.includes(r)){let P=await To(e);if(P.length>0){process.stderr.write(s.red("\u274C Strict policy violations prevent running this command:")+`
|
|
164
|
+
`);for(let S of P)process.stderr.write(s.red(` \u2022 ${S}`)+`
|
|
165
|
+
`);process.exit(1);}}if(g&&r&&So.includes(r)&&!A&&!n&&!u){a$1.debug(`Delegating to local CLI: ${g} ${o.join(" ")}`);let P=r==="init"?_o():process.env,S=spawn(g,o,{stdio:"inherit",cwd:e,shell:v,env:P});return S.on("close",b=>{process.exit(b??0);}),S.on("error",b=>{a$1.error(`Failed to run local rapidkit: ${b.message}`),process.exit(1);}),true}if(y&&await x__default.pathExists(y))try{if((await x__default.readJson(y)).engine==="pip"){let S=o[0],b=i(e),_=null;for(let M of b)if(await x__default.pathExists(M)){if(await t(M)){a$1.warn(`Skipping legacy/broken Windows launcher candidate: ${M}. Falling back to core bridge.`);continue}_=M;break}if(_&&S&&So.includes(S)&&S!=="init"&&!u){a$1.debug(`Delegating to local CLI (early detection): ${_} ${o.join(" ")}`);let M=S==="init"?_o():process.env,T=spawn(_,o,{stdio:"inherit",cwd:e,env:M});return T.on("close",I=>process.exit(I??0)),T.on("error",I=>{a$1.error(`Failed to run local rapidkit: ${I.message}`),process.exit(1);}),true}if(S==="shell"&&o[1]==="activate"){let M=a()?`# RapidKit: activation snippet (PowerShell)
|
|
3567
166
|
$venv = ".venv"
|
|
3568
167
|
if (Test-Path "$venv\\Scripts\\Activate.ps1") { . "$venv\\Scripts\\Activate.ps1" }
|
|
3569
168
|
$env:RAPIDKIT_PROJECT_ROOT = (Get-Location).Path
|
|
@@ -3581,14 +180,14 @@ elif [ -f "$VENV/bin/activate.fish" ]; then
|
|
|
3581
180
|
fi
|
|
3582
181
|
export RAPIDKIT_PROJECT_ROOT="$(pwd)"
|
|
3583
182
|
export PATH="$(pwd)/.rapidkit:$(pwd):$PATH"
|
|
3584
|
-
`;console.log(
|
|
183
|
+
`;console.log(s.green.bold(`
|
|
3585
184
|
\u2705 Activation snippet \u2014 run the following to activate this project in your current shell:
|
|
3586
|
-
`)),console.log(
|
|
185
|
+
`)),console.log(M),console.log(s.gray(`
|
|
3587
186
|
\u{1F4A1} After activation you can run: rapidkit dev
|
|
3588
|
-
`)),process.exit(0);}if(!a$2&&!
|
|
187
|
+
`)),process.exit(0);}if(!a$2&&!wt(S)&&S!=="init"&&!u){let M=await d(o,{cwd:e});process.exit(M);}}}catch{}return false}var Pe=null,yt=false,ce=new Command,un=process.env.RAPIDKIT_SHOW_LEGACY==="1"||process.env.RAPIDKIT_SHOW_LEGACY?.toLowerCase()==="true";async function mn(t){if(t.length===0)return false;let e=t[0],o=t[1];if(Vr.includes(e))return false;if(Lo.includes(e))return true;if(wt(e)||e==="shell"&&o==="activate")return false;if(t.includes("--tui"))return true;if(e==="--help"||e==="-h"||e==="help"||e==="--version"||e==="-V"||t.includes("--template")||t.includes("-t"))return false;let r=new Set(["--yes","-y","--skip-git","--skip-install","--debug","--dry-run","--no-update-check","--create-workspace","--no-workspace"]);if(t.some(i=>r.has(i)))return false;let n=await h$1();return n?n.has(e):!!(a$4.has(e)||t.length>1)}ce.name("rapidkit").description("Create RapidKit workspaces and projects").version(b());var fn=a()?"npx rapidkit init; npx rapidkit dev":"npx rapidkit init && npx rapidkit dev";ce.addHelpText("beforeAll",`RapidKit NPM CLI
|
|
3589
188
|
|
|
3590
189
|
Create workspaces, scaffold projects, and manage your development toolchain.
|
|
3591
|
-
`);
|
|
190
|
+
`);ce.addHelpText("afterAll",`
|
|
3592
191
|
Workspace Setup Commands
|
|
3593
192
|
rapidkit bootstrap Bootstrap projects in workspace (--profile python-only|node-only|go-only|polyglot|enterprise)
|
|
3594
193
|
rapidkit setup <runtime> Set up runtime toolchain (runtime: python | node | go)
|
|
@@ -3607,42 +206,42 @@ Quick start:
|
|
|
3607
206
|
npx rapidkit my-workspace # Create + bootstrap workspace
|
|
3608
207
|
cd my-workspace
|
|
3609
208
|
npx rapidkit create project # Interactive kit picker
|
|
3610
|
-
${
|
|
209
|
+
${fn} # Install deps + run
|
|
3611
210
|
|
|
3612
211
|
Notes:
|
|
3613
212
|
--skip-install (npm wrapper) enables fast-path for lock/dependency steps.
|
|
3614
213
|
It is different from core --skip-essentials (essential module installation).
|
|
3615
214
|
|
|
3616
215
|
Use "rapidkit help <command>" for more information.
|
|
3617
|
-
`);
|
|
216
|
+
`);ce.argument("[name]","Name of the workspace or project directory").addOption(new Option("-t, --template <template>","Legacy: create a project with template (fastapi, nestjs) instead of a workspace").hideHelp()).option("-y, --yes","Skip prompts and use defaults").option("--author <name>","Author/team name for workspace metadata").addOption(new Option("--skip-git","Skip git initialization").hideHelp()).addOption(new Option("--skip-install","Legacy: skip installing dependencies (template mode)").hideHelp()).option("--debug","Enable debug logging").addOption(new Option("--dry-run","Show what would be created without creating it").hideHelp()).addOption(new Option("--install-method <method>","Installation method: poetry, venv, or pipx").choices(["poetry","venv","pipx"]).hideHelp()).addOption(new Option("--profile <profile>","Workspace bootstrap profile: minimal, python-only, node-only, go-only, polyglot, enterprise").choices(["minimal","python-only","node-only","go-only","polyglot","enterprise"]).hideHelp()).addOption(new Option("--create-workspace","When creating a project outside a workspace: create and register a workspace in the current directory").hideHelp()).addOption(new Option("--no-workspace","When creating a project outside a workspace: do not create a workspace").hideHelp()).option("--no-update-check","Skip checking for updates").action(async(t,e$2)=>{try{e$2.debug&&(a$1.setDebug(true),a$1.debug("Debug mode enabled"));let o=await a$2();a$1.debug("User config loaded",o);let r=await b$1();a$1.debug("RapidKit config loaded",r);let n=c(o,r,{author:e$2.author,pythonVersion:void 0,skipGit:e$2.skipGit});a$1.debug("Merged config",n),e$2.updateCheck!==false&&await a$3(),console.log(s.blue.bold(`
|
|
3618
217
|
\u{1F680} Welcome to RapidKit NPM CLI!
|
|
3619
|
-
`)),
|
|
3620
|
-
\u274C ${
|
|
3621
|
-
`),process.exit(1)),
|
|
3622
|
-
\u274C Directory "${
|
|
218
|
+
`)),t||(Bo(),process.exit(0));try{vt(t);}catch(c){throw c instanceof e&&(a$1.error(`
|
|
219
|
+
\u274C ${c.message}`),c.details&&a$1.warn(`\u{1F4A1} ${c.details}
|
|
220
|
+
`),process.exit(1)),c}let i=f.resolve(process.cwd(),t);Pe=i,await x__default.pathExists(i)&&(a$1.error(`
|
|
221
|
+
\u274C Directory "${t}" already exists`),console.log(s.cyan(`
|
|
3623
222
|
\u{1F4A1} Choose a different name or delete the existing directory.
|
|
3624
|
-
`)),process.exit(1));let a=!!
|
|
223
|
+
`)),process.exit(1));let a=!!e$2.template;if(e$2.dryRun){console.log(s.cyan(`
|
|
3625
224
|
\u{1F50D} Dry-run mode - showing what would be created:
|
|
3626
|
-
`)),console.log(
|
|
3627
|
-
`)),a){let
|
|
3628
|
-
`),a$1.debug(`Synced Python version ${
|
|
3629
|
-
`),a$1.debug(`Re-synced Python version ${
|
|
3630
|
-
\u274C ${
|
|
3631
|
-
\u274C An unexpected error occurred:`),console.error(
|
|
3632
|
-
`)),console.log(
|
|
3633
|
-
`)),console.log(
|
|
3634
|
-
`)),console.log(
|
|
3635
|
-
`)),console.log(
|
|
3636
|
-
`)),console.log(
|
|
3637
|
-
`)),console.log(
|
|
3638
|
-
`)),console.log(
|
|
3639
|
-
`)),
|
|
3640
|
-
`))):console.log(
|
|
3641
|
-
`));}var
|
|
3642
|
-
|
|
3643
|
-
\u26A0\uFE0F Interrupted by user`))
|
|
225
|
+
`)),console.log(s.white("\u{1F4C2} Path:"),i),console.log(s.white("\u{1F4E6} Type:"),a?`Project (${e$2.template})`:"Workspace"),console.log();return}if(!e$2.yes&&!a?await he.prompt([{type:"input",name:"author",message:"Author name:",default:process.env.USER||"RapidKit User"}]):e$2.yes&&console.log(s.gray(`Using default values (--yes flag)
|
|
226
|
+
`)),a){let c=String(e$2.template||"").trim(),m=c.toLowerCase(),d$1=m==="fastapi"?"fastapi.standard":m==="nestjs"?"nestjs.standard":m==="go"||m==="fiber"?"gofiber.standard":m==="gin"?"gogin.standard":c;if(ht(d$1)){let S=f.resolve(process.cwd(),t),{generateGoFiberKit:b}=await import('./gofiber-standard-NQHMZTFK.js');await b(S,{project_name:t,module_path:t,skipGit:e$2.skipGit});return}if(Ue(d$1)){let S=f.resolve(process.cwd(),t),{generateGoGinKit:b}=await import('./gogin-standard-2G2C3VQL.js');await b(S,{project_name:t,module_path:t,skipGit:e$2.skipGit});return}if(!!!$t(process.cwd())){let{registerWorkspaceAtPath:S}=await import('./create-27NVMJAR.js');if(e$2.createWorkspace)await S(process.cwd(),{skipGit:e$2.skipGit,yes:e$2.yes,userConfig:o});else if(!e$2.noWorkspace)if(e$2.yes)await S(process.cwd(),{skipGit:e$2.skipGit,yes:true,userConfig:o});else {let{createWs:b}=await he.prompt([{type:"confirm",name:"createWs",message:"This project will be created outside a RapidKit workspace. Create and register a workspace here?",default:true}]);b&&await S(process.cwd(),{skipGit:e$2.skipGit,yes:false,userConfig:o});}}let u=["create","project",d$1,t,"--output",process.cwd()];e$2.yes&&u.push("--yes");let y=q(process.cwd()),v=!!e$2.skipInstall,p=v||!!y;v&&u.push("--skip-essentials");let g=p?{...process.env,RAPIDKIT_SKIP_LOCKS:"1",RAPIDKIT_GENERATE_LOCKS:"0"}:void 0,A=await e$1(u,{cwd:process.cwd(),env:g});A!==0&&process.exit(A),y&&!e$2.skipInstall&&(console.log(s.gray("\u2139\uFE0F Fast create mode (workspace): dependencies were deferred.")),console.log(s.white(" Next: cd <project-name> && npx rapidkit init")));let P=$t(process.cwd());if(P){let S=f.dirname(P),b=f.join(S,".python-version"),_=f.join(i,".python-version");try{if(await x__default.pathExists(b)){let M=w.readFileSync(b,"utf-8");w.writeFileSync(_,M.trim()+`
|
|
227
|
+
`),a$1.debug(`Synced Python version ${M.trim()} from workspace to project`);}}catch(M){a$1.debug("Could not sync Python version from workspace:",M);}}if(!e$2.skipInstall){let S=await d(["init",i],{cwd:process.cwd()});if(S!==0&&process.exit(S),P){let b=f.dirname(P),_=f.join(b,".python-version"),M=f.join(i,".python-version");try{if(await x__default.pathExists(_)){let T=w.readFileSync(_,"utf-8");w.writeFileSync(M,T.trim()+`
|
|
228
|
+
`),a$1.debug(`Re-synced Python version ${T.trim()} after init`);}}catch(T){a$1.debug("Could not re-sync Python version after init:",T);}}}}else {let{createProject:c}=await import('./create-27NVMJAR.js');await c(t,{skipGit:e$2.skipGit,dryRun:e$2.dryRun,yes:e$2.yes,userConfig:n,installMethod:e$2.installMethod,profile:e$2.profile});}}catch(o){o instanceof e?(a$1.error(`
|
|
229
|
+
\u274C ${o.message}`),o.details&&a$1.warn(`\u{1F4A1} ${o.details}`),a$1.debug("Error code:",o.code)):(a$1.error(`
|
|
230
|
+
\u274C An unexpected error occurred:`),console.error(o)),process.exit(1);}finally{Pe=null;}});mo(ce);Vt(ce);ce.command("shell <action>").description("Shell helpers (activate virtualenv in current shell)").action(async t=>{t!=="activate"&&(console.log(s.red(`Unknown shell command: ${t}`)),process.exit(1));let e=process.cwd();function o(m){let d=m;for(;;){let l=f.join(d,".rapidkit","context.json");if(w.existsSync(l))return l;let u=f.dirname(d);if(u===d)break;d=u;}return null}let r=o(e);function n(m){let d=m;for(;;){let l=f.join(d,".venv"),u=f.join(d,".rapidkit","activate");if(w.existsSync(u)||w.existsSync(l))return {venv:l,activateFile:u};let y=f.dirname(d);if(y===d)break;d=y;}return null}let i=n(e);!r&&!i&&(console.log(s.yellow("No RapidKit project found in this directory")),process.exit(1));let a$1;i&&w.existsSync(i.activateFile)?a$1=i.activateFile:i&&w.existsSync(i.venv)?a$1=h(i.venv):(console.log(s.yellow("No virtual environment found")),process.exit(1));let c=a();console.log(c?`call "${a$1}"`:`. "${a$1}"`);});ce.command("doctor [scope]").description("\u{1FA7A} Check RapidKit system health by default; use workspace for full workspace checks").option("--workspace","Check entire workspace (including all projects)").option("--json","Output results in JSON format (for CI/CD pipelines)").option("--fix","Automatically fix common issues (with confirmation)").action(async(t,e)=>{t&&t!=="workspace"&&(console.log(s.red(`Unknown doctor scope: ${t}`)),console.log(s.gray("Available: workspace")),console.log(s.gray("Usage: npx rapidkit doctor or npx rapidkit doctor workspace")),process.exit(1));let o=await Yr({scope:t,workspaceFlag:e.workspace});o.detected&&!e.json&&(console.log(s.yellow("\u26A0\uFE0F Local launcher shadow detected for doctor workspace checks.")),o.candidatePath&&console.log(s.gray(` Candidate: ${o.candidatePath}`)),console.log(s.gray(" Running npm-wrapper doctor workflow directly as safe fallback to avoid ambiguous rapidkit binary resolution.")));let{runDoctor:r}=await import('./doctor-P57TTWEP.js');await r({...e,workspace:e.workspace||t==="workspace"});});ce.command("workspace <action> [subaction] [key] [value]").description("Manage RapidKit workspaces (list, sync, policy)").action(async(t,e,o,r)=>{if(t==="list"){let{listWorkspaces:n}=await import('./workspace-VXNLNKCM.js');await n();}else if(t==="sync"){let n=q(process.cwd());n||(console.log(s.red("\u274C Not inside a RapidKit workspace")),console.log(s.gray("\u{1F4A1} Run this command from within a workspace directory")),process.exit(1));let{syncWorkspaceProjects:i}=await import('./workspace-VXNLNKCM.js');console.log(s.cyan(`\u{1F4C2} Scanning workspace: ${f.basename(n)}`)),await i(n);}else if(t==="policy"){let n=q(process.cwd());n||(console.log(s.red("\u274C Not inside a RapidKit workspace")),console.log(s.gray("\u{1F4A1} Run this command from within a workspace directory")),process.exit(1));let i=await ln(n,e,o,r);i!==0&&process.exit(i);}else console.log(s.red(`Unknown workspace action: ${t}`)),console.log(s.gray("Available: list, sync, policy")),process.exit(1);});function Bo(){let t=a()?"npx rapidkit init; npx rapidkit dev":"npx rapidkit init && npx rapidkit dev";console.log(s.white(`Usage:
|
|
231
|
+
`)),console.log(s.cyan(` npx rapidkit <workspace-name> [options]
|
|
232
|
+
`)),console.log(s.bold("Quick start \u2014 workspace workflow:")),console.log(s.cyan(" npx rapidkit my-workspace ")+s.gray("# Create workspace (interactive profile picker)")),console.log(s.cyan(" cd my-workspace")),console.log(s.cyan(" npx rapidkit bootstrap ")+s.gray("# Bootstrap all runtime toolchains")),console.log(s.cyan(" npx rapidkit create project ")+s.gray("# Interactive kit picker")),console.log(s.cyan(" cd my-api")),console.log(s.cyan(` ${t}
|
|
233
|
+
`)),console.log(s.bold("Workspace profiles (asked during creation):")),console.log(s.gray(" minimal Foundation files only \u2014 fastest bootstrap (default)")),console.log(s.gray(" python-only Python + Poetry (FastAPI, Django, ML)")),console.log(s.gray(" node-only Node.js runtime (NestJS, Express, Next.js)")),console.log(s.gray(" go-only Go runtime (Fiber, Gin, gRPC)")),console.log(s.gray(" polyglot Python + Node.js + Go multi-runtime")),console.log(s.gray(` enterprise Polyglot + governance + Sigstore
|
|
234
|
+
`)),console.log(s.bold("Workspace commands (inside a workspace):")),console.log(s.gray(" npx rapidkit bootstrap [--profile <p>] Re-bootstrap toolchains")),console.log(s.gray(" npx rapidkit workspace list List registered workspaces")),console.log(s.gray(" npx rapidkit workspace policy show Show effective workspace policies")),console.log(s.gray(" npx rapidkit workspace policy set <k> <v> Update workspace policy values")),console.log(s.gray(" npx rapidkit setup python|node|go [--warm-deps] Set up runtime (+ optional deps warm-up)")),console.log(s.gray(" npx rapidkit mirror [status|sync|verify|rotate] Registry mirror management")),console.log(s.gray(` npx rapidkit cache [status|clear|prune|repair] Package cache management
|
|
235
|
+
`)),console.log(s.bold("Options (workspace creation):")),console.log(s.gray(" -y, --yes Skip prompts and use defaults")),console.log(s.gray(" --author <name> Author/team name for workspace metadata")),console.log(s.gray(" --skip-git Skip git initialization")),console.log(s.gray(" --debug Enable debug logging")),console.log(s.gray(" --dry-run Show what would be created")),console.log(s.gray(" --create-workspace When creating a project outside a workspace: create and register a workspace in the current directory")),console.log(s.gray(" --no-workspace When creating a project outside a workspace: do not create a workspace")),console.log(s.gray(` --no-update-check Skip checking for updates
|
|
236
|
+
`)),console.log(s.bold("Project commands (inside a project):")),console.log(s.gray(" npx rapidkit create project Scaffold a new project")),console.log(s.gray(" cd my-api Change directory to the new project")),console.log(s.gray(" npx rapidkit init Install project dependencies")),console.log(s.gray(" npx rapidkit dev Start dev server")),console.log(s.gray(" npx rapidkit build Build for production")),console.log(s.gray(` npx rapidkit test Run tests
|
|
237
|
+
`)),console.log(s.bold("Flags clarification:")),console.log(s.gray(" --skip-install npm fast-path for lock/dependency steps")),console.log(s.gray(` --skip-essentials core flag for skipping essential module installation
|
|
238
|
+
`)),un?(console.log(s.bold("Legacy (shown because RAPIDKIT_SHOW_LEGACY=1):")),console.log(s.gray(" npx rapidkit my-project --template fastapi")),console.log(s.gray(" npx rapidkit my-project --template nestjs")),console.log(s.gray(` --skip-install Fast-path lock/deps (legacy template mode) \u2014 not same as --skip-essentials
|
|
239
|
+
`))):console.log(s.gray(`Tip: set RAPIDKIT_SHOW_LEGACY=1 to show legacy template flags in help.
|
|
240
|
+
`));}var Oo="__rapidkit_signal_handlers_registered__",Mo=globalThis;Mo[Oo]||(Mo[Oo]=true,process.on("SIGINT",async()=>{if(!yt){if(yt=true,console.log(s.yellow(`
|
|
241
|
+
|
|
242
|
+
\u26A0\uFE0F Interrupted by user`)),Pe&&await x__default.pathExists(Pe)){console.log(s.gray("Cleaning up partial installation..."));try{await x__default.remove(Pe),console.log(s.green("\u2713 Cleanup complete"));}catch(t){a$1.debug("Cleanup failed:",t);}}process.exit(130);}}),process.on("SIGTERM",async()=>{if(!yt){if(yt=true,a$1.debug("Received SIGTERM"),Pe&&await x__default.pathExists(Pe))try{await x__default.remove(Pe);}catch(t){a$1.debug("Cleanup failed:",t);}process.exit(143);}}));var gn=process.env.VITEST==="true"||process.env.VITEST==="1"||process.env.NODE_ENV==="test",yn=(()=>{let t=process.argv[1];if(!t)return false;try{return w.realpathSync(t)===w.realpathSync(fileURLToPath(import.meta.url))}catch{return f.resolve(t)===f.resolve(fileURLToPath(import.meta.url))}})(),hn=!gn||yn;if(hn){let t=process.argv.slice(2),e=t[0],o=process.cwd(),r=Wo(o),n=w.existsSync(f.join(o,".rapidkit","project.json")),i=Ao(e),a=e==="init"&&r&&!n;(t.length===0||t.length===1&&(e==="--help"||e==="-h"||e==="help"))&&(console.log(s.blue.bold(`
|
|
3644
243
|
\u{1F680} Welcome to RapidKit NPM CLI!
|
|
3645
|
-
`))
|
|
3646
|
-
`),process.exit(1);}):
|
|
3647
|
-
`),
|
|
3648
|
-
`),
|
|
244
|
+
`)),Bo(),process.exit(0)),i?ce.parse():a?Tt(t).then(m=>process.exit(m)).catch(m=>{process.stderr.write(`RapidKit (npm) failed to run workspace init: ${m?.message??m}
|
|
245
|
+
`),process.exit(1);}):pn().then(async m=>{if(!m){let d$1=process.argv.slice(2);if(process.env.RAPIDKIT_NPM_DEBUG_ARGS==="1"&&process.stderr.write(`[rapidkit-npm] argv=${JSON.stringify(d$1)}
|
|
246
|
+
`),Ao(d$1[0])){ce.parse();return}if(d$1[0]==="create"){let u=await Ko(d$1);process.exit(u);}if(d$1[0]==="init"){let u=await Tt(d$1);process.exit(u);}if(Br(d$1[0])){if(d$1[0]==="bootstrap"){let y=await nn(d$1);process.exit(y);}if(d$1[0]==="setup"){let y=await sn(d$1);process.exit(y);}if(d$1[0]==="cache"){let y=await cn(d$1);process.exit(y);}let u=await dn(d$1);process.exit(u);}if(Fo.includes(d$1[0])){let u=d$1[0],y=ye(process.cwd()),v=q(process.cwd());if(v){let g=f.join(v,".rapidkit","policies.yml");if(await x__default.pathExists(g))try{let A=await w.promises.readFile(g,"utf-8");if(((A.match(/^\s*enforcement_mode:\s*(warn|strict)\s*(?:#.*)?$/m)??A.match(/^\s*mode:\s*(warn|strict)\s*(?:#.*)?$/m))?.[1]??"warn")==="strict"){let b=f.join(v,".rapidkit","toolchain.lock"),_=[];if(!await x__default.pathExists(b))_.push("toolchain.lock is missing \u2014 run `rapidkit bootstrap` first (strict mode requires a reproducible toolchain).");else try{let I=JSON.parse(await w.promises.readFile(b,"utf-8")).runtime??{};ne(y,process.cwd())&&!I.go?.version&&_.push("Go runtime version is not pinned in toolchain.lock \u2014 run `rapidkit setup go` first."),ie(y,process.cwd())&&!I.node?.version&&_.push("Node runtime version is not pinned in toolchain.lock \u2014 run `rapidkit setup node` first."),se(y,process.cwd())&&!I.python?.version&&_.push("Python runtime version is not pinned in toolchain.lock \u2014 run `rapidkit setup python` first.");}catch{}let M=f.join(v,".rapidkit","workspace.json");if(await x__default.pathExists(M))try{let I=JSON.parse(await w.promises.readFile(M,"utf-8")).profile??"";I==="python-only"&&(ne(y,process.cwd())||ie(y,process.cwd()))&&_.push('Workspace profile is "python-only" but this project is not Python. Update the workspace profile or use a polyglot workspace.'),I==="node-only"&&(ne(y,process.cwd())||se(y,process.cwd()))&&_.push('Workspace profile is "node-only" but this project is not Node. Update the workspace profile or use a polyglot workspace.'),I==="go-only"&&(se(y,process.cwd())||ie(y,process.cwd()))&&_.push('Workspace profile is "go-only" but this project is not Go. Update the workspace profile or use a polyglot workspace.');}catch{}if(_.length>0){console.log(s.red(`\u274C Strict policy violations block \`${u}\`:`));for(let T of _)console.log(s.red(` \u2022 ${T}`));console.log(s.gray("\u{1F4A1} Fix violations or switch to warn mode: set mode: warn in .rapidkit/policies.yml")),process.exit(1);}}}catch{}}let p=await Jo(process.cwd(),async()=>{if(ne(y,process.cwd())){let g=Se("go",{runCommandInCwd:F,runCoreRapidkit:d}),A=u==="dev"?await g.runDev(process.cwd()):u==="test"?await g.runTest(process.cwd()):u==="build"?await g.runBuild(process.cwd()):await g.runStart(process.cwd());return A.message&&console.log(s.red(`\u274C ${A.message}`)),A.exitCode}if(ie(y,process.cwd()))return u==="dev"?await He("dev",process.cwd()):u==="test"?await He("test",process.cwd()):u==="build"?await He("build",process.cwd()):await He("start",process.cwd());if(se(y,process.cwd())){let g=Se("python",{runCommandInCwd:F,runCoreRapidkit:d});return u==="dev"?(await g.runDev(process.cwd())).exitCode:u==="test"?(await g.runTest(process.cwd())).exitCode:u==="build"?(await g.runBuild(process.cwd())).exitCode:(await g.runStart(process.cwd())).exitCode}return -1});p.ok||process.exit(p.code),p.value>=0&&process.exit(p.value);}if(d$1[0]==="add"||d$1[0]==="module"&&d$1[1]==="add"){let u=ye(process.cwd());(u?.runtime==="go"||u?.module_support===false)&&(console.error(s.red("\u274C RapidKit modules are not available for Go projects.")),console.error(s.gray(" The module system requires Python and is only supported for FastAPI and NestJS projects.")),process.exit(1));}let l=await mn(d$1);if(process.env.RAPIDKIT_NPM_DEBUG_ARGS==="1"&&process.stderr.write(`[rapidkit-npm] shouldForwardToCore=${l}
|
|
247
|
+
`),l){let u=await d(d$1,{cwd:process.cwd()});process.exit(u);}ce.parse();}});}export{Ur as NPM_ONLY_TOP_LEVEL_COMMANDS,Vr as WRAPPER_ORCHESTRATED_PROJECT_COMMANDS,Yr as detectWindowsDoctorWorkspaceShadow,nn as handleBootstrapCommand,cn as handleCacheCommand,Ko as handleCreateOrFallback,Tt as handleInitCommand,dn as handleMirrorCommand,sn as handleSetupCommand,mn as shouldForwardToCore};
|