@enderworld/onlyapi 1.5.1

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.
Files changed (160) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/LICENSE +21 -0
  3. package/README.md +338 -0
  4. package/dist/cli.js +14 -0
  5. package/package.json +69 -0
  6. package/src/application/dtos/admin.dto.ts +25 -0
  7. package/src/application/dtos/auth.dto.ts +97 -0
  8. package/src/application/dtos/index.ts +40 -0
  9. package/src/application/index.ts +2 -0
  10. package/src/application/services/admin.service.ts +150 -0
  11. package/src/application/services/api-key.service.ts +65 -0
  12. package/src/application/services/auth.service.ts +606 -0
  13. package/src/application/services/health.service.ts +97 -0
  14. package/src/application/services/index.ts +10 -0
  15. package/src/application/services/user.service.ts +95 -0
  16. package/src/cli/commands/help.ts +86 -0
  17. package/src/cli/commands/init.ts +301 -0
  18. package/src/cli/commands/upgrade.ts +471 -0
  19. package/src/cli/index.ts +76 -0
  20. package/src/cli/ui.ts +189 -0
  21. package/src/cluster.ts +62 -0
  22. package/src/core/entities/index.ts +1 -0
  23. package/src/core/entities/user.entity.ts +24 -0
  24. package/src/core/errors/app-error.ts +81 -0
  25. package/src/core/errors/index.ts +15 -0
  26. package/src/core/index.ts +7 -0
  27. package/src/core/ports/account-lockout.ts +15 -0
  28. package/src/core/ports/alert-sink.ts +27 -0
  29. package/src/core/ports/api-key.ts +37 -0
  30. package/src/core/ports/audit-log.ts +46 -0
  31. package/src/core/ports/cache.ts +24 -0
  32. package/src/core/ports/circuit-breaker.ts +42 -0
  33. package/src/core/ports/event-bus.ts +78 -0
  34. package/src/core/ports/index.ts +62 -0
  35. package/src/core/ports/job-queue.ts +73 -0
  36. package/src/core/ports/logger.ts +21 -0
  37. package/src/core/ports/metrics.ts +49 -0
  38. package/src/core/ports/oauth.ts +55 -0
  39. package/src/core/ports/password-hasher.ts +10 -0
  40. package/src/core/ports/password-history.ts +23 -0
  41. package/src/core/ports/password-policy.ts +43 -0
  42. package/src/core/ports/refresh-token-store.ts +37 -0
  43. package/src/core/ports/retry.ts +23 -0
  44. package/src/core/ports/token-blacklist.ts +16 -0
  45. package/src/core/ports/token-service.ts +23 -0
  46. package/src/core/ports/totp-service.ts +16 -0
  47. package/src/core/ports/user.repository.ts +40 -0
  48. package/src/core/ports/verification-token.ts +41 -0
  49. package/src/core/ports/webhook.ts +58 -0
  50. package/src/core/types/brand.ts +19 -0
  51. package/src/core/types/index.ts +19 -0
  52. package/src/core/types/pagination.ts +28 -0
  53. package/src/core/types/result.ts +52 -0
  54. package/src/infrastructure/alerting/index.ts +1 -0
  55. package/src/infrastructure/alerting/webhook.ts +100 -0
  56. package/src/infrastructure/cache/in-memory-cache.ts +111 -0
  57. package/src/infrastructure/cache/index.ts +6 -0
  58. package/src/infrastructure/cache/redis-cache.ts +204 -0
  59. package/src/infrastructure/config/config.ts +185 -0
  60. package/src/infrastructure/config/index.ts +1 -0
  61. package/src/infrastructure/database/in-memory-user.repository.ts +134 -0
  62. package/src/infrastructure/database/index.ts +37 -0
  63. package/src/infrastructure/database/migrations/001_create_users.ts +26 -0
  64. package/src/infrastructure/database/migrations/002_create_token_blacklist.ts +21 -0
  65. package/src/infrastructure/database/migrations/003_create_audit_log.ts +31 -0
  66. package/src/infrastructure/database/migrations/004_auth_platform.ts +112 -0
  67. package/src/infrastructure/database/migrations/runner.ts +120 -0
  68. package/src/infrastructure/database/mssql/index.ts +14 -0
  69. package/src/infrastructure/database/mssql/migrations.ts +299 -0
  70. package/src/infrastructure/database/mssql/mssql-account-lockout.ts +95 -0
  71. package/src/infrastructure/database/mssql/mssql-api-keys.ts +146 -0
  72. package/src/infrastructure/database/mssql/mssql-audit-log.ts +86 -0
  73. package/src/infrastructure/database/mssql/mssql-oauth-accounts.ts +118 -0
  74. package/src/infrastructure/database/mssql/mssql-password-history.ts +71 -0
  75. package/src/infrastructure/database/mssql/mssql-refresh-token-store.ts +144 -0
  76. package/src/infrastructure/database/mssql/mssql-token-blacklist.ts +54 -0
  77. package/src/infrastructure/database/mssql/mssql-user.repository.ts +263 -0
  78. package/src/infrastructure/database/mssql/mssql-verification-tokens.ts +120 -0
  79. package/src/infrastructure/database/postgres/index.ts +14 -0
  80. package/src/infrastructure/database/postgres/migrations.ts +235 -0
  81. package/src/infrastructure/database/postgres/pg-account-lockout.ts +75 -0
  82. package/src/infrastructure/database/postgres/pg-api-keys.ts +126 -0
  83. package/src/infrastructure/database/postgres/pg-audit-log.ts +74 -0
  84. package/src/infrastructure/database/postgres/pg-oauth-accounts.ts +101 -0
  85. package/src/infrastructure/database/postgres/pg-password-history.ts +61 -0
  86. package/src/infrastructure/database/postgres/pg-refresh-token-store.ts +117 -0
  87. package/src/infrastructure/database/postgres/pg-token-blacklist.ts +48 -0
  88. package/src/infrastructure/database/postgres/pg-user.repository.ts +237 -0
  89. package/src/infrastructure/database/postgres/pg-verification-tokens.ts +97 -0
  90. package/src/infrastructure/database/sqlite-account-lockout.ts +97 -0
  91. package/src/infrastructure/database/sqlite-api-keys.ts +155 -0
  92. package/src/infrastructure/database/sqlite-audit-log.ts +90 -0
  93. package/src/infrastructure/database/sqlite-oauth-accounts.ts +105 -0
  94. package/src/infrastructure/database/sqlite-password-history.ts +54 -0
  95. package/src/infrastructure/database/sqlite-refresh-token-store.ts +122 -0
  96. package/src/infrastructure/database/sqlite-token-blacklist.ts +47 -0
  97. package/src/infrastructure/database/sqlite-user.repository.ts +260 -0
  98. package/src/infrastructure/database/sqlite-verification-tokens.ts +112 -0
  99. package/src/infrastructure/events/event-bus.ts +105 -0
  100. package/src/infrastructure/events/event-factory.ts +31 -0
  101. package/src/infrastructure/events/in-memory-webhook-registry.ts +67 -0
  102. package/src/infrastructure/events/index.ts +4 -0
  103. package/src/infrastructure/events/webhook-dispatcher.ts +114 -0
  104. package/src/infrastructure/index.ts +58 -0
  105. package/src/infrastructure/jobs/index.ts +1 -0
  106. package/src/infrastructure/jobs/job-queue.ts +185 -0
  107. package/src/infrastructure/logging/index.ts +1 -0
  108. package/src/infrastructure/logging/logger.ts +63 -0
  109. package/src/infrastructure/metrics/index.ts +1 -0
  110. package/src/infrastructure/metrics/prometheus.ts +231 -0
  111. package/src/infrastructure/oauth/github.ts +116 -0
  112. package/src/infrastructure/oauth/google.ts +83 -0
  113. package/src/infrastructure/oauth/index.ts +2 -0
  114. package/src/infrastructure/resilience/circuit-breaker.ts +133 -0
  115. package/src/infrastructure/resilience/index.ts +2 -0
  116. package/src/infrastructure/resilience/retry.ts +50 -0
  117. package/src/infrastructure/security/account-lockout.ts +73 -0
  118. package/src/infrastructure/security/index.ts +6 -0
  119. package/src/infrastructure/security/password-hasher.ts +31 -0
  120. package/src/infrastructure/security/password-policy.ts +77 -0
  121. package/src/infrastructure/security/token-blacklist.ts +45 -0
  122. package/src/infrastructure/security/token-service.ts +144 -0
  123. package/src/infrastructure/security/totp-service.ts +142 -0
  124. package/src/infrastructure/tracing/index.ts +7 -0
  125. package/src/infrastructure/tracing/trace-context.ts +93 -0
  126. package/src/main.ts +479 -0
  127. package/src/presentation/context.ts +26 -0
  128. package/src/presentation/handlers/admin.handler.ts +114 -0
  129. package/src/presentation/handlers/api-key.handler.ts +68 -0
  130. package/src/presentation/handlers/auth.handler.ts +218 -0
  131. package/src/presentation/handlers/health.handler.ts +27 -0
  132. package/src/presentation/handlers/index.ts +15 -0
  133. package/src/presentation/handlers/metrics.handler.ts +21 -0
  134. package/src/presentation/handlers/oauth.handler.ts +61 -0
  135. package/src/presentation/handlers/openapi.handler.ts +543 -0
  136. package/src/presentation/handlers/response.ts +29 -0
  137. package/src/presentation/handlers/sse.handler.ts +165 -0
  138. package/src/presentation/handlers/user.handler.ts +81 -0
  139. package/src/presentation/handlers/webhook.handler.ts +92 -0
  140. package/src/presentation/handlers/websocket.handler.ts +226 -0
  141. package/src/presentation/i18n/index.ts +254 -0
  142. package/src/presentation/index.ts +5 -0
  143. package/src/presentation/middleware/api-key.ts +18 -0
  144. package/src/presentation/middleware/auth.ts +39 -0
  145. package/src/presentation/middleware/cors.ts +41 -0
  146. package/src/presentation/middleware/index.ts +12 -0
  147. package/src/presentation/middleware/rate-limit.ts +65 -0
  148. package/src/presentation/middleware/security-headers.ts +18 -0
  149. package/src/presentation/middleware/validate.ts +16 -0
  150. package/src/presentation/middleware/versioning.ts +69 -0
  151. package/src/presentation/routes/index.ts +1 -0
  152. package/src/presentation/routes/router.ts +272 -0
  153. package/src/presentation/server.ts +381 -0
  154. package/src/shared/cli.ts +294 -0
  155. package/src/shared/container.ts +65 -0
  156. package/src/shared/index.ts +2 -0
  157. package/src/shared/log-format.ts +148 -0
  158. package/src/shared/utils/id.ts +5 -0
  159. package/src/shared/utils/index.ts +2 -0
  160. package/src/shared/utils/timing-safe.ts +20 -0
