@tsuzuku/arise 1.1.0 → 1.1.1

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/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ const pkg = require('./package.json');
1
2
  const { parseArgs, showUsage } = require('./lib/cli');
2
3
  const { resolveConfiguration } = require('./lib/config');
3
4
  const { executeCreate } = require('./lib/lifecycle/create');
@@ -12,7 +13,7 @@ async function run(argv = process.argv.slice(2), cwd = process.cwd()) {
12
13
  }
13
14
 
14
15
  if (flags.showVersion) {
15
- console.log('arise v1.0.0');
16
+ console.log(`arise v${pkg.version}`);
16
17
  process.exit(0);
17
18
  }
18
19
 
package/lib/config.js CHANGED
@@ -70,7 +70,56 @@ function loadConfigFile(filePath) {
70
70
 
71
71
  function resolveConfiguration(flags = {}, cwd = process.cwd()) {
72
72
  const repoRoot = git.getRepoRootDir(cwd);
73
- const configFile = findConfigFile([cwd, repoRoot]);
73
+ const searchDirs = [];
74
+
75
+ const addSearchDir = (dir) => {
76
+ if (dir && !searchDirs.includes(dir)) {
77
+ searchDirs.push(dir);
78
+ }
79
+ };
80
+
81
+ let worktrees = [];
82
+ try {
83
+ worktrees = git.getWorktrees({ repoDir: repoRoot });
84
+ } catch (e) {}
85
+
86
+ // 1. Explicit directory if --dirname / -d is passed
87
+ if (flags.dirname) {
88
+ const resolvedDir = path.isAbsolute(flags.dirname)
89
+ ? flags.dirname
90
+ : path.resolve(repoRoot || cwd, flags.dirname);
91
+ addSearchDir(resolvedDir);
92
+ }
93
+
94
+ // 2. Directory for the specified branch (--branch / -b)
95
+ if (flags.branch) {
96
+ const matchingWt = worktrees.find((wt) => wt.branch === flags.branch);
97
+ if (matchingWt && matchingWt.path) {
98
+ addSearchDir(matchingWt.path);
99
+ }
100
+ const branchDir = path.resolve(repoRoot || cwd, flags.branch.replace(/\//g, '-'));
101
+ addSearchDir(branchDir);
102
+ }
103
+
104
+ // 3. Current working directory
105
+ addSearchDir(cwd);
106
+
107
+ // 4. Repository root directory
108
+ if (repoRoot) {
109
+ addSearchDir(repoRoot);
110
+ }
111
+
112
+ // 5. Fallback to existing worktrees (prioritizing primary branches like main, master, develop)
113
+ const primaryBranches = ['main', 'master', 'develop', 'prod', 'staging'];
114
+ const primaryWts = worktrees.filter((wt) => !wt.isBare && wt.branch && primaryBranches.includes(wt.branch));
115
+ for (const wt of primaryWts) {
116
+ if (wt.path) addSearchDir(wt.path);
117
+ }
118
+ for (const wt of worktrees) {
119
+ if (wt.path && !wt.isBare) addSearchDir(wt.path);
120
+ }
121
+
122
+ const configFile = findConfigFile(searchDirs);
74
123
  const fileConfig = loadConfigFile(configFile);
75
124
 
76
125
  // 1. Resolve Preset
@@ -80,7 +129,11 @@ function resolveConfiguration(flags = {}, cwd = process.cwd()) {
80
129
  preset = getPreset(presetName);
81
130
  }
82
131
  if (!preset) {
83
- preset = detectPreset(cwd);
132
+ const detectDir = (configFile && path.dirname(configFile)) || cwd;
133
+ preset = detectPreset(detectDir);
134
+ if (preset.name === 'generic' && detectDir !== cwd) {
135
+ preset = detectPreset(cwd);
136
+ }
84
137
  }
85
138
 
86
139
  // 2. Merge Repo Settings
package/lib/git.js CHANGED
@@ -7,6 +7,10 @@ function getGitPrefix(options = {}) {
7
7
  return `git --git-dir="${options.bareRepo}"`;
8
8
  }
9
9
  if (options.repoDir) {
10
+ const dotBare = path.join(options.repoDir, '.bare');
11
+ if (fs.existsSync(dotBare)) {
12
+ return `git --git-dir="${dotBare}"`;
13
+ }
10
14
  return `git -C "${options.repoDir}"`;
11
15
  }
12
16
  return 'git';
@@ -16,6 +20,10 @@ function getRepoRootDir(cwd = process.cwd(), bareRepo = null) {
16
20
  if (bareRepo && fs.existsSync(bareRepo)) {
17
21
  return bareRepo;
18
22
  }
23
+ const dotBare = path.join(cwd, '.bare');
24
+ if (fs.existsSync(dotBare)) {
25
+ return cwd;
26
+ }
19
27
  try {
20
28
  const commonDir = execSync('git rev-parse --git-common-dir', { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
21
29
  const absoluteCommonDir = path.isAbsolute(commonDir) ? commonDir : path.resolve(cwd, commonDir);
@@ -4,6 +4,7 @@ const git = require('../git');
4
4
  const herdr = require('../herdr');
5
5
  const layout = require('../layout');
6
6
  const { createContext } = require('../context');
7
+ const { resolveConfiguration } = require('../config');
7
8
  const { showUsage } = require('../cli');
8
9
 
9
10
  async function executeCreate(flags, config, cwd = process.cwd()) {
@@ -24,8 +25,8 @@ async function executeCreate(flags, config, cwd = process.cwd()) {
24
25
 
25
26
  // 2. Resolve Session / Workspace Name
26
27
  const baseWorkspace = flags.workspaceName || dirname.replace(/[\s.:]/g, '_');
27
- const prefix = config.workspace.labelPrefix || '';
28
- const sessionName = (prefix && !baseWorkspace.startsWith(prefix.trim()))
28
+ let prefix = config.workspace.labelPrefix || '';
29
+ let sessionName = (prefix && !baseWorkspace.startsWith(prefix.trim()))
29
30
  ? `${prefix}${baseWorkspace}`
30
31
  : baseWorkspace;
31
32
 
@@ -69,25 +70,32 @@ async function executeCreate(flags, config, cwd = process.cwd()) {
69
70
  }
70
71
  }
71
72
 
72
- // 5. Initialize Context for Hooks
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
73
81
  const ctx = createContext({
74
82
  worktreePath: worktreePathToUse,
75
83
  repoRoot,
76
- bareRepo: config.repo.bareRepo,
84
+ bareRepo: activeConfig.repo.bareRepo,
77
85
  branch,
78
- source: flags.source || config.repo.defaultBaseBranch,
86
+ source: flags.source || activeConfig.repo.defaultBaseBranch,
79
87
  flags,
80
- preset: config.preset,
81
- config,
88
+ preset: activeConfig.preset,
89
+ config: activeConfig,
82
90
  });
83
91
 
84
92
  // 6. Run Preset / Config Hooks
85
- if (typeof config.hooks.onSyncPrimary === 'function') {
86
- await config.hooks.onSyncPrimary(ctx);
93
+ if (typeof activeConfig.hooks.onSyncPrimary === 'function') {
94
+ await activeConfig.hooks.onSyncPrimary(ctx);
87
95
  }
88
96
 
89
- if (!worktreeExists && typeof config.hooks.onScaffold === 'function') {
90
- await config.hooks.onScaffold(ctx);
97
+ if (!worktreeExists && typeof activeConfig.hooks.onScaffold === 'function') {
98
+ await activeConfig.hooks.onScaffold(ctx);
91
99
  }
92
100
 
93
101
  // 7. Orchestrate Herdr Workspace
@@ -105,10 +113,10 @@ async function executeCreate(flags, config, cwd = process.cwd()) {
105
113
  // 8. Render Terminal Layout
106
114
  console.log(`==> Configuring terminal panes...`);
107
115
  layout.renderLayout({
108
- layout: config.layout,
116
+ layout: activeConfig.layout,
109
117
  rootPaneId: ws.rootPaneId,
110
118
  cwd: worktreePathToUse,
111
- focusTarget: flags.focusTarget || config.workspace.defaultFocus,
119
+ focusTarget: flags.focusTarget || activeConfig.workspace.defaultFocus,
112
120
  });
113
121
 
114
122
  // Focus the workspace
@@ -4,6 +4,7 @@ const { execSync } = require('child_process');
4
4
  const git = require('../git');
5
5
  const herdr = require('../herdr');
6
6
  const { createContext } = require('../context');
7
+ const { resolveConfiguration } = require('../config');
7
8
  const { showUsage } = require('../cli');
8
9
 
9
10
  async function executeNuke(flags, config, cwd = process.cwd()) {
@@ -50,6 +51,10 @@ async function executeNuke(flags, config, cwd = process.cwd()) {
50
51
  const targetDirname = path.basename(targetWorktreePath);
51
52
  const targetSessionName = targetDirname.replace(/[\s.:]/g, '_');
52
53
 
54
+ const activeConfig = fs.existsSync(targetWorktreePath)
55
+ ? resolveConfiguration(flags, targetWorktreePath)
56
+ : config;
57
+
53
58
  let targetBranch = flags.branch;
54
59
  if (!targetBranch && existingWorktree && existingWorktree.branch) {
55
60
  targetBranch = existingWorktree.branch;
@@ -64,14 +69,14 @@ async function executeNuke(flags, config, cwd = process.cwd()) {
64
69
  }
65
70
  if (!targetBranch) {
66
71
  try {
67
- const gitCmd = git.getGitPrefix({ repoDir: repoRoot, bareRepo: config.repo.bareRepo });
72
+ const gitCmd = git.getGitPrefix({ repoDir: repoRoot, bareRepo: activeConfig.repo.bareRepo });
68
73
  execSync(`${gitCmd} rev-parse --verify "${target}"`, { stdio: 'ignore' });
69
74
  targetBranch = target;
70
75
  } catch (e) {}
71
76
  }
72
77
 
73
78
  console.log(`\n=== Starting Worktree Nuke ===`);
74
- console.log(`Active Preset: ${config.preset ? config.preset.name : 'generic'}`);
79
+ console.log(`Active Preset: ${activeConfig.preset ? activeConfig.preset.name : 'generic'}`);
75
80
  console.log(`Repository Root: ${repoRoot}`);
76
81
  console.log(`Target Worktree Path: ${targetWorktreePath}`);
77
82
  console.log(`Associated Branch: ${targetBranch || '(None detected)'}`);
@@ -96,16 +101,16 @@ async function executeNuke(flags, config, cwd = process.cwd()) {
96
101
  const ctx = createContext({
97
102
  worktreePath: targetWorktreePath,
98
103
  repoRoot,
99
- bareRepo: config.repo.bareRepo,
104
+ bareRepo: activeConfig.repo.bareRepo,
100
105
  branch: targetBranch,
101
106
  flags,
102
- preset: config.preset,
103
- config,
107
+ preset: activeConfig.preset,
108
+ config: activeConfig,
104
109
  });
105
110
 
106
111
  // Pre-Nuke Hook
107
- if (typeof config.hooks.onPreNuke === 'function') {
108
- await config.hooks.onPreNuke(ctx);
112
+ if (typeof activeConfig.hooks.onPreNuke === 'function') {
113
+ await activeConfig.hooks.onPreNuke(ctx);
109
114
  }
110
115
 
111
116
  // 1. Remove Worktree & Directory
@@ -139,12 +144,12 @@ async function executeNuke(flags, config, cwd = process.cwd()) {
139
144
  // Helper for final cleanup steps (Herdr workspace close + Post-Nuke hook)
140
145
  const finishCleanup = async () => {
141
146
  // Post-Nuke Hook
142
- if (typeof config.hooks.onPostNuke === 'function') {
143
- await config.hooks.onPostNuke(ctx);
147
+ if (typeof activeConfig.hooks.onPostNuke === 'function') {
148
+ await activeConfig.hooks.onPostNuke(ctx);
144
149
  }
145
150
 
146
151
  // Close Herdr Workspace if running (performed last so active pane is not terminated mid-cleanup)
147
- const prefix = config.workspace.labelPrefix || '';
152
+ const prefix = activeConfig.workspace.labelPrefix || '';
148
153
  const matchTargets = [
149
154
  targetSessionName,
150
155
  targetDirname,
@@ -165,7 +170,7 @@ async function executeNuke(flags, config, cwd = process.cwd()) {
165
170
  return;
166
171
  }
167
172
 
168
- const protectedBranches = config.repo.protectedBranches || ['main', 'master', 'develop', 'prod', 'staging', 'production'];
173
+ const protectedBranches = activeConfig.repo.protectedBranches || ['main', 'master', 'develop', 'prod', 'staging', 'production'];
169
174
  if (targetBranch && protectedBranches.includes(targetBranch)) {
170
175
  console.warn(`\n==> WARNING: "${targetBranch}" is a protected branch. Skipping local and remote branch deletion.`);
171
176
  await finishCleanup();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsuzuku/arise",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -20,7 +20,6 @@
20
20
  "index.js",
21
21
  "types.d.ts",
22
22
  "arise.schema.json",
23
- "worktree.schema.json",
24
23
  "README.md"
25
24
  ],
26
25
  "engines": {
@@ -1,130 +0,0 @@
1
- {
2
- "$schema": "http://json-schema.org/draft-07/schema#",
3
- "title": "HerdrWorktreeConfig",
4
- "description": "Configuration schema for Herdr Worktree (.worktreerc.json)",
5
- "type": "object",
6
- "properties": {
7
- "preset": {
8
- "type": "string",
9
- "description": "Preset name to use ('node', 'laravel', 'generic' or custom registered preset)"
10
- },
11
- "repo": {
12
- "type": "object",
13
- "description": "Repository topology and branch configuration",
14
- "properties": {
15
- "bareRepo": {
16
- "type": ["string", "null"],
17
- "description": "Path to bare git repository (e.g. '/path/to/bare/repo.git')"
18
- },
19
- "worktreesBase": {
20
- "type": ["string", "null"],
21
- "description": "Base directory where worktrees should be placed (e.g. '/path/to/worktrees')"
22
- },
23
- "defaultBaseBranch": {
24
- "type": "string",
25
- "description": "Default base branch to branch off (e.g. 'develop', 'prod', 'main')"
26
- },
27
- "protectedBranches": {
28
- "type": "array",
29
- "items": { "type": "string" },
30
- "description": "Array of branch names protected against deletion during --nuke"
31
- }
32
- },
33
- "additionalProperties": false
34
- },
35
- "workspace": {
36
- "type": "object",
37
- "description": "Herdr workspace naming, agent, and focus configuration",
38
- "properties": {
39
- "labelPrefix": {
40
- "type": "string",
41
- "description": "Prefix added to Herdr workspace labels (e.g. '[BE] ')"
42
- },
43
- "agent": {
44
- "type": ["string", "object", "null"],
45
- "description": "CLI AI agent to run in the workspace pane ('agy', 'claude', 'aider', 'copilot', 'none', or custom command object)",
46
- "properties": {
47
- "cmd": {
48
- "type": ["string", "null"],
49
- "description": "Command to execute the agent"
50
- },
51
- "title": {
52
- "type": "string",
53
- "description": "Display title for the agent pane in Herdr"
54
- }
55
- }
56
- },
57
- "defaultFocus": {
58
- "type": "string",
59
- "description": "Default pane to focus upon creation ('agent', 'agy', 'claude', 'vim', 'logs', 'server', 'shell')"
60
- }
61
- },
62
- "additionalProperties": false
63
- },
64
- "layout": {
65
- "type": "array",
66
- "description": "Declarative 4-pane quadrant layout definitions",
67
- "items": {
68
- "type": "object",
69
- "required": ["id", "title"],
70
- "properties": {
71
- "id": {
72
- "type": "string",
73
- "description": "Unique identifier for this pane within the layout"
74
- },
75
- "title": {
76
- "type": "string",
77
- "description": "Title displayed on the Herdr pane"
78
- },
79
- "cmd": {
80
- "type": ["string", "null"],
81
- "description": "Command to run when pane is opened (e.g. 'vim .', 'npm run dev', 'tail -f ...')"
82
- },
83
- "position": {
84
- "type": "string",
85
- "enum": ["root"],
86
- "description": "Set to 'root' for the initial root pane"
87
- },
88
- "from": {
89
- "type": "string",
90
- "description": "ID of parent pane to split from"
91
- },
92
- "split": {
93
- "type": "string",
94
- "enum": ["right", "down"],
95
- "description": "Split direction ('right' or 'down')"
96
- },
97
- "focus": {
98
- "type": "boolean",
99
- "description": "Whether this pane should receive focus by default"
100
- },
101
- "isAgent": {
102
- "type": "boolean",
103
- "description": "Whether this pane is designated as the AI CLI agent pane"
104
- }
105
- },
106
- "additionalProperties": false
107
- }
108
- },
109
- "scaffold": {
110
- "type": "object",
111
- "description": "Scaffolding options (environment files, symlinks, dependency installation, etc.)",
112
- "properties": {
113
- "envSource": {
114
- "type": ["string", "null"],
115
- "description": "Path to .env source file to copy into new worktree"
116
- },
117
- "symlink": {
118
- "type": ["string", "null"],
119
- "description": "Path to web server symlink to update to active worktree"
120
- },
121
- "install": {
122
- "type": ["string", "boolean", "null"],
123
- "description": "Command to run to install dependencies upon creation (e.g. 'npm install --legacy-peer-deps', 'pnpm install', 'composer install --no-interaction'), or false/null to skip installation."
124
- }
125
- },
126
- "additionalProperties": true
127
- }
128
- },
129
- "additionalProperties": false
130
- }