tarsk 0.3.16 → 0.3.19
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/index.js +7 -8
- package/dist/managers/conversation-manager.d.ts +3 -2
- package/dist/managers/conversation-manager.js +4 -4
- package/dist/managers/metadata-manager.d.ts +3 -2
- package/dist/managers/metadata-manager.js +4 -3
- package/dist/managers/project-manager.d.ts +25 -3
- package/dist/managers/project-manager.js +85 -16
- package/dist/managers/thread-manager.d.ts +14 -6
- package/dist/managers/thread-manager.js +97 -28
- package/dist/paths.d.ts +22 -0
- package/dist/paths.js +26 -0
- package/dist/provider-data.d.ts +6 -6
- package/dist/provider-data.js +10 -4
- package/dist/public/assets/index-DBN5QJqj.css +1 -0
- package/dist/public/assets/{index-CLr9LKtA.js → index-DsG11Rhy.js} +1710 -1714
- package/dist/public/index.html +2 -2
- package/dist/routes/projects.js +46 -0
- package/dist/types/models.d.ts +10 -0
- package/package.json +1 -1
- package/dist/public/assets/index-CjXGVbI7.css +0 -1
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import { createProviderRoutes } from './routes/providers.js';
|
|
|
20
20
|
import { createModelRoutes } from './routes/models.js';
|
|
21
21
|
import { createGitRoutes } from './routes/git.js';
|
|
22
22
|
import { AVAILABLE_PROGRAMS } from './types/models.js';
|
|
23
|
+
import { getDataDir } from './paths.js';
|
|
23
24
|
const __filename = fileURLToPath(import.meta.url);
|
|
24
25
|
const __dirname = path.dirname(__filename);
|
|
25
26
|
// Parse arguments
|
|
@@ -30,20 +31,18 @@ const shouldOpenBrowser = args.includes('--open');
|
|
|
30
31
|
if (!isDebug) {
|
|
31
32
|
console.log = () => { };
|
|
32
33
|
}
|
|
33
|
-
const positionalArgs = args.filter(arg => !arg.startsWith('--'));
|
|
34
|
-
const rootFolderArg = positionalArgs[0];
|
|
35
34
|
const app = new Hono();
|
|
36
35
|
// Configure CORS middleware
|
|
37
36
|
app.use('/*', cors());
|
|
38
37
|
// Initialize managers
|
|
39
|
-
const
|
|
40
|
-
const metadataManager = new MetadataManager(
|
|
38
|
+
const dataDir = getDataDir();
|
|
39
|
+
const metadataManager = new MetadataManager(dataDir);
|
|
41
40
|
const gitManager = new GitManagerImpl();
|
|
42
|
-
const projectManager = new ProjectManagerImpl(rootFolder, metadataManager, gitManager);
|
|
43
|
-
const threadManager = new ThreadManagerImpl(metadataManager, gitManager);
|
|
44
|
-
const conversationManager = new ConversationManagerImpl(rootFolder);
|
|
45
|
-
const neovateExecutor = new NeovateExecutorImpl(metadataManager);
|
|
46
41
|
const processingStateManager = new ProcessingStateManagerImpl();
|
|
42
|
+
const projectManager = new ProjectManagerImpl(dataDir, metadataManager, gitManager, processingStateManager);
|
|
43
|
+
const threadManager = new ThreadManagerImpl(metadataManager, gitManager, processingStateManager);
|
|
44
|
+
const conversationManager = new ConversationManagerImpl(dataDir);
|
|
45
|
+
const neovateExecutor = new NeovateExecutorImpl(metadataManager);
|
|
47
46
|
// Initialize metadata storage
|
|
48
47
|
await metadataManager.initialize();
|
|
49
48
|
// Health check endpoint
|
|
@@ -51,9 +51,10 @@ export declare class ConversationManagerImpl implements ConversationManager {
|
|
|
51
51
|
private metadataDir;
|
|
52
52
|
/**
|
|
53
53
|
* Create a new ConversationManagerImpl
|
|
54
|
-
* @param
|
|
54
|
+
* @param dataDir - Directory where metadata files are stored
|
|
55
|
+
* (e.g. ~/Library/Application Support/Tarsk/data)
|
|
55
56
|
*/
|
|
56
|
-
constructor(
|
|
57
|
+
constructor(dataDir: string);
|
|
57
58
|
/**
|
|
58
59
|
* Starts a new conversation message
|
|
59
60
|
*/
|
|
@@ -10,7 +10,6 @@ import { promises as fs } from 'fs';
|
|
|
10
10
|
import { join, dirname } from 'path';
|
|
11
11
|
import { randomUUID } from 'crypto';
|
|
12
12
|
const CONVERSATION_FILE = 'conversation-history.json';
|
|
13
|
-
const METADATA_DIR = '.metadata';
|
|
14
13
|
/**
|
|
15
14
|
* ConversationManagerImpl provides the implementation for conversation operations
|
|
16
15
|
*/
|
|
@@ -18,10 +17,11 @@ export class ConversationManagerImpl {
|
|
|
18
17
|
metadataDir;
|
|
19
18
|
/**
|
|
20
19
|
* Create a new ConversationManagerImpl
|
|
21
|
-
* @param
|
|
20
|
+
* @param dataDir - Directory where metadata files are stored
|
|
21
|
+
* (e.g. ~/Library/Application Support/Tarsk/data)
|
|
22
22
|
*/
|
|
23
|
-
constructor(
|
|
24
|
-
this.metadataDir =
|
|
23
|
+
constructor(dataDir) {
|
|
24
|
+
this.metadataDir = dataDir;
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
27
|
* Starts a new conversation message
|
|
@@ -20,9 +20,10 @@ export declare class MetadataManager {
|
|
|
20
20
|
private metadataDir;
|
|
21
21
|
/**
|
|
22
22
|
* Create a new MetadataManager
|
|
23
|
-
* @param
|
|
23
|
+
* @param dataDir - Directory where metadata files will be stored
|
|
24
|
+
* (e.g. ~/Library/Application Support/Tarsk/data)
|
|
24
25
|
*/
|
|
25
|
-
constructor(
|
|
26
|
+
constructor(dataDir: string);
|
|
26
27
|
/**
|
|
27
28
|
* Initialize metadata directory if it doesn't exist
|
|
28
29
|
*/
|
|
@@ -13,10 +13,11 @@ export class MetadataManager {
|
|
|
13
13
|
metadataDir;
|
|
14
14
|
/**
|
|
15
15
|
* Create a new MetadataManager
|
|
16
|
-
* @param
|
|
16
|
+
* @param dataDir - Directory where metadata files will be stored
|
|
17
|
+
* (e.g. ~/Library/Application Support/Tarsk/data)
|
|
17
18
|
*/
|
|
18
|
-
constructor(
|
|
19
|
-
this.metadataDir =
|
|
19
|
+
constructor(dataDir) {
|
|
20
|
+
this.metadataDir = dataDir;
|
|
20
21
|
this.projectsFile = join(this.metadataDir, "projects.json");
|
|
21
22
|
this.threadsFile = join(this.metadataDir, "threads.json");
|
|
22
23
|
this.stateFile = join(this.metadataDir, "state.json");
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { Project, ProjectEvent, Command } from '../types/models.js';
|
|
11
11
|
import { MetadataManager } from './metadata-manager.js';
|
|
12
12
|
import { GitManager } from './git-manager.js';
|
|
13
|
+
import type { ProcessingStateManager } from './processing-state-manager.js';
|
|
13
14
|
/**
|
|
14
15
|
* ProjectManager interface defines the contract for project operations
|
|
15
16
|
*/
|
|
@@ -67,6 +68,12 @@ export interface ProjectManager {
|
|
|
67
68
|
* @param cwd - Optional working directory relative to thread path
|
|
68
69
|
*/
|
|
69
70
|
runCommand(threadId: string, commandLine: string, cwd?: string): AsyncGenerator<unknown>;
|
|
71
|
+
/**
|
|
72
|
+
* Updates the project's setup script
|
|
73
|
+
* @param projectId - The project ID
|
|
74
|
+
* @param setupScript - The setup script to run after clone (or null to clear)
|
|
75
|
+
*/
|
|
76
|
+
updateSetupScript(projectId: string, setupScript: string | null): Promise<void>;
|
|
70
77
|
}
|
|
71
78
|
/**
|
|
72
79
|
* ProjectManagerImpl provides the implementation for project operations
|
|
@@ -75,17 +82,19 @@ export declare class ProjectManagerImpl implements ProjectManager {
|
|
|
75
82
|
private rootFolder;
|
|
76
83
|
private metadataManager;
|
|
77
84
|
private gitManager;
|
|
85
|
+
private processingStateManager?;
|
|
78
86
|
/**
|
|
79
87
|
* Create a new ProjectManager
|
|
80
88
|
* @param rootFolder - Base directory where projects will be stored
|
|
81
89
|
* @param metadataManager - Manager for persisting metadata
|
|
82
90
|
* @param gitManager - Manager for git operations
|
|
91
|
+
* @param processingStateManager - Optional manager for processing state (for spinner during clone)
|
|
83
92
|
*
|
|
84
93
|
* Requirements:
|
|
85
94
|
* - 1.1 - WHEN a user provides a git URL to create a new Project, THE CLI SHALL clone the repository into a folder under the Root_Folder
|
|
86
95
|
* - 1.4 - THE CLI SHALL determine the storage path for each Project folder
|
|
87
96
|
*/
|
|
88
|
-
constructor(rootFolder: string, metadataManager: MetadataManager, gitManager: GitManager);
|
|
97
|
+
constructor(rootFolder: string, metadataManager: MetadataManager, gitManager: GitManager, processingStateManager?: ProcessingStateManager);
|
|
89
98
|
/**
|
|
90
99
|
* Creates a new project from a git URL
|
|
91
100
|
*
|
|
@@ -149,9 +158,9 @@ export declare class ProjectManagerImpl implements ProjectManager {
|
|
|
149
158
|
*/
|
|
150
159
|
private deriveProjectName;
|
|
151
160
|
/**
|
|
152
|
-
* Generates a project path under the root folder
|
|
161
|
+
* Generates a project path under the root folder using the project ID
|
|
153
162
|
*
|
|
154
|
-
* @param
|
|
163
|
+
* @param projectId - The unique project ID
|
|
155
164
|
* @returns The absolute path to the project folder
|
|
156
165
|
*
|
|
157
166
|
* Requirements:
|
|
@@ -189,6 +198,19 @@ export declare class ProjectManagerImpl implements ProjectManager {
|
|
|
189
198
|
* @param commandId - The ID of the command to delete
|
|
190
199
|
*/
|
|
191
200
|
deleteCommand(projectId: string, commandId: string): Promise<void>;
|
|
201
|
+
/**
|
|
202
|
+
* Detects a suggested setup script by checking for package.json and lock files
|
|
203
|
+
* Order: yarn.lock, bun.lock, pnpm-lock.yaml, else npm install
|
|
204
|
+
* @param chrootPath - Path to the cloned repo root
|
|
205
|
+
* @returns The suggested install command or null if no package.json
|
|
206
|
+
*/
|
|
207
|
+
private detectSetupScript;
|
|
208
|
+
/**
|
|
209
|
+
* Updates the project's setup script
|
|
210
|
+
* @param projectId - The project ID
|
|
211
|
+
* @param setupScript - The setup script to run after clone (or null to clear)
|
|
212
|
+
*/
|
|
213
|
+
updateSetupScript(projectId: string, setupScript: string | null): Promise<void>;
|
|
192
214
|
/**
|
|
193
215
|
* Runs a command in a thread's directory
|
|
194
216
|
* @param threadId - The thread ID where to run the command
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { randomUUID } from 'crypto';
|
|
11
11
|
import { join, relative } from 'path';
|
|
12
12
|
import { realpathSync } from 'fs';
|
|
13
|
+
import { access } from 'fs/promises';
|
|
13
14
|
/**
|
|
14
15
|
* ProjectManagerImpl provides the implementation for project operations
|
|
15
16
|
*/
|
|
@@ -17,20 +18,23 @@ export class ProjectManagerImpl {
|
|
|
17
18
|
rootFolder;
|
|
18
19
|
metadataManager;
|
|
19
20
|
gitManager;
|
|
21
|
+
processingStateManager;
|
|
20
22
|
/**
|
|
21
23
|
* Create a new ProjectManager
|
|
22
24
|
* @param rootFolder - Base directory where projects will be stored
|
|
23
25
|
* @param metadataManager - Manager for persisting metadata
|
|
24
26
|
* @param gitManager - Manager for git operations
|
|
27
|
+
* @param processingStateManager - Optional manager for processing state (for spinner during clone)
|
|
25
28
|
*
|
|
26
29
|
* Requirements:
|
|
27
30
|
* - 1.1 - WHEN a user provides a git URL to create a new Project, THE CLI SHALL clone the repository into a folder under the Root_Folder
|
|
28
31
|
* - 1.4 - THE CLI SHALL determine the storage path for each Project folder
|
|
29
32
|
*/
|
|
30
|
-
constructor(rootFolder, metadataManager, gitManager) {
|
|
33
|
+
constructor(rootFolder, metadataManager, gitManager, processingStateManager) {
|
|
31
34
|
this.rootFolder = rootFolder;
|
|
32
35
|
this.metadataManager = metadataManager;
|
|
33
36
|
this.gitManager = gitManager;
|
|
37
|
+
this.processingStateManager = processingStateManager;
|
|
34
38
|
}
|
|
35
39
|
/**
|
|
36
40
|
* Creates a new project from a git URL
|
|
@@ -63,27 +67,28 @@ export class ProjectManagerImpl {
|
|
|
63
67
|
};
|
|
64
68
|
return;
|
|
65
69
|
}
|
|
70
|
+
let initialThreadId;
|
|
66
71
|
try {
|
|
67
72
|
// Generate unique project ID
|
|
68
73
|
const projectId = randomUUID();
|
|
69
74
|
// Derive project name from git URL
|
|
70
75
|
const projectName = this.deriveProjectName(gitUrl);
|
|
71
|
-
// Generate project path
|
|
72
|
-
const projectPath = this.generateProjectPath(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
message: `Creating project "${projectName}" from ${gitUrl}...`
|
|
76
|
-
};
|
|
77
|
-
// Generate unique thread ID for the initial thread
|
|
78
|
-
const initialThreadId = randomUUID();
|
|
76
|
+
// Generate project path using the unique ID (not the name, to avoid collisions)
|
|
77
|
+
const projectPath = this.generateProjectPath(projectId);
|
|
78
|
+
// Generate unique thread ID for the initial thread (before clone so frontend can show spinner)
|
|
79
|
+
initialThreadId = randomUUID();
|
|
79
80
|
const firstThreadPath = join(projectPath, initialThreadId);
|
|
81
|
+
this.processingStateManager?.setProcessing(initialThreadId);
|
|
80
82
|
yield {
|
|
81
83
|
type: 'progress',
|
|
82
|
-
message: `
|
|
84
|
+
message: `Creating project "${projectName}" from ${gitUrl}...`,
|
|
85
|
+
threadId: initialThreadId,
|
|
86
|
+
projectId
|
|
83
87
|
};
|
|
84
88
|
// Stream git clone events
|
|
85
89
|
for await (const gitEvent of this.gitManager.cloneRepository(gitUrl, firstThreadPath)) {
|
|
86
90
|
if (gitEvent.type === 'error') {
|
|
91
|
+
this.processingStateManager?.clearProcessing(initialThreadId);
|
|
87
92
|
yield {
|
|
88
93
|
type: 'error',
|
|
89
94
|
message: `Git clone failed: ${gitEvent.message}`
|
|
@@ -98,7 +103,7 @@ export class ProjectManagerImpl {
|
|
|
98
103
|
}
|
|
99
104
|
}
|
|
100
105
|
// Create and checkout branch for initial thread
|
|
101
|
-
const initialThreadTitle = '
|
|
106
|
+
const initialThreadTitle = 'thread-1';
|
|
102
107
|
let branchName = this.gitManager.sanitizeBranchName(initialThreadTitle);
|
|
103
108
|
yield {
|
|
104
109
|
type: 'progress',
|
|
@@ -122,6 +127,7 @@ export class ProjectManagerImpl {
|
|
|
122
127
|
// Create and checkout the new branch
|
|
123
128
|
for await (const gitEvent of this.gitManager.createAndCheckoutBranch(firstThreadPath, branchName)) {
|
|
124
129
|
if (gitEvent.type === 'error') {
|
|
130
|
+
this.processingStateManager?.clearProcessing(initialThreadId);
|
|
125
131
|
yield {
|
|
126
132
|
type: 'error',
|
|
127
133
|
message: `Failed to create initial branch: ${gitEvent.message}`
|
|
@@ -139,6 +145,8 @@ export class ProjectManagerImpl {
|
|
|
139
145
|
type: 'progress',
|
|
140
146
|
message: `Branch "${branchName}" created and checked out`
|
|
141
147
|
};
|
|
148
|
+
// Detect suggested setup script from package.json and lock files
|
|
149
|
+
const suggestedSetupScript = await this.detectSetupScript(firstThreadPath);
|
|
142
150
|
// Create project metadata
|
|
143
151
|
const project = {
|
|
144
152
|
id: projectId,
|
|
@@ -169,13 +177,18 @@ export class ProjectManagerImpl {
|
|
|
169
177
|
message: `Project "${projectName}" created successfully with initial thread`
|
|
170
178
|
};
|
|
171
179
|
// Yield complete event with project data
|
|
180
|
+
this.processingStateManager?.clearProcessing(initialThreadId);
|
|
172
181
|
yield {
|
|
173
182
|
type: 'complete',
|
|
174
183
|
message: 'Project creation complete',
|
|
175
|
-
project
|
|
184
|
+
project,
|
|
185
|
+
suggestedSetupScript: suggestedSetupScript ?? undefined
|
|
176
186
|
};
|
|
177
187
|
}
|
|
178
188
|
catch (error) {
|
|
189
|
+
if (typeof initialThreadId !== 'undefined') {
|
|
190
|
+
this.processingStateManager?.clearProcessing(initialThreadId);
|
|
191
|
+
}
|
|
179
192
|
yield {
|
|
180
193
|
type: 'error',
|
|
181
194
|
message: `Failed to create project: ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -258,16 +271,16 @@ export class ProjectManagerImpl {
|
|
|
258
271
|
return name;
|
|
259
272
|
}
|
|
260
273
|
/**
|
|
261
|
-
* Generates a project path under the root folder
|
|
274
|
+
* Generates a project path under the root folder using the project ID
|
|
262
275
|
*
|
|
263
|
-
* @param
|
|
276
|
+
* @param projectId - The unique project ID
|
|
264
277
|
* @returns The absolute path to the project folder
|
|
265
278
|
*
|
|
266
279
|
* Requirements:
|
|
267
280
|
* - 1.4 - THE CLI SHALL determine the storage path for each Project folder
|
|
268
281
|
*/
|
|
269
|
-
generateProjectPath(
|
|
270
|
-
return join(this.rootFolder,
|
|
282
|
+
generateProjectPath(projectId) {
|
|
283
|
+
return join(this.rootFolder, projectId);
|
|
271
284
|
}
|
|
272
285
|
/**
|
|
273
286
|
* Gets the root folder path
|
|
@@ -391,6 +404,62 @@ export class ProjectManagerImpl {
|
|
|
391
404
|
}
|
|
392
405
|
await this.metadataManager.saveProjects(projects);
|
|
393
406
|
}
|
|
407
|
+
/**
|
|
408
|
+
* Detects a suggested setup script by checking for package.json and lock files
|
|
409
|
+
* Order: yarn.lock, bun.lock, pnpm-lock.yaml, else npm install
|
|
410
|
+
* @param chrootPath - Path to the cloned repo root
|
|
411
|
+
* @returns The suggested install command or null if no package.json
|
|
412
|
+
*/
|
|
413
|
+
async detectSetupScript(chrootPath) {
|
|
414
|
+
try {
|
|
415
|
+
await access(join(chrootPath, 'package.json'));
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
try {
|
|
421
|
+
await access(join(chrootPath, 'yarn.lock'));
|
|
422
|
+
return 'yarn install';
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
// continue
|
|
426
|
+
}
|
|
427
|
+
try {
|
|
428
|
+
await access(join(chrootPath, 'bun.lock'));
|
|
429
|
+
return 'bun install';
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
// continue
|
|
433
|
+
}
|
|
434
|
+
try {
|
|
435
|
+
await access(join(chrootPath, 'pnpm-lock.yaml'));
|
|
436
|
+
return 'pnpm install';
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
// continue
|
|
440
|
+
}
|
|
441
|
+
return 'npm install';
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Updates the project's setup script
|
|
445
|
+
* @param projectId - The project ID
|
|
446
|
+
* @param setupScript - The setup script to run after clone (or null to clear)
|
|
447
|
+
*/
|
|
448
|
+
async updateSetupScript(projectId, setupScript) {
|
|
449
|
+
const projects = await this.metadataManager.loadProjects();
|
|
450
|
+
const projectIndex = projects.findIndex(p => p.id === projectId);
|
|
451
|
+
if (projectIndex === -1) {
|
|
452
|
+
throw new Error(`Project not found: ${projectId}`);
|
|
453
|
+
}
|
|
454
|
+
// Update the project's setup script
|
|
455
|
+
if (setupScript === null || setupScript === '') {
|
|
456
|
+
delete projects[projectIndex].setupScript;
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
projects[projectIndex].setupScript = setupScript;
|
|
460
|
+
}
|
|
461
|
+
await this.metadataManager.saveProjects(projects);
|
|
462
|
+
}
|
|
394
463
|
/**
|
|
395
464
|
* Runs a command in a thread's directory
|
|
396
465
|
* @param threadId - The thread ID where to run the command
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { Thread, ThreadEvent } from '../types/models.js';
|
|
12
12
|
import { MetadataManager } from './metadata-manager.js';
|
|
13
13
|
import { GitManager } from './git-manager.js';
|
|
14
|
+
import type { ProcessingStateManager } from './processing-state-manager.js';
|
|
14
15
|
/**
|
|
15
16
|
* ThreadManager interface defines the contract for thread operations
|
|
16
17
|
*/
|
|
@@ -68,16 +69,18 @@ export interface ThreadManager {
|
|
|
68
69
|
export declare class ThreadManagerImpl implements ThreadManager {
|
|
69
70
|
private metadataManager;
|
|
70
71
|
private gitManager;
|
|
72
|
+
private processingStateManager?;
|
|
71
73
|
/**
|
|
72
74
|
* Create a new ThreadManager
|
|
73
75
|
* @param metadataManager - Manager for persisting metadata
|
|
74
76
|
* @param gitManager - Manager for git operations
|
|
77
|
+
* @param processingStateManager - Optional manager for processing state (for spinner during clone)
|
|
75
78
|
*
|
|
76
79
|
* Requirements:
|
|
77
80
|
* - 2.1 - WHEN a user creates a new Thread for a Project, THE CLI SHALL create a new clone of the Project's git repository
|
|
78
81
|
* - 2.4 - THE System SHALL maintain a title for each Thread
|
|
79
82
|
*/
|
|
80
|
-
constructor(metadataManager: MetadataManager, gitManager: GitManager);
|
|
83
|
+
constructor(metadataManager: MetadataManager, gitManager: GitManager, processingStateManager?: ProcessingStateManager);
|
|
81
84
|
/**
|
|
82
85
|
* Creates a new thread for an existing project
|
|
83
86
|
*
|
|
@@ -157,6 +160,13 @@ export declare class ThreadManagerImpl implements ThreadManager {
|
|
|
157
160
|
* @returns The selected thread ID or null
|
|
158
161
|
*/
|
|
159
162
|
getSelectedThreadId(): Promise<string | null>;
|
|
163
|
+
/**
|
|
164
|
+
* Runs a setup script in the given path and yields progress events
|
|
165
|
+
* @param threadPath - Path to run the command in
|
|
166
|
+
* @param commandLine - The command to execute
|
|
167
|
+
* @yields ThreadEvent progress events
|
|
168
|
+
*/
|
|
169
|
+
private runSetupScript;
|
|
160
170
|
/**
|
|
161
171
|
* Generates a unique thread path
|
|
162
172
|
*
|
|
@@ -175,12 +185,10 @@ export declare class ThreadManagerImpl implements ThreadManager {
|
|
|
175
185
|
/**
|
|
176
186
|
* Generates a thread title if not provided
|
|
177
187
|
*
|
|
178
|
-
* Format:
|
|
188
|
+
* Format: thread-{n} where n starts at 1 and increments for each new thread
|
|
179
189
|
*
|
|
180
|
-
* @param existingTitles - Set of titles already in use
|
|
181
|
-
* @
|
|
182
|
-
* @param projectName - Optional project name to include in title
|
|
183
|
-
* @returns The generated thread title
|
|
190
|
+
* @param existingTitles - Set of titles already in use for this project
|
|
191
|
+
* @returns The generated thread title (e.g. thread-1, thread-2, thread-3)
|
|
184
192
|
*/
|
|
185
193
|
private generateThreadTitle;
|
|
186
194
|
/**
|
|
@@ -16,18 +16,21 @@ import { join } from 'path';
|
|
|
16
16
|
export class ThreadManagerImpl {
|
|
17
17
|
metadataManager;
|
|
18
18
|
gitManager;
|
|
19
|
+
processingStateManager;
|
|
19
20
|
/**
|
|
20
21
|
* Create a new ThreadManager
|
|
21
22
|
* @param metadataManager - Manager for persisting metadata
|
|
22
23
|
* @param gitManager - Manager for git operations
|
|
24
|
+
* @param processingStateManager - Optional manager for processing state (for spinner during clone)
|
|
23
25
|
*
|
|
24
26
|
* Requirements:
|
|
25
27
|
* - 2.1 - WHEN a user creates a new Thread for a Project, THE CLI SHALL create a new clone of the Project's git repository
|
|
26
28
|
* - 2.4 - THE System SHALL maintain a title for each Thread
|
|
27
29
|
*/
|
|
28
|
-
constructor(metadataManager, gitManager) {
|
|
30
|
+
constructor(metadataManager, gitManager, processingStateManager) {
|
|
29
31
|
this.metadataManager = metadataManager;
|
|
30
32
|
this.gitManager = gitManager;
|
|
33
|
+
this.processingStateManager = processingStateManager;
|
|
31
34
|
}
|
|
32
35
|
/**
|
|
33
36
|
* Creates a new thread for an existing project
|
|
@@ -53,6 +56,7 @@ export class ThreadManagerImpl {
|
|
|
53
56
|
* - 7.4 - THE CLI SHALL ensure each Thread clone is stored in a unique directory path
|
|
54
57
|
*/
|
|
55
58
|
async *createThread(projectId, title) {
|
|
59
|
+
let threadId;
|
|
56
60
|
try {
|
|
57
61
|
// Load project to verify it exists and get git URL
|
|
58
62
|
const projects = await this.metadataManager.loadProjects();
|
|
@@ -65,7 +69,7 @@ export class ThreadManagerImpl {
|
|
|
65
69
|
return;
|
|
66
70
|
}
|
|
67
71
|
// Generate unique thread ID
|
|
68
|
-
|
|
72
|
+
threadId = randomUUID();
|
|
69
73
|
// Generate thread path: {projectPath}/{threadId}
|
|
70
74
|
const threadPath = this.generateThreadPath(project.path, threadId);
|
|
71
75
|
// Generate thread title if not provided
|
|
@@ -73,10 +77,12 @@ export class ThreadManagerImpl {
|
|
|
73
77
|
const existingTitles = new Set(existingThreads
|
|
74
78
|
.filter(t => t.projectId === projectId)
|
|
75
79
|
.map(t => t.title));
|
|
76
|
-
const threadTitle = title || this.generateThreadTitle(existingTitles
|
|
80
|
+
const threadTitle = title || this.generateThreadTitle(existingTitles);
|
|
81
|
+
this.processingStateManager?.setProcessing(threadId);
|
|
77
82
|
yield {
|
|
78
83
|
type: 'progress',
|
|
79
|
-
message: `Creating thread "${threadTitle}" for project "${project.name}"
|
|
84
|
+
message: `Creating thread "${threadTitle}" for project "${project.name}"...`,
|
|
85
|
+
threadId
|
|
80
86
|
};
|
|
81
87
|
yield {
|
|
82
88
|
type: 'progress',
|
|
@@ -85,6 +91,7 @@ export class ThreadManagerImpl {
|
|
|
85
91
|
// Stream git clone events
|
|
86
92
|
for await (const gitEvent of this.gitManager.cloneRepository(project.gitUrl, threadPath)) {
|
|
87
93
|
if (gitEvent.type === 'error') {
|
|
94
|
+
this.processingStateManager?.clearProcessing(threadId);
|
|
88
95
|
yield {
|
|
89
96
|
type: 'error',
|
|
90
97
|
message: `Git clone failed: ${gitEvent.message}`
|
|
@@ -122,6 +129,7 @@ export class ThreadManagerImpl {
|
|
|
122
129
|
// Create and checkout the new branch
|
|
123
130
|
for await (const gitEvent of this.gitManager.createAndCheckoutBranch(threadPath, branchName)) {
|
|
124
131
|
if (gitEvent.type === 'error') {
|
|
132
|
+
this.processingStateManager?.clearProcessing(threadId);
|
|
125
133
|
yield {
|
|
126
134
|
type: 'error',
|
|
127
135
|
message: `Failed to create branch: ${gitEvent.message}`
|
|
@@ -139,6 +147,16 @@ export class ThreadManagerImpl {
|
|
|
139
147
|
type: 'progress',
|
|
140
148
|
message: `Branch "${branchName}" created and checked out`
|
|
141
149
|
};
|
|
150
|
+
// Run setup script if project has one configured
|
|
151
|
+
if (project.setupScript) {
|
|
152
|
+
yield {
|
|
153
|
+
type: 'progress',
|
|
154
|
+
message: `Running setup script: ${project.setupScript}`
|
|
155
|
+
};
|
|
156
|
+
for await (const event of this.runSetupScript(threadPath, project.setupScript)) {
|
|
157
|
+
yield event;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
142
160
|
// Create thread metadata
|
|
143
161
|
const thread = {
|
|
144
162
|
id: threadId,
|
|
@@ -161,6 +179,7 @@ export class ThreadManagerImpl {
|
|
|
161
179
|
message: `Thread "${threadTitle}" created successfully`
|
|
162
180
|
};
|
|
163
181
|
// Yield complete event with thread data
|
|
182
|
+
this.processingStateManager?.clearProcessing(threadId);
|
|
164
183
|
yield {
|
|
165
184
|
type: 'complete',
|
|
166
185
|
message: 'Thread creation complete',
|
|
@@ -168,6 +187,9 @@ export class ThreadManagerImpl {
|
|
|
168
187
|
};
|
|
169
188
|
}
|
|
170
189
|
catch (error) {
|
|
190
|
+
if (typeof threadId !== 'undefined') {
|
|
191
|
+
this.processingStateManager?.clearProcessing(threadId);
|
|
192
|
+
}
|
|
171
193
|
yield {
|
|
172
194
|
type: 'error',
|
|
173
195
|
message: `Failed to create thread: ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -272,6 +294,64 @@ export class ThreadManagerImpl {
|
|
|
272
294
|
async getSelectedThreadId() {
|
|
273
295
|
return await this.metadataManager.getSelectedThread();
|
|
274
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* Runs a setup script in the given path and yields progress events
|
|
299
|
+
* @param threadPath - Path to run the command in
|
|
300
|
+
* @param commandLine - The command to execute
|
|
301
|
+
* @yields ThreadEvent progress events
|
|
302
|
+
*/
|
|
303
|
+
async *runSetupScript(threadPath, commandLine) {
|
|
304
|
+
const { spawn } = await import('child_process');
|
|
305
|
+
const child = spawn(commandLine, {
|
|
306
|
+
shell: true,
|
|
307
|
+
cwd: threadPath
|
|
308
|
+
});
|
|
309
|
+
const decoder = new TextDecoder();
|
|
310
|
+
let resolveNext = () => { };
|
|
311
|
+
const outputQueue = [];
|
|
312
|
+
const errorQueue = [];
|
|
313
|
+
child.stdout.on('data', (data) => {
|
|
314
|
+
outputQueue.push(decoder.decode(data));
|
|
315
|
+
resolveNext();
|
|
316
|
+
});
|
|
317
|
+
child.stderr.on('data', (data) => {
|
|
318
|
+
errorQueue.push(decoder.decode(data));
|
|
319
|
+
resolveNext();
|
|
320
|
+
});
|
|
321
|
+
let isDone = false;
|
|
322
|
+
child.on('close', (code) => {
|
|
323
|
+
if (code !== 0) {
|
|
324
|
+
errorQueue.push(`Process exited with code ${code}`);
|
|
325
|
+
}
|
|
326
|
+
isDone = true;
|
|
327
|
+
resolveNext();
|
|
328
|
+
});
|
|
329
|
+
child.on('error', (err) => {
|
|
330
|
+
errorQueue.push(err.message);
|
|
331
|
+
isDone = true;
|
|
332
|
+
resolveNext();
|
|
333
|
+
});
|
|
334
|
+
const waitForData = () => new Promise((resolve) => {
|
|
335
|
+
resolveNext = resolve;
|
|
336
|
+
});
|
|
337
|
+
while (!isDone || outputQueue.length > 0 || errorQueue.length > 0) {
|
|
338
|
+
if (outputQueue.length > 0) {
|
|
339
|
+
const text = outputQueue.shift();
|
|
340
|
+
for (const line of text.split('\n').filter(l => l.trim())) {
|
|
341
|
+
yield { type: 'progress', message: line };
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
else if (errorQueue.length > 0) {
|
|
345
|
+
const text = errorQueue.shift();
|
|
346
|
+
for (const line of text.split('\n').filter(l => l.trim())) {
|
|
347
|
+
yield { type: 'progress', message: line };
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
await waitForData();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
275
355
|
/**
|
|
276
356
|
* Generates a unique thread path
|
|
277
357
|
*
|
|
@@ -292,34 +372,23 @@ export class ThreadManagerImpl {
|
|
|
292
372
|
/**
|
|
293
373
|
* Generates a thread title if not provided
|
|
294
374
|
*
|
|
295
|
-
* Format:
|
|
375
|
+
* Format: thread-{n} where n starts at 1 and increments for each new thread
|
|
296
376
|
*
|
|
297
|
-
* @param existingTitles - Set of titles already in use
|
|
298
|
-
* @
|
|
299
|
-
* @param projectName - Optional project name to include in title
|
|
300
|
-
* @returns The generated thread title
|
|
377
|
+
* @param existingTitles - Set of titles already in use for this project
|
|
378
|
+
* @returns The generated thread title (e.g. thread-1, thread-2, thread-3)
|
|
301
379
|
*/
|
|
302
|
-
generateThreadTitle(existingTitles
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
'quarry', 'reef', 'strand', 'trail', 'vale', 'wharf', 'yard', 'zephyr'
|
|
312
|
-
];
|
|
313
|
-
const prefix = projectName ? `${projectName} Thread ` : 'Thread ';
|
|
314
|
-
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
315
|
-
const word1 = wordsA[Math.floor(Math.random() * wordsA.length)];
|
|
316
|
-
const word2 = wordsB[Math.floor(Math.random() * wordsB.length)];
|
|
317
|
-
const candidate = `${prefix}${word1}-${word2}`;
|
|
318
|
-
if (!existingTitles.has(candidate)) {
|
|
319
|
-
return candidate;
|
|
380
|
+
generateThreadTitle(existingTitles) {
|
|
381
|
+
const threadNumRegex = /^thread-(\d+)$/i;
|
|
382
|
+
let maxNum = 0;
|
|
383
|
+
for (const title of existingTitles) {
|
|
384
|
+
const match = title.match(threadNumRegex);
|
|
385
|
+
if (match) {
|
|
386
|
+
const n = parseInt(match[1], 10);
|
|
387
|
+
if (n > maxNum)
|
|
388
|
+
maxNum = n;
|
|
320
389
|
}
|
|
321
390
|
}
|
|
322
|
-
return
|
|
391
|
+
return `Thread ${maxNum + 1}`;
|
|
323
392
|
}
|
|
324
393
|
/**
|
|
325
394
|
* Lists all files in a thread's directory (tracked and untracked, but not ignored)
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized path configuration for Tarsk
|
|
3
|
+
*
|
|
4
|
+
* Application data is stored in ~/Library/Application Support/Tarsk/data/
|
|
5
|
+
* This includes projects.json, threads.json, state.json, and conversations.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* The root application support directory for Tarsk.
|
|
9
|
+
* On macOS: ~/Library/Application Support/Tarsk
|
|
10
|
+
*/
|
|
11
|
+
export declare const APP_SUPPORT_DIR: string;
|
|
12
|
+
/**
|
|
13
|
+
* The data directory where metadata files are stored.
|
|
14
|
+
* ~/Library/Application Support/Tarsk/data
|
|
15
|
+
*/
|
|
16
|
+
export declare const DATA_DIR: string;
|
|
17
|
+
/**
|
|
18
|
+
* Get the data directory path for storing metadata.
|
|
19
|
+
* @returns The absolute path to ~/Library/Application Support/Tarsk/data
|
|
20
|
+
*/
|
|
21
|
+
export declare function getDataDir(): string;
|
|
22
|
+
//# sourceMappingURL=paths.d.ts.map
|