@twinklerg/coden 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -0
  3. package/dist/index.js +21269 -0
  4. package/package.json +48 -0
  5. package/src/cli/agent-command.ts +497 -0
  6. package/src/cli/format.ts +42 -0
  7. package/src/cli/index.ts +149 -0
  8. package/src/cli/plugin-command.ts +217 -0
  9. package/src/config/config.ts +96 -0
  10. package/src/config/trust.ts +35 -0
  11. package/src/context/manager.ts +186 -0
  12. package/src/context/truncate.ts +9 -0
  13. package/src/core/events.ts +32 -0
  14. package/src/core/runtime.ts +402 -0
  15. package/src/core/types.ts +97 -0
  16. package/src/index.ts +14 -0
  17. package/src/observability/terminal.ts +201 -0
  18. package/src/observability/trace.ts +30 -0
  19. package/src/permissions/policy.ts +56 -0
  20. package/src/permissions/workspace.ts +139 -0
  21. package/src/plugins/api.ts +68 -0
  22. package/src/plugins/bun-package-manager.ts +35 -0
  23. package/src/plugins/installed-loader.ts +144 -0
  24. package/src/plugins/installer.ts +314 -0
  25. package/src/plugins/manifest.ts +89 -0
  26. package/src/plugins/package-manager.ts +10 -0
  27. package/src/plugins/package-metadata.ts +95 -0
  28. package/src/plugins/paths.ts +43 -0
  29. package/src/plugins/specifier.ts +63 -0
  30. package/src/plugins/transaction.ts +403 -0
  31. package/src/process/runner.ts +134 -0
  32. package/src/providers/anthropic.ts +117 -0
  33. package/src/providers/openai.ts +96 -0
  34. package/src/providers/scripted.ts +28 -0
  35. package/src/sessions/store.ts +278 -0
  36. package/src/tools/builtin/bash.ts +56 -0
  37. package/src/tools/builtin/edit.ts +42 -0
  38. package/src/tools/builtin/index.ts +9 -0
  39. package/src/tools/builtin/read.ts +91 -0
  40. package/src/tools/builtin/write.ts +34 -0
  41. package/src/tools/executor.ts +90 -0
  42. package/src/tools/plugin-loader.ts +122 -0
  43. package/src/tools/registry.ts +97 -0
