@sous-io/sous 0.1.0 → 0.2.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 (206) hide show
  1. package/README.md +121 -35
  2. package/bin/run.js +10 -1
  3. package/docs/markdown/README.md +27 -0
  4. package/docs/markdown/_sidebar.md +18 -0
  5. package/docs/markdown/commands.md +308 -0
  6. package/docs/markdown/config-discovery.md +74 -0
  7. package/docs/markdown/config-inspection.md +69 -0
  8. package/docs/markdown/config-layers.md +92 -0
  9. package/docs/markdown/config-variables.md +79 -0
  10. package/docs/markdown/configuration.md +71 -0
  11. package/docs/markdown/design-principles.md +59 -0
  12. package/docs/markdown/repositories-authoring.md +408 -0
  13. package/docs/markdown/repositories-consuming.md +580 -0
  14. package/docs/markdown/repositories-file-formats.md +1084 -0
  15. package/docs/markdown/repositories-variables.md +387 -0
  16. package/docs/markdown/repositories.md +303 -0
  17. package/docs/markdown/skill-categories.md +58 -0
  18. package/package.json +73 -9
  19. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/SKILL.tpl.md +20 -20
  20. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/examples/about-something.md +2 -2
  21. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/examples/do-something.md +1 -1
  22. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/advanced-patterns.md +6 -6
  23. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/commands.md +5 -5
  24. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/frontmatter.md +3 -3
  25. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-liquid-templates/SKILL.tpl.md +40 -25
  26. package/recipes/core/sous-skills/skills/about-sous/SKILL.tpl.md +70 -0
  27. package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +75 -0
  28. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/create-skill/SKILL.tpl.md +8 -9
  29. package/recipes/core/sous-skills/sous.recipe.yaml +45 -0
  30. package/sous.config.schema.json +337 -0
  31. package/src/base-command.ts +220 -67
  32. package/src/commands/build.ts +150 -73
  33. package/src/commands/clear.ts +23 -15
  34. package/src/commands/compile.ts +74 -16
  35. package/src/commands/config/get.ts +110 -0
  36. package/src/commands/config/show.ts +32 -0
  37. package/src/commands/config/validate.ts +53 -0
  38. package/src/commands/help.ts +46 -0
  39. package/src/commands/launch.ts +36 -14
  40. package/src/commands/lock/rebuild.ts +241 -0
  41. package/src/commands/lock/show.ts +115 -0
  42. package/src/commands/namespace/list.ts +117 -0
  43. package/src/commands/namespace/show.ts +110 -0
  44. package/src/commands/prune.ts +3 -11
  45. package/src/commands/recipe/list.ts +95 -0
  46. package/src/commands/recipe/show.ts +301 -0
  47. package/src/commands/repo/add.ts +145 -0
  48. package/src/commands/repo/gc.ts +172 -0
  49. package/src/commands/repo/init.ts +136 -0
  50. package/src/commands/repo/link.ts +500 -0
  51. package/src/commands/repo/list.ts +179 -0
  52. package/src/commands/repo/release.ts +619 -0
  53. package/src/commands/repo/remove.ts +193 -0
  54. package/src/commands/repo/search.ts +189 -0
  55. package/src/commands/repo/submit.ts +133 -0
  56. package/src/commands/repo/unlink.ts +147 -0
  57. package/src/commands/subscription/add.ts +285 -0
  58. package/src/commands/subscription/list.ts +129 -0
  59. package/src/commands/subscription/remove.ts +181 -0
  60. package/src/commands/vars/ask.ts +374 -0
  61. package/src/commands/vars/index.ts +79 -0
  62. package/src/commands/vars/list.ts +67 -0
  63. package/src/commands/vars/show.ts +77 -0
  64. package/src/config-command.ts +30 -0
  65. package/src/lib/build-service.ts +206 -54
  66. package/src/lib/config-discovery.ts +220 -27
  67. package/src/lib/config-inspect.ts +145 -0
  68. package/src/lib/config-kernel.mjs +377 -0
  69. package/src/lib/config-schema.ts +361 -0
  70. package/src/lib/env-file.ts +328 -0
  71. package/src/lib/env-local.ts +18 -1
  72. package/src/lib/errors.ts +32 -0
  73. package/src/lib/include-resolver.ts +108 -15
  74. package/src/lib/interactive.ts +165 -0
  75. package/src/lib/markdown-compiler.ts +118 -37
  76. package/src/lib/package-info.ts +25 -0
  77. package/src/lib/pid-service.ts +32 -21
  78. package/src/lib/refs/find.ts +589 -0
  79. package/src/lib/refs/index.ts +12 -0
  80. package/src/lib/refs/pick.ts +147 -0
  81. package/src/lib/refs/scopes.ts +61 -0
  82. package/src/lib/repos/catalog-display.ts +116 -0
  83. package/src/lib/repos/catalog-inputs.ts +160 -0
  84. package/src/lib/repos/catalog.ts +722 -0
  85. package/src/lib/repos/core-recipe.ts +105 -0
  86. package/src/lib/repos/defaults.ts +175 -0
  87. package/src/lib/repos/formats/common.ts +389 -0
  88. package/src/lib/repos/formats/index-file.ts +215 -0
  89. package/src/lib/repos/formats/links-map.ts +96 -0
  90. package/src/lib/repos/formats/lockfile.ts +167 -0
  91. package/src/lib/repos/formats/patterns.ts +57 -0
  92. package/src/lib/repos/formats/recipe-manifest.ts +395 -0
  93. package/src/lib/repos/formats/repo-manifest.ts +88 -0
  94. package/src/lib/repos/formats/store-entry.ts +84 -0
  95. package/src/lib/repos/freshness.ts +208 -0
  96. package/src/lib/repos/git-clone.ts +312 -0
  97. package/src/lib/repos/identity.ts +89 -0
  98. package/src/lib/repos/index.ts +58 -0
  99. package/src/lib/repos/links.ts +353 -0
  100. package/src/lib/repos/load-manifest.ts +236 -0
  101. package/src/lib/repos/lock-service.ts +453 -0
  102. package/src/lib/repos/locked-namespace-resolver.ts +90 -0
  103. package/src/lib/repos/locked-recipes.ts +254 -0
  104. package/src/lib/repos/managed-layer.ts +422 -0
  105. package/src/lib/repos/namespace-resolver.ts +370 -0
  106. package/src/lib/repos/providers/base.ts +206 -0
  107. package/src/lib/repos/providers/git.ts +233 -0
  108. package/src/lib/repos/providers/github.ts +294 -0
  109. package/src/lib/repos/providers/gitlab.ts +263 -0
  110. package/src/lib/repos/providers/http.ts +102 -0
  111. package/src/lib/repos/providers/index-cache.ts +382 -0
  112. package/src/lib/repos/providers/index.ts +106 -0
  113. package/src/lib/repos/providers/local.ts +391 -0
  114. package/src/lib/repos/providers/provider.ts +401 -0
  115. package/src/lib/repos/recipe-config-layers.ts +287 -0
  116. package/src/lib/repos/recipe-targets.ts +223 -0
  117. package/src/lib/repos/ref-search.ts +46 -0
  118. package/src/lib/repos/ref.ts +513 -0
  119. package/src/lib/repos/reference-report.ts +122 -0
  120. package/src/lib/repos/release/bump.ts +161 -0
  121. package/src/lib/repos/release/git-state.ts +305 -0
  122. package/src/lib/repos/release/index-builder.ts +635 -0
  123. package/src/lib/repos/release/index.ts +16 -0
  124. package/src/lib/repos/release/plan.ts +512 -0
  125. package/src/lib/repos/release/submit-service.ts +496 -0
  126. package/src/lib/repos/release/tags.ts +243 -0
  127. package/src/lib/repos/release/validate.ts +463 -0
  128. package/src/lib/repos/resolver.ts +789 -0
  129. package/src/lib/repos/scaffold/index.ts +238 -0
  130. package/src/lib/repos/scaffold/templates.ts +413 -0
  131. package/src/lib/repos/seed.ts +414 -0
  132. package/src/lib/repos/store/contract.ts +64 -0
  133. package/src/lib/repos/store/hash.ts +114 -0
  134. package/src/lib/repos/store/recipe-store.ts +599 -0
  135. package/src/lib/repos/store/settings.ts +58 -0
  136. package/src/lib/repos/subscription-service.ts +2678 -0
  137. package/src/lib/repos/trust.ts +447 -0
  138. package/src/lib/settings.ts +546 -189
  139. package/src/lib/sous-home.ts +104 -0
  140. package/src/lib/state.ts +52 -20
  141. package/src/lib/vars/ask.ts +1152 -0
  142. package/src/lib/vars/definition-source.ts +252 -0
  143. package/src/lib/vars/display.ts +233 -0
  144. package/src/lib/vars/index.ts +18 -0
  145. package/src/lib/vars/ladder.ts +282 -0
  146. package/src/lib/vars/mappings.ts +265 -0
  147. package/src/lib/vars/names.ts +94 -0
  148. package/src/lib/vars/preanswers.ts +395 -0
  149. package/src/lib/vars/question-plan.ts +218 -0
  150. package/src/lib/vars/report.ts +228 -0
  151. package/src/lib/vars/safe-regex.ts +235 -0
  152. package/src/lib/vars/validate.ts +312 -0
  153. package/src/lib/watch-loop.ts +148 -0
  154. package/src/templating/init-liquid-engine.ts +58 -16
  155. package/src/utils/choice-prompt.ts +143 -0
  156. package/src/utils/command-errors.ts +186 -0
  157. package/src/utils/command-help.ts +45 -0
  158. package/src/utils/confirm-prompt.ts +110 -0
  159. package/src/utils/flags.ts +153 -0
  160. package/src/utils/formatting.ts +540 -55
  161. package/src/utils/prompts.ts +35 -1
  162. package/src/utils/sous-directory.ts +245 -0
  163. package/src/utils/table.ts +603 -0
  164. package/src/utils/value-prompt.ts +119 -0
  165. package/bin/xcv +0 -5
  166. package/shared-prompts/_partials/resume-task.md +0 -51
  167. package/shared-prompts/_partials/sub-agent-delegation.md +0 -32
  168. package/shared-prompts/_partials/update-task-file.md +0 -52
  169. package/shared-prompts/memories/automated-browser-tasks/INDEX.tpl.md +0 -52
  170. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/SKILL.tpl.md +0 -102
  171. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/auth-failure-handling.mjs +0 -81
  172. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/chained-workflow.mjs +0 -126
  173. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/simple-fetch.mjs +0 -92
  174. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/architecture.md +0 -61
  175. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/auth-and-sessions.md +0 -65
  176. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/ctx-api.md +0 -96
  177. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/installation.md +0 -104
  178. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/script-conventions.md +0 -243
  179. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/chrome-state.mjs +0 -148
  180. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/debug.mjs +0 -383
  181. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/debug.spec.mjs +0 -267
  182. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/eslint.config.mjs +0 -56
  183. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/harness.mjs +0 -169
  184. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/keyring.mjs +0 -59
  185. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/logger.mjs +0 -25
  186. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/params.mjs +0 -140
  187. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/run.mjs +0 -140
  188. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/settings.tpl.mjs +0 -1
  189. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/utils.mjs +0 -185
  190. package/shared-prompts/skills/automated-browser-tasks/create-automated-browser-task/SKILL.tpl.md +0 -52
  191. package/shared-prompts/skills/automated-browser-tasks/running-automated-browser-tasks/SKILL.tpl.md +0 -59
  192. package/shared-prompts/skills/automated-browser-tasks/update-automated-browser-task/SKILL.tpl.md +0 -47
  193. package/shared-prompts/skills/control-flow/approve/SKILL.tpl.md +0 -26
  194. package/shared-prompts/skills/control-flow/opine/SKILL.tpl.md +0 -58
  195. package/shared-prompts/skills/control-flow/repeat/SKILL.tpl.md +0 -27
  196. package/shared-prompts/skills/control-flow/research/SKILL.tpl.md +0 -34
  197. package/shared-prompts/skills/sous-skills/about-sous/SKILL.tpl.md +0 -51
  198. package/shared-prompts/skills/task-files/about-task-files/SKILL.tpl.md +0 -122
  199. package/shared-prompts/skills/task-files/continue-task-in-new-branch/SKILL.tpl.md +0 -80
  200. package/shared-prompts/skills/task-files/go/SKILL.tpl.md +0 -14
  201. package/shared-prompts/skills/task-files/resume-task/SKILL.tpl.md +0 -13
  202. package/shared-prompts/skills/task-files/start-task/SKILL.tpl.md +0 -93
  203. package/shared-prompts/skills/task-files/update/SKILL.tpl.md +0 -14
  204. package/shared-prompts/skills/task-files/update-task-file/SKILL.tpl.md +0 -13
  205. /package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/substitutions.md +0 -0
  206. /package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-liquid-templates/references/liquid-filters.md +0 -0
