@tacuchi/agent-workflow-cli 21.10.0 → 21.11.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.
Files changed (30) hide show
  1. package/dist/application/dev-bootstrap-dsn-service.js +45 -35
  2. package/dist/application/dev-bootstrap-dsn-service.js.map +1 -1
  3. package/dist/application/docs-canon-service.js +68 -0
  4. package/dist/application/docs-canon-service.js.map +1 -0
  5. package/dist/application/dsn-reader-service.js +53 -2
  6. package/dist/application/dsn-reader-service.js.map +1 -1
  7. package/dist/application/mcp-dbhub-launcher.js +193 -34
  8. package/dist/application/mcp-dbhub-launcher.js.map +1 -1
  9. package/dist/application/mcp-doctor-service.js +13 -4
  10. package/dist/application/mcp-doctor-service.js.map +1 -1
  11. package/dist/application/mcp-test-connection-service.js +5 -11
  12. package/dist/application/mcp-test-connection-service.js.map +1 -1
  13. package/dist/application/visibility-doctor-service.js +89 -49
  14. package/dist/application/visibility-doctor-service.js.map +1 -1
  15. package/dist/application/workspace-migrate/apply.js +57 -0
  16. package/dist/application/workspace-migrate/apply.js.map +1 -0
  17. package/dist/application/workspace-migrate/markers.js +177 -0
  18. package/dist/application/workspace-migrate/markers.js.map +1 -0
  19. package/dist/application/workspace-migrate/plan.js +171 -0
  20. package/dist/application/workspace-migrate/plan.js.map +1 -0
  21. package/dist/application/workspace-migrate/preview.js +111 -0
  22. package/dist/application/workspace-migrate/preview.js.map +1 -0
  23. package/dist/cli/commands/unknown-flags.js +51 -0
  24. package/dist/cli/commands/unknown-flags.js.map +1 -0
  25. package/dist/cli/commands/visibility.js +105 -16
  26. package/dist/cli/commands/visibility.js.map +1 -1
  27. package/dist/cli/commands/workspace-migrate.js +70 -0
  28. package/dist/cli/commands/workspace-migrate.js.map +1 -0
  29. package/package.json +1 -1
  30. package/skills/w/commands/fix-git.md +17 -23
