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 +194 -0
- package/dist/api/client.js +63 -0
- package/dist/cli.js +73 -0
- package/dist/commands/jobs.js +84 -0
- package/dist/commands/login.js +126 -0
- package/dist/commands/logout.js +15 -0
- package/dist/commands/schedule.js +229 -0
- package/dist/config.js +43 -0
- package/dist/logger.js +26 -0
- package/dist/utils/auth.js +16 -0
- package/dist/utils/git.js +111 -0
- package/dist/utils/scheduler.js +66 -0
- package/package.json +30 -0
- package/src/api/client.ts +76 -0
- package/src/cli.ts +81 -0
- package/src/commands/jobs.ts +87 -0
- package/src/commands/login.ts +131 -0
- package/src/commands/logout.ts +12 -0
- package/src/commands/schedule.ts +241 -0
- package/src/config.ts +45 -0
- package/src/logger.ts +21 -0
- package/src/utils/auth.ts +15 -0
- package/src/utils/git.ts +93 -0
- package/src/utils/scheduler.ts +61 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { getApiClient } from '../api/client';
|
|
2
|
+
import { info, success, error, log } from '../logger';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { requireAuth } from '../utils/auth';
|
|
5
|
+
|
|
6
|
+
export async function handleJobs() {
|
|
7
|
+
if (!(await requireAuth())) {
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const api = getApiClient();
|
|
13
|
+
const jobs = await api.listJobs();
|
|
14
|
+
|
|
15
|
+
if (jobs.length === 0) {
|
|
16
|
+
info('No scheduled jobs');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
log('');
|
|
21
|
+
log(chalk.bold('Scheduled Jobs:'));
|
|
22
|
+
log('');
|
|
23
|
+
|
|
24
|
+
jobs.forEach((job: any, i: number) => {
|
|
25
|
+
const status = chalk.gray(job.status);
|
|
26
|
+
const time = new Date(job.scheduledAt).toLocaleString();
|
|
27
|
+
const branch = chalk.cyan(job.branch);
|
|
28
|
+
log(`${i + 1}. ${chalk.yellow(job._id)} [${status}]`);
|
|
29
|
+
log(` Branch: ${branch} @ ${time}`);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
log('');
|
|
33
|
+
} catch (e: any) {
|
|
34
|
+
error(`Failed to fetch jobs: ${e.message}`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function handleCancel(id: string) {
|
|
40
|
+
if (!(await requireAuth())) {
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const api = getApiClient();
|
|
46
|
+
await api.cancelJob(id);
|
|
47
|
+
success(`Job ${id} cancelled`);
|
|
48
|
+
} catch (e: any) {
|
|
49
|
+
error(`Failed to cancel job: ${e.message}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function handleList() {
|
|
55
|
+
if (!(await requireAuth())) {
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const api = getApiClient();
|
|
61
|
+
const jobs = await api.listAllJobs();
|
|
62
|
+
|
|
63
|
+
if (jobs.length === 0) {
|
|
64
|
+
info('No jobs found');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
log('');
|
|
69
|
+
log(chalk.bold('All Scheduled/Finished Jobs:'));
|
|
70
|
+
log('');
|
|
71
|
+
|
|
72
|
+
jobs.forEach((job: any, i: number) => {
|
|
73
|
+
const status = chalk.gray(job.status);
|
|
74
|
+
const time = new Date(job.scheduledAt).toLocaleString();
|
|
75
|
+
const branch = chalk.cyan(job.branch);
|
|
76
|
+
const user = chalk.green(job.username || 'unknown');
|
|
77
|
+
log(`${i + 1}. ${chalk.yellow(job._id)} [${status}]`);
|
|
78
|
+
log(` User: ${user}`);
|
|
79
|
+
log(` Branch: ${branch} @ ${time}`);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
log('');
|
|
83
|
+
} catch (e: any) {
|
|
84
|
+
error(`Failed to fetch jobs: ${e.message}`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import open from "open";
|
|
2
|
+
import inquirer from "inquirer";
|
|
3
|
+
import { createServer, Server } from "http";
|
|
4
|
+
import { saveSession, getSession } from "../config";
|
|
5
|
+
import { info, success, error } from "../logger";
|
|
6
|
+
|
|
7
|
+
export async function handleLogin() {
|
|
8
|
+
try {
|
|
9
|
+
const existing = getSession();
|
|
10
|
+
if (existing) {
|
|
11
|
+
success(`Already logged in as: ${existing.username}`);
|
|
12
|
+
const { confirm } = await inquirer.prompt([
|
|
13
|
+
{
|
|
14
|
+
type: "confirm",
|
|
15
|
+
name: "confirm",
|
|
16
|
+
message: "Login again?",
|
|
17
|
+
default: false,
|
|
18
|
+
},
|
|
19
|
+
]);
|
|
20
|
+
if (!confirm) return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const token = await waitForOAuthCallback();
|
|
24
|
+
|
|
25
|
+
if (!token) {
|
|
26
|
+
error("Authentication cancelled");
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
info("Verifying token...");
|
|
31
|
+
try {
|
|
32
|
+
const jwtPayload = parseJwt(token);
|
|
33
|
+
if (!jwtPayload.userId) {
|
|
34
|
+
throw new Error('Invalid token: missing userId');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const session = {
|
|
38
|
+
token,
|
|
39
|
+
userId: jwtPayload.userId,
|
|
40
|
+
username: jwtPayload.username || "user",
|
|
41
|
+
createdAt: new Date().toISOString(),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
saveSession(session);
|
|
45
|
+
success(`Logged in successfully as ${jwtPayload.username}!`);
|
|
46
|
+
const expiryDate = new Date(jwtPayload.exp * 1000);
|
|
47
|
+
info(`Token will expire on: ${expiryDate.toLocaleString()}`);
|
|
48
|
+
} catch (e: any) {
|
|
49
|
+
error(`Token validation failed: ${e.message}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
} catch (e: any) {
|
|
53
|
+
error(`Login failed: ${e.message}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function waitForOAuthCallback(): Promise<string | null> {
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
let server: Server | null = null;
|
|
61
|
+
const port = 3001;
|
|
62
|
+
const timeout = setTimeout(() => {
|
|
63
|
+
if (server) server.close();
|
|
64
|
+
error("Login timeout - no response from GitHub");
|
|
65
|
+
resolve(null);
|
|
66
|
+
}, 10 * 60 * 1000); // 10 minute timeout
|
|
67
|
+
|
|
68
|
+
server = createServer((req, res) => {
|
|
69
|
+
if (!req.url) {
|
|
70
|
+
res.writeHead(400);
|
|
71
|
+
res.end("Invalid request");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
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
|
+
|
|
80
|
+
if (errorParam) {
|
|
81
|
+
res.writeHead(400);
|
|
82
|
+
res.end(`Authentication failed: ${errorParam}`);
|
|
83
|
+
if (server) server.close();
|
|
84
|
+
clearTimeout(timeout);
|
|
85
|
+
resolve(null);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (token) {
|
|
90
|
+
res.writeHead(200);
|
|
91
|
+
res.end("Authentication successful! You can close this window.");
|
|
92
|
+
if (server) server.close();
|
|
93
|
+
clearTimeout(timeout);
|
|
94
|
+
resolve(token);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
res.writeHead(400);
|
|
99
|
+
res.end("Missing token");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
server.listen(port, () => {
|
|
103
|
+
info("Opening browser for GitHub authentication...");
|
|
104
|
+
const apiUrl = process.env.LAZYPUSH_API || "https://lazypush.onrender.com";
|
|
105
|
+
const redirectUri = `http://localhost:${port}/auth/callback`;
|
|
106
|
+
const loginUrl = `${apiUrl}/auth/github?redirect_uri=${encodeURIComponent(redirectUri)}`;
|
|
107
|
+
|
|
108
|
+
open(loginUrl);
|
|
109
|
+
info("Browser opened. If it did not open, visit:");
|
|
110
|
+
info(loginUrl);
|
|
111
|
+
info("");
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function parseJwt(token: string) {
|
|
117
|
+
try {
|
|
118
|
+
const base64Url = token.split(".")[1];
|
|
119
|
+
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
|
120
|
+
const jsonPayload = decodeURIComponent(
|
|
121
|
+
atob(base64)
|
|
122
|
+
.split("")
|
|
123
|
+
.map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2))
|
|
124
|
+
.join(""),
|
|
125
|
+
);
|
|
126
|
+
return JSON.parse(jsonPayload);
|
|
127
|
+
} catch {
|
|
128
|
+
return {};
|
|
129
|
+
}
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { clearSession } from '../config';
|
|
2
|
+
import { success, error } from '../logger';
|
|
3
|
+
|
|
4
|
+
export function handleLogout() {
|
|
5
|
+
try {
|
|
6
|
+
clearSession();
|
|
7
|
+
success('Logged out');
|
|
8
|
+
} catch (e: any) {
|
|
9
|
+
error(`Logout failed: ${e.message}`);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import dayjs from 'dayjs';
|
|
3
|
+
import { getApiClient } from '../api/client';
|
|
4
|
+
import { info, success, error, warn } from '../logger';
|
|
5
|
+
import { requireAuth } from '../utils/auth';
|
|
6
|
+
import { getRepoUrl, getBranch, createBundle, compressBundle, bundleToBase64, cleanupBundle, hasUncommittedChanges, getUncommittedFiles, getBranchTrackingInfo } from '../utils/git';
|
|
7
|
+
import { parseScheduleTime } from '../utils/scheduler';
|
|
8
|
+
|
|
9
|
+
export async function handleScheduleInteractive() {
|
|
10
|
+
if (!(await requireAuth())) {
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
// Pre-flight checks
|
|
16
|
+
info('═══ PRE-FLIGHT CHECKS ═══');
|
|
17
|
+
info('Detecting repository...');
|
|
18
|
+
const repoUrl = getRepoUrl();
|
|
19
|
+
const branch = getBranch();
|
|
20
|
+
info(`Repository: ${repoUrl}`);
|
|
21
|
+
info(`Branch: ${branch}`);
|
|
22
|
+
info('');
|
|
23
|
+
|
|
24
|
+
// Check for uncommitted changes
|
|
25
|
+
if (hasUncommittedChanges()) {
|
|
26
|
+
const files = getUncommittedFiles();
|
|
27
|
+
warn(`Found ${files.length} uncommitted file(s):`);
|
|
28
|
+
files.forEach(f => info(` - ${f}`));
|
|
29
|
+
info('');
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
require('child_process').execSync('git add .', { stdio: 'inherit' });
|
|
33
|
+
success('Files staged for commit');
|
|
34
|
+
const staged = require('child_process')
|
|
35
|
+
.execSync('git diff --name-only --cached', { encoding: 'utf8' })
|
|
36
|
+
.trim();
|
|
37
|
+
if (staged) {
|
|
38
|
+
info('Staged files:');
|
|
39
|
+
staged.split('\n').forEach((f: string) => info(` - ${f}`));
|
|
40
|
+
} else {
|
|
41
|
+
warn('No staged files found');
|
|
42
|
+
}
|
|
43
|
+
info('');
|
|
44
|
+
} catch (e) {
|
|
45
|
+
error('Failed to stage files');
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
success('Working directory clean');
|
|
50
|
+
info('');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check branch tracking
|
|
54
|
+
const tracking = getBranchTrackingInfo();
|
|
55
|
+
if (tracking) {
|
|
56
|
+
info(`Remote: ${tracking.remote}/${tracking.upstream}`);
|
|
57
|
+
} else {
|
|
58
|
+
warn(`No upstream tracking set for branch "${branch}"`);
|
|
59
|
+
info('Note: Will push to origin');
|
|
60
|
+
info('');
|
|
61
|
+
}
|
|
62
|
+
info('═══════════════════════');
|
|
63
|
+
info('');
|
|
64
|
+
|
|
65
|
+
// Step 1: Date, time, timezone
|
|
66
|
+
const timeAnswers = await inquirer.prompt([
|
|
67
|
+
{
|
|
68
|
+
type: 'input',
|
|
69
|
+
name: 'date',
|
|
70
|
+
message: 'Date (dd/mm/yyyy format, or press Enter for today)',
|
|
71
|
+
default: () => dayjs().format('DD/MM/YYYY'),
|
|
72
|
+
validate: (input: string) => {
|
|
73
|
+
const parts = input.split('/');
|
|
74
|
+
if (parts.length !== 3) return 'Invalid format. Use dd/mm/yyyy';
|
|
75
|
+
const d = parseInt(parts[0], 10);
|
|
76
|
+
const m = parseInt(parts[1], 10);
|
|
77
|
+
const y = parseInt(parts[2], 10);
|
|
78
|
+
if (d < 1 || d > 31 || m < 1 || m > 12) return 'Invalid date';
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
type: 'input',
|
|
84
|
+
name: 'time',
|
|
85
|
+
message: 'Time (e.g., 5:30pm, 17:30, 9:15am)',
|
|
86
|
+
validate: (input: string) => {
|
|
87
|
+
const match = input.match(/^(\d{1,2})(?::(\d{2}))?\s?(am|pm)?$/i);
|
|
88
|
+
if (!match) return 'Invalid time format. Use: 5pm, 5:30pm, or 17:30';
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
type: 'input',
|
|
94
|
+
name: 'timezone',
|
|
95
|
+
message: 'Timezone (or press Enter for local)',
|
|
96
|
+
default: 'local'
|
|
97
|
+
}
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
// Show timezone info
|
|
101
|
+
const tz = timeAnswers.timezone === 'local' ? undefined : timeAnswers.timezone;
|
|
102
|
+
const tzDisplay = tz || dayjs.tz.guess() || 'UTC';
|
|
103
|
+
info(`Local timezone: ${tzDisplay}`);
|
|
104
|
+
info('');
|
|
105
|
+
|
|
106
|
+
// Step 2: Commit message
|
|
107
|
+
const messageAnswers = await inquirer.prompt([
|
|
108
|
+
{
|
|
109
|
+
type: 'input',
|
|
110
|
+
name: 'message',
|
|
111
|
+
message: 'Commit message (optional)',
|
|
112
|
+
default: ''
|
|
113
|
+
}
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
// Step 3: Show repo/branch and ask for confirmation
|
|
117
|
+
info('');
|
|
118
|
+
info('═══ PUSH DETAILS ═══');
|
|
119
|
+
info(`Repository: ${repoUrl}`);
|
|
120
|
+
info(`Branch: ${branch}`);
|
|
121
|
+
info(`Commit Message: ${messageAnswers.message || '(none)'}`);
|
|
122
|
+
info('═══════════════════');
|
|
123
|
+
info('');
|
|
124
|
+
|
|
125
|
+
const confirmAnswers = await inquirer.prompt([
|
|
126
|
+
{
|
|
127
|
+
type: 'confirm',
|
|
128
|
+
name: 'confirm',
|
|
129
|
+
message: 'Confirm scheduling?',
|
|
130
|
+
default: true
|
|
131
|
+
}
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
if (!confirmAnswers.confirm) {
|
|
135
|
+
info('Scheduling cancelled');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
info('Parsing scheduled time...');
|
|
140
|
+
const dateStr = timeAnswers.date;
|
|
141
|
+
const timeStr = timeAnswers.time;
|
|
142
|
+
|
|
143
|
+
const [day, month, year] = dateStr.split('/');
|
|
144
|
+
let scheduledAt: Date;
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
let target = dayjs(`${year}-${month}-${day}`);
|
|
148
|
+
if (tz) target = target.tz(tz);
|
|
149
|
+
|
|
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) h += 12;
|
|
156
|
+
if (ampm?.toLowerCase() === 'am' && h === 12) h = 0;
|
|
157
|
+
target = target.hour(h).minute(m).second(0);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
scheduledAt = (tz ? target.utc() : target).toDate();
|
|
161
|
+
} catch (e) {
|
|
162
|
+
throw new Error('Invalid date/time');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
info(`Scheduled for: ${scheduledAt.toLocaleString()} UTC`);
|
|
166
|
+
|
|
167
|
+
info('Creating git bundle...');
|
|
168
|
+
const bundlePath = createBundle(branch);
|
|
169
|
+
|
|
170
|
+
info('Compressing bundle...');
|
|
171
|
+
const gzPath = compressBundle(bundlePath);
|
|
172
|
+
|
|
173
|
+
info('Encoding to base64...');
|
|
174
|
+
const bundleBase64 = bundleToBase64(gzPath);
|
|
175
|
+
|
|
176
|
+
info('Uploading to backend...');
|
|
177
|
+
const api = getApiClient();
|
|
178
|
+
const job = await api.scheduleJob({
|
|
179
|
+
repoUrl,
|
|
180
|
+
branch,
|
|
181
|
+
scheduledAt,
|
|
182
|
+
bundleBase64,
|
|
183
|
+
commitMessage: messageAnswers.message || undefined
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
cleanupBundle(bundlePath, gzPath);
|
|
187
|
+
|
|
188
|
+
success(`Job scheduled! ID: ${job.id}`);
|
|
189
|
+
info(`Push will occur at: ${scheduledAt.toLocaleString()}`);
|
|
190
|
+
} catch (e: any) {
|
|
191
|
+
error(`Failed: ${e.message}`);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function handleSchedule(timeInput: string, timezone?: string, message?: string) {
|
|
197
|
+
if (!(await requireAuth())) {
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
info('Detecting repository...');
|
|
203
|
+
const repoUrl = getRepoUrl();
|
|
204
|
+
const branch = getBranch();
|
|
205
|
+
info(`Repository: ${repoUrl}`);
|
|
206
|
+
info(`Branch: ${branch}`);
|
|
207
|
+
|
|
208
|
+
info('Parsing scheduled time...');
|
|
209
|
+
const tz = timezone || 'UTC';
|
|
210
|
+
info(`Using timezone: ${tz}`);
|
|
211
|
+
const scheduledAt = parseScheduleTime(timeInput, tz);
|
|
212
|
+
info(`Scheduled for: ${scheduledAt.toLocaleString()} UTC`);
|
|
213
|
+
|
|
214
|
+
info('Creating git bundle...');
|
|
215
|
+
const bundlePath = createBundle(branch);
|
|
216
|
+
|
|
217
|
+
info('Compressing bundle...');
|
|
218
|
+
const gzPath = compressBundle(bundlePath);
|
|
219
|
+
|
|
220
|
+
info('Encoding to base64...');
|
|
221
|
+
const bundleBase64 = bundleToBase64(gzPath);
|
|
222
|
+
|
|
223
|
+
info('Uploading to backend...');
|
|
224
|
+
const api = getApiClient();
|
|
225
|
+
const job = await api.scheduleJob({
|
|
226
|
+
repoUrl,
|
|
227
|
+
branch,
|
|
228
|
+
scheduledAt,
|
|
229
|
+
bundleBase64,
|
|
230
|
+
commitMessage: message
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
cleanupBundle(bundlePath, gzPath);
|
|
234
|
+
|
|
235
|
+
success(`Job scheduled! ID: ${job.id}`);
|
|
236
|
+
info(`Push will occur at: ${scheduledAt.toLocaleString()}`);
|
|
237
|
+
} catch (e: any) {
|
|
238
|
+
error(`Failed: ${e.message}`);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR = path.join(os.homedir(), '.lazypush');
|
|
6
|
+
const SESSION_FILE = path.join(CONFIG_DIR, 'session.json');
|
|
7
|
+
|
|
8
|
+
export interface Session {
|
|
9
|
+
token: string;
|
|
10
|
+
userId: string;
|
|
11
|
+
username: string;
|
|
12
|
+
createdAt: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function ensureConfigDir() {
|
|
16
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
17
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function saveSession(session: Session) {
|
|
22
|
+
ensureConfigDir();
|
|
23
|
+
fs.writeFileSync(SESSION_FILE, JSON.stringify(session, null, 2), 'utf8');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getSession(): Session | null {
|
|
27
|
+
if (!fs.existsSync(SESSION_FILE)) return null;
|
|
28
|
+
try {
|
|
29
|
+
const data = fs.readFileSync(SESSION_FILE, 'utf8');
|
|
30
|
+
return JSON.parse(data);
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function clearSession() {
|
|
37
|
+
if (fs.existsSync(SESSION_FILE)) {
|
|
38
|
+
fs.unlinkSync(SESSION_FILE);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getAuthToken(): string | null {
|
|
43
|
+
const session = getSession();
|
|
44
|
+
return session?.token || null;
|
|
45
|
+
}
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
export function info(msg: string) {
|
|
4
|
+
console.log(chalk.blue('ℹ'), msg);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function success(msg: string) {
|
|
8
|
+
console.log(chalk.green('✓'), msg);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function error(msg: string) {
|
|
12
|
+
console.log(chalk.red('✗'), msg);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function warn(msg: string) {
|
|
16
|
+
console.log(chalk.yellow('⚠'), msg);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function log(msg: string) {
|
|
20
|
+
console.log(msg);
|
|
21
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { handleLogin } from '../commands/login';
|
|
2
|
+
import { getAuthToken } from '../config';
|
|
3
|
+
import { warn, info } from '../logger';
|
|
4
|
+
|
|
5
|
+
export async function requireAuth(): Promise<boolean> {
|
|
6
|
+
const token = getAuthToken();
|
|
7
|
+
if (token) {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
warn('Not authenticated. Starting login...');
|
|
12
|
+
info('');
|
|
13
|
+
await handleLogin();
|
|
14
|
+
return getAuthToken() ? true : false;
|
|
15
|
+
}
|
package/src/utils/git.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import zlib from 'zlib';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
|
|
7
|
+
export function getRepoUrl(): string {
|
|
8
|
+
try {
|
|
9
|
+
const url = execSync('git remote get-url origin', { encoding: 'utf8' }).trim();
|
|
10
|
+
if (!url) throw new Error('No origin found');
|
|
11
|
+
return url;
|
|
12
|
+
} catch {
|
|
13
|
+
throw new Error('Not in a git repository or no origin set');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getBranch(): string {
|
|
18
|
+
try {
|
|
19
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim();
|
|
20
|
+
if (!branch || branch === 'HEAD') throw new Error('Cannot determine branch');
|
|
21
|
+
return branch;
|
|
22
|
+
} catch {
|
|
23
|
+
throw new Error('Could not detect current branch');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function hasUncommittedChanges(): boolean {
|
|
28
|
+
try {
|
|
29
|
+
const status = execSync('git status --porcelain', { encoding: 'utf8' }).trim();
|
|
30
|
+
return status.length > 0;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getUncommittedFiles(): string[] {
|
|
37
|
+
try {
|
|
38
|
+
const status = execSync('git status --porcelain', { encoding: 'utf8' }).trim();
|
|
39
|
+
if (!status) return [];
|
|
40
|
+
return status.split('\n').map(line => line.slice(3).trim()).filter(f => f);
|
|
41
|
+
} catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getBranchTrackingInfo(): { remote: string; upstream: string } | null {
|
|
47
|
+
try {
|
|
48
|
+
const remote = execSync('git config --get branch.$(git rev-parse --abbrev-ref HEAD).remote', { encoding: 'utf8' }).trim();
|
|
49
|
+
const upstream = execSync('git config --get branch.$(git rev-parse --abbrev-ref HEAD).merge', { encoding: 'utf8' }).trim();
|
|
50
|
+
if (!remote || !upstream) return null;
|
|
51
|
+
return { remote, upstream: upstream.replace('refs/heads/', '') };
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function createBundle(branch: string = 'HEAD'): string {
|
|
58
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lazypush-'));
|
|
59
|
+
const bundlePath = path.join(tmpDir, 'repo.bundle');
|
|
60
|
+
try {
|
|
61
|
+
// Create bundle with the branch name to preserve refs/heads/<branch>
|
|
62
|
+
execSync(`git bundle create "${bundlePath}" ${branch}`, { stdio: 'pipe' });
|
|
63
|
+
if (!fs.existsSync(bundlePath)) throw new Error('Bundle not created');
|
|
64
|
+
return bundlePath;
|
|
65
|
+
} catch (e) {
|
|
66
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
67
|
+
throw new Error(`Failed to create bundle: ${e}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function compressBundle(bundlePath: string): string {
|
|
72
|
+
const gzPath = bundlePath + '.gz';
|
|
73
|
+
try {
|
|
74
|
+
const bundle = fs.readFileSync(bundlePath);
|
|
75
|
+
const compressed = zlib.gzipSync(bundle);
|
|
76
|
+
fs.writeFileSync(gzPath, compressed);
|
|
77
|
+
return gzPath;
|
|
78
|
+
} catch (e) {
|
|
79
|
+
throw new Error(`Compression failed: ${e}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function bundleToBase64(gzPath: string): string {
|
|
84
|
+
const data = fs.readFileSync(gzPath);
|
|
85
|
+
return data.toString('base64');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function cleanupBundle(bundlePath: string, gzPath: string) {
|
|
89
|
+
try {
|
|
90
|
+
const tmpDir = path.dirname(bundlePath);
|
|
91
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
92
|
+
} catch {}
|
|
93
|
+
}
|