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.
- package/README.md +79 -0
- package/dist/index.js +250 -0
- 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
|
+
}
|