@@ -36,6 +36,7 @@ import {
3636 serializeEnvClassificationForJson ,
3737 serializeEnvStorageForJson as serializeEnvStorageForJsonShape ,
3838} from "../lib/hack-env-status.ts" ;
39+ import { appendHackHostTrustEnvironment } from "../lib/local-ca.ts" ;
3940import type { ProjectContext } from "../lib/project.ts" ;
4041import {
4142 defaultProjectSlugFromPath ,
@@ -120,6 +121,15 @@ const optTarget = defineOption({
120121 "Env view for host commands (default: host rewrites container-oriented addresses for local host execution)" ,
121122} as const ) ;
122123
124+ const optShellCommand = defineOption ( {
125+ name : "shellCommand" ,
126+ type : "string" ,
127+ long : "--shell" ,
128+ valueHint : "<command>" ,
129+ description :
130+ "Run a shell command string via /bin/sh -lc after env injection so `$VAR` expansion happens inside the child shell" ,
131+ } as const ) ;
132+
123133const SECRET_MASK = "***" ;
124134const MODERN_ENV_STATUS_CLASSIFICATION = {
125135 trust_model : "repo_managed_env_config" ,
@@ -187,9 +197,16 @@ const execSpec = defineCommand({
187197 summary : "Run a host command with project env injected" ,
188198 group : "Project" ,
189199 description :
190- "Inject the selected Hack env overlay directly into a one-off host command without materializing .hack/.env." ,
191- options : [ optPath , optProject , optEnv , optService , optTarget ] ,
192- positionals : [ { name : "command" , required : true , multiple : true } ] ,
200+ 'Inject the selected Hack env overlay directly into a one-off host command without materializing .hack/.env. To inspect a value, prefer `printenv KEY` or `sh -lc \'printf "%s\\n" "$KEY"\'`; `echo $KEY` expands in your current shell before Hack injects env.' ,
201+ options : [
202+ optPath ,
203+ optProject ,
204+ optEnv ,
205+ optService ,
206+ optTarget ,
207+ optShellCommand ,
208+ ] ,
209+ positionals : [ { name : "command" , required : false , multiple : true } ] ,
193210 subcommands : [ ] ,
194211} as const ) ;
195212
@@ -209,9 +226,9 @@ const hostExecSpec = defineCommand({
209226 summary : "Run a host command with project env injected" ,
210227 group : "Project" ,
211228 description :
212- " Run a one-off command on the host with the selected Hack env overlay injected. Use --scope when you want service-scoped values without running inside that service container." ,
213- options : [ optPath , optProject , optEnv , optScope , optTarget ] ,
214- positionals : [ { name : "command" , required : true , multiple : true } ] ,
229+ ' Run a one-off command on the host with the selected Hack env overlay injected. Use --scope when you want service-scoped values without running inside that service container. To inspect a value, prefer `printenv KEY` or `sh -lc \'printf "%s\\n" "$KEY"\'`; `echo $KEY` expands in your current shell before Hack injects env.' ,
230+ options : [ optPath , optProject , optEnv , optScope , optTarget , optShellCommand ] ,
231+ positionals : [ { name : "command" , required : false , multiple : true } ] ,
215232 subcommands : [ ] ,
216233} as const ) ;
217234
@@ -605,7 +622,7 @@ async function resolveEnvInjection(input: {
605622 target : input . target ,
606623 } ) ;
607624 return {
608- env : adaptEnvForHostExecution ( {
625+ env : await adaptEnvForHostExecution ( {
609626 env,
610627 target : input . target ,
611628 serviceNames,
@@ -627,7 +644,7 @@ async function resolveEnvInjection(input: {
627644 composeFile : input . project . composeFile ,
628645 } ) ;
629646 return {
630- env : adaptEnvForHostExecution ( {
647+ env : await adaptEnvForHostExecution ( {
631648 env : selectHackEnvValues ( {
632649 resolved,
633650 serviceName : input . serviceName ,
@@ -659,21 +676,21 @@ function adaptEnvForHostExecution(input: {
659676 readonly env : Readonly < Record < string , string > > ;
660677 readonly target : ( typeof HOST_ENV_TARGET_VALUES ) [ number ] ;
661678 readonly serviceNames : readonly string [ ] ;
662- } ) : Record < string , string > {
663- if ( input . target !== "host" ) {
664- return { ...input . env } ;
679+ } ) : Promise < Record < string , string > > {
680+ if ( input . target === "host" ) {
681+ const composeServiceNames = new Set ( input . serviceNames ) ;
682+ const out : Record < string , string > = { } ;
683+ for ( const [ key , value ] of Object . entries ( input . env ) ) {
684+ out [ key ] = rewriteEnvValueForHostExecution ( {
685+ key,
686+ value,
687+ composeServiceNames,
688+ } ) ;
689+ }
690+ return appendHackHostTrustEnvironment ( out ) ;
665691 }
666692
667- const composeServiceNames = new Set ( input . serviceNames ) ;
668- const out : Record < string , string > = { } ;
669- for ( const [ key , value ] of Object . entries ( input . env ) ) {
670- out [ key ] = rewriteEnvValueForHostExecution ( {
671- key,
672- value,
673- composeServiceNames,
674- } ) ;
675- }
676- return out ;
693+ return appendHackHostTrustEnvironment ( input . env ) ;
677694}
678695
679696function rewriteEnvValueForHostExecution ( input : {
@@ -1564,6 +1581,12 @@ function resolveInteractiveShellCommand(): readonly string[] {
15641581 return [ shellPath , "-l" ] ;
15651582}
15661583
1584+ function resolveShellCommandCommand ( input : {
1585+ readonly command : string ;
1586+ } ) : readonly string [ ] {
1587+ return [ "/bin/sh" , "-lc" , input . command ] ;
1588+ }
1589+
15671590function resolveExecutionScopeName ( input : {
15681591 readonly scopeName ?: string ;
15691592 readonly serviceName ?: string ;
@@ -1585,6 +1608,7 @@ async function runHostCommandWithInjectedEnv(input: {
15851608 readonly serviceName ?: string ;
15861609 readonly targetOpt : string | undefined ;
15871610 readonly command : readonly string [ ] ;
1611+ readonly shellCommandOpt ?: string ;
15881612} ) : Promise < number > {
15891613 const project = await resolveProjectForEnv ( {
15901614 ctx : input . ctx ,
@@ -1598,7 +1622,12 @@ async function runHostCommandWithInjectedEnv(input: {
15981622 const target = resolveHostEnvTarget ( {
15991623 targetOption : input . targetOpt ,
16001624 } ) ;
1601- if ( input . command . length === 0 ) {
1625+ const shellCommand = input . shellCommandOpt ?. trim ( ) ;
1626+ const positionalCommand = input . command ;
1627+ if ( shellCommand && positionalCommand . length > 0 ) {
1628+ throw new CliUsageError ( "Use either <command...> or --shell, not both." ) ;
1629+ }
1630+ if ( ! shellCommand && positionalCommand . length === 0 ) {
16021631 throw new CliUsageError ( "Command is required." ) ;
16031632 }
16041633
@@ -1612,11 +1641,16 @@ async function runHostCommandWithInjectedEnv(input: {
16121641 } ) ,
16131642 target,
16141643 } ) ;
1615- return await run ( input . command , {
1616- cwd : project . projectRoot ,
1617- env : envState . env ,
1618- stdin : "inherit" ,
1619- } ) ;
1644+ return await run (
1645+ shellCommand
1646+ ? resolveShellCommandCommand ( { command : shellCommand } )
1647+ : positionalCommand ,
1648+ {
1649+ cwd : project . projectRoot ,
1650+ env : envState . env ,
1651+ stdin : "inherit" ,
1652+ }
1653+ ) ;
16201654}
16211655
16221656async function openHostShellWithInjectedEnv ( input : {
@@ -1670,6 +1704,7 @@ const handleEnvExec: CommandHandlerFor<typeof execSpec> = async ({
16701704 serviceName : args . options . service ,
16711705 targetOpt : args . options . target ,
16721706 command : args . positionals . command ,
1707+ shellCommandOpt : args . options . shellCommand ,
16731708 } ) ;
16741709} ;
16751710
@@ -1699,6 +1734,7 @@ const handleHostExec: CommandHandlerFor<typeof hostExecSpec> = async ({
16991734 scopeName : args . options . scope ,
17001735 targetOpt : args . options . target ,
17011736 command : args . positionals . command ,
1737+ shellCommandOpt : args . options . shellCommand ,
17021738 } ) ;
17031739} ;
17041740
0 commit comments