ccc-notifier 0.2.0 → 0.4.0

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.
@@ -2,42 +2,177 @@
2
2
  import {
3
3
  notifyOS,
4
4
  notifySlack
5
- } from "./chunk-4JNZ7BHO.js";
5
+ } from "./chunk-NV5UOHJA.js";
6
+ import {
7
+ codexHome,
8
+ detectCodex
9
+ } from "./chunk-HTYUYKFW.js";
6
10
  import {
7
11
  paths,
8
12
  readConfig
9
- } from "./chunk-IIYMGLV4.js";
13
+ } from "./chunk-26CISNOE.js";
10
14
 
11
15
  // src/setup.ts
16
+ import {
17
+ copyFileSync as copyFileSync2,
18
+ existsSync as existsSync2,
19
+ mkdirSync as mkdirSync2,
20
+ readFileSync as readFileSync2,
21
+ rmSync,
22
+ writeFileSync as writeFileSync2
23
+ } from "fs";
24
+ import { basename, dirname as dirname2, join as join2 } from "path";
25
+ import { homedir } from "os";
26
+ import { fileURLToPath } from "url";
27
+ import * as p from "@clack/prompts";
28
+
29
+ // src/codex/setup.ts
12
30
  import {
13
31
  copyFileSync,
14
32
  existsSync,
15
33
  mkdirSync,
16
34
  readFileSync,
17
- rmSync,
18
35
  writeFileSync
19
36
  } from "fs";
