@sous-io/sous 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -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 +408 -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 +73 -9
- 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 +619 -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 +413 -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/bin/xcv +0 -5
- 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,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The index cache.
|
|
3
|
+
*
|
|
4
|
+
* Adding a repository fetches exactly one file, its `sous.index.json`, and that
|
|
5
|
+
* file is all sous needs to resolve a ref, enumerate versions and decide what to
|
|
6
|
+
* download. The cache keeps one copy per repository under
|
|
7
|
+
* `<storeRoot>/_indexes/`, beside a small sidecar recording when it was fetched
|
|
8
|
+
* and what the host called that version of it.
|
|
9
|
+
*
|
|
10
|
+
* The rule the whole Repositories system follows applies here too: a failed
|
|
11
|
+
* upstream check never breaks a build. When a refetch fails and a cached copy
|
|
12
|
+
* exists, the cached copy is used and the failure is reported as a warning.
|
|
13
|
+
*
|
|
14
|
+
* One thing the cache can do beyond storing and serving: an OVERLAY. Sous ships
|
|
15
|
+
* a copy of the core recipe inside its own package, and that copy has to be
|
|
16
|
+
* resolvable even when the repository publishing it has not published that
|
|
17
|
+
* version yet. An overlay folds such a fact into every copy of an index the
|
|
18
|
+
* cache hands out, and never into the file on disk, so the cached copy stays an
|
|
19
|
+
* honest record of what the repository actually served.
|
|
20
|
+
*
|
|
21
|
+
* The cache takes the store's root directory as a plain string, so it does not
|
|
22
|
+
* depend on the store implementation at all.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import fs from "node:fs";
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
import { parseIndexFile, type IndexFile } from "../formats/index-file.js";
|
|
28
|
+
import { INDEX_CACHE_DIRNAME, stableJsonStringify } from "../formats/common.js";
|
|
29
|
+
import { identitySegments } from "../identity.js";
|
|
30
|
+
import { ConfigError, isConfigError } from "../../errors.js";
|
|
31
|
+
import { warning } from "../../../utils/formatting.js";
|
|
32
|
+
// The freshness window has one definition, and it lives with the rest of the
|
|
33
|
+
// freshness rules; that module only borrows a type from here, so nothing loads
|
|
34
|
+
// in a circle at run time.
|
|
35
|
+
import { DEFAULT_FRESHNESS_SECONDS } from "../store/settings.js";
|
|
36
|
+
import type { ProviderOptions, RepoProvider } from "./provider.js";
|
|
37
|
+
import { ensureIndexCacheDirectory } from "../../../utils/sous-directory.js";
|
|
38
|
+
|
|
39
|
+
export { INDEX_CACHE_DIRNAME };
|
|
40
|
+
|
|
41
|
+
/** The suffix of the sidecar written beside each cached index. */
|
|
42
|
+
export const INDEX_SIDECAR_SUFFIX = ".meta.json";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* What sous remembers about a cached index. Written as JSON beside the index
|
|
46
|
+
* itself; a missing or unreadable sidecar simply means "no idea", never an
|
|
47
|
+
* error.
|
|
48
|
+
*/
|
|
49
|
+
export type IndexMeta = {
|
|
50
|
+
/** When the cached copy was fetched. */
|
|
51
|
+
fetchedAt: string;
|
|
52
|
+
/** The entity tag the host sent for that copy, when it sent one. */
|
|
53
|
+
etag?: string;
|
|
54
|
+
/** The git ref it was read at. */
|
|
55
|
+
ref?: string;
|
|
56
|
+
/** When sous last asked upstream whether there was anything newer. */
|
|
57
|
+
lastCheckedAt?: string;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** Where a returned index came from. */
|
|
61
|
+
export type IndexSource = "cache" | "network" | "stale";
|
|
62
|
+
|
|
63
|
+
/** What a cache lookup returns. */
|
|
64
|
+
export type IndexLookup = {
|
|
65
|
+
/** The validated index. */
|
|
66
|
+
index: IndexFile;
|
|
67
|
+
/** Whether it came from the cache, from the network, or from a stale fallback. */
|
|
68
|
+
source: IndexSource;
|
|
69
|
+
/** What sous remembers about the copy that was returned. */
|
|
70
|
+
meta: IndexMeta;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A fact sous knows about a repository that its published index does not carry,
|
|
75
|
+
* folded into every copy of that index the cache returns.
|
|
76
|
+
*
|
|
77
|
+
* An overlay must be pure: it is handed an index and returns one, and it never
|
|
78
|
+
* touches the cache's files. It is called for every repository, so it decides
|
|
79
|
+
* for itself which identity it applies to and returns the index unchanged for
|
|
80
|
+
* all the others.
|
|
81
|
+
*/
|
|
82
|
+
export type IndexOverlay = (identity: string, index: IndexFile) => IndexFile;
|
|
83
|
+
|
|
84
|
+
/** Which repository to fetch, and how fresh the answer has to be. */
|
|
85
|
+
export type GetIndexOptions = {
|
|
86
|
+
/** The repository URL, used when a fetch is needed. */
|
|
87
|
+
url: string;
|
|
88
|
+
/** The provider the repository entry names, when it names one. */
|
|
89
|
+
provider?: string;
|
|
90
|
+
/** How old a cached copy may be, in seconds. Defaults to five minutes. */
|
|
91
|
+
maxAgeSeconds?: number;
|
|
92
|
+
/** When true, refetch regardless of how fresh the cached copy is. */
|
|
93
|
+
force?: boolean;
|
|
94
|
+
/**
|
|
95
|
+
* What to call the repository in a message. A project's short name for it is
|
|
96
|
+
* what the person reading wrote in their own config, so that is what they are
|
|
97
|
+
* shown; the identity is used when nothing supplies one.
|
|
98
|
+
*/
|
|
99
|
+
label?: string;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/** How the cache is built. */
|
|
103
|
+
export type IndexCacheOptions = {
|
|
104
|
+
/** The store's root directory; the cache lives in a subdirectory of it. */
|
|
105
|
+
storeRoot: string;
|
|
106
|
+
/**
|
|
107
|
+
* How a provider is chosen for a repository URL. Defaults to the built-in
|
|
108
|
+
* providers; injected in tests, and by anyone with a provider list of their
|
|
109
|
+
* own.
|
|
110
|
+
*/
|
|
111
|
+
resolveProvider: (url: string, providerId?: string) => RepoProvider;
|
|
112
|
+
/** Options handed to every provider call (environment, fetch, subprocess runner). */
|
|
113
|
+
providerOptions?: ProviderOptions;
|
|
114
|
+
/** The clock, so tests can decide what "now" means. */
|
|
115
|
+
now?: () => Date;
|
|
116
|
+
/** Where warnings go. Defaults to the console warning banner. */
|
|
117
|
+
warn?: (message: string) => void;
|
|
118
|
+
/**
|
|
119
|
+
* A fact to fold into every index this cache returns, without ever writing it
|
|
120
|
+
* to disk. See `IndexOverlay`; `setOverlay` installs one later.
|
|
121
|
+
*/
|
|
122
|
+
overlay?: IndexOverlay;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/** One cached repository index, plus the machinery to keep it current. */
|
|
126
|
+
export class IndexCache {
|
|
127
|
+
private readonly storeRoot: string;
|
|
128
|
+
|
|
129
|
+
private readonly resolveProvider: (url: string, providerId?: string) => RepoProvider;
|
|
130
|
+
|
|
131
|
+
private readonly providerOptions: ProviderOptions;
|
|
132
|
+
|
|
133
|
+
private readonly now: () => Date;
|
|
134
|
+
|
|
135
|
+
private readonly warn: (message: string) => void;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* What sous itself knows about a repository, over and above what the
|
|
139
|
+
* repository published. Not readonly: the packaged core recipe's hash is only
|
|
140
|
+
* known once the store has been seeded, which happens after the cache exists.
|
|
141
|
+
*/
|
|
142
|
+
private overlay: IndexOverlay | undefined;
|
|
143
|
+
|
|
144
|
+
constructor(options: IndexCacheOptions) {
|
|
145
|
+
this.storeRoot = options.storeRoot;
|
|
146
|
+
this.resolveProvider = options.resolveProvider;
|
|
147
|
+
this.providerOptions = options.providerOptions ?? {};
|
|
148
|
+
this.now = options.now ?? (() => new Date());
|
|
149
|
+
this.warn = options.warn ?? warning;
|
|
150
|
+
this.overlay = options.overlay;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Installs the overlay folded into every index this cache returns from now
|
|
155
|
+
* on. Nothing already written to disk changes, and nothing written later
|
|
156
|
+
* carries it.
|
|
157
|
+
*
|
|
158
|
+
* @param overlay - The overlay to apply, or undefined to apply none.
|
|
159
|
+
*/
|
|
160
|
+
setOverlay(overlay: IndexOverlay | undefined): void {
|
|
161
|
+
this.overlay = overlay;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Folds the installed overlay, if there is one, into an index on its way out.
|
|
166
|
+
* An overlay that throws is ignored and warned about: a fact sous was trying
|
|
167
|
+
* to add is never worth losing the index the repository really published.
|
|
168
|
+
*
|
|
169
|
+
* @param identity - The repository's canonical identity.
|
|
170
|
+
* @param index - The index as it was read or fetched.
|
|
171
|
+
*/
|
|
172
|
+
private applyOverlay(identity: string, index: IndexFile): IndexFile {
|
|
173
|
+
if (this.overlay === undefined) return index;
|
|
174
|
+
try {
|
|
175
|
+
return this.overlay(identity, index);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
this.warn(
|
|
178
|
+
`Sous could not add what it knows about the repository '${identity}' to that ` +
|
|
179
|
+
`repository's index, so only what the repository published is available.\n` +
|
|
180
|
+
`${error instanceof Error ? error.message : String(error)}`
|
|
181
|
+
);
|
|
182
|
+
return index;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** The directory cached indexes live in. */
|
|
187
|
+
get directory(): string {
|
|
188
|
+
return path.join(this.storeRoot, INDEX_CACHE_DIRNAME);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Where a repository's cached index is written. The identity is several
|
|
193
|
+
* segments, so it becomes several directories, exactly as it does in the
|
|
194
|
+
* store: `_indexes/github.com/sous-io/sous-recipes.json`.
|
|
195
|
+
*
|
|
196
|
+
* @param identity - The repository's canonical identity.
|
|
197
|
+
*/
|
|
198
|
+
indexPath(identity: string): string {
|
|
199
|
+
const segments = identitySegments(identity);
|
|
200
|
+
const last = segments.pop() ?? identity;
|
|
201
|
+
return path.join(this.directory, ...segments, `${last}.json`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Where a repository's index sidecar is written, beside its index.
|
|
206
|
+
*
|
|
207
|
+
* @param identity - The repository's canonical identity.
|
|
208
|
+
*/
|
|
209
|
+
sidecarPath(identity: string): string {
|
|
210
|
+
const segments = identitySegments(identity);
|
|
211
|
+
const last = segments.pop() ?? identity;
|
|
212
|
+
return path.join(this.directory, ...segments, `${last}${INDEX_SIDECAR_SUFFIX}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Reads what sous remembers about a cached index. Returns undefined when
|
|
217
|
+
* there is no sidecar or it cannot be read; the sidecar is a convenience, and
|
|
218
|
+
* losing it only costs a refetch.
|
|
219
|
+
*
|
|
220
|
+
* @param identity - The repository's canonical identity.
|
|
221
|
+
*/
|
|
222
|
+
readMeta(identity: string): IndexMeta | undefined {
|
|
223
|
+
try {
|
|
224
|
+
const raw = JSON.parse(fs.readFileSync(this.sidecarPath(identity), "utf8")) as IndexMeta;
|
|
225
|
+
if (typeof raw?.fetchedAt !== "string") return undefined;
|
|
226
|
+
return raw;
|
|
227
|
+
} catch {
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Writes what sous remembers about a cached index.
|
|
234
|
+
*
|
|
235
|
+
* @param identity - The repository's canonical identity.
|
|
236
|
+
* @param meta - What to record.
|
|
237
|
+
*/
|
|
238
|
+
writeMeta(identity: string, meta: IndexMeta): void {
|
|
239
|
+
const file = this.sidecarPath(identity);
|
|
240
|
+
this.prepareDirectoryFor(file);
|
|
241
|
+
fs.writeFileSync(file, stableJsonStringify(meta), "utf8");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Makes sure the cache directory exists and explains itself, and that the
|
|
246
|
+
* subdirectories an identity turns into are there too.
|
|
247
|
+
*
|
|
248
|
+
* @param filePath - The file about to be written.
|
|
249
|
+
*/
|
|
250
|
+
private prepareDirectoryFor(filePath: string): void {
|
|
251
|
+
ensureIndexCacheDirectory(this.directory);
|
|
252
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Reads and validates the cached index, or undefined when there is none. A
|
|
257
|
+
* cached file that no longer parses is treated as absent, since it is only a
|
|
258
|
+
* copy of something upstream still has. Any installed overlay is folded into
|
|
259
|
+
* the answer; the file itself is left exactly as it was cached.
|
|
260
|
+
*
|
|
261
|
+
* @param identity - The repository's canonical identity.
|
|
262
|
+
*/
|
|
263
|
+
readCached(identity: string): IndexFile | undefined {
|
|
264
|
+
const file = this.indexPath(identity);
|
|
265
|
+
let text: string;
|
|
266
|
+
try {
|
|
267
|
+
text = fs.readFileSync(file, "utf8");
|
|
268
|
+
} catch {
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
return this.applyOverlay(identity, parseIndexFile(JSON.parse(text), file));
|
|
273
|
+
} catch {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* True when the cached copy is younger than the given window.
|
|
280
|
+
*
|
|
281
|
+
* @param meta - What sous remembers about the cached copy.
|
|
282
|
+
* @param maxAgeSeconds - How old the copy may be.
|
|
283
|
+
*/
|
|
284
|
+
isFresh(meta: IndexMeta | undefined, maxAgeSeconds: number): boolean {
|
|
285
|
+
if (meta === undefined) return false;
|
|
286
|
+
const fetched = Date.parse(meta.fetchedAt);
|
|
287
|
+
if (Number.isNaN(fetched)) return false;
|
|
288
|
+
const ageSeconds = (this.now().getTime() - fetched) / 1000;
|
|
289
|
+
return ageSeconds >= 0 && ageSeconds < maxAgeSeconds;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Returns a repository's index: the cached copy while it is fresh, otherwise a
|
|
294
|
+
* fresh fetch through the provider. When the fetch fails and a cached copy
|
|
295
|
+
* exists, the cached copy is returned and the failure is warned about; when
|
|
296
|
+
* there is no cached copy, the failure is raised.
|
|
297
|
+
*
|
|
298
|
+
* @param identity - The repository's canonical identity.
|
|
299
|
+
* @param options - The repository URL, its provider, and the freshness window.
|
|
300
|
+
*/
|
|
301
|
+
async getIndex(identity: string, options: GetIndexOptions): Promise<IndexLookup> {
|
|
302
|
+
const maxAgeSeconds = options.maxAgeSeconds ?? DEFAULT_FRESHNESS_SECONDS;
|
|
303
|
+
const meta = this.readMeta(identity);
|
|
304
|
+
|
|
305
|
+
if (options.force !== true && this.isFresh(meta, maxAgeSeconds)) {
|
|
306
|
+
const cached = this.readCached(identity);
|
|
307
|
+
if (cached !== undefined) return { index: cached, source: "cache", meta: meta! };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
return await this.refresh(identity, options);
|
|
312
|
+
} catch (error) {
|
|
313
|
+
const cached = this.readCached(identity);
|
|
314
|
+
if (cached === undefined) throw error;
|
|
315
|
+
|
|
316
|
+
const reason = isConfigError(error)
|
|
317
|
+
? (error as ConfigError).message
|
|
318
|
+
: (error as Error).message;
|
|
319
|
+
this.warn(
|
|
320
|
+
`Sous could not check the repository '${options.label ?? identity}' for updates, ` +
|
|
321
|
+
`so it is using the copy of its index that it already had.\n${reason}`
|
|
322
|
+
);
|
|
323
|
+
const stale: IndexMeta = meta ?? { fetchedAt: new Date(0).toISOString() };
|
|
324
|
+
return { index: cached, source: "stale", meta: stale };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Fetches a repository's index through its provider, validates it, and writes
|
|
330
|
+
* both the index and its sidecar. Raises rather than falling back; `getIndex`
|
|
331
|
+
* is where the last-good behavior lives.
|
|
332
|
+
*
|
|
333
|
+
* @param identity - The repository's canonical identity.
|
|
334
|
+
* @param options - The repository URL and its provider.
|
|
335
|
+
*/
|
|
336
|
+
async refresh(identity: string, options: GetIndexOptions): Promise<IndexLookup> {
|
|
337
|
+
const provider = this.resolveProvider(options.url, options.provider);
|
|
338
|
+
const repo = provider.canonicalize(options.url);
|
|
339
|
+
const fetched = await provider.fetchIndex(repo, this.providerOptions);
|
|
340
|
+
|
|
341
|
+
let parsed: unknown;
|
|
342
|
+
try {
|
|
343
|
+
parsed = JSON.parse(fetched.text);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
throw new ConfigError(
|
|
346
|
+
`The index that ${options.url} published is not valid JSON.\n` +
|
|
347
|
+
` ${(error as Error).message}\n` +
|
|
348
|
+
` A repository's index is written by 'sous repo release'; this one may be ` +
|
|
349
|
+
`damaged or may not be a sous repository at all.`
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const index = parseIndexFile(parsed, `${options.url} (${fetched.ref})`);
|
|
354
|
+
const timestamp = this.now().toISOString();
|
|
355
|
+
const meta: IndexMeta = {
|
|
356
|
+
fetchedAt: timestamp,
|
|
357
|
+
lastCheckedAt: timestamp,
|
|
358
|
+
ref: fetched.ref,
|
|
359
|
+
...(fetched.etag === undefined ? {} : { etag: fetched.etag }),
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const file = this.indexPath(identity);
|
|
363
|
+
this.prepareDirectoryFor(file);
|
|
364
|
+
fs.writeFileSync(file, stableJsonStringify(index), "utf8");
|
|
365
|
+
this.writeMeta(identity, meta);
|
|
366
|
+
|
|
367
|
+
// Written first, overlaid second: the file is what the repository served,
|
|
368
|
+
// and the overlay exists only in the copy handed back to the caller.
|
|
369
|
+
return { index: this.applyOverlay(identity, index), source: "network", meta };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Forgets a repository's cached index and sidecar, which is what removing a
|
|
374
|
+
* repository from a project does.
|
|
375
|
+
*
|
|
376
|
+
* @param identity - The repository's canonical identity.
|
|
377
|
+
*/
|
|
378
|
+
forget(identity: string): void {
|
|
379
|
+
fs.rmSync(this.indexPath(identity), { force: true });
|
|
380
|
+
fs.rmSync(this.sidecarPath(identity), { force: true });
|
|
381
|
+
}
|
|
382
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The provider registry.
|
|
3
|
+
*
|
|
4
|
+
* Import providers from here. This module is the one place that knows which
|
|
5
|
+
* concrete providers sous ships, so `provider.ts` stays free of them and the
|
|
6
|
+
* built-in list has exactly one definition.
|
|
7
|
+
*
|
|
8
|
+
* A new provider is one file: a class extending `ProviderBase` that implements
|
|
9
|
+
* the read path, declares the features it really answers, and overrides the
|
|
10
|
+
* write-path calls it supports. Adding it to `builtInProviders()` below is the
|
|
11
|
+
* only other change; nothing above the provider layer learns its name.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { LocalProvider } from "./local.js";
|
|
15
|
+
import { GithubProvider } from "./github.js";
|
|
16
|
+
import { GitlabProvider } from "./gitlab.js";
|
|
17
|
+
import { IndexCache, type IndexCacheOptions } from "./index-cache.js";
|
|
18
|
+
import {
|
|
19
|
+
detectProviderIn,
|
|
20
|
+
providerByIdIn,
|
|
21
|
+
requireProviderIn,
|
|
22
|
+
type RepoProvider,
|
|
23
|
+
} from "./provider.js";
|
|
24
|
+
|
|
25
|
+
// `runGit` is deliberately not re-exported here: `git-clone.ts` exports a `runGit` of its
|
|
26
|
+
// own and the repos barrel cannot carry two. Import this one from `providers/git.js` directly.
|
|
27
|
+
export { spawnCommand, tryCommand, fetchSubtree } from "./git.js";
|
|
28
|
+
export type { CommandResult, CommandRunner, RunOptions } from "./git.js";
|
|
29
|
+
export * from "./http.js";
|
|
30
|
+
export * from "./provider.js";
|
|
31
|
+
export * from "./base.js";
|
|
32
|
+
export * from "./github.js";
|
|
33
|
+
export * from "./gitlab.js";
|
|
34
|
+
export * from "./local.js";
|
|
35
|
+
export * from "./index-cache.js";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The providers sous ships, in the order they are tried. A fresh array every
|
|
39
|
+
* call, so a caller may add to it without affecting anyone else.
|
|
40
|
+
*/
|
|
41
|
+
export function builtInProviders(): RepoProvider[] {
|
|
42
|
+
// The local provider is last, and matches only a local absolute path or a
|
|
43
|
+
// `file://` URL, so it can never intercept a hosted repository's URL.
|
|
44
|
+
return [new GithubProvider(), new GitlabProvider(), new LocalProvider()];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Finds the provider that handles a repository URL, or undefined when none
|
|
49
|
+
* does. A repository entry may also name its provider outright, which is what a
|
|
50
|
+
* self-hosted instance behind an unfamiliar host name needs; see
|
|
51
|
+
* `requireProvider`.
|
|
52
|
+
*
|
|
53
|
+
* @param url - The repository URL.
|
|
54
|
+
* @param providers - The providers to consider. Defaults to the built-ins.
|
|
55
|
+
*/
|
|
56
|
+
export function detectProvider(
|
|
57
|
+
url: string,
|
|
58
|
+
providers: RepoProvider[] = builtInProviders()
|
|
59
|
+
): RepoProvider | undefined {
|
|
60
|
+
return detectProviderIn(url, providers);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Looks a provider up by its identifier, or undefined when there is none.
|
|
65
|
+
*
|
|
66
|
+
* @param id - The provider identifier from a repository entry.
|
|
67
|
+
* @param providers - The providers to consider. Defaults to the built-ins.
|
|
68
|
+
*/
|
|
69
|
+
export function providerById(
|
|
70
|
+
id: string,
|
|
71
|
+
providers: RepoProvider[] = builtInProviders()
|
|
72
|
+
): RepoProvider | undefined {
|
|
73
|
+
return providerByIdIn(id, providers);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Finds the provider for a repository entry: the one it names, otherwise the
|
|
78
|
+
* one that recognizes its URL. Raises a ConfigError when neither works.
|
|
79
|
+
*
|
|
80
|
+
* @param url - The repository URL.
|
|
81
|
+
* @param providerId - The provider named by the entry, when it named one.
|
|
82
|
+
* @param providers - The providers to consider. Defaults to the built-ins.
|
|
83
|
+
*/
|
|
84
|
+
export function requireProvider(
|
|
85
|
+
url: string,
|
|
86
|
+
providerId?: string,
|
|
87
|
+
providers: RepoProvider[] = builtInProviders()
|
|
88
|
+
): RepoProvider {
|
|
89
|
+
return requireProviderIn(url, providerId, providers);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Builds an index cache that resolves providers through the built-in list. Pass
|
|
94
|
+
* `resolveProvider` to override that, which is what tests do.
|
|
95
|
+
*
|
|
96
|
+
* @param options - The store root, and anything to override.
|
|
97
|
+
*/
|
|
98
|
+
export function createIndexCache(
|
|
99
|
+
options: Omit<IndexCacheOptions, "resolveProvider"> &
|
|
100
|
+
Partial<Pick<IndexCacheOptions, "resolveProvider">>
|
|
101
|
+
): IndexCache {
|
|
102
|
+
return new IndexCache({
|
|
103
|
+
resolveProvider: (url, providerId) => requireProvider(url, providerId),
|
|
104
|
+
...options,
|
|
105
|
+
});
|
|
106
|
+
}
|