@tsuzuku/arise 1.1.1 → 1.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 +28 -4
- package/index.js +47 -9
- package/lib/cli.js +39 -2
- package/lib/config/init.js +862 -0
- package/lib/config.js +24 -8
- package/lib/git.js +29 -0
- package/lib/herdr.js +47 -6
- package/lib/interactive.js +644 -0
- package/lib/layout.js +21 -7
- package/lib/lifecycle/create.js +28 -4
- package/lib/lifecycle/nuke.js +44 -2
- package/lib/logger.js +85 -0
- package/lib/skill.js +6 -0
- package/lib/tui/ansi.js +294 -0
- package/lib/tui/prompt.js +629 -0
- package/package.json +1 -1
- package/skills/arise/SKILL.md +6 -0
- package/types.d.ts +46 -0
|
@@ -0,0 +1,862 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const { mkdir, writeFile } = require('fs/promises');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const { ANSI, drawBox } = require('../tui/ansi');
|
|
6
|
+
const { promptSelect, promptMultiSelect, promptText, promptConfirm } = require('../tui/prompt');
|
|
7
|
+
const git = require('../git');
|
|
8
|
+
const { detectPreset, getPreset, builtInPresets } = require('../../presets');
|
|
9
|
+
const { installSkill } = require('../skill');
|
|
10
|
+
const pkg = require('../../package.json');
|
|
11
|
+
|
|
12
|
+
class ConfigInitWizard {
|
|
13
|
+
static async run(options = {}) {
|
|
14
|
+
const cwd = options.cwd || process.cwd();
|
|
15
|
+
let isInsideGit = false;
|
|
16
|
+
let repoRoot = null;
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
repoRoot = git.getRepoRootDir(cwd);
|
|
20
|
+
isInsideGit = Boolean(repoRoot);
|
|
21
|
+
} catch {
|
|
22
|
+
isInsideGit = false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log(`\n${ANSI.bold}${ANSI.brightCyan}=== 🚀 Arise Configuration Setup Wizard ===${ANSI.reset}`);
|
|
26
|
+
console.log(` ${ANSI.dim}Configure Git worktree topology, project presets, Herdr terminal layout, and AI agents.${ANSI.reset}\n`);
|
|
27
|
+
|
|
28
|
+
// Determine quick vs guided mode
|
|
29
|
+
let isQuick = Boolean(options.quick);
|
|
30
|
+
if (!isQuick && process.stdin.isTTY && !options.local && !options.global) {
|
|
31
|
+
console.log(` ${ANSI.bold}Configuration Components Overview:${ANSI.reset}`);
|
|
32
|
+
console.log(` ${ANSI.cyan}• Project Preset:${ANSI.reset} Node.js, Laravel/PHP, Generic, or custom`);
|
|
33
|
+
console.log(` ${ANSI.cyan}• Git Topology:${ANSI.reset} Default base branch (develop/main), protected branches, bare repo`);
|
|
34
|
+
console.log(` ${ANSI.cyan}• AI CLI Agent:${ANSI.reset} Antigravity (agy), Claude Code (claude), Aider, Copilot, or Custom`);
|
|
35
|
+
console.log(` ${ANSI.cyan}• Terminal Layout:${ANSI.reset} Declarative 4-pane quadrant layout (Editor, Server, Shell, AI Agent)`);
|
|
36
|
+
console.log(` ${ANSI.cyan}• Scaffolding:${ANSI.reset} .env copy, dependency auto-install, and web symlinks`);
|
|
37
|
+
console.log(` ${ANSI.cyan}• Agent Skill:${ANSI.reset} System integration for Antigravity and Claude Code\n`);
|
|
38
|
+
|
|
39
|
+
const modeChoice = await promptSelect({
|
|
40
|
+
message: 'Select initialization mode:',
|
|
41
|
+
choices: [
|
|
42
|
+
{
|
|
43
|
+
label: '🚀 Quick Setup (Recommended defaults)',
|
|
44
|
+
value: 'quick',
|
|
45
|
+
hint: 'Instant setup with detected preset, active branches, and Antigravity AI',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
label: '🛠️ Guided Step-by-Step Setup',
|
|
49
|
+
value: 'guided',
|
|
50
|
+
hint: 'Step-by-step walkthrough with full setting explanations and customization',
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (!modeChoice) {
|
|
56
|
+
console.log(`${ANSI.dim}Setup cancelled.${ANSI.reset}\n`);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
isQuick = modeChoice === 'quick';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Determine target scope (local vs global)
|
|
63
|
+
let isLocal = options.local ?? false;
|
|
64
|
+
if (!options.local && !options.global) {
|
|
65
|
+
if (isInsideGit) {
|
|
66
|
+
if (isQuick) {
|
|
67
|
+
isLocal = true;
|
|
68
|
+
} else {
|
|
69
|
+
console.log(`\n${ANSI.bold}${ANSI.cyan}── Configuration Scope ──${ANSI.reset}`);
|
|
70
|
+
console.log(` ${ANSI.dim}Arise supports local per-repo configuration or machine-wide global defaults:${ANSI.reset}\n`);
|
|
71
|
+
console.log(` ${ANSI.bold}Scope Comparison:${ANSI.reset}`);
|
|
72
|
+
console.log(` ${ANSI.green}• Local (.ariserc.json):${ANSI.reset} Saved in project root. Configures preset, branches,`);
|
|
73
|
+
console.log(` scaffolding, and layout specifically for this repository.`);
|
|
74
|
+
console.log(` ${ANSI.blue}• Global (~/.config/arise/):${ANSI.reset} Saved in user home directory. Defines machine-wide defaults,`);
|
|
75
|
+
console.log(` global AI agent preference, and fallback presets.\n`);
|
|
76
|
+
|
|
77
|
+
const scopeChoice = await promptSelect({
|
|
78
|
+
message: 'Where would you like to save this configuration?',
|
|
79
|
+
choices: [
|
|
80
|
+
{
|
|
81
|
+
label: '📁 Local Repository (.ariserc.json in project root)',
|
|
82
|
+
value: 'local',
|
|
83
|
+
hint: 'Recommended for project-specific rules, presets, and scaffolding',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
label: '🌐 Global User Config (~/.config/arise/.ariserc.json)',
|
|
87
|
+
value: 'global',
|
|
88
|
+
hint: 'For machine-wide defaults and shared AI agent preferences',
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
});
|
|
92
|
+
if (!scopeChoice) return null;
|
|
93
|
+
isLocal = scopeChoice === 'local';
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
isLocal = false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (isQuick) {
|
|
101
|
+
return this.runQuickInit(cwd, isLocal, isInsideGit, options);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return this.runGuidedInit(cwd, isLocal, isInsideGit, options);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
static async runQuickInit(cwd, isLocal, isInsideGit, options = {}) {
|
|
108
|
+
const detectedPreset = detectPreset(cwd);
|
|
109
|
+
const presetName = detectedPreset ? detectedPreset.name : 'generic';
|
|
110
|
+
let repoRoot = cwd;
|
|
111
|
+
let isBare = false;
|
|
112
|
+
let localBranches = ['main'];
|
|
113
|
+
|
|
114
|
+
if (isInsideGit) {
|
|
115
|
+
try {
|
|
116
|
+
repoRoot = git.getRepoRootDir(cwd);
|
|
117
|
+
isBare = git.isBareRepo(repoRoot);
|
|
118
|
+
localBranches = git.getLocalBranches({ repoDir: repoRoot, bareRepo: isBare ? repoRoot : null });
|
|
119
|
+
} catch {}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const defaultBase = localBranches.includes('develop')
|
|
123
|
+
? 'develop'
|
|
124
|
+
: (localBranches.includes('main') ? 'main' : (localBranches[0] || 'main'));
|
|
125
|
+
|
|
126
|
+
if (isLocal) {
|
|
127
|
+
const targetFilePath = options.targetPath || path.join(cwd, '.ariserc.json');
|
|
128
|
+
|
|
129
|
+
if (fs.existsSync(targetFilePath) && !options.force) {
|
|
130
|
+
console.log(` ${ANSI.yellow}Existing file found at ${targetFilePath}${ANSI.reset}\n`);
|
|
131
|
+
const overwrite = await promptConfirm({
|
|
132
|
+
message: `.ariserc.json already exists in ${path.dirname(targetFilePath)}. Overwrite?`,
|
|
133
|
+
defaultYes: false,
|
|
134
|
+
});
|
|
135
|
+
if (!overwrite) {
|
|
136
|
+
console.log(` ${ANSI.yellow}Existing configuration preserved.${ANSI.reset}\n`);
|
|
137
|
+
return targetFilePath;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const configObj = {
|
|
142
|
+
$schema: './arise.schema.json',
|
|
143
|
+
preset: presetName,
|
|
144
|
+
repo: {
|
|
145
|
+
defaultBaseBranch: defaultBase,
|
|
146
|
+
protectedBranches: ['main', 'master', 'develop', 'prod', 'staging'],
|
|
147
|
+
},
|
|
148
|
+
workspace: {
|
|
149
|
+
agent: 'agy',
|
|
150
|
+
defaultFocus: 'agy',
|
|
151
|
+
labelPrefix: '',
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
if (isBare) {
|
|
156
|
+
configObj.repo.bareRepo = repoRoot;
|
|
157
|
+
configObj.repo.worktreesBase = path.join(path.dirname(repoRoot), 'worktrees');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (presetName === 'node') {
|
|
161
|
+
configObj.scaffold = {
|
|
162
|
+
envSource: '.env',
|
|
163
|
+
install: 'npm install',
|
|
164
|
+
};
|
|
165
|
+
} else if (presetName === 'laravel') {
|
|
166
|
+
configObj.scaffold = {
|
|
167
|
+
envSource: '.env',
|
|
168
|
+
install: 'composer install',
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const content = `// Arise Local Repository Configuration (.ariserc.json)
|
|
173
|
+
// Documentation: https://github.com/tsuzuku/arise
|
|
174
|
+
//
|
|
175
|
+
// SETTINGS REFERENCE:
|
|
176
|
+
// • preset: Project preset ('node' | 'laravel' | 'generic' | custom)
|
|
177
|
+
// • repo.defaultBaseBranch: Default branch to branch off when creating worktrees
|
|
178
|
+
// • repo.protectedBranches: Branches protected from deletion during --nuke
|
|
179
|
+
// • repo.bareRepo: Path to bare repository (if using bare git topology)
|
|
180
|
+
// • repo.worktreesBase: Base directory where worktrees are created
|
|
181
|
+
// • workspace.agent: CLI AI agent ('agy' | 'claude' | 'aider' | 'copilot' | 'none')
|
|
182
|
+
// • workspace.defaultFocus: Pane to focus on workspace boot ('agy' | 'vim' | 'server' | 'shell')
|
|
183
|
+
// • workspace.labelPrefix: Prefix added to Herdr workspace labels (e.g. '[BE] ')
|
|
184
|
+
// • scaffold.envSource: Relative or absolute path to .env template to copy
|
|
185
|
+
// • scaffold.install: Command to run to install dependencies upon creation
|
|
186
|
+
${JSON.stringify(configObj, null, 2)}
|
|
187
|
+
`;
|
|
188
|
+
await writeFile(targetFilePath, content, 'utf8');
|
|
189
|
+
await this.handleGitignore(targetFilePath, isLocal, isInsideGit, cwd, options);
|
|
190
|
+
this.printSuccessCard(targetFilePath, 'Local repository configuration (.ariserc.json) created with detected defaults.');
|
|
191
|
+
return targetFilePath;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Global quick init
|
|
195
|
+
const globalDir = path.join(os.homedir(), '.config', 'arise');
|
|
196
|
+
const globalPath = options.targetPath || path.join(globalDir, '.ariserc.json');
|
|
197
|
+
|
|
198
|
+
if (fs.existsSync(globalPath) && !options.force) {
|
|
199
|
+
console.log(` ${ANSI.yellow}Existing global config found at ${globalPath}${ANSI.reset}\n`);
|
|
200
|
+
const overwrite = await promptConfirm({
|
|
201
|
+
message: `Global config already exists at ${globalPath}. Overwrite?`,
|
|
202
|
+
defaultYes: false,
|
|
203
|
+
});
|
|
204
|
+
if (!overwrite) {
|
|
205
|
+
console.log(` ${ANSI.yellow}Existing configuration preserved.${ANSI.reset}\n`);
|
|
206
|
+
return globalPath;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
await mkdir(path.dirname(globalPath), { recursive: true });
|
|
211
|
+
|
|
212
|
+
const globalObj = {
|
|
213
|
+
preset: presetName,
|
|
214
|
+
repo: {
|
|
215
|
+
defaultBaseBranch: defaultBase,
|
|
216
|
+
protectedBranches: ['main', 'master', 'develop', 'prod', 'staging'],
|
|
217
|
+
},
|
|
218
|
+
workspace: {
|
|
219
|
+
agent: 'agy',
|
|
220
|
+
defaultFocus: 'agy',
|
|
221
|
+
labelPrefix: '',
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const content = `// Arise Global Configuration (~/.config/arise/.ariserc.json)
|
|
226
|
+
// Documentation: https://github.com/tsuzuku/arise
|
|
227
|
+
//
|
|
228
|
+
// SETTINGS REFERENCE:
|
|
229
|
+
// • preset: Default project preset ('node' | 'laravel' | 'generic')
|
|
230
|
+
// • repo.defaultBaseBranch: Fallback base branch for new worktrees
|
|
231
|
+
// • repo.protectedBranches: Branches protected from deletion across all repos
|
|
232
|
+
// • workspace.agent: Default CLI AI agent ('agy' | 'claude' | 'aider' | 'copilot')
|
|
233
|
+
// • workspace.defaultFocus: Default pane to focus ('agy' | 'agent' | 'vim' | 'shell')
|
|
234
|
+
// • workspace.labelPrefix: Default workspace label prefix
|
|
235
|
+
${JSON.stringify(globalObj, null, 2)}
|
|
236
|
+
`;
|
|
237
|
+
await writeFile(globalPath, content, 'utf8');
|
|
238
|
+
this.printSuccessCard(globalPath, 'Global configuration created at ~/.config/arise/.ariserc.json.');
|
|
239
|
+
return globalPath;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
static async runGuidedInit(cwd, isLocal, isInsideGit, options = {}) {
|
|
243
|
+
let repoRoot = cwd;
|
|
244
|
+
let isBare = false;
|
|
245
|
+
let localBranches = ['main'];
|
|
246
|
+
|
|
247
|
+
if (isInsideGit) {
|
|
248
|
+
try {
|
|
249
|
+
repoRoot = git.getRepoRootDir(cwd);
|
|
250
|
+
isBare = git.isBareRepo(repoRoot);
|
|
251
|
+
localBranches = git.getLocalBranches({ repoDir: repoRoot, bareRepo: isBare ? repoRoot : null });
|
|
252
|
+
} catch {}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const detected = detectPreset(cwd);
|
|
256
|
+
const detectedName = detected ? detected.name : 'generic';
|
|
257
|
+
|
|
258
|
+
// Step 1: Project Preset & Framework Selection
|
|
259
|
+
console.log(`\n${ANSI.bold}${ANSI.cyan}── Step 1: Project Preset & Framework Selection (preset) ──${ANSI.reset}`);
|
|
260
|
+
console.log(
|
|
261
|
+
` ${ANSI.dim}Arise uses pluggable presets to configure quadrant terminal layouts, scaffolding hooks, and dependencies.${ANSI.reset}\n`
|
|
262
|
+
);
|
|
263
|
+
console.log(` ${ANSI.bold}Preset Options & Behaviors:${ANSI.reset}`);
|
|
264
|
+
console.log(` ${ANSI.yellow}• Node.js (node):${ANSI.reset} Auto-copies .env, runs npm install, boots dev server & agent panes.`);
|
|
265
|
+
console.log(` ${ANSI.green}• Laravel (laravel):${ANSI.reset} Auto-copies .env, runs composer install, tails storage logs & boots agent.`);
|
|
266
|
+
console.log(` ${ANSI.blue}• Generic (generic):${ANSI.reset} Standard 4-pane quadrant layout (Editor, Shell, Build, AI Agent).`);
|
|
267
|
+
console.log(` ${ANSI.magenta}• Custom Preset:${ANSI.reset} Enter custom preset module name or path.\n`);
|
|
268
|
+
|
|
269
|
+
const presetChoice = await promptSelect({
|
|
270
|
+
message: 'Choose project preset:',
|
|
271
|
+
choices: [
|
|
272
|
+
{
|
|
273
|
+
label: `✨ Node.js (node)${detectedName === 'node' ? ' (Detected)' : ''}`,
|
|
274
|
+
value: 'node',
|
|
275
|
+
hint: 'npm/pnpm/yarn lifecycle, .env scaffolding, dev server pane',
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
label: `🐘 Laravel / PHP (laravel)${detectedName === 'laravel' ? ' (Detected)' : ''}`,
|
|
279
|
+
value: 'laravel',
|
|
280
|
+
hint: 'composer lifecycle, storage permissions, log tail pane',
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
label: `📦 Generic (generic)${detectedName === 'generic' ? ' (Detected)' : ''}`,
|
|
284
|
+
value: 'generic',
|
|
285
|
+
hint: 'Universal 4-pane terminal quadrant layout',
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
label: '✏️ Custom Preset...',
|
|
289
|
+
value: '__custom__',
|
|
290
|
+
hint: 'Specify custom preset name',
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
defaultIndex: detectedName === 'laravel' ? 1 : (detectedName === 'node' ? 0 : 2),
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
if (!presetChoice) return null;
|
|
297
|
+
|
|
298
|
+
let finalPreset = presetChoice;
|
|
299
|
+
if (presetChoice === '__custom__') {
|
|
300
|
+
const customInput = await promptText({
|
|
301
|
+
message: 'Enter custom preset name:',
|
|
302
|
+
defaultValue: 'generic',
|
|
303
|
+
});
|
|
304
|
+
if (customInput === null) return null;
|
|
305
|
+
finalPreset = customInput.trim() || 'generic';
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Step 2: Git Topology & Branch Configuration
|
|
309
|
+
console.log(`\n${ANSI.bold}${ANSI.cyan}── Step 2: Git Topology & Branch Configuration (repo) ──${ANSI.reset}`);
|
|
310
|
+
console.log(
|
|
311
|
+
` ${ANSI.dim}Configure default base branch, protected branches, and repository directory topology.${ANSI.reset}\n`
|
|
312
|
+
);
|
|
313
|
+
console.log(` ${ANSI.bold}Topology Settings:${ANSI.reset}`);
|
|
314
|
+
console.log(` ${ANSI.yellow}• Base Branch (defaultBaseBranch):${ANSI.reset} Default source branch to branch off when creating worktrees.`);
|
|
315
|
+
console.log(` ${ANSI.green}• Protected Branches:${ANSI.reset} Branches shielded from accidental deletion during --nuke.`);
|
|
316
|
+
console.log(` ${ANSI.blue}• Bare Repository Topology:${ANSI.reset} Isolate worktrees in dedicated folders outside bare .git.\n`);
|
|
317
|
+
|
|
318
|
+
const defaultCandidate = localBranches.includes('develop')
|
|
319
|
+
? 'develop'
|
|
320
|
+
: (localBranches.includes('main') ? 'main' : (localBranches[0] || 'main'));
|
|
321
|
+
|
|
322
|
+
const baseBranchInput = await promptText({
|
|
323
|
+
message: 'Default base branch to branch off (defaultBaseBranch):',
|
|
324
|
+
defaultValue: defaultCandidate,
|
|
325
|
+
});
|
|
326
|
+
if (baseBranchInput === null) return null;
|
|
327
|
+
const defaultBaseBranch = baseBranchInput.trim() || defaultCandidate;
|
|
328
|
+
|
|
329
|
+
const commonProtected = ['main', 'master', 'develop', 'prod', 'staging'];
|
|
330
|
+
const protectedChoices = commonProtected.map((b) => ({
|
|
331
|
+
label: b,
|
|
332
|
+
value: b,
|
|
333
|
+
selected: true,
|
|
334
|
+
}));
|
|
335
|
+
|
|
336
|
+
const selectedProtected = await promptMultiSelect({
|
|
337
|
+
message: 'Select protected branches (cannot be nuked):',
|
|
338
|
+
choices: protectedChoices,
|
|
339
|
+
allowCustomInput: true,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
if (selectedProtected === null) return null;
|
|
343
|
+
const protectedBranches = selectedProtected.length > 0 ? selectedProtected : commonProtected;
|
|
344
|
+
|
|
345
|
+
let bareRepoPath = null;
|
|
346
|
+
let worktreesBasePath = null;
|
|
347
|
+
|
|
348
|
+
if (isInsideGit && isBare) {
|
|
349
|
+
console.log(` ${ANSI.yellow}Detected bare Git repository topology at ${repoRoot}.${ANSI.reset}`);
|
|
350
|
+
bareRepoPath = repoRoot;
|
|
351
|
+
const defaultWtBase = path.join(path.dirname(repoRoot), 'worktrees');
|
|
352
|
+
const wtBaseInput = await promptText({
|
|
353
|
+
message: 'Worktrees base directory (worktreesBase):',
|
|
354
|
+
defaultValue: defaultWtBase,
|
|
355
|
+
completer: 'dir',
|
|
356
|
+
});
|
|
357
|
+
if (wtBaseInput === null) return null;
|
|
358
|
+
worktreesBasePath = wtBaseInput.trim() || defaultWtBase;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Step 3: AI CLI Agent & Workspace Focus
|
|
362
|
+
console.log(`\n${ANSI.bold}${ANSI.cyan}── Step 3: AI CLI Agent & Herdr Workspace (workspace) ──${ANSI.reset}`);
|
|
363
|
+
console.log(
|
|
364
|
+
` ${ANSI.dim}Arise embeds an AI CLI agent directly in your 4-pane Herdr terminal workspace.${ANSI.reset}\n`
|
|
365
|
+
);
|
|
366
|
+
console.log(` ${ANSI.bold}Supported AI Agents:${ANSI.reset}`);
|
|
367
|
+
console.log(` ${ANSI.yellow}• Antigravity (agy):${ANSI.reset} Google Deepmind Advanced Agentic Coding CLI (Recommended).`);
|
|
368
|
+
console.log(` ${ANSI.green}• Claude Code (claude):${ANSI.reset} Anthropic Claude CLI agent.`);
|
|
369
|
+
console.log(` ${ANSI.blue}• Aider (aider):${ANSI.reset} Git-integrated pair programming CLI.`);
|
|
370
|
+
console.log(` ${ANSI.magenta}• Copilot (copilot):${ANSI.reset} GitHub Copilot in the CLI.`);
|
|
371
|
+
console.log(` ${ANSI.gray}• Disabled (none):${ANSI.reset} Standard interactive shell pane without AI agent.\n`);
|
|
372
|
+
|
|
373
|
+
const agentChoice = await promptSelect({
|
|
374
|
+
message: 'Select AI CLI agent for workspace quadrant:',
|
|
375
|
+
choices: [
|
|
376
|
+
{
|
|
377
|
+
label: '✨ Antigravity / AGY CLI (Recommended)',
|
|
378
|
+
value: 'agy',
|
|
379
|
+
hint: 'Deepmind agentic pair programmer, instant setup',
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
label: '🤖 Claude Code (claude)',
|
|
383
|
+
value: 'claude',
|
|
384
|
+
hint: 'Anthropic Claude Code CLI',
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
label: '⚡ Aider (aider)',
|
|
388
|
+
value: 'aider',
|
|
389
|
+
hint: 'AI pair programming in terminal',
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
label: '🐙 GitHub Copilot CLI (copilot)',
|
|
393
|
+
value: 'copilot',
|
|
394
|
+
hint: 'GitHub Copilot CLI tool',
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
label: '✏️ Custom Agent Command...',
|
|
398
|
+
value: '__custom__',
|
|
399
|
+
hint: 'Enter custom CLI command (e.g. cursor, llm, script)',
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
label: '🚫 None / Disabled',
|
|
403
|
+
value: 'none',
|
|
404
|
+
hint: 'Standard bash/zsh shell pane',
|
|
405
|
+
},
|
|
406
|
+
],
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
if (!agentChoice) return null;
|
|
410
|
+
|
|
411
|
+
let finalAgent = agentChoice;
|
|
412
|
+
if (agentChoice === '__custom__') {
|
|
413
|
+
const customAgent = await promptText({
|
|
414
|
+
message: 'Enter custom AI agent command:',
|
|
415
|
+
defaultValue: 'agy',
|
|
416
|
+
});
|
|
417
|
+
if (customAgent === null) return null;
|
|
418
|
+
finalAgent = customAgent.trim() || 'agy';
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const labelPrefixInput = await promptText({
|
|
422
|
+
message: 'Optional Herdr workspace label prefix (e.g. "[BE] " or press Enter for none):',
|
|
423
|
+
defaultValue: '',
|
|
424
|
+
});
|
|
425
|
+
if (labelPrefixInput === null) return null;
|
|
426
|
+
const labelPrefix = labelPrefixInput.trim();
|
|
427
|
+
|
|
428
|
+
// Step 4: Terminal Layout & Quadrant Architecture
|
|
429
|
+
console.log(`\n${ANSI.bold}${ANSI.cyan}── Step 4: Terminal Layout & Workspace Panes (layout) ──${ANSI.reset}`);
|
|
430
|
+
console.log(
|
|
431
|
+
` ${ANSI.dim}Configure how Herdr splits your terminal window into quadrants and startup command panes.${ANSI.reset}\n`
|
|
432
|
+
);
|
|
433
|
+
console.log(` ${ANSI.bold}Available Layout Architectures:${ANSI.reset}`);
|
|
434
|
+
console.log(` ${ANSI.yellow}• 4-Pane Quadrant (2x2 Grid):${ANSI.reset} Top-Left: Editor, Top-Right: Server/Logs, Bottom-Left: Shell, Bottom-Right: AI Agent.`);
|
|
435
|
+
console.log(` ${ANSI.green}• 3-Pane Side-Stack:${ANSI.reset} Editor on Left half; Server on Top-Right; AI Agent on Bottom-Right.`);
|
|
436
|
+
console.log(` ${ANSI.blue}• 2-Pane Side-by-Side (Vertical):${ANSI.reset} Left: Editor/Shell; Right: AI Agent.`);
|
|
437
|
+
console.log(` ${ANSI.magenta}• 2-Pane Top / Bottom (Horizontal):${ANSI.reset} Top: Editor/Shell; Bottom: AI Agent.`);
|
|
438
|
+
console.log(` ${ANSI.cyan}• 🛠️ Custom Pane-by-Pane Builder:${ANSI.reset} Interactively define every pane, command, parent, and split direction.\n`);
|
|
439
|
+
|
|
440
|
+
const layoutStyleChoice = await promptSelect({
|
|
441
|
+
message: 'Select terminal workspace layout architecture:',
|
|
442
|
+
choices: [
|
|
443
|
+
{
|
|
444
|
+
label: '🪟 4-Pane Quadrant (2x2 Grid) — Recommended',
|
|
445
|
+
value: 'quadrant',
|
|
446
|
+
hint: 'Top-Left: Editor | Top-Right: Server/Logs | Bottom-Left: Shell | Bottom-Right: AI Agent',
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
label: '🧱 3-Pane Side-Stack (Left Editor + Right Split)',
|
|
450
|
+
value: 'three_pane',
|
|
451
|
+
hint: 'Left: Editor | Right-Top: Server/Logs | Right-Bottom: AI Agent',
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
label: '🌗 2-Pane Side-by-Side (Vertical Split)',
|
|
455
|
+
value: 'split_vertical',
|
|
456
|
+
hint: 'Left: Editor/Shell | Right: AI Agent',
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
label: '⬒ 2-Pane Top / Bottom (Horizontal Split)',
|
|
460
|
+
value: 'split_horizontal',
|
|
461
|
+
hint: 'Top: Editor/Shell | Bottom: AI Agent',
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
label: '🛠️ Custom Interactive Pane Builder',
|
|
465
|
+
value: 'custom',
|
|
466
|
+
hint: 'Manually configure pane titles, startup commands, split parents, and directions',
|
|
467
|
+
},
|
|
468
|
+
],
|
|
469
|
+
defaultIndex: 0,
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
if (!layoutStyleChoice) return null;
|
|
473
|
+
|
|
474
|
+
let configuredLayout = [];
|
|
475
|
+
|
|
476
|
+
if (layoutStyleChoice === 'custom') {
|
|
477
|
+
console.log(`\n ${ANSI.bold}🛠️ Custom Pane Layout Builder:${ANSI.reset}`);
|
|
478
|
+
console.log(` ${ANSI.dim}Configure each pane in order. The first pane is the root pane; subsequent panes split from an existing pane.${ANSI.reset}\n`);
|
|
479
|
+
|
|
480
|
+
const paneCountInput = await promptText({
|
|
481
|
+
message: 'How many panes would you like to configure in this workspace? (1-6):',
|
|
482
|
+
defaultValue: '4',
|
|
483
|
+
validate: (val) => {
|
|
484
|
+
const num = parseInt(val.trim(), 10);
|
|
485
|
+
if (isNaN(num) || num < 1 || num > 6) return 'Please enter a number between 1 and 6';
|
|
486
|
+
return true;
|
|
487
|
+
},
|
|
488
|
+
});
|
|
489
|
+
if (paneCountInput === null) return null;
|
|
490
|
+
const paneCount = parseInt(paneCountInput.trim(), 10) || 4;
|
|
491
|
+
|
|
492
|
+
for (let i = 1; i <= paneCount; i++) {
|
|
493
|
+
const defaultId = `pane-${i}`;
|
|
494
|
+
const defaultTitle = `Pane ${i}`;
|
|
495
|
+
const defaultCmd = '';
|
|
496
|
+
|
|
497
|
+
const paneIdInput = await promptText({
|
|
498
|
+
message: `Pane ${i} unique identifier (id):`,
|
|
499
|
+
defaultValue: defaultId,
|
|
500
|
+
validate: (val) => Boolean(val.trim()) || 'Pane ID cannot be empty',
|
|
501
|
+
});
|
|
502
|
+
if (paneIdInput === null) return null;
|
|
503
|
+
const paneId = paneIdInput.trim();
|
|
504
|
+
|
|
505
|
+
const paneTitleInput = await promptText({
|
|
506
|
+
message: `Pane ${i} display title:`,
|
|
507
|
+
defaultValue: defaultTitle,
|
|
508
|
+
});
|
|
509
|
+
if (paneTitleInput === null) return null;
|
|
510
|
+
const paneTitle = paneTitleInput.trim() || paneId;
|
|
511
|
+
|
|
512
|
+
const paneCmdInput = await promptText({
|
|
513
|
+
message: `Pane ${i} startup command (leave empty for clean shell):`,
|
|
514
|
+
defaultValue: defaultCmd,
|
|
515
|
+
});
|
|
516
|
+
if (paneCmdInput === null) return null;
|
|
517
|
+
const paneCmd = paneCmdInput.trim() || null;
|
|
518
|
+
|
|
519
|
+
const paneDef = {
|
|
520
|
+
id: paneId,
|
|
521
|
+
title: paneTitle,
|
|
522
|
+
cmd: paneCmd,
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
if (i === 1) {
|
|
526
|
+
paneDef.position = 'root';
|
|
527
|
+
} else {
|
|
528
|
+
const previousPanes = configuredLayout.map((p) => ({
|
|
529
|
+
label: `Pane "${p.title}" (id: ${p.id})`,
|
|
530
|
+
value: p.id,
|
|
531
|
+
}));
|
|
532
|
+
|
|
533
|
+
const splitFrom = await promptSelect({
|
|
534
|
+
message: `Split Pane ${i} from which existing pane?`,
|
|
535
|
+
choices: previousPanes,
|
|
536
|
+
defaultIndex: previousPanes.length - 1,
|
|
537
|
+
});
|
|
538
|
+
if (!splitFrom) return null;
|
|
539
|
+
paneDef.from = splitFrom;
|
|
540
|
+
|
|
541
|
+
const splitDir = await promptSelect({
|
|
542
|
+
message: `Split direction from "${splitFrom}":`,
|
|
543
|
+
choices: [
|
|
544
|
+
{ label: '➡️ Right (Vertical split - side-by-side)', value: 'right' },
|
|
545
|
+
{ label: '⬇️ Down (Horizontal split - stacked)', value: 'down' },
|
|
546
|
+
],
|
|
547
|
+
defaultIndex: i % 2 === 0 ? 0 : 1,
|
|
548
|
+
});
|
|
549
|
+
if (!splitDir) return null;
|
|
550
|
+
paneDef.split = splitDir;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Auto-detect agent designation from command or ID without redundant yes/no prompts
|
|
554
|
+
if (finalAgent !== 'none') {
|
|
555
|
+
if (paneCmd === finalAgent || paneId === finalAgent || paneId === 'agy' || paneId === 'agent' || paneId === 'ai') {
|
|
556
|
+
paneDef.isAgent = true;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
configuredLayout.push(paneDef);
|
|
561
|
+
}
|
|
562
|
+
} else {
|
|
563
|
+
console.log(`\n ${ANSI.bold}Customize Pane Startup Commands:${ANSI.reset}`);
|
|
564
|
+
|
|
565
|
+
const editorCmdInput = await promptText({
|
|
566
|
+
message: 'Editor startup command (Top-Left / Main pane):',
|
|
567
|
+
defaultValue: 'vim .',
|
|
568
|
+
});
|
|
569
|
+
if (editorCmdInput === null) return null;
|
|
570
|
+
const editorCmd = editorCmdInput.trim() || null;
|
|
571
|
+
|
|
572
|
+
let defaultServerCmd = '';
|
|
573
|
+
if (finalPreset === 'node') defaultServerCmd = 'npm run dev';
|
|
574
|
+
else if (finalPreset === 'laravel') defaultServerCmd = 'tail -f storage/logs/laravel.log';
|
|
575
|
+
|
|
576
|
+
let serverCmd = null;
|
|
577
|
+
if (layoutStyleChoice === 'quadrant' || layoutStyleChoice === 'three_pane') {
|
|
578
|
+
const serverCmdInput = await promptText({
|
|
579
|
+
message: 'Dev server / logs startup command (Top-Right pane, leave empty for shell):',
|
|
580
|
+
defaultValue: defaultServerCmd,
|
|
581
|
+
});
|
|
582
|
+
if (serverCmdInput === null) return null;
|
|
583
|
+
serverCmd = serverCmdInput.trim() || null;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const agentCmd = finalAgent === 'none' ? null : finalAgent;
|
|
587
|
+
const agentTitle = finalAgent === 'none' ? 'shell' : finalAgent;
|
|
588
|
+
const serverTitle = serverCmd
|
|
589
|
+
? (finalPreset === 'laravel' ? 'logs' : 'server')
|
|
590
|
+
: 'shell';
|
|
591
|
+
|
|
592
|
+
if (layoutStyleChoice === 'quadrant') {
|
|
593
|
+
configuredLayout = [
|
|
594
|
+
{ id: 'vim', title: 'vim', cmd: editorCmd, position: 'root' },
|
|
595
|
+
{ id: 'server', title: serverTitle, cmd: serverCmd, split: 'right', from: 'vim' },
|
|
596
|
+
{ id: 'shell', title: 'shell', cmd: null, split: 'down', from: 'vim' },
|
|
597
|
+
{ id: 'agy', title: agentTitle, cmd: agentCmd, split: 'down', from: 'server', isAgent: true },
|
|
598
|
+
];
|
|
599
|
+
} else if (layoutStyleChoice === 'three_pane') {
|
|
600
|
+
configuredLayout = [
|
|
601
|
+
{ id: 'vim', title: 'vim', cmd: editorCmd, position: 'root' },
|
|
602
|
+
{ id: 'server', title: serverTitle, cmd: serverCmd, split: 'right', from: 'vim' },
|
|
603
|
+
{ id: 'agy', title: agentTitle, cmd: agentCmd, split: 'down', from: 'server', isAgent: true },
|
|
604
|
+
];
|
|
605
|
+
} else if (layoutStyleChoice === 'split_vertical') {
|
|
606
|
+
configuredLayout = [
|
|
607
|
+
{ id: 'vim', title: 'editor', cmd: editorCmd, position: 'root' },
|
|
608
|
+
{ id: 'agy', title: agentTitle, cmd: agentCmd, split: 'right', from: 'vim', isAgent: true },
|
|
609
|
+
];
|
|
610
|
+
} else if (layoutStyleChoice === 'split_horizontal') {
|
|
611
|
+
configuredLayout = [
|
|
612
|
+
{ id: 'vim', title: 'editor', cmd: editorCmd, position: 'root' },
|
|
613
|
+
{ id: 'agy', title: agentTitle, cmd: agentCmd, split: 'down', from: 'vim', isAgent: true },
|
|
614
|
+
];
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Dynamic Default Pane Focus selection based on actual configured panes
|
|
619
|
+
const focusChoices = configuredLayout.map((p) => {
|
|
620
|
+
const typeHint = p.isAgent
|
|
621
|
+
? 'AI Agent'
|
|
622
|
+
: (p.cmd ? `command: "${p.cmd}"` : 'clean shell');
|
|
623
|
+
return {
|
|
624
|
+
label: `Pane "${p.title}" (id: ${p.id}) — [${typeHint}]`,
|
|
625
|
+
value: p.id,
|
|
626
|
+
hint: p.isAgent ? 'Instant focus on pair programming agent' : (p.id === 'vim' ? 'Focus code editor upon workspace boot' : `Focus pane "${p.title}"`),
|
|
627
|
+
};
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
const agentIndex = configuredLayout.findIndex((p) => p.isAgent);
|
|
631
|
+
const defaultFocusIndex = agentIndex >= 0 ? agentIndex : 0;
|
|
632
|
+
|
|
633
|
+
const focusChoice = await promptSelect({
|
|
634
|
+
message: 'Default pane to focus upon creation (defaultFocus):',
|
|
635
|
+
choices: focusChoices,
|
|
636
|
+
defaultIndex: defaultFocusIndex,
|
|
637
|
+
});
|
|
638
|
+
if (!focusChoice) return null;
|
|
639
|
+
|
|
640
|
+
for (const pane of configuredLayout) {
|
|
641
|
+
if (pane.id === focusChoice) {
|
|
642
|
+
pane.focus = true;
|
|
643
|
+
} else {
|
|
644
|
+
delete pane.focus;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Step 5: Scaffolding & Environment Setup
|
|
649
|
+
console.log(`\n${ANSI.bold}${ANSI.cyan}── Step 5: Scaffolding & Environment Automation (scaffold) ──${ANSI.reset}`);
|
|
650
|
+
console.log(
|
|
651
|
+
` ${ANSI.dim}Automate environment file copying, dependency installation, and web server symlinks.${ANSI.reset}\n`
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
const defaultEnv = finalPreset === 'laravel' || finalPreset === 'node' ? '.env' : '';
|
|
655
|
+
const envSourceInput = await promptText({
|
|
656
|
+
message: 'Source .env file path to copy into new worktrees (press Enter to skip):',
|
|
657
|
+
defaultValue: defaultEnv,
|
|
658
|
+
completer: 'path',
|
|
659
|
+
});
|
|
660
|
+
if (envSourceInput === null) return null;
|
|
661
|
+
const envSource = envSourceInput.trim() || null;
|
|
662
|
+
|
|
663
|
+
let defaultInstall = '';
|
|
664
|
+
if (finalPreset === 'node') defaultInstall = 'npm install';
|
|
665
|
+
else if (finalPreset === 'laravel') defaultInstall = 'composer install';
|
|
666
|
+
|
|
667
|
+
const installInput = await promptText({
|
|
668
|
+
message: 'Dependency install command to run on creation (press Enter to skip):',
|
|
669
|
+
defaultValue: defaultInstall,
|
|
670
|
+
});
|
|
671
|
+
if (installInput === null) return null;
|
|
672
|
+
const installCmd = installInput.trim() || null;
|
|
673
|
+
|
|
674
|
+
const symlinkInput = await promptText({
|
|
675
|
+
message: 'Optional web server symlink path to update (e.g. /var/www/active, press Enter to skip):',
|
|
676
|
+
defaultValue: '',
|
|
677
|
+
completer: 'dir',
|
|
678
|
+
});
|
|
679
|
+
if (symlinkInput === null) return null;
|
|
680
|
+
const symlinkPath = symlinkInput.trim() || null;
|
|
681
|
+
|
|
682
|
+
// Step 6: Agent Skill Installation
|
|
683
|
+
let skillInstalled = false;
|
|
684
|
+
if (process.stdin.isTTY) {
|
|
685
|
+
console.log(`\n${ANSI.bold}${ANSI.cyan}── Step 6: AI Agent Skill Installation (skill) ──${ANSI.reset}`);
|
|
686
|
+
console.log(
|
|
687
|
+
` ${ANSI.dim}Install the Arise AI Agent Skill to enable Antigravity and Claude Code to orchestrate worktrees directly.${ANSI.reset}\n`
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
const wantSkill = await promptConfirm({
|
|
691
|
+
message: 'Install Arise agent skill for Antigravity & Claude Code now?',
|
|
692
|
+
defaultYes: true,
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
if (wantSkill) {
|
|
696
|
+
try {
|
|
697
|
+
installSkill({ scope: isLocal ? 'local' : 'global', cwd });
|
|
698
|
+
skillInstalled = true;
|
|
699
|
+
} catch (err) {
|
|
700
|
+
console.warn(` ${ANSI.yellow}Skill installation skipped: ${err.message}${ANSI.reset}`);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Step 7: Write Configuration File
|
|
706
|
+
console.log(`\n${ANSI.bold}${ANSI.cyan}── Step 7: Save Configuration ──${ANSI.reset}\n`);
|
|
707
|
+
|
|
708
|
+
const targetFilePath = options.targetPath || (isLocal
|
|
709
|
+
? path.join(cwd, '.ariserc.json')
|
|
710
|
+
: path.join(os.homedir(), '.config', 'arise', '.ariserc.json'));
|
|
711
|
+
|
|
712
|
+
if (fs.existsSync(targetFilePath) && !options.force) {
|
|
713
|
+
console.log(` ${ANSI.yellow}Note: Configuration already exists at ${targetFilePath}.${ANSI.reset}\n`);
|
|
714
|
+
const overwrite = await promptConfirm({
|
|
715
|
+
message: `Overwrite existing configuration file?`,
|
|
716
|
+
defaultYes: true,
|
|
717
|
+
});
|
|
718
|
+
if (!overwrite) {
|
|
719
|
+
console.log(` ${ANSI.yellow}Existing configuration preserved.${ANSI.reset}\n`);
|
|
720
|
+
return targetFilePath;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
await mkdir(path.dirname(targetFilePath), { recursive: true });
|
|
725
|
+
|
|
726
|
+
const configObj = {
|
|
727
|
+
$schema: './arise.schema.json',
|
|
728
|
+
preset: finalPreset,
|
|
729
|
+
repo: {
|
|
730
|
+
defaultBaseBranch,
|
|
731
|
+
protectedBranches,
|
|
732
|
+
},
|
|
733
|
+
workspace: {
|
|
734
|
+
agent: finalAgent,
|
|
735
|
+
defaultFocus: focusChoice,
|
|
736
|
+
labelPrefix,
|
|
737
|
+
},
|
|
738
|
+
layout: configuredLayout,
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
if (bareRepoPath) {
|
|
742
|
+
configObj.repo.bareRepo = bareRepoPath;
|
|
743
|
+
if (worktreesBasePath) configObj.repo.worktreesBase = worktreesBasePath;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (envSource || installCmd || symlinkPath) {
|
|
747
|
+
configObj.scaffold = {};
|
|
748
|
+
if (envSource) configObj.scaffold.envSource = envSource;
|
|
749
|
+
if (installCmd) configObj.scaffold.install = installCmd;
|
|
750
|
+
if (symlinkPath) configObj.scaffold.symlink = symlinkPath;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const scopeTitle = isLocal ? 'Local Repository Configuration (.ariserc.json)' : 'Global Configuration (~/.config/arise/.ariserc.json)';
|
|
754
|
+
const content = `// Arise ${scopeTitle}
|
|
755
|
+
// Documentation & Guide: https://github.com/tsuzuku/arise
|
|
756
|
+
//
|
|
757
|
+
// SETTINGS REFERENCE:
|
|
758
|
+
// • preset: Preset name ('node' | 'laravel' | 'generic' | custom)
|
|
759
|
+
// • repo.defaultBaseBranch: Default branch to branch off when creating worktrees
|
|
760
|
+
// • repo.protectedBranches: Branches protected from deletion during --nuke
|
|
761
|
+
// • repo.bareRepo: Path to bare repository (if using bare git topology)
|
|
762
|
+
// • repo.worktreesBase: Base directory where worktrees are created
|
|
763
|
+
// • workspace.agent: CLI AI agent ('agy' | 'claude' | 'aider' | 'copilot' | 'none')
|
|
764
|
+
// • workspace.defaultFocus: Pane to focus on workspace boot ('agy' | 'vim' | 'server' | 'shell')
|
|
765
|
+
// • workspace.labelPrefix: Prefix added to Herdr workspace labels (e.g. '[BE] ')
|
|
766
|
+
// • layout: Declarative terminal layout definitions (id, title, cmd, position, from, split, focus, isAgent)
|
|
767
|
+
// • scaffold.envSource: Source .env template file to copy into worktree
|
|
768
|
+
// • scaffold.install: Command to run to install dependencies upon creation
|
|
769
|
+
// • scaffold.symlink: Optional symlink to point to the active worktree
|
|
770
|
+
${JSON.stringify(configObj, null, 2)}
|
|
771
|
+
`;
|
|
772
|
+
|
|
773
|
+
await writeFile(targetFilePath, content, 'utf8');
|
|
774
|
+
await this.handleGitignore(targetFilePath, isLocal, isInsideGit, cwd, options);
|
|
775
|
+
this.printSuccessCard(targetFilePath, `Configuration saved successfully to ${targetFilePath}.`);
|
|
776
|
+
return targetFilePath;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
static async handleGitignore(targetFilePath, isLocal, isInsideGit, cwd, options = {}) {
|
|
780
|
+
if (!isLocal) {
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
let repoRoot = cwd;
|
|
785
|
+
if (isInsideGit) {
|
|
786
|
+
try {
|
|
787
|
+
repoRoot = git.getRepoRootDir(cwd) || cwd;
|
|
788
|
+
} catch {
|
|
789
|
+
repoRoot = cwd;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const gitignorePath = path.join(repoRoot, '.gitignore');
|
|
794
|
+
const relPath = path.relative(repoRoot, targetFilePath);
|
|
795
|
+
const configFileName = (relPath && !relPath.startsWith('..')) ? relPath : path.basename(targetFilePath);
|
|
796
|
+
|
|
797
|
+
let existingContent = '';
|
|
798
|
+
if (fs.existsSync(gitignorePath)) {
|
|
799
|
+
try {
|
|
800
|
+
existingContent = fs.readFileSync(gitignorePath, 'utf8');
|
|
801
|
+
} catch {}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const lines = existingContent.split(/\r?\n/).map((l) => l.trim());
|
|
805
|
+
const isAlreadyIgnored =
|
|
806
|
+
lines.includes(configFileName) ||
|
|
807
|
+
lines.includes(`/${configFileName}`) ||
|
|
808
|
+
(path.basename(targetFilePath) !== configFileName && lines.includes(path.basename(targetFilePath)));
|
|
809
|
+
|
|
810
|
+
if (isAlreadyIgnored) {
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
let shouldAdd = false;
|
|
815
|
+
if (typeof options.gitignore === 'boolean') {
|
|
816
|
+
shouldAdd = options.gitignore;
|
|
817
|
+
} else if (typeof options.addToGitignore === 'boolean') {
|
|
818
|
+
shouldAdd = options.addToGitignore;
|
|
819
|
+
} else if (process.stdin.isTTY) {
|
|
820
|
+
const wantGitignore = await promptConfirm({
|
|
821
|
+
message: `Add ${configFileName} to .gitignore?`,
|
|
822
|
+
defaultYes: false,
|
|
823
|
+
});
|
|
824
|
+
shouldAdd = Boolean(wantGitignore);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (shouldAdd) {
|
|
828
|
+
try {
|
|
829
|
+
const needsNewline = existingContent.length > 0 && !existingContent.endsWith('\n');
|
|
830
|
+
const appendContent = `${needsNewline ? '\n' : ''}${configFileName}\n`;
|
|
831
|
+
await writeFile(gitignorePath, existingContent + appendContent, 'utf8');
|
|
832
|
+
console.log(` ${ANSI.green}✔ Added ${configFileName} to .gitignore${ANSI.reset}`);
|
|
833
|
+
return true;
|
|
834
|
+
} catch (err) {
|
|
835
|
+
console.warn(` ${ANSI.yellow}Warning: Failed to update .gitignore: ${err.message}${ANSI.reset}`);
|
|
836
|
+
return false;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
return false;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
static printSuccessCard(filePath, message) {
|
|
844
|
+
const cardLines = [
|
|
845
|
+
`${ANSI.bold}${ANSI.green}✔ ${message}${ANSI.reset}`,
|
|
846
|
+
`${ANSI.dim}Config path: ${filePath}${ANSI.reset}`,
|
|
847
|
+
'',
|
|
848
|
+
`${ANSI.bold}Next Steps:${ANSI.reset}`,
|
|
849
|
+
` • ${ANSI.cyan}arise --branch <feature>${ANSI.reset} Create a new git worktree & Herdr workspace`,
|
|
850
|
+
` • ${ANSI.cyan}arise${ANSI.reset} Launch the interactive TUI menu`,
|
|
851
|
+
` • ${ANSI.cyan}arise --nuke${ANSI.reset} Tear down worktrees and cleanup branches safely`,
|
|
852
|
+
` • ${ANSI.cyan}arise --help${ANSI.reset} View all available CLI flags and options`,
|
|
853
|
+
];
|
|
854
|
+
|
|
855
|
+
const box = drawBox('⚙️ Setup Complete', cardLines, Math.min(80, (process.stdout.columns || 80) - 2));
|
|
856
|
+
console.log(`\n${box.join('\n')}\n`);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
module.exports = {
|
|
861
|
+
ConfigInitWizard,
|
|
862
|
+
};
|