@sous-io/sous 0.1.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 (82) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +154 -0
  3. package/bin/run.js +17 -0
  4. package/bin/xcv +5 -0
  5. package/package.json +81 -0
  6. package/shared-prompts/_partials/resume-task.md +51 -0
  7. package/shared-prompts/_partials/sub-agent-delegation.md +32 -0
  8. package/shared-prompts/_partials/update-task-file.md +52 -0
  9. package/shared-prompts/memories/automated-browser-tasks/INDEX.tpl.md +52 -0
  10. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/SKILL.tpl.md +102 -0
  11. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/auth-failure-handling.mjs +81 -0
  12. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/chained-workflow.mjs +126 -0
  13. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/simple-fetch.mjs +92 -0
  14. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/architecture.md +61 -0
  15. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/auth-and-sessions.md +65 -0
  16. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/ctx-api.md +96 -0
  17. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/installation.md +104 -0
  18. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/script-conventions.md +243 -0
  19. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/chrome-state.mjs +148 -0
  20. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/debug.mjs +383 -0
  21. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/debug.spec.mjs +267 -0
  22. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/eslint.config.mjs +56 -0
  23. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/harness.mjs +169 -0
  24. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/keyring.mjs +59 -0
  25. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/logger.mjs +25 -0
  26. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/params.mjs +140 -0
  27. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/run.mjs +140 -0
  28. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/settings.tpl.mjs +1 -0
  29. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/utils.mjs +185 -0
  30. package/shared-prompts/skills/automated-browser-tasks/create-automated-browser-task/SKILL.tpl.md +52 -0
  31. package/shared-prompts/skills/automated-browser-tasks/running-automated-browser-tasks/SKILL.tpl.md +59 -0
  32. package/shared-prompts/skills/automated-browser-tasks/update-automated-browser-task/SKILL.tpl.md +47 -0
  33. package/shared-prompts/skills/control-flow/approve/SKILL.tpl.md +26 -0
  34. package/shared-prompts/skills/control-flow/opine/SKILL.tpl.md +58 -0
  35. package/shared-prompts/skills/control-flow/repeat/SKILL.tpl.md +27 -0
  36. package/shared-prompts/skills/control-flow/research/SKILL.tpl.md +34 -0
  37. package/shared-prompts/skills/sous-skills/about-agent-skills/SKILL.tpl.md +177 -0
  38. package/shared-prompts/skills/sous-skills/about-agent-skills/examples/about-something.md +45 -0
  39. package/shared-prompts/skills/sous-skills/about-agent-skills/examples/do-something.md +33 -0
  40. package/shared-prompts/skills/sous-skills/about-agent-skills/references/advanced-patterns.md +87 -0
  41. package/shared-prompts/skills/sous-skills/about-agent-skills/references/commands.md +46 -0
  42. package/shared-prompts/skills/sous-skills/about-agent-skills/references/frontmatter.md +25 -0
  43. package/shared-prompts/skills/sous-skills/about-agent-skills/references/substitutions.md +50 -0
  44. package/shared-prompts/skills/sous-skills/about-liquid-templates/SKILL.tpl.md +268 -0
  45. package/shared-prompts/skills/sous-skills/about-liquid-templates/references/liquid-filters.md +82 -0
  46. package/shared-prompts/skills/sous-skills/about-sous/SKILL.tpl.md +51 -0
  47. package/shared-prompts/skills/sous-skills/create-skill/SKILL.tpl.md +114 -0
  48. package/shared-prompts/skills/task-files/about-task-files/SKILL.tpl.md +122 -0
  49. package/shared-prompts/skills/task-files/continue-task-in-new-branch/SKILL.tpl.md +80 -0
  50. package/shared-prompts/skills/task-files/go/SKILL.tpl.md +14 -0
  51. package/shared-prompts/skills/task-files/resume-task/SKILL.tpl.md +13 -0
  52. package/shared-prompts/skills/task-files/start-task/SKILL.tpl.md +93 -0
  53. package/shared-prompts/skills/task-files/update/SKILL.tpl.md +14 -0
  54. package/shared-prompts/skills/task-files/update-task-file/SKILL.tpl.md +13 -0
  55. package/src/base-command.ts +163 -0
  56. package/src/commands/build.ts +196 -0
  57. package/src/commands/clear.ts +71 -0
  58. package/src/commands/compile.ts +95 -0
  59. package/src/commands/launch.ts +111 -0
  60. package/src/commands/prune.ts +48 -0
  61. package/src/lib/build-service.ts +258 -0
  62. package/src/lib/config-discovery.ts +199 -0
  63. package/src/lib/env-local.ts +195 -0
  64. package/src/lib/include-resolver.ts +146 -0
  65. package/src/lib/markdown-compiler.ts +580 -0
  66. package/src/lib/pid-service.ts +88 -0
  67. package/src/lib/settings.ts +695 -0
  68. package/src/lib/state.ts +135 -0
  69. package/src/lib/watch-service.ts +115 -0
  70. package/src/templating/filters/bullet-list.ts +9 -0
  71. package/src/templating/filters/index.ts +8 -0
  72. package/src/templating/init-liquid-engine.ts +82 -0
  73. package/src/templating/lib/glob-files.ts +74 -0
  74. package/src/templating/lib/import-export.ts +32 -0
  75. package/src/templating/lib/tag-args.ts +19 -0
  76. package/src/templating/tags/exportScalarVarsJs.ts +43 -0
  77. package/src/templating/tags/getFiles.ts +89 -0
  78. package/src/templating/tags/index.ts +14 -0
  79. package/src/templating/tags/listFiles.ts +54 -0
  80. package/src/templating/tags/showVars.ts +22 -0
  81. package/src/utils/formatting.ts +338 -0
  82. package/src/utils/prompts.ts +19 -0
