1616
1717import './preamble.js'
1818
19- import chalk from 'chalk'
2019import { jsonStringifySortedPolicy } from 'lavamoat-core'
20+ import fs from 'node:fs'
2121import path from 'node:path'
2222import terminalLink from 'terminal-link'
2323import yargs from 'yargs'
2424import { hideBin } from 'yargs/helpers'
2525import * as constants from './constants.js'
2626import { run } from './exec/run.js'
27- import { assertAbsolutePath , readJsonFile } from './fs.js'
27+ import { readJsonFile } from './fs.js'
2828import { log } from './log.js'
2929import { generatePolicy } from './policy-gen/generate.js'
3030import { loadPolicies } from './policy-util.js'
3131import { resolveBinScript , resolveEntrypoint } from './resolve.js'
32+ import { hrPath , toPath } from './util.js'
3233
3334/**
3435 * @import {PackageJson} from 'type-fest';
@@ -45,11 +46,6 @@ const BEHAVIOR_GROUP = 'Behavior Options:'
4546 */
4647const PATH_GROUP = 'Path Options:'
4748
48- /**
49- * Use this to give emphasis to words in error messages
50- */
51- const em = chalk . yellow
52-
5349/**
5450 * Strip out all `lavamoat` CLI args from `process.argv` so that the entrypoint
5551 * receives a `process.argv` like it would if it were executed directly with
@@ -96,6 +92,14 @@ const stripProcessArgv = (entrypoint, nonOptionArguments = []) => {
9692 process . argv . splice ( start , deleteCount , ...items )
9793}
9894
95+ /**
96+ * @param {string } entrypoint
97+ * @returns {boolean }
98+ */
99+ const shouldTrustRoot = ( entrypoint ) => {
100+ return ! entrypoint . includes ( 'node_modules' )
101+ }
102+
99103/**
100104 * Main entry point to CLI
101105 */
@@ -105,7 +109,7 @@ const main = async (args = hideBin(process.argv)) => {
105109 // TODO: Use import attributes instead
106110 // #region use import attributes instead
107111 const pkgJson = /** @type {PackageJson } */ (
108- await readJsonFile ( new URL ( '../package.json' , import . meta. url ) )
112+ await readJsonFile ( toPath ( new URL ( '../package.json' , import . meta. url ) ) )
109113 )
110114 const version = `${ pkgJson . version } `
111115 const homepage = `${ pkgJson . homepage } `
@@ -136,6 +140,9 @@ const main = async (args = hideBin(process.argv)) => {
136140 * just so happens that both commands have idential `entrypoint` arguments
137141 * (positionals). All _other_ properties are defined as global options; if
138142 * _all_ properties were global, this could just live in global middleware.
143+ *
144+ * In other words, this is here to avoid the inevitable future bug when a new
145+ * command is added.
139146 * @param {{
140147 * entrypoint: string
141148 * bin?: boolean
@@ -148,10 +155,8 @@ const main = async (args = hideBin(process.argv)) => {
148155 argv . entrypoint = argv . bin
149156 ? resolveBinScript ( argv . entrypoint , { from : argv . root } )
150157 : resolveEntrypoint ( argv . entrypoint , argv . root )
151- if ( entrypoint !== argv . entrypoint ) {
152- // note: this will print if the original entrypoint is a relative path; we
153- // may or may not want to continue displaying it for that specific case.
154- log . warning ( `Resolved ${ entrypoint } to ${ argv . entrypoint } ` )
158+ if ( hrPath ( entrypoint ) !== hrPath ( argv . entrypoint ) ) {
159+ log . warning ( `Resolved ${ hrPath ( entrypoint ) } → ${ hrPath ( argv . entrypoint ) } ` )
155160 }
156161 }
157162
@@ -162,7 +167,7 @@ const main = async (args = hideBin(process.argv)) => {
162167 * project. This is _probably_ not an issue anywhere other than in a dev
163168 * environment, but I wanted to make sure.
164169 */
165- yargs ( args )
170+ await yargs ( args )
166171 . parserConfiguration ( {
167172 /**
168173 * We deviate from yargs' default behavior by disabling
@@ -206,7 +211,17 @@ const main = async (args = hideBin(process.argv)) => {
206211 global : true ,
207212 group : BEHAVIOR_GROUP ,
208213 } ,
209- // the three policy options are used for both reading and writing
214+
215+ // #region path args
216+
217+ /**
218+ * The three `policy*` options below are used for both reading and
219+ * writing.
220+ *
221+ * Note that `coerce: path.resolve` is _only_ appropriate for the `root`
222+ * option, as the others are computed from it!
223+ */
224+
210225 policy : {
211226 alias : [ 'p' ] ,
212227 describe : 'Filepath to a policy file' ,
@@ -223,15 +238,15 @@ const main = async (args = hideBin(process.argv)) => {
223238 describe : 'Filepath to a policy override file' ,
224239 type : 'string' ,
225240 normalize : true ,
226- default : constants . DEFAULT_POLICY_OVERRIDE_PATH ,
241+ defaultDescription : constants . DEFAULT_POLICY_OVERRIDE_PATH ,
227242 nargs : 1 ,
228243 requiresArg : true ,
229244 global : true ,
230245 group : PATH_GROUP ,
231246 } ,
232247 'policy-debug' : {
233248 describe : 'Filepath to a policy debug file' ,
234- default : constants . DEFAULT_POLICY_DEBUG_PATH ,
249+ defaultDescription : constants . DEFAULT_POLICY_DEBUG_PATH ,
235250 nargs : 1 ,
236251 type : 'string' ,
237252 requiresArg : true ,
@@ -250,6 +265,8 @@ const main = async (args = hideBin(process.argv)) => {
250265 global : true ,
251266 group : PATH_GROUP ,
252267 } ,
268+ // #endregion
269+
253270 dev : {
254271 describe : 'Include development dependencies' ,
255272 type : 'boolean' ,
@@ -272,50 +289,56 @@ const main = async (args = hideBin(process.argv)) => {
272289 . conflicts ( 'quiet' , 'verbose' )
273290 . middleware (
274291 /**
275- * This resolves all paths from `cwd`.
292+ * This _global_ middleware:
276293 *
277- * @remarks
278- * This runs _before_ validation (second parameter).
294+ * - Ensures `policy`, `policy-debug` and `policy-override` paths are
295+ * absolute
296+ * - If necessary, calculates default path(s) for `policy-debug` and
297+ * `policy-override` (relative to `policy`) and sets it
298+ * - Configures the global logger based on `verbose` and `quiet` flags
299+ *
300+ * It will throw an exception if the user _explicitly_ provided a path to
301+ * a policy override file and that file is unreadable (since this is
302+ * really the only time it is feasible to do so).
279303 */
280- ( argv ) => {
304+ async ( argv ) => {
305+ await Promise . resolve ( )
306+
281307 argv . policy = path . resolve ( argv . root , argv . policy )
282- argv [ 'policy-override' ] = path . resolve (
283- argv . root ,
284- argv [ 'policy-override' ]
285- )
286- argv [ 'policy-debug' ] = path . resolve ( argv . root , argv [ 'policy-debug' ] )
287308
309+ // TODO: this mini-algorithm should be extracted to a function since it's used elsewhere too
310+ argv [ 'policy-debug' ] = argv [ 'policy-debug' ]
311+ ? path . resolve ( argv . root , argv [ 'policy-debug' ] )
312+ : path . join (
313+ path . dirname ( argv . policy ) ,
314+ constants . DEFAULT_POLICY_DEBUG_FILENAME
315+ )
316+
317+ if ( argv [ 'policy-override' ] ) {
318+ argv [ 'policy-override' ] = path . resolve (
319+ argv . root ,
320+ argv [ 'policy-override' ]
321+ )
322+ try {
323+ await fs . promises . access ( argv [ 'policy-override' ] , fs . constants . R_OK )
324+ } catch ( err ) {
325+ throw new Error (
326+ `Cannot read specified policy override file: ${ argv [ 'policy-override' ] } ` ,
327+ { cause : err }
328+ )
329+ }
330+ } else {
331+ argv [ 'policy-override' ] = path . join (
332+ path . dirname ( argv . policy ) ,
333+ constants . DEFAULT_POLICY_OVERRIDE_FILENAME
334+ )
335+ }
288336 if ( argv . verbose ) {
289337 log . setLevel ( 'debug' )
290338 } else if ( argv . quiet ) {
339+ // This assumes that we will never use the "emergency" log level!
291340 log . setLevel ( 'emergency' )
292341 }
293- } ,
294- true // RUN BEFORE CHECK FN
295- )
296- . check (
297- /**
298- * This validator is _global_ and runs before command-specific validators
299- * (I think)
300- */
301- ( argv ) => {
302- assertAbsolutePath (
303- argv . root ,
304- `${ em ( 'root' ) } must be an absolute path; ${ reportThisBug } `
305- )
306- assertAbsolutePath (
307- argv . policy ,
308- `${ em ( 'policy' ) } must be an absolute path; ${ reportThisBug } `
309- )
310- assertAbsolutePath (
311- argv [ 'policy-override' ] ,
312- `${ em ( 'policy-override' ) } must be an absolute path; ${ reportThisBug } `
313- )
314- assertAbsolutePath (
315- argv [ 'policy-debug' ] ,
316- `${ em ( 'policy-debug' ) } must be an absolute path; ${ reportThisBug } `
317- )
318- return true
319342 }
320343 )
321344 /**
@@ -378,10 +401,7 @@ const main = async (args = hideBin(process.argv)) => {
378401 /**
379402 * Resolve entrypoint from `root`
380403 */
381- . middleware (
382- processEntrypointMiddleware ,
383- true // RUN BEFORE CHECK FN
384- ) ,
404+ . middleware ( processEntrypointMiddleware ) ,
385405 /**
386406 * Default command handler.
387407 *
@@ -399,9 +419,17 @@ const main = async (args = hideBin(process.argv)) => {
399419 'policy-debug' : policyDebugPath ,
400420 'policy-override' : policyOverridePath ,
401421 dev,
422+ root : projectRoot ,
402423 write,
403424 } = argv
404425
426+ const trustRoot = shouldTrustRoot ( entrypoint )
427+ if ( ! trustRoot ) {
428+ log . info (
429+ `Entrypoint is in a ${ hrPath ( 'node_modules/' ) } directory and is considered untrusted`
430+ )
431+ }
432+
405433 /**
406434 * This will be the policy merged with overrides, if present
407435 *
@@ -416,27 +444,17 @@ const main = async (args = hideBin(process.argv)) => {
416444 debug,
417445 policyPath,
418446 policyDebugPath,
447+ policyOverridePath,
419448 write,
420449 dev,
450+ trustRoot,
451+ projectRoot,
421452 } )
422453 } else {
423- try {
424- policy = await loadPolicies ( policyPath , policyOverridePath )
425- } catch ( e ) {
426- const err = /** @type {NodeJS.ErrnoException } */ ( e )
427- if ( err . code === 'ENOENT' ) {
428- throw new Error (
429- `Could not load policy from ${ em ( argv . policy ) } and/or ${ em ( argv [ 'policy-override' ] ) } ; reason:\n${ err . message } `
430- )
431- }
432- if ( err . code === 'EISDIR' ) {
433- // TODO: actually allow a directory and apply a default filename
434- throw new Error (
435- `Could not load policy from ${ em ( argv . policy ) } and/or ${ em ( argv [ 'policy-override' ] ) } ; specify a filepath instead of a directory`
436- )
437- }
438- throw err
439- }
454+ policy = await loadPolicies ( policyPath , {
455+ policyOverridePath,
456+ projectRoot,
457+ } )
440458 }
441459
442460 stripProcessArgv (
@@ -448,7 +466,12 @@ const main = async (args = hideBin(process.argv)) => {
448466 */ ( argv [ '--' ] )
449467 )
450468
451- await run ( entrypoint , policy )
469+ await run ( entrypoint , policy , {
470+ policyOverridePath,
471+ trustRoot,
472+ dev,
473+ projectRoot,
474+ } )
452475 }
453476 )
454477 . command (
@@ -477,29 +500,35 @@ const main = async (args = hideBin(process.argv)) => {
477500 describe : 'Application entry point' ,
478501 type : 'string' ,
479502 } )
480- . middleware ( processEntrypointMiddleware , true ) ,
503+ . middleware ( processEntrypointMiddleware ) ,
481504 async ( {
482505 entrypoint,
483506 debug,
484507 policy : policyPath ,
485508 'policy-debug' : policyDebugPath ,
509+ 'policy-override' : policyOverridePath ,
486510 dev,
487511 write,
488512 } ) => {
513+ const trustRoot = shouldTrustRoot ( entrypoint )
514+ if ( ! trustRoot ) {
515+ log . info (
516+ `Entrypoint is in a ${ hrPath ( 'node_modules/' ) } directory and is considered untrusted`
517+ )
518+ }
519+
489520 const policy = await generatePolicy ( entrypoint , {
490521 debug,
491522 write,
492523 policyPath,
493524 policyDebugPath,
525+ policyOverridePath,
494526 dev,
527+ trustRoot,
495528 } )
496529
497- if ( debug ) {
498- log . info ( `Wrote debug policy to ${ policyDebugPath } ` )
499- }
500- if ( write ) {
501- log . info ( `Wrote policy to ${ policyPath } ` )
502- } else {
530+ if ( ! write ) {
531+ // console used here since the logger only uses stderr
503532 // eslint-disable-next-line no-console
504533 console . log ( jsonStringifySortedPolicy ( policy ) )
505534 }
@@ -526,7 +555,7 @@ const main = async (args = hideBin(process.argv)) => {
526555 . showHelpOnFail ( false )
527556 . demandCommand ( 1 )
528557 . strict ( true )
529- . parse ( )
558+ . parseAsync ( )
530559}
531560
532561// void here means "ignore the return value". it's a Promise, if you must know.
0 commit comments