@luuthuong/devboard 0.0.0-stage → 1.0.1
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/.claude/skills/devboard/SKILL.md +111 -0
- package/bin/devboard.mjs +59 -0
- package/dist/assets/CardDetailView-BZE_MfjM.js +24 -0
- package/dist/assets/DiffPane-2WnoRvZ0.js +22 -0
- package/dist/assets/DiffPane-CpoDDQrA.css +1 -0
- package/dist/assets/KnowledgeView-C4WPFAUM.js +3 -0
- package/dist/assets/ListBoardView-DEE_eK4h.js +3 -0
- package/dist/assets/NewTaskDialog-D8cpc7Bc.js +1 -0
- package/dist/assets/OnboardingWizard-9ZkFPGvH.js +1 -0
- package/dist/assets/ProcessManagerView-DJOr44DY.js +1 -0
- package/dist/assets/ProposalCard-B8jqFtL8.js +1 -0
- package/dist/assets/SearchOverlay-qGxzmM_v.js +1 -0
- package/dist/assets/SettingsView-C3GkCbTz.js +1 -0
- package/dist/assets/SetupProjectView-CSx6ax5r.js +1 -0
- package/dist/assets/Surface-BQKeigoq.js +1 -0
- package/dist/assets/TaskDescriptionEditor-D0_b81zY.js +58 -0
- package/dist/assets/Terminal-BrP-ENHg.css +1 -0
- package/dist/assets/Terminal-yq90NOBX.js +80 -0
- package/dist/assets/deepEqual-BiJVGDfM.js +1 -0
- package/dist/assets/index-BtXUncAU.css +1 -0
- package/dist/assets/index-CQnL8JJ5.js +18 -0
- package/dist/assets/jsx-runtime-QmcfnBgR.js +1 -0
- package/dist/assets/plus-Cecrm8sZ.js +1 -0
- package/dist/assets/react-dom-DjtJUrx3.js +1 -0
- package/dist/assets/x-B6wVesqk.js +1 -0
- package/dist/index.html +22 -0
- package/package.json +81 -4
- package/server-dist/server/assets/remote-shell.sh +239 -0
- package/server-dist/server/board.js +486 -0
- package/server-dist/server/commands.js +390 -0
- package/server-dist/server/context.js +34 -0
- package/server-dist/server/coordination.js +442 -0
- package/server-dist/server/editor.js +101 -0
- package/server-dist/server/events.js +17 -0
- package/server-dist/server/fsBrowse.js +143 -0
- package/server-dist/server/git.js +258 -0
- package/server-dist/server/harness/config.js +90 -0
- package/server-dist/server/harness/entry.js +98 -0
- package/server-dist/server/harness/intents.js +268 -0
- package/server-dist/server/harness/kbIndex.js +291 -0
- package/server-dist/server/harness/kbStore.js +258 -0
- package/server-dist/server/harness/librarian.js +147 -0
- package/server-dist/server/harness/pack.js +182 -0
- package/server-dist/server/harness/proposals.js +113 -0
- package/server-dist/server/harness/repo.js +150 -0
- package/server-dist/server/harness/scrub.js +30 -0
- package/server-dist/server/harness/service.js +683 -0
- package/server-dist/server/harness/template/README.md +38 -0
- package/server-dist/server/harness/template/agents/code-reviewer.md +23 -0
- package/server-dist/server/harness/template/agents/security-reviewer.md +20 -0
- package/server-dist/server/harness/template/agents/test-writer.md +16 -0
- package/server-dist/server/harness/template/agents.yaml +41 -0
- package/server-dist/server/harness/template/harness.json +11 -0
- package/server-dist/server/harness/template/kb/team/README.md +4 -0
- package/server-dist/server/harness/template/skills/aidlc-design/SKILL.md +41 -0
- package/server-dist/server/harness/template/skills/aidlc-implement/SKILL.md +34 -0
- package/server-dist/server/harness/template/skills/aidlc-postmortem/SKILL.md +35 -0
- package/server-dist/server/harness/template/skills/aidlc-pr/SKILL.md +22 -0
- package/server-dist/server/harness/template/skills/aidlc-requirements/SKILL.md +40 -0
- package/server-dist/server/harness/template/skills/aidlc-units/SKILL.md +36 -0
- package/server-dist/server/harness/template/skills/project-kb/SKILL.md +41 -0
- package/server-dist/server/harness/usage.js +82 -0
- package/server-dist/server/hooks.js +222 -0
- package/server-dist/server/http.js +266 -0
- package/server-dist/server/index.js +77 -0
- package/server-dist/server/integrations.js +292 -0
- package/server-dist/server/pollers.js +332 -0
- package/server-dist/server/processes.js +123 -0
- package/server-dist/server/psmux.js +120 -0
- package/server-dist/server/pty.js +93 -0
- package/server-dist/server/remote.js +216 -0
- package/server-dist/server/sessionReaper.js +101 -0
- package/server-dist/server/sessions.js +315 -0
- package/server-dist/server/settings.js +125 -0
- package/server-dist/server/skill.js +29 -0
- package/server-dist/server/tmux.js +217 -0
- package/server-dist/server/transcripts.js +334 -0
- package/server-dist/server/util.js +286 -0
- package/server-dist/src/types/index.js +53 -0
- package/README.md +0 -3
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Proposed knowledge entries waiting for a person: kb-proposals.json in the
|
|
2
|
+
// data dir. Nothing the librarian writes reaches the knowledge base until a
|
|
3
|
+
// proposal is approved.
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { Mutex, devboardDataDir, ksuid, writeFileAtomic } from "../util.js";
|
|
7
|
+
import { isKnowledgeType, normalizeScope } from "./entry.js";
|
|
8
|
+
import { scrub } from "./scrub.js";
|
|
9
|
+
const MAX_TITLE = 120;
|
|
10
|
+
const MAX_SUMMARY = 240;
|
|
11
|
+
const MAX_BODY = 4000;
|
|
12
|
+
const DECIDED_RETENTION_MS = 182 * 24 * 3_600_000;
|
|
13
|
+
/** Validate and clean a proposal; throws with a readable message when unusable. */
|
|
14
|
+
export function cleanProposal(p) {
|
|
15
|
+
if (!p.kbId)
|
|
16
|
+
throw new Error("kbId is required");
|
|
17
|
+
if (!isKnowledgeType(p.type))
|
|
18
|
+
throw new Error(`unknown type: ${String(p.type)}`);
|
|
19
|
+
const title = scrub(String(p.title ?? "").trim()).slice(0, MAX_TITLE);
|
|
20
|
+
const body = scrub(String(p.body ?? "").trim()).slice(0, MAX_BODY);
|
|
21
|
+
if (!title)
|
|
22
|
+
throw new Error("title is required");
|
|
23
|
+
if (!body)
|
|
24
|
+
throw new Error("body is required");
|
|
25
|
+
const summary = scrub(String(p.summary ?? "").trim()).slice(0, MAX_SUMMARY) || title;
|
|
26
|
+
const scope = [...new Set((p.scope ?? []).map(normalizeScope).filter((s) => s && !s.startsWith("..")))];
|
|
27
|
+
return {
|
|
28
|
+
kbId: p.kbId,
|
|
29
|
+
type: p.type,
|
|
30
|
+
title,
|
|
31
|
+
summary,
|
|
32
|
+
body,
|
|
33
|
+
scope,
|
|
34
|
+
source: p.source,
|
|
35
|
+
reason: p.reason ? scrub(p.reason).slice(0, 500) : undefined,
|
|
36
|
+
origin: p.origin,
|
|
37
|
+
// Entry ids are whatever the files say; only reject what cannot be an id at all.
|
|
38
|
+
supersedes: p.supersedes?.filter((id) => typeof id === "string" && /^[\w.-]{1,100}$/.test(id)).slice(0, 10),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export class ProposalStore {
|
|
42
|
+
filePath;
|
|
43
|
+
lock = new Mutex();
|
|
44
|
+
constructor(base) {
|
|
45
|
+
this.filePath = path.join(base ?? devboardDataDir(), "kb-proposals.json");
|
|
46
|
+
}
|
|
47
|
+
/** Newest first. `cardId` keeps proposals made from that card's sessions. */
|
|
48
|
+
async list(status, from) {
|
|
49
|
+
const sessions = new Set(from?.sessionIds ?? []);
|
|
50
|
+
return (await this.read())
|
|
51
|
+
.filter((p) => !status || p.status === status)
|
|
52
|
+
.filter((p) => !from ||
|
|
53
|
+
(!!from.cardId && p.source?.cardId === from.cardId) ||
|
|
54
|
+
(!!p.source?.sessionId && sessions.has(p.source.sessionId)))
|
|
55
|
+
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
56
|
+
}
|
|
57
|
+
async get(id) {
|
|
58
|
+
return (await this.read()).find((p) => p.id === id);
|
|
59
|
+
}
|
|
60
|
+
/** Adds proposals, skipping any whose title already waits in the same KB. */
|
|
61
|
+
add(inputs) {
|
|
62
|
+
return this.lock.run(async () => {
|
|
63
|
+
const all = await this.read();
|
|
64
|
+
const pendingTitles = new Set(all.filter((p) => p.status === "pending").map((p) => `${p.kbId}\u0000${p.title.toLowerCase()}`));
|
|
65
|
+
const added = [];
|
|
66
|
+
for (const input of inputs) {
|
|
67
|
+
const clean = cleanProposal(input);
|
|
68
|
+
const key = `${clean.kbId}\u0000${clean.title.toLowerCase()}`;
|
|
69
|
+
if (pendingTitles.has(key))
|
|
70
|
+
continue;
|
|
71
|
+
pendingTitles.add(key);
|
|
72
|
+
const p = {
|
|
73
|
+
...clean,
|
|
74
|
+
id: ksuid("prop"),
|
|
75
|
+
status: "pending",
|
|
76
|
+
createdAt: new Date().toISOString(),
|
|
77
|
+
};
|
|
78
|
+
all.push(p);
|
|
79
|
+
added.push(p);
|
|
80
|
+
}
|
|
81
|
+
if (added.length)
|
|
82
|
+
await this.write(all);
|
|
83
|
+
return added;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
update(id, fn) {
|
|
87
|
+
return this.lock.run(async () => {
|
|
88
|
+
const all = await this.read();
|
|
89
|
+
const p = all.find((x) => x.id === id);
|
|
90
|
+
if (!p)
|
|
91
|
+
throw new Error(`proposal ${id} not found`);
|
|
92
|
+
fn(p);
|
|
93
|
+
await this.write(all);
|
|
94
|
+
return p;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
async read() {
|
|
98
|
+
try {
|
|
99
|
+
const parsed = JSON.parse(await fs.promises.readFile(this.filePath, "utf8"));
|
|
100
|
+
return Array.isArray(parsed?.proposals) ? parsed.proposals : [];
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async write(all) {
|
|
107
|
+
// Decided proposals are history; their entries live in the harness repo. Keep half a year.
|
|
108
|
+
const cutoff = new Date(Date.now() - DECIDED_RETENTION_MS).toISOString();
|
|
109
|
+
const kept = all.filter((p) => p.status === "pending" || (p.decidedAt ?? p.createdAt) >= cutoff);
|
|
110
|
+
await fs.promises.mkdir(path.dirname(this.filePath), { recursive: true });
|
|
111
|
+
await writeFileAtomic(this.filePath, JSON.stringify({ proposals: kept }, null, 2));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// Git plumbing for the private harness repo. Reads use the server's clone;
|
|
2
|
+
// every change goes through its own temporary worktree on a new branch and a
|
|
3
|
+
// pull request, so the clone the index reads from is never mid-edit. A repo
|
|
4
|
+
// without an `origin` (local trial) gets a plain commit instead.
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { Mutex, findOnPath, run } from "../util.js";
|
|
9
|
+
const GIT_TIMEOUT = 120_000;
|
|
10
|
+
const writeLock = new Mutex();
|
|
11
|
+
async function git(cwd, args, timeoutMs = GIT_TIMEOUT) {
|
|
12
|
+
const r = await run("git", args, { cwd, timeoutMs });
|
|
13
|
+
if (!r.ok)
|
|
14
|
+
throw new Error(`git ${args[0]} failed: ${(r.stderr || r.stdout).trim()}`);
|
|
15
|
+
return r.stdout.trim();
|
|
16
|
+
}
|
|
17
|
+
/** `~` and `~/x` mean the user's home; node's `path` does not expand them. */
|
|
18
|
+
export function expandHome(p) {
|
|
19
|
+
return p === "~" || /^~[\\/]/.test(p) ? path.join(os.homedir(), p.slice(1)) : p;
|
|
20
|
+
}
|
|
21
|
+
/** True only when `dir` is itself a repo root, not a folder inside some other repo. */
|
|
22
|
+
export async function isGitRepo(dir) {
|
|
23
|
+
const r = await run("git", ["rev-parse", "--show-toplevel"], { cwd: dir, timeoutMs: 10_000 });
|
|
24
|
+
if (!r.ok)
|
|
25
|
+
return false;
|
|
26
|
+
const norm = (x) => path.resolve(x).toLowerCase();
|
|
27
|
+
return norm(r.stdout.trim()) === norm(dir);
|
|
28
|
+
}
|
|
29
|
+
export async function hasOrigin(repo) {
|
|
30
|
+
const r = await run("git", ["remote", "get-url", "origin"], { cwd: repo, timeoutMs: 10_000 });
|
|
31
|
+
return r.ok && !!r.stdout.trim();
|
|
32
|
+
}
|
|
33
|
+
/** Clone `gitUrl` into `repoPath` unless a repo is already there. */
|
|
34
|
+
export async function ensureClone(repoPath, gitUrl) {
|
|
35
|
+
if (await isGitRepo(repoPath))
|
|
36
|
+
return;
|
|
37
|
+
if (!gitUrl)
|
|
38
|
+
throw new Error(`${repoPath} is not a git repository and no gitUrl is set`);
|
|
39
|
+
await fs.promises.mkdir(path.dirname(repoPath), { recursive: true });
|
|
40
|
+
const r = await run("git", ["clone", gitUrl, repoPath], { timeoutMs: 600_000 });
|
|
41
|
+
if (!r.ok)
|
|
42
|
+
throw new Error(`git clone failed: ${r.stderr.trim()}`);
|
|
43
|
+
}
|
|
44
|
+
/** Fast-forward the clone. Resolves true when HEAD moved. */
|
|
45
|
+
export async function pull(repoPath) {
|
|
46
|
+
if (!(await hasOrigin(repoPath)))
|
|
47
|
+
return false;
|
|
48
|
+
const before = await git(repoPath, ["rev-parse", "HEAD"]).catch(() => "");
|
|
49
|
+
await git(repoPath, ["pull", "--ff-only", "--quiet"]);
|
|
50
|
+
const after = await git(repoPath, ["rev-parse", "HEAD"]).catch(() => "");
|
|
51
|
+
return before !== after;
|
|
52
|
+
}
|
|
53
|
+
async function defaultBranch(repo) {
|
|
54
|
+
const r = await run("git", ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], { cwd: repo, timeoutMs: 10_000 });
|
|
55
|
+
if (r.ok && r.stdout.trim())
|
|
56
|
+
return r.stdout.trim().replace(/^origin\//, "");
|
|
57
|
+
return (await git(repo, ["rev-parse", "--abbrev-ref", "HEAD"])) || "main";
|
|
58
|
+
}
|
|
59
|
+
/** `-c user.*` only when the repo has no identity, so commits never fail on a fresh server. */
|
|
60
|
+
async function identityArgs(repo) {
|
|
61
|
+
const email = await run("git", ["config", "user.email"], { cwd: repo, timeoutMs: 10_000 });
|
|
62
|
+
return email.ok && email.stdout.trim() ? [] : ["-c", "user.name=DevBoard", "-c", "user.email=devboard@localhost"];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Apply `edit` to a fresh checkout of the default branch and propose it.
|
|
66
|
+
* `edit` gets the checkout's root and writes files there.
|
|
67
|
+
*/
|
|
68
|
+
export function proposeChange(repoPath, req, edit) {
|
|
69
|
+
return writeLock.run(async () => {
|
|
70
|
+
const ident = await identityArgs(repoPath);
|
|
71
|
+
if (!(await hasOrigin(repoPath))) {
|
|
72
|
+
await edit(repoPath);
|
|
73
|
+
await git(repoPath, ["add", "-A"]);
|
|
74
|
+
await git(repoPath, [...ident, "commit", "-q", "-m", req.message]);
|
|
75
|
+
return { commit: await git(repoPath, ["rev-parse", "HEAD"]), mode: "local" };
|
|
76
|
+
}
|
|
77
|
+
await git(repoPath, ["fetch", "--quiet", "origin"]);
|
|
78
|
+
const base = await defaultBranch(repoPath);
|
|
79
|
+
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "devboard-harness-"));
|
|
80
|
+
await fs.promises.rm(dir, { recursive: true, force: true });
|
|
81
|
+
await git(repoPath, ["worktree", "add", "-q", "-b", req.branch, dir, `origin/${base}`]);
|
|
82
|
+
try {
|
|
83
|
+
await edit(dir);
|
|
84
|
+
await git(dir, ["add", "-A"]);
|
|
85
|
+
await git(dir, [...ident, "commit", "-q", "-m", req.message]);
|
|
86
|
+
const commit = await git(dir, ["rev-parse", "HEAD"]);
|
|
87
|
+
await git(dir, ["push", "-q", "-u", "origin", req.branch]);
|
|
88
|
+
if (!findOnPath("gh"))
|
|
89
|
+
return { commit, branch: req.branch, mode: "branch" };
|
|
90
|
+
const pr = await run("gh", ["pr", "create", "--base", base, "--head", req.branch, "--title", req.prTitle, "--body", req.prBody], { cwd: dir, timeoutMs: GIT_TIMEOUT });
|
|
91
|
+
const url = pr.ok
|
|
92
|
+
? pr.stdout
|
|
93
|
+
.trim()
|
|
94
|
+
.split(/\s+/)
|
|
95
|
+
.find((s) => s.startsWith("http"))
|
|
96
|
+
: undefined;
|
|
97
|
+
return { commit, branch: req.branch, url, mode: url ? "pr" : "branch" };
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
await run("git", ["worktree", "remove", "--force", dir], { cwd: repoPath, timeoutMs: GIT_TIMEOUT });
|
|
101
|
+
await run("git", ["branch", "-D", req.branch], { cwd: repoPath, timeoutMs: GIT_TIMEOUT });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Take back an entry file (`file`, relative to the repo root) that `proposeChange` added.
|
|
107
|
+
* Not merged yet: close its pull request and delete its branch. Already merged: propose a
|
|
108
|
+
* change that deletes the file.
|
|
109
|
+
*/
|
|
110
|
+
export async function retractChange(repoPath, file, prev, req) {
|
|
111
|
+
const posix = file.split(path.sep).join("/");
|
|
112
|
+
const remove = async (dir) => {
|
|
113
|
+
await fs.promises.rm(path.join(dir, file), { force: true });
|
|
114
|
+
};
|
|
115
|
+
if (!(await hasOrigin(repoPath))) {
|
|
116
|
+
if (!fs.existsSync(path.join(repoPath, file)))
|
|
117
|
+
return { closed: true };
|
|
118
|
+
return { revert: await proposeChange(repoPath, req, remove) };
|
|
119
|
+
}
|
|
120
|
+
await git(repoPath, ["fetch", "--quiet", "origin"]);
|
|
121
|
+
const base = await defaultBranch(repoPath);
|
|
122
|
+
const merged = (await run("git", ["cat-file", "-e", `origin/${base}:${posix}`], { cwd: repoPath, timeoutMs: 10_000 }))
|
|
123
|
+
.ok;
|
|
124
|
+
if (merged)
|
|
125
|
+
return { revert: await proposeChange(repoPath, req, remove) };
|
|
126
|
+
if (prev.prUrl?.startsWith("http") && findOnPath("gh")) {
|
|
127
|
+
await run("gh", ["pr", "close", prev.prUrl, "--delete-branch"], { cwd: repoPath, timeoutMs: GIT_TIMEOUT });
|
|
128
|
+
}
|
|
129
|
+
// `gh pr close --delete-branch` may be unavailable or already done; either way the branch must go.
|
|
130
|
+
await run("git", ["push", "-q", "origin", "--delete", prev.branch], { cwd: repoPath, timeoutMs: GIT_TIMEOUT });
|
|
131
|
+
return { closed: true };
|
|
132
|
+
}
|
|
133
|
+
/** Every file changed in `repo` since `sinceIso`, with the time of its latest change (ms). One `git log`. */
|
|
134
|
+
export async function fileChangeTimes(repo, sinceIso) {
|
|
135
|
+
const r = await run("git", ["log", `--since=${sinceIso}`, "--name-only", "--format=%x01%ct"], {
|
|
136
|
+
cwd: repo,
|
|
137
|
+
timeoutMs: 60_000,
|
|
138
|
+
});
|
|
139
|
+
const out = new Map();
|
|
140
|
+
if (!r.ok)
|
|
141
|
+
return out;
|
|
142
|
+
let at = 0;
|
|
143
|
+
for (const line of r.stdout.split(/\r?\n/)) {
|
|
144
|
+
if (line.startsWith("\x01"))
|
|
145
|
+
at = Number(line.slice(1)) * 1000;
|
|
146
|
+
else if (line && !out.has(line))
|
|
147
|
+
out.set(line, at); // newest first: the first time seen is the latest
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Redaction applied to anything the librarian reads before it reaches a model,
|
|
2
|
+
// and to every proposal before it is written to the harness repo.
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
const PATTERNS = [
|
|
5
|
+
[/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, "<private-key>"],
|
|
6
|
+
[/\bsk-ant-[A-Za-z0-9_-]{20,}/g, "<anthropic-key>"],
|
|
7
|
+
[/\bsk-[A-Za-z0-9]{20,}/g, "<api-key>"],
|
|
8
|
+
[/\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{30,}/g, "<github-token>"],
|
|
9
|
+
[/\bgithub_pat_[A-Za-z0-9_]{30,}/g, "<github-token>"],
|
|
10
|
+
[/\bxox[abposr]-[A-Za-z0-9-]{10,}/g, "<slack-token>"],
|
|
11
|
+
[/\bAKIA[0-9A-Z]{16}\b/g, "<aws-key>"],
|
|
12
|
+
[/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g, "<jwt>"],
|
|
13
|
+
[/\b(https?:\/\/)[^\s/:@]+:[^\s/@]+@/g, "$1<credentials>@"],
|
|
14
|
+
[
|
|
15
|
+
/\b((?:api[_-]?key|secret|token|password|passwd|client[_-]?secret|access[_-]?key)["']?\s*[:=]\s*["']?)[^\s"',;]{6,}/gi,
|
|
16
|
+
"$1<redacted>",
|
|
17
|
+
],
|
|
18
|
+
];
|
|
19
|
+
export function scrub(text) {
|
|
20
|
+
let out = text;
|
|
21
|
+
for (const [re, repl] of PATTERNS)
|
|
22
|
+
out = out.replace(re, repl);
|
|
23
|
+
const home = os.homedir();
|
|
24
|
+
if (home && home.length > 1)
|
|
25
|
+
out = out.split(home).join("~");
|
|
26
|
+
const host = os.hostname();
|
|
27
|
+
if (host && host.length > 2)
|
|
28
|
+
out = out.split(host).join("<host>");
|
|
29
|
+
return out;
|
|
30
|
+
}
|