@sous-io/sous 0.1.1 → 0.2.0

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 (205) hide show
  1. package/README.md +115 -35
  2. package/bin/run.js +10 -1
  3. package/docs/markdown/README.md +27 -0
  4. package/docs/markdown/_sidebar.md +18 -0
  5. package/docs/markdown/commands.md +308 -0
  6. package/docs/markdown/config-discovery.md +74 -0
  7. package/docs/markdown/config-inspection.md +69 -0
  8. package/docs/markdown/config-layers.md +92 -0
  9. package/docs/markdown/config-variables.md +79 -0
  10. package/docs/markdown/configuration.md +71 -0
  11. package/docs/markdown/design-principles.md +59 -0
  12. package/docs/markdown/repositories-authoring.md +408 -0
  13. package/docs/markdown/repositories-consuming.md +580 -0
  14. package/docs/markdown/repositories-file-formats.md +1084 -0
  15. package/docs/markdown/repositories-variables.md +387 -0
  16. package/docs/markdown/repositories.md +303 -0
  17. package/docs/markdown/skill-categories.md +58 -0
  18. package/package.json +72 -8
  19. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/SKILL.tpl.md +20 -20
  20. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/examples/about-something.md +2 -2
  21. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/examples/do-something.md +1 -1
  22. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/advanced-patterns.md +6 -6
  23. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/commands.md +5 -5
  24. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/frontmatter.md +3 -3
  25. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-liquid-templates/SKILL.tpl.md +40 -25
  26. package/recipes/core/sous-skills/skills/about-sous/SKILL.tpl.md +70 -0
  27. package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +75 -0
  28. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/create-skill/SKILL.tpl.md +8 -9
  29. package/recipes/core/sous-skills/sous.recipe.yaml +45 -0
  30. package/sous.config.schema.json +337 -0
  31. package/src/base-command.ts +220 -67
  32. package/src/commands/build.ts +150 -73
  33. package/src/commands/clear.ts +23 -15
  34. package/src/commands/compile.ts +74 -16
  35. package/src/commands/config/get.ts +110 -0
  36. package/src/commands/config/show.ts +32 -0
  37. package/src/commands/config/validate.ts +53 -0
  38. package/src/commands/help.ts +46 -0
  39. package/src/commands/launch.ts +36 -14
  40. package/src/commands/lock/rebuild.ts +241 -0
  41. package/src/commands/lock/show.ts +115 -0
  42. package/src/commands/namespace/list.ts +117 -0
  43. package/src/commands/namespace/show.ts +110 -0
  44. package/src/commands/prune.ts +3 -11
  45. package/src/commands/recipe/list.ts +95 -0
  46. package/src/commands/recipe/show.ts +301 -0
  47. package/src/commands/repo/add.ts +145 -0
  48. package/src/commands/repo/gc.ts +172 -0
  49. package/src/commands/repo/init.ts +136 -0
  50. package/src/commands/repo/link.ts +500 -0
  51. package/src/commands/repo/list.ts +179 -0
  52. package/src/commands/repo/release.ts +619 -0
  53. package/src/commands/repo/remove.ts +193 -0
  54. package/src/commands/repo/search.ts +189 -0
  55. package/src/commands/repo/submit.ts +133 -0
  56. package/src/commands/repo/unlink.ts +147 -0
  57. package/src/commands/subscription/add.ts +285 -0
  58. package/src/commands/subscription/list.ts +129 -0
  59. package/src/commands/subscription/remove.ts +181 -0
  60. package/src/commands/vars/ask.ts +374 -0
  61. package/src/commands/vars/index.ts +79 -0
  62. package/src/commands/vars/list.ts +67 -0
  63. package/src/commands/vars/show.ts +77 -0
  64. package/src/config-command.ts +30 -0
  65. package/src/lib/build-service.ts +206 -54
  66. package/src/lib/config-discovery.ts +220 -27
  67. package/src/lib/config-inspect.ts +145 -0
  68. package/src/lib/config-kernel.mjs +377 -0
  69. package/src/lib/config-schema.ts +361 -0
  70. package/src/lib/env-file.ts +328 -0
  71. package/src/lib/env-local.ts +18 -1
  72. package/src/lib/errors.ts +32 -0
  73. package/src/lib/include-resolver.ts +108 -15
  74. package/src/lib/interactive.ts +165 -0
  75. package/src/lib/markdown-compiler.ts +118 -37
  76. package/src/lib/package-info.ts +25 -0
  77. package/src/lib/pid-service.ts +32 -21
  78. package/src/lib/refs/find.ts +589 -0
  79. package/src/lib/refs/index.ts +12 -0
  80. package/src/lib/refs/pick.ts +147 -0
  81. package/src/lib/refs/scopes.ts +61 -0
  82. package/src/lib/repos/catalog-display.ts +116 -0
  83. package/src/lib/repos/catalog-inputs.ts +160 -0
  84. package/src/lib/repos/catalog.ts +722 -0
  85. package/src/lib/repos/core-recipe.ts +105 -0
  86. package/src/lib/repos/defaults.ts +175 -0
  87. package/src/lib/repos/formats/common.ts +389 -0
  88. package/src/lib/repos/formats/index-file.ts +215 -0
  89. package/src/lib/repos/formats/links-map.ts +96 -0
  90. package/src/lib/repos/formats/lockfile.ts +167 -0
  91. package/src/lib/repos/formats/patterns.ts +57 -0
  92. package/src/lib/repos/formats/recipe-manifest.ts +395 -0
  93. package/src/lib/repos/formats/repo-manifest.ts +88 -0
  94. package/src/lib/repos/formats/store-entry.ts +84 -0
  95. package/src/lib/repos/freshness.ts +208 -0
  96. package/src/lib/repos/git-clone.ts +312 -0
  97. package/src/lib/repos/identity.ts +89 -0
  98. package/src/lib/repos/index.ts +58 -0
  99. package/src/lib/repos/links.ts +353 -0
  100. package/src/lib/repos/load-manifest.ts +236 -0
  101. package/src/lib/repos/lock-service.ts +453 -0
  102. package/src/lib/repos/locked-namespace-resolver.ts +90 -0
  103. package/src/lib/repos/locked-recipes.ts +254 -0
  104. package/src/lib/repos/managed-layer.ts +422 -0
  105. package/src/lib/repos/namespace-resolver.ts +370 -0
  106. package/src/lib/repos/providers/base.ts +206 -0
  107. package/src/lib/repos/providers/git.ts +233 -0
  108. package/src/lib/repos/providers/github.ts +294 -0
  109. package/src/lib/repos/providers/gitlab.ts +263 -0
  110. package/src/lib/repos/providers/http.ts +102 -0
  111. package/src/lib/repos/providers/index-cache.ts +382 -0
  112. package/src/lib/repos/providers/index.ts +106 -0
  113. package/src/lib/repos/providers/local.ts +391 -0
  114. package/src/lib/repos/providers/provider.ts +401 -0
  115. package/src/lib/repos/recipe-config-layers.ts +287 -0
  116. package/src/lib/repos/recipe-targets.ts +223 -0
  117. package/src/lib/repos/ref-search.ts +46 -0
  118. package/src/lib/repos/ref.ts +513 -0
  119. package/src/lib/repos/reference-report.ts +122 -0
  120. package/src/lib/repos/release/bump.ts +161 -0
  121. package/src/lib/repos/release/git-state.ts +305 -0
  122. package/src/lib/repos/release/index-builder.ts +635 -0
  123. package/src/lib/repos/release/index.ts +16 -0
  124. package/src/lib/repos/release/plan.ts +512 -0
  125. package/src/lib/repos/release/submit-service.ts +496 -0
  126. package/src/lib/repos/release/tags.ts +243 -0
  127. package/src/lib/repos/release/validate.ts +463 -0
  128. package/src/lib/repos/resolver.ts +789 -0
  129. package/src/lib/repos/scaffold/index.ts +238 -0
  130. package/src/lib/repos/scaffold/templates.ts +413 -0
  131. package/src/lib/repos/seed.ts +414 -0
  132. package/src/lib/repos/store/contract.ts +64 -0
  133. package/src/lib/repos/store/hash.ts +114 -0
  134. package/src/lib/repos/store/recipe-store.ts +599 -0
  135. package/src/lib/repos/store/settings.ts +58 -0
  136. package/src/lib/repos/subscription-service.ts +2678 -0
  137. package/src/lib/repos/trust.ts +447 -0
  138. package/src/lib/settings.ts +546 -189
  139. package/src/lib/sous-home.ts +104 -0
  140. package/src/lib/state.ts +52 -20
  141. package/src/lib/vars/ask.ts +1152 -0
  142. package/src/lib/vars/definition-source.ts +252 -0
  143. package/src/lib/vars/display.ts +233 -0
  144. package/src/lib/vars/index.ts +18 -0
  145. package/src/lib/vars/ladder.ts +282 -0
  146. package/src/lib/vars/mappings.ts +265 -0
  147. package/src/lib/vars/names.ts +94 -0
  148. package/src/lib/vars/preanswers.ts +395 -0
  149. package/src/lib/vars/question-plan.ts +218 -0
  150. package/src/lib/vars/report.ts +228 -0
  151. package/src/lib/vars/safe-regex.ts +235 -0
  152. package/src/lib/vars/validate.ts +312 -0
  153. package/src/lib/watch-loop.ts +148 -0
  154. package/src/templating/init-liquid-engine.ts +58 -16
  155. package/src/utils/choice-prompt.ts +143 -0
  156. package/src/utils/command-errors.ts +186 -0
  157. package/src/utils/command-help.ts +45 -0
  158. package/src/utils/confirm-prompt.ts +110 -0
  159. package/src/utils/flags.ts +153 -0
  160. package/src/utils/formatting.ts +540 -55
  161. package/src/utils/prompts.ts +35 -1
  162. package/src/utils/sous-directory.ts +245 -0
  163. package/src/utils/table.ts +603 -0
  164. package/src/utils/value-prompt.ts +119 -0
  165. package/shared-prompts/_partials/resume-task.md +0 -51
  166. package/shared-prompts/_partials/sub-agent-delegation.md +0 -32
  167. package/shared-prompts/_partials/update-task-file.md +0 -52
  168. package/shared-prompts/memories/automated-browser-tasks/INDEX.tpl.md +0 -52
  169. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/SKILL.tpl.md +0 -102
  170. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/auth-failure-handling.mjs +0 -81
  171. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/chained-workflow.mjs +0 -126
  172. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/simple-fetch.mjs +0 -92
  173. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/architecture.md +0 -61
  174. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/auth-and-sessions.md +0 -65
  175. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/ctx-api.md +0 -96
  176. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/installation.md +0 -104
  177. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/script-conventions.md +0 -243
  178. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/chrome-state.mjs +0 -148
  179. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/debug.mjs +0 -383
  180. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/debug.spec.mjs +0 -267
  181. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/eslint.config.mjs +0 -56
  182. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/harness.mjs +0 -169
  183. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/keyring.mjs +0 -59
  184. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/logger.mjs +0 -25
  185. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/params.mjs +0 -140
  186. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/run.mjs +0 -140
  187. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/settings.tpl.mjs +0 -1
  188. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/utils.mjs +0 -185
  189. package/shared-prompts/skills/automated-browser-tasks/create-automated-browser-task/SKILL.tpl.md +0 -52
  190. package/shared-prompts/skills/automated-browser-tasks/running-automated-browser-tasks/SKILL.tpl.md +0 -59
  191. package/shared-prompts/skills/automated-browser-tasks/update-automated-browser-task/SKILL.tpl.md +0 -47
  192. package/shared-prompts/skills/control-flow/approve/SKILL.tpl.md +0 -26
  193. package/shared-prompts/skills/control-flow/opine/SKILL.tpl.md +0 -58
  194. package/shared-prompts/skills/control-flow/repeat/SKILL.tpl.md +0 -27
  195. package/shared-prompts/skills/control-flow/research/SKILL.tpl.md +0 -34
  196. package/shared-prompts/skills/sous-skills/about-sous/SKILL.tpl.md +0 -51
  197. package/shared-prompts/skills/task-files/about-task-files/SKILL.tpl.md +0 -122
  198. package/shared-prompts/skills/task-files/continue-task-in-new-branch/SKILL.tpl.md +0 -80
  199. package/shared-prompts/skills/task-files/go/SKILL.tpl.md +0 -14
  200. package/shared-prompts/skills/task-files/resume-task/SKILL.tpl.md +0 -13
  201. package/shared-prompts/skills/task-files/start-task/SKILL.tpl.md +0 -93
  202. package/shared-prompts/skills/task-files/update/SKILL.tpl.md +0 -14
  203. package/shared-prompts/skills/task-files/update-task-file/SKILL.tpl.md +0 -13
  204. /package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/substitutions.md +0 -0
  205. /package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-liquid-templates/references/liquid-filters.md +0 -0
