@alint-js/config 0.0.18 → 0.0.20

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
@@ -11,34 +11,77 @@ This package owns the file-system side of configuration:
11
11
  - parses and stringifies setup TOML
12
12
  - merges setup layers
13
13
  - writes provider setup files
14
- - exports built-in ignore pattern groups
14
+ - installs remote plugin packages or local plugin directories and writes `.alint/plugins/lock.json`
15
+ - exports built-in ignore pattern groups for lower-level tooling
15
16
 
16
17
  It is used by `@alint-js/cli` and is useful for tools that need to inspect or prepare an `alint` project without running the linter.
17
18
 
19
+ Ordinary `alint.config.*` files should import ignore presets through the `@alint-js/cli` facade so projects only need the CLI package.
20
+
18
21
  ## How to use
19
22
 
20
23
  ```ts
21
24
  import {
22
25
  getGlobalSetupConfigPath,
26
+ installStaticPlugins,
23
27
  loadAlintConfig,
24
28
  loadSetupConfig,
25
29
  writeSetupConfig,
26
30
  } from '@alint-js/config'
27
31
 
28
32
  const setupPath = getGlobalSetupConfigPath()
29
- const setup = await loadSetupConfig({ cwd: process.cwd() })
30
- const config = await loadAlintConfig({ cwd: process.cwd() })
33
+ const cwd = process.cwd()
34
+ const setup = await loadSetupConfig(setupPath)
35
+
36
+ await installStaticPlugins({ cwd })
37
+
38
+ const config = await loadAlintConfig(cwd)
31
39
 
32
40
  await writeSetupConfig(setupPath, setup)
33
41
  ```
34
42
 
43
+ Static configs can use TOML, YAML, JSON, JSONC, or JSON5. They are data-only alternatives to executable JavaScript and TypeScript flat configs, and identify plugin sources with strings.
44
+
45
+ For example, TOML supports exact remote packages, config-relative directories, and native absolute paths:
46
+
47
+ ```toml
48
+ [[config.group]]
49
+
50
+ [config.group.plugins]
51
+ plugin_1 = "@scope/alint-plugin-example1@1.2.3" # package
52
+ plugin_2 = "./plugins/relative-plugin-examplex" # relative directory
53
+ plugin_3 = "/opt/alint/plugins/absolute-plugin" # absolute directory
54
+ ```
55
+
56
+ Relative paths use the config file's directory as their base. Windows native paths should use TOML literal strings so backslashes remain literal:
57
+
58
+ ```toml
59
+ [[config.group]]
60
+
61
+ [config.group.plugins]
62
+ native = 'C:\alint\plugins\native-plugin'
63
+ ```
64
+
65
+ Use `installStaticPlugins` after adding or changing plugin sources.
66
+
67
+ - **Package:** downloads the exact package version into `.alint/plugins/store`, verifies its integrity, and locks the installed snapshot.
68
+ - **Local:** installs the directory in place, validates its package root export, and locks its physical directory identity. It does not build the plugin or install its dependencies.
69
+
70
+ Run installation again after changing a source string, moving a local directory, or changing its symlink target. Changes inside the same local directory are loaded by the next process without reinstalling.
71
+
72
+ ## Plugin lockfile
73
+
74
+ `.alint/plugins/lock.json` stores package snapshots with integrity metadata and local directory source identities. Local plugin code executes as trusted Node.js; containment checks are not a sandbox.
75
+
35
76
  ## When to use
36
77
 
37
78
  - You are building CLI commands, editors, or automation around `alint` config.
38
79
  - You need to read or write provider setup TOML.
39
- - You need the same ignore defaults as the official CLI.
80
+ - You need to load `alint.config.*` outside the official CLI.
81
+ - You need to install remote packages or local plugin directories before loading static config.
82
+ - You need the same ignore defaults as the official CLI in lower-level tooling.
40
83
 
41
84
  ## When not to use
42
85
 
43
- - Use `@alint-js/cli` when you only need the `alint` command.
86
+ - Use `@alint-js/cli` when you only need the `alint` command or an ordinary `alint.config.*` file.
44
87
  - Use `@alint-js/core` when you need the rule DSL, source runtime, model resolution, or run engine.
package/dist/index.d.mts CHANGED
@@ -1,9 +1,56 @@
1
- import { AlintConfig, AlintConfig as AlintConfig$1, ModelSize, ProviderDefinition, ProviderType, RunnerConfig, RunnerStatsConfig, SetupConfig, SetupConfig as SetupConfig$1, SetupModelDefinition } from "@alint-js/core";
1
+ import { AlintConfig, AlintConfig as AlintConfig$1, AlintConfigItem, ModelSize, PluginDefinition, ProviderDefinition, ProviderType, RunnerConfig, RunnerStatsConfig, SetupConfig, SetupConfig as SetupConfig$1, SetupModelDefinition } from "@alint-js/core";
2
2
 
3
+ //#region src/plugins/spec.d.ts
4
+ interface DirectoryPluginSpecifier {
5
+ directory: string;
6
+ raw: string;
7
+ type: 'directory';
8
+ }
9
+ type ParsedPluginSpecifier = DirectoryPluginSpecifier | RegistryPluginSpecifier;
10
+ interface ParsePluginSpecifierOptions {
11
+ configFile?: string;
12
+ }
13
+ interface RegistryPluginSpecifier {
14
+ name: string;
15
+ raw: string;
16
+ registryPath: string;
17
+ segments: string[];
18
+ type: 'registry';
19
+ version: string;
20
+ }
21
+ declare function parsePluginSpecifier(value: string, options?: ParsePluginSpecifierOptions): ParsedPluginSpecifier;
22
+ //#endregion
23
+ //#region src/config/static.d.ts
24
+ interface ParsedStaticConfig {
25
+ groups: ParsedStaticConfigGroup[];
26
+ }
27
+ interface ParsedStaticConfigGroup {
28
+ item: StaticConfigItem;
29
+ plugins: StaticPluginReference[];
30
+ }
31
+ interface ParseStaticConfigOptions {
32
+ configFile?: string;
33
+ }
34
+ type StaticConfigInput = readonly StaticConfigInput[] | StaticConfigItem;
35
+ interface StaticConfigItem extends Omit<AlintConfigItem, 'plugins'> {
36
+ plugins?: Record<string, PluginDefinition | string>;
37
+ }
38
+ interface StaticPluginReference {
39
+ alias: string;
40
+ specifier: ParsedPluginSpecifier;
41
+ }
42
+ type StaticPluginResolver = (reference: StaticPluginReference) => Promise<PluginDefinition>;
43
+ interface ToAlintConfigOptions {
44
+ pluginResolver: StaticPluginResolver;
45
+ }
46
+ declare function parseStaticConfig(value: unknown, options?: ParseStaticConfigOptions): ParsedStaticConfig;
47
+ declare function toAlintConfig(config: ParsedStaticConfig, options: ToAlintConfigOptions): Promise<AlintConfig$1>;
48
+ //#endregion
3
49
  //#region src/config/load.d.ts
4
50
  declare function loadAlintConfig(cwd: string, configFile?: string): Promise<AlintConfig$1>;
51
+ declare function loadStaticConfig(cwd: string, configFile?: string): Promise<ParsedStaticConfig>;
5
52
  //#endregion
6
- //#region src/ignore-patterns.d.ts
53
+ //#region src/constants/patterns/ignore.d.ts
7
54
  declare const ignorePatternsEslintDefaults: readonly ["**/node_modules/**", ".git/**", "**/.git/**"];
8
55
  declare const ignorePatternsBuildOutputs: readonly ["**/dist/**", "**/coverage/**", "**/output/**", "**/.output/**", "**/.next/**", "**/.nuxt/**", "**/.svelte-kit/**", "**/.vercel/**"];
9
56
  declare const ignorePatternsCaches: readonly ["**/.cache/**", "**/.temp/**", "**/.tmp/**", "**/temp/**", "**/tmp/**", "**/.vitepress/cache/**", "**/.vite-inspect/**"];
