summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/cli/main.ts35
-rw-r--r--src/cli/overleafApi.ts11
-rw-r--r--src/cli/test.ts25
3 files changed, 71 insertions, 0 deletions
diff --git a/src/cli/main.ts b/src/cli/main.ts
index 1fa9fce..52573fe 100644
--- a/src/cli/main.ts
+++ b/src/cli/main.ts
@@ -186,6 +186,37 @@ async function cmdProjects(flags: Record<string, string | boolean>): Promise<voi
}
}
+async function cmdCopy(positional: string[], flags: Record<string, string | boolean>): Promise<void> {
+ const sourceRef = positional[0]
+ const newName = positional[1]
+ if (!sourceRef || !newName) {
+ exitWith(EXIT_USAGE, 'Usage: lattex-cli copy <source-id|name> <new-name>')
+ }
+
+ const cookie = await resolveAuth(flags)
+ const api = new OverleafApi(cookie)
+
+ // Resolve project ID from name if needed
+ let sourceId = sourceRef
+ if (!sourceRef.match(/^[0-9a-f]{24}$/)) {
+ const projects = await api.listProjects()
+ const match = projects.find(p => p.name === sourceRef)
+ if (!match) {
+ exitWith(EXIT_ERROR, `Project not found: ${sourceRef}`)
+ }
+ sourceId = match!.id
+ }
+
+ err(`Copying project ${sourceId} as "${newName}"...`)
+ const newId = await api.copyProject(sourceId, newName)
+
+ if (jsonMode) {
+ jsonOut({ ok: true, sourceId, newId, newName })
+ } else {
+ out(`Copied → ${newId} "${newName}"`)
+ }
+}
+
async function cmdClone(positional: string[], flags: Record<string, string | boolean>): Promise<void> {
const projectRef = positional[0]
const targetDir = positional[1]
@@ -693,6 +724,7 @@ Commands:
projects [--json] List Overleaf projects
+ copy <id|name> <name> Copy (clone) a project on Overleaf
clone <id|name> <dir> Download project to local directory
pull <dir> Update local dir from Overleaf
status <dir> Show local changes
@@ -726,6 +758,9 @@ async function main(): Promise<void> {
case 'projects':
await cmdProjects(parsed.flags)
break
+ case 'copy':
+ await cmdCopy(parsed.positional, parsed.flags)
+ break
case 'clone':
await cmdClone(parsed.positional, parsed.flags)
break
diff --git a/src/cli/overleafApi.ts b/src/cli/overleafApi.ts
index 6301638..94bdac2 100644
--- a/src/cli/overleafApi.ts
+++ b/src/cli/overleafApi.ts
@@ -348,6 +348,17 @@ export class OverleafApi {
}
}
+ /** Clone (copy) a project on Overleaf, returning the new project ID */
+ async copyProject(sourceProjectId: string, newName: string): Promise<string> {
+ const result = await this.requestWithCsrf('POST', `/project/${sourceProjectId}/clone`, {
+ projectName: newName
+ })
+ if (!result.ok || !(result.data as any)?.project_id) {
+ throw new Error(`Copy project failed: HTTP ${result.status}`)
+ }
+ return (result.data as any).project_id
+ }
+
/** Flush project (ensure OT changes are saved to database) */
async flushProject(projectId: string): Promise<void> {
await this.requestWithCsrf('POST', `/project/${projectId}/flush`)
diff --git a/src/cli/test.ts b/src/cli/test.ts
index e76182e..f81a89b 100644
--- a/src/cli/test.ts
+++ b/src/cli/test.ts
@@ -1,5 +1,6 @@
// Tests for CLI arg parsing and diff logic
import { parseArgs } from './args'
+import { OverleafApi } from './overleafApi'
import { walkRootFolder } from './fileTree'
import { join } from 'path'
import { mkdtemp, writeFile, mkdir, rm } from 'fs/promises'
@@ -229,6 +230,28 @@ l.3 \\usepackage{nonexistent}`
eq(entries4[0].line, 20, 'overfull line')
}
+// ── copy command arg parsing test ──
+
+function testCopyArgs(): void {
+ console.log('--- copy args ---')
+
+ const r1 = parseArgs(['node', 'cli', 'copy', '6a965e135690947a3be20882', 'My New Project'])
+ eq(r1.command, 'copy', 'copy command')
+ eq(r1.positional, ['6a965e135690947a3be20882', 'My New Project'], 'copy positionals')
+
+ const r2 = parseArgs(['node', 'cli', 'copy', 'My Source', 'My Copy', '--json'])
+ eq(r2.command, 'copy', 'copy command with json')
+ eq(r2.positional, ['My Source', 'My Copy'], 'copy positionals by name')
+ eq(r2.flags.json, true, 'json flag with copy')
+}
+
+// ── OverleafApi.copyProject existence test ──
+
+function testCopyProjectMethod(): void {
+ console.log('--- copyProject method ---')
+ assert(typeof OverleafApi.prototype.copyProject === 'function', 'copyProject is a method on OverleafApi')
+}
+
// ── Run all tests ──
async function runTests(): Promise<void> {
@@ -237,6 +260,8 @@ async function runTests(): Promise<void> {
testIsTextFile()
await testComputeDiff()
testParseCompileLog()
+ testCopyArgs()
+ testCopyProjectMethod()
console.log(`\n${passed} passed, ${failed} failed`)
process.exit(failed > 0 ? 1 : 0)