@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.
- package/README.md +11 -7
- package/dist/index.js +451 -116
- 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
|
|
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`
|
|
156
|
-
Vite `dist/assets/` layout
|
|
157
|
-
|
|
158
|
-
`
|
|
159
|
-
`
|
|
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,
|
|
536
|
+
async function cpGet(apiUrl, path8) {
|
|
537
537
|
const token = await getValidAccessToken();
|
|
538
|
-
const res = await fetch(`${apiUrl}${
|
|
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,
|
|
549
|
+
async function cpPostJson(apiUrl, path8, body) {
|
|
550
550
|
const token = await getValidAccessToken();
|
|
551
|
-
const res = await fetch(`${apiUrl}${
|
|
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,
|
|
565
|
+
async function cpDelete(apiUrl, path8) {
|
|
566
566
|
const token = await getValidAccessToken();
|
|
567
|
-
const res = await fetch(`${apiUrl}${
|
|
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,
|
|
575
|
+
async function cpPutJson(apiUrl, path8, body) {
|
|
576
576
|
const token = await getValidAccessToken();
|
|
577
|
-
const res = await fetch(`${apiUrl}${
|
|
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,
|
|
591
|
+
async function cpPatchJson(apiUrl, path8, body) {
|
|
592
592
|
const token = await getValidAccessToken();
|
|
593
|
-
const res = await fetch(`${apiUrl}${
|
|
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,
|
|
607
|
+
async function cpPutBytes(apiUrl, path8, body, contentType) {
|
|
608
608
|
const token = await getValidAccessToken();
|
|
609
|
-
const res = await fetch(`${apiUrl}${
|
|
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
|
|
1008
|
-
import
|
|
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 ${
|
|
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
|
|
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
|
-
|
|
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) :
|
|
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
|
-
|
|
1321
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1362
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
1638
|
-
import
|
|
1883
|
+
import { promises as fs5 } from "fs";
|
|
1884
|
+
import path5 from "path";
|
|
1639
1885
|
function guessContentType(filename) {
|
|
1640
|
-
const ext =
|
|
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 =
|
|
1676
|
-
const buf = await
|
|
1677
|
-
const name = opts.name ??
|
|
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 =
|
|
1715
|
-
await
|
|
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
|
|
1728
|
-
import
|
|
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
|
|
1769
|
-
const parent = parentDir(
|
|
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
|
|
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
|
|
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(
|
|
2155
|
+
return readJsonObject(path6.resolve(filePath));
|
|
1838
2156
|
}
|
|
1839
2157
|
async function findSiblingManifest(bundlePath) {
|
|
1840
|
-
const dir = await isDirectory(bundlePath) ? bundlePath :
|
|
1841
|
-
const candidate =
|
|
2158
|
+
const dir = await isDirectory(bundlePath) ? bundlePath : path6.dirname(bundlePath);
|
|
2159
|
+
const candidate = path6.join(dir, "groundfloor.manifest.json");
|
|
1842
2160
|
try {
|
|
1843
|
-
await
|
|
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. ${
|
|
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
|
|
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.
|
|
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 =
|
|
1998
|
-
const bundlePath = await isDirectory(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
|
|
2321
|
+
bytes = await fs6.readFile(bundlePath);
|
|
2002
2322
|
} catch {
|
|
2003
2323
|
throw new Error(
|
|
2004
|
-
`No bundle at ${bundlePath}.
|
|
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 =
|
|
2348
|
+
const ext = path6.extname(bundlePath).toLowerCase();
|
|
2029
2349
|
const isZip = ext === ".zip";
|
|
2030
|
-
if (isZip) {
|
|
2031
|
-
|
|
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
|
-
|
|
2034
|
-
const
|
|
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
|
|
2392
|
+
import { promises as fs7 } from "fs";
|
|
2070
2393
|
import os3 from "os";
|
|
2071
|
-
import
|
|
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
|
|
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
|
|
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
|
-
|
|
2423
|
+
path7.join(extracted, "shell-starter-kit"),
|
|
2101
2424
|
extracted
|
|
2102
2425
|
];
|
|
2103
|
-
const entries = await
|
|
2426
|
+
const entries = await fs7.readdir(extracted, { withFileTypes: true });
|
|
2104
2427
|
for (const entry of entries) {
|
|
2105
2428
|
if (entry.isDirectory()) {
|
|
2106
|
-
candidates.push(
|
|
2429
|
+
candidates.push(path7.join(extracted, entry.name));
|
|
2107
2430
|
}
|
|
2108
2431
|
}
|
|
2109
2432
|
for (const candidate of candidates) {
|
|
2110
|
-
if (await pathExists(
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
2212
|
-
await
|
|
2534
|
+
await fs7.mkdir(dest, { recursive: true });
|
|
2535
|
+
await fs7.cp(kitRoot, dest, { recursive: true, force: true });
|
|
2213
2536
|
} finally {
|
|
2214
|
-
await
|
|
2215
|
-
}
|
|
2216
|
-
const identityPath =
|
|
2217
|
-
const manifestPath =
|
|
2218
|
-
const identity = await
|
|
2219
|
-
await
|
|
2220
|
-
const manifest = await
|
|
2221
|
-
await
|
|
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.
|
|
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
|
|
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
|
|
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