@sous-io/sous 0.2.37 → 1.0.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 +14 -256
- package/dist/bin.js +11 -0
- package/dist/sous.js +1093 -0
- package/docs/markdown/README.md +5 -4
- package/docs/markdown/_sidebar.md +21 -1
- package/docs/markdown/commands.md +294 -431
- package/docs/markdown/config-discovery.md +25 -14
- package/docs/markdown/config-inspection.md +53 -31
- package/docs/markdown/config-layers.md +168 -66
- package/docs/markdown/config-variables.md +44 -11
- package/docs/markdown/configuration.md +104 -17
- package/docs/markdown/design-principles.md +4 -1
- package/docs/markdown/repositories-authoring.md +30 -0
- package/docs/markdown/repositories-consuming.md +72 -14
- package/docs/markdown/repositories-file-formats.md +31 -13
- package/docs/markdown/repositories-providers.md +48 -57
- package/docs/markdown/repositories-quickstart.md +3 -2
- package/docs/markdown/repositories-troubleshooting.md +27 -1
- package/docs/markdown/repositories-variables.md +6 -1
- package/docs/markdown/repositories.md +18 -8
- package/package.json +48 -107
- package/recipes/core/sous-skills/skills/about-liquid-templates/SKILL.tpl.md +4 -6
- package/recipes/core/sous-skills/skills/about-liquid-templates/references/liquid-filters.tpl.md +2 -0
- package/recipes/core/sous-skills/skills/about-sous/SKILL.tpl.md +10 -9
- package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +35 -14
- package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +51 -20
- package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
- package/sous.config.schema.json +351 -132
- package/bin/run.js +0 -51
- package/src/base-command.ts +0 -457
- package/src/commands/build.ts +0 -181
- package/src/commands/clear.ts +0 -79
- package/src/commands/compile.ts +0 -155
- package/src/commands/config/get.ts +0 -110
- package/src/commands/config/show.ts +0 -32
- package/src/commands/config/validate.ts +0 -53
- package/src/commands/help.ts +0 -46
- package/src/commands/init.ts +0 -289
- package/src/commands/launch.ts +0 -139
- package/src/commands/lock/rebuild.ts +0 -239
- package/src/commands/lock/show.ts +0 -115
- package/src/commands/namespace/list.ts +0 -138
- package/src/commands/namespace/show.ts +0 -130
- package/src/commands/prune.ts +0 -43
- package/src/commands/recipe/list.ts +0 -112
- package/src/commands/recipe/show.ts +0 -439
- package/src/commands/repo/add.ts +0 -145
- package/src/commands/repo/contribute.ts +0 -604
- package/src/commands/repo/gc.ts +0 -173
- package/src/commands/repo/init.ts +0 -136
- package/src/commands/repo/link.ts +0 -899
- package/src/commands/repo/list.ts +0 -236
- package/src/commands/repo/release.ts +0 -685
- package/src/commands/repo/remove.ts +0 -194
- package/src/commands/repo/search.ts +0 -243
- package/src/commands/repo/submit.ts +0 -341
- package/src/commands/repo/unlink.ts +0 -461
- package/src/commands/subscription/add.ts +0 -286
- package/src/commands/subscription/list.ts +0 -208
- package/src/commands/subscription/remove.ts +0 -182
- package/src/commands/subscription/update.ts +0 -216
- package/src/commands/update.ts +0 -178
- package/src/commands/vars/ask.ts +0 -417
- package/src/commands/vars/index.ts +0 -79
- package/src/commands/vars/list.ts +0 -67
- package/src/commands/vars/show.ts +0 -77
- package/src/config-command.ts +0 -30
- package/src/lib/build-preparation.ts +0 -128
- package/src/lib/build-service.ts +0 -580
- package/src/lib/config-discovery.ts +0 -378
- package/src/lib/config-inspect.ts +0 -145
- package/src/lib/config-kernel.mjs +0 -379
- package/src/lib/config-schema.ts +0 -401
- package/src/lib/env-file.ts +0 -328
- package/src/lib/env-local.ts +0 -212
- package/src/lib/errors.ts +0 -32
- package/src/lib/glob-pattern.ts +0 -24
- package/src/lib/include-resolver.ts +0 -523
- package/src/lib/interactive.ts +0 -165
- package/src/lib/markdown-compiler.ts +0 -947
- package/src/lib/package-info.ts +0 -25
- package/src/lib/pid-service.ts +0 -99
- package/src/lib/project-install.d.mts +0 -44
- package/src/lib/project-install.mjs +0 -234
- package/src/lib/project-scaffold/index.ts +0 -332
- package/src/lib/project-scaffold/templates.ts +0 -240
- package/src/lib/repos/catalog-display.ts +0 -215
- package/src/lib/repos/catalog-inputs.ts +0 -288
- package/src/lib/repos/catalog.ts +0 -842
- package/src/lib/repos/contribute.ts +0 -452
- package/src/lib/repos/core-recipe.ts +0 -105
- package/src/lib/repos/declarations.ts +0 -187
- package/src/lib/repos/defaults.ts +0 -175
- package/src/lib/repos/formats/common.ts +0 -440
- package/src/lib/repos/formats/index-file.ts +0 -253
- package/src/lib/repos/formats/links-map.ts +0 -98
- package/src/lib/repos/formats/lockfile.ts +0 -169
- package/src/lib/repos/formats/patterns.ts +0 -57
- package/src/lib/repos/formats/recipe-manifest.ts +0 -205
- package/src/lib/repos/formats/repo-manifest.ts +0 -97
- package/src/lib/repos/formats/store-entry.ts +0 -84
- package/src/lib/repos/formats/variable-definition.ts +0 -268
- package/src/lib/repos/freshness.ts +0 -264
- package/src/lib/repos/git-clone.ts +0 -850
- package/src/lib/repos/identity.ts +0 -89
- package/src/lib/repos/index.ts +0 -57
- package/src/lib/repos/links.ts +0 -356
- package/src/lib/repos/load-manifest.ts +0 -237
- package/src/lib/repos/lock-service.ts +0 -454
- package/src/lib/repos/locked-namespace-resolver.ts +0 -232
- package/src/lib/repos/locked-recipe-lookup.ts +0 -123
- package/src/lib/repos/locked-recipes.ts +0 -254
- package/src/lib/repos/managed-layer.ts +0 -422
- package/src/lib/repos/memories-hash-name.ts +0 -33
- package/src/lib/repos/namespace-resolver.ts +0 -527
- package/src/lib/repos/providers/base.ts +0 -268
- package/src/lib/repos/providers/git.ts +0 -245
- package/src/lib/repos/providers/github.ts +0 -589
- package/src/lib/repos/providers/gitlab.ts +0 -332
- package/src/lib/repos/providers/http.ts +0 -128
- package/src/lib/repos/providers/index-cache.ts +0 -421
- package/src/lib/repos/providers/index.ts +0 -106
- package/src/lib/repos/providers/local.ts +0 -410
- package/src/lib/repos/providers/provider.ts +0 -615
- package/src/lib/repos/recipe-config-layers.ts +0 -316
- package/src/lib/repos/recipe-key-matcher.ts +0 -62
- package/src/lib/repos/recipe-memories.ts +0 -178
- package/src/lib/repos/recipe-targets.ts +0 -236
- package/src/lib/repos/ref-search.ts +0 -46
- package/src/lib/repos/release/bump.ts +0 -161
- package/src/lib/repos/release/changelog.ts +0 -461
- package/src/lib/repos/release/git-state.ts +0 -460
- package/src/lib/repos/release/index-builder.ts +0 -1108
- package/src/lib/repos/release/index.ts +0 -19
- package/src/lib/repos/release/plan.ts +0 -521
- package/src/lib/repos/release/settle.ts +0 -154
- package/src/lib/repos/release/submissions.ts +0 -214
- package/src/lib/repos/release/submit-checkout.ts +0 -290
- package/src/lib/repos/release/submit-questions.ts +0 -153
- package/src/lib/repos/release/submit-service.ts +0 -898
- package/src/lib/repos/release/tags.ts +0 -239
- package/src/lib/repos/release/validate.ts +0 -463
- package/src/lib/repos/resolver.ts +0 -1028
- package/src/lib/repos/scaffold/index.ts +0 -245
- package/src/lib/repos/scaffold/templates.ts +0 -517
- package/src/lib/repos/seed.ts +0 -473
- package/src/lib/repos/store/contract.ts +0 -64
- package/src/lib/repos/store/hash.ts +0 -191
- package/src/lib/repos/store/recipe-store.ts +0 -599
- package/src/lib/repos/store/settings.ts +0 -58
- package/src/lib/repos/subscription-service.ts +0 -3859
- package/src/lib/repos/trust.ts +0 -447
- package/src/lib/repos/update-plan.ts +0 -235
- package/src/lib/self-update/index.ts +0 -14
- package/src/lib/self-update/installs.ts +0 -413
- package/src/lib/self-update/managers.ts +0 -126
- package/src/lib/self-update/plan.ts +0 -315
- package/src/lib/self-update/registry.ts +0 -99
- package/src/lib/self-update/run.ts +0 -506
- package/src/lib/self-update/versions.ts +0 -243
- package/src/lib/settings.ts +0 -1107
- package/src/lib/sous-home.ts +0 -104
- package/src/lib/state.ts +0 -167
- package/src/lib/vars/answers.ts +0 -192
- package/src/lib/vars/ask.ts +0 -1152
- package/src/lib/vars/definition-source.ts +0 -261
- package/src/lib/vars/display.ts +0 -231
- package/src/lib/vars/index.ts +0 -20
- package/src/lib/vars/ladder.ts +0 -282
- package/src/lib/vars/mappings.ts +0 -260
- package/src/lib/vars/names.ts +0 -94
- package/src/lib/vars/preanswers.ts +0 -395
- package/src/lib/vars/question-plan.ts +0 -241
- package/src/lib/vars/report.ts +0 -228
- package/src/lib/vars/safe-regex.ts +0 -235
- package/src/lib/vars/secrets.ts +0 -131
- package/src/lib/vars/validate.ts +0 -312
- package/src/lib/version-report.ts +0 -90
- package/src/lib/watch-loop.ts +0 -148
- package/src/lib/watch-service.ts +0 -116
- package/src/services/ref-resolver/container.ts +0 -107
- package/src/services/ref-resolver/dependency.ts +0 -60
- package/src/services/ref-resolver/format.ts +0 -197
- package/src/services/ref-resolver/glob.ts +0 -115
- package/src/services/ref-resolver/hash-names.ts +0 -155
- package/src/services/ref-resolver/index.ts +0 -55
- package/src/services/ref-resolver/injectable.ts +0 -36
- package/src/services/ref-resolver/location.ts +0 -62
- package/src/services/ref-resolver/lookups/cached-index-lookup.ts +0 -53
- package/src/services/ref-resolver/lookups/catalog-lookup.ts +0 -23
- package/src/services/ref-resolver/lookups/catalog-matcher.ts +0 -261
- package/src/services/ref-resolver/lookups/chained-lookup.ts +0 -27
- package/src/services/ref-resolver/lookups/env-var-lookup.ts +0 -65
- package/src/services/ref-resolver/lookups/fetched-index-lookup.ts +0 -150
- package/src/services/ref-resolver/lookups/recorded-dependency-lookup.ts +0 -62
- package/src/services/ref-resolver/lookups/ref-lookup.ts +0 -54
- package/src/services/ref-resolver/lookups/variable-lookup.ts +0 -123
- package/src/services/ref-resolver/named-refs.ts +0 -144
- package/src/services/ref-resolver/parser/env-var-splitter.ts +0 -27
- package/src/services/ref-resolver/parser/location-splitter.ts +0 -239
- package/src/services/ref-resolver/parser/name-path-splitter.ts +0 -200
- package/src/services/ref-resolver/parser/partial-ref.ts +0 -84
- package/src/services/ref-resolver/parser/query-splitter.ts +0 -61
- package/src/services/ref-resolver/parser/range-splitter.ts +0 -52
- package/src/services/ref-resolver/parser/ref-parser.ts +0 -115
- package/src/services/ref-resolver/parser/repo-qualifier-splitter.ts +0 -51
- package/src/services/ref-resolver/parts.ts +0 -115
- package/src/services/ref-resolver/pruners/command-line-pruner.ts +0 -47
- package/src/services/ref-resolver/pruners/include-pruner.ts +0 -66
- package/src/services/ref-resolver/pruners/manifest-pruner.ts +0 -130
- package/src/services/ref-resolver/pruners/ref-pruner.ts +0 -135
- package/src/services/ref-resolver/pruners/rule-helpers.ts +0 -44
- package/src/services/ref-resolver/pruners/stored-key-pruner.ts +0 -136
- package/src/services/ref-resolver/ref-helpers.ts +0 -62
- package/src/services/ref-resolver/ref-picker-service.ts +0 -204
- package/src/services/ref-resolver/ref-report.ts +0 -84
- package/src/services/ref-resolver/ref-resolve-arguments.ts +0 -47
- package/src/services/ref-resolver/ref-resolve-result.ts +0 -36
- package/src/services/ref-resolver/ref-resolver-service.ts +0 -323
- package/src/services/ref-resolver/source.ts +0 -48
- package/src/services/ref-resolver/tokens.ts +0 -25
- package/src/services/ref-resolver/types.ts +0 -155
- package/src/templating/filters/bullet-list.ts +0 -9
- package/src/templating/filters/index.ts +0 -8
- package/src/templating/init-liquid-engine.ts +0 -139
- package/src/templating/lib/glob-files.ts +0 -74
- package/src/templating/lib/import-export.ts +0 -32
- package/src/templating/lib/secret-scope.ts +0 -41
- package/src/templating/lib/tag-args.ts +0 -19
- package/src/templating/tags/exportScalarVarsJs.ts +0 -46
- package/src/templating/tags/getFiles.ts +0 -89
- package/src/templating/tags/index.ts +0 -14
- package/src/templating/tags/listFiles.ts +0 -54
- package/src/templating/tags/showVars.ts +0 -28
- package/src/utils/choice-prompt.ts +0 -143
- package/src/utils/command-errors.ts +0 -186
- package/src/utils/command-help.ts +0 -45
- package/src/utils/confirm-prompt.ts +0 -110
- package/src/utils/flags.ts +0 -177
- package/src/utils/formatting.ts +0 -849
- package/src/utils/prompts.ts +0 -53
- package/src/utils/sous-directory.ts +0 -245
- package/src/utils/table.ts +0 -603
- package/src/utils/value-prompt.ts +0 -119
|
@@ -1,523 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { globSync } from "glob";
|
|
4
|
-
import { minimatch } from "minimatch";
|
|
5
|
-
import { compareBytewise, hasGlob, type ViewFile } from "../services/ref-resolver/index.js";
|
|
6
|
-
import { expandHome } from "./config-discovery.js";
|
|
7
|
-
import { toGlobPattern } from "./glob-pattern.js";
|
|
8
|
-
import type {
|
|
9
|
-
NamespaceResolution,
|
|
10
|
-
NamespaceResolver,
|
|
11
|
-
RecipeRoot,
|
|
12
|
-
} from "./repos/namespace-resolver.js";
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* @include path resolution: aliases, `#` names, recipe references, globs,
|
|
16
|
-
* variable substitution and the ordered candidate search.
|
|
17
|
-
*
|
|
18
|
-
* An include path (the part after `@`) resolves to an ordered list of groups.
|
|
19
|
-
* The caller uses the first group that names at least one file, and gets every
|
|
20
|
-
* file in it; if none does, it errors listing every path tried.
|
|
21
|
-
*
|
|
22
|
-
* Resolution pipeline for a raw path P (with leading `@` already stripped):
|
|
23
|
-
* 1. Substitute ${vars} in P, then expand a leading `~/` to the home
|
|
24
|
-
* directory (the `~` sigil with nothing but a separator after it names no
|
|
25
|
-
* namespace, so `@~/notes/x.md` is unambiguous). If the result is
|
|
26
|
-
* absolute, it is the sole candidate (`@${sousRootPath}/x.md`).
|
|
27
|
-
* 2. Split the first segment (up to the first `/` or `:`) as the alias key,
|
|
28
|
-
* the remainder as `rest`. If the key is a registered alias, push
|
|
29
|
-
* join(base, rest) for EACH base in the alias's ordered array. A `#name`
|
|
30
|
-
* (`#project`) is registered in the alias map by the `#` name registry.
|
|
31
|
-
* 3. If P begins with `~` and holds a `/`, and a namespace resolver was
|
|
32
|
-
* supplied, hand the reference (P without the `~`) to it. The resolver
|
|
33
|
-
* reads it with the ref resolver service: `namespace/recipe/path`, any of
|
|
34
|
-
* the three possibly a glob, optionally with a `repo:` qualifier.
|
|
35
|
-
* 4. Always push the relative candidate: join(baseDir, P), the FULL path
|
|
36
|
-
* including the first segment. This lets an alias augment a real relative
|
|
37
|
-
* directory of the same name (e.g. `@stuff/x` tries the alias bases, then
|
|
38
|
-
* `./stuff/x`).
|
|
39
|
-
* 5. Follow every NON-glob candidate with its `.tpl.` twin: `x.md` is
|
|
40
|
-
* followed by `x.tpl.md`, and `x.tpl.md` by `x.md`. The literal spelling
|
|
41
|
-
* is always tried first, and the twin comes right after it, so an alias
|
|
42
|
-
* base still beats the relative fallback.
|
|
43
|
-
*
|
|
44
|
-
* Any path may use glob syntax (`*`, `**`, `?`, `[..]`, `{a,b}`) after
|
|
45
|
-
* substitution. A glob names every file it matches, in bytewise path order, and
|
|
46
|
-
* a `~` glob also matches recipes (`@~*` + `/*` + `/memories/*.md`).
|
|
47
|
-
*
|
|
48
|
-
* A key WITHOUT a sigil never reaches the namespace resolver: a bare `@path` is
|
|
49
|
-
* always a relative path or a declared alias, so include lines never masquerade
|
|
50
|
-
* as filesystem paths.
|
|
51
|
-
*
|
|
52
|
-
* Aliases whose names begin with `~` or `#` are reserved: `~` for the home
|
|
53
|
-
* directory and recipe namespaces, `#` for names sous or a plugin registers.
|
|
54
|
-
* The primary separator is `/` (`@alias/path`); `:` is accepted as an
|
|
55
|
-
* equivalent for an alias (`@alias:path`).
|
|
56
|
-
*/
|
|
57
|
-
|
|
58
|
-
/** An alias maps a name to an ordered list of absolute base directories. */
|
|
59
|
-
export type AliasMap = Record<string, string[]>;
|
|
60
|
-
|
|
61
|
-
/** The files each view lists (`#memories` and so on), keyed by the name with its `#`. */
|
|
62
|
-
export type ViewMap = Record<string, ViewFile[]>;
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Substitute ${varName} references in a string from a scope. Unknown
|
|
66
|
-
* references are left untouched (matches settings.substituteVars behavior).
|
|
67
|
-
*
|
|
68
|
-
* @param str - The string to substitute into.
|
|
69
|
-
* @param scope - Map of variable names to values.
|
|
70
|
-
* @returns The substituted string.
|
|
71
|
-
*/
|
|
72
|
-
export function substituteVars(str: string, scope: Record<string, string>): string {
|
|
73
|
-
return str.replace(/\$\{([^}]+)\}/g, (match, name: string) => scope[name] ?? match);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** The marker that makes a file a template, always right before the final extension. */
|
|
77
|
-
const TEMPLATE_MARKER = ".tpl";
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* The `.tpl.` twin of a path: `x.md` gives `x.tpl.md`, `x.tpl.md` gives `x.md`,
|
|
81
|
-
* and a path with no extension has no twin.
|
|
82
|
-
*
|
|
83
|
-
* @param filePath - Any path, absolute or not.
|
|
84
|
-
* @returns The twin, or undefined when there is none.
|
|
85
|
-
*/
|
|
86
|
-
export function templateTwin(filePath: string): string | undefined {
|
|
87
|
-
const dir = path.dirname(filePath);
|
|
88
|
-
const base = path.basename(filePath);
|
|
89
|
-
const ext = path.extname(base);
|
|
90
|
-
if (ext === "" || ext === base) return undefined;
|
|
91
|
-
const stem = base.slice(0, -ext.length);
|
|
92
|
-
const twin = stem.endsWith(TEMPLATE_MARKER)
|
|
93
|
-
? `${stem.slice(0, -TEMPLATE_MARKER.length)}${ext}`
|
|
94
|
-
: `${stem}${TEMPLATE_MARKER}${ext}`;
|
|
95
|
-
return path.join(dir, twin);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** Every candidate followed by its `.tpl.` twin, de-duplicated, order kept. */
|
|
99
|
-
function withTemplateTwins(candidates: readonly string[]): string[] {
|
|
100
|
-
const out: string[] = [];
|
|
101
|
-
for (const candidate of candidates) {
|
|
102
|
-
out.push(candidate);
|
|
103
|
-
const twin = templateTwin(candidate);
|
|
104
|
-
if (twin !== undefined) out.push(twin);
|
|
105
|
-
}
|
|
106
|
-
return [...new Set(out)];
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Split an include path into its leading alias key and the remainder. The key
|
|
111
|
-
* is the run of characters up to the first `/` or `:` separator.
|
|
112
|
-
*
|
|
113
|
-
* @param p - The include path (no leading `@`).
|
|
114
|
-
* @returns `{ key, rest }`; `rest` has no leading separator.
|
|
115
|
-
*/
|
|
116
|
-
export function splitAliasKey(p: string): { key: string; rest: string } {
|
|
117
|
-
const m = p.match(/^([^/:]+)[/:]([\s\S]*)$/);
|
|
118
|
-
if (!m) return { key: p, rest: "" };
|
|
119
|
-
return { key: m[1], rest: m[2] };
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** Options shared by {@link resolveInclude} and {@link resolveIncludeFiles}. */
|
|
123
|
-
export type IncludeResolveOptions = {
|
|
124
|
-
/** The resolved alias map (name → ordered base dirs); `#` names are in it too. */
|
|
125
|
-
aliases?: AliasMap;
|
|
126
|
-
/**
|
|
127
|
-
* The files each view lists. An include of `#name/<path or glob>` for a name
|
|
128
|
-
* here selects among those virtual paths and names the real files behind the
|
|
129
|
-
* ones that match, in the listed order.
|
|
130
|
-
*/
|
|
131
|
-
views?: ViewMap;
|
|
132
|
-
/** Variable scope for ${var} substitution. */
|
|
133
|
-
scope?: Record<string, string>;
|
|
134
|
-
/** Directory of the including file (for the relative candidate). */
|
|
135
|
-
baseDir: string;
|
|
136
|
-
/** Resolver consulted for a `~namespace` first segment; omitted means namespaces are unavailable. */
|
|
137
|
-
namespaceResolver?: NamespaceResolver;
|
|
138
|
-
/**
|
|
139
|
-
* Absolute path of the including file, handed to the namespace resolver so it
|
|
140
|
-
* can tell whether the include comes from inside a recipe. Defaults to
|
|
141
|
-
* `baseDir` for callers that only know the directory.
|
|
142
|
-
*/
|
|
143
|
-
fromFile?: string;
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
/** A `~` reference that produced no usable candidates, kept for error reporting. */
|
|
147
|
-
export type NamespaceIssue = {
|
|
148
|
-
/** The reference with its `~` sigil taken off. */
|
|
149
|
-
reference: string;
|
|
150
|
-
/** The file (or directory) that performed the include. */
|
|
151
|
-
fromFile: string;
|
|
152
|
-
/** What the resolver returned. */
|
|
153
|
-
resolution: NamespaceResolution;
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* One place an include may be found. A group is found when any of its paths
|
|
158
|
-
* names at least one file, and then it supplies ALL of them: a plain include
|
|
159
|
-
* has one path per group, a glob over recipes has one per recipe.
|
|
160
|
-
*/
|
|
161
|
-
export type IncludeGroup = {
|
|
162
|
-
/** Absolute paths, or glob patterns when `glob` is true. */
|
|
163
|
-
paths: string[];
|
|
164
|
-
/** True when the paths are patterns to expand. */
|
|
165
|
-
glob: boolean;
|
|
166
|
-
/**
|
|
167
|
-
* The recipe each path belongs to (same length and order as `paths`), when
|
|
168
|
-
* they came from a `~` reference. Every file a path names must have a real
|
|
169
|
-
* path inside that recipe's real directory; one that does not (a link
|
|
170
|
-
* leading out) is refused.
|
|
171
|
-
*/
|
|
172
|
-
roots?: RecipeRoot[];
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
/** The full result of resolving one include path. */
|
|
176
|
-
export type IncludeResolution = {
|
|
177
|
-
/** Every group, in the order they are tried; the first that names a file wins. */
|
|
178
|
-
groups: IncludeGroup[];
|
|
179
|
-
/** Every path or pattern, flattened, in order and de-duplicated: what an error lists as tried. */
|
|
180
|
-
candidates: string[];
|
|
181
|
-
/** True when the include path holds glob syntax. */
|
|
182
|
-
glob: boolean;
|
|
183
|
-
/**
|
|
184
|
-
* Present when a `~` reference could not be satisfied. The groups are still
|
|
185
|
-
* usable (they hold the alias and relative fallbacks); this only explains
|
|
186
|
-
* what went wrong with the reference.
|
|
187
|
-
*/
|
|
188
|
-
namespaceIssue?: NamespaceIssue;
|
|
189
|
-
/** Present when the path starts with a `#` name nothing registered. */
|
|
190
|
-
hashIssue?: string;
|
|
191
|
-
/** True when the path names a view; a glob over a view may match nothing without being an error. */
|
|
192
|
-
view?: boolean;
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
/** The files an include names, and how the search went. */
|
|
196
|
-
export type IncludeFiles = {
|
|
197
|
-
/** Every file the include names, each once, in the order they are included. */
|
|
198
|
-
files: string[];
|
|
199
|
-
/** Everything that was tried. */
|
|
200
|
-
candidates: string[];
|
|
201
|
-
/** True when the include path holds glob syntax. */
|
|
202
|
-
glob: boolean;
|
|
203
|
-
/** Why a `~` reference failed, when it did. */
|
|
204
|
-
namespaceIssue?: NamespaceIssue;
|
|
205
|
-
/** Why a `#` name failed, when it did. */
|
|
206
|
-
hashIssue?: string;
|
|
207
|
-
/** True when the path names a view. */
|
|
208
|
-
view?: boolean;
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
/** The `?name=value&...` query an include line may end with, after its `.md`. */
|
|
212
|
-
const QUERY_SUFFIX = /^(.*\.md)(\?[^?\s]*=\S*)$/;
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Splits the query off an include path: `a/b.md?name=value` gives the path
|
|
216
|
-
* `a/b.md` and the query `?name=value`.
|
|
217
|
-
*
|
|
218
|
-
* @param raw - The include path, without its `@`.
|
|
219
|
-
*/
|
|
220
|
-
export function splitIncludeQuery(raw: string): { path: string; query: string } {
|
|
221
|
-
const match = QUERY_SUFFIX.exec(raw);
|
|
222
|
-
return match === null ? { path: raw, query: "" } : { path: match[1]!, query: match[2]! };
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/** The group of one literal path followed by its `.tpl.` twin. */
|
|
226
|
-
function literalGroups(candidate: string, root?: RecipeRoot): IncludeGroup[] {
|
|
227
|
-
return withTemplateTwins([candidate]).map((entry) => ({
|
|
228
|
-
paths: [entry],
|
|
229
|
-
glob: false,
|
|
230
|
-
...(root === undefined ? {} : { roots: [root] }),
|
|
231
|
-
}));
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Whether a file's real path sits inside a directory's real path, so a link
|
|
236
|
-
* cannot lead out of it. A path that cannot be resolved (it does not exist) is
|
|
237
|
-
* not an escape.
|
|
238
|
-
*
|
|
239
|
-
* @param file - An absolute file path.
|
|
240
|
-
* @param dir - An absolute directory path.
|
|
241
|
-
*/
|
|
242
|
-
export function realPathInside(file: string, dir: string): boolean {
|
|
243
|
-
let realFile: string;
|
|
244
|
-
let realDir: string;
|
|
245
|
-
try {
|
|
246
|
-
realFile = fs.realpathSync(file);
|
|
247
|
-
realDir = fs.realpathSync(dir);
|
|
248
|
-
} catch {
|
|
249
|
-
return true;
|
|
250
|
-
}
|
|
251
|
-
return realFile === realDir || realFile.startsWith(realDir + path.sep);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Resolve an include path to its ordered groups plus any diagnostic. Nothing is
|
|
256
|
-
* read from disk. Use this when the caller wants to report WHY a `~` or `#`
|
|
257
|
-
* reference failed; {@link resolveIncludeFiles} reads the disk and returns the
|
|
258
|
-
* files.
|
|
259
|
-
*
|
|
260
|
-
* @param rawPath - The include path with the leading `@` already stripped.
|
|
261
|
-
* @param opts - Aliases, variable scope, including directory and optional namespace resolver.
|
|
262
|
-
* @returns The groups, the flat candidate list and, when a reference failed, the reason.
|
|
263
|
-
*/
|
|
264
|
-
export function resolveInclude(rawPath: string, opts: IncludeResolveOptions): IncludeResolution {
|
|
265
|
-
const aliases = opts.aliases ?? {};
|
|
266
|
-
const scope = opts.scope ?? {};
|
|
267
|
-
const withQuery = expandHome(substituteVars(rawPath, scope));
|
|
268
|
-
const substituted = splitIncludeQuery(withQuery).path;
|
|
269
|
-
const glob = hasGlob(substituted);
|
|
270
|
-
|
|
271
|
-
const finish = (
|
|
272
|
-
groups: IncludeGroup[],
|
|
273
|
-
extra: Pick<IncludeResolution, "namespaceIssue" | "hashIssue" | "view"> = {}
|
|
274
|
-
): IncludeResolution => ({
|
|
275
|
-
groups,
|
|
276
|
-
candidates: [...new Set(groups.flatMap((group) => group.paths))],
|
|
277
|
-
glob,
|
|
278
|
-
...extra,
|
|
279
|
-
});
|
|
280
|
-
const single = (candidate: string): IncludeGroup[] =>
|
|
281
|
-
glob ? [{ paths: [candidate], glob: true }] : literalGroups(candidate);
|
|
282
|
-
|
|
283
|
-
// 1. Substituted (or home-expanded) to an absolute path: that, and its twin.
|
|
284
|
-
if (path.isAbsolute(substituted)) {
|
|
285
|
-
return finish(single(path.normalize(substituted)));
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// A view lists virtual files; the include selects among them by glob or by
|
|
289
|
-
// exact virtual path and names the real files behind the matches.
|
|
290
|
-
const views = opts.views ?? {};
|
|
291
|
-
const viewKey = splitAliasKey(substituted);
|
|
292
|
-
if (viewKey.key.startsWith("#") && Object.prototype.hasOwnProperty.call(views, viewKey.key)) {
|
|
293
|
-
const includingRecipe = opts.namespaceResolver?.includingRecipe?.(opts.fromFile ?? opts.baseDir) ?? null;
|
|
294
|
-
if (includingRecipe !== null) {
|
|
295
|
-
return finish([], {
|
|
296
|
-
view: true,
|
|
297
|
-
hashIssue:
|
|
298
|
-
`The recipe "${includingRecipe}" may not include "${viewKey.key}", because a view lists ` +
|
|
299
|
-
`other recipes' files.\n` +
|
|
300
|
-
`A recipe includes another recipe's files with "@~namespace/recipe/..." and a ` +
|
|
301
|
-
`dependency declared in its manifest ("depends" or "subscribes").`,
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
const listed = views[viewKey.key]!;
|
|
305
|
-
const wanted = viewKey.rest;
|
|
306
|
-
const matched = listed.filter((entry) =>
|
|
307
|
-
glob ? minimatch(entry.path, wanted, { dot: true }) : entry.path === wanted
|
|
308
|
-
);
|
|
309
|
-
const issue =
|
|
310
|
-
!glob && matched.length === 0
|
|
311
|
-
? `The view "${viewKey.key}" lists no file at "${wanted}".\n` +
|
|
312
|
-
(listed.length > 0
|
|
313
|
-
? `It lists:\n${listed.map((entry) => ` ${viewKey.key}/${entry.path}`).join("\n")}`
|
|
314
|
-
: "It lists no files in this project.")
|
|
315
|
-
: undefined;
|
|
316
|
-
return finish(
|
|
317
|
-
matched.length > 0 ? [{ paths: [...new Set(matched.map((entry) => entry.file))], glob: false }] : [],
|
|
318
|
-
{ view: true, ...(issue === undefined ? {} : { hashIssue: issue }) }
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const groups: IncludeGroup[] = [];
|
|
323
|
-
let namespaceIssue: NamespaceIssue | undefined;
|
|
324
|
-
let hashIssue: string | undefined;
|
|
325
|
-
|
|
326
|
-
// 2. Alias bases (ordered), if the first segment is a registered alias. A
|
|
327
|
-
// `#` name is an alias too: the registry puts every `#name` in the map.
|
|
328
|
-
const { key, rest } = splitAliasKey(substituted);
|
|
329
|
-
if (key && Object.prototype.hasOwnProperty.call(aliases, key)) {
|
|
330
|
-
for (const base of aliases[key]!) groups.push(...single(path.resolve(base, rest)));
|
|
331
|
-
} else if (key.startsWith("#") && rest.length > 0) {
|
|
332
|
-
const known = [...Object.keys(aliases), ...Object.keys(views)].filter((name) =>
|
|
333
|
-
name.startsWith("#")
|
|
334
|
-
);
|
|
335
|
-
hashIssue =
|
|
336
|
-
`There is no built-in name "${key}" available here.\n` +
|
|
337
|
-
(known.length > 0
|
|
338
|
-
? `Available names: ${known.sort().join(", ")}.`
|
|
339
|
-
: "No '#' names are available in this project.");
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// 3. A recipe reference: a `~` sigil, then namespace, recipe and path. Only
|
|
343
|
-
// when a resolver was supplied and a `/` follows, so a file that is
|
|
344
|
-
// simply named `~notes.md` is still a relative path.
|
|
345
|
-
if (
|
|
346
|
-
opts.namespaceResolver &&
|
|
347
|
-
substituted.startsWith("~") &&
|
|
348
|
-
substituted.length > 1 &&
|
|
349
|
-
substituted.includes("/")
|
|
350
|
-
) {
|
|
351
|
-
const reference = withQuery.slice(1);
|
|
352
|
-
const fromFile = opts.fromFile ?? opts.baseDir;
|
|
353
|
-
const resolution = opts.namespaceResolver.resolve({ reference, fromFile });
|
|
354
|
-
|
|
355
|
-
if (resolution.kind === "candidates" && resolution.glob === true) {
|
|
356
|
-
groups.push({
|
|
357
|
-
paths: resolution.candidates.map((c) => c),
|
|
358
|
-
glob: true,
|
|
359
|
-
...(resolution.roots === undefined ? {} : { roots: resolution.roots }),
|
|
360
|
-
});
|
|
361
|
-
} else if (resolution.kind === "candidates" && resolution.candidates.length > 0) {
|
|
362
|
-
resolution.candidates.forEach((candidate, index) => {
|
|
363
|
-
groups.push(...literalGroups(path.normalize(candidate), resolution.roots?.[index]));
|
|
364
|
-
});
|
|
365
|
-
} else if (resolution.kind !== "candidates") {
|
|
366
|
-
namespaceIssue = { reference, fromFile, resolution };
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// 4. Relative fallback: the FULL substituted path under the including dir.
|
|
371
|
-
groups.push(...single(path.resolve(opts.baseDir, substituted)));
|
|
372
|
-
|
|
373
|
-
return finish(groups, {
|
|
374
|
-
...(namespaceIssue === undefined ? {} : { namespaceIssue }),
|
|
375
|
-
...(hashIssue === undefined ? {} : { hashIssue }),
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* Expands a glob pattern to the files it matches, sorted by path bytewise so
|
|
381
|
-
* the order is the same on every machine.
|
|
382
|
-
*
|
|
383
|
-
* @param pattern - An absolute glob pattern.
|
|
384
|
-
*/
|
|
385
|
-
function expandPattern(pattern: string): string[] {
|
|
386
|
-
return globSync(toGlobPattern(pattern), { absolute: true, nodir: true, dot: true }).sort(
|
|
387
|
-
compareBytewise
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* Resolve an include path to the files it names. The groups are tried in
|
|
393
|
-
* order and the first that names any file supplies all of them: a plain path
|
|
394
|
-
* is one file, a glob is every file it matches in sorted path order.
|
|
395
|
-
*
|
|
396
|
-
* @param rawPath - The include path with the leading `@` already stripped.
|
|
397
|
-
* @param opts - Aliases, variable scope, including directory and optional namespace resolver.
|
|
398
|
-
* @returns The files (empty when nothing was found) and what was tried.
|
|
399
|
-
*/
|
|
400
|
-
export function resolveIncludeFiles(rawPath: string, opts: IncludeResolveOptions): IncludeFiles {
|
|
401
|
-
const resolution = resolveInclude(rawPath, opts);
|
|
402
|
-
const out: IncludeFiles = {
|
|
403
|
-
files: [],
|
|
404
|
-
candidates: resolution.candidates,
|
|
405
|
-
glob: resolution.glob,
|
|
406
|
-
...(resolution.namespaceIssue === undefined ? {} : { namespaceIssue: resolution.namespaceIssue }),
|
|
407
|
-
...(resolution.hashIssue === undefined ? {} : { hashIssue: resolution.hashIssue }),
|
|
408
|
-
...(resolution.view === true ? { view: true } : {}),
|
|
409
|
-
};
|
|
410
|
-
for (const group of resolution.groups) {
|
|
411
|
-
const found = group.glob
|
|
412
|
-
? group.paths.flatMap(expandPattern)
|
|
413
|
-
: group.paths.filter((candidate) => fs.existsSync(candidate));
|
|
414
|
-
if (group.roots !== undefined) {
|
|
415
|
-
const roots = group.roots;
|
|
416
|
-
const leaving = group.paths.flatMap((entry, index) => {
|
|
417
|
-
const root = roots[index];
|
|
418
|
-
if (root === undefined) return [];
|
|
419
|
-
const files = group.glob ? expandPattern(entry) : fs.existsSync(entry) ? [entry] : [];
|
|
420
|
-
return files.filter((file) => !realPathInside(file, root.dir)).map(() => root);
|
|
421
|
-
});
|
|
422
|
-
const { recipe } = leaving[0] ?? { recipe: "" };
|
|
423
|
-
if (leaving.length > 0) {
|
|
424
|
-
out.files = [];
|
|
425
|
-
out.namespaceIssue = {
|
|
426
|
-
reference: rawPath.replace(/^~/, ""),
|
|
427
|
-
fromFile: opts.fromFile ?? opts.baseDir,
|
|
428
|
-
resolution: { kind: "escapes-recipe", recipe, reference: rawPath.replace(/^~/, "") },
|
|
429
|
-
};
|
|
430
|
-
return out;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
if (found.length > 0) {
|
|
434
|
-
out.files = [...new Set(found)];
|
|
435
|
-
return out;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
return out;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* Compute the ordered list of candidate absolute paths for an include.
|
|
443
|
-
*
|
|
444
|
-
* @param rawPath - The include path with the leading `@` already stripped.
|
|
445
|
-
* @param opts - Aliases, variable scope, including directory and optional namespace resolver.
|
|
446
|
-
* @returns Ordered, de-duplicated absolute candidate paths (patterns, for a glob).
|
|
447
|
-
*/
|
|
448
|
-
export function resolveIncludeCandidates(rawPath: string, opts: IncludeResolveOptions): string[] {
|
|
449
|
-
return resolveInclude(rawPath, opts).candidates;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
/**
|
|
453
|
-
* Expand a leading alias segment in a path or glob pattern to one candidate
|
|
454
|
-
* per alias base, in the alias's base order. Used for config entry paths
|
|
455
|
-
* (`entryGlob`/watch patterns), where, unlike @include resolution, there is
|
|
456
|
-
* no including file to supply a relative fallback, so a non-alias path is
|
|
457
|
-
* returned unchanged as the sole candidate.
|
|
458
|
-
*
|
|
459
|
-
* @param p - The path or glob pattern (already ${var}-substituted).
|
|
460
|
-
* @param aliases - The resolved alias map (name → ordered base dirs).
|
|
461
|
-
* @returns Ordered candidate paths/patterns; `[p]` when no alias applies.
|
|
462
|
-
*/
|
|
463
|
-
export function resolveAliasPrefix(p: string, aliases: AliasMap): string[] {
|
|
464
|
-
if (path.isAbsolute(p)) return [p];
|
|
465
|
-
const { key, rest } = splitAliasKey(p);
|
|
466
|
-
if (!key || !Object.prototype.hasOwnProperty.call(aliases, key)) return [p];
|
|
467
|
-
return aliases[key].map((base) => path.resolve(base, rest));
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
/**
|
|
471
|
-
* Build the resolved alias map from built-in aliases and user-defined
|
|
472
|
-
* `_aliases` entries.
|
|
473
|
-
*
|
|
474
|
-
* Precedence (later prepends to earlier so user/project entries are tried
|
|
475
|
-
* FIRST, then fall through to built-in bases):
|
|
476
|
-
* built-ins → root _aliases → project _aliases
|
|
477
|
-
*
|
|
478
|
-
* Each user alias value may be a single string or an array of strings, and
|
|
479
|
-
* each is run through ${var} substitution against `scope`.
|
|
480
|
-
*
|
|
481
|
-
* User aliases may NOT use names beginning with `~` or `#` (reserved);
|
|
482
|
-
* such entries are rejected via `onError` and ignored.
|
|
483
|
-
*
|
|
484
|
-
* @param opts.builtIns - Built-in alias map (already absolute; `#`-prefixed names).
|
|
485
|
-
* @param opts.userAliases - Ordered list of user `_aliases` blocks (root, then project).
|
|
486
|
-
* @param opts.scope - Variable scope for substituting alias values.
|
|
487
|
-
* @param opts.onError - Called with a message for each rejected/invalid entry.
|
|
488
|
-
* @returns The merged alias map.
|
|
489
|
-
*/
|
|
490
|
-
export function buildAliasMap(opts: {
|
|
491
|
-
builtIns?: AliasMap;
|
|
492
|
-
userAliases?: Array<Record<string, string | string[]> | undefined>;
|
|
493
|
-
scope?: Record<string, string>;
|
|
494
|
-
onError?: (message: string) => void;
|
|
495
|
-
}): AliasMap {
|
|
496
|
-
const scope = opts.scope ?? {};
|
|
497
|
-
const out: AliasMap = {};
|
|
498
|
-
|
|
499
|
-
// Start with built-ins.
|
|
500
|
-
for (const [name, bases] of Object.entries(opts.builtIns ?? {})) {
|
|
501
|
-
out[name] = [...bases];
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// Apply user blocks in order; each PREPENDS its bases so user entries win
|
|
505
|
-
// but still fall through to any built-in bases of the same name.
|
|
506
|
-
for (const block of opts.userAliases ?? []) {
|
|
507
|
-
if (!block) continue;
|
|
508
|
-
for (const [name, value] of Object.entries(block)) {
|
|
509
|
-
if (name.startsWith("~") || name.startsWith("#")) {
|
|
510
|
-
opts.onError?.(
|
|
511
|
-
`Alias "${name}" is invalid: names beginning with "~" or "#" are reserved ` +
|
|
512
|
-
`("~" for the home directory and recipe namespaces, "#" for built-in names).`
|
|
513
|
-
);
|
|
514
|
-
continue;
|
|
515
|
-
}
|
|
516
|
-
const values = Array.isArray(value) ? value : [value];
|
|
517
|
-
const bases = values.map((v) => substituteVars(v, scope));
|
|
518
|
-
out[name] = [...bases, ...(out[name] ?? [])];
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
return out;
|
|
523
|
-
}
|
package/src/lib/interactive.ts
DELETED
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The one rule for "may sous ask a question right now?".
|
|
3
|
-
*
|
|
4
|
-
* Every prompt in sous (the trust question, the subscribe confirmation, the
|
|
5
|
-
* choice between candidate refs, a variable question) is gated by this single
|
|
6
|
-
* predicate, so a run either can ask all of them or none of them. Three things
|
|
7
|
-
* can take the terminal away:
|
|
8
|
-
*
|
|
9
|
-
* - the global `--non-interactive` flag, which says so outright;
|
|
10
|
-
* - a truthy `CI` environment variable, which every continuous integration
|
|
11
|
-
* runner sets and which means no human is watching;
|
|
12
|
-
* - stdin or stdout not being a terminal, which is what piping or scripting
|
|
13
|
-
* a command looks like from in here.
|
|
14
|
-
*
|
|
15
|
-
* When a prompt cannot be shown, the run fails rather than guessing, and the
|
|
16
|
-
* failure names both the question that could not be asked and the flag (or the
|
|
17
|
-
* environment variables) that would have answered it ahead of time. Every
|
|
18
|
-
* confirmation in sous is answered by one shared flag, `--yes` (also `-y`,
|
|
19
|
-
* `--force`, and `--trust` on the commands that trust a repository), so a
|
|
20
|
-
* remedy names that flag and lists its other spellings once. The command layer
|
|
21
|
-
* prints the command's own help underneath that error, so the caller can see
|
|
22
|
-
* every flag without going looking; `sous help <command>` prints the same
|
|
23
|
-
* screen on demand.
|
|
24
|
-
*
|
|
25
|
-
* Every input is injectable, so a test can describe a terminal, a pipe or a CI
|
|
26
|
-
* runner without touching the real process.
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
import { ConfigError } from "./errors.js";
|
|
30
|
-
|
|
31
|
-
/** Everything the rule reads. Each field defaults to the real process. */
|
|
32
|
-
export type InteractiveInputs = {
|
|
33
|
-
/** The command line, scanned for `--non-interactive`. Defaults to `process.argv`. */
|
|
34
|
-
argv?: string[];
|
|
35
|
-
/** The environment, read for `CI`. Defaults to `process.env`. */
|
|
36
|
-
env?: NodeJS.ProcessEnv;
|
|
37
|
-
/** Whether stdin is a terminal. Defaults to the real stdin. */
|
|
38
|
-
stdinIsTTY?: boolean;
|
|
39
|
-
/** Whether stdout is a terminal. Defaults to the real stdout. */
|
|
40
|
-
stdoutIsTTY?: boolean;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
/** The global flag that turns every prompt into an error. */
|
|
44
|
-
export const NON_INTERACTIVE_FLAG = "--non-interactive";
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Values of `CI` that mean "not set". Everything else counts as set, because
|
|
48
|
-
* runners spell it `1`, `true` and `yes` in roughly equal measure and a run
|
|
49
|
-
* that guesses wrong hangs forever waiting on a question nobody can see.
|
|
50
|
-
*/
|
|
51
|
-
const FALSY_CI_VALUES = new Set(["", "0", "false", "no", "off"]);
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* True when sous is attached to a terminal in both directions, was not told to
|
|
55
|
-
* stay quiet, and is not running inside a continuous integration runner.
|
|
56
|
-
*
|
|
57
|
-
* @param inputs - Overrides for the command line, environment and streams.
|
|
58
|
-
*/
|
|
59
|
-
export function isInteractive(inputs: InteractiveInputs = {}): boolean {
|
|
60
|
-
return nonInteractiveReason(inputs) === undefined;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Why this run cannot ask a question, as a plain sentence, or undefined when it
|
|
65
|
-
* can. The reason is quoted in the error a blocked prompt raises, because "sous
|
|
66
|
-
* is not running where it can ask" is baffling on its own when the caller is
|
|
67
|
-
* sitting at a terminal and only `CI=1` in their shell made it true.
|
|
68
|
-
*
|
|
69
|
-
* @param inputs - Overrides for the command line, environment and streams.
|
|
70
|
-
*/
|
|
71
|
-
export function nonInteractiveReason(inputs: InteractiveInputs = {}): string | undefined {
|
|
72
|
-
const argv = inputs.argv ?? process.argv;
|
|
73
|
-
const env = inputs.env ?? process.env;
|
|
74
|
-
const stdinIsTTY = inputs.stdinIsTTY ?? process.stdin.isTTY === true;
|
|
75
|
-
const stdoutIsTTY = inputs.stdoutIsTTY ?? process.stdout.isTTY === true;
|
|
76
|
-
|
|
77
|
-
if (hasNonInteractiveFlag(argv)) {
|
|
78
|
-
return `the '${NON_INTERACTIVE_FLAG}' flag was passed`;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const ci = env.CI;
|
|
82
|
-
if (ci !== undefined && !FALSY_CI_VALUES.has(ci.trim().toLowerCase())) {
|
|
83
|
-
return `the 'CI' environment variable is set to '${ci}'`;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (!stdinIsTTY && !stdoutIsTTY) return "neither input nor output is a terminal";
|
|
87
|
-
if (!stdinIsTTY) return "input is not a terminal";
|
|
88
|
-
if (!stdoutIsTTY) return "output is not a terminal";
|
|
89
|
-
|
|
90
|
-
return undefined;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* True when the command line carries `--non-interactive`. Scanning stops at a
|
|
95
|
-
* bare `--`, exactly as the config-flag readers do: anything after it belongs to
|
|
96
|
-
* a launched tool, never to sous.
|
|
97
|
-
*
|
|
98
|
-
* @param argv - The raw command line.
|
|
99
|
-
*/
|
|
100
|
-
function hasNonInteractiveFlag(argv: string[]): boolean {
|
|
101
|
-
for (const arg of argv) {
|
|
102
|
-
if (arg === "--") return false;
|
|
103
|
-
if (arg === NON_INTERACTIVE_FLAG) return true;
|
|
104
|
-
}
|
|
105
|
-
return false;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* A ConfigError raised because a question could not be asked. It carries a flag
|
|
110
|
-
* telling the command layer to print the command's own help underneath it, so
|
|
111
|
-
* the caller sees every flag that could have answered the question.
|
|
112
|
-
*/
|
|
113
|
-
export class NonInteractiveError extends ConfigError {
|
|
114
|
-
/** Tells `BaseCommand` to print the command's help after this error. */
|
|
115
|
-
readonly showHelp = true;
|
|
116
|
-
|
|
117
|
-
constructor(message: string) {
|
|
118
|
-
super(message);
|
|
119
|
-
this.name = "NonInteractiveError";
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** True when an error asked for the command's help to be printed with it. */
|
|
124
|
-
export function wantsHelp(error: unknown): boolean {
|
|
125
|
-
return (
|
|
126
|
-
typeof error === "object" &&
|
|
127
|
-
error !== null &&
|
|
128
|
-
(error as { showHelp?: boolean }).showHelp === true
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** What a blocked prompt needs to describe itself. */
|
|
133
|
-
export type BlockedPrompt = {
|
|
134
|
-
/** The question that could not be asked, named as a person would name it. */
|
|
135
|
-
prompt: string;
|
|
136
|
-
/**
|
|
137
|
-
* How to answer it ahead of time: a flag, or the environment variables. A
|
|
138
|
-
* remedy names the flag's primary spelling, and lists its alternate spellings
|
|
139
|
-
* once, in one parenthetical; the alternates are defined in
|
|
140
|
-
* `utils/flags.ts` and the command's own help (printed underneath this error)
|
|
141
|
-
* lists them too.
|
|
142
|
-
*/
|
|
143
|
-
remedy: string;
|
|
144
|
-
/** Extra lines of context, such as the candidates that could not be chosen between. */
|
|
145
|
-
details?: string[];
|
|
146
|
-
/** Overrides for the command line, environment and streams. */
|
|
147
|
-
inputs?: InteractiveInputs;
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Builds the error a blocked prompt raises: what could not be asked, why sous
|
|
152
|
-
* could not ask it, and what would have answered it without a terminal.
|
|
153
|
-
*
|
|
154
|
-
* @param blocked - The prompt, the remedy and any extra context.
|
|
155
|
-
*/
|
|
156
|
-
export function nonInteractiveError(blocked: BlockedPrompt): NonInteractiveError {
|
|
157
|
-
const reason = nonInteractiveReason(blocked.inputs ?? {}) ?? "there is no terminal to ask on";
|
|
158
|
-
const lines = [
|
|
159
|
-
`Sous has to ask ${blocked.prompt}, and it is not running where it can ask.`,
|
|
160
|
-
` Why: ${reason}.`,
|
|
161
|
-
];
|
|
162
|
-
for (const detail of blocked.details ?? []) lines.push(` ${detail}`);
|
|
163
|
-
lines.push(` Answer it ahead of time: ${blocked.remedy}`);
|
|
164
|
-
return new NonInteractiveError(lines.join("\n"));
|
|
165
|
-
}
|