@@ -17,9 +64,83 @@ interface GlobalSetupConfigPathOptions {
17
64
  xdgConfig?: string;
18
65
  }
19
66
  declare function getGlobalSetupConfigPath(env?: NodeJS.ProcessEnv, options?: GlobalSetupConfigPathOptions): string;
67
+ declare function getProjectPluginLockPath(cwd: string): string;
68
+ declare function getProjectPluginStorePath(cwd: string): string;
20
69
  declare function getProjectSetupConfigPath(cwd: string): string;
21
70
  declare function getStatsDir(env?: NodeJS.ProcessEnv, options?: GlobalSetupConfigPathOptions): string;
22
71
  //#endregion
72
+ //#region src/plugins/types.d.ts
73
+ interface DirectoryPluginLockEntry {
74
+ alias: string;
75
+ path: string;
76
+ specifier: string;
77
+ type: 'directory';
78
+ }
79
+ interface ParsedDirectoryPluginLockEntry {
80
+ alias: string;
81
+ cwd: string;
82
+ lockEntry: DirectoryPluginLockEntry;
83
+ resolutionError?: unknown;
84
+ specifier: DirectoryPluginSpecifier;
85
+ type: 'directory';
86
+ }
87
+ type ParsedPluginLockEntry = ParsedDirectoryPluginLockEntry | ParsedRegistryPluginLockEntry;
88
+ interface ParsedPluginLockFile {
89
+ cwd: string;
90
+ entries: ParsedPluginLockEntry[];
91
+ file: PluginLockFile;
92
+ find: (reference: StaticPluginReference) => ParsedPluginLockEntry | undefined;
93
+ get: (reference: StaticPluginReference) => ParsedPluginLockEntry;
94
+ }
95
+ interface ParsedRegistryPluginLockEntry {
96
+ alias: string;
97
+ cwd: string;
98
+ lockEntry: RegistryPluginLockEntry;
99
+ resolutionError?: unknown;
100
+ specifier: RegistryPluginSpecifier;
101
+ type: 'registry';
102
+ }
103
+ type PluginLockEntry = DirectoryPluginLockEntry | RegistryPluginLockEntry;
104
+ interface PluginLockFile {
105
+ plugins: Record<string, PluginLockEntry>;
106
+ version: 2;
107
+ }
108
+ interface RegistryPluginLockEntry {
109
+ alias: string;
110
+ entry: string;
111
+ integrity: string;
112
+ name: string;
113
+ registry: string;
114
+ specifier: string;
115
+ tarball: string;
116
+ type: 'registry';
117
+ version: string;
118
+ }
119
+ interface StaticPluginInstallOptions {
120
+ configFile?: string;
121
+ cwd: string;
122
+ registry?: string;
123
+ }
124
+ interface StaticPluginInstallResult {
125
+ configuredPluginCount: number;
126
+ installedLocalDirectoryCount: number;
127
+ installedPackageCount: number;
128
+ lock: PluginLockFile;
129
+ }
130
+ //#endregion
131
+ //#region src/plugins/install.d.ts
132
+ declare function installStaticPlugins(options: StaticPluginInstallOptions): Promise<StaticPluginInstallResult>;
133
+ //#endregion
134
+ //#region src/plugins/lock.d.ts
135
+ declare function createEmptyPluginLockFile(): PluginLockFile;
136
+ declare function listMissing(config: ParsedStaticConfig, lock: ParsedPluginLockFile): StaticPluginReference[];
137
+ declare function listUnresolved(config: ParsedStaticConfig, lock: ParsedPluginLockFile): Promise<ParsedPluginLockEntry[]>;
138
+ declare function loadPluginLockFile(cwd: string): Promise<PluginLockFile>;
139
+ declare function parsePluginLockFile(value: unknown, options: {
140
+ cwd: string;
141
+ }): ParsedPluginLockFile;
142
+ declare function writePluginLockFile(cwd: string, lockFile: PluginLockFile): Promise<void>;
143
+ //#endregion
23
144
  //#region src/setup/load.d.ts
24
145
  declare const emptySetupConfig: SetupConfig$1;
25
146
  declare function loadSetupConfig(filePath: string): Promise<SetupConfig$1>;
@@ -32,4 +153,4 @@ declare function stringifySetupConfigToml(config: SetupConfig$1): string;
32
153
  //#region src/setup/write.d.ts
33
154
  declare function writeSetupConfig(filePath: string, config: SetupConfig$1): Promise<void>;
34
155
  //#endregion
35
- export { type AlintConfig, type GlobalSetupConfigPathOptions, type ModelSize, type ProviderDefinition, type ProviderType, type RunnerConfig, type RunnerStatsConfig, type SetupConfig, type SetupModelDefinition, emptySetupConfig, getGlobalSetupConfigPath, getProjectSetupConfigPath, getStatsDir, ignorePatternsAIAgents, ignorePatternsBuildOutputs, ignorePatternsCaches, ignorePatternsCommon, ignorePatternsEslintDefaults, ignorePatternsGenerated, loadAlintConfig, loadSetupConfig, mergeSetupConfigs, parseSetupConfigToml, stringifySetupConfigToml, writeSetupConfig };
156
+ export { type AlintConfig, type DirectoryPluginLockEntry, type GlobalSetupConfigPathOptions, type ModelSize, type ParseStaticConfigOptions, type ParsedDirectoryPluginLockEntry, type ParsedPluginLockEntry, type ParsedPluginLockFile, type ParsedPluginSpecifier, type ParsedRegistryPluginLockEntry, type ParsedStaticConfig, type ParsedStaticConfigGroup, type PluginLockEntry, type PluginLockFile, type ProviderDefinition, type ProviderType, type RegistryPluginLockEntry, type RunnerConfig, type RunnerStatsConfig, type SetupConfig, type SetupModelDefinition, type StaticConfigInput, type StaticConfigItem, type StaticPluginInstallOptions, type StaticPluginInstallResult, type StaticPluginReference, type StaticPluginResolver, type ToAlintConfigOptions, createEmptyPluginLockFile, emptySetupConfig, getGlobalSetupConfigPath, getProjectPluginLockPath, getProjectPluginStorePath, getProjectSetupConfigPath, getStatsDir, ignorePatternsAIAgents, ignorePatternsBuildOutputs, ignorePatternsCaches, ignorePatternsCommon, ignorePatternsEslintDefaults, ignorePatternsGenerated, installStaticPlugins, listMissing, listUnresolved, loadAlintConfig, loadPluginLockFile, loadSetupConfig, loadStaticConfig, mergeSetupConfigs, parsePluginLockFile, parsePluginSpecifier, parseSetupConfigToml, parseStaticConfig, stringifySetupConfigToml, toAlintConfig, writePluginLockFile, writeSetupConfig };
package/dist/index.mjs CHANGED
@@ -1,23 +1,735 @@
1
+ import { dirname, isAbsolute, join, normalize, posix, relative, resolve, sep, win32 } from "node:path";
1
2
  import { loadConfig } from "c12";
