@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,193 @@
1
+ /**
2
+ * `sous repo remove <name>`: stop trusting a repository.
3
+ *
4
+ * The exact reverse of adding one. Adding a repository IS trusting it, so
5
+ * removing the entry is how that trust is withdrawn, and everything the project
6
+ * held through it goes with it: every subscription that resolves into it, every
7
+ * recipe those subscriptions alone held, and the files those recipes compiled.
8
+ *
9
+ * Informed consent, never prevention: the command prints all of that before it
10
+ * writes anything, asks once, and then does exactly what it described. The
11
+ * checkout behind a link is never deleted, because it is somebody's working
12
+ * copy and sous did not necessarily put it there.
13
+ */
14
+
15
+ import { Args, Flags } from "@oclif/core";
16
+ import { BaseCommand } from "../../base-command.js";
17
+ import { buildProjectOutputs } from "../../lib/build-service.js";
18
+ import { ConfigError } from "../../lib/errors.js";
19
+ import { resolveRootScope } from "../../lib/settings.js";
20
+ import { subscriptionServiceFor } from "../../lib/repos/subscription-service.js";
21
+ import { confirmationFlag } from "../../utils/flags.js";
22
+ import { renderTable, type TableColumn } from "../../utils/table.js";
23
+ import {
24
+ blankLine,
25
+ dryRunNotice,
26
+ footer,
27
+ heading,
28
+ indent,
29
+ log,
30
+ showCommandVars,
31
+ subheading,
32
+ } from "../../utils/formatting.js";
33
+
34
+ /** How far every line of this command's output is indented. */
35
+ const INDENT = 2;
36
+
37
+ /** The columns the report of what stayed behind shows. */
38
+ const STAYED_COLUMNS: TableColumn[] = [
39
+ { key: "key", header: "Recipe", kind: "path", overflow: "truncate", minWidth: 12 },
40
+ { key: "heldBy", header: "Still held by", overflow: "wrap", flex: 1, minWidth: 16 },
41
+ ];
42
+
43
+ export default class RepoRemove extends BaseCommand {
44
+ static description = "Stop trusting a repository, and remove everything it brought in";
45
+
46
+ /**
47
+ * The other spelling of the topic. It lives under a hidden topic, so it is
48
+ * typable everywhere without ever reaching the top-level listing.
49
+ */
50
+ static aliases = ["repos:remove"];
51
+
52
+ static examples = [
53
+ "<%= config.bin %> repo remove my-recipes",
54
+ "<%= config.bin %> repo remove my-recipes --dry-run",
55
+ "<%= config.bin %> repo remove my-recipes --yes",
56
+ ];
57
+
58
+ static args = {
59
+ repo: Args.string({
60
+ description: "The repository's short name, as this project records it",
61
+ required: true,
62
+ }),
63
+ };
64
+
65
+ static flags = {
66
+ ...BaseCommand.baseFlags,
67
+ // The only question this command asks is whether to go ahead with what it
68
+ // has just described, so the shared confirmation flag answers it.
69
+ yes: confirmationFlag(),
70
+ "dry-run": Flags.boolean({
71
+ description: "Print what would be removed without writing anything",
72
+ default: false,
73
+ }),
74
+ "no-build": Flags.boolean({
75
+ description: "Remove the repository without rebuilding the project",
76
+ default: false,
77
+ }),
78
+ };
79
+
80
+ async run(): Promise<void> {
81
+ const { args, flags } = await this.parse(RepoRemove);
82
+ const dryRun = flags["dry-run"];
83
+
84
+ showCommandVars({
85
+ Project: this.projectLabel,
86
+ Config: this.configContext.configPath,
87
+ Repository: args.repo,
88
+ "Dry Run": dryRun,
89
+ });
90
+
91
+ // The heading is not followed by a blank line here: the plan that comes
92
+ // next opens with one of its own.
93
+ heading("Removing a repository");
94
+
95
+ if (dryRun) dryRunNotice("Nothing will be written.");
96
+
97
+ const service = subscriptionServiceFor({
98
+ configContext: this.configContext,
99
+ settings: this.settings,
100
+ shellEnv: this.shellEnv,
101
+ });
102
+
103
+ const outcome = await service.removeRepo({
104
+ name: args.repo,
105
+ yes: flags.yes,
106
+ dryRun,
107
+ // The output files a removed recipe wrote can only be named once the
108
+ // `recipeOutputs` destinations have had their variables substituted.
109
+ scope: resolveRootScope(this.settings, this.configContext),
110
+ });
111
+
112
+ blankLine();
113
+ subheading("Lockfile");
114
+ blankLine();
115
+ if (outcome.diff.unchanged) {
116
+ log(indent("Nothing changed; nothing was locked because of this repository."));
117
+ } else {
118
+ for (const line of outcome.diff.lines) log(indent(line));
119
+ }
120
+
121
+ if (outcome.stayed.length > 0) {
122
+ blankLine();
123
+ subheading("What stayed, and why");
124
+ blankLine();
125
+ const rows = outcome.stayed.map((entry) => ({
126
+ key: entry.key,
127
+ heldBy: entry.heldBy.join(", "),
128
+ }));
129
+
130
+ for (const line of renderTable(STAYED_COLUMNS, rows, { indent: INDENT })) {
131
+ log(indent(line, INDENT));
132
+ }
133
+ }
134
+
135
+ const rebuilding = !dryRun && !flags["no-build"];
136
+
137
+ // The closing sentence names the build only when this run is not about to
138
+ // do it, so nobody is told to run a command that is already running.
139
+ const pruneHint = rebuilding
140
+ ? ``
141
+ : ` Run 'sous build' to prune what it used to write.`;
142
+
143
+ blankLine();
144
+ log(
145
+ indent(
146
+ dryRun
147
+ ? "Nothing was written. Run the same command without '--dry-run' to remove it."
148
+ : outcome.optedOut
149
+ ? `The repository '${outcome.name}' is one sous provides itself, so it was ` +
150
+ `switched off rather than deleted: this project's config now records ` +
151
+ `'${outcome.name}: { enabled: false }'.${pruneHint}`
152
+ : `This project no longer trusts '${outcome.name}'. Nothing is read from it ` +
153
+ `any more, and its entry is gone from the repositories layer.${pruneHint}`
154
+ )
155
+ );
156
+
157
+ footer();
158
+
159
+ if (rebuilding) await this.rebuildProject(outcome.name);
160
+ }
161
+
162
+ /**
163
+ * Rebuilds the project now that the repository has been removed, so the files
164
+ * its recipes used to contribute are pruned before this command returns.
165
+ *
166
+ * The repositories and subscriptions both live in managed `conf.d/` layers, so
167
+ * the settings loaded when this command started no longer describe the
168
+ * project; they are reloaded before the build, or it would compile the old
169
+ * recipe set straight back onto disk. A build that fails leaves the removal in
170
+ * place, because it is already written and locked; the message says so and
171
+ * names the command to run once the cause is fixed.
172
+ *
173
+ * @param name - The repository that was just removed, for the failure message.
174
+ */
175
+ private async rebuildProject(name: string): Promise<void> {
176
+ await this.reloadDiscoveredConfig();
177
+
178
+ heading("Building the project");
179
+
180
+ const succeeded = await buildProjectOutputs(this.settings, this.configContext);
181
+
182
+ footer();
183
+
184
+ if (!succeeded) {
185
+ throw new ConfigError(
186
+ `The repository '${name}' was removed, but the build that followed it failed, ` +
187
+ `so this project may still hold files its recipes used to write. The removal ` +
188
+ `itself is recorded and locked; fix what the build reported above and run ` +
189
+ `'sous build' again.`
190
+ );
191
+ }
192
+ }
193
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * `sous repo search <text>`.
3
+ *
4
+ * Searches the cached index of every repository this project trusts: recipe
5
+ * names, namespace names and descriptions. It reads only what is already on
6
+ * disk, so it works offline and never downloads anything; a repository whose
7
+ * index has not been fetched yet is named at the end rather than silently left
8
+ * out of the results.
9
+ */
10
+
11
+ import { Args, Flags } from "@oclif/core";
12
+ import { BaseCommand } from "../../base-command.js";
13
+ import { subscriptionServiceFor } from "../../lib/repos/subscription-service.js";
14
+ import { renderTable, type TableColumn } from "../../utils/table.js";
15
+ import {
16
+ blankLine,
17
+ footer,
18
+ indent,
19
+ log,
20
+ paragraph,
21
+ section,
22
+ showCommandVars,
23
+ } from "../../utils/formatting.js";
24
+
25
+ /** How far every line of this command's output is indented. */
26
+ const INDENT = 2;
27
+
28
+ /**
29
+ * The columns the results show. The recipe and its versions are why anybody ran
30
+ * the search, so they stay however narrow the terminal is; the description takes
31
+ * whatever room is left and wraps rather than being cut, because half a sentence
32
+ * helps nobody.
33
+ */
34
+ const COLUMNS: TableColumn[] = [
35
+ { key: "key", header: "Recipe", kind: "path", overflow: "truncate", minWidth: 12 },
36
+ { key: "versions", header: "Versions", overflow: "truncate", minWidth: 7 },
37
+ { key: "repo", header: "Repository", overflow: "truncate", priority: "medium" },
38
+ {
39
+ key: "description",
40
+ header: "What it is",
41
+ overflow: "wrap",
42
+ flex: 1,
43
+ priority: "low",
44
+ minWidth: 16,
45
+ },
46
+ ];
47
+
48
+ /** One recipe that matched, ready to be shown. */
49
+ type Match = {
50
+ repo: string;
51
+ key: string;
52
+ description: string;
53
+ versions: string[];
54
+ };
55
+
56
+ export default class RepoSearch extends BaseCommand {
57
+ static description =
58
+ "Search the recipes every trusted repository publishes, by name or description";
59
+
60
+ /**
61
+ * Searching is the way into every other repository command, so it is also a
62
+ * top-level `sous search` and is listed as one. `repos:search` is the plural
63
+ * spelling of the topic.
64
+ */
65
+ static aliases = ["search", "repos:search"];
66
+
67
+ static examples = [
68
+ "<%= config.bin %> repo search task",
69
+ "<%= config.bin %> repo search browser --limit 50",
70
+ ];
71
+
72
+ static args = {
73
+ text: Args.string({
74
+ description: "The text to look for in a namespace, recipe name or description",
75
+ required: true,
76
+ }),
77
+ };
78
+
79
+ static flags = {
80
+ ...BaseCommand.baseFlags,
81
+ limit: Flags.integer({
82
+ description: "How many matches to show",
83
+ default: 25,
84
+ }),
85
+ };
86
+
87
+ async run(): Promise<void> {
88
+ const { args, flags } = await this.parse(RepoSearch);
89
+ const needle = args.text.trim().toLowerCase();
90
+
91
+ showCommandVars({
92
+ Project: this.projectLabel,
93
+ Config: this.configContext.configPath,
94
+ Searching: args.text,
95
+ });
96
+
97
+ section("Recipes matching your search");
98
+
99
+ const service = subscriptionServiceFor({
100
+ configContext: this.configContext,
101
+ settings: this.settings,
102
+ shellEnv: this.shellEnv,
103
+ });
104
+
105
+ const repos = Object.keys(service.currentRepos()).sort();
106
+ const matches: Match[] = [];
107
+ const notFetched: string[] = [];
108
+
109
+ for (const repo of repos) {
110
+ const index = service.cachedIndex(repo);
111
+ if (index === undefined) {
112
+ notFetched.push(repo);
113
+ continue;
114
+ }
115
+
116
+ for (const [key, recipe] of Object.entries(index.recipes)) {
117
+ const namespace = key.slice(0, key.indexOf("/"));
118
+ const haystack = [
119
+ key,
120
+ recipe.description ?? "",
121
+ index.namespaces[namespace]?.description ?? "",
122
+ ]
123
+ .join(" ")
124
+ .toLowerCase();
125
+ if (!haystack.includes(needle)) continue;
126
+
127
+ matches.push({
128
+ repo,
129
+ key,
130
+ description: recipe.description ?? "",
131
+ versions: Object.keys(recipe.versions).sort(),
132
+ });
133
+ }
134
+ }
135
+
136
+ matches.sort((left, right) =>
137
+ left.key === right.key
138
+ ? left.repo.localeCompare(right.repo)
139
+ : left.key.localeCompare(right.key)
140
+ );
141
+
142
+ // Whatever the results themselves could not say goes into one closing
143
+ // summary, rather than a stack of separate notes under the table.
144
+ const summary: string[] = [];
145
+
146
+ if (matches.length === 0) {
147
+ paragraph(
148
+ repos.length === 0
149
+ ? "This project trusts no repositories yet, so there is nothing to search. " +
150
+ "Add one with 'sous repo add <url>'."
151
+ : `Nothing in the repositories this project trusts matches '${args.text}'.`
152
+ );
153
+ } else {
154
+ const shown = matches.slice(0, flags.limit);
155
+ const rows = shown.map((match) => ({
156
+ key: match.key,
157
+ repo: match.repo,
158
+ versions: match.versions.join(", "),
159
+ description:
160
+ match.description.length > 0 ? match.description : "no description published",
161
+ }));
162
+
163
+ for (const line of renderTable(COLUMNS, rows, { indent: INDENT })) {
164
+ log(indent(line, INDENT));
165
+ }
166
+
167
+ if (matches.length > shown.length) {
168
+ summary.push(
169
+ `${matches.length - shown.length} more matches are not shown. Raise the ` +
170
+ `number with '--limit'.`
171
+ );
172
+ }
173
+ }
174
+
175
+ if (notFetched.length > 0) {
176
+ summary.push(
177
+ `Nothing has been fetched from these repositories yet, so they were not ` +
178
+ `searched: ${notFetched.join(", ")}.`
179
+ );
180
+ }
181
+
182
+ if (summary.length > 0) {
183
+ blankLine();
184
+ for (const line of summary) log(indent(line));
185
+ }
186
+
187
+ footer();
188
+ }
189
+ }
@@ -0,0 +1,133 @@
1
+ import { Command, Flags } from "@oclif/core";
2
+ import { SOUS_VERSION } from "../../lib/settings.js";
3
+ import { findRepoRoot, submitRepo } from "../../lib/repos/release/index.js";
4
+ import { reportCommandError } from "../../utils/command-errors.js";
5
+ import { nonInteractiveFlag } from "../../utils/flags.js";
6
+ import {
7
+ blankLine,
8
+ dryRunNotice,
9
+ footer,
10
+ header,
11
+ log,
12
+ section,
13
+ showCommandVars,
14
+ showVariables,
15
+ } from "../../utils/formatting.js";
16
+
17
+ /**
18
+ * `sous repo submit` proposes the committed changes in a recipe repository to
19
+ * that repository's maintainers.
20
+ *
21
+ * Like `sous repo init` and `sous repo release`, this command does NOT extend
22
+ * BaseCommand: it runs inside a RECIPE repository, which is not a sous project
23
+ * and has no `.sous/` directory of its own.
24
+ *
25
+ * Submitting never publishes and never writes to a repository directly. Sous
26
+ * validates first, then hands the fork, branch and pull request mechanics to the
27
+ * provider's own command line tool, which already holds the contributor's
28
+ * credentials. Every step is printed before it runs, so a failure halfway
29
+ * through leaves no doubt about what did happen.
30
+ */
31
+ export default class RepoSubmit extends Command {
32
+ static description =
33
+ "Propose this recipe repository's committed changes to its maintainers";
34
+
35
+ /**
36
+ * The other spelling of the topic. It lives under a hidden topic, so it is
37
+ * typable everywhere without ever reaching the top-level listing.
38
+ */
39
+ static aliases = ["repos:submit"];
40
+
41
+ static examples = [
42
+ "<%= config.bin %> repo submit",
43
+ '<%= config.bin %> repo submit --title "Add a linting recipe"',
44
+ "<%= config.bin %> repo submit --draft",
45
+ "<%= config.bin %> repo submit --dry-run",
46
+ ];
47
+
48
+ static flags = {
49
+ title: Flags.string({
50
+ description: "Title for the proposal. Defaults to the last commit's subject.",
51
+ }),
52
+ body: Flags.string({
53
+ description: "Body for the proposal. Defaults to a summary sous writes.",
54
+ }),
55
+ draft: Flags.boolean({
56
+ description: "Open the proposal as a draft",
57
+ default: false,
58
+ }),
59
+ "dry-run": Flags.boolean({
60
+ description: "Check everything and print the plan without sending anything",
61
+ default: false,
62
+ }),
63
+ // This command does not extend BaseCommand, so it declares the global
64
+ // non-interactive flag itself; the rule is the same everywhere.
65
+ "non-interactive": nonInteractiveFlag(),
66
+ };
67
+
68
+ async init(): Promise<void> {
69
+ await super.init();
70
+ header();
71
+ }
72
+
73
+ async run(): Promise<void> {
74
+ const { flags } = await this.parse(RepoSubmit);
75
+ const dryRun = flags["dry-run"];
76
+ const rootDir = findRepoRoot(process.cwd());
77
+
78
+ showCommandVars({
79
+ Repository: rootDir,
80
+ Title: flags.title ?? "(the last commit's subject)",
81
+ Draft: flags.draft,
82
+ "Dry Run": dryRun,
83
+ });
84
+
85
+ section("Proposing a change");
86
+
87
+ const result = await submitRepo({
88
+ rootDir,
89
+ title: flags.title,
90
+ body: flags.body,
91
+ draft: flags.draft,
92
+ dryRun,
93
+ sousVersion: SOUS_VERSION,
94
+ onStep: (message) => log(` ${message}`),
95
+ onNotice: (message) => (dryRun ? dryRunNotice(message) : log(` ${message}`)),
96
+ });
97
+
98
+ section(dryRun ? "What would be proposed" : "What was proposed");
99
+ showVariables({
100
+ Provider: result.provider,
101
+ Repository: `${result.repo.owner}/${result.repo.name}`,
102
+ Branch: result.branch,
103
+ "Target branch": result.baseBranch,
104
+ "Pushed to": result.dryRun ? "(nothing was pushed)" : result.pushedTo,
105
+ "Through a fork": result.usedFork,
106
+ Title: result.title,
107
+ Proposal: result.url ?? "(the provider printed no address)",
108
+ });
109
+
110
+ blankLine();
111
+ if (result.dryRun) {
112
+ log(" Nothing was sent. Run the command again without --dry-run to propose it.");
113
+ } else {
114
+ log(" The maintainers decide what happens next; sous never publishes on their");
115
+ log(" behalf. Anything they ask for goes on the same branch, and the proposal");
116
+ log(" updates itself when you push again.");
117
+ }
118
+
119
+ footer();
120
+ }
121
+
122
+ /**
123
+ * Renders a configuration error as a plain, readable message rather than an
124
+ * oclif stack trace, matching what BaseCommand does for every other command.
125
+ * An error raised because a question could not be asked also gets this
126
+ * command's own help underneath it.
127
+ */
128
+ protected async catch(error: Error & { exitCode?: number }): Promise<unknown> {
129
+ const exitCode = await reportCommandError(this, error);
130
+ if (exitCode === undefined) return super.catch(error);
131
+ return this.exit(exitCode);
132
+ }
133
+ }
@@ -0,0 +1,147 @@
1
+ import { Args, Flags } from "@oclif/core";
2
+ import { BaseCommand } from "../../base-command.js";
3
+ import { ConfigError } from "../../lib/errors.js";
4
+ import {
5
+ readGlobalLinks,
6
+ readProjectLinks,
7
+ writeGlobalLinks,
8
+ writeProjectLinks,
9
+ } from "../../lib/repos/links.js";
10
+ import {
11
+ blankLine,
12
+ dryRunNotice,
13
+ footer,
14
+ log,
15
+ section,
16
+ showCommandVars,
17
+ showVariables,
18
+ } from "../../utils/formatting.js";
19
+
20
+ /**
21
+ * `sous repo unlink` stops reading a repository from a working copy and goes
22
+ * back to published versions.
23
+ *
24
+ * It only ever removes a map entry. The checkout stays exactly where it is,
25
+ * because sous did not necessarily put it there, and even when it did, deleting
26
+ * a directory that may hold uncommitted work is not something a command like
27
+ * this should decide on its own. The path is printed so the user can delete it
28
+ * themselves if they want to.
29
+ */
30
+ export default class RepoUnlink extends BaseCommand {
31
+ static description =
32
+ "Stop reading a repository from a working copy and go back to published versions";
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:unlink"];
39
+
40
+ static examples = [
41
+ "<%= config.bin %> repo unlink sous-recipes",
42
+ "<%= config.bin %> repo unlink sous-recipes --global",
43
+ ];
44
+
45
+ static args = {
46
+ repo: Args.string({
47
+ description: "The repository's short name, as it appears in the links map",
48
+ required: true,
49
+ }),
50
+ };
51
+
52
+ static flags = {
53
+ ...BaseCommand.baseFlags,
54
+ global: Flags.boolean({
55
+ description:
56
+ "Remove the machine-wide link rather than this project's link",
57
+ default: false,
58
+ }),
59
+ "dry-run": Flags.boolean({
60
+ description: "Print what would change without writing anything",
61
+ default: false,
62
+ }),
63
+ };
64
+
65
+ async run(): Promise<void> {
66
+ const { args, flags } = await this.parse(RepoUnlink);
67
+ const { sousDir } = this.configContext;
68
+ const isGlobal = flags.global;
69
+ const name = args.repo;
70
+
71
+ showCommandVars({
72
+ Project: this.projectLabel,
73
+ Repository: name,
74
+ Scope: isGlobal ? "this machine" : "this project",
75
+ "Dry Run": flags["dry-run"],
76
+ });
77
+
78
+ section("Unlinking a repository");
79
+
80
+ const map = isGlobal ? readGlobalLinks() : readProjectLinks(sousDir);
81
+ const entry = map.links[name];
82
+
83
+ if (entry === undefined) {
84
+ throw new ConfigError(this.notLinkedMessage(name, isGlobal, sousDir));
85
+ }
86
+
87
+ if (flags["dry-run"]) {
88
+ dryRunNotice(`would unlink '${name}', which points at ${entry.path}`);
89
+ dryRunNotice("the checkout itself would be left where it is");
90
+ footer();
91
+ return;
92
+ }
93
+
94
+ delete map.links[name];
95
+ const linksPath = isGlobal ? writeGlobalLinks(map) : writeProjectLinks(sousDir, map);
96
+
97
+ showVariables({
98
+ Repository: name,
99
+ "Checkout left at": entry.path,
100
+ "Updated": linksPath,
101
+ });
102
+
103
+ blankLine();
104
+ log(` '${name}' now resolves to its published versions again.`);
105
+ log(
106
+ entry.origin === "clone"
107
+ ? " sous cloned that checkout; it has been left in place, and you may delete it."
108
+ : " That checkout was yours to begin with, and has not been touched."
109
+ );
110
+
111
+ footer();
112
+ }
113
+
114
+ /**
115
+ * Explains that nothing was unlinked, and says where the link actually is
116
+ * when the user asked about the wrong scope; getting the scope wrong is the
117
+ * easiest mistake to make here.
118
+ *
119
+ * @param name - The repository's short name, as the user typed it.
120
+ * @param isGlobal - Which map was searched.
121
+ * @param sousDir - The project's discovered `.sous/` directory.
122
+ */
123
+ private notLinkedMessage(name: string, isGlobal: boolean, sousDir: string): string {
124
+ const searched = isGlobal ? "the machine-wide links map" : "this project's links map";
125
+ const other = isGlobal ? readProjectLinks(sousDir) : readGlobalLinks();
126
+
127
+ if (other.links[name] !== undefined) {
128
+ const flag = isGlobal ? "without --global" : "with --global";
129
+ return (
130
+ `'${name}' is not linked in ${searched}.\n` +
131
+ ` It is linked in the ${isGlobal ? "project" : "machine-wide"} map, at ` +
132
+ `${other.links[name]!.path}.\n` +
133
+ ` Run the same command ${flag} to remove that one.`
134
+ );
135
+ }
136
+
137
+ const map = isGlobal ? readGlobalLinks() : readProjectLinks(sousDir);
138
+ const linked = Object.keys(map.links).sort();
139
+ return (
140
+ `'${name}' is not linked in ${searched}.\n` +
141
+ (linked.length > 0
142
+ ? ` Linked there: ${linked.join(", ")}.\n`
143
+ : ` Nothing is linked there.\n`) +
144
+ ` Run 'sous repo link ${name}' to link it.`
145
+ );
146
+ }
147
+ }