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.
package/README.md ADDED
@@ -0,0 +1,194 @@
1
+ # lazyload-cli
2
+
3
+ <div align="center">
4
+
5
+ [![npm version](https://img.shields.io/npm/v/lazyload-cli?color=3fb950&label=lazyload-cli&logo=npm&style=flat-square)](https://www.npmjs.com/package/lazyload-cli)
6
+ [![npm downloads](https://img.shields.io/npm/dm/lazyload-cli?color=58a6ff&style=flat-square)](https://www.npmjs.com/package/lazyload-cli)
7
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen?style=flat-square&logo=node.js)](https://nodejs.org)
8
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green?style=flat-square)](../LICENSE)
9
+
10
+ *Schedule Git commits for a future push — no local daemons, no open terminals, no guilt.*
11
+
12
+ </div>
13
+
14
+ ---
15
+
16
+ ## The Problem
17
+
18
+ You finished the feature at 2pm. The standup is at 10am tomorrow. Pushing now means everyone knows you had the afternoon free, and guess who gets the next ticket assigned in 4 minutes?
19
+
20
+ lazyload lets you schedule the push for later. Your git history stays clean. Your commit timestamp is exactly what you want it to be. You get your afternoon back.
21
+
22
+ ---
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ npm install -g lazyload-cli
28
+ ```
29
+
30
+ ---
31
+
32
+ ## Quickstart
33
+
34
+ ```bash
35
+ # Step 1: one-time login
36
+ lazyload login
37
+
38
+ # Step 2: schedule a push from inside your git repo
39
+ lazyload schedule
40
+ ```
41
+
42
+ You'll see an interactive prompt:
43
+
44
+ ```
45
+ ? Date (dd/mm/yyyy format, or press Enter for today) 20/05/2026
46
+ ? Time (e.g., 5:30pm, 17:30, 9:15am) 9:00pm
47
+ ? Timezone (or press Enter for local) IST
48
+ ℹ Local timezone: Asia/Calcutta
49
+
50
+ ? Commit message (optional) fix: handle null user
51
+
52
+ ℹ ═══ PUSH DETAILS ═══
53
+ ℹ Repository: https://github.com/you/your-repo
54
+ ℹ Branch: main
55
+ ℹ Commit Message: fix: handle null user
56
+ ℹ ═══════════════════
57
+
58
+ ? Confirm scheduling? Yes
59
+
60
+ ℹ Parsing scheduled time...
61
+ ℹ Scheduled for: 5/20/2026, 9:00:00 PM UTC
62
+ ℹ Creating git bundle...
63
+ ℹ Compressing bundle...
64
+ ℹ Encoding to base64...
65
+ ℹ Uploading to backend...
66
+ ✓ Job scheduled! ID: 6a0dd278c2dcaedb805baf3c
67
+ ℹ Push will occur at: 5/20/2026, 9:00:00 PM
68
+ ```
69
+
70
+ Shut your laptop. The push will happen in the cloud at exactly that time.
71
+
72
+ ---
73
+
74
+ ## Commands
75
+
76
+ | Command | Description |
77
+ |---------|-------------|
78
+ | `lazyload login` | Authenticate via GitHub OAuth. Opens browser, stores JWT locally. |
79
+ | `lazyload schedule` | Interactive prompt to schedule a push. Auto-detects repo and branch. |
80
+ | `lazyload jobs` | List all currently scheduled (pending) jobs. |
81
+ | `lazyload list` | List all jobs — scheduled and finished — latest first. |
82
+ | `lazyload cancel <id>` | Cancel a scheduled job by its ID. |
83
+ | `lazyload logout` | Clear local session. |
84
+ | `lazyload help` | Show all commands and options. |
85
+
86
+ ---
87
+
88
+ ## Time Format
89
+
90
+ The time prompt accepts natural human input:
91
+
92
+ | Input | Meaning |
93
+ |-------|---------|
94
+ | `5pm` | Today at 5:00 PM |
95
+ | `17:30` | Today at 5:30 PM (24h) |
96
+ | `tomorrow 9am` | Next day at 9:00 AM |
97
+ | `in 2 hours` | 2 hours from now |
98
+ | `friday 8pm` | Coming Friday at 8:00 PM |
99
+
100
+ Timezone examples: `IST`, `EST`, `UTC`, `America/New_York`, `Asia/Kolkata`
101
+
102
+ If no timezone is provided, your system's local timezone is used.
103
+
104
+ ---
105
+
106
+ ## How It Works
107
+
108
+ ```
109
+ Inside your repo:
110
+ ┌──────────────────────────────────────────────────────────────┐
111
+ │ lazyload schedule │
112
+ │ │
113
+ │ 1. git bundle create repo.bundle HEAD │
114
+ │ 2. gzip repo.bundle │
115
+ │ 3. base64 encode │
116
+ │ 4. POST /jobs { bundle, scheduledAt, repoUrl, branch } │
117
+ └──────────────────────────────────────────────────────────────┘
118
+
119
+
120
+ LazyPush Backend (cloud)
121
+ Stores job → polls every 60s
122
+ At T: restores bundle, rewrites timestamps, pushes
123
+ ```
124
+
125
+ No background process runs on your machine after `schedule` completes. The CLI's job ends the moment the upload finishes.
126
+
127
+ ---
128
+
129
+ ## Session Storage
130
+
131
+ After `lazyload login`, credentials are stored at:
132
+
133
+ ```
134
+ ~/.lazyload/session.json
135
+ ```
136
+
137
+ ```json
138
+ {
139
+ "token": "<jwt>",
140
+ "userId": "...",
141
+ "username": "vaibhavgupta5"
142
+ }
143
+ ```
144
+
145
+ Protected by OS file permissions. Run `lazyload logout` to clear it.
146
+
147
+ ---
148
+
149
+ ## Source Structure
150
+
151
+ ```
152
+ src/
153
+ ├── cli.ts # Entry point — command registration via Commander.js
154
+ ├── config.ts # Session read/write (~/.lazyload/session.json)
155
+ ├── logger.ts # Colored terminal output (✓ green, ✗ red, ℹ blue)
156
+ ├── api/
157
+ │ └── client.ts # Authenticated HTTP client to backend
158
+ ├── commands/
159
+ │ ├── login.ts # OAuth flow + token storage
160
+ │ ├── schedule.ts # Interactive prompt + bundle upload
161
+ │ ├── jobs.ts # Fetch and display active jobs
162
+ │ ├── list.ts # Fetch and display all jobs
163
+ │ ├── cancel.ts # Cancel job by ID
164
+ │ └── logout.ts # Clear session
165
+ └── utils/
166
+ ├── git.ts # git bundle, gzip, base64
167
+ └── scheduler.ts # Human-readable time parsing
168
+ ```
169
+
170
+ ---
171
+
172
+ ## Development
173
+
174
+ ```bash
175
+ npm install
176
+ npm run dev # ts-node-dev with auto-reload
177
+
178
+ # link locally to test as a global command
179
+ npm link
180
+ lazyload login
181
+ ```
182
+
183
+ ## Build & Publish
184
+
185
+ ```bash
186
+ npm run build # tsc → dist/
187
+ npm publish --access public
188
+ ```
189
+
190
+ ---
191
+
192
+ ## License
193
+
194
+ MIT © [Vaibhav Gupta](https://github.com/vaibhavgupta5)
@@ -0,0 +1,63 @@
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.ApiClient = void 0;
7
+ exports.getApiClient = getApiClient;
8
+ const axios_1 = __importDefault(require("axios"));
9
+ const config_1 = require("../config");
10
+ const DEFAULT_API = process.env.LAZYPUSH_API || 'https://lazypush.onrender.com';
11
+ class ApiClient {
12
+ constructor(apiUrl = DEFAULT_API) {
13
+ this.client = axios_1.default.create({
14
+ baseURL: apiUrl,
15
+ timeout: 30000
16
+ });
17
+ }
18
+ async login(code) {
19
+ const res = await this.client.post('/auth/callback', { code });
20
+ return res.data;
21
+ }
22
+ async getLoginUrl() {
23
+ const res = await this.client.get('/auth/github');
24
+ return res.request.path || '/auth/github';
25
+ }
26
+ async scheduleJob(data) {
27
+ const token = (0, config_1.getAuthToken)();
28
+ if (!token)
29
+ throw new Error('Not logged in');
30
+ const res = await this.client.post('/schedule', data, { headers: { Authorization: `Bearer ${token}` } });
31
+ return res.data;
32
+ }
33
+ async listJobs() {
34
+ const token = (0, config_1.getAuthToken)();
35
+ if (!token)
36
+ throw new Error('Not logged in');
37
+ const res = await this.client.get('/schedule', {
38
+ headers: { Authorization: `Bearer ${token}` }
39
+ });
40
+ return res.data;
41
+ }
42
+ async listAllJobs() {
43
+ const token = (0, config_1.getAuthToken)();
44
+ if (!token)
45
+ throw new Error('Not logged in');
46
+ const res = await this.client.get('/schedule/list', {
47
+ headers: { Authorization: `Bearer ${token}` }
48
+ });
49
+ return res.data;
50
+ }
51
+ async cancelJob(id) {
52
+ const token = (0, config_1.getAuthToken)();
53
+ if (!token)
54
+ throw new Error('Not logged in');
55
+ await this.client.delete(`/schedule/${id}`, {
56
+ headers: { Authorization: `Bearer ${token}` }
57
+ });
58
+ }
59
+ }
60
+ exports.ApiClient = ApiClient;
61
+ function getApiClient() {
62
+ return new ApiClient();
63
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const commander_1 = require("commander");
5
+ const login_1 = require("./commands/login");
6
+ const schedule_1 = require("./commands/schedule");
7
+ const jobs_1 = require("./commands/jobs");
8
+ const logout_1 = require("./commands/logout");
9
+ commander_1.program
10
+ .name('lazypush')
11
+ .description('Schedule Git commits for future push')
12
+ .version('0.1.0');
13
+ commander_1.program
14
+ .command('login')
15
+ .description('Authenticate with GitHub')
16
+ .action(login_1.handleLogin);
17
+ commander_1.program
18
+ .command('schedule')
19
+ .option('-a, --at <time>', 'Time to push (required). Examples: "5pm", "tomorrow 9am", "in 2 hours", "friday 8pm"')
20
+ .option('-t, --tz <timezone>', 'Timezone (default: local). Examples: IST, EST, UTC, America/New_York')
21
+ .option('-m, --message <msg>', 'Commit message (optional)')
22
+ .description('Schedule a push')
23
+ .action(async (opts) => {
24
+ if (!opts.at) {
25
+ await (0, schedule_1.handleScheduleInteractive)();
26
+ }
27
+ else {
28
+ await (0, schedule_1.handleSchedule)(opts.at, opts.tz, opts.message);
29
+ }
30
+ });
31
+ commander_1.program
32
+ .command('jobs')
33
+ .description('List scheduled jobs')
34
+ .action(async () => (0, jobs_1.handleJobs)());
35
+ commander_1.program
36
+ .command('list')
37
+ .description('List all scheduled and finished jobs (latest first)')
38
+ .action(async () => (0, jobs_1.handleList)());
39
+ commander_1.program
40
+ .command('cancel <id>')
41
+ .description('Cancel a scheduled job')
42
+ .action(async (id) => {
43
+ await (0, jobs_1.handleCancel)(id);
44
+ });
45
+ commander_1.program
46
+ .command('logout')
47
+ .description('Logout and clear session')
48
+ .action(logout_1.handleLogout);
49
+ commander_1.program
50
+ .command('help')
51
+ .description('Show command help')
52
+ .action(() => {
53
+ console.log('LazyPush CLI');
54
+ console.log('');
55
+ console.log('Commands:');
56
+ console.log(' login Authenticate with GitHub');
57
+ console.log(' schedule Schedule a push (interactive or flags)');
58
+ console.log(' jobs List your scheduled jobs');
59
+ console.log(' list List all scheduled and finished jobs');
60
+ console.log(' cancel <id> Cancel a scheduled job');
61
+ console.log(' logout Logout and clear session');
62
+ console.log(' help Show this help');
63
+ console.log('');
64
+ console.log('Examples:');
65
+ console.log(' lazypush schedule');
66
+ console.log(' lazypush schedule --at "tomorrow 9am"');
67
+ console.log(' lazypush jobs');
68
+ console.log(' lazypush list');
69
+ });
70
+ commander_1.program.parse(process.argv);
71
+ if (process.argv.length < 3) {
72
+ commander_1.program.help();
73
+ }
@@ -0,0 +1,84 @@
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.handleJobs = handleJobs;
7
+ exports.handleCancel = handleCancel;
8
+ exports.handleList = handleList;
9
+ const client_1 = require("../api/client");
10
+ const logger_1 = require("../logger");
11
+ const chalk_1 = __importDefault(require("chalk"));
12
+ const auth_1 = require("../utils/auth");
13
+ async function handleJobs() {
14
+ if (!(await (0, auth_1.requireAuth)())) {
15
+ process.exit(1);
16
+ }
17
+ try {
18
+ const api = (0, client_1.getApiClient)();
19
+ const jobs = await api.listJobs();
20
+ if (jobs.length === 0) {
21
+ (0, logger_1.info)('No scheduled jobs');
22
+ return;
23
+ }
24
+ (0, logger_1.log)('');
25
+ (0, logger_1.log)(chalk_1.default.bold('Scheduled Jobs:'));
26
+ (0, logger_1.log)('');
27
+ jobs.forEach((job, i) => {
28
+ const status = chalk_1.default.gray(job.status);
29
+ const time = new Date(job.scheduledAt).toLocaleString();
30
+ const branch = chalk_1.default.cyan(job.branch);
31
+ (0, logger_1.log)(`${i + 1}. ${chalk_1.default.yellow(job._id)} [${status}]`);
32
+ (0, logger_1.log)(` Branch: ${branch} @ ${time}`);
33
+ });
34
+ (0, logger_1.log)('');
35
+ }
36
+ catch (e) {
37
+ (0, logger_1.error)(`Failed to fetch jobs: ${e.message}`);
38
+ process.exit(1);
39
+ }
40
+ }
41
+ async function handleCancel(id) {
42
+ if (!(await (0, auth_1.requireAuth)())) {
43
+ process.exit(1);
44
+ }
45
+ try {
46
+ const api = (0, client_1.getApiClient)();
47
+ await api.cancelJob(id);
48
+ (0, logger_1.success)(`Job ${id} cancelled`);
49
+ }
50
+ catch (e) {
51
+ (0, logger_1.error)(`Failed to cancel job: ${e.message}`);
52
+ process.exit(1);
53
+ }
54
+ }
55
+ async function handleList() {
56
+ if (!(await (0, auth_1.requireAuth)())) {
57
+ process.exit(1);
58
+ }
59
+ try {
60
+ const api = (0, client_1.getApiClient)();
61
+ const jobs = await api.listAllJobs();
62
+ if (jobs.length === 0) {
63
+ (0, logger_1.info)('No jobs found');
64
+ return;
65
+ }
66
+ (0, logger_1.log)('');
67
+ (0, logger_1.log)(chalk_1.default.bold('All Scheduled/Finished Jobs:'));
68
+ (0, logger_1.log)('');
69
+ jobs.forEach((job, i) => {
70
+ const status = chalk_1.default.gray(job.status);
71
+ const time = new Date(job.scheduledAt).toLocaleString();
72
+ const branch = chalk_1.default.cyan(job.branch);
73
+ const user = chalk_1.default.green(job.username || 'unknown');
74
+ (0, logger_1.log)(`${i + 1}. ${chalk_1.default.yellow(job._id)} [${status}]`);
75
+ (0, logger_1.log)(` User: ${user}`);
76
+ (0, logger_1.log)(` Branch: ${branch} @ ${time}`);
77
+ });
78
+ (0, logger_1.log)('');
79
+ }
80
+ catch (e) {
81
+ (0, logger_1.error)(`Failed to fetch jobs: ${e.message}`);
82
+ process.exit(1);
83
+ }
84
+ }
@@ -0,0 +1,126 @@
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.handleLogin = handleLogin;
7
+ const open_1 = __importDefault(require("open"));
8
+ const inquirer_1 = __importDefault(require("inquirer"));
9
+ const http_1 = require("http");
10
+ const config_1 = require("../config");
11
+ const logger_1 = require("../logger");
12
+ async function handleLogin() {
13
+ try {
14
+ const existing = (0, config_1.getSession)();
15
+ if (existing) {
16
+ (0, logger_1.success)(`Already logged in as: ${existing.username}`);
17
+ const { confirm } = await inquirer_1.default.prompt([
18
+ {
19
+ type: "confirm",
20
+ name: "confirm",
21
+ message: "Login again?",
22
+ default: false,
23
+ },
24
+ ]);
25
+ if (!confirm)
26
+ return;
27
+ }
28
+ const token = await waitForOAuthCallback();
29
+ if (!token) {
30
+ (0, logger_1.error)("Authentication cancelled");
31
+ process.exit(1);
32
+ }
33
+ (0, logger_1.info)("Verifying token...");
34
+ try {
35
+ const jwtPayload = parseJwt(token);
36
+ if (!jwtPayload.userId) {
37
+ throw new Error('Invalid token: missing userId');
38
+ }
39
+ const session = {
40
+ token,
41
+ userId: jwtPayload.userId,
42
+ username: jwtPayload.username || "user",
43
+ createdAt: new Date().toISOString(),
44
+ };
45
+ (0, config_1.saveSession)(session);
46
+ (0, logger_1.success)(`Logged in successfully as ${jwtPayload.username}!`);
47
+ const expiryDate = new Date(jwtPayload.exp * 1000);
48
+ (0, logger_1.info)(`Token will expire on: ${expiryDate.toLocaleString()}`);
49
+ }
50
+ catch (e) {
51
+ (0, logger_1.error)(`Token validation failed: ${e.message}`);
52
+ process.exit(1);
53
+ }
54
+ }
55
+ catch (e) {
56
+ (0, logger_1.error)(`Login failed: ${e.message}`);
57
+ process.exit(1);
58
+ }
59
+ }
60
+ async function waitForOAuthCallback() {
61
+ return new Promise((resolve) => {
62
+ let server = null;
63
+ const port = 3001;
64
+ const timeout = setTimeout(() => {
65
+ if (server)
66
+ server.close();
67
+ (0, logger_1.error)("Login timeout - no response from GitHub");
68
+ resolve(null);
69
+ }, 10 * 60 * 1000); // 10 minute timeout
70
+ server = (0, http_1.createServer)((req, res) => {
71
+ if (!req.url) {
72
+ res.writeHead(400);
73
+ res.end("Invalid request");
74
+ return;
75
+ }
76
+ const urlObj = new URL(`http://localhost${req.url}`);
77
+ const token = urlObj.searchParams.get("token");
78
+ const errorParam = urlObj.searchParams.get("error");
79
+ if (errorParam) {
80
+ res.writeHead(400);
81
+ res.end(`Authentication failed: ${errorParam}`);
82
+ if (server)
83
+ server.close();
84
+ clearTimeout(timeout);
85
+ resolve(null);
86
+ return;
87
+ }
88
+ if (token) {
89
+ res.writeHead(200);
90
+ res.end("Authentication successful! You can close this window.");
91
+ if (server)
92
+ server.close();
93
+ clearTimeout(timeout);
94
+ resolve(token);
95
+ return;
96
+ }
97
+ res.writeHead(400);
98
+ res.end("Missing token");
99
+ });
100
+ server.listen(port, () => {
101
+ (0, logger_1.info)("Opening browser for GitHub authentication...");
102
+ const apiUrl = process.env.LAZYPUSH_API || "https://lazypush.onrender.com";
103
+ const redirectUri = `http://localhost:${port}/auth/callback`;
104
+ const loginUrl = `${apiUrl}/auth/github?redirect_uri=${encodeURIComponent(redirectUri)}`;
105
+ (0, open_1.default)(loginUrl);
106
+ (0, logger_1.info)("Browser opened. If it did not open, visit:");
107
+ (0, logger_1.info)(loginUrl);
108
+ (0, logger_1.info)("");
109
+ });
110
+ });
111
+ }
112
+ function parseJwt(token) {
113
+ try {
114
+ const base64Url = token.split(".")[1];
115
+ const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
116
+ const jsonPayload = decodeURIComponent(atob(base64)
117
+ .split("")
118
+ .map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2))
119
+ .join(""));
120
+ return JSON.parse(jsonPayload);
121
+ }
122
+ catch {
123
+ return {};
124
+ }
125
+ process.exit(1);
126
+ }
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleLogout = handleLogout;
4
+ const config_1 = require("../config");
5
+ const logger_1 = require("../logger");
6
+ function handleLogout() {
7
+ try {
8
+ (0, config_1.clearSession)();
9
+ (0, logger_1.success)('Logged out');
10
+ }
11
+ catch (e) {
12
+ (0, logger_1.error)(`Logout failed: ${e.message}`);
13
+ process.exit(1);
14
+ }
15
+ }