@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.
Files changed (244) hide show
  1. package/README.md +14 -256
  2. package/dist/bin.js +11 -0
  3. package/dist/sous.js +1094 -0
  4. package/docs/markdown/README.md +5 -4
  5. package/docs/markdown/_sidebar.md +21 -1
  6. package/docs/markdown/commands.md +294 -431
  7. package/docs/markdown/config-discovery.md +25 -14
  8. package/docs/markdown/config-inspection.md +53 -31
  9. package/docs/markdown/config-layers.md +168 -66
  10. package/docs/markdown/config-variables.md +44 -11
  11. package/docs/markdown/configuration.md +104 -17
  12. package/docs/markdown/design-principles.md +4 -1
  13. package/docs/markdown/repositories-authoring.md +30 -0
  14. package/docs/markdown/repositories-consuming.md +72 -14
  15. package/docs/markdown/repositories-file-formats.md +31 -13
  16. package/docs/markdown/repositories-providers.md +48 -57
  17. package/docs/markdown/repositories-quickstart.md +3 -2
  18. package/docs/markdown/repositories-troubleshooting.md +27 -1
  19. package/docs/markdown/repositories-variables.md +6 -1
  20. package/docs/markdown/repositories.md +18 -8
  21. package/package.json +48 -107
  22. package/recipes/core/sous-skills/skills/about-liquid-templates/SKILL.tpl.md +4 -6
  23. package/recipes/core/sous-skills/skills/about-liquid-templates/references/liquid-filters.tpl.md +2 -0
  24. package/recipes/core/sous-skills/skills/about-sous/SKILL.tpl.md +10 -9
  25. package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +35 -14
  26. package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +51 -20
  27. package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
  28. package/sous.config.schema.json +351 -132
  29. package/bin/run.js +0 -51
  30. package/src/base-command.ts +0 -457
  31. package/src/commands/build.ts +0 -181
  32. package/src/commands/clear.ts +0 -79
  33. package/src/commands/compile.ts +0 -155
  34. package/src/commands/config/get.ts +0 -110
  35. package/src/commands/config/show.ts +0 -32
  36. package/src/commands/config/validate.ts +0 -53
  37. package/src/commands/help.ts +0 -46
  38. package/src/commands/init.ts +0 -289
  39. package/src/commands/launch.ts +0 -139
  40. package/src/commands/lock/rebuild.ts +0 -239
  41. package/src/commands/lock/show.ts +0 -115
  42. package/src/commands/namespace/list.ts +0 -138
  43. package/src/commands/namespace/show.ts +0 -130
  44. package/src/commands/prune.ts +0 -43
  45. package/src/commands/recipe/list.ts +0 -112
  46. package/src/commands/recipe/show.ts +0 -439
  47. package/src/commands/repo/add.ts +0 -145
  48. package/src/commands/repo/contribute.ts +0 -604
  49. package/src/commands/repo/gc.ts +0 -173
  50. package/src/commands/repo/init.ts +0 -136
  51. package/src/commands/repo/link.ts +0 -899
  52. package/src/commands/repo/list.ts +0 -236
  53. package/src/commands/repo/release.ts +0 -685
  54. package/src/commands/repo/remove.ts +0 -194
  55. package/src/commands/repo/search.ts +0 -243
  56. package/src/commands/repo/submit.ts +0 -341
  57. package/src/commands/repo/unlink.ts +0 -461
  58. package/src/commands/subscription/add.ts +0 -286
  59. package/src/commands/subscription/list.ts +0 -208
  60. package/src/commands/subscription/remove.ts +0 -182
  61. package/src/commands/subscription/update.ts +0 -216
  62. package/src/commands/update.ts +0 -178
  63. package/src/commands/vars/ask.ts +0 -417
  64. package/src/commands/vars/index.ts +0 -79
  65. package/src/commands/vars/list.ts +0 -67
  66. package/src/commands/vars/show.ts +0 -77
  67. package/src/config-command.ts +0 -30
  68. package/src/lib/build-preparation.ts +0 -128
  69. package/src/lib/build-service.ts +0 -580
  70. package/src/lib/config-discovery.ts +0 -378
  71. package/src/lib/config-inspect.ts +0 -145
  72. package/src/lib/config-kernel.mjs +0 -379
  73. package/src/lib/config-schema.ts +0 -401
  74. package/src/lib/env-file.ts +0 -328
  75. package/src/lib/env-local.ts +0 -212
  76. package/src/lib/errors.ts +0 -32
  77. package/src/lib/glob-pattern.ts +0 -24
  78. package/src/lib/include-resolver.ts +0 -523
  79. package/src/lib/interactive.ts +0 -165
  80. package/src/lib/markdown-compiler.ts +0 -947
  81. package/src/lib/package-info.ts +0 -25
  82. package/src/lib/pid-service.ts +0 -99
  83. package/src/lib/project-install.d.mts +0 -44
  84. package/src/lib/project-install.mjs +0 -234
  85. package/src/lib/project-scaffold/index.ts +0 -332
  86. package/src/lib/project-scaffold/templates.ts +0 -240
  87. package/src/lib/repos/catalog-display.ts +0 -215
  88. package/src/lib/repos/catalog-inputs.ts +0 -288
  89. package/src/lib/repos/catalog.ts +0 -842
  90. package/src/lib/repos/contribute.ts +0 -452
  91. package/src/lib/repos/core-recipe.ts +0 -105
  92. package/src/lib/repos/declarations.ts +0 -187
  93. package/src/lib/repos/defaults.ts +0 -175
  94. package/src/lib/repos/formats/common.ts +0 -440
  95. package/src/lib/repos/formats/index-file.ts +0 -253
  96. package/src/lib/repos/formats/links-map.ts +0 -98
  97. package/src/lib/repos/formats/lockfile.ts +0 -169
  98. package/src/lib/repos/formats/patterns.ts +0 -57
  99. package/src/lib/repos/formats/recipe-manifest.ts +0 -205
  100. package/src/lib/repos/formats/repo-manifest.ts +0 -97
  101. package/src/lib/repos/formats/store-entry.ts +0 -84
  102. package/src/lib/repos/formats/variable-definition.ts +0 -268
  103. package/src/lib/repos/freshness.ts +0 -264
  104. package/src/lib/repos/git-clone.ts +0 -850
  105. package/src/lib/repos/identity.ts +0 -89
  106. package/src/lib/repos/index.ts +0 -57
  107. package/src/lib/repos/links.ts +0 -356
  108. package/src/lib/repos/load-manifest.ts +0 -237
  109. package/src/lib/repos/lock-service.ts +0 -454
  110. package/src/lib/repos/locked-namespace-resolver.ts +0 -232
  111. package/src/lib/repos/locked-recipe-lookup.ts +0 -123
  112. package/src/lib/repos/locked-recipes.ts +0 -254
  113. package/src/lib/repos/managed-layer.ts +0 -422
  114. package/src/lib/repos/memories-hash-name.ts +0 -33
  115. package/src/lib/repos/namespace-resolver.ts +0 -527
  116. package/src/lib/repos/providers/base.ts +0 -268
  117. package/src/lib/repos/providers/git.ts +0 -245
  118. package/src/lib/repos/providers/github.ts +0 -589
  119. package/src/lib/repos/providers/gitlab.ts +0 -332
  120. package/src/lib/repos/providers/http.ts +0 -128
  121. package/src/lib/repos/providers/index-cache.ts +0 -421
  122. package/src/lib/repos/providers/index.ts +0 -106
  123. package/src/lib/repos/providers/local.ts +0 -410
  124. package/src/lib/repos/providers/provider.ts +0 -615
  125. package/src/lib/repos/recipe-config-layers.ts +0 -316
  126. package/src/lib/repos/recipe-key-matcher.ts +0 -62
  127. package/src/lib/repos/recipe-memories.ts +0 -178
  128. package/src/lib/repos/recipe-targets.ts +0 -236
  129. package/src/lib/repos/ref-search.ts +0 -46
  130. package/src/lib/repos/release/bump.ts +0 -161
  131. package/src/lib/repos/release/changelog.ts +0 -461
  132. package/src/lib/repos/release/git-state.ts +0 -460
  133. package/src/lib/repos/release/index-builder.ts +0 -1108
  134. package/src/lib/repos/release/index.ts +0 -19
  135. package/src/lib/repos/release/plan.ts +0 -521
  136. package/src/lib/repos/release/settle.ts +0 -154
  137. package/src/lib/repos/release/submissions.ts +0 -214
  138. package/src/lib/repos/release/submit-checkout.ts +0 -290
  139. package/src/lib/repos/release/submit-questions.ts +0 -153
  140. package/src/lib/repos/release/submit-service.ts +0 -898
  141. package/src/lib/repos/release/tags.ts +0 -239
  142. package/src/lib/repos/release/validate.ts +0 -463
  143. package/src/lib/repos/resolver.ts +0 -1028
  144. package/src/lib/repos/scaffold/index.ts +0 -245
  145. package/src/lib/repos/scaffold/templates.ts +0 -517
  146. package/src/lib/repos/seed.ts +0 -473
  147. package/src/lib/repos/store/contract.ts +0 -64
  148. package/src/lib/repos/store/hash.ts +0 -191
  149. package/src/lib/repos/store/recipe-store.ts +0 -599
  150. package/src/lib/repos/store/settings.ts +0 -58
  151. package/src/lib/repos/subscription-service.ts +0 -3859
  152. package/src/lib/repos/trust.ts +0 -447
  153. package/src/lib/repos/update-plan.ts +0 -235
  154. package/src/lib/self-update/index.ts +0 -14
  155. package/src/lib/self-update/installs.ts +0 -413
  156. package/src/lib/self-update/managers.ts +0 -126
  157. package/src/lib/self-update/plan.ts +0 -315
  158. package/src/lib/self-update/registry.ts +0 -99
  159. package/src/lib/self-update/run.ts +0 -506
  160. package/src/lib/self-update/versions.ts +0 -243
  161. package/src/lib/settings.ts +0 -1107
  162. package/src/lib/sous-home.ts +0 -104
  163. package/src/lib/state.ts +0 -167
  164. package/src/lib/vars/answers.ts +0 -192
  165. package/src/lib/vars/ask.ts +0 -1152
  166. package/src/lib/vars/definition-source.ts +0 -261
  167. package/src/lib/vars/display.ts +0 -231
  168. package/src/lib/vars/index.ts +0 -20
  169. package/src/lib/vars/ladder.ts +0 -282
  170. package/src/lib/vars/mappings.ts +0 -260
  171. package/src/lib/vars/names.ts +0 -94
  172. package/src/lib/vars/preanswers.ts +0 -395
  173. package/src/lib/vars/question-plan.ts +0 -241
  174. package/src/lib/vars/report.ts +0 -228
  175. package/src/lib/vars/safe-regex.ts +0 -235
  176. package/src/lib/vars/secrets.ts +0 -131
  177. package/src/lib/vars/validate.ts +0 -312
  178. package/src/lib/version-report.ts +0 -90
  179. package/src/lib/watch-loop.ts +0 -148
  180. package/src/lib/watch-service.ts +0 -116
  181. package/src/services/ref-resolver/container.ts +0 -107
  182. package/src/services/ref-resolver/dependency.ts +0 -60
  183. package/src/services/ref-resolver/format.ts +0 -197
  184. package/src/services/ref-resolver/glob.ts +0 -115
  185. package/src/services/ref-resolver/hash-names.ts +0 -155
  186. package/src/services/ref-resolver/index.ts +0 -55
  187. package/src/services/ref-resolver/injectable.ts +0 -36
  188. package/src/services/ref-resolver/location.ts +0 -62
  189. package/src/services/ref-resolver/lookups/cached-index-lookup.ts +0 -53
  190. package/src/services/ref-resolver/lookups/catalog-lookup.ts +0 -23
  191. package/src/services/ref-resolver/lookups/catalog-matcher.ts +0 -261
  192. package/src/services/ref-resolver/lookups/chained-lookup.ts +0 -27
  193. package/src/services/ref-resolver/lookups/env-var-lookup.ts +0 -65
  194. package/src/services/ref-resolver/lookups/fetched-index-lookup.ts +0 -150
  195. package/src/services/ref-resolver/lookups/recorded-dependency-lookup.ts +0 -62
  196. package/src/services/ref-resolver/lookups/ref-lookup.ts +0 -54
  197. package/src/services/ref-resolver/lookups/variable-lookup.ts +0 -123
  198. package/src/services/ref-resolver/named-refs.ts +0 -144
  199. package/src/services/ref-resolver/parser/env-var-splitter.ts +0 -27
  200. package/src/services/ref-resolver/parser/location-splitter.ts +0 -239
  201. package/src/services/ref-resolver/parser/name-path-splitter.ts +0 -200
  202. package/src/services/ref-resolver/parser/partial-ref.ts +0 -84
  203. package/src/services/ref-resolver/parser/query-splitter.ts +0 -61
  204. package/src/services/ref-resolver/parser/range-splitter.ts +0 -52
  205. package/src/services/ref-resolver/parser/ref-parser.ts +0 -115
  206. package/src/services/ref-resolver/parser/repo-qualifier-splitter.ts +0 -51
  207. package/src/services/ref-resolver/parts.ts +0 -115
  208. package/src/services/ref-resolver/pruners/command-line-pruner.ts +0 -47
  209. package/src/services/ref-resolver/pruners/include-pruner.ts +0 -66
  210. package/src/services/ref-resolver/pruners/manifest-pruner.ts +0 -130
  211. package/src/services/ref-resolver/pruners/ref-pruner.ts +0 -135
  212. package/src/services/ref-resolver/pruners/rule-helpers.ts +0 -44
  213. package/src/services/ref-resolver/pruners/stored-key-pruner.ts +0 -136
  214. package/src/services/ref-resolver/ref-helpers.ts +0 -62
  215. package/src/services/ref-resolver/ref-picker-service.ts +0 -204
  216. package/src/services/ref-resolver/ref-report.ts +0 -84
  217. package/src/services/ref-resolver/ref-resolve-arguments.ts +0 -47
  218. package/src/services/ref-resolver/ref-resolve-result.ts +0 -36
  219. package/src/services/ref-resolver/ref-resolver-service.ts +0 -323
  220. package/src/services/ref-resolver/source.ts +0 -48
  221. package/src/services/ref-resolver/tokens.ts +0 -25
  222. package/src/services/ref-resolver/types.ts +0 -155
  223. package/src/templating/filters/bullet-list.ts +0 -9
  224. package/src/templating/filters/index.ts +0 -8
  225. package/src/templating/init-liquid-engine.ts +0 -139
  226. package/src/templating/lib/glob-files.ts +0 -74
  227. package/src/templating/lib/import-export.ts +0 -32
  228. package/src/templating/lib/secret-scope.ts +0 -41
  229. package/src/templating/lib/tag-args.ts +0 -19
  230. package/src/templating/tags/exportScalarVarsJs.ts +0 -46
  231. package/src/templating/tags/getFiles.ts +0 -89
  232. package/src/templating/tags/index.ts +0 -14
  233. package/src/templating/tags/listFiles.ts +0 -54
  234. package/src/templating/tags/showVars.ts +0 -28
  235. package/src/utils/choice-prompt.ts +0 -143
  236. package/src/utils/command-errors.ts +0 -186
  237. package/src/utils/command-help.ts +0 -45
  238. package/src/utils/confirm-prompt.ts +0 -110
  239. package/src/utils/flags.ts +0 -177
  240. package/src/utils/formatting.ts +0 -849
  241. package/src/utils/prompts.ts +0 -53
  242. package/src/utils/sous-directory.ts +0 -245
  243. package/src/utils/table.ts +0 -603
  244. package/src/utils/value-prompt.ts +0 -119
@@ -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
- }
@@ -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
- }
@@ -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
- }