20
- import { basename, dirname, join } from "path";
21
- import { homedir } from "os";
22
- import { fileURLToPath } from "url";
23
- import * as p from "@clack/prompts";
37
+ import { dirname, join } from "path";
38
+ function isPlainObject(v) {
39
+ return typeof v === "object" && v !== null && !Array.isArray(v);
40
+ }
41
+ function codexHooksFile() {
42
+ return join(codexHome(), "hooks.json");
43
+ }
44
+ function codexHookCommand(nodePath, cliPath) {
45
+ let node = nodePath;
46
+ let cli = cliPath;
47
+ if (process.platform === "win32") {
48
+ node = node.replace(/\\/g, "/");
49
+ cli = cli.replace(/\\/g, "/");
50
+ }
51
+ return `"${node}" "${cli}" track --codex`;
52
+ }
53
+ function ourCodexStopEntry(command) {
54
+ return { hooks: [{ type: "command", command }] };
55
+ }
56
+ function isOurStopEntry(entry) {
57
+ if (!isPlainObject(entry)) return false;
58
+ const hooks = entry.hooks;
59
+ if (!Array.isArray(hooks)) return false;
60
+ return hooks.some(
61
+ (h) => isPlainObject(h) && typeof h.command === "string" && matchesMarker(h.command)
62
+ );
63
+ }
64
+ function buildManualSnippet(command) {
65
+ return JSON.stringify(ourCodexStopEntry(command), null, 2);
66
+ }
67
+ function backupHooks(path) {
68
+ const backupPath = `${path}.bak-${Date.now()}`;
69
+ copyFileSync(path, backupPath);
70
+ return backupPath;
71
+ }
72
+ function writeHooks(path, obj) {
73
+ writeFileSync(path, JSON.stringify(obj, null, 2) + "\n", "utf8");
74
+ }
75
+ function registerCodexHook(nodePath, cliPath) {
76
+ const hooksFile = codexHooksFile();
77
+ const command = codexHookCommand(nodePath, cliPath);
78
+ if (!existsSync(hooksFile)) {
79
+ mkdirSync(dirname(hooksFile), { recursive: true });
80
+ writeHooks(hooksFile, { hooks: { Stop: [ourCodexStopEntry(command)] } });
81
+ return { status: "written", backupPath: null };
82
+ }
83
+ const raw = readFileSync(hooksFile, "utf8");
84
+ let parsed;
85
+ try {
86
+ parsed = JSON.parse(raw);
87
+ } catch {
88
+ return { status: "manual", backupPath: null, manualSnippet: buildManualSnippet(command) };
89
+ }
90
+ if (!isPlainObject(parsed)) {
91
+ return { status: "manual", backupPath: null, manualSnippet: buildManualSnippet(command) };
92
+ }
93
+ const obj = parsed;
94
+ const hooksVal = obj.hooks;
95
+ if (hooksVal !== void 0 && !isPlainObject(hooksVal)) {
96
+ return { status: "manual", backupPath: null, manualSnippet: buildManualSnippet(command) };
97
+ }
98
+ const hooks = hooksVal ?? {};
99
+ const stopVal = hooks.Stop;
100
+ if (stopVal !== void 0 && !Array.isArray(stopVal)) {
101
+ return { status: "manual", backupPath: null, manualSnippet: buildManualSnippet(command) };
102
+ }
103
+ const stop = stopVal ?? [];
104
+ const idx = stop.findIndex(isOurStopEntry);
105
+ if (idx >= 0) {
106
+ const entry = stop[idx];
107
+ const entryHooks = entry.hooks;
108
+ const needsUpdate = entryHooks.some(
109
+ (h) => isPlainObject(h) && typeof h.command === "string" && matchesMarker(h.command) && h.command !== command
110
+ );
111
+ if (!needsUpdate) return { status: "unchanged", backupPath: null };
112
+ const backupPath2 = backupHooks(hooksFile);
113
+ for (const h of entryHooks) {
114
+ if (isPlainObject(h) && typeof h.command === "string" && matchesMarker(h.command)) {
115
+ h.command = command;
116
+ }
117
+ }
118
+ obj.hooks = hooks;
119
+ hooks.Stop = stop;
120
+ writeHooks(hooksFile, obj);
121
+ return { status: "written", backupPath: backupPath2 };
122
+ }
123
+ const backupPath = backupHooks(hooksFile);
124
+ stop.push(ourCodexStopEntry(command));
125
+ obj.hooks = hooks;
126
+ hooks.Stop = stop;
127
+ writeHooks(hooksFile, obj);
128
+ return { status: "written", backupPath };
129
+ }
130
+ function removeCodexHook() {
131
+ const hooksFile = codexHooksFile();
132
+ if (!existsSync(hooksFile)) return { status: "unchanged", backupPath: null };
133
+ const raw = readFileSync(hooksFile, "utf8");
134
+ let parsed;
135
+ try {
136
+ parsed = JSON.parse(raw);
137
+ } catch {
138
+ return { status: "unchanged", backupPath: null };
139
+ }
140
+ if (!isPlainObject(parsed)) return { status: "unchanged", backupPath: null };
141
+ const obj = parsed;
142
+ const hooks = isPlainObject(obj.hooks) ? obj.hooks : null;
143
+ const stop = hooks && Array.isArray(hooks.Stop) ? hooks.Stop : null;
144
+ if (!hooks || !stop || !stop.some(isOurStopEntry)) {
145
+ return { status: "unchanged", backupPath: null };
146
+ }
147
+ const backupPath = backupHooks(hooksFile);
148
+ const filtered = stop.filter((e) => !isOurStopEntry(e));
149
+ if (filtered.length === 0) {
150
+ delete hooks.Stop;
151
+ } else {
152
+ hooks.Stop = filtered;
153
+ }
154
+ writeHooks(hooksFile, obj);
155
+ return { status: "written", backupPath };
156
+ }
157
+
158
+ // src/setup.ts
24
159
  var HOOK_MARKER = "ccc-notifier";
25
160
  var HOOK_TIMEOUT = 15;
26
161
  var SLACK_PROMPT_CHARS = 100;
27
162
  var DEFAULT_BUDGET_USD = 400;
