@sous-io/sous 0.2.37 → 1.0.2
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 +1094 -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
package/src/lib/env-file.ts
DELETED
|
@@ -1,328 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A line-preserving editor for `.sous/.env` and `.sous/.env.local`.
|
|
3
|
-
*
|
|
4
|
-
* These files belong to the user: they are hand-written, commented, ordered
|
|
5
|
-
* deliberately, and (for `.env`) committed. So sous edits them the way a
|
|
6
|
-
* careful person would. The file is parsed into a line model, exactly one line
|
|
7
|
-
* is rewritten or appended, and every other byte comes back out unchanged:
|
|
8
|
-
* comments, blank lines, ordering, quoting style and the `export ` prefix all
|
|
9
|
-
* survive.
|
|
10
|
-
*
|
|
11
|
-
* Comments are OUTPUT ONLY. sous writes a short generated header above each
|
|
12
|
-
* entry it appends, explaining where the value came from, and never reads a
|
|
13
|
-
* comment back or tries to keep one up to date. The header says as much, so
|
|
14
|
-
* nobody wonders whether editing it will confuse the tool.
|
|
15
|
-
*
|
|
16
|
-
* The parser here recognizes the same syntax `env-local.ts` reads, since these
|
|
17
|
-
* two modules are the write and read halves of one small format.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import fs from "node:fs";
|
|
21
|
-
import path from "node:path";
|
|
22
|
-
|
|
23
|
-
/** The key syntax accepted on an assignment line, matching the env file parser. */
|
|
24
|
-
const KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
25
|
-
|
|
26
|
-
/** One line of an env file, classified. */
|
|
27
|
-
export type EnvFileLine =
|
|
28
|
-
/** An empty or whitespace-only line. */
|
|
29
|
-
| { kind: "blank"; text: string }
|
|
30
|
-
/** A whole-line comment. */
|
|
31
|
-
| { kind: "comment"; text: string }
|
|
32
|
-
/** A `KEY=value` assignment, with everything needed to rewrite just the value. */
|
|
33
|
-
| {
|
|
34
|
-
kind: "assignment";
|
|
35
|
-
text: string;
|
|
36
|
-
/** Leading whitespace, preserved on rewrite. */
|
|
37
|
-
indent: string;
|
|
38
|
-
/** True when the line carried an `export ` prefix. */
|
|
39
|
-
exported: boolean;
|
|
40
|
-
/** The variable's name. */
|
|
41
|
-
key: string;
|
|
42
|
-
/** The value as written, before unquoting. */
|
|
43
|
-
rawValue: string;
|
|
44
|
-
/** A trailing ` # comment`, when the line had one, including its spacing. */
|
|
45
|
-
inlineComment: string;
|
|
46
|
-
}
|
|
47
|
-
/** Anything else: kept verbatim, never interpreted. */
|
|
48
|
-
| { kind: "other"; text: string };
|
|
49
|
-
|
|
50
|
-
/** A parsed env file: its lines, and how to put them back together. */
|
|
51
|
-
export interface EnvFileModel {
|
|
52
|
-
/** Every line, in order. */
|
|
53
|
-
lines: EnvFileLine[];
|
|
54
|
-
/** The line ending the file uses. */
|
|
55
|
-
eol: "\n" | "\r\n";
|
|
56
|
-
/** True when the file ended with a newline (or is empty and will). */
|
|
57
|
-
trailingNewline: boolean;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Splits an assignment line's right-hand side into the value and any trailing
|
|
62
|
-
* comment. Quoted values are scanned to their closing quote first, so a `#`
|
|
63
|
-
* inside quotes is part of the value.
|
|
64
|
-
*/
|
|
65
|
-
function splitValueAndComment(raw: string): { rawValue: string; inlineComment: string } {
|
|
66
|
-
const leadingSpaces = raw.length - raw.trimStart().length;
|
|
67
|
-
const body = raw.trimStart();
|
|
68
|
-
|
|
69
|
-
const quote = body.startsWith('"') ? '"' : body.startsWith("'") ? "'" : undefined;
|
|
70
|
-
if (quote !== undefined) {
|
|
71
|
-
for (let i = 1; i < body.length; i++) {
|
|
72
|
-
if (body[i] === "\\") {
|
|
73
|
-
i++;
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
if (body[i] === quote) {
|
|
77
|
-
return {
|
|
78
|
-
rawValue: raw.slice(0, leadingSpaces + i + 1),
|
|
79
|
-
inlineComment: raw.slice(leadingSpaces + i + 1),
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return { rawValue: raw, inlineComment: "" };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const commentAt = body.search(/\s#/);
|
|
87
|
-
if (commentAt === -1) return { rawValue: raw, inlineComment: "" };
|
|
88
|
-
|
|
89
|
-
// Walk back over every space before the comment, so the gap belongs to the
|
|
90
|
-
// comment and survives a rewrite of the value.
|
|
91
|
-
let start = leadingSpaces + commentAt;
|
|
92
|
-
while (start > 0 && /\s/.test(raw[start - 1]!)) start--;
|
|
93
|
-
return { rawValue: raw.slice(0, start), inlineComment: raw.slice(start) };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Parses env file text into a line model. Nothing is dropped: a line the
|
|
98
|
-
* parser does not recognize is kept verbatim as an `other` line.
|
|
99
|
-
*
|
|
100
|
-
* @param content - The file's contents.
|
|
101
|
-
*/
|
|
102
|
-
export function parseEnvFile(content: string): EnvFileModel {
|
|
103
|
-
const eol: "\n" | "\r\n" = content.includes("\r\n") ? "\r\n" : "\n";
|
|
104
|
-
const trailingNewline = content.length === 0 || content.endsWith("\n");
|
|
105
|
-
const body = content.endsWith("\r\n")
|
|
106
|
-
? content.slice(0, -2)
|
|
107
|
-
: content.endsWith("\n")
|
|
108
|
-
? content.slice(0, -1)
|
|
109
|
-
: content;
|
|
110
|
-
const rawLines = content.length === 0 ? [] : body.split(/\r?\n/);
|
|
111
|
-
|
|
112
|
-
const lines: EnvFileLine[] = rawLines.map((text) => {
|
|
113
|
-
if (text.trim() === "") return { kind: "blank", text };
|
|
114
|
-
if (text.trimStart().startsWith("#")) return { kind: "comment", text };
|
|
115
|
-
|
|
116
|
-
const indent = text.slice(0, text.length - text.trimStart().length);
|
|
117
|
-
const withoutIndent = text.slice(indent.length);
|
|
118
|
-
const exported = withoutIndent.startsWith("export ");
|
|
119
|
-
const assignment = exported ? withoutIndent.slice(7).trimStart() : withoutIndent;
|
|
120
|
-
|
|
121
|
-
const equals = assignment.indexOf("=");
|
|
122
|
-
if (equals <= 0) return { kind: "other", text };
|
|
123
|
-
|
|
124
|
-
const key = assignment.slice(0, equals).trim();
|
|
125
|
-
if (!KEY_PATTERN.test(key)) return { kind: "other", text };
|
|
126
|
-
|
|
127
|
-
const { rawValue, inlineComment } = splitValueAndComment(assignment.slice(equals + 1));
|
|
128
|
-
return { kind: "assignment", text, indent, exported, key, rawValue, inlineComment };
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
return { lines, eol, trailingNewline };
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Renders a line model back to text.
|
|
136
|
-
*
|
|
137
|
-
* @param model - The model to render.
|
|
138
|
-
*/
|
|
139
|
-
export function renderEnvFile(model: EnvFileModel): string {
|
|
140
|
-
if (model.lines.length === 0) return "";
|
|
141
|
-
const body = model.lines.map((line) => line.text).join(model.eol);
|
|
142
|
-
return model.trailingNewline ? body + model.eol : body;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Quotes a value if it needs quoting so the env file parser reads back exactly
|
|
147
|
-
* what was written. An ordinary word is left bare, which keeps the file
|
|
148
|
-
* readable.
|
|
149
|
-
*
|
|
150
|
-
* @param value - The value to write.
|
|
151
|
-
*/
|
|
152
|
-
export function quoteEnvValue(value: string): string {
|
|
153
|
-
const needsQuotes =
|
|
154
|
-
value.length === 0 ||
|
|
155
|
-
/[\s#"'\\]/.test(value) ||
|
|
156
|
-
value.startsWith("'") ||
|
|
157
|
-
value.startsWith('"');
|
|
158
|
-
|
|
159
|
-
if (!needsQuotes) return value;
|
|
160
|
-
|
|
161
|
-
const escaped = value
|
|
162
|
-
.replace(/\\/g, "\\\\")
|
|
163
|
-
.replace(/"/g, '\\"')
|
|
164
|
-
.replace(/\n/g, "\\n")
|
|
165
|
-
.replace(/\t/g, "\\t");
|
|
166
|
-
return `"${escaped}"`;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/** Options for `setEntry`. */
|
|
170
|
-
export interface SetEntryOptions {
|
|
171
|
-
/**
|
|
172
|
-
* Comment lines written above the entry when it is APPENDED. An existing
|
|
173
|
-
* entry keeps whatever comment is already above it, because sous only ever
|
|
174
|
-
* rewrites the value line. Lines may be passed with or without their leading
|
|
175
|
-
* `#`.
|
|
176
|
-
*/
|
|
177
|
-
header?: string | string[];
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/** Turns header text into `#`-prefixed comment lines. */
|
|
181
|
-
function headerLines(header: string | string[]): string[] {
|
|
182
|
-
const source = Array.isArray(header) ? header : header.split("\n");
|
|
183
|
-
return source.map((line) => {
|
|
184
|
-
const trimmed = line.trimEnd();
|
|
185
|
-
if (trimmed.length === 0) return "#";
|
|
186
|
-
return trimmed.startsWith("#") ? trimmed : `# ${trimmed}`;
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Sets one variable in a line model, in place.
|
|
192
|
-
*
|
|
193
|
-
* An existing assignment is rewritten where it stands, keeping its indentation,
|
|
194
|
-
* its `export ` prefix and any trailing comment. When the key appears more than
|
|
195
|
-
* once, the LAST one is rewritten, because that is the one the parser honors.
|
|
196
|
-
* A key that is not there yet is appended at the end of the file, under its
|
|
197
|
-
* generated header comment.
|
|
198
|
-
*
|
|
199
|
-
* @param model - The model to edit.
|
|
200
|
-
* @param key - The variable's name.
|
|
201
|
-
* @param value - The value to store, quoted as needed.
|
|
202
|
-
* @param options - Header comment for a newly appended entry.
|
|
203
|
-
* @returns Whether the entry was updated in place or appended.
|
|
204
|
-
*/
|
|
205
|
-
export function setEntry(
|
|
206
|
-
model: EnvFileModel,
|
|
207
|
-
key: string,
|
|
208
|
-
value: string,
|
|
209
|
-
options: SetEntryOptions = {}
|
|
210
|
-
): "updated" | "appended" {
|
|
211
|
-
const quoted = quoteEnvValue(value);
|
|
212
|
-
|
|
213
|
-
for (let i = model.lines.length - 1; i >= 0; i--) {
|
|
214
|
-
const line = model.lines[i]!;
|
|
215
|
-
if (line.kind !== "assignment" || line.key !== key) continue;
|
|
216
|
-
|
|
217
|
-
const prefix = line.exported ? "export " : "";
|
|
218
|
-
const text = `${line.indent}${prefix}${key}=${quoted}${line.inlineComment}`;
|
|
219
|
-
model.lines[i] = { ...line, text, rawValue: quoted };
|
|
220
|
-
return "updated";
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Appending: separate the new entry from whatever came before with one blank
|
|
224
|
-
// line, unless the file is empty or already ends with one.
|
|
225
|
-
const last = model.lines[model.lines.length - 1];
|
|
226
|
-
if (last !== undefined && last.kind !== "blank") {
|
|
227
|
-
model.lines.push({ kind: "blank", text: "" });
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (options.header !== undefined) {
|
|
231
|
-
for (const text of headerLines(options.header)) {
|
|
232
|
-
model.lines.push({ kind: "comment", text });
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
model.lines.push({
|
|
237
|
-
kind: "assignment",
|
|
238
|
-
text: `${key}=${quoted}`,
|
|
239
|
-
indent: "",
|
|
240
|
-
exported: false,
|
|
241
|
-
key,
|
|
242
|
-
rawValue: quoted,
|
|
243
|
-
inlineComment: "",
|
|
244
|
-
});
|
|
245
|
-
model.trailingNewline = true;
|
|
246
|
-
return "appended";
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Removes every assignment of one variable from a line model, leaving comments
|
|
251
|
-
* and blank lines alone (sous does not read comments, so it does not presume to
|
|
252
|
-
* know which ones belonged to the entry).
|
|
253
|
-
*
|
|
254
|
-
* @param model - The model to edit.
|
|
255
|
-
* @param key - The variable's name.
|
|
256
|
-
* @returns How many assignment lines were removed.
|
|
257
|
-
*/
|
|
258
|
-
export function removeEntry(model: EnvFileModel, key: string): number {
|
|
259
|
-
const before = model.lines.length;
|
|
260
|
-
model.lines = model.lines.filter(
|
|
261
|
-
(line) => !(line.kind === "assignment" && line.key === key)
|
|
262
|
-
);
|
|
263
|
-
return before - model.lines.length;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Reads an env file into a line model, returning an empty model when the file
|
|
268
|
-
* does not exist yet.
|
|
269
|
-
*
|
|
270
|
-
* @param filePath - Absolute path to the env file.
|
|
271
|
-
*/
|
|
272
|
-
export function readEnvFile(filePath: string): EnvFileModel {
|
|
273
|
-
if (!fs.existsSync(filePath)) return { lines: [], eol: "\n", trailingNewline: true };
|
|
274
|
-
return parseEnvFile(fs.readFileSync(filePath, "utf8"));
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Writes a line model back to disk atomically: the new contents go to a
|
|
279
|
-
* temporary file in the same directory and are renamed over the original, so an
|
|
280
|
-
* interrupted write can never leave a half-written env file behind.
|
|
281
|
-
*
|
|
282
|
-
* A file that does not exist yet is created readable and writable by its owner
|
|
283
|
-
* only, since `.env.local` holds secrets; an existing file keeps its mode.
|
|
284
|
-
*
|
|
285
|
-
* @param filePath - Absolute path to the env file.
|
|
286
|
-
* @param model - The model to write.
|
|
287
|
-
*/
|
|
288
|
-
export function writeEnvFile(filePath: string, model: EnvFileModel): void {
|
|
289
|
-
const directory = path.dirname(filePath);
|
|
290
|
-
fs.mkdirSync(directory, { recursive: true });
|
|
291
|
-
|
|
292
|
-
const temporary = path.join(
|
|
293
|
-
directory,
|
|
294
|
-
`.${path.basename(filePath)}.sous-${process.pid}-${Date.now()}.tmp`
|
|
295
|
-
);
|
|
296
|
-
|
|
297
|
-
let mode = 0o600;
|
|
298
|
-
try {
|
|
299
|
-
mode = fs.statSync(filePath).mode & 0o777;
|
|
300
|
-
} catch {
|
|
301
|
-
// No existing file, so the restrictive default stands.
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
fs.writeFileSync(temporary, renderEnvFile(model), { encoding: "utf8", mode });
|
|
305
|
-
fs.renameSync(temporary, filePath);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Reads, edits and rewrites one env file in a single call: the usual way to
|
|
310
|
-
* store an answer.
|
|
311
|
-
*
|
|
312
|
-
* @param filePath - Absolute path to the env file.
|
|
313
|
-
* @param key - The variable's name.
|
|
314
|
-
* @param value - The value to store.
|
|
315
|
-
* @param options - Header comment for a newly appended entry.
|
|
316
|
-
* @returns Whether the entry was updated in place or appended.
|
|
317
|
-
*/
|
|
318
|
-
export function updateEnvFile(
|
|
319
|
-
filePath: string,
|
|
320
|
-
key: string,
|
|
321
|
-
value: string,
|
|
322
|
-
options: SetEntryOptions = {}
|
|
323
|
-
): "updated" | "appended" {
|
|
324
|
-
const model = readEnvFile(filePath);
|
|
325
|
-
const outcome = setEntry(model, key, value, options);
|
|
326
|
-
writeEnvFile(filePath, model);
|
|
327
|
-
return outcome;
|
|
328
|
-
}
|
package/src/lib/env-local.ts
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { ENV_DEFAULTS_NAME, ENV_LOCAL_NAME } from "./config-discovery.js";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Parses `.env` / `.env.local` style content into a flat key/value map.
|
|
7
|
-
*
|
|
8
|
-
* Supported syntax (deliberately small — this is not a shell):
|
|
9
|
-
* - `KEY=value`
|
|
10
|
-
* - `export KEY=value` (the `export ` prefix is ignored)
|
|
11
|
-
* - `# comment` lines and blank lines are skipped
|
|
12
|
-
* - single- or double-quoted values are unquoted; `\n` and `\t` inside
|
|
13
|
-
* double quotes become real newlines/tabs
|
|
14
|
-
* - an unquoted value has a trailing ` # comment` stripped and is trimmed
|
|
15
|
-
*
|
|
16
|
-
* Lines that do not contain `=` are ignored rather than treated as errors, so a
|
|
17
|
-
* stray note in the file cannot break a build.
|
|
18
|
-
*
|
|
19
|
-
* @param content - Raw file contents.
|
|
20
|
-
* @returns Parsed variables in file order (later duplicates win).
|
|
21
|
-
*/
|
|
22
|
-
export function parseEnvLocal(content: string): Record<string, string> {
|
|
23
|
-
const result: Record<string, string> = {};
|
|
24
|
-
|
|
25
|
-
for (const rawLine of content.split(/\r?\n/)) {
|
|
26
|
-
const line = rawLine.trim();
|
|
27
|
-
if (line === "" || line.startsWith("#")) continue;
|
|
28
|
-
|
|
29
|
-
const withoutExport = line.startsWith("export ") ? line.slice(7).trim() : line;
|
|
30
|
-
|
|
31
|
-
const eq = withoutExport.indexOf("=");
|
|
32
|
-
if (eq <= 0) continue;
|
|
33
|
-
|
|
34
|
-
const key = withoutExport.slice(0, eq).trim();
|
|
35
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
|
|
36
|
-
|
|
37
|
-
result[key] = parseValue(withoutExport.slice(eq + 1));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return result;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Unquotes and cleans up the right-hand side of a `KEY=value` line.
|
|
45
|
-
*/
|
|
46
|
-
function parseValue(raw: string): string {
|
|
47
|
-
const trimmed = raw.trim();
|
|
48
|
-
|
|
49
|
-
if (trimmed.startsWith('"')) {
|
|
50
|
-
const end = findClosingQuote(trimmed, '"');
|
|
51
|
-
if (end !== -1) {
|
|
52
|
-
return trimmed
|
|
53
|
-
.slice(1, end)
|
|
54
|
-
.replace(/\\n/g, "\n")
|
|
55
|
-
.replace(/\\t/g, "\t")
|
|
56
|
-
.replace(/\\"/g, '"');
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (trimmed.startsWith("'")) {
|
|
61
|
-
const end = findClosingQuote(trimmed, "'");
|
|
62
|
-
if (end !== -1) return trimmed.slice(1, end);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Unquoted: strip an inline comment, then trim.
|
|
66
|
-
const commentAt = trimmed.search(/\s#/);
|
|
67
|
-
const body = commentAt === -1 ? trimmed : trimmed.slice(0, commentAt);
|
|
68
|
-
return body.trim();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Finds the index of the closing quote for a value that starts with `quote`,
|
|
73
|
-
* skipping backslash-escaped quotes. Returns -1 when unterminated.
|
|
74
|
-
*/
|
|
75
|
-
function findClosingQuote(value: string, quote: string): number {
|
|
76
|
-
for (let i = 1; i < value.length; i++) {
|
|
77
|
-
if (value[i] === "\\") {
|
|
78
|
-
i++;
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
if (value[i] === quote) return i;
|
|
82
|
-
}
|
|
83
|
-
return -1;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Reads one env file and returns its variables as a flat map, WITHOUT touching
|
|
88
|
-
* `process.env`. Returns an empty map when the file does not exist.
|
|
89
|
-
*
|
|
90
|
-
* The variables layer needs each env file on its own: by the time a command
|
|
91
|
-
* runs, `loadEnvFiles` has already merged both files into the process
|
|
92
|
-
* environment, so reading them separately is the only way to say which layer
|
|
93
|
-
* supplied an answer.
|
|
94
|
-
*
|
|
95
|
-
* @param filePath - Absolute path to the env file.
|
|
96
|
-
* @returns The parsed variables, or an empty map when there is no such file.
|
|
97
|
-
*/
|
|
98
|
-
export function readEnvFileMap(filePath: string): Record<string, string> {
|
|
99
|
-
if (!fs.existsSync(filePath)) return {};
|
|
100
|
-
return parseEnvLocal(fs.readFileSync(filePath, "utf8"));
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** The outcome of an attempted env file load. */
|
|
104
|
-
export type EnvLocalLoadResult = {
|
|
105
|
-
/** Absolute path checked. */
|
|
106
|
-
filePath: string;
|
|
107
|
-
/** True when the file existed and was read. */
|
|
108
|
-
loaded: boolean;
|
|
109
|
-
/** Names of variables that were injected into process.env. */
|
|
110
|
-
applied: string[];
|
|
111
|
-
/** Names present in the file but already set in the environment (left alone). */
|
|
112
|
-
skipped: string[];
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
/** The outcome of loading both env layers. */
|
|
116
|
-
export type EnvFilesLoadResult = {
|
|
117
|
-
/** Result for the gitignored `.sous/.env.local` layer (loaded first, so it wins). */
|
|
118
|
-
local: EnvLocalLoadResult;
|
|
119
|
-
/** Result for the committed `.sous/.env` defaults layer (loaded second). */
|
|
120
|
-
defaults: EnvLocalLoadResult;
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Loads a single env file into `env`, if it exists.
|
|
125
|
-
*
|
|
126
|
-
* A variable already present in `env` is never overwritten, which is what makes
|
|
127
|
-
* layering work: whoever gets there first wins.
|
|
128
|
-
*
|
|
129
|
-
* @param filePath - Absolute path to the env file.
|
|
130
|
-
* @param env - The environment object to mutate.
|
|
131
|
-
* @returns What was found and what was applied.
|
|
132
|
-
*/
|
|
133
|
-
function loadEnvFile(filePath: string, env: NodeJS.ProcessEnv): EnvLocalLoadResult {
|
|
134
|
-
if (!fs.existsSync(filePath)) {
|
|
135
|
-
return { filePath, loaded: false, applied: [], skipped: [] };
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const parsed = parseEnvLocal(fs.readFileSync(filePath, "utf8"));
|
|
139
|
-
const applied: string[] = [];
|
|
140
|
-
const skipped: string[] = [];
|
|
141
|
-
|
|
142
|
-
for (const [key, value] of Object.entries(parsed)) {
|
|
143
|
-
if (env[key] !== undefined) {
|
|
144
|
-
skipped.push(key);
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
env[key] = value;
|
|
148
|
-
applied.push(key);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return { filePath, loaded: true, applied, skipped };
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Loads `<sousDir>/.env` (committed shared defaults) into `process.env`, if it
|
|
156
|
-
* exists. Anything already set is left alone, so both your shell and
|
|
157
|
-
* `.env.local` outrank it.
|
|
158
|
-
*
|
|
159
|
-
* @param sousDir - The discovered `.sous/` directory.
|
|
160
|
-
* @param env - The environment object to mutate (injectable for tests).
|
|
161
|
-
* @returns What was found and what was applied.
|
|
162
|
-
*/
|
|
163
|
-
export function loadEnvDefaults(
|
|
164
|
-
sousDir: string,
|
|
165
|
-
env: NodeJS.ProcessEnv = process.env
|
|
166
|
-
): EnvLocalLoadResult {
|
|
167
|
-
return loadEnvFile(path.join(sousDir, ENV_DEFAULTS_NAME), env);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Loads `<sousDir>/.env.local` (machine-specific values and secrets) into
|
|
172
|
-
* `process.env`, if it exists.
|
|
173
|
-
*
|
|
174
|
-
* Existing environment values always win: a variable already set in the real
|
|
175
|
-
* environment is never overwritten, so `FOO=bar sous build` behaves as expected.
|
|
176
|
-
*
|
|
177
|
-
* @param sousDir - The discovered `.sous/` directory.
|
|
178
|
-
* @param env - The environment object to mutate (injectable for tests).
|
|
179
|
-
* @returns What was found and what was applied.
|
|
180
|
-
*/
|
|
181
|
-
export function loadEnvLocal(
|
|
182
|
-
sousDir: string,
|
|
183
|
-
env: NodeJS.ProcessEnv = process.env
|
|
184
|
-
): EnvLocalLoadResult {
|
|
185
|
-
return loadEnvFile(path.join(sousDir, ENV_LOCAL_NAME), env);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Loads both env layers out of `sousDir`. Precedence, highest first:
|
|
190
|
-
*
|
|
191
|
-
* real shell environment > `.sous/.env.local` > `.sous/.env`
|
|
192
|
-
*
|
|
193
|
-
* Because no load ever overwrites a value that is already set, the FIRST writer
|
|
194
|
-
* of a key wins. So the higher-precedence file is loaded first: `.env.local`,
|
|
195
|
-
* then `.env` fills in only the keys nobody else supplied. The real shell
|
|
196
|
-
* environment is already populated before either runs, so it outranks both.
|
|
197
|
-
*
|
|
198
|
-
* Must be called before any config or variable resolution so the `_env` block
|
|
199
|
-
* sees the injected values.
|
|
200
|
-
*
|
|
201
|
-
* @param sousDir - The discovered `.sous/` directory.
|
|
202
|
-
* @param env - The environment object to mutate (injectable for tests).
|
|
203
|
-
* @returns The per-file results.
|
|
204
|
-
*/
|
|
205
|
-
export function loadEnvFiles(
|
|
206
|
-
sousDir: string,
|
|
207
|
-
env: NodeJS.ProcessEnv = process.env
|
|
208
|
-
): EnvFilesLoadResult {
|
|
209
|
-
const local = loadEnvLocal(sousDir, env);
|
|
210
|
-
const defaults = loadEnvDefaults(sousDir, env);
|
|
211
|
-
return { local, defaults };
|
|
212
|
-
}
|
package/src/lib/errors.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared error types for sous.
|
|
3
|
-
*
|
|
4
|
-
* ConfigError lives in its own module (rather than settings.ts) so that
|
|
5
|
-
* config-discovery.ts can throw it without importing settings.ts — settings.ts
|
|
6
|
-
* imports discovery constants, and a back-import would create a cycle.
|
|
7
|
-
* settings.ts re-exports both names for backwards compatibility.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* A user-facing configuration error: the config file (or environment) is wrong,
|
|
12
|
-
* not the CLI. Commands render these as a plain message with no stack trace,
|
|
13
|
-
* since the stack points at Sous internals and tells the user nothing.
|
|
14
|
-
*/
|
|
15
|
-
export class ConfigError extends Error {
|
|
16
|
-
readonly isConfigError = true;
|
|
17
|
-
|
|
18
|
-
constructor(message: string) {
|
|
19
|
-
super(message);
|
|
20
|
-
this.name = "ConfigError";
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** True when the value is a ConfigError (safe across module instances). */
|
|
25
|
-
export function isConfigError(error: unknown): boolean {
|
|
26
|
-
return (
|
|
27
|
-
error instanceof ConfigError ||
|
|
28
|
-
(typeof error === "object" &&
|
|
29
|
-
error !== null &&
|
|
30
|
-
(error as { isConfigError?: boolean }).isConfigError === true)
|
|
31
|
-
);
|
|
32
|
-
}
|
package/src/lib/glob-pattern.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Filesystem paths as glob patterns.
|
|
3
|
-
*
|
|
4
|
-
* glob and minimatch read `\` in a pattern as an escape character, never as a
|
|
5
|
-
* path separator, and want `/` on every platform. A pattern built with
|
|
6
|
-
* `path.join` or `path.resolve` on Windows is all backslashes, so it silently
|
|
7
|
-
* matched nothing: a recipe's skills, memories and config layers, an include
|
|
8
|
-
* glob and an `entryGlob` target all came up empty without an error.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import path from "node:path";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Writes a path or a path-shaped glob with `/` separators, so glob reads it the
|
|
15
|
-
* same way on Windows as everywhere else. A no-op where `/` is the separator, so
|
|
16
|
-
* a pattern's own escapes survive on every other platform.
|
|
17
|
-
*
|
|
18
|
-
* toGlobPattern("C:\\recipes\\skills\\**\\*"); // -> "C:/recipes/skills/**\/*" on Windows
|
|
19
|
-
*
|
|
20
|
-
* @param value - A filesystem path, or a glob pattern built from one.
|
|
21
|
-
*/
|
|
22
|
-
export function toGlobPattern(value: string): string {
|
|
23
|
-
return path.sep === "\\" ? value.split(path.sep).join("/") : value;
|
|
24
|
-
}
|