calendit 1.0.3 → 2026.4.26

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.
Files changed (52) hide show
  1. package/README.md +81 -62
  2. package/dist/commands/accounts.d.ts +4 -0
  3. package/dist/commands/accounts.js +26 -0
  4. package/dist/commands/add.js +8 -0
  5. package/dist/commands/apply.js +5 -1
  6. package/dist/commands/auth.js +11 -2
  7. package/dist/commands/config.js +50 -16
  8. package/dist/commands/macos.d.ts +3 -0
  9. package/dist/commands/macos.js +401 -0
  10. package/dist/commands/onboard.d.ts +2 -0
  11. package/dist/commands/onboard.js +79 -0
  12. package/dist/commands/query.js +2 -2
  13. package/dist/commands/shared.d.ts +3 -2
  14. package/dist/commands/shared.js +21 -5
  15. package/dist/core/accountStatus.d.ts +18 -0
  16. package/dist/core/accountStatus.js +74 -0
  17. package/dist/core/auth.js +7 -11
  18. package/dist/core/authStatus.d.ts +20 -0
  19. package/dist/core/authStatus.js +82 -0
  20. package/dist/core/config.d.ts +11 -1
  21. package/dist/core/config.js +73 -6
  22. package/dist/core/datetime.js +3 -2
  23. package/dist/core/errors.d.ts +3 -0
  24. package/dist/core/errors.js +5 -0
  25. package/dist/core/eventkitBridgeFetch.d.ts +26 -0
  26. package/dist/core/eventkitBridgeFetch.js +159 -0
  27. package/dist/core/eventkitEnvFromConfig.d.ts +7 -0
  28. package/dist/core/eventkitEnvFromConfig.js +24 -0
  29. package/dist/core/eventkitHelper.d.ts +50 -0
  30. package/dist/core/eventkitHelper.js +336 -0
  31. package/dist/core/formatter.d.ts +41 -0
  32. package/dist/core/formatter.js +79 -0
  33. package/dist/core/i18n.d.ts +7 -0
  34. package/dist/core/i18n.js +52 -0
  35. package/dist/core/localeBootstrap.d.ts +12 -0
  36. package/dist/core/localeBootstrap.js +74 -0
  37. package/dist/core/logger.d.ts +2 -0
  38. package/dist/core/logger.js +5 -0
  39. package/dist/core/macosBridgeApp.d.ts +12 -0
  40. package/dist/core/macosBridgeApp.js +83 -0
  41. package/dist/core/macosTerminalRelay.d.ts +12 -0
  42. package/dist/core/macosTerminalRelay.js +62 -0
  43. package/dist/generated/locale-keys.d.ts +3 -0
  44. package/dist/generated/locale-keys.js +90 -0
  45. package/dist/index.js +99 -17
  46. package/dist/locales/en.json +128 -0
  47. package/dist/locales/ja.json +128 -0
  48. package/dist/services/macos.d.ts +14 -0
  49. package/dist/services/macos.js +115 -0
  50. package/dist/test_runner.js +11 -2
  51. package/dist/types/index.d.ts +12 -1
  52. package/package.json +16 -5
