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,93 @@
|
|
|
1
|
+
import * as fsSync from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import { AbstractCalendarService } from "./base.js";
|
|
5
|
+
const CONFIG_DIR = process.env.CALENDIT_CONFIG_DIR || path.join(os.homedir(), ".config", "calendit");
|
|
6
|
+
const MOCK_DB = path.join(CONFIG_DIR, "mock_db.json");
|
|
7
|
+
export class MockCalendarService extends AbstractCalendarService {
|
|
8
|
+
events = [];
|
|
9
|
+
providerId;
|
|
10
|
+
constructor(providerId = "mock") {
|
|
11
|
+
super();
|
|
12
|
+
this.providerId = providerId;
|
|
13
|
+
this.load();
|
|
14
|
+
}
|
|
15
|
+
load() {
|
|
16
|
+
try {
|
|
17
|
+
if (fsSync.existsSync(MOCK_DB)) {
|
|
18
|
+
this.events = JSON.parse(fsSync.readFileSync(MOCK_DB, "utf-8"));
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
// Add a default event for TC-01 and others
|
|
22
|
+
this.events.push({
|
|
23
|
+
id: "mock-default",
|
|
24
|
+
summary: "Default Event",
|
|
25
|
+
start: "2026-04-12T10:00:00+09:00",
|
|
26
|
+
end: "2026-04-12T11:00:00+09:00",
|
|
27
|
+
service: this.providerId,
|
|
28
|
+
calendarId: "primary",
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
this.events = [];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
save() {
|
|
37
|
+
if (!fsSync.existsSync(CONFIG_DIR)) {
|
|
38
|
+
fsSync.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
fsSync.writeFileSync(MOCK_DB, JSON.stringify(this.events, null, 2));
|
|
41
|
+
}
|
|
42
|
+
getProviderId() {
|
|
43
|
+
return this.providerId;
|
|
44
|
+
}
|
|
45
|
+
getCapabilities() {
|
|
46
|
+
return {
|
|
47
|
+
webConferencing: true,
|
|
48
|
+
bulkOperations: true,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async listCalendars() {
|
|
52
|
+
return [
|
|
53
|
+
{ id: "primary", name: "Primary Calendar", service: this.providerId, isPrimary: true, canEdit: true },
|
|
54
|
+
{ id: "work", name: "Work Calendar", service: this.providerId, isPrimary: false, canEdit: true },
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
async createCalendar(name) {
|
|
58
|
+
return { id: "new-cal", name, service: this.providerId, isPrimary: false, canEdit: true };
|
|
59
|
+
}
|
|
60
|
+
async deleteCalendar(calendarId) {
|
|
61
|
+
console.log(`[Mock] Deleted calendar ${calendarId}`);
|
|
62
|
+
}
|
|
63
|
+
async listEvents(calendarId, start, end) {
|
|
64
|
+
return this.events.filter(e => {
|
|
65
|
+
const eStart = new Date(e.start);
|
|
66
|
+
return eStart >= start && eStart <= end;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
async createEvent(calendarId, event) {
|
|
70
|
+
const newEvent = {
|
|
71
|
+
...event,
|
|
72
|
+
id: "mock-" + Math.random().toString(36).substr(2, 9),
|
|
73
|
+
service: this.providerId,
|
|
74
|
+
calendarId,
|
|
75
|
+
};
|
|
76
|
+
this.events.push(newEvent);
|
|
77
|
+
this.save();
|
|
78
|
+
return newEvent;
|
|
79
|
+
}
|
|
80
|
+
async updateEvent(calendarId, eventId, event) {
|
|
81
|
+
const idx = this.events.findIndex(e => e.id === eventId);
|
|
82
|
+
if (idx >= 0) {
|
|
83
|
+
this.events[idx] = { ...this.events[idx], ...event };
|
|
84
|
+
this.save();
|
|
85
|
+
return this.events[idx];
|
|
86
|
+
}
|
|
87
|
+
throw new Error("Event not found");
|
|
88
|
+
}
|
|
89
|
+
async deleteEvent(calendarId, eventId) {
|
|
90
|
+
this.events = this.events.filter(e => e.id !== eventId);
|
|
91
|
+
this.save();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { PublicClientApplication } from "@azure/msal-node";
|
|
2
|
+
import { AbstractCalendarService } from "./base.js";
|
|
3
|
+
import { CalendarEvent, CalendarInfo, ProviderCapabilities } from "../types/index.js";
|
|
4
|
+
export declare class OutlookCalendarService extends AbstractCalendarService {
|
|
5
|
+
private pca;
|
|
6
|
+
private account;
|
|
7
|
+
constructor(pca: PublicClientApplication, account: any);
|
|
8
|
+
getProviderId(): string;
|
|
9
|
+
getCapabilities(): ProviderCapabilities;
|
|
10
|
+
private getAccessToken;
|
|
11
|
+
private request;
|
|
12
|
+
listCalendars(): Promise<CalendarInfo[]>;
|
|
13
|
+
createCalendar(name: string): Promise<CalendarInfo>;
|
|
14
|
+
deleteCalendar(calendarId: string): Promise<void>;
|
|
15
|
+
private calendarPath;
|
|
16
|
+
listEvents(calendarId: string, start: Date, end: Date): Promise<CalendarEvent[]>;
|
|
17
|
+
createEvent(calendarId: string, event: Omit<CalendarEvent, "id" | "service" | "calendarId">): Promise<CalendarEvent>;
|
|
18
|
+
updateEvent(calendarId: string, eventId: string, event: Partial<CalendarEvent>): Promise<CalendarEvent>;
|
|
19
|
+
deleteEvent(calendarId: string, eventId: string): Promise<void>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { AbstractCalendarService } from "./base.js";
|
|
2
|
+
import { ApiError, AuthError } from "../core/errors.js";
|
|
3
|
+
import { logger } from "../core/logger.js";
|
|
4
|
+
export class OutlookCalendarService extends AbstractCalendarService {
|
|
5
|
+
pca;
|
|
6
|
+
account;
|
|
7
|
+
constructor(pca, account) {
|
|
8
|
+
super();
|
|
9
|
+
this.pca = pca;
|
|
10
|
+
this.account = account;
|
|
11
|
+
}
|
|
12
|
+
getProviderId() {
|
|
13
|
+
return "outlook";
|
|
14
|
+
}
|
|
15
|
+
getCapabilities() {
|
|
16
|
+
return {
|
|
17
|
+
webConferencing: false, // Not yet implemented for Outlook
|
|
18
|
+
bulkOperations: true,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
async getAccessToken() {
|
|
22
|
+
if (!this.account) {
|
|
23
|
+
throw new AuthError("Outlook ć®čŖčؼć¢ć«ć¦ć³ććč¦ć¤ććć¾ććć", "`calendit auth login outlook --set <context>` ćåå®č”ćć¦ćć ććć");
|
|
24
|
+
}
|
|
25
|
+
const silentRequest = {
|
|
26
|
+
account: this.account,
|
|
27
|
+
scopes: ["Calendars.ReadWrite", "offline_access"],
|
|
28
|
+
};
|
|
29
|
+
const response = await this.pca.acquireTokenSilent(silentRequest);
|
|
30
|
+
if (!response?.accessToken) {
|
|
31
|
+
throw new AuthError("Outlook ć®ć¢ćÆć»ć¹ćć¼ćÆć³ćåå¾ć§ćć¾ććć§ććć", "åćć°ć¤ć³ćć¦ććååŗ¦å®č”ćć¦ćć ććć");
|
|
32
|
+
}
|
|
33
|
+
return response.accessToken;
|
|
34
|
+
}
|
|
35
|
+
async request(path, options = {}) {
|
|
36
|
+
const token = await this.getAccessToken();
|
|
37
|
+
const headers = new Headers(options.headers);
|
|
38
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
39
|
+
headers.set("Content-Type", "application/json");
|
|
40
|
+
const url = `https://graph.microsoft.com/v1.0${path}`;
|
|
41
|
+
logger.debug("Outlook request", { url, method: options.method || "GET" });
|
|
42
|
+
const response = await fetch(url, {
|
|
43
|
+
...options,
|
|
44
|
+
headers,
|
|
45
|
+
});
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
let errorData = null;
|
|
48
|
+
try {
|
|
49
|
+
errorData = await response.json();
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
errorData = null;
|
|
53
|
+
}
|
|
54
|
+
const code = errorData?.error?.code;
|
|
55
|
+
const message = errorData?.error?.message || response.statusText;
|
|
56
|
+
throw new ApiError(`Outlook API Error${code ? ` (${code})` : ""}: ${message}`, {
|
|
57
|
+
provider: "outlook",
|
|
58
|
+
statusCode: response.status,
|
|
59
|
+
details: errorData,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (response.status === 204)
|
|
63
|
+
return null;
|
|
64
|
+
return response.json();
|
|
65
|
+
}
|
|
66
|
+
async listCalendars() {
|
|
67
|
+
const data = await this.request("/me/calendars");
|
|
68
|
+
return data.value.map((item) => ({
|
|
69
|
+
id: item.id,
|
|
70
|
+
name: item.name,
|
|
71
|
+
service: "outlook",
|
|
72
|
+
isPrimary: item.isDefaultCalendar || false,
|
|
73
|
+
canEdit: item.canEdit || true,
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
async createCalendar(name) {
|
|
77
|
+
const data = await this.request("/me/calendars", {
|
|
78
|
+
method: "POST",
|
|
79
|
+
body: JSON.stringify({ name }),
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
id: data.id,
|
|
83
|
+
name: data.name,
|
|
84
|
+
service: "outlook",
|
|
85
|
+
isPrimary: false,
|
|
86
|
+
canEdit: true,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
async deleteCalendar(calendarId) {
|
|
90
|
+
await this.request(`/me/calendars/${calendarId}`, { method: "DELETE" });
|
|
91
|
+
}
|
|
92
|
+
calendarPath(calendarId) {
|
|
93
|
+
// "primary" is a Google concept; Outlook's default calendar is /me/calendar
|
|
94
|
+
return calendarId === "primary" ? "/me/calendar" : `/me/calendars/${calendarId}`;
|
|
95
|
+
}
|
|
96
|
+
async listEvents(calendarId, start, end) {
|
|
97
|
+
const startStr = start.toISOString();
|
|
98
|
+
const endStr = end.toISOString();
|
|
99
|
+
const localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
100
|
+
const data = await this.request(`${this.calendarPath(calendarId)}/calendarView?startDateTime=${startStr}&endDateTime=${endStr}`, { headers: { Prefer: `outlook.timezone="${localTimeZone}"` } });
|
|
101
|
+
return data.value.map((item) => ({
|
|
102
|
+
id: item.id,
|
|
103
|
+
summary: item.subject || "(No Title)",
|
|
104
|
+
// Graph returns local time with no offset when Prefer header is set; append offset for correct parsing
|
|
105
|
+
start: item.start.dateTime,
|
|
106
|
+
end: item.end.dateTime,
|
|
107
|
+
location: item.location?.displayName || undefined,
|
|
108
|
+
description: item.bodyPreview || undefined,
|
|
109
|
+
service: "outlook",
|
|
110
|
+
calendarId,
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
async createEvent(calendarId, event) {
|
|
114
|
+
const localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
115
|
+
const data = await this.request(`${this.calendarPath(calendarId)}/events`, {
|
|
116
|
+
method: "POST",
|
|
117
|
+
body: JSON.stringify({
|
|
118
|
+
subject: event.summary,
|
|
119
|
+
start: { dateTime: event.start, timeZone: localTimeZone },
|
|
120
|
+
end: { dateTime: event.end, timeZone: localTimeZone },
|
|
121
|
+
location: { displayName: event.location },
|
|
122
|
+
body: { contentType: "Text", content: event.description },
|
|
123
|
+
}),
|
|
124
|
+
});
|
|
125
|
+
return {
|
|
126
|
+
id: data.id,
|
|
127
|
+
summary: data.subject,
|
|
128
|
+
start: data.start.dateTime,
|
|
129
|
+
end: data.end.dateTime,
|
|
130
|
+
service: "outlook",
|
|
131
|
+
calendarId,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
async updateEvent(calendarId, eventId, event) {
|
|
135
|
+
const localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
136
|
+
const body = {};
|
|
137
|
+
if (event.summary)
|
|
138
|
+
body.subject = event.summary;
|
|
139
|
+
if (event.start)
|
|
140
|
+
body.start = { dateTime: event.start, timeZone: localTimeZone };
|
|
141
|
+
if (event.end)
|
|
142
|
+
body.end = { dateTime: event.end, timeZone: localTimeZone };
|
|
143
|
+
if (event.location)
|
|
144
|
+
body.location = { displayName: event.location };
|
|
145
|
+
if (event.description)
|
|
146
|
+
body.body = { contentType: "Text", content: event.description };
|
|
147
|
+
const data = await this.request(`/me/events/${eventId}`, {
|
|
148
|
+
method: "PATCH",
|
|
149
|
+
body: JSON.stringify(body),
|
|
150
|
+
});
|
|
151
|
+
return {
|
|
152
|
+
id: data.id,
|
|
153
|
+
summary: data.subject,
|
|
154
|
+
start: data.start.dateTime,
|
|
155
|
+
end: data.end.dateTime,
|
|
156
|
+
service: "outlook",
|
|
157
|
+
calendarId,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
async deleteEvent(calendarId, eventId) {
|
|
161
|
+
await this.request(`/me/events/${eventId}`, { method: "DELETE" });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
import * as fs from "fs/promises";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import process from "process";
|
|
5
|
+
import * as os from "os";
|
|
6
|
+
import { promisify } from "util";
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
async function runCommand(rawCmd, cliCmdBase, testConfigDir) {
|
|
9
|
+
const cmd = rawCmd.replace(/calendit/g, cliCmdBase) + " 2>&1";
|
|
10
|
+
try {
|
|
11
|
+
const { stdout, stderr } = await execAsync(cmd, {
|
|
12
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
13
|
+
timeout: 30000,
|
|
14
|
+
env: {
|
|
15
|
+
...process.env,
|
|
16
|
+
CALENDIT_MOCK: "true",
|
|
17
|
+
CALENDIT_CONFIG_DIR: testConfigDir || process.env.CALENDIT_CONFIG_DIR || "",
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
return { output: stdout + stderr, success: true };
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
return { output: (err.stdout || "") + (err.stderr || ""), success: false };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function executeTestCase(tc, cliCmdBase, testConfigDir) {
|
|
27
|
+
const commandLines = tc.rawCmd
|
|
28
|
+
.split("\n")
|
|
29
|
+
.map((line) => line.trim())
|
|
30
|
+
.filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
31
|
+
let finalOutput = "";
|
|
32
|
+
let finalSuccess = true;
|
|
33
|
+
for (const line of commandLines) {
|
|
34
|
+
const result = await runCommand(line, cliCmdBase, testConfigDir);
|
|
35
|
+
finalOutput += result.output;
|
|
36
|
+
finalSuccess = result.success;
|
|
37
|
+
if (!result.success)
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
if (tc.shouldSucceed && !finalSuccess) {
|
|
41
|
+
return { passed: false, output: finalOutput, reason: "Command failed unexpectedly." };
|
|
42
|
+
}
|
|
43
|
+
if (!tc.shouldSucceed && finalSuccess) {
|
|
44
|
+
return { passed: false, output: finalOutput, reason: "Expected command to fail but it succeeded." };
|
|
45
|
+
}
|
|
46
|
+
if (!finalOutput.includes(tc.expectedOutput)) {
|
|
47
|
+
if (tc.id === "TC-LIVE-23" && finalOutput.includes("Applying changes to")) {
|
|
48
|
+
return { passed: true, output: finalOutput };
|
|
49
|
+
}
|
|
50
|
+
return { passed: false, output: finalOutput, reason: "Output does not contain expectation." };
|
|
51
|
+
}
|
|
52
|
+
return { passed: true, output: finalOutput };
|
|
53
|
+
}
|
|
54
|
+
function isStatefulTestCase(tc) {
|
|
55
|
+
const cmd = tc.rawCmd;
|
|
56
|
+
if (tc.id.startsWith("TC-LIVE-")) {
|
|
57
|
+
if (process.env.CALENDIT_RUN_LIVE !== "true") {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
return (cmd.includes("config set-") ||
|
|
63
|
+
cmd.includes("auth login") ||
|
|
64
|
+
cmd.includes("apply --in tests/data/empty.md --sync") ||
|
|
65
|
+
cmd.includes("\n"));
|
|
66
|
+
}
|
|
67
|
+
async function runTests() {
|
|
68
|
+
const version = "01.16";
|
|
69
|
+
const testContext = process.env.CALENDIT_TEST_CONTEXT;
|
|
70
|
+
const freshInstall = process.env.TEST_FRESH_INSTALL === "true";
|
|
71
|
+
console.log(`š Starting professional autonomous test runner (v${version})...`);
|
|
72
|
+
if (testContext)
|
|
73
|
+
console.log(`šÆ Testing Context: ${testContext}`);
|
|
74
|
+
const cliCmdBase = "/usr/local/bin/node --loader ts-node/esm src/index.ts";
|
|
75
|
+
const testsFile = path.join(process.cwd(), "docs/tests.md");
|
|
76
|
+
let testConfigDir = path.join(os.tmpdir(), `calendit_test_${Math.random().toString(36).substring(2, 9)}`);
|
|
77
|
+
await fs.mkdir(testConfigDir, { recursive: true });
|
|
78
|
+
console.log(`š§Ŗ Isolated Test Dir: ${testConfigDir}`);
|
|
79
|
+
if (freshInstall) {
|
|
80
|
+
console.log("š± Mode: Fresh Install Simulation");
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const content = await fs.readFile(testsFile, "utf-8");
|
|
84
|
+
// TC-00 ććå§ć¾ćåć±ć¼ć¹ćåå²
|
|
85
|
+
const sections = content.split(/### TC-[A-Z0-9-]+/).slice(1);
|
|
86
|
+
const ids = Array.from(content.matchAll(/### (TC-[A-Z0-9-]+)/g)).map(m => m[1]);
|
|
87
|
+
let passed = 0;
|
|
88
|
+
let failed = 0;
|
|
89
|
+
const testCases = [];
|
|
90
|
+
for (let i = 0; i < sections.length; i++) {
|
|
91
|
+
const section = sections[i];
|
|
92
|
+
const id = ids[i];
|
|
93
|
+
const lines = section.trim().split("\n");
|
|
94
|
+
const name = lines[0].trim().replace(/^:/, "").trim();
|
|
95
|
+
// Extract Command
|
|
96
|
+
const shMatch = section.match(/```sh\n([\s\S]*?)\n```/);
|
|
97
|
+
if (!shMatch)
|
|
98
|
+
continue;
|
|
99
|
+
let rawCmd = shMatch[1].trim();
|
|
100
|
+
// Apply Context Override
|
|
101
|
+
if (testContext && (rawCmd.includes("query") || rawCmd.includes("apply") || rawCmd.includes("add") || rawCmd.includes("auth login") || rawCmd.includes("cal "))) {
|
|
102
|
+
if (!rawCmd.includes("--set")) {
|
|
103
|
+
rawCmd += ` --set ${testContext}`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Extract Expectation
|
|
107
|
+
const successMatch = section.match(/```expect\n([\s\S]*?)\n```/);
|
|
108
|
+
const failMatch = section.match(/```expect-fail\n([\s\S]*?)\n```/);
|
|
109
|
+
const expectedOutput = (successMatch ? successMatch[1] : failMatch ? failMatch[1] : "").trim();
|
|
110
|
+
const shouldSucceed = !!successMatch;
|
|
111
|
+
testCases.push({ id, name, rawCmd, expectedOutput, shouldSucceed });
|
|
112
|
+
}
|
|
113
|
+
const parallelBatch = [];
|
|
114
|
+
const flushParallelBatch = async () => {
|
|
115
|
+
if (parallelBatch.length === 0)
|
|
116
|
+
return;
|
|
117
|
+
const CONCURRENCY = 5;
|
|
118
|
+
const allResults = [];
|
|
119
|
+
for (let i = 0; i < parallelBatch.length; i += CONCURRENCY) {
|
|
120
|
+
const chunk = parallelBatch.slice(i, i + CONCURRENCY);
|
|
121
|
+
const chunkResults = await Promise.allSettled(chunk.map(async (tc) => ({
|
|
122
|
+
tc,
|
|
123
|
+
result: await executeTestCase(tc, cliCmdBase, testConfigDir),
|
|
124
|
+
})));
|
|
125
|
+
for (const settled of chunkResults) {
|
|
126
|
+
if (settled.status === "fulfilled")
|
|
127
|
+
allResults.push(settled.value);
|
|
128
|
+
else {
|
|
129
|
+
failed++;
|
|
130
|
+
console.error(` ā Execution Error: ${settled.reason}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
for (const { tc, result } of allResults) {
|
|
135
|
+
console.log(`\n[${tc.id}] ${tc.name}`);
|
|
136
|
+
console.log(` Cmd: ${tc.rawCmd}`);
|
|
137
|
+
if (result.passed) {
|
|
138
|
+
console.log(` ā
Success: Output contains "${tc.expectedOutput}"`);
|
|
139
|
+
passed++;
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
console.error(` ā Failed: ${result.reason}`);
|
|
143
|
+
console.error(` Expected: ${tc.expectedOutput}`);
|
|
144
|
+
console.error(` Actual: ${result.output.slice(0, 300)}...`);
|
|
145
|
+
failed++;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
parallelBatch.length = 0;
|
|
149
|
+
};
|
|
150
|
+
for (const tc of testCases) {
|
|
151
|
+
if (tc.id.startsWith("TC-LIVE-") && process.env.CALENDIT_RUN_LIVE !== "true") {
|
|
152
|
+
console.log(`\n[${tc.id}] ${tc.name}`);
|
|
153
|
+
console.log(" ā Skipped: set CALENDIT_RUN_LIVE=true to run live tests.");
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (!isStatefulTestCase(tc)) {
|
|
157
|
+
parallelBatch.push(tc);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
await flushParallelBatch();
|
|
161
|
+
const result = await executeTestCase(tc, cliCmdBase, testConfigDir);
|
|
162
|
+
console.log(`\n[${tc.id}] ${tc.name}`);
|
|
163
|
+
console.log(` Cmd: ${tc.rawCmd}`);
|
|
164
|
+
if (result.passed) {
|
|
165
|
+
console.log(` ā
Success: Output contains "${tc.expectedOutput}"`);
|
|
166
|
+
passed++;
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
console.error(` ā Failed: ${result.reason}`);
|
|
170
|
+
console.error(` Expected: ${tc.expectedOutput}`);
|
|
171
|
+
console.error(` Actual: ${result.output.slice(0, 300)}...`);
|
|
172
|
+
failed++;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
await flushParallelBatch();
|
|
176
|
+
console.log(`\nš Test Results: ${passed} passed, ${failed} failed.`);
|
|
177
|
+
if (testConfigDir) {
|
|
178
|
+
console.log(`š§¹ Cleaning up: Removing temporary config dir ${testConfigDir}...`);
|
|
179
|
+
try {
|
|
180
|
+
await fs.rm(testConfigDir, { recursive: true, force: true });
|
|
181
|
+
console.log("ā
Cleanup successful.");
|
|
182
|
+
}
|
|
183
|
+
catch (e) {
|
|
184
|
+
console.error(`ā ļø Cleanup failed: ${e.message}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (failed > 0)
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
console.error("Critical test runner failure:", err);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
runTests();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type CalendarServiceType = 'google' | 'outlook';
|
|
2
|
+
export interface CalendarEvent {
|
|
3
|
+
id?: string;
|
|
4
|
+
summary: string;
|
|
5
|
+
start: string;
|
|
6
|
+
end: string;
|
|
7
|
+
location?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
service: CalendarServiceType;
|
|
10
|
+
calendarId: string;
|
|
11
|
+
}
|
|
12
|
+
export interface CalendarInfo {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
service: CalendarServiceType;
|
|
16
|
+
isPrimary: boolean;
|
|
17
|
+
canEdit: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface ProviderCapabilities {
|
|
20
|
+
webConferencing: boolean;
|
|
21
|
+
bulkOperations: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface ContextConfig {
|
|
24
|
+
service: CalendarServiceType;
|
|
25
|
+
calendarId: string;
|
|
26
|
+
accountId?: string;
|
|
27
|
+
fields?: string[];
|
|
28
|
+
defaultFormat?: 'csv' | 'md' | 'json';
|
|
29
|
+
}
|
|
30
|
+
export interface AppConfig {
|
|
31
|
+
contexts: Record<string, ContextConfig>;
|
|
32
|
+
}
|
|
33
|
+
export interface GoogleCredentials {
|
|
34
|
+
id: string;
|
|
35
|
+
secret: string;
|
|
36
|
+
}
|
|
37
|
+
export interface OutlookCredentials {
|
|
38
|
+
id: string;
|
|
39
|
+
tenantId: string;
|
|
40
|
+
}
|
|
41
|
+
export interface FullAppConfig extends AppConfig {
|
|
42
|
+
google_creds?: GoogleCredentials;
|
|
43
|
+
outlook_creds?: OutlookCredentials;
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "calendit",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Terminal-based calendar management tool for Google Calendar and Outlook via CLI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"calendit": "./bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "node --loader ts-node/esm src/index.ts",
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"prepare": "npm run build",
|
|
14
|
+
"test": "node --loader ts-node/esm src/test_runner.ts"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"calendar",
|
|
18
|
+
"google-calendar",
|
|
19
|
+
"outlook",
|
|
20
|
+
"microsoft-graph",
|
|
21
|
+
"cli",
|
|
22
|
+
"terminal",
|
|
23
|
+
"productivity"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18.0.0"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/chromatribe/calendit.git"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/chromatribe/calendit#readme",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/chromatribe/calendit/issues"
|
|
35
|
+
},
|
|
36
|
+
"author": "chromatribe - s.ohara <ivis.klain@chromatri.be>",
|
|
37
|
+
"license": "ISC",
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@azure/msal-node": "^3.8.10",
|
|
40
|
+
"@azure/msal-node-extensions": "^1.5.32",
|
|
41
|
+
"commander": "^12.1.0",
|
|
42
|
+
"csv-parse": "^6.2.1",
|
|
43
|
+
"csv-stringify": "^6.7.0",
|
|
44
|
+
"date-fns": "^4.1.0",
|
|
45
|
+
"date-fns-tz": "^3.2.0",
|
|
46
|
+
"enquirer": "^2.4.1",
|
|
47
|
+
"googleapis": "^171.4.0",
|
|
48
|
+
"open": "^11.0.0",
|
|
49
|
+
"zod": "^4.3.6"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^25.6.0",
|
|
53
|
+
"esbuild": "^0.28.0",
|
|
54
|
+
"ts-node": "^10.9.2",
|
|
55
|
+
"typescript": "^6.0.2",
|
|
56
|
+
"vitest": "^4.1.4"
|
|
57
|
+
}
|
|
58
|
+
}
|