@elench/testkit 0.1.114 → 0.1.116
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -8
- package/lib/cli/args.mjs +3 -3
- package/lib/cli/assistant/app.mjs +4 -2
- package/lib/cli/assistant/session.mjs +5 -1
- package/lib/cli/assistant/state.mjs +1 -2
- package/lib/cli/command-flags.mjs +4 -0
- package/lib/cli/commands/db/schema/refresh.mjs +21 -0
- package/lib/cli/commands/db/schema/verify.mjs +27 -0
- package/lib/cli/components/blocks/run-tree.mjs +7 -2
- package/lib/cli/components/hooks/use-element-layout.mjs +63 -0
- package/lib/cli/components/hooks/use-spinner-frame.mjs +26 -0
- package/lib/cli/entrypoint.mjs +1 -0
- package/lib/cli/operations/db/schema/refresh/operation.mjs +56 -0
- package/lib/cli/operations/db/{snapshot/capture → schema/verify}/operation.mjs +6 -27
- package/lib/cli/operations/run/operation.mjs +1 -0
- package/lib/cli/renderers/db-schema/text.mjs +7 -0
- package/lib/config/database.mjs +64 -0
- package/lib/config-api/index.d.ts +16 -1
- package/lib/config-api/index.mjs +31 -16
- package/lib/database/fingerprint.mjs +2 -0
- package/lib/database/index.mjs +142 -104
- package/lib/database/schema-source.mjs +295 -0
- package/lib/database/template-steps.mjs +158 -38
- package/lib/runner/orchestrator.mjs +4 -3
- package/lib/runner/template-steps.mjs +12 -1
- package/lib/runner/template.mjs +16 -1
- package/node_modules/@alcalzone/ansi-tokenize/README.md +0 -5
- package/node_modules/@alcalzone/ansi-tokenize/build/ansiCodes.d.ts +8 -0
- package/node_modules/@alcalzone/ansi-tokenize/build/ansiCodes.js +10 -8
- package/node_modules/@alcalzone/ansi-tokenize/build/ansiCodes.js.map +1 -1
- package/node_modules/@alcalzone/ansi-tokenize/build/tokenize.d.ts +1 -5
- package/node_modules/@alcalzone/ansi-tokenize/build/tokenize.js +9 -45
- package/node_modules/@alcalzone/ansi-tokenize/build/tokenize.js.map +1 -1
- package/node_modules/@alcalzone/ansi-tokenize/package.json +1 -1
- package/node_modules/@elench/next-analysis/package.json +1 -1
- package/node_modules/@elench/testkit-bridge/package.json +2 -2
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/node_modules/@elench/ts-analysis/package.json +1 -1
- package/node_modules/cli-boxes/index.d.ts +95 -90
- package/node_modules/cli-boxes/index.js +5 -2
- package/node_modules/cli-boxes/package.json +6 -13
- package/node_modules/cli-boxes/readme.md +15 -3
- package/node_modules/cli-truncate/index.d.ts +1 -1
- package/node_modules/cli-truncate/package.json +4 -4
- package/node_modules/cli-truncate/readme.md +1 -0
- package/node_modules/es-toolkit/CHANGELOG.md +801 -0
- package/node_modules/es-toolkit/src/compat/_internal/Equals.d.ts +1 -0
- package/node_modules/es-toolkit/src/compat/_internal/IsWritable.d.ts +3 -0
- package/node_modules/es-toolkit/src/compat/_internal/MutableList.d.ts +4 -0
- package/node_modules/es-toolkit/src/compat/_internal/RejectReadonly.d.ts +4 -0
- package/node_modules/esprima/ChangeLog +235 -0
- package/node_modules/ink/build/apply-styles.js +175 -0
- package/node_modules/ink/build/build-layout.js +77 -0
- package/node_modules/ink/build/calculate-wrapped-text.js +53 -0
- package/node_modules/ink/build/components/App.d.ts +1 -4
- package/node_modules/ink/build/components/App.js +22 -142
- package/node_modules/ink/build/components/App.js.map +1 -1
- package/node_modules/ink/build/components/AppContext.d.ts +3 -23
- package/node_modules/ink/build/components/AppContext.js +4 -7
- package/node_modules/ink/build/components/AppContext.js.map +1 -1
- package/node_modules/ink/build/components/Box.d.ts +3 -16
- package/node_modules/ink/build/components/Color.js +62 -0
- package/node_modules/ink/build/components/Cursor.d.ts +83 -0
- package/node_modules/ink/build/components/Cursor.js +53 -0
- package/node_modules/ink/build/components/Cursor.js.map +1 -0
- package/node_modules/ink/build/components/ErrorBoundary.d.ts +2 -2
- package/node_modules/ink/build/components/ErrorOverview.js +6 -6
- package/node_modules/ink/build/components/ErrorOverview.js.map +1 -1
- package/node_modules/ink/build/components/Static.js.map +1 -1
- package/node_modules/ink/build/components/StdinContext.d.ts +1 -7
- package/node_modules/ink/build/components/StdinContext.js +0 -1
- package/node_modules/ink/build/components/StdinContext.js.map +1 -1
- package/node_modules/ink/build/components/Text.d.ts +1 -1
- package/node_modules/ink/build/components/Text.js +1 -1
- package/node_modules/ink/build/components/Text.js.map +1 -1
- package/node_modules/ink/build/components/Transform.d.ts +1 -1
- package/node_modules/ink/build/devtools-window-polyfill.js +4 -7
- package/node_modules/ink/build/devtools-window-polyfill.js.map +1 -1
- package/node_modules/ink/build/devtools.js +6 -31
- package/node_modules/ink/build/devtools.js.map +1 -1
- package/node_modules/ink/build/dom.d.ts +1 -5
- package/node_modules/ink/build/dom.js +1 -20
- package/node_modules/ink/build/dom.js.map +1 -1
- package/node_modules/ink/build/experimental/apply-style.js +140 -0
- package/node_modules/ink/build/experimental/dom.js +123 -0
- package/node_modules/ink/build/experimental/output.js +91 -0
- package/node_modules/ink/build/experimental/reconciler.js +141 -0
- package/node_modules/ink/build/experimental/renderer.js +81 -0
- package/node_modules/ink/build/hooks/use-app.d.ts +1 -1
- package/node_modules/ink/build/hooks/use-app.js +1 -1
- package/node_modules/ink/build/hooks/use-cursor.d.ts +1 -1
- package/node_modules/ink/build/hooks/use-cursor.js +1 -1
- package/node_modules/ink/build/hooks/use-focus-manager.d.ts +2 -17
- package/node_modules/ink/build/hooks/use-focus-manager.js +1 -2
- package/node_modules/ink/build/hooks/use-focus-manager.js.map +1 -1
- package/node_modules/ink/build/hooks/use-focus.d.ts +1 -2
- package/node_modules/ink/build/hooks/use-focus.js +4 -5
- package/node_modules/ink/build/hooks/use-focus.js.map +1 -1
- package/node_modules/ink/build/hooks/use-input.d.ts +1 -2
- package/node_modules/ink/build/hooks/use-input.js +80 -82
- package/node_modules/ink/build/hooks/use-input.js.map +1 -1
- package/node_modules/ink/build/hooks/use-is-screen-reader-enabled.d.ts +1 -2
- package/node_modules/ink/build/hooks/use-is-screen-reader-enabled.js +1 -2
- package/node_modules/ink/build/hooks/use-is-screen-reader-enabled.js.map +1 -1
- package/node_modules/ink/build/hooks/use-stderr.d.ts +1 -1
- package/node_modules/ink/build/hooks/use-stderr.js +1 -1
- package/node_modules/ink/build/hooks/use-stdin.d.ts +2 -4
- package/node_modules/ink/build/hooks/use-stdin.js +1 -2
- package/node_modules/ink/build/hooks/use-stdin.js.map +1 -1
- package/node_modules/ink/build/hooks/use-stdout.d.ts +1 -1
- package/node_modules/ink/build/hooks/use-stdout.js +1 -1
- package/node_modules/ink/build/hooks/useInput.js +38 -0
- package/node_modules/ink/build/index.d.ts +1 -8
- package/node_modules/ink/build/index.js +0 -4
- package/node_modules/ink/build/index.js.map +1 -1
- package/node_modules/ink/build/ink.d.ts +3 -48
- package/node_modules/ink/build/ink.js +155 -325
- package/node_modules/ink/build/ink.js.map +1 -1
- package/node_modules/ink/build/input-parser.d.ts +1 -4
- package/node_modules/ink/build/input-parser.js +30 -70
- package/node_modules/ink/build/input-parser.js.map +1 -1
- package/node_modules/ink/build/instance.js +205 -0
- package/node_modules/ink/build/layout.d.ts +7 -0
- package/node_modules/ink/build/layout.js +33 -0
- package/node_modules/ink/build/layout.js.map +1 -0
- package/node_modules/ink/build/log-update.d.ts +0 -1
- package/node_modules/ink/build/log-update.js +1 -13
- package/node_modules/ink/build/log-update.js.map +1 -1
- package/node_modules/ink/build/measure-element.d.ts +0 -4
- package/node_modules/ink/build/measure-element.js +0 -4
- package/node_modules/ink/build/measure-element.js.map +1 -1
- package/node_modules/ink/build/options.d.ts +52 -0
- package/node_modules/ink/build/options.js +2 -0
- package/node_modules/ink/build/options.js.map +1 -0
- package/node_modules/ink/build/output.js +0 -25
- package/node_modules/ink/build/output.js.map +1 -1
- package/node_modules/ink/build/parse-keypress.d.ts +3 -1
- package/node_modules/ink/build/parse-keypress.js +17 -19
- package/node_modules/ink/build/parse-keypress.js.map +1 -1
- package/node_modules/ink/build/reconciler.js +27 -46
- package/node_modules/ink/build/reconciler.js.map +1 -1
- package/node_modules/ink/build/render-border.js +18 -29
- package/node_modules/ink/build/render-border.js.map +1 -1
- package/node_modules/ink/build/render-to-string.js +1 -2
- package/node_modules/ink/build/render-to-string.js.map +1 -1
- package/node_modules/ink/build/render.d.ts +2 -57
- package/node_modules/ink/build/render.js +11 -18
- package/node_modules/ink/build/render.js.map +1 -1
- package/node_modules/ink/build/screen-reader-update.d.ts +13 -0
- package/node_modules/ink/build/screen-reader-update.js +38 -0
- package/node_modules/ink/build/screen-reader-update.js.map +1 -0
- package/node_modules/ink/build/styles.d.ts +16 -78
- package/node_modules/ink/build/styles.js +31 -102
- package/node_modules/ink/build/styles.js.map +1 -1
- package/node_modules/ink/build/utils.d.ts +2 -9
- package/node_modules/ink/build/utils.js +3 -18
- package/node_modules/ink/build/utils.js.map +1 -1
- package/node_modules/ink/build/wrap-text.js +0 -7
- package/node_modules/ink/build/wrap-text.js.map +1 -1
- package/node_modules/ink/build/write-synchronized.d.ts +1 -1
- package/node_modules/ink/build/write-synchronized.js +2 -4
- package/node_modules/ink/build/write-synchronized.js.map +1 -1
- package/node_modules/ink/node_modules/emoji-regex/LICENSE-MIT.txt +20 -0
- package/node_modules/ink/node_modules/emoji-regex/README.md +107 -0
- package/node_modules/ink/node_modules/emoji-regex/index.d.ts +3 -0
- package/node_modules/ink/node_modules/emoji-regex/index.js +4 -0
- package/node_modules/ink/node_modules/emoji-regex/index.mjs +4 -0
- package/node_modules/ink/node_modules/emoji-regex/package.json +45 -0
- package/node_modules/{wrap-ansi → ink/node_modules/wrap-ansi}/index.d.ts +1 -1
- package/node_modules/ink/node_modules/wrap-ansi/index.js +222 -0
- package/node_modules/ink/node_modules/wrap-ansi/node_modules/string-width/index.d.ts +39 -0
- package/node_modules/ink/node_modules/wrap-ansi/node_modules/string-width/index.js +82 -0
- package/node_modules/ink/node_modules/wrap-ansi/node_modules/string-width/license +9 -0
- package/node_modules/ink/node_modules/wrap-ansi/node_modules/string-width/package.json +64 -0
- package/node_modules/ink/node_modules/wrap-ansi/node_modules/string-width/readme.md +66 -0
- package/node_modules/{wrap-ansi → ink/node_modules/wrap-ansi}/package.json +11 -11
- package/node_modules/{wrap-ansi → ink/node_modules/wrap-ansi}/readme.md +0 -2
- package/node_modules/ink/package.json +98 -34
- package/node_modules/ink/readme.md +48 -554
- package/node_modules/slice-ansi/index.d.ts +1 -1
- package/node_modules/slice-ansi/index.js +89 -146
- package/node_modules/slice-ansi/package.json +5 -5
- package/node_modules/slice-ansi/readme.md +0 -1
- package/node_modules/slice-ansi/tokenize-ansi.js +1 -1
- package/package.json +14 -10
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.d.ts +188 -0
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.d.ts.map +1 -0
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.js +293 -0
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.js.map +1 -0
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/package.json +25 -0
- package/lib/cli/commands/db/snapshot/capture.mjs +0 -26
- package/lib/cli/renderers/db-snapshot-capture/text.mjs +0 -3
- package/node_modules/@alcalzone/ansi-tokenize/build/consts.d.ts +0 -17
- package/node_modules/@alcalzone/ansi-tokenize/build/consts.js +0 -28
- package/node_modules/@alcalzone/ansi-tokenize/build/consts.js.map +0 -1
- package/node_modules/ink/build/components/AnimationContext.d.ts +0 -9
- package/node_modules/ink/build/components/AnimationContext.js +0 -13
- package/node_modules/ink/build/components/AnimationContext.js.map +0 -1
- package/node_modules/ink/build/hooks/use-animation.d.ts +0 -49
- package/node_modules/ink/build/hooks/use-animation.js +0 -87
- package/node_modules/ink/build/hooks/use-animation.js.map +0 -1
- package/node_modules/ink/build/hooks/use-box-metrics.d.ts +0 -59
- package/node_modules/ink/build/hooks/use-box-metrics.js +0 -88
- package/node_modules/ink/build/hooks/use-box-metrics.js.map +0 -1
- package/node_modules/ink/build/hooks/use-paste.d.ts +0 -35
- package/node_modules/ink/build/hooks/use-paste.js +0 -62
- package/node_modules/ink/build/hooks/use-paste.js.map +0 -1
- package/node_modules/ink/build/hooks/use-window-size.d.ts +0 -18
- package/node_modules/ink/build/hooks/use-window-size.js +0 -22
- package/node_modules/ink/build/hooks/use-window-size.js.map +0 -1
- package/node_modules/wrap-ansi/index.js +0 -468
- /package/node_modules/{wrap-ansi → ink/node_modules/wrap-ansi}/license +0 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { execa } from "execa";
|
|
5
|
+
import { resolveServiceCwd } from "../config/paths.mjs";
|
|
6
|
+
import { buildExecutionEnv } from "../runner/template.mjs";
|
|
7
|
+
import { dumpPostgresSchemaToFile, captureTemplateSnapshotText, runTemplateStep, sanitizeSnapshotText } from "./template-steps.mjs";
|
|
8
|
+
|
|
9
|
+
const SOURCE_SCHEMA_DIR = path.join(".testkit", "db");
|
|
10
|
+
const SOURCE_SCHEMA_FILE = "source-schema.sql";
|
|
11
|
+
const SOURCE_SCHEMA_META_FILE = "source-schema.meta.json";
|
|
12
|
+
|
|
13
|
+
export function getSourceSchemaCachePath(config) {
|
|
14
|
+
const configured = config.testkit.database?.sourceSchema?.cachePath || null;
|
|
15
|
+
return configured
|
|
16
|
+
? resolveServiceCwd(config.productDir, configured)
|
|
17
|
+
: path.join(config.productDir, SOURCE_SCHEMA_DIR, config.name, SOURCE_SCHEMA_FILE);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getSourceSchemaMetadataPath(config) {
|
|
21
|
+
const cachePath = getSourceSchemaCachePath(config);
|
|
22
|
+
return path.join(path.dirname(cachePath), SOURCE_SCHEMA_META_FILE);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolveSourceSchemaState(config, options = {}) {
|
|
26
|
+
const sourceSchema = config.testkit.database?.sourceSchema || null;
|
|
27
|
+
if (!sourceSchema) return { active: false };
|
|
28
|
+
|
|
29
|
+
const cachePath = getSourceSchemaCachePath(config);
|
|
30
|
+
const cacheExists = fs.existsSync(cachePath);
|
|
31
|
+
const env = buildExecutionEnv(config, {}, options.env || process.env);
|
|
32
|
+
const envNames = sourceSchema.kind === "env"
|
|
33
|
+
? [sourceSchema.env]
|
|
34
|
+
: [serviceSourceSchemaEnvName(config.name), "TESTKIT_SCHEMA_SOURCE_DATABASE_URL"];
|
|
35
|
+
const envName = envNames.find((name) => typeof env[name] === "string" && env[name].trim().length > 0) || null;
|
|
36
|
+
const sourceUrl = envName ? env[envName].trim() : null;
|
|
37
|
+
const explicit = sourceSchema.kind === "env";
|
|
38
|
+
const active = explicit || Boolean(sourceUrl) || cacheExists;
|
|
39
|
+
if (!active) return { active: false, cachePath };
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
active: true,
|
|
43
|
+
cachePath,
|
|
44
|
+
metadataPath: getSourceSchemaMetadataPath(config),
|
|
45
|
+
envName,
|
|
46
|
+
sourceUrl,
|
|
47
|
+
sourceSchema,
|
|
48
|
+
cacheExists,
|
|
49
|
+
refreshed: false,
|
|
50
|
+
unavailableMode: resolveUnavailableMode(sourceSchema.unavailable, options.env || process.env),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function prepareSourceSchemaCache(config, options = {}) {
|
|
55
|
+
const state = resolveSourceSchemaState(config, options);
|
|
56
|
+
if (!state.active) return state;
|
|
57
|
+
|
|
58
|
+
if (state.sourceUrl) {
|
|
59
|
+
if (shouldRefreshSourceSchema(state)) {
|
|
60
|
+
await refreshSourceSchemaCache(config, state, options);
|
|
61
|
+
return { ...state, refreshed: true, cacheExists: true };
|
|
62
|
+
}
|
|
63
|
+
options.setupRegistry?.recordCached({
|
|
64
|
+
config,
|
|
65
|
+
stage: "source-schema",
|
|
66
|
+
kind: "database-source-schema",
|
|
67
|
+
summary: "source schema cache hit",
|
|
68
|
+
});
|
|
69
|
+
return state;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (state.cacheExists && state.unavailableMode === "warn-cache") {
|
|
73
|
+
options.setupRegistry?.recordCached({
|
|
74
|
+
config,
|
|
75
|
+
stage: "source-schema",
|
|
76
|
+
kind: "database-source-schema",
|
|
77
|
+
summary: "source unavailable; using cached source schema",
|
|
78
|
+
});
|
|
79
|
+
return state;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const envLabel = state.sourceSchema.kind === "env"
|
|
83
|
+
? state.sourceSchema.env
|
|
84
|
+
: `${serviceSourceSchemaEnvName(config.name)} or TESTKIT_SCHEMA_SOURCE_DATABASE_URL`;
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Source schema database URL is unavailable for service "${config.name}". Set ${envLabel}.`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function forceRefreshSourceSchemaCache(config, previousState, options = {}) {
|
|
91
|
+
const state = resolveSourceSchemaState(config, options);
|
|
92
|
+
if (!state.active || !state.sourceUrl) {
|
|
93
|
+
const envLabel = previousState?.envName || previousState?.sourceSchema?.env || "source schema env";
|
|
94
|
+
throw new Error(`Cannot refresh source schema for service "${config.name}" because ${envLabel} is unavailable.`);
|
|
95
|
+
}
|
|
96
|
+
await refreshSourceSchemaCache(config, state, options);
|
|
97
|
+
return { ...state, refreshed: true, cacheExists: true };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function applySourceSchemaCache(config, databaseUrl, state, options = {}) {
|
|
101
|
+
if (!state?.active) return false;
|
|
102
|
+
if (!fs.existsSync(state.cachePath)) {
|
|
103
|
+
throw new Error(`Missing source schema cache for service "${config.name}": ${state.cachePath}`);
|
|
104
|
+
}
|
|
105
|
+
await runTemplateStep(
|
|
106
|
+
config,
|
|
107
|
+
"source-schema",
|
|
108
|
+
{
|
|
109
|
+
kind: "sql-file",
|
|
110
|
+
path: path.relative(config.productDir, state.cachePath).split(path.sep).join("/"),
|
|
111
|
+
cwd: null,
|
|
112
|
+
inputs: [],
|
|
113
|
+
},
|
|
114
|
+
0,
|
|
115
|
+
databaseUrl,
|
|
116
|
+
options
|
|
117
|
+
);
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function verifyLocalSchemaMatchesSource(config, databaseUrl, state, options = {}) {
|
|
122
|
+
if (!state?.active) return { status: "disabled" };
|
|
123
|
+
if (state.sourceSchema.verify === false || options.skipSchemaSourceVerify) {
|
|
124
|
+
options.setupRegistry?.recordCached({
|
|
125
|
+
config,
|
|
126
|
+
stage: "source-schema:verify",
|
|
127
|
+
kind: "database-source-schema",
|
|
128
|
+
summary: "source schema verification skipped",
|
|
129
|
+
});
|
|
130
|
+
return { status: "skipped" };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const sourceSchema = readSourceSchemaCacheText(state.cachePath);
|
|
134
|
+
const localSchema = await captureTemplateSnapshotText(config, databaseUrl, {
|
|
135
|
+
reporter: options.reporter || null,
|
|
136
|
+
});
|
|
137
|
+
if (localSchema === sourceSchema) {
|
|
138
|
+
return { status: "matched" };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const diagnostics = await writeSchemaMismatchDiagnostics(config, state.cachePath, sourceSchema, localSchema);
|
|
142
|
+
return {
|
|
143
|
+
status: "mismatch",
|
|
144
|
+
diagnostics,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function createSourceSchemaMismatchError(config, state, verification) {
|
|
149
|
+
const parts = [
|
|
150
|
+
`Local schema replay does not match source schema for service "${config.name}".`,
|
|
151
|
+
`Source cache: ${path.relative(config.productDir, state.cachePath)}`,
|
|
152
|
+
];
|
|
153
|
+
if (verification?.diagnostics?.diffPath) {
|
|
154
|
+
parts.push(`Diff: ${path.relative(config.productDir, verification.diagnostics.diffPath)}`);
|
|
155
|
+
}
|
|
156
|
+
return new Error(parts.join("\n"));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function appendSourceSchemaCacheToHash(hash, config) {
|
|
160
|
+
const sourceSchema = config.testkit.database?.sourceSchema || null;
|
|
161
|
+
if (!sourceSchema) return;
|
|
162
|
+
const cachePath = getSourceSchemaCachePath(config);
|
|
163
|
+
hash.update(`source-schema-cache:${path.relative(config.productDir, cachePath)}`);
|
|
164
|
+
if (!fs.existsSync(cachePath)) {
|
|
165
|
+
hash.update(":missing");
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const stat = fs.statSync(cachePath);
|
|
169
|
+
hash.update(`:${stat.size}:${stat.mtimeMs}`);
|
|
170
|
+
hash.update(fs.readFileSync(cachePath));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function refreshSourceSchemaCache(config, state, options = {}) {
|
|
174
|
+
const operation = options.setupRegistry?.start({
|
|
175
|
+
config,
|
|
176
|
+
stage: "source-schema:refresh",
|
|
177
|
+
kind: "database-source-schema",
|
|
178
|
+
summary: `source schema refresh: ${path.relative(config.productDir, state.cachePath)}`,
|
|
179
|
+
});
|
|
180
|
+
try {
|
|
181
|
+
await writeSourceSchemaCacheFromUrl(config, state.sourceUrl, state.cachePath, {
|
|
182
|
+
reporter: options.reporter || null,
|
|
183
|
+
logRecord: operation?._logRecord || null,
|
|
184
|
+
});
|
|
185
|
+
writeSourceSchemaMetadata(state.metadataPath, {
|
|
186
|
+
refreshedAt: new Date().toISOString(),
|
|
187
|
+
serviceName: config.name,
|
|
188
|
+
envName: state.envName,
|
|
189
|
+
cachePath: path.relative(config.productDir, state.cachePath),
|
|
190
|
+
});
|
|
191
|
+
const finished = operation
|
|
192
|
+
? options.setupRegistry.finish(operation, {
|
|
193
|
+
status: "passed",
|
|
194
|
+
summary: `source schema refresh: ${path.relative(config.productDir, state.cachePath)}`,
|
|
195
|
+
})
|
|
196
|
+
: null;
|
|
197
|
+
if (finished) options.reporter?.setupOperationFinished?.(finished);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
const finished = operation
|
|
200
|
+
? options.setupRegistry.finish(operation, {
|
|
201
|
+
status: "failed",
|
|
202
|
+
summary: `source schema refresh: ${path.relative(config.productDir, state.cachePath)}`,
|
|
203
|
+
error: error?.message || error,
|
|
204
|
+
})
|
|
205
|
+
: null;
|
|
206
|
+
if (finished) options.reporter?.setupOperationFinished?.(finished);
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function writeSourceSchemaCacheFromUrl(config, sourceUrl, cachePath, options = {}) {
|
|
212
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
213
|
+
const tempDir = fs.mkdtempSync(path.join(path.dirname(cachePath), ".source-schema-"));
|
|
214
|
+
const tempPath = path.join(tempDir, path.basename(cachePath));
|
|
215
|
+
try {
|
|
216
|
+
await dumpPostgresSchemaToFile(config, tempPath, sourceUrl, options);
|
|
217
|
+
fs.renameSync(tempPath, cachePath);
|
|
218
|
+
} finally {
|
|
219
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function readSourceSchemaCacheText(cachePath) {
|
|
224
|
+
return sanitizeSnapshotText(fs.readFileSync(cachePath, "utf8"));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function shouldRefreshSourceSchema(state) {
|
|
228
|
+
if (!state.cacheExists) return true;
|
|
229
|
+
if (state.sourceSchema.refresh?.mode === "ttl") {
|
|
230
|
+
const meta = readJson(state.metadataPath);
|
|
231
|
+
const refreshedAt = meta?.refreshedAt ? Date.parse(meta.refreshedAt) : 0;
|
|
232
|
+
if (!Number.isFinite(refreshedAt) || refreshedAt <= 0) return true;
|
|
233
|
+
return Date.now() - refreshedAt >= state.sourceSchema.refresh.ttlSeconds * 1000;
|
|
234
|
+
}
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function writeSourceSchemaMetadata(metadataPath, metadata) {
|
|
239
|
+
fs.mkdirSync(path.dirname(metadataPath), { recursive: true });
|
|
240
|
+
fs.writeFileSync(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function readJson(filePath) {
|
|
244
|
+
if (!filePath || !fs.existsSync(filePath)) return null;
|
|
245
|
+
try {
|
|
246
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
247
|
+
} catch {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function writeSchemaMismatchDiagnostics(config, sourceCachePath, sourceSchema, localSchema) {
|
|
253
|
+
const dir = path.join(config.productDir, ".testkit", "results", "schema");
|
|
254
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
255
|
+
const prefix = `${sanitizePathSegment(config.runtimeLabel || config.name)}__${sanitizePathSegment(config.name)}`;
|
|
256
|
+
const sourcePath = path.join(dir, `${prefix}__source-schema.sql`);
|
|
257
|
+
const localPath = path.join(dir, `${prefix}__local-replay-schema.sql`);
|
|
258
|
+
const diffPath = path.join(dir, `${prefix}__schema.diff`);
|
|
259
|
+
fs.writeFileSync(sourcePath, sourceSchema);
|
|
260
|
+
fs.writeFileSync(localPath, localSchema);
|
|
261
|
+
fs.writeFileSync(diffPath, await buildUnifiedDiff(sourcePath, localPath));
|
|
262
|
+
return {
|
|
263
|
+
sourceCachePath,
|
|
264
|
+
sourcePath,
|
|
265
|
+
localPath,
|
|
266
|
+
diffPath,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function buildUnifiedDiff(sourcePath, localPath) {
|
|
271
|
+
const result = await execa("diff", ["-u", sourcePath, localPath], {
|
|
272
|
+
reject: false,
|
|
273
|
+
stdout: "pipe",
|
|
274
|
+
stderr: "pipe",
|
|
275
|
+
}).catch((error) => ({ exitCode: 2, stdout: "", stderr: error.message }));
|
|
276
|
+
if (result.exitCode === 0 || result.exitCode === 1) return result.stdout || "";
|
|
277
|
+
return `Could not generate diff: ${result.stderr || "diff failed"}\n`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function resolveUnavailableMode(value, env) {
|
|
281
|
+
if (value === "fail" || value === "warn-cache") return value;
|
|
282
|
+
return env.CI === "true" ? "fail" : "warn-cache";
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function serviceSourceSchemaEnvName(serviceName) {
|
|
286
|
+
return `TESTKIT_${String(serviceName).toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_SCHEMA_SOURCE_DATABASE_URL`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function sanitizePathSegment(value) {
|
|
290
|
+
return String(value)
|
|
291
|
+
.trim()
|
|
292
|
+
.toLowerCase()
|
|
293
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
294
|
+
.replace(/^-+|-+$/g, "") || "schema";
|
|
295
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
2
3
|
import path from "path";
|
|
3
4
|
import { execa } from "execa";
|
|
4
5
|
import { buildTemplateExecutionEnv } from "../runner/template.mjs";
|
|
@@ -25,6 +26,25 @@ export async function runTemplateStage(config, stageName, databaseUrl, options =
|
|
|
25
26
|
reporter: options.reporter || null,
|
|
26
27
|
setupRegistry: options.setupRegistry || null,
|
|
27
28
|
parentOperation: options.parentOperation || null,
|
|
29
|
+
afterStep: options.afterStep || null,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function runTemplateStep(config, stageName, step, stepIndex, databaseUrl, options = {}) {
|
|
34
|
+
const env = {
|
|
35
|
+
...buildTemplateExecutionEnv(config, {}, process.env),
|
|
36
|
+
DATABASE_URL: databaseUrl,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
await runConfiguredSteps({
|
|
40
|
+
config,
|
|
41
|
+
steps: [step],
|
|
42
|
+
env,
|
|
43
|
+
labelPrefix: `template:${stageName}`,
|
|
44
|
+
reporter: options.reporter || null,
|
|
45
|
+
setupRegistry: options.setupRegistry || null,
|
|
46
|
+
parentOperation: options.parentOperation || null,
|
|
47
|
+
startIndex: stepIndex,
|
|
28
48
|
});
|
|
29
49
|
}
|
|
30
50
|
|
|
@@ -36,45 +56,95 @@ export function collectTemplateInputs(productDir, template = {}) {
|
|
|
36
56
|
});
|
|
37
57
|
}
|
|
38
58
|
|
|
39
|
-
export async function
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
59
|
+
export async function captureTemplateSnapshotText(config, databaseUrl, options = {}) {
|
|
60
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-schema-snapshot-"));
|
|
61
|
+
const tempPath = path.join(tempDir, "schema.sql");
|
|
62
|
+
try {
|
|
63
|
+
await dumpPostgresSchemaToFile(config, tempPath, databaseUrl, options);
|
|
64
|
+
return fs.readFileSync(tempPath, "utf8");
|
|
65
|
+
} finally {
|
|
66
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
43
69
|
|
|
44
|
-
|
|
45
|
-
|
|
70
|
+
export async function dumpPostgresSchemaToFile(config, outputPath, databaseUrl, options = {}) {
|
|
71
|
+
const env = {
|
|
72
|
+
...buildTemplateExecutionEnv(config, {}, process.env),
|
|
73
|
+
...buildPostgresConnectionEnv(databaseUrl),
|
|
74
|
+
};
|
|
75
|
+
const result = await runPgDumpCommand(config, "pg_dump", pgDumpArgs(), env, options);
|
|
76
|
+
if (result.exitCode !== 0 && isPgDumpServerVersionMismatch(result)) {
|
|
77
|
+
const serverMajor = parsePgDumpServerMajor(result);
|
|
78
|
+
if (serverMajor) {
|
|
79
|
+
const fallback = await runDockerizedPgDump(config, serverMajor, env, options);
|
|
80
|
+
if (fallback.exitCode === 0) {
|
|
81
|
+
fs.writeFileSync(outputPath, fallback.stdout);
|
|
82
|
+
sanitizeSnapshotFile(outputPath);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
throw new Error(fallback.shortMessage || fallback.stderr || fallback.stdout || "dockerized pg_dump failed");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (result.exitCode !== 0) {
|
|
89
|
+
throw new Error(result.shortMessage || result.stderr || result.stdout || "pg_dump failed");
|
|
90
|
+
}
|
|
91
|
+
fs.writeFileSync(outputPath, result.stdout);
|
|
92
|
+
sanitizeSnapshotFile(outputPath);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function pgDumpArgs() {
|
|
96
|
+
return [
|
|
97
|
+
"--schema-only",
|
|
98
|
+
"--no-owner",
|
|
99
|
+
"--no-privileges",
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function runDockerizedPgDump(config, serverMajor, env, options) {
|
|
104
|
+
const image = `${process.env.TESTKIT_PG_DUMP_IMAGE_PREFIX || "postgres"}:${serverMajor}`;
|
|
105
|
+
return runPgDumpCommand(
|
|
106
|
+
config,
|
|
107
|
+
"docker",
|
|
46
108
|
[
|
|
47
|
-
"
|
|
48
|
-
"--
|
|
49
|
-
"--
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
109
|
+
"run",
|
|
110
|
+
"--rm",
|
|
111
|
+
"--network",
|
|
112
|
+
"host",
|
|
113
|
+
"-e",
|
|
114
|
+
"PGHOST",
|
|
115
|
+
"-e",
|
|
116
|
+
"PGPORT",
|
|
117
|
+
"-e",
|
|
118
|
+
"PGDATABASE",
|
|
119
|
+
"-e",
|
|
120
|
+
"PGUSER",
|
|
121
|
+
"-e",
|
|
122
|
+
"PGPASSWORD",
|
|
123
|
+
"-e",
|
|
124
|
+
"PGSSLMODE",
|
|
125
|
+
image,
|
|
126
|
+
"pg_dump",
|
|
127
|
+
...pgDumpArgs(),
|
|
53
128
|
],
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
env: {
|
|
57
|
-
...buildTemplateExecutionEnv(config, {}, process.env),
|
|
58
|
-
DATABASE_URL: templateDbUrl,
|
|
59
|
-
},
|
|
60
|
-
stdout: "pipe",
|
|
61
|
-
stderr: "pipe",
|
|
62
|
-
reject: false,
|
|
63
|
-
}
|
|
129
|
+
env,
|
|
130
|
+
options
|
|
64
131
|
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function runPgDumpCommand(config, command, args, env, options = {}) {
|
|
135
|
+
const child = execa(command, args, {
|
|
136
|
+
cwd: config.productDir,
|
|
137
|
+
env,
|
|
138
|
+
stdout: "pipe",
|
|
139
|
+
stderr: "pipe",
|
|
140
|
+
reject: false,
|
|
141
|
+
});
|
|
65
142
|
const liveWriter =
|
|
66
143
|
options.reporter?.outputMode === "debug"
|
|
67
144
|
? (line) => options.reporter.writeDebugLine?.(line)
|
|
68
145
|
: null;
|
|
69
146
|
const logRecord = options.logRecord || null;
|
|
70
147
|
const drains = [
|
|
71
|
-
captureOutput(child.stdout, {
|
|
72
|
-
livePrefix: `[${config.runtimeLabel || config.name}:${config.name}]`,
|
|
73
|
-
liveWriter,
|
|
74
|
-
onLine(line) {
|
|
75
|
-
if (logRecord) logRecord.stream.write(`${new Date().toISOString()} [stdout] ${line}\n`);
|
|
76
|
-
},
|
|
77
|
-
}),
|
|
78
148
|
captureOutput(child.stderr, {
|
|
79
149
|
livePrefix: `[${config.runtimeLabel || config.name}:${config.name}]`,
|
|
80
150
|
liveWriter,
|
|
@@ -85,29 +155,79 @@ export async function captureTemplateSnapshot(config, outputPath, databaseUrl, o
|
|
|
85
155
|
];
|
|
86
156
|
const result = await child;
|
|
87
157
|
await Promise.all(drains);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function isPgDumpServerVersionMismatch(result) {
|
|
162
|
+
const text = `${result.stderr || ""}\n${result.stdout || ""}`;
|
|
163
|
+
return text.includes("server version mismatch");
|
|
164
|
+
}
|
|
91
165
|
|
|
92
|
-
|
|
93
|
-
|
|
166
|
+
function parsePgDumpServerMajor(result) {
|
|
167
|
+
const text = `${result.stderr || ""}\n${result.stdout || ""}`;
|
|
168
|
+
const match = text.match(/server version:\s*([0-9]+)/i);
|
|
169
|
+
return match ? Number(match[1]) : null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function buildPostgresConnectionEnv(databaseUrl) {
|
|
173
|
+
const parsed = new URL(databaseUrl);
|
|
174
|
+
return compactObject({
|
|
175
|
+
PGHOST: parsed.hostname,
|
|
176
|
+
PGPORT: parsed.port || "5432",
|
|
177
|
+
PGDATABASE: decodeURIComponent(parsed.pathname.replace(/^\//, "")),
|
|
178
|
+
PGUSER: decodeURIComponent(parsed.username || ""),
|
|
179
|
+
PGPASSWORD: decodeURIComponent(parsed.password || ""),
|
|
180
|
+
PGSSLMODE: parsed.searchParams.get("sslmode") || undefined,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function compactObject(value) {
|
|
185
|
+
return Object.fromEntries(
|
|
186
|
+
Object.entries(value).filter(([_key, entry]) => entry !== undefined && entry !== null && entry !== "")
|
|
187
|
+
);
|
|
94
188
|
}
|
|
95
189
|
|
|
96
190
|
export function sanitizeSnapshotFile(filePath) {
|
|
97
191
|
const dump = fs.readFileSync(filePath, "utf8");
|
|
98
|
-
const sanitized = dump
|
|
99
|
-
|
|
192
|
+
const sanitized = sanitizeSnapshotText(dump);
|
|
193
|
+
|
|
194
|
+
if (sanitized !== dump) {
|
|
195
|
+
fs.writeFileSync(filePath, sanitized);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function sanitizeSnapshotText(dump) {
|
|
200
|
+
return removePublicSchemaInitdbBlock(String(dump).split("\n"))
|
|
100
201
|
.filter((line) => {
|
|
101
202
|
const trimmed = line.trim();
|
|
102
203
|
return (
|
|
204
|
+
!trimmed.startsWith("-- Dumped from database version ") &&
|
|
205
|
+
!trimmed.startsWith("-- Dumped by pg_dump version ") &&
|
|
103
206
|
trimmed !== "SET transaction_timeout = 0;" &&
|
|
104
207
|
!trimmed.startsWith("\\restrict ") &&
|
|
105
208
|
!trimmed.startsWith("\\unrestrict ")
|
|
106
209
|
);
|
|
107
210
|
})
|
|
108
211
|
.join("\n");
|
|
212
|
+
}
|
|
109
213
|
|
|
110
|
-
|
|
111
|
-
|
|
214
|
+
function removePublicSchemaInitdbBlock(lines) {
|
|
215
|
+
const normalized = [];
|
|
216
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
217
|
+
if (lines[index]?.trim() === "--" && lines[index + 1]?.startsWith("-- Name: public; Type: SCHEMA;")) {
|
|
218
|
+
let cursor = index + 1;
|
|
219
|
+
while (cursor < lines.length && !lines[cursor]?.includes("*not* creating schema")) {
|
|
220
|
+
cursor += 1;
|
|
221
|
+
}
|
|
222
|
+
if (cursor < lines.length) {
|
|
223
|
+
while (cursor + 1 < lines.length && lines[cursor + 1]?.trim() === "") {
|
|
224
|
+
cursor += 1;
|
|
225
|
+
}
|
|
226
|
+
index = cursor;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
normalized.push(lines[index]);
|
|
112
231
|
}
|
|
232
|
+
return normalized;
|
|
113
233
|
}
|
|
@@ -146,9 +146,10 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
146
146
|
runtimeOptions: {
|
|
147
147
|
reporter,
|
|
148
148
|
logRegistry,
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
149
|
+
setupRegistry,
|
|
150
|
+
skipSchemaSourceVerify: opts.skipSchemaSourceVerify,
|
|
151
|
+
},
|
|
152
|
+
});
|
|
152
153
|
const timingUpdates = [];
|
|
153
154
|
|
|
154
155
|
try {
|
|
@@ -51,13 +51,16 @@ export async function runConfiguredSteps({
|
|
|
51
51
|
reporter = null,
|
|
52
52
|
setupRegistry = null,
|
|
53
53
|
parentOperation = null,
|
|
54
|
+
startIndex = 0,
|
|
55
|
+
afterStep = null,
|
|
54
56
|
}) {
|
|
55
57
|
if (steps.length === 0) return;
|
|
56
58
|
const resolvedToolchain = await resolveConfiguredToolchain(config);
|
|
57
59
|
await announceResolvedToolchain(config, resolvedToolchain, reporter);
|
|
58
60
|
|
|
59
61
|
for (const [index, step] of steps.entries()) {
|
|
60
|
-
const
|
|
62
|
+
const stepNumber = startIndex + index + 1;
|
|
63
|
+
const label = `${labelPrefix}:${config.name}:${stepNumber}`;
|
|
61
64
|
const stepOperation = setupRegistry?.start({
|
|
62
65
|
config,
|
|
63
66
|
stage: label,
|
|
@@ -89,6 +92,14 @@ export async function runConfiguredSteps({
|
|
|
89
92
|
if (finished) reporter?.setupOperationFinished?.(finished);
|
|
90
93
|
throw error;
|
|
91
94
|
}
|
|
95
|
+
if (afterStep) {
|
|
96
|
+
await afterStep({
|
|
97
|
+
step,
|
|
98
|
+
index: startIndex + index,
|
|
99
|
+
stepNumber,
|
|
100
|
+
label,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
92
103
|
}
|
|
93
104
|
}
|
|
94
105
|
|
package/lib/runner/template.mjs
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import path from "path";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
finalizeConfiguredInputs,
|
|
4
|
+
finalizeConfiguredSteps,
|
|
5
|
+
} from "../shared/configured-steps.mjs";
|
|
3
6
|
import { readDatabaseInfo } from "./state-io.mjs";
|
|
4
7
|
|
|
5
8
|
const PORT_STRIDE = 100;
|
|
@@ -135,6 +138,7 @@ export function resolveRuntimeConfig(
|
|
|
135
138
|
const database = config.testkit.database
|
|
136
139
|
? {
|
|
137
140
|
...config.testkit.database,
|
|
141
|
+
sourceSchema: finalizeSourceSchema(config.testkit.database.sourceSchema, context),
|
|
138
142
|
template: finalizeDatabaseTemplate(config.testkit.database.template, context),
|
|
139
143
|
}
|
|
140
144
|
: undefined;
|
|
@@ -193,6 +197,17 @@ function finalizeDatabaseTemplate(template, context) {
|
|
|
193
197
|
};
|
|
194
198
|
}
|
|
195
199
|
|
|
200
|
+
function finalizeSourceSchema(sourceSchema, context) {
|
|
201
|
+
if (!sourceSchema) return null;
|
|
202
|
+
return {
|
|
203
|
+
...sourceSchema,
|
|
204
|
+
...(typeof sourceSchema.env === "string" ? { env: finalizeString(sourceSchema.env, context) } : {}),
|
|
205
|
+
...(typeof sourceSchema.cachePath === "string"
|
|
206
|
+
? { cachePath: finalizeString(sourceSchema.cachePath, context) }
|
|
207
|
+
: {}),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
196
211
|
export function resolveServiceStateDir(runtimeDir, config) {
|
|
197
212
|
return path.join(runtimeDir, "services", config.name);
|
|
198
213
|
}
|
|
@@ -231,11 +231,6 @@ This automatically figures out the least amount of escape codes necessary to ach
|
|
|
231
231
|
Placeholder for next release:
|
|
232
232
|
### __WORK IN PROGRESS__
|
|
233
233
|
-->
|
|
234
|
-
### 0.3.0 (2026-02-20)
|
|
235
|
-
|
|
236
|
-
- Fix: preserve non-hyperlink OSC sequences when tokenizing (#54)
|
|
237
|
-
- Fix: support ST-terminated OSC hyperlinks (#53)
|
|
238
|
-
|
|
239
234
|
### 0.2.5 (2026-02-11)
|
|
240
235
|
|
|
241
236
|
- Fix: preserve grapheme clusters when tokenizing (#51)
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { AnsiCode } from "./tokenize.js";
|
|
2
|
+
export declare const ESCAPES: Set<number>;
|
|
3
|
+
export declare const CSI: number;
|
|
4
|
+
export declare const OSC: number;
|
|
2
5
|
export declare const endCodesSet: Set<string>;
|
|
6
|
+
export declare const linkCodePrefix = "\u001B]8;";
|
|
7
|
+
export declare const linkCodePrefixCharCodes: number[];
|
|
8
|
+
export declare const linkCodeSuffix = "\u0007";
|
|
9
|
+
export declare const linkCodeSuffixCharCode: number;
|
|
10
|
+
export declare const linkEndCode: string;
|
|
3
11
|
export declare function getLinkStartCode(url: string, params?: Record<string, string>): string;
|
|
4
12
|
export declare function getEndCode(code: string): string;
|
|
5
13
|
export declare function ansiCodesToString(codes: AnsiCode[]): string;
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import ansiStyles from "ansi-styles";
|
|
2
|
-
|
|
2
|
+
export const ESCAPES = new Set([27, 155]); // \x1b and \x9b
|
|
3
|
+
export const CSI = "[".codePointAt(0);
|
|
4
|
+
export const OSC = "]".codePointAt(0);
|
|
3
5
|
export const endCodesSet = new Set();
|
|
4
6
|
const endCodesMap = new Map();
|
|
5
7
|
for (const [start, end] of ansiStyles.codes) {
|
|
6
8
|
endCodesSet.add(ansiStyles.color.ansi(end));
|
|
7
9
|
endCodesMap.set(ansiStyles.color.ansi(start), ansiStyles.color.ansi(end));
|
|
8
10
|
}
|
|
11
|
+
export const linkCodePrefix = "\x1B]8;"; // OSC 8 link prefix (params and URL follow)
|
|
12
|
+
export const linkCodePrefixCharCodes = linkCodePrefix.split("").map((char) => char.charCodeAt(0));
|
|
13
|
+
export const linkCodeSuffix = "\x07";
|
|
14
|
+
export const linkCodeSuffixCharCode = linkCodeSuffix.charCodeAt(0);
|
|
15
|
+
export const linkEndCode = `\x1B]8;;${linkCodeSuffix}`;
|
|
9
16
|
export function getLinkStartCode(url, params) {
|
|
10
17
|
const paramsStr = params
|
|
11
18
|
? Object.entries(params)
|
|
@@ -21,13 +28,8 @@ export function getEndCode(code) {
|
|
|
21
28
|
return endCodesMap.get(code);
|
|
22
29
|
// We have a few special cases to handle here:
|
|
23
30
|
// Links:
|
|
24
|
-
if (code.startsWith(linkCodePrefix))
|
|
25
|
-
|
|
26
|
-
return linkEndCodeST;
|
|
27
|
-
if (code.endsWith("\x9C"))
|
|
28
|
-
return linkEndCodeC1ST;
|
|
29
|
-
return linkEndCode; // BEL (\x07)
|
|
30
|
-
}
|
|
31
|
+
if (code.startsWith(linkCodePrefix))
|
|
32
|
+
return linkEndCode;
|
|
31
33
|
code = code.slice(2);
|
|
32
34
|
// 8-bit/24-bit colors:
|
|
33
35
|
if (code.startsWith("38")) {
|