@@ -38,6 +38,7 @@ import {
3838 secretFileIgnored ,
3939 persistSiteUuid ,
4040 resolveConfig ,
41+ type ResolveConfigOptions ,
4142 writeConfigFile ,
4243 persistTimeout ,
4344} from './config.js' ;
@@ -76,6 +77,7 @@ import { runProtect, runVerify } from './protect/install/index.js';
7677import { formatRuntimeCheck , runRuntimeCheck , runtimeExitCode } from './protect/install/runtime/check.js' ;
7778import { runMap } from './map-command.js' ;
7879import { getStringFlag } from './flags.js' ;
80+ import { isCanonicalUuid } from './endpoint-policy.js' ;
7981import { setupProtection , wireBuildScripts } from './setup.js' ;
8082import type { SetupProtectionResult , WireBuildScriptsResult } from './setup.js' ;
8183import { isInstallOrBuildHook , isPreBundleBuildHook , undeliveredReportLines } from './build-hook.js' ;
@@ -296,7 +298,38 @@ function parseArgs(argv: string[]): ParsedArgs {
296298 } ;
297299}
298300
301+ /** The site/endpoint overrides every command accepts, resolved against the current directory. */
302+ function resolveCliConfig (
303+ args : ParsedArgs ,
304+ extra : Omit < ResolveConfigOptions , 'cwd' | 'cliSiteUuid' | 'cliEndpoint' > = { } ,
305+ ) : Promise < Config > {
306+ return resolveConfig ( {
307+ cwd : process . cwd ( ) ,
308+ cliSiteUuid : getStringFlag ( args . flags , 'site-uuid' ) ,
309+ cliEndpoint : getStringFlag ( args . flags , 'endpoint' ) ,
310+ ...extra ,
311+ } ) ;
312+ }
313+
314+ /**
315+ * Whether a build agent is running this. `CI=false` is how a platform says "not a CI build", so it
316+ * counts as absent — the interactive commands refuse only where there is really no one to answer.
317+ */
318+ function runningInCi ( ) : boolean {
319+ return process . env . CI !== undefined && process . env . CI !== '' && process . env . CI !== 'false' ;
320+ }
299321
322+ /**
323+ * What became of the credential file's ignore entry, for the person reading the output.
324+ *
325+ * Only claimed when `.gitignore` was read back and really covers it: an assurance that turns out to be
326+ * false is worse than none, because it is the reason somebody stops checking.
327+ */
328+ function gitignoreOutcomeLine ( ignore : { ignored : boolean ; reason ?: string } ) : string {
329+ return ignore . ignored
330+ ? ' Added to .gitignore.'
331+ : ` NOT ignored by git — ${ ignore . reason ?? 'unknown reason' } . Add it to .gitignore yourself before committing.` ;
332+ }
300333
301334async function runInit ( args : ParsedArgs ) : Promise < number > {
302335 const uuid = args . positional [ 0 ] ;
@@ -305,7 +338,7 @@ async function runInit(args: ParsedArgs): Promise<number> {
305338 console . error ( 'Usage: patchstack-connect init <site-uuid>' ) ;
306339 return 1 ;
307340 }
308- if ( ! / ^ [ 0 - 9 a - f ] { 8 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 12 } $ / i . test ( uuid ) ) {
341+ if ( ! isCanonicalUuid ( uuid ) ) {
309342 console . error ( `Error: "${ uuid } " does not look like a valid UUID.` ) ;
310343 return 1 ;
311344 }
@@ -319,16 +352,12 @@ async function runInit(args: ParsedArgs): Promise<number> {
319352
320353async function runClaim ( args : ParsedArgs ) : Promise < number > {
321354 // No browser and no human to sign in. A deploy inherits an already-claimed site; it never claims.
322- if ( process . env . CI !== undefined && process . env . CI !== '' && process . env . CI !== 'false' ) {
355+ if ( runningInCi ( ) ) {
323356 console . error ( '`claim` is interactive and cannot run in CI. Claim the site from a developer machine.' ) ;
324357 return 1 ;
325358 }
326359
327- const config = await resolveConfig ( {
328- cwd : process . cwd ( ) ,
329- cliSiteUuid : getStringFlag ( args . flags , 'site-uuid' ) ,
330- cliEndpoint : getStringFlag ( args . flags , 'endpoint' ) ,
331- } ) ;
360+ const config = await resolveCliConfig ( args ) ;
332361
333362 const siteUuid = config . siteUuid ;
334363 if ( siteUuid === null ) {
@@ -345,11 +374,7 @@ async function runClaim(args: ParsedArgs): Promise<number> {
345374 const ignore = await secretFileIgnored ( process . cwd ( ) ) ;
346375 // The value itself is never printed — only that it landed, and only that it is ignored when it is.
347376 console . log ( ` A credential for this site was issued and saved to ${ SECRET_CONFIG_FILENAME } .` ) ;
348- console . log (
349- ignore . ignored
350- ? ' Added to .gitignore.'
351- : ` NOT ignored by git — ${ ignore . reason ?? 'unknown reason' } . Add it to .gitignore yourself before committing.` ,
352- ) ;
377+ console . log ( gitignoreOutcomeLine ( ignore ) ) ;
353378 }
354379 console . log ( '' ) ;
355380 return 0 ;
@@ -438,28 +463,20 @@ async function runClaim(args: ParsedArgs): Promise<number> {
438463async function runLogin ( args : ParsedArgs ) : Promise < number > {
439464 // CI has no browser and no human; build agents must not print credentials
440465 // into logs. Deploys use PATCHSTACK_PULSE_AUTH from the platform's secrets.
441- if ( process . env . CI !== undefined && process . env . CI !== '' && process . env . CI !== 'false' ) {
466+ if ( runningInCi ( ) ) {
442467 console . error ( '`login` is interactive and cannot run in CI. Set PATCHSTACK_PULSE_AUTH instead.' ) ;
443468 return 1 ;
444469 }
445470
446- const config = await resolveConfig ( {
447- cwd : process . cwd ( ) ,
448- cliSiteUuid : getStringFlag ( args . flags , 'site-uuid' ) ,
449- cliEndpoint : getStringFlag ( args . flags , 'endpoint' ) ,
450- } ) ;
471+ const config = await resolveCliConfig ( args ) ;
451472
452473 // Checked here rather than carried up from the write: the rotation happens several layers down, and the
453474 // claim belongs to the line that prints it.
454475 const approved = async ( ) => {
455476 const ignore = await secretFileIgnored ( process . cwd ( ) ) ;
456477 // The value itself is never printed — only that it landed, and only that it is ignored when it is.
457478 console . log ( `\n ✓ Credential restored and saved to ${ SECRET_CONFIG_FILENAME } .` ) ;
458- console . log (
459- ignore . ignored
460- ? ' Added to .gitignore.'
461- : ` NOT ignored by git — ${ ignore . reason ?? 'unknown reason' } . Add it to .gitignore yourself before committing.` ,
462- ) ;
479+ console . log ( gitignoreOutcomeLine ( ignore ) ) ;
463480 console . log ( ' The previous credential no longer works. Update it anywhere else it was set:' ) ;
464481 console . log ( ' CI secrets, hosting env vars, preview environments, other checkouts.\n' ) ;
465482 return 0 ;
@@ -619,10 +636,7 @@ async function runScan(
619636 options : { showRemainingSetup ?: boolean } = { } ,
620637) : Promise < number > {
621638 const dryRun = args . flags . get ( 'dry-run' ) === true ;
622- const config = await resolveConfig ( {
623- cwd : process . cwd ( ) ,
624- cliSiteUuid : getStringFlag ( args . flags , 'site-uuid' ) ,
625- cliEndpoint : getStringFlag ( args . flags , 'endpoint' ) ,
639+ const config = await resolveCliConfig ( args , {
626640 cliClaimToken : getStringFlag ( args . flags , 'claim-token' ) ,
627641 // The one command that reports them, so the one command that resolves them.
628642 detectSiteIdentity : true ,
@@ -1009,12 +1023,7 @@ async function runDemoCommand(args: ParsedArgs): Promise<number> {
10091023 try {
10101024 const scenario = resolveDemoScenario ( args . positional [ 0 ] ) ;
10111025 const cwd = process . cwd ( ) ;
1012- const config = await resolveConfig ( {
1013- cwd,
1014- cliSiteUuid : getStringFlag ( args . flags , 'site-uuid' ) ,
1015- cliEndpoint : getStringFlag ( args . flags , 'endpoint' ) ,
1016- requireSiteUuid : true ,
1017- } ) ;
1026+ const config = await resolveCliConfig ( args , { requireSiteUuid : true } ) ;
10181027 if ( config . environment !== 'production' ) {
10191028 throw new DemoError (
10201029 'The production-backed demo requires PATCHSTACK_ENVIRONMENT=production. Unset the sandbox override and try again.' ,
@@ -1269,11 +1278,7 @@ function setupOutcome(
12691278}
12701279
12711280async function runStatus ( args : ParsedArgs ) : Promise < number > {
1272- const config = await resolveConfig ( {
1273- cwd : process . cwd ( ) ,
1274- cliSiteUuid : getStringFlag ( args . flags , 'site-uuid' ) ,
1275- cliEndpoint : getStringFlag ( args . flags , 'endpoint' ) ,
1276- } ) ;
1281+ const config = await resolveCliConfig ( args ) ;
12771282 console . log ( `Site UUID: ${ config . siteUuid ?? '(none yet — the next `scan` will provision one)' } ` ) ;
12781283 console . log (
12791284 `Endpoint: ${ config . endpoint } ${ config . endpoint === DEFAULT_ENDPOINT ? '' : ' (override)' } ` ,
@@ -1311,11 +1316,7 @@ async function runStatus(args: ParsedArgs): Promise<number> {
13111316}
13121317
13131318async function runUninstall ( args : ParsedArgs ) : Promise < number > {
1314- const config = await resolveConfig ( {
1315- cwd : process . cwd ( ) ,
1316- cliSiteUuid : getStringFlag ( args . flags , 'site-uuid' ) ,
1317- cliEndpoint : getStringFlag ( args . flags , 'endpoint' ) ,
1318- } ) ;
1319+ const config = await resolveCliConfig ( args ) ;
13191320
13201321 if ( config . siteUuid === null ) {
13211322 console . log ( 'No site UUID configured — there is no site record to signal about.' ) ;
0 commit comments