@nessielabs/daemon 0.3.2 → 0.5.26

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.
Files changed (49) hide show
  1. package/README.md +8 -19
  2. package/dist/cli.js +46 -18
  3. package/dist/commands/auth.js +116 -0
  4. package/dist/commands/integrations.js +84 -0
  5. package/dist/commands/login.js +145 -0
  6. package/dist/commands/run.js +21 -6
  7. package/dist/commands/service.js +18 -36
  8. package/dist/commands/sharing.js +399 -0
  9. package/dist/commands/status.js +67 -0
  10. package/dist/commands/teams.js +64 -0
  11. package/dist/nera/client.js +156 -0
  12. package/dist/nera/process.js +78 -0
  13. package/dist/nera/runtimeFile.js +25 -0
  14. package/dist/platform/binary.js +21 -0
  15. package/dist/platform/openBrowser.js +14 -0
  16. package/dist/platform/paths.js +13 -0
  17. package/dist/platform/table.js +85 -0
  18. package/package.json +9 -3
  19. package/vendor/nera/darwin-arm64/nera +0 -0
  20. package/vendor/nera/darwin-x64/nera +0 -0
  21. package/vendor/nera/linux-arm64/nera +0 -0
  22. package/vendor/nera/linux-x64/nera +0 -0
  23. package/vendor/nera/win32-arm64/nera.exe +0 -0
  24. package/vendor/nera/win32-x64/nera.exe +0 -0
  25. package/vendor/proto/nessie.edge.v1.proto +475 -0
  26. package/dist/api/http.js +0 -25
  27. package/dist/auth/deviceFlow.js +0 -36
  28. package/dist/auth/tokens.js +0 -30
  29. package/dist/commands/setup.js +0 -78
  30. package/dist/config/endpoints.js +0 -2
  31. package/dist/config/paths.js +0 -19
  32. package/dist/config/store.js +0 -63
  33. package/dist/git/repoResolver.js +0 -215
  34. package/dist/parser/claudeCode.js +0 -84
  35. package/dist/parser/codex.js +0 -86
  36. package/dist/parser/jsonl.js +0 -24
  37. package/dist/parser/types.js +0 -9
  38. package/dist/redact/secrets.js +0 -60
  39. package/dist/service/process.js +0 -134
  40. package/dist/service/systemd.js +0 -38
  41. package/dist/sources/detect.js +0 -43
  42. package/dist/sync/batch.js +0 -50
  43. package/dist/sync/client.js +0 -47
  44. package/dist/sync/ids.js +0 -11
  45. package/dist/sync/loop.js +0 -76
  46. package/dist/sync/map.js +0 -111
  47. package/dist/sync/scan.js +0 -60
  48. package/dist/sync/slices.js +0 -74
  49. package/dist/sync/types.js +0 -1
