depcurfew 1.0.0 → 1.1.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/dist/index.d.ts CHANGED
@@ -1,3 +1,8 @@
1
+ type FetchLike = (url: string, init: {
2
+ headers: Record<string, string>;
3
+ signal: AbortSignal;
4
+ redirect?: 'manual';
5
+ }) => Promise<Response>;
1
6
  declare const EXIT_OK = 0;
2
7
  declare const EXIT_FAILURE = 1;
3
8
  declare const EXIT_USAGE = 2;
@@ -9,11 +14,26 @@ interface CliIo {
9
14
  stdout: boolean;
10
15
  stderr: boolean;
11
16
  };
17
+ /** Whether each stream is a terminal; absent means it is not. */
18
+ terminal?: {
19
+ stdout: boolean;
20
+ stderr: boolean;
21
+ };
22
+ }
23
+ interface CheckDependencies {
24
+ fetch: FetchLike;
25
+ now: () => Date;
26
+ /** The whole of stdin, for hook mode. */
27
+ stdin: () => Promise<string>;
28
+ /** The user's home directory, with the user's .npmrc. */
29
+ home?: () => string;
12
30
  }
13
- declare function run(argv: readonly string[], output: CliIo): Promise<number>;
31
+ declare function run(argv: readonly string[], output: CliIo, dependencies?: CheckDependencies): Promise<number>;
14
32
  declare function getVersion(): string;
15
- type ToolId = 'npm' | 'pnpm' | 'yarn' | 'bun' | 'deno' | 'renovate' | 'dependabot';
33
+ type ToolId = 'npm' | 'pnpm' | 'yarn' | 'bun' | 'deno' | 'uv' | 'pip' | 'poetry' | 'pdm' | 'pipenv' | 'renovate' | 'dependabot';
16
34
  type ToolKind = 'package-manager' | 'update-bot';
