fix: make CCS Bar launch descriptor use private shim

Use a private safe ccs.js shim for native CCS Bar launch descriptors so symlinked Bun installs do not fall back to an app environment without node on PATH.
This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-06-23 12:57:33 -04:00
1 parent 4eda1b81da
commit fb9509d9be
7 files changed
+150 -36

No files matched your search

+13 -13
View File
@@ -99,19 +99,19 @@
"src/codex-auth/commands/import-default-command.ts:134",
"src/codex-auth/commands/import-default-command.ts:146",
"src/codex-auth/commands/import-default-command.ts:167",
"src/commands/bar/install-subcommand.ts:137",
"src/commands/bar/install-subcommand.ts:141",
"src/commands/bar/install-subcommand.ts:149",
"src/commands/bar/install-subcommand.ts:172",
"src/commands/bar/install-subcommand.ts:181",
"src/commands/bar/install-subcommand.ts:187",
"src/commands/bar/install-subcommand.ts:228",
"src/commands/bar/install-subcommand.ts:249",
"src/commands/bar/install-subcommand.ts:259",
"src/commands/bar/install-subcommand.ts:271",
"src/commands/bar/install-subcommand.ts:289",
"src/commands/bar/install-subcommand.ts:316",
"src/commands/bar/launch-subcommand.ts:212",
"src/commands/bar/install-subcommand.ts:138",
"src/commands/bar/install-subcommand.ts:142",
"src/commands/bar/install-subcommand.ts:150",
"src/commands/bar/install-subcommand.ts:173",
"src/commands/bar/install-subcommand.ts:182",
"src/commands/bar/install-subcommand.ts:188",
"src/commands/bar/install-subcommand.ts:229",
"src/commands/bar/install-subcommand.ts:250",
"src/commands/bar/install-subcommand.ts:260",
"src/commands/bar/install-subcommand.ts:272",
"src/commands/bar/install-subcommand.ts:290",
"src/commands/bar/install-subcommand.ts:317",
"src/commands/bar/launch-subcommand.ts:207",
"src/commands/config-channels-command.ts:431",
"src/commands/config-channels-command.ts:436",
"src/commands/config-channels-command.ts:447",
+1 -1
View File
@@ -41,7 +41,7 @@ export interface LaunchJson {
schema: typeof LAUNCH_JSON_SCHEMA;
/** Absolute path to the node/bun binary (process.execPath). */
runtime: string;
/** Absolute CCS entry point + subcommand args: [process.argv[1], 'bar', 'serve']. */
/** Absolute private CCS launcher shim + subcommand args: [ccs.js, 'bar', 'serve']. */
args: string[];
/** os.homedir() — cwd for the spawned server. */
home: string;
+3 -8
View File
@@ -20,8 +20,9 @@ import * as os from 'os';
import * as path from 'path';
import { getCcsDir } from '../../config/config-loader-facade';
import { hasAnyFlag } from '../arg-extractor';
import { getLaunchJsonPath, LAUNCH_JSON_SCHEMA } from './bar-paths';
import { getLaunchJsonPath } from './bar-paths';
import type { LaunchJson } from './bar-paths';
import { createBarLaunchDescriptor } from './launch-descriptor';
// ---------------------------------------------------------------------------
// Constants
@@ -671,13 +672,7 @@ export async function handleBarInstall(
// Non-fatal — install has already succeeded at this point.
try {
const launchJsonPath = getLaunchJsonPath(ccsDir);
const launchDescriptor: LaunchJson = {
schema: LAUNCH_JSON_SCHEMA,
runtime: process.execPath,
args: [process.argv[1], 'bar', 'serve'],
home: os.homedir(),
...(process.env.CCS_HOME ? { ccsHome: process.env.CCS_HOME } : {}),
};
const launchDescriptor = createBarLaunchDescriptor();
writeLaunchDescriptor(launchJsonPath, launchDescriptor);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
+68
View File
@@ -0,0 +1,68 @@
/**
* Safe launch descriptor builder for the native CCS Bar app.
*
* The Swift app intentionally distrusts `~/.ccs/bar/launch.json`; it only
* accepts a regular, non-group-writable/non-world-writable `ccs.js` entrypoint.
* Bun global installs expose `~/.bun/bin/ccs` as a symlink and the target file
* can be group/world writable, so the descriptor points at a private shim
* instead of the package-manager entrypoint.
*/
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { ConfigError } from '../../errors/error-types';
import { LAUNCH_JSON_SCHEMA } from './bar-paths';
import type { LaunchJson } from './bar-paths';
const SHIM_MODE = 0o700;
export interface LaunchDescriptorOptions {
entrypointPath?: string;
runtime?: string;
home?: string;
ccsHome?: string;
}
export function getLaunchShimPath(home: string = os.homedir()): string {
return path.join(home, 'Library', 'Application Support', 'CCS Bar', 'launcher', 'ccs.js');
}
function resolveEntrypoint(entrypointPath?: string): string {
const candidate = entrypointPath ?? process.argv[1];
if (!candidate) {
throw new ConfigError('Unable to resolve the current CCS entrypoint for CCS Bar launch.json.');
}
return fs.realpathSync(candidate);
}
export function writeLaunchShim(home: string, entrypointPath?: string): string {
const resolvedEntrypoint = resolveEntrypoint(entrypointPath);
const shimPath = getLaunchShimPath(home);
const shimDir = path.dirname(shimPath);
const contents = [
'#!/usr/bin/env node',
`require(${JSON.stringify(resolvedEntrypoint)});`,
'',
].join('\n');
fs.mkdirSync(shimDir, { recursive: true, mode: SHIM_MODE });
fs.writeFileSync(shimPath, contents, { mode: SHIM_MODE });
fs.chmodSync(shimDir, SHIM_MODE);
fs.chmodSync(shimPath, SHIM_MODE);
return shimPath;
}
export function createBarLaunchDescriptor(options: LaunchDescriptorOptions = {}): LaunchJson {
const home = options.home ?? os.homedir();
const entrypoint = writeLaunchShim(home, options.entrypointPath);
const ccsHome = options.ccsHome ?? process.env.CCS_HOME;
return {
schema: LAUNCH_JSON_SCHEMA,
runtime: options.runtime ?? process.execPath,
args: [entrypoint, 'bar', 'serve'],
home,
...(ccsHome ? { ccsHome } : {}),
};
}
+3 -14
View File
@@ -22,14 +22,9 @@ import * as path from 'path';
import type { ChildProcess } from 'child_process';
import { getCcsDir } from '../../config/config-loader-facade';
import { BAR_AUTH_TOKEN_HEADER, getOrCreateBarAuthToken } from '../../utils/bar-auth-token';
import {
getBarDir,
getBarJsonPath,
getLaunchJsonPath,
getServeLogPath,
LAUNCH_JSON_SCHEMA,
} from './bar-paths';
import { getBarDir, getBarJsonPath, getLaunchJsonPath, getServeLogPath } from './bar-paths';
import type { LaunchJson } from './bar-paths';
import { createBarLaunchDescriptor } from './launch-descriptor';
import {
defaultFindRunningServer as _defaultFindRunningServer,
resolveBarPort as _resolveBarPort,
@@ -305,14 +300,8 @@ export async function handleBarLaunch(
}
// 2b. Write/refresh launch.json so the Swift app can self-start next time.
const launchDescriptor: LaunchJson = {
schema: LAUNCH_JSON_SCHEMA,
runtime: process.execPath,
args: [process.argv[1], 'bar', 'serve'],
home: os.homedir(),
...(process.env.CCS_HOME ? { ccsHome: process.env.CCS_HOME } : {}),
};
try {
const launchDescriptor = createBarLaunchDescriptor();
writeLaunchDescriptor(launchJsonPath, launchDescriptor);
} catch (err) {
// Non-fatal — the Swift app falls back to resolving `ccs` via PATH.
+3
View File
@@ -99,6 +99,7 @@ beforeEach(() => {
afterEach(() => {
restoreConsole();
mock.restore();
process.exitCode = 0;
if (originalCcsHome === undefined) {
delete process.env.CCS_HOME;
@@ -197,6 +198,8 @@ describe('bar command dispatcher (index.ts)', () => {
const handleBarCommand = await loadHandleBarCommand();
// Should print help or error but not crash
await expect(handleBarCommand(['unknown-subcommand'])).resolves.toBeUndefined();
expect(process.exitCode).toBe(1);
process.exitCode = 0;
});
it('dispatches `ccs bar --help` to help subcommand and does not launch', async () => {
@@ -98,6 +98,11 @@ async function loadInstallSubcommand() {
};
}
async function loadLaunchDescriptor() {
moduleSeq++;
return import(`../../../src/commands/bar/launch-descriptor?test=${Date.now()}-${moduleSeq}`);
}
// ---------------------------------------------------------------------------
// Setup / teardown
// ---------------------------------------------------------------------------
@@ -718,6 +723,8 @@ describe('launch: detached-spawn model', () => {
};
expect(desc.schema).toBe(1);
expect(desc.runtime).toBe(process.execPath);
expect(path.basename(desc.args[0])).toBe('ccs.js');
expect(desc.args[0]).not.toContain(`${path.sep}.ccs${path.sep}`);
expect(desc.args).toContain('bar');
expect(desc.args).toContain('serve');
expect(desc.home).toBe(os.homedir());
@@ -754,6 +761,56 @@ describe('launch: detached-spawn model', () => {
});
});
// ---------------------------------------------------------------------------
// launch-descriptor: safe shim for native app self-start
// ---------------------------------------------------------------------------
describe('launch descriptor shim', () => {
it('creates a private ccs.js shim for symlinked Bun-style entrypoints', async () => {
const ccsDir = path.join(tempHome, '.ccs');
const packageDist = path.join(
tempHome,
'.bun',
'install',
'global',
'node_modules',
'@kaitranntt',
'ccs',
'dist'
);
const binDir = path.join(tempHome, '.bun', 'bin');
const realEntrypoint = path.join(packageDist, 'ccs.js');
const symlinkedEntrypoint = path.join(binDir, 'ccs');
fs.mkdirSync(packageDist, { recursive: true });
fs.mkdirSync(binDir, { recursive: true });
fs.writeFileSync(realEntrypoint, 'console.log("ccs");\n', { mode: 0o777 });
fs.symlinkSync(realEntrypoint, symlinkedEntrypoint);
const { createBarLaunchDescriptor, getLaunchShimPath } = await loadLaunchDescriptor();
const descriptor = createBarLaunchDescriptor({
entrypointPath: symlinkedEntrypoint,
runtime: '/usr/local/bin/node',
home: tempHome,
ccsHome: ccsDir,
});
const shimPath = getLaunchShimPath(tempHome);
expect(descriptor.runtime).toBe('/usr/local/bin/node');
expect(descriptor.args).toEqual([shimPath, 'bar', 'serve']);
expect(path.basename(descriptor.args[0])).toBe('ccs.js');
expect(descriptor.args[0]).not.toContain(`${path.sep}.ccs${path.sep}`);
expect(fs.lstatSync(descriptor.args[0]).isSymbolicLink()).toBe(false);
const mode = fs.statSync(descriptor.args[0]).mode & 0o777;
expect((mode & 0o022) === 0).toBe(true);
const resolvedEntrypoint = fs.realpathSync(realEntrypoint);
expect(fs.readFileSync(descriptor.args[0], 'utf8')).toContain(
`require(${JSON.stringify(resolvedEntrypoint)});`
);
});
});
// ---------------------------------------------------------------------------
// install-subcommand: writeLaunchDescriptor after successful install
// ---------------------------------------------------------------------------
@@ -800,6 +857,8 @@ describe('install: writeLaunchDescriptor called after successful install', () =>
};
expect(desc.schema).toBe(1);
expect(desc.runtime).toBe(process.execPath);
expect(path.basename(desc.args[0])).toBe('ccs.js');
expect(desc.args[0]).not.toContain(`${path.sep}.ccs${path.sep}`);
expect(desc.args).toContain('bar');
expect(desc.args).toContain('serve');
expect(desc.home).toBe(os.homedir());