@@ -0,0 +1,314 @@
1
+ import { constants } from "node:fs";
2
+ import { access, copyFile, mkdir, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { CodeNError, type ToolDefinition } from "../core/types.js";
5
+ import {
6
+ composePackageRegistry,
7
+ type InstalledPluginLoader,
8
+ type LoadedPackagePlugin,
9
+ type PackagePluginFailure,
10
+ } from "./installed-loader.js";
11
+ import {
12
+ type PluginManifest,
13
+ readPluginManifest,
14
+ runtimePackageJson,
15
+ serializePluginManifest,
16
+ } from "./manifest.js";
17
+ import type { PackageManager } from "./package-manager.js";
18
+ import { readInstalledPackageMetadata } from "./package-metadata.js";
19
+ import { type PluginPaths, type PluginScope, resolvePluginPaths } from "./paths.js";
20
+ import { isValidNpmPackageName, parseNpmPluginSpecifier } from "./specifier.js";
21
+ import { PluginTransaction, type PluginTransactionCandidate } from "./transaction.js";
22
+
23
+ export interface PluginOperationOptions {
24
+ scope: PluginScope;
25
+ allowScripts: boolean;
26
+ signal?: AbortSignal;
27
+ }
28
+
29
+ export interface InstalledPluginSummary {
30
+ packageName: string;
31
+ requested: string;
32
+ version: string;
33
+ tools: string[];
34
+ scope: PluginScope;
35
+ }
36
+
37
+ export interface ListedPlugin extends InstalledPluginSummary {
38
+ shadowedByProject: boolean;
39
+ }
40
+
41
+ export class PluginInstaller {
42
+ constructor(
43
+ private readonly workspace: string,
44
+ private readonly dataDir: string,
45
+ private readonly packageManager: PackageManager,
46
+ private readonly loader: InstalledPluginLoader,
47
+ private readonly builtins: ToolDefinition[],
48
+ ) {}
49
+
50
+ async install(raw: string, options: PluginOperationOptions): Promise<InstalledPluginSummary> {
51
+ const specifier = parseNpmPluginSpecifier(raw);
52
+ const paths = this.paths(options.scope);
53
+ await new PluginTransaction(paths).recover();
54
+ const current = await readPluginManifest(paths.manifestPath);
55
+
56
+ if (current.plugins[specifier.packageName]?.requested === specifier.requested) {
57
+ const existing = await this.loader.loadScope(paths);
58
+ const plugin = existing.loaded.find((item) => item.packageName === specifier.packageName);
59
+ if (existing.failed.length === 0 && plugin)
60
+ return summaryFor(plugin, specifier.requested, options.scope);
61
+ }
62
+
63
+ const next = cloneManifest(current);
64
+ next.plugins[specifier.packageName] = { source: "npm", requested: specifier.requested };
65
+
66
+ let installed: LoadedPackagePlugin[] = [];
67
+ await new PluginTransaction(paths).run(async (candidate) => {
68
+ installed = await this.buildCandidate(candidate, next, paths, options, false);
69
+ await this.validateCandidate(paths.scope, installed);
70
+ });
71
+
72
+ const plugin = installed.find((item) => item.packageName === specifier.packageName);
73
+ if (!plugin)
74
+ throw packageLoadFailure([
75
+ {
76
+ packageName: specifier.packageName,
77
+ path: paths.runtimeDir,
78
+ message: "plugin did not load",
79
+ },
80
+ ]);
81
+ return summaryFor(plugin, specifier.requested, options.scope);
82
+ }
83
+
84
+ async remove(packageName: string, options: PluginOperationOptions): Promise<void> {
85
+ if (!isValidNpmPackageName(packageName)) throw invalidPackageName(packageName);
86
+ const paths = this.paths(options.scope);
87
+ await new PluginTransaction(paths).recover();
88
+ const current = await readPluginManifest(paths.manifestPath);
89
+ if (!current.plugins[packageName]) {
90
+ throw new CodeNError(
91
+ "plugin",
92
+ "plugin.install_failed",
93
+ `plugin.install_failed: ${packageName} is not installed in ${options.scope} scope`,
94
+ );
95
+ }
96
+
97
+ const next = cloneManifest(current);
98
+ delete next.plugins[packageName];
99
+
100
+ await new PluginTransaction(paths).run(async (candidate) => {
101
+ const installed = await this.buildCandidate(candidate, next, paths, options, false);
102
+ await this.validateCandidate(paths.scope, installed);
103
+ });
104
+ }
105
+
106
+ async sync(options: PluginOperationOptions): Promise<InstalledPluginSummary[]> {
107
+ const paths = this.paths(options.scope);
108
+ await new PluginTransaction(paths).recover();
109
+ const manifest = await readPluginManifest(paths.manifestPath);
110
+ if (
111
+ options.scope === "project" &&
112
+ Object.keys(manifest.plugins).length > 0 &&
113
+ !(await pathExists(path.join(paths.runtimeDir, "bun.lock")))
114
+ ) {
115
+ throw new CodeNError(
116
+ "plugin",
117
+ "plugin.lock_missing",
118
+ `plugin.lock_missing: ${options.scope} plugin sync requires ${path.join(paths.runtimeDir, "bun.lock")}`,
119
+ );
120
+ }
121
+
122
+ let installed: LoadedPackagePlugin[] = [];
123
+ await new PluginTransaction(paths).run(async (candidate) => {
124
+ installed = await this.buildCandidate(candidate, manifest, paths, options, true);
125
+ await this.validateCandidate(paths.scope, installed);
126
+ });
127
+
128
+ return installed.map((plugin) =>
129
+ summaryFor(
130
+ plugin,
131
+ manifest.plugins[plugin.packageName]?.requested ?? "latest",
132
+ options.scope,
133
+ ),
134
+ );
135
+ }
136
+
137
+ async list(): Promise<{ project: ListedPlugin[]; global: ListedPlugin[] }> {
138
+ const [project, global] = await Promise.all([
139
+ this.listScope(this.paths("project")),
140
+ this.listScope(this.paths("global")),
141
+ ]);
142
+ const projectNames = new Set(project.map((item) => item.packageName));
143
+ return {
144
+ project,
145
+ global: global.map((item) => ({
146
+ ...item,
147
+ shadowedByProject: projectNames.has(item.packageName),
148
+ })),
149
+ };
150
+ }
151
+
152
+ private paths(scope: PluginScope): PluginPaths {
153
+ return resolvePluginPaths(this.workspace, scope, this.dataDir);
154
+ }
155
+
156
+ private async buildCandidate(
157
+ candidate: PluginTransactionCandidate,
158
+ manifest: PluginManifest,
159
+ sourcePaths: PluginPaths,
160
+ options: PluginOperationOptions,
161
+ frozenLockfile: boolean,
162
+ ): Promise<LoadedPackagePlugin[]> {
163
+ await mkdir(candidate.runtimeDir, { recursive: true });
164
+ await writeFile(candidate.manifestPath, serializePluginManifest(manifest), "utf8");
165
+ await writeFile(
166
+ path.join(candidate.runtimeDir, "package.json"),
167
+ `${JSON.stringify(runtimePackageJson(manifest), null, 2)}\n`,
168
+ "utf8",
169
+ );
170
+ if (await pathExists(path.join(sourcePaths.runtimeDir, "bun.lock"))) {
171
+ await copyFile(
172
+ path.join(sourcePaths.runtimeDir, "bun.lock"),
173
+ path.join(candidate.runtimeDir, "bun.lock"),
174
+ );
175
+ }
176
+
177
+ try {
178
+ await this.packageManager.install({
179
+ cwd: candidate.runtimeDir,
180
+ frozenLockfile,
181
+ allowScripts: options.allowScripts,
182
+ ...(options.signal ? { signal: options.signal } : {}),
183
+ });
184
+ } catch (error) {
185
+ throw mapPackageManagerError(error, frozenLockfile);
186
+ }
187
+
188
+ await writeEmptyManifestLockfile(candidate.runtimeDir, manifest);
189
+ await writeRuntimeGitignore(candidate.runtimeDir, sourcePaths.scope);
190
+ const loaded = await this.loader.loadScope({
191
+ ...sourcePaths,
192
+ manifestPath: candidate.manifestPath,
193
+ runtimeDir: candidate.runtimeDir,
194
+ });
195
+ if (loaded.failed.length > 0) throw packageLoadFailure(loaded.failed);
196
+ return loaded.loaded;
197
+ }
198
+
199
+ private async validateCandidate(
200
+ scope: PluginScope,
201
+ staged: LoadedPackagePlugin[],
202
+ ): Promise<void> {
203
+ if (scope === "global") {
204
+ composePackageRegistry(this.builtins, staged, []);
205
+ return;
206
+ }
207
+
208
+ const globalPaths = this.paths("global");
209
+ await new PluginTransaction(globalPaths).recover();
210
+ const currentGlobals = await this.loadScopeOrThrow(globalPaths);
211
+ composePackageRegistry(this.builtins, currentGlobals, staged);
212
+ }
213
+
214
+ private async loadScopeOrThrow(paths: PluginPaths): Promise<LoadedPackagePlugin[]> {
215
+ const result = await this.loader.loadScope(paths);
216
+ if (result.failed.length > 0) throw packageLoadFailure(result.failed);
217
+ return result.loaded;
218
+ }
219
+
220
+ private async listScope(paths: PluginPaths): Promise<ListedPlugin[]> {
221
+ await new PluginTransaction(paths).recover();
222
+ const manifest = await readPluginManifest(paths.manifestPath);
223
+ const listed: ListedPlugin[] = [];
224
+ for (const packageName of Object.keys(manifest.plugins).sort()) {
225
+ const metadata = await readInstalledPackageMetadata(paths.runtimeDir, packageName);
226
+ listed.push({
227
+ packageName,
228
+ requested: manifest.plugins[packageName]?.requested ?? "latest",
229
+ version: metadata.version,
230
+ tools: [],
231
+ scope: paths.scope,
232
+ shadowedByProject: false,
233
+ });
234
+ }
235
+ return listed;
236
+ }
237
+ }
238
+
239
+ async function writeEmptyManifestLockfile(
240
+ runtimeDir: string,
241
+ manifest: PluginManifest,
242
+ ): Promise<void> {
243
+ if (Object.keys(manifest.plugins).length > 0) return;
244
+ await writeFile(path.join(runtimeDir, "bun.lock"), "", "utf8");
245
+ }
246
+
247
+ async function writeRuntimeGitignore(runtimeDir: string, scope: PluginScope): Promise<void> {
248
+ if (scope !== "project") return;
249
+ await writeFile(path.join(runtimeDir, ".gitignore"), "*\n!.gitignore\n!bun.lock\n", "utf8");
250
+ }
251
+
252
+ function summaryFor(
253
+ plugin: LoadedPackagePlugin,
254
+ requested: string,
255
+ scope: PluginScope,
256
+ ): InstalledPluginSummary {
257
+ return {
258
+ packageName: plugin.packageName,
259
+ requested,
260
+ version: plugin.version,
261
+ tools: plugin.tools.map((tool) => tool.name).sort(),
262
+ scope,
263
+ };
264
+ }
265
+
266
+ function cloneManifest(manifest: PluginManifest): PluginManifest {
267
+ return JSON.parse(serializePluginManifest(manifest)) as PluginManifest;
268
+ }
269
+
270
+ function packageLoadFailure(failures: PackagePluginFailure[]): CodeNError {
271
+ const message = failures
272
+ .map((failure) => `${failure.packageName}: ${failure.message} (${failure.path})`)
273
+ .join("; ");
274
+ return new CodeNError("plugin", "plugin.sync_failed", `plugin.sync_failed: ${message}`);
275
+ }
276
+
277
+ function invalidPackageName(packageName: string): CodeNError {
278
+ return new CodeNError(
279
+ "plugin",
280
+ "plugin.specifier_invalid",
281
+ `plugin.specifier_invalid: ${packageName}`,
282
+ );
283
+ }
284
+
285
+ function mapPackageManagerError(error: unknown, frozenLockfile: boolean): unknown {
286
+ if (frozenLockfile && isFrozenLockMismatch(error)) {
287
+ return new CodeNError(
288
+ "plugin",
289
+ "plugin.lock_outdated",
290
+ `plugin.lock_outdated: committed plugin lockfile is out of date`,
291
+ false,
292
+ undefined,
293
+ error instanceof Error ? { cause: error } : undefined,
294
+ );
295
+ }
296
+ return error;
297
+ }
298
+
299
+ function isFrozenLockMismatch(error: unknown): boolean {
300
+ if (!(error instanceof Error)) return false;
301
+ const text = `${error.message}\n${error instanceof CodeNError ? error.code : ""}`;
302
+ return /frozen-lockfile|lockfile.*out.?of.?date|lockfile.*would.*change|lockfile had changes/i.test(
303
+ text,
304
+ );
305
+ }
306
+
307
+ async function pathExists(file: string): Promise<boolean> {
308
+ try {
309
+ await access(file, constants.F_OK);
310
+ return true;
311
+ } catch {
312
+ return false;
313
+ }
314
+ }
@@ -0,0 +1,89 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { CodeNError } from "../core/types.js";
3
+ import { isValidNpmPackageName } from "./specifier.js";
4
+
5
+ export interface PluginManifestEntry {
6
+ source: "npm";
7
+ requested: string;
8
+ }
9
+
10
+ export interface PluginManifest {
11
+ schemaVersion: 1;
12
+ plugins: Record<string, PluginManifestEntry>;
13
+ }
14
+
15
+ export const emptyPluginManifest = (): PluginManifest => ({ schemaVersion: 1, plugins: {} });
16
+
17
+ export async function readPluginManifest(file: string): Promise<PluginManifest> {
18
+ try {
19
+ const raw = JSON.parse(await readFile(file, "utf8")) as unknown;
20
+ return normalizePluginManifest(raw);
21
+ } catch (error) {
22
+ if (isMissingFile(error)) return emptyPluginManifest();
23
+ if (error instanceof CodeNError) throw error;
24
+ throw manifestError(error, file);
25
+ }
26
+ }
27
+
28
+ export function serializePluginManifest(manifest: PluginManifest): string {
29
+ return `${JSON.stringify(normalizePluginManifest(manifest), null, 2)}\n`;
30
+ }
31
+
32
+ export function runtimePackageJson(manifest: PluginManifest): {
33
+ private: true;
34
+ dependencies: Record<string, string>;
35
+ } {
36
+ const normalized = normalizePluginManifest(manifest);
37
+ const dependencies: Record<string, string> = {};
38
+ for (const [name, entry] of Object.entries(normalized.plugins))
39
+ dependencies[name] = entry.requested;
40
+ return { private: true, dependencies };
41
+ }
42
+
43
+ function normalizePluginManifest(value: unknown): PluginManifest {
44
+ if (!value || typeof value !== "object") throw manifestError();
45
+ const manifest = value as Partial<PluginManifest> & {
46
+ plugins?: unknown;
47
+ schemaVersion?: unknown;
48
+ };
49
+ if (manifest.schemaVersion !== 1) throw manifestError();
50
+ if (!manifest.plugins || typeof manifest.plugins !== "object" || Array.isArray(manifest.plugins))
51
+ throw manifestError();
52
+
53
+ const plugins: Record<string, PluginManifestEntry> = {};
54
+ for (const name of Object.keys(manifest.plugins).sort()) {
55
+ if (!isValidNpmPackageName(name)) throw manifestError();
56
+ const entry = (manifest.plugins as Record<string, unknown>)[name];
57
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw manifestError();
58
+ const pluginEntry = entry as Partial<PluginManifestEntry>;
59
+ if (
60
+ pluginEntry.source !== "npm" ||
61
+ typeof pluginEntry.requested !== "string" ||
62
+ !pluginEntry.requested
63
+ )
64
+ throw manifestError();
65
+ plugins[name] = { source: "npm", requested: pluginEntry.requested };
66
+ }
67
+
68
+ return { schemaVersion: 1, plugins };
69
+ }
70
+
71
+ function isMissingFile(error: unknown): boolean {
72
+ return (
73
+ typeof error === "object" &&
74
+ error !== null &&
75
+ "code" in error &&
76
+ (error as { code?: string }).code === "ENOENT"
77
+ );
78
+ }
79
+
80
+ function manifestError(cause?: unknown, file?: string): CodeNError {
81
+ return new CodeNError(
82
+ "plugin",
83
+ "plugin.manifest_invalid",
84
+ file ? `plugin.manifest_invalid: ${file}` : "plugin.manifest_invalid",
85
+ false,
86
+ undefined,
87
+ cause instanceof Error ? { cause } : undefined,
88
+ );
89
+ }
@@ -0,0 +1,10 @@
1
+ export interface PackageInstallRequest {
2
+ cwd: string;
3
+ frozenLockfile: boolean;
4
+ allowScripts: boolean;
5
+ signal?: AbortSignal;
6
+ }
7
+
8
+ export interface PackageManager {
9
+ install(request: PackageInstallRequest): Promise<void>;
10
+ }
@@ -0,0 +1,95 @@
1
+ import { readFile, realpath } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { CodeNError } from "../core/types.js";
4
+ import { isValidNpmPackageName } from "./specifier.js";
5
+
6
+ export interface InstalledPackageMetadata {
7
+ packageName: string;
8
+ version: string;
9
+ packageDirectory: string;
10
+ entryPath: string;
11
+ apiVersion: 1;
12
+ }
13
+
14
+ export async function readInstalledPackageMetadata(
15
+ runtimeDirectory: string,
16
+ packageName: string,
17
+ ): Promise<InstalledPackageMetadata> {
18
+ if (!isValidNpmPackageName(packageName)) throw metadataError();
19
+ const packageDirectory = path.join(runtimeDirectory, "node_modules", ...packageName.split("/"));
20
+ const packageJsonPath = path.join(packageDirectory, "package.json");
21
+ let packageJson: unknown;
22
+ try {
23
+ packageJson = JSON.parse(await readFile(packageJsonPath, "utf8")) as unknown;
24
+ } catch {
25
+ throw metadataError();
26
+ }
27
+
28
+ const metadata = normalizePackageJson(packageJson, packageName);
29
+ const packageReal = await safeRealpath(packageDirectory);
30
+ const entryPath = path.resolve(packageDirectory, metadata.entry);
31
+ const entryReal = await safeRealpath(entryPath, true);
32
+ if (!withinBoundary(packageReal, entryReal)) throw entryError();
33
+
34
+ return {
35
+ packageName,
36
+ version: metadata.version,
37
+ packageDirectory: packageReal,
38
+ entryPath: entryReal,
39
+ apiVersion: 1,
40
+ };
41
+ }
42
+
43
+ function normalizePackageJson(
44
+ packageJson: unknown,
45
+ packageName: string,
46
+ ): { version: string; entry: string } {
47
+ if (!packageJson || typeof packageJson !== "object") throw metadataError();
48
+ const manifest = packageJson as Record<string, unknown>;
49
+ if (manifest.name !== packageName || typeof manifest.version !== "string" || !manifest.version)
50
+ throw metadataError();
51
+ if (manifest.type !== "module") throw metadataError();
52
+ if (!manifest.coden || typeof manifest.coden !== "object" || Array.isArray(manifest.coden))
53
+ throw metadataError();
54
+ const coden = manifest.coden as Record<string, unknown>;
55
+ if (coden.apiVersion !== 1) throw unsupportedApiError(coden.apiVersion);
56
+ if (typeof coden.plugin !== "string" || !isValidEntryPath(coden.plugin)) throw entryError();
57
+ return { version: manifest.version, entry: coden.plugin };
58
+ }
59
+
60
+ async function safeRealpath(file: string, allowMissing = false): Promise<string> {
61
+ try {
62
+ return await realpath(file);
63
+ } catch {
64
+ if (allowMissing) throw entryError();
65
+ throw metadataError();
66
+ }
67
+ }
68
+
69
+ function withinBoundary(root: string, candidate: string): boolean {
70
+ return candidate === root || candidate.startsWith(`${root}${path.sep}`);
71
+ }
72
+
73
+ function isValidEntryPath(entry: string): boolean {
74
+ if (!entry.startsWith("./")) return false;
75
+ if (entry.includes("\\") || entry.includes("://") || /\s/.test(entry)) return false;
76
+ if (!(entry.endsWith(".js") || entry.endsWith(".mjs"))) return false;
77
+ if (entry.split("/").includes("..")) return false;
78
+ return true;
79
+ }
80
+
81
+ function metadataError(): CodeNError {
82
+ return new CodeNError("plugin", "plugin.metadata_missing", "plugin.metadata_missing");
83
+ }
84
+
85
+ function unsupportedApiError(apiVersion: unknown): CodeNError {
86
+ return new CodeNError(
87
+ "plugin",
88
+ "plugin.api_unsupported",
89
+ `plugin.api_unsupported: ${String(apiVersion)}`,
90
+ );
91
+ }
92
+
93
+ function entryError(): CodeNError {
94
+ return new CodeNError("plugin", "plugin.entry_invalid", "plugin.entry_invalid");
95
+ }
@@ -0,0 +1,43 @@
1
+ import path from "node:path";
2
+ import { userDataDir } from "../config/config.js";
3
+
4
+ export type PluginScope = "project" | "global";
5
+
6
+ export interface PluginPaths {
7
+ scope: PluginScope;
8
+ root: string;
9
+ manifestPath: string;
10
+ runtimeDir: string;
11
+ lockPath: string;
12
+ transactionPath: string;
13
+ }
14
+
15
+ export function resolvePluginPaths(
16
+ workspace: string,
17
+ scope: PluginScope,
18
+ dataDir = userDataDir(),
19
+ ): PluginPaths {
20
+ if (scope === "project") {
21
+ const root = path.join(workspace, ".coden");
22
+ const runtimeDir = path.join(root, "plugin-runtime");
23
+ return {
24
+ scope,
25
+ root,
26
+ manifestPath: path.join(root, "plugins.json"),
27
+ runtimeDir,
28
+ lockPath: path.join(root, "plugin-lock"),
29
+ transactionPath: path.join(root, "plugin-transaction.json"),
30
+ };
31
+ }
32
+
33
+ const root = path.join(dataDir, "plugins");
34
+ const runtimeDir = path.join(root, "runtime");
35
+ return {
36
+ scope,
37
+ root,
38
+ manifestPath: path.join(root, "plugins.json"),
39
+ runtimeDir,
40
+ lockPath: path.join(root, "plugin-lock"),
41
+ transactionPath: path.join(root, "plugin-transaction.json"),
42
+ };
43
+ }
@@ -0,0 +1,63 @@
1
+ import { CodeNError } from "../core/types.js";
2
+
3
+ export interface NpmPluginSpecifier {
4
+ source: "npm";
5
+ packageName: string;
6
+ requested: string;
7
+ raw: string;
8
+ }
9
+
10
+ const PACKAGE_NAME_PATTERN =
11
+ /^(?:@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*|[a-z0-9][a-z0-9._-]*)$/;
12
+
13
+ export function isValidNpmPackageName(name: string): boolean {
14
+ return PACKAGE_NAME_PATTERN.test(name) && !name.includes("..") && !name.includes("\\");
15
+ }
16
+
17
+ export function parseNpmPluginSpecifier(raw: string): NpmPluginSpecifier {
18
+ if (typeof raw !== "string" || !raw.startsWith("npm:")) throw invalidSpecifier(raw);
19
+ const body = raw.slice(4);
20
+ if (!body || /\s/.test(body) || body.includes("\\") || body.includes("://"))
21
+ throw invalidSpecifier(raw);
22
+
23
+ const { packageName, requested } = splitSpecifierBody(body);
24
+ if (!isValidNpmPackageName(packageName)) throw invalidSpecifier(raw);
25
+ if (
26
+ !requested ||
27
+ /\s/.test(requested) ||
28
+ requested.includes("\\") ||
29
+ requested.includes("/") ||
30
+ requested.includes(":") ||
31
+ requested.includes("..")
32
+ )
33
+ throw invalidSpecifier(raw);
34
+ return { source: "npm", packageName, requested, raw };
35
+ }
36
+
37
+ function splitSpecifierBody(body: string): { packageName: string; requested: string } {
38
+ if (body.startsWith("@")) {
39
+ const slash = body.indexOf("/");
40
+ if (slash < 2) throw invalidSpecifier(body);
41
+ const versionDelimiter = body.lastIndexOf("@");
42
+ if (versionDelimiter <= slash) return { packageName: body, requested: "latest" };
43
+ return {
44
+ packageName: body.slice(0, versionDelimiter),
45
+ requested: body.slice(versionDelimiter + 1),
46
+ };
47
+ }
48
+
49
+ const versionDelimiter = body.lastIndexOf("@");
50
+ if (versionDelimiter === -1) return { packageName: body, requested: "latest" };
51
+ return {
52
+ packageName: body.slice(0, versionDelimiter),
53
+ requested: body.slice(versionDelimiter + 1),
54
+ };
55
+ }
56
+
57
+ function invalidSpecifier(value: string): CodeNError {
58
+ return new CodeNError(
59
+ "plugin",
60
+ "plugin.specifier_invalid",
61
+ `plugin.specifier_invalid: ${String(value)}`,
62
+ );
63
+ }