@devrouter/cli 0.5.1 → 0.5.3

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.
@@ -936,6 +936,20 @@ function hasExactComposeIdentity(container, options) {
936
936
  const actualFiles = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").map((file) => file.trim()).filter(Boolean);
937
937
  const providerGeneratedFiles = actualFiles.filter((file) => {
938
938
  const normalized = file.replaceAll("\\", "/");
939
+ if (process.env.DEVPOD_HOME) {
940
+ const relative = import_node_path4.default.relative(import_node_path4.default.resolve(process.env.DEVPOD_HOME), file).replaceAll("\\", "/");
941
+ if (/^agent\/contexts\/[^/]+\/workspaces\/[^/]+\/\.docker-compose\/docker-compose\.devcontainer\.containerFeatures-[^/]+\.yml$/.test(
942
+ relative
943
+ ))
944
+ return true;
945
+ }
946
+ if (process.env.DEVSY_HOME) {
947
+ const relative = import_node_path4.default.relative(import_node_path4.default.resolve(process.env.DEVSY_HOME), file).replaceAll("\\", "/");
948
+ if (/^contexts\/[^/]+\/workspaces\/[^/]+\/agent\/\.docker-compose\/docker-compose\.devcontainer\.containerFeatures-[^/]+\.yml$/.test(
949
+ relative
950
+ ))
951
+ return true;
952
+ }
939
953
  return /\/(?:\.devpod\/agent\/contexts\/[^/]+\/workspaces\/[^/]+|\.devsy\/contexts\/[^/]+\/workspaces\/[^/]+\/agent)\/\.docker-compose\/docker-compose\.devcontainer\.containerFeatures-[^/]+\.yml$/.test(
940
954
  normalized
941
955
  );
@@ -2192,7 +2206,7 @@ function installedVersionOutput() {
2192
2206
  return [result.stdout, result.stderr].filter(Boolean).join("\n").trim() || void 0;
2193
2207
  }
2194
2208
  function nativeAssetName(platform, arch) {
2195
- if (platform !== "darwin") return void 0;
2209
+ if (platform !== "darwin" && platform !== "linux") return void 0;
2196
2210
  if (arch === "arm64") return "devsy-linux-arm64";
2197
2211
  if (arch === "x64") return "devsy-linux-amd64";
2198
2212
  return void 0;
@@ -5383,7 +5397,7 @@ function retain(session, previous) {
5383
5397
 
5384
5398
  // src/core/reliability-operation-store.ts
5385
5399
  function installedCliVersion() {
5386
- return true ? "0.5.1" : "0.0.0-dev";
5400
+ return true ? "0.5.3" : "0.0.0-dev";
5387
5401
  }
5388
5402
  function compareCliVersions(left, right) {
5389
5403
  const parts = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
@@ -7393,7 +7407,7 @@ function loadRepoConfig(repoPath, read2 = (file) => import_node_fs19.default.rea
7393
7407
  const config = parseConfig(parsed ?? {}, configPath);
7394
7408
  const requiredVersion = config.devrouter?.version;
7395
7409
  if (requiredVersion && !hasWarnedVersionMismatch) {
7396
- const cliVersion = true ? "0.5.1" : "0.0.0-dev";
7410
+ const cliVersion = true ? "0.5.3" : "0.0.0-dev";
7397
7411
  if (cliVersion !== "0.0.0-dev" && compareSemver2(requiredVersion, cliVersion) > 0) {
7398
7412
  hasWarnedVersionMismatch = true;
7399
7413
  process.stderr.write(
@@ -11325,10 +11339,33 @@ var DEFAULT_OUTPUT_PAGE_BYTES = 48 * 1024;
11325
11339
  function workerGroupAbsent(pid) {
11326
11340
  try {
11327
11341
  process.kill(-pid, 0);
11328
- return false;
11329
11342
  } catch (error) {
11330
11343
  return error.code === "ESRCH";
11331
11344
  }
11345
+ if (process.platform !== "linux") return false;
11346
+ let observedZombie = false;
11347
+ try {
11348
+ for (const entry of import_node_fs29.default.readdirSync("/proc")) {
11349
+ if (!/^\d+$/.test(entry)) continue;
11350
+ let stat;
11351
+ try {
11352
+ stat = import_node_fs29.default.readFileSync(`/proc/${entry}/stat`, "utf8");
11353
+ } catch (error) {
11354
+ if (error.code === "ENOENT") continue;
11355
+ return false;
11356
+ }
11357
+ const end = stat.lastIndexOf(")");
11358
+ if (end < 0) return false;
11359
+ const fields2 = stat.slice(end + 1).trim().split(/\s+/);
11360
+ if (!/^\d+$/.test(fields2[2] ?? "") || !/^[A-Za-z]$/.test(fields2[0] ?? "")) return false;
11361
+ if (Number(fields2[2]) !== pid) continue;
11362
+ if (fields2[0] !== "Z") return false;
11363
+ observedZombie = true;
11364
+ }
11365
+ } catch {
11366
+ return false;
11367
+ }
11368
+ return observedZombie;
11332
11369
  }
11333
11370
 
11334
11371
  // src/core/workspace-ensure.ts
package/dist/devrouter.js CHANGED
@@ -1067,6 +1067,20 @@ function hasExactComposeIdentity(container, options) {
1067
1067
  const actualFiles = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").map((file) => file.trim()).filter(Boolean);
1068
1068
  const providerGeneratedFiles = actualFiles.filter((file) => {
1069
1069
  const normalized = file.replaceAll("\\", "/");
1070
+ if (process.env.DEVPOD_HOME) {
1071
+ const relative2 = import_node_path4.default.relative(import_node_path4.default.resolve(process.env.DEVPOD_HOME), file).replaceAll("\\", "/");
1072
+ if (/^agent\/contexts\/[^/]+\/workspaces\/[^/]+\/\.docker-compose\/docker-compose\.devcontainer\.containerFeatures-[^/]+\.yml$/.test(
1073
+ relative2
1074
+ ))
1075
+ return true;
1076
+ }
1077
+ if (process.env.DEVSY_HOME) {
1078
+ const relative2 = import_node_path4.default.relative(import_node_path4.default.resolve(process.env.DEVSY_HOME), file).replaceAll("\\", "/");
1079
+ if (/^contexts\/[^/]+\/workspaces\/[^/]+\/agent\/\.docker-compose\/docker-compose\.devcontainer\.containerFeatures-[^/]+\.yml$/.test(
1080
+ relative2
1081
+ ))
1082
+ return true;
1083
+ }
1070
1084
  return /\/(?:\.devpod\/agent\/contexts\/[^/]+\/workspaces\/[^/]+|\.devsy\/contexts\/[^/]+\/workspaces\/[^/]+\/agent)\/\.docker-compose\/docker-compose\.devcontainer\.containerFeatures-[^/]+\.yml$/.test(
1071
1085
  normalized
1072
1086
  );
@@ -4774,7 +4788,7 @@ function loadRepoConfig(repoPath, read2 = (file) => import_node_fs8.default.read
4774
4788
  const config = parseConfig(parsed ?? {}, configPath);
4775
4789
  const requiredVersion = config.devrouter?.version;
4776
4790
  if (requiredVersion && !hasWarnedVersionMismatch) {
4777
- const cliVersion = true ? "0.5.1" : "0.0.0-dev";
4791
+ const cliVersion = true ? "0.5.3" : "0.0.0-dev";
4778
4792
  if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
4779
4793
  hasWarnedVersionMismatch = true;
4780
4794
  process.stderr.write(
@@ -6773,7 +6787,7 @@ var init_reliability_contract = __esm({
6773
6787
 
6774
6788
  // src/core/reliability-operation-store.ts
6775
6789
  function installedCliVersion() {
6776
- return true ? "0.5.1" : "0.0.0-dev";
6790
+ return true ? "0.5.3" : "0.0.0-dev";
6777
6791
  }
6778
6792
  function compareCliVersions(left, right) {
6779
6793
  const parts = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
@@ -10596,7 +10610,7 @@ function installedVersionOutput() {
10596
10610
  return [result.stdout, result.stderr].filter(Boolean).join("\n").trim() || void 0;
10597
10611
  }
10598
10612
  function nativeAssetName(platform, arch) {
10599
- if (platform !== "darwin") return void 0;
10613
+ if (platform !== "darwin" && platform !== "linux") return void 0;
10600
10614
  if (arch === "arm64") return "devsy-linux-arm64";
10601
10615
  if (arch === "x64") return "devsy-linux-amd64";
10602
10616
  return void 0;
@@ -15530,6 +15544,12 @@ var init_reliability_model = __esm({
15530
15544
  function isLifecycleProgressPhase(value) {
15531
15545
  return typeof value === "string" && Object.hasOwn(ROLES, value);
15532
15546
  }
15547
+ function muteLifecycleProgressHeartbeat() {
15548
+ heartbeatMuted = true;
15549
+ }
15550
+ function lifecycleProgressHeartbeatMuted() {
15551
+ return heartbeatMuted;
15552
+ }
15533
15553
  function lifecycleProgressSnapshot(phase, receivedAt, now) {
15534
15554
  const elapsedMs = receivedAt === void 0 ? null : Math.max(0, Math.floor(now - receivedAt));
15535
15555
  return {
@@ -15541,7 +15561,7 @@ function lifecycleProgressSnapshot(phase, receivedAt, now) {
15541
15561
  liveness: "unknown"
15542
15562
  };
15543
15563
  }
15544
- var ROLES;
15564
+ var ROLES, heartbeatMuted;
15545
15565
  var init_lifecycle_progress = __esm({
15546
15566
  "src/core/lifecycle-progress.ts"() {
15547
15567
  "use strict";
@@ -15559,6 +15579,7 @@ var init_lifecycle_progress = __esm({
15559
15579
  rollback: "lifecycle-worker",
15560
15580
  stop: "lifecycle-stop"
15561
15581
  };
15582
+ heartbeatMuted = false;
15562
15583
  }
15563
15584
  });
15564
15585
 
@@ -15579,10 +15600,33 @@ function hasDuplicateOperation(record5, requestId, operationId) {
15579
15600
  function workerGroupAbsent(pid) {
15580
15601
  try {
15581
15602
  process.kill(-pid, 0);
15582
- return false;
15583
15603
  } catch (error) {
15584
15604
  return error.code === "ESRCH";
15585
15605
  }
15606
+ if (process.platform !== "linux") return false;
15607
+ let observedZombie = false;
15608
+ try {
15609
+ for (const entry of import_node_fs31.default.readdirSync("/proc")) {
15610
+ if (!/^\d+$/.test(entry)) continue;
15611
+ let stat;
15612
+ try {
15613
+ stat = import_node_fs31.default.readFileSync(`/proc/${entry}/stat`, "utf8");
15614
+ } catch (error) {
15615
+ if (error.code === "ENOENT") continue;
15616
+ return false;
15617
+ }
15618
+ const end = stat.lastIndexOf(")");
15619
+ if (end < 0) return false;
15620
+ const fields2 = stat.slice(end + 1).trim().split(/\s+/);
15621
+ if (!/^\d+$/.test(fields2[2] ?? "") || !/^[A-Za-z]$/.test(fields2[0] ?? "")) return false;
15622
+ if (Number(fields2[2]) !== pid) continue;
15623
+ if (fields2[0] !== "Z") return false;
15624
+ observedZombie = true;
15625
+ }
15626
+ } catch {
15627
+ return false;
15628
+ }
15629
+ return observedZombie;
15586
15630
  }
15587
15631
  function signalOwnedGroup(child, signal) {
15588
15632
  if (!child.pid || child.exitCode !== null || child.signalCode !== null) return;
@@ -15620,7 +15664,7 @@ async function runLifecycleWorker(request2, supervision, beforeAdmission) {
15620
15664
  let progressReceivedAt;
15621
15665
  let progressTimer;
15622
15666
  const emitProgress = () => {
15623
- if (closed || cancelled || request2.kind === "exec") return;
15667
+ if (closed || cancelled || request2.kind === "exec" || lifecycleProgressHeartbeatMuted()) return;
15624
15668
  const data = `${JSON.stringify(lifecycleProgressSnapshot(progressPhase, progressReceivedAt, performance.now()))}
15625
15669
  `;
15626
15670
  try {
@@ -22349,7 +22393,7 @@ function buildOnboardingPrompt(options = {}) {
22349
22393
  "- Ensure reconciles interrupted or never-dispatched ensure after positive worker drainage. Unknown arbitrary exec never replays; use explicit stop to reconcile that uncertainty. Preserve journals and generated configuration if evidence is unavailable. This does not detect or prevent OOM; bounded automatic recovery runs only when the operator enables the capacity recovery policy.",
22350
22394
  "- Capacity admission is opt-in: only an operator-enabled capacity policy with an enrolled checkout routes ensure and exec through controller admission. The CLI follows the decision with bounded reconnecting waits that survive controller restarts, worker results are journalled atomically, and stop bypasses admission. Without an enabled policy lifecycle behavior is unchanged. An optional recovery block adds bounded automatic recovery: one journal-admitted corrective ensure per positively failed required capability, bounded by per-scope restart counts, an aggregate corrective-action cap and a rolling observation window. It is inert unless enabled.",
22351
22395
  "- A lock that cannot prove its owner fails closed with the failing inspection stage, the exact lock path, and a portable reproduction command; run the canonical command in a permitted host context. No identity fallback is attempted, and `devrouter controller` prints the underlying cause on stderr while keeping the stable `controller-unavailable` JSON on stdout.",
22352
- "- Devsy agent readiness: run `devrouter setup --yes --workspace-runtime devsy` once per installed Devsy release, rerunning it after a Devsy update, so Devrouter acquires and verifies the official Linux agent in its own machine cache. Devrouter supports releases in the range >=1.16.2 <2.0.0, verifies a release's published asset against the SHA-256 digest GitHub reports, and never injects a release without one. `doctor` reports `ready`, `missing`, `stale`, or `invalid` without network access; a supported release with no recorded manifest is `ready` with a drift warning and no injected agent until `setup` records one, while a version outside the supported range or unparseable stays `stale`. `ensure` fails before the provider queue when readiness is not `ready`. An explicit `DEVSY_AGENT_BINARY` remains authoritative and must match the official asset for the installed supported release. `ensure` and `stop` stream bounded phase progress on stderr, so parse command JSON from stdout only.",
22396
+ "- Devsy agent readiness: run `devrouter setup --yes --workspace-runtime devsy` once per installed Devsy release, rerunning it after a Devsy update, so Devrouter acquires and verifies the official Linux agent in its own machine cache. Devrouter selects agents on macOS and Linux arm64/x64 hosts. Use Devsy 1.16.2 for warm profile changes; 1.23.2 refuses changed runServices without recreation, which devrouter never applies automatically. Devrouter supports agent releases in the range >=1.16.2 <2.0.0, verifies a release's published asset against the SHA-256 digest GitHub reports, and never injects a release without one. `doctor` reports `ready`, `missing`, `stale`, or `invalid` without network access; a supported release with no recorded manifest is `ready` with a drift warning and no injected agent until `setup` records one, while a version outside the supported range or unparseable stays `stale`. `ensure` fails before the provider queue when readiness is not `ready`. An explicit `DEVSY_AGENT_BINARY` remains authoritative and must match the official asset for the installed supported release. `ensure` and `stop` stream bounded phase progress on stderr, so parse command JSON from stdout only.",
22353
22397
  "- A managed adapter plus `postCreateCommand` requires `waitFor` exactly `postCreateCommand` or `postStartCommand`; managed selective config preserves lifecycle fields and changes only `runServices`.",
22354
22398
  "- Managed process reuse fingerprints command argv, workspace identity, and the exact adapter snapshot. If a non-secret runtime value also affects reuse, set `DEVROUTER_PROCESS_FINGERPRINT_ENV` to its comma-separated environment names; secret-like names are rejected and raw values are never persisted.",
22355
22399
  "- Managed adapters may use devrouter-process ensure --prepare-command <command> for synchronous dependency preparation under the process lock before app launch. Unchanged owned processes skip preparation; it participates in default fingerprints and must not daemonize or detach.",
@@ -24479,7 +24523,7 @@ function probeInstalledCliVersion(executable) {
24479
24523
  }
24480
24524
  function buildCliPathCheck(options = {}) {
24481
24525
  const id5 = "global.cli-path";
24482
- const runningVersion = options.runningVersion ?? (true ? "0.5.1" : "0.0.0-dev");
24526
+ const runningVersion = options.runningVersion ?? (true ? "0.5.3" : "0.0.0-dev");
24483
24527
  if (runningVersion === "0.0.0-dev") {
24484
24528
  return {
24485
24529
  id: id5,
@@ -25375,7 +25419,7 @@ async function buildDoctorReport(options = {}) {
25375
25419
  suggestion: `Remedy ${expansionNotice.remedy.code}: set a named default profile other than '${expansionNotice.profile}'${remedyCandidates}.`
25376
25420
  });
25377
25421
  }
25378
- const cliVersion = true ? "0.5.1" : "0.0.0-dev";
25422
+ const cliVersion = true ? "0.5.3" : "0.0.0-dev";
25379
25423
  const configVersion = config.devrouter?.version;
25380
25424
  const installedKnown = cliVersion !== "0.0.0-dev";
25381
25425
  const versionComparison = installedKnown && configVersion ? compareSemver(configVersion, cliVersion) : void 0;
@@ -33775,6 +33819,7 @@ async function runWorkspaceIdleCommand(options) {
33775
33819
  let stops = [];
33776
33820
  if (options.stop) {
33777
33821
  const paths = idleSweepPaths();
33822
+ if (!process.stderr.isTTY) muteLifecycleProgressHeartbeat();
33778
33823
  import_node_fs61.default.mkdirSync(paths.directory, { recursive: true, mode: 448 });
33779
33824
  const result = await withFileLock(paths.lock, { activity: "idle sweep" }, async () => {
33780
33825
  try {
@@ -33983,6 +34028,7 @@ var init_workspace2 = __esm({
33983
34028
  init_docker_leftovers();
33984
34029
  init_file_lock();
33985
34030
  init_idle_sweep();
34031
+ init_lifecycle_progress();
33986
34032
  init_output();
33987
34033
  init_reliability_lifecycle();
33988
34034
  init_repo_config();
@@ -34036,7 +34082,7 @@ var init_version = __esm({
34036
34082
 
34037
34083
  // src/cli.ts
34038
34084
  var import_commander = require("commander");
34039
- var CLI_VERSION = true ? "0.5.1" : "0.0.0-dev";
34085
+ var CLI_VERSION = true ? "0.5.3" : "0.0.0-dev";
34040
34086
  var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
34041
34087
  function withErrorHandling(action2) {
34042
34088
  return async (...args) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devrouter/cli",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "Local dev routing CLI with shared Traefik reverse proxy",
5
5
  "author": "Roland Schlaefli",
6
6
  "homepage": "https://github.com/rschlaefli/devrouter#readme",
@@ -0,0 +1,25 @@
1
+ # Upgrade to devrouter 0.5.2
2
+
3
+ This patch release keeps the idle-stop agent's `sweep.log` readable.
4
+ `workspace idle --stop` no longer writes a lifecycle progress line every ten
5
+ seconds of each stop when its stderr is not a terminal, as under launchd, so
6
+ the log records only stop outcomes and errors. An interactive sweep still shows
7
+ progress. There is no configuration change and no migration.
8
+
9
+ ## Task
10
+
11
+ Install the published 0.5.2 CLI. No repository change is needed. Bump a
12
+ repository's `devrouter.version` to 0.5.2 only when consumer source changes are
13
+ authorized. A machine that uses the idle-stop agent needs no setup rerun when
14
+ devrouter is installed through Volta, because the agent's entry path does not
15
+ change between versions.
16
+
17
+ ## Validation
18
+
19
+ 1. Run `devrouter -V` from the consumer checkout.
20
+ 2. Run `devrouter workspace idle --json` and confirm it completes.
21
+
22
+ ## Report
23
+
24
+ Report the release revision and the resolved executable with its installed
25
+ version.
@@ -0,0 +1,36 @@
1
+ # Upgrade to devrouter 0.5.3
2
+
3
+ This patch release supports verified Devsy agents on Linux arm64/x64,
4
+ custom DevPod and Devsy runtime homes, and drainage of Linux process groups
5
+ proven to contain only zombies. There is no configuration schema migration.
6
+
7
+ ## Task
8
+
9
+ Install the published 0.5.3 CLI. Bump a consumer's `devrouter.version` only when
10
+ consumer source changes are authorized. Preserve exact checkout ownership,
11
+ containers, databases, dependency volumes, and existing runtime directories.
12
+ Changing the machine preference does not migrate a DevPod-owned checkout.
13
+
14
+ For Devsy, run `devrouter setup --yes --workspace-runtime devsy` to verify the
15
+ installed release's official Linux agent. Pin Devsy 1.16.2 when warm profile
16
+ changes are needed: 1.23.2 rejects a changed `runServices` list as structural.
17
+ Do not follow its recreation suggestion on a retained environment. Keep any
18
+ explicit `DEVSY_AGENT_BINARY` consistent with the installed Devsy release.
19
+
20
+ ## Validation
21
+
22
+ 1. Run `devrouter -V` from the consumer checkout and confirm 0.5.3.
23
+ 2. Run `devrouter doctor --json` and inspect runtime ownership and agent readiness.
24
+ 3. Run `devrouter ensure .` with the consumer's intended profile, then an existing
25
+ smoke or browser test. Do not replay destructive bootstrap hooks for a
26
+ retained database just to test an upgrade.
27
+ 4. If testing profile changes or stop/resume, verify the exact container and
28
+ volumes are retained. Never bypass lifecycle fences or edit ownership records.
29
+
30
+ A zombie lifecycle worker retaining its recorded birth identity remains a
31
+ known recovery limitation on hosts whose PID 1 does not reap children.
32
+
33
+ ## Report
34
+
35
+ Report the published version and resolved executable, selected runtime and
36
+ version, tested profile and functional outcomes, and any remaining limitations.