@ait-co/console-cli 0.1.16 → 0.1.18
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 +135 -5
- 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
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { defineCommand, runMain } from "citty";
|
|
3
|
-
import { access, chmod, mkdir, mkdtemp, readFile, rename, rm, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { access, chmod, copyFile, 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
6
|
import { unzipSync } from "fflate";
|
|
7
7
|
import { parse } from "yaml";
|
|
8
8
|
import { imageSize } from "image-size";
|
|
9
|
-
import { spawn } from "node:child_process";
|
|
10
|
-
import { constants } from "node:fs";
|
|
9
|
+
import { execFile, spawn } from "node:child_process";
|
|
10
|
+
import { constants, createReadStream } from "node:fs";
|
|
11
|
+
import { promisify } from "node:util";
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
11
13
|
//#region src/api/http.ts
|
|
12
14
|
var TossApiError = class extends Error {
|
|
13
15
|
constructor(status, errorCode, reason, errorType) {
|
|
@@ -718,7 +720,9 @@ const ExitCode = {
|
|
|
718
720
|
LoginCookieCaptureFailed: 16,
|
|
719
721
|
ApiError: 17,
|
|
720
722
|
UpgradeUnavailable: 20,
|
|
721
|
-
UpgradeAlreadyLatest: 21
|
|
723
|
+
UpgradeAlreadyLatest: 21,
|
|
724
|
+
UpgradeChecksumFailed: 22,
|
|
725
|
+
UpgradeSmokeTestFailed: 23
|
|
722
726
|
};
|
|
723
727
|
//#endregion
|
|
724
728
|
//#region src/flush.ts
|
|
@@ -5459,6 +5463,9 @@ async function fetchLatestReleaseConditional(previousEtag) {
|
|
|
5459
5463
|
etag
|
|
5460
5464
|
};
|
|
5461
5465
|
}
|
|
5466
|
+
function findSha256SumsAsset(release) {
|
|
5467
|
+
return release.assets.find((a) => a.name === "SHA256SUMS");
|
|
5468
|
+
}
|
|
5462
5469
|
function versionFromTag(tag) {
|
|
5463
5470
|
const at = tag.lastIndexOf("@");
|
|
5464
5471
|
const candidate = at >= 0 ? tag.slice(at + 1) : tag;
|
|
@@ -5522,6 +5529,31 @@ function compareSemver(a, b) {
|
|
|
5522
5529
|
return pa.pre > pb.pre ? 1 : -1;
|
|
5523
5530
|
}
|
|
5524
5531
|
//#endregion
|
|
5532
|
+
//#region src/sha256.ts
|
|
5533
|
+
function parseSha256Sums(text) {
|
|
5534
|
+
const out = /* @__PURE__ */ new Map();
|
|
5535
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
5536
|
+
const line = rawLine.trim();
|
|
5537
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
5538
|
+
const match = line.match(/^([0-9a-fA-F]{64})\s+\*?(.+)$/);
|
|
5539
|
+
if (!match) continue;
|
|
5540
|
+
const hash = match[1]?.toLowerCase();
|
|
5541
|
+
const name = match[2]?.trim();
|
|
5542
|
+
if (!hash || !name) continue;
|
|
5543
|
+
out.set(name, hash);
|
|
5544
|
+
}
|
|
5545
|
+
return out;
|
|
5546
|
+
}
|
|
5547
|
+
function sha256OfFile(path) {
|
|
5548
|
+
return new Promise((resolve, reject) => {
|
|
5549
|
+
const hash = createHash("sha256");
|
|
5550
|
+
const stream = createReadStream(path);
|
|
5551
|
+
stream.on("error", reject);
|
|
5552
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
5553
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
5554
|
+
});
|
|
5555
|
+
}
|
|
5556
|
+
//#endregion
|
|
5525
5557
|
//#region src/version.ts
|
|
5526
5558
|
function resolveVersion() {
|
|
5527
5559
|
try {
|
|
@@ -5529,13 +5561,14 @@ function resolveVersion() {
|
|
|
5529
5561
|
if (typeof injected === "string" && injected.length > 0) return injected;
|
|
5530
5562
|
} catch {}
|
|
5531
5563
|
try {
|
|
5532
|
-
return "0.1.
|
|
5564
|
+
return "0.1.18";
|
|
5533
5565
|
} catch {}
|
|
5534
5566
|
return "0.0.0-dev";
|
|
5535
5567
|
}
|
|
5536
5568
|
const VERSION = resolveVersion();
|
|
5537
5569
|
//#endregion
|
|
5538
5570
|
//#region src/commands/upgrade.ts
|
|
5571
|
+
const execFileP = promisify(execFile);
|
|
5539
5572
|
function isStandaloneBinary() {
|
|
5540
5573
|
return basename(process.execPath).toLowerCase().startsWith("aitcc");
|
|
5541
5574
|
}
|
|
@@ -5650,12 +5683,70 @@ const upgradeCommand = defineCommand({
|
|
|
5650
5683
|
}, `Failed to download new binary: ${err.message}`);
|
|
5651
5684
|
process.exit(ExitCode.NetworkError);
|
|
5652
5685
|
}
|
|
5686
|
+
const sumsAsset = findSha256SumsAsset(release);
|
|
5687
|
+
if (!sumsAsset) {
|
|
5688
|
+
await unlink(stagingPath).catch(() => {});
|
|
5689
|
+
emitError({
|
|
5690
|
+
reason: "sums-missing",
|
|
5691
|
+
tag: release.tag_name
|
|
5692
|
+
}, `Release ${release.tag_name} has no SHA256SUMS asset. It may still be uploading; retry shortly.`);
|
|
5693
|
+
process.exit(ExitCode.UpgradeChecksumFailed);
|
|
5694
|
+
}
|
|
5695
|
+
let expected;
|
|
5696
|
+
let actual;
|
|
5697
|
+
try {
|
|
5698
|
+
const sumsRes = await fetch(sumsAsset.browser_download_url);
|
|
5699
|
+
if (!sumsRes.ok) throw new Error(`SHA256SUMS download failed: ${sumsRes.status} ${sumsRes.statusText}`);
|
|
5700
|
+
expected = parseSha256Sums(await sumsRes.text()).get(platform.assetName);
|
|
5701
|
+
actual = (await sha256OfFile(stagingPath)).toLowerCase();
|
|
5702
|
+
} catch (err) {
|
|
5703
|
+
await unlink(stagingPath).catch(() => {});
|
|
5704
|
+
emitError({
|
|
5705
|
+
reason: "sums-fetch-failed",
|
|
5706
|
+
message: err.message
|
|
5707
|
+
}, `Failed to verify checksum: ${err.message}`);
|
|
5708
|
+
process.exit(ExitCode.UpgradeChecksumFailed);
|
|
5709
|
+
}
|
|
5710
|
+
if (!expected) {
|
|
5711
|
+
await unlink(stagingPath).catch(() => {});
|
|
5712
|
+
emitError({
|
|
5713
|
+
reason: "sums-no-entry",
|
|
5714
|
+
assetName: platform.assetName,
|
|
5715
|
+
tag: release.tag_name
|
|
5716
|
+
}, `SHA256SUMS for ${release.tag_name} has no entry for ${platform.assetName}.`);
|
|
5717
|
+
process.exit(ExitCode.UpgradeChecksumFailed);
|
|
5718
|
+
}
|
|
5719
|
+
if (expected.toLowerCase() !== actual) {
|
|
5720
|
+
await unlink(stagingPath).catch(() => {});
|
|
5721
|
+
emitError({
|
|
5722
|
+
reason: "sha256-mismatch",
|
|
5723
|
+
assetName: platform.assetName,
|
|
5724
|
+
expected: expected.toLowerCase(),
|
|
5725
|
+
actual
|
|
5726
|
+
}, `Checksum mismatch for ${platform.assetName}: expected ${expected.toLowerCase()}, got ${actual}.`);
|
|
5727
|
+
process.exit(ExitCode.UpgradeChecksumFailed);
|
|
5728
|
+
}
|
|
5729
|
+
if (!args.json) process.stdout.write("Checksum OK.\n");
|
|
5730
|
+
const backupPath = process.platform === "win32" ? null : `${exePath}.bak.${Date.now()}`;
|
|
5731
|
+
if (backupPath) try {
|
|
5732
|
+
await copyFile(exePath, backupPath);
|
|
5733
|
+
} catch (err) {
|
|
5734
|
+
await unlink(stagingPath).catch(() => {});
|
|
5735
|
+
emitError({
|
|
5736
|
+
reason: "backup-failed",
|
|
5737
|
+
message: err.message,
|
|
5738
|
+
exePath,
|
|
5739
|
+
backupPath
|
|
5740
|
+
}, `Failed to create rollback backup at ${backupPath}: ${err.message}`);
|
|
5741
|
+
process.exit(ExitCode.Generic);
|
|
5742
|
+
}
|
|
5653
5743
|
try {
|
|
5654
5744
|
if (process.platform === "win32") {
|
|
5655
5745
|
await rename(exePath, `${exePath}.old`);
|
|
5656
5746
|
await rename(stagingPath, exePath);
|
|
5657
5747
|
} else await rename(stagingPath, exePath);
|
|
5658
5748
|
} catch (err) {
|
|
5749
|
+
if (backupPath) await unlink(backupPath).catch(() => {});
|
|
5659
5750
|
emitError({
|
|
5660
5751
|
reason: "replace-failed",
|
|
5661
5752
|
message: err.message,
|
|
@@ -5664,6 +5755,45 @@ const upgradeCommand = defineCommand({
|
|
|
5664
5755
|
}, `Failed to replace binary at ${exePath}: ${err.message}`);
|
|
5665
5756
|
process.exit(ExitCode.Generic);
|
|
5666
5757
|
}
|
|
5758
|
+
let smokeFailure = null;
|
|
5759
|
+
try {
|
|
5760
|
+
const { stdout } = await execFileP(exePath, ["--version"], {
|
|
5761
|
+
timeout: 1e4,
|
|
5762
|
+
windowsHide: true
|
|
5763
|
+
});
|
|
5764
|
+
if (!stdout.trim()) smokeFailure = "empty stdout from --version";
|
|
5765
|
+
} catch (err) {
|
|
5766
|
+
smokeFailure = err.message;
|
|
5767
|
+
}
|
|
5768
|
+
if (smokeFailure) {
|
|
5769
|
+
let rollbackError = null;
|
|
5770
|
+
let recoveryHint = null;
|
|
5771
|
+
try {
|
|
5772
|
+
if (process.platform === "win32") {
|
|
5773
|
+
try {
|
|
5774
|
+
await unlink(exePath);
|
|
5775
|
+
} catch (err) {
|
|
5776
|
+
recoveryHint = `Failed to remove broken binary at ${exePath}; remove it manually then rename ${exePath}.old back to ${exePath}.`;
|
|
5777
|
+
throw err;
|
|
5778
|
+
}
|
|
5779
|
+
await rename(`${exePath}.old`, exePath);
|
|
5780
|
+
} else if (backupPath) await rename(backupPath, exePath);
|
|
5781
|
+
} catch (err) {
|
|
5782
|
+
rollbackError = err.message;
|
|
5783
|
+
if (!recoveryHint) recoveryHint = process.platform === "win32" ? `Rename ${exePath}.old back to ${exePath} to restore the previous binary.` : `Rename ${backupPath} back to ${exePath} to restore the previous binary.`;
|
|
5784
|
+
}
|
|
5785
|
+
emitError({
|
|
5786
|
+
reason: "smoke-test-failed",
|
|
5787
|
+
message: smokeFailure,
|
|
5788
|
+
exePath,
|
|
5789
|
+
...rollbackError ? {
|
|
5790
|
+
rollbackError,
|
|
5791
|
+
backupPath
|
|
5792
|
+
} : { rolledBack: true }
|
|
5793
|
+
}, rollbackError ? `New binary failed --version smoke test: ${smokeFailure}\nRollback also failed: ${rollbackError}\n${recoveryHint}` : `New binary failed --version smoke test: ${smokeFailure}\nReverted to previous binary.`);
|
|
5794
|
+
process.exit(ExitCode.UpgradeSmokeTestFailed);
|
|
5795
|
+
}
|
|
5796
|
+
if (backupPath) await unlink(backupPath).catch(() => {});
|
|
5667
5797
|
emit({
|
|
5668
5798
|
ok: true,
|
|
5669
5799
|
status: "upgraded",
|