@hexclave/cli 1.0.51 → 1.0.53
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 -8
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/commands/config-file.test.ts +17 -7
- package/src/commands/config-file.ts +25 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hexclave/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.53",
|
|
4
4
|
"repository": "https://github.com/hexclave/hexclave",
|
|
5
5
|
"description": "The CLI for Hexclave. https://hexclave.com",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"commander": "^13.1.0",
|
|
26
26
|
"extract-zip": "^2.0.1",
|
|
27
27
|
"jiti": "^2.4.2",
|
|
28
|
-
"@hexclave/
|
|
29
|
-
"@hexclave/
|
|
30
|
-
"@hexclave/shared": "1.0.
|
|
28
|
+
"@hexclave/js": "1.0.53",
|
|
29
|
+
"@hexclave/shared": "1.0.53",
|
|
30
|
+
"@hexclave/shared-backend": "1.0.53"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node": "20.17.6",
|
|
@@ -2,7 +2,7 @@ import * as fs from "fs";
|
|
|
2
2
|
import * as os from "os";
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
-
import { buildConfigPushSource, resolveConfigFilePathForPull
|
|
5
|
+
import { assertConfigPullTarget, buildConfigPushSource, resolveConfigFilePathForPull } from "./config-file.js";
|
|
6
6
|
|
|
7
7
|
describe("resolveConfigFilePathForPull", () => {
|
|
8
8
|
let tmpDir: string;
|
|
@@ -46,7 +46,7 @@ describe("resolveConfigFilePathForPull", () => {
|
|
|
46
46
|
});
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
-
describe("
|
|
49
|
+
describe("assertConfigPullTarget", () => {
|
|
50
50
|
let tmpDir: string;
|
|
51
51
|
|
|
52
52
|
beforeEach(() => {
|
|
@@ -57,16 +57,26 @@ describe("shouldReplaceConfigFileForPull", () => {
|
|
|
57
57
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
it("
|
|
60
|
+
it("does not throw when the target file does not exist", () => {
|
|
61
|
+
expect(() => assertConfigPullTarget(path.join(tmpDir, "hexclave.config.ts"), {})).not.toThrow();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("throws when the target file already exists and --overwrite is not set", () => {
|
|
61
65
|
const configPath = path.join(tmpDir, "hexclave.config.ts");
|
|
62
66
|
fs.writeFileSync(configPath, "export const config = {};\n");
|
|
67
|
+
expect(() => assertConfigPullTarget(configPath, {})).toThrow(/already exists.*--overwrite/s);
|
|
68
|
+
});
|
|
63
69
|
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
it("throws for an existing placeholder/comment-only file without --overwrite", () => {
|
|
71
|
+
const configPath = path.join(tmpDir, "stack.config.ts");
|
|
72
|
+
fs.writeFileSync(configPath, "// placeholder so the file exists\n");
|
|
73
|
+
expect(() => assertConfigPullTarget(configPath, {})).toThrow(/already exists/);
|
|
66
74
|
});
|
|
67
75
|
|
|
68
|
-
it("
|
|
69
|
-
|
|
76
|
+
it("does not throw when --overwrite is set, even if the file exists", () => {
|
|
77
|
+
const configPath = path.join(tmpDir, "hexclave.config.ts");
|
|
78
|
+
fs.writeFileSync(configPath, "export const config = {};\n");
|
|
79
|
+
expect(() => assertConfigPullTarget(configPath, { overwrite: true })).not.toThrow();
|
|
70
80
|
});
|
|
71
81
|
});
|
|
72
82
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { replaceConfigObject
|
|
1
|
+
import { replaceConfigObject } from "@hexclave/shared-backend";
|
|
2
2
|
import { detectImportPackageFromDir } from "@hexclave/shared/dist/config-eval";
|
|
3
3
|
import { isValidConfig } from "@hexclave/shared/dist/config/format";
|
|
4
4
|
import type { EnvironmentConfigOverrideOverride } from "@hexclave/shared/dist/config/schema";
|
|
@@ -217,8 +217,20 @@ export function resolveConfigFilePathForPull(opts: { configFile?: string }, cwd:
|
|
|
217
217
|
return candidate;
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
|
|
221
|
-
|
|
220
|
+
// `config pull` means "download the entire branch config into a fresh local file". It always writes
|
|
221
|
+
// the whole file (via replaceConfigObject) and never edits an existing file in place. In-place,
|
|
222
|
+
// hand-authored-preserving edits are the job of the config *update* flow (e.g. from the RDE), which
|
|
223
|
+
// routes through updateConfigObject's agent-assisted rewrite — that path is intentionally not
|
|
224
|
+
// reachable from `pull`.
|
|
225
|
+
//
|
|
226
|
+
// Because pull writes the whole file, it would clobber whatever is already at the target path. To
|
|
227
|
+
// avoid silently destroying a hand-authored config, we refuse to write over an existing file and
|
|
228
|
+
// require the user to opt in explicitly with --overwrite.
|
|
229
|
+
export function assertConfigPullTarget(filePath: string, opts: { overwrite?: boolean }): void {
|
|
230
|
+
if (opts.overwrite === true) return;
|
|
231
|
+
if (fs.existsSync(filePath)) {
|
|
232
|
+
throw new CliError(`A config file already exists at ${filePath}. Pass --overwrite to replace it with the pulled config, or remove the file first.`);
|
|
233
|
+
}
|
|
222
234
|
}
|
|
223
235
|
|
|
224
236
|
export function registerConfigCommand(program: Command) {
|
|
@@ -231,30 +243,28 @@ export function registerConfigCommand(program: Command) {
|
|
|
231
243
|
.description("Pull branch config to a local file")
|
|
232
244
|
.option("--cloud-project-id <id>", "Cloud project ID to pull config from (defaults to the STACK_PROJECT_ID env var)")
|
|
233
245
|
.option("--config-file <path>", "Path to write config file (.ts); defaults to ./hexclave.config.ts in the current directory")
|
|
234
|
-
.option("--overwrite", "
|
|
246
|
+
.option("--overwrite", "Replace the config file if one already exists at the target path")
|
|
235
247
|
.action(async (opts) => {
|
|
236
248
|
const auth = resolveAuth(resolveProjectId(opts.cloudProjectId));
|
|
237
249
|
if (!isProjectAuthWithRefreshToken(auth)) {
|
|
238
250
|
throw new CliError("`hexclave config pull` requires `hexclave login`. Remove STACK_SECRET_SERVER_KEY and try again.");
|
|
239
251
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const configOverride = await project.getConfigOverride("branch");
|
|
243
|
-
if (!isValidConfig(configOverride)) {
|
|
244
|
-
throw new CliError("Pulled branch config is not a valid local config object.");
|
|
245
|
-
}
|
|
252
|
+
// Resolve and validate the target file before any network work so we fail fast (e.g. when the
|
|
253
|
+
// target already exists without --overwrite) instead of paying for a wasted round-trip.
|
|
246
254
|
const filePath = resolveConfigFilePathForPull(opts, process.cwd());
|
|
247
255
|
const ext = path.extname(filePath);
|
|
248
|
-
|
|
249
256
|
if (ext !== ".ts") {
|
|
250
257
|
throw new CliError("Config file must have a .ts extension. Typed config files require TypeScript.");
|
|
251
258
|
}
|
|
259
|
+
assertConfigPullTarget(filePath, opts);
|
|
252
260
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
261
|
+
const project = await getAdminProject(auth);
|
|
262
|
+
|
|
263
|
+
const configOverride = await project.getConfigOverride("branch");
|
|
264
|
+
if (!isValidConfig(configOverride)) {
|
|
265
|
+
throw new CliError("Pulled branch config is not a valid local config object.");
|
|
257
266
|
}
|
|
267
|
+
await replaceConfigObject(filePath, configOverride);
|
|
258
268
|
console.log(`Config written to ${filePath}`);
|
|
259
269
|
});
|
|
260
270
|
|