@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 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.16";
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",