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,58 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import type { CommandContext, RegistryItem } from "../../types.js";
4
+ import type { RuntimePorts } from "../../shell/runtime/ports.js";
5
+ import { filterItemsByQuery, parseSelectedNames, selectItemsFromFlags } from "../selection.js";
6
+
7
+ const items: RegistryItem[] = [
8
+ {
9
+ name: "check",
10
+ title: "Check",
11
+ description: "Check icon",
12
+ type: "registry:icon",
13
+ dependencies: [],
14
+ devDependencies: [],
15
+ registryDependencies: [],
16
+ files: [{ type: "registry:file", path: "icons/check.tsx" }],
17
+ sourceMeta: { type: "directory", baseDir: "/registry" },
18
+ },
19
+ {
20
+ name: "calendar",
21
+ title: "Calendar",
22
+ description: "Calendar icon",
23
+ type: "registry:icon",
24
+ dependencies: [],
25
+ devDependencies: [],
26
+ registryDependencies: [],
27
+ files: [{ type: "registry:file", path: "icons/calendar.tsx" }],
28
+ sourceMeta: { type: "directory", baseDir: "/registry" },
29
+ },
30
+ ];
31
+
32
+ function context(flags: CommandContext["args"]["flags"]): CommandContext {
33
+ return {
34
+ cwd: "/tmp/project",
35
+ args: { flags, positionals: ["add"] },
36
+ runtime: {} as RuntimePorts,
37
+ };
38
+ }
39
+
40
+ describe("selection core", () => {
41
+ it("parses --select values", () => {
42
+ expect(parseSelectedNames("check,calendar")).toEqual(["check", "calendar"]);
43
+ });
44
+
45
+ it("filters by query against name/title/description", () => {
46
+ expect(filterItemsByQuery(items, "cal")).toHaveLength(1);
47
+ expect(filterItemsByQuery(items, "icon")).toHaveLength(2);
48
+ });
49
+
50
+ it("selects all when --all is set", () => {
51
+ expect(selectItemsFromFlags(items, context({ all: true }))).toEqual({ ok: true, value: items });
52
+ });
53
+
54
+ it("selects explicit items when --select is set", () => {
55
+ const result = selectItemsFromFlags(items, context({ select: "check" }));
56
+ expect(result.ok && result.value?.map((item) => item.name)).toEqual(["check"]);
57
+ });
58
+ });
@@ -0,0 +1,51 @@
1
+ import type { InstallPlan, PlannedWrite, RegpickConfig, RegistryItem } from "../types.js";
2
+ import { resolveOutputPathFromPolicy } from "./pathPolicy.js";
3
+ import { err, ok, type Result } from "../core/result.js";
4
+ import type { AppError } from "../core/errors.js";
5
+
6
+ function unique(values: string[]): string[] {
7
+ return [...new Set(values.filter(Boolean))];
8
+ }
9
+
10
+ function buildDependencyPlan(selectedItems: RegistryItem[]): InstallPlan["dependencyPlan"] {
11
+ return {
12
+ dependencies: unique(selectedItems.flatMap((item) => item.dependencies || [])),
13
+ devDependencies: unique(selectedItems.flatMap((item) => item.devDependencies || [])),
14
+ };
15
+ }
16
+
17
+ export function buildInstallPlan(
18
+ selectedItems: RegistryItem[],
19
+ cwd: string,
20
+ config: RegpickConfig,
21
+ existingTargets: Set<string> = new Set(),
22
+ ): Result<InstallPlan, AppError> {
23
+ const plannedWrites: PlannedWrite[] = [];
24
+ const conflicts: PlannedWrite[] = [];
25
+
26
+ for (const item of selectedItems) {
27
+ for (const file of item.files) {
28
+ const outputRes = resolveOutputPathFromPolicy(item, file, cwd, config);
29
+ if (!outputRes.ok) return outputRes;
30
+
31
+ const { absoluteTarget, relativeTarget } = outputRes.value;
32
+ const planned: PlannedWrite = {
33
+ itemName: item.name,
34
+ sourceFile: file,
35
+ absoluteTarget,
36
+ relativeTarget,
37
+ };
38
+ plannedWrites.push(planned);
39
+ if (existingTargets.has(absoluteTarget)) {
40
+ conflicts.push(planned);
41
+ }
42
+ }
43
+ }
44
+
45
+ return ok({
46
+ selectedItems,
47
+ plannedWrites,
48
+ dependencyPlan: buildDependencyPlan(selectedItems),
49
+ conflicts,
50
+ });
51
+ }
@@ -0,0 +1,13 @@
1
+ import type { RegpickConfig } from "../types.js";
2
+
3
+ export function applyAliases(content: string, config: RegpickConfig): string {
4
+ let result = content;
5
+ for (const [oldAlias, newAlias] of Object.entries(config.aliases || {})) {
6
+ const regex = new RegExp(`from ["']${oldAlias}(.*?)["']`, "g");
7
+ result = result.replace(regex, `from "${newAlias}$1"`);
8
+ // Also handle dynamic imports
9
+ const dynRegex = new RegExp(`import\\(["']${oldAlias}(.*?)["']\\)`, "g");
10
+ result = result.replace(dynRegex, `import("${newAlias}$1")`);
11
+ }
12
+ return result;
13
+ }
@@ -0,0 +1,15 @@
1
+ type InitDecision = "created" | "ask-overwrite" | "overwrite" | "keep" | "cancelled";
2
+
3
+ export function decideInitAfterFirstWrite(firstWriteSucceeded: boolean): InitDecision {
4
+ return firstWriteSucceeded ? "created" : "ask-overwrite";
5
+ }
6
+
7
+ export function decideInitAfterOverwritePrompt(
8
+ isPromptCancelled: boolean,
9
+ shouldOverwrite: boolean,
10
+ ): InitDecision {
11
+ if (isPromptCancelled) {
12
+ return "cancelled";
13
+ }
14
+ return shouldOverwrite ? "overwrite" : "keep";
15
+ }
@@ -0,0 +1,34 @@
1
+ import type { RegistryItem } from "../types.js";
2
+
3
+ type Registries = Record<string, string>;
4
+
5
+ export function resolveRegistrySourceFromAliases(
6
+ input: string | undefined,
7
+ registries: Registries,
8
+ ): string | null {
9
+ if (!input) {
10
+ return null;
11
+ }
12
+
13
+ return registries[input] ? String(registries[input]) : input;
14
+ }
15
+
16
+ export function resolveListSourceDecision(
17
+ providedInput: string | undefined,
18
+ registries: Registries,
19
+ ): { source: string | null; requiresPrompt: boolean } {
20
+ const fromInput = resolveRegistrySourceFromAliases(providedInput, registries);
21
+ if (fromInput) {
22
+ return { source: fromInput, requiresPrompt: false };
23
+ }
24
+
25
+ const defaultAlias = Object.keys(registries)[0];
26
+ if (defaultAlias) {
27
+ return {
28
+ source: resolveRegistrySourceFromAliases(defaultAlias, registries),
29
+ requiresPrompt: false,
30
+ };
31
+ }
32
+
33
+ return { source: null, requiresPrompt: true };
34
+ }
@@ -0,0 +1,44 @@
1
+ export function extractDependencies(content: string): string[] {
2
+ const importRegex = /import\s+[\s\S]*?from\s+["']([^"']+)["']/g;
3
+ const dynamicImportRegex = /import\(["']([^"']+)["']\)/g;
4
+
5
+ const deps = new Set<string>();
6
+
7
+ let match;
8
+ while ((match = importRegex.exec(content)) !== null) {
9
+ const specifier = match[1];
10
+ if (
11
+ !specifier.startsWith(".") &&
12
+ !specifier.startsWith("/") &&
13
+ !specifier.startsWith("~") &&
14
+ !specifier.startsWith("@/") &&
15
+ !specifier.startsWith("@\\")
16
+ ) {
17
+ const parts = specifier.split("/");
18
+ if (specifier.startsWith("@") && parts.length > 1) {
19
+ deps.add(`${parts[0]}/${parts[1]}`);
20
+ } else {
21
+ deps.add(parts[0]);
22
+ }
23
+ }
24
+ }
25
+ while ((match = dynamicImportRegex.exec(content)) !== null) {
26
+ const specifier = match[1];
27
+ if (
28
+ !specifier.startsWith(".") &&
29
+ !specifier.startsWith("/") &&
30
+ !specifier.startsWith("~") &&
31
+ !specifier.startsWith("@/") &&
32
+ !specifier.startsWith("@\\")
33
+ ) {
34
+ const parts = specifier.split("/");
35
+ if (specifier.startsWith("@") && parts.length > 1) {
36
+ deps.add(`${parts[0]}/${parts[1]}`);
37
+ } else {
38
+ deps.add(parts[0]);
39
+ }
40
+ }
41
+ }
42
+
43
+ return Array.from(deps);
44
+ }
@@ -0,0 +1,61 @@
1
+ import path from "node:path";
2
+
3
+ import type { RegpickConfig, RegistryFile, RegistryItem } from "../types.js";
4
+
5
+ import { appError, type AppError } from "../core/errors.js";
6
+ import { err, ok, type Result } from "../core/result.js";
7
+
8
+ function normalizeSlashes(relativePath: string): string {
9
+ return relativePath.replace(/\\/g, "/");
10
+ }
11
+
12
+ function assertInsideProject(
13
+ projectRoot: string,
14
+ outputPath: string,
15
+ allowOutsideProject: boolean,
16
+ ): Result<void, AppError> {
17
+ const projectRootWithSep = `${path.resolve(projectRoot)}${path.sep}`;
18
+ const resolvedOutput = path.resolve(outputPath);
19
+ if (allowOutsideProject) {
20
+ return ok(undefined);
21
+ }
22
+ if (
23
+ resolvedOutput !== path.resolve(projectRoot) &&
24
+ !resolvedOutput.startsWith(projectRootWithSep)
25
+ ) {
26
+ return err(appError("ValidationError", `Refusing to write outside project: ${resolvedOutput}`));
27
+ }
28
+ return ok(undefined);
29
+ }
30
+
31
+ export function resolveOutputPathFromPolicy(
32
+ item: RegistryItem,
33
+ file: RegistryFile,
34
+ cwd: string,
35
+ config: RegpickConfig,
36
+ ): Result<{ absoluteTarget: string; relativeTarget: string }, AppError> {
37
+ const typeKey = file.type || item.type || "registry:file";
38
+ const mappedBase = config.targetsByType?.[typeKey];
39
+ const preferManifestTarget = config.preferManifestTarget !== false;
40
+ const fallbackFileName = path.basename(file.path || `${item.name}.txt`);
41
+
42
+ let relativeTarget: string;
43
+ if (preferManifestTarget && file.target) {
44
+ relativeTarget = file.target;
45
+ } else if (mappedBase) {
46
+ relativeTarget = path.join(mappedBase, fallbackFileName);
47
+ } else if (file.target) {
48
+ relativeTarget = file.target;
49
+ } else {
50
+ relativeTarget = path.join("src", fallbackFileName);
51
+ }
52
+
53
+ const absoluteTarget = path.resolve(cwd, relativeTarget);
54
+ const assertRes = assertInsideProject(cwd, absoluteTarget, Boolean(config.allowOutsideProject));
55
+ if (!assertRes.ok) return assertRes;
56
+
57
+ return ok({
58
+ absoluteTarget,
59
+ relativeTarget: normalizeSlashes(path.relative(cwd, absoluteTarget)),
60
+ });
61
+ }
@@ -0,0 +1,100 @@
1
+ import type { RegistryFile, RegistryItem, RegistrySourceMeta } from "../types.js";
2
+ import { err, ok, type Result } from "../core/result.js";
3
+ import { appError, type AppError } from "../core/errors.js";
4
+
5
+ type JsonRecord = Record<string, unknown>;
6
+
7
+ function asStringArray(value: unknown): string[] {
8
+ if (!value) {
9
+ return [];
10
+ }
11
+ if (Array.isArray(value)) {
12
+ return value.filter((entry): entry is string => typeof entry === "string");
13
+ }
14
+ if (typeof value === "string") {
15
+ return [value];
16
+ }
17
+ return [];
18
+ }
19
+
20
+ function asObjectArray<T extends JsonRecord>(value: unknown): T[] {
21
+ if (!Array.isArray(value)) {
22
+ return [];
23
+ }
24
+ return value.filter((entry): entry is T => Boolean(entry && typeof entry === "object"));
25
+ }
26
+
27
+ export function normalizeItem(rawItem: JsonRecord, sourceMeta: RegistrySourceMeta): RegistryItem {
28
+ const rawFiles = asObjectArray<JsonRecord>(rawItem.files);
29
+ const files: RegistryFile[] = rawFiles.map((file) => ({
30
+ path: typeof file.path === "string" ? file.path : undefined,
31
+ target: typeof file.target === "string" ? file.target : undefined,
32
+ type:
33
+ typeof file.type === "string"
34
+ ? file.type
35
+ : typeof rawItem.type === "string"
36
+ ? rawItem.type
37
+ : "registry:file",
38
+ content: typeof file.content === "string" ? file.content : undefined,
39
+ url: typeof file.url === "string" ? file.url : undefined,
40
+ }));
41
+
42
+ const name =
43
+ typeof rawItem.name === "string"
44
+ ? rawItem.name
45
+ : typeof rawItem.title === "string"
46
+ ? rawItem.title
47
+ : "unnamed-item";
48
+
49
+ return {
50
+ name,
51
+ title: typeof rawItem.title === "string" ? rawItem.title : name,
52
+ description: typeof rawItem.description === "string" ? rawItem.description : "",
53
+ type: typeof rawItem.type === "string" ? rawItem.type : "registry:file",
54
+ dependencies: asStringArray(rawItem.dependencies),
55
+ devDependencies: asStringArray(rawItem.devDependencies),
56
+ registryDependencies: asStringArray(rawItem.registryDependencies),
57
+ files,
58
+ sourceMeta,
59
+ };
60
+ }
61
+
62
+ export function extractItemReferences(payload: JsonRecord): string[] {
63
+ const items = asObjectArray<JsonRecord>(payload.items);
64
+ return items
65
+ .map((entry) => {
66
+ if (Array.isArray(entry.files)) {
67
+ return null;
68
+ }
69
+ return typeof entry.url === "string"
70
+ ? entry.url
71
+ : typeof entry.href === "string"
72
+ ? entry.href
73
+ : typeof entry.path === "string"
74
+ ? entry.path
75
+ : null;
76
+ })
77
+ .filter((value): value is string => Boolean(value));
78
+ }
79
+
80
+ export function normalizeManifestInline(data: unknown, sourceMeta: RegistrySourceMeta): Result<RegistryItem[], AppError> {
81
+ if (Array.isArray(data)) {
82
+ const items = data
83
+ .filter((entry): entry is JsonRecord => Boolean(entry && typeof entry === "object"))
84
+ .map((entry) => normalizeItem(entry, sourceMeta));
85
+ return ok(items);
86
+ }
87
+
88
+ if (data && typeof data === "object" && Array.isArray((data as JsonRecord).items)) {
89
+ const entries = asObjectArray<JsonRecord>((data as JsonRecord).items).filter((entry) =>
90
+ Array.isArray(entry.files),
91
+ );
92
+ return ok(entries.map((entry) => normalizeItem(entry, sourceMeta)));
93
+ }
94
+
95
+ if (data && typeof data === "object" && Array.isArray((data as JsonRecord).files)) {
96
+ return ok([normalizeItem(data as JsonRecord, sourceMeta)]);
97
+ }
98
+
99
+ return err(appError("RegistryError", "Unsupported manifest structure."));
100
+ }
@@ -0,0 +1,47 @@
1
+ import { appError, type AppError } from "../core/errors.js";
2
+ import { err, ok, type Result } from "../core/result.js";
3
+ import type { CommandContext, RegistryItem } from "../types.js";
4
+
5
+ export function parseSelectedNames(rawSelectFlag: string | boolean | undefined): string[] {
6
+ if (!rawSelectFlag) {
7
+ return [];
8
+ }
9
+
10
+ return String(rawSelectFlag)
11
+ .split(",")
12
+ .map((entry) => entry.trim())
13
+ .filter(Boolean);
14
+ }
15
+
16
+ export function filterItemsByQuery(items: RegistryItem[], query: string): RegistryItem[] {
17
+ if (!query) {
18
+ return items;
19
+ }
20
+
21
+ const lowered = query.toLowerCase();
22
+ return items.filter((item) => {
23
+ return (
24
+ item.name.toLowerCase().includes(lowered) ||
25
+ item.title.toLowerCase().includes(lowered) ||
26
+ (item.description || "").toLowerCase().includes(lowered)
27
+ );
28
+ });
29
+ }
30
+
31
+ export function selectItemsFromFlags(items: RegistryItem[], context: CommandContext): Result<RegistryItem[] | null, AppError> {
32
+ const { flags } = context.args;
33
+ const explicit = parseSelectedNames(flags.select);
34
+ if (Boolean(flags.all)) {
35
+ return ok(items);
36
+ }
37
+
38
+ if (explicit.length) {
39
+ const selected = items.filter((item) => explicit.includes(item.name));
40
+ if (!selected.length) {
41
+ return err(appError("ValidationError", `No items matched --select=${String(flags.select)}`));
42
+ }
43
+ return ok(selected);
44
+ }
45
+
46
+ return ok(null);
47
+ }
package/src/index.ts ADDED
@@ -0,0 +1,117 @@
1
+ import path from "node:path";
2
+ import pc from "picocolors";
3
+
4
+ import type { CommandContext, CommandOutcome } from "./types.js";
5
+ import { type AppError, toAppError } from "./core/errors.js";
6
+ import type { Result } from "./core/result.js";
7
+ import { runAddCommand } from "./commands/add.js";
8
+ import { runInitCommand } from "./commands/init.js";
9
+ import { runListCommand } from "./commands/list.js";
10
+ import { runUpdateCommand } from "./commands/update.js";
11
+ import { runPackCommand } from "./commands/pack.js";
12
+ import { parseCliArgs } from "./shell/cli/args.js";
13
+ import { createRuntimePorts } from "./shell/runtime/ports.js";
14
+
15
+ function printHelp(): void {
16
+ console.log(`
17
+ Usage:
18
+ regpick init
19
+ regpick list [registry-name-or-url]
20
+ regpick add [registry-name-or-url]
21
+ regpick update
22
+ regpick pack [directory]
23
+
24
+ Options:
25
+ --cwd=<path> Working directory (default: current directory)
26
+ --all Select all items in add flow
27
+ --select=a,b,c Select explicit item names in add flow
28
+ --yes Skip confirmation prompts where safe
29
+ --help Show this help
30
+ `);
31
+ }
32
+
33
+ async function run(): Promise<void> {
34
+ const abortController = new AbortController();
35
+
36
+ // Abort prompts on background errors or process termination
37
+ const handleTerminate = (err?: Error) => {
38
+ if (!abortController.signal.aborted) {
39
+ abortController.abort(err);
40
+ }
41
+ if (err instanceof Error) {
42
+ console.error(pc.red(`\n[Fatal Error] ${err.message}`));
43
+ }
44
+ process.exit(1);
45
+ };
46
+
47
+ process.on("SIGINT", () => handleTerminate());
48
+ process.on("SIGTERM", () => handleTerminate());
49
+ process.on("uncaughtException", handleTerminate);
50
+ process.on("unhandledRejection", (reason) => handleTerminate(reason instanceof Error ? reason : new Error(String(reason))));
51
+
52
+ const runtime = createRuntimePorts({ signal: abortController.signal });
53
+ const parsed = parseCliArgs(process.argv.slice(2));
54
+ const command = parsed.positionals[0];
55
+
56
+ if (!command || parsed.flags.help) {
57
+ printHelp();
58
+ return;
59
+ }
60
+
61
+ const context: CommandContext = {
62
+ cwd: parsed.flags.cwd ? path.resolve(process.cwd(), String(parsed.flags.cwd)) : process.cwd(),
63
+ args: parsed,
64
+ runtime,
65
+ };
66
+
67
+ runtime.prompt.intro(pc.cyan("regpick"));
68
+
69
+ try {
70
+ let result: Result<CommandOutcome, AppError>;
71
+ if (command === "init") {
72
+ result = await runInitCommand(context);
73
+ } else if (command === "list") {
74
+ result = await runListCommand(context);
75
+ } else if (command === "add") {
76
+ result = await runAddCommand(context);
77
+ } else if (command === "update") {
78
+ result = await runUpdateCommand(context);
79
+ } else if (command === "pack") {
80
+ result = await runPackCommand(context);
81
+ } else {
82
+ runtime.prompt.error(`Unknown command: ${command}`);
83
+ printHelp();
84
+ process.exitCode = 1;
85
+ return;
86
+ }
87
+
88
+ if (!result.ok) {
89
+ handleAppError(result.error, runtime.prompt.error);
90
+ runtime.prompt.outro(pc.red("Failed."));
91
+ process.exitCode = 1;
92
+ return;
93
+ }
94
+
95
+ if (result.value.kind === "noop") {
96
+ runtime.prompt.outro(pc.yellow(result.value.message));
97
+ return;
98
+ }
99
+
100
+ runtime.prompt.outro(pc.green("Done."));
101
+ } catch (error) {
102
+ const appErr = toAppError(error);
103
+ handleAppError(appErr, runtime.prompt.error);
104
+ runtime.prompt.outro(pc.red("Failed."));
105
+ process.exitCode = 1;
106
+ }
107
+ }
108
+
109
+ function handleAppError(error: AppError, write: (message: string) => void): void {
110
+ if (error.kind === "UserCancelled") {
111
+ write(error.message);
112
+ return;
113
+ }
114
+ write(`[${error.kind}] ${error.message}`);
115
+ }
116
+
117
+ void run();
@@ -0,0 +1,37 @@
1
+ import type { CliArgs, FlagValue } from "../../types.js";
2
+
3
+ function parseValue(rawValue: string): FlagValue {
4
+ if (rawValue === "true") {
5
+ return true;
6
+ }
7
+ if (rawValue === "false") {
8
+ return false;
9
+ }
10
+ return rawValue;
11
+ }
12
+
13
+ export function parseCliArgs(argv: string[]): CliArgs {
14
+ const flags: Record<string, FlagValue> = {};
15
+ const positionals: string[] = [];
16
+
17
+ for (const token of argv) {
18
+ if (!token.startsWith("--")) {
19
+ positionals.push(token);
20
+ continue;
21
+ }
22
+
23
+ const withoutPrefix = token.slice(2);
24
+ const separatorIndex = withoutPrefix.indexOf("=");
25
+
26
+ if (separatorIndex === -1) {
27
+ flags[withoutPrefix] = true;
28
+ continue;
29
+ }
30
+
31
+ const key = withoutPrefix.slice(0, separatorIndex);
32
+ const value = withoutPrefix.slice(separatorIndex + 1);
33
+ flags[key] = parseValue(value);
34
+ }
35
+
36
+ return { flags, positionals };
37
+ }
@@ -0,0 +1,105 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs/promises";
3
+ import { cosmiconfig } from "cosmiconfig";
4
+
5
+ import type { RegpickConfig } from "../types.js";
6
+
7
+ const DEFAULT_CONFIG: RegpickConfig = {
8
+ registries: {
9
+ tebra: "./tebra-icon-registry/registry",
10
+ },
11
+ targetsByType: {
12
+ "registry:icon": "src/components/ui/icons",
13
+ "registry:component": "src/components/ui",
14
+ "registry:file": "src/components/ui",
15
+ },
16
+ aliases: {},
17
+ overwritePolicy: "prompt",
18
+ packageManager: "auto",
19
+ preferManifestTarget: true,
20
+ allowOutsideProject: false,
21
+ };
22
+
23
+ export function getConfigPath(cwd: string): string {
24
+ return path.join(cwd, "regpick.json");
25
+ }
26
+
27
+ export async function readConfig(cwd: string): Promise<{
28
+ config: RegpickConfig;
29
+ configPath: string | null;
30
+ }> {
31
+ const explorer = cosmiconfig("regpick", {
32
+ searchPlaces: ["regpick.json", ".regpickrc", ".regpickrc.json"],
33
+ });
34
+
35
+ const result = await explorer.search(cwd);
36
+
37
+ if (!result || !result.config) {
38
+ return {
39
+ config: { ...DEFAULT_CONFIG },
40
+ configPath: null,
41
+ };
42
+ }
43
+
44
+ const config = result.config as Partial<RegpickConfig>;
45
+
46
+ return {
47
+ config: {
48
+ ...DEFAULT_CONFIG,
49
+ ...config,
50
+ registries: {
51
+ ...DEFAULT_CONFIG.registries,
52
+ ...(config.registries || {}),
53
+ },
54
+ targetsByType: {
55
+ ...DEFAULT_CONFIG.targetsByType,
56
+ ...(config.targetsByType || {}),
57
+ },
58
+ aliases: {
59
+ ...DEFAULT_CONFIG.aliases,
60
+ ...(config.aliases || {}),
61
+ },
62
+ },
63
+ configPath: result.filepath,
64
+ };
65
+ }
66
+
67
+ export async function writeDefaultConfig(
68
+ cwd: string,
69
+ { overwrite = false }: { overwrite?: boolean } = {},
70
+ ): Promise<{ filePath: string; written: boolean }> {
71
+ return writeConfig(cwd, DEFAULT_CONFIG, { overwrite });
72
+ }
73
+
74
+ export async function writeConfig(
75
+ cwd: string,
76
+ config: RegpickConfig,
77
+ { overwrite = false }: { overwrite?: boolean } = {},
78
+ ): Promise<{ filePath: string; written: boolean }> {
79
+ const filePath = getConfigPath(cwd);
80
+
81
+ let exists = false;
82
+ try {
83
+ await fs.access(filePath);
84
+ exists = true;
85
+ } catch {}
86
+
87
+ if (exists && !overwrite) {
88
+ return { filePath, written: false };
89
+ }
90
+
91
+ await fs.writeFile(filePath, JSON.stringify(config, null, 2), "utf8");
92
+ return { filePath, written: true };
93
+ }
94
+
95
+ export function resolveRegistrySource(input: string | undefined, config: RegpickConfig): string | null {
96
+ if (!input) {
97
+ return null;
98
+ }
99
+
100
+ if (config.registries[input]) {
101
+ return String(config.registries[input]);
102
+ }
103
+
104
+ return input;
105
+ }