@@ -0,0 +1,50 @@
1
+ /** Same directory layout as native/eventkit-bridge (Swift). */
2
+ export declare function defaultCalenditDataDir(): string;
3
+ /** Unix socket path when `CALENDIT_EVENTKIT_BRIDGE=1`. */
4
+ export declare function defaultEventkitBridgeSocketPath(): string;
5
+ export declare function bridgeTokenPath(): string;
6
+ /**
7
+ * If default socket + token exist and the socket file is a real Unix socket, use the bridge
8
+ * (no env var required on darwin). Otherwise returns null to fall back to the helper.
9
+ */
10
+ export declare function resolveAutoBridgeSocketPathIfReady(): string | null;
11
+ /**
12
+ * Resolve which Unix socket the bridge is listening on for this run (if any).
13
+ * - Explicit: `=1` / `unix:...` (always return path; missing token is an error on connect).
14
+ * - `=0` / `false` / `no` / `off`: return null (always use helper process).
15
+ * - Unset on darwin: auto if token + live socket file exist, else null.
16
+ * - `=auto`: same as auto branch.
17
+ */
18
+ export declare function resolveBridgeSocketPathForRun(): string | null;
19
+ /** @deprecated use resolveBridgeSocketPathForRun */
20
+ export declare function resolveBridgeSocketPath(): string | null;
21
+ /** true if a helper binary exists, or a bridge socket path is in use (explicit or auto). */
22
+ export declare function hasEventkitTransport(): boolean;
23
+ /** 優先: CALENDIT_EVENTKIT_HELPER → リポジトリ直下の release ビルド */
24
+ export declare function resolveEventkitHelperPath(): string | null;
25
+ /**
26
+ * Run EventKit helper: **bridge** when a socket is resolved, else spawn `eventkit-helper`.
27
+ * Fallback to spawn only if `CALENDIT_EVENTKIT_BRIDGE_FALLBACK=1`.
28
+ */
29
+ export declare function runEventkitHelper(args: string[], options?: {
30
+ stdin?: string;
31
+ timeoutMs?: number;
32
+ }): Promise<{
33
+ stdout: string;
34
+ stderr: string;
35
+ }>;
36
+ export declare function eventkitDoctorJson(): Promise<{
37
+ ok: boolean;
38
+ platform?: string;
39
+ calendarAccess?: string;
40
+ helperVersion?: number;
41
+ transport?: string;
42
+ }>;
43
+ export declare function eventkitListCalendarsJson(): Promise<{
44
+ calendars: Array<{
45
+ calendarIdentifier: string;
46
+ title: string;
47
+ sourceTitle: string;
48
+ allowsContentModification: boolean;
49
+ }>;
50
+ }>;
@@ -0,0 +1,336 @@
1
+ import { spawn } from "child_process";
2
+ import * as fs from "fs";
3
+ import * as net from "net";
4
+ import * as os from "os";
5
+ import * as path from "path";
6
+ import { t } from "./i18n.js";
7
+ /** Same directory layout as native/eventkit-bridge (Swift). */
8
+ export function defaultCalenditDataDir() {
9
+ const fromEnv = process.env.CALENDIT_CONFIG_DIR?.trim();
10
+ if (fromEnv) {
11
+ return path.resolve(fromEnv);
12
+ }
13
+ return path.join(os.homedir(), "Library", "Application Support", "calendit");
14
+ }
15
+ /** Unix socket path when `CALENDIT_EVENTKIT_BRIDGE=1`. */
16
+ export function defaultEventkitBridgeSocketPath() {
17
+ return path.join(defaultCalenditDataDir(), "eventkit-bridge.sock");
18
+ }
19
+ export function bridgeTokenPath() {
20
+ return path.join(defaultCalenditDataDir(), "bridge.token");
21
+ }
22
+ const BRIDGE_DISABLE = new Set(["0", "false", "no", "off"]);
23
+ function isBridgeExplicitlyDisabled() {
24
+ const raw = process.env.CALENDIT_EVENTKIT_BRIDGE?.trim();
25
+ if (raw === undefined) {
26
+ return false;
27
+ }
28
+ if (raw === "") {
29
+ return false;
30
+ }
31
+ return BRIDGE_DISABLE.has(raw.toLowerCase());
32
+ }
33
+ /**
34
+ * If default socket + token exist and the socket file is a real Unix socket, use the bridge
35
+ * (no env var required on darwin). Otherwise returns null to fall back to the helper.
36
+ */
37
+ export function resolveAutoBridgeSocketPathIfReady() {
38
+ if (process.platform !== "darwin") {
39
+ return null;
40
+ }
41
+ const socketPath = defaultEventkitBridgeSocketPath();
42
+ const tokenPath = bridgeTokenPath();
43
+ if (!fs.existsSync(tokenPath)) {
44
+ return null;
45
+ }
46
+ if (!fs.existsSync(socketPath)) {
47
+ return null;
48
+ }
49
+ try {
50
+ if (!fs.statSync(socketPath).isSocket()) {
51
+ return null;
52
+ }
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ return socketPath;
58
+ }
59
+ /**
60
+ * `CALENDIT_EVENTKIT_BRIDGE=1` / `true` or `unix:...` (explicit bridge only; no auto heuristics).
61
+ */
62
+ function resolveExplicitBridgeOnly() {
63
+ const raw = process.env.CALENDIT_EVENTKIT_BRIDGE?.trim();
64
+ if (!raw || isBridgeExplicitlyDisabled()) {
65
+ return null;
66
+ }
67
+ const v = raw.toLowerCase();
68
+ if (v === "1" || v === "true" || v === "yes" || v === "on") {
69
+ return defaultEventkitBridgeSocketPath();
70
+ }
71
+ if (v === "auto") {
72
+ return null;
73
+ }
74
+ if (raw.startsWith("unix:")) {
75
+ let rest = raw.slice("unix:".length);
76
+ while (rest.startsWith("//")) {
77
+ rest = rest.slice(1);
78
+ }
79
+ if (!rest) {
80
+ return null;
81
+ }
82
+ return path.isAbsolute(rest) ? rest : path.resolve(process.cwd(), rest);
83
+ }
84
+ return null;
85
+ }
86
+ /**
87
+ * Resolve which Unix socket the bridge is listening on for this run (if any).
88
+ * - Explicit: `=1` / `unix:...` (always return path; missing token is an error on connect).
89
+ * - `=0` / `false` / `no` / `off`: return null (always use helper process).
90
+ * - Unset on darwin: auto if token + live socket file exist, else null.
91
+ * - `=auto`: same as auto branch.
92
+ */
93
+ export function resolveBridgeSocketPathForRun() {
94
+ if (isBridgeExplicitlyDisabled()) {
95
+ return null;
96
+ }
97
+ const explicit = resolveExplicitBridgeOnly();
98
+ if (explicit) {
99
+ return explicit;
100
+ }
101
+ const raw = process.env.CALENDIT_EVENTKIT_BRIDGE?.trim();
102
+ if (raw === "auto" || !raw) {
103
+ return process.platform === "darwin" ? resolveAutoBridgeSocketPathIfReady() : null;
104
+ }
105
+ return null;
106
+ }
107
+ /** @deprecated use resolveBridgeSocketPathForRun */
108
+ export function resolveBridgeSocketPath() {
109
+ return resolveBridgeSocketPathForRun();
110
+ }
111
+ /** true if a helper binary exists, or a bridge socket path is in use (explicit or auto). */
112
+ export function hasEventkitTransport() {
113
+ if (isBridgeExplicitlyDisabled()) {
114
+ return resolveEventkitHelperPath() !== null;
115
+ }
116
+ return resolveEventkitHelperPath() !== null || resolveBridgeSocketPathForRun() !== null;
117
+ }
118
+ function readBridgeToken() {
119
+ const p = bridgeTokenPath();
120
+ if (!fs.existsSync(p)) {
121
+ throw new Error(`${t("eventkit.bridge.tokenMissing", { path: p })} ${t("eventkit.bridge.hintStartBridge")}`);
122
+ }
123
+ return fs.readFileSync(p, "utf8").trim();
124
+ }
125
+ function buildBridgePayload(args, stdin) {
126
+ const cmd = args[0];
127
+ const token = readBridgeToken();
128
+ if (cmd === "doctor") {
129
+ return { v: 1, op: "doctor", token, body: {} };
130
+ }
131
+ if (cmd === "list-calendars") {
132
+ return { v: 1, op: "list-calendars", token, body: {} };
133
+ }
134
+ if (cmd === "list-events") {
135
+ if (args.length < 4) {
136
+ throw new Error("list-events requires calendarId, start, end");
137
+ }
138
+ return {
139
+ v: 1,
140
+ op: "list-events",
141
+ token,
142
+ body: { calendarId: args[1], start: args[2], end: args[3] },
143
+ };
144
+ }
145
+ if (cmd === "create-event") {
146
+ let body = {};
147
+ if (stdin !== undefined && stdin.length > 0) {
148
+ body = JSON.parse(stdin);
149
+ }
150
+ return { v: 1, op: "create-event", token, body };
151
+ }
152
+ if (cmd === "update-event") {
153
+ let body = {};
154
+ if (stdin !== undefined && stdin.length > 0) {
155
+ body = JSON.parse(stdin);
156
+ }
157
+ return { v: 1, op: "update-event", token, body };
158
+ }
159
+ if (cmd === "delete-event") {
160
+ if (args.length < 3) {
161
+ throw new Error("delete-event requires calendarId and eventId");
162
+ }
163
+ return {
164
+ v: 1,
165
+ op: "delete-event",
166
+ token,
167
+ body: { calendarId: args[1], eventId: args[2] },
168
+ };
169
+ }
170
+ throw new Error(`Unknown EventKit command for bridge: ${cmd}`);
171
+ }
172
+ function runEventkitBridgeSocket(socketPath, args, options) {
173
+ const timeout = options.timeoutMs ?? 120_000;
174
+ const payload = buildBridgePayload(args, options.stdin);
175
+ const line = JSON.stringify(payload);
176
+ return new Promise((resolve, reject) => {
177
+ const client = net.createConnection({ path: socketPath });
178
+ let buf = "";
179
+ let settled = false;
180
+ const timer = setTimeout(() => {
181
+ if (settled)
182
+ return;
183
+ settled = true;
184
+ client.destroy();
185
+ reject(new Error(t("eventkit.bridge.timeout", { ms: String(timeout) })));
186
+ }, timeout);
187
+ const fail = (err) => {
188
+ if (settled)
189
+ return;
190
+ settled = true;
191
+ clearTimeout(timer);
192
+ client.destroy();
193
+ reject(err);
194
+ };
195
+ const ok = (stdout) => {
196
+ if (settled)
197
+ return;
198
+ settled = true;
199
+ clearTimeout(timer);
200
+ client.end();
201
+ resolve({ stdout, stderr: "" });
202
+ };
203
+ client.on("error", (err) => {
204
+ fail(new Error(`${t("eventkit.bridge.connectFailed", { path: socketPath, message: err.message })} ${t("eventkit.bridge.hintStartBridge")}`));
205
+ });
206
+ client.on("connect", () => {
207
+ client.write(`${line}\n`);
208
+ });
209
+ client.on("data", (chunk) => {
210
+ buf += chunk.toString("utf8");
211
+ const nl = buf.indexOf("\n");
212
+ if (nl === -1) {
213
+ return;
214
+ }
215
+ const responseLine = buf.slice(0, nl).trim();
216
+ try {
217
+ const parsed = JSON.parse(responseLine);
218
+ if (parsed.bridgeError === true) {
219
+ fail(new Error(String(parsed.error ?? t("eventkit.bridge.bridgeError"))));
220
+ return;
221
+ }
222
+ ok(responseLine);
223
+ }
224
+ catch {
225
+ fail(new Error(t("eventkit.bridge.invalidJson", { line: responseLine.slice(0, 200) })));
226
+ }
227
+ });
228
+ client.on("close", () => {
229
+ if (settled)
230
+ return;
231
+ if (!buf.includes("\n")) {
232
+ fail(new Error(t("eventkit.bridge.closedWithoutResponse")));
233
+ }
234
+ });
235
+ });
236
+ }
237
+ function isRetryableBridgeError(err) {
238
+ if (!(err instanceof Error)) {
239
+ return false;
240
+ }
241
+ return /could not connect|ECONNREFUSED|not connect|connect ECONNREFUSED/i.test(err.message);
242
+ }
243
+ async function runEventkitBridgeSocketWithRetry(socketPath, args, options) {
244
+ const maxAttempts = 4;
245
+ let last;
246
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
247
+ try {
248
+ if (attempt > 0) {
249
+ await new Promise((r) => setTimeout(r, 400 * attempt));
250
+ }
251
+ return await runEventkitBridgeSocket(socketPath, args, options);
252
+ }
253
+ catch (e) {
254
+ last = e;
255
+ if (!isRetryableBridgeError(e) || attempt === maxAttempts - 1) {
256
+ break;
257
+ }
258
+ }
259
+ }
260
+ throw last;
261
+ }
262
+ async function runEventkitHelperSpawn(args, options) {
263
+ const exe = resolveEventkitHelperPath();
264
+ if (!exe) {
265
+ return Promise.reject(new Error(`${t("eventkit.helper.missing")} ${t("eventkit.helper.hintBuild")}`));
266
+ }
267
+ const timeout = options.timeoutMs ?? 120_000;
268
+ return new Promise((resolve, reject) => {
269
+ const child = spawn(exe, args, { stdio: ["pipe", "pipe", "pipe"] });
270
+ let stdout = "";
271
+ let stderr = "";
272
+ child.stdout?.on("data", (chunk) => {
273
+ stdout += chunk.toString("utf8");
274
+ });
275
+ child.stderr?.on("data", (chunk) => {
276
+ stderr += chunk.toString("utf8");
277
+ });
278
+ if (options.stdin !== undefined) {
279
+ child.stdin?.write(options.stdin, "utf8");
280
+ }
281
+ child.stdin?.end();
282
+ const timer = setTimeout(() => {
283
+ child.kill("SIGTERM");
284
+ reject(new Error(t("eventkit.helper.timeout", { ms: String(timeout) })));
285
+ }, timeout);
286
+ child.on("error", (err) => {
287
+ clearTimeout(timer);
288
+ reject(err);
289
+ });
290
+ child.on("close", (code) => {
291
+ clearTimeout(timer);
292
+ if (code === 0) {
293
+ resolve({ stdout, stderr });
294
+ }
295
+ else {
296
+ reject(new Error(stderr.trim() || t("eventkit.helper.exitCode", { code: String(code ?? "null") })));
297
+ }
298
+ });
299
+ });
300
+ }
301
+ /** 優先: CALENDIT_EVENTKIT_HELPER → リポジトリ直下の release ビルド */
302
+ export function resolveEventkitHelperPath() {
303
+ const fromEnv = process.env.CALENDIT_EVENTKIT_HELPER?.trim();
304
+ if (fromEnv && fs.existsSync(fromEnv)) {
305
+ return fromEnv;
306
+ }
307
+ const rel = path.join(process.cwd(), "native", "eventkit-helper", ".build", "release", "eventkit-helper");
308
+ if (fs.existsSync(rel)) {
309
+ return rel;
310
+ }
311
+ return null;
312
+ }
313
+ /**
314
+ * Run EventKit helper: **bridge** when a socket is resolved, else spawn `eventkit-helper`.
315
+ * Fallback to spawn only if `CALENDIT_EVENTKIT_BRIDGE_FALLBACK=1`.
316
+ */
317
+ export function runEventkitHelper(args, options = {}) {
318
+ const socketPath = resolveBridgeSocketPathForRun();
319
+ if (socketPath) {
320
+ return runEventkitBridgeSocketWithRetry(socketPath, args, options).catch((err) => {
321
+ if (process.env.CALENDIT_EVENTKIT_BRIDGE_FALLBACK === "1") {
322
+ return runEventkitHelperSpawn(args, options);
323
+ }
324
+ throw err;
325
+ });
326
+ }
327
+ return runEventkitHelperSpawn(args, options);
328
+ }
329
+ export async function eventkitDoctorJson() {
330
+ const { stdout } = await runEventkitHelper(["doctor"], { timeoutMs: 120_000 });
331
+ return JSON.parse(stdout.trim());
332
+ }
333
+ export async function eventkitListCalendarsJson() {
334
+ const { stdout } = await runEventkitHelper(["list-calendars"], { timeoutMs: 120_000 });
335
+ return JSON.parse(stdout.trim());
336
+ }
@@ -1,4 +1,31 @@
1
1
  import { CalendarEvent } from "../types/index.js";
