supacloud 0.5.3 → 0.6.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 +14 -10
  2. package/dist/index.js +91 -28
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -25,19 +25,22 @@ npx --package supacloud supacloudctl admin status
25
25
  it gives you the full toolset. You can still install either CLI on its own
26
26
  (`@supacloud/cli` exposes `supacloud-cli`, `@supacloud/admin` exposes `supacloud-admin`).
27
27
 
28
- ## 更新提示
28
+ ## 更新检查
29
29
 
30
- `supacloudctl cli ...` and `supacloudctl admin ...` check the npm `latest` dist-tag before dispatch.
31
- When a newer package exists, the dispatcher prints an update notice but still runs the dependency
32
- version bundled with the installed `supacloud` package. It never downloads or executes a newer
33
- operator CLI implicitly.
30
+ Normal `supacloudctl cli ...` and `supacloudctl admin ...` dispatch is local-only and does not
31
+ contact npm. Run an explicit check when needed:
34
32
 
35
- Disable the latest check when you need a pinned local version:
33
+ ```bash
34
+ supacloudctl check-update
35
+ supacloudctl check-update cli
36
+ supacloudctl check-update admin
37
+ ```
38
+
39
+ The dispatcher never downloads or executes a newer operator CLI implicitly. To opt into the old
40
+ notify-before-dispatch behavior, set:
36
41
 
37
42
  ```bash
38
- SUPACLOUD_NO_AUTO_UPDATE=1 supacloudctl cli status
39
- # or
40
- SUPACLOUD_AUTO_UPDATE=0 supacloudctl admin status
43
+ SUPACLOUD_AUTO_UPDATE=1 supacloudctl cli status
41
44
  ```
42
45
 
43
46
  Use `SUPACLOUD_NPM_REGISTRY` or `npm_config_registry` to point the check at a registry mirror.
@@ -67,10 +70,11 @@ supacloudctl admin --help
67
70
 
68
71
  - `SUPABASE_URL` / `SUPACLOUD_API_URL`
69
72
  - `SUPABASE_SERVICE_ROLE_KEY` / `SUPACLOUD_API_TOKEN`
73
+ - `SUPACLOUD_PROJECT_REF` when it cannot be inferred from a managed `<ref>.api.*` hostname
70
74
 
71
75
  `supacloudctl admin` expects platform context:
72
76
 
73
- - `SUPACLOUD_HOST` + `SUPACLOUD_SSH_KEY` / `SUPACLOUD_SSH_PASS`
77
+ - `SUPACLOUD_HOST` + `SUPACLOUD_SSH_KEY` / `SUPACLOUD_SSH_PASS` + `SUPACLOUD_SSH_HOST_FINGERPRINT`
74
78
  - `SUPACLOUD_API_URL` + `SUPACLOUD_API_TOKEN`
75
79
 
76
80
  > Note: `/usr/local/bin/supacloud` is the compiled management-api server binary.
package/dist/index.js CHANGED
@@ -75,6 +75,11 @@ function isAutoUpdateDisabled(env = process.env) {
75
75
  const autoUpdate = env.SUPACLOUD_AUTO_UPDATE;
76
76
  return Boolean(autoUpdate && ["0", "false", "no", "off"].includes(autoUpdate.toLowerCase()));
77
77
  }
