@anthais/glsync 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/dist/index.js +967 -0
- package/dist/index.js.map +1 -0
- package/package.json +40 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,967 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/init.ts
|
|
7
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
8
|
+
import { join as join2 } from "path";
|
|
9
|
+
import { createInterface } from "readline";
|
|
10
|
+
|
|
11
|
+
// src/core/config.ts
|
|
12
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
var GLSYNC_DIR = ".gitlab/.glsync";
|
|
15
|
+
var CONFIG_FILE = "config.json";
|
|
16
|
+
function configPath(projectRoot) {
|
|
17
|
+
return join(projectRoot, GLSYNC_DIR, CONFIG_FILE);
|
|
18
|
+
}
|
|
19
|
+
function glsyncDir(projectRoot) {
|
|
20
|
+
return join(projectRoot, GLSYNC_DIR);
|
|
21
|
+
}
|
|
22
|
+
async function readConfig(projectRoot) {
|
|
23
|
+
const filePath = configPath(projectRoot);
|
|
24
|
+
try {
|
|
25
|
+
const raw = await readFile(filePath, "utf-8");
|
|
26
|
+
return JSON.parse(raw);
|
|
27
|
+
} catch {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Config not found at ${filePath}. Run "glsync init" first.`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function writeConfig(projectRoot, config) {
|
|
34
|
+
const dir = join(projectRoot, GLSYNC_DIR);
|
|
35
|
+
await mkdir(dir, { recursive: true });
|
|
36
|
+
await writeFile(configPath(projectRoot), JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
37
|
+
}
|
|
38
|
+
function defaultConfig() {
|
|
39
|
+
return {
|
|
40
|
+
issue_dir: ".gitlab/issues",
|
|
41
|
+
milestone_dir: ".gitlab/milestones",
|
|
42
|
+
label_dir: ".gitlab/labels"
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/core/gitlab-client.ts
|
|
47
|
+
var GitLabClient = class {
|
|
48
|
+
baseUrl;
|
|
49
|
+
token;
|
|
50
|
+
projectId;
|
|
51
|
+
constructor(opts) {
|
|
52
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
53
|
+
this.token = opts.token;
|
|
54
|
+
this.projectId = opts.projectId;
|
|
55
|
+
}
|
|
56
|
+
apiUrl(path) {
|
|
57
|
+
const resolved = path.replace(":id", encodeURIComponent(this.projectId));
|
|
58
|
+
return `${this.baseUrl}/api/v4${resolved}`;
|
|
59
|
+
}
|
|
60
|
+
headers() {
|
|
61
|
+
return {
|
|
62
|
+
"PRIVATE-TOKEN": this.token,
|
|
63
|
+
"Content-Type": "application/json"
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async request(url, init, retries = 3) {
|
|
67
|
+
const res = await fetch(url, {
|
|
68
|
+
...init,
|
|
69
|
+
headers: { ...this.headers(), ...init?.headers }
|
|
70
|
+
});
|
|
71
|
+
if (res.status === 429 && retries > 0) {
|
|
72
|
+
const retryAfter = Number(res.headers.get("Retry-After") || "2");
|
|
73
|
+
await sleep(retryAfter * 1e3);
|
|
74
|
+
return this.request(url, init, retries - 1);
|
|
75
|
+
}
|
|
76
|
+
if (!res.ok) {
|
|
77
|
+
const body = await res.text().catch(() => "");
|
|
78
|
+
throw new Error(
|
|
79
|
+
`GitLab API error ${res.status} ${res.statusText}: ${init?.method ?? "GET"} ${url}
|
|
80
|
+
${body}`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return res.json();
|
|
84
|
+
}
|
|
85
|
+
/** Paginated fetch - loops through all pages with per_page=100 */
|
|
86
|
+
async listAll(path, params) {
|
|
87
|
+
const results = [];
|
|
88
|
+
let page = 1;
|
|
89
|
+
while (true) {
|
|
90
|
+
const url = new URL(this.apiUrl(path));
|
|
91
|
+
url.searchParams.set("per_page", "100");
|
|
92
|
+
url.searchParams.set("page", String(page));
|
|
93
|
+
if (params) {
|
|
94
|
+
for (const [k, v] of Object.entries(params)) {
|
|
95
|
+
url.searchParams.set(k, v);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const items = await this.request(url.toString());
|
|
99
|
+
results.push(...items);
|
|
100
|
+
if (items.length < 100) break;
|
|
101
|
+
page++;
|
|
102
|
+
}
|
|
103
|
+
return results;
|
|
104
|
+
}
|
|
105
|
+
async get(path) {
|
|
106
|
+
return this.request(this.apiUrl(path));
|
|
107
|
+
}
|
|
108
|
+
async create(path, body) {
|
|
109
|
+
return this.request(this.apiUrl(path), {
|
|
110
|
+
method: "POST",
|
|
111
|
+
body: JSON.stringify(body)
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
async update(path, body) {
|
|
115
|
+
return this.request(this.apiUrl(path), {
|
|
116
|
+
method: "PUT",
|
|
117
|
+
body: JSON.stringify(body)
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
function sleep(ms) {
|
|
122
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/utils/git.ts
|
|
126
|
+
import { execFile } from "child_process";
|
|
127
|
+
import { promisify } from "util";
|
|
128
|
+
var exec = promisify(execFile);
|
|
129
|
+
async function getRemoteUrl(cwd2) {
|
|
130
|
+
const { stdout } = await exec("git", ["remote", "get-url", "origin"], {
|
|
131
|
+
cwd: cwd2
|
|
132
|
+
});
|
|
133
|
+
return stdout.trim();
|
|
134
|
+
}
|
|
135
|
+
function parseGitLabUrl(remoteUrl) {
|
|
136
|
+
const sshMatch = remoteUrl.match(
|
|
137
|
+
/^git@([^:]+):(.+?)(?:\.git)?$/
|
|
138
|
+
);
|
|
139
|
+
if (sshMatch) {
|
|
140
|
+
return {
|
|
141
|
+
gitlabUrl: `https://${sshMatch[1]}`,
|
|
142
|
+
projectPath: sshMatch[2]
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const httpsMatch = remoteUrl.match(
|
|
146
|
+
/^https?:\/\/([^/]+)\/(.+?)(?:\.git)?$/
|
|
147
|
+
);
|
|
148
|
+
if (httpsMatch) {
|
|
149
|
+
return {
|
|
150
|
+
gitlabUrl: `https://${httpsMatch[1]}`,
|
|
151
|
+
projectPath: httpsMatch[2]
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
throw new Error(`Cannot parse GitLab URL from remote: ${remoteUrl}`);
|
|
155
|
+
}
|
|
156
|
+
async function detectGitRemote(cwd2) {
|
|
157
|
+
const remoteUrl = await getRemoteUrl(cwd2);
|
|
158
|
+
return parseGitLabUrl(remoteUrl);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/utils/logger.ts
|
|
162
|
+
import chalk from "chalk";
|
|
163
|
+
import ora from "ora";
|
|
164
|
+
var log = {
|
|
165
|
+
info: (msg) => console.log(chalk.blue("\u2139"), msg),
|
|
166
|
+
success: (msg) => console.log(chalk.green("\u2714"), msg),
|
|
167
|
+
warn: (msg) => console.log(chalk.yellow("\u26A0"), msg),
|
|
168
|
+
error: (msg) => console.error(chalk.red("\u2716"), msg),
|
|
169
|
+
dim: (msg) => console.log(chalk.dim(msg))
|
|
170
|
+
};
|
|
171
|
+
function spinner(text) {
|
|
172
|
+
return ora({ text, color: "cyan" });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/commands/init.ts
|
|
176
|
+
async function initCommand(projectRoot) {
|
|
177
|
+
log.info("Initializing glsync...");
|
|
178
|
+
let gitlabUrl;
|
|
179
|
+
let projectPath;
|
|
180
|
+
try {
|
|
181
|
+
const remote = await detectGitRemote(projectRoot);
|
|
182
|
+
gitlabUrl = remote.gitlabUrl;
|
|
183
|
+
projectPath = remote.projectPath;
|
|
184
|
+
log.success(`Detected GitLab: ${gitlabUrl}/${projectPath}`);
|
|
185
|
+
} catch {
|
|
186
|
+
log.warn("Could not detect git remote. Please enter manually.");
|
|
187
|
+
gitlabUrl = await ask("GitLab URL (e.g. https://gitlab.ftl.vn): ");
|
|
188
|
+
projectPath = await ask("Project path (e.g. group/project): ");
|
|
189
|
+
}
|
|
190
|
+
const tokenUrl = `${gitlabUrl}/-/user_settings/personal_access_tokens?name=glsync&scopes=api,read_user,write_repository`;
|
|
191
|
+
log.info(`Create a Personal Access Token at:
|
|
192
|
+
${tokenUrl}`);
|
|
193
|
+
const token = await ask("Paste your token: ");
|
|
194
|
+
if (!token.trim()) {
|
|
195
|
+
log.error("Token is required.");
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
const s = spinner("Looking up project ID...").start();
|
|
199
|
+
const client = new GitLabClient({
|
|
200
|
+
baseUrl: gitlabUrl,
|
|
201
|
+
token: token.trim(),
|
|
202
|
+
projectId: ""
|
|
203
|
+
});
|
|
204
|
+
let projectId;
|
|
205
|
+
try {
|
|
206
|
+
const project = await client.get(
|
|
207
|
+
`/projects/${encodeURIComponent(projectPath)}`
|
|
208
|
+
);
|
|
209
|
+
projectId = String(project.id);
|
|
210
|
+
s.succeed(`Project ID: ${projectId}`);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
s.fail("Failed to lookup project.");
|
|
213
|
+
log.error(String(err));
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
const defaults = defaultConfig();
|
|
217
|
+
const dirs = [
|
|
218
|
+
".gitlab/.glsync/objects/issues",
|
|
219
|
+
".gitlab/.glsync/objects/milestones",
|
|
220
|
+
".gitlab/.glsync/objects/labels",
|
|
221
|
+
defaults.issue_dir,
|
|
222
|
+
defaults.milestone_dir,
|
|
223
|
+
defaults.label_dir
|
|
224
|
+
];
|
|
225
|
+
for (const dir of dirs) {
|
|
226
|
+
await mkdir2(join2(projectRoot, dir), { recursive: true });
|
|
227
|
+
}
|
|
228
|
+
log.success("Created directories.");
|
|
229
|
+
const config = {
|
|
230
|
+
gitlab_url: gitlabUrl,
|
|
231
|
+
project_id: projectId,
|
|
232
|
+
token: token.trim(),
|
|
233
|
+
issue_dir: defaults.issue_dir,
|
|
234
|
+
milestone_dir: defaults.milestone_dir,
|
|
235
|
+
label_dir: defaults.label_dir
|
|
236
|
+
};
|
|
237
|
+
await writeConfig(projectRoot, config);
|
|
238
|
+
log.success("Config saved to .gitlab/.glsync/config.json");
|
|
239
|
+
await ensureGitignore(projectRoot);
|
|
240
|
+
log.success("Updated .gitignore");
|
|
241
|
+
log.success("glsync initialized! Run `glsync pull` to sync.");
|
|
242
|
+
}
|
|
243
|
+
async function ensureGitignore(projectRoot) {
|
|
244
|
+
const gitignorePath = join2(projectRoot, ".gitignore");
|
|
245
|
+
let content = "";
|
|
246
|
+
try {
|
|
247
|
+
content = await readFile2(gitignorePath, "utf-8");
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
const entries = [".gitlab/.glsync/", "*.md.bak"];
|
|
251
|
+
const lines = content.split("\n");
|
|
252
|
+
let modified = false;
|
|
253
|
+
for (const entry of entries) {
|
|
254
|
+
if (!lines.some((l) => l.trim() === entry)) {
|
|
255
|
+
lines.push(entry);
|
|
256
|
+
modified = true;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (modified) {
|
|
260
|
+
await writeFile2(gitignorePath, lines.join("\n").trimEnd() + "\n", "utf-8");
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function ask(question) {
|
|
264
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
265
|
+
return new Promise((resolve) => {
|
|
266
|
+
rl.question(question, (answer) => {
|
|
267
|
+
rl.close();
|
|
268
|
+
resolve(answer);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/core/hasher.ts
|
|
274
|
+
import { createHash } from "crypto";
|
|
275
|
+
function deepHash(obj) {
|
|
276
|
+
const normalized = sortKeys(obj);
|
|
277
|
+
const json = JSON.stringify(normalized);
|
|
278
|
+
return createHash("sha256").update(json, "utf-8").digest("hex");
|
|
279
|
+
}
|
|
280
|
+
function sortKeys(value) {
|
|
281
|
+
if (value === null || value === void 0) return value;
|
|
282
|
+
if (Array.isArray(value)) {
|
|
283
|
+
return value.map(sortKeys);
|
|
284
|
+
}
|
|
285
|
+
if (typeof value === "object") {
|
|
286
|
+
const sorted = {};
|
|
287
|
+
for (const key of Object.keys(value).sort()) {
|
|
288
|
+
sorted[key] = sortKeys(value[key]);
|
|
289
|
+
}
|
|
290
|
+
return sorted;
|
|
291
|
+
}
|
|
292
|
+
return value;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/core/markdown.ts
|
|
296
|
+
import matter from "gray-matter";
|
|
297
|
+
function parseMarkdown(raw) {
|
|
298
|
+
const { data, content } = matter(raw);
|
|
299
|
+
return {
|
|
300
|
+
frontmatter: data,
|
|
301
|
+
content: content.trim()
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function serializeMarkdown(doc) {
|
|
305
|
+
return matter.stringify(doc.content + "\n", doc.frontmatter);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// src/utils/slugify.ts
|
|
309
|
+
function slugify(text) {
|
|
310
|
+
return text.normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/đ/g, "d").replace(/Đ/g, "D").toLowerCase().replace(/[\/\\:*?"<>|#%&{}@!`^~]/g, "").replace(/[\s_]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 50);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// src/resources/base.ts
|
|
314
|
+
import { readFile as readFile4, writeFile as writeFile4, readdir as readdir2, mkdir as mkdir4, rename, unlink } from "fs/promises";
|
|
315
|
+
import { join as join4 } from "path";
|
|
316
|
+
|
|
317
|
+
// src/core/diff.ts
|
|
318
|
+
import { createTwoFilesPatch } from "diff";
|
|
319
|
+
import chalk2 from "chalk";
|
|
320
|
+
function createDiff(filename, oldContent, newContent) {
|
|
321
|
+
return createTwoFilesPatch(
|
|
322
|
+
`a/${filename}`,
|
|
323
|
+
`b/${filename}`,
|
|
324
|
+
oldContent,
|
|
325
|
+
newContent
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
function colorizeDiff(raw) {
|
|
329
|
+
return raw.split("\n").map((line) => {
|
|
330
|
+
if (line.startsWith("@@")) return chalk2.cyan(line);
|
|
331
|
+
if (line.startsWith("---")) return chalk2.dim(line);
|
|
332
|
+
if (line.startsWith("+++")) return chalk2.dim(line);
|
|
333
|
+
if (line.startsWith("-")) return chalk2.red(line);
|
|
334
|
+
if (line.startsWith("+")) return chalk2.green(line);
|
|
335
|
+
if (line.startsWith("===")) return "";
|
|
336
|
+
if (line.startsWith("Index:")) return "";
|
|
337
|
+
return chalk2.dim(line);
|
|
338
|
+
}).filter((line) => line !== "").join("\n");
|
|
339
|
+
}
|
|
340
|
+
function summarizeDiff(raw) {
|
|
341
|
+
let added = 0;
|
|
342
|
+
let removed = 0;
|
|
343
|
+
for (const line of raw.split("\n")) {
|
|
344
|
+
if (line.startsWith("+") && !line.startsWith("+++")) added++;
|
|
345
|
+
if (line.startsWith("-") && !line.startsWith("---")) removed++;
|
|
346
|
+
}
|
|
347
|
+
return { added, removed };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// src/core/object-store.ts
|
|
351
|
+
import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3, readdir } from "fs/promises";
|
|
352
|
+
import { join as join3 } from "path";
|
|
353
|
+
function objectDir(projectRoot, resource) {
|
|
354
|
+
return join3(glsyncDir(projectRoot), "objects", resource);
|
|
355
|
+
}
|
|
356
|
+
function objectPath(projectRoot, resource, id) {
|
|
357
|
+
return join3(objectDir(projectRoot, resource), `${id}.json`);
|
|
358
|
+
}
|
|
359
|
+
async function readObject(projectRoot, resource, id) {
|
|
360
|
+
try {
|
|
361
|
+
const raw = await readFile3(objectPath(projectRoot, resource, id), "utf-8");
|
|
362
|
+
return JSON.parse(raw);
|
|
363
|
+
} catch {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
async function writeObject(projectRoot, resource, snapshot) {
|
|
368
|
+
const dir = objectDir(projectRoot, resource);
|
|
369
|
+
await mkdir3(dir, { recursive: true });
|
|
370
|
+
await writeFile3(
|
|
371
|
+
objectPath(projectRoot, resource, snapshot.id),
|
|
372
|
+
JSON.stringify(snapshot, null, 2) + "\n",
|
|
373
|
+
"utf-8"
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
async function listObjects(projectRoot, resource) {
|
|
377
|
+
const dir = objectDir(projectRoot, resource);
|
|
378
|
+
let files;
|
|
379
|
+
try {
|
|
380
|
+
files = await readdir(dir);
|
|
381
|
+
} catch {
|
|
382
|
+
return [];
|
|
383
|
+
}
|
|
384
|
+
const results = [];
|
|
385
|
+
for (const file of files) {
|
|
386
|
+
if (!file.endsWith(".json")) continue;
|
|
387
|
+
try {
|
|
388
|
+
const raw = await readFile3(join3(dir, file), "utf-8");
|
|
389
|
+
results.push(JSON.parse(raw));
|
|
390
|
+
} catch {
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return results;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/resources/base.ts
|
|
397
|
+
var BaseResource = class {
|
|
398
|
+
// ".gitlab/issues"
|
|
399
|
+
constructor(client, projectRoot) {
|
|
400
|
+
this.client = client;
|
|
401
|
+
this.projectRoot = projectRoot;
|
|
402
|
+
}
|
|
403
|
+
client;
|
|
404
|
+
projectRoot;
|
|
405
|
+
// --- Shared workflow ---
|
|
406
|
+
localPath() {
|
|
407
|
+
return join4(this.projectRoot, this.localDir);
|
|
408
|
+
}
|
|
409
|
+
/** Fetch all items from GitLab API and save to object store */
|
|
410
|
+
async fetch() {
|
|
411
|
+
const items = await this.client.listAll(this.apiPath);
|
|
412
|
+
for (const item of items) {
|
|
413
|
+
const id = this.getId(item);
|
|
414
|
+
const serverHash = this.computeHash(item);
|
|
415
|
+
const existing = await readObject(
|
|
416
|
+
this.projectRoot,
|
|
417
|
+
this.resourceType,
|
|
418
|
+
id
|
|
419
|
+
);
|
|
420
|
+
await writeObject(this.projectRoot, this.resourceType, {
|
|
421
|
+
id,
|
|
422
|
+
base_hash: existing?.base_hash ?? "",
|
|
423
|
+
server_hash: serverHash,
|
|
424
|
+
data: item,
|
|
425
|
+
fetched_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
return items.length;
|
|
429
|
+
}
|
|
430
|
+
/** Pull: fetch first, then write .md files from object store */
|
|
431
|
+
async pull(opts = {}) {
|
|
432
|
+
await this.fetch();
|
|
433
|
+
const objects = await listObjects(this.projectRoot, this.resourceType);
|
|
434
|
+
const result = { updated: [], conflicts: [], skipped: [] };
|
|
435
|
+
await mkdir4(this.localPath(), { recursive: true });
|
|
436
|
+
const idToLocalFile = /* @__PURE__ */ new Map();
|
|
437
|
+
const localFiles = await this.listLocalFiles();
|
|
438
|
+
for (const file of localFiles) {
|
|
439
|
+
const content = await safeReadFile(join4(this.localPath(), file));
|
|
440
|
+
if (content !== null) {
|
|
441
|
+
const data = this.fromMarkdown(content);
|
|
442
|
+
const id = this.getId(data);
|
|
443
|
+
if (id !== null && id !== void 0) {
|
|
444
|
+
idToLocalFile.set(id, file);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
for (const obj of objects) {
|
|
449
|
+
const filename = this.filenameFor(obj.data);
|
|
450
|
+
const filePath = join4(this.localPath(), filename);
|
|
451
|
+
const objId = this.getId(obj.data);
|
|
452
|
+
const oldFile = idToLocalFile.get(objId);
|
|
453
|
+
if (oldFile && oldFile !== filename) {
|
|
454
|
+
await unlink(join4(this.localPath(), oldFile));
|
|
455
|
+
}
|
|
456
|
+
const localContent = await safeReadFile(filePath);
|
|
457
|
+
if (localContent !== null) {
|
|
458
|
+
const localData = this.fromMarkdown(localContent);
|
|
459
|
+
const localHash = this.computeHash(localData);
|
|
460
|
+
if (localHash !== obj.base_hash && obj.base_hash !== "") {
|
|
461
|
+
if (!opts.force) {
|
|
462
|
+
if (opts.backup) {
|
|
463
|
+
await rename(filePath, filePath + ".bak");
|
|
464
|
+
} else {
|
|
465
|
+
result.conflicts.push(filename);
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (obj.server_hash === obj.base_hash) {
|
|
471
|
+
result.skipped.push(filename);
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
const md = this.toMarkdown(obj.data);
|
|
476
|
+
await writeFile4(filePath, md, "utf-8");
|
|
477
|
+
await writeObject(this.projectRoot, this.resourceType, {
|
|
478
|
+
...obj,
|
|
479
|
+
base_hash: obj.server_hash
|
|
480
|
+
});
|
|
481
|
+
result.updated.push(filename);
|
|
482
|
+
}
|
|
483
|
+
return result;
|
|
484
|
+
}
|
|
485
|
+
/** Push: read local .md, conflict check, then update GitLab */
|
|
486
|
+
async push(opts = {}) {
|
|
487
|
+
const result = {
|
|
488
|
+
pushed: [],
|
|
489
|
+
created: [],
|
|
490
|
+
conflicts: [],
|
|
491
|
+
skipped: []
|
|
492
|
+
};
|
|
493
|
+
const files = await this.listLocalFiles();
|
|
494
|
+
for (const file of files) {
|
|
495
|
+
const filePath = join4(this.localPath(), file);
|
|
496
|
+
const content = await readFile4(filePath, "utf-8");
|
|
497
|
+
const localData = this.fromMarkdown(content);
|
|
498
|
+
const localHash = this.computeHash(localData);
|
|
499
|
+
const apiId = this.getApiId(localData);
|
|
500
|
+
if (apiId === null || apiId === void 0) {
|
|
501
|
+
const payload2 = this.toApiPayload(localData);
|
|
502
|
+
const created = await this.client.create(this.apiPath, payload2);
|
|
503
|
+
const newId = this.getId(created);
|
|
504
|
+
const serverHash2 = this.computeHash(created);
|
|
505
|
+
const newMd = this.toMarkdown(created);
|
|
506
|
+
const newFilename = this.filenameFor(created);
|
|
507
|
+
await writeFile4(join4(this.localPath(), newFilename), newMd, "utf-8");
|
|
508
|
+
if (newFilename !== file) {
|
|
509
|
+
const { unlink: unlink2 } = await import("fs/promises");
|
|
510
|
+
await unlink2(filePath);
|
|
511
|
+
}
|
|
512
|
+
await writeObject(this.projectRoot, this.resourceType, {
|
|
513
|
+
id: newId,
|
|
514
|
+
base_hash: serverHash2,
|
|
515
|
+
server_hash: serverHash2,
|
|
516
|
+
data: created,
|
|
517
|
+
fetched_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
518
|
+
});
|
|
519
|
+
result.created.push(newFilename);
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
const storeId = this.getId(localData);
|
|
523
|
+
const obj = await readObject(this.projectRoot, this.resourceType, storeId);
|
|
524
|
+
if (obj && localHash === obj.base_hash) {
|
|
525
|
+
result.skipped.push(file);
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
const serverItem = await this.client.get(
|
|
529
|
+
`${this.apiPath}/${encodeURIComponent(String(apiId))}`
|
|
530
|
+
);
|
|
531
|
+
const serverHash = this.computeHash(serverItem);
|
|
532
|
+
if (obj && serverHash !== obj.base_hash && !opts.force) {
|
|
533
|
+
result.conflicts.push(file);
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
const payload = this.toApiPayload(localData);
|
|
537
|
+
const updated = await this.client.update(
|
|
538
|
+
`${this.apiPath}/${encodeURIComponent(String(apiId))}`,
|
|
539
|
+
payload
|
|
540
|
+
);
|
|
541
|
+
const updatedHash = this.computeHash(updated);
|
|
542
|
+
const updatedStoreId = this.getId(updated);
|
|
543
|
+
await writeObject(this.projectRoot, this.resourceType, {
|
|
544
|
+
id: updatedStoreId,
|
|
545
|
+
base_hash: updatedHash,
|
|
546
|
+
server_hash: updatedHash,
|
|
547
|
+
data: updated,
|
|
548
|
+
fetched_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
549
|
+
});
|
|
550
|
+
result.pushed.push(file);
|
|
551
|
+
}
|
|
552
|
+
return result;
|
|
553
|
+
}
|
|
554
|
+
/** Diff: compare object store vs local .md files */
|
|
555
|
+
async diff() {
|
|
556
|
+
const results = [];
|
|
557
|
+
const objects = await listObjects(this.projectRoot, this.resourceType);
|
|
558
|
+
const files = await this.listLocalFiles();
|
|
559
|
+
const processedFiles = /* @__PURE__ */ new Set();
|
|
560
|
+
for (const obj of objects) {
|
|
561
|
+
const filename = this.filenameFor(obj.data);
|
|
562
|
+
const filePath = join4(this.localPath(), filename);
|
|
563
|
+
processedFiles.add(filename);
|
|
564
|
+
const localContent = await safeReadFile(filePath);
|
|
565
|
+
if (localContent === null) {
|
|
566
|
+
results.push({ id: obj.id, filename, status: "deleted" });
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
const serverMd = this.toMarkdown(obj.data);
|
|
570
|
+
if (localContent.trim() === serverMd.trim()) {
|
|
571
|
+
results.push({ id: obj.id, filename, status: "unchanged" });
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
results.push({
|
|
575
|
+
id: obj.id,
|
|
576
|
+
filename,
|
|
577
|
+
status: "modified",
|
|
578
|
+
diff: createDiff(filename, serverMd, localContent)
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
for (const file of files) {
|
|
582
|
+
if (!processedFiles.has(file)) {
|
|
583
|
+
results.push({ id: null, filename: file, status: "added" });
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return results;
|
|
587
|
+
}
|
|
588
|
+
async listLocalFiles() {
|
|
589
|
+
try {
|
|
590
|
+
const files = await readdir2(this.localPath());
|
|
591
|
+
return files.filter((f) => f.endsWith(".md") && !f.endsWith(".bak"));
|
|
592
|
+
} catch {
|
|
593
|
+
return [];
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
async function safeReadFile(path) {
|
|
598
|
+
try {
|
|
599
|
+
return await readFile4(path, "utf-8");
|
|
600
|
+
} catch {
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// src/resources/issues.ts
|
|
606
|
+
var IssueResource = class extends BaseResource {
|
|
607
|
+
resourceType = "issues";
|
|
608
|
+
apiPath = "/projects/:id/issues";
|
|
609
|
+
localDir = ".gitlab/issues";
|
|
610
|
+
getId(item) {
|
|
611
|
+
return item.iid;
|
|
612
|
+
}
|
|
613
|
+
getApiId(item) {
|
|
614
|
+
return item.iid ?? null;
|
|
615
|
+
}
|
|
616
|
+
computeHash(item) {
|
|
617
|
+
const labels = [...item.labels ?? []].sort();
|
|
618
|
+
return deepHash({
|
|
619
|
+
title: item.title ?? "",
|
|
620
|
+
description: item.description ?? "",
|
|
621
|
+
labels,
|
|
622
|
+
milestone: item.milestone ? typeof item.milestone === "string" ? item.milestone : item.milestone.title : ""
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
filenameFor(item) {
|
|
626
|
+
const slug = slugify(item.title);
|
|
627
|
+
return `${item.iid}.${slug}.md`;
|
|
628
|
+
}
|
|
629
|
+
toMarkdown(item) {
|
|
630
|
+
const frontmatter = {
|
|
631
|
+
id: item.id,
|
|
632
|
+
iid: item.iid,
|
|
633
|
+
title: item.title,
|
|
634
|
+
labels: item.labels,
|
|
635
|
+
milestone: item.milestone?.title ?? null,
|
|
636
|
+
assignees: item.assignees?.map((a) => a.username) ?? [],
|
|
637
|
+
state: item.state
|
|
638
|
+
};
|
|
639
|
+
return serializeMarkdown({
|
|
640
|
+
frontmatter,
|
|
641
|
+
content: item.description ?? ""
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
fromMarkdown(content) {
|
|
645
|
+
const { frontmatter, content: body } = parseMarkdown(content);
|
|
646
|
+
return {
|
|
647
|
+
id: frontmatter.id ?? void 0,
|
|
648
|
+
iid: frontmatter.iid ?? void 0,
|
|
649
|
+
title: frontmatter.title,
|
|
650
|
+
description: body,
|
|
651
|
+
labels: frontmatter.labels ?? [],
|
|
652
|
+
milestone: frontmatter.milestone ? { title: frontmatter.milestone } : null,
|
|
653
|
+
assignees: (frontmatter.assignees ?? []).map(
|
|
654
|
+
(u) => ({ username: u })
|
|
655
|
+
),
|
|
656
|
+
state: frontmatter.state
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
toApiPayload(item) {
|
|
660
|
+
return {
|
|
661
|
+
title: item.title,
|
|
662
|
+
description: item.description ?? "",
|
|
663
|
+
labels: (item.labels ?? []).join(","),
|
|
664
|
+
milestone_id: void 0,
|
|
665
|
+
// TODO: resolve milestone title → id
|
|
666
|
+
assignee_ids: void 0
|
|
667
|
+
// TODO: resolve usernames → ids
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
// src/resources/milestones.ts
|
|
673
|
+
var MilestoneResource = class extends BaseResource {
|
|
674
|
+
resourceType = "milestones";
|
|
675
|
+
apiPath = "/projects/:id/milestones";
|
|
676
|
+
localDir = ".gitlab/milestones";
|
|
677
|
+
getId(item) {
|
|
678
|
+
return item.id;
|
|
679
|
+
}
|
|
680
|
+
getApiId(item) {
|
|
681
|
+
return item.id ?? null;
|
|
682
|
+
}
|
|
683
|
+
computeHash(item) {
|
|
684
|
+
return deepHash({
|
|
685
|
+
title: item.title ?? "",
|
|
686
|
+
description: item.description ?? "",
|
|
687
|
+
due_date: item.due_date ?? "",
|
|
688
|
+
start_date: item.start_date ?? "",
|
|
689
|
+
state: item.state ?? ""
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
filenameFor(item) {
|
|
693
|
+
return `${slugify(item.title)}.md`;
|
|
694
|
+
}
|
|
695
|
+
toMarkdown(item) {
|
|
696
|
+
const frontmatter = {
|
|
697
|
+
id: item.id,
|
|
698
|
+
title: item.title,
|
|
699
|
+
state: item.state,
|
|
700
|
+
start_date: item.start_date,
|
|
701
|
+
due_date: item.due_date
|
|
702
|
+
};
|
|
703
|
+
return serializeMarkdown({
|
|
704
|
+
frontmatter,
|
|
705
|
+
content: item.description ?? ""
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
fromMarkdown(content) {
|
|
709
|
+
const { frontmatter, content: body } = parseMarkdown(content);
|
|
710
|
+
return {
|
|
711
|
+
id: frontmatter.id ?? void 0,
|
|
712
|
+
title: frontmatter.title,
|
|
713
|
+
description: body,
|
|
714
|
+
state: frontmatter.state,
|
|
715
|
+
start_date: frontmatter.start_date,
|
|
716
|
+
due_date: frontmatter.due_date
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
toApiPayload(item) {
|
|
720
|
+
return {
|
|
721
|
+
title: item.title,
|
|
722
|
+
description: item.description ?? "",
|
|
723
|
+
start_date: item.start_date ?? void 0,
|
|
724
|
+
due_date: item.due_date ?? void 0
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
// src/resources/labels.ts
|
|
730
|
+
var LabelResource = class extends BaseResource {
|
|
731
|
+
resourceType = "labels";
|
|
732
|
+
apiPath = "/projects/:id/labels";
|
|
733
|
+
localDir = ".gitlab/labels";
|
|
734
|
+
getId(item) {
|
|
735
|
+
return item.id;
|
|
736
|
+
}
|
|
737
|
+
getApiId(item) {
|
|
738
|
+
return item.id ?? null;
|
|
739
|
+
}
|
|
740
|
+
computeHash(item) {
|
|
741
|
+
return deepHash({
|
|
742
|
+
name: item.name ?? "",
|
|
743
|
+
color: item.color ?? "",
|
|
744
|
+
description: item.description ?? "",
|
|
745
|
+
priority: item.priority ?? null
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
filenameFor(item) {
|
|
749
|
+
return `${slugify(item.name)}.md`;
|
|
750
|
+
}
|
|
751
|
+
toMarkdown(item) {
|
|
752
|
+
const frontmatter = {
|
|
753
|
+
id: item.id,
|
|
754
|
+
name: item.name,
|
|
755
|
+
color: item.color,
|
|
756
|
+
priority: item.priority
|
|
757
|
+
};
|
|
758
|
+
return serializeMarkdown({
|
|
759
|
+
frontmatter,
|
|
760
|
+
content: item.description ?? ""
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
fromMarkdown(content) {
|
|
764
|
+
const { frontmatter, content: body } = parseMarkdown(content);
|
|
765
|
+
return {
|
|
766
|
+
id: frontmatter.id ?? void 0,
|
|
767
|
+
name: frontmatter.name,
|
|
768
|
+
color: frontmatter.color,
|
|
769
|
+
description: body,
|
|
770
|
+
priority: frontmatter.priority
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
toApiPayload(item) {
|
|
774
|
+
return {
|
|
775
|
+
name: item.name,
|
|
776
|
+
color: item.color,
|
|
777
|
+
description: item.description ?? "",
|
|
778
|
+
priority: item.priority ?? void 0
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
// src/resources/index.ts
|
|
784
|
+
var RESOURCE_MAP = {
|
|
785
|
+
issues: IssueResource,
|
|
786
|
+
milestones: MilestoneResource,
|
|
787
|
+
labels: LabelResource
|
|
788
|
+
};
|
|
789
|
+
var ALL_RESOURCES = Object.keys(RESOURCE_MAP);
|
|
790
|
+
function getResources(config, projectRoot, filter) {
|
|
791
|
+
const client = new GitLabClient({
|
|
792
|
+
baseUrl: config.gitlab_url,
|
|
793
|
+
token: config.token,
|
|
794
|
+
projectId: config.project_id
|
|
795
|
+
});
|
|
796
|
+
const types = filter ? [filter] : ALL_RESOURCES;
|
|
797
|
+
return types.map((type) => {
|
|
798
|
+
const Ctor = RESOURCE_MAP[type];
|
|
799
|
+
if (!Ctor) throw new Error(`Unknown resource type: ${type}`);
|
|
800
|
+
return new Ctor(client, projectRoot);
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// src/commands/fetch.ts
|
|
805
|
+
async function fetchCommand(projectRoot, resource) {
|
|
806
|
+
const config = await readConfig(projectRoot);
|
|
807
|
+
const resources = getResources(config, projectRoot, resource);
|
|
808
|
+
for (const res of resources) {
|
|
809
|
+
const s = spinner(`Fetching ${res.resourceType}...`).start();
|
|
810
|
+
try {
|
|
811
|
+
const count = await res.fetch();
|
|
812
|
+
s.succeed(`Fetched ${count} ${res.resourceType}`);
|
|
813
|
+
} catch (err) {
|
|
814
|
+
s.fail(`Failed to fetch ${res.resourceType}`);
|
|
815
|
+
log.error(String(err));
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// src/commands/pull.ts
|
|
821
|
+
async function pullCommand(projectRoot, opts) {
|
|
822
|
+
const config = await readConfig(projectRoot);
|
|
823
|
+
const resources = getResources(config, projectRoot, opts.resource);
|
|
824
|
+
for (const res of resources) {
|
|
825
|
+
const s = spinner(`Pulling ${res.resourceType}...`).start();
|
|
826
|
+
try {
|
|
827
|
+
const result = await res.pull({
|
|
828
|
+
force: opts.force,
|
|
829
|
+
backup: opts.backup
|
|
830
|
+
});
|
|
831
|
+
s.succeed(`Pulled ${res.resourceType}`);
|
|
832
|
+
if (result.updated.length > 0) {
|
|
833
|
+
log.success(` Updated: ${result.updated.join(", ")}`);
|
|
834
|
+
}
|
|
835
|
+
if (result.skipped.length > 0) {
|
|
836
|
+
log.dim(` Skipped (no change): ${result.skipped.length} files`);
|
|
837
|
+
}
|
|
838
|
+
if (result.conflicts.length > 0) {
|
|
839
|
+
log.warn(` Conflicts: ${result.conflicts.join(", ")}`);
|
|
840
|
+
log.warn(" Use --force to overwrite or --backup to save .bak first.");
|
|
841
|
+
}
|
|
842
|
+
} catch (err) {
|
|
843
|
+
s.fail(`Failed to pull ${res.resourceType}`);
|
|
844
|
+
log.error(String(err));
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// src/commands/diff.ts
|
|
850
|
+
import chalk3 from "chalk";
|
|
851
|
+
async function diffCommand(projectRoot, resource) {
|
|
852
|
+
const config = await readConfig(projectRoot);
|
|
853
|
+
const resources = getResources(config, projectRoot, resource);
|
|
854
|
+
let hasChanges = false;
|
|
855
|
+
let totalAdded = 0;
|
|
856
|
+
let totalRemoved = 0;
|
|
857
|
+
let totalFiles = 0;
|
|
858
|
+
for (const res of resources) {
|
|
859
|
+
const results = await res.diff();
|
|
860
|
+
const changed = results.filter((r) => r.status !== "unchanged");
|
|
861
|
+
if (changed.length === 0) continue;
|
|
862
|
+
hasChanges = true;
|
|
863
|
+
console.log(chalk3.bold.underline(`
|
|
864
|
+
${res.resourceType}`));
|
|
865
|
+
for (const item of changed) {
|
|
866
|
+
totalFiles++;
|
|
867
|
+
switch (item.status) {
|
|
868
|
+
case "added":
|
|
869
|
+
console.log(chalk3.green(` + ${item.filename}`) + chalk3.dim(" (new local file)"));
|
|
870
|
+
break;
|
|
871
|
+
case "deleted":
|
|
872
|
+
console.log(chalk3.red(` - ${item.filename}`) + chalk3.dim(" (missing local file)"));
|
|
873
|
+
break;
|
|
874
|
+
case "modified": {
|
|
875
|
+
const { added, removed } = item.diff ? summarizeDiff(item.diff) : { added: 0, removed: 0 };
|
|
876
|
+
totalAdded += added;
|
|
877
|
+
totalRemoved += removed;
|
|
878
|
+
const stat = [
|
|
879
|
+
added > 0 ? chalk3.green(`+${added}`) : null,
|
|
880
|
+
removed > 0 ? chalk3.red(`-${removed}`) : null
|
|
881
|
+
].filter(Boolean).join(" ");
|
|
882
|
+
console.log(chalk3.yellow(` ~ ${item.filename}`) + ` ${stat}`);
|
|
883
|
+
if (item.diff) {
|
|
884
|
+
console.log(colorizeDiff(item.diff));
|
|
885
|
+
}
|
|
886
|
+
break;
|
|
887
|
+
}
|
|
888
|
+
case "conflict":
|
|
889
|
+
console.log(chalk3.red.bold(` ! ${item.filename}`) + chalk3.dim(" (conflict)"));
|
|
890
|
+
break;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
if (!hasChanges) {
|
|
895
|
+
log.success("No changes detected.");
|
|
896
|
+
} else {
|
|
897
|
+
const parts = [
|
|
898
|
+
`${totalFiles} file${totalFiles > 1 ? "s" : ""} changed`,
|
|
899
|
+
totalAdded > 0 ? chalk3.green(`+${totalAdded}`) : null,
|
|
900
|
+
totalRemoved > 0 ? chalk3.red(`-${totalRemoved}`) : null
|
|
901
|
+
].filter(Boolean).join(", ");
|
|
902
|
+
console.log(chalk3.dim(`
|
|
903
|
+
${parts}`));
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// src/commands/push.ts
|
|
908
|
+
import { createInterface as createInterface2 } from "readline";
|
|
909
|
+
async function pushCommand(projectRoot, opts) {
|
|
910
|
+
const config = await readConfig(projectRoot);
|
|
911
|
+
const resources = getResources(config, projectRoot, opts.resource);
|
|
912
|
+
if (opts.force && !opts.yes) {
|
|
913
|
+
log.warn("Force push will bypass conflict checks and overwrite server data.");
|
|
914
|
+
const confirmed = await confirm("Continue? [y/N] ");
|
|
915
|
+
if (!confirmed) {
|
|
916
|
+
log.info("Push cancelled.");
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
for (const res of resources) {
|
|
921
|
+
const s = spinner(`Pushing ${res.resourceType}...`).start();
|
|
922
|
+
try {
|
|
923
|
+
const result = await res.push({
|
|
924
|
+
force: opts.force,
|
|
925
|
+
yes: opts.yes
|
|
926
|
+
});
|
|
927
|
+
s.succeed(`Pushed ${res.resourceType}`);
|
|
928
|
+
if (result.created.length > 0) {
|
|
929
|
+
log.success(` Created: ${result.created.join(", ")}`);
|
|
930
|
+
}
|
|
931
|
+
if (result.pushed.length > 0) {
|
|
932
|
+
log.success(` Updated: ${result.pushed.join(", ")}`);
|
|
933
|
+
}
|
|
934
|
+
if (result.skipped.length > 0) {
|
|
935
|
+
log.dim(` Skipped (no change): ${result.skipped.length} files`);
|
|
936
|
+
}
|
|
937
|
+
if (result.conflicts.length > 0) {
|
|
938
|
+
log.warn(` Conflicts: ${result.conflicts.join(", ")}`);
|
|
939
|
+
log.warn(" Run `glsync pull` first, or use --force to override.");
|
|
940
|
+
}
|
|
941
|
+
} catch (err) {
|
|
942
|
+
s.fail(`Failed to push ${res.resourceType}`);
|
|
943
|
+
log.error(String(err));
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
function confirm(question) {
|
|
948
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
949
|
+
return new Promise((resolve) => {
|
|
950
|
+
rl.question(question, (answer) => {
|
|
951
|
+
rl.close();
|
|
952
|
+
resolve(answer.toLowerCase() === "y");
|
|
953
|
+
});
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// src/index.ts
|
|
958
|
+
var cwd = process.cwd();
|
|
959
|
+
var program = new Command();
|
|
960
|
+
program.name("glsync").description("CLI tool for two-way sync between GitLab and local Markdown files").version("0.1.0");
|
|
961
|
+
program.command("init").description("Initialize glsync in the current project").action(() => initCommand(cwd));
|
|
962
|
+
program.command("fetch").description("Fetch resources from GitLab to object store").option("-r, --resource <type>", "Resource type: issues, milestones, labels").action((opts) => fetchCommand(cwd, opts.resource));
|
|
963
|
+
program.command("pull").description("Pull resources from GitLab to local .md files").option("-r, --resource <type>", "Resource type: issues, milestones, labels").option("--force", "Overwrite local changes without asking").option("--backup", "Create .md.bak before overwriting").action((opts) => pullCommand(cwd, opts));
|
|
964
|
+
program.command("diff").description("Show diff between object store and local .md files").option("-r, --resource <type>", "Resource type: issues, milestones, labels").action((opts) => diffCommand(cwd, opts.resource));
|
|
965
|
+
program.command("push").description("Push local .md changes to GitLab").option("-r, --resource <type>", "Resource type: issues, milestones, labels").option("--force", "Bypass conflict check").option("--yes", "Skip confirmation prompt").action((opts) => pushCommand(cwd, opts));
|
|
966
|
+
program.parse();
|
|
967
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/commands/init.ts","../src/core/config.ts","../src/core/gitlab-client.ts","../src/utils/git.ts","../src/utils/logger.ts","../src/core/hasher.ts","../src/core/markdown.ts","../src/utils/slugify.ts","../src/resources/base.ts","../src/core/diff.ts","../src/core/object-store.ts","../src/resources/issues.ts","../src/resources/milestones.ts","../src/resources/labels.ts","../src/resources/index.ts","../src/commands/fetch.ts","../src/commands/pull.ts","../src/commands/diff.ts","../src/commands/push.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { initCommand } from \"./commands/init.js\";\nimport { fetchCommand } from \"./commands/fetch.js\";\nimport { pullCommand } from \"./commands/pull.js\";\nimport { diffCommand } from \"./commands/diff.js\";\nimport { pushCommand } from \"./commands/push.js\";\n\nconst cwd = process.cwd();\n\nconst program = new Command();\n\nprogram\n .name(\"glsync\")\n .description(\"CLI tool for two-way sync between GitLab and local Markdown files\")\n .version(\"0.1.0\");\n\nprogram\n .command(\"init\")\n .description(\"Initialize glsync in the current project\")\n .action(() => initCommand(cwd));\n\nprogram\n .command(\"fetch\")\n .description(\"Fetch resources from GitLab to object store\")\n .option(\"-r, --resource <type>\", \"Resource type: issues, milestones, labels\")\n .action((opts) => fetchCommand(cwd, opts.resource));\n\nprogram\n .command(\"pull\")\n .description(\"Pull resources from GitLab to local .md files\")\n .option(\"-r, --resource <type>\", \"Resource type: issues, milestones, labels\")\n .option(\"--force\", \"Overwrite local changes without asking\")\n .option(\"--backup\", \"Create .md.bak before overwriting\")\n .action((opts) => pullCommand(cwd, opts));\n\nprogram\n .command(\"diff\")\n .description(\"Show diff between object store and local .md files\")\n .option(\"-r, --resource <type>\", \"Resource type: issues, milestones, labels\")\n .action((opts) => diffCommand(cwd, opts.resource));\n\nprogram\n .command(\"push\")\n .description(\"Push local .md changes to GitLab\")\n .option(\"-r, --resource <type>\", \"Resource type: issues, milestones, labels\")\n .option(\"--force\", \"Bypass conflict check\")\n .option(\"--yes\", \"Skip confirmation prompt\")\n .action((opts) => pushCommand(cwd, opts));\n\nprogram.parse();\n","import { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { createInterface } from \"node:readline\";\nimport { writeConfig, defaultConfig, type GlsyncConfig } from \"../core/config.js\";\nimport { GitLabClient } from \"../core/gitlab-client.js\";\nimport { detectGitRemote } from \"../utils/git.js\";\nimport { log, spinner } from \"../utils/logger.js\";\n\nexport async function initCommand(projectRoot: string): Promise<void> {\n log.info(\"Initializing glsync...\");\n\n // 1. Auto-detect git remote\n let gitlabUrl: string;\n let projectPath: string;\n\n try {\n const remote = await detectGitRemote(projectRoot);\n gitlabUrl = remote.gitlabUrl;\n projectPath = remote.projectPath;\n log.success(`Detected GitLab: ${gitlabUrl}/${projectPath}`);\n } catch {\n log.warn(\"Could not detect git remote. Please enter manually.\");\n gitlabUrl = await ask(\"GitLab URL (e.g. https://gitlab.ftl.vn): \");\n projectPath = await ask(\"Project path (e.g. group/project): \");\n }\n\n // 2. Ask for token\n const tokenUrl = `${gitlabUrl}/-/user_settings/personal_access_tokens?name=glsync&scopes=api,read_user,write_repository`;\n log.info(`Create a Personal Access Token at:\\n ${tokenUrl}`);\n const token = await ask(\"Paste your token: \");\n\n if (!token.trim()) {\n log.error(\"Token is required.\");\n process.exit(1);\n }\n\n // 3. Lookup project ID\n const s = spinner(\"Looking up project ID...\").start();\n const client = new GitLabClient({\n baseUrl: gitlabUrl,\n token: token.trim(),\n projectId: \"\",\n });\n\n let projectId: string;\n try {\n const project = await client.get<{ id: number }>(\n `/projects/${encodeURIComponent(projectPath)}`\n );\n projectId = String(project.id);\n s.succeed(`Project ID: ${projectId}`);\n } catch (err) {\n s.fail(\"Failed to lookup project.\");\n log.error(String(err));\n process.exit(1);\n }\n\n // 4. Create directories\n const defaults = defaultConfig();\n const dirs = [\n \".gitlab/.glsync/objects/issues\",\n \".gitlab/.glsync/objects/milestones\",\n \".gitlab/.glsync/objects/labels\",\n defaults.issue_dir!,\n defaults.milestone_dir!,\n defaults.label_dir!,\n ];\n for (const dir of dirs) {\n await mkdir(join(projectRoot, dir), { recursive: true });\n }\n log.success(\"Created directories.\");\n\n // 5. Write config\n const config: GlsyncConfig = {\n gitlab_url: gitlabUrl,\n project_id: projectId,\n token: token.trim(),\n issue_dir: defaults.issue_dir!,\n milestone_dir: defaults.milestone_dir!,\n label_dir: defaults.label_dir!,\n };\n await writeConfig(projectRoot, config);\n log.success(\"Config saved to .gitlab/.glsync/config.json\");\n\n // 6. Update .gitignore\n await ensureGitignore(projectRoot);\n log.success(\"Updated .gitignore\");\n\n log.success(\"glsync initialized! Run `glsync pull` to sync.\");\n}\n\nasync function ensureGitignore(projectRoot: string): Promise<void> {\n const gitignorePath = join(projectRoot, \".gitignore\");\n let content = \"\";\n try {\n content = await readFile(gitignorePath, \"utf-8\");\n } catch {\n // file doesn't exist\n }\n\n const entries = [\".gitlab/.glsync/\", \"*.md.bak\"];\n const lines = content.split(\"\\n\");\n let modified = false;\n\n for (const entry of entries) {\n if (!lines.some((l) => l.trim() === entry)) {\n lines.push(entry);\n modified = true;\n }\n }\n\n if (modified) {\n await writeFile(gitignorePath, lines.join(\"\\n\").trimEnd() + \"\\n\", \"utf-8\");\n }\n}\n\nfunction ask(question: string): Promise<string> {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n return new Promise((resolve) => {\n rl.question(question, (answer) => {\n rl.close();\n resolve(answer);\n });\n });\n}\n","import { readFile, writeFile, mkdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nexport interface GlsyncConfig {\n gitlab_url: string;\n project_id: string;\n token: string;\n issue_dir: string;\n milestone_dir: string;\n label_dir: string;\n}\n\nconst GLSYNC_DIR = \".gitlab/.glsync\";\nconst CONFIG_FILE = \"config.json\";\n\nfunction configPath(projectRoot: string): string {\n return join(projectRoot, GLSYNC_DIR, CONFIG_FILE);\n}\n\nexport function glsyncDir(projectRoot: string): string {\n return join(projectRoot, GLSYNC_DIR);\n}\n\nexport async function readConfig(projectRoot: string): Promise<GlsyncConfig> {\n const filePath = configPath(projectRoot);\n try {\n const raw = await readFile(filePath, \"utf-8\");\n return JSON.parse(raw) as GlsyncConfig;\n } catch {\n throw new Error(\n `Config not found at ${filePath}. Run \"glsync init\" first.`\n );\n }\n}\n\nexport async function writeConfig(\n projectRoot: string,\n config: GlsyncConfig\n): Promise<void> {\n const dir = join(projectRoot, GLSYNC_DIR);\n await mkdir(dir, { recursive: true });\n await writeFile(configPath(projectRoot), JSON.stringify(config, null, 2) + \"\\n\", \"utf-8\");\n}\n\nexport function defaultConfig(): Partial<GlsyncConfig> {\n return {\n issue_dir: \".gitlab/issues\",\n milestone_dir: \".gitlab/milestones\",\n label_dir: \".gitlab/labels\",\n };\n}\n","export interface GitLabClientOptions {\n baseUrl: string;\n token: string;\n projectId: string;\n}\n\nexport class GitLabClient {\n private baseUrl: string;\n private token: string;\n private projectId: string;\n\n constructor(opts: GitLabClientOptions) {\n this.baseUrl = opts.baseUrl.replace(/\\/+$/, \"\");\n this.token = opts.token;\n this.projectId = opts.projectId;\n }\n\n private apiUrl(path: string): string {\n const resolved = path.replace(\":id\", encodeURIComponent(this.projectId));\n return `${this.baseUrl}/api/v4${resolved}`;\n }\n\n private headers(): Record<string, string> {\n return {\n \"PRIVATE-TOKEN\": this.token,\n \"Content-Type\": \"application/json\",\n };\n }\n\n private async request<T>(\n url: string,\n init?: RequestInit,\n retries = 3\n ): Promise<T> {\n const res = await fetch(url, {\n ...init,\n headers: { ...this.headers(), ...init?.headers },\n });\n\n // Retry on rate limit (429)\n if (res.status === 429 && retries > 0) {\n const retryAfter = Number(res.headers.get(\"Retry-After\") || \"2\");\n await sleep(retryAfter * 1000);\n return this.request<T>(url, init, retries - 1);\n }\n\n if (!res.ok) {\n const body = await res.text().catch(() => \"\");\n throw new Error(\n `GitLab API error ${res.status} ${res.statusText}: ${init?.method ?? \"GET\"} ${url}\\n${body}`\n );\n }\n\n return res.json() as Promise<T>;\n }\n\n /** Paginated fetch - loops through all pages with per_page=100 */\n async listAll<T>(\n path: string,\n params?: Record<string, string>\n ): Promise<T[]> {\n const results: T[] = [];\n let page = 1;\n\n while (true) {\n const url = new URL(this.apiUrl(path));\n url.searchParams.set(\"per_page\", \"100\");\n url.searchParams.set(\"page\", String(page));\n if (params) {\n for (const [k, v] of Object.entries(params)) {\n url.searchParams.set(k, v);\n }\n }\n\n const items = await this.request<T[]>(url.toString());\n results.push(...items);\n\n if (items.length < 100) break;\n page++;\n }\n\n return results;\n }\n\n async get<T>(path: string): Promise<T> {\n return this.request<T>(this.apiUrl(path));\n }\n\n async create<T>(path: string, body: object): Promise<T> {\n return this.request<T>(this.apiUrl(path), {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n }\n\n async update<T>(path: string, body: object): Promise<T> {\n return this.request<T>(this.apiUrl(path), {\n method: \"PUT\",\n body: JSON.stringify(body),\n });\n }\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","import { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\n\nconst exec = promisify(execFile);\n\nexport interface GitRemoteInfo {\n gitlabUrl: string;\n projectPath: string;\n}\n\n/**\n * Get the remote origin URL from current git repo.\n */\nasync function getRemoteUrl(cwd: string): Promise<string> {\n const { stdout } = await exec(\"git\", [\"remote\", \"get-url\", \"origin\"], {\n cwd,\n });\n return stdout.trim();\n}\n\n/**\n * Parse a GitLab remote URL into base URL + project path.\n *\n * Supports:\n * - https://gitlab.ftl.vn/group/project.git\n * - git@gitlab.ftl.vn:group/project.git\n */\nexport function parseGitLabUrl(remoteUrl: string): GitRemoteInfo {\n // SSH: git@gitlab.ftl.vn:group/project.git\n const sshMatch = remoteUrl.match(\n /^git@([^:]+):(.+?)(?:\\.git)?$/\n );\n if (sshMatch) {\n return {\n gitlabUrl: `https://${sshMatch[1]}`,\n projectPath: sshMatch[2],\n };\n }\n\n // HTTPS: https://gitlab.ftl.vn/group/project.git\n const httpsMatch = remoteUrl.match(\n /^https?:\\/\\/([^/]+)\\/(.+?)(?:\\.git)?$/\n );\n if (httpsMatch) {\n return {\n gitlabUrl: `https://${httpsMatch[1]}`,\n projectPath: httpsMatch[2],\n };\n }\n\n throw new Error(`Cannot parse GitLab URL from remote: ${remoteUrl}`);\n}\n\n/**\n * Detect GitLab info from the current git repository.\n */\nexport async function detectGitRemote(cwd: string): Promise<GitRemoteInfo> {\n const remoteUrl = await getRemoteUrl(cwd);\n return parseGitLabUrl(remoteUrl);\n}\n","import chalk from \"chalk\";\nimport ora, { type Ora } from \"ora\";\n\nexport const log = {\n info: (msg: string) => console.log(chalk.blue(\"ℹ\"), msg),\n success: (msg: string) => console.log(chalk.green(\"✔\"), msg),\n warn: (msg: string) => console.log(chalk.yellow(\"⚠\"), msg),\n error: (msg: string) => console.error(chalk.red(\"✖\"), msg),\n dim: (msg: string) => console.log(chalk.dim(msg)),\n};\n\nexport function spinner(text: string): Ora {\n return ora({ text, color: \"cyan\" });\n}\n","import { createHash } from \"node:crypto\";\n\n/**\n * Deep hash: sort keys recursively, then SHA-256.\n * Arrays inside are sorted if they contain primitives.\n */\nexport function deepHash(obj: Record<string, unknown>): string {\n const normalized = sortKeys(obj);\n const json = JSON.stringify(normalized);\n return createHash(\"sha256\").update(json, \"utf-8\").digest(\"hex\");\n}\n\nfunction sortKeys(value: unknown): unknown {\n if (value === null || value === undefined) return value;\n if (Array.isArray(value)) {\n return value.map(sortKeys);\n }\n if (typeof value === \"object\") {\n const sorted: Record<string, unknown> = {};\n for (const key of Object.keys(value as Record<string, unknown>).sort()) {\n sorted[key] = sortKeys((value as Record<string, unknown>)[key]);\n }\n return sorted;\n }\n return value;\n}\n","import matter from \"gray-matter\";\n\nexport interface MarkdownFile<T = Record<string, unknown>> {\n frontmatter: T;\n content: string;\n}\n\nexport function parseMarkdown<T = Record<string, unknown>>(\n raw: string\n): MarkdownFile<T> {\n const { data, content } = matter(raw);\n return {\n frontmatter: data as T,\n content: content.trim(),\n };\n}\n\nexport function serializeMarkdown<T extends Record<string, unknown>>(\n doc: MarkdownFile<T>\n): string {\n return matter.stringify(doc.content + \"\\n\", doc.frontmatter);\n}\n","/**\n * Convert text to filesystem-safe slug, handling all Unicode scripts.\n *\n * Strategy:\n * 1. Latin with diacritics (Vietnamese, French...) → decompose + strip marks → ASCII\n * 2. Non-Latin scripts (CJK, Arabic, Cyrillic...) → keep as-is (valid in filenames)\n * 3. Only strip characters unsafe for filesystems: / \\ : * ? \" < > |\n *\n * Examples:\n * \"Tự động ghi nhận\" → \"tu-dong-ghi-nhan\"\n * \"自动记录工作时间\" → \"自动记录工作时间\"\n * \"Sprint 1 - 自动记录\" → \"sprint-1-自动记录\"\n */\nexport function slugify(text: string): string {\n return text\n .normalize(\"NFD\") // decompose: ự → u + combining marks\n .replace(/[\\u0300-\\u036f]/g, \"\") // strip combining diacritical marks\n .replace(/đ/g, \"d\") // handle đ separately (NFD doesn't decompose đ)\n .replace(/Đ/g, \"D\")\n .toLowerCase()\n .replace(/[\\/\\\\:*?\"<>|#%&{}@!`^~]/g, \"\") // strip filesystem-unsafe chars\n .replace(/[\\s_]+/g, \"-\") // spaces/underscores → hyphen\n .replace(/-+/g, \"-\") // collapse multiple hyphens\n .replace(/^-|-$/g, \"\") // trim leading/trailing hyphens\n .slice(0, 50);\n}\n","import { readFile, writeFile, readdir, mkdir, rename, unlink } from \"node:fs/promises\";\nimport { join, basename } from \"node:path\";\nimport { createDiff } from \"../core/diff.js\";\nimport { GitLabClient } from \"../core/gitlab-client.js\";\nimport {\n readObject,\n writeObject,\n listObjects,\n type ObjectSnapshot,\n} from \"../core/object-store.js\";\nimport { parseMarkdown, serializeMarkdown } from \"../core/markdown.js\";\n\nexport interface PullOptions {\n force?: boolean;\n backup?: boolean;\n}\n\nexport interface PushOptions {\n force?: boolean;\n yes?: boolean;\n}\n\nexport interface DiffResult {\n id: number | string | null;\n filename: string;\n status: \"added\" | \"modified\" | \"deleted\" | \"conflict\" | \"unchanged\";\n diff?: string;\n}\n\nexport abstract class BaseResource<T extends Record<string, unknown>> {\n abstract readonly resourceType: string; // \"issues\" | \"milestones\" | \"labels\"\n abstract readonly apiPath: string; // \"/projects/:id/issues\"\n abstract readonly localDir: string; // \".gitlab/issues\"\n\n constructor(\n protected client: GitLabClient,\n protected projectRoot: string\n ) {}\n\n // --- Abstract methods (each resource implements) ---\n\n abstract toMarkdown(item: T): string;\n abstract fromMarkdown(content: string): Partial<T>;\n abstract computeHash(item: T): string;\n abstract toApiPayload(item: Partial<T>): object;\n abstract filenameFor(item: T): string;\n abstract getId(item: T): number | string;\n /** ID used in API URLs. Issues use iid, others use id. */\n abstract getApiId(item: Partial<T>): number | string | null;\n\n // --- Shared workflow ---\n\n protected localPath(): string {\n return join(this.projectRoot, this.localDir);\n }\n\n /** Fetch all items from GitLab API and save to object store */\n async fetch(): Promise<number> {\n const items = await this.client.listAll<T>(this.apiPath);\n\n for (const item of items) {\n const id = this.getId(item);\n const serverHash = this.computeHash(item);\n\n const existing = await readObject(\n this.projectRoot,\n this.resourceType,\n id\n );\n\n await writeObject(this.projectRoot, this.resourceType, {\n id,\n base_hash: existing?.base_hash ?? \"\",\n server_hash: serverHash,\n data: item,\n fetched_at: new Date().toISOString(),\n });\n }\n\n return items.length;\n }\n\n /** Pull: fetch first, then write .md files from object store */\n async pull(opts: PullOptions = {}): Promise<{\n updated: string[];\n conflicts: string[];\n skipped: string[];\n }> {\n await this.fetch();\n\n const objects = await listObjects<T>(this.projectRoot, this.resourceType);\n const result = { updated: [] as string[], conflicts: [] as string[], skipped: [] as string[] };\n\n await mkdir(this.localPath(), { recursive: true });\n\n // Build map of ID → existing local filename for rename detection\n const idToLocalFile = new Map<number | string, string>();\n const localFiles = await this.listLocalFiles();\n for (const file of localFiles) {\n const content = await safeReadFile(join(this.localPath(), file));\n if (content !== null) {\n const data = this.fromMarkdown(content) as T;\n const id = this.getId(data);\n if (id !== null && id !== undefined) {\n idToLocalFile.set(id, file);\n }\n }\n }\n\n for (const obj of objects) {\n const filename = this.filenameFor(obj.data);\n const filePath = join(this.localPath(), filename);\n const objId = this.getId(obj.data);\n\n // Remove old file if filename changed (e.g. title renamed on server)\n const oldFile = idToLocalFile.get(objId);\n if (oldFile && oldFile !== filename) {\n await unlink(join(this.localPath(), oldFile));\n }\n\n // Check if local file exists and has changes\n const localContent = await safeReadFile(filePath);\n if (localContent !== null) {\n const localData = this.fromMarkdown(localContent) as T;\n const localHash = this.computeHash(localData);\n\n // Local has changes not yet pushed\n if (localHash !== obj.base_hash && obj.base_hash !== \"\") {\n if (!opts.force) {\n if (opts.backup) {\n await rename(filePath, filePath + \".bak\");\n } else {\n result.conflicts.push(filename);\n continue;\n }\n }\n }\n\n // No change from server\n if (obj.server_hash === obj.base_hash) {\n result.skipped.push(filename);\n continue;\n }\n }\n\n // Write .md file\n const md = this.toMarkdown(obj.data);\n await writeFile(filePath, md, \"utf-8\");\n\n // Update base_hash to match server\n await writeObject(this.projectRoot, this.resourceType, {\n ...obj,\n base_hash: obj.server_hash,\n });\n\n result.updated.push(filename);\n }\n\n return result;\n }\n\n /** Push: read local .md, conflict check, then update GitLab */\n async push(opts: PushOptions = {}): Promise<{\n pushed: string[];\n created: string[];\n conflicts: string[];\n skipped: string[];\n }> {\n const result = {\n pushed: [] as string[],\n created: [] as string[],\n conflicts: [] as string[],\n skipped: [] as string[],\n };\n\n const files = await this.listLocalFiles();\n\n for (const file of files) {\n const filePath = join(this.localPath(), file);\n const content = await readFile(filePath, \"utf-8\");\n const localData = this.fromMarkdown(content);\n const localHash = this.computeHash(localData as T);\n\n const apiId = this.getApiId(localData);\n\n // --- Create new resource ---\n if (apiId === null || apiId === undefined) {\n const payload = this.toApiPayload(localData);\n const created = await this.client.create<T>(this.apiPath, payload);\n const newId = this.getId(created);\n const serverHash = this.computeHash(created);\n\n // Update local file with new ID\n const newMd = this.toMarkdown(created);\n const newFilename = this.filenameFor(created);\n await writeFile(join(this.localPath(), newFilename), newMd, \"utf-8\");\n\n // Remove old file if filename changed\n if (newFilename !== file) {\n const { unlink } = await import(\"node:fs/promises\");\n await unlink(filePath);\n }\n\n // Save object\n await writeObject(this.projectRoot, this.resourceType, {\n id: newId,\n base_hash: serverHash,\n server_hash: serverHash,\n data: created,\n fetched_at: new Date().toISOString(),\n });\n\n result.created.push(newFilename);\n continue;\n }\n\n // --- Update existing resource ---\n const storeId = this.getId(localData as T);\n const obj = await readObject<T>(this.projectRoot, this.resourceType, storeId);\n\n // No local changes\n if (obj && localHash === obj.base_hash) {\n result.skipped.push(file);\n continue;\n }\n\n // Conflict check: fetch real-time server state\n const serverItem = await this.client.get<T>(\n `${this.apiPath}/${encodeURIComponent(String(apiId))}`\n );\n const serverHash = this.computeHash(serverItem);\n\n if (obj && serverHash !== obj.base_hash && !opts.force) {\n result.conflicts.push(file);\n continue;\n }\n\n // Push update\n const payload = this.toApiPayload(localData);\n const updated = await this.client.update<T>(\n `${this.apiPath}/${encodeURIComponent(String(apiId))}`,\n payload\n );\n const updatedHash = this.computeHash(updated);\n\n // Update object store\n const updatedStoreId = this.getId(updated);\n await writeObject(this.projectRoot, this.resourceType, {\n id: updatedStoreId,\n base_hash: updatedHash,\n server_hash: updatedHash,\n data: updated,\n fetched_at: new Date().toISOString(),\n });\n\n result.pushed.push(file);\n }\n\n return result;\n }\n\n /** Diff: compare object store vs local .md files */\n async diff(): Promise<DiffResult[]> {\n const results: DiffResult[] = [];\n const objects = await listObjects<T>(this.projectRoot, this.resourceType);\n const files = await this.listLocalFiles();\n const processedFiles = new Set<string>();\n\n // Compare objects with local files\n for (const obj of objects) {\n const filename = this.filenameFor(obj.data);\n const filePath = join(this.localPath(), filename);\n processedFiles.add(filename);\n\n const localContent = await safeReadFile(filePath);\n\n if (localContent === null) {\n results.push({ id: obj.id, filename, status: \"deleted\" });\n continue;\n }\n\n const serverMd = this.toMarkdown(obj.data);\n if (localContent.trim() === serverMd.trim()) {\n results.push({ id: obj.id, filename, status: \"unchanged\" });\n continue;\n }\n\n results.push({\n id: obj.id,\n filename,\n status: \"modified\",\n diff: createDiff(filename, serverMd, localContent),\n });\n }\n\n // Files not in object store (new local files)\n for (const file of files) {\n if (!processedFiles.has(file)) {\n results.push({ id: null, filename: file, status: \"added\" });\n }\n }\n\n return results;\n }\n\n private async listLocalFiles(): Promise<string[]> {\n try {\n const files = await readdir(this.localPath());\n return files.filter((f) => f.endsWith(\".md\") && !f.endsWith(\".bak\"));\n } catch {\n return [];\n }\n }\n}\n\nasync function safeReadFile(path: string): Promise<string | null> {\n try {\n return await readFile(path, \"utf-8\");\n } catch {\n return null;\n }\n}\n","import { createTwoFilesPatch } from \"diff\";\nimport chalk from \"chalk\";\n\nexport function createDiff(\n filename: string,\n oldContent: string,\n newContent: string\n): string {\n return createTwoFilesPatch(\n `a/${filename}`,\n `b/${filename}`,\n oldContent,\n newContent\n );\n}\n\nexport function colorizeDiff(raw: string): string {\n return raw\n .split(\"\\n\")\n .map((line) => {\n if (line.startsWith(\"@@\")) return chalk.cyan(line);\n if (line.startsWith(\"---\")) return chalk.dim(line);\n if (line.startsWith(\"+++\")) return chalk.dim(line);\n if (line.startsWith(\"-\")) return chalk.red(line);\n if (line.startsWith(\"+\")) return chalk.green(line);\n if (line.startsWith(\"===\")) return \"\"; // hide separator\n if (line.startsWith(\"Index:\")) return \"\"; // hide index line\n return chalk.dim(line); // context lines\n })\n .filter((line) => line !== \"\")\n .join(\"\\n\");\n}\n\n/** Summarize changes in human-readable format */\nexport function summarizeDiff(raw: string): { added: number; removed: number } {\n let added = 0;\n let removed = 0;\n for (const line of raw.split(\"\\n\")) {\n if (line.startsWith(\"+\") && !line.startsWith(\"+++\")) added++;\n if (line.startsWith(\"-\") && !line.startsWith(\"---\")) removed++;\n }\n return { added, removed };\n}\n","import { readFile, writeFile, mkdir, readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { glsyncDir } from \"./config.js\";\n\nexport interface ObjectSnapshot<T = Record<string, unknown>> {\n id: number | string;\n base_hash: string;\n server_hash: string;\n data: T;\n fetched_at: string;\n}\n\nfunction objectDir(projectRoot: string, resource: string): string {\n return join(glsyncDir(projectRoot), \"objects\", resource);\n}\n\nfunction objectPath(\n projectRoot: string,\n resource: string,\n id: number | string\n): string {\n return join(objectDir(projectRoot, resource), `${id}.json`);\n}\n\nexport async function readObject<T>(\n projectRoot: string,\n resource: string,\n id: number | string\n): Promise<ObjectSnapshot<T> | null> {\n try {\n const raw = await readFile(objectPath(projectRoot, resource, id), \"utf-8\");\n return JSON.parse(raw) as ObjectSnapshot<T>;\n } catch {\n return null;\n }\n}\n\nexport async function writeObject<T>(\n projectRoot: string,\n resource: string,\n snapshot: ObjectSnapshot<T>\n): Promise<void> {\n const dir = objectDir(projectRoot, resource);\n await mkdir(dir, { recursive: true });\n await writeFile(\n objectPath(projectRoot, resource, snapshot.id),\n JSON.stringify(snapshot, null, 2) + \"\\n\",\n \"utf-8\"\n );\n}\n\nexport async function listObjects<T>(\n projectRoot: string,\n resource: string\n): Promise<ObjectSnapshot<T>[]> {\n const dir = objectDir(projectRoot, resource);\n let files: string[];\n try {\n files = await readdir(dir);\n } catch {\n return [];\n }\n\n const results: ObjectSnapshot<T>[] = [];\n for (const file of files) {\n if (!file.endsWith(\".json\")) continue;\n try {\n const raw = await readFile(join(dir, file), \"utf-8\");\n results.push(JSON.parse(raw) as ObjectSnapshot<T>);\n } catch {\n // skip corrupted files\n }\n }\n return results;\n}\n","import { deepHash } from \"../core/hasher.js\";\nimport { parseMarkdown, serializeMarkdown } from \"../core/markdown.js\";\nimport { slugify } from \"../utils/slugify.js\";\nimport { BaseResource } from \"./base.js\";\n\nexport interface GitLabIssue extends Record<string, unknown> {\n id: number;\n iid: number;\n title: string;\n description: string | null;\n labels: string[];\n milestone: { title: string } | null;\n assignees: { username: string }[];\n state: string;\n}\n\ninterface IssueFrontmatter {\n id: number | null;\n iid: number | null;\n title: string;\n labels: string[];\n milestone: string | null;\n assignees: string[];\n state: string;\n}\n\nexport class IssueResource extends BaseResource<GitLabIssue> {\n readonly resourceType = \"issues\";\n readonly apiPath = \"/projects/:id/issues\";\n readonly localDir = \".gitlab/issues\";\n\n getId(item: GitLabIssue): number {\n return item.iid;\n }\n\n getApiId(item: Partial<GitLabIssue>): number | null {\n return item.iid ?? null;\n }\n\n computeHash(item: Partial<GitLabIssue>): string {\n const labels = [...(item.labels ?? [])].sort();\n return deepHash({\n title: item.title ?? \"\",\n description: item.description ?? \"\",\n labels,\n milestone: item.milestone\n ? typeof item.milestone === \"string\"\n ? item.milestone\n : item.milestone.title\n : \"\",\n });\n }\n\n filenameFor(item: GitLabIssue): string {\n const slug = slugify(item.title);\n return `${item.iid}.${slug}.md`;\n }\n\n toMarkdown(item: GitLabIssue): string {\n const frontmatter: IssueFrontmatter = {\n id: item.id,\n iid: item.iid,\n title: item.title,\n labels: item.labels,\n milestone: item.milestone?.title ?? null,\n assignees: item.assignees?.map((a) => a.username) ?? [],\n state: item.state,\n };\n return serializeMarkdown({\n frontmatter: frontmatter as unknown as Record<string, unknown>,\n content: item.description ?? \"\",\n });\n }\n\n fromMarkdown(content: string): Partial<GitLabIssue> {\n const { frontmatter, content: body } = parseMarkdown<IssueFrontmatter>(content);\n return {\n id: frontmatter.id ?? undefined,\n iid: frontmatter.iid ?? undefined,\n title: frontmatter.title,\n description: body,\n labels: frontmatter.labels ?? [],\n milestone: frontmatter.milestone\n ? ({ title: frontmatter.milestone } as GitLabIssue[\"milestone\"])\n : null,\n assignees: (frontmatter.assignees ?? []).map(\n (u) => ({ username: u }) as GitLabIssue[\"assignees\"][0]\n ),\n state: frontmatter.state,\n } as Partial<GitLabIssue>;\n }\n\n toApiPayload(item: Partial<GitLabIssue>): object {\n return {\n title: item.title,\n description: item.description ?? \"\",\n labels: (item.labels ?? []).join(\",\"),\n milestone_id: undefined, // TODO: resolve milestone title → id\n assignee_ids: undefined, // TODO: resolve usernames → ids\n };\n }\n}\n\n","import { deepHash } from \"../core/hasher.js\";\nimport { parseMarkdown, serializeMarkdown } from \"../core/markdown.js\";\nimport { slugify } from \"../utils/slugify.js\";\nimport { BaseResource } from \"./base.js\";\n\nexport interface GitLabMilestone extends Record<string, unknown> {\n id: number;\n title: string;\n description: string | null;\n state: string;\n start_date: string | null;\n due_date: string | null;\n}\n\ninterface MilestoneFrontmatter {\n id: number | null;\n title: string;\n state: string;\n start_date: string | null;\n due_date: string | null;\n}\n\nexport class MilestoneResource extends BaseResource<GitLabMilestone> {\n readonly resourceType = \"milestones\";\n readonly apiPath = \"/projects/:id/milestones\";\n readonly localDir = \".gitlab/milestones\";\n\n getId(item: GitLabMilestone): number {\n return item.id;\n }\n\n getApiId(item: Partial<GitLabMilestone>): number | null {\n return item.id ?? null;\n }\n\n computeHash(item: Partial<GitLabMilestone>): string {\n return deepHash({\n title: item.title ?? \"\",\n description: item.description ?? \"\",\n due_date: item.due_date ?? \"\",\n start_date: item.start_date ?? \"\",\n state: item.state ?? \"\",\n });\n }\n\n filenameFor(item: GitLabMilestone): string {\n return `${slugify(item.title)}.md`;\n }\n\n toMarkdown(item: GitLabMilestone): string {\n const frontmatter: MilestoneFrontmatter = {\n id: item.id,\n title: item.title,\n state: item.state,\n start_date: item.start_date,\n due_date: item.due_date,\n };\n return serializeMarkdown({\n frontmatter: frontmatter as unknown as Record<string, unknown>,\n content: item.description ?? \"\",\n });\n }\n\n fromMarkdown(content: string): Partial<GitLabMilestone> {\n const { frontmatter, content: body } =\n parseMarkdown<MilestoneFrontmatter>(content);\n return {\n id: frontmatter.id ?? undefined,\n title: frontmatter.title,\n description: body,\n state: frontmatter.state,\n start_date: frontmatter.start_date,\n due_date: frontmatter.due_date,\n } as Partial<GitLabMilestone>;\n }\n\n toApiPayload(item: Partial<GitLabMilestone>): object {\n return {\n title: item.title,\n description: item.description ?? \"\",\n start_date: item.start_date ?? undefined,\n due_date: item.due_date ?? undefined,\n };\n }\n}\n","import { deepHash } from \"../core/hasher.js\";\nimport { parseMarkdown, serializeMarkdown } from \"../core/markdown.js\";\nimport { slugify } from \"../utils/slugify.js\";\nimport { BaseResource } from \"./base.js\";\n\nexport interface GitLabLabel extends Record<string, unknown> {\n id: number;\n name: string;\n color: string;\n description: string | null;\n priority: number | null;\n}\n\ninterface LabelFrontmatter {\n id: number | null;\n name: string;\n color: string;\n priority: number | null;\n}\n\nexport class LabelResource extends BaseResource<GitLabLabel> {\n readonly resourceType = \"labels\";\n readonly apiPath = \"/projects/:id/labels\";\n readonly localDir = \".gitlab/labels\";\n\n getId(item: GitLabLabel): number {\n return item.id;\n }\n\n getApiId(item: Partial<GitLabLabel>): number | null {\n return item.id ?? null;\n }\n\n computeHash(item: Partial<GitLabLabel>): string {\n return deepHash({\n name: item.name ?? \"\",\n color: item.color ?? \"\",\n description: item.description ?? \"\",\n priority: item.priority ?? null,\n });\n }\n\n filenameFor(item: GitLabLabel): string {\n return `${slugify(item.name)}.md`;\n }\n\n toMarkdown(item: GitLabLabel): string {\n const frontmatter: LabelFrontmatter = {\n id: item.id,\n name: item.name,\n color: item.color,\n priority: item.priority,\n };\n return serializeMarkdown({\n frontmatter: frontmatter as unknown as Record<string, unknown>,\n content: item.description ?? \"\",\n });\n }\n\n fromMarkdown(content: string): Partial<GitLabLabel> {\n const { frontmatter, content: body } =\n parseMarkdown<LabelFrontmatter>(content);\n return {\n id: frontmatter.id ?? undefined,\n name: frontmatter.name,\n color: frontmatter.color,\n description: body,\n priority: frontmatter.priority,\n } as Partial<GitLabLabel>;\n }\n\n toApiPayload(item: Partial<GitLabLabel>): object {\n return {\n name: item.name,\n color: item.color,\n description: item.description ?? \"\",\n priority: item.priority ?? undefined,\n };\n }\n}\n","import { GitLabClient } from \"../core/gitlab-client.js\";\nimport { type GlsyncConfig } from \"../core/config.js\";\nimport { BaseResource } from \"./base.js\";\nimport { IssueResource } from \"./issues.js\";\nimport { MilestoneResource } from \"./milestones.js\";\nimport { LabelResource } from \"./labels.js\";\n\nconst RESOURCE_MAP: Record<\n string,\n new (client: GitLabClient, root: string) => BaseResource<any>\n> = {\n issues: IssueResource,\n milestones: MilestoneResource,\n labels: LabelResource,\n};\n\nexport const ALL_RESOURCES = Object.keys(RESOURCE_MAP);\n\nexport function getResources(\n config: GlsyncConfig,\n projectRoot: string,\n filter?: string\n): BaseResource<any>[] {\n const client = new GitLabClient({\n baseUrl: config.gitlab_url,\n token: config.token,\n projectId: config.project_id,\n });\n\n const types = filter ? [filter] : ALL_RESOURCES;\n\n return types.map((type) => {\n const Ctor = RESOURCE_MAP[type];\n if (!Ctor) throw new Error(`Unknown resource type: ${type}`);\n return new Ctor(client, projectRoot);\n });\n}\n","import { readConfig } from \"../core/config.js\";\nimport { getResources } from \"../resources/index.js\";\nimport { log, spinner } from \"../utils/logger.js\";\n\nexport async function fetchCommand(\n projectRoot: string,\n resource?: string\n): Promise<void> {\n const config = await readConfig(projectRoot);\n const resources = getResources(config, projectRoot, resource);\n\n for (const res of resources) {\n const s = spinner(`Fetching ${res.resourceType}...`).start();\n try {\n const count = await res.fetch();\n s.succeed(`Fetched ${count} ${res.resourceType}`);\n } catch (err) {\n s.fail(`Failed to fetch ${res.resourceType}`);\n log.error(String(err));\n }\n }\n}\n","import { readConfig } from \"../core/config.js\";\nimport { getResources } from \"../resources/index.js\";\nimport { log, spinner } from \"../utils/logger.js\";\n\nexport async function pullCommand(\n projectRoot: string,\n opts: { resource?: string; force?: boolean; backup?: boolean }\n): Promise<void> {\n const config = await readConfig(projectRoot);\n const resources = getResources(config, projectRoot, opts.resource);\n\n for (const res of resources) {\n const s = spinner(`Pulling ${res.resourceType}...`).start();\n try {\n const result = await res.pull({\n force: opts.force,\n backup: opts.backup,\n });\n\n s.succeed(`Pulled ${res.resourceType}`);\n\n if (result.updated.length > 0) {\n log.success(` Updated: ${result.updated.join(\", \")}`);\n }\n if (result.skipped.length > 0) {\n log.dim(` Skipped (no change): ${result.skipped.length} files`);\n }\n if (result.conflicts.length > 0) {\n log.warn(` Conflicts: ${result.conflicts.join(\", \")}`);\n log.warn(\" Use --force to overwrite or --backup to save .bak first.\");\n }\n } catch (err) {\n s.fail(`Failed to pull ${res.resourceType}`);\n log.error(String(err));\n }\n }\n}\n","import chalk from \"chalk\";\nimport { readConfig } from \"../core/config.js\";\nimport { colorizeDiff, summarizeDiff } from \"../core/diff.js\";\nimport { getResources } from \"../resources/index.js\";\nimport { log } from \"../utils/logger.js\";\n\nexport async function diffCommand(\n projectRoot: string,\n resource?: string\n): Promise<void> {\n const config = await readConfig(projectRoot);\n const resources = getResources(config, projectRoot, resource);\n\n let hasChanges = false;\n let totalAdded = 0;\n let totalRemoved = 0;\n let totalFiles = 0;\n\n for (const res of resources) {\n const results = await res.diff();\n\n const changed = results.filter((r) => r.status !== \"unchanged\");\n if (changed.length === 0) continue;\n\n hasChanges = true;\n console.log(chalk.bold.underline(`\\n${res.resourceType}`));\n\n for (const item of changed) {\n totalFiles++;\n switch (item.status) {\n case \"added\":\n console.log(chalk.green(` + ${item.filename}`) + chalk.dim(\" (new local file)\"));\n break;\n case \"deleted\":\n console.log(chalk.red(` - ${item.filename}`) + chalk.dim(\" (missing local file)\"));\n break;\n case \"modified\": {\n const { added, removed } = item.diff ? summarizeDiff(item.diff) : { added: 0, removed: 0 };\n totalAdded += added;\n totalRemoved += removed;\n const stat = [\n added > 0 ? chalk.green(`+${added}`) : null,\n removed > 0 ? chalk.red(`-${removed}`) : null,\n ].filter(Boolean).join(\" \");\n console.log(chalk.yellow(` ~ ${item.filename}`) + ` ${stat}`);\n if (item.diff) {\n console.log(colorizeDiff(item.diff));\n }\n break;\n }\n case \"conflict\":\n console.log(chalk.red.bold(` ! ${item.filename}`) + chalk.dim(\" (conflict)\"));\n break;\n }\n }\n }\n\n if (!hasChanges) {\n log.success(\"No changes detected.\");\n } else {\n // Summary line\n const parts = [\n `${totalFiles} file${totalFiles > 1 ? \"s\" : \"\"} changed`,\n totalAdded > 0 ? chalk.green(`+${totalAdded}`) : null,\n totalRemoved > 0 ? chalk.red(`-${totalRemoved}`) : null,\n ].filter(Boolean).join(\", \");\n console.log(chalk.dim(`\\n${parts}`));\n }\n}\n","import { createInterface } from \"node:readline\";\nimport { readConfig } from \"../core/config.js\";\nimport { getResources } from \"../resources/index.js\";\nimport { log, spinner } from \"../utils/logger.js\";\n\nexport async function pushCommand(\n projectRoot: string,\n opts: { resource?: string; force?: boolean; yes?: boolean }\n): Promise<void> {\n const config = await readConfig(projectRoot);\n const resources = getResources(config, projectRoot, opts.resource);\n\n // Force push warning + confirmation\n if (opts.force && !opts.yes) {\n log.warn(\"Force push will bypass conflict checks and overwrite server data.\");\n const confirmed = await confirm(\"Continue? [y/N] \");\n if (!confirmed) {\n log.info(\"Push cancelled.\");\n return;\n }\n }\n\n for (const res of resources) {\n const s = spinner(`Pushing ${res.resourceType}...`).start();\n try {\n const result = await res.push({\n force: opts.force,\n yes: opts.yes,\n });\n\n s.succeed(`Pushed ${res.resourceType}`);\n\n if (result.created.length > 0) {\n log.success(` Created: ${result.created.join(\", \")}`);\n }\n if (result.pushed.length > 0) {\n log.success(` Updated: ${result.pushed.join(\", \")}`);\n }\n if (result.skipped.length > 0) {\n log.dim(` Skipped (no change): ${result.skipped.length} files`);\n }\n if (result.conflicts.length > 0) {\n log.warn(` Conflicts: ${result.conflicts.join(\", \")}`);\n log.warn(\" Run `glsync pull` first, or use --force to override.\");\n }\n } catch (err) {\n s.fail(`Failed to push ${res.resourceType}`);\n log.error(String(err));\n }\n }\n}\n\nfunction confirm(question: string): Promise<boolean> {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n return new Promise((resolve) => {\n rl.question(question, (answer) => {\n rl.close();\n resolve(answer.toLowerCase() === \"y\");\n });\n });\n}\n"],"mappings":"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","names":["mkdir","readFile","writeFile","join","cwd","mkdir","join","readFile","writeFile","readFile","writeFile","readdir","mkdir","join","chalk","readFile","writeFile","mkdir","join","join","readFile","mkdir","writeFile","join","mkdir","writeFile","readFile","payload","serverHash","unlink","readdir","chalk","chalk","createInterface","createInterface"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@anthais/glsync",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI tool for two-way sync between GitLab and local Markdown files",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"glsync": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup",
|
|
14
|
+
"dev": "tsup --watch",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"test:watch": "vitest"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"gitlab",
|
|
20
|
+
"sync",
|
|
21
|
+
"cli",
|
|
22
|
+
"markdown",
|
|
23
|
+
"issues"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"chalk": "^5.6.2",
|
|
28
|
+
"commander": "^14.0.3",
|
|
29
|
+
"diff": "^8.0.4",
|
|
30
|
+
"gray-matter": "^4.0.3",
|
|
31
|
+
"ora": "^9.3.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/diff": "^7.0.2",
|
|
35
|
+
"@types/node": "^25.5.2",
|
|
36
|
+
"tsup": "^8.5.1",
|
|
37
|
+
"typescript": "^6.0.2",
|
|
38
|
+
"vitest": "^4.1.2"
|
|
39
|
+
}
|
|
40
|
+
}
|