@hexclave/cli 1.0.70 → 1.0.71

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hexclave/cli",
3
- "version": "1.0.70",
3
+ "version": "1.0.71",
4
4
  "repository": "https://github.com/hexclave/hexclave",
5
5
  "description": "The CLI for Hexclave. https://hexclave.com",
6
6
  "main": "dist/index.js",
@@ -23,14 +23,16 @@
23
23
  "@inquirer/prompts": "^7.0.0",
24
24
  "@sentry/node": "^10.42.0",
25
25
  "commander": "^13.1.0",
26
+ "cross-spawn": "^7.0.6",
26
27
  "extract-zip": "^2.0.1",
27
28
  "jiti": "^2.4.2",
28
29
  "tar": "^7.4.3",
29
- "@hexclave/shared-backend": "1.0.70",
30
- "@hexclave/js": "1.0.70",
31
- "@hexclave/shared": "1.0.70"
30
+ "@hexclave/shared-backend": "1.0.71",
31
+ "@hexclave/js": "1.0.71",
32
+ "@hexclave/shared": "1.0.71"
32
33
  },
33
34
  "devDependencies": {
35
+ "@types/cross-spawn": "^6.0.6",
34
36
  "@types/node": "22.15.21",
35
37
  "rimraf": "^6.0.1",
36
38
  "tsdown": "^0.22.3",
@@ -1,9 +1,9 @@
1
- import { mkdtempSync, rmSync } from "fs";
1
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
2
2
  import { tmpdir } from "os";
3
3
  import { join } from "path";
4
4
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
5
  import { recordLocalDashboardProcess } from "../lib/dev-env-state.js";
6
- import { configErrorLogPrefix, devDashboardCommandFromEnv, isHeartbeatResponse, isVersionNewer, killLocalDashboard, logConfigSyncEvents, processExists, shouldRestartDashboard } from "./dev.js";
6
+ import { configErrorLogPrefix, dashboardEnvWithStatePath, devDashboardCommandFromEnv, isHeartbeatResponse, isVersionNewer, killLocalDashboard, logConfigSyncEvents, processExists, runChildProcess, shouldRestartDashboard } from "./dev.js";
7
7
 
8
8
  describe("isVersionNewer", () => {
9
9
  it("compares core versions numerically", () => {
@@ -88,6 +88,91 @@ describe("devDashboardCommandFromEnv", () => {
88
88
  });
89
89
  });
90
90
 
91
+ describe("dashboardEnvWithStatePath", () => {
92
+ it("passes the resolved state path to the dashboard child", () => {
93
+ expect(dashboardEnvWithStatePath({ NODE_ENV: "production" }, "C:\\Users\\Test\\AppData\\Local\\Hexclave\\dev-envs.json")).toEqual({
94
+ NODE_ENV: "production",
95
+ STACK_DEV_ENVS_PATH: "C:\\Users\\Test\\AppData\\Local\\Hexclave\\dev-envs.json",
96
+ });
97
+ });
98
+
99
+ it("preserves an explicit state path override", () => {
100
+ expect(dashboardEnvWithStatePath({
101
+ STACK_DEV_ENVS_PATH: "C:\\custom\\dev-envs.json",
102
+ }, "C:\\Users\\Test\\AppData\\Local\\Hexclave\\dev-envs.json").STACK_DEV_ENVS_PATH).toBe("C:\\custom\\dev-envs.json");
103
+ });
104
+ });
105
+
106
+ describe("runChildProcess", () => {
107
+ const args = [
108
+ "plain",
109
+ "space containing argument",
110
+ "a&b",
111
+ "%PATH%",
112
+ "!PATH!",
113
+ "^caret",
114
+ 'a"quoted"b',
115
+ '{"json":"value"}',
116
+ "trailing\\",
117
+ 'back\\slash"quote',
118
+ "",
119
+ ];
120
+
121
+ function createArgvShim() {
122
+ const tempDir = mkdtempSync(join(tmpdir(), "hexclave-windows-argv-"));
123
+ const outputPath = join(tempDir, "argv.json");
124
+ const scriptPath = join(tempDir, "print-argv.cjs");
125
+ const shimPath = join(tempDir, "print-argv.cmd");
126
+
127
+ writeFileSync(scriptPath, [
128
+ 'const fs = require("node:fs");',
129
+ "const args = process.argv.slice(2);",
130
+ 'fs.writeFileSync(process.env.HEXCLAVE_TEST_ARGV_OUTPUT, JSON.stringify(args));',
131
+ ].join("\n"));
132
+ writeFileSync(shimPath, [
133
+ "@echo off",
134
+ `node "%~dp0print-argv.cjs" %*`,
135
+ ].join("\r\n"));
136
+
137
+ return { outputPath, shimPath, tempDir };
138
+ }
139
+
140
+ it.skipIf(process.platform !== "win32")("preserves argv through a Windows command shim", async () => {
141
+ const { outputPath, shimPath, tempDir } = createArgvShim();
142
+
143
+ try {
144
+ await expect(runChildProcess({
145
+ command: shimPath,
146
+ args,
147
+ }, {
148
+ ...process.env,
149
+ HEXCLAVE_TEST_ARGV_OUTPUT: outputPath,
150
+ })).resolves.toBe(0);
151
+ expect(JSON.parse(readFileSync(outputPath, "utf8"))).toEqual(args);
152
+ } finally {
153
+ rmSync(tempDir, { recursive: true, force: true });
154
+ }
155
+ });
156
+
157
+ it.skipIf(process.platform !== "win32")("resolves a bare command name to its Windows shim", async () => {
158
+ const { outputPath, tempDir } = createArgvShim();
159
+
160
+ try {
161
+ await expect(runChildProcess({
162
+ command: "print-argv",
163
+ args,
164
+ }, {
165
+ ...process.env,
166
+ PATH: `;${tempDir};${process.env.PATH ?? ""}`,
167
+ HEXCLAVE_TEST_ARGV_OUTPUT: outputPath,
168
+ })).resolves.toBe(0);
169
+ expect(JSON.parse(readFileSync(outputPath, "utf8"))).toEqual(args);
170
+ } finally {
171
+ rmSync(tempDir, { recursive: true, force: true });
172
+ }
173
+ });
174
+ });
175
+
91
176
  describe("configErrorLogPrefix", () => {
92
177
  it("highlights the config error badge when color is supported", () => {
93
178
  expect(configErrorLogPrefix(true)).toBe("[Hexclave] \x1b[41;37;1m[CONFIG ERROR]\x1b[0m ");
@@ -1,5 +1,6 @@
1
1
  import { execFileSync, spawn, type ChildProcess } from "child_process";
2
2
  import { Command } from "commander";
3
+ import crossSpawn from "cross-spawn";
3
4
  import { chmodSync, closeSync, cpSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "fs";
4
5
  import { dirname, join, resolve } from "path";
5
6
  import { DEFAULT_API_URL, DEFAULT_PUBLISHABLE_CLIENT_KEY, resolveLoginConfig } from "../lib/auth.js";
@@ -70,6 +71,13 @@ const REQUIRED_DASHBOARD_RUNTIME_ENV_VARS = new Set([
70
71
  DASHBOARD_PORT_ENV_VAR,
71
72
  ]);
72
73
 
74
+ export function dashboardEnvWithStatePath(env: NodeJS.ProcessEnv, statePath: string): NodeJS.ProcessEnv {
75
+ return {
76
+ ...env,
77
+ STACK_DEV_ENVS_PATH: env.STACK_DEV_ENVS_PATH ?? statePath,
78
+ };
79
+ }
80
+
73
81
  type DashboardSessionState = {
74
82
  session: DashboardSessionResponse,
75
83
  dashboardReachableSinceMs: number,
@@ -436,7 +444,7 @@ async function startDashboardIfNeeded(options: { apiBaseUrl: string, secret: str
436
444
 
437
445
  const progress = startProgress(`Hexclave dashboard not found on port ${options.port}. Starting now`, { prefix: LOG_PREFIX });
438
446
  const dashboardEnv = {
439
- ...process.env,
447
+ ...dashboardEnvWithStatePath(process.env, devEnvStatePath()),
440
448
  NODE_ENV: devDashboardCommand == null ? "production" : "development",
441
449
  PORT: String(options.port),
442
450
  HOSTNAME: "0.0.0.0",
@@ -718,10 +726,11 @@ child.on("error", (error) => {
718
726
  });
719
727
  `;
720
728
 
721
- function runChildProcess(command: ChildCommand, env: NodeJS.ProcessEnv): Promise<number> {
729
+ export function runChildProcess(command: ChildCommand, env: NodeJS.ProcessEnv): Promise<number> {
722
730
  return new Promise((resolvePromise, reject) => {
723
731
  const child = process.platform === "win32"
724
- ? spawn(command.command, command.args, { stdio: "inherit", env })
732
+ // cross-spawn handles Windows command shims that Node cannot spawn directly.
733
+ ? crossSpawn(command.command, command.args, { stdio: "inherit", env })
725
734
  : spawn(process.execPath, ["-e", APP_COMMAND_WRAPPER_SCRIPT], {
726
735
  detached: true,
727
736
  stdio: "inherit",