hereya-cli 0.95.5 → 0.96.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.
- package/README.md +73 -72
- package/dist/commands/devenv/config/index.d.ts +12 -0
- package/dist/commands/devenv/config/index.js +66 -13
- package/dist/commands/devenv/ssh-proxy/index.d.ts +27 -0
- package/dist/commands/devenv/ssh-proxy/index.js +80 -0
- package/dist/commands/workspace/executor/install/index.js +4 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/oclif.manifest.json +277 -244
- package/package.json +1 -1
|
@@ -6,20 +6,43 @@ import { getBackend } from '../../../backend/index.js';
|
|
|
6
6
|
import { getExecutorForWorkspace } from '../../../executor/context.js';
|
|
7
7
|
import { setKeyFilePermissions, setSshDirPermissions } from '../../../lib/ssh-utils.js';
|
|
8
8
|
export function buildSshConfigBlock(options) {
|
|
9
|
-
const { host, hostAlias, keyFilePath, sshUser } = options;
|
|
9
|
+
const { host, hostAlias, keyFilePath, proxyCommand, sshUser } = options;
|
|
10
10
|
const knownHostsFile = process.platform === 'win32' ? 'NUL' : '/dev/null';
|
|
11
11
|
const beginMarker = `# hereya-managed:${hostAlias} BEGIN`;
|
|
12
12
|
const endMarker = `# hereya-managed:${hostAlias} END`;
|
|
13
|
-
|
|
13
|
+
// On-demand devenvs (proxyCommand provided) need a placeholder HostName —
|
|
14
|
+
// ssh requires HostName to start a connection but the actual address is
|
|
15
|
+
// resolved by ProxyCommand at connect time. The alias itself is fine as
|
|
16
|
+
// a placeholder; ProxyCommand ignores %h.
|
|
17
|
+
const lines = [
|
|
14
18
|
beginMarker,
|
|
15
19
|
`Host ${hostAlias}`,
|
|
16
20
|
` HostName ${host}`,
|
|
17
21
|
` User ${sshUser}`,
|
|
18
22
|
` IdentityFile ${keyFilePath}`,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
];
|
|
24
|
+
if (proxyCommand) {
|
|
25
|
+
lines.push(` ProxyCommand ${proxyCommand}`);
|
|
26
|
+
}
|
|
27
|
+
lines.push(` StrictHostKeyChecking no`, ` UserKnownHostsFile ${knownHostsFile}`, endMarker);
|
|
28
|
+
return lines.join('\n');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolve the path to the hereya binary so we can bake an absolute
|
|
32
|
+
* ProxyCommand into the SSH config. SSH invokes ProxyCommand without
|
|
33
|
+
* inheriting the user's interactive shell PATH (it uses /bin/sh and the
|
|
34
|
+
* stripped login env), so a bare `hereya` often won't resolve. Prefer
|
|
35
|
+
* `process.argv[1]` (the actual binary that ran us) — that path is what's
|
|
36
|
+
* already on the user's machine and just worked. Fall back to `hereya` if
|
|
37
|
+
* argv[1] is missing for any reason; users can fix their PATH or re-run
|
|
38
|
+
* `devenv config` from a binary they want the SSH proxy to use.
|
|
39
|
+
*/
|
|
40
|
+
export function resolveHereyaBinForProxyCommand() {
|
|
41
|
+
const argv1 = process.argv[1];
|
|
42
|
+
if (argv1 && argv1.length > 0) {
|
|
43
|
+
return argv1;
|
|
44
|
+
}
|
|
45
|
+
return 'hereya';
|
|
23
46
|
}
|
|
24
47
|
export function upsertSshConfigBlock(existingConfig, newBlock, hostAlias) {
|
|
25
48
|
const beginMarker = `# hereya-managed:${hostAlias} BEGIN`;
|
|
@@ -37,7 +60,7 @@ export function upsertSshConfigBlock(existingConfig, newBlock, hostAlias) {
|
|
|
37
60
|
return existingConfig.trimEnd() + '\n\n' + newBlock + '\n';
|
|
38
61
|
}
|
|
39
62
|
export default class DevenvConfig extends Command {
|
|
40
|
-
static description = 'Configure SSH for a dev environment and display connection details.';
|
|
63
|
+
static description = 'Configure SSH for a dev environment and display connection details. On-demand devenvs use a ProxyCommand that wakes the instance on first connection.';
|
|
41
64
|
static examples = [
|
|
42
65
|
'<%= config.bin %> <%= command.id %> -w my-workspace',
|
|
43
66
|
];
|
|
@@ -67,10 +90,15 @@ export default class DevenvConfig extends Command {
|
|
|
67
90
|
const sshPrivateKey = env.devEnvSshPrivateKey;
|
|
68
91
|
const sshUser = env.devEnvSshUser;
|
|
69
92
|
const sshHostDns = env.devEnvSshHostDns;
|
|
70
|
-
|
|
71
|
-
|
|
93
|
+
const wakeUrl = env.devEnvWakeUrl;
|
|
94
|
+
if (!sshPrivateKey || !sshUser) {
|
|
95
|
+
this.error('devEnvSshPrivateKey and devEnvSshUser must be set in the workspace environment');
|
|
96
|
+
}
|
|
97
|
+
// Always-on devenvs need a cached host; on-demand devenvs resolve via
|
|
98
|
+
// the broker so devEnvSshHost is just a hint (and is often stale).
|
|
99
|
+
if (!wakeUrl && !sshHost) {
|
|
100
|
+
this.error('devEnvSshHost must be set in the workspace environment for always-on devenvs');
|
|
72
101
|
}
|
|
73
|
-
const host = sshHostDns || sshHost;
|
|
74
102
|
const sshDir = path.join(os.homedir(), '.ssh');
|
|
75
103
|
const sanitizedWorkspace = workspace.replaceAll('/', '-');
|
|
76
104
|
const keyFilePath = path.join(sshDir, `hereya-devenv-${sanitizedWorkspace}`);
|
|
@@ -87,15 +115,40 @@ export default class DevenvConfig extends Command {
|
|
|
87
115
|
catch {
|
|
88
116
|
// File doesn't exist yet, that's fine
|
|
89
117
|
}
|
|
90
|
-
|
|
91
|
-
|
|
118
|
+
// For on-demand devenvs, emit a ProxyCommand that calls back into this
|
|
119
|
+
// CLI to wake the box and resolve the live IP every connection. The
|
|
120
|
+
// cached devEnvSshHost would otherwise drift on every stop/start (EC2
|
|
121
|
+
// reassigns public IPs). The HostName field still has to be present
|
|
122
|
+
// for ssh to even try the connection — use the alias as a placeholder
|
|
123
|
+
// since ProxyCommand ignores %h.
|
|
124
|
+
const isOnDemand = Boolean(wakeUrl);
|
|
125
|
+
const block = isOnDemand
|
|
126
|
+
? buildSshConfigBlock({
|
|
127
|
+
host: hostAlias,
|
|
128
|
+
hostAlias,
|
|
129
|
+
keyFilePath,
|
|
130
|
+
proxyCommand: `${resolveHereyaBinForProxyCommand()} devenv ssh-proxy -w ${workspace}`,
|
|
131
|
+
sshUser,
|
|
132
|
+
})
|
|
133
|
+
: buildSshConfigBlock({
|
|
134
|
+
host: sshHostDns || sshHost,
|
|
135
|
+
hostAlias,
|
|
136
|
+
keyFilePath,
|
|
137
|
+
sshUser,
|
|
138
|
+
});
|
|
139
|
+
const updatedConfig = upsertSshConfigBlock(existingConfig, block, hostAlias);
|
|
92
140
|
await fs.writeFile(sshConfigPath, updatedConfig, { mode: 0o600 });
|
|
93
141
|
this.log('SSH connection configured successfully.');
|
|
94
142
|
this.log('');
|
|
95
143
|
this.log('Fill in the Claude Code SSH connection form with:');
|
|
96
144
|
this.log('');
|
|
97
145
|
this.log(` Name: ${workspace}`);
|
|
98
|
-
|
|
146
|
+
if (isOnDemand) {
|
|
147
|
+
this.log(` SSH Host: ${hostAlias} (on-demand; wake handled automatically via ProxyCommand)`);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
this.log(` SSH Host: ${sshUser}@${sshHostDns || sshHost}`);
|
|
151
|
+
}
|
|
99
152
|
this.log(` SSH Port: 22`);
|
|
100
153
|
this.log(` Identity File: ${keyFilePath}`);
|
|
101
154
|
this.log('');
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
/**
|
|
3
|
+
* Hidden command used as `ProxyCommand` from the SSH config block written
|
|
4
|
+
* by `hereya devenv config`. Wakes the devenv (on-demand path) via the
|
|
5
|
+
* broker, resolves the live IP, then pipes stdin/stdout to <host>:22 so
|
|
6
|
+
* any SSH client (plain `ssh`, VS Code Remote SSH, Cursor, etc.) connects
|
|
7
|
+
* through the resolved address transparently.
|
|
8
|
+
*
|
|
9
|
+
* Equivalent to `ssh -W %h:%p` semantics, except %h/%p are resolved here
|
|
10
|
+
* (host comes from the broker, port is fixed at 22). Uses Node's built-in
|
|
11
|
+
* `net` module so we don't depend on an external `nc`/`ncat` being on PATH.
|
|
12
|
+
*/
|
|
13
|
+
export default class DevenvSshProxy extends Command {
|
|
14
|
+
static description: string;
|
|
15
|
+
static flags: {
|
|
16
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
};
|
|
18
|
+
static hidden: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Open a TCP socket to <host>:<port> and pipe between stdin/stdout and
|
|
21
|
+
* the socket — the canonical SSH ProxyCommand contract. Resolves when
|
|
22
|
+
* the SSH client closes (EOF on stdin / socket end), with the appropriate
|
|
23
|
+
* exit code on error.
|
|
24
|
+
*/
|
|
25
|
+
protected pipeToHost(host: string, port: number): Promise<void>;
|
|
26
|
+
run(): Promise<void>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import * as net from 'node:net';
|
|
3
|
+
import { getBackend } from '../../../backend/index.js';
|
|
4
|
+
import { getExecutorForWorkspace } from '../../../executor/context.js';
|
|
5
|
+
import { resolveDevenvSshHost } from '../../../lib/devenv-wake.js';
|
|
6
|
+
/**
|
|
7
|
+
* Hidden command used as `ProxyCommand` from the SSH config block written
|
|
8
|
+
* by `hereya devenv config`. Wakes the devenv (on-demand path) via the
|
|
9
|
+
* broker, resolves the live IP, then pipes stdin/stdout to <host>:22 so
|
|
10
|
+
* any SSH client (plain `ssh`, VS Code Remote SSH, Cursor, etc.) connects
|
|
11
|
+
* through the resolved address transparently.
|
|
12
|
+
*
|
|
13
|
+
* Equivalent to `ssh -W %h:%p` semantics, except %h/%p are resolved here
|
|
14
|
+
* (host comes from the broker, port is fixed at 22). Uses Node's built-in
|
|
15
|
+
* `net` module so we don't depend on an external `nc`/`ncat` being on PATH.
|
|
16
|
+
*/
|
|
17
|
+
export default class DevenvSshProxy extends Command {
|
|
18
|
+
static description = 'Internal: SSH ProxyCommand for on-demand devenvs (wake + connect).';
|
|
19
|
+
static flags = {
|
|
20
|
+
workspace: Flags.string({
|
|
21
|
+
char: 'w',
|
|
22
|
+
description: 'name of the workspace to proxy SSH for',
|
|
23
|
+
required: true,
|
|
24
|
+
}),
|
|
25
|
+
};
|
|
26
|
+
static hidden = true;
|
|
27
|
+
/**
|
|
28
|
+
* Open a TCP socket to <host>:<port> and pipe between stdin/stdout and
|
|
29
|
+
* the socket — the canonical SSH ProxyCommand contract. Resolves when
|
|
30
|
+
* the SSH client closes (EOF on stdin / socket end), with the appropriate
|
|
31
|
+
* exit code on error.
|
|
32
|
+
*/
|
|
33
|
+
pipeToHost(host, port) {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const socket = net.createConnection({ host, port });
|
|
36
|
+
socket.once('error', (err) => {
|
|
37
|
+
process.stderr.write(`hereya devenv ssh-proxy: failed to connect to ${host}:${port}: ${err.message}\n`);
|
|
38
|
+
reject(err);
|
|
39
|
+
});
|
|
40
|
+
socket.once('connect', () => {
|
|
41
|
+
process.stdin.pipe(socket);
|
|
42
|
+
socket.pipe(process.stdout);
|
|
43
|
+
});
|
|
44
|
+
socket.once('end', () => {
|
|
45
|
+
resolve();
|
|
46
|
+
});
|
|
47
|
+
socket.once('close', () => {
|
|
48
|
+
resolve();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
async run() {
|
|
53
|
+
const { flags } = await this.parse(DevenvSshProxy);
|
|
54
|
+
const { workspace } = flags;
|
|
55
|
+
const backend = await getBackend();
|
|
56
|
+
const getWorkspaceEnvOutput = await backend.getWorkspaceEnv({ workspace });
|
|
57
|
+
if (!getWorkspaceEnvOutput.success) {
|
|
58
|
+
this.error(getWorkspaceEnvOutput.reason);
|
|
59
|
+
}
|
|
60
|
+
let { env } = getWorkspaceEnvOutput;
|
|
61
|
+
const executor$ = await getExecutorForWorkspace(workspace);
|
|
62
|
+
if (!executor$.success) {
|
|
63
|
+
this.error(executor$.reason);
|
|
64
|
+
}
|
|
65
|
+
const { executor } = executor$;
|
|
66
|
+
env = await executor.resolveEnvValues({ env });
|
|
67
|
+
let host;
|
|
68
|
+
try {
|
|
69
|
+
host = await resolveDevenvSshHost(env);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
73
|
+
// Print to stderr so SSH surfaces it, then exit non-zero so SSH
|
|
74
|
+
// reports a clean failure to the user.
|
|
75
|
+
process.stderr.write(`hereya devenv ssh-proxy: ${message}\n`);
|
|
76
|
+
this.exit(1);
|
|
77
|
+
}
|
|
78
|
+
await this.pipeToHost(host, 22);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -10,14 +10,14 @@ export default class WorkspaceExecutorInstall extends Command {
|
|
|
10
10
|
static description = `Install a remote executor into a workspace.
|
|
11
11
|
|
|
12
12
|
Provisions hereya/remote-executor-aws. Two modes are supported:
|
|
13
|
-
-
|
|
14
|
-
-
|
|
13
|
+
- ephemeral (default): ASG scaled to 0 with a broker Lambda + OIDC for on-demand wake.
|
|
14
|
+
- always-on: a long-lived EC2 polls hereya-cloud 24/7 (legacy).`;
|
|
15
15
|
static flags = {
|
|
16
16
|
debug: Flags.boolean({ default: false, description: 'enable debug mode' }),
|
|
17
17
|
force: Flags.boolean({ char: 'f', default: false, description: 'force reinstall even if executor is already installed' }),
|
|
18
18
|
mode: Flags.string({
|
|
19
|
-
default: '
|
|
20
|
-
description: 'executor mode:
|
|
19
|
+
default: 'ephemeral',
|
|
20
|
+
description: 'executor mode: ephemeral (ASG min=0/max=1 with broker Lambda) or always-on (legacy long-lived EC2)',
|
|
21
21
|
options: ['ephemeral', 'always-on'],
|
|
22
22
|
}),
|
|
23
23
|
parameter: Flags.string({
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { type GetInfrastructureFn, type GetWorkspaceEnvFn, type MintInstallationTokenFn, type ResolveEnvProviders, resolveEnvValues, type ResolveEnvValuesInput, type ResolveEnvValuesOutput, resolveGithubAppMarkers, type ResolveSimpleEnvFn, } from './executor/resolve-env.js';
|
|
2
2
|
export { InfrastructureType } from './infrastructure/common.js';
|
|
3
3
|
export { awsProviderFactory, getInfrastructure, type GetInfrastructureInput, type GetInfrastructureOutput, type InfrastructureProviderFactory, registerInfrastructureProvider, resetInfrastructureProviders, } from './infrastructure/registry.js';
|
|
4
|
+
export { mintInstallationToken, type MintInstallationTokenInput, } from './lib/github-app.js';
|
|
4
5
|
export { run } from '@oclif/core';
|
package/dist/index.js
CHANGED
|
@@ -5,4 +5,5 @@
|
|
|
5
5
|
export { resolveEnvValues, resolveGithubAppMarkers, } from './executor/resolve-env.js';
|
|
6
6
|
export { InfrastructureType } from './infrastructure/common.js';
|
|
7
7
|
export { awsProviderFactory, getInfrastructure, registerInfrastructureProvider, resetInfrastructureProviders, } from './infrastructure/registry.js';
|
|
8
|
+
export { mintInstallationToken, } from './lib/github-app.js';
|
|
8
9
|
export { run } from '@oclif/core';
|