@isomorph.ai/cli 0.4.0-rc.1 → 0.4.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.
@@ -11,7 +11,7 @@ import { appRoot, requestResourceName } from "./kit.js";
11
11
  import { initKit } from "./starter.js";
12
12
  import { agentPaths, agentSetup } from "./agent-setup.js";
13
13
  import { detachDev, startDev } from "./dev.js";
14
- import { ensureSdk, LocalRuntime, readDevLock, releaseDevLock, runCommand } from "./local-runtime.js";
14
+ import { endProcess, ensureSdk, LocalRuntime, readDevLock, releaseDevLock, runCommand } from "./local-runtime.js";
15
15
  import { CHECKS_FAILED_HINT, checksFailedMessage, runChecks } from "./check.js";
16
16
  import { assertDeployReady, companySystemsLine, GovernanceClient, groupGrants, IDENTITY_WORDS, integrationsCatalog, integrationsStatus, renderGrantGroup, renderIntegrationsCatalog, requestIntegrations } from "./integrations.js";
17
17
  import { runJob } from "./jobs.js";
@@ -144,12 +144,14 @@ else {
144
144
  else if (command === "stop") {
145
145
  const runtime = new LocalRuntime(target);
146
146
  const lock = await readDevLock(target);
147
- if (lock && lock.pid !== process.pid) {
148
- try {
149
- process.kill(lock.pid, "SIGTERM");
150
- }
151
- catch { /* already gone */ }
152
- }
147
+ // The dev process first (its own shutdown ends what it can), then the
148
+ // children it recorded, then Postgres by its controller: after a
149
+ // terminated dev on Windows, or `dev --detach`, nothing else ends them.
150
+ if (lock && lock.pid !== process.pid)
151
+ await endProcess(lock.pid);
152
+ for (const pid of Object.values(lock?.children ?? {}))
153
+ if (pid && pid !== process.pid)
154
+ await endProcess(pid);
153
155
  const stopped = await runtime.stop();
154
156
  await releaseDevLock(target);
155
157
  emit(summaryEnvelope({ project: runtime.project, stopped: stopped.stopped, volumesRetained: true }));
@@ -5,7 +5,7 @@ import { join } from "node:path";
5
5
  import { createForwarder } from "./forwarder.js";
6
6
  import { readKitLock } from "./kit.js";
7
7
  import { GovernanceClient, ensureLinkedApp, fileDeclaredRequests, IDENTITY_WORDS, renderGrantGroup } from "./integrations.js";
8
- import { acquireDevLock, allocatePorts, ensureSdk, LocalRuntime, nodePackageCommand, pidAlive, readDevLock, releaseDevLock, runCommand } from "./local-runtime.js";
8
+ import { acquireDevLock, allocatePorts, ensureSdk, LocalRuntime, nodePackageCommand, pidAlive, readDevLock, recordDevChildren, releaseDevLock, runCommand } from "./local-runtime.js";
9
9
  import { kitPaths } from "./kit.js";
10
10
  import { CliError, safeError } from "./output.js";
11
11
  /**
@@ -76,6 +76,7 @@ export async function startDev(root, options) {
76
76
  const npm = nodePackageCommand("npm", ["run", "dev"]);
77
77
  vite = spawn(npm.command, npm.args, { cwd: root, env: { ...env, ISOMORPH_LOCAL_ORIGIN: origin, ISOMORPH_VITE_PORT: String(ports.vite) }, stdio: ["ignore", "inherit", "inherit"] });
78
78
  vite.on("error", () => options.output("Vite could not be started; is npm installed and `npm install` done?"));
79
+ await recordDevChildren(root, { ...(vite.pid ? { vite: vite.pid } : {}), ...(runtime.gatewayPid ? { gateway: runtime.gatewayPid } : {}) });
79
80
  const company = options.company;
80
81
  let warnedAuth = false;
81
82
  let lockAppId = (await readKitLock(root))?.appId || undefined;
@@ -133,6 +133,28 @@ export async function readDevLock(root) {
133
133
  }
134
134
  }
135
135
  export async function releaseDevLock(root) { await rm(kitPaths(root).devLock, { force: true }); }
136
+ /** Records the child processes of a running dev session in its lock (see `DevLock.children`). */
137
+ export async function recordDevChildren(root, children) {
138
+ const lock = await readDevLock(root);
139
+ if (!lock)
140
+ return;
141
+ await writeFile(kitPaths(root).devLock, JSON.stringify({ ...lock, children: { ...lock.children, ...children } }));
142
+ }
143
+ /**
144
+ * Ends a process this CLI started from another process: SIGTERM where a signal
145
+ * reaches a shutdown handler, `taskkill /t` on Windows where it does not and
146
+ * the process tree (npm → vite → esbuild) would otherwise survive.
147
+ */
148
+ export async function endProcess(pid, platform = process.platform) {
149
+ if (platform === "win32") {
150
+ await runWindowsController("taskkill", ["/pid", String(pid), "/f", "/t"]);
151
+ return;
152
+ }
153
+ try {
154
+ process.kill(pid, "SIGTERM");
155
+ }
156
+ catch { /* already gone */ }
157
+ }
136
158
  /** The running dev origin for this app, if its lock holder is alive. */
137
159
  export async function runningOrigin(root, isAlive = pidAlive) {
138
160
  const lock = await readDevLock(root);
@@ -162,6 +184,8 @@ export class LocalRuntime {
162
184
  this.run = run;
163
185
  this.project = project;
164
186
  }
187
+ /** The App Gateway process this runtime started, for the dev lock. */
188
+ get gatewayPid() { return this.gateway?.pid; }
165
189
  async writeFiles(bundle, ports) {
166
190
  const paths = kitPaths(this.root);
167
191
  await mkdir(paths.state, { recursive: true });
@@ -270,11 +294,22 @@ export class LocalRuntime {
270
294
  this.gateway?.kill("SIGTERM");
271
295
  for (const child of this.gates.values())
272
296
  child.kill("SIGTERM");
297
+ const dataDir = join(kitPaths(this.root).state, "postgres");
273
298
  if (this.windowsPgCtl) {
274
- await runWindowsController(this.windowsPgCtl, ["stop", "-w", "-D", join(kitPaths(this.root).state, "postgres"), "-m", "fast"]);
299
+ await runWindowsController(this.windowsPgCtl, ["stop", "-w", "-D", dataDir, "-m", "fast"]);
300
+ }
301
+ else if (this.postgres) {
302
+ await this.postgres.stop().catch(() => undefined);
275
303
  }
276
- else {
277
- await this.postgres?.stop().catch(() => undefined);
304
+ else if (await exists(join(dataDir, "postmaster.pid"))) {
305
+ // A fresh process (`isomorph stop` after `dev --detach`, or after a
306
+ // terminated dev on Windows) owns no Postgres handle: the bundled
307
+ // controller stops the server the data directory says is running.
308
+ try {
309
+ const { pg_ctl } = await hydrateEmbeddedPostgres(this.run);
310
+ await this.run(pg_ctl, ["stop", "-w", "-D", dataDir, "-m", "fast"], { quiet: true });
311
+ }
312
+ catch { /* no controller for this platform: nothing more to stop */ }
278
313
  }
279
314
  return true;
280
315
  }
@@ -1 +1 @@
1
- export const CLI_VERSION = "0.4.0-rc.1";
1
+ export const CLI_VERSION = "0.4.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isomorph.ai/cli",
3
- "version": "0.4.0-rc.1",
3
+ "version": "0.4.0",
4
4
  "description": "Isomorph development kit CLI",
5
5
  "type": "module",
6
6
  "bin": {