@sous-io/sous 0.2.35 → 0.2.37
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/providers/git.ts +10 -2
- package/src/lib/repos/providers/local.ts +20 -1
- 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/self-update/installs.ts +2 -1
- package/src/lib/settings.ts +6 -3
- 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
|
/**
|
|
@@ -28,7 +28,14 @@ export type CommandResult = {
|
|
|
28
28
|
export type CommandRunner = (
|
|
29
29
|
command: string,
|
|
30
30
|
args: string[],
|
|
31
|
-
options: {
|
|
31
|
+
options: {
|
|
32
|
+
cwd?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Start the command through the shell. Only for a fixed command line: it
|
|
35
|
+
* is how Windows finds a `.cmd` shim such as npm's.
|
|
36
|
+
*/
|
|
37
|
+
shell?: boolean;
|
|
38
|
+
}
|
|
32
39
|
) => Promise<CommandResult>;
|
|
33
40
|
|
|
34
41
|
/**
|
|
@@ -38,13 +45,14 @@ export type CommandRunner = (
|
|
|
38
45
|
*
|
|
39
46
|
* @param command - The executable to run.
|
|
40
47
|
* @param args - Its arguments, already split.
|
|
41
|
-
* @param options - Optional working directory.
|
|
48
|
+
* @param options - Optional working directory, and whether to start it through the shell.
|
|
42
49
|
*/
|
|
43
50
|
export const spawnCommand: CommandRunner = (command, args, options = {}) =>
|
|
44
51
|
new Promise<CommandResult>((resolve) => {
|
|
45
52
|
const child = spawn(command, args, {
|
|
46
53
|
cwd: options.cwd,
|
|
47
54
|
stdio: ["ignore", "pipe", "pipe"],
|
|
55
|
+
shell: options.shell === true,
|
|
48
56
|
});
|
|
49
57
|
|
|
50
58
|
let stdout = "";
|
|
@@ -83,6 +83,25 @@ export function localRepoPath(url: string): string | undefined {
|
|
|
83
83
|
return path.isAbsolute(trimmed) ? path.normalize(trimmed) : undefined;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* A directory spelled as an identity's owner path: its segments joined with `/`.
|
|
88
|
+
* The identity becomes directories in the store and the index cache, split on
|
|
89
|
+
* `/`, so a Windows path would otherwise land there as one segment holding its
|
|
90
|
+
* drive colon and backslashes. A drive (`C:`) or UNC (`\\server\share`) root
|
|
91
|
+
* becomes plain segments. A POSIX path comes back unchanged.
|
|
92
|
+
*
|
|
93
|
+
* identityOwnerPath("/home/me"); // -> "/home/me"
|
|
94
|
+
* identityOwnerPath("C:\\Users\\me"); // -> "/C/Users/me" (on Windows)
|
|
95
|
+
*
|
|
96
|
+
* @param directory - An absolute, normalized directory.
|
|
97
|
+
*/
|
|
98
|
+
function identityOwnerPath(directory: string): string {
|
|
99
|
+
const { root } = path.parse(directory);
|
|
100
|
+
const rootSegments = root.split(/[\\/]/).map((segment) => segment.replace(/:$/, ""));
|
|
101
|
+
const segments = [...rootSegments, ...directory.slice(root.length).split(path.sep)];
|
|
102
|
+
return "/" + segments.filter((segment) => segment.length > 0).join("/");
|
|
103
|
+
}
|
|
104
|
+
|
|
86
105
|
/** A URL that names its scheme, such as `https://` or `ssh://`. */
|
|
87
106
|
const SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
88
107
|
|
|
@@ -290,7 +309,7 @@ export class LocalProvider extends ProviderBase {
|
|
|
290
309
|
|
|
291
310
|
return {
|
|
292
311
|
host: "localhost",
|
|
293
|
-
owner: path.dirname(directory),
|
|
312
|
+
owner: identityOwnerPath(path.dirname(directory)),
|
|
294
313
|
name: path.basename(directory),
|
|
295
314
|
httpsUrl: directory,
|
|
296
315
|
sshUrl: pathToFileURL(directory).href,
|
|
@@ -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
|
|
|
@@ -361,7 +361,8 @@ async function globalRoot(manager: GlobalManager, run: CommandRunner): Promise<s
|
|
|
361
361
|
const query = GLOBAL_ROOT_QUERIES[manager];
|
|
362
362
|
let output: string;
|
|
363
363
|
try {
|
|
364
|
-
|
|
364
|
+
// On Windows the managers are `.cmd` shims, which only the shell finds.
|
|
365
|
+
const result = await run(manager, query.args, { shell: process.platform === "win32" });
|
|
365
366
|
if (result.code !== 0) return undefined;
|
|
366
367
|
output = result.stdout.trim().split(/\r?\n/).pop()?.trim() ?? "";
|
|
367
368
|
} catch {
|
package/src/lib/settings.ts
CHANGED
|
@@ -2,10 +2,11 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } 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,
|
|
@@ -328,7 +329,9 @@ export async function loadSettingsWithLayers(
|
|
|
328
329
|
|
|
329
330
|
const attempts: { label: string; args: string[] }[] = [
|
|
330
331
|
{ label: "node", args: [CONFIG_KERNEL_PATH] },
|
|
331
|
-
|
|
332
|
+
// `--import` takes a module specifier, so the path goes as a file:// URL:
|
|
333
|
+
// a bare Windows path reads as a URL whose scheme is the drive letter.
|
|
334
|
+
{ label: "tsx", args: ["--import", pathToFileURL(tsxPath).href, CONFIG_KERNEL_PATH] },
|
|
332
335
|
];
|
|
333
336
|
|
|
334
337
|
const failures: { label: string; message: string }[] = [];
|
|
@@ -1010,7 +1013,7 @@ export function resolveCompilation(
|
|
|
1010
1013
|
// base; the first base that matches any files wins, mirroring the first-existing-wins
|
|
1011
1014
|
// rule of @include resolution.
|
|
1012
1015
|
const rawPattern = substituteVarsStrict(target.entryGlob!, targetScope, `${where}.entryGlob`);
|
|
1013
|
-
const patternCandidates = resolveAliasPrefix(rawPattern, aliases);
|
|
1016
|
+
const patternCandidates = resolveAliasPrefix(rawPattern, aliases).map(toGlobPattern);
|
|
1014
1017
|
let pattern = patternCandidates[0];
|
|
1015
1018
|
let matchedFiles: string[] = [];
|
|
1016
1019
|
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 });
|