@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.
Files changed (166) hide show
  1. package/dist/asyncapi/document.d.ts +46 -0
  2. package/dist/asyncapi/document.d.ts.map +1 -0
  3. package/dist/asyncapi/document.js +115 -0
  4. package/dist/capability/capability.d.ts +7 -0
  5. package/dist/capability/capability.d.ts.map +1 -1
  6. package/dist/capability/composition.d.ts.map +1 -1
  7. package/dist/capability/composition.js +5 -1
  8. package/dist/capability/settings.d.ts +10 -2
  9. package/dist/capability/settings.d.ts.map +1 -1
  10. package/dist/capability/settings.js +5 -3
  11. package/dist/cli/command.d.ts +364 -0
  12. package/dist/cli/command.d.ts.map +1 -0
  13. package/dist/cli/command.js +418 -0
  14. package/dist/cli/commandCatalog.generated.d.ts +2275 -0
  15. package/dist/cli/commandCatalog.generated.d.ts.map +1 -0
  16. package/dist/cli/commandCatalog.generated.js +2390 -0
  17. package/dist/cli/commandFacts.d.ts +164 -0
  18. package/dist/cli/commandFacts.d.ts.map +1 -0
  19. package/dist/cli/commandFacts.js +215 -0
  20. package/dist/controlPlane/error/errors.d.ts.map +1 -1
  21. package/dist/controlPlane/error/errors.js +4 -3
  22. package/dist/controlPlane/http/guard.d.ts.map +1 -1
  23. package/dist/controlPlane/http/guard.js +11 -1
  24. package/dist/controlPlane/http/routes.d.ts +1 -1
  25. package/dist/controlPlane/http/routes.d.ts.map +1 -1
  26. package/dist/controlPlane/http/routes.js +34 -7
  27. package/dist/createBackend.d.ts +7 -0
  28. package/dist/createBackend.d.ts.map +1 -1
  29. package/dist/createBackend.js +34 -4
  30. package/dist/entitlement/require.js +1 -1
  31. package/dist/error/cause.d.ts +16 -116
  32. package/dist/error/cause.d.ts.map +1 -1
  33. package/dist/error/cause.js +19 -153
  34. package/dist/error/http.d.ts +40 -0
  35. package/dist/error/http.d.ts.map +1 -1
  36. package/dist/error/payload.d.ts +80 -0
  37. package/dist/error/payload.d.ts.map +1 -1
  38. package/dist/error/payload.js +26 -0
  39. package/dist/error/pithyError.d.ts +44 -0
  40. package/dist/error/pithyError.d.ts.map +1 -1
  41. package/dist/error/pithyError.js +56 -1
  42. package/dist/error/unresolved.d.ts +265 -0
  43. package/dist/error/unresolved.d.ts.map +1 -0
  44. package/dist/error/unresolved.js +599 -0
  45. package/dist/error/wire.d.ts +20 -0
  46. package/dist/error/wire.d.ts.map +1 -0
  47. package/dist/error/wire.js +19 -0
  48. package/dist/http/oidcWebhook.d.ts +3 -3
  49. package/dist/http/oidcWebhook.js +3 -3
  50. package/dist/http/routeContract.js +1 -1
  51. package/dist/http/routeSpec.d.ts +66 -0
  52. package/dist/http/routeSpec.d.ts.map +1 -0
  53. package/dist/http/routeSpec.js +40 -0
  54. package/dist/http/sameOrigin.d.ts +1 -1
  55. package/dist/http/sameOrigin.js +1 -1
  56. package/dist/http/signedWebhook.d.ts +3 -3
  57. package/dist/http/signedWebhook.js +3 -3
  58. package/dist/http/socket.d.ts +52 -0
  59. package/dist/http/socket.d.ts.map +1 -0
  60. package/dist/http/socket.js +2 -0
  61. package/dist/http/subprotocol.d.ts +29 -0
  62. package/dist/http/subprotocol.d.ts.map +1 -0
  63. package/dist/http/subprotocol.js +37 -0
  64. package/dist/http/validation.d.ts +40 -8
  65. package/dist/http/validation.d.ts.map +1 -1
  66. package/dist/http/validation.js +25 -1
  67. package/dist/index.js +2 -2
  68. package/dist/logger/record.d.ts +23 -0
  69. package/dist/logger/record.d.ts.map +1 -1
  70. package/dist/migrations/bookkeeping.d.ts +21 -0
  71. package/dist/migrations/bookkeeping.d.ts.map +1 -1
  72. package/dist/migrations/bookkeeping.js +32 -3
  73. package/dist/migrations/groups.d.ts +140 -0
  74. package/dist/migrations/groups.d.ts.map +1 -0
  75. package/dist/migrations/groups.js +191 -0
  76. package/dist/migrations/migrateStep.d.ts +13 -0
  77. package/dist/migrations/migrateStep.d.ts.map +1 -0
  78. package/dist/migrations/migrateStep.js +20 -0
  79. package/dist/migrations/owner.d.ts +7 -0
  80. package/dist/migrations/owner.d.ts.map +1 -1
  81. package/dist/migrations/owner.js +11 -7
  82. package/dist/migrations/retained.js +2 -2
  83. package/dist/migrations/runner.d.ts +76 -5
  84. package/dist/migrations/runner.d.ts.map +1 -1
  85. package/dist/migrations/runner.js +130 -19
  86. package/dist/naming/domains.d.ts +30 -0
  87. package/dist/naming/domains.d.ts.map +1 -1
  88. package/dist/naming/domains.js +83 -4
  89. package/dist/naming/environment.d.ts.map +1 -1
  90. package/dist/naming/environment.js +2 -1
  91. package/dist/naming/packageName.d.ts +17 -0
  92. package/dist/naming/packageName.d.ts.map +1 -0
  93. package/dist/naming/packageName.js +22 -0
  94. package/dist/naming/provisionScope.d.ts.map +1 -1
  95. package/dist/naming/provisionScope.js +2 -1
  96. package/dist/openapi/document.d.ts +106 -0
  97. package/dist/openapi/document.d.ts.map +1 -0
  98. package/dist/openapi/document.js +314 -0
  99. package/dist/openapi/routeGroups.d.ts +8 -0
  100. package/dist/openapi/routeGroups.d.ts.map +1 -0
  101. package/dist/openapi/routeGroups.js +23 -0
  102. package/dist/seed/devLogin.d.ts +33 -0
  103. package/dist/seed/devLogin.d.ts.map +1 -1
  104. package/dist/seed/devLogin.js +28 -1
  105. package/dist/text/comments.js +66 -4
  106. package/dist/text/literals.d.ts +43 -0
  107. package/dist/text/literals.d.ts.map +1 -0
  108. package/dist/text/literals.js +293 -0
  109. package/dist/version.generated.d.ts +1 -1
  110. package/dist/version.generated.d.ts.map +1 -1
  111. package/dist/version.generated.js +1 -1
  112. package/dist/workflow/dispatchRoute.d.ts.map +1 -1
  113. package/dist/workflow/dispatchRoute.js +12 -6
  114. package/dist/workflow/faults.d.ts.map +1 -1
  115. package/dist/workflow/faults.js +2 -3
  116. package/dist/workflow/hostEnv.d.ts +1 -0
  117. package/dist/workflow/hostEnv.d.ts.map +1 -1
  118. package/dist/workflow/hostEnv.js +4 -2
  119. package/dist/workflow/loopback.d.ts.map +1 -1
  120. package/dist/workflow/loopback.js +2 -1
  121. package/package.json +3 -3
  122. package/src/asyncapi/document.ts +164 -0
  123. package/src/capability/capability.ts +7 -0
  124. package/src/capability/composition.ts +2 -1
  125. package/src/capability/settings.ts +19 -2
  126. package/src/cli/command.ts +865 -0
  127. package/src/cli/commandCatalog.generated.ts +2533 -0
  128. package/src/cli/commandFacts.ts +219 -0
  129. package/src/controlPlane/error/errors.ts +10 -3
  130. package/src/controlPlane/http/guard.ts +14 -2
  131. package/src/controlPlane/http/routes.ts +57 -9
  132. package/src/createBackend.ts +35 -2
  133. package/src/entitlement/require.ts +1 -1
  134. package/src/error/cause.ts +18 -181
  135. package/src/error/payload.ts +59 -0
  136. package/src/error/pithyError.ts +76 -0
  137. package/src/error/unresolved.ts +853 -0
  138. package/src/error/wire.ts +40 -0
  139. package/src/http/oidcWebhook.ts +3 -3
  140. package/src/http/routeContract.ts +1 -1
  141. package/src/http/routeSpec.ts +81 -0
  142. package/src/http/sameOrigin.ts +1 -1
  143. package/src/http/signedWebhook.ts +3 -3
  144. package/src/http/socket.ts +55 -0
  145. package/src/http/subprotocol.ts +47 -0
  146. package/src/http/validation.ts +50 -8
  147. package/src/migrations/bookkeeping.ts +39 -2
  148. package/src/migrations/groups.ts +295 -0
  149. package/src/migrations/migrateStep.ts +19 -0
  150. package/src/migrations/owner.ts +10 -6
  151. package/src/migrations/retained.ts +2 -2
  152. package/src/migrations/runner.ts +214 -22
  153. package/src/naming/domains.ts +97 -16
  154. package/src/naming/environment.ts +2 -1
  155. package/src/naming/packageName.ts +20 -0
  156. package/src/naming/provisionScope.ts +2 -1
  157. package/src/openapi/document.ts +412 -0
  158. package/src/openapi/routeGroups.ts +25 -0
  159. package/src/seed/devLogin.ts +31 -0
  160. package/src/text/comments.ts +56 -4
  161. package/src/text/literals.ts +294 -0
  162. package/src/version.generated.ts +1 -1
  163. package/src/workflow/dispatchRoute.ts +13 -6
  164. package/src/workflow/faults.ts +5 -3
  165. package/src/workflow/hostEnv.ts +9 -1
  166. package/src/workflow/loopback.ts +2 -1
