@tsuzuku/arise 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -0
- package/arise.schema.json +130 -0
- package/bin/cli.js +8 -0
- package/index.js +37 -0
- package/lib/cli.js +129 -0
- package/lib/config.js +229 -0
- package/lib/context.js +113 -0
- package/lib/git.js +306 -0
- package/lib/herdr.js +217 -0
- package/lib/layout.js +88 -0
- package/lib/lifecycle/create.js +131 -0
- package/lib/lifecycle/nuke.js +218 -0
- package/lib/skill.js +200 -0
- package/package.json +42 -0
- package/presets/generic.js +33 -0
- package/presets/index.js +39 -0
- package/presets/laravel.js +115 -0
- package/presets/node.js +50 -0
- package/skills/arise/SKILL.md +86 -0
- package/types.d.ts +157 -0
package/README.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Arise (`arise`)
|
|
2
|
+
|
|
3
|
+
Unified, workplace-agnostic Git worktree and Herdr workspace orchestrator with pluggable project presets.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Unified Lifecycle**: Handles git worktree creation, branch resolution, Herdr workspace orchestration, 4-pane quadrant terminal setup, and safe teardown/nuke.
|
|
10
|
+
- **Pluggable Presets**: Built-in support for **Node.js** (`npm`/`yarn`/`pnpm`), **Laravel/PHP** (`composer`, logs, permissions), and **Generic** projects.
|
|
11
|
+
- **Zero-Config Auto-Detection**: Automatically detects project types based on directory markers (`package.json`, `composer.json`, `artisan`, etc.).
|
|
12
|
+
- **Workplace & Repo Agnostic**: Supports standard Git repos and bare repositories (`--git-dir`). Custom environment paths and symlinks are completely configurable via `.ariserc.json` / `.worktreerc.json` / `arise.config.js`.
|
|
13
|
+
- **Full Feature Parity**: Both Node and PHP projects get full `--nuke` / `--cleanup` suites with protected branch safety and Herdr workspace auto-closing.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Install globally via npm
|
|
21
|
+
npm install -g arise
|
|
22
|
+
|
|
23
|
+
# Or run directly without installing via npx
|
|
24
|
+
npx arise --branch <branch-name>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Aliases provided: `arise`, `herdr-worktree`, `herder-worktree`, and `hwk`.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
### 1. Create a Worktree Session
|
|
34
|
+
```bash
|
|
35
|
+
# Auto-detects project type (Node, Laravel, etc.)
|
|
36
|
+
arise --branch feature/login
|
|
37
|
+
|
|
38
|
+
# Explicitly specify a preset
|
|
39
|
+
arise --branch feature/login --preset laravel
|
|
40
|
+
|
|
41
|
+
# Choose your AI CLI agent (e.g. Claude Code, Aider, Antigravity, or Copilot)
|
|
42
|
+
arise --branch feature/login --agent claude
|
|
43
|
+
arise --branch feature/login -a aider
|
|
44
|
+
|
|
45
|
+
# Specify base branch, custom workspace name, or focus pane
|
|
46
|
+
arise --branch feature/login --source develop --focus claude
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 2. Nuke / Clean Up a Worktree
|
|
50
|
+
```bash
|
|
51
|
+
# Inside a worktree directory (auto-detects current worktree):
|
|
52
|
+
arise --nuke
|
|
53
|
+
|
|
54
|
+
# From anywhere, by branch or directory name:
|
|
55
|
+
arise --nuke feature-login
|
|
56
|
+
|
|
57
|
+
# Keep branches, only remove directory:
|
|
58
|
+
arise --nuke feature-login --dir-only
|
|
59
|
+
|
|
60
|
+
# Delete local branch, keep remote on origin:
|
|
61
|
+
arise --nuke feature-login --keep-remote
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 3. Install AI Agent Skill (Antigravity `agy`, Claude Code, etc.)
|
|
65
|
+
```bash
|
|
66
|
+
# Install globally to ~/.agents/skills and link to ~/.gemini/skills:
|
|
67
|
+
arise --install-skill
|
|
68
|
+
|
|
69
|
+
# Install locally to workspace (.agents/skills/arise):
|
|
70
|
+
arise --install-skill --local
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Customizing via `.ariserc.json` or `arise.config.js`
|
|
76
|
+
|
|
77
|
+
Place a `.ariserc.json` in your repository root, worktrees base directory, or `~/.config/arise/config.js` (also supports `.worktreerc.json` / `~/.config/herdr-worktree/` for backwards compatibility):
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"preset": "laravel",
|
|
82
|
+
"repo": {
|
|
83
|
+
"bareRepo": "/path/to/bare/repo.git",
|
|
84
|
+
"worktreesBase": "/path/to/worktrees",
|
|
85
|
+
"defaultBaseBranch": "main"
|
|
86
|
+
},
|
|
87
|
+
"workspace": {
|
|
88
|
+
"labelPrefix": "[API] ",
|
|
89
|
+
"agent": "claude",
|
|
90
|
+
"defaultFocus": "claude"
|
|
91
|
+
},
|
|
92
|
+
"scaffold": {
|
|
93
|
+
"envSource": "/path/to/shared/.env",
|
|
94
|
+
"symlink": "/var/www/my-app"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Architecture & Modular Structure
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
arise/
|
|
105
|
+
├── package.json
|
|
106
|
+
├── index.js # Main runner module
|
|
107
|
+
├── bin/
|
|
108
|
+
│ └── cli.js # Executable CLI
|
|
109
|
+
├── lib/
|
|
110
|
+
│ ├── cli.js # CLI argument parsing & help output
|
|
111
|
+
│ ├── config.js # Config discovery & preset merging
|
|
112
|
+
│ ├── git.js # Git worktree & branch operations
|
|
113
|
+
│ ├── herdr.js # Herdr workspace & pane operations
|
|
114
|
+
│ ├── layout.js # Declarative terminal layout renderer
|
|
115
|
+
│ ├── context.js # Lifecycle execution context & helpers
|
|
116
|
+
│ ├── skill.js # Agent skill installer for agy/claude
|
|
117
|
+
│ └── lifecycle/
|
|
118
|
+
│ ├── create.js # Worktree creation & layout pipeline
|
|
119
|
+
│ └── nuke.js # Safe teardown & branch deletion pipeline
|
|
120
|
+
└── presets/
|
|
121
|
+
├── index.js # Preset registry & auto-detection
|
|
122
|
+
├── node.js # Node / JS project preset
|
|
123
|
+
├── laravel.js # Laravel / PHP project preset
|
|
124
|
+
└── generic.js # Fallback generic preset
|
|
125
|
+
```
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "AriseConfig",
|
|
4
|
+
"description": "Configuration schema for Arise (.ariserc.json / .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
|
+
}
|
package/bin/cli.js
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const pkg = require('./package.json');
|
|
2
|
+
const { parseArgs, showUsage } = require('./lib/cli');
|
|
3
|
+
const { resolveConfiguration } = require('./lib/config');
|
|
4
|
+
const { executeCreate } = require('./lib/lifecycle/create');
|
|
5
|
+
const { executeNuke } = require('./lib/lifecycle/nuke');
|
|
6
|
+
|
|
7
|
+
async function run(argv = process.argv.slice(2), cwd = process.cwd()) {
|
|
8
|
+
const flags = parseArgs(argv);
|
|
9
|
+
|
|
10
|
+
if (flags.showHelp) {
|
|
11
|
+
showUsage();
|
|
12
|
+
process.exit(0);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (flags.showVersion) {
|
|
16
|
+
console.log(`arise v${pkg.version}`);
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (flags.installSkill) {
|
|
21
|
+
const { installSkill } = require('./lib/skill');
|
|
22
|
+
installSkill({ scope: flags.skillScope, cwd });
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const config = resolveConfiguration(flags, cwd);
|
|
27
|
+
|
|
28
|
+
if (flags.isCleanup) {
|
|
29
|
+
await executeNuke(flags, config, cwd);
|
|
30
|
+
} else {
|
|
31
|
+
await executeCreate(flags, config, cwd);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = {
|
|
36
|
+
run,
|
|
37
|
+
};
|
package/lib/cli.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
function showUsage() {
|
|
4
|
+
console.log(`
|
|
5
|
+
Arise - Unified Worktree & Herdr Workspace Orchestrator
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
arise --branch <branch> [options]
|
|
9
|
+
arise --nuke [<worktree dir or branch>] [nuke-options]
|
|
10
|
+
arise --cleanup [<worktree dir or branch>] [nuke-options]
|
|
11
|
+
|
|
12
|
+
Creation Arguments:
|
|
13
|
+
--branch, -b <branch> (Required for creation) Git branch to create or boot into.
|
|
14
|
+
--dirname, -d <dirname> (Optional) Directory name of the worktree. Defaults to sanitized branch name.
|
|
15
|
+
--workspace, -w <name> (Optional) Custom Herdr workspace name.
|
|
16
|
+
--source, -s, --base <src> (Optional) Base branch if creating a new branch. Defaults to preset default (e.g. 'develop' / 'prod').
|
|
17
|
+
--preset, -p <preset> (Optional) Project preset ('node', 'laravel', 'generic', or custom). Auto-detected if omitted.
|
|
18
|
+
--agent, -a <agent> (Optional) AI CLI agent to run in workspace ('agy', 'claude', 'aider', 'copilot', etc.).
|
|
19
|
+
--focus, -f <pane> (Optional) Pane to focus ('agent', 'agy', 'claude', 'vim', 'logs', 'server', 'shell'). Defaults to active agent or 'agy'.
|
|
20
|
+
|
|
21
|
+
Nuke / Cleanup Arguments:
|
|
22
|
+
--nuke, -n [<target>] Nuke the worktree: closes Herdr workspace, removes worktree directory,
|
|
23
|
+
deletes local branch, and deletes remote branch on origin.
|
|
24
|
+
(Auto-detects current worktree if omitted inside a worktree directory).
|
|
25
|
+
--cleanup, -c [<target>] Alias for --nuke.
|
|
26
|
+
--dir-only, --keep-branch (Optional) Only remove the worktree directory; keep local and remote branches.
|
|
27
|
+
--keep-remote, --local-only (Optional) Delete local branch, but keep remote branch on origin.
|
|
28
|
+
--force, -f (Optional) Force worktree removal even if changes are uncommitted.
|
|
29
|
+
|
|
30
|
+
Agent Skill Arguments:
|
|
31
|
+
--install-skill, -i Install the agent skill for Antigravity (agy), Claude Code, and other AI agents.
|
|
32
|
+
--global (Default) Install skill globally to ~/.agents/skills and ~/.gemini/skills.
|
|
33
|
+
--local, --workspace Install skill locally to current workspace (.agents/skills).
|
|
34
|
+
|
|
35
|
+
General Arguments:
|
|
36
|
+
--yes, -y (Optional) Automatically answer yes to confirmation prompts (e.g. installing Herdr).
|
|
37
|
+
--help, -h Show this help message.
|
|
38
|
+
--version, -v Show version information.
|
|
39
|
+
`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseArgs(argv = process.argv.slice(2)) {
|
|
43
|
+
const flags = {
|
|
44
|
+
isCleanup: false,
|
|
45
|
+
cleanupTarget: null,
|
|
46
|
+
dirOnly: false,
|
|
47
|
+
keepRemote: false,
|
|
48
|
+
force: false,
|
|
49
|
+
yes: false,
|
|
50
|
+
branch: null,
|
|
51
|
+
dirname: null,
|
|
52
|
+
workspaceName: null,
|
|
53
|
+
source: null,
|
|
54
|
+
presetName: null,
|
|
55
|
+
agent: null,
|
|
56
|
+
focusTarget: null,
|
|
57
|
+
installSkill: false,
|
|
58
|
+
skillScope: 'global',
|
|
59
|
+
showHelp: false,
|
|
60
|
+
showVersion: false,
|
|
61
|
+
rawArgs: argv,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
for (let i = 0; i < argv.length; i++) {
|
|
65
|
+
const arg = argv[i];
|
|
66
|
+
|
|
67
|
+
if (arg === '--help' || arg === '-h') {
|
|
68
|
+
flags.showHelp = true;
|
|
69
|
+
} else if (arg === '--version' || arg === '-v') {
|
|
70
|
+
flags.showVersion = true;
|
|
71
|
+
} else if (arg === '--install-skill' || arg === '--install-agent-skill' || arg === '--setup-skill' || arg === '-i') {
|
|
72
|
+
flags.installSkill = true;
|
|
73
|
+
} else if (arg === '--global') {
|
|
74
|
+
flags.skillScope = 'global';
|
|
75
|
+
} else if (arg === '--local' || arg === '--workspace') {
|
|
76
|
+
flags.skillScope = 'local';
|
|
77
|
+
} else if (arg === '--nuke' || arg === '-n' || arg === 'nuke' || arg === '--cleanup' || arg === '-c' || arg === 'cleanup') {
|
|
78
|
+
flags.isCleanup = true;
|
|
79
|
+
if (argv[i + 1] && !argv[i + 1].startsWith('-')) {
|
|
80
|
+
flags.cleanupTarget = argv[i + 1];
|
|
81
|
+
i++;
|
|
82
|
+
}
|
|
83
|
+
} else if (arg === '--dir-only' || arg === '--only-dir' || arg === '--keep-branch' || arg === '--keep-branches') {
|
|
84
|
+
flags.dirOnly = true;
|
|
85
|
+
} else if (arg === '--keep-remote' || arg === '--local-only') {
|
|
86
|
+
flags.keepRemote = true;
|
|
87
|
+
} else if (arg === '--force' || arg === '-f') {
|
|
88
|
+
flags.force = true;
|
|
89
|
+
} else if (arg === '--yes' || arg === '-y') {
|
|
90
|
+
flags.yes = true;
|
|
91
|
+
} else if (arg === '--branch' || arg === '-b') {
|
|
92
|
+
flags.branch = argv[i + 1];
|
|
93
|
+
i++;
|
|
94
|
+
} else if (arg === '--dirname' || arg === '-d') {
|
|
95
|
+
flags.dirname = argv[i + 1];
|
|
96
|
+
i++;
|
|
97
|
+
} else if (arg === '--workspace' || arg === '--workspace-name' || arg === '-w' || arg === '--name') {
|
|
98
|
+
flags.workspaceName = argv[i + 1];
|
|
99
|
+
i++;
|
|
100
|
+
} else if (arg === '--source' || arg === '--base' || arg === '-s') {
|
|
101
|
+
flags.source = argv[i + 1];
|
|
102
|
+
i++;
|
|
103
|
+
} else if (arg === '--preset' || arg === '-p') {
|
|
104
|
+
flags.presetName = argv[i + 1];
|
|
105
|
+
i++;
|
|
106
|
+
} else if (arg === '--agent' || arg === '-a') {
|
|
107
|
+
flags.agent = argv[i + 1];
|
|
108
|
+
i++;
|
|
109
|
+
} else if (arg.startsWith('--agent=')) {
|
|
110
|
+
flags.agent = arg.slice(8);
|
|
111
|
+
} else if (arg === '--focus') {
|
|
112
|
+
flags.focusTarget = argv[i + 1];
|
|
113
|
+
i++;
|
|
114
|
+
} else if (!arg.startsWith('-')) {
|
|
115
|
+
if (flags.isCleanup && !flags.cleanupTarget) {
|
|
116
|
+
flags.cleanupTarget = arg;
|
|
117
|
+
} else if (!flags.branch) {
|
|
118
|
+
flags.branch = arg;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return flags;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = {
|
|
127
|
+
parseArgs,
|
|
128
|
+
showUsage,
|
|
129
|
+
};
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const { getPreset, detectPreset } = require('../presets');
|
|
5
|
+
const git = require('./git');
|
|
6
|
+
|
|
7
|
+
function findConfigFile(searchDirs = []) {
|
|
8
|
+
const configNames = [
|
|
9
|
+
'.ariserc.js',
|
|
10
|
+
'arise.config.js',
|
|
11
|
+
'.ariserc.json',
|
|
12
|
+
'.ariserc',
|
|
13
|
+
'.worktreerc.js',
|
|
14
|
+
'worktree.config.js',
|
|
15
|
+
'.worktreerc.json',
|
|
16
|
+
'.worktreerc',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
for (const dir of searchDirs) {
|
|
20
|
+
if (!dir || !fs.existsSync(dir)) continue;
|
|
21
|
+
for (const name of configNames) {
|
|
22
|
+
const fullPath = path.join(dir, name);
|
|
23
|
+
if (fs.existsSync(fullPath)) {
|
|
24
|
+
return fullPath;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Check home directory ~/.config/arise then ~/.config/herdr-worktree
|
|
30
|
+
const homeConfigDirs = [
|
|
31
|
+
path.join(os.homedir(), '.config', 'arise'),
|
|
32
|
+
path.join(os.homedir(), '.config', 'herdr-worktree'),
|
|
33
|
+
];
|
|
34
|
+
for (const homeDir of homeConfigDirs) {
|
|
35
|
+
for (const name of configNames) {
|
|
36
|
+
const fullPath = path.join(homeDir, name);
|
|
37
|
+
if (fs.existsSync(fullPath)) {
|
|
38
|
+
return fullPath;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const homeRcFiles = [
|
|
44
|
+
path.join(os.homedir(), '.ariserc.json'),
|
|
45
|
+
path.join(os.homedir(), '.worktreerc.json'),
|
|
46
|
+
];
|
|
47
|
+
for (const rcFile of homeRcFiles) {
|
|
48
|
+
if (fs.existsSync(rcFile)) {
|
|
49
|
+
return rcFile;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function loadConfigFile(filePath) {
|
|
57
|
+
if (!filePath) return {};
|
|
58
|
+
try {
|
|
59
|
+
if (filePath.endsWith('.json') || filePath.endsWith('.worktreerc') || filePath.endsWith('.ariserc')) {
|
|
60
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
61
|
+
return JSON.parse(content);
|
|
62
|
+
} else {
|
|
63
|
+
return require(path.resolve(filePath));
|
|
64
|
+
}
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.warn(`Warning: Failed to load config from "${filePath}": ${err.message}`);
|
|
67
|
+
return {};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resolveConfiguration(flags = {}, cwd = process.cwd()) {
|
|
72
|
+
const repoRoot = git.getRepoRootDir(cwd);
|
|
73
|
+
const isBare = git.isBareRepo(repoRoot);
|
|
74
|
+
const searchDirs = [];
|
|
75
|
+
|
|
76
|
+
const addSearchDir = (dir) => {
|
|
77
|
+
if (dir && !searchDirs.includes(dir)) {
|
|
78
|
+
searchDirs.push(dir);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
let worktrees = [];
|
|
83
|
+
try {
|
|
84
|
+
worktrees = git.getWorktrees({ repoDir: repoRoot, bareRepo: isBare ? repoRoot : null });
|
|
85
|
+
} catch (e) {}
|
|
86
|
+
|
|
87
|
+
// 1. Explicit directory if --dirname / -d is passed
|
|
88
|
+
if (flags.dirname) {
|
|
89
|
+
const resolvedDir = path.isAbsolute(flags.dirname)
|
|
90
|
+
? flags.dirname
|
|
91
|
+
: path.resolve(repoRoot || cwd, flags.dirname);
|
|
92
|
+
addSearchDir(resolvedDir);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 2. Directory for the specified branch (--branch / -b)
|
|
96
|
+
if (flags.branch) {
|
|
97
|
+
const matchingWt = worktrees.find((wt) => wt.branch === flags.branch);
|
|
98
|
+
if (matchingWt && matchingWt.path) {
|
|
99
|
+
addSearchDir(matchingWt.path);
|
|
100
|
+
}
|
|
101
|
+
const branchDir = path.resolve(repoRoot || cwd, flags.branch.replace(/\//g, '-'));
|
|
102
|
+
addSearchDir(branchDir);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 3. Current working directory
|
|
106
|
+
addSearchDir(cwd);
|
|
107
|
+
|
|
108
|
+
// 4. Repository root directory
|
|
109
|
+
if (repoRoot) {
|
|
110
|
+
addSearchDir(repoRoot);
|
|
111
|
+
if (isBare) {
|
|
112
|
+
addSearchDir(path.dirname(repoRoot));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 5. Fallback to existing worktrees (prioritizing primary branches like main, master, develop)
|
|
117
|
+
const primaryBranches = ['main', 'master', 'develop', 'prod', 'staging'];
|
|
118
|
+
const primaryWts = worktrees.filter((wt) => !wt.isBare && wt.branch && primaryBranches.includes(wt.branch));
|
|
119
|
+
for (const wt of primaryWts) {
|
|
120
|
+
if (wt.path) addSearchDir(wt.path);
|
|
121
|
+
}
|
|
122
|
+
for (const wt of worktrees) {
|
|
123
|
+
if (wt.path && !wt.isBare) addSearchDir(wt.path);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const configFile = findConfigFile(searchDirs);
|
|
127
|
+
const fileConfig = loadConfigFile(configFile);
|
|
128
|
+
|
|
129
|
+
// 1. Resolve Preset
|
|
130
|
+
let presetName = flags.presetName || fileConfig.preset;
|
|
131
|
+
let preset = null;
|
|
132
|
+
if (presetName) {
|
|
133
|
+
preset = getPreset(presetName);
|
|
134
|
+
}
|
|
135
|
+
if (!preset) {
|
|
136
|
+
const detectDir = (configFile && path.dirname(configFile)) || cwd;
|
|
137
|
+
preset = detectPreset(detectDir);
|
|
138
|
+
if (preset.name === 'generic' && detectDir !== cwd) {
|
|
139
|
+
preset = detectPreset(cwd);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 2. Merge Repo Settings
|
|
144
|
+
const repoConfig = {
|
|
145
|
+
bareRepo: (fileConfig.repo && fileConfig.repo.bareRepo) || (preset.repo && preset.repo.bareRepo) || (isBare ? repoRoot : null),
|
|
146
|
+
worktreesBase: (fileConfig.repo && fileConfig.repo.worktreesBase) || (preset.repo && preset.repo.worktreesBase) || null,
|
|
147
|
+
defaultBaseBranch: (fileConfig.repo && fileConfig.repo.defaultBaseBranch) || (preset.repo && preset.repo.defaultBaseBranch) || 'develop',
|
|
148
|
+
protectedBranches: (fileConfig.repo && fileConfig.repo.protectedBranches) || (preset.repo && preset.repo.protectedBranches) || ['main', 'master', 'develop', 'prod', 'staging'],
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// 3. Resolve AI CLI Agent
|
|
152
|
+
const resolvedAgent = flags.agent
|
|
153
|
+
|| process.env.ARISE_AGENT
|
|
154
|
+
|| (fileConfig.workspace && fileConfig.workspace.agent)
|
|
155
|
+
|| (preset.workspace && preset.workspace.agent)
|
|
156
|
+
|| 'agy';
|
|
157
|
+
|
|
158
|
+
// 4. Merge Workspace Settings
|
|
159
|
+
const defaultFocus = (fileConfig.workspace && fileConfig.workspace.defaultFocus)
|
|
160
|
+
|| (preset.workspace && preset.workspace.defaultFocus)
|
|
161
|
+
|| 'agy';
|
|
162
|
+
|
|
163
|
+
const workspaceConfig = {
|
|
164
|
+
labelPrefix: (fileConfig.workspace && fileConfig.workspace.labelPrefix !== undefined)
|
|
165
|
+
? fileConfig.workspace.labelPrefix
|
|
166
|
+
: (preset.workspace && preset.workspace.labelPrefix !== undefined ? preset.workspace.labelPrefix : ''),
|
|
167
|
+
agent: resolvedAgent,
|
|
168
|
+
defaultFocus,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// 5. Merge & Customize Layout for Active Agent
|
|
172
|
+
const baseLayout = fileConfig.layout || preset.layout || [];
|
|
173
|
+
const layout = baseLayout.map((pane) => {
|
|
174
|
+
const isAgentPane = pane.isAgent || pane.id === 'agy' || pane.id === 'agent' || pane.id === 'ai';
|
|
175
|
+
if (!isAgentPane) return { ...pane };
|
|
176
|
+
|
|
177
|
+
let agentCmd = pane.cmd;
|
|
178
|
+
let agentTitle = pane.title;
|
|
179
|
+
|
|
180
|
+
if (typeof resolvedAgent === 'string') {
|
|
181
|
+
const lower = resolvedAgent.toLowerCase().trim();
|
|
182
|
+
if (lower === 'none' || lower === 'false' || lower === 'null' || lower === 'disabled') {
|
|
183
|
+
agentCmd = null;
|
|
184
|
+
agentTitle = 'shell';
|
|
185
|
+
} else {
|
|
186
|
+
agentCmd = resolvedAgent;
|
|
187
|
+
agentTitle = resolvedAgent;
|
|
188
|
+
}
|
|
189
|
+
} else if (typeof resolvedAgent === 'object' && resolvedAgent !== null) {
|
|
190
|
+
agentCmd = resolvedAgent.cmd !== undefined ? resolvedAgent.cmd : (resolvedAgent.command || null);
|
|
191
|
+
agentTitle = resolvedAgent.title || resolvedAgent.cmd || pane.title;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
...pane,
|
|
196
|
+
cmd: agentCmd,
|
|
197
|
+
title: agentTitle,
|
|
198
|
+
isAgent: true,
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// 5. Merge Scaffolding Settings
|
|
203
|
+
const scaffoldConfig = {
|
|
204
|
+
...(preset.scaffold || {}),
|
|
205
|
+
...(fileConfig.scaffold || {}),
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// 6. Merge Hooks
|
|
209
|
+
const hooks = {
|
|
210
|
+
...(preset.hooks || {}),
|
|
211
|
+
...(fileConfig.hooks || {}),
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
preset,
|
|
216
|
+
repo: repoConfig,
|
|
217
|
+
workspace: workspaceConfig,
|
|
218
|
+
layout,
|
|
219
|
+
scaffold: scaffoldConfig,
|
|
220
|
+
hooks,
|
|
221
|
+
configFile,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
module.exports = {
|
|
226
|
+
findConfigFile,
|
|
227
|
+
loadConfigFile,
|
|
228
|
+
resolveConfiguration,
|
|
229
|
+
};
|