35
+ /** The registry a package manager installs from; package names in the policy are compared per ecosystem. */
36
+ type Ecosystem = 'npm' | 'pypi';
17
37
  interface Evidence$1<T> {
18
38
  value: T;
19
39
  /** URL of official documentation or changelog. */
@@ -95,6 +115,8 @@ interface SourcesKnowledge {
95
115
  interface WorkspaceDeclaration {
96
116
  files: readonly string[];
97
117
  key: string;
118
+ /** Key of a list of patterns that exclude packages, next to the package list; such patterns win. */
119
+ excludeKey?: string;
98
120
  }
99
121
  /** Facts for recognising a package manager in a repository. */
100
122
  interface ProjectKnowledge {
@@ -102,7 +124,8 @@ interface ProjectKnowledge {
102
124
  lockfiles: Fact<Versioned<readonly string[]>>;
103
125
  /** Files that make a directory a project or a workspace package for this tool. */
104
126
  manifests: Fact<readonly string[]>;
105
- workspaces: Fact<WorkspaceDeclaration>;
127
+ /** `null` when the tool has no workspaces. In TOML files the key is a dotted path to a table. */
128
+ workspaces: Fact<WorkspaceDeclaration | null>;
106
129
  /** Key of the package list when the workspace declaration is an object instead of a list; `null` if only lists are accepted. */
107
130
  workspacesObjectKey: Fact<string | null>;
108
131
  }
@@ -110,17 +133,19 @@ interface ToolKnowledge {
110
133
  id: ToolId;
111
134
  name: string;
112
135
  kind: ToolKind;
136
+ /** Package managers only; `npm` when absent. */
137
+ ecosystem?: Ecosystem;
113
138
  cooldown: CooldownKnowledge;
114
139
  installScripts: InstallScriptsKnowledge;
115
140
  /** Package managers only: bots do not resolve dependency sources themselves. */
116
141
  sources?: SourcesKnowledge;
117
- /** Package managers only: how to recognise the tool in a repository. */
142
+ /** Package managers only: how to recognize the tool in a repository. */
118
143
  project?: ProjectKnowledge;
119
144
  /** Further sourced facts that matter for generators and `doctor`, keyed by a short name. */
120
145
  extra?: Readonly<Record<string, Fact<unknown>>>;
121
146
  }
122
- /** A tool that environment facts refer to: Node.js or a package manager. */
123
- type RuntimeOrTool = 'node' | ToolId;
147
+ /** A tool that environment facts refer to: Node.js, Python or a package manager. */
148
+ type RuntimeOrTool = 'node' | 'python' | ToolId;
124
149
  interface PackageManagerFieldKnowledge {
125
150
  /** Key in package.json, e.g. `packageManager`. */
126
151
  key: string;
@@ -174,6 +199,122 @@ interface StopCommandsKnowledge {
174
199
  stop: string;
175
200
  resume: string;
176
201
  }
202
+ /** The public npm registry, as depcurfew check reads it. */
203
+ interface NpmRegistryKnowledge {
204
+ url: string;
205
+ /** Path of a package document; scoped names have their slash escaped as %2f. */
206
+ packagePath: string;
207
+ /** Field of the full document that maps versions to publish times; the abbreviated install document lacks it. */
208
+ timeField: string;
209
+ /** Field of the document that maps tags (latest, next) to versions. */
210
+ distTagsField: string;
211
+ /** The tag every package has, which a spec without a version installs. */
212
+ defaultTag: string;
213
+ /** Field of the document that maps versions to their version objects. */
214
+ versionsField: string;
215
+ /** Field of a version object with the lifecycle scripts of its package.json. */
216
+ scriptsField: string;
217
+ /** Field of a version object that holds the deprecation message. */
218
+ deprecatedField: string;
219
+ /** Lifecycle scripts a package runs when it is installed as a dependency. */
220
+ installScripts: readonly string[];
221
+ }
222
+ /** How the Python interpreter runs a module (python -m pip) or a command string. */
223
+ interface PythonInterpreterKnowledge {
224
+ /** Executable names; versioned names such as python3.12 are the same. */
225
+ executables: readonly string[];
226
+ /** Runs the named module as a script; the options end there. */
227
+ moduleOption: string;
228
+ /** Runs a command string; the options end there. */
229
+ commandOption: string;
230
+ /** Other options followed by a separate value. */
231
+ valueOptions: readonly string[];
232
+ /**
233
+ * One-letter options may be grouped after one dash (-Im pip): flags, then perhaps an option with a value, which is
234
+ * the rest of the word or the next word.
235
+ */
236
+ groupedShortOptions: boolean;
237
+ }
238
+ /** The JSON form of the simple repository API of a Python package index (PEP 691, PEP 700, PEP 792). */
239
+ interface PypiIndexKnowledge {
240
+ /** Base URL; a project's page is the normalized name followed by a slash. */
241
+ url: string;
242
+ /** Accept header that asks for the JSON form. */
243
+ accept: string;
244
+ /** Status of a project the index does not know. */
245
+ notFoundStatus: number;
246
+ /** Key of the list of distribution files, and the keys of a file. */
247
+ filesField: string;
248
+ filenameField: string;
249
+ /** ISO 8601 upload time in UTC; optional, since API version 1.1. */
250
+ uploadTimeField: string;
251
+ /** true or a reason string when the file is yanked. */
252
+ yankedField: string;
253
+ /** Key of the list of all versions of the project, since API version 1.1. */
254
+ versionsField: string;
255
+ /** Key of the project status object, its status and its reason, since API version 1.4. */
256
+ statusField: string;
257
+ statusValueField: string;
258
+ statusReasonField: string;
259
+ }
260
+ /** Project status markers (PEP 792): which mean that no file may be downloaded, and which installers may warn about. */
261
+ interface ProjectStatusKnowledge {
262
+ /** The index offers no file of the project. */
263
+ blocked: readonly string[];
264
+ /** Installers may warn about the project. */
265
+ warned: readonly string[];
266
+ }
267
+ /** Claude Code hooks that run before a tool call and can block it (used by depcurfew check --hook claude-code). */
268
+ interface ClaudeCodeHooksKnowledge {
269
+ /** Project settings file that can be committed. */
270
+ settingsFile: string;
271
+ event: string;
272
+ /** Separates the tool names of a matcher that lists several, each matched exactly. */
273
+ matcherSeparator: string;
274
+ /** Fields of the JSON a command hook reads on stdin. */
275
+ input: {
276
+ event: string;
277
+ tool: string;
278
+ command: string;
279
+ cwd: string;
280
+ };
281
+ /** Fields of the JSON object a hook prints to decide. */
282
+ output: {
283
+ container: string;
284
+ event: string;
285
+ decision: string;
286
+ reason: string;
287
+ };
288
+ decisions: {
289
+ deny: string;
290
+ ask: string;
291
+ };
292
+ /** A command hook that exits with this code blocks the call; stderr is the reason. */
293
+ blockingExitCode: number;
294
+ /** The handler field that blocks the call when the hook fails or times out, and the version that has it. */
295
+ onFailure: {
296
+ field: string;
297
+ value: string;
298
+ since: string;
299
+ };
300
+ /** Placeholder for the project root in a hook command. */
301
+ projectDir: string;
302
+ /** Keys of a settings file: the hooks map, a matcher group and a command handler run without a shell. */
303
+ settings: {
304
+ hooks: string;
305
+ matcher: string;
306
+ handlers: string;
307
+ type: string;
308
+ commandType: string;
309
+ command: string;
310
+ args: string;
311
+ };
312
+ }
313
+ /** A Claude Code tool that runs shell commands, by the name a matcher and the event use, and the shell it runs them in. */
314
+ interface ClaudeCodeShellTool {
315
+ tool: string;
316
+ shell: 'bash' | 'powershell';
317
+ }
177
318
  /**
178
319
  * Annotation workflow commands: `::{command} {property}={value},…::{message}` on a line of its own. Values are
179
320
  * escaped by replacing each character listed in `escape` (in order), properties also with `escapeProperty`.
@@ -202,6 +343,33 @@ interface StepSummaryKnowledge {
202
343
  variable: string;
203
344
  maxBytes: number;
204
345
  }
346
+ interface GitIgnoreKnowledge {
347
+ /** The per-directory pattern file, e.g. .gitignore. */
348
+ file: string;
349
+ /** The repository's own pattern file, relative to the working tree. */
350
+ excludeFile: string;
351
+ /** The index, whose entries are the tracked files. */
352
+ indexFile: string;
353
+ /** Whether a pattern also hides a file git already tracks (it does not). */
354
+ trackedFilesIgnored: boolean;
355
+ }
356
+ /** npm's shims for the programs of packages on Windows. */
357
+ interface CommandShimsKnowledge {
358
+ /** The files written next to each program (besides a sh script). */
359
+ extensions: readonly string[];
360
+ /** The shim cmd.exe runs, which passes its arguments on with `argumentsVariable`, so they are read once more. */
361
+ batchExtension: string;
362
+ argumentsVariable: string;
363
+ }
364
+ /** The git setting that makes .gitignore patterns and index lookups ignore case. */
365
+ interface GitIgnoreCaseKnowledge {
366
+ /** The repository's configuration file, relative to the working tree. */
367
+ configFile: string;
368
+ section: string;
369
+ key: string;
370
+ /** Values read as true (case-insensitive); a key without a value is true too. */
371
+ trueValues: readonly string[];
372
+ }
205
373
  interface EnvironmentKnowledge {
206
374
  corepack: {
207
375
  packageManagerField: Fact<PackageManagerFieldKnowledge>;
@@ -228,14 +396,71 @@ interface EnvironmentKnowledge {
228
396
  pnpmSetup: Fact<SetupActionKnowledge>;
229
397
  setupBun: Fact<SetupActionKnowledge>;
230
398
  setupDeno: Fact<SetupActionKnowledge>;
399
+ setupPython: Fact<SetupActionKnowledge>;
400
+ setupUv: Fact<SetupActionKnowledge>;
401
+ /** Third-party actions that install Poetry; Poetry has no official action. */
402
+ installPoetry: Fact<SetupActionKnowledge>;
403
+ actionsPoetry: Fact<SetupActionKnowledge>;
231
404
  codeScanningUpload: Fact<CodeScanningUploadKnowledge>;
232
405
  stopCommands: Fact<StopCommandsKnowledge>;
233
406
  annotations: Fact<AnnotationsKnowledge>;
234
407
  annotationLimits: Fact<AnnotationLimitsKnowledge>;
235
408
  stepSummary: Fact<StepSummaryKnowledge>;
236
409
  };
410
+ npmRegistry: {
411
+ api: Fact<NpmRegistryKnowledge>;
412
+ notFoundStatus: Fact<number>;
413
+ /** Search endpoint, its result limit and the field with weekly downloads. */
414
+ search: Fact<{
415
+ path: string;
416
+ maxSize: number;
417
+ weeklyDownloadsField: string;
418
+ }>;
419
+ };
420
+ pypiIndex: {
421
+ api: Fact<PypiIndexKnowledge>;
422
+ projectStatus: Fact<ProjectStatusKnowledge>;
423
+ };
424
+ claudeCode: {
425
+ hooks: Fact<ClaudeCodeHooksKnowledge>;
426
+ /** The tools whose PreToolUse events carry a shell command in the hooks fact's input.command. */
427
+ shellTools: Fact<readonly ClaudeCodeShellTool[]>;
428
+ };
429
+ git: {
430
+ /** Where git reads the patterns of files it does not pick up, and the index that lists the tracked files. */
431
+ ignore: Fact<GitIgnoreKnowledge>;
432
+ ignoreCase: Fact<GitIgnoreCaseKnowledge>;
433
+ };
434
+ windows: {
435
+ commandShims: Fact<CommandShimsKnowledge>;
436
+ /** Files whose arguments PowerShell hands to cmd.exe as a raw command line. */
437
+ batchArguments: Fact<{
438
+ extensions: readonly string[];
439
+ }>;
440
+ };
441
+ python: {
442
+ /** How Python package names are compared: lowercased, with every run of these characters replaced by `replacement`. */
443
+ nameNormalization: Fact<{
444
+ separators: string;
445
+ replacement: string;
446
+ }>;
447
+ /** Python package names: ASCII letters and digits, `.`, `_` and `-`, starting and ending with a letter or digit. */
448
+ namePattern: Fact<string>;
449
+ /** The project file of a Python project, whose [tool.<name>] tables hold tool settings. */
450
+ projectFile: Fact<string>;
451
+ /** The file extension of a wheel, a built distribution that installs without running package code. */
452
+ wheelExtension: Fact<string>;
453
+ /** File extensions of source distributions on an index, which are built when installed. */
454
+ sourceDistributionExtensions: Fact<readonly string[]>;
455
+ interpreter: Fact<PythonInterpreterKnowledge>;
456
+ /** Hatch, which has no supply-chain settings of its own: how a project shows it uses Hatch. */
457
+ hatch: Fact<{
458
+ pyprojectTable: string;
459
+ configFile: string;
460
+ }>;
461
+ };
237
462
  }
238
- type PackageManagerId = Extract<ToolId, 'npm' | 'pnpm' | 'yarn' | 'bun' | 'deno'>;
463
+ type PackageManagerId = Extract<ToolId, 'npm' | 'pnpm' | 'yarn' | 'bun' | 'deno' | 'uv' | 'pip' | 'poetry' | 'pdm' | 'pipenv'>;
239
464
  type BotId = Extract<ToolId, 'renovate' | 'dependabot'>;
240
465
  /** Why a package manager is considered in use. Ordered from the strongest signal. */
241
466
  type EvidenceKind = 'lockfile' | 'packageManager' | 'devEngines' | 'config' | 'ci' | 'default';
@@ -244,7 +469,8 @@ interface Evidence {
244
469
  /** Repository-relative path with `/` separators. */
245
470
  file: string;
246
471
  }
247
- type VersionSource = 'packageManager' | 'devEngines' | 'volta' | 'tool-versions' | 'github-actions';
472
+ /** tool-config: a version the tool's own configuration requires, e.g. required-version in [tool.uv]. */
473
+ type VersionSource = 'packageManager' | 'tool-config' | 'devEngines' | 'volta' | 'tool-versions' | 'github-actions';
248
474
  /** A version declared somewhere in the repository, as written (it may be a range or an expression). */
249
475
  type VersionDeclaration = ({
250
476
  version: string;
@@ -398,6 +624,70 @@ declare const policyShape: Shape<{
398
624
  type Policy = Input<typeof policyShape>;
399
625
  /** The policy after validation: defaults filled in, durations parsed. */
400
626
  type ResolvedPolicy = Output<typeof policyShape>;
627
+ declare const APPLY_WARNINGS: {
628
+ readonly 'nothing-to-configure': "depcurfew found no supported package manager or bot, so nothing is configured.";
629
+ readonly 'file-unreadable': "A file that shows which tools a project uses cannot be read or parsed (too large, a symlink out of the repository, no read permission, invalid), so depcurfew may configure fewer tools than the project uses.";
630
+ readonly 'tool-not-supported': "A detected tool is not supported (Yarn Classic), so nothing is written for it.";
631
+ readonly 'tool-only-in-ci': "A tool appears only in a CI setup step, with no manifest, lockfile or configuration in the repository, so targets \"auto\" does not configure it.";
632
+ readonly 'version-not-pinned': "No exact version of a package manager is pinned, so depcurfew configures for the latest version it knows.";
633
+ readonly 'version-too-old': "The pinned version of a tool lacks a setting the policy needs, so that setting is not written.";
634
+ readonly 'cooldown-not-written': "The release-age delay cannot be written for a tool.";
635
+ readonly 'cooldown-shortened': "A release-age delay set outside depcurfew's markers (in JSON files also one an earlier apply wrote) is longer than the policy, and apply replaces it with the shorter one.";
636
+ readonly 'cooldown-environment-only': "pip reads no project configuration, so the release-age delay has to be set in the environment where pip runs.";
637
+ readonly 'exclude-pattern-unsupported': "A cooldown exception uses a wildcard the tool does not support, so it is not written.";
638
+ readonly 'exclude-unsupported': "The tool has no setting for cooldown exceptions, so the release-age delay applies to every package.";
639
+ readonly 'allow-pattern-unsupported': "An installScripts.allow entry is a pattern, but the tool takes package names only, so it is not written.";
640
+ readonly 'allow-all-unsupported': "The tool has no setting that allows every install script, so installScripts \"allow\" is not written.";
641
+ readonly 'allow-all-replaced': "A setting that allowed every install script was replaced, because the policy denies install scripts.";
642
+ readonly 'allow-list-entry-outside-policy': "An install-script allow-list in the repository lets a package run scripts the policy does not allow; apply does not remove it.";
643
+ readonly 'built-in-list-replaced': "Bun's built-in list of trusted packages is replaced by the policy's allow-list.";
644
+ readonly 'scripts-not-enforceable': "The pinned npm or pnpm runs every install script and has no allow-list, so \"deny\" cannot be enforced.";
645
+ readonly 'before-overrides-cooldown': "before= in .npmrc takes the place of the release-age delay apply writes, or makes npm stop with an error next to it.";
646
+ readonly 'exact-version-gap': "The pinned pnpm skips the release-age delay for packages requested by exact version.";
647
+ readonly 'source-not-enforceable': "The tool has no setting for a dependency source the policy denies; doctor checks the lockfile instead.";
648
+ readonly 'source-switch-indirect-only': "The pnpm setting for git and tarball sources stops them only for indirect dependencies.";
649
+ readonly 'source-switch-combined': "One pnpm setting covers git and tarball sources, so denying one denies both.";
650
+ readonly 'forced-source-build': "Python packages on the allow-list are always built from source, because no tool can only allow a build.";
651
+ readonly 'build-switch-scope': "The tool's switch forbids building only packages from an index; git, local and archive dependencies are still built.";
652
+ readonly 'build-switch-install-only': "Poetry applies its build switch when installing; poetry lock may still build source distributions.";
653
+ readonly 'build-switch-environment-only': "The tool has no project setting that forbids building from source, so it has to be set in the environment where the tool runs.";
654
+ readonly 'requirements-file-missing': "There is no requirements file to write pip options into.";
655
+ readonly 'manifest-missing': "A setting belongs in a project manifest (package.json, deno.json, pyproject.toml, Pipfile) that does not exist, and apply does not create manifests.";
656
+ readonly 'lockfile-out-of-date': "The lockfile records, or is checked against, a setting apply changed, so it has to be locked again.";
657
+ readonly 'file-ignored-by-git': "A file apply writes matches a .gitignore rule, so the change may never reach the repository.";
658
+ readonly 'bot-config-missing': "A bot has no configuration file, and depcurfew does not create one.";
659
+ readonly 'bot-config-not-edited': "A bot configuration file is in a place or format depcurfew does not edit; a snippet shows what to add.";
660
+ readonly 'bot-entries-missing': "The Dependabot configuration has no entries for the package managers in use.";
661
+ readonly 'security-delay-unsupported': "Dependabot cannot delay security updates, so securityUpdates \"delay\" is not applied.";
662
+ };
663
+ declare const APPLY_ERRORS: {
664
+ readonly 'cannot-edit': "A file cannot be read or edited safely (invalid syntax, a symlink, a style depcurfew does not edit), so nothing is written.";
665
+ readonly 'cannot-write': "A file cannot be written (no permission, a read-only file system, a full disk, a directory or symlink in its place); the files already written are put back, so nothing changes.";
666
+ readonly 'policy-missing': "The directory has no depcurfew.json.";
667
+ readonly 'policy-invalid': "depcurfew.json does not follow the schema; issues lists the problems.";
668
+ readonly 'policy-unreadable': "depcurfew.json cannot be read.";
669
+ readonly 'policy-exists': "depcurfew init found a depcurfew.json and writes it only with --force.";
670
+ readonly 'directory-not-found': "The directory given on the command line does not exist or cannot be opened.";
671
+ readonly 'not-a-directory': "The path given on the command line is a file, not a directory.";
672
+ };
673
+ type ApplyWarningCode = keyof typeof APPLY_WARNINGS;
674
+ type ApplyErrorCode = keyof typeof APPLY_ERRORS;
675
+ interface ApplyWarning {
676
+ code: ApplyWarningCode;
677
+ /** The tool it concerns, when there is one. */
678
+ tool?: ToolId;
679
+ /** The file it concerns, when there is one, and the line when it is known. */
680
+ location?: {
681
+ file: string;
682
+ line?: number;
683
+ };
684
+ /** What happened, for people; it starts with the tool's name when it concerns one. */
685
+ message: string;
686
+ /** What to do about it, when there is something to do. */
687
+ fix?: string;
688
+ }
689
+ /** A warning as one line of text, as ApplyReport.warnings has it. */
690
+ declare function warningText(warning: ApplyWarning): string;
401
691
  interface FileChange {
402
692
  /** Repository-relative path with `/` separators. */
403
693
  file: string;
@@ -405,6 +695,30 @@ interface FileChange {
405
695
  before: string | null;
406
696
  after: string;
407
697
  }
698
+ /**
699
+ * A file could not be written. writeChanges has removed its temporary files and put back the files it had already
700
+ * replaced, so nothing changed, except for the files in `unrestored` (whose earlier text is in `backups` when it could
701
+ * be written) and the temporary files in `leftovers`. The message says why, without the file name.
702
+ */
703
+ declare class WriteFailed extends Error {
704
+ readonly file: string;
705
+ /** Why, in plain words. */
706
+ readonly reason: string;
707
+ /** Files already replaced that could not be put back (normally none). */
708
+ readonly unrestored: string[];
709
+ /** For files in `unrestored`: the temporary file, next to it, that holds its earlier text. */
710
+ readonly backups: Readonly<Record<string, string>>;
711
+ /** Temporary files that could not be removed, relative to the root. */
712
+ readonly leftovers: string[];
713
+ /** Another program holds the file, or (on Windows) it is read-only. */
714
+ readonly busy: boolean;
715
+ readonly platform: NodeJS.Platform;
716
+ constructor(file: string, error: unknown, unrestored: string[], details?: {
717
+ platform?: NodeJS.Platform;
718
+ backups?: Record<string, string>;
719
+ leftovers?: string[];
720
+ });
721
+ }
408
722
  interface Snippet {
409
723
  tool: ToolId;
410
724
  file: string;
@@ -418,7 +732,10 @@ interface EditFailure {
418
732
  }
419
733
  interface ApplyReport {
420
734
  changes: FileChange[];
735
+ /** The warnings as one line of text each. */
421
736
  warnings: string[];
737
+ /** The same warnings with a code, the tool, the file and what to do (apply --json). */
738
+ warningDetails: ApplyWarning[];
422
739
  snippets: Snippet[];
423
740
  /** Files that could not be edited, as messages. When there are any, nothing is written. */
424
741
  errors: string[];
@@ -431,18 +748,137 @@ declare function botDuration(policy: Duration, managerMinutes: readonly number[]
431
748
  declare function planApply(root: string, policy: ResolvedPolicy, detection: Detection, tools?: readonly ToolKnowledge[]): ApplyReport;
432
749
  declare class PolicyError extends Error {
433
750
  readonly issues: string[];
434
- constructor(message: string, issues?: string[]);
751
+ readonly code: ApplyErrorCode;
752
+ /** What to do, when the message does not say it. */
753
+ readonly fix: string | undefined;
754
+ constructor(message: string, issues?: string[], code?: ApplyErrorCode, fix?: string);
755
+ /** The message and what to do, as one line of text. */
756
+ get text(): string;
435
757
  }
436
758
  declare function loadPolicy(root: string): ResolvedPolicy;
759
+ /**
760
+ * A file could not be written (code cannot-write). The files apply had already replaced were put back, so the
761
+ * repository is as it was, unless `unrestored` lists files. `report` is the plan that was being written.
762
+ */
763
+ declare class ApplyWriteError extends Error {
764
+ readonly code: "cannot-write";
765
+ /** The file that could not be written, relative to the repository. */
766
+ readonly file: string;
767
+ /** What to do. */
768
+ readonly fix: string;
769
+ /** Files already replaced that could not be put back (normally none). */
770
+ readonly unrestored: string[];
771
+ readonly report: ApplyReport;
772
+ constructor(failure: WriteFailed, report: ApplyReport);
773
+ }
437
774
  /**
438
775
  * Reads the policy, detects the project, plans the changes and (unless told otherwise) writes them. If any file
439
776
  * could not be edited, nothing is written, so a run either applies the whole policy or leaves the repository as it was.
777
+ * A file that cannot be written throws ApplyWriteError after the files already written are put back.
440
778
  */
441
779
  declare function applyProject(root: string, options?: {
442
780
  write?: boolean;
443
781
  }): ApplyReport;
444
782
  /** A unified diff with three lines of context; empty when the texts are equal. `before` is null for a new file. */
445
783
  declare function unifiedDiff(file: string, before: string | null, after: string, context?: number): string;
784
+ interface SimilarName {
785
+ /** The popular package the name resembles. */
786
+ match: string;
787
+ /** edit: a letter or two differ; separators: only -, _ or . differ. */
788
+ reason: 'edit' | 'separators';
789
+ }
790
+ declare class PopularNames {
791
+ #private;
792
+ constructor(names: readonly string[]);
793
+ has(name: string): boolean;
794
+ /** The popular package `name` resembles, or undefined (also when `name` is itself popular). */
795
+ similar(name: string): SimilarName | undefined;
796
+ }
797
+ type SpecKind = 'registry' | 'git' | 'remoteTarball' | 'file' | 'directory' | 'workspace' | 'other';
798
+ type FindingCode = 'invalid-spec' | 'forbidden-source' | 'unchecked-source' | 'unverified' | 'not-found' | 'no-version' | 'too-young' | 'install-scripts' | 'similar-name' | 'deprecated' | 'yanked' | 'archived' | 'quarantined' | 'insecure-registry';
799
+ interface CheckFinding {
800
+ level: 'error' | 'warning';
801
+ code: FindingCode;
802
+ message: string;
803
+ /** What to do about it. */
804
+ fix: string;
805
+ }
806
+ interface PackageReport {
807
+ spec: string;
808
+ kind: SpecKind | 'invalid';
809
+ name?: string;
810
+ version?: string;
811
+ publishedAt?: string;
812
+ latest?: {
813
+ version: string;
814
+ publishedAt?: string;
815
+ };
816
+ /** npm: the dist-tag the version was picked by, when it is not the default one (--tag, npm_config_tag, tag in .npmrc). */
817
+ tag?: string;
818
+ /** Python: pre-releases allowed or forbidden by the command. */
819
+ prereleases?: 'allow' | 'deny';
820
+ /** Python: the command builds the package from source (true) or installs wheels only (false). */
821
+ buildFromSource?: boolean;
822
+ findings: CheckFinding[];
823
+ }
824
+ /** A spec with what the install command changes about the version or the build. */
825
+ interface CheckRequest {
826
+ spec: string;
827
+ /** npm: the dist-tag npm prefers for a name without a version or with a range; the registry's default when left out. */
828
+ tag?: string;
829
+ /** The package manager is not npm, so tag= in .npmrc (CheckOptions.defaultTag) does not choose its version. */
830
+ ignoresConfigTag?: boolean;
831
+ /** Python: pre-releases allowed or forbidden; PEP 440's default when left out. */
832
+ prereleases?: 'allow' | 'deny';
833
+ /** Python: the package is built from source even when it has wheels (true), or only wheels are used (false). */
834
+ buildFromSource?: boolean;
835
+ }
836
+ interface CheckOptions {
837
+ policy: ResolvedPolicy;
838
+ fetch: FetchLike;
839
+ now: Date;
840
+ registry: string;
841
+ popular: PopularNames;
842
+ /** Registries the project assigns to scopes (@scope:registry in .npmrc); those packages are not looked up. */
843
+ scopeRegistries?: ReadonlyMap<string, string>;
844
+ /** The Python package index pypi: packages are looked up in; PyPI when not given. */
845
+ index?: string;
846
+ /** Popular Python projects, by normalized name, for look-alike names. */
847
+ popularPython?: PopularNames;
848
+ /** npm: the dist-tag npm prefers when a request names none (tag in .npmrc); the registry's default when left out. */
849
+ defaultTag?: string;
850
+ /**
851
+ * Whether a finding about install scripts quotes them (the default) or names them only, for the hook: their body is
852
+ * written by the package's publisher and should not reach the model as part of depcurfew's reason.
853
+ */
854
+ scriptBodies?: boolean;
855
+ }
856
+ declare function checkSpec(request: string | CheckRequest, options: CheckOptions): Promise<PackageReport>;
857
+ /** Checks the specs a few at a time; the reports keep the order of the specs. */
858
+ declare function checkSpecs(specs: readonly (string | CheckRequest)[], options: CheckOptions): Promise<PackageReport[]>;
859
+ interface InstallRequest {
860
+ tool: ToolId;
861
+ /** The command as written, for messages. */
862
+ command: string;
863
+ spec: string;
864
+ /** npm: the dist-tag the command makes npm prefer (--tag, npm_config_tag). */
865
+ tag?: string;
866
+ /** Python: the command allows pre-releases of the package, or forbids them; left out, the tool's default applies. */
867
+ prereleases?: 'allow' | 'deny';
868
+ /** Python: the command makes the tool build the package from source (true) or install wheels only (false). */
869
+ buildFromSource?: boolean;
870
+ }
871
+ interface CommandScan {
872
+ requests: InstallRequest[];
873
+ /** Why an install command could not be read; undefined when every one could. */
874
+ unreadable: string | undefined;
875
+ }
876
+ interface ScanOptions {
877
+ /** The shell that runs the line: a POSIX shell (the default; also Git Bash) or PowerShell. */
878
+ shell?: 'posix' | 'powershell';
879
+ }
880
+ /** The packages a command line installs or downloads to run with npm, pnpm, Yarn, Bun, Deno, pip, uv, Poetry, PDM, Pipenv or pipx. */
881
+ declare function scanCommandLine(line: string, options?: ScanOptions): CommandScan;
446
882
  /**
447
883
  * Describes the repository at `root`. Never executes anything from the repository and never leaves it;
448
884
  * problems with individual files become warnings.
@@ -465,6 +901,8 @@ declare class Repository {
465
901
  listDirectories(path: string): string[];
466
902
  /** Parses a file with the given parser, reporting failures as warnings. */
467
903
  parse<T>(path: string, parser: (text: string) => T): T | undefined;
904
+ /** Like parse, but each file is read and parsed once per parser: several rules read the same file. */
905
+ parseOnce<T>(path: string, parser: (text: string) => T): T | undefined;
468
906
  relative(absolute: string): string;
469
907
  }
470
908
  /** The SARIF levels: an error fails `doctor`, warnings and notes do not. */
@@ -517,7 +955,8 @@ interface Rule {
517
955
  }
518
956
  interface DoctorReport {
519
957
  schemaVersion: 1;
520
- policy: 'valid' | 'missing';
958
+ /** invalid: depcurfew.json does not follow the schema or cannot be read (only with invalidPolicy: 'report'). */
959
+ policy: 'valid' | 'missing' | 'invalid';
521
960
  findings: Finding[];
522
961
  notices: Notice[];
523
962
  }
@@ -535,11 +974,21 @@ declare function countLevels(findings: readonly Finding[]): Record<'error' | 'wa
535
974
  declare function renderText(report: DoctorReport, rules: readonly Rule[], colors?: Paint): string;
536
975
  /** The text of a line in a repository file, to give regions the end column GitHub asks for. */
537
976
  type LineReader = (file: string, line: number) => string | undefined;
538
- declare function toSarif(report: DoctorReport, rules: readonly Rule[], version: string, readLine?: LineReader): object;
539
- /** Runs the rules on a repository. Throws PolicyError when depcurfew.json exists but is invalid. */
977
+ /** Why doctor could not run, with a stable code. */
978
+ interface SarifError {
979
+ code: string;
980
+ message: string;
981
+ fix?: string;
982
+ }
983
+ declare function toSarif(report: DoctorReport, rules: readonly Rule[], version: string, readLine?: LineReader, errors?: readonly SarifError[]): object;
984
+ /**
985
+ * Runs the rules on a repository. When depcurfew.json exists but is invalid, it throws PolicyError, or with
986
+ * invalidPolicy: 'report' runs the rules that need no policy and reports the problems as an invalid-policy finding.
987
+ */
540
988
  declare function runDoctor(root: string, options?: {
541
989
  rules?: readonly Rule[];
542
990
  tools?: readonly ToolKnowledge[];
991
+ invalidPolicy?: 'throw' | 'report';
543
992
  }): DoctorReport;
544
993
  type NumericDelayUnit = Exclude<DelayUnit, 'duration-string'>;
545
994
  declare const DURATION_STRING_UNITS: readonly ["minutes", "hours", "days", "weeks"];
@@ -559,6 +1008,7 @@ type CooldownConversion = {
559
1008
  ok: false;
560
1009
  tool: ToolId;
561
1010
  reason: string;
1011
+ fix: string;
562
1012
  };
563
1013
  declare function convertMinutes(minutes: number, unit: NumericDelayUnit): Converted<number>;
564
1014
  /** Writes minutes as a duration string such as `36 hours` or `3 days`. */
@@ -606,4 +1056,4 @@ declare function validatePolicy(document: unknown, tools?: readonly ToolKnowledg
606
1056
  /** Parses and validates the text of a policy file. */
607
1057
  declare function parsePolicy(source: string, tools?: readonly ToolKnowledge[]): PolicyResult;
608
1058
  declare function formatIssue(issue: PolicyIssue): string;
609
- export { type ApplyReport, type CliIo, type Converted, type CooldownConversion, DEFAULT_POLICY, type Detection, type DetectionWarning, type DoctorReport, type Duration, type DurationResult, type DurationStringUnit, EXIT_FAILURE, EXIT_OK, EXIT_USAGE, type FileChange, type Finding, type Level, type Location, type Notice, type NumericDelayUnit, POLICY_FILE, type PackageManagerInfo, type Policy, PolicyError, type PolicyIssue, type PolicyResult, RULES, type ResolvedPolicy, type Rule, SCHEMA_URL, type Snippet, type VersionDeclaration, applyProject, botDuration, convertCooldown, convertMinutes, countLevels, detectProject, formatDuration, formatIssue, getVersion, loadPolicy, parseDuration, parsePolicy, planApply, renderPolicy, renderText, run, runDoctor, toDurationString, toSarif, unifiedDiff, validatePolicy };
1059
+ export { APPLY_ERRORS, APPLY_WARNINGS, type ApplyErrorCode, type ApplyReport, type ApplyWarning, type ApplyWarningCode, ApplyWriteError, type CheckFinding, type CheckOptions, type CliIo, type CommandScan, type Converted, type CooldownConversion, DEFAULT_POLICY, type Detection, type DetectionWarning, type DoctorReport, type Duration, type DurationResult, type DurationStringUnit, EXIT_FAILURE, EXIT_OK, EXIT_USAGE, type FileChange, type Finding, type FindingCode, type InstallRequest, type Level, type Location, type Notice, type NumericDelayUnit, POLICY_FILE, type PackageManagerInfo, type PackageReport, type Policy, PolicyError, type PolicyIssue, type PolicyResult, PopularNames, RULES, type ResolvedPolicy, type Rule, SCHEMA_URL, type ScanOptions, type Snippet, type VersionDeclaration, applyProject, botDuration, checkSpec, checkSpecs, convertCooldown, convertMinutes, countLevels, detectProject, formatDuration, formatIssue, getVersion, loadPolicy, parseDuration, parsePolicy, planApply, renderPolicy, renderText, run, runDoctor, scanCommandLine, toDurationString, toSarif, unifiedDiff, validatePolicy, warningText };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export { w as DEFAULT_POLICY, s as EXIT_FAILURE, c as EXIT_OK, l as EXIT_USAGE, T as POLICY_FILE, d as PolicyError, o as RULES, E as SCHEMA_URL, f as applyProject, p as botDuration, y as convertCooldown, b as convertMinutes, i as countLevels, O as detectProject, S as formatDuration, g as formatIssue, k as getVersion, m as loadPolicy, C as parseDuration, _ as parsePolicy, h as planApply, D as renderPolicy, a as renderText, t as run, n as runDoctor, x as toDurationString, r as toSarif, u as unifiedDiff, v as validatePolicy } from "./run-D7KMlsVh.js";
1
+ export { b as APPLY_ERRORS, x as APPLY_WARNINGS, m as ApplyWriteError, M as DEFAULT_POLICY, d as EXIT_FAILURE, f as EXIT_OK, p as EXIT_USAGE, N as POLICY_FILE, h as PolicyError, s as PopularNames, o as RULES, P as SCHEMA_URL, g as applyProject, _ as botDuration, l as checkSpec, u as checkSpecs, D as convertCooldown, O as convertMinutes, i as countLevels, I as detectProject, A as formatDuration, w as formatIssue, L as getVersion, v as loadPolicy, j as parseDuration, T as parsePolicy, y as planApply, F as renderPolicy, a as renderText, t as run, n as runDoctor, c as scanCommandLine, k as toDurationString, r as toSarif, C as unifiedDiff, E as validatePolicy, S as warningText } from "./run-CvdVkFPn.js";