Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: "22"
registry-url: https://registry.npmjs.org
cache: npm

- name: Install dependencies
Expand Down
2 changes: 2 additions & 0 deletions desktop/src-tauri/binaries/node
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Placeholder for Tauri resource validation.
Release packaging replaces this file with a real Node binary.
2 changes: 2 additions & 0 deletions desktop/src-tauri/binaries/node.exe
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Placeholder for Tauri resource validation.
Release packaging replaces this file with a real Node binary.
20 changes: 15 additions & 5 deletions desktop/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

mod rpc;

use rpc::{RpcState, rpc_kill, rpc_send, rpc_spawn};
use rpc::{rpc_kill, rpc_send, rpc_spawn, RpcState};
use serde::Serialize;
use std::path::Path;

Expand Down Expand Up @@ -83,7 +83,9 @@ fn walk_dir(dir: &Path, depth: u32, max_depth: u32, out: &mut Vec<FileEntry>) {
if name.starts_with('.') || SKIP_DIRS.contains(&name.as_str()) {
continue;
}
let Ok(file_type) = entry.file_type() else { continue };
let Ok(file_type) = entry.file_type() else {
continue;
};
let path = entry.path().to_string_lossy().into_owned();
if file_type.is_dir() {
out.push(FileEntry {
Expand Down Expand Up @@ -129,7 +131,10 @@ fn git_status(root: String) -> Result<Vec<GitStatusEntry>, String> {
return Err(format!("not a directory: {root}"));
}
let mut cmd = Command::new("git");
cmd.arg("status").arg("--porcelain").arg("-z").current_dir(root_path);
cmd.arg("status")
.arg("--porcelain")
.arg("-z")
.current_dir(root_path);
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
Expand Down Expand Up @@ -198,7 +203,9 @@ fn open_in_editor(command: String, path: String, line: Option<u32>) -> Result<()
cmd.arg(&path);
}
}
cmd.stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null());
cmd.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
cmd.spawn().map_err(|e| format!("spawn {trimmed}: {e}"))?;
Ok(())
}
Expand Down Expand Up @@ -245,7 +252,10 @@ fn main() {
let _ = w.center();
}
}
if std::env::var("REASONIX_DEVTOOLS").is_ok() {
if std::env::var("CARBONCODE_DEVTOOLS")
.or_else(|_| std::env::var("REASONIX_DEVTOOLS"))
.is_ok()
{
#[cfg(debug_assertions)]
w.open_devtools();
}
Expand Down
32 changes: 23 additions & 9 deletions desktop/src-tauri/src/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};

use anyhow::{Context, Result, anyhow};
use anyhow::{anyhow, Context, Result};
use parking_lot::Mutex;
use serde::Serialize;
use tauri::{AppHandle, Emitter, State};
#[cfg(not(debug_assertions))]
use tauri::Manager;
use tauri::{AppHandle, Emitter, State};
use which::which_all;

#[derive(Default)]
Expand Down Expand Up @@ -40,12 +40,17 @@ struct ExitEvent {
code: Option<i32>,
}

fn cli_override() -> Option<(&'static str, String)> {
env::var("CARBONCODE_CLI")
.map(|value| ("CARBONCODE_CLI", value))
.or_else(|_| env::var("REASONIX_CLI").map(|value| ("REASONIX_CLI", value)))
.ok()
}

