relight-cli 0.2.0 → 0.4.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.
@@ -1,13 +1,12 @@
1
1
  import { createInterface } from "readline";
2
- import { randomBytes } from "crypto";
3
2
  import { phase, status, success, hint, fatal, fmt, generateAppName } from "../lib/output.js";
4
3
  import { readLink, linkApp, resolveAppName } from "../lib/link.js";
5
- import { resolveCloudId, getCloudCfg, getProvider } from "../lib/providers/resolve.js";
4
+ import { resolveTarget } from "../lib/providers/resolve.js";
6
5
  import { dockerBuild, dockerTag, dockerPush, dockerLogin } from "../lib/docker.js";
7
6
 
8
7
  export async function deploy(nameOrPath, path, options) {
9
- var cloud = resolveCloudId(options.cloud);
10
- var cfg = getCloudCfg(cloud);
8
+ var target = await resolveTarget(options);
9
+ var cfg = target.cfg;
11
10
 
12
11
  // Smart arg parsing: if first arg looks like a path, shift args
13
12
  var name;
@@ -26,12 +25,12 @@ export async function deploy(nameOrPath, path, options) {
26
25
  var tag = options.tag || `${Date.now()}`;
27
26
 
28
27
  // Get registry credentials and image tag
29
- var registry = await getProvider(cloud, "registry");
28
+ var registry = await target.provider("registry");
30
29
  var remoteTag = await registry.getImageTag(cfg, name, tag);
31
30
  var localTag = `relight-${name}:${tag}`;
32
31
 
33
32
  // Load existing config from deployed worker (null on first deploy)
34
- var appProvider = await getProvider(cloud, "app");
33
+ var appProvider = await target.provider("app");
35
34
  var appConfig;
36
35
  try {
37
36
  appConfig = await appProvider.getAppConfig(cfg, name);
@@ -41,7 +40,7 @@ export async function deploy(nameOrPath, path, options) {
41
40
 
42
41
  var isFirstDeploy = !appConfig;
43
42
 
44
- // Get valid regions for this cloud
43
+ // Get valid regions for this cloud/service
45
44
  var validRegions = appProvider.getRegions();
46
45
  var validCodes = validRegions.map((r) => r.code);
47
46
 
@@ -89,12 +88,13 @@ export async function deploy(nameOrPath, path, options) {
89
88
  }
90
89
  }
91
90
 
92
- var defaultRegion = cloud === "gcp" ? "us-central1" : cloud === "aws" ? "us-east-1" : cloud === "slicervm" ? "self-hosted" : "enam";
91
+ var isService = target.kind === "service";
92
+ var defaultRegion = isService ? "self-hosted" : target.type === "gcp" ? "us-central1" : target.type === "aws" ? "us-east-1" : "enam";
93
93
  var regions;
94
94
 
95
95
  if (options.regions) {
96
96
  regions = options.regions.split(",").map((r) => r.trim());
97
- } else if ((cloud === "gcp" || cloud === "aws") && process.stdin.isTTY) {
97
+ } else if (!isService && (target.type === "gcp" || target.type === "aws") && process.stdin.isTTY) {
98
98
  // Interactive region picker for GCP/AWS first deploy
99
99
  var { createInterface: createRL } = await import("readline");
100
100
  var rl = createRL({ input: process.stdin, output: process.stderr });
@@ -130,10 +130,10 @@ export async function deploy(nameOrPath, path, options) {
130
130
  appConfig = {
131
131
  name,
132
132
  regions,
133
- instances: options.instances || (cloud === "slicervm" ? 1 : 2),
133
+ instances: options.instances || (isService ? 1 : 2),
134
134
  port: options.port || 8080,
135
135
  sleepAfter: options.sleep || "30s",
136
- instanceType: options.instanceType || (cloud === "gcp" || cloud === "aws" || cloud === "slicervm" ? undefined : "lite"),
136
+ instanceType: options.instanceType || (isService || target.type === "gcp" || target.type === "aws" ? undefined : "lite"),
137
137
  vcpu: options.vcpu || undefined,
138
138
  memory: options.memory || undefined,
139
139
  disk: options.disk || undefined,
@@ -147,48 +147,7 @@ export async function deploy(nameOrPath, path, options) {
147
147
  };
148
148
  }
149
149
 
150
- // --- D1 database (--db flag, CF only) ---
151
150
  var newSecrets = {};
152
- if (options.db && !appConfig.dbId) {
153
- if (cloud === "gcp") {
154
- process.stderr.write(
155
- `\n${fmt.dim("Cloud SQL provisioning takes 3-10 minutes. Use 'relight db create' separately after deploy.")}\n\n`
156
- );
157
- } else if (cloud === "aws") {
158
- process.stderr.write(
159
- `\n${fmt.dim("RDS provisioning takes 5-15 minutes. Use 'relight db create' separately after deploy.")}\n\n`
160
- );
161
- } else if (cloud === "cf") {
162
- var { createD1Database, getWorkersSubdomain } = await import("../lib/clouds/cf.js");
163
- var dbResult = await createD1Database(cfg.accountId, cfg.apiToken, `relight-${name}`, {
164
- locationHint: options.dbLocation,
165
- jurisdiction: options.dbJurisdiction,
166
- });
167
- appConfig.dbId = dbResult.uuid;
168
- appConfig.dbName = `relight-${name}`;
169
-
170
- if (!appConfig.envKeys) appConfig.envKeys = [];
171
- if (!appConfig.secretKeys) appConfig.secretKeys = [];
172
- if (!appConfig.env) appConfig.env = {};
173
-
174
- var subdomain = await getWorkersSubdomain(cfg.accountId, cfg.apiToken);
175
- var dbUrl = subdomain
176
- ? `https://relight-${name}.${subdomain}.workers.dev`
177
- : null;
178
-
179
- if (dbUrl) {
180
- appConfig.env["DB_URL"] = dbUrl;
181
- if (!appConfig.envKeys.includes("DB_URL")) appConfig.envKeys.push("DB_URL");
182
- }
183
-
184
- appConfig.env["DB_TOKEN"] = "[hidden]";
185
- appConfig.secretKeys = appConfig.secretKeys.filter((k) => k !== "DB_TOKEN");
186
- appConfig.secretKeys.push("DB_TOKEN");
187
- appConfig.envKeys = appConfig.envKeys.filter((k) => k !== "DB_TOKEN");
188
-
189
- newSecrets.DB_TOKEN = randomBytes(32).toString("hex");
190
- }
191
- }
192
151
 
193
152
  // --- Summary & confirmation ---
194
153
  var instanceDesc = appConfig.vcpu
@@ -198,7 +157,11 @@ export async function deploy(nameOrPath, path, options) {
198
157
  process.stderr.write(`\n${fmt.bold("Deploy summary")}\n`);
199
158
  process.stderr.write(`${fmt.dim("-".repeat(40))}\n`);
200
159
  process.stderr.write(` ${fmt.bold("App:")} ${fmt.app(name)}${isFirstDeploy ? fmt.dim(" (new)") : ""}\n`);
201
- process.stderr.write(` ${fmt.bold("Cloud:")} ${fmt.cloud(cloud)}\n`);
160
+ if (target.kind === "service") {
161
+ process.stderr.write(` ${fmt.bold("Service:")} ${fmt.cloud(target.id)} ${fmt.dim(`(${target.type})`)}\n`);
162
+ } else {
163
+ process.stderr.write(` ${fmt.bold("Cloud:")} ${fmt.cloud(target.id)}\n`);
164
+ }
202
165
  process.stderr.write(` ${fmt.bold("Path:")} ${dockerPath}\n`);
203
166
  process.stderr.write(` ${fmt.bold("Image:")} ${remoteTag}\n`);
204
167
  process.stderr.write(` ${fmt.bold("Regions:")} ${appConfig.regions.join(", ")}\n`);
@@ -225,7 +188,7 @@ export async function deploy(nameOrPath, path, options) {
225
188
 
226
189
  // 1. Build Docker image
227
190
  var platform = "linux/amd64";
228
- if (cloud === "slicervm") {
191
+ if (target.type === "slicervm") {
229
192
  // Match VM architecture
230
193
  var { listNodes } = await import("../lib/clouds/slicervm.js");
231
194
  var nodes = await listNodes(cfg);
@@ -236,8 +199,8 @@ export async function deploy(nameOrPath, path, options) {
236
199
  status(`${localTag} for ${platform}`);
237
200
  dockerBuild(dockerPath, localTag, { platform });
238
201
 
239
- if (cloud === "slicervm") {
240
- // SlicerVM: skip registry push - deploy extracts and uploads the image directly
202
+ if (target.kind === "service") {
203
+ // Service: skip registry push - deploy extracts and uploads the image directly
241
204
  phase("Deploying");
242
205
  await appProvider.deploy(cfg, name, localTag, {
243
206
  appConfig,
@@ -268,21 +231,20 @@ export async function deploy(nameOrPath, path, options) {
268
231
  var url = await appProvider.getAppUrl(cfg, name);
269
232
 
270
233
  if (options.json) {
271
- console.log(
272
- JSON.stringify(
273
- {
274
- name,
275
- cloud,
276
- image: remoteTag,
277
- url,
278
- regions: appConfig.regions,
279
- instances: appConfig.instances,
280
- firstDeploy: isFirstDeploy,
281
- },
282
- null,
283
- 2
284
- )
285
- );
234
+ var result = {
235
+ name,
236
+ image: remoteTag,
237
+ url,
238
+ regions: appConfig.regions,
239
+ instances: appConfig.instances,
240
+ firstDeploy: isFirstDeploy,
241
+ };
242
+ if (target.kind === "service") {
243
+ result.service = target.id;
244
+ } else {
245
+ result.cloud = target.id;
246
+ }
247
+ console.log(JSON.stringify(result, null, 2));
286
248
  } else {
287
249
  success(`App ${fmt.app(name)} deployed!`);
288
250
  process.stderr.write(` ${fmt.bold("Name:")} ${fmt.app(name)}\n`);
@@ -294,5 +256,9 @@ export async function deploy(nameOrPath, path, options) {
294
256
  }
295
257
 
296
258
  // Link this directory to the app
297
- linkApp(name, cloud, options.dns, options.dbCloud);
259
+ if (target.kind === "service") {
260
+ linkApp(name, null, options.dns, null, target.id);
261
+ } else {
262
+ linkApp(name, target.id, options.dns);
263
+ }
298
264
  }
@@ -4,10 +4,14 @@ import {
4
4
  tryGetConfig,
5
5
  CONFIG_PATH,
6
6
  CLOUD_NAMES,
7
+ SERVICE_TYPES,
8
+ getRegisteredServices,
9
+ normalizeServiceConfig,
7
10
  } from "../lib/config.js";
8
11
  import { verifyToken as cfVerify, getWorkersSubdomain } from "../lib/clouds/cf.js";
9
- import { mintAccessToken, verifyProject as gcpVerifyProject, listRegions as gcpListRegions, gcpApi, AR_API, SQLADMIN_API, DNS_API } from "../lib/clouds/gcp.js";
12
+ import { mintAccessToken, verifyProject as gcpVerifyProject, listAllServices as gcpListServices, gcpApi, AR_API, SQLADMIN_API, DNS_API } from "../lib/clouds/gcp.js";
10
13
  import { verifyCredentials as awsVerify, checkAppRunner, awsJsonApi, awsQueryApi, awsRestXmlApi } from "../lib/clouds/aws.js";
14
+ import { verifyConnection as slicerVerify } from "../lib/clouds/slicervm.js";
11
15
  import kleur from "kleur";
12
16
 
13
17
  var PASS = kleur.green("[ok]");
@@ -16,7 +20,7 @@ var SKIP = kleur.yellow("[--]");
16
20
 
17
21
  export async function doctor() {
18
22
  process.stderr.write(`\n${kleur.bold("relight doctor")}\n`);
19
- process.stderr.write(`${kleur.dim("".repeat(50))}\n\n`);
23
+ process.stderr.write(`${kleur.dim("-".repeat(50))}\n\n`);
20
24
  var allGood = true;
21
25
 
22
26
  // --- General checks ---
@@ -73,9 +77,29 @@ export async function doctor() {
73
77
  }
74
78
  }
75
79
 
80
+ // --- Services ---
81
+
82
+ var services = getRegisteredServices();
83
+ if (services.length > 0) {
84
+ process.stderr.write(`\n${kleur.bold(" Services")}\n`);
85
+
86
+ for (var service of services) {
87
+ var typeName = SERVICE_TYPES[service.type]?.name || service.type;
88
+ var endpoint = service.socketPath || service.apiUrl || "unknown";
89
+
90
+ allGood =
91
+ (await asyncCheck(`${service.name} (${typeName} - ${endpoint})`, async () => {
92
+ if (service.type === "slicervm") {
93
+ var cfg = normalizeServiceConfig(service);
94
+ await slicerVerify(cfg);
95
+ }
96
+ })) && allGood;
97
+ }
98
+ }
99
+
76
100
  // --- Summary ---
77
101
 
78
- process.stderr.write(`\n${kleur.dim("".repeat(50))}\n`);
102
+ process.stderr.write(`\n${kleur.dim("-".repeat(50))}\n`);
79
103
  if (allGood) {
80
104
  process.stderr.write(kleur.green("All checks passed.\n\n"));
81
105
  } else {
@@ -132,7 +156,7 @@ async function checkGCP(cfg) {
132
156
 
133
157
  ok =
134
158
  (await asyncCheck("Cloud Run API reachable", async () => {
135
- await gcpListRegions(token, cfg.project);
159
+ await gcpListServices(token, cfg.project);
136
160
  })) && ok;
137
161
 
138
162
  ok =
@@ -204,7 +228,7 @@ function check(label, fn) {
204
228
  return true;
205
229
  } catch (e) {
206
230
  process.stderr.write(` ${FAIL} ${label}`);
207
- if (e.message) process.stderr.write(kleur.dim(` ${e.message}`));
231
+ if (e.message) process.stderr.write(kleur.dim(` - ${e.message}`));
208
232
  process.stderr.write("\n");
209
233
  return false;
210
234
  }
@@ -217,7 +241,7 @@ async function asyncCheck(label, fn) {
217
241
  return true;
218
242
  } catch (e) {
219
243
  process.stderr.write(` ${FAIL} ${label}`);
220
- if (e.message) process.stderr.write(kleur.dim(` ${truncate(e.message, 80)}`));
244
+ if (e.message) process.stderr.write(kleur.dim(` - ${truncate(e.message, 80)}`));
221
245
  process.stderr.write("\n");
222
246
  return false;
223
247
  }
@@ -1,7 +1,7 @@
1
1
  import { createInterface } from "readline";
2
2
  import { phase, status, success, fatal, hint, fmt } from "../lib/output.js";
3
3
  import { resolveAppName, resolveDns, readLink, linkApp } from "../lib/link.js";
4
- import { resolveCloudId, getCloudCfg, getProvider } from "../lib/providers/resolve.js";
4
+ import { resolveTarget, resolveCloudId, getCloudCfg, getProvider } from "../lib/providers/resolve.js";
5
5
  import kleur from "kleur";
6
6
 
7
7
  function prompt(rl, question) {
@@ -10,9 +10,9 @@ function prompt(rl, question) {
10
10
 
11
11
  export async function domainsList(name, options) {
12
12
  name = resolveAppName(name);
13
- var cloud = resolveCloudId(options.cloud);
14
- var cfg = getCloudCfg(cloud);
15
- var dnsProvider = await getProvider(cloud, "dns");
13
+ var target = await resolveTarget(options);
14
+ var cfg = target.cfg;
15
+ var dnsProvider = await target.provider("dns");
16
16
 
17
17
  var result = await dnsProvider.listDomains(cfg, name);
18
18
 
@@ -58,16 +58,16 @@ export async function domainsAdd(args, options) {
58
58
  domain = null;
59
59
  }
60
60
 
61
- var appCloud = resolveCloudId(options.cloud);
62
- var appCfg = getCloudCfg(appCloud);
63
- var appProvider = await getProvider(appCloud, "app");
61
+ var target = await resolveTarget(options);
62
+ var appCfg = target.cfg;
63
+ var appProvider = await target.provider("app");
64
64
 
65
65
  // Cross-cloud DNS: --dns flag or .relight dns field specifies a different cloud for DNS records
66
66
  var dnsFlag = options.dns || resolveDns();
67
- var crossCloud = dnsFlag && resolveCloudId(dnsFlag) !== appCloud;
68
- var dnsCloud = crossCloud ? resolveCloudId(dnsFlag) : appCloud;
67
+ var crossCloud = dnsFlag && (target.kind === "service" || resolveCloudId(dnsFlag) !== target.id);
68
+ var dnsCloud = crossCloud ? resolveCloudId(dnsFlag) : (target.kind === "cloud" ? target.id : null);
69
69
  var dnsCfg = crossCloud ? getCloudCfg(dnsCloud) : appCfg;
70
- var dnsProvider = await getProvider(dnsCloud, "dns");
70
+ var dnsProvider = crossCloud ? await getProvider(dnsCloud, "dns") : await target.provider("dns");
71
71
 
72
72
  var appConfig = await appProvider.getAppConfig(appCfg, name);
73
73
  if (!appConfig) {
@@ -144,18 +144,29 @@ export async function domainsAdd(args, options) {
144
144
  rl.close();
145
145
 
146
146
  if (crossCloud) {
147
- // Cross-cloud: DNS record on one cloud, app config on another
147
+ // Cross-cloud: set up domain mapping on the app cloud first (e.g. Firebase Hosting for GCP)
148
+ var dnsTarget;
149
+ var dnsProxied = true;
150
+ if (appProvider.mapCustomDomain) {
151
+ status(`Setting up hosting for ${domain}...`);
152
+ var mapping = await appProvider.mapCustomDomain(appCfg, name, domain);
153
+ dnsTarget = mapping.dnsTarget;
154
+ if (mapping.proxied === false) dnsProxied = false;
155
+ } else {
156
+ var appUrl = await appProvider.getAppUrl(appCfg, name);
157
+ dnsTarget = new URL(appUrl).hostname;
158
+ }
159
+
160
+ // Create DNS record pointing to the hosting provider
148
161
  status(`Creating DNS record for ${domain}...`);
149
- var appUrl = await appProvider.getAppUrl(appCfg, name);
150
- var target = new URL(appUrl).hostname;
151
162
  try {
152
- await dnsProvider.addDnsRecord(dnsCfg, domain, target, zone);
163
+ await dnsProvider.addDnsRecord(dnsCfg, domain, dnsTarget, zone, { proxied: dnsProxied });
153
164
  } catch (e) {
154
165
  fatal(e.message);
155
166
  }
156
167
 
157
- // Update app config on the app cloud
158
- status(`Updating app config on ${appCloud}...`);
168
+ // Update app config on the app cloud/service
169
+ status(`Updating app config...`);
159
170
  if (!appConfig.domains) appConfig.domains = [];
160
171
  if (!appConfig.domains.includes(domain)) {
161
172
  appConfig.domains.push(domain);
@@ -165,7 +176,7 @@ export async function domainsAdd(args, options) {
165
176
  // Persist dns cloud in .relight so future commands don't need --dns
166
177
  var linked = readLink();
167
178
  if (linked && !linked.dns) {
168
- linkApp(linked.app, linked.cloud, dnsCloud);
179
+ linkApp(linked.app, linked.cloud, dnsCloud, undefined, linked.compute);
169
180
  }
170
181
  } else {
171
182
  // Same-cloud: existing flow
@@ -193,22 +204,28 @@ export async function domainsRemove(args, options) {
193
204
  fatal("Usage: relight domains remove [name] <domain>");
194
205
  }
195
206
 
196
- var appCloud = resolveCloudId(options.cloud);
197
- var appCfg = getCloudCfg(appCloud);
207
+ var target = await resolveTarget(options);
208
+ var appCfg = target.cfg;
198
209
 
199
210
  var dnsFlag = options.dns || resolveDns();
200
- var crossCloud = dnsFlag && resolveCloudId(dnsFlag) !== appCloud;
201
- var dnsCloud = crossCloud ? resolveCloudId(dnsFlag) : appCloud;
211
+ var crossCloud = dnsFlag && (target.kind === "service" || resolveCloudId(dnsFlag) !== target.id);
212
+ var dnsCloud = crossCloud ? resolveCloudId(dnsFlag) : (target.kind === "cloud" ? target.id : null);
202
213
  var dnsCfg = crossCloud ? getCloudCfg(dnsCloud) : appCfg;
203
- var dnsProvider = await getProvider(dnsCloud, "dns");
214
+ var dnsProvider = crossCloud ? await getProvider(dnsCloud, "dns") : await target.provider("dns");
204
215
 
205
216
  status(`Removing ${domain}...`);
206
217
 
207
218
  if (crossCloud) {
208
- // Cross-cloud: remove DNS record from dns cloud, update app config on app cloud
219
+ // Cross-cloud: remove DNS record from dns cloud, update app config on app cloud/service
209
220
  await dnsProvider.removeDnsRecord(dnsCfg, domain);
210
221
 
211
- var appProvider = await getProvider(appCloud, "app");
222
+ var appProvider = await target.provider("app");
223
+
224
+ // Remove domain mapping on the app cloud if supported
225
+ if (appProvider.unmapCustomDomain) {
226
+ await appProvider.unmapCustomDomain(appCfg, name, domain);
227
+ }
228
+
212
229
  var appConfig = await appProvider.getAppConfig(appCfg, name);
213
230
  if (appConfig) {
214
231
  appConfig.domains = (appConfig.domains || []).filter((d) => d !== domain);
@@ -1,13 +1,13 @@
1
1
  import { fatal, fmt } from "../lib/output.js";
2
2
  import { resolveAppName } from "../lib/link.js";
3
- import { resolveCloudId, getCloudCfg, getProvider } from "../lib/providers/resolve.js";
3
+ import { resolveTarget } from "../lib/providers/resolve.js";
4
4
  import kleur from "kleur";
5
5
 
6
6
  export async function logs(name, options) {
7
7
  name = resolveAppName(name);
8
- var cloud = resolveCloudId(options.cloud);
9
- var cfg = getCloudCfg(cloud);
10
- var appProvider = await getProvider(cloud, "app");
8
+ var target = await resolveTarget(options);
9
+ var cfg = target.cfg;
10
+ var appProvider = await target.provider("app");
11
11
 
12
12
  process.stderr.write(
13
13
  `Tailing logs for ${fmt.app(name)}... ${fmt.dim("(ctrl+c to stop)")}\n\n`
@@ -2,13 +2,13 @@ import { execSync } from "child_process";
2
2
  import { platform } from "os";
3
3
  import { fatal, fmt } from "../lib/output.js";
4
4
  import { resolveAppName } from "../lib/link.js";
5
- import { resolveCloudId, getCloudCfg, getProvider } from "../lib/providers/resolve.js";
5
+ import { resolveTarget } from "../lib/providers/resolve.js";
6
6
 
7
7
  export async function open(name, options) {
8
8
  name = resolveAppName(name);
9
- var cloud = resolveCloudId(options.cloud);
10
- var cfg = getCloudCfg(cloud);
11
- var appProvider = await getProvider(cloud, "app");
9
+ var target = await resolveTarget(options);
10
+ var cfg = target.cfg;
11
+ var appProvider = await target.provider("app");
12
12
 
13
13
  var url = await appProvider.getAppUrl(cfg, name);
14
14
 
@@ -1,13 +1,13 @@
1
1
  import { fatal, fmt, table } from "../lib/output.js";
2
2
  import { resolveAppName } from "../lib/link.js";
3
- import { resolveCloudId, getCloudCfg, getProvider } from "../lib/providers/resolve.js";
3
+ import { resolveTarget } from "../lib/providers/resolve.js";
4
4
  import kleur from "kleur";
5
5
 
6
6
  export async function ps(name, options) {
7
7
  name = resolveAppName(name);
8
- var cloud = resolveCloudId(options.cloud);
9
- var cfg = getCloudCfg(cloud);
10
- var appProvider = await getProvider(cloud, "app");
8
+ var target = await resolveTarget(options);
9
+ var cfg = target.cfg;
10
+ var appProvider = await target.provider("app");
11
11
 
12
12
  var appConfig = await appProvider.getAppConfig(cfg, name);
13
13
 
@@ -23,22 +23,25 @@ export async function ps(name, options) {
23
23
  // Fetch live metrics
24
24
  var metrics = await appProvider.getContainerStatus(cfg, name);
25
25
 
26
- // Aggregate: group by region + durableObjectId, keep only active instances
26
+ // Aggregate: group by region + id, keep only active instances
27
27
  var containers = [];
28
28
  for (var row of metrics) {
29
29
  var dim = row.dimensions;
30
- if (!dim.active) continue;
31
- var existing = containers.find(
32
- (c) => c.region === dim.region && c.doId === dim.durableObjectId
33
- );
30
+ if (dim.active === false) continue;
31
+ var id = dim.durableObjectId || dim.hostname || null;
32
+ var region = dim.region || dim.hostname || "-";
33
+ var existing = id ? containers.find(
34
+ (c) => c.region === region && c.id === id
35
+ ) : null;
34
36
  if (existing) {
35
37
  existing.cpuSamples++;
36
38
  existing.cpuLoad += row.avg?.cpuLoad || 0;
37
39
  existing.memory += row.avg?.memory || 0;
38
40
  } else {
39
41
  containers.push({
40
- region: dim.region,
41
- doId: dim.durableObjectId,
42
+ region,
43
+ id,
44
+ status: dim.status || null,
42
45
  cpuLoad: row.avg?.cpuLoad || 0,
43
46
  memory: row.avg?.memory || 0,
44
47
  cpuSamples: 1,
@@ -49,7 +52,7 @@ export async function ps(name, options) {
49
52
  c.cpuLoad = c.cpuLoad / c.cpuSamples;
50
53
  c.memory = c.memory / c.cpuSamples;
51
54
  }
52
- containers.sort((a, b) => a.region.localeCompare(b.region) || a.doId.localeCompare(b.doId));
55
+ containers.sort((a, b) => (a.region || "").localeCompare(b.region || ""));
53
56
 
54
57
  if (options.json) {
55
58
  console.log(
@@ -61,7 +64,8 @@ export async function ps(name, options) {
61
64
  instances,
62
65
  containers: containers.map((c) => ({
63
66
  region: c.region,
64
- id: c.doId,
67
+ id: c.id || null,
68
+ status: c.status || null,
65
69
  cpu: +(c.cpuLoad * 100).toFixed(1),
66
70
  memoryMiB: +(c.memory / 1024 / 1024).toFixed(0),
67
71
  })),
@@ -102,11 +106,12 @@ export async function ps(name, options) {
102
106
  console.log(`\n${fmt.bold("Containers:")}`);
103
107
 
104
108
  if (containers.length > 0) {
105
- var headers = ["", "REGION", "ID", "CPU", "MEMORY"];
109
+ var hasIds = containers.some((c) => c.id);
110
+ var headers = hasIds ? ["", "REGION", "ID", "CPU", "MEMORY"] : ["", "REGION", "STATUS", "CPU", "MEMORY"];
106
111
  var rows = containers.map((c) => [
107
112
  kleur.green("*"),
108
113
  c.region,
109
- c.doId.slice(0, 8),
114
+ hasIds ? (c.id || "-").slice(0, 8) : (c.status || "running"),
110
115
  (c.cpuLoad * 100).toFixed(1) + "%",
111
116
  (c.memory / 1024 / 1024).toFixed(0) + " MiB",
112
117
  ]);
@@ -1,12 +1,12 @@
1
1
  import { success, fatal, fmt } from "../lib/output.js";
2
2
  import { resolveAppName } from "../lib/link.js";
3
- import { resolveCloudId, getCloudCfg, getProvider } from "../lib/providers/resolve.js";
3
+ import { resolveTarget } from "../lib/providers/resolve.js";
4
4
 
5
5
  export async function scale(name, options) {
6
6
  name = resolveAppName(name);
7
- var cloud = resolveCloudId(options.cloud);
8
- var cfg = getCloudCfg(cloud);
9
- var appProvider = await getProvider(cloud, "app");
7
+ var target = await resolveTarget(options);
8
+ var cfg = target.cfg;
9
+ var appProvider = await target.provider("app");
10
10
 
11
11
  var appConfig = await appProvider.getAppConfig(cfg, name);
12
12