@keyou007/agent-bot 0.1.15-alpha.4 → 0.1.15-alpha.6
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/CHANGELOG.md +12 -0
- package/README.md +7 -2
- package/README.zh.md +7 -2
- package/config.example.yaml +6 -0
- package/dist/commands/CommandRouter.js +7 -0
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/config/loadConfig.js +4 -0
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/config/schema.js +31 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/feishu/CardRenderer.js +76 -9
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FinalResponseBranding.js +1 -1
- package/dist/feishu/FinalResponseBranding.js.map +1 -1
- package/dist/feishu/GroupNameFormatter.js +193 -0
- package/dist/feishu/GroupNameFormatter.js.map +1 -0
- package/dist/proxy/ProxySessionController.js +197 -148
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/shell/ShellCommandJobManager.js +264 -0
- package/dist/shell/ShellCommandJobManager.js.map +1 -0
- package/dist/shell/shellCommandRunner.js +157 -0
- package/dist/shell/shellCommandRunner.js.map +1 -0
- package/dist/utils/executeShellCommand.js +1 -1
- package/dist/utils/executeShellCommand.js.map +1 -1
- package/docs/technical-reference.md +19 -0
- package/docs/technical-reference.zh.md +19 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/commands/CommandRouter.ts +6 -0
- package/src/commands/commandTypes.ts +1 -0
- package/src/config/loadConfig.ts +4 -0
- package/src/config/schema.ts +33 -0
- package/src/feishu/CardRenderer.ts +91 -17
- package/src/feishu/FinalResponseBranding.ts +2 -1
- package/src/feishu/GroupNameFormatter.ts +222 -0
- package/src/proxy/ProxySessionController.ts +238 -160
- package/src/shell/ShellCommandJobManager.ts +341 -0
- package/src/shell/shellCommandRunner.ts +163 -0
- package/src/utils/executeShellCommand.ts +1 -1
package/src/config/schema.ts
CHANGED
|
@@ -3,6 +3,37 @@ import { DEFAULT_LOG_PATH, DEFAULT_SQLITE_PATH } from "./paths.js";
|
|
|
3
3
|
|
|
4
4
|
const envRecordSchema = z.record(z.string(), z.string()).default({});
|
|
5
5
|
|
|
6
|
+
export const DEFAULT_GROUP_NAME_FORMAT = {
|
|
7
|
+
project: "[{agent}] [{project}] {taskname}",
|
|
8
|
+
projectless: "[{agent}] {taskname}",
|
|
9
|
+
dateFormat: "MM-dd",
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
const groupNameTemplateSchema = z.string().min(1).max(240).superRefine((template, context) => {
|
|
13
|
+
const tokens = [...template.matchAll(/\{([^{}]+)\}/gu)].map((match) => match[1]!);
|
|
14
|
+
const taskNameCount = tokens.filter((token) => token === "taskname").length;
|
|
15
|
+
if (taskNameCount !== 1) {
|
|
16
|
+
context.addIssue({
|
|
17
|
+
code: "custom",
|
|
18
|
+
message: "A group name format must contain exactly one {taskname} placeholder.",
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
for (const token of tokens) {
|
|
22
|
+
if (["os", "agent", "project", "taskname", "date"].includes(token)) continue;
|
|
23
|
+
if (token.startsWith("date:") && token.slice("date:".length).trim()) continue;
|
|
24
|
+
context.addIssue({
|
|
25
|
+
code: "custom",
|
|
26
|
+
message: `Unsupported group name placeholder: {${token}}.`,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const groupNameFormatSchema = z.object({
|
|
32
|
+
project: groupNameTemplateSchema.default(DEFAULT_GROUP_NAME_FORMAT.project),
|
|
33
|
+
projectless: groupNameTemplateSchema.default(DEFAULT_GROUP_NAME_FORMAT.projectless),
|
|
34
|
+
dateFormat: z.string().min(1).max(80).default(DEFAULT_GROUP_NAME_FORMAT.dateFormat),
|
|
35
|
+
}).default(DEFAULT_GROUP_NAME_FORMAT);
|
|
36
|
+
|
|
6
37
|
const agentKindSchema = z.union([
|
|
7
38
|
z.literal("acp"),
|
|
8
39
|
z.literal("app-server"),
|
|
@@ -30,6 +61,7 @@ export const appConfigSchema = z.object({
|
|
|
30
61
|
userOpenId: z.string().optional(),
|
|
31
62
|
respondToOwnerOnly: z.boolean().default(true),
|
|
32
63
|
respondToAllGroupMessages: z.boolean().default(true),
|
|
64
|
+
groupNameFormat: groupNameFormatSchema,
|
|
33
65
|
thinkingCardLayout: z.enum(["grouped", "timeline"]).default("grouped"),
|
|
34
66
|
useConsoleWhenMissingCredentials: z.boolean().default(false),
|
|
35
67
|
}),
|
|
@@ -53,4 +85,5 @@ export const appConfigSchema = z.object({
|
|
|
53
85
|
});
|
|
54
86
|
|
|
55
87
|
export type AgentConfig = z.infer<typeof agentConfigSchema>;
|
|
88
|
+
export type GroupNameFormatConfig = z.infer<typeof groupNameFormatSchema>;
|
|
56
89
|
export type AppConfig = z.infer<typeof appConfigSchema>;
|
|
@@ -240,11 +240,12 @@ export interface DismissGroupCardView {
|
|
|
240
240
|
}
|
|
241
241
|
|
|
242
242
|
export interface ShellCommandCardView {
|
|
243
|
+
jobId?: string;
|
|
244
|
+
contextKey?: string;
|
|
243
245
|
command: string;
|
|
244
246
|
cwd: string;
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
status: "running" | "completed" | "failed" | "timed_out";
|
|
247
|
+
output: string;
|
|
248
|
+
status: "running" | "cancelling" | "completed" | "failed" | "cancelled" | "timed_out";
|
|
248
249
|
exitCode?: number | null;
|
|
249
250
|
elapsedMs: number;
|
|
250
251
|
outputTruncated: boolean;
|
|
@@ -258,12 +259,7 @@ export class CardRenderer {
|
|
|
258
259
|
}
|
|
259
260
|
|
|
260
261
|
renderShellCommandCard(view: ShellCommandCardView): Record<string, unknown> {
|
|
261
|
-
const
|
|
262
|
-
const stderr = normalizeTerminalOutput(view.stderr).trimEnd();
|
|
263
|
-
const rawOutput = [
|
|
264
|
-
stdout,
|
|
265
|
-
stderr ? `[stderr]\n${stderr}` : "",
|
|
266
|
-
].filter(Boolean).join("\n");
|
|
262
|
+
const rawOutput = normalizeTerminalOutput(view.output).trimEnd();
|
|
267
263
|
const displayOutput = truncateMiddle(
|
|
268
264
|
rawOutput,
|
|
269
265
|
6_000,
|
|
@@ -273,15 +269,32 @@ export class CardRenderer {
|
|
|
273
269
|
const output = displayOutput || (view.status === "running" ? "等待输出..." : "(无输出)");
|
|
274
270
|
const status = shellCommandStatus(view);
|
|
275
271
|
const outputWasTruncated = view.outputTruncated || rawOutput.length > 6_000;
|
|
272
|
+
const elements = [
|
|
273
|
+
markdown(codeBlock(`$ ${command}\n${output}`, 6_700)),
|
|
274
|
+
markdown([
|
|
275
|
+
`${inlineCode(view.cwd)} · ${status.label} · ${formatTurnDuration(view.elapsedMs)}`,
|
|
276
|
+
...(outputWasTruncated ? ["输出过长,已保留开头和结尾并截断中间内容。"] : []),
|
|
277
|
+
].join("\n")),
|
|
278
|
+
];
|
|
279
|
+
if (view.status === "running" && view.jobId && view.contextKey) {
|
|
280
|
+
elements.push({
|
|
281
|
+
tag: "button",
|
|
282
|
+
text: { tag: "plain_text", content: "Cancel" },
|
|
283
|
+
type: "danger",
|
|
284
|
+
size: "small",
|
|
285
|
+
behaviors: [{
|
|
286
|
+
type: "callback",
|
|
287
|
+
value: {
|
|
288
|
+
action: "shell_command_cancel",
|
|
289
|
+
jobId: view.jobId,
|
|
290
|
+
contextKey: view.contextKey,
|
|
291
|
+
},
|
|
292
|
+
}],
|
|
293
|
+
});
|
|
294
|
+
}
|
|
276
295
|
return sectionCard(
|
|
277
296
|
status.title,
|
|
278
|
-
|
|
279
|
-
markdown(codeBlock(`$ ${command}\n${output}`, 6_700)),
|
|
280
|
-
markdown([
|
|
281
|
-
`${inlineCode(view.cwd)} · ${status.label} · ${formatTurnDuration(view.elapsedMs)}`,
|
|
282
|
-
...(outputWasTruncated ? ["输出过长,已保留开头和结尾并截断中间内容。"] : []),
|
|
283
|
-
].join("\n")),
|
|
284
|
-
],
|
|
297
|
+
elements,
|
|
285
298
|
status.template,
|
|
286
299
|
);
|
|
287
300
|
}
|
|
@@ -2052,11 +2065,66 @@ function renderToolDetails(tool: ToolState, projectCwd?: string): string {
|
|
|
2052
2065
|
const displayCommand = tool.kind === "command"
|
|
2053
2066
|
? unwrapShellCommand(normalizedCommand) ?? normalizedCommand
|
|
2054
2067
|
: normalizedCommand;
|
|
2055
|
-
const commandText = truncateText(
|
|
2068
|
+
const commandText = truncateText(
|
|
2069
|
+
tool.kind === "command" ? formatShellCommandForDisplay(displayCommand) : displayCommand,
|
|
2070
|
+
600,
|
|
2071
|
+
);
|
|
2056
2072
|
const resultText = result ? truncateToolResult(normalizeTerminalOutput(result).trim()) : undefined;
|
|
2057
2073
|
return codeBlock([`$ ${commandText}`, resultText].filter((part): part is string => part !== undefined).join("\n"), 2_003);
|
|
2058
2074
|
}
|
|
2059
2075
|
|
|
2076
|
+
function formatShellCommandForDisplay(command: string): string {
|
|
2077
|
+
const normalized = command.replace(/\r\n/g, "\n");
|
|
2078
|
+
let result = "";
|
|
2079
|
+
let quote: "'" | "\"" | undefined;
|
|
2080
|
+
|
|
2081
|
+
for (let index = 0; index < normalized.length; index += 1) {
|
|
2082
|
+
const character = normalized[index] ?? "";
|
|
2083
|
+
|
|
2084
|
+
if (quote) {
|
|
2085
|
+
result += character;
|
|
2086
|
+
if ((character === "\\" || character === "`") && quote === "\"") {
|
|
2087
|
+
if (index + 1 < normalized.length) result += normalized[++index];
|
|
2088
|
+
} else if (character === quote) {
|
|
2089
|
+
quote = undefined;
|
|
2090
|
+
}
|
|
2091
|
+
continue;
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
if (character === "'" || character === "\"") {
|
|
2095
|
+
quote = character;
|
|
2096
|
+
result += character;
|
|
2097
|
+
continue;
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
if (character === "\\" || character === "`" || character === "^") {
|
|
2101
|
+
result += character;
|
|
2102
|
+
if (index + 1 < normalized.length) result += normalized[++index];
|
|
2103
|
+
continue;
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
const operator = readShellDisplayOperator(normalized, index);
|
|
2107
|
+
if (!operator) {
|
|
2108
|
+
result += character;
|
|
2109
|
+
continue;
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
result += operator;
|
|
2113
|
+
index += operator.length - 1;
|
|
2114
|
+
while (index + 1 < normalized.length && /\s/.test(normalized[index + 1] ?? "")) index += 1;
|
|
2115
|
+
if (index + 1 < normalized.length) result += " \\\n ";
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
return result;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
function readShellDisplayOperator(command: string, index: number): string | undefined {
|
|
2122
|
+
for (const operator of ["&&", "||", "|&", ";", "|"]) {
|
|
2123
|
+
if (command.startsWith(operator, index)) return operator;
|
|
2124
|
+
}
|
|
2125
|
+
return undefined;
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2060
2128
|
function normalizeTerminalOutput(value: string): string {
|
|
2061
2129
|
const clean = stripAnsi(value).replace(/\r\n/g, "\n");
|
|
2062
2130
|
return clean
|
|
@@ -2167,6 +2235,12 @@ function shellCommandStatus(view: ShellCommandCardView): {
|
|
|
2167
2235
|
if (view.status === "running") {
|
|
2168
2236
|
return { title: "正在执行命令", label: "运行中", template: "blue" };
|
|
2169
2237
|
}
|
|
2238
|
+
if (view.status === "cancelling") {
|
|
2239
|
+
return { title: "正在取消命令", label: "正在取消", template: "orange" };
|
|
2240
|
+
}
|
|
2241
|
+
if (view.status === "cancelled") {
|
|
2242
|
+
return { title: "命令已取消", label: "已取消", template: "grey" };
|
|
2243
|
+
}
|
|
2170
2244
|
if (view.status === "timed_out") {
|
|
2171
2245
|
return { title: "命令执行超时", label: "已超时(120s)", template: "orange" };
|
|
2172
2246
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export const FINAL_RESPONSE_BRANDING =
|
|
1
|
+
export const FINAL_RESPONSE_BRANDING =
|
|
2
|
+
"> Powered by [AgentBot](https://keyou.github.io/agent-bot/)";
|
|
2
3
|
export const FINAL_RESPONSE_BRANDING_BLOCK = `----\n\n${FINAL_RESPONSE_BRANDING}`;
|
|
3
4
|
|
|
4
5
|
export function splitFinalResponseBranding(markdown: string): {
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_GROUP_NAME_FORMAT,
|
|
5
|
+
type GroupNameFormatConfig,
|
|
6
|
+
} from "../config/schema.js";
|
|
7
|
+
|
|
8
|
+
export const FEISHU_GROUP_NAME_MAX_LENGTH = 60;
|
|
9
|
+
const GROUP_PROJECT_NAME_MAX_LENGTH = 15;
|
|
10
|
+
const PLACEHOLDER_PATTERN = /\{([^{}]+)\}/gu;
|
|
11
|
+
const TASK_NAME_PLACEHOLDER = "{taskname}";
|
|
12
|
+
|
|
13
|
+
export interface GroupNameFormatInput {
|
|
14
|
+
agentName: string;
|
|
15
|
+
projectCwd?: string;
|
|
16
|
+
taskName: string;
|
|
17
|
+
date?: Date;
|
|
18
|
+
platform?: NodeJS.Platform;
|
|
19
|
+
format?: GroupNameFormatConfig;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface GroupNameParseInput {
|
|
23
|
+
agentName: string;
|
|
24
|
+
groupName: string;
|
|
25
|
+
projectCwd?: string;
|
|
26
|
+
platform?: NodeJS.Platform;
|
|
27
|
+
format?: GroupNameFormatConfig;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function formatNewGroupName(input: GroupNameFormatInput): string {
|
|
31
|
+
const format = input.format ?? DEFAULT_GROUP_NAME_FORMAT;
|
|
32
|
+
const template = input.projectCwd ? format.project : format.projectless;
|
|
33
|
+
const taskNameIndex = template.indexOf(TASK_NAME_PLACEHOLDER);
|
|
34
|
+
const values = templateValues(input, format);
|
|
35
|
+
const prefix = renderTemplate(template.slice(0, taskNameIndex), values);
|
|
36
|
+
const suffix = renderTemplate(template.slice(taskNameIndex + TASK_NAME_PLACEHOLDER.length), values);
|
|
37
|
+
const availableTaskNameLength = FEISHU_GROUP_NAME_MAX_LENGTH
|
|
38
|
+
- Array.from(prefix).length
|
|
39
|
+
- Array.from(suffix).length;
|
|
40
|
+
const taskName = truncateTail(input.taskName, availableTaskNameLength);
|
|
41
|
+
return truncateTail(`${prefix}${taskName}${suffix}`.trim(), FEISHU_GROUP_NAME_MAX_LENGTH);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function parseTaskNameFromGroupName(input: GroupNameParseInput): string | undefined {
|
|
45
|
+
const format = input.format ?? DEFAULT_GROUP_NAME_FORMAT;
|
|
46
|
+
const template = input.projectCwd ? format.project : format.projectless;
|
|
47
|
+
const values = templateValues(input, format);
|
|
48
|
+
let pattern = "^";
|
|
49
|
+
let cursor = 0;
|
|
50
|
+
let match: RegExpExecArray | null;
|
|
51
|
+
PLACEHOLDER_PATTERN.lastIndex = 0;
|
|
52
|
+
while ((match = PLACEHOLDER_PATTERN.exec(template)) !== null) {
|
|
53
|
+
pattern += escapeRegExp(template.slice(cursor, match.index));
|
|
54
|
+
const token = match[1]!;
|
|
55
|
+
if (token === "taskname") pattern += "(?<taskname>.+?)";
|
|
56
|
+
else if (token === "date" || token.startsWith("date:")) {
|
|
57
|
+
const dateFormat = token === "date" ? format.dateFormat : token.slice("date:".length);
|
|
58
|
+
pattern += dateFormatPattern(dateFormat);
|
|
59
|
+
} else {
|
|
60
|
+
pattern += escapeRegExp(placeholderValue(token, values));
|
|
61
|
+
}
|
|
62
|
+
cursor = match.index + match[0].length;
|
|
63
|
+
}
|
|
64
|
+
pattern += `${escapeRegExp(template.slice(cursor))}$`;
|
|
65
|
+
const parsed = new RegExp(pattern, "iu").exec(input.groupName.trim())?.groups?.taskname?.trim();
|
|
66
|
+
return parsed || undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function formatGroupNameDate(date: Date, format: string): string {
|
|
70
|
+
const year = date.getFullYear();
|
|
71
|
+
const month = date.getMonth() + 1;
|
|
72
|
+
const day = date.getDate();
|
|
73
|
+
const hour = date.getHours();
|
|
74
|
+
const minute = date.getMinutes();
|
|
75
|
+
const second = date.getSeconds();
|
|
76
|
+
const values = new Map<string, string>([
|
|
77
|
+
["yyyy", String(year).padStart(4, "0")],
|
|
78
|
+
["YYYY", String(year).padStart(4, "0")],
|
|
79
|
+
["yy", String(year % 100).padStart(2, "0")],
|
|
80
|
+
["YY", String(year % 100).padStart(2, "0")],
|
|
81
|
+
["MM", String(month).padStart(2, "0")],
|
|
82
|
+
["M", String(month)],
|
|
83
|
+
["dd", String(day).padStart(2, "0")],
|
|
84
|
+
["DD", String(day).padStart(2, "0")],
|
|
85
|
+
["d", String(day)],
|
|
86
|
+
["D", String(day)],
|
|
87
|
+
["HH", String(hour).padStart(2, "0")],
|
|
88
|
+
["H", String(hour)],
|
|
89
|
+
["mm", String(minute).padStart(2, "0")],
|
|
90
|
+
["m", String(minute)],
|
|
91
|
+
["ss", String(second).padStart(2, "0")],
|
|
92
|
+
["s", String(second)],
|
|
93
|
+
]);
|
|
94
|
+
return replaceDateTokens(format, (token) => values.get(token)!);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function groupNameOs(platform: NodeJS.Platform = process.platform): string {
|
|
98
|
+
if (platform === "win32") return "win";
|
|
99
|
+
if (platform === "darwin") return "mac";
|
|
100
|
+
if (platform === "linux") return "linux";
|
|
101
|
+
return platform;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function formatGroupProjectName(projectCwd: string): string {
|
|
105
|
+
const value = abbreviateHomeDirectory(projectCwd);
|
|
106
|
+
const levels = value
|
|
107
|
+
.replace(/[\\/]+$/, "")
|
|
108
|
+
.split(/[\\/]+/)
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
.slice(-2);
|
|
111
|
+
const separator = value.includes("\\") ? path.win32.sep : path.posix.sep;
|
|
112
|
+
return fitTrailingPathLevels(levels, GROUP_PROJECT_NAME_MAX_LENGTH, separator);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface TemplateValues {
|
|
116
|
+
agent: string;
|
|
117
|
+
project: string;
|
|
118
|
+
os: string;
|
|
119
|
+
date: Date;
|
|
120
|
+
dateFormat: string;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function templateValues(
|
|
124
|
+
input: Pick<GroupNameFormatInput, "agentName" | "projectCwd" | "date" | "platform">,
|
|
125
|
+
format: GroupNameFormatConfig,
|
|
126
|
+
): TemplateValues {
|
|
127
|
+
return {
|
|
128
|
+
agent: input.agentName,
|
|
129
|
+
project: input.projectCwd ? formatGroupProjectName(input.projectCwd) : "",
|
|
130
|
+
os: groupNameOs(input.platform),
|
|
131
|
+
date: input.date ?? new Date(),
|
|
132
|
+
dateFormat: format.dateFormat,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function renderTemplate(template: string, values: TemplateValues): string {
|
|
137
|
+
PLACEHOLDER_PATTERN.lastIndex = 0;
|
|
138
|
+
return template.replace(PLACEHOLDER_PATTERN, (_placeholder, token: string) => {
|
|
139
|
+
if (token === "date") return formatGroupNameDate(values.date, values.dateFormat);
|
|
140
|
+
if (token.startsWith("date:")) {
|
|
141
|
+
return formatGroupNameDate(values.date, token.slice("date:".length));
|
|
142
|
+
}
|
|
143
|
+
return placeholderValue(token, values);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function placeholderValue(token: string, values: TemplateValues): string {
|
|
148
|
+
if (token === "agent") return values.agent;
|
|
149
|
+
if (token === "project") return values.project;
|
|
150
|
+
if (token === "os") return values.os;
|
|
151
|
+
return "";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function dateFormatPattern(format: string): string {
|
|
155
|
+
return replaceDateTokens(format, (token) => {
|
|
156
|
+
if (["yyyy", "YYYY"].includes(token)) return "\\d{4}";
|
|
157
|
+
if (["yy", "YY", "MM", "dd", "DD", "HH", "mm", "ss"].includes(token)) return "\\d{2}";
|
|
158
|
+
return "\\d{1,2}";
|
|
159
|
+
}, escapeRegExp);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const DATE_TOKENS = [
|
|
163
|
+
"yyyy", "YYYY", "yy", "YY", "MM", "dd", "DD", "HH", "mm", "ss", "M", "d", "D", "H", "m", "s",
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
function replaceDateTokens(
|
|
167
|
+
format: string,
|
|
168
|
+
replaceToken: (token: string) => string,
|
|
169
|
+
replaceLiteral: (literal: string) => string = (literal) => literal,
|
|
170
|
+
): string {
|
|
171
|
+
let result = "";
|
|
172
|
+
for (let index = 0; index < format.length;) {
|
|
173
|
+
const token = DATE_TOKENS.find((candidate) => format.startsWith(candidate, index));
|
|
174
|
+
if (token) {
|
|
175
|
+
result += replaceToken(token);
|
|
176
|
+
index += token.length;
|
|
177
|
+
} else {
|
|
178
|
+
result += replaceLiteral(format[index]!);
|
|
179
|
+
index += 1;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function abbreviateHomeDirectory(value: string): string {
|
|
186
|
+
const homeDirectory = os.homedir();
|
|
187
|
+
const valueIsWindows = isWindowsAbsolutePath(value);
|
|
188
|
+
const homeIsWindows = isWindowsAbsolutePath(homeDirectory);
|
|
189
|
+
if (valueIsWindows !== homeIsWindows && (valueIsWindows || path.posix.isAbsolute(value))) return value;
|
|
190
|
+
const pathApi = valueIsWindows ? path.win32 : path;
|
|
191
|
+
const relative = pathApi.relative(homeDirectory, value);
|
|
192
|
+
if (relative === "") return "~";
|
|
193
|
+
if (pathApi.isAbsolute(relative) || relative === ".." || relative.startsWith(`..${pathApi.sep}`)) return value;
|
|
194
|
+
return `~${pathApi.sep}${relative}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function isWindowsAbsolutePath(value: string): boolean {
|
|
198
|
+
return /^[a-z]:[\\/]|^\\\\/iu.test(value);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function fitTrailingPathLevels(levels: string[], maxLength: number, separator: string): string {
|
|
202
|
+
if (levels.length === 0) return "";
|
|
203
|
+
if (levels.length === 1) return truncateTail(levels[0]!, maxLength);
|
|
204
|
+
const parent = levels[0]!;
|
|
205
|
+
const leaf = levels[1]!;
|
|
206
|
+
const joined = `${parent}${separator}${leaf}`;
|
|
207
|
+
if (Array.from(joined).length <= maxLength) return joined;
|
|
208
|
+
if (Array.from(leaf).length <= maxLength) return leaf;
|
|
209
|
+
return truncateTail(leaf, maxLength);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function truncateTail(value: string, maxLength: number): string {
|
|
213
|
+
const characters = Array.from(value);
|
|
214
|
+
if (characters.length <= maxLength) return value;
|
|
215
|
+
if (maxLength <= 0) return "";
|
|
216
|
+
if (maxLength <= 3) return ".".repeat(maxLength);
|
|
217
|
+
return `${characters.slice(0, maxLength - 3).join("")}...`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function escapeRegExp(value: string): string {
|
|
221
|
+
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
222
|
+
}
|