@hexclave/cli 1.0.69 → 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/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
- package/src/commands/dev.test.ts +87 -2
- package/src/commands/dev.ts +12 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hexclave/cli",
|
|
3
|
-
"version": "1.0.
|
|
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.
|
|
30
|
-
"@hexclave/
|
|
31
|
-
"@hexclave/
|
|
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",
|
package/src/commands/dev.test.ts
CHANGED
|
@@ -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 ");
|
package/src/commands/dev.ts
CHANGED
|
@@ -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
|
-
|
|
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",
|