glm-coding-router 0.1.0

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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +248 -0
  3. package/dist/bin/glm-chat.js +40 -0
  4. package/dist/bin/glm-review.js +47 -0
  5. package/dist/bin/glm-worker.js +57 -0
  6. package/dist/cli.js +108 -0
  7. package/dist/commands/config.js +39 -0
  8. package/dist/commands/context.js +20 -0
  9. package/dist/commands/doctor-command.js +64 -0
  10. package/dist/commands/doctor.js +93 -0
  11. package/dist/commands/init.js +123 -0
  12. package/dist/commands/key.js +56 -0
  13. package/dist/commands/project-init.js +61 -0
  14. package/dist/commands/project-remove.js +36 -0
  15. package/dist/commands/skill.js +43 -0
  16. package/dist/commands/status.js +57 -0
  17. package/dist/commands/uninstall.js +81 -0
  18. package/dist/core/claude.js +104 -0
  19. package/dist/core/config.js +150 -0
  20. package/dist/core/env.js +22 -0
  21. package/dist/core/errors.js +121 -0
  22. package/dist/core/logging.js +54 -0
  23. package/dist/core/main-guard.js +20 -0
  24. package/dist/core/paths.js +13 -0
  25. package/dist/core/platform.js +20 -0
  26. package/dist/core/process.js +60 -0
  27. package/dist/core/prompt.js +32 -0
  28. package/dist/core/version.js +3 -0
  29. package/dist/core/zai-key.js +84 -0
  30. package/dist/integrations/claude.js +18 -0
  31. package/dist/integrations/codex.js +18 -0
  32. package/dist/integrations/index.js +3 -0
  33. package/dist/integrations/skill.js +35 -0
  34. package/dist/project/atomic-write.js +21 -0
  35. package/dist/project/managed-block.js +100 -0
  36. package/dist/project/managed-file.js +67 -0
  37. package/dist/project/ownership.js +42 -0
  38. package/dist/project/project-root.js +26 -0
  39. package/dist/templates/agents-block.js +46 -0
  40. package/dist/templates/claude-block.js +49 -0
  41. package/dist/templates/glm-delegation-skill.js +68 -0
  42. package/package.json +46 -0
