padrone 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -1
- package/LICENSE +1 -1
- package/README.md +60 -30
- package/dist/args-CKNh7Dm9.mjs +175 -0
- package/dist/args-CKNh7Dm9.mjs.map +1 -0
- package/dist/chunk-y_GBKt04.mjs +5 -0
- package/dist/codegen/index.d.mts +305 -0
- package/dist/codegen/index.d.mts.map +1 -0
- package/dist/codegen/index.mjs +1348 -0
- package/dist/codegen/index.mjs.map +1 -0
- package/dist/completion.d.mts +64 -0
- package/dist/completion.d.mts.map +1 -0
- package/dist/completion.mjs +417 -0
- package/dist/completion.mjs.map +1 -0
- package/dist/docs/index.d.mts +34 -0
- package/dist/docs/index.d.mts.map +1 -0
- package/dist/docs/index.mjs +404 -0
- package/dist/docs/index.mjs.map +1 -0
- package/dist/formatter-Dvx7jFXr.d.mts +82 -0
- package/dist/formatter-Dvx7jFXr.d.mts.map +1 -0
- package/dist/help-mUIX0T0V.mjs +1195 -0
- package/dist/help-mUIX0T0V.mjs.map +1 -0
- package/dist/index.d.mts +120 -546
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1180 -1197
- package/dist/index.mjs.map +1 -1
- package/dist/test.d.mts +112 -0
- package/dist/test.d.mts.map +1 -0
- package/dist/test.mjs +138 -0
- package/dist/test.mjs.map +1 -0
- package/dist/types-qrtt0135.d.mts +1037 -0
- package/dist/types-qrtt0135.d.mts.map +1 -0
- package/dist/update-check-EbNDkzyV.mjs +146 -0
- package/dist/update-check-EbNDkzyV.mjs.map +1 -0
- package/package.json +61 -21
- package/src/args.ts +365 -0
- package/src/cli/completions.ts +29 -0
- package/src/cli/docs.ts +86 -0
- package/src/cli/doctor.ts +312 -0
- package/src/cli/index.ts +159 -0
- package/src/cli/init.ts +135 -0
- package/src/cli/link.ts +320 -0
- package/src/cli/wrap.ts +152 -0
- package/src/codegen/README.md +118 -0
- package/src/codegen/code-builder.ts +226 -0
- package/src/codegen/discovery.ts +232 -0
- package/src/codegen/file-emitter.ts +73 -0
- package/src/codegen/generators/barrel-file.ts +16 -0
- package/src/codegen/generators/command-file.ts +184 -0
- package/src/codegen/generators/command-tree.ts +124 -0
- package/src/codegen/index.ts +33 -0
- package/src/codegen/parsers/fish.ts +163 -0
- package/src/codegen/parsers/help.ts +378 -0
- package/src/codegen/parsers/merge.ts +158 -0
- package/src/codegen/parsers/zsh.ts +221 -0
- package/src/codegen/schema-to-code.ts +199 -0
- package/src/codegen/template.ts +69 -0
- package/src/codegen/types.ts +143 -0
- package/src/colorizer.ts +2 -2
- package/src/command-utils.ts +501 -0
- package/src/completion.ts +110 -97
- package/src/create.ts +1036 -305
- package/src/docs/index.ts +607 -0
- package/src/errors.ts +131 -0
- package/src/formatter.ts +149 -63
- package/src/help.ts +151 -55
- package/src/index.ts +12 -15
- package/src/interactive.ts +169 -0
- package/src/parse.ts +31 -16
- package/src/repl-loop.ts +317 -0
- package/src/runtime.ts +304 -0
- package/src/shell-utils.ts +83 -0
- package/src/test.ts +285 -0
- package/src/type-helpers.ts +10 -10
- package/src/type-utils.ts +124 -14
- package/src/types.ts +752 -154
- package/src/update-check.ts +244 -0
- package/src/wrap.ts +44 -40
- package/src/zod.d.ts +2 -2
- package/src/options.ts +0 -180
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import type { ResolvedPadroneRuntime } from './runtime.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Configuration for the update check feature.
|
|
5
|
+
*/
|
|
6
|
+
export type UpdateCheckConfig = {
|
|
7
|
+
/**
|
|
8
|
+
* The npm package name to check. Defaults to the program name.
|
|
9
|
+
*/
|
|
10
|
+
packageName?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Registry to check for updates.
|
|
13
|
+
* - `'npm'` — checks the npm registry (default)
|
|
14
|
+
* - A URL string — custom registry endpoint that returns JSON with a `version` or `dist-tags.latest` field
|
|
15
|
+
*/
|
|
16
|
+
registry?: 'npm' | string;
|
|
17
|
+
/**
|
|
18
|
+
* How often to check for updates. Accepts shorthand like `'1d'`, `'12h'`, `'30m'`.
|
|
19
|
+
* Defaults to `'1d'` (once per day).
|
|
20
|
+
*/
|
|
21
|
+
interval?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Path to the cache file for storing the last check timestamp and latest version.
|
|
24
|
+
* Defaults to `~/.config/<programName>-update-check.json`.
|
|
25
|
+
*/
|
|
26
|
+
cache?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Environment variable name to disable update checks (e.g. `'MYAPP_NO_UPDATE_CHECK'`).
|
|
29
|
+
* When set to a truthy value, update checks are skipped.
|
|
30
|
+
* Defaults to `'<PROGRAM_NAME>_NO_UPDATE_CHECK'` (uppercased, hyphens to underscores).
|
|
31
|
+
*/
|
|
32
|
+
disableEnvVar?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type CacheData = {
|
|
36
|
+
lastCheck: number;
|
|
37
|
+
latestVersion: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parses an interval string like '1d', '12h', '30m', '1w' into milliseconds.
|
|
42
|
+
*/
|
|
43
|
+
export function parseInterval(interval: string): number {
|
|
44
|
+
const match = interval.match(/^(\d+)\s*(ms|s|m|h|d|w)$/);
|
|
45
|
+
if (!match) return 86_400_000; // default 1d
|
|
46
|
+
|
|
47
|
+
const value = parseInt(match[1]!, 10);
|
|
48
|
+
const unit = match[2]!;
|
|
49
|
+
|
|
50
|
+
switch (unit) {
|
|
51
|
+
case 'ms':
|
|
52
|
+
return value;
|
|
53
|
+
case 's':
|
|
54
|
+
return value * 1000;
|
|
55
|
+
case 'm':
|
|
56
|
+
return value * 60_000;
|
|
57
|
+
case 'h':
|
|
58
|
+
return value * 3_600_000;
|
|
59
|
+
case 'd':
|
|
60
|
+
return value * 86_400_000;
|
|
61
|
+
case 'w':
|
|
62
|
+
return value * 604_800_000;
|
|
63
|
+
default:
|
|
64
|
+
return 86_400_000;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Compares two semver version strings.
|
|
70
|
+
* Returns true if `latest` is newer than `current`.
|
|
71
|
+
*/
|
|
72
|
+
export function isNewerVersion(current: string, latest: string): boolean {
|
|
73
|
+
const parse = (v: string) => {
|
|
74
|
+
const cleaned = v.replace(/^v/, '');
|
|
75
|
+
const parts = cleaned.split('-');
|
|
76
|
+
const nums = parts[0]!.split('.').map(Number);
|
|
77
|
+
return { major: nums[0] ?? 0, minor: nums[1] ?? 0, patch: nums[2] ?? 0, prerelease: parts[1] };
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const c = parse(current);
|
|
81
|
+
const l = parse(latest);
|
|
82
|
+
|
|
83
|
+
// Don't notify about pre-release versions unless user is already on a pre-release
|
|
84
|
+
if (l.prerelease && !c.prerelease) return false;
|
|
85
|
+
|
|
86
|
+
if (l.major !== c.major) return l.major > c.major;
|
|
87
|
+
if (l.minor !== c.minor) return l.minor > c.minor;
|
|
88
|
+
if (l.patch !== c.patch) return l.patch > c.patch;
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Reads the update check cache file.
|
|
94
|
+
*/
|
|
95
|
+
function readCache(cachePath: string): CacheData | undefined {
|
|
96
|
+
try {
|
|
97
|
+
const { existsSync, readFileSync } = require('node:fs') as typeof import('node:fs');
|
|
98
|
+
if (!existsSync(cachePath)) return undefined;
|
|
99
|
+
const data = JSON.parse(readFileSync(cachePath, 'utf-8'));
|
|
100
|
+
if (typeof data.lastCheck === 'number' && typeof data.latestVersion === 'string') {
|
|
101
|
+
return data as CacheData;
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
// Ignore errors
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Writes the update check cache file.
|
|
111
|
+
*/
|
|
112
|
+
function writeCache(cachePath: string, data: CacheData): void {
|
|
113
|
+
try {
|
|
114
|
+
const { existsSync, mkdirSync, writeFileSync } = require('node:fs') as typeof import('node:fs');
|
|
115
|
+
const { dirname } = require('node:path') as typeof import('node:path');
|
|
116
|
+
const dir = dirname(cachePath);
|
|
117
|
+
if (!existsSync(dir)) {
|
|
118
|
+
mkdirSync(dir, { recursive: true });
|
|
119
|
+
}
|
|
120
|
+
writeFileSync(cachePath, JSON.stringify(data), 'utf-8');
|
|
121
|
+
} catch {
|
|
122
|
+
// Ignore errors — cache is best-effort
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Resolves the cache path, expanding `~` to the home directory.
|
|
128
|
+
*/
|
|
129
|
+
function resolveCachePath(cachePath: string): string {
|
|
130
|
+
const { homedir } = require('node:os') as typeof import('node:os');
|
|
131
|
+
const { resolve } = require('node:path') as typeof import('node:path');
|
|
132
|
+
if (cachePath.startsWith('~')) {
|
|
133
|
+
return cachePath.replace('~', homedir());
|
|
134
|
+
}
|
|
135
|
+
return resolve(cachePath);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Fetches the latest version from the registry.
|
|
140
|
+
*/
|
|
141
|
+
async function fetchLatestVersion(packageName: string, registry: string): Promise<string | undefined> {
|
|
142
|
+
const url = registry === 'npm' ? `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest` : registry;
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const response = await fetch(url);
|
|
146
|
+
if (!response.ok) return undefined;
|
|
147
|
+
const data = (await response.json()) as Record<string, unknown>;
|
|
148
|
+
|
|
149
|
+
// npm registry returns { version: "x.y.z" }
|
|
150
|
+
if (typeof data.version === 'string') return data.version;
|
|
151
|
+
|
|
152
|
+
// Custom endpoint may return { "dist-tags": { latest: "x.y.z" } }
|
|
153
|
+
const distTags = data['dist-tags'] as Record<string, string> | undefined;
|
|
154
|
+
if (distTags?.latest) return distTags.latest;
|
|
155
|
+
} catch {
|
|
156
|
+
// Network errors are expected (offline, firewall, etc.)
|
|
157
|
+
}
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Formats the update notification message.
|
|
163
|
+
*/
|
|
164
|
+
export function formatUpdateMessage(currentVersion: string, latestVersion: string, packageName: string): string {
|
|
165
|
+
const updateCommand = `npm update -g ${packageName}`;
|
|
166
|
+
return `\n Update available: ${currentVersion} \u2192 ${latestVersion}\n Run "${updateCommand}" to update\n`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Checks for updates in the background. Returns a function that, when called,
|
|
171
|
+
* prints the update notification if a newer version was found.
|
|
172
|
+
*
|
|
173
|
+
* This is designed to be non-blocking: the check starts immediately but the
|
|
174
|
+
* result is only consumed after command execution completes.
|
|
175
|
+
*/
|
|
176
|
+
export function createUpdateChecker(
|
|
177
|
+
programName: string,
|
|
178
|
+
currentVersion: string,
|
|
179
|
+
config: UpdateCheckConfig,
|
|
180
|
+
runtime: ResolvedPadroneRuntime,
|
|
181
|
+
): () => void {
|
|
182
|
+
const packageName = config.packageName ?? programName;
|
|
183
|
+
const registry = config.registry ?? 'npm';
|
|
184
|
+
const intervalMs = parseInterval(config.interval ?? '1d');
|
|
185
|
+
const disableEnvVar = config.disableEnvVar ?? `${programName.toUpperCase().replace(/-/g, '_')}_NO_UPDATE_CHECK`;
|
|
186
|
+
|
|
187
|
+
const defaultCachePath = `~/.config/${programName}-update-check.json`;
|
|
188
|
+
const cachePath = resolveCachePath(config.cache ?? defaultCachePath);
|
|
189
|
+
|
|
190
|
+
// Check if disabled
|
|
191
|
+
const env = runtime.env();
|
|
192
|
+
if (env.CI || env.CONTINUOUS_INTEGRATION) return noop;
|
|
193
|
+
if (env[disableEnvVar]) return noop;
|
|
194
|
+
if (typeof process !== 'undefined' && !process.stdout?.isTTY) return noop;
|
|
195
|
+
|
|
196
|
+
// Check cache — if we checked recently, use cached result
|
|
197
|
+
const cached = readCache(cachePath);
|
|
198
|
+
if (cached && Date.now() - cached.lastCheck < intervalMs) {
|
|
199
|
+
// Use cached version for display
|
|
200
|
+
if (isNewerVersion(currentVersion, cached.latestVersion)) {
|
|
201
|
+
return () => {
|
|
202
|
+
runtime.error(formatUpdateMessage(currentVersion, cached.latestVersion, packageName));
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
return noop;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Start background fetch
|
|
209
|
+
const fetchPromise = fetchLatestVersion(packageName, registry).then((latestVersion) => {
|
|
210
|
+
if (latestVersion) {
|
|
211
|
+
writeCache(cachePath, { lastCheck: Date.now(), latestVersion });
|
|
212
|
+
if (isNewerVersion(currentVersion, latestVersion)) {
|
|
213
|
+
return latestVersion;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return undefined;
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Return a function that blocks on the result (briefly — the fetch should be done by now)
|
|
220
|
+
let resolved: string | undefined | null = null; // null = not yet resolved
|
|
221
|
+
fetchPromise.then(
|
|
222
|
+
(v) => {
|
|
223
|
+
resolved = v;
|
|
224
|
+
},
|
|
225
|
+
() => {
|
|
226
|
+
resolved = undefined;
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
return () => {
|
|
231
|
+
// If the fetch already resolved, use the result synchronously
|
|
232
|
+
if (resolved !== null) {
|
|
233
|
+
if (resolved) {
|
|
234
|
+
runtime.error(formatUpdateMessage(currentVersion, resolved, packageName));
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Otherwise, we can't block — just skip this time.
|
|
240
|
+
// The cache will be written when the promise resolves, so next invocation will show the message.
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function noop() {}
|
package/src/wrap.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
import { ValidationError } from './errors.ts';
|
|
2
3
|
import type { PadroneSchema } from './types.ts';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
* Configuration
|
|
6
|
+
* Configuration for wrapping an external CLI tool.
|
|
6
7
|
*/
|
|
7
|
-
export type WrapConfig<
|
|
8
|
+
export type WrapConfig<TCommandArgs extends PadroneSchema = PadroneSchema, TWrapArgs extends PadroneSchema = TCommandArgs> = {
|
|
8
9
|
/**
|
|
9
10
|
* The command to execute (e.g., 'git', 'docker', 'npm').
|
|
10
11
|
*/
|
|
11
12
|
command: string;
|
|
12
13
|
/**
|
|
13
|
-
* Optional fixed arguments that always precede the
|
|
14
|
+
* Optional fixed arguments that always precede the arguments (e.g., ['commit'] for 'git commit').
|
|
14
15
|
*/
|
|
15
16
|
args?: string[];
|
|
16
17
|
/**
|
|
@@ -24,12 +25,12 @@ export type WrapConfig<TCommandOpts extends PadroneSchema = PadroneSchema, TWrap
|
|
|
24
25
|
*/
|
|
25
26
|
inheritStdio?: boolean;
|
|
26
27
|
/**
|
|
27
|
-
* Optional schema that transforms command
|
|
28
|
-
* The schema's input type should match the command
|
|
28
|
+
* Optional schema that transforms command arguments to external CLI arguments.
|
|
29
|
+
* The schema's input type should match the command arguments, and its output type defines
|
|
29
30
|
* the arguments expected by the external command.
|
|
30
|
-
* If not provided, command
|
|
31
|
+
* If not provided, command arguments are passed through as-is.
|
|
31
32
|
*/
|
|
32
|
-
schema?:
|
|
33
|
+
schema?: TWrapArgs | ((commandArguments: TCommandArgs) => TWrapArgs);
|
|
33
34
|
};
|
|
34
35
|
|
|
35
36
|
/**
|
|
@@ -55,28 +56,28 @@ export type WrapResult = {
|
|
|
55
56
|
};
|
|
56
57
|
|
|
57
58
|
/**
|
|
58
|
-
* Converts parsed
|
|
59
|
+
* Converts parsed arguments to CLI arguments for an external command.
|
|
59
60
|
*/
|
|
60
|
-
function
|
|
61
|
+
function argsToCliArgs(input: Record<string, unknown> | undefined, positional: string[] = []): string[] {
|
|
61
62
|
const args: string[] = [];
|
|
62
63
|
|
|
63
|
-
// Handle undefined or null
|
|
64
|
-
if (!
|
|
64
|
+
// Handle undefined or null input
|
|
65
|
+
if (!input) return args;
|
|
65
66
|
|
|
66
67
|
const positionalValues: Record<string, unknown> = {};
|
|
67
|
-
const
|
|
68
|
+
const regularArguments: Record<string, unknown> = {};
|
|
68
69
|
|
|
69
|
-
// Separate positional and regular
|
|
70
|
-
for (const [key, value] of Object.entries(
|
|
70
|
+
// Separate positional and regular arguments
|
|
71
|
+
for (const [key, value] of Object.entries(input)) {
|
|
71
72
|
if (positional.includes(key) || positional.includes(`...${key}`)) {
|
|
72
73
|
positionalValues[key] = value;
|
|
73
74
|
} else {
|
|
74
|
-
|
|
75
|
+
regularArguments[key] = value;
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
// Add regular
|
|
79
|
-
for (const [key, value] of Object.entries(
|
|
79
|
+
// Add regular arguments first
|
|
80
|
+
for (const [key, value] of Object.entries(regularArguments)) {
|
|
80
81
|
if (value === undefined || value === null) continue;
|
|
81
82
|
|
|
82
83
|
// Use the key as-is with -- prefix
|
|
@@ -115,38 +116,41 @@ function optionsToArgs(options: Record<string, unknown> | undefined, positional:
|
|
|
115
116
|
/**
|
|
116
117
|
* Creates an action handler that wraps an external CLI tool.
|
|
117
118
|
* @param config - Configuration for wrapping the external command (includes optional schema)
|
|
118
|
-
* @param
|
|
119
|
+
* @param commandArguments - The command's arguments schema
|
|
119
120
|
* @param commandPositional - Default positional config from the wrapping command
|
|
120
121
|
*/
|
|
121
|
-
export function createWrapHandler<
|
|
122
|
-
config: WrapConfig<
|
|
123
|
-
|
|
122
|
+
export function createWrapHandler<TCommandArgs extends PadroneSchema, TWrapArgs extends PadroneSchema>(
|
|
123
|
+
config: WrapConfig<TCommandArgs, TWrapArgs>,
|
|
124
|
+
commandArguments: TCommandArgs,
|
|
124
125
|
commandPositional?: string[],
|
|
125
|
-
): (
|
|
126
|
-
return async (
|
|
126
|
+
): (args: StandardSchemaV1.InferOutput<TCommandArgs>) => Promise<WrapResult> {
|
|
127
|
+
return async (args: StandardSchemaV1.InferOutput<TCommandArgs>): Promise<WrapResult> => {
|
|
127
128
|
const { command, args: fixedArgs = [], inheritStdio = true, positional = commandPositional, schema: wrapSchema } = config;
|
|
128
129
|
|
|
129
130
|
// Get the wrap schema (handle function or direct schema)
|
|
130
|
-
const schema = wrapSchema ? (typeof wrapSchema === 'function' ? wrapSchema(
|
|
131
|
+
const schema = wrapSchema ? (typeof wrapSchema === 'function' ? wrapSchema(commandArguments) : wrapSchema) : commandArguments;
|
|
131
132
|
|
|
132
|
-
// Transform command
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
133
|
+
// Transform command arguments to external CLI arguments using the wrap schema
|
|
134
|
+
const validationResult = schema['~standard'].validate(args);
|
|
135
|
+
|
|
136
|
+
const processResult = (result: StandardSchemaV1.Result<unknown>) => {
|
|
137
|
+
if (result.issues) {
|
|
138
|
+
const issueMessages = result.issues
|
|
139
|
+
.map((i: StandardSchemaV1.Issue) => ` - ${(i.path as (string | number)[] | undefined)?.join('.') || 'root'}: ${i.message}`)
|
|
140
|
+
.join('\n');
|
|
141
|
+
throw new ValidationError(`Wrap schema validation failed:\n${issueMessages}`, result.issues as any);
|
|
142
|
+
}
|
|
143
|
+
return result.value;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const externalArguments =
|
|
147
|
+
validationResult instanceof Promise ? await validationResult.then(processResult) : processResult(validationResult);
|
|
144
148
|
|
|
145
|
-
// Convert
|
|
146
|
-
const
|
|
149
|
+
// Convert arguments to CLI arguments
|
|
150
|
+
const regularArgs = argsToCliArgs(externalArguments as Record<string, unknown>, positional);
|
|
147
151
|
|
|
148
|
-
// Combine fixed args and
|
|
149
|
-
const allArgs = [...fixedArgs, ...
|
|
152
|
+
// Combine fixed args and regular args
|
|
153
|
+
const allArgs = [...fixedArgs, ...regularArgs];
|
|
150
154
|
|
|
151
155
|
// Execute the external command
|
|
152
156
|
const proc = Bun.spawn([command, ...allArgs], {
|
package/src/zod.d.ts
CHANGED
package/src/options.ts
DELETED
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import type { StandardJSONSchemaV1 } from '@standard-schema/spec';
|
|
2
|
-
|
|
3
|
-
export interface PadroneOptionsMeta {
|
|
4
|
-
description?: string;
|
|
5
|
-
alias?: string[] | string;
|
|
6
|
-
deprecated?: boolean | string;
|
|
7
|
-
hidden?: boolean;
|
|
8
|
-
examples?: unknown[];
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
type PositionalArgs<TObj> =
|
|
12
|
-
TObj extends Record<string, any>
|
|
13
|
-
? {
|
|
14
|
-
[K in keyof TObj]: TObj[K] extends Array<any> ? `...${K & string}` : K & string;
|
|
15
|
-
}[keyof TObj]
|
|
16
|
-
: string;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Meta configuration for options including positional arguments.
|
|
20
|
-
* The `positional` array defines which options are positional arguments and their order.
|
|
21
|
-
* Use '...name' prefix to indicate variadic (rest) arguments, matching JS/TS rest syntax.
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* ```ts
|
|
25
|
-
* .arguments(schema, {
|
|
26
|
-
* positional: ['source', '...files', 'dest'], // '...files' is variadic
|
|
27
|
-
* })
|
|
28
|
-
* ```
|
|
29
|
-
*/
|
|
30
|
-
export interface PadroneMeta<TObj = Record<string, any>> {
|
|
31
|
-
/**
|
|
32
|
-
* Array of option names that should be treated as positional arguments.
|
|
33
|
-
* Order in array determines position. Use '...name' prefix for variadic args.
|
|
34
|
-
* @example ['source', '...files', 'dest'] - 'files' captures multiple values
|
|
35
|
-
*/
|
|
36
|
-
positional?: PositionalArgs<TObj>[];
|
|
37
|
-
/**
|
|
38
|
-
* Per-option metadata.
|
|
39
|
-
*/
|
|
40
|
-
options?: { [K in keyof TObj]?: PadroneOptionsMeta };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Parse positional configuration to extract names and variadic info.
|
|
45
|
-
*/
|
|
46
|
-
export function parsePositionalConfig(positional: string[]): { name: string; variadic: boolean }[] {
|
|
47
|
-
return positional.map((p) => {
|
|
48
|
-
const isVariadic = p.startsWith('...');
|
|
49
|
-
const name = isVariadic ? p.slice(3) : p;
|
|
50
|
-
return { name, variadic: isVariadic };
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Result type for extractSchemaMetadata function.
|
|
56
|
-
*/
|
|
57
|
-
interface SchemaMetadataResult {
|
|
58
|
-
aliases: Record<string, string>;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Extract all option metadata from schema and meta in a single pass.
|
|
63
|
-
* This consolidates aliases, env bindings, and config keys extraction.
|
|
64
|
-
*/
|
|
65
|
-
export function extractSchemaMetadata(
|
|
66
|
-
schema: StandardJSONSchemaV1,
|
|
67
|
-
meta?: Record<string, PadroneOptionsMeta | undefined>,
|
|
68
|
-
): SchemaMetadataResult {
|
|
69
|
-
const aliases: Record<string, string> = {};
|
|
70
|
-
|
|
71
|
-
// Extract from meta object
|
|
72
|
-
if (meta) {
|
|
73
|
-
for (const [key, value] of Object.entries(meta)) {
|
|
74
|
-
if (!value) continue;
|
|
75
|
-
|
|
76
|
-
// Extract aliases
|
|
77
|
-
if (value.alias) {
|
|
78
|
-
const list = typeof value.alias === 'string' ? [value.alias] : value.alias;
|
|
79
|
-
for (const aliasKey of list) {
|
|
80
|
-
if (typeof aliasKey === 'string' && aliasKey && aliasKey !== key) {
|
|
81
|
-
aliases[aliasKey] = key;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Extract from JSON schema properties
|
|
89
|
-
try {
|
|
90
|
-
const jsonSchema = schema['~standard'].jsonSchema.input({ target: 'draft-2020-12' }) as Record<string, any>;
|
|
91
|
-
if (jsonSchema.type === 'object' && jsonSchema.properties) {
|
|
92
|
-
for (const [propertyName, propertySchema] of Object.entries(jsonSchema.properties as Record<string, any>)) {
|
|
93
|
-
if (!propertySchema) continue;
|
|
94
|
-
|
|
95
|
-
// Extract aliases from schema
|
|
96
|
-
const propAlias = propertySchema.alias;
|
|
97
|
-
if (propAlias) {
|
|
98
|
-
const list = typeof propAlias === 'string' ? [propAlias] : propAlias;
|
|
99
|
-
if (Array.isArray(list)) {
|
|
100
|
-
for (const aliasKey of list) {
|
|
101
|
-
if (typeof aliasKey === 'string' && aliasKey && aliasKey !== propertyName && !(aliasKey in aliases)) {
|
|
102
|
-
aliases[aliasKey] = propertyName;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
} catch {
|
|
110
|
-
// Ignore errors from JSON schema generation
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return { aliases };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function preprocessAliases(data: Record<string, unknown>, aliases: Record<string, string>): Record<string, unknown> {
|
|
117
|
-
const result = { ...data };
|
|
118
|
-
|
|
119
|
-
for (const [aliasKey, fullOptionName] of Object.entries(aliases)) {
|
|
120
|
-
if (aliasKey in data && aliasKey !== fullOptionName) {
|
|
121
|
-
const aliasValue = data[aliasKey];
|
|
122
|
-
// Prefer full option name if it exists
|
|
123
|
-
if (!(fullOptionName in result)) result[fullOptionName] = aliasValue;
|
|
124
|
-
delete result[aliasKey];
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return result;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
interface ParseOptionsContext {
|
|
132
|
-
aliases?: Record<string, string>;
|
|
133
|
-
envData?: Record<string, unknown>;
|
|
134
|
-
configData?: Record<string, unknown>;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Apply values directly to options.
|
|
139
|
-
* CLI values take precedence over the provided values.
|
|
140
|
-
*/
|
|
141
|
-
function applyValues(data: Record<string, unknown>, values: Record<string, unknown>): Record<string, unknown> {
|
|
142
|
-
const result = { ...data };
|
|
143
|
-
|
|
144
|
-
for (const [key, value] of Object.entries(values)) {
|
|
145
|
-
// Only apply value if option wasn't already set
|
|
146
|
-
if (key in result && result[key] !== undefined) continue;
|
|
147
|
-
if (value !== undefined) {
|
|
148
|
-
result[key] = value;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return result;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Combined preprocessing of options with all features.
|
|
157
|
-
* Precedence order (highest to lowest): CLI args > env vars > config file
|
|
158
|
-
*/
|
|
159
|
-
export function preprocessOptions(data: Record<string, unknown>, ctx: ParseOptionsContext): Record<string, unknown> {
|
|
160
|
-
let result = { ...data };
|
|
161
|
-
|
|
162
|
-
// 1. Apply aliases first
|
|
163
|
-
if (ctx.aliases && Object.keys(ctx.aliases).length > 0) {
|
|
164
|
-
result = preprocessAliases(result, ctx.aliases);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// 2. Apply environment variables (higher precedence than config)
|
|
168
|
-
// These only apply if CLI didn't set the option
|
|
169
|
-
if (ctx.envData) {
|
|
170
|
-
result = applyValues(result, ctx.envData);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// 3. Apply config file values (lowest precedence)
|
|
174
|
-
// These only apply if neither CLI nor env set the option
|
|
175
|
-
if (ctx.configData) {
|
|
176
|
-
result = applyValues(result, ctx.configData);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return result;
|
|
180
|
-
}
|