@groundfloorcloud/cli 0.1.3 → 0.1.5

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 +11 -7
  2. package/dist/index.js +451 -116
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -65,7 +65,7 @@ Authentication, mode `groundfloor` or `external`) until the realm is active.
65
65
 
66
66
  | `--workload-type` | Also called | Package notes |
67
67
  |-------------------|-------------|---------------|
68
- | `function` | Function | HTTP on `PORT`/8080. Node needs `scripts.start`. |
68
+ | `function` | Function | HTTP on `0.0.0.0:PORT`/8080. Node needs `scripts.start`. |
69
69
  | `job` | Background job | Run-to-completion. Then `gf coderunner run`. |
70
70
  | `schedule` | Scheduled job | Cron. No public URL. |
71
71
  | `service` | **Deployment** | Long-running HTTP. **Root `Dockerfile` is required.** |
@@ -88,7 +88,9 @@ How the target is chosen:
88
88
 
89
89
  Runtime is auto-detected when omitted (`package.json` → node,
90
90
  `requirements.txt` / `pyproject.toml` → python), defaulting to python.
91
- Node images run `npm start` — add `"scripts": { "start": "node index.js" }`.
91
+ Node without a Dockerfile uses the platform bootstrap (`CMD ["npm","start"]`,
92
+ port 8080). Add `"scripts": { "start": "node index.js" }` and bind
93
+ `0.0.0.0`. `gf deploy` fails immediately if `scripts.start` is missing.
92
94
 
93
95
  ### Local with live data
94
96
 
@@ -152,11 +154,12 @@ gf apps publish --path release.zip
152
154
  (`/downloads/shell-starter-kit.zip` on the current cell) and stamps
153
155
  `APP_ID` / `groundfloor.manifest.json` `appId` to the slug.
154
156
 
155
- `gf apps publish` uploads `release.zip` (or `remoteEntry.js`), flattens a
156
- Vite `dist/assets/` layout so chunks sit next to `remoteEntry.js`, finalizes
157
- the release, and by default PATCHes the Portal manifest from
158
- `groundfloor.manifest.json` next to the bundle. Pass an app id/slug, or set
159
- `appId` in that manifest. Prefer `npm run release` over zipping `dist/` by hand.
157
+ `gf apps publish` uploads **only** `release.zip` from `npm run release`.
158
+ It flattens a Vite `dist/assets/` layout when needed, then **rejects** a zip
159
+ that is only `remoteEntry.js` (missing `__federation_*.js` / CSS) or whose
160
+ federation `name` does not match `federationRemoteName(slug)`. A lone
161
+ `.js` file is also rejected. Pass an app id/slug, or set `appId` in
162
+ `groundfloor.manifest.json` next to the zip.
160
163
 
161
164
  Wrap an existing **service** coderunner as a product:
162
165
 
@@ -200,6 +203,7 @@ gf apps get <id|slug>
200
203
  | `gf workspaces` (`ws`) | List workspaces; `gf workspaces use <id>` sets the default. |
201
204
  | `gf coderunner ls` (`cr ls`) | List coderunners. |
202
205
  | `gf coderunner status -c <id\|slug>` | Status and current deployment. |
206
+ | `gf coderunner logs -c <id\|slug>` | Process Log (stdout/stderr). Also printed automatically on `gf deploy` failure. |
203
207
  | `gf coderunner run -c <id\|slug> [--payload '{}']` | Invoke a deployed function or job. |
204
208
  | `gf deploy` | Package code and deploy as a coderunner. |
205
209
  | `gf domains ls\|add\|rm\|verify -c <id\|slug>` | Custom domains for a service. |
package/dist/index.js CHANGED
@@ -533,9 +533,9 @@ async function parseError(res) {
533
533
  }
534
534
  return `${res.status} ${detail}`;
535
535
  }
