@seekrit/cli 0.23.2 → 0.23.4

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 (2) hide show
  1. package/dist/index.js +129 -9
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { spawn } from "node:child_process";
2
+ import { spawn, spawnSync } from "node:child_process";
3
3
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
4
4
  import { z } from "zod";
5
5
  import { Command } from "commander";
@@ -1976,7 +1976,7 @@ function isServiceToken(value) {
1976
1976
  }
1977
1977
  //#endregion
1978
1978
  //#region package.json
1979
- var version = "0.23.2";
1979
+ var version = "0.23.4";
1980
1980
  //#endregion
1981
1981
  //#region ../../packages/api-client/src/index.ts
1982
1982
  var SeekritApiError = class extends Error {
@@ -4574,6 +4574,116 @@ async function materializeForRun(options) {
4574
4574
  };
4575
4575
  }
4576
4576
  }
4577
+ /**
4578
+ * Every live process `seekrit run` is responsible for tearing down, from a
4579
+ * single `ps(1)` snapshot: each descendant of `root`, plus — when we lead our
4580
+ * own process group — every other member of that group.
4581
+ *
4582
+ * Two mechanisms, because each covers the other's blind spot:
4583
+ *
4584
+ * - The parent-chain walk reaches a process that left our group via
4585
+ * setsid(2)/setpgid(2), which no group-wide kill and no tty-generated signal
4586
+ * can touch. But it loses anything whose chain up to `root` has already
4587
+ * broken — teardown kills the middle layers first, and an orphan reparents to
4588
+ * init, out of reach of any walk down from `root`.
4589
+ * - Group membership survives exactly that: a pgid outlives the parent that
4590
+ * passed it down. But it misses the ones that left the group.
4591
+ *
4592
+ * The group half applies only when our pid *is* our pgid, which is the shell-job
4593
+ * case: the shell put this command in a new group, so every other member of it
4594
+ * descends from us. Nested inside a shell script or a CI runner we are not the
4595
+ * leader and the group holds processes we did not start, so there we use the
4596
+ * walk alone.
4597
+ *
4598
+ * Zombies are skipped: they are already dead and waiting to be reaped, so
4599
+ * counting them would stall teardown for the full grace period and then report
4600
+ * a kill that did nothing. Returns nothing if `ps` is unavailable; callers fall
4601
+ * back to signalling the immediate child.
4602
+ */
4603
+ function teardownTargets(root) {
4604
+ const ps = spawnSync("ps", [
4605
+ "-A",
4606
+ "-o",
4607
+ "pid=,ppid=,pgid=,stat="
4608
+ ], { encoding: "utf8" });
4609
+ if (ps.status !== 0 || typeof ps.stdout !== "string") return [];
4610
+ const childrenOf = /* @__PURE__ */ new Map();
4611
+ const groupOf = /* @__PURE__ */ new Map();
4612
+ const isId = (value) => value !== void 0 && Number.isInteger(value);
4613
+ for (const line of ps.stdout.split("\n")) {
4614
+ const [pid, ppid, pgid, stat] = line.trim().split(/\s+/, 4);
4615
+ const [id, parent, group] = [
4616
+ pid,
4617
+ ppid,
4618
+ pgid
4619
+ ].map(Number);
4620
+ if (!isId(id) || !isId(parent) || !isId(group)) continue;
4621
+ if (stat?.startsWith("Z")) continue;
4622
+ const siblings = childrenOf.get(parent);
4623
+ if (siblings) siblings.push(id);
4624
+ else childrenOf.set(parent, [id]);
4625
+ groupOf.set(id, group);
4626
+ }
4627
+ const found = /* @__PURE__ */ new Set();
4628
+ const queue = [root];
4629
+ const seen = new Set(queue);
4630
+ for (let pid = queue.shift(); pid !== void 0; pid = queue.shift()) for (const kid of childrenOf.get(pid) ?? []) {
4631
+ if (seen.has(kid)) continue;
4632
+ seen.add(kid);
4633
+ found.add(kid);
4634
+ queue.push(kid);
4635
+ }
4636
+ if (groupOf.get(process.pid) === process.pid) {
4637
+ for (const [pid, group] of groupOf) if (group === process.pid) found.add(pid);
4638
+ }
4639
+ found.delete(process.pid);
4640
+ found.delete(root);
4641
+ return [...found];
4642
+ }
4643
+ /** Whether `pid` still exists — signal 0 tests reachability, delivers nothing. */
4644
+ function isAlive(pid) {
4645
+ try {
4646
+ process.kill(pid, 0);
4647
+ return true;
4648
+ } catch {
4649
+ return false;
4650
+ }
4651
+ }
4652
+ /** How long a straggler gets to finish shutting down before SIGKILL. */
4653
+ const STRAGGLER_GRACE_MS = 2e3;
4654
+ /**
4655
+ * Once the command has exited, make sure nothing it started outlives us.
4656
+ *
4657
+ * Anything still alive here has ignored the signal we forwarded *and* lost the
4658
+ * parent that launched it, and we are the last process that knows its pid:
4659
+ * the tty won't hang it up (it is no longer in the foreground group, and the
4660
+ * shell has moved on), so it survives until the machine reboots. That is how
4661
+ * `tsx` and the server under it stacked up for days.
4662
+ *
4663
+ * So escalate rather than trust. SIGTERM first — it costs no extra time and is
4664
+ * a rung the SIGINT-only handlers common in dev servers don't catch — then
4665
+ * SIGKILL whatever is left after the grace period. Nothing is killed unless it
4666
+ * was asked to stop first and declined, and this runs only when we forwarded a
4667
+ * termination signal: a command that exits on its own having deliberately left a
4668
+ * daemon behind keeps working.
4669
+ */
4670
+ async function reapStragglers(pids, signal) {
4671
+ let live = [...pids].filter(isAlive);
4672
+ if (live.length === 0) return;
4673
+ for (const pid of live) try {
4674
+ process.kill(pid, "SIGTERM");
4675
+ } catch {}
4676
+ const deadline = Date.now() + STRAGGLER_GRACE_MS;
4677
+ while (live.length > 0 && Date.now() < deadline) {
4678
+ await new Promise((resolve) => setTimeout(resolve, 50));
4679
+ live = live.filter(isAlive);
4680
+ }
4681
+ if (live.length === 0) return;
4682
+ console.error(`seekrit: force-killed ${live.length} leftover process(es) that ignored ${signal} and SIGTERM (${live.join(", ")})`);
4683
+ for (const pid of live) try {
4684
+ process.kill(pid, "SIGKILL");
4685
+ } catch {}
4686
+ }
4577
4687
  program.command("run").description("run a command with decrypted secrets injected (process env > .env > app > group)").passThroughOptions().option("--org <slug>").option("--app <slug>", "application slug (token path infers this)").option("--env <slug>", "environment slug (token path infers this)").option("--with <group=env>", "override one group’s slice for this run", collectKv).option("--env-file <path>", "a .env file to overlay (repeatable; default .env)", collectList).option("--explain", "print where each variable resolved from (to stderr)").argument("<command...>", "command to run (prefix with -- to pass flags)").action(async (commandParts, options) => {
4578
4688
  const [cmd, ...args] = commandParts;
4579
4689
  if (!cmd) fail("no command given");
@@ -4588,8 +4698,7 @@ program.command("run").description("run a command with decrypted secrets injecte
4588
4698
  env: {
4589
4699
  ...values,
4590
4700
  ...process.env
4591
- },
4592
- detached: posix
4701
+ }
4593
4702
  });
