calendit 1.0.2 → 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 +103 -18
  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,74 @@
1
+ import { buildAuthStatusRows } from "./authStatus.js";
2
+ import { eventkitDoctorJson, eventkitListCalendarsJson, hasEventkitTransport, } from "./eventkitHelper.js";
3
+ async function evaluateMacosContext(ctx) {
4
+ const fallbackAccount = ctx.accountId ?? "(default)";
5
+ if (process.env.CALENDIT_MOCK === "true") {
6
+ return { connection: "OK", account: fallbackAccount };
7
+ }
8
+ if (process.platform !== "darwin") {
9
+ return { connection: "N/A (non-macOS)", account: fallbackAccount };
10
+ }
11
+ if (!hasEventkitTransport()) {
12
+ return { connection: "NO HELPER", account: fallbackAccount };
13
+ }
14
+ try {
15
+ const doc = await eventkitDoctorJson();
16
+ if (!doc.ok) {
17
+ return { connection: "HELPER ERROR", account: fallbackAccount };
18
+ }
19
+ if (doc.calendarAccess !== "authorized") {
20
+ return { connection: "NO CALENDAR ACCESS", account: fallbackAccount };
21
+ }
22
+ const list = await eventkitListCalendarsJson();
23
+ const cal = list.calendars?.find((c) => c.calendarIdentifier === ctx.calendarId);
24
+ const viaBridge = doc.transport === "bridge";
25
+ const connection = cal
26
+ ? viaBridge
27
+ ? "OK (bridge)"
28
+ : "OK"
29
+ : "CALENDAR NOT FOUND";
30
+ const sourceLike = cal?.sourceTitle?.trim() || cal?.title?.trim();
31
+ const account = sourceLike || fallbackAccount;
32
+ return { connection, account };
33
+ }
34
+ catch {
35
+ return { connection: "HELPER ERROR", account: fallbackAccount };
36
+ }
37
+ }
38
+ export async function buildAccountStatusRows(contexts, deps) {
39
+ const entries = Object.entries(contexts).sort(([a], [b]) => a.localeCompare(b));
40
+ const rows = [];
41
+ for (const [contextName, ctx] of entries) {
42
+ const account = ctx.accountId ?? "(default)";
43
+ if (ctx.service === "macos") {
44
+ const { connection, account: macosAccount } = await evaluateMacosContext(ctx);
45
+ rows.push({
46
+ context: contextName,
47
+ service: "macos",
48
+ calendar: ctx.calendarId,
49
+ account: macosAccount,
50
+ connection,
51
+ });
52
+ continue;
53
+ }
54
+ const single = await buildAuthStatusRows({ [contextName]: ctx }, deps);
55
+ const r = single[0];
56
+ rows.push({
57
+ context: r.context,
58
+ service: r.service,
59
+ calendar: r.calendar,
60
+ account: r.account,
61
+ connection: r.token,
62
+ });
63
+ }
64
+ return rows;
65
+ }
66
+ export function formatAccountStatusTable(rows) {
67
+ const headers = ["CONTEXT", "SERVICE", "CALENDAR", "ACCOUNT", "CONNECTION"];
68
+ const dataRows = rows.map((r) => [r.context, r.service, r.calendar, r.account, r.connection]);
69
+ const all = [headers, ...dataRows.map((cols) => cols.map(String))];
70
+ const widths = headers.map((_, colIdx) => Math.max(...all.map((row) => row[colIdx].length)));
71
+ const sep = widths.map((w) => "-".repeat(w)).join(" ");
72
+ const fmt = (cols) => cols.map((c, i) => c.padEnd(widths[i])).join(" ");
73
+ return [fmt(headers), sep, ...dataRows.map((cols) => fmt(cols.map(String)))].join("\n");
74
+ }
package/dist/core/auth.js CHANGED
@@ -2,17 +2,12 @@ import { PublicClientApplication } from "@azure/msal-node";
2
2
  import { KeychainPersistence, PersistenceCachePlugin } from "@azure/msal-node-extensions";
3
3
  import { google } from "googleapis";
4
4
  import * as path from "path";
5
- import * as os from "os";
6
5
  import * as fs from "fs/promises";
