calendit 1.0.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.
- package/LICENSE +15 -0
- package/README.md +94 -0
- package/bin/cli.js +13 -0
- package/dist/commands/add.d.ts +3 -0
- package/dist/commands/add.js +51 -0
- package/dist/commands/apply.d.ts +3 -0
- package/dist/commands/apply.js +67 -0
- package/dist/commands/auth.d.ts +3 -0
- package/dist/commands/auth.js +36 -0
- package/dist/commands/cal.d.ts +3 -0
- package/dist/commands/cal.js +53 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.js +88 -0
- package/dist/commands/query.d.ts +3 -0
- package/dist/commands/query.js +64 -0
- package/dist/commands/shared.d.ts +16 -0
- package/dist/commands/shared.js +69 -0
- package/dist/core/applier.d.ts +26 -0
- package/dist/core/applier.js +141 -0
- package/dist/core/auth.d.ts +22 -0
- package/dist/core/auth.js +153 -0
- package/dist/core/config.d.ts +14 -0
- package/dist/core/config.js +75 -0
- package/dist/core/datetime.d.ts +12 -0
- package/dist/core/datetime.js +61 -0
- package/dist/core/errors.d.ts +21 -0
- package/dist/core/errors.js +25 -0
- package/dist/core/formatter.d.ts +27 -0
- package/dist/core/formatter.js +164 -0
- package/dist/core/logger.d.ts +9 -0
- package/dist/core/logger.js +68 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +95 -0
- package/dist/services/base.d.ts +52 -0
- package/dist/services/base.js +24 -0
- package/dist/services/google.d.ts +16 -0
- package/dist/services/google.js +151 -0
- package/dist/services/mock.d.ts +18 -0
- package/dist/services/mock.js +93 -0
- package/dist/services/outlook.d.ts +20 -0
- package/dist/services/outlook.js +163 -0
- package/dist/test_runner.d.ts +1 -0
- package/dist/test_runner.js +195 -0
- package/dist/types/index.d.ts +44 -0
- package/dist/types/index.js +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { ValidationError } from "./errors.js";
|
|
2
|
+
import { logger } from "./logger.js";
|
|
3
|
+
export class Applier {
|
|
4
|
+
service;
|
|
5
|
+
constructor(service) {
|
|
6
|
+
this.service = service;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* カレンダーに予定を適用する
|
|
10
|
+
*/
|
|
11
|
+
async apply(calendarId, inputEvents, timerange, options = {}) {
|
|
12
|
+
// 1. バリデーション
|
|
13
|
+
const errors = [];
|
|
14
|
+
const seenIds = new Set();
|
|
15
|
+
for (let i = 0; i < inputEvents.length; i++) {
|
|
16
|
+
const event = inputEvents[i];
|
|
17
|
+
const label = event.summary || `Event #${i + 1}`;
|
|
18
|
+
if (event.id) {
|
|
19
|
+
if (seenIds.has(event.id)) {
|
|
20
|
+
errors.push(`Duplicate ID found: ${event.id} (${label})`);
|
|
21
|
+
}
|
|
22
|
+
seenIds.add(event.id);
|
|
23
|
+
}
|
|
24
|
+
if (event.start && event.end) {
|
|
25
|
+
if (new Date(event.start) >= new Date(event.end)) {
|
|
26
|
+
errors.push(`Invalid time range: ${label} (${event.start} - ${event.end})`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (errors.length > 0) {
|
|
31
|
+
throw new ValidationError(`入力イベントに不整合があります:\n${errors.map((e) => ` - ${e}`).join("\n")}`, "重複ID・開始終了時刻の逆転を修正してから再実行してください。");
|
|
32
|
+
}
|
|
33
|
+
let range = timerange;
|
|
34
|
+
if (!range && inputEvents.length > 0) {
|
|
35
|
+
// 入力された予定から期間を自動計算
|
|
36
|
+
let minStart = Infinity;
|
|
37
|
+
let maxEnd = -Infinity;
|
|
38
|
+
for (const event of inputEvents) {
|
|
39
|
+
if (event.start) {
|
|
40
|
+
const s = new Date(event.start).getTime();
|
|
41
|
+
if (s < minStart)
|
|
42
|
+
minStart = s;
|
|
43
|
+
}
|
|
44
|
+
if (event.end) {
|
|
45
|
+
const e = new Date(event.end).getTime();
|
|
46
|
+
if (e > maxEnd)
|
|
47
|
+
maxEnd = e;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (minStart !== Infinity && maxEnd !== -Infinity) {
|
|
51
|
+
// 当日の開始・終了までバッファを持たせる
|
|
52
|
+
const start = new Date(minStart);
|
|
53
|
+
start.setHours(0, 0, 0, 0);
|
|
54
|
+
const end = new Date(maxEnd);
|
|
55
|
+
end.setHours(23, 59, 59, 999);
|
|
56
|
+
range = { start, end };
|
|
57
|
+
logger.info(`Auto-detected sync range: ${range.start.toISOString()} to ${range.end.toISOString()}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (!range) {
|
|
61
|
+
if (options.sync) {
|
|
62
|
+
const now = new Date();
|
|
63
|
+
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
64
|
+
const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999);
|
|
65
|
+
range = { start, end };
|
|
66
|
+
logger.info(`Sync range fallback applied: ${start.toISOString()} to ${end.toISOString()}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (!range) {
|
|
70
|
+
// 範囲を特定できない場合は空の実行とする(安全のため全削除は行わない)
|
|
71
|
+
return { created: [], updated: [], deleted: [] };
|
|
72
|
+
}
|
|
73
|
+
// 現在の予定を取得
|
|
74
|
+
const existingEvents = await this.service.listEvents(calendarId, range.start, range.end);
|
|
75
|
+
const existingMap = new Map(existingEvents.map((e) => [e.id, e]));
|
|
76
|
+
const results = {
|
|
77
|
+
created: [],
|
|
78
|
+
updated: [],
|
|
79
|
+
deleted: [],
|
|
80
|
+
};
|
|
81
|
+
// 1. 追加・更新
|
|
82
|
+
for (const input of inputEvents) {
|
|
83
|
+
if (input.id && existingMap.has(input.id)) {
|
|
84
|
+
// 更新
|
|
85
|
+
const existing = existingMap.get(input.id);
|
|
86
|
+
const diffs = [];
|
|
87
|
+
if (input.summary && input.summary !== existing.summary) {
|
|
88
|
+
diffs.push(`Summary: "${existing.summary}" -> "${input.summary}"`);
|
|
89
|
+
}
|
|
90
|
+
// Compare as timestamps to avoid false diffs from timezone format differences
|
|
91
|
+
if (input.start && new Date(input.start).getTime() !== new Date(existing.start).getTime()) {
|
|
92
|
+
diffs.push(`Start: ${existing.start} -> ${input.start}`);
|
|
93
|
+
}
|
|
94
|
+
if (input.end && new Date(input.end).getTime() !== new Date(existing.end).getTime()) {
|
|
95
|
+
diffs.push(`End: ${existing.end} -> ${input.end}`);
|
|
96
|
+
}
|
|
97
|
+
if (diffs.length > 0) {
|
|
98
|
+
if (options.dryRun) {
|
|
99
|
+
logger.info(`[Dry Run] Update: ${input.summary || existing.summary} (${input.id})`);
|
|
100
|
+
diffs.forEach(d => logger.info(` └ ${d}`));
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
await this.service.updateEvent(calendarId, input.id, input);
|
|
104
|
+
}
|
|
105
|
+
results.updated.push({ input, existing, diffs });
|
|
106
|
+
}
|
|
107
|
+
existingMap.delete(input.id); // 処理済みとして削除
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
// 新規作成
|
|
111
|
+
if (options.dryRun) {
|
|
112
|
+
logger.info(`[Dry Run] Create: ${input.summary}`);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
// 型の整合性のために必要な情報を補完
|
|
116
|
+
await this.service.createEvent(calendarId, {
|
|
117
|
+
summary: input.summary || "(No Title)",
|
|
118
|
+
start: input.start,
|
|
119
|
+
end: input.end,
|
|
120
|
+
location: input.location,
|
|
121
|
+
description: input.description,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
results.created.push(input);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// 2. 削除 (Sync モード時)
|
|
128
|
+
if (options.sync) {
|
|
129
|
+
for (const [id, event] of existingMap.entries()) {
|
|
130
|
+
if (options.dryRun) {
|
|
131
|
+
logger.info(`[Dry Run] Delete: ${event.summary} (${id})`);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
await this.service.deleteEvent(calendarId, id);
|
|
135
|
+
}
|
|
136
|
+
results.deleted.push(event);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return results;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { PublicClientApplication } from "@azure/msal-node";
|
|
2
|
+
import { OAuth2Client } from "google-auth-library";
|
|
3
|
+
export declare class AuthManager {
|
|
4
|
+
private msalClient?;
|
|
5
|
+
constructor();
|
|
6
|
+
/**
|
|
7
|
+
* Outlook (Microsoft Graph) 認証の初期化と取得
|
|
8
|
+
*/
|
|
9
|
+
getOutlookClient(clientId: string, tenantId?: string): Promise<PublicClientApplication>;
|
|
10
|
+
/**
|
|
11
|
+
* Google 認証クライアントの取得
|
|
12
|
+
*/
|
|
13
|
+
getGoogleAuth(clientId: string, clientSecret: string, accountId?: string): Promise<OAuth2Client>;
|
|
14
|
+
/**
|
|
15
|
+
* Google の対話型ログインフロー
|
|
16
|
+
*/
|
|
17
|
+
loginGoogle(clientId: string, clientSecret: string, accountId?: string): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Outlook の対話型ログインフロー
|
|
20
|
+
*/
|
|
21
|
+
loginOutlook(clientId: string, tenantId?: string, accountId?: string): Promise<void>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { PublicClientApplication } from "@azure/msal-node";
|
|
2
|
+
import { KeychainPersistence, PersistenceCachePlugin } from "@azure/msal-node-extensions";
|
|
3
|
+
import { google } from "googleapis";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import * as os from "os";
|
|
6
|
+
import * as fs from "fs/promises";
|
|
7
|
+
import * as http from "http";
|
|
8
|
+
import open from "open";
|
|
9
|
+
import { AuthError } from "./errors.js";
|
|
10
|
+
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
|
+
}
|
|
16
|
+
export class AuthManager {
|
|
17
|
+
msalClient;
|
|
18
|
+
constructor() { }
|
|
19
|
+
/**
|
|
20
|
+
* Outlook (Microsoft Graph) 認証の初期化と取得
|
|
21
|
+
*/
|
|
22
|
+
async getOutlookClient(clientId, tenantId = "common") {
|
|
23
|
+
if (!this.msalClient) {
|
|
24
|
+
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
25
|
+
const cachePath = path.join(CONFIG_DIR, "msal_cache.json");
|
|
26
|
+
const persistence = await KeychainPersistence.create(cachePath, "calendit-service", "outlook-account");
|
|
27
|
+
const cachePlugin = new PersistenceCachePlugin(persistence);
|
|
28
|
+
const msalConfig = {
|
|
29
|
+
auth: {
|
|
30
|
+
clientId,
|
|
31
|
+
authority: `https://login.microsoftonline.com/${tenantId}`,
|
|
32
|
+
},
|
|
33
|
+
cache: {
|
|
34
|
+
cachePlugin,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
this.msalClient = new PublicClientApplication(msalConfig);
|
|
38
|
+
}
|
|
39
|
+
return this.msalClient;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Google 認証クライアントの取得
|
|
43
|
+
*/
|
|
44
|
+
async getGoogleAuth(clientId, clientSecret, accountId) {
|
|
45
|
+
const oauth2Client = new google.auth.OAuth2(clientId, clientSecret, "http://localhost:3000");
|
|
46
|
+
const tokenPath = getTokenPath(accountId);
|
|
47
|
+
try {
|
|
48
|
+
const tokenData = await fs.readFile(tokenPath, "utf-8");
|
|
49
|
+
oauth2Client.setCredentials(JSON.parse(tokenData));
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
// トークンがない場合は何もしない
|
|
53
|
+
}
|
|
54
|
+
return oauth2Client;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Google の対話型ログインフロー
|
|
58
|
+
*/
|
|
59
|
+
async loginGoogle(clientId, clientSecret, accountId) {
|
|
60
|
+
const oauth2Client = new google.auth.OAuth2(clientId, clientSecret, "http://localhost:3000");
|
|
61
|
+
const tokenPath = getTokenPath(accountId);
|
|
62
|
+
const authUrl = oauth2Client.generateAuthUrl({
|
|
63
|
+
access_type: "offline",
|
|
64
|
+
scope: ["https://www.googleapis.com/auth/calendar"],
|
|
65
|
+
prompt: "consent",
|
|
66
|
+
});
|
|
67
|
+
logger.info(`Opening browser for Google Authentication (${accountId || "default"})...`);
|
|
68
|
+
logger.info("URL:", authUrl);
|
|
69
|
+
logger.info("ブラウザが開かない場合は上記URLを手動で開いてください。3分以内に認証を完了してください。");
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
let server;
|
|
72
|
+
const timeout = setTimeout(() => {
|
|
73
|
+
server.close();
|
|
74
|
+
reject(new AuthError("Google 認証がタイムアウトしました。", "再度 `calendit auth login google` を実行して、3分以内に認証を完了してください。"));
|
|
75
|
+
}, 180_000);
|
|
76
|
+
server = http.createServer(async (req, res) => {
|
|
77
|
+
try {
|
|
78
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
79
|
+
const code = url.searchParams.get("code");
|
|
80
|
+
if (code) {
|
|
81
|
+
const { tokens } = await oauth2Client.getToken(code);
|
|
82
|
+
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
83
|
+
await fs.writeFile(tokenPath, JSON.stringify(tokens, null, 2));
|
|
84
|
+
res.end("Authentication successful! You can close this tab and return to the terminal.");
|
|
85
|
+
clearTimeout(timeout);
|
|
86
|
+
server.close();
|
|
87
|
+
resolve();
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
res.end("No code found in the redirect.");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
res.end("Authentication failed.");
|
|
95
|
+
clearTimeout(timeout);
|
|
96
|
+
reject(e);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
server.listen(3000, () => {
|
|
100
|
+
open(authUrl);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Outlook の対話型ログインフロー
|
|
106
|
+
*/
|
|
107
|
+
async loginOutlook(clientId, tenantId = "common", accountId) {
|
|
108
|
+
const pca = await this.getOutlookClient(clientId, tenantId);
|
|
109
|
+
const authCodeUrlParameters = {
|
|
110
|
+
scopes: ["https://graph.microsoft.com/Calendars.ReadWrite"],
|
|
111
|
+
redirectUri: "http://localhost:3000",
|
|
112
|
+
};
|
|
113
|
+
const authUrl = await pca.getAuthCodeUrl(authCodeUrlParameters);
|
|
114
|
+
logger.info(`Opening browser for Outlook Authentication (${accountId || "default"})...`);
|
|
115
|
+
logger.info("URL:", authUrl);
|
|
116
|
+
logger.info("ブラウザが開かない場合は上記URLを手動で開いてください。3分以内に認証を完了してください。");
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
let server;
|
|
119
|
+
const timeout = setTimeout(() => {
|
|
120
|
+
server.close();
|
|
121
|
+
reject(new AuthError("Outlook 認証がタイムアウトしました。", "再度 `calendit auth login outlook` を実行して、3分以内に認証を完了してください。"));
|
|
122
|
+
}, 180_000);
|
|
123
|
+
server = http.createServer(async (req, res) => {
|
|
124
|
+
try {
|
|
125
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
126
|
+
const code = url.searchParams.get("code");
|
|
127
|
+
if (code) {
|
|
128
|
+
await pca.acquireTokenByCode({
|
|
129
|
+
code,
|
|
130
|
+
scopes: ["https://graph.microsoft.com/Calendars.ReadWrite"],
|
|
131
|
+
redirectUri: "http://localhost:3000",
|
|
132
|
+
});
|
|
133
|
+
res.end("Authentication successful! You can close this tab and return to the terminal.");
|
|
134
|
+
clearTimeout(timeout);
|
|
135
|
+
server.close();
|
|
136
|
+
resolve();
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
res.end("No code found in the redirect.");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
res.end("Authentication failed.");
|
|
144
|
+
clearTimeout(timeout);
|
|
145
|
+
reject(e);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
server.listen(3000, () => {
|
|
149
|
+
open(authUrl);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ContextConfig, GoogleCredentials, OutlookCredentials } from "../types/index.js";
|
|
2
|
+
export declare class ConfigManager {
|
|
3
|
+
private config;
|
|
4
|
+
constructor();
|
|
5
|
+
load(): Promise<void>;
|
|
6
|
+
save(): Promise<void>;
|
|
7
|
+
getContext(name: string): ContextConfig | undefined;
|
|
8
|
+
setContext(name: string, context: ContextConfig): void;
|
|
9
|
+
getAllContexts(): Record<string, ContextConfig>;
|
|
10
|
+
getGoogleCreds(): GoogleCredentials | undefined;
|
|
11
|
+
setGoogleCreds(id: string, secret: string): void;
|
|
12
|
+
getOutlookCreds(): OutlookCredentials | undefined;
|
|
13
|
+
setOutlookCreds(id: string, tenantId: string): void;
|
|
14
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as fs from "fs/promises";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { ConfigError } from "./errors.js";
|
|
6
|
+
const CONFIG_DIR = process.env.CALENDIT_CONFIG_DIR || path.join(os.homedir(), ".config", "calendit");
|
|
7
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
8
|
+
const contextConfigSchema = z.object({
|
|
9
|
+
service: z.enum(["google", "outlook"]),
|
|
10
|
+
calendarId: z.string().min(1),
|
|
11
|
+
accountId: z.string().optional(),
|
|
12
|
+
fields: z.array(z.string()).optional(),
|
|
13
|
+
defaultFormat: z.enum(["csv", "md", "json"]).optional(),
|
|
14
|
+
});
|
|
15
|
+
const fullAppConfigSchema = z.object({
|
|
16
|
+
contexts: z.record(z.string(), contextConfigSchema),
|
|
17
|
+
google_creds: z
|
|
18
|
+
.object({
|
|
19
|
+
id: z.string().min(1),
|
|
20
|
+
secret: z.string().min(1),
|
|
21
|
+
})
|
|
22
|
+
.optional(),
|
|
23
|
+
outlook_creds: z
|
|
24
|
+
.object({
|
|
25
|
+
id: z.string().min(1),
|
|
26
|
+
tenantId: z.string().min(1).default("common"),
|
|
27
|
+
})
|
|
28
|
+
.optional(),
|
|
29
|
+
});
|
|
30
|
+
export class ConfigManager {
|
|
31
|
+
config = { contexts: {} };
|
|
32
|
+
constructor() { }
|
|
33
|
+
async load() {
|
|
34
|
+
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
35
|
+
try {
|
|
36
|
+
const data = await fs.readFile(CONFIG_FILE, "utf-8");
|
|
37
|
+
const parsed = JSON.parse(data);
|
|
38
|
+
this.config = fullAppConfigSchema.parse(parsed);
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
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>` などでセットアップしてください。");
|
|
43
|
+
}
|
|
44
|
+
if (e instanceof z.ZodError) {
|
|
45
|
+
throw new ConfigError("設定ファイルの形式が不正です。", `config.json を確認してください。詳細: ${e.issues.map((issue) => issue.message).join(", ")}`);
|
|
46
|
+
}
|
|
47
|
+
throw new ConfigError("設定ファイルの読み込みに失敗しました。", "config.json のJSON形式を確認してください。");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async save() {
|
|
51
|
+
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
52
|
+
await fs.writeFile(CONFIG_FILE, JSON.stringify(this.config, null, 2), "utf-8");
|
|
53
|
+
}
|
|
54
|
+
getContext(name) {
|
|
55
|
+
return this.config.contexts[name];
|
|
56
|
+
}
|
|
57
|
+
setContext(name, context) {
|
|
58
|
+
this.config.contexts[name] = context;
|
|
59
|
+
}
|
|
60
|
+
getAllContexts() {
|
|
61
|
+
return this.config.contexts;
|
|
62
|
+
}
|
|
63
|
+
getGoogleCreds() {
|
|
64
|
+
return this.config.google_creds;
|
|
65
|
+
}
|
|
66
|
+
setGoogleCreds(id, secret) {
|
|
67
|
+
this.config.google_creds = { id, secret };
|
|
68
|
+
}
|
|
69
|
+
getOutlookCreds() {
|
|
70
|
+
return this.config.outlook_creds;
|
|
71
|
+
}
|
|
72
|
+
setOutlookCreds(id, tenantId) {
|
|
73
|
+
this.config.outlook_creds = { id, tenantId };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { formatInTimeZone } from "date-fns-tz";
|
|
2
|
+
import { ValidationError } from "./errors.js";
|
|
3
|
+
import { logger } from "./logger.js";
|
|
4
|
+
/**
|
|
5
|
+
* サポート形式:
|
|
6
|
+
* - today
|
|
7
|
+
* - tomorrow
|
|
8
|
+
* - today HH:mm
|
|
9
|
+
* - tomorrow HH:mm
|
|
10
|
+
* - HH:mm
|
|
11
|
+
* - YYYY-MM-DD
|
|
12
|
+
* - YYYY-MM-DD HH:mm
|
|
13
|
+
* - ISO 8601
|
|
14
|
+
*/
|
|
15
|
+
export function parseDateTime(input, defaultOffset = 0) {
|
|
16
|
+
const now = new Date();
|
|
17
|
+
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
18
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
19
|
+
const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
|
|
20
|
+
const todayStr = formatInTimeZone(today, timeZone, "yyyy-MM-dd");
|
|
21
|
+
const tomorrowStr = formatInTimeZone(tomorrow, timeZone, "yyyy-MM-dd");
|
|
22
|
+
let date;
|
|
23
|
+
const raw = input?.trim();
|
|
24
|
+
if (!raw || raw === "today") {
|
|
25
|
+
date = new Date(today.getTime() + defaultOffset * 24 * 60 * 60 * 1000);
|
|
26
|
+
}
|
|
27
|
+
else if (raw === "tomorrow") {
|
|
28
|
+
date = new Date(tomorrow.getTime() + defaultOffset * 24 * 60 * 60 * 1000);
|
|
29
|
+
}
|
|
30
|
+
else if (/^tomorrow\s+\d{1,2}:\d{2}$/.test(raw)) {
|
|
31
|
+
const time = raw.split(/\s+/)[1];
|
|
32
|
+
date = new Date(`${tomorrowStr}T${time}:00`);
|
|
33
|
+
}
|
|
34
|
+
else if (/^today\s+\d{1,2}:\d{2}$/.test(raw)) {
|
|
35
|
+
const time = raw.split(/\s+/)[1];
|
|
36
|
+
date = new Date(`${todayStr}T${time}:00`);
|
|
37
|
+
}
|
|
38
|
+
else if (/^\d{1,2}:\d{2}$/.test(raw)) {
|
|
39
|
+
date = new Date(`${todayStr}T${raw}:00`);
|
|
40
|
+
}
|
|
41
|
+
else if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) {
|
|
42
|
+
const [y, m, d] = raw.split("-").map(Number);
|
|
43
|
+
date = new Date(y, m - 1, d);
|
|
44
|
+
}
|
|
45
|
+
else if (/^\d{4}-\d{2}-\d{2}\s+\d{1,2}:\d{2}$/.test(raw)) {
|
|
46
|
+
// YYYY-MM-DD HH:mm (スペース区切り)
|
|
47
|
+
const [datePart, timePart] = raw.split(/\s+/);
|
|
48
|
+
date = new Date(`${datePart}T${timePart}:00`);
|
|
49
|
+
}
|
|
50
|
+
else if (/^\d{4}-\d{2}-\d{2}T/.test(raw)) {
|
|
51
|
+
date = new Date(raw);
|
|
52
|
+
}
|
|
53
|
+
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
|
+
}
|
|
56
|
+
if (Number.isNaN(date.getTime())) {
|
|
57
|
+
throw new ValidationError(`日時として解釈できません: "${input}"`, "入力値を見直してください。時間は HH:mm 形式で指定してください。");
|
|
58
|
+
}
|
|
59
|
+
logger.debug("parseDateTime", { input, parsed: date.toISOString() });
|
|
60
|
+
return date;
|
|
61
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare class CalendarError extends Error {
|
|
2
|
+
readonly hint?: string;
|
|
3
|
+
constructor(message: string, hint?: string);
|
|
4
|
+
}
|
|
5
|
+
export declare class ConfigError extends CalendarError {
|
|
6
|
+
}
|
|
7
|
+
export declare class AuthError extends CalendarError {
|
|
8
|
+
}
|
|
9
|
+
export declare class ValidationError extends CalendarError {
|
|
10
|
+
}
|
|
11
|
+
export declare class ApiError extends CalendarError {
|
|
12
|
+
readonly statusCode?: number;
|
|
13
|
+
readonly provider?: string;
|
|
14
|
+
readonly details?: unknown;
|
|
15
|
+
constructor(message: string, options?: {
|
|
16
|
+
statusCode?: number;
|
|
17
|
+
provider?: string;
|
|
18
|
+
details?: unknown;
|
|
19
|
+
hint?: string;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export class CalendarError extends Error {
|
|
2
|
+
hint;
|
|
3
|
+
constructor(message, hint) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = new.target.name;
|
|
6
|
+
this.hint = hint;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class ConfigError extends CalendarError {
|
|
10
|
+
}
|
|
11
|
+
export class AuthError extends CalendarError {
|
|
12
|
+
}
|
|
13
|
+
export class ValidationError extends CalendarError {
|
|
14
|
+
}
|
|
15
|
+
export class ApiError extends CalendarError {
|
|
16
|
+
statusCode;
|
|
17
|
+
provider;
|
|
18
|
+
details;
|
|
19
|
+
constructor(message, options = {}) {
|
|
20
|
+
super(message, options.hint);
|
|
21
|
+
this.statusCode = options.statusCode;
|
|
22
|
+
this.provider = options.provider;
|
|
23
|
+
this.details = options.details;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { CalendarEvent } from "../types/index.js";
|
|
2
|
+
export interface ParseResult {
|
|
3
|
+
events: Partial<CalendarEvent>[];
|
|
4
|
+
warnings: string[];
|
|
5
|
+
}
|
|
6
|
+
export declare class Formatter {
|
|
7
|
+
/**
|
|
8
|
+
* 予定の配列を CSV 文字列に変換
|
|
9
|
+
*/
|
|
10
|
+
static toCsv(events: CalendarEvent[]): string;
|
|
11
|
+
/**
|
|
12
|
+
* CSV 文字列を予定の配列に変換
|
|
13
|
+
*/
|
|
14
|
+
static fromCsv(csv: string): CalendarEvent[];
|
|
15
|
+
/**
|
|
16
|
+
* 予定の配列を Markdown 文字列に変換
|
|
17
|
+
*/
|
|
18
|
+
static toMarkdown(events: CalendarEvent[]): string;
|
|
19
|
+
/**
|
|
20
|
+
* Markdown 文字列から予定を抽出
|
|
21
|
+
* 形式: - [ ] **Title** (HH:mm - HH:mm) (ID: id)
|
|
22
|
+
* インデントされた行は説明文として扱う
|
|
23
|
+
*/
|
|
24
|
+
static fromMarkdown(md: string, strict?: boolean): ParseResult;
|
|
25
|
+
private static groupByDate;
|
|
26
|
+
private static formatTime;
|
|
27
|
+
}
|