@groundfloorcloud/cli 0.1.0 → 0.1.2

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 +137 -114
  2. package/dist/index.js +1090 -30
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -64,27 +64,95 @@ async function updateConfig(patch) {
64
64
  }
65
65
 
66
66
  // src/auth/config.ts
67
- var DEFAULTS = {
68
- issuer: "https://auth.dev.groundfloor.cloud/realms/groundfloor_dev",
69
- clientId: "groundfloor-cli",
70
- apiUrl: "http://localhost:8088"
67
+ var CELLS = {
68
+ production: {
69
+ apiUrl: "https://platform.groundfloor.cloud",
70
+ issuer: "https://auth.groundfloor.cloud/realms/groundfloor",
71
+ clientId: "groundfloor-cli",
72
+ consoleUrl: "https://console.groundfloor.cloud"
73
+ },
74
+ stage: {
75
+ apiUrl: "https://platform.stage.groundfloor.cloud",
76
+ issuer: "https://auth.stage.groundfloor.cloud/realms/groundfloor_pico_stage-realm",
77
+ clientId: "groundfloor-cli",
78
+ consoleUrl: "https://console.stage.groundfloor.cloud"
79
+ },
80
+ dev: {
81
+ apiUrl: "https://platform.dev.groundfloor.cloud",
82
+ issuer: "https://auth.dev.groundfloor.cloud/realms/groundfloor_dev",
83
+ clientId: "groundfloor-cli",
84
+ consoleUrl: "https://console.dev.groundfloor.cloud"
85
+ }
86
+ };
87
+ var STARTER_KIT_ZIP_PATH = "/downloads/shell-starter-kit.zip";
88
+ function starterKitZipUrl(cell = "production") {
89
+ return `${CELLS[cell].consoleUrl}${STARTER_KIT_ZIP_PATH}`;
90
+ }
91
+ var CELL_ALIASES = {
92
+ production: "production",
93
+ prod: "production",
94
+ stage: "stage",
95
+ staging: "stage",
96
+ dev: "dev",
97
+ development: "dev"
71
98
  };
99
+ function parseCell(raw) {
100
+ const cell = CELL_ALIASES[raw.trim().toLowerCase()];
101
+ if (!cell) {
102
+ throw new Error(
103
+ `Unknown cell "${raw}". Use production, stage, or dev.`
104
+ );
105
+ }
106
+ return cell;
107
+ }
108
+ function cellFromLoginOpts(opts) {
109
+ const selected = [];
110
+ if (opts.dev) selected.push("dev");
111
+ if (opts.stage) selected.push("stage");
112
+ if (opts.env) selected.push(parseCell(opts.env));
113
+ const unique = [...new Set(selected)];
114
+ if (unique.length > 1) {
115
+ throw new Error("Use only one of --dev, --stage, or --env.");
116
+ }
117
+ return unique[0] ?? "production";
118
+ }
119
+ function inferCell(apiUrl, issuer) {
120
+ const api = apiUrl.replace(/\/+$/, "");
121
+ const iss = (issuer ?? "").replace(/\/+$/, "");
122
+ for (const [name, hosts] of Object.entries(CELLS)) {
123
+ if (api === hosts.apiUrl || iss === hosts.issuer) return name;
124
+ }
125
+ return void 0;
126
+ }
72
127
  function trimTrailingSlash(value) {
73
128
  return value.replace(/\/+$/, "");
74
129
  }
75
130
  async function resolveConfig(overrides = {}) {
76
131
  const file = await readConfig();
77
- const issuer = overrides.issuer ?? process.env.GROUNDFLOOR_ISSUER ?? file.issuer ?? DEFAULTS.issuer;
78
- const clientId = overrides.clientId ?? process.env.GROUNDFLOOR_CLIENT_ID ?? file.clientId ?? DEFAULTS.clientId;
79
- const apiUrl = overrides.apiUrl ?? process.env.GROUNDFLOOR_API_URL ?? file.apiUrl ?? DEFAULTS.apiUrl;
132
+ const production = CELLS.production;
133
+ const issuer = overrides.issuer ?? process.env.GROUNDFLOOR_ISSUER ?? file.issuer ?? production.issuer;
134
+ const clientId = overrides.clientId ?? process.env.GROUNDFLOOR_CLIENT_ID ?? file.clientId ?? production.clientId;
135
+ const apiUrl = overrides.apiUrl ?? process.env.GROUNDFLOOR_API_URL ?? file.apiUrl ?? production.apiUrl;
80
136
  const workspaceId = overrides.workspaceId ?? process.env.GROUNDFLOOR_WORKSPACE_ID ?? file.workspaceId;
137
+ const environment = overrides.environment ?? inferCell(apiUrl, issuer) ?? file.environment;
81
138
  return {
82
139
  issuer: trimTrailingSlash(issuer),
83
140
  clientId,
84
141
  apiUrl: trimTrailingSlash(apiUrl),
85
- workspaceId: workspaceId || void 0
142
+ workspaceId: workspaceId || void 0,
143
+ environment
86
144
  };
87
145
  }
146
+ async function requireWorkspace(workspace) {
147
+ const cfg = await resolveConfig({ workspaceId: workspace });
148
+ if (!cfg.workspaceId) {
149
+ throw new Error(
150
+ "No workspace selected. Run `gf workspaces use <id>` or pass --workspace <id>."
151
+ );
152
+ }
153
+ return { ...cfg, workspaceId: cfg.workspaceId };
154
+ }
155
+ var defaults = CELLS.production;
88
156
 
89
157
  // src/auth/oauth.ts
90
158
  import crypto from "crypto";
@@ -323,10 +391,13 @@ async function refreshTokens(opts) {
323
391
  var EXPIRY_SKEW_MS = 3e4;
324
392
  var NotLoggedInError = class extends Error {
325
393
  constructor() {
326
- super("Not logged in. Run `gf login` first.");
394
+ super(
395
+ "Not logged in. Set GROUNDFLOOR_TOKEN, or run `gf login` first."
396
+ );
327
397
  this.name = "NotLoggedInError";
328
398
  }
329
399
  };
400
+ var accessTokenProvider = null;
330
401
  function decodeJwt(token) {
331
402
  const parts = token.split(".");
332
403
  if (parts.length < 2) return null;
@@ -353,6 +424,9 @@ function isFresh(session) {
353
424
  return Date.now() < session.expiresAt - EXPIRY_SKEW_MS;
354
425
  }
355
426
  async function getValidAccessToken() {
427
+ if (accessTokenProvider) {
428
+ return accessTokenProvider();
429
+ }
356
430
  const session = await readAuth();
357
431
  if (!session || !session.accessToken) {
358
432
  throw new NotLoggedInError();
@@ -382,13 +456,16 @@ async function getValidAccessToken() {
382
456
 
383
457
  // src/commands/login.ts
384
458
  async function loginCommand(opts) {
459
+ const cell = cellFromLoginOpts(opts);
460
+ const hosts = CELLS[cell];
385
461
  const cfg = await resolveConfig({
386
- issuer: opts.issuer,
387
- clientId: opts.clientId,
388
- apiUrl: opts.apiUrl
462
+ issuer: opts.issuer ?? hosts.issuer,
463
+ clientId: opts.clientId ?? hosts.clientId,
464
+ apiUrl: opts.apiUrl ?? hosts.apiUrl,
465
+ environment: cell
389
466
  });
390
467
  process.stderr.write(
391
- `Signing in to ${cfg.issuer}
468
+ `Signing in to Groundfloor ${cell}
392
469
  client: ${cfg.clientId}
393
470
  api: ${cfg.apiUrl}
394
471
  `
@@ -416,7 +493,8 @@ async function loginCommand(opts) {
416
493
  await updateConfig({
417
494
  issuer: cfg.issuer,
418
495
  clientId: cfg.clientId,
419
- apiUrl: cfg.apiUrl
496
+ apiUrl: cfg.apiUrl,
497
+ environment: cell
420
498
  });
421
499
  const claims = decodeJwt(tokens.access_token);
422
500
  const who = claims?.email || claims?.preferred_username || claims?.sub || "unknown user";
@@ -455,9 +533,9 @@ async function parseError(res) {
455
533
  }
456
534
  return `${res.status} ${detail}`;
457
535
  }
458
- async function cpGet(apiUrl, path4) {
536
+ async function cpGet(apiUrl, path7) {
459
537
  const token = await getValidAccessToken();
460
- const res = await fetch(`${apiUrl}${path4}`, {
538
+ const res = await fetch(`${apiUrl}${path7}`, {
461
539
  headers: {
462
540
  Authorization: `Bearer ${token}`,
463
541
  Accept: "application/json"
@@ -468,9 +546,9 @@ async function cpGet(apiUrl, path4) {
468
546
  }
469
547
  return await res.json();
470
548
  }
471
- async function cpPostJson(apiUrl, path4, body) {
549
+ async function cpPostJson(apiUrl, path7, body) {
472
550
  const token = await getValidAccessToken();
473
- const res = await fetch(`${apiUrl}${path4}`, {
551
+ const res = await fetch(`${apiUrl}${path7}`, {
474
552
  method: "POST",
475
553
  headers: {
476
554
  Authorization: `Bearer ${token}`,
@@ -484,9 +562,9 @@ async function cpPostJson(apiUrl, path4, body) {
484
562
  }
485
563
  return await res.json();
486
564
  }
487
- async function cpDelete(apiUrl, path4) {
565
+ async function cpDelete(apiUrl, path7) {
488
566
  const token = await getValidAccessToken();
489
- const res = await fetch(`${apiUrl}${path4}`, {
567
+ const res = await fetch(`${apiUrl}${path7}`, {
490
568
  method: "DELETE",
491
569
  headers: { Authorization: `Bearer ${token}`, Accept: "application/json" }
492
570
  });
@@ -494,6 +572,61 @@ async function cpDelete(apiUrl, path4) {
494
572
  throw new CpError(res.status, await parseError(res));
495
573
  }
496
574
  }
575
+ async function cpPutJson(apiUrl, path7, body) {
576
+ const token = await getValidAccessToken();
577
+ const res = await fetch(`${apiUrl}${path7}`, {
578
+ method: "PUT",
579
+ headers: {
580
+ Authorization: `Bearer ${token}`,
581
+ Accept: "application/json",
582
+ "Content-Type": "application/json"
583
+ },
584
+ body: JSON.stringify(body)
585
+ });
586
+ if (!res.ok) {
587
+ throw new CpError(res.status, await parseError(res));
588
+ }
589
+ return await res.json();
590
+ }
591
+ async function cpPatchJson(apiUrl, path7, body) {
592
+ const token = await getValidAccessToken();
593
+ const res = await fetch(`${apiUrl}${path7}`, {
594
+ method: "PATCH",
595
+ headers: {
596
+ Authorization: `Bearer ${token}`,
597
+ Accept: "application/json",
598
+ "Content-Type": "application/json"
599
+ },
600
+ body: JSON.stringify(body)
601
+ });
602
+ if (!res.ok) {
603
+ throw new CpError(res.status, await parseError(res));
604
+ }
605
+ return await res.json();
606
+ }
607
+ async function cpPutBytes(apiUrl, path7, body, contentType) {
608
+ const token = await getValidAccessToken();
609
+ const res = await fetch(`${apiUrl}${path7}`, {
610
+ method: "PUT",
611
+ headers: {
612
+ Authorization: `Bearer ${token}`,
613
+ Accept: "application/json",
614
+ "Content-Type": contentType
615
+ },
616
+ body
617
+ });
618
+ if (!res.ok) {
619
+ throw new CpError(res.status, await parseError(res));
620
+ }
621
+ }
622
+ function queryString(params) {
623
+ const u = new URLSearchParams();
624
+ for (const [key, value] of Object.entries(params)) {
625
+ if (value) u.set(key, value);
626
+ }
627
+ const s = u.toString();
628
+ return s ? `?${s}` : "";
629
+ }
497
630
  function listWorkspaces(apiUrl) {
498
631
  return cpGet(apiUrl, "/v1/workspaces");
499
632
  }
@@ -624,6 +757,134 @@ function verifyDomain(apiUrl, workspaceId, coderunnerId, domain) {
624
757
  {}
625
758
  );
626
759
  }
760
+ function wsPath(workspaceId, suffix) {
761
+ return `/v1/workspaces/${encodeURIComponent(workspaceId)}${suffix}`;
762
+ }
763
+ function listSecrets(apiUrl, workspaceId, environmentId) {
764
+ return cpGet(
765
+ apiUrl,
766
+ `${wsPath(workspaceId, "/secrets")}${queryString({
767
+ environment_id: environmentId
768
+ })}`
769
+ );
770
+ }
771
+ function getSecret(apiUrl, workspaceId, key, environmentId) {
772
+ return cpGet(
773
+ apiUrl,
774
+ `${wsPath(workspaceId, `/secrets/${encodeURIComponent(key)}`)}${queryString({
775
+ environment_id: environmentId
776
+ })}`
777
+ );
778
+ }
779
+ function putSecret(apiUrl, workspaceId, key, payload, environmentId) {
780
+ return cpPutJson(
781
+ apiUrl,
782
+ `${wsPath(workspaceId, `/secrets/${encodeURIComponent(key)}`)}${queryString({
783
+ environment_id: environmentId
784
+ })}`,
785
+ payload
786
+ );
787
+ }
788
+ function deleteSecret(apiUrl, workspaceId, key, environmentId) {
789
+ return cpDelete(
790
+ apiUrl,
791
+ `${wsPath(workspaceId, `/secrets/${encodeURIComponent(key)}`)}${queryString({
792
+ environment_id: environmentId
793
+ })}`
794
+ );
795
+ }
796
+ function listFiles(apiUrl, workspaceId) {
797
+ return cpGet(apiUrl, wsPath(workspaceId, "/files"));
798
+ }
799
+ function initFileUpload(apiUrl, workspaceId, payload) {
800
+ return cpPostJson(
801
+ apiUrl,
802
+ wsPath(workspaceId, "/files/upload-url"),
803
+ payload
804
+ );
805
+ }
806
+ function finalizeFileUpload(apiUrl, workspaceId, fileId) {
807
+ return cpPostJson(
808
+ apiUrl,
809
+ wsPath(workspaceId, `/files/${encodeURIComponent(fileId)}/finalize`),
810
+ {}
811
+ );
812
+ }
813
+ function getFileDownloadUrl(apiUrl, workspaceId, fileId) {
814
+ return cpGet(
815
+ apiUrl,
816
+ wsPath(workspaceId, `/files/${encodeURIComponent(fileId)}/download-url`)
817
+ );
818
+ }
819
+ function deleteFile(apiUrl, workspaceId, fileId) {
820
+ return cpDelete(
821
+ apiUrl,
822
+ wsPath(workspaceId, `/files/${encodeURIComponent(fileId)}`)
823
+ );
824
+ }
825
+ function listApps(apiUrl, workspaceId) {
826
+ return cpGet(
827
+ apiUrl,
828
+ `${wsPath(workspaceId, "/apps")}${queryString({ kind: "product" })}`
829
+ );
830
+ }
831
+ function getApp(apiUrl, workspaceId, appId) {
832
+ return cpGet(
833
+ apiUrl,
834
+ wsPath(workspaceId, `/apps/${encodeURIComponent(appId)}`)
835
+ );
836
+ }
837
+ function createApp(apiUrl, workspaceId, body) {
838
+ return cpPostJson(apiUrl, wsPath(workspaceId, "/apps"), body);
839
+ }
840
+ function patchApp(apiUrl, workspaceId, appId, body) {
841
+ return cpPatchJson(
842
+ apiUrl,
843
+ wsPath(workspaceId, `/apps/${encodeURIComponent(appId)}`),
844
+ body
845
+ );
846
+ }
847
+ function initAppReleaseUpload(apiUrl, workspaceId, appId, body) {
848
+ return cpPostJson(
849
+ apiUrl,
850
+ wsPath(workspaceId, `/apps/${encodeURIComponent(appId)}/releases/upload-url`),
851
+ body
852
+ );
853
+ }
854
+ function uploadAppReleaseContent(apiUrl, workspaceId, appId, releaseId, bytes, contentType) {
855
+ return cpPutBytes(
856
+ apiUrl,
857
+ wsPath(
858
+ workspaceId,
859
+ `/apps/${encodeURIComponent(appId)}/releases/${encodeURIComponent(releaseId)}/content`
860
+ ),
861
+ bytes,
862
+ contentType
863
+ );
864
+ }
865
+ function finalizeAppRelease(apiUrl, workspaceId, appId, releaseId) {
866
+ return cpPostJson(
867
+ apiUrl,
868
+ wsPath(
869
+ workspaceId,
870
+ `/apps/${encodeURIComponent(appId)}/releases/${encodeURIComponent(releaseId)}/finalize`
871
+ ),
872
+ {}
873
+ );
874
+ }
875
+ function getDataplaneStatus(apiUrl, workspaceId) {
876
+ return cpGet(
877
+ apiUrl,
878
+ wsPath(workspaceId, "/dataplane/status")
879
+ );
880
+ }
881
+ function provisionDataplane(apiUrl, workspaceId) {
882
+ return cpPostJson(
883
+ apiUrl,
884
+ wsPath(workspaceId, "/dataplane/provision"),
885
+ {}
886
+ );
887
+ }
627
888
 
628
889
  // src/commands/whoami.ts
629
890
  async function whoamiCommand() {
@@ -636,6 +897,10 @@ async function whoamiCommand() {
636
897
  `);
637
898
  process.stdout.write(`Subject: ${subject}
638
899
  `);
900
+ if (cfg.environment) {
901
+ process.stdout.write(`Cell: ${cfg.environment}
902
+ `);
903
+ }
639
904
  process.stdout.write(`Issuer: ${cfg.issuer}
640
905
  `);
641
906
  process.stdout.write(`API: ${cfg.apiUrl}
@@ -666,6 +931,9 @@ async function envCommand() {
666
931
  if (cfg.workspaceId) {
667
932
  lines.push(`export GROUNDFLOOR_WORKSPACE_ID=${cfg.workspaceId}`);
668
933
  }
934
+ if (cfg.environment) {
935
+ lines.push(`export GROUNDFLOOR_ENV=${cfg.environment}`);
936
+ }
669
937
  process.stdout.write(`${lines.join("\n")}
670
938
  `);
671
939
  }
@@ -803,9 +1071,9 @@ async function walkFileList(dir) {
803
1071
  await walk(dir, "");
804
1072
  return out;
805
1073
  }
806
- async function buildZip(sourceDir, files) {
1074
+ async function buildZip(sourceDir, files2) {
807
1075
  const zip = new AdmZip();
808
- for (const rel of files) {
1076
+ for (const rel of files2) {
809
1077
  const abs = path2.join(sourceDir, rel);
810
1078
  try {
811
1079
  const data = await fs2.readFile(abs);
@@ -856,13 +1124,13 @@ async function packageSource(opts) {
856
1124
  throw new Error(`Not a directory: ${sourceDir}`);
857
1125
  }
858
1126
  const useGit = await isGitRepo(sourceDir);
859
- const files = useGit ? await gitFileList(sourceDir) : await walkFileList(sourceDir);
860
- if (files.length === 0) {
1127
+ const files2 = useGit ? await gitFileList(sourceDir) : await walkFileList(sourceDir);
1128
+ if (files2.length === 0) {
861
1129
  throw new Error(
862
1130
  `No files to package in ${sourceDir} (everything ignored or empty).`
863
1131
  );
864
1132
  }
865
- const zip = await buildZip(sourceDir, files);
1133
+ const zip = await buildZip(sourceDir, files2);
866
1134
  let gitSha = null;
867
1135
  const shaRoot = cleanup ?? (useGit ? sourceDir : null);
868
1136
  if (shaRoot) {
@@ -873,7 +1141,7 @@ async function packageSource(opts) {
873
1141
  gitSha = null;
874
1142
  }
875
1143
  }
876
- return { zip, fileCount: files.length, sourceDir, cleanup, gitSha };
1144
+ return { zip, fileCount: files2.length, sourceDir, cleanup, gitSha };
877
1145
  } catch (err) {
878
1146
  if (cleanup) {
879
1147
  await fs2.rm(cleanup, { recursive: true, force: true }).catch(() => {
@@ -1271,6 +1539,726 @@ async function domainsVerifyCommand(domain, opts) {
1271
1539
  }
1272
1540
  }
1273
1541
 
1542
+ // src/commands/secrets.ts
1543
+ async function readValue(value) {
1544
+ if (value === void 0) {
1545
+ throw new Error("Pass a value, or `-` to read from stdin.");
1546
+ }
1547
+ if (value === "-") {
1548
+ const chunks = [];
1549
+ for await (const chunk of process.stdin) {
1550
+ chunks.push(Buffer.from(chunk));
1551
+ }
1552
+ return Buffer.concat(chunks).toString("utf8").replace(/\n$/, "");
1553
+ }
1554
+ return value;
1555
+ }
1556
+ async function secretsListCommand(opts) {
1557
+ const cfg = await requireWorkspace(opts.workspace);
1558
+ const data = await listSecrets(cfg.apiUrl, cfg.workspaceId, opts.environment);
1559
+ if (opts.json) {
1560
+ process.stdout.write(`${JSON.stringify(data, null, 2)}
1561
+ `);
1562
+ return;
1563
+ }
1564
+ if (data.secrets.length === 0) {
1565
+ process.stdout.write("No secrets in this workspace.\n");
1566
+ return;
1567
+ }
1568
+ for (const secret of data.secrets) {
1569
+ process.stdout.write(`${secret.key}
1570
+ `);
1571
+ }
1572
+ }
1573
+ async function secretsGetCommand(key, opts) {
1574
+ const cfg = await requireWorkspace(opts.workspace);
1575
+ const secret = await getSecret(
1576
+ cfg.apiUrl,
1577
+ cfg.workspaceId,
1578
+ key,
1579
+ opts.environment
1580
+ );
1581
+ if (opts.json) {
1582
+ process.stdout.write(`${JSON.stringify(secret, null, 2)}
1583
+ `);
1584
+ return;
1585
+ }
1586
+ process.stdout.write(`${secret.value}
1587
+ `);
1588
+ }
1589
+ async function secretsSetCommand(key, value, opts) {
1590
+ const cfg = await requireWorkspace(opts.workspace);
1591
+ const resolved = await readValue(value);
1592
+ const saved = await putSecret(
1593
+ cfg.apiUrl,
1594
+ cfg.workspaceId,
1595
+ key,
1596
+ { value: resolved, description: opts.description },
1597
+ opts.environment
1598
+ );
1599
+ process.stdout.write(`Saved secret ${saved.key}
1600
+ `);
1601
+ }
1602
+ async function secretsRmCommand(key, opts) {
1603
+ const cfg = await requireWorkspace(opts.workspace);
1604
+ await deleteSecret(cfg.apiUrl, cfg.workspaceId, key, opts.environment);
1605
+ process.stdout.write(`Deleted secret ${key}
1606
+ `);
1607
+ }
1608
+
1609
+ // src/commands/files.ts
1610
+ import { promises as fs4 } from "fs";
1611
+ import path4 from "path";
1612
+ function guessContentType(filename) {
1613
+ const ext = path4.extname(filename).toLowerCase();
1614
+ const types = {
1615
+ ".json": "application/json",
1616
+ ".txt": "text/plain",
1617
+ ".csv": "text/csv",
1618
+ ".pdf": "application/pdf",
1619
+ ".png": "image/png",
1620
+ ".jpg": "image/jpeg",
1621
+ ".jpeg": "image/jpeg",
1622
+ ".zip": "application/zip"
1623
+ };
1624
+ return types[ext] ?? "application/octet-stream";
1625
+ }
1626
+ async function filesListCommand(opts) {
1627
+ const cfg = await requireWorkspace(opts.workspace);
1628
+ const data = await listFiles(cfg.apiUrl, cfg.workspaceId);
1629
+ if (opts.json) {
1630
+ process.stdout.write(`${JSON.stringify(data, null, 2)}
1631
+ `);
1632
+ return;
1633
+ }
1634
+ if (data.files.length === 0) {
1635
+ process.stdout.write("No files in this workspace.\n");
1636
+ return;
1637
+ }
1638
+ for (const file of data.files) {
1639
+ const size = file.size_bytes != null ? `${file.size_bytes}B` : "";
1640
+ process.stdout.write(
1641
+ `${file.id} ${file.name ?? ""} ${size} [${file.status ?? ""}]
1642
+ `
1643
+ );
1644
+ }
1645
+ }
1646
+ async function filesUploadCommand(filePath, opts) {
1647
+ const cfg = await requireWorkspace(opts.workspace);
1648
+ const resolved = path4.resolve(filePath);
1649
+ const buf = await fs4.readFile(resolved);
1650
+ const name = opts.name ?? path4.basename(resolved);
1651
+ const contentType = guessContentType(name);
1652
+ const init = await initFileUpload(cfg.apiUrl, cfg.workspaceId, {
1653
+ name,
1654
+ content_type: contentType,
1655
+ size_bytes: buf.byteLength
1656
+ });
1657
+ const put = await fetch(init.upload_url, {
1658
+ method: "PUT",
1659
+ headers: { "Content-Type": contentType },
1660
+ body: buf
1661
+ });
1662
+ if (!put.ok) {
1663
+ throw new Error(
1664
+ `Upload to object storage failed: ${put.status} ${put.statusText}`
1665
+ );
1666
+ }
1667
+ const file = await finalizeFileUpload(
1668
+ cfg.apiUrl,
1669
+ cfg.workspaceId,
1670
+ init.file.id
1671
+ );
1672
+ process.stdout.write(`Uploaded ${file.id} ${file.name ?? name}
1673
+ `);
1674
+ }
1675
+ async function filesDownloadCommand(fileId, opts) {
1676
+ const cfg = await requireWorkspace(opts.workspace);
1677
+ const { download_url: url } = await getFileDownloadUrl(
1678
+ cfg.apiUrl,
1679
+ cfg.workspaceId,
1680
+ fileId
1681
+ );
1682
+ const res = await fetch(url);
1683
+ if (!res.ok) {
1684
+ throw new Error(`Download failed: ${res.status} ${res.statusText}`);
1685
+ }
1686
+ const buf = Buffer.from(await res.arrayBuffer());
1687
+ const dest = path4.resolve(opts.out ?? fileId);
1688
+ await fs4.writeFile(dest, buf);
1689
+ process.stdout.write(`Wrote ${dest}
1690
+ `);
1691
+ }
1692
+ async function filesRmCommand(fileId, opts) {
1693
+ const cfg = await requireWorkspace(opts.workspace);
1694
+ await deleteFile(cfg.apiUrl, cfg.workspaceId, fileId);
1695
+ process.stdout.write(`Deleted file ${fileId}
1696
+ `);
1697
+ }
1698
+
1699
+ // src/commands/apps.ts
1700
+ import { promises as fs5 } from "fs";
1701
+ import path5 from "path";
1702
+
1703
+ // src/release-zip.ts
1704
+ import AdmZip2 from "adm-zip";
1705
+ var REMOTE_ENTRY = "remoteEntry.js";
1706
+ var FLATTEN_SUFFIXES = [".js", ".mjs", ".cjs", ".css", ".map", ".wasm"];
1707
+ function normalizeZipEntryName(filename) {
1708
+ return filename.replace(/\\/g, "/").replace(/^\/+/, "");
1709
+ }
1710
+ function parentDir(rel) {
1711
+ const i = rel.lastIndexOf("/");
1712
+ return i === -1 ? "" : rel.slice(0, i);
1713
+ }
1714
+ function basename(rel) {
1715
+ const i = rel.lastIndexOf("/");
1716
+ return i === -1 ? rel : rel.slice(i + 1);
1717
+ }
1718
+ function chooseRemoteEntry(relpaths) {
1719
+ const entries = relpaths.filter(
1720
+ (p) => p === REMOTE_ENTRY || p.endsWith(`/${REMOTE_ENTRY}`)
1721
+ );
1722
+ if (entries.length === 0) return void 0;
1723
+ if (entries.includes(REMOTE_ENTRY)) return REMOTE_ENTRY;
1724
+ return [...entries].sort((a, b) => {
1725
+ const score = (p) => {
1726
+ const parts = p.split("/");
1727
+ const inAssets = parts.length >= 2 && parts[parts.length - 2] === "assets";
1728
+ return `${inAssets ? 0 : 1}:${parts.length}:${p}`;
1729
+ };
1730
+ return score(a).localeCompare(score(b));
1731
+ })[0];
1732
+ }
1733
+ function flattenFederatedBundlePlan(relpaths) {
1734
+ const paths = relpaths.map(normalizeZipEntryName).filter((name) => name && !name.endsWith("/") && !name.split("/").includes(".."));
1735
+ const pathset = new Set(paths);
1736
+ const entry = chooseRemoteEntry([...pathset]);
1737
+ if (!entry) return {};
1738
+ const promoteDirs = /* @__PURE__ */ new Set();
1739
+ const entryDir = parentDir(entry);
1740
+ if (entryDir) promoteDirs.add(entryDir);
1741
+ for (const path7 of pathset) {
1742
+ const parent = parentDir(path7);
1743
+ if (parent === "assets" || parent.endsWith("/assets") && parent.split("/").length - 1 < 2) {
1744
+ promoteDirs.add(parent);
1745
+ }
1746
+ }
1747
+ const destToSrc = {};
1748
+ for (const src of paths) {
1749
+ const parent = parentDir(src);
1750
+ if (!promoteDirs.has(parent)) continue;
1751
+ const dest = basename(src);
1752
+ if (dest === "release.zip") continue;
1753
+ const lower = dest.toLowerCase();
1754
+ if (dest !== REMOTE_ENTRY && !FLATTEN_SUFFIXES.some((ext) => lower.endsWith(ext))) {
1755
+ continue;
1756
+ }
1757
+ if (pathset.has(dest)) continue;
1758
+ const prev = destToSrc[dest];
1759
+ if (prev === void 0 || src.split("/").length < prev.split("/").length) {
1760
+ destToSrc[dest] = src;
1761
+ }
1762
+ }
1763
+ return destToSrc;
1764
+ }
1765
+ function flattenFederatedReleaseZip(bytes) {
1766
+ const zip = new AdmZip2(bytes);
1767
+ const rels = [];
1768
+ const byNorm = /* @__PURE__ */ new Map();
1769
+ for (const entry of zip.getEntries()) {
1770
+ if (entry.isDirectory) continue;
1771
+ const name = normalizeZipEntryName(entry.entryName);
1772
+ rels.push(name);
1773
+ byNorm.set(name, entry);
1774
+ }
1775
+ const plan = flattenFederatedBundlePlan(rels);
1776
+ const keys = Object.keys(plan);
1777
+ if (keys.length === 0) return bytes;
1778
+ for (const dest of keys) {
1779
+ const src = plan[dest];
1780
+ const source = byNorm.get(src);
1781
+ if (!source) continue;
1782
+ zip.addFile(dest, source.getData());
1783
+ }
1784
+ return zip.toBuffer();
1785
+ }
1786
+ function federatedReleaseZipHint() {
1787
+ 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).";
1788
+ }
1789
+
1790
+ // src/commands/apps.ts
1791
+ import AdmZip3 from "adm-zip";
1792
+ var MAX_RELEASE_BYTES = 32 * 1024 * 1024;
1793
+ function slugify2(input) {
1794
+ const slug = input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1795
+ return slug || "app";
1796
+ }
1797
+ function formatApp(app) {
1798
+ return `${app.id} ${app.name ?? app.slug ?? ""} ${app.app_kind ?? ""} [${app.status ?? ""}]`;
1799
+ }
1800
+ async function readJsonObject(filePath) {
1801
+ const raw = await fs5.readFile(filePath, "utf8");
1802
+ const parsed = JSON.parse(raw);
1803
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1804
+ throw new Error(`${filePath} must contain a JSON object`);
1805
+ }
1806
+ return parsed;
1807
+ }
1808
+ async function maybeReadManifest(filePath) {
1809
+ if (!filePath) return void 0;
1810
+ return readJsonObject(path5.resolve(filePath));
1811
+ }
1812
+ async function findSiblingManifest(bundlePath) {
1813
+ const dir = await isDirectory(bundlePath) ? bundlePath : path5.dirname(bundlePath);
1814
+ const candidate = path5.join(dir, "groundfloor.manifest.json");
1815
+ try {
1816
+ await fs5.access(candidate);
1817
+ } catch {
1818
+ return void 0;
1819
+ }
1820
+ return { path: candidate, manifest: await readJsonObject(candidate) };
1821
+ }
1822
+ function zipHasRemoteEntry(bytes) {
1823
+ const zip = new AdmZip3(bytes);
1824
+ return zip.getEntries().some((entry) => {
1825
+ if (entry.isDirectory) return false;
1826
+ const name = normalizeZipEntryName(entry.entryName);
1827
+ return name === REMOTE_ENTRY || name.endsWith(`/${REMOTE_ENTRY}`);
1828
+ });
1829
+ }
1830
+ function prepareReleaseZip(bytes) {
1831
+ if (!zipHasRemoteEntry(bytes)) {
1832
+ throw new Error(
1833
+ `Zip has no remoteEntry.js. ${federatedReleaseZipHint()}`
1834
+ );
1835
+ }
1836
+ const flattened = flattenFederatedReleaseZip(bytes);
1837
+ const plan = flattenFederatedBundlePlan(
1838
+ new AdmZip3(bytes).getEntries().filter((e) => !e.isDirectory).map((e) => normalizeZipEntryName(e.entryName))
1839
+ );
1840
+ if (Object.keys(plan).length > 0) {
1841
+ process.stderr.write(
1842
+ `Note: flattened Vite dist/assets layout so chunks sit next to remoteEntry.js.
1843
+ Prefer: ${federatedReleaseZipHint()}
1844
+ `
1845
+ );
1846
+ }
1847
+ return flattened;
1848
+ }
1849
+ async function isDirectory(filePath) {
1850
+ try {
1851
+ return (await fs5.stat(filePath)).isDirectory();
1852
+ } catch {
1853
+ return false;
1854
+ }
1855
+ }
1856
+ async function resolveAppId(apiUrl, workspaceId, idOrSlug) {
1857
+ const listed = await listApps(apiUrl, workspaceId);
1858
+ const match = listed.apps.find(
1859
+ (app) => app.id === idOrSlug || app.slug === idOrSlug
1860
+ );
1861
+ if (match) return match;
1862
+ try {
1863
+ return await getApp(apiUrl, workspaceId, idOrSlug);
1864
+ } catch (err) {
1865
+ if (err instanceof CpError && err.status === 404) {
1866
+ throw new Error(
1867
+ `App not found: ${idOrSlug}. Run \`gf apps ls\` or \`gf apps create\`.`
1868
+ );
1869
+ }
1870
+ throw err;
1871
+ }
1872
+ }
1873
+ async function appsListCommand(opts) {
1874
+ const cfg = await requireWorkspace(opts.workspace);
1875
+ const data = await listApps(cfg.apiUrl, cfg.workspaceId);
1876
+ if (opts.json) {
1877
+ process.stdout.write(`${JSON.stringify(data, null, 2)}
1878
+ `);
1879
+ return;
1880
+ }
1881
+ if (data.apps.length === 0) {
1882
+ process.stdout.write("No apps in this workspace.\n");
1883
+ return;
1884
+ }
1885
+ for (const app of data.apps) {
1886
+ process.stdout.write(`${formatApp(app)}
1887
+ `);
1888
+ }
1889
+ }
1890
+ async function appsGetCommand(appId, opts) {
1891
+ const cfg = await requireWorkspace(opts.workspace);
1892
+ const app = await resolveAppId(cfg.apiUrl, cfg.workspaceId, appId);
1893
+ if (opts.json) {
1894
+ process.stdout.write(`${JSON.stringify(app, null, 2)}
1895
+ `);
1896
+ return;
1897
+ }
1898
+ process.stdout.write(`${formatApp(app)}
1899
+ `);
1900
+ }
1901
+ async function appsCreateCommand(opts) {
1902
+ const cfg = await requireWorkspace(opts.workspace);
1903
+ const kindRaw = (opts.kind ?? "shell_federated").trim();
1904
+ if (kindRaw === "coderunner") {
1905
+ throw new Error(
1906
+ "Do not create app_kind=coderunner. Deploy workloads with `gf deploy`. Use --kind shell_federated or --kind standalone."
1907
+ );
1908
+ }
1909
+ if (kindRaw !== "shell_federated" && kindRaw !== "standalone") {
1910
+ throw new Error(
1911
+ `--kind must be shell_federated or standalone (got ${kindRaw})`
1912
+ );
1913
+ }
1914
+ const kind = kindRaw;
1915
+ const localManifest = await maybeReadManifest(opts.manifest);
1916
+ const name = opts.name?.trim() || (typeof localManifest?.name === "string" ? localManifest.name : "");
1917
+ const slug = opts.slug?.trim() || (typeof localManifest?.appId === "string" ? localManifest.appId : name ? slugify2(name) : "");
1918
+ if (!name) {
1919
+ throw new Error("Pass --name, or a manifest with a name field.");
1920
+ }
1921
+ if (!slug) {
1922
+ throw new Error("Pass --slug, or a manifest with appId.");
1923
+ }
1924
+ if (kind === "standalone" && !opts.primaryCoderunner?.trim()) {
1925
+ throw new Error(
1926
+ "Standalone apps require --primary-coderunner <id> (an existing service coderunner)."
1927
+ );
1928
+ }
1929
+ const body = {
1930
+ name,
1931
+ slug,
1932
+ app_kind: kind
1933
+ };
1934
+ if (opts.environment) body.environment_id = opts.environment;
1935
+ if (opts.primaryCoderunner) {
1936
+ body.primary_coderunner_id = opts.primaryCoderunner;
1937
+ }
1938
+ if (localManifest) body.manifest = localManifest;
1939
+ let app;
1940
+ try {
1941
+ app = await createApp(cfg.apiUrl, cfg.workspaceId, body);
1942
+ } catch (err) {
1943
+ if (err instanceof CpError && err.status === 409) {
1944
+ throw new Error(
1945
+ `App slug ${slug} already exists. Use \`gf apps publish ${slug}\` to ship a build.`
1946
+ );
1947
+ }
1948
+ throw err;
1949
+ }
1950
+ if (opts.json) {
1951
+ process.stdout.write(`${JSON.stringify(app, null, 2)}
1952
+ `);
1953
+ return;
1954
+ }
1955
+ process.stdout.write(`Created ${formatApp(app)}
1956
+ `);
1957
+ if (kind === "shell_federated") {
1958
+ process.stdout.write(
1959
+ `Next:
1960
+ gf apps init --slug ${slug}
1961
+ cd ${slug} && npm install && npm run release
1962
+ gf apps publish --path release.zip
1963
+ Do not scaffold src/App.tsx from scratch \u2014 BrowserRouter in App.tsx crashes the Shell.
1964
+ `
1965
+ );
1966
+ }
1967
+ }
1968
+ async function appsPublishCommand(appRef, opts) {
1969
+ const cfg = await requireWorkspace(opts.workspace);
1970
+ const inputPath = path5.resolve(opts.path ?? "release.zip");
1971
+ const bundlePath = await isDirectory(inputPath) ? path5.join(inputPath, "release.zip") : inputPath;
1972
+ let bytes;
1973
+ try {
1974
+ bytes = await fs5.readFile(bundlePath);
1975
+ } catch {
1976
+ throw new Error(
1977
+ `No bundle at ${bundlePath}. Run \`npm run release\` in the starter-kit, then pass --path release.zip.`
1978
+ );
1979
+ }
1980
+ if (bytes.byteLength === 0) {
1981
+ throw new Error(`Bundle is empty: ${bundlePath}`);
1982
+ }
1983
+ if (bytes.byteLength > MAX_RELEASE_BYTES) {
1984
+ throw new Error(
1985
+ `Bundle exceeds ${MAX_RELEASE_BYTES} bytes (${bytes.byteLength}).`
1986
+ );
1987
+ }
1988
+ const sibling = await findSiblingManifest(bundlePath);
1989
+ const ref = appRef?.trim() || opts.app?.trim() || (typeof sibling?.manifest.appId === "string" ? sibling.manifest.appId : "");
1990
+ if (!ref) {
1991
+ throw new Error(
1992
+ "Pass an app id or slug, or set appId in groundfloor.manifest.json next to the bundle."
1993
+ );
1994
+ }
1995
+ const app = await resolveAppId(cfg.apiUrl, cfg.workspaceId, ref);
1996
+ if (app.app_kind && app.app_kind !== "shell_federated") {
1997
+ throw new Error(
1998
+ `Releases are only for shell_federated apps (this app is ${app.app_kind}). Use \`gf deploy\` for coderunner workloads.`
1999
+ );
2000
+ }
2001
+ const ext = path5.extname(bundlePath).toLowerCase();
2002
+ const isZip = ext === ".zip";
2003
+ if (isZip) {
2004
+ bytes = prepareReleaseZip(bytes);
2005
+ }
2006
+ const bundle = isZip ? "zip" : "remote_entry";
2007
+ const contentType = isZip ? "application/zip" : "application/javascript";
2008
+ if (opts.syncManifest !== false && sibling) {
2009
+ await patchApp(cfg.apiUrl, cfg.workspaceId, app.id, {
2010
+ manifest: sibling.manifest
2011
+ });
2012
+ }
2013
+ const init = await initAppReleaseUpload(cfg.apiUrl, cfg.workspaceId, app.id, {
2014
+ bundle,
2015
+ label: opts.label
2016
+ });
2017
+ await uploadAppReleaseContent(
2018
+ cfg.apiUrl,
2019
+ cfg.workspaceId,
2020
+ app.id,
2021
+ init.release.id,
2022
+ bytes,
2023
+ contentType
2024
+ );
2025
+ const release = await finalizeAppRelease(
2026
+ cfg.apiUrl,
2027
+ cfg.workspaceId,
2028
+ app.id,
2029
+ init.release.id
2030
+ );
2031
+ if (opts.json) {
2032
+ process.stdout.write(`${JSON.stringify({ app, release }, null, 2)}
2033
+ `);
2034
+ return;
2035
+ }
2036
+ process.stdout.write(
2037
+ `Published ${app.slug ?? app.id} build ${release.build_number}` + (release.remote_url ? ` ${release.remote_url}` : "") + "\n"
2038
+ );
2039
+ }
2040
+
2041
+ // src/commands/apps-init.ts
2042
+ import { promises as fs6 } from "fs";
2043
+ import os3 from "os";
2044
+ import path6 from "path";
2045
+ import AdmZip4 from "adm-zip";
2046
+ var MAX_KIT_BYTES = 8 * 1024 * 1024;
2047
+ function slugify3(input) {
2048
+ const slug = input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
2049
+ return slug || "app";
2050
+ }
2051
+ function titleFromSlug(slug) {
2052
+ return slug.split("-").filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ") || slug;
2053
+ }
2054
+ async function pathExists(filePath) {
2055
+ try {
2056
+ await fs6.access(filePath);
2057
+ return true;
2058
+ } catch {
2059
+ return false;
2060
+ }
2061
+ }
2062
+ async function isEmptyDir(dir) {
2063
+ try {
2064
+ const entries = await fs6.readdir(dir);
2065
+ return entries.length === 0;
2066
+ } catch (err) {
2067
+ if (err.code === "ENOENT") return true;
2068
+ throw err;
2069
+ }
2070
+ }
2071
+ async function findKitRoot(extracted) {
2072
+ const candidates = [
2073
+ path6.join(extracted, "shell-starter-kit"),
2074
+ extracted
2075
+ ];
2076
+ const entries = await fs6.readdir(extracted, { withFileTypes: true });
2077
+ for (const entry of entries) {
2078
+ if (entry.isDirectory()) {
2079
+ candidates.push(path6.join(extracted, entry.name));
2080
+ }
2081
+ }
2082
+ for (const candidate of candidates) {
2083
+ if (await pathExists(path6.join(candidate, "groundfloor.manifest.json"))) {
2084
+ return candidate;
2085
+ }
2086
+ }
2087
+ throw new Error(
2088
+ "Starter-kit ZIP did not contain groundfloor.manifest.json. Re-download with `gf apps init`."
2089
+ );
2090
+ }
2091
+ function stampAppIdentity(source, slug) {
2092
+ if (!/export const APP_ID\s*=/.test(source)) {
2093
+ throw new Error("src/appIdentity.ts does not export APP_ID");
2094
+ }
2095
+ const safe = slug.replace(/['"]/g, "");
2096
+ return source.replace(
2097
+ /export const APP_ID\s*=\s*['"][^'"]*['"]/,
2098
+ `export const APP_ID = '${safe}'`
2099
+ );
2100
+ }
2101
+ function stampManifest(raw, slug, name) {
2102
+ const parsed = JSON.parse(raw);
2103
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2104
+ throw new Error("groundfloor.manifest.json must be a JSON object");
2105
+ }
2106
+ const manifest = parsed;
2107
+ manifest.appId = slug;
2108
+ manifest.name = name;
2109
+ return `${JSON.stringify(manifest, null, 2)}
2110
+ `;
2111
+ }
2112
+ async function downloadKit(url) {
2113
+ let res;
2114
+ try {
2115
+ res = await fetch(url, {
2116
+ redirect: "follow",
2117
+ signal: AbortSignal.timeout(6e4),
2118
+ headers: { "User-Agent": "groundfloor-cli/0.1.1" }
2119
+ });
2120
+ } catch (err) {
2121
+ const message = err instanceof Error ? err.message : String(err);
2122
+ throw new Error(`Could not download starter-kit from ${url}: ${message}`);
2123
+ }
2124
+ if (!res.ok) {
2125
+ throw new Error(
2126
+ `Starter-kit download failed (${res.status}) from ${url}`
2127
+ );
2128
+ }
2129
+ const bytes = Buffer.from(await res.arrayBuffer());
2130
+ if (bytes.byteLength === 0) {
2131
+ throw new Error(`Starter-kit ZIP is empty: ${url}`);
2132
+ }
2133
+ if (bytes.byteLength > MAX_KIT_BYTES) {
2134
+ throw new Error(
2135
+ `Starter-kit ZIP is too large (${bytes.byteLength} bytes) from ${url}`
2136
+ );
2137
+ }
2138
+ if (bytes[0] !== 80 || bytes[1] !== 75) {
2139
+ const head = bytes.subarray(0, 80).toString("utf8");
2140
+ throw new Error(
2141
+ `Expected a ZIP from ${url}, got ${res.headers.get("content-type") ?? "unknown"} (${head.trim()}). Do not scaffold a federated App.tsx from memory.`
2142
+ );
2143
+ }
2144
+ return bytes;
2145
+ }
2146
+ function assertSafeZipEntries(zip) {
2147
+ for (const entry of zip.getEntries()) {
2148
+ const name = entry.entryName.replace(/\\/g, "/");
2149
+ if (name.startsWith("/") || name.includes("..")) {
2150
+ throw new Error(`Refusing starter-kit ZIP with unsafe path: ${name}`);
2151
+ }
2152
+ }
2153
+ }
2154
+ async function appsInitCommand(opts) {
2155
+ const rawSlug = opts.slug?.trim() ?? "";
2156
+ if (!rawSlug) {
2157
+ throw new Error("Pass --slug <portal-slug>.");
2158
+ }
2159
+ const slug = slugify3(rawSlug);
2160
+ if (slug !== rawSlug) {
2161
+ process.stderr.write(`Note: slug normalized to ${slug}
2162
+ `);
2163
+ }
2164
+ const name = opts.name?.trim() || titleFromSlug(slug);
2165
+ const dest = path6.resolve(opts.dir?.trim() || path6.join(process.cwd(), slug));
2166
+ const cfg = await resolveConfig();
2167
+ const cell = cfg.environment ?? "production";
2168
+ const kitUrl = opts.kitUrl?.trim() || starterKitZipUrl(cell);
2169
+ if (!await isEmptyDir(dest) && !opts.force) {
2170
+ throw new Error(
2171
+ `Destination ${dest} is not empty. Pass --force to overwrite, or choose another --dir.`
2172
+ );
2173
+ }
2174
+ if (opts.force && await pathExists(dest)) {
2175
+ await fs6.rm(dest, { recursive: true, force: true });
2176
+ }
2177
+ const bytes = await downloadKit(kitUrl);
2178
+ const zip = new AdmZip4(bytes);
2179
+ assertSafeZipEntries(zip);
2180
+ const tmp = await fs6.mkdtemp(path6.join(os3.tmpdir(), "gf-starter-kit-"));
2181
+ try {
2182
+ zip.extractAllTo(tmp, true);
2183
+ const kitRoot = await findKitRoot(tmp);
2184
+ await fs6.mkdir(dest, { recursive: true });
2185
+ await fs6.cp(kitRoot, dest, { recursive: true, force: true });
2186
+ } finally {
2187
+ await fs6.rm(tmp, { recursive: true, force: true });
2188
+ }
2189
+ const identityPath = path6.join(dest, "src", "appIdentity.ts");
2190
+ const manifestPath = path6.join(dest, "groundfloor.manifest.json");
2191
+ const identity = await fs6.readFile(identityPath, "utf8");
2192
+ await fs6.writeFile(identityPath, stampAppIdentity(identity, slug), "utf8");
2193
+ const manifest = await fs6.readFile(manifestPath, "utf8");
2194
+ await fs6.writeFile(manifestPath, stampManifest(manifest, slug, name), "utf8");
2195
+ const result = {
2196
+ dir: dest,
2197
+ slug,
2198
+ name,
2199
+ kitUrl,
2200
+ next: [
2201
+ `cd ${dest}`,
2202
+ "npm install",
2203
+ `gf apps create --name ${JSON.stringify(name)} --slug ${slug} --kind shell_federated --manifest ./groundfloor.manifest.json`,
2204
+ "npm run release",
2205
+ "gf apps publish --path release.zip"
2206
+ ]
2207
+ };
2208
+ if (opts.json) {
2209
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
2210
+ `);
2211
+ return;
2212
+ }
2213
+ process.stdout.write(`Starter-kit ready in ${dest}
2214
+ `);
2215
+ process.stdout.write(`Stamped APP_ID / manifest appId = ${slug}
2216
+ `);
2217
+ process.stdout.write(
2218
+ "Do not wrap src/App.tsx in BrowserRouter (DevShell already has one).\n"
2219
+ );
2220
+ process.stdout.write("Next:\n");
2221
+ for (const step of result.next) {
2222
+ process.stdout.write(` ${step}
2223
+ `);
2224
+ }
2225
+ }
2226
+
2227
+ // src/commands/dataplane.ts
2228
+ async function dataplaneStatusCommand(opts) {
2229
+ const cfg = await requireWorkspace(opts.workspace);
2230
+ const status = await getDataplaneStatus(cfg.apiUrl, cfg.workspaceId);
2231
+ if (opts.json) {
2232
+ process.stdout.write(`${JSON.stringify(status, null, 2)}
2233
+ `);
2234
+ return;
2235
+ }
2236
+ const ready = status.service_key_configured ? "configured" : "not configured";
2237
+ process.stdout.write(
2238
+ `Dataplane ${ready}${status.dataplane_tenant_id ? ` tenant ${status.dataplane_tenant_id}` : ""}
2239
+ `
2240
+ );
2241
+ }
2242
+ async function dataplaneProvisionCommand(opts) {
2243
+ const cfg = await requireWorkspace(opts.workspace);
2244
+ const result = await provisionDataplane(cfg.apiUrl, cfg.workspaceId);
2245
+ if (opts.json) {
2246
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
2247
+ `);
2248
+ return;
2249
+ }
2250
+ if (result.created) {
2251
+ process.stdout.write("Dataplane provisioned for this workspace.\n");
2252
+ if (result.api_key) {
2253
+ process.stderr.write(
2254
+ "A service key was minted into workspace secrets. It is not printed again.\n"
2255
+ );
2256
+ }
2257
+ } else {
2258
+ process.stdout.write("Dataplane already configured for this workspace.\n");
2259
+ }
2260
+ }
2261
+
1274
2262
  // src/index.ts
1275
2263
  function collect(value, previous) {
1276
2264
  return previous.concat([value]);
@@ -1285,9 +2273,14 @@ function run(fn) {
1285
2273
  }
1286
2274
  var program = new Command();
1287
2275
  program.name("gf").description(
1288
- "Groundfloor CLI \u2014 log in to the Control Plane and reuse the session during local development."
1289
- ).version("0.1.0");
1290
- program.command("login").description("Log in via the browser (or --device) and cache the session").option("--issuer <url>", "Identity provider issuer URL").option("--client-id <id>", "OIDC client id").option("--api-url <url>", "Control Plane base URL").option("--device", "Use the device-code flow (headless / no local browser)").option(
2276
+ "Groundfloor CLI \u2014 sign in, deploy coderunners, publish Shell apps, and manage workspace resources."
2277
+ ).version("0.1.1");
2278
+ program.command("login").description(
2279
+ "Log in via the browser (production by default; --dev / --stage for other cells)"
2280
+ ).option("--dev", "Sign in to Groundfloor dev").option("--stage", "Sign in to Groundfloor stage").option(
2281
+ "--env <name>",
2282
+ "Cell to sign in to: production (default), stage, or dev"
2283
+ ).option("--issuer <url>", "Identity provider issuer URL").option("--client-id <id>", "OIDC client id").option("--api-url <url>", "Control Plane base URL").option("--device", "Use the device-code flow (headless / no local browser)").option(
1291
2284
  "--redirect-host <host>",
1292
2285
  "Loopback host for the redirect URI (default 127.0.0.1; try localhost)"
1293
2286
  ).option(
@@ -1325,8 +2318,75 @@ domains.command("rm <domain>").alias("remove").description("Remove a custom doma
1325
2318
  domains.command("verify <domain>").description("Re-check the DNS records for a custom domain").requiredOption("-c, --coderunner <id|slug>", "Target coderunner id or slug").option("-w, --workspace <id>", "Workspace id override").option("--json", "Output raw JSON").action(
1326
2319
  (domain, opts) => run(() => domainsVerifyCommand(domain, opts))
1327
2320
  );
2321
+ var secrets = program.command("secrets").description("Manage workspace secrets");
2322
+ secrets.command("ls").alias("list").description("List secret keys (values are not shown)").option("-w, --workspace <id>", "Workspace id override").option("-e, --environment <id>", "Environment id").option("--json", "Output raw JSON").action((opts) => run(() => secretsListCommand(opts)));
2323
+ secrets.command("get <key>").description("Print a secret value").option("-w, --workspace <id>", "Workspace id override").option("-e, --environment <id>", "Environment id").option("--json", "Output raw JSON").action((key, opts) => run(() => secretsGetCommand(key, opts)));
2324
+ secrets.command("set <key> [value]").description("Create or update a secret (pass - to read the value from stdin)").option("-w, --workspace <id>", "Workspace id override").option("-e, --environment <id>", "Environment id").option("--description <text>", "Optional description").action(
2325
+ (key, value, opts) => run(() => secretsSetCommand(key, value, opts))
2326
+ );
2327
+ secrets.command("rm <key>").alias("delete").description("Delete a secret").option("-w, --workspace <id>", "Workspace id override").option("-e, --environment <id>", "Environment id").action((key, opts) => run(() => secretsRmCommand(key, opts)));
2328
+ var files = program.command("files").description("Manage workspace files");
2329
+ files.command("ls").alias("list").description("List files").option("-w, --workspace <id>", "Workspace id override").option("--json", "Output raw JSON").action((opts) => run(() => filesListCommand(opts)));
2330
+ files.command("upload <path>").description("Upload a local file").option("-w, --workspace <id>", "Workspace id override").option("-n, --name <name>", "Display name (default: basename)").action(
2331
+ (filePath, opts) => run(() => filesUploadCommand(filePath, opts))
2332
+ );
2333
+ files.command("download <fileId>").description("Download a file").option("-w, --workspace <id>", "Workspace id override").option("-o, --out <path>", "Output path").action(
2334
+ (fileId, opts) => run(() => filesDownloadCommand(fileId, opts))
2335
+ );
2336
+ files.command("rm <fileId>").alias("delete").description("Delete a file").option("-w, --workspace <id>", "Workspace id override").action((fileId, opts) => run(() => filesRmCommand(fileId, opts)));
2337
+ var apps = program.command("apps").description(
2338
+ "Product apps (Shell federated / standalone) \u2014 init, create, list, publish"
2339
+ );
2340
+ apps.command("ls").alias("list").description("List apps").option("-w, --workspace <id>", "Workspace id override").option("--json", "Output raw JSON").action((opts) => run(() => appsListCommand(opts)));
2341
+ apps.command("get <appId>").description("Show one app (id or slug)").option("-w, --workspace <id>", "Workspace id override").option("--json", "Output raw JSON").action((appId, opts) => run(() => appsGetCommand(appId, opts)));
2342
+ apps.command("init").description(
2343
+ "Download the official Shell starter-kit and stamp APP_ID / manifest appId"
2344
+ ).requiredOption(
2345
+ "--slug <slug>",
2346
+ "Portal app slug (written to APP_ID and groundfloor.manifest.json appId)"
2347
+ ).option("-n, --name <name>", "Display name (default: title-cased slug)").option(
2348
+ "-d, --dir <path>",
2349
+ "Destination directory (default: ./<slug>)"
2350
+ ).option("--kit-url <url>", "Override starter-kit ZIP URL").option("--force", "Overwrite an existing destination directory").option("--json", "Output raw JSON").argument("[dir]", "Destination directory (same as --dir)").action(
2351
+ (dir, opts) => run(
2352
+ () => appsInitCommand({
2353
+ slug: opts.slug,
2354
+ name: opts.name,
2355
+ dir: opts.dir || dir,
2356
+ kitUrl: opts.kitUrl,
2357
+ force: opts.force,
2358
+ json: opts.json
2359
+ })
2360
+ )
2361
+ );
2362
+ apps.command("create").description("Register a Shell federated or standalone product app").option("-w, --workspace <id>", "Workspace id override").option("-n, --name <name>", "Display name").option("--slug <slug>", "URL-safe slug (unique in the workspace)").option(
2363
+ "--kind <kind>",
2364
+ "shell_federated (default) or standalone",
2365
+ "shell_federated"
2366
+ ).option(
2367
+ "--primary-coderunner <id>",
2368
+ "Required for --kind standalone: existing service coderunner id"
2369
+ ).option("-e, --environment <id>", "Environment id to bind").option(
2370
+ "--manifest <path>",
2371
+ "Path to groundfloor.manifest.json (Shell apps)"
2372
+ ).option("--json", "Output raw JSON").action((opts) => run(() => appsCreateCommand(opts)));
2373
+ apps.command("publish [appId]").description(
2374
+ "Upload release.zip (or remoteEntry.js) and publish a Shell app"
2375
+ ).option("-w, --workspace <id>", "Workspace id override").option("--app <id|slug>", "App id or slug (else manifest appId)").option(
2376
+ "-p, --path <file>",
2377
+ "release.zip, remoteEntry.js, or a folder containing release.zip",
2378
+ "release.zip"
2379
+ ).option("-m, --label <text>", "Optional release label").option(
2380
+ "--no-sync-manifest",
2381
+ "Do not PATCH the Portal manifest from groundfloor.manifest.json"
2382
+ ).option("--json", "Output raw JSON").action(
2383
+ (appId, opts) => run(() => appsPublishCommand(appId, opts))
2384
+ );
2385
+ var dataplane = program.command("dataplane").description("Workspace Dataplane setup (required for vault, files, secrets)");
2386
+ dataplane.command("status").description("Show Dataplane setup status").option("-w, --workspace <id>", "Workspace id override").option("--json", "Output raw JSON").action((opts) => run(() => dataplaneStatusCommand(opts)));
2387
+ dataplane.command("provision").description("Provision Dataplane for this workspace (idempotent)").option("-w, --workspace <id>", "Workspace id override").option("--json", "Output raw JSON").action((opts) => run(() => dataplaneProvisionCommand(opts)));
1328
2388
  program.command("deploy").description(
1329
- "Package code (local folder or --git repo) and deploy as a standalone coderunner (D-061; not an App)"
2389
+ "Package code and deploy as a coderunner (not an App)"
1330
2390
  ).option("-w, --workspace <id>", "Workspace id override").option("--api-url <url>", "Control Plane base URL").option(
1331
2391
  "-c, --coderunner <id>",
1332
2392
  "Deploy into an existing coderunner id (skip find/create)"