@@ -0,0 +1,471 @@
1
+ /**
2
+ * `onlyapi upgrade` — upgrade an existing onlyApi project to the latest version.
3
+ *
4
+ * Steps:
5
+ * 1. Verify we're inside an onlyApi project
6
+ * 2. Read current version from package.json
7
+ * 3. Fetch latest version from GitHub / npm
8
+ * 4. Compare versions
9
+ * 5. If newer, download and apply updates to core files
10
+ * 6. Re-install dependencies
11
+ * 7. Show changelog summary
12
+ */
13
+
14
+ import { existsSync } from "node:fs";
15
+ import { join, resolve } from "node:path";
16
+ import {
17
+ blank,
18
+ bold,
19
+ confirm,
20
+ createSpinner,
21
+ cyan,
22
+ dim,
23
+ error,
24
+ formatDuration,
25
+ green,
26
+ icons,
27
+ info,
28
+ log,
29
+ logo,
30
+ printKeyValue,
31
+ section,
32
+ step,
33
+ success,
34
+ warn,
35
+ yellow,
36
+ } from "../ui.js";
37
+
38
+ // ── Constants ───────────────────────────────────────────────────────────
39
+
40
+ const GITHUB_API = "https://api.github.com/repos/lysari/onlyapi";
41
+ const TARBALL_URL = (tag: string) =>
42
+ `https://github.com/lysari/onlyapi/archive/refs/tags/${tag}.tar.gz`;
43
+ const TARBALL_MAIN_URL = "https://github.com/lysari/onlyapi/archive/refs/heads/main.tar.gz";
44
+ const NPM_REGISTRY = "https://registry.npmjs.org/only-api";
45
+
46
+ /**
47
+ * Files that are safe to upgrade (framework internals).
48
+ * User-modified files like handlers and services are NOT touched.
49
+ */
50
+ const UPGRADEABLE_PATHS = [
51
+ "src/core/errors/app-error.ts",
52
+ "src/core/types/brand.ts",
53
+ "src/core/types/result.ts",
54
+ "src/infrastructure/logging/logger.ts",
55
+ "src/infrastructure/security/password-hasher.ts",
56
+ "src/infrastructure/security/token-service.ts",
57
+ "src/presentation/middleware/cors.ts",
58
+ "src/presentation/middleware/rate-limit.ts",
59
+ "src/presentation/middleware/security-headers.ts",
60
+ "src/presentation/server.ts",
61
+ "src/presentation/context.ts",
62
+ "src/shared/cli.ts",
63
+ "src/shared/container.ts",
64
+ "src/shared/utils/id.ts",
65
+ "src/shared/utils/timing-safe.ts",
66
+ "src/shared/log-format.ts",
67
+ "src/cluster.ts",
68
+ "tsconfig.json",
69
+ "biome.json",
70
+ ] as const;
71
+
72
+ // ── Helpers ─────────────────────────────────────────────────────────────
73
+
74
+ const exec = async (
75
+ cmd: string[],
76
+ cwd: string = process.cwd(),
77
+ ): Promise<{ stdout: string; stderr: string; exitCode: number }> => {
78
+ const proc = Bun.spawn(cmd, {
79
+ cwd,
80
+ stdout: "pipe",
81
+ stderr: "pipe",
82
+ });
83
+
84
+ const [stdout, stderr] = await Promise.all([
85
+ new Response(proc.stdout).text(),
86
+ new Response(proc.stderr).text(),
87
+ ]);
88
+
89
+ const exitCode = await proc.exited;
90
+ return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode };
91
+ };
92
+
93
+ const parseVersion = (v: string): [number, number, number] => {
94
+ const parts = v.replace(/^v/, "").split(".").map(Number);
95
+ return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
96
+ };
97
+
98
+ const isNewer = (latest: string, current: string): boolean => {
99
+ const [lMaj, lMin, lPatch] = parseVersion(latest);
100
+ const [cMaj, cMin, cPatch] = parseVersion(current);
101
+
102
+ if (lMaj !== cMaj) return lMaj > cMaj;
103
+ if (lMin !== cMin) return lMin > cMin;
104
+ return lPatch > cPatch;
105
+ };
106
+
107
+ const fetchLatestVersion = async (): Promise<string | null> => {
108
+ // Try GitHub releases first
109
+ try {
110
+ const res = await fetch(`${GITHUB_API}/releases/latest`, {
111
+ headers: { Accept: "application/vnd.github.v3+json" },
112
+ });
113
+ if (res.ok) {
114
+ const data = (await res.json()) as { tag_name: string };
115
+ return data.tag_name.replace(/^v/, "");
116
+ }
117
+ } catch {
118
+ // fall through
119
+ }
120
+
121
+ // Try GitHub tags
122
+ try {
123
+ const res = await fetch(`${GITHUB_API}/tags?per_page=1`, {
124
+ headers: { Accept: "application/vnd.github.v3+json" },
125
+ });
126
+ if (res.ok) {
127
+ const tags = (await res.json()) as { name: string }[];
128
+ if (tags.length > 0) {
129
+ const first = tags[0];
130
+ return first ? first.name.replace(/^v/, "") : null;
131
+ }
132
+ }
133
+ } catch {
134
+ // fall through
135
+ }
136
+
137
+ // Try npm registry
138
+ try {
139
+ const res = await fetch(NPM_REGISTRY);
140
+ if (res.ok) {
141
+ const data = (await res.json()) as { "dist-tags": { latest: string } };
142
+ return data["dist-tags"].latest;
143
+ }
144
+ } catch {
145
+ // fall through
146
+ }
147
+
148
+ return null;
149
+ };
150
+
151
+ // ── Main ────────────────────────────────────────────────────────────────
152
+
153
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: CLI upgrade wizard is inherently branchy
154
+ export const upgradeCommand = async (args: string[], version: string): Promise<void> => {
155
+ const startTime = performance.now();
156
+ const projectDir = resolve(process.cwd());
157
+
158
+ blank();
159
+ log(logo(version));
160
+ blank();
161
+
162
+ // ── Verify onlyApi project ──
163
+ const pkgPath = join(projectDir, "package.json");
164
+ if (!existsSync(pkgPath)) {
165
+ error("No package.json found in current directory.");
166
+ info("Run this command from the root of your onlyApi project.");
167
+ process.exit(1);
168
+ }
169
+
170
+ let currentVersion: string;
171
+ try {
172
+ const pkg = JSON.parse(await Bun.file(pkgPath).text());
173
+ currentVersion = pkg.version ?? "0.0.0";
174
+ } catch {
175
+ error("Could not read package.json.");
176
+ process.exit(1);
177
+ }
178
+
179
+ // Check if it looks like an onlyApi project
180
+ const hasOnlyApiStructure =
181
+ existsSync(join(projectDir, "src/main.ts")) &&
182
+ existsSync(join(projectDir, "src/core")) &&
183
+ existsSync(join(projectDir, "src/presentation"));
184
+
185
+ if (!hasOnlyApiStructure) {
186
+ error("This doesn't appear to be an onlyApi project.");
187
+ info("Expected to find src/main.ts, src/core/, and src/presentation/");
188
+ process.exit(1);
189
+ }
190
+
191
+ // ── Check for updates ──
192
+ section("Checking for updates");
193
+
194
+ const checkSpinner = createSpinner("Fetching latest version...");
195
+ checkSpinner.start();
196
+
197
+ const latestVersion = await fetchLatestVersion();
198
+
199
+ if (!latestVersion) {
200
+ checkSpinner.stop();
201
+ warn("Could not determine the latest version.");
202
+ info("This may be due to network issues or API rate limits.");
203
+ blank();
204
+
205
+ const forceUpgrade = args.includes("--force") || args.includes("-f");
206
+ if (!forceUpgrade) {
207
+ const shouldContinue = await confirm("Continue with upgrade from main branch?", false);
208
+ if (!shouldContinue) {
209
+ info("Aborted.");
210
+ process.exit(0);
211
+ }
212
+ }
213
+ } else {
214
+ checkSpinner.stop("Version check complete");
215
+ blank();
216
+
217
+ printKeyValue([
218
+ ["Current version", currentVersion],
219
+ ["Latest version", latestVersion],
220
+ ]);
221
+
222
+ blank();
223
+
224
+ if (
225
+ !isNewer(latestVersion, currentVersion) &&
226
+ !args.includes("--force") &&
227
+ !args.includes("-f")
228
+ ) {
229
+ success("You're already on the latest version!");
230
+ blank();
231
+ process.exit(0);
232
+ }
233
+
234
+ if (isNewer(latestVersion, currentVersion)) {
235
+ info(
236
+ `Update available: ${bold(yellow(currentVersion))} ${dim("→")} ${bold(green(latestVersion))}`,
237
+ );
238
+ } else {
239
+ info(`Re-applying latest version ${dim("(--force)")}`);
240
+ }
241
+ }
242
+
243
+ // ── Check for uncommitted changes ──
244
+ const hasGit = existsSync(join(projectDir, ".git"));
245
+ if (hasGit) {
246
+ const { stdout: gitStatus } = await exec(["git", "status", "--porcelain"], projectDir);
247
+ if (gitStatus) {
248
+ blank();
249
+ warn("You have uncommitted changes.");
250
+ const shouldContinue = await confirm("Continue anyway?", false);
251
+ if (!shouldContinue) {
252
+ info("Commit your changes first, then retry.");
253
+ process.exit(0);
254
+ }
255
+ }
256
+ }
257
+
258
+ // ── Download latest source ──
259
+ section("Downloading update");
260
+
261
+ const downloadSpinner = createSpinner("Downloading latest source...");
262
+ downloadSpinner.start();
263
+
264
+ const tmpDir = join(projectDir, ".onlyapi-upgrade-tmp");
265
+ try {
266
+ // Clean up any previous tmp
267
+ if (existsSync(tmpDir)) {
268
+ const { rmSync } = await import("node:fs");
269
+ rmSync(tmpDir, { recursive: true, force: true });
270
+ }
271
+ const { mkdirSync } = await import("node:fs");
272
+ mkdirSync(tmpDir, { recursive: true });
273
+
274
+ const tarballUrl = latestVersion ? TARBALL_URL(`v${latestVersion}`) : TARBALL_MAIN_URL;
275
+
276
+ const response = await fetch(tarballUrl);
277
+
278
+ // Fallback to main branch if tag doesn't exist
279
+ let finalResponse = response;
280
+ if (!response.ok && latestVersion) {
281
+ downloadSpinner.update("Tag not found, trying main branch...");
282
+ finalResponse = await fetch(TARBALL_MAIN_URL);
283
+ }
284
+
285
+ if (!finalResponse.ok) {
286
+ throw new Error(`HTTP ${finalResponse.status}`);
287
+ }
288
+
289
+ const tarPath = join(tmpDir, "update.tar.gz");
290
+ await Bun.write(tarPath, finalResponse);
291
+
292
+ downloadSpinner.update("Extracting...");
293
+ await exec(["tar", "xzf", tarPath, "--strip-components=1"], tmpDir);
294
+ const { rmSync: rm } = await import("node:fs");
295
+ rm(tarPath, { force: true });
296
+
297
+ downloadSpinner.stop("Download complete");
298
+ } catch (e) {
299
+ downloadSpinner.stop();
300
+ error(`Failed to download update: ${e instanceof Error ? e.message : String(e)}`);
301
+ // Cleanup
302
+ if (existsSync(tmpDir)) {
303
+ const { rmSync: rm } = await import("node:fs");
304
+ rm(tmpDir, { recursive: true, force: true });
305
+ }
306
+ process.exit(1);
307
+ }
308
+
309
+ // ── Apply updates ──
310
+ section("Applying updates");
311
+
312
+ let updatedCount = 0;
313
+ let skippedCount = 0;
314
+
315
+ const dryRun = args.includes("--dry-run");
316
+
317
+ for (const filePath of UPGRADEABLE_PATHS) {
318
+ const srcFile = join(tmpDir, filePath);
319
+ const destFile = join(projectDir, filePath);
320
+
321
+ if (!existsSync(srcFile)) {
322
+ continue;
323
+ }
324
+
325
+ try {
326
+ const newContent = await Bun.file(srcFile).text();
327
+
328
+ if (existsSync(destFile)) {
329
+ const currentContent = await Bun.file(destFile).text();
330
+ if (currentContent === newContent) {
331
+ skippedCount++;
332
+ continue; // No changes needed
333
+ }
334
+ }
335
+
336
+ if (!dryRun) {
337
+ // Ensure parent directory exists
338
+ const dir = destFile.substring(0, destFile.lastIndexOf("/"));
339
+ const { mkdirSync } = await import("node:fs");
340
+ mkdirSync(dir, { recursive: true });
341
+ await Bun.write(destFile, newContent);
342
+ }
343
+
344
+ step(`${dryRun ? `${dim("[dry-run]")} ` : ""}Updated ${bold(cyan(filePath))}`);
345
+ updatedCount++;
346
+ } catch {
347
+ warn(`Could not update ${filePath}`);
348
+ }
349
+ }
350
+
351
+ // ── Update dependencies ──
352
+ const newPkgPath = join(tmpDir, "package.json");
353
+ if (existsSync(newPkgPath)) {
354
+ try {
355
+ const newPkg = JSON.parse(await Bun.file(newPkgPath).text());
356
+ const currentPkg = JSON.parse(await Bun.file(pkgPath).text());
357
+
358
+ let depsChanged = false;
359
+
360
+ // Merge dependencies (add new ones, update existing)
361
+ if (newPkg.dependencies) {
362
+ currentPkg.dependencies = currentPkg.dependencies ?? {};
363
+ for (const [dep, ver] of Object.entries(newPkg.dependencies)) {
364
+ if (currentPkg.dependencies[dep] !== ver) {
365
+ currentPkg.dependencies[dep] = ver;
366
+ depsChanged = true;
367
+ }
368
+ }
369
+ }
370
+
371
+ // Merge devDependencies
372
+ if (newPkg.devDependencies) {
373
+ currentPkg.devDependencies = currentPkg.devDependencies ?? {};
374
+ for (const [dep, ver] of Object.entries(newPkg.devDependencies)) {
375
+ if (currentPkg.devDependencies[dep] !== ver) {
376
+ currentPkg.devDependencies[dep] = ver;
377
+ depsChanged = true;
378
+ }
379
+ }
380
+ }
381
+
382
+ // Update version
383
+ if (latestVersion) {
384
+ currentPkg.version = latestVersion;
385
+ }
386
+
387
+ if (!dryRun) {
388
+ await Bun.write(pkgPath, `${JSON.stringify(currentPkg, null, 2)}\n`);
389
+ }
390
+
391
+ if (depsChanged) {
392
+ step(
393
+ `${dryRun ? `${dim("[dry-run]")} ` : ""}Updated dependencies in ${bold(cyan("package.json"))}`,
394
+ );
395
+ }
396
+ } catch {
397
+ warn("Could not merge package.json dependencies");
398
+ }
399
+ }
400
+
401
+ // ── Cleanup tmp ──
402
+ if (existsSync(tmpDir)) {
403
+ const { rmSync: rm } = await import("node:fs");
404
+ rm(tmpDir, { recursive: true, force: true });
405
+ }
406
+
407
+ // ── Re-install dependencies ──
408
+ if (!dryRun && updatedCount > 0) {
409
+ section("Installing dependencies");
410
+
411
+ const installSpinner = createSpinner("Running bun install...");
412
+ installSpinner.start();
413
+
414
+ const { exitCode: installExit } = await exec(["bun", "install"], projectDir);
415
+
416
+ if (installExit !== 0) {
417
+ installSpinner.stop();
418
+ warn("bun install failed — run it manually.");
419
+ } else {
420
+ installSpinner.stop("Dependencies installed");
421
+ }
422
+ }
423
+
424
+ // ── Git commit ──
425
+ if (hasGit && !dryRun && updatedCount > 0) {
426
+ const shouldCommit = await confirm("Create a git commit for this upgrade?");
427
+ if (shouldCommit) {
428
+ const commitMsg = latestVersion
429
+ ? `chore: upgrade onlyApi to v${latestVersion}`
430
+ : "chore: upgrade onlyApi to latest";
431
+ await exec(["git", "add", "-A"], projectDir);
432
+ await exec(["git", "commit", "-m", commitMsg, "--no-verify"], projectDir);
433
+ step("Created upgrade commit");
434
+ }
435
+ }
436
+
437
+ // ── Summary ──
438
+ const elapsed = performance.now() - startTime;
439
+
440
+ blank();
441
+ if (updatedCount > 0) {
442
+ log(
443
+ ` ${icons.rocket} ${bold(green("Upgrade complete!"))} ${dim(`(${formatDuration(elapsed)})`)}`,
444
+ );
445
+ blank();
446
+ printKeyValue([
447
+ ["Files updated", String(updatedCount)],
448
+ ["Files unchanged", String(skippedCount)],
449
+ ]);
450
+ } else if (dryRun) {
451
+ log(` ${icons.info} ${bold(cyan("Dry run complete"))} — no files were modified.`);
452
+ } else {
453
+ success("All files are already up to date!");
454
+ }
455
+
456
+ blank();
457
+
458
+ // ── Show what's NOT upgraded ──
459
+ if (updatedCount > 0) {
460
+ log(` ${dim("Note: The following files are NOT auto-upgraded (your custom code):")}`);
461
+ log(` ${dim(" - src/application/ (your services & DTOs)")}`);
462
+ log(` ${dim(" - src/core/entities/ (your domain entities)")}`);
463
+ log(` ${dim(" - src/core/ports/ (your port interfaces)")}`);
464
+ log(` ${dim(" - src/presentation/handlers/ (your route handlers)")}`);
465
+ log(` ${dim(" - src/presentation/routes/ (your routes)")}`);
466
+ log(` ${dim(" - src/main.ts (your bootstrap)")}`);
467
+ blank();
468
+ log(` ${dim("Review the")} ${cyan("CHANGELOG.md")} ${dim("for breaking changes.")}`);
469
+ blank();
470
+ }
471
+ };
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * onlyApi CLI — developer tooling for scaffolding and upgrading projects.
5
+ *
6
+ * Usage:
7
+ * onlyapi init <project-name> Create a new project
8
+ * onlyapi upgrade Upgrade current project
9
+ * onlyapi version Show version
10
+ * onlyapi help Show help
11
+ */
12
+
13
+ import { helpCommand } from "./commands/help.js";
14
+ import { initCommand } from "./commands/init.js";
15
+ import { upgradeCommand } from "./commands/upgrade.js";
16
+ import { blank, bold, cyan, dim, error, log, white } from "./ui.js";
17
+
18
+ // ── Version ─────────────────────────────────────────────────────────────
19
+
20
+ const VERSION = "1.5.1";
21
+
22
+ // ── Arg parsing ─────────────────────────────────────────────────────────
23
+
24
+ const args = process.argv.slice(2);
25
+ const command = args[0]?.toLowerCase() ?? "";
26
+ const commandArgs = args.slice(1);
27
+
28
+ // ── Route command ───────────────────────────────────────────────────────
29
+
30
+ const run = async (): Promise<void> => {
31
+ try {
32
+ switch (command) {
33
+ case "init":
34
+ case "create":
35
+ case "new":
36
+ await initCommand(commandArgs, VERSION);
37
+ break;
38
+
39
+ case "upgrade":
40
+ case "update":
41
+ await upgradeCommand(commandArgs, VERSION);
42
+ break;
43
+
44
+ case "version":
45
+ case "-v":
46
+ case "--version":
47
+ log(`onlyapi v${VERSION}`);
48
+ break;
49
+
50
+ case "help":
51
+ case "-h":
52
+ case "--help":
53
+ helpCommand(VERSION);
54
+ break;
55
+
56
+ case "":
57
+ helpCommand(VERSION);
58
+ break;
59
+
60
+ default:
61
+ blank();
62
+ error(`Unknown command: ${bold(white(command))}`);
63
+ blank();
64
+ log(` ${dim("Run")} ${cyan("onlyapi help")} ${dim("to see available commands.")}`);
65
+ blank();
66
+ process.exit(1);
67
+ }
68
+ } catch (e) {
69
+ blank();
70
+ error(e instanceof Error ? e.message : String(e));
71
+ blank();
72
+ process.exit(1);
73
+ }
74
+ };
75
+
76
+ run();