@sous-io/sous 0.2.28 → 0.2.30
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.
- package/docs/markdown/_sidebar.md +2 -0
- package/docs/markdown/commands.md +10 -6
- package/docs/markdown/repositories-authoring.md +19 -11
- package/docs/markdown/repositories-consuming.md +34 -13
- package/docs/markdown/repositories-file-formats.md +88 -13
- package/docs/markdown/repositories-providers.md +8 -6
- package/docs/markdown/repositories-troubleshooting.md +6 -0
- package/docs/markdown/repositories-variables.md +13 -3
- package/docs/markdown/repositories.md +3 -3
- package/package.json +1 -1
- package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +11 -7
- package/recipes/core/sous-skills/skills/about-sous-repos/SKILL.tpl.md +7 -4
- package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
- package/src/commands/lock/rebuild.ts +7 -6
- package/src/commands/recipe/show.ts +163 -64
- package/src/commands/repo/contribute.ts +1 -10
- package/src/commands/repo/gc.ts +3 -2
- package/src/commands/repo/release.ts +20 -3
- package/src/commands/repo/search.ts +2 -1
- package/src/commands/vars/ask.ts +32 -10
- package/src/lib/refs/find.ts +202 -21
- package/src/lib/refs/index.ts +2 -0
- package/src/lib/refs/parse.ts +844 -0
- package/src/lib/refs/scopes.ts +34 -0
- package/src/lib/refs/settle.ts +219 -0
- package/src/lib/repos/catalog.ts +87 -80
- package/src/lib/repos/declarations.ts +177 -0
- package/src/lib/repos/formats/common.ts +14 -0
- package/src/lib/repos/formats/index-file.ts +42 -2
- package/src/lib/repos/formats/lockfile.ts +5 -3
- package/src/lib/repos/formats/recipe-manifest.ts +16 -212
- package/src/lib/repos/formats/variable-definition.ts +268 -0
- package/src/lib/repos/index.ts +0 -1
- package/src/lib/repos/lock-service.ts +3 -2
- package/src/lib/repos/locked-namespace-resolver.ts +21 -15
- package/src/lib/repos/locked-recipes.ts +2 -2
- package/src/lib/repos/namespace-resolver.ts +40 -47
- package/src/lib/repos/providers/base.ts +30 -0
- package/src/lib/repos/providers/github.ts +22 -0
- package/src/lib/repos/providers/gitlab.ts +68 -0
- package/src/lib/repos/providers/provider.ts +96 -0
- package/src/lib/repos/release/index-builder.ts +221 -47
- package/src/lib/repos/release/index.ts +1 -0
- package/src/lib/repos/release/plan.ts +8 -6
- package/src/lib/repos/release/settle.ts +237 -0
- package/src/lib/repos/release/tags.ts +3 -7
- package/src/lib/repos/resolver.ts +251 -53
- package/src/lib/repos/subscription-service.ts +355 -49
- package/src/lib/repos/update-plan.ts +3 -2
- package/src/lib/vars/mappings.ts +17 -30
- package/src/lib/vars/question-plan.ts +44 -21
- package/src/lib/repos/ref.ts +0 -513
package/src/lib/vars/mappings.ts
CHANGED
|
@@ -26,12 +26,9 @@ import {
|
|
|
26
26
|
managedLayerHeader,
|
|
27
27
|
updateManagedLayer,
|
|
28
28
|
} from "../repos/managed-layer.js";
|
|
29
|
-
import {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
REPO_NAME_PATTERN,
|
|
33
|
-
VARIABLE_NAME_PATTERN,
|
|
34
|
-
} from "../repos/formats/patterns.js";
|
|
29
|
+
import { VARIABLE_NAME_PATTERN } from "../repos/formats/patterns.js";
|
|
30
|
+
import { RefSource } from "../refs/scopes.js";
|
|
31
|
+
import { formatRef, parseShortRef, type ParsedRef } from "../refs/parse.js";
|
|
35
32
|
import type { DefinedVariable } from "./definition-source.js";
|
|
36
33
|
|
|
37
34
|
/** File name of the machine-written mapping record layer. */
|
|
@@ -84,32 +81,22 @@ export function parseMappingTarget(input: string): MappingTarget {
|
|
|
84
81
|
const trimmed = typeof input === "string" ? input.trim() : "";
|
|
85
82
|
if (trimmed.length === 0) fail("a target must not be empty.");
|
|
86
83
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
`the repository qualifier '${repo}' must be lowercase kebab-case: a letter, ` +
|
|
97
|
-
"then letters, digits or hyphens."
|
|
98
|
-
);
|
|
99
|
-
}
|
|
84
|
+
// A target is a recipe ref with the variable after one more slash, so the
|
|
85
|
+
// ref part is read by the one ref parser and held to the stored form.
|
|
86
|
+
const slash = trimmed.lastIndexOf("/");
|
|
87
|
+
const variable = slash === -1 ? "" : trimmed.slice(slash + 1);
|
|
88
|
+
let ref: ParsedRef = { namespace: "" };
|
|
89
|
+
try {
|
|
90
|
+
ref = parseShortRef(trimmed.slice(0, Math.max(slash, 0)), RefSource.CommandLine);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
fail((error as Error).message.split("\n")[0]!.replace(/^Invalid ref '[^']*': /, ""));
|
|
100
93
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (segments.length !== 3) {
|
|
94
|
+
const { repo, namespace, recipe } = ref;
|
|
95
|
+
if (recipe === undefined || ref.range !== undefined) {
|
|
104
96
|
fail("a target names a namespace, a recipe and a variable, joined by slashes.");
|
|
105
97
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (!NAMESPACE_NAME_PATTERN.test(namespace)) {
|
|
109
|
-
fail(`the namespace '${namespace}' must be lowercase kebab-case.`);
|
|
110
|
-
}
|
|
111
|
-
if (!RECIPE_NAME_PATTERN.test(recipe)) {
|
|
112
|
-
fail(`the recipe name '${recipe}' must be lowercase kebab-case.`);
|
|
98
|
+
if (formatRef(ref) !== formatRef(ref).toLowerCase()) {
|
|
99
|
+
fail(`the repository, namespace and recipe in '${formatRef(ref)}' must be lowercase.`);
|
|
113
100
|
}
|
|
114
101
|
if (!VARIABLE_NAME_PATTERN.test(variable)) {
|
|
115
102
|
fail(
|
|
@@ -118,7 +105,7 @@ export function parseMappingTarget(input: string): MappingTarget {
|
|
|
118
105
|
);
|
|
119
106
|
}
|
|
120
107
|
|
|
121
|
-
const parsed: MappingTarget = { namespace, recipe
|
|
108
|
+
const parsed: MappingTarget = { namespace, recipe: recipe!, variable };
|
|
122
109
|
if (repo !== undefined) parsed.repo = repo;
|
|
123
110
|
return parsed;
|
|
124
111
|
}
|
|
@@ -93,11 +93,19 @@ function questionCount(count: number): string {
|
|
|
93
93
|
return count === 1 ? "1 question" : `${count} questions`;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
/**
|
|
97
|
-
|
|
96
|
+
/**
|
|
97
|
+
* The labeled facts one planned question shows.
|
|
98
|
+
*
|
|
99
|
+
* @param planned - The planned question.
|
|
100
|
+
* @param answerHints - Whether to name the `--answer` flag that answers it ahead of time.
|
|
101
|
+
*/
|
|
102
|
+
export function plannedVariableFacts(
|
|
103
|
+
planned: PlannedVariable,
|
|
104
|
+
answerHints = true
|
|
105
|
+
): LabeledFact[] {
|
|
98
106
|
const { definition } = planned.defined;
|
|
99
107
|
|
|
100
|
-
|
|
108
|
+
const facts: LabeledFact[] = [
|
|
101
109
|
{ label: "about", lines: [firstSentence(definition.description)] },
|
|
102
110
|
{ label: "example", lines: [String(definition.example)] },
|
|
103
111
|
{ label: "stored-as", lines: [planned.storedAs] },
|
|
@@ -112,8 +120,11 @@ export function plannedVariableFacts(planned: PlannedVariable): LabeledFact[] {
|
|
|
112
120
|
: "no, and an answer is optional",
|
|
113
121
|
],
|
|
114
122
|
},
|
|
115
|
-
{ label: "answer-with", lines: [`--answer ${definition.name}=<value>`] },
|
|
116
123
|
];
|
|
124
|
+
if (answerHints) {
|
|
125
|
+
facts.push({ label: "answer-with", lines: [`--answer ${definition.name}=<value>`] });
|
|
126
|
+
}
|
|
127
|
+
return facts;
|
|
117
128
|
}
|
|
118
129
|
|
|
119
130
|
/** What `formatQuestionPlan` needs beyond the questions themselves. */
|
|
@@ -121,26 +132,37 @@ export interface QuestionPlanFormatOptions {
|
|
|
121
132
|
/** The column to wrap descriptions at. */
|
|
122
133
|
width?: number;
|
|
123
134
|
/**
|
|
124
|
-
* Recipes in the closure whose
|
|
125
|
-
*
|
|
126
|
-
* silently left out of it.
|
|
135
|
+
* Recipes in the closure whose questions are unknown: their files are not on
|
|
136
|
+
* this machine, and their repository's index does not record their
|
|
137
|
+
* questions. They are named in the plan rather than silently left out of it.
|
|
127
138
|
*/
|
|
128
139
|
unreadable?: string[];
|
|
140
|
+
/**
|
|
141
|
+
* Whether the plan says how to answer its questions ahead of time: the
|
|
142
|
+
* `--answer` flag under each question, and a closing sentence. True for the
|
|
143
|
+
* dry run of a command that asks them; false where the plan only describes.
|
|
144
|
+
*/
|
|
145
|
+
answerHints?: boolean;
|
|
129
146
|
}
|
|
130
147
|
|
|
131
148
|
/**
|
|
132
|
-
* The one line that names the recipes
|
|
133
|
-
*
|
|
134
|
-
*
|
|
149
|
+
* The one line that names the recipes whose questions are unknown. Nothing is
|
|
150
|
+
* downloaded to plan, so a recipe this machine does not hold yet is described
|
|
151
|
+
* from its repository's index, and an index written before sous recorded
|
|
152
|
+
* questions there cannot say; naming those recipes is more useful than leaving
|
|
153
|
+
* them out of the plan.
|
|
135
154
|
*
|
|
136
|
-
* @param unreadable - The recipe keys whose
|
|
155
|
+
* @param unreadable - The recipe keys whose questions are unknown.
|
|
156
|
+
* @param answerHints - Whether the plan belongs to a dry run that could be run for real.
|
|
137
157
|
*/
|
|
138
|
-
function unreadableLine(unreadable: string[]): string {
|
|
139
|
-
|
|
140
|
-
`Not on this machine yet,
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
158
|
+
function unreadableLine(unreadable: string[], answerHints: boolean): string {
|
|
159
|
+
const line =
|
|
160
|
+
`Not on this machine yet, and their repository's index does not record their ` +
|
|
161
|
+
`questions, so they cannot be listed: ${unreadable.join(", ")}.`;
|
|
162
|
+
return answerHints
|
|
163
|
+
? `${line} A dry run downloads nothing; run this command again without ` +
|
|
164
|
+
`'--dry-run' to install them and be asked.`
|
|
165
|
+
: line;
|
|
144
166
|
}
|
|
145
167
|
|
|
146
168
|
/**
|
|
@@ -157,6 +179,7 @@ export function formatQuestionPlan(
|
|
|
157
179
|
options: QuestionPlanFormatOptions = {}
|
|
158
180
|
): string[] {
|
|
159
181
|
const unreadable = options.unreadable ?? [];
|
|
182
|
+
const answerHints = options.answerHints ?? true;
|
|
160
183
|
|
|
161
184
|
const columns = options.width ?? wrapColumns();
|
|
162
185
|
/** Wraps one sentence to the width the caller's indentation leaves for it. */
|
|
@@ -166,7 +189,7 @@ export function formatQuestionPlan(
|
|
|
166
189
|
if (planned.length === 0) {
|
|
167
190
|
return unreadable.length === 0
|
|
168
191
|
? sentence("None of these recipes ask any questions, so nothing needs answering.")
|
|
169
|
-
: sentence(unreadableLine(unreadable), palette.note);
|
|
192
|
+
: sentence(unreadableLine(unreadable, answerHints), palette.note);
|
|
170
193
|
}
|
|
171
194
|
|
|
172
195
|
const unanswered = planned.filter((entry) => !entry.answered).length;
|
|
@@ -194,15 +217,15 @@ export function formatQuestionPlan(
|
|
|
194
217
|
for (const entry of entries) {
|
|
195
218
|
lines.push("", ` ${color.cyan(entry.defined.definition.name)}`);
|
|
196
219
|
// The facts block indents itself, so the plan adds nothing on top of it.
|
|
197
|
-
lines.push(...renderFacts(plannedVariableFacts(entry), columns - 2));
|
|
220
|
+
lines.push(...renderFacts(plannedVariableFacts(entry, answerHints), columns - 2));
|
|
198
221
|
}
|
|
199
222
|
}
|
|
200
223
|
|
|
201
224
|
if (unreadable.length > 0) {
|
|
202
|
-
lines.push("", ...sentence(unreadableLine(unreadable), palette.note));
|
|
225
|
+
lines.push("", ...sentence(unreadableLine(unreadable, answerHints), palette.note));
|
|
203
226
|
}
|
|
204
227
|
|
|
205
|
-
if (unanswered > 0) {
|
|
228
|
+
if (unanswered > 0 && answerHints) {
|
|
206
229
|
lines.push(
|
|
207
230
|
"",
|
|
208
231
|
...sentence(
|