cloverflow-mcp-server 0.0.8
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 +58 -0
- package/bin/cloverflow-mcp-server.js +259 -0
- package/package.json +19 -0
- package/vendor/mcp_server.exe +0 -0
- package/vendor/service-data/workflows/baseline-workflow/00-preflight.sh +13 -0
- package/vendor/service-data/workflows/baseline-workflow/01-start-feature.sh +18 -0
- package/vendor/service-data/workflows/baseline-workflow/02-select-minimal-patch.sh +9 -0
- package/vendor/service-data/workflows/baseline-workflow/03-patch-sink.sh +26 -0
- package/vendor/service-data/workflows/baseline-workflow/04-broadcast-shared-base.sh +44 -0
- package/vendor/service-data/workflows/baseline-workflow/04-patch-broadcast.sh +24 -0
- package/vendor/service-data/workflows/baseline-workflow/05-squash-feature.sh +18 -0
- package/vendor/service-data/workflows/baseline-workflow/06-feature-retract.sh +28 -0
- package/vendor/service-data/workflows/baseline-workflow/06-merge-commit-feature.sh +28 -0
- package/vendor/service-data/workflows/baseline-workflow/07-feature-retract.sh +34 -0
- package/vendor/service-data/workflows/baseline-workflow/07-rebase-origin.sh +27 -0
- package/vendor/service-data/workflows/baseline-workflow/08-push-origin.sh +5 -0
- package/vendor/service-data/workflows/baseline-workflow/09-generate-patches.sh +11 -0
- package/vendor/service-data/workflows/baseline-workflow/profile.json +196 -0
- package/vendor/service-data/workflows/commit-merge-split-lab/00-preflight.sh +8 -0
- package/vendor/service-data/workflows/commit-merge-split-lab/01-show-recent-commits.sh +3 -0
- package/vendor/service-data/workflows/commit-merge-split-lab/02-merge-commits-in-range.sh +5 -0
- package/vendor/service-data/workflows/commit-merge-split-lab/03-split-commit-to-working-tree.sh +6 -0
- package/vendor/service-data/workflows/commit-merge-split-lab/04-recommit-selected-path.sh +6 -0
- package/vendor/service-data/workflows/commit-merge-split-lab/05-continue-rewrite.sh +3 -0
- package/vendor/service-data/workflows/commit-merge-split-lab/profile.json +80 -0
- package/vendor/service-data/workflows/patch-export-for-review/00-preflight.sh +8 -0
- package/vendor/service-data/workflows/patch-export-for-review/01-ensure-patch-out-dir.sh +4 -0
- package/vendor/service-data/workflows/patch-export-for-review/02-format-patch-by-folder.sh +6 -0
- package/vendor/service-data/workflows/patch-export-for-review/03-list-generated-patches.sh +7 -0
- package/vendor/service-data/workflows/patch-export-for-review/profile.json +62 -0
- package/vendor/service-data/workflows/shared-origin-sync-workflow/00-preflight.sh +17 -0
- package/vendor/service-data/workflows/shared-origin-sync-workflow/01-sync-shared-origin.sh +17 -0
- package/vendor/service-data/workflows/shared-origin-sync-workflow/02-push-origin.sh +6 -0
- package/vendor/service-data/workflows/shared-origin-sync-workflow/03-generate-patches.sh +11 -0
- package/vendor/service-data/workflows/shared-origin-sync-workflow/profile.json +156 -0
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# cloverflow-mcp-server
|
|
2
|
+
|
|
3
|
+
CloverFlow MCP launcher package compatible with `install-mcp`.
|
|
4
|
+
|
|
5
|
+
## What this package does
|
|
6
|
+
|
|
7
|
+
- Starts CloverFlow backend `mcp_server` over HTTP.
|
|
8
|
+
- Bridges HTTP MCP to stdio using `mcp-remote@latest`.
|
|
9
|
+
- Exposes a bin command: `cloverflow-mcp-server`.
|
|
10
|
+
|
|
11
|
+
## Configure Your AI Assistant
|
|
12
|
+
|
|
13
|
+
Use `install-mcp` to add the server to your AI assistant:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx -y install-mcp cloverflow-mcp-server --client cursor --yes
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Supported clients: `claude-code`, `cursor`, `windsurf`, `vscode`, `cline`, `roo-cline`, `claude`, `zed`, `goose`, `warp`, `codex`
|
|
20
|
+
|
|
21
|
+
### Claude Code
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx -y install-mcp cloverflow-mcp-server --client claude-code --yes
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Cursor
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx -y install-mcp cloverflow-mcp-server --client cursor --yes
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### VS Code / Copilot
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx -y install-mcp cloverflow-mcp-server --client vscode --yes
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Windsurf
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx -y install-mcp cloverflow-mcp-server --client windsurf --yes
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Cline
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx -y install-mcp cloverflow-mcp-server --client cline --yes
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Restart your AI assistant after adding the configuration.
|
|
52
|
+
|
|
53
|
+
## Environment variables
|
|
54
|
+
|
|
55
|
+
- `WF_MCP_SERVER_BIN`: absolute path to `mcp_server` binary (optional)
|
|
56
|
+
- `WF_SERVICE_DATA_DIR`: absolute path to `service/data` (optional)
|
|
57
|
+
- `WF_MCP_HTTP_HOST`: default `127.0.0.1`
|
|
58
|
+
- `WF_MCP_HTTP_PORT`: optional fixed port (if unset, launcher auto-selects a free port)
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require("node:fs");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
const net = require("node:net");
|
|
5
|
+
const { spawn } = require("node:child_process");
|
|
6
|
+
const packageMeta = require("../package.json");
|
|
7
|
+
|
|
8
|
+
function sleep(ms) {
|
|
9
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isWindows() {
|
|
13
|
+
return process.platform === "win32";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function fileExists(filePath) {
|
|
17
|
+
try {
|
|
18
|
+
return fs.existsSync(filePath);
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function candidateRoots() {
|
|
25
|
+
const cwd = process.cwd();
|
|
26
|
+
return [cwd, path.resolve(cwd, ".."), path.resolve(cwd, "..", "..")];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveServiceDataDir() {
|
|
30
|
+
const bundledData = path.join(__dirname, "..", "vendor", "service-data");
|
|
31
|
+
if (fileExists(bundledData)) {
|
|
32
|
+
return bundledData;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (process.env.WF_SERVICE_DATA_DIR && fileExists(process.env.WF_SERVICE_DATA_DIR)) {
|
|
36
|
+
return process.env.WF_SERVICE_DATA_DIR;
|
|
37
|
+
}
|
|
38
|
+
for (const root of candidateRoots()) {
|
|
39
|
+
const candidate = path.join(root, "service", "data");
|
|
40
|
+
if (fileExists(candidate)) {
|
|
41
|
+
return candidate;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return "";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolveServerLaunch() {
|
|
48
|
+
if (process.env.WF_MCP_SERVER_BIN && fileExists(process.env.WF_MCP_SERVER_BIN)) {
|
|
49
|
+
return { command: process.env.WF_MCP_SERVER_BIN, args: [] };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const exeName = isWindows() ? "mcp_server.exe" : "mcp_server";
|
|
53
|
+
const bundledPath = path.join(__dirname, "..", "vendor", exeName);
|
|
54
|
+
if (fileExists(bundledPath)) {
|
|
55
|
+
return { command: bundledPath, args: [] };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const root of candidateRoots()) {
|
|
59
|
+
const debugPath = path.join(root, "service", "target", "debug", exeName);
|
|
60
|
+
const releasePath = path.join(root, "service", "target", "release", exeName);
|
|
61
|
+
if (fileExists(debugPath)) return { command: debugPath, args: [] };
|
|
62
|
+
if (fileExists(releasePath)) return { command: releasePath, args: [] };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const root of candidateRoots()) {
|
|
66
|
+
const manifestPath = path.join(root, "service", "Cargo.toml");
|
|
67
|
+
if (fileExists(manifestPath)) {
|
|
68
|
+
return {
|
|
69
|
+
command: "cargo",
|
|
70
|
+
args: ["run", "--manifest-path", manifestPath, "--bin", "mcp_server", "--"],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
throw new Error(
|
|
76
|
+
"Cannot locate cloverflow mcp_server. Build service/bin/mcp_server or set WF_MCP_SERVER_BIN."
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function resolvePort() {
|
|
81
|
+
if (process.env.WF_MCP_HTTP_PORT) {
|
|
82
|
+
return String(process.env.WF_MCP_HTTP_PORT);
|
|
83
|
+
}
|
|
84
|
+
return String(
|
|
85
|
+
await new Promise((resolve, reject) => {
|
|
86
|
+
const server = net.createServer();
|
|
87
|
+
server.listen(0, "127.0.0.1", () => {
|
|
88
|
+
const address = server.address();
|
|
89
|
+
if (!address || typeof address === "string") {
|
|
90
|
+
server.close();
|
|
91
|
+
reject(new Error("Failed to allocate free TCP port."));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const freePort = address.port;
|
|
95
|
+
server.close(() => resolve(freePort));
|
|
96
|
+
});
|
|
97
|
+
server.on("error", reject);
|
|
98
|
+
})
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function waitHealth(endpoint, timeoutMs) {
|
|
103
|
+
const started = Date.now();
|
|
104
|
+
while (Date.now() - started < timeoutMs) {
|
|
105
|
+
try {
|
|
106
|
+
const response = await fetch(`${endpoint}/health`);
|
|
107
|
+
if (response.ok) return;
|
|
108
|
+
} catch {
|
|
109
|
+
// keep waiting
|
|
110
|
+
}
|
|
111
|
+
await sleep(300);
|
|
112
|
+
}
|
|
113
|
+
throw new Error(`mcp_server health check timeout: ${endpoint}/health`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function spawnBackend(command, args, env) {
|
|
117
|
+
console.error(`[cloverflow-mcp] backend launch command=${command} args=${JSON.stringify(args)}`);
|
|
118
|
+
const child = spawn(command, args, {
|
|
119
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
120
|
+
env,
|
|
121
|
+
shell: false,
|
|
122
|
+
windowsHide: true,
|
|
123
|
+
});
|
|
124
|
+
child.on("error", (error) => {
|
|
125
|
+
console.error(`[cloverflow-mcp] backend spawn error: ${error.message}`);
|
|
126
|
+
});
|
|
127
|
+
child.stdout.on("data", (chunk) => {
|
|
128
|
+
process.stderr.write(String(chunk));
|
|
129
|
+
});
|
|
130
|
+
child.stderr.on("data", (chunk) => {
|
|
131
|
+
process.stderr.write(String(chunk));
|
|
132
|
+
});
|
|
133
|
+
return child;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function spawnBridge(env, endpoint) {
|
|
137
|
+
const protocolVersion = (process.env.WF_MCP_PROTOCOL_VERSION || "").trim();
|
|
138
|
+
const transportStrategy = process.env.WF_MCP_TRANSPORT_STRATEGY || "http-first";
|
|
139
|
+
const authTimeoutSeconds = process.env.WF_MCP_AUTH_TIMEOUT_SECONDS || "120";
|
|
140
|
+
const remoteArgs = [
|
|
141
|
+
"-y",
|
|
142
|
+
"mcp-remote@latest",
|
|
143
|
+
endpoint,
|
|
144
|
+
"--allow-http",
|
|
145
|
+
"--transport",
|
|
146
|
+
transportStrategy,
|
|
147
|
+
"--auth-timeout",
|
|
148
|
+
authTimeoutSeconds,
|
|
149
|
+
];
|
|
150
|
+
if (protocolVersion) {
|
|
151
|
+
remoteArgs.push("--header", `MCP-Protocol-Version:${protocolVersion}`);
|
|
152
|
+
}
|
|
153
|
+
const command = isWindows() ? (process.env.ComSpec || "cmd.exe") : "npx";
|
|
154
|
+
const args = isWindows()
|
|
155
|
+
? ["/d", "/s", "/c", `npx ${remoteArgs.join(" ")}`]
|
|
156
|
+
: remoteArgs;
|
|
157
|
+
console.error(`[cloverflow-mcp] bridge launch command=${command} args=${JSON.stringify(args)}`);
|
|
158
|
+
|
|
159
|
+
const child = spawn(command, args, {
|
|
160
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
161
|
+
env,
|
|
162
|
+
shell: false,
|
|
163
|
+
windowsHide: true,
|
|
164
|
+
});
|
|
165
|
+
child.on("error", (error) => {
|
|
166
|
+
console.error(`[cloverflow-mcp] bridge spawn error: ${error.message}`);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
let bridgeReady = false;
|
|
170
|
+
const pendingInputChunks = [];
|
|
171
|
+
const forwardOrBuffer = (chunk) => {
|
|
172
|
+
if (bridgeReady) {
|
|
173
|
+
child.stdin.write(chunk);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
pendingInputChunks.push(Buffer.from(chunk));
|
|
177
|
+
};
|
|
178
|
+
const flushPending = () => {
|
|
179
|
+
if (!bridgeReady) return;
|
|
180
|
+
while (pendingInputChunks.length > 0) {
|
|
181
|
+
const next = pendingInputChunks.shift();
|
|
182
|
+
child.stdin.write(next);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
process.stdin.on("data", forwardOrBuffer);
|
|
187
|
+
process.stdin.on("end", () => {
|
|
188
|
+
child.stdin.end();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
child.stderr.on("data", (chunk) => {
|
|
192
|
+
const text = String(chunk);
|
|
193
|
+
process.stderr.write(text);
|
|
194
|
+
if (!bridgeReady && (text.includes("Local STDIO server running") || text.includes("Proxy established successfully"))) {
|
|
195
|
+
bridgeReady = true;
|
|
196
|
+
flushPending();
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
child.stdout.pipe(process.stdout);
|
|
201
|
+
return child;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function main() {
|
|
205
|
+
const host = process.env.WF_MCP_HTTP_HOST || "127.0.0.1";
|
|
206
|
+
const port = await resolvePort();
|
|
207
|
+
const endpoint = `http://${host}:${port}`;
|
|
208
|
+
console.error(
|
|
209
|
+
`[cloverflow-mcp] launcher version=${packageMeta.version || "unknown"} package=${packageMeta.name || "unknown"}`
|
|
210
|
+
);
|
|
211
|
+
const serviceDataDir = resolveServiceDataDir();
|
|
212
|
+
const launch = resolveServerLaunch();
|
|
213
|
+
|
|
214
|
+
const backendEnv = { ...process.env };
|
|
215
|
+
if (serviceDataDir) {
|
|
216
|
+
backendEnv.WF_SERVICE_DATA_DIR = serviceDataDir;
|
|
217
|
+
}
|
|
218
|
+
backendEnv.WF_MCP_NPM_PACKAGE = packageMeta.name || "cloverflow-mcp-server";
|
|
219
|
+
backendEnv.WF_MCP_NPM_VERSION = packageMeta.version || "";
|
|
220
|
+
|
|
221
|
+
const backend = spawnBackend(
|
|
222
|
+
launch.command,
|
|
223
|
+
[...launch.args, "--host", host, "--port", String(port)],
|
|
224
|
+
backendEnv
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
let shuttingDown = false;
|
|
228
|
+
const shutdown = () => {
|
|
229
|
+
if (shuttingDown) return;
|
|
230
|
+
shuttingDown = true;
|
|
231
|
+
if (!backend.killed) {
|
|
232
|
+
backend.kill();
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
process.on("SIGINT", shutdown);
|
|
237
|
+
process.on("SIGTERM", shutdown);
|
|
238
|
+
process.on("exit", shutdown);
|
|
239
|
+
|
|
240
|
+
backend.on("exit", (code) => {
|
|
241
|
+
if (!shuttingDown && code !== 0) {
|
|
242
|
+
console.error(`[cloverflow-mcp] backend exited early with code ${code}`);
|
|
243
|
+
process.exit(code || 1);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
await waitHealth(endpoint, 20000);
|
|
248
|
+
|
|
249
|
+
const bridge = spawnBridge(process.env, endpoint);
|
|
250
|
+
bridge.on("exit", (code) => {
|
|
251
|
+
shutdown();
|
|
252
|
+
process.exit(code || 0);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
main().catch((error) => {
|
|
257
|
+
console.error(`[cloverflow-mcp] ${error.message}`);
|
|
258
|
+
process.exit(1);
|
|
259
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cloverflow-mcp-server",
|
|
3
|
+
"version": "0.0.8",
|
|
4
|
+
"description": "MCP stdio launcher for CloverFlow service mcp_server",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cloverflow-mcp-server": "bin/cloverflow-mcp-server.js"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"prepack": "node scripts/prepack.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"bin",
|
|
17
|
+
"vendor"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
wf require WF_TARGET_FOLDER
|
|
3
|
+
wf require WF_SHARED_REPO_PATH
|
|
4
|
+
echo "[Status] Copying shared repo snapshot from: $WF_SHARED_REPO_PATH"
|
|
5
|
+
wf test is-git-repo "$WF_SHARED_REPO_PATH"
|
|
6
|
+
if wf test is-path-exists "$WF_TARGET_FOLDER"; then
|
|
7
|
+
wf test is-dir "$WF_TARGET_FOLDER"
|
|
8
|
+
wf test is-empty-dir "$WF_TARGET_FOLDER"
|
|
9
|
+
else
|
|
10
|
+
mkdir -p "$WF_TARGET_FOLDER"
|
|
11
|
+
fi
|
|
12
|
+
cp -a "$WF_SHARED_REPO_PATH/." "$WF_TARGET_FOLDER/"
|
|
13
|
+
branch_name="feature/$(basename "$WF_TARGET_FOLDER")"
|
|
14
|
+
if git -C "$WF_TARGET_FOLDER" rev-parse --verify --quiet "refs/heads/$branch_name" >/dev/null; then
|
|
15
|
+
git -C "$WF_TARGET_FOLDER" checkout "$branch_name"
|
|
16
|
+
else
|
|
17
|
+
git -C "$WF_TARGET_FOLDER" checkout -b "$branch_name"
|
|
18
|
+
fi
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
wf require WF_TARGET_FOLDER
|
|
3
|
+
wf require WF_PATCH_COMMITS
|
|
4
|
+
first="$(printf '%s\n' "$WF_PATCH_COMMITS" | awk '{print $1}')"
|
|
5
|
+
wf test is-git-repo "$WF_TARGET_FOLDER"
|
|
6
|
+
wf test is-git-commit "$WF_TARGET_FOLDER" "$first"
|
|
7
|
+
echo "[Status] Open rebase todo from: $first^"
|
|
8
|
+
echo "[Hint] Keep earliest hash as pick, change later selected hashes to squash."
|
|
9
|
+
git -C "$WF_TARGET_FOLDER" rebase -i "$first^"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
wf require WF_TARGET_FOLDER
|
|
3
|
+
wf require WF_SHARED_REPO_PATH
|
|
4
|
+
wf require WF_COMMIT_HASH
|
|
5
|
+
# Validate repositories and selected commit before rewriting history.
|
|
6
|
+
wf test is-git-repo "$WF_TARGET_FOLDER"
|
|
7
|
+
wf test is-git-repo "$WF_SHARED_REPO_PATH"
|
|
8
|
+
wf test is-git-commit "$WF_TARGET_FOLDER" "$WF_COMMIT_HASH"
|
|
9
|
+
|
|
10
|
+
# Use merge-base(target HEAD, shared HEAD) as the rebase base.
|
|
11
|
+
shared_head="$(git -C "$WF_SHARED_REPO_PATH" rev-parse HEAD)"
|
|
12
|
+
lcp="$(git -C "$WF_TARGET_FOLDER" merge-base HEAD "$shared_head")"
|
|
13
|
+
if [ "$lcp" != "$shared_head" ]; then
|
|
14
|
+
echo "[Error] LCP mismatch: lcp($lcp) != shared_head($shared_head)"
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
selected_short="$(git -C "$WF_TARGET_FOLDER" rev-parse --short "$WF_COMMIT_HASH")"
|
|
18
|
+
|
|
19
|
+
# Reorder rebase todo: move selected commit line to the top.
|
|
20
|
+
editor_cmd="sh -c 'f=\"\$1\"; awk -v s=\"$selected_short\" '\''/^pick / { h=\$2; if (index(h, s)==1 || index(s, h)==1) { x=\$0; next } } { r = r \$0 ORS } END { if (!x) { print \"[Error] commit not in todo.\" > \"/dev/stderr\"; exit 3 } print x; printf \"%s\", r }'\'' \"\$f\" > \"\$f.tmp\" && mv \"\$f.tmp\" \"\$f\"' _"
|
|
21
|
+
GIT_SEQUENCE_EDITOR="$editor_cmd" git -C "$WF_TARGET_FOLDER" rebase -i "$lcp"
|
|
22
|
+
|
|
23
|
+
# Sync selected patch to shared branch.
|
|
24
|
+
git -C "$WF_SHARED_REPO_PATH" fetch "$WF_TARGET_FOLDER" "$WF_COMMIT_HASH"
|
|
25
|
+
git -C "$WF_SHARED_REPO_PATH" cherry-pick FETCH_HEAD
|
|
26
|
+
echo "[Status] Patch sink completed."
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Rebase target repos onto shared base head.
|
|
3
|
+
wf require WF_SHARED_REPO_PATH
|
|
4
|
+
wf require WF_TARGET_REPOS
|
|
5
|
+
wf test is-git-repo "$WF_SHARED_REPO_PATH"
|
|
6
|
+
|
|
7
|
+
shared_head=""
|
|
8
|
+
handoff_file="$WF_SHARED_REPO_PATH/.cloverflow-last-shared-head"
|
|
9
|
+
if [ -n "${WF_SHARED_HEAD_HINT:-}" ]; then
|
|
10
|
+
wf test is-git-commit "$WF_SHARED_REPO_PATH" "$WF_SHARED_HEAD_HINT"
|
|
11
|
+
shared_head="$WF_SHARED_HEAD_HINT"
|
|
12
|
+
echo "[Status] Broadcast uses pinned shared head from param: $shared_head"
|
|
13
|
+
elif [ -f "$handoff_file" ]; then
|
|
14
|
+
candidate_head="$(awk 'NR==1 { print; exit }' "$handoff_file")"
|
|
15
|
+
if [ -n "$candidate_head" ] && git -C "$WF_SHARED_REPO_PATH" rev-parse --verify --quiet "$candidate_head^{commit}" >/dev/null 2>&1; then
|
|
16
|
+
shared_head="$candidate_head"
|
|
17
|
+
echo "[Status] Broadcast uses handoff shared head file: $shared_head"
|
|
18
|
+
fi
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
if [ -z "$shared_head" ]; then
|
|
22
|
+
shared_head="$(git -C "$WF_SHARED_REPO_PATH" rev-parse HEAD)"
|
|
23
|
+
echo "[Status] Broadcast uses current shared head: $shared_head"
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# Validate all target repos first (fail fast before any mutation).
|
|
27
|
+
printf '%s\n' "$WF_TARGET_REPOS" | while IFS= read -r repo_path; do
|
|
28
|
+
[ -z "$repo_path" ] && continue
|
|
29
|
+
[ "$repo_path" = "$WF_SHARED_REPO_PATH" ] && continue
|
|
30
|
+
wf test is-git-repo "$repo_path"
|
|
31
|
+
done
|
|
32
|
+
|
|
33
|
+
# Rebase each validated target repo onto shared base.
|
|
34
|
+
printf '%s\n' "$WF_TARGET_REPOS" | while IFS= read -r repo_path; do
|
|
35
|
+
[ -z "$repo_path" ] && continue
|
|
36
|
+
[ "$repo_path" = "$WF_SHARED_REPO_PATH" ] && continue
|
|
37
|
+
git -C "$repo_path" fetch "$WF_SHARED_REPO_PATH" "$shared_head"
|
|
38
|
+
fetched_shared_head="$(git -C "$repo_path" rev-parse FETCH_HEAD)"
|
|
39
|
+
lcp="$(git -C "$repo_path" merge-base HEAD "$fetched_shared_head")"
|
|
40
|
+
[ "$lcp" = "$fetched_shared_head" ] && continue
|
|
41
|
+
git -C "$repo_path" rebase --onto "$fetched_shared_head" "$lcp"
|
|
42
|
+
done
|
|
43
|
+
|
|
44
|
+
echo "[Status] Shared base broadcast completed."
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Rebase target repos onto latest shared head.
|
|
3
|
+
wf require WF_SHARED_REPO_PATH
|
|
4
|
+
wf require WF_TARGET_REPOS
|
|
5
|
+
wf test is-git-repo "$WF_SHARED_REPO_PATH"
|
|
6
|
+
shared_head="$(git -C "$WF_SHARED_REPO_PATH" rev-parse HEAD)"
|
|
7
|
+
|
|
8
|
+
# Validate all target repos first (fail fast before any mutation).
|
|
9
|
+
for repo_path in $WF_TARGET_REPOS; do
|
|
10
|
+
[ "$repo_path" = "$WF_SHARED_REPO_PATH" ] && continue
|
|
11
|
+
wf test is-git-repo "$repo_path"
|
|
12
|
+
done
|
|
13
|
+
|
|
14
|
+
# Rebase each validated target repo onto shared latest base.
|
|
15
|
+
for repo_path in $WF_TARGET_REPOS; do
|
|
16
|
+
[ "$repo_path" = "$WF_SHARED_REPO_PATH" ] && continue
|
|
17
|
+
git -C "$repo_path" fetch "$WF_SHARED_REPO_PATH" "$shared_head"
|
|
18
|
+
fetched_shared_head="$(git -C "$repo_path" rev-parse FETCH_HEAD)"
|
|
19
|
+
lcp="$(git -C "$repo_path" merge-base HEAD "$fetched_shared_head")"
|
|
20
|
+
[ "$lcp" = "$fetched_shared_head" ] && continue
|
|
21
|
+
git -C "$repo_path" rebase --onto "$fetched_shared_head" "$lcp"
|
|
22
|
+
done
|
|
23
|
+
|
|
24
|
+
echo "[Status] Broadcast completed."
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
wf require WF_TARGET_FOLDER
|
|
3
|
+
wf require WF_SHARED_REPO_PATH
|
|
4
|
+
echo "[Status] Squashing commits from source branch into shared as one commit..."
|
|
5
|
+
wf test is-git-repo "$WF_TARGET_FOLDER"
|
|
6
|
+
wf test is-git-repo "$WF_SHARED_REPO_PATH"
|
|
7
|
+
shared_head="$(git -C "$WF_SHARED_REPO_PATH" rev-parse HEAD)"
|
|
8
|
+
source_head="$(git -C "$WF_TARGET_FOLDER" rev-parse HEAD)"
|
|
9
|
+
git -C "$WF_SHARED_REPO_PATH" fetch "$WF_TARGET_FOLDER" "$source_head"
|
|
10
|
+
fetched_source_head="$(git -C "$WF_SHARED_REPO_PATH" rev-parse FETCH_HEAD)"
|
|
11
|
+
commit_count="$(git -C "$WF_SHARED_REPO_PATH" rev-list --count "${shared_head}..${fetched_source_head}")"
|
|
12
|
+
if [ "$commit_count" -lt 1 ]; then
|
|
13
|
+
echo "[Error] No commits after shared base to squash."
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
16
|
+
git -C "$WF_SHARED_REPO_PATH" merge --squash "$fetched_source_head"
|
|
17
|
+
git -C "$WF_SHARED_REPO_PATH" commit -m "squash: from $(basename "$WF_TARGET_FOLDER") after shared base"
|
|
18
|
+
echo "[Status] Shared squash commit created."
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
wf require WF_COMMIT_HASH
|
|
3
|
+
wf require WF_SHARED_REPO_PATH
|
|
4
|
+
echo "[Status] Dropping latest shared squash commit: $WF_COMMIT_HASH..."
|
|
5
|
+
|
|
6
|
+
# Ensure shared repo and target commit are valid before any history operation.
|
|
7
|
+
wf test is-git-repo "$WF_SHARED_REPO_PATH"
|
|
8
|
+
wf test is-git-commit "$WF_SHARED_REPO_PATH" "$WF_COMMIT_HASH"
|
|
9
|
+
|
|
10
|
+
# Step6 must revert the current shared HEAD produced by Step5.
|
|
11
|
+
head_commit="$(git -C "$WF_SHARED_REPO_PATH" rev-parse HEAD)"
|
|
12
|
+
if [ "$head_commit" != "$WF_COMMIT_HASH" ]; then
|
|
13
|
+
echo "[Error] WF_COMMIT_HASH must be current shared HEAD for retract."
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
# Guard against dropping arbitrary commits: only allow Step5 squash commit pattern.
|
|
18
|
+
commit_subject="$(git -C "$WF_SHARED_REPO_PATH" log -1 --format=%s "$WF_COMMIT_HASH")"
|
|
19
|
+
case "$commit_subject" in
|
|
20
|
+
squash:*)
|
|
21
|
+
;;
|
|
22
|
+
*)
|
|
23
|
+
echo "[Error] commit is not a squash commit from Step5."
|
|
24
|
+
exit 1
|
|
25
|
+
;;
|
|
26
|
+
esac
|
|
27
|
+
git -C "$WF_SHARED_REPO_PATH" reset --hard "$WF_COMMIT_HASH^"
|
|
28
|
+
echo "[Status] Shared squash commit dropped."
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
wf require WF_TARGET_FOLDER
|
|
3
|
+
wf require WF_SHARED_REPO_PATH
|
|
4
|
+
echo "[Status] Push target branch to shared, then merge --no-ff into shared HEAD..."
|
|
5
|
+
wf test is-git-repo "$WF_TARGET_FOLDER"
|
|
6
|
+
wf test is-git-repo "$WF_SHARED_REPO_PATH"
|
|
7
|
+
shared_head="$(git -C "$WF_SHARED_REPO_PATH" rev-parse HEAD)"
|
|
8
|
+
|
|
9
|
+
source_branch="$(git -C "$WF_TARGET_FOLDER" branch --show-current)"
|
|
10
|
+
if [ -z "$source_branch" ]; then
|
|
11
|
+
echo "[Error] Target repo is detached; cannot push branch."
|
|
12
|
+
exit 1
|
|
13
|
+
fi
|
|
14
|
+
shared_branch="$(git -C "$WF_SHARED_REPO_PATH" branch --show-current)"
|
|
15
|
+
if [ "$source_branch" = "$shared_branch" ]; then
|
|
16
|
+
echo "[Error] Target branch equals current shared branch; refusing to overwrite."
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
git -C "$WF_TARGET_FOLDER" push "$WF_SHARED_REPO_PATH" "HEAD:refs/heads/$source_branch"
|
|
21
|
+
fetched_source_head="$(git -C "$WF_SHARED_REPO_PATH" rev-parse "$source_branch")"
|
|
22
|
+
commit_count="$(git -C "$WF_SHARED_REPO_PATH" rev-list --count "${shared_head}..${fetched_source_head}")"
|
|
23
|
+
if [ "$commit_count" -lt 1 ]; then
|
|
24
|
+
echo "[Error] No commits after shared base to merge."
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
git -C "$WF_SHARED_REPO_PATH" merge --no-ff "$fetched_source_head"
|
|
28
|
+
echo "[Status] Shared merge commit created."
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
wf require WF_COMMIT_HASH
|
|
3
|
+
wf require WF_SHARED_REPO_PATH
|
|
4
|
+
echo "[Status] Retracting latest shared integration commit: $WF_COMMIT_HASH..."
|
|
5
|
+
|
|
6
|
+
wf test is-git-repo "$WF_SHARED_REPO_PATH"
|
|
7
|
+
wf test is-git-commit "$WF_SHARED_REPO_PATH" "$WF_COMMIT_HASH"
|
|
8
|
+
|
|
9
|
+
head_commit="$(git -C "$WF_SHARED_REPO_PATH" rev-parse HEAD)"
|
|
10
|
+
if [ "$head_commit" != "$WF_COMMIT_HASH" ]; then
|
|
11
|
+
echo "[Error] WF_COMMIT_HASH must be current shared HEAD for retract."
|
|
12
|
+
exit 1
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
parent_line="$(git -C "$WF_SHARED_REPO_PATH" rev-list --parents -n 1 "$WF_COMMIT_HASH")"
|
|
16
|
+
parent_count="$(printf '%s\n' "$parent_line" | awk '{print NF - 1}')"
|
|
17
|
+
|
|
18
|
+
if [ "$parent_count" -ge 2 ]; then
|
|
19
|
+
git -C "$WF_SHARED_REPO_PATH" reset --hard "$WF_COMMIT_HASH^1"
|
|
20
|
+
echo "[Status] Shared merge commit dropped."
|
|
21
|
+
exit 0
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
commit_subject="$(git -C "$WF_SHARED_REPO_PATH" log -1 --format=%s "$WF_COMMIT_HASH")"
|
|
25
|
+
case "$commit_subject" in
|
|
26
|
+
squash:*)
|
|
27
|
+
git -C "$WF_SHARED_REPO_PATH" reset --hard "$WF_COMMIT_HASH^"
|
|
28
|
+
echo "[Status] Shared squash commit dropped."
|
|
29
|
+
;;
|
|
30
|
+
*)
|
|
31
|
+
echo "[Error] commit is neither Step5 squash commit nor Step6 merge commit."
|
|
32
|
+
exit 1
|
|
33
|
+
;;
|
|
34
|
+
esac
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
wf require WF_SHARED_REPO_PATH
|
|
3
|
+
wf require WF_TARGET_REPOS
|
|
4
|
+
echo "[Status] Rebasing shared onto origin/main, then rebasing non-shared repos onto shared..."
|
|
5
|
+
|
|
6
|
+
# Validate all repos first (fail fast before any mutation).
|
|
7
|
+
wf test is-git-repo "$WF_SHARED_REPO_PATH"
|
|
8
|
+
for repo_path in $WF_TARGET_REPOS; do
|
|
9
|
+
[ "$repo_path" = "$WF_SHARED_REPO_PATH" ] && continue
|
|
10
|
+
wf test is-git-repo "$repo_path"
|
|
11
|
+
done
|
|
12
|
+
|
|
13
|
+
# Rebase shared repo to origin/main first.
|
|
14
|
+
git -C "$WF_SHARED_REPO_PATH" fetch origin
|
|
15
|
+
git -C "$WF_SHARED_REPO_PATH" rebase origin/main
|
|
16
|
+
shared_head="$(git -C "$WF_SHARED_REPO_PATH" rev-parse HEAD)"
|
|
17
|
+
|
|
18
|
+
# Rebase each target repo to the new shared base.
|
|
19
|
+
for repo_path in $WF_TARGET_REPOS; do
|
|
20
|
+
[ "$repo_path" = "$WF_SHARED_REPO_PATH" ] && continue
|
|
21
|
+
git -C "$repo_path" fetch "$WF_SHARED_REPO_PATH" "$shared_head"
|
|
22
|
+
fetched_shared_head="$(git -C "$repo_path" rev-parse FETCH_HEAD)"
|
|
23
|
+
lcp="$(git -C "$repo_path" merge-base HEAD "$fetched_shared_head")"
|
|
24
|
+
[ "$lcp" = "$fetched_shared_head" ] && continue
|
|
25
|
+
git -C "$repo_path" rebase --onto "$fetched_shared_head" "$lcp"
|
|
26
|
+
done
|
|
27
|
+
echo "[Status] Shared and non-shared repos are rebased to latest base."
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
wf require WF_TARGET_FOLDER
|
|
3
|
+
wf require WF_TARGET_REPO_PATH
|
|
4
|
+
wf require WF_SHARED_REPO_PATH
|
|
5
|
+
wf test is-git-repo "$WF_TARGET_REPO_PATH"
|
|
6
|
+
wf test is-git-repo "$WF_SHARED_REPO_PATH"
|
|
7
|
+
wf test is-path-exists "$WF_TARGET_FOLDER"
|
|
8
|
+
echo "[Status] Generating exported patch files for $WF_TARGET_FOLDER..."
|
|
9
|
+
mkdir -p "$WF_TARGET_REPO_PATH/patches_out"
|
|
10
|
+
git -C "$WF_TARGET_REPO_PATH" format-patch -o "$WF_TARGET_REPO_PATH/patches_out/" -1 HEAD -- $WF_TARGET_FOLDER
|
|
11
|
+
echo "[Status] Patch sequence generated."
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "baseline-workflow",
|
|
3
|
+
"name": "Baseline Workflow",
|
|
4
|
+
"nameI18n": {
|
|
5
|
+
"en-US": "Baseline Workflow",
|
|
6
|
+
"zh-CN": "基础工作流"
|
|
7
|
+
},
|
|
8
|
+
"description": "Default profile for daily Git operations with local shared-repo state and patch/feature handling.",
|
|
9
|
+
"descriptionI18n": {
|
|
10
|
+
"en-US": "Default profile for daily Git operations with local shared-repo state and patch/feature handling.",
|
|
11
|
+
"zh-CN": "用于日常 Git 操作的基础配置,包含共享仓库本地状态以及补丁/特性整理流程。"
|
|
12
|
+
},
|
|
13
|
+
"preflightScript": "00-preflight.sh",
|
|
14
|
+
"storageFields": [
|
|
15
|
+
{
|
|
16
|
+
"key": "repoPaths",
|
|
17
|
+
"type": "pathList",
|
|
18
|
+
"required": true,
|
|
19
|
+
"linkSource": "profileState",
|
|
20
|
+
"description": "Locally persisted repository folder paths used by this profile.",
|
|
21
|
+
"descriptionI18n": {
|
|
22
|
+
"en-US": "Locally persisted repository folder paths used by this profile.",
|
|
23
|
+
"zh-CN": "该 profile 在本地保存的仓库目录路径列表。"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"key": "lastTargetFolder",
|
|
28
|
+
"type": "path",
|
|
29
|
+
"required": false,
|
|
30
|
+
"description": "Last target folder used when running steps.",
|
|
31
|
+
"descriptionI18n": {
|
|
32
|
+
"en-US": "Last target folder used when running steps.",
|
|
33
|
+
"zh-CN": "上次执行步骤时使用的目标文件夹。"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"key": "sharedRepoPath",
|
|
38
|
+
"type": "path",
|
|
39
|
+
"required": true,
|
|
40
|
+
"description": "Shared source repository used as Step1 baseline.",
|
|
41
|
+
"descriptionI18n": {
|
|
42
|
+
"en-US": "Shared source repository used as Step1 baseline.",
|
|
43
|
+
"zh-CN": "Step1 使用的共享源仓库路径。"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"key": "lastSharedHead",
|
|
48
|
+
"type": "string",
|
|
49
|
+
"required": false,
|
|
50
|
+
"description": "Optional pinned shared HEAD used by broadcast step.",
|
|
51
|
+
"descriptionI18n": {
|
|
52
|
+
"en-US": "Optional pinned shared HEAD used by broadcast step.",
|
|
53
|
+
"zh-CN": "广播步骤使用的可选共享基线提交。"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
"steps": [
|
|
58
|
+
{
|
|
59
|
+
"id": "start-feature",
|
|
60
|
+
"name": "Start Feature",
|
|
61
|
+
"nameI18n": {
|
|
62
|
+
"en-US": "Start Feature",
|
|
63
|
+
"zh-CN": "创建特性分支"
|
|
64
|
+
},
|
|
65
|
+
"description": "Create feature branch and copy full snapshot from shared source repo.",
|
|
66
|
+
"descriptionI18n": {
|
|
67
|
+
"en-US": "Create feature branch and copy full snapshot from shared source repo.",
|
|
68
|
+
"zh-CN": "创建特性分支,并从共享源仓库全量复制快照。"
|
|
69
|
+
},
|
|
70
|
+
"paramBindings": [
|
|
71
|
+
{
|
|
72
|
+
"paramKey": "TARGET_FOLDER",
|
|
73
|
+
"storageKey": "lastTargetFolder",
|
|
74
|
+
"uiControl": "select",
|
|
75
|
+
"optionsFromStorageKey": "repoPaths",
|
|
76
|
+
"placeholderI18n": {
|
|
77
|
+
"en-US": "Select a target folder",
|
|
78
|
+
"zh-CN": "请选择目标目录"
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"paramKey": "SHARED_REPO_PATH",
|
|
83
|
+
"storageKey": "sharedRepoPath",
|
|
84
|
+
"valueSource": "storageDirect"
|
|
85
|
+
}
|
|
86
|
+
],
|
|
87
|
+
"script": "01-start-feature.sh"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"id": "select-minimal-patch",
|
|
91
|
+
"name": "Select Minimal Patch",
|
|
92
|
+
"nameI18n": {
|
|
93
|
+
"en-US": "Select Minimal Patch",
|
|
94
|
+
"zh-CN": "选定最小修复"
|
|
95
|
+
},
|
|
96
|
+
"descriptionI18n": {
|
|
97
|
+
"en-US": "Select commit hashes and squash them into the earliest one via rebase.",
|
|
98
|
+
"zh-CN": "输入 commit hash 列表,并在 rebase 中将它们压缩到最早提交。"
|
|
99
|
+
},
|
|
100
|
+
"paramBindings": [
|
|
101
|
+
{
|
|
102
|
+
"paramKey": "TARGET_FOLDER",
|
|
103
|
+
"storageKey": "lastTargetFolder",
|
|
104
|
+
"uiControl": "select",
|
|
105
|
+
"optionsFromStorageKey": "repoPaths",
|
|
106
|
+
"placeholderI18n": {
|
|
107
|
+
"en-US": "Select a target folder",
|
|
108
|
+
"zh-CN": "请选择目标目录"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
],
|
|
112
|
+
"script": "02-select-minimal-patch.sh"
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"id": "patch-sink",
|
|
116
|
+
"name": "Patch Sink",
|
|
117
|
+
"nameI18n": {
|
|
118
|
+
"en-US": "Patch Sink",
|
|
119
|
+
"zh-CN": "补丁下沉"
|
|
120
|
+
},
|
|
121
|
+
"script": "03-patch-sink.sh"
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"id": "broadcast-shared-base",
|
|
125
|
+
"name": "Broadcast Shared Base",
|
|
126
|
+
"nameI18n": {
|
|
127
|
+
"en-US": "Broadcast Shared Base",
|
|
128
|
+
"zh-CN": "共享基线分发"
|
|
129
|
+
},
|
|
130
|
+
"paramBindings": [
|
|
131
|
+
{
|
|
132
|
+
"paramKey": "SHARED_REPO_PATH",
|
|
133
|
+
"storageKey": "sharedRepoPath",
|
|
134
|
+
"valueSource": "storageDirect"
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"paramKey": "TARGET_REPOS",
|
|
138
|
+
"storageKey": "repoPaths",
|
|
139
|
+
"valueSource": "storageDirect"
|
|
140
|
+
}
|
|
141
|
+
],
|
|
142
|
+
"script": "04-broadcast-shared-base.sh"
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"id": "squash-feature",
|
|
146
|
+
"name": "Squash Feature",
|
|
147
|
+
"nameI18n": {
|
|
148
|
+
"en-US": "Squash Feature",
|
|
149
|
+
"zh-CN": "特性收敛"
|
|
150
|
+
},
|
|
151
|
+
"paramBindings": [
|
|
152
|
+
{
|
|
153
|
+
"paramKey": "TARGET_FOLDER",
|
|
154
|
+
"storageKey": "lastTargetFolder",
|
|
155
|
+
"uiControl": "select",
|
|
156
|
+
"optionsFromStorageKey": "repoPaths",
|
|
157
|
+
"placeholderI18n": {
|
|
158
|
+
"en-US": "Select a target folder",
|
|
159
|
+
"zh-CN": "请选择目标目录"
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
],
|
|
163
|
+
"script": "05-squash-feature.sh"
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"id": "merge-commit-feature",
|
|
167
|
+
"name": "Merge Commit Feature",
|
|
168
|
+
"nameI18n": {
|
|
169
|
+
"en-US": "Merge Commit Feature",
|
|
170
|
+
"zh-CN": "特性合并提交"
|
|
171
|
+
},
|
|
172
|
+
"paramBindings": [
|
|
173
|
+
{
|
|
174
|
+
"paramKey": "TARGET_FOLDER",
|
|
175
|
+
"storageKey": "lastTargetFolder",
|
|
176
|
+
"uiControl": "select",
|
|
177
|
+
"optionsFromStorageKey": "repoPaths",
|
|
178
|
+
"placeholderI18n": {
|
|
179
|
+
"en-US": "Select a target folder",
|
|
180
|
+
"zh-CN": "请选择目标目录"
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
],
|
|
184
|
+
"script": "06-merge-commit-feature.sh"
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"id": "feature-retract",
|
|
188
|
+
"name": "Feature Retract",
|
|
189
|
+
"nameI18n": {
|
|
190
|
+
"en-US": "Feature Retract",
|
|
191
|
+
"zh-CN": "特性撤回"
|
|
192
|
+
},
|
|
193
|
+
"script": "07-feature-retract.sh"
|
|
194
|
+
}
|
|
195
|
+
]
|
|
196
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "commit-merge-split-lab",
|
|
3
|
+
"name": "Commit Merge and Split Lab",
|
|
4
|
+
"nameI18n": {
|
|
5
|
+
"en-US": "Commit Merge and Split Lab",
|
|
6
|
+
"zh-CN": "提交合并与分割实验室"
|
|
7
|
+
},
|
|
8
|
+
"descriptionI18n": {
|
|
9
|
+
"en-US": "Interactive profile for combining or splitting commits safely.",
|
|
10
|
+
"zh-CN": "用于交互式合并或分割提交的实验配置。"
|
|
11
|
+
},
|
|
12
|
+
"preflightScript": "00-preflight.sh",
|
|
13
|
+
"storageFields": [
|
|
14
|
+
{
|
|
15
|
+
"key": "lastRewriteBaseRef",
|
|
16
|
+
"type": "string",
|
|
17
|
+
"required": false,
|
|
18
|
+
"descriptionI18n": {
|
|
19
|
+
"en-US": "Last base ref used for interactive rebase.",
|
|
20
|
+
"zh-CN": "最近一次交互式 rebase 使用的基准引用。"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"key": "lastSplitCommitHash",
|
|
25
|
+
"type": "string",
|
|
26
|
+
"required": false,
|
|
27
|
+
"descriptionI18n": {
|
|
28
|
+
"en-US": "Last commit hash selected for split operation.",
|
|
29
|
+
"zh-CN": "最近一次用于分割的提交哈希。"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"steps": [
|
|
34
|
+
{
|
|
35
|
+
"id": "show-recent-commits",
|
|
36
|
+
"name": "Show Recent Commits",
|
|
37
|
+
"nameI18n": {
|
|
38
|
+
"en-US": "Show Recent Commits",
|
|
39
|
+
"zh-CN": "查看最近提交"
|
|
40
|
+
},
|
|
41
|
+
"script": "01-show-recent-commits.sh"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "merge-commits-in-range",
|
|
45
|
+
"name": "Merge Commits In Range",
|
|
46
|
+
"nameI18n": {
|
|
47
|
+
"en-US": "Merge Commits In Range",
|
|
48
|
+
"zh-CN": "范围内合并提交"
|
|
49
|
+
},
|
|
50
|
+
"script": "02-merge-commits-in-range.sh"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"id": "split-commit-to-working-tree",
|
|
54
|
+
"name": "Split Commit To Working Tree",
|
|
55
|
+
"nameI18n": {
|
|
56
|
+
"en-US": "Split Commit To Working Tree",
|
|
57
|
+
"zh-CN": "将提交拆回工作区"
|
|
58
|
+
},
|
|
59
|
+
"script": "03-split-commit-to-working-tree.sh"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"id": "recommit-selected-path",
|
|
63
|
+
"name": "Recommit Selected Path",
|
|
64
|
+
"nameI18n": {
|
|
65
|
+
"en-US": "Recommit Selected Path",
|
|
66
|
+
"zh-CN": "按路径重新提交"
|
|
67
|
+
},
|
|
68
|
+
"script": "04-recommit-selected-path.sh"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"id": "continue-rewrite",
|
|
72
|
+
"name": "Continue Rewrite",
|
|
73
|
+
"nameI18n": {
|
|
74
|
+
"en-US": "Continue Rewrite",
|
|
75
|
+
"zh-CN": "继续历史重写"
|
|
76
|
+
},
|
|
77
|
+
"script": "05-continue-rewrite.sh"
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
wf require WF_TARGET_FOLDER
|
|
3
|
+
wf require WF_BASE_REF
|
|
4
|
+
echo "[Status] Exporting patch set from $WF_BASE_REF for folder $WF_TARGET_FOLDER..."
|
|
5
|
+
git format-patch "$WF_BASE_REF" -- "$WF_TARGET_FOLDER" -o patches_out/
|
|
6
|
+
echo "[Status] Patch files exported to patches_out"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "patch-export-for-review",
|
|
3
|
+
"name": "Patch Export For Review",
|
|
4
|
+
"nameI18n": {
|
|
5
|
+
"en-US": "Patch Export For Review",
|
|
6
|
+
"zh-CN": "补丁导出评审"
|
|
7
|
+
},
|
|
8
|
+
"descriptionI18n": {
|
|
9
|
+
"en-US": "Export folder-scoped patch series for review or archival.",
|
|
10
|
+
"zh-CN": "按目录导出补丁序列,用于评审或归档。"
|
|
11
|
+
},
|
|
12
|
+
"preflightScript": "00-preflight.sh",
|
|
13
|
+
"storageFields": [
|
|
14
|
+
{
|
|
15
|
+
"key": "lastPatchBaseRef",
|
|
16
|
+
"type": "string",
|
|
17
|
+
"required": false,
|
|
18
|
+
"descriptionI18n": {
|
|
19
|
+
"en-US": "Most recently used base ref for format-patch.",
|
|
20
|
+
"zh-CN": "最近一次生成 patch 使用的基准引用。"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"key": "lastPatchTargetFolder",
|
|
25
|
+
"type": "path",
|
|
26
|
+
"required": false,
|
|
27
|
+
"descriptionI18n": {
|
|
28
|
+
"en-US": "Most recently exported target folder.",
|
|
29
|
+
"zh-CN": "最近一次导出补丁的目标目录。"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"steps": [
|
|
34
|
+
{
|
|
35
|
+
"id": "ensure-patch-out-dir",
|
|
36
|
+
"name": "Ensure Patch Output Directory",
|
|
37
|
+
"nameI18n": {
|
|
38
|
+
"en-US": "Ensure Patch Output Directory",
|
|
39
|
+
"zh-CN": "准备补丁输出目录"
|
|
40
|
+
},
|
|
41
|
+
"script": "01-ensure-patch-out-dir.sh"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "format-patch-by-folder",
|
|
45
|
+
"name": "Format Patch By Folder",
|
|
46
|
+
"nameI18n": {
|
|
47
|
+
"en-US": "Format Patch By Folder",
|
|
48
|
+
"zh-CN": "按目录导出补丁"
|
|
49
|
+
},
|
|
50
|
+
"script": "02-format-patch-by-folder.sh"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"id": "list-generated-patches",
|
|
54
|
+
"name": "List Generated Patches",
|
|
55
|
+
"nameI18n": {
|
|
56
|
+
"en-US": "List Generated Patches",
|
|
57
|
+
"zh-CN": "列出已生成补丁"
|
|
58
|
+
},
|
|
59
|
+
"script": "03-list-generated-patches.sh"
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
|
|
3
|
+
wf require WF_SHARED_REPO_PATH
|
|
4
|
+
wf test is-git-repo "$WF_SHARED_REPO_PATH"
|
|
5
|
+
wf test is-git-detached "$WF_SHARED_REPO_PATH"
|
|
6
|
+
wf test is-git-dirty "$WF_SHARED_REPO_PATH"
|
|
7
|
+
|
|
8
|
+
if [ -n "${WF_REPO_PATHS:-}" ]; then
|
|
9
|
+
printf '%s\n' "$WF_REPO_PATHS" | while IFS= read -r repo; do
|
|
10
|
+
[ -z "$repo" ] && continue
|
|
11
|
+
if ! wf check is-git-repo "$repo"; then
|
|
12
|
+
continue
|
|
13
|
+
fi
|
|
14
|
+
wf test is-git-detached "$repo"
|
|
15
|
+
wf test is-git-dirty "$repo"
|
|
16
|
+
done
|
|
17
|
+
fi
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
wf require WF_SHARED_REPO_PATH
|
|
3
|
+
wf require WF_REMOTE_BASE_BRANCH
|
|
4
|
+
echo "[Status] Syncing shared repo onto remote base branch..."
|
|
5
|
+
wf test is-git-repo "$WF_SHARED_REPO_PATH"
|
|
6
|
+
|
|
7
|
+
remote_branch="${WF_REMOTE_BASE_BRANCH:-main}"
|
|
8
|
+
echo "[Status] Sync target remote branch: origin/$remote_branch"
|
|
9
|
+
git -C "$WF_SHARED_REPO_PATH" fetch origin
|
|
10
|
+
wf test is-git-commit "$WF_SHARED_REPO_PATH" "origin/$remote_branch"
|
|
11
|
+
git -C "$WF_SHARED_REPO_PATH" rebase "origin/$remote_branch"
|
|
12
|
+
|
|
13
|
+
shared_head="$(git -C "$WF_SHARED_REPO_PATH" rev-parse HEAD)"
|
|
14
|
+
handoff_file="$WF_SHARED_REPO_PATH/.cloverflow-last-shared-head"
|
|
15
|
+
printf '%s\n' "$shared_head" > "$handoff_file"
|
|
16
|
+
echo "[Status] Shared repo synced to: $shared_head"
|
|
17
|
+
echo "[Status] Handoff file updated: $handoff_file"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
wf require WF_TARGET_FOLDER
|
|
3
|
+
wf require WF_TARGET_REPO_PATH
|
|
4
|
+
wf require WF_SHARED_REPO_PATH
|
|
5
|
+
wf test is-git-repo "$WF_TARGET_REPO_PATH"
|
|
6
|
+
wf test is-git-repo "$WF_SHARED_REPO_PATH"
|
|
7
|
+
wf test is-path-exists "$WF_TARGET_FOLDER"
|
|
8
|
+
echo "[Status] Generating exported patch files for $WF_TARGET_FOLDER..."
|
|
9
|
+
mkdir -p "$WF_TARGET_REPO_PATH/patches_out"
|
|
10
|
+
git -C "$WF_TARGET_REPO_PATH" format-patch -o "$WF_TARGET_REPO_PATH/patches_out/" -1 HEAD -- "$WF_TARGET_FOLDER"
|
|
11
|
+
echo "[Status] Patch sequence generated."
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "shared-origin-sync-workflow",
|
|
3
|
+
"name": "Shared Origin Sync Workflow",
|
|
4
|
+
"nameI18n": {
|
|
5
|
+
"en-US": "Shared Origin Sync Workflow",
|
|
6
|
+
"zh-CN": "共享远端同步工作流"
|
|
7
|
+
},
|
|
8
|
+
"description": "Profile for syncing shared repo with origin and handling remote push/export flow.",
|
|
9
|
+
"descriptionI18n": {
|
|
10
|
+
"en-US": "Profile for syncing shared repo with origin and handling remote push/export flow.",
|
|
11
|
+
"zh-CN": "用于同步共享仓库与 origin,并处理推送和补丁导出流程。"
|
|
12
|
+
},
|
|
13
|
+
"preflightScript": "00-preflight.sh",
|
|
14
|
+
"storageFields": [
|
|
15
|
+
{
|
|
16
|
+
"key": "repoPaths",
|
|
17
|
+
"type": "pathList",
|
|
18
|
+
"required": true,
|
|
19
|
+
"linkSource": "profileState",
|
|
20
|
+
"description": "Locally persisted repository folder paths used by this profile.",
|
|
21
|
+
"descriptionI18n": {
|
|
22
|
+
"en-US": "Locally persisted repository folder paths used by this profile.",
|
|
23
|
+
"zh-CN": "该 profile 在本地保存的仓库目录路径列表。"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"key": "lastTargetFolder",
|
|
28
|
+
"type": "path",
|
|
29
|
+
"required": false,
|
|
30
|
+
"description": "Last target folder used when running steps.",
|
|
31
|
+
"descriptionI18n": {
|
|
32
|
+
"en-US": "Last target folder used when running steps.",
|
|
33
|
+
"zh-CN": "上次执行步骤时使用的目标文件夹。"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"key": "lastTargetRepoPath",
|
|
38
|
+
"type": "path",
|
|
39
|
+
"required": false,
|
|
40
|
+
"description": "Last target repository path used by patch export.",
|
|
41
|
+
"descriptionI18n": {
|
|
42
|
+
"en-US": "Last target repository path used by patch export.",
|
|
43
|
+
"zh-CN": "补丁导出步骤最近使用的目标仓库路径。"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"key": "lastPatchScope",
|
|
48
|
+
"type": "string",
|
|
49
|
+
"required": false,
|
|
50
|
+
"description": "Last path scope used by patch export (for example: . or src/module).",
|
|
51
|
+
"descriptionI18n": {
|
|
52
|
+
"en-US": "Last path scope used by patch export (for example: . or src/module).",
|
|
53
|
+
"zh-CN": "补丁导出最近使用的路径范围(例如 . 或 src/module)。"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"key": "sharedRepoPath",
|
|
58
|
+
"type": "path",
|
|
59
|
+
"required": true,
|
|
60
|
+
"description": "Shared source repository used as baseline.",
|
|
61
|
+
"descriptionI18n": {
|
|
62
|
+
"en-US": "Shared source repository used as baseline.",
|
|
63
|
+
"zh-CN": "作为共享基线的共享源仓库路径。"
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"key": "remoteBaseBranch",
|
|
68
|
+
"type": "string",
|
|
69
|
+
"required": false,
|
|
70
|
+
"description": "Remote base branch used for sync (default: main).",
|
|
71
|
+
"descriptionI18n": {
|
|
72
|
+
"en-US": "Remote base branch used for sync (default: main).",
|
|
73
|
+
"zh-CN": "同步使用的远端基线分支(默认 main)。"
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"key": "lastSharedHead",
|
|
78
|
+
"type": "string",
|
|
79
|
+
"required": false,
|
|
80
|
+
"description": "Optional pinned shared HEAD for cross-profile handoff.",
|
|
81
|
+
"descriptionI18n": {
|
|
82
|
+
"en-US": "Optional pinned shared HEAD for cross-profile handoff.",
|
|
83
|
+
"zh-CN": "跨 profile 交接时可选记录的共享基线提交。"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
],
|
|
87
|
+
"steps": [
|
|
88
|
+
{
|
|
89
|
+
"id": "sync-shared-origin",
|
|
90
|
+
"name": "Sync Shared Origin",
|
|
91
|
+
"nameI18n": {
|
|
92
|
+
"en-US": "Sync Shared Origin",
|
|
93
|
+
"zh-CN": "同步共享基线"
|
|
94
|
+
},
|
|
95
|
+
"paramBindings": [
|
|
96
|
+
{
|
|
97
|
+
"paramKey": "SHARED_REPO_PATH",
|
|
98
|
+
"storageKey": "sharedRepoPath",
|
|
99
|
+
"valueSource": "storageDirect"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"paramKey": "REMOTE_BASE_BRANCH",
|
|
103
|
+
"storageKey": "remoteBaseBranch",
|
|
104
|
+
"valueSource": "storageDirect"
|
|
105
|
+
}
|
|
106
|
+
],
|
|
107
|
+
"script": "01-sync-shared-origin.sh"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"id": "push-origin",
|
|
111
|
+
"name": "Push Origin",
|
|
112
|
+
"nameI18n": {
|
|
113
|
+
"en-US": "Push Origin",
|
|
114
|
+
"zh-CN": "推送提交"
|
|
115
|
+
},
|
|
116
|
+
"paramBindings": [
|
|
117
|
+
{
|
|
118
|
+
"paramKey": "SHARED_REPO_PATH",
|
|
119
|
+
"storageKey": "sharedRepoPath",
|
|
120
|
+
"valueSource": "storageDirect"
|
|
121
|
+
}
|
|
122
|
+
],
|
|
123
|
+
"script": "02-push-origin.sh"
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"id": "generate-patches",
|
|
127
|
+
"name": "Generate Patches",
|
|
128
|
+
"nameI18n": {
|
|
129
|
+
"en-US": "Generate Patches",
|
|
130
|
+
"zh-CN": "生成补丁"
|
|
131
|
+
},
|
|
132
|
+
"paramBindings": [
|
|
133
|
+
{
|
|
134
|
+
"paramKey": "TARGET_FOLDER",
|
|
135
|
+
"storageKey": "lastPatchScope"
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"paramKey": "TARGET_REPO_PATH",
|
|
139
|
+
"storageKey": "lastTargetRepoPath",
|
|
140
|
+
"uiControl": "select",
|
|
141
|
+
"optionsFromStorageKey": "repoPaths",
|
|
142
|
+
"placeholderI18n": {
|
|
143
|
+
"en-US": "Select a target repository",
|
|
144
|
+
"zh-CN": "请选择目标仓库"
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"paramKey": "SHARED_REPO_PATH",
|
|
149
|
+
"storageKey": "sharedRepoPath",
|
|
150
|
+
"valueSource": "storageDirect"
|
|
151
|
+
}
|
|
152
|
+
],
|
|
153
|
+
"script": "03-generate-patches.sh"
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
}
|