28
- function isPlainObject(v) {
163
+ function isPlainObject2(v) {
29
164
  return typeof v === "object" && v !== null && !Array.isArray(v);
30
165
  }
31
166
  function settingsPath() {
32
- return process.env.CCCN_CLAUDE_SETTINGS || join(homedir(), ".claude", "settings.json");
167
+ return process.env.CCCN_CLAUDE_SETTINGS || join2(homedir(), ".claude", "settings.json");
33
168
  }
34
169
  function resolveCliPath() {
35
170
  const override = process.env.CCCN_CLI_PATH;
36
171
  if (override) return override;
37
172
  const here = fileURLToPath(import.meta.url);
38
- const dir = dirname(here);
39
- if (basename(dir) === "dist") return join(dir, "cli.js");
40
- return join(dir, "..", "dist", "cli.js");
173
+ const dir = dirname2(here);
174
+ if (basename(dir) === "dist") return join2(dir, "cli.js");
175
+ return join2(dir, "..", "dist", "cli.js");
41
176
  }
42
177
  function buildHookCommand() {
43
178
  let nodePath = process.execPath;
@@ -51,12 +186,12 @@ function buildHookCommand() {
51
186
  function matchesMarker(command) {
52
187
  return command.includes(HOOK_MARKER);
53
188
  }
54
- function isOurStopEntry(entry) {
55
- if (!isPlainObject(entry)) return false;
189
+ function isOurStopEntry2(entry) {
190
+ if (!isPlainObject2(entry)) return false;
56
191
  const hooks = entry.hooks;
57
192
  if (!Array.isArray(hooks)) return false;
58
193
  return hooks.some(
59
- (h) => isPlainObject(h) && typeof h.command === "string" && matchesMarker(h.command)
194
+ (h) => isPlainObject2(h) && typeof h.command === "string" && matchesMarker(h.command)
60
195
  );
61
196
  }
62
197
  function ourStopEntry(command) {
@@ -64,7 +199,7 @@ function ourStopEntry(command) {
64
199
  }
65
200
  function backupSettings(path) {
66
201
  const backupPath = `${path}.bak-${Date.now()}`;
67
- copyFileSync(path, backupPath);
202
+ copyFileSync2(path, backupPath);
68
203
  return backupPath;
69
204
  }
70
205
  function printManualSnippet(path, command, reason) {
@@ -100,13 +235,22 @@ function takeValue(argv, i, prefix) {
100
235
  return argv[i + 1];
101
236
  }
102
237
  function parseInitFlags(argv) {
103
- const flags = { yes: false, osOnly: false, slackOnly: false, noNotify: false };
238
+ const flags = {
239
+ yes: false,
240
+ osOnly: false,
241
+ slackOnly: false,
242
+ noNotify: false,
243
+ codex: false,
244
+ noCodex: false
245
+ };
104
246
  for (let i = 0; i < argv.length; i++) {
105
247
  const a = argv[i];
106
248
  if (a === "--yes" || a === "-y") flags.yes = true;
107
249
  else if (a === "--os-only") flags.osOnly = true;
108
250
  else if (a === "--slack-only") flags.slackOnly = true;
109
251
  else if (a === "--no-notify") flags.noNotify = true;
252
+ else if (a === "--codex") flags.codex = true;
253
+ else if (a === "--no-codex") flags.noCodex = true;
110
254
  else if (a === "--slack-webhook" || a.startsWith("--slack-webhook=")) {
111
255
  flags.slackWebhook = takeValue(argv, i, "--slack-webhook");
112
256
  if (!a.includes("=")) i++;
@@ -132,13 +276,13 @@ function parseUninstallFlags(argv) {
132
276
  return flags;
133
277
  }
134
278
  function mergeSettings(sPath, command) {
135
- if (!existsSync(sPath)) {
136
- mkdirSync(dirname(sPath), { recursive: true });
279
+ if (!existsSync2(sPath)) {
280
+ mkdirSync2(dirname2(sPath), { recursive: true });
137
281
  const fresh = { hooks: { Stop: [ourStopEntry(command)] } };
138
- writeFileSync(sPath, JSON.stringify(fresh, null, 2) + "\n", "utf8");
282
+ writeFileSync2(sPath, JSON.stringify(fresh, null, 2) + "\n", "utf8");
139
283
  return { ok: true, backupPath: null };
140
284
  }
141
- const raw = readFileSync(sPath, "utf8");
285
+ const raw = readFileSync2(sPath, "utf8");
142
286
  let parsed;
143
287
  try {
144
288
  parsed = JSON.parse(raw);
@@ -146,22 +290,22 @@ function mergeSettings(sPath, command) {
146
290
  printManualSnippet(sPath, command, "settings.json \u306E JSON \u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F");
147
291
  return { ok: false, backupPath: null };
148
292
  }
149
- if (!isPlainObject(parsed)) {
293
+ if (!isPlainObject2(parsed)) {
150
294
  printManualSnippet(sPath, command, "settings.json \u306E\u30EB\u30FC\u30C8\u304C\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u306F\u3042\u308A\u307E\u305B\u3093");
151
295
  return { ok: false, backupPath: null };
152
296
  }
153
297
  const obj = parsed;
154
298
  const backupPath = backupSettings(sPath);
155
- if (!isPlainObject(obj.hooks)) obj.hooks = {};
299
+ if (!isPlainObject2(obj.hooks)) obj.hooks = {};
156
300
  const hooks = obj.hooks;
157
301
  if (!Array.isArray(hooks.Stop)) hooks.Stop = [];
158
302
  const stop = hooks.Stop;
159
- const idx = stop.findIndex(isOurStopEntry);
303
+ const idx = stop.findIndex(isOurStopEntry2);
160
304
  if (idx >= 0) {
161
305
  const entry = stop[idx];
162
306
  const entryHooks = entry.hooks;
163
307
  for (const h of entryHooks) {
164
- if (isPlainObject(h) && typeof h.command === "string" && matchesMarker(h.command)) {
308
+ if (isPlainObject2(h) && typeof h.command === "string" && matchesMarker(h.command)) {
165
309
  h.command = command;
166
310
  h.timeout = HOOK_TIMEOUT;
167
311
  }
@@ -169,11 +313,16 @@ function mergeSettings(sPath, command) {
169
313
  } else {
170
314
  stop.push(ourStopEntry(command));
171
315
  }
172
- writeFileSync(sPath, JSON.stringify(obj, null, 2) + "\n", "utf8");
316
+ writeFileSync2(sPath, JSON.stringify(obj, null, 2) + "\n", "utf8");
173
317
  return { ok: true, backupPath };
174
318
  }
175
319
  async function runInit(argv) {
176
320
  const flags = parseInitFlags(argv);
321
+ if (flags.codex && flags.noCodex) {
322
+ console.error("--codex \u3068 --no-codex \u306F\u540C\u6642\u306B\u6307\u5B9A\u3067\u304D\u307E\u305B\u3093");
323
+ return 1;
324
+ }
325
+ let installCodex = false;
177
326
  const cfg = readConfig();
178
327
  const initialChannel = !cfg.notify.os && !cfg.notify.slack ? "none" : cfg.notify.slack ? cfg.notify.os ? "both" : "slack" : "os";
179
328
  cfg.notify.os = true;
@@ -235,6 +384,7 @@ async function runInit(argv) {
235
384
  } else {
236
385
  cfg.monthlyBudgetUSD = budgetDefault;
237
386
  }
387
+ installCodex = flags.codex;
238
388
  } else {
239
389
  p.intro("ccc-notifier \u30BB\u30C3\u30C8\u30A2\u30C3\u30D7");
240
390
  const channel = await p.select({
@@ -316,14 +466,33 @@ async function runInit(argv) {
316
466
  }
317
467
  const parsedBudget = Number(budgetStr);
318
468
  if (Number.isFinite(parsedBudget) && parsedBudget >= 0) cfg.monthlyBudgetUSD = parsedBudget;
469
+ if (flags.codex) {
470
+ installCodex = true;
471
+ } else if (flags.noCodex) {
472
+ installCodex = false;
473
+ } else if (detectCodex()) {
474
+ const codexConfirm = await p.confirm({
475
+ message: "Codex CLI \u3092\u691C\u51FA\u3057\u307E\u3057\u305F\u3002Codex \u306B\u3082\u30B3\u30B9\u30C8\u901A\u77E5\u3092\u5165\u308C\u307E\u3059\u304B?",
476
+ initialValue: true
477
+ });
478
+ if (p.isCancel(codexConfirm)) {
479
+ p.cancel("\u30AD\u30E3\u30F3\u30BB\u30EB\u3057\u307E\u3057\u305F");
480
+ return 1;
481
+ }
482
+ installCodex = codexConfirm;
483
+ }
319
484
  p.outro("\u8A2D\u5B9A\u3092\u9069\u7528\u3057\u307E\u3059");
320
485
  }
321
486
  const cccn = paths();
322
- writeFileSync(cccn.configFile, JSON.stringify(cfg, null, 2) + "\n", "utf8");
487
+ writeFileSync2(cccn.configFile, JSON.stringify(cfg, null, 2) + "\n", "utf8");
323
488
  const command = buildHookCommand();
324
489
  const sPath = settingsPath();
325
490
  const result = mergeSettings(sPath, command);
326
491
  if (!result.ok) return 1;
492
+ let codexResult = null;
493
+ if (installCodex) {
494
+ codexResult = registerCodexHook(process.execPath, resolveCliPath());
495
+ }
327
496
  const notifyDisabled = !cfg.notify.os && !cfg.notify.slack;
328
497
  if (notifyDisabled) {
329
498
  console.log("\u30C6\u30B9\u30C8\u901A\u77E5: \u901A\u77E5\u306A\u3057\u30E2\u30FC\u30C9\u306E\u305F\u3081\u30B9\u30AD\u30C3\u30D7\u3057\u307E\u3057\u305F");
@@ -347,15 +516,35 @@ async function runInit(argv) {
347
516
  } else {
348
517
  console.log("Claude Code \u3067\u4F55\u304B\u5B9F\u884C\u3059\u308B\u3068\u901A\u77E5\u304C\u5C4A\u304D\u307E\u3059\u3002\u78BA\u8A8D: npx ccc-notifier doctor");
349
518
  }
519
+ if (codexResult) {
520
+ if (codexResult.status === "written") {
521
+ console.log(`Codex \u306B\u3082 Stop hook \u3092\u767B\u9332\u3057\u307E\u3057\u305F: ${codexHooksFile()}`);
522
+ if (codexResult.backupPath) {
523
+ console.log(` \u30D0\u30C3\u30AF\u30A2\u30C3\u30D7: ${codexResult.backupPath}`);
524
+ }
525
+ console.log(
526
+ "\u6B21\u56DE codex \u8D77\u52D5\u6642\u306B\u300EHooks need review\u300F\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002\u300ETrust all and continue\u300F\u3092\u9078\u3076\u3068\u6709\u52B9\u306B\u306A\u308A\u307E\u3059(\u627F\u8A8D\u307E\u3067\u306F\u52D5\u304D\u307E\u305B\u3093)"
527
+ );
528
+ } else if (codexResult.status === "unchanged") {
529
+ console.log("Codex \u306E Stop hook \u306F\u767B\u9332\u6E08\u307F\u3067\u3059");
530
+ } else {
531
+ console.error(`Codex \u306E hooks.json \u3092\u81EA\u52D5\u7DE8\u96C6\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F: ${codexHooksFile()}`);
532
+ console.error("\u5B89\u5168\u306E\u305F\u3081 hooks.json \u306E\u81EA\u52D5\u7DE8\u96C6\u3092\u4E2D\u6B62\u3057\u307E\u3057\u305F\u3002");
533
+ console.error("\u4EE5\u4E0B\u306E JSON \u3092 hooks.json \u306E hooks.Stop \u914D\u5217\u306B\u624B\u52D5\u3067\u8FFD\u52A0\u3057\u3066\u304F\u3060\u3055\u3044:");
534
+ console.error("");
535
+ console.error(codexResult.manualSnippet ?? "");
536
+ console.error("");
537
+ }
538
+ }
350
539
  return 0;
351
540
  }
352
541
  async function runUninstall(argv) {
353
542
  const flags = parseUninstallFlags(argv);
354
543
  const sPath = settingsPath();
355
- if (!existsSync(sPath)) {
544
+ if (!existsSync2(sPath)) {
356
545
  console.log("\u767B\u9332\u306A\u3057: settings.json \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3067\u3057\u305F\u3002");
357
546
  } else {
358
- const raw = readFileSync(sPath, "utf8");
547
+ const raw = readFileSync2(sPath, "utf8");
359
548
  let parsed;
360
549
  let parseOk = true;
361
550
  try {
@@ -363,29 +552,33 @@ async function runUninstall(argv) {
363
552
  } catch {
364
553
  parseOk = false;
365
554
  }
366
- if (!parseOk || !isPlainObject(parsed)) {
555
+ if (!parseOk || !isPlainObject2(parsed)) {
367
556
  console.error(`settings.json \u3092\u89E3\u6790\u3067\u304D\u306A\u3044\u305F\u3081\u7DE8\u96C6\u3092\u30B9\u30AD\u30C3\u30D7\u3057\u307E\u3057\u305F: ${sPath}`);
368
557
  } else {
369
558
  const obj = parsed;
370
- const hooks = isPlainObject(obj.hooks) ? obj.hooks : null;
559
+ const hooks = isPlainObject2(obj.hooks) ? obj.hooks : null;
371
560
  const stop = hooks && Array.isArray(hooks.Stop) ? hooks.Stop : null;
372
- const hasMarker = stop ? stop.some(isOurStopEntry) : false;
561
+ const hasMarker = stop ? stop.some(isOurStopEntry2) : false;
373
562
  if (!hasMarker || !hooks || !stop) {
374
563
  console.log("\u767B\u9332\u306A\u3057: \u672C\u30C4\u30FC\u30EB\u306E Stop \u30D5\u30C3\u30AF\u306F\u767B\u9332\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002");
375
564
  } else {
376
565
  const backupPath = backupSettings(sPath);
377
- const filtered = stop.filter((e) => !isOurStopEntry(e));
566
+ const filtered = stop.filter((e) => !isOurStopEntry2(e));
378
567
  if (filtered.length === 0) {
379
568
  delete hooks.Stop;
380
569
  } else {
381
570
  hooks.Stop = filtered;
382
571
  }
383
- writeFileSync(sPath, JSON.stringify(obj, null, 2) + "\n", "utf8");
572
+ writeFileSync2(sPath, JSON.stringify(obj, null, 2) + "\n", "utf8");
384
573
  console.log(`\u672C\u30C4\u30FC\u30EB\u306E Stop \u30D5\u30C3\u30AF\u3092\u9664\u53BB\u3057\u307E\u3057\u305F: ${sPath}`);
385
574
  console.log(` \u30D0\u30C3\u30AF\u30A2\u30C3\u30D7: ${backupPath}`);
386
575
  }
387
576
  }
388
577
  }
578
+ const codexRemoval = removeCodexHook();
579
+ if (codexRemoval.status === "written") {
580
+ console.log("Codex \u306E Stop hook \u3092\u524A\u9664\u3057\u307E\u3057\u305F");
581
+ }
389
582
  if (flags.purge) {
390
583
  const home = paths().home;
391
584
  let doPurge = flags.yes;
@@ -409,6 +602,7 @@ async function runUninstall(argv) {
409
602
  }
410
603
 
411
604
  export {
605
+ codexHooksFile,
412
606
  matchesMarker,
413
607
  runInit,
414
608
  runUninstall