@@ -0,0 +1,589 @@
1
+ /**
2
+ * Working out what a word on the command line names.
3
+ *
4
+ * Sous commands take a reference where a person would say a name: `sous
5
+ * subscribe task-files`, `sous namespace show workflow`, `sous vars ask apiUrl`.
6
+ * One module settles what such a word means, for every command, so a reference
7
+ * that resolves in one place resolves the same way everywhere.
8
+ *
9
+ * A reference may be written at any level of qualification, and the fully
10
+ * qualified spelling is always accepted:
11
+ *
12
+ * sous-recipes a repository
13
+ * sous-recipes:workflow a namespace, fully qualified
14
+ * workflow the same namespace, bare
15
+ * sous-recipes:workflow/task-files a recipe, fully qualified
16
+ * workflow/task-files the same recipe, partly qualified
17
+ * task-files the same recipe, bare
18
+ * workflow/task-files.taskFileRoot a variable, partly qualified
19
+ * taskFileRoot the same variable, bare
20
+ * SOUS_VAR_TASK_FILE_ROOT the environment variable answering it
21
+ *
22
+ * Matching is case-sensitive, because every identifier in sous is (namespaces
23
+ * and recipes are lowercase kebab-case, variables are camelCase, environment
24
+ * variable names are upper snake case), and a case-insensitive search would
25
+ * report a variable and its own environment variable name as the same thing.
26
+ *
27
+ * The order matches come back in is part of the contract, because it is the
28
+ * order they are offered in and the one `--accept-first` picks from. Matches
29
+ * are sorted by how qualified the spelling that matched was (a fully qualified
30
+ * reference first, then a partly qualified one, then a bare name, and an
31
+ * environment variable name last), then by scope in the order `SCOPE_ORDER`
32
+ * documents, then by repository in search order, then alphabetically by
33
+ * namespace, recipe and variable.
34
+ */
35
+
36
+ import type { IndexFile } from "../repos/formats/index-file.js";
37
+ import type { ParsedRef } from "../repos/ref.js";
38
+ import type { DefinedVariable } from "../vars/definition-source.js";
39
+ import { variableCandidates, type LadderContext } from "../vars/ladder.js";
40
+ import { bareName } from "../vars/names.js";
41
+ import { SCOPE_LABELS, SousScope, scopeRank } from "./scopes.js";
42
+
43
+ // --- What the search reads ----------------------------------------------------------------------
44
+
45
+ /** One namespace a reference could name. */
46
+ export type ReferenceNamespace = {
47
+ /** The namespace name. */
48
+ name: string;
49
+ /** Its one-paragraph summary, when the index carries one. */
50
+ description?: string;
51
+ };
52
+
53
+ /** One recipe a reference could name. */
54
+ export type ReferenceRecipe = {
55
+ /** The namespace publishing it. */
56
+ namespace: string;
57
+ /** The recipe name. */
58
+ name: string;
59
+ /** Its one-paragraph summary, when the index carries one. */
60
+ description?: string;
61
+ };
62
+
63
+ /** One repository a reference could name, and what it publishes. */
64
+ export type ReferenceRepo = {
65
+ /** The short name this project calls it. */
66
+ name: string;
67
+ /** Where it lives, as the project's config records it. */
68
+ url?: string;
69
+ /** Every namespace it publishes. */
70
+ namespaces: ReferenceNamespace[];
71
+ /** Every recipe it publishes. */
72
+ recipes: ReferenceRecipe[];
73
+ };
74
+
75
+ /**
76
+ * Everything a reference resolves against. Each part is optional, because a
77
+ * command supplies only what it can see: a browsing command has repositories
78
+ * and their indexes, `sous vars ask --file` has definitions and no repository
79
+ * at all.
80
+ */
81
+ export type ReferenceContext = {
82
+ /**
83
+ * The repositories to search, in the order their matches should be listed:
84
+ * the built-in repository first, then the ones the config names, in config
85
+ * order.
86
+ */
87
+ repos?: ReferenceRepo[];
88
+ /** The variable definitions in play, with the recipe that published each. */
89
+ variables?: DefinedVariable[];
90
+ /**
91
+ * The environment layers, which decide which generated environment variable
92
+ * names are in use. Without it only a definition's declared `env` name is
93
+ * searched.
94
+ */
95
+ ladder?: LadderContext;
96
+ };
97
+
98
+ // --- What the search answers --------------------------------------------------------------------
99
+
100
+ /**
101
+ * How qualified the spelling that matched was. Lower is more specific, and the
102
+ * listing order follows it.
103
+ */
104
+ export enum Qualification {
105
+ /** The reference named the whole identity, repository included. */
106
+ Full = 0,
107
+ /** The reference named part of the identity, such as `namespace/recipe`. */
108
+ Partial = 1,
109
+ /** The reference was a bare name. */
110
+ Bare = 2,
111
+ /** The reference was an environment variable name. */
112
+ EnvName = 3,
113
+ }
114
+
115
+ /** One thing a reference could have meant. */
116
+ export type ReferenceMatch = {
117
+ /** What kind of thing it is. */
118
+ scope: SousScope;
119
+ /**
120
+ * The fully qualified name of it: `repo`, `repo:namespace`,
121
+ * `repo:namespace/recipe`, or `repo:namespace/recipe.variable`. Two matches
122
+ * never share a key within one scope, so it is safe to compare and to print.
123
+ */
124
+ key: string;
125
+ /** The short name of the thing itself, without any qualifier. */
126
+ label: string;
127
+ /** What it is, in one line: a description, a prompt, or where it lives. */
128
+ detail?: string;
129
+ /** The repository it belongs to, when it has one. */
130
+ repo?: string;
131
+ /** The namespace it is, or the one it lives in. */
132
+ namespace?: string;
133
+ /** The recipe it is, or the one that declares it. */
134
+ recipe?: string;
135
+ /** The variable it is, or the one an environment variable name answers. */
136
+ variable?: string;
137
+ /** The environment variable name, when the reference named one. */
138
+ envName?: string;
139
+ /** How qualified the spelling that matched was. */
140
+ qualification: Qualification;
141
+ };
142
+
143
+ // --- The search ---------------------------------------------------------------------------------
144
+
145
+ /**
146
+ * Every meaning a reference could have, in the documented order.
147
+ *
148
+ * Nothing is fetched and nothing is asked: this reads the context it is handed
149
+ * and returns what matched. An empty result means the reference named nothing
150
+ * the caller can see; one result is the answer; several are offered to the
151
+ * caller by `pickReference`.
152
+ *
153
+ * @param search - The reference exactly as it was written.
154
+ * @param scopes - The kinds of thing this command accepts.
155
+ * @param context - The repositories, definitions and environment layers to search.
156
+ */
157
+ export function findReference(
158
+ search: string,
159
+ scopes: readonly SousScope[],
160
+ context: ReferenceContext
161
+ ): ReferenceMatch[] {
162
+ const wanted = new Set(scopes);
163
+ const term = search.trim();
164
+ if (term.length === 0) return [];
165
+
166
+ const matches: ReferenceMatch[] = [];
167
+ const repoOrder = new Map((context.repos ?? []).map((repo, position) => [repo.name, position]));
168
+
169
+ for (const repo of context.repos ?? []) {
170
+ if (wanted.has(SousScope.Repository) && term === repo.name) {
171
+ matches.push({
172
+ scope: SousScope.Repository,
173
+ key: repo.name,
174
+ label: repo.name,
175
+ repo: repo.name,
176
+ qualification: Qualification.Full,
177
+ ...(repo.url === undefined ? {} : { detail: repo.url }),
178
+ });
179
+ }
180
+
181
+ if (wanted.has(SousScope.Namespace)) {
182
+ for (const namespace of repo.namespaces) {
183
+ const key = `${repo.name}:${namespace.name}`;
184
+ const qualification = qualificationOf(term, [
185
+ [key, Qualification.Full],
186
+ [namespace.name, Qualification.Bare],
187
+ ]);
188
+ if (qualification === undefined) continue;
189
+ matches.push({
190
+ scope: SousScope.Namespace,
191
+ key,
192
+ label: namespace.name,
193
+ repo: repo.name,
194
+ namespace: namespace.name,
195
+ qualification,
196
+ ...(namespace.description === undefined ? {} : { detail: namespace.description }),
197
+ });
198
+ }
199
+ }
200
+
201
+ if (wanted.has(SousScope.Recipe)) {
202
+ for (const recipe of repo.recipes) {
203
+ const key = `${repo.name}:${recipe.namespace}/${recipe.name}`;
204
+ const qualification = qualificationOf(term, [
205
+ [key, Qualification.Full],
206
+ [`${recipe.namespace}/${recipe.name}`, Qualification.Partial],
207
+ [`${repo.name}:${recipe.name}`, Qualification.Partial],
208
+ [recipe.name, Qualification.Bare],
209
+ ]);
210
+ if (qualification === undefined) continue;
211
+ matches.push({
212
+ scope: SousScope.Recipe,
213
+ key,
214
+ label: recipe.name,
215
+ repo: repo.name,
216
+ namespace: recipe.namespace,
217
+ recipe: recipe.name,
218
+ qualification,
219
+ ...(recipe.description === undefined ? {} : { detail: recipe.description }),
220
+ });
221
+ }
222
+ }
223
+ }
224
+
225
+ if (wanted.has(SousScope.VariableName) || wanted.has(SousScope.EnvVarName)) {
226
+ for (const defined of context.variables ?? []) {
227
+ const key = variableReferenceKey(defined);
228
+ const { repo, namespace, name: recipe } = defined.recipe;
229
+ const variable = defined.definition.name;
230
+
231
+ if (wanted.has(SousScope.VariableName)) {
232
+ const qualification = qualificationOf(term, [
233
+ [key, Qualification.Full],
234
+ [`${namespace}/${recipe}.${variable}`, Qualification.Partial],
235
+ [`${recipe}.${variable}`, Qualification.Partial],
236
+ [`${repo}:${variable}`, Qualification.Partial],
237
+ [variable, Qualification.Bare],
238
+ ]);
239
+ if (qualification !== undefined) {
240
+ matches.push({
241
+ scope: SousScope.VariableName,
242
+ key,
243
+ label: variable,
244
+ repo,
245
+ namespace,
246
+ recipe,
247
+ variable,
248
+ qualification,
249
+ detail: defined.definition.prompt,
250
+ });
251
+ continue;
252
+ }
253
+ }
254
+
255
+ if (wanted.has(SousScope.EnvVarName) && environmentNamesFor(defined, context).has(term)) {
256
+ matches.push({
257
+ scope: SousScope.EnvVarName,
258
+ key,
259
+ label: term,
260
+ repo,
261
+ namespace,
262
+ recipe,
263
+ variable,
264
+ envName: term,
265
+ qualification: Qualification.EnvName,
266
+ detail: defined.definition.prompt,
267
+ });
268
+ }
269
+ }
270
+ }
271
+
272
+ return sortMatches(matches, repoOrder);
273
+ }
274
+
275
+ /**
276
+ * The repository a reference names.
277
+ *
278
+ * @param search - The reference exactly as it was written.
279
+ * @param context - What to search.
280
+ */
281
+ export function findRepository(
282
+ search: string,
283
+ context: ReferenceContext
284
+ ): ReferenceMatch[] {
285
+ return findReference(search, [SousScope.Repository], context);
286
+ }
287
+
288
+ /**
289
+ * The namespace a reference names, bare or written as `repository:namespace`.
290
+ *
291
+ * @param search - The reference exactly as it was written.
292
+ * @param context - What to search.
293
+ */
294
+ export function findNamespace(search: string, context: ReferenceContext): ReferenceMatch[] {
295
+ return findReference(search, [SousScope.Namespace], context);
296
+ }
297
+
298
+ /**
299
+ * The recipe a reference names, at any level of qualification.
300
+ *
301
+ * @param search - The reference exactly as it was written.
302
+ * @param context - What to search.
303
+ */
304
+ export function findRecipe(search: string, context: ReferenceContext): ReferenceMatch[] {
305
+ return findReference(search, [SousScope.Recipe], context);
306
+ }
307
+
308
+ /**
309
+ * The variable a reference names, by its own name or by the name of an
310
+ * environment variable that answers it.
311
+ *
312
+ * @param search - The reference exactly as it was written.
313
+ * @param context - What to search.
314
+ */
315
+ export function findVariable(search: string, context: ReferenceContext): ReferenceMatch[] {
316
+ return findReference(search, [SousScope.VariableName, SousScope.EnvVarName], context);
317
+ }
318
+
319
+ // --- Describing what was found ------------------------------------------------------------------
320
+
321
+ /**
322
+ * Describes one match in the words a person choosing between them needs: the
323
+ * fully qualified name, and what it actually means.
324
+ *
325
+ * @param match - The match to describe.
326
+ */
327
+ export function describeReference(match: ReferenceMatch): string {
328
+ const summary = match.detail === undefined ? "" : `: ${match.detail}`;
329
+
330
+ switch (match.scope) {
331
+ case SousScope.Repository:
332
+ return (
333
+ `${match.key} (the repository '${match.repo}'` +
334
+ `${match.detail === undefined ? "" : `, at ${match.detail}`})`
335
+ );
336
+ case SousScope.Namespace:
337
+ return (
338
+ `${match.key} (the whole namespace '${match.namespace}' in the ` +
339
+ `repository '${match.repo}')`
340
+ );
341
+ case SousScope.Recipe:
342
+ return (
343
+ `${match.key} (the recipe '${match.recipe}' in the namespace ` +
344
+ `'${match.namespace}' of the repository '${match.repo}'${summary})`
345
+ );
346
+ case SousScope.VariableName:
347
+ return (
348
+ `${match.key} (the variable '${match.variable}' of the recipe ` +
349
+ `'${match.namespace}/${match.recipe}'${summary})`
350
+ );
351
+ case SousScope.EnvVarName:
352
+ return (
353
+ `${match.envName} (the environment variable answering '${match.variable}' ` +
354
+ `of the recipe '${match.namespace}/${match.recipe}'${summary})`
355
+ );
356
+ }
357
+ }
358
+
359
+ /**
360
+ * The plain-language name of what a match is, for a sentence that has to say
361
+ * what kind of thing was found.
362
+ *
363
+ * @param match - The match to name.
364
+ */
365
+ export function referenceKindLabel(match: ReferenceMatch): string {
366
+ return SCOPE_LABELS[match.scope];
367
+ }
368
+
369
+ // --- Building a context -------------------------------------------------------------------------
370
+
371
+ /**
372
+ * Turns cached repository indexes into the repositories a reference searches,
373
+ * in the order they were given.
374
+ *
375
+ * @param repoOrder - The repository short names, in search order.
376
+ * @param indexes - Each repository's cached index, keyed by short name.
377
+ */
378
+ export function referenceReposFromIndexes(
379
+ repoOrder: readonly string[],
380
+ indexes: Map<string, IndexFile>,
381
+ urls: Record<string, string | undefined> = {}
382
+ ): ReferenceRepo[] {
383
+ const repos: ReferenceRepo[] = [];
384
+
385
+ for (const name of repoOrder) {
386
+ const index = indexes.get(name);
387
+ if (index === undefined) continue;
388
+
389
+ const namespaces: ReferenceNamespace[] = Object.entries(index.namespaces).map(
390
+ ([namespace, declared]) => ({
391
+ name: namespace,
392
+ ...(declared?.description === undefined ? {} : { description: declared.description }),
393
+ })
394
+ );
395
+
396
+ const recipes: ReferenceRecipe[] = [];
397
+ for (const [key, recipe] of Object.entries(index.recipes)) {
398
+ const slash = key.indexOf("/");
399
+ if (slash === -1) continue;
400
+ recipes.push({
401
+ namespace: key.slice(0, slash),
402
+ name: key.slice(slash + 1),
403
+ ...(recipe.description === undefined ? {} : { description: recipe.description }),
404
+ });
405
+ }
406
+
407
+ const url = urls[name];
408
+ repos.push({ name, namespaces, recipes, ...(url === undefined ? {} : { url }) });
409
+ }
410
+
411
+ return repos;
412
+ }
413
+
414
+ /**
415
+ * The repositories, namespaces and recipes the variable definitions in play
416
+ * belong to, derived from the definitions themselves.
417
+ *
418
+ * This is the context a variables command searches: a namespace that publishes
419
+ * no variable this project holds has no questions to ask, so naming it would
420
+ * resolve to an empty set of questions rather than to an answer. Deriving the
421
+ * context from the definitions also means `sous vars ask --file` resolves
422
+ * references exactly as a subscribed project does.
423
+ *
424
+ * @param variables - The variable definitions in play.
425
+ * @param ladder - The environment layers, for environment variable names.
426
+ */
427
+ export function referenceContextFromVariables(
428
+ variables: DefinedVariable[],
429
+ ladder?: LadderContext
430
+ ): ReferenceContext {
431
+ const repos = new Map<string, ReferenceRepo>();
432
+
433
+ for (const defined of variables) {
434
+ const { repo: repoName, namespace, name: recipe } = defined.recipe;
435
+
436
+ let repo = repos.get(repoName);
437
+ if (repo === undefined) {
438
+ repo = { name: repoName, namespaces: [], recipes: [] };
439
+ repos.set(repoName, repo);
440
+ }
441
+
442
+ if (!repo.namespaces.some((entry) => entry.name === namespace)) {
443
+ repo.namespaces.push({ name: namespace });
444
+ }
445
+ if (!repo.recipes.some((entry) => entry.namespace === namespace && entry.name === recipe)) {
446
+ repo.recipes.push({ namespace, name: recipe });
447
+ }
448
+ }
449
+
450
+ return {
451
+ repos: [...repos.values()],
452
+ variables,
453
+ ...(ladder === undefined ? {} : { ladder }),
454
+ };
455
+ }
456
+
457
+ /**
458
+ * The fully qualified name of one variable: `repo:namespace/recipe.variable`.
459
+ * It is what a reference to a variable resolves to, and what a command hands
460
+ * back to the asking machinery when it has decided which variables to ask.
461
+ *
462
+ * @param defined - The definition and the recipe that published it.
463
+ */
464
+ export function variableReferenceKey(defined: DefinedVariable): string {
465
+ const { repo, namespace, name } = defined.recipe;
466
+ return `${repo}:${namespace}/${name}.${defined.definition.name}`;
467
+ }
468
+
469
+ // --- Turning a match back into a ref --------------------------------------------------------------
470
+
471
+ /**
472
+ * Turns a chosen match into a parsed ref, carrying over the version range the
473
+ * original ref asked for. The repository qualifier is kept, so what gets
474
+ * resolved is exactly the match that was chosen and not another repository's
475
+ * recipe of the same name.
476
+ *
477
+ * @param match - The match that was chosen.
478
+ * @param original - The ref as the user wrote it.
479
+ */
480
+ export function referenceToRef(match: ReferenceMatch, original: ParsedRef): ParsedRef {
481
+ return {
482
+ ...(match.repo === undefined ? {} : { repo: match.repo }),
483
+ namespace: match.namespace ?? original.namespace,
484
+ ...(match.recipe === undefined ? {} : { recipe: match.recipe }),
485
+ ...(original.range === undefined ? {} : { range: original.range }),
486
+ };
487
+ }
488
+
489
+ // --- The pieces -----------------------------------------------------------------------------------
490
+
491
+ /**
492
+ * How qualified a spelling of one thing the search term matched, or undefined
493
+ * when it matched none of them. Spellings are given most specific first, and
494
+ * the first one that matches wins.
495
+ *
496
+ * @param term - The search term.
497
+ * @param spellings - Each spelling of this thing, with how qualified it is.
498
+ */
499
+ function qualificationOf(
500
+ term: string,
501
+ spellings: [string, Qualification][]
502
+ ): Qualification | undefined {
503
+ for (const [spelling, qualification] of spellings) {
504
+ if (spelling === term) return qualification;
505
+ }
506
+ return undefined;
507
+ }
508
+
509
+ /**
510
+ * Every environment variable name that would be recognized as naming one
511
+ * variable: the name its definition declares (a recipe may bind an existing
512
+ * variable such as `GITHUB_TOKEN`), plus any generated name on the resolution
513
+ * ladder that is actually in use in `.sous/.env` or `.sous/.env.local`.
514
+ *
515
+ * Generated names are only searched when they are in use, because every
516
+ * variable generates four of them and a project would otherwise be told that a
517
+ * name nothing has ever set names one of its variables.
518
+ *
519
+ * @param defined - The definition and the recipe that published it.
520
+ * @param context - The environment layers, when the caller has them.
521
+ */
522
+ function environmentNamesFor(
523
+ defined: DefinedVariable,
524
+ context: ReferenceContext
525
+ ): Set<string> {
526
+ const names = new Set<string>([bareName(defined.definition)]);
527
+
528
+ const ladder = context.ladder;
529
+ if (ladder === undefined) return names;
530
+
531
+ const inUse = new Set([...Object.keys(ladder.localEnv), ...Object.keys(ladder.sharedEnv)]);
532
+ for (const candidate of variableCandidates(defined, ladder)) {
533
+ if (inUse.has(candidate.envName)) names.add(candidate.envName);
534
+ }
535
+
536
+ return names;
537
+ }
538
+
539
+ /**
540
+ * Sorts matches into the documented listing order and drops anything the same
541
+ * search found twice.
542
+ *
543
+ * @param matches - Every match, in the order they were collected.
544
+ * @param repoOrder - Where each repository sits in the search order.
545
+ */
546
+ function sortMatches(
547
+ matches: ReferenceMatch[],
548
+ repoOrder: Map<string, number>
549
+ ): ReferenceMatch[] {
550
+ const position = (repo: string | undefined): number =>
551
+ repo === undefined ? 0 : (repoOrder.get(repo) ?? repoOrder.size);
552
+
553
+ const sorted = [...matches].sort((left, right) => {
554
+ if (left.qualification !== right.qualification) {
555
+ return left.qualification - right.qualification;
556
+ }
557
+ const scopes = scopeRank(left.scope) - scopeRank(right.scope);
558
+ if (scopes !== 0) return scopes;
559
+
560
+ const repos = position(left.repo) - position(right.repo);
561
+ if (repos !== 0) return repos;
562
+
563
+ return compare(
564
+ [left.namespace, left.recipe, left.variable],
565
+ [right.namespace, right.recipe, right.variable]
566
+ );
567
+ });
568
+
569
+ const seen = new Set<string>();
570
+ return sorted.filter((match) => {
571
+ const identity = `${match.scope}${match.key}${match.envName ?? ""}`;
572
+ if (seen.has(identity)) return false;
573
+ seen.add(identity);
574
+ return true;
575
+ });
576
+ }
577
+
578
+ /** Bytewise comparison of the parts of an identity, so listings are stable. */
579
+ function compare(
580
+ left: (string | undefined)[],
581
+ right: (string | undefined)[]
582
+ ): number {
583
+ for (let index = 0; index < left.length; index += 1) {
584
+ const one = left[index] ?? "";
585
+ const other = right[index] ?? "";
586
+ if (one !== other) return one < other ? -1 : 1;
587
+ }
588
+ return 0;
589
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * References: how every sous command turns a word on the command line into the
3
+ * one thing it names.
4
+ *
5
+ * Import from here rather than from the individual modules. `scopes.ts` says
6
+ * what a reference can name, `find.ts` says what one means, and `pick.ts`
7
+ * settles which meaning a run proceeds with.
8
+ */
9
+
10
+ export * from "./scopes.js";
11
+ export * from "./find.js";
12
+ export * from "./pick.js";