@@ -0,0 +1,418 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { InternalError } from "../error/pithyError.js";
4
+ import { COMMANDS, GLOBAL_FLAGS, GROUPS } from "./commandCatalog.generated.js";
5
+ import { CONFIRMATIONS, CONFIRM_FLAGS, CREDENTIAL_FLAGS, DESTRUCTIVE_COMMANDS, DESTRUCTIVE_FLAGS, EVERY_ENVIRONMENT_ALWAYS, EVERY_ENVIRONMENT_WITHOUT_ENV, MEANING_FLAGS, MEANING_ONLY_WITH, NO_WORKER_MODES, PRODUCTION_CONFIRMS, PRODUCTION_ENVIRONMENTS, REQUIRED_BESIDE, WORKER_SCOPED } from "./commandFacts.js";
6
+ //#region src/cli/command.ts
7
+ /** The CLI itself: the first word of every line built here, and what a line names when it means no command. */
8
+ const CLI = "pithy";
9
+ /**
10
+ * The Worker a directory holds, as `--worker` names it: the directory's own name. Takes the directory rather
11
+ * than a name, because a name in hand is as often the deployed script name, which `--worker` does not resolve.
12
+ */
13
+ function workerDirOf(worker) {
14
+ const name = worker.dir.split(/[\\/]/).filter((part) => part.length > 0).at(-1);
15
+ if (name === void 0) throw new InternalError({
16
+ message: "A Worker directory has a name.",
17
+ detail: worker.dir
18
+ });
19
+ return name;
20
+ }
21
+ /** Whether `path` is a command the CLI runs. */
22
+ function isCommandPath(path) {
23
+ return Object.hasOwn(COMMANDS, path);
24
+ }
25
+ /** Whether `path` is a group. */
26
+ function isGroupPath(path) {
27
+ return GROUPS.includes(path);
28
+ }
29
+ /**
30
+ * What the catalog says about one flag of one command — its type, whether it is required, its default — or
31
+ * `undefined` when the command does not declare it.
32
+ */
33
+ function commandFlag(path, flag) {
34
+ if (!isCommandPath(path)) return void 0;
35
+ return COMMANDS[path].flags[flag];
36
+ }
37
+ /** A value nobody can run: a `<placeholder>`, or nothing at all. */
38
+ function isPlaceholder(value, options) {
39
+ if (value.trim().length === 0) return true;
40
+ return (value.match(/<[^>]*>/g) ?? []).some((mark) => !(options.interpolated === true && mark === "<x>"));
41
+ }
42
+ /** Whether the environment a line names is a production one. */
43
+ function isProduction(env, options) {
44
+ const name = env.trim().toLowerCase();
45
+ return [...PRODUCTION_ENVIRONMENTS, ...options.productionEnvironments ?? []].some((one) => one.trim().toLowerCase() === name);
46
+ }
47
+ /**
48
+ * **Every reason a command line would not run as written, or would do more than a remedy may.** Empty when it
49
+ * runs. The builder throws on any; `runnableCommands.test.ts` holds every hand-spelled line it reads to the same
50
+ * function, so the gate and the builder cannot disagree about a rule.
51
+ */
52
+ function commandFaults(path, parts = {}, options = {}) {
53
+ const shown = `${CLI} ${path}`;
54
+ if (!isCommandPath(path)) return [isGroupPath(path) ? `\`${shown}\` is a group, which runs nothing.` : `\`${shown}\` is not a command.`];
55
+ const def = COMMANDS[path];
56
+ const faults = [];
57
+ const args = parts.args ?? [];
58
+ const flags = Object.entries(parts.flags ?? {}).filter((entry) => entry[1] !== void 0);
59
+ const named = new Set(flags.map(([flag]) => flag));
60
+ const required = def.positionals.filter((one) => one.required).length;
61
+ const variadic = def.positionals.some((one) => one.variadic);
62
+ if (args.length < required) faults.push(`\`${shown}\` needs its ${def.positionals[args.length]?.name} argument.`);
63
+ if (!variadic && args.length > def.positionals.length) faults.push(`\`${shown}\` takes ${def.positionals.length} arguments.`);
64
+ for (const arg of options.spelled === true ? args : []) if (isPlaceholder(arg, options)) faults.push(`\`${shown}\` is given "${arg}", which nobody can run.`);
65
+ const global = new Set(GLOBAL_FLAGS);
66
+ for (const [flag, value] of flags) {
67
+ const flagDef = def.flags[flag];
68
+ if (flagDef === void 0) {
69
+ if (!global.has(flag)) faults.push(`\`${shown}\` has no ${flag}.`);
70
+ continue;
71
+ }
72
+ const values = typeof value === "string" ? [value] : value === true ? [] : [...value];
73
+ if (flagDef.type === "boolean" && value !== true) faults.push(`\`${shown} ${flag}\` takes no value.`);
74
+ if (flagDef.type === "string" && (value === true || values.length === 0)) faults.push(`\`${shown} ${flag}\` needs a value.`);
75
+ if (values.length > 1 && !flagDef.repeatable) faults.push(`\`${shown} ${flag}\` is passed once.`);
76
+ for (const one of values) if (options.spelled === true && isPlaceholder(one, options)) faults.push(`\`${shown} ${flag}\` is given "${one}", which nobody can run.`);
77
+ else if (options.given !== true && flagDef.enum !== void 0 && !flagDef.enum.includes(one) && !(options.interpolated && one === "<x>")) faults.push(`\`${shown} ${flag}\` takes ${flagDef.enum.join(", ")}, not ${one}.`);
78
+ }
79
+ const confirms = new Set(CONFIRM_FLAGS);
80
+ for (const flag of named) {
81
+ if (confirms.has(flag)) faults.push(`\`${shown} ${flag}\` carries a confirmation only a person gives.`);
82
+ if (CREDENTIAL_FLAGS.includes(flag)) faults.push(`\`${shown} ${flag}\` carries a credential, which no line repeats.`);
83
+ }
84
+ for (const [flag, flagDef] of Object.entries(def.flags)) {
85
+ if (named.has(flag)) continue;
86
+ if (flagDef.required && !(options.person === true && confirms.has(flag))) faults.push(`\`${shown}\` is refused without ${flag}.`);
87
+ const onlyWith = MEANING_ONLY_WITH[path];
88
+ const meaningful = onlyWith === void 0 || named.has(onlyWith);
89
+ if (MEANING_FLAGS.includes(flag) && flagDef.default !== void 0 && meaningful) faults.push(`\`${shown}\` without ${flag} acts on ${flagDef.default}; a line names the one it is about.`);
90
+ }
91
+ for (const group of def.groups) faults.push(...groupFaults(shown, group, named));
92
+ for (const rule of REQUIRED_BESIDE) if (rule.path === path && named.has(rule.with) && !named.has(rule.unless) && !named.has(rule.needs)) faults.push(`\`${shown} ${rule.with}\` is refused without ${rule.needs}.`);
93
+ const noWorker = (NO_WORKER_MODES[path] ?? []).some((flag) => named.has(flag));
94
+ if (WORKER_SCOPED.includes(path) && !noWorker && !named.has("--worker") && !options.runtime) faults.push(`\`${shown}\` acts on one Worker and names none.`);
95
+ if (options.person !== true) for (const consequence of consequencesOf(path, parts, options)) faults.push(`\`${shown}\` ${consequence}: a person's decision.`);
96
+ return faults;
97
+ }
98
+ /**
99
+ * **What a command does that only a person may decide**: what it deletes, or that it runs against production. Empty
100
+ * for a command a remedy may name.
101
+ */
102
+ function consequencesOf(path, parts = {}, options = {}) {
103
+ const consequences = [];
104
+ if (Object.hasOwn(DESTRUCTIVE_COMMANDS, path)) consequences.push(`deletes ${DESTRUCTIVE_COMMANDS[path]}`);
105
+ for (const [flag, value] of Object.entries(parts.flags ?? {})) if (value !== void 0 && Object.hasOwn(DESTRUCTIVE_FLAGS, flag)) consequences.push(`deletes ${DESTRUCTIVE_FLAGS[flag]}`);
106
+ const env = parts.flags?.["--env"];
107
+ if (Object.hasOwn(PRODUCTION_CONFIRMS, path) && typeof env === "string" && isProduction(env, options)) consequences.push(`runs against ${env}, a production environment`);
108
+ if (env === void 0 && actsOnEveryEnvironment(path) || actsOnEveryEnvironmentAlways(path)) consequences.push("runs against every environment, production included");
109
+ return consequences;
110
+ }
111
+ /**
112
+ * Whether `path` acts on every environment, production among them, when its `--env` is left off — `pithy testers
113
+ * provision` (`commandFacts.ts`'s {@link EVERY_ENVIRONMENT_WITHOUT_ENV}). A line about one environment names it.
114
+ */
115
+ function actsOnEveryEnvironment(path) {
116
+ return EVERY_ENVIRONMENT_WITHOUT_ENV.includes(path);
117
+ }
118
+ /**
119
+ * Whether `path` acts on every environment, production among them, on every run, with no `--env` to narrow it —
120
+ * `pithy secrets provision` (`commandFacts.ts`'s {@link EVERY_ENVIRONMENT_ALWAYS}). A line naming it is a person's
121
+ * decision, always.
122
+ */
123
+ function actsOnEveryEnvironmentAlways(path) {
124
+ return EVERY_ENVIRONMENT_ALWAYS.includes(path);
125
+ }
126
+ /** What one rule over several flags says about the flags a line names. */
127
+ function groupFaults(shown, group, named) {
128
+ const among = (group.flags ?? []).filter((flag) => named.has(flag));
129
+ const list = (group.flags ?? []).join(", ");
130
+ switch (group.kind) {
131
+ case "exactlyOne": return among.length === 1 ? [] : [`\`${shown}\` takes exactly one of ${list}.`];
132
+ case "atMostOne": return among.length <= 1 ? [] : [`\`${shown}\` takes at most one of ${list}.`];
133
+ case "allOrNone": return among.length === 0 || among.length === (group.flags ?? []).length ? [] : [`\`${shown}\` takes all of ${list}, or none.`];
134
+ case "requires": {
135
+ if (group.flag === void 0 || !named.has(group.flag)) return [];
136
+ const needs = group.needs ?? [];
137
+ return (group.any === true ? needs.some((flag) => named.has(flag)) : needs.every((flag) => named.has(flag))) ? [] : [`\`${shown} ${group.flag}\` needs ${needs.join(group.any === true ? " or " : " and ")}.`];
138
+ }
139
+ }
140
+ }
141
+ /** A word as a shell reads it back: quoted only when it has to be. */
142
+ function shellWord(word) {
143
+ return /^[A-Za-z0-9_@%+=:,./-]+$/.test(word) ? word : `'${word.replaceAll("'", `'\\''`)}'`;
144
+ }
145
+ /** The line, spelled: arguments first, then each flag in the order the caller wrote it. */
146
+ function spell(path, parts) {
147
+ const words = [
148
+ CLI,
149
+ path,
150
+ ...(parts.args ?? []).map(shellWord)
151
+ ];
152
+ for (const [flag, value] of Object.entries(parts.flags ?? {})) {
153
+ if (value === void 0) continue;
154
+ if (value === true) words.push(flag);
155
+ else for (const one of typeof value === "string" ? [value] : value) words.push(flag, shellWord(one));
156
+ }
157
+ return words.join(" ");
158
+ }
159
+ /** Throw what {@link commandFaults} found, or hand back nothing. */
160
+ function refuseFaults(path, parts, options) {
161
+ const faults = commandFaults(path, parts, options);
162
+ if (faults.length > 0) throw new InternalError({
163
+ message: `A line names a command that does not run as written: ${faults.join(" ")}`,
164
+ detail: spell(path, parts)
165
+ });
166
+ }
167
+ /**
168
+ * **A command line that runs as written**, from a literal path: the tree checks its command, its flags and the
169
+ * flags it needs at compile time, and {@link commandFaults} checks the rest when it is built.
170
+ */
171
+ function pithyCommand(path, ...parts) {
172
+ return build(path, parts[0] ?? {}, {});
173
+ }
174
+ /** Check a line's shape, and spell it. */
175
+ function build(path, parts, options) {
176
+ refuseFaults(path, parts, options);
177
+ return spell(path, parts);
178
+ }
179
+ /** Whether a production run of `path` is a person's decision, so its `--env` decides what kind of line it is. */
180
+ function isProductionPath(path) {
181
+ return Object.hasOwn(PRODUCTION_CONFIRMS, path);
182
+ }
183
+ function pithyCommandOf(path, parts = {}, options = {}) {
184
+ if (parts.flags?.["--env"] !== void 0 && (options.runtime === true || isProductionPath(path))) throw new InternalError({ message: `A line names an environment it read through remedy, never through pithyCommandOf: \`${spell(path, parts)}\`.` });
185
+ return build(path, parts, options);
186
+ }
187
+ /**
188
+ * **A command a person decides on, said as one.** For a fault whose only remedy destroys something, or runs
189
+ * against production: the sentence names the command, says what it does and that the choice is a person's, and
190
+ * leaves every confirmation flag for them to add. It returns a sentence, never a {@link Command}, so it cannot
191
+ * become a `fix`.
192
+ *
193
+ * It takes a command that destroys something, or a line naming a flag that does — shape, never a value. A command
194
+ * whose production run is a person's decision is a decision only for a production `--env`, which is data, so that
195
+ * line is {@link remedy}'s, which decides by the value; here it is refused for every value.
196
+ */
197
+ function personDecision(path, parts, options = {}) {
198
+ if (!(Object.hasOwn(DESTRUCTIVE_COMMANDS, path) || Object.entries(parts.flags ?? {}).some(([flag, value]) => value !== void 0 && Object.hasOwn(DESTRUCTIVE_FLAGS, flag)))) throw new InternalError({ message: `\`${spell(path, parts)}\` destroys nothing; a person's decision names what it destroys, and a production run is remedy's to say.` });
199
+ return decision(path, parts, options);
200
+ }
201
+ /** The sentence {@link personDecision} says, for any command with something a person must decide. */
202
+ function decision(path, parts, options) {
203
+ refuseFaults(path, parts, {
204
+ ...options,
205
+ person: true
206
+ });
207
+ const consequences = consequencesOf(path, parts, options);
208
+ if (consequences.length === 0) throw new InternalError({ message: `\`${spell(path, parts)}\` has nothing a person must decide; build it as a command.` });
209
+ const named = Object.keys(parts.flags ?? {});
210
+ const env = parts.flags?.["--env"];
211
+ const flags = CONFIRMATIONS.filter((one) => one.path === path && (one.when === void 0 || named.includes(one.when)) && !(typeof env === "string" && (one.unlessEnv ?? []).includes(env))).map((one) => one.flag);
212
+ const production = PRODUCTION_CONFIRMS[path];
213
+ const everyEnvironment = env === void 0 && actsOnEveryEnvironment(path) || actsOnEveryEnvironmentAlways(path);
214
+ if (typeof production === "string" && (typeof env === "string" && isProduction(env, options) || everyEnvironment)) flags.push(production);
215
+ const confirm = flags.length === 0 ? "" : ` with its ${[...new Set(flags)].join(" and ")} ${flags.length === 1 ? "phrase" : "phrases"}, which only a person gives,`;
216
+ const deleted = consequences.filter((one) => one.startsWith("deletes ")).map((one) => one.slice(8));
217
+ const said = [...deleted.length === 0 ? [] : [`deletes ${deleted.join(" and ")}`], ...consequences.filter((one) => !one.startsWith("deletes "))];
218
+ return `\`${spell(path, parts)}\`${confirm} ${said.join(" and ")}. That is a person's decision, not a fix.`;
219
+ }
220
+ /**
221
+ * **The remedy a line names**, decided by the command rather than by its site: a command a remedy may name is a
222
+ * `fix` and goes into the sentence; one that destroys something or runs against production is a person's
223
+ * decision, said as one, with no `fix`. A command that does not run is refused either way.
224
+ */
225
+ function remedy(path, parts = {}, options = {}) {
226
+ return remedyOf(path, parts, options);
227
+ }
228
+ /** {@link remedy}, with what a builder knows about where the values came from. */
229
+ function remedyOf(path, parts, options) {
230
+ if (isCommandPath(path) && consequencesOf(path, parts, options).length > 0) {
231
+ const sentence = decision(path, parts, options);
232
+ return {
233
+ fix: void 0,
234
+ say: () => sentence
235
+ };
236
+ }
237
+ const command = build(path, parts, options);
238
+ return {
239
+ fix: command,
240
+ say: (wrap) => wrap(command)
241
+ };
242
+ }
243
+ /** `key=value` entries, by key; an entry with no `=` names nothing. */
244
+ function setEntries(entries) {
245
+ const list = typeof entries === "string" ? [entries] : Array.isArray(entries) ? entries : [];
246
+ const values = {};
247
+ for (const entry of list) {
248
+ if (typeof entry !== "string") continue;
249
+ const at = entry.indexOf("=");
250
+ if (at > 0) values[entry.slice(0, at)] = entry.slice(at + 1);
251
+ }
252
+ return values;
253
+ }
254
+ /**
255
+ * **A run's own context**, from its parsed arguments. `set` is the raw `--set` entries where the parser keeps only the
256
+ * last (`collectSetFlags`); absent, they are read from `given.set`. `answered` is what the Worker's config states.
257
+ */
258
+ function callerOf(path, given, context = {}) {
259
+ return {
260
+ path,
261
+ given,
262
+ set: setEntries(context.set ?? given.set),
263
+ answered: new Set(context.answered ?? [])
264
+ };
265
+ }
266
+ /** The same caller, with what the run settled on its way — a prompt's answers — carried as `--set` too. */
267
+ function settledBy(caller, values) {
268
+ return {
269
+ ...caller,
270
+ set: {
271
+ ...caller.set,
272
+ ...values
273
+ }
274
+ };
275
+ }
276
+ /** The same caller, with the options its Worker's config answers. */
277
+ function answeredBy(caller, keys) {
278
+ return {
279
+ ...caller,
280
+ answered: /* @__PURE__ */ new Set([...caller.answered, ...keys])
281
+ };
282
+ }
283
+ /**
284
+ * A run again, as parts: each flag the command declares, carried when it was given — a boolean when true, a value
285
+ * when it differs from its default or when its default would change the meaning (`--env`, where the command reads
286
+ * it). A confirmation or a credential is never carried: each is a person's to give again.
287
+ */
288
+ function rerunParts(caller, options) {
289
+ const { path, given } = caller;
290
+ const def = COMMANDS[path];
291
+ const flags = {};
292
+ for (const [flag, flagDef] of Object.entries(def.flags)) {
293
+ if (CONFIRM_FLAGS.includes(flag)) continue;
294
+ if (CREDENTIAL_FLAGS.includes(flag)) continue;
295
+ const value = flag === "--set" ? Object.entries(caller.set).map(([key, one]) => `${key}=${one}`) : given[flag.slice(2)];
296
+ if (flagDef.type === "boolean") {
297
+ if (value === true) flags[flag] = true;
298
+ continue;
299
+ }
300
+ if (Array.isArray(value)) {
301
+ const values = value.filter((one) => typeof one === "string" && one.length > 0);
302
+ if (values.length > 0) flags[flag] = values;
303
+ continue;
304
+ }
305
+ if (typeof value !== "string" || value.length === 0) continue;
306
+ const onlyWith = MEANING_ONLY_WITH[path];
307
+ const meaning = MEANING_FLAGS.includes(flag) && (onlyWith === void 0 || given[onlyWith.slice(2)] === true);
308
+ if (value !== flagDef.default || meaning) flags[flag] = value;
309
+ }
310
+ return {
311
+ args: options.args ?? [],
312
+ flags: {
313
+ ...flags,
314
+ ...options.flags
315
+ }
316
+ };
317
+ }
318
+ /**
319
+ * **The caller's own command, to run again (#711)**, as a {@link Command} — rebuilt from the run's {@link Caller}, so
320
+ * a rerun asks for nothing the first run did not. `options.flags` adds what the run resolved for itself (the Worker it
321
+ * picked) or drops a flag whose work is done (`undefined`).
322
+ *
323
+ * It takes no command whose run again could be a person's decision — one that destroys something, or one a
324
+ * production `--env` makes a person's: that run again is {@link rerunRemedy}'s, which says so rather than throwing.
325
+ */
326
+ function rerunCommand(caller, options = {}) {
327
+ const path = caller.path;
328
+ if (!isCommandPath(path)) return pithyCommandOf(path);
329
+ if (Object.hasOwn(DESTRUCTIVE_COMMANDS, path) || Object.hasOwn(PRODUCTION_CONFIRMS, path)) throw new InternalError({ message: `\`${commandName(path)}\` may be a person's decision to run again; name it with rerunRemedy.` });
330
+ return build(path, rerunParts(caller, options), { given: true });
331
+ }
332
+ /**
333
+ * **The caller's own command, as a line names it to run again (#711)** — the {@link Remedy} for
334
+ * {@link rerunCommand}'s parts, for any command. A run again that a production `--env` makes a person's decision is
335
+ * said as one, with no `fix`, rather than thrown after the run did its work. `say` hands `wrap` the command in code
336
+ * and — where the run was given a credential by flag, which no line repeats — the flags to pass again beside it.
337
+ */
338
+ function rerunRemedy(caller, options = {}) {
339
+ const { path, given } = caller;
340
+ if (!isCommandPath(path)) return remedy(path);
341
+ const found = remedyOf(path, rerunParts(caller, options), {
342
+ productionEnvironments: options.productionEnvironments ?? [],
343
+ given: true
344
+ });
345
+ const withheld = CREDENTIAL_FLAGS.filter((flag) => {
346
+ const value = given[flag.slice(2)];
347
+ return typeof value === "string" && value.length > 0;
348
+ });
349
+ const credentials = withheld.join(" and ");
350
+ if (found.fix === void 0) {
351
+ const sentence = found.say((command) => command);
352
+ return {
353
+ fix: void 0,
354
+ say: () => withheld.length === 0 ? sentence : `${sentence} It takes the ${credentials} this run was given.`
355
+ };
356
+ }
357
+ const line = withheld.length === 0 ? `\`${found.fix}\`` : `\`${found.fix}\` with the ${credentials} this run was given`;
358
+ return {
359
+ fix: found.fix,
360
+ say: (wrap) => wrap(line)
361
+ };
362
+ }
363
+ /**
364
+ * **A command named in prose, not given to run** — "as `pithy add` wrote it". Checked to exist, and spelled with
365
+ * nothing after it. A line that tells somebody to run a command builds it with {@link pithyCommand}.
366
+ */
367
+ function commandName(path) {
368
+ return `${CLI} ${path}`;
369
+ }
370
+ /** {@link commandName}, for a path that is a value: refused when the CLI has no such command or group. */
371
+ function commandNameOf(path) {
372
+ if (!isCommandPath(path) && !isGroupPath(path)) throw new InternalError({ message: `A line names \`pithy ${path}\`, which is not a command.` });
373
+ return `${CLI} ${path}`;
374
+ }
375
+ /**
376
+ * {@link commandNameOf}, for a path as a parser walked it, word by word. Empty is the root, which is the CLI
377
+ * itself rather than a command, and is named as such.
378
+ */
379
+ function commandNameOfPath(path) {
380
+ return path.length === 0 ? CLI : commandNameOf(path.join(" "));
381
+ }
382
+ /**
383
+ * **The CLI at a version** — `pithy 0.9.5`, as a line names a release rather than a command. A line never spells
384
+ * `pithy` before an interpolation: the gate reads that as a command it cannot check.
385
+ */
386
+ function cliAtVersion(version) {
387
+ return `${CLI} ${version}`;
388
+ }
389
+ /**
390
+ * **How each package manager runs a project's own binary**: `bun x`, `pnpm exec`, `yarn`, `npx`. The one table
391
+ * both a spawned process (`packages/cli/src/project/packageManager.ts`'s `execArgs`) and a line naming one read.
392
+ */
393
+ const PACKAGE_EXEC = {
394
+ bun: ["bun", "x"],
395
+ pnpm: ["pnpm", "exec"],
396
+ yarn: ["yarn"],
397
+ npm: ["npx"]
398
+ };
399
+ /**
400
+ * **A built command, run through the project's own package manager** — the `pithy` the project installed, never
401
+ * a global one, which may be an older CLI than the line is about. Takes a {@link Command}, so what follows the
402
+ * prefix was built and runs as written.
403
+ */
404
+ function viaPackageManager(pm, command) {
405
+ return [...PACKAGE_EXEC[pm], command].join(" ");
406
+ }
407
+ /** `a`, `a or b`, `a, b or c`, each in code: commands a reader picks one of. */
408
+ function commandAlternatives(commands) {
409
+ const coded = commands.map((command) => `\`${command}\``);
410
+ return coded.length <= 1 ? coded.join("") : `${coded.slice(0, -1).join(", ")} or ${coded.at(-1)}`;
411
+ }
412
+ /** `a`, `a and b`, `a, b and c`, each in code: commands that all run. */
413
+ function commandSequence(commands) {
414
+ const coded = commands.map((command) => `\`${command}\``);
415
+ return coded.length <= 1 ? coded.join("") : `${coded.slice(0, -1).join(", ")} and ${coded.at(-1)}`;
416
+ }
417
+ //#endregion
418
+ export { PACKAGE_EXEC, actsOnEveryEnvironment, actsOnEveryEnvironmentAlways, answeredBy, callerOf, cliAtVersion, commandAlternatives, commandFaults, commandFlag, commandName, commandNameOf, commandNameOfPath, commandSequence, consequencesOf, isCommandPath, isGroupPath, personDecision, pithyCommand, pithyCommandOf, remedy, rerunCommand, rerunRemedy, settledBy, viaPackageManager, workerDirOf };