@warlock.js/core 4.11.0 → 4.12.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 +84 -0
- package/esm/cli/cli-commands.manager.mjs +28 -5
- package/esm/cli/cli-commands.manager.mjs.map +1 -1
- package/esm/cli/commands/add.command.mjs +4 -2
- package/esm/cli/commands/add.command.mjs.map +1 -1
- package/esm/cli/commands/generate/generate.command.mjs +42 -21
- package/esm/cli/commands/generate/generate.command.mjs.map +1 -1
- package/esm/cli/commands/migrate.command.mjs +6 -2
- package/esm/cli/commands/migrate.command.mjs.map +1 -1
- package/esm/cli/parse-cli-args.mjs +86 -9
- package/esm/cli/parse-cli-args.mjs.map +1 -1
- package/esm/database/migrate-action.mjs +53 -1
- package/esm/database/migrate-action.mjs.map +1 -1
- package/esm/database/pending-exit-code.mjs +39 -0
- package/esm/database/pending-exit-code.mjs.map +1 -0
- package/esm/database/resolve-pending-migrations.mjs +49 -0
- package/esm/database/resolve-pending-migrations.mjs.map +1 -0
- package/esm/image/image.d.mts +3 -2
- package/esm/image/image.d.mts.map +1 -1
- package/esm/image/image.mjs +63 -17
- package/esm/image/image.mjs.map +1 -1
- package/esm/mail/mailer-pool.d.mts.map +1 -1
- package/esm/mail/mailer-pool.mjs.map +1 -1
- package/esm/react/index.d.mts +4 -0
- package/esm/react/index.d.mts.map +1 -1
- package/esm/react/index.mjs +88 -14
- package/esm/react/index.mjs.map +1 -1
- package/package.json +12 -12
- package/skills/process-image/SKILL.md +3 -1
- package/skills/write-cli-command/SKILL.md +19 -1
|
@@ -2,22 +2,93 @@ import { toCamelCase } from "@mongez/reinforcements";
|
|
|
2
2
|
|
|
3
3
|
//#region ../core/src/cli/parse-cli-args.ts
|
|
4
4
|
/**
|
|
5
|
+
* Thrown when a declared boolean option is given a value that is neither
|
|
6
|
+
* true-ish nor false-ish.
|
|
7
|
+
*
|
|
8
|
+
* We refuse to guess. Guessing is exactly what produced #21: `--rollback=false`
|
|
9
|
+
* was kept as the truthy string `"false"` and dropped every table. An option
|
|
10
|
+
* that gates a destructive action must fail loudly on input it cannot read.
|
|
11
|
+
*/
|
|
12
|
+
var CliOptionValueError = class extends Error {
|
|
13
|
+
constructor(optionToken, value) {
|
|
14
|
+
super(`Invalid value ${JSON.stringify(value)} for boolean option ${optionToken}. Expected one of: true, false, 1, 0, yes, no — or pass ${optionToken} on its own for true.`);
|
|
15
|
+
this.optionToken = optionToken;
|
|
16
|
+
this.value = value;
|
|
17
|
+
this.name = "CliOptionValueError";
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const TRUE_VALUES = new Set([
|
|
21
|
+
"true",
|
|
22
|
+
"1",
|
|
23
|
+
"yes"
|
|
24
|
+
]);
|
|
25
|
+
const FALSE_VALUES = new Set([
|
|
26
|
+
"false",
|
|
27
|
+
"0",
|
|
28
|
+
"no"
|
|
29
|
+
]);
|
|
30
|
+
/**
|
|
31
|
+
* Collect the camelCased keys (names AND aliases) of every option the command
|
|
32
|
+
* declared as `type: "boolean"`.
|
|
33
|
+
*
|
|
34
|
+
* Both sides are camelCased because declared names keep their raw kebab form
|
|
35
|
+
* (`"pending-only"`) while the parser emits camelCase keys (`pendingOnly`).
|
|
36
|
+
*/
|
|
37
|
+
function collectBooleanKeys(schema) {
|
|
38
|
+
const keys = /* @__PURE__ */ new Set();
|
|
39
|
+
for (const option of schema) {
|
|
40
|
+
if (option.type !== "boolean") continue;
|
|
41
|
+
if (option.name) keys.add(toCamelCase(option.name));
|
|
42
|
+
if (option.alias) keys.add(toCamelCase(option.alias));
|
|
43
|
+
}
|
|
44
|
+
return keys;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Turn the value written after `=` into a real boolean, or throw.
|
|
48
|
+
*/
|
|
49
|
+
function toBoolean(optionToken, value) {
|
|
50
|
+
const normalized = value.toLowerCase();
|
|
51
|
+
if (TRUE_VALUES.has(normalized)) return true;
|
|
52
|
+
if (FALSE_VALUES.has(normalized)) return false;
|
|
53
|
+
throw new CliOptionValueError(optionToken, value);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
5
56
|
* Parse CLI arguments from process.argv
|
|
6
57
|
*
|
|
58
|
+
* Called with a `schema` (the resolved command's declared options), every
|
|
59
|
+
* option declared `type: "boolean"` is resolved as a boolean:
|
|
60
|
+
* `--flag` → true, `--flag=false|0|no` → false, `--flag=true|1|yes` → true,
|
|
61
|
+
* and `--flag <token>` leaves `<token>` as a POSITIONAL instead of swallowing
|
|
62
|
+
* it as the flag's value. Anything else throws {@link CliOptionValueError}.
|
|
63
|
+
*
|
|
64
|
+
* Called without a schema — which is how the command name is discovered,
|
|
65
|
+
* before any command object exists — behaviour is unchanged: every value stays
|
|
66
|
+
* a raw string and a bare `--flag` still consumes the following non-dash token.
|
|
67
|
+
* Nothing is blanket-coerced here on purpose: `--name=false` on a string-typed
|
|
68
|
+
* option must survive as the string `"false"`.
|
|
69
|
+
*
|
|
70
|
+
* Options the schema does not declare keep the schema-less behaviour.
|
|
71
|
+
*
|
|
7
72
|
* @example
|
|
8
73
|
* parseCliArgs(["node", "warlock", "migrate", "--rollback", "file.ts"])
|
|
9
74
|
* // Returns: { name: "migrate", args: [], options: { rollback: "file.ts" } }
|
|
10
75
|
*
|
|
11
76
|
* @example
|
|
77
|
+
* // `rollback` declared as type: "boolean" — the file stays a positional
|
|
78
|
+
* parseCliArgs(["node", "warlock", "migrate", "--rollback", "file.ts"], migrateOptions)
|
|
79
|
+
* // Returns: { name: "migrate", args: ["file.ts"], options: { rollback: true } }
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
12
82
|
* parseCliArgs(["node", "warlock", "dev", "--port=3000", "--fresh"])
|
|
13
83
|
* // Returns: { name: "dev", args: [], options: { port: "3000", fresh: true } }
|
|
14
84
|
*/
|
|
15
|
-
function parseCliArgs(argv) {
|
|
85
|
+
function parseCliArgs(argv, schema = []) {
|
|
16
86
|
const potentialCommand = argv[2] || "";
|
|
17
87
|
const isFirstArgOption = potentialCommand.startsWith("-");
|
|
18
88
|
const command = isFirstArgOption ? "" : potentialCommand;
|
|
19
89
|
const args = [];
|
|
20
90
|
const options = {};
|
|
91
|
+
const booleanKeys = collectBooleanKeys(schema);
|
|
21
92
|
const startIndex = isFirstArgOption ? 2 : 3;
|
|
22
93
|
for (let i = startIndex; i < argv.length; i++) {
|
|
23
94
|
const arg = argv[i];
|
|
@@ -26,11 +97,13 @@ function parseCliArgs(argv) {
|
|
|
26
97
|
const equalIndex = withoutDashes.indexOf("=");
|
|
27
98
|
if (equalIndex !== -1) {
|
|
28
99
|
const key = toCamelCase(withoutDashes.slice(0, equalIndex));
|
|
29
|
-
|
|
100
|
+
const value = withoutDashes.slice(equalIndex + 1);
|
|
101
|
+
options[key] = booleanKeys.has(key) ? toBoolean(`--${withoutDashes.slice(0, equalIndex)}`, value) : value;
|
|
30
102
|
} else {
|
|
31
103
|
const key = toCamelCase(withoutDashes);
|
|
32
104
|
const nextArg = argv[i + 1];
|
|
33
|
-
if (
|
|
105
|
+
if (booleanKeys.has(key)) options[key] = true;
|
|
106
|
+
else if (nextArg && !nextArg.startsWith("-")) {
|
|
34
107
|
options[key] = nextArg;
|
|
35
108
|
i++;
|
|
36
109
|
} else options[key] = true;
|
|
@@ -39,14 +112,18 @@ function parseCliArgs(argv) {
|
|
|
39
112
|
const flags = arg.slice(1);
|
|
40
113
|
const equalIndex = flags.indexOf("=");
|
|
41
114
|
if (equalIndex !== -1) {
|
|
42
|
-
const
|
|
43
|
-
|
|
115
|
+
const rawKey = flags.slice(0, equalIndex);
|
|
116
|
+
const key = toCamelCase(rawKey);
|
|
117
|
+
const value = flags.slice(equalIndex + 1);
|
|
118
|
+
options[key] = booleanKeys.has(key) ? toBoolean(`-${rawKey}`, value) : value;
|
|
44
119
|
} else if (flags.length === 1) {
|
|
120
|
+
const key = toCamelCase(flags);
|
|
45
121
|
const nextArg = argv[i + 1];
|
|
46
|
-
if (
|
|
47
|
-
|
|
122
|
+
if (booleanKeys.has(key)) options[key] = true;
|
|
123
|
+
else if (nextArg && !nextArg.startsWith("-")) {
|
|
124
|
+
options[key] = nextArg;
|
|
48
125
|
i++;
|
|
49
|
-
} else options[
|
|
126
|
+
} else options[key] = true;
|
|
50
127
|
} else for (const flag of flags) options[toCamelCase(flag)] = true;
|
|
51
128
|
} else args.push(arg);
|
|
52
129
|
}
|
|
@@ -58,5 +135,5 @@ function parseCliArgs(argv) {
|
|
|
58
135
|
}
|
|
59
136
|
|
|
60
137
|
//#endregion
|
|
61
|
-
export { parseCliArgs };
|
|
138
|
+
export { CliOptionValueError, parseCliArgs };
|
|
62
139
|
//# sourceMappingURL=parse-cli-args.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse-cli-args.mjs","names":[],"sources":["../../../../../../../core/src/cli/parse-cli-args.ts"],"sourcesContent":["import { toCamelCase } from \"@mongez/reinforcements\";\n\nexport type ParsedCliArgs = {\n name: string;\n args: string[];\n options: Record<string, string | boolean>;\n};\n\n/**\n * Parse CLI arguments from process.argv\n *\n * @example\n * parseCliArgs([\"node\", \"warlock\", \"migrate\", \"--rollback\", \"file.ts\"])\n * // Returns: { name: \"migrate\", args: [], options: { rollback: \"file.ts\" } }\n *\n * @example\n * parseCliArgs([\"node\", \"warlock\", \"dev\", \"--port=3000\", \"--fresh\"])\n * // Returns: { name: \"dev\", args: [], options: { port: \"3000\", fresh: true } }\n */\nexport function parseCliArgs(argv: string[]): ParsedCliArgs {\n // Command is at index 2 (after \"node\" and script path)\n // But if index 2 starts with \"-\", it's an option, not a command\n const potentialCommand = argv[2] || \"\";\n const isFirstArgOption = potentialCommand.startsWith(\"-\");\n const command = isFirstArgOption ? \"\" : potentialCommand;\n const args: string[] = [];\n const options: Record<string, string | boolean> = {};\n\n // Parse arguments starting from index 3 (or 2 if first arg was an option)\n const startIndex = isFirstArgOption ? 2 : 3;\n for (let i = startIndex; i < argv.length; i++) {\n const arg = argv[i];\n\n if (arg.startsWith(\"--\")) {\n // Long option: --key or --key=value\n const withoutDashes = arg.slice(2);\n const equalIndex = withoutDashes.indexOf(\"=\");\n\n if (equalIndex !== -1) {\n // --key=value format\n const key = toCamelCase(withoutDashes.slice(0, equalIndex));\n const value = withoutDashes.slice(equalIndex + 1);\n options[key] = value;\n } else {\n // --key format (check if next arg is a value)\n const key = toCamelCase(withoutDashes);\n const nextArg = argv[i + 1];\n\n //
|
|
1
|
+
{"version":3,"file":"parse-cli-args.mjs","names":[],"sources":["../../../../../../../core/src/cli/parse-cli-args.ts"],"sourcesContent":["import { toCamelCase } from \"@mongez/reinforcements\";\n\nexport type ParsedCliArgs = {\n name: string;\n args: string[];\n options: Record<string, string | boolean>;\n};\n\n/**\n * The slice of a declared command option this parser needs.\n *\n * `CLICommand.commandOptions` (`ResolvedCLICommandOption[]`) satisfies it, but\n * the parser stays independent of the command classes so it can be unit-tested\n * and reused with a hand-written schema.\n */\nexport type CliOptionSchema = {\n name?: string;\n alias?: string;\n type?: \"string\" | \"boolean\" | \"number\";\n};\n\n/**\n * Thrown when a declared boolean option is given a value that is neither\n * true-ish nor false-ish.\n *\n * We refuse to guess. Guessing is exactly what produced #21: `--rollback=false`\n * was kept as the truthy string `\"false\"` and dropped every table. An option\n * that gates a destructive action must fail loudly on input it cannot read.\n */\nexport class CliOptionValueError extends Error {\n public constructor(\n public readonly optionToken: string,\n public readonly value: string,\n ) {\n super(\n `Invalid value ${JSON.stringify(value)} for boolean option ${optionToken}. ` +\n `Expected one of: true, false, 1, 0, yes, no — or pass ${optionToken} on its own for true.`,\n );\n\n this.name = \"CliOptionValueError\";\n }\n}\n\nconst TRUE_VALUES = new Set([\"true\", \"1\", \"yes\"]);\nconst FALSE_VALUES = new Set([\"false\", \"0\", \"no\"]);\n\n/**\n * Collect the camelCased keys (names AND aliases) of every option the command\n * declared as `type: \"boolean\"`.\n *\n * Both sides are camelCased because declared names keep their raw kebab form\n * (`\"pending-only\"`) while the parser emits camelCase keys (`pendingOnly`).\n */\nfunction collectBooleanKeys(schema: CliOptionSchema[]): Set<string> {\n const keys = new Set<string>();\n\n for (const option of schema) {\n if (option.type !== \"boolean\") continue;\n\n if (option.name) {\n keys.add(toCamelCase(option.name));\n }\n\n if (option.alias) {\n keys.add(toCamelCase(option.alias));\n }\n }\n\n return keys;\n}\n\n/**\n * Turn the value written after `=` into a real boolean, or throw.\n */\nfunction toBoolean(optionToken: string, value: string): boolean {\n const normalized = value.toLowerCase();\n\n if (TRUE_VALUES.has(normalized)) return true;\n if (FALSE_VALUES.has(normalized)) return false;\n\n throw new CliOptionValueError(optionToken, value);\n}\n\n/**\n * Parse CLI arguments from process.argv\n *\n * Called with a `schema` (the resolved command's declared options), every\n * option declared `type: \"boolean\"` is resolved as a boolean:\n * `--flag` → true, `--flag=false|0|no` → false, `--flag=true|1|yes` → true,\n * and `--flag <token>` leaves `<token>` as a POSITIONAL instead of swallowing\n * it as the flag's value. Anything else throws {@link CliOptionValueError}.\n *\n * Called without a schema — which is how the command name is discovered,\n * before any command object exists — behaviour is unchanged: every value stays\n * a raw string and a bare `--flag` still consumes the following non-dash token.\n * Nothing is blanket-coerced here on purpose: `--name=false` on a string-typed\n * option must survive as the string `\"false\"`.\n *\n * Options the schema does not declare keep the schema-less behaviour.\n *\n * @example\n * parseCliArgs([\"node\", \"warlock\", \"migrate\", \"--rollback\", \"file.ts\"])\n * // Returns: { name: \"migrate\", args: [], options: { rollback: \"file.ts\" } }\n *\n * @example\n * // `rollback` declared as type: \"boolean\" — the file stays a positional\n * parseCliArgs([\"node\", \"warlock\", \"migrate\", \"--rollback\", \"file.ts\"], migrateOptions)\n * // Returns: { name: \"migrate\", args: [\"file.ts\"], options: { rollback: true } }\n *\n * @example\n * parseCliArgs([\"node\", \"warlock\", \"dev\", \"--port=3000\", \"--fresh\"])\n * // Returns: { name: \"dev\", args: [], options: { port: \"3000\", fresh: true } }\n */\nexport function parseCliArgs(argv: string[], schema: CliOptionSchema[] = []): ParsedCliArgs {\n // Command is at index 2 (after \"node\" and script path)\n // But if index 2 starts with \"-\", it's an option, not a command\n const potentialCommand = argv[2] || \"\";\n const isFirstArgOption = potentialCommand.startsWith(\"-\");\n const command = isFirstArgOption ? \"\" : potentialCommand;\n const args: string[] = [];\n const options: Record<string, string | boolean> = {};\n const booleanKeys = collectBooleanKeys(schema);\n\n // Parse arguments starting from index 3 (or 2 if first arg was an option)\n const startIndex = isFirstArgOption ? 2 : 3;\n for (let i = startIndex; i < argv.length; i++) {\n const arg = argv[i];\n\n if (arg.startsWith(\"--\")) {\n // Long option: --key or --key=value\n const withoutDashes = arg.slice(2);\n const equalIndex = withoutDashes.indexOf(\"=\");\n\n if (equalIndex !== -1) {\n // --key=value format\n const key = toCamelCase(withoutDashes.slice(0, equalIndex));\n const value = withoutDashes.slice(equalIndex + 1);\n options[key] = booleanKeys.has(key)\n ? toBoolean(`--${withoutDashes.slice(0, equalIndex)}`, value)\n : value;\n } else {\n // --key format (check if next arg is a value)\n const key = toCamelCase(withoutDashes);\n const nextArg = argv[i + 1];\n\n // A declared boolean NEVER consumes the next token: `--rollback\n // users.ts` means \"rollback, and here is a positional\", not \"rollback\n // the file users.ts\". Swallowing it silently discarded the operator's\n // only stated scope and rolled back everything.\n if (booleanKeys.has(key)) {\n options[key] = true;\n } else if (nextArg && !nextArg.startsWith(\"-\")) {\n // If next arg exists and doesn't start with -, treat it as value\n options[key] = nextArg;\n i++; // Skip next arg\n } else {\n options[key] = true;\n }\n }\n } else if (arg.startsWith(\"-\") && arg.length > 1) {\n // Short option: -f, -t=value, or -abc (multiple flags)\n const flags = arg.slice(1);\n const equalIndex = flags.indexOf(\"=\");\n\n if (equalIndex !== -1) {\n // -t=value format\n const rawKey = flags.slice(0, equalIndex);\n const key = toCamelCase(rawKey);\n const value = flags.slice(equalIndex + 1);\n options[key] = booleanKeys.has(key) ? toBoolean(`-${rawKey}`, value) : value;\n } else if (flags.length === 1) {\n // Single flag: -f\n const key = toCamelCase(flags);\n const nextArg = argv[i + 1];\n\n if (booleanKeys.has(key)) {\n options[key] = true;\n } else if (nextArg && !nextArg.startsWith(\"-\")) {\n options[key] = nextArg;\n i++; // Skip next arg\n } else {\n options[key] = true;\n }\n } else {\n // Multiple flags: -abc becomes { a: true, b: true, c: true }\n // A bundle carries no value and never consumes the next token, so a\n // declared boolean already resolves to `true` here.\n for (const flag of flags) {\n options[toCamelCase(flag)] = true;\n }\n }\n } else {\n // Positional argument\n args.push(arg);\n }\n }\n\n return { name: command, args, options };\n}\n"],"mappings":"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"}
|
|
@@ -4,13 +4,30 @@ import { Path } from "../dev-server/path.mjs";
|
|
|
4
4
|
import { warlockConfigManager } from "../warlock-config/warlock-config.manager.mjs";
|
|
5
5
|
import { getFilesFromDirectory } from "../dev-server/utils.mjs";
|
|
6
6
|
import { filesOrchestrator } from "../dev-server/files-orchestrator.mjs";
|
|
7
|
+
import { PENDING_EXIT_CODE, exitCodeFor } from "./pending-exit-code.mjs";
|
|
8
|
+
import { resolvePendingMigrations } from "./resolve-pending-migrations.mjs";
|
|
7
9
|
import { exportMigrationsSQL, freshMigrate, listExecutedMigrations, migrationRunner, rollbackMigrations, runMigrations } from "@warlock.js/cascade";
|
|
8
10
|
import path from "path";
|
|
9
11
|
import { colors } from "@mongez/copper";
|
|
10
12
|
import dayjs from "dayjs";
|
|
11
13
|
|
|
12
14
|
//#region ../core/src/database/migrate-action.ts
|
|
15
|
+
/**
|
|
16
|
+
* `--list` — print the migration state: what has run, then what will run next.
|
|
17
|
+
*
|
|
18
|
+
* The executed section is printed FIRST and unconditionally, before anything
|
|
19
|
+
* touches disk or config. That ordering is the contract: reading migrations
|
|
20
|
+
* from the table cannot fail because of a broken migration file, and `--list`
|
|
21
|
+
* is the command an operator reaches for *during* an incident. The pending
|
|
22
|
+
* section is best-effort on top of an answer that is already on screen.
|
|
23
|
+
*
|
|
24
|
+
* Always exits 0. It is a report; `--pending` is the gate.
|
|
25
|
+
*/
|
|
13
26
|
async function listMigrationsAction() {
|
|
27
|
+
await printExecutedMigrations();
|
|
28
|
+
printPendingMigrations(await resolvePendingMigrations(loadAllMigrations));
|
|
29
|
+
}
|
|
30
|
+
async function printExecutedMigrations() {
|
|
14
31
|
const createdMigrations = await listExecutedMigrations();
|
|
15
32
|
console.log(`\nTotal Executed Migrations: ${colors.green(createdMigrations.length)}\n`);
|
|
16
33
|
if (createdMigrations.length === 0) {
|
|
@@ -26,6 +43,40 @@ async function listMigrationsAction() {
|
|
|
26
43
|
console.log("");
|
|
27
44
|
}
|
|
28
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Print the pending section shared by `--list` and `--pending`.
|
|
48
|
+
*
|
|
49
|
+
* An unavailable result prints an explicit line and NEVER a count. "0 pending"
|
|
50
|
+
* over a tree we failed to read is the one output this whole feature exists to
|
|
51
|
+
* prevent.
|
|
52
|
+
*/
|
|
53
|
+
function printPendingMigrations(result) {
|
|
54
|
+
if (result.type === "unavailable") {
|
|
55
|
+
console.log(`${colors.yellowBright("Pending: unavailable")} — ${result.reason}\n` + colors.gray(" The executed list above is still accurate.\n"));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const { migrations } = result;
|
|
59
|
+
console.log(`Total Pending Migrations: ${colors.green(migrations.length)}\n`);
|
|
60
|
+
if (migrations.length === 0) {
|
|
61
|
+
console.log(colors.gray(" Nothing pending — the database is up to date.\n"));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
migrations.forEach((migration, index) => {
|
|
65
|
+
console.log(` ${colors.gray(`${index + 1}.`)} ${colors.cyanBright(migration.name)}`);
|
|
66
|
+
});
|
|
67
|
+
console.log("");
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* `--pending` — the scriptable half. Same computation as `--list`'s pending
|
|
71
|
+
* section, different exit contract: see {@link PENDING_EXIT_CODE}.
|
|
72
|
+
*/
|
|
73
|
+
async function pendingMigrationsAction() {
|
|
74
|
+
const result = await resolvePendingMigrations(loadAllMigrations);
|
|
75
|
+
printPendingMigrations(result);
|
|
76
|
+
const exitCode = exitCodeFor(result);
|
|
77
|
+
if (exitCode === PENDING_EXIT_CODE.unavailable) console.error(colors.redBright("Could not determine the pending set — exiting 2 rather than reporting 0."));
|
|
78
|
+
process.exit(exitCode);
|
|
79
|
+
}
|
|
29
80
|
async function allMigrationsFilesAction() {
|
|
30
81
|
const files = (await migrationFiles()).map((path) => Path.toRelative(path));
|
|
31
82
|
console.log(`Total Migration Files: ${colors.green(files.length)}`);
|
|
@@ -37,8 +88,9 @@ async function allMigrationsFilesAction() {
|
|
|
37
88
|
* If rollback is provided, then run the migration runner against all files in reverse order
|
|
38
89
|
*/
|
|
39
90
|
async function migrateAction(options) {
|
|
40
|
-
const { fresh, path, rollback, all, list, sql, pendingOnly, compact } = options.options;
|
|
91
|
+
const { fresh, path, rollback, all, list, pending, sql, pendingOnly, compact } = options.options;
|
|
41
92
|
if (list) return await listMigrationsAction();
|
|
93
|
+
if (pending) return await pendingMigrationsAction();
|
|
42
94
|
if (all) return await allMigrationsFilesAction();
|
|
43
95
|
if (path) await loadMigrationFile(Path.toAbsolute(path));
|
|
44
96
|
else await loadAllMigrations();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrate-action.mjs","names":[],"sources":["../../../../../../../core/src/database/migrate-action.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport {\r\n exportMigrationsSQL,\r\n freshMigrate,\r\n listExecutedMigrations,\r\n Migration,\r\n migrationRunner,\r\n rollbackMigrations,\r\n runMigrations,\r\n} from \"@warlock.js/cascade\";\r\nimport dayjs from \"dayjs\";\r\nimport path from \"path\";\r\nimport { CommandActionData } from \"../cli/types\";\r\nimport { filesOrchestrator } from \"../dev-server/files-orchestrator\";\r\nimport { Path } from \"../dev-server/path\";\r\nimport { getFilesFromDirectory } from \"../dev-server/utils\";\r\nimport { srcPath } from \"../utils\";\r\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\r\n\r\nasync function listMigrationsAction() {\r\n const createdMigrations = await listExecutedMigrations();\r\n\r\n console.log(`\\nTotal Executed Migrations: ${colors.green(createdMigrations.length)}\\n`);\r\n\r\n if (createdMigrations.length === 0) {\r\n console.log(colors.gray(\" No migrations have been executed yet.\\n\"));\r\n return;\r\n }\r\n\r\n // Display each migration as a block\r\n for (const migration of createdMigrations) {\r\n const executedAt = dayjs(migration.executedAt).format(\"DD-MM-YYYY hh:mm:ss A\");\r\n const createdAt = migration.createdAt\r\n ? dayjs(migration.createdAt).format(\"DD-MM-YYYY hh:mm:ss A\")\r\n : null;\r\n\r\n // Migration name with checkmark icon\r\n console.log(` ${colors.green(\"✔\")} ${colors.cyanBright(migration.name)}`);\r\n\r\n // Executed date\r\n console.log(` ${colors.gray(\"Executed:\")} ${colors.white(executedAt)}`);\r\n\r\n // Created date (if available)\r\n if (createdAt) {\r\n console.log(` ${colors.gray(\"Created:\")} ${colors.yellow(createdAt)}`);\r\n }\r\n\r\n console.log(\"\"); // Empty line between migrations\r\n }\r\n}\r\n\r\nasync function allMigrationsFilesAction() {\r\n // get all available migration files in the project\r\n const files = (await migrationFiles()).map((path) => Path.toRelative(path));\r\n console.log(`Total Migration Files: ${colors.green(files.length)}`);\r\n\r\n for (const file of files) {\r\n console.log(colors.yellowBright(file));\r\n }\r\n}\r\n\r\n/**\r\n * If path is provided, then run the migration runner against that file only\r\n * If fresh is provided, then rollback all migrations and run all migrations\r\n * If rollback is provided, then run the migration runner against all files in reverse order\r\n */\r\nexport async function migrateAction(options: CommandActionData) {\r\n const { fresh, path, rollback, all, list, sql, pendingOnly, compact } = options.options;\r\n\r\n if (list) {\r\n return await listMigrationsAction();\r\n }\r\n\r\n if (all) {\r\n return await allMigrationsFilesAction();\r\n }\r\n\r\n if (path) {\r\n await loadMigrationFile(Path.toAbsolute(path as string));\r\n } else {\r\n await loadAllMigrations();\r\n }\r\n\r\n if (fresh && rollback) {\r\n console.log(colors.redBright(\"You can't use --fresh and --rollback together\"));\r\n process.exit(1);\r\n }\r\n\r\n if (rollback) {\r\n await rollbackMigrations({ all: true });\r\n return;\r\n }\r\n\r\n if (sql) {\r\n await exportMigrationsSQL({\r\n pendingOnly: pendingOnly as boolean,\r\n compact: compact as boolean,\r\n });\r\n return;\r\n }\r\n\r\n if (fresh) {\r\n await freshMigrate();\r\n return;\r\n }\r\n\r\n await runMigrations();\r\n}\r\n\r\nasync function loadMigrationFile(absPath: string) {\r\n const relativePath = Path.toRelative(absPath);\r\n\r\n const loadedModule = await filesOrchestrator.load<{ default: typeof Migration }>(relativePath);\r\n\r\n if (!loadedModule?.default) {\r\n throw new Error(`${Path.toRelative(absPath)} must have a default export`);\r\n }\r\n\r\n const MigrationClass = loadedModule.default;\r\n\r\n if (!MigrationClass.migrationName) {\r\n MigrationClass.migrationName = path\r\n .basename(absPath)\r\n .split(\".\")[0]\r\n .replace(\"-migration\", \"\")\r\n .replace(\"_migration\", \"\");\r\n }\r\n\r\n // Extract createdAt timestamp from filename if not already set\r\n // Expected format: MM-DD-YYYY_HH-MM-SS-name.migration.ts\r\n if (!MigrationClass.createdAt) {\r\n const filename = path.basename(absPath);\r\n const timestampMatch = filename.match(/^(\\d{2}-\\d{2}-\\d{4}_\\d{2}-\\d{2}-\\d{2})/);\r\n if (timestampMatch) {\r\n MigrationClass.createdAt = timestampMatch[1];\r\n }\r\n }\r\n\r\n migrationRunner.register(MigrationClass);\r\n}\r\n\r\n/**\r\n *\r\n * @returns List of absolute paths to migration files\r\n */\r\nasync function migrationFiles() {\r\n const migrationFiles = await getFilesFromDirectory(srcPath(\"app\"), \"*/models/*/migrations/*\");\r\n const separateMigrationsFolderFIles = await getFilesFromDirectory(\r\n srcPath(\"app\"),\r\n \"*/migrations/*\",\r\n );\r\n\r\n const migrations = [...migrationFiles, ...separateMigrationsFolderFIles];\r\n\r\n return migrations;\r\n}\r\n\r\nasync function loadAllMigrations() {\r\n // Load config-registered migrations (from packages like @warlock.js/auth)\r\n const configMigrations = warlockConfigManager.get(\"database\")?.migrations || [];\r\n\r\n for (const MigrationClass of configMigrations) {\r\n // Use class name as migration name if not set\r\n if (!MigrationClass.migrationName) {\r\n MigrationClass.migrationName = MigrationClass.name;\r\n }\r\n migrationRunner.register(MigrationClass);\r\n }\r\n\r\n // Always load file-based migrations from src/app, regardless of config\r\n const migrations = await migrationFiles();\r\n for (const migrationFile of migrations) {\r\n await loadMigrationFile(migrationFile);\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;;AAmBA,eAAe,uBAAuB;CACpC,MAAM,oBAAoB,MAAM,uBAAuB;CAEvD,QAAQ,IAAI,gCAAgC,OAAO,MAAM,kBAAkB,MAAM,EAAE,GAAG;CAEtF,IAAI,kBAAkB,WAAW,GAAG;EAClC,QAAQ,IAAI,OAAO,KAAK,2CAA2C,CAAC;EACpE;CACF;CAGA,KAAK,MAAM,aAAa,mBAAmB;EACzC,MAAM,aAAa,MAAM,UAAU,UAAU,CAAC,CAAC,OAAO,uBAAuB;EAC7E,MAAM,YAAY,UAAU,YACxB,MAAM,UAAU,SAAS,CAAC,CAAC,OAAO,uBAAuB,IACzD;EAGJ,QAAQ,IAAI,KAAK,OAAO,MAAM,GAAG,EAAE,GAAG,OAAO,WAAW,UAAU,IAAI,GAAG;EAGzE,QAAQ,IAAI,OAAO,OAAO,KAAK,WAAW,EAAE,GAAG,OAAO,MAAM,UAAU,GAAG;EAGzE,IAAI,WACF,QAAQ,IAAI,OAAO,OAAO,KAAK,UAAU,EAAE,IAAI,OAAO,OAAO,SAAS,GAAG;EAG3E,QAAQ,IAAI,EAAE;CAChB;AACF;AAEA,eAAe,2BAA2B;CAExC,MAAM,SAAS,MAAM,eAAe,EAAC,CAAE,KAAK,SAAS,KAAK,WAAW,IAAI,CAAC;CAC1E,QAAQ,IAAI,0BAA0B,OAAO,MAAM,MAAM,MAAM,GAAG;CAElE,KAAK,MAAM,QAAQ,OACjB,QAAQ,IAAI,OAAO,aAAa,IAAI,CAAC;AAEzC;;;;;;AAOA,eAAsB,cAAc,SAA4B;CAC9D,MAAM,EAAE,OAAO,MAAM,UAAU,KAAK,MAAM,KAAK,aAAa,YAAY,QAAQ;CAEhF,IAAI,MACF,OAAO,MAAM,qBAAqB;CAGpC,IAAI,KACF,OAAO,MAAM,yBAAyB;CAGxC,IAAI,MACF,MAAM,kBAAkB,KAAK,WAAW,IAAc,CAAC;MAEvD,MAAM,kBAAkB;CAG1B,IAAI,SAAS,UAAU;EACrB,QAAQ,IAAI,OAAO,UAAU,+CAA+C,CAAC;EAC7E,QAAQ,KAAK,CAAC;CAChB;CAEA,IAAI,UAAU;EACZ,MAAM,mBAAmB,EAAE,KAAK,KAAK,CAAC;EACtC;CACF;CAEA,IAAI,KAAK;EACP,MAAM,oBAAoB;GACX;GACJ;EACX,CAAC;EACD;CACF;CAEA,IAAI,OAAO;EACT,MAAM,aAAa;EACnB;CACF;CAEA,MAAM,cAAc;AACtB;AAEA,eAAe,kBAAkB,SAAiB;CAChD,MAAM,eAAe,KAAK,WAAW,OAAO;CAE5C,MAAM,eAAe,MAAM,kBAAkB,KAAoC,YAAY;CAE7F,IAAI,CAAC,cAAc,SACjB,MAAM,IAAI,MAAM,GAAG,KAAK,WAAW,OAAO,EAAE,4BAA4B;CAG1E,MAAM,iBAAiB,aAAa;CAEpC,IAAI,CAAC,eAAe,eAClB,eAAe,gBAAgB,KAC5B,SAAS,OAAO,CAAC,CACjB,MAAM,GAAG,CAAC,CAAC,EAAE,CACb,QAAQ,cAAc,EAAE,CAAC,CACzB,QAAQ,cAAc,EAAE;CAK7B,IAAI,CAAC,eAAe,WAAW;EAE7B,MAAM,iBADW,KAAK,SAAS,OACD,CAAC,CAAC,MAAM,wCAAwC;EAC9E,IAAI,gBACF,eAAe,YAAY,eAAe;CAE9C;CAEA,gBAAgB,SAAS,cAAc;AACzC;;;;;AAMA,eAAe,iBAAiB;CAC9B,MAAM,iBAAiB,MAAM,sBAAsB,QAAQ,KAAK,GAAG,yBAAyB;CAC5F,MAAM,gCAAgC,MAAM,sBAC1C,QAAQ,KAAK,GACb,gBACF;CAIA,OAAO,CAFa,GAAG,gBAAgB,GAAG,6BAE1B;AAClB;AAEA,eAAe,oBAAoB;CAEjC,MAAM,mBAAmB,qBAAqB,IAAI,UAAU,CAAC,EAAE,cAAc,CAAC;CAE9E,KAAK,MAAM,kBAAkB,kBAAkB;EAE7C,IAAI,CAAC,eAAe,eAClB,eAAe,gBAAgB,eAAe;EAEhD,gBAAgB,SAAS,cAAc;CACzC;CAGA,MAAM,aAAa,MAAM,eAAe;CACxC,KAAK,MAAM,iBAAiB,YAC1B,MAAM,kBAAkB,aAAa;AAEzC"}
|
|
1
|
+
{"version":3,"file":"migrate-action.mjs","names":[],"sources":["../../../../../../../core/src/database/migrate-action.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport {\r\n exportMigrationsSQL,\r\n freshMigrate,\r\n listExecutedMigrations,\r\n Migration,\r\n migrationRunner,\r\n rollbackMigrations,\r\n runMigrations,\r\n} from \"@warlock.js/cascade\";\r\nimport dayjs from \"dayjs\";\r\nimport path from \"path\";\r\nimport { CommandActionData } from \"../cli/types\";\r\nimport { filesOrchestrator } from \"../dev-server/files-orchestrator\";\r\nimport { Path } from \"../dev-server/path\";\r\nimport { getFilesFromDirectory } from \"../dev-server/utils\";\r\nimport { srcPath } from \"../utils\";\r\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\r\nimport { exitCodeFor, PENDING_EXIT_CODE } from \"./pending-exit-code\";\r\nimport {\r\n resolvePendingMigrations,\r\n type PendingMigrationsResult,\r\n} from \"./resolve-pending-migrations\";\r\n\r\n/**\r\n * `--list` — print the migration state: what has run, then what will run next.\r\n *\r\n * The executed section is printed FIRST and unconditionally, before anything\r\n * touches disk or config. That ordering is the contract: reading migrations\r\n * from the table cannot fail because of a broken migration file, and `--list`\r\n * is the command an operator reaches for *during* an incident. The pending\r\n * section is best-effort on top of an answer that is already on screen.\r\n *\r\n * Always exits 0. It is a report; `--pending` is the gate.\r\n */\r\nasync function listMigrationsAction() {\r\n await printExecutedMigrations();\r\n\r\n const pending = await resolvePendingMigrations(loadAllMigrations);\r\n\r\n printPendingMigrations(pending);\r\n}\r\n\r\nasync function printExecutedMigrations() {\r\n const createdMigrations = await listExecutedMigrations();\r\n\r\n console.log(`\\nTotal Executed Migrations: ${colors.green(createdMigrations.length)}\\n`);\r\n\r\n if (createdMigrations.length === 0) {\r\n console.log(colors.gray(\" No migrations have been executed yet.\\n\"));\r\n return;\r\n }\r\n\r\n // Display each migration as a block\r\n for (const migration of createdMigrations) {\r\n const executedAt = dayjs(migration.executedAt).format(\"DD-MM-YYYY hh:mm:ss A\");\r\n const createdAt = migration.createdAt\r\n ? dayjs(migration.createdAt).format(\"DD-MM-YYYY hh:mm:ss A\")\r\n : null;\r\n\r\n // Migration name with checkmark icon\r\n console.log(` ${colors.green(\"✔\")} ${colors.cyanBright(migration.name)}`);\r\n\r\n // Executed date\r\n console.log(` ${colors.gray(\"Executed:\")} ${colors.white(executedAt)}`);\r\n\r\n // Created date (if available)\r\n if (createdAt) {\r\n console.log(` ${colors.gray(\"Created:\")} ${colors.yellow(createdAt)}`);\r\n }\r\n\r\n console.log(\"\"); // Empty line between migrations\r\n }\r\n}\r\n\r\n/**\r\n * Print the pending section shared by `--list` and `--pending`.\r\n *\r\n * An unavailable result prints an explicit line and NEVER a count. \"0 pending\"\r\n * over a tree we failed to read is the one output this whole feature exists to\r\n * prevent.\r\n */\r\nfunction printPendingMigrations(result: PendingMigrationsResult) {\r\n if (result.type === \"unavailable\") {\r\n console.log(\r\n `${colors.yellowBright(\"Pending: unavailable\")} — ${result.reason}\\n` +\r\n colors.gray(\" The executed list above is still accurate.\\n\"),\r\n );\r\n return;\r\n }\r\n\r\n const { migrations } = result;\r\n\r\n console.log(`Total Pending Migrations: ${colors.green(migrations.length)}\\n`);\r\n\r\n if (migrations.length === 0) {\r\n console.log(colors.gray(\" Nothing pending — the database is up to date.\\n\"));\r\n return;\r\n }\r\n\r\n // Numbered because the ORDER is the answer: this is what the next\r\n // `warlock migrate` will do, in the sequence it will do it.\r\n migrations.forEach((migration, index) => {\r\n console.log(` ${colors.gray(`${index + 1}.`)} ${colors.cyanBright(migration.name)}`);\r\n });\r\n\r\n console.log(\"\");\r\n}\r\n\r\n/**\r\n * `--pending` — the scriptable half. Same computation as `--list`'s pending\r\n * section, different exit contract: see {@link PENDING_EXIT_CODE}.\r\n */\r\nasync function pendingMigrationsAction() {\r\n const result = await resolvePendingMigrations(loadAllMigrations);\r\n\r\n printPendingMigrations(result);\r\n\r\n const exitCode = exitCodeFor(result);\r\n\r\n if (exitCode === PENDING_EXIT_CODE.unavailable) {\r\n console.error(\r\n colors.redBright(\"Could not determine the pending set — exiting 2 rather than reporting 0.\"),\r\n );\r\n }\r\n\r\n process.exit(exitCode);\r\n}\r\n\r\nasync function allMigrationsFilesAction() {\r\n // get all available migration files in the project\r\n const files = (await migrationFiles()).map((path) => Path.toRelative(path));\r\n console.log(`Total Migration Files: ${colors.green(files.length)}`);\r\n\r\n for (const file of files) {\r\n console.log(colors.yellowBright(file));\r\n }\r\n}\r\n\r\n/**\r\n * If path is provided, then run the migration runner against that file only\r\n * If fresh is provided, then rollback all migrations and run all migrations\r\n * If rollback is provided, then run the migration runner against all files in reverse order\r\n */\r\nexport async function migrateAction(options: CommandActionData) {\r\n const { fresh, path, rollback, all, list, pending, sql, pendingOnly, compact } = options.options;\r\n\r\n if (list) {\r\n return await listMigrationsAction();\r\n }\r\n\r\n if (pending) {\r\n return await pendingMigrationsAction();\r\n }\r\n\r\n if (all) {\r\n return await allMigrationsFilesAction();\r\n }\r\n\r\n if (path) {\r\n await loadMigrationFile(Path.toAbsolute(path as string));\r\n } else {\r\n await loadAllMigrations();\r\n }\r\n\r\n if (fresh && rollback) {\r\n console.log(colors.redBright(\"You can't use --fresh and --rollback together\"));\r\n process.exit(1);\r\n }\r\n\r\n if (rollback) {\r\n await rollbackMigrations({ all: true });\r\n return;\r\n }\r\n\r\n if (sql) {\r\n await exportMigrationsSQL({\r\n pendingOnly: pendingOnly as boolean,\r\n compact: compact as boolean,\r\n });\r\n return;\r\n }\r\n\r\n if (fresh) {\r\n await freshMigrate();\r\n return;\r\n }\r\n\r\n await runMigrations();\r\n}\r\n\r\nasync function loadMigrationFile(absPath: string) {\r\n const relativePath = Path.toRelative(absPath);\r\n\r\n const loadedModule = await filesOrchestrator.load<{ default: typeof Migration }>(relativePath);\r\n\r\n if (!loadedModule?.default) {\r\n throw new Error(`${Path.toRelative(absPath)} must have a default export`);\r\n }\r\n\r\n const MigrationClass = loadedModule.default;\r\n\r\n if (!MigrationClass.migrationName) {\r\n MigrationClass.migrationName = path\r\n .basename(absPath)\r\n .split(\".\")[0]\r\n .replace(\"-migration\", \"\")\r\n .replace(\"_migration\", \"\");\r\n }\r\n\r\n // Extract createdAt timestamp from filename if not already set\r\n // Expected format: MM-DD-YYYY_HH-MM-SS-name.migration.ts\r\n if (!MigrationClass.createdAt) {\r\n const filename = path.basename(absPath);\r\n const timestampMatch = filename.match(/^(\\d{2}-\\d{2}-\\d{4}_\\d{2}-\\d{2}-\\d{2})/);\r\n if (timestampMatch) {\r\n MigrationClass.createdAt = timestampMatch[1];\r\n }\r\n }\r\n\r\n migrationRunner.register(MigrationClass);\r\n}\r\n\r\n/**\r\n *\r\n * @returns List of absolute paths to migration files\r\n */\r\nasync function migrationFiles() {\r\n const migrationFiles = await getFilesFromDirectory(srcPath(\"app\"), \"*/models/*/migrations/*\");\r\n const separateMigrationsFolderFIles = await getFilesFromDirectory(\r\n srcPath(\"app\"),\r\n \"*/migrations/*\",\r\n );\r\n\r\n const migrations = [...migrationFiles, ...separateMigrationsFolderFIles];\r\n\r\n return migrations;\r\n}\r\n\r\nasync function loadAllMigrations() {\r\n // Load config-registered migrations (from packages like @warlock.js/auth)\r\n const configMigrations = warlockConfigManager.get(\"database\")?.migrations || [];\r\n\r\n for (const MigrationClass of configMigrations) {\r\n // Use class name as migration name if not set\r\n if (!MigrationClass.migrationName) {\r\n MigrationClass.migrationName = MigrationClass.name;\r\n }\r\n migrationRunner.register(MigrationClass);\r\n }\r\n\r\n // Always load file-based migrations from src/app, regardless of config\r\n const migrations = await migrationFiles();\r\n for (const migrationFile of migrations) {\r\n await loadMigrationFile(migrationFile);\r\n }\r\n}\r\n"],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//#region ../core/src/database/pending-exit-code.ts
|
|
2
|
+
/**
|
|
3
|
+
* Exit codes for `warlock migrate --pending`.
|
|
4
|
+
*
|
|
5
|
+
* `--pending` is a GATE, not a report: it is written into `migrate --pending &&
|
|
6
|
+
* deploy` and into CI steps asserting nothing is outstanding. So it must
|
|
7
|
+
* distinguish three outcomes, not two.
|
|
8
|
+
*
|
|
9
|
+
* Two codes would fold `pending` and `unavailable` into a single non-zero, and
|
|
10
|
+
* a script could no longer tell "three migrations are waiting" from "I could
|
|
11
|
+
* not work out what is waiting". Those demand opposite responses — the first is
|
|
12
|
+
* "run them", the second is "stop and get a human".
|
|
13
|
+
*
|
|
14
|
+
* `warlock migrate --list` is the report half and always exits `0`, whatever
|
|
15
|
+
* the pending section says.
|
|
16
|
+
*/
|
|
17
|
+
const PENDING_EXIT_CODE = {
|
|
18
|
+
/** Computed, and nothing is pending. */
|
|
19
|
+
clear: 0,
|
|
20
|
+
/** Computed, and at least one migration is pending. */
|
|
21
|
+
pending: 1,
|
|
22
|
+
/** Could not be computed. Never conflate this with `clear`. */
|
|
23
|
+
unavailable: 2
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Map a resolution outcome onto the exit code a gate should report.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* exitCodeFor({ type: "resolved", migrations: [] }); // 0
|
|
30
|
+
* exitCodeFor({ type: "unavailable", reason: "…" }); // 2
|
|
31
|
+
*/
|
|
32
|
+
function exitCodeFor(result) {
|
|
33
|
+
if (result.type === "unavailable") return PENDING_EXIT_CODE.unavailable;
|
|
34
|
+
return result.migrations.length > 0 ? PENDING_EXIT_CODE.pending : PENDING_EXIT_CODE.clear;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
//#endregion
|
|
38
|
+
export { PENDING_EXIT_CODE, exitCodeFor };
|
|
39
|
+
//# sourceMappingURL=pending-exit-code.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pending-exit-code.mjs","names":[],"sources":["../../../../../../../core/src/database/pending-exit-code.ts"],"sourcesContent":["import type { PendingMigrationsResult } from \"./resolve-pending-migrations\";\n\n/**\n * Exit codes for `warlock migrate --pending`.\n *\n * `--pending` is a GATE, not a report: it is written into `migrate --pending &&\n * deploy` and into CI steps asserting nothing is outstanding. So it must\n * distinguish three outcomes, not two.\n *\n * Two codes would fold `pending` and `unavailable` into a single non-zero, and\n * a script could no longer tell \"three migrations are waiting\" from \"I could\n * not work out what is waiting\". Those demand opposite responses — the first is\n * \"run them\", the second is \"stop and get a human\".\n *\n * `warlock migrate --list` is the report half and always exits `0`, whatever\n * the pending section says.\n */\nexport const PENDING_EXIT_CODE = {\n /** Computed, and nothing is pending. */\n clear: 0,\n /** Computed, and at least one migration is pending. */\n pending: 1,\n /** Could not be computed. Never conflate this with `clear`. */\n unavailable: 2,\n} as const;\n\nexport type PendingExitCode = (typeof PENDING_EXIT_CODE)[keyof typeof PENDING_EXIT_CODE];\n\n/**\n * Map a resolution outcome onto the exit code a gate should report.\n *\n * @example\n * exitCodeFor({ type: \"resolved\", migrations: [] }); // 0\n * exitCodeFor({ type: \"unavailable\", reason: \"…\" }); // 2\n */\nexport function exitCodeFor(result: PendingMigrationsResult): PendingExitCode {\n if (result.type === \"unavailable\") {\n return PENDING_EXIT_CODE.unavailable;\n }\n\n return result.migrations.length > 0 ? PENDING_EXIT_CODE.pending : PENDING_EXIT_CODE.clear;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAiBA,MAAa,oBAAoB;;CAE/B,OAAO;;CAEP,SAAS;;CAET,aAAa;AACf;;;;;;;;AAWA,SAAgB,YAAY,QAAkD;CAC5E,IAAI,OAAO,SAAS,eAClB,OAAO,kBAAkB;CAG3B,OAAO,OAAO,WAAW,SAAS,IAAI,kBAAkB,UAAU,kBAAkB;AACtF"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { listPendingMigrations } from "@warlock.js/cascade";
|
|
2
|
+
|
|
3
|
+
//#region ../core/src/database/resolve-pending-migrations.ts
|
|
4
|
+
/**
|
|
5
|
+
* Load the project's migrations, then compute which of them have not run yet.
|
|
6
|
+
*
|
|
7
|
+
* `loadMigrations` must be the SAME registration path a real `warlock migrate`
|
|
8
|
+
* takes. Skipping it does not fail — it silently yields an empty pending set,
|
|
9
|
+
* because the pending set is computed from the runner's registry.
|
|
10
|
+
*
|
|
11
|
+
* Loading executes project code (a migration file is imported, and a broken one
|
|
12
|
+
* throws), so every failure is converted into an `unavailable` result rather
|
|
13
|
+
* than propagating. Callers decide what an unknown means for them: a listing
|
|
14
|
+
* prints it and carries on, a gate refuses to pass.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const result = await resolvePendingMigrations(loadAllMigrations);
|
|
18
|
+
*
|
|
19
|
+
* if (result.type === "unavailable") {
|
|
20
|
+
* console.error(`Pending: unavailable — ${result.reason}`);
|
|
21
|
+
* }
|
|
22
|
+
*/
|
|
23
|
+
async function resolvePendingMigrations(loadMigrations) {
|
|
24
|
+
try {
|
|
25
|
+
await loadMigrations();
|
|
26
|
+
return {
|
|
27
|
+
type: "resolved",
|
|
28
|
+
migrations: await listPendingMigrations()
|
|
29
|
+
};
|
|
30
|
+
} catch (error) {
|
|
31
|
+
return {
|
|
32
|
+
type: "unavailable",
|
|
33
|
+
reason: describeFailure(error)
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Reduce a thrown value to a single line an operator can act on. The message is
|
|
39
|
+
* the actionable part — a stack trace in the middle of a listing buries the
|
|
40
|
+
* executed section the reader still needs.
|
|
41
|
+
*/
|
|
42
|
+
function describeFailure(error) {
|
|
43
|
+
if (error instanceof Error) return error.message;
|
|
44
|
+
return String(error);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
//#endregion
|
|
48
|
+
export { resolvePendingMigrations };
|
|
49
|
+
//# sourceMappingURL=resolve-pending-migrations.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-pending-migrations.mjs","names":[],"sources":["../../../../../../../core/src/database/resolve-pending-migrations.ts"],"sourcesContent":["import { listPendingMigrations, type PendingMigration } from \"@warlock.js/cascade\";\n\n/**\n * Outcome of trying to compute the pending migration set.\n *\n * The \"unavailable\" arm exists because the two failure modes are NOT the same\n * answer: a database with nothing pending and a tree we could not read both\n * produce an empty list, and only one of them means it is safe to proceed.\n * Collapsing them into `[]` is how a pre-flight check reports a confident\n * all-clear over an unknown.\n */\nexport type PendingMigrationsResult =\n | {\n readonly type: \"resolved\";\n readonly migrations: PendingMigration[];\n }\n | {\n readonly type: \"unavailable\";\n readonly reason: string;\n };\n\n/**\n * Load the project's migrations, then compute which of them have not run yet.\n *\n * `loadMigrations` must be the SAME registration path a real `warlock migrate`\n * takes. Skipping it does not fail — it silently yields an empty pending set,\n * because the pending set is computed from the runner's registry.\n *\n * Loading executes project code (a migration file is imported, and a broken one\n * throws), so every failure is converted into an `unavailable` result rather\n * than propagating. Callers decide what an unknown means for them: a listing\n * prints it and carries on, a gate refuses to pass.\n *\n * @example\n * const result = await resolvePendingMigrations(loadAllMigrations);\n *\n * if (result.type === \"unavailable\") {\n * console.error(`Pending: unavailable — ${result.reason}`);\n * }\n */\nexport async function resolvePendingMigrations(\n loadMigrations: () => Promise<void>,\n): Promise<PendingMigrationsResult> {\n try {\n await loadMigrations();\n\n const migrations = await listPendingMigrations();\n\n return { type: \"resolved\", migrations };\n } catch (error) {\n return { type: \"unavailable\", reason: describeFailure(error) };\n }\n}\n\n/**\n * Reduce a thrown value to a single line an operator can act on. The message is\n * the actionable part — a stack trace in the middle of a listing buries the\n * executed section the reader still needs.\n */\nfunction describeFailure(error: unknown): string {\n if (error instanceof Error) {\n return error.message;\n }\n\n return String(error);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAwCA,eAAsB,yBACpB,gBACkC;CAClC,IAAI;EACF,MAAM,eAAe;EAIrB,OAAO;GAAE,MAAM;GAAY,kBAFF,sBAAsB;EAET;CACxC,SAAS,OAAO;EACd,OAAO;GAAE,MAAM;GAAe,QAAQ,gBAAgB,KAAK;EAAE;CAC/D;AACF;;;;;;AAOA,SAAS,gBAAgB,OAAwB;CAC/C,IAAI,iBAAiB,OACnB,OAAO,MAAM;CAGf,OAAO,OAAO,KAAK;AACrB"}
|
package/esm/image/image.d.mts
CHANGED
|
@@ -153,8 +153,9 @@ type InternalOptions = {
|
|
|
153
153
|
* **Important:** This class requires the `sharp` package to be installed.
|
|
154
154
|
* Install it with: `warlock add image` or `npm install sharp`
|
|
155
155
|
*
|
|
156
|
-
* Sharp is
|
|
157
|
-
*
|
|
156
|
+
* Sharp is resolved synchronously on the first construction that needs it, so
|
|
157
|
+
* the constructor and all chainable methods remain synchronous, and a missing
|
|
158
|
+
* sharp throws at construction rather than at some later await.
|
|
158
159
|
*
|
|
159
160
|
* All operations are synchronous and stored as descriptors.
|
|
160
161
|
* The pipeline is executed only when calling output methods:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image.d.mts","names":[],"sources":["../../../../../../../core/src/image/image.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"image.d.mts","names":[],"sources":["../../../../../../../core/src/image/image.ts"],"mappings":"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"}
|
package/esm/image/image.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import { http } from "@mongez/http";
|
|
2
3
|
|
|
3
4
|
//#region ../core/src/image/image.ts
|
|
@@ -17,33 +18,79 @@ Or manually:
|
|
|
17
18
|
yarn add sharp
|
|
18
19
|
`.trim();
|
|
19
20
|
/**
|
|
20
|
-
*
|
|
21
|
+
* Cached sharp function, populated by the first `resolveSharp()` call
|
|
21
22
|
*/
|
|
22
|
-
let
|
|
23
|
+
let sharpFn = null;
|
|
23
24
|
/**
|
|
24
|
-
*
|
|
25
|
+
* Whether the resolution attempt already ran (success or failure)
|
|
25
26
|
*/
|
|
26
|
-
let
|
|
27
|
+
let sharpResolved = false;
|
|
27
28
|
/**
|
|
28
|
-
*
|
|
29
|
+
* Why the resolution failed, when it failed for a reason other than absence.
|
|
30
|
+
*
|
|
31
|
+
* Cached and re-thrown on every later call: the resolution attempt runs exactly
|
|
32
|
+
* once, so without this the second construction would fall through to the
|
|
33
|
+
* "not installed" branch and report a different, wrong cause.
|
|
34
|
+
*/
|
|
35
|
+
let sharpLoadError = null;
|
|
36
|
+
const ErrorWithCause = Error;
|
|
37
|
+
/**
|
|
38
|
+
* Whether `error` says sharp itself is absent, as opposed to present but broken.
|
|
39
|
+
*
|
|
40
|
+
* Both halves are load-bearing. `MODULE_NOT_FOUND` alone is not enough: a
|
|
41
|
+
* dependency missing *inside* sharp raises the very same code (`Cannot find
|
|
42
|
+
* module 'color'`), and treating that as absence would tell the operator to
|
|
43
|
+
* install a package they already have. So the message has to name the specifier
|
|
44
|
+
* `'sharp'` exactly — quoted, which is also what keeps `'sharp-cli'` and friends
|
|
45
|
+
* from matching.
|
|
46
|
+
*/
|
|
47
|
+
function isSharpMissing(error) {
|
|
48
|
+
if (!(error instanceof Error)) return false;
|
|
49
|
+
return error.code === "MODULE_NOT_FOUND" && error.message.includes("Cannot find module 'sharp'");
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Resolve sharp synchronously, on first use.
|
|
53
|
+
*
|
|
54
|
+
* Resolution is deliberately *lazy* and *synchronous*:
|
|
55
|
+
*
|
|
56
|
+
* - Lazy, because a top-level require would drag sharp's native binary into
|
|
57
|
+
* every `import "@warlock.js/core"`, including apps that never touch images.
|
|
58
|
+
* - Synchronous, because the `Image` constructor is synchronous. An async
|
|
59
|
+
* import kicked off at module load leaves a window in which the module is
|
|
60
|
+
* neither loaded nor known to be missing, and a constructor running inside
|
|
61
|
+
* that window has no correct answer to give.
|
|
62
|
+
*
|
|
63
|
+
* `createRequire` gives an ESM-safe `require`, and the outcome — module, absence
|
|
64
|
+
* or load failure — is cached, so the resolution runs exactly once per process.
|
|
65
|
+
* The failure *reason* is cached too, not just the fact of it: every call after
|
|
66
|
+
* a failed load has to report the same cause as the first one.
|
|
67
|
+
*
|
|
68
|
+
* @throws when sharp is not installed, or is installed but cannot be loaded
|
|
29
69
|
*/
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
70
|
+
function resolveSharp() {
|
|
71
|
+
if (!sharpResolved) {
|
|
72
|
+
sharpResolved = true;
|
|
73
|
+
try {
|
|
74
|
+
const module = createRequire(import.meta.url)("sharp");
|
|
75
|
+
sharpFn = module.default ?? module;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
sharpFn = null;
|
|
78
|
+
if (!isSharpMissing(error)) sharpLoadError = new ErrorWithCause(`Failed to load "sharp": ${error.message}`, { cause: error });
|
|
79
|
+
}
|
|
36
80
|
}
|
|
81
|
+
if (sharpLoadError) throw sharpLoadError;
|
|
82
|
+
if (!sharpFn) throw new Error(`sharp is not installed.\n\n${SHARP_INSTALL_INSTRUCTIONS}`);
|
|
83
|
+
return sharpFn;
|
|
37
84
|
}
|
|
38
|
-
loadSharpModule();
|
|
39
85
|
/**
|
|
40
86
|
* Image manipulation class with deferred pipeline execution.
|
|
41
87
|
*
|
|
42
88
|
* **Important:** This class requires the `sharp` package to be installed.
|
|
43
89
|
* Install it with: `warlock add image` or `npm install sharp`
|
|
44
90
|
*
|
|
45
|
-
* Sharp is
|
|
46
|
-
*
|
|
91
|
+
* Sharp is resolved synchronously on the first construction that needs it, so
|
|
92
|
+
* the constructor and all chainable methods remain synchronous, and a missing
|
|
93
|
+
* sharp throws at construction rather than at some later await.
|
|
47
94
|
*
|
|
48
95
|
* All operations are synchronous and stored as descriptors.
|
|
49
96
|
* The pipeline is executed only when calling output methods:
|
|
@@ -81,9 +128,8 @@ var Image = class Image {
|
|
|
81
128
|
this.operations = [];
|
|
82
129
|
this.cachedMetadata = null;
|
|
83
130
|
this.pipelineExecuted = false;
|
|
84
|
-
if (moduleExists === false) throw new Error(`sharp is not installed.\n\n${SHARP_INSTALL_INSTRUCTIONS}`);
|
|
85
131
|
if (image instanceof Object && "clone" in image && typeof image.clone === "function") this.image = image;
|
|
86
|
-
else this.image =
|
|
132
|
+
else this.image = resolveSharp()(image);
|
|
87
133
|
}
|
|
88
134
|
/**
|
|
89
135
|
* Create image instance from file path
|
|
@@ -321,7 +367,7 @@ var Image = class Image {
|
|
|
321
367
|
*/
|
|
322
368
|
async resolveImageBuffer(input) {
|
|
323
369
|
if (input instanceof Image) return input.image.toBuffer();
|
|
324
|
-
return
|
|
370
|
+
return resolveSharp()(input).toBuffer();
|
|
325
371
|
}
|
|
326
372
|
/**
|
|
327
373
|
* Apply format and quality options.
|