@ait-co/console-cli 0.1.14 → 0.1.15

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,341 @@ 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
+ /**
1021
+ * Read the `.ait` at `path`, extract `app.json`, and pull out
1022
+ * `_metadata.deploymentId`. Returns both the id and the raw bytes so the
1023
+ * caller can forward them to the S3 upload without re-reading the file.
1024
+ *
1025
+ * Errors are all surfaced as `AitBundleError` with a structured `reason`
1026
+ * so the command layer can render a typed `--json` failure.
1027
+ */
1028
+ async function readAitBundle(path) {
1029
+ let buf;
1030
+ try {
1031
+ buf = await readFile(path);
1032
+ } catch (err) {
1033
+ throw new AitBundleError({
1034
+ path,
1035
+ reason: "file-unreadable",
1036
+ message: err.message
1037
+ });
1038
+ }
1039
+ const bytes = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
1040
+ return {
1041
+ deploymentId: deploymentIdFromBundleBytes(bytes, path),
1042
+ bytes
1043
+ };
1044
+ }
1045
+ /**
1046
+ * Pure helper split out so tests can feed raw zip bytes without a tmp
1047
+ * file. Throws `AitBundleError` on any parse failure.
1048
+ */
1049
+ function deploymentIdFromBundleBytes(bytes, pathForError) {
1050
+ let entries;
1051
+ try {
1052
+ entries = unzipSync(bytes, { filter: (file) => file.name === "app.json" });
1053
+ } catch (err) {
1054
+ throw new AitBundleError({
1055
+ path: pathForError,
1056
+ reason: "invalid-zip",
1057
+ message: `not a valid zip: ${err.message}`
1058
+ });
1059
+ }
1060
+ const entry = entries["app.json"];
1061
+ if (!entry) throw new AitBundleError({
1062
+ path: pathForError,
1063
+ reason: "missing-app-json",
1064
+ message: "app.json is not present at the root of the bundle"
1065
+ });
1066
+ let parsed;
1067
+ try {
1068
+ parsed = JSON.parse(new TextDecoder().decode(entry));
1069
+ } catch (err) {
1070
+ throw new AitBundleError({
1071
+ path: pathForError,
1072
+ reason: "invalid-app-json",
1073
+ message: `app.json is not valid JSON: ${err.message}`
1074
+ });
1075
+ }
1076
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new AitBundleError({
1077
+ path: pathForError,
1078
+ reason: "invalid-app-json",
1079
+ message: "app.json is not a JSON object"
1080
+ });
1081
+ const metadata = parsed._metadata;
1082
+ if (metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) throw new AitBundleError({
1083
+ path: pathForError,
1084
+ reason: "missing-deployment-id",
1085
+ message: "app.json._metadata is missing; is your build outputting the modern app.json schema?"
1086
+ });
1087
+ const deploymentId = metadata.deploymentId;
1088
+ if (typeof deploymentId !== "string" || deploymentId === "") throw new AitBundleError({
1089
+ path: pathForError,
1090
+ reason: "missing-deployment-id",
1091
+ message: "app.json._metadata.deploymentId is missing or empty; is your build outputting the modern app.json schema?"
1092
+ });
1093
+ return deploymentId;
1094
+ }
1095
+ //#endregion
1096
+ //#region src/commands/app-deploy.ts
1097
+ function parseAppIdStrict(raw) {
1098
+ if (raw === "") return null;
1099
+ if (!/^[1-9]\d*$/.test(raw)) return null;
1100
+ const n = Number.parseInt(raw, 10);
1101
+ return Number.isSafeInteger(n) ? n : null;
1102
+ }
1103
+ async function runDeploy(args, deps = {}) {
1104
+ if (typeof args.app !== "string" || args.app === "") {
1105
+ if (args.json) emitJson({
1106
+ ok: false,
1107
+ reason: "missing-app-id",
1108
+ message: "--app <id> is required"
1109
+ });
1110
+ else process.stderr.write("app deploy: --app <id> is required.\n");
1111
+ return exitAfterFlush(ExitCode.Usage);
1112
+ }
1113
+ const appId = parseAppIdStrict(args.app);
1114
+ if (appId === null) {
1115
+ if (args.json) emitJson({
1116
+ ok: false,
1117
+ reason: "invalid-id",
1118
+ message: `--app must be a positive integer (got ${JSON.stringify(args.app)})`
1119
+ });
1120
+ else process.stderr.write(`app deploy: invalid --app ${JSON.stringify(args.app)}\n`);
1121
+ return exitAfterFlush(ExitCode.Usage);
1122
+ }
1123
+ if (typeof args.path !== "string" || args.path === "") {
1124
+ if (args.json) emitJson({
1125
+ ok: false,
1126
+ reason: "missing-path",
1127
+ message: "path to .ait bundle is required"
1128
+ });
1129
+ else process.stderr.write("app deploy: path to .ait bundle is required.\n");
1130
+ return exitAfterFlush(ExitCode.Usage);
1131
+ }
1132
+ const requestReview = Boolean(args.requestReview);
1133
+ const release = Boolean(args.release);
1134
+ const confirm = Boolean(args.confirm);
1135
+ const releaseNotes = typeof args.releaseNotes === "string" ? args.releaseNotes : void 0;
1136
+ if (requestReview && releaseNotes === void 0) {
1137
+ if (args.json) emitJson({
1138
+ ok: false,
1139
+ reason: "missing-release-notes",
1140
+ message: "--release-notes <text> is required with --request-review"
1141
+ });
1142
+ else process.stderr.write("app deploy: --release-notes <text> is required with --request-review.\n");
1143
+ return exitAfterFlush(ExitCode.Usage);
1144
+ }
1145
+ if (release && !confirm) {
1146
+ if (args.json) emitJson({
1147
+ ok: false,
1148
+ reason: "not-confirmed",
1149
+ message: "--release is destructive; pass --confirm to proceed"
1150
+ });
1151
+ else process.stderr.write("app deploy: --release publishes the bundle to end users.\n Re-run with --confirm to proceed.\n");
1152
+ return exitAfterFlush(ExitCode.Usage);
1153
+ }
1154
+ const readBundle = deps.readBundleImpl ?? readAitBundle;
1155
+ let bundleInfo;
1156
+ try {
1157
+ bundleInfo = await readBundle(args.path);
1158
+ } catch (err) {
1159
+ if (err instanceof AitBundleError) {
1160
+ const reason = err.reason === "file-unreadable" ? "file-unreadable" : "invalid-bundle";
1161
+ if (args.json) emitJson({
1162
+ ok: false,
1163
+ reason,
1164
+ path: err.path,
1165
+ bundleReason: err.reason,
1166
+ message: err.message
1167
+ });
1168
+ else process.stderr.write(`app deploy: ${err.message}\n`);
1169
+ return exitAfterFlush(ExitCode.Usage);
1170
+ }
1171
+ throw err;
1172
+ }
1173
+ const deploymentId = typeof args.deploymentId === "string" && args.deploymentId !== "" ? args.deploymentId : bundleInfo.deploymentId;
1174
+ if (deploymentId === "") {
1175
+ if (args.json) emitJson({
1176
+ ok: false,
1177
+ reason: "invalid-bundle",
1178
+ path: args.path,
1179
+ message: "deploymentId is empty"
1180
+ });
1181
+ else process.stderr.write("app deploy: deploymentId is empty.\n");
1182
+ return exitAfterFlush(ExitCode.Usage);
1183
+ }
1184
+ const ctx = await resolveWorkspaceContext(args);
1185
+ if (!ctx) return;
1186
+ const { session, workspaceId } = ctx;
1187
+ const memo = typeof args.memo === "string" && args.memo.length > 0 ? args.memo : void 0;
1188
+ const steps = ["upload"];
1189
+ if (requestReview) steps.push("review");
1190
+ if (release) steps.push("release");
1191
+ if (args.dryRun) {
1192
+ if (args.json) emitJson({
1193
+ ok: true,
1194
+ dryRun: true,
1195
+ workspaceId,
1196
+ appId,
1197
+ deploymentId,
1198
+ bytes: bundleInfo.bytes.byteLength,
1199
+ steps,
1200
+ memo: memo ?? null,
1201
+ releaseNotes: releaseNotes ?? null,
1202
+ confirmed: confirm
1203
+ });
1204
+ else {
1205
+ const stepsLine = steps.map((s) => {
1206
+ if (s === "review") return `review (releaseNotes: ${JSON.stringify(releaseNotes ?? "")})`;
1207
+ if (s === "release") return `release (${confirm ? "confirmed" : "NOT confirmed"})`;
1208
+ return s;
1209
+ }).join(" → ");
1210
+ 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`);
1211
+ }
1212
+ return exitAfterFlush(ExitCode.Ok);
1213
+ }
1214
+ const apiOpts = deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {};
1215
+ let uploaded = false;
1216
+ let bundleRecord = null;
1217
+ let reviewed = false;
1218
+ let reviewResult = null;
1219
+ try {
1220
+ const init = await postDeploymentsInitialize(workspaceId, appId, deploymentId, session.cookies, apiOpts);
1221
+ if (init.reviewStatus !== "PREPARE") {
1222
+ if (args.json) emitJson({
1223
+ ok: false,
1224
+ reason: "bundle-not-prepare",
1225
+ workspaceId,
1226
+ appId,
1227
+ deploymentId,
1228
+ reviewStatus: init.reviewStatus,
1229
+ message: "이미 존재하는 버전이에요."
1230
+ });
1231
+ else process.stderr.write(`app deploy: deployment ${deploymentId} is already in state ${init.reviewStatus}; upload refused.\n`);
1232
+ return exitAfterFlush(ExitCode.Usage);
1233
+ }
1234
+ await putBundleToUploadUrl(init.uploadUrl, bundleInfo.bytes, apiOpts);
1235
+ bundleRecord = await postDeploymentsComplete(workspaceId, appId, deploymentId, session.cookies, apiOpts);
1236
+ if (memo !== void 0) await postBundleMemo(workspaceId, appId, deploymentId, memo, session.cookies, apiOpts);
1237
+ uploaded = true;
1238
+ } catch (err) {
1239
+ return emitFailureFromError(args.json, err);
1240
+ }
1241
+ if (requestReview) try {
1242
+ reviewResult = await postBundleReview({
1243
+ workspaceId,
1244
+ miniAppId: appId,
1245
+ deploymentId,
1246
+ releaseNotes: releaseNotes ?? ""
1247
+ }, session.cookies, apiOpts);
1248
+ reviewed = true;
1249
+ } catch (err) {
1250
+ return emitPartialFailure(args.json, err, {
1251
+ workspaceId,
1252
+ appId,
1253
+ deploymentId,
1254
+ uploaded: true,
1255
+ reviewed: false,
1256
+ released: false
1257
+ });
1258
+ }
1259
+ let releaseResult = null;
1260
+ if (release) try {
1261
+ releaseResult = await postBundleRelease({
1262
+ workspaceId,
1263
+ miniAppId: appId,
1264
+ deploymentId
1265
+ }, session.cookies, apiOpts);
1266
+ } catch (err) {
1267
+ return emitPartialFailure(args.json, err, {
1268
+ workspaceId,
1269
+ appId,
1270
+ deploymentId,
1271
+ uploaded: true,
1272
+ reviewed,
1273
+ released: false
1274
+ });
1275
+ }
1276
+ if (args.json) {
1277
+ emitJson({
1278
+ ok: true,
1279
+ workspaceId,
1280
+ appId,
1281
+ deploymentId,
1282
+ uploaded,
1283
+ reviewed,
1284
+ released: release,
1285
+ bundle: bundleRecord,
1286
+ reviewResult,
1287
+ releaseResult
1288
+ });
1289
+ return exitAfterFlush(ExitCode.Ok);
1290
+ }
1291
+ process.stdout.write(`Deployed bundle for app ${appId} (ws ${workspaceId})\n deploymentId ${deploymentId}\n bytes ${bundleInfo.bytes.byteLength}\n steps ${steps.join(" → ")}\n`);
1292
+ return exitAfterFlush(ExitCode.Ok);
1293
+ }
1294
+ /**
1295
+ * Partial-failure emitter. The upload succeeded (so the user does NOT
1296
+ * need to re-upload on retry) but a downstream step failed. Keeping the
1297
+ * `uploaded: true` bit in the JSON lets agent-plugin skip to the
1298
+ * specific failing step on retry instead of re-running the whole
1299
+ * pipeline.
1300
+ */
1301
+ async function emitPartialFailure(json, err, progress) {
1302
+ if (err instanceof TossApiError && err.isAuthError) {
1303
+ if (json) emitJson({
1304
+ ok: true,
1305
+ authenticated: false,
1306
+ reason: "session-expired",
1307
+ ...progress
1308
+ });
1309
+ else process.stderr.write("Session is no longer valid. Run `aitcc login` again.\n");
1310
+ return exitAfterFlush(ExitCode.NotAuthenticated);
1311
+ }
1312
+ if (err instanceof TossApiError) {
1313
+ if (json) emitJson({
1314
+ ok: false,
1315
+ reason: "api-error",
1316
+ status: err.status,
1317
+ ...err.errorCode !== void 0 ? { errorCode: err.errorCode } : {},
1318
+ message: err.message,
1319
+ ...progress
1320
+ });
1321
+ else process.stderr.write(`Unexpected error: ${err.message}\n`);
1322
+ return exitAfterFlush(ExitCode.ApiError);
1323
+ }
1324
+ if (err instanceof NetworkError) {
1325
+ if (json) emitJson({
1326
+ ok: false,
1327
+ reason: "network-error",
1328
+ message: err.message,
1329
+ ...progress
1330
+ });
1331
+ else process.stderr.write(`Network error reaching the console API: ${err.message}.\n`);
1332
+ return exitAfterFlush(ExitCode.NetworkError);
1333
+ }
1334
+ if (json) emitJson({
1335
+ ok: false,
1336
+ reason: "api-error",
1337
+ message: err.message,
1338
+ ...progress
1339
+ });
1340
+ else process.stderr.write(`Unexpected error: ${err.message}\n`);
1341
+ return exitAfterFlush(ExitCode.ApiError);
1342
+ }
1343
+ //#endregion
876
1344
  //#region src/config/app-manifest.ts
877
1345
  var ManifestError = class extends Error {
878
1346
  kind;
@@ -1945,7 +2413,7 @@ const reportsCommand = defineCommand({
1945
2413
  const bundlesCommand = defineCommand({
1946
2414
  meta: {
1947
2415
  name: "bundles",
1948
- description: "Inspect upload bundles for a mini-app."
2416
+ description: "Inspect and manage upload bundles for a mini-app."
1949
2417
  },
1950
2418
  subCommands: {
1951
2419
  ls: defineCommand({
@@ -2123,33 +2591,491 @@ const bundlesCommand = defineCommand({
2123
2591
  return emitFailureFromError(args.json, err);
2124
2592
  }
2125
2593
  }
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
2594
+ }),
2595
+ upload: defineCommand({
2596
+ meta: {
2597
+ name: "upload",
2598
+ description: "Upload an .ait bundle (initialize → PUT → complete [+ memo])."
2144
2599
  },
2145
- workspace: {
2146
- type: "string",
2147
- description: "Workspace ID. Defaults to the selected workspace."
2600
+ args: {
2601
+ id: {
2602
+ type: "positional",
2603
+ description: "Mini-app ID.",
2604
+ required: true
2605
+ },
2606
+ path: {
2607
+ type: "positional",
2608
+ description: "Path to the .ait bundle file.",
2609
+ required: true
2610
+ },
2611
+ "deployment-id": {
2612
+ type: "string",
2613
+ description: "deploymentId embedded in the bundle (from app.json._metadata.deploymentId)."
2614
+ },
2615
+ memo: {
2616
+ type: "string",
2617
+ description: "Optional memo attached to this bundle version."
2618
+ },
2619
+ workspace: {
2620
+ type: "string",
2621
+ description: "Workspace ID. Defaults to the selected workspace."
2622
+ },
2623
+ "dry-run": {
2624
+ type: "boolean",
2625
+ description: "Validate inputs and show what would be sent, without touching the server.",
2626
+ default: false
2627
+ },
2628
+ json: {
2629
+ type: "boolean",
2630
+ description: "Emit machine-readable JSON.",
2631
+ default: false
2632
+ }
2148
2633
  },
2149
- json: {
2150
- type: "boolean",
2151
- description: "Emit machine-readable JSON.",
2152
- default: false
2634
+ async run({ args }) {
2635
+ const appId = parseAppId(args.id);
2636
+ if (appId === null) {
2637
+ if (args.json) emitJson({
2638
+ ok: false,
2639
+ reason: "invalid-id",
2640
+ message: `app id must be a positive integer (got ${JSON.stringify(args.id)})`
2641
+ });
2642
+ else process.stderr.write(`app bundles upload: invalid id ${JSON.stringify(args.id)}\n`);
2643
+ return exitAfterFlush(ExitCode.Usage);
2644
+ }
2645
+ const deploymentId = typeof args["deployment-id"] === "string" ? args["deployment-id"] : "";
2646
+ if (deploymentId === "") {
2647
+ if (args.json) emitJson({
2648
+ ok: false,
2649
+ reason: "missing-deployment-id",
2650
+ message: "--deployment-id is required; read app.json._metadata.deploymentId from inside the .ait"
2651
+ });
2652
+ 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");
2653
+ return exitAfterFlush(ExitCode.Usage);
2654
+ }
2655
+ const filePath = typeof args.path === "string" ? args.path : "";
2656
+ let bytes;
2657
+ try {
2658
+ const { readFile } = await import("node:fs/promises");
2659
+ const buf = await readFile(filePath);
2660
+ bytes = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
2661
+ } catch (err) {
2662
+ const message = err instanceof Error ? err.message : String(err);
2663
+ if (args.json) emitJson({
2664
+ ok: false,
2665
+ reason: "file-unreadable",
2666
+ path: filePath,
2667
+ message
2668
+ });
2669
+ else process.stderr.write(`app bundles upload: cannot read ${filePath}: ${message}\n`);
2670
+ return exitAfterFlush(ExitCode.Usage);
2671
+ }
2672
+ const ctx = await resolveWorkspaceContext(args);
2673
+ if (!ctx) return;
2674
+ const { session, workspaceId } = ctx;
2675
+ const memo = typeof args.memo === "string" && args.memo.length > 0 ? args.memo : void 0;
2676
+ if (args["dry-run"]) {
2677
+ if (args.json) emitJson({
2678
+ ok: true,
2679
+ dryRun: true,
2680
+ workspaceId,
2681
+ appId,
2682
+ deploymentId,
2683
+ bytes: bytes.byteLength,
2684
+ memo: memo ?? null
2685
+ });
2686
+ else process.stdout.write(`DRY RUN\n workspace ${workspaceId}\n appId ${appId}\n deploymentId ${deploymentId}\n bytes ${bytes.byteLength}\n memo ${memo ?? "(none)"}\n`);
2687
+ return exitAfterFlush(ExitCode.Ok);
2688
+ }
2689
+ try {
2690
+ const init = await postDeploymentsInitialize(workspaceId, appId, deploymentId, session.cookies);
2691
+ if (init.reviewStatus !== "PREPARE") {
2692
+ if (args.json) emitJson({
2693
+ ok: false,
2694
+ reason: "bundle-not-prepare",
2695
+ workspaceId,
2696
+ appId,
2697
+ deploymentId,
2698
+ reviewStatus: init.reviewStatus,
2699
+ message: "이미 존재하는 버전이에요."
2700
+ });
2701
+ else process.stderr.write(`app bundles upload: deployment ${deploymentId} is already in state ${init.reviewStatus}; bundle upload refused.\n`);
2702
+ return exitAfterFlush(ExitCode.Usage);
2703
+ }
2704
+ await putBundleToUploadUrl(init.uploadUrl, bytes);
2705
+ const bundle = await postDeploymentsComplete(workspaceId, appId, deploymentId, session.cookies);
2706
+ let memoApplied = false;
2707
+ if (memo !== void 0) {
2708
+ await postBundleMemo(workspaceId, appId, deploymentId, memo, session.cookies);
2709
+ memoApplied = true;
2710
+ }
2711
+ if (args.json) {
2712
+ emitJson({
2713
+ ok: true,
2714
+ workspaceId,
2715
+ appId,
2716
+ deploymentId,
2717
+ reviewStatus: init.reviewStatus,
2718
+ bundle,
2719
+ memoApplied
2720
+ });
2721
+ return exitAfterFlush(ExitCode.Ok);
2722
+ }
2723
+ process.stdout.write(`Uploaded bundle for app ${appId} (ws ${workspaceId})\n deploymentId ${deploymentId}\n bytes ${bytes.byteLength}\n memo ${memoApplied ? "applied" : "(none)"}\n`);
2724
+ return exitAfterFlush(ExitCode.Ok);
2725
+ } catch (err) {
2726
+ return emitFailureFromError(args.json, err);
2727
+ }
2728
+ }
2729
+ }),
2730
+ review: defineCommand({
2731
+ meta: {
2732
+ name: "review",
2733
+ description: "Submit (or withdraw) an uploaded bundle for review."
2734
+ },
2735
+ args: {
2736
+ id: {
2737
+ type: "positional",
2738
+ description: "Mini-app ID.",
2739
+ required: true
2740
+ },
2741
+ "deployment-id": {
2742
+ type: "string",
2743
+ description: "deploymentId of the uploaded bundle."
2744
+ },
2745
+ "release-notes": {
2746
+ type: "string",
2747
+ description: "Release notes shown to the reviewer. Ignored with --withdraw."
2748
+ },
2749
+ withdraw: {
2750
+ type: "boolean",
2751
+ description: "Withdraw the existing review request instead of submitting a new one.",
2752
+ default: false
2753
+ },
2754
+ workspace: {
2755
+ type: "string",
2756
+ description: "Workspace ID. Defaults to the selected workspace."
2757
+ },
2758
+ json: {
2759
+ type: "boolean",
2760
+ description: "Emit machine-readable JSON.",
2761
+ default: false
2762
+ }
2763
+ },
2764
+ async run({ args }) {
2765
+ const appId = parseAppId(args.id);
2766
+ if (appId === null) {
2767
+ if (args.json) emitJson({
2768
+ ok: false,
2769
+ reason: "invalid-id",
2770
+ message: `app id must be a positive integer (got ${JSON.stringify(args.id)})`
2771
+ });
2772
+ else process.stderr.write(`app bundles review: invalid id ${JSON.stringify(args.id)}\n`);
2773
+ return exitAfterFlush(ExitCode.Usage);
2774
+ }
2775
+ const deploymentId = typeof args["deployment-id"] === "string" ? args["deployment-id"] : "";
2776
+ if (deploymentId === "") {
2777
+ if (args.json) emitJson({
2778
+ ok: false,
2779
+ reason: "missing-deployment-id"
2780
+ });
2781
+ else process.stderr.write("app bundles review: --deployment-id <uuid> is required.\n");
2782
+ return exitAfterFlush(ExitCode.Usage);
2783
+ }
2784
+ const withdraw = Boolean(args.withdraw);
2785
+ const releaseNotes = typeof args["release-notes"] === "string" ? args["release-notes"] : void 0;
2786
+ if (!withdraw && releaseNotes === void 0) {
2787
+ if (args.json) emitJson({
2788
+ ok: false,
2789
+ reason: "missing-release-notes"
2790
+ });
2791
+ else process.stderr.write("app bundles review: --release-notes <text> is required to submit for review.\n");
2792
+ return exitAfterFlush(ExitCode.Usage);
2793
+ }
2794
+ const ctx = await resolveWorkspaceContext(args);
2795
+ if (!ctx) return;
2796
+ const { session, workspaceId } = ctx;
2797
+ try {
2798
+ if (withdraw) {
2799
+ const result = await postBundleReviewWithdrawal(workspaceId, appId, deploymentId, session.cookies);
2800
+ if (args.json) {
2801
+ emitJson({
2802
+ ok: true,
2803
+ workspaceId,
2804
+ appId,
2805
+ deploymentId,
2806
+ action: "withdraw",
2807
+ result
2808
+ });
2809
+ return exitAfterFlush(ExitCode.Ok);
2810
+ }
2811
+ process.stdout.write(`Withdrew review for bundle ${deploymentId} (app ${appId}, ws ${workspaceId})\n`);
2812
+ return exitAfterFlush(ExitCode.Ok);
2813
+ }
2814
+ const result = await postBundleReview({
2815
+ workspaceId,
2816
+ miniAppId: appId,
2817
+ deploymentId,
2818
+ releaseNotes: releaseNotes ?? ""
2819
+ }, session.cookies);
2820
+ if (args.json) {
2821
+ emitJson({
2822
+ ok: true,
2823
+ workspaceId,
2824
+ appId,
2825
+ deploymentId,
2826
+ action: "submit",
2827
+ result
2828
+ });
2829
+ return exitAfterFlush(ExitCode.Ok);
2830
+ }
2831
+ const versionName = typeof result.versionName === "string" ? result.versionName : "";
2832
+ process.stdout.write(`Submitted bundle ${deploymentId} for review (app ${appId}, ws ${workspaceId})` + (versionName ? ` — version ${versionName}` : "") + "\n");
2833
+ return exitAfterFlush(ExitCode.Ok);
2834
+ } catch (err) {
2835
+ return emitFailureFromError(args.json, err);
2836
+ }
2837
+ }
2838
+ }),
2839
+ release: defineCommand({
2840
+ meta: {
2841
+ name: "release",
2842
+ description: "Release (publish) an APPROVED bundle to end users."
2843
+ },
2844
+ args: {
2845
+ id: {
2846
+ type: "positional",
2847
+ description: "Mini-app ID.",
2848
+ required: true
2849
+ },
2850
+ "deployment-id": {
2851
+ type: "string",
2852
+ description: "deploymentId of the APPROVED bundle to publish."
2853
+ },
2854
+ confirm: {
2855
+ type: "boolean",
2856
+ description: "Required to actually release — without it, the command refuses.",
2857
+ default: false
2858
+ },
2859
+ workspace: {
2860
+ type: "string",
2861
+ description: "Workspace ID. Defaults to the selected workspace."
2862
+ },
2863
+ json: {
2864
+ type: "boolean",
2865
+ description: "Emit machine-readable JSON.",
2866
+ default: false
2867
+ }
2868
+ },
2869
+ async run({ args }) {
2870
+ const appId = parseAppId(args.id);
2871
+ if (appId === null) {
2872
+ if (args.json) emitJson({
2873
+ ok: false,
2874
+ reason: "invalid-id",
2875
+ message: `app id must be a positive integer (got ${JSON.stringify(args.id)})`
2876
+ });
2877
+ else process.stderr.write(`app bundles release: invalid id ${JSON.stringify(args.id)}\n`);
2878
+ return exitAfterFlush(ExitCode.Usage);
2879
+ }
2880
+ const deploymentId = typeof args["deployment-id"] === "string" ? args["deployment-id"] : "";
2881
+ if (deploymentId === "") {
2882
+ if (args.json) emitJson({
2883
+ ok: false,
2884
+ reason: "missing-deployment-id"
2885
+ });
2886
+ else process.stderr.write("app bundles release: --deployment-id <uuid> is required.\n");
2887
+ return exitAfterFlush(ExitCode.Usage);
2888
+ }
2889
+ if (!args.confirm) {
2890
+ if (args.json) emitJson({
2891
+ ok: false,
2892
+ reason: "not-confirmed",
2893
+ message: "release is destructive; pass --confirm to proceed"
2894
+ });
2895
+ else process.stderr.write("app bundles release: this publishes the bundle to end users.\n Re-run with --confirm to proceed.\n");
2896
+ return exitAfterFlush(ExitCode.Usage);
2897
+ }
2898
+ const ctx = await resolveWorkspaceContext(args);
2899
+ if (!ctx) return;
2900
+ const { session, workspaceId } = ctx;
2901
+ try {
2902
+ const result = await postBundleRelease({
2903
+ workspaceId,
2904
+ miniAppId: appId,
2905
+ deploymentId
2906
+ }, session.cookies);
2907
+ if (args.json) {
2908
+ emitJson({
2909
+ ok: true,
2910
+ workspaceId,
2911
+ appId,
2912
+ deploymentId,
2913
+ result
2914
+ });
2915
+ return exitAfterFlush(ExitCode.Ok);
2916
+ }
2917
+ process.stdout.write(`Released bundle ${deploymentId} for app ${appId} (ws ${workspaceId})\n`);
2918
+ return exitAfterFlush(ExitCode.Ok);
2919
+ } catch (err) {
2920
+ return emitFailureFromError(args.json, err);
2921
+ }
2922
+ }
2923
+ }),
2924
+ "test-push": defineCommand({
2925
+ meta: {
2926
+ name: "test-push",
2927
+ description: "Send a test push so the uploader can open this bundle on their device."
2928
+ },
2929
+ args: {
2930
+ id: {
2931
+ type: "positional",
2932
+ description: "Mini-app ID.",
2933
+ required: true
2934
+ },
2935
+ "deployment-id": {
2936
+ type: "string",
2937
+ description: "deploymentId of the bundle to test."
2938
+ },
2939
+ workspace: {
2940
+ type: "string",
2941
+ description: "Workspace ID. Defaults to the selected workspace."
2942
+ },
2943
+ json: {
2944
+ type: "boolean",
2945
+ description: "Emit machine-readable JSON.",
2946
+ default: false
2947
+ }
2948
+ },
2949
+ async run({ args }) {
2950
+ const appId = parseAppId(args.id);
2951
+ if (appId === null) {
2952
+ if (args.json) emitJson({
2953
+ ok: false,
2954
+ reason: "invalid-id",
2955
+ message: `app id must be a positive integer (got ${JSON.stringify(args.id)})`
2956
+ });
2957
+ else process.stderr.write(`app bundles test-push: invalid id ${JSON.stringify(args.id)}\n`);
2958
+ return exitAfterFlush(ExitCode.Usage);
2959
+ }
2960
+ const deploymentId = typeof args["deployment-id"] === "string" ? args["deployment-id"] : "";
2961
+ if (deploymentId === "") {
2962
+ if (args.json) emitJson({
2963
+ ok: false,
2964
+ reason: "missing-deployment-id"
2965
+ });
2966
+ else process.stderr.write("app bundles test-push: --deployment-id <uuid> is required.\n");
2967
+ return exitAfterFlush(ExitCode.Usage);
2968
+ }
2969
+ const ctx = await resolveWorkspaceContext(args);
2970
+ if (!ctx) return;
2971
+ const { session, workspaceId } = ctx;
2972
+ try {
2973
+ const result = await postBundleTestPush(workspaceId, appId, deploymentId, session.cookies);
2974
+ if (args.json) {
2975
+ emitJson({
2976
+ ok: true,
2977
+ workspaceId,
2978
+ appId,
2979
+ deploymentId,
2980
+ result
2981
+ });
2982
+ return exitAfterFlush(ExitCode.Ok);
2983
+ }
2984
+ process.stdout.write(`Sent test push for bundle ${deploymentId} (app ${appId})\n`);
2985
+ return exitAfterFlush(ExitCode.Ok);
2986
+ } catch (err) {
2987
+ return emitFailureFromError(args.json, err);
2988
+ }
2989
+ }
2990
+ }),
2991
+ "test-links": defineCommand({
2992
+ meta: {
2993
+ name: "test-links",
2994
+ description: "Show per-device test URLs for the mini-app."
2995
+ },
2996
+ args: {
2997
+ id: {
2998
+ type: "positional",
2999
+ description: "Mini-app ID.",
3000
+ required: true
3001
+ },
3002
+ workspace: {
3003
+ type: "string",
3004
+ description: "Workspace ID. Defaults to the selected workspace."
3005
+ },
3006
+ json: {
3007
+ type: "boolean",
3008
+ description: "Emit machine-readable JSON.",
3009
+ default: false
3010
+ }
3011
+ },
3012
+ async run({ args }) {
3013
+ const appId = parseAppId(args.id);
3014
+ if (appId === null) {
3015
+ if (args.json) emitJson({
3016
+ ok: false,
3017
+ reason: "invalid-id",
3018
+ message: `app id must be a positive integer (got ${JSON.stringify(args.id)})`
3019
+ });
3020
+ else process.stderr.write(`app bundles test-links: invalid id ${JSON.stringify(args.id)}\n`);
3021
+ return exitAfterFlush(ExitCode.Usage);
3022
+ }
3023
+ const ctx = await resolveWorkspaceContext(args);
3024
+ if (!ctx) return;
3025
+ const { session, workspaceId } = ctx;
3026
+ try {
3027
+ const links = await fetchBundleTestLinks(workspaceId, appId, session.cookies);
3028
+ if (args.json) {
3029
+ emitJson({
3030
+ ok: true,
3031
+ workspaceId,
3032
+ appId,
3033
+ links
3034
+ });
3035
+ return exitAfterFlush(ExitCode.Ok);
3036
+ }
3037
+ const keys = Object.keys(links);
3038
+ if (keys.length === 0) {
3039
+ process.stdout.write(`App ${appId} (ws ${workspaceId}): no test links available\n`);
3040
+ return exitAfterFlush(ExitCode.Ok);
3041
+ }
3042
+ process.stdout.write(`App ${appId} (ws ${workspaceId}):\n`);
3043
+ for (const k of keys) {
3044
+ const v = links[k];
3045
+ process.stdout.write(` ${k}\t${typeof v === "string" ? v : JSON.stringify(v)}\n`);
3046
+ }
3047
+ return exitAfterFlush(ExitCode.Ok);
3048
+ } catch (err) {
3049
+ return emitFailureFromError(args.json, err);
3050
+ }
3051
+ }
3052
+ })
3053
+ }
3054
+ });
3055
+ const certsCommand = defineCommand({
3056
+ meta: {
3057
+ name: "certs",
3058
+ description: "Inspect mTLS certificates for a mini-app."
3059
+ },
3060
+ subCommands: { ls: defineCommand({
3061
+ meta: {
3062
+ name: "ls",
3063
+ description: "List mTLS certificates issued for a mini-app."
3064
+ },
3065
+ args: {
3066
+ id: {
3067
+ type: "positional",
3068
+ description: "Mini-app ID.",
3069
+ required: true
3070
+ },
3071
+ workspace: {
3072
+ type: "string",
3073
+ description: "Workspace ID. Defaults to the selected workspace."
3074
+ },
3075
+ json: {
3076
+ type: "boolean",
3077
+ description: "Emit machine-readable JSON.",
3078
+ default: false
2153
3079
  }
2154
3080
  },
2155
3081
  async run({ args }) {
@@ -2965,6 +3891,79 @@ const appCommand = defineCommand({
2965
3891
  ...args.config !== void 0 ? { config: args.config } : {}
2966
3892
  });
2967
3893
  }
3894
+ }),
3895
+ deploy: defineCommand({
3896
+ meta: {
3897
+ name: "deploy",
3898
+ description: "Upload a bundle, optionally request review, optionally release. Auto-detects deploymentId from the .ait if --deployment-id is omitted."
3899
+ },
3900
+ args: {
3901
+ path: {
3902
+ type: "positional",
3903
+ description: "Path to the .ait bundle file.",
3904
+ required: true
3905
+ },
3906
+ app: {
3907
+ type: "string",
3908
+ description: "Mini-app ID. Required — no top-level \"selected app\" concept yet."
3909
+ },
3910
+ "deployment-id": {
3911
+ type: "string",
3912
+ description: "deploymentId of the bundle. Defaults to app.json._metadata.deploymentId inside the .ait."
3913
+ },
3914
+ memo: {
3915
+ type: "string",
3916
+ description: "Optional memo attached to the uploaded bundle."
3917
+ },
3918
+ "request-review": {
3919
+ type: "boolean",
3920
+ description: "After upload, submit the bundle for review.",
3921
+ default: false
3922
+ },
3923
+ "release-notes": {
3924
+ type: "string",
3925
+ description: "Release notes for the review request. Required with --request-review."
3926
+ },
3927
+ release: {
3928
+ type: "boolean",
3929
+ description: "After review submit, publish the bundle. Requires the bundle to already be APPROVED; typically used on a second `app deploy` run.",
3930
+ default: false
3931
+ },
3932
+ confirm: {
3933
+ type: "boolean",
3934
+ description: "Required with --release — confirms the destructive publish step.",
3935
+ default: false
3936
+ },
3937
+ workspace: {
3938
+ type: "string",
3939
+ description: "Workspace ID. Defaults to the selected workspace."
3940
+ },
3941
+ "dry-run": {
3942
+ type: "boolean",
3943
+ description: "Print the planned pipeline without touching the server.",
3944
+ default: false
3945
+ },
3946
+ json: {
3947
+ type: "boolean",
3948
+ description: "Emit machine-readable JSON.",
3949
+ default: false
3950
+ }
3951
+ },
3952
+ async run({ args }) {
3953
+ await runDeploy({
3954
+ path: typeof args.path === "string" ? args.path : "",
3955
+ app: typeof args.app === "string" ? args.app : void 0,
3956
+ json: args.json,
3957
+ dryRun: args["dry-run"],
3958
+ requestReview: args["request-review"],
3959
+ release: args.release,
3960
+ confirm: args.confirm,
3961
+ ...args["deployment-id"] !== void 0 ? { deploymentId: args["deployment-id"] } : {},
3962
+ ...args.memo !== void 0 ? { memo: args.memo } : {},
3963
+ ...args["release-notes"] !== void 0 ? { releaseNotes: args["release-notes"] } : {},
3964
+ ...args.workspace !== void 0 ? { workspace: args.workspace } : {}
3965
+ });
3966
+ }
2968
3967
  })
2969
3968
  }
2970
3969
  });
@@ -4411,7 +5410,7 @@ function resolveVersion() {
4411
5410
  if (typeof injected === "string" && injected.length > 0) return injected;
4412
5411
  } catch {}
4413
5412
  try {
4414
- return "0.1.14";
5413
+ return "0.1.15";
4415
5414
  } catch {}
4416
5415
  return "0.0.0-dev";
4417
5416
  }