@aloth/olcli 0.7.0 → 0.9.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 +24 -3
- package/dist/cli.js +231 -14
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +18 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +46 -18
- package/dist/client.js.map +1 -1
- package/dist/mcp.js +71 -9
- package/dist/mcp.js.map +1 -1
- package/dist/paths.d.ts +28 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +64 -0
- package/dist/paths.js.map +1 -0
- package/dist/rename-plan.d.ts +46 -0
- package/dist/rename-plan.d.ts.map +1 -0
- package/dist/rename-plan.js +81 -0
- package/dist/rename-plan.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,6 +103,8 @@ olcli push
|
|
|
103
103
|
|
|
104
104
|
```bash
|
|
105
105
|
olcli pdf
|
|
106
|
+
# Compile a specific .tex file (for multi-doc projects):
|
|
107
|
+
olcli pdf -r appendix.tex
|
|
106
108
|
```
|
|
107
109
|
|
|
108
110
|
### 5. Or use native git commands
|
|
@@ -128,12 +130,14 @@ All commands auto-detect the project when run from a synced directory (contains
|
|
|
128
130
|
| `olcli list` | List all projects |
|
|
129
131
|
| `olcli info [project]` | Show project details and file list |
|
|
130
132
|
| `olcli pull [project] [dir]` | Download project files to local directory |
|
|
131
|
-
| `olcli push [dir]` | Upload local changes to Overleaf |
|
|
133
|
+
| `olcli push [dir]` | Upload local changes to Overleaf (`--delete` also removes files deleted locally) |
|
|
132
134
|
| `olcli sync [dir]` | Bidirectional sync (pull + push) |
|
|
133
|
-
| `olcli upload <file> [project]` | Upload a single file |
|
|
135
|
+
| `olcli upload <file> [project]` | Upload a single file (`--to <path>` sets the remote destination) |
|
|
134
136
|
| `olcli download <file> [project]` | Download a single file |
|
|
135
137
|
| `olcli delete <file> [project]` | Delete a remote file or folder (alias: `rm`) |
|
|
136
138
|
| `olcli rename <old> <new> [project]` | Rename a remote file or folder (alias: `mv`) |
|
|
139
|
+
| `olcli project rename <new> [project]` | Rename the project itself (`--dry-run`) |
|
|
140
|
+
| `olcli project rename-bulk` | Rename many projects by pattern (dry-run unless `--apply`) |
|
|
137
141
|
| `olcli compile [project]` | Trigger PDF compilation |
|
|
138
142
|
| `olcli pdf [project]` | Compile and download PDF |
|
|
139
143
|
| `olcli output [type]` | Download compile output files |
|
|
@@ -150,6 +154,16 @@ All commands auto-detect the project when run from a synced directory (contains
|
|
|
150
154
|
| `olcli config set-timeout <ms>` | Set default HTTP timeout |
|
|
151
155
|
| `olcli check` | Show config paths and credential sources |
|
|
152
156
|
|
|
157
|
+
### Compile Options
|
|
158
|
+
|
|
159
|
+
The compile-related commands (`compile`, `pdf`, `output`) accept:
|
|
160
|
+
|
|
161
|
+
| Flag | Description |
|
|
162
|
+
|------|-------------|
|
|
163
|
+
| `-r, --resource <path>` | Compile a specific `.tex` file as the root document (e.g. `appendix.tex`, `folder/test.tex`) |
|
|
164
|
+
|
|
165
|
+
Useful in multi-doc projects: each `-r` run compiles the file as if it were the main document.
|
|
166
|
+
|
|
153
167
|
### Global Options
|
|
154
168
|
|
|
155
169
|
| Flag | Description |
|
|
@@ -252,11 +266,18 @@ olcli sync && olcli pdf -o draft.pdf
|
|
|
252
266
|
# Quick PDF download
|
|
253
267
|
olcli pdf "Conference Paper" -o paper.pdf
|
|
254
268
|
|
|
269
|
+
# Compile a specific root document
|
|
270
|
+
olcli pdf "Conference Paper" -r appendix.tex -o appendix.pdf
|
|
271
|
+
olcli compile "Conference Paper" -r folder/test.tex
|
|
272
|
+
|
|
255
273
|
# Upload figures
|
|
256
|
-
olcli upload figures/diagram.png
|
|
274
|
+
olcli upload figures/diagram.png # relative path is preserved
|
|
275
|
+
olcli upload /tmp/build/diagram.png # absolute path lands in the project root
|
|
276
|
+
olcli upload /tmp/build/diagram.png --to figures/diagram.png # explicit destination
|
|
257
277
|
|
|
258
278
|
# arXiv submission prep
|
|
259
279
|
olcli output bbl -o main.bbl
|
|
280
|
+
olcli output bbl -r folder/test.tex -o main.bbl # compile artifacts from a specific root doc
|
|
260
281
|
olcli zip -o arxiv-submission.zip
|
|
261
282
|
|
|
262
283
|
# Backup all projects
|
package/dist/cli.js
CHANGED
|
@@ -12,6 +12,8 @@ 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 } from './paths.js';
|
|
16
|
+
import { planProjectRenames } from './rename-plan.js';
|
|
15
17
|
import { loadIgnore, shouldIgnore, buildTexSiblingSet, } from './ignore.js';
|
|
16
18
|
// Read version from package.json
|
|
17
19
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -545,6 +547,7 @@ program
|
|
|
545
547
|
.command('pdf [project]')
|
|
546
548
|
.description('Compile and download PDF')
|
|
547
549
|
.option('-o, --output <path>', 'Output path (default: <project-name>.pdf)')
|
|
550
|
+
.option('-r, --resource <path>', 'Compile this .tex file as root document (e.g. paper.tex, folder/test.tex)')
|
|
548
551
|
.option('--cookie <session>', 'Session cookie override')
|
|
549
552
|
.action(async (project, options) => {
|
|
550
553
|
const spinner = ora('Compiling project...').start();
|
|
@@ -552,7 +555,7 @@ program
|
|
|
552
555
|
const client = await getClient(options.cookie);
|
|
553
556
|
const proj = await resolveProject(client, project);
|
|
554
557
|
spinner.text = 'Compiling...';
|
|
555
|
-
const pdf = await client.downloadPdf(proj.id);
|
|
558
|
+
const pdf = await client.downloadPdf(proj.id, undefined, options.resource);
|
|
556
559
|
const outputPath = options.output || `${proj.name.replace(/[^a-zA-Z0-9-_]/g, '_')}.pdf`;
|
|
557
560
|
writeFileSync(outputPath, pdf);
|
|
558
561
|
spinner.succeed(`Downloaded PDF: ${outputPath} (${(pdf.length / 1024).toFixed(1)} KB)`);
|
|
@@ -567,6 +570,7 @@ program
|
|
|
567
570
|
.command('output [type]')
|
|
568
571
|
.description('Download compile output files (bbl, log, aux, etc.)')
|
|
569
572
|
.option('-o, --output <path>', 'Output path')
|
|
573
|
+
.option('-r, --resource <path>', 'Compile this .tex file as root document (e.g. paper.tex, folder/test.tex)')
|
|
570
574
|
.option('--list', 'List available output files')
|
|
571
575
|
.option('--project <name>', 'Project name or ID')
|
|
572
576
|
.option('--cookie <session>', 'Session cookie override')
|
|
@@ -587,9 +591,9 @@ program
|
|
|
587
591
|
}
|
|
588
592
|
}
|
|
589
593
|
const proj = await resolveProject(client, projectArg);
|
|
590
|
-
const result = await client.compileWithOutputs(proj.id);
|
|
594
|
+
const result = await client.compileWithOutputs(proj.id, options.resource);
|
|
591
595
|
if (result.status !== 'success') {
|
|
592
|
-
spinner.warn(`Compilation ${result.status}, but output files may still be available`);
|
|
596
|
+
spinner.warn(`Compilation ${result.status}, but output files may still be available${result.failureHint ?? ''}`);
|
|
593
597
|
}
|
|
594
598
|
if (options.list || !actualType) {
|
|
595
599
|
spinner.stop();
|
|
@@ -627,6 +631,7 @@ program
|
|
|
627
631
|
program
|
|
628
632
|
.command('upload <file> [project]')
|
|
629
633
|
.description('Upload a file to a project')
|
|
634
|
+
.option('--to <path>', 'Destination path within the project (default: derived from <file>)')
|
|
630
635
|
.option('--folder <id>', 'Target folder ID (default: root)')
|
|
631
636
|
.option('--cookie <session>', 'Session cookie override')
|
|
632
637
|
.action(async (file, project, options) => {
|
|
@@ -639,11 +644,11 @@ program
|
|
|
639
644
|
process.exit(1);
|
|
640
645
|
}
|
|
641
646
|
const content = readFileSync(file);
|
|
642
|
-
//
|
|
643
|
-
//
|
|
644
|
-
//
|
|
645
|
-
//
|
|
646
|
-
const fileName = file
|
|
647
|
+
// Derive the remote path: an explicit --to wins, absolute paths collapse
|
|
648
|
+
// to their basename, relative paths keep their directory part so
|
|
649
|
+
// 'figures/fig01.png' still lands in the 'figures' folder. uploadFile()
|
|
650
|
+
// lazy-resolves the folder tree when no folderId/tree is supplied.
|
|
651
|
+
const fileName = resolveRemotePath(file, options.to);
|
|
647
652
|
// Pass folder ID or null for root folder (client will compute it)
|
|
648
653
|
const folderId = options.folder || null;
|
|
649
654
|
const result = await client.uploadFile(proj.id, folderId, fileName, content);
|
|
@@ -705,18 +710,153 @@ program
|
|
|
705
710
|
}
|
|
706
711
|
});
|
|
707
712
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
713
|
+
// PROJECT COMMANDS (rename the project itself, not files inside it)
|
|
714
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
715
|
+
const projectCmd = program
|
|
716
|
+
.command('project')
|
|
717
|
+
.description('Operate on projects themselves (rename, bulk rename)');
|
|
718
|
+
projectCmd
|
|
719
|
+
.command('rename <newname> [project]')
|
|
720
|
+
.description('Rename a project')
|
|
721
|
+
.option('--dry-run', 'Show what would change without applying')
|
|
722
|
+
.option('--cookie <session>', 'Session cookie override')
|
|
723
|
+
.action(async (newname, project, options) => {
|
|
724
|
+
const spinner = ora('Renaming project...').start();
|
|
725
|
+
try {
|
|
726
|
+
const client = await getClient(options.cookie);
|
|
727
|
+
const proj = await resolveProject(client, project);
|
|
728
|
+
if (proj.name === newname) {
|
|
729
|
+
spinner.info(`Project is already named "${newname}"`);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
if (options.dryRun) {
|
|
733
|
+
spinner.stop();
|
|
734
|
+
console.log(chalk.bold('Would rename project:'));
|
|
735
|
+
console.log(` ${chalk.cyan(proj.name)} \u2192 ${chalk.cyan(newname)} ${chalk.dim(`(${proj.id})`)}`);
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
await client.renameProject(proj.id, newname);
|
|
739
|
+
spinner.succeed(`Renamed project: ${proj.name} \u2192 ${newname}`);
|
|
740
|
+
setLastProject(proj.id);
|
|
741
|
+
}
|
|
742
|
+
catch (error) {
|
|
743
|
+
spinner.fail(`Failed: ${error.message}`);
|
|
744
|
+
process.exit(1);
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
projectCmd
|
|
748
|
+
.command('rename-bulk')
|
|
749
|
+
.description('Rename many projects at once by pattern (dry-run unless --apply)')
|
|
750
|
+
.option('--match <regex>', 'Only consider projects whose name matches this regex')
|
|
751
|
+
.option('--search <text>', 'Literal substring to replace in the name')
|
|
752
|
+
.option('--replace <text>', 'Replacement for --search or --match (supports $1, $2 backrefs)')
|
|
753
|
+
.option('--prefix <text>', 'Prepend this to the name')
|
|
754
|
+
.option('--suffix <text>', 'Append this to the name')
|
|
755
|
+
.option('--apply', 'Actually rename. Without this flag nothing is changed.')
|
|
756
|
+
.option('--max <n>', 'Refuse to apply if more than n projects would change', parseInt)
|
|
757
|
+
.option('--cookie <session>', 'Session cookie override')
|
|
758
|
+
.action(async (options) => {
|
|
759
|
+
// Inverted default on purpose: for a single project a dry-run flag is a
|
|
760
|
+
// convenience, but a bulk rename that fires on a typo is unrecoverable
|
|
761
|
+
// (Overleaf keeps no project-name history). So doing nothing is the
|
|
762
|
+
// default and --apply is the deliberate act.
|
|
763
|
+
const spinner = ora('Fetching projects...').start();
|
|
764
|
+
try {
|
|
765
|
+
const client = await getClient(options.cookie);
|
|
766
|
+
const projects = await client.listProjects();
|
|
767
|
+
let plan;
|
|
768
|
+
try {
|
|
769
|
+
plan = planProjectRenames(projects, {
|
|
770
|
+
match: options.match,
|
|
771
|
+
search: options.search,
|
|
772
|
+
replace: options.replace,
|
|
773
|
+
prefix: options.prefix,
|
|
774
|
+
suffix: options.suffix,
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
catch (err) {
|
|
778
|
+
spinner.fail(err.message);
|
|
779
|
+
process.exit(1);
|
|
780
|
+
}
|
|
781
|
+
const { planned, skipped, collisions } = plan;
|
|
782
|
+
spinner.stop();
|
|
783
|
+
if (planned.length === 0) {
|
|
784
|
+
console.log(chalk.yellow('No project names would change.'));
|
|
785
|
+
for (const s of skipped) {
|
|
786
|
+
console.log(chalk.dim(` skipped ${s.name}: ${s.reason}`));
|
|
787
|
+
}
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
console.log(chalk.bold(`${planned.length} project(s) would be renamed:`));
|
|
791
|
+
for (const p of planned) {
|
|
792
|
+
console.log(` ${chalk.cyan(p.from)} ${chalk.dim('\u2192')} ${chalk.cyan(p.to)} ${chalk.dim(`(${p.id})`)}`);
|
|
793
|
+
}
|
|
794
|
+
for (const s of skipped) {
|
|
795
|
+
console.log(chalk.dim(` skipped ${s.name}: ${s.reason}`));
|
|
796
|
+
}
|
|
797
|
+
if (collisions.length > 0) {
|
|
798
|
+
console.log();
|
|
799
|
+
console.log(chalk.red(`Name collisions (${collisions.length}):`));
|
|
800
|
+
for (const c of collisions)
|
|
801
|
+
console.log(chalk.red(` ${c}`));
|
|
802
|
+
console.log(chalk.red('Refusing to apply. Overleaf allows duplicate names, so this would'));
|
|
803
|
+
console.log(chalk.red('succeed silently and leave projects you cannot tell apart.'));
|
|
804
|
+
process.exit(1);
|
|
805
|
+
}
|
|
806
|
+
if (options.max !== undefined && planned.length > options.max) {
|
|
807
|
+
console.log();
|
|
808
|
+
console.log(chalk.red(`Refusing to apply: ${planned.length} changes exceed --max ${options.max}.`));
|
|
809
|
+
process.exit(1);
|
|
810
|
+
}
|
|
811
|
+
if (!options.apply) {
|
|
812
|
+
console.log();
|
|
813
|
+
console.log(chalk.yellow('Dry run. Nothing was changed. Re-run with --apply to rename.'));
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
const applySpinner = ora(`Renaming ${planned.length} project(s)...`).start();
|
|
817
|
+
let renamed = 0;
|
|
818
|
+
const failures = [];
|
|
819
|
+
for (const p of planned) {
|
|
820
|
+
try {
|
|
821
|
+
await client.renameProject(p.id, p.to);
|
|
822
|
+
renamed++;
|
|
823
|
+
applySpinner.text = `Renaming... (${renamed}/${planned.length})`;
|
|
824
|
+
}
|
|
825
|
+
catch (error) {
|
|
826
|
+
// Keep going: a partial rename is recoverable by re-running, while
|
|
827
|
+
// aborting midway leaves the same partial state plus no report.
|
|
828
|
+
failures.push({ from: p.from, reason: error.message || String(error) });
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
if (failures.length > 0) {
|
|
832
|
+
applySpinner.warn(`Renamed ${renamed} project(s), ${failures.length} failed`);
|
|
833
|
+
for (const f of failures) {
|
|
834
|
+
console.log(chalk.yellow(` ${f.from}: ${f.reason}`));
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
else {
|
|
838
|
+
applySpinner.succeed(`Renamed ${renamed} project(s)`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
catch (error) {
|
|
842
|
+
spinner.fail(`Failed: ${error.message}`);
|
|
843
|
+
process.exit(1);
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
708
847
|
// COMPILE COMMAND
|
|
709
848
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
710
849
|
program
|
|
711
850
|
.command('compile [project]')
|
|
712
851
|
.description('Compile a project (trigger PDF generation)')
|
|
852
|
+
.option('-r, --resource <path>', 'Compile this .tex file as root document (e.g. paper.tex, folder/test.tex)')
|
|
713
853
|
.option('--cookie <session>', 'Session cookie override')
|
|
714
854
|
.action(async (project, options) => {
|
|
715
855
|
const spinner = ora('Compiling...').start();
|
|
716
856
|
try {
|
|
717
857
|
const client = await getClient(options.cookie);
|
|
718
858
|
const proj = await resolveProject(client, project);
|
|
719
|
-
const result = await client.compileProject(proj.id);
|
|
859
|
+
const result = await client.compileProject(proj.id, options.resource);
|
|
720
860
|
spinner.succeed(`Compiled "${proj.name}"`);
|
|
721
861
|
console.log(chalk.dim(`PDF URL: ${result.pdfUrl}`));
|
|
722
862
|
setLastProject(proj.id);
|
|
@@ -858,6 +998,7 @@ program
|
|
|
858
998
|
.description('Upload local changes to Overleaf project')
|
|
859
999
|
.option('--project <name>', 'Project name or ID (overrides .olcli.json)')
|
|
860
1000
|
.option('--all', 'Upload all files (not just changed)')
|
|
1001
|
+
.option('--delete', 'Propagate local deletions to the remote (opt-in; see docs)')
|
|
861
1002
|
.option('--dry-run', 'Show what would be uploaded without uploading')
|
|
862
1003
|
.option('--probe-folder', 'Probe for correct folder ID (use if uploads fail with folder_not_found)')
|
|
863
1004
|
.option('--no-default-ignore', 'Disable built-in LaTeX artifact ignore list (only .olignore applies)')
|
|
@@ -872,12 +1013,21 @@ program
|
|
|
872
1013
|
let projectName;
|
|
873
1014
|
let lastPull;
|
|
874
1015
|
let rootFolderId;
|
|
1016
|
+
let previousPushManifest = [];
|
|
875
1017
|
if (existsSync(metaPath)) {
|
|
876
1018
|
const meta = JSON.parse(readFileSync(metaPath, 'utf-8'));
|
|
877
1019
|
projectId = meta.projectId;
|
|
878
1020
|
projectName = meta.projectName;
|
|
879
1021
|
lastPull = meta.lastPull ? new Date(meta.lastPull) : undefined;
|
|
880
1022
|
rootFolderId = meta.rootFolderId;
|
|
1023
|
+
// Written by a previous push (pushManifest) or pull (remoteManifest).
|
|
1024
|
+
// Either is a valid baseline for "what did this directory last put there".
|
|
1025
|
+
if (Array.isArray(meta.pushManifest)) {
|
|
1026
|
+
previousPushManifest = meta.pushManifest;
|
|
1027
|
+
}
|
|
1028
|
+
else if (Array.isArray(meta.remoteManifest)) {
|
|
1029
|
+
previousPushManifest = meta.remoteManifest;
|
|
1030
|
+
}
|
|
881
1031
|
}
|
|
882
1032
|
if (options.project) {
|
|
883
1033
|
// Override with command line option
|
|
@@ -915,6 +1065,10 @@ program
|
|
|
915
1065
|
const { readdirSync, statSync } = await import('node:fs');
|
|
916
1066
|
const filesToUpload = [];
|
|
917
1067
|
const filesIgnored = [];
|
|
1068
|
+
// Every local file that survives ignore filtering, regardless of mtime.
|
|
1069
|
+
// filesToUpload is mtime-filtered and therefore useless as a deletion
|
|
1070
|
+
// baseline: an unchanged file would look "absent" and get deleted.
|
|
1071
|
+
const allLocalPaths = new Set();
|
|
918
1072
|
function scanDir(currentDir, relativeBase = '') {
|
|
919
1073
|
const entries = readdirSync(currentDir, { withFileTypes: true });
|
|
920
1074
|
// Pre-compute sibling .tex set for the PDF special rule.
|
|
@@ -938,6 +1092,7 @@ program
|
|
|
938
1092
|
filesIgnored.push(relativePath);
|
|
939
1093
|
continue;
|
|
940
1094
|
}
|
|
1095
|
+
allLocalPaths.add(relativePath);
|
|
941
1096
|
// Check if file is newer than last pull (unless --all)
|
|
942
1097
|
if (options.all || !lastPull) {
|
|
943
1098
|
filesToUpload.push({ path: fullPath, relativePath });
|
|
@@ -960,15 +1115,46 @@ program
|
|
|
960
1115
|
}
|
|
961
1116
|
spinner.start('Scanning files...');
|
|
962
1117
|
}
|
|
963
|
-
|
|
1118
|
+
// Deletion candidates: tracked by a previous push/pull from THIS directory,
|
|
1119
|
+
// gone locally now. Never derived from the remote listing - that would also
|
|
1120
|
+
// sweep away files someone else uploaded through the Overleaf editor.
|
|
1121
|
+
//
|
|
1122
|
+
// Skipped entirely without a baseline manifest: on a first push we cannot
|
|
1123
|
+
// tell "deleted locally" from "never existed here", and guessing wrong
|
|
1124
|
+
// destroys remote work.
|
|
1125
|
+
const filesToDelete = [];
|
|
1126
|
+
if (options.delete && previousPushManifest.length > 0) {
|
|
1127
|
+
for (const path of previousPushManifest) {
|
|
1128
|
+
if (path === 'output.pdf' || path.endsWith('/output.pdf'))
|
|
1129
|
+
continue;
|
|
1130
|
+
if (!allLocalPaths.has(path))
|
|
1131
|
+
filesToDelete.push(path);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
const noBaseline = options.delete === true && previousPushManifest.length === 0;
|
|
1135
|
+
if (filesToUpload.length === 0 && filesToDelete.length === 0) {
|
|
964
1136
|
spinner.info('No files to upload');
|
|
1137
|
+
if (noBaseline) {
|
|
1138
|
+
console.log(chalk.dim(' --delete skipped: no manifest yet (first push from this directory)'));
|
|
1139
|
+
}
|
|
965
1140
|
return;
|
|
966
1141
|
}
|
|
967
1142
|
if (options.dryRun) {
|
|
968
1143
|
spinner.stop();
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1144
|
+
if (filesToUpload.length > 0) {
|
|
1145
|
+
console.log(chalk.bold(`Would upload ${filesToUpload.length} file(s) to "${projectName}":`));
|
|
1146
|
+
for (const f of filesToUpload) {
|
|
1147
|
+
console.log(` ${chalk.cyan(f.relativePath)}`);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
if (filesToDelete.length > 0) {
|
|
1151
|
+
console.log(chalk.bold(`Would delete ${filesToDelete.length} file(s) on "${projectName}":`));
|
|
1152
|
+
for (const p of filesToDelete) {
|
|
1153
|
+
console.log(` ${chalk.red(p)}`);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
if (noBaseline) {
|
|
1157
|
+
console.log(chalk.dim(' --delete skipped: no manifest yet (first push from this directory)'));
|
|
972
1158
|
}
|
|
973
1159
|
return;
|
|
974
1160
|
}
|
|
@@ -1019,10 +1205,29 @@ program
|
|
|
1019
1205
|
}
|
|
1020
1206
|
}
|
|
1021
1207
|
}
|
|
1022
|
-
//
|
|
1208
|
+
// Deletions run after uploads: a rename arrives as add+remove, and doing
|
|
1209
|
+
// it in this order never leaves the remote without the file.
|
|
1210
|
+
let deleted = 0;
|
|
1211
|
+
const deleteSkipped = [];
|
|
1212
|
+
if (filesToDelete.length > 0) {
|
|
1213
|
+
spinner.text = `Deleting ${filesToDelete.length} remote file(s)...`;
|
|
1214
|
+
for (const path of filesToDelete) {
|
|
1215
|
+
try {
|
|
1216
|
+
await client.deleteByPath(projectId, path);
|
|
1217
|
+
deleted++;
|
|
1218
|
+
}
|
|
1219
|
+
catch (error) {
|
|
1220
|
+
// Already gone remotely is the common case and not an error worth
|
|
1221
|
+
// failing the push over.
|
|
1222
|
+
deleteSkipped.push({ path, reason: error.message || String(error) });
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
// Update last push time and the manifest that the next --delete reads.
|
|
1023
1227
|
if (existsSync(metaPath)) {
|
|
1024
1228
|
const meta = JSON.parse(readFileSync(metaPath, 'utf-8'));
|
|
1025
1229
|
meta.lastPush = new Date().toISOString();
|
|
1230
|
+
meta.pushManifest = Array.from(allLocalPaths).sort();
|
|
1026
1231
|
writeFileSync(metaPath, JSON.stringify(meta, null, 2));
|
|
1027
1232
|
}
|
|
1028
1233
|
if (failed > 0) {
|
|
@@ -1034,6 +1239,18 @@ program
|
|
|
1034
1239
|
else {
|
|
1035
1240
|
spinner.succeed(`Uploaded ${uploaded} file(s) to "${projectName}"`);
|
|
1036
1241
|
}
|
|
1242
|
+
if (deleted > 0) {
|
|
1243
|
+
console.log(chalk.dim(` ✖ ${deleted} deleted on remote`));
|
|
1244
|
+
}
|
|
1245
|
+
if (deleteSkipped.length > 0) {
|
|
1246
|
+
console.log(chalk.yellow(` ${deleteSkipped.length} deletion(s) skipped:`));
|
|
1247
|
+
for (const s of deleteSkipped) {
|
|
1248
|
+
console.log(chalk.dim(` ${s.path}: ${s.reason}`));
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
if (noBaseline) {
|
|
1252
|
+
console.log(chalk.dim(' --delete skipped: no manifest yet; next push has a baseline'));
|
|
1253
|
+
}
|
|
1037
1254
|
setLastProject(projectId);
|
|
1038
1255
|
}
|
|
1039
1256
|
catch (error) {
|