fn resolve_cli(app: &AppHandle) -> Result<(String, Vec<String>)> {
if let Ok(custom) = env::var("REASONIX_CLI") {
if let Some((env_name, custom)) = cli_override() {
let mut parts = custom.split_whitespace().map(String::from);
let program = parts
.next()
.ok_or_else(|| anyhow!("REASONIX_CLI is empty"))?;
let program = parts.next().ok_or_else(|| anyhow!("{env_name} is empty"))?;
return Ok((program, parts.collect()));
}

Expand All @@ -69,7 +74,10 @@ fn resolve_cli(app: &AppHandle) -> Result<(String, Vec<String>)> {
if is_real_node && cli_path.exists() {
return Ok((
node_path.to_string_lossy().into_owned(),
vec![cli_path.to_string_lossy().into_owned(), "desktop".to_string()],
vec![
cli_path.to_string_lossy().into_owned(),
"desktop".to_string(),
],
));
}
}
Expand Down Expand Up @@ -122,7 +130,11 @@ fn find_real_node() -> Result<PathBuf> {
"{} ({} bytes{})",
p.display(),
size,
if is_ms_store_shim { ", MS Store shim" } else { "" },
if is_ms_store_shim {
", MS Store shim"
} else {
""
},
));
}
}
Expand Down Expand Up @@ -259,7 +271,9 @@ fn kill_process_tree(pid: u32) {
#[tauri::command]
pub fn rpc_kill(state: State<'_, RpcState>) -> Result<(), String> {
let handle_opt = state.inner.lock().take();
let Some(handle) = handle_opt else { return Ok(()) };
let Some(handle) = handle_opt else {
return Ok(());
};

drop(handle.stdin);

Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@carboncode/cli",
"version": "0.1.2",
"version": "0.1.5",
"description": "Chinese-first DeepSeek-powered terminal coding agent for personal developer workflows.",
"type": "module",
"bin": {
Expand Down
22 changes: 14 additions & 8 deletions src/server/api/health.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { existsSync, readdirSync, statSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { listSessions } from "../../memory/session.js";
import { VERSION } from "../../version.js";
import type { DashboardContext } from "../context.js";
import { type DashboardContext, resolveCarboncodeHome } from "../context.js";
import type { ApiResult } from "../router.js";

interface DirStat {
Expand Down Expand Up @@ -56,6 +54,17 @@ function dirSize(path: string): DirStat {
return { path, exists: true, fileCount, totalBytes };
}

function countSessionFiles(path: string): number {
if (!existsSync(path)) return 0;
try {
return readdirSync(path).filter(
(file) => file.endsWith(".jsonl") && !file.endsWith(".events.jsonl"),
).length;
} catch {
return 0;
}
}

export async function handleHealth(
method: string,
_rest: string[],
Expand All @@ -65,8 +74,7 @@ export async function handleHealth(
if (method !== "GET") {
return { status: 405, body: { error: "GET only" } };
}
const home = homedir();
const carboncodeHome = join(home, ".carboncode");
const carboncodeHome = resolveCarboncodeHome(ctx.configPath);

const sessionsStat = dirSize(join(carboncodeHome, "sessions"));
const memoryStat = dirSize(join(carboncodeHome, "memory"));
Expand All @@ -81,8 +89,6 @@ export async function handleHealth(
}
}

const sessions = listSessions();

return {
status: 200,
body: {
Expand All @@ -91,7 +97,7 @@ export async function handleHealth(
carboncodeHome,
sessions: {
path: sessionsStat.path,
count: sessions.length,
count: countSessionFiles(sessionsStat.path),
totalBytes: sessionsStat.totalBytes,
},
memory: {
Expand Down
16 changes: 8 additions & 8 deletions src/server/api/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,25 @@ import {
unlinkSync,
writeFileSync,
} from "node:fs";
import { homedir } from "node:os";
import { basename, dirname, join, resolve as resolvePath } from "node:path";
import {
PROJECT_MEMORY_FILE,
findProjectMemoryPath,
resolveProjectMemoryWritePath,
} from "../../memory/project.js";
import type { DashboardContext } from "../context.js";
import { type DashboardContext, resolveCarboncodeHome } from "../context.js";
import type { ApiResult } from "../router.js";

function projectHash(rootDir: string): string {
return createHash("sha1").update(resolvePath(rootDir)).digest("hex").slice(0, 16);
}

function globalMemoryDir(): string {
return join(homedir(), ".carboncode", "memory", "global");
function globalMemoryDir(carboncodeHome: string): string {
return join(carboncodeHome, "memory", "global");
}

function projectMemoryDir(rootDir: string): string {
return join(homedir(), ".carboncode", "memory", projectHash(rootDir));
function projectMemoryDir(carboncodeHome: string, rootDir: string): string {
return join(carboncodeHome, "memory", projectHash(rootDir));
}

interface WriteBody {
Expand Down Expand Up @@ -74,8 +73,9 @@ export async function handleMemory(
ctx: DashboardContext,
): Promise<ApiResult> {
const cwd = ctx.getCurrentCwd?.();
const globalDir = globalMemoryDir();
const projectMemDir = cwd ? projectMemoryDir(cwd) : "";
const carboncodeHome = resolveCarboncodeHome(ctx.configPath);
const globalDir = globalMemoryDir(carboncodeHome);
const projectMemDir = cwd ? projectMemoryDir(carboncodeHome, cwd) : "";

if (method === "GET" && rest.length === 0) {
const existingProjectMemory = cwd ? findProjectMemoryPath(cwd) : null;
Expand Down
9 changes: 9 additions & 0 deletions src/server/context.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
/** Callbacks (not refs) so endpoints read live loop state per request, not a frozen closure. */

import { homedir } from "node:os";
import { basename, dirname, join } from "node:path";
import type { McpServerSummary } from "../cli/ui/slash/types.js";
import type { EditMode } from "../config.js";
import type { CacheFirstLoop } from "../loop.js";
import type { ToolRegistry } from "../tools.js";
import type { JobRegistry } from "../tools/jobs.js";

export function resolveCarboncodeHome(configPath: string): string {
if (!configPath.trim()) return join(homedir(), ".carboncode");
const configDir = dirname(configPath);
if (basename(configDir).toLowerCase() === ".carboncode") return configDir;
return join(configDir, ".carboncode");
}

export interface DashboardContext {
/** Caller resolves via `defaultConfigPath()`; module deliberately avoids `homedir()` so tests can redirect. */
configPath: string;
Expand Down
12 changes: 12 additions & 0 deletions tests/carbon-productization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,18 @@ describe("Carbon broad Reasonix import", () => {
expect(indexHtml).toContain("<title>Carbon Code</title>");
});

test("desktop developer env vars prefer Carbon names with legacy fallback", () => {
const rpc = readFileSync(resolve("desktop/src-tauri/src/rpc.rs"), "utf8");
const main = readFileSync(resolve("desktop/src-tauri/src/main.rs"), "utf8");

expect(rpc).toContain('"CARBONCODE_CLI"');
expect(rpc).toContain('"REASONIX_CLI"');
expect(rpc.indexOf('"CARBONCODE_CLI"')).toBeLessThan(rpc.indexOf('"REASONIX_CLI"'));
expect(main).toContain('"CARBONCODE_DEVTOOLS"');
expect(main).toContain('"REASONIX_DEVTOOLS"');
expect(main.indexOf('"CARBONCODE_DEVTOOLS"')).toBeLessThan(main.indexOf('"REASONIX_DEVTOOLS"'));
});

test("CLI startup and empty-session branding uses Carbon Code", () => {
const bootSplash = readFileSync(resolve("src/cli/ui/BootSplash.tsx"), "utf8");
const welcomeBanner = readFileSync(resolve("src/cli/ui/WelcomeBanner.tsx"), "utf8");
Expand Down
8 changes: 8 additions & 0 deletions tests/cli-bare-routing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { writeConfig } from "../src/config.js";

const HEAP_REEXEC_ENV = "REASONIX_HEAP_REEXEC";
const codeCommand = vi.fn(async () => {});
const chatCommand = vi.fn(async () => {});
const setupCommand = vi.fn(async () => {});
Expand All @@ -25,6 +26,7 @@ describe("bare CLI routing", () => {
let cwd: string;
const origHome = process.env.HOME;
const origUserProfile = process.env.USERPROFILE;
const origHeapReexec = process.env[HEAP_REEXEC_ENV];
const origArgv = process.argv;
const origCwd = process.cwd();
let stderr: ReturnType<typeof vi.spyOn>;
Expand All @@ -38,6 +40,7 @@ describe("bare CLI routing", () => {
cwd = realpathSync(mkdtempSync(join(tmpdir(), "carboncode-cli-cwd-")));
process.env.HOME = home;
process.env.USERPROFILE = home;
process.env[HEAP_REEXEC_ENV] = "1";
process.chdir(cwd);
codeCommand.mockClear();
chatCommand.mockClear();
Expand All @@ -63,6 +66,11 @@ describe("bare CLI routing", () => {
} else {
process.env.USERPROFILE = origUserProfile;
}
if (origHeapReexec === undefined) {
delete process.env[HEAP_REEXEC_ENV];
} else {
process.env[HEAP_REEXEC_ENV] = origHeapReexec;
}
});

it("routes bare carboncode to code mode rooted at cwd", async () => {
Expand Down
1 change: 1 addition & 0 deletions tests/helpers/codex-parity-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ function runCommand(
const child = spawn(command, [...args], {
cwd,
env: { ...process.env, CI: "1" },
shell: process.platform === "win32",
stdio: ["ignore", "pipe", "pipe"],
});
const chunks: Buffer[] = [];
Expand Down