@ait-co/console-cli 0.1.15 → 0.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/dist/cli.mjs +206 -13
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -99,3 +99,15 @@ Every command accepts `--json`. When set:
|
|
|
99
99
|
## Status
|
|
100
100
|
|
|
101
101
|
`login`, `logout`, `whoami`, and `upgrade` are implemented end-to-end — `login` drives a real browser over CDP and `whoami` reads the live console member API. `deploy`, `logs`, `status` are next — see [TODO.md](./TODO.md). See the [organization landing page](https://apps-in-toss-community.github.io/) for the full roadmap.
|
|
102
|
+
|
|
103
|
+
## Pre-commit hook
|
|
104
|
+
|
|
105
|
+
Optional but recommended. After cloning, activate the standard pre-commit hook (runs `biome check` on staged files):
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
git config core.hooksPath .githooks
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
This is a developer convenience for fast feedback before push. CI runs the same checks as the enforcement layer, so contributors who don't activate the hook will still see lint failures in their PR.
|
|
112
|
+
|
|
113
|
+
선택 사항이지만 권장합니다. clone 후 표준 pre-commit hook을 활성화하면 staged 파일에 `biome check`가 자동으로 돕니다 (push 전에 빠른 피드백). 활성화하지 않아도 동일한 검사가 CI에서 실행되므로 PR 단계에서 lint 실패를 볼 수 있습니다.
|
package/dist/cli.mjs
CHANGED
|
@@ -7,7 +7,8 @@ import { unzipSync } from "fflate";
|
|
|
7
7
|
import { parse } from "yaml";
|
|
8
8
|
import { imageSize } from "image-size";
|
|
9
9
|
import { spawn } from "node:child_process";
|
|
10
|
-
import { constants } from "node:fs";
|
|
10
|
+
import { constants, createReadStream } from "node:fs";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
11
12
|
//#region src/api/http.ts
|
|
12
13
|
var TossApiError = class extends Error {
|
|
13
14
|
constructor(status, errorCode, reason, errorType) {
|
|
@@ -718,7 +719,8 @@ const ExitCode = {
|
|
|
718
719
|
LoginCookieCaptureFailed: 16,
|
|
719
720
|
ApiError: 17,
|
|
720
721
|
UpgradeUnavailable: 20,
|
|
721
|
-
UpgradeAlreadyLatest: 21
|
|
722
|
+
UpgradeAlreadyLatest: 21,
|
|
723
|
+
UpgradeChecksumFailed: 22
|
|
722
724
|
};
|
|
723
725
|
//#endregion
|
|
724
726
|
//#region src/flush.ts
|
|
@@ -1017,13 +1019,37 @@ var AitBundleError = class extends Error {
|
|
|
1017
1019
|
this.reason = args.reason;
|
|
1018
1020
|
}
|
|
1019
1021
|
};
|
|
1022
|
+
const AIT_MAGIC = new Uint8Array([
|
|
1023
|
+
65,
|
|
1024
|
+
73,
|
|
1025
|
+
84,
|
|
1026
|
+
66,
|
|
1027
|
+
85,
|
|
1028
|
+
78,
|
|
1029
|
+
68,
|
|
1030
|
+
76
|
|
1031
|
+
]);
|
|
1032
|
+
const ZIP_MAGIC = new Uint8Array([
|
|
1033
|
+
80,
|
|
1034
|
+
75,
|
|
1035
|
+
3,
|
|
1036
|
+
4
|
|
1037
|
+
]);
|
|
1038
|
+
function startsWith(bytes, prefix) {
|
|
1039
|
+
if (bytes.length < prefix.length) return false;
|
|
1040
|
+
for (let i = 0; i < prefix.length; i++) if (bytes[i] !== prefix[i]) return false;
|
|
1041
|
+
return true;
|
|
1042
|
+
}
|
|
1043
|
+
function detectBundleFormat(bytes) {
|
|
1044
|
+
if (startsWith(bytes, AIT_MAGIC)) return "ait";
|
|
1045
|
+
if (startsWith(bytes, ZIP_MAGIC)) return "zip";
|
|
1046
|
+
return "unknown";
|
|
1047
|
+
}
|
|
1020
1048
|
/**
|
|
1021
|
-
* Read the `.ait` at `path
|
|
1022
|
-
*
|
|
1023
|
-
*
|
|
1024
|
-
*
|
|
1025
|
-
* Errors are all surfaced as `AitBundleError` with a structured `reason`
|
|
1026
|
-
* so the command layer can render a typed `--json` failure.
|
|
1049
|
+
* Read the `.ait` at `path` and pull out `deploymentId`, auto-detecting
|
|
1050
|
+
* whether the file is the modern AIT header format or a legacy plain
|
|
1051
|
+
* zip. Returns the raw bytes so the caller can forward them to S3
|
|
1052
|
+
* without re-reading the file.
|
|
1027
1053
|
*/
|
|
1028
1054
|
async function readAitBundle(path) {
|
|
1029
1055
|
let buf;
|
|
@@ -1037,16 +1063,109 @@ async function readAitBundle(path) {
|
|
|
1037
1063
|
});
|
|
1038
1064
|
}
|
|
1039
1065
|
const bytes = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
1066
|
+
const { deploymentId, format } = deploymentIdFromBundleBytes(bytes, path);
|
|
1040
1067
|
return {
|
|
1041
|
-
deploymentId
|
|
1042
|
-
bytes
|
|
1068
|
+
deploymentId,
|
|
1069
|
+
bytes,
|
|
1070
|
+
format
|
|
1043
1071
|
};
|
|
1044
1072
|
}
|
|
1045
1073
|
/**
|
|
1046
|
-
* Pure helper split out so tests can feed raw
|
|
1047
|
-
*
|
|
1074
|
+
* Pure helper split out so tests can feed raw bytes without a tmp file.
|
|
1075
|
+
* Throws `AitBundleError` on any parse failure. Returns the detected
|
|
1076
|
+
* format so callers that want to log it can.
|
|
1048
1077
|
*/
|
|
1049
1078
|
function deploymentIdFromBundleBytes(bytes, pathForError) {
|
|
1079
|
+
const format = detectBundleFormat(bytes);
|
|
1080
|
+
if (format === "ait") return {
|
|
1081
|
+
deploymentId: deploymentIdFromAitHeader(bytes, pathForError),
|
|
1082
|
+
format
|
|
1083
|
+
};
|
|
1084
|
+
if (format === "zip") return {
|
|
1085
|
+
deploymentId: deploymentIdFromLegacyZip(bytes, pathForError),
|
|
1086
|
+
format
|
|
1087
|
+
};
|
|
1088
|
+
throw new AitBundleError({
|
|
1089
|
+
path: pathForError,
|
|
1090
|
+
reason: "unrecognized-format",
|
|
1091
|
+
message: "bundle does not start with AITBUNDL or PK magic bytes — not a valid .ait or legacy zip bundle"
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
const AIT_MAGIC_SIZE = 8;
|
|
1095
|
+
const AIT_VERSION_SIZE = 4;
|
|
1096
|
+
const AIT_HEADER_SIZE = AIT_MAGIC_SIZE + AIT_VERSION_SIZE + 8;
|
|
1097
|
+
function deploymentIdFromAitHeader(bytes, pathForError) {
|
|
1098
|
+
if (bytes.length < AIT_HEADER_SIZE) throw new AitBundleError({
|
|
1099
|
+
path: pathForError,
|
|
1100
|
+
reason: "invalid-ait",
|
|
1101
|
+
message: "buffer too small to be a valid AIT file"
|
|
1102
|
+
});
|
|
1103
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1104
|
+
const bundleLen = Number(view.getBigUint64(AIT_MAGIC_SIZE + AIT_VERSION_SIZE, false));
|
|
1105
|
+
if (!Number.isFinite(bundleLen) || bundleLen <= 0) throw new AitBundleError({
|
|
1106
|
+
path: pathForError,
|
|
1107
|
+
reason: "invalid-ait",
|
|
1108
|
+
message: `AIT bundle length is invalid (${bundleLen})`
|
|
1109
|
+
});
|
|
1110
|
+
const bundleStart = AIT_HEADER_SIZE;
|
|
1111
|
+
const bundleEnd = bundleStart + bundleLen;
|
|
1112
|
+
if (bytes.length < bundleEnd) throw new AitBundleError({
|
|
1113
|
+
path: pathForError,
|
|
1114
|
+
reason: "invalid-ait",
|
|
1115
|
+
message: "unexpected end of buffer reading AIT bundle protobuf"
|
|
1116
|
+
});
|
|
1117
|
+
const deploymentId = readProtobufStringFields(bytes.subarray(bundleStart, bundleEnd), [2, 3]).get(2);
|
|
1118
|
+
if (typeof deploymentId !== "string" || deploymentId === "") throw new AitBundleError({
|
|
1119
|
+
path: pathForError,
|
|
1120
|
+
reason: "missing-deployment-id",
|
|
1121
|
+
message: "AIT bundle protobuf is missing deploymentId (field 2)"
|
|
1122
|
+
});
|
|
1123
|
+
return deploymentId;
|
|
1124
|
+
}
|
|
1125
|
+
function readProtobufStringFields(bytes, wantedFieldNumbers) {
|
|
1126
|
+
const wanted = new Set(wantedFieldNumbers);
|
|
1127
|
+
const out = /* @__PURE__ */ new Map();
|
|
1128
|
+
const decoder = new TextDecoder("utf-8", { fatal: false });
|
|
1129
|
+
let offset = 0;
|
|
1130
|
+
while (offset < bytes.length) {
|
|
1131
|
+
const { value: tag, next } = readVarint(bytes, offset);
|
|
1132
|
+
offset = next;
|
|
1133
|
+
const fieldNumber = Number(tag >> 3n);
|
|
1134
|
+
const wireType = Number(tag & 7n);
|
|
1135
|
+
if (wireType === 0) offset = readVarint(bytes, offset).next;
|
|
1136
|
+
else if (wireType === 1) offset += 8;
|
|
1137
|
+
else if (wireType === 2) {
|
|
1138
|
+
const { value: len, next: afterLen } = readVarint(bytes, offset);
|
|
1139
|
+
offset = afterLen;
|
|
1140
|
+
const payloadEnd = offset + Number(len);
|
|
1141
|
+
if (payloadEnd > bytes.length) break;
|
|
1142
|
+
if (wanted.has(fieldNumber)) out.set(fieldNumber, decoder.decode(bytes.subarray(offset, payloadEnd)));
|
|
1143
|
+
offset = payloadEnd;
|
|
1144
|
+
} else if (wireType === 5) offset += 4;
|
|
1145
|
+
else break;
|
|
1146
|
+
}
|
|
1147
|
+
return out;
|
|
1148
|
+
}
|
|
1149
|
+
function readVarint(bytes, start) {
|
|
1150
|
+
let value = 0n;
|
|
1151
|
+
let shift = 0n;
|
|
1152
|
+
let i = start;
|
|
1153
|
+
for (let n = 0; n < 10 && i < bytes.length; n++, i++) {
|
|
1154
|
+
const byte = bytes[i];
|
|
1155
|
+
if (byte === void 0) break;
|
|
1156
|
+
value |= BigInt(byte & 127) << shift;
|
|
1157
|
+
if ((byte & 128) === 0) return {
|
|
1158
|
+
value,
|
|
1159
|
+
next: i + 1
|
|
1160
|
+
};
|
|
1161
|
+
shift += 7n;
|
|
1162
|
+
}
|
|
1163
|
+
return {
|
|
1164
|
+
value,
|
|
1165
|
+
next: i
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
function deploymentIdFromLegacyZip(bytes, pathForError) {
|
|
1050
1169
|
let entries;
|
|
1051
1170
|
try {
|
|
1052
1171
|
entries = unzipSync(bytes, { filter: (file) => file.name === "app.json" });
|
|
@@ -1195,6 +1314,7 @@ async function runDeploy(args, deps = {}) {
|
|
|
1195
1314
|
workspaceId,
|
|
1196
1315
|
appId,
|
|
1197
1316
|
deploymentId,
|
|
1317
|
+
bundleFormat: bundleInfo.format,
|
|
1198
1318
|
bytes: bundleInfo.bytes.byteLength,
|
|
1199
1319
|
steps,
|
|
1200
1320
|
memo: memo ?? null,
|
|
@@ -1279,6 +1399,7 @@ async function runDeploy(args, deps = {}) {
|
|
|
1279
1399
|
workspaceId,
|
|
1280
1400
|
appId,
|
|
1281
1401
|
deploymentId,
|
|
1402
|
+
bundleFormat: bundleInfo.format,
|
|
1282
1403
|
uploaded,
|
|
1283
1404
|
reviewed,
|
|
1284
1405
|
released: release,
|
|
@@ -5340,6 +5461,9 @@ async function fetchLatestReleaseConditional(previousEtag) {
|
|
|
5340
5461
|
etag
|
|
5341
5462
|
};
|
|
5342
5463
|
}
|
|
5464
|
+
function findSha256SumsAsset(release) {
|
|
5465
|
+
return release.assets.find((a) => a.name === "SHA256SUMS");
|
|
5466
|
+
}
|
|
5343
5467
|
function versionFromTag(tag) {
|
|
5344
5468
|
const at = tag.lastIndexOf("@");
|
|
5345
5469
|
const candidate = at >= 0 ? tag.slice(at + 1) : tag;
|
|
@@ -5403,6 +5527,31 @@ function compareSemver(a, b) {
|
|
|
5403
5527
|
return pa.pre > pb.pre ? 1 : -1;
|
|
5404
5528
|
}
|
|
5405
5529
|
//#endregion
|
|
5530
|
+
//#region src/sha256.ts
|
|
5531
|
+
function parseSha256Sums(text) {
|
|
5532
|
+
const out = /* @__PURE__ */ new Map();
|
|
5533
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
5534
|
+
const line = rawLine.trim();
|
|
5535
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
5536
|
+
const match = line.match(/^([0-9a-fA-F]{64})\s+\*?(.+)$/);
|
|
5537
|
+
if (!match) continue;
|
|
5538
|
+
const hash = match[1]?.toLowerCase();
|
|
5539
|
+
const name = match[2]?.trim();
|
|
5540
|
+
if (!hash || !name) continue;
|
|
5541
|
+
out.set(name, hash);
|
|
5542
|
+
}
|
|
5543
|
+
return out;
|
|
5544
|
+
}
|
|
5545
|
+
function sha256OfFile(path) {
|
|
5546
|
+
return new Promise((resolve, reject) => {
|
|
5547
|
+
const hash = createHash("sha256");
|
|
5548
|
+
const stream = createReadStream(path);
|
|
5549
|
+
stream.on("error", reject);
|
|
5550
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
5551
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
5552
|
+
});
|
|
5553
|
+
}
|
|
5554
|
+
//#endregion
|
|
5406
5555
|
//#region src/version.ts
|
|
5407
5556
|
function resolveVersion() {
|
|
5408
5557
|
try {
|
|
@@ -5410,7 +5559,7 @@ function resolveVersion() {
|
|
|
5410
5559
|
if (typeof injected === "string" && injected.length > 0) return injected;
|
|
5411
5560
|
} catch {}
|
|
5412
5561
|
try {
|
|
5413
|
-
return "0.1.
|
|
5562
|
+
return "0.1.17";
|
|
5414
5563
|
} catch {}
|
|
5415
5564
|
return "0.0.0-dev";
|
|
5416
5565
|
}
|
|
@@ -5531,6 +5680,50 @@ const upgradeCommand = defineCommand({
|
|
|
5531
5680
|
}, `Failed to download new binary: ${err.message}`);
|
|
5532
5681
|
process.exit(ExitCode.NetworkError);
|
|
5533
5682
|
}
|
|
5683
|
+
const sumsAsset = findSha256SumsAsset(release);
|
|
5684
|
+
if (!sumsAsset) {
|
|
5685
|
+
await unlink(stagingPath).catch(() => {});
|
|
5686
|
+
emitError({
|
|
5687
|
+
reason: "sums-missing",
|
|
5688
|
+
tag: release.tag_name
|
|
5689
|
+
}, `Release ${release.tag_name} has no SHA256SUMS asset. It may still be uploading; retry shortly.`);
|
|
5690
|
+
process.exit(ExitCode.UpgradeChecksumFailed);
|
|
5691
|
+
}
|
|
5692
|
+
let expected;
|
|
5693
|
+
let actual;
|
|
5694
|
+
try {
|
|
5695
|
+
const sumsRes = await fetch(sumsAsset.browser_download_url);
|
|
5696
|
+
if (!sumsRes.ok) throw new Error(`SHA256SUMS download failed: ${sumsRes.status} ${sumsRes.statusText}`);
|
|
5697
|
+
expected = parseSha256Sums(await sumsRes.text()).get(platform.assetName);
|
|
5698
|
+
actual = (await sha256OfFile(stagingPath)).toLowerCase();
|
|
5699
|
+
} catch (err) {
|
|
5700
|
+
await unlink(stagingPath).catch(() => {});
|
|
5701
|
+
emitError({
|
|
5702
|
+
reason: "sums-fetch-failed",
|
|
5703
|
+
message: err.message
|
|
5704
|
+
}, `Failed to verify checksum: ${err.message}`);
|
|
5705
|
+
process.exit(ExitCode.UpgradeChecksumFailed);
|
|
5706
|
+
}
|
|
5707
|
+
if (!expected) {
|
|
5708
|
+
await unlink(stagingPath).catch(() => {});
|
|
5709
|
+
emitError({
|
|
5710
|
+
reason: "sums-no-entry",
|
|
5711
|
+
assetName: platform.assetName,
|
|
5712
|
+
tag: release.tag_name
|
|
5713
|
+
}, `SHA256SUMS for ${release.tag_name} has no entry for ${platform.assetName}.`);
|
|
5714
|
+
process.exit(ExitCode.UpgradeChecksumFailed);
|
|
5715
|
+
}
|
|
5716
|
+
if (expected.toLowerCase() !== actual) {
|
|
5717
|
+
await unlink(stagingPath).catch(() => {});
|
|
5718
|
+
emitError({
|
|
5719
|
+
reason: "sha256-mismatch",
|
|
5720
|
+
assetName: platform.assetName,
|
|
5721
|
+
expected: expected.toLowerCase(),
|
|
5722
|
+
actual
|
|
5723
|
+
}, `Checksum mismatch for ${platform.assetName}: expected ${expected.toLowerCase()}, got ${actual}.`);
|
|
5724
|
+
process.exit(ExitCode.UpgradeChecksumFailed);
|
|
5725
|
+
}
|
|
5726
|
+
if (!args.json) process.stdout.write("Checksum OK.\n");
|
|
5534
5727
|
try {
|
|
5535
5728
|
if (process.platform === "win32") {
|
|
5536
5729
|
await rename(exePath, `${exePath}.old`);
|