@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 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
- const certsCommand = defineCommand({
2130
- meta: {
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
- json: {
2150
- type: "boolean",
2151
- description: "Emit machine-readable JSON.",
2152
- default: false
2153
- }
2154
- },
2155
- async run({ args }) {
2156
- const appId = parseAppId(args.id);
2157
- if (appId === null) {
2158
- if (args.json) emitJson({
2159
- ok: false,
2160
- reason: "invalid-id",
2161
- message: `app id must be a positive integer (got ${JSON.stringify(args.id)})`
2162
- });
2163
- else process.stderr.write(`app certs ls: invalid id ${JSON.stringify(args.id)}\n`);
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.14";
5532
+ return "0.1.16";
4415
5533
  } catch {}
4416
5534
  return "0.0.0-dev";
4417
5535
  }