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.
package/dist/lock.js ADDED
@@ -0,0 +1,168 @@
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
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.withPingletLock = withPingletLock;
37
+ exports.withCommandLock = withCommandLock;
38
+ const fs = __importStar(require("fs"));
39
+ const os = __importStar(require("os"));
40
+ const path = __importStar(require("path"));
41
+ const crypto_1 = require("crypto");
42
+ const storage_1 = require("./storage");
43
+ // Outside ~/.pinglet so purge cannot remove a lock held by another command.
44
+ const LOCK_PATH = path.join(os.homedir(), ".pinglet.lock");
45
+ function owner(lockPath) {
46
+ try {
47
+ return fs.readFileSync(lockPath, "utf8");
48
+ }
49
+ catch (error) {
50
+ if ((0, storage_1.isMissing)(error))
51
+ return null;
52
+ throw error;
53
+ }
54
+ }
55
+ function abandoned(value, lockPath) {
56
+ const pid = Number(value.split(/[:-]/)[0]);
57
+ if (!Number.isInteger(pid) || pid <= 0) {
58
+ // The creating process may not have written its PID yet.
59
+ return Date.now() - fs.statSync(lockPath).mtimeMs > 30_000;
60
+ }
61
+ try {
62
+ process.kill(pid, 0);
63
+ return false;
64
+ }
65
+ catch (error) {
66
+ return error.code === "ESRCH";
67
+ }
68
+ }
69
+ function recover(lockPath) {
70
+ const recoveryPath = lockPath + ".recovery";
71
+ const identity = `${process.pid}-${(0, crypto_1.randomUUID)()}`;
72
+ // Serialize reapers and re-read ownership: a second reaper must never unlink
73
+ // a fresh lock acquired after the first reaper removed the abandoned one.
74
+ try {
75
+ fs.mkdirSync(recoveryPath, { mode: 0o700 });
76
+ }
77
+ catch (error) {
78
+ if (error.code === "EEXIST") {
79
+ // A killed reaper must not block recovery forever. Unique owner filenames
80
+ // ensure only the process that removed the old owner may remove its dir.
81
+ try {
82
+ const entries = fs.readdirSync(recoveryPath);
83
+ if (entries.length === 1 && abandoned(entries[0], recoveryPath)) {
84
+ fs.unlinkSync(path.join(recoveryPath, entries[0]));
85
+ fs.rmdirSync(recoveryPath);
86
+ }
87
+ else if (entries.length === 0 && Date.now() - fs.statSync(recoveryPath).mtimeMs > 30_000) {
88
+ // Legacy/partially-created recovery directory; allow initialization grace.
89
+ fs.rmdirSync(recoveryPath);
90
+ }
91
+ }
92
+ catch (cleanupError) {
93
+ if (!(0, storage_1.isMissing)(cleanupError) && !["ENOTEMPTY", "EEXIST"].includes(cleanupError.code ?? ""))
94
+ throw cleanupError;
95
+ }
96
+ return;
97
+ }
98
+ throw error;
99
+ }
100
+ const ownerPath = path.join(recoveryPath, identity);
101
+ try {
102
+ fs.writeFileSync(ownerPath, "", { flag: "wx", mode: 0o600 });
103
+ const value = owner(lockPath);
104
+ if (value !== null && abandoned(value, lockPath))
105
+ fs.unlinkSync(lockPath);
106
+ }
107
+ catch (error) {
108
+ if (!(0, storage_1.isMissing)(error))
109
+ throw error;
110
+ }
111
+ finally {
112
+ try {
113
+ fs.unlinkSync(ownerPath);
114
+ fs.rmdirSync(recoveryPath);
115
+ }
116
+ catch (error) {
117
+ if (!(0, storage_1.isMissing)(error))
118
+ throw error;
119
+ }
120
+ }
121
+ }
122
+ /** Short local read/modify/write transactions. Never hold this across network I/O. */
123
+ async function withPingletLock(action, options = {}) {
124
+ const lockPath = options.command ? LOCK_PATH + ".command" : LOCK_PATH;
125
+ const identity = `${process.pid}:${(0, crypto_1.randomUUID)()}`;
126
+ const deadline = Date.now() + (options.timeoutMs ?? 15_000);
127
+ for (;;) {
128
+ let fd;
129
+ try {
130
+ fd = fs.openSync(lockPath, "wx", 0o600);
131
+ }
132
+ catch (error) {
133
+ if (error.code !== "EEXIST")
134
+ throw error;
135
+ recover(lockPath);
136
+ if (owner(lockPath) === null)
137
+ continue;
138
+ if (options.skipIfBusy)
139
+ return undefined;
140
+ if (Date.now() >= deadline)
141
+ throw new Error("Pinglet is busy. Retry after the other command finishes.");
142
+ await new Promise((resolve) => setTimeout(resolve, 50));
143
+ continue;
144
+ }
145
+ try {
146
+ fs.writeFileSync(fd, identity);
147
+ }
148
+ catch (error) {
149
+ fs.unlinkSync(lockPath);
150
+ throw error;
151
+ }
152
+ finally {
153
+ fs.closeSync(fd);
154
+ }
155
+ break;
156
+ }
157
+ try {
158
+ return await action();
159
+ }
160
+ finally {
161
+ if (owner(lockPath) === identity)
162
+ fs.unlinkSync(lockPath);
163
+ }
164
+ }
165
+ /** Serializes administrative/network commands without excluding local hooks. */
166
+ function withCommandLock(action) {
167
+ return withPingletLock(action, { command: true });
168
+ }
@@ -0,0 +1,98 @@
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
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.enqueueNotification = enqueueNotification;
37
+ exports.drainNotifications = drainNotifications;
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const crypto_1 = require("crypto");
41
+ const config_1 = require("./config");
42
+ const notify_1 = require("./commands/notify");
43
+ const storage_1 = require("./storage");
44
+ const PREFIX = "notify-";
45
+ /** Unique files allow enqueueing without waiting for the state lock. Store only
46
+ * the event type, never the Codex payload (which can contain user/code content).
47
+ * Do not mkdir: a concurrent purge must not be undone by a stale hook.
48
+ */
49
+ function enqueueNotification(args) {
50
+ if (!fs.existsSync(config_1.CONFIG_PATH))
51
+ return false;
52
+ const config = (0, config_1.loadConfig)();
53
+ const adapter = config.adapters.codex;
54
+ if (!adapter)
55
+ return false;
56
+ let type = "agent-turn-complete";
57
+ try {
58
+ type = JSON.parse(args[args.length - 1] ?? "")?.type ?? type;
59
+ }
60
+ catch { /* manual invocation */ }
61
+ if (type !== "agent-turn-complete")
62
+ return false;
63
+ const file = path.join(config_1.PINGLET_DIR, `${PREFIX}${(0, crypto_1.randomUUID)()}.json`);
64
+ const temp = file + ".tmp";
65
+ try {
66
+ fs.writeFileSync(temp, JSON.stringify({ type, installedAt: adapter.installedAt, apiBaseUrl: config.apiBaseUrl }), { flag: "wx", mode: 0o600 });
67
+ fs.renameSync(temp, file);
68
+ return true;
69
+ }
70
+ catch (error) {
71
+ if (!(0, storage_1.isMissing)(error))
72
+ throw error;
73
+ return false;
74
+ }
75
+ finally {
76
+ try {
77
+ fs.unlinkSync(temp);
78
+ }
79
+ catch (error) {
80
+ if (!(0, storage_1.isMissing)(error))
81
+ throw error;
82
+ }
83
+ }
84
+ }
85
+ /** Caller holds the short state lock. Failed work stays on disk for retry. */
86
+ function drainNotifications() {
87
+ if (!fs.existsSync(config_1.CONFIG_PATH))
88
+ return;
89
+ const config = (0, config_1.loadConfig)();
90
+ for (const name of fs.readdirSync(config_1.PINGLET_DIR).filter(name => name.startsWith(PREFIX) && name.endsWith(".json"))) {
91
+ const file = path.join(config_1.PINGLET_DIR, name);
92
+ const event = JSON.parse(fs.readFileSync(file, "utf8"));
93
+ if (config.adapters.codex?.installedAt === event.installedAt && config.apiBaseUrl === event.apiBaseUrl) {
94
+ (0, notify_1.runNotify)([JSON.stringify({ type: event.type })], "evt_" + name.slice(PREFIX.length, -5));
95
+ }
96
+ fs.unlinkSync(file); // Stale installation notifications must not survive reinstall.
97
+ }
98
+ }
@@ -0,0 +1,73 @@
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
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.pendingPostId = pendingPostId;
37
+ exports.completePost = completePost;
38
+ const crypto_1 = require("crypto");
39
+ const path = __importStar(require("path"));
40
+ const config_1 = require("./config");
41
+ const PENDING_PATH = path.join(config_1.PINGLET_DIR, "pending-posts.json");
42
+ /** Persist before sending so retrying an ambiguous network failure uses the same ID.
43
+ * Only hashes are stored: no message text or bearer token in the retry journal.
44
+ */
45
+ function pendingPostId(config, text, category) {
46
+ // The JWT subject is used only as a local journal namespace, never for auth.
47
+ // Refreshing/replacing a token for the same user must retain pending requests.
48
+ let account = config.userToken;
49
+ try {
50
+ const payload = JSON.parse(Buffer.from(config.userToken?.split(".")[1] ?? "", "base64url").toString());
51
+ if (payload.kind === "user" && typeof payload.sub === "string")
52
+ account = payload.sub;
53
+ }
54
+ catch { /* Legacy/non-JWT fixture tokens retain their previous namespace. */ }
55
+ const digest = (identity) => (0, crypto_1.createHash)("sha256").update(JSON.stringify([config.apiBaseUrl, identity, text, category || null])).digest("hex");
56
+ const key = digest(account);
57
+ const pending = (0, config_1.readJsonFile)(PENDING_PATH) ?? [];
58
+ const existing = pending.find((p) => p.key === key || p.key === digest(config.userToken));
59
+ if (existing) {
60
+ if (existing.key !== key) {
61
+ existing.key = key;
62
+ (0, config_1.writeJsonFile)(PENDING_PATH, pending);
63
+ }
64
+ return existing.requestId;
65
+ }
66
+ const requestId = (0, crypto_1.randomUUID)();
67
+ (0, config_1.writeJsonFile)(PENDING_PATH, [...pending, { key, requestId }]);
68
+ return requestId;
69
+ }
70
+ function completePost(requestId) {
71
+ const pending = (0, config_1.readJsonFile)(PENDING_PATH) ?? [];
72
+ (0, config_1.writeJsonFile)(PENDING_PATH, pending.filter((p) => p.requestId !== requestId));
73
+ }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const install_1 = require("./commands/install");
4
4
  const i18n_1 = require("./i18n");
