@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.
- package/dist/admin/import-tasks.d.ts +10 -2
- package/dist/admin/import-tasks.d.ts.map +1 -1
- package/dist/index.js +415 -132
- package/dist/index.js.map +4 -4
- package/dist/internal.d.ts +3 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/nexus/index.d.ts +2 -0
- package/dist/nexus/index.d.ts.map +1 -1
- package/dist/nexus/transfer-types.d.ts +123 -0
- package/dist/nexus/transfer-types.d.ts.map +1 -0
- package/dist/nexus/transfer.d.ts +31 -0
- package/dist/nexus/transfer.d.ts.map +1 -0
- package/dist/store/brain-sqlite.d.ts +4 -1
- package/dist/store/brain-sqlite.d.ts.map +1 -1
- package/dist/store/nexus-sqlite.d.ts +4 -1
- package/dist/store/nexus-sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.d.ts +4 -1
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/tasks-schema.d.ts +3 -3
- package/dist/store/tasks-schema.d.ts.map +1 -1
- package/dist/store/validation-schemas.d.ts +5 -4
- package/dist/store/validation-schemas.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/admin/import-tasks.ts +53 -29
- package/src/internal.ts +7 -1
- package/src/nexus/__tests__/transfer.test.ts +446 -0
- package/src/nexus/index.ts +14 -0
- package/src/nexus/transfer-types.ts +129 -0
- package/src/nexus/transfer.ts +314 -0
- package/src/store/brain-sqlite.ts +7 -3
- package/src/store/nexus-sqlite.ts +7 -3
- package/src/store/sqlite.ts +9 -3
- package/src/store/tasks-schema.ts +1 -1
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for cross-project task transfer via NEXUS.
|
|
3
|
+
*
|
|
4
|
+
* @task T046, T047
|
|
5
|
+
* @epic T4540
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Task } from '@cleocode/contracts';
|
|
9
|
+
|
|
10
|
+
/** Transfer mode: copy keeps source tasks, move archives them. */
|
|
11
|
+
export type TransferMode = 'copy' | 'move';
|
|
12
|
+
|
|
13
|
+
/** Transfer scope: single task or full subtree. */
|
|
14
|
+
export type TransferScope = 'single' | 'subtree';
|
|
15
|
+
|
|
16
|
+
/** Conflict resolution when target has tasks with duplicate titles. */
|
|
17
|
+
export type TransferOnConflict = 'duplicate' | 'rename' | 'skip' | 'fail';
|
|
18
|
+
|
|
19
|
+
/** How to handle missing dependencies in the target project. */
|
|
20
|
+
export type TransferOnMissingDep = 'strip' | 'fail';
|
|
21
|
+
|
|
22
|
+
/** Parameters for a cross-project transfer operation. */
|
|
23
|
+
export interface TransferParams {
|
|
24
|
+
/** Task IDs to transfer from the source project. */
|
|
25
|
+
taskIds: string[];
|
|
26
|
+
/** Source project name or hash. */
|
|
27
|
+
sourceProject: string;
|
|
28
|
+
/** Target project name or hash. */
|
|
29
|
+
targetProject: string;
|
|
30
|
+
/** Copy (default) keeps source tasks; move archives them. */
|
|
31
|
+
mode?: TransferMode;
|
|
32
|
+
/** Single transfers individual tasks; subtree transfers tasks + all descendants. */
|
|
33
|
+
scope?: TransferScope;
|
|
34
|
+
/** How to handle title conflicts in the target. */
|
|
35
|
+
onConflict?: TransferOnConflict;
|
|
36
|
+
/** How to handle missing deps in the target. */
|
|
37
|
+
onMissingDep?: TransferOnMissingDep;
|
|
38
|
+
/** Whether to add provenance notes to transferred tasks. */
|
|
39
|
+
provenance?: boolean;
|
|
40
|
+
/** Override parent ID in target project. */
|
|
41
|
+
targetParent?: string;
|
|
42
|
+
/** Whether to transfer brain observations linked to source tasks. */
|
|
43
|
+
transferBrain?: boolean;
|
|
44
|
+
/** Dry run: preview without writing. */
|
|
45
|
+
dryRun?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A single task entry in the transfer manifest. */
|
|
49
|
+
export interface TransferManifestEntry {
|
|
50
|
+
/** Original task ID in source project. */
|
|
51
|
+
sourceId: string;
|
|
52
|
+
/** New task ID in target project. */
|
|
53
|
+
targetId: string;
|
|
54
|
+
/** Task title. */
|
|
55
|
+
title: string;
|
|
56
|
+
/** Task type (task, epic, milestone, etc.). */
|
|
57
|
+
type: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Manifest describing what was (or would be) transferred. */
|
|
61
|
+
export interface TransferManifest {
|
|
62
|
+
/** Source project name. */
|
|
63
|
+
sourceProject: string;
|
|
64
|
+
/** Target project name. */
|
|
65
|
+
targetProject: string;
|
|
66
|
+
/** Transfer mode used. */
|
|
67
|
+
mode: TransferMode;
|
|
68
|
+
/** Transfer scope used. */
|
|
69
|
+
scope: TransferScope;
|
|
70
|
+
/** Tasks included in the transfer. */
|
|
71
|
+
entries: TransferManifestEntry[];
|
|
72
|
+
/** ID remap table: sourceId -> targetId. */
|
|
73
|
+
idRemap: Record<string, string>;
|
|
74
|
+
/** Number of brain observations transferred. */
|
|
75
|
+
brainObservationsTransferred: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Result of a transfer operation. */
|
|
79
|
+
export interface TransferResult {
|
|
80
|
+
/** Whether this was a dry run. */
|
|
81
|
+
dryRun: boolean;
|
|
82
|
+
/** Number of tasks transferred. */
|
|
83
|
+
transferred: number;
|
|
84
|
+
/** Number of tasks skipped (conflict resolution). */
|
|
85
|
+
skipped: number;
|
|
86
|
+
/** Number of source tasks archived (move mode only). */
|
|
87
|
+
archived: number;
|
|
88
|
+
/** Number of external links created. */
|
|
89
|
+
linksCreated: number;
|
|
90
|
+
/** Number of brain observations transferred. */
|
|
91
|
+
brainObservationsTransferred: number;
|
|
92
|
+
/** Transfer manifest with full details. */
|
|
93
|
+
manifest: TransferManifest;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Options passed to importFromPackage (extracted from importTasksPackage). */
|
|
97
|
+
export interface ImportFromPackageOptions {
|
|
98
|
+
/** Working directory for the target project. */
|
|
99
|
+
cwd?: string;
|
|
100
|
+
/** Dry run: preview without writing. */
|
|
101
|
+
dryRun?: boolean;
|
|
102
|
+
/** Parent task ID in target. */
|
|
103
|
+
parent?: string;
|
|
104
|
+
/** Phase override. */
|
|
105
|
+
phase?: string;
|
|
106
|
+
/** Label to add to imported tasks. */
|
|
107
|
+
addLabel?: string;
|
|
108
|
+
/** Whether to add provenance notes. */
|
|
109
|
+
provenance?: boolean;
|
|
110
|
+
/** Status to reset imported tasks to. */
|
|
111
|
+
resetStatus?: Task['status'];
|
|
112
|
+
/** Conflict resolution strategy. */
|
|
113
|
+
onConflict?: TransferOnConflict;
|
|
114
|
+
/** Missing dependency strategy. */
|
|
115
|
+
onMissingDep?: TransferOnMissingDep;
|
|
116
|
+
/** Force import (skip duplicate checks). */
|
|
117
|
+
force?: boolean;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Result from importFromPackage. */
|
|
121
|
+
export interface ImportFromPackageResult {
|
|
122
|
+
imported: number;
|
|
123
|
+
skipped: number;
|
|
124
|
+
idRemap: Record<string, string>;
|
|
125
|
+
dryRun?: boolean;
|
|
126
|
+
preview?: {
|
|
127
|
+
tasks: Array<{ id: string; title: string; type: string }>;
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-project task transfer engine for NEXUS.
|
|
3
|
+
*
|
|
4
|
+
* Provides executeTransfer() and previewTransfer() for moving/copying
|
|
5
|
+
* tasks between registered NEXUS projects with full provenance tracking.
|
|
6
|
+
*
|
|
7
|
+
* @task T046, T049, T050, T051, T052, T053
|
|
8
|
+
* @epic T4540
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { randomUUID } from 'node:crypto';
|
|
12
|
+
import { importFromPackage } from '../admin/import-tasks.js';
|
|
13
|
+
import { getLogger } from '../logger.js';
|
|
14
|
+
import { createLink } from '../reconciliation/link-store.js';
|
|
15
|
+
import { BrainDataAccessor } from '../store/brain-accessor.js';
|
|
16
|
+
import { getBrainDb } from '../store/brain-sqlite.js';
|
|
17
|
+
import { getAccessor } from '../store/data-accessor.js';
|
|
18
|
+
import { exportSingle, exportSubtree } from '../store/export.js';
|
|
19
|
+
import { requirePermission } from './permissions.js';
|
|
20
|
+
import { nexusGetProject } from './registry.js';
|
|
21
|
+
import type {
|
|
22
|
+
TransferManifest,
|
|
23
|
+
TransferManifestEntry,
|
|
24
|
+
TransferParams,
|
|
25
|
+
TransferResult,
|
|
26
|
+
} from './transfer-types.js';
|
|
27
|
+
|
|
28
|
+
const log = getLogger('nexus:transfer');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Preview a transfer without writing any data.
|
|
32
|
+
* Validates projects, permissions, and builds the manifest.
|
|
33
|
+
*/
|
|
34
|
+
export async function previewTransfer(params: TransferParams): Promise<TransferResult> {
|
|
35
|
+
return executeTransferInternal({ ...params, dryRun: true });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Execute a cross-project task transfer.
|
|
40
|
+
*
|
|
41
|
+
* Pipeline:
|
|
42
|
+
* 1. Validate source/target projects via nexusGetProject()
|
|
43
|
+
* 2. Check permissions: read on source, write on target
|
|
44
|
+
* 3. Read source tasks
|
|
45
|
+
* 4. Build ExportPackage via exportSubtree/exportSingle
|
|
46
|
+
* 5. Import into target via importFromPackage()
|
|
47
|
+
* 6. Create bidirectional external_task_links
|
|
48
|
+
* 7. Write nexus_audit_log entry
|
|
49
|
+
* 8. If move mode: archive source tasks
|
|
50
|
+
* 9. If transferBrain: copy brain observations
|
|
51
|
+
*/
|
|
52
|
+
export async function executeTransfer(params: TransferParams): Promise<TransferResult> {
|
|
53
|
+
return executeTransferInternal(params);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function executeTransferInternal(params: TransferParams): Promise<TransferResult> {
|
|
57
|
+
const {
|
|
58
|
+
taskIds,
|
|
59
|
+
sourceProject: sourceProjectRef,
|
|
60
|
+
targetProject: targetProjectRef,
|
|
61
|
+
mode = 'copy',
|
|
62
|
+
scope = 'subtree',
|
|
63
|
+
onConflict = 'rename',
|
|
64
|
+
onMissingDep = 'strip',
|
|
65
|
+
provenance = true,
|
|
66
|
+
targetParent,
|
|
67
|
+
transferBrain = false,
|
|
68
|
+
dryRun = false,
|
|
69
|
+
} = params;
|
|
70
|
+
|
|
71
|
+
if (!taskIds.length) {
|
|
72
|
+
throw new Error('No task IDs specified for transfer');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Step 1: Validate projects
|
|
76
|
+
const sourceProject = await nexusGetProject(sourceProjectRef);
|
|
77
|
+
if (!sourceProject) {
|
|
78
|
+
throw new Error(`Source project not found: ${sourceProjectRef}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const targetProject = await nexusGetProject(targetProjectRef);
|
|
82
|
+
if (!targetProject) {
|
|
83
|
+
throw new Error(`Target project not found: ${targetProjectRef}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (sourceProject.hash === targetProject.hash) {
|
|
87
|
+
throw new Error('Source and target projects must be different');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Step 2: Check permissions
|
|
91
|
+
await requirePermission(sourceProject.hash, 'read', 'nexus.transfer');
|
|
92
|
+
await requirePermission(targetProject.hash, 'write', 'nexus.transfer');
|
|
93
|
+
|
|
94
|
+
// Step 3: Read source tasks
|
|
95
|
+
const sourceAccessor = await getAccessor(sourceProject.path);
|
|
96
|
+
const { tasks: allSourceTasks } = await sourceAccessor.queryTasks({});
|
|
97
|
+
|
|
98
|
+
// Step 4: Build ExportPackage for each task
|
|
99
|
+
const exportPackages = [];
|
|
100
|
+
for (const taskId of taskIds) {
|
|
101
|
+
const pkg =
|
|
102
|
+
scope === 'subtree'
|
|
103
|
+
? exportSubtree(taskId, allSourceTasks, sourceProject.name)
|
|
104
|
+
: exportSingle(taskId, allSourceTasks, sourceProject.name);
|
|
105
|
+
|
|
106
|
+
if (!pkg) {
|
|
107
|
+
throw new Error(`Task not found in source project: ${taskId}`);
|
|
108
|
+
}
|
|
109
|
+
exportPackages.push(pkg);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Merge all packages into one (dedup by task ID)
|
|
113
|
+
const seenIds = new Set<string>();
|
|
114
|
+
const mergedTasks = [];
|
|
115
|
+
for (const pkg of exportPackages) {
|
|
116
|
+
for (const task of pkg.tasks) {
|
|
117
|
+
if (!seenIds.has(task.id)) {
|
|
118
|
+
seenIds.add(task.id);
|
|
119
|
+
mergedTasks.push(task);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Use the first package as base and replace tasks
|
|
125
|
+
const mergedPkg = { ...exportPackages[0]! };
|
|
126
|
+
mergedPkg.tasks = mergedTasks;
|
|
127
|
+
mergedPkg._meta = { ...mergedPkg._meta, taskCount: mergedTasks.length };
|
|
128
|
+
|
|
129
|
+
// Step 5: Import into target
|
|
130
|
+
const importResult = await importFromPackage(mergedPkg, {
|
|
131
|
+
cwd: targetProject.path,
|
|
132
|
+
dryRun,
|
|
133
|
+
parent: targetParent,
|
|
134
|
+
provenance,
|
|
135
|
+
onConflict,
|
|
136
|
+
onMissingDep,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Build manifest
|
|
140
|
+
const entries: TransferManifestEntry[] = mergedTasks.map((t) => ({
|
|
141
|
+
sourceId: t.id,
|
|
142
|
+
targetId: importResult.idRemap[t.id] ?? t.id,
|
|
143
|
+
title: t.title,
|
|
144
|
+
type: t.type ?? 'task',
|
|
145
|
+
}));
|
|
146
|
+
|
|
147
|
+
const manifest: TransferManifest = {
|
|
148
|
+
sourceProject: sourceProject.name,
|
|
149
|
+
targetProject: targetProject.name,
|
|
150
|
+
mode,
|
|
151
|
+
scope,
|
|
152
|
+
entries,
|
|
153
|
+
idRemap: importResult.idRemap,
|
|
154
|
+
brainObservationsTransferred: 0,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const result: TransferResult = {
|
|
158
|
+
dryRun,
|
|
159
|
+
transferred: importResult.imported,
|
|
160
|
+
skipped: importResult.skipped,
|
|
161
|
+
archived: 0,
|
|
162
|
+
linksCreated: 0,
|
|
163
|
+
brainObservationsTransferred: 0,
|
|
164
|
+
manifest,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
if (dryRun) {
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Step 6: Create bidirectional external_task_links
|
|
172
|
+
// Only create links for tasks that were actually imported (verify they exist in target)
|
|
173
|
+
let linksCreated = 0;
|
|
174
|
+
const targetAccessor = await getAccessor(targetProject.path);
|
|
175
|
+
const { tasks: targetTasks } = await targetAccessor.queryTasks({});
|
|
176
|
+
const targetTaskIds = new Set(targetTasks.map((t) => t.id));
|
|
177
|
+
|
|
178
|
+
for (const entry of entries) {
|
|
179
|
+
if (importResult.idRemap[entry.sourceId] && targetTaskIds.has(entry.targetId)) {
|
|
180
|
+
// Link in target: points back to source
|
|
181
|
+
await createLink(
|
|
182
|
+
{
|
|
183
|
+
taskId: entry.targetId,
|
|
184
|
+
providerId: `nexus:${sourceProject.name}`,
|
|
185
|
+
externalId: entry.sourceId,
|
|
186
|
+
externalTitle: entry.title,
|
|
187
|
+
linkType: 'transferred',
|
|
188
|
+
syncDirection: 'inbound',
|
|
189
|
+
metadata: {
|
|
190
|
+
transferMode: mode,
|
|
191
|
+
transferScope: scope,
|
|
192
|
+
sourceProject: sourceProject.name,
|
|
193
|
+
transferredAt: new Date().toISOString(),
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
targetProject.path,
|
|
197
|
+
);
|
|
198
|
+
linksCreated++;
|
|
199
|
+
|
|
200
|
+
// Link in source: points to target
|
|
201
|
+
await createLink(
|
|
202
|
+
{
|
|
203
|
+
taskId: entry.sourceId,
|
|
204
|
+
providerId: `nexus:${targetProject.name}`,
|
|
205
|
+
externalId: entry.targetId,
|
|
206
|
+
externalTitle: entry.title,
|
|
207
|
+
linkType: 'transferred',
|
|
208
|
+
syncDirection: 'outbound',
|
|
209
|
+
metadata: {
|
|
210
|
+
transferMode: mode,
|
|
211
|
+
transferScope: scope,
|
|
212
|
+
targetProject: targetProject.name,
|
|
213
|
+
transferredAt: new Date().toISOString(),
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
sourceProject.path,
|
|
217
|
+
);
|
|
218
|
+
linksCreated++;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
result.linksCreated = linksCreated;
|
|
222
|
+
|
|
223
|
+
// Step 7: Write audit log
|
|
224
|
+
try {
|
|
225
|
+
const { getNexusDb } = await import('../store/nexus-sqlite.js');
|
|
226
|
+
const { nexusAuditLog } = await import('../store/nexus-schema.js');
|
|
227
|
+
const db = await getNexusDb();
|
|
228
|
+
await db.insert(nexusAuditLog).values({
|
|
229
|
+
id: randomUUID(),
|
|
230
|
+
action: 'transfer',
|
|
231
|
+
projectHash: sourceProject.hash,
|
|
232
|
+
projectId: sourceProject.projectId,
|
|
233
|
+
domain: 'nexus',
|
|
234
|
+
operation: 'transfer',
|
|
235
|
+
success: 1,
|
|
236
|
+
detailsJson: JSON.stringify({
|
|
237
|
+
sourceProject: sourceProject.name,
|
|
238
|
+
targetProject: targetProject.name,
|
|
239
|
+
mode,
|
|
240
|
+
scope,
|
|
241
|
+
taskCount: result.transferred,
|
|
242
|
+
idRemap: importResult.idRemap,
|
|
243
|
+
}),
|
|
244
|
+
});
|
|
245
|
+
} catch (err) {
|
|
246
|
+
log.warn({ err }, 'nexus transfer audit write failed');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Step 8: Move mode — archive source tasks
|
|
250
|
+
if (mode === 'move') {
|
|
251
|
+
let archived = 0;
|
|
252
|
+
for (const entry of entries) {
|
|
253
|
+
if (importResult.idRemap[entry.sourceId]) {
|
|
254
|
+
try {
|
|
255
|
+
await sourceAccessor.archiveSingleTask(entry.sourceId, {
|
|
256
|
+
archivedAt: new Date().toISOString(),
|
|
257
|
+
archiveReason: `Transferred to ${targetProject.name} as ${entry.targetId}`,
|
|
258
|
+
});
|
|
259
|
+
archived++;
|
|
260
|
+
} catch (err) {
|
|
261
|
+
log.warn({ err, taskId: entry.sourceId }, 'failed to archive source task after transfer');
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
result.archived = archived;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Step 9: Brain observation transfer
|
|
269
|
+
if (transferBrain) {
|
|
270
|
+
let brainTransferred = 0;
|
|
271
|
+
try {
|
|
272
|
+
const sourceBrainDb = await getBrainDb(sourceProject.path);
|
|
273
|
+
const targetBrainDb = await getBrainDb(targetProject.path);
|
|
274
|
+
const sourceBrain = new BrainDataAccessor(sourceBrainDb);
|
|
275
|
+
const targetBrain = new BrainDataAccessor(targetBrainDb);
|
|
276
|
+
|
|
277
|
+
for (const entry of entries) {
|
|
278
|
+
if (!importResult.idRemap[entry.sourceId]) continue;
|
|
279
|
+
|
|
280
|
+
const links = await sourceBrain.getLinksForTask(entry.sourceId);
|
|
281
|
+
for (const link of links) {
|
|
282
|
+
if (link.memoryType !== 'observation') continue;
|
|
283
|
+
|
|
284
|
+
const observation = await sourceBrain.getObservation(link.memoryId);
|
|
285
|
+
if (!observation) continue;
|
|
286
|
+
|
|
287
|
+
const newObsId = `O-${randomUUID().slice(0, 8)}`;
|
|
288
|
+
await targetBrain.addObservation({
|
|
289
|
+
...observation,
|
|
290
|
+
id: newObsId,
|
|
291
|
+
createdAt: observation.createdAt,
|
|
292
|
+
updatedAt: new Date().toISOString().replace('T', ' ').slice(0, 19),
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
await targetBrain.addLink({
|
|
296
|
+
memoryType: 'observation',
|
|
297
|
+
memoryId: newObsId,
|
|
298
|
+
taskId: entry.targetId,
|
|
299
|
+
linkType: 'applies_to',
|
|
300
|
+
createdAt: new Date().toISOString().replace('T', ' ').slice(0, 19),
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
brainTransferred++;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
} catch (err) {
|
|
307
|
+
log.warn({ err }, 'brain observation transfer failed');
|
|
308
|
+
}
|
|
309
|
+
result.brainObservationsTransferred = brainTransferred;
|
|
310
|
+
result.manifest.brainObservationsTransferred = brainTransferred;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return result;
|
|
314
|
+
}
|
|
@@ -50,13 +50,17 @@ export function getBrainDbPath(cwd?: string): string {
|
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
52
|
* Resolve the path to the drizzle-brain migrations folder.
|
|
53
|
-
* Works from both src/ (dev via tsx) and dist/ (compiled).
|
|
53
|
+
* Works from both src/ (dev via tsx) and dist/ (compiled via esbuild bundle).
|
|
54
|
+
*
|
|
55
|
+
* - Source layout: __dirname = src/store/ → need ../../migrations/drizzle-brain
|
|
56
|
+
* - Bundled layout: __dirname = dist/ → need ../migrations/drizzle-brain
|
|
54
57
|
*/
|
|
55
58
|
export function resolveBrainMigrationsFolder(): string {
|
|
56
59
|
const __filename = fileURLToPath(import.meta.url);
|
|
57
60
|
const __dirname = dirname(__filename);
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
const isBundled = __dirname.endsWith('/dist') || __dirname.endsWith('\\dist');
|
|
62
|
+
const pkgRoot = isBundled ? join(__dirname, '..') : join(__dirname, '..', '..');
|
|
63
|
+
return join(pkgRoot, 'migrations', 'drizzle-brain');
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
/**
|
|
@@ -46,13 +46,17 @@ export function getNexusDbPath(): string {
|
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
48
|
* Resolve the path to the drizzle-nexus migrations folder.
|
|
49
|
-
* Works from both src/ (dev via tsx) and dist/ (compiled).
|
|
49
|
+
* Works from both src/ (dev via tsx) and dist/ (compiled via esbuild bundle).
|
|
50
|
+
*
|
|
51
|
+
* - Source layout: __dirname = src/store/ → need ../../migrations/drizzle-nexus
|
|
52
|
+
* - Bundled layout: __dirname = dist/ → need ../migrations/drizzle-nexus
|
|
50
53
|
*/
|
|
51
54
|
export function resolveNexusMigrationsFolder(): string {
|
|
52
55
|
const __filename = fileURLToPath(import.meta.url);
|
|
53
56
|
const __dirname = dirname(__filename);
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
const isBundled = __dirname.endsWith('/dist') || __dirname.endsWith('\\dist');
|
|
58
|
+
const pkgRoot = isBundled ? join(__dirname, '..') : join(__dirname, '..', '..');
|
|
59
|
+
return join(pkgRoot, 'migrations', 'drizzle-nexus');
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
/**
|
package/src/store/sqlite.ts
CHANGED
|
@@ -357,13 +357,19 @@ export async function getDb(cwd?: string): Promise<NodeSQLiteDatabase<typeof sch
|
|
|
357
357
|
|
|
358
358
|
/**
|
|
359
359
|
* Resolve the path to the drizzle migrations folder.
|
|
360
|
-
* Works from both src/ (dev via tsx) and dist/ (compiled).
|
|
360
|
+
* Works from both src/ (dev via tsx) and dist/ (compiled via esbuild bundle).
|
|
361
|
+
*
|
|
362
|
+
* - Source layout: __dirname = src/store/ → need ../../migrations/drizzle-tasks
|
|
363
|
+
* - Bundled layout: __dirname = dist/ → need ../migrations/drizzle-tasks
|
|
361
364
|
*/
|
|
362
365
|
export function resolveMigrationsFolder(): string {
|
|
363
366
|
const __filename = fileURLToPath(import.meta.url);
|
|
364
367
|
const __dirname = dirname(__filename);
|
|
365
|
-
//
|
|
366
|
-
|
|
368
|
+
// When esbuild bundles into dist/index.js, __dirname is dist/ (1 level deep).
|
|
369
|
+
// When running from source via tsx, __dirname is src/store/ (2 levels deep).
|
|
370
|
+
const isBundled = __dirname.endsWith('/dist') || __dirname.endsWith('\\dist');
|
|
371
|
+
const pkgRoot = isBundled ? join(__dirname, '..') : join(__dirname, '..', '..');
|
|
372
|
+
return join(pkgRoot, 'migrations', 'drizzle-tasks');
|
|
367
373
|
}
|
|
368
374
|
|
|
369
375
|
/**
|
|
@@ -127,7 +127,7 @@ export const TASK_RELATION_TYPES = [
|
|
|
127
127
|
export const LIFECYCLE_TRANSITION_TYPES = ['automatic', 'manual', 'forced'] as const;
|
|
128
128
|
|
|
129
129
|
/** External task link types matching DB constraint on external_task_links.link_type. */
|
|
130
|
-
export const EXTERNAL_LINK_TYPES = ['created', 'matched', 'manual'] as const;
|
|
130
|
+
export const EXTERNAL_LINK_TYPES = ['created', 'matched', 'manual', 'transferred'] as const;
|
|
131
131
|
|
|
132
132
|
/** Sync direction types matching DB constraint on external_task_links.sync_direction. */
|
|
133
133
|
export const SYNC_DIRECTIONS = ['inbound', 'outbound', 'bidirectional'] as const;
|