@sous-io/sous 0.1.1 → 0.2.1

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 +409 -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 +625 -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 +415 -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,95 @@
1
+ /**
2
+ * `sous recipe list`.
3
+ *
4
+ * Shows every recipe the repositories this project trusts publish, with the
5
+ * latest published version, the version this project pins, and whether it is
6
+ * subscribed. It reads only the indexes sous already has on disk, so it works
7
+ * offline; a repository whose index has never been fetched is named at the end
8
+ * rather than being silently left out.
9
+ */
10
+
11
+ import { BaseCommand } from "../../base-command.js";
12
+ import { subscriptionServiceFor } from "../../lib/repos/subscription-service.js";
13
+ import { catalogContextFor } from "../../lib/repos/catalog-inputs.js";
14
+ import { listRecipes } from "../../lib/repos/catalog.js";
15
+ import {
16
+ INDENT,
17
+ RECIPE_COLUMNS,
18
+ recipeRows,
19
+ } from "../../lib/repos/catalog-display.js";
20
+ import { renderTable } from "../../utils/table.js";
21
+ import {
22
+ blankLine,
23
+ footer,
24
+ heading,
25
+ indent,
26
+ log,
27
+ paragraph,
28
+ showCommandVars,
29
+ } from "../../utils/formatting.js";
30
+
31
+ export default class RecipeList extends BaseCommand {
32
+ static description = "List the recipes the repositories this project trusts publish";
33
+
34
+ /**
35
+ * The other spelling of the topic. It lives under a hidden topic, so it is
36
+ * typable everywhere without ever reaching the top-level listing.
37
+ */
38
+ static aliases = ["recipes:list"];
39
+
40
+ static examples = ["<%= config.bin %> recipe list"];
41
+
42
+ static flags = { ...BaseCommand.baseFlags };
43
+
44
+ async run(): Promise<void> {
45
+ await this.parse(RecipeList);
46
+
47
+ showCommandVars({
48
+ Project: this.projectLabel,
49
+ Config: this.configContext.configPath,
50
+ });
51
+
52
+ heading("Recipes in the repositories this project trusts");
53
+
54
+ const service = subscriptionServiceFor({
55
+ configContext: this.configContext,
56
+ settings: this.settings,
57
+ shellEnv: this.shellEnv,
58
+ });
59
+
60
+ const { inputs, notFetched } = catalogContextFor({
61
+ service,
62
+ sousDir: this.configContext.sousDir,
63
+ settings: this.settings,
64
+ });
65
+
66
+ const listings = listRecipes(inputs);
67
+
68
+ blankLine();
69
+
70
+ if (listings.length === 0) {
71
+ paragraph(
72
+ inputs.repos.length === 0
73
+ ? "Sous has read no repository index for this project, so there are no " +
74
+ "recipes to show."
75
+ : "The repositories this project trusts publish no recipes."
76
+ );
77
+ } else {
78
+ for (const line of renderTable(RECIPE_COLUMNS, recipeRows(listings), {
79
+ indent: INDENT,
80
+ })) {
81
+ log(indent(line, INDENT));
82
+ }
83
+ }
84
+
85
+ if (notFetched.length > 0) {
86
+ blankLine();
87
+ paragraph(
88
+ `These repositories are trusted and their index has not been fetched yet, so ` +
89
+ `nothing in them is listed: ${notFetched.join(", ")}.`
90
+ );
91
+ }
92
+
93
+ footer();
94
+ }
95
+ }
@@ -0,0 +1,301 @@
1
+ /**
2
+ * `sous recipe show <ref>`.
3
+ *
4
+ * Shows one recipe in full: where it is published, every version it publishes,
5
+ * what the version this project would use depends on, the questions it asks,
6
+ * and where its files land in this project. It reads only what sous already has
7
+ * on disk: the repository's cached index, the project's lockfile, and the
8
+ * recipe's own files when they are in the store or a linked working copy.
9
+ */
10
+
11
+ import { Args } from "@oclif/core";
12
+ import { BaseCommand } from "../../base-command.js";
13
+ import { resolveRootScope } from "../../lib/settings.js";
14
+ import { subscriptionServiceFor } from "../../lib/repos/subscription-service.js";
15
+ import { catalogContextFor } from "../../lib/repos/catalog-inputs.js";
16
+ import {
17
+ describeRecipe,
18
+ type RecipeContentListing,
19
+ type RecipeDependencyListing,
20
+ type RecipeVariableListing,
21
+ type RecipeVersionListing,
22
+ } from "../../lib/repos/catalog.js";
23
+ import {
24
+ INDENT,
25
+ describeVersionStatus,
26
+ factIf,
27
+ printFacts,
28
+ } from "../../lib/repos/catalog-display.js";
29
+ import { renderTable, type TableColumn } from "../../utils/table.js";
30
+ import {
31
+ blankLine,
32
+ footer,
33
+ heading,
34
+ indent,
35
+ log,
36
+ paragraph,
37
+ showCommandVars,
38
+ subheading,
39
+ } from "../../utils/formatting.js";
40
+
41
+ /** The columns the version history shows. */
42
+ const VERSION_COLUMNS: TableColumn[] = [
43
+ { key: "version", header: "Version", overflow: "truncate", minWidth: 7 },
44
+ { key: "status", header: "What it is", overflow: "wrap", flex: 1, minWidth: 16 },
45
+ { key: "prerelease", header: "Prerelease", priority: "medium" },
46
+ { key: "released", header: "Released", overflow: "truncate", priority: "low" },
47
+ ];
48
+
49
+ /** The columns the dependency listing shows. */
50
+ const DEPENDENCY_COLUMNS: TableColumn[] = [
51
+ { key: "key", header: "Dependency", kind: "path", overflow: "truncate", minWidth: 12 },
52
+ { key: "declared", header: "Declared as", overflow: "wrap", flex: 1, minWidth: 12 },
53
+ { key: "resolved", header: "Resolved to", overflow: "truncate", minWidth: 11 },
54
+ { key: "repo", header: "Repository", overflow: "truncate", priority: "low", minWidth: 12 },
55
+ { key: "kind", header: "Kind", overflow: "wrap", priority: "medium", minWidth: 16 },
56
+ ];
57
+
58
+ /** The columns the variable listing shows. */
59
+ const VARIABLE_COLUMNS: TableColumn[] = [
60
+ { key: "name", header: "Variable", overflow: "truncate", minWidth: 10 },
61
+ { key: "type", header: "Type", priority: "medium" },
62
+ { key: "env", header: "Environment variable", overflow: "truncate", minWidth: 12 },
63
+ { key: "required", header: "Required", priority: "medium" },
64
+ { key: "secret", header: "Secret", priority: "low" },
65
+ { key: "prompt", header: "What it asks", overflow: "wrap", flex: 1, priority: "low", minWidth: 16 },
66
+ ];
67
+
68
+ /** The columns the content listing shows. */
69
+ const CONTENT_COLUMNS: TableColumn[] = [
70
+ { key: "kind", header: "Content", minWidth: 7 },
71
+ { key: "include", header: "Files", overflow: "wrap", flex: 1, minWidth: 14 },
72
+ {
73
+ key: "destination",
74
+ header: "Where they land",
75
+ kind: "path",
76
+ overflow: "wrap",
77
+ flex: 2,
78
+ minWidth: 16,
79
+ },
80
+ ];
81
+
82
+ export default class RecipeShow extends BaseCommand {
83
+ static description = "Show one recipe: its versions, dependencies, variables and files";
84
+
85
+ /**
86
+ * The other spelling of the topic. It lives under a hidden topic, so it is
87
+ * typable everywhere without ever reaching the top-level listing.
88
+ */
89
+ static aliases = ["recipes:show"];
90
+
91
+ static examples = [
92
+ "<%= config.bin %> recipe show workflow/task-files",
93
+ "<%= config.bin %> recipe show task-files",
94
+ "<%= config.bin %> recipe show sous-recipes:core/about-sous",
95
+ ];
96
+
97
+ static args = {
98
+ ref: Args.string({
99
+ description:
100
+ "A recipe, written as 'namespace/recipe', a recipe name on its own, or either with a 'repository:' qualifier",
101
+ required: true,
102
+ }),
103
+ };
104
+
105
+ static flags = { ...BaseCommand.baseFlags };
106
+
107
+ async run(): Promise<void> {
108
+ const { args } = await this.parse(RecipeShow);
109
+
110
+ showCommandVars({
111
+ Project: this.projectLabel,
112
+ Config: this.configContext.configPath,
113
+ Recipe: args.ref,
114
+ });
115
+
116
+ const service = subscriptionServiceFor({
117
+ configContext: this.configContext,
118
+ settings: this.settings,
119
+ shellEnv: this.shellEnv,
120
+ });
121
+
122
+ const { inputs } = catalogContextFor({
123
+ service,
124
+ sousDir: this.configContext.sousDir,
125
+ settings: this.settings,
126
+ scope: resolveRootScope(this.settings, this.configContext),
127
+ });
128
+
129
+ const detail = describeRecipe(inputs, args.ref);
130
+
131
+ heading(detail.key);
132
+ blankLine();
133
+
134
+ printFacts([
135
+ { label: "Repository", lines: [detail.repo] },
136
+ ...factIf("Location", detail.repoUrl),
137
+ ...factIf("About", detail.description),
138
+ { label: "Folder", lines: [detail.path] },
139
+ { label: "Latest version", lines: [detail.latest ?? "none published"] },
140
+ { label: "Pinned version", lines: [detail.pinned ?? "this project pins none"] },
141
+ { label: "Subscribed", lines: [detail.subscribed ? "yes" : "no"] },
142
+ ...factIf("Described below", detail.describing),
143
+ ]);
144
+
145
+ this.printVersions(detail.versions);
146
+ this.printDependencies(detail.dependencies, detail.manifestRead);
147
+
148
+ if (!detail.manifestRead) {
149
+ blankLine();
150
+ paragraph(
151
+ "The recipe's own files are not on this machine, so the questions it asks and " +
152
+ "the files it publishes are not known here. Subscribing to it fetches them."
153
+ );
154
+ footer();
155
+ return;
156
+ }
157
+
158
+ this.printVariables(detail.variables);
159
+ this.printContents(detail.contents);
160
+
161
+ footer();
162
+ }
163
+
164
+ /**
165
+ * Every published version, newest first, with what each one is to this
166
+ * project.
167
+ *
168
+ * @param versions - The versions the catalog listed.
169
+ */
170
+ private printVersions(versions: RecipeVersionListing[]): void {
171
+ blankLine();
172
+ subheading("Published versions");
173
+ blankLine();
174
+
175
+ const rows = versions.map((entry) => ({
176
+ version: entry.version,
177
+ status: describeVersionStatus(entry.status),
178
+ prerelease: entry.prerelease ? "yes" : "no",
179
+ released: entry.releasedAt ?? "not recorded",
180
+ }));
181
+
182
+ for (const line of renderTable(VERSION_COLUMNS, rows, { indent: INDENT })) {
183
+ log(indent(line, INDENT));
184
+ }
185
+ }
186
+
187
+ /**
188
+ * What the described version depends on, from both sides: what the recipe's
189
+ * manifest declares, and what its repository's index resolved that to when it
190
+ * was released.
191
+ *
192
+ * @param dependencies - The dependencies the catalog listed.
193
+ * @param manifestRead - Whether the recipe's own manifest could be read.
194
+ */
195
+ private printDependencies(
196
+ dependencies: RecipeDependencyListing[],
197
+ manifestRead: boolean
198
+ ): void {
199
+ blankLine();
200
+ subheading("What it depends on");
201
+ blankLine();
202
+
203
+ if (dependencies.length === 0) {
204
+ paragraph(
205
+ manifestRead
206
+ ? "This recipe depends on nothing else."
207
+ : "The repository's index records no dependencies for this version."
208
+ );
209
+ return;
210
+ }
211
+
212
+ const rows = dependencies.map((entry) => ({
213
+ key: entry.key,
214
+ declared: entry.declared ?? "not declared in the manifest",
215
+ resolved:
216
+ entry.resolvedVersion ??
217
+ (entry.resolvedRange === undefined
218
+ ? "not recorded in the index"
219
+ : `the range ${entry.resolvedRange}`),
220
+ repo: entry.repo ?? "this repository",
221
+ kind: describeDependencyKind(entry.kind),
222
+ }));
223
+
224
+ for (const line of renderTable(DEPENDENCY_COLUMNS, rows, { indent: INDENT })) {
225
+ log(indent(line, INDENT));
226
+ }
227
+ }
228
+
229
+ /**
230
+ * The questions the recipe asks, and the environment variable each answer is
231
+ * stored under.
232
+ *
233
+ * @param variables - The variables the catalog listed.
234
+ */
235
+ private printVariables(variables: RecipeVariableListing[]): void {
236
+ blankLine();
237
+ subheading("What it asks you");
238
+ blankLine();
239
+
240
+ if (variables.length === 0) {
241
+ paragraph("This recipe asks no questions.");
242
+ return;
243
+ }
244
+
245
+ const rows = variables.map((entry) => ({
246
+ name: entry.name,
247
+ type: entry.type,
248
+ env: entry.env,
249
+ required: entry.required ? "yes" : "no",
250
+ secret: entry.secret ? "yes" : "no",
251
+ prompt: entry.prompt,
252
+ }));
253
+
254
+ for (const line of renderTable(VARIABLE_COLUMNS, rows, { indent: INDENT })) {
255
+ log(indent(line, INDENT));
256
+ }
257
+ }
258
+
259
+ /**
260
+ * What the recipe contributes, and where each kind's files are written in
261
+ * this project.
262
+ *
263
+ * @param contents - The content kinds the catalog listed.
264
+ */
265
+ private printContents(contents: RecipeContentListing[]): void {
266
+ blankLine();
267
+ subheading("Where its files land in this project");
268
+ blankLine();
269
+
270
+ if (contents.length === 0) {
271
+ paragraph("This recipe publishes no files.");
272
+ return;
273
+ }
274
+
275
+ const rows = contents.map((entry) => ({
276
+ kind: entry.kind,
277
+ include: entry.include.join(", "),
278
+ destination:
279
+ entry.destinations.length > 0
280
+ ? entry.destinations.join(", ")
281
+ : entry.kind === "config"
282
+ ? "loaded as a config layer, so nothing is written"
283
+ : `nowhere: this project sets no 'recipeOutputs.${entry.kind}' directory`,
284
+ }));
285
+
286
+ for (const line of renderTable(CONTENT_COLUMNS, rows, { indent: INDENT })) {
287
+ log(indent(line, INDENT));
288
+ }
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Plain-language wording for how a dependency was declared.
294
+ *
295
+ * @param kind - What the manifest declared it as, when the manifest was read.
296
+ */
297
+ function describeDependencyKind(kind: "depends" | "subscribes" | undefined): string {
298
+ if (kind === "depends") return "build dependency";
299
+ if (kind === "subscribes") return "co-subscription";
300
+ return "unknown";
301
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * `sous repo add <url>`.
3
+ *
4
+ * Adding a repository is the trust ceremony, and the two are deliberately the
5
+ * same act: a repository sous will read from is one written into this project's
6
+ * config, and removing that entry withdraws the trust. Nothing is downloaded
7
+ * before the question is answered, not even the repository's index.
8
+ *
9
+ * Once the answer is yes, exactly one file is fetched: `sous.index.json`. That
10
+ * is all sous needs in order to resolve a ref, list versions and decide what to
11
+ * download later, so adding a repository costs one small request and installs
12
+ * nothing.
13
+ */
14
+
15
+ import { Args, Flags } from "@oclif/core";
16
+ import { BaseCommand } from "../../base-command.js";
17
+ import { subscriptionServiceFor } from "../../lib/repos/subscription-service.js";
18
+ import type { ProviderId } from "../../lib/repos/providers/provider.js";
19
+ import {
20
+ blankLine,
21
+ dryRunNotice,
22
+ footer,
23
+ heading,
24
+ note,
25
+ showCommandVars,
26
+ showVariables,
27
+ } from "../../utils/formatting.js";
28
+ import { confirmationFlag } from "../../utils/flags.js";
29
+
30
+ export default class RepoAdd extends BaseCommand {
31
+ static description =
32
+ "Add a recipe repository to this project, which is also how you trust it";
33
+
34
+ /**
35
+ * The other spelling of the topic. It lives under a hidden topic, so it is
36
+ * typable everywhere without ever reaching the top-level listing.
37
+ */
38
+ static aliases = ["repos:add"];
39
+
40
+ static examples = [
41
+ "<%= config.bin %> repo add https://github.com/sous-io/sous-recipes",
42
+ "<%= config.bin %> repo add https://github.com/sous-io/sous-recipes --name recipes",
43
+ "<%= config.bin %> repo add /home/me/Projects/my-recipes --trust",
44
+ "<%= config.bin %> repo add ../my-recipes --trust",
45
+ ];
46
+
47
+ static args = {
48
+ url: Args.string({
49
+ description:
50
+ "Where the repository lives: its URL, or the path of one on this machine",
51
+ required: true,
52
+ }),
53
+ };
54
+
55
+ static flags = {
56
+ ...BaseCommand.baseFlags,
57
+ name: Flags.string({
58
+ description:
59
+ "The short name refs will use for it. Defaults to the last segment of the URL",
60
+ }),
61
+ provider: Flags.string({
62
+ description:
63
+ "The provider that handles it, for a host the URL does not give away",
64
+ options: ["github", "gitlab", "local"],
65
+ }),
66
+ // The only question this command asks is the trust question, so the shared
67
+ // confirmation flag answers it; `--trust` stays a spelling of it, because
68
+ // that is the word the ceremony is named after.
69
+ yes: confirmationFlag({ extraAliases: ["trust"] }),
70
+ "dry-run": Flags.boolean({
71
+ description: "Print what would change without trusting or fetching anything",
72
+ default: false,
73
+ }),
74
+ };
75
+
76
+ async run(): Promise<void> {
77
+ const { args, flags } = await this.parse(RepoAdd);
78
+ const dryRun = flags["dry-run"];
79
+
80
+ showCommandVars({
81
+ Project: this.projectLabel,
82
+ Config: this.configContext.configPath,
83
+ Repository: args.url,
84
+ "Dry Run": dryRun,
85
+ });
86
+
87
+ // The heading is not followed by a blank line here: the block that comes
88
+ // next opens with one of its own, and the trust ceremony in the middle may
89
+ // print before either of them.
90
+ heading("Adding a repository");
91
+
92
+ if (dryRun) {
93
+ dryRunNotice("Nothing will be trusted, written or downloaded.");
94
+ blankLine();
95
+ }
96
+
97
+ const service = subscriptionServiceFor({
98
+ configContext: this.configContext,
99
+ settings: this.settings,
100
+ shellEnv: this.shellEnv,
101
+ });
102
+
103
+ const outcome = await service.addRepo({
104
+ url: args.url,
105
+ ...(flags.name === undefined ? {} : { name: flags.name }),
106
+ ...(flags.provider === undefined
107
+ ? {}
108
+ : { provider: flags.provider as ProviderId }),
109
+ trust: flags.yes,
110
+ dryRun,
111
+ });
112
+
113
+ blankLine();
114
+
115
+ if (outcome.dryRun) {
116
+ note(
117
+ `The repository would be added as '${outcome.name}', read through the ` +
118
+ `${outcome.provider} provider, and its index would be fetched.`
119
+ );
120
+ footer();
121
+ return;
122
+ }
123
+
124
+ showVariables({
125
+ Repository: outcome.name,
126
+ Location: outcome.url,
127
+ Provider: outcome.provider,
128
+ Namespaces:
129
+ outcome.namespaces.length > 0
130
+ ? outcome.namespaces.join(", ")
131
+ : "none; this repository publishes nothing yet",
132
+ Recipes: String(outcome.recipeCount),
133
+ });
134
+
135
+ blankLine();
136
+ note(
137
+ outcome.alreadyTrusted
138
+ ? `This project already trusted '${outcome.name}', so only its index was ` +
139
+ `refreshed.`
140
+ : `This project now trusts '${outcome.name}'. Nothing from it has been installed.`
141
+ );
142
+
143
+ footer();
144
+ }
145
+ }