@tsuzuku/arise 1.1.0 → 1.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/README.md +28 -4
- package/index.js +49 -10
- package/lib/cli.js +39 -2
- package/lib/config/init.js +862 -0
- package/lib/config.js +75 -6
- package/lib/git.js +37 -0
- package/lib/herdr.js +47 -6
- package/lib/interactive.js +644 -0
- package/lib/layout.js +21 -7
- package/lib/lifecycle/create.js +47 -15
- package/lib/lifecycle/nuke.js +60 -13
- package/lib/logger.js +85 -0
- package/lib/skill.js +6 -0
- package/lib/tui/ansi.js +294 -0
- package/lib/tui/prompt.js +629 -0
- package/package.json +1 -2
- package/skills/arise/SKILL.md +6 -0
- package/types.d.ts +46 -0
- package/worktree.schema.json +0 -130
package/lib/config.js
CHANGED
|
@@ -53,12 +53,19 @@ function findConfigFile(searchDirs = []) {
|
|
|
53
53
|
return null;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
function stripJsonComments(str) {
|
|
57
|
+
return str
|
|
58
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
59
|
+
.replace(/\/\/.*$/gm, '')
|
|
60
|
+
.trim();
|
|
61
|
+
}
|
|
62
|
+
|
|
56
63
|
function loadConfigFile(filePath) {
|
|
57
64
|
if (!filePath) return {};
|
|
58
65
|
try {
|
|
59
66
|
if (filePath.endsWith('.json') || filePath.endsWith('.worktreerc') || filePath.endsWith('.ariserc')) {
|
|
60
67
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
61
|
-
return JSON.parse(content);
|
|
68
|
+
return JSON.parse(stripJsonComments(content));
|
|
62
69
|
} else {
|
|
63
70
|
return require(path.resolve(filePath));
|
|
64
71
|
}
|
|
@@ -68,9 +75,67 @@ function loadConfigFile(filePath) {
|
|
|
68
75
|
}
|
|
69
76
|
}
|
|
70
77
|
|
|
71
|
-
function resolveConfiguration(flags = {}, cwd = process.cwd()) {
|
|
72
|
-
const repoRoot = git.getRepoRootDir(cwd);
|
|
73
|
-
const
|
|
78
|
+
function resolveConfiguration(flags = {}, cwd = process.cwd(), callerCwd = null) {
|
|
79
|
+
const repoRoot = git.getRepoRootDir(cwd) || (callerCwd ? git.getRepoRootDir(callerCwd) : null);
|
|
80
|
+
const isBare = git.isBareRepo(repoRoot);
|
|
81
|
+
const searchDirs = [];
|
|
82
|
+
|
|
83
|
+
const addSearchDir = (dir) => {
|
|
84
|
+
if (dir && !searchDirs.includes(dir)) {
|
|
85
|
+
searchDirs.push(dir);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
let worktrees = [];
|
|
90
|
+
try {
|
|
91
|
+
worktrees = git.getWorktrees({ repoDir: repoRoot, bareRepo: isBare ? repoRoot : null });
|
|
92
|
+
} catch (e) {}
|
|
93
|
+
|
|
94
|
+
// 1. Explicit directory if --dirname / -d is passed
|
|
95
|
+
if (flags.dirname) {
|
|
96
|
+
const resolvedDir = path.isAbsolute(flags.dirname)
|
|
97
|
+
? flags.dirname
|
|
98
|
+
: path.resolve(repoRoot || cwd, flags.dirname);
|
|
99
|
+
addSearchDir(resolvedDir);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 2. Directory for the specified branch (--branch / -b)
|
|
103
|
+
if (flags.branch) {
|
|
104
|
+
const matchingWt = worktrees.find((wt) => wt.branch === flags.branch);
|
|
105
|
+
if (matchingWt && matchingWt.path) {
|
|
106
|
+
addSearchDir(matchingWt.path);
|
|
107
|
+
}
|
|
108
|
+
const branchDir = path.resolve(repoRoot || cwd, flags.branch.replace(/\//g, '-'));
|
|
109
|
+
addSearchDir(branchDir);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 3. Target working directory
|
|
113
|
+
addSearchDir(cwd);
|
|
114
|
+
|
|
115
|
+
// 4. Fallback working directory (caller working directory)
|
|
116
|
+
if (callerCwd) {
|
|
117
|
+
addSearchDir(callerCwd);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 5. Repository root directory
|
|
121
|
+
if (repoRoot) {
|
|
122
|
+
addSearchDir(repoRoot);
|
|
123
|
+
if (isBare) {
|
|
124
|
+
addSearchDir(path.dirname(repoRoot));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 6. Fallback to existing worktrees (prioritizing primary branches like main, master, develop)
|
|
129
|
+
const primaryBranches = ['main', 'master', 'develop', 'prod', 'staging'];
|
|
130
|
+
const primaryWts = worktrees.filter((wt) => !wt.isBare && wt.branch && primaryBranches.includes(wt.branch));
|
|
131
|
+
for (const wt of primaryWts) {
|
|
132
|
+
if (wt.path) addSearchDir(wt.path);
|
|
133
|
+
}
|
|
134
|
+
for (const wt of worktrees) {
|
|
135
|
+
if (wt.path && !wt.isBare) addSearchDir(wt.path);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const configFile = findConfigFile(searchDirs);
|
|
74
139
|
const fileConfig = loadConfigFile(configFile);
|
|
75
140
|
|
|
76
141
|
// 1. Resolve Preset
|
|
@@ -80,12 +145,16 @@ function resolveConfiguration(flags = {}, cwd = process.cwd()) {
|
|
|
80
145
|
preset = getPreset(presetName);
|
|
81
146
|
}
|
|
82
147
|
if (!preset) {
|
|
83
|
-
|
|
148
|
+
const detectDir = (configFile && path.dirname(configFile)) || cwd;
|
|
149
|
+
preset = detectPreset(detectDir);
|
|
150
|
+
if (preset.name === 'generic' && detectDir !== cwd) {
|
|
151
|
+
preset = detectPreset(cwd);
|
|
152
|
+
}
|
|
84
153
|
}
|
|
85
154
|
|
|
86
155
|
// 2. Merge Repo Settings
|
|
87
156
|
const repoConfig = {
|
|
88
|
-
bareRepo: (fileConfig.repo && fileConfig.repo.bareRepo) || (preset.repo && preset.repo.bareRepo) || null,
|
|
157
|
+
bareRepo: (fileConfig.repo && fileConfig.repo.bareRepo) || (preset.repo && preset.repo.bareRepo) || (isBare ? repoRoot : null),
|
|
89
158
|
worktreesBase: (fileConfig.repo && fileConfig.repo.worktreesBase) || (preset.repo && preset.repo.worktreesBase) || null,
|
|
90
159
|
defaultBaseBranch: (fileConfig.repo && fileConfig.repo.defaultBaseBranch) || (preset.repo && preset.repo.defaultBaseBranch) || 'develop',
|
|
91
160
|
protectedBranches: (fileConfig.repo && fileConfig.repo.protectedBranches) || (preset.repo && preset.repo.protectedBranches) || ['main', 'master', 'develop', 'prod', 'staging'],
|
package/lib/git.js
CHANGED
|
@@ -2,11 +2,39 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { execSync } = require('child_process');
|
|
4
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 getLocalBranches(options = {}) {
|
|
16
|
+
const gitCmd = getGitPrefix(options);
|
|
17
|
+
try {
|
|
18
|
+
const output = execSync(`${gitCmd} branch --format="%(refname:short)"`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
19
|
+
if (!output) return [];
|
|
20
|
+
return output.split('\n').map(b => b.trim()).filter(Boolean);
|
|
21
|
+
} catch (err) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
5
26
|
function getGitPrefix(options = {}) {
|
|
6
27
|
if (options.bareRepo) {
|
|
7
28
|
return `git --git-dir="${options.bareRepo}"`;
|
|
8
29
|
}
|
|
9
30
|
if (options.repoDir) {
|
|
31
|
+
const dotBare = path.join(options.repoDir, '.bare');
|
|
32
|
+
if (fs.existsSync(dotBare)) {
|
|
33
|
+
return `git --git-dir="${dotBare}"`;
|
|
34
|
+
}
|
|
35
|
+
if (isBareRepo(options.repoDir)) {
|
|
36
|
+
return `git --git-dir="${options.repoDir}"`;
|
|
37
|
+
}
|
|
10
38
|
return `git -C "${options.repoDir}"`;
|
|
11
39
|
}
|
|
12
40
|
return 'git';
|
|
@@ -16,9 +44,16 @@ function getRepoRootDir(cwd = process.cwd(), bareRepo = null) {
|
|
|
16
44
|
if (bareRepo && fs.existsSync(bareRepo)) {
|
|
17
45
|
return bareRepo;
|
|
18
46
|
}
|
|
47
|
+
const dotBare = path.join(cwd, '.bare');
|
|
48
|
+
if (fs.existsSync(dotBare)) {
|
|
49
|
+
return cwd;
|
|
50
|
+
}
|
|
19
51
|
try {
|
|
20
52
|
const commonDir = execSync('git rev-parse --git-common-dir', { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
21
53
|
const absoluteCommonDir = path.isAbsolute(commonDir) ? commonDir : path.resolve(cwd, commonDir);
|
|
54
|
+
if (isBareRepo(absoluteCommonDir)) {
|
|
55
|
+
return absoluteCommonDir;
|
|
56
|
+
}
|
|
22
57
|
return path.dirname(absoluteCommonDir);
|
|
23
58
|
} catch (err) {
|
|
24
59
|
return cwd;
|
|
@@ -278,4 +313,6 @@ module.exports = {
|
|
|
278
313
|
pruneWorktrees,
|
|
279
314
|
deleteLocalBranch,
|
|
280
315
|
deleteRemoteBranch,
|
|
316
|
+
getLocalBranches,
|
|
317
|
+
isBareRepo,
|
|
281
318
|
};
|
package/lib/herdr.js
CHANGED
|
@@ -176,15 +176,50 @@ function runInPane(paneId, command) {
|
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
function
|
|
180
|
-
|
|
181
|
-
|
|
179
|
+
function listPanes() {
|
|
180
|
+
try {
|
|
181
|
+
const listOutput = execSync('herdr pane list', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
182
|
+
const data = parseJsonFromOutput(listOutput);
|
|
183
|
+
return (data && data.result && data.result.panes) || [];
|
|
184
|
+
} catch (err) {
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function closeWorkspacesForWorktree(worktreePath, matchTargets = []) {
|
|
190
|
+
const matchedWorkspaceIds = new Set();
|
|
191
|
+
const normalizedPath = worktreePath ? path.resolve(worktreePath) : null;
|
|
182
192
|
|
|
193
|
+
// 1. Path-based matching via herdr pane list (highest fidelity, immune to prefix/label changes)
|
|
194
|
+
if (normalizedPath) {
|
|
195
|
+
const panes = listPanes();
|
|
196
|
+
for (const pane of panes) {
|
|
197
|
+
if (pane.workspace_id) {
|
|
198
|
+
const paneCwd = pane.cwd ? path.resolve(pane.cwd) : null;
|
|
199
|
+
const fgCwd = pane.foreground_cwd ? path.resolve(pane.foreground_cwd) : null;
|
|
200
|
+
if (
|
|
201
|
+
(paneCwd && (paneCwd === normalizedPath || paneCwd.startsWith(normalizedPath + path.sep))) ||
|
|
202
|
+
(fgCwd && (fgCwd === normalizedPath || fgCwd.startsWith(normalizedPath + path.sep)))
|
|
203
|
+
) {
|
|
204
|
+
matchedWorkspaceIds.add(pane.workspace_id);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 2. Exact label matching if matchTargets are provided
|
|
211
|
+
const targets = (Array.isArray(matchTargets) ? matchTargets : [matchTargets]).filter(Boolean);
|
|
183
212
|
const workspaces = listWorkspaces();
|
|
184
|
-
|
|
213
|
+
if (targets.length > 0) {
|
|
214
|
+
for (const ws of workspaces) {
|
|
215
|
+
if (ws.label && targets.includes(ws.label)) {
|
|
216
|
+
matchedWorkspaceIds.add(ws.workspace_id);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
185
220
|
|
|
186
|
-
for (const ws of
|
|
187
|
-
if (ws.workspace_id) {
|
|
221
|
+
for (const ws of workspaces) {
|
|
222
|
+
if (matchedWorkspaceIds.has(ws.workspace_id)) {
|
|
188
223
|
console.log(`Closing Herdr workspace "${ws.label}" (ID: ${ws.workspace_id}) and terminating panes...`);
|
|
189
224
|
closeWorkspace(ws.workspace_id);
|
|
190
225
|
console.log(`Herdr workspace "${ws.label}" closed.`);
|
|
@@ -192,6 +227,10 @@ function closeWorkspacesMatching(matchTargets = []) {
|
|
|
192
227
|
}
|
|
193
228
|
}
|
|
194
229
|
|
|
230
|
+
function closeWorkspacesMatching(matchTargets = [], worktreePath = null) {
|
|
231
|
+
return closeWorkspacesForWorktree(worktreePath, matchTargets);
|
|
232
|
+
}
|
|
233
|
+
|
|
195
234
|
function attachOrSwitchSession(sessionName) {
|
|
196
235
|
if (process.env.HERDR_ENV === '1') {
|
|
197
236
|
console.log(`Already inside herdr. Workspace "${sessionName}" created and focused!`);
|
|
@@ -206,6 +245,7 @@ module.exports = {
|
|
|
206
245
|
getRecommendedInstallCommand,
|
|
207
246
|
ensureHerdrInstalled,
|
|
208
247
|
listWorkspaces,
|
|
248
|
+
listPanes,
|
|
209
249
|
createWorkspace,
|
|
210
250
|
closeWorkspace,
|
|
211
251
|
focusWorkspace,
|
|
@@ -213,5 +253,6 @@ module.exports = {
|
|
|
213
253
|
renamePane,
|
|
214
254
|
runInPane,
|
|
215
255
|
closeWorkspacesMatching,
|
|
256
|
+
closeWorkspacesForWorktree,
|
|
216
257
|
attachOrSwitchSession,
|
|
217
258
|
};
|