supacloud 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +79 -0
  2. package/dist/index.js +250 -0
  3. package/package.json +41 -0
package/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # supacloud
2
+
3
+ Unified entrypoint that bundles both SupaCloud CLIs behind a single `supacloud` command.
4
+
5
+ ```
6
+ supacloud cli → @supacloud/cli (project-scoped developer tools)
7
+ supacloud admin → @supacloud/admin (platform operations: install / SSH / tenant management)
8
+ ```
9
+
10
+ ## 安装
11
+
12
+ ```bash
13
+ npm install -g supacloud
14
+ supacloud --help
15
+ ```
16
+
17
+ One-off execution:
18
+
19
+ ```bash
20
+ npx supacloud cli status
21
+ npx supacloud admin status
22
+ ```
23
+
24
+ `supacloud` depends on both `@supacloud/cli` and `@supacloud/admin`, so installing
25
+ it gives you the full toolset. You can still install either CLI on its own
26
+ (`@supacloud/cli` exposes `supacloud-cli`, `@supacloud/admin` exposes `supacloud-admin`).
27
+
28
+ ## 自动更新
29
+
30
+ `supacloud cli ...` and `supacloud admin ...` check the npm `latest` dist-tag before dispatch.
31
+ When a newer `@supacloud/cli` or `@supacloud/admin` version exists, the dispatcher runs it with
32
+ `npm exec --package <pkg>@<latest> -- <bin> ...args`. If the registry is unavailable, it falls
33
+ back to the dependency version bundled with the installed `supacloud` package.
34
+
35
+ Disable the latest check when you need a pinned local version:
36
+
37
+ ```bash
38
+ SUPACLOUD_NO_AUTO_UPDATE=1 supacloud cli status
39
+ # or
40
+ SUPACLOUD_AUTO_UPDATE=0 supacloud admin status
41
+ ```
42
+
43
+ Use `SUPACLOUD_NPM_REGISTRY` or `npm_config_registry` to point the check at a registry mirror.
44
+
45
+ ## 用法
46
+
47
+ ```bash
48
+ supacloud cli status
49
+ supacloud cli project get
50
+ supacloud cli gateway routes --ref <ref>
51
+
52
+ supacloud admin status
53
+ supacloud admin project list
54
+ supacloud admin ssh ping
55
+ ```
56
+
57
+ Each subcommand accepts its own `--help`:
58
+
59
+ ```bash
60
+ supacloud cli --help
61
+ supacloud admin --help
62
+ ```
63
+
64
+ ## 上下文
65
+
66
+ `supacloud cli` auto-links the current project from `.env`:
67
+
68
+ - `SUPABASE_URL` / `SUPACLOUD_API_URL`
69
+ - `SUPABASE_SERVICE_ROLE_KEY` / `SUPACLOUD_API_TOKEN`
70
+
71
+ `supacloud admin` expects platform context:
72
+
73
+ - `SUPACLOUD_HOST` + `SUPACLOUD_SSH_KEY` / `SUPACLOUD_SSH_PASS`
74
+ - `SUPACLOUD_API_URL` + `SUPACLOUD_API_TOKEN`
75
+
76
+ > Note: on hosts where SupaCloud is installed as a server, `/usr/local/bin/supacloud`
77
+ > is the compiled management-api binary (the server process). This npm package's
78
+ > `supacloud` command targets developer/operator workflows and is meant for your
79
+ > local machine, not the server's process namespace.
package/dist/index.js ADDED
@@ -0,0 +1,250 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { spawn } from "node:child_process";
5
+ import { existsSync, readFileSync } from "node:fs";
6
+ import { createRequire } from "node:module";
7
+ import { dirname, join, parse } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ var require2 = createRequire(import.meta.url);
10
+ var SUBCOMMANDS = {
11
+ cli: { pkg: "@supacloud/cli", bin: "supacloud-cli", desc: "项目级 CLI(开发者工具)" },
12
+ admin: { pkg: "@supacloud/admin", bin: "supacloud-admin", desc: "平台运维 CLI(安装/SSH/租户管理)" }
13
+ };
14
+ function resolveSubpackageEntry(pkgName) {
15
+ try {
16
+ return require2.resolve(pkgName);
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+ var COMMAND = "supacloud";
22
+ var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
23
+ var AUTO_UPDATE_TIMEOUT_MS = 2000;
24
+ function isRecord(value) {
25
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
26
+ }
27
+ function readPackageVersion(packageJsonPath) {
28
+ try {
29
+ const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
30
+ if (isRecord(parsed) && typeof parsed.version === "string")
31
+ return parsed.version;
32
+ } catch {}
33
+ return null;
34
+ }
35
+ function packageJsonMatches(path, pkgName) {
36
+ try {
37
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
38
+ return isRecord(parsed) && parsed.name === pkgName;
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+ function findPackageJsonFromEntry(entry, pkgName) {
44
+ let dir = dirname(entry);
45
+ const root = parse(dir).root;
46
+ while (dir && dir !== root) {
47
+ const candidate = join(dir, "package.json");
48
+ if (existsSync(candidate) && packageJsonMatches(candidate, pkgName)) {
49
+ return candidate;
50
+ }
51
+ dir = dirname(dir);
52
+ }
53
+ return null;
54
+ }
55
+ function resolveInstalledPackage(pkgName) {
56
+ const entry = resolveSubpackageEntry(pkgName);
57
+ if (!entry)
58
+ return null;
59
+ let packageJsonPath = null;
60
+ try {
61
+ packageJsonPath = require2.resolve(`${pkgName}/package.json`);
62
+ } catch {
63
+ packageJsonPath = findPackageJsonFromEntry(entry, pkgName);
64
+ }
65
+ return {
66
+ entry,
67
+ version: packageJsonPath ? readPackageVersion(packageJsonPath) : null
68
+ };
69
+ }
70
+ function isAutoUpdateDisabled(env = process.env) {
71
+ const noAutoUpdate = env.SUPACLOUD_NO_AUTO_UPDATE;
72
+ if (noAutoUpdate && !["0", "false", "no", "off"].includes(noAutoUpdate.toLowerCase())) {
73
+ return true;
74
+ }
75
+ const autoUpdate = env.SUPACLOUD_AUTO_UPDATE;
76
+ return Boolean(autoUpdate && ["0", "false", "no", "off"].includes(autoUpdate.toLowerCase()));
77
+ }
78
+ function getNpmRegistry(env = process.env) {
79
+ const registry = env.SUPACLOUD_NPM_REGISTRY || env.npm_config_registry || env.NPM_CONFIG_REGISTRY || DEFAULT_NPM_REGISTRY;
80
+ return registry.endsWith("/") ? registry : `${registry}/`;
81
+ }
82
+ function buildLatestMetadataUrl(pkgName, registry = DEFAULT_NPM_REGISTRY) {
83
+ const base = registry.endsWith("/") ? registry : `${registry}/`;
84
+ const encodedName = encodeURIComponent(pkgName).replace(/^%40/, "@");
85
+ return `${base}${encodedName}/latest`;
86
+ }
87
+ async function fetchLatestVersion(pkgName, env = process.env, fetchImpl) {
88
+ const activeFetch = fetchImpl ?? globalThis.fetch;
89
+ if (!activeFetch)
90
+ return null;
91
+ const controller = new AbortController;
92
+ const timeout = setTimeout(() => controller.abort(), AUTO_UPDATE_TIMEOUT_MS);
93
+ try {
94
+ const response = await activeFetch(buildLatestMetadataUrl(pkgName, getNpmRegistry(env)), {
95
+ headers: { Accept: "application/json" },
96
+ signal: controller.signal
97
+ });
98
+ if (!response.ok)
99
+ return null;
100
+ const metadata = await response.json();
101
+ if (isRecord(metadata) && typeof metadata.version === "string") {
102
+ return metadata.version;
103
+ }
104
+ return null;
105
+ } catch {
106
+ return null;
107
+ } finally {
108
+ clearTimeout(timeout);
109
+ }
110
+ }
111
+ function parseSemver(version) {
112
+ const match = version.trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
113
+ if (!match)
114
+ return null;
115
+ return {
116
+ main: [Number(match[1]), Number(match[2]), Number(match[3])],
117
+ prerelease: match[4] ?? null
118
+ };
119
+ }
120
+ function compareSemver(left, right) {
121
+ const a = parseSemver(left);
122
+ const b = parseSemver(right);
123
+ if (!a || !b)
124
+ return left.localeCompare(right);
125
+ for (let i = 0;i < 3; i += 1) {
126
+ if (a.main[i] !== b.main[i])
127
+ return a.main[i] > b.main[i] ? 1 : -1;
128
+ }
129
+ if (a.prerelease === b.prerelease)
130
+ return 0;
131
+ if (!a.prerelease)
132
+ return 1;
133
+ if (!b.prerelease)
134
+ return -1;
135
+ return a.prerelease.localeCompare(b.prerelease);
136
+ }
137
+ function isNewerVersion(latestVersion, currentVersion) {
138
+ if (!currentVersion)
139
+ return true;
140
+ return compareSemver(latestVersion, currentVersion) > 0;
141
+ }
142
+ async function createLaunchPlan(target, forwardedArgs, options = {}) {
143
+ const env = options.env ?? process.env;
144
+ const resolveInstalled = options.resolveInstalled ?? resolveInstalledPackage;
145
+ const fetchLatest = options.fetchLatest ?? fetchLatestVersion;
146
+ const installed = resolveInstalled(target.pkg);
147
+ if (!isAutoUpdateDisabled(env)) {
148
+ const latestVersion = await fetchLatest(target.pkg, env);
149
+ if (latestVersion && isNewerVersion(latestVersion, installed?.version ?? null)) {
150
+ return {
151
+ mode: "latest",
152
+ command: options.npmCommand ?? "npm",
153
+ args: ["exec", "--yes", "--package", `${target.pkg}@${latestVersion}`, "--", target.bin, ...forwardedArgs],
154
+ shell: (options.platform ?? process.platform) === "win32"
155
+ };
156
+ }
157
+ }
158
+ if (!installed) {
159
+ throw new Error(`${target.pkg} 未安装,且无法从 npm registry 获取最新版。`);
160
+ }
161
+ return {
162
+ mode: "local",
163
+ command: options.nodePath ?? process.execPath,
164
+ args: [installed.entry, ...forwardedArgs],
165
+ shell: false
166
+ };
167
+ }
168
+ function buildHelp() {
169
+ const subs = Object.entries(SUBCOMMANDS).map(([name, sub]) => ` ${name.padEnd(6)} ${sub.desc}`).join(`
170
+ `);
171
+ return `
172
+ ╔═══════════════════════════════════════════════════════════╗
173
+ ║ supacloud ║
174
+ ║ 统一入口 · 路由到项目级 CLI 与平台运维 CLI ║
175
+ ╚═══════════════════════════════════════════════════════════╝
176
+
177
+ 用法
178
+
179
+ ${COMMAND} <子命令> [args...]
180
+ ${COMMAND} --help
181
+
182
+ 子命令
183
+
184
+ ${subs}
185
+
186
+ 示例
187
+
188
+ ${COMMAND} cli status
189
+ ${COMMAND} cli project get
190
+ ${COMMAND} cli gateway routes --ref <ref>
191
+ ${COMMAND} admin status
192
+ ${COMMAND} admin project list
193
+ ${COMMAND} admin ssh ping
194
+
195
+ 每次执行子命令时默认检查 npm latest;发现 @supacloud/cli 或
196
+ @supacloud/admin 有新版本时会自动通过 npm exec 运行最新版。
197
+ 离线时回退到本地依赖。设置 SUPACLOUD_NO_AUTO_UPDATE=1 可禁用。
198
+ `;
199
+ }
200
+ async function run(args) {
201
+ const sub = args[0];
202
+ if (!sub || sub === "--help" || sub === "-h" || sub === "help") {
203
+ console.error(buildHelp());
204
+ process.exit(0);
205
+ }
206
+ const target = SUBCOMMANDS[sub];
207
+ if (!target) {
208
+ console.error(`❌ 未知子命令: ${sub}
209
+ `);
210
+ console.error(buildHelp());
211
+ process.exit(1);
212
+ }
213
+ const plan = await createLaunchPlan(target, args.slice(1));
214
+ const child = spawn(plan.command, plan.args, {
215
+ stdio: "inherit",
216
+ shell: plan.shell
217
+ });
218
+ child.on("close", (code) => process.exit(code ?? 1));
219
+ child.on("error", (err) => {
220
+ console.error(`❌ 启动 ${target.pkg} 失败: ${err.message}`);
221
+ process.exit(1);
222
+ });
223
+ }
224
+ var isMainEntry = (() => {
225
+ try {
226
+ return fileURLToPath(import.meta.url) === process.argv[1];
227
+ } catch {
228
+ return false;
229
+ }
230
+ })();
231
+ if (isMainEntry) {
232
+ run(process.argv.slice(2)).catch((err) => {
233
+ const message = err instanceof Error ? err.message : String(err);
234
+ console.error(`❌ supacloud 启动失败: ${message}`);
235
+ process.exit(1);
236
+ });
237
+ }
238
+ export {
239
+ resolveSubpackageEntry,
240
+ resolveInstalledPackage,
241
+ isNewerVersion,
242
+ isAutoUpdateDisabled,
243
+ getNpmRegistry,
244
+ fetchLatestVersion,
245
+ createLaunchPlan,
246
+ compareSemver,
247
+ buildLatestMetadataUrl,
248
+ buildHelp,
249
+ SUBCOMMANDS
250
+ };
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "supacloud",
3
+ "version": "0.3.0",
4
+ "description": "Unified entrypoint: `supacloud cli` and `supacloud admin` in one command",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "bin": {
8
+ "supacloud": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "dev": "bun run --watch src/index.ts",
16
+ "build": "bun build src/index.ts --outdir dist --target node",
17
+ "prepublishOnly": "bun run build",
18
+ "typecheck": "tsc --noEmit"
19
+ },
20
+ "keywords": [
21
+ "supacloud",
22
+ "cli",
23
+ "admin",
24
+ "supabase",
25
+ "umbrella"
26
+ ],
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/zuohuadong/supacloud.git",
31
+ "directory": "packages/supacloud"
32
+ },
33
+ "dependencies": {
34
+ "@supacloud/cli": "^0.8.0",
35
+ "@supacloud/admin": "^0.2.0"
36
+ },
37
+ "devDependencies": {
38
+ "@types/bun": "^1.3.14",
39
+ "typescript": "^6.0.3"
40
+ }
41
+ }