@reliverse/rempts-core 1.6.1 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +398 -102
- package/dist/cli.d.ts +32 -0
- package/dist/cli.js +731 -0
- package/dist/config-loader.d.ts +42 -0
- package/dist/config-loader.js +20 -0
- package/dist/config.d.ts +99 -0
- package/dist/config.js +188 -0
- package/dist/file-loader.d.ts +43 -0
- package/dist/file-loader.js +199 -0
- package/dist/global-flags.d.ts +36 -0
- package/dist/global-flags.js +36 -0
- package/dist/mod.d.ts +13 -0
- package/dist/mod.js +19 -0
- package/dist/parser.d.ts +6 -0
- package/dist/parser.js +137 -0
- package/dist/plugin/context.d.ts +13 -0
- package/dist/plugin/context.js +53 -0
- package/dist/plugin/create.d.ts +92 -0
- package/dist/plugin/create.js +61 -0
- package/dist/plugin/loader.d.ts +12 -0
- package/dist/plugin/loader.js +65 -0
- package/dist/plugin/manager.d.ts +53 -0
- package/dist/plugin/manager.js +135 -0
- package/dist/plugin/mod.d.ts +10 -0
- package/dist/plugin/mod.js +27 -0
- package/dist/plugin/store.d.ts +45 -0
- package/dist/plugin/store.js +60 -0
- package/dist/plugin/testing.d.ts +38 -0
- package/dist/plugin/testing.js +175 -0
- package/dist/plugin/types.d.ts +146 -0
- package/dist/tui/registry.d.ts +8 -0
- package/dist/tui/registry.js +10 -0
- package/dist/tui/types.d.ts +58 -0
- package/dist/tui/types.js +10 -0
- package/dist/types.d.ts +178 -0
- package/dist/types.js +25 -0
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.js +27 -0
- package/dist/utils/merge.d.ts +13 -0
- package/dist/utils/merge.js +25 -0
- package/dist/utils/mod.d.ts +6 -0
- package/dist/utils/mod.js +2 -0
- package/dist/utils/type-helpers.d.ts +41 -0
- package/dist/utils/type-helpers.js +0 -0
- package/dist/validation.d.ts +30 -0
- package/dist/validation.js +121 -0
- package/package.json +47 -44
- package/src/cli.ts +1049 -0
- package/src/config-loader.ts +71 -0
- package/src/config.ts +270 -0
- package/src/file-loader.ts +346 -0
- package/src/global-flags.ts +50 -0
- package/src/mod.ts +74 -0
- package/src/parser.ts +212 -0
- package/src/plugin/context.ts +88 -0
- package/src/plugin/create.ts +174 -0
- package/src/plugin/loader.ts +111 -0
- package/src/plugin/manager.ts +244 -0
- package/src/plugin/mod.ts +51 -0
- package/src/plugin/store.ts +124 -0
- package/src/plugin/testing.ts +236 -0
- package/src/plugin/types.ts +206 -0
- package/src/tui/registry.ts +22 -0
- package/src/tui/types.ts +79 -0
- package/src/types.ts +285 -0
- package/src/utils/logger.ts +43 -0
- package/src/utils/merge.ts +54 -0
- package/src/utils/mod.ts +7 -0
- package/src/utils/type-helpers.ts +151 -0
- package/src/validation.ts +177 -0
- package/LICENSE +0 -21
- package/bin/core-impl/anykey/anykey-mod.d.ts +0 -12
- package/bin/core-impl/anykey/anykey-mod.js +0 -125
- package/bin/core-impl/date/date.d.ts +0 -2
- package/bin/core-impl/date/date.js +0 -236
- package/bin/core-impl/editor/editor-mod.d.ts +0 -25
- package/bin/core-impl/editor/editor-mod.js +0 -896
- package/bin/core-impl/figures/figures-mod.d.ts +0 -233
- package/bin/core-impl/figures/figures-mod.js +0 -286
- package/bin/core-impl/figures/figures.test.d.ts +0 -1
- package/bin/core-impl/figures/figures.test.js +0 -474
- package/bin/core-impl/input/confirm-prompt.d.ts +0 -5
- package/bin/core-impl/input/confirm-prompt.js +0 -173
- package/bin/core-impl/input/input-prompt.d.ts +0 -16
- package/bin/core-impl/input/input-prompt.js +0 -370
- package/bin/core-impl/launcher/_parser.d.ts +0 -2
- package/bin/core-impl/launcher/_parser.js +0 -122
- package/bin/core-impl/launcher/_utils.d.ts +0 -8
- package/bin/core-impl/launcher/_utils.js +0 -29
- package/bin/core-impl/launcher/args.d.ts +0 -3
- package/bin/core-impl/launcher/args.js +0 -89
- package/bin/core-impl/launcher/command.d.ts +0 -8
- package/bin/core-impl/launcher/command.js +0 -68
- package/bin/core-impl/launcher/launcher-mod.d.ts +0 -8
- package/bin/core-impl/launcher/launcher-mod.js +0 -34
- package/bin/core-impl/launcher/usage.d.ts +0 -3
- package/bin/core-impl/launcher/usage.js +0 -104
- package/bin/core-impl/msg-fmt/colors.d.ts +0 -30
- package/bin/core-impl/msg-fmt/colors.js +0 -42
- package/bin/core-impl/msg-fmt/logger.d.ts +0 -17
- package/bin/core-impl/msg-fmt/logger.js +0 -106
- package/bin/core-impl/msg-fmt/mapping.d.ts +0 -3
- package/bin/core-impl/msg-fmt/mapping.js +0 -49
- package/bin/core-impl/msg-fmt/messages.d.ts +0 -35
- package/bin/core-impl/msg-fmt/messages.js +0 -314
- package/bin/core-impl/msg-fmt/terminal.d.ts +0 -15
- package/bin/core-impl/msg-fmt/terminal.js +0 -59
- package/bin/core-impl/msg-fmt/variants.d.ts +0 -11
- package/bin/core-impl/msg-fmt/variants.js +0 -52
- package/bin/core-impl/next-steps/next-steps.d.ts +0 -14
- package/bin/core-impl/next-steps/next-steps.js +0 -24
- package/bin/core-impl/number/number-mod.d.ts +0 -28
- package/bin/core-impl/number/number-mod.js +0 -197
- package/bin/core-impl/results/results.d.ts +0 -7
- package/bin/core-impl/results/results.js +0 -27
- package/bin/core-impl/select/multiselect-prompt.d.ts +0 -2
- package/bin/core-impl/select/multiselect-prompt.js +0 -341
- package/bin/core-impl/select/nummultiselect-prompt.d.ts +0 -6
- package/bin/core-impl/select/nummultiselect-prompt.js +0 -105
- package/bin/core-impl/select/numselect-prompt.d.ts +0 -7
- package/bin/core-impl/select/numselect-prompt.js +0 -115
- package/bin/core-impl/select/select-prompt.d.ts +0 -33
- package/bin/core-impl/select/select-prompt.js +0 -302
- package/bin/core-impl/select/toggle-prompt.d.ts +0 -5
- package/bin/core-impl/select/toggle-prompt.js +0 -208
- package/bin/core-impl/st-end/end.d.ts +0 -2
- package/bin/core-impl/st-end/end.js +0 -42
- package/bin/core-impl/st-end/start.d.ts +0 -17
- package/bin/core-impl/st-end/start.js +0 -66
- package/bin/core-impl/task/progress.d.ts +0 -2
- package/bin/core-impl/task/progress.js +0 -57
- package/bin/core-impl/task/spinner.d.ts +0 -15
- package/bin/core-impl/task/spinner.js +0 -110
- package/bin/core-impl/utils/colorize.d.ts +0 -2
- package/bin/core-impl/utils/colorize.js +0 -134
- package/bin/core-impl/utils/errors.d.ts +0 -1
- package/bin/core-impl/utils/errors.js +0 -15
- package/bin/core-impl/utils/prevent.d.ts +0 -10
- package/bin/core-impl/utils/prevent.js +0 -69
- package/bin/core-impl/utils/prompt-end.d.ts +0 -8
- package/bin/core-impl/utils/prompt-end.js +0 -33
- package/bin/core-impl/utils/stream-text.d.ts +0 -18
- package/bin/core-impl/utils/stream-text.js +0 -136
- package/bin/core-impl/utils/system.d.ts +0 -6
- package/bin/core-impl/utils/system.js +0 -7
- package/bin/core-impl/utils/validate.d.ts +0 -22
- package/bin/core-impl/utils/validate.js +0 -17
- package/bin/core-impl/visual/animate/animate.d.ts +0 -14
- package/bin/core-impl/visual/animate/animate.js +0 -64
- package/bin/core-impl/visual/ascii-art/ascii-art.d.ts +0 -6
- package/bin/core-impl/visual/ascii-art/ascii-art.js +0 -12
- package/bin/core-types.d.ts +0 -434
- package/bin/main.d.ts +0 -41
- package/bin/main.js +0 -96
- /package/{bin/core-types.js → dist/plugin/types.js} +0 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { remptsConfigSchema } from "./config";
|
|
4
|
+
|
|
5
|
+
// Type for loaded config with defaults applied by Zod
|
|
6
|
+
export interface LoadedConfig {
|
|
7
|
+
name?: string;
|
|
8
|
+
version?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
commands?: {
|
|
11
|
+
manifest?: string;
|
|
12
|
+
directory?: string;
|
|
13
|
+
generateReport?: boolean;
|
|
14
|
+
};
|
|
15
|
+
// Zod applies defaults, so these objects are never undefined
|
|
16
|
+
build: {
|
|
17
|
+
entry?: string | string[];
|
|
18
|
+
outdir?: string;
|
|
19
|
+
targets: string[]; // Always has default ['native']
|
|
20
|
+
compress: boolean; // Always has default false
|
|
21
|
+
minify: boolean; // Always has default false
|
|
22
|
+
external?: string[];
|
|
23
|
+
sourcemap: boolean; // Always has default true
|
|
24
|
+
};
|
|
25
|
+
dev: {
|
|
26
|
+
watch: boolean; // Always has default true
|
|
27
|
+
inspect: boolean; // Always has default false
|
|
28
|
+
port?: number;
|
|
29
|
+
};
|
|
30
|
+
test: {
|
|
31
|
+
pattern: string | string[]; // Always has default ['**/*.test.ts', '**/*.spec.ts']
|
|
32
|
+
coverage: boolean; // Always has default false
|
|
33
|
+
watch: boolean; // Always has default false
|
|
34
|
+
};
|
|
35
|
+
workspace: {
|
|
36
|
+
packages?: string[];
|
|
37
|
+
shared?: any;
|
|
38
|
+
versionStrategy: "fixed" | "independent"; // Always has default 'fixed'
|
|
39
|
+
};
|
|
40
|
+
release: {
|
|
41
|
+
npm: boolean; // Always has default true
|
|
42
|
+
github: boolean; // Always has default false
|
|
43
|
+
tagFormat: string; // Always has default 'v{{version}}'
|
|
44
|
+
conventionalCommits: boolean; // Always has default true
|
|
45
|
+
};
|
|
46
|
+
plugins: any[]; // Always has default []
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Config file names to search for
|
|
50
|
+
const CONFIG_NAMES = ["dler.config.ts", "dler.config.js", "dler.config.mjs"];
|
|
51
|
+
|
|
52
|
+
export async function loadConfig(cwd = process.cwd()): Promise<LoadedConfig> {
|
|
53
|
+
// Look for config file
|
|
54
|
+
for (const configName of CONFIG_NAMES) {
|
|
55
|
+
const configPath = path.join(cwd, configName);
|
|
56
|
+
if (existsSync(configPath)) {
|
|
57
|
+
try {
|
|
58
|
+
const module = await import(configPath);
|
|
59
|
+
// Arktype assert automatically applies all defaults and validates
|
|
60
|
+
const config = remptsConfigSchema.assert(module.default || module) as LoadedConfig;
|
|
61
|
+
return config;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error(`Error loading config from ${configPath}:`, error);
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Throw error if no config file found
|
|
70
|
+
throw new Error(`No configuration file found. Please create one of: ${CONFIG_NAMES.join(", ")}`);
|
|
71
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Valid workspace version strategies
|
|
5
|
+
*/
|
|
6
|
+
const VersionStrategy = type("'fixed'|'independent'");
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Valid port range with descriptive error messages
|
|
10
|
+
*/
|
|
11
|
+
const PortNumber = type("number.integer > 0 & number.integer < 65536")
|
|
12
|
+
.configure({
|
|
13
|
+
description: "a valid port number",
|
|
14
|
+
})
|
|
15
|
+
.narrow((n: number) => {
|
|
16
|
+
// Check for commonly problematic ports
|
|
17
|
+
const reservedPorts = [22, 80, 443, 3306, 5432]; // SSH, HTTP, HTTPS, MySQL, PostgreSQL
|
|
18
|
+
if (reservedPorts.includes(n)) {
|
|
19
|
+
// Note: ctx.warn is not available in current arktype version
|
|
20
|
+
// This is just informational for future enhancement
|
|
21
|
+
console.warn(`Port ${n} is commonly used by system services and may cause conflicts`);
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* File path validation with basic path safety checks
|
|
28
|
+
*/
|
|
29
|
+
const SafePath = type("string")
|
|
30
|
+
.configure({
|
|
31
|
+
description: "a valid file path",
|
|
32
|
+
})
|
|
33
|
+
.narrow((path: string, ctx) => {
|
|
34
|
+
if (!path.trim()) {
|
|
35
|
+
return ctx.reject("path cannot be empty or only whitespace");
|
|
36
|
+
}
|
|
37
|
+
// Basic path traversal protection
|
|
38
|
+
if (path.includes("../") || path.includes("..\\")) {
|
|
39
|
+
// Note: ctx.warn is not available in current arktype version
|
|
40
|
+
console.warn("path contains directory traversal (..) which may be unsafe");
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build targets validation
|
|
47
|
+
*/
|
|
48
|
+
const BuildTargets = type("string[]")
|
|
49
|
+
.configure({
|
|
50
|
+
description: "an array of valid build targets",
|
|
51
|
+
})
|
|
52
|
+
.narrow((targets: string[], ctx) => {
|
|
53
|
+
const validTargets = [
|
|
54
|
+
"darwin-arm64",
|
|
55
|
+
"darwin-x64",
|
|
56
|
+
"linux-arm64",
|
|
57
|
+
"linux-x64",
|
|
58
|
+
"windows-x64",
|
|
59
|
+
"bun-linux-x64-modern",
|
|
60
|
+
"bun-darwin-x64-modern",
|
|
61
|
+
"bun-windows-x64-modern",
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const invalidTargets = targets.filter((target) => !validTargets.includes(target));
|
|
65
|
+
if (invalidTargets.length > 0) {
|
|
66
|
+
return ctx.reject({
|
|
67
|
+
expected: `valid build targets: ${validTargets.join(", ")}`,
|
|
68
|
+
actual: `invalid targets: ${invalidTargets.join(", ")}`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Comprehensive Rempts configuration schema with enhanced validation
|
|
76
|
+
* Codegen and TypeScript are REQUIRED for all Rempts projects
|
|
77
|
+
*/
|
|
78
|
+
export const remptsConfigSchema = type({
|
|
79
|
+
// Base configuration (required for CLI creation, optional for partial configs)
|
|
80
|
+
"name?": type("string")
|
|
81
|
+
.configure({
|
|
82
|
+
description: "package name (npm naming conventions)",
|
|
83
|
+
})
|
|
84
|
+
.narrow((name: string, ctx) => {
|
|
85
|
+
if (name.length < 1) {
|
|
86
|
+
return ctx.reject("package name cannot be empty");
|
|
87
|
+
}
|
|
88
|
+
if (name.length > 214) {
|
|
89
|
+
return ctx.reject("package name too long (max 214 characters)");
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
}),
|
|
93
|
+
"version?": type("string.semver").configure({
|
|
94
|
+
description: "semantic version string",
|
|
95
|
+
}),
|
|
96
|
+
"description?": type("string")
|
|
97
|
+
.configure({
|
|
98
|
+
description: "package description",
|
|
99
|
+
})
|
|
100
|
+
.narrow((desc: string, ctx) => {
|
|
101
|
+
return desc.length <= 300 || ctx.reject("description too long (max 300 characters)");
|
|
102
|
+
}),
|
|
103
|
+
|
|
104
|
+
// Commands configuration
|
|
105
|
+
"commands?": type({
|
|
106
|
+
"directory?": SafePath.configure({
|
|
107
|
+
description:
|
|
108
|
+
"directory containing command files following pattern: <cmd-name>/cmd.{ts,js,mjs}",
|
|
109
|
+
}),
|
|
110
|
+
"generateReport?": "boolean",
|
|
111
|
+
}).configure({
|
|
112
|
+
description: "command-related configuration",
|
|
113
|
+
}),
|
|
114
|
+
|
|
115
|
+
// Build configuration - TypeScript REQUIRED
|
|
116
|
+
"build?": type({
|
|
117
|
+
"entry?": type("string|string[]")
|
|
118
|
+
.configure({
|
|
119
|
+
description: "entry file(s) for bundling",
|
|
120
|
+
})
|
|
121
|
+
.narrow((entry: string | string[], _ctx) => {
|
|
122
|
+
const entries = Array.isArray(entry) ? entry : [entry];
|
|
123
|
+
if (entries.length === 0) {
|
|
124
|
+
return _ctx.reject("at least one entry file must be specified");
|
|
125
|
+
}
|
|
126
|
+
// Check for common file extensions
|
|
127
|
+
const hasValidExtension = entries.every((e) => /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(e));
|
|
128
|
+
if (!hasValidExtension) {
|
|
129
|
+
// Note: ctx.warn is not available in current arktype version
|
|
130
|
+
console.warn(
|
|
131
|
+
"entry files should typically have .ts, .tsx, .js, .jsx, .mjs, or .cjs extensions"
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
return true;
|
|
135
|
+
}),
|
|
136
|
+
"outdir?": SafePath.configure({
|
|
137
|
+
description: "output directory for build artifacts",
|
|
138
|
+
}),
|
|
139
|
+
"targets?": BuildTargets,
|
|
140
|
+
"compress?": "boolean",
|
|
141
|
+
"minify?": "boolean",
|
|
142
|
+
"external?": type("string[]")
|
|
143
|
+
.configure({
|
|
144
|
+
description: "external dependencies to exclude from bundle",
|
|
145
|
+
})
|
|
146
|
+
.narrow((externals: string[], _ctx) => {
|
|
147
|
+
// Warn about potentially problematic externals
|
|
148
|
+
const problematic = externals.filter(
|
|
149
|
+
(ext) => ext.startsWith("@types/") || ext.includes("*")
|
|
150
|
+
);
|
|
151
|
+
if (problematic.length > 0) {
|
|
152
|
+
// Note: ctx.warn is not available in current arktype version
|
|
153
|
+
console.warn(`potentially problematic externals detected: ${problematic.join(", ")}`);
|
|
154
|
+
}
|
|
155
|
+
return true;
|
|
156
|
+
}),
|
|
157
|
+
"sourcemap?": "boolean",
|
|
158
|
+
}).configure({
|
|
159
|
+
description: "build configuration for bundling and compilation",
|
|
160
|
+
}),
|
|
161
|
+
|
|
162
|
+
// Development configuration
|
|
163
|
+
"dev?": type({
|
|
164
|
+
"watch?": "boolean",
|
|
165
|
+
"inspect?": "boolean",
|
|
166
|
+
"port?": PortNumber,
|
|
167
|
+
}).configure({
|
|
168
|
+
description: "development server configuration",
|
|
169
|
+
}),
|
|
170
|
+
|
|
171
|
+
// Test configuration
|
|
172
|
+
"test?": type({
|
|
173
|
+
"pattern?": type("string|string[]")
|
|
174
|
+
.configure({
|
|
175
|
+
description: "glob patterns for test files",
|
|
176
|
+
})
|
|
177
|
+
.narrow((patterns: string | string[], _ctx) => {
|
|
178
|
+
const patternList = Array.isArray(patterns) ? patterns : [patterns];
|
|
179
|
+
// Basic validation for glob patterns
|
|
180
|
+
const invalidPatterns = patternList.filter((p) => p.includes("../") || p.startsWith("/"));
|
|
181
|
+
if (invalidPatterns.length > 0) {
|
|
182
|
+
// Note: ctx.warn is not available in current arktype version
|
|
183
|
+
console.warn(`glob patterns should be relative: ${invalidPatterns.join(", ")}`);
|
|
184
|
+
}
|
|
185
|
+
return true;
|
|
186
|
+
}),
|
|
187
|
+
"coverage?": "boolean",
|
|
188
|
+
"watch?": "boolean",
|
|
189
|
+
}).configure({
|
|
190
|
+
description: "test configuration",
|
|
191
|
+
}),
|
|
192
|
+
|
|
193
|
+
// Workspace configuration
|
|
194
|
+
"workspace?": type({
|
|
195
|
+
"packages?": type("string[]")
|
|
196
|
+
.configure({
|
|
197
|
+
description: "array of package paths in the workspace",
|
|
198
|
+
})
|
|
199
|
+
.narrow((packages: string[], _ctx) => {
|
|
200
|
+
if (packages.length === 0) {
|
|
201
|
+
// Note: ctx.warn is not available in current arktype version
|
|
202
|
+
console.warn("workspace.packages is empty - no packages will be included");
|
|
203
|
+
}
|
|
204
|
+
// Check for relative paths
|
|
205
|
+
const absolutePaths = packages.filter((pkg) => pkg.startsWith("/"));
|
|
206
|
+
if (absolutePaths.length > 0) {
|
|
207
|
+
// Note: ctx.warn is not available in current arktype version
|
|
208
|
+
console.warn("workspace packages should use relative paths, not absolute paths");
|
|
209
|
+
}
|
|
210
|
+
return true;
|
|
211
|
+
}),
|
|
212
|
+
"shared?": "unknown",
|
|
213
|
+
"versionStrategy?": VersionStrategy.configure({
|
|
214
|
+
description: "how versions are managed across workspace packages",
|
|
215
|
+
}),
|
|
216
|
+
}).configure({
|
|
217
|
+
description: "monorepo workspace configuration",
|
|
218
|
+
}),
|
|
219
|
+
|
|
220
|
+
// Release configuration
|
|
221
|
+
"release?": type({
|
|
222
|
+
"npm?": "boolean",
|
|
223
|
+
"github?": "boolean",
|
|
224
|
+
"tagFormat?": type("string")
|
|
225
|
+
.configure({
|
|
226
|
+
description: "format for git tags (e.g., 'v${version}')",
|
|
227
|
+
})
|
|
228
|
+
.narrow((format: string, ctx) => {
|
|
229
|
+
if (!format.includes("${version}")) {
|
|
230
|
+
return ctx.reject("tagFormat must include '${version}' placeholder");
|
|
231
|
+
}
|
|
232
|
+
return true;
|
|
233
|
+
}),
|
|
234
|
+
"conventionalCommits?": "boolean",
|
|
235
|
+
}).configure({
|
|
236
|
+
description: "release and publishing configuration",
|
|
237
|
+
}),
|
|
238
|
+
|
|
239
|
+
// Plugins configuration
|
|
240
|
+
"plugins?": type("unknown[]").configure({
|
|
241
|
+
description: "array of plugin configurations",
|
|
242
|
+
}),
|
|
243
|
+
}).configure({
|
|
244
|
+
description: "Rempts CLI configuration schema with comprehensive validation",
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Inferred TypeScript type from the schema
|
|
249
|
+
* This ensures runtime validation matches compile-time types
|
|
250
|
+
*/
|
|
251
|
+
export type RemptsConfig = typeof remptsConfigSchema.infer;
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Strict schema for CLI creation that requires name and version
|
|
255
|
+
* Codegen and TypeScript are automatically enabled
|
|
256
|
+
*/
|
|
257
|
+
export const remptsConfigStrictSchema = remptsConfigSchema.and({
|
|
258
|
+
name: "string",
|
|
259
|
+
version: "string.semver",
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
export type RemptsConfigStrict = typeof remptsConfigStrictSchema.infer;
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Helper function to define configuration with type safety
|
|
266
|
+
* Codegen and TypeScript are automatically configured
|
|
267
|
+
*/
|
|
268
|
+
export function defineConfig(config: RemptsConfig): RemptsConfig {
|
|
269
|
+
return config;
|
|
270
|
+
}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { join, relative } from "node:path";
|
|
2
|
+
import type { Command } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* File-based command loader that automatically discovers and loads commands
|
|
6
|
+
* from a directory structure following the pattern: <cmds-dir>/<cmd-name>/cmd.{ts,js,mjs}
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Pre-compiled regex patterns for performance
|
|
10
|
+
const CMD_FILE_PATTERN = /\/cmd\.[^.]+$/;
|
|
11
|
+
const PATH_SEPARATOR_PATTERN = /\//g;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Create a file command loader
|
|
15
|
+
*/
|
|
16
|
+
export function createFileCommandLoader() {
|
|
17
|
+
return {
|
|
18
|
+
/**
|
|
19
|
+
* Load commands from a directory structure
|
|
20
|
+
* @param cmdsDir Directory containing command files following pattern: <cmd-name>/cmd.{ts,js,mjs}
|
|
21
|
+
* @returns Promise resolving to loaded command tree
|
|
22
|
+
*/
|
|
23
|
+
async loadFromDirectory(cmdsDir: string): Promise<CommandFileTree> {
|
|
24
|
+
const commandFiles = await scanCommandFiles(cmdsDir);
|
|
25
|
+
const conflicts = detectConflicts(commandFiles, cmdsDir);
|
|
26
|
+
|
|
27
|
+
if (conflicts.length > 0) {
|
|
28
|
+
const conflictMessages = conflicts.map(
|
|
29
|
+
(conflict) =>
|
|
30
|
+
`Command "${conflict.commandName}" conflicts between:\n` +
|
|
31
|
+
` - ${conflict.files[0]}\n` +
|
|
32
|
+
` - ${conflict.files[1]}`
|
|
33
|
+
);
|
|
34
|
+
throw new Error(`Command conflicts detected:\n${conflictMessages.join("\n\n")}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return buildCommandTree(commandFiles, cmdsDir);
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Load and register commands from file tree
|
|
42
|
+
* Returns commands with their names inferred from file paths
|
|
43
|
+
*/
|
|
44
|
+
async loadCommandsFromTree(tree: CommandFileTree): Promise<CommandWithName[]> {
|
|
45
|
+
return loadCommandsFromTree(tree);
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Scan for command files in directory
|
|
52
|
+
* Only finds files matching the pattern: <cmd-name>/cmd.{ts,js,mjs}
|
|
53
|
+
*/
|
|
54
|
+
async function scanCommandFiles(cmdsDir: string): Promise<string[]> {
|
|
55
|
+
try {
|
|
56
|
+
// Only look for cmd.{ts,js,mjs} files in subdirectories
|
|
57
|
+
const glob = new Bun.Glob("**/cmd.{ts,js,mjs}");
|
|
58
|
+
const files = await Array.fromAsync(glob.scan({ cwd: cmdsDir }));
|
|
59
|
+
|
|
60
|
+
const commandFiles: string[] = [];
|
|
61
|
+
|
|
62
|
+
// Process files in parallel for better performance
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
const fullPath = join(cmdsDir, file);
|
|
65
|
+
// All cmd.{ts,js,mjs} files in subdirectories are considered valid command files
|
|
66
|
+
commandFiles.push(fullPath);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return commandFiles;
|
|
70
|
+
} catch {
|
|
71
|
+
console.warn(`Warning: Could not scan commands directory: ${cmdsDir}`);
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Detect conflicts between command files
|
|
78
|
+
*/
|
|
79
|
+
function detectConflicts(commandFiles: string[], cmdsDir: string): CommandConflict[] {
|
|
80
|
+
const conflicts: CommandConflict[] = [];
|
|
81
|
+
const commandMap = new Map<string, string[]>(); // commandName -> [filePaths]
|
|
82
|
+
const directoryMap = new Map<string, string[]>(); // directory -> [filePaths]
|
|
83
|
+
|
|
84
|
+
for (const filePath of commandFiles) {
|
|
85
|
+
const relativePath = relative(cmdsDir, filePath);
|
|
86
|
+
const commandName = getCommandName(relativePath);
|
|
87
|
+
|
|
88
|
+
// Track files by command name
|
|
89
|
+
if (!commandMap.has(commandName)) {
|
|
90
|
+
commandMap.set(commandName, []);
|
|
91
|
+
}
|
|
92
|
+
commandMap.get(commandName)?.push(filePath);
|
|
93
|
+
|
|
94
|
+
// Track files by directory (for variant conflicts)
|
|
95
|
+
const directory = relativePath.replace(CMD_FILE_PATTERN, "");
|
|
96
|
+
if (!directoryMap.has(directory)) {
|
|
97
|
+
directoryMap.set(directory, []);
|
|
98
|
+
}
|
|
99
|
+
directoryMap.get(directory)?.push(filePath);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check for multiple file variants in same directory first (more specific error)
|
|
103
|
+
const directoriesWithVariants = new Set<string>();
|
|
104
|
+
for (const [directory, files] of directoryMap) {
|
|
105
|
+
if (files.length > 1) {
|
|
106
|
+
conflicts.push({
|
|
107
|
+
commandName: `${directory} has multiple file variants`,
|
|
108
|
+
files,
|
|
109
|
+
});
|
|
110
|
+
directoriesWithVariants.add(directory);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Check for multiple files mapping to same command name (but skip if already reported as variant conflict)
|
|
115
|
+
for (const [commandName, files] of commandMap) {
|
|
116
|
+
if (files.length > 1 && !directoriesWithVariants.has(commandName)) {
|
|
117
|
+
conflicts.push({ commandName, files });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return conflicts;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get command name from file path
|
|
126
|
+
* For the strict structure: <commands-dir>/<command>/cmd.{ts,js,mjs}
|
|
127
|
+
* - greet/cmd.ts -> "greet"
|
|
128
|
+
* - git/status/cmd.ts -> "git status"
|
|
129
|
+
*/
|
|
130
|
+
function getCommandName(filePath: string): string {
|
|
131
|
+
// Remove the "cmd" part and extension: "greet/cmd.ts" -> "greet/"
|
|
132
|
+
const pathWithoutCmd = filePath.replace(CMD_FILE_PATTERN, "");
|
|
133
|
+
|
|
134
|
+
// Remove trailing slash if present: "greet/" -> "greet"
|
|
135
|
+
const trimmed = pathWithoutCmd.replace(/\/$/, "");
|
|
136
|
+
|
|
137
|
+
// Convert path separators to spaces for command hierarchy
|
|
138
|
+
// Handle multiple consecutive slashes and normalize
|
|
139
|
+
return trimmed.replace(PATH_SEPARATOR_PATTERN, " ").replace(/\s+/g, " ").trim();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Build command tree from file structure
|
|
144
|
+
*/
|
|
145
|
+
async function buildCommandTree(commandFiles: string[], cmdsDir: string): Promise<CommandFileTree> {
|
|
146
|
+
const tree: CommandFileTree = {};
|
|
147
|
+
|
|
148
|
+
for (const filePath of commandFiles) {
|
|
149
|
+
const relativePath = relative(cmdsDir, filePath);
|
|
150
|
+
const commandName = getCommandName(relativePath);
|
|
151
|
+
|
|
152
|
+
// For strict structure, command name is just the directory name(s)
|
|
153
|
+
// e.g., "greet/cmd.ts" -> "greet", "build/binary/cmd.ts" -> "build binary"
|
|
154
|
+
const commandNameParts = commandName.split(" ");
|
|
155
|
+
let current = tree;
|
|
156
|
+
|
|
157
|
+
// Build nested structure for multi-level commands
|
|
158
|
+
// If a parent command file exists (e.g., build/cmd.ts), we need to preserve it
|
|
159
|
+
for (let i = 0; i < commandNameParts.length - 1; i++) {
|
|
160
|
+
const part = commandNameParts[i]!;
|
|
161
|
+
if (!current[part]) {
|
|
162
|
+
current[part] = {};
|
|
163
|
+
} else if ("filePath" in current[part]) {
|
|
164
|
+
// Parent command file exists (e.g., build/cmd.ts)
|
|
165
|
+
// Convert it to a tree structure to hold subcommands
|
|
166
|
+
const existingCommand = current[part] as CommandFileInfo;
|
|
167
|
+
current[part] = {
|
|
168
|
+
// Store the parent command file under a special key or as the base
|
|
169
|
+
// We'll handle this in loadCommandsFromTree
|
|
170
|
+
} as CommandFileTree;
|
|
171
|
+
// Re-add the parent command file to the tree
|
|
172
|
+
const treeNode = current[part] as CommandFileTree;
|
|
173
|
+
(treeNode as Record<string, unknown>).__parent__ = existingCommand;
|
|
174
|
+
}
|
|
175
|
+
current = current[part] as CommandFileTree;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const finalPart = commandNameParts.at(-1)!;
|
|
179
|
+
|
|
180
|
+
// If this is a single-part command (e.g., "build") and there's already a tree here,
|
|
181
|
+
// it means we have subcommands - store the parent command file separately
|
|
182
|
+
if (
|
|
183
|
+
commandNameParts.length === 1 &&
|
|
184
|
+
finalPart in current &&
|
|
185
|
+
"filePath" in current[finalPart]!
|
|
186
|
+
) {
|
|
187
|
+
// This shouldn't happen - single-part commands shouldn't conflict
|
|
188
|
+
// But handle it gracefully
|
|
189
|
+
const existing = current[finalPart] as CommandFileInfo;
|
|
190
|
+
if (existing.filePath !== relativePath) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`Command conflict: "${finalPart}" is defined in both "${existing.filePath}" and "${relativePath}"`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
(current as any)[finalPart] = {
|
|
198
|
+
filePath: relativePath,
|
|
199
|
+
importPath: getImportPath(filePath),
|
|
200
|
+
commandName,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return tree;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get import path for a command file
|
|
209
|
+
*/
|
|
210
|
+
function getImportPath(filePath: string): string {
|
|
211
|
+
// For dynamic imports, return the file:// URL for the absolute path
|
|
212
|
+
// This ensures proper resolution regardless of the importing module's location
|
|
213
|
+
return `file://${filePath}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Command with its inferred name from file path
|
|
218
|
+
*/
|
|
219
|
+
export interface CommandWithName {
|
|
220
|
+
name: string;
|
|
221
|
+
command: Command<any, any>;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Load and register commands from file tree
|
|
226
|
+
* Returns commands with their names inferred from file paths
|
|
227
|
+
*/
|
|
228
|
+
async function loadCommandsFromTree(tree: CommandFileTree): Promise<CommandWithName[]> {
|
|
229
|
+
async function loadFromTree(
|
|
230
|
+
obj: CommandFileTree,
|
|
231
|
+
path: string[] = []
|
|
232
|
+
): Promise<CommandWithName[]> {
|
|
233
|
+
const loadedCommands: CommandWithName[] = [];
|
|
234
|
+
|
|
235
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
236
|
+
if (key === "__parent__") {
|
|
237
|
+
// This is a parent command file stored in a tree (e.g., build/cmd.ts when build/binary/cmd.ts exists)
|
|
238
|
+
const commandInfo = value as CommandFileInfo;
|
|
239
|
+
try {
|
|
240
|
+
const module = await import(commandInfo.importPath);
|
|
241
|
+
const command = module.default || module;
|
|
242
|
+
|
|
243
|
+
// Load the parent command with its full name
|
|
244
|
+
loadedCommands.push({
|
|
245
|
+
name: commandInfo.commandName,
|
|
246
|
+
command,
|
|
247
|
+
});
|
|
248
|
+
} catch (error) {
|
|
249
|
+
console.warn(`Failed to load command from ${commandInfo.importPath}:`, error);
|
|
250
|
+
}
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (typeof value === "object" && "filePath" in value) {
|
|
255
|
+
// This is a command file (leaf node)
|
|
256
|
+
const commandInfo = value as CommandFileInfo;
|
|
257
|
+
try {
|
|
258
|
+
const module = await import(commandInfo.importPath);
|
|
259
|
+
const command = module.default || module;
|
|
260
|
+
|
|
261
|
+
// Name is always inferred from file path
|
|
262
|
+
// e.g., "build/binary/cmd.ts" -> "build binary"
|
|
263
|
+
const inferredName = commandInfo.commandName;
|
|
264
|
+
|
|
265
|
+
loadedCommands.push({
|
|
266
|
+
name: inferredName,
|
|
267
|
+
command,
|
|
268
|
+
});
|
|
269
|
+
} catch (error) {
|
|
270
|
+
console.warn(`Failed to load command from ${commandInfo.importPath}:`, error);
|
|
271
|
+
}
|
|
272
|
+
} else {
|
|
273
|
+
// This is a nested tree - may contain subcommands and/or a parent command
|
|
274
|
+
const subCommands = await loadFromTree(value as CommandFileTree, [...path, key]);
|
|
275
|
+
if (subCommands.length > 0) {
|
|
276
|
+
// Check if there's a parent command file (stored as __parent__)
|
|
277
|
+
const parentCommandInfo = (value as Record<string, unknown>).__parent__ as
|
|
278
|
+
| CommandFileInfo
|
|
279
|
+
| undefined;
|
|
280
|
+
|
|
281
|
+
if (parentCommandInfo) {
|
|
282
|
+
// Parent command file exists (e.g., build/cmd.ts)
|
|
283
|
+
// Load it - subcommands are already registered separately from files
|
|
284
|
+
try {
|
|
285
|
+
const module = await import(parentCommandInfo.importPath);
|
|
286
|
+
const parentCommand = module.default || module;
|
|
287
|
+
|
|
288
|
+
// Register the parent command - subcommands are discovered dynamically from commands map
|
|
289
|
+
loadedCommands.push({
|
|
290
|
+
name: parentCommandInfo.commandName,
|
|
291
|
+
command: parentCommand,
|
|
292
|
+
});
|
|
293
|
+
} catch (error) {
|
|
294
|
+
console.warn(
|
|
295
|
+
`Failed to load parent command from ${parentCommandInfo.importPath}:`,
|
|
296
|
+
error
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
} else {
|
|
300
|
+
// No parent command file - create a synthetic parent command
|
|
301
|
+
// Subcommands are discovered dynamically from the commands map
|
|
302
|
+
// The synthetic parent has no handler/render, so run() will show help when subcommands exist
|
|
303
|
+
const parentCommand: Command<any, any> = {
|
|
304
|
+
description: `${key} commands`,
|
|
305
|
+
};
|
|
306
|
+
loadedCommands.push({
|
|
307
|
+
name: key,
|
|
308
|
+
command: parentCommand,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return loadedCommands;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return await loadFromTree(tree);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Types for file-based command loading
|
|
323
|
+
*/
|
|
324
|
+
export interface CommandFileInfo {
|
|
325
|
+
filePath: string;
|
|
326
|
+
importPath: string;
|
|
327
|
+
commandName: string;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export interface CommandFileTree {
|
|
331
|
+
[key: string]: CommandFileTree | CommandFileInfo | undefined;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export interface CommandConflict {
|
|
335
|
+
commandName: string;
|
|
336
|
+
files: string[];
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Utility function to load commands from directory
|
|
341
|
+
*/
|
|
342
|
+
export async function loadCommandsFromDirectory(cmdsDir: string): Promise<CommandWithName[]> {
|
|
343
|
+
const loader = createFileCommandLoader();
|
|
344
|
+
const tree = await loader.loadFromDirectory(cmdsDir);
|
|
345
|
+
return loader.loadCommandsFromTree(tree);
|
|
346
|
+
}
|