@poco-ai/tokenarena 0.2.3 → 0.2.4

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/dist/index.js CHANGED
@@ -1,21 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- formatBullet,
4
- formatHeader,
5
- formatKeyValue,
6
- formatMutedPath,
7
- formatSection,
8
- formatStatusBadge,
9
- isCommandAvailable,
10
- isInteractiveTerminal,
11
- logger,
12
- maskSecret,
13
- promptConfirm,
14
- promptPassword,
15
- promptSelect,
16
- promptText,
17
- runInstallService
18
- } from "./chunk-XJKRJ3K2.js";
19
2
 
20
3
  // src/parsers/claude-code.ts
21
4
  import { existsSync as existsSync3 } from "fs";
@@ -1830,6 +1813,150 @@ function getDefaultApiUrl() {
1830
1813
  return process.env.TOKEN_ARENA_API_URL || DEFAULT_API_URL;
1831
1814
  }
1832
1815
 
1816
+ // src/infrastructure/ui/format.ts
1817
+ var hasColor = Boolean(process.stdout.isTTY && process.env.NO_COLOR !== "1");
1818
+ function withCode(code, value) {
1819
+ if (!hasColor) return value;
1820
+ return `\x1B[${code}m${value}\x1B[0m`;
1821
+ }
1822
+ function bold(value) {
1823
+ return withCode("1", value);
1824
+ }
1825
+ function dim(value) {
1826
+ return withCode("2", value);
1827
+ }
1828
+ function cyan(value) {
1829
+ return withCode("36", value);
1830
+ }
1831
+ function green(value) {
1832
+ return withCode("32", value);
1833
+ }
1834
+ function yellow(value) {
1835
+ return withCode("33", value);
1836
+ }
1837
+ function red(value) {
1838
+ return withCode("31", value);
1839
+ }
1840
+ function magenta(value) {
1841
+ return withCode("35", value);
1842
+ }
1843
+ function formatHeader(title, subtitle) {
1844
+ const lines = [`${cyan("\u25C8")} ${bold(title)}`];
1845
+ if (subtitle) {
1846
+ lines.push(dim(subtitle));
1847
+ }
1848
+ return `
1849
+ ${lines.join("\n")}`;
1850
+ }
1851
+ function formatSection(title) {
1852
+ return `
1853
+ ${bold(title)}`;
1854
+ }
1855
+ function formatKeyValue(label, value) {
1856
+ return ` ${dim(label.padEnd(14, " "))} ${value}`;
1857
+ }
1858
+ function formatBullet(value, tone = "neutral") {
1859
+ const icon = tone === "success" ? green("\u2714") : tone === "warning" ? yellow("!") : tone === "danger" ? red("\u2716") : cyan("\u2022");
1860
+ return ` ${icon} ${value}`;
1861
+ }
1862
+ function formatMutedPath(path) {
1863
+ return dim(path);
1864
+ }
1865
+ function maskSecret(value, visible = 8) {
1866
+ if (!value) return "(empty)";
1867
+ if (value.length <= visible) return value;
1868
+ return `${value.slice(0, visible)}\u2026`;
1869
+ }
1870
+ function formatStatusBadge(label, tone = "neutral") {
1871
+ if (tone === "success") return green(label);
1872
+ if (tone === "warning") return yellow(label);
1873
+ if (tone === "danger") return red(label);
1874
+ return magenta(label);
1875
+ }
1876
+
1877
+ // src/infrastructure/ui/prompts.ts
1878
+ import {
1879
+ confirm,
1880
+ input,
1881
+ password,
1882
+ select
1883
+ } from "@inquirer/prompts";
1884
+ function isInteractiveTerminal() {
1885
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
1886
+ }
1887
+ async function promptConfirm(options) {
1888
+ return confirm({
1889
+ message: options.message,
1890
+ default: options.defaultValue
1891
+ });
1892
+ }
1893
+ async function promptText(options) {
1894
+ return input({
1895
+ message: options.message,
1896
+ default: options.defaultValue,
1897
+ validate: options.validate
1898
+ });
1899
+ }
1900
+ async function promptPassword(options) {
1901
+ return password({
1902
+ message: options.message,
1903
+ mask: options.mask ?? "*",
1904
+ validate: options.validate
1905
+ });
1906
+ }
1907
+ async function promptSelect(options) {
1908
+ return select({
1909
+ message: options.message,
1910
+ choices: [...options.choices],
1911
+ pageSize: Math.min(Math.max(options.choices.length, 6), 10)
1912
+ });
1913
+ }
1914
+
1915
+ // src/utils/logger.ts
1916
+ var LOG_LEVELS = {
1917
+ debug: 0,
1918
+ info: 1,
1919
+ warn: 2,
1920
+ error: 3
1921
+ };
1922
+ var Logger = class {
1923
+ level;
1924
+ constructor(level = "info") {
1925
+ this.level = level;
1926
+ }
1927
+ setLevel(level) {
1928
+ this.level = level;
1929
+ }
1930
+ debug(msg) {
1931
+ if (LOG_LEVELS[this.level] <= LOG_LEVELS.debug) {
1932
+ process.stderr.write(`[debug] ${msg}
1933
+ `);
1934
+ }
1935
+ }
1936
+ info(msg) {
1937
+ if (LOG_LEVELS[this.level] <= LOG_LEVELS.info) {
1938
+ process.stdout.write(`${msg}
1939
+ `);
1940
+ }
1941
+ }
1942
+ warn(msg) {
1943
+ if (LOG_LEVELS[this.level] <= LOG_LEVELS.warn) {
1944
+ process.stderr.write(`warn: ${msg}
1945
+ `);
1946
+ }
1947
+ }
1948
+ error(msg) {
1949
+ if (LOG_LEVELS[this.level] <= LOG_LEVELS.error) {
1950
+ process.stderr.write(`error: ${msg}
1951
+ `);
1952
+ }
1953
+ }
1954
+ log(msg) {
1955
+ this.info(msg);
1956
+ }
1957
+ };
1958
+ var logger = new Logger();
1959
+
1833
1960
  // src/commands/config.ts
1834
1961
  var VALID_KEYS = ["apiKey", "apiUrl", "syncInterval", "logLevel"];