@@ -1,64 +1,74 @@
1
1
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
  export function runBootstrapDsn(paths, input) {
4
- const target = paths.userDsnFile();
5
- const lines = [];
6
- const written = [];
7
- if (input.certDsn) {
8
- lines.push(`DB_CERT_DSN=${input.certDsn}`);
9
- written.push("DB_CERT_DSN");
10
- }
11
- if (input.prodDsn) {
12
- lines.push(`DB_PROD_DSN=${input.prodDsn}`);
13
- written.push("DB_PROD_DSN");
14
- }
15
- if (lines.length === 0) {
4
+ const entries = [];
5
+ if (input.certDsn)
6
+ entries.push({ key: "DB_CERT_DSN", value: input.certDsn });
7
+ if (input.prodDsn)
8
+ entries.push({ key: "DB_PROD_DSN", value: input.prodDsn });
9
+ if (entries.length === 0) {
16
10
  return {
17
11
  error: "Ni DB_CERT_DSN ni DB_PROD_DSN visibles en el entorno actual. Exportalas en ~/.zshenv (macOS/Linux) o System Environment (Windows) y reabrí Claude Code desde una terminal donde 'echo $DB_CERT_DSN' devuelva valor.",
18
12
  exitCode: 2,
19
13
  };
20
14
  }
21
- mkdirSync(dirname(target), { recursive: true });
22
- writeFileSync(target, `${lines.join("\n")}\n`, "utf-8");
23
- try {
24
- chmodSync(target, 0o600);
25
- }
26
- catch {
27
- // ignore chmod failures
28
- }
29
- return { ok: true, path: target, wrote: written };
15
+ const path = writeDsnValues(paths, entries);
16
+ return { ok: true, path, wrote: entries.map((entry) => entry.key) };
30
17
  }
31
18
  export function writeDsnValue(paths, input) {
19
+ const path = writeDsnValues(paths, [input]);
20
+ return { ok: true, path, key: input.key };
21
+ }
22
+ /**
23
+ * Upsert of every entry into dsn.env, preserving the lines this call does not
24
+ * own: the file is shared by every connection the user ever bootstrapped, so a
25
+ * whole-file rewrite would drop the DSNs of the other ones.
26
+ */
27
+ function writeDsnValues(paths, entries) {
32
28
  const target = paths.userDsnFile();
33
- const existing = readDsnLines(target);
34
- const next = upsertDsnLine(existing, input.key, input.value);
29
+ let lines = readDsnLines(target);
30
+ for (const entry of entries) {
31
+ lines = upsertDsnLine(lines, entry.key, entry.value);
32
+ }
35
33
  mkdirSync(dirname(target), { recursive: true });
36
- writeFileSync(target, `${next.join("\n")}\n`, "utf-8");
34
+ writeFileSync(target, `${lines.join("\n")}\n`, "utf-8");
37
35
  try {
38
36
  chmodSync(target, 0o600);
39
37
  }
40
38
  catch {
41
- // ignore chmod failures
39
+ // Best effort: filesystems without POSIX modes (Windows, FAT) must not fail
40
+ // a write that already landed. The DSN is still written.
42
41
  }
43
- return { ok: true, path: target, key: input.key };
42
+ return target;
44
43
  }
45
44
  function readDsnLines(path) {
46
45
  if (!existsSync(path))
47
46
  return [];
48
- return readFileSync(path, "utf-8")
49
- .split(/\r?\n/)
50
- .filter((line) => line.length > 0);
47
+ const lines = readFileSync(path, "utf-8").split(/\r?\n/);
48
+ // Only the empty tail the final newline produces is dropped. Blank lines
49
+ // inside the file are the user's own grouping of a file this tool shares with
50
+ // them, and an upsert has no business collapsing it.
51
+ if (lines[lines.length - 1] === "")
52
+ lines.pop();
53
+ return lines;
51
54
  }
52
55
  function upsertDsnLine(lines, key, value) {
53
56
  const prefix = `${key}=`;
57
+ const next = [];
54
58
  let replaced = false;
55
- const next = lines.map((line) => {
56
- if (line.startsWith(prefix)) {
57
- replaced = true;
58
- return `${key}=${value}`;
59
+ for (const line of lines) {
60
+ // Compare the line trimmed: an indented assignment still assigns, and
61
+ // leaving it behind would keep a stale credential alive in a file the user
62
+ // believes updated. Duplicates of the same key collapse into the first.
63
+ if (!line.trimStart().startsWith(prefix)) {
64
+ next.push(line);
65
+ continue;
59
66
  }
60
- return line;
61
- });
67
+ if (replaced)
68
+ continue;
69
+ replaced = true;
70
+ next.push(`${key}=${value}`);
71
+ }
62
72
  if (!replaced)
63
73
  next.push(`${key}=${value}`);
64
74
  return next;
@@ -1 +1 @@
1
- {"version":3,"file":"dev-bootstrap-dsn-service.js","sourceRoot":"","sources":["../../src/application/dev-bootstrap-dsn-service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dev-bootstrap-dsn-service.js","sourceRoot":"","sources":["../../src/application/dev-bootstrap-dsn-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ import { parse as parseToml } from "smol-toml";
2
+ import { checkSafeRelativePath } from "../domain/safe-path.js";
3
+ const TABLE = "docs";
4
+ export async function resolveDocsCanon(fs, paths, categories) {
5
+ const canon = {};
6
+ // Workspace last: it overrides the user-global default, same order as skills.
7
+ for (const path of [paths.userSkillsToml(), paths.cwdSkillsToml()]) {
8
+ if (!(await fs.exists(path)))
9
+ continue;
10
+ const level = await readDocsTable(fs, path);
11
+ if (!level.ok)
12
+ return level;
13
+ for (const [category, dir] of Object.entries(level.table)) {
14
+ const checked = checkDestination(path, category, dir, categories);
15
+ if (!checked.ok)
16
+ return checked;
17
+ canon[category] = checked.dir;
18
+ }
19
+ }
20
+ return { ok: true, canon };
21
+ }
22
+ async function readDocsTable(fs, path) {
23
+ let parsed;
24
+ try {
25
+ parsed = parseToml(await fs.readText(path));
26
+ }
27
+ catch (error) {
28
+ return { ok: false, error: `${path} no se puede leer como TOML: ${String(error)}` };
29
+ }
30
+ const table = parsed[TABLE];
31
+ if (table === undefined || table === null)
32
+ return { ok: true, table: {} };
33
+ if (typeof table !== "object" || Array.isArray(table)) {
34
+ return { ok: false, error: `${path}: [${TABLE}] no es una tabla` };
35
+ }
36
+ return { ok: true, table: table };
37
+ }
38
+ function checkDestination(path, category, dir, categories) {
39
+ if (!categories.includes(category)) {
40
+ return {
41
+ ok: false,
42
+ error: `${path}: [${TABLE}] no reconoce '${category}'; las categorías son: ${categories.join(", ")}`,
43
+ };
44
+ }
45
+ if (typeof dir !== "string") {
46
+ return { ok: false, error: `${path}: [${TABLE}].${category} tiene que ser una ruta de texto` };
47
+ }
48
+ // Same guard every write boundary uses, so a canon can move a category inside
49
+ // the workspace and never outside it.
50
+ const safe = checkSafeRelativePath(dir.replace(/\/+$/, ""));
51
+ if (!safe.ok) {
52
+ return { ok: false, error: `${path}: [${TABLE}].${category} = '${dir}' ${safe.why}` };
53
+ }
54
+ // Inside the workspace is not enough: it has to be DOCUMENTAL. A canon
55
+ // pointing at a dot-directory would publish dossiers into the CLI's own
56
+ // runtime — `.workflow/sessions` is the sharp case, because a published unit
57
+ // named `NNN-export-…` is then enumerated as a session and the workspace grows
58
+ // a phantom open line that shows up in `status` and in the next export's
59
+ // corpus. The tool's own state is not a place a document may be published to.
60
+ if (safe.path.split("/")[0]?.startsWith(".") === true) {
61
+ return {
62
+ ok: false,
63
+ error: `${path}: [${TABLE}].${category} = '${dir}' apunta a un directorio oculto; el canon documental publica documentos, no estado interno de la herramienta`,
64
+ };
65
+ }
66
+ return { ok: true, dir: safe.path };
67
+ }
68
+ //# sourceMappingURL=docs-canon-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"docs-canon-service.js","sourceRoot":"","sources":["../../src/application/docs-canon-service.ts"],"names":[],"mappings":"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"}
@@ -22,7 +22,58 @@ export function readBootstrapDsn(paths) {
22
22
  return { path, exists: true, values };
23
23
  }
24
24
  export function dsnKeyForInstance(instance) {
25
- const normalized = normalizeMcpInstance(instance);
26
- return `DB_${normalized.toUpperCase().replace(/-/g, "_")}_DSN`;
25
+ return dsnKeyFromSegments(instanceSegments(instance));
26
+ }
27
+ /**
28
+ * The variable names that may hold the DSN of `instance`, most specific first:
29
+ * the canonical key, and the one without its leading segment. An alias usually
30
+ * carries an organisation prefix (`qtc-cert`) that the variable the user really
31
+ * exported does not (`DB_CERT_DSN`).
32
+ *
33
+ * Exactly ONE segment is ever dropped. `qtc-cert-ro` may fall back to
34
+ * DB_CERT_RO_DSN but never to DB_RO_DSN, because collapsing an alias to its last
35
+ * segment makes different organisations share one generic name — `acme-prod` and
36
+ * `qtc-prod` would both reach DB_PROD_DSN, and starting a server against another
37
+ * environment's credential is a worse outcome than refusing to start.
38
+ */
39
+ export function dsnKeyCandidates(instance) {
40
+ const segments = instanceSegments(instance);
41
+ // A single-segment or degenerate alias still yields its canonical key: callers
42
+ // must always have at least one name to probe and to name in errors.
43
+ if (segments.length <= 1)
44
+ return [dsnKeyFromSegments(segments)];
45
+ return [dsnKeyFromSegments(segments), dsnKeyFromSegments(segments.slice(1))];
46
+ }
47
+ function instanceSegments(instance) {
48
+ return normalizeMcpInstance(instance)
49
+ .split("-")
50
+ .filter((segment) => segment.length > 0);
51
+ }
52
+ function dsnKeyFromSegments(segments) {
53
+ return `DB_${segments.join("_").toUpperCase()}_DSN`;
54
+ }
55
+ /**
56
+ * Single resolution path shared by the dbhub launcher and the connection
57
+ * tester. Precedence: the whole candidate list in `env` first, then the whole
58
+ * list in the dsn.env file — an exported variable beats a persisted one, and
59
+ * among exported variables the most specific name wins.
60
+ *
61
+ * Throws whatever `readBootstrapDsn` throws (unreadable dsn.env): each caller
62
+ * decides how that surfaces instead of it being swallowed here.
63
+ */
64
+ export function resolveDsnFromCandidates(candidates, env, paths) {
65
+ for (const variable of candidates) {
66
+ const fromEnv = env[variable];
67
+ if (fromEnv && fromEnv.length > 0)
68
+ return { dsn: fromEnv, variable, source: "env" };
69
+ }
70
+ // Read once, and only after every candidate missed in the environment.
71
+ const values = readBootstrapDsn(paths).values;
72
+ for (const variable of candidates) {
73
+ const fromFile = values[variable];
74
+ if (fromFile && fromFile.length > 0)
75
+ return { dsn: fromFile, variable, source: "dsn.env" };
76
+ }
77
+ return null;
27
78
  }
28
79
  //# sourceMappingURL=dsn-reader-service.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"dsn-reader-service.js","sourceRoot":"","sources":["../../src/application/dsn-reader-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAoB,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAShF,MAAM,UAAU,gBAAgB,CAAC,KAAmB;IAClD,MAAM,IAAI,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;IACjC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACtB,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IAC7C,CAAC;IACD,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACzC,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QACxD,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC7B,IAAI,EAAE,IAAI,CAAC;YAAE,SAAS;QACtB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACrC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACtC,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC;YAAE,MAAM,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;IACxC,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AACxC,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,QAAqB;IACrD,MAAM,UAAU,GAAG,oBAAoB,CAAC,QAAQ,CAAC,CAAC;IAClD,OAAO,MAAM,UAAU,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC;AACjE,CAAC"}
1
+ {"version":3,"file":"dsn-reader-service.js","sourceRoot":"","sources":["../../src/application/dsn-reader-service.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,9 @@
1
- // Launcher that resolves the DSN and spawns `npx -y @bytebase/dbhub` with stdio inherit.
1
+ // Launcher that resolves the DSN and spawns `npx -y @bytebase/dbhub`.
2
+ // stdin/stderr are inherited; stdout is piped so the dbhub startup banner never
3
+ // reaches the MCP JSON-RPC channel (see DbhubBannerFilter).
2
4
  import { spawn } from "node:child_process";
3
5
  import { normalizeDsnVarName, validateDsnVarName, validateMcpInstance, } from "../domain/mcp-entry.js";
4
- import { dsnKeyForInstance, readBootstrapDsn } from "./dsn-reader-service.js";
6
+ import { dsnKeyCandidates, resolveDsnFromCandidates, } from "./dsn-reader-service.js";
5
7
  export const DBHUB_DSN_VAR_ENV = "DBHUB_DSN_VAR";
6
8
  export class DbhubLauncherError extends Error {
7
9
  constructor(message) {
@@ -10,38 +12,154 @@ export class DbhubLauncherError extends Error {
10
12
  }
11
13
  }
12
14
  export function resolveDsn(instance, deps) {
13
- const dsnVar = resolveDsnVar(instance, deps);
14
- const fromEnv = deps.env[dsnVar];
15
- if (fromEnv && fromEnv.length > 0) {
16
- return { dsn: fromEnv, source: "env" };
17
- }
18
- // An unreadable dsn.env (e.g. it is a directory, or a permission/race error)
19
- // falls through to the DbhubLauncherError below, not a raw fs throw.
20
- let fromFile;
15
+ const candidates = dsnVarCandidates(instance, deps);
16
+ let resolved = null;
17
+ let fileError = null;
21
18
  try {
22
- fromFile = readBootstrapDsn(deps.paths).values[dsnVar];
19
+ resolved = resolveDsnFromCandidates(candidates, deps.env, deps.paths);
23
20
  }
24
- catch {
25
- fromFile = undefined;
21
+ catch (err) {
22
+ // An unreadable dsn.env (a directory in its place, a permission or race
23
+ // error) surfaces as this launcher's own diagnostic naming the cause,
24
+ // never as a raw fs throw and never as a silent "not found".
25
+ fileError = err instanceof Error ? err.message : String(err);
26
26
  }
27
- if (fromFile && fromFile.length > 0) {
28
- return { dsn: fromFile, source: "dsn.env" };
27
+ if (resolved === null) {
28
+ throw new DbhubLauncherError(buildUnresolvedDsnMessage(instance, candidates, deps, fileError));
29
29
  }
30
- throw new DbhubLauncherError(`[dbhub-mcp-runner] ${dsnVar} no visible — no está en process.env ni en ${deps.paths.userDsnFile()}. Asegurate de exportarlo en ~/.zshenv (macOS/Linux) o System Environment (Windows) y reiniciar Claude Code desde una terminal donde 'echo $${dsnVar}' devuelva valor.`);
30
+ reportNonCanonicalVariable(resolved, candidates, deps);
31
+ return resolved;
31
32
  }
32
- function resolveDsnVar(instance, deps) {
33
+ /**
34
+ * Variable names to probe, most specific first. An explicit DBHUB_DSN_VAR names
35
+ * the variable exactly — no prefix-dropping candidates are derived from it.
36
+ */
37
+ function dsnVarCandidates(instance, deps) {
33
38
  const configured = deps.env[DBHUB_DSN_VAR_ENV];
34
- if (configured === undefined || configured.trim().length === 0) {
35
- return dsnKeyForInstance(instance);
39
+ if (configured !== undefined && configured.trim().length > 0) {
40
+ const validation = validateDsnVarName(configured);
41
+ if (!validation.ok) {
42
+ throw new DbhubLauncherError(`[dbhub-mcp-runner] ${DBHUB_DSN_VAR_ENV} inválida '${configured}': ${validation.error}`);
43
+ }
44
+ return [normalizeDsnVarName(validation.value)];
36
45
  }
37
- const validation = validateDsnVarName(configured);
38
- if (!validation.ok) {
39
- throw new DbhubLauncherError(`[dbhub-mcp-runner] ${DBHUB_DSN_VAR_ENV} inválida '${configured}': ${validation.error}`);
46
+ const [canonical, ...rest] = dsnKeyCandidates(instance);
47
+ if (canonical === undefined) {
48
+ throw new DbhubLauncherError(`[dbhub-mcp-runner] la conexión '${instance}' no deriva ninguna variable DSN`);
49
+ }
50
+ return [canonical, ...rest];
51
+ }
52
+ /**
53
+ * A DSN found under a non-canonical name is a resolution the user did not
54
+ * spell out: say which variable was used and which one was missing, so the
55
+ * mismatch never passes as a silent default.
56
+ */
57
+ function reportNonCanonicalVariable(resolved, candidates, deps) {
58
+ const canonical = candidates[0];
59
+ if (resolved.variable === canonical)
60
+ return;
61
+ const origin = resolved.source === "env" ? "process.env" : deps.paths.userDsnFile();
62
+ stderrWriter(deps)(`[dbhub-mcp-runner] usando ${resolved.variable} (desde ${origin}) porque ${canonical} no está definida.\n`);
63
+ }
64
+ function buildUnresolvedDsnMessage(instance, candidates, deps, fileError) {
65
+ const dsnFile = deps.paths.userDsnFile();
66
+ const probed = candidates.length === 1 ? "la variable" : "estas variables, en orden";
67
+ const where = fileError === null
68
+ ? `primero en process.env y después en ${dsnFile}`
69
+ : `primero en process.env; ${dsnFile} no se pudo leer (${fileError})`;
70
+ return [
71
+ `[dbhub-mcp-runner] no encontré el DSN de la conexión '${instance}'.`,
72
+ `Probé ${probed}: ${candidates.join(", ")} — ${where}.`,
73
+ `Salidas: exportá ${candidates[0]} en ~/.zshenv (macOS/Linux) o System Environment (Windows)`,
74
+ `y reabrí el host desde una terminal donde 'echo $${candidates[0]}' devuelva valor;`,
75
+ `o nombrá la variable exacta con ${DBHUB_DSN_VAR_ENV} en el entorno del servidor MCP;`,
76
+ `o registrá la conexión con 'aw mcp setup --instance ${instance} --dsn-var <NOMBRE>'.`,
77
+ ].join(" ");
78
+ }
79
+ function stderrWriter(deps) {
80
+ return (deps.stderr ??
81
+ ((chunk) => {
82
+ process.stderr.write(chunk);
83
+ }));
84
+ }
85
+ const NEWLINE = 0x0a;
86
+ const OPEN_BRACE = 0x7b;
87
+ const EMPTY_CHUNK = Buffer.alloc(0);
88
+ /** Narrow enough that a banner containing it verbatim is not a realistic risk. */
89
+ const JSONRPC_MARKER = Buffer.from('{"jsonrpc"');
90
+ /**
91
+ * Splits dbhub's stdout into banner (stderr) and protocol (stdout).
92
+ *
93
+ * `@bytebase/dbhub` prints an ASCII banner on stdout before speaking JSON-RPC,
94
+ * which corrupts the stdio channel every MCP client reads. Complete lines are
95
+ * routed to stderr until the first one whose first non-blank byte is `{`; from
96
+ * that line on the filter is `started` and the caller must hand the pipe over
97
+ * raw, so the protocol is never re-sliced. Pure on purpose: testable without
98
+ * spawning a process.
99
+ */
100
+ export class DbhubBannerFilter {
101
+ pending = EMPTY_CHUNK;
102
+ jsonStarted = false;
103
+ /** True once the JSON-RPC stream began: the caller switches to raw passthrough. */
104
+ get started() {
105
+ return this.jsonStarted;
106
+ }
107
+ push(chunk) {
108
+ if (this.jsonStarted)
109
+ return { stdout: chunk, stderr: EMPTY_CHUNK };
110
+ this.pending = this.pending.length === 0 ? chunk : Buffer.concat([this.pending, chunk]);
111
+ // A banner that does not end in a newline would otherwise swallow the first
112
+ // message glued to its tail, so the protocol marker is a second and narrower
113
+ // cut point: bytes before it are banner, bytes from it on are protocol.
114
+ const marker = this.pending.indexOf(JSONRPC_MARKER);
115
+ const banner = [];
116
+ let consumed = 0;
117
+ let newline = this.pending.indexOf(NEWLINE, consumed);
118
+ while (newline !== -1) {
119
+ const line = this.pending.subarray(consumed, newline + 1);
120
+ // A line that already opens the protocol is handed over from its first
121
+ // byte, blanks included: the marker cut is for a banner glued to the
122
+ // message, never an excuse to re-slice bytes that are already protocol.
123
+ if (startsJsonRpc(line))
124
+ return this.startAt(consumed, consumed, banner);
125
+ if (marker !== -1 && marker < newline)
126
+ return this.startAt(marker, consumed, banner);
127
+ banner.push(line);
128
+ consumed = newline + 1;
129
+ newline = this.pending.indexOf(NEWLINE, consumed);
130
+ }
131
+ if (marker !== -1)
132
+ return this.startAt(marker, consumed, banner);
133
+ this.pending = Buffer.from(this.pending.subarray(consumed));
134
+ return { stdout: EMPTY_CHUNK, stderr: Buffer.concat(banner) };
135
+ }
136
+ /** Protocol starts at `cut`: [consumed, cut) is banner, [cut, …) goes out verbatim. */
137
+ startAt(cut, consumed, banner) {
138
+ this.jsonStarted = true;
139
+ if (cut > consumed)
140
+ banner.push(this.pending.subarray(consumed, cut));
141
+ const stdout = Buffer.from(this.pending.subarray(cut));
142
+ this.pending = EMPTY_CHUNK;
143
+ return { stdout, stderr: Buffer.concat(banner) };
144
+ }
145
+ /** Stream ended: anything still buffered never became a JSON line, so it is banner. */
146
+ end() {
147
+ const stderr = this.pending;
148
+ this.pending = EMPTY_CHUNK;
149
+ return { stdout: EMPTY_CHUNK, stderr };
150
+ }
151
+ }
152
+ function startsJsonRpc(line) {
153
+ for (const byte of line) {
154
+ // Skip leading blanks (space, tab, CR, LF); a fully blank line is banner.
155
+ if (byte === 0x20 || byte === 0x09 || byte === 0x0d || byte === NEWLINE)
156
+ continue;
157
+ return byte === OPEN_BRACE;
40
158
  }
41
- return normalizeDsnVarName(validation.value);
159
+ return false;
42
160
  }
43
161
  /**
44
- * Resolves DSN and spawns `npx -y @bytebase/dbhub` with stdio inherited.
162
+ * Resolves the DSN and spawns `npx -y @bytebase/dbhub`.
45
163
  * Resolves only when the spawned child exits.
46
164
  */
47
165
  export async function runDbhubLauncher(input) {
@@ -53,25 +171,66 @@ export async function runDbhubLauncher(input) {
53
171
  const { dsn } = resolveDsn(instance, input.deps);
54
172
  const isWin = input.deps.platform === "win32";
55
173
  const cmd = isWin ? "npx.cmd" : "npx";
174
+ const protocol = input.deps.stdout ?? process.stdout;
56
175
  return new Promise((resolve, reject) => {
57
176
  const child = spawn(cmd, ["-y", "@bytebase/dbhub"], {
58
- stdio: "inherit",
177
+ stdio: ["inherit", "pipe", "inherit"],
59
178
  env: { ...input.deps.env, DSN: dsn },
60
179
  shell: isWin,
61
180
  });
181
+ if (child.stdout === null) {
182
+ reject(new DbhubLauncherError("[dbhub-mcp-runner] no pude capturar el stdout de dbhub"));
183
+ return;
184
+ }
185
+ forwardDbhubStdout(child.stdout, {
186
+ protocol,
187
+ diagnostics: stderrWriter(input.deps),
188
+ });
62
189
  child.on("error", (err) => reject(new DbhubLauncherError(`[dbhub-mcp-runner] ${err.message}`)));
63
- child.on("exit", (code, signal) => {
64
- if (signal) {
65
- // Honor signal: emulate `process.kill(process.pid, signal)` from JS launcher
66
- // by mapping to a non-zero exit. The CLI command will exit with that code.
67
- resolve({ exitCode: 128 + (signalToNumber(signal) ?? 0) });
68
- }
69
- else {
70
- resolve({ exitCode: code ?? 0 });
71
- }
190
+ // `close`, not `exit`: `exit` fires before the child's stdout is drained, so
191
+ // the tail of the protocol would still be in flight. And piping made a second
192
+ // loss possible that `stdio: "inherit"` ruled out structurally — the child
193
+ // used to write fd 1 itself, whereas now every byte is re-written through OUR
194
+ // stdout, and the CLI ends with process.exit(), which drops whatever is still
195
+ // queued on a pipe. The zero-length write is a drain barrier: its callback
196
+ // runs once everything before it reached the OS.
197
+ child.on("close", (code, signal) => {
198
+ // Honor signal: emulate `process.kill(process.pid, signal)` from JS launcher
199
+ // by mapping to a non-zero exit. The CLI command will exit with that code.
200
+ const exitCode = signal ? 128 + (signalToNumber(signal) ?? 0) : (code ?? 0);
201
+ protocol.write(EMPTY_CHUNK, () => resolve({ exitCode }));
72
202
  });
73
203
  });
74
204
  }
205
+ /**
206
+ * Wires a dbhub stdout stream to this process's channels: banner lines to
207
+ * diagnostics, protocol bytes to the protocol channel. Takes its channels as
208
+ * parameters so the handover can be exercised without spawning anything.
209
+ */
210
+ export function forwardDbhubStdout(source, channels) {
211
+ const filter = new DbhubBannerFilter();
212
+ const onData = (chunk) => {
213
+ const routed = filter.push(chunk);
214
+ if (routed.stderr.length > 0)
215
+ channels.diagnostics(routed.stderr.toString("utf-8"));
216
+ if (routed.stdout.length > 0)
217
+ channels.protocol.write(routed.stdout);
218
+ if (!filter.started)
219
+ return;
220
+ // Protocol started: pause before detaching, so no chunk can be emitted
221
+ // between removing the listener and attaching the pipe. `end: false` keeps
222
+ // the child's EOF from closing our own stdout.
223
+ source.pause();
224
+ source.removeListener("data", onData);
225
+ source.pipe(channels.protocol, { end: false });
226
+ };
227
+ source.on("data", onData);
228
+ source.on("end", () => {
229
+ const rest = filter.end();
230
+ if (rest.stderr.length > 0)
231
+ channels.diagnostics(rest.stderr.toString("utf-8"));
232
+ });
233
+ }
75
234
  function signalToNumber(signal) {
76
235
  // Common signals; fallback to null (will produce exitCode 128 + 0 = 128).
77
236
  const map = {
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-dbhub-launcher.js","sourceRoot":"","sources":["../../src/application/mcp-dbhub-launcher.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mcp-dbhub-launcher.js","sourceRoot":"","sources":["../../src/application/mcp-dbhub-launcher.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { isDeepStrictEqual } from "node:util";
2
2
  import { buildMcpEntry, normalizeMcpInstance, } from "../domain/mcp-entry.js";
3
- import { dsnKeyForInstance, readBootstrapDsn } from "./dsn-reader-service.js";
3
+ import { dsnKeyCandidates, dsnKeyForInstance, readBootstrapDsn } from "./dsn-reader-service.js";
4
4
  import { readMcpEntry } from "./mcp-host-reader.js";
5
5
  import { resolveScopeDir } from "./mcp-scope-common.js";
6
6
  export function runMcpDoctor(env, paths, input) {
@@ -22,10 +22,19 @@ export function runMcpDoctor(env, paths, input) {
22
22
  return { scope: input.scope, scope_dir: scopeDir, reports, summary };
23
23
  }
24
24
  function buildReport(env, host, instance, scopeDir, dsn, scope, dsnVars) {
25
- const dsnKey = dsnVars?.[normalizeMcpInstance(instance)] ?? dsnKeyForInstance(instance);
26
- const entry = buildMcpEntry(instance, dsnVars?.[normalizeMcpInstance(instance)]);
25
+ const declaredVar = dsnVars?.[normalizeMcpInstance(instance)];
26
+ const entry = buildMcpEntry(instance, declaredVar);
27
27
  const snapshot = readMcpEntry(host, scopeDir, entry.name, scope);
28
- const dsnPresent = Boolean(env.get(dsnKey)) || (dsn.exists && Boolean(dsn.values[dsnKey]));
28
+ // Probe the same chain the launcher probes, in the same order. A doctor that
29
+ // only knows the canonical name reports `missing-dsn` for a connection the
30
+ // launcher would start happily — a diagnosis contradicting the runtime is
31
+ // worse than none. What gets reported is the variable that carries the value,
32
+ // never the canonical name assumed.
33
+ const candidates = declaredVar !== undefined ? [declaredVar] : dsnKeyCandidates(instance);
34
+ const resolvedKey = candidates.find((key) => Boolean(env.get(key))) ??
35
+ (dsn.exists ? candidates.find((key) => Boolean(dsn.values[key])) : undefined);
36
+ const dsnKey = resolvedKey ?? candidates[0] ?? dsnKeyForInstance(instance);
37
+ const dsnPresent = resolvedKey !== undefined;
29
38
  const dsnInfo = {
30
39
  path: dsn.path,
31
40
  exists: dsn.exists,