@groundfloorcloud/cli 0.1.2 → 0.1.4
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 +36 -9
- package/dist/index.js +118 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,15 +54,28 @@ curl -H "Authorization: Bearer $(gf token)" \
|
|
|
54
54
|
create if needed, upload a version, wait for the build, deploy, poll until
|
|
55
55
|
complete, and print IDs and the URL.
|
|
56
56
|
|
|
57
|
-
Do not create an App just to run a function, job, schedule, or service
|
|
58
|
-
Use `gf deploy`. Bind `--app-id` only when the coderunner should
|
|
59
|
-
of an existing product App.
|
|
57
|
+
Do not create an App just to run a function, job, schedule, or service
|
|
58
|
+
(Deployment). Use `gf deploy`. Bind `--app-id` only when the coderunner should
|
|
59
|
+
be a helper of an existing product App.
|
|
60
|
+
|
|
61
|
+
**Before the first create:** enable workspace Authentication (Administer →
|
|
62
|
+
Authentication, mode `groundfloor` or `external`) until the realm is active.
|
|
63
|
+
|
|
64
|
+
### Workload types
|
|
65
|
+
|
|
66
|
+
| `--workload-type` | Also called | Package notes |
|
|
67
|
+
|-------------------|-------------|---------------|
|
|
68
|
+
| `function` | Function | HTTP on `PORT`/8080. Node needs `scripts.start`. |
|
|
69
|
+
| `job` | Background job | Run-to-completion. Then `gf coderunner run`. |
|
|
70
|
+
| `schedule` | Scheduled job | Cron. No public URL. |
|
|
71
|
+
| `service` | **Deployment** | Long-running HTTP. **Root `Dockerfile` is required.** |
|
|
60
72
|
|
|
61
73
|
```bash
|
|
62
74
|
gf deploy
|
|
63
75
|
gf deploy --git https://github.com/me/my-fn.git --ref main
|
|
64
76
|
gf deploy --name my-fn --runtime python --cpu 250m --memory 256Mi \
|
|
65
77
|
-e API_KEY=secret
|
|
78
|
+
gf deploy --workload-type service
|
|
66
79
|
gf deploy --app-id <app-uuid>
|
|
67
80
|
```
|
|
68
81
|
|
|
@@ -75,6 +88,19 @@ How the target is chosen:
|
|
|
75
88
|
|
|
76
89
|
Runtime is auto-detected when omitted (`package.json` → node,
|
|
77
90
|
`requirements.txt` / `pyproject.toml` → python), defaulting to python.
|
|
91
|
+
Node images run `npm start` — add `"scripts": { "start": "node index.js" }`.
|
|
92
|
+
|
|
93
|
+
### Local with live data
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
eval "$(gf env)"
|
|
97
|
+
export PORT=8080
|
|
98
|
+
npm start
|
|
99
|
+
curl -sS -H "Authorization: Bearer $(gf token)" \
|
|
100
|
+
"$CONTROLPLANE_URL/v1/workspaces/$GROUNDFLOOR_WORKSPACE_ID/vault/collections"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Then `gf deploy` the same tree. Do not zip `.env` files.
|
|
78
104
|
|
|
79
105
|
### `groundfloor.json`
|
|
80
106
|
|
|
@@ -98,7 +124,7 @@ Runtime is auto-detected when omitted (`package.json` → node,
|
|
|
98
124
|
| `--git <url>` / `--ref <ref>` | Clone and deploy a remote repo. |
|
|
99
125
|
| `--subdir <dir>` | Package a subdirectory of the source. |
|
|
100
126
|
| `--runtime <r>` | `python` \| `node` \| `dotnet-script`. |
|
|
101
|
-
| `--workload-type <t>` | `function` \| `service` \| `job` \| `schedule`. |
|
|
127
|
+
| `--workload-type <t>` | `function` \| `service` (Deployment) \| `job` \| `schedule`. Service requires a Dockerfile. |
|
|
102
128
|
| `--cpu` / `--memory` | Resource requests, e.g. `250m` / `256Mi`. |
|
|
103
129
|
| `-e, --env KEY=VALUE` | Deployment env var (repeatable). |
|
|
104
130
|
| `-m, --message <text>` | Version description. |
|
|
@@ -126,11 +152,12 @@ gf apps publish --path release.zip
|
|
|
126
152
|
(`/downloads/shell-starter-kit.zip` on the current cell) and stamps
|
|
127
153
|
`APP_ID` / `groundfloor.manifest.json` `appId` to the slug.
|
|
128
154
|
|
|
129
|
-
`gf apps publish` uploads `release.zip`
|
|
130
|
-
Vite `dist/assets/` layout
|
|
131
|
-
|
|
132
|
-
`
|
|
133
|
-
`
|
|
155
|
+
`gf apps publish` uploads **only** `release.zip` from `npm run release`.
|
|
156
|
+
It flattens a Vite `dist/assets/` layout when needed, then **rejects** a zip
|
|
157
|
+
that is only `remoteEntry.js` (missing `__federation_*.js` / CSS) or whose
|
|
158
|
+
federation `name` does not match `federationRemoteName(slug)`. A lone
|
|
159
|
+
`.js` file is also rejected. Pass an app id/slug, or set `appId` in
|
|
160
|
+
`groundfloor.manifest.json` next to the zip.
|
|
134
161
|
|
|
135
162
|
Wrap an existing **service** coderunner as a product:
|
|
136
163
|
|
package/dist/index.js
CHANGED
|
@@ -1239,6 +1239,33 @@ async function deployCommand(opts) {
|
|
|
1239
1239
|
`Invalid workload type "${workloadType}" (expected: ${[...WORKLOAD_TYPES].join(", ")}).`
|
|
1240
1240
|
);
|
|
1241
1241
|
}
|
|
1242
|
+
if (workloadType === "service") {
|
|
1243
|
+
try {
|
|
1244
|
+
await fs3.access(path3.join(pkg.sourceDir, "Dockerfile"));
|
|
1245
|
+
} catch {
|
|
1246
|
+
throw new Error(
|
|
1247
|
+
'workload type "service" (also called Deployment) requires a Dockerfile at the project root.'
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
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
|
+
}
|
|
1268
|
+
}
|
|
1242
1269
|
let coderunnerId = opts.coderunner;
|
|
1243
1270
|
if (!coderunnerId) {
|
|
1244
1271
|
const name = opts.name ?? manifest.name ?? (opts.git ? nameFromGitUrl(opts.git) : path3.basename(path3.resolve(opts.path ?? process.cwd())));
|
|
@@ -1783,8 +1810,80 @@ function flattenFederatedReleaseZip(bytes) {
|
|
|
1783
1810
|
}
|
|
1784
1811
|
return zip.toBuffer();
|
|
1785
1812
|
}
|
|
1786
|
-
function federatedReleaseZipHint() {
|
|
1787
|
-
return
|
|
1813
|
+
function federatedReleaseZipHint(slug) {
|
|
1814
|
+
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)}`;
|
|
1815
|
+
}
|
|
1816
|
+
function federationRemoteName(appId) {
|
|
1817
|
+
const sanitized = appId.replace(/[^a-zA-Z0-9_$]/g, "_");
|
|
1818
|
+
return sanitized.length > 0 ? sanitized : "federatedApp";
|
|
1819
|
+
}
|
|
1820
|
+
function initThenPublishHint(slug) {
|
|
1821
|
+
const s = (slug ?? "").trim() || "<portal-slug>";
|
|
1822
|
+
return `gf apps init --slug ${s} && cd ${s} && npm install && npm run release && gf apps publish --path release.zip`;
|
|
1823
|
+
}
|
|
1824
|
+
function extractReferencedSiblings(remoteEntrySource) {
|
|
1825
|
+
const names = /* @__PURE__ */ new Set();
|
|
1826
|
+
for (const match of remoteEntrySource.matchAll(
|
|
1827
|
+
/["']\.\/([^"'/]+\.(?:js|mjs|cjs|css))["']/g
|
|
1828
|
+
)) {
|
|
1829
|
+
if (match[1] !== REMOTE_ENTRY) names.add(match[1]);
|
|
1830
|
+
}
|
|
1831
|
+
for (const match of remoteEntrySource.matchAll(
|
|
1832
|
+
/["']([A-Za-z0-9._-]+\.css)["']/g
|
|
1833
|
+
)) {
|
|
1834
|
+
names.add(match[1]);
|
|
1835
|
+
}
|
|
1836
|
+
return [...names];
|
|
1837
|
+
}
|
|
1838
|
+
function extractFederationScope(remoteEntrySource) {
|
|
1839
|
+
return remoteEntrySource.match(/css__([A-Za-z0-9_-]+)__/)?.[1];
|
|
1840
|
+
}
|
|
1841
|
+
function zipRootNames(bytes) {
|
|
1842
|
+
const zip = new AdmZip2(bytes);
|
|
1843
|
+
return zip.getEntries().filter((entry) => !entry.isDirectory).map((entry) => normalizeZipEntryName(entry.entryName));
|
|
1844
|
+
}
|
|
1845
|
+
function readZipFile(bytes, rel) {
|
|
1846
|
+
const zip = new AdmZip2(bytes);
|
|
1847
|
+
const entry = zip.getEntries().find((item) => {
|
|
1848
|
+
if (item.isDirectory) return false;
|
|
1849
|
+
return normalizeZipEntryName(item.entryName) === rel;
|
|
1850
|
+
});
|
|
1851
|
+
return entry ? entry.getData().toString("utf8") : void 0;
|
|
1852
|
+
}
|
|
1853
|
+
function assertFederatedReleaseZip(bytes, slug) {
|
|
1854
|
+
const hint = initThenPublishHint(slug);
|
|
1855
|
+
const names = zipRootNames(bytes);
|
|
1856
|
+
const present = new Set(names.map((name) => basename(name)));
|
|
1857
|
+
const entryRel = chooseRemoteEntry(names);
|
|
1858
|
+
if (!entryRel) {
|
|
1859
|
+
throw new Error(`Zip has no remoteEntry.js. ${hint}`);
|
|
1860
|
+
}
|
|
1861
|
+
const source = readZipFile(bytes, entryRel);
|
|
1862
|
+
if (!source) {
|
|
1863
|
+
throw new Error(`Zip has no remoteEntry.js. ${hint}`);
|
|
1864
|
+
}
|
|
1865
|
+
const siblings = extractReferencedSiblings(source);
|
|
1866
|
+
const missing = siblings.filter((name) => !present.has(name));
|
|
1867
|
+
const jsOrCssAtRoot = names.filter((name) => {
|
|
1868
|
+
if (name.includes("/")) return false;
|
|
1869
|
+
const lower = name.toLowerCase();
|
|
1870
|
+
return lower.endsWith(".js") || lower.endsWith(".mjs") || lower.endsWith(".css");
|
|
1871
|
+
});
|
|
1872
|
+
if (missing.length > 0 || siblings.length === 0 && jsOrCssAtRoot.length < 2) {
|
|
1873
|
+
const detail = missing.length > 0 ? `Missing next to remoteEntry.js: ${missing.join(", ")}.` : "Zip is only remoteEntry.js (Vite chunks were never packed).";
|
|
1874
|
+
throw new Error(
|
|
1875
|
+
`${detail} Do not zip dist/ or upload a lone remoteEntry.js. ${hint}`
|
|
1876
|
+
);
|
|
1877
|
+
}
|
|
1878
|
+
const scope = extractFederationScope(source);
|
|
1879
|
+
if (slug && scope) {
|
|
1880
|
+
const expected = federationRemoteName(slug);
|
|
1881
|
+
if (scope !== expected) {
|
|
1882
|
+
throw new Error(
|
|
1883
|
+
`Federation name is "${scope}" but Shell looks up "${expected}" (from slug ${slug}). Use the starter-kit vite.config (federationRemoteName). ${hint}`
|
|
1884
|
+
);
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1788
1887
|
}
|
|
1789
1888
|
|
|
1790
1889
|
// src/commands/apps.ts
|
|
@@ -1827,10 +1926,10 @@ function zipHasRemoteEntry(bytes) {
|
|
|
1827
1926
|
return name === REMOTE_ENTRY || name.endsWith(`/${REMOTE_ENTRY}`);
|
|
1828
1927
|
});
|
|
1829
1928
|
}
|
|
1830
|
-
function prepareReleaseZip(bytes) {
|
|
1929
|
+
function prepareReleaseZip(bytes, slug) {
|
|
1831
1930
|
if (!zipHasRemoteEntry(bytes)) {
|
|
1832
1931
|
throw new Error(
|
|
1833
|
-
`Zip has no remoteEntry.js. ${
|
|
1932
|
+
`Zip has no remoteEntry.js. ${initThenPublishHint(slug)}`
|
|
1834
1933
|
);
|
|
1835
1934
|
}
|
|
1836
1935
|
const flattened = flattenFederatedReleaseZip(bytes);
|
|
@@ -1840,10 +1939,11 @@ function prepareReleaseZip(bytes) {
|
|
|
1840
1939
|
if (Object.keys(plan).length > 0) {
|
|
1841
1940
|
process.stderr.write(
|
|
1842
1941
|
`Note: flattened Vite dist/assets layout so chunks sit next to remoteEntry.js.
|
|
1843
|
-
Prefer: ${federatedReleaseZipHint()}
|
|
1942
|
+
Prefer: ${federatedReleaseZipHint(slug)}
|
|
1844
1943
|
`
|
|
1845
1944
|
);
|
|
1846
1945
|
}
|
|
1946
|
+
assertFederatedReleaseZip(flattened, slug);
|
|
1847
1947
|
return flattened;
|
|
1848
1948
|
}
|
|
1849
1949
|
async function isDirectory(filePath) {
|
|
@@ -1942,7 +2042,8 @@ async function appsCreateCommand(opts) {
|
|
|
1942
2042
|
} catch (err) {
|
|
1943
2043
|
if (err instanceof CpError && err.status === 409) {
|
|
1944
2044
|
throw new Error(
|
|
1945
|
-
`App slug ${slug} already exists.
|
|
2045
|
+
`App slug ${slug} already exists. Init the kit if you do not have it, then publish:
|
|
2046
|
+
${initThenPublishHint(slug)}`
|
|
1946
2047
|
);
|
|
1947
2048
|
}
|
|
1948
2049
|
throw err;
|
|
@@ -1974,7 +2075,7 @@ async function appsPublishCommand(appRef, opts) {
|
|
|
1974
2075
|
bytes = await fs5.readFile(bundlePath);
|
|
1975
2076
|
} catch {
|
|
1976
2077
|
throw new Error(
|
|
1977
|
-
`No bundle at ${bundlePath}.
|
|
2078
|
+
`No bundle at ${bundlePath}. ${initThenPublishHint()}`
|
|
1978
2079
|
);
|
|
1979
2080
|
}
|
|
1980
2081
|
if (bytes.byteLength === 0) {
|
|
@@ -2000,11 +2101,14 @@ async function appsPublishCommand(appRef, opts) {
|
|
|
2000
2101
|
}
|
|
2001
2102
|
const ext = path5.extname(bundlePath).toLowerCase();
|
|
2002
2103
|
const isZip = ext === ".zip";
|
|
2003
|
-
if (isZip) {
|
|
2004
|
-
|
|
2104
|
+
if (!isZip) {
|
|
2105
|
+
throw new Error(
|
|
2106
|
+
`Publish release.zip from \`npm run release\`, not a lone ${path5.basename(bundlePath)}. ` + initThenPublishHint(app.slug)
|
|
2107
|
+
);
|
|
2005
2108
|
}
|
|
2006
|
-
|
|
2007
|
-
const
|
|
2109
|
+
bytes = prepareReleaseZip(bytes, app.slug);
|
|
2110
|
+
const bundle = "zip";
|
|
2111
|
+
const contentType = "application/zip";
|
|
2008
2112
|
if (opts.syncManifest !== false && sibling) {
|
|
2009
2113
|
await patchApp(cfg.apiUrl, cfg.workspaceId, app.id, {
|
|
2010
2114
|
manifest: sibling.manifest
|
|
@@ -2274,7 +2378,7 @@ function run(fn) {
|
|
|
2274
2378
|
var program = new Command();
|
|
2275
2379
|
program.name("gf").description(
|
|
2276
2380
|
"Groundfloor CLI \u2014 sign in, deploy coderunners, publish Shell apps, and manage workspace resources."
|
|
2277
|
-
).version("0.1.
|
|
2381
|
+
).version("0.1.4");
|
|
2278
2382
|
program.command("login").description(
|
|
2279
2383
|
"Log in via the browser (production by default; --dev / --stage for other cells)"
|
|
2280
2384
|
).option("--dev", "Sign in to Groundfloor dev").option("--stage", "Sign in to Groundfloor stage").option(
|
|
@@ -2371,10 +2475,10 @@ apps.command("create").description("Register a Shell federated or standalone pro
|
|
|
2371
2475
|
"Path to groundfloor.manifest.json (Shell apps)"
|
|
2372
2476
|
).option("--json", "Output raw JSON").action((opts) => run(() => appsCreateCommand(opts)));
|
|
2373
2477
|
apps.command("publish [appId]").description(
|
|
2374
|
-
"Upload release.zip
|
|
2478
|
+
"Upload release.zip from npm run release (not a lone remoteEntry.js)"
|
|
2375
2479
|
).option("-w, --workspace <id>", "Workspace id override").option("--app <id|slug>", "App id or slug (else manifest appId)").option(
|
|
2376
2480
|
"-p, --path <file>",
|
|
2377
|
-
"release.zip
|
|
2481
|
+
"release.zip from npm run release (or a folder containing it)",
|
|
2378
2482
|
"release.zip"
|
|
2379
2483
|
).option("-m, --label <text>", "Optional release label").option(
|
|
2380
2484
|
"--no-sync-manifest",
|
package/package.json
CHANGED