@ait-co/console-cli 0.1.14 → 0.1.16
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/dist/cli.mjs +1157 -39
- package/dist/cli.mjs.map +1 -1
- package/package.json +2 -1
package/dist/cli.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { defineCommand, runMain } from "citty";
|
|
|
3
3
|
import { access, chmod, mkdir, mkdtemp, readFile, rename, rm, unlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import { basename, dirname, isAbsolute, join, resolve, win32 } from "node:path";
|
|
5
5
|
import { homedir, tmpdir } from "node:os";
|
|
6
|
+
import { unzipSync } from "fflate";
|
|
6
7
|
import { parse } from "yaml";
|
|
7
8
|
import { imageSize } from "image-size";
|
|
8
9
|
import { spawn } from "node:child_process";
|
|
@@ -516,6 +517,138 @@ async function fetchDeployedBundle(workspaceId, miniAppId, cookies, opts = {}) {
|
|
|
516
517
|
if (typeof raw !== "object" || Array.isArray(raw)) throw new Error(`Unexpected deployed-bundle shape for app=${miniAppId}`);
|
|
517
518
|
return raw;
|
|
518
519
|
}
|
|
520
|
+
async function postDeploymentsInitialize(workspaceId, miniAppId, deploymentId, cookies, opts = {}) {
|
|
521
|
+
const raw = await requestConsoleApi({
|
|
522
|
+
method: "POST",
|
|
523
|
+
url: `${BASE$4}/workspaces/${workspaceId}/mini-app/${miniAppId}/deployments/initialize`,
|
|
524
|
+
body: { deploymentId },
|
|
525
|
+
cookies,
|
|
526
|
+
...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
|
|
527
|
+
});
|
|
528
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new Error(`Unexpected deployments/initialize shape for app=${miniAppId}`);
|
|
529
|
+
const data = raw;
|
|
530
|
+
const uploadUrl = data.uploadUrl;
|
|
531
|
+
if (typeof uploadUrl !== "string") throw new Error(`Unexpected deployments/initialize shape for app=${miniAppId}: missing uploadUrl`);
|
|
532
|
+
const deployment = data.deployment && typeof data.deployment === "object" ? data.deployment : {};
|
|
533
|
+
return {
|
|
534
|
+
uploadUrl,
|
|
535
|
+
deployment,
|
|
536
|
+
reviewStatus: typeof deployment.reviewStatus === "string" ? deployment.reviewStatus : "UNKNOWN"
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
async function postDeploymentsComplete(workspaceId, miniAppId, deploymentId, cookies, opts = {}) {
|
|
540
|
+
const raw = await requestConsoleApi({
|
|
541
|
+
method: "POST",
|
|
542
|
+
url: `${BASE$4}/workspaces/${workspaceId}/mini-app/${miniAppId}/deployments/complete`,
|
|
543
|
+
body: { deploymentId },
|
|
544
|
+
cookies,
|
|
545
|
+
...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
|
|
546
|
+
});
|
|
547
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
548
|
+
return raw;
|
|
549
|
+
}
|
|
550
|
+
async function postBundleMemo(workspaceId, miniAppId, deploymentId, memo, cookies, opts = {}) {
|
|
551
|
+
const raw = await requestConsoleApi({
|
|
552
|
+
method: "POST",
|
|
553
|
+
url: `${BASE$4}/workspaces/${workspaceId}/mini-app/${miniAppId}/bundles/memos`,
|
|
554
|
+
body: {
|
|
555
|
+
deploymentId,
|
|
556
|
+
memo
|
|
557
|
+
},
|
|
558
|
+
cookies,
|
|
559
|
+
...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
|
|
560
|
+
});
|
|
561
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
562
|
+
return raw;
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* PUT the raw .ait bytes to the S3 presigned URL returned by
|
|
566
|
+
* `postDeploymentsInitialize`. This is a direct-to-S3 call with NO Toss
|
|
567
|
+
* envelope — the response is empty on success (HTTP 200). Any cookies are
|
|
568
|
+
* intentionally NOT sent because S3 would reject the signed request if
|
|
569
|
+
* extra auth headers contradict the signature.
|
|
570
|
+
*/
|
|
571
|
+
async function putBundleToUploadUrl(uploadUrl, body, opts = {}) {
|
|
572
|
+
const impl = opts.fetchImpl ?? ((i, init) => fetch(i, init));
|
|
573
|
+
const view = new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
|
|
574
|
+
let res;
|
|
575
|
+
try {
|
|
576
|
+
res = await impl(uploadUrl, {
|
|
577
|
+
method: "PUT",
|
|
578
|
+
headers: { "Content-Type": "application/zip" },
|
|
579
|
+
body: view
|
|
580
|
+
});
|
|
581
|
+
} catch (err) {
|
|
582
|
+
throw new Error(`PUT to upload URL failed: ${err.message}`);
|
|
583
|
+
}
|
|
584
|
+
if (!res.ok) {
|
|
585
|
+
const preview = await res.text().catch(() => "");
|
|
586
|
+
throw new Error(`PUT to upload URL returned HTTP ${res.status}: ${preview.slice(0, 200)}`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
async function postBundleReview(params, cookies, opts = {}) {
|
|
590
|
+
const url = `${BASE$4}/workspaces/${params.workspaceId}/mini-app/${params.miniAppId}/bundles/reviews`;
|
|
591
|
+
const body = {
|
|
592
|
+
deploymentId: params.deploymentId,
|
|
593
|
+
releaseNotes: params.releaseNotes
|
|
594
|
+
};
|
|
595
|
+
if (params.featureList !== void 0) body.featureList = params.featureList;
|
|
596
|
+
if (params.screenshotImagePaths !== void 0) body.screenshotImagePaths = params.screenshotImagePaths;
|
|
597
|
+
const raw = await requestConsoleApi({
|
|
598
|
+
method: "POST",
|
|
599
|
+
url,
|
|
600
|
+
body,
|
|
601
|
+
cookies,
|
|
602
|
+
...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
|
|
603
|
+
});
|
|
604
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
605
|
+
return raw;
|
|
606
|
+
}
|
|
607
|
+
async function postBundleReviewWithdrawal(workspaceId, miniAppId, deploymentId, cookies, opts = {}) {
|
|
608
|
+
const raw = await requestConsoleApi({
|
|
609
|
+
method: "POST",
|
|
610
|
+
url: `${BASE$4}/workspaces/${workspaceId}/mini-app/${miniAppId}/bundles/reviews/withdrawal`,
|
|
611
|
+
body: { deploymentId },
|
|
612
|
+
cookies,
|
|
613
|
+
...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
|
|
614
|
+
});
|
|
615
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
616
|
+
return raw;
|
|
617
|
+
}
|
|
618
|
+
async function postBundleRelease(params, cookies, opts = {}) {
|
|
619
|
+
const url = `${BASE$4}/workspaces/${params.workspaceId}/mini-app/${params.miniAppId}/bundles/release`;
|
|
620
|
+
const body = { deploymentId: params.deploymentId };
|
|
621
|
+
if (params.contentImages !== void 0) body.contentImages = params.contentImages;
|
|
622
|
+
const raw = await requestConsoleApi({
|
|
623
|
+
method: "POST",
|
|
624
|
+
url,
|
|
625
|
+
body,
|
|
626
|
+
cookies,
|
|
627
|
+
...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
|
|
628
|
+
});
|
|
629
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
630
|
+
return raw;
|
|
631
|
+
}
|
|
632
|
+
async function postBundleTestPush(workspaceId, miniAppId, deploymentId, cookies, opts = {}) {
|
|
633
|
+
const raw = await requestConsoleApi({
|
|
634
|
+
method: "POST",
|
|
635
|
+
url: `${BASE$4}/workspaces/${workspaceId}/mini-app/${miniAppId}/bundles/test-push`,
|
|
636
|
+
body: { deploymentId },
|
|
637
|
+
cookies,
|
|
638
|
+
...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
|
|
639
|
+
});
|
|
640
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
641
|
+
return raw;
|
|
642
|
+
}
|
|
643
|
+
async function fetchBundleTestLinks(workspaceId, miniAppId, cookies, opts = {}) {
|
|
644
|
+
const raw = await requestConsoleApi({
|
|
645
|
+
url: `${BASE$4}/workspaces/${workspaceId}/mini-app/${miniAppId}/bundles/test-links`,
|
|
646
|
+
cookies,
|
|
647
|
+
...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
|
|
648
|
+
});
|
|
649
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
650
|
+
return raw;
|
|
651
|
+
}
|
|
519
652
|
async function createMiniApp(workspaceId, payload, cookies, opts = {}) {
|
|
520
653
|
return normalizeCreateResult(await requestConsoleApi({
|
|
521
654
|
url: `${BASE$4}/workspaces/${workspaceId}/mini-app/review`,
|
|
@@ -873,6 +1006,460 @@ async function requireSession(json) {
|
|
|
873
1006
|
return session;
|
|
874
1007
|
}
|
|
875
1008
|
//#endregion
|
|
1009
|
+
//#region src/config/ait-bundle.ts
|
|
1010
|
+
var AitBundleError = class extends Error {
|
|
1011
|
+
path;
|
|
1012
|
+
reason;
|
|
1013
|
+
constructor(args) {
|
|
1014
|
+
super(args.message);
|
|
1015
|
+
this.name = "AitBundleError";
|
|
1016
|
+
this.path = args.path;
|
|
1017
|
+
this.reason = args.reason;
|
|
1018
|
+
}
|
|
1019
|
+
};
|
|
1020
|
+
const AIT_MAGIC = new Uint8Array([
|
|
1021
|
+
65,
|
|
1022
|
+
73,
|
|
1023
|
+
84,
|
|
1024
|
+
66,
|
|
1025
|
+
85,
|
|
1026
|
+
78,
|
|
1027
|
+
68,
|
|
1028
|
+
76
|
|
1029
|
+
]);
|
|
1030
|
+
const ZIP_MAGIC = new Uint8Array([
|
|
1031
|
+
80,
|
|
1032
|
+
75,
|
|
1033
|
+
3,
|
|
1034
|
+
4
|
|
1035
|
+
]);
|
|
1036
|
+
function startsWith(bytes, prefix) {
|
|
1037
|
+
if (bytes.length < prefix.length) return false;
|
|
1038
|
+
for (let i = 0; i < prefix.length; i++) if (bytes[i] !== prefix[i]) return false;
|
|
1039
|
+
return true;
|
|
1040
|
+
}
|
|
1041
|
+
function detectBundleFormat(bytes) {
|
|
1042
|
+
if (startsWith(bytes, AIT_MAGIC)) return "ait";
|
|
1043
|
+
if (startsWith(bytes, ZIP_MAGIC)) return "zip";
|
|
1044
|
+
return "unknown";
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Read the `.ait` at `path` and pull out `deploymentId`, auto-detecting
|
|
1048
|
+
* whether the file is the modern AIT header format or a legacy plain
|
|
1049
|
+
* zip. Returns the raw bytes so the caller can forward them to S3
|
|
1050
|
+
* without re-reading the file.
|
|
1051
|
+
*/
|
|
1052
|
+
async function readAitBundle(path) {
|
|
1053
|
+
let buf;
|
|
1054
|
+
try {
|
|
1055
|
+
buf = await readFile(path);
|
|
1056
|
+
} catch (err) {
|
|
1057
|
+
throw new AitBundleError({
|
|
1058
|
+
path,
|
|
1059
|
+
reason: "file-unreadable",
|
|
1060
|
+
message: err.message
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
const bytes = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
1064
|
+
const { deploymentId, format } = deploymentIdFromBundleBytes(bytes, path);
|
|
1065
|
+
return {
|
|
1066
|
+
deploymentId,
|
|
1067
|
+
bytes,
|
|
1068
|
+
format
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Pure helper split out so tests can feed raw bytes without a tmp file.
|
|
1073
|
+
* Throws `AitBundleError` on any parse failure. Returns the detected
|
|
1074
|
+
* format so callers that want to log it can.
|
|
1075
|
+
*/
|
|
1076
|
+
function deploymentIdFromBundleBytes(bytes, pathForError) {
|
|
1077
|
+
const format = detectBundleFormat(bytes);
|
|
1078
|
+
if (format === "ait") return {
|
|
1079
|
+
deploymentId: deploymentIdFromAitHeader(bytes, pathForError),
|
|
1080
|
+
format
|
|
1081
|
+
};
|
|
1082
|
+
if (format === "zip") return {
|
|
1083
|
+
deploymentId: deploymentIdFromLegacyZip(bytes, pathForError),
|
|
1084
|
+
format
|
|
1085
|
+
};
|
|
1086
|
+
throw new AitBundleError({
|
|
1087
|
+
path: pathForError,
|
|
1088
|
+
reason: "unrecognized-format",
|
|
1089
|
+
message: "bundle does not start with AITBUNDL or PK magic bytes — not a valid .ait or legacy zip bundle"
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
const AIT_MAGIC_SIZE = 8;
|
|
1093
|
+
const AIT_VERSION_SIZE = 4;
|
|
1094
|
+
const AIT_HEADER_SIZE = AIT_MAGIC_SIZE + AIT_VERSION_SIZE + 8;
|
|
1095
|
+
function deploymentIdFromAitHeader(bytes, pathForError) {
|
|
1096
|
+
if (bytes.length < AIT_HEADER_SIZE) throw new AitBundleError({
|
|
1097
|
+
path: pathForError,
|
|
1098
|
+
reason: "invalid-ait",
|
|
1099
|
+
message: "buffer too small to be a valid AIT file"
|
|
1100
|
+
});
|
|
1101
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1102
|
+
const bundleLen = Number(view.getBigUint64(AIT_MAGIC_SIZE + AIT_VERSION_SIZE, false));
|
|
1103
|
+
if (!Number.isFinite(bundleLen) || bundleLen <= 0) throw new AitBundleError({
|
|
1104
|
+
path: pathForError,
|
|
1105
|
+
reason: "invalid-ait",
|
|
1106
|
+
message: `AIT bundle length is invalid (${bundleLen})`
|
|
1107
|
+
});
|
|
1108
|
+
const bundleStart = AIT_HEADER_SIZE;
|
|
1109
|
+
const bundleEnd = bundleStart + bundleLen;
|
|
1110
|
+
if (bytes.length < bundleEnd) throw new AitBundleError({
|
|
1111
|
+
path: pathForError,
|
|
1112
|
+
reason: "invalid-ait",
|
|
1113
|
+
message: "unexpected end of buffer reading AIT bundle protobuf"
|
|
1114
|
+
});
|
|
1115
|
+
const deploymentId = readProtobufStringFields(bytes.subarray(bundleStart, bundleEnd), [2, 3]).get(2);
|
|
1116
|
+
if (typeof deploymentId !== "string" || deploymentId === "") throw new AitBundleError({
|
|
1117
|
+
path: pathForError,
|
|
1118
|
+
reason: "missing-deployment-id",
|
|
1119
|
+
message: "AIT bundle protobuf is missing deploymentId (field 2)"
|
|
1120
|
+
});
|
|
1121
|
+
return deploymentId;
|
|
1122
|
+
}
|
|
1123
|
+
function readProtobufStringFields(bytes, wantedFieldNumbers) {
|
|
1124
|
+
const wanted = new Set(wantedFieldNumbers);
|
|
1125
|
+
const out = /* @__PURE__ */ new Map();
|
|
1126
|
+
const decoder = new TextDecoder("utf-8", { fatal: false });
|
|
1127
|
+
let offset = 0;
|
|
1128
|
+
while (offset < bytes.length) {
|
|
1129
|
+
const { value: tag, next } = readVarint(bytes, offset);
|
|
1130
|
+
offset = next;
|
|
1131
|
+
const fieldNumber = Number(tag >> 3n);
|
|
1132
|
+
const wireType = Number(tag & 7n);
|
|
1133
|
+
if (wireType === 0) offset = readVarint(bytes, offset).next;
|
|
1134
|
+
else if (wireType === 1) offset += 8;
|
|
1135
|
+
else if (wireType === 2) {
|
|
1136
|
+
const { value: len, next: afterLen } = readVarint(bytes, offset);
|
|
1137
|
+
offset = afterLen;
|
|
1138
|
+
const payloadEnd = offset + Number(len);
|
|
1139
|
+
if (payloadEnd > bytes.length) break;
|
|
1140
|
+
if (wanted.has(fieldNumber)) out.set(fieldNumber, decoder.decode(bytes.subarray(offset, payloadEnd)));
|
|
1141
|
+
offset = payloadEnd;
|
|
1142
|
+
} else if (wireType === 5) offset += 4;
|
|
1143
|
+
else break;
|
|
1144
|
+
}
|
|
1145
|
+
return out;
|
|
1146
|
+
}
|
|
1147
|
+
function readVarint(bytes, start) {
|
|
1148
|
+
let value = 0n;
|
|
1149
|
+
let shift = 0n;
|
|
1150
|
+
let i = start;
|
|
1151
|
+
for (let n = 0; n < 10 && i < bytes.length; n++, i++) {
|
|
1152
|
+
const byte = bytes[i];
|
|
1153
|
+
if (byte === void 0) break;
|
|
1154
|
+
value |= BigInt(byte & 127) << shift;
|
|
1155
|
+
if ((byte & 128) === 0) return {
|
|
1156
|
+
value,
|
|
1157
|
+
next: i + 1
|
|
1158
|
+
};
|
|
1159
|
+
shift += 7n;
|
|
1160
|
+
}
|
|
1161
|
+
return {
|
|
1162
|
+
value,
|
|
1163
|
+
next: i
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
function deploymentIdFromLegacyZip(bytes, pathForError) {
|
|
1167
|
+
let entries;
|
|
1168
|
+
try {
|
|
1169
|
+
entries = unzipSync(bytes, { filter: (file) => file.name === "app.json" });
|
|
1170
|
+
} catch (err) {
|
|
1171
|
+
throw new AitBundleError({
|
|
1172
|
+
path: pathForError,
|
|
1173
|
+
reason: "invalid-zip",
|
|
1174
|
+
message: `not a valid zip: ${err.message}`
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
const entry = entries["app.json"];
|
|
1178
|
+
if (!entry) throw new AitBundleError({
|
|
1179
|
+
path: pathForError,
|
|
1180
|
+
reason: "missing-app-json",
|
|
1181
|
+
message: "app.json is not present at the root of the bundle"
|
|
1182
|
+
});
|
|
1183
|
+
let parsed;
|
|
1184
|
+
try {
|
|
1185
|
+
parsed = JSON.parse(new TextDecoder().decode(entry));
|
|
1186
|
+
} catch (err) {
|
|
1187
|
+
throw new AitBundleError({
|
|
1188
|
+
path: pathForError,
|
|
1189
|
+
reason: "invalid-app-json",
|
|
1190
|
+
message: `app.json is not valid JSON: ${err.message}`
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new AitBundleError({
|
|
1194
|
+
path: pathForError,
|
|
1195
|
+
reason: "invalid-app-json",
|
|
1196
|
+
message: "app.json is not a JSON object"
|
|
1197
|
+
});
|
|
1198
|
+
const metadata = parsed._metadata;
|
|
1199
|
+
if (metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) throw new AitBundleError({
|
|
1200
|
+
path: pathForError,
|
|
1201
|
+
reason: "missing-deployment-id",
|
|
1202
|
+
message: "app.json._metadata is missing; is your build outputting the modern app.json schema?"
|
|
1203
|
+
});
|
|
1204
|
+
const deploymentId = metadata.deploymentId;
|
|
1205
|
+
if (typeof deploymentId !== "string" || deploymentId === "") throw new AitBundleError({
|
|
1206
|
+
path: pathForError,
|
|
1207
|
+
reason: "missing-deployment-id",
|
|
1208
|
+
message: "app.json._metadata.deploymentId is missing or empty; is your build outputting the modern app.json schema?"
|
|
1209
|
+
});
|
|
1210
|
+
return deploymentId;
|
|
1211
|
+
}
|
|
1212
|
+
//#endregion
|
|
1213
|
+
//#region src/commands/app-deploy.ts
|
|
1214
|
+
function parseAppIdStrict(raw) {
|
|
1215
|
+
if (raw === "") return null;
|
|
1216
|
+
if (!/^[1-9]\d*$/.test(raw)) return null;
|
|
1217
|
+
const n = Number.parseInt(raw, 10);
|
|
1218
|
+
return Number.isSafeInteger(n) ? n : null;
|
|
1219
|
+
}
|
|
1220
|
+
async function runDeploy(args, deps = {}) {
|
|
1221
|
+
if (typeof args.app !== "string" || args.app === "") {
|
|
1222
|
+
if (args.json) emitJson({
|
|
1223
|
+
ok: false,
|
|
1224
|
+
reason: "missing-app-id",
|
|
1225
|
+
message: "--app <id> is required"
|
|
1226
|
+
});
|
|
1227
|
+
else process.stderr.write("app deploy: --app <id> is required.\n");
|
|
1228
|
+
return exitAfterFlush(ExitCode.Usage);
|
|
1229
|
+
}
|
|
1230
|
+
const appId = parseAppIdStrict(args.app);
|
|
1231
|
+
if (appId === null) {
|
|
1232
|
+
if (args.json) emitJson({
|
|
1233
|
+
ok: false,
|
|
1234
|
+
reason: "invalid-id",
|
|
1235
|
+
message: `--app must be a positive integer (got ${JSON.stringify(args.app)})`
|
|
1236
|
+
});
|
|
1237
|
+
else process.stderr.write(`app deploy: invalid --app ${JSON.stringify(args.app)}\n`);
|
|
1238
|
+
return exitAfterFlush(ExitCode.Usage);
|
|
1239
|
+
}
|
|
1240
|
+
if (typeof args.path !== "string" || args.path === "") {
|
|
1241
|
+
if (args.json) emitJson({
|
|
1242
|
+
ok: false,
|
|
1243
|
+
reason: "missing-path",
|
|
1244
|
+
message: "path to .ait bundle is required"
|
|
1245
|
+
});
|
|
1246
|
+
else process.stderr.write("app deploy: path to .ait bundle is required.\n");
|
|
1247
|
+
return exitAfterFlush(ExitCode.Usage);
|
|
1248
|
+
}
|
|
1249
|
+
const requestReview = Boolean(args.requestReview);
|
|
1250
|
+
const release = Boolean(args.release);
|
|
1251
|
+
const confirm = Boolean(args.confirm);
|
|
1252
|
+
const releaseNotes = typeof args.releaseNotes === "string" ? args.releaseNotes : void 0;
|
|
1253
|
+
if (requestReview && releaseNotes === void 0) {
|
|
1254
|
+
if (args.json) emitJson({
|
|
1255
|
+
ok: false,
|
|
1256
|
+
reason: "missing-release-notes",
|
|
1257
|
+
message: "--release-notes <text> is required with --request-review"
|
|
1258
|
+
});
|
|
1259
|
+
else process.stderr.write("app deploy: --release-notes <text> is required with --request-review.\n");
|
|
1260
|
+
return exitAfterFlush(ExitCode.Usage);
|
|
1261
|
+
}
|
|
1262
|
+
if (release && !confirm) {
|
|
1263
|
+
if (args.json) emitJson({
|
|
1264
|
+
ok: false,
|
|
1265
|
+
reason: "not-confirmed",
|
|
1266
|
+
message: "--release is destructive; pass --confirm to proceed"
|
|
1267
|
+
});
|
|
1268
|
+
else process.stderr.write("app deploy: --release publishes the bundle to end users.\n Re-run with --confirm to proceed.\n");
|
|
1269
|
+
return exitAfterFlush(ExitCode.Usage);
|
|
1270
|
+
}
|
|
1271
|
+
const readBundle = deps.readBundleImpl ?? readAitBundle;
|
|
1272
|
+
let bundleInfo;
|
|
1273
|
+
try {
|
|
1274
|
+
bundleInfo = await readBundle(args.path);
|
|
1275
|
+
} catch (err) {
|
|
1276
|
+
if (err instanceof AitBundleError) {
|
|
1277
|
+
const reason = err.reason === "file-unreadable" ? "file-unreadable" : "invalid-bundle";
|
|
1278
|
+
if (args.json) emitJson({
|
|
1279
|
+
ok: false,
|
|
1280
|
+
reason,
|
|
1281
|
+
path: err.path,
|
|
1282
|
+
bundleReason: err.reason,
|
|
1283
|
+
message: err.message
|
|
1284
|
+
});
|
|
1285
|
+
else process.stderr.write(`app deploy: ${err.message}\n`);
|
|
1286
|
+
return exitAfterFlush(ExitCode.Usage);
|
|
1287
|
+
}
|
|
1288
|
+
throw err;
|
|
1289
|
+
}
|
|
1290
|
+
const deploymentId = typeof args.deploymentId === "string" && args.deploymentId !== "" ? args.deploymentId : bundleInfo.deploymentId;
|
|
1291
|
+
if (deploymentId === "") {
|
|
1292
|
+
if (args.json) emitJson({
|
|
1293
|
+
ok: false,
|
|
1294
|
+
reason: "invalid-bundle",
|
|
1295
|
+
path: args.path,
|
|
1296
|
+
message: "deploymentId is empty"
|
|
1297
|
+
});
|
|
1298
|
+
else process.stderr.write("app deploy: deploymentId is empty.\n");
|
|
1299
|
+
return exitAfterFlush(ExitCode.Usage);
|
|
1300
|
+
}
|
|
1301
|
+
const ctx = await resolveWorkspaceContext(args);
|
|
1302
|
+
if (!ctx) return;
|
|
1303
|
+
const { session, workspaceId } = ctx;
|
|
1304
|
+
const memo = typeof args.memo === "string" && args.memo.length > 0 ? args.memo : void 0;
|
|
1305
|
+
const steps = ["upload"];
|
|
1306
|
+
if (requestReview) steps.push("review");
|
|
1307
|
+
if (release) steps.push("release");
|
|
1308
|
+
if (args.dryRun) {
|
|
1309
|
+
if (args.json) emitJson({
|
|
1310
|
+
ok: true,
|
|
1311
|
+
dryRun: true,
|
|
1312
|
+
workspaceId,
|
|
1313
|
+
appId,
|
|
1314
|
+
deploymentId,
|
|
1315
|
+
bundleFormat: bundleInfo.format,
|
|
1316
|
+
bytes: bundleInfo.bytes.byteLength,
|
|
1317
|
+
steps,
|
|
1318
|
+
memo: memo ?? null,
|
|
1319
|
+
releaseNotes: releaseNotes ?? null,
|
|
1320
|
+
confirmed: confirm
|
|
1321
|
+
});
|
|
1322
|
+
else {
|
|
1323
|
+
const stepsLine = steps.map((s) => {
|
|
1324
|
+
if (s === "review") return `review (releaseNotes: ${JSON.stringify(releaseNotes ?? "")})`;
|
|
1325
|
+
if (s === "release") return `release (${confirm ? "confirmed" : "NOT confirmed"})`;
|
|
1326
|
+
return s;
|
|
1327
|
+
}).join(" → ");
|
|
1328
|
+
process.stdout.write(`DRY RUN\n app ${appId}\n workspace ${workspaceId}\n bundle ${args.path} (${bundleInfo.bytes.byteLength} bytes)\n deploymentId ${deploymentId}\n memo ${memo ?? "(none)"}\n steps ${stepsLine}\n`);
|
|
1329
|
+
}
|
|
1330
|
+
return exitAfterFlush(ExitCode.Ok);
|
|
1331
|
+
}
|
|
1332
|
+
const apiOpts = deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {};
|
|
1333
|
+
let uploaded = false;
|
|
1334
|
+
let bundleRecord = null;
|
|
1335
|
+
let reviewed = false;
|
|
1336
|
+
let reviewResult = null;
|
|
1337
|
+
try {
|
|
1338
|
+
const init = await postDeploymentsInitialize(workspaceId, appId, deploymentId, session.cookies, apiOpts);
|
|
1339
|
+
if (init.reviewStatus !== "PREPARE") {
|
|
1340
|
+
if (args.json) emitJson({
|
|
1341
|
+
ok: false,
|
|
1342
|
+
reason: "bundle-not-prepare",
|
|
1343
|
+
workspaceId,
|
|
1344
|
+
appId,
|
|
1345
|
+
deploymentId,
|
|
1346
|
+
reviewStatus: init.reviewStatus,
|
|
1347
|
+
message: "이미 존재하는 버전이에요."
|
|
1348
|
+
});
|
|
1349
|
+
else process.stderr.write(`app deploy: deployment ${deploymentId} is already in state ${init.reviewStatus}; upload refused.\n`);
|
|
1350
|
+
return exitAfterFlush(ExitCode.Usage);
|
|
1351
|
+
}
|
|
1352
|
+
await putBundleToUploadUrl(init.uploadUrl, bundleInfo.bytes, apiOpts);
|
|
1353
|
+
bundleRecord = await postDeploymentsComplete(workspaceId, appId, deploymentId, session.cookies, apiOpts);
|
|
1354
|
+
if (memo !== void 0) await postBundleMemo(workspaceId, appId, deploymentId, memo, session.cookies, apiOpts);
|
|
1355
|
+
uploaded = true;
|
|
1356
|
+
} catch (err) {
|
|
1357
|
+
return emitFailureFromError(args.json, err);
|
|
1358
|
+
}
|
|
1359
|
+
if (requestReview) try {
|
|
1360
|
+
reviewResult = await postBundleReview({
|
|
1361
|
+
workspaceId,
|
|
1362
|
+
miniAppId: appId,
|
|
1363
|
+
deploymentId,
|
|
1364
|
+
releaseNotes: releaseNotes ?? ""
|
|
1365
|
+
}, session.cookies, apiOpts);
|
|
1366
|
+
reviewed = true;
|
|
1367
|
+
} catch (err) {
|
|
1368
|
+
return emitPartialFailure(args.json, err, {
|
|
1369
|
+
workspaceId,
|
|
1370
|
+
appId,
|
|
1371
|
+
deploymentId,
|
|
1372
|
+
uploaded: true,
|
|
1373
|
+
reviewed: false,
|
|
1374
|
+
released: false
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
let releaseResult = null;
|
|
1378
|
+
if (release) try {
|
|
1379
|
+
releaseResult = await postBundleRelease({
|
|
1380
|
+
workspaceId,
|
|
1381
|
+
miniAppId: appId,
|
|
1382
|
+
deploymentId
|
|
1383
|
+
}, session.cookies, apiOpts);
|
|
1384
|
+
} catch (err) {
|
|
1385
|
+
return emitPartialFailure(args.json, err, {
|
|
1386
|
+
workspaceId,
|
|
1387
|
+
appId,
|
|
1388
|
+
deploymentId,
|
|
1389
|
+
uploaded: true,
|
|
1390
|
+
reviewed,
|
|
1391
|
+
released: false
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
if (args.json) {
|
|
1395
|
+
emitJson({
|
|
1396
|
+
ok: true,
|
|
1397
|
+
workspaceId,
|
|
1398
|
+
appId,
|
|
1399
|
+
deploymentId,
|
|
1400
|
+
bundleFormat: bundleInfo.format,
|
|
1401
|
+
uploaded,
|
|
1402
|
+
reviewed,
|
|
1403
|
+
released: release,
|
|
1404
|
+
bundle: bundleRecord,
|
|
1405
|
+
reviewResult,
|
|
1406
|
+
releaseResult
|
|
1407
|
+
});
|
|
1408
|
+
return exitAfterFlush(ExitCode.Ok);
|
|
1409
|
+
}
|
|
1410
|
+
process.stdout.write(`Deployed bundle for app ${appId} (ws ${workspaceId})\n deploymentId ${deploymentId}\n bytes ${bundleInfo.bytes.byteLength}\n steps ${steps.join(" → ")}\n`);
|
|
1411
|
+
return exitAfterFlush(ExitCode.Ok);
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Partial-failure emitter. The upload succeeded (so the user does NOT
|
|
1415
|
+
* need to re-upload on retry) but a downstream step failed. Keeping the
|
|
1416
|
+
* `uploaded: true` bit in the JSON lets agent-plugin skip to the
|
|
1417
|
+
* specific failing step on retry instead of re-running the whole
|
|
1418
|
+
* pipeline.
|
|
1419
|
+
*/
|
|
1420
|
+
async function emitPartialFailure(json, err, progress) {
|
|
1421
|
+
if (err instanceof TossApiError && err.isAuthError) {
|
|
1422
|
+
if (json) emitJson({
|
|
1423
|
+
ok: true,
|
|
1424
|
+
authenticated: false,
|
|
1425
|
+
reason: "session-expired",
|
|
1426
|
+
...progress
|
|
1427
|
+
});
|
|
1428
|
+
else process.stderr.write("Session is no longer valid. Run `aitcc login` again.\n");
|
|
1429
|
+
return exitAfterFlush(ExitCode.NotAuthenticated);
|
|
1430
|
+
}
|
|
1431
|
+
if (err instanceof TossApiError) {
|
|
1432
|
+
if (json) emitJson({
|
|
1433
|
+
ok: false,
|
|
1434
|
+
reason: "api-error",
|
|
1435
|
+
status: err.status,
|
|
1436
|
+
...err.errorCode !== void 0 ? { errorCode: err.errorCode } : {},
|
|
1437
|
+
message: err.message,
|
|
1438
|
+
...progress
|
|
1439
|
+
});
|
|
1440
|
+
else process.stderr.write(`Unexpected error: ${err.message}\n`);
|
|
1441
|
+
return exitAfterFlush(ExitCode.ApiError);
|
|
1442
|
+
}
|
|
1443
|
+
if (err instanceof NetworkError) {
|
|
1444
|
+
if (json) emitJson({
|
|
1445
|
+
ok: false,
|
|
1446
|
+
reason: "network-error",
|
|
1447
|
+
message: err.message,
|
|
1448
|
+
...progress
|
|
1449
|
+
});
|
|
1450
|
+
else process.stderr.write(`Network error reaching the console API: ${err.message}.\n`);
|
|
1451
|
+
return exitAfterFlush(ExitCode.NetworkError);
|
|
1452
|
+
}
|
|
1453
|
+
if (json) emitJson({
|
|
1454
|
+
ok: false,
|
|
1455
|
+
reason: "api-error",
|
|
1456
|
+
message: err.message,
|
|
1457
|
+
...progress
|
|
1458
|
+
});
|
|
1459
|
+
else process.stderr.write(`Unexpected error: ${err.message}\n`);
|
|
1460
|
+
return exitAfterFlush(ExitCode.ApiError);
|
|
1461
|
+
}
|
|
1462
|
+
//#endregion
|
|
876
1463
|
//#region src/config/app-manifest.ts
|
|
877
1464
|
var ManifestError = class extends Error {
|
|
878
1465
|
kind;
|
|
@@ -1945,7 +2532,7 @@ const reportsCommand = defineCommand({
|
|
|
1945
2532
|
const bundlesCommand = defineCommand({
|
|
1946
2533
|
meta: {
|
|
1947
2534
|
name: "bundles",
|
|
1948
|
-
description: "Inspect upload bundles for a mini-app."
|
|
2535
|
+
description: "Inspect and manage upload bundles for a mini-app."
|
|
1949
2536
|
},
|
|
1950
2537
|
subCommands: {
|
|
1951
2538
|
ls: defineCommand({
|
|
@@ -2123,44 +2710,502 @@ const bundlesCommand = defineCommand({
|
|
|
2123
2710
|
return emitFailureFromError(args.json, err);
|
|
2124
2711
|
}
|
|
2125
2712
|
}
|
|
2126
|
-
})
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
name: "certs",
|
|
2132
|
-
description: "Inspect mTLS certificates for a mini-app."
|
|
2133
|
-
},
|
|
2134
|
-
subCommands: { ls: defineCommand({
|
|
2135
|
-
meta: {
|
|
2136
|
-
name: "ls",
|
|
2137
|
-
description: "List mTLS certificates issued for a mini-app."
|
|
2138
|
-
},
|
|
2139
|
-
args: {
|
|
2140
|
-
id: {
|
|
2141
|
-
type: "positional",
|
|
2142
|
-
description: "Mini-app ID.",
|
|
2143
|
-
required: true
|
|
2144
|
-
},
|
|
2145
|
-
workspace: {
|
|
2146
|
-
type: "string",
|
|
2147
|
-
description: "Workspace ID. Defaults to the selected workspace."
|
|
2713
|
+
}),
|
|
2714
|
+
upload: defineCommand({
|
|
2715
|
+
meta: {
|
|
2716
|
+
name: "upload",
|
|
2717
|
+
description: "Upload an .ait bundle (initialize → PUT → complete [+ memo])."
|
|
2148
2718
|
},
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2719
|
+
args: {
|
|
2720
|
+
id: {
|
|
2721
|
+
type: "positional",
|
|
2722
|
+
description: "Mini-app ID.",
|
|
2723
|
+
required: true
|
|
2724
|
+
},
|
|
2725
|
+
path: {
|
|
2726
|
+
type: "positional",
|
|
2727
|
+
description: "Path to the .ait bundle file.",
|
|
2728
|
+
required: true
|
|
2729
|
+
},
|
|
2730
|
+
"deployment-id": {
|
|
2731
|
+
type: "string",
|
|
2732
|
+
description: "deploymentId embedded in the bundle (from app.json._metadata.deploymentId)."
|
|
2733
|
+
},
|
|
2734
|
+
memo: {
|
|
2735
|
+
type: "string",
|
|
2736
|
+
description: "Optional memo attached to this bundle version."
|
|
2737
|
+
},
|
|
2738
|
+
workspace: {
|
|
2739
|
+
type: "string",
|
|
2740
|
+
description: "Workspace ID. Defaults to the selected workspace."
|
|
2741
|
+
},
|
|
2742
|
+
"dry-run": {
|
|
2743
|
+
type: "boolean",
|
|
2744
|
+
description: "Validate inputs and show what would be sent, without touching the server.",
|
|
2745
|
+
default: false
|
|
2746
|
+
},
|
|
2747
|
+
json: {
|
|
2748
|
+
type: "boolean",
|
|
2749
|
+
description: "Emit machine-readable JSON.",
|
|
2750
|
+
default: false
|
|
2751
|
+
}
|
|
2752
|
+
},
|
|
2753
|
+
async run({ args }) {
|
|
2754
|
+
const appId = parseAppId(args.id);
|
|
2755
|
+
if (appId === null) {
|
|
2756
|
+
if (args.json) emitJson({
|
|
2757
|
+
ok: false,
|
|
2758
|
+
reason: "invalid-id",
|
|
2759
|
+
message: `app id must be a positive integer (got ${JSON.stringify(args.id)})`
|
|
2760
|
+
});
|
|
2761
|
+
else process.stderr.write(`app bundles upload: invalid id ${JSON.stringify(args.id)}\n`);
|
|
2762
|
+
return exitAfterFlush(ExitCode.Usage);
|
|
2763
|
+
}
|
|
2764
|
+
const deploymentId = typeof args["deployment-id"] === "string" ? args["deployment-id"] : "";
|
|
2765
|
+
if (deploymentId === "") {
|
|
2766
|
+
if (args.json) emitJson({
|
|
2767
|
+
ok: false,
|
|
2768
|
+
reason: "missing-deployment-id",
|
|
2769
|
+
message: "--deployment-id is required; read app.json._metadata.deploymentId from inside the .ait"
|
|
2770
|
+
});
|
|
2771
|
+
else process.stderr.write("app bundles upload: --deployment-id <uuid> is required.\n The .ait bundle is a zip; read app.json inside and copy _metadata.deploymentId.\n");
|
|
2772
|
+
return exitAfterFlush(ExitCode.Usage);
|
|
2773
|
+
}
|
|
2774
|
+
const filePath = typeof args.path === "string" ? args.path : "";
|
|
2775
|
+
let bytes;
|
|
2776
|
+
try {
|
|
2777
|
+
const { readFile } = await import("node:fs/promises");
|
|
2778
|
+
const buf = await readFile(filePath);
|
|
2779
|
+
bytes = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
2780
|
+
} catch (err) {
|
|
2781
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2782
|
+
if (args.json) emitJson({
|
|
2783
|
+
ok: false,
|
|
2784
|
+
reason: "file-unreadable",
|
|
2785
|
+
path: filePath,
|
|
2786
|
+
message
|
|
2787
|
+
});
|
|
2788
|
+
else process.stderr.write(`app bundles upload: cannot read ${filePath}: ${message}\n`);
|
|
2789
|
+
return exitAfterFlush(ExitCode.Usage);
|
|
2790
|
+
}
|
|
2791
|
+
const ctx = await resolveWorkspaceContext(args);
|
|
2792
|
+
if (!ctx) return;
|
|
2793
|
+
const { session, workspaceId } = ctx;
|
|
2794
|
+
const memo = typeof args.memo === "string" && args.memo.length > 0 ? args.memo : void 0;
|
|
2795
|
+
if (args["dry-run"]) {
|
|
2796
|
+
if (args.json) emitJson({
|
|
2797
|
+
ok: true,
|
|
2798
|
+
dryRun: true,
|
|
2799
|
+
workspaceId,
|
|
2800
|
+
appId,
|
|
2801
|
+
deploymentId,
|
|
2802
|
+
bytes: bytes.byteLength,
|
|
2803
|
+
memo: memo ?? null
|
|
2804
|
+
});
|
|
2805
|
+
else process.stdout.write(`DRY RUN\n workspace ${workspaceId}\n appId ${appId}\n deploymentId ${deploymentId}\n bytes ${bytes.byteLength}\n memo ${memo ?? "(none)"}\n`);
|
|
2806
|
+
return exitAfterFlush(ExitCode.Ok);
|
|
2807
|
+
}
|
|
2808
|
+
try {
|
|
2809
|
+
const init = await postDeploymentsInitialize(workspaceId, appId, deploymentId, session.cookies);
|
|
2810
|
+
if (init.reviewStatus !== "PREPARE") {
|
|
2811
|
+
if (args.json) emitJson({
|
|
2812
|
+
ok: false,
|
|
2813
|
+
reason: "bundle-not-prepare",
|
|
2814
|
+
workspaceId,
|
|
2815
|
+
appId,
|
|
2816
|
+
deploymentId,
|
|
2817
|
+
reviewStatus: init.reviewStatus,
|
|
2818
|
+
message: "이미 존재하는 버전이에요."
|
|
2819
|
+
});
|
|
2820
|
+
else process.stderr.write(`app bundles upload: deployment ${deploymentId} is already in state ${init.reviewStatus}; bundle upload refused.\n`);
|
|
2821
|
+
return exitAfterFlush(ExitCode.Usage);
|
|
2822
|
+
}
|
|
2823
|
+
await putBundleToUploadUrl(init.uploadUrl, bytes);
|
|
2824
|
+
const bundle = await postDeploymentsComplete(workspaceId, appId, deploymentId, session.cookies);
|
|
2825
|
+
let memoApplied = false;
|
|
2826
|
+
if (memo !== void 0) {
|
|
2827
|
+
await postBundleMemo(workspaceId, appId, deploymentId, memo, session.cookies);
|
|
2828
|
+
memoApplied = true;
|
|
2829
|
+
}
|
|
2830
|
+
if (args.json) {
|
|
2831
|
+
emitJson({
|
|
2832
|
+
ok: true,
|
|
2833
|
+
workspaceId,
|
|
2834
|
+
appId,
|
|
2835
|
+
deploymentId,
|
|
2836
|
+
reviewStatus: init.reviewStatus,
|
|
2837
|
+
bundle,
|
|
2838
|
+
memoApplied
|
|
2839
|
+
});
|
|
2840
|
+
return exitAfterFlush(ExitCode.Ok);
|
|
2841
|
+
}
|
|
2842
|
+
process.stdout.write(`Uploaded bundle for app ${appId} (ws ${workspaceId})\n deploymentId ${deploymentId}\n bytes ${bytes.byteLength}\n memo ${memoApplied ? "applied" : "(none)"}\n`);
|
|
2843
|
+
return exitAfterFlush(ExitCode.Ok);
|
|
2844
|
+
} catch (err) {
|
|
2845
|
+
return emitFailureFromError(args.json, err);
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
}),
|
|
2849
|
+
review: defineCommand({
|
|
2850
|
+
meta: {
|
|
2851
|
+
name: "review",
|
|
2852
|
+
description: "Submit (or withdraw) an uploaded bundle for review."
|
|
2853
|
+
},
|
|
2854
|
+
args: {
|
|
2855
|
+
id: {
|
|
2856
|
+
type: "positional",
|
|
2857
|
+
description: "Mini-app ID.",
|
|
2858
|
+
required: true
|
|
2859
|
+
},
|
|
2860
|
+
"deployment-id": {
|
|
2861
|
+
type: "string",
|
|
2862
|
+
description: "deploymentId of the uploaded bundle."
|
|
2863
|
+
},
|
|
2864
|
+
"release-notes": {
|
|
2865
|
+
type: "string",
|
|
2866
|
+
description: "Release notes shown to the reviewer. Ignored with --withdraw."
|
|
2867
|
+
},
|
|
2868
|
+
withdraw: {
|
|
2869
|
+
type: "boolean",
|
|
2870
|
+
description: "Withdraw the existing review request instead of submitting a new one.",
|
|
2871
|
+
default: false
|
|
2872
|
+
},
|
|
2873
|
+
workspace: {
|
|
2874
|
+
type: "string",
|
|
2875
|
+
description: "Workspace ID. Defaults to the selected workspace."
|
|
2876
|
+
},
|
|
2877
|
+
json: {
|
|
2878
|
+
type: "boolean",
|
|
2879
|
+
description: "Emit machine-readable JSON.",
|
|
2880
|
+
default: false
|
|
2881
|
+
}
|
|
2882
|
+
},
|
|
2883
|
+
async run({ args }) {
|
|
2884
|
+
const appId = parseAppId(args.id);
|
|
2885
|
+
if (appId === null) {
|
|
2886
|
+
if (args.json) emitJson({
|
|
2887
|
+
ok: false,
|
|
2888
|
+
reason: "invalid-id",
|
|
2889
|
+
message: `app id must be a positive integer (got ${JSON.stringify(args.id)})`
|
|
2890
|
+
});
|
|
2891
|
+
else process.stderr.write(`app bundles review: invalid id ${JSON.stringify(args.id)}\n`);
|
|
2892
|
+
return exitAfterFlush(ExitCode.Usage);
|
|
2893
|
+
}
|
|
2894
|
+
const deploymentId = typeof args["deployment-id"] === "string" ? args["deployment-id"] : "";
|
|
2895
|
+
if (deploymentId === "") {
|
|
2896
|
+
if (args.json) emitJson({
|
|
2897
|
+
ok: false,
|
|
2898
|
+
reason: "missing-deployment-id"
|
|
2899
|
+
});
|
|
2900
|
+
else process.stderr.write("app bundles review: --deployment-id <uuid> is required.\n");
|
|
2901
|
+
return exitAfterFlush(ExitCode.Usage);
|
|
2902
|
+
}
|
|
2903
|
+
const withdraw = Boolean(args.withdraw);
|
|
2904
|
+
const releaseNotes = typeof args["release-notes"] === "string" ? args["release-notes"] : void 0;
|
|
2905
|
+
if (!withdraw && releaseNotes === void 0) {
|
|
2906
|
+
if (args.json) emitJson({
|
|
2907
|
+
ok: false,
|
|
2908
|
+
reason: "missing-release-notes"
|
|
2909
|
+
});
|
|
2910
|
+
else process.stderr.write("app bundles review: --release-notes <text> is required to submit for review.\n");
|
|
2911
|
+
return exitAfterFlush(ExitCode.Usage);
|
|
2912
|
+
}
|
|
2913
|
+
const ctx = await resolveWorkspaceContext(args);
|
|
2914
|
+
if (!ctx) return;
|
|
2915
|
+
const { session, workspaceId } = ctx;
|
|
2916
|
+
try {
|
|
2917
|
+
if (withdraw) {
|
|
2918
|
+
const result = await postBundleReviewWithdrawal(workspaceId, appId, deploymentId, session.cookies);
|
|
2919
|
+
if (args.json) {
|
|
2920
|
+
emitJson({
|
|
2921
|
+
ok: true,
|
|
2922
|
+
workspaceId,
|
|
2923
|
+
appId,
|
|
2924
|
+
deploymentId,
|
|
2925
|
+
action: "withdraw",
|
|
2926
|
+
result
|
|
2927
|
+
});
|
|
2928
|
+
return exitAfterFlush(ExitCode.Ok);
|
|
2929
|
+
}
|
|
2930
|
+
process.stdout.write(`Withdrew review for bundle ${deploymentId} (app ${appId}, ws ${workspaceId})\n`);
|
|
2931
|
+
return exitAfterFlush(ExitCode.Ok);
|
|
2932
|
+
}
|
|
2933
|
+
const result = await postBundleReview({
|
|
2934
|
+
workspaceId,
|
|
2935
|
+
miniAppId: appId,
|
|
2936
|
+
deploymentId,
|
|
2937
|
+
releaseNotes: releaseNotes ?? ""
|
|
2938
|
+
}, session.cookies);
|
|
2939
|
+
if (args.json) {
|
|
2940
|
+
emitJson({
|
|
2941
|
+
ok: true,
|
|
2942
|
+
workspaceId,
|
|
2943
|
+
appId,
|
|
2944
|
+
deploymentId,
|
|
2945
|
+
action: "submit",
|
|
2946
|
+
result
|
|
2947
|
+
});
|
|
2948
|
+
return exitAfterFlush(ExitCode.Ok);
|
|
2949
|
+
}
|
|
2950
|
+
const versionName = typeof result.versionName === "string" ? result.versionName : "";
|
|
2951
|
+
process.stdout.write(`Submitted bundle ${deploymentId} for review (app ${appId}, ws ${workspaceId})` + (versionName ? ` — version ${versionName}` : "") + "\n");
|
|
2952
|
+
return exitAfterFlush(ExitCode.Ok);
|
|
2953
|
+
} catch (err) {
|
|
2954
|
+
return emitFailureFromError(args.json, err);
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
}),
|
|
2958
|
+
release: defineCommand({
|
|
2959
|
+
meta: {
|
|
2960
|
+
name: "release",
|
|
2961
|
+
description: "Release (publish) an APPROVED bundle to end users."
|
|
2962
|
+
},
|
|
2963
|
+
args: {
|
|
2964
|
+
id: {
|
|
2965
|
+
type: "positional",
|
|
2966
|
+
description: "Mini-app ID.",
|
|
2967
|
+
required: true
|
|
2968
|
+
},
|
|
2969
|
+
"deployment-id": {
|
|
2970
|
+
type: "string",
|
|
2971
|
+
description: "deploymentId of the APPROVED bundle to publish."
|
|
2972
|
+
},
|
|
2973
|
+
confirm: {
|
|
2974
|
+
type: "boolean",
|
|
2975
|
+
description: "Required to actually release — without it, the command refuses.",
|
|
2976
|
+
default: false
|
|
2977
|
+
},
|
|
2978
|
+
workspace: {
|
|
2979
|
+
type: "string",
|
|
2980
|
+
description: "Workspace ID. Defaults to the selected workspace."
|
|
2981
|
+
},
|
|
2982
|
+
json: {
|
|
2983
|
+
type: "boolean",
|
|
2984
|
+
description: "Emit machine-readable JSON.",
|
|
2985
|
+
default: false
|
|
2986
|
+
}
|
|
2987
|
+
},
|
|
2988
|
+
async run({ args }) {
|
|
2989
|
+
const appId = parseAppId(args.id);
|
|
2990
|
+
if (appId === null) {
|
|
2991
|
+
if (args.json) emitJson({
|
|
2992
|
+
ok: false,
|
|
2993
|
+
reason: "invalid-id",
|
|
2994
|
+
message: `app id must be a positive integer (got ${JSON.stringify(args.id)})`
|
|
2995
|
+
});
|
|
2996
|
+
else process.stderr.write(`app bundles release: invalid id ${JSON.stringify(args.id)}\n`);
|
|
2997
|
+
return exitAfterFlush(ExitCode.Usage);
|
|
2998
|
+
}
|
|
2999
|
+
const deploymentId = typeof args["deployment-id"] === "string" ? args["deployment-id"] : "";
|
|
3000
|
+
if (deploymentId === "") {
|
|
3001
|
+
if (args.json) emitJson({
|
|
3002
|
+
ok: false,
|
|
3003
|
+
reason: "missing-deployment-id"
|
|
3004
|
+
});
|
|
3005
|
+
else process.stderr.write("app bundles release: --deployment-id <uuid> is required.\n");
|
|
3006
|
+
return exitAfterFlush(ExitCode.Usage);
|
|
3007
|
+
}
|
|
3008
|
+
if (!args.confirm) {
|
|
3009
|
+
if (args.json) emitJson({
|
|
3010
|
+
ok: false,
|
|
3011
|
+
reason: "not-confirmed",
|
|
3012
|
+
message: "release is destructive; pass --confirm to proceed"
|
|
3013
|
+
});
|
|
3014
|
+
else process.stderr.write("app bundles release: this publishes the bundle to end users.\n Re-run with --confirm to proceed.\n");
|
|
3015
|
+
return exitAfterFlush(ExitCode.Usage);
|
|
3016
|
+
}
|
|
3017
|
+
const ctx = await resolveWorkspaceContext(args);
|
|
3018
|
+
if (!ctx) return;
|
|
3019
|
+
const { session, workspaceId } = ctx;
|
|
3020
|
+
try {
|
|
3021
|
+
const result = await postBundleRelease({
|
|
3022
|
+
workspaceId,
|
|
3023
|
+
miniAppId: appId,
|
|
3024
|
+
deploymentId
|
|
3025
|
+
}, session.cookies);
|
|
3026
|
+
if (args.json) {
|
|
3027
|
+
emitJson({
|
|
3028
|
+
ok: true,
|
|
3029
|
+
workspaceId,
|
|
3030
|
+
appId,
|
|
3031
|
+
deploymentId,
|
|
3032
|
+
result
|
|
3033
|
+
});
|
|
3034
|
+
return exitAfterFlush(ExitCode.Ok);
|
|
3035
|
+
}
|
|
3036
|
+
process.stdout.write(`Released bundle ${deploymentId} for app ${appId} (ws ${workspaceId})\n`);
|
|
3037
|
+
return exitAfterFlush(ExitCode.Ok);
|
|
3038
|
+
} catch (err) {
|
|
3039
|
+
return emitFailureFromError(args.json, err);
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
}),
|
|
3043
|
+
"test-push": defineCommand({
|
|
3044
|
+
meta: {
|
|
3045
|
+
name: "test-push",
|
|
3046
|
+
description: "Send a test push so the uploader can open this bundle on their device."
|
|
3047
|
+
},
|
|
3048
|
+
args: {
|
|
3049
|
+
id: {
|
|
3050
|
+
type: "positional",
|
|
3051
|
+
description: "Mini-app ID.",
|
|
3052
|
+
required: true
|
|
3053
|
+
},
|
|
3054
|
+
"deployment-id": {
|
|
3055
|
+
type: "string",
|
|
3056
|
+
description: "deploymentId of the bundle to test."
|
|
3057
|
+
},
|
|
3058
|
+
workspace: {
|
|
3059
|
+
type: "string",
|
|
3060
|
+
description: "Workspace ID. Defaults to the selected workspace."
|
|
3061
|
+
},
|
|
3062
|
+
json: {
|
|
3063
|
+
type: "boolean",
|
|
3064
|
+
description: "Emit machine-readable JSON.",
|
|
3065
|
+
default: false
|
|
3066
|
+
}
|
|
3067
|
+
},
|
|
3068
|
+
async run({ args }) {
|
|
3069
|
+
const appId = parseAppId(args.id);
|
|
3070
|
+
if (appId === null) {
|
|
3071
|
+
if (args.json) emitJson({
|
|
3072
|
+
ok: false,
|
|
3073
|
+
reason: "invalid-id",
|
|
3074
|
+
message: `app id must be a positive integer (got ${JSON.stringify(args.id)})`
|
|
3075
|
+
});
|
|
3076
|
+
else process.stderr.write(`app bundles test-push: invalid id ${JSON.stringify(args.id)}\n`);
|
|
3077
|
+
return exitAfterFlush(ExitCode.Usage);
|
|
3078
|
+
}
|
|
3079
|
+
const deploymentId = typeof args["deployment-id"] === "string" ? args["deployment-id"] : "";
|
|
3080
|
+
if (deploymentId === "") {
|
|
3081
|
+
if (args.json) emitJson({
|
|
3082
|
+
ok: false,
|
|
3083
|
+
reason: "missing-deployment-id"
|
|
3084
|
+
});
|
|
3085
|
+
else process.stderr.write("app bundles test-push: --deployment-id <uuid> is required.\n");
|
|
3086
|
+
return exitAfterFlush(ExitCode.Usage);
|
|
3087
|
+
}
|
|
3088
|
+
const ctx = await resolveWorkspaceContext(args);
|
|
3089
|
+
if (!ctx) return;
|
|
3090
|
+
const { session, workspaceId } = ctx;
|
|
3091
|
+
try {
|
|
3092
|
+
const result = await postBundleTestPush(workspaceId, appId, deploymentId, session.cookies);
|
|
3093
|
+
if (args.json) {
|
|
3094
|
+
emitJson({
|
|
3095
|
+
ok: true,
|
|
3096
|
+
workspaceId,
|
|
3097
|
+
appId,
|
|
3098
|
+
deploymentId,
|
|
3099
|
+
result
|
|
3100
|
+
});
|
|
3101
|
+
return exitAfterFlush(ExitCode.Ok);
|
|
3102
|
+
}
|
|
3103
|
+
process.stdout.write(`Sent test push for bundle ${deploymentId} (app ${appId})\n`);
|
|
3104
|
+
return exitAfterFlush(ExitCode.Ok);
|
|
3105
|
+
} catch (err) {
|
|
3106
|
+
return emitFailureFromError(args.json, err);
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
}),
|
|
3110
|
+
"test-links": defineCommand({
|
|
3111
|
+
meta: {
|
|
3112
|
+
name: "test-links",
|
|
3113
|
+
description: "Show per-device test URLs for the mini-app."
|
|
3114
|
+
},
|
|
3115
|
+
args: {
|
|
3116
|
+
id: {
|
|
3117
|
+
type: "positional",
|
|
3118
|
+
description: "Mini-app ID.",
|
|
3119
|
+
required: true
|
|
3120
|
+
},
|
|
3121
|
+
workspace: {
|
|
3122
|
+
type: "string",
|
|
3123
|
+
description: "Workspace ID. Defaults to the selected workspace."
|
|
3124
|
+
},
|
|
3125
|
+
json: {
|
|
3126
|
+
type: "boolean",
|
|
3127
|
+
description: "Emit machine-readable JSON.",
|
|
3128
|
+
default: false
|
|
3129
|
+
}
|
|
3130
|
+
},
|
|
3131
|
+
async run({ args }) {
|
|
3132
|
+
const appId = parseAppId(args.id);
|
|
3133
|
+
if (appId === null) {
|
|
3134
|
+
if (args.json) emitJson({
|
|
3135
|
+
ok: false,
|
|
3136
|
+
reason: "invalid-id",
|
|
3137
|
+
message: `app id must be a positive integer (got ${JSON.stringify(args.id)})`
|
|
3138
|
+
});
|
|
3139
|
+
else process.stderr.write(`app bundles test-links: invalid id ${JSON.stringify(args.id)}\n`);
|
|
3140
|
+
return exitAfterFlush(ExitCode.Usage);
|
|
3141
|
+
}
|
|
3142
|
+
const ctx = await resolveWorkspaceContext(args);
|
|
3143
|
+
if (!ctx) return;
|
|
3144
|
+
const { session, workspaceId } = ctx;
|
|
3145
|
+
try {
|
|
3146
|
+
const links = await fetchBundleTestLinks(workspaceId, appId, session.cookies);
|
|
3147
|
+
if (args.json) {
|
|
3148
|
+
emitJson({
|
|
3149
|
+
ok: true,
|
|
3150
|
+
workspaceId,
|
|
3151
|
+
appId,
|
|
3152
|
+
links
|
|
3153
|
+
});
|
|
3154
|
+
return exitAfterFlush(ExitCode.Ok);
|
|
3155
|
+
}
|
|
3156
|
+
const keys = Object.keys(links);
|
|
3157
|
+
if (keys.length === 0) {
|
|
3158
|
+
process.stdout.write(`App ${appId} (ws ${workspaceId}): no test links available\n`);
|
|
3159
|
+
return exitAfterFlush(ExitCode.Ok);
|
|
3160
|
+
}
|
|
3161
|
+
process.stdout.write(`App ${appId} (ws ${workspaceId}):\n`);
|
|
3162
|
+
for (const k of keys) {
|
|
3163
|
+
const v = links[k];
|
|
3164
|
+
process.stdout.write(` ${k}\t${typeof v === "string" ? v : JSON.stringify(v)}\n`);
|
|
3165
|
+
}
|
|
3166
|
+
return exitAfterFlush(ExitCode.Ok);
|
|
3167
|
+
} catch (err) {
|
|
3168
|
+
return emitFailureFromError(args.json, err);
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
})
|
|
3172
|
+
}
|
|
3173
|
+
});
|
|
3174
|
+
const certsCommand = defineCommand({
|
|
3175
|
+
meta: {
|
|
3176
|
+
name: "certs",
|
|
3177
|
+
description: "Inspect mTLS certificates for a mini-app."
|
|
3178
|
+
},
|
|
3179
|
+
subCommands: { ls: defineCommand({
|
|
3180
|
+
meta: {
|
|
3181
|
+
name: "ls",
|
|
3182
|
+
description: "List mTLS certificates issued for a mini-app."
|
|
3183
|
+
},
|
|
3184
|
+
args: {
|
|
3185
|
+
id: {
|
|
3186
|
+
type: "positional",
|
|
3187
|
+
description: "Mini-app ID.",
|
|
3188
|
+
required: true
|
|
3189
|
+
},
|
|
3190
|
+
workspace: {
|
|
3191
|
+
type: "string",
|
|
3192
|
+
description: "Workspace ID. Defaults to the selected workspace."
|
|
3193
|
+
},
|
|
3194
|
+
json: {
|
|
3195
|
+
type: "boolean",
|
|
3196
|
+
description: "Emit machine-readable JSON.",
|
|
3197
|
+
default: false
|
|
3198
|
+
}
|
|
3199
|
+
},
|
|
3200
|
+
async run({ args }) {
|
|
3201
|
+
const appId = parseAppId(args.id);
|
|
3202
|
+
if (appId === null) {
|
|
3203
|
+
if (args.json) emitJson({
|
|
3204
|
+
ok: false,
|
|
3205
|
+
reason: "invalid-id",
|
|
3206
|
+
message: `app id must be a positive integer (got ${JSON.stringify(args.id)})`
|
|
3207
|
+
});
|
|
3208
|
+
else process.stderr.write(`app certs ls: invalid id ${JSON.stringify(args.id)}\n`);
|
|
2164
3209
|
return exitAfterFlush(ExitCode.Usage);
|
|
2165
3210
|
}
|
|
2166
3211
|
const ctx = await resolveWorkspaceContext(args);
|
|
@@ -2965,6 +4010,79 @@ const appCommand = defineCommand({
|
|
|
2965
4010
|
...args.config !== void 0 ? { config: args.config } : {}
|
|
2966
4011
|
});
|
|
2967
4012
|
}
|
|
4013
|
+
}),
|
|
4014
|
+
deploy: defineCommand({
|
|
4015
|
+
meta: {
|
|
4016
|
+
name: "deploy",
|
|
4017
|
+
description: "Upload a bundle, optionally request review, optionally release. Auto-detects deploymentId from the .ait if --deployment-id is omitted."
|
|
4018
|
+
},
|
|
4019
|
+
args: {
|
|
4020
|
+
path: {
|
|
4021
|
+
type: "positional",
|
|
4022
|
+
description: "Path to the .ait bundle file.",
|
|
4023
|
+
required: true
|
|
4024
|
+
},
|
|
4025
|
+
app: {
|
|
4026
|
+
type: "string",
|
|
4027
|
+
description: "Mini-app ID. Required — no top-level \"selected app\" concept yet."
|
|
4028
|
+
},
|
|
4029
|
+
"deployment-id": {
|
|
4030
|
+
type: "string",
|
|
4031
|
+
description: "deploymentId of the bundle. Defaults to app.json._metadata.deploymentId inside the .ait."
|
|
4032
|
+
},
|
|
4033
|
+
memo: {
|
|
4034
|
+
type: "string",
|
|
4035
|
+
description: "Optional memo attached to the uploaded bundle."
|
|
4036
|
+
},
|
|
4037
|
+
"request-review": {
|
|
4038
|
+
type: "boolean",
|
|
4039
|
+
description: "After upload, submit the bundle for review.",
|
|
4040
|
+
default: false
|
|
4041
|
+
},
|
|
4042
|
+
"release-notes": {
|
|
4043
|
+
type: "string",
|
|
4044
|
+
description: "Release notes for the review request. Required with --request-review."
|
|
4045
|
+
},
|
|
4046
|
+
release: {
|
|
4047
|
+
type: "boolean",
|
|
4048
|
+
description: "After review submit, publish the bundle. Requires the bundle to already be APPROVED; typically used on a second `app deploy` run.",
|
|
4049
|
+
default: false
|
|
4050
|
+
},
|
|
4051
|
+
confirm: {
|
|
4052
|
+
type: "boolean",
|
|
4053
|
+
description: "Required with --release — confirms the destructive publish step.",
|
|
4054
|
+
default: false
|
|
4055
|
+
},
|
|
4056
|
+
workspace: {
|
|
4057
|
+
type: "string",
|
|
4058
|
+
description: "Workspace ID. Defaults to the selected workspace."
|
|
4059
|
+
},
|
|
4060
|
+
"dry-run": {
|
|
4061
|
+
type: "boolean",
|
|
4062
|
+
description: "Print the planned pipeline without touching the server.",
|
|
4063
|
+
default: false
|
|
4064
|
+
},
|
|
4065
|
+
json: {
|
|
4066
|
+
type: "boolean",
|
|
4067
|
+
description: "Emit machine-readable JSON.",
|
|
4068
|
+
default: false
|
|
4069
|
+
}
|
|
4070
|
+
},
|
|
4071
|
+
async run({ args }) {
|
|
4072
|
+
await runDeploy({
|
|
4073
|
+
path: typeof args.path === "string" ? args.path : "",
|
|
4074
|
+
app: typeof args.app === "string" ? args.app : void 0,
|
|
4075
|
+
json: args.json,
|
|
4076
|
+
dryRun: args["dry-run"],
|
|
4077
|
+
requestReview: args["request-review"],
|
|
4078
|
+
release: args.release,
|
|
4079
|
+
confirm: args.confirm,
|
|
4080
|
+
...args["deployment-id"] !== void 0 ? { deploymentId: args["deployment-id"] } : {},
|
|
4081
|
+
...args.memo !== void 0 ? { memo: args.memo } : {},
|
|
4082
|
+
...args["release-notes"] !== void 0 ? { releaseNotes: args["release-notes"] } : {},
|
|
4083
|
+
...args.workspace !== void 0 ? { workspace: args.workspace } : {}
|
|
4084
|
+
});
|
|
4085
|
+
}
|
|
2968
4086
|
})
|
|
2969
4087
|
}
|
|
2970
4088
|
});
|
|
@@ -4411,7 +5529,7 @@ function resolveVersion() {
|
|
|
4411
5529
|
if (typeof injected === "string" && injected.length > 0) return injected;
|
|
4412
5530
|
} catch {}
|
|
4413
5531
|
try {
|
|
4414
|
-
return "0.1.
|
|
5532
|
+
return "0.1.16";
|
|
4415
5533
|
} catch {}
|
|
4416
5534
|
return "0.0.0-dev";
|
|
4417
5535
|
}
|