@@ -0,0 +1,93 @@
1
+ import os from "node:os";
2
+ import { execFileSync } from "node:child_process";
3
+ import { defaultConfig, loadConfig } from "../core/config.js";
4
+ import { locateClaude, locateCodex, searchPathFor } from "../core/claude.js";
5
+ import { isWindows, windowsVersionName } from "../core/platform.js";
6
+ import { resolveZaiApiKey } from "../core/zai-key.js";
7
+ import { configPath } from "../core/paths.js";
8
+ import fs from "node:fs";
9
+ import { CodexSkillInstaller } from "../integrations/skill.js";
10
+ import { GLM_DELEGATION_SKILL_NAME } from "../templates/glm-delegation-skill.js";
11
+ function check(section, name, status, detail, note) {
12
+ return { section, name, status, detail, note };
13
+ }
14
+ function nodeVersionMajor() {
15
+ return Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
16
+ }
17
+ function gitFound() {
18
+ try {
19
+ execFileSync("git", ["--version"], { windowsHide: true, stdio: "ignore" });
20
+ return true;
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ }
26
+ /** All doctor checks (spec §9). Read-only; never exposes the key value. */
27
+ export function runDoctorChecks(options = {}) {
28
+ const results = [];
29
+ const home = options.home ?? os.homedir();
30
+ // --- System ---
31
+ results.push(check("System", isWindows() ? windowsVersionName() : `Platform ${process.platform}`, isWindows() ? "ok" : "fail", undefined, isWindows() ? undefined : "v0.1 targets Windows only."));
32
+ const nodeMajor = nodeVersionMajor();
33
+ results.push(check("System", `Node.js ${process.versions.node}`, nodeMajor >= 20 ? "ok" : "fail", undefined, nodeMajor >= 20 ? undefined : "glm-coding-router requires Node.js >= 20."));
34
+ results.push(check("System", "Git", gitFound() ? "ok" : "warn"));
35
+ // --- Z.ai / config ---
36
+ let config = defaultConfig();
37
+ const configFile = configPath(home);
38
+ const configExists = fs.existsSync(configFile);
39
+ if (!configExists) {
40
+ results.push(check("Z.ai", "Configuration", "ok", "defaults (config.json not created yet)"));
41
+ }
42
+ else {
43
+ try {
44
+ config = loadConfig(home);
45
+ results.push(check("Z.ai", "Configuration", "ok", configFile));
46
+ }
47
+ catch (error) {
48
+ results.push(check("Z.ai", "Configuration", "fail", error instanceof Error ? error.message : String(error)));
49
+ }
50
+ }
51
+ // --- Agents ---
52
+ try {
53
+ const claudePath = locateClaude(config);
54
+ results.push(check("Agents", "Claude Code", "ok", claudePath));
55
+ }
56
+ catch (error) {
57
+ results.push(check("Agents", "Claude Code", "fail", error instanceof Error ? error.message : String(error)));
58
+ }
59
+ const codexPath = locateCodex(config);
60
+ results.push(check("Agents", "Codex", codexPath ? "ok" : "warn", codexPath, codexPath ? undefined : "Optional — Claude-only setups are supported."));
61
+ // --- Z.ai key ---
62
+ const resolved = resolveZaiApiKey();
63
+ results.push(check("Z.ai", "ZAI_API_KEY", resolved ? "ok" : "fail", resolved ? `configured (${resolved.source})` : "not found", resolved ? undefined : "Run: glm-router key set"));
64
+ results.push(check("Z.ai", "Anthropic endpoint", "ok", config.provider.anthropicBaseUrl));
65
+ // --- Commands (PATH shims; a dev checkout warns instead of failing) ---
66
+ for (const command of ["glm-chat", "glm-worker", "glm-review"]) {
67
+ const found = searchPathFor(command);
68
+ results.push(check("Commands", command, found ? "ok" : "warn", found, found ? undefined : "Not on PATH — install globally (npm install -g glm-coding-router) or use npm run dev."));
69
+ }
70
+ // --- Claude / Codex integrations ---
71
+ results.push(check("Claude", "Integration", config.integrations.claude ? "ok" : "warn", config.integrations.claude ? "enabled" : "disabled in config"));
72
+ results.push(check("Codex", "AGENTS.md integration", config.integrations.codex ? "ok" : "warn", config.integrations.codex ? "enabled" : "disabled in config"));
73
+ const skillInstaller = new CodexSkillInstaller(home);
74
+ const skillDetected = skillInstaller.detect() !== null;
75
+ const skillInstalled = skillDetected && skillInstaller.isInstalled(GLM_DELEGATION_SKILL_NAME);
76
+ results.push(check("Codex", "Delegation skill", skillInstalled ? "ok" : "warn", skillInstalled
77
+ ? "installed"
78
+ : skillDetected
79
+ ? "not installed (optional)"
80
+ : "Codex home not detected — skill skipped (optional)"));
81
+ // --- Environment: the Orca stale-env case (spec §9, §10) ---
82
+ const hasProcessKey = Boolean(process.env.ZAI_API_KEY && process.env.ZAI_API_KEY.trim());
83
+ if (hasProcessKey) {
84
+ results.push(check("Environment", "Process environment", "ok", "ZAI_API_KEY visible in current process"));
85
+ }
86
+ else if (resolved) {
87
+ results.push(check("Environment", "Process environment", "warn", "Current process does not contain ZAI_API_KEY", "This is safe. GLM workers reload the key automatically from the Windows User Environment."));
88
+ }
89
+ return { results, config, keySource: resolved?.source };
90
+ }
91
+ export function doctorHasFailures(results) {
92
+ return results.some((result) => result.status === "fail");
93
+ }
@@ -0,0 +1,123 @@
1
+ import prompts from "prompts";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import { loadConfig, saveConfig } from "../core/config.js";
5
+ import { configPath } from "../core/paths.js";
6
+ import { runDoctorChecks } from "./doctor.js";
7
+ import { setWindowsUserEnv, ZAI_API_KEY_ENV } from "../core/zai-key.js";
8
+ import { isWindows } from "../core/platform.js";
9
+ import { CodexSkillInstaller, glmDelegationSkill } from "../integrations/skill.js";
10
+ import { version } from "../core/version.js";
11
+ function renderEnvironment(results) {
12
+ const lines = [];
13
+ for (const result of results) {
14
+ const symbol = result.status === "ok" ? "✓" : result.status === "warn" ? "⚠" : "✗";
15
+ lines.push(`${symbol} ${result.name}`);
16
+ }
17
+ return lines.join("\n");
18
+ }
19
+ async function askChoices(existingKey, options) {
20
+ if (options.yes) {
21
+ return { configureKey: !existingKey, claude: true, codex: true, codexSkill: true };
22
+ }
23
+ if (!process.stdin.isTTY) {
24
+ process.stdout.write("Non-interactive terminal detected. Re-run with --yes to accept defaults.\n");
25
+ process.exit(2);
26
+ }
27
+ const response = await prompts([
28
+ {
29
+ type: existingKey ? null : "confirm",
30
+ name: "configureKey",
31
+ message: "Configure Z.ai Coding Plan key?",
32
+ initial: true,
33
+ },
34
+ { type: "confirm", name: "claude", message: "Install Claude integration?", initial: true },
35
+ { type: "confirm", name: "codex", message: "Install Codex integration?", initial: true },
36
+ { type: "confirm", name: "codexSkill", message: "Install Codex delegation skill?", initial: true },
37
+ ]);
38
+ if (response.claude === undefined) {
39
+ process.stdout.write("Cancelled.\n");
40
+ process.exit(1);
41
+ }
42
+ return {
43
+ configureKey: existingKey ? true : Boolean(response.configureKey),
44
+ claude: Boolean(response.claude),
45
+ codex: Boolean(response.codex),
46
+ codexSkill: Boolean(response.codexSkill),
47
+ };
48
+ }
49
+ /** glm-router init (spec §7): environment report, key setup, config, skill. Idempotent. */
50
+ export async function initCommand(options) {
51
+ process.stdout.write(`GLM Coding Router v${version}\n\n`);
52
+ const report = runDoctorChecks();
53
+ process.stdout.write("Environment\n\n");
54
+ process.stdout.write(renderEnvironment(report.results) + "\n\n");
55
+ const existingKey = report.keySource !== undefined;
56
+ const choices = await askChoices(existingKey, options);
57
+ process.stdout.write("\nInstalling...\n\n");
58
+ const home = os.homedir();
59
+ const config = loadConfig();
60
+ const nextConfig = {
61
+ ...config,
62
+ integrations: {
63
+ claude: choices.claude,
64
+ codex: choices.codex,
65
+ codexSkill: choices.codexSkill,
66
+ },
67
+ };
68
+ // Z.ai key
69
+ if (existingKey) {
70
+ process.stdout.write(`✓ ${ZAI_API_KEY_ENV} already configured (${report.keySource})\n`);
71
+ }
72
+ else if (choices.configureKey) {
73
+ if (!isWindows()) {
74
+ process.stdout.write("⚠ Key storage requires Windows in v0.1 — skipped\n");
75
+ }
76
+ else {
77
+ const keyResponse = await prompts({
78
+ type: "password",
79
+ name: "key",
80
+ message: "Enter Z.ai Coding Plan API key:",
81
+ validate: (value) => (value.trim().length > 0 ? true : "Key cannot be empty"),
82
+ });
83
+ if (keyResponse.key === undefined) {
84
+ process.stderr.write("Cancelled.\n");
85
+ return 1;
86
+ }
87
+ setWindowsUserEnv(ZAI_API_KEY_ENV, String(keyResponse.key).trim());
88
+ process.stdout.write(`✓ ${ZAI_API_KEY_ENV} configured\n`);
89
+ }
90
+ }
91
+ else {
92
+ process.stdout.write(`⚠ Skipped — run "glm-router key set" later\n`);
93
+ }
94
+ // Config
95
+ const alreadyConfigured = fs.existsSync(configPath(home));
96
+ saveConfig(nextConfig, home);
97
+ process.stdout.write(alreadyConfigured
98
+ ? `✓ Configuration updated (${configPath(home)})\n`
99
+ : `✓ Configuration created (${configPath(home)})\n`);
100
+ // GLM commands
101
+ process.stdout.write(`✓ GLM commands available: glm-chat, glm-worker, glm-review\n`);
102
+ // Integrations
103
+ process.stdout.write(choices.claude ? "✓ Claude integration ready (CLAUDE.md managed block via: glm-router project init)\n"
104
+ : "⚠ Claude integration disabled\n");
105
+ process.stdout.write(choices.codex ? "✓ Codex integration ready (AGENTS.md managed block via: glm-router project init)\n"
106
+ : "⚠ Codex integration disabled\n");
107
+ // Skill
108
+ if (choices.codexSkill) {
109
+ const skillInstaller = new CodexSkillInstaller(home);
110
+ if (skillInstaller.detect()) {
111
+ skillInstaller.install(glmDelegationSkill());
112
+ process.stdout.write("✓ Codex skill installed\n");
113
+ }
114
+ else {
115
+ process.stdout.write("⚠ Codex home (~/.codex) not detected — skill skipped (optional). AGENTS.md integration is unaffected.\n");
116
+ }
117
+ }
118
+ else {
119
+ process.stdout.write("⚠ Codex skill skipped\n");
120
+ }
121
+ process.stdout.write("\nSetup complete.\n\nRun:\n\n glm-router doctor\n");
122
+ return 0;
123
+ }
@@ -0,0 +1,56 @@
1
+ import prompts from "prompts";
2
+ import { assertWindows } from "../core/platform.js";
3
+ import { Errors } from "../core/errors.js";
4
+ import { logger } from "../core/logging.js";
5
+ import { ZAI_API_KEY_ENV, deleteWindowsUserEnv, resolveZaiApiKey, setWindowsUserEnv, } from "../core/zai-key.js";
6
+ import { emitJson } from "./context.js";
7
+ /** glm-router key set (spec §11): prompt, save to Windows User Environment. */
8
+ export async function keySetCommand(_options) {
9
+ assertWindows();
10
+ const response = await prompts({
11
+ type: "password",
12
+ name: "key",
13
+ message: "Enter Z.ai Coding Plan API key:",
14
+ validate: (value) => (value.trim().length > 0 ? true : "Key cannot be empty"),
15
+ });
16
+ if (response.key === undefined) {
17
+ process.stderr.write("Cancelled.\n");
18
+ return 1;
19
+ }
20
+ const key = String(response.key).trim();
21
+ try {
22
+ setWindowsUserEnv(ZAI_API_KEY_ENV, key);
23
+ }
24
+ catch (error) {
25
+ logger.error(`Failed to write the Windows User Environment: ${error instanceof Error ? error.message : String(error)}`);
26
+ return 1;
27
+ }
28
+ process.stdout.write(`\n✓ Saved to Windows User Environment:\n ${ZAI_API_KEY_ENV}\n`);
29
+ process.stdout.write("\nOpen a NEW terminal (or restart your Orca terminal) so the current process picks it up.\n");
30
+ return 0;
31
+ }
32
+ /** glm-router key check (spec §11): report presence + source; never the value. */
33
+ export function keyCheckCommand(options) {
34
+ const resolved = resolveZaiApiKey();
35
+ if (options.json) {
36
+ emitJson({
37
+ configured: Boolean(resolved),
38
+ source: resolved?.source ?? null,
39
+ });
40
+ return resolved ? 0 : 10;
41
+ }
42
+ if (!resolved) {
43
+ // Formatted per spec §36; exit 10 (spec §35).
44
+ const error = Errors.zaiKeyMissing();
45
+ process.stderr.write(`ERROR [${error.codeName}]\n\n${error.message}\n\nRun:\n\n glm-router key set\n`);
46
+ return error.exitCode;
47
+ }
48
+ const sourceLabel = resolved.source === "process-env" ? "process environment" : "Windows User Environment";
49
+ process.stdout.write(`${ZAI_API_KEY_ENV}: configured\nSource: ${sourceLabel}\n`);
50
+ return 0;
51
+ }
52
+ /** Used by uninstall; keeps the key by default (spec §44). */
53
+ export async function keyRemoveCommand() {
54
+ assertWindows();
55
+ deleteWindowsUserEnv(ZAI_API_KEY_ENV);
56
+ }
@@ -0,0 +1,61 @@
1
+ import { findProjectRoot } from "../project/project-root.js";
2
+ import { installClaudeIntegration } from "../integrations/claude.js";
3
+ import { installCodexIntegration } from "../integrations/codex.js";
4
+ import { loadConfig } from "../core/config.js";
5
+ /** Lines of the managed block region (markers included), for --dry-run previews (spec §45). */
6
+ function managedLines(content) {
7
+ const start = content.indexOf("<!-- glm-coding-router:start -->");
8
+ if (start === -1)
9
+ return [];
10
+ const endMarker = "<!-- glm-coding-router:end -->";
11
+ const end = content.indexOf(endMarker);
12
+ const regionEnd = end === -1 ? content.length : end + endMarker.length;
13
+ return content.slice(start, regionEnd).split(/\r?\n/);
14
+ }
15
+ /** Unified-diff-style preview for --dry-run (spec §45). */
16
+ export function renderChangeDiff(change) {
17
+ const lines = [];
18
+ lines.push(`--- ${change.file}${change.created ? " (new file)" : ""}`);
19
+ for (const line of managedLines(change.oldContent)) {
20
+ lines.push(`- ${line}`);
21
+ }
22
+ for (const line of managedLines(change.newContent)) {
23
+ lines.push(`+ ${line}`);
24
+ }
25
+ return lines.join("\n");
26
+ }
27
+ /**
28
+ * glm-router project init (spec §19, §23): create/update the managed block in
29
+ * CLAUDE.md and AGENTS.md at the project root. Idempotent; never overwrites
30
+ * user content; supports --dry-run.
31
+ */
32
+ export function projectInitCommand(options) {
33
+ const root = findProjectRoot();
34
+ const config = loadConfig();
35
+ const changes = [];
36
+ if (config.integrations.claude) {
37
+ changes.push(installClaudeIntegration(root, { dryRun: options.dryRun }));
38
+ }
39
+ if (config.integrations.codex) {
40
+ changes.push(installCodexIntegration(root, { dryRun: options.dryRun }));
41
+ }
42
+ if (changes.length === 0) {
43
+ process.stdout.write("All integrations are disabled in config — nothing to do.\n");
44
+ return 0;
45
+ }
46
+ for (const change of changes) {
47
+ if (options.dryRun) {
48
+ process.stdout.write(`[dry-run] would update ${change.file}:\n${renderChangeDiff(change)}\n`);
49
+ }
50
+ else if (!change.changed) {
51
+ process.stdout.write(`✓ ${change.file} — already up to date\n`);
52
+ }
53
+ else if (change.created) {
54
+ process.stdout.write(`✓ ${change.file} — created\n`);
55
+ }
56
+ else {
57
+ process.stdout.write(`✓ ${change.file} — managed block updated\n`);
58
+ }
59
+ }
60
+ return 0;
61
+ }
@@ -0,0 +1,36 @@
1
+ import { findProjectRoot } from "../project/project-root.js";
2
+ import { removeClaudeIntegration } from "../integrations/claude.js";
3
+ import { removeCodexIntegration } from "../integrations/codex.js";
4
+ /**
5
+ * glm-router project remove (spec §43): remove only the managed block,
6
+ * preserve all user content, and delete router-created files that become empty.
7
+ */
8
+ export function projectRemoveCommand(options) {
9
+ const root = findProjectRoot();
10
+ const changes = [
11
+ removeClaudeIntegration(root, { dryRun: options.dryRun }),
12
+ removeCodexIntegration(root, { dryRun: options.dryRun }),
13
+ ];
14
+ for (const change of changes) {
15
+ if (options.dryRun) {
16
+ if (change.changed) {
17
+ const action = change.deleted ? "delete (router-created, now empty)" : "remove managed block from";
18
+ process.stdout.write(`[dry-run] would ${action}: ${change.file}\n`);
19
+ }
20
+ else {
21
+ process.stdout.write(`[dry-run] no managed block in ${change.file}\n`);
22
+ }
23
+ continue;
24
+ }
25
+ if (change.deleted) {
26
+ process.stdout.write(`✓ ${change.file} — deleted (was created by glm-coding-router)\n`);
27
+ }
28
+ else if (change.changed) {
29
+ process.stdout.write(`✓ ${change.file} — managed block removed\n`);
30
+ }
31
+ else {
32
+ process.stdout.write(`✓ ${change.file} — no managed block present\n`);
33
+ }
34
+ }
35
+ return 0;
36
+ }
@@ -0,0 +1,43 @@
1
+ import os from "node:os";
2
+ import { CodexSkillInstaller, glmDelegationSkill } from "../integrations/skill.js";
3
+ function installer() {
4
+ return new CodexSkillInstaller(os.homedir());
5
+ }
6
+ /** glm-router skill install (spec §27): optional enhancement; warn+skip when unsupported. */
7
+ export function skillInstallCommand(options) {
8
+ const skillInstaller = installer();
9
+ const location = skillInstaller.detect();
10
+ if (!location) {
11
+ process.stdout.write("⚠ Codex skill directory not detected (~/.codex not found).\n" +
12
+ " Skipping optional skill — AGENTS.md integration keeps working.\n");
13
+ return 0;
14
+ }
15
+ const skill = glmDelegationSkill();
16
+ if (skillInstaller.isInstalled(skill.name) && !options.force) {
17
+ process.stdout.write(`✓ Skill "${skill.name}" already installed\n`);
18
+ return 0;
19
+ }
20
+ if (options.dryRun) {
21
+ process.stdout.write(`[dry-run] would install skill "${skill.name}" to ${location.skillsDir}\n`);
22
+ return 0;
23
+ }
24
+ skillInstaller.install(skill);
25
+ process.stdout.write(`✓ Skill "${skill.name}" installed at ${location.skillsDir}\n`);
26
+ return 0;
27
+ }
28
+ /** glm-router skill remove. */
29
+ export function skillRemoveCommand(options) {
30
+ const skillInstaller = installer();
31
+ const skill = glmDelegationSkill();
32
+ if (!skillInstaller.isInstalled(skill.name)) {
33
+ process.stdout.write(`✓ Skill "${skill.name}" is not installed\n`);
34
+ return 0;
35
+ }
36
+ if (options.dryRun) {
37
+ process.stdout.write(`[dry-run] would remove skill "${skill.name}"\n`);
38
+ return 0;
39
+ }
40
+ skillInstaller.remove(skill.name);
41
+ process.stdout.write(`✓ Skill "${skill.name}" removed\n`);
42
+ return 0;
43
+ }
@@ -0,0 +1,57 @@
1
+ import os from "node:os";
2
+ import { loadConfig } from "../core/config.js";
3
+ import { locateClaude, locateCodex } from "../core/claude.js";
4
+ import { version } from "../core/version.js";
5
+ import { resolveZaiApiKey } from "../core/zai-key.js";
6
+ import { CodexSkillInstaller } from "../integrations/skill.js";
7
+ import { GLM_DELEGATION_SKILL_NAME } from "../templates/glm-delegation-skill.js";
8
+ import { emitJson } from "./context.js";
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();
14
+ const claudeInstalled = (() => {
15
+ try {
16
+ locateClaude(config);
17
+ return true;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ })();
23
+ const codexInstalled = Boolean(locateCodex(config));
24
+ const skillInstaller = new CodexSkillInstaller(home);
25
+ const skillInstalled = skillInstaller.detect() !== null && skillInstaller.isInstalled(GLM_DELEGATION_SKILL_NAME);
26
+ if (options.json) {
27
+ emitJson({
28
+ version,
29
+ zaiKeyConfigured: Boolean(resolved),
30
+ claude: claudeInstalled ? "installed" : "missing",
31
+ codex: codexInstalled ? "installed" : "missing",
32
+ integrations: {
33
+ claude: config.integrations.claude,
34
+ codex: config.integrations.codex,
35
+ codexSkill: config.integrations.codexSkill && skillInstalled,
36
+ },
37
+ models: config.models,
38
+ });
39
+ return 0;
40
+ }
41
+ const lines = [
42
+ `GLM Coding Router v${version}`,
43
+ "",
44
+ `Z.ai key ${resolved ? "configured" : "not configured"}`,
45
+ `Claude ${claudeInstalled ? "installed" : "missing"}`,
46
+ `Codex ${codexInstalled ? "installed" : "missing"}`,
47
+ "",
48
+ `Claude policy ${config.integrations.claude ? "enabled" : "disabled"}`,
49
+ `Codex policy ${config.integrations.codex ? "enabled" : "disabled"}`,
50
+ `Codex skill ${skillInstalled ? "enabled" : "disabled"}`,
51
+ "",
52
+ `Main model ${config.models.main}`,
53
+ `Fast model ${config.models.fast}`,
54
+ ];
55
+ process.stdout.write(lines.join("\n") + "\n");
56
+ return 0;
57
+ }
@@ -0,0 +1,81 @@
1
+ import prompts from "prompts";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import { configDir } from "../core/paths.js";
5
+ import { CodexSkillInstaller, glmDelegationSkill } from "../integrations/skill.js";
6
+ import { keyRemoveCommand } from "./key.js";
7
+ import { removeClaudeIntegration, removeCodexIntegration } from "../integrations/index.js";
8
+ async function askChoices(options) {
9
+ // Defaults per spec §44: keep ZAI_API_KEY; destructive credential removal
10
+ // requires explicit consent, so --yes never flips it.
11
+ const defaults = {
12
+ removeConfig: true,
13
+ removeSkill: true,
14
+ removeProjectIntegration: false,
15
+ removeKey: false,
16
+ };
17
+ if (options.yes || options.force) {
18
+ return options.force ? { ...defaults, removeProjectIntegration: true } : defaults;
19
+ }
20
+ if (!process.stdin.isTTY) {
21
+ process.stdout.write("Non-interactive terminal detected. Re-run with --yes for safe defaults.\n");
22
+ process.exit(2);
23
+ }
24
+ const response = await prompts([
25
+ { type: "confirm", name: "removeConfig", message: "Remove global configuration?", initial: true },
26
+ { type: "confirm", name: "removeSkill", message: "Remove Codex skill?", initial: true },
27
+ { type: "confirm", name: "removeProject", message: "Remove current project integration?", initial: false },
28
+ { type: "confirm", name: "removeKey", message: "Remove ZAI_API_KEY?", initial: false },
29
+ ]);
30
+ if (response.removeConfig === undefined) {
31
+ process.stdout.write("Cancelled.\n");
32
+ process.exit(1);
33
+ }
34
+ return {
35
+ removeConfig: Boolean(response.removeConfig),
36
+ removeSkill: Boolean(response.removeSkill),
37
+ removeProjectIntegration: Boolean(response.removeProject),
38
+ removeKey: Boolean(response.removeKey),
39
+ };
40
+ }
41
+ /** 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();
45
+ if (choices.removeSkill) {
46
+ const skillInstaller = new CodexSkillInstaller(home);
47
+ const skill = glmDelegationSkill();
48
+ if (skillInstaller.isInstalled(skill.name)) {
49
+ skillInstaller.remove(skill.name);
50
+ process.stdout.write("✓ Codex skill removed\n");
51
+ }
52
+ else {
53
+ process.stdout.write("✓ Codex skill not installed\n");
54
+ }
55
+ }
56
+ if (choices.removeProjectIntegration) {
57
+ const root = process.cwd();
58
+ removeClaudeIntegration(root);
59
+ removeCodexIntegration(root);
60
+ process.stdout.write("✓ Project integration removed (CLAUDE.md / AGENTS.md managed blocks)\n");
61
+ }
62
+ if (choices.removeConfig) {
63
+ const dir = configDir(home);
64
+ if (fs.existsSync(dir)) {
65
+ fs.rmSync(dir, { recursive: true, force: true });
66
+ process.stdout.write(`✓ Global configuration removed (${dir})\n`);
67
+ }
68
+ else {
69
+ process.stdout.write("✓ Global configuration not present\n");
70
+ }
71
+ }
72
+ if (choices.removeKey) {
73
+ await keyRemoveCommand();
74
+ process.stdout.write("✓ ZAI_API_KEY removed from Windows User Environment\n");
75
+ }
76
+ else {
77
+ process.stdout.write("✓ ZAI_API_KEY kept\n");
78
+ }
79
+ process.stdout.write("\nUninstall complete. Run `npm uninstall -g glm-coding-router` to remove the binaries.\n");
80
+ return 0;
81
+ }
@@ -0,0 +1,104 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { Errors } from "./errors.js";
5
+ import { isWindows } from "./platform.js";
6
+ function runWhere(name) {
7
+ const finder = isWindows() ? "where.exe" : "which";
8
+ try {
9
+ const output = execFileSync(finder, [name], {
10
+ encoding: "utf8",
11
+ windowsHide: true,
12
+ stdio: ["ignore", "pipe", "ignore"],
13
+ });
14
+ return output
15
+ .split(/\r?\n/)
16
+ .map((line) => line.trim())
17
+ .filter((line) => line.length > 0);
18
+ }
19
+ catch {
20
+ return [];
21
+ }
22
+ }
23
+ /** Manual PATH scan for candidate executables, preferring .exe over shims (spec §33). */
24
+ export function searchPathFor(name, env = process.env) {
25
+ const pathValue = env.PATH ?? "";
26
+ const separators = isWindows() ? [".exe", ".cmd", ".bat", ""] : [""];
27
+ const dirs = pathValue.split(path.delimiter).filter((d) => d.length > 0);
28
+ const candidates = [];
29
+ for (const dir of dirs) {
30
+ for (const ext of separators) {
31
+ const candidate = path.join(dir, name + ext);
32
+ if (isExecutableFile(candidate)) {
33
+ candidates.push(candidate);
34
+ }
35
+ }
36
+ }
37
+ const exe = candidates.find((c) => c.toLowerCase().endsWith(".exe"));
38
+ return exe ?? candidates[0];
39
+ }
40
+ function isExecutableFile(file) {
41
+ try {
42
+ return fs.statSync(file).isFile();
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ }
48
+ /** Prefer native executables; npm .cmd shims are a last resort (spec §33). */
49
+ function preferNative(matches) {
50
+ return matches.find((m) => m.toLowerCase().endsWith(".exe")) ?? matches[0];
51
+ }
52
+ /**
53
+ * Locate claude.exe (spec §33):
54
+ * 1. where.exe claude
55
+ * 2. PATH search through Node
56
+ * 3. config override
57
+ * 4. error
58
+ */
59
+ export function locateClaude(config) {
60
+ const matches = runWhere("claude");
61
+ const fromWhere = preferNative(matches);
62
+ if (fromWhere) {
63
+ return fromWhere;
64
+ }
65
+ const fromPath = searchPathFor("claude");
66
+ if (fromPath) {
67
+ return fromPath;
68
+ }
69
+ const override = config?.claudePath;
70
+ if (override && isExecutableFile(override)) {
71
+ return override;
72
+ }
73
+ const detail = override && !isExecutableFile(override)
74
+ ? `Configured claudePath override does not exist: ${override}`
75
+ : undefined;
76
+ throw Errors.claudeNotFound(detail);
77
+ }
78
+ /**
79
+ * Locate codex (spec §34). Absence is a WARN for doctor, not an error here —
80
+ * the tool must work with a Claude-only setup. `required` callers
81
+ * (glm commands that need codex) get a proper error.
82
+ */
83
+ export function locateCodex(config) {
84
+ const matches = runWhere("codex");
85
+ if (matches.length > 0) {
86
+ return preferNative(matches);
87
+ }
88
+ const fromPath = searchPathFor("codex");
89
+ if (fromPath) {
90
+ return fromPath;
91
+ }
92
+ const override = config?.codexPath;
93
+ if (override && isExecutableFile(override)) {
94
+ return override;
95
+ }
96
+ return undefined;
97
+ }
98
+ export function codexRequired(config) {
99
+ const found = locateCodex(config);
100
+ if (!found) {
101
+ throw Errors.codexNotFound();
102
+ }
103
+ return found;
104
+ }