@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.
- package/README.md +115 -35
- package/bin/run.js +10 -1
- package/docs/markdown/README.md +27 -0
- package/docs/markdown/_sidebar.md +18 -0
- package/docs/markdown/commands.md +308 -0
- package/docs/markdown/config-discovery.md +74 -0
- package/docs/markdown/config-inspection.md +69 -0
- package/docs/markdown/config-layers.md +92 -0
- package/docs/markdown/config-variables.md +79 -0
- package/docs/markdown/configuration.md +71 -0
- package/docs/markdown/design-principles.md +59 -0
- package/docs/markdown/repositories-authoring.md +409 -0
- package/docs/markdown/repositories-consuming.md +580 -0
- package/docs/markdown/repositories-file-formats.md +1084 -0
- package/docs/markdown/repositories-variables.md +387 -0
- package/docs/markdown/repositories.md +303 -0
- package/docs/markdown/skill-categories.md +58 -0
- package/package.json +72 -8
- package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/SKILL.tpl.md +20 -20
- package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/examples/about-something.md +2 -2
- package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/examples/do-something.md +1 -1
- package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/advanced-patterns.md +6 -6
- package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/commands.md +5 -5
- package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/frontmatter.md +3 -3
- package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-liquid-templates/SKILL.tpl.md +40 -25
- package/recipes/core/sous-skills/skills/about-sous/SKILL.tpl.md +70 -0
- package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +75 -0
- package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/create-skill/SKILL.tpl.md +8 -9
- package/recipes/core/sous-skills/sous.recipe.yaml +45 -0
- package/sous.config.schema.json +337 -0
- package/src/base-command.ts +220 -67
- package/src/commands/build.ts +150 -73
- package/src/commands/clear.ts +23 -15
- package/src/commands/compile.ts +74 -16
- package/src/commands/config/get.ts +110 -0
- package/src/commands/config/show.ts +32 -0
- package/src/commands/config/validate.ts +53 -0
- package/src/commands/help.ts +46 -0
- package/src/commands/launch.ts +36 -14
- package/src/commands/lock/rebuild.ts +241 -0
- package/src/commands/lock/show.ts +115 -0
- package/src/commands/namespace/list.ts +117 -0
- package/src/commands/namespace/show.ts +110 -0
- package/src/commands/prune.ts +3 -11
- package/src/commands/recipe/list.ts +95 -0
- package/src/commands/recipe/show.ts +301 -0
- package/src/commands/repo/add.ts +145 -0
- package/src/commands/repo/gc.ts +172 -0
- package/src/commands/repo/init.ts +136 -0
- package/src/commands/repo/link.ts +500 -0
- package/src/commands/repo/list.ts +179 -0
- package/src/commands/repo/release.ts +625 -0
- package/src/commands/repo/remove.ts +193 -0
- package/src/commands/repo/search.ts +189 -0
- package/src/commands/repo/submit.ts +133 -0
- package/src/commands/repo/unlink.ts +147 -0
- package/src/commands/subscription/add.ts +285 -0
- package/src/commands/subscription/list.ts +129 -0
- package/src/commands/subscription/remove.ts +181 -0
- package/src/commands/vars/ask.ts +374 -0
- package/src/commands/vars/index.ts +79 -0
- package/src/commands/vars/list.ts +67 -0
- package/src/commands/vars/show.ts +77 -0
- package/src/config-command.ts +30 -0
- package/src/lib/build-service.ts +206 -54
- package/src/lib/config-discovery.ts +220 -27
- package/src/lib/config-inspect.ts +145 -0
- package/src/lib/config-kernel.mjs +377 -0
- package/src/lib/config-schema.ts +361 -0
- package/src/lib/env-file.ts +328 -0
- package/src/lib/env-local.ts +18 -1
- package/src/lib/errors.ts +32 -0
- package/src/lib/include-resolver.ts +108 -15
- package/src/lib/interactive.ts +165 -0
- package/src/lib/markdown-compiler.ts +118 -37
- package/src/lib/package-info.ts +25 -0
- package/src/lib/pid-service.ts +32 -21
- package/src/lib/refs/find.ts +589 -0
- package/src/lib/refs/index.ts +12 -0
- package/src/lib/refs/pick.ts +147 -0
- package/src/lib/refs/scopes.ts +61 -0
- package/src/lib/repos/catalog-display.ts +116 -0
- package/src/lib/repos/catalog-inputs.ts +160 -0
- package/src/lib/repos/catalog.ts +722 -0
- package/src/lib/repos/core-recipe.ts +105 -0
- package/src/lib/repos/defaults.ts +175 -0
- package/src/lib/repos/formats/common.ts +389 -0
- package/src/lib/repos/formats/index-file.ts +215 -0
- package/src/lib/repos/formats/links-map.ts +96 -0
- package/src/lib/repos/formats/lockfile.ts +167 -0
- package/src/lib/repos/formats/patterns.ts +57 -0
- package/src/lib/repos/formats/recipe-manifest.ts +395 -0
- package/src/lib/repos/formats/repo-manifest.ts +88 -0
- package/src/lib/repos/formats/store-entry.ts +84 -0
- package/src/lib/repos/freshness.ts +208 -0
- package/src/lib/repos/git-clone.ts +312 -0
- package/src/lib/repos/identity.ts +89 -0
- package/src/lib/repos/index.ts +58 -0
- package/src/lib/repos/links.ts +353 -0
- package/src/lib/repos/load-manifest.ts +236 -0
- package/src/lib/repos/lock-service.ts +453 -0
- package/src/lib/repos/locked-namespace-resolver.ts +90 -0
- package/src/lib/repos/locked-recipes.ts +254 -0
- package/src/lib/repos/managed-layer.ts +422 -0
- package/src/lib/repos/namespace-resolver.ts +370 -0
- package/src/lib/repos/providers/base.ts +206 -0
- package/src/lib/repos/providers/git.ts +233 -0
- package/src/lib/repos/providers/github.ts +294 -0
- package/src/lib/repos/providers/gitlab.ts +263 -0
- package/src/lib/repos/providers/http.ts +102 -0
- package/src/lib/repos/providers/index-cache.ts +382 -0
- package/src/lib/repos/providers/index.ts +106 -0
- package/src/lib/repos/providers/local.ts +391 -0
- package/src/lib/repos/providers/provider.ts +401 -0
- package/src/lib/repos/recipe-config-layers.ts +287 -0
- package/src/lib/repos/recipe-targets.ts +223 -0
- package/src/lib/repos/ref-search.ts +46 -0
- package/src/lib/repos/ref.ts +513 -0
- package/src/lib/repos/reference-report.ts +122 -0
- package/src/lib/repos/release/bump.ts +161 -0
- package/src/lib/repos/release/git-state.ts +305 -0
- package/src/lib/repos/release/index-builder.ts +635 -0
- package/src/lib/repos/release/index.ts +16 -0
- package/src/lib/repos/release/plan.ts +512 -0
- package/src/lib/repos/release/submit-service.ts +496 -0
- package/src/lib/repos/release/tags.ts +243 -0
- package/src/lib/repos/release/validate.ts +463 -0
- package/src/lib/repos/resolver.ts +789 -0
- package/src/lib/repos/scaffold/index.ts +238 -0
- package/src/lib/repos/scaffold/templates.ts +415 -0
- package/src/lib/repos/seed.ts +414 -0
- package/src/lib/repos/store/contract.ts +64 -0
- package/src/lib/repos/store/hash.ts +114 -0
- package/src/lib/repos/store/recipe-store.ts +599 -0
- package/src/lib/repos/store/settings.ts +58 -0
- package/src/lib/repos/subscription-service.ts +2678 -0
- package/src/lib/repos/trust.ts +447 -0
- package/src/lib/settings.ts +546 -189
- package/src/lib/sous-home.ts +104 -0
- package/src/lib/state.ts +52 -20
- package/src/lib/vars/ask.ts +1152 -0
- package/src/lib/vars/definition-source.ts +252 -0
- package/src/lib/vars/display.ts +233 -0
- package/src/lib/vars/index.ts +18 -0
- package/src/lib/vars/ladder.ts +282 -0
- package/src/lib/vars/mappings.ts +265 -0
- package/src/lib/vars/names.ts +94 -0
- package/src/lib/vars/preanswers.ts +395 -0
- package/src/lib/vars/question-plan.ts +218 -0
- package/src/lib/vars/report.ts +228 -0
- package/src/lib/vars/safe-regex.ts +235 -0
- package/src/lib/vars/validate.ts +312 -0
- package/src/lib/watch-loop.ts +148 -0
- package/src/templating/init-liquid-engine.ts +58 -16
- package/src/utils/choice-prompt.ts +143 -0
- package/src/utils/command-errors.ts +186 -0
- package/src/utils/command-help.ts +45 -0
- package/src/utils/confirm-prompt.ts +110 -0
- package/src/utils/flags.ts +153 -0
- package/src/utils/formatting.ts +540 -55
- package/src/utils/prompts.ts +35 -1
- package/src/utils/sous-directory.ts +245 -0
- package/src/utils/table.ts +603 -0
- package/src/utils/value-prompt.ts +119 -0
- package/shared-prompts/_partials/resume-task.md +0 -51
- package/shared-prompts/_partials/sub-agent-delegation.md +0 -32
- package/shared-prompts/_partials/update-task-file.md +0 -52
- package/shared-prompts/memories/automated-browser-tasks/INDEX.tpl.md +0 -52
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/SKILL.tpl.md +0 -102
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/auth-failure-handling.mjs +0 -81
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/chained-workflow.mjs +0 -126
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/simple-fetch.mjs +0 -92
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/architecture.md +0 -61
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/auth-and-sessions.md +0 -65
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/ctx-api.md +0 -96
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/installation.md +0 -104
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/script-conventions.md +0 -243
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/chrome-state.mjs +0 -148
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/debug.mjs +0 -383
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/debug.spec.mjs +0 -267
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/eslint.config.mjs +0 -56
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/harness.mjs +0 -169
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/keyring.mjs +0 -59
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/logger.mjs +0 -25
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/params.mjs +0 -140
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/run.mjs +0 -140
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/settings.tpl.mjs +0 -1
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/utils.mjs +0 -185
- package/shared-prompts/skills/automated-browser-tasks/create-automated-browser-task/SKILL.tpl.md +0 -52
- package/shared-prompts/skills/automated-browser-tasks/running-automated-browser-tasks/SKILL.tpl.md +0 -59
- package/shared-prompts/skills/automated-browser-tasks/update-automated-browser-task/SKILL.tpl.md +0 -47
- package/shared-prompts/skills/control-flow/approve/SKILL.tpl.md +0 -26
- package/shared-prompts/skills/control-flow/opine/SKILL.tpl.md +0 -58
- package/shared-prompts/skills/control-flow/repeat/SKILL.tpl.md +0 -27
- package/shared-prompts/skills/control-flow/research/SKILL.tpl.md +0 -34
- package/shared-prompts/skills/sous-skills/about-sous/SKILL.tpl.md +0 -51
- package/shared-prompts/skills/task-files/about-task-files/SKILL.tpl.md +0 -122
- package/shared-prompts/skills/task-files/continue-task-in-new-branch/SKILL.tpl.md +0 -80
- package/shared-prompts/skills/task-files/go/SKILL.tpl.md +0 -14
- package/shared-prompts/skills/task-files/resume-task/SKILL.tpl.md +0 -13
- package/shared-prompts/skills/task-files/start-task/SKILL.tpl.md +0 -93
- package/shared-prompts/skills/task-files/update/SKILL.tpl.md +0 -14
- package/shared-prompts/skills/task-files/update-task-file/SKILL.tpl.md +0 -13
- /package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/substitutions.md +0 -0
- /package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-liquid-templates/references/liquid-filters.md +0 -0
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regenerating a repository's `sous.index.json` from what it actually publishes.
|
|
3
|
+
*
|
|
4
|
+
* The index is the portable contract every provider hands back, so it must
|
|
5
|
+
* describe published reality and nothing else. Three rules follow from that,
|
|
6
|
+
* and this module exists to keep them:
|
|
7
|
+
*
|
|
8
|
+
* 1. A published version is IMMUTABLE. An entry that is already in the index
|
|
9
|
+
* keeps the hash it was published with; a recomputed hash that disagrees is
|
|
10
|
+
* reported as an error, never quietly written over.
|
|
11
|
+
* 2. A version is published when a tag carries it, and every version the index
|
|
12
|
+
* records has to have one. The single exception is the version a recipe's
|
|
13
|
+
* manifest declares right now: a release regenerates the index, commits it
|
|
14
|
+
* alongside the manifests, and tags that commit, so for the length of that
|
|
15
|
+
* one commit the index describes a version whose tag is about to exist. Any
|
|
16
|
+
* OLDER version without a tag is an error, because that is a published
|
|
17
|
+
* version nothing can fetch.
|
|
18
|
+
* 3. The tags are the backstop. A version that has a tag but is missing from the
|
|
19
|
+
* index is rebuilt from the tagged tree, so deleting the index file and
|
|
20
|
+
* regenerating it restores the same catalog.
|
|
21
|
+
*
|
|
22
|
+
* Each version entry also records what it was released against, so a consumer
|
|
23
|
+
* installs the versions a recipe was published with rather than re-resolving
|
|
24
|
+
* its ranges long afterwards.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import fs from "node:fs";
|
|
28
|
+
import path from "node:path";
|
|
29
|
+
import semver from "semver";
|
|
30
|
+
import { INDEX_FILENAME } from "../formats/common.js";
|
|
31
|
+
import {
|
|
32
|
+
parseIndexFile,
|
|
33
|
+
stringifyIndexFile,
|
|
34
|
+
type IndexDependency,
|
|
35
|
+
type IndexFile,
|
|
36
|
+
type IndexRecipe,
|
|
37
|
+
type IndexVersion,
|
|
38
|
+
} from "../formats/index-file.js";
|
|
39
|
+
import { parseRecipeManifest } from "../formats/recipe-manifest.js";
|
|
40
|
+
import { loadJsonFile, parseYamlText, parseJsoncText } from "../load-manifest.js";
|
|
41
|
+
import { hashDirectory } from "../store/hash.js";
|
|
42
|
+
import type { RunOptions } from "../providers/git.js";
|
|
43
|
+
import {
|
|
44
|
+
listRecipeTags,
|
|
45
|
+
readFileAtTag,
|
|
46
|
+
recipeTagKey,
|
|
47
|
+
tagCommitDate,
|
|
48
|
+
tagFor,
|
|
49
|
+
withTaggedTree,
|
|
50
|
+
type RecipeTag,
|
|
51
|
+
} from "./tags.js";
|
|
52
|
+
import {
|
|
53
|
+
describeError,
|
|
54
|
+
type RepoValidation,
|
|
55
|
+
type ValidatedRecipe,
|
|
56
|
+
type ValidationProblem,
|
|
57
|
+
} from "./validate.js";
|
|
58
|
+
import { parseDependencyRef } from "../ref.js";
|
|
59
|
+
|
|
60
|
+
/** A version that is ready to publish but has no tag yet. */
|
|
61
|
+
export type PendingRelease = {
|
|
62
|
+
/** The recipe's key, `namespace/recipe`. */
|
|
63
|
+
key: string;
|
|
64
|
+
/** The recipe folder, relative to the repository root. */
|
|
65
|
+
path: string;
|
|
66
|
+
/** The version its manifest declares. */
|
|
67
|
+
version: string;
|
|
68
|
+
/** The tag that would carry it. */
|
|
69
|
+
tag: string;
|
|
70
|
+
/** The content hash of the recipe folder as it stands in the working tree. */
|
|
71
|
+
hash: string;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** What one regeneration produced. */
|
|
75
|
+
export type IndexBuildResult = {
|
|
76
|
+
/** The regenerated index, ready to be written. */
|
|
77
|
+
index: IndexFile;
|
|
78
|
+
/** The index exactly as it would be written, so a caller can compare or save it. */
|
|
79
|
+
text: string;
|
|
80
|
+
/** True when the committed index differs from the regenerated one. */
|
|
81
|
+
stale: boolean;
|
|
82
|
+
/** Versions with no tag yet, in the order the repo manifest lists their recipes. */
|
|
83
|
+
pending: PendingRelease[];
|
|
84
|
+
/** Everything the regeneration found wrong, errors and warnings together. */
|
|
85
|
+
problems: ValidationProblem[];
|
|
86
|
+
/** Every recipe release tag the repository carries. */
|
|
87
|
+
tags: RecipeTag[];
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** What `buildIndex` needs to know. */
|
|
91
|
+
export type BuildIndexOptions = {
|
|
92
|
+
/** The validated repository, from `validateRepo`. */
|
|
93
|
+
validation: RepoValidation;
|
|
94
|
+
/** The committed index, when there is a readable one. */
|
|
95
|
+
existing?: IndexFile;
|
|
96
|
+
/** The version of sous doing the regeneration, recorded as the generator. */
|
|
97
|
+
sousVersion: string;
|
|
98
|
+
/** When the regeneration ran. Defaults to now. */
|
|
99
|
+
now?: Date;
|
|
100
|
+
/** Every release tag, when the caller has already listed them. */
|
|
101
|
+
tags?: RecipeTag[];
|
|
102
|
+
/** The command runner git calls go through. */
|
|
103
|
+
run?: RunOptions["run"];
|
|
104
|
+
/**
|
|
105
|
+
* The versions this run is about to publish, keyed `namespace/recipe`. Their
|
|
106
|
+
* tags do not exist yet: the index is regenerated, committed alongside the
|
|
107
|
+
* manifests, and only then tagged, so the tagged commit already carries an
|
|
108
|
+
* index that describes itself.
|
|
109
|
+
*/
|
|
110
|
+
publishing?: Record<string, string>;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/** The index file's absolute path in a repository. */
|
|
114
|
+
export function indexFilePath(rootDir: string): string {
|
|
115
|
+
return path.join(rootDir, INDEX_FILENAME);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Reads and validates the committed index, or returns undefined when there is
|
|
120
|
+
* none. A file that is present but unreadable raises a ConfigError; the caller
|
|
121
|
+
* decides whether to fail or to regenerate from the tags instead.
|
|
122
|
+
*
|
|
123
|
+
* @param rootDir - The repository's root directory.
|
|
124
|
+
*/
|
|
125
|
+
export function readIndexFile(rootDir: string): IndexFile | undefined {
|
|
126
|
+
const filePath = indexFilePath(rootDir);
|
|
127
|
+
if (!fs.existsSync(filePath)) return undefined;
|
|
128
|
+
return parseIndexFile(loadJsonFile(filePath, "repository index"), filePath);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Regenerates a repository's index from its recipe manifests and its tags.
|
|
133
|
+
*
|
|
134
|
+
* @param options - The validated repository and everything the regeneration needs.
|
|
135
|
+
*/
|
|
136
|
+
export async function buildIndex(
|
|
137
|
+
options: BuildIndexOptions
|
|
138
|
+
): Promise<IndexBuildResult> {
|
|
139
|
+
const { validation, existing, sousVersion } = options;
|
|
140
|
+
const rootDir = validation.rootDir;
|
|
141
|
+
const now = options.now ?? new Date();
|
|
142
|
+
const run = options.run;
|
|
143
|
+
|
|
144
|
+
const tags = options.tags ?? (await listRecipeTags(rootDir, { run }));
|
|
145
|
+
const tagsByKey = groupTagsByKey(tags);
|
|
146
|
+
const problems: ValidationProblem[] = [];
|
|
147
|
+
const pending: PendingRelease[] = [];
|
|
148
|
+
|
|
149
|
+
const namespaces: IndexFile["namespaces"] = {};
|
|
150
|
+
for (const [name, declaration] of Object.entries(validation.manifest.namespaces)) {
|
|
151
|
+
namespaces[name] =
|
|
152
|
+
declaration.description === undefined
|
|
153
|
+
? {}
|
|
154
|
+
: { description: declaration.description };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const recipes: IndexFile["recipes"] = {};
|
|
158
|
+
|
|
159
|
+
// What each recipe in this repository is published at once this run is done,
|
|
160
|
+
// and every version each of them publishes. Both are needed to turn a
|
|
161
|
+
// dependency's declared range into the exact version a consumer installs.
|
|
162
|
+
const publishing = options.publishing ?? {};
|
|
163
|
+
const siblingVersions = new Map<string, string>();
|
|
164
|
+
const publishedVersions = new Map<string, string[]>();
|
|
165
|
+
for (const recipe of validation.recipes) {
|
|
166
|
+
const settled = publishing[recipe.key] ?? recipe.manifest.version;
|
|
167
|
+
siblingVersions.set(recipe.key, settled);
|
|
168
|
+
const known = new Set(
|
|
169
|
+
(tagsByKey.get(recipe.key) ?? []).map((tag) => tag.version)
|
|
170
|
+
);
|
|
171
|
+
for (const version of Object.keys(existing?.recipes[recipe.key]?.versions ?? {})) {
|
|
172
|
+
known.add(version);
|
|
173
|
+
}
|
|
174
|
+
known.add(settled);
|
|
175
|
+
publishedVersions.set(recipe.key, [...known]);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
for (const recipe of validation.recipes) {
|
|
179
|
+
if (!Object.hasOwn(namespaces, recipe.manifest.namespace)) continue;
|
|
180
|
+
|
|
181
|
+
const key = recipe.key;
|
|
182
|
+
const existingVersions = existing?.recipes[key]?.versions ?? {};
|
|
183
|
+
const versions: Record<string, IndexVersion> = { ...existingVersions };
|
|
184
|
+
const recipeTags = tagsByKey.get(key) ?? [];
|
|
185
|
+
const manifestName = path.basename(recipe.manifestPath);
|
|
186
|
+
|
|
187
|
+
// Rule 3: every tagged version belongs in the index, including ones the
|
|
188
|
+
// committed index has lost.
|
|
189
|
+
for (const tag of recipeTags) {
|
|
190
|
+
if (Object.hasOwn(versions, tag.version)) continue;
|
|
191
|
+
versions[tag.version] = {
|
|
192
|
+
hash: await hashTaggedRecipe(rootDir, tag.tag, recipe.path, run),
|
|
193
|
+
tag: tag.tag,
|
|
194
|
+
prerelease: semver.prerelease(tag.version) !== null,
|
|
195
|
+
...(await releasedAtOf(rootDir, tag.tag, run)),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const version = recipe.manifest.version;
|
|
200
|
+
const tagName = tagFor(recipe.manifest.namespace, recipe.manifest.name, version);
|
|
201
|
+
const hasTag = recipeTags.some((entry) => entry.tag === tagName);
|
|
202
|
+
const workingHash = await hashDirectory(recipe.dir);
|
|
203
|
+
const where = relativeTo(rootDir, recipe.manifestPath);
|
|
204
|
+
const dependencies = resolveIndexDependencies(
|
|
205
|
+
recipe,
|
|
206
|
+
validation,
|
|
207
|
+
siblingVersions,
|
|
208
|
+
publishedVersions
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// A version the index publishes must have a tag, with one exception: the
|
|
212
|
+
// version the manifest declares right now, which is the one a release is in
|
|
213
|
+
// the middle of publishing. Everything older is history, and history with a
|
|
214
|
+
// missing tag is a published version nothing can fetch.
|
|
215
|
+
for (const [known, published] of Object.entries(existingVersions)) {
|
|
216
|
+
if (known === version) continue;
|
|
217
|
+
if (recipeTags.some((entry) => entry.version === known)) continue;
|
|
218
|
+
problems.push({
|
|
219
|
+
level: "error",
|
|
220
|
+
where: `${INDEX_FILENAME} recipes['${key}'].versions['${known}']`,
|
|
221
|
+
message:
|
|
222
|
+
`the index publishes version ${known}, but the tag '${published.tag}' does not ` +
|
|
223
|
+
`exist in this repository. A missing tag must never hide a published version; ` +
|
|
224
|
+
`restore the tag, or remove the version from the index.`,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (hasTag) {
|
|
229
|
+
problems.push(
|
|
230
|
+
...(await checkTaggedMetadata(rootDir, tagName, recipe.path, manifestName, version, where, run))
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const taggedHash = await hashTaggedRecipe(rootDir, tagName, recipe.path, run);
|
|
234
|
+
const published = existingVersions[version];
|
|
235
|
+
|
|
236
|
+
if (published !== undefined && published.hash !== taggedHash) {
|
|
237
|
+
problems.push({
|
|
238
|
+
level: "error",
|
|
239
|
+
where: `${INDEX_FILENAME} recipes['${key}'].versions['${version}']`,
|
|
240
|
+
message:
|
|
241
|
+
`the index records a different content hash from the one the tag '${tagName}' ` +
|
|
242
|
+
`now carries. A published version never changes, so either the tag was moved ` +
|
|
243
|
+
`or the index was edited; restore whichever is wrong before releasing again.`,
|
|
244
|
+
});
|
|
245
|
+
} else {
|
|
246
|
+
// A release records a version before its tag exists, dating it from the
|
|
247
|
+
// run; the tag that follows carries a date of its own. Keeping the date
|
|
248
|
+
// the index already published is what stops the very next regeneration
|
|
249
|
+
// from calling a perfectly current index stale.
|
|
250
|
+
versions[version] = {
|
|
251
|
+
hash: taggedHash,
|
|
252
|
+
tag: tagName,
|
|
253
|
+
prerelease: semver.prerelease(version) !== null,
|
|
254
|
+
...(published?.releasedAt === undefined
|
|
255
|
+
? await releasedAtOf(rootDir, tagName, run)
|
|
256
|
+
: { releasedAt: published.releasedAt }),
|
|
257
|
+
...(dependencies === undefined ? {} : { dependencies }),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (taggedHash !== workingHash) {
|
|
262
|
+
// A recipe this run is not publishing is allowed to have unpublished
|
|
263
|
+
// changes sitting in it: that is what a scoped release leaves behind,
|
|
264
|
+
// and it is worth saying rather than stopping for.
|
|
265
|
+
const publishingThis =
|
|
266
|
+
options.publishing === undefined || Object.hasOwn(publishing, key);
|
|
267
|
+
problems.push({
|
|
268
|
+
level: publishingThis ? "error" : "warning",
|
|
269
|
+
where,
|
|
270
|
+
message: publishingThis
|
|
271
|
+
? `version ${version} was already published as the tag '${tagName}', and the ` +
|
|
272
|
+
`files in this folder no longer match what that tag carries. A published ` +
|
|
273
|
+
`version never changes: bump the version in this manifest, or with ` +
|
|
274
|
+
`'sous repo release --bump patch'.`
|
|
275
|
+
: `version ${version} is published as the tag '${tagName}', and this folder has ` +
|
|
276
|
+
`changed since. Those changes are outside this release, so they stay ` +
|
|
277
|
+
`unpublished; release '${key}' to publish them.`,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
} else {
|
|
281
|
+
// No tag yet. The version is either one this run is about to cut, or one
|
|
282
|
+
// a release commit already recorded and CI will cut after the merge; in
|
|
283
|
+
// both cases the index describes it from the working tree, and the tag
|
|
284
|
+
// follows on the very commit that carries this index.
|
|
285
|
+
const published = existingVersions[version];
|
|
286
|
+
if (options.publishing?.[key] === version || published !== undefined) {
|
|
287
|
+
versions[version] = {
|
|
288
|
+
hash: workingHash,
|
|
289
|
+
tag: tagName,
|
|
290
|
+
prerelease: semver.prerelease(version) !== null,
|
|
291
|
+
releasedAt: published?.releasedAt ?? now.toISOString(),
|
|
292
|
+
...(dependencies === undefined ? {} : { dependencies }),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
pending.push({
|
|
297
|
+
key,
|
|
298
|
+
path: recipe.path,
|
|
299
|
+
version,
|
|
300
|
+
tag: tagName,
|
|
301
|
+
hash: workingHash,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (Object.keys(versions).length > 0) {
|
|
306
|
+
const entry: IndexRecipe = { path: recipe.path, versions };
|
|
307
|
+
if (recipe.manifest.description !== undefined) {
|
|
308
|
+
entry.description = recipe.manifest.description;
|
|
309
|
+
}
|
|
310
|
+
recipes[key] = entry;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
problems.push(...checkOrphanTags(validation, tags));
|
|
315
|
+
|
|
316
|
+
const index: IndexFile = {
|
|
317
|
+
formatVersion: 1,
|
|
318
|
+
name: validation.manifest.name,
|
|
319
|
+
generatedAt: now.toISOString(),
|
|
320
|
+
generator: sousVersion,
|
|
321
|
+
namespaces,
|
|
322
|
+
recipes,
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// The stamp changes on every run, so it is only allowed to change the file
|
|
326
|
+
// when something else did too; otherwise `--check` would call a perfectly
|
|
327
|
+
// current index stale every time it ran.
|
|
328
|
+
if (existing !== undefined && sameExceptStamp(existing, index)) {
|
|
329
|
+
index.generatedAt = existing.generatedAt;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const text = stringifyIndexFile(index);
|
|
333
|
+
const stale = existing === undefined || stringifyIndexFile(existing) !== text;
|
|
334
|
+
|
|
335
|
+
return { index, text, stale, pending, problems, tags };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Says, in plain language, how a committed index differs from a regenerated
|
|
340
|
+
* one. This is what `--check` prints when it refuses: an author needs to know
|
|
341
|
+
* which recipe and which version is out of step, not that two files differ.
|
|
342
|
+
*
|
|
343
|
+
* @param existing - The committed index, or undefined when there is none.
|
|
344
|
+
* @param rebuilt - The regenerated index.
|
|
345
|
+
*/
|
|
346
|
+
export function describeIndexDrift(
|
|
347
|
+
existing: IndexFile | undefined,
|
|
348
|
+
rebuilt: IndexFile
|
|
349
|
+
): string[] {
|
|
350
|
+
if (existing === undefined) {
|
|
351
|
+
return [`there is no ${INDEX_FILENAME} in this repository yet`];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const lines: string[] = [];
|
|
355
|
+
|
|
356
|
+
if (existing.name !== rebuilt.name) {
|
|
357
|
+
lines.push(
|
|
358
|
+
`the index calls this repository '${existing.name}', and its manifest calls it ` +
|
|
359
|
+
`'${rebuilt.name}'`
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
if (existing.generator !== rebuilt.generator) {
|
|
363
|
+
lines.push(
|
|
364
|
+
`the index was generated by sous ${existing.generator}, and this is sous ` +
|
|
365
|
+
`${rebuilt.generator}`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
for (const name of Object.keys(rebuilt.namespaces)) {
|
|
370
|
+
if (!Object.hasOwn(existing.namespaces, name)) {
|
|
371
|
+
lines.push(`the namespace '${name}' is missing from the index`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
for (const name of Object.keys(existing.namespaces)) {
|
|
375
|
+
if (!Object.hasOwn(rebuilt.namespaces, name)) {
|
|
376
|
+
lines.push(`the index still declares the namespace '${name}'`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
for (const [key, recipe] of Object.entries(rebuilt.recipes)) {
|
|
381
|
+
const before = existing.recipes[key];
|
|
382
|
+
if (before === undefined) {
|
|
383
|
+
lines.push(`the recipe '${key}' is missing from the index`);
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (before.path !== recipe.path) {
|
|
387
|
+
lines.push(
|
|
388
|
+
`the recipe '${key}' is listed at '${before.path}' in the index and at ` +
|
|
389
|
+
`'${recipe.path}' in the repository`
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
if (before.description !== recipe.description) {
|
|
393
|
+
lines.push(`the description of '${key}' has changed since the index was written`);
|
|
394
|
+
}
|
|
395
|
+
for (const version of Object.keys(recipe.versions)) {
|
|
396
|
+
if (!Object.hasOwn(before.versions, version)) {
|
|
397
|
+
lines.push(`version ${version} of '${key}' is missing from the index`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
for (const [key, recipe] of Object.entries(existing.recipes)) {
|
|
403
|
+
if (!Object.hasOwn(rebuilt.recipes, key)) {
|
|
404
|
+
lines.push(`the index still publishes '${key}', which this repository does not`);
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
for (const version of Object.keys(recipe.versions)) {
|
|
408
|
+
if (!Object.hasOwn(rebuilt.recipes[key]!.versions, version)) {
|
|
409
|
+
lines.push(`the index still publishes version ${version} of '${key}'`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (lines.length === 0) {
|
|
415
|
+
lines.push("the index differs from what this repository describes");
|
|
416
|
+
}
|
|
417
|
+
return lines;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// --- Internals ----------------------------------------------------------------------------------
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Turns one recipe's declared dependencies into the exact versions its index
|
|
424
|
+
* entry publishes.
|
|
425
|
+
*
|
|
426
|
+
* This is the whole point of recording dependencies in the index: a consumer
|
|
427
|
+
* installing version 1.4.0 of a recipe installs what 1.4.0 was released
|
|
428
|
+
* against, not whatever its ranges happen to reach months later.
|
|
429
|
+
*
|
|
430
|
+
* - A SIBLING with no range means "the version released alongside me", which
|
|
431
|
+
* is the version this run settles that sibling on.
|
|
432
|
+
* - A SIBLING with a range resolves against the versions this repository
|
|
433
|
+
* publishes, including the ones this run is adding.
|
|
434
|
+
* - A CROSS-REPOSITORY dependency carries the identity of the repository it
|
|
435
|
+
* lives in, which is what a consumer needs in order to add that repository
|
|
436
|
+
* and find the recipe. Its exact version belongs to that repository's own
|
|
437
|
+
* index, so the range the manifest declared is recorded instead.
|
|
438
|
+
*
|
|
439
|
+
* @param recipe - The recipe whose dependencies are being resolved.
|
|
440
|
+
* @param validation - The validated repository, for expanding namespace refs.
|
|
441
|
+
* @param siblingVersions - What each recipe here is published at after this run.
|
|
442
|
+
* @param publishedVersions - Every version each recipe here publishes.
|
|
443
|
+
*/
|
|
444
|
+
function resolveIndexDependencies(
|
|
445
|
+
recipe: ValidatedRecipe,
|
|
446
|
+
validation: RepoValidation,
|
|
447
|
+
siblingVersions: Map<string, string>,
|
|
448
|
+
publishedVersions: Map<string, string[]>
|
|
449
|
+
): Record<string, IndexDependency> | undefined {
|
|
450
|
+
const declared = [
|
|
451
|
+
...(recipe.manifest.depends ?? []),
|
|
452
|
+
...(recipe.manifest.subscribes ?? []),
|
|
453
|
+
];
|
|
454
|
+
if (declared.length === 0) return undefined;
|
|
455
|
+
|
|
456
|
+
const resolved: Record<string, IndexDependency> = {};
|
|
457
|
+
|
|
458
|
+
/** Records one sibling, at the version this run settles it on. */
|
|
459
|
+
const recordSibling = (key: string, range: string | undefined): void => {
|
|
460
|
+
if (range === undefined) {
|
|
461
|
+
const settled = siblingVersions.get(key);
|
|
462
|
+
resolved[key] = settled === undefined ? { range: "*" } : { version: settled };
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const best = semver.maxSatisfying(publishedVersions.get(key) ?? [], range);
|
|
466
|
+
resolved[key] = best === null ? { range } : { version: best };
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
for (const written of declared) {
|
|
470
|
+
let parsed;
|
|
471
|
+
try {
|
|
472
|
+
parsed = parseDependencyRef(written);
|
|
473
|
+
} catch {
|
|
474
|
+
// A dependency that does not parse is already reported by validation.
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (parsed.kind === "remote") {
|
|
479
|
+
resolved[`${parsed.namespace}/${parsed.recipe}`] = {
|
|
480
|
+
repo: parsed.canonicalRepo!,
|
|
481
|
+
range: parsed.range ?? "*",
|
|
482
|
+
};
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (parsed.recipe !== undefined) {
|
|
487
|
+
recordSibling(`${parsed.namespace}/${parsed.recipe}`, parsed.range);
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// A whole-namespace dependency means every recipe in that namespace.
|
|
492
|
+
for (const entry of validation.recipes) {
|
|
493
|
+
if (entry.manifest.namespace !== parsed.namespace) continue;
|
|
494
|
+
if (entry.key === recipe.key) continue;
|
|
495
|
+
recordSibling(entry.key, undefined);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return Object.keys(resolved).length === 0 ? undefined : resolved;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** Groups release tags by the recipe key they belong to. */
|
|
503
|
+
function groupTagsByKey(tags: ReadonlyArray<RecipeTag>): Map<string, RecipeTag[]> {
|
|
504
|
+
const grouped = new Map<string, RecipeTag[]>();
|
|
505
|
+
for (const tag of tags) {
|
|
506
|
+
const key = recipeTagKey(tag);
|
|
507
|
+
const list = grouped.get(key);
|
|
508
|
+
if (list === undefined) grouped.set(key, [tag]);
|
|
509
|
+
else list.push(tag);
|
|
510
|
+
}
|
|
511
|
+
return grouped;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** The content hash of a recipe folder as it stood at a tag. */
|
|
515
|
+
async function hashTaggedRecipe(
|
|
516
|
+
rootDir: string,
|
|
517
|
+
tag: string,
|
|
518
|
+
recipePath: string,
|
|
519
|
+
run: RunOptions["run"]
|
|
520
|
+
): Promise<string> {
|
|
521
|
+
return withTaggedTree(rootDir, tag, recipePath, (dir) => hashDirectory(dir), { run });
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** The `releasedAt` field for a tag, or nothing when git cannot date it. */
|
|
525
|
+
async function releasedAtOf(
|
|
526
|
+
rootDir: string,
|
|
527
|
+
tag: string,
|
|
528
|
+
run: RunOptions["run"]
|
|
529
|
+
): Promise<{ releasedAt?: string }> {
|
|
530
|
+
const date = await tagCommitDate(rootDir, tag, { run });
|
|
531
|
+
return date === undefined ? {} : { releasedAt: date };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Confirms the recipe manifest carried by a tag declares the version the tag
|
|
536
|
+
* names. A tag whose metadata says something else is the exact failure the
|
|
537
|
+
* design forbids: a version hidden behind a tag that disagrees with it.
|
|
538
|
+
*/
|
|
539
|
+
async function checkTaggedMetadata(
|
|
540
|
+
rootDir: string,
|
|
541
|
+
tag: string,
|
|
542
|
+
recipePath: string,
|
|
543
|
+
manifestName: string,
|
|
544
|
+
version: string,
|
|
545
|
+
where: string,
|
|
546
|
+
run: RunOptions["run"]
|
|
547
|
+
): Promise<ValidationProblem[]> {
|
|
548
|
+
const text = await readFileAtTag(rootDir, tag, `${recipePath}/${manifestName}`, { run });
|
|
549
|
+
if (text === undefined) {
|
|
550
|
+
return [
|
|
551
|
+
{
|
|
552
|
+
level: "error",
|
|
553
|
+
where,
|
|
554
|
+
message:
|
|
555
|
+
`the tag '${tag}' does not carry a '${manifestName}' at '${recipePath}'. The tag ` +
|
|
556
|
+
`and the recipe metadata must agree about what was published.`,
|
|
557
|
+
},
|
|
558
|
+
];
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
let taggedVersion: string | undefined;
|
|
562
|
+
try {
|
|
563
|
+
const raw = manifestName.endsWith(".json")
|
|
564
|
+
? parseJsoncText(text, `${tag}:${recipePath}/${manifestName}`)
|
|
565
|
+
: parseYamlText(text, `${tag}:${recipePath}/${manifestName}`);
|
|
566
|
+
taggedVersion = parseRecipeManifest(raw, `${tag}:${recipePath}/${manifestName}`).version;
|
|
567
|
+
} catch (error) {
|
|
568
|
+
return [
|
|
569
|
+
{
|
|
570
|
+
level: "warning",
|
|
571
|
+
where,
|
|
572
|
+
message:
|
|
573
|
+
`the recipe manifest at the tag '${tag}' could not be read, so sous cannot confirm ` +
|
|
574
|
+
`the tag and the metadata agree.\n ${describeError(error)}`,
|
|
575
|
+
},
|
|
576
|
+
];
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (taggedVersion !== version) {
|
|
580
|
+
return [
|
|
581
|
+
{
|
|
582
|
+
level: "error",
|
|
583
|
+
where,
|
|
584
|
+
message:
|
|
585
|
+
`the tag '${tag}' carries a manifest declaring version ${taggedVersion}, not ` +
|
|
586
|
+
`${version}. Recipe metadata is the source of truth for versions, so the tag is ` +
|
|
587
|
+
`wrong; delete it and let 'sous repo release' cut it again.`,
|
|
588
|
+
},
|
|
589
|
+
];
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return [];
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Reports release tags that belong to no recipe this repository publishes any
|
|
597
|
+
* more. That is ordinary history after a recipe is renamed or retired, so it is
|
|
598
|
+
* a warning: it is worth saying, and it is not worth stopping for.
|
|
599
|
+
*/
|
|
600
|
+
function checkOrphanTags(
|
|
601
|
+
validation: RepoValidation,
|
|
602
|
+
tags: ReadonlyArray<RecipeTag>
|
|
603
|
+
): ValidationProblem[] {
|
|
604
|
+
const known = new Set(validation.recipes.map((recipe) => recipe.key));
|
|
605
|
+
const orphaned = new Map<string, string[]>();
|
|
606
|
+
|
|
607
|
+
for (const tag of tags) {
|
|
608
|
+
const key = recipeTagKey(tag);
|
|
609
|
+
if (known.has(key)) continue;
|
|
610
|
+
const list = orphaned.get(key);
|
|
611
|
+
if (list === undefined) orphaned.set(key, [tag.tag]);
|
|
612
|
+
else list.push(tag.tag);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return [...orphaned.entries()].map(([key, tagNames]) => ({
|
|
616
|
+
level: "warning" as const,
|
|
617
|
+
where: `tags for '${key}'`,
|
|
618
|
+
message:
|
|
619
|
+
`this repository carries ${tagNames.length} release ${
|
|
620
|
+
tagNames.length === 1 ? "tag" : "tags"
|
|
621
|
+
} for '${key}' (${tagNames.join(", ")}), which it no longer publishes. That is normal ` +
|
|
622
|
+
`after a recipe is renamed or retired; the tags are left alone.`,
|
|
623
|
+
}));
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/** True when two indexes differ only in when they were generated. */
|
|
627
|
+
function sameExceptStamp(left: IndexFile, right: IndexFile): boolean {
|
|
628
|
+
const strip = (index: IndexFile) => stringifyIndexFile({ ...index, generatedAt: "" });
|
|
629
|
+
return strip(left) === strip(right);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** Renders an absolute path as a repository-relative one, with forward slashes. */
|
|
633
|
+
function relativeTo(rootDir: string, target: string): string {
|
|
634
|
+
return path.relative(rootDir, target).split(path.sep).join("/");
|
|
635
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The publish side of the Repositories layer: everything `sous repo release`
|
|
3
|
+
* and `sous repo submit` are built from.
|
|
4
|
+
*
|
|
5
|
+
* Import from here rather than from the individual modules, so a caller has one
|
|
6
|
+
* place to look for validation, tags, working-tree state, index regeneration
|
|
7
|
+
* and version bumping.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export * from "./validate.js";
|
|
11
|
+
export * from "./tags.js";
|
|
12
|
+
export * from "./git-state.js";
|
|
13
|
+
export * from "./index-builder.js";
|
|
14
|
+
export * from "./plan.js";
|
|
15
|
+
export * from "./bump.js";
|
|
16
|
+
export * from "./submit-service.js";
|