@@ -1,63 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { mkdir, readFile, writeFile } from "node:fs/promises";
3
- import { hostname } from "node:os";
4
- import { dirname } from "node:path";
5
- import { PARSER_VERSION } from "../parser/types.js";
6
- import { getDaemonPaths } from "./paths.js";
7
- export function createConfig(input) {
8
- const deviceId = input.deviceId ?? randomUUID();
9
- return {
10
- deviceId,
11
- deviceName: input.deviceName ?? hostname(),
12
- accessToken: input.accessToken,
13
- refreshToken: input.refreshToken ?? null,
14
- apiKey: input.apiKey ?? null,
15
- selectedSources: input.selectedSources.map((source) => ({
16
- ...source,
17
- localId: randomUUID(),
18
- stableKey: `device:${deviceId}`,
19
- })),
20
- };
21
- }
22
- export function createInitialState() {
23
- return {
24
- files: {},
25
- pushedSourceRootIds: [],
26
- pushedSessions: {},
27
- parserVersion: PARSER_VERSION,
28
- lastSyncAt: null,
29
- };
30
- }
31
- export async function readConfig(paths = getDaemonPaths()) {
32
- return JSON.parse(await readFile(paths.configFile, "utf8"));
33
- }
34
- export async function writeConfig(config, paths = getDaemonPaths()) {
35
- await mkdir(dirname(paths.configFile), { recursive: true });
36
- await writeFile(paths.configFile, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
37
- }
38
- export async function readState(paths = getDaemonPaths()) {
39
- try {
40
- const parsed = JSON.parse(await readFile(paths.stateFile, "utf8"));
41
- return {
42
- files: parsed.files ?? {},
43
- pushedSourceRootIds: parsed.pushedSourceRootIds ?? [],
44
- pushedSessions: parsed.pushedSessions ?? {},
45
- // State written before parser versioning existed predates every
46
- // versioned parser change, so it always needs the backfill pass.
47
- parserVersion: parsed.parserVersion ?? 0,
48
- lastSyncAt: parsed.lastSyncAt ?? null,
49
- };
50
- }
51
- catch (error) {
52
- if (isMissingFileError(error))
53
- return createInitialState();
54
- throw error;
55
- }
56
- }
57
- export async function writeState(state, paths = getDaemonPaths()) {
58
- await mkdir(dirname(paths.stateFile), { recursive: true });
59
- await writeFile(paths.stateFile, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
60
- }
61
- function isMissingFileError(error) {
62
- return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
63
- }
@@ -1,215 +0,0 @@
1
- import { readFileSync, statSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { dirname, isAbsolute, join, resolve } from "node:path";
4
- /** Directory levels to walk up looking for `.git` before giving up. */
5
- const MAX_PARENT_TRAVERSAL = 64;
6
- /**
7
- * Resolves the git repository identity for a working directory by reading
8
- * `.git` metadata from disk, without invoking the `git` binary.
9
- *
10
- * Resolution is best-effort by design: paths that are not inside a git
11
- * repository, repositories without remotes, and repositories deleted since
12
- * the session simply resolve to null.
13
- */
14
- export class GitRepoResolver {
15
- /**
16
- * Memoized results per workspace path: conversations in the same directory
17
- * repeat heavily within one sync run.
18
- */
19
- cache = new Map();
20
- resolve(workspacePath) {
21
- const standardized = resolve(expandTilde(workspacePath));
22
- const cached = this.cache.get(standardized);
23
- if (cached !== undefined)
24
- return cached;
25
- const resolved = resolveUncached(standardized);
26
- this.cache.set(standardized, resolved);
27
- return resolved;
28
- }
29
- }
30
- function expandTilde(path) {
31
- if (path === "~")
32
- return homedir();
33
- if (path.startsWith("~/"))
34
- return join(homedir(), path.slice(2));
35
- return path;
36
- }
37
- function resolveUncached(workspacePath) {
38
- let directory = workspacePath;
39
- for (let level = 0; level < MAX_PARENT_TRAVERSAL; level += 1) {
40
- const configPath = gitConfigPath(join(directory, ".git"));
41
- if (configPath) {
42
- const config = readTextFile(configPath);
43
- const remoteUrl = config !== null ? remoteUrlFromGitConfig(config) : null;
44
- if (remoteUrl) {
45
- return { remoteUrl, repoKey: normalizedRepoKey(remoteUrl) };
46
- }
47
- // A repo without a remote keeps the walk going so a nested remoteless
48
- // checkout attributes to its enclosing repo. Matches NessieCore's
49
- // GitRepoResolver.resolveUncached, the source of truth for repo
50
- // identity; both implementations must group conversations identically.
51
- }
52
- const parent = dirname(directory);
53
- if (parent === directory)
54
- return null;
55
- directory = parent;
56
- }
57
- return null;
58
- }
59
- /**
60
- * Locate the `config` file for a `.git` entry, which is a directory for
61
- * normal checkouts and a `gitdir:` pointer file for worktrees and submodules.
62
- */
63
- function gitConfigPath(gitEntry) {
64
- const stats = statSync(gitEntry, { throwIfNoEntry: false });
65
- if (!stats)
66
- return null;
67
- if (stats.isDirectory()) {
68
- return existingFile(join(gitEntry, "config"));
69
- }
70
- const pointer = readTextFile(gitEntry);
71
- const gitDir = pointer !== null ? gitDirPath(pointer) : null;
72
- if (!gitDir)
73
- return null;
74
- const resolvedGitDir = absolutePath(gitDir, dirname(gitEntry));
75
- // Worktree gitdirs keep shared state (including `config`) in the main
76
- // repository's git directory, referenced by a `commondir` file.
77
- const commonDir = readTextFile(join(resolvedGitDir, "commondir"))?.trim();
78
- if (commonDir) {
79
- return existingFile(join(absolutePath(commonDir, resolvedGitDir), "config"));
80
- }
81
- return existingFile(join(resolvedGitDir, "config"));
82
- }
83
- function existingFile(path) {
84
- return statSync(path, { throwIfNoEntry: false })?.isFile() ? path : null;
85
- }
86
- function readTextFile(path) {
87
- try {
88
- return readFileSync(path, "utf8");
89
- }
90
- catch {
91
- return null;
92
- }
93
- }
94
- export function gitDirPath(pointerContents) {
95
- for (const line of pointerContents.split(/\r?\n/)) {
96
- const trimmed = line.trim();
97
- if (!trimmed.startsWith("gitdir:"))
98
- continue;
99
- const path = trimmed.slice("gitdir:".length).trim();
100
- return path.length > 0 ? path : null;
101
- }
102
- return null;
103
- }
104
- function absolutePath(path, base) {
105
- return isAbsolute(path) ? resolve(path) : resolve(base, path);
106
- }
107
- /**
108
- * Extract a remote URL from raw `.git/config` contents, preferring `origin`
109
- * and falling back to the first remote with a URL.
110
- */
111
- export function remoteUrlFromGitConfig(contents) {
112
- let currentRemote = null;
113
- let originUrl = null;
114
- let firstUrl = null;
115
- for (const line of contents.split(/\r?\n/)) {
116
- const trimmed = line.trim();
117
- if (trimmed.startsWith("[")) {
118
- currentRemote = remoteNameFromSectionHeader(trimmed);
119
- continue;
120
- }
121
- if (!currentRemote)
122
- continue;
123
- const url = configValue(trimmed, "url");
124
- if (!url)
125
- continue;
126
- if (currentRemote === "origin" && originUrl === null) {
127
- originUrl = url;
128
- }
129
- if (firstUrl === null) {
130
- firstUrl = url;
131
- }
132
- }
133
- return originUrl ?? firstUrl;
134
- }
135
- function remoteNameFromSectionHeader(header) {
136
- // [remote "origin"]
137
- if (!header.startsWith("[remote "))
138
- return null;
139
- const openQuote = header.indexOf('"');
140
- const closeQuote = header.lastIndexOf('"');
141
- if (openQuote === -1 || closeQuote <= openQuote)
142
- return null;
143
- return header.slice(openQuote + 1, closeQuote);
144
- }
145
- function configValue(line, key) {
146
- const equals = line.indexOf("=");
147
- if (equals === -1)
148
- return null;
149
- if (line.slice(0, equals).trim() !== key)
150
- return null;
151
- const value = line.slice(equals + 1).trim();
152
- return value.length > 0 ? value : null;
153
- }
154
- /**
155
- * Normalize a configured remote URL to a stable `host/path` identity so the
156
- * same repository matches across protocols, credentials, machines, and
157
- * clones: `git@github.com:Org/Repo.git` and `https://github.com/org/repo`
158
- * both become `github.com/org/repo`.
159
- */
160
- export function normalizedRepoKey(remoteUrl) {
161
- let rest = remoteUrl.trim();
162
- if (rest.length === 0)
163
- return null;
164
- const schemeEnd = rest.indexOf("://");
165
- if (schemeEnd !== -1) {
166
- const scheme = rest.slice(0, schemeEnd).toLowerCase();
167
- // Local remotes have no cross-machine identity.
168
- if (scheme === "file")
169
- return null;
170
- rest = rest.slice(schemeEnd + "://".length);
171
- }
172
- else if (rest.includes(":") && (rest.includes("@") || !rest.startsWith("/"))) {
173
- // scp-like syntax: [user@]host:path
174
- const colon = rest.indexOf(":");
175
- const hostPart = rest.slice(0, colon);
176
- const path = rest.slice(colon + 1);
177
- if (hostPart.length === 0 || path.length === 0 || hostPart.includes("/"))
178
- return null;
179
- rest = `${hostPart}/${path}`;
180
- }
181
- else {
182
- // Bare filesystem path remote.
183
- return null;
184
- }
185
- // Strip userinfo.
186
- const lastAt = rest.lastIndexOf("@");
187
- const firstSlash = rest.indexOf("/");
188
- if (lastAt !== -1 && firstSlash !== -1 && lastAt < firstSlash) {
189
- rest = rest.slice(lastAt + 1);
190
- }
191
- else if (rest.includes("@") && !rest.includes("/")) {
192
- rest = rest.slice(rest.indexOf("@") + 1);
193
- }
194
- const components = rest.split("/").filter((component) => component.length > 0);
195
- if (components.length < 2)
196
- return null;
197
- // Lowercase the host and drop any port; path case is preserved except for
198
- // trailing `.git`.
199
- let host = components[0].toLowerCase();
200
- const portColon = host.indexOf(":");
201
- if (portColon !== -1) {
202
- host = host.slice(0, portColon);
203
- }
204
- if (!host.includes(".") && host !== "localhost")
205
- return null;
206
- components[0] = host;
207
- let last = components[components.length - 1];
208
- if (last.toLowerCase().endsWith(".git")) {
209
- last = last.slice(0, -4);
210
- }
211
- if (last.length === 0)
212
- return null;
213
- components[components.length - 1] = last;
214
- return components.join("/");
215
- }
@@ -1,84 +0,0 @@
1
- import { basename } from "node:path";
2
- import { redactSecrets } from "../redact/secrets.js";
3
- import { asRecord, readJsonl, stringValue } from "./jsonl.js";
4
- export async function parseClaudeCodeSession(path) {
5
- const records = await readJsonl(path);
6
- const messages = [];
7
- // Anchor session identity to the JSONL filename, matching the macOS client.
8
- // The in-file `sessionId` is reused across files for worktree/resumed
9
- // sessions, so keying on it collapses distinct transcripts into one
10
- // conversation id and makes their deterministic message ids collide (409).
11
- const sessionId = basename(path, ".jsonl");
12
- let title = null;
13
- let createdAt = null;
14
- let updatedAt = null;
15
- let cwd = null;
16
- for (const value of records) {
17
- const record = asRecord(value);
18
- if (!record)
19
- continue;
20
- // Every transcript line stamps cwd, including sidechain lines, so capture
21
- // it before any filtering. The path-encoded project directory name is
22
- // lossy (dashes vs. path separators), so this is the only reliable source.
23
- cwd ??= stringValue(record.cwd);
24
- if (record.isSidechain === true)
25
- continue;
26
- const timestamp = stringValue(record.timestamp);
27
- if (timestamp) {
28
- createdAt ??= timestamp;
29
- updatedAt = timestamp;
30
- }
31
- const role = normalizeRole(stringValue(record.type) ?? stringValue(record.role));
32
- if (!role)
33
- continue;
34
- const text = extractClaudeText(record);
35
- if (!text || shouldDropScaffolding(text))
36
- continue;
37
- messages.push({ role, content: text, timestamp });
38
- if (!title && role === "user")
39
- title = redactSecrets(firstLine(text));
40
- }
41
- if (messages.length === 0)
42
- return null;
43
- const now = new Date().toISOString();
44
- return {
45
- id: sessionId,
46
- sourceKind: "claude_code",
47
- sourceId: path,
48
- title: title ?? "Claude Code conversation",
49
- createdAt: createdAt ?? now,
50
- updatedAt: updatedAt ?? createdAt ?? now,
51
- cwd,
52
- messages,
53
- };
54
- }
55
- function normalizeRole(role) {
56
- if (role === "user" || role === "assistant" || role === "system" || role === "tool")
57
- return role;
58
- return null;
59
- }
60
- function extractClaudeText(record) {
61
- const direct = stringValue(record.message);
62
- if (direct)
63
- return direct;
64
- const message = asRecord(record.message);
65
- if (!message)
66
- return null;
67
- const content = message.content;
68
- if (typeof content === "string")
69
- return content;
70
- if (!Array.isArray(content))
71
- return null;
72
- const parts = content.flatMap((item) => {
73
- const part = asRecord(item);
74
- const text = part ? stringValue(part.text) : null;
75
- return text ? [text] : [];
76
- });
77
- return parts.length > 0 ? parts.join("\n\n") : null;
78
- }
79
- function shouldDropScaffolding(text) {
80
- return text.startsWith("<command-name>") || text.startsWith("<local-command-stdout>");
81
- }
82
- function firstLine(text) {
83
- return text.split(/\r?\n/, 1)[0]?.slice(0, 80) || "Claude Code conversation";
84
- }
@@ -1,86 +0,0 @@
1
- import { basename } from "node:path";
2
- import { redactSecrets } from "../redact/secrets.js";
3
- import { asRecord, readJsonl, stringValue } from "./jsonl.js";
4
- export async function parseCodexSession(path) {
5
- const records = await readJsonl(path);
6
- const messages = [];
7
- let sessionId = basename(path, ".jsonl");
8
- let title = null;
9
- let createdAt = null;
10
- let updatedAt = null;
11
- let cwd = null;
12
- for (const value of records) {
13
- const record = asRecord(value);
14
- if (!record)
15
- continue;
16
- const type = stringValue(record.type);
17
- const timestamp = stringValue(record.timestamp) ?? stringValue(record.created_at);
18
- if (timestamp) {
19
- createdAt ??= timestamp;
20
- updatedAt = timestamp;
21
- }
22
- if (type === "session_meta") {
23
- sessionId = stringValue(record.id) ?? stringValue(record.session_id) ?? sessionId;
24
- title = redactTitle(stringValue(record.title)) ?? title;
25
- const meta = asRecord(record.payload) ?? record;
26
- cwd ??= stringValue(meta.cwd);
27
- continue;
28
- }
29
- if (type !== "event_msg")
30
- continue;
31
- const payload = asRecord(record.payload) ?? record;
32
- const messageType = stringValue(payload.type) ?? stringValue(payload.kind);
33
- const text = extractText(payload);
34
- if (!text)
35
- continue;
36
- const role = roleForCodexMessage(messageType, payload);
37
- messages.push({ role, content: text, timestamp });
38
- if (!title && role === "user")
39
- title = redactTitle(firstLine(text));
40
- }
41
- if (messages.length === 0)
42
- return null;
43
- const now = new Date().toISOString();
44
- return {
45
- id: sessionId,
46
- sourceKind: "codex",
47
- sourceId: path,
48
- title: title ?? "Codex conversation",
49
- createdAt: createdAt ?? now,
50
- updatedAt: updatedAt ?? createdAt ?? now,
51
- cwd,
52
- messages,
53
- };
54
- }
55
- function roleForCodexMessage(messageType, payload) {
56
- const role = stringValue(payload.role);
57
- if (role === "user" || role === "assistant" || role === "system" || role === "tool")
58
- return role;
59
- if (messageType?.includes("user"))
60
- return "user";
61
- if (messageType?.includes("agent") || messageType?.includes("assistant"))
62
- return "assistant";
63
- if (messageType?.includes("tool"))
64
- return "tool";
65
- return "assistant";
66
- }
67
- function extractText(payload) {
68
- for (const key of ["message", "text", "content", "delta"]) {
69
- const direct = stringValue(payload[key]);
70
- if (direct)
71
- return direct;
72
- }
73
- const message = asRecord(payload.message);
74
- if (message) {
75
- const content = stringValue(message.content);
76
- if (content)
77
- return content;
78
- }
79
- return null;
80
- }
81
- function firstLine(text) {
82
- return text.split(/\r?\n/, 1)[0]?.slice(0, 80) || "Codex conversation";
83
- }
84
- function redactTitle(title) {
85
- return title ? redactSecrets(title) : null;
86
- }
@@ -1,24 +0,0 @@
1
- import { readFile } from "node:fs/promises";
2
- export async function readJsonl(path) {
3
- const content = await readFile(path, "utf8");
4
- const records = [];
5
- for (const line of content.split(/\r?\n/)) {
6
- const trimmed = line.trim();
7
- if (!trimmed)
8
- continue;
9
- try {
10
- records.push(JSON.parse(trimmed));
11
- }
12
- catch {
13
- // Agent history files are written while active. A trailing partial JSONL
14
- // record should not prevent earlier complete records from syncing.
15
- }
16
- }
17
- return records;
18
- }
19
- export function asRecord(value) {
20
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
21
- }
22
- export function stringValue(value) {
23
- return typeof value === "string" && value.trim().length > 0 ? value : null;
24
- }
@@ -1,9 +0,0 @@
1
- /**
2
- * Version of the daemon's transcript parsing + enrichment semantics. Bumping
3
- * it makes the next sync reparse every session file and re-push conversation
4
- * headers as field-masked merges, so previously synced conversations pick up
5
- * newly extracted metadata. Messages are append-only and are never re-pushed.
6
- *
7
- * 1: workspace/repo metadata (cwd, git remote, repo key) on conversations.
8
- */
9
- export const PARSER_VERSION = 1;
@@ -1,60 +0,0 @@
1
- export const secretReplacement = "[REDACTED]";
2
- const literalIndicators = [
3
- "ghp_",
4
- "github_pat_",
5
- "napi_",
6
- "npg_",
7
- "sk-",
8
- ];
9
- const caseInsensitiveIndicators = [
10
- "bearer ",
11
- "begin private key",
12
- "begin rsa private key",
13
- "begin ec private key",
14
- "begin openssh private key",
15
- "token=",
16
- "key=",
17
- "api_key=",
18
- "connection_string=",
19
- "database_url=",
20
- "db_url=",
21
- "password=",
22
- "postgres_url=",
23
- "postgresql_url=",
24
- "postgres://",
25
- "postgresql://",
26
- "secret=",
27
- "supabase",
28
- ];
29
- const fullReplacementPatterns = [
30
- /\bghp_[A-Za-z0-9_]{20,}\b/g,
31
- /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g,
32
- /\bnapi_[A-Za-z0-9_-]{16,}\b/g,
33
- /\bnpg_[A-Za-z0-9_-]{16,}\b/g,
34
- /\bsk-[A-Za-z0-9_-]{16,}\b/g,
35
- /\bsb_(?:publishable|secret)_[A-Za-z0-9_-]{16,}\b/g,
36
- /\bsbp_[A-Za-z0-9_-]{16,}\b/g,
37
- /\bpostgres(?:ql)?:\/\/[^\s'"`<>]+/gi,
38
- /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}\b/gi,
39
- /-----BEGIN (?:[A-Z0-9 ]+ )?PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z0-9 ]+ )?PRIVATE KEY-----/gi,
40
- ];
41
- const uuidPattern = /\b[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}\b/gi;
42
- const sensitiveKeyNamePattern = String.raw `[A-Za-z0-9_]*(?:token|api[_-]?key|apikey|password|secret|[_-]key|connection[_-]?string|database[_-]?url|db[_-]?url|postgres(?:ql)?[_-]?url)|key`;
43
- const unquotedAssignmentValuePattern = "[^\\s&;,'\"`}\\[\\]]+";
44
- const assignmentPattern = new RegExp(String.raw `\b(${sensitiveKeyNamePattern})\b(\s*=\s*)(["']?)(${unquotedAssignmentValuePattern})\3?`, "gi");
45
- const quotedAssignmentPattern = new RegExp(String.raw `(["'])(${sensitiveKeyNamePattern})\1(\s*:\s*)(["'])([^"']+)\4`, "gi");
46
- export function redactSecrets(text) {
47
- const withoutUuids = text.replace(uuidPattern, secretReplacement);
48
- if (!mightContainSecret(withoutUuids))
49
- return withoutUuids;
50
- const withoutWholeSecrets = fullReplacementPatterns.reduce((result, pattern) => result.replace(pattern, secretReplacement), withoutUuids);
51
- return withoutWholeSecrets
52
- .replace(quotedAssignmentPattern, (_match, keyQuote, key, separator, valueQuote) => `${keyQuote}${key}${keyQuote}${separator}${valueQuote}${secretReplacement}${valueQuote}`)
53
- .replace(assignmentPattern, (_match, key, separator, valueQuote) => `${key}${separator}${valueQuote}${secretReplacement}${valueQuote}`);
54
- }
55
- export function mightContainSecret(text) {
56
- if (literalIndicators.some((indicator) => text.includes(indicator)))
57
- return true;
58
- const lowered = text.toLowerCase();
59
- return caseInsensitiveIndicators.some((indicator) => lowered.includes(indicator));
60
- }
@@ -1,134 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { closeSync, openSync } from "node:fs";
3
- import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
4
- import { getDaemonPaths } from "../config/paths.js";
5
- const defaultProcessDeps = {
6
- spawn,
7
- argv: process.argv,
8
- execPath: process.execPath,
9
- kill: process.kill,
10
- readProcessCmdline,
11
- };
12
- export async function startBackgroundDaemon(paths = getDaemonPaths(), depsOverride = {}) {
13
- const deps = resolveProcessDeps(depsOverride);
14
- const existingPid = await readPid(paths);
15
- if (existingPid && await isDaemonRunning(existingPid, deps)) {
16
- return `nessie-daemon is already running with pid ${existingPid}.`;
17
- }
18
- if (existingPid)
19
- await removePid(paths);
20
- await mkdir(paths.stateDir, { recursive: true });
21
- if (!await reservePidFile(paths))
22
- return "nessie-daemon is already starting.";
23
- const out = openSync(paths.logFile, "a");
24
- const err = openSync(paths.logFile, "a");
25
- let child;
26
- try {
27
- child = deps.spawn(deps.execPath, [cliPath(deps.argv), "run"], {
28
- detached: true,
29
- stdio: ["ignore", out, err],
30
- });
31
- child.unref();
32
- }
33
- finally {
34
- closeSync(out);
35
- closeSync(err);
36
- }
37
- if (!child.pid) {
38
- await removePid(paths);
39
- throw new Error("Failed to start nessie-daemon in background.");
40
- }
41
- await writeFile(paths.pidFile, `${child.pid}\n`, { mode: 0o600 });
42
- return `Started nessie-daemon with pid ${child.pid}. Logs: ${paths.logFile}`;
43
- }
44
- export async function stopBackgroundDaemon(paths = getDaemonPaths(), depsOverride = {}) {
45
- const deps = resolveProcessDeps(depsOverride);
46
- const pid = await readPid(paths);
47
- if (!pid)
48
- return "nessie-daemon is not running.";
49
- if (!await isDaemonRunning(pid, deps)) {
50
- await removePid(paths);
51
- return "nessie-daemon is not running.";
52
- }
53
- deps.kill(pid, "SIGTERM");
54
- await removePid(paths);
55
- return `Stopped nessie-daemon with pid ${pid}.`;
56
- }
57
- export async function backgroundDaemonStatus(paths = getDaemonPaths(), depsOverride = {}) {
58
- const deps = resolveProcessDeps(depsOverride);
59
- const pid = await readPid(paths);
60
- if (!pid)
61
- return null;
62
- if (!await isDaemonRunning(pid, deps)) {
63
- await removePid(paths);
64
- return "nessie-daemon is not running.";
65
- }
66
- return `nessie-daemon is running with pid ${pid}. Logs: ${paths.logFile}`;
67
- }
68
- async function readPid(paths) {
69
- try {
70
- const raw = await readFile(paths.pidFile, "utf8");
71
- const pid = Number.parseInt(raw.trim(), 10);
72
- return Number.isFinite(pid) && pid > 0 ? pid : null;
73
- }
74
- catch (error) {
75
- if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT")
76
- return null;
77
- throw error;
78
- }
79
- }
80
- async function removePid(paths) {
81
- try {
82
- await unlink(paths.pidFile);
83
- }
84
- catch (error) {
85
- if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT")
86
- return;
87
- throw error;
88
- }
89
- }
90
- async function reservePidFile(paths) {
91
- try {
92
- await writeFile(paths.pidFile, "starting\n", { mode: 0o600, flag: "wx" });
93
- return true;
94
- }
95
- catch (error) {
96
- if (typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST")
97
- return false;
98
- throw error;
99
- }
100
- }
101
- async function isDaemonRunning(pid, deps) {
102
- try {
103
- deps.kill(pid, 0);
104
- }
105
- catch {
106
- return false;
107
- }
108
- const cmdline = await deps.readProcessCmdline(pid);
109
- if (cmdline === null)
110
- return false;
111
- return cmdline.includes(cliPath(deps.argv)) && cmdline.includes(" run");
112
- }
113
- async function readProcessCmdline(pid) {
114
- try {
115
- const raw = await readFile(`/proc/${pid}/cmdline`);
116
- const cmdline = raw.toString("utf8").replace(/\0/g, " ").trim();
117
- return cmdline || null;
118
- }
119
- catch {
120
- return null;
121
- }
122
- }
123
- function resolveProcessDeps(overrides) {
124
- return {
125
- ...defaultProcessDeps,
126
- ...overrides,
127
- };
128
- }
129
- function cliPath(argv) {
130
- const path = argv[1];
131
- if (!path)
132
- throw new Error("Cannot determine nessie-daemon CLI path.");
133
- return path;
134
- }