depcurfew 1.0.0 → 1.2.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,19 +1,7 @@
1
- declare const EXIT_OK = 0;
2
- declare const EXIT_FAILURE = 1;
3
- declare const EXIT_USAGE = 2;
4
- interface CliIo {
5
- stdout(text: string): void;
6
- stderr(text: string): void;
7
- /** Whether each stream may be colored (see color.ts); absent means plain text. */
8
- colors?: {
9
- stdout: boolean;
10
- stderr: boolean;
11
- };
12
- }
13
- declare function run(argv: readonly string[], output: CliIo): Promise<number>;
14
- declare function getVersion(): string;
15
- type ToolId = 'npm' | 'pnpm' | 'yarn' | 'bun' | 'deno' | 'renovate' | 'dependabot';
1
+ type ToolId = 'npm' | 'pnpm' | 'yarn' | 'bun' | 'deno' | 'uv' | 'pip' | 'poetry' | 'pdm' | 'pipenv' | 'renovate' | 'dependabot';
16
2
  type ToolKind = 'package-manager' | 'update-bot';
3
+ /** The registry a package manager installs from; package names in the policy are compared per ecosystem. */
4
+ type Ecosystem = 'npm' | 'pypi';
17
5
  interface Evidence$1<T> {
18
6
  value: T;
19
7
  /** URL of official documentation or changelog. */
@@ -95,6 +83,8 @@ interface SourcesKnowledge {
95
83
  interface WorkspaceDeclaration {
96
84
  files: readonly string[];
97
85
  key: string;
86
+ /** Key of a list of patterns that exclude packages, next to the package list; such patterns win. */
87
+ excludeKey?: string;
98
88
  }
99
89
  /** Facts for recognising a package manager in a repository. */
100
90
  interface ProjectKnowledge {
@@ -102,25 +92,127 @@ interface ProjectKnowledge {
102
92
  lockfiles: Fact<Versioned<readonly string[]>>;
103
93
  /** Files that make a directory a project or a workspace package for this tool. */
104
94
  manifests: Fact<readonly string[]>;
105
- workspaces: Fact<WorkspaceDeclaration>;
95
+ /** `null` when the tool has no workspaces. In TOML files the key is a dotted path to a table. */
96
+ workspaces: Fact<WorkspaceDeclaration | null>;
106
97
  /** Key of the package list when the workspace declaration is an object instead of a list; `null` if only lists are accepted. */
107
98
  workspacesObjectKey: Fact<string | null>;
108
99
  }
100
+ /** A platform family for user-level paths. */
101
+ type UserPlatform = 'linux' | 'darwin' | 'win32';
102
+ /** A base directory for a user-level file. */
103
+ interface UserDirectory {
104
+ /**
105
+ * Where it starts: an environment variable (used only when it is set and not empty, and with `absolute` only when
106
+ * it is an absolute path), or the home directory (HOME on Linux and macOS, USERPROFILE on Windows). With
107
+ * `relativeToCwd` the tool takes a relative value as it is, so it reads the file relative to the directory each
108
+ * command runs in; without it and without `absolute`, what the tool does with a relative value is not known.
109
+ */
110
+ from: {
111
+ env: string;
112
+ absolute?: boolean;
113
+ relativeToCwd?: boolean;
114
+ } | 'home';
115
+ /** Path below it, with / separators; empty for the directory itself. */
116
+ path: string;
117
+ /** Use this directory only when it already exists (pip on macOS). */
118
+ ifExists?: boolean;
119
+ /** Skip this directory while this environment variable is set and not empty (pip 26.2+ on macOS and XDG_DATA_HOME). */
120
+ unlessEnv?: string;
121
+ }
122
+ /** Where the user file is on some platforms: the first directory that applies is used. */
123
+ interface UserLocation {
124
+ platforms: readonly UserPlatform[];
125
+ directories: readonly UserDirectory[];
126
+ file: string;
127
+ }
128
+ /** The user-level configuration file of a range of versions. */
129
+ interface UserFile {
130
+ format: 'ini' | 'yaml' | 'toml';
131
+ locations: readonly UserLocation[];
132
+ /** INI only: the [sections] whose keys the tool reads, '' for the keys above the first header; absent: every key. */
133
+ sections?: readonly string[];
134
+ /** Other user-level files this version reads the same settings from, whose keys win over this file's. */
135
+ overriddenBy?: readonly UserFile[];
136
+ /** INI only: the tool reads a key with underscores as the same key with dashes (pip: only_binary is only-binary). */
137
+ underscoresAsDashes?: boolean;
138
+ /** YAML only: the tool's loader applies merge keys (<<: *defaults), so merged keys count as keys of the map. */
139
+ mergeKeys?: boolean;
140
+ }
141
+ /** What depcurfew writes with a user-level setting. */
142
+ type UserSettingPurpose = 'cooldown' | 'cooldownExclude' | 'cooldownStrict' | 'scriptsAllow' | 'scriptsStrict' | 'buildsDeny' | 'buildsAllow' | 'sourceGit' | 'sourceRemote' | 'sourceFile' | 'sourceDirectory' | 'sourcesExotic';
143
+ /** A setting the tool reads from its user file: the key there and the versions that read it from that file. */
144
+ interface UserSetting {
145
+ purpose: UserSettingPurpose;
146
+ key: string;
147
+ since: string;
148
+ /** First version that no longer reads it from the user file. */
149
+ until?: string;
150
+ }
151
+ /**
152
+ * The values of a switch that turns a protection off, given by the values the tool reads as off: every other value
153
+ * turns it on (fail closed), so a string "false" or "yes" that the tool takes for true is not missed. In INI files and
154
+ * environment variables values are compared as text after trimming, elsewhere with their type (false, 0, null, "").
155
+ */
156
+ interface UnlessOff<T> {
157
+ except: readonly T[];
158
+ }
159
+ /** A value in the user file that weakens every project that does not set the key itself (doctor --user). */
160
+ interface UserWeakening {
161
+ key: string;
162
+ /** 'any' for every value, the values that weaken, or every value except those the tool reads as off. */
163
+ values: 'any' | readonly (string | number | boolean)[] | UnlessOff<string | number | boolean | null>;
164
+ /** What it does, for the message. */
165
+ effect: string;
166
+ }
167
+ /** An environment variable that bypasses or overrides the user file or the policy (doctor --user). */
168
+ interface UserVariable {
169
+ /** The variable name; matched without regard to case when `anyCase`. */
170
+ name: string;
171
+ anyCase?: boolean;
172
+ /** 'any' when every value matters, the values that weaken, or every value except those the tool reads as off. */
173
+ values: 'any' | readonly string[] | UnlessOff<string>;
174
+ effect: string;
175
+ /** First version that reads the variable; every version when absent. */
176
+ since?: string;
177
+ /** First version that no longer reads it. */
178
+ until?: string;
179
+ /** The file the tool reads when the variable is unset, from the home directory (~/.npmrc): naming it changes nothing. */
180
+ defaultPath?: string;
181
+ }
182
+ /** The tool's user-level configuration, for `depcurfew apply --user` and `doctor --user`. */
183
+ interface UserConfigKnowledge {
184
+ /** The user file per version, oldest first; null for versions without a user file. */
185
+ files: Fact<Versioned<UserFile | null>>;
186
+ /** Settings depcurfew can write there. */
187
+ settings: Fact<readonly UserSetting[]>;
188
+ /** What a version does with a key it does not know in the user file: ignore it, warn, or fail. */
189
+ unknownKeys: Fact<'ignore' | 'warn' | 'fail'>;
190
+ /** Whether a value the installed version cannot parse makes every command fail; depcurfew then needs the exact version. */
191
+ invalidValueFails: Fact<boolean>;
192
+ /** Values in the user file that weaken projects. */
193
+ weakening: Fact<readonly UserWeakening[]>;
194
+ /** Environment variables that move or skip the user file or override settings. */
195
+ variables: Fact<readonly UserVariable[]>;
196
+ }
109
197
  interface ToolKnowledge {
110
198
  id: ToolId;
111
199
  name: string;
112
200
  kind: ToolKind;
201
+ /** Package managers only; `npm` when absent. */
202
+ ecosystem?: Ecosystem;
113
203
  cooldown: CooldownKnowledge;
114
204
  installScripts: InstallScriptsKnowledge;
115
205
  /** Package managers only: bots do not resolve dependency sources themselves. */
116
206
  sources?: SourcesKnowledge;
117
- /** Package managers only: how to recognise the tool in a repository. */
207
+ /** Package managers only: how to recognize the tool in a repository. */
118
208
  project?: ProjectKnowledge;
209
+ /** Package managers only: the user-level configuration (`--user`). */
210
+ user?: UserConfigKnowledge;
119
211
  /** Further sourced facts that matter for generators and `doctor`, keyed by a short name. */
120
212
  extra?: Readonly<Record<string, Fact<unknown>>>;
121
213
  }
122
- /** A tool that environment facts refer to: Node.js or a package manager. */
123
- type RuntimeOrTool = 'node' | ToolId;
214
+ /** A tool that environment facts refer to: Node.js, Python or a package manager. */
215
+ type RuntimeOrTool = 'node' | 'python' | ToolId;
124
216
  interface PackageManagerFieldKnowledge {
125
217
  /** Key in package.json, e.g. `packageManager`. */
126
218
  key: string;
@@ -174,6 +266,122 @@ interface StopCommandsKnowledge {
174
266
  stop: string;
175
267
  resume: string;
176
268
  }
269
+ /** The public npm registry, as depcurfew check reads it. */
270
+ interface NpmRegistryKnowledge {
271
+ url: string;
272
+ /** Path of a package document; scoped names have their slash escaped as %2f. */
273
+ packagePath: string;
274
+ /** Field of the full document that maps versions to publish times; the abbreviated install document lacks it. */
275
+ timeField: string;
276
+ /** Field of the document that maps tags (latest, next) to versions. */
277
+ distTagsField: string;
278
+ /** The tag every package has, which a spec without a version installs. */
279
+ defaultTag: string;
280
+ /** Field of the document that maps versions to their version objects. */
281
+ versionsField: string;
282
+ /** Field of a version object with the lifecycle scripts of its package.json. */
283
+ scriptsField: string;
284
+ /** Field of a version object that holds the deprecation message. */
285
+ deprecatedField: string;
286
+ /** Lifecycle scripts a package runs when it is installed as a dependency. */
287
+ installScripts: readonly string[];
288
+ }
289
+ /** How the Python interpreter runs a module (python -m pip) or a command string. */
290
+ interface PythonInterpreterKnowledge {
291
+ /** Executable names; versioned names such as python3.12 are the same. */
292
+ executables: readonly string[];
293
+ /** Runs the named module as a script; the options end there. */
294
+ moduleOption: string;
295
+ /** Runs a command string; the options end there. */
296
+ commandOption: string;
297
+ /** Other options followed by a separate value. */
298
+ valueOptions: readonly string[];
299
+ /**
300
+ * One-letter options may be grouped after one dash (-Im pip): flags, then perhaps an option with a value, which is
301
+ * the rest of the word or the next word.
302
+ */
303
+ groupedShortOptions: boolean;
304
+ }
305
+ /** The JSON form of the simple repository API of a Python package index (PEP 691, PEP 700, PEP 792). */
306
+ interface PypiIndexKnowledge {
307
+ /** Base URL; a project's page is the normalized name followed by a slash. */
308
+ url: string;
309
+ /** Accept header that asks for the JSON form. */
310
+ accept: string;
311
+ /** Status of a project the index does not know. */
312
+ notFoundStatus: number;
313
+ /** Key of the list of distribution files, and the keys of a file. */
314
+ filesField: string;
315
+ filenameField: string;
316
+ /** ISO 8601 upload time in UTC; optional, since API version 1.1. */
317
+ uploadTimeField: string;
318
+ /** true or a reason string when the file is yanked. */
319
+ yankedField: string;
320
+ /** Key of the list of all versions of the project, since API version 1.1. */
321
+ versionsField: string;
322
+ /** Key of the project status object, its status and its reason, since API version 1.4. */
323
+ statusField: string;
324
+ statusValueField: string;
325
+ statusReasonField: string;
326
+ }
327
+ /** Project status markers (PEP 792): which mean that no file may be downloaded, and which installers may warn about. */
328
+ interface ProjectStatusKnowledge {
329
+ /** The index offers no file of the project. */
330
+ blocked: readonly string[];
331
+ /** Installers may warn about the project. */
332
+ warned: readonly string[];
333
+ }
334
+ /** Claude Code hooks that run before a tool call and can block it (used by depcurfew check --hook claude-code). */
335
+ interface ClaudeCodeHooksKnowledge {
336
+ /** Project settings file that can be committed. */
337
+ settingsFile: string;
338
+ event: string;
339
+ /** Separates the tool names of a matcher that lists several, each matched exactly. */
340
+ matcherSeparator: string;
341
+ /** Fields of the JSON a command hook reads on stdin. */
342
+ input: {
343
+ event: string;
344
+ tool: string;
345
+ command: string;
346
+ cwd: string;
347
+ };
348
+ /** Fields of the JSON object a hook prints to decide. */
349
+ output: {
350
+ container: string;
351
+ event: string;
352
+ decision: string;
353
+ reason: string;
354
+ };
355
+ decisions: {
356
+ deny: string;
357
+ ask: string;
358
+ };
359
+ /** A command hook that exits with this code blocks the call; stderr is the reason. */
360
+ blockingExitCode: number;
361
+ /** The handler field that blocks the call when the hook fails or times out, and the version that has it. */
362
+ onFailure: {
363
+ field: string;
364
+ value: string;
365
+ since: string;
366
+ };
367
+ /** Placeholder for the project root in a hook command. */
368
+ projectDir: string;
369
+ /** Keys of a settings file: the hooks map, a matcher group and a command handler run without a shell. */
370
+ settings: {
371
+ hooks: string;
372
+ matcher: string;
373
+ handlers: string;
374
+ type: string;
375
+ commandType: string;
376
+ command: string;
377
+ args: string;
378
+ };
379
+ }
380
+ /** A Claude Code tool that runs shell commands, by the name a matcher and the event use, and the shell it runs them in. */
381
+ interface ClaudeCodeShellTool {
382
+ tool: string;
383
+ shell: 'bash' | 'powershell';
384
+ }
177
385
  /**
178
386
  * Annotation workflow commands: `::{command} {property}={value},…::{message}` on a line of its own. Values are
179
387
  * escaped by replacing each character listed in `escape` (in order), properties also with `escapeProperty`.
@@ -198,10 +406,56 @@ interface AnnotationLimitsKnowledge {
198
406
  perJob: number;
199
407
  }
200
408
  /** The Markdown job summary of a step: the environment variable naming its file and its size limit. */
409
+ /** Creating a check run with the workflow token, so that the result shows in the pull request's Checks tab. */
410
+ interface CheckRunsKnowledge {
411
+ /** POST endpoint, with {repository} as owner/name. */
412
+ endpoint: string;
413
+ apiVersion: string;
414
+ /** The workflow permission the token needs. */
415
+ permission: Record<string, string>;
416
+ /** Limit of output.summary, in characters. */
417
+ summaryMaxLength: number;
418
+ /** Variables the runner sets: API URL, owner/name, commit and the event payload file. */
419
+ variables: {
420
+ apiUrl: string;
421
+ repository: string;
422
+ sha: string;
423
+ eventPath: string;
424
+ };
425
+ /** Path in the event payload of the commit a pull request check belongs to (GITHUB_SHA is the merge commit there). */
426
+ pullRequestHeadSha: readonly string[];
427
+ }
201
428
  interface StepSummaryKnowledge {
202
429
  variable: string;
203
430
  maxBytes: number;
204
431
  }
432
+ interface GitIgnoreKnowledge {
433
+ /** The per-directory pattern file, e.g. .gitignore. */
434
+ file: string;
435
+ /** The repository's own pattern file, relative to the working tree. */
436
+ excludeFile: string;
437
+ /** The index, whose entries are the tracked files. */
438
+ indexFile: string;
439
+ /** Whether a pattern also hides a file git already tracks (it does not). */
440
+ trackedFilesIgnored: boolean;
441
+ }
442
+ /** npm's shims for the programs of packages on Windows. */
443
+ interface CommandShimsKnowledge {
444
+ /** The files written next to each program (besides a sh script). */
445
+ extensions: readonly string[];
446
+ /** The shim cmd.exe runs, which passes its arguments on with `argumentsVariable`, so they are read once more. */
447
+ batchExtension: string;
448
+ argumentsVariable: string;
449
+ }
450
+ /** The git setting that makes .gitignore patterns and index lookups ignore case. */
451
+ interface GitIgnoreCaseKnowledge {
452
+ /** The repository's configuration file, relative to the working tree. */
453
+ configFile: string;
454
+ section: string;
455
+ key: string;
456
+ /** Values read as true (case-insensitive); a key without a value is true too. */
457
+ trueValues: readonly string[];
458
+ }
205
459
  interface EnvironmentKnowledge {
206
460
  corepack: {
207
461
  packageManagerField: Fact<PackageManagerFieldKnowledge>;
@@ -228,14 +482,72 @@ interface EnvironmentKnowledge {
228
482
  pnpmSetup: Fact<SetupActionKnowledge>;
229
483
  setupBun: Fact<SetupActionKnowledge>;
230
484
  setupDeno: Fact<SetupActionKnowledge>;
485
+ setupPython: Fact<SetupActionKnowledge>;
486
+ setupUv: Fact<SetupActionKnowledge>;
487
+ /** Third-party actions that install Poetry; Poetry has no official action. */
488
+ installPoetry: Fact<SetupActionKnowledge>;
489
+ actionsPoetry: Fact<SetupActionKnowledge>;
231
490
  codeScanningUpload: Fact<CodeScanningUploadKnowledge>;
232
491
  stopCommands: Fact<StopCommandsKnowledge>;
233
492
  annotations: Fact<AnnotationsKnowledge>;
234
493
  annotationLimits: Fact<AnnotationLimitsKnowledge>;
235
494
  stepSummary: Fact<StepSummaryKnowledge>;
495
+ checkRuns: Fact<CheckRunsKnowledge>;
496
+ };
497
+ npmRegistry: {
498
+ api: Fact<NpmRegistryKnowledge>;
499
+ notFoundStatus: Fact<number>;
500
+ /** Search endpoint, its result limit and the field with weekly downloads. */
501
+ search: Fact<{
502
+ path: string;
503
+ maxSize: number;
504
+ weeklyDownloadsField: string;
505
+ }>;
506
+ };
507
+ pypiIndex: {
508
+ api: Fact<PypiIndexKnowledge>;
509
+ projectStatus: Fact<ProjectStatusKnowledge>;
510
+ };
511
+ claudeCode: {
512
+ hooks: Fact<ClaudeCodeHooksKnowledge>;
513
+ /** The tools whose PreToolUse events carry a shell command in the hooks fact's input.command. */
514
+ shellTools: Fact<readonly ClaudeCodeShellTool[]>;
515
+ };
516
+ git: {
517
+ /** Where git reads the patterns of files it does not pick up, and the index that lists the tracked files. */
518
+ ignore: Fact<GitIgnoreKnowledge>;
519
+ ignoreCase: Fact<GitIgnoreCaseKnowledge>;
520
+ };
521
+ windows: {
522
+ commandShims: Fact<CommandShimsKnowledge>;
523
+ /** Files whose arguments PowerShell hands to cmd.exe as a raw command line. */
524
+ batchArguments: Fact<{
525
+ extensions: readonly string[];
526
+ }>;
527
+ };
528
+ python: {
529
+ /** How Python package names are compared: lowercased, with every run of these characters replaced by `replacement`. */
530
+ nameNormalization: Fact<{
531
+ separators: string;
532
+ replacement: string;
533
+ }>;
534
+ /** Python package names: ASCII letters and digits, `.`, `_` and `-`, starting and ending with a letter or digit. */
535
+ namePattern: Fact<string>;
536
+ /** The project file of a Python project, whose [tool.<name>] tables hold tool settings. */
537
+ projectFile: Fact<string>;
538
+ /** The file extension of a wheel, a built distribution that installs without running package code. */
539
+ wheelExtension: Fact<string>;
540
+ /** File extensions of source distributions on an index, which are built when installed. */
541
+ sourceDistributionExtensions: Fact<readonly string[]>;
542
+ interpreter: Fact<PythonInterpreterKnowledge>;
543
+ /** Hatch, which has no supply-chain settings of its own: how a project shows it uses Hatch. */
544
+ hatch: Fact<{
545
+ pyprojectTable: string;
546
+ configFile: string;
547
+ }>;
236
548
  };
237
549
  }
238
- type PackageManagerId = Extract<ToolId, 'npm' | 'pnpm' | 'yarn' | 'bun' | 'deno'>;
550
+ type PackageManagerId = Extract<ToolId, 'npm' | 'pnpm' | 'yarn' | 'bun' | 'deno' | 'uv' | 'pip' | 'poetry' | 'pdm' | 'pipenv'>;
239
551
  type BotId = Extract<ToolId, 'renovate' | 'dependabot'>;
240
552
  /** Why a package manager is considered in use. Ordered from the strongest signal. */
241
553
  type EvidenceKind = 'lockfile' | 'packageManager' | 'devEngines' | 'config' | 'ci' | 'default';
@@ -244,7 +556,8 @@ interface Evidence {
244
556
  /** Repository-relative path with `/` separators. */
245
557
  file: string;
246
558
  }
247
- type VersionSource = 'packageManager' | 'devEngines' | 'volta' | 'tool-versions' | 'github-actions';
559
+ /** tool-config: a version the tool's own configuration requires, e.g. required-version in [tool.uv]. */
560
+ type VersionSource = 'packageManager' | 'tool-config' | 'devEngines' | 'volta' | 'tool-versions' | 'github-actions';
248
561
  /** A version declared somewhere in the repository, as written (it may be a range or an expression). */
249
562
  type VersionDeclaration = ({
250
563
  version: string;
@@ -296,6 +609,50 @@ interface Detection {
296
609
  workflows: string[];
297
610
  warnings: DetectionWarning[];
298
611
  }
612
+ interface UserEnvironment {
613
+ platform: NodeJS.Platform;
614
+ /** HOME on Linux and macOS, USERPROFILE on Windows: what os.homedir() returns. */
615
+ home: string;
616
+ env: Readonly<Record<string, string | undefined>>;
617
+ /** The current directory, against which a tool resolves a relative XDG_CONFIG_HOME (relativeToCwd); process.cwd() by default. */
618
+ cwd?: string;
619
+ /** Whether a directory exists; replaceable in tests. */
620
+ isDirectory?: (path: string) => boolean;
621
+ }
622
+ type FetchLike = (url: string, init: {
623
+ headers: Record<string, string>;
624
+ signal: AbortSignal;
625
+ redirect?: 'manual';
626
+ }) => Promise<Response>;
627
+ declare const EXIT_OK = 0;
628
+ declare const EXIT_FAILURE = 1;
629
+ declare const EXIT_USAGE = 2;
630
+ interface CliIo {
631
+ stdout(text: string): void;
632
+ stderr(text: string): void;
633
+ /** Whether each stream may be colored (see color.ts); absent means plain text. */
634
+ colors?: {
635
+ stdout: boolean;
636
+ stderr: boolean;
637
+ };
638
+ /** Whether each stream is a terminal; absent means it is not. */
639
+ terminal?: {
640
+ stdout: boolean;
641
+ stderr: boolean;
642
+ };
643
+ }
644
+ interface CheckDependencies {
645
+ fetch: FetchLike;
646
+ now: () => Date;
647
+ /** The whole of stdin, for hook mode. */
648
+ stdin: () => Promise<string>;
649
+ /** The user's home directory, with the user's .npmrc. */
650
+ home?: () => string;
651
+ /** --user: platform, home directory and environment variables (tests replace them). */
652
+ userEnvironment?: () => UserEnvironment;
653
+ }
654
+ declare function run(argv: readonly string[], output: CliIo, dependencies?: CheckDependencies): Promise<number>;
655
+ declare function getVersion(): string;
299
656
  interface Duration {
300
657
  /** The duration as written in the policy. */
301
658
  text: string;
@@ -366,6 +723,7 @@ declare const policyShape: Shape<{
366
723
  installScripts?: {
367
724
  default: "allow" | "deny";
368
725
  allow?: readonly string[];
726
+ deny?: readonly string[];
369
727
  };
370
728
  sources?: {
371
729
  git?: "allow" | "deny";
@@ -386,6 +744,7 @@ declare const policyShape: Shape<{
386
744
  installScripts?: {
387
745
  default: "allow" | "deny";
388
746
  allow: readonly string[];
747
+ deny: readonly string[];
389
748
  };
390
749
  sources?: {
391
750
  git?: "allow" | "deny";
@@ -398,6 +757,82 @@ declare const policyShape: Shape<{
398
757
  type Policy = Input<typeof policyShape>;
399
758
  /** The policy after validation: defaults filled in, durations parsed. */
400
759
  type ResolvedPolicy = Output<typeof policyShape>;
760
+ declare const APPLY_WARNINGS: {
761
+ readonly 'nothing-to-configure': "depcurfew found no supported package manager or bot, so nothing is configured.";
762
+ readonly 'environment-overrides': "apply --user: an environment variable makes a tool read another user file or none, or overrides a value apply writes there.";
763
+ readonly 'depcurfew-outdated': "The project installs a newer depcurfew than the one running, which may not know settings that version writes.";
764
+ 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.";
765
+ readonly 'tool-not-supported': "A detected or installed tool is not supported (Yarn Classic), so nothing is written for it.";
766
+ 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.";
767
+ readonly 'version-not-pinned': "No exact version of a package manager is pinned, so depcurfew configures for the latest version it knows.";
768
+ readonly 'version-too-old': "The pinned version of a tool (with apply --user, the installed one) lacks a setting the policy needs, so that setting is not written.";
769
+ readonly 'version-unknown': "apply --user: the installed version of a tool cannot be read from its files, and a value it does not understand would stop every command, so nothing is written for it.";
770
+ readonly 'no-user-config': "apply --user: the tool has no user-level configuration file, so nothing is written for it.";
771
+ readonly 'user-setting-project-only': "apply --user: the tool reads a part of the policy only from project files, so it is not written into the user file.";
772
+ readonly 'user-directory-relative': "apply --user: the environment variable that names the directory of the user file (XDG_CONFIG_HOME, APPDATA) is a relative path, so the tool looks in another place from each directory, or where it looks then is not known; nothing is written for it.";
773
+ readonly 'user-setting-overridden': "apply --user: a file the tool reads ahead of its user file (~/.npmrc for pnpm 10) sets a key apply writes there to another value, so the value written has no effect.";
774
+ readonly 'version-not-used': "apply --user: --for gives the version of a tool that apply --user does not configure (not in PATH with targets \"auto\", or not listed under targets).";
775
+ readonly 'cooldown-not-written': "The release-age delay cannot be written for a tool.";
776
+ 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.";
777
+ readonly 'cooldown-environment-only': "pip reads no project configuration, so the release-age delay has to be set in the environment where pip runs.";
778
+ readonly 'exclude-pattern-unsupported': "A cooldown exception uses a wildcard the tool does not support, so it is not written.";
779
+ readonly 'exclude-unsupported': "The tool has no setting for cooldown exceptions, so the release-age delay applies to every package.";
780
+ readonly 'allow-pattern-unsupported': "An installScripts.allow entry is a pattern, but the tool takes package names only, so it is not written.";
781
+ readonly 'deny-pattern-unsupported': "An installScripts.deny entry is a pattern, but the tool records denials by package name only, so it is not written.";
782
+ readonly 'allow-all-unsupported': "The tool has no setting that allows every install script, so installScripts \"allow\" is not written.";
783
+ readonly 'allow-all-replaced': "A setting that allowed every install script (with any value the tool reads as true, also one that a YAML merge key brings in, or pnpm 10's dangerously-allow-all-builds in .npmrc) was set to false, because the policy denies install scripts.";
784
+ readonly 'allow-list-entry-outside-policy': "An install-script allow-list in the repository (with apply --user, in a user file) lets a package run scripts the policy does not allow; apply does not remove it.";
785
+ readonly 'deny-conflicts-with-file': "An entry in a tool's own list lets a package of installScripts.deny run its install scripts, or blocks a package of installScripts.allow; apply does not change entries it did not write.";
786
+ readonly 'unexpected-value': "A setting apply adds to has a value of a type the tool does not accept there, so apply leaves it as it is and writes nothing into it.";
787
+ readonly 'built-in-list-replaced': "Bun's built-in list of trusted packages is replaced by the policy's allow-list.";
788
+ readonly 'scripts-not-enforceable': "The pinned (with apply --user, the installed) npm or pnpm runs every install script and has no allow-list, so \"deny\" cannot be enforced.";
789
+ 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 (apply --user then does not write the delay).";
790
+ readonly 'exact-version-gap': "The pinned (with apply --user, the installed) pnpm skips the release-age delay for packages requested by exact version.";
791
+ readonly 'source-not-enforceable': "The tool has no setting for a dependency source the policy denies; doctor checks the lockfile instead.";
792
+ readonly 'source-switch-indirect-only': "The pnpm setting for git and tarball sources stops them only for indirect dependencies.";
793
+ readonly 'source-switch-combined': "One pnpm setting covers git and tarball sources, so denying one denies both.";
794
+ readonly 'forced-source-build': "Python packages on the allow-list are always built from source, because no tool can only allow a build.";
795
+ readonly 'build-switch-scope': "The tool's switch forbids building only packages from an index; git, local and archive dependencies are still built.";
796
+ readonly 'build-switch-install-only': "Poetry applies its build switch when installing; poetry lock may still build source distributions.";
797
+ 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.";
798
+ readonly 'requirements-file-missing': "There is no requirements file to write pip options into.";
799
+ 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.";
800
+ readonly 'lockfile-out-of-date': "The lockfile records, or is checked against, a setting apply changed, so it has to be locked again.";
801
+ readonly 'file-ignored-by-git': "A file apply writes matches a .gitignore rule, so the change may never reach the repository.";
802
+ readonly 'bot-config-missing': "A bot has no configuration file, and depcurfew does not create one.";
803
+ 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.";
804
+ readonly 'bot-entries-missing': "The Dependabot configuration has no entries for the package managers in use.";
805
+ readonly 'security-delay-unsupported': "Dependabot cannot delay security updates, so securityUpdates \"delay\" is not applied.";
806
+ };
807
+ declare const APPLY_ERRORS: {
808
+ 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.";
809
+ 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.";
810
+ readonly 'policy-missing': "The directory has no depcurfew.json.";
811
+ readonly 'policy-invalid': "depcurfew.json does not follow the schema; issues lists the problems.";
812
+ readonly 'policy-unreadable': "depcurfew.json cannot be read.";
813
+ readonly 'policy-exists': "depcurfew init found a depcurfew.json and writes it only with --force.";
814
+ readonly 'directory-not-found': "The directory given on the command line does not exist or cannot be opened.";
815
+ readonly 'not-a-directory': "The path given on the command line is a file, not a directory.";
816
+ readonly 'home-missing': "apply --user and init --user: the home directory (HOME, USERPROFILE on Windows) does not exist or is not an absolute path, so nothing is written.";
817
+ };
818
+ type ApplyWarningCode = keyof typeof APPLY_WARNINGS;
819
+ type ApplyErrorCode = keyof typeof APPLY_ERRORS;
820
+ interface ApplyWarning {
821
+ code: ApplyWarningCode;
822
+ /** The tool it concerns, when there is one. */
823
+ tool?: ToolId;
824
+ /** The file it concerns, when there is one, and the line when it is known. */
825
+ location?: {
826
+ file: string;
827
+ line?: number;
828
+ };
829
+ /** What happened, for people; it starts with the tool's name when it concerns one. */
830
+ message: string;
831
+ /** What to do about it, when there is something to do. */
832
+ fix?: string;
833
+ }
834
+ /** A warning as one line of text, as ApplyReport.warnings has it. */
835
+ declare function warningText(warning: ApplyWarning): string;
401
836
  interface FileChange {
402
837
  /** Repository-relative path with `/` separators. */
403
838
  file: string;
@@ -405,6 +840,30 @@ interface FileChange {
405
840
  before: string | null;
406
841
  after: string;
407
842
  }
843
+ /**
844
+ * A file could not be written. writeChanges has removed its temporary files and put back the files it had already
845
+ * replaced, so nothing changed, except for the files in `unrestored` (whose earlier text is in `backups` when it could
846
+ * be written) and the temporary files in `leftovers`. The message says why, without the file name.
847
+ */
848
+ declare class WriteFailed extends Error {
849
+ readonly file: string;
850
+ /** Why, in plain words. */
851
+ readonly reason: string;
852
+ /** Files already replaced that could not be put back (normally none). */
853
+ readonly unrestored: string[];
854
+ /** For files in `unrestored`: the temporary file, next to it, that holds its earlier text. */
855
+ readonly backups: Readonly<Record<string, string>>;
856
+ /** Temporary files that could not be removed, named like `file` (relative to the root, or as WriteOptions.name says). */
857
+ readonly leftovers: string[];
858
+ /** Another program holds the file, or (on Windows) it is read-only. */
859
+ readonly busy: boolean;
860
+ readonly platform: NodeJS.Platform;
861
+ constructor(file: string, error: unknown, unrestored: string[], details?: {
862
+ platform?: NodeJS.Platform;
863
+ backups?: Record<string, string>;
864
+ leftovers?: string[];
865
+ });
866
+ }
408
867
  interface Snippet {
409
868
  tool: ToolId;
410
869
  file: string;
@@ -418,7 +877,10 @@ interface EditFailure {
418
877
  }
419
878
  interface ApplyReport {
420
879
  changes: FileChange[];
880
+ /** The warnings as one line of text each. */
421
881
  warnings: string[];
882
+ /** The same warnings with a code, the tool, the file and what to do (apply --json). */
883
+ warningDetails: ApplyWarning[];
422
884
  snippets: Snippet[];
423
885
  /** Files that could not be edited, as messages. When there are any, nothing is written. */
424
886
  errors: string[];
@@ -431,18 +893,153 @@ declare function botDuration(policy: Duration, managerMinutes: readonly number[]
431
893
  declare function planApply(root: string, policy: ResolvedPolicy, detection: Detection, tools?: readonly ToolKnowledge[]): ApplyReport;
432
894
  declare class PolicyError extends Error {
433
895
  readonly issues: string[];
434
- constructor(message: string, issues?: string[]);
896
+ readonly code: ApplyErrorCode;
897
+ /** What to do, when the message does not say it. */
898
+ readonly fix: string | undefined;
899
+ constructor(message: string, issues?: string[], code?: ApplyErrorCode, fix?: string);
900
+ /** The message and what to do, as one line of text. */
901
+ get text(): string;
435
902
  }
436
903
  declare function loadPolicy(root: string): ResolvedPolicy;
904
+ /**
905
+ * A file could not be written (code cannot-write). The files apply had already replaced were put back, so the
906
+ * repository is as it was, unless `unrestored` lists files. `report` is the plan that was being written.
907
+ */
908
+ declare class ApplyWriteError extends Error {
909
+ readonly code: "cannot-write";
910
+ /** The file that could not be written, relative to the repository. */
911
+ readonly file: string;
912
+ /** What to do. */
913
+ readonly fix: string;
914
+ /** Files already replaced that could not be put back (normally none). */
915
+ readonly unrestored: string[];
916
+ readonly report: ApplyReport;
917
+ /**
918
+ * `command` is what the fix tells the user to run again (depcurfew apply --user for user files), and
919
+ * `versionControl` whether the files may be in version control, to restore them from.
920
+ */
921
+ constructor(failure: WriteFailed, report: ApplyReport, options?: {
922
+ command?: string;
923
+ versionControl?: boolean;
924
+ });
925
+ }
437
926
  /**
438
927
  * Reads the policy, detects the project, plans the changes and (unless told otherwise) writes them. If any file
439
928
  * could not be edited, nothing is written, so a run either applies the whole policy or leaves the repository as it was.
929
+ * A file that cannot be written throws ApplyWriteError after the files already written are put back.
440
930
  */
441
931
  declare function applyProject(root: string, options?: {
442
932
  write?: boolean;
443
933
  }): ApplyReport;
444
- /** A unified diff with three lines of context; empty when the texts are equal. `before` is null for a new file. */
445
- declare function unifiedDiff(file: string, before: string | null, after: string, context?: number): string;
934
+ interface DiffOptions {
935
+ /** Shows the values of secrets as <redacted> in every line (redactSecrets), for a diff that people or logs see. */
936
+ redact?: boolean;
937
+ }
938
+ /**
939
+ * A unified diff with three lines of context; empty when the texts are equal. `before` is null for a new file.
940
+ * Control characters in the file name are shown as text (printable), so that a name cannot start a line of its own
941
+ * and forge a header or a hunk. A file with an absolute path (a user file outside the home directory) is named as it
942
+ * is, without the a/ and b/ of repository paths.
943
+ */
944
+ declare function unifiedDiff(file: string, before: string | null, after: string, context?: number, options?: DiffOptions): string;
945
+ interface SimilarName {
946
+ /** The popular package the name resembles. */
947
+ match: string;
948
+ /** edit: a letter or two differ; separators: only -, _ or . differ. */
949
+ reason: 'edit' | 'separators';
950
+ }
951
+ declare class PopularNames {
952
+ #private;
953
+ constructor(names: readonly string[]);
954
+ has(name: string): boolean;
955
+ /** The popular package `name` resembles, or undefined (also when `name` is itself popular). */
956
+ similar(name: string): SimilarName | undefined;
957
+ }
958
+ type SpecKind = 'registry' | 'git' | 'remoteTarball' | 'file' | 'directory' | 'workspace' | 'other';
959
+ 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';
960
+ interface CheckFinding {
961
+ level: 'error' | 'warning';
962
+ code: FindingCode;
963
+ message: string;
964
+ /** What to do about it. */
965
+ fix: string;
966
+ }
967
+ interface PackageReport {
968
+ spec: string;
969
+ kind: SpecKind | 'invalid';
970
+ name?: string;
971
+ version?: string;
972
+ publishedAt?: string;
973
+ latest?: {
974
+ version: string;
975
+ publishedAt?: string;
976
+ };
977
+ /** npm: the dist-tag the version was picked by, when it is not the default one (--tag, npm_config_tag, tag in .npmrc). */
978
+ tag?: string;
979
+ /** Python: pre-releases allowed or forbidden by the command. */
980
+ prereleases?: 'allow' | 'deny';
981
+ /** Python: the command builds the package from source (true) or installs wheels only (false). */
982
+ buildFromSource?: boolean;
983
+ findings: CheckFinding[];
984
+ }
985
+ /** A spec with what the install command changes about the version or the build. */
986
+ interface CheckRequest {
987
+ spec: string;
988
+ /** npm: the dist-tag npm prefers for a name without a version or with a range; the registry's default when left out. */
989
+ tag?: string;
990
+ /** The package manager is not npm, so tag= in .npmrc (CheckOptions.defaultTag) does not choose its version. */
991
+ ignoresConfigTag?: boolean;
992
+ /** Python: pre-releases allowed or forbidden; PEP 440's default when left out. */
993
+ prereleases?: 'allow' | 'deny';
994
+ /** Python: the package is built from source even when it has wheels (true), or only wheels are used (false). */
995
+ buildFromSource?: boolean;
996
+ }
997
+ interface CheckOptions {
998
+ policy: ResolvedPolicy;
999
+ fetch: FetchLike;
1000
+ now: Date;
1001
+ registry: string;
1002
+ popular: PopularNames;
1003
+ /** Registries the project assigns to scopes (@scope:registry in .npmrc); those packages are not looked up. */
1004
+ scopeRegistries?: ReadonlyMap<string, string>;
1005
+ /** The Python package index pypi: packages are looked up in; PyPI when not given. */
1006
+ index?: string;
1007
+ /** Popular Python projects, by normalized name, for look-alike names. */
1008
+ popularPython?: PopularNames;
1009
+ /** npm: the dist-tag npm prefers when a request names none (tag in .npmrc); the registry's default when left out. */
1010
+ defaultTag?: string;
1011
+ /**
1012
+ * Whether a finding about install scripts quotes them (the default) or names them only, for the hook: their body is
1013
+ * written by the package's publisher and should not reach the model as part of depcurfew's reason.
1014
+ */
1015
+ scriptBodies?: boolean;
1016
+ }
1017
+ declare function checkSpec(request: string | CheckRequest, options: CheckOptions): Promise<PackageReport>;
1018
+ /** Checks the specs a few at a time; the reports keep the order of the specs. */
1019
+ declare function checkSpecs(specs: readonly (string | CheckRequest)[], options: CheckOptions): Promise<PackageReport[]>;
1020
+ interface InstallRequest {
1021
+ tool: ToolId;
1022
+ /** The command as written, for messages. */
1023
+ command: string;
1024
+ spec: string;
1025
+ /** npm: the dist-tag the command makes npm prefer (--tag, npm_config_tag). */
1026
+ tag?: string;
1027
+ /** Python: the command allows pre-releases of the package, or forbids them; left out, the tool's default applies. */
1028
+ prereleases?: 'allow' | 'deny';
1029
+ /** Python: the command makes the tool build the package from source (true) or install wheels only (false). */
1030
+ buildFromSource?: boolean;
1031
+ }
1032
+ interface CommandScan {
1033
+ requests: InstallRequest[];
1034
+ /** Why an install command could not be read; undefined when every one could. */
1035
+ unreadable: string | undefined;
1036
+ }
1037
+ interface ScanOptions {
1038
+ /** The shell that runs the line: a POSIX shell (the default; also Git Bash) or PowerShell. */
1039
+ shell?: 'posix' | 'powershell';
1040
+ }
1041
+ /** The packages a command line installs or downloads to run with npm, pnpm, Yarn, Bun, Deno, pip, uv, Poetry, PDM, Pipenv or pipx. */
1042
+ declare function scanCommandLine(line: string, options?: ScanOptions): CommandScan;
446
1043
  /**
447
1044
  * Describes the repository at `root`. Never executes anything from the repository and never leaves it;
448
1045
  * problems with individual files become warnings.
@@ -452,9 +1049,13 @@ declare class Repository {
452
1049
  #private;
453
1050
  readonly root: string;
454
1051
  readonly warnings: DetectionWarning[];
1052
+ /** Repository-relative paths of the files that were read, for the log of what doctor checked. */
1053
+ readonly read: Set<string>;
455
1054
  constructor(root: string);
456
1055
  warn(code: string, message: string, files: readonly string[]): void;
457
1056
  isFile(path: string): boolean;
1057
+ /** Whether the path exists in the repository as something other than a regular file or a directory (a named pipe, a socket, a device). */
1058
+ isSpecialFile(path: string): boolean;
458
1059
  /** The text of a file, without a byte order mark; undefined (with a warning when useful) if it cannot be read. */
459
1060
  readText(path: string, limit?: number): string | undefined;
460
1061
  /** The first bytes of a file, e.g. to recognize a lockfile format without reading a large file. */
@@ -465,6 +1066,8 @@ declare class Repository {
465
1066
  listDirectories(path: string): string[];
466
1067
  /** Parses a file with the given parser, reporting failures as warnings. */
467
1068
  parse<T>(path: string, parser: (text: string) => T): T | undefined;
1069
+ /** Like parse, but each file is read and parsed once per parser: several rules read the same file. */
1070
+ parseOnce<T>(path: string, parser: (text: string) => T): T | undefined;
468
1071
  relative(absolute: string): string;
469
1072
  }
470
1073
  /** The SARIF levels: an error fails `doctor`, warnings and notes do not. */
@@ -474,6 +1077,8 @@ interface Location {
474
1077
  file: string;
475
1078
  /** 1-based; absent when the finding concerns the whole file. */
476
1079
  line?: number;
1080
+ /** doctor --user: the environment variable, as it is set, of a finding whose file is "environment". */
1081
+ variable?: string;
477
1082
  }
478
1083
  interface Finding {
479
1084
  rule: string;
@@ -505,6 +1110,8 @@ interface Rule {
505
1110
  id: string;
506
1111
  /** One sentence: what the rule finds. */
507
1112
  summary: string;
1113
+ /** What the rule checks, said as what holds when it passes, for the list of what doctor checked. */
1114
+ checks?: string;
508
1115
  /** Why it matters. */
509
1116
  description: string;
510
1117
  /** How to fix it in general. */
@@ -515,11 +1122,43 @@ interface Rule {
515
1122
  requiresPolicy: boolean;
516
1123
  check(context: RuleContext): void;
517
1124
  }
1125
+ /** A package manager or bot doctor checked, with the version it checked against when one is known. */
1126
+ interface CheckedTool {
1127
+ id: string;
1128
+ name: string;
1129
+ version?: string;
1130
+ /** Where the version comes from: a repository file, or PATH for doctor --user. */
1131
+ versionFrom?: string;
1132
+ }
1133
+ /** One check of the list of what doctor checked: a rule and what came of it. */
1134
+ interface RuleResult {
1135
+ rule: string;
1136
+ /** What the rule checks, in plain words (Rule.check). */
1137
+ check: string;
1138
+ /** passed: it ran and found nothing; problems: it reported findings; skipped: it needs a policy that is missing or invalid. */
1139
+ result: 'passed' | 'problems' | 'skipped';
1140
+ errors: number;
1141
+ warnings: number;
1142
+ notes: number;
1143
+ }
1144
+ /** What doctor looked at, shown below the result, so that a clean report says what it covers. */
1145
+ interface Checked {
1146
+ tools: CheckedTool[];
1147
+ /** Files that were read, repository-relative (doctor) or as shown (~/.npmrc, doctor --user). */
1148
+ files: string[];
1149
+ /** Every rule, in the order it ran, with its result. */
1150
+ rules: RuleResult[];
1151
+ /** The policy that skipped rules need, as messages show it. */
1152
+ policyFile: string;
1153
+ }
518
1154
  interface DoctorReport {
519
1155
  schemaVersion: 1;
520
- policy: 'valid' | 'missing';
1156
+ /** invalid: depcurfew.json does not follow the schema or cannot be read (only with invalidPolicy: 'report'). */
1157
+ policy: 'valid' | 'missing' | 'invalid';
521
1158
  findings: Finding[];
522
1159
  notices: Notice[];
1160
+ /** What was checked. */
1161
+ checked?: Checked;
523
1162
  }
524
1163
  /** Every doctor rule, in the order they are documented. unreadable-config runs last, after every other rule has read its files. */
525
1164
  declare const RULES: readonly Rule[];
@@ -535,11 +1174,21 @@ declare function countLevels(findings: readonly Finding[]): Record<'error' | 'wa
535
1174
  declare function renderText(report: DoctorReport, rules: readonly Rule[], colors?: Paint): string;
536
1175
  /** The text of a line in a repository file, to give regions the end column GitHub asks for. */
537
1176
  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. */
1177
+ /** Why doctor could not run, with a stable code. */
1178
+ interface SarifError {
1179
+ code: string;
1180
+ message: string;
1181
+ fix?: string;
1182
+ }
1183
+ declare function toSarif(report: DoctorReport, rules: readonly Rule[], version: string, readLine?: LineReader, errors?: readonly SarifError[]): object;
1184
+ /**
1185
+ * Runs the rules on a repository. When depcurfew.json exists but is invalid, it throws PolicyError, or with
1186
+ * invalidPolicy: 'report' runs the rules that need no policy and reports the problems as an invalid-policy finding.
1187
+ */
540
1188
  declare function runDoctor(root: string, options?: {
541
1189
  rules?: readonly Rule[];
542
1190
  tools?: readonly ToolKnowledge[];
1191
+ invalidPolicy?: 'throw' | 'report';
543
1192
  }): DoctorReport;
544
1193
  type NumericDelayUnit = Exclude<DelayUnit, 'duration-string'>;
545
1194
  declare const DURATION_STRING_UNITS: readonly ["minutes", "hours", "days", "weeks"];
@@ -559,6 +1208,7 @@ type CooldownConversion = {
559
1208
  ok: false;
560
1209
  tool: ToolId;
561
1210
  reason: string;
1211
+ fix: string;
562
1212
  };
563
1213
  declare function convertMinutes(minutes: number, unit: NumericDelayUnit): Converted<number>;
564
1214
  /** Writes minutes as a duration string such as `36 hours` or `3 days`. */
@@ -583,6 +1233,7 @@ declare const DEFAULT_POLICY: {
583
1233
  readonly installScripts: {
584
1234
  readonly default: "deny";
585
1235
  readonly allow: readonly [];
1236
+ readonly deny: readonly [];
586
1237
  };
587
1238
  readonly sources: {
588
1239
  readonly git: "deny";
@@ -606,4 +1257,4 @@ declare function validatePolicy(document: unknown, tools?: readonly ToolKnowledg
606
1257
  /** Parses and validates the text of a policy file. */
607
1258
  declare function parsePolicy(source: string, tools?: readonly ToolKnowledge[]): PolicyResult;
608
1259
  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 };
1260
+ 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 };