@tsuzuku/arise 0.2.0

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/lib/context.js ADDED
@@ -0,0 +1,113 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { execSync, spawnSync } = require('child_process');
4
+
5
+ function createContext({
6
+ worktreePath,
7
+ repoRoot,
8
+ bareRepo,
9
+ branch,
10
+ source,
11
+ flags = {},
12
+ preset = {},
13
+ config = {},
14
+ }) {
15
+ const ctx = {
16
+ worktreePath,
17
+ repoRoot,
18
+ bareRepo,
19
+ branch,
20
+ source,
21
+ flags,
22
+ preset,
23
+ config,
24
+
25
+ log(msg) {
26
+ console.log(msg);
27
+ },
28
+
29
+ warn(msg) {
30
+ console.warn(msg);
31
+ },
32
+
33
+ error(msg) {
34
+ console.error(msg);
35
+ },
36
+
37
+ exec(command, options = {}) {
38
+ const cwd = options.cwd || worktreePath;
39
+ const shell = options.shell || '/bin/bash';
40
+ return execSync(command, {
41
+ cwd,
42
+ stdio: 'inherit',
43
+ shell,
44
+ ...options,
45
+ });
46
+ },
47
+
48
+ spawn(command, args = [], options = {}) {
49
+ const cwd = options.cwd || worktreePath;
50
+ const shell = options.shell !== undefined ? options.shell : true;
51
+ return spawnSync(command, args, {
52
+ cwd,
53
+ stdio: 'inherit',
54
+ shell,
55
+ ...options,
56
+ });
57
+ },
58
+
59
+ copyFile(src, dst) {
60
+ const resolvedDst = path.isAbsolute(dst) ? dst : path.join(worktreePath, dst);
61
+ if (fs.existsSync(src)) {
62
+ console.log(`Copying from "${src}" to "${resolvedDst}"...`);
63
+ const dstDir = path.dirname(resolvedDst);
64
+ if (!fs.existsSync(dstDir)) {
65
+ fs.mkdirSync(dstDir, { recursive: true });
66
+ }
67
+ fs.copyFileSync(src, resolvedDst);
68
+ return true;
69
+ } else {
70
+ console.warn(`Source file "${src}" does not exist to copy.`);
71
+ return false;
72
+ }
73
+ },
74
+
75
+ copyFromRoot(relativeSrc, relativeDst = relativeSrc) {
76
+ if (!repoRoot) {
77
+ console.warn(`Cannot copy from root: no repo root identified.`);
78
+ return false;
79
+ }
80
+ const src = path.join(repoRoot, relativeSrc);
81
+ const dst = path.join(worktreePath, relativeDst);
82
+ return ctx.copyFile(src, dst);
83
+ },
84
+
85
+ setSymlink(symlinkPath, target = worktreePath) {
86
+ console.log(`==> Updating symlink "${symlinkPath}" -> "${target}"...`);
87
+ try {
88
+ if (fs.existsSync(symlinkPath) || fs.lstatSync(symlinkPath).isSymbolicLink()) {
89
+ const lstat = fs.lstatSync(symlinkPath);
90
+ if (lstat.isSymbolicLink()) {
91
+ fs.unlinkSync(symlinkPath);
92
+ } else {
93
+ const backupPath = `${symlinkPath}_BAK_${Date.now()}`;
94
+ console.warn(`==> Warning: "${symlinkPath}" is a directory, not a symlink. Backing up to "${backupPath}"`);
95
+ fs.renameSync(symlinkPath, backupPath);
96
+ }
97
+ }
98
+ fs.symlinkSync(target, symlinkPath);
99
+ console.log(`==> Symlink updated: ${symlinkPath} -> ${target}`);
100
+ return true;
101
+ } catch (err) {
102
+ console.error(`==> Failed to update symlink: ${err.message}`);
103
+ return false;
104
+ }
105
+ },
106
+ };
107
+
108
+ return ctx;
109
+ }
110
+
111
+ module.exports = {
112
+ createContext,
113
+ };
package/lib/git.js ADDED
@@ -0,0 +1,306 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { execSync } = require('child_process');
4
+
5
+ function isBareRepo(repoPath) {
6
+ if (!repoPath || !fs.existsSync(repoPath)) return false;
7
+ try {
8
+ const isBare = execSync(`git --git-dir="${repoPath}" rev-parse --is-bare-repository`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
9
+ return isBare === 'true';
10
+ } catch (e) {
11
+ return false;
12
+ }
13
+ }
14
+
15
+ function getGitPrefix(options = {}) {
16
+ if (options.bareRepo) {
17
+ return `git --git-dir="${options.bareRepo}"`;
18
+ }
19
+ if (options.repoDir) {
20
+ const dotBare = path.join(options.repoDir, '.bare');
21
+ if (fs.existsSync(dotBare)) {
22
+ return `git --git-dir="${dotBare}"`;
23
+ }
24
+ if (isBareRepo(options.repoDir)) {
25
+ return `git --git-dir="${options.repoDir}"`;
26
+ }
27
+ return `git -C "${options.repoDir}"`;
28
+ }
29
+ return 'git';
30
+ }
31
+
32
+ function getRepoRootDir(cwd = process.cwd(), bareRepo = null) {
33
+ if (bareRepo && fs.existsSync(bareRepo)) {
34
+ return bareRepo;
35
+ }
36
+ const dotBare = path.join(cwd, '.bare');
37
+ if (fs.existsSync(dotBare)) {
38
+ return cwd;
39
+ }
40
+ try {
41
+ const commonDir = execSync('git rev-parse --git-common-dir', { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
42
+ const absoluteCommonDir = path.isAbsolute(commonDir) ? commonDir : path.resolve(cwd, commonDir);
43
+ if (isBareRepo(absoluteCommonDir)) {
44
+ return absoluteCommonDir;
45
+ }
46
+ return path.dirname(absoluteCommonDir);
47
+ } catch (err) {
48
+ return cwd;
49
+ }
50
+ }
51
+
52
+ function getWorktrees(options = {}) {
53
+ const gitCmd = getGitPrefix(options);
54
+ try {
55
+ const output = execSync(`${gitCmd} worktree list --porcelain`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
56
+ const worktrees = [];
57
+ const entries = output.trim().split('\n\n');
58
+ for (const entry of entries) {
59
+ if (!entry.trim()) continue;
60
+ const lines = entry.split('\n');
61
+ let wtPath = null;
62
+ let wtBranch = null;
63
+ let isBare = false;
64
+ let isDetached = false;
65
+ for (const line of lines) {
66
+ if (line.startsWith('worktree ')) {
67
+ wtPath = line.substring('worktree '.length).trim();
68
+ } else if (line.startsWith('branch ')) {
69
+ const ref = line.substring('branch '.length).trim();
70
+ wtBranch = ref.replace(/^refs\/heads\//, '');
71
+ } else if (line === 'bare') {
72
+ isBare = true;
73
+ } else if (line === 'detached') {
74
+ isDetached = true;
75
+ }
76
+ }
77
+ if (wtPath) {
78
+ worktrees.push({ path: wtPath, branch: wtBranch, isBare, isDetached });
79
+ }
80
+ }
81
+ return worktrees;
82
+ } catch (err) {
83
+ // Fallback to standard worktree list if porcelain fails
84
+ try {
85
+ const output = execSync(`${gitCmd} worktree list`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
86
+ return output.trim().split('\n').filter(Boolean).map(line => {
87
+ const parts = line.split(/\s+/);
88
+ const wtPath = parts[0];
89
+ const branchMatch = line.match(/\[(.*?)\]/);
90
+ const wtBranch = branchMatch ? branchMatch[1] : null;
91
+ return { path: wtPath, branch: wtBranch, isBare: line.includes('(bare)'), isDetached: line.includes('(detached)') };
92
+ });
93
+ } catch (e) {
94
+ return [];
95
+ }
96
+ }
97
+ }
98
+
99
+ function fetchOrigin(options = {}) {
100
+ const gitCmd = getGitPrefix(options);
101
+ try {
102
+ const remotes = execSync(`${gitCmd} remote`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim().split(/\s+/);
103
+ if (!remotes.includes('origin')) return;
104
+
105
+ // In bare repos, remote.origin.fetch is often not set by default.
106
+ // Setting or fetching with the standard refspec ensures refs/remotes/origin/* are populated.
107
+ try {
108
+ const fetchRefspec = execSync(`${gitCmd} config --get remote.origin.fetch`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
109
+ if (!fetchRefspec) {
110
+ execSync(`${gitCmd} config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"`, { stdio: 'ignore' });
111
+ }
112
+ } catch (e) {
113
+ try {
114
+ execSync(`${gitCmd} config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"`, { stdio: 'ignore' });
115
+ } catch (err) {}
116
+ }
117
+
118
+ execSync(`${gitCmd} fetch origin`, { stdio: 'inherit' });
119
+ } catch (e) {
120
+ // Network or remote fetch error - continue gracefully with local refs
121
+ }
122
+ }
123
+
124
+ function branchExistsLocally(branch, options = {}) {
125
+ const gitCmd = getGitPrefix(options);
126
+ try {
127
+ execSync(`${gitCmd} rev-parse --verify "refs/heads/${branch}"`, { stdio: 'ignore' });
128
+ return true;
129
+ } catch (e) {
130
+ try {
131
+ execSync(`${gitCmd} rev-parse --verify "${branch}"`, { stdio: 'ignore' });
132
+ return true;
133
+ } catch (err) {
134
+ try {
135
+ const list = execSync(`${gitCmd} branch --list "${branch}"`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
136
+ return list !== '';
137
+ } catch (err2) {
138
+ return false;
139
+ }
140
+ }
141
+ }
142
+ }
143
+
144
+ function branchExistsRemotely(branch, options = {}) {
145
+ const gitCmd = getGitPrefix(options);
146
+ try {
147
+ execSync(`${gitCmd} rev-parse --verify "refs/remotes/origin/${branch}"`, { stdio: 'ignore' });
148
+ return true;
149
+ } catch (e) {
150
+ try {
151
+ execSync(`${gitCmd} rev-parse --verify "origin/${branch}"`, { stdio: 'ignore' });
152
+ return true;
153
+ } catch (err) {
154
+ try {
155
+ const remoteList = execSync(`${gitCmd} branch -r --list "origin/${branch}"`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
156
+ if (remoteList) return true;
157
+ const lsRemote = execSync(`${gitCmd} ls-remote --heads origin "${branch}"`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
158
+ return Boolean(lsRemote);
159
+ } catch (err2) {
160
+ return false;
161
+ }
162
+ }
163
+ }
164
+ }
165
+
166
+ function createWorktree({ worktreePath, branch, source = 'develop', repoDir, bareRepo }) {
167
+ const options = { repoDir, bareRepo };
168
+ const gitCmd = getGitPrefix(options);
169
+
170
+ console.log('Fetching latest from origin...');
171
+ fetchOrigin(options);
172
+
173
+ const localExists = branchExistsLocally(branch, options);
174
+ const remoteExists = branchExistsRemotely(branch, options);
175
+
176
+ if (localExists) {
177
+ console.log(`Branch "${branch}" exists locally. Adding worktree...`);
178
+ execSync(`${gitCmd} worktree add "${worktreePath}" "${branch}"`, { stdio: 'inherit' });
179
+ } else if (remoteExists) {
180
+ console.log(`Branch "${branch}" exists on remote. Checking out and adding worktree...`);
181
+ let startPoint = `origin/${branch}`;
182
+ try {
183
+ execSync(`${gitCmd} rev-parse --verify "${startPoint}"`, { stdio: 'ignore' });
184
+ } catch (e) {
185
+ startPoint = branch;
186
+ }
187
+ if (bareRepo) {
188
+ execSync(`${gitCmd} branch --track "${branch}" "${startPoint}"`, { stdio: 'inherit' });
189
+ execSync(`${gitCmd} worktree add "${worktreePath}" "${branch}"`, { stdio: 'inherit' });
190
+ } else {
191
+ execSync(`${gitCmd} worktree add "${worktreePath}" -b "${branch}" "${startPoint}"`, { stdio: 'inherit' });
192
+ }
193
+ } else {
194
+ // Creating a brand new branch off source
195
+ let startPoint = null;
196
+ try {
197
+ execSync(`${gitCmd} rev-parse --verify "origin/${source}"`, { stdio: 'ignore' });
198
+ startPoint = `origin/${source}`;
199
+ } catch (e) {
200
+ try {
201
+ execSync(`${gitCmd} rev-parse --verify "${source}"`, { stdio: 'ignore' });
202
+ startPoint = source;
203
+ } catch (err) {
204
+ throw new Error(`Cannot create branch "${branch}" off "${source}": Base branch or ref "${source}" does not exist locally or on remote.`);
205
+ }
206
+ }
207
+
208
+ console.log(`Creating branch "${branch}" off "${startPoint}" and adding worktree...`);
209
+ if (bareRepo) {
210
+ execSync(`${gitCmd} branch --no-track "${branch}" "${startPoint}"`, { stdio: 'inherit' });
211
+ execSync(`${gitCmd} worktree add "${worktreePath}" "${branch}"`, { stdio: 'inherit' });
212
+ } else {
213
+ execSync(`${gitCmd} worktree add "${worktreePath}" -b "${branch}" "${startPoint}"`, { stdio: 'inherit' });
214
+ }
215
+ }
216
+ }
217
+
218
+ function syncPrimaryBranch({ branch, worktreePath, repoDir, bareRepo, protectedBranches = [] }) {
219
+ if (!protectedBranches.includes(branch)) return;
220
+
221
+ console.log(`==> Primary branch "${branch}" detected. Checking for local changes...`);
222
+ const options = { repoDir, bareRepo };
223
+ const gitCmd = getGitPrefix(options);
224
+
225
+ try {
226
+ const statusCmd = bareRepo
227
+ ? `${gitCmd} --work-tree="${worktreePath}" status --porcelain`
228
+ : `git -C "${worktreePath}" status --porcelain`;
229
+
230
+ const isDirty = execSync(statusCmd, { encoding: 'utf8' }).trim() !== '';
231
+
232
+ if (isDirty) {
233
+ console.warn(`==> WARNING: Local changes detected in ${branch}. Skipping hard reset to origin/${branch} to prevent data loss.`);
234
+ console.warn(`==> Please commit, stash, or discard your changes if you want to sync with origin.`);
235
+ } else {
236
+ console.log(`==> Syncing ${branch} with origin/${branch}...`);
237
+ fetchOrigin(options);
238
+ const resetCmd = bareRepo
239
+ ? `${gitCmd} --work-tree="${worktreePath}" reset --hard origin/${branch}`
240
+ : `git -C "${worktreePath}" reset --hard origin/${branch}`;
241
+ execSync(resetCmd, { stdio: 'inherit' });
242
+ }
243
+ } catch (err) {
244
+ console.warn(`==> Warning syncing primary branch: ${err.message}`);
245
+ }
246
+ }
247
+
248
+ function removeWorktree(worktreePath, options = {}) {
249
+ const gitCmd = getGitPrefix(options);
250
+ const forceFlag = options.force ? '--force' : '';
251
+ try {
252
+ execSync(`${gitCmd} worktree remove ${forceFlag} "${worktreePath}"`, { stdio: 'inherit' });
253
+ console.log(`Successfully removed git worktree at "${worktreePath}".`);
254
+ } catch (err) {
255
+ console.warn(`"git worktree remove" standard attempt failed. Trying force removal...`);
256
+ try {
257
+ execSync(`${gitCmd} worktree remove --force "${worktreePath}"`, { stdio: 'inherit' });
258
+ console.log(`Successfully force-removed git worktree.`);
259
+ } catch (e) {
260
+ console.warn(`Git worktree force remove warning: ${e.message}`);
261
+ }
262
+ }
263
+ }
264
+
265
+ function pruneWorktrees(options = {}) {
266
+ const gitCmd = getGitPrefix(options);
267
+ try {
268
+ execSync(`${gitCmd} worktree prune`, { stdio: 'ignore' });
269
+ } catch (e) {}
270
+ }
271
+
272
+ function deleteLocalBranch(branch, options = {}) {
273
+ const gitCmd = getGitPrefix(options);
274
+ try {
275
+ execSync(`${gitCmd} branch -D "${branch}"`, { stdio: 'inherit' });
276
+ console.log(`Deleted local branch "${branch}".`);
277
+ } catch (err) {
278
+ console.error(`Failed to delete local branch "${branch}": ${err.message}`);
279
+ }
280
+ }
281
+
282
+ function deleteRemoteBranch(branch, options = {}) {
283
+ const gitCmd = getGitPrefix(options);
284
+ try {
285
+ execSync(`${gitCmd} push origin --delete "${branch}"`, { stdio: 'inherit' });
286
+ console.log(`Deleted remote branch "${branch}" from origin.`);
287
+ } catch (err) {
288
+ console.error(`Failed to delete remote branch "${branch}": ${err.message}`);
289
+ }
290
+ }
291
+
292
+ module.exports = {
293
+ getGitPrefix,
294
+ getRepoRootDir,
295
+ getWorktrees,
296
+ fetchOrigin,
297
+ branchExistsLocally,
298
+ branchExistsRemotely,
299
+ createWorktree,
300
+ syncPrimaryBranch,
301
+ removeWorktree,
302
+ pruneWorktrees,
303
+ deleteLocalBranch,
304
+ deleteRemoteBranch,
305
+ isBareRepo,
306
+ };
package/lib/herdr.js ADDED
@@ -0,0 +1,217 @@
1
+ const { execSync, spawnSync } = require('child_process');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const readline = require('readline');
5
+
6
+ function isCommandAvailable(cmd) {
7
+ try {
8
+ const checkCmd = process.platform === 'win32' ? `where ${cmd}` : `command -v ${cmd}`;
9
+ execSync(checkCmd, { stdio: 'ignore' });
10
+ return true;
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
15
+
16
+ function isHerdrInstalled() {
17
+ const localBin = path.join(os.homedir(), '.local', 'bin');
18
+ if (process.env.PATH && !process.env.PATH.split(path.delimiter).includes(localBin)) {
19
+ process.env.PATH = `${localBin}${path.delimiter}${process.env.PATH}`;
20
+ }
21
+ return isCommandAvailable('herdr');
22
+ }
23
+
24
+ function getRecommendedInstallCommand() {
25
+ if (process.platform === 'darwin' && isCommandAvailable('brew')) {
26
+ return {
27
+ name: 'Homebrew',
28
+ cmd: 'brew install herdr',
29
+ docsUrl: 'https://herdr.dev/docs/install/#install-with-homebrew',
30
+ };
31
+ }
32
+ if (isCommandAvailable('mise')) {
33
+ return {
34
+ name: 'mise',
35
+ cmd: 'mise use -g herdr',
36
+ docsUrl: 'https://herdr.dev/docs/install/#install-with-mise',
37
+ };
38
+ }
39
+ if (process.platform === 'win32') {
40
+ return {
41
+ name: 'PowerShell Installer',
42
+ cmd: 'powershell -ExecutionPolicy Bypass -c "irm https://herdr.dev/install.ps1 | iex"',
43
+ docsUrl: 'https://herdr.dev/docs/install/',
44
+ };
45
+ }
46
+ return {
47
+ name: 'Official Install Script',
48
+ cmd: 'curl -fsSL https://herdr.dev/install.sh | sh',
49
+ docsUrl: 'https://herdr.dev/docs/install/',
50
+ };
51
+ }
52
+
53
+ function promptUser(question) {
54
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
55
+ return new Promise((resolve) => {
56
+ rl.question(question, (answer) => {
57
+ rl.close();
58
+ resolve(answer.trim());
59
+ });
60
+ });
61
+ }
62
+
63
+ async function ensureHerdrInstalled(options = {}) {
64
+ if (isHerdrInstalled()) {
65
+ return true;
66
+ }
67
+
68
+ const { name, cmd, docsUrl } = getRecommendedInstallCommand();
69
+ const autoConfirm = Boolean(options.yes);
70
+
71
+ if (!autoConfirm && (!process.stdin.isTTY || process.env.CI)) {
72
+ console.error(`\n[arise] Error: "herdr" is required to manage terminal workspaces but was not found in PATH.`);
73
+ console.error(`Please install Herdr before running arise:\n ${cmd}\n`);
74
+ console.error(`Docs: ${docsUrl || 'https://herdr.dev/docs/install/'}`);
75
+ process.exit(1);
76
+ }
77
+
78
+ if (!autoConfirm) {
79
+ console.log(`\n[arise] "herdr" is required to manage terminal workspaces but was not found in PATH.`);
80
+ const answer = await promptUser(`Would you like arise to install Herdr now via ${name} (\`${cmd}\`)? [Y/n]: `);
81
+ if (answer && !/^(y|yes)$/i.test(answer)) {
82
+ console.error('\nCannot proceed without Herdr. Aborting.');
83
+ process.exit(1);
84
+ }
85
+ }
86
+
87
+ console.log(`\n==> Installing Herdr via ${name}...`);
88
+ const result = spawnSync(cmd, { stdio: 'inherit', shell: true });
89
+
90
+ if (result.status !== 0 || !isHerdrInstalled()) {
91
+ console.error(`\n[arise] Failed to automatically install Herdr.`);
92
+ console.error(`Please visit https://herdr.dev/docs/install/ to install manually.`);
93
+ process.exit(1);
94
+ }
95
+
96
+ console.log(`[arise] Herdr installed successfully!\n`);
97
+ return true;
98
+ }
99
+
100
+ function parseJsonFromOutput(output) {
101
+ if (!output) return null;
102
+ const line = output.split('\n').find(l => l.trim().startsWith('{'));
103
+ if (line) {
104
+ try {
105
+ return JSON.parse(line);
106
+ } catch (e) {}
107
+ }
108
+ try {
109
+ return JSON.parse(output);
110
+ } catch (e) {
111
+ return null;
112
+ }
113
+ }
114
+
115
+ function listWorkspaces() {
116
+ try {
117
+ const listOutput = execSync('herdr workspace list', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
118
+ const data = parseJsonFromOutput(listOutput);
119
+ return (data && data.result && data.result.workspaces) || [];
120
+ } catch (err) {
121
+ return [];
122
+ }
123
+ }
124
+
125
+ function createWorkspace({ label, cwd }) {
126
+ const wsOutput = execSync(`herdr workspace create --label "${label}" --cwd "${cwd}"`, { encoding: 'utf8' }).trim();
127
+ const res = parseJsonFromOutput(wsOutput);
128
+ if (!res || !res.result || !res.result.workspace) {
129
+ throw new Error(`Failed to create Herdr workspace "${label}". Raw output: ${wsOutput}`);
130
+ }
131
+ return {
132
+ workspaceId: res.result.workspace.workspace_id,
133
+ rootPaneId: res.result.root_pane ? res.result.root_pane.pane_id : null,
134
+ };
135
+ }
136
+
137
+ function closeWorkspace(workspaceId) {
138
+ try {
139
+ execSync(`herdr workspace close ${workspaceId}`, { stdio: 'ignore' });
140
+ return true;
141
+ } catch (err) {
142
+ return false;
143
+ }
144
+ }
145
+
146
+ function focusWorkspace(workspaceId) {
147
+ try {
148
+ execSync(`herdr workspace focus ${workspaceId}`, { stdio: 'ignore' });
149
+ } catch (e) {}
150
+ }
151
+
152
+ function splitPane({ paneId, direction = 'right', cwd, focus = false }) {
153
+ const focusFlag = focus ? '--focus' : '--no-focus';
154
+ const cwdFlag = cwd ? `--cwd "${cwd}"` : '';
155
+ const output = execSync(`herdr pane split --pane ${paneId} --direction ${direction} ${cwdFlag} ${focusFlag}`, { encoding: 'utf8' }).trim();
156
+ const res = parseJsonFromOutput(output);
157
+ if (!res || !res.result || !res.result.pane) {
158
+ throw new Error(`Failed to split pane ${paneId} direction ${direction}`);
159
+ }
160
+ return res.result.pane.pane_id;
161
+ }
162
+
163
+ function renamePane(paneId, name) {
164
+ try {
165
+ execSync(`herdr pane rename ${paneId} "${name}"`, { stdio: 'ignore' });
166
+ } catch (e) {}
167
+ }
168
+
169
+ function runInPane(paneId, command) {
170
+ if (!command) return;
171
+ try {
172
+ execSync(`herdr pane send-text ${paneId} "${command}"`, { stdio: 'ignore' });
173
+ execSync(`herdr pane send-keys ${paneId} enter`, { stdio: 'ignore' });
174
+ } catch (e) {
175
+ console.error(`Failed to execute command "${command}" in pane ${paneId}: ${e.message}`);
176
+ }
177
+ }
178
+
179
+ function closeWorkspacesMatching(matchTargets = []) {
180
+ const targets = matchTargets.filter(Boolean);
181
+ if (!targets.length) return;
182
+
183
+ const workspaces = listWorkspaces();
184
+ const matchingWorkspaces = workspaces.filter(w => targets.includes(w.label));
185
+
186
+ for (const ws of matchingWorkspaces) {
187
+ if (ws.workspace_id) {
188
+ console.log(`Closing Herdr workspace "${ws.label}" (ID: ${ws.workspace_id}) and terminating panes...`);
189
+ closeWorkspace(ws.workspace_id);
190
+ console.log(`Herdr workspace "${ws.label}" closed.`);
191
+ }
192
+ }
193
+ }
194
+
195
+ function attachOrSwitchSession(sessionName) {
196
+ if (process.env.HERDR_ENV === '1') {
197
+ console.log(`Already inside herdr. Workspace "${sessionName}" created and focused!`);
198
+ } else {
199
+ console.log(`Starting herdr session...`);
200
+ spawnSync('herdr', [], { stdio: 'inherit', shell: true });
201
+ }
202
+ }
203
+
204
+ module.exports = {
205
+ isHerdrInstalled,
206
+ getRecommendedInstallCommand,
207
+ ensureHerdrInstalled,
208
+ listWorkspaces,
209
+ createWorkspace,
210
+ closeWorkspace,
211
+ focusWorkspace,
212
+ splitPane,
213
+ renamePane,
214
+ runInPane,
215
+ closeWorkspacesMatching,
216
+ attachOrSwitchSession,
217
+ };