@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
|
@@ -0,0 +1,211 @@
|
|
|
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 { showUsage } = require('../cli');
|
|
8
|
+
|
|
9
|
+
async function executeNuke(flags, config, cwd = process.cwd()) {
|
|
10
|
+
const repoRoot = git.getRepoRootDir(cwd, config.repo.bareRepo);
|
|
11
|
+
const worktrees = git.getWorktrees({ repoDir: repoRoot, bareRepo: config.repo.bareRepo });
|
|
12
|
+
|
|
13
|
+
let target = flags.cleanupTarget || flags.dirname || flags.branch;
|
|
14
|
+
|
|
15
|
+
// 1. Auto-detect worktree if invoked inside a worktree directory without arguments
|
|
16
|
+
if (!target) {
|
|
17
|
+
const currentCwd = path.resolve(cwd);
|
|
18
|
+
const matchingWt = worktrees.find(wt => {
|
|
19
|
+
const resolvedWtPath = path.resolve(wt.path);
|
|
20
|
+
return resolvedWtPath !== path.resolve(repoRoot) && (
|
|
21
|
+
currentCwd === resolvedWtPath ||
|
|
22
|
+
currentCwd.startsWith(resolvedWtPath + path.sep)
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (matchingWt) {
|
|
27
|
+
target = matchingWt.path;
|
|
28
|
+
console.log(`Auto-detected current worktree: "${target}"`);
|
|
29
|
+
} else {
|
|
30
|
+
console.error('Error: --nuke / --cleanup requires a worktree directory name or branch when run from the root repository.');
|
|
31
|
+
showUsage();
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 2. Resolve worktree target path and matching branch
|
|
37
|
+
const baseDir = config.repo.worktreesBase || repoRoot;
|
|
38
|
+
const resolvedTarget = path.isAbsolute(target) ? path.resolve(target) : path.resolve(baseDir, target);
|
|
39
|
+
|
|
40
|
+
const existingWorktree = worktrees.find(wt => {
|
|
41
|
+
return (
|
|
42
|
+
path.resolve(wt.path) === resolvedTarget ||
|
|
43
|
+
path.basename(wt.path) === target ||
|
|
44
|
+
(wt.branch && wt.branch === target) ||
|
|
45
|
+
(flags.branch && wt.branch === flags.branch)
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const targetWorktreePath = existingWorktree ? existingWorktree.path : resolvedTarget;
|
|
50
|
+
const targetDirname = path.basename(targetWorktreePath);
|
|
51
|
+
const targetSessionName = targetDirname.replace(/[\s.:]/g, '_');
|
|
52
|
+
|
|
53
|
+
let targetBranch = flags.branch;
|
|
54
|
+
if (!targetBranch && existingWorktree && existingWorktree.branch) {
|
|
55
|
+
targetBranch = existingWorktree.branch;
|
|
56
|
+
}
|
|
57
|
+
if (!targetBranch && fs.existsSync(targetWorktreePath)) {
|
|
58
|
+
try {
|
|
59
|
+
const detected = execSync(`git -C "${targetWorktreePath}" rev-parse --abbrev-ref HEAD`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
60
|
+
if (detected && detected !== 'HEAD') {
|
|
61
|
+
targetBranch = detected;
|
|
62
|
+
}
|
|
63
|
+
} catch (e) {}
|
|
64
|
+
}
|
|
65
|
+
if (!targetBranch) {
|
|
66
|
+
try {
|
|
67
|
+
const gitCmd = git.getGitPrefix({ repoDir: repoRoot, bareRepo: config.repo.bareRepo });
|
|
68
|
+
execSync(`${gitCmd} rev-parse --verify "${target}"`, { stdio: 'ignore' });
|
|
69
|
+
targetBranch = target;
|
|
70
|
+
} catch (e) {}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log(`\n=== Starting Worktree Nuke ===`);
|
|
74
|
+
console.log(`Active Preset: ${config.preset ? config.preset.name : 'generic'}`);
|
|
75
|
+
console.log(`Repository Root: ${repoRoot}`);
|
|
76
|
+
console.log(`Target Worktree Path: ${targetWorktreePath}`);
|
|
77
|
+
console.log(`Associated Branch: ${targetBranch || '(None detected)'}`);
|
|
78
|
+
console.log(`Directory Only Mode: ${flags.dirOnly ? 'Yes (branches will be preserved)' : 'No'}`);
|
|
79
|
+
console.log(`Keep Remote Branch: ${flags.keepRemote ? 'Yes' : 'No'}\n`);
|
|
80
|
+
|
|
81
|
+
// Safety check: Never delete root repository
|
|
82
|
+
if (path.resolve(targetWorktreePath) === path.resolve(repoRoot)) {
|
|
83
|
+
console.error(`Error: Cannot delete the repository root directory "${repoRoot}".`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// If cwd is inside the target worktree, switch to rootDir/baseDir
|
|
88
|
+
if (path.resolve(process.cwd()).startsWith(path.resolve(targetWorktreePath))) {
|
|
89
|
+
console.log(`Current working directory is inside target worktree. Switching to: ${baseDir}`);
|
|
90
|
+
try {
|
|
91
|
+
process.chdir(baseDir);
|
|
92
|
+
} catch (e) {}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Initialize context for hooks
|
|
96
|
+
const ctx = createContext({
|
|
97
|
+
worktreePath: targetWorktreePath,
|
|
98
|
+
repoRoot,
|
|
99
|
+
bareRepo: config.repo.bareRepo,
|
|
100
|
+
branch: targetBranch,
|
|
101
|
+
flags,
|
|
102
|
+
preset: config.preset,
|
|
103
|
+
config,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Pre-Nuke Hook
|
|
107
|
+
if (typeof config.hooks.onPreNuke === 'function') {
|
|
108
|
+
await config.hooks.onPreNuke(ctx);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 1. Close Herdr Workspace if running
|
|
112
|
+
const prefix = config.workspace.labelPrefix || '';
|
|
113
|
+
const matchTargets = [
|
|
114
|
+
targetSessionName,
|
|
115
|
+
targetDirname,
|
|
116
|
+
targetBranch,
|
|
117
|
+
prefix ? `${prefix}${targetSessionName}` : null,
|
|
118
|
+
prefix ? `${prefix}${targetDirname}` : null,
|
|
119
|
+
].filter(Boolean);
|
|
120
|
+
|
|
121
|
+
herdr.closeWorkspacesMatching(matchTargets);
|
|
122
|
+
|
|
123
|
+
// 2. Remove Worktree & Directory
|
|
124
|
+
console.log(`\n==> [1/3] Removing git worktree and directory...`);
|
|
125
|
+
const isRegisteredWorktree = worktrees.some(wt => path.resolve(wt.path) === path.resolve(targetWorktreePath));
|
|
126
|
+
|
|
127
|
+
if (isRegisteredWorktree) {
|
|
128
|
+
git.removeWorktree(targetWorktreePath, {
|
|
129
|
+
force: flags.force,
|
|
130
|
+
repoDir: repoRoot,
|
|
131
|
+
bareRepo: config.repo.bareRepo,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Prune any stale worktree entries
|
|
136
|
+
git.pruneWorktrees({ repoDir: repoRoot, bareRepo: config.repo.bareRepo });
|
|
137
|
+
|
|
138
|
+
// Remove any remaining files or untracked directory
|
|
139
|
+
if (fs.existsSync(targetWorktreePath)) {
|
|
140
|
+
console.log(`Removing remaining directory "${targetWorktreePath}"...`);
|
|
141
|
+
try {
|
|
142
|
+
fs.rmSync(targetWorktreePath, { recursive: true, force: true });
|
|
143
|
+
console.log(`Directory deleted successfully.`);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
console.error(`Failed to delete directory "${targetWorktreePath}": ${err.message}`);
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
console.log(`Worktree directory is clean.`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 3. Delete Local Branch
|
|
152
|
+
if (flags.dirOnly) {
|
|
153
|
+
console.log(`\n==> Skipping branch deletion (--dir-only / --keep-branch).`);
|
|
154
|
+
if (typeof config.hooks.onPostNuke === 'function') await config.hooks.onPostNuke(ctx);
|
|
155
|
+
console.log(`\nCleanup complete!`);
|
|
156
|
+
process.exit(0);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const protectedBranches = config.repo.protectedBranches || ['main', 'master', 'develop', 'prod', 'staging', 'production'];
|
|
160
|
+
if (targetBranch && protectedBranches.includes(targetBranch)) {
|
|
161
|
+
console.warn(`\n==> WARNING: "${targetBranch}" is a protected branch. Skipping local and remote branch deletion.`);
|
|
162
|
+
if (typeof config.hooks.onPostNuke === 'function') await config.hooks.onPostNuke(ctx);
|
|
163
|
+
console.log(`\nCleanup complete!`);
|
|
164
|
+
process.exit(0);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!targetBranch) {
|
|
168
|
+
console.log(`\n==> No branch associated with "${target}". Skipping branch deletion.`);
|
|
169
|
+
if (typeof config.hooks.onPostNuke === 'function') await config.hooks.onPostNuke(ctx);
|
|
170
|
+
console.log(`\nCleanup complete!`);
|
|
171
|
+
process.exit(0);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
console.log(`\n==> [2/3] Deleting local branch "${targetBranch}"...`);
|
|
175
|
+
const gitOpts = { repoDir: repoRoot, bareRepo: config.repo.bareRepo };
|
|
176
|
+
const existsLocally = git.branchExistsLocally(targetBranch, gitOpts);
|
|
177
|
+
|
|
178
|
+
if (existsLocally) {
|
|
179
|
+
git.deleteLocalBranch(targetBranch, gitOpts);
|
|
180
|
+
} else {
|
|
181
|
+
console.log(`Local branch "${targetBranch}" does not exist or was already deleted.`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 4. Delete Remote Branch
|
|
185
|
+
if (flags.keepRemote) {
|
|
186
|
+
console.log(`\n==> Skipping remote branch deletion (--keep-remote / --local-only).`);
|
|
187
|
+
if (typeof config.hooks.onPostNuke === 'function') await config.hooks.onPostNuke(ctx);
|
|
188
|
+
console.log(`\nCleanup complete!`);
|
|
189
|
+
process.exit(0);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
console.log(`\n==> [3/3] Deleting remote branch "${targetBranch}" from origin...`);
|
|
193
|
+
const existsRemotely = git.branchExistsRemotely(targetBranch, gitOpts);
|
|
194
|
+
|
|
195
|
+
if (existsRemotely) {
|
|
196
|
+
git.deleteRemoteBranch(targetBranch, gitOpts);
|
|
197
|
+
} else {
|
|
198
|
+
console.log(`Remote branch "${targetBranch}" does not exist on origin or was already deleted.`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Post-Nuke Hook
|
|
202
|
+
if (typeof config.hooks.onPostNuke === 'function') {
|
|
203
|
+
await config.hooks.onPostNuke(ctx);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
console.log(`\nCleanup complete!`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
module.exports = {
|
|
210
|
+
executeNuke,
|
|
211
|
+
};
|
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,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tsuzuku/arise",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Arise - Unified workplace-agnostic Git worktree and Herdr workspace orchestrator with pluggable project presets",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"arise": "bin/cli.js",
|
|
8
|
+
"herdr-worktree": "bin/cli.js",
|
|
9
|
+
"herder-worktree": "bin/cli.js",
|
|
10
|
+
"hwk": "bin/cli.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"lib",
|
|
15
|
+
"presets",
|
|
16
|
+
"skills",
|
|
17
|
+
"index.js",
|
|
18
|
+
"types.d.ts",
|
|
19
|
+
"arise.schema.json",
|
|
20
|
+
"worktree.schema.json",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18.0.0"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"test": "node --test test/*.test.js"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"git",
|
|
31
|
+
"worktree",
|
|
32
|
+
"herdr",
|
|
33
|
+
"workspace",
|
|
34
|
+
"laravel",
|
|
35
|
+
"node",
|
|
36
|
+
"cli"
|
|
37
|
+
],
|
|
38
|
+
"author": "",
|
|
39
|
+
"license": "ISC"
|
|
40
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
name: 'generic',
|
|
6
|
+
|
|
7
|
+
detect(cwd) {
|
|
8
|
+
return true; // Fallback preset
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
repo: {
|
|
12
|
+
defaultBaseBranch: 'main',
|
|
13
|
+
protectedBranches: ['main', 'master', 'develop', 'prod', 'staging', 'production'],
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
workspace: {
|
|
17
|
+
labelPrefix: '',
|
|
18
|
+
defaultFocus: 'agy',
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
layout: [
|
|
22
|
+
{ id: 'vim', title: 'vim', cmd: 'vim .', position: 'root' },
|
|
23
|
+
{ id: 'shell', title: 'shell', cmd: null, split: 'right', from: 'vim' },
|
|
24
|
+
{ id: 'agy', title: 'agy', cmd: 'agy', split: 'down', from: 'shell', focus: true },
|
|
25
|
+
],
|
|
26
|
+
|
|
27
|
+
hooks: {
|
|
28
|
+
async onScaffold(ctx) {
|
|
29
|
+
// Copy .env from root if available
|
|
30
|
+
ctx.copyFromRoot('.env', '.env');
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
};
|
package/presets/index.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const nodePreset = require('./node');
|
|
2
|
+
const laravelPreset = require('./laravel');
|
|
3
|
+
const genericPreset = require('./generic');
|
|
4
|
+
|
|
5
|
+
const builtInPresets = [
|
|
6
|
+
laravelPreset, // Check Laravel/PHP before generic
|
|
7
|
+
nodePreset, // Check Node before generic
|
|
8
|
+
genericPreset, // Catch-all fallback
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
function getPreset(name) {
|
|
12
|
+
if (!name) return null;
|
|
13
|
+
const normalized = name.toLowerCase().trim();
|
|
14
|
+
if (normalized === 'php' || normalized === 'laravel' || normalized === 'api' || normalized === 'be') {
|
|
15
|
+
return laravelPreset;
|
|
16
|
+
}
|
|
17
|
+
if (normalized === 'node' || normalized === 'js' || normalized === 'ts' || normalized === 'fe' || normalized === 'react') {
|
|
18
|
+
return nodePreset;
|
|
19
|
+
}
|
|
20
|
+
if (normalized === 'generic' || normalized === 'default') {
|
|
21
|
+
return genericPreset;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function detectPreset(cwd = process.cwd()) {
|
|
27
|
+
for (const preset of builtInPresets) {
|
|
28
|
+
if (typeof preset.detect === 'function' && preset.detect(cwd)) {
|
|
29
|
+
return preset;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return genericPreset;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = {
|
|
36
|
+
getPreset,
|
|
37
|
+
detectPreset,
|
|
38
|
+
builtInPresets,
|
|
39
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const git = require('../lib/git');
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
name: 'laravel',
|
|
7
|
+
|
|
8
|
+
detect(cwd) {
|
|
9
|
+
return fs.existsSync(path.join(cwd, 'artisan')) || fs.existsSync(path.join(cwd, 'composer.json'));
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
repo: {
|
|
13
|
+
defaultBaseBranch: 'main',
|
|
14
|
+
protectedBranches: ['staging', 'prod', 'master', 'main', 'develop'],
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
workspace: {
|
|
18
|
+
labelPrefix: '',
|
|
19
|
+
defaultFocus: 'agy',
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
layout: [
|
|
23
|
+
{ id: 'vim', title: 'vim', cmd: 'vim .', position: 'root' },
|
|
24
|
+
{ id: 'logs', title: 'logs', cmd: 'tail -f storage/logs/laravel.log', split: 'right', from: 'vim' },
|
|
25
|
+
{ id: 'shell', title: 'shell', cmd: null, split: 'down', from: 'vim' },
|
|
26
|
+
{ id: 'agy', title: 'agy', cmd: 'agy', split: 'down', from: 'logs', focus: true },
|
|
27
|
+
],
|
|
28
|
+
|
|
29
|
+
hooks: {
|
|
30
|
+
async onSyncPrimary(ctx) {
|
|
31
|
+
git.syncPrimaryBranch({
|
|
32
|
+
branch: ctx.branch,
|
|
33
|
+
worktreePath: ctx.worktreePath,
|
|
34
|
+
repoDir: ctx.repoRoot,
|
|
35
|
+
bareRepo: ctx.bareRepo,
|
|
36
|
+
protectedBranches: ['staging', 'prod', 'master', 'main', 'develop'],
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
async onScaffold(ctx) {
|
|
41
|
+
// 1. Environment (.env) Setup
|
|
42
|
+
console.log('==> Ensuring .env file...');
|
|
43
|
+
const rootEnv = ctx.repoRoot ? path.join(ctx.repoRoot, '.env') : null;
|
|
44
|
+
const targetEnv = path.join(ctx.worktreePath, '.env');
|
|
45
|
+
|
|
46
|
+
if (!fs.existsSync(targetEnv)) {
|
|
47
|
+
if (ctx.config.scaffold && ctx.config.scaffold.envSource && fs.existsSync(ctx.config.scaffold.envSource)) {
|
|
48
|
+
ctx.copyFile(ctx.config.scaffold.envSource, targetEnv);
|
|
49
|
+
} else if (rootEnv && fs.existsSync(rootEnv)) {
|
|
50
|
+
ctx.copyFile(rootEnv, targetEnv);
|
|
51
|
+
} else {
|
|
52
|
+
console.warn('==> No .env source found to copy.');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 2. Dependencies & Initialization
|
|
57
|
+
console.log('==> Initializing repository (permissions, composer, etc.)...');
|
|
58
|
+
try {
|
|
59
|
+
const installCmd = (ctx.config.scaffold && ctx.config.scaffold.install !== undefined)
|
|
60
|
+
? ctx.config.scaffold.install
|
|
61
|
+
: 'composer install --no-interaction';
|
|
62
|
+
|
|
63
|
+
if (installCmd) {
|
|
64
|
+
console.log(`Running dependency installation: "${installCmd}" in "${ctx.worktreePath}"...`);
|
|
65
|
+
ctx.exec(installCmd);
|
|
66
|
+
} else {
|
|
67
|
+
console.log('Skipping composer installation (scaffold.install is disabled).');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const permissionsScript = `
|
|
71
|
+
mkdir -p storage/logs
|
|
72
|
+
chmod 777 storage/logs
|
|
73
|
+
touch storage/logs/laravel.log
|
|
74
|
+
chmod 666 storage/logs/laravel.log
|
|
75
|
+
mkdir -p storage/framework/sessions
|
|
76
|
+
chmod o+w storage/framework/sessions
|
|
77
|
+
mkdir -p storage/framework/data
|
|
78
|
+
chmod o+w storage/framework/data
|
|
79
|
+
mkdir -p storage/framework/cache/data
|
|
80
|
+
chmod 777 storage/framework/cache/data
|
|
81
|
+
mkdir -p bootstrap/cache
|
|
82
|
+
chmod 777 bootstrap/cache
|
|
83
|
+
mkdir -p storage/temp
|
|
84
|
+
chmod 777 storage/temp
|
|
85
|
+
if [ -d .githooks ]; then git config core.hooksPath .githooks; fi
|
|
86
|
+
`;
|
|
87
|
+
ctx.exec(permissionsScript);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
console.warn(`==> Warning during repository initialization: ${err.message}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 3. Update Web Server Symlink if configured
|
|
93
|
+
const symlinkPath = ctx.config.scaffold && ctx.config.scaffold.symlink;
|
|
94
|
+
if (symlinkPath) {
|
|
95
|
+
ctx.setSymlink(symlinkPath, ctx.worktreePath);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
async onPreNuke(ctx) {
|
|
100
|
+
const symlinkPath = ctx.config.scaffold && ctx.config.scaffold.symlink;
|
|
101
|
+
if (symlinkPath && fs.existsSync(symlinkPath)) {
|
|
102
|
+
try {
|
|
103
|
+
const lstat = fs.lstatSync(symlinkPath);
|
|
104
|
+
if (lstat.isSymbolicLink()) {
|
|
105
|
+
const currentTarget = fs.realpathSync(symlinkPath);
|
|
106
|
+
if (path.resolve(currentTarget) === path.resolve(ctx.worktreePath)) {
|
|
107
|
+
console.log(`==> Unlinking active web symlink "${symlinkPath}" pointing to nuked worktree...`);
|
|
108
|
+
fs.unlinkSync(symlinkPath);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch (e) {}
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
};
|
package/presets/node.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
name: 'node',
|
|
6
|
+
|
|
7
|
+
detect(cwd) {
|
|
8
|
+
return fs.existsSync(path.join(cwd, 'package.json'));
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
repo: {
|
|
12
|
+
defaultBaseBranch: 'develop',
|
|
13
|
+
protectedBranches: ['main', 'master', 'develop', 'prod', 'staging', 'production'],
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
workspace: {
|
|
17
|
+
labelPrefix: '',
|
|
18
|
+
defaultFocus: 'agy',
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
layout: [
|
|
22
|
+
{ id: 'vim', title: 'vim', cmd: 'vim .', position: 'root' },
|
|
23
|
+
{ id: 'server', title: 'npm server', cmd: 'npm run dev', split: 'right', from: 'vim' },
|
|
24
|
+
{ id: 'shell', title: 'shell', cmd: null, split: 'down', from: 'vim' },
|
|
25
|
+
{ id: 'agy', title: 'agy', cmd: 'agy', split: 'down', from: 'server', focus: true },
|
|
26
|
+
],
|
|
27
|
+
|
|
28
|
+
hooks: {
|
|
29
|
+
async onScaffold(ctx) {
|
|
30
|
+
// 1. Copy .env from root if available
|
|
31
|
+
ctx.copyFromRoot('.env', '.env');
|
|
32
|
+
|
|
33
|
+
// 2. Install dependencies
|
|
34
|
+
const installCmd = (ctx.config.scaffold && ctx.config.scaffold.install !== undefined)
|
|
35
|
+
? ctx.config.scaffold.install
|
|
36
|
+
: 'npm install';
|
|
37
|
+
|
|
38
|
+
if (installCmd) {
|
|
39
|
+
console.log(`Running dependency installation: "${installCmd}" in "${ctx.worktreePath}"...`);
|
|
40
|
+
try {
|
|
41
|
+
ctx.exec(installCmd);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.warn(`Warning: Dependency installation exited with an error: ${err.message}`);
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
console.log('Skipping dependency installation (scaffold.install is disabled).');
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|