@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
|
@@ -0,0 +1,152 @@
|
|
|
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.readDatabase = readDatabase;
|
|
27
|
+
exports.writeDatabase = writeDatabase;
|
|
28
|
+
exports.getWorkspaces = getWorkspaces;
|
|
29
|
+
exports.createWorkspace = createWorkspace;
|
|
30
|
+
exports.deleteWorkspace = deleteWorkspace;
|
|
31
|
+
exports.updateWorkspace = updateWorkspace;
|
|
32
|
+
exports.createTerminalTab = createTerminalTab;
|
|
33
|
+
exports.deleteTerminalTab = deleteTerminalTab;
|
|
34
|
+
exports.updateTerminalTab = updateTerminalTab;
|
|
35
|
+
const fs = __importStar(require("fs"));
|
|
36
|
+
const os = __importStar(require("os"));
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const dbPath = path.join(os.homedir(), '.exeggutor-sessions.json'); // Absolute system path pointing to the sessions flat-file JSON database.
|
|
39
|
+
// Reads and parses the sessions database from the local file system.
|
|
40
|
+
function readDatabase() {
|
|
41
|
+
if (!fs.existsSync(dbPath)) {
|
|
42
|
+
const initialDb = { workspaces: [] }; // The initial structural database template to write when the file does not exist.
|
|
43
|
+
fs.writeFileSync(dbPath, JSON.stringify(initialDb, null, 2), 'utf8');
|
|
44
|
+
return initialDb;
|
|
45
|
+
}
|
|
46
|
+
const rawData = fs.readFileSync(dbPath, 'utf8'); // Loaded string content from the session JSON file.
|
|
47
|
+
const parsed = JSON.parse(rawData); // Parsed sessions database schema object.
|
|
48
|
+
return parsed;
|
|
49
|
+
}
|
|
50
|
+
// Serializes and saves the database state back to the local file system.
|
|
51
|
+
function writeDatabase(db) {
|
|
52
|
+
const serialized = JSON.stringify(db, null, 2); // Serialized JSON string of the session database structure.
|
|
53
|
+
const tempPath = dbPath + '.tmp'; // Temporary file path to write.
|
|
54
|
+
fs.writeFileSync(tempPath, serialized, 'utf8');
|
|
55
|
+
fs.renameSync(tempPath, dbPath);
|
|
56
|
+
}
|
|
57
|
+
// Retrieves all workspaces registered in the database.
|
|
58
|
+
function getWorkspaces() {
|
|
59
|
+
const db = readDatabase(); // The current session database instance loaded from disk.
|
|
60
|
+
const list = db.workspaces; // The list of workspaces extracted from the loaded database.
|
|
61
|
+
return list;
|
|
62
|
+
}
|
|
63
|
+
// Creates a new workspace and initializes it in the persistent database.
|
|
64
|
+
function createWorkspace(name, folderPath) {
|
|
65
|
+
const db = readDatabase(); // The active database object loaded from persistent storage.
|
|
66
|
+
const newWorkspace = {
|
|
67
|
+
id: 'ws_' + Math.random().toString(36).substring(2, 9), // Dynamically generated unique workspace ID string.
|
|
68
|
+
name: name, // The workspace name passed as a parameter.
|
|
69
|
+
path: path.resolve(folderPath), // Resolved absolute path string.
|
|
70
|
+
tabs: [], // Initialized empty list of terminal tabs.
|
|
71
|
+
}; // The new workspace structure to append.
|
|
72
|
+
db.workspaces.push(newWorkspace);
|
|
73
|
+
if (!db.activeWorkspaceId) {
|
|
74
|
+
db.activeWorkspaceId = newWorkspace.id; // Sets the first workspace as the default active workspace.
|
|
75
|
+
}
|
|
76
|
+
writeDatabase(db);
|
|
77
|
+
const result = newWorkspace; // The created workspace returned to caller.
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
// Deletes a workspace by its ID from the persistent database.
|
|
81
|
+
function deleteWorkspace(id) {
|
|
82
|
+
const db = readDatabase(); // The current database structure loaded from disk.
|
|
83
|
+
const filtered = db.workspaces.filter(ws => ws.id !== id); // Filtered array of workspaces excluding the target workspace ID.
|
|
84
|
+
db.workspaces = filtered;
|
|
85
|
+
if (db.activeWorkspaceId === id) {
|
|
86
|
+
const fallbackId = db.workspaces.length > 0 ? db.workspaces[0].id : undefined; // Fallback workspace ID if the deleted one was active.
|
|
87
|
+
db.activeWorkspaceId = fallbackId;
|
|
88
|
+
}
|
|
89
|
+
writeDatabase(db);
|
|
90
|
+
}
|
|
91
|
+
// Updates details of an existing workspace in the database.
|
|
92
|
+
function updateWorkspace(id, updates) {
|
|
93
|
+
const db = readDatabase(); // The loaded session database object.
|
|
94
|
+
const wsIndex = db.workspaces.findIndex(ws => ws.id === id); // Index of the target workspace in the array.
|
|
95
|
+
if (wsIndex === -1) {
|
|
96
|
+
const nullResult = null; // Represents a missing workspace lookup outcome.
|
|
97
|
+
return nullResult;
|
|
98
|
+
}
|
|
99
|
+
const updatedWs = { ...db.workspaces[wsIndex], ...updates }; // Blended workspace object with updated values.
|
|
100
|
+
db.workspaces[wsIndex] = updatedWs;
|
|
101
|
+
writeDatabase(db);
|
|
102
|
+
const result = updatedWs; // Returns the updated workspace structure.
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
// Creates a new terminal tab under a specific workspace in the database.
|
|
106
|
+
function createTerminalTab(workspaceId, name, cwd, shell) {
|
|
107
|
+
const db = readDatabase(); // The database session object retrieved from file system.
|
|
108
|
+
const ws = db.workspaces.find(w => w.id === workspaceId); // Workspace object referenced by workspaceId.
|
|
109
|
+
if (!ws) {
|
|
110
|
+
const errorResult = null; // Represents a failure to find the workspace.
|
|
111
|
+
return errorResult;
|
|
112
|
+
}
|
|
113
|
+
const newTab = {
|
|
114
|
+
id: 'tab_' + Math.random().toString(36).substring(2, 9), // Dynamically generated unique tab ID string.
|
|
115
|
+
name: name, // User assigned name of the terminal tab.
|
|
116
|
+
cwd: path.resolve(cwd), // Resolved absolute current working directory.
|
|
117
|
+
shell: shell, // The optional custom shell path override.
|
|
118
|
+
}; // The new terminal tab object to be created.
|
|
119
|
+
ws.tabs.push(newTab);
|
|
120
|
+
writeDatabase(db);
|
|
121
|
+
const result = newTab; // Returns the newly created terminal tab.
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
// Deletes a terminal tab from a workspace in the database.
|
|
125
|
+
function deleteTerminalTab(workspaceId, tabId) {
|
|
126
|
+
const db = readDatabase(); // The database state loaded from sessions.json.
|
|
127
|
+
const ws = db.workspaces.find(w => w.id === workspaceId); // Target workspace object.
|
|
128
|
+
if (ws) {
|
|
129
|
+
const filteredTabs = ws.tabs.filter(t => t.id !== tabId); // Filtered terminal tabs excluding the target tab ID.
|
|
130
|
+
ws.tabs = filteredTabs;
|
|
131
|
+
writeDatabase(db);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Updates details of an existing terminal tab in a workspace.
|
|
135
|
+
function updateTerminalTab(workspaceId, tabId, updates) {
|
|
136
|
+
const db = readDatabase(); // The loaded session database object.
|
|
137
|
+
const ws = db.workspaces.find(w => w.id === workspaceId); // Target workspace instance.
|
|
138
|
+
if (!ws) {
|
|
139
|
+
const errorResult = null; // Target workspace not found.
|
|
140
|
+
return errorResult;
|
|
141
|
+
}
|
|
142
|
+
const tabIndex = ws.tabs.findIndex(t => t.id === tabId); // Index of the target tab.
|
|
143
|
+
if (tabIndex === -1) {
|
|
144
|
+
const errorResult = null; // Target tab not found.
|
|
145
|
+
return errorResult;
|
|
146
|
+
}
|
|
147
|
+
const updatedTab = { ...ws.tabs[tabIndex], ...updates }; // Blended terminal tab configuration.
|
|
148
|
+
ws.tabs[tabIndex] = updatedTab;
|
|
149
|
+
writeDatabase(db);
|
|
150
|
+
const result = updatedTab; // Returns the updated tab structure.
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
REAL_GIT=$(command -v git 2>/dev/null)
|
|
3
|
+
if [ -z "$REAL_GIT" ]; then
|
|
4
|
+
REAL_GIT=$(which git 2>/dev/null)
|
|
5
|
+
fi
|
|
6
|
+
if [ -z "$REAL_GIT" ]; then
|
|
7
|
+
echo "ERROR: git not found" >&2
|
|
8
|
+
exit 1
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
is_branch_delete() {
|
|
12
|
+
local args=("$@")
|
|
13
|
+
if [ ${#args[@]} -lt 3 ]; then
|
|
14
|
+
return 1
|
|
15
|
+
fi
|
|
16
|
+
if [ "${args[0]}" != "branch" ]; then
|
|
17
|
+
return 1
|
|
18
|
+
fi
|
|
19
|
+
local i
|
|
20
|
+
for ((i=1; i<${#args[@]}-1; i++)); do
|
|
21
|
+
local flag="${args[$i]}"
|
|
22
|
+
if [ "$flag" = "-d" ] || [ "$flag" = "-D" ] || [ "$flag" = "--delete" ]; then
|
|
23
|
+
local next_idx=$((i+1))
|
|
24
|
+
local next="${args[$next_idx]}"
|
|
25
|
+
if [ "${next:0:1}" != "-" ]; then
|
|
26
|
+
echo "$next"
|
|
27
|
+
return 0
|
|
28
|
+
fi
|
|
29
|
+
fi
|
|
30
|
+
done
|
|
31
|
+
return 1
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
BRANCH=$(is_branch_delete "$@")
|
|
35
|
+
if [ $? -eq 0 ] && [ -n "$BRANCH" ]; then
|
|
36
|
+
CONFIG_PATH="$HOME/.exeggutor.json"
|
|
37
|
+
PORT=17492
|
|
38
|
+
TOKEN=""
|
|
39
|
+
if [ -f "$CONFIG_PATH" ]; then
|
|
40
|
+
CONF_PORT=$(grep -o '"backendPort"[[:space:]]*:[[:space:]]*[0-9]*' "$CONFIG_PATH" | grep -o '[0-9]*')
|
|
41
|
+
if [ -n "$CONF_PORT" ]; then
|
|
42
|
+
PORT=$CONF_PORT
|
|
43
|
+
fi
|
|
44
|
+
CONF_TOKEN=$(grep -o '"authToken"[[:space:]]*:[[:space:]]*"[^"]*"' "$CONFIG_PATH" | head -n 1 | cut -d '"' -f 4)
|
|
45
|
+
if [ -n "$CONF_TOKEN" ]; then
|
|
46
|
+
TOKEN=$CONF_TOKEN
|
|
47
|
+
fi
|
|
48
|
+
fi
|
|
49
|
+
RESPONSE=$(curl -sf -H "Authorization: Bearer $TOKEN" "http://localhost:$PORT/api/branches/in-use?name=$BRANCH" 2>/dev/null)
|
|
50
|
+
if [ $? -ne 0 ]; then
|
|
51
|
+
echo "ERROR: Cannot verify branch status. Exeggutor backend must be running. Branch deletion blocked for safety." >&2
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
54
|
+
if echo "$RESPONSE" | grep -q '"inUse":true'; then
|
|
55
|
+
echo "ERROR: Branch '$BRANCH' is in use by an active Exeggutor terminal. Switch the tab to another branch or close it first." >&2
|
|
56
|
+
exit 1
|
|
57
|
+
fi
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
exec "$REAL_GIT" "$@"
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Runtime.InteropServices;
|
|
3
|
+
using System.Threading;
|
|
4
|
+
|
|
5
|
+
// Native folder picker using IFileOpenDialog COM interface with FOS_PICKFOLDERS.
|
|
6
|
+
// Shows the modern Windows folder selection dialog available since Vista.
|
|
7
|
+
// Runs on its own STA thread to ensure proper COM apartment initialization.
|
|
8
|
+
// Exit codes:
|
|
9
|
+
// 0 = Success, selected path printed to stdout.
|
|
10
|
+
// 1 = Error (message printed to stderr).
|
|
11
|
+
// 2 = User cancelled (no output).
|
|
12
|
+
|
|
13
|
+
[ComImport, Guid("DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7")]
|
|
14
|
+
class FileOpenDialogRCW { }
|
|
15
|
+
|
|
16
|
+
[ComImport, Guid("d57c7288-d4ad-4768-be02-9d969532d960")]
|
|
17
|
+
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
18
|
+
interface IFileOpenDialog
|
|
19
|
+
{
|
|
20
|
+
[PreserveSig] int Show(IntPtr parent); // IModalWindow
|
|
21
|
+
[PreserveSig] int SetFileTypes();
|
|
22
|
+
[PreserveSig] int SetFileTypeIndex(int iFileType);
|
|
23
|
+
[PreserveSig] int GetFileTypeIndex(out int piFileType);
|
|
24
|
+
[PreserveSig] int Advise();
|
|
25
|
+
[PreserveSig] int Unadvise();
|
|
26
|
+
[PreserveSig] int SetOptions(uint fos);
|
|
27
|
+
[PreserveSig] int GetOptions(out uint pfos);
|
|
28
|
+
[PreserveSig] int SetDefaultFolder([MarshalAs(UnmanagedType.Interface)] IShellItem psi);
|
|
29
|
+
[PreserveSig] int SetFolder([MarshalAs(UnmanagedType.Interface)] IShellItem psi);
|
|
30
|
+
[PreserveSig] int GetFolder([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
|
|
31
|
+
[PreserveSig] int GetCurrentSelection([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
|
|
32
|
+
[PreserveSig] int SetFileName([MarshalAs(UnmanagedType.LPWStr)] string pszName);
|
|
33
|
+
[PreserveSig] int GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName);
|
|
34
|
+
[PreserveSig] int SetTitle([MarshalAs(UnmanagedType.LPWStr)] string pszTitle);
|
|
35
|
+
[PreserveSig] int SetOkButtonLabel([MarshalAs(UnmanagedType.LPWStr)] string pszText);
|
|
36
|
+
[PreserveSig] int SetFileNameLabel([MarshalAs(UnmanagedType.LPWStr)] string pszLabel);
|
|
37
|
+
[PreserveSig] int GetResult([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
|
|
38
|
+
[PreserveSig] int AddPlace([MarshalAs(UnmanagedType.Interface)] IShellItem psi, int alignment);
|
|
39
|
+
[PreserveSig] int SetDefaultExtension([MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension);
|
|
40
|
+
[PreserveSig] int Close(int hr);
|
|
41
|
+
[PreserveSig] int SetClientGuid();
|
|
42
|
+
[PreserveSig] int ClearClientData();
|
|
43
|
+
[PreserveSig] int SetFilter([MarshalAs(UnmanagedType.IUnknown)] object pFilter);
|
|
44
|
+
[PreserveSig] int GetResults([MarshalAs(UnmanagedType.Interface)] out IShellItemArray ppenum);
|
|
45
|
+
[PreserveSig] int GetSelectedItems([MarshalAs(UnmanagedType.IUnknown)] out object ppsai);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
[ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE")]
|
|
49
|
+
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
50
|
+
interface IShellItem
|
|
51
|
+
{
|
|
52
|
+
[PreserveSig] int BindToHandler();
|
|
53
|
+
[PreserveSig] int GetParent();
|
|
54
|
+
[PreserveSig] int GetDisplayName(uint sigdnName, [MarshalAs(UnmanagedType.LPWStr)] out string ppszName);
|
|
55
|
+
[PreserveSig] int GetAttributes();
|
|
56
|
+
[PreserveSig] int Compare();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
[ComImport, Guid("b63ea76d-1f85-456f-a19c-48159efa858b")]
|
|
60
|
+
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
61
|
+
interface IShellItemArray
|
|
62
|
+
{
|
|
63
|
+
[PreserveSig] int BindToHandler();
|
|
64
|
+
[PreserveSig] int GetPropertyStore();
|
|
65
|
+
[PreserveSig] int GetPropertyDescriptionList();
|
|
66
|
+
[PreserveSig] int GetAttributes();
|
|
67
|
+
[PreserveSig] int GetCount(out int pdwNumItems);
|
|
68
|
+
[PreserveSig] int GetItemAt(int dwIndex, [MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
|
|
69
|
+
[PreserveSig] int EnumItems();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class FolderPicker
|
|
73
|
+
{
|
|
74
|
+
[DllImport("user32.dll")]
|
|
75
|
+
static extern IntPtr GetForegroundWindow();
|
|
76
|
+
|
|
77
|
+
const uint FOS_PICKFOLDERS = 0x20;
|
|
78
|
+
const uint SIGDN_FILESYSPATH = 0x80058000;
|
|
79
|
+
const int ERROR_CANCELLED = unchecked((int)0x800704C7);
|
|
80
|
+
|
|
81
|
+
// Entry point; creates the folder picker COM dialog on an STA thread then exits with the result code.
|
|
82
|
+
static void Main()
|
|
83
|
+
{
|
|
84
|
+
string selectedPath = null; // The folder path selected by the user, or null on cancel/error.
|
|
85
|
+
Exception caughtError = null; // Any exception caught during dialog display.
|
|
86
|
+
|
|
87
|
+
Thread staThread = new Thread(() => // STA thread required by COM for the folder picker dialog.
|
|
88
|
+
{
|
|
89
|
+
try
|
|
90
|
+
{
|
|
91
|
+
IFileOpenDialog dialog = (IFileOpenDialog)new FileOpenDialogRCW(); // The IFileOpenDialog COM interface instance.
|
|
92
|
+
|
|
93
|
+
dialog.SetOptions(FOS_PICKFOLDERS);
|
|
94
|
+
dialog.SetTitle("Select Workspace Folder");
|
|
95
|
+
|
|
96
|
+
IntPtr parentHwnd = GetForegroundWindow(); // Handle of the browser window that triggered the dialog.
|
|
97
|
+
int hr = dialog.Show(parentHwnd); // HRESULT from the dialog; 0 = OK, ERROR_CANCELLED = user cancelled.
|
|
98
|
+
if (hr == ERROR_CANCELLED)
|
|
99
|
+
{
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (hr != 0)
|
|
103
|
+
{
|
|
104
|
+
Marshal.ThrowExceptionForHR(hr);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
IShellItem result; // The IShellItem representing the selected folder.
|
|
108
|
+
dialog.GetResult(out result);
|
|
109
|
+
|
|
110
|
+
string path; // The filesystem path string extracted from the shell item.
|
|
111
|
+
result.GetDisplayName(SIGDN_FILESYSPATH, out path);
|
|
112
|
+
selectedPath = path;
|
|
113
|
+
}
|
|
114
|
+
catch (Exception ex)
|
|
115
|
+
{
|
|
116
|
+
caughtError = ex;
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
staThread.SetApartmentState(ApartmentState.STA);
|
|
121
|
+
staThread.Start();
|
|
122
|
+
staThread.Join();
|
|
123
|
+
|
|
124
|
+
if (caughtError != null)
|
|
125
|
+
{
|
|
126
|
+
Console.Error.Write("ERROR: " + caughtError.Message);
|
|
127
|
+
Environment.Exit(1);
|
|
128
|
+
}
|
|
129
|
+
if (selectedPath != null)
|
|
130
|
+
{
|
|
131
|
+
Console.Write(selectedPath);
|
|
132
|
+
Environment.Exit(0);
|
|
133
|
+
}
|
|
134
|
+
else
|
|
135
|
+
{
|
|
136
|
+
Environment.Exit(2);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "backend",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"start": "node dist/index.js",
|
|
10
|
+
"compile-picker": "node scripts/compile-picker.js",
|
|
11
|
+
"postinstall": "node scripts/compile-picker.js"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@fastify/cors": "^9.0.1",
|
|
15
|
+
"@fastify/static": "^7.0.4",
|
|
16
|
+
"@fastify/websocket": "^10.0.1",
|
|
17
|
+
"fastify": "^4.28.1",
|
|
18
|
+
"node-pty": "^1.1.0-beta21"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^20.14.9",
|
|
22
|
+
"ts-node-dev": "^2.0.0",
|
|
23
|
+
"typescript": "^5.5.2"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const { execFileSync } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
// FolderPicker is Windows-only; skip silently on other platforms.
|
|
6
|
+
if (process.platform !== 'win32') {
|
|
7
|
+
process.exit(0);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const SOURCE = path.join(__dirname, '..', 'native', 'FolderPicker.cs'); // Path to the C# source file.
|
|
11
|
+
const OUTPUT = path.join(__dirname, '..', 'bin', 'FolderPicker.exe'); // Output path for the compiled binary.
|
|
12
|
+
|
|
13
|
+
const windir = process.env.windir || 'C:\\Windows'; // Windows directory, defaulting to C:\Windows.
|
|
14
|
+
const CSC_PATHS = [ // Candidate paths for the C# compiler executable.
|
|
15
|
+
path.join(windir, 'Microsoft.NET', 'Framework64', 'v4.0.30319', 'csc.exe'),
|
|
16
|
+
path.join(windir, 'Microsoft.NET', 'Framework', 'v4.0.30319', 'csc.exe'),
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
// Locates the C# compiler executable from the known .NET Framework SDK paths.
|
|
20
|
+
function findCsc() {
|
|
21
|
+
return CSC_PATHS.find(p => fs.existsSync(p));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Compiles FolderPicker.cs into a winexe binary using the .NET Framework C# compiler.
|
|
25
|
+
function compile() {
|
|
26
|
+
const csc = findCsc(); // The located csc.exe path, or undefined if not found.
|
|
27
|
+
|
|
28
|
+
if (!csc) {
|
|
29
|
+
console.error(
|
|
30
|
+
'Warning: C# compiler (csc.exe) not found.\n' +
|
|
31
|
+
'The Browse folder button will not be available.\n' +
|
|
32
|
+
'Install .NET Framework SDK or use Visual Studio Build Tools to enable it.\n' +
|
|
33
|
+
'You can still type workspace paths manually.'
|
|
34
|
+
);
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const outDir = path.dirname(OUTPUT); // Parent directory for the output binary.
|
|
39
|
+
|
|
40
|
+
if (!fs.existsSync(outDir)) {
|
|
41
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
execFileSync(csc, [
|
|
46
|
+
'/nologo',
|
|
47
|
+
'/target:winexe',
|
|
48
|
+
'/platform:anycpu',
|
|
49
|
+
`/out:${OUTPUT}`,
|
|
50
|
+
SOURCE,
|
|
51
|
+
], { stdio: 'inherit' });
|
|
52
|
+
|
|
53
|
+
console.log(`Folder picker compiled successfully: ${OUTPUT}`);
|
|
54
|
+
return true;
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error('Failed to compile folder picker:', err.message);
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
compile();
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
function global:git {
|
|
2
|
+
param(
|
|
3
|
+
[Parameter(ValueFromRemainingArguments = $true)]
|
|
4
|
+
[string[]]$UserArgs
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
$isBranchDelete = $false
|
|
8
|
+
$targetBranch = $null
|
|
9
|
+
|
|
10
|
+
if ($UserArgs.Count -ge 3 -and $UserArgs[0] -eq 'branch') {
|
|
11
|
+
for ($i = 1; $i -lt $UserArgs.Count - 1; $i++) {
|
|
12
|
+
$arg = $UserArgs[$i]
|
|
13
|
+
if ($arg -eq '-d' -or $arg -eq '-D' -or $arg -eq '--delete') {
|
|
14
|
+
$nextIdx = $i + 1
|
|
15
|
+
if ($nextIdx -lt $UserArgs.Count -and $UserArgs[$nextIdx] -notlike '-*') {
|
|
16
|
+
$isBranchDelete = $true
|
|
17
|
+
$targetBranch = $UserArgs[$nextIdx]
|
|
18
|
+
break
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if ($isBranchDelete -and $targetBranch) {
|
|
25
|
+
try {
|
|
26
|
+
$configPath = Join-Path $env:USERPROFILE ".exeggutor.json"
|
|
27
|
+
$port = 17492
|
|
28
|
+
$token = ""
|
|
29
|
+
if (Test-Path $configPath) {
|
|
30
|
+
$config = Get-Content $configPath -Raw | ConvertFrom-Json
|
|
31
|
+
if ($config.backendPort) { $port = $config.backendPort }
|
|
32
|
+
if ($config.authToken) { $token = $config.authToken }
|
|
33
|
+
}
|
|
34
|
+
$headers = @{ Authorization = "Bearer $token" }
|
|
35
|
+
$response = Invoke-RestMethod -Uri "http://localhost:$port/api/branches/in-use?name=$targetBranch" -Headers $headers -ErrorAction Stop
|
|
36
|
+
if ($response.inUse -eq $true) {
|
|
37
|
+
Write-Error "Branch '$targetBranch' is in use by an active Exeggutor terminal. Switch the tab to another branch or close it first."
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
Write-Error "Cannot verify branch status. Exeggutor backend must be running. Branch deletion blocked for safety."
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
& "git.exe" @UserArgs
|
|
48
|
+
}
|