pinglet-cli 0.5.4 → 0.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.
@@ -40,6 +40,7 @@ const readline = __importStar(require("readline"));
40
40
  const config_1 = require("../config");
41
41
  const api_1 = require("../api");
42
42
  const i18n_1 = require("../i18n");
43
+ const lock_1 = require("../lock");
43
44
  const LOGIN_TIMEOUT_MS = 180_000;
44
45
  function prompt(question) {
45
46
  const rl = readline.createInterface({
@@ -58,7 +59,9 @@ function openBrowser(url) {
58
59
  ? ["cmd", "/c", "start", "", url]
59
60
  : ["xdg-open", url];
60
61
  try {
61
- (0, child_process_1.spawn)(cmd[0], cmd.slice(1), { stdio: "ignore" }).unref();
62
+ const child = (0, child_process_1.spawn)(cmd[0], cmd.slice(1), { stdio: "ignore" });
63
+ child.on("error", () => { });
64
+ child.unref();
62
65
  return true;
63
66
  }
64
67
  catch {
@@ -106,7 +109,7 @@ function loginViaLoopback(apiBaseUrl, provider, quiet) {
106
109
  resolve(token);
107
110
  };
108
111
  const timer = setTimeout(() => done(null), LOGIN_TIMEOUT_MS);
109
- const server = http.createServer((req, res) => {
112
+ const server = http.createServer(async (req, res) => {
110
113
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
111
114
  if (url.pathname !== "/callback") {
112
115
  res.statusCode = 404;
@@ -115,8 +118,11 @@ function loginViaLoopback(apiBaseUrl, provider, quiet) {
115
118
  }
116
119
  const token = url.searchParams.get("token");
117
120
  res.setHeader("content-type", "text/html; charset=utf-8");
118
- res.end(RESULT_HTML(Boolean(token)));
119
- done(token);
121
+ const valid = Boolean(token) && await (0, api_1.validateUserToken)({ apiBaseUrl }, token);
122
+ res.statusCode = valid ? 200 : 401;
123
+ res.end(RESULT_HTML(valid));
124
+ if (valid)
125
+ done(token);
120
126
  });
121
127
  server.on("error", () => done(null));
122
128
  server.listen(0, "127.0.0.1", () => {
@@ -172,8 +178,24 @@ async function runLogin(args) {
172
178
  process.exitCode = 1;
173
179
  return;
174
180
  }
181
+ if (!await (0, api_1.validateUserToken)(config, token)) {
182
+ console.log((0, i18n_1.t)("login.invalidToken"));
183
+ if (!quiet)
184
+ process.exitCode = 1;
185
+ return;
186
+ }
187
+ if (config.userToken && config.userToken !== token) {
188
+ if (!await (0, api_1.releaseCredential)(config, "user", config.userToken)) {
189
+ throw new Error((0, i18n_1.t)("logout.failed"));
190
+ }
191
+ }
192
+ await (0, lock_1.withPingletLock)(() => {
193
+ // Hooks may have added adapter restoration metadata while OAuth was open.
194
+ const current = (0, config_1.loadConfig)();
195
+ current.userToken = token;
196
+ (0, config_1.saveConfig)(current);
197
+ });
175
198
  config.userToken = token;
176
- (0, config_1.saveConfig)(config);
177
199
  console.log((0, i18n_1.t)("login.saved"));
178
200
  for (const [agentType, record] of Object.entries(config.installations)) {
179
201
  const linked = await (0, api_1.linkInstallation)(config, record.token);
@@ -3,18 +3,30 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.runLogout = runLogout;
4
4
  const config_1 = require("../config");
5
5
  const i18n_1 = require("../i18n");
6
+ const api_1 = require("../api");
7
+ const lock_1 = require("../lock");
6
8
  /**
7
- * `pinglet logout` — 로컬에 저장된 로그인 토큰만 지운다.
8
- * 통합 설정·installation·캐시는 그대로라 읽기는 계속 되고, 다시 `pinglet login`으로
9
- * 다른 계정(GitHub/Google)을 연결할 수 있다. 서버 측 토큰 폐기는 하지 않는다 (JWT 만료까지 유효).
9
+ * Revoke the user session and unlink this device's installations before
10
+ * removing local credentials. On failure preserve credentials for retry.
10
11
  */
11
- function runLogout() {
12
+ async function runLogout() {
12
13
  const config = (0, config_1.loadConfig)();
14
+ for (const record of Object.values(config.installations)) {
15
+ if (!await (0, api_1.releaseCredential)(config, "unlink", record.token)) {
16
+ throw new Error((0, i18n_1.t)("logout.failed"));
17
+ }
18
+ }
13
19
  if (!config.userToken) {
14
20
  console.log((0, i18n_1.t)("logout.notLoggedIn"));
15
21
  return;
16
22
  }
17
- delete config.userToken;
18
- (0, config_1.saveConfig)(config);
23
+ if (!await (0, api_1.releaseCredential)(config, "user", config.userToken)) {
24
+ throw new Error((0, i18n_1.t)("logout.failed"));
25
+ }
26
+ await (0, lock_1.withPingletLock)(() => {
27
+ const current = (0, config_1.loadConfig)();
28
+ delete current.userToken;
29
+ (0, config_1.saveConfig)(current);
30
+ });
19
31
  console.log((0, i18n_1.t)("logout.done"));
20
32
  }
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.runNotify = runNotify;
4
37
  const child_process_1 = require("child_process");
@@ -6,12 +39,16 @@ const cache_1 = require("../cache");
6
39
  const picker_1 = require("../picker");
7
40
  const queue_1 = require("../queue");
8
41
  const runtime_1 = require("../runtime");
42
+ const fs = __importStar(require("fs"));
43
+ const config_1 = require("../config");
9
44
  /**
10
45
  * Codex CLI notify hook 진입점 (experimental).
11
46
  * Codex는 turn 완료 시 마지막 인자로 notification JSON을 넘긴다.
12
47
  * notify 프로세스는 TUI 밖에서 돌기 때문에 MVP에서는 macOS 알림으로 Ping을 전달한다.
13
48
  */
14
- function runNotify(args) {
49
+ function runNotify(args, eventId) {
50
+ if (!fs.existsSync(config_1.CONFIG_PATH) || !(0, config_1.loadConfig)().adapters.codex)
51
+ return;
15
52
  let notificationType;
16
53
  try {
17
54
  const payload = JSON.parse(args[args.length - 1] ?? "");
@@ -31,7 +68,7 @@ function runNotify(args) {
31
68
  (0, cache_1.saveState)(state);
32
69
  return;
33
70
  }
34
- (0, queue_1.appendEvent)({ agentType: "CODEX", type: "DELIVERED", messageId: message.id });
71
+ (0, queue_1.appendEvent)({ eventId, agentType: "CODEX", type: "DELIVERED", messageId: message.id });
35
72
  state.seen[message.id] = (state.seen[message.id] ?? 0) + 1;
36
73
  (0, runtime_1.runMaintenance)(state, now);
37
74
  (0, cache_1.saveState)(state);
@@ -5,6 +5,7 @@ const config_1 = require("../config");
5
5
  const api_1 = require("../api");
6
6
  const cache_1 = require("../cache");
7
7
  const i18n_1 = require("../i18n");
8
+ const pending_post_1 = require("../pending-post");
8
9
  const REASON_KEYS = {
9
10
  EMPTY: "reason.EMPTY",
10
11
  TOO_LONG: "reason.TOO_LONG",
@@ -40,7 +41,12 @@ async function runPost(args) {
40
41
  return;
41
42
  }
42
43
  const config = (0, config_1.loadConfig)();
43
- const result = await (0, api_1.createMessage)(config, text, category);
44
+ const requestId = (0, pending_post_1.pendingPostId)(config, text, category);
45
+ const result = await (0, api_1.createMessage)(config, text, category, requestId);
46
+ // Only a definitive response resolves an earlier ambiguous commit. In
47
+ // particular 401/429 say nothing about whether the previous request committed.
48
+ if (result.ok)
49
+ (0, pending_post_1.completePost)(requestId);
44
50
  // 슬래시 명령 안에서는 같은 방식의 슬래시 명령을 안내한다.
45
51
  const loginCmd = quiet ? "/pinglet-login" : "pinglet login";
46
52
  if (!result.ok) {
@@ -1,16 +1,53 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.runRefresh = runRefresh;
4
37
  const config_1 = require("../config");
38
+ const fs = __importStar(require("fs"));
5
39
  const api_1 = require("../api");
6
40
  const update_1 = require("../update");
7
41
  const cache_1 = require("../cache");
8
42
  const claude_1 = require("../adapters/claude");
43
+ const lock_1 = require("../lock");
9
44
  /**
10
45
  * 서버 feed를 미리 받아 Local Feed Cache를 갱신한다 (아키텍처 §6).
11
46
  * 설치 당시 오프라인이었다면 installation 등록도 여기서 재시도한다.
12
47
  */
13
48
  async function runRefresh() {
49
+ if (!fs.existsSync(config_1.CONFIG_PATH))
50
+ return;
14
51
  const config = (0, config_1.loadConfig)();
15
52
  const wanted = [];
16
53
  if (config.adapters.claude)
@@ -21,31 +58,37 @@ async function runRefresh() {
21
58
  if (!config.installations[agentType]) {
22
59
  const record = await (0, api_1.registerInstallation)(config, agentType);
23
60
  if (record) {
61
+ await (0, lock_1.withPingletLock)(() => {
62
+ const current = (0, config_1.loadConfig)();
63
+ current.installations[agentType] = record;
64
+ (0, config_1.saveConfig)(current);
65
+ });
24
66
  config.installations[agentType] = record;
25
- (0, config_1.saveConfig)(config);
26
67
  }
27
68
  }
28
69
  }
29
- const records = Object.values(config.installations);
70
+ const records = wanted.flatMap((agent) => config.installations[agent] ? [config.installations[agent]] : []);
30
71
  if (records.length === 0)
31
72
  return;
32
73
  const onlineCounts = await Promise.all(records.map((r) => (0, api_1.sendHeartbeat)(config, r)));
33
74
  const online = onlineCounts.find((n) => n !== null);
34
75
  if (online !== undefined) {
35
- (0, cache_1.saveOnline)(online);
76
+ await (0, lock_1.withPingletLock)(() => (0, cache_1.saveOnline)(online));
36
77
  }
37
78
  const messages = await (0, api_1.fetchFeed)(config, records[0]);
38
79
  if (messages) {
39
- (0, cache_1.saveFeedMessages)(messages);
40
- // spinner 회전과 DELIVERED/QUALIFIED 기록은 statusline이 담당한다 (pool=1).
41
- // 여기서는 armed된 메시지가 서버에서 사라진 경우만 즉시 정리한다.
42
- const state = (0, cache_1.loadState)();
43
- const current = state.current;
44
- if (current && !messages.some((m) => m.id === current.messageId)) {
45
- (0, claude_1.armSpinnerMessage)(config, messages[0] ?? null);
46
- state.current = undefined; // 다음 statusline tick이 정식으로 다음 메시지를 armed한다
47
- (0, cache_1.saveState)(state);
48
- }
80
+ await (0, lock_1.withPingletLock)(() => {
81
+ (0, cache_1.saveFeedMessages)(messages);
82
+ // spinner 회전과 DELIVERED/QUALIFIED 기록은 statusline이 담당한다 (pool=1).
83
+ // 여기서는 armed된 메시지가 서버에서 사라진 경우만 즉시 정리한다.
84
+ const state = (0, cache_1.loadState)();
85
+ const current = state.current;
86
+ if (current && !messages.some((m) => m.id === current.messageId)) {
87
+ (0, claude_1.armSpinnerMessage)((0, config_1.loadConfig)(), messages[0] ?? null);
88
+ state.current = undefined; // 다음 statusline tick이 정식으로 다음 메시지를 armed한다
89
+ (0, cache_1.saveState)(state);
90
+ }
91
+ });
49
92
  }
50
93
  // 하루 1회 새 버전 확인 후 npm 전역 설치면 백그라운드 자동 업데이트.
51
94
  await (0, update_1.maybeSelfUpdate)(config);
@@ -101,6 +101,7 @@ function rotateSpinner(state, now) {
101
101
  messageId: prev.messageId,
102
102
  visibleMs: prev.visibleMs,
103
103
  });
104
+ prev.visibleMs = 0; // A failed replacement must not account this interval twice.
104
105
  }
105
106
  const message = (0, picker_1.pickMessage)((0, cache_1.loadFeedMessages)(), state);
106
107
  if (!message) {
@@ -108,7 +109,12 @@ function rotateSpinner(state, now) {
108
109
  state.current = undefined;
109
110
  return;
110
111
  }
111
- (0, claude_1.armSpinnerMessage)(config, message);
112
+ if (!(0, claude_1.armSpinnerMessage)(config, message)) {
113
+ // No delivery/current transition until the settings write actually commits.
114
+ // Clear attribution of subsequent API time and retry on the next tick.
115
+ state.current = undefined;
116
+ return;
117
+ }
112
118
  state.seen[message.id] = (state.seen[message.id] ?? 0) + 1;
113
119
  state.delivered ??= {};
114
120
  if (!state.delivered[message.id]) {
@@ -135,11 +141,14 @@ function rotateSpinner(state, now) {
135
141
  * - flush/refresh 백그라운드 트리거 (Claude 사용 중 항상 호출되므로 이 경로에서)
136
142
  */
137
143
  function runStatusline() {
144
+ if (!fs.existsSync(config_1.CONFIG_PATH) || !(0, config_1.loadConfig)().adapters.claude)
145
+ return;
138
146
  const payload = readPayload();
139
147
  const state = (0, cache_1.loadState)();
140
148
  const now = Date.now();
141
149
  trackSpinnerActivity(state, payload);
142
- const rotateDue = !state.current || now - state.current.shownAt >= runtime_1.ROTATE_MS;
150
+ const rotateDue = !state.current || now - state.current.shownAt >= runtime_1.ROTATE_MS ||
151
+ !(0, cache_1.loadFeedMessages)().some((message) => message.id === state.current?.messageId);
143
152
  // 300ms마다 호출되므로 저장/maintenance는 1초 스로틀. 회전 시점에는 항상 저장.
144
153
  const saveDue = rotateDue ||
145
154
  state.lastTickAt === undefined ||
@@ -39,21 +39,63 @@ const config_1 = require("../config");
39
39
  const claude_1 = require("../adapters/claude");
40
40
  const codex_1 = require("../adapters/codex");
41
41
  const i18n_1 = require("../i18n");
42
- function runUninstall(args) {
43
- const config = (0, config_1.loadConfig)();
44
- const claudeRemoved = (0, claude_1.uninstallClaudeIntegration)(config);
45
- const codexRemoved = (0, codex_1.uninstallCodexIntegration)(config);
42
+ const api_1 = require("../api");
43
+ const lock_1 = require("../lock");
44
+ const queue_1 = require("../queue");
45
+ const cache_1 = require("../cache");
46
+ const flush_1 = require("./flush");
47
+ async function runUninstall(args) {
48
+ const restored = await (0, lock_1.withPingletLock)(() => {
49
+ const config = (0, config_1.loadConfig)();
50
+ (0, queue_1.bindEvents)();
51
+ const claudeRemoved = (0, claude_1.uninstallClaudeIntegration)(config);
52
+ const codexRemoved = (0, codex_1.uninstallCodexIntegration)(config);
53
+ (0, cache_1.saveState)({ seen: {} });
54
+ return { config, claudeRemoved, codexRemoved };
55
+ });
56
+ if (!restored)
57
+ return;
58
+ const { config, claudeRemoved, codexRemoved } = restored;
46
59
  console.log(claudeRemoved
47
60
  ? "✓ Claude integration removed"
48
61
  : "○ Claude integration not installed");
49
62
  console.log(codexRemoved
50
63
  ? "✓ Codex integration removed"
51
64
  : "○ Codex integration not installed");
65
+ await (0, flush_1.runFlush)();
66
+ for (const [agent, record] of Object.entries(config.installations)) {
67
+ if (!await (0, api_1.releaseCredential)(config, "installation", record.token)) {
68
+ throw new Error((0, i18n_1.t)("uninstall.failed"));
69
+ }
70
+ await (0, lock_1.withPingletLock)(() => {
71
+ const pending = (0, queue_1.readEvents)().filter(event => event.agentType === agent);
72
+ (0, queue_1.quarantineEvents)(pending, "uninstalled-before-upload");
73
+ if (pending.length && !args.includes("--purge"))
74
+ console.log(`○ ${pending.length} unsent events archived in events-quarantine.jsonl (not reassigned on reinstall)`);
75
+ delete config.installations[agent];
76
+ (0, config_1.saveConfig)(config);
77
+ });
78
+ }
79
+ // Also retire offline events produced before registration ever succeeded.
80
+ await (0, lock_1.withPingletLock)(() => {
81
+ const pending = (0, queue_1.readEvents)();
82
+ (0, queue_1.quarantineEvents)(pending, "uninstalled-without-registration");
83
+ if (pending.length && !args.includes("--purge"))
84
+ console.log(`○ ${pending.length} unregistered events archived in events-quarantine.jsonl`);
85
+ });
86
+ if (config.userToken) {
87
+ if (!await (0, api_1.releaseCredential)(config, "user", config.userToken)) {
88
+ throw new Error((0, i18n_1.t)("uninstall.failed"));
89
+ }
90
+ delete config.userToken;
91
+ await (0, lock_1.withPingletLock)(() => (0, config_1.saveConfig)(config));
92
+ }
52
93
  if (args.includes("--purge")) {
53
- fs.rmSync(config_1.PINGLET_DIR, { recursive: true, force: true });
94
+ await (0, lock_1.withPingletLock)(() => fs.rmSync(config_1.PINGLET_DIR, { recursive: true, force: true }));
54
95
  console.log("✓ ~/.pinglet removed (config / cache / event queue)");
55
96
  }
56
97
  else {
57
98
  console.log((0, i18n_1.t)("uninstall.keep"));
58
99
  }
100
+ console.log((0, i18n_1.t)("uninstall.npm"));
59
101
  }
package/dist/config.js CHANGED
@@ -43,6 +43,7 @@ exports.cliPath = cliPath;
43
43
  const fs = __importStar(require("fs"));
44
44
  const os = __importStar(require("os"));
45
45
  const path = __importStar(require("path"));
46
+ const storage_1 = require("./storage");
46
47
  /** package.json의 version — 하드코딩하면 npm version bump와 어긋난다. */
47
48
  exports.VERSION = (() => {
48
49
  try {
@@ -66,25 +67,36 @@ exports.ONLINE_PATH = path.join(exports.PINGLET_DIR, "online.json");
66
67
  exports.UPDATE_PATH = path.join(exports.PINGLET_DIR, "update.json");
67
68
  exports.DEFAULT_API_BASE_URL = process.env.PINGLET_API_URL ?? "https://pinglet.halluci.co.kr";
68
69
  function ensurePingletDir() {
69
- fs.mkdirSync(exports.PINGLET_DIR, { recursive: true });
70
+ fs.mkdirSync(exports.PINGLET_DIR, { recursive: true, mode: 0o700 });
70
71
  }
71
- function readJsonFile(filePath) {
72
+ function readJsonFile(filePath, strict = false) {
72
73
  try {
73
- return JSON.parse(fs.readFileSync(filePath, "utf8"));
74
+ const value = JSON.parse(fs.readFileSync(filePath, "utf8"));
75
+ if (strict && (!value || typeof value !== "object" || Array.isArray(value))) {
76
+ throw new Error("Expected a JSON object");
77
+ }
78
+ return value;
74
79
  }
75
- catch {
80
+ catch (error) {
81
+ if (strict && !(0, storage_1.isMissing)(error))
82
+ throw new Error(`Cannot read ${filePath}; original file preserved.`, { cause: error });
76
83
  return null;
77
84
  }
78
85
  }
79
86
  function writeJsonFile(filePath, value) {
80
87
  ensurePingletDir();
81
- const tmp = filePath + ".tmp";
82
- fs.writeFileSync(tmp, JSON.stringify(value, null, 2));
83
- fs.renameSync(tmp, filePath);
88
+ (0, storage_1.atomicWrite)(filePath, JSON.stringify(value, null, 2));
84
89
  }
85
90
  function loadConfig() {
86
- const existing = readJsonFile(exports.CONFIG_PATH);
91
+ const existing = readJsonFile(exports.CONFIG_PATH, true);
87
92
  if (existing) {
93
+ const object = (v) => !!v && typeof v === "object" && !Array.isArray(v);
94
+ if (typeof existing.apiBaseUrl !== "string" || typeof existing.createdAt !== "string" ||
95
+ (existing.userToken !== undefined && typeof existing.userToken !== "string") ||
96
+ (existing.installations !== undefined && !object(existing.installations)) ||
97
+ (existing.adapters !== undefined && !object(existing.adapters))) {
98
+ throw new Error(`Invalid ${exports.CONFIG_PATH}; original file preserved.`);
99
+ }
88
100
  existing.installations ??= {};
89
101
  existing.adapters ??= {};
90
102
  return existing;
package/dist/i18n.js CHANGED
@@ -11,7 +11,7 @@ Usage: pinglet <command>
11
11
 
12
12
  install [--api <url>] [--force] Install the Claude Code integration
13
13
  [--claude | --codex] Codex uses OS notifications; opt in with --codex (experimental)
14
- uninstall [--purge] Remove the integration (--purge: also delete ~/.pinglet)
14
+ uninstall [--purge] Restore settings and revoke credentials (--purge: delete ~/.pinglet)
15
15
  login [--github | --google] Link your account (GitHub or Google; pick in the browser if omitted)
16
16
  [--token <jwt>]
17
17
  logout Sign out (keeps the integration and cache)
@@ -43,6 +43,10 @@ Internal commands (called by the integration):
43
43
  "login.html.failSub": "Please try again from the terminal.",
44
44
  // ---- logout
45
45
  "logout.notLoggedIn": "○ You are not logged in.",
46
+ "logout.failed": "Server sign-out failed. Credentials are preserved; retry pinglet logout when connected.",
47
+ "uninstall.failed": "Server revocation failed. Local data is preserved; retry pinglet uninstall when connected.",
48
+ "uninstall.npm": "To remove the CLI package too, run: npm uninstall -g pinglet-cli (npm itself is kept).",
49
+ "login.invalidToken": "Login could not be verified. Existing credentials were preserved; check your token and connection.",
46
50
  "logout.done": "✓ Logged out. Reading still works; run `pinglet login` again to post.",
47
51
  // ---- post
48
52
  "reason.EMPTY": "empty messages cannot be posted",
@@ -89,7 +93,7 @@ Internal commands (called by the integration):
89
93
  "doctor.loggedIn": "Logged in",
90
94
  "doctor.notLoggedIn": "Not logged in — reading works; run `pinglet login` to post / react",
91
95
  // ---- misc commands
92
- "uninstall.keep": "○ ~/.pinglet is kept. Use --purge to remove everything.",
96
+ "uninstall.keep": "○ Local cache and history are kept. Use --purge to delete ~/.pinglet too.",
93
97
  "ping.none": "No Ping to show — one will appear once the feed is received.",
94
98
  "postinstall.start": "\npinglet: running first-time setup automatically (`pinglet install`)",
95
99
  "postinstall.failed": "pinglet: automatic setup failed. Please run `pinglet install` yourself.",
@@ -99,7 +103,7 @@ Internal commands (called by the integration):
99
103
  "codex.existingNotify": "An existing notify setting was found (use --force to back it up and replace).",
100
104
  "slash.postDesc": "Post a message to Pinglet so it shows in other developers' terminals",
101
105
  "slash.loginDesc": "Link your Pinglet account (GitHub or Google login in the browser; --github/--google to choose)",
102
- "slash.logoutDesc": "Sign out of Pinglet (removes the login token on this machine only)",
106
+ "slash.logoutDesc": "Sign out of Pinglet (revoke this session and unlink this device)",
103
107
  };
104
108
  const ko = {
105
109
  "cli.help": (p) => `pinglet v${p.version} — AI가 생각하는 동안 만나는 개발자 메시지 네트워크
@@ -108,7 +112,7 @@ const ko = {
108
112
 
109
113
  install [--api <url>] [--force] Claude Code integration 설치
110
114
  [--claude | --codex] Codex는 OS 알림 방식이라 --codex로 opt-in (experimental)
111
- uninstall [--purge] integration 제거 (--purge: ~/.pinglet까지 삭제)
115
+ uninstall [--purge] 설정 복원·서버 인증 해제 (--purge: ~/.pinglet까지 삭제)
112
116
  login [--github | --google] 계정 연결 (GitHub 또는 Google, 생략 시 브라우저에서 선택)
113
117
  [--token <jwt>]
114
118
  logout 로그인 해제 (설치·캐시는 유지)
@@ -138,6 +142,10 @@ const ko = {
138
142
  "login.html.fail": "✗ 로그인 실패",
139
143
  "login.html.failSub": "터미널에서 다시 시도해 주세요.",
140
144
  "logout.notLoggedIn": "○ 로그인 상태가 아닙니다.",
145
+ "logout.failed": "서버 로그아웃에 실패했습니다. 재시도할 수 있도록 인증 정보를 보존했습니다. 연결 후 pinglet logout을 다시 실행하세요.",
146
+ "uninstall.failed": "서버 인증 해제에 실패해 로컬 데이터를 보존했습니다. 연결 후 pinglet uninstall을 다시 실행하세요.",
147
+ "uninstall.npm": "CLI 패키지도 제거하려면: npm uninstall -g pinglet-cli (npm 자체는 유지됩니다).",
148
+ "login.invalidToken": "로그인을 확인할 수 없습니다. 기존 인증 정보는 보존했습니다. 토큰과 서버 연결을 확인하세요.",
141
149
  "logout.done": "✓ 로그아웃했습니다. 읽기는 계속 되고, 메시지 작성은 `pinglet login` 후 가능합니다.",
142
150
  "reason.EMPTY": "빈 메시지는 등록할 수 없습니다",
143
151
  "reason.TOO_LONG": "메시지가 너무 깁니다 (기본 100자 제한)",
@@ -180,7 +188,7 @@ const ko = {
180
188
  "doctor.apiUnreachable": (p) => `API unreachable (${p.api}) — 오프라인 모드로 동작 중`,
181
189
  "doctor.loggedIn": "로그인됨",
182
190
  "doctor.notLoggedIn": "로그인 안 됨 — 읽기는 가능, 메시지 작성/반응은 `pinglet login` 후",
183
- "uninstall.keep": "○ ~/.pinglet은 유지됩니다. 전부 지우려면 --purge를 사용하세요.",
191
+ "uninstall.keep": "○ 로컬 캐시·기록은 유지됩니다. ~/.pinglet도 지우려면 --purge를 사용하세요.",
184
192
  "ping.none": "표시할 Ping이 없습니다 — 서버 feed를 받으면 표시됩니다.",
185
193
  "postinstall.start": "\npinglet: 첫 설정을 자동으로 진행합니다 (`pinglet install`)",
186
194
  "postinstall.failed": "pinglet: 자동 설정에 실패했습니다. `pinglet install`을 직접 실행해 주세요.",
@@ -189,7 +197,7 @@ const ko = {
189
197
  "codex.existingNotify": "기존 notify 설정이 있습니다 (백업 후 교체하려면 --force).",
190
198
  "slash.postDesc": "메시지를 Pinglet에 등록해 다른 개발자들의 터미널에 표시",
191
199
  "slash.loginDesc": "Pinglet 계정 연결 (브라우저에서 GitHub 또는 Google 로그인, --github/--google로 지정 가능)",
192
- "slash.logoutDesc": "Pinglet 로그인 해제 (이 기기의 로그인 토큰만 제거)",
200
+ "slash.logoutDesc": "Pinglet 로그인 해제 (서버 세션 폐기·이 기기의 계정 연결 해제)",
193
201
  };
194
202
  const ja = {
195
203
  "cli.help": (p) => `pinglet v${p.version} — AIが考えている間に出会う開発者メッセージネットワーク
@@ -198,7 +206,7 @@ const ja = {
198
206
 
199
207
  install [--api <url>] [--force] Claude Code integration をインストール
200
208
  [--claude | --codex] Codex は OS 通知方式のため --codex で opt-in (experimental)
201
- uninstall [--purge] integration を削除 (--purge: ~/.pinglet も削除)
209
+ uninstall [--purge] 設定を復元・認証を解除 (--purge: ~/.pinglet も削除)
202
210
  login [--github | --google] アカウント連携 (GitHub または Google、省略時はブラウザで選択)
203
211
  [--token <jwt>]
204
212
  logout ログアウト (連携設定・キャッシュは維持)
@@ -228,6 +236,10 @@ const ja = {
228
236
  "login.html.fail": "✗ ログイン失敗",
229
237
  "login.html.failSub": "ターミナルからもう一度お試しください。",
230
238
  "logout.notLoggedIn": "○ ログインしていません。",
239
+ "logout.failed": "サーバーのログアウトに失敗しました。認証情報は保持しています。接続後に pinglet logout を再実行してください。",
240
+ "uninstall.failed": "サーバーの認証解除に失敗したため、ローカルデータを保持しました。接続後に pinglet uninstall を再実行してください。",
241
+ "uninstall.npm": "CLI パッケージも削除するには: npm uninstall -g pinglet-cli(npm 本体は保持されます)。",
242
+ "login.invalidToken": "ログインを確認できませんでした。既存の認証情報は保持しています。トークンと接続を確認してください。",
231
243
  "logout.done": "✓ ログアウトしました。閲覧は引き続き可能で、投稿は `pinglet login` 後にできます。",
232
244
  "reason.EMPTY": "空のメッセージは投稿できません",
233
245
  "reason.TOO_LONG": "メッセージが長すぎます (デフォルト100文字まで)",
@@ -270,7 +282,7 @@ const ja = {
270
282
  "doctor.apiUnreachable": (p) => `API unreachable (${p.api}) — オフラインモードで動作中`,
271
283
  "doctor.loggedIn": "ログイン済み",
272
284
  "doctor.notLoggedIn": "未ログイン — 閲覧は可能、投稿/リアクションは `pinglet login` 後",
273
- "uninstall.keep": "○ ~/.pinglet は維持されます。すべて削除するには --purge を使ってください。",
285
+ "uninstall.keep": "○ ローカルキャッシュと履歴は保持されます。~/.pinglet も削除するには --purge を使ってください。",
274
286
  "ping.none": "表示する Ping がありません — サーバーから feed を受信すると表示されます。",
275
287
  "postinstall.start": "\npinglet: 初期設定を自動的に行います (`pinglet install`)",
276
288
  "postinstall.failed": "pinglet: 自動設定に失敗しました。`pinglet install` を手動で実行してください。",
@@ -279,7 +291,7 @@ const ja = {
279
291
  "codex.existingNotify": "既存の notify 設定があります (バックアップして置き換えるには --force)。",
280
292
  "slash.postDesc": "メッセージを Pinglet に投稿して他の開発者のターミナルに表示",
281
293
  "slash.loginDesc": "Pinglet アカウント連携 (ブラウザで GitHub または Google ログイン、--github/--google で指定可)",
282
- "slash.logoutDesc": "Pinglet ログアウト (このマシンのログイントークンのみ削除)",
294
+ "slash.logoutDesc": "Pinglet ログアウト (サーバーセッション失効・この端末のアカウント連携解除)",
283
295
  };
284
296
  const tables = { en, ko, ja };
285
297
  function t(key, params = {}) {