clawspec 1.0.19 → 1.0.21
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 +6 -0
- package/README.zh-CN.md +6 -0
- package/package.json +1 -2
- package/src/bootstrap/state.ts +128 -0
- package/src/dependencies/acpx.ts +6 -0
- package/src/dependencies/openspec.ts +5 -0
- package/src/index.ts +125 -43
- package/src/watchers/manager.ts +69 -1
- package/test/acp-client.test.ts +0 -309
- package/test/acpx-dependency.test.ts +0 -133
- package/test/assistant-journal.test.ts +0 -203
- package/test/command-surface.test.ts +0 -24
- package/test/config.test.ts +0 -77
- package/test/detach-attach.test.ts +0 -98
- package/test/doctor.test.ts +0 -142
- package/test/file-lock.test.ts +0 -88
- package/test/fs-utils.test.ts +0 -22
- package/test/helpers/harness.ts +0 -305
- package/test/helpers.test.ts +0 -108
- package/test/keywords.test.ts +0 -92
- package/test/notifier.test.ts +0 -29
- package/test/openspec-dependency.test.ts +0 -68
- package/test/paths-utils.test.ts +0 -30
- package/test/pause-cancel.test.ts +0 -55
- package/test/planning-journal.test.ts +0 -155
- package/test/plugin-registration.test.ts +0 -35
- package/test/project-memory.test.ts +0 -42
- package/test/proposal.test.ts +0 -24
- package/test/queue-planning.test.ts +0 -322
- package/test/queue-work.test.ts +0 -220
- package/test/recovery.test.ts +0 -603
- package/test/service-archive.test.ts +0 -87
- package/test/shell-command.test.ts +0 -48
- package/test/state-store.test.ts +0 -74
- package/test/tasks-and-checkpoint.test.ts +0 -60
- package/test/use-project.test.ts +0 -67
- package/test/watcher-planning.test.ts +0 -533
- package/test/watcher-work.test.ts +0 -1771
- package/test/worker-command.test.ts +0 -66
- package/test/worker-io-helper.test.ts +0 -97
- package/test/worker-skills.test.ts +0 -12
package/README.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# ClawSpec
|
|
2
2
|
|
|
3
|
+
<p align="center">
|
|
4
|
+
<a href="https://linux.do" target="_blank">
|
|
5
|
+
<img src="https://img.shields.io/badge/LINUX-DO-FFB003?style=for-the-badge&logo=linux&logoColor=white" alt="LINUX DO" />
|
|
6
|
+
</a>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
3
9
|
[Chinese (Simplified)](./README.zh-CN.md)
|
|
4
10
|
|
|
5
11
|
ClawSpec is an OpenClaw plugin that embeds an OpenSpec workflow directly into chat. It splits project control and execution on purpose:
|
package/README.zh-CN.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# ClawSpec
|
|
2
2
|
|
|
3
|
+
<p align="center">
|
|
4
|
+
<a href="https://linux.do" target="_blank">
|
|
5
|
+
<img src="https://img.shields.io/badge/LINUX-DO-FFB003?style=for-the-badge&logo=linux&logoColor=white" alt="LINUX DO" />
|
|
6
|
+
</a>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
3
9
|
[English](./README.md)
|
|
4
10
|
|
|
5
11
|
ClawSpec 是一个把 OpenSpec 工作流嵌入 OpenClaw 聊天窗口的插件。它有意把“项目控制”和“执行触发”分成两层:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawspec",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw plugin that orchestrates OpenSpec project workflows with visible main-agent execution.",
|
|
6
6
|
"keywords": [
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
"index.ts",
|
|
24
24
|
"src",
|
|
25
25
|
"skills",
|
|
26
|
-
"test",
|
|
27
26
|
"README.md",
|
|
28
27
|
"README.zh-CN.md",
|
|
29
28
|
"openclaw.plugin.json",
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
export type BootstrapDependency = "openspec" | "acpx" | "service";
|
|
2
|
+
export type BootstrapPhase = "initializing" | "checking" | "installing" | "starting" | "ready";
|
|
3
|
+
export type BootstrapStatus = "idle" | "running" | "ready" | "failed";
|
|
4
|
+
|
|
5
|
+
export type BootstrapSnapshot = {
|
|
6
|
+
status: BootstrapStatus;
|
|
7
|
+
attempt: number;
|
|
8
|
+
updatedAt: string;
|
|
9
|
+
dependency?: BootstrapDependency;
|
|
10
|
+
phase?: BootstrapPhase;
|
|
11
|
+
detail?: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type BootstrapProgress = {
|
|
16
|
+
dependency?: BootstrapDependency;
|
|
17
|
+
phase: BootstrapPhase;
|
|
18
|
+
detail: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class BootstrapCoordinator {
|
|
22
|
+
private readonly runner: (report: (progress: BootstrapProgress) => void | Promise<void>) => Promise<void>;
|
|
23
|
+
private readonly onFailure?: (error: unknown) => void;
|
|
24
|
+
private snapshot: BootstrapSnapshot = {
|
|
25
|
+
status: "idle",
|
|
26
|
+
attempt: 0,
|
|
27
|
+
updatedAt: new Date(0).toISOString(),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
private inFlight?: Promise<void>;
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
runner: (report: (progress: BootstrapProgress) => void | Promise<void>) => Promise<void>,
|
|
34
|
+
onFailure?: (error: unknown) => void,
|
|
35
|
+
) {
|
|
36
|
+
this.runner = runner;
|
|
37
|
+
this.onFailure = onFailure;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getSnapshot(): BootstrapSnapshot {
|
|
41
|
+
return { ...this.snapshot };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async start(): Promise<void> {
|
|
45
|
+
if (this.snapshot.status === "ready") {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (this.inFlight) {
|
|
49
|
+
return await this.inFlight;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const attempt = this.snapshot.attempt + 1;
|
|
53
|
+
this.snapshot = {
|
|
54
|
+
status: "running",
|
|
55
|
+
attempt,
|
|
56
|
+
updatedAt: new Date().toISOString(),
|
|
57
|
+
phase: "initializing",
|
|
58
|
+
detail: "Initializing ClawSpec bootstrap.",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const report = async (progress: BootstrapProgress) => {
|
|
62
|
+
this.snapshot = {
|
|
63
|
+
status: "running",
|
|
64
|
+
attempt,
|
|
65
|
+
updatedAt: new Date().toISOString(),
|
|
66
|
+
dependency: progress.dependency,
|
|
67
|
+
phase: progress.phase,
|
|
68
|
+
detail: progress.detail,
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
this.inFlight = (async () => {
|
|
73
|
+
try {
|
|
74
|
+
await this.runner(report);
|
|
75
|
+
this.snapshot = {
|
|
76
|
+
status: "ready",
|
|
77
|
+
attempt,
|
|
78
|
+
updatedAt: new Date().toISOString(),
|
|
79
|
+
phase: "ready",
|
|
80
|
+
detail: "ClawSpec dependencies are ready.",
|
|
81
|
+
};
|
|
82
|
+
} catch (error) {
|
|
83
|
+
this.snapshot = {
|
|
84
|
+
status: "failed",
|
|
85
|
+
attempt,
|
|
86
|
+
updatedAt: new Date().toISOString(),
|
|
87
|
+
dependency: this.snapshot.dependency,
|
|
88
|
+
phase: this.snapshot.phase,
|
|
89
|
+
detail: this.snapshot.detail,
|
|
90
|
+
error: error instanceof Error ? error.message : String(error),
|
|
91
|
+
};
|
|
92
|
+
this.onFailure?.(error);
|
|
93
|
+
} finally {
|
|
94
|
+
this.inFlight = undefined;
|
|
95
|
+
}
|
|
96
|
+
})();
|
|
97
|
+
|
|
98
|
+
return await this.inFlight;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
startInBackground(): void {
|
|
102
|
+
void this.start();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
reset(): void {
|
|
106
|
+
this.inFlight = undefined;
|
|
107
|
+
this.snapshot = {
|
|
108
|
+
status: "idle",
|
|
109
|
+
attempt: 0,
|
|
110
|
+
updatedAt: new Date().toISOString(),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function buildBootstrapPendingMessage(snapshot: BootstrapSnapshot): string {
|
|
116
|
+
const detail = snapshot.detail?.trim() || "ClawSpec is preparing required dependencies.";
|
|
117
|
+
return `${detail} Try again in a moment.`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function buildBootstrapFailureMessage(snapshot: BootstrapSnapshot): string {
|
|
121
|
+
const detail = snapshot.detail?.trim() || "ClawSpec dependency bootstrap failed.";
|
|
122
|
+
const error = snapshot.error?.trim();
|
|
123
|
+
return [
|
|
124
|
+
`${detail} Bootstrap failed.`,
|
|
125
|
+
error ? `Reason: ${error}` : undefined,
|
|
126
|
+
"Retrying dependency bootstrap in the background now. Try again in a moment.",
|
|
127
|
+
].filter(Boolean).join("\n");
|
|
128
|
+
}
|
package/src/dependencies/acpx.ts
CHANGED
|
@@ -25,6 +25,7 @@ export type EnsureAcpxCliOptions = {
|
|
|
25
25
|
runner?: CommandRunner;
|
|
26
26
|
expectedVersion?: string;
|
|
27
27
|
runtimeEntrypoint?: string;
|
|
28
|
+
onInstallStart?: (info: { packageName: string; reason: string; expectedVersion: string }) => void | Promise<void>;
|
|
28
29
|
};
|
|
29
30
|
|
|
30
31
|
export type EnsureAcpxCliResult = {
|
|
@@ -104,6 +105,11 @@ export async function ensureAcpxCli(
|
|
|
104
105
|
options.logger?.warn?.(
|
|
105
106
|
`[clawspec] acpx CLI not ready (${globalCheck.message}); installing plugin-local ${ACPX_PACKAGE_NAME}@${expectedVersion}`,
|
|
106
107
|
);
|
|
108
|
+
await options.onInstallStart?.({
|
|
109
|
+
packageName: ACPX_PACKAGE_NAME,
|
|
110
|
+
reason: globalCheck.message,
|
|
111
|
+
expectedVersion,
|
|
112
|
+
});
|
|
107
113
|
|
|
108
114
|
const install = await runner({
|
|
109
115
|
command: "npm",
|
|
@@ -22,6 +22,7 @@ export type EnsureOpenSpecCliOptions = {
|
|
|
22
22
|
logger?: PluginLogger;
|
|
23
23
|
env?: NodeJS.ProcessEnv;
|
|
24
24
|
runner?: CommandRunner;
|
|
25
|
+
onInstallStart?: (info: { packageName: string; reason: string }) => void | Promise<void>;
|
|
25
26
|
};
|
|
26
27
|
|
|
27
28
|
export type EnsureOpenSpecCliResult = {
|
|
@@ -72,6 +73,10 @@ export async function ensureOpenSpecCli(
|
|
|
72
73
|
options.logger?.warn?.(
|
|
73
74
|
`[clawspec] openspec CLI not ready (${globalCheck.message}); installing plugin-local ${OPENSPEC_PACKAGE_NAME}`,
|
|
74
75
|
);
|
|
76
|
+
await options.onInstallStart?.({
|
|
77
|
+
packageName: OPENSPEC_PACKAGE_NAME,
|
|
78
|
+
reason: globalCheck.message,
|
|
79
|
+
});
|
|
75
80
|
|
|
76
81
|
const install = await runner({
|
|
77
82
|
command: "npm",
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,11 @@ import { WatcherManager } from "./watchers/manager.ts";
|
|
|
19
19
|
import { ensureOpenSpecCli } from "./dependencies/openspec.ts";
|
|
20
20
|
import { ensureAcpxCli } from "./dependencies/acpx.ts";
|
|
21
21
|
import { getConfiguredDefaultWorkerAgent } from "./acp/openclaw-config.ts";
|
|
22
|
+
import {
|
|
23
|
+
BootstrapCoordinator,
|
|
24
|
+
buildBootstrapFailureMessage,
|
|
25
|
+
buildBootstrapPendingMessage,
|
|
26
|
+
} from "./bootstrap/state.ts";
|
|
22
27
|
|
|
23
28
|
const PLUGIN_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
24
29
|
const LOCAL_BIN_DIR = path.join(PLUGIN_ROOT, "node_modules", ".bin");
|
|
@@ -51,6 +56,106 @@ const plugin = {
|
|
|
51
56
|
});
|
|
52
57
|
let watcherManager: WatcherManager | undefined;
|
|
53
58
|
let service: ClawSpecService | undefined;
|
|
59
|
+
const bootstrap = new BootstrapCoordinator(
|
|
60
|
+
async (report) => {
|
|
61
|
+
let nextWatcherManager: WatcherManager | undefined;
|
|
62
|
+
try {
|
|
63
|
+
service = undefined;
|
|
64
|
+
watcherManager = undefined;
|
|
65
|
+
|
|
66
|
+
await report({
|
|
67
|
+
phase: "initializing",
|
|
68
|
+
detail: "ClawSpec is initializing local state.",
|
|
69
|
+
});
|
|
70
|
+
await ensureDir(pluginStateRoot);
|
|
71
|
+
await ensureDir(config.defaultWorkspace);
|
|
72
|
+
await initStores();
|
|
73
|
+
|
|
74
|
+
await report({
|
|
75
|
+
dependency: "openspec",
|
|
76
|
+
phase: "checking",
|
|
77
|
+
detail: "ClawSpec is checking the OpenSpec CLI.",
|
|
78
|
+
});
|
|
79
|
+
await ensureOpenSpecCli({
|
|
80
|
+
pluginRoot: PLUGIN_ROOT,
|
|
81
|
+
logger: api.logger,
|
|
82
|
+
onInstallStart: async ({ packageName, reason }) => {
|
|
83
|
+
await report({
|
|
84
|
+
dependency: "openspec",
|
|
85
|
+
phase: "installing",
|
|
86
|
+
detail: `ClawSpec is installing ${packageName} because OpenSpec is unavailable (${reason}).`,
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
await report({
|
|
92
|
+
dependency: "acpx",
|
|
93
|
+
phase: "checking",
|
|
94
|
+
detail: "ClawSpec is checking the ACPX CLI.",
|
|
95
|
+
});
|
|
96
|
+
const acpx = await ensureAcpxCli({
|
|
97
|
+
pluginRoot: PLUGIN_ROOT,
|
|
98
|
+
logger: api.logger,
|
|
99
|
+
onInstallStart: async ({ packageName, reason, expectedVersion }) => {
|
|
100
|
+
await report({
|
|
101
|
+
dependency: "acpx",
|
|
102
|
+
phase: "installing",
|
|
103
|
+
detail: `ClawSpec is installing ${packageName}@${expectedVersion} because no compatible ACPX CLI is available (${reason}).`,
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await report({
|
|
109
|
+
dependency: "service",
|
|
110
|
+
phase: "starting",
|
|
111
|
+
detail: "ClawSpec dependencies are ready. Starting services.",
|
|
112
|
+
});
|
|
113
|
+
const configuredDefaultWorkerAgent = getConfiguredDefaultWorkerAgent(api.config) ?? "codex";
|
|
114
|
+
const acpClient = new AcpWorkerClient({
|
|
115
|
+
agentId: configuredDefaultWorkerAgent,
|
|
116
|
+
logger: api.logger,
|
|
117
|
+
command: acpx.command,
|
|
118
|
+
env: acpx.env,
|
|
119
|
+
});
|
|
120
|
+
nextWatcherManager = new WatcherManager({
|
|
121
|
+
stateStore,
|
|
122
|
+
openSpec,
|
|
123
|
+
archiveDirName: config.archiveDirName,
|
|
124
|
+
logger: api.logger,
|
|
125
|
+
notifier,
|
|
126
|
+
acpClient,
|
|
127
|
+
pollIntervalMs: config.watcherPollIntervalMs,
|
|
128
|
+
});
|
|
129
|
+
const nextService = new ClawSpecService({
|
|
130
|
+
api,
|
|
131
|
+
config: api.config,
|
|
132
|
+
logger: api.logger,
|
|
133
|
+
stateStore,
|
|
134
|
+
memoryStore,
|
|
135
|
+
openSpec,
|
|
136
|
+
archiveDirName: config.archiveDirName,
|
|
137
|
+
allowedChannels: config.allowedChannels,
|
|
138
|
+
defaultWorkspace: config.defaultWorkspace,
|
|
139
|
+
defaultWorkerAgentId: undefined,
|
|
140
|
+
workspaceStore,
|
|
141
|
+
watcherManager: nextWatcherManager,
|
|
142
|
+
});
|
|
143
|
+
await nextWatcherManager.start();
|
|
144
|
+
watcherManager = nextWatcherManager;
|
|
145
|
+
service = nextService;
|
|
146
|
+
} catch (error) {
|
|
147
|
+
service = undefined;
|
|
148
|
+
await nextWatcherManager?.stop();
|
|
149
|
+
watcherManager = undefined;
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
(error) => {
|
|
154
|
+
api.logger.error?.(
|
|
155
|
+
`[clawspec] bootstrap failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
156
|
+
);
|
|
157
|
+
},
|
|
158
|
+
);
|
|
54
159
|
|
|
55
160
|
const initStores = () => Promise.all([
|
|
56
161
|
stateStore.initialize(),
|
|
@@ -61,51 +166,13 @@ const plugin = {
|
|
|
61
166
|
api.registerService({
|
|
62
167
|
id: "clawspec.bootstrap",
|
|
63
168
|
async start() {
|
|
64
|
-
await
|
|
65
|
-
await ensureDir(config.defaultWorkspace);
|
|
66
|
-
await initStores();
|
|
67
|
-
await ensureOpenSpecCli({
|
|
68
|
-
pluginRoot: PLUGIN_ROOT,
|
|
69
|
-
logger: api.logger,
|
|
70
|
-
});
|
|
71
|
-
const acpx = await ensureAcpxCli({
|
|
72
|
-
pluginRoot: PLUGIN_ROOT,
|
|
73
|
-
logger: api.logger,
|
|
74
|
-
});
|
|
75
|
-
const configuredDefaultWorkerAgent = getConfiguredDefaultWorkerAgent(api.config) ?? "codex";
|
|
76
|
-
const acpClient = new AcpWorkerClient({
|
|
77
|
-
agentId: configuredDefaultWorkerAgent,
|
|
78
|
-
logger: api.logger,
|
|
79
|
-
command: acpx.command,
|
|
80
|
-
env: acpx.env,
|
|
81
|
-
});
|
|
82
|
-
watcherManager = new WatcherManager({
|
|
83
|
-
stateStore,
|
|
84
|
-
openSpec,
|
|
85
|
-
archiveDirName: config.archiveDirName,
|
|
86
|
-
logger: api.logger,
|
|
87
|
-
notifier,
|
|
88
|
-
acpClient,
|
|
89
|
-
pollIntervalMs: config.watcherPollIntervalMs,
|
|
90
|
-
});
|
|
91
|
-
service = new ClawSpecService({
|
|
92
|
-
api,
|
|
93
|
-
config: api.config,
|
|
94
|
-
logger: api.logger,
|
|
95
|
-
stateStore,
|
|
96
|
-
memoryStore,
|
|
97
|
-
openSpec,
|
|
98
|
-
archiveDirName: config.archiveDirName,
|
|
99
|
-
allowedChannels: config.allowedChannels,
|
|
100
|
-
defaultWorkspace: config.defaultWorkspace,
|
|
101
|
-
defaultWorkerAgentId: undefined,
|
|
102
|
-
workspaceStore,
|
|
103
|
-
watcherManager,
|
|
104
|
-
});
|
|
105
|
-
await watcherManager.start();
|
|
169
|
+
await bootstrap.start();
|
|
106
170
|
},
|
|
107
171
|
async stop() {
|
|
108
172
|
await watcherManager?.stop();
|
|
173
|
+
watcherManager = undefined;
|
|
174
|
+
service = undefined;
|
|
175
|
+
bootstrap.reset();
|
|
109
176
|
},
|
|
110
177
|
});
|
|
111
178
|
|
|
@@ -137,9 +204,24 @@ const plugin = {
|
|
|
137
204
|
handler: async (ctx) => {
|
|
138
205
|
await initStores();
|
|
139
206
|
if (!service) {
|
|
207
|
+
const snapshot = bootstrap.getSnapshot();
|
|
208
|
+
if (snapshot.status === "failed") {
|
|
209
|
+
bootstrap.startInBackground();
|
|
210
|
+
return {
|
|
211
|
+
ok: false,
|
|
212
|
+
text: buildBootstrapFailureMessage(snapshot),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
if (snapshot.status === "idle") {
|
|
216
|
+
bootstrap.startInBackground();
|
|
217
|
+
return {
|
|
218
|
+
ok: false,
|
|
219
|
+
text: buildBootstrapPendingMessage(bootstrap.getSnapshot()),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
140
222
|
return {
|
|
141
223
|
ok: false,
|
|
142
|
-
text:
|
|
224
|
+
text: buildBootstrapPendingMessage(snapshot),
|
|
143
225
|
};
|
|
144
226
|
}
|
|
145
227
|
const subcommand = parseSubcommand(ctx.args);
|
package/src/watchers/manager.ts
CHANGED
|
@@ -2795,7 +2795,7 @@ function parseWorkerProgressEvent(line: string): WorkerProgressEvent | undefined
|
|
|
2795
2795
|
|
|
2796
2796
|
function formatWorkerProgressMessage(project: ProjectState, event: WorkerProgressEvent): string | undefined {
|
|
2797
2797
|
const rawMessage = typeof event.message === "string" ? event.message : "";
|
|
2798
|
-
const message = shortenActivityText(rawMessage, 120);
|
|
2798
|
+
const message = shortenActivityText(compactWorkerProgressDisplayPaths(project, rawMessage), 120);
|
|
2799
2799
|
if (!message) {
|
|
2800
2800
|
return undefined;
|
|
2801
2801
|
}
|
|
@@ -2916,6 +2916,74 @@ function compactProjectLabel(project: ProjectState): string {
|
|
|
2916
2916
|
return `${projectName}-${changeName}`;
|
|
2917
2917
|
}
|
|
2918
2918
|
|
|
2919
|
+
function compactWorkerProgressDisplayPaths(project: ProjectState, text: string): string {
|
|
2920
|
+
try {
|
|
2921
|
+
const compactRoot = compactWorkerProgressRoot(project);
|
|
2922
|
+
if (!compactRoot) {
|
|
2923
|
+
return text;
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
let compacted = text;
|
|
2927
|
+
if (project.changeDir) {
|
|
2928
|
+
compacted = replaceDisplayPathPrefix(compacted, project.changeDir, `${compactRoot}:`);
|
|
2929
|
+
}
|
|
2930
|
+
if (project.repoPath) {
|
|
2931
|
+
compacted = replaceDisplayPathPrefix(compacted, project.repoPath, `${compactRoot}:`);
|
|
2932
|
+
}
|
|
2933
|
+
compacted = normalizeCompactedDisplayPaths(compacted, compactRoot);
|
|
2934
|
+
return compacted.length < text.length ? compacted : text;
|
|
2935
|
+
} catch {
|
|
2936
|
+
return text;
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
function compactWorkerProgressRoot(project: ProjectState): string | undefined {
|
|
2941
|
+
const changeName = project.changeName?.trim();
|
|
2942
|
+
if (!changeName) {
|
|
2943
|
+
return undefined;
|
|
2944
|
+
}
|
|
2945
|
+
const projectName = project.projectName?.trim()
|
|
2946
|
+
|| (project.repoPath ? path.basename(project.repoPath) : undefined)
|
|
2947
|
+
|| "project";
|
|
2948
|
+
return `${projectName}@${changeName}`;
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
function replaceDisplayPathPrefix(text: string, targetPath: string, replacement: string): string {
|
|
2952
|
+
const pattern = buildDisplayPathPrefixPattern(targetPath);
|
|
2953
|
+
if (!pattern) {
|
|
2954
|
+
return text;
|
|
2955
|
+
}
|
|
2956
|
+
return text.replace(pattern, replacement);
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
function normalizeCompactedDisplayPaths(text: string, compactRoot: string): string {
|
|
2960
|
+
const prefix = escapeRegExp(`${compactRoot}:`);
|
|
2961
|
+
const pattern = new RegExp("(" + prefix + ")([^\\s\"'`,)\\]}]+)", "g");
|
|
2962
|
+
return text.replace(pattern, (_match, prefix: string, suffix: string) => `${prefix}${suffix.replace(/\\/g, "/")}`);
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
function buildDisplayPathPrefixPattern(targetPath: string): RegExp | undefined {
|
|
2966
|
+
const normalized = normalizeSlashes(targetPath).replace(/\/+$/, "");
|
|
2967
|
+
if (!normalized) {
|
|
2968
|
+
return undefined;
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
const escaped = normalized
|
|
2972
|
+
.split("/")
|
|
2973
|
+
.map((segment) => escapeRegExp(segment))
|
|
2974
|
+
.join("[/\\\\]+");
|
|
2975
|
+
if (!escaped) {
|
|
2976
|
+
return undefined;
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
const flags = /^[A-Za-z]:/.test(normalized) ? "gi" : "g";
|
|
2980
|
+
return new RegExp(`${escaped}(?:[/\\\\]+)?`, flags);
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
function escapeRegExp(value: string): string {
|
|
2984
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2919
2987
|
function compactProgressMarker(current?: number, total?: number): string {
|
|
2920
2988
|
if (!total || total <= 0 || !current || current <= 0) {
|
|
2921
2989
|
return "";
|