3
+ import { createJiti } from "jiti/static";
4
+ import { createHash, randomUUID } from "node:crypto";
5
+ import { mkdir, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
6
+ import { array, boolean, literal, looseObject, optional, parse, picklist, record, strictObject, string, tuple, union, unknown, variant } from "valibot";
2
7
  import process from "node:process";
3
8
  import { homedir } from "node:os";
4
- import { dirname, join } from "pathe";
9
+ import { dirname as dirname$1, extname, join as join$1 } from "pathe";
5
10
  import { xdgConfig } from "xdg-basedir";
6
- import { mkdir, readFile, writeFile } from "node:fs/promises";
7
- import { parse, stringify } from "smol-toml";
11
+ import { isError } from "@moeru/std/error";
12
+ import { exports } from "resolve.exports";
13
+ import { Buffer } from "node:buffer";
14
+ import { createWriteStream } from "node:fs";
15
+ import { Readable } from "node:stream";
16
+ import { pipeline } from "node:stream/promises";
17
+ import { createGunzip } from "node:zlib";
18
+ import tar from "tar-stream";
19
+ import { ofetch } from "ofetch";
20
+ import { pathToFileURL } from "node:url";
21
+ import { isPlainObject } from "es-toolkit/compat";
22
+ import { parse as parse$1, stringify } from "smol-toml";
23
+ //#region src/paths.ts
24
+ function getGlobalSetupConfigPath(env = process.env, options = {}) {
25
+ return join$1(resolveAlintHome(env, options), "config.toml");
26
+ }
27
+ function getProjectPluginLockPath(cwd) {
28
+ return join$1(cwd, ".alint", "plugins", "lock.json");
29
+ }
30
+ function getProjectPluginStorePath(cwd) {
31
+ return join$1(cwd, ".alint", "plugins", "store");
32
+ }
33
+ function getProjectSetupConfigPath(cwd) {
34
+ return join$1(cwd, ".alint", "config.toml");
35
+ }
36
+ function getStatsDir(env = process.env, options = {}) {
37
+ return join$1(resolveAlintHome(env, options), "stats");
38
+ }
39
+ function resolveAlintHome(env, options) {
40
+ return join$1(env.XDG_CONFIG_HOME ?? resolveDefaultConfigHome(options), "alint");
41
+ }
42
+ function resolveDefaultConfigHome(options) {
43
+ if (options.isMacOS ?? process.platform === "darwin") return join$1(homedir(), ".config");
44
+ return options.xdgConfig ?? xdgConfig ?? join$1(homedir(), ".config");
45
+ }
46
+ //#endregion
47
+ //#region src/utils/fs.ts
48
+ async function exists(path) {
49
+ try {
50
+ await stat(path);
51
+ return true;
52
+ } catch (error) {
53
+ if (isENOENTError(error)) return false;
54
+ throw error;
55
+ }
56
+ }
57
+ function isENOENTError(error) {
58
+ return isNodeErrorCode(error, "ENOENT");
59
+ }
60
+ function isNodeErrorCode(error, code) {
61
+ return isError(error) && "code" in error && error.code === code;
62
+ }
63
+ function isPathInside(path, parent) {
64
+ const childRelativePath = relative(parent, path);
65
+ return childRelativePath === "" || childRelativePath !== ".." && !childRelativePath.startsWith(`..${sep}`) && !isAbsolute(childRelativePath);
66
+ }
67
+ //#endregion
68
+ //#region src/plugins/integrity.ts
69
+ const SUPPORTED_INTEGRITY_ALGORITHMS = ["sha512", "sha256"];
70
+ const SUPPORTED_INTEGRITY_TOKEN_PATTERN = /^(sha512|sha256)-([A-Za-z0-9+/]+={0,2})$/u;
71
+ function checkIntegrity(data, integrity, specifier) {
72
+ const result = parseIntegrity(integrity, specifier);
73
+ if (!result.tokens.some(({ algorithm, expected }) => {
74
+ if (algorithm !== result.strongestAlgorithm) return false;
75
+ return createHash(algorithm).update(data).digest("base64") === expected;
76
+ })) throw new Error(`Integrity mismatch for "${specifier}".`);
77
+ }
78
+ function parseIntegrity(integrity, specifier) {
79
+ const tokens = integrity.trim().split(/\s+/u).map((token) => SUPPORTED_INTEGRITY_TOKEN_PATTERN.exec(token)).filter((match) => match !== null).map(([, algorithm, expected]) => ({
80
+ algorithm,
81
+ expected
82
+ }));
83
+ const strongestAlgorithm = SUPPORTED_INTEGRITY_ALGORITHMS.find((algorithm) => tokens.some((token) => token.algorithm === algorithm));
84
+ if (strongestAlgorithm === void 0) throw new Error(`Unsupported npm integrity format for "${specifier}": "${integrity}".`);
85
+ return {
86
+ strongestAlgorithm,
87
+ tokens
88
+ };
89
+ }
90
+ //#endregion
91
+ //#region src/plugins/sources/manifest.ts
92
+ function resolveRelativeRootEntry(packageJson) {
93
+ const [entry] = exports(packageJson, ".", {
94
+ browser: false,
95
+ require: false
96
+ }) ?? [];
97
+ if (entry === void 0) {
98
+ const name = typeof packageJson.name === "string" ? packageJson.name : "<unknown>";
99
+ throw new Error(`Package "${name}" does not define a resolvable "." export.`);
100
+ }
101
+ return entry.startsWith("./") ? entry.slice(2) : entry;
102
+ }
103
+ //#endregion
104
+ //#region src/plugins/sources/local/index.ts
105
+ async function createLockEntry$1(installed, options) {
106
+ const relativePath = relative(await realpath(options.cwd), installed.path);
107
+ const path = isAbsolute(relativePath) || win32.isAbsolute(relativePath) ? installed.path : relativePath === "" ? "." : relativePath;
108
+ return {
109
+ alias: options.alias,
110
+ path,
111
+ specifier: options.specifier.raw,
112
+ type: "directory"
113
+ };
114
+ }
115
+ async function install$1(options) {
116
+ const packageDir = await canonicalRoot(options.alias, options.specifier.directory);
117
+ await validateRoot(options.alias, packageDir);
118
+ return {
119
+ path: packageDir,
120
+ type: "directory"
121
+ };
122
+ }
123
+ async function resolve$2(entry) {
124
+ const lockedPath = isAbsolute(entry.lockEntry.path) || win32.isAbsolute(entry.lockEntry.path) ? entry.lockEntry.path : resolve(entry.cwd, entry.lockEntry.path);
125
+ const [packageDir, lockedPackageDir] = await Promise.all([lockedRoot(entry.alias, entry.specifier.directory, "configured"), lockedRoot(entry.alias, lockedPath, "locked")]);
126
+ if (packageDir !== lockedPackageDir) throw new Error(`Directory plugin "${entry.alias}" has moved or its symlink target changed. Run: alint plugin install`);
127
+ return {
128
+ cache: "content",
129
+ entry: await validateRoot(entry.alias, packageDir)
130
+ };
131
+ }
132
+ async function canonicalRoot(alias, directory) {
133
+ let packageDir;
134
+ try {
135
+ packageDir = await realpath(directory);
136
+ } catch (error) {
137
+ if (isENOENTError(error)) throw new Error(`Directory plugin "${alias}" does not exist at "${directory}".`, { cause: error });
138
+ throw new Error(`Could not resolve directory plugin "${alias}" at "${directory}".`, { cause: error });
139
+ }
140
+ let directoryStat;
141
+ try {
142
+ directoryStat = await stat(packageDir);
143
+ } catch (error) {
144
+ throw new Error(`Could not inspect directory plugin "${alias}" at "${directory}".`, { cause: error });
145
+ }
146
+ if (!directoryStat.isDirectory()) throw new Error(`Directory plugin "${alias}" path "${directory}" is not a directory.`);
147
+ return packageDir;
148
+ }
149
+ async function lockedRoot(alias, directory, source) {
150
+ try {
151
+ return await realpath(directory);
152
+ } catch (error) {
153
+ if (isENOENTError(error)) throw new Error(`Directory plugin "${alias}" has moved or its symlink target changed. Run: alint plugin install`, { cause: error });
154
+ throw new Error(`Could not resolve ${source} directory plugin "${alias}" at "${directory}".`, { cause: error });
155
+ }
156
+ }
157
+ async function validateRoot(alias, packageDir) {
158
+ const manifestPath = join(packageDir, "package.json");
159
+ let packageJson;
160
+ try {
161
+ packageJson = JSON.parse(await readFile(manifestPath, "utf8"));
162
+ } catch (error) {
163
+ throw new Error(`Directory plugin "${alias}" has an unreadable or invalid package.json at "${manifestPath}".`, { cause: error });
164
+ }
165
+ const relativeEntry = resolveRelativeRootEntry(packageJson);
166
+ const entry = resolve(packageDir, relativeEntry);
167
+ if (!isPathInside(entry, packageDir)) throw new Error(`Directory plugin "${alias}" root export escapes the package directory.`);
168
+ let physicalEntry;
169
+ try {
170
+ physicalEntry = await realpath(entry);
171
+ } catch (error) {
172
+ if (isENOENTError(error)) throw new Error(`Directory plugin "${alias}" entry "${relativeEntry}" does not exist. Build the package and try again.`, { cause: error });
173
+ throw new Error(`Could not resolve directory plugin "${alias}" entry "${relativeEntry}".`, { cause: error });
174
+ }
175
+ if (!isPathInside(physicalEntry, packageDir)) throw new Error(`Directory plugin "${alias}" entry physically escapes the package directory.`);
176
+ let entryStat;
177
+ try {
178
+ entryStat = await stat(physicalEntry);
179
+ } catch (error) {
180
+ if (isENOENTError(error)) throw new Error(`Directory plugin "${alias}" entry "${relativeEntry}" does not exist. Build the package and try again.`, { cause: error });
181
+ throw new Error(`Could not inspect directory plugin "${alias}" entry "${relativeEntry}".`, { cause: error });
182
+ }
183
+ if (!entryStat.isFile()) throw new Error(`Directory plugin "${alias}" entry "${relativeEntry}" is not a regular file.`);
184
+ return physicalEntry;
185
+ }
186
+ //#endregion
187
+ //#region src/plugins/sources/package/index.ts
188
+ function createLockEntry(installed, options) {
189
+ return {
190
+ alias: options.alias,
191
+ entry: installed.entry,
192
+ integrity: installed.integrity,
193
+ name: installed.name,
194
+ registry: installed.registry,
195
+ specifier: options.specifier.raw,
196
+ tarball: installed.tarball,
197
+ type: "registry",
198
+ version: installed.version
199
+ };
200
+ }
201
+ async function install(options) {
202
+ const { name, segments, version } = options.specifier;
203
+ const npmRegistry = options.npmRegistry.endsWith("/") ? options.npmRegistry : `${options.npmRegistry}/`;
204
+ const dist = (await ofetch(`${npmRegistry.replace(/\/$/u, "")}/${options.specifier.registryPath}`)).versions?.[version]?.dist;
205
+ if (dist?.tarball === void 0) throw new Error(`Npm metadata for "${name}" does not include a tarball for version ${version}.`);
206
+ if (dist.integrity === void 0 || dist.integrity.trim() === "") throw new Error(`Npm metadata for "${name}" does not include integrity for version ${version}.`);
207
+ const storePath = getProjectPluginStorePath(options.cwd);
208
+ const packageDir = join(storePath, ...segments, version, "package");
209
+ if (!isPathInside(resolve(packageDir), resolve(storePath))) throw new Error(`Static plugin package "${name}" resolves outside the project plugin store.`);
210
+ const stagingPackageDir = join(dirname(packageDir), `package-staging-${randomUUID()}`);
211
+ let relativeEntry;
212
+ try {
213
+ const tarball = Buffer.from(await ofetch(dist.tarball, { responseType: "arrayBuffer" }));
214
+ checkIntegrity(tarball, dist.integrity, `${name}@${version}`);
215
+ await mkdir(stagingPackageDir, { recursive: true });
216
+ await extractTarball(tarball, stagingPackageDir);
217
+ relativeEntry = resolveRelativeRootEntry(JSON.parse(await readFile(join(stagingPackageDir, "package.json"), "utf8")));
218
+ await replaceDirectory(packageDir, stagingPackageDir);
219
+ } catch (error) {
220
+ await rm(stagingPackageDir, {
221
+ force: true,
222
+ recursive: true
223
+ });
224
+ throw error;
225
+ }
226
+ return {
227
+ entry: posix.join(".alint/plugins/store", ...segments, version, "package", relativeEntry.split(/[\\/]/u).join("/")),
228
+ integrity: dist.integrity,
229
+ name,
230
+ registry: npmRegistry,
231
+ tarball: dist.tarball,
232
+ type: "registry",
233
+ version
234
+ };
235
+ }
236
+ async function resolve$1(entry) {
237
+ const projectRoot = resolve(entry.cwd);
238
+ const pluginRoot = join(projectRoot, ".alint", "plugins", "store");
239
+ const rawEntryPath = entry.lockEntry.entry;
240
+ const resolvedEntry = isAbsolute(rawEntryPath) ? resolve(rawEntryPath) : resolve(projectRoot, rawEntryPath);
241
+ if (!isPathInside(resolvedEntry, projectRoot)) throw new Error(`Plugin lock entry "${entry.alias}" resolves outside the project root.`);
242
+ const [physicalEntry, physicalPluginRoot] = await Promise.all([resolvePhysicalPath(resolvedEntry), realpath(pluginRoot)]);
243
+ if (!isPathInside(physicalEntry, physicalPluginRoot)) throw new Error(`Plugin lock entry "${entry.alias}" resolves outside the plugin store.`);
244
+ const packageDir = getLockedPackageDir(projectRoot, pluginRoot, entry);
245
+ if (await realpath(packageDir) !== join(physicalPluginRoot, ...getPackagePathSegments(entry.lockEntry.name), entry.lockEntry.version, "package")) throw new Error(`Plugin lock entry "${entry.alias}" resolves outside the locked package directory.`);
246
+ if (!isPathInside(resolvedEntry, packageDir)) throw new Error(`Plugin lock entry "${entry.alias}" resolves outside the locked package directory.`);
247
+ JSON.parse(await readFile(join(packageDir, "package.json"), "utf8"));
248
+ return {
249
+ cache: "default",
250
+ entry: resolvedEntry
251
+ };
252
+ }
253
+ async function extractEntry(packageDir, header, stream) {
254
+ const entryName = header.name;
255
+ if (!entryName.startsWith("package/")) {
256
+ stream.resume();
257
+ return;
258
+ }
259
+ const relativeEntry = entryName.slice(8);
260
+ const normalized = posix.normalize(relativeEntry);
261
+ if (relativeEntry === "" || normalized === ".") {
262
+ stream.resume();
263
+ return;
264
+ }
265
+ if (normalized === ".." || normalized.startsWith("../") || posix.isAbsolute(normalized)) {
266
+ stream.resume();
267
+ throw new Error(`Plugin tarball entry "${entryName}" escapes the package directory.`);
268
+ }
269
+ const outputPath = resolve(packageDir, normalized);
270
+ if (!isPathInside(outputPath, packageDir)) {
271
+ stream.resume();
272
+ throw new Error(`Plugin tarball entry "${entryName}" escapes the package directory.`);
273
+ }
274
+ if (header.type === "directory") {
275
+ stream.resume();
276
+ await mkdir(outputPath, { recursive: true });
277
+ return;
278
+ }
279
+ if (header.type !== "file") {
280
+ stream.resume();
281
+ return;
282
+ }
283
+ await mkdir(dirname(outputPath), { recursive: true });
284
+ await pipeline(stream, createWriteStream(outputPath));
285
+ }
286
+ async function extractTarball(tarball, packageDir) {
287
+ const extract = tar.extract();
288
+ extract.on("entry", (header, stream, next) => {
289
+ extractEntry(packageDir, header, stream).then(() => next()).catch((error) => extract.destroy(error));
290
+ });
291
+ await pipeline(Readable.from(tarball), createGunzip(), extract);
292
+ }
293
+ function getLockedPackageDir(projectRoot, pluginRoot, entry) {
294
+ const name = entry.lockEntry.name;
295
+ const version = entry.lockEntry.version;
296
+ if (entry.specifier.name !== name) throw new Error(`Plugin lock entry "${entry.alias}" package name does not match its specifier.`);
297
+ if (entry.specifier.version !== version) throw new Error(`Plugin lock entry "${entry.alias}" package version does not match its specifier.`);
298
+ const packageDir = join(pluginRoot, ...getPackagePathSegments(name), version, "package");
299
+ if (!isPathInside(packageDir, projectRoot) || !isPathInside(packageDir, pluginRoot)) throw new Error(`Plugin lock entry "${entry.alias}" package metadata resolves outside the plugin store.`);
300
+ return packageDir;
301
+ }
302
+ function getPackagePathSegments(name) {
303
+ const segments = name.split("/");
304
+ const segmentPattern = /^[\w.-]+$/u;
305
+ if (segments.length === 2) {
306
+ const [scope, packageName] = segments;
307
+ if (scope === void 0 || packageName === void 0 || !scope.startsWith("@") || !segmentPattern.test(scope.slice(1)) || !segmentPattern.test(packageName)) throw new Error(`Invalid static plugin package name "${name}".`);
308
+ return segments;
309
+ }
310
+ if (segments.length !== 1 || !segmentPattern.test(segments[0])) throw new Error(`Invalid static plugin package name "${name}".`);
311
+ return segments;
312
+ }
313
+ async function nearestExistingParent(path) {
314
+ let current = path;
315
+ while (current !== dirname(current)) {
316
+ if (await exists(current)) return current;
317
+ current = dirname(current);
318
+ }
319
+ return current;
320
+ }
321
+ async function replaceDirectory(packageDir, stagingPackageDir) {
322
+ const backupPackageDir = join(dirname(packageDir), `package-backup-${randomUUID()}`);
323
+ let hasBackup = false;
324
+ let replaced = false;
325
+ try {
326
+ await rename(packageDir, backupPackageDir);
327
+ hasBackup = true;
328
+ } catch (error) {
329
+ if (!isENOENTError(error)) throw error;
330
+ }
331
+ try {
332
+ await rename(stagingPackageDir, packageDir);
333
+ replaced = true;
334
+ } catch (error) {
335
+ if (hasBackup) {
336
+ await rename(backupPackageDir, packageDir);
337
+ hasBackup = false;
338
+ }
339
+ throw error;
340
+ } finally {
341
+ if (replaced || !hasBackup) await rm(backupPackageDir, {
342
+ force: true,
343
+ recursive: true
344
+ });
345
+ }
346
+ }
347
+ async function resolvePhysicalPath(path) {
348
+ try {
349
+ return await realpath(path);
350
+ } catch (error) {
351
+ if (!isENOENTError(error)) throw error;
352
+ }
353
+ const parent = await nearestExistingParent(dirname(path));
354
+ return join(await realpath(parent), relative(parent, path));
355
+ }
356
+ //#endregion
357
+ //#region src/plugins/resolve.ts
358
+ function resolvePluginImportTarget(entry) {
359
+ return entry.type === "directory" ? resolve$2(entry) : resolve$1(entry);
360
+ }
361
+ //#endregion
362
+ //#region src/plugins/spec.ts
363
+ const explicitRelativePathPattern = /^\.{1,2}(?:[\\/]|$)/u;
364
+ function getPluginSpecifierKey(specifier) {
365
+ return specifier.type === "registry" ? `registry:${specifier.raw}` : `directory:${normalizeDirectoryIdentity(specifier.directory)}`;
366
+ }
367
+ function isDirectoryPluginSpecifier(value) {
368
+ return isAbsolute(value) || win32.isAbsolute(value) || explicitRelativePathPattern.test(value);
369
+ }
370
+ function parsePluginSpecifier(value, options = {}) {
371
+ if (/^file:/iu.test(value)) throw new Error("Static plugin file URLs are not supported. Use a relative or absolute directory path instead.");
372
+ if (isAbsolute(value) || win32.isAbsolute(value)) return {
373
+ directory: isAbsolute(value) ? normalize(value) : win32.normalize(value),
374
+ raw: value,
375
+ type: "directory"
376
+ };
377
+ if (explicitRelativePathPattern.test(value)) {
378
+ if (options.configFile === void 0) throw new Error(`Relative static plugin directory "${value}" requires a config file.`);
379
+ const relativePath = value.split(/[\\/]/u).join(sep);
380
+ return {
381
+ directory: resolve(dirname(options.configFile), relativePath),
382
+ raw: value,
383
+ type: "directory"
384
+ };
385
+ }
386
+ const versionSeparator = value.lastIndexOf("@");
387
+ if (versionSeparator <= 0 || versionSeparator === value.length - 1) throw new Error(`Static plugin specifier "${value}" must include an exact package version.`);
388
+ const name = value.slice(0, versionSeparator);
389
+ const segments = parsePackageNameSegments(name);
390
+ return {
391
+ name,
392
+ raw: value,
393
+ registryPath: name.startsWith("@") ? name.replace("/", "%2f") : encodeURIComponent(name),
394
+ segments,
395
+ type: "registry",
396
+ version: value.slice(versionSeparator + 1)
397
+ };
398
+ }
399
+ function normalizeDirectoryIdentity(directory) {
400
+ return isAbsolute(directory) ? normalize(directory) : win32.normalize(directory);
401
+ }
402
+ function parsePackageNameSegments(name) {
403
+ if (name === "" || name.includes("\\") || isAbsolute(name) || posix.isAbsolute(name)) throw new Error(`Invalid static plugin package name "${name}".`);
404
+ const segments = name.split("/");
405
+ if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) throw new Error(`Invalid static plugin package name "${name}".`);
406
+ const segmentPattern = /^[a-z0-9][a-z0-9._~-]*$/u;
407
+ if (name.startsWith("@")) {
408
+ const [scope, packageName, extraSegment] = segments;
409
+ if (segments.length !== 2 || scope === void 0 || packageName === void 0 || extraSegment !== void 0 || !scope.startsWith("@") || !segmentPattern.test(scope.slice(1)) || !segmentPattern.test(packageName)) throw new Error(`Invalid static plugin package name "${name}".`);
410
+ return segments;
411
+ }
412
+ if (segments.length !== 1 || !segmentPattern.test(segments[0])) throw new Error(`Invalid static plugin package name "${name}".`);
413
+ return segments;
414
+ }
415
+ //#endregion
416
+ //#region src/plugins/lock.ts
417
+ const PluginLockEntrySchema = variant("type", [strictObject({
418
+ alias: string(),
419
+ path: string(),
420
+ specifier: string(),
421
+ type: literal("directory")
422
+ }), strictObject({
423
+ alias: string(),
424
+ entry: string(),
425
+ integrity: string(),
426
+ name: string(),
427
+ registry: string(),
428
+ specifier: string(),
429
+ tarball: string(),
430
+ type: literal("registry"),
431
+ version: string()
432
+ })]);
433
+ const PluginLockFileSchema = strictObject({
434
+ plugins: record(string(), PluginLockEntrySchema),
435
+ version: literal(2)
436
+ });
437
+ function createEmptyPluginLockFile() {
438
+ return {
439
+ plugins: {},
440
+ version: 2
441
+ };
442
+ }
443
+ function listMissing(config, lock) {
444
+ const missing = [];
445
+ for (const group of config.groups) for (const reference of group.plugins) if (lock.find(reference) === void 0) missing.push(reference);
446
+ return missing;
447
+ }
448
+ async function listUnresolved(config, lock) {
449
+ const unresolved = [];
450
+ const checkedAliases = /* @__PURE__ */ new Set();
451
+ for (const group of config.groups) for (const reference of group.plugins) {
452
+ const entry = lock.find(reference);
453
+ if (entry === void 0 || checkedAliases.has(entry.alias)) continue;
454
+ checkedAliases.add(entry.alias);
455
+ try {
456
+ await resolvePluginImportTarget(entry);
457
+ } catch (error) {
458
+ unresolved.push({
459
+ ...entry,
460
+ resolutionError: error
461
+ });
462
+ }
463
+ }
464
+ return unresolved;
465
+ }
466
+ async function loadPluginLockFile(cwd) {
467
+ try {
468
+ return parsePluginLockFileValue(await readFile(getProjectPluginLockPath(cwd), "utf8"));
469
+ } catch (error) {
470
+ if (isENOENTError(error)) return createEmptyPluginLockFile();
471
+ throw error;
472
+ }
473
+ }
474
+ function parsePluginLockFile(value, options) {
475
+ const file = parsePluginLockFileValue(value);
476
+ const entries = Object.values(file.plugins).map((entry) => {
477
+ if (entry.type === "registry") {
478
+ const specifier = parsePluginSpecifier(entry.specifier);
479
+ if (specifier.type !== "registry") throw new Error(`Registry plugin lock entry "${entry.alias}" must use a registry specifier.`);
480
+ return {
481
+ alias: entry.alias,
482
+ cwd: options.cwd,
483
+ lockEntry: entry,
484
+ specifier,
485
+ type: "registry"
486
+ };
487
+ }
488
+ const specifier = parsePluginSpecifier(resolveDirectoryLockIdentity(entry.path, options.cwd));
489
+ if (specifier.type !== "directory") throw new Error(`Directory plugin lock entry "${entry.alias}" must use a directory path.`);
490
+ return {
491
+ alias: entry.alias,
492
+ cwd: options.cwd,
493
+ lockEntry: entry,
494
+ specifier: {
495
+ ...specifier,
496
+ raw: entry.specifier
497
+ },
498
+ type: "directory"
499
+ };
500
+ });
501
+ const byAlias = new Map(entries.map((entry) => [entry.alias, entry]));
502
+ return {
503
+ cwd: options.cwd,
504
+ entries,
505
+ file,
506
+ find(reference) {
507
+ const entry = byAlias.get(reference.alias);
508
+ return entry !== void 0 && matchesReference(entry, reference) ? associateCurrentSpecifier(entry, reference) : void 0;
509
+ },
510
+ get(reference) {
511
+ const entry = byAlias.get(reference.alias);
512
+ const expected = reference.specifier.raw;
513
+ if (entry === void 0) throw new Error(`Plugin "${reference.alias}" requires ${expected}, but no matching lock entry exists.\nRun: alint plugin install`);
514
+ if (!matchesReference(entry, reference)) throw new Error(`Plugin "${reference.alias}" is locked to ${entry.lockEntry.specifier}, but config requires ${expected}.\nRun: alint plugin install`);
515
+ return associateCurrentSpecifier(entry, reference);
516
+ }
517
+ };
518
+ }
519
+ async function writePluginLockFile(cwd, lockFile) {
520
+ const content = `${JSON.stringify(parsePluginLockFileValue(lockFile), null, 2)}\n`;
521
+ const lockPath = getProjectPluginLockPath(cwd);
522
+ const lockDir = dirname(lockPath);
523
+ const tempPath = join(lockDir, `lock.${randomUUID()}.tmp`);
524
+ await mkdir(lockDir, { recursive: true });
525
+ try {
526
+ await writeFile(tempPath, content, "utf8");
527
+ await rename(tempPath, lockPath);
528
+ } catch (error) {
529
+ await rm(tempPath, { force: true });
530
+ throw error;
531
+ }
532
+ }
533
+ function associateCurrentSpecifier(entry, reference) {
534
+ if (entry.type === "directory" && reference.specifier.type === "directory") return {
535
+ ...entry,
536
+ specifier: reference.specifier
537
+ };
538
+ return entry;
539
+ }
540
+ function matchesReference(entry, reference) {
541
+ if (entry.type !== reference.specifier.type) return false;
542
+ if (entry.type === "directory") return entry.lockEntry.specifier === reference.specifier.raw || getPluginSpecifierKey(entry.specifier) === getPluginSpecifierKey(reference.specifier);
543
+ return getPluginSpecifierKey(entry.specifier) === getPluginSpecifierKey(reference.specifier);
544
+ }
545
+ function parsePluginLockFileValue(value) {
546
+ const parsedValue = typeof value === "string" ? JSON.parse(value) : value;
547
+ if (typeof parsedValue === "object" && parsedValue !== null && "version" in parsedValue && parsedValue.version === 1) throw new Error("Unsupported plugin lock version 1. Run: alint plugin install");
548
+ const file = parse(PluginLockFileSchema, parsedValue);
549
+ for (const [alias, entry] of Object.entries(file.plugins)) {
550
+ if (entry.alias !== alias) throw new Error(`Plugin lock entry key "${alias}" must match alias "${entry.alias}".`);
551
+ if (entry.type === "registry") {
552
+ parseIntegrity(entry.integrity, entry.specifier);
553
+ if (isDirectoryPluginSpecifier(entry.specifier)) throw new Error(`Registry plugin lock entry "${alias}" must use a registry specifier.`);
554
+ const specifier = parsePluginSpecifier(entry.specifier);
555
+ if (specifier.type !== "registry" || specifier.name !== entry.name || specifier.version !== entry.version) throw new Error(`Registry plugin lock entry "${alias}" identity does not match specifier "${entry.specifier}".`);
556
+ } else if (!isDirectoryPluginSpecifier(entry.specifier)) throw new Error(`Directory plugin lock entry "${alias}" must use a directory specifier.`);
557
+ }
558
+ return file;
559
+ }
560
+ function resolveDirectoryLockIdentity(path, cwd) {
561
+ return isAbsolute(path) || win32.isAbsolute(path) ? path : resolve(cwd, path);
562
+ }
563
+ //#endregion
564
+ //#region src/plugins/sources/import.ts
565
+ async function importPlugin(target) {
566
+ const url = pathToFileURL(target.entry);
567
+ if (target.cache === "content") url.searchParams.set("content", createHash("sha256").update(await readFile(target.entry)).digest("hex"));
568
+ return getDefaultExport(await import(url.href));
569
+ }
570
+ function getDefaultExport(value) {
571
+ if (!isPlainObject(value)) return value;
572
+ const objectValue = value;
573
+ return Object.hasOwn(objectValue, "default") ? Reflect.get(objectValue, "default") : value;
574
+ }
575
+ //#endregion
576
+ //#region src/config/static.ts
577
+ const filePatternSchema = union([string(), array(string())]);
578
+ const ruleSeveritySchema = picklist([
579
+ "error",
580
+ "off",
581
+ "warn"
582
+ ]);
583
+ const ruleConfigEntrySchema = union([ruleSeveritySchema, tuple([ruleSeveritySchema])]);
584
+ const staticConfigItemSchema = looseObject({
585
+ agent: optional(unknown()),
586
+ basePath: optional(string()),
587
+ extends: optional(array(unknown())),
588
+ files: optional(array(filePatternSchema)),
589
+ ignore: optional(looseObject({ gitignore: optional(boolean()) })),
590
+ ignores: optional(array(string())),
591
+ language: optional(string()),
592
+ languageOptions: optional(record(string(), unknown())),
593
+ linterOptions: optional(looseObject({
594
+ noInlineConfig: optional(boolean()),
595
+ reportUnusedDisableDirectives: optional(ruleSeveritySchema)
596
+ })),
597
+ name: optional(string()),
598
+ plugins: optional(record(string(), unknown())),
599
+ processor: optional(unknown()),
600
+ rules: optional(record(string(), ruleConfigEntrySchema)),
601
+ runner: optional(unknown()),
602
+ settings: optional(record(string(), unknown()))
603
+ });
604
+ function parseStaticConfig(value, options = {}) {
605
+ const items = toAlintConfigItems(value, options);
606
+ const pluginsByAlias = /* @__PURE__ */ new Map();
607
+ return { groups: items.map((item) => {
608
+ return {
609
+ item,
610
+ plugins: readStaticPluginReferences(item, pluginsByAlias, options)
611
+ };
612
+ }) };
613
+ }
614
+ async function toAlintConfig(config, options) {
615
+ const pluginCache = /* @__PURE__ */ new Map();
616
+ return Promise.all(config.groups.map((group) => resolveStaticPlugins(group, options, pluginCache)));
617
+ }
618
+ function isPlainObject$2(value) {
619
+ return typeof value === "object" && value !== null && !Array.isArray(value);
620
+ }
621
+ function isTomlConfig(configFile) {
622
+ return extname(configFile ?? "").toLowerCase() === ".toml";
623
+ }
624
+ function parseStaticConfigInputArray(value, arrayStack = []) {
625
+ if (arrayStack.includes(value)) throw new Error("Circular static config array.");
626
+ return value.flatMap((item) => Array.isArray(item) ? parseStaticConfigInputArray(item, [...arrayStack, value]) : [parseStaticConfigItem(item)]);
627
+ }
628
+ function parseStaticConfigItem(value) {
629
+ if (Array.isArray(value)) throw new TypeError("Invalid type: Expected Object but received Array");
630
+ return parse(staticConfigItemSchema, value);
631
+ }
632
+ function parseStaticConfigItems(value) {
633
+ return value.map(parseStaticConfigItem);
634
+ }
635
+ function readConfigGroup(value) {
636
+ if (!isPlainObject$2(value)) return;
637
+ const wrapper = value;
638
+ if (wrapper.config === void 0) return;
639
+ if (!isPlainObject$2(wrapper.config) || !Array.isArray(wrapper.config.group)) throw new TypeError("Static config field \"config.group\" must be an array.");
640
+ return wrapper.config.group;
641
+ }
642
+ function readStaticPluginReferences(item, pluginsByAlias, options) {
643
+ if (!isPlainObject$2(item.plugins)) return [];
644
+ const references = [];
645
+ for (const [alias, plugin] of Object.entries(item.plugins)) {
646
+ if (typeof plugin !== "string") continue;
647
+ const specifier = parsePluginSpecifier(plugin, options);
648
+ const existing = pluginsByAlias.get(alias);
649
+ if (existing !== void 0 && getPluginSpecifierKey(existing) !== getPluginSpecifierKey(specifier)) throw new Error(`Static plugin "${alias}" is configured with multiple specifiers: "${existing.raw}" and "${specifier.raw}".`);
650
+ pluginsByAlias.set(alias, specifier);
651
+ references.push({
652
+ alias,
653
+ specifier
654
+ });
655
+ }
656
+ return references;
657
+ }
658
+ async function resolveStaticPlugins(group, options, pluginCache) {
659
+ if (group.item.plugins === void 0) {
660
+ const { plugins: _plugins, ...item } = group.item;
661
+ return item;
662
+ }
663
+ const plugins = {};
664
+ for (const [alias, plugin] of Object.entries(group.item.plugins)) {
665
+ if (typeof plugin !== "string") {
666
+ plugins[alias] = plugin;
667
+ continue;
668
+ }
669
+ const reference = group.plugins.find((item) => item.alias === alias);
670
+ if (reference === void 0) throw new Error(`Static plugin "${alias}" is missing a parsed plugin reference.`);
671
+ const pluginCacheKey = getPluginSpecifierKey(reference.specifier);
672
+ let resolvedPlugin = pluginCache.get(pluginCacheKey);
673
+ if (resolvedPlugin === void 0) {
674
+ resolvedPlugin = options.pluginResolver(reference);
675
+ pluginCache.set(pluginCacheKey, resolvedPlugin);
676
+ }
677
+ plugins[alias] = await resolvedPlugin;
678
+ }
679
+ return {
680
+ ...group.item,
681
+ plugins
682
+ };
683
+ }
684
+ function toAlintConfigItems(value, options = {}) {
685
+ if (Array.isArray(value)) return parseStaticConfigInputArray(value);
686
+ const group = readConfigGroup(value);
687
+ if (group !== void 0) return parseStaticConfigItems(group);
688
+ if (isTomlConfig(options.configFile)) throw new Error("Static TOML config must use [[config.group]].");
689
+ if (value === void 0 || value === null) return [];
690
+ throw new Error("Static config must be a flat config array or { config: { group: [...] } }.");
691
+ }
692
+ //#endregion
8
693
  //#region src/config/load.ts
