@aloth/olcli 0.9.1 → 0.10.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 +38 -0
- package/dist/cli.js +164 -91
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +12 -13
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/diff.d.ts +85 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +168 -0
- package/dist/diff.js.map +1 -0
- package/dist/ignore.js.map +1 -1
- package/dist/mcp.js +96 -1
- package/dist/mcp.js.map +1 -1
- package/dist/remote-helper.js +1 -1
- package/dist/remote-helper.js.map +1 -1
- package/dist/scan.d.ts +42 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +57 -0
- package/dist/scan.js.map +1 -0
- package/package.json +8 -4
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@ Work with Overleaf projects directly from your command line. Edit locally with y
|
|
|
20
20
|
- ⬇️ **Pull** project files to local directory for offline editing
|
|
21
21
|
- ⬆️ **Push** local changes back to Overleaf
|
|
22
22
|
- 🔄 **Sync** bidirectionally with smart conflict detection
|
|
23
|
+
- 🔍 **Diff** local files against the live remote before pushing
|
|
23
24
|
- 🔀 **Git remote** — use Overleaf as a native git remote ([docs](docs/GIT-REMOTE.md))
|
|
24
25
|
- ✌️ **Two-way deletions** — files removed locally are deleted on Overleaf on next sync
|
|
25
26
|
- 🗑️ **Delete** and ✏️ **rename** remote files by path
|
|
@@ -132,6 +133,7 @@ All commands auto-detect the project when run from a synced directory (contains
|
|
|
132
133
|
| `olcli pull [project] [dir]` | Download project files to local directory |
|
|
133
134
|
| `olcli push [dir]` | Upload local changes to Overleaf (`--delete` also removes files deleted locally) |
|
|
134
135
|
| `olcli sync [dir]` | Bidirectional sync (pull + push) |
|
|
136
|
+
| `olcli diff [project] [dir]` | Show content-level changes between local files and the remote |
|
|
135
137
|
| `olcli upload <file> [project]` | Upload a single file (`--to <path>` sets the remote destination) |
|
|
136
138
|
| `olcli download <file> [project]` | Download a single file |
|
|
137
139
|
| `olcli delete <file> [project]` | Delete a remote file or folder (alias: `rm`) |
|
|
@@ -150,8 +152,11 @@ All commands auto-detect the project when run from a synced directory (contains
|
|
|
150
152
|
| `olcli comments delete <id>` | Delete a comment thread |
|
|
151
153
|
| `olcli ignored [dir]` | List ignore patterns in effect |
|
|
152
154
|
| `olcli config set-url <url>` | Set self-hosted base URL |
|
|
155
|
+
| `olcli config get-url` | Show the configured base URL |
|
|
153
156
|
| `olcli config set-cookie-name <name>` | Set session cookie name |
|
|
157
|
+
| `olcli config get-cookie-name` | Show the configured session cookie name |
|
|
154
158
|
| `olcli config set-timeout <ms>` | Set default HTTP timeout |
|
|
159
|
+
| `olcli config get-timeout` | Show the configured HTTP timeout |
|
|
155
160
|
| `olcli check` | Show config paths and credential sources |
|
|
156
161
|
|
|
157
162
|
### Compile Options
|
|
@@ -192,6 +197,39 @@ Useful in multi-doc projects: each `-r` run compiles the file as if it were the
|
|
|
192
197
|
- **Propagates local deletions** — use `--no-delete` to opt out
|
|
193
198
|
- Use `--dry-run` to preview without applying
|
|
194
199
|
|
|
200
|
+
### Diff
|
|
201
|
+
|
|
202
|
+
`olcli diff` compares the bytes of your local files against the project's
|
|
203
|
+
current contents and prints a unified diff.
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
olcli diff # every changed file, as patches
|
|
207
|
+
olcli diff --name-only # just the changed paths
|
|
208
|
+
olcli diff --file main.tex # one file
|
|
209
|
+
olcli diff -U 8 # wider context
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**The remote side is fetched fresh on every run.** The diff describes the
|
|
213
|
+
project as it is at that moment — which is what a subsequent `push` would
|
|
214
|
+
overwrite — not a comparison against your last `pull`. `.olcli.json` records
|
|
215
|
+
remote *paths*, never remote *contents*, so there is no stored snapshot to
|
|
216
|
+
compare against; and the whole project arrives in a single request, the same
|
|
217
|
+
one `pull` makes, so fetching fresh costs one round trip rather than one per
|
|
218
|
+
file. A collaborator editing between `diff` and `push` can still change the
|
|
219
|
+
outcome, which is why the fetch time is printed.
|
|
220
|
+
|
|
221
|
+
In the output, `a/` is the remote and `b/` is local: a `+` line is content
|
|
222
|
+
`push` would upload, a `-` line is content it would overwrite. Files that
|
|
223
|
+
differ only in bytes that are not text (PDFs, images) are reported as
|
|
224
|
+
`Binary files ... differ`. Both sides pass through the same ignore layers, so
|
|
225
|
+
build artifacts sitting on Overleaf are not reported as locally deleted.
|
|
226
|
+
|
|
227
|
+
`diff --name-only` and `push --dry-run` answer different questions and will
|
|
228
|
+
disagree. `push --dry-run` lists files whose **modification time** is newer
|
|
229
|
+
than the last pull, because that is what `push` uploads; `diff` lists files
|
|
230
|
+
whose **contents** actually differ. A file you touched without editing appears
|
|
231
|
+
in the first and not the second.
|
|
232
|
+
|
|
195
233
|
#### How deletion propagation works
|
|
196
234
|
|
|
197
235
|
`olcli` records a manifest of remote files in `.olcli.json`. On next sync:
|
package/dist/cli.js
CHANGED
|
@@ -12,9 +12,11 @@ import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
|
12
12
|
import { join, dirname, basename } from 'node:path';
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
14
14
|
import { OverleafClient } from './client.js';
|
|
15
|
-
import { resolveRemotePath, resolveWithin } from './paths.js';
|
|
15
|
+
import { resolveRemotePath, resolveWithin, normalizeRemotePath } from './paths.js';
|
|
16
16
|
import { planProjectRenames } from './rename-plan.js';
|
|
17
|
-
import {
|
|
17
|
+
import { scanLocalFiles } from './scan.js';
|
|
18
|
+
import { compareTrees, filterRemoteTree, renderFileDiff, statusLetter } from './diff.js';
|
|
19
|
+
import { loadIgnore } from './ignore.js';
|
|
18
20
|
// Read version from package.json
|
|
19
21
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
22
|
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
@@ -87,7 +89,7 @@ async function resolveProject(client, projectArg, dir = '.') {
|
|
|
87
89
|
return { id: projectArg, name: projectArg };
|
|
88
90
|
}
|
|
89
91
|
// Otherwise, look up by name
|
|
90
|
-
|
|
92
|
+
const proj = await client.getProject(projectArg);
|
|
91
93
|
if (!proj) {
|
|
92
94
|
throw new Error(`Project not found: ${projectArg}`);
|
|
93
95
|
}
|
|
@@ -276,21 +278,6 @@ program
|
|
|
276
278
|
process.exit(1);
|
|
277
279
|
}
|
|
278
280
|
});
|
|
279
|
-
function printFolder(folder, indent) {
|
|
280
|
-
// Print subfolders
|
|
281
|
-
for (const f of folder.folders || []) {
|
|
282
|
-
console.log(`${indent}📁 ${chalk.blue(f.name)}/`);
|
|
283
|
-
printFolder(f, indent + ' ');
|
|
284
|
-
}
|
|
285
|
-
// Print docs
|
|
286
|
-
for (const d of folder.docs || []) {
|
|
287
|
-
console.log(`${indent}📄 ${d.name}`);
|
|
288
|
-
}
|
|
289
|
-
// Print files
|
|
290
|
-
for (const f of folder.fileRefs || []) {
|
|
291
|
-
console.log(`${indent}📎 ${f.name}`);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
281
|
const commentsCmd = program
|
|
295
282
|
.command('comments')
|
|
296
283
|
.description('View and manage Overleaf comments');
|
|
@@ -955,7 +942,7 @@ program
|
|
|
955
942
|
continue;
|
|
956
943
|
}
|
|
957
944
|
}
|
|
958
|
-
catch
|
|
945
|
+
catch {
|
|
959
946
|
// File doesn't exist or can't stat, proceed with download
|
|
960
947
|
}
|
|
961
948
|
}
|
|
@@ -1075,51 +1062,19 @@ program
|
|
|
1075
1062
|
disableAll: options.ignore === false,
|
|
1076
1063
|
});
|
|
1077
1064
|
// Get list of files to upload
|
|
1078
|
-
const {
|
|
1065
|
+
const { files: localFileList, ignored: filesIgnored } = scanLocalFiles(targetDir, ignoreCtx);
|
|
1079
1066
|
const filesToUpload = [];
|
|
1080
|
-
const filesIgnored = [];
|
|
1081
1067
|
// Every local file that survives ignore filtering, regardless of mtime.
|
|
1082
1068
|
// filesToUpload is mtime-filtered and therefore useless as a deletion
|
|
1083
1069
|
// baseline: an unchanged file would look "absent" and get deleted.
|
|
1084
1070
|
const allLocalPaths = new Set();
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
//
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
// Skip hidden files and .olcli.json (always — predates ignore subsystem)
|
|
1091
|
-
if (entry.name.startsWith('.'))
|
|
1092
|
-
continue;
|
|
1093
|
-
const fullPath = join(currentDir, entry.name);
|
|
1094
|
-
const relativePath = relativeBase ? `${relativeBase}/${entry.name}` : entry.name;
|
|
1095
|
-
if (entry.isDirectory()) {
|
|
1096
|
-
// Test directory ignore (gitignore semantics: trailing slash matches dir)
|
|
1097
|
-
if (shouldIgnore(`${relativePath}/`, ignoreCtx)) {
|
|
1098
|
-
filesIgnored.push(`${relativePath}/`);
|
|
1099
|
-
continue;
|
|
1100
|
-
}
|
|
1101
|
-
scanDir(fullPath, relativePath);
|
|
1102
|
-
}
|
|
1103
|
-
else {
|
|
1104
|
-
if (shouldIgnore(relativePath, ignoreCtx, texSiblings)) {
|
|
1105
|
-
filesIgnored.push(relativePath);
|
|
1106
|
-
continue;
|
|
1107
|
-
}
|
|
1108
|
-
allLocalPaths.add(relativePath);
|
|
1109
|
-
// Check if file is newer than last pull (unless --all)
|
|
1110
|
-
if (options.all || !lastPull) {
|
|
1111
|
-
filesToUpload.push({ path: fullPath, relativePath });
|
|
1112
|
-
}
|
|
1113
|
-
else {
|
|
1114
|
-
const stats = statSync(fullPath);
|
|
1115
|
-
if (stats.mtime > lastPull) {
|
|
1116
|
-
filesToUpload.push({ path: fullPath, relativePath });
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1071
|
+
for (const file of localFileList) {
|
|
1072
|
+
allLocalPaths.add(file.relativePath);
|
|
1073
|
+
// Check if file is newer than last pull (unless --all)
|
|
1074
|
+
if (options.all || !lastPull || file.mtime > lastPull) {
|
|
1075
|
+
filesToUpload.push({ path: file.path, relativePath: file.relativePath });
|
|
1120
1076
|
}
|
|
1121
1077
|
}
|
|
1122
|
-
scanDir(targetDir);
|
|
1123
1078
|
if (options.showIgnored && filesIgnored.length > 0) {
|
|
1124
1079
|
spinner.stop();
|
|
1125
1080
|
console.log(chalk.bold(chalk.dim(`Ignored ${filesIgnored.length} file(s)/dir(s):`)));
|
|
@@ -1169,6 +1124,12 @@ program
|
|
|
1169
1124
|
if (noBaseline) {
|
|
1170
1125
|
console.log(chalk.dim(' --delete skipped: no manifest yet (first push from this directory)'));
|
|
1171
1126
|
}
|
|
1127
|
+
// This list is mtime-based: a file touched but not edited is in it, and
|
|
1128
|
+
// a file whose bytes already match the remote is too. `olcli diff`
|
|
1129
|
+
// compares content instead.
|
|
1130
|
+
if (filesToUpload.length > 0) {
|
|
1131
|
+
console.log(chalk.dim(' selected by modification time — run `olcli diff` to see content changes'));
|
|
1132
|
+
}
|
|
1172
1133
|
return;
|
|
1173
1134
|
}
|
|
1174
1135
|
// If --probe-folder is set, or if we don't have a cached rootFolderId, try probing
|
|
@@ -1334,41 +1295,17 @@ program
|
|
|
1334
1295
|
});
|
|
1335
1296
|
// Track local modifications
|
|
1336
1297
|
const localFiles = new Map();
|
|
1337
|
-
|
|
1338
|
-
const { readdirSync, statSync } = await import('node:fs');
|
|
1339
|
-
function scanLocalFiles(currentDir, relativeBase = '') {
|
|
1340
|
-
if (!existsSync(currentDir))
|
|
1341
|
-
return;
|
|
1342
|
-
const entries = readdirSync(currentDir, { withFileTypes: true });
|
|
1343
|
-
const texSiblings = buildTexSiblingSet(entries.filter((e) => !e.isDirectory()).map((e) => e.name));
|
|
1344
|
-
for (const entry of entries) {
|
|
1345
|
-
if (entry.name.startsWith('.'))
|
|
1346
|
-
continue;
|
|
1347
|
-
const fullPath = join(currentDir, entry.name);
|
|
1348
|
-
const relativePath = relativeBase ? `${relativeBase}/${entry.name}` : entry.name;
|
|
1349
|
-
if (entry.isDirectory()) {
|
|
1350
|
-
if (shouldIgnore(`${relativePath}/`, ignoreCtx)) {
|
|
1351
|
-
filesIgnored.push(`${relativePath}/`);
|
|
1352
|
-
continue;
|
|
1353
|
-
}
|
|
1354
|
-
scanLocalFiles(fullPath, relativePath);
|
|
1355
|
-
}
|
|
1356
|
-
else {
|
|
1357
|
-
if (shouldIgnore(relativePath, ignoreCtx, texSiblings)) {
|
|
1358
|
-
filesIgnored.push(relativePath);
|
|
1359
|
-
continue;
|
|
1360
|
-
}
|
|
1361
|
-
const stats = statSync(fullPath);
|
|
1362
|
-
localFiles.set(relativePath, {
|
|
1363
|
-
mtime: stats.mtime,
|
|
1364
|
-
content: readFileSync(fullPath)
|
|
1365
|
-
});
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1298
|
+
let filesIgnored = [];
|
|
1369
1299
|
// Read local files before overwriting
|
|
1370
|
-
if (existsSync(metaPath)) {
|
|
1371
|
-
scanLocalFiles(targetDir);
|
|
1300
|
+
if (existsSync(targetDir) && existsSync(metaPath)) {
|
|
1301
|
+
const scan = scanLocalFiles(targetDir, ignoreCtx);
|
|
1302
|
+
filesIgnored = scan.ignored;
|
|
1303
|
+
for (const file of scan.files) {
|
|
1304
|
+
localFiles.set(file.relativePath, {
|
|
1305
|
+
mtime: file.mtime,
|
|
1306
|
+
content: readFileSync(file.path)
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1372
1309
|
}
|
|
1373
1310
|
if (options.showIgnored && filesIgnored.length > 0) {
|
|
1374
1311
|
spinner.stop();
|
|
@@ -1553,6 +1490,142 @@ program
|
|
|
1553
1490
|
process.exit(1);
|
|
1554
1491
|
}
|
|
1555
1492
|
});
|
|
1493
|
+
program
|
|
1494
|
+
.command('diff [project] [dir]')
|
|
1495
|
+
.description('Show content-level differences between local files and the remote project')
|
|
1496
|
+
.option('--name-only', 'List changed paths instead of printing patches')
|
|
1497
|
+
.option('--file <path>', 'Diff a single file')
|
|
1498
|
+
.option('-U, --unified <n>', 'Lines of context around each hunk (default: 3)', parseInt)
|
|
1499
|
+
.option('--no-default-ignore', 'Disable built-in LaTeX artifact ignore list (only .olignore applies)')
|
|
1500
|
+
.option('--no-ignore', 'Disable all ignore filtering')
|
|
1501
|
+
.option('--cookie <session>', 'Session cookie override')
|
|
1502
|
+
.addHelpText('after', `
|
|
1503
|
+
The remote side is fetched fresh on every run, so the diff describes the
|
|
1504
|
+
project as it is right now - which is what a subsequent push would overwrite.
|
|
1505
|
+
It is not a comparison against the last pull. A collaborator editing between
|
|
1506
|
+
diff and push can still change the outcome; the fetch time is printed for that
|
|
1507
|
+
reason.`)
|
|
1508
|
+
.action(async (project, dir, options) => {
|
|
1509
|
+
const targetDir = dir || '.';
|
|
1510
|
+
if (!existsSync(targetDir)) {
|
|
1511
|
+
console.error(chalk.red(`Directory not found: ${targetDir}`));
|
|
1512
|
+
process.exit(1);
|
|
1513
|
+
}
|
|
1514
|
+
const spinner = ora('Connecting...').start();
|
|
1515
|
+
try {
|
|
1516
|
+
const client = await getClient(options.cookie);
|
|
1517
|
+
let resolved;
|
|
1518
|
+
try {
|
|
1519
|
+
resolved = await resolveProject(client, project, targetDir);
|
|
1520
|
+
}
|
|
1521
|
+
catch (error) {
|
|
1522
|
+
spinner.fail(error.message);
|
|
1523
|
+
console.error('Either run from a directory with .olcli.json or pass a project name/ID');
|
|
1524
|
+
process.exit(1);
|
|
1525
|
+
}
|
|
1526
|
+
const { id: projectId, name: projectName } = resolved;
|
|
1527
|
+
// The whole project arrives as one zip in a single request - the same
|
|
1528
|
+
// call pull and sync already make. Fetching per-file would mean one
|
|
1529
|
+
// request per file and could not tell us which files differ without
|
|
1530
|
+
// downloading them anyway.
|
|
1531
|
+
spinner.text = 'Fetching remote project...';
|
|
1532
|
+
const zipBuffer = await client.downloadProject(projectId);
|
|
1533
|
+
const fetchedAt = new Date();
|
|
1534
|
+
const AdmZip = (await import('adm-zip')).default;
|
|
1535
|
+
const zip = new AdmZip(zipBuffer);
|
|
1536
|
+
const ignoreCtx = loadIgnore(targetDir, {
|
|
1537
|
+
noDefaults: options.defaultIgnore === false,
|
|
1538
|
+
disableAll: options.ignore === false,
|
|
1539
|
+
});
|
|
1540
|
+
// Both sides go through the same filters; see filterRemoteTree.
|
|
1541
|
+
const remoteFiles = filterRemoteTree(zip.getEntries()
|
|
1542
|
+
.filter((e) => !e.isDirectory)
|
|
1543
|
+
.map((e) => ({ path: e.entryName, data: e.getData() })), ignoreCtx, (path) => resolveWithin(targetDir, path) !== null);
|
|
1544
|
+
const scan = scanLocalFiles(targetDir, ignoreCtx);
|
|
1545
|
+
const localFiles = new Map();
|
|
1546
|
+
for (const file of scan.files) {
|
|
1547
|
+
localFiles.set(file.relativePath, readFileSync(file.path));
|
|
1548
|
+
}
|
|
1549
|
+
let entries = compareTrees(localFiles, remoteFiles).filter((e) => e.status !== 'unchanged');
|
|
1550
|
+
if (options.file) {
|
|
1551
|
+
const wanted = normalizeRemotePath(options.file);
|
|
1552
|
+
entries = entries.filter((e) => e.path === wanted);
|
|
1553
|
+
if (entries.length === 0) {
|
|
1554
|
+
spinner.info(`No differences in ${wanted}`);
|
|
1555
|
+
if (!localFiles.has(wanted) && !remoteFiles.has(wanted)) {
|
|
1556
|
+
console.log(chalk.dim(' (file is on neither side, or is filtered by an ignore rule)'));
|
|
1557
|
+
}
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
spinner.stop();
|
|
1562
|
+
if (entries.length === 0) {
|
|
1563
|
+
console.log(chalk.green(`No differences — local files match "${projectName}"`));
|
|
1564
|
+
console.log(chalk.dim(` remote fetched ${fetchedAt.toISOString()}`));
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
if (options.nameOnly) {
|
|
1568
|
+
for (const e of entries) {
|
|
1569
|
+
const colour = e.status === 'added' ? chalk.green
|
|
1570
|
+
: e.status === 'deleted' ? chalk.red
|
|
1571
|
+
: chalk.yellow;
|
|
1572
|
+
console.log(`${colour(statusLetter(e.status))} ${e.path}`);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
else {
|
|
1576
|
+
for (const e of entries) {
|
|
1577
|
+
const patch = renderFileDiff(e, localFiles.get(e.path), remoteFiles.get(e.path), { context: options.unified });
|
|
1578
|
+
patch.replace(/\n$/, '').split('\n').forEach((line, index) => {
|
|
1579
|
+
console.log(colourizeDiffLine(line, index, e.binary));
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
console.log();
|
|
1584
|
+
// With --file the summary would count only the one file asked for, which
|
|
1585
|
+
// reads as "this is all that differs". Report totals only for a full run.
|
|
1586
|
+
if (!options.file) {
|
|
1587
|
+
const counts = {
|
|
1588
|
+
added: entries.filter((e) => e.status === 'added').length,
|
|
1589
|
+
modified: entries.filter((e) => e.status === 'modified').length,
|
|
1590
|
+
deleted: entries.filter((e) => e.status === 'deleted').length,
|
|
1591
|
+
};
|
|
1592
|
+
console.log(chalk.bold(`${entries.length} file(s) differ from "${projectName}": ` +
|
|
1593
|
+
`${counts.added} added, ${counts.modified} modified, ${counts.deleted} remote-only`));
|
|
1594
|
+
if (counts.deleted > 0) {
|
|
1595
|
+
console.log(chalk.dim(' remote-only files are left alone by push; use push --delete to remove them'));
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
console.log(chalk.dim(` a/ = remote as of ${fetchedAt.toISOString()}, b/ = local`));
|
|
1599
|
+
setLastProject(projectId);
|
|
1600
|
+
}
|
|
1601
|
+
catch (error) {
|
|
1602
|
+
spinner.fail(`Failed: ${error.message}`);
|
|
1603
|
+
process.exit(1);
|
|
1604
|
+
}
|
|
1605
|
+
});
|
|
1606
|
+
/**
|
|
1607
|
+
* Colourize one line of a rendered patch. chalk already no-ops when stdout is
|
|
1608
|
+
* not a TTY, so this needs no flag of its own.
|
|
1609
|
+
*
|
|
1610
|
+
* The file headers are identified by position, not by prefix: a removed line
|
|
1611
|
+
* whose own content starts with `--` renders as `--- something` and would
|
|
1612
|
+
* otherwise be mistaken for the `---` header and shown as unchanged.
|
|
1613
|
+
*/
|
|
1614
|
+
function colourizeDiffLine(line, index, binary) {
|
|
1615
|
+
if (index === 0)
|
|
1616
|
+
return chalk.bold(line); // our `diff --olcli` header
|
|
1617
|
+
if (binary)
|
|
1618
|
+
return chalk.magenta(line); // the single summary line
|
|
1619
|
+
if (index <= 2)
|
|
1620
|
+
return chalk.bold(line); // `---` / `+++`
|
|
1621
|
+
if (line.startsWith('@@'))
|
|
1622
|
+
return chalk.cyan(line);
|
|
1623
|
+
if (line.startsWith('+'))
|
|
1624
|
+
return chalk.green(line);
|
|
1625
|
+
if (line.startsWith('-'))
|
|
1626
|
+
return chalk.red(line);
|
|
1627
|
+
return line;
|
|
1628
|
+
}
|
|
1556
1629
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1557
1630
|
// HELP
|
|
1558
1631
|
// ─────────────────────────────────────────────────────────────────────────────
|