78
+ function isAutoUpdateEnabled(env = process.env) {
79
+ if (isAutoUpdateDisabled(env))
80
+ return false;
81
+ return Boolean(env.SUPACLOUD_AUTO_UPDATE);
82
+ }
78
83
  function getNpmRegistry(env = process.env) {
79
84
  const registry = env.SUPACLOUD_NPM_REGISTRY || env.npm_config_registry || env.NPM_CONFIG_REGISTRY || DEFAULT_NPM_REGISTRY;
80
85
  return registry.endsWith("/") ? registry : `${registry}/`;
@@ -139,21 +144,23 @@ function isNewerVersion(latestVersion, currentVersion) {
139
144
  return true;
140
145
  return compareSemver(latestVersion, currentVersion) > 0;
141
146
  }
147
+ async function updateNoticeForLaunch(target, installed, env, fetchLatest) {
148
+ if (!isAutoUpdateEnabled(env))
149
+ return;
150
+ const latestVersion = await fetchLatest(target.pkg, env);
151
+ if (!latestVersion || !isNewerVersion(latestVersion, installed.version))
152
+ return;
153
+ return installed.version ? `${target.pkg} ${latestVersion} 可用;当前固定使用已安装版本 ${installed.version}。请显式运行包管理器更新。` : `${target.pkg} ${latestVersion} 可用;已安装版本无法识别,请通过包管理器检查锁定版本。`;
154
+ }
142
155
  async function createLaunchPlan(target, forwardedArgs, options = {}) {
143
156
  const env = options.env ?? process.env;
144
157
  const resolveInstalled = options.resolveInstalled ?? resolveInstalledPackage;
145
158
  const fetchLatest = options.fetchLatest ?? fetchLatestVersion;
146
159
  const installed = resolveInstalled(target.pkg);
147
- let updateNotice;
148
- if (!isAutoUpdateDisabled(env)) {
149
- const latestVersion = await fetchLatest(target.pkg, env);
150
- if (latestVersion && isNewerVersion(latestVersion, installed?.version ?? null)) {
151
- updateNotice = installed?.version ? `${target.pkg} ${latestVersion} 可用;当前固定使用已安装版本 ${installed.version}。请显式运行包管理器更新。` : `${target.pkg} ${latestVersion} 可用;请先通过包管理器显式安装。`;
152
- }
153
- }
154
160
  if (!installed) {
155
161
  throw new Error(`${target.pkg} 未安装。请先通过包管理器显式安装固定版本。`);
156
162
  }
163
+ const updateNotice = await updateNoticeForLaunch(target, installed, env, fetchLatest);
157
164
  return {
158
165
  mode: "local",
159
166
  command: options.nodePath ?? process.execPath,
@@ -162,8 +169,42 @@ async function createLaunchPlan(target, forwardedArgs, options = {}) {
162
169
  ...updateNotice ? { updateNotice } : {}
163
170
  };
164
171
  }
172
+ async function checkUpdate(target, options = {}) {
173
+ const env = options.env ?? process.env;
174
+ const resolveInstalled = options.resolveInstalled ?? resolveInstalledPackage;
175
+ const fetchLatest = options.fetchLatest ?? fetchLatestVersion;
176
+ const installed = resolveInstalled(target.pkg);
177
+ if (!installed)
178
+ return missingPackageCheck(target.pkg);
179
+ const latestVersion = await fetchLatest(target.pkg, env);
180
+ if (!latestVersion)
181
+ return unavailableRegistryCheck(target.pkg, installed.version);
182
+ return completedUpdateCheck(target.pkg, installed.version, latestVersion);
183
+ }
184
+ function missingPackageCheck(packageName) {
185
+ return { packageName, currentVersion: null, latestVersion: null, status: "not_installed" };
186
+ }
187
+ function unavailableRegistryCheck(packageName, currentVersion) {
188
+ return { packageName, currentVersion, latestVersion: null, status: "registry_unavailable" };
189
+ }
190
+ function completedUpdateCheck(packageName, currentVersion, latestVersion) {
191
+ const status = isNewerVersion(latestVersion, currentVersion) ? "update_available" : "up_to_date";
192
+ return { packageName, currentVersion, latestVersion, status };
193
+ }
194
+ function formatUpdateCheck(check) {
195
+ switch (check.status) {
196
+ case "update_available":
197
+ return `${check.packageName}: ${check.latestVersion} 可用(当前 ${check.currentVersion ?? "未知"})`;
198
+ case "up_to_date":
199
+ return `${check.packageName}: 已是最新版本 ${check.currentVersion ?? check.latestVersion ?? "未知"}`;
200
+ case "not_installed":
201
+ return `${check.packageName}: 未安装`;
202
+ case "registry_unavailable":
203
+ return `${check.packageName}: 无法从 npm registry 获取版本信息`;
204
+ }
205
+ }
165
206
  function buildHelp() {
166
- const subs = Object.entries(SUBCOMMANDS).map(([name, sub]) => ` ${name.padEnd(6)} ${sub.desc}`).join(`
207
+ const subcommandHelp = Object.entries(SUBCOMMANDS).map(([name, sub]) => ` ${name.padEnd(6)} ${sub.desc}`).join(`
167
208
  `);
168
209
  return `
169
210
  ╔═══════════════════════════════════════════════════════════╗
@@ -174,11 +215,12 @@ function buildHelp() {
174
215
  用法
175
216
 
176
217
  ${COMMAND} <子命令> [args...]
218
+ ${COMMAND} check-update [cli|admin]
177
219
  ${COMMAND} --help
178
220
 
179
221
  子命令
180
222
 
181
- ${subs}
223
+ ${subcommandHelp}
182
224
 
183
225
  示例
184
226
 
@@ -188,37 +230,56 @@ ${subs}
188
230
  ${COMMAND} admin status
189
231
  ${COMMAND} admin project list
190
232
  ${COMMAND} admin ssh ping
233
+ ${COMMAND} check-update
234
+ ${COMMAND} check-update cli
191
235
 
192
- 每次执行子命令时默认检查 npm latest;发现新版本时只输出更新提示,
193
- 始终运行已安装的固定版本。设置 SUPACLOUD_NO_AUTO_UPDATE=1 可禁用检查。
236
+ 普通分发默认不访问 npm,始终运行已安装的固定版本。
237
+ 使用 check-update 显式查询更新;SUPACLOUD_AUTO_UPDATE=1 可选择恢复分发前提示。
194
238
  `;
195
239
  }
240
+ async function runUpdateCheckCommand(args) {
241
+ const targetName = args[1];
242
+ if (args.length > 2 || targetName && !SUBCOMMANDS[targetName]) {
243
+ console.error(`❌ 未知更新检查目标: ${targetName ?? ""}`);
244
+ process.exit(1);
245
+ }
246
+ const targets = targetName ? [SUBCOMMANDS[targetName]] : Object.values(SUBCOMMANDS);
247
+ const checks = await Promise.all(targets.map((target) => checkUpdate(target)));
248
+ for (const check of checks)
249
+ console.log(formatUpdateCheck(check));
250
+ const failed = checks.some((check) => ["registry_unavailable", "not_installed"].includes(check.status));
251
+ process.exit(failed ? 1 : 0);
252
+ }
253
+ async function dispatchSubcommand(target, forwardedArgs) {
254
+ const launchPlan = await createLaunchPlan(target, forwardedArgs);
255
+ if (launchPlan.updateNotice)
256
+ console.error(`ℹ️ ${launchPlan.updateNotice}`);
257
+ const childProcess = spawn(launchPlan.command, launchPlan.args, {
258
+ stdio: "inherit",
259
+ shell: launchPlan.shell
260
+ });
261
+ childProcess.on("close", (code) => process.exit(code ?? 1));
262
+ childProcess.on("error", (error) => {
263
+ console.error(`❌ 启动 ${target.pkg} 失败: ${error.message}`);
264
+ process.exit(1);
265
+ });
266
+ }
196
267
  async function run(args) {
197
- const sub = args[0];
198
- if (!sub || sub === "--help" || sub === "-h" || sub === "help") {
268
+ const subcommandName = args[0];
269
+ if (!subcommandName || subcommandName === "--help" || subcommandName === "-h" || subcommandName === "help") {
199
270
  console.error(buildHelp());
200
271
  process.exit(0);
201
272
  }
202
- const target = SUBCOMMANDS[sub];
273
+ if (subcommandName === "check-update")
274
+ await runUpdateCheckCommand(args);
275
+ const target = SUBCOMMANDS[subcommandName];
203
276
  if (!target) {
204
- console.error(`❌ 未知子命令: ${sub}
277
+ console.error(`❌ 未知子命令: ${subcommandName}
205
278
  `);
206
279
  console.error(buildHelp());
207
280
  process.exit(1);
208
281
  }
209
- const plan = await createLaunchPlan(target, args.slice(1));
210
- if (plan.updateNotice) {
211
- console.error(`ℹ️ ${plan.updateNotice}`);
212
- }
213
- const child = spawn(plan.command, plan.args, {
214
- stdio: "inherit",
215
- shell: plan.shell
216
- });
217
- child.on("close", (code) => process.exit(code ?? 1));
218
- child.on("error", (err) => {
219
- console.error(`❌ 启动 ${target.pkg} 失败: ${err.message}`);
220
- process.exit(1);
221
- });
282
+ await dispatchSubcommand(target, args.slice(1));
222
283
  }
223
284
  function isMainModule(moduleUrl, argvEntry = process.argv[1]) {
224
285
  if (!argvEntry)
@@ -241,11 +302,13 @@ export {
241
302
  resolveInstalledPackage,
242
303
  isNewerVersion,
243
304
  isMainModule,
305
+ isAutoUpdateEnabled,
244
306
  isAutoUpdateDisabled,
245
307
  getNpmRegistry,
246
308
  fetchLatestVersion,
247
309
  createLaunchPlan,
248
310
  compareSemver,
311
+ checkUpdate,
249
312
  buildLatestMetadataUrl,
250
313
  buildHelp,
251
314
  SUBCOMMANDS
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supacloud",
3
- "version": "0.5.3",
3
+ "version": "0.6.0",
4
4
  "description": "Unified entrypoint: `supacloudctl cli` and `supacloudctl admin` in one command",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -31,8 +31,8 @@
31
31
  "directory": "packages/supacloud"
32
32
  },
33
33
  "dependencies": {
34
- "@supacloud/cli": "^0.9.2",
35
- "@supacloud/admin": "^0.3.2"
34
+ "@supacloud/cli": "^0.11.0",
35
+ "@supacloud/admin": "^0.5.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/bun": "^1.3.14",