@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,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raising, or setting, the version in a recipe manifest.
|
|
3
|
+
*
|
|
4
|
+
* A recipe manifest is HAND-WRITTEN, and the scaffold sous writes is mostly
|
|
5
|
+
* comments, so a version bump must give the file back to its author looking the
|
|
6
|
+
* way they left it. Both supported formats are edited in place rather than
|
|
7
|
+
* re-serialized from a parsed object: YAML through the `yaml` package's
|
|
8
|
+
* document model, which keeps comments and layout, and JSON through
|
|
9
|
+
* `jsonc-parser`, which edits the exact byte range of the value and leaves
|
|
10
|
+
* everything else, comments included, untouched.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import semver from "semver";
|
|
16
|
+
import YAML from "yaml";
|
|
17
|
+
import { applyEdits, modify } from "jsonc-parser";
|
|
18
|
+
import { ConfigError } from "../../errors.js";
|
|
19
|
+
import { parseJsoncText } from "../load-manifest.js";
|
|
20
|
+
|
|
21
|
+
/** How far a version is being raised. */
|
|
22
|
+
export const BUMP_LEVELS = ["patch", "minor", "major", "prerelease"] as const;
|
|
23
|
+
|
|
24
|
+
/** One of the ways `sous repo release --bump` can raise a version. */
|
|
25
|
+
export type BumpLevel = (typeof BUMP_LEVELS)[number];
|
|
26
|
+
|
|
27
|
+
/** What one bump did. */
|
|
28
|
+
export type BumpResult = {
|
|
29
|
+
/** The manifest that was rewritten. */
|
|
30
|
+
manifestPath: string;
|
|
31
|
+
/** The version it declared before. */
|
|
32
|
+
from: string;
|
|
33
|
+
/** The version it declares now. */
|
|
34
|
+
to: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The version one level up from the current one, using npm's own semantics.
|
|
39
|
+
*
|
|
40
|
+
* @param current - The version the manifest declares now.
|
|
41
|
+
* @param level - How far to raise it.
|
|
42
|
+
*/
|
|
43
|
+
export function nextVersion(current: string, level: BumpLevel): string {
|
|
44
|
+
const next = semver.inc(current, level);
|
|
45
|
+
if (next === null) {
|
|
46
|
+
throw new ConfigError(
|
|
47
|
+
`Cannot raise the version '${current}' by a ${level} step.\n` +
|
|
48
|
+
` A recipe version is an exact semantic version, such as '1.4.0' or '2.0.0-beta.1'.`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return next;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Rewrites a recipe manifest's `version` field in place, preserving the rest of
|
|
56
|
+
* the file as written.
|
|
57
|
+
*
|
|
58
|
+
* @param manifestPath - Absolute path to the recipe manifest.
|
|
59
|
+
* @param level - How far to raise the version.
|
|
60
|
+
*/
|
|
61
|
+
export function bumpRecipeVersion(manifestPath: string, level: BumpLevel): BumpResult {
|
|
62
|
+
return rewriteVersion(manifestPath, (current) => nextVersion(current, level));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Writes an exact version into a recipe manifest, preserving the rest of the
|
|
67
|
+
* file as written.
|
|
68
|
+
*
|
|
69
|
+
* This is what the release pipeline uses to hold the packaged core recipe at
|
|
70
|
+
* the sous package's own version (see `scripts/sync-core-version.mts`), where
|
|
71
|
+
* the new version is dictated rather than stepped. A manifest that already
|
|
72
|
+
* declares this version is left untouched, byte for byte, so running the sync
|
|
73
|
+
* twice cannot reformat a hand-written file.
|
|
74
|
+
*
|
|
75
|
+
* @param manifestPath - Absolute path to the recipe manifest.
|
|
76
|
+
* @param version - The exact semantic version the manifest should declare.
|
|
77
|
+
*/
|
|
78
|
+
export function setRecipeVersion(manifestPath: string, version: string): BumpResult {
|
|
79
|
+
if (semver.valid(version) === null) {
|
|
80
|
+
throw new ConfigError(
|
|
81
|
+
`Cannot set the recipe version to '${version}'.\n` +
|
|
82
|
+
` A recipe version is an exact semantic version, such as '1.4.0' or '2.0.0-beta.1'.`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return rewriteVersion(manifestPath, () => version);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The one writer both callers share: read the version the manifest declares,
|
|
90
|
+
* work out what it becomes, and rewrite that value alone.
|
|
91
|
+
*
|
|
92
|
+
* @param manifestPath - Absolute path to the recipe manifest.
|
|
93
|
+
* @param nextFrom - Given the declared version, the version to write.
|
|
94
|
+
*/
|
|
95
|
+
function rewriteVersion(
|
|
96
|
+
manifestPath: string,
|
|
97
|
+
nextFrom: (current: string) => string
|
|
98
|
+
): BumpResult {
|
|
99
|
+
const text = fs.readFileSync(manifestPath, "utf8");
|
|
100
|
+
const extension = path.extname(manifestPath).toLowerCase();
|
|
101
|
+
|
|
102
|
+
if (extension === ".json" || extension === ".jsonc") {
|
|
103
|
+
return rewriteJson(manifestPath, text, nextFrom);
|
|
104
|
+
}
|
|
105
|
+
return rewriteYaml(manifestPath, text, nextFrom);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Rewrites the version in a YAML manifest, keeping its comments and layout. */
|
|
109
|
+
function rewriteYaml(
|
|
110
|
+
manifestPath: string,
|
|
111
|
+
text: string,
|
|
112
|
+
nextFrom: (current: string) => string
|
|
113
|
+
): BumpResult {
|
|
114
|
+
const document = YAML.parseDocument(text);
|
|
115
|
+
const node = document.get("version", true);
|
|
116
|
+
|
|
117
|
+
if (!YAML.isScalar(node) || typeof node.value !== "string") {
|
|
118
|
+
throw missingVersion(manifestPath);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const from = node.value;
|
|
122
|
+
const to = nextFrom(from);
|
|
123
|
+
if (to === from) return { manifestPath, from, to };
|
|
124
|
+
|
|
125
|
+
node.value = to;
|
|
126
|
+
fs.writeFileSync(manifestPath, document.toString(), "utf8");
|
|
127
|
+
|
|
128
|
+
return { manifestPath, from, to };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Rewrites the version in a JSON or JSONC manifest, editing only that value's bytes. */
|
|
132
|
+
function rewriteJson(
|
|
133
|
+
manifestPath: string,
|
|
134
|
+
text: string,
|
|
135
|
+
nextFrom: (current: string) => string
|
|
136
|
+
): BumpResult {
|
|
137
|
+
// The manifest dialect allows comments and trailing commas, so it is read
|
|
138
|
+
// through the loader's own permissive parser rather than JSON.parse.
|
|
139
|
+
const parsed = parseJsoncText(text, manifestPath);
|
|
140
|
+
const from =
|
|
141
|
+
typeof parsed === "object" && parsed !== null
|
|
142
|
+
? (parsed as Record<string, unknown>).version
|
|
143
|
+
: undefined;
|
|
144
|
+
if (typeof from !== "string") throw missingVersion(manifestPath);
|
|
145
|
+
|
|
146
|
+
const to = nextFrom(from);
|
|
147
|
+
if (to === from) return { manifestPath, from, to };
|
|
148
|
+
|
|
149
|
+
const edits = modify(text, ["version"], to, {});
|
|
150
|
+
fs.writeFileSync(manifestPath, applyEdits(text, edits), "utf8");
|
|
151
|
+
|
|
152
|
+
return { manifestPath, from, to };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The error for a manifest with no usable `version` field. */
|
|
156
|
+
function missingVersion(manifestPath: string): ConfigError {
|
|
157
|
+
return new ConfigError(
|
|
158
|
+
`The recipe manifest at ${manifestPath} has no 'version' field to write.\n` +
|
|
159
|
+
` Every recipe declares an exact semantic version; add one, then try again.`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What git says about the working tree a release or a submission is standing in.
|
|
3
|
+
*
|
|
4
|
+
* Both `sous repo release` and `sous repo submit` need the same handful of
|
|
5
|
+
* facts before they are allowed to do anything: is anything uncommitted, which
|
|
6
|
+
* branch is checked out, which branch is the default one, and where does
|
|
7
|
+
* `origin` point. A release makes exactly one commit of its own (its version
|
|
8
|
+
* bumps and its index, through `commitPaths`) and refuses while anything else
|
|
9
|
+
* is uncommitted; everything else here is read to refuse politely rather than
|
|
10
|
+
* to fix anything.
|
|
11
|
+
*
|
|
12
|
+
* Every function takes the injectable command runner, so tests never spawn git
|
|
13
|
+
* unless they mean to.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { runGit, type RunOptions } from "../providers/git.js";
|
|
17
|
+
|
|
18
|
+
/** One path git reports as changed, with the two-letter status it reported. */
|
|
19
|
+
export type ChangedPath = {
|
|
20
|
+
/** The status code from `git status --porcelain`, such as `M `, `??` or `A `. */
|
|
21
|
+
status: string;
|
|
22
|
+
/** The path, relative to the repository root. */
|
|
23
|
+
path: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Everything `git status --porcelain` reports: staged changes, unstaged changes
|
|
28
|
+
* and untracked files alike. An empty list means the working tree is clean.
|
|
29
|
+
*
|
|
30
|
+
* @param rootDir - The repository's root directory.
|
|
31
|
+
* @param options - The command runner to use.
|
|
32
|
+
*/
|
|
33
|
+
export async function uncommittedChanges(
|
|
34
|
+
rootDir: string,
|
|
35
|
+
options: RunOptions = {}
|
|
36
|
+
): Promise<ChangedPath[]> {
|
|
37
|
+
const output = await runGit(["status", "--porcelain"], {
|
|
38
|
+
cwd: rootDir,
|
|
39
|
+
run: options.run,
|
|
40
|
+
});
|
|
41
|
+
if (output.length === 0) return [];
|
|
42
|
+
|
|
43
|
+
const changed: ChangedPath[] = [];
|
|
44
|
+
for (const line of output.split("\n")) {
|
|
45
|
+
// The status field is one or two characters, and the command runner trims
|
|
46
|
+
// its output, so a leading space (an unstaged edit) is already gone by the
|
|
47
|
+
// time the line arrives here. Split on the whitespace instead of counting
|
|
48
|
+
// columns. A rename is reported as 'old -> new'; the new name is the one
|
|
49
|
+
// that still exists, so that is the one reported.
|
|
50
|
+
const match = /^(\S{1,2})\s+(.*)$/.exec(line.trim());
|
|
51
|
+
if (match === null) continue;
|
|
52
|
+
const target = match[2]!;
|
|
53
|
+
const arrow = target.indexOf(" -> ");
|
|
54
|
+
changed.push({
|
|
55
|
+
status: match[1]!,
|
|
56
|
+
path: arrow === -1 ? target : target.slice(arrow + 4),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return changed;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The name of the checked-out branch, or undefined when HEAD is detached (which
|
|
64
|
+
* is what a CI checkout of a tag looks like).
|
|
65
|
+
*
|
|
66
|
+
* @param rootDir - The repository's root directory.
|
|
67
|
+
* @param options - The command runner to use.
|
|
68
|
+
*/
|
|
69
|
+
export async function currentBranch(
|
|
70
|
+
rootDir: string,
|
|
71
|
+
options: RunOptions = {}
|
|
72
|
+
): Promise<string | undefined> {
|
|
73
|
+
try {
|
|
74
|
+
const name = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
75
|
+
cwd: rootDir,
|
|
76
|
+
run: options.run,
|
|
77
|
+
});
|
|
78
|
+
return name === "HEAD" ? undefined : name;
|
|
79
|
+
} catch {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The repository's default branch, taken from what `origin/HEAD` points at.
|
|
86
|
+
* Returns undefined when the remote never told this clone, which is normal for
|
|
87
|
+
* a repository cloned with `--depth 1` or created locally.
|
|
88
|
+
*
|
|
89
|
+
* @param rootDir - The repository's root directory.
|
|
90
|
+
* @param options - The command runner to use.
|
|
91
|
+
*/
|
|
92
|
+
export async function defaultBranch(
|
|
93
|
+
rootDir: string,
|
|
94
|
+
options: RunOptions = {}
|
|
95
|
+
): Promise<string | undefined> {
|
|
96
|
+
try {
|
|
97
|
+
const ref = await runGit(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], {
|
|
98
|
+
cwd: rootDir,
|
|
99
|
+
run: options.run,
|
|
100
|
+
});
|
|
101
|
+
const slash = ref.indexOf("/");
|
|
102
|
+
return slash === -1 ? ref : ref.slice(slash + 1);
|
|
103
|
+
} catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The URL of a remote, or undefined when the repository has no such remote.
|
|
110
|
+
*
|
|
111
|
+
* @param rootDir - The repository's root directory.
|
|
112
|
+
* @param remote - The remote's name, normally `origin`.
|
|
113
|
+
* @param options - The command runner to use.
|
|
114
|
+
*/
|
|
115
|
+
export async function remoteUrl(
|
|
116
|
+
rootDir: string,
|
|
117
|
+
remote: string,
|
|
118
|
+
options: RunOptions = {}
|
|
119
|
+
): Promise<string | undefined> {
|
|
120
|
+
try {
|
|
121
|
+
const url = await runGit(["remote", "get-url", remote], {
|
|
122
|
+
cwd: rootDir,
|
|
123
|
+
run: options.run,
|
|
124
|
+
});
|
|
125
|
+
return url.length > 0 ? url : undefined;
|
|
126
|
+
} catch {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* True when a path is tracked by git and identical to what HEAD holds. It is how
|
|
133
|
+
* a caller confirms that a file it is about to act on is the one the repository
|
|
134
|
+
* actually committed.
|
|
135
|
+
*
|
|
136
|
+
* @param rootDir - The repository's root directory.
|
|
137
|
+
* @param relativePath - The path to check, relative to the repository root.
|
|
138
|
+
* @param options - The command runner to use.
|
|
139
|
+
*/
|
|
140
|
+
export async function isCommittedAndUnchanged(
|
|
141
|
+
rootDir: string,
|
|
142
|
+
relativePath: string,
|
|
143
|
+
options: RunOptions = {}
|
|
144
|
+
): Promise<boolean> {
|
|
145
|
+
const tracked = await runGit(["ls-files", "--", relativePath], {
|
|
146
|
+
cwd: rootDir,
|
|
147
|
+
run: options.run,
|
|
148
|
+
});
|
|
149
|
+
if (tracked.length === 0) return false;
|
|
150
|
+
|
|
151
|
+
const changed = await runGit(["status", "--porcelain", "--", relativePath], {
|
|
152
|
+
cwd: rootDir,
|
|
153
|
+
run: options.run,
|
|
154
|
+
});
|
|
155
|
+
return changed.length === 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* True when git can work out who is committing, which is what it needs before
|
|
160
|
+
* it will make a commit or an annotated tag.
|
|
161
|
+
*
|
|
162
|
+
* `git var GIT_AUTHOR_IDENT` answers the exact question git asks itself: it
|
|
163
|
+
* honours the `user.name` and `user.email` settings, the `GIT_AUTHOR_*` and
|
|
164
|
+
* `GIT_COMMITTER_*` environment variables, and the strict rules that reject a
|
|
165
|
+
* guessed identity. It fails with the same "empty ident name" or "unable to
|
|
166
|
+
* auto-detect email address" that a commit would have failed with, which is
|
|
167
|
+
* why the check is made ahead of time rather than left to the commit.
|
|
168
|
+
*
|
|
169
|
+
* @param rootDir - The repository's root directory.
|
|
170
|
+
* @param options - The command runner to use.
|
|
171
|
+
*/
|
|
172
|
+
export async function hasCommitIdentity(
|
|
173
|
+
rootDir: string,
|
|
174
|
+
options: RunOptions = {}
|
|
175
|
+
): Promise<boolean> {
|
|
176
|
+
for (const name of ["GIT_AUTHOR_IDENT", "GIT_COMMITTER_IDENT"]) {
|
|
177
|
+
try {
|
|
178
|
+
const ident = await runGit(["var", name], { cwd: rootDir, run: options.run });
|
|
179
|
+
if (ident.length === 0) return false;
|
|
180
|
+
} catch {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Stages exactly the given paths and commits them.
|
|
189
|
+
*
|
|
190
|
+
* This is the one place sous commits on an author's behalf, and it is
|
|
191
|
+
* deliberately narrow: a release writes version bumps and an index, and those
|
|
192
|
+
* are the only paths it stages. Anything else in the working tree is left
|
|
193
|
+
* exactly as it was.
|
|
194
|
+
*
|
|
195
|
+
* @param rootDir - The repository's root directory.
|
|
196
|
+
* @param paths - The paths to stage, relative to the repository root.
|
|
197
|
+
* @param message - The commit message.
|
|
198
|
+
* @param options - The command runner to use.
|
|
199
|
+
*/
|
|
200
|
+
export async function commitPaths(
|
|
201
|
+
rootDir: string,
|
|
202
|
+
paths: ReadonlyArray<string>,
|
|
203
|
+
message: string,
|
|
204
|
+
options: RunOptions = {}
|
|
205
|
+
): Promise<void> {
|
|
206
|
+
if (paths.length === 0) return;
|
|
207
|
+
await runGit(["add", "--", ...paths], { cwd: rootDir, run: options.run });
|
|
208
|
+
await runGit(["commit", "--message", message, "--", ...paths], {
|
|
209
|
+
cwd: rootDir,
|
|
210
|
+
run: options.run,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* True when any of the given paths differs from what HEAD holds, so a release
|
|
216
|
+
* knows whether it has anything to commit.
|
|
217
|
+
*
|
|
218
|
+
* @param rootDir - The repository's root directory.
|
|
219
|
+
* @param paths - The paths to check, relative to the repository root.
|
|
220
|
+
* @param options - The command runner to use.
|
|
221
|
+
*/
|
|
222
|
+
export async function anythingToCommit(
|
|
223
|
+
rootDir: string,
|
|
224
|
+
paths: ReadonlyArray<string>,
|
|
225
|
+
options: RunOptions = {}
|
|
226
|
+
): Promise<boolean> {
|
|
227
|
+
if (paths.length === 0) return false;
|
|
228
|
+
const changed = await runGit(["status", "--porcelain", "--", ...paths], {
|
|
229
|
+
cwd: rootDir,
|
|
230
|
+
run: options.run,
|
|
231
|
+
});
|
|
232
|
+
return changed.length > 0;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Creates a branch at HEAD and checks it out.
|
|
237
|
+
*
|
|
238
|
+
* @param rootDir - The repository's root directory.
|
|
239
|
+
* @param branch - The branch to create.
|
|
240
|
+
* @param options - The command runner to use.
|
|
241
|
+
*/
|
|
242
|
+
export async function createBranch(
|
|
243
|
+
rootDir: string,
|
|
244
|
+
branch: string,
|
|
245
|
+
options: RunOptions = {}
|
|
246
|
+
): Promise<void> {
|
|
247
|
+
await runGit(["checkout", "-b", branch], { cwd: rootDir, run: options.run });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Pushes one branch to a remote, setting it as the branch's upstream.
|
|
252
|
+
*
|
|
253
|
+
* @param rootDir - The repository's root directory.
|
|
254
|
+
* @param remote - The remote to push to.
|
|
255
|
+
* @param branch - The branch to push.
|
|
256
|
+
* @param options - The command runner to use.
|
|
257
|
+
*/
|
|
258
|
+
export async function pushBranch(
|
|
259
|
+
rootDir: string,
|
|
260
|
+
remote: string,
|
|
261
|
+
branch: string,
|
|
262
|
+
options: RunOptions = {}
|
|
263
|
+
): Promise<void> {
|
|
264
|
+
await runGit(["push", "--set-upstream", remote, branch], {
|
|
265
|
+
cwd: rootDir,
|
|
266
|
+
run: options.run,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* The subject line of the most recent commit, or undefined when there is none.
|
|
272
|
+
* It is the default title for a proposed change, which is what a contributor
|
|
273
|
+
* would have typed anyway.
|
|
274
|
+
*
|
|
275
|
+
* @param rootDir - The repository's root directory.
|
|
276
|
+
* @param options - The command runner to use.
|
|
277
|
+
*/
|
|
278
|
+
export async function lastCommitSubject(
|
|
279
|
+
rootDir: string,
|
|
280
|
+
options: RunOptions = {}
|
|
281
|
+
): Promise<string | undefined> {
|
|
282
|
+
try {
|
|
283
|
+
const subject = await runGit(["log", "-1", "--format=%s"], {
|
|
284
|
+
cwd: rootDir,
|
|
285
|
+
run: options.run,
|
|
286
|
+
});
|
|
287
|
+
return subject.length > 0 ? subject : undefined;
|
|
288
|
+
} catch {
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* The branch name sous proposes for a submission, stamped with the minute it
|
|
295
|
+
* was made so two submissions from one checkout never collide.
|
|
296
|
+
*
|
|
297
|
+
* @param now - The moment the branch is being created.
|
|
298
|
+
*/
|
|
299
|
+
export function submitBranchName(now: Date): string {
|
|
300
|
+
const pad = (value: number) => String(value).padStart(2, "0");
|
|
301
|
+
const stamp =
|
|
302
|
+
`${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}` +
|
|
303
|
+
`-${pad(now.getHours())}${pad(now.getMinutes())}`;
|
|
304
|
+
return `sous/submit-${stamp}`;
|
|
305
|
+
}
|