@xdfnet/ispeak 1.6.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.
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const fs = require("fs");
5
+ const os = require("os");
6
+ const path = require("path");
7
+ const { spawnSync } = require("child_process");
8
+
9
+ const root = path.resolve(__dirname, "..");
10
+ const home = os.homedir();
11
+ const binDir = path.join(home, ".local", "bin");
12
+ const configDir = path.join(home, ".config", "iSpeak");
13
+ const plistPath = path.join(home, "Library", "LaunchAgents", "com.iSpeak.plist");
14
+ const socketPath = path.join(configDir, "ispeak.sock");
15
+ const binaryPath = path.join(binDir, "ispeakd");
16
+ const cliPath = path.join(binDir, "ispeak");
17
+ const buildPath = path.join(root, "build", "ispeakd");
18
+
19
+ function run(cmd, args, options = {}) {
20
+ const result = spawnSync(cmd, args, {
21
+ cwd: root,
22
+ stdio: options.stdio || "inherit",
23
+ encoding: "utf8",
24
+ });
25
+ if (result.error) {
26
+ throw result.error;
27
+ }
28
+ if (result.status !== 0 && !options.allowFailure) {
29
+ throw new Error(`${cmd} ${args.join(" ")} failed with exit code ${result.status}`);
30
+ }
31
+ return result;
32
+ }
33
+
34
+ function ensureDir(dir) {
35
+ fs.mkdirSync(dir, { recursive: true });
36
+ }
37
+
38
+ function copyExecutable(src, dst) {
39
+ fs.copyFileSync(src, dst);
40
+ fs.chmodSync(dst, 0o755);
41
+ }
42
+
43
+ function symlinkForce(target, linkPath) {
44
+ try {
45
+ fs.rmSync(linkPath, { force: true });
46
+ } catch (_) {
47
+ // Ignore stale link cleanup failures; copyExecutable below will surface real errors.
48
+ }
49
+ fs.symlinkSync(target, linkPath);
50
+ }
51
+
52
+ function copyIfMissing(src, dst, mode) {
53
+ if (fs.existsSync(dst)) {
54
+ console.log(`配置文件已存在: ${dst}`);
55
+ return;
56
+ }
57
+ fs.copyFileSync(src, dst);
58
+ if (mode) {
59
+ fs.chmodSync(dst, mode);
60
+ }
61
+ console.log(`配置文件已创建: ${dst}`);
62
+ }
63
+
64
+ function sleep(ms) {
65
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
66
+ }
67
+
68
+ function waitForSocket(timeoutMs) {
69
+ const deadline = Date.now() + timeoutMs;
70
+ while (Date.now() < deadline) {
71
+ try {
72
+ if (fs.statSync(socketPath).isSocket()) {
73
+ return true;
74
+ }
75
+ } catch (_) {
76
+ // Socket not ready yet.
77
+ }
78
+ sleep(100);
79
+ }
80
+ return false;
81
+ }
82
+
83
+ function main() {
84
+ if (process.platform !== "darwin") {
85
+ throw new Error("@xdfnet/ispeak currently supports macOS only.");
86
+ }
87
+
88
+ const goCheck = spawnSync("go", ["version"], { stdio: "ignore" });
89
+ if (goCheck.status !== 0) {
90
+ throw new Error("Go is required to install @xdfnet/ispeak from npm. Install Go first, then rerun npm i -g @xdfnet/ispeak.");
91
+ }
92
+
93
+ ensureDir(path.dirname(buildPath));
94
+ ensureDir(binDir);
95
+ ensureDir(configDir);
96
+ ensureDir(path.dirname(plistPath));
97
+
98
+ console.log("编译 ispeakd...");
99
+ run("go", ["build", "-ldflags=-s -w", "-o", buildPath, "."]);
100
+
101
+ console.log("停止旧服务...");
102
+ run("launchctl", ["unload", plistPath], { allowFailure: true, stdio: "ignore" });
103
+
104
+ copyExecutable(buildPath, binaryPath);
105
+ copyExecutable(path.join(root, "scripts", "ispeak"), cliPath);
106
+ symlinkForce(cliPath, path.join(binDir, "ispeak-claude"));
107
+ symlinkForce(cliPath, path.join(binDir, "ispeak-codex"));
108
+
109
+ copyIfMissing(path.join(root, "configs", "config.example.json"), path.join(configDir, "config.json"));
110
+ copyIfMissing(path.join(root, "configs", "hook-speak.sh"), path.join(configDir, "hook-speak.sh"), 0o755);
111
+
112
+ const plist = fs
113
+ .readFileSync(path.join(root, "configs", "com.iSpeak.plist"), "utf8")
114
+ .replaceAll("BINARY_PATH_PLACEHOLDER", binaryPath);
115
+ fs.writeFileSync(plistPath, plist);
116
+
117
+ console.log("启动服务...");
118
+ run("launchctl", ["load", plistPath]);
119
+
120
+ if (!waitForSocket(5000)) {
121
+ throw new Error(`service started but socket is not ready: ${socketPath}`);
122
+ }
123
+
124
+ const status = run(cliPath, ["status"], { stdio: "pipe" });
125
+ process.stdout.write(status.stdout);
126
+ console.log("\n安装成功!");
127
+ }
128
+
129
+ try {
130
+ main();
131
+ } catch (err) {
132
+ console.error(`ispeak npm install failed: ${err.message}`);
133
+ process.exit(1);
134
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@xdfnet/ispeak",
3
+ "version": "1.6.1",
4
+ "description": "Local macOS TTS daemon for AI coding assistants, powered by Volcengine streaming TTS.",
5
+ "license": "MIT",
6
+ "homepage": "https://github.com/xdfnet/iSpeak#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/xdfnet/iSpeak.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/xdfnet/iSpeak/issues"
13
+ },
14
+ "os": [
15
+ "darwin"
16
+ ],
17
+ "cpu": [
18
+ "arm64",
19
+ "x64"
20
+ ],
21
+ "bin": {
22
+ "ispeak": "scripts/ispeak",
23
+ "ispeak-claude": "scripts/ispeak",
24
+ "ispeak-codex": "scripts/ispeak"
25
+ },
26
+ "scripts": {
27
+ "build": "go build -ldflags=\"-s -w\" -o build/ispeakd .",
28
+ "test": "go test ./...",
29
+ "postinstall": "node npm/postinstall.js",
30
+ "pack:dry-run": "npm pack --dry-run"
31
+ },
32
+ "files": [
33
+ "main.go",
34
+ "go.mod",
35
+ "go.sum",
36
+ "scripts/",
37
+ "configs/",
38
+ "npm/",
39
+ "Docs/",
40
+ "README.md",
41
+ "LICENSE"
42
+ ],
43
+ "engines": {
44
+ "node": ">=18"
45
+ }
46
+ }
package/scripts/ispeak ADDED
@@ -0,0 +1,58 @@
1
+ #!/bin/bash
2
+ # ispeak — iSpeak 控制命令
3
+ set -euo pipefail
4
+
5
+ VERSION="1.6.1"
6
+ SOCK="$HOME/.config/iSpeak/ispeak.sock"
7
+ PLIST="$HOME/Library/LaunchAgents/com.iSpeak.plist"
8
+ CMD_NAME="$(basename "$0")"
9
+ SOURCE_PREFIX=""
10
+
11
+ case "$CMD_NAME" in
12
+ ispeak-claude) SOURCE_PREFIX="{source:claude}" ;;
13
+ ispeak-codex) SOURCE_PREFIX="{source:codex}" ;;
14
+ esac
15
+
16
+ cmd_status() {
17
+ echo "== iSpeak =="
18
+ if pgrep ispeakd > /dev/null 2>&1; then
19
+ echo " 进程: $(pgrep ispeakd) ✓"
20
+ else
21
+ echo " 进程: 未运行 ✗"
22
+ fi
23
+ if [[ -S "$SOCK" ]]; then
24
+ echo " Socket: ✓"
25
+ else
26
+ echo " Socket: ✗"
27
+ fi
28
+ }
29
+
30
+ cmd_restart() {
31
+ launchctl unload "$PLIST" 2>/dev/null || true
32
+ launchctl load "$PLIST"
33
+ sleep 0.5
34
+ if [[ -S "$SOCK" ]]; then
35
+ echo "ispeak: 已重启"
36
+ else
37
+ echo "ispeak: 重启失败" >&2
38
+ exit 1
39
+ fi
40
+ }
41
+
42
+ case "${1:-}" in
43
+ status) cmd_status ;;
44
+ restart) cmd_restart ;;
45
+ version|-v|--version) echo "iSpeak $VERSION" ;;
46
+ "")
47
+ echo "iSpeak $VERSION"
48
+ echo ""
49
+ echo "用法:"
50
+ echo " ispeak \"文本\" 播报"
51
+ echo " ispeak status 服务状态"
52
+ echo " ispeak restart 重启服务"
53
+ echo " ispeak version 版本"
54
+ ;;
55
+ *)
56
+ printf "%s%s" "$SOURCE_PREFIX" "$*" | nc -U -w5 "$SOCK" 2>/dev/null || echo "ispeak: socket 不可用" >&2
57
+ ;;
58
+ esac