kantban-cli 0.1.51 → 0.1.52
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/dist/{chunk-X2CJ3ZAI.js → chunk-3A4B7CUH.js} +215 -25
- package/dist/chunk-3A4B7CUH.js.map +1 -0
- package/dist/{cron-OCKARAAM.js → cron-VZMNM2XT.js} +2 -2
- package/dist/index.js +3 -3
- package/dist/{pipeline-6J64Z7VH.js → pipeline-NRG2Q2TE.js} +56 -194
- package/dist/pipeline-NRG2Q2TE.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-X2CJ3ZAI.js.map +0 -1
- package/dist/pipeline-6J64Z7VH.js.map +0 -1
- /package/dist/{cron-OCKARAAM.js.map → cron-VZMNM2XT.js.map} +0 -0
|
@@ -5,10 +5,155 @@ import {
|
|
|
5
5
|
crossSpawnOptions,
|
|
6
6
|
defaultPath,
|
|
7
7
|
killProcessTree,
|
|
8
|
+
normalizeEol,
|
|
8
9
|
npxCommand,
|
|
9
10
|
resolveCommand
|
|
10
11
|
} from "./chunk-5ZU2OOES.js";
|
|
11
12
|
|
|
13
|
+
// src/lib/worktree.ts
|
|
14
|
+
import { execFile as defaultExecFile, execFileSync } from "child_process";
|
|
15
|
+
import { homedir } from "os";
|
|
16
|
+
import { join } from "path";
|
|
17
|
+
function generateWorktreeName(ticketNumber, columnName) {
|
|
18
|
+
const slug = columnSlug(columnName);
|
|
19
|
+
return `kantban-${ticketNumber}-${slug}`;
|
|
20
|
+
}
|
|
21
|
+
function columnSlug(columnName) {
|
|
22
|
+
return columnName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
23
|
+
}
|
|
24
|
+
function renderWorktreePath(ticketNumber, columnName, pathPattern, options) {
|
|
25
|
+
const slug = columnSlug(columnName);
|
|
26
|
+
const worktreeName = generateWorktreeName(ticketNumber, columnName);
|
|
27
|
+
if (pathPattern) {
|
|
28
|
+
return pathPattern.replace(/\{ticket_number\}/g, String(ticketNumber)).replace(/\{column_slug\}/g, slug).replace(/\{worktree_name\}/g, worktreeName);
|
|
29
|
+
}
|
|
30
|
+
return join(options.defaultRoot, options.boardId, slug, String(ticketNumber));
|
|
31
|
+
}
|
|
32
|
+
function defaultWorktreeRoot() {
|
|
33
|
+
return join(homedir(), ".kantban", "worktrees");
|
|
34
|
+
}
|
|
35
|
+
function isPlausibleRemoteUrl(url) {
|
|
36
|
+
return url.includes("/") || url.includes("@") || url.includes("://");
|
|
37
|
+
}
|
|
38
|
+
function ensureWorktreeRemote(worktreePath) {
|
|
39
|
+
try {
|
|
40
|
+
const remotes = normalizeEol(execFileSync("git", ["-C", worktreePath, "remote"], {
|
|
41
|
+
stdio: "pipe",
|
|
42
|
+
encoding: "utf-8"
|
|
43
|
+
})).trim();
|
|
44
|
+
if (remotes.split("\n").includes("origin")) {
|
|
45
|
+
const currentUrl = normalizeEol(execFileSync("git", ["-C", worktreePath, "remote", "get-url", "origin"], {
|
|
46
|
+
stdio: "pipe",
|
|
47
|
+
encoding: "utf-8"
|
|
48
|
+
})).trim();
|
|
49
|
+
if (isPlausibleRemoteUrl(currentUrl)) return;
|
|
50
|
+
console.error(`[worktree] Invalid origin URL in ${worktreePath}: "${currentUrl}" \u2014 fixing`);
|
|
51
|
+
execFileSync("git", ["-C", worktreePath, "remote", "remove", "origin"], {
|
|
52
|
+
stdio: "pipe"
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const originUrl = normalizeEol(execFileSync("git", ["remote", "get-url", "origin"], {
|
|
56
|
+
stdio: "pipe",
|
|
57
|
+
encoding: "utf-8"
|
|
58
|
+
})).trim();
|
|
59
|
+
if (originUrl && isPlausibleRemoteUrl(originUrl)) {
|
|
60
|
+
execFileSync("git", ["-C", worktreePath, "remote", "add", "origin", originUrl], {
|
|
61
|
+
stdio: "pipe"
|
|
62
|
+
});
|
|
63
|
+
console.error(`[worktree] Added missing origin remote to ${worktreePath}: ${originUrl}`);
|
|
64
|
+
} else {
|
|
65
|
+
console.error(`[worktree] WARNING: main repo origin URL is also invalid: "${originUrl}"`);
|
|
66
|
+
}
|
|
67
|
+
} catch (err) {
|
|
68
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
69
|
+
console.error(`[worktree] Failed to ensure remote for ${worktreePath}: ${msg}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function cleanupWorktree(worktreeName, exec = defaultExecFile) {
|
|
73
|
+
let target = worktreeName;
|
|
74
|
+
try {
|
|
75
|
+
const path = await findWorktreeForBranch(exec, worktreeName);
|
|
76
|
+
if (!path) return true;
|
|
77
|
+
target = path;
|
|
78
|
+
} catch {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
return new Promise((resolve) => {
|
|
82
|
+
exec("git", ["worktree", "remove", "--force", target], (err) => {
|
|
83
|
+
if (err) {
|
|
84
|
+
console.error(`[worktree] cleanup failed for ${worktreeName} (${target}): ${err.message}`);
|
|
85
|
+
resolve(false);
|
|
86
|
+
} else {
|
|
87
|
+
resolve(true);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function execPromise(exec, cmd, args) {
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
exec(cmd, args, (err, stdout, stderr) => {
|
|
95
|
+
if (err) reject(Object.assign(err, { stdout, stderr }));
|
|
96
|
+
else resolve({ stdout, stderr });
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
async function findWorktreeForBranch(exec, branch) {
|
|
101
|
+
try {
|
|
102
|
+
const { stdout } = await execPromise(exec, "git", ["worktree", "list", "--porcelain"]);
|
|
103
|
+
const targetRef = `refs/heads/${branch}`;
|
|
104
|
+
let currentPath = null;
|
|
105
|
+
for (const line of normalizeEol(stdout).split("\n")) {
|
|
106
|
+
if (line.startsWith("worktree ")) currentPath = line.slice("worktree ".length);
|
|
107
|
+
if (line.startsWith("branch ") && line.slice("branch ".length) === targetRef && currentPath) {
|
|
108
|
+
return currentPath;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function mergeWorktreeBranch(worktreeName, integrationBranch, exec = defaultExecFile) {
|
|
117
|
+
try {
|
|
118
|
+
try {
|
|
119
|
+
await execPromise(exec, "git", ["rev-parse", "--verify", worktreeName]);
|
|
120
|
+
} catch {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
await execPromise(exec, "git", ["branch", integrationBranch, "HEAD"]).catch(() => {
|
|
124
|
+
});
|
|
125
|
+
const checkedOutPath = await findWorktreeForBranch(exec, integrationBranch);
|
|
126
|
+
if (checkedOutPath) {
|
|
127
|
+
await execPromise(exec, "git", ["-C", checkedOutPath, "merge", "--no-edit", worktreeName]);
|
|
128
|
+
console.error(`[worktree] merged ${worktreeName} \u2192 ${integrationBranch}`);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
const { stdout: baseOut } = await execPromise(exec, "git", ["merge-base", integrationBranch, worktreeName]);
|
|
132
|
+
const mergeBase = baseOut.trim();
|
|
133
|
+
const { stdout: integrationSha } = await execPromise(exec, "git", ["rev-parse", integrationBranch]);
|
|
134
|
+
if (integrationSha.trim() === mergeBase) {
|
|
135
|
+
const { stdout: worktreeSha } = await execPromise(exec, "git", ["rev-parse", worktreeName]);
|
|
136
|
+
await execPromise(exec, "git", ["update-ref", `refs/heads/${integrationBranch}`, worktreeSha.trim()]);
|
|
137
|
+
console.error(`[worktree] fast-forward merged ${worktreeName} \u2192 ${integrationBranch}`);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
const tmpWorktree = `merge-tmp-${Date.now()}`;
|
|
141
|
+
try {
|
|
142
|
+
await execPromise(exec, "git", ["worktree", "add", tmpWorktree, integrationBranch]);
|
|
143
|
+
await execPromise(exec, "git", ["-C", tmpWorktree, "merge", "--no-edit", worktreeName]);
|
|
144
|
+
console.error(`[worktree] merged ${worktreeName} \u2192 ${integrationBranch}`);
|
|
145
|
+
} finally {
|
|
146
|
+
await execPromise(exec, "git", ["worktree", "remove", "--force", tmpWorktree]).catch(() => {
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return true;
|
|
150
|
+
} catch (err) {
|
|
151
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
152
|
+
console.error(`[worktree] merge failed for ${worktreeName} \u2192 ${integrationBranch}: ${msg}`);
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
12
157
|
// src/lib/stuck-detector.ts
|
|
13
158
|
import { z } from "zod";
|
|
14
159
|
|
|
@@ -971,13 +1116,13 @@ function fingerprintsMatch(a, b) {
|
|
|
971
1116
|
|
|
972
1117
|
// src/lib/mcp-config.ts
|
|
973
1118
|
import { writeFileSync, unlinkSync, mkdirSync, existsSync, readdirSync, rmdirSync, rmSync } from "fs";
|
|
974
|
-
import { join, dirname } from "path";
|
|
1119
|
+
import { join as join2, dirname } from "path";
|
|
975
1120
|
import { fileURLToPath } from "url";
|
|
976
|
-
import { homedir } from "os";
|
|
1121
|
+
import { homedir as homedir2 } from "os";
|
|
977
1122
|
var __filename = fileURLToPath(import.meta.url);
|
|
978
1123
|
var __dirname = dirname(__filename);
|
|
979
1124
|
function generateMcpConfig(apiUrl, apiToken, boardId) {
|
|
980
|
-
const localMcpPath = existsSync(
|
|
1125
|
+
const localMcpPath = existsSync(join2(__dirname, "..", "..", "mcp", "dist", "index.js")) ? join2(__dirname, "..", "..", "mcp", "dist", "index.js") : join2(__dirname, "..", "..", "..", "mcp", "dist", "index.js");
|
|
981
1126
|
const useLocal = existsSync(localMcpPath);
|
|
982
1127
|
const kantbanServer = useLocal ? {
|
|
983
1128
|
command: "node",
|
|
@@ -999,9 +1144,9 @@ function generateMcpConfig(apiUrl, apiToken, boardId) {
|
|
|
999
1144
|
kantban: kantbanServer
|
|
1000
1145
|
}
|
|
1001
1146
|
};
|
|
1002
|
-
const dir =
|
|
1147
|
+
const dir = join2(homedir2(), ".kantban", "pipelines", boardId, String(process.pid));
|
|
1003
1148
|
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
1004
|
-
const filePath =
|
|
1149
|
+
const filePath = join2(dir, "mcp-config.json");
|
|
1005
1150
|
writeFileSync(filePath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
1006
1151
|
return filePath;
|
|
1007
1152
|
}
|
|
@@ -1018,10 +1163,10 @@ function cleanupMcpConfig(filePath) {
|
|
|
1018
1163
|
}
|
|
1019
1164
|
}
|
|
1020
1165
|
function generateGateProxyMcpConfig(apiUrl, apiToken, boardId, gateConfigPath, columnId, columnName, projectId, gateCwd, ticketId) {
|
|
1021
|
-
const localMcpPath = existsSync(
|
|
1166
|
+
const localMcpPath = existsSync(join2(__dirname, "..", "..", "mcp", "dist", "index.js")) ? join2(__dirname, "..", "..", "mcp", "dist", "index.js") : join2(__dirname, "..", "..", "..", "mcp", "dist", "index.js");
|
|
1022
1167
|
const useLocal = existsSync(localMcpPath);
|
|
1023
1168
|
const kantbanServer = useLocal ? { command: "node", args: [localMcpPath], env: { KANTBAN_API_TOKEN: apiToken, KANTBAN_API_URL: apiUrl, KANTBAN_HIDDEN_TOOLS: "kantban_move_ticket,kantban_move_tickets,kantban_complete_task,kantban_move_to_board" } } : { command: npxCommand(), args: ["-y", "kantban-mcp@latest"], env: { KANTBAN_API_TOKEN: apiToken, KANTBAN_API_URL: apiUrl, KANTBAN_HIDDEN_TOOLS: "kantban_move_ticket,kantban_move_tickets,kantban_complete_task,kantban_move_to_board" } };
|
|
1024
|
-
const gateProxyPath = existsSync(
|
|
1169
|
+
const gateProxyPath = existsSync(join2(__dirname, "lib", "gate-proxy-server.js")) ? join2(__dirname, "lib", "gate-proxy-server.js") : join2(__dirname, "gate-proxy-server.js");
|
|
1025
1170
|
const gateProxyEnv = {
|
|
1026
1171
|
GATE_CONFIG_PATH: gateConfigPath,
|
|
1027
1172
|
COLUMN_ID: columnId,
|
|
@@ -1047,10 +1192,10 @@ function generateGateProxyMcpConfig(apiUrl, apiToken, boardId, gateConfigPath, c
|
|
|
1047
1192
|
"kantban-gates": gateProxyServer
|
|
1048
1193
|
}
|
|
1049
1194
|
};
|
|
1050
|
-
const dir =
|
|
1195
|
+
const dir = join2(homedir2(), ".kantban", "pipelines", boardId, String(process.pid));
|
|
1051
1196
|
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
1052
1197
|
const suffix = ticketId ? `${columnId}-${ticketId}` : columnId;
|
|
1053
|
-
const filePath =
|
|
1198
|
+
const filePath = join2(dir, `mcp-config-${suffix}.json`);
|
|
1054
1199
|
writeFileSync(filePath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
1055
1200
|
return filePath;
|
|
1056
1201
|
}
|
|
@@ -1060,7 +1205,7 @@ function cleanupGateProxyConfigs(pipelineDir) {
|
|
|
1060
1205
|
for (const f of files) {
|
|
1061
1206
|
if (f.startsWith("mcp-config-") && f.endsWith(".json")) {
|
|
1062
1207
|
try {
|
|
1063
|
-
unlinkSync(
|
|
1208
|
+
unlinkSync(join2(pipelineDir, f));
|
|
1064
1209
|
} catch {
|
|
1065
1210
|
}
|
|
1066
1211
|
}
|
|
@@ -1088,7 +1233,7 @@ function reapOrphanedMcpConfigDirs(boardDir) {
|
|
|
1088
1233
|
}
|
|
1089
1234
|
if (!alive) {
|
|
1090
1235
|
try {
|
|
1091
|
-
rmSync(
|
|
1236
|
+
rmSync(join2(boardDir, entry), { recursive: true, force: true });
|
|
1092
1237
|
} catch {
|
|
1093
1238
|
}
|
|
1094
1239
|
}
|
|
@@ -1096,10 +1241,10 @@ function reapOrphanedMcpConfigDirs(boardDir) {
|
|
|
1096
1241
|
}
|
|
1097
1242
|
|
|
1098
1243
|
// src/providers/claude-provider.ts
|
|
1099
|
-
import { spawn } from "child_process";
|
|
1100
|
-
import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
1101
|
-
import { join as
|
|
1102
|
-
import { homedir as
|
|
1244
|
+
import { spawn, execFileSync as execFileSync2 } from "child_process";
|
|
1245
|
+
import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2, rmSync as rmSync2 } from "fs";
|
|
1246
|
+
import { join as join3 } from "path";
|
|
1247
|
+
import { homedir as homedir3 } from "os";
|
|
1103
1248
|
|
|
1104
1249
|
// src/providers/claude-stream-parser.ts
|
|
1105
1250
|
var ClaudeStreamParser = class {
|
|
@@ -1202,7 +1347,7 @@ var ClaudeProvider = class {
|
|
|
1202
1347
|
supportsMaxTurns: true,
|
|
1203
1348
|
supportsMcpConfigInjection: true,
|
|
1204
1349
|
supportsMcpConfigOverride: false,
|
|
1205
|
-
supportsWorktreeFlag:
|
|
1350
|
+
supportsWorktreeFlag: false,
|
|
1206
1351
|
supportsSandboxModes: false,
|
|
1207
1352
|
supportedModels: [
|
|
1208
1353
|
{ id: "claude-haiku-4-5-20251001", displayName: "Haiku 4.5", tier: "fast" },
|
|
@@ -1215,13 +1360,56 @@ var ClaudeProvider = class {
|
|
|
1215
1360
|
async invoke(request) {
|
|
1216
1361
|
const args = this.buildArgs(request);
|
|
1217
1362
|
const startTime = Date.now();
|
|
1363
|
+
if (request.workingDirectory) {
|
|
1364
|
+
const branch = request.branch ?? request.workingDirectory;
|
|
1365
|
+
if (!existsSync2(request.workingDirectory)) {
|
|
1366
|
+
try {
|
|
1367
|
+
execFileSync2("git", ["worktree", "add", "-b", branch, request.workingDirectory, "HEAD"], {
|
|
1368
|
+
stdio: "pipe"
|
|
1369
|
+
});
|
|
1370
|
+
} catch {
|
|
1371
|
+
try {
|
|
1372
|
+
execFileSync2("git", ["worktree", "add", request.workingDirectory, branch], {
|
|
1373
|
+
stdio: "pipe"
|
|
1374
|
+
});
|
|
1375
|
+
} catch (err) {
|
|
1376
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1377
|
+
throw new Error(`worktree_creation_failed: ${msg}`);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
} else {
|
|
1381
|
+
try {
|
|
1382
|
+
execFileSync2("git", ["-C", request.workingDirectory, "rev-parse", "--git-dir"], {
|
|
1383
|
+
stdio: "pipe"
|
|
1384
|
+
});
|
|
1385
|
+
} catch {
|
|
1386
|
+
try {
|
|
1387
|
+
rmSync2(request.workingDirectory, { recursive: true, force: true });
|
|
1388
|
+
execFileSync2("git", ["worktree", "add", "-b", branch, request.workingDirectory, "HEAD"], {
|
|
1389
|
+
stdio: "pipe"
|
|
1390
|
+
});
|
|
1391
|
+
} catch {
|
|
1392
|
+
try {
|
|
1393
|
+
execFileSync2("git", ["worktree", "add", request.workingDirectory, branch], {
|
|
1394
|
+
stdio: "pipe"
|
|
1395
|
+
});
|
|
1396
|
+
} catch (err) {
|
|
1397
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1398
|
+
throw new Error(`worktree_creation_failed: ${msg}`);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
if (existsSync2(request.workingDirectory)) {
|
|
1404
|
+
ensureWorktreeRemote(request.workingDirectory);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1218
1407
|
const [cmd, prefixArgs] = resolveCommand("claude");
|
|
1219
1408
|
const resolvedArgs = [...prefixArgs, ...args];
|
|
1220
1409
|
return new Promise((resolve) => {
|
|
1221
1410
|
const child = spawn(cmd, resolvedArgs, {
|
|
1222
1411
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1223
|
-
|
|
1224
|
-
// Claude Code's --worktree flag creates it internally.
|
|
1412
|
+
...request.workingDirectory ? { cwd: request.workingDirectory } : {},
|
|
1225
1413
|
// Only use shell:true as fallback when resolveCommand couldn't resolve
|
|
1226
1414
|
...prefixArgs.length > 0 ? {} : crossSpawnOptions()
|
|
1227
1415
|
});
|
|
@@ -1300,10 +1488,6 @@ var ClaudeProvider = class {
|
|
|
1300
1488
|
if (request.maxTurns) {
|
|
1301
1489
|
args.push("--max-turns", String(request.maxTurns));
|
|
1302
1490
|
}
|
|
1303
|
-
const worktreeName = request.branch ?? request.workingDirectory;
|
|
1304
|
-
if (worktreeName) {
|
|
1305
|
-
args.push("--worktree", worktreeName);
|
|
1306
|
-
}
|
|
1307
1491
|
if (request.toolRestrictions) {
|
|
1308
1492
|
const tr = request.toolRestrictions;
|
|
1309
1493
|
if (tr.tools !== void 0) args.push("--tools", tr.tools);
|
|
@@ -1315,15 +1499,21 @@ var ClaudeProvider = class {
|
|
|
1315
1499
|
writeMcpConfigJson(mcpConfig) {
|
|
1316
1500
|
if (!mcpConfig) return "";
|
|
1317
1501
|
const config = { mcpServers: mcpConfig.servers };
|
|
1318
|
-
const dir =
|
|
1502
|
+
const dir = join3(homedir3(), ".kantban", "tmp");
|
|
1319
1503
|
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
1320
|
-
const filePath =
|
|
1504
|
+
const filePath = join3(dir, `mcp-config-${Date.now()}.json`);
|
|
1321
1505
|
writeFileSync2(filePath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
1322
1506
|
return filePath;
|
|
1323
1507
|
}
|
|
1324
1508
|
};
|
|
1325
1509
|
|
|
1326
1510
|
export {
|
|
1511
|
+
generateWorktreeName,
|
|
1512
|
+
renderWorktreePath,
|
|
1513
|
+
defaultWorktreeRoot,
|
|
1514
|
+
ensureWorktreeRemote,
|
|
1515
|
+
cleanupWorktree,
|
|
1516
|
+
mergeWorktreeBranch,
|
|
1327
1517
|
parseJsonFromLlmOutput,
|
|
1328
1518
|
composeStuckDetectionPrompt,
|
|
1329
1519
|
parseStuckDetectionResponse,
|
|
@@ -1337,4 +1527,4 @@ export {
|
|
|
1337
1527
|
reapOrphanedMcpConfigDirs,
|
|
1338
1528
|
ClaudeProvider
|
|
1339
1529
|
};
|
|
1340
|
-
//# sourceMappingURL=chunk-
|
|
1530
|
+
//# sourceMappingURL=chunk-3A4B7CUH.js.map
|