regpick 0.2.3

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 (48) hide show
  1. package/.github/workflows/release.yml +51 -0
  2. package/.release-it.json +22 -0
  3. package/CHANGELOG.md +12 -0
  4. package/README.md +56 -0
  5. package/bin/regpick.js +3 -0
  6. package/docs/mvp-decisions.md +77 -0
  7. package/examples/README.md +26 -0
  8. package/examples/complex-ui-registry/registry.json +35 -0
  9. package/examples/simple-utils-registry/registry.json +28 -0
  10. package/package.json +39 -0
  11. package/regpick.config.schema.json +40 -0
  12. package/src/commands/add.ts +261 -0
  13. package/src/commands/init.ts +89 -0
  14. package/src/commands/list.ts +54 -0
  15. package/src/commands/pack.ts +97 -0
  16. package/src/commands/update.ts +139 -0
  17. package/src/core/__tests__/result-errors.test.ts +19 -0
  18. package/src/core/errors.ts +36 -0
  19. package/src/core/result.ts +19 -0
  20. package/src/domain/__tests__/addPlan.test.ts +64 -0
  21. package/src/domain/__tests__/initCore.test.ts +28 -0
  22. package/src/domain/__tests__/listCore.test.ts +29 -0
  23. package/src/domain/__tests__/pathPolicy.test.ts +64 -0
  24. package/src/domain/__tests__/registryModel.test.ts +32 -0
  25. package/src/domain/__tests__/selection.test.ts +58 -0
  26. package/src/domain/addPlan.ts +51 -0
  27. package/src/domain/aliasCore.ts +13 -0
  28. package/src/domain/initCore.ts +15 -0
  29. package/src/domain/listCore.ts +34 -0
  30. package/src/domain/packCore.ts +44 -0
  31. package/src/domain/pathPolicy.ts +61 -0
  32. package/src/domain/registryModel.ts +100 -0
  33. package/src/domain/selection.ts +47 -0
  34. package/src/index.ts +117 -0
  35. package/src/shell/cli/args.ts +37 -0
  36. package/src/shell/config.ts +105 -0
  37. package/src/shell/installer.ts +70 -0
  38. package/src/shell/lockfile.ts +35 -0
  39. package/src/shell/packageManagers/__tests__/resolver.test.ts +61 -0
  40. package/src/shell/packageManagers/__tests__/strategy.test.ts +40 -0
  41. package/src/shell/packageManagers/resolver.ts +27 -0
  42. package/src/shell/packageManagers/strategy.ts +65 -0
  43. package/src/shell/registry.ts +182 -0
  44. package/src/shell/runtime/ports.ts +200 -0
  45. package/src/types.ts +92 -0
  46. package/test-clack.ts +2 -0
  47. package/tsconfig.json +15 -0
  48. package/tsdown.config.ts +8 -0
