@junctionpanel/server 0.1.27 → 0.1.29
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/server/client/daemon-client.d.ts +39 -4
- package/dist/server/client/daemon-client.d.ts.map +1 -1
- package/dist/server/client/daemon-client.js +83 -3
- package/dist/server/client/daemon-client.js.map +1 -1
- package/dist/server/server/agent/agent-sdk-types.d.ts +1 -0
- package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
- package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -1
- package/dist/server/server/agent/providers/codex-app-server-agent.js +1 -39
- package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
- package/dist/server/server/agent/providers/gemini-agent.d.ts +4 -1
- package/dist/server/server/agent/providers/gemini-agent.d.ts.map +1 -1
- package/dist/server/server/agent/providers/gemini-agent.js +36 -8
- package/dist/server/server/agent/providers/gemini-agent.js.map +1 -1
- package/dist/server/server/agent/providers/image-attachments.d.ts +8 -0
- package/dist/server/server/agent/providers/image-attachments.d.ts.map +1 -0
- package/dist/server/server/agent/providers/image-attachments.js +47 -0
- package/dist/server/server/agent/providers/image-attachments.js.map +1 -0
- package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +3 -0
- package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -1
- package/dist/server/server/daemon-doctor.d.ts +39 -0
- package/dist/server/server/daemon-doctor.d.ts.map +1 -0
- package/dist/server/server/daemon-doctor.js +260 -0
- package/dist/server/server/daemon-doctor.js.map +1 -0
- package/dist/server/server/daemon-provider-settings.d.ts +42 -0
- package/dist/server/server/daemon-provider-settings.d.ts.map +1 -0
- package/dist/server/server/daemon-provider-settings.js +207 -0
- package/dist/server/server/daemon-provider-settings.js.map +1 -0
- package/dist/server/server/file-explorer/service.d.ts +4 -2
- package/dist/server/server/file-explorer/service.d.ts.map +1 -1
- package/dist/server/server/file-explorer/service.js +104 -2
- package/dist/server/server/file-explorer/service.js.map +1 -1
- package/dist/server/server/persisted-config.d.ts +24 -24
- package/dist/server/server/session.d.ts +10 -1
- package/dist/server/server/session.d.ts.map +1 -1
- package/dist/server/server/session.js +421 -60
- package/dist/server/server/session.js.map +1 -1
- package/dist/server/server/worktree-bootstrap.d.ts +1 -0
- package/dist/server/server/worktree-bootstrap.d.ts.map +1 -1
- package/dist/server/server/worktree-bootstrap.js +4 -0
- package/dist/server/server/worktree-bootstrap.js.map +1 -1
- package/dist/server/shared/messages.d.ts +3673 -22
- package/dist/server/shared/messages.d.ts.map +1 -1
- package/dist/server/shared/messages.js +151 -0
- package/dist/server/shared/messages.js.map +1 -1
- package/dist/server/utils/checkout-git.d.ts +23 -4
- package/dist/server/utils/checkout-git.d.ts.map +1 -1
- package/dist/server/utils/checkout-git.js +298 -79
- package/dist/server/utils/checkout-git.js.map +1 -1
- package/dist/server/utils/directory-suggestions.d.ts +4 -0
- package/dist/server/utils/directory-suggestions.d.ts.map +1 -1
- package/dist/server/utils/directory-suggestions.js +83 -5
- package/dist/server/utils/directory-suggestions.js.map +1 -1
- package/dist/server/utils/workspace-ref-files.d.ts +31 -0
- package/dist/server/utils/workspace-ref-files.d.ts.map +1 -0
- package/dist/server/utils/workspace-ref-files.js +207 -0
- package/dist/server/utils/workspace-ref-files.js.map +1 -0
- package/dist/server/utils/worktree.d.ts +6 -3
- package/dist/server/utils/worktree.d.ts.map +1 -1
- package/dist/server/utils/worktree.js +46 -45
- package/dist/server/utils/worktree.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
-
import { watch } from 'node:fs';
|
|
2
|
+
import { existsSync, watch } from 'node:fs';
|
|
3
3
|
import { exec } from 'child_process';
|
|
4
4
|
import { promisify } from 'util';
|
|
5
|
-
import { resolve, sep, basename, dirname } from 'path';
|
|
6
|
-
import { homedir } from 'node:os';
|
|
5
|
+
import { resolve, sep, basename, dirname, parse as parsePath } from 'path';
|
|
6
|
+
import { homedir, hostname } from 'node:os';
|
|
7
7
|
import { z } from 'zod';
|
|
8
8
|
import { serializeAgentStreamEvent, } from './messages.js';
|
|
9
9
|
import { BinaryMuxChannel, TerminalBinaryFlags, TerminalBinaryMessageType, } from '../shared/binary-mux.js';
|
|
10
10
|
import { buildConfigOverrides, buildSessionConfig, extractTimestamps, extractTimelineSnapshot, } from './persistence-hooks.js';
|
|
11
11
|
import { experimental_createMCPClient } from 'ai';
|
|
12
12
|
import { buildProviderRegistry } from './agent/provider-registry.js';
|
|
13
|
+
import { applyProviderEnv, } from './agent/provider-launch-config.js';
|
|
13
14
|
import { scheduleAgentMetadataGeneration } from './agent/agent-metadata-generator.js';
|
|
14
15
|
import { resolveEffectiveThinkingOptionId, toAgentPayload } from './agent/agent-projections.js';
|
|
15
16
|
import { appendTimelineItemIfAgentKnown, emitLiveTimelineItemIfAgentKnown, } from './agent/timeline-append.js';
|
|
@@ -20,14 +21,18 @@ import { listDirectoryEntries, readExplorerFile, getDownloadableFileInfo, } from
|
|
|
20
21
|
import { slugify, validateBranchSlug, listJunctionWorktrees, deleteJunctionWorktree, isJunctionOwnedWorktreeCwd, resolveJunctionWorktreeRootForCwd, createInRepoWorktree, restoreInRepoWorktree, } from '../utils/worktree.js';
|
|
21
22
|
import { readJunctionWorktreeMetadata } from '../utils/worktree-metadata.js';
|
|
22
23
|
import { runAsyncWorktreeBootstrap } from './worktree-bootstrap.js';
|
|
23
|
-
import { getCheckoutDiff, getCheckoutStatus, getCheckoutStatusLite, listBranchSuggestions, NotGitRepoError, MergeConflictError, MergeFromBaseConflictError, commitChanges, mergeToBase, mergeFromBase, pushCurrentBranch, createPullRequest, getPullRequestFailureLogs, getPullRequestStatus, mergePullRequest, resolveBaseRef, } from '../utils/checkout-git.js';
|
|
24
|
+
import { getCheckoutDiff, getCheckoutStatus, getCheckoutStatusLite, listBranchSuggestions, NotGitRepoError, MergeConflictError, MergeFromBaseConflictError, commitChanges, mergeToBase, mergeFromBase, pushCurrentBranch, createPullRequest, getPullRequestFailureLogs, getPullRequestStatus, listGitRemotes, mergePullRequest, resolveBaseRef, } from '../utils/checkout-git.js';
|
|
24
25
|
import { getProjectIcon } from '../utils/project-icon.js';
|
|
25
26
|
import { expandTilde } from '../utils/path.js';
|
|
26
|
-
import { searchHomeDirectories, searchWorkspaceEntries, searchGitRepositories, checkIsGitRepo } from '../utils/directory-suggestions.js';
|
|
27
|
+
import { searchHomeDirectories, searchWorkspaceEntries, searchWorkspaceEntriesAtGitRef, searchGitRepositories, checkIsGitRepo, } from '../utils/directory-suggestions.js';
|
|
27
28
|
import { cloneRepository } from '../utils/git-clone.js';
|
|
28
29
|
import { initRepository } from '../utils/git-init.js';
|
|
29
30
|
import { resolveClientMessageId } from './client-message-id.js';
|
|
30
31
|
import { deriveProjectGroupingKey, deriveProjectGroupingName } from '../shared/project-grouping.js';
|
|
32
|
+
import { resolveDaemonVersion } from './daemon-version.js';
|
|
33
|
+
import { runDaemonDoctor } from './daemon-doctor.js';
|
|
34
|
+
import { MANAGED_DAEMON_PROVIDERS, autoRouteProviderExecutable, loadDaemonProviderSettings, saveDaemonProviderExecutablePath, } from './daemon-provider-settings.js';
|
|
35
|
+
import { loadPersistedConfig } from './persisted-config.js';
|
|
31
36
|
const execAsync = promisify(exec);
|
|
32
37
|
const READ_ONLY_GIT_ENV = {
|
|
33
38
|
...process.env,
|
|
@@ -50,6 +55,7 @@ class SessionRequestError extends Error {
|
|
|
50
55
|
}
|
|
51
56
|
}
|
|
52
57
|
const SAFE_GIT_REF_PATTERN = /^[A-Za-z0-9._\/-]+$/;
|
|
58
|
+
const SAFE_GIT_REMOTE_NAME_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
53
59
|
function coerceAgentProvider(logger, value, agentId) {
|
|
54
60
|
if (isValidAgentProvider(value)) {
|
|
55
61
|
return value;
|
|
@@ -800,9 +806,15 @@ export class Session {
|
|
|
800
806
|
case 'branch_suggestions_request':
|
|
801
807
|
await this.handleBranchSuggestionsRequest(msg);
|
|
802
808
|
break;
|
|
809
|
+
case 'git_remotes_request':
|
|
810
|
+
await this.handleGitRemotesRequest(msg);
|
|
811
|
+
break;
|
|
803
812
|
case 'directory_suggestions_request':
|
|
804
813
|
await this.handleDirectorySuggestionsRequest(msg);
|
|
805
814
|
break;
|
|
815
|
+
case 'workspace_file_suggestions_request':
|
|
816
|
+
await this.handleWorkspaceFileSuggestionsRequest(msg);
|
|
817
|
+
break;
|
|
806
818
|
case 'git_clone_request':
|
|
807
819
|
await this.handleGitCloneRequest(msg);
|
|
808
820
|
break;
|
|
@@ -863,6 +875,18 @@ export class Session {
|
|
|
863
875
|
case 'list_available_providers_request':
|
|
864
876
|
await this.handleListAvailableProvidersRequest(msg);
|
|
865
877
|
break;
|
|
878
|
+
case 'run_daemon_doctor_request':
|
|
879
|
+
await this.handleRunDaemonDoctorRequest(msg);
|
|
880
|
+
break;
|
|
881
|
+
case 'get_daemon_provider_settings_request':
|
|
882
|
+
await this.handleGetDaemonProviderSettingsRequest(msg);
|
|
883
|
+
break;
|
|
884
|
+
case 'update_daemon_provider_settings_request':
|
|
885
|
+
await this.handleUpdateDaemonProviderSettingsRequest(msg);
|
|
886
|
+
break;
|
|
887
|
+
case 'auto_route_provider_request':
|
|
888
|
+
await this.handleAutoRouteProviderRequest(msg);
|
|
889
|
+
break;
|
|
866
890
|
case 'clear_agent_attention':
|
|
867
891
|
await this.handleClearAgentAttention(msg.agentId);
|
|
868
892
|
break;
|
|
@@ -1150,19 +1174,43 @@ export class Session {
|
|
|
1150
1174
|
if (!record) {
|
|
1151
1175
|
throw new Error(`Agent not found: ${agentId}`);
|
|
1152
1176
|
}
|
|
1177
|
+
const allRecords = await this.agentStorage.list();
|
|
1178
|
+
const siblingRecords = record.archivedWorktree?.cleanupState === 'deleted' && record.archivedWorktree
|
|
1179
|
+
? allRecords.filter((candidate) => {
|
|
1180
|
+
if (candidate.id === record.id || !candidate.archivedAt) {
|
|
1181
|
+
return false;
|
|
1182
|
+
}
|
|
1183
|
+
return candidate.cwd === record.cwd;
|
|
1184
|
+
})
|
|
1185
|
+
: [];
|
|
1153
1186
|
let nextRecord = {
|
|
1154
1187
|
...record,
|
|
1155
1188
|
archivedAt: null,
|
|
1156
1189
|
};
|
|
1157
1190
|
let restoredWorktree = null;
|
|
1158
1191
|
if (record.archivedWorktree?.cleanupState === 'deleted') {
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1192
|
+
try {
|
|
1193
|
+
restoredWorktree = await restoreInRepoWorktree({
|
|
1194
|
+
repoRoot: record.archivedWorktree.repoRoot,
|
|
1195
|
+
baseBranch: record.archivedWorktree.baseBranch,
|
|
1196
|
+
branchName: record.archivedWorktree.branchName,
|
|
1197
|
+
worktreeSlug: record.archivedWorktree.worktreeSlug,
|
|
1198
|
+
runSetup: false,
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
catch (error) {
|
|
1202
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1203
|
+
const restoredPath = record.archivedWorktree.originalCwd;
|
|
1204
|
+
if (!message.includes('Worktree path already exists') || !existsSync(restoredPath)) {
|
|
1205
|
+
throw error;
|
|
1206
|
+
}
|
|
1207
|
+
restoredWorktree = {
|
|
1208
|
+
branchName: record.archivedWorktree.branchName,
|
|
1209
|
+
worktreePath: restoredPath,
|
|
1210
|
+
baseBranch: record.archivedWorktree.baseBranch,
|
|
1211
|
+
workspaceName: basename(restoredPath),
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1166
1214
|
nextRecord = {
|
|
1167
1215
|
...nextRecord,
|
|
1168
1216
|
cwd: restoredWorktree.worktreePath,
|
|
@@ -1174,13 +1222,34 @@ export class Session {
|
|
|
1174
1222
|
},
|
|
1175
1223
|
};
|
|
1176
1224
|
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1225
|
+
const recordsToRestore = [record, ...siblingRecords].map((candidate) => {
|
|
1226
|
+
const nextArchivedWorktree = restoredWorktree && candidate.archivedWorktree
|
|
1227
|
+
? {
|
|
1228
|
+
...candidate.archivedWorktree,
|
|
1229
|
+
originalCwd: restoredWorktree.worktreePath,
|
|
1230
|
+
cleanupState: 'active',
|
|
1231
|
+
cleanedUpAt: null,
|
|
1232
|
+
}
|
|
1233
|
+
: candidate.archivedWorktree;
|
|
1234
|
+
return {
|
|
1235
|
+
...candidate,
|
|
1236
|
+
archivedAt: null,
|
|
1237
|
+
cwd: restoredWorktree ? restoredWorktree.worktreePath : candidate.cwd,
|
|
1238
|
+
archivedWorktree: nextArchivedWorktree,
|
|
1239
|
+
};
|
|
1240
|
+
});
|
|
1241
|
+
for (const restoredRecord of recordsToRestore) {
|
|
1242
|
+
await this.agentStorage.upsert(restoredRecord);
|
|
1243
|
+
const liveAgent = this.agentManager.getAgent(restoredRecord.id);
|
|
1244
|
+
if (liveAgent) {
|
|
1245
|
+
this.agentManager.notifyAgentState(restoredRecord.id);
|
|
1246
|
+
}
|
|
1247
|
+
else {
|
|
1248
|
+
await this.forwardStoredAgentRecordUpdate(restoredRecord);
|
|
1249
|
+
}
|
|
1250
|
+
if (restoredRecord.id === agentId) {
|
|
1251
|
+
nextRecord = restoredRecord;
|
|
1252
|
+
}
|
|
1184
1253
|
}
|
|
1185
1254
|
this.emit({
|
|
1186
1255
|
type: 'agent_unarchived',
|
|
@@ -1363,7 +1432,7 @@ export class Session {
|
|
|
1363
1432
|
* Handle create agent request
|
|
1364
1433
|
*/
|
|
1365
1434
|
async handleCreateAgentRequest(msg) {
|
|
1366
|
-
const { config, worktreeName, requestId, initialPrompt, clientMessageId, outputSchema, git, bootstrapSetupOverride, images, labels, } = msg;
|
|
1435
|
+
const { config, worktreeName, requestId, initialPrompt, clientMessageId, outputSchema, git, bootstrapSetupOverride, generalPreferencesApplied, images, labels, } = msg;
|
|
1367
1436
|
this.sessionLogger.info({ cwd: config.cwd, provider: config.provider, worktreeName }, `Creating agent in ${config.cwd} (${config.provider})${worktreeName ? ` with worktree ${worktreeName}` : ''}`);
|
|
1368
1437
|
try {
|
|
1369
1438
|
const { sessionConfig, worktreeConfig, autoWorkspaceName } = await this.buildAgentSessionConfig(config, git, worktreeName, labels);
|
|
@@ -1387,20 +1456,23 @@ export class Session {
|
|
|
1387
1456
|
},
|
|
1388
1457
|
});
|
|
1389
1458
|
}
|
|
1390
|
-
const trimmedPrompt = initialPrompt?.trim();
|
|
1459
|
+
const trimmedPrompt = initialPrompt?.trim() ?? '';
|
|
1460
|
+
const hasInitialMessage = trimmedPrompt.length > 0 || Boolean(images?.length);
|
|
1391
1461
|
const runInitialPrompt = async () => {
|
|
1392
|
-
if (!
|
|
1462
|
+
if (!hasInitialMessage) {
|
|
1393
1463
|
return;
|
|
1394
1464
|
}
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1465
|
+
if (trimmedPrompt.length > 0) {
|
|
1466
|
+
scheduleAgentMetadataGeneration({
|
|
1467
|
+
agentManager: this.agentManager,
|
|
1468
|
+
agentId: snapshot.id,
|
|
1469
|
+
cwd: snapshot.cwd,
|
|
1470
|
+
initialPrompt: trimmedPrompt,
|
|
1471
|
+
explicitTitle: snapshot.config.title,
|
|
1472
|
+
junctionHome: this.junctionHome,
|
|
1473
|
+
logger: this.sessionLogger,
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1404
1476
|
await this.handleSendAgentMessage(snapshot.id, trimmedPrompt, resolveClientMessageId(clientMessageId), images, outputSchema ? { outputSchema } : undefined);
|
|
1405
1477
|
};
|
|
1406
1478
|
const handleInitialPromptError = (promptError) => {
|
|
@@ -1415,7 +1487,7 @@ export class Session {
|
|
|
1415
1487
|
},
|
|
1416
1488
|
});
|
|
1417
1489
|
};
|
|
1418
|
-
if (
|
|
1490
|
+
if (hasInitialMessage && !worktreeConfig) {
|
|
1419
1491
|
void runInitialPrompt().catch(handleInitialPromptError);
|
|
1420
1492
|
}
|
|
1421
1493
|
if (worktreeConfig) {
|
|
@@ -1423,6 +1495,7 @@ export class Session {
|
|
|
1423
1495
|
agentId: snapshot.id,
|
|
1424
1496
|
worktree: worktreeConfig,
|
|
1425
1497
|
setupOverride: bootstrapSetupOverride,
|
|
1498
|
+
generalPreferencesApplied,
|
|
1426
1499
|
terminalManager: this.terminalManager,
|
|
1427
1500
|
appendTimelineItem: (item) => appendTimelineItemIfAgentKnown({
|
|
1428
1501
|
agentManager: this.agentManager,
|
|
@@ -1435,7 +1508,7 @@ export class Session {
|
|
|
1435
1508
|
item,
|
|
1436
1509
|
}),
|
|
1437
1510
|
onSetupSettled: async (result) => {
|
|
1438
|
-
if (!
|
|
1511
|
+
if (!hasInitialMessage || result.setupStatus === 'failed') {
|
|
1439
1512
|
return;
|
|
1440
1513
|
}
|
|
1441
1514
|
await runInitialPrompt().catch(handleInitialPromptError);
|
|
@@ -1587,7 +1660,7 @@ export class Session {
|
|
|
1587
1660
|
this.handleAgentRunError(agentId, error, 'Failed to cancel running agent on request');
|
|
1588
1661
|
}
|
|
1589
1662
|
}
|
|
1590
|
-
async buildAgentSessionConfig(config, gitOptions, legacyWorktreeName,
|
|
1663
|
+
async buildAgentSessionConfig(config, gitOptions, legacyWorktreeName, labels) {
|
|
1591
1664
|
const cwd = expandTilde(config.cwd);
|
|
1592
1665
|
const normalized = this.normalizeGitOptions(gitOptions, legacyWorktreeName);
|
|
1593
1666
|
let repoRoot;
|
|
@@ -1608,7 +1681,26 @@ export class Session {
|
|
|
1608
1681
|
catch {
|
|
1609
1682
|
throw new Error('Selected project must be a git repository. Junction always creates a new worktree in .junction/.');
|
|
1610
1683
|
}
|
|
1611
|
-
|
|
1684
|
+
if (!normalized) {
|
|
1685
|
+
const ownership = await isJunctionOwnedWorktreeCwd(cwd, {
|
|
1686
|
+
junctionHome: this.junctionHome,
|
|
1687
|
+
});
|
|
1688
|
+
if (ownership.allowed) {
|
|
1689
|
+
const resolvedWorktree = await resolveJunctionWorktreeRootForCwd(cwd, {
|
|
1690
|
+
junctionHome: this.junctionHome,
|
|
1691
|
+
});
|
|
1692
|
+
const workspaceName = labels?.['junction:workspace']
|
|
1693
|
+
?? (resolvedWorktree ? basename(resolvedWorktree.worktreePath) : basename(cwd));
|
|
1694
|
+
return {
|
|
1695
|
+
sessionConfig: {
|
|
1696
|
+
...config,
|
|
1697
|
+
cwd,
|
|
1698
|
+
},
|
|
1699
|
+
autoWorkspaceName: workspaceName,
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
const baseBranch = normalized?.baseBranch ?? (await resolveBaseRef(repoRoot, { remoteName: normalized?.remoteName }));
|
|
1612
1704
|
if (!baseBranch) {
|
|
1613
1705
|
throw new Error('Unable to determine a base branch for worktree creation');
|
|
1614
1706
|
}
|
|
@@ -1616,6 +1708,7 @@ export class Session {
|
|
|
1616
1708
|
const createdWorktree = await createInRepoWorktree({
|
|
1617
1709
|
repoRoot,
|
|
1618
1710
|
baseBranch,
|
|
1711
|
+
remoteName: normalized?.remoteName,
|
|
1619
1712
|
runSetup: false,
|
|
1620
1713
|
});
|
|
1621
1714
|
return {
|
|
@@ -1684,6 +1777,153 @@ export class Session {
|
|
|
1684
1777
|
});
|
|
1685
1778
|
}
|
|
1686
1779
|
}
|
|
1780
|
+
resolveDaemonVersionSafe() {
|
|
1781
|
+
try {
|
|
1782
|
+
return resolveDaemonVersion(import.meta.url);
|
|
1783
|
+
}
|
|
1784
|
+
catch {
|
|
1785
|
+
return null;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
toDaemonMetadata(input) {
|
|
1789
|
+
const homeDir = input?.homeDir ?? homedir();
|
|
1790
|
+
const rootDir = input?.rootDir ?? parsePath(homeDir).root;
|
|
1791
|
+
return {
|
|
1792
|
+
hostname: hostname(),
|
|
1793
|
+
version: this.resolveDaemonVersionSafe(),
|
|
1794
|
+
platform: process.platform,
|
|
1795
|
+
homeDir,
|
|
1796
|
+
rootDir,
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
async handleRunDaemonDoctorRequest(msg) {
|
|
1800
|
+
try {
|
|
1801
|
+
const result = await runDaemonDoctor(this.junctionHome, this.sessionLogger);
|
|
1802
|
+
this.emit({
|
|
1803
|
+
type: 'run_daemon_doctor_response',
|
|
1804
|
+
payload: {
|
|
1805
|
+
daemon: result.daemon,
|
|
1806
|
+
summary: result.summary,
|
|
1807
|
+
checks: result.checks,
|
|
1808
|
+
ranAt: result.ranAt,
|
|
1809
|
+
error: null,
|
|
1810
|
+
requestId: msg.requestId,
|
|
1811
|
+
},
|
|
1812
|
+
});
|
|
1813
|
+
}
|
|
1814
|
+
catch (error) {
|
|
1815
|
+
this.sessionLogger.error({ err: error }, 'Failed to run daemon doctor');
|
|
1816
|
+
const snapshot = loadDaemonProviderSettings(this.junctionHome);
|
|
1817
|
+
this.emit({
|
|
1818
|
+
type: 'run_daemon_doctor_response',
|
|
1819
|
+
payload: {
|
|
1820
|
+
daemon: this.toDaemonMetadata(snapshot),
|
|
1821
|
+
summary: 'fail',
|
|
1822
|
+
checks: [],
|
|
1823
|
+
ranAt: new Date().toISOString(),
|
|
1824
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1825
|
+
requestId: msg.requestId,
|
|
1826
|
+
},
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
async handleGetDaemonProviderSettingsRequest(msg) {
|
|
1831
|
+
try {
|
|
1832
|
+
const snapshot = loadDaemonProviderSettings(this.junctionHome);
|
|
1833
|
+
this.emit({
|
|
1834
|
+
type: 'get_daemon_provider_settings_response',
|
|
1835
|
+
payload: {
|
|
1836
|
+
daemon: this.toDaemonMetadata(snapshot),
|
|
1837
|
+
providers: MANAGED_DAEMON_PROVIDERS.map((provider) => snapshot.providers[provider]),
|
|
1838
|
+
error: null,
|
|
1839
|
+
requestId: msg.requestId,
|
|
1840
|
+
},
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
catch (error) {
|
|
1844
|
+
this.sessionLogger.error({ err: error }, 'Failed to load daemon provider settings');
|
|
1845
|
+
this.emit({
|
|
1846
|
+
type: 'get_daemon_provider_settings_response',
|
|
1847
|
+
payload: {
|
|
1848
|
+
daemon: this.toDaemonMetadata(),
|
|
1849
|
+
providers: [],
|
|
1850
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1851
|
+
requestId: msg.requestId,
|
|
1852
|
+
},
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
async handleUpdateDaemonProviderSettingsRequest(msg) {
|
|
1857
|
+
try {
|
|
1858
|
+
const snapshot = saveDaemonProviderExecutablePath({
|
|
1859
|
+
junctionHome: this.junctionHome,
|
|
1860
|
+
provider: msg.provider,
|
|
1861
|
+
executablePath: msg.executablePath,
|
|
1862
|
+
});
|
|
1863
|
+
this.emit({
|
|
1864
|
+
type: 'update_daemon_provider_settings_response',
|
|
1865
|
+
payload: {
|
|
1866
|
+
daemon: this.toDaemonMetadata(snapshot),
|
|
1867
|
+
provider: snapshot.providers[msg.provider],
|
|
1868
|
+
error: null,
|
|
1869
|
+
requestId: msg.requestId,
|
|
1870
|
+
},
|
|
1871
|
+
});
|
|
1872
|
+
await this.handleRestartServerRequest(msg.requestId, 'settings_update');
|
|
1873
|
+
}
|
|
1874
|
+
catch (error) {
|
|
1875
|
+
this.sessionLogger.error({ err: error, provider: msg.provider }, 'Failed to update daemon provider settings');
|
|
1876
|
+
this.emit({
|
|
1877
|
+
type: 'update_daemon_provider_settings_response',
|
|
1878
|
+
payload: {
|
|
1879
|
+
daemon: this.toDaemonMetadata(),
|
|
1880
|
+
provider: null,
|
|
1881
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1882
|
+
requestId: msg.requestId,
|
|
1883
|
+
},
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
async handleAutoRouteProviderRequest(msg) {
|
|
1888
|
+
try {
|
|
1889
|
+
const config = loadPersistedConfig(this.junctionHome);
|
|
1890
|
+
const env = applyProviderEnv(process.env, config.agents?.providers?.[msg.provider]);
|
|
1891
|
+
const executablePath = autoRouteProviderExecutable(msg.provider, {
|
|
1892
|
+
env,
|
|
1893
|
+
platform: process.platform,
|
|
1894
|
+
});
|
|
1895
|
+
if (!executablePath) {
|
|
1896
|
+
throw new SessionRequestError('provider_not_found', `Could not automatically locate ${msg.provider} on this daemon.`);
|
|
1897
|
+
}
|
|
1898
|
+
const snapshot = saveDaemonProviderExecutablePath({
|
|
1899
|
+
junctionHome: this.junctionHome,
|
|
1900
|
+
provider: msg.provider,
|
|
1901
|
+
executablePath,
|
|
1902
|
+
});
|
|
1903
|
+
this.emit({
|
|
1904
|
+
type: 'auto_route_provider_response',
|
|
1905
|
+
payload: {
|
|
1906
|
+
daemon: this.toDaemonMetadata(snapshot),
|
|
1907
|
+
provider: snapshot.providers[msg.provider],
|
|
1908
|
+
error: null,
|
|
1909
|
+
requestId: msg.requestId,
|
|
1910
|
+
},
|
|
1911
|
+
});
|
|
1912
|
+
await this.handleRestartServerRequest(msg.requestId, 'settings_update');
|
|
1913
|
+
}
|
|
1914
|
+
catch (error) {
|
|
1915
|
+
this.sessionLogger.error({ err: error, provider: msg.provider }, 'Failed to auto-route provider executable');
|
|
1916
|
+
this.emit({
|
|
1917
|
+
type: 'auto_route_provider_response',
|
|
1918
|
+
payload: {
|
|
1919
|
+
daemon: this.toDaemonMetadata(),
|
|
1920
|
+
provider: null,
|
|
1921
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1922
|
+
requestId: msg.requestId,
|
|
1923
|
+
},
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1687
1927
|
normalizeGitOptions(gitOptions, legacyWorktreeName) {
|
|
1688
1928
|
const fallbackOptions = legacyWorktreeName
|
|
1689
1929
|
? {
|
|
@@ -1698,6 +1938,7 @@ export class Session {
|
|
|
1698
1938
|
return null;
|
|
1699
1939
|
}
|
|
1700
1940
|
const baseBranch = merged.baseBranch?.trim() || undefined;
|
|
1941
|
+
const remoteName = merged.remoteName?.trim() || undefined;
|
|
1701
1942
|
const createWorktree = Boolean(merged.createWorktree);
|
|
1702
1943
|
const createNewBranch = Boolean(merged.createNewBranch);
|
|
1703
1944
|
const normalizedBranchName = merged.newBranchName ? slugify(merged.newBranchName) : undefined;
|
|
@@ -1710,6 +1951,9 @@ export class Session {
|
|
|
1710
1951
|
if (baseBranch) {
|
|
1711
1952
|
this.assertSafeGitRef(baseBranch, 'base branch');
|
|
1712
1953
|
}
|
|
1954
|
+
if (remoteName) {
|
|
1955
|
+
this.assertSafeRemoteName(remoteName);
|
|
1956
|
+
}
|
|
1713
1957
|
if (createWorktree && !baseBranch) {
|
|
1714
1958
|
throw new Error('Base branch is required when creating a worktree');
|
|
1715
1959
|
}
|
|
@@ -1733,6 +1977,7 @@ export class Session {
|
|
|
1733
1977
|
}
|
|
1734
1978
|
return {
|
|
1735
1979
|
baseBranch,
|
|
1980
|
+
remoteName,
|
|
1736
1981
|
createNewBranch,
|
|
1737
1982
|
newBranchName: normalizedBranchName,
|
|
1738
1983
|
createWorktree,
|
|
@@ -1744,6 +1989,11 @@ export class Session {
|
|
|
1744
1989
|
throw new Error(`Invalid ${label}: ${ref}`);
|
|
1745
1990
|
}
|
|
1746
1991
|
}
|
|
1992
|
+
assertSafeRemoteName(remoteName) {
|
|
1993
|
+
if (!SAFE_GIT_REMOTE_NAME_PATTERN.test(remoteName)) {
|
|
1994
|
+
throw new Error(`Invalid remote name: ${remoteName}`);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1747
1997
|
toCheckoutError(error) {
|
|
1748
1998
|
if (error instanceof NotGitRepoError) {
|
|
1749
1999
|
return { code: 'NOT_GIT_REPO', message: error.message };
|
|
@@ -2201,6 +2451,10 @@ export class Session {
|
|
|
2201
2451
|
const { cwd, branchName, requestId } = msg;
|
|
2202
2452
|
try {
|
|
2203
2453
|
const resolvedCwd = expandTilde(cwd);
|
|
2454
|
+
const remoteName = msg.remoteName?.trim() || undefined;
|
|
2455
|
+
if (remoteName) {
|
|
2456
|
+
this.assertSafeRemoteName(remoteName);
|
|
2457
|
+
}
|
|
2204
2458
|
// Try local branch first
|
|
2205
2459
|
try {
|
|
2206
2460
|
await execAsync(`git rev-parse --verify ${branchName}`, {
|
|
@@ -2222,26 +2476,45 @@ export class Session {
|
|
|
2222
2476
|
catch {
|
|
2223
2477
|
// Local branch doesn't exist, try remote
|
|
2224
2478
|
}
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2479
|
+
const { stdout: remoteStdout } = await execAsync('git remote', {
|
|
2480
|
+
cwd: resolvedCwd,
|
|
2481
|
+
env: READ_ONLY_GIT_ENV,
|
|
2482
|
+
});
|
|
2483
|
+
const configuredRemotes = remoteStdout
|
|
2484
|
+
.split('\n')
|
|
2485
|
+
.map((line) => line.trim())
|
|
2486
|
+
.filter((line) => line.length > 0)
|
|
2487
|
+
.sort((left, right) => left.localeCompare(right));
|
|
2488
|
+
const remoteCandidates = remoteName
|
|
2489
|
+
? [remoteName, 'origin', ...configuredRemotes]
|
|
2490
|
+
: ['origin', ...configuredRemotes];
|
|
2491
|
+
const seen = new Set();
|
|
2492
|
+
for (const candidateRemote of remoteCandidates) {
|
|
2493
|
+
const trimmedRemote = candidateRemote.trim();
|
|
2494
|
+
if (!trimmedRemote || seen.has(trimmedRemote)) {
|
|
2495
|
+
continue;
|
|
2496
|
+
}
|
|
2497
|
+
seen.add(trimmedRemote);
|
|
2498
|
+
try {
|
|
2499
|
+
await execAsync(`git rev-parse --verify ${trimmedRemote}/${branchName}`, {
|
|
2500
|
+
cwd: resolvedCwd,
|
|
2501
|
+
env: READ_ONLY_GIT_ENV,
|
|
2502
|
+
});
|
|
2503
|
+
this.emit({
|
|
2504
|
+
type: 'validate_branch_response',
|
|
2505
|
+
payload: {
|
|
2506
|
+
exists: true,
|
|
2507
|
+
resolvedRef: `${trimmedRemote}/${branchName}`,
|
|
2508
|
+
isRemote: true,
|
|
2509
|
+
error: null,
|
|
2510
|
+
requestId,
|
|
2511
|
+
},
|
|
2512
|
+
});
|
|
2513
|
+
return;
|
|
2514
|
+
}
|
|
2515
|
+
catch {
|
|
2516
|
+
// try next remote
|
|
2517
|
+
}
|
|
2245
2518
|
}
|
|
2246
2519
|
// Branch not found anywhere
|
|
2247
2520
|
this.emit({
|
|
@@ -2272,7 +2545,11 @@ export class Session {
|
|
|
2272
2545
|
const { cwd, query, limit, requestId } = msg;
|
|
2273
2546
|
try {
|
|
2274
2547
|
const resolvedCwd = expandTilde(cwd);
|
|
2275
|
-
const branches = await listBranchSuggestions(resolvedCwd, {
|
|
2548
|
+
const branches = await listBranchSuggestions(resolvedCwd, {
|
|
2549
|
+
query,
|
|
2550
|
+
limit,
|
|
2551
|
+
remoteName: msg.remoteName,
|
|
2552
|
+
});
|
|
2276
2553
|
this.emit({
|
|
2277
2554
|
type: 'branch_suggestions_response',
|
|
2278
2555
|
payload: {
|
|
@@ -2293,6 +2570,30 @@ export class Session {
|
|
|
2293
2570
|
});
|
|
2294
2571
|
}
|
|
2295
2572
|
}
|
|
2573
|
+
async handleGitRemotesRequest(msg) {
|
|
2574
|
+
const { cwd, requestId } = msg;
|
|
2575
|
+
try {
|
|
2576
|
+
const remotes = await listGitRemotes(expandTilde(cwd));
|
|
2577
|
+
this.emit({
|
|
2578
|
+
type: 'git_remotes_response',
|
|
2579
|
+
payload: {
|
|
2580
|
+
remotes,
|
|
2581
|
+
error: null,
|
|
2582
|
+
requestId,
|
|
2583
|
+
},
|
|
2584
|
+
});
|
|
2585
|
+
}
|
|
2586
|
+
catch (error) {
|
|
2587
|
+
this.emit({
|
|
2588
|
+
type: 'git_remotes_response',
|
|
2589
|
+
payload: {
|
|
2590
|
+
remotes: [],
|
|
2591
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2592
|
+
requestId,
|
|
2593
|
+
},
|
|
2594
|
+
});
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2296
2597
|
async handleDirectorySuggestionsRequest(msg) {
|
|
2297
2598
|
const { query, limit, requestId, cwd, includeFiles, includeDirectories, onlyGitRepos } = msg;
|
|
2298
2599
|
try {
|
|
@@ -2351,6 +2652,55 @@ export class Session {
|
|
|
2351
2652
|
});
|
|
2352
2653
|
}
|
|
2353
2654
|
}
|
|
2655
|
+
async handleWorkspaceFileSuggestionsRequest(msg) {
|
|
2656
|
+
const { cwd, query, limit, requestId, includeDirectories, includeFiles, ref } = msg;
|
|
2657
|
+
try {
|
|
2658
|
+
const workspaceCwd = expandTilde(cwd);
|
|
2659
|
+
if (ref) {
|
|
2660
|
+
this.assertSafeGitRef(ref, 'workspace file ref');
|
|
2661
|
+
}
|
|
2662
|
+
const entries = ref
|
|
2663
|
+
? await searchWorkspaceEntriesAtGitRef({
|
|
2664
|
+
cwd: workspaceCwd,
|
|
2665
|
+
ref,
|
|
2666
|
+
query,
|
|
2667
|
+
limit,
|
|
2668
|
+
includeDirectories,
|
|
2669
|
+
includeFiles,
|
|
2670
|
+
})
|
|
2671
|
+
: await searchWorkspaceEntries({
|
|
2672
|
+
cwd: workspaceCwd,
|
|
2673
|
+
query,
|
|
2674
|
+
limit,
|
|
2675
|
+
includeDirectories,
|
|
2676
|
+
includeFiles,
|
|
2677
|
+
});
|
|
2678
|
+
this.emit({
|
|
2679
|
+
type: 'workspace_file_suggestions_response',
|
|
2680
|
+
payload: {
|
|
2681
|
+
cwd,
|
|
2682
|
+
query,
|
|
2683
|
+
ref: ref ?? null,
|
|
2684
|
+
entries,
|
|
2685
|
+
error: null,
|
|
2686
|
+
requestId,
|
|
2687
|
+
},
|
|
2688
|
+
});
|
|
2689
|
+
}
|
|
2690
|
+
catch (error) {
|
|
2691
|
+
this.emit({
|
|
2692
|
+
type: 'workspace_file_suggestions_response',
|
|
2693
|
+
payload: {
|
|
2694
|
+
cwd,
|
|
2695
|
+
query,
|
|
2696
|
+
ref: ref ?? null,
|
|
2697
|
+
entries: [],
|
|
2698
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2699
|
+
requestId,
|
|
2700
|
+
},
|
|
2701
|
+
});
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2354
2704
|
async handleGitCloneRequest(msg) {
|
|
2355
2705
|
const { url, targetDirectory, requestId } = msg;
|
|
2356
2706
|
try {
|
|
@@ -2792,6 +3142,7 @@ export class Session {
|
|
|
2792
3142
|
}
|
|
2793
3143
|
await mergeFromBase(cwd, {
|
|
2794
3144
|
baseRef: msg.baseRef,
|
|
3145
|
+
remoteName: msg.remoteName,
|
|
2795
3146
|
requireCleanTarget: msg.requireCleanTarget ?? true,
|
|
2796
3147
|
});
|
|
2797
3148
|
this.scheduleCheckoutDiffRefreshForCwd(cwd);
|
|
@@ -2820,7 +3171,7 @@ export class Session {
|
|
|
2820
3171
|
async handleCheckoutPushRequest(msg) {
|
|
2821
3172
|
const { cwd, requestId } = msg;
|
|
2822
3173
|
try {
|
|
2823
|
-
await pushCurrentBranch(cwd);
|
|
3174
|
+
await pushCurrentBranch(cwd, { remoteName: msg.remoteName });
|
|
2824
3175
|
this.emit({
|
|
2825
3176
|
type: 'checkout_push_response',
|
|
2826
3177
|
payload: {
|
|
@@ -2859,6 +3210,7 @@ export class Session {
|
|
|
2859
3210
|
title,
|
|
2860
3211
|
body,
|
|
2861
3212
|
base: msg.baseRef,
|
|
3213
|
+
remoteName: msg.remoteName,
|
|
2862
3214
|
});
|
|
2863
3215
|
this.emit({
|
|
2864
3216
|
type: 'checkout_pr_create_response',
|
|
@@ -2887,7 +3239,7 @@ export class Session {
|
|
|
2887
3239
|
async handleCheckoutPrStatusRequest(msg) {
|
|
2888
3240
|
const { cwd, requestId } = msg;
|
|
2889
3241
|
try {
|
|
2890
|
-
const prStatus = await getPullRequestStatus(cwd);
|
|
3242
|
+
const prStatus = await getPullRequestStatus(cwd, { remoteName: msg.remoteName });
|
|
2891
3243
|
this.emit({
|
|
2892
3244
|
type: 'checkout_pr_status_response',
|
|
2893
3245
|
payload: {
|
|
@@ -2915,7 +3267,7 @@ export class Session {
|
|
|
2915
3267
|
async handleCheckoutPrFailureLogsRequest(msg) {
|
|
2916
3268
|
const { cwd, requestId } = msg;
|
|
2917
3269
|
try {
|
|
2918
|
-
const result = await getPullRequestFailureLogs(cwd);
|
|
3270
|
+
const result = await getPullRequestFailureLogs(cwd, { remoteName: msg.remoteName });
|
|
2919
3271
|
this.emit({
|
|
2920
3272
|
type: 'checkout_pr_failure_logs_response',
|
|
2921
3273
|
payload: {
|
|
@@ -2945,6 +3297,7 @@ export class Session {
|
|
|
2945
3297
|
try {
|
|
2946
3298
|
await mergePullRequest(cwd, {
|
|
2947
3299
|
method: msg.method ?? 'squash',
|
|
3300
|
+
remoteName: msg.remoteName,
|
|
2948
3301
|
});
|
|
2949
3302
|
this.emit({
|
|
2950
3303
|
type: 'checkout_pr_merge_response',
|
|
@@ -3295,19 +3648,24 @@ export class Session {
|
|
|
3295
3648
|
* Handle read-only file explorer requests scoped to a workspace cwd
|
|
3296
3649
|
*/
|
|
3297
3650
|
async handleWorkspaceFileExplorerRequest(request) {
|
|
3298
|
-
const { cwd, path: requestedPath = '.', mode, requestId } = request;
|
|
3651
|
+
const { cwd, path: requestedPath = '.', mode, requestId, ref } = request;
|
|
3299
3652
|
try {
|
|
3300
3653
|
const root = expandTilde(cwd);
|
|
3654
|
+
if (ref) {
|
|
3655
|
+
this.assertSafeGitRef(ref, 'workspace file ref');
|
|
3656
|
+
}
|
|
3301
3657
|
if (mode === 'list') {
|
|
3302
3658
|
const directory = await listDirectoryEntries({
|
|
3303
3659
|
root,
|
|
3304
3660
|
relativePath: requestedPath,
|
|
3661
|
+
ref,
|
|
3305
3662
|
});
|
|
3306
3663
|
this.emit({
|
|
3307
3664
|
type: 'workspace_file_explorer_response',
|
|
3308
3665
|
payload: {
|
|
3309
3666
|
cwd,
|
|
3310
3667
|
path: directory.path,
|
|
3668
|
+
ref: ref ?? null,
|
|
3311
3669
|
mode,
|
|
3312
3670
|
directory,
|
|
3313
3671
|
file: null,
|
|
@@ -3320,12 +3678,14 @@ export class Session {
|
|
|
3320
3678
|
const file = await readExplorerFile({
|
|
3321
3679
|
root,
|
|
3322
3680
|
relativePath: requestedPath,
|
|
3681
|
+
ref,
|
|
3323
3682
|
});
|
|
3324
3683
|
this.emit({
|
|
3325
3684
|
type: 'workspace_file_explorer_response',
|
|
3326
3685
|
payload: {
|
|
3327
3686
|
cwd,
|
|
3328
3687
|
path: file.path,
|
|
3688
|
+
ref: ref ?? null,
|
|
3329
3689
|
mode,
|
|
3330
3690
|
directory: null,
|
|
3331
3691
|
file,
|
|
@@ -3342,6 +3702,7 @@ export class Session {
|
|
|
3342
3702
|
payload: {
|
|
3343
3703
|
cwd,
|
|
3344
3704
|
path: requestedPath,
|
|
3705
|
+
ref: ref ?? null,
|
|
3345
3706
|
mode,
|
|
3346
3707
|
directory: null,
|
|
3347
3708
|
file: null,
|