4594
4703
  const signals = [
4595
4704
  "SIGINT",
@@ -4597,16 +4706,27 @@ program.command("run").description("run a command with decrypted secrets injecte
4597
4706
  "SIGHUP",
4598
4707
  "SIGQUIT"
4599
4708
  ];
4709
+ const stragglers = /* @__PURE__ */ new Set();
4710
+ let forwarded;
4711
+ const collect = () => {
4712
+ if (!posix || !child.pid) return;
4713
+ for (const pid of teardownTargets(child.pid)) stragglers.add(pid);
4714
+ };
4600
4715
  const forward = (signal) => {
4601
4716
  if (child.exitCode !== null || child.signalCode !== null) return;
4602
- if (posix && child.pid) try {
4603
- process.kill(-child.pid, signal);
4604
- return;
4605
- } catch {}
4717
+ forwarded ??= signal;
4718
+ collect();
4606
4719
  child.kill(signal);
4720
+ for (const pid of stragglers) try {
4721
+ process.kill(pid, signal);
4722
+ } catch {}
4607
4723
  };
4608
4724
  for (const signal of signals) process.on(signal, forward);
4609
- child.on("exit", (code, signal) => {
4725
+ child.on("exit", async (code, signal) => {
4726
+ if (forwarded) {
4727
+ collect();
4728
+ await reapStragglers(stragglers, forwarded);
4729
+ }
4610
4730
  for (const s of signals) process.off(s, forward);
4611
4731
  if (signal) process.kill(process.pid, signal);
4612
4732
  else process.exit(code ?? 1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seekrit/cli",
3
- "version": "0.23.2",
3
+ "version": "0.23.4",
4
4
  "description": "End-to-end encrypted secrets manager CLI — inject decrypted secrets into any command.",
5
5
  "type": "module",
6
6
  "publishConfig": {