536
- async function cpGet(apiUrl, path7) {
536
+ async function cpGet(apiUrl, path8) {
537
537
  const token = await getValidAccessToken();
538
- const res = await fetch(`${apiUrl}${path7}`, {
538
+ const res = await fetch(`${apiUrl}${path8}`, {
539
539
  headers: {
540
540
  Authorization: `Bearer ${token}`,
541
541
  Accept: "application/json"
@@ -546,9 +546,9 @@ async function cpGet(apiUrl, path7) {
546
546
  }
547
547
  return await res.json();
548
548
  }
549
- async function cpPostJson(apiUrl, path7, body) {
549
+ async function cpPostJson(apiUrl, path8, body) {
550
550
  const token = await getValidAccessToken();
551
- const res = await fetch(`${apiUrl}${path7}`, {
551
+ const res = await fetch(`${apiUrl}${path8}`, {
552
552
  method: "POST",
553
553
  headers: {
554
554
  Authorization: `Bearer ${token}`,
@@ -562,9 +562,9 @@ async function cpPostJson(apiUrl, path7, body) {
562
562
  }
563
563
  return await res.json();
564
564
  }
565
- async function cpDelete(apiUrl, path7) {
565
+ async function cpDelete(apiUrl, path8) {
566
566
  const token = await getValidAccessToken();
567
- const res = await fetch(`${apiUrl}${path7}`, {
567
+ const res = await fetch(`${apiUrl}${path8}`, {
568
568
  method: "DELETE",
569
569
  headers: { Authorization: `Bearer ${token}`, Accept: "application/json" }
570
570
  });
@@ -572,9 +572,9 @@ async function cpDelete(apiUrl, path7) {
572
572
  throw new CpError(res.status, await parseError(res));
573
573
  }
574
574
  }
575
- async function cpPutJson(apiUrl, path7, body) {
575
+ async function cpPutJson(apiUrl, path8, body) {
576
576
  const token = await getValidAccessToken();
577
- const res = await fetch(`${apiUrl}${path7}`, {
577
+ const res = await fetch(`${apiUrl}${path8}`, {
578
578
  method: "PUT",
579
579
  headers: {
580
580
  Authorization: `Bearer ${token}`,
@@ -588,9 +588,9 @@ async function cpPutJson(apiUrl, path7, body) {
588
588
  }
589
589
  return await res.json();
590
590
  }
591
- async function cpPatchJson(apiUrl, path7, body) {
591
+ async function cpPatchJson(apiUrl, path8, body) {
592
592
  const token = await getValidAccessToken();
593
- const res = await fetch(`${apiUrl}${path7}`, {
593
+ const res = await fetch(`${apiUrl}${path8}`, {
594
594
  method: "PATCH",
595
595
  headers: {
596
596
  Authorization: `Bearer ${token}`,
@@ -604,9 +604,9 @@ async function cpPatchJson(apiUrl, path7, body) {
604
604
  }
605
605
  return await res.json();
606
606
  }
607
- async function cpPutBytes(apiUrl, path7, body, contentType) {
607
+ async function cpPutBytes(apiUrl, path8, body, contentType) {
608
608
  const token = await getValidAccessToken();
609
- const res = await fetch(`${apiUrl}${path7}`, {
609
+ const res = await fetch(`${apiUrl}${path8}`, {
610
610
  method: "PUT",
611
611
  headers: {
612
612
  Authorization: `Bearer ${token}`,
@@ -724,6 +724,18 @@ function runCoderunner(apiUrl, workspaceId, coderunnerId, payload) {
724
724
  payload
725
725
  );
726
726
  }
727
+ function getProcessLogs(apiUrl, workspaceId, query = {}) {
728
+ const params = new URLSearchParams();
729
+ for (const [key, value] of Object.entries(query)) {
730
+ if (value == null || value === "") continue;
731
+ params.set(key, String(value));
732
+ }
733
+ const qs = params.toString();
734
+ return cpGet(
735
+ apiUrl,
736
+ `/v1/workspaces/${encodeURIComponent(workspaceId)}/logs${qs ? `?${qs}` : ""}`
737
+ );
738
+ }
727
739
  function domainsBase(workspaceId, coderunnerId) {
728
740
  return `/v1/workspaces/${encodeURIComponent(
729
741
  workspaceId
@@ -1003,9 +1015,188 @@ async function coderunnerListCommand(opts) {
1003
1015
  }
1004
1016
  }
1005
1017
 
1018
+ // src/process-log.ts
1019
+ var PROCESS_STATUS_LABELS = {
1020
+ 1: "Pending",
1021
+ 2: "Processing",
1022
+ 3: "Completed",
1023
+ 4: "Failed",
1024
+ 5: "Deleting",
1025
+ 6: "Deleted",
1026
+ 7: "Stopped"
1027
+ };
1028
+ function statusLabel(status) {
1029
+ if (status == null) return "unknown";
1030
+ const name = PROCESS_STATUS_LABELS[status];
1031
+ return name ? `${name} (${status})` : `status=${status}`;
1032
+ }
1033
+ function customerPortalOrigin(apiUrl) {
1034
+ try {
1035
+ const url = new URL(apiUrl);
1036
+ url.host = url.host.replace(/^platform\./, "app.");
1037
+ return url.origin;
1038
+ } catch {
1039
+ return "https://app.groundfloor.cloud";
1040
+ }
1041
+ }
1042
+ function processLogPortalUrl(portalOrigin, workspaceId, runnerId) {
1043
+ const params = new URLSearchParams({
1044
+ workspace_id: workspaceId,
1045
+ runner_id: runnerId
1046
+ });
1047
+ return `${portalOrigin.replace(/\/+$/, "")}/process-log?${params.toString()}`;
1048
+ }
1049
+ function formatLogPreview(entries, limit = 20) {
1050
+ const slice = entries.slice(0, limit);
1051
+ return [...slice].reverse().map((entry) => {
1052
+ const ts = entry.ts?.trim() ? `${entry.ts} ` : "";
1053
+ return `${ts}${entry.line}`;
1054
+ }).join("\n");
1055
+ }
1056
+ function formatDeployFailure(input) {
1057
+ const lines = [input.headline, ""];
1058
+ lines.push(`coderunner: ${input.coderunnerId}`);
1059
+ if (input.versionId) lines.push(`version: ${input.versionId}`);
1060
+ if (input.deploymentId) lines.push(`deployment: ${input.deploymentId}`);
1061
+ if (input.status != null) lines.push(`status: ${statusLabel(input.status)}`);
1062
+ if (input.logLines) {
1063
+ lines.push("", "logs:", input.logLines);
1064
+ } else {
1065
+ lines.push("", "logs: (none yet)");
1066
+ }
1067
+ if (input.portalUrl) {
1068
+ lines.push("", `portal: ${input.portalUrl}`);
1069
+ }
1070
+ lines.push(`more logs: gf coderunner logs -c ${input.coderunnerId}`);
1071
+ return lines.join("\n");
1072
+ }
1073
+ async function fetchFailureLogEntries(apiUrl, workspaceId, ids) {
1074
+ const attempts = [];
1075
+ if (ids.deploymentId) attempts.push({ deployment_id: ids.deploymentId });
1076
+ attempts.push({ runner_id: ids.coderunnerId });
1077
+ if (ids.versionId && ids.versionId !== ids.coderunnerId) {
1078
+ attempts.push({ runner_id: ids.versionId });
1079
+ }
1080
+ const seen = /* @__PURE__ */ new Set();
1081
+ let lastError;
1082
+ let anyOk = false;
1083
+ for (const query of attempts) {
1084
+ const key = JSON.stringify(query);
1085
+ if (seen.has(key)) continue;
1086
+ seen.add(key);
1087
+ try {
1088
+ const page = await getProcessLogs(apiUrl, workspaceId, {
1089
+ ...query,
1090
+ limit: 20,
1091
+ since_seconds: 3600
1092
+ });
1093
+ anyOk = true;
1094
+ if (page.entries?.length) return page.entries;
1095
+ } catch (err) {
1096
+ lastError = err;
1097
+ }
1098
+ }
1099
+ if (!anyOk && lastError) throw lastError;
1100
+ return [];
1101
+ }
1102
+ async function diagnoseDeployFailure(opts) {
1103
+ let logLines = "";
1104
+ try {
1105
+ const entries = await fetchFailureLogEntries(opts.apiUrl, opts.workspaceId, {
1106
+ coderunnerId: opts.coderunnerId,
1107
+ versionId: opts.versionId ?? void 0,
1108
+ deploymentId: opts.deploymentId ?? void 0
1109
+ });
1110
+ logLines = formatLogPreview(entries);
1111
+ } catch (err) {
1112
+ const message = err instanceof Error ? err.message : String(err);
1113
+ logLines = `(could not fetch Process Log: ${message})`;
1114
+ }
1115
+ const runnerForPortal = opts.deploymentId || opts.coderunnerId;
1116
+ return formatDeployFailure({
1117
+ headline: opts.headline,
1118
+ coderunnerId: opts.coderunnerId,
1119
+ versionId: opts.versionId,
1120
+ deploymentId: opts.deploymentId,
1121
+ status: opts.status,
1122
+ logLines,
1123
+ portalUrl: processLogPortalUrl(
1124
+ customerPortalOrigin(opts.apiUrl),
1125
+ opts.workspaceId,
1126
+ runnerForPortal
1127
+ )
1128
+ });
1129
+ }
1130
+
1131
+ // src/resolve-coderunner.ts
1132
+ async function resolveCoderunnerId(apiUrl, workspaceId, idOrSlug) {
1133
+ const list = await listCoderunners(apiUrl, workspaceId);
1134
+ const match = list.find((c) => c.id === idOrSlug || c.slug === idOrSlug);
1135
+ if (!match) {
1136
+ throw new Error(`Coderunner not found: ${idOrSlug}`);
1137
+ }
1138
+ return match.id;
1139
+ }
1140
+
1141
+ // src/commands/logs.ts
1142
+ async function logsCommand(opts) {
1143
+ const cfg = await resolveConfig({ workspaceId: opts.workspace });
1144
+ if (!cfg.workspaceId) {
1145
+ throw new Error(
1146
+ "No workspace selected. Run `gf workspaces use <id>` or pass --workspace <id>."
1147
+ );
1148
+ }
1149
+ const coderunnerId = await resolveCoderunnerId(
1150
+ cfg.apiUrl,
1151
+ cfg.workspaceId,
1152
+ opts.coderunner
1153
+ );
1154
+ const limit = opts.limit ? Number(opts.limit) : 50;
1155
+ const sinceSeconds = opts.sinceSeconds ? Number(opts.sinceSeconds) : 3600;
1156
+ if (!Number.isFinite(limit) || limit < 1) {
1157
+ throw new Error("Invalid --limit (expected a positive number).");
1158
+ }
1159
+ if (!Number.isFinite(sinceSeconds) || sinceSeconds < 10) {
1160
+ throw new Error("Invalid --since-seconds (minimum 10).");
1161
+ }
1162
+ const page = await getProcessLogs(cfg.apiUrl, cfg.workspaceId, {
1163
+ runner_id: coderunnerId,
1164
+ limit,
1165
+ since_seconds: sinceSeconds,
1166
+ search: opts.search
1167
+ });
1168
+ if (opts.json) {
1169
+ process.stdout.write(`${JSON.stringify(page, null, 2)}
1170
+ `);
1171
+ return;
1172
+ }
1173
+ const preview = formatLogPreview(page.entries ?? [], limit);
1174
+ if (!preview) {
1175
+ process.stdout.write("No log entries.\n");
1176
+ } else {
1177
+ process.stdout.write(`${preview}
1178
+ `);
1179
+ }
1180
+ if (page.has_more) {
1181
+ process.stdout.write(
1182
+ `(more available \u2014 raise --limit or narrow --since-seconds)
1183
+ `
1184
+ );
1185
+ }
1186
+ process.stdout.write(
1187
+ `
1188
+ portal: ${processLogPortalUrl(
1189
+ customerPortalOrigin(cfg.apiUrl),
1190
+ cfg.workspaceId,
1191
+ coderunnerId
1192
+ )}
1193
+ `
1194
+ );
1195
+ }
1196
+
1006
1197
  // src/commands/deploy.ts
1007
- import { promises as fs3 } from "fs";
1008
- import path3 from "path";
1198
+ import { promises as fs4 } from "fs";
1199
+ import path4 from "path";
1009
1200
 
1010
1201
  // src/deploy/package.ts
1011
1202
  import { execFile } from "child_process";
@@ -1173,6 +1364,45 @@ async function detectRuntime(dir) {
1173
1364
  return null;
1174
1365
  }
1175
1366
 
1367
+ // src/deploy/preflight.ts
1368
+ import { promises as fs3 } from "fs";
1369
+ import path3 from "path";
1370
+ var NODE_BOOTSTRAP_MISSING_START = 'Node packages without a Dockerfile use the platform bootstrap image (CMD ["npm","start"], EXPOSE 8080). Add package.json "scripts": { "start": "node <entrypoint>" }. The process must listen on process.env.PORT || 8080 and bind 0.0.0.0 (not 127.0.0.1).';
1371
+ var NODE_BOOTSTRAP_MISSING_PACKAGE = 'Node packages without a Dockerfile need package.json with "scripts.start" (bootstrap image runs `npm start` and probes 0.0.0.0:8080).';
1372
+ var NODE_DOCKERFILE_NPM_START_MISSING = 'Dockerfile runs `npm start` but package.json has no "scripts.start". Add "scripts": { "start": "node <entrypoint>" }.';
1373
+ function dockerfileRunsNpmStart(text) {
1374
+ return /npm["'\s,]+start/.test(text);
1375
+ }
1376
+ async function readOptional(filePath) {
1377
+ try {
1378
+ return await fs3.readFile(filePath, "utf8");
1379
+ } catch {
1380
+ return null;
1381
+ }
1382
+ }
1383
+ async function preflightNodePackage(sourceDir) {
1384
+ const dockerfile = await readOptional(path3.join(sourceDir, "Dockerfile"));
1385
+ const needsStartScript = dockerfile == null || dockerfileRunsNpmStart(dockerfile);
1386
+ if (!needsStartScript) return;
1387
+ const raw = await readOptional(path3.join(sourceDir, "package.json"));
1388
+ if (raw == null) {
1389
+ throw new Error(
1390
+ dockerfile == null ? NODE_BOOTSTRAP_MISSING_PACKAGE : NODE_DOCKERFILE_NPM_START_MISSING
1391
+ );
1392
+ }
1393
+ let npm;
1394
+ try {
1395
+ npm = JSON.parse(raw);
1396
+ } catch {
1397
+ throw new Error("package.json is not valid JSON.");
1398
+ }
1399
+ if (!npm.scripts?.start?.trim()) {
1400
+ throw new Error(
1401
+ dockerfile == null ? NODE_BOOTSTRAP_MISSING_START : NODE_DOCKERFILE_NPM_START_MISSING
1402
+ );
1403
+ }
1404
+ }
1405
+
1176
1406
  // src/commands/deploy.ts
1177
1407
  var RUNTIMES = /* @__PURE__ */ new Set(["python", "node", "dotnet-script"]);
1178
1408
  var WORKLOAD_TYPES = /* @__PURE__ */ new Set(["function", "service", "job", "schedule"]);
@@ -1202,6 +1432,9 @@ function parseEnvPairs(pairs) {
1202
1432
  }
1203
1433
  var log = (msg) => process.stderr.write(`${msg}
1204
1434
  `);
1435
+ async function failDeploy(opts) {
1436
+ throw new Error(await diagnoseDeployFailure(opts));
1437
+ }
1205
1438
  async function deployCommand(opts) {
1206
1439
  const cfg = await resolveConfig({
1207
1440
  apiUrl: opts.apiUrl,
@@ -1214,7 +1447,7 @@ async function deployCommand(opts) {
1214
1447
  }
1215
1448
  const ws = cfg.workspaceId;
1216
1449
  log(
1217
- opts.git ? `Cloning ${opts.git}${opts.ref ? ` (${opts.ref})` : ""}...` : `Packaging ${path3.resolve(opts.path ?? process.cwd())}...`
1450
+ opts.git ? `Cloning ${opts.git}${opts.ref ? ` (${opts.ref})` : ""}...` : `Packaging ${path4.resolve(opts.path ?? process.cwd())}...`
1218
1451
  );
1219
1452
  const pkg = await packageSource({
1220
1453
  path: opts.path,
@@ -1241,7 +1474,7 @@ async function deployCommand(opts) {
1241
1474
  }
1242
1475
  if (workloadType === "service") {
1243
1476
  try {
1244
- await fs3.access(path3.join(pkg.sourceDir, "Dockerfile"));
1477
+ await fs4.access(path4.join(pkg.sourceDir, "Dockerfile"));
1245
1478
  } catch {
1246
1479
  throw new Error(
1247
1480
  'workload type "service" (also called Deployment) requires a Dockerfile at the project root.'
@@ -1249,26 +1482,11 @@ async function deployCommand(opts) {
1249
1482
  }
1250
1483
  }
1251
1484
  if (runtime === "node") {
1252
- try {
1253
- const raw = await fs3.readFile(
1254
- path3.join(pkg.sourceDir, "package.json"),
1255
- "utf8"
1256
- );
1257
- const npm = JSON.parse(raw);
1258
- if (!npm.scripts?.start?.trim()) {
1259
- throw new Error(
1260
- 'Node Coderunner packages require package.json "scripts.start" (runtime CMD is npm start).'
1261
- );
1262
- }
1263
- } catch (err) {
1264
- if (err instanceof Error && err.message.includes("scripts.start")) {
1265
- throw err;
1266
- }
1267
- }
1485
+ await preflightNodePackage(pkg.sourceDir);
1268
1486
  }
1269
1487
  let coderunnerId = opts.coderunner;
1270
1488
  if (!coderunnerId) {
1271
- const name = opts.name ?? manifest.name ?? (opts.git ? nameFromGitUrl(opts.git) : path3.basename(path3.resolve(opts.path ?? process.cwd())));
1489
+ const name = opts.name ?? manifest.name ?? (opts.git ? nameFromGitUrl(opts.git) : path4.basename(path4.resolve(opts.path ?? process.cwd())));
1272
1490
  const slug = manifest.slug ?? slugify(name);
1273
1491
  const existing = await listCoderunners(cfg.apiUrl, ws);
1274
1492
  const match = existing.find((c) => c.slug === slug || c.name === name);
@@ -1317,16 +1535,38 @@ async function deployCommand(opts) {
1317
1535
  break;
1318
1536
  }
1319
1537
  if (status === 4) {
1320
- throw new Error(
1321
- `Build failed for version ${uploaded.version_id} (status=4).`
1322
- );
1538
+ return await failDeploy({
1539
+ apiUrl: cfg.apiUrl,
1540
+ workspaceId: ws,
1541
+ headline: `Build failed for version ${uploaded.version_id} (${statusLabel(4)}).`,
1542
+ coderunnerId,
1543
+ versionId: uploaded.version_id,
1544
+ status: 4
1545
+ });
1323
1546
  }
1324
1547
  process.stderr.write(".");
1325
1548
  await new Promise((r) => setTimeout(r, VERSION_POLL_INTERVAL_MS));
1326
1549
  }
1327
1550
  process.stderr.write("\n");
1328
1551
  if (!built) {
1329
- throw new Error("Timed out waiting for the build to complete.");
1552
+ let lastStatus = null;
1553
+ try {
1554
+ lastStatus = (await getVersion(
1555
+ cfg.apiUrl,
1556
+ ws,
1557
+ coderunnerId,
1558
+ uploaded.version_id
1559
+ )).status ?? null;
1560
+ } catch {
1561
+ }
1562
+ return await failDeploy({
1563
+ apiUrl: cfg.apiUrl,
1564
+ workspaceId: ws,
1565
+ headline: "Timed out waiting for the build to complete.",
1566
+ coderunnerId,
1567
+ versionId: uploaded.version_id,
1568
+ status: lastStatus
1569
+ });
1330
1570
  }
1331
1571
  log("Build completed.");
1332
1572
  if (opts.skipDeploy) {
@@ -1358,16 +1598,38 @@ async function deployCommand(opts) {
1358
1598
  break;
1359
1599
  }
1360
1600
  if (dStatus === 4) {
1361
- throw new Error(
1362
- `Deployment failed for coderunner ${coderunnerId} (status=4).`
1363
- );
1601
+ return await failDeploy({
1602
+ apiUrl: cfg.apiUrl,
1603
+ workspaceId: ws,
1604
+ headline: `Deployment failed for coderunner ${coderunnerId} (${statusLabel(4)}).`,
1605
+ coderunnerId,
1606
+ versionId: uploaded.version_id,
1607
+ deploymentId: result.deployment_id ?? deployment?.id,
1608
+ status: 4
1609
+ });
1364
1610
  }
1365
1611
  process.stderr.write(".");
1366
1612
  await new Promise((r) => setTimeout(r, DEPLOY_POLL_INTERVAL_MS));
1367
1613
  }
1368
1614
  process.stderr.write("\n");
1369
1615
  if (!deployOk) {
1370
- throw new Error("Timed out waiting for the deployment to complete.");
1616
+ let lastStatus = null;
1617
+ let deploymentId = result.deployment_id;
1618
+ try {
1619
+ const latest = await getDeployment(cfg.apiUrl, ws, coderunnerId);
1620
+ lastStatus = latest?.status ?? null;
1621
+ deploymentId = latest?.id ?? deploymentId;
1622
+ } catch {
1623
+ }
1624
+ return await failDeploy({
1625
+ apiUrl: cfg.apiUrl,
1626
+ workspaceId: ws,
1627
+ headline: "Timed out waiting for the deployment to complete.",
1628
+ coderunnerId,
1629
+ versionId: uploaded.version_id,
1630
+ deploymentId,
1631
+ status: lastStatus
1632
+ });
1371
1633
  }
1372
1634
  log("Deployment completed.");
1373
1635
  process.stdout.write(`
@@ -1388,21 +1650,13 @@ Deployed coderunner ${coderunnerId}
1388
1650
  }
1389
1651
  } finally {
1390
1652
  if (pkg.cleanup) {
1391
- await fs3.rm(pkg.cleanup, { recursive: true, force: true }).catch(() => {
1653
+ await fs4.rm(pkg.cleanup, { recursive: true, force: true }).catch(() => {
1392
1654
  });
1393
1655
  }
1394
1656
  }
1395
1657
  }
1396
1658
 
1397
1659
  // src/commands/run.ts
1398
- async function resolveCoderunnerId(apiUrl, workspaceId, idOrSlug) {
1399
- const list = await listCoderunners(apiUrl, workspaceId);
1400
- const match = list.find((c) => c.id === idOrSlug || c.slug === idOrSlug);
1401
- if (!match) {
1402
- throw new Error(`Coderunner not found: ${idOrSlug}`);
1403
- }
1404
- return match.id;
1405
- }
1406
1660
  async function runCommand(opts) {
1407
1661
  const cfg = await resolveConfig({ workspaceId: opts.workspace });
1408
1662
  if (!cfg.workspaceId) {
@@ -1436,14 +1690,6 @@ async function runCommand(opts) {
1436
1690
  }
1437
1691
 
1438
1692
  // src/commands/status.ts
1439
- async function resolveCoderunnerId2(apiUrl, workspaceId, idOrSlug) {
1440
- const list = await listCoderunners(apiUrl, workspaceId);
1441
- const match = list.find((c) => c.id === idOrSlug || c.slug === idOrSlug);
1442
- if (!match) {
1443
- throw new Error(`Coderunner not found: ${idOrSlug}`);
1444
- }
1445
- return match.id;
1446
- }
1447
1693
  async function statusCommand(opts) {
1448
1694
  const cfg = await resolveConfig({ workspaceId: opts.workspace });
1449
1695
  if (!cfg.workspaceId) {
@@ -1451,7 +1697,7 @@ async function statusCommand(opts) {
1451
1697
  "No workspace selected. Run `gf workspaces use <id>` or pass --workspace <id>."
1452
1698
  );
1453
1699
  }
1454
- const coderunnerId = await resolveCoderunnerId2(
1700
+ const coderunnerId = await resolveCoderunnerId(
1455
1701
  cfg.apiUrl,
1456
1702
  cfg.workspaceId,
1457
1703
  opts.coderunner
@@ -1634,10 +1880,10 @@ async function secretsRmCommand(key, opts) {
1634
1880
  }
1635
1881
 
1636
1882
  // src/commands/files.ts
1637
- import { promises as fs4 } from "fs";
1638
- import path4 from "path";
1883
+ import { promises as fs5 } from "fs";
1884
+ import path5 from "path";
1639
1885
  function guessContentType(filename) {
1640
- const ext = path4.extname(filename).toLowerCase();
1886
+ const ext = path5.extname(filename).toLowerCase();
1641
1887
  const types = {
1642
1888
  ".json": "application/json",
1643
1889
  ".txt": "text/plain",
@@ -1672,9 +1918,9 @@ async function filesListCommand(opts) {
1672
1918
  }
1673
1919
  async function filesUploadCommand(filePath, opts) {
1674
1920
  const cfg = await requireWorkspace(opts.workspace);
1675
- const resolved = path4.resolve(filePath);
1676
- const buf = await fs4.readFile(resolved);
1677
- const name = opts.name ?? path4.basename(resolved);
1921
+ const resolved = path5.resolve(filePath);
1922
+ const buf = await fs5.readFile(resolved);
1923
+ const name = opts.name ?? path5.basename(resolved);
1678
1924
  const contentType = guessContentType(name);
1679
1925
  const init = await initFileUpload(cfg.apiUrl, cfg.workspaceId, {
1680
1926
  name,
@@ -1711,8 +1957,8 @@ async function filesDownloadCommand(fileId, opts) {
1711
1957
  throw new Error(`Download failed: ${res.status} ${res.statusText}`);
1712
1958
  }
1713
1959
  const buf = Buffer.from(await res.arrayBuffer());
1714
- const dest = path4.resolve(opts.out ?? fileId);
1715
- await fs4.writeFile(dest, buf);
1960
+ const dest = path5.resolve(opts.out ?? fileId);
1961
+ await fs5.writeFile(dest, buf);
1716
1962
  process.stdout.write(`Wrote ${dest}
1717
1963
  `);
1718
1964
  }
@@ -1724,8 +1970,8 @@ async function filesRmCommand(fileId, opts) {
1724
1970
  }
1725
1971
 
1726
1972
  // src/commands/apps.ts
1727
- import { promises as fs5 } from "fs";
1728
- import path5 from "path";
1973
+ import { promises as fs6 } from "fs";
1974
+ import path6 from "path";
1729
1975
 
1730
1976
  // src/release-zip.ts
1731
1977
  import AdmZip2 from "adm-zip";
@@ -1765,8 +2011,8 @@ function flattenFederatedBundlePlan(relpaths) {
1765
2011
  const promoteDirs = /* @__PURE__ */ new Set();
1766
2012
  const entryDir = parentDir(entry);
1767
2013
  if (entryDir) promoteDirs.add(entryDir);
1768
- for (const path7 of pathset) {
1769
- const parent = parentDir(path7);
2014
+ for (const path8 of pathset) {
2015
+ const parent = parentDir(path8);
1770
2016
  if (parent === "assets" || parent.endsWith("/assets") && parent.split("/").length - 1 < 2) {
1771
2017
  promoteDirs.add(parent);
1772
2018
  }
@@ -1810,8 +2056,80 @@ function flattenFederatedReleaseZip(bytes) {
1810
2056
  }
1811
2057
  return zip.toBuffer();
1812
2058
  }
1813
- function federatedReleaseZipHint() {
1814
- return "Run `npm run release` in a kit from `gf apps init --slug <portal-slug>` (do not zip Vite dist/ or dist/assets/ by hand).";
2059
+ function federatedReleaseZipHint(slug) {
2060
+ return `Run \`npm run release\` in a kit from \`gf apps init\`, then publish that zip (do not zip Vite dist/ or upload only remoteEntry.js). ${initThenPublishHint(slug)}`;
2061
+ }
2062
+ function federationRemoteName(appId) {
2063
+ const sanitized = appId.replace(/[^a-zA-Z0-9_$]/g, "_");
2064
+ return sanitized.length > 0 ? sanitized : "federatedApp";
2065
+ }
2066
+ function initThenPublishHint(slug) {
2067
+ const s = (slug ?? "").trim() || "<portal-slug>";
2068
+ return `gf apps init --slug ${s} && cd ${s} && npm install && npm run release && gf apps publish --path release.zip`;
2069
+ }
2070
+ function extractReferencedSiblings(remoteEntrySource) {
2071
+ const names = /* @__PURE__ */ new Set();
2072
+ for (const match of remoteEntrySource.matchAll(
2073
+ /["']\.\/([^"'/]+\.(?:js|mjs|cjs|css))["']/g
2074
+ )) {
2075
+ if (match[1] !== REMOTE_ENTRY) names.add(match[1]);
2076
+ }
2077
+ for (const match of remoteEntrySource.matchAll(
2078
+ /["']([A-Za-z0-9._-]+\.css)["']/g
2079
+ )) {
2080
+ names.add(match[1]);
2081
+ }
2082
+ return [...names];
2083
+ }
2084
+ function extractFederationScope(remoteEntrySource) {
2085
+ return remoteEntrySource.match(/css__([A-Za-z0-9_-]+)__/)?.[1];
2086
+ }
2087
+ function zipRootNames(bytes) {
2088
+ const zip = new AdmZip2(bytes);
2089
+ return zip.getEntries().filter((entry) => !entry.isDirectory).map((entry) => normalizeZipEntryName(entry.entryName));
2090
+ }
2091
+ function readZipFile(bytes, rel) {
2092
+ const zip = new AdmZip2(bytes);
2093
+ const entry = zip.getEntries().find((item) => {
2094
+ if (item.isDirectory) return false;
2095
+ return normalizeZipEntryName(item.entryName) === rel;
2096
+ });
2097
+ return entry ? entry.getData().toString("utf8") : void 0;
2098
+ }
2099
+ function assertFederatedReleaseZip(bytes, slug) {
2100
+ const hint = initThenPublishHint(slug);
2101
+ const names = zipRootNames(bytes);
2102
+ const present = new Set(names.map((name) => basename(name)));
2103
+ const entryRel = chooseRemoteEntry(names);
2104
+ if (!entryRel) {
2105
+ throw new Error(`Zip has no remoteEntry.js. ${hint}`);
2106
+ }
2107
+ const source = readZipFile(bytes, entryRel);
2108
+ if (!source) {
2109
+ throw new Error(`Zip has no remoteEntry.js. ${hint}`);
2110
+ }
2111
+ const siblings = extractReferencedSiblings(source);
2112
+ const missing = siblings.filter((name) => !present.has(name));
2113
+ const jsOrCssAtRoot = names.filter((name) => {
2114
+ if (name.includes("/")) return false;
2115
+ const lower = name.toLowerCase();
2116
+ return lower.endsWith(".js") || lower.endsWith(".mjs") || lower.endsWith(".css");
2117
+ });
2118
+ if (missing.length > 0 || siblings.length === 0 && jsOrCssAtRoot.length < 2) {
2119
+ const detail = missing.length > 0 ? `Missing next to remoteEntry.js: ${missing.join(", ")}.` : "Zip is only remoteEntry.js (Vite chunks were never packed).";
2120
+ throw new Error(
2121
+ `${detail} Do not zip dist/ or upload a lone remoteEntry.js. ${hint}`
2122
+ );
2123
+ }
2124
+ const scope = extractFederationScope(source);
2125
+ if (slug && scope) {
2126
+ const expected = federationRemoteName(slug);
2127
+ if (scope !== expected) {
2128
+ throw new Error(
2129
+ `Federation name is "${scope}" but Shell looks up "${expected}" (from slug ${slug}). Use the starter-kit vite.config (federationRemoteName). ${hint}`
2130
+ );
2131
+ }
2132
+ }
1815
2133
  }
1816
2134
 
1817
2135
  // src/commands/apps.ts
@@ -1825,7 +2143,7 @@ function formatApp(app) {
1825
2143
  return `${app.id} ${app.name ?? app.slug ?? ""} ${app.app_kind ?? ""} [${app.status ?? ""}]`;
1826
2144
  }
1827
2145
  async function readJsonObject(filePath) {
1828
- const raw = await fs5.readFile(filePath, "utf8");
2146
+ const raw = await fs6.readFile(filePath, "utf8");
1829
2147
  const parsed = JSON.parse(raw);
1830
2148
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1831
2149
  throw new Error(`${filePath} must contain a JSON object`);
@@ -1834,13 +2152,13 @@ async function readJsonObject(filePath) {
1834
2152
  }
1835
2153
  async function maybeReadManifest(filePath) {
1836
2154
  if (!filePath) return void 0;
1837
- return readJsonObject(path5.resolve(filePath));
2155
+ return readJsonObject(path6.resolve(filePath));
1838
2156
  }
1839
2157
  async function findSiblingManifest(bundlePath) {
1840
- const dir = await isDirectory(bundlePath) ? bundlePath : path5.dirname(bundlePath);
1841
- const candidate = path5.join(dir, "groundfloor.manifest.json");
2158
+ const dir = await isDirectory(bundlePath) ? bundlePath : path6.dirname(bundlePath);
2159
+ const candidate = path6.join(dir, "groundfloor.manifest.json");
1842
2160
  try {
1843
- await fs5.access(candidate);
2161
+ await fs6.access(candidate);
1844
2162
  } catch {
1845
2163
  return void 0;
1846
2164
  }
@@ -1854,10 +2172,10 @@ function zipHasRemoteEntry(bytes) {
1854
2172
  return name === REMOTE_ENTRY || name.endsWith(`/${REMOTE_ENTRY}`);
1855
2173
  });
1856
2174
  }
1857
- function prepareReleaseZip(bytes) {
2175
+ function prepareReleaseZip(bytes, slug) {
1858
2176
  if (!zipHasRemoteEntry(bytes)) {
1859
2177
  throw new Error(
1860
- `Zip has no remoteEntry.js. ${federatedReleaseZipHint()}`
2178
+ `Zip has no remoteEntry.js. ${initThenPublishHint(slug)}`
1861
2179
  );
1862
2180
  }
1863
2181
  const flattened = flattenFederatedReleaseZip(bytes);
@@ -1867,15 +2185,16 @@ function prepareReleaseZip(bytes) {
1867
2185
  if (Object.keys(plan).length > 0) {
1868
2186
  process.stderr.write(
1869
2187
  `Note: flattened Vite dist/assets layout so chunks sit next to remoteEntry.js.
1870
- Prefer: ${federatedReleaseZipHint()}
2188
+ Prefer: ${federatedReleaseZipHint(slug)}
1871
2189
  `
1872
2190
  );
1873
2191
  }
2192
+ assertFederatedReleaseZip(flattened, slug);
1874
2193
  return flattened;
1875
2194
  }
1876
2195
  async function isDirectory(filePath) {
1877
2196
  try {
1878
- return (await fs5.stat(filePath)).isDirectory();
2197
+ return (await fs6.stat(filePath)).isDirectory();
1879
2198
  } catch {
1880
2199
  return false;
1881
2200
  }
@@ -1969,7 +2288,8 @@ async function appsCreateCommand(opts) {
1969
2288
  } catch (err) {
1970
2289
  if (err instanceof CpError && err.status === 409) {
1971
2290
  throw new Error(
1972
- `App slug ${slug} already exists. Use \`gf apps publish ${slug}\` to ship a build.`
2291
+ `App slug ${slug} already exists. Init the kit if you do not have it, then publish:
2292
+ ${initThenPublishHint(slug)}`
1973
2293
  );
1974
2294
  }
1975
2295
  throw err;
@@ -1994,14 +2314,14 @@ Do not scaffold src/App.tsx from scratch \u2014 BrowserRouter in App.tsx crashes
1994
2314
  }
1995
2315
  async function appsPublishCommand(appRef, opts) {
1996
2316
  const cfg = await requireWorkspace(opts.workspace);
1997
- const inputPath = path5.resolve(opts.path ?? "release.zip");
1998
- const bundlePath = await isDirectory(inputPath) ? path5.join(inputPath, "release.zip") : inputPath;
2317
+ const inputPath = path6.resolve(opts.path ?? "release.zip");
2318
+ const bundlePath = await isDirectory(inputPath) ? path6.join(inputPath, "release.zip") : inputPath;
1999
2319
  let bytes;
2000
2320
  try {
2001
- bytes = await fs5.readFile(bundlePath);
2321
+ bytes = await fs6.readFile(bundlePath);
2002
2322
  } catch {
2003
2323
  throw new Error(
2004
- `No bundle at ${bundlePath}. Run \`npm run release\` in the starter-kit, then pass --path release.zip.`
2324
+ `No bundle at ${bundlePath}. ${initThenPublishHint()}`
2005
2325
  );
2006
2326
  }
2007
2327
  if (bytes.byteLength === 0) {
@@ -2025,13 +2345,16 @@ async function appsPublishCommand(appRef, opts) {
2025
2345
  `Releases are only for shell_federated apps (this app is ${app.app_kind}). Use \`gf deploy\` for coderunner workloads.`
2026
2346
  );
2027
2347
  }
2028
- const ext = path5.extname(bundlePath).toLowerCase();
2348
+ const ext = path6.extname(bundlePath).toLowerCase();
2029
2349
  const isZip = ext === ".zip";
2030
- if (isZip) {
2031
- bytes = prepareReleaseZip(bytes);
2350
+ if (!isZip) {
2351
+ throw new Error(
2352
+ `Publish release.zip from \`npm run release\`, not a lone ${path6.basename(bundlePath)}. ` + initThenPublishHint(app.slug)
2353
+ );
2032
2354
  }
2033
- const bundle = isZip ? "zip" : "remote_entry";
2034
- const contentType = isZip ? "application/zip" : "application/javascript";
2355
+ bytes = prepareReleaseZip(bytes, app.slug);
2356
+ const bundle = "zip";
2357
+ const contentType = "application/zip";
2035
2358
  if (opts.syncManifest !== false && sibling) {
2036
2359
  await patchApp(cfg.apiUrl, cfg.workspaceId, app.id, {
2037
2360
  manifest: sibling.manifest
@@ -2066,9 +2389,9 @@ async function appsPublishCommand(appRef, opts) {
2066
2389
  }
2067
2390
 
2068
2391
  // src/commands/apps-init.ts
2069
- import { promises as fs6 } from "fs";
2392
+ import { promises as fs7 } from "fs";
2070
2393
  import os3 from "os";
2071
- import path6 from "path";
2394
+ import path7 from "path";
2072
2395
  import AdmZip4 from "adm-zip";
2073
2396
  var MAX_KIT_BYTES = 8 * 1024 * 1024;
2074
2397
  function slugify3(input) {
@@ -2080,7 +2403,7 @@ function titleFromSlug(slug) {
2080
2403
  }
2081
2404
  async function pathExists(filePath) {
2082
2405
  try {
2083
- await fs6.access(filePath);
2406
+ await fs7.access(filePath);
2084
2407
  return true;
2085
2408
  } catch {
2086
2409
  return false;
@@ -2088,7 +2411,7 @@ async function pathExists(filePath) {
2088
2411
  }
2089
2412
  async function isEmptyDir(dir) {
2090
2413
  try {
2091
- const entries = await fs6.readdir(dir);
2414
+ const entries = await fs7.readdir(dir);
2092
2415
  return entries.length === 0;
2093
2416
  } catch (err) {
2094
2417
  if (err.code === "ENOENT") return true;
@@ -2097,17 +2420,17 @@ async function isEmptyDir(dir) {
2097
2420
  }
2098
2421
  async function findKitRoot(extracted) {
2099
2422
  const candidates = [
2100
- path6.join(extracted, "shell-starter-kit"),
2423
+ path7.join(extracted, "shell-starter-kit"),
2101
2424
  extracted
2102
2425
  ];
2103
- const entries = await fs6.readdir(extracted, { withFileTypes: true });
2426
+ const entries = await fs7.readdir(extracted, { withFileTypes: true });
2104
2427
  for (const entry of entries) {
2105
2428
  if (entry.isDirectory()) {
2106
- candidates.push(path6.join(extracted, entry.name));
2429
+ candidates.push(path7.join(extracted, entry.name));
2107
2430
  }
2108
2431
  }
2109
2432
  for (const candidate of candidates) {
2110
- if (await pathExists(path6.join(candidate, "groundfloor.manifest.json"))) {
2433
+ if (await pathExists(path7.join(candidate, "groundfloor.manifest.json"))) {
2111
2434
  return candidate;
2112
2435
  }
2113
2436
  }
@@ -2189,7 +2512,7 @@ async function appsInitCommand(opts) {
2189
2512
  `);
2190
2513
  }
2191
2514
  const name = opts.name?.trim() || titleFromSlug(slug);
2192
- const dest = path6.resolve(opts.dir?.trim() || path6.join(process.cwd(), slug));
2515
+ const dest = path7.resolve(opts.dir?.trim() || path7.join(process.cwd(), slug));
2193
2516
  const cfg = await resolveConfig();
2194
2517
  const cell = cfg.environment ?? "production";
2195
2518
  const kitUrl = opts.kitUrl?.trim() || starterKitZipUrl(cell);
@@ -2199,26 +2522,26 @@ async function appsInitCommand(opts) {
2199
2522
  );
2200
2523
  }
2201
2524
  if (opts.force && await pathExists(dest)) {
2202
- await fs6.rm(dest, { recursive: true, force: true });
2525
+ await fs7.rm(dest, { recursive: true, force: true });
2203
2526
  }
2204
2527
  const bytes = await downloadKit(kitUrl);
2205
2528
  const zip = new AdmZip4(bytes);
2206
2529
  assertSafeZipEntries(zip);
2207
- const tmp = await fs6.mkdtemp(path6.join(os3.tmpdir(), "gf-starter-kit-"));
2530
+ const tmp = await fs7.mkdtemp(path7.join(os3.tmpdir(), "gf-starter-kit-"));
2208
2531
  try {
2209
2532
  zip.extractAllTo(tmp, true);
2210
2533
  const kitRoot = await findKitRoot(tmp);
2211
- await fs6.mkdir(dest, { recursive: true });
2212
- await fs6.cp(kitRoot, dest, { recursive: true, force: true });
2534
+ await fs7.mkdir(dest, { recursive: true });
2535
+ await fs7.cp(kitRoot, dest, { recursive: true, force: true });
2213
2536
  } finally {
2214
- await fs6.rm(tmp, { recursive: true, force: true });
2215
- }
2216
- const identityPath = path6.join(dest, "src", "appIdentity.ts");
2217
- const manifestPath = path6.join(dest, "groundfloor.manifest.json");
2218
- const identity = await fs6.readFile(identityPath, "utf8");
2219
- await fs6.writeFile(identityPath, stampAppIdentity(identity, slug), "utf8");
2220
- const manifest = await fs6.readFile(manifestPath, "utf8");
2221
- await fs6.writeFile(manifestPath, stampManifest(manifest, slug, name), "utf8");
2537
+ await fs7.rm(tmp, { recursive: true, force: true });
2538
+ }
2539
+ const identityPath = path7.join(dest, "src", "appIdentity.ts");
2540
+ const manifestPath = path7.join(dest, "groundfloor.manifest.json");
2541
+ const identity = await fs7.readFile(identityPath, "utf8");
2542
+ await fs7.writeFile(identityPath, stampAppIdentity(identity, slug), "utf8");
2543
+ const manifest = await fs7.readFile(manifestPath, "utf8");
2544
+ await fs7.writeFile(manifestPath, stampManifest(manifest, slug, name), "utf8");
2222
2545
  const result = {
2223
2546
  dir: dest,
2224
2547
  slug,
@@ -2301,7 +2624,7 @@ function run(fn) {
2301
2624
  var program = new Command();
2302
2625
  program.name("gf").description(
2303
2626
  "Groundfloor CLI \u2014 sign in, deploy coderunners, publish Shell apps, and manage workspace resources."
2304
- ).version("0.1.1");
2627
+ ).version("0.1.5");
2305
2628
  program.command("login").description(
2306
2629
  "Log in via the browser (production by default; --dev / --stage for other cells)"
2307
2630
  ).option("--dev", "Sign in to Groundfloor dev").option("--stage", "Sign in to Groundfloor stage").option(
@@ -2326,6 +2649,18 @@ workspaces.command("use <workspaceId>").description("Set the default workspace")
2326
2649
  var coderunner = program.command("coderunner").alias("cr").description("Coderunner commands");
2327
2650
  coderunner.command("ls").alias("list").description("List coderunners in the default (or --workspace) workspace").option("-w, --workspace <id>", "Workspace id override").option("--json", "Output raw JSON").action((opts) => run(() => coderunnerListCommand(opts)));
2328
2651
  coderunner.command("status").description("Show coderunner status + current deployment JSON").requiredOption("-c, --coderunner <id|slug>", "Coderunner id or slug").option("-w, --workspace <id>", "Workspace id override").option("--json", "Output raw JSON").action((opts) => run(() => statusCommand(opts)));
2652
+ coderunner.command("logs").description("Show Process Log lines for a coderunner (stdout/stderr)").requiredOption("-c, --coderunner <id|slug>", "Coderunner id or slug").option("-w, --workspace <id>", "Workspace id override").option("--limit <n>", "Max lines (default 50)").option("--since-seconds <n>", "Lookback window in seconds (default 3600)").option("--search <text>", "Substring filter").option("--json", "Output raw JSON").action(
2653
+ (opts) => run(
2654
+ () => logsCommand({
2655
+ workspace: opts.workspace,
2656
+ coderunner: opts.coderunner,
2657
+ limit: opts.limit,
2658
+ sinceSeconds: opts.sinceSeconds,
2659
+ search: opts.search,
2660
+ json: opts.json
2661
+ })
2662
+ )
2663
+ );
2329
2664
  coderunner.command("run").description("Invoke a deployed function/job (smoke run)").requiredOption("-c, --coderunner <id|slug>", "Coderunner id or slug").option("-w, --workspace <id>", "Workspace id override").option("--payload <json>", "JSON payload object", "{}").option("--json", "Output raw JSON").action(
2330
2665
  (opts) => run(
2331
2666
  () => runCommand({
@@ -2398,10 +2733,10 @@ apps.command("create").description("Register a Shell federated or standalone pro
2398
2733
  "Path to groundfloor.manifest.json (Shell apps)"
2399
2734
  ).option("--json", "Output raw JSON").action((opts) => run(() => appsCreateCommand(opts)));
2400
2735
  apps.command("publish [appId]").description(
2401
- "Upload release.zip (or remoteEntry.js) and publish a Shell app"
2736
+ "Upload release.zip from npm run release (not a lone remoteEntry.js)"
2402
2737
  ).option("-w, --workspace <id>", "Workspace id override").option("--app <id|slug>", "App id or slug (else manifest appId)").option(
2403
2738
  "-p, --path <file>",
2404
- "release.zip, remoteEntry.js, or a folder containing release.zip",
2739
+ "release.zip from npm run release (or a folder containing it)",
2405
2740
  "release.zip"
2406
2741
  ).option("-m, --label <text>", "Optional release label").option(
2407
2742
  "--no-sync-manifest",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groundfloorcloud/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Groundfloor gf CLI — sign in, deploy coderunners, publish Shell apps, and manage secrets, files, and Dataplane.",
5
5
  "type": "module",
6
6
  "bin": {