| 104 | } |
| 105 | |
| 106 | export class Transport { |
| 107 | #child?: ChildProcessWithoutNullStreams; |
| 108 | #stdin?: Writable; |
| 109 | #pending = new Map<number, Pending>(); |
| 110 | #nextId = 1; |
| 111 | #closed = false; |
| 112 | #starting?: Promise<void>; |
| 113 | |
| 114 | constructor( |
| 115 | private readonly cfg: ClientConfig, |
| 116 | private readonly binPath: string, |
| 117 | ) {} |
| 118 | |
| 119 | /** |
| 120 | * @internal Test seam: drive the demux off a provided stdin/stdout pair |
| 121 | * instead of spawning a binary. Used by the transport unit tests, mirroring |
| 122 | * the Python test that injects a fake process. |
| 123 | */ |
| 124 | attachStreamsForTest(stdin: Writable, stdout: Readable): void { |
| 125 | this.#stdin = stdin; |
| 126 | this.#starting = Promise.resolve(); |
| 127 | this.#child = { stdin } as unknown as ChildProcessWithoutNullStreams; |
| 128 | this.#startReader(stdout); |
| 129 | } |
| 130 | |
| 131 | // -- lifecycle --------------------------------------------------------- |
| 132 | |
| 133 | private async ensureStarted(): Promise<void> { |
| 134 | if (this.#child) return; |
| 135 | if (this.#starting) return this.#starting; |
| 136 | this.#starting = this.#spawn(); |
| 137 | try { |
| 138 | await this.#starting; |
| 139 | } finally { |
| 140 | this.#starting = undefined; |
| 141 | } |
| 142 | } |
| 143 | |
| 144 | async #spawn(): Promise<void> { |
| 145 | const child = spawn(this.binPath, ["--stdio"], { |
| 146 | stdio: ["pipe", "pipe", "pipe"], |
| 147 | env: { |
| 148 | ...process.env, |
| 149 | CMDOP_TOKEN: this.cfg.token, |
| 150 | CMDOP_BASE_URL: this.cfg.baseUrl, |
| 151 | // Django platform plane (skills) — separate credential + base URL. |
| 152 | CMDOP_API_BASE_URL: this.cfg.apiBaseUrl, |
| 153 | ...(this.cfg.apiKey ? { CMDOP_API_KEY: this.cfg.apiKey } : {}), |
| 154 | }, |
| 155 | windowsHide: true, |
| 156 | }); |
| 157 | this.#child = child; |
| 158 | this.#stdin = child.stdin; |
| 159 | |
| 160 | // stderr is NOT part of the protocol — drain it so it never blocks/corrupts |
| 161 | // the pipe (the core logs to a file when CMDOP_DEBUG=1). |
| 162 | child.stderr.on("data", () => {}); |
| 163 |
nothing calls this directly
no outgoing calls
no test coverage detected