9
694
  async function loadAlintConfig(cwd, configFile) {
695
+ const staticConfig = await loadStaticConfig(cwd, configFile);
696
+ if (staticConfig.groups.flatMap((group) => group.plugins).length === 0) return toAlintConfig(staticConfig, { pluginResolver: async (reference) => {
697
+ throw new Error(`Static plugin "${reference.alias}" was not expected to require resolution.`);
698
+ } });
699
+ const lock = parsePluginLockFile(await loadPluginLockFile(cwd), { cwd });
700
+ const missing = listMissing(staticConfig, lock);
701
+ if (missing.length > 0) throw new Error(`Static plugin references are missing from the lock file: ${formatStaticPluginReferences(missing)}.\nRun: alint plugin install`);
702
+ const unresolved = await listUnresolved(staticConfig, lock);
703
+ if (unresolved.length > 0) {
704
+ const message = `Static plugins could not be resolved from the lock file: ${formatPluginLockEntries(unresolved)}.\nRun: alint plugin install`;
705
+ throw new Error(message, { cause: unresolved[0]?.resolutionError });
706
+ }
707
+ return toAlintConfig(staticConfig, { async pluginResolver(reference) {
708
+ return importPlugin(await resolvePluginImportTarget(lock.get(reference)));
709
+ } });
710
+ }
711
+ async function loadStaticConfig(cwd, configFile) {
10
712
  const result = await loadConfig({
11
713
  configFile,
12
714
  cwd,
13
715
  dotenv: true,
716
+ jiti: createJiti(resolve(cwd, configFile ?? "alint.config"), {
717
+ interopDefault: true,
718
+ moduleCache: false
719
+ }),
14
720
  name: "alint"
15
721
  });
16
- if (result._configFile === void 0) return [];
17
- return result.config ?? [];
722
+ if (result._configFile === void 0) return parseStaticConfig(void 0);
723
+ return parseStaticConfig(result.config, { configFile: result._configFile });
724
+ }
725
+ function formatPluginLockEntries(entries) {
726
+ return entries.map((entry) => `${entry.alias} (${entry.specifier.raw})`).join(", ");
727
+ }
728
+ function formatStaticPluginReferences(references) {
729
+ return references.map((reference) => `${reference.alias} (${reference.specifier.raw})`).join(", ");
18
730
  }