7
6
  import * as http from "http";
8
7
  import open from "open";
9
8
  import { AuthError } from "./errors.js";
10
9
  import { logger } from "./logger.js";
11
- const CONFIG_DIR = path.join(os.homedir(), ".config", "calendit");
12
- function getTokenPath(name) {
13
- const filename = name ? `google_token_${name}.json` : "google_token.json";
14
- return path.join(CONFIG_DIR, filename);
15
- }
10
+ import { getCalenditConfigDir, getGoogleTokenFilePath } from "./config.js";
16
11
  export class AuthManager {
17
12
  msalClient;
18
13
  constructor() { }
@@ -21,8 +16,9 @@ export class AuthManager {
21
16
  */
22
17
  async getOutlookClient(clientId, tenantId = "common") {
23
18
  if (!this.msalClient) {
24
- await fs.mkdir(CONFIG_DIR, { recursive: true });
25
- const cachePath = path.join(CONFIG_DIR, "msal_cache.json");
19
+ const configDir = getCalenditConfigDir();
20
+ await fs.mkdir(configDir, { recursive: true });
21
+ const cachePath = path.join(configDir, "msal_cache.json");
26
22
  const persistence = await KeychainPersistence.create(cachePath, "calendit-service", "outlook-account");
27
23
  const cachePlugin = new PersistenceCachePlugin(persistence);
28
24
  const msalConfig = {
@@ -43,7 +39,7 @@ export class AuthManager {
43
39
  */
44
40
  async getGoogleAuth(clientId, clientSecret, accountId) {
45
41
  const oauth2Client = new google.auth.OAuth2(clientId, clientSecret, "http://localhost:3000");
46
- const tokenPath = getTokenPath(accountId);
42
+ const tokenPath = getGoogleTokenFilePath(accountId);
47
43
  try {
48
44
  const tokenData = await fs.readFile(tokenPath, "utf-8");
49
45
  oauth2Client.setCredentials(JSON.parse(tokenData));
@@ -58,7 +54,7 @@ export class AuthManager {
58
54
  */
59
55
  async loginGoogle(clientId, clientSecret, accountId) {
60
56
  const oauth2Client = new google.auth.OAuth2(clientId, clientSecret, "http://localhost:3000");
61
- const tokenPath = getTokenPath(accountId);
57
+ const tokenPath = getGoogleTokenFilePath(accountId);
62
58
  const authUrl = oauth2Client.generateAuthUrl({
63
59
  access_type: "offline",
64
60
  scope: ["https://www.googleapis.com/auth/calendar"],
@@ -79,7 +75,7 @@ export class AuthManager {
79
75
  const code = url.searchParams.get("code");
80
76
  if (code) {
81
77
  const { tokens } = await oauth2Client.getToken(code);
82
- await fs.mkdir(CONFIG_DIR, { recursive: true });
78
+ await fs.mkdir(getCalenditConfigDir(), { recursive: true });
83
79
  await fs.writeFile(tokenPath, JSON.stringify(tokens, null, 2));
84
80
  res.end("Authentication successful! You can close this tab and return to the terminal.");
85
81
  clearTimeout(timeout);
@@ -0,0 +1,20 @@
1
+ import type { ContextConfig } from "../types/index.js";
2
+ import type { OutlookCredentials } from "../types/index.js";
3
+ import type { AuthManager } from "./auth.js";
4
+ export type AuthTokenColumn = "OK" | "NOT LOGGED IN" | "EXPIRED" | "NOT CONFIGURED";
5
+ export interface AuthStatusRow {
6
+ context: string;
7
+ service: string;
8
+ calendar: string;
9
+ account: string;
10
+ token: AuthTokenColumn;
11
+ }
12
+ /** `getServiceForContext` と同じトークンキー */
13
+ export declare function googleTokenKeyForContext(contextName: string, ctx: ContextConfig): string | undefined;
14
+ export declare function evaluateGoogleToken(tokenPath: string): Promise<AuthTokenColumn>;
15
+ export declare function evaluateOutlookToken(auth: AuthManager, creds: OutlookCredentials, ctx: ContextConfig): Promise<AuthTokenColumn>;
16
+ export declare function buildAuthStatusRows(contexts: Record<string, ContextConfig>, deps: {
17
+ auth: AuthManager;
18
+ outlookCreds?: OutlookCredentials;
19
+ }): Promise<AuthStatusRow[]>;
20
+ export declare function formatAuthStatusTable(rows: AuthStatusRow[]): string;
@@ -0,0 +1,82 @@
1
+ import * as fs from "fs/promises";
2
+ import { getGoogleTokenFilePath } from "./config.js";
3
+ /** `getServiceForContext` と同じトークンキー */
4
+ export function googleTokenKeyForContext(contextName, ctx) {
5
+ return contextName || ctx.accountId;
6
+ }
7
+ export async function evaluateGoogleToken(tokenPath) {
8
+ try {
9
+ const raw = await fs.readFile(tokenPath, "utf-8");
10
+ const data = JSON.parse(raw);
11
+ const now = Date.now();
12
+ if (data.expiry_date !== undefined && data.expiry_date > now) {
13
+ return "OK";
14
+ }
15
+ if (data.refresh_token) {
16
+ return "OK";
17
+ }
18
+ if (data.expiry_date !== undefined && data.expiry_date <= now) {
19
+ return "EXPIRED";
20
+ }
21
+ return "NOT LOGGED IN";
22
+ }
23
+ catch {
24
+ return "NOT LOGGED IN";
25
+ }
26
+ }
27
+ export async function evaluateOutlookToken(auth, creds, ctx) {
28
+ const pca = await auth.getOutlookClient(creds.id, creds.tenantId);
29
+ const accounts = await pca.getTokenCache().getAllAccounts();
30
+ if (accounts.length === 0) {
31
+ return "NOT LOGGED IN";
32
+ }
33
+ if (!ctx.accountId) {
34
+ return "OK";
35
+ }
36
+ const match = accounts.find((a) => a.username === ctx.accountId || a.homeAccountId === ctx.accountId);
37
+ return match ? "OK" : "NOT LOGGED IN";
38
+ }
39
+ export function buildAuthStatusRows(contexts, deps) {
40
+ const entries = Object.entries(contexts).sort(([a], [b]) => a.localeCompare(b));
41
+ return Promise.all(entries.map(async ([contextName, ctx]) => {
42
+ const account = ctx.accountId ?? "(default)";
43
+ if (ctx.service === "google") {
44
+ const tokenKey = googleTokenKeyForContext(contextName, ctx);
45
+ const tokenPath = getGoogleTokenFilePath(tokenKey);
46
+ const token = await evaluateGoogleToken(tokenPath);
47
+ return {
48
+ context: contextName,
49
+ service: "google",
50
+ calendar: ctx.calendarId,
51
+ account,
52
+ token,
53
+ };
54
+ }
55
+ if (!deps.outlookCreds) {
56
+ return {
57
+ context: contextName,
58
+ service: "outlook",
59
+ calendar: ctx.calendarId,
60
+ account,
61
+ token: "NOT CONFIGURED",
62
+ };
63
+ }
64
+ const token = await evaluateOutlookToken(deps.auth, deps.outlookCreds, ctx);
65
+ return {
66
+ context: contextName,
67
+ service: "outlook",
68
+ calendar: ctx.calendarId,
69
+ account,
70
+ token,
71
+ };
72
+ }));
73
+ }
74
+ export function formatAuthStatusTable(rows) {
75
+ const headers = ["CONTEXT", "SERVICE", "CALENDAR", "ACCOUNT", "TOKEN"];
76
+ const dataRows = rows.map((r) => [r.context, r.service, r.calendar, r.account, r.token]);
77
+ const all = [headers, ...dataRows.map((cols) => cols.map(String))];
78
+ const widths = headers.map((_, colIdx) => Math.max(...all.map((row) => row[colIdx].length)));
79
+ const sep = widths.map((w) => "-".repeat(w)).join(" ");
80
+ const fmt = (cols) => cols.map((c, i) => c.padEnd(widths[i])).join(" ");
81
+ return [fmt(headers), sep, ...dataRows.map((cols) => fmt(cols.map(String)))].join("\n");
82
+ }
@@ -1,7 +1,15 @@
1
- import { ContextConfig, GoogleCredentials, OutlookCredentials } from "../types/index.js";
1
+ import { AppEventkitConfig, AppUiConfig, ContextConfig, GoogleCredentials, OutlookCredentials } from "../types/index.js";
2
+ export declare function getCalenditConfigDir(): string;
3
+ /** Google OAuth トークンファイルの絶対パス(`auth login` と `getServiceForContext` と同一ルール) */
4
+ export declare function getGoogleTokenFilePath(accountId?: string): string;
2
5
  export declare class ConfigManager {
3
6
  private config;
4
7
  constructor();
8
+ getUi(): AppUiConfig | undefined;
9
+ setUi(ui: AppUiConfig): void;
10
+ /** Replace in-memory config with minimal file (first-run language choice). */
11
+ resetMinimalWithUi(locale: "en" | "ja"): void;
12
+ loadOptional(): Promise<boolean>;
5
13
  load(): Promise<void>;
6
14
  save(): Promise<void>;
7
15
  getContext(name: string): ContextConfig | undefined;
@@ -12,4 +20,6 @@ export declare class ConfigManager {
12
20
  setGoogleCreds(id: string, secret: string): void;
13
21
  getOutlookCreds(): OutlookCredentials | undefined;
14
22
  setOutlookCreds(id: string, tenantId: string): void;
23
+ getEventkitDefaultTransport(): AppEventkitConfig["defaultTransport"] | undefined;
24
+ setEventkitDefaultTransport(value: AppEventkitConfig["defaultTransport"]): void;
15
25
  }
@@ -3,15 +3,28 @@ import * as path from "path";
3
3
  import * as os from "os";
4
4
  import { z } from "zod";
5
5
  import { ConfigError } from "./errors.js";
6
- const CONFIG_DIR = process.env.CALENDIT_CONFIG_DIR || path.join(os.homedir(), ".config", "calendit");
6
+ import { t } from "./i18n.js";
7
+ export function getCalenditConfigDir() {
8
+ return process.env.CALENDIT_CONFIG_DIR || path.join(os.homedir(), ".config", "calendit");
9
+ }
10
+ /** Google OAuth トークンファイルの絶対パス(`auth login` と `getServiceForContext` と同一ルール) */
11
+ export function getGoogleTokenFilePath(accountId) {
12
+ const filename = accountId ? `google_token_${accountId}.json` : "google_token.json";
13
+ return path.join(getCalenditConfigDir(), filename);
14
+ }
15
+ const CONFIG_DIR = getCalenditConfigDir();
7
16
  const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
8
17
  const contextConfigSchema = z.object({
9
- service: z.enum(["google", "outlook"]),
18
+ service: z.enum(["google", "outlook", "macos"]),
10
19
  calendarId: z.string().min(1),
11
20
  accountId: z.string().optional(),
12
21
  fields: z.array(z.string()).optional(),
13
22
  defaultFormat: z.enum(["csv", "md", "json"]).optional(),
14
23
  });
24
+ const uiConfigSchema = z.object({
25
+ locale: z.enum(["en", "ja"]),
26
+ localePromptCompleted: z.boolean().optional(),
27
+ });
15
28
  const fullAppConfigSchema = z.object({
16
29
  contexts: z.record(z.string(), contextConfigSchema),
17
30
  google_creds: z
@@ -26,29 +39,77 @@ const fullAppConfigSchema = z.object({
26
39
  tenantId: z.string().min(1).default("common"),
27
40
  })
28
41
  .optional(),
42
+ ui: uiConfigSchema.optional(),
43
+ eventkit: z
44
+ .object({
45
+ defaultTransport: z.enum(["auto", "bridge", "helper"]),
46
+ })
47
+ .optional(),
29
48
  });
49
+ function defaultUi() {
50
+ return { locale: "en", localePromptCompleted: true };
51
+ }
52
+ function ensureUiMerged(config) {
53
+ if (!config.ui) {
54
+ config.ui = defaultUi();
55
+ }
56
+ }
30
57
  export class ConfigManager {
31
- config = { contexts: {} };
58
+ config = { contexts: {}, ui: defaultUi() };
32
59
  constructor() { }
60
+ getUi() {
61
+ return this.config.ui;
62
+ }
63
+ setUi(ui) {
64
+ this.config.ui = ui;
65
+ }
66
+ /** Replace in-memory config with minimal file (first-run language choice). */
67
+ resetMinimalWithUi(locale) {
68
+ this.config = {
69
+ contexts: {},
70
+ ui: { locale, localePromptCompleted: true },
71
+ };
72
+ }
73
+ async loadOptional() {
74
+ await fs.mkdir(CONFIG_DIR, { recursive: true });
75
+ try {
76
+ const data = await fs.readFile(CONFIG_FILE, "utf-8");
77
+ const parsed = JSON.parse(data);
78
+ this.config = fullAppConfigSchema.parse(parsed);
79
+ ensureUiMerged(this.config);
80
+ return true;
81
+ }
82
+ catch (e) {
83
+ if (e && (e.code === "ENOENT" || /no such file/i.test(String(e.message)))) {
84
+ return false;
85
+ }
86
+ if (e instanceof z.ZodError) {
87
+ throw new ConfigError(t("errors.config.invalidFormat"), t("errors.config.invalidFormatDetails", { details: e.issues.map((issue) => issue.message).join(", ") }), "invalid_schema");
88
+ }
89
+ throw new ConfigError(t("errors.config.readFailed"), t("errors.config.readFailedHint"), "read_failed");
90
+ }
91
+ }
33
92
  async load() {
34
93
  await fs.mkdir(CONFIG_DIR, { recursive: true });
35
94
  try {
36
95
  const data = await fs.readFile(CONFIG_FILE, "utf-8");
37
96
  const parsed = JSON.parse(data);
38
97
  this.config = fullAppConfigSchema.parse(parsed);
98
+ ensureUiMerged(this.config);
39
99
  }
40
100
  catch (e) {
41
101
  if (e && (e.code === "ENOENT" || /no such file/i.test(String(e.message)))) {
42
- throw new ConfigError("設定ファイルが見つかりません。", "初回は `calendit config set-google --id <id> --secret <secret>` などでセットアップしてください。");
102
+ throw new ConfigError(t("errors.config.fileNotFound"), t("errors.config.fileNotFoundHint"), "missing_file");
43
103
  }
44
104
  if (e instanceof z.ZodError) {
45
- throw new ConfigError("設定ファイルの形式が不正です。", `config.json を確認してください。詳細: ${e.issues.map((issue) => issue.message).join(", ")}`);
105
+ throw new ConfigError(t("errors.config.invalidFormat"), t("errors.config.invalidFormatDetails", { details: e.issues.map((issue) => issue.message).join(", ") }), "invalid_schema");
46
106
  }
47
- throw new ConfigError("設定ファイルの読み込みに失敗しました。", "config.json のJSON形式を確認してください。");
107
+ throw new ConfigError(t("errors.config.readFailed"), t("errors.config.readFailedHint"), "read_failed");
48
108
  }
49
109
  }
50
110
  async save() {
51
111
  await fs.mkdir(CONFIG_DIR, { recursive: true });
112
+ ensureUiMerged(this.config);
52
113
  await fs.writeFile(CONFIG_FILE, JSON.stringify(this.config, null, 2), "utf-8");
53
114
  }
54
115
  getContext(name) {
@@ -78,4 +139,10 @@ export class ConfigManager {
78
139
  setOutlookCreds(id, tenantId) {
79
140
  this.config.outlook_creds = { id, tenantId };
80
141
  }
142
+ getEventkitDefaultTransport() {
143
+ return this.config.eventkit?.defaultTransport;
144
+ }
145
+ setEventkitDefaultTransport(value) {
146
+ this.config.eventkit = { defaultTransport: value };
147
+ }
81
148
  }
@@ -1,6 +1,7 @@
1
1
  import { formatInTimeZone } from "date-fns-tz";
2
2
  import { ValidationError } from "./errors.js";
3
3
  import { logger } from "./logger.js";
4
+ import { t } from "./i18n.js";
4
5
  /**
5
6
  * サポート形式:
6
7
  * - today
@@ -51,10 +52,10 @@ export function parseDateTime(input, defaultOffset = 0) {
51
52
  date = new Date(raw);
52
53
  }
53
54
  else {
54
- throw new ValidationError(`日時フォーマットが不正です: "${input}"`, "例: `today 10:00`, `tomorrow`, `2026-04-16`, `2026-04-16 10:00`, `2026-04-16T10:00:00+09:00`");
55
+ throw new ValidationError(t("errors.datetime.invalidFormat", { input: input ?? "" }), "e.g. `today 10:00`, `tomorrow`, `2026-04-16`, `2026-04-16 10:00`, `2026-04-16T10:00:00+09:00`");
55
56
  }
56
57
  if (Number.isNaN(date.getTime())) {
57
- throw new ValidationError(`日時として解釈できません: "${input}"`, "入力値を見直してください。時間は HH:mm 形式で指定してください。");
58
+ throw new ValidationError(t("errors.datetime.invalidParse", { input: input ?? "" }), t("errors.datetime.invalidParseHint"));
58
59
  }
59
60
  logger.debug("parseDateTime", { input, parsed: date.toISOString() });
60
61
  return date;
@@ -2,7 +2,10 @@ export declare class CalendarError extends Error {
2
2
  readonly hint?: string;
3
3
  constructor(message: string, hint?: string);
4
4
  }
5
+ export type ConfigErrorCause = "missing_file" | "invalid_schema" | "read_failed";
5
6
  export declare class ConfigError extends CalendarError {
7
+ readonly causeCode?: ConfigErrorCause;
8
+ constructor(message: string, hint?: string, causeCode?: ConfigErrorCause);
6
9
  }
7
10
  export declare class AuthError extends CalendarError {
8
11
  }
@@ -7,6 +7,11 @@ export class CalendarError extends Error {
7
7
  }
8
8
  }
9
9
  export class ConfigError extends CalendarError {
10
+ causeCode;
11
+ constructor(message, hint, causeCode) {
12
+ super(message, hint);
13
+ this.causeCode = causeCode;
14
+ }
10
15
  }
11
16
  export class AuthError extends CalendarError {
12
17
  }
@@ -0,0 +1,26 @@
1
+ /** Same `CALENDIT_CONFIG_DIR` rules as the bridge: fetched sources live under defaultCalenditDataDir. */
2
+ export declare function getFetchedEventkitBridgePath(): string;
3
+ /**
4
+ * Public default URL: GitHub `tar.gz` of the ref (entire repository archive).
5
+ */
6
+ export declare function getDefaultEventkitFetchUrlFromEnv(): {
7
+ url: string;
8
+ ref: string;
9
+ owner: string;
10
+ repo: string;
11
+ };
12
+ /**
13
+ * `HEAD` request: returns `Content-Length` as bytes or null.
14
+ */
15
+ export declare function probeHttpArchiveSizeBytes(url: string): Promise<number | null>;
16
+ export declare function formatMebibytes(bytes: number): string;
17
+ /**
18
+ * Download archive, extract the `native/eventkit-bridge` subtree to `getFetchedEventkitBridgePath()`.
19
+ */
20
+ export declare function materializeEventkitBridgeFromNetwork(opts: {
21
+ url: string;
22
+ force: boolean;
23
+ }): Promise<{
24
+ dest: string;
25
+ writtenBytes: number;
26
+ }>;
@@ -0,0 +1,159 @@
1
+ import { execFile } from "child_process";
2
+ import { tmpdir } from "os";
3
+ import { promisify } from "util";
4
+ import * as fs from "fs";
5
+ import * as path from "path";
6
+ import { defaultCalenditDataDir } from "./eventkitHelper.js";
7
+ import { resolveCalenditPackageRootFromModule } from "./macosBridgeApp.js";
8
+ const execFileAsync = promisify(execFile);
9
+ const fsp = fs.promises;
10
+ /** Same `CALENDIT_CONFIG_DIR` rules as the bridge: fetched sources live under defaultCalenditDataDir. */
11
+ export function getFetchedEventkitBridgePath() {
12
+ return path.join(defaultCalenditDataDir(), "fetched-eventkit-bridge");
13
+ }
14
+ const DEFAULT_REF = "main";
15
+ const ALLOWED_FETCH_HOSTS = new Set([
16
+ "github.com",
17
+ "codeload.github.com",
18
+ "raw.githubusercontent.com",
19
+ "www.github.com",
20
+ ]);
21
+ function isAllowedUrl(u) {
22
+ try {
23
+ const parsed = new URL(u);
24
+ if (parsed.protocol !== "https:") {
25
+ return false;
26
+ }
27
+ return ALLOWED_FETCH_HOSTS.has(parsed.hostname);
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
33
+ /**
34
+ * Resolves `owner/repo` from the installed `calendit` `package.json` (repository.url), with defaults.
35
+ */
36
+ function readDefaultOwnerRepoFromPackageJson() {
37
+ const calenditRoot = resolveCalenditPackageRootFromModule();
38
+ if (calenditRoot) {
39
+ const pkgPath = path.join(calenditRoot, "package.json");
40
+ if (fs.existsSync(pkgPath)) {
41
+ try {
42
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
43
+ const raw = typeof pkg.repository === "string" ? pkg.repository : (pkg.repository?.url ?? "");
44
+ const m = raw.match(/github\.com[:/]([^/]+)\/([^.?#/]+)/i);
45
+ if (m) {
46
+ const repo = m[2].replace(/\.git$/, "");
47
+ return { owner: m[1], repo };
48
+ }
49
+ }
50
+ catch {
51
+ // fall through
52
+ }
53
+ }
54
+ }
55
+ return { owner: "chromatribe", repo: "calendit" };
56
+ }
57
+ function refToArchivePathSegment(ref) {
58
+ const t = ref.trim();
59
+ if (t.startsWith("v") && /v\d+/.test(t)) {
60
+ return `refs/tags/${t}`;
61
+ }
62
+ return `refs/heads/${t || DEFAULT_REF}`;
63
+ }
64
+ /**
65
+ * Public default URL: GitHub `tar.gz` of the ref (entire repository archive).
66
+ */
67
+ export function getDefaultEventkitFetchUrlFromEnv() {
68
+ const { owner, repo } = readDefaultOwnerRepoFromPackageJson();
69
+ const ref = process.env.CALENDIT_EVENTKIT_FETCH_REF?.trim() || DEFAULT_REF;
70
+ const fromEnv = process.env.CALENDIT_EVENTKIT_FETCH_URL?.trim();
71
+ if (fromEnv) {
72
+ if (!isAllowedUrl(fromEnv)) {
73
+ throw new Error(`Invalid or disallowed fetch URL: ${fromEnv} (https on github.com only)`);
74
+ }
75
+ return { url: fromEnv, ref, owner, repo };
76
+ }
77
+ const seg = refToArchivePathSegment(ref);
78
+ const url = `https://github.com/${owner}/${repo}/archive/${seg}.tar.gz`;
79
+ return { url, ref, owner, repo };
80
+ }
81
+ /**
82
+ * `HEAD` request: returns `Content-Length` as bytes or null.
83
+ */
84
+ export async function probeHttpArchiveSizeBytes(url) {
85
+ if (!isAllowedUrl(url)) {
86
+ return null;
87
+ }
88
+ try {
89
+ const res = await fetch(url, { method: "HEAD", redirect: "follow" });
90
+ const cl = res.headers.get("content-length");
91
+ if (cl == null || cl === "") {
92
+ return null;
93
+ }
94
+ const n = parseInt(cl, 10);
95
+ return Number.isFinite(n) && n > 0 ? n : null;
96
+ }
97
+ catch {
98
+ return null;
99
+ }
100
+ }
101
+ export function formatMebibytes(bytes) {
102
+ if (!Number.isFinite(bytes) || bytes < 0) {
103
+ return "?";
104
+ }
105
+ return (bytes / (1024 * 1024)).toFixed(1);
106
+ }
107
+ /**
108
+ * Download archive, extract the `native/eventkit-bridge` subtree to `getFetchedEventkitBridgePath()`.
109
+ */
110
+ export async function materializeEventkitBridgeFromNetwork(opts) {
111
+ if (!isAllowedUrl(opts.url)) {
112
+ throw new Error(`Refusing to fetch: ${opts.url} (https on github.com only)`);
113
+ }
114
+ const dest = getFetchedEventkitBridgePath();
115
+ if (fs.existsSync(path.join(dest, "Package.swift")) && !opts.force) {
116
+ return { dest, writtenBytes: 0 };
117
+ }
118
+ if (opts.force && fs.existsSync(dest)) {
119
+ fs.rmSync(dest, { recursive: true, force: true });
120
+ }
121
+ const tdir = await fsp.mkdtemp(path.join(tmpdir(), "calendit-ek-fetch-"));
122
+ const tarball = path.join(tdir, "archive.tar.gz");
123
+ const extractRoot = path.join(tdir, "extracted");
124
+ try {
125
+ const res = await fetch(opts.url, { redirect: "follow" });
126
+ if (!res.ok) {
127
+ throw new Error(`HTTP ${res.status} ${res.statusText || ""}`.trim());
128
+ }
129
+ const ab = await res.arrayBuffer();
130
+ const buf = Buffer.from(ab);
131
+ fs.writeFileSync(tarball, buf);
132
+ await fsp.mkdir(extractRoot, { recursive: true });
133
+ await execFileAsync("/usr/bin/tar", ["-xzf", tarball, "-C", extractRoot]);
134
+ const top = fs.readdirSync(extractRoot);
135
+ if (top.length < 1) {
136
+ throw new Error("empty archive");
137
+ }
138
+ const one = path.join(extractRoot, top[0]);
139
+ const bridgePkg = path.join(one, "native", "eventkit-bridge", "Package.swift");
140
+ if (!fs.existsSync(bridgePkg)) {
141
+ throw new Error(`no native/eventkit-bridge/Package.swift in archive (root: ${one})`);
142
+ }
143
+ const bridgeDir = path.dirname(bridgePkg);
144
+ await fsp.mkdir(path.dirname(dest), { recursive: true });
145
+ if (fs.existsSync(dest)) {
146
+ fs.rmSync(dest, { recursive: true, force: true });
147
+ }
148
+ await fsp.cp(bridgeDir, dest, { recursive: true });
149
+ return { dest, writtenBytes: buf.byteLength };
150
+ }
151
+ finally {
152
+ try {
153
+ fs.rmSync(tdir, { recursive: true, force: true });
154
+ }
155
+ catch {
156
+ // ignore
157
+ }
158
+ }
159
+ }
@@ -0,0 +1,7 @@
1
+ import type { ConfigManager } from "./config.js";
2
+ /**
3
+ * If the user did not set `CALENDIT_EVENTKIT_BRIDGE` in the environment, apply
4
+ * the persisted `eventkit.defaultTransport` from config (when present).
5
+ * Shell env always wins; call only after `loadOptional()`.
6
+ */
7
+ export declare function applyEventkitConfigToEnvAfterLoad(config: ConfigManager): void;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * If the user did not set `CALENDIT_EVENTKIT_BRIDGE` in the environment, apply
3
+ * the persisted `eventkit.defaultTransport` from config (when present).
4
+ * Shell env always wins; call only after `loadOptional()`.
5
+ */
6
+ export function applyEventkitConfigToEnvAfterLoad(config) {
7
+ const raw = process.env.CALENDIT_EVENTKIT_BRIDGE;
8
+ if (raw !== undefined && String(raw).trim() !== "") {
9
+ return;
10
+ }
11
+ const v = config.getEventkitDefaultTransport();
12
+ if (!v) {
13
+ return;
14
+ }
15
+ if (v === "bridge") {
16
+ process.env.CALENDIT_EVENTKIT_BRIDGE = "1";
17
+ }
18
+ else if (v === "helper") {
19
+ process.env.CALENDIT_EVENTKIT_BRIDGE = "0";
20
+ }
21
+ else {
22
+ delete process.env.CALENDIT_EVENTKIT_BRIDGE;
23
+ }
24
+ }