@tsuzuku/arise 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/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
+ }