@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.
- package/CHANGELOG.md +201 -0
- package/LICENSE +21 -0
- package/README.md +338 -0
- package/dist/cli.js +14 -0
- package/package.json +69 -0
- package/src/application/dtos/admin.dto.ts +25 -0
- package/src/application/dtos/auth.dto.ts +97 -0
- package/src/application/dtos/index.ts +40 -0
- package/src/application/index.ts +2 -0
- package/src/application/services/admin.service.ts +150 -0
- package/src/application/services/api-key.service.ts +65 -0
- package/src/application/services/auth.service.ts +606 -0
- package/src/application/services/health.service.ts +97 -0
- package/src/application/services/index.ts +10 -0
- package/src/application/services/user.service.ts +95 -0
- package/src/cli/commands/help.ts +86 -0
- package/src/cli/commands/init.ts +301 -0
- package/src/cli/commands/upgrade.ts +471 -0
- package/src/cli/index.ts +76 -0
- package/src/cli/ui.ts +189 -0
- package/src/cluster.ts +62 -0
- package/src/core/entities/index.ts +1 -0
- package/src/core/entities/user.entity.ts +24 -0
- package/src/core/errors/app-error.ts +81 -0
- package/src/core/errors/index.ts +15 -0
- package/src/core/index.ts +7 -0
- package/src/core/ports/account-lockout.ts +15 -0
- package/src/core/ports/alert-sink.ts +27 -0
- package/src/core/ports/api-key.ts +37 -0
- package/src/core/ports/audit-log.ts +46 -0
- package/src/core/ports/cache.ts +24 -0
- package/src/core/ports/circuit-breaker.ts +42 -0
- package/src/core/ports/event-bus.ts +78 -0
- package/src/core/ports/index.ts +62 -0
- package/src/core/ports/job-queue.ts +73 -0
- package/src/core/ports/logger.ts +21 -0
- package/src/core/ports/metrics.ts +49 -0
- package/src/core/ports/oauth.ts +55 -0
- package/src/core/ports/password-hasher.ts +10 -0
- package/src/core/ports/password-history.ts +23 -0
- package/src/core/ports/password-policy.ts +43 -0
- package/src/core/ports/refresh-token-store.ts +37 -0
- package/src/core/ports/retry.ts +23 -0
- package/src/core/ports/token-blacklist.ts +16 -0
- package/src/core/ports/token-service.ts +23 -0
- package/src/core/ports/totp-service.ts +16 -0
- package/src/core/ports/user.repository.ts +40 -0
- package/src/core/ports/verification-token.ts +41 -0
- package/src/core/ports/webhook.ts +58 -0
- package/src/core/types/brand.ts +19 -0
- package/src/core/types/index.ts +19 -0
- package/src/core/types/pagination.ts +28 -0
- package/src/core/types/result.ts +52 -0
- package/src/infrastructure/alerting/index.ts +1 -0
- package/src/infrastructure/alerting/webhook.ts +100 -0
- package/src/infrastructure/cache/in-memory-cache.ts +111 -0
- package/src/infrastructure/cache/index.ts +6 -0
- package/src/infrastructure/cache/redis-cache.ts +204 -0
- package/src/infrastructure/config/config.ts +185 -0
- package/src/infrastructure/config/index.ts +1 -0
- package/src/infrastructure/database/in-memory-user.repository.ts +134 -0
- package/src/infrastructure/database/index.ts +37 -0
- package/src/infrastructure/database/migrations/001_create_users.ts +26 -0
- package/src/infrastructure/database/migrations/002_create_token_blacklist.ts +21 -0
- package/src/infrastructure/database/migrations/003_create_audit_log.ts +31 -0
- package/src/infrastructure/database/migrations/004_auth_platform.ts +112 -0
- package/src/infrastructure/database/migrations/runner.ts +120 -0
- package/src/infrastructure/database/mssql/index.ts +14 -0
- package/src/infrastructure/database/mssql/migrations.ts +299 -0
- package/src/infrastructure/database/mssql/mssql-account-lockout.ts +95 -0
- package/src/infrastructure/database/mssql/mssql-api-keys.ts +146 -0
- package/src/infrastructure/database/mssql/mssql-audit-log.ts +86 -0
- package/src/infrastructure/database/mssql/mssql-oauth-accounts.ts +118 -0
- package/src/infrastructure/database/mssql/mssql-password-history.ts +71 -0
- package/src/infrastructure/database/mssql/mssql-refresh-token-store.ts +144 -0
- package/src/infrastructure/database/mssql/mssql-token-blacklist.ts +54 -0
- package/src/infrastructure/database/mssql/mssql-user.repository.ts +263 -0
- package/src/infrastructure/database/mssql/mssql-verification-tokens.ts +120 -0
- package/src/infrastructure/database/postgres/index.ts +14 -0
- package/src/infrastructure/database/postgres/migrations.ts +235 -0
- package/src/infrastructure/database/postgres/pg-account-lockout.ts +75 -0
- package/src/infrastructure/database/postgres/pg-api-keys.ts +126 -0
- package/src/infrastructure/database/postgres/pg-audit-log.ts +74 -0
- package/src/infrastructure/database/postgres/pg-oauth-accounts.ts +101 -0
- package/src/infrastructure/database/postgres/pg-password-history.ts +61 -0
- package/src/infrastructure/database/postgres/pg-refresh-token-store.ts +117 -0
- package/src/infrastructure/database/postgres/pg-token-blacklist.ts +48 -0
- package/src/infrastructure/database/postgres/pg-user.repository.ts +237 -0
- package/src/infrastructure/database/postgres/pg-verification-tokens.ts +97 -0
- package/src/infrastructure/database/sqlite-account-lockout.ts +97 -0
- package/src/infrastructure/database/sqlite-api-keys.ts +155 -0
- package/src/infrastructure/database/sqlite-audit-log.ts +90 -0
- package/src/infrastructure/database/sqlite-oauth-accounts.ts +105 -0
- package/src/infrastructure/database/sqlite-password-history.ts +54 -0
- package/src/infrastructure/database/sqlite-refresh-token-store.ts +122 -0
- package/src/infrastructure/database/sqlite-token-blacklist.ts +47 -0
- package/src/infrastructure/database/sqlite-user.repository.ts +260 -0
- package/src/infrastructure/database/sqlite-verification-tokens.ts +112 -0
- package/src/infrastructure/events/event-bus.ts +105 -0
- package/src/infrastructure/events/event-factory.ts +31 -0
- package/src/infrastructure/events/in-memory-webhook-registry.ts +67 -0
- package/src/infrastructure/events/index.ts +4 -0
- package/src/infrastructure/events/webhook-dispatcher.ts +114 -0
- package/src/infrastructure/index.ts +58 -0
- package/src/infrastructure/jobs/index.ts +1 -0
- package/src/infrastructure/jobs/job-queue.ts +185 -0
- package/src/infrastructure/logging/index.ts +1 -0
- package/src/infrastructure/logging/logger.ts +63 -0
- package/src/infrastructure/metrics/index.ts +1 -0
- package/src/infrastructure/metrics/prometheus.ts +231 -0
- package/src/infrastructure/oauth/github.ts +116 -0
- package/src/infrastructure/oauth/google.ts +83 -0
- package/src/infrastructure/oauth/index.ts +2 -0
- package/src/infrastructure/resilience/circuit-breaker.ts +133 -0
- package/src/infrastructure/resilience/index.ts +2 -0
- package/src/infrastructure/resilience/retry.ts +50 -0
- package/src/infrastructure/security/account-lockout.ts +73 -0
- package/src/infrastructure/security/index.ts +6 -0
- package/src/infrastructure/security/password-hasher.ts +31 -0
- package/src/infrastructure/security/password-policy.ts +77 -0
- package/src/infrastructure/security/token-blacklist.ts +45 -0
- package/src/infrastructure/security/token-service.ts +144 -0
- package/src/infrastructure/security/totp-service.ts +142 -0
- package/src/infrastructure/tracing/index.ts +7 -0
- package/src/infrastructure/tracing/trace-context.ts +93 -0
- package/src/main.ts +479 -0
- package/src/presentation/context.ts +26 -0
- package/src/presentation/handlers/admin.handler.ts +114 -0
- package/src/presentation/handlers/api-key.handler.ts +68 -0
- package/src/presentation/handlers/auth.handler.ts +218 -0
- package/src/presentation/handlers/health.handler.ts +27 -0
- package/src/presentation/handlers/index.ts +15 -0
- package/src/presentation/handlers/metrics.handler.ts +21 -0
- package/src/presentation/handlers/oauth.handler.ts +61 -0
- package/src/presentation/handlers/openapi.handler.ts +543 -0
- package/src/presentation/handlers/response.ts +29 -0
- package/src/presentation/handlers/sse.handler.ts +165 -0
- package/src/presentation/handlers/user.handler.ts +81 -0
- package/src/presentation/handlers/webhook.handler.ts +92 -0
- package/src/presentation/handlers/websocket.handler.ts +226 -0
- package/src/presentation/i18n/index.ts +254 -0
- package/src/presentation/index.ts +5 -0
- package/src/presentation/middleware/api-key.ts +18 -0
- package/src/presentation/middleware/auth.ts +39 -0
- package/src/presentation/middleware/cors.ts +41 -0
- package/src/presentation/middleware/index.ts +12 -0
- package/src/presentation/middleware/rate-limit.ts +65 -0
- package/src/presentation/middleware/security-headers.ts +18 -0
- package/src/presentation/middleware/validate.ts +16 -0
- package/src/presentation/middleware/versioning.ts +69 -0
- package/src/presentation/routes/index.ts +1 -0
- package/src/presentation/routes/router.ts +272 -0
- package/src/presentation/server.ts +381 -0
- package/src/shared/cli.ts +294 -0
- package/src/shared/container.ts +65 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/log-format.ts +148 -0
- package/src/shared/utils/id.ts +5 -0
- package/src/shared/utils/index.ts +2 -0
- 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
|
+
};
|
package/src/cli/index.ts
ADDED
|
@@ -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();
|