2
+ import { z } from "zod";
3
+ declare const calendarJsonSchema: z.ZodObject<{
4
+ version: z.ZodLiteral<"1">;
5
+ generated_at: z.ZodString;
6
+ context: z.ZodOptional<z.ZodString>;
7
+ service: z.ZodOptional<z.ZodEnum<{
8
+ google: "google";
9
+ outlook: "outlook";
10
+ macos: "macos";
11
+ }>>;
12
+ calendar_id: z.ZodOptional<z.ZodString>;
13
+ events: z.ZodArray<z.ZodObject<{
14
+ id: z.ZodOptional<z.ZodString>;
15
+ summary: z.ZodString;
16
+ start: z.ZodString;
17
+ end: z.ZodString;
18
+ location: z.ZodOptional<z.ZodString>;
19
+ description: z.ZodOptional<z.ZodString>;
20
+ service: z.ZodOptional<z.ZodEnum<{
21
+ google: "google";
22
+ outlook: "outlook";
23
+ macos: "macos";
24
+ }>>;
25
+ calendar_id: z.ZodOptional<z.ZodString>;
26
+ }, z.core.$strip>>;
27
+ }, z.core.$strip>;
28
+ export type CalendarJsonFormat = z.infer<typeof calendarJsonSchema>;
2
29
  export interface ParseResult {
3
30
  events: Partial<CalendarEvent>[];
4
31
  warnings: string[];
@@ -22,6 +49,20 @@ export declare class Formatter {
22
49
  * インデントされた行は説明文として扱う
23
50
  */
24
51
  static fromMarkdown(md: string, strict?: boolean): ParseResult;
52
+ /**
53
+ * 予定の配列を CalendarJSON ラッパー形式の文字列に変換
54
+ */
55
+ static toJson(events: CalendarEvent[], meta?: {
56
+ context?: string;
57
+ service?: "google" | "outlook" | "macos" | "mock";
58
+ calendarId?: string;
59
+ }): string;
60
+ /**
61
+ * CalendarJSON 文字列から予定を抽出
62
+ * 旧来の events 配列形式も警告付きで受け入れる(後方互換)
63
+ */
64
+ static fromJson(data: string): ParseResult;
25
65
  private static groupByDate;
26
66
  private static formatTime;
27
67
  }
68
+ export {};
@@ -11,6 +11,24 @@ const partialEventSchema = z.object({
11
11
  location: z.string().optional(),
12
12
  description: z.string().optional(),
13
13
  });
14
+ const jsonEventSchema = z.object({
15
+ id: z.string().optional(),
16
+ summary: z.string().min(1, "summary is required"),
17
+ start: z.string(),
18
+ end: z.string(),
19
+ location: z.string().optional(),
20
+ description: z.string().optional(),
21
+ service: z.enum(["google", "outlook", "macos"]).optional(),
22
+ calendar_id: z.string().optional(),
23
+ });
24
+ const calendarJsonSchema = z.object({
25
+ version: z.literal("1"),
26
+ generated_at: z.string(),
27
+ context: z.string().optional(),
28
+ service: z.enum(["google", "outlook", "macos"]).optional(),
29
+ calendar_id: z.string().optional(),
30
+ events: z.array(jsonEventSchema),
31
+ });
14
32
  export class Formatter {
15
33
  /**
16
34
  * 予定の配列を CSV 文字列に変換
@@ -142,6 +160,67 @@ export class Formatter {
142
160
  warnings.forEach((warning) => logger.warn(warning));
143
161
  return { events, warnings };
144
162
  }
163
+ /**
164
+ * 予定の配列を CalendarJSON ラッパー形式の文字列に変換
165
+ */
166
+ static toJson(events, meta) {
167
+ const serviceValue = meta?.service === "google" || meta?.service === "outlook" || meta?.service === "macos" ? meta.service : undefined;
168
+ const payload = {
169
+ version: "1",
170
+ generated_at: new Date().toISOString(),
171
+ context: meta?.context,
172
+ service: serviceValue,
173
+ calendar_id: meta?.calendarId,
174
+ events: events.map((e) => ({
175
+ id: e.id,
176
+ summary: e.summary,
177
+ start: e.start,
178
+ end: e.end,
179
+ location: e.location,
180
+ description: e.description,
181
+ service: e.service,
182
+ calendar_id: e.calendarId,
183
+ })),
184
+ };
185
+ return JSON.stringify(payload, null, 2);
186
+ }
187
+ /**
188
+ * CalendarJSON 文字列から予定を抽出
189
+ * 旧来の events 配列形式も警告付きで受け入れる(後方互換)
190
+ */
191
+ static fromJson(data) {
192
+ const warnings = [];
193
+ let raw;
194
+ try {
195
+ raw = JSON.parse(data);
196
+ }
197
+ catch {
198
+ throw new ValidationError("JSON のパースに失敗しました。", "有効な JSON ファイルを指定してください。");
199
+ }
200
+ // 後方互換: 旧来の plain array 形式
201
+ if (Array.isArray(raw)) {
202
+ warnings.push("後方互換: イベントの配列形式を検出しました。CalendarJSON ラッパー形式(version: \"1\")の使用を推奨します。");
203
+ warnings.forEach((w) => logger.warn(w));
204
+ return { events: raw, warnings };
205
+ }
206
+ const result = calendarJsonSchema.safeParse(raw);
207
+ if (!result.success) {
208
+ const detail = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ");
209
+ throw new ValidationError(`JSON スキーマ検証エラー: ${detail}`, "`calendit query --format json` で出力した JSON ファイルを使用してください。");
210
+ }
211
+ const events = result.data.events.map((e) => ({
212
+ id: e.id,
213
+ summary: e.summary,
214
+ start: e.start,
215
+ end: e.end,
216
+ location: e.location,
217
+ description: e.description,
218
+ service: e.service,
219
+ calendarId: e.calendar_id,
220
+ }));
221
+ warnings.forEach((w) => logger.warn(w));
222
+ return { events, warnings };
223
+ }
145
224
  static groupByDate(events) {
146
225
  const groups = {};
147
226
  for (const e of events) {
@@ -0,0 +1,7 @@
1
+ import type { LocaleKey } from "../generated/locale-keys.js";
2
+ export declare const SUPPORTED_UI_LOCALES: readonly ["en", "ja"];
3
+ export type UiLocale = (typeof SUPPORTED_UI_LOCALES)[number];
4
+ export declare function initI18n(locale: string): void;
5
+ export declare function getActiveLocale(): UiLocale;
6
+ export declare function t(key: LocaleKey, vars?: Record<string, string | number>): string;
7
+ export declare function isUiLocale(value: string): value is UiLocale;
@@ -0,0 +1,52 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { logger } from "./logger.js";
5
+ export const SUPPORTED_UI_LOCALES = ["en", "ja"];
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ function readLocaleFile(name) {
8
+ const file = path.join(__dirname, "..", "locales", `${name}.json`);
9
+ return JSON.parse(fs.readFileSync(file, "utf-8"));
10
+ }
11
+ const catalogs = {
12
+ en: readLocaleFile("en"),
13
+ ja: readLocaleFile("ja"),
14
+ };
15
+ let activeLocale = "en";
16
+ function getByPath(obj, parts) {
17
+ let cur = obj;
18
+ for (const p of parts) {
19
+ if (cur === null || typeof cur !== "object" || !(p in cur))
20
+ return undefined;
21
+ cur = cur[p];
22
+ }
23
+ return cur;
24
+ }
25
+ function interpolate(template, vars) {
26
+ if (!vars)
27
+ return template;
28
+ return template.replace(/\{(\w+)\}/g, (_, key) => String(vars[key] ?? `{${key}}`));
29
+ }
30
+ export function initI18n(locale) {
31
+ activeLocale = locale === "ja" ? "ja" : "en";
32
+ }
33
+ export function getActiveLocale() {
34
+ return activeLocale;
35
+ }
36
+ export function t(key, vars) {
37
+ let raw = getByPath(catalogs[activeLocale], key.split("."));
38
+ if (typeof raw !== "string") {
39
+ raw = getByPath(catalogs.en, key.split("."));
40
+ if (typeof raw !== "string") {
41
+ logger.debug("i18n", `Missing locale key: ${key}`);
42
+ return key;
43
+ }
44
+ if (activeLocale !== "en") {
45
+ logger.debug("i18n", `Fallback to en for key: ${key}`);
46
+ }
47
+ }
48
+ return interpolate(raw, vars);
49
+ }
50
+ export function isUiLocale(value) {
51
+ return SUPPORTED_UI_LOCALES.includes(value);
52
+ }
@@ -0,0 +1,12 @@
1
+ import { Command } from "commander";
2
+ import { ConfigManager } from "./config.js";
3
+ /** Skip locale/bootstrap IO for `--help` / `--version` only. */
4
+ export declare function shouldSkipLocaleBootstrap(): boolean;
5
+ /**
6
+ * After config is loaded, set active locale: CALENDIT_LOCALE > --locale > config.ui.locale > en.
7
+ */
8
+ export declare function applyResolvedLocale(program: Command, config: ConfigManager): void;
9
+ /**
10
+ * First-run language prompt when config.json is absent; then initI18n.
11
+ */
12
+ export declare function ensureLocalePreference(program: Command, config: ConfigManager): Promise<void>;
@@ -0,0 +1,74 @@
1
+ import Enquirer from "enquirer";
2
+ import { initI18n, isUiLocale } from "./i18n.js";
3
+ /** Skip locale/bootstrap IO for `--help` / `--version` only. */
4
+ export function shouldSkipLocaleBootstrap() {
5
+ const a = process.argv;
6
+ if (a.includes("--help") || a.includes("-h") || a.includes("--version") || a.includes("-V"))
7
+ return true;
8
+ return false;
9
+ }
10
+ function shouldSkipLocalePrompt() {
11
+ if (process.env.CALENDIT_SKIP_LOCALE_PROMPT === "1")
12
+ return true;
13
+ if (process.env.CALENDIT_MOCK === "true")
14
+ return true;
15
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
16
+ return true;
17
+ return false;
18
+ }
19
+ function resolveLocaleFromEnvAndArgv(program) {
20
+ const fromEnv = process.env.CALENDIT_LOCALE?.trim();
21
+ if (fromEnv && isUiLocale(fromEnv))
22
+ return fromEnv;
23
+ const opts = program.optsWithGlobals();
24
+ if (opts.locale && isUiLocale(opts.locale))
25
+ return opts.locale;
26
+ return undefined;
27
+ }
28
+ /**
29
+ * After config is loaded, set active locale: CALENDIT_LOCALE > --locale > config.ui.locale > en.
30
+ */
31
+ export function applyResolvedLocale(program, config) {
32
+ const fromEnvArgv = resolveLocaleFromEnvAndArgv(program);
33
+ if (fromEnvArgv) {
34
+ initI18n(fromEnvArgv);
35
+ return;
36
+ }
37
+ const ui = config.getUi();
38
+ initI18n(ui?.locale ?? "en");
39
+ }
40
+ /**
41
+ * First-run language prompt when config.json is absent; then initI18n.
42
+ */
43
+ export async function ensureLocalePreference(program, config) {
44
+ const forced = resolveLocaleFromEnvAndArgv(program);
45
+ initI18n(forced ?? "en");
46
+ if (shouldSkipLocaleBootstrap()) {
47
+ return;
48
+ }
49
+ if (forced) {
50
+ return;
51
+ }
52
+ const loaded = await config.loadOptional();
53
+ if (!loaded) {
54
+ if (shouldSkipLocalePrompt()) {
55
+ config.resetMinimalWithUi("en");
56
+ initI18n("en");
57
+ return;
58
+ }
59
+ const { locale: picked } = await Enquirer.prompt({
60
+ type: "select",
61
+ name: "locale",
62
+ message: "Choose your language for calendit messages:",
63
+ choices: [
64
+ { name: "English", value: "en" },
65
+ { name: "Japanese (日本語)", value: "ja" },
66
+ ],
67
+ });
68
+ const locale = picked === "ja" ? "ja" : "en";
69
+ config.resetMinimalWithUi(locale);
70
+ await config.save();
71
+ initI18n(locale);
72
+ return;
73
+ }
74
+ }
@@ -1,6 +1,8 @@
1
1
  export type LogLevel = "debug" | "info" | "warn" | "error";
2
2
  export declare function setLogLevel(level: LogLevel): void;
3
3
  export declare function setDebugDump(filePath: string): void;
4
+ /** ログプレフィックスなしの標準出力(表形式のユーザー向け表示用) */
5
+ export declare function writeStdoutLine(line: string): void;
4
6
  export declare const logger: {
5
7
  debug: (...args: unknown[]) => void;
6
8
  info: (...args: unknown[]) => void;