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.
- package/README.md +81 -62
- package/dist/commands/accounts.d.ts +4 -0
- package/dist/commands/accounts.js +26 -0
- package/dist/commands/add.js +8 -0
- package/dist/commands/apply.js +5 -1
- package/dist/commands/auth.js +11 -2
- package/dist/commands/config.js +50 -16
- package/dist/commands/macos.d.ts +3 -0
- package/dist/commands/macos.js +401 -0
- package/dist/commands/onboard.d.ts +2 -0
- package/dist/commands/onboard.js +79 -0
- package/dist/commands/query.js +2 -2
- package/dist/commands/shared.d.ts +3 -2
- package/dist/commands/shared.js +21 -5
- package/dist/core/accountStatus.d.ts +18 -0
- package/dist/core/accountStatus.js +74 -0
- package/dist/core/auth.js +7 -11
- package/dist/core/authStatus.d.ts +20 -0
- package/dist/core/authStatus.js +82 -0
- package/dist/core/config.d.ts +11 -1
- package/dist/core/config.js +73 -6
- package/dist/core/datetime.js +3 -2
- package/dist/core/errors.d.ts +3 -0
- package/dist/core/errors.js +5 -0
- package/dist/core/eventkitBridgeFetch.d.ts +26 -0
- package/dist/core/eventkitBridgeFetch.js +159 -0
- package/dist/core/eventkitEnvFromConfig.d.ts +7 -0
- package/dist/core/eventkitEnvFromConfig.js +24 -0
- package/dist/core/eventkitHelper.d.ts +50 -0
- package/dist/core/eventkitHelper.js +336 -0
- package/dist/core/formatter.d.ts +41 -0
- package/dist/core/formatter.js +79 -0
- package/dist/core/i18n.d.ts +7 -0
- package/dist/core/i18n.js +52 -0
- package/dist/core/localeBootstrap.d.ts +12 -0
- package/dist/core/localeBootstrap.js +74 -0
- package/dist/core/logger.d.ts +2 -0
- package/dist/core/logger.js +5 -0
- package/dist/core/macosBridgeApp.d.ts +12 -0
- package/dist/core/macosBridgeApp.js +83 -0
- package/dist/core/macosTerminalRelay.d.ts +12 -0
- package/dist/core/macosTerminalRelay.js +62 -0
- package/dist/generated/locale-keys.d.ts +3 -0
- package/dist/generated/locale-keys.js +90 -0
- package/dist/index.js +99 -17
- package/dist/locales/en.json +128 -0
- package/dist/locales/ja.json +128 -0
- package/dist/services/macos.d.ts +14 -0
- package/dist/services/macos.js +115 -0
- package/dist/test_runner.js +11 -2
- package/dist/types/index.d.ts +12 -1
- 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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
+
}
|
package/dist/core/config.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/core/config.js
CHANGED
|
@@ -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
|
-
|
|
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("
|
|
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("
|
|
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("
|
|
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
|
}
|
package/dist/core/datetime.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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;
|
package/dist/core/errors.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/core/errors.js
CHANGED
|
@@ -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
|
+
}
|