@@ -0,0 +1,70 @@
1
+ import path from "node:path";
2
+
3
+ import type { OverwritePolicy, RegpickConfig, RegistryItem } from "../types.js";
4
+ import { appError, type AppError } from "../core/errors.js";
5
+ import { err, ok, type Result } from "../core/result.js";
6
+ import { resolveOutputPathFromPolicy } from "../domain/pathPolicy.js";
7
+ import {
8
+ getPackageManagerStrategy,
9
+ type RuntimePackageManager,
10
+ } from "../shell/packageManagers/strategy.js";
11
+ import type { RuntimePorts } from "../shell/runtime/ports.js";
12
+ import { resolvePackageManager } from "../shell/packageManagers/resolver.js";
13
+
14
+ function unique(values: string[]): string[] {
15
+ return [...new Set(values.filter(Boolean))];
16
+ }
17
+
18
+ export function collectMissingDependencies(
19
+ items: RegistryItem[],
20
+ cwd: string,
21
+ runtime: RuntimePorts,
22
+ ): { missingDependencies: string[]; missingDevDependencies: string[] } {
23
+ const packageJsonPath = path.join(cwd, "package.json");
24
+ if (!runtime.fs.existsSync(packageJsonPath)) {
25
+ return { missingDependencies: [], missingDevDependencies: [] };
26
+ }
27
+
28
+ const packageJsonResult = runtime.fs.readJsonSync<{
29
+ dependencies?: Record<string, string>;
30
+ devDependencies?: Record<string, string>;
31
+ peerDependencies?: Record<string, string>;
32
+ }>(packageJsonPath);
33
+ const packageJson = packageJsonResult.ok ? packageJsonResult.value : {};
34
+ const declared = {
35
+ ...(packageJson.dependencies || {}),
36
+ ...(packageJson.devDependencies || {}),
37
+ ...(packageJson.peerDependencies || {}),
38
+ };
39
+
40
+ const allDeps = unique(items.flatMap((item) => item.dependencies || []));
41
+ const allDevDeps = unique(items.flatMap((item) => item.devDependencies || []));
42
+
43
+ const missingDependencies = allDeps.filter((dep) => !declared[dep]);
44
+ const missingDevDependencies = allDevDeps.filter((dep) => !declared[dep]);
45
+
46
+ return { missingDependencies, missingDevDependencies };
47
+ }
48
+
49
+ export function installDependencies(
50
+ cwd: string,
51
+ packageManager: RuntimePackageManager,
52
+ dependencies: string[],
53
+ devDependencies: string[],
54
+ runtime: RuntimePorts,
55
+ ): Result<void, AppError> {
56
+ if (!dependencies.length && !devDependencies.length) {
57
+ return ok(undefined);
58
+ }
59
+
60
+ const strategy = getPackageManagerStrategy(packageManager);
61
+ const commands = strategy.buildInstallCommands(dependencies, devDependencies);
62
+ for (const command of commands) {
63
+ const result = runtime.process.run(command.command, command.args, cwd);
64
+ if (result.status !== 0) {
65
+ return err(appError("InstallError", `Dependency install failed: ${command.command} ${command.args.join(" ")}`));
66
+ }
67
+ }
68
+ return ok(undefined);
69
+ }
70
+
@@ -0,0 +1,35 @@
1
+ import path from "node:path";
2
+ import crypto from "node:crypto";
3
+ import type { RuntimePorts } from "./runtime/ports.js";
4
+ import type { RegpickLockfile } from "../types.js";
5
+
6
+ const LOCKFILE_NAME = "regpick-lock.json";
7
+
8
+ export function getLockfilePath(cwd: string): string {
9
+ return path.join(cwd, LOCKFILE_NAME);
10
+ }
11
+
12
+ export async function readLockfile(cwd: string, runtime: RuntimePorts): Promise<RegpickLockfile> {
13
+ const lockfilePath = getLockfilePath(cwd);
14
+ const exists = await runtime.fs.pathExists(lockfilePath);
15
+
16
+ if (!exists) {
17
+ return { components: {} };
18
+ }
19
+
20
+ const readRes = runtime.fs.readJsonSync<RegpickLockfile>(lockfilePath);
21
+ if (!readRes.ok) {
22
+ return { components: {} };
23
+ }
24
+
25
+ return readRes.value;
26
+ }
27
+
28
+ export async function writeLockfile(cwd: string, lockfile: RegpickLockfile, runtime: RuntimePorts): Promise<void> {
29
+ const lockfilePath = getLockfilePath(cwd);
30
+ await runtime.fs.writeJson(lockfilePath, lockfile, { spaces: 2 });
31
+ }
32
+
33
+ export function computeHash(content: string): string {
34
+ return crypto.createHash("sha256").update(content).digest("hex");
35
+ }
@@ -0,0 +1,61 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import type { RuntimePorts } from "../../runtime/ports.js";
4
+ import { resolvePackageManager } from "../resolver.js";
5
+ import { ok, err } from "../../../core/result.js";
6
+ import { appError } from "../../../core/errors.js";
7
+
8
+ function runtimeWithLockfiles(lockfiles: string[]): RuntimePorts {
9
+ return {
10
+ fs: {
11
+ existsSync: (filePath: string) => lockfiles.some((lock) => filePath.endsWith(lock)),
12
+ pathExists: async () => false,
13
+ ensureDir: async () => ok(undefined),
14
+ writeFile: async () => ok(undefined),
15
+ readFile: async () => ok(""),
16
+ readJsonSync: <T>() => ok({} as T),
17
+ writeJson: async () => ok(undefined),
18
+ stat: async () => err(appError("RuntimeError", "not implemented")),
19
+ readdir: async () => ok([]),
20
+ },
21
+ http: {
22
+ getJson: async <T>() => ok({} as T),
23
+ getText: async () => ok(""),
24
+ },
25
+ prompt: {
26
+ intro: () => undefined,
27
+ outro: () => undefined,
28
+ cancel: () => undefined,
29
+ isCancel: () => false,
30
+ info: () => undefined,
31
+ warn: () => undefined,
32
+ error: () => undefined,
33
+ success: () => undefined,
34
+ text: async () => "",
35
+ confirm: async () => true,
36
+ select: async () => "overwrite",
37
+ multiselect: async () => [],
38
+ autocompleteMultiselect: async () => [],
39
+ },
40
+ process: {
41
+ run: () => ({ status: 0 }),
42
+ },
43
+ };
44
+ }
45
+
46
+ describe("package manager resolver", () => {
47
+ it("prefers configured manager over lockfiles", () => {
48
+ const runtime = runtimeWithLockfiles(["pnpm-lock.yaml"]);
49
+ expect(resolvePackageManager("/tmp/project", "yarn", runtime)).toBe("yarn");
50
+ });
51
+
52
+ it("resolves lockfile-based manager", () => {
53
+ expect(resolvePackageManager("/tmp/project", "auto", runtimeWithLockfiles(["pnpm-lock.yaml"]))).toBe("pnpm");
54
+ expect(resolvePackageManager("/tmp/project", "auto", runtimeWithLockfiles(["yarn.lock"]))).toBe("yarn");
55
+ expect(resolvePackageManager("/tmp/project", "auto", runtimeWithLockfiles(["package-lock.json"]))).toBe("npm");
56
+ });
57
+
58
+ it("falls back to npm when no lockfile exists", () => {
59
+ expect(resolvePackageManager("/tmp/project", "auto", runtimeWithLockfiles([]))).toBe("npm");
60
+ });
61
+ });
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { getPackageManagerStrategy } from "../strategy.js";
4
+
5
+ describe("package manager strategy", () => {
6
+ it("builds npm commands for deps and devDeps", () => {
7
+ const strategy = getPackageManagerStrategy("npm");
8
+ const commands = strategy.buildInstallCommands(["react"], ["@types/react"]);
9
+ expect(commands).toEqual([
10
+ { command: "npm", args: ["install", "react"] },
11
+ { command: "npm", args: ["install", "-D", "@types/react"] },
12
+ ]);
13
+ });
14
+
15
+ it("builds yarn commands", () => {
16
+ const strategy = getPackageManagerStrategy("yarn");
17
+ const commands = strategy.buildInstallCommands(["react"], ["@types/react"]);
18
+ expect(commands).toEqual([
19
+ { command: "yarn", args: ["add", "react"] },
20
+ { command: "yarn", args: ["add", "-D", "@types/react"] },
21
+ ]);
22
+ });
23
+
24
+ it("builds pnpm commands", () => {
25
+ const strategy = getPackageManagerStrategy("pnpm");
26
+ const commands = strategy.buildInstallCommands(["react"], ["@types/react"]);
27
+ expect(commands).toEqual([
28
+ { command: "pnpm", args: ["add", "react"] },
29
+ { command: "pnpm", args: ["add", "-D", "@types/react"] },
30
+ ]);
31
+ });
32
+
33
+ it("omits empty command groups", () => {
34
+ const strategy = getPackageManagerStrategy("npm");
35
+ expect(strategy.buildInstallCommands([], [])).toEqual([]);
36
+ expect(strategy.buildInstallCommands(["react"], [])).toEqual([
37
+ { command: "npm", args: ["install", "react"] },
38
+ ]);
39
+ });
40
+ });
@@ -0,0 +1,27 @@
1
+ import path from "node:path";
2
+
3
+ import type { PackageManager } from "../../types.js";
4
+ import type { RuntimePorts } from "../runtime/ports.js";
5
+ import type { RuntimePackageManager } from "./strategy.js";
6
+
7
+ export function resolvePackageManager(
8
+ cwd: string,
9
+ configured: PackageManager,
10
+ runtime: RuntimePorts,
11
+ ): RuntimePackageManager {
12
+ if (configured && configured !== "auto") {
13
+ return configured;
14
+ }
15
+
16
+ if (runtime.fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) {
17
+ return "pnpm";
18
+ }
19
+ if (runtime.fs.existsSync(path.join(cwd, "yarn.lock"))) {
20
+ return "yarn";
21
+ }
22
+ if (runtime.fs.existsSync(path.join(cwd, "package-lock.json"))) {
23
+ return "npm";
24
+ }
25
+
26
+ return "npm";
27
+ }
@@ -0,0 +1,65 @@
1
+ import type { PackageManager } from "../../types.js";
2
+
3
+ export type RuntimePackageManager = Exclude<PackageManager, "auto">;
4
+
5
+ type InstallCommand = {
6
+ command: string;
7
+ args: string[];
8
+ };
9
+
10
+ type PackageManagerStrategy = {
11
+ manager: RuntimePackageManager;
12
+ buildInstallCommands: (dependencies: string[], devDependencies: string[]) => InstallCommand[];
13
+ };
14
+
15
+ function buildNpmCommands(dependencies: string[], devDependencies: string[]): InstallCommand[] {
16
+ const commands: InstallCommand[] = [];
17
+ if (dependencies.length) {
18
+ commands.push({ command: "npm", args: ["install", ...dependencies] });
19
+ }
20
+ if (devDependencies.length) {
21
+ commands.push({ command: "npm", args: ["install", "-D", ...devDependencies] });
22
+ }
23
+ return commands;
24
+ }
25
+
26
+ function buildYarnCommands(dependencies: string[], devDependencies: string[]): InstallCommand[] {
27
+ const commands: InstallCommand[] = [];
28
+ if (dependencies.length) {
29
+ commands.push({ command: "yarn", args: ["add", ...dependencies] });
30
+ }
31
+ if (devDependencies.length) {
32
+ commands.push({ command: "yarn", args: ["add", "-D", ...devDependencies] });
33
+ }
34
+ return commands;
35
+ }
36
+
37
+ function buildPnpmCommands(dependencies: string[], devDependencies: string[]): InstallCommand[] {
38
+ const commands: InstallCommand[] = [];
39
+ if (dependencies.length) {
40
+ commands.push({ command: "pnpm", args: ["add", ...dependencies] });
41
+ }
42
+ if (devDependencies.length) {
43
+ commands.push({ command: "pnpm", args: ["add", "-D", ...devDependencies] });
44
+ }
45
+ return commands;
46
+ }
47
+
48
+ const strategyMap: Record<RuntimePackageManager, PackageManagerStrategy> = {
49
+ npm: {
50
+ manager: "npm",
51
+ buildInstallCommands: buildNpmCommands,
52
+ },
53
+ yarn: {
54
+ manager: "yarn",
55
+ buildInstallCommands: buildYarnCommands,
56
+ },
57
+ pnpm: {
58
+ manager: "pnpm",
59
+ buildInstallCommands: buildPnpmCommands,
60
+ },
61
+ };
62
+
63
+ export function getPackageManagerStrategy(manager: RuntimePackageManager): PackageManagerStrategy {
64
+ return strategyMap[manager];
65
+ }
@@ -0,0 +1,182 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ import type { RegistryFile, RegistryItem, RegistrySourceMeta } from "../types.js";
5
+ import { appError, type AppError } from "../core/errors.js";
6
+ import { err, ok, type Result } from "../core/result.js";
7
+ import { extractItemReferences, normalizeItem, normalizeManifestInline } from "../domain/registryModel.js";
8
+ import type { RuntimePorts } from "../shell/runtime/ports.js";
9
+
10
+ function isHttpUrl(value: string): boolean {
11
+ return /^https?:\/\//i.test(value);
12
+ }
13
+
14
+ function isFileUrl(value: string): boolean {
15
+ return /^file:\/\//i.test(value);
16
+ }
17
+
18
+ function joinUrl(baseUrl: string, relativePath: string): string {
19
+ return new URL(relativePath, baseUrl).toString();
20
+ }
21
+
22
+ async function normalizeManifest(
23
+ data: unknown,
24
+ sourceMeta: RegistrySourceMeta,
25
+ runtime: RuntimePorts,
26
+ ): Promise<Result<RegistryItem[], AppError>> {
27
+ const inlineItemsRes = normalizeManifestInline(data, sourceMeta);
28
+
29
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
30
+ return inlineItemsRes;
31
+ }
32
+
33
+ const references = extractItemReferences(data as Record<string, unknown>);
34
+ if (!references.length) {
35
+ return inlineItemsRes;
36
+ }
37
+
38
+ const inlineItems = inlineItemsRes.ok ? inlineItemsRes.value : [];
39
+
40
+ const resolvedItems: RegistryItem[] = [];
41
+ for (const itemRef of references) {
42
+ let itemData: unknown;
43
+ if (isHttpUrl(itemRef)) {
44
+ const res = await runtime.http.getJson(itemRef);
45
+ if (!res.ok) return err(res.error);
46
+ itemData = res.value;
47
+ } else if (sourceMeta.type === "http" && sourceMeta.baseUrl) {
48
+ const res = await runtime.http.getJson(joinUrl(sourceMeta.baseUrl, itemRef));
49
+ if (!res.ok) return err(res.error);
50
+ itemData = res.value;
51
+ } else if ((sourceMeta.type === "file" || sourceMeta.type === "directory") && sourceMeta.baseDir) {
52
+ const res = await runtime.fs.readFile(path.resolve(sourceMeta.baseDir, itemRef), "utf8");
53
+ if (!res.ok) return err(res.error);
54
+ try { itemData = JSON.parse(res.value); } catch { return err(appError("RegistryError", `Invalid JSON: ${itemRef}`)); }
55
+ } else {
56
+ const res = await runtime.fs.readFile(path.resolve(itemRef), "utf8");
57
+ if (!res.ok) return err(res.error);
58
+ try { itemData = JSON.parse(res.value); } catch { return err(appError("RegistryError", `Invalid JSON: ${itemRef}`)); }
59
+ }
60
+
61
+ if (itemData && typeof itemData === "object") {
62
+ resolvedItems.push(normalizeItem(itemData as Record<string, unknown>, sourceMeta));
63
+ }
64
+ }
65
+
66
+ return ok([...inlineItems, ...resolvedItems]);
67
+ }
68
+
69
+ async function loadDirectoryRegistry(directoryPath: string, runtime: RuntimePorts): Promise<Result<RegistryItem[], AppError>> {
70
+ const absoluteDir = path.resolve(directoryPath);
71
+ const dirRes = await runtime.fs.readdir(absoluteDir);
72
+ if (!dirRes.ok) return err(dirRes.error);
73
+
74
+ const files = dirRes.value;
75
+ const jsonFiles = files.filter((file) => file.endsWith(".json"));
76
+
77
+ const items: RegistryItem[] = [];
78
+ for (const fileName of jsonFiles) {
79
+ const fullPath = path.join(absoluteDir, fileName);
80
+ const readRes = await runtime.fs.readFile(fullPath, "utf8");
81
+ if (!readRes.ok) return err(readRes.error);
82
+
83
+ let parsed: unknown;
84
+ try { parsed = JSON.parse(readRes.value); } catch { continue; }
85
+
86
+ if (!parsed || typeof parsed !== "object" || !Array.isArray((parsed as Record<string, unknown>).files)) {
87
+ continue;
88
+ }
89
+ items.push(normalizeItem(parsed as Record<string, unknown>, { type: "directory", baseDir: absoluteDir }));
90
+ }
91
+
92
+ return ok(items);
93
+ }
94
+
95
+ export async function loadRegistry(
96
+ source: string,
97
+ cwd: string,
98
+ runtime: RuntimePorts,
99
+ ): Promise<Result<{ items: RegistryItem[]; source: string }, AppError>> {
100
+ if (!source) {
101
+ return err(appError("ValidationError", "Registry source is required."));
102
+ }
103
+
104
+ const resolved = isHttpUrl(source) || isFileUrl(source) ? source : path.resolve(cwd, source);
105
+
106
+ if (isHttpUrl(resolved)) {
107
+ const dataRes = await runtime.http.getJson(resolved);
108
+ if (!dataRes.ok) return err(dataRes.error);
109
+ const baseUrl = resolved.endsWith("/") ? resolved : resolved.replace(/[^/]*$/, "");
110
+ const itemsRes = await normalizeManifest(dataRes.value, { type: "http", baseUrl }, runtime);
111
+ if (!itemsRes.ok) return err(itemsRes.error);
112
+ return ok({ items: itemsRes.value, source: resolved });
113
+ }
114
+
115
+ const fileSystemPath = isFileUrl(resolved) ? fileURLToPath(new URL(resolved)) : path.resolve(resolved);
116
+ const statsRes = await runtime.fs.stat(fileSystemPath);
117
+ if (!statsRes.ok) {
118
+ return err(appError("RegistryError", `Registry source not found: ${source}`));
119
+ }
120
+ const stats = statsRes.value;
121
+
122
+ if (stats.isDirectory()) {
123
+ const itemsRes = await loadDirectoryRegistry(fileSystemPath, runtime);
124
+ if (!itemsRes.ok) return err(itemsRes.error);
125
+ return ok({ items: itemsRes.value, source: fileSystemPath });
126
+ }
127
+
128
+ const readRes = await runtime.fs.readFile(fileSystemPath, "utf8");
129
+ if (!readRes.ok) return err(readRes.error);
130
+
131
+ let parsed: unknown;
132
+ try {
133
+ parsed = JSON.parse(readRes.value);
134
+ } catch (cause) {
135
+ return err(appError("RegistryError", "Failed to parse registry JSON.", cause));
136
+ }
137
+
138
+ const itemsRes = await normalizeManifest(
139
+ parsed,
140
+ {
141
+ type: "file",
142
+ baseDir: path.dirname(fileSystemPath),
143
+ },
144
+ runtime,
145
+ );
146
+
147
+ if (!itemsRes.ok) return err(itemsRes.error);
148
+ return ok({ items: itemsRes.value, source: fileSystemPath });
149
+ }
150
+
151
+ export async function resolveFileContent(
152
+ file: RegistryFile,
153
+ item: RegistryItem,
154
+ cwd: string,
155
+ runtime: RuntimePorts,
156
+ ): Promise<Result<string, AppError>> {
157
+ if (typeof file.content === "string") {
158
+ return ok(file.content);
159
+ }
160
+
161
+ const targetPathOrUrl = file.url || file.path;
162
+
163
+ if (!targetPathOrUrl) {
164
+ return err(appError("ValidationError", `File entry in "${item.name}" is missing both content and path/url.`));
165
+ }
166
+
167
+ if (isHttpUrl(targetPathOrUrl)) {
168
+ return await runtime.http.getText(targetPathOrUrl);
169
+ }
170
+
171
+ if (item.sourceMeta.type === "http" && item.sourceMeta.baseUrl) {
172
+ const remoteUrl = joinUrl(item.sourceMeta.baseUrl, targetPathOrUrl);
173
+ return await runtime.http.getText(remoteUrl);
174
+ }
175
+
176
+ const localPath =
177
+ item.sourceMeta.baseDir && !path.isAbsolute(targetPathOrUrl)
178
+ ? path.resolve(item.sourceMeta.baseDir, targetPathOrUrl)
179
+ : path.resolve(cwd, targetPathOrUrl);
180
+
181
+ return await runtime.fs.readFile(localPath, "utf8");
182
+ }
@@ -0,0 +1,200 @@
1
+ import fs from "node:fs";
2
+ import fsPromises from "node:fs/promises";
3
+ import {
4
+ cancel,
5
+ confirm,
6
+ intro,
7
+ isCancel,
8
+ log,
9
+ multiselect,
10
+ autocompleteMultiselect,
11
+ outro,
12
+ select,
13
+ text,
14
+ } from "@clack/prompts";
15
+ import { spawnSync } from "node:child_process";
16
+ import { type Result, ok, err } from "../../core/result.js";
17
+ import { appError, type AppError } from "../../core/errors.js";
18
+
19
+ export type FileSystemPort = {
20
+ existsSync(path: string): boolean;
21
+ pathExists(path: string): Promise<boolean>;
22
+ ensureDir(path: string): Promise<Result<void, AppError>>;
23
+ writeFile(path: string, content: string, encoding: BufferEncoding): Promise<Result<void, AppError>>;
24
+ readFile(path: string, encoding: BufferEncoding): Promise<Result<string, AppError>>;
25
+ readJsonSync<T = unknown>(path: string): Result<T, AppError>;
26
+ writeJson(path: string, value: unknown, options?: { spaces?: number }): Promise<Result<void, AppError>>;
27
+ stat(path: string): Promise<Result<import("fs").Stats, AppError>>;
28
+ readdir(path: string): Promise<Result<string[], AppError>>;
29
+ };
30
+
31
+ export type HttpPort = {
32
+ getJson<T = unknown>(url: string, timeoutMs?: number): Promise<Result<T, AppError>>;
33
+ getText(url: string, timeoutMs?: number): Promise<Result<string, AppError>>;
34
+ };
35
+
36
+ export type PromptPort = {
37
+ intro(message: string): void;
38
+ outro(message: string): void;
39
+ cancel(message: string): void;
40
+ isCancel(value: unknown): boolean;
41
+ info(message: string): void;
42
+ warn(message: string): void;
43
+ error(message: string): void;
44
+ success(message: string): void;
45
+ text(options: { message: string; placeholder?: string; defaultValue?: string }): Promise<string | symbol>;
46
+ confirm(options: { message: string; initialValue?: boolean }): Promise<boolean | symbol>;
47
+ select(options: {
48
+ message: string;
49
+ options: Array<{ value: string; label: string; hint?: string }>;
50
+ }): Promise<string | symbol>;
51
+ multiselect(options: {
52
+ message: string;
53
+ options: Array<{ value: string; label: string; hint?: string }>;
54
+ maxItems?: number;
55
+ required?: boolean;
56
+ }): Promise<Array<string> | symbol>;
57
+ autocompleteMultiselect(options: {
58
+ message: string;
59
+ options: Array<{ value: string; label: string; hint?: string }>;
60
+ maxItems?: number;
61
+ required?: boolean;
62
+ }): Promise<Array<string> | symbol>;
63
+ };
64
+
65
+ export type ProcessPort = {
66
+ run(command: string, args: string[], cwd: string): { status: number | null };
67
+ };
68
+
69
+ export type RuntimePorts = {
70
+ fs: FileSystemPort;
71
+ http: HttpPort;
72
+ prompt: PromptPort;
73
+ process: ProcessPort;
74
+ };
75
+
76
+ export const createRuntimePorts = (options?: { signal?: AbortSignal }): RuntimePorts => ({
77
+ fs: {
78
+ existsSync: (path) => fs.existsSync(path),
79
+ pathExists: async (path) => {
80
+ try {
81
+ await fsPromises.access(path);
82
+ return true;
83
+ } catch {
84
+ return false;
85
+ }
86
+ },
87
+ ensureDir: async (path) => {
88
+ try {
89
+ await fsPromises.mkdir(path, { recursive: true });
90
+ return ok(undefined);
91
+ } catch (cause) {
92
+ return err(appError("RuntimeError", `Failed to ensure directory: ${path}`, cause));
93
+ }
94
+ },
95
+ writeFile: async (path, content, encoding) => {
96
+ try {
97
+ await fsPromises.writeFile(path, content, encoding);
98
+ return ok(undefined);
99
+ } catch (cause) {
100
+ return err(appError("RuntimeError", `Failed to write file: ${path}`, cause));
101
+ }
102
+ },
103
+ readFile: async (path, encoding) => {
104
+ try {
105
+ const content = await fsPromises.readFile(path, encoding);
106
+ return ok(content);
107
+ } catch (cause) {
108
+ return err(appError("RuntimeError", `Failed to read file: ${path}`, cause));
109
+ }
110
+ },
111
+ readJsonSync: <T = unknown>(path: string) => {
112
+ try {
113
+ const content = fs.readFileSync(path, "utf8");
114
+ return ok(JSON.parse(content) as T);
115
+ } catch (cause) {
116
+ return err(appError("RuntimeError", `Failed to read JSON: ${path}`, cause));
117
+ }
118
+ },
119
+ writeJson: async (path, value, opts) => {
120
+ try {
121
+ const content = JSON.stringify(value, null, opts?.spaces ?? 2);
122
+ await fsPromises.writeFile(path, content, "utf8");
123
+ return ok(undefined);
124
+ } catch (cause) {
125
+ return err(appError("RuntimeError", `Failed to write JSON: ${path}`, cause));
126
+ }
127
+ },
128
+ stat: async (path) => {
129
+ try {
130
+ const stats = await fsPromises.stat(path);
131
+ return ok(stats);
132
+ } catch (cause) {
133
+ return err(appError("RuntimeError", `Failed to stat path: ${path}`, cause));
134
+ }
135
+ },
136
+ readdir: async (path) => {
137
+ try {
138
+ const files = await fsPromises.readdir(path);
139
+ return ok(files);
140
+ } catch (cause) {
141
+ return err(appError("RuntimeError", `Failed to read directory: ${path}`, cause));
142
+ }
143
+ },
144
+ },
145
+ http: {
146
+ getJson: async <T>(url: string, timeoutMs = 15000): Promise<Result<T, AppError>> => {
147
+ try {
148
+ const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
149
+
150
+ if (!response.ok) {
151
+ return err(appError("RuntimeError", `HTTP error! status: ${response.status} when fetching JSON from: ${url}`));
152
+ }
153
+
154
+ const data = await response.json();
155
+ return ok(data as T);
156
+ } catch (cause) {
157
+ return err(appError("RuntimeError", `Failed to fetch JSON from: ${url}`, cause));
158
+ }
159
+ },
160
+ getText: async (url: string, timeoutMs = 15000): Promise<Result<string, AppError>> => {
161
+ try {
162
+ const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
163
+
164
+ if (!response.ok) {
165
+ return err(appError("RuntimeError", `HTTP error! status: ${response.status} when fetching text from: ${url}`));
166
+ }
167
+
168
+ const data = await response.text();
169
+ return ok(data);
170
+ } catch (cause) {
171
+ return err(appError("RuntimeError", `Failed to fetch text from: ${url}`, cause));
172
+ }
173
+ },
174
+ },
175
+ prompt: {
176
+ intro: (message) => intro(message),
177
+ outro: (message) => outro(message),
178
+ cancel: (message) => cancel(message),
179
+ isCancel: (value) => isCancel(value),
180
+ info: (message) => log.info(message),
181
+ warn: (message) => log.warn(message),
182
+ error: (message) => log.error(message),
183
+ success: (message) => log.success(message),
184
+ text: (opts) => text({ signal: options?.signal, ...opts }),
185
+ confirm: (opts) => confirm({ signal: options?.signal, ...opts }),
186
+ select: (opts) => select({ signal: options?.signal, ...opts } as any),
187
+ multiselect: (opts) => multiselect({ signal: options?.signal, ...opts } as any),
188
+ autocompleteMultiselect: (opts) => autocompleteMultiselect({ signal: options?.signal, ...opts } as any),
189
+ },
190
+ process: {
191
+ run: (command, args, cwd) =>
192
+ spawnSync(command, args, {
193
+ cwd,
194
+ stdio: "inherit",
195
+ shell: process.platform === "win32",
196
+ }),
197
+ },
198
+ });
199
+
200
+ export const defaultRuntimePorts: RuntimePorts = createRuntimePorts();