5
+ const lock_1 = require("./lock");
5
6
  /**
6
7
  * npm install -g pinglet-cli 직후 자동으로 `pinglet install`을 수행한다.
7
8
  * - 글로벌 설치(npm_config_global=true)일 때만 동작 — 개발용 npm install,
@@ -14,7 +15,7 @@ async function main() {
14
15
  if (process.env.npm_config_global !== "true")
15
16
  return;
16
17
  console.log((0, i18n_1.t)("postinstall.start"));
17
- await (0, install_1.runInstall)([]);
18
+ await (0, lock_1.withCommandLock)(() => (0, install_1.runInstall)([]));
18
19
  }
19
20
  main()
20
21
  .catch(() => {
package/dist/queue.js CHANGED
@@ -37,18 +37,25 @@ exports.appendEvent = appendEvent;
37
37
  exports.readEvents = readEvents;
38
38
  exports.countEvents = countEvents;
39
39
  exports.removeEvents = removeEvents;
40
+ exports.quarantineEvents = quarantineEvents;
41
+ exports.bindEvents = bindEvents;
42
+ exports.invalidEventReason = invalidEventReason;
40
43
  const fs = __importStar(require("fs"));
41
44
  const crypto_1 = require("crypto");
42
45
  const config_1 = require("./config");
46
+ const path = __importStar(require("path"));
47
+ const storage_1 = require("./storage");
43
48
  /**
44
- * Local Event Queue (append-only JSONL).
49
+ * Local Event Queue (JSONL). Writers hold the short local transaction lock;
50
+ * uploads use a snapshot and acknowledge only its event IDs afterwards.
45
51
  * 노출 즉시 서버를 호출하지 않고 여기 기록했다가 batch 전송한다 (아키텍처 §8).
46
52
  */
