@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 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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { run } = require('../index');
4
+
5
+ run(process.argv.slice(2), process.cwd()).catch(err => {
6
+ console.error(`Fatal error: ${err.message}`);
7
+ process.exit(1);
8
+ });
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
+ };