@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,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Freshness: when sous bothers to look upstream.
|
|
3
|
+
*
|
|
4
|
+
* A build does not want to talk to the network on every run, and a lockfile
|
|
5
|
+
* means it usually has no reason to. So the rule is deliberately dull: a
|
|
6
|
+
* repository is checked when it has never been checked, when the freshness
|
|
7
|
+
* window has lapsed since the last check, or when something asks to always
|
|
8
|
+
* pull. Watch mode passes a shorter window; a one-off command can force it.
|
|
9
|
+
*
|
|
10
|
+
* The last-check time is remembered in the sidecar the index cache already
|
|
11
|
+
* writes, so there is one record per repository rather than two.
|
|
12
|
+
*
|
|
13
|
+
* A failed check NEVER breaks a build. That belongs to the index cache, which
|
|
14
|
+
* falls back to the copy it already holds; this module only decides whether to
|
|
15
|
+
* look.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import semver from "semver";
|
|
19
|
+
import type { IndexFile } from "./formats/index-file.js";
|
|
20
|
+
import type { IndexMeta } from "./providers/index-cache.js";
|
|
21
|
+
|
|
22
|
+
/** How long a check stays good when nothing says otherwise: five minutes. */
|
|
23
|
+
// The freshness default is shared with the store settings so there is one number.
|
|
24
|
+
import { DEFAULT_FRESHNESS_SECONDS } from "./store/settings.js";
|
|
25
|
+
|
|
26
|
+
/** The part of the index cache this module uses; IndexCache satisfies it. */
|
|
27
|
+
export type UpstreamCheckRecord = {
|
|
28
|
+
readMeta(repoName: string): IndexMeta | undefined;
|
|
29
|
+
writeMeta(repoName: string, meta: IndexMeta): void;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** What decides whether to look upstream. */
|
|
33
|
+
export type FreshnessInput = {
|
|
34
|
+
/** When sous last asked upstream, from the repository's sidecar. */
|
|
35
|
+
lastCheckedAt?: string;
|
|
36
|
+
/** How long a check stays good, in seconds. Defaults to five minutes. */
|
|
37
|
+
freshnessSeconds?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Whether something in play prefers a newer in-range version over the locked
|
|
40
|
+
* one. Always-pull still respects the freshness window; it changes what
|
|
41
|
+
* happens after the check, not how often the check happens.
|
|
42
|
+
*/
|
|
43
|
+
alwaysPull?: boolean;
|
|
44
|
+
/** Check regardless, which is what an explicit update command does. */
|
|
45
|
+
force?: boolean;
|
|
46
|
+
/** The clock. Defaults to now. */
|
|
47
|
+
now?: Date;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* True when sous should ask upstream whether there is anything newer.
|
|
52
|
+
*
|
|
53
|
+
* shouldCheckUpstream({ lastCheckedAt: "2026-01-01T00:00:00.000Z", freshnessSeconds: 300 })
|
|
54
|
+
* // -> false a minute later, true six minutes later
|
|
55
|
+
*
|
|
56
|
+
* @param input - The last check, the window, and the flags.
|
|
57
|
+
*/
|
|
58
|
+
export function shouldCheckUpstream(input: FreshnessInput): boolean {
|
|
59
|
+
if (input.force === true) return true;
|
|
60
|
+
if (input.lastCheckedAt === undefined) return true;
|
|
61
|
+
|
|
62
|
+
const last = Date.parse(input.lastCheckedAt);
|
|
63
|
+
if (Number.isNaN(last)) return true;
|
|
64
|
+
|
|
65
|
+
const now = (input.now ?? new Date()).getTime();
|
|
66
|
+
const windowSeconds = input.freshnessSeconds ?? DEFAULT_FRESHNESS_SECONDS;
|
|
67
|
+
if (windowSeconds <= 0) return true;
|
|
68
|
+
|
|
69
|
+
const ageSeconds = (now - last) / 1000;
|
|
70
|
+
// A last-check time in the future means a clock changed under sous; check
|
|
71
|
+
// rather than trusting an answer that cannot be right.
|
|
72
|
+
if (ageSeconds < 0) return true;
|
|
73
|
+
return ageSeconds >= windowSeconds;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Records that sous just looked upstream for a repository, whether or not the
|
|
78
|
+
* look succeeded. Recording a failed check too is what stops an unreachable
|
|
79
|
+
* host from being retried on every single build.
|
|
80
|
+
*
|
|
81
|
+
* @param record - The index cache, or anything with its sidecar methods.
|
|
82
|
+
* @param identity - The repository's canonical identity, which the cache is keyed by.
|
|
83
|
+
* @param when - The moment to record. Defaults to now.
|
|
84
|
+
*/
|
|
85
|
+
export function recordUpstreamCheck(
|
|
86
|
+
record: UpstreamCheckRecord,
|
|
87
|
+
identity: string,
|
|
88
|
+
when: Date = new Date()
|
|
89
|
+
): IndexMeta {
|
|
90
|
+
const existing = record.readMeta(identity);
|
|
91
|
+
const meta: IndexMeta = {
|
|
92
|
+
fetchedAt: existing?.fetchedAt ?? when.toISOString(),
|
|
93
|
+
...(existing?.etag === undefined ? {} : { etag: existing.etag }),
|
|
94
|
+
...(existing?.ref === undefined ? {} : { ref: existing.ref }),
|
|
95
|
+
lastCheckedAt: when.toISOString(),
|
|
96
|
+
};
|
|
97
|
+
record.writeMeta(identity, meta);
|
|
98
|
+
return meta;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** What an always-pull check found. */
|
|
102
|
+
export type NewerVersion = {
|
|
103
|
+
/** The recipe key. */
|
|
104
|
+
key: string;
|
|
105
|
+
/** The version currently locked. */
|
|
106
|
+
from: string;
|
|
107
|
+
/** The newer version that also satisfies the range. */
|
|
108
|
+
to: string;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Finds a newer in-range version of a locked recipe, or undefined when the
|
|
113
|
+
* locked one is still the best the range allows. This is the whole of what
|
|
114
|
+
* always-pull does: it re-resolves WITHIN the declared range and never widens
|
|
115
|
+
* it, so a dependency's constraint still holds.
|
|
116
|
+
*
|
|
117
|
+
* @param options - The index, the recipe, its locked version and its range.
|
|
118
|
+
*/
|
|
119
|
+
export function findNewerInRange(options: {
|
|
120
|
+
/** The repository's index. */
|
|
121
|
+
index: IndexFile;
|
|
122
|
+
/** The recipe key, `namespace/recipe`. */
|
|
123
|
+
key: string;
|
|
124
|
+
/** The version the lockfile pins. */
|
|
125
|
+
lockedVersion: string;
|
|
126
|
+
/** The range the subscription or dependency declared. Defaults to any version. */
|
|
127
|
+
range?: string;
|
|
128
|
+
/** Whether prereleases take part. */
|
|
129
|
+
prerelease?: boolean;
|
|
130
|
+
}): NewerVersion | undefined {
|
|
131
|
+
const entry = options.index.recipes[options.key];
|
|
132
|
+
if (entry === undefined) return undefined;
|
|
133
|
+
|
|
134
|
+
const prerelease = options.prerelease ?? false;
|
|
135
|
+
const candidates = Object.keys(entry.versions).filter((version) => {
|
|
136
|
+
if (!prerelease && entry.versions[version]!.prerelease === true) return false;
|
|
137
|
+
return semver.satisfies(version, options.range ?? "*", { includePrerelease: prerelease });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const best = semver.maxSatisfying(candidates, "*", { includePrerelease: prerelease });
|
|
141
|
+
if (best === null) return undefined;
|
|
142
|
+
if (semver.lte(best, options.lockedVersion)) return undefined;
|
|
143
|
+
|
|
144
|
+
return { key: options.key, from: options.lockedVersion, to: best };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The holder meaning the project subscribed to a recipe itself. */
|
|
148
|
+
const PROJECT = "project";
|
|
149
|
+
|
|
150
|
+
/** Every range a locked recipe's holders declared, or nothing when one cannot be told. */
|
|
151
|
+
export type DeclaredRangeLookup = {
|
|
152
|
+
/**
|
|
153
|
+
* The range the project's subscription declares for a recipe key, `"*"` when
|
|
154
|
+
* the subscription declares none, or undefined when the project holds nothing
|
|
155
|
+
* matching it any more.
|
|
156
|
+
*/
|
|
157
|
+
subscriptionRange(key: string): string | undefined;
|
|
158
|
+
/**
|
|
159
|
+
* The range one holding recipe's manifest declares for a dependency, `"*"`
|
|
160
|
+
* when it declares none, or undefined when its manifest cannot be read or no
|
|
161
|
+
* longer names that dependency.
|
|
162
|
+
*/
|
|
163
|
+
dependencyRange(holder: string, key: string): string | undefined;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The version range an always-pull check may move ONE locked recipe within, or
|
|
168
|
+
* undefined when sous cannot tell and therefore must not move it at all.
|
|
169
|
+
*
|
|
170
|
+
* Always-pull re-resolves within what was declared; it never widens it. What was
|
|
171
|
+
* declared depends on who holds the recipe. The project's own hold means the
|
|
172
|
+
* subscription's range, which may genuinely be any version. A hold by another
|
|
173
|
+
* recipe means the range that recipe's manifest declared in `depends`, and no
|
|
174
|
+
* subscription anywhere carries it, so falling back to `*` for such an entry
|
|
175
|
+
* would move it straight past the constraint the dependency declared. Several
|
|
176
|
+
* holders mean every one of their ranges at once: whitespace between comparator
|
|
177
|
+
* sets is AND in semver, which is exactly that.
|
|
178
|
+
*
|
|
179
|
+
* Returning undefined is the safe answer, and the entry stays where the lockfile
|
|
180
|
+
* pins it. It happens when a holder's declaration cannot be read and when the
|
|
181
|
+
* combined range is not one semver can express.
|
|
182
|
+
*
|
|
183
|
+
* @param key - The locked recipe key, `namespace/recipe`.
|
|
184
|
+
* @param requestedBy - Its lockfile holders: "project", and any recipe keys.
|
|
185
|
+
* @param lookup - How to read what each holder declared.
|
|
186
|
+
*/
|
|
187
|
+
export function effectiveRangeForHolders(
|
|
188
|
+
key: string,
|
|
189
|
+
requestedBy: readonly string[],
|
|
190
|
+
lookup: DeclaredRangeLookup
|
|
191
|
+
): string | undefined {
|
|
192
|
+
const ranges: string[] = [];
|
|
193
|
+
|
|
194
|
+
for (const holder of requestedBy) {
|
|
195
|
+
const declared =
|
|
196
|
+
holder === PROJECT ? lookup.subscriptionRange(key) : lookup.dependencyRange(holder, key);
|
|
197
|
+
if (declared === undefined) return undefined;
|
|
198
|
+
ranges.push(declared);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (ranges.length === 0) return undefined;
|
|
202
|
+
|
|
203
|
+
const constraints = [...new Set(ranges.filter((range) => range !== "*"))];
|
|
204
|
+
if (constraints.length === 0) return "*";
|
|
205
|
+
|
|
206
|
+
const combined = constraints.join(" ");
|
|
207
|
+
return semver.validRange(combined) === null ? undefined : combined;
|
|
208
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The thin git layer used by `sous repo link`.
|
|
3
|
+
*
|
|
4
|
+
* Linking a repository sometimes means cloning it, and always means asking
|
|
5
|
+
* whether a directory already on disk is the right checkout. Both jobs are done
|
|
6
|
+
* by shelling out to the user's own `git`, rather than by bundling a git
|
|
7
|
+
* implementation: the user's credentials, SSH agent, proxy settings and
|
|
8
|
+
* `insteadOf` rewrites are already configured there, and sous inheriting all of
|
|
9
|
+
* that for free is worth more than any library.
|
|
10
|
+
*
|
|
11
|
+
* Every function takes an optional `runner`, so tests can drive the whole
|
|
12
|
+
* surface without a real git or a network connection.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { spawnSync } from "node:child_process";
|
|
18
|
+
import { ConfigError } from "../errors.js";
|
|
19
|
+
|
|
20
|
+
/** What one git invocation produced. */
|
|
21
|
+
export type GitResult = {
|
|
22
|
+
/** The process exit code, or null when it was killed by a signal. */
|
|
23
|
+
status: number | null;
|
|
24
|
+
/** Everything git wrote to standard output, trimmed. */
|
|
25
|
+
stdout: string;
|
|
26
|
+
/** Everything git wrote to standard error, trimmed. */
|
|
27
|
+
stderr: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Runs one git command. Swappable so tests never need a real git binary.
|
|
32
|
+
*
|
|
33
|
+
* @param args - The arguments passed to git, without the leading "git".
|
|
34
|
+
* @param options - Where to run it.
|
|
35
|
+
*/
|
|
36
|
+
export type GitRunner = (args: string[], options: { cwd?: string }) => GitResult;
|
|
37
|
+
|
|
38
|
+
/** Options shared by every function here. */
|
|
39
|
+
export type GitOptions = {
|
|
40
|
+
/** The git runner to use. Defaults to the real `git` on PATH. */
|
|
41
|
+
runner?: GitRunner;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The default runner: invokes the real `git` on PATH and captures its output.
|
|
46
|
+
*
|
|
47
|
+
* @param args - The arguments passed to git, without the leading "git".
|
|
48
|
+
* @param options - Where to run it.
|
|
49
|
+
*/
|
|
50
|
+
export const runGit: GitRunner = (args, options = {}) => {
|
|
51
|
+
const result = spawnSync("git", args, {
|
|
52
|
+
cwd: options.cwd,
|
|
53
|
+
encoding: "utf8",
|
|
54
|
+
// A clone must never stop to ask for a password; a prompt in a
|
|
55
|
+
// non-interactive run would hang the command with no explanation.
|
|
56
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (result.error !== undefined) {
|
|
60
|
+
const reason = (result.error as NodeJS.ErrnoException).code === "ENOENT"
|
|
61
|
+
? "git is not installed, or is not on your PATH"
|
|
62
|
+
: result.error.message;
|
|
63
|
+
throw new ConfigError(
|
|
64
|
+
`Could not run git.\n` +
|
|
65
|
+
` ${reason}.\n` +
|
|
66
|
+
` sous uses your own git so that your credentials and configuration apply; ` +
|
|
67
|
+
`install git and try again.`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
status: result.status,
|
|
73
|
+
stdout: (result.stdout ?? "").trim(),
|
|
74
|
+
stderr: (result.stderr ?? "").trim(),
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Runs git and throws a ConfigError, carrying git's own message, on failure. */
|
|
79
|
+
function runGitOrThrow(
|
|
80
|
+
args: string[],
|
|
81
|
+
options: { cwd?: string; runner?: GitRunner; what: string }
|
|
82
|
+
): GitResult {
|
|
83
|
+
const runner = options.runner ?? runGit;
|
|
84
|
+
const result = runner(args, { cwd: options.cwd });
|
|
85
|
+
if (result.status !== 0) {
|
|
86
|
+
const detail = result.stderr.length > 0 ? result.stderr : result.stdout;
|
|
87
|
+
throw new ConfigError(
|
|
88
|
+
`${options.what} failed.\n` +
|
|
89
|
+
` Command: git ${args.join(" ")}\n` +
|
|
90
|
+
(detail.length > 0 ? ` git said: ${detail}\n` : "") +
|
|
91
|
+
` Fix the problem git reported, then run the command again.`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* True when the directory is inside a git working tree whose root is that same
|
|
99
|
+
* directory. A subdirectory of a checkout is deliberately not a checkout here:
|
|
100
|
+
* linking half a repository would silently produce a repo with no manifest at
|
|
101
|
+
* its root.
|
|
102
|
+
*
|
|
103
|
+
* @param directory - The directory to test.
|
|
104
|
+
* @param options - The git runner to use.
|
|
105
|
+
*/
|
|
106
|
+
export function isGitCheckout(directory: string, options: GitOptions = {}): boolean {
|
|
107
|
+
if (!directoryExists(directory)) return false;
|
|
108
|
+
|
|
109
|
+
const runner = options.runner ?? runGit;
|
|
110
|
+
const result = runner(["rev-parse", "--show-toplevel"], { cwd: directory });
|
|
111
|
+
if (result.status !== 0) return false;
|
|
112
|
+
|
|
113
|
+
return samePath(result.stdout, directory);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The URL of a checkout's `origin` remote, or undefined when it has none (a
|
|
118
|
+
* repository created locally with `git init` has no remote until one is added).
|
|
119
|
+
*
|
|
120
|
+
* @param directory - The checkout to inspect.
|
|
121
|
+
* @param options - The git runner to use.
|
|
122
|
+
*/
|
|
123
|
+
export function remoteUrlOf(directory: string, options: GitOptions = {}): string | undefined {
|
|
124
|
+
const runner = options.runner ?? runGit;
|
|
125
|
+
const result = runner(["remote", "get-url", "origin"], { cwd: directory });
|
|
126
|
+
if (result.status !== 0 || result.stdout.length === 0) return undefined;
|
|
127
|
+
return result.stdout;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** What a clone actually did. */
|
|
131
|
+
export type CloneResult = {
|
|
132
|
+
/** The depth git was finally asked for; 0 means the full history. */
|
|
133
|
+
depth: number;
|
|
134
|
+
/** True when a shallow clone was refused and the full history was fetched instead. */
|
|
135
|
+
fellBackToFullClone: boolean;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Clones a repository into a directory that does not yet exist, creating its
|
|
140
|
+
* parents. A shallow clone is the default for a link, because a linked checkout
|
|
141
|
+
* exists to be read and edited, not to carry the project's whole history; pass
|
|
142
|
+
* `depth: 0` to ask for the full history outright.
|
|
143
|
+
*
|
|
144
|
+
* Not every remote will serve a shallow clone (`file://` transports and some
|
|
145
|
+
* servers refuse one), so a failed shallow attempt is retried in full rather
|
|
146
|
+
* than reported as a failure. The result says whether that happened, so the
|
|
147
|
+
* caller can tell the user why the clone took longer than they expected.
|
|
148
|
+
*
|
|
149
|
+
* @param url - Where the repository lives.
|
|
150
|
+
* @param destDir - Absolute path the working copy is created at.
|
|
151
|
+
* @param options - Clone depth and the git runner to use.
|
|
152
|
+
*/
|
|
153
|
+
export function cloneRepo(
|
|
154
|
+
url: string,
|
|
155
|
+
destDir: string,
|
|
156
|
+
options: GitOptions & { depth?: number } = {}
|
|
157
|
+
): CloneResult {
|
|
158
|
+
if (fs.existsSync(destDir) && !isEmptyDirectory(destDir)) {
|
|
159
|
+
throw new ConfigError(
|
|
160
|
+
`Cannot clone into ${destDir}: the directory already exists and is not empty.\n` +
|
|
161
|
+
` Move or delete it, or link the checkout that is already there by passing ` +
|
|
162
|
+
`its path to 'sous repo link'.`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
fs.mkdirSync(path.dirname(destDir), { recursive: true });
|
|
167
|
+
|
|
168
|
+
const depth = options.depth ?? 1;
|
|
169
|
+
const runner = options.runner ?? runGit;
|
|
170
|
+
|
|
171
|
+
if (depth > 0) {
|
|
172
|
+
const shallow = runner(["clone", "--depth", String(depth), "--", url, destDir], {});
|
|
173
|
+
if (shallow.status === 0) return { depth, fellBackToFullClone: false };
|
|
174
|
+
// git leaves the destination behind on some failures; clear it so the retry
|
|
175
|
+
// is not refused by its own leftovers.
|
|
176
|
+
fs.rmSync(destDir, { recursive: true, force: true });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
runGitOrThrow(["clone", "--", url, destDir], {
|
|
180
|
+
runner: options.runner,
|
|
181
|
+
what: `Cloning ${url}`,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
return { depth: 0, fellBackToFullClone: depth > 0 };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* True when two remote URLs name the same repository, ignoring the differences
|
|
189
|
+
* that never change what is fetched: a `.git` suffix, a trailing slash, the
|
|
190
|
+
* host's letter case, and SCP-style syntax (`git@host:owner/repo`) against URL
|
|
191
|
+
* syntax (`https://host/owner/repo`).
|
|
192
|
+
*
|
|
193
|
+
* @param a - One remote URL.
|
|
194
|
+
* @param b - The other remote URL.
|
|
195
|
+
*/
|
|
196
|
+
export function sameRemote(a: string, b: string): boolean {
|
|
197
|
+
return normalizeRemoteUrl(a) === normalizeRemoteUrl(b);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Reduces a remote URL to `host/path` in lower case, with any `.git` suffix,
|
|
202
|
+
* trailing slash, scheme, port and user info removed, so two spellings of one
|
|
203
|
+
* repository compare equal.
|
|
204
|
+
*
|
|
205
|
+
* @param url - The remote URL to normalize.
|
|
206
|
+
*/
|
|
207
|
+
export function normalizeRemoteUrl(url: string): string {
|
|
208
|
+
let value = url.trim();
|
|
209
|
+
|
|
210
|
+
// SCP-style syntax, which is not a URL: git@github.com:sous-io/sous.git
|
|
211
|
+
const scp = /^(?:[^@/]+@)?([^/:]+):(?!\/\/)(.+)$/.exec(value);
|
|
212
|
+
if (scp !== null) {
|
|
213
|
+
value = `${scp[1]}/${scp[2]}`;
|
|
214
|
+
} else {
|
|
215
|
+
value = value.replace(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//, "");
|
|
216
|
+
// Strip user info (user@ or user:password@) from an authority.
|
|
217
|
+
value = value.replace(/^[^/]*@/, "");
|
|
218
|
+
// Strip a port from the host.
|
|
219
|
+
value = value.replace(/^([^/]+):\d+/, "$1");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
value = value.replace(/\/+$/, "");
|
|
223
|
+
value = value.replace(/\.git$/i, "");
|
|
224
|
+
return value.toLowerCase();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** A repository's owner and name, as taken from its URL. */
|
|
228
|
+
export type RepoSlug = {
|
|
229
|
+
/** The owning user or organization, or "repos" when the URL has no owner segment. */
|
|
230
|
+
owner: string;
|
|
231
|
+
/** The repository's own name, with any `.git` suffix removed. */
|
|
232
|
+
name: string;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Splits a remote URL into the owner and name that decide where a default clone
|
|
237
|
+
* lands (`.sous/repos/<owner>/<name>`). A URL with no owner segment yields the
|
|
238
|
+
* literal owner "repos", so the layout stays two levels deep whatever the URL
|
|
239
|
+
* looked like.
|
|
240
|
+
*
|
|
241
|
+
* @param url - The remote URL to read.
|
|
242
|
+
*/
|
|
243
|
+
export function repoSlugFromUrl(url: string): RepoSlug {
|
|
244
|
+
const normalized = normalizeRemoteUrl(url);
|
|
245
|
+
const segments = normalized.split("/").filter((segment) => segment.length > 0);
|
|
246
|
+
|
|
247
|
+
if (segments.length < 2) {
|
|
248
|
+
throw new ConfigError(
|
|
249
|
+
`Could not work out an owner and a repository name from the URL '${url}'.\n` +
|
|
250
|
+
` A repository URL looks like 'https://github.com/sous-io/sous-recipes'. ` +
|
|
251
|
+
`Pass a path to 'sous repo link' to link a checkout that is already on disk.`
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const name = segments[segments.length - 1]!;
|
|
256
|
+
const owner = segments.length >= 3 ? segments[segments.length - 2]! : "repos";
|
|
257
|
+
return { owner: sanitizeSegment(owner), name: sanitizeSegment(name) };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* The short name a repository is known by when its URL was given on the command
|
|
262
|
+
* line rather than configured: the last segment of the URL, with any `.git`
|
|
263
|
+
* suffix removed.
|
|
264
|
+
*
|
|
265
|
+
* @param url - The remote URL to read.
|
|
266
|
+
*/
|
|
267
|
+
export function repoNameFromUrl(url: string): string {
|
|
268
|
+
return repoSlugFromUrl(url).name;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* True when the string looks like something git can clone rather than a short
|
|
273
|
+
* name: an explicit scheme, SCP-style syntax, or an absolute path.
|
|
274
|
+
*
|
|
275
|
+
* @param value - The string to test.
|
|
276
|
+
*/
|
|
277
|
+
export function looksLikeRepoUrl(value: string): boolean {
|
|
278
|
+
if (value.includes("://")) return true;
|
|
279
|
+
if (/^[^@/\s]+@[^/\s:]+:/.test(value)) return true;
|
|
280
|
+
return value.startsWith("/") || value.startsWith("./") || value.startsWith("../");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// --- Small filesystem helpers -------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
/** Replaces anything outside a safe path segment, so a URL can never escape the store. */
|
|
286
|
+
function sanitizeSegment(segment: string): string {
|
|
287
|
+
const cleaned = segment.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^\.+/, "");
|
|
288
|
+
return cleaned.length > 0 ? cleaned : "repo";
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** True when the path exists and is a directory. */
|
|
292
|
+
function directoryExists(candidate: string): boolean {
|
|
293
|
+
try {
|
|
294
|
+
return fs.statSync(candidate).isDirectory();
|
|
295
|
+
} catch {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** True when the path is a directory holding nothing at all. */
|
|
301
|
+
function isEmptyDirectory(candidate: string): boolean {
|
|
302
|
+
try {
|
|
303
|
+
return fs.readdirSync(candidate).length === 0;
|
|
304
|
+
} catch {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Compares two paths after resolving them, so a trailing slash never matters. */
|
|
310
|
+
function samePath(a: string, b: string): boolean {
|
|
311
|
+
return path.resolve(a) === path.resolve(b);
|
|
312
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical repository identity: what the machine-wide store, the index cache
|
|
3
|
+
* and the lockfile key a repository by.
|
|
4
|
+
*
|
|
5
|
+
* A repository's short name (`sous-recipes`) is a CONSUMER's label. Two
|
|
6
|
+
* projects may call the same repository different things, and two different
|
|
7
|
+
* repositories may be called the same thing in two projects, so a short name
|
|
8
|
+
* can never key anything shared between projects. The identity can: it is
|
|
9
|
+
* derived from the location the repository actually lives at, so every project
|
|
10
|
+
* on a machine agrees about it.
|
|
11
|
+
*
|
|
12
|
+
* github.com/sous-io/sous-recipes
|
|
13
|
+
* gitlab.example.com/group/subgroup/project
|
|
14
|
+
* localhost/home/me/Projects/my-recipes
|
|
15
|
+
*
|
|
16
|
+
* The shape is `<host>/<owner path>/<name>`, lowercased, with any `.git`
|
|
17
|
+
* suffix already gone (the providers strip it while canonicalizing). Short
|
|
18
|
+
* names stay exactly where they were: in a project's config, in its lockfile
|
|
19
|
+
* keys, and in everything sous prints, because that is what a person typed.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { CanonicalRepo } from "./providers/provider.js";
|
|
23
|
+
import { REPO_NAME_PATTERN } from "./formats/patterns.js";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The canonical identity of a repository, as every machine-wide key spells it.
|
|
27
|
+
*
|
|
28
|
+
* repoIdentity({ host: "GitHub.com", owner: "sous-io", name: "sous-recipes" });
|
|
29
|
+
* // -> "github.com/sous-io/sous-recipes"
|
|
30
|
+
*
|
|
31
|
+
* @param canonical - The repository, taken apart by its provider.
|
|
32
|
+
*/
|
|
33
|
+
export function repoIdentity(canonical: CanonicalRepo): string {
|
|
34
|
+
const name = canonical.name.replace(/\.git$/i, "");
|
|
35
|
+
return identitySegments(`${canonical.host}/${canonical.owner}/${name}`).join("/");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The path segments an identity is made of, with empty segments dropped and
|
|
40
|
+
* every segment lowercased. A local repository's owner is an absolute path, so
|
|
41
|
+
* its leading separator would otherwise produce an empty first segment.
|
|
42
|
+
*
|
|
43
|
+
* identitySegments("localhost//home/me/recipes");
|
|
44
|
+
* // -> ["localhost", "home", "me", "recipes"]
|
|
45
|
+
*
|
|
46
|
+
* @param identity - The identity, or the pieces of one joined with slashes.
|
|
47
|
+
*/
|
|
48
|
+
export function identitySegments(identity: string): string[] {
|
|
49
|
+
return identity
|
|
50
|
+
.split("/")
|
|
51
|
+
.map((segment) => segment.trim().toLowerCase())
|
|
52
|
+
.filter((segment) => segment.length > 0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A short name derived from an identity, used when sous has to name a
|
|
57
|
+
* repository a project has never added (a dependency's locator URL, for
|
|
58
|
+
* instance) before the person has chosen a name for it.
|
|
59
|
+
*
|
|
60
|
+
* shortNameFromIdentity("github.com/sous-io/sous-recipes"); // -> "sous-recipes"
|
|
61
|
+
*
|
|
62
|
+
* @param identity - The repository's canonical identity.
|
|
63
|
+
*/
|
|
64
|
+
export function shortNameFromIdentity(identity: string): string {
|
|
65
|
+
const segments = identitySegments(identity);
|
|
66
|
+
const last = segments[segments.length - 1] ?? "";
|
|
67
|
+
const cleaned = last
|
|
68
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
69
|
+
.replace(/-+/g, "-")
|
|
70
|
+
.replace(/^-+|-+$/g, "");
|
|
71
|
+
|
|
72
|
+
// A short name is lowercase kebab-case starting with a letter. A repository
|
|
73
|
+
// whose name starts with a digit, or is made entirely of characters a name
|
|
74
|
+
// may not carry, still needs something to be called.
|
|
75
|
+
if (REPO_NAME_PATTERN.test(cleaned)) return cleaned;
|
|
76
|
+
return cleaned.length === 0 ? "repository" : `repo-${cleaned}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* True when two identities name the same repository. Identities are already
|
|
81
|
+
* normalized, so this is a plain comparison; it exists so callers read as what
|
|
82
|
+
* they mean rather than as a string equality.
|
|
83
|
+
*
|
|
84
|
+
* @param left - One identity.
|
|
85
|
+
* @param right - The other.
|
|
86
|
+
*/
|
|
87
|
+
export function sameRepoIdentity(left: string, right: string): boolean {
|
|
88
|
+
return left === right;
|
|
89
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Repositories layer: every on-disk format, the loaders that read them, and
|
|
3
|
+
* the ref parser that names what is inside them.
|
|
4
|
+
*
|
|
5
|
+
* Import from here rather than from the individual modules, so later phases
|
|
6
|
+
* (the store, the providers, the resolver, the CLI surface) have one place to
|
|
7
|
+
* look for what this layer offers.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// The regular expressions in formats/patterns.js reach here through
|
|
11
|
+
// formats/common.js, which re-exports them; listing patterns.js again would
|
|
12
|
+
// make every one of those names an ambiguous star export.
|
|
13
|
+
export * from "./formats/common.js";
|
|
14
|
+
export * from "./formats/repo-manifest.js";
|
|
15
|
+
export * from "./formats/recipe-manifest.js";
|
|
16
|
+
export * from "./formats/index-file.js";
|
|
17
|
+
export * from "./formats/lockfile.js";
|
|
18
|
+
export * from "./formats/store-entry.js";
|
|
19
|
+
export * from "./formats/links-map.js";
|
|
20
|
+
export * from "./load-manifest.js";
|
|
21
|
+
export * from "./ref.js";
|
|
22
|
+
export * from "./identity.js";
|
|
23
|
+
|
|
24
|
+
// Phase 2a: the machine-wide recipe store, under the user-level sous directory.
|
|
25
|
+
export * from "./store/contract.js";
|
|
26
|
+
export * from "./store/hash.js";
|
|
27
|
+
export * from "./store/recipe-store.js";
|
|
28
|
+
export * from "./store/settings.js";
|
|
29
|
+
|
|
30
|
+
// Phase 6a: the CLI surface for editable checkouts (`sous repo init`, `link`,
|
|
31
|
+
// `unlink`). The links map and its ignore hygiene, the thin git layer both of
|
|
32
|
+
// them use, and the scaffold that `repo init` writes.
|
|
33
|
+
export * from "./links.js";
|
|
34
|
+
export * from "./git-clone.js";
|
|
35
|
+
export * from "./scaffold/index.js";
|
|
36
|
+
|
|
37
|
+
// Phase 2b: the store contract, the providers, the resolver, the trust layer
|
|
38
|
+
// and the lockfile service.
|
|
39
|
+
export * from "./providers/index.js";
|
|
40
|
+
export * from "./resolver.js";
|
|
41
|
+
export * from "./managed-layer.js";
|
|
42
|
+
export * from "./trust.js";
|
|
43
|
+
export * from "./lock-service.js";
|
|
44
|
+
export * from "./freshness.js";
|
|
45
|
+
|
|
46
|
+
// Phase 3: the consumer surface. Where a locked recipe's files are, the
|
|
47
|
+
// namespace resolver and compile targets built from that, and the service the
|
|
48
|
+
// `repo`, `subscribe` and `unsubscribe` commands drive.
|
|
49
|
+
export * from "./locked-recipes.js";
|
|
50
|
+
export * from "./locked-namespace-resolver.js";
|
|
51
|
+
export * from "./subscription-service.js";
|
|
52
|
+
|
|
53
|
+
// Phase 7: the core namespace. The recipe that ships inside the package, the
|
|
54
|
+
// seed that puts it in the store on first run, and the built-in repository and
|
|
55
|
+
// subscription every project gets unless it opts out.
|
|
56
|
+
export * from "./core-recipe.js";
|
|
57
|
+
export * from "./seed.js";
|
|
58
|
+
export * from "./defaults.js";
|