@runuai/host 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +91 -0
- package/bin/uai-host.mjs +14 -0
- package/db/migrations/0000_host_tasks.sql +12 -0
- package/db/migrations/0001_host_ui.sql +11 -0
- package/db/migrations/0002_host_github_tokens.sql +8 -0
- package/db/migrations/0003_host_ssh_keys.sql +8 -0
- package/db/migrations/0004_host_owner_name.sql +1 -0
- package/db/migrations/meta/_journal.json +41 -0
- package/db/schema.ts +82 -0
- package/images/standard/Dockerfile +232 -0
- package/images/standard/README.md +122 -0
- package/images/standard/container/code-server-settings.json +36 -0
- package/images/standard/container/uai-init +215 -0
- package/images/standard/tool-versions +2 -0
- package/lib/agent.ts +292 -0
- package/lib/agents/claude.ts +343 -0
- package/lib/agents/codex.ts +522 -0
- package/lib/agents/factory.ts +34 -0
- package/lib/agents/mock.ts +133 -0
- package/lib/agents/proc.ts +172 -0
- package/lib/agents/registry.ts +109 -0
- package/lib/agents/types.ts +133 -0
- package/lib/attachments.ts +46 -0
- package/lib/cloud-state.ts +56 -0
- package/lib/command-db.ts +278 -0
- package/lib/db.ts +68 -0
- package/lib/env.ts +140 -0
- package/lib/git-diff.ts +370 -0
- package/lib/git-identity.ts +65 -0
- package/lib/github-tokens.ts +321 -0
- package/lib/orchestrator.ts +975 -0
- package/lib/preview-ports.ts +85 -0
- package/lib/repo-clone.ts +127 -0
- package/lib/runtime-state.ts +120 -0
- package/lib/secrets.ts +71 -0
- package/lib/ssh.ts +186 -0
- package/lib/standard-image.ts +152 -0
- package/lib/task-diff.ts +113 -0
- package/lib/task-status.ts +46 -0
- package/lib/transcript.ts +30 -0
- package/lib/ulid.ts +7 -0
- package/package.json +85 -0
- package/scripts/agent/_common.sh +248 -0
- package/scripts/agent/task-down.sh +113 -0
- package/scripts/agent/task-status.sh +54 -0
- package/scripts/agent/task-up.sh +457 -0
- package/scripts/install/darwin.ts +167 -0
- package/scripts/install/linux.ts +115 -0
- package/scripts/install/types.ts +35 -0
- package/scripts/install/util.ts +39 -0
- package/scripts/install/win.ts +130 -0
- package/src/cli.ts +445 -0
- package/src/index.ts +375 -0
- package/src/load-env.ts +52 -0
- package/src/main.ts +1156 -0
- package/src/paths.ts +64 -0
- package/src/protocol.ts +413 -0
- package/src/ui/server.ts +343 -0
- package/src/ui/types.ts +78 -0
- package/ui/app.js +264 -0
- package/ui/index.html +55 -0
- package/ui/style.css +359 -0
- package/ui/uai-logo-black.svg +9 -0
package/lib/git-diff.ts
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse `git diff` unified output into structured per-file patches.
|
|
3
|
+
*
|
|
4
|
+
* Input is whatever `git diff origin/<base>..HEAD` produces — multi-file,
|
|
5
|
+
* with headers, mode/rename/binary annotations, and `@@` hunks. The
|
|
6
|
+
* output is a list of `DiffFile`s the UI can render per-file with
|
|
7
|
+
* collapse/expand and syntax-highlighted hunks.
|
|
8
|
+
*
|
|
9
|
+
* Hand-rolled rather than pulled in as a dep — the unified-diff grammar
|
|
10
|
+
* is small and the chat-side Markdown renderer already sets the
|
|
11
|
+
* dependency-free precedent (see components/markdown.tsx).
|
|
12
|
+
*/
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
14
|
+
|
|
15
|
+
export type DiffStatus =
|
|
16
|
+
| "added"
|
|
17
|
+
| "deleted"
|
|
18
|
+
| "modified"
|
|
19
|
+
| "renamed"
|
|
20
|
+
| "copied"
|
|
21
|
+
| "binary";
|
|
22
|
+
|
|
23
|
+
export interface DiffLine {
|
|
24
|
+
/** ' ' for context, '+' for added, '-' for removed. */
|
|
25
|
+
kind: "ctx" | "add" | "del";
|
|
26
|
+
/** Line content without the leading marker. */
|
|
27
|
+
content: string;
|
|
28
|
+
/** Line number in the old file (1-based) for ctx/del lines. */
|
|
29
|
+
oldNo?: number;
|
|
30
|
+
/** Line number in the new file (1-based) for ctx/add lines. */
|
|
31
|
+
newNo?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface DiffHunk {
|
|
35
|
+
oldStart: number;
|
|
36
|
+
oldLines: number;
|
|
37
|
+
newStart: number;
|
|
38
|
+
newLines: number;
|
|
39
|
+
/** Raw `@@ ... @@` header text, useful for the section label git
|
|
40
|
+
* sometimes includes after the second `@@`. */
|
|
41
|
+
header: string;
|
|
42
|
+
lines: DiffLine[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface DiffFile {
|
|
46
|
+
path: string;
|
|
47
|
+
/** Set on rename / copy — the source path before the change. */
|
|
48
|
+
oldPath?: string;
|
|
49
|
+
status: DiffStatus;
|
|
50
|
+
/** Best-effort language hint based on the file extension. */
|
|
51
|
+
language?: string;
|
|
52
|
+
/** A `Binary files … differ` patch has no hunks. */
|
|
53
|
+
binary: boolean;
|
|
54
|
+
hunks: DiffHunk[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const GIT_DIFF_ARGS = [
|
|
58
|
+
"-c",
|
|
59
|
+
"core.quotepath=false",
|
|
60
|
+
"diff",
|
|
61
|
+
"--no-color",
|
|
62
|
+
"--no-ext-diff",
|
|
63
|
+
"--src-prefix=a/",
|
|
64
|
+
"--dst-prefix=b/",
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const NON_INTERACTIVE_GIT_ENV = {
|
|
68
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
69
|
+
GCM_INTERACTIVE: "never",
|
|
70
|
+
GIT_SSH_COMMAND: "ssh -o BatchMode=yes",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Run `git diff <range>` in `cwd` and return the raw unified-diff text.
|
|
75
|
+
* Throws if git exits non-zero. Empty diff returns an empty string.
|
|
76
|
+
*/
|
|
77
|
+
export function runGitDiff(cwd: string, range: string): Promise<string> {
|
|
78
|
+
return runDiffCommand(
|
|
79
|
+
"git",
|
|
80
|
+
["-C", cwd, ...GIT_DIFF_ARGS, range],
|
|
81
|
+
"git diff",
|
|
82
|
+
{
|
|
83
|
+
...process.env,
|
|
84
|
+
...NON_INTERACTIVE_GIT_ENV,
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Run `git diff <range>` inside a running task container.
|
|
91
|
+
*
|
|
92
|
+
* Active task worktrees are bind-mounted into the container at
|
|
93
|
+
* `/workspace`. Running diff there keeps all Git credential behavior
|
|
94
|
+
* inside the task sandbox instead of leaking to the host's SSH agent
|
|
95
|
+
* (for example macOS 1Password confirmation prompts).
|
|
96
|
+
*/
|
|
97
|
+
export function runGitDiffInContainer(
|
|
98
|
+
containerName: string,
|
|
99
|
+
cwd: string,
|
|
100
|
+
range: string,
|
|
101
|
+
): Promise<string> {
|
|
102
|
+
return runDiffCommand(
|
|
103
|
+
"docker",
|
|
104
|
+
[
|
|
105
|
+
"exec",
|
|
106
|
+
"-e",
|
|
107
|
+
`GIT_TERMINAL_PROMPT=${NON_INTERACTIVE_GIT_ENV.GIT_TERMINAL_PROMPT}`,
|
|
108
|
+
"-e",
|
|
109
|
+
`GCM_INTERACTIVE=${NON_INTERACTIVE_GIT_ENV.GCM_INTERACTIVE}`,
|
|
110
|
+
"-e",
|
|
111
|
+
`GIT_SSH_COMMAND=${NON_INTERACTIVE_GIT_ENV.GIT_SSH_COMMAND}`,
|
|
112
|
+
"-w",
|
|
113
|
+
cwd,
|
|
114
|
+
containerName,
|
|
115
|
+
"git",
|
|
116
|
+
...GIT_DIFF_ARGS,
|
|
117
|
+
range,
|
|
118
|
+
],
|
|
119
|
+
"docker exec git diff",
|
|
120
|
+
process.env,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function runDiffCommand(
|
|
125
|
+
command: string,
|
|
126
|
+
args: string[],
|
|
127
|
+
label: string,
|
|
128
|
+
env: NodeJS.ProcessEnv,
|
|
129
|
+
): Promise<string> {
|
|
130
|
+
return new Promise((res, rej) => {
|
|
131
|
+
// Force ASCII paths and pull a generous unified context so the UI
|
|
132
|
+
// can render meaningful blocks. No color codes.
|
|
133
|
+
const child = spawn(command, args, {
|
|
134
|
+
env,
|
|
135
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
136
|
+
});
|
|
137
|
+
let stdout = "";
|
|
138
|
+
let stderr = "";
|
|
139
|
+
child.stdout.on("data", (b) => {
|
|
140
|
+
stdout += b.toString("utf8");
|
|
141
|
+
});
|
|
142
|
+
child.stderr.on("data", (b) => {
|
|
143
|
+
stderr += b.toString("utf8");
|
|
144
|
+
});
|
|
145
|
+
child.on("error", rej);
|
|
146
|
+
child.on("close", (code) => {
|
|
147
|
+
if (code === 0) {
|
|
148
|
+
res(stdout);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
rej(new Error(`${label} exited ${code}: ${stderr.trim()}`));
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Parse the output of `git diff` into structured file patches.
|
|
158
|
+
*/
|
|
159
|
+
export function parseGitDiff(text: string): DiffFile[] {
|
|
160
|
+
if (!text) return [];
|
|
161
|
+
const lines = text.split("\n");
|
|
162
|
+
const files: DiffFile[] = [];
|
|
163
|
+
let i = 0;
|
|
164
|
+
|
|
165
|
+
while (i < lines.length) {
|
|
166
|
+
const line = lines[i] ?? "";
|
|
167
|
+
if (!line.startsWith("diff --git ")) {
|
|
168
|
+
i += 1;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
// Each file section starts with `diff --git a/<path> b/<path>` and
|
|
172
|
+
// runs to the next `diff --git` line (or EOF).
|
|
173
|
+
const header = parseDiffGitHeader(line);
|
|
174
|
+
let path = header.bPath ?? header.aPath ?? "";
|
|
175
|
+
let oldPath: string | undefined;
|
|
176
|
+
let status: DiffStatus = "modified";
|
|
177
|
+
let binary = false;
|
|
178
|
+
const hunks: DiffHunk[] = [];
|
|
179
|
+
|
|
180
|
+
i += 1;
|
|
181
|
+
while (i < lines.length && !(lines[i] ?? "").startsWith("diff --git ")) {
|
|
182
|
+
const ln = lines[i] ?? "";
|
|
183
|
+
if (ln.startsWith("new file mode")) status = "added";
|
|
184
|
+
else if (ln.startsWith("deleted file mode")) status = "deleted";
|
|
185
|
+
else if (ln.startsWith("rename from ")) {
|
|
186
|
+
status = "renamed";
|
|
187
|
+
oldPath = ln.slice("rename from ".length);
|
|
188
|
+
} else if (ln.startsWith("rename to ")) {
|
|
189
|
+
path = ln.slice("rename to ".length);
|
|
190
|
+
} else if (ln.startsWith("copy from ")) {
|
|
191
|
+
status = "copied";
|
|
192
|
+
oldPath = ln.slice("copy from ".length);
|
|
193
|
+
} else if (ln.startsWith("copy to ")) {
|
|
194
|
+
path = ln.slice("copy to ".length);
|
|
195
|
+
} else if (ln.startsWith("Binary files ")) {
|
|
196
|
+
binary = true;
|
|
197
|
+
status = status === "modified" ? "binary" : status;
|
|
198
|
+
} else if (ln.startsWith("--- ")) {
|
|
199
|
+
const p = stripPathPrefix(ln.slice(4));
|
|
200
|
+
if (p !== "/dev/null" && status === "modified") oldPath = oldPath ?? p;
|
|
201
|
+
if (p === "/dev/null") status = "added";
|
|
202
|
+
} else if (ln.startsWith("+++ ")) {
|
|
203
|
+
const p = stripPathPrefix(ln.slice(4));
|
|
204
|
+
if (p !== "/dev/null") path = p;
|
|
205
|
+
else status = "deleted";
|
|
206
|
+
} else if (ln.startsWith("@@")) {
|
|
207
|
+
const { hunk, consumed } = parseHunk(lines, i);
|
|
208
|
+
if (hunk) hunks.push(hunk);
|
|
209
|
+
i += consumed;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
i += 1;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
files.push({
|
|
216
|
+
path,
|
|
217
|
+
oldPath: oldPath && oldPath !== path ? oldPath : undefined,
|
|
218
|
+
status,
|
|
219
|
+
language: languageFor(path),
|
|
220
|
+
binary,
|
|
221
|
+
hunks,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return files;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function parseDiffGitHeader(line: string): {
|
|
229
|
+
aPath?: string;
|
|
230
|
+
bPath?: string;
|
|
231
|
+
} {
|
|
232
|
+
// `diff --git a/foo b/foo` — paths may contain spaces if quoted.
|
|
233
|
+
// Quoted form is `diff --git "a/path with space" "b/path with space"`.
|
|
234
|
+
// For simplicity (uai paths are well-behaved), greedy match on the
|
|
235
|
+
// common forms.
|
|
236
|
+
const m = line.match(
|
|
237
|
+
/^diff --git (?:"a\/(.*?)"|a\/(\S+)) (?:"b\/(.*?)"|b\/(\S+))$/,
|
|
238
|
+
);
|
|
239
|
+
if (!m) return {};
|
|
240
|
+
const a = m[1] ?? m[2];
|
|
241
|
+
const b = m[3] ?? m[4];
|
|
242
|
+
return { aPath: a, bPath: b };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function stripPathPrefix(s: string): string {
|
|
246
|
+
// `a/foo`, `b/foo`, or `/dev/null`. Quoted form: `"a/foo"`.
|
|
247
|
+
const t = s.trim().replace(/^"(.*)"$/, "$1");
|
|
248
|
+
if (t === "/dev/null") return t;
|
|
249
|
+
if (t.startsWith("a/") || t.startsWith("b/")) return t.slice(2);
|
|
250
|
+
return t;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function parseHunk(
|
|
254
|
+
lines: string[],
|
|
255
|
+
start: number,
|
|
256
|
+
): { hunk: DiffHunk | null; consumed: number } {
|
|
257
|
+
const headerLine = lines[start] ?? "";
|
|
258
|
+
// `@@ -<oldStart>,<oldLines> +<newStart>,<newLines> @@ optional label`
|
|
259
|
+
const m = headerLine.match(
|
|
260
|
+
/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@ ?(.*)$/,
|
|
261
|
+
);
|
|
262
|
+
if (!m) return { hunk: null, consumed: 1 };
|
|
263
|
+
const oldStart = Number(m[1]);
|
|
264
|
+
const oldLines = m[2] === undefined ? 1 : Number(m[2]);
|
|
265
|
+
const newStart = Number(m[3]);
|
|
266
|
+
const newLines = m[4] === undefined ? 1 : Number(m[4]);
|
|
267
|
+
const header = m[5] ?? "";
|
|
268
|
+
|
|
269
|
+
const out: DiffLine[] = [];
|
|
270
|
+
let oldNo = oldStart;
|
|
271
|
+
let newNo = newStart;
|
|
272
|
+
let i = start + 1;
|
|
273
|
+
let oldSeen = 0;
|
|
274
|
+
let newSeen = 0;
|
|
275
|
+
|
|
276
|
+
while (i < lines.length) {
|
|
277
|
+
const ln = lines[i] ?? "";
|
|
278
|
+
// End conditions: next hunk in the same file, next file, or EOF.
|
|
279
|
+
if (ln.startsWith("@@") || ln.startsWith("diff --git ")) break;
|
|
280
|
+
// `` markers — annotate the previous
|
|
281
|
+
// line by not advancing counters. Just skip.
|
|
282
|
+
if (ln.startsWith("\\ ")) {
|
|
283
|
+
i += 1;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const c = ln.charAt(0);
|
|
287
|
+
if (c === "+") {
|
|
288
|
+
out.push({ kind: "add", content: ln.slice(1), newNo });
|
|
289
|
+
newNo += 1;
|
|
290
|
+
newSeen += 1;
|
|
291
|
+
} else if (c === "-") {
|
|
292
|
+
out.push({ kind: "del", content: ln.slice(1), oldNo });
|
|
293
|
+
oldNo += 1;
|
|
294
|
+
oldSeen += 1;
|
|
295
|
+
} else if (c === " " || ln === "") {
|
|
296
|
+
// Context line. Git emits a space prefix for context; an empty
|
|
297
|
+
// line in unified diff is still a context line (an empty file
|
|
298
|
+
// line).
|
|
299
|
+
out.push({
|
|
300
|
+
kind: "ctx",
|
|
301
|
+
content: c === " " ? ln.slice(1) : "",
|
|
302
|
+
oldNo,
|
|
303
|
+
newNo,
|
|
304
|
+
});
|
|
305
|
+
oldNo += 1;
|
|
306
|
+
newNo += 1;
|
|
307
|
+
oldSeen += 1;
|
|
308
|
+
newSeen += 1;
|
|
309
|
+
} else {
|
|
310
|
+
// Anything else — probably the start of metadata for a fresh
|
|
311
|
+
// file we somehow didn't catch. Bail out so the outer loop can
|
|
312
|
+
// re-process it.
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
i += 1;
|
|
316
|
+
// Hunks are self-delimiting by their counts. Once we've consumed
|
|
317
|
+
// them, stop — anything further belongs to the next section.
|
|
318
|
+
if (oldSeen >= oldLines && newSeen >= newLines) break;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
hunk: {
|
|
323
|
+
oldStart,
|
|
324
|
+
oldLines,
|
|
325
|
+
newStart,
|
|
326
|
+
newLines,
|
|
327
|
+
header,
|
|
328
|
+
lines: out,
|
|
329
|
+
},
|
|
330
|
+
consumed: i - start,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const LANG_BY_EXT: Record<string, string> = {
|
|
335
|
+
ts: "typescript",
|
|
336
|
+
tsx: "typescript",
|
|
337
|
+
js: "javascript",
|
|
338
|
+
jsx: "javascript",
|
|
339
|
+
mjs: "javascript",
|
|
340
|
+
cjs: "javascript",
|
|
341
|
+
json: "json",
|
|
342
|
+
md: "markdown",
|
|
343
|
+
mdx: "markdown",
|
|
344
|
+
css: "css",
|
|
345
|
+
scss: "scss",
|
|
346
|
+
html: "html",
|
|
347
|
+
yml: "yaml",
|
|
348
|
+
yaml: "yaml",
|
|
349
|
+
toml: "toml",
|
|
350
|
+
py: "python",
|
|
351
|
+
rb: "ruby",
|
|
352
|
+
go: "go",
|
|
353
|
+
rs: "rust",
|
|
354
|
+
java: "java",
|
|
355
|
+
c: "c",
|
|
356
|
+
h: "c",
|
|
357
|
+
cpp: "cpp",
|
|
358
|
+
hpp: "cpp",
|
|
359
|
+
sh: "bash",
|
|
360
|
+
bash: "bash",
|
|
361
|
+
sql: "sql",
|
|
362
|
+
swift: "swift",
|
|
363
|
+
kt: "kotlin",
|
|
364
|
+
php: "php",
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
function languageFor(path: string): string | undefined {
|
|
368
|
+
const ext = path.includes(".") ? path.split(".").pop()!.toLowerCase() : "";
|
|
369
|
+
return LANG_BY_EXT[ext];
|
|
370
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Set the task creator's git identity inside the container (ADR-029) so commits
|
|
3
|
+
* are authored as the user who created the task, not the image's generic
|
|
4
|
+
* identity. Best-effort, mirrors the SSH setup: a failure logs but never aborts
|
|
5
|
+
* task-up. Name falls back to the email when no display name is known.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
|
|
10
|
+
const EXEC_TIMEOUT_MS = 10_000;
|
|
11
|
+
|
|
12
|
+
/** Injectable docker-exec seam (mocked in tests). */
|
|
13
|
+
export type DockerExec = (args: string[]) => {
|
|
14
|
+
status: number | null;
|
|
15
|
+
stderr: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const defaultExec: DockerExec = (args) => {
|
|
19
|
+
const res = spawnSync("docker", args, {
|
|
20
|
+
encoding: "utf8",
|
|
21
|
+
timeout: EXEC_TIMEOUT_MS,
|
|
22
|
+
});
|
|
23
|
+
return { status: res.status, stderr: res.stderr ?? "" };
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function setupTaskGitIdentity(
|
|
27
|
+
taskId: string,
|
|
28
|
+
name: string | null,
|
|
29
|
+
email: string | null,
|
|
30
|
+
exec: DockerExec = defaultExec,
|
|
31
|
+
): boolean {
|
|
32
|
+
if (!email) {
|
|
33
|
+
console.log(`[git] task ${taskId}: no owner email — leaving git identity`);
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const displayName = name && name.trim() ? name.trim() : email;
|
|
37
|
+
const container = `task-${taskId}-app-1`;
|
|
38
|
+
// Values are passed as argv (never interpolated into a shell string), so a
|
|
39
|
+
// name/email can't shell-inject.
|
|
40
|
+
const entries: ReadonlyArray<readonly [string, string]> = [
|
|
41
|
+
["user.name", displayName],
|
|
42
|
+
["user.email", email],
|
|
43
|
+
];
|
|
44
|
+
for (const [key, value] of entries) {
|
|
45
|
+
const res = exec([
|
|
46
|
+
"exec",
|
|
47
|
+
"-u",
|
|
48
|
+
"node",
|
|
49
|
+
container,
|
|
50
|
+
"git",
|
|
51
|
+
"config",
|
|
52
|
+
"--global",
|
|
53
|
+
key,
|
|
54
|
+
value,
|
|
55
|
+
]);
|
|
56
|
+
if (res.status !== 0) {
|
|
57
|
+
console.warn(
|
|
58
|
+
`[git] task ${taskId}: git config ${key} failed: ${res.stderr.trim()}`,
|
|
59
|
+
);
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
console.log(`[git] task ${taskId}: git identity → ${displayName} <${email}>`);
|
|
64
|
+
return true;
|
|
65
|
+
}
|