@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/README.md +125 -0
- package/arise.schema.json +130 -0
- package/bin/cli.js +8 -0
- package/index.js +37 -0
- package/lib/cli.js +129 -0
- package/lib/config.js +229 -0
- package/lib/context.js +113 -0
- package/lib/git.js +306 -0
- package/lib/herdr.js +217 -0
- package/lib/layout.js +88 -0
- package/lib/lifecycle/create.js +131 -0
- package/lib/lifecycle/nuke.js +218 -0
- package/lib/skill.js +200 -0
- package/package.json +42 -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 +157 -0
package/lib/layout.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
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, isAgent: 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. 'agent', 'agy', 'claude', '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
|
+
const cmdMatch = paneDef.cmd && paneDef.cmd.toLowerCase().split(/\s+/)[0] === normalizedFocus;
|
|
35
|
+
const agentAlias = (normalizedFocus === 'agent' || normalizedFocus === 'agy' || normalizedFocus === 'ai') &&
|
|
36
|
+
(paneDef.id === 'agy' || paneDef.id === 'agent' || paneDef.isAgent);
|
|
37
|
+
return Boolean(idMatch || titleMatch || cmdMatch || agentAlias);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 2. Iterate through pane definitions
|
|
41
|
+
for (const paneDef of layout) {
|
|
42
|
+
let currentPaneId;
|
|
43
|
+
|
|
44
|
+
if (paneDef.position === 'root' || !paneDef.from) {
|
|
45
|
+
currentPaneId = rootPaneId;
|
|
46
|
+
} else {
|
|
47
|
+
const parentPaneId = paneMap.get(paneDef.from);
|
|
48
|
+
if (!parentPaneId) {
|
|
49
|
+
console.warn(`Layout warning: parent pane "${paneDef.from}" not found for "${paneDef.id}".`);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const shouldFocus = isFocusMatch(paneDef);
|
|
54
|
+
currentPaneId = herdr.splitPane({
|
|
55
|
+
paneId: parentPaneId,
|
|
56
|
+
direction: paneDef.split || 'right',
|
|
57
|
+
cwd,
|
|
58
|
+
focus: shouldFocus,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (currentPaneId) {
|
|
63
|
+
paneMap.set(paneDef.id, currentPaneId);
|
|
64
|
+
|
|
65
|
+
if (paneDef.title) {
|
|
66
|
+
herdr.renamePane(currentPaneId, paneDef.title);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (paneDef.cmd) {
|
|
70
|
+
herdr.runInPane(currentPaneId, paneDef.cmd);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (isFocusMatch(paneDef)) {
|
|
74
|
+
targetFocusPaneId = currentPaneId;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
paneMap,
|
|
81
|
+
targetFocusPaneId,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = {
|
|
86
|
+
renderLayout,
|
|
87
|
+
DEFAULT_QUADRANT_LAYOUT,
|
|
88
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
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 { resolveConfiguration } = require('../config');
|
|
8
|
+
const { showUsage } = require('../cli');
|
|
9
|
+
|
|
10
|
+
async function executeCreate(flags, config, cwd = process.cwd()) {
|
|
11
|
+
const branch = flags.branch;
|
|
12
|
+
if (!branch) {
|
|
13
|
+
console.error('Error: --branch is a required argument for creation.');
|
|
14
|
+
showUsage();
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 1. Resolve Dirname and Worktree Path
|
|
19
|
+
const sanitizedBranch = branch.replace(/\//g, '-');
|
|
20
|
+
const dirname = flags.dirname || sanitizedBranch;
|
|
21
|
+
|
|
22
|
+
const repoRoot = git.getRepoRootDir(cwd, config.repo.bareRepo);
|
|
23
|
+
const baseDir = config.repo.worktreesBase || (git.isBareRepo(repoRoot) ? path.dirname(repoRoot) : repoRoot);
|
|
24
|
+
const worktreePath = path.resolve(baseDir, dirname);
|
|
25
|
+
|
|
26
|
+
// 2. Resolve Session / Workspace Name
|
|
27
|
+
const baseWorkspace = flags.workspaceName || dirname.replace(/[\s.:]/g, '_');
|
|
28
|
+
let prefix = config.workspace.labelPrefix || '';
|
|
29
|
+
let sessionName = (prefix && !baseWorkspace.startsWith(prefix.trim()))
|
|
30
|
+
? `${prefix}${baseWorkspace}`
|
|
31
|
+
: baseWorkspace;
|
|
32
|
+
|
|
33
|
+
console.log(`\n=== Starting Herdr Worktree Session ===`);
|
|
34
|
+
console.log(`Active Preset: ${config.preset ? config.preset.name : 'generic'}`);
|
|
35
|
+
console.log(`Repository Root: ${repoRoot}`);
|
|
36
|
+
console.log(`Target Worktree Path: ${worktreePath}`);
|
|
37
|
+
console.log(`Workspace Name: ${sessionName}`);
|
|
38
|
+
console.log(`Base Branch (Source): ${flags.source || config.repo.defaultBaseBranch}\n`);
|
|
39
|
+
|
|
40
|
+
// 3. Determine if Worktree Already Exists
|
|
41
|
+
const worktrees = git.getWorktrees({ repoDir: repoRoot, bareRepo: config.repo.bareRepo });
|
|
42
|
+
const existingWorktree = worktrees.find(wt => wt.branch === branch || path.resolve(wt.path) === path.resolve(worktreePath));
|
|
43
|
+
|
|
44
|
+
let worktreePathToUse = worktreePath;
|
|
45
|
+
let worktreeExists = false;
|
|
46
|
+
|
|
47
|
+
if (existingWorktree) {
|
|
48
|
+
worktreeExists = true;
|
|
49
|
+
worktreePathToUse = existingWorktree.path;
|
|
50
|
+
console.log(`Worktree already exists for branch "${branch}" at "${worktreePathToUse}".`);
|
|
51
|
+
} else if (fs.existsSync(worktreePath)) {
|
|
52
|
+
worktreeExists = true;
|
|
53
|
+
console.log(`Directory already exists at "${worktreePath}". Skipping git worktree creation.`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 4. Create Worktree if it doesn't exist
|
|
57
|
+
if (!worktreeExists) {
|
|
58
|
+
console.log(`Creating new git worktree for branch "${branch}"...`);
|
|
59
|
+
try {
|
|
60
|
+
git.createWorktree({
|
|
61
|
+
worktreePath,
|
|
62
|
+
branch,
|
|
63
|
+
source: flags.source || config.repo.defaultBaseBranch,
|
|
64
|
+
repoDir: repoRoot,
|
|
65
|
+
bareRepo: config.repo.bareRepo,
|
|
66
|
+
});
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.error(`Failed to create worktree: ${err.message}`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 5. Re-resolve config using target worktree directory for branch-specific overrides
|
|
74
|
+
const activeConfig = resolveConfiguration(flags, worktreePathToUse);
|
|
75
|
+
prefix = activeConfig.workspace.labelPrefix || '';
|
|
76
|
+
sessionName = (prefix && !baseWorkspace.startsWith(prefix.trim()))
|
|
77
|
+
? `${prefix}${baseWorkspace}`
|
|
78
|
+
: baseWorkspace;
|
|
79
|
+
|
|
80
|
+
// Initialize Context for Hooks
|
|
81
|
+
const ctx = createContext({
|
|
82
|
+
worktreePath: worktreePathToUse,
|
|
83
|
+
repoRoot,
|
|
84
|
+
bareRepo: activeConfig.repo.bareRepo,
|
|
85
|
+
branch,
|
|
86
|
+
source: flags.source || activeConfig.repo.defaultBaseBranch,
|
|
87
|
+
flags,
|
|
88
|
+
preset: activeConfig.preset,
|
|
89
|
+
config: activeConfig,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// 6. Run Preset / Config Hooks
|
|
93
|
+
if (typeof activeConfig.hooks.onSyncPrimary === 'function') {
|
|
94
|
+
await activeConfig.hooks.onSyncPrimary(ctx);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!worktreeExists && typeof activeConfig.hooks.onScaffold === 'function') {
|
|
98
|
+
await activeConfig.hooks.onScaffold(ctx);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 7. Orchestrate Herdr Workspace
|
|
102
|
+
await herdr.ensureHerdrInstalled({ yes: flags.yes });
|
|
103
|
+
|
|
104
|
+
console.log(`\n==> Creating Herdr workspace "${sessionName}"...`);
|
|
105
|
+
let ws;
|
|
106
|
+
try {
|
|
107
|
+
ws = herdr.createWorkspace({ label: sessionName, cwd: worktreePathToUse });
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error(`Failed to create herdr workspace: ${err.message}`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 8. Render Terminal Layout
|
|
114
|
+
console.log(`==> Configuring terminal panes...`);
|
|
115
|
+
layout.renderLayout({
|
|
116
|
+
layout: activeConfig.layout,
|
|
117
|
+
rootPaneId: ws.rootPaneId,
|
|
118
|
+
cwd: worktreePathToUse,
|
|
119
|
+
focusTarget: flags.focusTarget || activeConfig.workspace.defaultFocus,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Focus the workspace
|
|
123
|
+
herdr.focusWorkspace(ws.workspaceId);
|
|
124
|
+
|
|
125
|
+
// 9. Attach or Switch
|
|
126
|
+
herdr.attachOrSwitchSession(sessionName);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
executeCreate,
|
|
131
|
+
};
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const git = require('../git');
|
|
5
|
+
const herdr = require('../herdr');
|
|
6
|
+
const { createContext } = require('../context');
|
|
7
|
+
const { resolveConfiguration } = require('../config');
|
|
8
|
+
const { showUsage } = require('../cli');
|
|
9
|
+
|
|
10
|
+
async function executeNuke(flags, config, cwd = process.cwd()) {
|
|
11
|
+
const repoRoot = git.getRepoRootDir(cwd, config.repo.bareRepo);
|
|
12
|
+
const worktrees = git.getWorktrees({ repoDir: repoRoot, bareRepo: config.repo.bareRepo });
|
|
13
|
+
|
|
14
|
+
let target = flags.cleanupTarget || flags.dirname || flags.branch;
|
|
15
|
+
|
|
16
|
+
// 1. Auto-detect worktree if invoked inside a worktree directory without arguments
|
|
17
|
+
if (!target) {
|
|
18
|
+
const currentCwd = path.resolve(cwd);
|
|
19
|
+
const matchingWt = worktrees.find(wt => {
|
|
20
|
+
if (wt.isBare) return false;
|
|
21
|
+
const resolvedWtPath = path.resolve(wt.path);
|
|
22
|
+
return resolvedWtPath !== path.resolve(repoRoot) && (
|
|
23
|
+
currentCwd === resolvedWtPath ||
|
|
24
|
+
currentCwd.startsWith(resolvedWtPath + path.sep)
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (matchingWt) {
|
|
29
|
+
target = matchingWt.path;
|
|
30
|
+
console.log(`Auto-detected current worktree: "${target}"`);
|
|
31
|
+
} else {
|
|
32
|
+
console.error('Error: --nuke / --cleanup requires a worktree directory name or branch when run from the root repository.');
|
|
33
|
+
showUsage();
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 2. Resolve worktree target path and matching branch
|
|
39
|
+
const baseDir = config.repo.worktreesBase || repoRoot;
|
|
40
|
+
const resolvedTarget = path.isAbsolute(target) ? path.resolve(target) : path.resolve(baseDir, target);
|
|
41
|
+
|
|
42
|
+
const existingWorktree = worktrees.find(wt => {
|
|
43
|
+
return (
|
|
44
|
+
path.resolve(wt.path) === resolvedTarget ||
|
|
45
|
+
path.basename(wt.path) === target ||
|
|
46
|
+
(wt.branch && wt.branch === target) ||
|
|
47
|
+
(flags.branch && wt.branch === flags.branch)
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const targetWorktreePath = existingWorktree ? existingWorktree.path : resolvedTarget;
|
|
52
|
+
const targetDirname = path.basename(targetWorktreePath);
|
|
53
|
+
const targetSessionName = targetDirname.replace(/[\s.:]/g, '_');
|
|
54
|
+
|
|
55
|
+
const activeConfig = fs.existsSync(targetWorktreePath)
|
|
56
|
+
? resolveConfiguration(flags, targetWorktreePath)
|
|
57
|
+
: config;
|
|
58
|
+
|
|
59
|
+
let targetBranch = flags.branch;
|
|
60
|
+
if (!targetBranch && existingWorktree && existingWorktree.branch) {
|
|
61
|
+
targetBranch = existingWorktree.branch;
|
|
62
|
+
}
|
|
63
|
+
if (!targetBranch && fs.existsSync(targetWorktreePath)) {
|
|
64
|
+
try {
|
|
65
|
+
const detected = execSync(`git -C "${targetWorktreePath}" rev-parse --abbrev-ref HEAD`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
66
|
+
if (detected && detected !== 'HEAD') {
|
|
67
|
+
targetBranch = detected;
|
|
68
|
+
}
|
|
69
|
+
} catch (e) {}
|
|
70
|
+
}
|
|
71
|
+
if (!targetBranch) {
|
|
72
|
+
try {
|
|
73
|
+
const gitCmd = git.getGitPrefix({ repoDir: repoRoot, bareRepo: activeConfig.repo.bareRepo });
|
|
74
|
+
execSync(`${gitCmd} rev-parse --verify "${target}"`, { stdio: 'ignore' });
|
|
75
|
+
targetBranch = target;
|
|
76
|
+
} catch (e) {}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.log(`\n=== Starting Worktree Nuke ===`);
|
|
80
|
+
console.log(`Active Preset: ${activeConfig.preset ? activeConfig.preset.name : 'generic'}`);
|
|
81
|
+
console.log(`Repository Root: ${repoRoot}`);
|
|
82
|
+
console.log(`Target Worktree Path: ${targetWorktreePath}`);
|
|
83
|
+
console.log(`Associated Branch: ${targetBranch || '(None detected)'}`);
|
|
84
|
+
console.log(`Directory Only Mode: ${flags.dirOnly ? 'Yes (branches will be preserved)' : 'No'}`);
|
|
85
|
+
console.log(`Keep Remote Branch: ${flags.keepRemote ? 'Yes' : 'No'}\n`);
|
|
86
|
+
|
|
87
|
+
// Safety check: Never delete root repository
|
|
88
|
+
if (path.resolve(targetWorktreePath) === path.resolve(repoRoot)) {
|
|
89
|
+
console.error(`Error: Cannot delete the repository root directory "${repoRoot}".`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// If cwd is inside the target worktree, switch to rootDir/baseDir
|
|
94
|
+
if (path.resolve(process.cwd()).startsWith(path.resolve(targetWorktreePath))) {
|
|
95
|
+
console.log(`Current working directory is inside target worktree. Switching to: ${baseDir}`);
|
|
96
|
+
try {
|
|
97
|
+
process.chdir(baseDir);
|
|
98
|
+
} catch (e) {}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Initialize context for hooks
|
|
102
|
+
const ctx = createContext({
|
|
103
|
+
worktreePath: targetWorktreePath,
|
|
104
|
+
repoRoot,
|
|
105
|
+
bareRepo: activeConfig.repo.bareRepo,
|
|
106
|
+
branch: targetBranch,
|
|
107
|
+
flags,
|
|
108
|
+
preset: activeConfig.preset,
|
|
109
|
+
config: activeConfig,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Pre-Nuke Hook
|
|
113
|
+
if (typeof activeConfig.hooks.onPreNuke === 'function') {
|
|
114
|
+
await activeConfig.hooks.onPreNuke(ctx);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 1. Remove Worktree & Directory
|
|
118
|
+
console.log(`\n==> [1/3] Removing git worktree and directory...`);
|
|
119
|
+
const isRegisteredWorktree = worktrees.some(wt => path.resolve(wt.path) === path.resolve(targetWorktreePath));
|
|
120
|
+
|
|
121
|
+
if (isRegisteredWorktree) {
|
|
122
|
+
git.removeWorktree(targetWorktreePath, {
|
|
123
|
+
force: flags.force,
|
|
124
|
+
repoDir: repoRoot,
|
|
125
|
+
bareRepo: config.repo.bareRepo,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Prune any stale worktree entries
|
|
130
|
+
git.pruneWorktrees({ repoDir: repoRoot, bareRepo: config.repo.bareRepo });
|
|
131
|
+
|
|
132
|
+
// Remove any remaining files or untracked directory
|
|
133
|
+
if (fs.existsSync(targetWorktreePath)) {
|
|
134
|
+
console.log(`Removing remaining directory "${targetWorktreePath}"...`);
|
|
135
|
+
try {
|
|
136
|
+
fs.rmSync(targetWorktreePath, { recursive: true, force: true });
|
|
137
|
+
console.log(`Directory deleted successfully.`);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
console.error(`Failed to delete directory "${targetWorktreePath}": ${err.message}`);
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
console.log(`Worktree directory is clean.`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Helper for final cleanup steps (Herdr workspace close + Post-Nuke hook)
|
|
146
|
+
const finishCleanup = async () => {
|
|
147
|
+
// Post-Nuke Hook
|
|
148
|
+
if (typeof activeConfig.hooks.onPostNuke === 'function') {
|
|
149
|
+
await activeConfig.hooks.onPostNuke(ctx);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Close Herdr Workspace if running (performed last so active pane is not terminated mid-cleanup)
|
|
153
|
+
const prefix = activeConfig.workspace.labelPrefix || '';
|
|
154
|
+
const matchTargets = [
|
|
155
|
+
targetSessionName,
|
|
156
|
+
targetDirname,
|
|
157
|
+
targetBranch,
|
|
158
|
+
prefix ? `${prefix}${targetSessionName}` : null,
|
|
159
|
+
prefix ? `${prefix}${targetDirname}` : null,
|
|
160
|
+
].filter(Boolean);
|
|
161
|
+
|
|
162
|
+
herdr.closeWorkspacesMatching(matchTargets);
|
|
163
|
+
|
|
164
|
+
console.log(`\nCleanup complete!`);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// 2. Delete Local Branch
|
|
168
|
+
if (flags.dirOnly) {
|
|
169
|
+
console.log(`\n==> Skipping branch deletion (--dir-only / --keep-branch).`);
|
|
170
|
+
await finishCleanup();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const protectedBranches = activeConfig.repo.protectedBranches || ['main', 'master', 'develop', 'prod', 'staging', 'production'];
|
|
175
|
+
if (targetBranch && protectedBranches.includes(targetBranch)) {
|
|
176
|
+
console.warn(`\n==> WARNING: "${targetBranch}" is a protected branch. Skipping local and remote branch deletion.`);
|
|
177
|
+
await finishCleanup();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!targetBranch) {
|
|
182
|
+
console.log(`\n==> No branch associated with "${target}". Skipping branch deletion.`);
|
|
183
|
+
await finishCleanup();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
console.log(`\n==> [2/3] Deleting local branch "${targetBranch}"...`);
|
|
188
|
+
const gitOpts = { repoDir: repoRoot, bareRepo: config.repo.bareRepo };
|
|
189
|
+
const existsLocally = git.branchExistsLocally(targetBranch, gitOpts);
|
|
190
|
+
|
|
191
|
+
if (existsLocally) {
|
|
192
|
+
git.deleteLocalBranch(targetBranch, gitOpts);
|
|
193
|
+
} else {
|
|
194
|
+
console.log(`Local branch "${targetBranch}" does not exist or was already deleted.`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 3. Delete Remote Branch
|
|
198
|
+
if (flags.keepRemote) {
|
|
199
|
+
console.log(`\n==> Skipping remote branch deletion (--keep-remote / --local-only).`);
|
|
200
|
+
await finishCleanup();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
console.log(`\n==> [3/3] Deleting remote branch "${targetBranch}" from origin...`);
|
|
205
|
+
const existsRemotely = git.branchExistsRemotely(targetBranch, gitOpts);
|
|
206
|
+
|
|
207
|
+
if (existsRemotely) {
|
|
208
|
+
git.deleteRemoteBranch(targetBranch, gitOpts);
|
|
209
|
+
} else {
|
|
210
|
+
console.log(`Remote branch "${targetBranch}" does not exist on origin or was already deleted.`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
await finishCleanup();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = {
|
|
217
|
+
executeNuke,
|
|
218
|
+
};
|
package/lib/skill.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const SKILL_CONTENT = `---
|
|
6
|
+
name: arise
|
|
7
|
+
description: User guide, CLI reference, and command executor for the \`arise\` utility (Git worktree and Herdr workspace orchestrator). Activate this skill whenever the user asks questions about how to use arise, how worktree orchestration works, how to configure \`.ariserc.json\` or \`.worktreerc.json\`, or asks the assistant to create, switch to, or nuke/clean up Git worktrees and Herdr workspaces on their behalf.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Arise Operator & Assistant Guide
|
|
11
|
+
|
|
12
|
+
Use this skill to answer questions about \`arise\` and run worktree management commands on the user's behalf.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 1. CLI Quick Reference & Cheatsheet
|
|
17
|
+
|
|
18
|
+
### Creating Worktrees
|
|
19
|
+
\`\`\`bash
|
|
20
|
+
# Create or open a worktree for a branch (auto-detects preset, boots Herdr workspace)
|
|
21
|
+
arise --branch <branch-name>
|
|
22
|
+
|
|
23
|
+
# Create worktree based off a specific base branch (e.g. develop, prod, main)
|
|
24
|
+
arise --branch <branch-name> --base <base-branch>
|
|
25
|
+
|
|
26
|
+
# Custom directory name or workspace name
|
|
27
|
+
arise -b <branch-name> -d <dir-name> -w <workspace-name>
|
|
28
|
+
|
|
29
|
+
# Explicit preset or pane focus ('agy', 'vim', 'logs', 'server', 'shell')
|
|
30
|
+
arise -b <branch-name> --preset laravel --focus agy
|
|
31
|
+
\`\`\`
|
|
32
|
+
|
|
33
|
+
### Nuking / Teardown
|
|
34
|
+
\`\`\`bash
|
|
35
|
+
# Nuke active worktree (when run inside a worktree directory)
|
|
36
|
+
arise --nuke
|
|
37
|
+
|
|
38
|
+
# Nuke specific worktree by branch or directory name
|
|
39
|
+
arise --nuke <branch-or-dir>
|
|
40
|
+
|
|
41
|
+
# Remove directory only (keep local and remote git branches)
|
|
42
|
+
arise --nuke <target> --dir-only
|
|
43
|
+
|
|
44
|
+
# Delete local branch and directory, but keep remote branch on origin
|
|
45
|
+
arise --nuke <target> --keep-remote
|
|
46
|
+
|
|
47
|
+
# Force removal even if uncommitted changes exist
|
|
48
|
+
arise --nuke <target> --force
|
|
49
|
+
\`\`\`
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 2. Configuration (\`.ariserc.json\` / \`.worktreerc.json\` / \`arise.config.js\`)
|
|
54
|
+
|
|
55
|
+
Projects can configure custom topology, bare repos, and layouts via \`.ariserc.json\` or \`.worktreerc.json\` in the repository root or base directory:
|
|
56
|
+
|
|
57
|
+
\`\`\`json
|
|
58
|
+
{
|
|
59
|
+
"preset": "laravel",
|
|
60
|
+
"repo": {
|
|
61
|
+
"bareRepo": "/path/to/bare.git",
|
|
62
|
+
"worktreesBase": "/path/to/worktrees",
|
|
63
|
+
"defaultBaseBranch": "develop",
|
|
64
|
+
"protectedBranches": ["main", "master", "develop", "prod", "staging"]
|
|
65
|
+
},
|
|
66
|
+
"workspace": {
|
|
67
|
+
"labelPrefix": "[API] ",
|
|
68
|
+
"defaultFocus": "agy"
|
|
69
|
+
},
|
|
70
|
+
"scaffold": {
|
|
71
|
+
"envSource": "/path/to/shared/.env",
|
|
72
|
+
"symlink": "/path/to/webserver/symlink",
|
|
73
|
+
"install": "composer install --no-interaction"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
\`\`\`
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 3. How to Assist the User
|
|
81
|
+
|
|
82
|
+
### When the User Asks Questions:
|
|
83
|
+
1. Consult the CLI options and configuration schema above.
|
|
84
|
+
2. If working inside a repository with local documentation or \`.ariserc.json\` / \`.worktreerc.json\`, inspect those files.
|
|
85
|
+
3. Provide clear explanations with executable CLI examples and config snippets.
|
|
86
|
+
|
|
87
|
+
### When the User Asks You to Perform an Action:
|
|
88
|
+
1. Formulate the appropriate \`arise\` CLI command.
|
|
89
|
+
2. Execute the command on behalf of the user using the available command runner.
|
|
90
|
+
3. Confirm the status of the created or nuked worktree and Herdr workspace.
|
|
91
|
+
`;
|
|
92
|
+
|
|
93
|
+
function ensureSourceSkillExists(repoRoot) {
|
|
94
|
+
const sourceDir = path.join(repoRoot, 'skills', 'arise');
|
|
95
|
+
const sourceFile = path.join(sourceDir, 'SKILL.md');
|
|
96
|
+
if (fs.existsSync(sourceFile)) {
|
|
97
|
+
return sourceDir;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
fs.mkdirSync(sourceDir, { recursive: true });
|
|
101
|
+
fs.writeFileSync(sourceFile, SKILL_CONTENT, 'utf8');
|
|
102
|
+
} catch (err) {
|
|
103
|
+
// Proceed with sourceDir even if write fails (e.g. read-only global node_modules)
|
|
104
|
+
}
|
|
105
|
+
return sourceDir;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function safeSymlink(target, linkPath) {
|
|
109
|
+
try {
|
|
110
|
+
if (fs.existsSync(linkPath) || fs.lstatSync(linkPath).isSymbolicLink()) {
|
|
111
|
+
fs.rmSync(linkPath, { recursive: true, force: true });
|
|
112
|
+
}
|
|
113
|
+
} catch (err) {
|
|
114
|
+
// If doesn't exist, proceed
|
|
115
|
+
}
|
|
116
|
+
fs.symlinkSync(target, linkPath, 'dir');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function installSkill(options = {}) {
|
|
120
|
+
const scope = options.scope || 'global';
|
|
121
|
+
const cwd = options.cwd || process.cwd();
|
|
122
|
+
const homedir = options.homedir || os.homedir();
|
|
123
|
+
const repoRoot = options.repoRoot || path.resolve(__dirname, '..');
|
|
124
|
+
|
|
125
|
+
const sourceDir = ensureSourceSkillExists(repoRoot);
|
|
126
|
+
const installedPaths = [];
|
|
127
|
+
|
|
128
|
+
if (scope === 'local' || scope === 'workspace') {
|
|
129
|
+
const localAgentsSkillsDir = path.join(cwd, '.agents', 'skills');
|
|
130
|
+
fs.mkdirSync(localAgentsSkillsDir, { recursive: true });
|
|
131
|
+
const localLink = path.join(localAgentsSkillsDir, 'arise');
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
safeSymlink(sourceDir, localLink);
|
|
135
|
+
installedPaths.push(localLink);
|
|
136
|
+
console.log(`✓ Symlinked local agent skill: ${localLink} -> ${sourceDir}`);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
// Fallback to direct file copy if symlink creation fails
|
|
139
|
+
const fallbackFile = path.join(localLink, 'SKILL.md');
|
|
140
|
+
fs.mkdirSync(localLink, { recursive: true });
|
|
141
|
+
fs.writeFileSync(fallbackFile, SKILL_CONTENT, 'utf8');
|
|
142
|
+
installedPaths.push(fallbackFile);
|
|
143
|
+
console.log(`✓ Installed local agent skill (copy): ${fallbackFile}`);
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
// Universal ~/.agents/skills/ directory
|
|
147
|
+
const globalAgentsSkillsDir = path.join(homedir, '.agents', 'skills');
|
|
148
|
+
fs.mkdirSync(globalAgentsSkillsDir, { recursive: true });
|
|
149
|
+
const globalAgentsLink = path.join(globalAgentsSkillsDir, 'arise');
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
safeSymlink(sourceDir, globalAgentsLink);
|
|
153
|
+
installedPaths.push(globalAgentsLink);
|
|
154
|
+
console.log(`✓ Symlinked global agent skill: ${globalAgentsLink} -> ${sourceDir}`);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
// Fallback to copy if symlink permissions fail
|
|
157
|
+
const fallbackDir = path.join(globalAgentsSkillsDir, 'arise');
|
|
158
|
+
fs.mkdirSync(fallbackDir, { recursive: true });
|
|
159
|
+
const targetFile = path.join(fallbackDir, 'SKILL.md');
|
|
160
|
+
fs.writeFileSync(targetFile, SKILL_CONTENT, 'utf8');
|
|
161
|
+
installedPaths.push(targetFile);
|
|
162
|
+
console.log(`✓ Installed global agent skill (copy): ${targetFile}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Link ~/.gemini/skills/arise for Antigravity CLI (agy)
|
|
166
|
+
const geminiSkillsDir = path.join(homedir, '.gemini', 'skills');
|
|
167
|
+
try {
|
|
168
|
+
fs.mkdirSync(geminiSkillsDir, { recursive: true });
|
|
169
|
+
const geminiLink = path.join(geminiSkillsDir, 'arise');
|
|
170
|
+
safeSymlink(globalAgentsLink, geminiLink);
|
|
171
|
+
installedPaths.push(geminiLink);
|
|
172
|
+
console.log(`✓ Linked to Antigravity skills: ${geminiLink} -> ${globalAgentsLink}`);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
// Non-fatal
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Link ~/.claude/skills/arise if Claude exists
|
|
178
|
+
const claudeDir = path.join(homedir, '.claude');
|
|
179
|
+
if (fs.existsSync(claudeDir)) {
|
|
180
|
+
try {
|
|
181
|
+
const claudeSkillsDir = path.join(claudeDir, 'skills');
|
|
182
|
+
fs.mkdirSync(claudeSkillsDir, { recursive: true });
|
|
183
|
+
const claudeLink = path.join(claudeSkillsDir, 'arise');
|
|
184
|
+
safeSymlink(globalAgentsLink, claudeLink);
|
|
185
|
+
installedPaths.push(claudeLink);
|
|
186
|
+
console.log(`✓ Linked to Claude skills: ${claudeLink} -> ${globalAgentsLink}`);
|
|
187
|
+
} catch (err) {
|
|
188
|
+
// Non-fatal
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return installedPaths;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = {
|
|
197
|
+
SKILL_CONTENT,
|
|
198
|
+
ensureSourceSkillExists,
|
|
199
|
+
installSkill,
|
|
200
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tsuzuku/arise",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"description": "Arise - Unified workplace-agnostic Git worktree and Herdr workspace orchestrator with pluggable project presets",
|
|
8
|
+
"main": "index.js",
|
|
9
|
+
"bin": {
|
|
10
|
+
"arise": "bin/cli.js",
|
|
11
|
+
"herdr-worktree": "bin/cli.js",
|
|
12
|
+
"herder-worktree": "bin/cli.js",
|
|
13
|
+
"hwk": "bin/cli.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"bin",
|
|
17
|
+
"lib",
|
|
18
|
+
"presets",
|
|
19
|
+
"skills",
|
|
20
|
+
"index.js",
|
|
21
|
+
"types.d.ts",
|
|
22
|
+
"arise.schema.json",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18.0.0"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test": "node --test test/*.test.js"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"git",
|
|
33
|
+
"worktree",
|
|
34
|
+
"herdr",
|
|
35
|
+
"workspace",
|
|
36
|
+
"laravel",
|
|
37
|
+
"node",
|
|
38
|
+
"cli"
|
|
39
|
+
],
|
|
40
|
+
"author": "",
|
|
41
|
+
"license": "ISC"
|
|
42
|
+
}
|