47
53
  function appendEvent(input) {
48
54
  (0, config_1.ensurePingletDir)();
49
55
  const event = {
50
- eventId: "evt_" + (0, crypto_1.randomUUID)(),
56
+ eventId: input.eventId ?? "evt_" + (0, crypto_1.randomUUID)(),
51
57
  agentType: input.agentType,
58
+ installationId: (0, config_1.loadConfig)().installations[input.agentType]?.installationId,
52
59
  type: input.type,
53
60
  messageId: input.messageId,
54
61
  ...(input.visibleMs !== undefined && { visibleMs: input.visibleMs }),
@@ -70,12 +77,16 @@ function readEvents() {
70
77
  continue;
71
78
  try {
72
79
  const parsed = JSON.parse(line);
73
- if (parsed.eventId && parsed.messageId && parsed.type) {
74
- events.push(parsed);
75
- }
80
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || typeof parsed.eventId !== "string" || !parsed.eventId)
81
+ throw new Error("invalid queue record");
82
+ events.push(parsed);
76
83
  }
77
84
  catch {
78
- // 손상된 라인은 버린다.
85
+ // Preserve damaged input for quarantine rather than silently deleting it
86
+ // when the next successful upload rewrites the queue.
87
+ events.push({ eventId: "invalid_" + (0, crypto_1.createHash)("sha256").update(line).digest("hex").slice(0, 48),
88
+ agentType: "CLAUDE", type: "DELIVERED", messageId: "", occurredAt: "",
89
+ queueError: "malformed-queue-line", rawLine: line });
79
90
  }
80
91
  }
81
92
  return events;
@@ -87,5 +98,43 @@ function countEvents() {
87
98
  function removeEvents(sentEventIds) {
88
99
  const remaining = readEvents().filter((e) => !sentEventIds.has(e.eventId));
89
100
  const body = remaining.map((e) => JSON.stringify(e)).join("\n");
90
- fs.writeFileSync(config_1.EVENTS_PATH, body ? body + "\n" : "");
101
+ (0, storage_1.atomicWrite)(config_1.EVENTS_PATH, body ? body + "\n" : "");
102
+ }
103
+ /** Persist before removing from the retry queue. No tokens or server writes. */
104
+ function quarantineEvents(events, reason) {
105
+ if (!events.length)
106
+ return;
107
+ (0, config_1.ensurePingletDir)();
108
+ fs.appendFileSync(path.join(config_1.PINGLET_DIR, "events-quarantine.jsonl"), events.map(event => JSON.stringify({ reason, quarantinedAt: new Date().toISOString(), event })).join("\n") + "\n", { mode: 0o600 });
109
+ removeEvents(new Set(events.map(event => event.eventId)));
110
+ }
111
+ /** Legacy/offline events bind once, before any installation can be revoked. */
112
+ function bindEvents() {
113
+ const config = (0, config_1.loadConfig)();
114
+ const events = readEvents();
115
+ let changed = false;
116
+ for (const event of events) {
117
+ if (!event.installationId && config.installations[event.agentType]) {
118
+ event.installationId = config.installations[event.agentType].installationId;
119
+ changed = true;
120
+ }
121
+ }
122
+ if (changed)
123
+ (0, storage_1.atomicWrite)(config_1.EVENTS_PATH, events.map(event => JSON.stringify(event)).join("\n") + "\n");
124
+ return events;
125
+ }
126
+ function invalidEventReason(event) {
127
+ if (event.queueError)
128
+ return event.queueError;
129
+ if (!["CLAUDE", "CODEX"].includes(event.agentType) ||
130
+ !["DELIVERED", "QUALIFIED_IMPRESSION"].includes(event.type))
131
+ return "invalid-type";
132
+ if ([event.eventId, event.messageId].some(value => typeof value !== "string" || !value.length || value.length > 64))
133
+ return "invalid-id";
134
+ if (event.visibleMs != null && (!Number.isInteger(event.visibleMs) || event.visibleMs < 0 || event.visibleMs > 60_000))
135
+ return "invalid-visible-ms";
136
+ const time = typeof event.occurredAt === "string" ? Date.parse(event.occurredAt) : NaN;
137
+ if (!Number.isFinite(time) || time < Date.UTC(2000, 0, 1) || time > Date.now() + 5 * 60_000)
138
+ return "invalid-timestamp";
139
+ return null;
91
140
  }
package/dist/runtime.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FEED_TTL_MS = exports.FLUSH_COUNT = exports.FLUSH_INTERVAL_MS = exports.QUALIFIED_MS = exports.ROTATE_MS = void 0;
4
+ exports.spawnDetachedCommand = spawnDetachedCommand;
4
5
  exports.runMaintenance = runMaintenance;
5
6
  const child_process_1 = require("child_process");
6
7
  const config_1 = require("./config");
@@ -17,6 +18,7 @@ function spawnDetachedCommand(args) {
17
18
  detached: true,
18
19
  stdio: "ignore",
19
20
  });
21
+ child.on("error", () => { });
20
22
  child.unref();
21
23
  }
