@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.
- package/README.md +137 -114
- package/dist/index.js +1090 -30
- 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
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
78
|
-
const
|
|
79
|
-
const
|
|
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(
|
|
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 ${
|
|
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,
|
|
536
|
+
async function cpGet(apiUrl, path7) {
|
|
459
537
|
const token = await getValidAccessToken();
|
|
460
|
-
const res = await fetch(`${apiUrl}${
|
|
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,
|
|
549
|
+
async function cpPostJson(apiUrl, path7, body) {
|
|
472
550
|
const token = await getValidAccessToken();
|
|
473
|
-
const res = await fetch(`${apiUrl}${
|
|
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,
|
|
565
|
+
async function cpDelete(apiUrl, path7) {
|
|
488
566
|
const token = await getValidAccessToken();
|
|
489
|
-
const res = await fetch(`${apiUrl}${
|
|
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,
|
|
1074
|
+
async function buildZip(sourceDir, files2) {
|
|
807
1075
|
const zip = new AdmZip();
|
|
808
|
-
for (const rel of
|
|
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
|
|
860
|
-
if (
|
|
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,
|
|
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:
|
|
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
|
|
1289
|
-
).version("0.1.
|
|
1290
|
-
program.command("login").description(
|
|
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
|
|
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)"
|