@pithy-sh/core 0.8.0 → 0.10.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/dist/asyncapi/document.d.ts +46 -0
- package/dist/asyncapi/document.d.ts.map +1 -0
- package/dist/asyncapi/document.js +115 -0
- package/dist/capability/capability.d.ts +7 -0
- package/dist/capability/capability.d.ts.map +1 -1
- package/dist/capability/composition.d.ts.map +1 -1
- package/dist/capability/composition.js +5 -1
- package/dist/capability/settings.d.ts +10 -2
- package/dist/capability/settings.d.ts.map +1 -1
- package/dist/capability/settings.js +5 -3
- package/dist/cli/command.d.ts +364 -0
- package/dist/cli/command.d.ts.map +1 -0
- package/dist/cli/command.js +418 -0
- package/dist/cli/commandCatalog.generated.d.ts +2275 -0
- package/dist/cli/commandCatalog.generated.d.ts.map +1 -0
- package/dist/cli/commandCatalog.generated.js +2390 -0
- package/dist/cli/commandFacts.d.ts +164 -0
- package/dist/cli/commandFacts.d.ts.map +1 -0
- package/dist/cli/commandFacts.js +215 -0
- package/dist/controlPlane/error/errors.d.ts.map +1 -1
- package/dist/controlPlane/error/errors.js +4 -3
- package/dist/controlPlane/http/guard.d.ts.map +1 -1
- package/dist/controlPlane/http/guard.js +11 -1
- package/dist/controlPlane/http/routes.d.ts +1 -1
- package/dist/controlPlane/http/routes.d.ts.map +1 -1
- package/dist/controlPlane/http/routes.js +34 -7
- package/dist/createBackend.d.ts +7 -0
- package/dist/createBackend.d.ts.map +1 -1
- package/dist/createBackend.js +34 -4
- package/dist/entitlement/require.js +1 -1
- package/dist/error/cause.d.ts +16 -116
- package/dist/error/cause.d.ts.map +1 -1
- package/dist/error/cause.js +19 -153
- package/dist/error/http.d.ts +40 -0
- package/dist/error/http.d.ts.map +1 -1
- package/dist/error/payload.d.ts +80 -0
- package/dist/error/payload.d.ts.map +1 -1
- package/dist/error/payload.js +26 -0
- package/dist/error/pithyError.d.ts +44 -0
- package/dist/error/pithyError.d.ts.map +1 -1
- package/dist/error/pithyError.js +56 -1
- package/dist/error/unresolved.d.ts +265 -0
- package/dist/error/unresolved.d.ts.map +1 -0
- package/dist/error/unresolved.js +599 -0
- package/dist/error/wire.d.ts +20 -0
- package/dist/error/wire.d.ts.map +1 -0
- package/dist/error/wire.js +19 -0
- package/dist/http/oidcWebhook.d.ts +3 -3
- package/dist/http/oidcWebhook.js +3 -3
- package/dist/http/routeContract.js +1 -1
- package/dist/http/routeSpec.d.ts +66 -0
- package/dist/http/routeSpec.d.ts.map +1 -0
- package/dist/http/routeSpec.js +40 -0
- package/dist/http/sameOrigin.d.ts +1 -1
- package/dist/http/sameOrigin.js +1 -1
- package/dist/http/signedWebhook.d.ts +3 -3
- package/dist/http/signedWebhook.js +3 -3
- package/dist/http/socket.d.ts +52 -0
- package/dist/http/socket.d.ts.map +1 -0
- package/dist/http/socket.js +2 -0
- package/dist/http/subprotocol.d.ts +29 -0
- package/dist/http/subprotocol.d.ts.map +1 -0
- package/dist/http/subprotocol.js +37 -0
- package/dist/http/validation.d.ts +40 -8
- package/dist/http/validation.d.ts.map +1 -1
- package/dist/http/validation.js +25 -1
- package/dist/index.js +2 -2
- package/dist/logger/record.d.ts +23 -0
- package/dist/logger/record.d.ts.map +1 -1
- package/dist/migrations/bookkeeping.d.ts +21 -0
- package/dist/migrations/bookkeeping.d.ts.map +1 -1
- package/dist/migrations/bookkeeping.js +32 -3
- package/dist/migrations/groups.d.ts +140 -0
- package/dist/migrations/groups.d.ts.map +1 -0
- package/dist/migrations/groups.js +191 -0
- package/dist/migrations/migrateStep.d.ts +13 -0
- package/dist/migrations/migrateStep.d.ts.map +1 -0
- package/dist/migrations/migrateStep.js +20 -0
- package/dist/migrations/owner.d.ts +7 -0
- package/dist/migrations/owner.d.ts.map +1 -1
- package/dist/migrations/owner.js +11 -7
- package/dist/migrations/retained.js +2 -2
- package/dist/migrations/runner.d.ts +76 -5
- package/dist/migrations/runner.d.ts.map +1 -1
- package/dist/migrations/runner.js +130 -19
- package/dist/naming/domains.d.ts +30 -0
- package/dist/naming/domains.d.ts.map +1 -1
- package/dist/naming/domains.js +83 -4
- package/dist/naming/environment.d.ts.map +1 -1
- package/dist/naming/environment.js +2 -1
- package/dist/naming/packageName.d.ts +17 -0
- package/dist/naming/packageName.d.ts.map +1 -0
- package/dist/naming/packageName.js +22 -0
- package/dist/naming/provisionScope.d.ts.map +1 -1
- package/dist/naming/provisionScope.js +2 -1
- package/dist/openapi/document.d.ts +106 -0
- package/dist/openapi/document.d.ts.map +1 -0
- package/dist/openapi/document.js +314 -0
- package/dist/openapi/routeGroups.d.ts +8 -0
- package/dist/openapi/routeGroups.d.ts.map +1 -0
- package/dist/openapi/routeGroups.js +23 -0
- package/dist/seed/devLogin.d.ts +33 -0
- package/dist/seed/devLogin.d.ts.map +1 -1
- package/dist/seed/devLogin.js +28 -1
- package/dist/text/comments.js +66 -4
- package/dist/text/literals.d.ts +43 -0
- package/dist/text/literals.d.ts.map +1 -0
- package/dist/text/literals.js +293 -0
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.d.ts.map +1 -1
- package/dist/version.generated.js +1 -1
- package/dist/workflow/dispatchRoute.d.ts.map +1 -1
- package/dist/workflow/dispatchRoute.js +12 -6
- package/dist/workflow/faults.d.ts.map +1 -1
- package/dist/workflow/faults.js +2 -3
- package/dist/workflow/hostEnv.d.ts +1 -0
- package/dist/workflow/hostEnv.d.ts.map +1 -1
- package/dist/workflow/hostEnv.js +4 -2
- package/dist/workflow/loopback.d.ts.map +1 -1
- package/dist/workflow/loopback.js +2 -1
- package/package.json +3 -3
- package/src/asyncapi/document.ts +164 -0
- package/src/capability/capability.ts +7 -0
- package/src/capability/composition.ts +2 -1
- package/src/capability/settings.ts +19 -2
- package/src/cli/command.ts +865 -0
- package/src/cli/commandCatalog.generated.ts +2533 -0
- package/src/cli/commandFacts.ts +219 -0
- package/src/controlPlane/error/errors.ts +10 -3
- package/src/controlPlane/http/guard.ts +14 -2
- package/src/controlPlane/http/routes.ts +57 -9
- package/src/createBackend.ts +35 -2
- package/src/entitlement/require.ts +1 -1
- package/src/error/cause.ts +18 -181
- package/src/error/payload.ts +59 -0
- package/src/error/pithyError.ts +76 -0
- package/src/error/unresolved.ts +853 -0
- package/src/error/wire.ts +40 -0
- package/src/http/oidcWebhook.ts +3 -3
- package/src/http/routeContract.ts +1 -1
- package/src/http/routeSpec.ts +81 -0
- package/src/http/sameOrigin.ts +1 -1
- package/src/http/signedWebhook.ts +3 -3
- package/src/http/socket.ts +55 -0
- package/src/http/subprotocol.ts +47 -0
- package/src/http/validation.ts +50 -8
- package/src/migrations/bookkeeping.ts +39 -2
- package/src/migrations/groups.ts +295 -0
- package/src/migrations/migrateStep.ts +19 -0
- package/src/migrations/owner.ts +10 -6
- package/src/migrations/retained.ts +2 -2
- package/src/migrations/runner.ts +214 -22
- package/src/naming/domains.ts +97 -16
- package/src/naming/environment.ts +2 -1
- package/src/naming/packageName.ts +20 -0
- package/src/naming/provisionScope.ts +2 -1
- package/src/openapi/document.ts +412 -0
- package/src/openapi/routeGroups.ts +25 -0
- package/src/seed/devLogin.ts +31 -0
- package/src/text/comments.ts +56 -4
- package/src/text/literals.ts +294 -0
- package/src/version.generated.ts +1 -1
- package/src/workflow/dispatchRoute.ts +13 -6
- package/src/workflow/faults.ts +5 -3
- package/src/workflow/hostEnv.ts +9 -1
- package/src/workflow/loopback.ts +2 -1
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* **Every string a source file writes, as it reads at run time near enough to scan (#711)** — each quoted or
|
|
6
|
+
* template literal's text, quotes off, escapes resolved to the character they stand for, and every `${…}` blanked
|
|
7
|
+
* to `<x>`.
|
|
8
|
+
*
|
|
9
|
+
* Gates that read what the code *says* — `packages/cli/src/ci/runnableCommands.test.ts` reads every `pithy …` a
|
|
10
|
+
* line names — used one regular expression for "a string or template literal", and it has a measured false
|
|
11
|
+
* reading: a template whose interpolation holds a template of its own, `` `${path}${detail ? ` — ${detail}` :
|
|
12
|
+
* ""}` ``, closed at the inner backtick, and everything after it was read as a literal that opened there. The
|
|
13
|
+
* scan then reported text the file does not contain, and missed the text it does. So this walks the source and
|
|
14
|
+
* keeps a stack: a `${` opens an expression, whose own literals are read as literals in their turn, and its
|
|
15
|
+
* matching `}` resumes the template it interrupted.
|
|
16
|
+
*
|
|
17
|
+
* Comments must be blanked first (`./comments`), as every caller here already does. A regex literal is stepped
|
|
18
|
+
* over by the same rule `./comments` uses — a `/` where a value may begin opens one — so a quote inside a pattern
|
|
19
|
+
* does not open a string.
|
|
20
|
+
*/
|
|
21
|
+
export function literalTexts(source: string, options: LiteralOptions = {}): string[] {
|
|
22
|
+
return literalSpans(source, options).map((span) => span.text);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** How a source is read. */
|
|
26
|
+
export interface LiteralOptions {
|
|
27
|
+
/**
|
|
28
|
+
* The source is a `.tsx` file: an element's text and its attribute strings are read as literals too, and a
|
|
29
|
+
* `<` where a value may begin opens an element rather than comparing. A JSX text run is read with its
|
|
30
|
+
* whitespace collapsed, as React renders it; a `{…}` child is `<x>`.
|
|
31
|
+
*/
|
|
32
|
+
readonly jsx?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** One literal: its text as {@link literalTexts} reads it, and the source range it spans, delimiters included. */
|
|
36
|
+
export interface LiteralSpan {
|
|
37
|
+
readonly text: string;
|
|
38
|
+
readonly start: number;
|
|
39
|
+
readonly end: number;
|
|
40
|
+
/** A template's interpolations, each the range of the code between `${` and `}`. Absent for any other literal. */
|
|
41
|
+
readonly holes?: readonly (readonly [number, number])[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* {@link literalTexts}, with where each one sits — so a gate can read what is around a literal: a `+` beside it, or
|
|
46
|
+
* the array it is an element of being joined.
|
|
47
|
+
*/
|
|
48
|
+
export function literalSpans(source: string, options: LiteralOptions = {}): LiteralSpan[] {
|
|
49
|
+
const found: LiteralSpan[] = [];
|
|
50
|
+
walk(source, 0, found, false, options.jsx === true);
|
|
51
|
+
return found;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** After one of these, a `/` opens a regex literal; after anything else it divides. */
|
|
55
|
+
const VALUE_POSITION = new Set([
|
|
56
|
+
"",
|
|
57
|
+
"(",
|
|
58
|
+
",",
|
|
59
|
+
"=",
|
|
60
|
+
":",
|
|
61
|
+
"[",
|
|
62
|
+
"!",
|
|
63
|
+
"&",
|
|
64
|
+
"|",
|
|
65
|
+
"?",
|
|
66
|
+
"{",
|
|
67
|
+
"}",
|
|
68
|
+
";",
|
|
69
|
+
"+",
|
|
70
|
+
"-",
|
|
71
|
+
"*",
|
|
72
|
+
"%",
|
|
73
|
+
"^",
|
|
74
|
+
"~",
|
|
75
|
+
"<",
|
|
76
|
+
">",
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Walk code from `start`, pushing every literal it meets. Inside an interpolation (`nested`), an unmatched `}`
|
|
81
|
+
* closes it: the walk returns the index just past that brace.
|
|
82
|
+
*/
|
|
83
|
+
function walk(source: string, start: number, found: LiteralSpan[], nested: boolean, jsx: boolean): number {
|
|
84
|
+
let depth = 0;
|
|
85
|
+
let previous = "";
|
|
86
|
+
let index = start;
|
|
87
|
+
while (index < source.length) {
|
|
88
|
+
const char = source[index] as string;
|
|
89
|
+
if (char === '"' || char === "'") {
|
|
90
|
+
index = quoted(source, index, char, found);
|
|
91
|
+
previous = char;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (char === "`") {
|
|
95
|
+
index = template(source, index, found, jsx);
|
|
96
|
+
previous = char;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (jsx && char === "<" && opensElement(source, index, previous)) {
|
|
100
|
+
// An element is read whole, or — when it does not parse as one, as a generic arrow `<T,>` does not — the
|
|
101
|
+
// `<` is code after all, and what it read is put back.
|
|
102
|
+
const mark = found.length;
|
|
103
|
+
const end = element(source, index, found);
|
|
104
|
+
if (end !== null) {
|
|
105
|
+
index = end;
|
|
106
|
+
previous = ")";
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
found.length = mark;
|
|
110
|
+
}
|
|
111
|
+
if (char === "/" && VALUE_POSITION.has(previous)) {
|
|
112
|
+
index = endOfRegex(source, index);
|
|
113
|
+
previous = "/";
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (char === "{") depth += 1;
|
|
117
|
+
if (char === "}") {
|
|
118
|
+
if (depth === 0 && nested) return index + 1;
|
|
119
|
+
depth -= 1;
|
|
120
|
+
}
|
|
121
|
+
if (!/\s/.test(char)) previous = char;
|
|
122
|
+
index += 1;
|
|
123
|
+
}
|
|
124
|
+
return index;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** One quoted string: its text pushed, the index past its closing quote returned. A newline ends it unclosed. */
|
|
128
|
+
function quoted(source: string, start: number, quote: string, found: LiteralSpan[]): number {
|
|
129
|
+
let text = "";
|
|
130
|
+
for (let index = start + 1; index < source.length; index += 1) {
|
|
131
|
+
const char = source[index] as string;
|
|
132
|
+
if (char === "\\") {
|
|
133
|
+
text += source[index + 1] ?? "";
|
|
134
|
+
index += 1;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (char === quote || char === "\n") {
|
|
138
|
+
const end = char === quote ? index + 1 : index;
|
|
139
|
+
found.push({ text, start, end });
|
|
140
|
+
return end;
|
|
141
|
+
}
|
|
142
|
+
text += char;
|
|
143
|
+
}
|
|
144
|
+
found.push({ text, start, end: source.length });
|
|
145
|
+
return source.length;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* One template: its text pushed with each `${…}` as `<x>`, and every literal inside an interpolation pushed in
|
|
150
|
+
* its own right. Returns the index past the closing backtick.
|
|
151
|
+
*/
|
|
152
|
+
function template(source: string, start: number, found: LiteralSpan[], jsx: boolean): number {
|
|
153
|
+
let text = "";
|
|
154
|
+
const holes: [number, number][] = [];
|
|
155
|
+
const slot = found.length;
|
|
156
|
+
found.push({ text: "", start, end: start });
|
|
157
|
+
const close = (end: number): LiteralSpan => ({ text, start, end, ...(holes.length > 0 ? { holes } : {}) });
|
|
158
|
+
let index = start + 1;
|
|
159
|
+
while (index < source.length) {
|
|
160
|
+
const char = source[index] as string;
|
|
161
|
+
if (char === "\\") {
|
|
162
|
+
text += source[index + 1] ?? "";
|
|
163
|
+
index += 2;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (char === "`") {
|
|
167
|
+
found[slot] = close(index + 1);
|
|
168
|
+
return index + 1;
|
|
169
|
+
}
|
|
170
|
+
if (char === "$" && source[index + 1] === "{") {
|
|
171
|
+
text += "<x>";
|
|
172
|
+
const from = index + 2;
|
|
173
|
+
index = walk(source, from, found, true, jsx);
|
|
174
|
+
// `walk` returns past the `}` that closed it; an unclosed one ran to the end.
|
|
175
|
+
holes.push([from, source[index - 1] === "}" ? index - 1 : index]);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
text += char;
|
|
179
|
+
index += 1;
|
|
180
|
+
}
|
|
181
|
+
found[slot] = close(source.length);
|
|
182
|
+
return source.length;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** After one of these words, as after a {@link VALUE_POSITION} character, a value begins. */
|
|
186
|
+
const VALUE_WORDS = new Set(["return", "yield", "await", "default", "case", "else", "do"]);
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Whether the `<` at `index` opens a JSX element: where a value may begin — after an operator, a bracket, or a
|
|
190
|
+
* word such as `return` — and followed by a tag name or, for a fragment, `>`. After an identifier or a closing
|
|
191
|
+
* bracket it compares, or opens a type's arguments.
|
|
192
|
+
*/
|
|
193
|
+
function opensElement(source: string, index: number, previous: string): boolean {
|
|
194
|
+
if (!/[A-Za-z_$>]/.test(source[index + 1] ?? "")) return false;
|
|
195
|
+
if (VALUE_POSITION.has(previous)) return true;
|
|
196
|
+
const before = /([A-Za-z_$][\w$]*)\s*$/.exec(source.slice(Math.max(0, index - 32), index));
|
|
197
|
+
return before !== null && VALUE_WORDS.has(before[1] as string);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** A JSX text run as React renders it: whitespace collapsed, and nothing pushed for a run that is only space. */
|
|
201
|
+
function pushText(text: string, start: number, end: number, found: LiteralSpan[]): void {
|
|
202
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
203
|
+
if (collapsed.length > 0) found.push({ text: collapsed, start, end });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* One JSX element from its `<`: each attribute string and each text run pushed, every `{…}` walked as code. Returns
|
|
208
|
+
* the index past the element, or `null` when what follows the `<` is not an element — a generic arrow `<T,>(…)`,
|
|
209
|
+
* or a `<` that never closes — for the caller to read as code.
|
|
210
|
+
*/
|
|
211
|
+
function element(source: string, start: number, found: LiteralSpan[]): number | null {
|
|
212
|
+
let index = start + 1;
|
|
213
|
+
const name = /^[A-Za-z_$][\w$.:-]*/.exec(source.slice(index))?.[0] ?? "";
|
|
214
|
+
index += name.length;
|
|
215
|
+
// The attributes, up to `>` or `/>`.
|
|
216
|
+
for (;;) {
|
|
217
|
+
while (/\s/.test(source[index] ?? "")) index += 1;
|
|
218
|
+
const char = source[index];
|
|
219
|
+
if (char === undefined) return null;
|
|
220
|
+
if (char === "/" && source[index + 1] === ">") return index + 2;
|
|
221
|
+
if (char === ">") {
|
|
222
|
+
index += 1;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
if (char === "{") {
|
|
226
|
+
index = walk(source, index + 1, found, true, true);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const attribute = /^[A-Za-z_$][\w$:-]*/.exec(source.slice(index))?.[0];
|
|
230
|
+
if (attribute === undefined || name === "") return null;
|
|
231
|
+
index += attribute.length;
|
|
232
|
+
while (/\s/.test(source[index] ?? "")) index += 1;
|
|
233
|
+
if (source[index] !== "=") continue;
|
|
234
|
+
index += 1;
|
|
235
|
+
while (/\s/.test(source[index] ?? "")) index += 1;
|
|
236
|
+
const value = source[index];
|
|
237
|
+
if (value === "{") {
|
|
238
|
+
index = walk(source, index + 1, found, true, true);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (value !== '"' && value !== "'") return null;
|
|
242
|
+
// A JSX attribute string has no escapes, and may run across lines.
|
|
243
|
+
const close = source.indexOf(value, index + 1);
|
|
244
|
+
if (close === -1) return null;
|
|
245
|
+
found.push({ text: source.slice(index + 1, close), start: index, end: close + 1 });
|
|
246
|
+
index = close + 1;
|
|
247
|
+
}
|
|
248
|
+
// The children, up to the closing tag.
|
|
249
|
+
let text = "";
|
|
250
|
+
let from = index;
|
|
251
|
+
while (index < source.length) {
|
|
252
|
+
const char = source[index] as string;
|
|
253
|
+
if (char === "{") {
|
|
254
|
+
text += "<x>";
|
|
255
|
+
index = walk(source, index + 1, found, true, true);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (char === "<") {
|
|
259
|
+
const closing = /^<\s*\//.exec(source.slice(index));
|
|
260
|
+
if (closing !== null) {
|
|
261
|
+
pushText(text, from, index, found);
|
|
262
|
+
const end = source.indexOf(">", index);
|
|
263
|
+
return end === -1 ? null : end + 1;
|
|
264
|
+
}
|
|
265
|
+
pushText(text, from, index, found);
|
|
266
|
+
const end = element(source, index, found);
|
|
267
|
+
if (end === null) return null;
|
|
268
|
+
index = end;
|
|
269
|
+
text = "";
|
|
270
|
+
from = index;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
text += char;
|
|
274
|
+
index += 1;
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Where a regex literal ends. A `/` inside a character class does not close it; a newline does. */
|
|
280
|
+
function endOfRegex(source: string, start: number): number {
|
|
281
|
+
let inClass = false;
|
|
282
|
+
for (let index = start + 1; index < source.length; index += 1) {
|
|
283
|
+
const char = source[index];
|
|
284
|
+
if (char === "\\") {
|
|
285
|
+
index += 1;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (char === "\n") return index;
|
|
289
|
+
if (char === "[") inClass = true;
|
|
290
|
+
else if (char === "]") inClass = false;
|
|
291
|
+
else if (char === "/" && !inClass) return index + 1;
|
|
292
|
+
}
|
|
293
|
+
return source.length;
|
|
294
|
+
}
|
package/src/version.generated.ts
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
|
-
import { zValidator } from "@hono/zod-validator";
|
|
5
4
|
import type { Context, Hono, Next } from "hono";
|
|
6
5
|
import type { PithyHonoEnv } from "../capability/capability";
|
|
6
|
+
import { commandName } from "../cli/command";
|
|
7
7
|
import { type AmbientEnv, ambientEnv, compositionEnvironment } from "../env/ambient";
|
|
8
8
|
import { ForbiddenError, PithyError } from "../error/pithyError";
|
|
9
|
-
import {
|
|
9
|
+
import { describeRoute } from "../http/routeSpec";
|
|
10
|
+
import { validate } from "../http/validation";
|
|
10
11
|
import type { VerificationStrategy } from "../http/verification";
|
|
11
12
|
import { workflowBindingFor } from "./dispatch";
|
|
12
13
|
import {
|
|
13
14
|
WORKFLOW_DISPATCH_ROUTE,
|
|
14
15
|
WorkflowDispatchParams,
|
|
15
16
|
WorkflowDispatchRequest,
|
|
16
|
-
|
|
17
|
+
WorkflowDispatchResponse,
|
|
17
18
|
} from "./schemas";
|
|
18
19
|
import type { RegisteredWorkflow, WorkflowRegistry } from "./spec";
|
|
19
20
|
|
|
@@ -104,7 +105,7 @@ function specForBinding(registry: WorkflowRegistry, binding: string): Registered
|
|
|
104
105
|
function refuseOutsideDev(capability: string, environment: string | undefined): ForbiddenError {
|
|
105
106
|
return new ForbiddenError({
|
|
106
107
|
message: `Workflow dispatch over HTTP is served only in the ${DISPATCH_ENVIRONMENT} environment.`,
|
|
107
|
-
action: `Dispatch through the cross-script ${capability} Workflow binding, or run this host under
|
|
108
|
+
action: `Dispatch through the cross-script ${capability} Workflow binding, or run this host under ${commandName("dev")}.`,
|
|
108
109
|
detail: `The composition's ENVIRONMENT is ${environment ?? "unstamped"}.`,
|
|
109
110
|
});
|
|
110
111
|
}
|
|
@@ -129,9 +130,15 @@ export function registerWorkflowDispatchRoute(app: Hono<PithyHonoEnv>, options:
|
|
|
129
130
|
|
|
130
131
|
app.post(
|
|
131
132
|
WORKFLOW_DISPATCH_ROUTE,
|
|
133
|
+
describeRoute({
|
|
134
|
+
summary: "Start a Workflow instance on this host, in dev only.",
|
|
135
|
+
description: "Refused outside a `dev` composition. Deployed Workers dispatch through a Workflow binding instead.",
|
|
136
|
+
verification: WORKFLOW_DISPATCH_VERIFICATION,
|
|
137
|
+
responses: { 202: WorkflowDispatchResponse },
|
|
138
|
+
}),
|
|
132
139
|
guard,
|
|
133
|
-
|
|
134
|
-
|
|
140
|
+
validate("param", WorkflowDispatchParams),
|
|
141
|
+
validate("json", WorkflowDispatchRequest),
|
|
135
142
|
async (c) => {
|
|
136
143
|
const { binding } = c.req.valid("param");
|
|
137
144
|
const { id, params } = c.req.valid("json");
|
package/src/workflow/faults.ts
CHANGED
|
@@ -259,8 +259,10 @@ export function classifiedSteps(
|
|
|
259
259
|
* answer that cannot change.
|
|
260
260
|
*/
|
|
261
261
|
sleep(name: string, durationMs: number): Promise<void> {
|
|
262
|
-
|
|
263
|
-
|
|
262
|
+
// Read to test for presence, then called off `step` itself. On Cloudflare the step is an RPC
|
|
263
|
+
// stub, where a property read is another stub: `.call` on it is a remote call to a method named
|
|
264
|
+
// `call`, which the receiver does not implement.
|
|
265
|
+
if (step.sleep === undefined) {
|
|
264
266
|
return runner.do<void>(`${name}/cannot-pause`, async () => {
|
|
265
267
|
throw new InternalError({
|
|
266
268
|
message: "This Workflow step runner cannot pause.",
|
|
@@ -269,7 +271,7 @@ export function classifiedSteps(
|
|
|
269
271
|
});
|
|
270
272
|
});
|
|
271
273
|
}
|
|
272
|
-
return
|
|
274
|
+
return step.sleep(name, durationMs);
|
|
273
275
|
},
|
|
274
276
|
};
|
|
275
277
|
return runner;
|
package/src/workflow/hostEnv.ts
CHANGED
|
@@ -59,6 +59,13 @@ export const HostEnvProvider = z
|
|
|
59
59
|
.min(1)
|
|
60
60
|
.optional()
|
|
61
61
|
.describe("The `pithy` command that fills it, where one does. Rendered as the report's action line."),
|
|
62
|
+
decision: z
|
|
63
|
+
.string()
|
|
64
|
+
.min(1)
|
|
65
|
+
.optional()
|
|
66
|
+
.describe(
|
|
67
|
+
"What fills it when only a person may run the command that does — one that touches production — said as their decision. Rendered in place of `command`.",
|
|
68
|
+
),
|
|
62
69
|
})
|
|
63
70
|
.describe("What fills one field of a host worker's env, so a failure names the thing an operator changes.");
|
|
64
71
|
export type HostEnvProvider = z.infer<typeof HostEnvProvider>;
|
|
@@ -170,7 +177,8 @@ export function hostEnvProviderSentence(provider: HostEnvProvider): string {
|
|
|
170
177
|
: provider.kind === "secret"
|
|
171
178
|
? `Secret ${provider.name}, read through @pithy-sh/secrets.`
|
|
172
179
|
: `Config key ${provider.name}.`;
|
|
173
|
-
|
|
180
|
+
if (provider.command) return `${where} Run ${provider.command}.`;
|
|
181
|
+
return provider.decision ? `${where} ${provider.decision}` : where;
|
|
174
182
|
}
|
|
175
183
|
|
|
176
184
|
/**
|
package/src/workflow/loopback.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
|
+
import { commandName } from "../cli/command";
|
|
4
5
|
import { UpstreamError, UpstreamTimeoutError } from "../error/pithyError";
|
|
5
6
|
import { type WorkflowDispatchRequest, workflowDispatchPath } from "./schemas";
|
|
6
7
|
import type { WorkflowBinding } from "./spec";
|
|
@@ -84,7 +85,7 @@ export function loopbackWorkflowBinding(options: LoopbackWorkflowOptions): Workf
|
|
|
84
85
|
const owner = options.capability ?? options.binding;
|
|
85
86
|
const send = options.fetch ?? ((target: string, init: RequestInit) => fetch(target, init));
|
|
86
87
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
87
|
-
const running = `Check that the ${owner} host is running under
|
|
88
|
+
const running = `Check that the ${owner} host is running under ${commandName("dev")} at ${origin}.`;
|
|
88
89
|
|
|
89
90
|
return {
|
|
90
91
|
async create(instance?: { id?: string; params?: unknown }): Promise<unknown> {
|