22
24
  catch {
@@ -0,0 +1,61 @@
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
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.isMissing = isMissing;
37
+ exports.atomicWrite = atomicWrite;
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const crypto_1 = require("crypto");
41
+ function isMissing(error) {
42
+ return error.code === "ENOENT";
43
+ }
44
+ /** A unique, private temporary file prevents partial reads and writer collisions. */
45
+ function atomicWrite(filePath, body) {
46
+ fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
47
+ const tmp = `${filePath}.${process.pid}.${(0, crypto_1.randomUUID)()}.tmp`;
48
+ try {
49
+ fs.writeFileSync(tmp, body, { flag: "wx", mode: 0o600 });
50
+ fs.renameSync(tmp, filePath);
51
+ }
52
+ finally {
53
+ try {
54
+ fs.unlinkSync(tmp);
55
+ }
56
+ catch (error) {
57
+ if (!isMissing(error))
58
+ throw error;
59
+ }
60
+ }
61
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinglet-cli",
3
- "version": "0.5.4",
3
+ "version": "0.6.1",
4
4
  "description": "Short messages from other developers shown in your Claude Code spinner while the AI thinks — zero tokens, never reads your code. Claude Code / Codex integration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -27,6 +27,7 @@
27
27
  ],
28
28
  "scripts": {
29
29
  "build": "tsc",
30
+ "test": "npm run build && node --test test/*.test.cjs",
30
31
  "dev": "tsc --watch",
31
32
  "prepare": "npm run build",
32
33
  "postinstall": "node dist/postinstall.js || exit 0"