glm-coding-router 0.1.0 → 0.1.1

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.
@@ -3,6 +3,7 @@ import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import { loadConfig, saveConfig } from "../core/config.js";
5
5
  import { configPath } from "../core/paths.js";
6
+ import { ExitCode } from "../core/errors.js";
6
7
  import { runDoctorChecks } from "./doctor.js";
7
8
  import { setWindowsUserEnv, ZAI_API_KEY_ENV } from "../core/zai-key.js";
8
9
  import { isWindows } from "../core/platform.js";
@@ -16,15 +17,14 @@ function renderEnvironment(results) {
16
17
  }
17
18
  return lines.join("\n");
18
19
  }
19
- async function askChoices(existingKey, options) {
20
+ async function askChoices(existingKey, options, prompt) {
20
21
  if (options.yes) {
21
- return { configureKey: !existingKey, claude: true, codex: true, codexSkill: true };
22
+ return { kind: "ok", choices: { configureKey: !existingKey, claude: true, codex: true, codexSkill: true } };
22
23
  }
23
24
  if (!process.stdin.isTTY) {
24
- process.stdout.write("Non-interactive terminal detected. Re-run with --yes to accept defaults.\n");
25
- process.exit(2);
25
+ return { kind: "non-interactive" };
26
26
  }
27
- const response = await prompts([
27
+ const response = await prompt([
28
28
  {
29
29
  type: existingKey ? null : "confirm",
30
30
  name: "configureKey",
@@ -36,27 +36,41 @@ async function askChoices(existingKey, options) {
36
36
  { type: "confirm", name: "codexSkill", message: "Install Codex delegation skill?", initial: true },
37
37
  ]);
38
38
  if (response.claude === undefined) {
39
- process.stdout.write("Cancelled.\n");
40
- process.exit(1);
39
+ return { kind: "cancelled" };
41
40
  }
42
41
  return {
43
- configureKey: existingKey ? true : Boolean(response.configureKey),
44
- claude: Boolean(response.claude),
45
- codex: Boolean(response.codex),
46
- codexSkill: Boolean(response.codexSkill),
42
+ kind: "ok",
43
+ choices: {
44
+ configureKey: existingKey ? true : Boolean(response.configureKey),
45
+ claude: Boolean(response.claude),
46
+ codex: Boolean(response.codex),
47
+ codexSkill: Boolean(response.codexSkill),
48
+ },
47
49
  };
48
50
  }
49
51
  /** glm-router init (spec §7): environment report, key setup, config, skill. Idempotent. */
50
- export async function initCommand(options) {
52
+ export async function initCommand(options, deps = {}) {
53
+ const prompt = deps.prompt ?? prompts;
54
+ const home = deps.home ?? os.homedir();
55
+ const env = deps.env ?? process.env;
56
+ const setEnv = deps.setEnv ?? setWindowsUserEnv;
51
57
  process.stdout.write(`GLM Coding Router v${version}\n\n`);
52
- const report = runDoctorChecks();
58
+ const report = runDoctorChecks({ home, env, readUserEnv: deps.readUserEnv });
53
59
  process.stdout.write("Environment\n\n");
54
60
  process.stdout.write(renderEnvironment(report.results) + "\n\n");
55
61
  const existingKey = report.keySource !== undefined;
56
- const choices = await askChoices(existingKey, options);
62
+ const askResult = await askChoices(existingKey, options, prompt);
63
+ if (askResult.kind === "non-interactive") {
64
+ process.stdout.write("Non-interactive terminal detected. Re-run with --yes to accept defaults.\n");
65
+ return ExitCode.InvalidArgs;
66
+ }
67
+ if (askResult.kind === "cancelled") {
68
+ process.stdout.write("Cancelled.\n");
69
+ return ExitCode.GenericFailure;
70
+ }
71
+ const choices = askResult.choices;
57
72
  process.stdout.write("\nInstalling...\n\n");
58
- const home = os.homedir();
59
- const config = loadConfig();
73
+ const config = loadConfig(home);
60
74
  const nextConfig = {
61
75
  ...config,
62
76
  integrations: {
@@ -74,7 +88,7 @@ export async function initCommand(options) {
74
88
  process.stdout.write("⚠ Key storage requires Windows in v0.1 — skipped\n");
75
89
  }
76
90
  else {
77
- const keyResponse = await prompts({
91
+ const keyResponse = await prompt({
78
92
  type: "password",
79
93
  name: "key",
80
94
  message: "Enter Z.ai Coding Plan API key:",
@@ -84,7 +98,7 @@ export async function initCommand(options) {
84
98
  process.stderr.write("Cancelled.\n");
85
99
  return 1;
86
100
  }
87
- setWindowsUserEnv(ZAI_API_KEY_ENV, String(keyResponse.key).trim());
101
+ setEnv(ZAI_API_KEY_ENV, String(keyResponse.key).trim());
88
102
  process.stdout.write(`✓ ${ZAI_API_KEY_ENV} configured\n`);
89
103
  }
90
104
  }
@@ -5,9 +5,11 @@ import { logger } from "../core/logging.js";
5
5
  import { ZAI_API_KEY_ENV, deleteWindowsUserEnv, resolveZaiApiKey, setWindowsUserEnv, } from "../core/zai-key.js";
6
6
  import { emitJson } from "./context.js";
7
7
  /** glm-router key set (spec §11): prompt, save to Windows User Environment. */
8
- export async function keySetCommand(_options) {
8
+ export async function keySetCommand(_options, deps = {}) {
9
9
  assertWindows();
10
- const response = await prompts({
10
+ const prompt = deps.prompt ?? prompts;
11
+ const setEnv = deps.setEnv ?? setWindowsUserEnv;
12
+ const response = await prompt({
11
13
  type: "password",
12
14
  name: "key",
13
15
  message: "Enter Z.ai Coding Plan API key:",
@@ -19,7 +21,7 @@ export async function keySetCommand(_options) {
19
21
  }
20
22
  const key = String(response.key).trim();
21
23
  try {
22
- setWindowsUserEnv(ZAI_API_KEY_ENV, key);
24
+ setEnv(ZAI_API_KEY_ENV, key);
23
25
  }
24
26
  catch (error) {
25
27
  logger.error(`Failed to write the Windows User Environment: ${error instanceof Error ? error.message : String(error)}`);
@@ -30,8 +32,8 @@ export async function keySetCommand(_options) {
30
32
  return 0;
31
33
  }
32
34
  /** glm-router key check (spec §11): report presence + source; never the value. */
33
- export function keyCheckCommand(options) {
34
- const resolved = resolveZaiApiKey();
35
+ export function keyCheckCommand(options, deps = {}) {
36
+ const resolved = resolveZaiApiKey({ env: deps.env, readUserEnv: deps.readUserEnv });
35
37
  if (options.json) {
36
38
  emitJson({
37
39
  configured: Boolean(resolved),
@@ -50,7 +52,8 @@ export function keyCheckCommand(options) {
50
52
  return 0;
51
53
  }
52
54
  /** Used by uninstall; keeps the key by default (spec §44). */
53
- export async function keyRemoveCommand() {
55
+ export async function keyRemoveCommand(deps = {}) {
54
56
  assertWindows();
55
- deleteWindowsUserEnv(ZAI_API_KEY_ENV);
57
+ const deleteEnv = deps.deleteEnv ?? deleteWindowsUserEnv;
58
+ deleteEnv(ZAI_API_KEY_ENV);
56
59
  }
@@ -29,15 +29,15 @@ export function renderChangeDiff(change) {
29
29
  * CLAUDE.md and AGENTS.md at the project root. Idempotent; never overwrites
30
30
  * user content; supports --dry-run.
31
31
  */
32
- export function projectInitCommand(options) {
33
- const root = findProjectRoot();
34
- const config = loadConfig();
32
+ export function projectInitCommand(options, deps = {}) {
33
+ const root = deps.root ?? findProjectRoot();
34
+ const config = loadConfig(deps.home);
35
35
  const changes = [];
36
36
  if (config.integrations.claude) {
37
- changes.push(installClaudeIntegration(root, { dryRun: options.dryRun }));
37
+ changes.push(installClaudeIntegration(root, { home: deps.home, dryRun: options.dryRun }));
38
38
  }
39
39
  if (config.integrations.codex) {
40
- changes.push(installCodexIntegration(root, { dryRun: options.dryRun }));
40
+ changes.push(installCodexIntegration(root, { home: deps.home, dryRun: options.dryRun }));
41
41
  }
42
42
  if (changes.length === 0) {
43
43
  process.stdout.write("All integrations are disabled in config — nothing to do.\n");
@@ -45,7 +45,12 @@ export function projectInitCommand(options) {
45
45
  }
46
46
  for (const change of changes) {
47
47
  if (options.dryRun) {
48
- process.stdout.write(`[dry-run] would update ${change.file}:\n${renderChangeDiff(change)}\n`);
48
+ if (change.changed) {
49
+ process.stdout.write(`[dry-run] would update ${change.file}:\n${renderChangeDiff(change)}\n`);
50
+ }
51
+ else {
52
+ process.stdout.write(`[dry-run] ${change.file} — already up to date\n`);
53
+ }
49
54
  }
50
55
  else if (!change.changed) {
51
56
  process.stdout.write(`✓ ${change.file} — already up to date\n`);
@@ -7,20 +7,21 @@ import { CodexSkillInstaller } from "../integrations/skill.js";
7
7
  import { GLM_DELEGATION_SKILL_NAME } from "../templates/glm-delegation-skill.js";
8
8
  import { emitJson } from "./context.js";
9
9
  /** Fast, fully offline summary (spec §41) — no API requests, no key values. */
10
- export function statusCommand(options) {
11
- const config = loadConfig();
12
- const home = os.homedir();
13
- const resolved = resolveZaiApiKey();
10
+ export function statusCommand(options, deps = {}) {
11
+ const home = deps.home ?? os.homedir();
12
+ const env = deps.env ?? process.env;
13
+ const config = loadConfig(home);
14
+ const resolved = resolveZaiApiKey({ env, readUserEnv: deps.readUserEnv });
14
15
  const claudeInstalled = (() => {
15
16
  try {
16
- locateClaude(config);
17
+ locateClaude(config, env);
17
18
  return true;
18
19
  }
19
20
  catch {
20
21
  return false;
21
22
  }
22
23
  })();
23
- const codexInstalled = Boolean(locateCodex(config));
24
+ const codexInstalled = Boolean(locateCodex(config, env));
24
25
  const skillInstaller = new CodexSkillInstaller(home);
25
26
  const skillInstalled = skillInstaller.detect() !== null && skillInstaller.isInstalled(GLM_DELEGATION_SKILL_NAME);
26
27
  if (options.json) {
@@ -2,10 +2,11 @@ import prompts from "prompts";
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import { configDir } from "../core/paths.js";
5
+ import { ExitCode } from "../core/errors.js";
5
6
  import { CodexSkillInstaller, glmDelegationSkill } from "../integrations/skill.js";
6
7
  import { keyRemoveCommand } from "./key.js";
7
8
  import { removeClaudeIntegration, removeCodexIntegration } from "../integrations/index.js";
8
- async function askChoices(options) {
9
+ async function askChoices(options, prompt) {
9
10
  // Defaults per spec §44: keep ZAI_API_KEY; destructive credential removal
10
11
  // requires explicit consent, so --yes never flips it.
11
12
  const defaults = {
@@ -15,33 +16,46 @@ async function askChoices(options) {
15
16
  removeKey: false,
16
17
  };
17
18
  if (options.yes || options.force) {
18
- return options.force ? { ...defaults, removeProjectIntegration: true } : defaults;
19
+ return {
20
+ kind: "ok",
21
+ choices: options.force ? { ...defaults, removeProjectIntegration: true } : defaults,
22
+ };
19
23
  }
20
24
  if (!process.stdin.isTTY) {
21
- process.stdout.write("Non-interactive terminal detected. Re-run with --yes for safe defaults.\n");
22
- process.exit(2);
25
+ return { kind: "non-interactive" };
23
26
  }
24
- const response = await prompts([
27
+ const response = await prompt([
25
28
  { type: "confirm", name: "removeConfig", message: "Remove global configuration?", initial: true },
26
29
  { type: "confirm", name: "removeSkill", message: "Remove Codex skill?", initial: true },
27
30
  { type: "confirm", name: "removeProject", message: "Remove current project integration?", initial: false },
28
31
  { type: "confirm", name: "removeKey", message: "Remove ZAI_API_KEY?", initial: false },
29
32
  ]);
30
33
  if (response.removeConfig === undefined) {
31
- process.stdout.write("Cancelled.\n");
32
- process.exit(1);
34
+ return { kind: "cancelled" };
33
35
  }
34
36
  return {
35
- removeConfig: Boolean(response.removeConfig),
36
- removeSkill: Boolean(response.removeSkill),
37
- removeProjectIntegration: Boolean(response.removeProject),
38
- removeKey: Boolean(response.removeKey),
37
+ kind: "ok",
38
+ choices: {
39
+ removeConfig: Boolean(response.removeConfig),
40
+ removeSkill: Boolean(response.removeSkill),
41
+ removeProjectIntegration: Boolean(response.removeProject),
42
+ removeKey: Boolean(response.removeKey),
43
+ },
39
44
  };
40
45
  }
41
46
  /** glm-router uninstall (spec §44): wizard with safe defaults. */
42
- export async function uninstallCommand(options) {
43
- const choices = await askChoices(options);
44
- const home = os.homedir();
47
+ export async function uninstallCommand(options, deps = {}) {
48
+ const askResult = await askChoices(options, deps.prompt ?? prompts);
49
+ if (askResult.kind === "non-interactive") {
50
+ process.stdout.write("Non-interactive terminal detected. Re-run with --yes for safe defaults.\n");
51
+ return ExitCode.InvalidArgs;
52
+ }
53
+ if (askResult.kind === "cancelled") {
54
+ process.stdout.write("Cancelled.\n");
55
+ return ExitCode.GenericFailure;
56
+ }
57
+ const choices = askResult.choices;
58
+ const home = deps.home ?? os.homedir();
45
59
  if (choices.removeSkill) {
46
60
  const skillInstaller = new CodexSkillInstaller(home);
47
61
  const skill = glmDelegationSkill();
@@ -54,7 +68,7 @@ export async function uninstallCommand(options) {
54
68
  }
55
69
  }
56
70
  if (choices.removeProjectIntegration) {
57
- const root = process.cwd();
71
+ const root = deps.root ?? process.cwd();
58
72
  removeClaudeIntegration(root);
59
73
  removeCodexIntegration(root);
60
74
  process.stdout.write("✓ Project integration removed (CLAUDE.md / AGENTS.md managed blocks)\n");
@@ -70,7 +84,7 @@ export async function uninstallCommand(options) {
70
84
  }
71
85
  }
72
86
  if (choices.removeKey) {
73
- await keyRemoveCommand();
87
+ await keyRemoveCommand({ deleteEnv: deps.deleteEnv });
74
88
  process.stdout.write("✓ ZAI_API_KEY removed from Windows User Environment\n");
75
89
  }
76
90
  else {
@@ -3,13 +3,14 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { Errors } from "./errors.js";
5
5
  import { isWindows } from "./platform.js";
6
- function runWhere(name) {
6
+ function runWhere(name, env = process.env) {
7
7
  const finder = isWindows() ? "where.exe" : "which";
8
8
  try {
9
9
  const output = execFileSync(finder, [name], {
10
10
  encoding: "utf8",
11
11
  windowsHide: true,
12
12
  stdio: ["ignore", "pipe", "ignore"],
13
+ env,
13
14
  });
14
15
  return output
15
16
  .split(/\r?\n/)
@@ -55,14 +56,17 @@ function preferNative(matches) {
55
56
  * 2. PATH search through Node
56
57
  * 3. config override
57
58
  * 4. error
59
+ *
60
+ * `env` defaults to `process.env` and is threaded through for testability —
61
+ * production callers never pass it.
58
62
  */
59
- export function locateClaude(config) {
60
- const matches = runWhere("claude");
63
+ export function locateClaude(config, env = process.env) {
64
+ const matches = runWhere("claude", env);
61
65
  const fromWhere = preferNative(matches);
62
66
  if (fromWhere) {
63
67
  return fromWhere;
64
68
  }
65
- const fromPath = searchPathFor("claude");
69
+ const fromPath = searchPathFor("claude", env);
66
70
  if (fromPath) {
67
71
  return fromPath;
68
72
  }
@@ -80,12 +84,12 @@ export function locateClaude(config) {
80
84
  * the tool must work with a Claude-only setup. `required` callers
81
85
  * (glm commands that need codex) get a proper error.
82
86
  */
83
- export function locateCodex(config) {
84
- const matches = runWhere("codex");
87
+ export function locateCodex(config, env = process.env) {
88
+ const matches = runWhere("codex", env);
85
89
  if (matches.length > 0) {
86
90
  return preferNative(matches);
87
91
  }
88
- const fromPath = searchPathFor("codex");
92
+ const fromPath = searchPathFor("codex", env);
89
93
  if (fromPath) {
90
94
  return fromPath;
91
95
  }
@@ -95,8 +99,8 @@ export function locateCodex(config) {
95
99
  }
96
100
  return undefined;
97
101
  }
98
- export function codexRequired(config) {
99
- const found = locateCodex(config);
102
+ export function codexRequired(config, env = process.env) {
103
+ const found = locateCodex(config, env);
100
104
  if (!found) {
101
105
  throw Errors.codexNotFound();
102
106
  }
@@ -1,46 +1,46 @@
1
1
  /** Managed block content for AGENTS.md (spec §24). Keep in sync with the spec. */
2
- export const AGENTS_MANAGED_BLOCK = `<!-- glm-coding-router:start -->
3
-
4
- ## GLM Worker Delegation
5
-
6
- Available commands:
7
-
8
- - \`glm-worker "<task>"\`
9
- - \`glm-review "<task>"\`
10
-
11
- Codex is the primary orchestrator.
12
-
13
- Delegate:
14
- - CRUD
15
- - boilerplate
16
- - tests
17
- - documentation
18
- - mechanical refactoring
19
- - repository exploration
20
- - straightforward implementation
21
-
22
- Keep in Codex:
23
- - requirements
24
- - planning
25
- - architecture
26
- - ambiguous business logic
27
- - complex debugging
28
- - security decisions
29
- - integration
30
- - final review
31
-
32
- Before delegation create a task packet:
33
-
34
- TASK
35
- SCOPE
36
- FILES ALLOWED TO MODIFY
37
- FILES NOT TO MODIFY
38
- REQUIREMENTS
39
- CONSTRAINTS
40
- ACCEPTANCE CRITERIA
41
- VALIDATION
42
- EXPECTED OUTPUT
43
-
44
- Never trust a worker's success report without inspecting the resulting changes.
45
-
2
+ export const AGENTS_MANAGED_BLOCK = `<!-- glm-coding-router:start -->
3
+
4
+ ## GLM Worker Delegation
5
+
6
+ Available commands:
7
+
8
+ - \`glm-worker "<task>"\`
9
+ - \`glm-review "<task>"\`
10
+
11
+ Codex is the primary orchestrator.
12
+
13
+ Delegate:
14
+ - CRUD
15
+ - boilerplate
16
+ - tests
17
+ - documentation
18
+ - mechanical refactoring
19
+ - repository exploration
20
+ - straightforward implementation
21
+
22
+ Keep in Codex:
23
+ - requirements
24
+ - planning
25
+ - architecture
26
+ - ambiguous business logic
27
+ - complex debugging
28
+ - security decisions
29
+ - integration
30
+ - final review
31
+
32
+ Before delegation create a task packet:
33
+
34
+ TASK
35
+ SCOPE
36
+ FILES ALLOWED TO MODIFY
37
+ FILES NOT TO MODIFY
38
+ REQUIREMENTS
39
+ CONSTRAINTS
40
+ ACCEPTANCE CRITERIA
41
+ VALIDATION
42
+ EXPECTED OUTPUT
43
+
44
+ Never trust a worker's success report without inspecting the resulting changes.
45
+
46
46
  <!-- glm-coding-router:end -->`;
@@ -1,49 +1,49 @@
1
1
  /** Managed block content for CLAUDE.md (spec §20). Keep in sync with the spec. */
2
- export const CLAUDE_MANAGED_BLOCK = `<!-- glm-coding-router:start -->
3
-
4
- ## GLM Worker Delegation
5
-
6
- GLM workers available:
7
-
8
- - \`glm-worker "<task>"\`
9
- - \`glm-review "<task>"\`
10
-
11
- Delegate well-scoped, implementation-heavy work to GLM.
12
-
13
- Use GLM for:
14
- - repository exploration
15
- - CRUD
16
- - boilerplate
17
- - tests
18
- - documentation
19
- - mechanical refactoring
20
- - straightforward implementation
21
-
22
- Claude remains responsible for:
23
- - requirements
24
- - architecture
25
- - ambiguous business rules
26
- - security-sensitive decisions
27
- - complex debugging
28
- - integration
29
- - final review
30
-
31
- Before delegation, define:
32
- - task
33
- - scope
34
- - allowed files
35
- - forbidden files
36
- - requirements
37
- - constraints
38
- - acceptance criteria
39
- - validation command
40
- - expected output
41
-
42
- After worker completion:
43
- 1. inspect the actual diff
44
- 2. validate against requirements
45
- 3. run relevant tests
46
- 4. resolve integration problems
47
- 5. accept only after verification
48
-
2
+ export const CLAUDE_MANAGED_BLOCK = `<!-- glm-coding-router:start -->
3
+
4
+ ## GLM Worker Delegation
5
+
6
+ GLM workers available:
7
+
8
+ - \`glm-worker "<task>"\`
9
+ - \`glm-review "<task>"\`
10
+
11
+ Delegate well-scoped, implementation-heavy work to GLM.
12
+
13
+ Use GLM for:
14
+ - repository exploration
15
+ - CRUD
16
+ - boilerplate
17
+ - tests
18
+ - documentation
19
+ - mechanical refactoring
20
+ - straightforward implementation
21
+
22
+ Claude remains responsible for:
23
+ - requirements
24
+ - architecture
25
+ - ambiguous business rules
26
+ - security-sensitive decisions
27
+ - complex debugging
28
+ - integration
29
+ - final review
30
+
31
+ Before delegation, define:
32
+ - task
33
+ - scope
34
+ - allowed files
35
+ - forbidden files
36
+ - requirements
37
+ - constraints
38
+ - acceptance criteria
39
+ - validation command
40
+ - expected output
41
+
42
+ After worker completion:
43
+ 1. inspect the actual diff
44
+ 2. validate against requirements
45
+ 3. run relevant tests
46
+ 4. resolve integration problems
47
+ 5. accept only after verification
48
+
49
49
  <!-- glm-coding-router:end -->`;