mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-11 22:50:06 +00:00
chore: Connect the Claude Code desktop app to Codespaces (no-changelog) (#39524)
Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3220953fb4
commit
529761ff03
@@ -0,0 +1,46 @@
|
||||
// Writes an OpenSSH host entry for the session Codespace, so tools that use the
|
||||
// system ssh (the Claude Code desktop app, VS Code Remote-SSH, plain `ssh`) can
|
||||
// connect by a fixed name.
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
export const HOST_ALIAS = 'n8n-codespace';
|
||||
const HOST_FILE = `${HOST_ALIAS}.conf`;
|
||||
const INCLUDE_LINE = `Include ~/.ssh/${HOST_FILE}`;
|
||||
|
||||
/**
|
||||
* Replaces the host name in `gh codespace ssh --config` output with a fixed alias.
|
||||
* gh names the host `cs.<codespace>.<branch>`, so its name changes when the
|
||||
* checkout changes branch. A saved connection must not depend on that.
|
||||
*/
|
||||
export function aliasHostConfig(ghConfig, alias = HOST_ALIAS) {
|
||||
const hosts = ghConfig.match(/^Host .*$/gm) ?? [];
|
||||
if (hosts.length !== 1 || !/^\s*ProxyCommand /m.test(ghConfig)) {
|
||||
throw new Error(`Expected one host entry from gh codespace ssh --config, got ${hosts.length}`);
|
||||
}
|
||||
return ghConfig.replace(/^Host .*$/m, `Host ${alias}`).trimEnd() + '\n';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ssh config with the Include line at the top, or undefined if the line
|
||||
* is already there. An Include after a Host block applies only to that block.
|
||||
*/
|
||||
export function withInclude(sshConfig, includeLine = INCLUDE_LINE) {
|
||||
if (sshConfig.split('\n').some((line) => line.trim() === includeLine)) return undefined;
|
||||
return `${includeLine}\n${sshConfig.length ? '\n' : ''}${sshConfig}`;
|
||||
}
|
||||
|
||||
export function writeSshConfig(ghConfig, sshDir = join(homedir(), '.ssh')) {
|
||||
mkdirSync(sshDir, { recursive: true, mode: 0o700 });
|
||||
|
||||
const hostPath = join(sshDir, HOST_FILE);
|
||||
writeFileSync(hostPath, aliasHostConfig(ghConfig), { mode: 0o600 });
|
||||
|
||||
const configPath = join(sshDir, 'config');
|
||||
const current = existsSync(configPath) ? readFileSync(configPath, 'utf8') : '';
|
||||
const updated = withInclude(current);
|
||||
// The mode applies only when the file is new. An existing file keeps its mode.
|
||||
if (updated !== undefined) writeFileSync(configPath, updated, { mode: 0o600 });
|
||||
return { hostPath, configPath, includeAdded: updated !== undefined };
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
// pnpm session:opencode [name] connect a local OpenCode client
|
||||
// pnpm session ls list Codespaces and tmux sessions
|
||||
// pnpm session tunnel [port…] forward ports to localhost
|
||||
// pnpm session ssh-config add an `n8n-codespace` host to ~/.ssh/config
|
||||
// pnpm session stop stop the Codespace
|
||||
// pnpm session rm delete the Codespace
|
||||
import { execFileSync, spawnSync } from 'node:child_process';
|
||||
@@ -21,7 +22,10 @@ const DEVCONTAINER = '.devcontainer/codespaces/devcontainer.json';
|
||||
const MACHINE = 'premiumLinux';
|
||||
|
||||
const gh = (...args) => execFileSync('gh', args, { encoding: 'utf8' }).trim();
|
||||
const ghTty = (...args) => spawnSync('gh', args, { stdio: 'inherit' });
|
||||
// On a TTY, gh queries the terminal background colour. The reply can reach the
|
||||
// shell (or the remote session) as text. A fixed GLAMOUR_STYLE skips the query.
|
||||
const ghTty = (...args) =>
|
||||
spawnSync('gh', args, { stdio: 'inherit', env: { ...process.env, GLAMOUR_STYLE: 'dark' } });
|
||||
|
||||
// Check the remote terminal database before tmux starts. Older images can lack
|
||||
// terminal definitions such as xterm-ghostty.
|
||||
@@ -216,6 +220,25 @@ switch (cmd) {
|
||||
process.exitCode = status ?? 1;
|
||||
break;
|
||||
}
|
||||
case 'ssh-config': {
|
||||
const name = ensureCodespace();
|
||||
// A first connection starts a stopped codespace and creates gh's automatic
|
||||
// key pair. The generated config names that key.
|
||||
const { status } = ghTty('codespace', 'ssh', '-c', name, '--', 'true');
|
||||
if (status !== 0) {
|
||||
console.error(`Could not connect to ${name} over ssh.`);
|
||||
process.exit(status ?? 1);
|
||||
}
|
||||
const { writeSshConfig, HOST_ALIAS } = await import('./cloud-session-ssh-config.mjs');
|
||||
const { hostPath, configPath, includeAdded } = writeSshConfig(
|
||||
gh('codespace', 'ssh', '--config', '-c', name),
|
||||
);
|
||||
console.log(`Wrote ${hostPath}${includeAdded ? ` and included it from ${configPath}` : ''}.`);
|
||||
console.log(`Connect with \`ssh ${HOST_ALIAS}\`, or add the SSH host \`${HOST_ALIAS}\` in the`);
|
||||
console.log('Claude Code desktop app and open /workspaces/n8n.');
|
||||
console.log('Run this command again after you recreate the codespace.');
|
||||
break;
|
||||
}
|
||||
case 'stop':
|
||||
case 'rm': {
|
||||
const cs = findCodespace();
|
||||
|
||||
Reference in New Issue
Block a user