فهرست منبع

fix: config set auto-coerces types and validates keys

Numbers stored as numbers, booleans as booleans. Unknown keys are
rejected with "Did you mean?" suggestion.
Ma 7 ماه پیش
والد
کامیت
b2bae4249d
2فایلهای تغییر یافته به همراه40 افزوده شده و 10 حذف شده
  1. 7 9
      packages/cli/src/__tests__/cli-integration.test.ts
  2. 33 1
      packages/cli/src/commands/config.ts

+ 7 - 9
packages/cli/src/__tests__/cli-integration.test.ts

@@ -114,9 +114,9 @@ describe("CLI integration", () => {
   });
   });
 
 
   describe("inkos config set", () => {
   describe("inkos config set", () => {
-    it("sets a top-level config value", () => {
-      const output = run(["config", "set", "name", "test-project"]);
-      expect(output).toContain("Set name = test-project");
+    it("sets a known config value", () => {
+      const output = run(["config", "set", "llm.provider", "anthropic"]);
+      expect(output).toContain("Set llm.provider = anthropic");
     });
     });
 
 
     it("sets a nested config value", async () => {
     it("sets a nested config value", async () => {
@@ -126,11 +126,10 @@ describe("CLI integration", () => {
       expect(config.llm.model).toBe("gpt-5");
       expect(config.llm.model).toBe("gpt-5");
     });
     });
 
 
-    it("creates intermediate keys for deep paths", async () => {
-      run(["config", "set", "custom.nested.key", "value"]);
-      const raw = await readFile(join(projectDir, "inkos.json"), "utf-8");
-      const config = JSON.parse(raw);
-      expect(config.custom.nested.key).toBe("value");
+    it("rejects unknown config keys", () => {
+      expect(() => {
+        run(["config", "set", "custom.nested.key", "value"]);
+      }).toThrow();
     });
     });
   });
   });
 
 
@@ -138,7 +137,6 @@ describe("CLI integration", () => {
     it("shows current config as JSON", () => {
     it("shows current config as JSON", () => {
       const output = run(["config", "show"]);
       const output = run(["config", "show"]);
       const config = JSON.parse(output);
       const config = JSON.parse(output);
-      expect(config.name).toBe("test-project");
       expect(config.llm.model).toBe("gpt-5");
       expect(config.llm.model).toBe("gpt-5");
     });
     });
   });
   });

+ 33 - 1
packages/cli/src/commands/config.ts

@@ -20,6 +20,28 @@ configCommand
       const config = JSON.parse(raw);
       const config = JSON.parse(raw);
 
 
       const keys = key.split(".");
       const keys = key.split(".");
+
+      const KNOWN_KEYS = new Set([
+        "llm.provider", "llm.baseUrl", "llm.model", "llm.temperature",
+        "llm.maxTokens", "llm.thinkingBudget", "llm.apiFormat", "llm.stream",
+        "daemon.schedule.radarCron", "daemon.schedule.writeCron",
+        "daemon.maxConcurrentBooks", "daemon.chaptersPerCycle",
+        "daemon.retryDelayMs", "daemon.cooldownAfterChapterMs",
+        "daemon.maxChaptersPerDay",
+      ]);
+      if (!KNOWN_KEYS.has(key)) {
+        // Find closest match for typo suggestion
+        const candidates = [...KNOWN_KEYS];
+        const suggestion = candidates.find(k => {
+          const parts = k.split(".");
+          const inputParts = key.split(".");
+          return parts.length === inputParts.length && parts[0] === inputParts[0];
+        });
+        logError(`Unknown config key "${key}".${suggestion ? ` Did you mean "${suggestion}"?` : ""}`);
+        log(`Known keys: ${candidates.join(", ")}`);
+        process.exit(1);
+      }
+
       let target = config;
       let target = config;
       for (let i = 0; i < keys.length - 1; i++) {
       for (let i = 0; i < keys.length - 1; i++) {
         const k = keys[i]!;
         const k = keys[i]!;
@@ -28,7 +50,17 @@ configCommand
         }
         }
         target = target[k];
         target = target[k];
       }
       }
-      target[keys[keys.length - 1]!] = value;
+      const finalKey = keys[keys.length - 1]!;
+      // Auto-coerce types: numbers and booleans shouldn't be stored as strings
+      if (/^\d+(\.\d+)?$/.test(value)) {
+        target[finalKey] = parseFloat(value);
+      } else if (value === "true") {
+        target[finalKey] = true;
+      } else if (value === "false") {
+        target[finalKey] = false;
+      } else {
+        target[finalKey] = value;
+      }
 
 
       await writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
       await writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
       log(`Set ${key} = ${value}`);
       log(`Set ${key} = ${value}`);