@devrouter/cli 0.0.74 → 0.0.75

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.
package/dist/devrouter.js CHANGED
@@ -4256,7 +4256,7 @@ function loadRepoConfig(repoPath, read2 = (file) => import_node_fs7.default.read
4256
4256
  const config = parseConfig(parsed ?? {}, configPath);
4257
4257
  const requiredVersion = config.devrouter?.version;
4258
4258
  if (requiredVersion && !hasWarnedVersionMismatch) {
4259
- const cliVersion = true ? "0.0.74" : "0.0.0-dev";
4259
+ const cliVersion = true ? "0.0.75" : "0.0.0-dev";
4260
4260
  if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
4261
4261
  hasWarnedVersionMismatch = true;
4262
4262
  process.stderr.write(
@@ -5509,7 +5509,7 @@ var init_reliability_contract = __esm({
5509
5509
 
5510
5510
  // src/core/reliability-operation-store.ts
5511
5511
  function installedCliVersion() {
5512
- return true ? "0.0.74" : "0.0.0-dev";
5512
+ return true ? "0.0.75" : "0.0.0-dev";
5513
5513
  }
5514
5514
  function compareCliVersions(left, right) {
5515
5515
  const parts = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
@@ -8413,6 +8413,12 @@ function isWildcard(values) {
8413
8413
  function sha256(contents) {
8414
8414
  return (0, import_node_crypto11.createHash)("sha256").update(contents, "utf-8").digest("hex");
8415
8415
  }
8416
+ function safeComposeProject(value) {
8417
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(value)) {
8418
+ throw new Error(`Docker Compose project '${value}' is not a safe exact identifier.`);
8419
+ }
8420
+ return value;
8421
+ }
8416
8422
  function inspectManagedDevcontainerConfig(options) {
8417
8423
  const managedRuntime = options.config.managedRuntime;
8418
8424
  if (!managedRuntime) {
@@ -8531,6 +8537,91 @@ function managedComposeEnvironment(workspace) {
8531
8537
  }
8532
8538
  return env;
8533
8539
  }
8540
+ function assertReadableComposeFile(file) {
8541
+ try {
8542
+ if (!import_node_fs18.default.statSync(file).isFile()) throw new Error("not a regular file");
8543
+ import_node_fs18.default.accessSync(file, import_node_fs18.default.constants.R_OK);
8544
+ } catch {
8545
+ throw new Error("Managed container Compose configuration contains an unreadable file.");
8546
+ }
8547
+ }
8548
+ function assertManagedContainerConfigUnchanged(options) {
8549
+ if (options.containers.length === 0) {
8550
+ throw new Error("Managed container configuration cannot be verified without containers.");
8551
+ }
8552
+ for (const container of options.containers) {
8553
+ const project = container.labels["com.docker.compose.project"];
8554
+ const service = container.labels["com.docker.compose.service"];
8555
+ const workingDirectory = container.labels["com.docker.compose.project.working_dir"];
8556
+ const recordedConfigFiles = container.labels["com.docker.compose.project.config_files"];
8557
+ const expectedHash = container.labels["com.docker.compose.config-hash"];
8558
+ if (!project || !service || !workingDirectory || !recordedConfigFiles || !expectedHash || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(service) || !/^[a-f0-9]{64}$/.test(expectedHash)) {
8559
+ throw new Error("Managed container is missing valid Compose identity and hash labels.");
8560
+ }
8561
+ const composeProject = safeComposeProject(project);
8562
+ const configFiles2 = recordedConfigFiles.split(",").map((file) => file.trim());
8563
+ if (configFiles2.some((file) => file.length === 0)) {
8564
+ throw new Error("Managed container has an invalid recorded Compose file list.");
8565
+ }
8566
+ if (options.plan.composeFiles.some(
8567
+ (expected) => !configFiles2.some((actual) => sameWorkspacePath(actual, expected))
8568
+ )) {
8569
+ throw new Error("Managed container Compose files do not include the managed plan files.");
8570
+ }
8571
+ for (const file of configFiles2) assertReadableComposeFile(file);
8572
+ const fileArgs = configFiles2.flatMap((file) => ["-f", file]);
8573
+ const result = (() => {
8574
+ try {
8575
+ const composeArgs = [
8576
+ "compose",
8577
+ "--project-name",
8578
+ composeProject,
8579
+ "--project-directory",
8580
+ workingDirectory
8581
+ ];
8582
+ const env = managedComposeEnvironment(options.workspace);
8583
+ const rendered = (0, import_node_child_process8.spawnSync)(
8584
+ "docker",
8585
+ [...composeArgs, ...fileArgs, "config", "--format", "json"],
8586
+ {
8587
+ cwd: workingDirectory,
8588
+ encoding: "utf-8",
8589
+ env,
8590
+ stdio: ["ignore", "pipe", "ignore"],
8591
+ timeout: COMPOSE_HASH_TIMEOUT_MS,
8592
+ maxBuffer: 1024 * 1024
8593
+ }
8594
+ );
8595
+ if (rendered.status !== 0 || rendered.error || !rendered.stdout?.trim()) {
8596
+ throw new Error("Compose configuration rendering failed.");
8597
+ }
8598
+ return (0, import_node_child_process8.spawnSync)(
8599
+ "docker",
8600
+ [...composeArgs, "-f", "-", "config", "--no-interpolate", "--hash", service],
8601
+ {
8602
+ cwd: workingDirectory,
8603
+ encoding: "utf-8",
8604
+ env,
8605
+ input: rendered.stdout,
8606
+ stdio: ["pipe", "pipe", "ignore"],
8607
+ timeout: COMPOSE_HASH_TIMEOUT_MS,
8608
+ maxBuffer: 1024 * 1024
8609
+ }
8610
+ );
8611
+ } catch {
8612
+ throw new Error(`Could not verify managed Compose configuration for service '${service}'.`);
8613
+ }
8614
+ })();
8615
+ if (result.status !== 0 || result.error) {
8616
+ throw new Error(`Could not verify managed Compose configuration for service '${service}'.`);
8617
+ }
8618
+ const output2 = typeof result.stdout === "string" ? result.stdout.trim() : "";
8619
+ const match = /^(\S+)\s+([a-f0-9]{64})$/.exec(output2);
8620
+ if (!match || match[1] !== service || match[2] !== expectedHash) {
8621
+ throw new Error(`Managed Compose configuration changed for service '${service}'.`);
8622
+ }
8623
+ }
8624
+ }
8534
8625
  function stopExactManagedService(containerId, service, options) {
8535
8626
  assertSafeContainerId(containerId);
8536
8627
  const result = (0, import_node_child_process8.spawnSync)("docker", ["stop", containerId], {
@@ -8543,7 +8634,7 @@ function stopExactManagedService(containerId, service, options) {
8543
8634
  throw new Error(`Could not stop exact managed service '${service}' (${containerId}).`);
8544
8635
  }
8545
8636
  }
8546
- var import_node_child_process8, import_node_crypto11, import_node_fs18, import_node_path25, import_yaml3, MANAGED_DEVCONTAINER_PATH, MANAGED_DEVCONTAINER_MARKER;
8637
+ var import_node_child_process8, import_node_crypto11, import_node_fs18, import_node_path25, import_yaml3, MANAGED_DEVCONTAINER_PATH, MANAGED_DEVCONTAINER_MARKER, COMPOSE_HASH_TIMEOUT_MS;
8547
8638
  var init_devcontainer_profile = __esm({
8548
8639
  "src/core/devcontainer-profile.ts"() {
8549
8640
  "use strict";
@@ -8559,6 +8650,7 @@ var init_devcontainer_profile = __esm({
8559
8650
  init_workspace();
8560
8651
  MANAGED_DEVCONTAINER_PATH = ".devcontainer/devcontainer.devrouter.json";
8561
8652
  MANAGED_DEVCONTAINER_MARKER = "// devrouter:managed devcontainer profile";
8653
+ COMPOSE_HASH_TIMEOUT_MS = 1e4;
8562
8654
  }
8563
8655
  });
8564
8656
 
@@ -10529,8 +10621,27 @@ function proveInitialManagedDevsyAbsence(repoPath, expectedId) {
10529
10621
  }
10530
10622
  function stopInitialManagedDevsyWorkspace(repoPath, devsyId) {
10531
10623
  if (!import_node_fs25.default.existsSync(import_node_path33.default.join(repoPath, ".devrouter.yml"))) return false;
10624
+ if (!loadRepoConfig(repoPath).managedRuntime) return false;
10532
10625
  const linked = isLinkedWorktree(repoPath);
10533
10626
  const workspace = linked ? resolveWorktreeWorkspace(repoPath) : void 0;
10627
+ const identity2 = { repoPath, workspace: workspace ?? null, provider: "devsy" };
10628
+ const readAuthority = () => {
10629
+ const record5 = readReliabilityOperation(identity2);
10630
+ const operation = record5?.state.operation;
10631
+ const entries = record5?.state.operationHistory.filter((entry2) => entry2.id === operation?.id);
10632
+ const entry = entries?.[0];
10633
+ if (!record5 || !(0, import_node_util9.isDeepStrictEqual)(record5.identity, identity2) || record5.worker || !operation || operation.kind !== "ensure" || !operation.drained || !["COMPLETED", "INTERRUPTED", "COMPLETION_UNKNOWN"].includes(operation.status) || record5.state.phase !== "stopping" || record5.state.desired !== "stopped-by-user" || entries?.length !== 1 || !entry || entry.kind !== "ensure" || !entry.profile || !entry.drained || entry.status !== operation.status)
10634
+ throw new Error("Initial managed stop requires the drained ensure's recorded profile.");
10635
+ return {
10636
+ identity: record5.identity,
10637
+ fence: reliabilityFence(record5.state),
10638
+ operation,
10639
+ entry
10640
+ };
10641
+ };
10642
+ const authority = readAuthority();
10643
+ const gitCommonDir = linked ? resolveGitCommonDir(repoPath) : void 0;
10644
+ const workspaceEnv = workspace && gitCommonDir ? { token: workspace, gitCommonDir } : void 0;
10534
10645
  const registration2 = () => {
10535
10646
  resetWorkspaceRuntimeCaches();
10536
10647
  if (resolveWorkspaceRuntimeOrDefault(repoPath) !== "devsy")
@@ -10540,14 +10651,20 @@ function stopInitialManagedDevsyWorkspace(repoPath, devsyId) {
10540
10651
  throw new Error("Initial managed stop requires one exact Devsy registration.");
10541
10652
  if (linked) {
10542
10653
  const record5 = workspace ? readWorkspaceOwnership(repoPath, workspace) : void 0;
10543
- if (!record5 || record5.devpodId !== devsyId || !sameWorkspacePath(record5.worktreePath, repoPath) || resolveWorktreeWorkspace(repoPath) !== workspace)
10654
+ if (!record5 || record5.devpodId !== devsyId || !sameWorkspacePath(record5.worktreePath, repoPath) || resolveWorktreeWorkspace(repoPath) !== workspace || resolveGitCommonDir(repoPath) !== gitCommonDir || inspectWorkspaceOwnership(record5, listGitWorktrees(repoPath), void 0).ownerStatus !== "present")
10544
10655
  throw new Error("Initial managed stop workspace ownership changed.");
10545
10656
  }
10546
10657
  return owner2.workspace;
10547
10658
  };
10548
- const runtime = loadRuntimeConfig(repoPath, workspace ?? "");
10549
- if (!runtime.config.managedRuntime) return false;
10659
+ const runtime = loadRuntimeConfig(repoPath, workspace ?? "", authority.entry.profile);
10660
+ if (!runtime.config.managedRuntime || runtime.profile !== authority.entry.profile)
10661
+ throw new Error("Initial managed stop requires the recorded managed profile.");
10550
10662
  const owner = registration2();
10663
+ proveLocalDockerSelection(owner);
10664
+ const endpoint = resolveManagedStopEndpoint();
10665
+ if (!supportsManagedStopBaseline(endpoint))
10666
+ throw new Error("Initial managed stop requires a pinned local Docker endpoint.");
10667
+ const daemon = inspectManagedStopDaemon(endpoint);
10551
10668
  if (!owner.context || !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(owner.context) || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(devsyId))
10552
10669
  throw new Error("Initial managed stop requires exact provider context.");
10553
10670
  const plan = inspectManagedDevcontainerConfig({
@@ -10567,27 +10684,51 @@ function stopInitialManagedDevsyWorkspace(repoPath, devsyId) {
10567
10684
  ".docker-compose"
10568
10685
  );
10569
10686
  const observe = () => {
10570
- if (!(0, import_node_util9.isDeepStrictEqual)(registration2(), owner) || readManagedRuntimeState(repoPath, workspace))
10571
- throw new Error("Initial managed stop ownership or runtime state changed.");
10687
+ if (!(0, import_node_util9.isDeepStrictEqual)(readAuthority(), authority) || !(0, import_node_util9.isDeepStrictEqual)(registration2(), owner) || readManagedRuntimeState(repoPath, workspace) || resolveManagedStopEndpoint() !== endpoint || inspectManagedStopDaemon(endpoint) !== daemon)
10688
+ throw new Error("Initial managed stop authority or runtime state changed.");
10689
+ proveLocalDockerSelection(owner);
10690
+ if (listDevpodWorkspacesRaw({ readLocalWhenMissing: true }).some(
10691
+ (entry) => entry.id === devsyId || entry.source.localFolder !== "" && sameWorkspacePath(entry.source.localFolder, repoPath)
10692
+ ))
10693
+ throw new Error("Initial managed stop found conflicting provider ownership.");
10694
+ const currentRuntime = loadRuntimeConfig(repoPath, workspace ?? "", authority.entry.profile);
10695
+ const currentPlan = inspectManagedDevcontainerConfig({
10696
+ repoPath,
10697
+ config: currentRuntime.config,
10698
+ profile: currentRuntime.resolvedProfile,
10699
+ linked
10700
+ });
10701
+ if (currentRuntime.profile !== authority.entry.profile || !(0, import_node_util9.isDeepStrictEqual)(currentRuntime.resolvedProfile, runtime.resolvedProfile) || !(0, import_node_util9.isDeepStrictEqual)(currentPlan, plan) || inspectManagedDevcontainerGeneratedConfig(currentPlan).status !== "valid")
10702
+ throw new Error("Initial managed stop configuration changed.");
10572
10703
  const matches = inspectWorkspaceContainers().filter(
10573
10704
  (c) => sameWorkspacePath(
10574
10705
  c.labels["com.docker.compose.project.working_dir"] ?? "",
10575
10706
  plan.composeDirectory
10576
10707
  )
10577
10708
  );
10578
- if (!matches.length) return [];
10709
+ if (!matches.length)
10710
+ throw new Error("Initial managed stop requires its complete selected population.");
10579
10711
  const projects = new Set(matches.map((c) => c.labels["com.docker.compose.project"]));
10580
10712
  if (projects.size !== 1 || !matches[0]?.labels["com.docker.compose.project"])
10581
10713
  throw new Error("Initial managed stop requires one exact Compose project.");
10582
10714
  const population = inspectManagedStopContainers(
10583
- matches[0].labels["com.docker.compose.project"]
10715
+ matches[0].labels["com.docker.compose.project"],
10716
+ endpoint
10584
10717
  );
10585
10718
  if (!sameSet(
10719
+ inspectManagedStopWorkspaceIds(endpoint, plan.composeDirectory),
10720
+ population.map((c) => c.id)
10721
+ ) || !sameSet(
10586
10722
  matches.map((c) => c.id),
10587
10723
  population.map((c) => c.id)
10588
10724
  ))
10589
10725
  throw new Error("Initial managed stop population changed.");
10590
- proveManagedComposePopulation({
10726
+ if (!sameSet(
10727
+ population.map((c) => c.labels["com.docker.compose.service"] ?? ""),
10728
+ plan.desiredServices
10729
+ ))
10730
+ throw new Error("Initial managed stop requires its complete selected population.");
10731
+ const primary = proveManagedComposePopulation({
10591
10732
  plan,
10592
10733
  repoPath,
10593
10734
  composeProject: matches[0].labels["com.docker.compose.project"],
@@ -10595,9 +10736,23 @@ function stopInitialManagedDevsyWorkspace(repoPath, devsyId) {
10595
10736
  featureDirectory,
10596
10737
  containers: population
10597
10738
  });
10739
+ const uidBytes = Buffer.byteLength(owner.uid ?? "");
10740
+ const runnerId = (uidBytes === 16 || uidBytes === 40 ? owner.uid : void 0) ?? devsyId;
10741
+ if (owner.source.container && owner.source.container !== primary.id || !owner.source.container && inspectManagedStopRunnerId(endpoint, primary.id) !== runnerId)
10742
+ throw new Error("Initial managed stop provider-to-container binding changed.");
10743
+ if (!owner.source.container && !sameSet(inspectProviderRunnerContainers(endpoint, runnerId), [primary.id]))
10744
+ throw new Error("Initial managed stop provider runner population changed.");
10745
+ assertManagedContainerConfigUnchanged({
10746
+ plan,
10747
+ containers: population,
10748
+ workspace: workspaceEnv
10749
+ });
10750
+ if (!(0, import_node_util9.isDeepStrictEqual)(readAuthority(), authority) || !(0, import_node_util9.isDeepStrictEqual)(registration2(), owner) || resolveManagedStopEndpoint() !== endpoint || inspectManagedStopDaemon(endpoint) !== daemon)
10751
+ throw new Error("Initial managed stop authority changed during inspection.");
10598
10752
  return population;
10599
10753
  };
10600
10754
  const initial = observe();
10755
+ const stopped = new Set(initial.filter((c) => !c.state.Running).map((c) => c.id));
10601
10756
  const stable = () => {
10602
10757
  const current = observe();
10603
10758
  if (!sameSet(
@@ -10605,19 +10760,21 @@ function stopInitialManagedDevsyWorkspace(repoPath, devsyId) {
10605
10760
  current.map((c) => c.id)
10606
10761
  ) || current.some((c) => {
10607
10762
  const prior = initial.find((p) => p.id === c.id);
10608
- return !prior || containerIdentity(prior) !== containerIdentity(c) || !prior.state.Running && c.state.Running;
10763
+ return !prior || containerIdentity(prior) !== containerIdentity(c) || stopped.has(c.id) && c.state.Running;
10609
10764
  }))
10610
10765
  throw new Error("Initial managed stop population or identity changed.");
10766
+ for (const c of current) if (!c.state.Running) stopped.add(c.id);
10611
10767
  return current;
10612
10768
  };
10613
10769
  stable();
10614
- if (!initial.length) return false;
10615
10770
  for (const container of initial) {
10616
10771
  const current = stable().find((c) => c.id === container.id);
10617
- if (current?.state.Running)
10618
- stopExactManagedService(current.id, current.labels["com.docker.compose.service"] ?? "", {
10619
- timeoutMs: 3e4
10620
- });
10772
+ if (current?.state.Running) {
10773
+ claimLifecycleEffect();
10774
+ stopPinnedManagedContainer(endpoint, current.id);
10775
+ if (stable().find((c) => c.id === current.id)?.state.Running)
10776
+ throw new Error("Initial managed stop container cessation is not proven.");
10777
+ }
10621
10778
  }
10622
10779
  if (stable().some((c) => c.state.Running))
10623
10780
  throw new Error("Initial managed stop left a workload running.");
@@ -10837,11 +10994,15 @@ var init_managed_devsy_stop = __esm({
10837
10994
  init_devcontainer_profile();
10838
10995
  init_devpod_environment();
10839
10996
  init_devpod_registry();
10997
+ init_devsy_exec_proof();
10840
10998
  init_devsy_workspaces();
10841
10999
  init_host_routes();
10842
11000
  init_managed_compose_population();
10843
11001
  init_managed_runtime_state();
10844
11002
  init_managed_stop_recovery();
11003
+ init_reliability_context();
11004
+ init_reliability_contract();
11005
+ init_reliability_operation_store();
10845
11006
  init_repo_config();
10846
11007
  init_route_publication();
10847
11008
  init_traefik_route_health();
@@ -20955,7 +21116,7 @@ async function buildDoctorReport(options = {}) {
20955
21116
  const config = runtimeConfig.config;
20956
21117
  loadedConfig = config;
20957
21118
  loadedWorkspace = runtimeConfig.workspace;
20958
- const cliVersion = true ? "0.0.74" : "0.0.0-dev";
21119
+ const cliVersion = true ? "0.0.75" : "0.0.0-dev";
20959
21120
  const configVersion = config.devrouter?.version;
20960
21121
  if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
20961
21122
  addCheck(checks, {
@@ -26396,7 +26557,7 @@ var init_version = __esm({
26396
26557
 
26397
26558
  // src/cli.ts
26398
26559
  var import_commander = require("commander");
26399
- var CLI_VERSION = true ? "0.0.74" : "0.0.0-dev";
26560
+ var CLI_VERSION = true ? "0.0.75" : "0.0.0-dev";
26400
26561
  var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
26401
26562
  function withErrorHandling(action2) {
26402
26563
  return async (...args) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devrouter/cli",
3
- "version": "0.0.74",
3
+ "version": "0.0.75",
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,35 @@
1
+ # Upgrade to devrouter 0.0.75
2
+
3
+ This patch fixes canonical stop after initial managed Devsy startup is cancelled
4
+ before managed runtime state is saved. Stop uses the exact drained ensure's
5
+ recorded profile and validates complete live ownership and Compose configuration
6
+ hashes. No configuration schema, command, flag or runtime-provider migration is
7
+ required.
8
+
9
+ ## Task
10
+
11
+ Use the published 0.0.75 CLI. Update `.devrouter.yml` devrouter.version to 0.0.75
12
+ only when consumer source changes are authorized. Preserve staged work owned by
13
+ another task. Refresh generated agent guidance only with the repository owner's
14
+ authority. No DevPod installation is needed.
15
+
16
+ For an affected exact checkout, use `devrouter stop <path> --json` with the same
17
+ non-secret Compose inputs used at startup. Existing interrupted workspaces can
18
+ recover when the exact operation history and all live configuration and ownership
19
+ proofs remain available. Missing, unreadable, conflicting or changed evidence
20
+ remains a refusal; never fabricate journal state or replace the selected profile
21
+ with the default to bypass it. Stop is non-destructive and retains volumes.
22
+
23
+ ## Validation
24
+
25
+ Verify the released CLI revision and exact checkout ownership. Let the checkout's
26
+ runtime owner perform the canonical stop, require provider cessation and route
27
+ removal proof, then verify stopped lifecycle state. Revalidate port conflicts
28
+ before a new ensure and require its own provider, application and route proof.
29
+ A successful stop or source test does not prove successful application startup.
30
+
31
+ ## Report
32
+
33
+ Report release version/revision, exact checkout, producing stop result and
34
+ lifecycle state, subsequent ensure evidence or precise refusal, and source/index
35
+ preservation. Separate released source from live consumer acceptance.