lazypush-cli 0.1.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.
@@ -0,0 +1,229 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.handleScheduleInteractive = handleScheduleInteractive;
7
+ exports.handleSchedule = handleSchedule;
8
+ const inquirer_1 = __importDefault(require("inquirer"));
9
+ const dayjs_1 = __importDefault(require("dayjs"));
10
+ const client_1 = require("../api/client");
11
+ const logger_1 = require("../logger");
12
+ const auth_1 = require("../utils/auth");
13
+ const git_1 = require("../utils/git");
14
+ const scheduler_1 = require("../utils/scheduler");
15
+ async function handleScheduleInteractive() {
16
+ if (!(await (0, auth_1.requireAuth)())) {
17
+ process.exit(1);
18
+ }
19
+ try {
20
+ // Pre-flight checks
21
+ (0, logger_1.info)('═══ PRE-FLIGHT CHECKS ═══');
22
+ (0, logger_1.info)('Detecting repository...');
23
+ const repoUrl = (0, git_1.getRepoUrl)();
24
+ const branch = (0, git_1.getBranch)();
25
+ (0, logger_1.info)(`Repository: ${repoUrl}`);
26
+ (0, logger_1.info)(`Branch: ${branch}`);
27
+ (0, logger_1.info)('');
28
+ // Check for uncommitted changes
29
+ if ((0, git_1.hasUncommittedChanges)()) {
30
+ const files = (0, git_1.getUncommittedFiles)();
31
+ (0, logger_1.warn)(`Found ${files.length} uncommitted file(s):`);
32
+ files.forEach(f => (0, logger_1.info)(` - ${f}`));
33
+ (0, logger_1.info)('');
34
+ try {
35
+ require('child_process').execSync('git add .', { stdio: 'inherit' });
36
+ (0, logger_1.success)('Files staged for commit');
37
+ const staged = require('child_process')
38
+ .execSync('git diff --name-only --cached', { encoding: 'utf8' })
39
+ .trim();
40
+ if (staged) {
41
+ (0, logger_1.info)('Staged files:');
42
+ staged.split('\n').forEach((f) => (0, logger_1.info)(` - ${f}`));
43
+ }
44
+ else {
45
+ (0, logger_1.warn)('No staged files found');
46
+ }
47
+ (0, logger_1.info)('');
48
+ }
49
+ catch (e) {
50
+ (0, logger_1.error)('Failed to stage files');
51
+ process.exit(1);
52
+ }
53
+ }
54
+ else {
55
+ (0, logger_1.success)('Working directory clean');
56
+ (0, logger_1.info)('');
57
+ }
58
+ // Check branch tracking
59
+ const tracking = (0, git_1.getBranchTrackingInfo)();
60
+ if (tracking) {
61
+ (0, logger_1.info)(`Remote: ${tracking.remote}/${tracking.upstream}`);
62
+ }
63
+ else {
64
+ (0, logger_1.warn)(`No upstream tracking set for branch "${branch}"`);
65
+ (0, logger_1.info)('Note: Will push to origin');
66
+ (0, logger_1.info)('');
67
+ }
68
+ (0, logger_1.info)('═══════════════════════');
69
+ (0, logger_1.info)('');
70
+ // Step 1: Date, time, timezone
71
+ const timeAnswers = await inquirer_1.default.prompt([
72
+ {
73
+ type: 'input',
74
+ name: 'date',
75
+ message: 'Date (dd/mm/yyyy format, or press Enter for today)',
76
+ default: () => (0, dayjs_1.default)().format('DD/MM/YYYY'),
77
+ validate: (input) => {
78
+ const parts = input.split('/');
79
+ if (parts.length !== 3)
80
+ return 'Invalid format. Use dd/mm/yyyy';
81
+ const d = parseInt(parts[0], 10);
82
+ const m = parseInt(parts[1], 10);
83
+ const y = parseInt(parts[2], 10);
84
+ if (d < 1 || d > 31 || m < 1 || m > 12)
85
+ return 'Invalid date';
86
+ return true;
87
+ }
88
+ },
89
+ {
90
+ type: 'input',
91
+ name: 'time',
92
+ message: 'Time (e.g., 5:30pm, 17:30, 9:15am)',
93
+ validate: (input) => {
94
+ const match = input.match(/^(\d{1,2})(?::(\d{2}))?\s?(am|pm)?$/i);
95
+ if (!match)
96
+ return 'Invalid time format. Use: 5pm, 5:30pm, or 17:30';
97
+ return true;
98
+ }
99
+ },
100
+ {
101
+ type: 'input',
102
+ name: 'timezone',
103
+ message: 'Timezone (or press Enter for local)',
104
+ default: 'local'
105
+ }
106
+ ]);
107
+ // Show timezone info
108
+ const tz = timeAnswers.timezone === 'local' ? undefined : timeAnswers.timezone;
109
+ const tzDisplay = tz || dayjs_1.default.tz.guess() || 'UTC';
110
+ (0, logger_1.info)(`Local timezone: ${tzDisplay}`);
111
+ (0, logger_1.info)('');
112
+ // Step 2: Commit message
113
+ const messageAnswers = await inquirer_1.default.prompt([
114
+ {
115
+ type: 'input',
116
+ name: 'message',
117
+ message: 'Commit message (optional)',
118
+ default: ''
119
+ }
120
+ ]);
121
+ // Step 3: Show repo/branch and ask for confirmation
122
+ (0, logger_1.info)('');
123
+ (0, logger_1.info)('═══ PUSH DETAILS ═══');
124
+ (0, logger_1.info)(`Repository: ${repoUrl}`);
125
+ (0, logger_1.info)(`Branch: ${branch}`);
126
+ (0, logger_1.info)(`Commit Message: ${messageAnswers.message || '(none)'}`);
127
+ (0, logger_1.info)('═══════════════════');
128
+ (0, logger_1.info)('');
129
+ const confirmAnswers = await inquirer_1.default.prompt([
130
+ {
131
+ type: 'confirm',
132
+ name: 'confirm',
133
+ message: 'Confirm scheduling?',
134
+ default: true
135
+ }
136
+ ]);
137
+ if (!confirmAnswers.confirm) {
138
+ (0, logger_1.info)('Scheduling cancelled');
139
+ return;
140
+ }
141
+ (0, logger_1.info)('Parsing scheduled time...');
142
+ const dateStr = timeAnswers.date;
143
+ const timeStr = timeAnswers.time;
144
+ const [day, month, year] = dateStr.split('/');
145
+ let scheduledAt;
146
+ try {
147
+ let target = (0, dayjs_1.default)(`${year}-${month}-${day}`);
148
+ if (tz)
149
+ target = target.tz(tz);
150
+ const timeMatch = timeStr.match(/^(\d{1,2})(?::(\d{2}))?\s?(am|pm)?$/i);
151
+ if (timeMatch) {
152
+ let [, hour, minutes, ampm] = timeMatch;
153
+ let h = parseInt(hour, 10);
154
+ let m = minutes ? parseInt(minutes, 10) : 0;
155
+ if (ampm?.toLowerCase() === 'pm' && h !== 12)
156
+ h += 12;
157
+ if (ampm?.toLowerCase() === 'am' && h === 12)
158
+ h = 0;
159
+ target = target.hour(h).minute(m).second(0);
160
+ }
161
+ scheduledAt = (tz ? target.utc() : target).toDate();
162
+ }
163
+ catch (e) {
164
+ throw new Error('Invalid date/time');
165
+ }
166
+ (0, logger_1.info)(`Scheduled for: ${scheduledAt.toLocaleString()} UTC`);
167
+ (0, logger_1.info)('Creating git bundle...');
168
+ const bundlePath = (0, git_1.createBundle)(branch);
169
+ (0, logger_1.info)('Compressing bundle...');
170
+ const gzPath = (0, git_1.compressBundle)(bundlePath);
171
+ (0, logger_1.info)('Encoding to base64...');
172
+ const bundleBase64 = (0, git_1.bundleToBase64)(gzPath);
173
+ (0, logger_1.info)('Uploading to backend...');
174
+ const api = (0, client_1.getApiClient)();
175
+ const job = await api.scheduleJob({
176
+ repoUrl,
177
+ branch,
178
+ scheduledAt,
179
+ bundleBase64,
180
+ commitMessage: messageAnswers.message || undefined
181
+ });
182
+ (0, git_1.cleanupBundle)(bundlePath, gzPath);
183
+ (0, logger_1.success)(`Job scheduled! ID: ${job.id}`);
184
+ (0, logger_1.info)(`Push will occur at: ${scheduledAt.toLocaleString()}`);
185
+ }
186
+ catch (e) {
187
+ (0, logger_1.error)(`Failed: ${e.message}`);
188
+ process.exit(1);
189
+ }
190
+ }
191
+ async function handleSchedule(timeInput, timezone, message) {
192
+ if (!(await (0, auth_1.requireAuth)())) {
193
+ process.exit(1);
194
+ }
195
+ try {
196
+ (0, logger_1.info)('Detecting repository...');
197
+ const repoUrl = (0, git_1.getRepoUrl)();
198
+ const branch = (0, git_1.getBranch)();
199
+ (0, logger_1.info)(`Repository: ${repoUrl}`);
200
+ (0, logger_1.info)(`Branch: ${branch}`);
201
+ (0, logger_1.info)('Parsing scheduled time...');
202
+ const tz = timezone || 'UTC';
203
+ (0, logger_1.info)(`Using timezone: ${tz}`);
204
+ const scheduledAt = (0, scheduler_1.parseScheduleTime)(timeInput, tz);
205
+ (0, logger_1.info)(`Scheduled for: ${scheduledAt.toLocaleString()} UTC`);
206
+ (0, logger_1.info)('Creating git bundle...');
207
+ const bundlePath = (0, git_1.createBundle)(branch);
208
+ (0, logger_1.info)('Compressing bundle...');
209
+ const gzPath = (0, git_1.compressBundle)(bundlePath);
210
+ (0, logger_1.info)('Encoding to base64...');
211
+ const bundleBase64 = (0, git_1.bundleToBase64)(gzPath);
212
+ (0, logger_1.info)('Uploading to backend...');
213
+ const api = (0, client_1.getApiClient)();
214
+ const job = await api.scheduleJob({
215
+ repoUrl,
216
+ branch,
217
+ scheduledAt,
218
+ bundleBase64,
219
+ commitMessage: message
220
+ });
221
+ (0, git_1.cleanupBundle)(bundlePath, gzPath);
222
+ (0, logger_1.success)(`Job scheduled! ID: ${job.id}`);
223
+ (0, logger_1.info)(`Push will occur at: ${scheduledAt.toLocaleString()}`);
224
+ }
225
+ catch (e) {
226
+ (0, logger_1.error)(`Failed: ${e.message}`);
227
+ process.exit(1);
228
+ }
229
+ }
package/dist/config.js ADDED
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.saveSession = saveSession;
7
+ exports.getSession = getSession;
8
+ exports.clearSession = clearSession;
9
+ exports.getAuthToken = getAuthToken;
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const os_1 = __importDefault(require("os"));
13
+ const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.lazypush');
14
+ const SESSION_FILE = path_1.default.join(CONFIG_DIR, 'session.json');
15
+ function ensureConfigDir() {
16
+ if (!fs_1.default.existsSync(CONFIG_DIR)) {
17
+ fs_1.default.mkdirSync(CONFIG_DIR, { recursive: true });
18
+ }
19
+ }
20
+ function saveSession(session) {
21
+ ensureConfigDir();
22
+ fs_1.default.writeFileSync(SESSION_FILE, JSON.stringify(session, null, 2), 'utf8');
23
+ }
24
+ function getSession() {
25
+ if (!fs_1.default.existsSync(SESSION_FILE))
26
+ return null;
27
+ try {
28
+ const data = fs_1.default.readFileSync(SESSION_FILE, 'utf8');
29
+ return JSON.parse(data);
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ function clearSession() {
36
+ if (fs_1.default.existsSync(SESSION_FILE)) {
37
+ fs_1.default.unlinkSync(SESSION_FILE);
38
+ }
39
+ }
40
+ function getAuthToken() {
41
+ const session = getSession();
42
+ return session?.token || null;
43
+ }
package/dist/logger.js ADDED
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.info = info;
7
+ exports.success = success;
8
+ exports.error = error;
9
+ exports.warn = warn;
10
+ exports.log = log;
11
+ const chalk_1 = __importDefault(require("chalk"));
12
+ function info(msg) {
13
+ console.log(chalk_1.default.blue('ℹ'), msg);
14
+ }
15
+ function success(msg) {
16
+ console.log(chalk_1.default.green('✓'), msg);
17
+ }
18
+ function error(msg) {
19
+ console.log(chalk_1.default.red('✗'), msg);
20
+ }
21
+ function warn(msg) {
22
+ console.log(chalk_1.default.yellow('⚠'), msg);
23
+ }
24
+ function log(msg) {
25
+ console.log(msg);
26
+ }
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.requireAuth = requireAuth;
4
+ const login_1 = require("../commands/login");
5
+ const config_1 = require("../config");
6
+ const logger_1 = require("../logger");
7
+ async function requireAuth() {
8
+ const token = (0, config_1.getAuthToken)();
9
+ if (token) {
10
+ return true;
11
+ }
12
+ (0, logger_1.warn)('Not authenticated. Starting login...');
13
+ (0, logger_1.info)('');
14
+ await (0, login_1.handleLogin)();
15
+ return (0, config_1.getAuthToken)() ? true : false;
16
+ }
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getRepoUrl = getRepoUrl;
7
+ exports.getBranch = getBranch;
8
+ exports.hasUncommittedChanges = hasUncommittedChanges;
9
+ exports.getUncommittedFiles = getUncommittedFiles;
10
+ exports.getBranchTrackingInfo = getBranchTrackingInfo;
11
+ exports.createBundle = createBundle;
12
+ exports.compressBundle = compressBundle;
13
+ exports.bundleToBase64 = bundleToBase64;
14
+ exports.cleanupBundle = cleanupBundle;
15
+ const child_process_1 = require("child_process");
16
+ const fs_1 = __importDefault(require("fs"));
17
+ const zlib_1 = __importDefault(require("zlib"));
18
+ const path_1 = __importDefault(require("path"));
19
+ const os_1 = __importDefault(require("os"));
20
+ function getRepoUrl() {
21
+ try {
22
+ const url = (0, child_process_1.execSync)('git remote get-url origin', { encoding: 'utf8' }).trim();
23
+ if (!url)
24
+ throw new Error('No origin found');
25
+ return url;
26
+ }
27
+ catch {
28
+ throw new Error('Not in a git repository or no origin set');
29
+ }
30
+ }
31
+ function getBranch() {
32
+ try {
33
+ const branch = (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim();
34
+ if (!branch || branch === 'HEAD')
35
+ throw new Error('Cannot determine branch');
36
+ return branch;
37
+ }
38
+ catch {
39
+ throw new Error('Could not detect current branch');
40
+ }
41
+ }
42
+ function hasUncommittedChanges() {
43
+ try {
44
+ const status = (0, child_process_1.execSync)('git status --porcelain', { encoding: 'utf8' }).trim();
45
+ return status.length > 0;
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ }
51
+ function getUncommittedFiles() {
52
+ try {
53
+ const status = (0, child_process_1.execSync)('git status --porcelain', { encoding: 'utf8' }).trim();
54
+ if (!status)
55
+ return [];
56
+ return status.split('\n').map(line => line.slice(3).trim()).filter(f => f);
57
+ }
58
+ catch {
59
+ return [];
60
+ }
61
+ }
62
+ function getBranchTrackingInfo() {
63
+ try {
64
+ const remote = (0, child_process_1.execSync)('git config --get branch.$(git rev-parse --abbrev-ref HEAD).remote', { encoding: 'utf8' }).trim();
65
+ const upstream = (0, child_process_1.execSync)('git config --get branch.$(git rev-parse --abbrev-ref HEAD).merge', { encoding: 'utf8' }).trim();
66
+ if (!remote || !upstream)
67
+ return null;
68
+ return { remote, upstream: upstream.replace('refs/heads/', '') };
69
+ }
70
+ catch {
71
+ return null;
72
+ }
73
+ }
74
+ function createBundle(branch = 'HEAD') {
75
+ const tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'lazypush-'));
76
+ const bundlePath = path_1.default.join(tmpDir, 'repo.bundle');
77
+ try {
78
+ // Create bundle with the branch name to preserve refs/heads/<branch>
79
+ (0, child_process_1.execSync)(`git bundle create "${bundlePath}" ${branch}`, { stdio: 'pipe' });
80
+ if (!fs_1.default.existsSync(bundlePath))
81
+ throw new Error('Bundle not created');
82
+ return bundlePath;
83
+ }
84
+ catch (e) {
85
+ fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
86
+ throw new Error(`Failed to create bundle: ${e}`);
87
+ }
88
+ }
89
+ function compressBundle(bundlePath) {
90
+ const gzPath = bundlePath + '.gz';
91
+ try {
92
+ const bundle = fs_1.default.readFileSync(bundlePath);
93
+ const compressed = zlib_1.default.gzipSync(bundle);
94
+ fs_1.default.writeFileSync(gzPath, compressed);
95
+ return gzPath;
96
+ }
97
+ catch (e) {
98
+ throw new Error(`Compression failed: ${e}`);
99
+ }
100
+ }
101
+ function bundleToBase64(gzPath) {
102
+ const data = fs_1.default.readFileSync(gzPath);
103
+ return data.toString('base64');
104
+ }
105
+ function cleanupBundle(bundlePath, gzPath) {
106
+ try {
107
+ const tmpDir = path_1.default.dirname(bundlePath);
108
+ fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
109
+ }
110
+ catch { }
111
+ }
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.parseScheduleTime = parseScheduleTime;
7
+ const dayjs_1 = __importDefault(require("dayjs"));
8
+ const utc_1 = __importDefault(require("dayjs/plugin/utc"));
9
+ const timezone_1 = __importDefault(require("dayjs/plugin/timezone"));
10
+ dayjs_1.default.extend(utc_1.default);
11
+ dayjs_1.default.extend(timezone_1.default);
12
+ function parseScheduleTime(input, timezone) {
13
+ const lower = input.toLowerCase().trim();
14
+ let target = timezone ? (0, dayjs_1.default)().tz(timezone) : (0, dayjs_1.default)();
15
+ if (lower === 'now') {
16
+ return target.utc().toDate();
17
+ }
18
+ if (lower.startsWith('in ')) {
19
+ const match = lower.match(/^in (\d+)\s*(hour|minute|day)s?$/);
20
+ if (match) {
21
+ const [, num, unit] = match;
22
+ const amount = parseInt(num, 10);
23
+ if (unit === 'hour')
24
+ target = target.add(amount, 'hour');
25
+ else if (unit === 'minute')
26
+ target = target.add(amount, 'minute');
27
+ else if (unit === 'day')
28
+ target = target.add(amount, 'day');
29
+ return target.utc().toDate();
30
+ }
31
+ }
32
+ const timeMatch = lower.match(/(\d{1,2})(?::(\d{2}))?(\s)?(am|pm)?/);
33
+ if (timeMatch) {
34
+ let [, hour, minutes, , ampm] = timeMatch;
35
+ let h = parseInt(hour, 10);
36
+ let m = minutes ? parseInt(minutes, 10) : 0;
37
+ if (ampm === 'pm' && h !== 12)
38
+ h += 12;
39
+ if (ampm === 'am' && h === 12)
40
+ h = 0;
41
+ const dayMatch = lower.match(/^(monday|tuesday|wednesday|thursday|friday|saturday|sunday)\s+/i);
42
+ if (dayMatch) {
43
+ const dayName = dayMatch[1].toLowerCase();
44
+ const dayMap = {
45
+ monday: 1, tuesday: 2, wednesday: 3, thursday: 4,
46
+ friday: 5, saturday: 6, sunday: 0
47
+ };
48
+ const targetDay = dayMap[dayName];
49
+ const today = target.day();
50
+ let daysAhead = targetDay - today;
51
+ if (daysAhead <= 0)
52
+ daysAhead += 7;
53
+ target = target.add(daysAhead, 'day');
54
+ }
55
+ else if (lower.startsWith('tomorrow')) {
56
+ target = target.add(1, 'day');
57
+ }
58
+ target = target.hour(h).minute(m).second(0);
59
+ const now = timezone ? (0, dayjs_1.default)().tz(timezone) : (0, dayjs_1.default)();
60
+ if (target.isBefore(now)) {
61
+ target = target.add(1, 'day');
62
+ }
63
+ return target.utc().toDate();
64
+ }
65
+ throw new Error(`Cannot parse time: ${input}. Try: "5pm", "5:47pm", "17:30", "tomorrow 9am", "in 2 hours", "friday 8pm"`);
66
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "lazypush-cli",
3
+ "version": "0.1.0",
4
+ "description": "Schedule Git commits for future push",
5
+ "main": "dist/cli.js",
6
+ "bin": {
7
+ "lazypush": "dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "dev": "ts-node-dev --respawn --transpile-only src/cli.ts",
11
+ "build": "tsc -p tsconfig.json",
12
+ "link": "tsc -p tsconfig.json && npm link",
13
+ "prepublishOnly": "npm run build"
14
+ },
15
+ "preferGlobal": true,
16
+ "dependencies": {
17
+ "axios": "^1.4.0",
18
+ "chalk": "^4.1.2",
19
+ "commander": "^9.4.1",
20
+ "dayjs": "^1.11.9",
21
+ "inquirer": "^8.2.5",
22
+ "open": "^8.4.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/inquirer": "^8.2.12",
26
+ "@types/node": "^20.0.0",
27
+ "ts-node-dev": "^2.0.0",
28
+ "typescript": "^5.0.0"
29
+ }
30
+ }
@@ -0,0 +1,76 @@
1
+ import axios, { AxiosInstance } from 'axios';
2
+ import { getAuthToken } from '../config';
3
+
4
+ const DEFAULT_API = process.env.LAZYPUSH_API || 'https://lazypush.onrender.com';
5
+
6
+ export class ApiClient {
7
+ private client: AxiosInstance;
8
+
9
+ constructor(apiUrl: string = DEFAULT_API) {
10
+ this.client = axios.create({
11
+ baseURL: apiUrl,
12
+ timeout: 30000
13
+ });
14
+ }
15
+
16
+ async login(code: string) {
17
+ const res = await this.client.post('/auth/callback', { code });
18
+ return res.data;
19
+ }
20
+
21
+ async getLoginUrl(): Promise<string> {
22
+ const res = await this.client.get('/auth/github');
23
+ return res.request.path || '/auth/github';
24
+ }
25
+
26
+ async scheduleJob(data: {
27
+ repoUrl: string;
28
+ branch: string;
29
+ scheduledAt: Date;
30
+ bundleBase64: string;
31
+ commitMessage?: string;
32
+ }) {
33
+ const token = getAuthToken();
34
+ if (!token) throw new Error('Not logged in');
35
+
36
+ const res = await this.client.post(
37
+ '/schedule',
38
+ data,
39
+ { headers: { Authorization: `Bearer ${token}` } }
40
+ );
41
+ return res.data;
42
+ }
43
+
44
+ async listJobs() {
45
+ const token = getAuthToken();
46
+ if (!token) throw new Error('Not logged in');
47
+
48
+ const res = await this.client.get('/schedule', {
49
+ headers: { Authorization: `Bearer ${token}` }
50
+ });
51
+ return res.data;
52
+ }
53
+
54
+ async listAllJobs() {
55
+ const token = getAuthToken();
56
+ if (!token) throw new Error('Not logged in');
57
+
58
+ const res = await this.client.get('/schedule/list', {
59
+ headers: { Authorization: `Bearer ${token}` }
60
+ });
61
+ return res.data;
62
+ }
63
+
64
+ async cancelJob(id: string) {
65
+ const token = getAuthToken();
66
+ if (!token) throw new Error('Not logged in');
67
+
68
+ await this.client.delete(`/schedule/${id}`, {
69
+ headers: { Authorization: `Bearer ${token}` }
70
+ });
71
+ }
72
+ }
73
+
74
+ export function getApiClient(): ApiClient {
75
+ return new ApiClient();
76
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { program } from 'commander';
4
+ import { handleLogin } from './commands/login';
5
+ import { handleSchedule, handleScheduleInteractive } from './commands/schedule';
6
+ import { handleJobs, handleCancel, handleList } from './commands/jobs';
7
+ import { handleLogout } from './commands/logout';
8
+
9
+ program
10
+ .name('lazypush')
11
+ .description('Schedule Git commits for future push')
12
+ .version('0.1.0');
13
+
14
+ program
15
+ .command('login')
16
+ .description('Authenticate with GitHub')
17
+ .action(handleLogin);
18
+
19
+ program
20
+ .command('schedule')
21
+ .option('-a, --at <time>', 'Time to push (required). Examples: "5pm", "tomorrow 9am", "in 2 hours", "friday 8pm"')
22
+ .option('-t, --tz <timezone>', 'Timezone (default: local). Examples: IST, EST, UTC, America/New_York')
23
+ .option('-m, --message <msg>', 'Commit message (optional)')
24
+ .description('Schedule a push')
25
+ .action(async (opts: any) => {
26
+ if (!opts.at) {
27
+ await handleScheduleInteractive();
28
+ } else {
29
+ await handleSchedule(opts.at, opts.tz, opts.message);
30
+ }
31
+ });
32
+
33
+ program
34
+ .command('jobs')
35
+ .description('List scheduled jobs')
36
+ .action(async () => handleJobs());
37
+
38
+ program
39
+ .command('list')
40
+ .description('List all scheduled and finished jobs (latest first)')
41
+ .action(async () => handleList());
42
+
43
+ program
44
+ .command('cancel <id>')
45
+ .description('Cancel a scheduled job')
46
+ .action(async (id: string) => {
47
+ await handleCancel(id);
48
+ });
49
+
50
+ program
51
+ .command('logout')
52
+ .description('Logout and clear session')
53
+ .action(handleLogout);
54
+
55
+ program
56
+ .command('help')
57
+ .description('Show command help')
58
+ .action(() => {
59
+ console.log('LazyPush CLI');
60
+ console.log('');
61
+ console.log('Commands:');
62
+ console.log(' login Authenticate with GitHub');
63
+ console.log(' schedule Schedule a push (interactive or flags)');
64
+ console.log(' jobs List your scheduled jobs');
65
+ console.log(' list List all scheduled and finished jobs');
66
+ console.log(' cancel <id> Cancel a scheduled job');
67
+ console.log(' logout Logout and clear session');
68
+ console.log(' help Show this help');
69
+ console.log('');
70
+ console.log('Examples:');
71
+ console.log(' lazypush schedule');
72
+ console.log(' lazypush schedule --at "tomorrow 9am"');
73
+ console.log(' lazypush jobs');
74
+ console.log(' lazypush list');
75
+ });
76
+
77
+ program.parse(process.argv);
78
+
79
+ if (process.argv.length < 3) {
80
+ program.help();
81
+ }