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.
- package/.github/workflows/release.yml +51 -0
- package/.release-it.json +22 -0
- package/CHANGELOG.md +12 -0
- package/README.md +56 -0
- package/bin/regpick.js +3 -0
- package/docs/mvp-decisions.md +77 -0
- package/examples/README.md +26 -0
- package/examples/complex-ui-registry/registry.json +35 -0
- package/examples/simple-utils-registry/registry.json +28 -0
- package/package.json +39 -0
- package/regpick.config.schema.json +40 -0
- package/src/commands/add.ts +261 -0
- package/src/commands/init.ts +89 -0
- package/src/commands/list.ts +54 -0
- package/src/commands/pack.ts +97 -0
- package/src/commands/update.ts +139 -0
- package/src/core/__tests__/result-errors.test.ts +19 -0
- package/src/core/errors.ts +36 -0
- package/src/core/result.ts +19 -0
- package/src/domain/__tests__/addPlan.test.ts +64 -0
- package/src/domain/__tests__/initCore.test.ts +28 -0
- package/src/domain/__tests__/listCore.test.ts +29 -0
- package/src/domain/__tests__/pathPolicy.test.ts +64 -0
- package/src/domain/__tests__/registryModel.test.ts +32 -0
- package/src/domain/__tests__/selection.test.ts +58 -0
- package/src/domain/addPlan.ts +51 -0
- package/src/domain/aliasCore.ts +13 -0
- package/src/domain/initCore.ts +15 -0
- package/src/domain/listCore.ts +34 -0
- package/src/domain/packCore.ts +44 -0
- package/src/domain/pathPolicy.ts +61 -0
- package/src/domain/registryModel.ts +100 -0
- package/src/domain/selection.ts +47 -0
- package/src/index.ts +117 -0
- package/src/shell/cli/args.ts +37 -0
- package/src/shell/config.ts +105 -0
- package/src/shell/installer.ts +70 -0
- package/src/shell/lockfile.ts +35 -0
- package/src/shell/packageManagers/__tests__/resolver.test.ts +61 -0
- package/src/shell/packageManagers/__tests__/strategy.test.ts +40 -0
- package/src/shell/packageManagers/resolver.ts +27 -0
- package/src/shell/packageManagers/strategy.ts +65 -0
- package/src/shell/registry.ts +182 -0
- package/src/shell/runtime/ports.ts +200 -0
- package/src/types.ts +92 -0
- package/test-clack.ts +2 -0
- package/tsconfig.json +15 -0
- 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
|
+
}
|