@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sous-io/sous",
3
- "version": "0.2.35",
3
+ "version": "0.2.37",
4
4
  "description": "Compiles AI coding agent configuration (CLAUDE.md, skills, memories) from LiquidJS templates",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -11,7 +11,7 @@ formatVersion: 1
11
11
 
12
12
  namespace: core
13
13
  name: sous-skills
14
- version: 0.2.35
14
+ version: 0.2.37
15
15
 
16
16
  description: >-
17
17
  The skills that teach an agent what sous is and how it works: which files sous
@@ -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
- const matches = globSync(pattern, { absolute: true })
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(compareBytewise);
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: { cwd?: string }
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) => path.join(recipe.dir, 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
- const result = await run(manager, query.args, {});
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 {
@@ -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
- { label: "tsx", args: ["--import", tsxPath, CONFIG_KERNEL_PATH] },
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) {
@@ -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 });