@k-msg/cli 0.1.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ # @k-msg/cli
2
+
3
+ ## 0.1.1 — 2026-02-15
4
+
5
+ ### Patch changes
6
+
7
+ - Initial CLI release.
8
+
package/README.md ADDED
@@ -0,0 +1,160 @@
1
+ # k-msg CLI (`apps/cli`)
2
+
3
+ This CLI is built with Bunli and uses the unified `k-msg` package (KMsg + Providers).
4
+
5
+ ## Install (recommended)
6
+
7
+ ### npm
8
+
9
+ ```bash
10
+ npm install -g @k-msg/cli
11
+ # or: pnpm add -g @k-msg/cli
12
+
13
+ k-msg --help
14
+ ```
15
+
16
+ Note: the npm package downloads a native binary from GitHub Releases on first run
17
+ (`bunli build:all` artifacts: `k-msg-cli-<version>-<target>.tar.gz`), verifies it
18
+ using `checksums.txt`, then extracts and caches it under your OS cache directory
19
+ (`K_MSG_CLI_CACHE_DIR` to override).
20
+
21
+ Env overrides:
22
+
23
+ - `K_MSG_CLI_BASE_URL`: override GitHub release base URL (default: `https://github.com/k-otp/k-msg/releases/download/cli-v<version>`)
24
+ - `K_MSG_CLI_CACHE_DIR`: override where the extracted binary is cached
25
+ - `K_MSG_CLI_LOCAL_BINARY`: copy a local binary instead of downloading (useful for local testing)
26
+
27
+ ### GitHub Releases (manual)
28
+
29
+ The distribution workflow also publishes prebuilt binaries to GitHub Releases as:
30
+
31
+ - `k-msg-cli-<version>-darwin-arm64.tar.gz`
32
+ - `k-msg-cli-<version>-darwin-x64.tar.gz`
33
+ - `k-msg-cli-<version>-linux-arm64.tar.gz`
34
+ - `k-msg-cli-<version>-linux-x64.tar.gz`
35
+ - `k-msg-cli-<version>-windows-x64.tar.gz`
36
+
37
+ After extracting, you'll find the binary at `<target>/k-msg` (or `<target>/k-msg.exe`).
38
+
39
+ ### macOS/Linux
40
+
41
+ ```bash
42
+ tar -xzf k-msg-cli-<version>-<target>.tar.gz
43
+ sudo install -m 0755 <target>/k-msg /usr/local/bin/k-msg
44
+
45
+ # optional alias
46
+ sudo ln -sf /usr/local/bin/k-msg /usr/local/bin/kmsg
47
+
48
+ k-msg --help
49
+ ```
50
+
51
+ ### Windows
52
+
53
+ Extract the archive and put `k-msg.exe` somewhere on your `PATH`.
54
+ Optionally copy it as `kmsg.exe` as an alias.
55
+
56
+ ## Run (local/dev)
57
+
58
+ ```bash
59
+ # Build native binary
60
+ bun run --cwd apps/cli build
61
+ ./apps/cli/dist/k-msg --help
62
+
63
+ # Build Bun-runtime JS bundle (optional)
64
+ bun run --cwd apps/cli build:js
65
+ bun --cwd apps/cli dist/k-msg.js --help
66
+
67
+ # Or run TS directly (dev)
68
+ bun --cwd apps/cli src/k-msg.ts --help
69
+ ```
70
+
71
+ ## Config (`k-msg.config.json`)
72
+
73
+ Default config path: `./k-msg.config.json`
74
+
75
+ Override:
76
+
77
+ ```bash
78
+ k-msg --config /path/to/k-msg.config.json providers list
79
+ ```
80
+
81
+ Example file: `apps/cli/k-msg.config.example.json`
82
+
83
+ ### `env:` substitution
84
+
85
+ Any string value like `"env:NAME"` is replaced with the `NAME` environment variable at runtime.
86
+ If the env var is missing/empty, commands that need runtime providers will fail with exit code `2`.
87
+
88
+ ## Commands
89
+
90
+ - `k-msg config init|show|validate`
91
+ - `k-msg providers list|health`
92
+ - `k-msg sms send`
93
+ - `k-msg alimtalk send`
94
+ - `k-msg send --input <json> | --file <path> | --stdin`
95
+ - `k-msg kakao channel categories|list|auth|add`
96
+ - `k-msg kakao template list|get|create|update|delete|request`
97
+
98
+ ## Send
99
+
100
+ ### SMS
101
+
102
+ ```bash
103
+ k-msg sms send --to 01012345678 --text "hello"
104
+ ```
105
+
106
+ ### AlimTalk
107
+
108
+ Terminology: the CLI uses **Kakao Channel** and **senderKey** (never “profile”).
109
+
110
+ ```bash
111
+ k-msg alimtalk send \
112
+ --to 01012345678 \
113
+ --template-code TPL_001 \
114
+ --vars '{"name":"Jane"}' \
115
+ --channel main
116
+ ```
117
+
118
+ ### Advanced JSON send
119
+
120
+ ```bash
121
+ k-msg send --input '{"to":"01012345678","text":"hello"}'
122
+ ```
123
+
124
+ ## Kakao Channel (Aligo capability)
125
+
126
+ ```bash
127
+ k-msg kakao channel categories
128
+ k-msg kakao channel list
129
+ k-msg kakao channel auth --plus-id @my_channel --phone 01012345678
130
+ k-msg kakao channel add \
131
+ --plus-id @my_channel \
132
+ --auth-num 123456 \
133
+ --phone 01012345678 \
134
+ --category-code 001001001 \
135
+ --save main
136
+ ```
137
+
138
+ ## Kakao Template (IWINV/Aligo)
139
+
140
+ Channel scope (Aligo): use `--channel <alias>` or `--sender-key <value>`.
141
+
142
+ ```bash
143
+ k-msg kakao template list
144
+ k-msg kakao template get --template-code TPL_001
145
+ k-msg kakao template create --name "Welcome" --content "Hello #{name}" --channel main
146
+ k-msg kakao template update --template-code TPL_001 --name "Updated"
147
+ k-msg kakao template delete --template-code TPL_001
148
+
149
+ # inspection request is provider-dependent (supported by Aligo)
150
+ k-msg kakao template request --template-code TPL_001 --channel main
151
+ ```
152
+
153
+ ## Output / Exit Codes
154
+
155
+ - `--json`: print machine-readable JSON
156
+ - exit code:
157
+ - `0`: success
158
+ - `2`: input/config error
159
+ - `3`: provider/network error
160
+ - `4`: capability not supported
package/bin/k-msg.js ADDED
@@ -0,0 +1,267 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+
4
+ import { createHash } from "node:crypto";
5
+ import { spawnSync } from "node:child_process";
6
+ import fs from "node:fs";
7
+ import os from "node:os";
8
+ import path from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import { get as httpsGet } from "node:https";
11
+ import { gunzipSync } from "node:zlib";
12
+
13
+ function pkgRoot() {
14
+ const here = path.dirname(fileURLToPath(import.meta.url));
15
+ return path.resolve(here, "..");
16
+ }
17
+
18
+ function readJsonFile(filePath) {
19
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
20
+ }
21
+
22
+ function readVersion() {
23
+ const pkg = readJsonFile(path.join(pkgRoot(), "package.json"));
24
+ if (typeof pkg.version !== "string" || pkg.version.trim().length === 0) {
25
+ throw new Error("Invalid package.json version");
26
+ }
27
+ return pkg.version;
28
+ }
29
+
30
+ function resolveTarget(platform, arch) {
31
+ if (platform === "darwin" && arch === "arm64") return "darwin-arm64";
32
+ if (platform === "darwin" && arch === "x64") return "darwin-x64";
33
+ if (platform === "linux" && arch === "arm64") return "linux-arm64";
34
+ if (platform === "linux" && arch === "x64") return "linux-x64";
35
+ if (platform === "win32" && arch === "x64") return "windows-x64";
36
+ throw new Error(`Unsupported platform/arch: ${platform}/${arch}`);
37
+ }
38
+
39
+ function cacheBaseDir() {
40
+ const override = process.env.K_MSG_CLI_CACHE_DIR;
41
+ if (typeof override === "string" && override.trim().length > 0) {
42
+ return override;
43
+ }
44
+
45
+ if (process.platform === "win32") {
46
+ return (
47
+ process.env.LOCALAPPDATA ||
48
+ process.env.APPDATA ||
49
+ path.join(os.homedir(), "AppData", "Local")
50
+ );
51
+ }
52
+
53
+ if (process.platform === "darwin") {
54
+ return path.join(os.homedir(), "Library", "Caches");
55
+ }
56
+
57
+ return process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache");
58
+ }
59
+
60
+ function downloadText(url) {
61
+ return new Promise((resolve, reject) => {
62
+ const req = httpsGet(url, { headers: { "User-Agent": "@k-msg/cli" } }, (res) => {
63
+ const status = res.statusCode || 0;
64
+ if (status >= 300 && status < 400 && res.headers.location) {
65
+ const redirected = new URL(res.headers.location, url).toString();
66
+ res.resume();
67
+ downloadText(redirected).then(resolve, reject);
68
+ return;
69
+ }
70
+ if (status !== 200) {
71
+ res.resume();
72
+ reject(new Error(`GET ${url} failed (status=${status})`));
73
+ return;
74
+ }
75
+ res.setEncoding("utf8");
76
+ let data = "";
77
+ res.on("data", (chunk) => (data += chunk));
78
+ res.on("end", () => resolve(data));
79
+ });
80
+ req.on("error", reject);
81
+ });
82
+ }
83
+
84
+ function downloadToFile(url, destPath) {
85
+ return new Promise((resolve, reject) => {
86
+ const req = httpsGet(url, { headers: { "User-Agent": "@k-msg/cli" } }, (res) => {
87
+ const status = res.statusCode || 0;
88
+ if (status >= 300 && status < 400 && res.headers.location) {
89
+ const redirected = new URL(res.headers.location, url).toString();
90
+ res.resume();
91
+ downloadToFile(redirected, destPath).then(resolve, reject);
92
+ return;
93
+ }
94
+ if (status !== 200) {
95
+ res.resume();
96
+ reject(new Error(`GET ${url} failed (status=${status})`));
97
+ return;
98
+ }
99
+
100
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
101
+ const file = fs.createWriteStream(destPath);
102
+ res.pipe(file);
103
+ file.on("finish", () => file.close(resolve));
104
+ file.on("error", reject);
105
+ });
106
+ req.on("error", reject);
107
+ });
108
+ }
109
+
110
+ function sha256File(filePath) {
111
+ return new Promise((resolve, reject) => {
112
+ const hash = createHash("sha256");
113
+ const stream = fs.createReadStream(filePath);
114
+ stream.on("error", reject);
115
+ stream.on("data", (chunk) => hash.update(chunk));
116
+ stream.on("end", () => resolve(hash.digest("hex")));
117
+ });
118
+ }
119
+
120
+ function parseChecksums(text) {
121
+ const out = new Map();
122
+ for (const line of text.split("\n")) {
123
+ const trimmed = line.trim();
124
+ if (trimmed.length === 0) continue;
125
+ const m = /^([a-f0-9]{64})\s+\*?(.+)$/.exec(trimmed);
126
+ if (!m) continue;
127
+ const [, sum, file] = m;
128
+ out.set(file, sum);
129
+ }
130
+ return out;
131
+ }
132
+
133
+ function tarReadString(header, start, len) {
134
+ const slice = header.subarray(start, start + len);
135
+ const zero = slice.indexOf(0);
136
+ const end = zero === -1 ? slice.length : zero;
137
+ return slice.subarray(0, end).toString("utf8");
138
+ }
139
+
140
+ function tarIsEmptyBlock(block) {
141
+ for (let i = 0; i < block.length; i++) {
142
+ if (block[i] !== 0) return false;
143
+ }
144
+ return true;
145
+ }
146
+
147
+ function extractFileFromTarGz({ archivePath, filePathInTar, outPath }) {
148
+ const tar = gunzipSync(fs.readFileSync(archivePath));
149
+ let off = 0;
150
+
151
+ while (off + 512 <= tar.length) {
152
+ const header = tar.subarray(off, off + 512);
153
+ if (tarIsEmptyBlock(header)) break;
154
+
155
+ const name = tarReadString(header, 0, 100);
156
+ const prefix = tarReadString(header, 345, 155);
157
+ const fullName = prefix ? `${prefix}/${name}` : name;
158
+
159
+ const sizeOct = tarReadString(header, 124, 12).trim();
160
+ const size = sizeOct.length ? Number.parseInt(sizeOct, 8) : 0;
161
+ const typeflag = header[156];
162
+
163
+ const dataStart = off + 512;
164
+ const dataEnd = dataStart + size;
165
+
166
+ const isFile = typeflag === 0 || typeflag === 48; // '\0' or '0'
167
+ if (isFile && fullName === filePathInTar) {
168
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
169
+ fs.writeFileSync(outPath, tar.subarray(dataStart, dataEnd));
170
+ return;
171
+ }
172
+
173
+ const blocks = Math.ceil(size / 512);
174
+ off = dataStart + blocks * 512;
175
+ }
176
+
177
+ throw new Error(`Missing file in archive: ${filePathInTar}`);
178
+ }
179
+
180
+ async function ensureBinary() {
181
+ const version = readVersion();
182
+ const target = resolveTarget(process.platform, process.arch);
183
+ const ext = process.platform === "win32" ? ".exe" : "";
184
+
185
+ const cacheDir = path.join(cacheBaseDir(), "k-msg", "cli", version, target);
186
+ const dest = path.join(cacheDir, `k-msg${ext}`);
187
+
188
+ if (fs.existsSync(dest)) return dest;
189
+
190
+ const local = process.env.K_MSG_CLI_LOCAL_BINARY;
191
+ if (typeof local === "string" && local.trim().length > 0) {
192
+ fs.mkdirSync(cacheDir, { recursive: true });
193
+ fs.copyFileSync(local, dest);
194
+ if (process.platform !== "win32") {
195
+ fs.chmodSync(dest, 0o755);
196
+ }
197
+ return dest;
198
+ }
199
+
200
+ const baseUrl =
201
+ process.env.K_MSG_CLI_BASE_URL ||
202
+ `https://github.com/k-otp/k-msg/releases/download/cli-v${version}`;
203
+ const assetName = `k-msg-cli-${version}-${target}.tar.gz`;
204
+ const assetUrl = `${baseUrl}/${assetName}`;
205
+ const checksumsUrl = `${baseUrl}/checksums.txt`;
206
+
207
+ fs.mkdirSync(cacheDir, { recursive: true });
208
+
209
+ const archiveTmp = `${dest}.tar.gz.download`;
210
+ const binTmp = `${dest}.download`;
211
+
212
+ console.error(`[k-msg] Installing native binary (${target})...`);
213
+
214
+ try {
215
+ const checksumsText = await downloadText(checksumsUrl);
216
+ const checksums = parseChecksums(checksumsText);
217
+ const expected = checksums.get(assetName);
218
+ if (!expected) {
219
+ throw new Error(`Missing checksum entry for ${assetName}`);
220
+ }
221
+
222
+ await downloadToFile(assetUrl, archiveTmp);
223
+ const actual = await sha256File(archiveTmp);
224
+ if (actual !== expected) {
225
+ throw new Error(
226
+ `Checksum mismatch for ${assetName}\nexpected=${expected}\nactual=${actual}`,
227
+ );
228
+ }
229
+
230
+ extractFileFromTarGz({
231
+ archivePath: archiveTmp,
232
+ filePathInTar: `${target}/k-msg${ext}`,
233
+ outPath: binTmp,
234
+ });
235
+
236
+ fs.renameSync(binTmp, dest);
237
+ if (process.platform !== "win32") {
238
+ fs.chmodSync(dest, 0o755);
239
+ }
240
+ fs.rmSync(archiveTmp, { force: true });
241
+ return dest;
242
+ } catch (err) {
243
+ fs.rmSync(archiveTmp, { force: true });
244
+ fs.rmSync(binTmp, { force: true });
245
+ throw err;
246
+ }
247
+ }
248
+
249
+ async function main() {
250
+ const bin = await ensureBinary();
251
+ const result = spawnSync(bin, process.argv.slice(2), { stdio: "inherit" });
252
+ if (result.error) {
253
+ console.error(result.error);
254
+ process.exitCode = 1;
255
+ return;
256
+ }
257
+ if (typeof result.status === "number") {
258
+ process.exitCode = result.status;
259
+ return;
260
+ }
261
+ process.exitCode = 1;
262
+ }
263
+
264
+ main().catch((err) => {
265
+ console.error(err instanceof Error ? err.message : String(err));
266
+ process.exitCode = 1;
267
+ });
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@k-msg/cli",
3
+ "version": "0.1.1",
4
+ "private": false,
5
+ "description": "k-msg CLI (prebuilt binaries via GitHub Releases)",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=18"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "bin": {
14
+ "k-msg": "./bin/k-msg.js",
15
+ "kmsg": "./bin/k-msg.js"
16
+ },
17
+ "files": [
18
+ "bin",
19
+ "README.md",
20
+ "CHANGELOG.md"
21
+ ],
22
+ "scripts": {
23
+ "build": "bunli build",
24
+ "build:all": "bunli build --targets all",
25
+ "build:native": "bunli build --targets native",
26
+ "build:js": "bun build src/k-msg.ts --outdir=dist --format=esm --target=bun",
27
+ "dev": "bun --watch src/k-msg.ts",
28
+ "test": "bun test",
29
+ "clean": "rm -rf dist"
30
+ },
31
+ "devDependencies": {
32
+ "@bunli/core": "^0.5.4",
33
+ "@bunli/test": "^0.3.2",
34
+ "@types/bun": "latest",
35
+ "@types/node": "^22.0.0",
36
+ "k-msg": "0.7.3",
37
+ "bunli": "^0.5.3",
38
+ "typescript": "^5.7.2",
39
+ "zod": "^4.0.14"
40
+ },
41
+ "keywords": [
42
+ "alimtalk",
43
+ "cli",
44
+ "messaging",
45
+ "kakao"
46
+ ],
47
+ "license": "MIT"
48
+ }