1835
1962
  function isConfigKey(value) {
@@ -2135,14 +2262,14 @@ import { hostname as hostname3 } from "os";
2135
2262
 
2136
2263
  // src/domain/project-identity.ts
2137
2264
  import { createHmac } from "crypto";
2138
- function toProjectIdentity(input) {
2139
- if (input.mode === "disabled") {
2265
+ function toProjectIdentity(input2) {
2266
+ if (input2.mode === "disabled") {
2140
2267
  return { projectKey: "unknown", projectLabel: "Unknown Project" };
2141
2268
  }
2142
- if (input.mode === "raw") {
2143
- return { projectKey: input.project, projectLabel: input.project };
2269
+ if (input2.mode === "raw") {
2270
+ return { projectKey: input2.project, projectLabel: input2.project };
2144
2271
  }
2145
- const projectKey = createHmac("sha256", input.salt).update(input.project).digest("hex").slice(0, 16);
2272
+ const projectKey = createHmac("sha256", input2.salt).update(input2.project).digest("hex").slice(0, 16);
2146
2273
  return {
2147
2274
  projectKey,
2148
2275
  projectLabel: `Project ${projectKey.slice(0, 6)}`
@@ -2158,13 +2285,13 @@ function shortHash(value) {
2158
2285
  function normalizeApiUrl(apiUrl) {
2159
2286
  return apiUrl.replace(/\/+$/, "");
2160
2287
  }
2161
- function buildUploadManifestScope(input) {
2288
+ function buildUploadManifestScope(input2) {
2162
2289
  return {
2163
- apiKeyHash: shortHash(input.apiKey),
2164
- apiUrl: normalizeApiUrl(input.apiUrl),
2165
- deviceId: input.deviceId,
2166
- projectHashSaltHash: shortHash(input.settings.projectHashSalt),
2167
- projectMode: input.settings.projectMode
2290
+ apiKeyHash: shortHash(input2.apiKey),
2291
+ apiUrl: normalizeApiUrl(input2.apiUrl),
2292
+ deviceId: input2.deviceId,
2293
+ projectHashSaltHash: shortHash(input2.settings.projectHashSalt),
2294
+ projectMode: input2.settings.projectMode
2168
2295
  };
2169
2296
  }
2170
2297
  function describeUploadManifestScopeChanges(previous, current) {
@@ -2234,20 +2361,20 @@ function buildRecordHashes(items, getKey, getHash) {
2234
2361
  }
2235
2362
  return hashes;
2236
2363
  }
2237
- function createUploadManifest(input) {
2364
+ function createUploadManifest(input2) {
2238
2365
  return {
2239
2366
  buckets: buildRecordHashes(
2240
- input.buckets,
2367
+ input2.buckets,
2241
2368
  getUploadBucketManifestKey,
2242
2369
  getUploadBucketContentHash
2243
2370
  ),
2244
- scope: input.scope,
2371
+ scope: input2.scope,
2245
2372
  sessions: buildRecordHashes(
2246
- input.sessions,
2373
+ input2.sessions,
2247
2374
  getUploadSessionManifestKey,
2248
2375
  getUploadSessionContentHash
2249
2376
  ),
2250
- updatedAt: input.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
2377
+ updatedAt: input2.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
2251
2378
  version: MANIFEST_VERSION
2252
2379
  };
2253
2380
  }
@@ -2260,21 +2387,21 @@ function countRemovedRecords(previous, current) {
2260
2387
  }
2261
2388
  return removed;
2262
2389
  }
2263
- function diffUploadManifest(input) {
2390
+ function diffUploadManifest(input2) {
2264
2391
  const nextManifest = createUploadManifest({
2265
- buckets: input.buckets,
2266
- scope: input.scope,
2267
- sessions: input.sessions,
2268
- updatedAt: input.updatedAt
2392
+ buckets: input2.buckets,
2393
+ scope: input2.scope,
2394
+ sessions: input2.sessions,
2395
+ updatedAt: input2.updatedAt
2269
2396
  });
2270
- const scopeChangedReasons = input.previous ? describeUploadManifestScopeChanges(input.previous.scope, input.scope) : [];
2271
- const previousBuckets = input.previous && scopeChangedReasons.length === 0 ? input.previous.buckets : {};
2272
- const previousSessions = input.previous && scopeChangedReasons.length === 0 ? input.previous.sessions : {};
2273
- const bucketsToUpload = input.buckets.filter((bucket) => {
2397
+ const scopeChangedReasons = input2.previous ? describeUploadManifestScopeChanges(input2.previous.scope, input2.scope) : [];
2398
+ const previousBuckets = input2.previous && scopeChangedReasons.length === 0 ? input2.previous.buckets : {};
2399
+ const previousSessions = input2.previous && scopeChangedReasons.length === 0 ? input2.previous.sessions : {};
2400
+ const bucketsToUpload = input2.buckets.filter((bucket) => {
2274
2401
  const key = getUploadBucketManifestKey(bucket);
2275
2402
  return previousBuckets[key] !== nextManifest.buckets[key];
2276
2403
  });
2277
- const sessionsToUpload = input.sessions.filter((session) => {
2404
+ const sessionsToUpload = input2.sessions.filter((session) => {
2278
2405
  const key = getUploadSessionManifestKey(session);
2279
2406
  return previousSessions[key] !== nextManifest.sessions[key];
2280
2407
  });
@@ -2288,8 +2415,8 @@ function diffUploadManifest(input) {
2288
2415
  ),
2289
2416
  scopeChangedReasons,
2290
2417
  sessionsToUpload,
2291
- unchangedBuckets: input.buckets.length - bucketsToUpload.length,
2292
- unchangedSessions: input.sessions.length - sessionsToUpload.length
2418
+ unchangedBuckets: input2.buckets.length - bucketsToUpload.length,
2419
+ unchangedSessions: input2.sessions.length - sessionsToUpload.length
2293
2420
  };
2294
2421
  }
2295
2422
 
@@ -3149,18 +3276,706 @@ View your dashboard at: ${apiUrl}/usage`);
3149
3276
  }
3150
3277
 
3151
3278
  // src/commands/init.ts
3152
- import { execFileSync as execFileSync2, spawn } from "child_process";
3153
- import { existsSync as existsSync16 } from "fs";
3279
+ import { execFileSync as execFileSync4, spawn } from "child_process";
3280
+ import { existsSync as existsSync18 } from "fs";
3154
3281
  import { appendFile, mkdir, readFile } from "fs/promises";
3282
+ import { homedir as homedir14, platform as platform4 } from "os";
3283
+ import { dirname as dirname3, join as join17, posix, win32 } from "path";
3284
+
3285
+ // src/infrastructure/service/index.ts
3286
+ import { platform as platform3 } from "os";
3287
+
3288
+ // src/infrastructure/service/linux-systemd.ts
3289
+ import { execFileSync as execFileSync2 } from "child_process";
3290
+ import { existsSync as existsSync16, mkdirSync as mkdirSync3, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
3155
3291
  import { homedir as homedir12, platform } from "os";
3156
- import { dirname as dirname3, join as join15, posix, win32 } from "path";
3292
+ import { join as join15 } from "path";
3293
+
3294
+ // src/utils/command.ts
3295
+ import { execSync } from "child_process";
3296
+ function isCommandAvailable(command) {
3297
+ try {
3298
+ execSync(`command -v ${command}`, { stdio: "ignore" });
3299
+ return true;
3300
+ } catch {
3301
+ return false;
3302
+ }
3303
+ }
3304
+
3305
+ // src/infrastructure/service/utils.ts
3306
+ var SERVICE_PATH_FALLBACKS = [
3307
+ "/opt/homebrew/bin",
3308
+ "/usr/local/bin",
3309
+ "/usr/bin",
3310
+ "/bin",
3311
+ "/usr/sbin",
3312
+ "/sbin"
3313
+ ];
3314
+ var SERVICE_ENV_KEYS = [
3315
+ "TOKEN_ARENA_DEV",
3316
+ "XDG_CONFIG_HOME",
3317
+ "XDG_STATE_HOME",
3318
+ "XDG_RUNTIME_DIR"
3319
+ ];
3320
+ function dedupePaths(paths) {
3321
+ return [...new Set(paths.filter(Boolean))];
3322
+ }
3323
+ function getManagedServiceEnvironment(env = process.env) {
3324
+ const pathEntries = dedupePaths([
3325
+ ...env.PATH?.split(":") ?? [],
3326
+ ...SERVICE_PATH_FALLBACKS
3327
+ ]);
3328
+ const next = {
3329
+ PATH: pathEntries.join(":")
3330
+ };
3331
+ for (const key of SERVICE_ENV_KEYS) {
3332
+ const value = env[key];
3333
+ if (value) {
3334
+ next[key] = value;
3335
+ }
3336
+ }
3337
+ return next;
3338
+ }
3339
+ function resolveManagedDaemonCommand(execPath = process.execPath, argv = process.argv) {
3340
+ const scriptPath = argv[1];
3341
+ if (!scriptPath) {
3342
+ throw new Error("\u65E0\u6CD5\u89E3\u6790 CLI \u5165\u53E3\u8DEF\u5F84\uFF0C\u8BF7\u901A\u8FC7 tokenarena \u547D\u4EE4\u91CD\u65B0\u6267\u884C\u3002");
3343
+ }
3344
+ return {
3345
+ execPath,
3346
+ args: [scriptPath, "daemon", "--service"]
3347
+ };
3348
+ }
3349
+ function escapeDoubleQuotedValue(value) {
3350
+ return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
3351
+ }
3352
+ function escapeXml(value) {
3353
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
3354
+ }
3355
+
3356
+ // src/infrastructure/service/linux-systemd.ts
3357
+ var SYSTEMD_SERVICE_NAME = "tokenarena";
3358
+ function getLinuxSystemdServiceDir(homePath = homedir12()) {
3359
+ return join15(homePath, ".config/systemd/user");
3360
+ }
3361
+ function getLinuxSystemdServiceFile(homePath = homedir12()) {
3362
+ return join15(
3363
+ getLinuxSystemdServiceDir(homePath),
3364
+ `${SYSTEMD_SERVICE_NAME}.service`
3365
+ );
3366
+ }
3367
+ function buildSystemdServiceContent(options) {
3368
+ const envLines = Object.entries(options.environment).map(
3369
+ ([key, value]) => `Environment="${escapeDoubleQuotedValue(key)}=${escapeDoubleQuotedValue(value)}"`
3370
+ ).join("\n");
3371
+ const execArgs = [options.execPath, ...options.args].map((value) => `"${escapeDoubleQuotedValue(value)}"`).join(" ");
3372
+ return `[Unit]
3373
+ Description=TokenArena Daemon - AI Usage Tracker
3374
+ After=network-online.target
3375
+ Wants=network-online.target
3376
+
3377
+ [Service]
3378
+ Type=simple
3379
+ ExecStart=${execArgs}
3380
+ Restart=on-failure
3381
+ RestartSec=10
3382
+ ${envLines}
3383
+
3384
+ [Install]
3385
+ WantedBy=default.target
3386
+ `;
3387
+ }
3388
+ function getSystemdSupport() {
3389
+ if (platform() !== "linux") {
3390
+ return {
3391
+ ok: false,
3392
+ reason: "systemd \u4EC5\u5728 Linux \u4E0A\u53EF\u7528\u3002"
3393
+ };
3394
+ }
3395
+ if (!isCommandAvailable("systemctl")) {
3396
+ return {
3397
+ ok: false,
3398
+ reason: "\u672A\u68C0\u6D4B\u5230 systemctl\u3002"
3399
+ };
3400
+ }
3401
+ return { ok: true };
3402
+ }
3403
+ function execSystemctl(args) {
3404
+ execFileSync2("systemctl", ["--user", ...args], {
3405
+ stdio: "inherit"
3406
+ });
3407
+ }
3408
+ function ensureSystemdAvailable() {
3409
+ const support = getSystemdSupport();
3410
+ if (support.ok) {
3411
+ return true;
3412
+ }
3413
+ logger.info(formatBullet(`systemd \u4E0D\u53EF\u7528\u3002${support.reason}`, "warning"));
3414
+ return false;
3415
+ }
3416
+ function createLinuxSystemdServiceBackend() {
3417
+ function isInstalled() {
3418
+ return existsSync16(getLinuxSystemdServiceFile());
3419
+ }
3420
+ async function setup(skipPrompt = false) {
3421
+ if (!ensureSystemdAvailable()) {
3422
+ return;
3423
+ }
3424
+ const serviceDir = getLinuxSystemdServiceDir();
3425
+ const serviceFile = getLinuxSystemdServiceFile();
3426
+ logger.info(formatHeader("\u8BBE\u7F6E systemd \u670D\u52A1", "TokenArena daemon"));
3427
+ if (!skipPrompt) {
3428
+ const shouldSetup = await promptConfirm({
3429
+ message: "\u662F\u5426\u521B\u5EFA\u5E76\u542F\u7528 systemd \u7528\u6237\u670D\u52A1\uFF1F",
3430
+ defaultValue: true
3431
+ });
3432
+ if (!shouldSetup) {
3433
+ logger.info(formatBullet("\u5DF2\u53D6\u6D88\u670D\u52A1\u8BBE\u7F6E\u3002"));
3434
+ return;
3435
+ }
3436
+ }
3437
+ try {
3438
+ const command = resolveManagedDaemonCommand();
3439
+ const content = buildSystemdServiceContent({
3440
+ environment: getManagedServiceEnvironment(),
3441
+ execPath: command.execPath,
3442
+ args: command.args
3443
+ });
3444
+ mkdirSync3(serviceDir, { recursive: true });
3445
+ writeFileSync5(serviceFile, content, "utf-8");
3446
+ execSystemctl(["daemon-reload"]);
3447
+ execSystemctl(["enable", SYSTEMD_SERVICE_NAME]);
3448
+ execSystemctl(["start", SYSTEMD_SERVICE_NAME]);
3449
+ logger.info(formatSection("\u670D\u52A1\u5DF2\u8BBE\u7F6E"));
3450
+ logger.info(formatBullet(`\u670D\u52A1\u6587\u4EF6: ${serviceFile}`, "success"));
3451
+ logger.info(formatBullet("\u670D\u52A1\u5DF2\u542F\u7528\u5E76\u542F\u52A8", "success"));
3452
+ logger.info(
3453
+ formatKeyValue("\u67E5\u770B\u72B6\u6001", "systemctl --user status tokenarena")
3454
+ );
3455
+ } catch (err) {
3456
+ logger.error(`\u8BBE\u7F6E\u670D\u52A1\u5931\u8D25: ${err.message}`);
3457
+ throw err;
3458
+ }
3459
+ }
3460
+ async function start() {
3461
+ if (!ensureSystemdAvailable()) {
3462
+ return;
3463
+ }
3464
+ if (!isInstalled()) {
3465
+ logger.info(
3466
+ formatBullet(
3467
+ "\u670D\u52A1\u6587\u4EF6\u4E0D\u5B58\u5728\u3002\u8BF7\u5148\u8FD0\u884C 'tokenarena service setup'\u3002",
3468
+ "warning"
3469
+ )
3470
+ );
3471
+ return;
3472
+ }
3473
+ try {
3474
+ execSystemctl(["start", SYSTEMD_SERVICE_NAME]);
3475
+ logger.info(formatBullet("\u670D\u52A1\u5DF2\u542F\u52A8", "success"));
3476
+ } catch (err) {
3477
+ logger.error(`\u542F\u52A8\u670D\u52A1\u5931\u8D25: ${err.message}`);
3478
+ throw err;
3479
+ }
3480
+ }
3481
+ async function stop() {
3482
+ if (!ensureSystemdAvailable()) {
3483
+ return;
3484
+ }
3485
+ if (!isInstalled()) {
3486
+ logger.info(
3487
+ formatBullet(
3488
+ "\u670D\u52A1\u6587\u4EF6\u4E0D\u5B58\u5728\u3002\u8BF7\u5148\u8FD0\u884C 'tokenarena service setup'\u3002",
3489
+ "warning"
3490
+ )
3491
+ );
3492
+ return;
3493
+ }
3494
+ try {
3495
+ execSystemctl(["stop", SYSTEMD_SERVICE_NAME]);
3496
+ logger.info(formatBullet("\u670D\u52A1\u5DF2\u505C\u6B62", "success"));
3497
+ } catch (err) {
3498
+ logger.error(`\u505C\u6B62\u670D\u52A1\u5931\u8D25: ${err.message}`);
3499
+ throw err;
3500
+ }
3501
+ }
3502
+ async function restart() {
3503
+ if (!ensureSystemdAvailable()) {
3504
+ return;
3505
+ }
3506
+ if (!isInstalled()) {
3507
+ logger.info(
3508
+ formatBullet(
3509
+ "\u670D\u52A1\u6587\u4EF6\u4E0D\u5B58\u5728\u3002\u8BF7\u5148\u8FD0\u884C 'tokenarena service setup'\u3002",
3510
+ "warning"
3511
+ )
3512
+ );
3513
+ return;
3514
+ }
3515
+ try {
3516
+ execSystemctl(["restart", SYSTEMD_SERVICE_NAME]);
3517
+ logger.info(formatBullet("\u670D\u52A1\u5DF2\u91CD\u542F", "success"));
3518
+ } catch (err) {
3519
+ logger.error(`\u91CD\u542F\u670D\u52A1\u5931\u8D25: ${err.message}`);
3520
+ throw err;
3521
+ }
3522
+ }
3523
+ async function status() {
3524
+ if (!ensureSystemdAvailable()) {
3525
+ return;
3526
+ }
3527
+ if (!isInstalled()) {
3528
+ logger.info(
3529
+ formatBullet(
3530
+ "\u670D\u52A1\u6587\u4EF6\u4E0D\u5B58\u5728\u3002\u8BF7\u5148\u8FD0\u884C 'tokenarena service setup'\u3002",
3531
+ "warning"
3532
+ )
3533
+ );
3534
+ return;
3535
+ }
3536
+ try {
3537
+ execSystemctl(["status", SYSTEMD_SERVICE_NAME]);
3538
+ } catch {
3539
+ }
3540
+ }
3541
+ async function uninstall(skipPrompt = false) {
3542
+ const serviceFile = getLinuxSystemdServiceFile();
3543
+ if (!existsSync16(serviceFile)) {
3544
+ logger.info(formatBullet("\u670D\u52A1\u6587\u4EF6\u4E0D\u5B58\u5728\u3002", "warning"));
3545
+ return;
3546
+ }
3547
+ if (!skipPrompt) {
3548
+ const shouldUninstall = await promptConfirm({
3549
+ message: "\u662F\u5426\u5378\u8F7D systemd \u670D\u52A1\uFF1F",
3550
+ defaultValue: false
3551
+ });
3552
+ if (!shouldUninstall) {
3553
+ logger.info(formatBullet("\u5DF2\u53D6\u6D88\u5378\u8F7D\u3002"));
3554
+ return;
3555
+ }
3556
+ }
3557
+ const support = getSystemdSupport();
3558
+ if (support.ok) {
3559
+ try {
3560
+ execSystemctl(["stop", SYSTEMD_SERVICE_NAME]);
3561
+ } catch {
3562
+ }
3563
+ try {
3564
+ execSystemctl(["disable", SYSTEMD_SERVICE_NAME]);
3565
+ } catch {
3566
+ }
3567
+ } else {
3568
+ logger.info(
3569
+ formatBullet(
3570
+ `\u5F53\u524D\u65E0\u6CD5\u8BBF\u95EE systemd\uFF0C\u5C06\u53EA\u5220\u9664\u670D\u52A1\u6587\u4EF6\u3002${support.reason}`,
3571
+ "warning"
3572
+ )
3573
+ );
3574
+ }
3575
+ try {
3576
+ rmSync2(serviceFile);
3577
+ if (support.ok) {
3578
+ execSystemctl(["daemon-reload"]);
3579
+ }
3580
+ logger.info(formatSection("\u670D\u52A1\u5DF2\u5378\u8F7D"));
3581
+ logger.info(formatBullet("\u670D\u52A1\u5DF2\u505C\u7528\u5E76\u5220\u9664", "success"));
3582
+ } catch (err) {
3583
+ logger.error(`\u5378\u8F7D\u670D\u52A1\u5931\u8D25: ${err.message}`);
3584
+ throw err;
3585
+ }
3586
+ }
3587
+ return {
3588
+ displayName: "systemd \u7528\u6237\u670D\u52A1",
3589
+ canSetup: getSystemdSupport,
3590
+ isInstalled,
3591
+ getDefinitionPath: getLinuxSystemdServiceFile,
3592
+ getStatusHint: () => "systemctl --user status tokenarena",
3593
+ setup,
3594
+ start,
3595
+ stop,
3596
+ restart,
3597
+ status,
3598
+ uninstall
3599
+ };
3600
+ }
3601
+
3602
+ // src/infrastructure/service/macos-launchd.ts
3603
+ import { execFileSync as execFileSync3 } from "child_process";
3604
+ import { existsSync as existsSync17, mkdirSync as mkdirSync4, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
3605
+ import { homedir as homedir13, platform as platform2 } from "os";
3606
+ import { join as join16 } from "path";
3607
+ var MACOS_LAUNCHD_LABEL = "com.poco-ai.tokenarena";
3608
+ function getCurrentUid() {
3609
+ return typeof process.getuid === "function" ? process.getuid() : null;
3610
+ }
3611
+ function getMacosLaunchAgentDir(homePath = homedir13()) {
3612
+ return join16(homePath, "Library/LaunchAgents");
3613
+ }
3614
+ function getMacosLaunchAgentFile(homePath = homedir13()) {
3615
+ return join16(getMacosLaunchAgentDir(homePath), `${MACOS_LAUNCHD_LABEL}.plist`);
3616
+ }
3617
+ function getMacosLaunchdDomain(uid = getCurrentUid()) {
3618
+ if (uid == null) {
3619
+ throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5F53\u524D\u7528\u6237 UID\u3002");
3620
+ }
3621
+ return `gui/${uid}`;
3622
+ }
3623
+ function getMacosLaunchdServiceTarget(uid = getCurrentUid()) {
3624
+ return `${getMacosLaunchdDomain(uid)}/${MACOS_LAUNCHD_LABEL}`;
3625
+ }
3626
+ function getMacosLaunchdLogPaths(stateDir = getStateDir()) {
3627
+ return {
3628
+ stdoutPath: join16(stateDir, "launchd.stdout.log"),
3629
+ stderrPath: join16(stateDir, "launchd.stderr.log")
3630
+ };
3631
+ }
3632
+ function renderPlistArray(values) {
3633
+ return values.map((value) => ` <string>${escapeXml(value)}</string>`).join("\n");
3634
+ }
3635
+ function renderPlistDict(entries) {
3636
+ return Object.entries(entries).map(
3637
+ ([key, value]) => ` <key>${escapeXml(key)}</key>
3638
+ <string>${escapeXml(value)}</string>`
3639
+ ).join("\n");
3640
+ }
3641
+ function buildLaunchdPlist(options) {
3642
+ return `<?xml version="1.0" encoding="UTF-8"?>
3643
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3644
+ <plist version="1.0">
3645
+ <dict>
3646
+ <key>Label</key>
3647
+ <string>${escapeXml(options.label)}</string>
3648
+ <key>ProgramArguments</key>
3649
+ <array>
3650
+ ${renderPlistArray(options.programArguments)}
3651
+ </array>
3652
+ <key>WorkingDirectory</key>
3653
+ <string>${escapeXml(options.workingDirectory)}</string>
3654
+ <key>EnvironmentVariables</key>
3655
+ <dict>
3656
+ ${renderPlistDict(options.environment)}
3657
+ </dict>
3658
+ <key>KeepAlive</key>
3659
+ <dict>
3660
+ <key>SuccessfulExit</key>
3661
+ <false/>
3662
+ </dict>
3663
+ <key>ProcessType</key>
3664
+ <string>Background</string>
3665
+ <key>ThrottleInterval</key>
3666
+ <integer>30</integer>
3667
+ <key>ExitTimeOut</key>
3668
+ <integer>15</integer>
3669
+ <key>StandardOutPath</key>
3670
+ <string>${escapeXml(options.standardOutPath)}</string>
3671
+ <key>StandardErrorPath</key>
3672
+ <string>${escapeXml(options.standardErrorPath)}</string>
3673
+ </dict>
3674
+ </plist>
3675
+ `;
3676
+ }
3677
+ function getLaunchctlSupport() {
3678
+ if (platform2() !== "darwin") {
3679
+ return {
3680
+ ok: false,
3681
+ reason: "launchd \u4EC5\u5728 macOS \u4E0A\u53EF\u7528\u3002"
3682
+ };
3683
+ }
3684
+ if (!isCommandAvailable("launchctl")) {
3685
+ return {
3686
+ ok: false,
3687
+ reason: "\u672A\u68C0\u6D4B\u5230 launchctl\u3002"
3688
+ };
3689
+ }
3690
+ const uid = getCurrentUid();
3691
+ if (uid == null) {
3692
+ return {
3693
+ ok: false,
3694
+ reason: "\u65E0\u6CD5\u83B7\u53D6\u5F53\u524D\u7528\u6237 UID\u3002"
3695
+ };
3696
+ }
3697
+ try {
3698
+ execFileSync3("launchctl", ["print", getMacosLaunchdDomain(uid)], {
3699
+ stdio: "ignore"
3700
+ });
3701
+ return { ok: true };
3702
+ } catch {
3703
+ return {
3704
+ ok: false,
3705
+ reason: "\u5F53\u524D\u672A\u68C0\u6D4B\u5230\u56FE\u5F62\u5316\u767B\u5F55\u4F1A\u8BDD\uFF0C\u8BF7\u5728\u684C\u9762\u7EC8\u7AEF\u4E2D\u6267\u884C\u3002"
3706
+ };
3707
+ }
3708
+ }
3709
+ function execLaunchctl(args, inherit = true) {
3710
+ execFileSync3("launchctl", args, {
3711
+ stdio: inherit ? "inherit" : "ignore"
3712
+ });
3713
+ }
3714
+ function isLoaded() {
3715
+ try {
3716
+ execLaunchctl(["print", getMacosLaunchdServiceTarget()], false);
3717
+ return true;
3718
+ } catch {
3719
+ return false;
3720
+ }
3721
+ }
3722
+ function ensureLaunchctlAvailable() {
3723
+ const support = getLaunchctlSupport();
3724
+ if (support.ok) {
3725
+ return true;
3726
+ }
3727
+ logger.info(formatBullet(`launchd \u4E0D\u53EF\u7528\u3002${support.reason}`, "warning"));
3728
+ return false;
3729
+ }
3730
+ function writeLaunchAgentPlist() {
3731
+ const plistFile = getMacosLaunchAgentFile();
3732
+ const { stdoutPath, stderrPath } = getMacosLaunchdLogPaths();
3733
+ const command = resolveManagedDaemonCommand();
3734
+ const plist = buildLaunchdPlist({
3735
+ label: MACOS_LAUNCHD_LABEL,
3736
+ programArguments: [command.execPath, ...command.args],
3737
+ environment: getManagedServiceEnvironment(),
3738
+ workingDirectory: homedir13(),
3739
+ standardOutPath: stdoutPath,
3740
+ standardErrorPath: stderrPath
3741
+ });
3742
+ ensureAppDirs();
3743
+ mkdirSync4(getMacosLaunchAgentDir(), { recursive: true });
3744
+ writeFileSync6(plistFile, plist, "utf-8");
3745
+ return plistFile;
3746
+ }
3747
+ function bootstrapLaunchAgent() {
3748
+ const domain = getMacosLaunchdDomain();
3749
+ const serviceTarget = getMacosLaunchdServiceTarget();
3750
+ const plistFile = getMacosLaunchAgentFile();
3751
+ if (isLoaded()) {
3752
+ try {
3753
+ execLaunchctl(["bootout", serviceTarget]);
3754
+ } catch {
3755
+ }
3756
+ }
3757
+ execLaunchctl(["bootstrap", domain, plistFile]);
3758
+ execLaunchctl(["enable", serviceTarget]);
3759
+ execLaunchctl(["kickstart", "-k", serviceTarget]);
3760
+ }
3761
+ function createMacosLaunchdServiceBackend() {
3762
+ function isInstalled() {
3763
+ return existsSync17(getMacosLaunchAgentFile());
3764
+ }
3765
+ async function setup(skipPrompt = false) {
3766
+ if (!ensureLaunchctlAvailable()) {
3767
+ return;
3768
+ }
3769
+ logger.info(formatHeader("\u8BBE\u7F6E launchd \u670D\u52A1", "TokenArena daemon"));
3770
+ if (!skipPrompt) {
3771
+ const shouldSetup = await promptConfirm({
3772
+ message: "\u662F\u5426\u521B\u5EFA\u5E76\u542F\u7528 launchd \u7528\u6237\u670D\u52A1\uFF1F",
3773
+ defaultValue: true
3774
+ });
3775
+ if (!shouldSetup) {
3776
+ logger.info(formatBullet("\u5DF2\u53D6\u6D88\u670D\u52A1\u8BBE\u7F6E\u3002"));
3777
+ return;
3778
+ }
3779
+ }
3780
+ try {
3781
+ const plistFile = writeLaunchAgentPlist();
3782
+ bootstrapLaunchAgent();
3783
+ logger.info(formatSection("\u670D\u52A1\u5DF2\u8BBE\u7F6E"));
3784
+ logger.info(formatBullet(`\u670D\u52A1\u6587\u4EF6: ${plistFile}`, "success"));
3785
+ logger.info(formatBullet("\u670D\u52A1\u5DF2\u542F\u7528\u5E76\u542F\u52A8", "success"));
3786
+ logger.info(
3787
+ formatKeyValue(
3788
+ "\u67E5\u770B\u72B6\u6001",
3789
+ `launchctl print ${getMacosLaunchdServiceTarget()}`
3790
+ )
3791
+ );
3792
+ } catch (err) {
3793
+ logger.error(`\u8BBE\u7F6E\u670D\u52A1\u5931\u8D25: ${err.message}`);
3794
+ throw err;
3795
+ }
3796
+ }
3797
+ async function start() {
3798
+ if (!ensureLaunchctlAvailable()) {
3799
+ return;
3800
+ }
3801
+ if (!isInstalled()) {
3802
+ logger.info(
3803
+ formatBullet(
3804
+ "\u670D\u52A1\u6587\u4EF6\u4E0D\u5B58\u5728\u3002\u8BF7\u5148\u8FD0\u884C 'tokenarena service setup'\u3002",
3805
+ "warning"
3806
+ )
3807
+ );
3808
+ return;
3809
+ }
3810
+ try {
3811
+ if (!isLoaded()) {
3812
+ execLaunchctl([
3813
+ "bootstrap",
3814
+ getMacosLaunchdDomain(),
3815
+ getMacosLaunchAgentFile()
3816
+ ]);
3817
+ }
3818
+ execLaunchctl(["enable", getMacosLaunchdServiceTarget()]);
3819
+ execLaunchctl(["kickstart", "-k", getMacosLaunchdServiceTarget()]);
3820
+ logger.info(formatBullet("\u670D\u52A1\u5DF2\u542F\u52A8", "success"));
3821
+ } catch (err) {
3822
+ logger.error(`\u542F\u52A8\u670D\u52A1\u5931\u8D25: ${err.message}`);
3823
+ throw err;
3824
+ }
3825
+ }
3826
+ async function stop() {
3827
+ if (!isInstalled()) {
3828
+ logger.info(
3829
+ formatBullet(
3830
+ "\u670D\u52A1\u6587\u4EF6\u4E0D\u5B58\u5728\u3002\u8BF7\u5148\u8FD0\u884C 'tokenarena service setup'\u3002",
3831
+ "warning"
3832
+ )
3833
+ );
3834
+ return;
3835
+ }
3836
+ if (!ensureLaunchctlAvailable()) {
3837
+ return;
3838
+ }
3839
+ if (!isLoaded()) {
3840
+ logger.info(formatBullet("\u670D\u52A1\u5F53\u524D\u672A\u8FD0\u884C\u3002", "warning"));
3841
+ return;
3842
+ }
3843
+ try {
3844
+ execLaunchctl(["bootout", getMacosLaunchdServiceTarget()]);
3845
+ logger.info(formatBullet("\u670D\u52A1\u5DF2\u505C\u6B62", "success"));
3846
+ } catch (err) {
3847
+ logger.error(`\u505C\u6B62\u670D\u52A1\u5931\u8D25: ${err.message}`);
3848
+ throw err;
3849
+ }
3850
+ }
3851
+ async function restart() {
3852
+ if (!ensureLaunchctlAvailable()) {
3853
+ return;
3854
+ }
3855
+ if (!isInstalled()) {
3856
+ logger.info(
3857
+ formatBullet(
3858
+ "\u670D\u52A1\u6587\u4EF6\u4E0D\u5B58\u5728\u3002\u8BF7\u5148\u8FD0\u884C 'tokenarena service setup'\u3002",
3859
+ "warning"
3860
+ )
3861
+ );
3862
+ return;
3863
+ }
3864
+ try {
3865
+ if (isLoaded()) {
3866
+ execLaunchctl(["kickstart", "-k", getMacosLaunchdServiceTarget()]);
3867
+ } else {
3868
+ execLaunchctl([
3869
+ "bootstrap",
3870
+ getMacosLaunchdDomain(),
3871
+ getMacosLaunchAgentFile()
3872
+ ]);
3873
+ execLaunchctl(["enable", getMacosLaunchdServiceTarget()]);
3874
+ execLaunchctl(["kickstart", "-k", getMacosLaunchdServiceTarget()]);
3875
+ }
3876
+ logger.info(formatBullet("\u670D\u52A1\u5DF2\u91CD\u542F", "success"));
3877
+ } catch (err) {
3878
+ logger.error(`\u91CD\u542F\u670D\u52A1\u5931\u8D25: ${err.message}`);
3879
+ throw err;
3880
+ }
3881
+ }
3882
+ async function status() {
3883
+ if (!isInstalled()) {
3884
+ logger.info(
3885
+ formatBullet(
3886
+ "\u670D\u52A1\u6587\u4EF6\u4E0D\u5B58\u5728\u3002\u8BF7\u5148\u8FD0\u884C 'tokenarena service setup'\u3002",
3887
+ "warning"
3888
+ )
3889
+ );
3890
+ return;
3891
+ }
3892
+ if (!ensureLaunchctlAvailable()) {
3893
+ return;
3894
+ }
3895
+ try {
3896
+ execLaunchctl(["print", getMacosLaunchdServiceTarget()]);
3897
+ } catch {
3898
+ logger.info(formatBullet("\u670D\u52A1\u5F53\u524D\u672A\u52A0\u8F7D\u3002", "warning"));
3899
+ logger.info(formatKeyValue("\u670D\u52A1\u6587\u4EF6", getMacosLaunchAgentFile()));
3900
+ }
3901
+ }
3902
+ async function uninstall(skipPrompt = false) {
3903
+ const plistFile = getMacosLaunchAgentFile();
3904
+ if (!existsSync17(plistFile)) {
3905
+ logger.info(formatBullet("\u670D\u52A1\u6587\u4EF6\u4E0D\u5B58\u5728\u3002", "warning"));
3906
+ return;
3907
+ }
3908
+ if (!skipPrompt) {
3909
+ const shouldUninstall = await promptConfirm({
3910
+ message: "\u662F\u5426\u5378\u8F7D launchd \u670D\u52A1\uFF1F",
3911
+ defaultValue: false
3912
+ });
3913
+ if (!shouldUninstall) {
3914
+ logger.info(formatBullet("\u5DF2\u53D6\u6D88\u5378\u8F7D\u3002"));
3915
+ return;
3916
+ }
3917
+ }
3918
+ const support = getLaunchctlSupport();
3919
+ if (support.ok && isLoaded()) {
3920
+ try {
3921
+ execLaunchctl(["bootout", getMacosLaunchdServiceTarget()]);
3922
+ } catch {
3923
+ }
3924
+ } else if (!support.ok) {
3925
+ logger.info(
3926
+ formatBullet(
3927
+ `\u5F53\u524D\u65E0\u6CD5\u8BBF\u95EE launchd\uFF0C\u4F1A\u76F4\u63A5\u5220\u9664\u672C\u5730 plist \u6587\u4EF6\u3002${support.reason}`,
3928
+ "warning"
3929
+ )
3930
+ );
3931
+ }
3932
+ try {
3933
+ const { stdoutPath, stderrPath } = getMacosLaunchdLogPaths();
3934
+ rmSync3(plistFile);
3935
+ rmSync3(stdoutPath, { force: true });
3936
+ rmSync3(stderrPath, { force: true });
3937
+ logger.info(formatSection("\u670D\u52A1\u5DF2\u5378\u8F7D"));
3938
+ logger.info(formatBullet("\u670D\u52A1\u5DF2\u505C\u7528\u5E76\u5220\u9664", "success"));
3939
+ } catch (err) {
3940
+ logger.error(`\u5378\u8F7D\u670D\u52A1\u5931\u8D25: ${err.message}`);
3941
+ throw err;
3942
+ }
3943
+ }
3944
+ return {
3945
+ displayName: "launchd \u7528\u6237\u670D\u52A1",
3946
+ canSetup: getLaunchctlSupport,
3947
+ isInstalled,
3948
+ getDefinitionPath: getMacosLaunchAgentFile,
3949
+ getStatusHint: () => `launchctl print ${getMacosLaunchdServiceTarget()}`,
3950
+ setup,
3951
+ start,
3952
+ stop,
3953
+ restart,
3954
+ status,
3955
+ uninstall
3956
+ };
3957
+ }
3958
+
3959
+ // src/infrastructure/service/index.ts
3960
+ function getServiceBackend(currentPlatform = platform3()) {
3961
+ switch (currentPlatform) {
3962
+ case "linux":
3963
+ return createLinuxSystemdServiceBackend();
3964
+ case "darwin":
3965
+ return createMacosLaunchdServiceBackend();
3966
+ default:
3967
+ return null;
3968
+ }
3969
+ }
3970
+
3971
+ // src/commands/init.ts
3157
3972
  function joinForPlatform(currentPlatform, ...parts) {
3158
3973
  return currentPlatform === "win32" ? win32.join(...parts) : posix.join(...parts);
3159
3974
  }
3160
- function basenameLikeShell(input) {
3161
- return input.split(/[\\/]+/).pop()?.replace(/\.exe$/i, "") ?? input;
3975
+ function basenameLikeShell(input2) {
3976
+ return input2.split(/[\\/]+/).pop()?.replace(/\.exe$/i, "") ?? input2;
3162
3977
  }
3163
- function getBrowserLaunchCommand(url, currentPlatform = platform()) {
3978
+ function getBrowserLaunchCommand(url, currentPlatform = platform4()) {
3164
3979
  switch (currentPlatform) {
3165
3980
  case "darwin":
3166
3981
  return {
@@ -3197,11 +4012,11 @@ function resolvePowerShellProfilePath() {
3197
4012
  const systemRoot = process.env.SYSTEMROOT || "C:\\Windows";
3198
4013
  const candidates = [
3199
4014
  "pwsh.exe",
3200
- join15(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
4015
+ join17(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
3201
4016
  ];
3202
4017
  for (const command of candidates) {
3203
4018
  try {
3204
- const output = execFileSync2(
4019
+ const output = execFileSync4(
3205
4020
  command,
3206
4021
  [
3207
4022
  "-NoLogo",
@@ -3224,10 +4039,10 @@ function resolvePowerShellProfilePath() {
3224
4039
  return null;
3225
4040
  }
3226
4041
  function resolveShellAliasSetup(options = {}) {
3227
- const currentPlatform = options.currentPlatform ?? platform();
4042
+ const currentPlatform = options.currentPlatform ?? platform4();
3228
4043
  const env = options.env ?? process.env;
3229
- const homeDir = options.homeDir ?? homedir12();
3230
- const pathExists = options.exists ?? existsSync16;
4044
+ const homeDir = options.homeDir ?? homedir14();
4045
+ const pathExists = options.exists ?? existsSync18;
3231
4046
  const shellFromEnv = env.SHELL ? basenameLikeShell(env.SHELL).toLowerCase() : "";
3232
4047
  const shellName = shellFromEnv || (currentPlatform === "win32" ? "powershell" : "");
3233
4048
  const aliasName = "ta";
@@ -3383,29 +4198,29 @@ async function runInit(opts = {}) {
3383
4198
  logger.info(formatBullet("TokenArena \u5DF2\u51C6\u5907\u5C31\u7EEA\u3002", "success"));
3384
4199
  logger.info(formatKeyValue("\u63A7\u5236\u53F0", `${apiUrl}/usage`));
3385
4200
  await setupShellAlias();
3386
- await setupSystemdService();
4201
+ await setupBackgroundService();
3387
4202
  }
3388
- async function setupSystemdService() {
3389
- const currentPlatform = platform();
3390
- if (currentPlatform !== "linux") {
4203
+ async function setupBackgroundService() {
4204
+ const backend = getServiceBackend();
4205
+ if (!backend) {
3391
4206
  return;
3392
4207
  }
3393
- if (!isCommandAvailable("systemctl")) {
4208
+ const support = backend.canSetup();
4209
+ if (!support.ok) {
3394
4210
  return;
3395
4211
  }
3396
4212
  const shouldSetup = await promptConfirm({
3397
- message: "\u662F\u5426\u8BBE\u7F6E systemd \u670D\u52A1\u4EE5\u81EA\u52A8\u8FD0\u884C daemon\uFF1F",
4213
+ message: `\u662F\u5426\u8BBE\u7F6E ${backend.displayName} \u4EE5\u81EA\u52A8\u8FD0\u884C daemon\uFF1F`,
3398
4214
  defaultValue: true
3399
4215
  });
3400
4216
  if (!shouldSetup) {
3401
- logger.info(formatBullet("\u5DF2\u8DF3\u8FC7 systemd \u670D\u52A1\u8BBE\u7F6E\u3002"));
4217
+ logger.info(formatBullet("\u5DF2\u8DF3\u8FC7\u540E\u53F0\u670D\u52A1\u8BBE\u7F6E\u3002"));
3402
4218
  return;
3403
4219
  }
3404
4220
  try {
3405
- const { runInstallService: runInstallService2 } = await import("./service-4U7K4DKW.js");
3406
- await runInstallService2({ action: "setup", skipPrompt: true });
4221
+ await backend.setup(true);
3407
4222
  } catch (err) {
3408
- logger.warn(`systemd \u670D\u52A1\u8BBE\u7F6E\u5931\u8D25: ${err.message}`);
4223
+ logger.warn(`${backend.displayName} \u8BBE\u7F6E\u5931\u8D25: ${err.message}`);
3409
4224
  }
3410
4225
  }
3411
4226
  async function setupShellAlias() {
@@ -3424,7 +4239,7 @@ async function setupShellAlias() {
3424
4239
  try {
3425
4240
  await mkdir(dirname3(setup.configFile), { recursive: true });
3426
4241
  let existingContent = "";
3427
- if (existsSync16(setup.configFile)) {
4242
+ if (existsSync18(setup.configFile)) {
3428
4243
  existingContent = await readFile(setup.configFile, "utf-8");
3429
4244
  }
3430
4245
  const normalizedContent = existingContent.toLowerCase();
@@ -3468,6 +4283,9 @@ function log(msg) {
3468
4283
  function sleep(ms) {
3469
4284
  return new Promise((resolve2) => setTimeout(resolve2, ms));
3470
4285
  }
4286
+ function getDaemonExitCode(opts = {}) {
4287
+ return opts.service ? 0 : 1;
4288
+ }
3471
4289
  async function runDaemon(opts = {}) {
3472
4290
  const config = loadConfig();
3473
4291
  if (!config?.apiKey) {
@@ -3489,12 +4307,14 @@ async function runDaemon(opts = {}) {
3489
4307
  logger.info(formatBullet("\u5DF2\u53D6\u6D88\u542F\u52A8 daemon\u3002", "warning"));
3490
4308
  return;
3491
4309
  }
3492
- logger.error("Not configured. Run `tokenarena init` first.");
3493
- process.exit(1);
4310
+ const message = opts.service ? "Not configured. Exiting service mode." : "Not configured. Run `tokenarena init` first.";
4311
+ logger.error(message);
4312
+ process.exit(getDaemonExitCode(opts));
3494
4313
  }
3495
4314
  const interval = opts.interval || config.syncInterval || DEFAULT_INTERVAL;
3496
4315
  const intervalMin = Math.round(interval / 6e4);
3497
- log(`Daemon started (sync every ${intervalMin}m, Ctrl+C to stop)`);
4316
+ const stopHint = opts.service ? "service mode" : "Ctrl+C to stop";
4317
+ log(`Daemon started (sync every ${intervalMin}m, ${stopHint})`);
3498
4318
  while (true) {
3499
4319
  try {
3500
4320
  await runSync(config, {
@@ -3504,8 +4324,9 @@ async function runDaemon(opts = {}) {
3504
4324
  });
3505
4325
  } catch (err) {
3506
4326
  if (err.message === "UNAUTHORIZED") {
3507
- log("API key invalid. Exiting.");
3508
- process.exit(1);
4327
+ const message = opts.service ? "API key invalid. Exiting service mode." : "API key invalid. Exiting.";
4328
+ log(message);
4329
+ process.exit(getDaemonExitCode(opts));
3509
4330
  }
3510
4331
  log(`Sync error: ${err.message}`);
3511
4332
  }
@@ -3617,8 +4438,8 @@ async function runSyncCommand(opts = {}) {
3617
4438
  }
3618
4439
 
3619
4440
  // src/commands/uninstall.ts
3620
- import { existsSync as existsSync17, readFileSync as readFileSync10, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
3621
- import { homedir as homedir13, platform as platform2 } from "os";
4441
+ import { existsSync as existsSync19, readFileSync as readFileSync10, rmSync as rmSync4, writeFileSync as writeFileSync7 } from "fs";
4442
+ import { homedir as homedir15, platform as platform5 } from "os";
3622
4443
  function removeShellAlias() {
3623
4444
  const shell = process.env.SHELL;
3624
4445
  if (!shell) return;
@@ -3627,22 +4448,22 @@ function removeShellAlias() {
3627
4448
  let configFile;
3628
4449
  switch (shellName) {
3629
4450
  case "zsh":
3630
- configFile = `${homedir13()}/.zshrc`;
4451
+ configFile = `${homedir15()}/.zshrc`;
3631
4452
  break;
3632
4453
  case "bash":
3633
- if (platform2() === "darwin" && existsSync17(`${homedir13()}/.bash_profile`)) {
3634
- configFile = `${homedir13()}/.bash_profile`;
4454
+ if (platform5() === "darwin" && existsSync19(`${homedir15()}/.bash_profile`)) {
4455
+ configFile = `${homedir15()}/.bash_profile`;
3635
4456
  } else {
3636
- configFile = `${homedir13()}/.bashrc`;
4457
+ configFile = `${homedir15()}/.bashrc`;
3637
4458
  }
3638
4459
  break;
3639
4460
  case "fish":
3640
- configFile = `${homedir13()}/.config/fish/config.fish`;
4461
+ configFile = `${homedir15()}/.config/fish/config.fish`;
3641
4462
  break;
3642
4463
  default:
3643
4464
  return;
3644
4465
  }
3645
- if (!existsSync17(configFile)) return;
4466
+ if (!existsSync19(configFile)) return;
3646
4467
  try {
3647
4468
  let content = readFileSync10(configFile, "utf-8");
3648
4469
  const aliasPatterns = [
@@ -3666,7 +4487,7 @@ function removeShellAlias() {
3666
4487
  content = next;
3667
4488
  }
3668
4489
  }
3669
- writeFileSync5(configFile, content, "utf-8");
4490
+ writeFileSync7(configFile, content, "utf-8");
3670
4491
  logger.info(`Removed shell alias from ${configFile}`);
3671
4492
  } catch (err) {
3672
4493
  logger.warn(
@@ -3677,7 +4498,12 @@ function removeShellAlias() {
3677
4498
  async function runUninstall() {
3678
4499
  const configPath = getConfigPath();
3679
4500
  const configDir = getConfigDir();
3680
- if (!existsSync17(configPath)) {
4501
+ const stateDir = getStateDir();
4502
+ const runtimeDir = getRuntimeDirPath();
4503
+ const serviceBackend = getServiceBackend();
4504
+ const hasInstalledService = serviceBackend?.isInstalled() ?? false;
4505
+ const hasLocalArtifacts = existsSync19(configPath) || existsSync19(configDir) || existsSync19(stateDir) || existsSync19(runtimeDir) || hasInstalledService;
4506
+ if (!hasLocalArtifacts) {
3681
4507
  logger.info(formatHeader("\u5378\u8F7D TokenArena"));
3682
4508
  logger.info(formatBullet("\u672A\u53D1\u73B0\u672C\u5730\u914D\u7F6E\uFF0C\u65E0\u9700\u5378\u8F7D\u3002"));
3683
4509
  return;
@@ -3693,8 +4519,11 @@ async function runUninstall() {
3693
4519
  logger.info(formatKeyValue("API Key", maskSecret(config.apiKey)));
3694
4520
  }
3695
4521
  logger.info(formatKeyValue("\u914D\u7F6E\u76EE\u5F55", configDir));
3696
- logger.info(formatKeyValue("\u72B6\u6001\u76EE\u5F55", getStateDir()));
3697
- logger.info(formatKeyValue("\u8FD0\u884C\u76EE\u5F55", getRuntimeDirPath()));
4522
+ logger.info(formatKeyValue("\u72B6\u6001\u76EE\u5F55", stateDir));
4523
+ logger.info(formatKeyValue("\u8FD0\u884C\u76EE\u5F55", runtimeDir));
4524
+ if (hasInstalledService && serviceBackend) {
4525
+ logger.info(formatKeyValue("\u540E\u53F0\u670D\u52A1", serviceBackend.getDefinitionPath()));
4526
+ }
3698
4527
  const shouldUninstall = await promptConfirm({
3699
4528
  message: "\u786E\u8BA4\u7EE7\u7EED\u5378\u8F7D\u672C\u5730 TokenArena \u6570\u636E\uFF1F",
3700
4529
  defaultValue: false
@@ -3703,24 +4532,37 @@ async function runUninstall() {
3703
4532
  logger.info(formatBullet("\u5DF2\u53D6\u6D88\u5378\u8F7D\u3002", "warning"));
3704
4533
  return;
3705
4534
  }
3706
- deleteConfig();
4535
+ if (hasInstalledService && serviceBackend) {
4536
+ const shouldRemoveService = await promptConfirm({
4537
+ message: `\u68C0\u6D4B\u5230\u5DF2\u5B89\u88C5 ${serviceBackend.displayName}\uFF0C\u662F\u5426\u4E00\u5E76\u5378\u8F7D\uFF1F`,
4538
+ defaultValue: true
4539
+ });
4540
+ if (shouldRemoveService) {
4541
+ try {
4542
+ await serviceBackend.uninstall(true);
4543
+ } catch (err) {
4544
+ logger.warn(`\u540E\u53F0\u670D\u52A1\u5378\u8F7D\u5931\u8D25: ${err.message}`);
4545
+ }
4546
+ }
4547
+ }
3707
4548
  logger.info(formatSection("\u6267\u884C\u7ED3\u679C"));
3708
- logger.info(formatBullet("\u5DF2\u5220\u9664\u914D\u7F6E\u6587\u4EF6\u3002", "success"));
3709
- if (existsSync17(configDir)) {
4549
+ if (existsSync19(configPath)) {
4550
+ deleteConfig();
4551
+ logger.info(formatBullet("\u5DF2\u5220\u9664\u914D\u7F6E\u6587\u4EF6\u3002", "success"));
4552
+ }
4553
+ if (existsSync19(configDir)) {
3710
4554
  try {
3711
- rmSync2(configDir, { recursive: false, force: true });
4555
+ rmSync4(configDir, { recursive: false, force: true });
3712
4556
  logger.info(formatBullet("\u5DF2\u5220\u9664\u914D\u7F6E\u76EE\u5F55\u3002", "success"));
3713
4557
  } catch {
3714
4558
  }
3715
4559
  }
3716
- const stateDir = getStateDir();
3717
- if (existsSync17(stateDir)) {
3718
- rmSync2(stateDir, { recursive: true, force: true });
4560
+ if (existsSync19(stateDir)) {
4561
+ rmSync4(stateDir, { recursive: true, force: true });
3719
4562
  logger.info(formatBullet("\u5DF2\u5220\u9664\u72B6\u6001\u6570\u636E\u3002", "success"));
3720
4563
  }
3721
- const runtimeDir = getRuntimeDirPath();
3722
- if (existsSync17(runtimeDir)) {
3723
- rmSync2(runtimeDir, { recursive: true, force: true });
4564
+ if (existsSync19(runtimeDir)) {
4565
+ rmSync4(runtimeDir, { recursive: true, force: true });
3724
4566
  logger.info(formatBullet("\u5DF2\u5220\u9664\u8FD0\u884C\u65F6\u6570\u636E\u3002", "success"));
3725
4567
  }
3726
4568
  removeShellAlias();
@@ -3842,9 +4684,70 @@ async function runHome(program) {
3842
4684
  }
3843
4685
  }
3844
4686
 
4687
+ // src/commands/service.ts
4688
+ function printUsage(backendName) {
4689
+ logger.info(formatHeader("TokenArena \u540E\u53F0\u670D\u52A1\u7BA1\u7406"));
4690
+ if (backendName) {
4691
+ logger.info(formatKeyValue("\u5F53\u524D\u5B9E\u73B0", backendName));
4692
+ }
4693
+ logger.info(formatSection("\u53EF\u7528\u64CD\u4F5C"));
4694
+ logger.info(formatBullet("setup - \u521B\u5EFA\u5E76\u542F\u7528\u670D\u52A1"));
4695
+ logger.info(formatBullet("start - \u542F\u52A8\u670D\u52A1"));
4696
+ logger.info(formatBullet("stop - \u505C\u6B62\u670D\u52A1"));
4697
+ logger.info(formatBullet("restart - \u91CD\u542F\u670D\u52A1"));
4698
+ logger.info(formatBullet("status - \u67E5\u770B\u670D\u52A1\u72B6\u6001"));
4699
+ logger.info(formatBullet("uninstall - \u5378\u8F7D\u670D\u52A1"));
4700
+ }
4701
+ async function runServiceCommand(opts) {
4702
+ const backend = getServiceBackend();
4703
+ if (!backend) {
4704
+ logger.info(
4705
+ formatBullet(
4706
+ "\u540E\u53F0\u670D\u52A1\u4EC5\u5728 Linux(systemd) \u548C macOS(launchd) \u4E0A\u652F\u6301\u3002",
4707
+ "warning"
4708
+ )
4709
+ );
4710
+ return;
4711
+ }
4712
+ const action = opts.action?.toLowerCase();
4713
+ if (!action) {
4714
+ printUsage(backend.displayName);
4715
+ const support = backend.canSetup();
4716
+ if (!support.ok && support.reason) {
4717
+ logger.info(
4718
+ formatBullet(`\u5F53\u524D\u73AF\u5883\u6682\u4E0D\u53EF\u7BA1\u7406\u670D\u52A1\u3002${support.reason}`, "warning")
4719
+ );
4720
+ }
4721
+ return;
4722
+ }
4723
+ switch (action) {
4724
+ case "setup":
4725
+ await backend.setup(opts.skipPrompt);
4726
+ break;
4727
+ case "start":
4728
+ await backend.start();
4729
+ break;
4730
+ case "stop":
4731
+ await backend.stop();
4732
+ break;
4733
+ case "restart":
4734
+ await backend.restart();
4735
+ break;
4736
+ case "status":
4737
+ await backend.status();
4738
+ break;
4739
+ case "uninstall":
4740
+ await backend.uninstall(opts.skipPrompt);
4741
+ break;
4742
+ default:
4743
+ logger.error(`\u672A\u77E5\u64CD\u4F5C: ${action}`);
4744
+ process.exit(1);
4745
+ }
4746
+ }
4747
+
3845
4748
  // src/infrastructure/runtime/cli-version.ts
3846
4749
  import { readFileSync as readFileSync11 } from "fs";
3847
- import { dirname as dirname4, join as join16 } from "path";
4750
+ import { dirname as dirname4, join as join18 } from "path";
3848
4751
  import { fileURLToPath } from "url";
3849
4752
  var FALLBACK_VERSION = "0.0.0";
3850
4753
  var cachedVersion;
@@ -3852,7 +4755,7 @@ function getCliVersion(metaUrl = import.meta.url) {
3852
4755
  if (cachedVersion) {
3853
4756
  return cachedVersion;
3854
4757
  }
3855
- const packageJsonPath = join16(
4758
+ const packageJsonPath = join18(
3856
4759
  dirname4(fileURLToPath(metaUrl)),
3857
4760
  "..",
3858
4761
  "package.json"
@@ -3888,13 +4791,13 @@ function createCli() {
3888
4791
  program.command("sync").description("Manually sync usage data to server").addOption(new Option("--quiet").hideHelp()).action(async (opts) => {
3889
4792
  await runSyncCommand(opts);
3890
4793
  });
3891
- program.command("daemon").description("Run continuous sync (every 5 minutes by default)").option("--interval <ms>", "Sync interval in milliseconds", parseInt).action(async (opts) => {
4794
+ program.command("daemon").description("Run continuous sync (every 5 minutes by default)").option("--interval <ms>", "Sync interval in milliseconds", parseInt).addOption(new Option("--service").hideHelp()).action(async (opts) => {
3892
4795
  await runDaemon(opts);
3893
4796
  });
3894
4797
  program.command("service [action]").description(
3895
- "Manage systemd user service (setup|start|stop|restart|status|uninstall)"
4798
+ "Manage background service (setup|start|stop|restart|status|uninstall)"
3896
4799
  ).action(async (action) => {
3897
- await runInstallService({ action });
4800
+ await runServiceCommand({ action });
3898
4801
  });
3899
4802
  program.command("status").description("Show configuration and detected tools").action(async () => {
3900
4803
  await runStatus();
@@ -3913,7 +4816,7 @@ function createCli() {
3913
4816
  }
3914
4817
 
3915
4818
  // src/infrastructure/runtime/main-module.ts
3916
- import { existsSync as existsSync18, realpathSync } from "fs";
4819
+ import { existsSync as existsSync20, realpathSync } from "fs";
3917
4820
  import { resolve } from "path";
3918
4821
  import { fileURLToPath as fileURLToPath2 } from "url";
3919
4822
  function isMainModule(argvEntry = process.argv[1], metaUrl = import.meta.url) {
@@ -3924,7 +4827,7 @@ function isMainModule(argvEntry = process.argv[1], metaUrl = import.meta.url) {
3924
4827
  try {
3925
4828
  return realpathSync(argvEntry) === realpathSync(currentModulePath);
3926
4829
  } catch {
3927
- if (!existsSync18(argvEntry)) {
4830
+ if (!existsSync20(argvEntry)) {
3928
4831
  return false;
3929
4832
  }
3930
4833
  return resolve(argvEntry) === resolve(currentModulePath);