@tsuzuku/arise 1.0.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/README.md +120 -0
- package/arise.schema.json +112 -0
- package/bin/cli.js +8 -0
- package/index.js +36 -0
- package/lib/cli.js +118 -0
- package/lib/config.js +134 -0
- package/lib/context.js +113 -0
- package/lib/git.js +226 -0
- package/lib/herdr.js +117 -0
- package/lib/layout.js +85 -0
- package/lib/lifecycle/create.js +121 -0
- package/lib/lifecycle/nuke.js +211 -0
- package/lib/skill.js +200 -0
- package/package.json +40 -0
- package/presets/generic.js +33 -0
- package/presets/index.js +39 -0
- package/presets/laravel.js +115 -0
- package/presets/node.js +50 -0
- package/skills/arise/SKILL.md +86 -0
- package/types.d.ts +151 -0
- package/worktree.schema.json +112 -0
package/lib/git.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
function getGitPrefix(options = {}) {
|
|
6
|
+
if (options.bareRepo) {
|
|
7
|
+
return `git --git-dir="${options.bareRepo}"`;
|
|
8
|
+
}
|
|
9
|
+
if (options.repoDir) {
|
|
10
|
+
return `git -C "${options.repoDir}"`;
|
|
11
|
+
}
|
|
12
|
+
return 'git';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getRepoRootDir(cwd = process.cwd(), bareRepo = null) {
|
|
16
|
+
if (bareRepo && fs.existsSync(bareRepo)) {
|
|
17
|
+
return bareRepo;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const commonDir = execSync('git rev-parse --git-common-dir', { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
21
|
+
const absoluteCommonDir = path.isAbsolute(commonDir) ? commonDir : path.resolve(cwd, commonDir);
|
|
22
|
+
return path.dirname(absoluteCommonDir);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
return cwd;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getWorktrees(options = {}) {
|
|
29
|
+
const gitCmd = getGitPrefix(options);
|
|
30
|
+
try {
|
|
31
|
+
const output = execSync(`${gitCmd} worktree list --porcelain`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
32
|
+
const worktrees = [];
|
|
33
|
+
const entries = output.trim().split('\n\n');
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
if (!entry.trim()) continue;
|
|
36
|
+
const lines = entry.split('\n');
|
|
37
|
+
let wtPath = null;
|
|
38
|
+
let wtBranch = null;
|
|
39
|
+
let isBare = false;
|
|
40
|
+
let isDetached = false;
|
|
41
|
+
for (const line of lines) {
|
|
42
|
+
if (line.startsWith('worktree ')) {
|
|
43
|
+
wtPath = line.substring('worktree '.length).trim();
|
|
44
|
+
} else if (line.startsWith('branch ')) {
|
|
45
|
+
const ref = line.substring('branch '.length).trim();
|
|
46
|
+
wtBranch = ref.replace(/^refs\/heads\//, '');
|
|
47
|
+
} else if (line === 'bare') {
|
|
48
|
+
isBare = true;
|
|
49
|
+
} else if (line === 'detached') {
|
|
50
|
+
isDetached = true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (wtPath) {
|
|
54
|
+
worktrees.push({ path: wtPath, branch: wtBranch, isBare, isDetached });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return worktrees;
|
|
58
|
+
} catch (err) {
|
|
59
|
+
// Fallback to standard worktree list if porcelain fails
|
|
60
|
+
try {
|
|
61
|
+
const output = execSync(`${gitCmd} worktree list`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
62
|
+
return output.trim().split('\n').filter(Boolean).map(line => {
|
|
63
|
+
const parts = line.split(/\s+/);
|
|
64
|
+
const wtPath = parts[0];
|
|
65
|
+
const branchMatch = line.match(/\[(.*?)\]/);
|
|
66
|
+
const wtBranch = branchMatch ? branchMatch[1] : null;
|
|
67
|
+
return { path: wtPath, branch: wtBranch, isBare: line.includes('(bare)'), isDetached: line.includes('(detached)') };
|
|
68
|
+
});
|
|
69
|
+
} catch (e) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function branchExistsLocally(branch, options = {}) {
|
|
76
|
+
const gitCmd = getGitPrefix(options);
|
|
77
|
+
try {
|
|
78
|
+
execSync(`${gitCmd} rev-parse --verify "${branch}"`, { stdio: 'ignore' });
|
|
79
|
+
return true;
|
|
80
|
+
} catch (e) {
|
|
81
|
+
try {
|
|
82
|
+
const list = execSync(`${gitCmd} branch --list "${branch}"`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
83
|
+
return list !== '';
|
|
84
|
+
} catch (err) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function branchExistsRemotely(branch, options = {}) {
|
|
91
|
+
const gitCmd = getGitPrefix(options);
|
|
92
|
+
try {
|
|
93
|
+
execSync(`${gitCmd} rev-parse --verify "origin/${branch}"`, { stdio: 'ignore' });
|
|
94
|
+
return true;
|
|
95
|
+
} catch (e) {
|
|
96
|
+
try {
|
|
97
|
+
const remoteList = execSync(`${gitCmd} branch -r --list "origin/${branch}"`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
98
|
+
if (remoteList) return true;
|
|
99
|
+
const lsRemote = execSync(`${gitCmd} ls-remote --heads origin "${branch}"`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
100
|
+
return Boolean(lsRemote);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function createWorktree({ worktreePath, branch, source = 'develop', repoDir, bareRepo }) {
|
|
108
|
+
const options = { repoDir, bareRepo };
|
|
109
|
+
const gitCmd = getGitPrefix(options);
|
|
110
|
+
|
|
111
|
+
const localExists = branchExistsLocally(branch, options);
|
|
112
|
+
const remoteExists = branchExistsRemotely(branch, options);
|
|
113
|
+
|
|
114
|
+
if (localExists) {
|
|
115
|
+
console.log(`Branch "${branch}" exists locally. Adding worktree...`);
|
|
116
|
+
execSync(`${gitCmd} worktree add "${worktreePath}" "${branch}"`, { stdio: 'inherit' });
|
|
117
|
+
} else if (remoteExists) {
|
|
118
|
+
console.log(`Branch "${branch}" exists on remote. Checking out and adding worktree...`);
|
|
119
|
+
execSync(`${gitCmd} worktree add "${worktreePath}" -b "${branch}" "origin/${branch}"`, { stdio: 'inherit' });
|
|
120
|
+
} else {
|
|
121
|
+
// Creating a brand new branch off source
|
|
122
|
+
console.log('Fetching latest from origin...');
|
|
123
|
+
try {
|
|
124
|
+
execSync(`${gitCmd} fetch origin`, { stdio: 'inherit' });
|
|
125
|
+
} catch (e) {}
|
|
126
|
+
|
|
127
|
+
const remoteSourceExists = branchExistsRemotely(source, options);
|
|
128
|
+
const startPoint = remoteSourceExists ? `origin/${source}` : source;
|
|
129
|
+
|
|
130
|
+
console.log(`Creating branch "${branch}" off "${startPoint}" and adding worktree...`);
|
|
131
|
+
if (bareRepo) {
|
|
132
|
+
execSync(`${gitCmd} branch --no-track "${branch}" "${startPoint}"`, { stdio: 'inherit' });
|
|
133
|
+
execSync(`${gitCmd} worktree add "${worktreePath}" "${branch}"`, { stdio: 'inherit' });
|
|
134
|
+
} else {
|
|
135
|
+
execSync(`${gitCmd} worktree add "${worktreePath}" -b "${branch}" "${startPoint}"`, { stdio: 'inherit' });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function syncPrimaryBranch({ branch, worktreePath, repoDir, bareRepo, protectedBranches = [] }) {
|
|
141
|
+
if (!protectedBranches.includes(branch)) return;
|
|
142
|
+
|
|
143
|
+
console.log(`==> Primary branch "${branch}" detected. Checking for local changes...`);
|
|
144
|
+
const options = { repoDir, bareRepo };
|
|
145
|
+
const gitCmd = getGitPrefix(options);
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const statusCmd = bareRepo
|
|
149
|
+
? `${gitCmd} --work-tree="${worktreePath}" status --porcelain`
|
|
150
|
+
: `git -C "${worktreePath}" status --porcelain`;
|
|
151
|
+
|
|
152
|
+
const isDirty = execSync(statusCmd, { encoding: 'utf8' }).trim() !== '';
|
|
153
|
+
|
|
154
|
+
if (isDirty) {
|
|
155
|
+
console.warn(`==> WARNING: Local changes detected in ${branch}. Skipping hard reset to origin/${branch} to prevent data loss.`);
|
|
156
|
+
console.warn(`==> Please commit, stash, or discard your changes if you want to sync with origin.`);
|
|
157
|
+
} else {
|
|
158
|
+
console.log(`==> Syncing ${branch} with origin/${branch}...`);
|
|
159
|
+
execSync(`${gitCmd} fetch origin ${branch}`, { stdio: 'inherit' });
|
|
160
|
+
const resetCmd = bareRepo
|
|
161
|
+
? `${gitCmd} --work-tree="${worktreePath}" reset --hard origin/${branch}`
|
|
162
|
+
: `git -C "${worktreePath}" reset --hard origin/${branch}`;
|
|
163
|
+
execSync(resetCmd, { stdio: 'inherit' });
|
|
164
|
+
}
|
|
165
|
+
} catch (err) {
|
|
166
|
+
console.warn(`==> Warning syncing primary branch: ${err.message}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function removeWorktree(worktreePath, options = {}) {
|
|
171
|
+
const gitCmd = getGitPrefix(options);
|
|
172
|
+
const forceFlag = options.force ? '--force' : '';
|
|
173
|
+
try {
|
|
174
|
+
execSync(`${gitCmd} worktree remove ${forceFlag} "${worktreePath}"`, { stdio: 'inherit' });
|
|
175
|
+
console.log(`Successfully removed git worktree at "${worktreePath}".`);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
console.warn(`"git worktree remove" standard attempt failed. Trying force removal...`);
|
|
178
|
+
try {
|
|
179
|
+
execSync(`${gitCmd} worktree remove --force "${worktreePath}"`, { stdio: 'inherit' });
|
|
180
|
+
console.log(`Successfully force-removed git worktree.`);
|
|
181
|
+
} catch (e) {
|
|
182
|
+
console.warn(`Git worktree force remove warning: ${e.message}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function pruneWorktrees(options = {}) {
|
|
188
|
+
const gitCmd = getGitPrefix(options);
|
|
189
|
+
try {
|
|
190
|
+
execSync(`${gitCmd} worktree prune`, { stdio: 'ignore' });
|
|
191
|
+
} catch (e) {}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function deleteLocalBranch(branch, options = {}) {
|
|
195
|
+
const gitCmd = getGitPrefix(options);
|
|
196
|
+
try {
|
|
197
|
+
execSync(`${gitCmd} branch -D "${branch}"`, { stdio: 'inherit' });
|
|
198
|
+
console.log(`Deleted local branch "${branch}".`);
|
|
199
|
+
} catch (err) {
|
|
200
|
+
console.error(`Failed to delete local branch "${branch}": ${err.message}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function deleteRemoteBranch(branch, options = {}) {
|
|
205
|
+
const gitCmd = getGitPrefix(options);
|
|
206
|
+
try {
|
|
207
|
+
execSync(`${gitCmd} push origin --delete "${branch}"`, { stdio: 'inherit' });
|
|
208
|
+
console.log(`Deleted remote branch "${branch}" from origin.`);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
console.error(`Failed to delete remote branch "${branch}": ${err.message}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
module.exports = {
|
|
215
|
+
getGitPrefix,
|
|
216
|
+
getRepoRootDir,
|
|
217
|
+
getWorktrees,
|
|
218
|
+
branchExistsLocally,
|
|
219
|
+
branchExistsRemotely,
|
|
220
|
+
createWorktree,
|
|
221
|
+
syncPrimaryBranch,
|
|
222
|
+
removeWorktree,
|
|
223
|
+
pruneWorktrees,
|
|
224
|
+
deleteLocalBranch,
|
|
225
|
+
deleteRemoteBranch,
|
|
226
|
+
};
|
package/lib/herdr.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const { execSync, spawnSync } = require('child_process');
|
|
2
|
+
|
|
3
|
+
function parseJsonFromOutput(output) {
|
|
4
|
+
if (!output) return null;
|
|
5
|
+
const line = output.split('\n').find(l => l.trim().startsWith('{'));
|
|
6
|
+
if (line) {
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(line);
|
|
9
|
+
} catch (e) {}
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(output);
|
|
13
|
+
} catch (e) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function listWorkspaces() {
|
|
19
|
+
try {
|
|
20
|
+
const listOutput = execSync('herdr workspace list', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
21
|
+
const data = parseJsonFromOutput(listOutput);
|
|
22
|
+
return (data && data.result && data.result.workspaces) || [];
|
|
23
|
+
} catch (err) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createWorkspace({ label, cwd }) {
|
|
29
|
+
const wsOutput = execSync(`herdr workspace create --label "${label}" --cwd "${cwd}"`, { encoding: 'utf8' }).trim();
|
|
30
|
+
const res = parseJsonFromOutput(wsOutput);
|
|
31
|
+
if (!res || !res.result || !res.result.workspace) {
|
|
32
|
+
throw new Error(`Failed to create Herdr workspace "${label}". Raw output: ${wsOutput}`);
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
workspaceId: res.result.workspace.workspace_id,
|
|
36
|
+
rootPaneId: res.result.root_pane ? res.result.root_pane.pane_id : null,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function closeWorkspace(workspaceId) {
|
|
41
|
+
try {
|
|
42
|
+
execSync(`herdr workspace close ${workspaceId}`, { stdio: 'ignore' });
|
|
43
|
+
return true;
|
|
44
|
+
} catch (err) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function focusWorkspace(workspaceId) {
|
|
50
|
+
try {
|
|
51
|
+
execSync(`herdr workspace focus ${workspaceId}`, { stdio: 'ignore' });
|
|
52
|
+
} catch (e) {}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function splitPane({ paneId, direction = 'right', cwd, focus = false }) {
|
|
56
|
+
const focusFlag = focus ? '--focus' : '--no-focus';
|
|
57
|
+
const cwdFlag = cwd ? `--cwd "${cwd}"` : '';
|
|
58
|
+
const output = execSync(`herdr pane split --pane ${paneId} --direction ${direction} ${cwdFlag} ${focusFlag}`, { encoding: 'utf8' }).trim();
|
|
59
|
+
const res = parseJsonFromOutput(output);
|
|
60
|
+
if (!res || !res.result || !res.result.pane) {
|
|
61
|
+
throw new Error(`Failed to split pane ${paneId} direction ${direction}`);
|
|
62
|
+
}
|
|
63
|
+
return res.result.pane.pane_id;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function renamePane(paneId, name) {
|
|
67
|
+
try {
|
|
68
|
+
execSync(`herdr pane rename ${paneId} "${name}"`, { stdio: 'ignore' });
|
|
69
|
+
} catch (e) {}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function runInPane(paneId, command) {
|
|
73
|
+
if (!command) return;
|
|
74
|
+
try {
|
|
75
|
+
execSync(`herdr pane send-text ${paneId} "${command}"`, { stdio: 'ignore' });
|
|
76
|
+
execSync(`herdr pane send-keys ${paneId} enter`, { stdio: 'ignore' });
|
|
77
|
+
} catch (e) {
|
|
78
|
+
console.error(`Failed to execute command "${command}" in pane ${paneId}: ${e.message}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function closeWorkspacesMatching(matchTargets = []) {
|
|
83
|
+
const targets = matchTargets.filter(Boolean);
|
|
84
|
+
if (!targets.length) return;
|
|
85
|
+
|
|
86
|
+
const workspaces = listWorkspaces();
|
|
87
|
+
const matchingWorkspaces = workspaces.filter(w => targets.includes(w.label));
|
|
88
|
+
|
|
89
|
+
for (const ws of matchingWorkspaces) {
|
|
90
|
+
if (ws.workspace_id) {
|
|
91
|
+
console.log(`Closing Herdr workspace "${ws.label}" (ID: ${ws.workspace_id}) and terminating panes...`);
|
|
92
|
+
closeWorkspace(ws.workspace_id);
|
|
93
|
+
console.log(`Herdr workspace "${ws.label}" closed.`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function attachOrSwitchSession(sessionName) {
|
|
99
|
+
if (process.env.HERDR_ENV === '1') {
|
|
100
|
+
console.log(`Already inside herdr. Workspace "${sessionName}" created and focused!`);
|
|
101
|
+
} else {
|
|
102
|
+
console.log(`Starting herdr session...`);
|
|
103
|
+
spawnSync('herdr', [], { stdio: 'inherit', shell: true });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
listWorkspaces,
|
|
109
|
+
createWorkspace,
|
|
110
|
+
closeWorkspace,
|
|
111
|
+
focusWorkspace,
|
|
112
|
+
splitPane,
|
|
113
|
+
renamePane,
|
|
114
|
+
runInPane,
|
|
115
|
+
closeWorkspacesMatching,
|
|
116
|
+
attachOrSwitchSession,
|
|
117
|
+
};
|
package/lib/layout.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const herdr = require('./herdr');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default standard 4-pane quadrant layout recipe
|
|
5
|
+
*/
|
|
6
|
+
const DEFAULT_QUADRANT_LAYOUT = [
|
|
7
|
+
{ id: 'vim', title: 'vim', cmd: 'vim .', position: 'root' },
|
|
8
|
+
{ id: 'server', title: 'server', cmd: null, split: 'right', from: 'vim' },
|
|
9
|
+
{ id: 'shell', title: 'shell', cmd: null, split: 'down', from: 'vim' },
|
|
10
|
+
{ id: 'agy', title: 'agy', cmd: 'agy', split: 'down', from: 'server', focus: true },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Render a declarative layout onto a Herdr workspace
|
|
15
|
+
*
|
|
16
|
+
* @param {Object} options
|
|
17
|
+
* @param {Array} options.layout Array of pane definitions
|
|
18
|
+
* @param {string} options.rootPaneId ID of the root pane created with workspace
|
|
19
|
+
* @param {string} options.cwd Working directory for panes
|
|
20
|
+
* @param {string} options.focusTarget Name or ID of pane to focus (e.g. 'agy', 'vim', 'logs', 'shell')
|
|
21
|
+
*/
|
|
22
|
+
function renderLayout({ layout = DEFAULT_QUADRANT_LAYOUT, rootPaneId, cwd, focusTarget = 'agy' }) {
|
|
23
|
+
const paneMap = new Map(); // id -> paneId
|
|
24
|
+
let targetFocusPaneId = null;
|
|
25
|
+
|
|
26
|
+
// 1. First pass: identify which pane is targeted for focus
|
|
27
|
+
const normalizedFocus = (focusTarget || 'agy').toLowerCase().trim();
|
|
28
|
+
|
|
29
|
+
// Helper to check if a pane matches focus target
|
|
30
|
+
function isFocusMatch(paneDef) {
|
|
31
|
+
if (!normalizedFocus) return false;
|
|
32
|
+
const idMatch = paneDef.id && paneDef.id.toLowerCase() === normalizedFocus;
|
|
33
|
+
const titleMatch = paneDef.title && paneDef.title.toLowerCase() === normalizedFocus;
|
|
34
|
+
return idMatch || titleMatch;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 2. Iterate through pane definitions
|
|
38
|
+
for (const paneDef of layout) {
|
|
39
|
+
let currentPaneId;
|
|
40
|
+
|
|
41
|
+
if (paneDef.position === 'root' || !paneDef.from) {
|
|
42
|
+
currentPaneId = rootPaneId;
|
|
43
|
+
} else {
|
|
44
|
+
const parentPaneId = paneMap.get(paneDef.from);
|
|
45
|
+
if (!parentPaneId) {
|
|
46
|
+
console.warn(`Layout warning: parent pane "${paneDef.from}" not found for "${paneDef.id}".`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const shouldFocus = isFocusMatch(paneDef);
|
|
51
|
+
currentPaneId = herdr.splitPane({
|
|
52
|
+
paneId: parentPaneId,
|
|
53
|
+
direction: paneDef.split || 'right',
|
|
54
|
+
cwd,
|
|
55
|
+
focus: shouldFocus,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (currentPaneId) {
|
|
60
|
+
paneMap.set(paneDef.id, currentPaneId);
|
|
61
|
+
|
|
62
|
+
if (paneDef.title) {
|
|
63
|
+
herdr.renamePane(currentPaneId, paneDef.title);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (paneDef.cmd) {
|
|
67
|
+
herdr.runInPane(currentPaneId, paneDef.cmd);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (isFocusMatch(paneDef)) {
|
|
71
|
+
targetFocusPaneId = currentPaneId;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
paneMap,
|
|
78
|
+
targetFocusPaneId,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = {
|
|
83
|
+
renderLayout,
|
|
84
|
+
DEFAULT_QUADRANT_LAYOUT,
|
|
85
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const git = require('../git');
|
|
4
|
+
const herdr = require('../herdr');
|
|
5
|
+
const layout = require('../layout');
|
|
6
|
+
const { createContext } = require('../context');
|
|
7
|
+
const { showUsage } = require('../cli');
|
|
8
|
+
|
|
9
|
+
async function executeCreate(flags, config, cwd = process.cwd()) {
|
|
10
|
+
const branch = flags.branch;
|
|
11
|
+
if (!branch) {
|
|
12
|
+
console.error('Error: --branch is a required argument for creation.');
|
|
13
|
+
showUsage();
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 1. Resolve Dirname and Worktree Path
|
|
18
|
+
const sanitizedBranch = branch.replace(/\//g, '-');
|
|
19
|
+
const dirname = flags.dirname || sanitizedBranch;
|
|
20
|
+
|
|
21
|
+
const repoRoot = git.getRepoRootDir(cwd, config.repo.bareRepo);
|
|
22
|
+
const baseDir = config.repo.worktreesBase || repoRoot;
|
|
23
|
+
const worktreePath = path.resolve(baseDir, dirname);
|
|
24
|
+
|
|
25
|
+
// 2. Resolve Session / Workspace Name
|
|
26
|
+
const baseWorkspace = flags.workspaceName || dirname.replace(/[\s.:]/g, '_');
|
|
27
|
+
const prefix = config.workspace.labelPrefix || '';
|
|
28
|
+
const sessionName = (prefix && !baseWorkspace.startsWith(prefix.trim()))
|
|
29
|
+
? `${prefix}${baseWorkspace}`
|
|
30
|
+
: baseWorkspace;
|
|
31
|
+
|
|
32
|
+
console.log(`\n=== Starting Herdr Worktree Session ===`);
|
|
33
|
+
console.log(`Active Preset: ${config.preset ? config.preset.name : 'generic'}`);
|
|
34
|
+
console.log(`Repository Root: ${repoRoot}`);
|
|
35
|
+
console.log(`Target Worktree Path: ${worktreePath}`);
|
|
36
|
+
console.log(`Workspace Name: ${sessionName}`);
|
|
37
|
+
console.log(`Base Branch (Source): ${flags.source || config.repo.defaultBaseBranch}\n`);
|
|
38
|
+
|
|
39
|
+
// 3. Determine if Worktree Already Exists
|
|
40
|
+
const worktrees = git.getWorktrees({ repoDir: repoRoot, bareRepo: config.repo.bareRepo });
|
|
41
|
+
const existingWorktree = worktrees.find(wt => wt.branch === branch || path.resolve(wt.path) === path.resolve(worktreePath));
|
|
42
|
+
|
|
43
|
+
let worktreePathToUse = worktreePath;
|
|
44
|
+
let worktreeExists = false;
|
|
45
|
+
|
|
46
|
+
if (existingWorktree) {
|
|
47
|
+
worktreeExists = true;
|
|
48
|
+
worktreePathToUse = existingWorktree.path;
|
|
49
|
+
console.log(`Worktree already exists for branch "${branch}" at "${worktreePathToUse}".`);
|
|
50
|
+
} else if (fs.existsSync(worktreePath)) {
|
|
51
|
+
worktreeExists = true;
|
|
52
|
+
console.log(`Directory already exists at "${worktreePath}". Skipping git worktree creation.`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 4. Create Worktree if it doesn't exist
|
|
56
|
+
if (!worktreeExists) {
|
|
57
|
+
console.log(`Creating new git worktree for branch "${branch}"...`);
|
|
58
|
+
try {
|
|
59
|
+
git.createWorktree({
|
|
60
|
+
worktreePath,
|
|
61
|
+
branch,
|
|
62
|
+
source: flags.source || config.repo.defaultBaseBranch,
|
|
63
|
+
repoDir: repoRoot,
|
|
64
|
+
bareRepo: config.repo.bareRepo,
|
|
65
|
+
});
|
|
66
|
+
} catch (err) {
|
|
67
|
+
console.error(`Failed to create worktree: ${err.message}`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 5. Initialize Context for Hooks
|
|
73
|
+
const ctx = createContext({
|
|
74
|
+
worktreePath: worktreePathToUse,
|
|
75
|
+
repoRoot,
|
|
76
|
+
bareRepo: config.repo.bareRepo,
|
|
77
|
+
branch,
|
|
78
|
+
source: flags.source || config.repo.defaultBaseBranch,
|
|
79
|
+
flags,
|
|
80
|
+
preset: config.preset,
|
|
81
|
+
config,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// 6. Run Preset / Config Hooks
|
|
85
|
+
if (typeof config.hooks.onSyncPrimary === 'function') {
|
|
86
|
+
await config.hooks.onSyncPrimary(ctx);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!worktreeExists && typeof config.hooks.onScaffold === 'function') {
|
|
90
|
+
await config.hooks.onScaffold(ctx);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 7. Orchestrate Herdr Workspace
|
|
94
|
+
console.log(`\n==> Creating Herdr workspace "${sessionName}"...`);
|
|
95
|
+
let ws;
|
|
96
|
+
try {
|
|
97
|
+
ws = herdr.createWorkspace({ label: sessionName, cwd: worktreePathToUse });
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error(`Failed to create herdr workspace: ${err.message}`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 8. Render Terminal Layout
|
|
104
|
+
console.log(`==> Configuring terminal panes...`);
|
|
105
|
+
layout.renderLayout({
|
|
106
|
+
layout: config.layout,
|
|
107
|
+
rootPaneId: ws.rootPaneId,
|
|
108
|
+
cwd: worktreePathToUse,
|
|
109
|
+
focusTarget: flags.focusTarget || config.workspace.defaultFocus,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Focus the workspace
|
|
113
|
+
herdr.focusWorkspace(ws.workspaceId);
|
|
114
|
+
|
|
115
|
+
// 9. Attach or Switch
|
|
116
|
+
herdr.attachOrSwitchSession(sessionName);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = {
|
|
120
|
+
executeCreate,
|
|
121
|
+
};
|