@@ -0,0 +1,196 @@
1
+ import path from "node:path";
2
+ import { Flags } from "@oclif/core";
3
+ import { BaseCommand } from "../base-command.js";
4
+ import { BuildService } from "../lib/build-service.js";
5
+ import { PidService } from "../lib/pid-service.js";
6
+ import { CLI_ROOT, loadSettings, resolveRootScope, resolveScope, resolveWatchConfig } from "../lib/settings.js";
7
+ import type { WatchHandle } from "../lib/watch-service.js";
8
+ import { WatchService } from "../lib/watch-service.js";
9
+ import { footer, heading, log, showCommandVars } from "../utils/formatting.js";
10
+
11
+ export default class Build extends BaseCommand {
12
+ static description = "Compile outputs and prune stale files (compile + prune)";
13
+
14
+ static examples = [
15
+ "<%= config.bin %> build",
16
+ "<%= config.bin %> build --no-prune",
17
+ "<%= config.bin %> build --rebuild",
18
+ "<%= config.bin %> build --dry-run",
19
+ ];
20
+
21
+ static flags = {
22
+ ...BaseCommand.baseFlags,
23
+ "no-prune": Flags.boolean({
24
+ description: "Skip the prune step",
25
+ default: false,
26
+ }),
27
+ "no-compile": Flags.boolean({
28
+ description: "Skip the compile step (prune only)",
29
+ default: false,
30
+ }),
31
+ rebuild: Flags.boolean({
32
+ description: "Ignore cached hashes and reprocess all outputs",
33
+ default: false,
34
+ }),
35
+ "dry-run": Flags.boolean({
36
+ description: "Print what would be written/pruned without making changes",
37
+ default: false,
38
+ }),
39
+ strict: Flags.boolean({
40
+ description: "Fail on any compilation error",
41
+ default: false,
42
+ }),
43
+ watch: Flags.boolean({
44
+ char: "w",
45
+ description: "Watch source files and rebuild on changes",
46
+ default: false,
47
+ }),
48
+ };
49
+
50
+ async run(): Promise<void> {
51
+ const { flags } = await this.parse(Build);
52
+
53
+ const project = this.resolveProject(flags.project);
54
+
55
+ showCommandVars({
56
+ Project: project.name,
57
+ Config: this.configContext.configPath,
58
+ Rebuild: flags.rebuild,
59
+ "Dry Run": flags["dry-run"],
60
+ "No Compile": flags["no-compile"],
61
+ "No Prune": flags["no-prune"],
62
+ });
63
+
64
+ heading("Building");
65
+
66
+ const buildOptions = {
67
+ strict: flags.strict,
68
+ rebuild: flags.rebuild,
69
+ dryRun: flags["dry-run"],
70
+ noCompile: flags["no-compile"],
71
+ noPrune: flags["no-prune"],
72
+ configContext: this.configContext,
73
+ };
74
+
75
+ const buildService = new BuildService();
76
+ const success = await buildService.build(project.key, this.settings, buildOptions);
77
+
78
+ footer();
79
+
80
+ if (!success && !flags.watch) {
81
+ this.exit(1);
82
+ }
83
+
84
+ if (flags.watch) {
85
+ const configPath = this.configContext.configPath;
86
+
87
+ const rootScope = resolveRootScope(this.settings, this.configContext);
88
+ const projectScope = resolveScope(project._vars ?? {}, rootScope);
89
+
90
+ // --- PID file enforcement ---
91
+ const pidService = new PidService();
92
+ const pidFilePath = pidService.getFilePath(project.key, projectScope, this.projectCount);
93
+ await pidService.acquire(pidFilePath, project.key);
94
+
95
+ let isRebuilding = false;
96
+
97
+ const cleanup = async (watchHandle?: WatchHandle) => {
98
+ if (process.stdin.isTTY) {
99
+ process.stdin.setRawMode(false);
100
+ }
101
+ process.stdin.pause();
102
+ if (watchHandle) await watchHandle.stop();
103
+ await pidService.release(pidFilePath);
104
+ };
105
+
106
+ process.on("SIGINT", () => { void cleanup().then(() => process.exit(0)); });
107
+ process.on("SIGTERM", () => { void cleanup().then(() => process.exit(0)); });
108
+
109
+ const watchService = new WatchService();
110
+
111
+ /**
112
+ * Builds a WatchConfig from current settings, injecting the config file path
113
+ * and the templating directory into fullRebuildPaths.
114
+ */
115
+ const buildWatchConfig = () => {
116
+ const currentRootScope = resolveRootScope(this.settings, this.configContext);
117
+ const config = resolveWatchConfig(project, currentRootScope, project.key);
118
+ config.fullRebuildPaths = [
119
+ ...(config.fullRebuildPaths ?? []),
120
+ configPath,
121
+ path.join(CLI_ROOT, "src", "templating"),
122
+ ];
123
+ return config;
124
+ };
125
+
126
+ /** Starts a new watcher and updates the shared handle reference. */
127
+ const startWatcher = (handle: { current: WatchHandle | null }) => {
128
+ const watchConfig = buildWatchConfig();
129
+ handle.current = watchService.watch(watchConfig, async (event) => {
130
+ if (event.type === "partial") {
131
+ if (isRebuilding) return;
132
+ isRebuilding = true;
133
+ log(`\nChange detected: ${event.filePath}`);
134
+ heading("Rebuilding");
135
+ await buildService.build(project.key, this.settings, {
136
+ ...buildOptions,
137
+ // --rebuild means full clean build on every trigger; skip partial optimisation
138
+ changedFile: buildOptions.rebuild ? undefined : event.filePath,
139
+ });
140
+ footer();
141
+ isRebuilding = false;
142
+ } else {
143
+ // Full rebuild: stop current watcher, reload settings, restart
144
+ if (isRebuilding) return;
145
+ isRebuilding = true;
146
+ log(`\nConfig changed (${event.filePath}), reloading settings and restarting watcher...`);
147
+ await handle.current!.stop();
148
+
149
+ this.settings = await loadSettings(configPath);
150
+
151
+ heading("Rebuilding");
152
+ await buildService.build(project.key, this.settings, buildOptions);
153
+ footer();
154
+ isRebuilding = false;
155
+
156
+ startWatcher(handle);
157
+ }
158
+ });
159
+ };
160
+
161
+ const handle: { current: WatchHandle | null } = { current: null };
162
+ startWatcher(handle);
163
+
164
+ const triggerFullRebuild = async (reason: string) => {
165
+ if (isRebuilding) return;
166
+ isRebuilding = true;
167
+ log(`\n${reason}`);
168
+ heading("Rebuilding");
169
+ await buildService.build(project.key, this.settings, buildOptions);
170
+ footer();
171
+ isRebuilding = false;
172
+ };
173
+
174
+ // Display the interactive prompt
175
+ log("[ Press Q to quit | any other key: rebuild ]");
176
+
177
+ if (process.stdin.isTTY) {
178
+ process.stdin.setRawMode(true);
179
+ process.stdin.resume();
180
+ process.stdin.setEncoding("utf8");
181
+
182
+ process.stdin.on("data", (key: string) => {
183
+ // q, Q, or Ctrl+C → clean exit
184
+ if (key === "q" || key === "Q" || key === "\u0003") {
185
+ void cleanup(handle.current ?? undefined).then(() => process.exit(0));
186
+ return;
187
+ }
188
+ // Any other key → trigger a full rebuild immediately (bypass debounce)
189
+ void triggerFullRebuild("Manual rebuild triggered.");
190
+ });
191
+ }
192
+
193
+ await new Promise(() => {}); // keep process alive
194
+ }
195
+ }
196
+ }
@@ -0,0 +1,71 @@
1
+ import { Flags } from "@oclif/core";
2
+ import { confirm } from "@inquirer/prompts";
3
+ import fs from "node:fs";
4
+ import { BaseCommand } from "../base-command.js";
5
+ import { resolveStateFilePath } from "../lib/build-service.js";
6
+ import { StateService } from "../lib/state.js";
7
+ import { displayError, footer, heading, log, showCommandVars } from "../utils/formatting.js";
8
+
9
+ export default class Clear extends BaseCommand {
10
+ static description = "Delete all files and directories written by Sous for a project";
11
+
12
+ static examples = [
13
+ "<%= config.bin %> clear",
14
+ "<%= config.bin %> clear --force",
15
+ ];
16
+
17
+ static flags = {
18
+ ...BaseCommand.baseFlags,
19
+ force: Flags.boolean({
20
+ char: "f",
21
+ description: "Skip confirmation prompt",
22
+ default: false,
23
+ }),
24
+ };
25
+
26
+ async run(): Promise<void> {
27
+ const { flags } = await this.parse(Clear);
28
+
29
+ const project = this.resolveProject(flags.project);
30
+ const stateFilePath = resolveStateFilePath(project.key, this.settings, this.configContext);
31
+
32
+ const stateService = new StateService();
33
+ const state = await stateService.load(stateFilePath);
34
+
35
+ if (!state) {
36
+ displayError(
37
+ `No state file found at ${stateFilePath}. Run 'xcv build' first, then 'xcv clear' to recover.`
38
+ );
39
+ this.exit(1);
40
+ }
41
+
42
+ showCommandVars({ Project: project.name, Config: this.configContext.configPath });
43
+
44
+ const fileCount = state!.files.length;
45
+ const dirCount = state!.dirs.length;
46
+
47
+ if (!flags.force) {
48
+ const confirmed = await confirm({
49
+ message: `Delete ${fileCount} file(s) and ${dirCount} director(ies) for project '${project.key}'?`,
50
+ default: false,
51
+ });
52
+ if (!confirmed) {
53
+ log("Aborted.");
54
+ return;
55
+ }
56
+ }
57
+
58
+ heading("Clearing");
59
+
60
+ stateService.deleteTrackedFiles(state!.files, state!.dirs);
61
+ for (const entry of state!.files) log(` ✗ ${entry.dest}`);
62
+
63
+ // Delete state file itself
64
+ if (fs.existsSync(stateFilePath)) {
65
+ fs.rmSync(stateFilePath);
66
+ log(` ✗ ${stateFilePath} (state file)`);
67
+ }
68
+
69
+ footer();
70
+ }
71
+ }
@@ -0,0 +1,95 @@
1
+ import { Flags } from "@oclif/core";
2
+ import { BaseCommand } from "../base-command.js";
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";
6
+ import { WatchService } from "../lib/watch-service.js";
7
+ import { displayError, footer, heading, log, showCommandVars } from "../utils/formatting.js";
8
+
9
+ export default class Compile extends BaseCommand {
10
+ static description = "Compile markdown templates into output files";
11
+
12
+ static examples = [
13
+ "<%= config.bin %> compile",
14
+ "<%= config.bin %> compile --project myproject",
15
+ "<%= config.bin %> compile --rebuild",
16
+ "<%= config.bin %> compile --dry-run",
17
+ "<%= config.bin %> compile --strict",
18
+ ];
19
+
20
+ static flags = {
21
+ ...BaseCommand.baseFlags,
22
+ strict: Flags.boolean({
23
+ description: "Fail immediately on any error",
24
+ default: false,
25
+ }),
26
+ rebuild: Flags.boolean({
27
+ description: "Ignore cached hashes and reprocess all outputs",
28
+ default: false,
29
+ }),
30
+ "dry-run": Flags.boolean({
31
+ description: "Print what would be written without making changes",
32
+ default: false,
33
+ }),
34
+ watch: Flags.boolean({
35
+ char: "w",
36
+ description: "Watch source files and recompile on changes",
37
+ default: false,
38
+ }),
39
+ };
40
+
41
+ async run(): Promise<void> {
42
+ const { flags } = await this.parse(Compile);
43
+
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);
47
+
48
+ if (!config) {
49
+ displayError(`Project '${project.key}' has no compilation config`);
50
+ this.exit(1);
51
+ }
52
+
53
+ const stateFilePath = resolveStateFilePath(project.key, this.settings, this.configContext);
54
+
55
+ showCommandVars({
56
+ Project: project.name,
57
+ Config: this.configContext.configPath,
58
+ Strict: flags.strict,
59
+ Rebuild: flags.rebuild,
60
+ "Dry Run": flags["dry-run"],
61
+ });
62
+
63
+ heading("Compiling");
64
+
65
+ const compilerOptions = {
66
+ strict: flags.strict,
67
+ rebuild: flags.rebuild,
68
+ dryRun: flags["dry-run"],
69
+ };
70
+
71
+ const compiler = new CompilationService(compilerOptions);
72
+ const success = await compiler.compile(config!, stateFilePath);
73
+
74
+ footer();
75
+
76
+ if (!success && flags.strict && !flags.watch) {
77
+ this.exit(1);
78
+ }
79
+
80
+ if (flags.watch) {
81
+ const watchConfig = resolveWatchConfig(project, rootScope, project.key);
82
+ const watchService = new WatchService();
83
+
84
+ watchService.watch(watchConfig, async (changedFile) => {
85
+ log(`\nChange detected: ${changedFile}`);
86
+ heading("Recompiling");
87
+ const recompiler = new CompilationService(compilerOptions);
88
+ await recompiler.compile(config!, stateFilePath);
89
+ footer();
90
+ });
91
+
92
+ await new Promise(() => {}); // keep process alive
93
+ }
94
+ }
95
+ }
@@ -0,0 +1,111 @@
1
+ import { Args, Flags } from "@oclif/core";
2
+ import { spawnSync } from "node:child_process";
3
+ import fs from "node:fs";
4
+ import { BaseCommand } from "../base-command.js";
5
+ import { BuildService } from "../lib/build-service.js";
6
+ import { resolveProjectTools, resolveRootScope, resolveScope } from "../lib/settings.js";
7
+ import { displayError, footer, heading, showCommandVars } from "../utils/formatting.js";
8
+
9
+ export default class Launch extends BaseCommand {
10
+ static description = "Build and launch a coding agent for a project";
11
+
12
+ static examples = [
13
+ "<%= config.bin %> launch claude",
14
+ "<%= config.bin %> launch codex --no-build",
15
+ "<%= config.bin %> launch claude --continuous",
16
+ "<%= config.bin %> launch codex --project myproject",
17
+ ];
18
+
19
+ static args = {
20
+ tool: Args.string({
21
+ description: "Tool to launch (e.g. claude, codex)",
22
+ required: true,
23
+ }),
24
+ };
25
+
26
+ static flags = {
27
+ ...BaseCommand.baseFlags,
28
+ "no-build": Flags.boolean({
29
+ description: "Skip xcv build before launching",
30
+ default: false,
31
+ }),
32
+ continuous: Flags.boolean({
33
+ description: "Restart the agent automatically on exit",
34
+ default: false,
35
+ }),
36
+ };
37
+
38
+ async run(): Promise<void> {
39
+ const { args, flags } = await this.parse(Launch);
40
+
41
+ const project = this.resolveProject(flags.project);
42
+ const rootScope = resolveRootScope(this.settings, this.configContext);
43
+ const tools = resolveProjectTools(project, rootScope, project.key);
44
+ const toolConfig = tools[args.tool];
45
+
46
+ if (!toolConfig) {
47
+ displayError(
48
+ `Tool '${args.tool}' not found in project '${project.key}'. ` +
49
+ `Available tools: ${Object.keys(tools).join(", ") || "(none)"}`
50
+ );
51
+ this.exit(1);
52
+ }
53
+
54
+ const projectScope = resolveScope(project._vars ?? {}, rootScope);
55
+ const projectRoot = projectScope.projectRoot ?? process.cwd();
56
+
57
+ showCommandVars({
58
+ Project: project.name,
59
+ Config: this.configContext.configPath,
60
+ Tool: args.tool,
61
+ "Project Root": projectRoot,
62
+ "No Build": flags["no-build"],
63
+ Continuous: flags.continuous,
64
+ });
65
+
66
+ const buildService = new BuildService();
67
+
68
+ do {
69
+ // Build step (unless --no-build)
70
+ if (!flags["no-build"]) {
71
+ heading("Building");
72
+ await buildService.build(project.key, this.settings, {
73
+ configContext: this.configContext,
74
+ });
75
+ footer();
76
+ }
77
+
78
+ // Resolve promptFile content
79
+ const launchArgs = [...(toolConfig.args ?? [])];
80
+ if (toolConfig.promptFile) {
81
+ if (!fs.existsSync(toolConfig.promptFile)) {
82
+ displayError(`promptFile not found: ${toolConfig.promptFile}`);
83
+ this.exit(1);
84
+ }
85
+ const promptContent = fs.readFileSync(toolConfig.promptFile, "utf8");
86
+ launchArgs.push(promptContent);
87
+ }
88
+
89
+ heading(`Launching ${args.tool}`);
90
+
91
+ // Run the tool with inherited stdio (interactive)
92
+ const result = spawnSync(toolConfig.command, launchArgs, {
93
+ cwd: projectRoot,
94
+ stdio: "inherit",
95
+ shell: false,
96
+ });
97
+
98
+ if (result.error) {
99
+ displayError(`Failed to launch '${args.tool}': ${result.error.message}`);
100
+ this.exit(1);
101
+ }
102
+
103
+ if (flags.continuous) {
104
+ // Small delay to avoid tight spin on immediate crash
105
+ await new Promise(resolve => setTimeout(resolve, 150));
106
+ }
107
+ } while (flags.continuous);
108
+
109
+ footer();
110
+ }
111
+ }
@@ -0,0 +1,48 @@
1
+ import { Flags } from "@oclif/core";
2
+ import { BaseCommand } from "../base-command.js";
3
+ import { BuildService, resolveStateFilePath } from "../lib/build-service.js";
4
+ import { footer, heading, showCommandVars } from "../utils/formatting.js";
5
+
6
+ export default class Prune extends BaseCommand {
7
+ static description = "Remove output files that are no longer in the current config";
8
+
9
+ static examples = [
10
+ "<%= config.bin %> prune",
11
+ "<%= config.bin %> prune --dry-run",
12
+ "<%= config.bin %> prune --project myproject",
13
+ ];
14
+
15
+ static flags = {
16
+ ...BaseCommand.baseFlags,
17
+ "dry-run": Flags.boolean({
18
+ description: "Print what would be pruned without deleting",
19
+ default: false,
20
+ }),
21
+ };
22
+
23
+ async run(): Promise<void> {
24
+ const { flags } = await this.parse(Prune);
25
+
26
+ const project = this.resolveProject(flags.project);
27
+ const stateFilePath = resolveStateFilePath(project.key, this.settings, this.configContext);
28
+
29
+ showCommandVars({
30
+ Project: project.name,
31
+ Config: this.configContext.configPath,
32
+ "Dry Run": flags["dry-run"],
33
+ });
34
+
35
+ heading("Pruning");
36
+
37
+ const buildService = new BuildService();
38
+ await buildService.prune(
39
+ project.key,
40
+ this.settings,
41
+ stateFilePath,
42
+ flags["dry-run"],
43
+ this.configContext
44
+ );
45
+
46
+ footer();
47
+ }
48
+ }