@ts47andres/exeggutor 1.1.2
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/LICENSE +201 -0
- package/README.md +230 -0
- package/bin/exeggutor.js +217 -0
- package/package.json +63 -0
- package/packages/backend/dist/gitWorktree.js +336 -0
- package/packages/backend/dist/index.js +538 -0
- package/packages/backend/dist/ptyManager.js +409 -0
- package/packages/backend/dist/tailscale.js +145 -0
- package/packages/backend/dist/workspaceDb.js +152 -0
- package/packages/backend/git-wrapper/git +60 -0
- package/packages/backend/native/FolderPicker.cs +139 -0
- package/packages/backend/package.json +25 -0
- package/packages/backend/scripts/compile-picker.js +61 -0
- package/packages/backend/scripts/git-guard.ps1 +48 -0
- package/packages/backend/src/gitWorktree.ts +320 -0
- package/packages/backend/src/index.ts +554 -0
- package/packages/backend/src/ptyManager.ts +414 -0
- package/packages/backend/src/tailscale.ts +138 -0
- package/packages/backend/src/workspaceDb.ts +151 -0
- package/packages/frontend/dist/assets/index-B3TWNlss.css +47 -0
- package/packages/frontend/dist/assets/index-DfUyE-fY.js +192 -0
- package/packages/frontend/dist/index.html +17 -0
- package/packages/frontend/package.json +29 -0
- package/src/autostart.js +162 -0
- package/src/cli.js +613 -0
- package/src/server-manager.js +139 -0
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ts47andres/exeggutor",
|
|
3
|
+
"version": "1.1.2",
|
|
4
|
+
"description": "Terminal Multiplexer & Git Worktree Manager - local-first workspace coordinator",
|
|
5
|
+
"private": false,
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"bin": {
|
|
8
|
+
"exeggutor": "./bin/exeggutor.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "node bin/exeggutor.js",
|
|
12
|
+
"dev:backend": "cd packages/backend && npm run dev",
|
|
13
|
+
"dev:frontend": "cd packages/frontend && npm run dev",
|
|
14
|
+
"build": "cd packages/backend && npm run build && cd ../.. && cd packages/frontend && npm run build",
|
|
15
|
+
"postinstall": "node packages/backend/scripts/compile-picker.js",
|
|
16
|
+
"prepublishOnly": "npm run build",
|
|
17
|
+
"status": "node bin/exeggutor.js --status",
|
|
18
|
+
"stop": "node bin/exeggutor.js --stop",
|
|
19
|
+
"start": "node bin/exeggutor.js"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18.0.0"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@fastify/cors": "^9.0.1",
|
|
26
|
+
"@fastify/static": "^7.0.4",
|
|
27
|
+
"@fastify/websocket": "^10.0.1",
|
|
28
|
+
"fastify": "^4.28.1",
|
|
29
|
+
"node-pty": "^1.1.0-beta21"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"terminal",
|
|
33
|
+
"multiplexer",
|
|
34
|
+
"git",
|
|
35
|
+
"worktree",
|
|
36
|
+
"workspace",
|
|
37
|
+
"tui",
|
|
38
|
+
"dev-tools",
|
|
39
|
+
"productivity"
|
|
40
|
+
],
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/TS47Andres/Exeggutor.git"
|
|
44
|
+
},
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/TS47Andres/Exeggutor/issues"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/TS47Andres/Exeggutor#readme",
|
|
49
|
+
"files": [
|
|
50
|
+
"bin/",
|
|
51
|
+
"src/",
|
|
52
|
+
"packages/backend/package.json",
|
|
53
|
+
"packages/backend/bin/",
|
|
54
|
+
"packages/backend/dist/",
|
|
55
|
+
"packages/backend/scripts/",
|
|
56
|
+
"packages/backend/git-wrapper/",
|
|
57
|
+
"packages/backend/native/",
|
|
58
|
+
"packages/backend/src/",
|
|
59
|
+
"packages/frontend/package.json",
|
|
60
|
+
"packages/frontend/dist/",
|
|
61
|
+
"LICENSE"
|
|
62
|
+
]
|
|
63
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.validateBranchName = validateBranchName;
|
|
27
|
+
exports.execAsync = execAsync;
|
|
28
|
+
exports.isGitRepository = isGitRepository;
|
|
29
|
+
exports.getBranches = getBranches;
|
|
30
|
+
exports.setupGitWorktree = setupGitWorktree;
|
|
31
|
+
exports.removeGitWorktree = removeGitWorktree;
|
|
32
|
+
exports.createBranch = createBranch;
|
|
33
|
+
exports.showFolderPicker = showFolderPicker;
|
|
34
|
+
const child_process_1 = require("child_process");
|
|
35
|
+
const path = __importStar(require("path"));
|
|
36
|
+
const fs = __importStar(require("fs"));
|
|
37
|
+
const os = __importStar(require("os"));
|
|
38
|
+
// Validates git branch names to prevent command injection and ensure compliance with git rules.
|
|
39
|
+
function validateBranchName(branch) {
|
|
40
|
+
const cleanPattern = /^[a-zA-Z0-9-_./@]+$/; // Pattern matching safe git branch characters.
|
|
41
|
+
if (!branch || !cleanPattern.test(branch)) {
|
|
42
|
+
throw new Error('Invalid branch name. Only alphanumeric, dashes, underscores, dots, slashes, and @ are permitted.');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Helper function that executes a terminal command asynchronously and returns its stdout.
|
|
46
|
+
function execAsync(command, cwd) {
|
|
47
|
+
console.log(`[GIT] execAsync(command="${command}", cwd="${cwd}")`);
|
|
48
|
+
const p = new Promise((resolve, reject) => {
|
|
49
|
+
const child = (0, child_process_1.exec)(command, { cwd, windowsHide: true }, (error, stdout, stderr) => {
|
|
50
|
+
if (error) {
|
|
51
|
+
const errMsg = stderr || error.message; // Resolves the exact error details from stderr or standard node error.
|
|
52
|
+
reject(new Error(errMsg));
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
const cleanedOutput = stdout.trim(); // The stripped stdout returned from the execution.
|
|
56
|
+
resolve(cleanedOutput);
|
|
57
|
+
}
|
|
58
|
+
}); // The child process handle spawned for this command run.
|
|
59
|
+
}); // The promise mapping the command execution flow.
|
|
60
|
+
return p;
|
|
61
|
+
}
|
|
62
|
+
// Verifies if the target directory is a valid git repository.
|
|
63
|
+
async function isGitRepository(folderPath) {
|
|
64
|
+
try {
|
|
65
|
+
const resolvedPath = path.resolve(folderPath); // Resolved absolute target path string.
|
|
66
|
+
await execAsync('git rev-parse --is-inside-work-tree', resolvedPath);
|
|
67
|
+
const successResult = true; // Flag denoting a valid git workspace.
|
|
68
|
+
return successResult;
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
const failResult = false; // Flag denoting an invalid git workspace.
|
|
72
|
+
return failResult;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Retrieves the list of all local git branches available in the repository.
|
|
76
|
+
async function getBranches(folderPath) {
|
|
77
|
+
const resolvedPath = path.resolve(folderPath); // Resolved absolute target path string.
|
|
78
|
+
const isGit = await isGitRepository(resolvedPath); // Flag verifying if the directory has git initialized.
|
|
79
|
+
if (!isGit) {
|
|
80
|
+
const emptyList = []; // Initialized empty branches array.
|
|
81
|
+
return emptyList;
|
|
82
|
+
}
|
|
83
|
+
const rawBranches = await execAsync('git branch --format="%(refname:short)"', resolvedPath); // Raw branch string output from command line.
|
|
84
|
+
const branchesList = rawBranches.split('\n').map(b => b.trim()).filter(b => b.length > 0); // Parsed and filtered array of branch names.
|
|
85
|
+
return branchesList;
|
|
86
|
+
}
|
|
87
|
+
// Sets up a git worktree for a specific branch inside a hidden sub-folder.
|
|
88
|
+
async function setupGitWorktree(repoPath, branch) {
|
|
89
|
+
console.log(`[GIT] setupGitWorktree(repoPath="${repoPath}", branch="${branch}")`);
|
|
90
|
+
validateBranchName(branch);
|
|
91
|
+
const resolvedRepo = path.resolve(repoPath); // Resolved absolute parent repository path.
|
|
92
|
+
const isGit = await isGitRepository(resolvedRepo); // Check flag verifying if the path is a git repo.
|
|
93
|
+
if (!isGit) {
|
|
94
|
+
throw new Error('Target folder is not a valid Git repository');
|
|
95
|
+
}
|
|
96
|
+
console.log(`[GIT] setupGitWorktree: checking existing worktrees for branch="${branch}"`);
|
|
97
|
+
// Check if the branch is already checked out in any worktree (including the main repository).
|
|
98
|
+
const worktreeListOutput = await execAsync('git worktree list --porcelain', resolvedRepo); // Porcelain worktree list output.
|
|
99
|
+
const worktreeLines = worktreeListOutput.split('\n'); // Split by lines.
|
|
100
|
+
let currentWorktreePath = resolvedRepo; // Holds the path of the current worktree being processed.
|
|
101
|
+
for (const line of worktreeLines) {
|
|
102
|
+
if (line.startsWith('worktree ')) {
|
|
103
|
+
currentWorktreePath = line.substring(9).trim(); // Extract path.
|
|
104
|
+
}
|
|
105
|
+
else if (line.startsWith('branch ')) {
|
|
106
|
+
const ref = line.substring(7).trim(); // Extract branch ref.
|
|
107
|
+
if (ref === `refs/heads/${branch}`) {
|
|
108
|
+
const foundWorktreePath = path.resolve(currentWorktreePath); // Found matching worktree path.
|
|
109
|
+
return foundWorktreePath;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const sanitizedBranch = branch.replace(/[^a-zA-Z0-9-_]/g, '_'); // Sanitized branch name to avoid unsafe folder characters.
|
|
114
|
+
const worktreePath = path.join(resolvedRepo, '.exeggutor-worktrees', sanitizedBranch); // Path to host the worktree outside the hidden git directory.
|
|
115
|
+
const worktreeParent = path.dirname(worktreePath); // Parent directory of the target worktree path.
|
|
116
|
+
if (!fs.existsSync(worktreeParent)) {
|
|
117
|
+
fs.mkdirSync(worktreeParent, { recursive: true });
|
|
118
|
+
}
|
|
119
|
+
// Ensure .exeggutor-worktrees is ignored in git.
|
|
120
|
+
const gitignorePath = path.join(resolvedRepo, '.gitignore'); // Path to workspace gitignore file.
|
|
121
|
+
const ignorePattern = '.exeggutor-worktrees/'; // Pattern to ignore.
|
|
122
|
+
try {
|
|
123
|
+
if (fs.existsSync(gitignorePath)) {
|
|
124
|
+
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8'); // Loaded gitignore content.
|
|
125
|
+
if (!gitignoreContent.includes(ignorePattern)) {
|
|
126
|
+
const finalContent = gitignoreContent.endsWith('\n') || gitignoreContent.length === 0
|
|
127
|
+
? gitignoreContent + ignorePattern + '\n'
|
|
128
|
+
: gitignoreContent + '\n' + ignorePattern + '\n'; // Structured appended content.
|
|
129
|
+
fs.writeFileSync(gitignorePath, finalContent, 'utf8');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
fs.writeFileSync(gitignorePath, ignorePattern + '\n', 'utf8');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch (_) {
|
|
137
|
+
// Safe ignore ignore-write errors.
|
|
138
|
+
}
|
|
139
|
+
if (fs.existsSync(worktreePath)) {
|
|
140
|
+
console.log(`[GIT] setupGitWorktree: worktree path already exists at "${worktreePath}"`);
|
|
141
|
+
const pathExistsResult = worktreePath; // Returns the existing path directly if the worktree directory is already present.
|
|
142
|
+
return pathExistsResult;
|
|
143
|
+
}
|
|
144
|
+
const branches = await getBranches(resolvedRepo); // Fetch the list of local branches.
|
|
145
|
+
const branchExists = branches.includes(branch); // Flag indicating if the requested branch exists locally.
|
|
146
|
+
if (branchExists) {
|
|
147
|
+
console.log(`[GIT] setupGitWorktree: adding existing branch "${branch}" to worktree`);
|
|
148
|
+
await execAsync(`git worktree add "${worktreePath}" "${branch}"`, resolvedRepo);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
console.log(`[GIT] setupGitWorktree: creating new branch "${branch}" with worktree`);
|
|
152
|
+
await execAsync(`git worktree add -b "${branch}" "${worktreePath}"`, resolvedRepo);
|
|
153
|
+
}
|
|
154
|
+
console.log(`[GIT] setupGitWorktree: done, path="${worktreePath}"`);
|
|
155
|
+
const finalPath = worktreePath; // Path of the newly created worktree.
|
|
156
|
+
return finalPath;
|
|
157
|
+
}
|
|
158
|
+
// Removes a git worktree and prunes the worktree directory reference.
|
|
159
|
+
async function removeGitWorktree(repoPath, worktreePath) {
|
|
160
|
+
console.log(`[GIT] removeGitWorktree(repoPath="${repoPath}", worktreePath="${worktreePath}")`);
|
|
161
|
+
const resolvedRepo = path.resolve(repoPath); // Resolved absolute repository path.
|
|
162
|
+
const isGit = await isGitRepository(resolvedRepo); // Verification flag.
|
|
163
|
+
if (!isGit) {
|
|
164
|
+
console.log(`[GIT] removeGitWorktree: "${resolvedRepo}" is not a git repo, skipping`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const normalizedWorktreePath = path.resolve(worktreePath); // Normalized path of the target worktree.
|
|
168
|
+
let retries = 5; // Maximum retries count for lock back-off.
|
|
169
|
+
while (retries > 0) {
|
|
170
|
+
try {
|
|
171
|
+
console.log(`[GIT] removeGitWorktree: running "git worktree remove --force" (attempt ${6 - retries})`);
|
|
172
|
+
await execAsync(`git worktree remove --force "${normalizedWorktreePath}"`, resolvedRepo);
|
|
173
|
+
console.log(`[GIT] removeGitWorktree: remove OK`);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
console.log(`[GIT] removeGitWorktree: remove attempt ${6 - retries} failed: ${err}`);
|
|
178
|
+
retries--;
|
|
179
|
+
if (retries === 0) {
|
|
180
|
+
// Final fallback skip.
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
console.log(`[GIT] removeGitWorktree: pruning worktrees`);
|
|
188
|
+
await execAsync('git worktree prune', resolvedRepo);
|
|
189
|
+
console.log(`[GIT] removeGitWorktree: done`);
|
|
190
|
+
}
|
|
191
|
+
// Creates a new Git branch in the specified repository.
|
|
192
|
+
async function createBranch(repoPath, branchName) {
|
|
193
|
+
console.log(`[GIT] createBranch(repoPath="${repoPath}", branchName="${branchName}")`);
|
|
194
|
+
validateBranchName(branchName);
|
|
195
|
+
const resolvedRepo = path.resolve(repoPath); // Resolved absolute repository path.
|
|
196
|
+
const isGit = await isGitRepository(resolvedRepo); // Verification flag.
|
|
197
|
+
if (!isGit) {
|
|
198
|
+
throw new Error('Target folder is not a valid Git repository');
|
|
199
|
+
}
|
|
200
|
+
await execAsync(`git branch "${branchName}"`, resolvedRepo);
|
|
201
|
+
console.log(`[GIT] createBranch: branch "${branchName}" created`);
|
|
202
|
+
}
|
|
203
|
+
// Mutex flag preventing more than one concurrent folder picker dialog.
|
|
204
|
+
let pickerInFlight = false;
|
|
205
|
+
// Opens a native folder picker dialog using platform-specific tools.
|
|
206
|
+
// Windows: compiled FolderPicker.exe (C#/.NET) using FolderBrowserDialog.
|
|
207
|
+
// macOS: osascript with choose folder AppleScript command.
|
|
208
|
+
// Linux: zenity --file-selection, with kdialog as a fallback.
|
|
209
|
+
// Returns the selected path, or an empty string if the user cancelled.
|
|
210
|
+
async function showFolderPicker() {
|
|
211
|
+
// Guard: reject concurrent requests — only one picker dialog at a time.
|
|
212
|
+
if (pickerInFlight) {
|
|
213
|
+
return ''; // A dialog is already open; silently return empty.
|
|
214
|
+
}
|
|
215
|
+
pickerInFlight = true;
|
|
216
|
+
try {
|
|
217
|
+
const platform = os.platform();
|
|
218
|
+
if (platform === 'win32') {
|
|
219
|
+
const binaryPath = path.join(__dirname, '..', 'bin', 'FolderPicker.exe');
|
|
220
|
+
const resolvedPath = path.resolve(binaryPath);
|
|
221
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
222
|
+
throw new Error(`Native folder picker not available (not found at ${resolvedPath}). ` +
|
|
223
|
+
'Type the workspace path manually.');
|
|
224
|
+
}
|
|
225
|
+
return await new Promise((resolve, reject) => {
|
|
226
|
+
const child = (0, child_process_1.spawn)(resolvedPath, [], {
|
|
227
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
228
|
+
windowsHide: false, // Show the dialog window.
|
|
229
|
+
});
|
|
230
|
+
let stdout = '';
|
|
231
|
+
let stderr = '';
|
|
232
|
+
child.stdout.on('data', (data) => {
|
|
233
|
+
stdout += data.toString();
|
|
234
|
+
});
|
|
235
|
+
child.stderr.on('data', (data) => {
|
|
236
|
+
stderr += data.toString();
|
|
237
|
+
});
|
|
238
|
+
child.on('close', (code) => {
|
|
239
|
+
if (code === 0) {
|
|
240
|
+
resolve(stdout.trim()); // Path selected.
|
|
241
|
+
}
|
|
242
|
+
else if (code === 2) {
|
|
243
|
+
resolve(''); // User cancelled — not an error.
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
const errMsg = stderr.trim() || `Folder picker exited with code ${code}`;
|
|
247
|
+
reject(new Error(errMsg));
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
child.on('error', (err) => {
|
|
251
|
+
reject(new Error(`Failed to launch folder picker: ${err.message}`));
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
if (platform === 'darwin') {
|
|
256
|
+
// osascript exits with code 1 and stderr containing 'User canceled.' on cancel.
|
|
257
|
+
return await new Promise((resolve, reject) => {
|
|
258
|
+
const child = (0, child_process_1.spawn)('osascript', [
|
|
259
|
+
'-e',
|
|
260
|
+
'POSIX path of (choose folder with prompt "Select workspace folder")'
|
|
261
|
+
], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
262
|
+
let stdout = '';
|
|
263
|
+
let stderr = '';
|
|
264
|
+
child.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
265
|
+
child.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
266
|
+
child.on('close', (code) => {
|
|
267
|
+
if (code === 0) {
|
|
268
|
+
resolve(stdout.trim());
|
|
269
|
+
}
|
|
270
|
+
else if (stderr.toLowerCase().includes('user canceled') || stderr.toLowerCase().includes('cancelled')) {
|
|
271
|
+
resolve(''); // User dismissed the dialog.
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
reject(new Error('Failed to open macOS folder picker. Type the workspace path manually.'));
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
child.on('error', () => {
|
|
278
|
+
reject(new Error('osascript not found. Type the workspace path manually.'));
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
// Linux: zenity exits 0 on OK, 1 on cancel. kdialog exits 0 on OK, 1 on cancel.
|
|
283
|
+
try {
|
|
284
|
+
return await new Promise((resolve, reject) => {
|
|
285
|
+
const child = (0, child_process_1.spawn)('zenity', [
|
|
286
|
+
'--file-selection', '--directory',
|
|
287
|
+
'--title=Select workspace folder'
|
|
288
|
+
], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
289
|
+
let stdout = '';
|
|
290
|
+
child.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
291
|
+
child.on('close', (code) => {
|
|
292
|
+
if (code === 0) {
|
|
293
|
+
resolve(stdout.trim());
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
resolve(''); // code 1 = cancel (or dismiss), not an error.
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
child.on('error', (_err) => {
|
|
300
|
+
reject(new Error('zenity_not_found'));
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
catch (err1) {
|
|
305
|
+
// zenity not installed — try kdialog (KDE).
|
|
306
|
+
try {
|
|
307
|
+
return await new Promise((resolve, reject) => {
|
|
308
|
+
const child = (0, child_process_1.spawn)('kdialog', [
|
|
309
|
+
'--getexistingdirectory',
|
|
310
|
+
'--title', 'Select workspace folder'
|
|
311
|
+
], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
312
|
+
let stdout = '';
|
|
313
|
+
child.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
314
|
+
child.on('close', (code) => {
|
|
315
|
+
if (code === 0) {
|
|
316
|
+
resolve(stdout.trim());
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
resolve(''); // code 1 = cancel.
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
child.on('error', (_err) => {
|
|
323
|
+
reject(new Error('kdialog_not_found'));
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
catch (err2) {
|
|
328
|
+
throw new Error('Folder picker not available. Install zenity (GNOME) or kdialog (KDE), ' +
|
|
329
|
+
'or type the workspace path manually.');
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
finally {
|
|
334
|
+
pickerInFlight = false; // Always release the lock.
|
|
335
|
+
}
|
|
336
|
+
}
|