@sous-io/sous 0.2.34 → 0.2.36
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/package.json +1 -1
- package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
- package/src/lib/config-kernel.mjs +3 -1
- package/src/lib/glob-pattern.ts +24 -0
- package/src/lib/include-resolver.ts +4 -1
- package/src/lib/repos/git-clone.ts +8 -2
- package/src/lib/repos/providers/git.ts +4 -0
- package/src/lib/repos/recipe-config-layers.ts +3 -2
- package/src/lib/repos/recipe-memories.ts +5 -2
- package/src/lib/repos/recipe-targets.ts +3 -2
- package/src/lib/repos/store/hash.ts +17 -0
- package/src/lib/settings.ts +2 -1
- package/src/lib/watch-service.ts +2 -1
package/package.json
CHANGED
|
@@ -247,7 +247,9 @@ async function main() {
|
|
|
247
247
|
async loadConfigs(globPattern) {
|
|
248
248
|
const where = `loadConfigs(${JSON.stringify(globPattern)}) in ${currentFile}`;
|
|
249
249
|
const pattern = resolveBuilderPath(globPattern, where);
|
|
250
|
-
|
|
250
|
+
// glob reads `\` as an escape, so a Windows path is handed over with `/`
|
|
251
|
+
// separators (see src/lib/glob-pattern.ts; this file stays plain JS).
|
|
252
|
+
const matches = globSync(pattern.split(path.sep).join("/"), { absolute: true })
|
|
251
253
|
.filter((p) => fs.statSync(p).isFile())
|
|
252
254
|
.sort(bytewiseCompare);
|
|
253
255
|
for (const match of matches) {
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
}
|
|
@@ -4,6 +4,7 @@ import { globSync } from "glob";
|
|
|
4
4
|
import { minimatch } from "minimatch";
|
|
5
5
|
import { compareBytewise, hasGlob, type ViewFile } from "../services/ref-resolver/index.js";
|
|
6
6
|
import { expandHome } from "./config-discovery.js";
|
|
7
|
+
import { toGlobPattern } from "./glob-pattern.js";
|
|
7
8
|
import type {
|
|
8
9
|
NamespaceResolution,
|
|
9
10
|
NamespaceResolver,
|
|
@@ -382,7 +383,9 @@ export function resolveInclude(rawPath: string, opts: IncludeResolveOptions): In
|
|
|
382
383
|
* @param pattern - An absolute glob pattern.
|
|
383
384
|
*/
|
|
384
385
|
function expandPattern(pattern: string): string[] {
|
|
385
|
-
return globSync(pattern, { absolute: true, nodir: true, dot: true }).sort(
|
|
386
|
+
return globSync(toGlobPattern(pattern), { absolute: true, nodir: true, dot: true }).sort(
|
|
387
|
+
compareBytewise
|
|
388
|
+
);
|
|
386
389
|
}
|
|
387
390
|
|
|
388
391
|
/**
|
|
@@ -16,6 +16,7 @@ import fs from "node:fs";
|
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
import { spawnSync } from "node:child_process";
|
|
18
18
|
import { ConfigError } from "../errors.js";
|
|
19
|
+
import { LF_CHECKOUT_OPTIONS } from "./store/hash.js";
|
|
19
20
|
|
|
20
21
|
/** What one git invocation produced. */
|
|
21
22
|
export type GitResult = {
|
|
@@ -211,15 +212,20 @@ export function cloneRepo(
|
|
|
211
212
|
const depth = options.depth ?? 1;
|
|
212
213
|
const runner = options.runner ?? runGit;
|
|
213
214
|
|
|
215
|
+
// A linked checkout is hashed when its recipes are released or compiled, so
|
|
216
|
+
// it keeps the committed bytes whatever this machine's line-ending settings are.
|
|
214
217
|
if (depth > 0) {
|
|
215
|
-
const shallow = runner(
|
|
218
|
+
const shallow = runner(
|
|
219
|
+
["clone", ...LF_CHECKOUT_OPTIONS, "--depth", String(depth), "--", url, destDir],
|
|
220
|
+
{}
|
|
221
|
+
);
|
|
216
222
|
if (shallow.status === 0) return { depth, fellBackToFullClone: false };
|
|
217
223
|
// git leaves the destination behind on some failures; clear it so the retry
|
|
218
224
|
// is not refused by its own leftovers.
|
|
219
225
|
fs.rmSync(destDir, { recursive: true, force: true });
|
|
220
226
|
}
|
|
221
227
|
|
|
222
|
-
runGitOrThrow(["clone", "--", url, destDir], {
|
|
228
|
+
runGitOrThrow(["clone", ...LF_CHECKOUT_OPTIONS, "--", url, destDir], {
|
|
223
229
|
runner: options.runner,
|
|
224
230
|
what: `Cloning ${url}`,
|
|
225
231
|
});
|
|
@@ -14,6 +14,7 @@ import { spawn } from "node:child_process";
|
|
|
14
14
|
import fs from "node:fs/promises";
|
|
15
15
|
import path from "node:path";
|
|
16
16
|
import { ConfigError } from "../../errors.js";
|
|
17
|
+
import { LF_CHECKOUT_OPTIONS } from "../store/hash.js";
|
|
17
18
|
|
|
18
19
|
/** What a finished subprocess reports back. */
|
|
19
20
|
export type CommandResult = {
|
|
@@ -174,6 +175,9 @@ export async function fetchSubtree(options: {
|
|
|
174
175
|
"1",
|
|
175
176
|
"--filter=blob:none",
|
|
176
177
|
"--sparse",
|
|
178
|
+
// The fetched subtree is hashed against its pin, so its bytes must be the
|
|
179
|
+
// committed ones whatever this machine's line-ending settings are.
|
|
180
|
+
...LF_CHECKOUT_OPTIONS,
|
|
177
181
|
"--branch",
|
|
178
182
|
tag,
|
|
179
183
|
// Everything after this is a path or a URL, never an option, whatever it
|
|
@@ -27,6 +27,7 @@ import fs from "node:fs";
|
|
|
27
27
|
import path from "node:path";
|
|
28
28
|
import { globSync } from "glob";
|
|
29
29
|
import { parse as parseYaml } from "yaml";
|
|
30
|
+
import { toGlobPattern } from "../glob-pattern.js";
|
|
30
31
|
import { parseJsoncText } from "./load-manifest.js";
|
|
31
32
|
import { listLockedRecipes, readRecipeManifestIn } from "./locked-recipes.js";
|
|
32
33
|
|
|
@@ -224,11 +225,11 @@ export function listRecipeConfigLayers(
|
|
|
224
225
|
if (content.kind !== "config") continue;
|
|
225
226
|
|
|
226
227
|
const ignore = (content.exclude ?? []).map((pattern) =>
|
|
227
|
-
path.join(recipe.dir, pattern)
|
|
228
|
+
toGlobPattern(path.join(recipe.dir, pattern))
|
|
228
229
|
);
|
|
229
230
|
|
|
230
231
|
for (const include of content.include) {
|
|
231
|
-
for (const filePath of globSync(path.join(recipe.dir, include), {
|
|
232
|
+
for (const filePath of globSync(toGlobPattern(path.join(recipe.dir, include)), {
|
|
232
233
|
absolute: true,
|
|
233
234
|
ignore,
|
|
234
235
|
})) {
|
|
@@ -22,6 +22,7 @@ import { globSync } from "glob";
|
|
|
22
22
|
import { compareBytewise } from "../../services/ref-resolver/index.js";
|
|
23
23
|
import { realPathInside } from "../include-resolver.js";
|
|
24
24
|
import { inferGlobBase } from "../markdown-compiler.js";
|
|
25
|
+
import { toGlobPattern } from "../glob-pattern.js";
|
|
25
26
|
import { compileRecipeKeyMatcher } from "./recipe-key-matcher.js";
|
|
26
27
|
import {
|
|
27
28
|
listLockedRecipes,
|
|
@@ -137,9 +138,11 @@ export function listMemories(options: MemoryListingOptions): MemoryFile[] {
|
|
|
137
138
|
const mine = new Map<string, MemoryFile>();
|
|
138
139
|
for (const content of manifest.contents) {
|
|
139
140
|
if (content.kind !== "memories") continue;
|
|
140
|
-
const ignore = (content.exclude ?? []).map((pattern) =>
|
|
141
|
+
const ignore = (content.exclude ?? []).map((pattern) =>
|
|
142
|
+
toGlobPattern(path.join(recipe.dir, pattern))
|
|
143
|
+
);
|
|
141
144
|
for (const include of content.include) {
|
|
142
|
-
const pattern = path.join(recipe.dir, include);
|
|
145
|
+
const pattern = toGlobPattern(path.join(recipe.dir, include));
|
|
143
146
|
const base = inferGlobBase(pattern);
|
|
144
147
|
for (const file of globSync(pattern, { absolute: true, ignore, dot: true })) {
|
|
145
148
|
if (!isFile(file)) continue;
|
|
@@ -30,6 +30,7 @@ import path from "node:path";
|
|
|
30
30
|
import { globSync } from "glob";
|
|
31
31
|
import type { CompilationTarget } from "../markdown-compiler.js";
|
|
32
32
|
import { inferGlobBase } from "../markdown-compiler.js";
|
|
33
|
+
import { toGlobPattern } from "../glob-pattern.js";
|
|
33
34
|
import type { Settings, VarScope } from "../settings.js";
|
|
34
35
|
import { normalizeConfigPath, substituteVarsStrict } from "../settings.js";
|
|
35
36
|
import type { ContentKind } from "./formats/recipe-manifest.js";
|
|
@@ -186,11 +187,11 @@ export function buildRecipeTargets(options: RecipeTargetOptions): RecipeTargets
|
|
|
186
187
|
const recipeScope = options.scopeFor?.(recipe) ?? options.scope ?? {};
|
|
187
188
|
|
|
188
189
|
const ignore = (content.exclude ?? []).map((pattern) =>
|
|
189
|
-
path.join(recipe.dir, pattern)
|
|
190
|
+
toGlobPattern(path.join(recipe.dir, pattern))
|
|
190
191
|
);
|
|
191
192
|
|
|
192
193
|
for (const include of content.include) {
|
|
193
|
-
const pattern = path.join(recipe.dir, include);
|
|
194
|
+
const pattern = toGlobPattern(path.join(recipe.dir, include));
|
|
194
195
|
const matched = globSync(pattern, { absolute: true, ignore }).filter(isFile);
|
|
195
196
|
const globBase = normalizeConfigPath(inferGlobBase(pattern));
|
|
196
197
|
|
|
@@ -35,6 +35,23 @@ import fs from "node:fs/promises";
|
|
|
35
35
|
import path from "node:path";
|
|
36
36
|
import { STORE_ENTRY_FILENAME } from "../formats/common.js";
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* `git clone` options that check files out exactly as they were committed.
|
|
40
|
+
*
|
|
41
|
+
* A content hash is over raw bytes, and Git for Windows installs with
|
|
42
|
+
* `core.autocrlf=true` by default, which rewrites every text file to CRLF on
|
|
43
|
+
* checkout. A recipe published with LF then hashed differently on such a
|
|
44
|
+
* machine and failed its own pin. `--config` also writes these into the
|
|
45
|
+
* clone's own `.git/config`, so later checkouts in a linked working copy keep
|
|
46
|
+
* the same bytes.
|
|
47
|
+
*/
|
|
48
|
+
export const LF_CHECKOUT_OPTIONS = [
|
|
49
|
+
"--config",
|
|
50
|
+
"core.autocrlf=false",
|
|
51
|
+
"--config",
|
|
52
|
+
"core.eol=lf",
|
|
53
|
+
] as const;
|
|
54
|
+
|
|
38
55
|
/** Directory name never included in a content hash. */
|
|
39
56
|
const GIT_DIR_NAME = ".git";
|
|
40
57
|
|
package/src/lib/settings.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { globSync } from "glob";
|
|
7
7
|
import { inferGlobBase, type CompilationConfig, type CompilationTarget, type ResolvedRuntimeContext } from "./markdown-compiler.js";
|
|
8
8
|
import { buildAliasMap, resolveAliasPrefix, type AliasMap } from "./include-resolver.js";
|
|
9
|
+
import { toGlobPattern } from "./glob-pattern.js";
|
|
9
10
|
import {
|
|
10
11
|
CONFD_DIR_NAME,
|
|
11
12
|
ENV_DEFAULTS_NAME,
|
|
@@ -1010,7 +1011,7 @@ export function resolveCompilation(
|
|
|
1010
1011
|
// base; the first base that matches any files wins, mirroring the first-existing-wins
|
|
1011
1012
|
// rule of @include resolution.
|
|
1012
1013
|
const rawPattern = substituteVarsStrict(target.entryGlob!, targetScope, `${where}.entryGlob`);
|
|
1013
|
-
const patternCandidates = resolveAliasPrefix(rawPattern, aliases);
|
|
1014
|
+
const patternCandidates = resolveAliasPrefix(rawPattern, aliases).map(toGlobPattern);
|
|
1014
1015
|
let pattern = patternCandidates[0];
|
|
1015
1016
|
let matchedFiles: string[] = [];
|
|
1016
1017
|
for (const candidate of patternCandidates) {
|
package/src/lib/watch-service.ts
CHANGED
|
@@ -2,6 +2,7 @@ import chokidar from "chokidar";
|
|
|
2
2
|
import { minimatch } from "minimatch";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import type { WatchConfig } from "./settings.js";
|
|
5
|
+
import { toGlobPattern } from "./glob-pattern.js";
|
|
5
6
|
import { log } from "../utils/formatting.js";
|
|
6
7
|
|
|
7
8
|
/** Milliseconds to wait after a change before triggering a rebuild. */
|
|
@@ -98,7 +99,7 @@ export class WatchService {
|
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
const isExactFile = files.includes(filePath);
|
|
101
|
-
const matchesGlob = globs.some(g => minimatch(filePath, g));
|
|
102
|
+
const matchesGlob = globs.some(g => minimatch(filePath, toGlobPattern(g)));
|
|
102
103
|
|
|
103
104
|
if (isExactFile || matchesGlob) {
|
|
104
105
|
schedule({ type: "partial", filePath });
|