@shaferllc/keel 0.68.0 → 0.77.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/AGENTS.md +2 -0
- package/README.md +15 -5
- package/dist/api/api.config.stub +9 -0
- package/dist/api/config.d.ts +13 -0
- package/dist/api/config.js +14 -0
- package/dist/api/index.d.ts +16 -0
- package/dist/api/index.js +13 -0
- package/dist/api/provider.d.ts +10 -0
- package/dist/api/provider.js +17 -0
- package/dist/api/query.d.ts +35 -0
- package/dist/api/query.js +42 -0
- package/dist/api/resource.d.ts +93 -0
- package/dist/api/resource.js +185 -0
- package/dist/core/application.js +6 -0
- package/dist/core/cache.d.ts +1 -2
- package/dist/core/cache.js +9 -2
- package/dist/core/cli/index.d.ts +28 -0
- package/dist/core/cli/index.js +427 -0
- package/dist/core/cli/stubs.d.ts +14 -0
- package/dist/core/cli/stubs.js +105 -0
- package/dist/core/console-prompt.d.ts +79 -0
- package/dist/core/console-prompt.js +239 -0
- package/dist/core/console-ui.d.ts +96 -0
- package/dist/core/console-ui.js +187 -0
- package/dist/core/console.d.ts +188 -0
- package/dist/core/console.js +395 -0
- package/dist/core/database.d.ts +70 -1
- package/dist/core/database.js +174 -15
- package/dist/core/env.d.ts +96 -0
- package/dist/core/env.js +140 -0
- package/dist/core/health.d.ts +2 -2
- package/dist/core/health.js +2 -2
- package/dist/core/http/kernel.d.ts +2 -0
- package/dist/core/http/kernel.js +48 -0
- package/dist/core/http/router.d.ts +5 -5
- package/dist/core/http/router.js +5 -5
- package/dist/core/i18n.d.ts +162 -0
- package/dist/core/i18n.js +472 -0
- package/dist/core/index.d.ts +25 -4
- package/dist/core/index.js +13 -3
- package/dist/core/instrumentation.d.ts +113 -0
- package/dist/core/instrumentation.js +52 -0
- package/dist/core/logger.d.ts +7 -0
- package/dist/core/logger.js +28 -1
- package/dist/core/notification.js +10 -1
- package/dist/core/package.d.ts +117 -0
- package/dist/core/package.js +169 -0
- package/dist/core/pages.d.ts +108 -0
- package/dist/core/pages.js +199 -0
- package/dist/core/queue.js +26 -5
- package/dist/core/repl.d.ts +33 -0
- package/dist/core/repl.js +88 -0
- package/dist/core/scheduler.js +6 -0
- package/dist/core/social.d.ts +4 -4
- package/dist/core/social.js +4 -4
- package/dist/core/storage.js +15 -3
- package/dist/core/telemetry.d.ts +208 -0
- package/dist/core/telemetry.js +383 -0
- package/dist/core/template.d.ts +2 -3
- package/dist/core/template.js +2 -3
- package/dist/core/testing.d.ts +171 -1
- package/dist/core/testing.js +505 -2
- package/dist/db/d1.js +13 -0
- package/dist/db/pg.d.ts +13 -0
- package/dist/db/pg.js +46 -4
- package/dist/openapi/config.d.ts +28 -0
- package/dist/openapi/config.js +25 -0
- package/dist/openapi/doc.d.ts +40 -0
- package/dist/openapi/doc.js +20 -0
- package/dist/openapi/export.d.ts +8 -0
- package/dist/openapi/export.js +19 -0
- package/dist/openapi/gate.d.ts +15 -0
- package/dist/openapi/gate.js +27 -0
- package/dist/openapi/index.d.ts +19 -0
- package/dist/openapi/index.js +15 -0
- package/dist/openapi/openapi.config.stub +29 -0
- package/dist/openapi/provider.d.ts +18 -0
- package/dist/openapi/provider.js +35 -0
- package/dist/openapi/routes.d.ts +9 -0
- package/dist/openapi/routes.js +23 -0
- package/dist/openapi/spec.d.ts +23 -0
- package/dist/openapi/spec.js +132 -0
- package/dist/openapi/ui.d.ts +8 -0
- package/dist/openapi/ui.js +31 -0
- package/dist/openapi/zod.d.ts +12 -0
- package/dist/openapi/zod.js +46 -0
- package/dist/watch/config.d.ts +33 -0
- package/dist/watch/config.js +38 -0
- package/dist/watch/entry.d.ts +53 -0
- package/dist/watch/entry.js +105 -0
- package/dist/watch/gate.d.ts +20 -0
- package/dist/watch/gate.js +32 -0
- package/dist/watch/index.d.ts +21 -0
- package/dist/watch/index.js +17 -0
- package/dist/watch/migration.d.ts +7 -0
- package/dist/watch/migration.js +20 -0
- package/dist/watch/provider.d.ts +22 -0
- package/dist/watch/provider.js +58 -0
- package/dist/watch/prune.d.ts +11 -0
- package/dist/watch/prune.js +22 -0
- package/dist/watch/recorder.d.ts +24 -0
- package/dist/watch/recorder.js +39 -0
- package/dist/watch/routes.d.ts +13 -0
- package/dist/watch/routes.js +55 -0
- package/dist/watch/store.d.ts +54 -0
- package/dist/watch/store.js +158 -0
- package/dist/watch/ui/dist/watch.css +1 -0
- package/dist/watch/ui/dist/watch.js +555 -0
- package/dist/watch/ui-shell.d.ts +12 -0
- package/dist/watch/ui-shell.js +24 -0
- package/dist/watch/watch.config.stub +47 -0
- package/dist/watch/watchers.d.ts +12 -0
- package/dist/watch/watchers.js +156 -0
- package/docs/ai-manifest.json +737 -3
- package/docs/api-resources.md +119 -0
- package/docs/configuration.md +74 -0
- package/docs/console.md +217 -13
- package/docs/database.md +101 -0
- package/docs/examples/api-resources.ts +86 -0
- package/docs/examples/configuration.ts +40 -0
- package/docs/examples/console.ts +134 -0
- package/docs/examples/database.ts +90 -0
- package/docs/examples/i18n.ts +117 -0
- package/docs/examples/logger.ts +74 -0
- package/docs/examples/mail.ts +103 -0
- package/docs/examples/pages.ts +82 -0
- package/docs/examples/telemetry.ts +127 -0
- package/docs/examples/testing.ts +150 -0
- package/docs/health.md +4 -4
- package/docs/i18n.md +302 -0
- package/docs/logger.md +156 -10
- package/docs/mail.md +73 -0
- package/docs/openapi.md +111 -0
- package/docs/packages.md +118 -0
- package/docs/pages.md +217 -0
- package/docs/storage.md +5 -3
- package/docs/telemetry.md +263 -0
- package/docs/templates.md +3 -4
- package/docs/testing.md +252 -0
- package/docs/watch.md +118 -0
- package/llms-full.txt +2389 -241
- package/llms.txt +13 -1
- package/package.json +32 -6
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The console — a real command system, not a wrapper round `process.argv`.
|
|
3
|
+
*
|
|
4
|
+
* export const greet = defineCommand({
|
|
5
|
+
* name: "greet",
|
|
6
|
+
* description: "Greet someone",
|
|
7
|
+
* args: { name: arg.string({ description: "who to greet" }) },
|
|
8
|
+
* flags: { loud: flag.boolean({ alias: "l", description: "SHOUT IT" }) },
|
|
9
|
+
*
|
|
10
|
+
* async run({ args, flags, ui }) {
|
|
11
|
+
* const message = `Hello, ${args.name}!`;
|
|
12
|
+
* ui.success(flags.loud ? message.toUpperCase() : message);
|
|
13
|
+
* },
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* `args.name` is a `string` and `flags.loud` is a `boolean` — **inferred from the
|
|
17
|
+
* spec**, not cast. Declare an arg optional and its type becomes `string |
|
|
18
|
+
* undefined`; give it a default and it's a `string` again. The types can't drift
|
|
19
|
+
* from the parsing, because the parsing is generated from them.
|
|
20
|
+
*
|
|
21
|
+
* A command gets a `ui` (colors, tables, tasks, spinners) and a `prompt` (ask,
|
|
22
|
+
* confirm, choice…), both of which switch to a captured, colorless "raw" mode
|
|
23
|
+
* under test, so you can assert on what a command printed and script its answers.
|
|
24
|
+
*/
|
|
25
|
+
import type { Ui } from "./console-ui.js";
|
|
26
|
+
import type { Prompt } from "./console-prompt.js";
|
|
27
|
+
export type ArgType = "string" | "number" | "spread";
|
|
28
|
+
export type FlagType = "string" | "number" | "boolean" | "array";
|
|
29
|
+
export interface ArgSpec<T = unknown> {
|
|
30
|
+
readonly kind: "arg";
|
|
31
|
+
readonly type: ArgType;
|
|
32
|
+
readonly description?: string;
|
|
33
|
+
readonly required: boolean;
|
|
34
|
+
readonly default?: T;
|
|
35
|
+
readonly parse?: (raw: string) => T;
|
|
36
|
+
/** @internal — carries the value type. */
|
|
37
|
+
readonly __value?: T;
|
|
38
|
+
}
|
|
39
|
+
export interface FlagSpec<T = unknown> {
|
|
40
|
+
readonly kind: "flag";
|
|
41
|
+
readonly type: FlagType;
|
|
42
|
+
readonly description?: string;
|
|
43
|
+
readonly required: boolean;
|
|
44
|
+
readonly default?: T;
|
|
45
|
+
/** A single-character shorthand: `-f`. */
|
|
46
|
+
readonly alias?: string;
|
|
47
|
+
readonly parse?: (raw: string) => T;
|
|
48
|
+
/** @internal — carries the value type. */
|
|
49
|
+
readonly __value?: T;
|
|
50
|
+
}
|
|
51
|
+
interface BaseOptions {
|
|
52
|
+
description?: string;
|
|
53
|
+
}
|
|
54
|
+
interface ArgOptions<T> extends BaseOptions {
|
|
55
|
+
/** Args are required by default. */
|
|
56
|
+
required?: boolean;
|
|
57
|
+
default?: T;
|
|
58
|
+
parse?: (raw: string) => T;
|
|
59
|
+
}
|
|
60
|
+
interface FlagOptions<T> extends BaseOptions {
|
|
61
|
+
/** Flags are optional by default. */
|
|
62
|
+
required?: boolean;
|
|
63
|
+
default?: T;
|
|
64
|
+
alias?: string;
|
|
65
|
+
parse?: (raw: string) => T;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Whether a spec's value can be absent. Required, or defaulted, means present —
|
|
69
|
+
* which is exactly what makes `args.name` a `string` rather than a
|
|
70
|
+
* `string | undefined` you have to keep checking.
|
|
71
|
+
*/
|
|
72
|
+
type Present<O, Fallback extends boolean> = O extends {
|
|
73
|
+
required: true;
|
|
74
|
+
} ? true : O extends {
|
|
75
|
+
default: unknown;
|
|
76
|
+
} ? true : O extends {
|
|
77
|
+
required: false;
|
|
78
|
+
} ? false : Fallback;
|
|
79
|
+
type ArgValue<O, T> = Present<O, true> extends true ? T : T | undefined;
|
|
80
|
+
type FlagValue<O, T> = Present<O, false> extends true ? T : T | undefined;
|
|
81
|
+
/** Positional arguments: `keel greet <name>`. */
|
|
82
|
+
export declare const arg: {
|
|
83
|
+
string<const O extends ArgOptions<string> = Record<never, never>>(options?: O & ArgOptions<string>): ArgSpec<ArgValue<O, string>>;
|
|
84
|
+
number<const O extends ArgOptions<number> = Record<never, never>>(options?: O & ArgOptions<number>): ArgSpec<ArgValue<O, number>>;
|
|
85
|
+
/** Swallows every remaining value. Must be the last argument. */
|
|
86
|
+
spread<const O extends ArgOptions<string[]> = Record<never, never>>(options?: O & ArgOptions<string[]>): ArgSpec<ArgValue<O, string[]>>;
|
|
87
|
+
};
|
|
88
|
+
/** Named options: `--force`, `-f`, `--name=Ada`. */
|
|
89
|
+
export declare const flag: {
|
|
90
|
+
/** `--force` / `-f`, and `--no-force`. Defaults to `false`, so it's never undefined. */
|
|
91
|
+
boolean<const O extends FlagOptions<boolean> = Record<never, never>>(options?: O & FlagOptions<boolean>): FlagSpec<boolean>;
|
|
92
|
+
string<const O extends FlagOptions<string> = Record<never, never>>(options?: O & FlagOptions<string>): FlagSpec<FlagValue<O, string>>;
|
|
93
|
+
number<const O extends FlagOptions<number> = Record<never, never>>(options?: O & FlagOptions<number>): FlagSpec<FlagValue<O, number>>;
|
|
94
|
+
/** Repeatable: `--tag a --tag b` gives `["a", "b"]`. Defaults to `[]`. */
|
|
95
|
+
array<const O extends FlagOptions<string[]> = Record<never, never>>(options?: O & FlagOptions<string[]>): FlagSpec<string[]>;
|
|
96
|
+
};
|
|
97
|
+
export type ArgsSpec = Record<string, ArgSpec<unknown>>;
|
|
98
|
+
export type FlagsSpec = Record<string, FlagSpec<unknown>>;
|
|
99
|
+
/** The values a spec produces — this is where `args.name: string` comes from. */
|
|
100
|
+
export type Values<S extends Record<string, {
|
|
101
|
+
__value?: unknown;
|
|
102
|
+
}>> = {
|
|
103
|
+
[K in keyof S]: S[K] extends {
|
|
104
|
+
__value?: infer V;
|
|
105
|
+
} ? V : never;
|
|
106
|
+
};
|
|
107
|
+
/** What a command's `run` receives. */
|
|
108
|
+
export interface CommandContext<A extends ArgsSpec, F extends FlagsSpec> {
|
|
109
|
+
args: Values<A>;
|
|
110
|
+
flags: Values<F>;
|
|
111
|
+
/** Everything after `--`, plus any unrecognized flags. */
|
|
112
|
+
rest: string[];
|
|
113
|
+
ui: Ui;
|
|
114
|
+
prompt: Prompt;
|
|
115
|
+
/** The kernel running this command — for `keel help`, or calling a sibling. */
|
|
116
|
+
kernel: ConsoleKernel;
|
|
117
|
+
}
|
|
118
|
+
export interface CommandDefinition<A extends ArgsSpec = ArgsSpec, F extends FlagsSpec = FlagsSpec> {
|
|
119
|
+
/** What the user types. `:` namespaces it — `make:controller`. */
|
|
120
|
+
name: string;
|
|
121
|
+
description?: string;
|
|
122
|
+
/** Extra lines shown under `keel help <name>`. */
|
|
123
|
+
help?: string[];
|
|
124
|
+
/** Other names this command answers to. */
|
|
125
|
+
aliases?: string[];
|
|
126
|
+
args?: A;
|
|
127
|
+
flags?: F;
|
|
128
|
+
/** Don't fail on flags you didn't declare — they land in `rest`. */
|
|
129
|
+
allowUnknownFlags?: boolean;
|
|
130
|
+
/** Return a number to set the exit code. Throwing sets it to 1. */
|
|
131
|
+
run(context: CommandContext<A, F>): void | number | Promise<void | number>;
|
|
132
|
+
}
|
|
133
|
+
/** Any command, whatever its arg/flag types — what the kernel stores. */
|
|
134
|
+
export type AnyCommand = CommandDefinition<ArgsSpec, FlagsSpec>;
|
|
135
|
+
/**
|
|
136
|
+
* Define a command. The `args` and `flags` you declare become the types of
|
|
137
|
+
* `args` and `flags` inside `run` — the whole point of the exercise.
|
|
138
|
+
*/
|
|
139
|
+
export declare function defineCommand<A extends ArgsSpec = Record<never, never>, F extends FlagsSpec = Record<never, never>>(definition: CommandDefinition<A, F>): CommandDefinition<A, F>;
|
|
140
|
+
/** Thrown when the argv doesn't fit the command's spec. */
|
|
141
|
+
export declare class ConsoleError extends Error {
|
|
142
|
+
constructor(message: string);
|
|
143
|
+
}
|
|
144
|
+
export interface Parsed {
|
|
145
|
+
args: Record<string, unknown>;
|
|
146
|
+
flags: Record<string, unknown>;
|
|
147
|
+
rest: string[];
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Turn argv into a command's args and flags.
|
|
151
|
+
*
|
|
152
|
+
* Handles `--flag value`, `--flag=value`, `--no-flag`, `-f value`, bundled short
|
|
153
|
+
* flags (`-abc`), repeatable array flags, a `--` terminator, and a trailing
|
|
154
|
+
* spread argument.
|
|
155
|
+
*/
|
|
156
|
+
export declare function parseArgv(argv: string[], definition: AnyCommand): Parsed;
|
|
157
|
+
export interface ConsoleKernelOptions {
|
|
158
|
+
/** The binary's name, shown in help. Default: `"keel"`. */
|
|
159
|
+
binary?: string;
|
|
160
|
+
ui?: Ui;
|
|
161
|
+
prompt?: Prompt;
|
|
162
|
+
}
|
|
163
|
+
/** Registers commands, parses argv, and runs the right one. */
|
|
164
|
+
export declare class ConsoleKernel {
|
|
165
|
+
private options;
|
|
166
|
+
private commands;
|
|
167
|
+
private aliases;
|
|
168
|
+
constructor(options?: ConsoleKernelOptions);
|
|
169
|
+
get binary(): string;
|
|
170
|
+
/** Register one or more commands. */
|
|
171
|
+
register(...commands: AnyCommand[]): this;
|
|
172
|
+
/** Every registered command, sorted by name. */
|
|
173
|
+
list(): AnyCommand[];
|
|
174
|
+
find(name: string): AnyCommand | undefined;
|
|
175
|
+
private ui;
|
|
176
|
+
private prompt;
|
|
177
|
+
/**
|
|
178
|
+
* Run a command from argv (without the node/script prefix). Returns the exit
|
|
179
|
+
* code: 0 for success, 1 for a failure — never throws, because a console is a
|
|
180
|
+
* bad place to surface a stack trace to a user who mistyped a flag.
|
|
181
|
+
*/
|
|
182
|
+
run(argv: string[]): Promise<number>;
|
|
183
|
+
/** The `keel help` screen: every command, grouped by namespace. */
|
|
184
|
+
help(): string;
|
|
185
|
+
/** The help screen for a single command: its usage, args, and flags. */
|
|
186
|
+
commandHelp(command: AnyCommand): string;
|
|
187
|
+
}
|
|
188
|
+
export {};
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The console — a real command system, not a wrapper round `process.argv`.
|
|
3
|
+
*
|
|
4
|
+
* export const greet = defineCommand({
|
|
5
|
+
* name: "greet",
|
|
6
|
+
* description: "Greet someone",
|
|
7
|
+
* args: { name: arg.string({ description: "who to greet" }) },
|
|
8
|
+
* flags: { loud: flag.boolean({ alias: "l", description: "SHOUT IT" }) },
|
|
9
|
+
*
|
|
10
|
+
* async run({ args, flags, ui }) {
|
|
11
|
+
* const message = `Hello, ${args.name}!`;
|
|
12
|
+
* ui.success(flags.loud ? message.toUpperCase() : message);
|
|
13
|
+
* },
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* `args.name` is a `string` and `flags.loud` is a `boolean` — **inferred from the
|
|
17
|
+
* spec**, not cast. Declare an arg optional and its type becomes `string |
|
|
18
|
+
* undefined`; give it a default and it's a `string` again. The types can't drift
|
|
19
|
+
* from the parsing, because the parsing is generated from them.
|
|
20
|
+
*
|
|
21
|
+
* A command gets a `ui` (colors, tables, tasks, spinners) and a `prompt` (ask,
|
|
22
|
+
* confirm, choice…), both of which switch to a captured, colorless "raw" mode
|
|
23
|
+
* under test, so you can assert on what a command printed and script its answers.
|
|
24
|
+
*/
|
|
25
|
+
/** Positional arguments: `keel greet <name>`. */
|
|
26
|
+
export const arg = {
|
|
27
|
+
string(options) {
|
|
28
|
+
return { kind: "arg", type: "string", required: true, ...options };
|
|
29
|
+
},
|
|
30
|
+
number(options) {
|
|
31
|
+
return { kind: "arg", type: "number", required: true, ...options };
|
|
32
|
+
},
|
|
33
|
+
/** Swallows every remaining value. Must be the last argument. */
|
|
34
|
+
spread(options) {
|
|
35
|
+
return { kind: "arg", type: "spread", required: true, ...options };
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
/** Named options: `--force`, `-f`, `--name=Ada`. */
|
|
39
|
+
export const flag = {
|
|
40
|
+
/** `--force` / `-f`, and `--no-force`. Defaults to `false`, so it's never undefined. */
|
|
41
|
+
boolean(options) {
|
|
42
|
+
return { kind: "flag", type: "boolean", required: false, default: false, ...options };
|
|
43
|
+
},
|
|
44
|
+
string(options) {
|
|
45
|
+
return { kind: "flag", type: "string", required: false, ...options };
|
|
46
|
+
},
|
|
47
|
+
number(options) {
|
|
48
|
+
return { kind: "flag", type: "number", required: false, ...options };
|
|
49
|
+
},
|
|
50
|
+
/** Repeatable: `--tag a --tag b` gives `["a", "b"]`. Defaults to `[]`. */
|
|
51
|
+
array(options) {
|
|
52
|
+
return { kind: "flag", type: "array", required: false, default: [], ...options };
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Define a command. The `args` and `flags` you declare become the types of
|
|
57
|
+
* `args` and `flags` inside `run` — the whole point of the exercise.
|
|
58
|
+
*/
|
|
59
|
+
export function defineCommand(definition) {
|
|
60
|
+
return definition;
|
|
61
|
+
}
|
|
62
|
+
/** Thrown when the argv doesn't fit the command's spec. */
|
|
63
|
+
export class ConsoleError extends Error {
|
|
64
|
+
constructor(message) {
|
|
65
|
+
super(message);
|
|
66
|
+
this.name = "ConsoleError";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/* --------------------------------- parsing -------------------------------- */
|
|
70
|
+
function coerce(spec, raw, label) {
|
|
71
|
+
if (spec.parse)
|
|
72
|
+
return spec.parse(raw);
|
|
73
|
+
if (spec.type === "number") {
|
|
74
|
+
const value = Number(raw);
|
|
75
|
+
if (Number.isNaN(value))
|
|
76
|
+
throw new ConsoleError(`${label} expects a number, got "${raw}".`);
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
return raw;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Turn argv into a command's args and flags.
|
|
83
|
+
*
|
|
84
|
+
* Handles `--flag value`, `--flag=value`, `--no-flag`, `-f value`, bundled short
|
|
85
|
+
* flags (`-abc`), repeatable array flags, a `--` terminator, and a trailing
|
|
86
|
+
* spread argument.
|
|
87
|
+
*/
|
|
88
|
+
export function parseArgv(argv, definition) {
|
|
89
|
+
const argSpecs = Object.entries(definition.args ?? {});
|
|
90
|
+
const flagSpecs = Object.entries(definition.flags ?? {});
|
|
91
|
+
const byAlias = new Map();
|
|
92
|
+
for (const [name, spec] of flagSpecs)
|
|
93
|
+
if (spec.alias)
|
|
94
|
+
byAlias.set(spec.alias, name);
|
|
95
|
+
const flags = {};
|
|
96
|
+
const positional = [];
|
|
97
|
+
const rest = [];
|
|
98
|
+
const specFor = (token) => {
|
|
99
|
+
const name = byAlias.get(token) ?? token;
|
|
100
|
+
const spec = definition.flags?.[name];
|
|
101
|
+
return spec ? [name, spec] : undefined;
|
|
102
|
+
};
|
|
103
|
+
const setFlag = (name, spec, raw) => {
|
|
104
|
+
const label = `--${name}`;
|
|
105
|
+
if (spec.type === "array") {
|
|
106
|
+
const list = flags[name] ?? [];
|
|
107
|
+
list.push(String(coerce(spec, raw, label)));
|
|
108
|
+
flags[name] = list;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
flags[name] = coerce(spec, raw, label);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
for (let i = 0; i < argv.length; i++) {
|
|
115
|
+
const token = argv[i];
|
|
116
|
+
// Everything after `--` is verbatim.
|
|
117
|
+
if (token === "--") {
|
|
118
|
+
rest.push(...argv.slice(i + 1));
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
if (token.startsWith("--")) {
|
|
122
|
+
const body = token.slice(2);
|
|
123
|
+
const eq = body.indexOf("=");
|
|
124
|
+
const name = eq === -1 ? body : body.slice(0, eq);
|
|
125
|
+
const inline = eq === -1 ? undefined : body.slice(eq + 1);
|
|
126
|
+
// `--no-force` turns a boolean off.
|
|
127
|
+
if (name.startsWith("no-")) {
|
|
128
|
+
const found = specFor(name.slice(3));
|
|
129
|
+
if (found && found[1].type === "boolean") {
|
|
130
|
+
flags[found[0]] = false;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const found = specFor(name);
|
|
135
|
+
if (!found) {
|
|
136
|
+
if (definition.allowUnknownFlags) {
|
|
137
|
+
rest.push(token);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
throw new ConsoleError(`Unknown flag "--${name}".`);
|
|
141
|
+
}
|
|
142
|
+
const [key, spec] = found;
|
|
143
|
+
if (spec.type === "boolean") {
|
|
144
|
+
flags[key] = inline === undefined ? true : inline !== "false";
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const value = inline ?? argv[++i];
|
|
148
|
+
if (value === undefined)
|
|
149
|
+
throw new ConsoleError(`Flag "--${key}" expects a value.`);
|
|
150
|
+
setFlag(key, spec, value);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
// `-f`, or bundled `-abc` where only the last may take a value.
|
|
154
|
+
if (token.startsWith("-") && token.length > 1) {
|
|
155
|
+
const letters = [...token.slice(1)];
|
|
156
|
+
for (let j = 0; j < letters.length; j++) {
|
|
157
|
+
const letter = letters[j];
|
|
158
|
+
const found = specFor(letter);
|
|
159
|
+
if (!found) {
|
|
160
|
+
if (definition.allowUnknownFlags) {
|
|
161
|
+
rest.push(`-${letter}`);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
throw new ConsoleError(`Unknown flag "-${letter}".`);
|
|
165
|
+
}
|
|
166
|
+
const [key, spec] = found;
|
|
167
|
+
if (spec.type === "boolean") {
|
|
168
|
+
flags[key] = true;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
// A value-taking flag must be last in the bundle.
|
|
172
|
+
const value = j === letters.length - 1 ? argv[++i] : letters.slice(j + 1).join("");
|
|
173
|
+
if (value === undefined)
|
|
174
|
+
throw new ConsoleError(`Flag "-${letter}" expects a value.`);
|
|
175
|
+
setFlag(key, spec, value);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
positional.push(token);
|
|
181
|
+
}
|
|
182
|
+
// Positionals -> declared args.
|
|
183
|
+
const args = {};
|
|
184
|
+
let cursor = 0;
|
|
185
|
+
for (const [name, spec] of argSpecs) {
|
|
186
|
+
if (spec.type === "spread") {
|
|
187
|
+
const values = positional.slice(cursor);
|
|
188
|
+
cursor = positional.length;
|
|
189
|
+
if (!values.length && spec.required && spec.default === undefined) {
|
|
190
|
+
throw new ConsoleError(`Missing argument "${name}".`);
|
|
191
|
+
}
|
|
192
|
+
args[name] = values.length ? values : (spec.default ?? []);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const raw = positional[cursor++];
|
|
196
|
+
if (raw === undefined) {
|
|
197
|
+
if (spec.default !== undefined) {
|
|
198
|
+
args[name] = spec.default;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (spec.required)
|
|
202
|
+
throw new ConsoleError(`Missing argument "${name}".`);
|
|
203
|
+
args[name] = undefined;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
args[name] = coerce(spec, raw, `Argument "${name}"`);
|
|
207
|
+
}
|
|
208
|
+
// Leftover positionals nobody claimed.
|
|
209
|
+
rest.push(...positional.slice(cursor));
|
|
210
|
+
// Defaults, then required checks.
|
|
211
|
+
for (const [name, spec] of flagSpecs) {
|
|
212
|
+
if (flags[name] === undefined && spec.default !== undefined)
|
|
213
|
+
flags[name] = spec.default;
|
|
214
|
+
if (flags[name] === undefined && spec.required) {
|
|
215
|
+
throw new ConsoleError(`Missing required flag "--${name}".`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return { args, flags, rest };
|
|
219
|
+
}
|
|
220
|
+
/** Registers commands, parses argv, and runs the right one. */
|
|
221
|
+
export class ConsoleKernel {
|
|
222
|
+
options;
|
|
223
|
+
commands = new Map();
|
|
224
|
+
aliases = new Map();
|
|
225
|
+
constructor(options = {}) {
|
|
226
|
+
this.options = options;
|
|
227
|
+
}
|
|
228
|
+
get binary() {
|
|
229
|
+
return this.options.binary ?? "keel";
|
|
230
|
+
}
|
|
231
|
+
/** Register one or more commands. */
|
|
232
|
+
register(...commands) {
|
|
233
|
+
for (const command of commands) {
|
|
234
|
+
this.commands.set(command.name, command);
|
|
235
|
+
for (const alias of command.aliases ?? [])
|
|
236
|
+
this.aliases.set(alias, command.name);
|
|
237
|
+
}
|
|
238
|
+
return this;
|
|
239
|
+
}
|
|
240
|
+
/** Every registered command, sorted by name. */
|
|
241
|
+
list() {
|
|
242
|
+
return [...this.commands.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
243
|
+
}
|
|
244
|
+
find(name) {
|
|
245
|
+
return this.commands.get(name) ?? this.commands.get(this.aliases.get(name) ?? "");
|
|
246
|
+
}
|
|
247
|
+
async ui() {
|
|
248
|
+
if (this.options.ui)
|
|
249
|
+
return this.options.ui;
|
|
250
|
+
const { createUi } = await import("./console-ui.js");
|
|
251
|
+
return createUi();
|
|
252
|
+
}
|
|
253
|
+
async prompt() {
|
|
254
|
+
if (this.options.prompt)
|
|
255
|
+
return this.options.prompt;
|
|
256
|
+
const { createPrompt } = await import("./console-prompt.js");
|
|
257
|
+
return createPrompt();
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Run a command from argv (without the node/script prefix). Returns the exit
|
|
261
|
+
* code: 0 for success, 1 for a failure — never throws, because a console is a
|
|
262
|
+
* bad place to surface a stack trace to a user who mistyped a flag.
|
|
263
|
+
*/
|
|
264
|
+
async run(argv) {
|
|
265
|
+
const ui = await this.ui();
|
|
266
|
+
const [name, ...rest] = argv;
|
|
267
|
+
// No command, or an explicit ask for help: list what's available.
|
|
268
|
+
if (!name || name === "help" || name === "--help" || name === "-h") {
|
|
269
|
+
const target = name === "help" ? rest[0] : undefined;
|
|
270
|
+
if (target) {
|
|
271
|
+
const command = this.find(target);
|
|
272
|
+
if (!command) {
|
|
273
|
+
ui.error(`Unknown command "${target}".`);
|
|
274
|
+
return 1;
|
|
275
|
+
}
|
|
276
|
+
ui.write(this.commandHelp(command));
|
|
277
|
+
return 0;
|
|
278
|
+
}
|
|
279
|
+
ui.write(this.help());
|
|
280
|
+
return 0;
|
|
281
|
+
}
|
|
282
|
+
const command = this.find(name);
|
|
283
|
+
if (!command) {
|
|
284
|
+
ui.error(`Unknown command "${name}". Run "${this.binary} help" to see what's available.`);
|
|
285
|
+
return 1;
|
|
286
|
+
}
|
|
287
|
+
// `--help` on a command shows its own help rather than running it.
|
|
288
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
289
|
+
ui.write(this.commandHelp(command));
|
|
290
|
+
return 0;
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
const parsed = parseArgv(rest, command);
|
|
294
|
+
const prompt = await this.prompt();
|
|
295
|
+
const code = await command.run({
|
|
296
|
+
args: parsed.args,
|
|
297
|
+
flags: parsed.flags,
|
|
298
|
+
rest: parsed.rest,
|
|
299
|
+
ui,
|
|
300
|
+
prompt,
|
|
301
|
+
kernel: this,
|
|
302
|
+
});
|
|
303
|
+
return typeof code === "number" ? code : 0;
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
// A usage error is the user's mistake — say what's wrong and how to fix it,
|
|
307
|
+
// don't dump a stack at them.
|
|
308
|
+
if (error instanceof ConsoleError) {
|
|
309
|
+
ui.error(error.message);
|
|
310
|
+
ui.write("");
|
|
311
|
+
ui.write(this.commandHelp(command));
|
|
312
|
+
return 1;
|
|
313
|
+
}
|
|
314
|
+
ui.error(error instanceof Error ? error.message : String(error));
|
|
315
|
+
if (error instanceof Error && error.stack && process.env.KEEL_DEBUG) {
|
|
316
|
+
ui.write(error.stack);
|
|
317
|
+
}
|
|
318
|
+
return 1;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/** The `keel help` screen: every command, grouped by namespace. */
|
|
322
|
+
help() {
|
|
323
|
+
const lines = [`Usage: ${this.binary} <command> [options]`, ""];
|
|
324
|
+
const groups = new Map();
|
|
325
|
+
for (const command of this.list()) {
|
|
326
|
+
const namespace = command.name.includes(":") ? command.name.split(":")[0] : "";
|
|
327
|
+
const group = groups.get(namespace) ?? [];
|
|
328
|
+
group.push(command);
|
|
329
|
+
groups.set(namespace, group);
|
|
330
|
+
}
|
|
331
|
+
// Ungrouped commands first, then each namespace.
|
|
332
|
+
const order = [...groups.keys()].sort((a, b) => (a === "" ? -1 : b === "" ? 1 : a.localeCompare(b)));
|
|
333
|
+
const width = Math.max(...this.list().map((c) => c.name.length), 0);
|
|
334
|
+
for (const namespace of order) {
|
|
335
|
+
lines.push(namespace ? `${namespace}:` : "Commands:");
|
|
336
|
+
for (const command of groups.get(namespace)) {
|
|
337
|
+
lines.push(` ${command.name.padEnd(width + 2)}${command.description ?? ""}`);
|
|
338
|
+
}
|
|
339
|
+
lines.push("");
|
|
340
|
+
}
|
|
341
|
+
lines.push(`Run "${this.binary} help <command>" for more on one of them.`);
|
|
342
|
+
return lines.join("\n");
|
|
343
|
+
}
|
|
344
|
+
/** The help screen for a single command: its usage, args, and flags. */
|
|
345
|
+
commandHelp(command) {
|
|
346
|
+
const args = Object.entries(command.args ?? {});
|
|
347
|
+
const flags = Object.entries(command.flags ?? {});
|
|
348
|
+
const usage = [`${this.binary} ${command.name}`];
|
|
349
|
+
for (const [name, spec] of args) {
|
|
350
|
+
const label = spec.type === "spread" ? `...${name}` : name;
|
|
351
|
+
usage.push(spec.required && spec.default === undefined ? `<${label}>` : `[${label}]`);
|
|
352
|
+
}
|
|
353
|
+
if (flags.length)
|
|
354
|
+
usage.push("[options]");
|
|
355
|
+
const lines = [];
|
|
356
|
+
if (command.description)
|
|
357
|
+
lines.push(command.description, "");
|
|
358
|
+
lines.push(`Usage: ${usage.join(" ")}`, "");
|
|
359
|
+
if (args.length) {
|
|
360
|
+
lines.push("Arguments:");
|
|
361
|
+
const width = Math.max(...args.map(([n]) => n.length));
|
|
362
|
+
for (const [name, spec] of args) {
|
|
363
|
+
const meta = [];
|
|
364
|
+
if (!spec.required || spec.default !== undefined)
|
|
365
|
+
meta.push("optional");
|
|
366
|
+
if (spec.default !== undefined)
|
|
367
|
+
meta.push(`default: ${JSON.stringify(spec.default)}`);
|
|
368
|
+
const suffix = meta.length ? ` (${meta.join(", ")})` : "";
|
|
369
|
+
lines.push(` ${name.padEnd(width + 2)}${spec.description ?? ""}${suffix}`);
|
|
370
|
+
}
|
|
371
|
+
lines.push("");
|
|
372
|
+
}
|
|
373
|
+
if (flags.length) {
|
|
374
|
+
lines.push("Options:");
|
|
375
|
+
const labels = flags.map(([name, spec]) => (spec.alias ? `-${spec.alias}, --${name}` : ` --${name}`));
|
|
376
|
+
const width = Math.max(...labels.map((l) => l.length));
|
|
377
|
+
flags.forEach(([, spec], i) => {
|
|
378
|
+
const meta = [];
|
|
379
|
+
if (spec.required)
|
|
380
|
+
meta.push("required");
|
|
381
|
+
if (spec.default !== undefined && spec.type !== "boolean" && spec.type !== "array") {
|
|
382
|
+
meta.push(`default: ${JSON.stringify(spec.default)}`);
|
|
383
|
+
}
|
|
384
|
+
const suffix = meta.length ? ` (${meta.join(", ")})` : "";
|
|
385
|
+
lines.push(` ${labels[i].padEnd(width + 2)}${spec.description ?? ""}${suffix}`);
|
|
386
|
+
});
|
|
387
|
+
lines.push("");
|
|
388
|
+
}
|
|
389
|
+
if (command.aliases?.length)
|
|
390
|
+
lines.push(`Aliases: ${command.aliases.join(", ")}`, "");
|
|
391
|
+
if (command.help?.length)
|
|
392
|
+
lines.push(...command.help, "");
|
|
393
|
+
return lines.join("\n").replace(/\n+$/, "");
|
|
394
|
+
}
|
|
395
|
+
}
|
package/dist/core/database.d.ts
CHANGED
|
@@ -28,11 +28,32 @@ export interface Connection {
|
|
|
28
28
|
select(sql: string, bindings: unknown[]): Promise<Row[]>;
|
|
29
29
|
/** Run an INSERT/UPDATE/DELETE and return write metadata. */
|
|
30
30
|
write(sql: string, bindings: unknown[]): Promise<WriteResult>;
|
|
31
|
+
/**
|
|
32
|
+
* Start a transaction on a **dedicated** connection.
|
|
33
|
+
*
|
|
34
|
+
* This is optional, but it is not optional for a *pooled* driver, and getting
|
|
35
|
+
* that wrong is the classic way to ship a transaction that silently isn't one:
|
|
36
|
+
* a pool hands each statement to whichever connection is free, so a `BEGIN`
|
|
37
|
+
* and the `INSERT` after it can land on different connections. The `BEGIN`
|
|
38
|
+
* then wraps nothing, the `COMMIT` commits nothing, and a failure half-writes.
|
|
39
|
+
*
|
|
40
|
+
* A driver that pools MUST implement this by checking out one connection and
|
|
41
|
+
* running the whole transaction on it. A driver that owns a single connection
|
|
42
|
+
* (SQLite, libSQL, a bare `pg.Client`) can leave it out, and Keel falls back to
|
|
43
|
+
* issuing `BEGIN` / `COMMIT` / `ROLLBACK` on the connection it has.
|
|
44
|
+
*/
|
|
45
|
+
begin?(): Promise<TransactionConnection>;
|
|
46
|
+
}
|
|
47
|
+
/** A connection with an open transaction on it. */
|
|
48
|
+
export interface TransactionConnection extends Connection {
|
|
49
|
+
commit(): Promise<void>;
|
|
50
|
+
rollback(): Promise<void>;
|
|
31
51
|
}
|
|
32
52
|
export type Dialect = "sqlite" | "mysql" | "postgres";
|
|
33
53
|
export type Operator = "=" | "!=" | "<" | "<=" | ">" | ">=" | "like";
|
|
34
|
-
/** A registered connection — the driver bridge
|
|
54
|
+
/** A registered connection — the driver bridge, its name, and its SQL dialect. */
|
|
35
55
|
interface Source {
|
|
56
|
+
name: string;
|
|
36
57
|
conn: Connection;
|
|
37
58
|
dialect: Dialect;
|
|
38
59
|
}
|
|
@@ -124,5 +145,53 @@ export interface ConnectionHandle {
|
|
|
124
145
|
* const reporting = connection("reporting");
|
|
125
146
|
* await reporting.table("events").where("kind", "signup").count();
|
|
126
147
|
*/
|
|
148
|
+
/** A handle to the transaction in flight, handed to `transaction()`'s callback. */
|
|
149
|
+
export interface TransactionHandle extends ConnectionHandle {
|
|
150
|
+
/**
|
|
151
|
+
* Roll back now and abandon the rest of the transaction. The callback should
|
|
152
|
+
* return straight after — queries on a rolled-back transaction will fail.
|
|
153
|
+
*/
|
|
154
|
+
rollback(): Promise<void>;
|
|
155
|
+
/** 0 for the outermost transaction; deeper values are savepoints. */
|
|
156
|
+
readonly depth: number;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Run `fn` inside a database transaction. It commits when `fn` returns, and
|
|
160
|
+
* **rolls back if `fn` throws** — which is the entire point: two related writes
|
|
161
|
+
* either both land or neither does, so a failure between them can't leave the row
|
|
162
|
+
* charged and the order missing.
|
|
163
|
+
*
|
|
164
|
+
* await transaction(async () => {
|
|
165
|
+
* await db("orders").insert(order);
|
|
166
|
+
* await db("stock").where("id", id).decrement("count"); // a throw here undoes the insert
|
|
167
|
+
* });
|
|
168
|
+
*
|
|
169
|
+
* Queries inside are **ambient**: `db()`, models, and relations all pick up the
|
|
170
|
+
* transaction's connection without being handed it. The callback also gets an
|
|
171
|
+
* explicit handle if you'd rather be obvious about it:
|
|
172
|
+
*
|
|
173
|
+
* await transaction(async (tx) => {
|
|
174
|
+
* await tx.table("orders").insert(order);
|
|
175
|
+
* });
|
|
176
|
+
*
|
|
177
|
+
* **Nesting uses savepoints.** A `transaction()` inside another doesn't open a
|
|
178
|
+
* second transaction — databases don't have those — it takes a savepoint, so an
|
|
179
|
+
* inner failure rolls back only the inner work and the outer transaction carries
|
|
180
|
+
* on. Without that, a nested helper's failure would silently abandon its caller's
|
|
181
|
+
* writes too.
|
|
182
|
+
*/
|
|
183
|
+
export declare function transaction<T>(fn: (tx: TransactionHandle) => Promise<T> | T, connectionName?: string): Promise<T>;
|
|
184
|
+
/** Whether a transaction is open on this connection in the current context. */
|
|
185
|
+
export declare function inTransaction(connectionName?: string): boolean;
|
|
127
186
|
export declare function connection(name?: string): ConnectionHandle;
|
|
187
|
+
/**
|
|
188
|
+
* The raw driver bridge and dialect for a connection (or the default) — what the
|
|
189
|
+
* `Migrator` needs. Unlike `connection()`, this hands back the unadjusted
|
|
190
|
+
* `Connection` so the migrator can apply its own placeholder conversion.
|
|
191
|
+
*/
|
|
192
|
+
export declare function getConnection(name?: string): {
|
|
193
|
+
connection: Connection;
|
|
194
|
+
dialect: Dialect;
|
|
195
|
+
name: string;
|
|
196
|
+
};
|
|
128
197
|
export {};
|