@exodus/stasis 1.0.0-beta.8 → 1.0.0-beta.9

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 CHANGED
@@ -78,6 +78,7 @@ _Lockfiles (npm/pnpm/etc) not mentioned: they are like the "tarball" column, but
78
78
  | `stasis sbom --format=cyclonedx app.stasis.code.br` | export a CycloneDX SBOM for a lockfile or bundle |
79
79
 
80
80
  Each command's options are documented in [doc/](https://github.com/PreventiveMeasures/stasis/tree/main/doc):
81
+ [`run`](https://github.com/PreventiveMeasures/stasis/blob/main/doc/run.md),
81
82
  [`bundle`](https://github.com/PreventiveMeasures/stasis/blob/main/doc/bundle.md) (and `add`),
82
83
  [`github-bundle`](https://github.com/PreventiveMeasures/stasis/blob/main/doc/github-bundle.md),
83
84
  [`build`](https://github.com/PreventiveMeasures/stasis/blob/main/doc/build.md),
package/bin/stasis.js CHANGED
@@ -18,109 +18,34 @@ const jsname = argv.shift()
18
18
  const pathsEqual = (a, b) => a === b || (existsSync(a) && realpathSync(a) === b)
19
19
  assert(basename(jsname) === 'stasis' || pathsEqual(jsname, fileURLToPath(import.meta.url)))
20
20
 
21
- function usage(prefix = '') {
22
- console.error(`${prefix}\nUsage:
21
+ const HELP = `Usage:
23
22
  stasis run --lock=(add|replace|frozen|ignore) [--bundle=(add|replace|load|frozen|ignore)] [--bundle-file=path/to/bundle.br] [--resources-bundle-file=path/to/resources.br] [--dependencies] [--child-process] [--package-json] [--mock] [--import=module ...] [--fs=(sync|async)] [--resources=ext,ext] [--brotli-quality=0..11] path/to/file.js ...
24
- (--import forwards extra preload modules (repeatable) to the node process running the entry,
25
- e.g. --import=./instrument.mjs for a setup/instrumentation preload; a preload's own module
26
- graph is runner infrastructure like stasis's loader itself, so it stays out of the
27
- lockfile/bundle -- except modules the app graph also reaches, which are attested like any
28
- other app code; with --mock, preloads run under the mock's side-effect denials, so a preload
29
- that spawns helper processes cannot work there)
23
+ stasis bundle [--scope=(node_modules|full)] [--conditions=cond1,cond2] [--mainFields=field1,field2] [--metro [--metro-resolver] --platforms=ios,android] [--jsx] [--flow] [--typescript [--tsconfig=path/to/tsconfig.json]] [--resources=ext,ext] [--package-json] [--lockfile=path/to/stasis.lock.json] [--brotli-quality=0..11] [--add] [--output=(path|-)] path/to/file.(js|ts|jsx|tsx) ...
30
24
  stasis bundle [--mapping=path/to/remappings(.txt|.toml)] [--manifests] [--add] [--output=(path|-)] path/to/(file.sol|dir) ...
31
- (Solidity: a directory stands for every .sol file under it (a missing or empty one is skipped) --
32
- "stasis bundle src test script" is what "forge build" compiles, "stasis bundle contracts" what
33
- Hardhat does; imports resolve through the remappings "forge build" uses (remappings.txt,
34
- foundry.toml's profile, FOUNDRY_PROFILE, lib/ auto-detection incl. dependencies' own configs),
35
- else a root remappings.txt, then node_modules by file path; --mapping instead takes exactly the
36
- remappings that one file lists; an import must reach a .sol file inside the project, and a
37
- dependency's only other dependencies' files; --manifests also carries foundry.toml,
38
- remappings.txt, foundry.lock, soldeer.lock, .gitmodules and package.json of the project and its
39
- bundled dependencies, as written: RPC/Etherscan keys and URL credentials in them included)
40
- stasis bundle [--add] [--output=(path|-)] path/to/file.php ...
41
- stasis bundle [--scope=(node_modules|full)] [--conditions=cond1,cond2] [--mainFields=field1,field2] [--jsx] [--flow] [--typescript [--tsconfig=path/to/tsconfig.json]] [--resources=ext,ext] [--package-json] [--lockfile=path/to/stasis.lock.json] [--add] [--output=(path|-)] path/to/file.(js|ts|jsx|tsx) ...
42
- stasis bundle --metro [--metro-resolver] --platforms=ios,android [--platforms=web] [--jsx] [--flow] [--typescript [--tsconfig=path/to/tsconfig.json]] [--resources=ext,ext] [--package-json] [--lockfile=path/to/stasis.lock.json] [--add] [--output=(path|-)] path/to/file.(js|ts|jsx|tsx) ...
43
- stasis bundle [--add] [--output=(path|-)] path/to/file.(sh|bash) ...
44
25
  stasis bundle [--cargo] [--cargo-features=a,b,pkg/c] [--cargo-no-default-features] [--cargo-all-features] [--cargo-target=(triple|host)] [--cargo-manifests] [--add] [--output=(path|-)] path/to/file.rs ...
45
- (Rust: each crate's Cargo features are resolved from Cargo.toml/Cargo.lock like "cargo build" of the
46
- entries' packages, so #[cfg(feature = ...)] code that is off stays out -- by cargo's own resolver
47
- when there is a Cargo.lock (version 3 or 4; an older one stops the build), --cargo-target and every
48
- locked package in-tree, by replaying the manifests otherwise (said, with why); --cargo takes the resolution
49
- from "cargo metadata" instead -- it runs cargo, so only on a project you trust; the --cargo-*
50
- feature flags are cargo's --features / --no-default-features / --all-features for those packages;
51
- --cargo-target asks rustc for the target's cfgs so #[cfg(unix)]-style code for other targets stays
52
- out too, otherwise it is all kept; --cargo-manifests also bundles each bundled package's Cargo.toml
53
- and build script (a vendored crate's .cargo-checksum.json too), the workspace Cargo.toml, Cargo.lock
54
- and cargo configs, as written: tokens and URL credentials in them included)
55
- (writes to stasis.code.br by default, a JS bundle's in the project root its paths are relative to,
56
- where stasis run --bundle=load finds it; --output=- streams to stdout; --add merges into an
57
- existing bundle instead of replacing it (not with --output=-); --brotli-quality=0..11, default 9;
58
- --jsx parses JSX in .js/.cjs/.mjs files, e.g. React Native source (.jsx/.tsx files need no flag;
59
- put JSX-in-TS in a .tsx file);
60
- --flow strips Flow types from .js/.cjs/.mjs sources oxc can't parse (needs the optional flow-remove-types dep);
61
- --typescript resolves TS the way tsc does: an import of ./x.js lands on ./x.ts when no .js is on disk
62
- (likewise .mjs/.cjs -> .mts/.cts, extensionless ./x -> ./x.ts, incl. exports/imports/main targets),
63
- honouring tsconfig compilerOptions.paths aliases (from the tsconfig.json beside the importing
64
- file's package.json -- its workspace package's in a monorepo, else the project's -- or --tsconfig=path);
65
- never from or into node_modules (by real path, so linked workspace packages still are); not with --metro-resolver; implied when every entry is TS and each relative ./x.js-style import
66
- in them exists only as its TS source (otherwise a failing import it would resolve names it);
67
- --resources carries reached assets (e.g. --resources=png,svg) as resources instead of failing to bundle them;
68
- --package-json auto-includes each bundled module's package.json, even ones the scan never reached)
69
- stasis github-bundle --github=owner/name [--sha=commit|--tag=name] [--directory=path] [--package-manager=(pnpm|yarn1|npm|soldeer)] [--package-manager-version=version] [--generate=prisma] [--lockfile=path/to/stasis.lock.json] [--output=(path|-)] [stasis bundle's options for the entries] [path/in/repo/to/(file.(js|ts|jsx|tsx)|file.sol|dir) ...]
70
- (bundles a GitHub repo at a commit, or the one --tag names, its default branch's head without
71
- either, as "stasis bundle" bundles a clone of it once installed: the tree is fetched (with
72
- GITHUB_TOKEN where set) and held to its git tree id, and the dependencies are laid out in memory
73
- from the lockfile alone, as "pnpm install --frozen-lockfile --ignore-scripts" (pnpm 9, 10, 11 or
74
- 12), "yarn install --frozen-lockfile --ignore-scripts" (yarn 1.22), "npm ci --ignore-scripts"
75
- (npm 10.9.3 to 10.9.9, or 11.11.1 to 11.21.0) or "soldeer install" (0.12, for .sol entries)
76
- would: nothing is installed, no package script runs, and every tarball and zip is held to the
77
- lockfile, all cached where "stasis audit" caches. Without --package-manager, the one whose
78
- lockfile installs --directory (or the repo's root), if only one's does; at
79
- --package-manager-version, else at the root package.json's packageManager pin, else pnpm
80
- 10.33.4, yarn 1.22.22, npm 11.21.0 (which no packageManager pins) or Soldeer 0.12.0. The entries
81
- are paths in --directory, or in the repo; without them, the JS files its package.json names as
82
- main, exports and bin, resolved as the build resolves them (--conditions, --mainFields, --metro
83
- --platforms, --typescript), or for Soldeer its .sol files directly in it, under contracts/, and
84
- under its source directory (foundry.toml's src, else src/), but tests, scripts, mocks and
85
- dependency or build directories; its paths, and the directory its repo names, are
86
- --directory's, or of a JS bundle, the innermost package's at or above it holding every file it
87
- bundles, the project root's where one is outside; not with --metro-resolver, --cargo* or --add;
88
- --generate=prisma first writes, for each project with a "prisma-client" generator, the Prisma
89
- Client its "prisma generate" would (Prisma 7.4.0 to 7.10.0), with the optional
90
- @prisma/client-generator-ts 7.10.0 dependency: nothing of the repo runs, its schema and config
91
- are read as data, and its prisma for its version alone;
92
- writes to owner-name.<commit's first 7>.stasis.code.br by default,
93
- owner-name.<--directory, its / made ->.<commit's first 7>.stasis.code.br with --directory, each
94
- character outside [A-Za-z0-9._-] made _, and a directory too deep to fit in 255 characters cut,
95
- with a hash of it; --output=- streams to stdout)
26
+ stasis bundle [--add] [--output=(path|-)] path/to/file.(php|sh|bash) ...
27
+ stasis github-bundle --github=owner/name [--sha=commit|--tag=name] [--directory=path] [--package-manager=(pnpm|yarn1|npm|soldeer) [--package-manager-version=version]] [--generate=prisma] [--lockfile=path/to/stasis.lock.json] [--output=(path|-)] [stasis bundle's options for the entries] [path/in/repo/to/(file.(js|ts|jsx|tsx)|file.sol|dir) ...]
96
28
  stasis add path/to/(file|dir) ...
97
- (adds the listed files to the project's bundle(s) with no dependency resolution;
98
- a directory expands to its files. Requires a stasis.config.json (all fields optional).)
99
29
  stasis build --output=(dir|file.js) [--format=(esm|cjs|iife)] [--platform=(node|browser|neutral|hermes)] [--babel] [--minify] [--sourcemap] [--define=K=V ...] [--external=pkg ...] [--loader=.ext:name ...] path/to/(stasis.code.br|stasis.lock.json) [entry]
100
- (--platform=hermes downlevels every file for Hermes (classes, let/const, arrows, ...; via esbuild + Babel)
101
- and defaults --format to iife; --babel instead transforms every file with the project's own
102
- babel.config.(js|cjs|mjs|json); both resolve @babel/core and plugins from the project's dev deps)
103
30
  stasis extract [--output=path/to/dir] path/to/bundle.stasis.code.br
104
31
  stasis diff --stat [--imports] path/to/(lockfile|bundle) path/to/(lockfile|bundle)
105
32
  stasis prune [path/to/project]
106
- stasis audit [--why|--why-deep] [--why-full] [--reason=consumer] [--repo-advisories] path/to/file ...
107
- (audits the dependencies the files hold: npm packages by npm's advisories, vendored crates and
108
- Composer packages by OSV's, and Soldeer packages and GitHub repos (Foundry's lib/) by those their
109
- GitHub repository publishes, asked with GITHUB_TOKEN where it is set; a Composer dev version is
110
- listed as not audited, no database having one;
111
- --why lists, per advisory, the cross-module import paths that pull an npm package in,
112
- prefixed by the bundle consumer that imports each chain at the top level, e.g. "run: a -> b -> c";
113
- it skips chains whose full tail is already listed as its own chain -- --why-deep keeps them all;
114
- --why-full spells every chain out instead of collapsing repeated tails to "a -> b -> ... -> d";
115
- --reason=consumer shows only advisories related to that consumer, and with --why only its chains;
116
- --repo-advisories also asks each package's GitHub repository for the advisories its maintainers
117
- published there, before GitHub reviews them into the databases above; it needs a token in
118
- GITHUB_TOKEN, and asks the repository each package's artifact records, else the one the
119
- package.json a bundle carries of an npm package names, else looks one up and caches it for a
120
- month in the user cache dir, e.g. ~/.cache/stasis)
33
+ stasis audit [--why|--why-deep] [--why-full] [--reason=consumer] [--repo-advisories] path/to/(lockfile|bundle) ...
121
34
  stasis sbom --format=(spdx|cyclonedx) [--output=(path|-)] path/to/(lockfile|bundle) ...
122
- (streams to stdout by default; --output=- is explicit stdout)
123
- `.trim())
35
+ stasis --version
36
+
37
+ Each command's options: https://github.com/PreventiveMeasures/stasis/blob/main/doc/<command>.md
38
+ (run, bundle, github-bundle, build, extract, diff, prune, audit, sbom; add is in bundle.md)`
39
+
40
+ // The help, on stderr. Exits 0 when asked for (--help), 1 when printed for want of a command.
41
+ function usage(asked = false) {
42
+ console.error(HELP)
43
+ process.exit(asked ? 0 : 1)
44
+ }
45
+
46
+ // A usage error: the message and where the help is, never the help itself.
47
+ function fail(message) {
48
+ console.error(`${message}\nRun 'stasis --help' for usage.`)
124
49
  process.exit(1)
125
50
  }
126
51
 
@@ -143,6 +68,8 @@ const command = argv.shift()
143
68
  if (command === '-v' || command === '--version') {
144
69
  console.log(`v${pkg.version}`)
145
70
  process.exit(0)
71
+ } else if (command === '-h' || command === '--help') {
72
+ usage(true)
146
73
  } else if (command === 'run') {
147
74
  const options = {
148
75
  lock: { type: 'string', default: 'none' },
@@ -161,27 +88,27 @@ if (command === '-v' || command === '--version') {
161
88
  }
162
89
  const values = parseLeadingOptions(argv, options, {
163
90
  valueFlags: ['--bundle', '--bundle-file', '--resources-bundle-file', '--lock', '--import', '--resources', '--brotli-quality'],
164
- onError: usage,
91
+ onError: fail,
165
92
  })
166
- if (argv.length === 0) usage('Nothing to run: no path to file given')
167
- if (!['none', 'ignore', 'add', 'replace', 'frozen'].includes(values.lock)) usage('Error: invalid --lock value')
93
+ if (argv.length === 0) fail('Nothing to run: no path to file given')
94
+ if (!['none', 'ignore', 'add', 'replace', 'frozen'].includes(values.lock)) fail('Error: invalid --lock value')
168
95
  const lock = values.lock
169
96
  const scope = values.dependencies ? 'node_modules' : 'full'
170
97
  const bundle = values.bundle
171
98
  const bundleFile = values['bundle-file'] ? resolve(values['bundle-file']) : ''
172
99
  const resourcesBundleFile = values['resources-bundle-file'] ? resolve(values['resources-bundle-file']) : ''
173
100
  const debug = values.debug ? '1' : ''
174
- if (!['none', 'ignore', 'add', 'replace', 'load', 'frozen'].includes(bundle)) usage('Error: invalid --bundle value')
175
- if (bundleFile && bundle === 'none') usage('Error: --bundle-file requires --bundle=(add|replace|load|frozen|ignore)')
101
+ if (!['none', 'ignore', 'add', 'replace', 'load', 'frozen'].includes(bundle)) fail('Error: invalid --bundle value')
102
+ if (bundleFile && bundle === 'none') fail('Error: --bundle-file requires --bundle=(add|replace|load|frozen|ignore)')
176
103
  // --resources-bundle-file needs an active bundle mode (unlike --bundle-file, inert under ignore).
177
- if (resourcesBundleFile && (bundle === 'none' || bundle === 'ignore')) usage('Error: --resources-bundle-file requires --bundle=(add|replace|load|frozen)')
178
- if (bundle === 'load' && lock !== 'frozen' && lock !== 'none' && lock !== 'ignore') usage('Error: --bundle=load is incompatible with --lock=(add|replace)')
179
- if (lock === 'none' && bundle === 'none') usage('Error: stasis needs a lockfile or a bundle: set --lock or --bundle')
180
- if (values.mock && bundle === 'load') usage('Error: --mock is for capturing imports while building a bundle; not compatible with --bundle=load')
104
+ if (resourcesBundleFile && (bundle === 'none' || bundle === 'ignore')) fail('Error: --resources-bundle-file requires --bundle=(add|replace|load|frozen)')
105
+ if (bundle === 'load' && lock !== 'frozen' && lock !== 'none' && lock !== 'ignore') fail('Error: --bundle=load is incompatible with --lock=(add|replace)')
106
+ if (lock === 'none' && bundle === 'none') fail('Error: stasis needs a lockfile or a bundle: set --lock or --bundle')
107
+ if (values.mock && bundle === 'load') fail('Error: --mock is for capturing imports while building a bundle; not compatible with --bundle=load')
181
108
  // --fs requires a bundle mode (add|replace|load); `sync` patches the sync fs readers,
182
109
  // `async` adds their callback + fs.promises counterparts on top.
183
- if (values.fs !== undefined && !['sync', 'async'].includes(values.fs)) usage("Error: --fs must be 'sync' or 'async'")
184
- if (values.fs !== undefined && !['add', 'replace', 'load'].includes(bundle)) usage('Error: --fs requires --bundle=(add|replace|load)')
110
+ if (values.fs !== undefined && !['sync', 'async'].includes(values.fs)) fail("Error: --fs must be 'sync' or 'async'")
111
+ if (values.fs !== undefined && !['add', 'replace', 'load'].includes(bundle)) fail('Error: --fs requires --bundle=(add|replace|load)')
185
112
  const captureFs = values.fs ?? ''
186
113
  // --resources: comma-separated extension/filename allowlist for `--fs` captures.
187
114
  const resources = values.resources ?? ''
@@ -190,13 +117,13 @@ if (command === '-v' || command === '--version') {
190
117
  try {
191
118
  brotliQuality = parseBrotliQuality('--brotli-quality', values['brotli-quality'])
192
119
  } catch (cause) {
193
- usage(`Error: ${cause.message}`)
120
+ fail(`Error: ${cause.message}`)
194
121
  }
195
122
  }
196
123
  // --import: extra preload module(s) passed through to the spawned node. Node resolves each
197
124
  // against the project cwd; they ride AFTER stasis's own loader import.
198
125
  const imports = values.import ?? []
199
- if (imports.some((s) => s === '')) usage('Error: --import requires a module specifier (e.g. --import=./instrument.mjs)')
126
+ if (imports.some((s) => s === '')) fail('Error: --import requires a module specifier (e.g. --import=./instrument.mjs)')
200
127
  // Not a usage error: plain instrumentation/setup preloads still work under the mock; only ones
201
128
  // that need the denied surfaces (spawning helper processes, network) die, and they do so loudly.
202
129
  if (values.mock && imports.length > 0) {
@@ -207,7 +134,7 @@ if (command === '-v' || command === '--version') {
207
134
  // --package-json: auto-include every bundled module's package.json. Only meaningful while WRITING
208
135
  // a bundle (add|replace) -- there's nothing to fold the manifests into otherwise.
209
136
  if (values['package-json'] && bundle !== 'add' && bundle !== 'replace') {
210
- usage('Error: --package-json requires --bundle=(add|replace)')
137
+ fail('Error: --package-json requires --bundle=(add|replace)')
211
138
  }
212
139
  const packageJSON = values['package-json'] ? '1' : ''
213
140
  console.warn('[stasis] Running stasis with config:', { lock, scope, bundle, ...(bundleFile && { bundleFile }), ...(resourcesBundleFile && { resourcesBundleFile }), ...(childProcess && { childProcess: true }), ...(packageJSON && { packageJSON: true }), ...(values.mock && { mock: true }), ...(imports.length > 0 && { import: imports }), ...(values.fs && { fs: values.fs }), ...(resources && { resources }), ...(brotliQuality !== undefined && { brotliQuality }) })
@@ -240,7 +167,7 @@ if (command === '-v' || command === '--version') {
240
167
  while (!existsSync(p) && dirname(p) !== p) p = dirname(p)
241
168
  // Refuse write access to the filesystem root -- it would disable the --permission layer everywhere.
242
169
  if (p === dirname(p)) {
243
- usage(`Error: no existing parent directory for ${flag}=${file}; create one first or choose a path under an existing directory`)
170
+ fail(`Error: no existing parent directory for ${flag}=${file}; create one first or choose a path under an existing directory`)
244
171
  }
245
172
  if (!writeAllow.includes(p)) writeAllow.push(p)
246
173
  }
@@ -290,36 +217,36 @@ if (command === '-v' || command === '--version') {
290
217
  }
291
218
  const values = parseLeadingOptions(argv, options, {
292
219
  valueFlags: ['--mapping', '--output', '--scope', '--lockfile', '--conditions', '--mainFields', '--platforms', '--resources', '--tsconfig', '--cargo-features', '--cargo-target', '--brotli-quality', '-o'],
293
- onError: usage,
220
+ onError: fail,
294
221
  })
295
- if (argv.length === 0) usage('Nothing to bundle: no entry file given')
222
+ if (argv.length === 0) fail('Nothing to bundle: no entry file given')
296
223
  // A directory entry stands for the .sol files under it (Solidity only); an extensionless path
297
224
  // that doesn't exist is a missing one (skipped with a warning).
298
225
  const { directoryEntryError, isSolidityEntry } = await import('../src/cmd/bundle.js')
299
226
  const dirError = directoryEntryError(argv)
300
- if (dirError !== null) usage(`Error: ${dirError}`)
227
+ if (dirError !== null) fail(`Error: ${dirError}`)
301
228
  const allSol = argv.every((f) => isSolidityEntry(f))
302
229
  const allPhp = argv.every((f) => f.endsWith('.php'))
303
230
  const allJs = argv.every((f) => /\.(?:js|cjs|mjs|ts|cts|mts|jsx|tsx)$/u.test(f))
304
231
  const allBash = argv.every((f) => /\.(?:sh|bash)$/u.test(f))
305
232
  const allRust = argv.every((f) => f.endsWith('.rs'))
306
233
  if (!allSol && !allPhp && !allJs && !allBash && !allRust) {
307
- usage('Error: bundle entries must all be .sol, all be .php, all be .js/.cjs/.mjs/.ts/.cts/.mts/.jsx/.tsx, all be .sh/.bash, or all be .rs')
234
+ fail('Error: bundle entries must all be .sol, all be .php, all be .js/.cjs/.mjs/.ts/.cts/.mts/.jsx/.tsx, all be .sh/.bash, or all be .rs')
308
235
  }
309
- if (values.mapping && !allSol) usage('Error: --mapping is only valid for .sol bundles')
236
+ if (values.mapping && !allSol) fail('Error: --mapping is only valid for .sol bundles')
310
237
  // --manifests: carry the Solidity build's description files (foundry.toml, remappings.txt, ...).
311
238
  const manifests = Boolean(values.manifests)
312
- if (manifests && !allSol) usage('Error: --manifests is only valid for .sol bundles')
239
+ if (manifests && !allSol) fail('Error: --manifests is only valid for .sol bundles')
313
240
  // --cargo: take the Rust feature/dependency resolution from `cargo metadata` (runs cargo; opt-in).
314
241
  const cargo = Boolean(values.cargo)
315
- if (cargo && !allRust) usage('Error: --cargo is only valid for Rust bundles')
242
+ if (cargo && !allRust) fail('Error: --cargo is only valid for Rust bundles')
316
243
  // --cargo-features / --cargo-no-default-features / --cargo-all-features: cargo's own feature flags
317
244
  // for the entries' packages (`pkg/feat` targets one, or a dependency). --cargo-features is
318
245
  // repeatable and/or comma-separated; parseFeatureList is the one splitter (the resolver reuses it).
319
246
  const { parseFeatureList } = await import('../src/loaders/cargo.js')
320
247
  const cargoFeatures = parseFeatureList(values['cargo-features'] ?? [])
321
248
  if (values['cargo-features'] !== undefined && cargoFeatures.length === 0) {
322
- usage('Error: --cargo-features must list at least one feature (e.g. --cargo-features=serde,app/tls)')
249
+ fail('Error: --cargo-features must list at least one feature (e.g. --cargo-features=serde,app/tls)')
323
250
  }
324
251
  const cargoNoDefaultFeatures = Boolean(values['cargo-no-default-features'])
325
252
  const cargoAllFeatures = Boolean(values['cargo-all-features'])
@@ -327,50 +254,50 @@ if (command === '-v' || command === '--version') {
327
254
  // `#[cfg(unix)]`-style code. A triple is letters, digits, `-`, `_` and `.` (`x86_64-unknown-linux-gnu`).
328
255
  const cargoTarget = values['cargo-target'] ?? null
329
256
  if (cargoTarget !== null && !/^[\w.-]+$/u.test(cargoTarget)) {
330
- usage('Error: --cargo-target must be a target triple or "host" (e.g. --cargo-target=aarch64-apple-darwin)')
257
+ fail('Error: --cargo-target must be a target triple or "host" (e.g. --cargo-target=aarch64-apple-darwin)')
331
258
  }
332
259
  // --cargo-manifests: carry each bundled package's Cargo.toml and build script, the workspace
333
260
  // Cargo.toml, Cargo.lock and .cargo/config.toml too (the Rust counterpart of --package-json).
334
261
  const cargoManifests = Boolean(values['cargo-manifests'])
335
262
  if (!allRust) {
336
263
  const given = { 'cargo-features': cargoFeatures.length > 0, 'cargo-no-default-features': cargoNoDefaultFeatures, 'cargo-all-features': cargoAllFeatures, 'cargo-target': cargoTarget !== null, 'cargo-manifests': cargoManifests }
337
- for (const [flag, on] of Object.entries(given)) if (on) usage(`Error: --${flag} is only valid for Rust bundles`)
264
+ for (const [flag, on] of Object.entries(given)) if (on) fail(`Error: --${flag} is only valid for Rust bundles`)
338
265
  }
339
- if (values.scope && !allJs) usage('Error: --scope is only valid for JS bundles')
266
+ if (values.scope && !allJs) fail('Error: --scope is only valid for JS bundles')
340
267
  if (values.scope && !['node_modules', 'full'].includes(values.scope)) {
341
- usage('Error: --scope must be node_modules or full')
268
+ fail('Error: --scope must be node_modules or full')
342
269
  }
343
- if (values.lockfile && !allJs) usage('Error: --lockfile is only valid for JS bundles')
270
+ if (values.lockfile && !allJs) fail('Error: --lockfile is only valid for JS bundles')
344
271
  // --mainFields/--metro always emit a full-scope bundle, so they'd silently ignore --scope.
345
272
  if (values.scope !== undefined && (values.mainFields !== undefined || values.metro)) {
346
- usage('Error: --scope is not supported with --mainFields or --metro')
273
+ fail('Error: --scope is not supported with --mainFields or --metro')
347
274
  }
348
275
  // --conditions: extra exports/imports resolution conditions merged onto Node's defaults;
349
276
  // they don't honour legacy `mainFields` or platform suffixes (see --mainFields).
350
- if (values.conditions !== undefined && !allJs) usage('Error: --conditions is only valid for JS bundles')
277
+ if (values.conditions !== undefined && !allJs) fail('Error: --conditions is only valid for JS bundles')
351
278
  const conditions = values.conditions === undefined
352
279
  ? []
353
280
  : values.conditions.split(',').map((s) => s.trim()).filter(Boolean)
354
281
  if (values.conditions !== undefined && conditions.length === 0) {
355
- usage('Error: --conditions must list at least one condition name (e.g. --conditions=react-native,browser)')
282
+ fail('Error: --conditions must list at least one condition name (e.g. --conditions=react-native,browser)')
356
283
  }
357
284
  // --flow: strip Flow type syntax (via the optional flow-remove-types dep) before parsing; JS-only.
358
- if (values.flow && !allJs) usage('Error: --flow is only valid for JS bundles')
285
+ if (values.flow && !allJs) fail('Error: --flow is only valid for JS bundles')
359
286
  const flow = Boolean(values.flow)
360
287
  // --typescript: resolve like tsc -- a specifier naming a missing JS output (./x.js, .mjs, .cjs,
361
288
  // extensionless) lands on its on-disk TS source (./x.ts, .mts, .cts); an existing .js always wins.
362
- if (values.typescript && !allJs) usage('Error: --typescript is only valid for JS bundles')
289
+ if (values.typescript && !allJs) fail('Error: --typescript is only valid for JS bundles')
363
290
  const typescript = Boolean(values.typescript)
364
291
  // --tsconfig: the config whose compilerOptions.paths aliases --typescript honours (default: the
365
292
  // tsconfig.json beside each importing file's package.json). Meaningless without --typescript.
366
- if (values.tsconfig !== undefined && !typescript) usage('Error: --tsconfig is only valid with --typescript')
293
+ if (values.tsconfig !== undefined && !typescript) fail('Error: --tsconfig is only valid with --typescript')
367
294
  // --mainFields: legacy package entry fields (e.g. react-native,browser,main) for the non-exports resolver.
368
- if (values.mainFields !== undefined && !allJs) usage('Error: --mainFields is only valid for JS bundles')
295
+ if (values.mainFields !== undefined && !allJs) fail('Error: --mainFields is only valid for JS bundles')
369
296
  const mainFields = values.mainFields === undefined
370
297
  ? undefined
371
298
  : values.mainFields.split(',').map((s) => s.trim()).filter(Boolean)
372
299
  if (values.mainFields !== undefined && mainFields.length === 0) {
373
- usage('Error: --mainFields must list at least one field (e.g. --mainFields=react-native,browser,main)')
300
+ fail('Error: --mainFields must list at least one field (e.g. --mainFields=react-native,browser,main)')
374
301
  }
375
302
  // --metro: resolve like Metro/RN (its own conditions + mainFields, platform suffixes per
376
303
  // --platforms target). --platforms is repeatable and/or comma-separated; the values union.
@@ -381,55 +308,55 @@ if (command === '-v' || command === '--version') {
381
308
  const platforms = [...new Set((values.platforms ?? []).flatMap((p) => p.split(',')).map((s) => s.trim()).filter(Boolean))]
382
309
  // A platform name becomes an edge key: reject '/' (parsers reject it) and '*' (reserved placeholder).
383
310
  for (const p of platforms) {
384
- if (p === '*' || p.includes('/')) usage(`Error: invalid --platforms value '${p}' (a platform name can't contain '/' or be '*')`)
311
+ if (p === '*' || p.includes('/')) fail(`Error: invalid --platforms value '${p}' (a platform name can't contain '/' or be '*')`)
385
312
  }
386
- if ((metro || platforms.length > 0) && !allJs) usage('Error: --metro is only valid for JS bundles')
313
+ if ((metro || platforms.length > 0) && !allJs) fail('Error: --metro is only valid for JS bundles')
387
314
  // --jsx: parse React Native's JSX-in-.js source in the static scanner (off by default; oxc,
388
315
  // like tsc, auto-enables JSX for .jsx/.tsx only). Orthogonal to the resolver, so it pairs with
389
316
  // plain/--mainFields/--metro alike -- JS-only, since no other entry language is scanned.
390
317
  const jsx = Boolean(values.jsx)
391
- if (jsx && !allJs) usage('Error: --jsx is only valid for JS bundles')
318
+ if (jsx && !allJs) fail('Error: --jsx is only valid for JS bundles')
392
319
  // --package-json: fold each bundled module's package.json into the bundle. JS-only (only JS
393
320
  // modules carry an npm package.json; the sol/php/bash/rust bucketizers have no such manifest).
394
321
  const packageJSON = Boolean(values['package-json'])
395
- if (packageJSON && !allJs) usage('Error: --package-json is only valid for JS bundles')
322
+ if (packageJSON && !allJs) fail('Error: --package-json is only valid for JS bundles')
396
323
  // --resources: comma-separated extension/filename allowlist for assets reached through the graph
397
324
  // (e.g. --resources=png,svg). They're carried as resources instead of failing "can't carry" --
398
325
  // for graphs that aren't fully loadable in JS (Metro consumes such assets). JS-only.
399
- if (values.resources !== undefined && !allJs) usage('Error: --resources is only valid for JS bundles')
326
+ if (values.resources !== undefined && !allJs) fail('Error: --resources is only valid for JS bundles')
400
327
  const resources = values.resources === undefined
401
328
  ? []
402
329
  : values.resources.split(',').map((s) => s.trim()).filter(Boolean)
403
330
  if (values.resources !== undefined && resources.length === 0) {
404
- usage('Error: --resources must list at least one extension or filename (e.g. --resources=png,svg)')
331
+ fail('Error: --resources must list at least one extension or filename (e.g. --resources=png,svg)')
405
332
  }
406
333
  try {
407
334
  parseResourcesOption('--resources', resources) // validate early for a clean usage error (code exts / bad chars rejected)
408
335
  } catch (cause) {
409
- usage(`Error: ${cause.message}`)
336
+ fail(`Error: ${cause.message}`)
410
337
  }
411
338
  if (metro) {
412
- if (conditions.length > 0) usage("Error: --conditions can't be combined with --metro (it sets its own conditions)")
413
- if (mainFields !== undefined) usage("Error: --mainFields can't be combined with --metro (it sets its own mainFields)")
414
- if (platforms.length === 0) usage('Error: --metro requires --platforms (e.g. --platforms=ios,android)')
339
+ if (conditions.length > 0) fail("Error: --conditions can't be combined with --metro (it sets its own conditions)")
340
+ if (mainFields !== undefined) fail("Error: --mainFields can't be combined with --metro (it sets its own mainFields)")
341
+ if (platforms.length === 0) fail('Error: --metro requires --platforms (e.g. --platforms=ios,android)')
415
342
  } else if (platforms.length > 0) {
416
- usage('Error: --platforms is only valid with --metro')
343
+ fail('Error: --platforms is only valid with --metro')
417
344
  }
418
- if (metroResolver && !metro) usage('Error: --metro-resolver is only valid with --metro')
345
+ if (metroResolver && !metro) fail('Error: --metro-resolver is only valid with --metro')
419
346
  // metro-resolver can't substitute .js -> .ts, so --typescript would silently not apply -- reject it.
420
- if (typescript && metroResolver) usage("Error: --typescript is not supported with --metro-resolver (the project's metro-resolver doesn't substitute .js -> .ts)")
347
+ if (typescript && metroResolver) fail("Error: --typescript is not supported with --metro-resolver (the project's metro-resolver doesn't substitute .js -> .ts)")
421
348
  // --brotli-quality: valid for every entry language (no allJs gate).
422
349
  let brotliQuality
423
350
  if (values['brotli-quality'] !== undefined) {
424
351
  try {
425
352
  brotliQuality = parseBrotliQuality('--brotli-quality', values['brotli-quality'])
426
353
  } catch (cause) {
427
- usage(`Error: ${cause.message}`)
354
+ fail(`Error: ${cause.message}`)
428
355
  }
429
356
  }
430
357
  // --add merges into the existing bundle at --output, so it can't target write-only stdout.
431
358
  const add = Boolean(values.add)
432
- if (add && values.output === '-') usage('Error: --add cannot be combined with --output=-')
359
+ if (add && values.output === '-') fail('Error: --add cannot be combined with --output=-')
433
360
  const { bundleCommand } = await import('../src/cmd/bundle.js')
434
361
  await bundleCommand({
435
362
  cwd: process.cwd(),
@@ -486,10 +413,10 @@ if (command === '-v' || command === '--version') {
486
413
  generate: { type: 'string', multiple: true },
487
414
  }, {
488
415
  valueFlags: ['--github', '--sha', '--tag', '--directory', '--package-manager', '--package-manager-version', '--mapping', '--output', '--scope', '--lockfile', '--conditions', '--mainFields', '--platforms', '--tsconfig', '--resources', '--brotli-quality', '--generate', '-o'],
489
- onError: usage,
416
+ onError: fail,
490
417
  })
491
- if (values.github === undefined) usage('Error: github-bundle requires --github=owner/name, the repo to bundle')
492
- if (values.sha !== undefined && values.tag !== undefined) usage('Error: github-bundle takes --sha or --tag, not both')
418
+ if (values.github === undefined) fail('Error: github-bundle requires --github=owner/name, the repo to bundle')
419
+ if (values.sha !== undefined && values.tag !== undefined) fail('Error: github-bundle takes --sha or --tag, not both')
493
420
  // Whether each option applies to the entries is buildGitHubBundle's to say.
494
421
  const list = (value) => [value ?? []].flat().flatMap((v) => v.split(',')).map((s) => s.trim()).filter(Boolean)
495
422
  let brotliQuality
@@ -497,7 +424,7 @@ if (command === '-v' || command === '--version') {
497
424
  try {
498
425
  brotliQuality = parseBrotliQuality('--brotli-quality', values['brotli-quality'])
499
426
  } catch (cause) {
500
- usage(`Error: ${cause.message}`)
427
+ fail(`Error: ${cause.message}`)
501
428
  }
502
429
  }
503
430
  // Tarballs, zips and GitHub trees are cached where `stasis audit` caches.
@@ -533,10 +460,10 @@ if (command === '-v' || command === '--version') {
533
460
  } else if (command === 'add') {
534
461
  // add packs the listed files (no resolver), taking split targets + resource allowlist from
535
462
  // stasis.config.json; it takes no flags.
536
- if (argv.length === 0) usage('Nothing to add: no file given')
537
- if (argv.some((a) => a.startsWith('-'))) usage('Error: add takes no options; its targets and resource allowlist come from stasis.config.json')
463
+ if (argv.length === 0) fail('Nothing to add: no file given')
464
+ if (argv.some((a) => a.startsWith('-'))) fail('Error: add takes no options; its targets and resource allowlist come from stasis.config.json')
538
465
  const { addCommand } = await import('@exodus/stasis-core/add')
539
- // Let addCommand's runtime errors propagate as-is; wrapping in usage() would bury the cause.
466
+ // Let addCommand's runtime errors propagate as-is; wrapping in fail() would bury the cause.
540
467
  addCommand({ cwd: process.cwd(), entries: argv, logLabel: 'stasis' })
541
468
  } else if (command === 'build') {
542
469
  const options = {
@@ -553,37 +480,37 @@ if (command === '-v' || command === '--version') {
553
480
  }
554
481
  const values = parseLeadingOptions(argv, options, {
555
482
  valueFlags: ['--output', '-o', '--format', '--platform', '--define', '--external', '--loader'],
556
- onError: usage,
483
+ onError: fail,
557
484
  })
558
- if (argv.length === 0) usage('Nothing to build: no bundle or lockfile given')
559
- if (argv.length > 2) usage('Error: build takes a bundle or lockfile and an optional entry point')
485
+ if (argv.length === 0) fail('Nothing to build: no bundle or lockfile given')
486
+ if (argv.length > 2) fail('Error: build takes a bundle or lockfile and an optional entry point')
560
487
  // esbuild output knobs; the import graph itself is fixed by the artifact, not these.
561
488
  if (values.format !== undefined && !['esm', 'cjs', 'iife'].includes(values.format)) {
562
- usage('Error: --format must be esm, cjs, or iife')
489
+ fail('Error: --format must be esm, cjs, or iife')
563
490
  }
564
491
  if (values.platform !== undefined && !['node', 'browser', 'neutral', 'hermes'].includes(values.platform)) {
565
- usage('Error: --platform must be node, browser, neutral, or hermes')
492
+ fail('Error: --platform must be node, browser, neutral, or hermes')
566
493
  }
567
494
  // Hermes has no import/export; --format defaults to iife under --platform=hermes (see buildCommand).
568
495
  if (values.platform === 'hermes' && values.format === 'esm') {
569
- usage('Error: --platform=hermes cannot emit ESM; use --format=iife (the hermes default) or cjs')
496
+ fail('Error: --platform=hermes cannot emit ESM; use --format=iife (the hermes default) or cjs')
570
497
  }
571
- if (!values.output) usage('Error: --output is required (a .js/.cjs/.mjs file, or a directory)')
498
+ if (!values.output) fail('Error: --output is required (a .js/.cjs/.mjs file, or a directory)')
572
499
  // build can't stream to stdout; without this, --output=- would create a dir literally named `-`.
573
- if (values.output === '-') usage('Error: stasis build cannot stream to stdout; --output must be a .js/.cjs/.mjs file or a directory')
500
+ if (values.output === '-') fail('Error: stasis build cannot stream to stdout; --output must be a .js/.cjs/.mjs file or a directory')
574
501
  // --define=KEY=VALUE -> esbuild define map; VALUE is forwarded verbatim and must be valid JS
575
502
  // (a JSON literal or an identifier), e.g. --define=DEBUG=false.
576
503
  const define = {}
577
504
  for (const entry of values.define ?? []) {
578
505
  const eq = entry.indexOf('=')
579
- if (eq <= 0) usage(`Error: --define must be KEY=VALUE (got '${entry}')`)
506
+ if (eq <= 0) fail(`Error: --define must be KEY=VALUE (got '${entry}')`)
580
507
  define[entry.slice(0, eq)] = entry.slice(eq + 1)
581
508
  }
582
509
  // --loader=.ext:NAME -> esbuild loader override for that extension (e.g. --loader=.js:jsx).
583
510
  const loader = {}
584
511
  for (const entry of values.loader ?? []) {
585
512
  const colon = entry.indexOf(':')
586
- if (colon <= 0 || !entry.startsWith('.')) usage(`Error: --loader must be .EXT:LOADER (got '${entry}')`)
513
+ if (colon <= 0 || !entry.startsWith('.')) fail(`Error: --loader must be .EXT:LOADER (got '${entry}')`)
587
514
  loader[entry.slice(0, colon)] = entry.slice(colon + 1)
588
515
  }
589
516
  const { buildCommand } = await import('../src/cmd/build.js')
@@ -605,9 +532,9 @@ if (command === '-v' || command === '--version') {
605
532
  const options = {
606
533
  output: { type: 'string', short: 'o' },
607
534
  }
608
- const values = parseLeadingOptions(argv, options, { valueFlags: ['--output', '-o'], onError: usage })
609
- if (argv.length === 0) usage('Nothing to extract: no bundle file given')
610
- if (argv.length > 1) usage('Error: extract takes exactly one bundle file')
535
+ const values = parseLeadingOptions(argv, options, { valueFlags: ['--output', '-o'], onError: fail })
536
+ if (argv.length === 0) fail('Nothing to extract: no bundle file given')
537
+ if (argv.length > 1) fail('Error: extract takes exactly one bundle file')
611
538
  const { extractCommand } = await import('@exodus/stasis-core/extract')
612
539
  extractCommand({ cwd: process.cwd(), bundleFile: argv[0], output: values.output, logLabel: 'stasis' })
613
540
  } else if (command === 'diff') {
@@ -615,27 +542,27 @@ if (command === '-v' || command === '--version') {
615
542
  stat: { type: 'boolean' },
616
543
  imports: { type: 'boolean' },
617
544
  }
618
- const values = parseLeadingOptions(argv, options, { onError: usage })
545
+ const values = parseLeadingOptions(argv, options, { onError: fail })
619
546
  // --stat is required for now (it reports what changed, not the content); bare `stasis diff`
620
547
  // is reserved for a future content-level diff.
621
- if (!values.stat) usage('Error: stasis diff currently requires --stat')
622
- if (argv.length !== 2) usage('Error: stasis diff takes exactly two files (lockfile or bundle)')
548
+ if (!values.stat) fail('Error: stasis diff currently requires --stat')
549
+ if (argv.length !== 2) fail('Error: stasis diff takes exactly two files (lockfile or bundle)')
623
550
  // --imports also diffs the resolution graphs (added/removed/redirected edges); off by default (verbose).
624
551
  const { diffCommand } = await import('../src/cmd/diff.js')
625
552
  const { differences } = diffCommand({ cwd: process.cwd(), left: argv[0], right: argv[1], stat: true, imports: values.imports })
626
553
  // Exit non-zero when the artifacts differ, so it composes in CI.
627
554
  process.exitCode = differences ? 1 : 0
628
555
  } else if (command === 'prune') {
629
- if (argv.length > 1) usage('Error: prune takes at most one path argument')
556
+ if (argv.length > 1) fail('Error: prune takes at most one path argument')
630
557
  const root = argv[0] ? resolve(argv[0]) : process.cwd()
631
558
  const { prune } = await import('@exodus/stasis-core/prune')
632
559
  const { removed, validated } = prune({ root })
633
560
  console.warn(`[stasis] prune: validated ${validated.length} file(s), removed ${removed.length} file(s)`)
634
561
  } else if (command === 'audit') {
635
- const values = parseLeadingOptions(argv, { why: { type: 'boolean' }, 'why-deep': { type: 'boolean' }, 'why-full': { type: 'boolean' }, reason: { type: 'string' }, 'repo-advisories': { type: 'boolean' } }, { valueFlags: ['--reason'], onError: usage })
636
- if (argv.length === 0) usage('Nothing to audit: no path to file given')
562
+ const values = parseLeadingOptions(argv, { why: { type: 'boolean' }, 'why-deep': { type: 'boolean' }, 'why-full': { type: 'boolean' }, reason: { type: 'string' }, 'repo-advisories': { type: 'boolean' } }, { valueFlags: ['--reason'], onError: fail })
563
+ if (argv.length === 0) fail('Nothing to audit: no path to file given')
637
564
  const repoAdvisories = Boolean(values['repo-advisories'])
638
- if (repoAdvisories && !process.env.GITHUB_TOKEN) usage('Error: --repo-advisories requires a GitHub token in GITHUB_TOKEN')
565
+ if (repoAdvisories && !process.env.GITHUB_TOKEN) fail('Error: --repo-advisories requires a GitHub token in GITHUB_TOKEN')
639
566
  // A Soldeer package's and a GitHub repo's advisories are their repository's, asked of GitHub with
640
567
  // GITHUB_TOKEN where it is set; the client asks nothing for any other package by itself.
641
568
  const github = (await import('@preventive/upstream/github.js')).createClient({ token: process.env.GITHUB_TOKEN || null })
@@ -653,10 +580,10 @@ if (command === '-v' || command === '--version') {
653
580
  format: { type: 'string' },
654
581
  output: { type: 'string', short: 'o' },
655
582
  }
656
- const values = parseLeadingOptions(argv, options, { valueFlags: ['--format', '--output', '-o'], onError: usage })
657
- if (!values.format) usage('Error: stasis sbom requires --format=(spdx|cyclonedx)')
658
- if (!['spdx', 'cyclonedx'].includes(values.format)) usage('Error: --format must be spdx or cyclonedx')
659
- if (argv.length === 0) usage('Nothing to export: no lockfile or bundle given')
583
+ const values = parseLeadingOptions(argv, options, { valueFlags: ['--format', '--output', '-o'], onError: fail })
584
+ if (!values.format) fail('Error: stasis sbom requires --format=(spdx|cyclonedx)')
585
+ if (!['spdx', 'cyclonedx'].includes(values.format)) fail('Error: --format must be spdx or cyclonedx')
586
+ if (argv.length === 0) fail('Nothing to export: no lockfile or bundle given')
660
587
  const { sbomCommand } = await import('../src/cmd/sbom.js')
661
588
  sbomCommand({
662
589
  cwd: process.cwd(),
@@ -664,6 +591,8 @@ if (command === '-v' || command === '--version') {
664
591
  format: values.format,
665
592
  output: values.output,
666
593
  })
667
- } else {
594
+ } else if (command === undefined) {
668
595
  usage()
596
+ } else {
597
+ fail(`Error: unknown command '${command}'`)
669
598
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/stasis",
3
- "version": "1.0.0-beta.8",
3
+ "version": "1.0.0-beta.9",
4
4
  "description": "File-level source bundles and lockfiles",
5
5
  "type": "module",
6
6
  "exports": {
@@ -41,6 +41,7 @@
41
41
  "src/cmd/sbom.js",
42
42
  "src/diff.js",
43
43
  "src/esbuild.js",
44
+ "src/installed-commits.js",
44
45
  "src/json-stream.js",
45
46
  "src/loader.js",
46
47
  "src/loader-mock.js",
@@ -94,12 +95,12 @@
94
95
  "homepage": "https://github.com/PreventiveMeasures/stasis#readme",
95
96
  "dependencies": {
96
97
  "@exodus/bytes": "^1.16.0",
97
- "@exodus/stasis-core": "1.0.0-beta.8",
98
- "@exodus/stasis-plugins": "1.0.0-beta.8",
98
+ "@exodus/stasis-core": "1.0.0-beta.9",
99
+ "@exodus/stasis-plugins": "1.0.0-beta.9",
99
100
  "@preventive/archive": "1.0.0-beta.4",
100
- "@preventive/deptree": "1.0.0-alpha.9",
101
+ "@preventive/deptree": "1.0.0-alpha.11",
101
102
  "@preventive/lockfile": "1.0.0-alpha.7",
102
- "@preventive/upstream": "1.0.0-alpha.11",
103
+ "@preventive/upstream": "1.0.0-alpha.12",
103
104
  "@preventive/vfs": "1.0.0-beta.4",
104
105
  "oxc-parser": "^0.152.0"
105
106
  },