@tsuzuku/arise 0.2.0 → 0.3.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 +18 -6
- package/lib/git.js +12 -0
- package/lib/herdr.js +47 -6
- package/lib/interactive.js +644 -0
- package/lib/layout.js +21 -7
- package/lib/lifecycle/create.js +27 -3
- package/lib/lifecycle/nuke.js +43 -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
package/lib/lifecycle/create.js
CHANGED
|
@@ -6,6 +6,7 @@ const layout = require('../layout');
|
|
|
6
6
|
const { createContext } = require('../context');
|
|
7
7
|
const { resolveConfiguration } = require('../config');
|
|
8
8
|
const { showUsage } = require('../cli');
|
|
9
|
+
const logger = require('../logger');
|
|
9
10
|
|
|
10
11
|
async function executeCreate(flags, config, cwd = process.cwd()) {
|
|
11
12
|
const branch = flags.branch;
|
|
@@ -65,13 +66,36 @@ async function executeCreate(flags, config, cwd = process.cwd()) {
|
|
|
65
66
|
bareRepo: config.repo.bareRepo,
|
|
66
67
|
});
|
|
67
68
|
} catch (err) {
|
|
68
|
-
|
|
69
|
+
logger.error(`Failed to create worktree: ${err.message}`, err);
|
|
69
70
|
process.exit(1);
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
// 5. Re-resolve config using target worktree directory for branch-specific overrides
|
|
74
|
-
|
|
74
|
+
// 5. Re-resolve config using target worktree directory for branch-specific overrides with caller cwd fallback
|
|
75
|
+
let activeConfig = resolveConfiguration(flags, worktreePathToUse, cwd);
|
|
76
|
+
|
|
77
|
+
// If the worktree was freshly created and caller had an active configuration file in cwd, sync it into the new worktree
|
|
78
|
+
if (!worktreeExists && config.configFile && fs.existsSync(config.configFile)) {
|
|
79
|
+
try {
|
|
80
|
+
const targetConfigPath = path.join(worktreePathToUse, path.basename(config.configFile));
|
|
81
|
+
fs.copyFileSync(config.configFile, targetConfigPath);
|
|
82
|
+
logger.debug(`Synced configuration file from caller to new worktree: ${config.configFile} -> ${targetConfigPath}`);
|
|
83
|
+
activeConfig = resolveConfiguration(flags, worktreePathToUse, cwd);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
logger.warn(`Could not sync caller config file to new worktree: ${err.message}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (config.layout && config.layout.length > 0 && config.configFile && (!activeConfig.configFile || activeConfig.configFile.includes('.bare'))) {
|
|
90
|
+
activeConfig.layout = config.layout;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
logger.debug(`Active configuration for target="${worktreePathToUse}" (caller="${cwd}"):`, {
|
|
94
|
+
configFile: activeConfig.configFile,
|
|
95
|
+
layoutCount: activeConfig.layout?.length,
|
|
96
|
+
panes: activeConfig.layout?.map((p) => ({ id: p.id, title: p.title, from: p.from, split: p.split })),
|
|
97
|
+
});
|
|
98
|
+
|
|
75
99
|
prefix = activeConfig.workspace.labelPrefix || '';
|
|
76
100
|
sessionName = (prefix && !baseWorkspace.startsWith(prefix.trim()))
|
|
77
101
|
? `${prefix}${baseWorkspace}`
|
package/lib/lifecycle/nuke.js
CHANGED
|
@@ -6,6 +6,13 @@ const herdr = require('../herdr');
|
|
|
6
6
|
const { createContext } = require('../context');
|
|
7
7
|
const { resolveConfiguration } = require('../config');
|
|
8
8
|
const { showUsage } = require('../cli');
|
|
9
|
+
const { promptConfirm } = require('../tui/prompt');
|
|
10
|
+
|
|
11
|
+
function getRepoDisplayName(repoPath) {
|
|
12
|
+
if (!repoPath) return 'unknown';
|
|
13
|
+
const cleaned = repoPath.replace(/[/\\]\.bare$/, '').replace(/[/\\]\.git$/, '');
|
|
14
|
+
return path.basename(cleaned) || repoPath;
|
|
15
|
+
}
|
|
9
16
|
|
|
10
17
|
async function executeNuke(flags, config, cwd = process.cwd()) {
|
|
11
18
|
const repoRoot = git.getRepoRootDir(cwd, config.repo.bareRepo);
|
|
@@ -84,12 +91,46 @@ async function executeNuke(flags, config, cwd = process.cwd()) {
|
|
|
84
91
|
console.log(`Directory Only Mode: ${flags.dirOnly ? 'Yes (branches will be preserved)' : 'No'}`);
|
|
85
92
|
console.log(`Keep Remote Branch: ${flags.keepRemote ? 'Yes' : 'No'}\n`);
|
|
86
93
|
|
|
87
|
-
// Safety check: Never delete root repository
|
|
94
|
+
// Safety check 1: Never delete root repository
|
|
88
95
|
if (path.resolve(targetWorktreePath) === path.resolve(repoRoot)) {
|
|
89
96
|
console.error(`Error: Cannot delete the repository root directory "${repoRoot}".`);
|
|
90
97
|
process.exit(1);
|
|
91
98
|
}
|
|
92
99
|
|
|
100
|
+
// Safety check 2: Cross-repository nuke detection
|
|
101
|
+
const callerRepoRoot = git.getRepoRootDir(cwd, config.repo.bareRepo);
|
|
102
|
+
const targetRepoRoot = fs.existsSync(targetWorktreePath)
|
|
103
|
+
? git.getRepoRootDir(targetWorktreePath, activeConfig.repo.bareRepo)
|
|
104
|
+
: repoRoot;
|
|
105
|
+
|
|
106
|
+
const isCrossRepo = callerRepoRoot && targetRepoRoot &&
|
|
107
|
+
path.resolve(callerRepoRoot) !== path.resolve(targetRepoRoot);
|
|
108
|
+
|
|
109
|
+
if (isCrossRepo) {
|
|
110
|
+
const callerRepoName = getRepoDisplayName(callerRepoRoot);
|
|
111
|
+
const targetRepoName = getRepoDisplayName(targetRepoRoot);
|
|
112
|
+
|
|
113
|
+
console.warn(`\x1b[1m\x1b[33m⚠️ CROSS-REPOSITORY NUKE DETECTED\x1b[39m\x1b[22m`);
|
|
114
|
+
console.warn(`─────────────────────────────────────────────────────────────────`);
|
|
115
|
+
console.warn(`• Caller CWD: ${cwd} (Repo: ${callerRepoName})`);
|
|
116
|
+
console.warn(`• Target Worktree: ${targetWorktreePath} (Repo: ${targetRepoName})`);
|
|
117
|
+
console.warn(`• Associated Branch: ${targetBranch || '(None detected)'}`);
|
|
118
|
+
console.warn(`─────────────────────────────────────────────────────────────────`);
|
|
119
|
+
console.warn(`You are invoking --nuke from a different repository than the target worktree!\n`);
|
|
120
|
+
|
|
121
|
+
if (!flags.yes && !flags.force && process.stdin.isTTY && !process.env.CI) {
|
|
122
|
+
const confirmed = await promptConfirm({
|
|
123
|
+
question: `Are you sure you want to nuke this worktree from another repository?`,
|
|
124
|
+
defaultYes: false,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (!confirmed) {
|
|
128
|
+
console.log(`\nAborted. No worktrees or branches were deleted.`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
93
134
|
// If cwd is inside the target worktree, switch to rootDir/baseDir
|
|
94
135
|
if (path.resolve(process.cwd()).startsWith(path.resolve(targetWorktreePath))) {
|
|
95
136
|
console.log(`Current working directory is inside target worktree. Switching to: ${baseDir}`);
|
|
@@ -159,7 +200,7 @@ async function executeNuke(flags, config, cwd = process.cwd()) {
|
|
|
159
200
|
prefix ? `${prefix}${targetDirname}` : null,
|
|
160
201
|
].filter(Boolean);
|
|
161
202
|
|
|
162
|
-
herdr.
|
|
203
|
+
herdr.closeWorkspacesForWorktree(targetWorktreePath, matchTargets);
|
|
163
204
|
|
|
164
205
|
console.log(`\nCleanup complete!`);
|
|
165
206
|
};
|
package/lib/logger.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const { ANSI } = require('./tui/ansi');
|
|
5
|
+
|
|
6
|
+
let isDebugEnabled = false;
|
|
7
|
+
let logFilePath = null;
|
|
8
|
+
|
|
9
|
+
function initLogger(flags = {}) {
|
|
10
|
+
isDebugEnabled = Boolean(
|
|
11
|
+
flags.debug ||
|
|
12
|
+
flags.verbose ||
|
|
13
|
+
process.env.ARISE_DEBUG === '1' ||
|
|
14
|
+
process.env.DEBUG === 'arise' ||
|
|
15
|
+
process.env.DEBUG === '*'
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const logDir = path.join(os.homedir(), '.config', 'arise', 'logs');
|
|
20
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
21
|
+
logFilePath = path.join(logDir, 'arise.log');
|
|
22
|
+
} catch (e) {
|
|
23
|
+
logFilePath = path.join(os.tmpdir(), 'arise-debug.log');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function writeToFile(level, message, details = null) {
|
|
28
|
+
if (!logFilePath) return;
|
|
29
|
+
try {
|
|
30
|
+
const timestamp = new Date().toISOString();
|
|
31
|
+
let line = `[${timestamp}] [${level.toUpperCase()}] ${message}\n`;
|
|
32
|
+
if (details) {
|
|
33
|
+
line += typeof details === 'object' ? JSON.stringify(details, null, 2) + '\n' : String(details) + '\n';
|
|
34
|
+
}
|
|
35
|
+
fs.appendFileSync(logFilePath, line, 'utf8');
|
|
36
|
+
} catch (e) {}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function debug(message, details = null) {
|
|
40
|
+
writeToFile('DEBUG', message, details);
|
|
41
|
+
if (isDebugEnabled) {
|
|
42
|
+
console.log(` ${ANSI.dim}[DEBUG] ${message}${ANSI.reset}`);
|
|
43
|
+
if (details) {
|
|
44
|
+
console.log(ANSI.dim + (typeof details === 'object' ? JSON.stringify(details, null, 2) : details) + ANSI.reset);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function info(message) {
|
|
50
|
+
writeToFile('INFO', message);
|
|
51
|
+
console.log(message);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function warn(message, details = null) {
|
|
55
|
+
writeToFile('WARN', message, details);
|
|
56
|
+
console.warn(`${ANSI.yellow}Warning: ${message}${ANSI.reset}`);
|
|
57
|
+
if (isDebugEnabled && details) {
|
|
58
|
+
console.warn(ANSI.dim + (typeof details === 'object' ? JSON.stringify(details, null, 2) : details) + ANSI.reset);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function error(message, err = null) {
|
|
63
|
+
writeToFile('ERROR', message, err && err.stack ? err.stack : err);
|
|
64
|
+
console.error(`${ANSI.red}Error: ${message}${ANSI.reset}`);
|
|
65
|
+
if (err) {
|
|
66
|
+
if (isDebugEnabled && err.stack) {
|
|
67
|
+
console.error(ANSI.dim + err.stack + ANSI.reset);
|
|
68
|
+
} else if (err.message) {
|
|
69
|
+
console.error(`${ANSI.dim}Details: ${err.message}${ANSI.reset}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!isDebugEnabled && logFilePath) {
|
|
73
|
+
console.error(`${ANSI.dim}(Detailed logs saved to ${logFilePath})${ANSI.reset}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
initLogger,
|
|
79
|
+
isDebugEnabled: () => isDebugEnabled,
|
|
80
|
+
getLogFilePath: () => logFilePath,
|
|
81
|
+
debug,
|
|
82
|
+
info,
|
|
83
|
+
warn,
|
|
84
|
+
error,
|
|
85
|
+
};
|
package/lib/skill.js
CHANGED
|
@@ -15,6 +15,12 @@ Use this skill to answer questions about \`arise\` and run worktree management c
|
|
|
15
15
|
|
|
16
16
|
## 1. CLI Quick Reference & Cheatsheet
|
|
17
17
|
|
|
18
|
+
### Interactive Mode
|
|
19
|
+
\`\`\`bash
|
|
20
|
+
# Launch interactive menu (create, switch/open, list, nuke)
|
|
21
|
+
arise
|
|
22
|
+
\`\`\`
|
|
23
|
+
|
|
18
24
|
### Creating Worktrees
|
|
19
25
|
\`\`\`bash
|
|
20
26
|
# Create or open a worktree for a branch (auto-detects preset, boots Herdr workspace)
|
package/lib/tui/ansi.js
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
const ANSI = {
|
|
2
|
+
reset: '\x1b[0m',
|
|
3
|
+
bold: '\x1b[1m',
|
|
4
|
+
dim: '\x1b[2m',
|
|
5
|
+
italic: '\x1b[3m',
|
|
6
|
+
underline: '\x1b[4m',
|
|
7
|
+
inverse: '\x1b[7m',
|
|
8
|
+
hidden: '\x1b[8m',
|
|
9
|
+
strikethrough: '\x1b[9m',
|
|
10
|
+
|
|
11
|
+
// Foreground
|
|
12
|
+
black: '\x1b[30m',
|
|
13
|
+
red: '\x1b[31m',
|
|
14
|
+
green: '\x1b[32m',
|
|
15
|
+
yellow: '\x1b[33m',
|
|
16
|
+
blue: '\x1b[34m',
|
|
17
|
+
magenta: '\x1b[35m',
|
|
18
|
+
cyan: '\x1b[36m',
|
|
19
|
+
white: '\x1b[37m',
|
|
20
|
+
gray: '\x1b[90m',
|
|
21
|
+
|
|
22
|
+
// Bright Foreground
|
|
23
|
+
brightRed: '\x1b[91m',
|
|
24
|
+
brightGreen: '\x1b[92m',
|
|
25
|
+
brightYellow: '\x1b[93m',
|
|
26
|
+
brightBlue: '\x1b[94m',
|
|
27
|
+
brightMagenta: '\x1b[95m',
|
|
28
|
+
brightCyan: '\x1b[96m',
|
|
29
|
+
brightWhite: '\x1b[97m',
|
|
30
|
+
|
|
31
|
+
// Background
|
|
32
|
+
bgBlack: '\x1b[40m',
|
|
33
|
+
bgRed: '\x1b[41m',
|
|
34
|
+
bgGreen: '\x1b[42m',
|
|
35
|
+
bgYellow: '\x1b[43m',
|
|
36
|
+
bgBlue: '\x1b[44m',
|
|
37
|
+
bgMagenta: '\x1b[45m',
|
|
38
|
+
bgCyan: '\x1b[46m',
|
|
39
|
+
bgWhite: '\x1b[47m',
|
|
40
|
+
bgGray: '\x1b[100m',
|
|
41
|
+
|
|
42
|
+
// Cursor & Screen
|
|
43
|
+
clearScreen: '\x1b[2J\x1b[H',
|
|
44
|
+
clearLine: '\x1b[2K\r',
|
|
45
|
+
hideCursor: '\x1b[?25l',
|
|
46
|
+
showCursor: '\x1b[?25h',
|
|
47
|
+
altScreenEnter: '\x1b[?1049h',
|
|
48
|
+
altScreenExit: '\x1b[?1049l',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function stripAnsi(text) {
|
|
52
|
+
if (typeof text !== 'string') return '';
|
|
53
|
+
return text
|
|
54
|
+
// Strip OSC sequences
|
|
55
|
+
.replace(/\x1b\](?:[^\x07\x1b]|\x1b[^\\])*?(?:\x07|\x1b\\)/g, '')
|
|
56
|
+
// Strip CSI sequences
|
|
57
|
+
.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, '')
|
|
58
|
+
// Strip other escape sequences
|
|
59
|
+
.replace(/\x1b[@-Z\\-_]/g, '');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function visibleLength(text) {
|
|
63
|
+
return stripAnsi(text).length;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
class AnsiStyleTracker {
|
|
67
|
+
constructor() {
|
|
68
|
+
this.fg = '';
|
|
69
|
+
this.bg = '';
|
|
70
|
+
this.bold = false;
|
|
71
|
+
this.dim = false;
|
|
72
|
+
this.italic = false;
|
|
73
|
+
this.underline = false;
|
|
74
|
+
this.strike = false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
processCode(codeStr) {
|
|
78
|
+
const match = codeStr.match(/^\x1b\[([0-9;?]*)m$/);
|
|
79
|
+
if (!match) return;
|
|
80
|
+
const rawCodes = match[1] ? match[1].split(';').map(Number) : [0];
|
|
81
|
+
|
|
82
|
+
for (const c of rawCodes) {
|
|
83
|
+
if (c === 0) {
|
|
84
|
+
this.fg = '';
|
|
85
|
+
this.bg = '';
|
|
86
|
+
this.bold = false;
|
|
87
|
+
this.dim = false;
|
|
88
|
+
this.italic = false;
|
|
89
|
+
this.underline = false;
|
|
90
|
+
this.strike = false;
|
|
91
|
+
} else if (c === 1) {
|
|
92
|
+
this.bold = true;
|
|
93
|
+
} else if (c === 2) {
|
|
94
|
+
this.dim = true;
|
|
95
|
+
} else if (c === 3) {
|
|
96
|
+
this.italic = true;
|
|
97
|
+
} else if (c === 4) {
|
|
98
|
+
this.underline = true;
|
|
99
|
+
} else if (c === 9) {
|
|
100
|
+
this.strike = true;
|
|
101
|
+
} else if (c === 22) {
|
|
102
|
+
this.bold = false;
|
|
103
|
+
this.dim = false;
|
|
104
|
+
} else if (c === 23) {
|
|
105
|
+
this.italic = false;
|
|
106
|
+
} else if (c === 24) {
|
|
107
|
+
this.underline = false;
|
|
108
|
+
} else if (c === 29) {
|
|
109
|
+
this.strike = false;
|
|
110
|
+
} else if (c === 39) {
|
|
111
|
+
this.fg = '';
|
|
112
|
+
} else if (c === 49) {
|
|
113
|
+
this.bg = '';
|
|
114
|
+
} else if ((c >= 30 && c <= 37) || (c >= 90 && c <= 97)) {
|
|
115
|
+
this.fg = `\x1b[${c}m`;
|
|
116
|
+
} else if ((c >= 40 && c <= 47) || (c >= 100 && c <= 107)) {
|
|
117
|
+
this.bg = `\x1b[${c}m`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
getActiveCodes() {
|
|
123
|
+
let result = '';
|
|
124
|
+
if (this.bold) result += '\x1b[1m';
|
|
125
|
+
if (this.dim) result += '\x1b[2m';
|
|
126
|
+
if (this.italic) result += '\x1b[3m';
|
|
127
|
+
if (this.underline) result += '\x1b[4m';
|
|
128
|
+
if (this.strike) result += '\x1b[9m';
|
|
129
|
+
if (this.fg) result += this.fg;
|
|
130
|
+
if (this.bg) result += this.bg;
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
hasActiveStyles() {
|
|
135
|
+
return this.bold || this.dim || this.italic || this.underline || this.strike || Boolean(this.fg) || Boolean(this.bg);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function wrapAnsiLine(line, maxWidth, hangingIndent = '') {
|
|
140
|
+
if (maxWidth <= 0) return [line];
|
|
141
|
+
const totalVisible = visibleLength(line);
|
|
142
|
+
if (totalVisible <= maxWidth) {
|
|
143
|
+
return [line];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const tokenRegex = /(?:\x1b\](?:[^\x07\x1b]|\x1b[^\\])*?(?:\x07|\x1b\\)|\x1b\[[0-9;?]*[a-zA-Z]|\x1b[@-Z\\-_]|[^\s\x1b]+|\s+)/g;
|
|
147
|
+
const tokens = line.match(tokenRegex) || [line];
|
|
148
|
+
|
|
149
|
+
const result = [];
|
|
150
|
+
const tracker = new AnsiStyleTracker();
|
|
151
|
+
|
|
152
|
+
let currentLine = '';
|
|
153
|
+
let currentVisibleWidth = 0;
|
|
154
|
+
let isFirstLine = true;
|
|
155
|
+
const hangingVisibleWidth = visibleLength(hangingIndent);
|
|
156
|
+
|
|
157
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
158
|
+
const token = tokens[i];
|
|
159
|
+
|
|
160
|
+
if (token.startsWith('\x1b')) {
|
|
161
|
+
tracker.processCode(token);
|
|
162
|
+
currentLine += token;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const tokenVisibleLen = stripAnsi(token).length;
|
|
167
|
+
const isWhitespace = /^\s+$/.test(token);
|
|
168
|
+
|
|
169
|
+
if (isWhitespace) {
|
|
170
|
+
if (currentVisibleWidth === hangingVisibleWidth && !isFirstLine) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (currentVisibleWidth + tokenVisibleLen <= maxWidth) {
|
|
174
|
+
currentLine += token;
|
|
175
|
+
currentVisibleWidth += tokenVisibleLen;
|
|
176
|
+
} else {
|
|
177
|
+
if (tracker.hasActiveStyles()) {
|
|
178
|
+
currentLine += ANSI.reset;
|
|
179
|
+
}
|
|
180
|
+
result.push(currentLine);
|
|
181
|
+
isFirstLine = false;
|
|
182
|
+
currentLine = hangingIndent + tracker.getActiveCodes();
|
|
183
|
+
currentVisibleWidth = hangingVisibleWidth;
|
|
184
|
+
}
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Word token
|
|
189
|
+
if (currentVisibleWidth + tokenVisibleLen <= maxWidth) {
|
|
190
|
+
currentLine += token;
|
|
191
|
+
currentVisibleWidth += tokenVisibleLen;
|
|
192
|
+
} else {
|
|
193
|
+
if (currentVisibleWidth > (isFirstLine ? 0 : hangingVisibleWidth)) {
|
|
194
|
+
if (tracker.hasActiveStyles()) {
|
|
195
|
+
currentLine += ANSI.reset;
|
|
196
|
+
}
|
|
197
|
+
result.push(currentLine);
|
|
198
|
+
isFirstLine = false;
|
|
199
|
+
currentLine = hangingIndent + tracker.getActiveCodes();
|
|
200
|
+
currentVisibleWidth = hangingVisibleWidth;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (tokenVisibleLen > maxWidth - currentVisibleWidth) {
|
|
204
|
+
let remaining = token;
|
|
205
|
+
while (remaining.length > 0) {
|
|
206
|
+
const available = Math.max(1, maxWidth - currentVisibleWidth);
|
|
207
|
+
const chunk = remaining.slice(0, available);
|
|
208
|
+
remaining = remaining.slice(available);
|
|
209
|
+
|
|
210
|
+
currentLine += chunk;
|
|
211
|
+
currentVisibleWidth += chunk.length;
|
|
212
|
+
|
|
213
|
+
if (remaining.length > 0) {
|
|
214
|
+
if (tracker.hasActiveStyles()) {
|
|
215
|
+
currentLine += ANSI.reset;
|
|
216
|
+
}
|
|
217
|
+
result.push(currentLine);
|
|
218
|
+
isFirstLine = false;
|
|
219
|
+
currentLine = hangingIndent + tracker.getActiveCodes();
|
|
220
|
+
currentVisibleWidth = hangingVisibleWidth;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
currentLine += token;
|
|
225
|
+
currentVisibleWidth += tokenVisibleLen;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (currentLine.length > 0) {
|
|
231
|
+
if (tracker.hasActiveStyles()) {
|
|
232
|
+
currentLine += ANSI.reset;
|
|
233
|
+
}
|
|
234
|
+
result.push(currentLine);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return result.length > 0 ? result : [''];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function truncate(text, maxWidth) {
|
|
241
|
+
if (maxWidth <= 0) return '';
|
|
242
|
+
const totalVisible = visibleLength(text);
|
|
243
|
+
if (totalVisible <= maxWidth) return text;
|
|
244
|
+
|
|
245
|
+
const targetWidth = Math.max(1, maxWidth - 1);
|
|
246
|
+
const tokenRegex = /(?:\x1b\](?:[^\x07\x1b]|\x1b[^\\])*?(?:\x07|\x1b\\)|\x1b\[[0-9;?]*[a-zA-Z]|\x1b[@-Z\\-_]|[^\x1b])/g;
|
|
247
|
+
const tokens = text.match(tokenRegex) || [];
|
|
248
|
+
|
|
249
|
+
let result = '';
|
|
250
|
+
let currentVisible = 0;
|
|
251
|
+
|
|
252
|
+
for (const token of tokens) {
|
|
253
|
+
if (token.startsWith('\x1b')) {
|
|
254
|
+
result += token;
|
|
255
|
+
} else {
|
|
256
|
+
if (currentVisible + 1 > targetWidth) {
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
result += token;
|
|
260
|
+
currentVisible += 1;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return result + ANSI.reset + '…';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function drawBox(title, contentLines = [], width = 70) {
|
|
268
|
+
const safeWidth = Math.max(10, width);
|
|
269
|
+
const horizontal = '─'.repeat(Math.max(0, safeWidth - 2));
|
|
270
|
+
const titleFormatted = title ? ` ${ANSI.bold}${title}${ANSI.reset} ` : '';
|
|
271
|
+
const titleLen = visibleLength(titleFormatted);
|
|
272
|
+
const topBar = `┌${titleFormatted}${'─'.repeat(Math.max(0, safeWidth - 2 - titleLen))}┐`;
|
|
273
|
+
const bottomBar = `└${horizontal}┘`;
|
|
274
|
+
|
|
275
|
+
const maxInner = Math.max(0, safeWidth - 4);
|
|
276
|
+
const renderedLines = contentLines.map((line) => {
|
|
277
|
+
const truncatedLine = truncate(line, maxInner);
|
|
278
|
+
const plain = visibleLength(truncatedLine);
|
|
279
|
+
const padding = Math.max(0, maxInner - plain);
|
|
280
|
+
return `│ ${truncatedLine}${' '.repeat(padding)} │`;
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
return [topBar, ...renderedLines, bottomBar];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
module.exports = {
|
|
287
|
+
ANSI,
|
|
288
|
+
stripAnsi,
|
|
289
|
+
visibleLength,
|
|
290
|
+
AnsiStyleTracker,
|
|
291
|
+
wrapAnsiLine,
|
|
292
|
+
truncate,
|
|
293
|
+
drawBox,
|
|
294
|
+
};
|