mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-06 08:13:49 +00:00
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:
1 parent
4eda1b81da
commit
fb9509d9be
7 files changed
+150
-36
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 } : {}),
|
||||
};
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in new issue
Block a user