@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.
@@ -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
+ };
@@ -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
+ };
@@ -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
+ };