19
731
  //#endregion
20
- //#region src/ignore-patterns.ts
732
+ //#region src/constants/patterns/ignore.ts
21
733
  const ignorePatternsEslintDefaults = [
22
734
  "**/node_modules/**",
23
735
  ".git/**",
@@ -57,22 +769,61 @@ const ignorePatternsCommon = [
57
769
  ...ignorePatternsGenerated
58
770
  ];
59
771
  //#endregion
60
- //#region src/paths.ts
61
- function getGlobalSetupConfigPath(env = process.env, options = {}) {
62
- return join(resolveAlintHome(env, options), "config.toml");
63
- }
64
- function getProjectSetupConfigPath(cwd) {
65
- return join(cwd, ".alint", "config.toml");
66
- }
67
- function getStatsDir(env = process.env, options = {}) {
68
- return join(resolveAlintHome(env, options), "stats");
69
- }
70
- function resolveAlintHome(env, options) {
71
- return join(env.XDG_CONFIG_HOME ?? resolveDefaultConfigHome(options), "alint");
72
- }
73
- function resolveDefaultConfigHome(options) {
74
- if (options.isMacOS ?? process.platform === "darwin") return join(homedir(), ".config");
75
- return options.xdgConfig ?? xdgConfig ?? join(homedir(), ".config");
772
+ //#region src/plugins/install.ts
773
+ const DEFAULT_REGISTRY = "https://registry.npmjs.org/";
774
+ async function installStaticPlugins(options) {
775
+ const configuredPlugins = (await loadStaticConfig(options.cwd, options.configFile)).groups.flatMap((group) => group.plugins);
776
+ const npmRegistry = options.registry ?? DEFAULT_REGISTRY;
777
+ const lockFile = createEmptyPluginLockFile();
778
+ const packageInstallations = /* @__PURE__ */ new Map();
779
+ const localDirectoryInstallations = /* @__PURE__ */ new Map();
780
+ const installedLocalDirectories = /* @__PURE__ */ new Map();
781
+ for (const configuredPlugin of configuredPlugins) {
782
+ const specifier = configuredPlugin.specifier;
783
+ if (specifier.type === "directory") {
784
+ const key = getPluginSpecifierKey(specifier);
785
+ let installation = localDirectoryInstallations.get(key);
786
+ if (installation === void 0) {
787
+ installation = install$1({
788
+ alias: configuredPlugin.alias,
789
+ specifier
790
+ });
791
+ localDirectoryInstallations.set(key, installation);
792
+ }
793
+ const installed = await installation;
794
+ const canonical = installed.path;
795
+ const entry = installedLocalDirectories.get(canonical) ?? installed;
796
+ installedLocalDirectories.set(canonical, entry);
797
+ lockFile.plugins[configuredPlugin.alias] = await createLockEntry$1(entry, {
798
+ alias: configuredPlugin.alias,
799
+ cwd: options.cwd,
800
+ specifier
801
+ });
802
+ continue;
803
+ }
804
+ const key = getPluginSpecifierKey(specifier);
805
+ let packageInstallation = packageInstallations.get(key);
806
+ if (packageInstallation === void 0) {
807
+ packageInstallation = install({
808
+ cwd: options.cwd,
809
+ npmRegistry,
810
+ specifier
811
+ });
812
+ packageInstallations.set(key, packageInstallation);
813
+ }
814
+ const installedPackage = await packageInstallation;
815
+ lockFile.plugins[configuredPlugin.alias] = createLockEntry(installedPackage, {
816
+ alias: configuredPlugin.alias,
817
+ specifier
818
+ });
819
+ }
820
+ await writePluginLockFile(options.cwd, lockFile);
821
+ return {
822
+ configuredPluginCount: configuredPlugins.length,
823
+ installedLocalDirectoryCount: installedLocalDirectories.size,
824
+ installedPackageCount: packageInstallations.size,
825
+ lock: lockFile
826
+ };
76
827
  }
77
828
  //#endregion
78
829
  //#region src/setup/toml.ts
@@ -82,7 +833,7 @@ const modelSizes = /* @__PURE__ */ new Set([
82
833
  "small"
83
834
  ]);
84
835
  function parseSetupConfigToml(toml) {
85
- const rawConfig = parse(toml);
836
+ const rawConfig = parse$1(toml);
86
837
  if (rawConfig.version !== 1) throw new Error("Invalid setup config: version must be 1.");
87
838
  if (!Array.isArray(rawConfig.providers)) throw new TypeError("Invalid setup config: providers must be an array.");
88
839
  const parsedConfig = {
@@ -103,7 +854,7 @@ function stringifySetupConfigToml(config) {
103
854
  function isModelSize(value) {
104
855
  return typeof value === "string" && modelSizes.has(value);
105
856
  }
106
- function isPlainObject(value) {
857
+ function isPlainObject$1(value) {
107
858
  return typeof value === "object" && value !== null && !Array.isArray(value);
108
859
  }
109
860
  function parseModel(providerId, model) {
@@ -149,7 +900,7 @@ function parseRunner(runner) {
149
900
  }
150
901
  function parseRunnerCache(cache) {
151
902
  if (typeof cache === "boolean") return cache;
152
- if (!isPlainObject(cache)) throw new TypeError("Invalid runner cache: must be a boolean or table.");
903
+ if (!isPlainObject$1(cache)) throw new TypeError("Invalid runner cache: must be a boolean or table.");
153
904
  const tomlCache = cache;
154
905
  const parsedCache = {};
155
906
  if (tomlCache.enabled !== void 0) {
@@ -161,7 +912,7 @@ function parseRunnerCache(cache) {
161
912
  }
162
913
  function parseRunnerStats(stats) {
163
914
  if (typeof stats === "boolean") return stats;
164
- if (!isPlainObject(stats)) throw new TypeError("Invalid runner stats: must be a boolean or an object.");
915
+ if (!isPlainObject$1(stats)) throw new TypeError("Invalid runner stats: must be a boolean or an object.");
165
916
  const tomlStats = stats;
166
917
  const parsedStats = {};
167
918
  if (tomlStats.enabled !== void 0) {
@@ -182,7 +933,7 @@ function readNonEmptyString(value, label) {
182
933
  return stringValue;
183
934
  }
184
935
  function readRecord(value, label) {
185
- if (!isPlainObject(value)) throw new Error(`Invalid ${label}: must be a table.`);
936
+ if (!isPlainObject$1(value)) throw new Error(`Invalid ${label}: must be a table.`);
186
937
  return value;
187
938
  }
188
939
  function readString(value, label) {
@@ -248,7 +999,7 @@ async function loadSetupConfig(filePath) {
248
999
  try {
249
1000
  return parseSetupConfigToml(await readFile(filePath, "utf8"));
250
1001
  } catch (error) {
251
- if (isNodeError(error) && error.code === "ENOENT") return createEmptySetupConfig();
1002
+ if (isENOENTError(error)) return createEmptySetupConfig();
252
1003
  throw error;
253
1004
  }
254
1005
  }
@@ -321,9 +1072,6 @@ function createEmptySetupConfig() {
321
1072
  version: 1
322
1073
  };
323
1074
  }
324
- function isNodeError(error) {
325
- return error instanceof Error && "code" in error;
326
- }
327
1075
  function mergeModel(existingModel, incomingModel) {
328
1076
  const existing = cloneModel(existingModel);
329
1077
  const incoming = cloneModel(incomingModel);
@@ -342,8 +1090,8 @@ function prioritizeProviders(providers, prioritizedProviders) {
342
1090
  //#endregion
343
1091
  //#region src/setup/write.ts
344
1092
  async function writeSetupConfig(filePath, config) {
345
- await mkdir(dirname(filePath), { recursive: true });
1093
+ await mkdir(dirname$1(filePath), { recursive: true });
346
1094
  await writeFile(filePath, stringifySetupConfigToml(config), "utf8");
347
1095
  }
348
1096
  //#endregion
349
- export { emptySetupConfig, getGlobalSetupConfigPath, getProjectSetupConfigPath, getStatsDir, ignorePatternsAIAgents, ignorePatternsBuildOutputs, ignorePatternsCaches, ignorePatternsCommon, ignorePatternsEslintDefaults, ignorePatternsGenerated, loadAlintConfig, loadSetupConfig, mergeSetupConfigs, parseSetupConfigToml, stringifySetupConfigToml, writeSetupConfig };
1097
+ export { createEmptyPluginLockFile, emptySetupConfig, getGlobalSetupConfigPath, getProjectPluginLockPath, getProjectPluginStorePath, getProjectSetupConfigPath, getStatsDir, ignorePatternsAIAgents, ignorePatternsBuildOutputs, ignorePatternsCaches, ignorePatternsCommon, ignorePatternsEslintDefaults, ignorePatternsGenerated, installStaticPlugins, listMissing, listUnresolved, loadAlintConfig, loadPluginLockFile, loadSetupConfig, loadStaticConfig, mergeSetupConfigs, parsePluginLockFile, parsePluginSpecifier, parseSetupConfigToml, parseStaticConfig, stringifySetupConfigToml, toAlintConfig, writePluginLockFile, writeSetupConfig };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@alint-js/config",
3
3
  "type": "module",
4
- "version": "0.0.18",
4
+ "version": "0.0.20",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.mts",
@@ -13,14 +13,24 @@
13
13
  "dist"
14
14
  ],
15
15
  "dependencies": {
16
+ "@moeru/std": "^0.1.0-beta.18",
17
+ "@package-json/types": "^0.0.13",
16
18
  "c12": "^3.3.4",
19
+ "es-toolkit": "^1.49.0",
20
+ "jiti": "^2.7.0",
21
+ "ofetch": "^1.5.1",
17
22
  "pathe": "^2.0.3",
23
+ "resolve.exports": "2.0.3",
18
24
  "smol-toml": "^1.7.0",
25
+ "tar-stream": "^3.2.0",
26
+ "valibot": "^1.4.1",
19
27
  "xdg-basedir": "^5.1.0",
20
- "@alint-js/core": "0.0.18"
28
+ "@alint-js/core": "0.0.20"
21
29
  },
22
30
  "devDependencies": {
23
31
  "@types/node": "^26.0.1",
32
+ "@types/tar-stream": "^3.1.4",
33
+ "h3": "2.0.1-rc.25",
24
34
  "tsdown": "^0.22.3",
25
35
  "typescript": "^6.0.3",
26
36
  "vitest": "^4.1.9"