@sous-io/sous 0.1.1 → 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 (205) hide show
  1. package/README.md +115 -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 +72 -8
  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/shared-prompts/_partials/resume-task.md +0 -51
  166. package/shared-prompts/_partials/sub-agent-delegation.md +0 -32
  167. package/shared-prompts/_partials/update-task-file.md +0 -52
  168. package/shared-prompts/memories/automated-browser-tasks/INDEX.tpl.md +0 -52
  169. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/SKILL.tpl.md +0 -102
  170. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/auth-failure-handling.mjs +0 -81
  171. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/chained-workflow.mjs +0 -126
  172. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/simple-fetch.mjs +0 -92
  173. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/architecture.md +0 -61
  174. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/auth-and-sessions.md +0 -65
  175. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/ctx-api.md +0 -96
  176. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/installation.md +0 -104
  177. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/script-conventions.md +0 -243
  178. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/chrome-state.mjs +0 -148
  179. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/debug.mjs +0 -383
  180. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/debug.spec.mjs +0 -267
  181. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/eslint.config.mjs +0 -56
  182. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/harness.mjs +0 -169
  183. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/keyring.mjs +0 -59
  184. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/logger.mjs +0 -25
  185. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/params.mjs +0 -140
  186. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/run.mjs +0 -140
  187. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/settings.tpl.mjs +0 -1
  188. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/utils.mjs +0 -185
  189. package/shared-prompts/skills/automated-browser-tasks/create-automated-browser-task/SKILL.tpl.md +0 -52
  190. package/shared-prompts/skills/automated-browser-tasks/running-automated-browser-tasks/SKILL.tpl.md +0 -59
  191. package/shared-prompts/skills/automated-browser-tasks/update-automated-browser-task/SKILL.tpl.md +0 -47
  192. package/shared-prompts/skills/control-flow/approve/SKILL.tpl.md +0 -26
  193. package/shared-prompts/skills/control-flow/opine/SKILL.tpl.md +0 -58
  194. package/shared-prompts/skills/control-flow/repeat/SKILL.tpl.md +0 -27
  195. package/shared-prompts/skills/control-flow/research/SKILL.tpl.md +0 -34
  196. package/shared-prompts/skills/sous-skills/about-sous/SKILL.tpl.md +0 -51
  197. package/shared-prompts/skills/task-files/about-task-files/SKILL.tpl.md +0 -122
  198. package/shared-prompts/skills/task-files/continue-task-in-new-branch/SKILL.tpl.md +0 -80
  199. package/shared-prompts/skills/task-files/go/SKILL.tpl.md +0 -14
  200. package/shared-prompts/skills/task-files/resume-task/SKILL.tpl.md +0 -13
  201. package/shared-prompts/skills/task-files/start-task/SKILL.tpl.md +0 -93
  202. package/shared-prompts/skills/task-files/update/SKILL.tpl.md +0 -14
  203. package/shared-prompts/skills/task-files/update-task-file/SKILL.tpl.md +0 -13
  204. /package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/substitutions.md +0 -0
  205. /package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-liquid-templates/references/liquid-filters.md +0 -0
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Settling on one of the things a reference could have meant.
3
+ *
4
+ * Finding is separate from choosing on purpose: `findReference` is pure and
5
+ * tells the caller everything a word could have meant, and this is the one
6
+ * place that decides which of them the run proceeds with. Every command that
7
+ * takes a reference goes through it, so a word that means two things behaves
8
+ * the same everywhere:
9
+ *
10
+ * - one match proceeds, and what it resolved to is written as the facts about
11
+ * it followed by one sentence (`formatResolvedReference`), so the reader
12
+ * sees what the word they typed actually meant;
13
+ * - several matches are offered as a list to choose from;
14
+ * - `--accept-first` takes the first one in the documented listing order;
15
+ * - a run with no terminal fails, naming the question it could not ask and
16
+ * the flag that would have answered it (`src/lib/interactive.ts` holds that
17
+ * rule, and every prompt in sous is gated by it).
18
+ */
19
+
20
+ import { ConfigError } from "../errors.js";
21
+ import { nonInteractiveError } from "../interactive.js";
22
+ import { formatParagraph, log } from "../../utils/formatting.js";
23
+ import { askChoice } from "../../utils/prompts.js";
24
+ import {
25
+ formatResolvedReference,
26
+ resolvedReferenceFacts,
27
+ } from "../repos/reference-report.js";
28
+ import { describeReference, type ReferenceMatch } from "./find.js";
29
+
30
+ /** The flag that answers "which one did you mean?" ahead of time. */
31
+ export const ACCEPT_FIRST_FLAG = "--accept-first";
32
+
33
+ /** How `pickReference` should behave. */
34
+ export type PickReferenceOptions = {
35
+ /** The reference exactly as it was written, for every message. */
36
+ search: string;
37
+ /** Whether a question may be asked. A run that may not fails instead. */
38
+ interactive: boolean;
39
+ /** Take the first match rather than asking, because `--accept-first` was passed. */
40
+ acceptFirst?: boolean;
41
+ /** The question to ask when there is more than one match. */
42
+ prompt?: string;
43
+ /**
44
+ * Write the facts about what the reference resolved to. On by default; a
45
+ * caller that reports the resolution itself turns it off, and is still told
46
+ * when `--accept-first` settled an ambiguous word.
47
+ */
48
+ announce?: boolean;
49
+ /** Extra lines for the error a reference that matched nothing raises. */
50
+ details?: string[];
51
+ /** Where a resolution is reported. Defaults to the console. */
52
+ write?: (message: string) => void;
53
+ /** How the choice is asked. Defaults to the shared choice prompt. */
54
+ choose?: (message: string, matches: ReferenceMatch[]) => Promise<ReferenceMatch>;
55
+ };
56
+
57
+ /**
58
+ * The one match a reference proceeds with.
59
+ *
60
+ * @param matches - What the reference could have meant, in listing order.
61
+ * @param options - What was searched for, and whether a question may be asked.
62
+ * @returns The match the run proceeds with.
63
+ */
64
+ export async function pickReference(
65
+ matches: ReferenceMatch[],
66
+ options: PickReferenceOptions
67
+ ): Promise<ReferenceMatch> {
68
+ const write = options.write ?? ((message: string) => log(message));
69
+
70
+ if (matches.length === 0) {
71
+ throw new ConfigError(
72
+ [`Nothing called '${options.search}' was found.`, ...(options.details ?? [])].join("\n")
73
+ );
74
+ }
75
+
76
+ const first = matches[0]!;
77
+
78
+ if (matches.length === 1) {
79
+ if (options.announce !== false) announce(write, first, options.search);
80
+ return first;
81
+ }
82
+
83
+ if (options.acceptFirst === true) {
84
+ const reason =
85
+ `'${options.search}' named ${matches.length} things, and ` +
86
+ `'${ACCEPT_FIRST_FLAG}' was passed, so the first one listed is being used.`;
87
+
88
+ // A caller that reports the resolution itself still has to be told that the
89
+ // word was ambiguous, so the sentence is written even when the facts are not.
90
+ if (options.announce === false) {
91
+ for (const line of formatParagraph(reason)) write(line);
92
+ } else {
93
+ announce(write, first, options.search, reason);
94
+ }
95
+ return first;
96
+ }
97
+
98
+ if (!options.interactive) {
99
+ // The example is a spelling that says more than what was typed, so a
100
+ // reference that is already its own fully qualified name is not offered
101
+ // back unchanged.
102
+ const example =
103
+ matches.find((match) => match.key !== options.search)?.key ?? first.key;
104
+
105
+ throw nonInteractiveError({
106
+ prompt: `which '${options.search}' you meant`,
107
+ remedy:
108
+ `write the full reference (for example '${example}'), or pass ` +
109
+ `'${ACCEPT_FIRST_FLAG}' to take the first candidate listed above.`,
110
+ details: [
111
+ `'${options.search}' matched ${matches.length} things:`,
112
+ ...matches.map((match) => ` ${describeReference(match)}`),
113
+ ],
114
+ });
115
+ }
116
+
117
+ const choose =
118
+ options.choose ??
119
+ ((message: string, offered: ReferenceMatch[]) =>
120
+ askChoice(
121
+ message,
122
+ offered.map((match) => ({ name: describeReference(match), value: match }))
123
+ ));
124
+
125
+ return choose(options.prompt ?? `Which '${options.search}' did you mean?`, matches);
126
+ }
127
+
128
+ /**
129
+ * Writes what a reference resolved to, as the key and value list every other
130
+ * set of facts in the CLI is written as, followed by one sentence saying why
131
+ * that candidate won.
132
+ *
133
+ * @param write - Where the report goes.
134
+ * @param match - The match the run proceeds with.
135
+ * @param search - The reference exactly as it was written.
136
+ * @param reason - The closing sentence, when it was not simply the only match.
137
+ */
138
+ function announce(
139
+ write: (message: string) => void,
140
+ match: ReferenceMatch,
141
+ search: string,
142
+ reason?: string
143
+ ): void {
144
+ for (const line of formatResolvedReference(resolvedReferenceFacts(match, search, reason))) {
145
+ write(line);
146
+ }
147
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The kinds of thing a reference on the command line can name.
3
+ *
4
+ * Every sous command that takes a `<ref>` or a `[NAME]` argument is naming one
5
+ * of these five things, and each command accepts only the kinds that make sense
6
+ * for it: `sous subscribe` accepts a namespace or a recipe, `sous vars ask`
7
+ * accepts all five. The scope list a command passes to `findReference` is
8
+ * therefore part of that command's contract, and it is the only thing that
9
+ * differs between one command's resolution and another's.
10
+ */
11
+
12
+ /** One kind of thing a reference can name. */
13
+ export enum SousScope {
14
+ /** A repository this project trusts, named by its short name. */
15
+ Repository = "repository",
16
+ /** A namespace published by a repository. */
17
+ Namespace = "namespace",
18
+ /** A recipe published in a namespace. */
19
+ Recipe = "recipe",
20
+ /** A variable declared by a recipe, named as the recipe's author named it. */
21
+ VariableName = "variableName",
22
+ /** An environment variable name that answers a variable. */
23
+ EnvVarName = "envVarName",
24
+ }
25
+
26
+ /**
27
+ * Every scope, in the order matches of equal specificity are listed in. It is
28
+ * the order the one-word ref search has always used (a whole namespace before
29
+ * the recipes inside it), widened to the other three kinds: the broadest thing
30
+ * a word could have meant is offered first, and the environment variable names
31
+ * (the least likely reading of a plain word) come last.
32
+ */
33
+ export const SCOPE_ORDER: readonly SousScope[] = [
34
+ SousScope.Repository,
35
+ SousScope.Namespace,
36
+ SousScope.Recipe,
37
+ SousScope.VariableName,
38
+ SousScope.EnvVarName,
39
+ ] as const;
40
+
41
+ /** Every scope, for a command that accepts anything a reference can name. */
42
+ export const ALL_SCOPES: readonly SousScope[] = SCOPE_ORDER;
43
+
44
+ /** What each scope is called in a sentence. */
45
+ export const SCOPE_LABELS: Record<SousScope, string> = {
46
+ [SousScope.Repository]: "repository",
47
+ [SousScope.Namespace]: "namespace",
48
+ [SousScope.Recipe]: "recipe",
49
+ [SousScope.VariableName]: "variable",
50
+ [SousScope.EnvVarName]: "environment variable",
51
+ };
52
+
53
+ /**
54
+ * Where a scope sits in the listing order.
55
+ *
56
+ * @param scope - The scope to rank.
57
+ */
58
+ export function scopeRank(scope: SousScope): number {
59
+ const rank = SCOPE_ORDER.indexOf(scope);
60
+ return rank === -1 ? SCOPE_ORDER.length : rank;
61
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Shared display helpers for the browsing commands.
3
+ *
4
+ * `sous namespace show` and `sous recipe list` both print a table of recipes,
5
+ * and every one of the four commands turns the catalog's words into the ones a
6
+ * person reads. One module holds them so the four never drift apart in wording.
7
+ *
8
+ * The labeled facts block is the one `sous vars show` prints; it is rendered by
9
+ * `renderFacts`, which is already generic over a label and its lines.
10
+ */
11
+
12
+ import { renderFacts, type LabeledFact } from "../vars/display.js";
13
+ import { log, wrapColumns } from "../../utils/formatting.js";
14
+ import type { TableColumn } from "../../utils/table.js";
15
+ import type {
16
+ NamespaceCoverage,
17
+ RecipeListing,
18
+ VersionStatus,
19
+ } from "./catalog.js";
20
+
21
+ /** How far every line of a browsing command's output is indented. */
22
+ export const INDENT = 2;
23
+
24
+ /**
25
+ * The columns a recipe listing shows. The recipe and its versions are why
26
+ * anybody ran the command, so they stay however narrow the terminal is; the
27
+ * description takes the room that is left and wraps rather than being cut.
28
+ */
29
+ export const RECIPE_COLUMNS: TableColumn[] = [
30
+ { key: "key", header: "Recipe", kind: "path", overflow: "truncate", minWidth: 12 },
31
+ { key: "repo", header: "Repository", overflow: "truncate", priority: "medium" },
32
+ { key: "latest", header: "Latest", overflow: "truncate", minWidth: 6 },
33
+ { key: "pinned", header: "Pinned", overflow: "truncate", minWidth: 6 },
34
+ { key: "subscribed", header: "Subscribed", priority: "medium" },
35
+ {
36
+ key: "description",
37
+ header: "What it is",
38
+ overflow: "wrap",
39
+ flex: 1,
40
+ priority: "low",
41
+ minWidth: 16,
42
+ },
43
+ ];
44
+
45
+ /** One rendered recipe row, in the shape `RECIPE_COLUMNS` reads. */
46
+ export type RecipeRow = {
47
+ key: string;
48
+ repo: string;
49
+ latest: string;
50
+ pinned: string;
51
+ subscribed: string;
52
+ description: string;
53
+ };
54
+
55
+ /**
56
+ * Turns recipe listings into table rows. A recipe the project does not pin
57
+ * leaves the pinned column blank, because there is nothing to report there
58
+ * rather than something worth a word.
59
+ *
60
+ * @param listings - What the catalog produced.
61
+ */
62
+ export function recipeRows(listings: RecipeListing[]): RecipeRow[] {
63
+ return listings.map((entry) => ({
64
+ key: entry.key,
65
+ repo: entry.repo,
66
+ latest: entry.latest ?? "none published",
67
+ pinned: entry.pinned ?? "",
68
+ subscribed: entry.subscribed ? "yes" : "no",
69
+ description: entry.description ?? "no description published",
70
+ }));
71
+ }
72
+
73
+ /**
74
+ * Plain-language wording for how much of a namespace a project subscribes to.
75
+ *
76
+ * @param coverage - What the catalog worked out.
77
+ */
78
+ export function describeCoverage(coverage: NamespaceCoverage): string {
79
+ if (coverage === "whole namespace") return "the whole namespace";
80
+ if (coverage === "some recipes") return "some recipes";
81
+ return "no";
82
+ }
83
+
84
+ /**
85
+ * Plain-language wording for what one published version is to this project.
86
+ *
87
+ * @param status - What the catalog worked out.
88
+ */
89
+ export function describeVersionStatus(status: VersionStatus): string {
90
+ if (status === "latest and pinned") return "the latest version, and the one pinned here";
91
+ if (status === "latest") return "the latest version";
92
+ if (status === "pinned") return "the version pinned here";
93
+ return "an earlier version";
94
+ }
95
+
96
+ /**
97
+ * Prints a labeled facts block, wrapped to the terminal. The renderer indents
98
+ * the block itself, so every facts block in the CLI sits at the same depth
99
+ * whichever command printed it.
100
+ *
101
+ * @param facts - The facts to print.
102
+ */
103
+ export function printFacts(facts: LabeledFact[]): void {
104
+ for (const line of renderFacts(facts, wrapColumns())) log(line);
105
+ }
106
+
107
+ /**
108
+ * One labeled fact, when there is anything to say. Used to keep an absent field
109
+ * out of a facts block rather than printing an empty line for it.
110
+ *
111
+ * @param label - The label to show.
112
+ * @param value - The text beside it, or undefined to leave the fact out.
113
+ */
114
+ export function factIf(label: string, value: string | undefined): LabeledFact[] {
115
+ return value === undefined || value.length === 0 ? [] : [{ label, lines: [value] }];
116
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Wiring the catalog to a running command.
3
+ *
4
+ * `catalog.ts` is pure: it reads indexes, a lockfile and a list of subscription
5
+ * keys. This module is where those come from in a real project, and it is
6
+ * deliberately the only place that knows: the subscription service for the
7
+ * trusted repositories and their cached indexes, the lockfile service for what
8
+ * is pinned, the links map and the store for a recipe's own files, and
9
+ * `recipeOutputs` for where a content kind lands.
10
+ *
11
+ * Nothing here downloads anything. A repository whose index has never been
12
+ * fetched is left out of the catalog and named separately, so a browsing command
13
+ * is safe offline.
14
+ */
15
+
16
+ import type { Settings, VarScope } from "../settings.js";
17
+ import type { SubscriptionService } from "./subscription-service.js";
18
+ import type { CatalogInputs, CatalogRepo } from "./catalog.js";
19
+ import { linkedPathFor } from "./links.js";
20
+ import { mapLinkedRecipes, readRecipeManifestIn } from "./locked-recipes.js";
21
+ import { WRITABLE_CONTENT_KINDS, destinationsFor } from "./recipe-targets.js";
22
+ import type { WritableContentKind } from "./recipe-targets.js";
23
+
24
+ /** What building the catalog's inputs needs. */
25
+ export type CatalogInputsOptions = {
26
+ /** The subscription service for this project. */
27
+ service: SubscriptionService;
28
+ /** The project's `.sous/` directory. */
29
+ sousDir: string;
30
+ /** The merged project config. */
31
+ settings: Settings;
32
+ /**
33
+ * The resolved settings scope, when the caller has one. Only the destinations
34
+ * a recipe's files land in need it, so a command that does not show them may
35
+ * leave it out.
36
+ */
37
+ scope?: VarScope;
38
+ /** The environment to read; decides where the store and the links map are. */
39
+ env?: NodeJS.ProcessEnv;
40
+ };
41
+
42
+ /** The catalog's inputs, plus what could not be read. */
43
+ export type CatalogContext = {
44
+ /** What the catalog functions read. */
45
+ inputs: CatalogInputs;
46
+ /**
47
+ * Trusted repositories whose index has never been fetched, so nothing in them
48
+ * could be listed. Sorted.
49
+ */
50
+ notFetched: string[];
51
+ };
52
+
53
+ /**
54
+ * Builds the catalog's inputs for one project: every trusted repository whose
55
+ * index sous already has, the lockfile, and the subscription keys the project
56
+ * declares.
57
+ *
58
+ * @param options - The subscription service, the project's directory and config.
59
+ */
60
+ export function catalogContextFor(options: CatalogInputsOptions): CatalogContext {
61
+ const { service } = options;
62
+ const env = options.env ?? process.env;
63
+
64
+ const repos: CatalogRepo[] = [];
65
+ const notFetched: string[] = [];
66
+
67
+ const trusted = service.currentRepos();
68
+ for (const name of Object.keys(trusted).sort()) {
69
+ const index = service.cachedIndex(name);
70
+ if (index === undefined) {
71
+ notFetched.push(name);
72
+ continue;
73
+ }
74
+ const url = trusted[name]?.url;
75
+ repos.push({ name, ...(url === undefined ? {} : { url }), index });
76
+ }
77
+
78
+ const inputs: CatalogInputs = {
79
+ repos,
80
+ lock: service.lockService.read(),
81
+ subscriptions: Object.keys(service.allSubscriptions()).sort(),
82
+ readManifest: (recipe) => {
83
+ const directory = recipeFilesDirectory({
84
+ service,
85
+ sousDir: options.sousDir,
86
+ env,
87
+ repo: recipe.repo,
88
+ key: recipe.key,
89
+ namespace: recipe.namespace,
90
+ name: recipe.name,
91
+ version: recipe.version,
92
+ });
93
+ return directory === undefined ? undefined : readRecipeManifestIn(directory);
94
+ },
95
+ destinationsFor: (kind) => {
96
+ // Config layers are loaded, not written into the project, so they land
97
+ // nowhere a listing could name.
98
+ if (!isWritableKind(kind)) return [];
99
+ return destinationsFor(kind, {
100
+ sousDir: options.sousDir,
101
+ settings: options.settings,
102
+ ...(options.scope === undefined ? {} : { scope: options.scope }),
103
+ env,
104
+ });
105
+ },
106
+ };
107
+
108
+ return { inputs, notFetched };
109
+ }
110
+
111
+ /** Which recipe, at which version, in which of this project's repositories. */
112
+ export type RecipeFilesQuery = {
113
+ /** The subscription service, which knows the store and the repository identities. */
114
+ service: SubscriptionService;
115
+ /** The project's `.sous/` directory, which holds the links map. */
116
+ sousDir: string;
117
+ /** The environment to read; decides where the store is. Defaults to `process.env`. */
118
+ env?: NodeJS.ProcessEnv;
119
+ /** The short name of the repository publishing it. */
120
+ repo: string;
121
+ /** The recipe key, `namespace/recipe`. */
122
+ key: string;
123
+ namespace: string;
124
+ name: string;
125
+ /** The exact version wanted. */
126
+ version: string;
127
+ };
128
+
129
+ /**
130
+ * The directory one published recipe's files are read from: a linked working
131
+ * copy when the repository is linked, and the store entry for that exact
132
+ * version otherwise. Undefined when neither is on this machine.
133
+ *
134
+ * Nothing is fetched, and the directory is not checked for existence: the
135
+ * caller reads what is there, and an absent manifest is an ordinary answer.
136
+ *
137
+ * @param input - The recipe's identity, and where this project keeps its state.
138
+ */
139
+ export function recipeFilesDirectory(input: RecipeFilesQuery): string | undefined {
140
+ const checkout = linkedPathFor(input.repo, input.sousDir, input.env ?? process.env);
141
+ if (checkout !== undefined) {
142
+ const linked = mapLinkedRecipes(checkout)[input.key];
143
+ if (linked !== undefined) return linked;
144
+ }
145
+
146
+ const identity = input.service.identityForRepo(input.repo);
147
+ if (identity === undefined) return undefined;
148
+
149
+ return input.service.store.entryDir({
150
+ identity,
151
+ namespace: input.namespace,
152
+ name: input.name,
153
+ version: input.version,
154
+ });
155
+ }
156
+
157
+ /** True when a content kind's files are written into the project. */
158
+ function isWritableKind(kind: string): kind is WritableContentKind {
159
+ return (WRITABLE_CONTENT_KINDS as readonly string[]).includes(kind);
160
+ }