kalvium-worklog 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/README.md +124 -0
- package/dist/capture-token.d.ts +5 -0
- package/dist/capture-token.js +128 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +209 -0
- package/dist/config.d.ts +29 -0
- package/dist/config.js +75 -0
- package/dist/daily-submit.d.ts +2 -0
- package/dist/daily-submit.js +9 -0
- package/dist/discover-position.d.ts +5 -0
- package/dist/discover-position.js +121 -0
- package/dist/generate-webapp.d.ts +4 -0
- package/dist/generate-webapp.js +61 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +7 -0
- package/dist/postinstall.d.ts +10 -0
- package/dist/postinstall.js +73 -0
- package/dist/refresh.d.ts +7 -0
- package/dist/refresh.js +42 -0
- package/dist/scheduler.d.ts +15 -0
- package/dist/scheduler.js +200 -0
- package/dist/submit.d.ts +26 -0
- package/dist/submit.js +196 -0
- package/package.json +51 -0
- package/templates/worklog_standalone.html +423 -0
- package/templates/worklog_template.html +387 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { chromium } from "playwright";
|
|
2
|
+
import { PROFILE_DIR, CONFIG_FILE, ensureInstallDir, saveConfig, } from "./config.js";
|
|
3
|
+
/**
|
|
4
|
+
* Discover the user's internship position ID by capturing API calls.
|
|
5
|
+
* Tries headless first, falls back to headed browser for manual login.
|
|
6
|
+
*/
|
|
7
|
+
export async function discoverPosition() {
|
|
8
|
+
ensureInstallDir();
|
|
9
|
+
console.log("=== Discovering internship position ID ===\n");
|
|
10
|
+
// Step 1: Try headless first
|
|
11
|
+
console.log("1. Trying headless mode (existing session)...");
|
|
12
|
+
let captured = await discoverViaBrowser(true);
|
|
13
|
+
if (!captured.token) {
|
|
14
|
+
console.log(" No valid session. Opening browser for manual login...");
|
|
15
|
+
captured = await discoverViaBrowser(false);
|
|
16
|
+
}
|
|
17
|
+
if (!captured.token) {
|
|
18
|
+
console.log("\nERROR: Could not capture token. Please log in and try again.");
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
console.log(` Token captured: ${captured.token.slice(0, 40)}...`);
|
|
22
|
+
// Step 2: Extract position ID from captured API URLs
|
|
23
|
+
const positionId = extractPositionId(captured.api_url ?? "");
|
|
24
|
+
if (!positionId) {
|
|
25
|
+
console.log("\nERROR: Could not determine position ID from API calls");
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
// Step 3: Save config
|
|
29
|
+
const worklogUrl = `https://student-api.kalvium.community/api/internships/worklogs/${positionId}`;
|
|
30
|
+
saveConfig({ position_id: positionId, worklog_api_url: worklogUrl });
|
|
31
|
+
console.log(`\n=== SUCCESS ===`);
|
|
32
|
+
console.log(`Position ID: ${positionId}`);
|
|
33
|
+
console.log(`Worklog API: ${worklogUrl}`);
|
|
34
|
+
console.log(`Saved to: ${CONFIG_FILE}`);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
async function discoverViaBrowser(headless) {
|
|
38
|
+
const captured = {};
|
|
39
|
+
const context = await chromium.launchPersistentContext(PROFILE_DIR, {
|
|
40
|
+
headless: false,
|
|
41
|
+
args: [
|
|
42
|
+
"--disable-blink-features=AutomationControlled",
|
|
43
|
+
...(headless ? ["--headless=new"] : []),
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
const page = context.pages()[0] ?? (await context.newPage());
|
|
47
|
+
page.on("request", (req) => {
|
|
48
|
+
const url = req.url();
|
|
49
|
+
if (url.includes("student-api.kalvium")) {
|
|
50
|
+
const auth = req.headers()["authorization"] ?? "";
|
|
51
|
+
if (auth.startsWith("Bearer ")) {
|
|
52
|
+
captured.token = auth;
|
|
53
|
+
}
|
|
54
|
+
if (url.includes("internships")) {
|
|
55
|
+
captured.api_url = url;
|
|
56
|
+
console.log(` Found API call: ${url}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
if (headless) {
|
|
61
|
+
console.log("Loading kalvium.community/internships (headless)...");
|
|
62
|
+
try {
|
|
63
|
+
await page.goto("https://kalvium.community/internships", {
|
|
64
|
+
waitUntil: "domcontentloaded",
|
|
65
|
+
timeout: 30000,
|
|
66
|
+
});
|
|
67
|
+
await page
|
|
68
|
+
.waitForLoadState("networkidle", { timeout: 20000 })
|
|
69
|
+
.catch(() => { });
|
|
70
|
+
await page.waitForTimeout(8000);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// ignore
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
console.log("\n ========================================");
|
|
78
|
+
console.log(" Browser opened. Please log in now:");
|
|
79
|
+
console.log(" 1. Click 'Continue with Google'");
|
|
80
|
+
console.log(" 2. Sign in with your Kalvium account");
|
|
81
|
+
console.log(" 3. Complete 2FA if prompted");
|
|
82
|
+
console.log(" 4. Wait — the script continues automatically");
|
|
83
|
+
console.log(" ========================================\n");
|
|
84
|
+
await page.goto("https://kalvium.community/internships", {
|
|
85
|
+
waitUntil: "domcontentloaded",
|
|
86
|
+
timeout: 120000,
|
|
87
|
+
});
|
|
88
|
+
console.log(" Waiting for login to complete (up to 5 minutes)...");
|
|
89
|
+
for (let i = 0; i < 300; i++) {
|
|
90
|
+
if (captured.token && captured.api_url)
|
|
91
|
+
break;
|
|
92
|
+
await page.waitForTimeout(1000);
|
|
93
|
+
if (i > 0 && i % 30 === 0) {
|
|
94
|
+
const url = page.url();
|
|
95
|
+
if (url.includes("kalvium.community/internships") &&
|
|
96
|
+
!url.includes("error")) {
|
|
97
|
+
await page
|
|
98
|
+
.waitForLoadState("networkidle", { timeout: 5000 })
|
|
99
|
+
.catch(() => { });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
await context.close();
|
|
105
|
+
return captured;
|
|
106
|
+
}
|
|
107
|
+
function extractPositionId(apiUrl) {
|
|
108
|
+
if (!apiUrl)
|
|
109
|
+
return null;
|
|
110
|
+
const parts = apiUrl.split("/");
|
|
111
|
+
const internshipsIdx = parts.indexOf("internships");
|
|
112
|
+
if (internshipsIdx === -1)
|
|
113
|
+
return null;
|
|
114
|
+
for (const part of parts.slice(internshipsIdx + 1)) {
|
|
115
|
+
// Position IDs are UUIDs (36 chars, 4 dashes)
|
|
116
|
+
if (part.length === 36 && part.split("-").length === 5) {
|
|
117
|
+
return part;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { loadToken, ensureInstallDir, getPositionIdFromToken } from "./config.js";
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
/**
|
|
8
|
+
* Generate a phone-friendly web app HTML file with the refresh token embedded.
|
|
9
|
+
*/
|
|
10
|
+
export async function generateWebapp(outputPath) {
|
|
11
|
+
ensureInstallDir();
|
|
12
|
+
const token = loadToken();
|
|
13
|
+
if (!token) {
|
|
14
|
+
console.log("ERROR: No token found. Run `kalvium-worklog login` first.");
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
// Find template — check dist/templates (published), then ../templates (dev)
|
|
18
|
+
const templateCandidates = [
|
|
19
|
+
join(__dirname, "..", "templates", "worklog_template.html"),
|
|
20
|
+
join(__dirname, "templates", "worklog_template.html"),
|
|
21
|
+
];
|
|
22
|
+
let templateHtml = null;
|
|
23
|
+
for (const path of templateCandidates) {
|
|
24
|
+
if (existsSync(path)) {
|
|
25
|
+
templateHtml = readFileSync(path, "utf-8");
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (!templateHtml) {
|
|
30
|
+
console.log("ERROR: Template not found.");
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
// Inject token into HTML
|
|
34
|
+
const tokenJson = JSON.stringify(token);
|
|
35
|
+
const injectScript = `
|
|
36
|
+
<script>
|
|
37
|
+
const EMBEDDED_TOKEN = ${tokenJson};
|
|
38
|
+
if (!localStorage.getItem("kalvium_tokens")) {
|
|
39
|
+
localStorage.setItem("kalvium_tokens", JSON.stringify(EMBEDDED_TOKEN));
|
|
40
|
+
}
|
|
41
|
+
</script>
|
|
42
|
+
`;
|
|
43
|
+
const html = templateHtml.replace("</body>", injectScript + "\n</body>");
|
|
44
|
+
// Write output
|
|
45
|
+
const outPath = outputPath ?? join(homedir(), "KalviumWorklog.html");
|
|
46
|
+
writeFileSync(outPath, html);
|
|
47
|
+
console.log(` Web app generated: ${outPath}`);
|
|
48
|
+
console.log(` Size: ${html.length} bytes`);
|
|
49
|
+
// Show position ID from JWT
|
|
50
|
+
const posId = getPositionIdFromToken(token);
|
|
51
|
+
if (posId) {
|
|
52
|
+
console.log(` Position ID (from JWT): ${posId}`);
|
|
53
|
+
}
|
|
54
|
+
console.log(`\n To set up on phone:`);
|
|
55
|
+
console.log(` 1. AirDrop '${outPath}' to your phone, OR`);
|
|
56
|
+
console.log(` 2. Open the file in Safari → Share → Add to Home Screen`);
|
|
57
|
+
console.log(`\n Or manually transfer the token:`);
|
|
58
|
+
console.log(` - Open the web app on your phone`);
|
|
59
|
+
console.log(` - Tap 'Setup / Token' → paste token JSON → Save`);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { captureToken } from "./capture-token.js";
|
|
2
|
+
export { discoverPosition } from "./discover-position.js";
|
|
3
|
+
export { submitWorklog, checkStatus, dailySubmit, STATUS_OPTIONS } from "./submit.js";
|
|
4
|
+
export type { SubmitOptions, SubmitResult } from "./submit.js";
|
|
5
|
+
export { setupScheduler, removeScheduler } from "./scheduler.js";
|
|
6
|
+
export type { ScheduleOptions } from "./scheduler.js";
|
|
7
|
+
export { generateWebapp } from "./generate-webapp.js";
|
|
8
|
+
export { refreshToken } from "./refresh.js";
|
|
9
|
+
export { loadToken, saveToken, loadConfig, saveConfig, getWorklogApi, INSTALL_DIR, TOKEN_FILE, CONFIG_FILE, LOG_FILE, } from "./config.js";
|
|
10
|
+
export type { TokenData, ConfigData } from "./config.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { captureToken } from "./capture-token.js";
|
|
2
|
+
export { discoverPosition } from "./discover-position.js";
|
|
3
|
+
export { submitWorklog, checkStatus, dailySubmit, STATUS_OPTIONS } from "./submit.js";
|
|
4
|
+
export { setupScheduler, removeScheduler } from "./scheduler.js";
|
|
5
|
+
export { generateWebapp } from "./generate-webapp.js";
|
|
6
|
+
export { refreshToken } from "./refresh.js";
|
|
7
|
+
export { loadToken, saveToken, loadConfig, saveConfig, getWorklogApi, INSTALL_DIR, TOKEN_FILE, CONFIG_FILE, LOG_FILE, } from "./config.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Postinstall script — runs automatically after `npm install kalvium-worklog`.
|
|
3
|
+
*
|
|
4
|
+
* If the package is installed globally, prompts the user to run setup.
|
|
5
|
+
* If installed locally (as a dependency), just prints info.
|
|
6
|
+
*
|
|
7
|
+
* IMPORTANT: This must not block or require user input during npm install
|
|
8
|
+
* in CI environments. It only prints guidance.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Postinstall script — runs automatically after `npm install kalvium-worklog`.
|
|
3
|
+
*
|
|
4
|
+
* If the package is installed globally, prompts the user to run setup.
|
|
5
|
+
* If installed locally (as a dependency), just prints info.
|
|
6
|
+
*
|
|
7
|
+
* IMPORTANT: This must not block or require user input during npm install
|
|
8
|
+
* in CI environments. It only prints guidance.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync } from "fs";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
import { homedir } from "os";
|
|
13
|
+
const TOKEN_FILE = join(homedir(), ".kalvium", "refresh_token.json");
|
|
14
|
+
const CONFIG_FILE = join(homedir(), ".kalvium", "config.json");
|
|
15
|
+
// Detect if this is a global install
|
|
16
|
+
const isGlobal = process.env.npm_config_global === "true";
|
|
17
|
+
// Don't run postinstall in CI or during npm publish
|
|
18
|
+
const isCI = process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
|
|
19
|
+
if (isCI) {
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
// Use stderr for output — npm suppresses stdout from lifecycle scripts
|
|
23
|
+
// but stderr is always shown
|
|
24
|
+
const log = (msg) => console.error(msg);
|
|
25
|
+
const GREEN = "\x1b[0;32m";
|
|
26
|
+
const CYAN = "\x1b[0;36m";
|
|
27
|
+
const YELLOW = "\x1b[1;33m";
|
|
28
|
+
const BOLD = "\x1b[1m";
|
|
29
|
+
const NC = "\x1b[0m";
|
|
30
|
+
log("");
|
|
31
|
+
log(CYAN + "==".repeat(25) + NC);
|
|
32
|
+
log(CYAN + " " + BOLD + "Kalvium Worklog" + NC + CYAN + " installed successfully!" + " ".repeat(7) + NC);
|
|
33
|
+
log(CYAN + "==".repeat(25) + NC);
|
|
34
|
+
log("");
|
|
35
|
+
const alreadySetup = existsSync(TOKEN_FILE) && existsSync(CONFIG_FILE);
|
|
36
|
+
if (alreadySetup) {
|
|
37
|
+
log(GREEN + "OK" + NC + " You are already set up!");
|
|
38
|
+
log("");
|
|
39
|
+
log(" Quick commands:");
|
|
40
|
+
log(" " + BOLD + "kalvium-worklog submit" + NC + " submit worklog");
|
|
41
|
+
log(" " + BOLD + "kalvium-worklog status" + NC + " check status");
|
|
42
|
+
log("");
|
|
43
|
+
}
|
|
44
|
+
else if (isGlobal) {
|
|
45
|
+
log(" To get started, run:");
|
|
46
|
+
log("");
|
|
47
|
+
log(" " + BOLD + "kalvium-worklog install" + NC);
|
|
48
|
+
log("");
|
|
49
|
+
log(" This will:");
|
|
50
|
+
log(" 1. Open a browser for you to log in to Kalvium");
|
|
51
|
+
log(" 2. Auto-discover your internship position ID");
|
|
52
|
+
log(" 3. Set up daily auto-submit at your chosen time");
|
|
53
|
+
log(" 4. Generate a phone web app");
|
|
54
|
+
log("");
|
|
55
|
+
log(" Or set up step by step:");
|
|
56
|
+
log(" " + BOLD + "kalvium-worklog login" + NC + " log in");
|
|
57
|
+
log(" " + BOLD + "kalvium-worklog discover" + NC + " find position ID");
|
|
58
|
+
log(" " + BOLD + "kalvium-worklog schedule 14:00" + NC + " daily at 2 PM");
|
|
59
|
+
log("");
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
// Local install — use npx
|
|
63
|
+
log(" To get started, run:");
|
|
64
|
+
log("");
|
|
65
|
+
log(" " + BOLD + "npx kalvium-worklog install" + NC);
|
|
66
|
+
log("");
|
|
67
|
+
log(" Or install globally first:");
|
|
68
|
+
log(" " + BOLD + "npm install -g kalvium-worklog" + NC);
|
|
69
|
+
log(" " + BOLD + "kalvium-worklog install" + NC);
|
|
70
|
+
log("");
|
|
71
|
+
}
|
|
72
|
+
log(" Docs: " + YELLOW + "https://github.com/kp/kalvium-worklog" + NC);
|
|
73
|
+
log("");
|
package/dist/refresh.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { appendFileSync } from "fs";
|
|
2
|
+
import { LOG_FILE, ensureInstallDir } from "./config.js";
|
|
3
|
+
export function log(message) {
|
|
4
|
+
ensureInstallDir();
|
|
5
|
+
const timestamp = new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
6
|
+
const line = `${timestamp} ${message}\n`;
|
|
7
|
+
try {
|
|
8
|
+
appendFileSync(LOG_FILE, line);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
// ignore
|
|
12
|
+
}
|
|
13
|
+
console.log(message);
|
|
14
|
+
}
|
|
15
|
+
export async function refreshToken() {
|
|
16
|
+
const { loadToken, saveToken, KEYCLOAK_URL, CLIENT_ID } = await import("./config.js");
|
|
17
|
+
const token = loadToken();
|
|
18
|
+
if (!token?.refresh_token) {
|
|
19
|
+
return { success: false, error: "No refresh token found" };
|
|
20
|
+
}
|
|
21
|
+
const body = new URLSearchParams({
|
|
22
|
+
grant_type: "refresh_token",
|
|
23
|
+
client_id: CLIENT_ID,
|
|
24
|
+
refresh_token: token.refresh_token,
|
|
25
|
+
});
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetch(KEYCLOAK_URL, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
30
|
+
body,
|
|
31
|
+
});
|
|
32
|
+
const data = await res.json();
|
|
33
|
+
if (data.error) {
|
|
34
|
+
return { success: false, error: data.error };
|
|
35
|
+
}
|
|
36
|
+
saveToken(data);
|
|
37
|
+
return { success: true, accessToken: data.access_token };
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
return { success: false, error: String(e) };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ScheduleOptions {
|
|
2
|
+
hour: number;
|
|
3
|
+
minute: number;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Set up daily auto-submit scheduler.
|
|
7
|
+
* macOS: launchd
|
|
8
|
+
* Windows: Task Scheduler
|
|
9
|
+
* Linux: cron
|
|
10
|
+
*/
|
|
11
|
+
export declare function setupScheduler(options: ScheduleOptions): Promise<boolean>;
|
|
12
|
+
/**
|
|
13
|
+
* Remove the scheduled task.
|
|
14
|
+
*/
|
|
15
|
+
export declare function removeScheduler(): Promise<boolean>;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { homedir, platform } from "os";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { writeFileSync, existsSync, mkdirSync } from "fs";
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
import { INSTALL_DIR, LOG_FILE, ensureInstallDir } from "./config.js";
|
|
6
|
+
import { dirname } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const IS_MAC = platform() === "darwin";
|
|
10
|
+
const IS_WINDOWS = platform() === "win32";
|
|
11
|
+
const IS_LINUX = platform() === "linux";
|
|
12
|
+
/**
|
|
13
|
+
* Set up daily auto-submit scheduler.
|
|
14
|
+
* macOS: launchd
|
|
15
|
+
* Windows: Task Scheduler
|
|
16
|
+
* Linux: cron
|
|
17
|
+
*/
|
|
18
|
+
export async function setupScheduler(options) {
|
|
19
|
+
const { hour, minute } = options;
|
|
20
|
+
if (IS_MAC) {
|
|
21
|
+
return setupLaunchd(hour, minute);
|
|
22
|
+
}
|
|
23
|
+
else if (IS_WINDOWS) {
|
|
24
|
+
return setupTaskScheduler(hour, minute);
|
|
25
|
+
}
|
|
26
|
+
else if (IS_LINUX) {
|
|
27
|
+
return setupCron(hour, minute);
|
|
28
|
+
}
|
|
29
|
+
console.log("Unknown OS — cannot set up scheduler. Use `kalvium-worklog submit` manually.");
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
function formatTime(hour, minute) {
|
|
33
|
+
if (hour < 12)
|
|
34
|
+
return `${hour}:${minute.toString().padStart(2, "0")} AM`;
|
|
35
|
+
if (hour === 12)
|
|
36
|
+
return `12:${minute.toString().padStart(2, "0")} PM`;
|
|
37
|
+
return `${hour - 12}:${minute.toString().padStart(2, "0")} PM`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Copy the daily-submit.js script to ~/.kalvium/ as a wrapper that
|
|
41
|
+
* imports from the npm package's dist directory (using absolute paths).
|
|
42
|
+
* This way it works even if the package is updated.
|
|
43
|
+
*/
|
|
44
|
+
function copyDailyScript() {
|
|
45
|
+
ensureInstallDir();
|
|
46
|
+
const dst = join(INSTALL_DIR, "daily_submit.js");
|
|
47
|
+
// Create a wrapper that imports from the package's dist directory
|
|
48
|
+
const distDir = __dirname;
|
|
49
|
+
const wrapper = `#!/usr/bin/env node
|
|
50
|
+
// Auto-generated by kalvium-worklog. Do not edit.
|
|
51
|
+
import { dailySubmit } from "${join(distDir, "submit.js")}";
|
|
52
|
+
dailySubmit().then((ok) => process.exit(ok ? 0 : 1));
|
|
53
|
+
`;
|
|
54
|
+
writeFileSync(dst, wrapper);
|
|
55
|
+
return dst;
|
|
56
|
+
}
|
|
57
|
+
function setupLaunchd(hour, minute) {
|
|
58
|
+
const launchAgentsDir = join(homedir(), "Library", "LaunchAgents");
|
|
59
|
+
if (!existsSync(launchAgentsDir)) {
|
|
60
|
+
mkdirSync(launchAgentsDir, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
const plistPath = join(launchAgentsDir, "com.kalvium.worklog.plist");
|
|
63
|
+
const dailyScript = copyDailyScript();
|
|
64
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
65
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
66
|
+
<plist version="1.0">
|
|
67
|
+
<dict>
|
|
68
|
+
<key>Label</key>
|
|
69
|
+
<string>com.kalvium.worklog</string>
|
|
70
|
+
<key>ProgramArguments</key>
|
|
71
|
+
<array>
|
|
72
|
+
<string>${process.execPath}</string>
|
|
73
|
+
<string>${dailyScript}</string>
|
|
74
|
+
</array>
|
|
75
|
+
<key>StartCalendarInterval</key>
|
|
76
|
+
<dict>
|
|
77
|
+
<key>Hour</key>
|
|
78
|
+
<integer>${hour}</integer>
|
|
79
|
+
<key>Minute</key>
|
|
80
|
+
<integer>${minute}</integer>
|
|
81
|
+
<key>Weekday</key>
|
|
82
|
+
<array>
|
|
83
|
+
<integer>1</integer>
|
|
84
|
+
<integer>2</integer>
|
|
85
|
+
<integer>3</integer>
|
|
86
|
+
<integer>4</integer>
|
|
87
|
+
<integer>5</integer>
|
|
88
|
+
</array>
|
|
89
|
+
</dict>
|
|
90
|
+
<key>StandardOutPath</key>
|
|
91
|
+
<string>${LOG_FILE}</string>
|
|
92
|
+
<key>StandardErrorPath</key>
|
|
93
|
+
<string>${LOG_FILE}</string>
|
|
94
|
+
<key>EnvironmentVariables</key>
|
|
95
|
+
<dict>
|
|
96
|
+
<key>HOME</key>
|
|
97
|
+
<string>${homedir()}</string>
|
|
98
|
+
<key>PATH</key>
|
|
99
|
+
<string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin</string>
|
|
100
|
+
</dict>
|
|
101
|
+
</dict>
|
|
102
|
+
</plist>`;
|
|
103
|
+
writeFileSync(plistPath, plist);
|
|
104
|
+
execSync(`launchctl unload "${plistPath}"`, { stdio: "ignore" });
|
|
105
|
+
execSync(`launchctl load "${plistPath}"`, { stdio: "ignore" });
|
|
106
|
+
console.log(`Daily auto-submit installed (${formatTime(hour, minute)} every weekday)`);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
function setupTaskScheduler(hour, minute) {
|
|
110
|
+
const dailyScript = copyDailyScript();
|
|
111
|
+
const batPath = join(INSTALL_DIR, "daily_submit.bat");
|
|
112
|
+
// Create batch wrapper
|
|
113
|
+
writeFileSync(batPath, `@echo off\r\n"${process.execPath}" "${dailyScript}"\r\n`);
|
|
114
|
+
// Remove existing task
|
|
115
|
+
try {
|
|
116
|
+
execSync('schtasks /Delete /TN "KalviumWorklog" /F', { stdio: "ignore" });
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// task doesn't exist yet
|
|
120
|
+
}
|
|
121
|
+
const timeStr = `${hour.toString().padStart(2, "0")}:${minute
|
|
122
|
+
.toString()
|
|
123
|
+
.padStart(2, "0")}`;
|
|
124
|
+
try {
|
|
125
|
+
execSync(`schtasks /Create /TN "KalviumWorklog" /TR "${batPath}" /SC WEEKLY /D MON,TUE,WED,THU,FRI /ST ${timeStr} /F`, { stdio: "pipe" });
|
|
126
|
+
console.log(`Daily auto-submit installed (${formatTime(hour, minute)} every weekday)`);
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
console.log(`Failed to create scheduled task: ${e}`);
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function setupCron(hour, minute) {
|
|
135
|
+
const dailyScript = copyDailyScript();
|
|
136
|
+
const cronLine = `${minute} ${hour} * * 1-5 ${process.execPath} ${dailyScript} >> ${LOG_FILE} 2>&1`;
|
|
137
|
+
// Read existing crontab
|
|
138
|
+
let crontab = "";
|
|
139
|
+
try {
|
|
140
|
+
crontab = execSync("crontab -l", { encoding: "utf-8" });
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// no crontab yet
|
|
144
|
+
}
|
|
145
|
+
// Remove existing kalvium entry
|
|
146
|
+
const lines = crontab
|
|
147
|
+
.split("\n")
|
|
148
|
+
.filter((line) => !line.includes("kalvium") && !line.includes("KalviumWorklog"));
|
|
149
|
+
// Add new entry
|
|
150
|
+
lines.push(cronLine);
|
|
151
|
+
// Write back
|
|
152
|
+
const newCrontab = lines.join("\n") + "\n";
|
|
153
|
+
execSync(`echo "${newCrontab.replace(/"/g, '\\"')}" | crontab -`);
|
|
154
|
+
console.log(`Daily auto-submit installed (${formatTime(hour, minute)} every weekday via cron)`);
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Remove the scheduled task.
|
|
159
|
+
*/
|
|
160
|
+
export async function removeScheduler() {
|
|
161
|
+
if (IS_MAC) {
|
|
162
|
+
const plistPath = join(homedir(), "Library/LaunchAgents/com.kalvium.worklog.plist");
|
|
163
|
+
try {
|
|
164
|
+
execSync(`launchctl unload "${plistPath}"`, { stdio: "ignore" });
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// ignore
|
|
168
|
+
}
|
|
169
|
+
if (existsSync(plistPath)) {
|
|
170
|
+
execSync(`rm "${plistPath}"`);
|
|
171
|
+
}
|
|
172
|
+
console.log("Daily auto-submit removed.");
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
else if (IS_WINDOWS) {
|
|
176
|
+
try {
|
|
177
|
+
execSync('schtasks /Delete /TN "KalviumWorklog" /F', { stdio: "ignore" });
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// ignore
|
|
181
|
+
}
|
|
182
|
+
console.log("Daily auto-submit removed.");
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
else if (IS_LINUX) {
|
|
186
|
+
try {
|
|
187
|
+
const crontab = execSync("crontab -l", { encoding: "utf-8" });
|
|
188
|
+
const lines = crontab
|
|
189
|
+
.split("\n")
|
|
190
|
+
.filter((line) => !line.includes("kalvium") && !line.includes("KalviumWorklog"));
|
|
191
|
+
execSync(`echo "${lines.join("\n")}" | crontab -`);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// ignore
|
|
195
|
+
}
|
|
196
|
+
console.log("Daily auto-submit removed.");
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
200
|
+
}
|
package/dist/submit.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface SubmitOptions {
|
|
2
|
+
text?: string;
|
|
3
|
+
status?: string;
|
|
4
|
+
dryRun?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface SubmitResult {
|
|
7
|
+
success: boolean;
|
|
8
|
+
message: string;
|
|
9
|
+
worklogId?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare const STATUS_OPTIONS: Record<string, string>;
|
|
12
|
+
/**
|
|
13
|
+
* Submit worklog with auto-relogin retry.
|
|
14
|
+
* If the refresh token has expired, automatically opens a browser
|
|
15
|
+
* for re-login and retries the submission.
|
|
16
|
+
*/
|
|
17
|
+
export declare function submitWorklog(options?: SubmitOptions): Promise<SubmitResult>;
|
|
18
|
+
/**
|
|
19
|
+
* Check recent worklog status with auto-relogin retry.
|
|
20
|
+
*/
|
|
21
|
+
export declare function checkStatus(): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Daily auto-submit (used by scheduler).
|
|
24
|
+
* Skips weekends. Logs to file.
|
|
25
|
+
*/
|
|
26
|
+
export declare function dailySubmit(): Promise<boolean>;
|