@@ -1,9 +1,10 @@
1
- import { Flags } from "@oclif/core";
2
1
  import { confirm } from "@inquirer/prompts";
3
2
  import fs from "node:fs";
4
3
  import { BaseCommand } from "../base-command.js";
5
4
  import { resolveStateFilePath } from "../lib/build-service.js";
6
- import { StateService } from "../lib/state.js";
5
+ import { isProtectedPath, StateService } from "../lib/state.js";
6
+ import { protectedRepoPaths } from "../lib/repos/links.js";
7
+ import { confirmationFlag } from "../utils/flags.js";
7
8
  import { displayError, footer, heading, log, showCommandVars } from "../utils/formatting.js";
8
9
 
9
10
  export default class Clear extends BaseCommand {
@@ -12,41 +13,48 @@ export default class Clear extends BaseCommand {
12
13
  static examples = [
13
14
  "<%= config.bin %> clear",
14
15
  "<%= config.bin %> clear --force",
16
+ "<%= config.bin %> clear -y",
15
17
  ];
16
18
 
17
19
  static flags = {
18
20
  ...BaseCommand.baseFlags,
19
- force: Flags.boolean({
20
- char: "f",
21
- description: "Skip confirmation prompt",
22
- default: false,
23
- }),
21
+ // `--force` is this command's original spelling, so it stays the primary
22
+ // one; `--yes` and `-y` are aliases of it, and the confirmation therefore
23
+ // answers the same way here as it does on every other command.
24
+ force: confirmationFlag({ primary: "force" }),
24
25
  };
25
26
 
26
27
  async run(): Promise<void> {
27
28
  const { flags } = await this.parse(Clear);
28
29
 
29
- const project = this.resolveProject(flags.project);
30
- const stateFilePath = resolveStateFilePath(project.key, this.settings, this.configContext);
30
+ const stateFilePath = resolveStateFilePath(this.settings, this.configContext);
31
31
 
32
32
  const stateService = new StateService();
33
33
  const state = await stateService.load(stateFilePath);
34
34
 
35
35
  if (!state) {
36
36
  displayError(
37
- `No state file found at ${stateFilePath}. Run 'xcv build' first, then 'xcv clear' to recover.`
37
+ `No state file found at ${stateFilePath}. Run 'sous build' first, then 'sous clear' to recover.`
38
38
  );
39
39
  this.exit(1);
40
40
  }
41
41
 
42
- showCommandVars({ Project: project.name, Config: this.configContext.configPath });
42
+ showCommandVars({ Project: this.projectLabel, Config: this.configContext.configPath });
43
43
 
44
- const fileCount = state!.files.length;
44
+ // A linked checkout holds somebody's unpushed edits and the recipe store is
45
+ // shared by every project on this machine, so neither is ever clearable,
46
+ // whatever a stale state entry claims.
47
+ const protectedPaths = protectedRepoPaths(this.configContext.sousDir);
48
+ const clearable = state!.files.filter(
49
+ (entry) => !isProtectedPath(entry.dest, protectedPaths)
50
+ );
51
+
52
+ const fileCount = clearable.length;
45
53
  const dirCount = state!.dirs.length;
46
54
 
47
55
  if (!flags.force) {
48
56
  const confirmed = await confirm({
49
- message: `Delete ${fileCount} file(s) and ${dirCount} director(ies) for project '${project.key}'?`,
57
+ message: `Delete ${fileCount} file(s) and ${dirCount} director(ies) for '${this.projectLabel}'?`,
50
58
  default: false,
51
59
  });
52
60
  if (!confirmed) {
@@ -57,8 +65,8 @@ export default class Clear extends BaseCommand {
57
65
 
58
66
  heading("Clearing");
59
67
 
60
- stateService.deleteTrackedFiles(state!.files, state!.dirs);
61
- for (const entry of state!.files) log(` ✗ ${entry.dest}`);
68
+ stateService.deleteTrackedFiles(clearable, state!.dirs, protectedPaths);
69
+ for (const entry of clearable) log(` ✗ ${entry.dest}`);
62
70
 
63
71
  // Delete state file itself
64
72
  if (fs.existsSync(stateFilePath)) {
@@ -1,17 +1,30 @@
1
1
  import { Flags } from "@oclif/core";
2
2
  import { BaseCommand } from "../base-command.js";
3
3
  import { CompilationService } from "../lib/markdown-compiler.js";
4
- import { resolveProjectCompilation, resolveRootScope, resolveWatchConfig } from "../lib/settings.js";
5
- import { resolveStateFilePath } from "../lib/build-service.js";
4
+ import { resolveCompilation, resolveRootScope } from "../lib/settings.js";
5
+ import {
6
+ resolveRecipeTargets,
7
+ resolveStateFilePath,
8
+ withRecipeTargets,
9
+ } from "../lib/build-service.js";
10
+ import { createProjectNamespaceResolver } from "../lib/repos/locked-namespace-resolver.js";
11
+ import { describeLinkedRepos } from "../lib/repos/links.js";
12
+ import { buildReloadWatchConfig, startConfigReloadWatch } from "../lib/watch-loop.js";
6
13
  import { WatchService } from "../lib/watch-service.js";
7
- import { displayError, footer, heading, log, showCommandVars } from "../utils/formatting.js";
14
+ import {
15
+ displayError,
16
+ footer,
17
+ heading,
18
+ log,
19
+ showCommandVars,
20
+ warning,
21
+ } from "../utils/formatting.js";
8
22
 
9
23
  export default class Compile extends BaseCommand {
10
24
  static description = "Compile markdown templates into output files";
11
25
 
12
26
  static examples = [
13
27
  "<%= config.bin %> compile",
14
- "<%= config.bin %> compile --project myproject",
15
28
  "<%= config.bin %> compile --rebuild",
16
29
  "<%= config.bin %> compile --dry-run",
17
30
  "<%= config.bin %> compile --strict",
@@ -41,19 +54,33 @@ export default class Compile extends BaseCommand {
41
54
  async run(): Promise<void> {
42
55
  const { flags } = await this.parse(Compile);
43
56
 
44
- const project = this.resolveProject(flags.project);
45
- const rootScope = resolveRootScope(this.settings, this.configContext);
46
- const config = resolveProjectCompilation(project, rootScope, this.settings, project.key);
57
+ // The recipes this project subscribes to contribute compile targets
58
+ // alongside its own; both go through the same compiler.
59
+ const withRecipes = () => {
60
+ const scope = resolveRootScope(this.settings, this.configContext);
61
+ const recipes = resolveRecipeTargets(this.settings, scope, this.configContext);
62
+ for (const notice of recipes.warnings) warning(notice);
63
+ return withRecipeTargets(
64
+ resolveCompilation(this.settings, scope),
65
+ recipes,
66
+ this.settings,
67
+ scope
68
+ );
69
+ };
70
+
71
+ for (const line of describeLinkedRepos(this.configContext.sousDir)) log(line);
72
+
73
+ const config = withRecipes();
47
74
 
48
75
  if (!config) {
49
- displayError(`Project '${project.key}' has no compilation config`);
76
+ displayError(`No compilation config found in ${this.configContext.configPath}`);
50
77
  this.exit(1);
51
78
  }
52
79
 
53
- const stateFilePath = resolveStateFilePath(project.key, this.settings, this.configContext);
80
+ const stateFilePath = resolveStateFilePath(this.settings, this.configContext);
54
81
 
55
82
  showCommandVars({
56
- Project: project.name,
83
+ Project: this.projectLabel,
57
84
  Config: this.configContext.configPath,
58
85
  Strict: flags.strict,
59
86
  Rebuild: flags.rebuild,
@@ -62,13 +89,24 @@ export default class Compile extends BaseCommand {
62
89
 
63
90
  heading("Compiling");
64
91
 
92
+ // Rebuilt on every compile rather than captured once: a watch-mode reload
93
+ // may follow a subscribe, and the lockfile is what this reads.
94
+ const namespaceResolver = () =>
95
+ createProjectNamespaceResolver({
96
+ sousDir: this.configContext.sousDir,
97
+ settings: this.settings,
98
+ });
99
+
65
100
  const compilerOptions = {
66
101
  strict: flags.strict,
67
102
  rebuild: flags.rebuild,
68
103
  dryRun: flags["dry-run"],
69
104
  };
70
105
 
71
- const compiler = new CompilationService(compilerOptions);
106
+ const compiler = new CompilationService({
107
+ ...compilerOptions,
108
+ namespaceResolver: namespaceResolver(),
109
+ });
72
110
  const success = await compiler.compile(config!, stateFilePath);
73
111
 
74
112
  footer();
@@ -78,15 +116,35 @@ export default class Compile extends BaseCommand {
78
116
  }
79
117
 
80
118
  if (flags.watch) {
81
- const watchConfig = resolveWatchConfig(project, rootScope, project.key);
82
119
  const watchService = new WatchService();
83
120
 
84
- watchService.watch(watchConfig, async (changedFile) => {
85
- log(`\nChange detected: ${changedFile}`);
121
+ // Recompiles using the command's CURRENT settings. Called for partial
122
+ // rebuilds (a changed source) and, after a clean reload, for full
123
+ // rebuilds. The compilation config and state-file path are re-resolved
124
+ // each time so a config reload takes effect. Owns the "Recompiling"
125
+ // heading/footer. (`changedFile` is accepted for parity with build and
126
+ // logged by the watch loop; compile always does a full recompile.)
127
+ const rebuild = async (_changedFile?: string) => {
128
+ const currentConfig = withRecipes();
129
+ if (!currentConfig) {
130
+ displayError(`No compilation config found in ${this.configContext.configPath}`);
131
+ return;
132
+ }
133
+ const currentStateFilePath = resolveStateFilePath(this.settings, this.configContext);
86
134
  heading("Recompiling");
87
- const recompiler = new CompilationService(compilerOptions);
88
- await recompiler.compile(config!, stateFilePath);
135
+ const recompiler = new CompilationService({
136
+ ...compilerOptions,
137
+ namespaceResolver: namespaceResolver(),
138
+ });
139
+ await recompiler.compile(currentConfig, currentStateFilePath);
89
140
  footer();
141
+ };
142
+
143
+ startConfigReloadWatch({
144
+ watchService,
145
+ buildWatchConfig: () => buildReloadWatchConfig(this.settings, this.configContext),
146
+ rebuild,
147
+ reloadConfig: () => this.reloadDiscoveredConfig(),
90
148
  });
91
149
 
92
150
  await new Promise(() => {}); // keep process alive
@@ -0,0 +1,110 @@
1
+ import path from "node:path";
2
+ import { Args, Flags } from "@oclif/core";
3
+ import { ConfigCommand } from "../../config-command.js";
4
+ import { ConfigError, loadSettingsWithLayers } from "../../lib/settings.js";
5
+ import {
6
+ isScalar,
7
+ lookupPath,
8
+ NOT_FOUND,
9
+ parsePath,
10
+ renderJson,
11
+ truncateJson,
12
+ } from "../../lib/config-inspect.js";
13
+
14
+ export default class ConfigGet extends ConfigCommand {
15
+ static description =
16
+ "Print one value from the merged config by dot-path (e.g. compilation.targets[0].entryPoint)";
17
+
18
+ /**
19
+ * The other spelling of the topic. It lives under a hidden topic, so it is
20
+ * typable everywhere without ever reaching the top-level listing.
21
+ */
22
+ static aliases = ["configs:get"];
23
+
24
+ static examples = [
25
+ "<%= config.bin %> config get name",
26
+ "<%= config.bin %> config get compilation.targets[0].entryPoint",
27
+ "<%= config.bin %> config get compilation --layers",
28
+ ];
29
+
30
+ static args = {
31
+ path: Args.string({
32
+ description: "Dot-path into the config, with [n] for array indices",
33
+ required: true,
34
+ }),
35
+ };
36
+
37
+ static flags = {
38
+ ...ConfigCommand.baseFlags,
39
+ layers: Flags.boolean({
40
+ description: "Show which config layer set the value (per-layer provenance)",
41
+ default: false,
42
+ }),
43
+ };
44
+
45
+ async run(): Promise<void> {
46
+ const { args, flags } = await this.parse(ConfigGet);
47
+ const segments = parsePath(args.path);
48
+
49
+ if (flags.layers) {
50
+ await this.showLayers(args.path, segments);
51
+ return;
52
+ }
53
+
54
+ const value = lookupPath(this.settings, segments);
55
+ if (value === NOT_FOUND) {
56
+ throw new ConfigError(
57
+ `No value at config path '${args.path}'.\n` +
58
+ ` It is not present in the merged config (${this.configContext.configPath}).\n` +
59
+ ` Run 'sous config show' to see the whole config.`
60
+ );
61
+ }
62
+
63
+ // Scalars print raw (no quotes) so the output is directly usable; objects and
64
+ // arrays print as pretty JSON, colorized only for a TTY.
65
+ if (isScalar(value)) {
66
+ process.stdout.write(`${String(value)}\n`);
67
+ } else {
68
+ process.stdout.write(`${renderJson(value, Boolean(process.stdout.isTTY))}\n`);
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Prints one line per config layer whose cumulative snapshot CHANGED the value
74
+ * at `pathStr`, showing old -> new (JSON-encoded, truncated). The first layer
75
+ * counts as a change from (unset).
76
+ */
77
+ private async showLayers(pathStr: string, segments: (string | number)[]): Promise<void> {
78
+ const { layers } = await loadSettingsWithLayers(this.discovered, { trace: true });
79
+ const sousDir = this.configContext.sousDir;
80
+
81
+ let previous: unknown = NOT_FOUND;
82
+ let printedAny = false;
83
+
84
+ for (const layer of layers) {
85
+ const current = lookupPath(layer.config, segments);
86
+ // JSON round-trip so structurally-equal values compare equal.
87
+ const unchanged =
88
+ current !== NOT_FOUND
89
+ ? previous !== NOT_FOUND && JSON.stringify(current) === JSON.stringify(previous)
90
+ : previous === NOT_FOUND;
91
+ if (unchanged) continue;
92
+
93
+ const rel = path.relative(sousDir, layer.path);
94
+ const label = rel && !rel.startsWith("..") ? rel : layer.path;
95
+ const oldStr = previous === NOT_FOUND ? "(unset)" : truncateJson(previous);
96
+ const newStr = current === NOT_FOUND ? "(unset)" : truncateJson(current);
97
+ process.stdout.write(`${label}: ${oldStr} -> ${newStr}\n`);
98
+
99
+ previous = current;
100
+ printedAny = true;
101
+ }
102
+
103
+ if (!printedAny) {
104
+ throw new ConfigError(
105
+ `No value at config path '${pathStr}' in any config layer.\n` +
106
+ ` Run 'sous config show' to see the whole merged config.`
107
+ );
108
+ }
109
+ }
110
+ }
@@ -0,0 +1,32 @@
1
+ import { ConfigCommand } from "../../config-command.js";
2
+ import { renderJson } from "../../lib/config-inspect.js";
3
+
4
+ export default class ConfigShow extends ConfigCommand {
5
+ static description =
6
+ "Print the merged config (all conf.d layers merged, before variable resolution) as JSON";
7
+
8
+ /**
9
+ * The other spelling of the topic. It lives under a hidden topic, so it is
10
+ * typable everywhere without ever reaching the top-level listing.
11
+ */
12
+ static aliases = ["configs:show"];
13
+
14
+ static examples = [
15
+ "<%= config.bin %> config show",
16
+ "<%= config.bin %> config show | jq .compilation",
17
+ ];
18
+
19
+ static flags = {
20
+ ...ConfigCommand.baseFlags,
21
+ };
22
+
23
+ async run(): Promise<void> {
24
+ await this.parse(ConfigShow);
25
+
26
+ // this.settings is the merged config as written: every conf.d layer has been
27
+ // deep-merged and the whole thing schema-validated, but no ${var} has been
28
+ // resolved yet. Colorize only for a TTY so piped output stays valid JSON.
29
+ const useColor = Boolean(process.stdout.isTTY);
30
+ process.stdout.write(`${renderJson(this.settings, useColor)}\n`);
31
+ }
32
+ }
@@ -0,0 +1,53 @@
1
+ import { BaseCommand } from "../../base-command.js";
2
+ import {
3
+ resolveCompilation,
4
+ resolveRootScope,
5
+ resolveTools,
6
+ resolveWatchConfig,
7
+ } from "../../lib/settings.js";
8
+ import { footer, heading, showVariables } from "../../utils/formatting.js";
9
+
10
+ export default class ConfigValidate extends BaseCommand {
11
+ static description =
12
+ "Validate the merged config: schema, then full variable resolution (fixpoint + substitution)";
13
+
14
+ /**
15
+ * The other spelling of the topic. It lives under a hidden topic, so it is
16
+ * typable everywhere without ever reaching the top-level listing.
17
+ */
18
+ static aliases = ["configs:validate"];
19
+
20
+ static examples = ["<%= config.bin %> config validate"];
21
+
22
+ static flags = {
23
+ ...BaseCommand.baseFlags,
24
+ };
25
+
26
+ async run(): Promise<void> {
27
+ await this.parse(ConfigValidate);
28
+
29
+ // Discovery, the config kernel, assertFlatConfig and zod validation have all
30
+ // already run in BaseCommand.init(); a failure there exits before we get here.
31
+ // Running the resolvers now surfaces the errors validation alone cannot:
32
+ // fixpoint cycles, undefined ${vars}, and unresolved substitutions. Any
33
+ // ConfigError they throw is rendered by BaseCommand.catch (non-zero exit).
34
+ const rootScope = resolveRootScope(this.settings, this.configContext);
35
+ resolveCompilation(this.settings, rootScope);
36
+ const tools = resolveTools(this.settings, rootScope);
37
+ resolveWatchConfig(this.settings, rootScope);
38
+
39
+ const layerCount = this.configContext.layerPaths?.length ?? 1;
40
+ const targetCount = this.settings.compilation?.targets.length ?? 0;
41
+ const toolNames = Object.keys(tools);
42
+
43
+ heading("Config is valid");
44
+ showVariables({
45
+ "Config File": this.configContext.configPath,
46
+ Layers: layerCount,
47
+ Targets: targetCount,
48
+ Tools: toolNames.length > 0 ? toolNames.join(", ") : "(none)",
49
+ });
50
+
51
+ footer();
52
+ }
53
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * `sous help`, `sous help <command>`, `sous help <topic>`, `sous help <topic> <command>`.
3
+ *
4
+ * The word form of `--help`, and deliberately the same screen: it hands the
5
+ * arguments straight to the help class oclif itself uses for `--help`, so there
6
+ * is exactly one help renderer and no second copy to fall out of date.
7
+ *
8
+ * This exists because `sous help build` is what people type, and without a
9
+ * command by that name oclif answers "command help not found", which reads like
10
+ * sous has no help at all. Adding it here rather than installing
11
+ * `@oclif/plugin-help` keeps the dependency list as it is.
12
+ *
13
+ * It does not extend `BaseCommand`: reading the help must work from anywhere,
14
+ * including a directory with no `.sous/` above it, and `BaseCommand` requires a
15
+ * discoverable config.
16
+ */
17
+
18
+ import { Args, Command, loadHelpClass } from "@oclif/core";
19
+
20
+ export default class Help extends Command {
21
+ static description = "Print the help for sous, or for one command or topic";
22
+
23
+ static examples = [
24
+ "<%= config.bin %> help",
25
+ "<%= config.bin %> help build",
26
+ "<%= config.bin %> help repo",
27
+ "<%= config.bin %> help repo add",
28
+ ];
29
+
30
+ /** A command id is one or more words, so the argument list is open-ended. */
31
+ static strict = false;
32
+
33
+ static args = {
34
+ command: Args.string({
35
+ description: "The command or topic to describe, such as 'build' or 'repo add'",
36
+ required: false,
37
+ }),
38
+ };
39
+
40
+ async run(): Promise<void> {
41
+ const { argv } = await this.parse(Help);
42
+ const HelpClass = await loadHelpClass(this.config);
43
+ const help = new HelpClass(this.config, this.config.pjson.oclif.helpOptions);
44
+ await help.showHelp(argv as string[]);
45
+ }
46
+ }
@@ -3,17 +3,19 @@ import { spawnSync } from "node:child_process";
3
3
  import fs from "node:fs";
4
4
  import { BaseCommand } from "../base-command.js";
5
5
  import { BuildService } from "../lib/build-service.js";
6
- import { resolveProjectTools, resolveRootScope, resolveScope } from "../lib/settings.js";
6
+ import { resolveRootScope, resolveTools } from "../lib/settings.js";
7
7
  import { displayError, footer, heading, showCommandVars } from "../utils/formatting.js";
8
8
 
9
9
  export default class Launch extends BaseCommand {
10
- static description = "Build and launch a coding agent for a project";
10
+ static description =
11
+ "Build this project's outputs, then start a coding agent configured in its config";
11
12
 
12
13
  static examples = [
13
14
  "<%= config.bin %> launch claude",
14
15
  "<%= config.bin %> launch codex --no-build",
15
16
  "<%= config.bin %> launch claude --continuous",
16
- "<%= config.bin %> launch codex --project myproject",
17
+ "<%= config.bin %> launch claude --resume",
18
+ "<%= config.bin %> launch claude -- -c",
17
19
  ];
18
20
 
19
21
  static args = {
@@ -26,7 +28,7 @@ export default class Launch extends BaseCommand {
26
28
  static flags = {
27
29
  ...BaseCommand.baseFlags,
28
30
  "no-build": Flags.boolean({
29
- description: "Skip xcv build before launching",
31
+ description: "Skip sous build before launching",
30
32
  default: false,
31
33
  }),
32
34
  continuous: Flags.boolean({
@@ -35,32 +37,52 @@ export default class Launch extends BaseCommand {
35
37
  }),
36
38
  };
37
39
 
40
+ // Any argument launch does not recognize is forwarded to the tool, after the
41
+ // config-defined tool args. Flags that collide with sous's own (e.g. claude's
42
+ // -c / --continue vs sous's -c / --config) can be forced through with `--`:
43
+ // everything after it is forwarded verbatim.
44
+ //
45
+ // strict=false lets extra positionals through; "--"=false is what routes
46
+ // unknown flags into argv instead of a NonExistentFlagsError (oclif rejects
47
+ // them even in non-strict mode otherwise). Disabling oclif's `--` handling
48
+ // means it would keep parsing sous flags past the separator, so run() splits
49
+ // argv at the first `--` itself, before oclif ever sees the tail.
50
+ static strict = false;
51
+ static "--" = false;
52
+
38
53
  async run(): Promise<void> {
39
- const { args, flags } = await this.parse(Launch);
54
+ const sepIndex = this.argv.indexOf("--");
55
+ const ownArgv = sepIndex === -1 ? this.argv : this.argv.slice(0, sepIndex);
56
+ const verbatimArgs = sepIndex === -1 ? [] : this.argv.slice(sepIndex + 1);
57
+
58
+ const { args, argv, flags } = await this.parse(Launch, ownArgv);
59
+
60
+ // argv holds every token oclif did not claim, in original order; the first
61
+ // is the tool name itself.
62
+ const passThroughArgs = [...argv.slice(1).map(String), ...verbatimArgs];
40
63
 
41
- const project = this.resolveProject(flags.project);
42
64
  const rootScope = resolveRootScope(this.settings, this.configContext);
43
- const tools = resolveProjectTools(project, rootScope, project.key);
65
+ const tools = resolveTools(this.settings, rootScope);
44
66
  const toolConfig = tools[args.tool];
45
67
 
46
68
  if (!toolConfig) {
47
69
  displayError(
48
- `Tool '${args.tool}' not found in project '${project.key}'. ` +
70
+ `Tool '${args.tool}' not found in ${this.configContext.configPath}. ` +
49
71
  `Available tools: ${Object.keys(tools).join(", ") || "(none)"}`
50
72
  );
51
73
  this.exit(1);
52
74
  }
53
75
 
54
- const projectScope = resolveScope(project._vars ?? {}, rootScope);
55
- const projectRoot = projectScope.projectRoot ?? process.cwd();
76
+ const projectRoot = rootScope.projectRoot ?? process.cwd();
56
77
 
57
78
  showCommandVars({
58
- Project: project.name,
79
+ Project: this.projectLabel,
59
80
  Config: this.configContext.configPath,
60
81
  Tool: args.tool,
61
82
  "Project Root": projectRoot,
62
83
  "No Build": flags["no-build"],
63
84
  Continuous: flags.continuous,
85
+ ...(passThroughArgs.length > 0 && { "Tool Args": passThroughArgs.join(" ") }),
64
86
  });
65
87
 
66
88
  const buildService = new BuildService();
@@ -69,14 +91,14 @@ export default class Launch extends BaseCommand {
69
91
  // Build step (unless --no-build)
70
92
  if (!flags["no-build"]) {
71
93
  heading("Building");
72
- await buildService.build(project.key, this.settings, {
94
+ await buildService.build(this.settings, {
73
95
  configContext: this.configContext,
74
96
  });
75
97
  footer();
76
98
  }
77
99
 
78
- // Resolve promptFile content
79
- const launchArgs = [...(toolConfig.args ?? [])];
100
+ // Config-defined args first, then pass-through, then promptFile content
101
+ const launchArgs = [...(toolConfig.args ?? []), ...passThroughArgs];
80
102
  if (toolConfig.promptFile) {
81
103
  if (!fs.existsSync(toolConfig.promptFile)) {
82
104
  displayError(`promptFile not found: ${toolConfig.promptFile}`);