@cleocode/core 2026.3.44 → 2026.3.45

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.
@@ -3,13 +3,14 @@
3
3
  *
4
4
  * Extracted from CLI import-tasks command for dispatch layer access.
5
5
  *
6
- * @task T5323, T5328
6
+ * @task T5323, T5328, T046
7
7
  * @epic T4545
8
8
  */
9
9
 
10
10
  import { constants as fsConstants } from 'node:fs';
11
11
  import { access, readFile } from 'node:fs/promises';
12
12
  import type { Task, TaskStatus } from '@cleocode/contracts';
13
+ import type { ImportFromPackageOptions, ImportFromPackageResult } from '../nexus/transfer-types.js';
13
14
  import { getAccessor } from '../store/data-accessor.js';
14
15
  import type { ExportPackage } from '../store/export.js';
15
16
  import {
@@ -74,25 +75,13 @@ export interface ImportTasksResult {
74
75
  }
75
76
 
76
77
  /**
77
- * Import tasks from a cross-project export package with ID remapping.
78
+ * Import tasks from an in-memory ExportPackage with ID remapping.
79
+ * Core logic extracted from importTasksPackage for reuse by transfer engine.
78
80
  */
79
- export async function importTasksPackage(params: ImportTasksParams): Promise<ImportTasksResult> {
80
- const { file } = params;
81
-
82
- try {
83
- await access(file, fsConstants.R_OK);
84
- } catch {
85
- throw new Error(`Export file not found: ${file}`);
86
- }
87
-
88
- const content = await readFile(file, 'utf-8');
89
- let exportPkg: ExportPackage;
90
- try {
91
- exportPkg = JSON.parse(content) as ExportPackage;
92
- } catch {
93
- throw new Error(`Invalid JSON in: ${file}`);
94
- }
95
-
81
+ export async function importFromPackage(
82
+ exportPkg: ExportPackage,
83
+ options: ImportFromPackageOptions = {},
84
+ ): Promise<ImportFromPackageResult> {
96
85
  if (exportPkg._meta?.format !== 'cleo-export') {
97
86
  throw new Error(
98
87
  `Invalid export format (expected 'cleo-export', got '${exportPkg._meta?.format}')`,
@@ -103,17 +92,17 @@ export async function importTasksPackage(params: ImportTasksParams): Promise<Imp
103
92
  throw new Error('Export package contains no tasks');
104
93
  }
105
94
 
106
- const accessor = await getAccessor(params.cwd);
95
+ const accessor = await getAccessor(options.cwd);
107
96
  const { tasks: existingTasks } = await accessor.queryTasks({});
108
97
 
109
- const onConflict: OnConflict = params.onConflict ?? 'fail';
110
- const onMissingDep: OnMissingDep = params.onMissingDep ?? 'strip';
111
- const force = params.force ?? false;
112
- const parentId = params.parent;
113
- const phaseOverride = params.phase;
114
- const addLabel = params.addLabel;
115
- const resetStatus = params.resetStatus;
116
- const addProvenance = params.provenance !== false;
98
+ const onConflict: OnConflict = options.onConflict ?? 'fail';
99
+ const onMissingDep: OnMissingDep = options.onMissingDep === 'fail' ? 'fail' : 'strip';
100
+ const force = options.force ?? false;
101
+ const parentId = options.parent;
102
+ const phaseOverride = options.phase;
103
+ const addLabel = options.addLabel;
104
+ const resetStatus = options.resetStatus;
105
+ const addProvenance = options.provenance !== false;
117
106
 
118
107
  if (parentId) {
119
108
  const parentExists = existingTasks.some((t) => t.id === parentId);
@@ -195,7 +184,7 @@ export async function importTasksPackage(params: ImportTasksParams): Promise<Imp
195
184
  existingIds.add(remapped.id);
196
185
  }
197
186
 
198
- if (params.dryRun) {
187
+ if (options.dryRun) {
199
188
  return {
200
189
  imported: transformed.length,
201
190
  skipped: skipped.length,
@@ -217,3 +206,38 @@ export async function importTasksPackage(params: ImportTasksParams): Promise<Imp
217
206
  idRemap: idRemapJson,
218
207
  };
219
208
  }
209
+
210
+ /**
211
+ * Import tasks from a cross-project export package file with ID remapping.
212
+ * Thin wrapper around importFromPackage that handles file I/O.
213
+ */
214
+ export async function importTasksPackage(params: ImportTasksParams): Promise<ImportTasksResult> {
215
+ const { file } = params;
216
+
217
+ try {
218
+ await access(file, fsConstants.R_OK);
219
+ } catch {
220
+ throw new Error(`Export file not found: ${file}`);
221
+ }
222
+
223
+ const content = await readFile(file, 'utf-8');
224
+ let exportPkg: ExportPackage;
225
+ try {
226
+ exportPkg = JSON.parse(content) as ExportPackage;
227
+ } catch {
228
+ throw new Error(`Invalid JSON in: ${file}`);
229
+ }
230
+
231
+ return importFromPackage(exportPkg, {
232
+ cwd: params.cwd,
233
+ dryRun: params.dryRun,
234
+ parent: params.parent,
235
+ phase: params.phase,
236
+ addLabel: params.addLabel,
237
+ provenance: params.provenance,
238
+ resetStatus: params.resetStatus,
239
+ onConflict: params.onConflict,
240
+ onMissingDep: params.onMissingDep === 'placeholder' ? 'strip' : params.onMissingDep,
241
+ force: params.force,
242
+ });
243
+ }
package/src/internal.ts CHANGED
@@ -27,7 +27,7 @@ export { exportTasksPackage } from './admin/export-tasks.js';
27
27
  // Admin
28
28
  export { computeHelp } from './admin/help.js';
29
29
  export { importTasks } from './admin/import.js';
30
- export { importTasksPackage } from './admin/import-tasks.js';
30
+ export { importFromPackage, importTasksPackage } from './admin/import-tasks.js';
31
31
  // ADRs
32
32
  export { findAdrs } from './adrs/find.js';
33
33
  export { listAdrs, showAdr, syncAdrsToDb, validateAllAdrs } from './adrs/index.js';
@@ -668,6 +668,12 @@ export {
668
668
  } from './nexus/discover.js';
669
669
  // Nexus — readRegistry (exported as nexusReadRegistry to avoid name clash with skills readRegistry)
670
670
  export { readRegistry as nexusReadRegistry } from './nexus/registry.js';
671
+ // Nexus — transfer
672
+ export { executeTransfer, previewTransfer } from './nexus/transfer.js';
673
+ export type {
674
+ TransferParams,
675
+ TransferResult,
676
+ } from './nexus/transfer-types.js';
671
677
  export type { DependencyAnalysis } from './orchestration/analyze.js';
672
678
  export { analyzeDependencies as orchestrationAnalyzeDependencies } from './orchestration/analyze.js';
673
679
  export { getCriticalPath as orchestrationGetCriticalPath } from './orchestration/critical-path.js';
@@ -0,0 +1,446 @@
1
+ /**
2
+ * Tests for NEXUS cross-project task transfer.
3
+ *
4
+ * @task T046, T055
5
+ * @epic T4540
6
+ */
7
+
8
+ import { mkdir, mkdtemp, rm } from 'node:fs/promises';
9
+ import { tmpdir } from 'node:os';
10
+ import { join } from 'node:path';
11
+ import type { Task } from '@cleocode/contracts';
12
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
13
+ import { getLinksByTaskId } from '../../reconciliation/link-store.js';
14
+ import { seedTasks } from '../../store/__tests__/test-db-helper.js';
15
+ import { resetDbState } from '../../store/sqlite.js';
16
+ import { createSqliteDataAccessor } from '../../store/sqlite-data-accessor.js';
17
+ import { nexusInit, nexusRegister, resetNexusDbState } from '../registry.js';
18
+ import { executeTransfer, previewTransfer } from '../transfer.js';
19
+
20
+ /** Create a test project with tasks in SQLite (tasks.db). */
21
+ async function createTestProjectDb(
22
+ dir: string,
23
+ tasks: Array<Partial<Task> & { id: string }>,
24
+ ): Promise<void> {
25
+ await mkdir(join(dir, '.cleo'), { recursive: true });
26
+ resetDbState();
27
+ const accessor = await createSqliteDataAccessor(dir);
28
+ await seedTasks(accessor, tasks);
29
+ await accessor.close();
30
+ resetDbState();
31
+ }
32
+
33
+ let testDir: string;
34
+ let registryDir: string;
35
+ let sourceDir: string;
36
+ let targetDir: string;
37
+
38
+ beforeEach(async () => {
39
+ testDir = await mkdtemp(join(tmpdir(), 'nexus-transfer-test-'));
40
+ registryDir = join(testDir, 'cleo-home');
41
+ sourceDir = join(testDir, 'source-project');
42
+ targetDir = join(testDir, 'target-project');
43
+
44
+ await mkdir(registryDir, { recursive: true });
45
+
46
+ // Create source project with a task hierarchy
47
+ await createTestProjectDb(sourceDir, [
48
+ { id: 'T001', title: 'Epic: Auth', type: 'epic', status: 'in-progress' },
49
+ {
50
+ id: 'T002',
51
+ title: 'Login form',
52
+ parentId: 'T001',
53
+ status: 'pending',
54
+ description: 'Build login',
55
+ },
56
+ {
57
+ id: 'T003',
58
+ title: 'JWT tokens',
59
+ parentId: 'T001',
60
+ depends: ['T002'],
61
+ status: 'pending',
62
+ },
63
+ { id: 'T004', title: 'Unrelated task', status: 'done' },
64
+ ]);
65
+
66
+ // Create empty target project
67
+ await createTestProjectDb(targetDir, []);
68
+
69
+ // Point env vars to test dirs
70
+ process.env['CLEO_HOME'] = registryDir;
71
+ process.env['NEXUS_HOME'] = join(registryDir, 'nexus');
72
+ process.env['NEXUS_CACHE_DIR'] = join(registryDir, 'nexus', 'cache');
73
+ process.env['NEXUS_SKIP_PERMISSION_CHECK'] = 'true';
74
+
75
+ resetNexusDbState();
76
+
77
+ // Register both projects
78
+ await nexusInit();
79
+ await nexusRegister(sourceDir, 'source-project', 'read');
80
+ resetDbState();
81
+ await nexusRegister(targetDir, 'target-project', 'write');
82
+ resetDbState();
83
+ });
84
+
85
+ afterEach(async () => {
86
+ delete process.env['CLEO_HOME'];
87
+ delete process.env['NEXUS_HOME'];
88
+ delete process.env['NEXUS_CACHE_DIR'];
89
+ delete process.env['NEXUS_SKIP_PERMISSION_CHECK'];
90
+ resetNexusDbState();
91
+ resetDbState();
92
+ await rm(testDir, { recursive: true, force: true });
93
+ });
94
+
95
+ describe('previewTransfer', () => {
96
+ it('returns a dry-run result without writing', async () => {
97
+ const result = await previewTransfer({
98
+ taskIds: ['T001'],
99
+ sourceProject: 'source-project',
100
+ targetProject: 'target-project',
101
+ scope: 'subtree',
102
+ });
103
+
104
+ expect(result.dryRun).toBe(true);
105
+ expect(result.transferred).toBe(3); // T001 + T002 + T003
106
+ expect(result.manifest.sourceProject).toBe('source-project');
107
+ expect(result.manifest.targetProject).toBe('target-project');
108
+ expect(result.manifest.entries).toHaveLength(3);
109
+
110
+ // Verify nothing was written to target
111
+ resetDbState();
112
+ const accessor = await createSqliteDataAccessor(targetDir);
113
+ const { tasks } = await accessor.queryTasks({});
114
+ expect(tasks).toHaveLength(0);
115
+ await accessor.close();
116
+ });
117
+
118
+ it('returns single-task preview when scope is single', async () => {
119
+ const result = await previewTransfer({
120
+ taskIds: ['T001'],
121
+ sourceProject: 'source-project',
122
+ targetProject: 'target-project',
123
+ scope: 'single',
124
+ });
125
+
126
+ expect(result.dryRun).toBe(true);
127
+ expect(result.transferred).toBe(1);
128
+ expect(result.manifest.entries).toHaveLength(1);
129
+ expect(result.manifest.entries[0]!.sourceId).toBe('T001');
130
+ });
131
+ });
132
+
133
+ describe('executeTransfer - copy mode', () => {
134
+ it('copies a subtree to the target project', async () => {
135
+ const result = await executeTransfer({
136
+ taskIds: ['T001'],
137
+ sourceProject: 'source-project',
138
+ targetProject: 'target-project',
139
+ mode: 'copy',
140
+ scope: 'subtree',
141
+ });
142
+
143
+ expect(result.dryRun).toBe(false);
144
+ expect(result.transferred).toBe(3);
145
+ expect(result.skipped).toBe(0);
146
+ expect(result.archived).toBe(0);
147
+ expect(result.manifest.mode).toBe('copy');
148
+
149
+ // Verify tasks exist in target
150
+ resetDbState();
151
+ const targetAccessor = await createSqliteDataAccessor(targetDir);
152
+ const { tasks: targetTasks } = await targetAccessor.queryTasks({});
153
+ expect(targetTasks).toHaveLength(3);
154
+ await targetAccessor.close();
155
+
156
+ // Verify source tasks still exist (not archived)
157
+ resetDbState();
158
+ const sourceAccessor = await createSqliteDataAccessor(sourceDir);
159
+ const { tasks: sourceTasks } = await sourceAccessor.queryTasks({});
160
+ expect(sourceTasks).toHaveLength(4); // all 4 original tasks
161
+ await sourceAccessor.close();
162
+ });
163
+
164
+ it('remaps task IDs in the target', async () => {
165
+ // Seed target with a task so IDs don't accidentally collide
166
+ resetDbState();
167
+ const accessor = await createSqliteDataAccessor(targetDir);
168
+ await seedTasks(accessor, [{ id: 'T010', title: 'Existing target task', status: 'pending' }]);
169
+ await accessor.close();
170
+ resetDbState();
171
+
172
+ const result = await executeTransfer({
173
+ taskIds: ['T001'],
174
+ sourceProject: 'source-project',
175
+ targetProject: 'target-project',
176
+ scope: 'subtree',
177
+ onConflict: 'rename',
178
+ });
179
+
180
+ expect(Object.keys(result.manifest.idRemap).length).toBeGreaterThan(0);
181
+
182
+ // All target IDs should be different from source IDs since target has T010
183
+ for (const entry of result.manifest.entries) {
184
+ expect(entry.targetId).not.toBe(entry.sourceId);
185
+ }
186
+ });
187
+
188
+ it('preserves parent-child hierarchy in target', async () => {
189
+ const result = await executeTransfer({
190
+ taskIds: ['T001'],
191
+ sourceProject: 'source-project',
192
+ targetProject: 'target-project',
193
+ scope: 'subtree',
194
+ });
195
+
196
+ resetDbState();
197
+ const accessor = await createSqliteDataAccessor(targetDir);
198
+ const { tasks } = await accessor.queryTasks({});
199
+
200
+ const epicId = result.manifest.idRemap['T001']!;
201
+ const loginId = result.manifest.idRemap['T002']!;
202
+ const jwtId = result.manifest.idRemap['T003']!;
203
+
204
+ const loginTask = tasks.find((t) => t.id === loginId);
205
+ const jwtTask = tasks.find((t) => t.id === jwtId);
206
+
207
+ expect(loginTask?.parentId).toBe(epicId);
208
+ expect(jwtTask?.parentId).toBe(epicId);
209
+ await accessor.close();
210
+ });
211
+
212
+ it('preserves dependencies in target', async () => {
213
+ const result = await executeTransfer({
214
+ taskIds: ['T001'],
215
+ sourceProject: 'source-project',
216
+ targetProject: 'target-project',
217
+ scope: 'subtree',
218
+ });
219
+
220
+ resetDbState();
221
+ const accessor = await createSqliteDataAccessor(targetDir);
222
+ const { tasks } = await accessor.queryTasks({});
223
+
224
+ const loginId = result.manifest.idRemap['T002']!;
225
+ const jwtId = result.manifest.idRemap['T003']!;
226
+ const jwtTask = tasks.find((t) => t.id === jwtId);
227
+
228
+ expect(jwtTask?.depends).toContain(loginId);
229
+ await accessor.close();
230
+ });
231
+
232
+ it('adds provenance notes by default', async () => {
233
+ const result = await executeTransfer({
234
+ taskIds: ['T001'],
235
+ sourceProject: 'source-project',
236
+ targetProject: 'target-project',
237
+ scope: 'single',
238
+ });
239
+
240
+ resetDbState();
241
+ const accessor = await createSqliteDataAccessor(targetDir);
242
+ const { tasks } = await accessor.queryTasks({});
243
+
244
+ const epicId = result.manifest.idRemap['T001']!;
245
+ const task = tasks.find((t) => t.id === epicId);
246
+ expect(task?.notes?.some((n) => n.includes('Imported from source-project'))).toBe(true);
247
+ await accessor.close();
248
+ });
249
+
250
+ it('creates bidirectional external links', async () => {
251
+ const result = await executeTransfer({
252
+ taskIds: ['T004'],
253
+ sourceProject: 'source-project',
254
+ targetProject: 'target-project',
255
+ scope: 'single',
256
+ });
257
+
258
+ expect(result.linksCreated).toBe(2); // one in each project
259
+
260
+ // Check target link
261
+ resetDbState();
262
+ const targetId = result.manifest.idRemap['T004']!;
263
+ const targetLinks = await getLinksByTaskId(targetId, targetDir);
264
+ expect(targetLinks).toHaveLength(1);
265
+ expect(targetLinks[0]!.providerId).toBe('nexus:source-project');
266
+ expect(targetLinks[0]!.externalId).toBe('T004');
267
+ expect(targetLinks[0]!.linkType).toBe('transferred');
268
+ expect(targetLinks[0]!.syncDirection).toBe('inbound');
269
+
270
+ // Check source link
271
+ resetDbState();
272
+ const sourceLinks = await getLinksByTaskId('T004', sourceDir);
273
+ expect(sourceLinks).toHaveLength(1);
274
+ expect(sourceLinks[0]!.providerId).toBe('nexus:target-project');
275
+ expect(sourceLinks[0]!.externalId).toBe(targetId);
276
+ expect(sourceLinks[0]!.linkType).toBe('transferred');
277
+ expect(sourceLinks[0]!.syncDirection).toBe('outbound');
278
+ });
279
+
280
+ it('transfers a single task without descendants', async () => {
281
+ const result = await executeTransfer({
282
+ taskIds: ['T004'],
283
+ sourceProject: 'source-project',
284
+ targetProject: 'target-project',
285
+ scope: 'single',
286
+ });
287
+
288
+ expect(result.transferred).toBe(1);
289
+ expect(result.manifest.entries).toHaveLength(1);
290
+ expect(result.manifest.entries[0]!.sourceId).toBe('T004');
291
+ });
292
+ });
293
+
294
+ describe('executeTransfer - move mode', () => {
295
+ it('archives source tasks after transfer', async () => {
296
+ const result = await executeTransfer({
297
+ taskIds: ['T004'],
298
+ sourceProject: 'source-project',
299
+ targetProject: 'target-project',
300
+ mode: 'move',
301
+ scope: 'single',
302
+ });
303
+
304
+ expect(result.transferred).toBe(1);
305
+ expect(result.archived).toBe(1);
306
+ expect(result.manifest.mode).toBe('move');
307
+
308
+ // Source task should be archived (queryTasks excludes archived by default)
309
+ resetDbState();
310
+ const sourceAccessor = await createSqliteDataAccessor(sourceDir);
311
+ const { tasks: sourceTasks } = await sourceAccessor.queryTasks({
312
+ status: 'archived',
313
+ });
314
+ const archivedTask = sourceTasks.find((t) => t.id === 'T004');
315
+ expect(archivedTask?.status).toBe('archived');
316
+ await sourceAccessor.close();
317
+ });
318
+ });
319
+
320
+ describe('executeTransfer - error handling', () => {
321
+ it('throws when source project not found', async () => {
322
+ await expect(
323
+ executeTransfer({
324
+ taskIds: ['T001'],
325
+ sourceProject: 'nonexistent',
326
+ targetProject: 'target-project',
327
+ }),
328
+ ).rejects.toThrow('Source project not found');
329
+ });
330
+
331
+ it('throws when target project not found', async () => {
332
+ await expect(
333
+ executeTransfer({
334
+ taskIds: ['T001'],
335
+ sourceProject: 'source-project',
336
+ targetProject: 'nonexistent',
337
+ }),
338
+ ).rejects.toThrow('Target project not found');
339
+ });
340
+
341
+ it('throws when source and target are the same', async () => {
342
+ await expect(
343
+ executeTransfer({
344
+ taskIds: ['T001'],
345
+ sourceProject: 'source-project',
346
+ targetProject: 'source-project',
347
+ }),
348
+ ).rejects.toThrow('Source and target projects must be different');
349
+ });
350
+
351
+ it('throws when task not found in source', async () => {
352
+ await expect(
353
+ executeTransfer({
354
+ taskIds: ['T999'],
355
+ sourceProject: 'source-project',
356
+ targetProject: 'target-project',
357
+ }),
358
+ ).rejects.toThrow('Task not found in source project: T999');
359
+ });
360
+
361
+ it('throws when no task IDs specified', async () => {
362
+ await expect(
363
+ executeTransfer({
364
+ taskIds: [],
365
+ sourceProject: 'source-project',
366
+ targetProject: 'target-project',
367
+ }),
368
+ ).rejects.toThrow('No task IDs specified');
369
+ });
370
+ });
371
+
372
+ describe('executeTransfer - conflict resolution', () => {
373
+ it('renames tasks with duplicate titles by default', async () => {
374
+ // Create a task in target with the same title as source
375
+ resetDbState();
376
+ const accessor = await createSqliteDataAccessor(targetDir);
377
+ await seedTasks(accessor, [{ id: 'T001', title: 'Unrelated task', status: 'pending' }]);
378
+ await accessor.close();
379
+ resetDbState();
380
+
381
+ const result = await executeTransfer({
382
+ taskIds: ['T004'],
383
+ sourceProject: 'source-project',
384
+ targetProject: 'target-project',
385
+ scope: 'single',
386
+ onConflict: 'rename',
387
+ });
388
+
389
+ expect(result.transferred).toBe(1);
390
+
391
+ resetDbState();
392
+ const targetAccessor = await createSqliteDataAccessor(targetDir);
393
+ const { tasks: targetTasks } = await targetAccessor.queryTasks({});
394
+ const transferredTask = targetTasks.find((t) => t.id === result.manifest.idRemap['T004']);
395
+ expect(transferredTask?.title).toContain('imported');
396
+ await targetAccessor.close();
397
+ });
398
+
399
+ it('skips tasks with duplicate titles when onConflict=skip', async () => {
400
+ resetDbState();
401
+ const accessor = await createSqliteDataAccessor(targetDir);
402
+ await seedTasks(accessor, [{ id: 'T001', title: 'Unrelated task', status: 'pending' }]);
403
+ await accessor.close();
404
+ resetDbState();
405
+
406
+ const result = await executeTransfer({
407
+ taskIds: ['T004'],
408
+ sourceProject: 'source-project',
409
+ targetProject: 'target-project',
410
+ scope: 'single',
411
+ onConflict: 'skip',
412
+ });
413
+
414
+ expect(result.skipped).toBe(1);
415
+ expect(result.transferred).toBe(0);
416
+ });
417
+ });
418
+
419
+ describe('executeTransfer - multiple tasks', () => {
420
+ it('transfers multiple independent tasks', async () => {
421
+ const result = await executeTransfer({
422
+ taskIds: ['T001', 'T004'],
423
+ sourceProject: 'source-project',
424
+ targetProject: 'target-project',
425
+ scope: 'single',
426
+ });
427
+
428
+ expect(result.transferred).toBe(2);
429
+ expect(result.manifest.entries).toHaveLength(2);
430
+ });
431
+
432
+ it('deduplicates tasks when subtree overlaps', async () => {
433
+ // T001 subtree includes T002 and T003
434
+ // Requesting T001 and T002 should not duplicate T002
435
+ const result = await executeTransfer({
436
+ taskIds: ['T001', 'T002'],
437
+ sourceProject: 'source-project',
438
+ targetProject: 'target-project',
439
+ scope: 'subtree',
440
+ });
441
+
442
+ // T001 subtree is T001, T002, T003. T002 subtree is just T002.
443
+ // Merged = T001, T002, T003 (deduped)
444
+ expect(result.transferred).toBe(3);
445
+ });
446
+ });
@@ -98,3 +98,17 @@ export {
98
98
  type SharingStatus,
99
99
  syncGitignore,
100
100
  } from './sharing/index.js';
101
+ // Transfer - cross-project task transfer
102
+ export { executeTransfer, previewTransfer } from './transfer.js';
103
+ export type {
104
+ ImportFromPackageOptions,
105
+ ImportFromPackageResult,
106
+ TransferManifest,
107
+ TransferManifestEntry,
108
+ TransferMode,
109
+ TransferOnConflict,
110
+ TransferOnMissingDep,
111
+ TransferParams,
112
+ TransferResult,
113
+ TransferScope,
114
+ } from './transfer-types.js';