@sous-io/sous 0.1.0 → 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 (206) hide show
  1. package/README.md +121 -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 +73 -9
  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/bin/xcv +0 -5
  166. package/shared-prompts/_partials/resume-task.md +0 -51
  167. package/shared-prompts/_partials/sub-agent-delegation.md +0 -32
  168. package/shared-prompts/_partials/update-task-file.md +0 -52
  169. package/shared-prompts/memories/automated-browser-tasks/INDEX.tpl.md +0 -52
  170. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/SKILL.tpl.md +0 -102
  171. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/auth-failure-handling.mjs +0 -81
  172. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/chained-workflow.mjs +0 -126
  173. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/simple-fetch.mjs +0 -92
  174. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/architecture.md +0 -61
  175. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/auth-and-sessions.md +0 -65
  176. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/ctx-api.md +0 -96
  177. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/installation.md +0 -104
  178. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/script-conventions.md +0 -243
  179. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/chrome-state.mjs +0 -148
  180. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/debug.mjs +0 -383
  181. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/debug.spec.mjs +0 -267
  182. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/eslint.config.mjs +0 -56
  183. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/harness.mjs +0 -169
  184. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/keyring.mjs +0 -59
  185. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/logger.mjs +0 -25
  186. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/params.mjs +0 -140
  187. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/run.mjs +0 -140
  188. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/settings.tpl.mjs +0 -1
  189. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/utils.mjs +0 -185
  190. package/shared-prompts/skills/automated-browser-tasks/create-automated-browser-task/SKILL.tpl.md +0 -52
  191. package/shared-prompts/skills/automated-browser-tasks/running-automated-browser-tasks/SKILL.tpl.md +0 -59
  192. package/shared-prompts/skills/automated-browser-tasks/update-automated-browser-task/SKILL.tpl.md +0 -47
  193. package/shared-prompts/skills/control-flow/approve/SKILL.tpl.md +0 -26
  194. package/shared-prompts/skills/control-flow/opine/SKILL.tpl.md +0 -58
  195. package/shared-prompts/skills/control-flow/repeat/SKILL.tpl.md +0 -27
  196. package/shared-prompts/skills/control-flow/research/SKILL.tpl.md +0 -34
  197. package/shared-prompts/skills/sous-skills/about-sous/SKILL.tpl.md +0 -51
  198. package/shared-prompts/skills/task-files/about-task-files/SKILL.tpl.md +0 -122
  199. package/shared-prompts/skills/task-files/continue-task-in-new-branch/SKILL.tpl.md +0 -80
  200. package/shared-prompts/skills/task-files/go/SKILL.tpl.md +0 -14
  201. package/shared-prompts/skills/task-files/resume-task/SKILL.tpl.md +0 -13
  202. package/shared-prompts/skills/task-files/start-task/SKILL.tpl.md +0 -93
  203. package/shared-prompts/skills/task-files/update/SKILL.tpl.md +0 -14
  204. package/shared-prompts/skills/task-files/update-task-file/SKILL.tpl.md +0 -13
  205. /package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/substitutions.md +0 -0
  206. /package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-liquid-templates/references/liquid-filters.md +0 -0
@@ -1,267 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { existsSync, readFileSync, rmSync, mkdtempSync } from 'fs';
3
- import { join } from 'path';
4
- import { tmpdir } from 'os';
5
- import {
6
- createDebug,
7
- slug,
8
- truncate,
9
- formatElement,
10
- formatValue,
11
- regexParts,
12
- escapeRegExp,
13
- } from './debug.mjs';
14
-
15
- // --- Pure helpers ----------------------------------------------------------
16
-
17
- describe('slug()', () => {
18
- it('replaces unsafe characters with dashes', () => {
19
- expect(slug('Foo Bar/baz!!')).toBe('Foo-Bar-baz');
20
- });
21
- it('trims leading/trailing dashes', () => {
22
- expect(slug(' !!hi!! ')).toBe('hi');
23
- });
24
- it('falls back to "debug" for empty/blank input', () => {
25
- expect(slug(' ')).toBe('debug');
26
- expect(slug('')).toBe('debug');
27
- });
28
- it('caps length at 80 chars', () => {
29
- expect(slug('a'.repeat(200)).length).toBe(80);
30
- });
31
- });
32
-
33
- describe('truncate()', () => {
34
- it('returns the string unchanged when within max', () => {
35
- expect(truncate('hello', 10)).toBe('hello');
36
- });
37
- it('cuts and annotates with total length when over max', () => {
38
- expect(truncate('abcdefghij', 5)).toBe('abcde… (10 chars total)');
39
- });
40
- it('handles null/undefined as empty', () => {
41
- expect(truncate(null, 5)).toBe('');
42
- expect(truncate(undefined, 5)).toBe('');
43
- });
44
- });
45
-
46
- describe('formatElement()', () => {
47
- it('formats tag, role, href, and text', () => {
48
- expect(formatElement({ tag: 'A', role: 'tab', href: '/x', text: 'Checks', visible: true }))
49
- .toBe('a role=tab href=/x — "Checks"');
50
- });
51
- it('marks hidden elements', () => {
52
- expect(formatElement({ tag: 'DIV', visible: false, text: '' })).toBe('div (hidden)');
53
- });
54
- it('omits text segment when there is no text', () => {
55
- expect(formatElement({ tag: 'BUTTON', visible: true })).toBe('button');
56
- });
57
- });
58
-
59
- describe('formatValue()', () => {
60
- it('stringifies objects compactly', () => {
61
- expect(formatValue({ a: 1 })).toBe('{"a":1}');
62
- });
63
- it('passes through scalars', () => {
64
- expect(formatValue(42)).toBe('42');
65
- expect(formatValue(null)).toBe('null');
66
- expect(formatValue(undefined)).toBe('undefined');
67
- });
68
- });
69
-
70
- describe('regexParts() / escapeRegExp()', () => {
71
- it('decomposes a RegExp into source + flags', () => {
72
- expect(regexParts(/SUCCEEDED|FAILED/i)).toEqual({ source: 'SUCCEEDED|FAILED', flags: 'i' });
73
- });
74
- it('escapes a string pattern and defaults to case-insensitive', () => {
75
- expect(regexParts('a.b')).toEqual({ source: 'a\\.b', flags: 'i' });
76
- });
77
- it('escapeRegExp escapes regex metacharacters', () => {
78
- expect(escapeRegExp('a.b*c')).toBe('a\\.b\\*c');
79
- });
80
- });
81
-
82
- // --- Page-driven tools (fake page) -----------------------------------------
83
-
84
- /** A logger that records messages instead of printing. */
85
- function fakeLogger() {
86
- const messages = [];
87
- const mk = () => ({
88
- info: (m) => messages.push(`info:${m}`),
89
- warn: (m) => messages.push(`warn:${m}`),
90
- error: (m) => messages.push(`error:${m}`),
91
- child: () => mk(),
92
- });
93
- const l = mk();
94
- l.messages = messages;
95
- return l;
96
- }
97
-
98
- /** A configurable fake Playwright Page recording what the tools ask of it. */
99
- function fakePage(overrides = {}) {
100
- const calls = { screenshot: [], evaluate: 0, waitForTimeout: [] };
101
- return {
102
- calls,
103
- url: () => overrides.url ?? 'https://example.com/page',
104
- title: async () => overrides.title ?? 'Example Title',
105
- content: async () => overrides.content ?? '<html><body>hi</body></html>',
106
- async screenshot(o) {
107
- calls.screenshot.push(o);
108
- if (overrides.screenshotThrows) throw new Error('boom');
109
- // emulate playwright writing the file
110
- const { writeFileSync } = await import('fs');
111
- writeFileSync(o.path, 'PNG');
112
- },
113
- async evaluate(fn, arg) {
114
- calls.evaluate += 1;
115
- if (typeof overrides.evaluate === 'function') return overrides.evaluate(fn, arg);
116
- return overrides.evaluateResult ?? null;
117
- },
118
- async $$eval(sel, fn, arg) {
119
- if (typeof overrides.$$eval === 'function') return overrides.$$eval(sel, fn, arg);
120
- return overrides.$$evalResult ?? [];
121
- },
122
- async waitForTimeout(ms) {
123
- calls.waitForTimeout.push(ms);
124
- },
125
- };
126
- }
127
-
128
- describe('createDebug() — artifacts', () => {
129
- let dir;
130
- beforeEach(() => {
131
- dir = mkdtempSync(join(tmpdir(), 'debug-spec-'));
132
- });
133
- afterEach(() => rmSync(dir, { recursive: true, force: true }));
134
-
135
- it('does not create the dir until something is written', () => {
136
- const sub = join(dir, 'lazy');
137
- createDebug(fakePage(), fakeLogger(), { dir: sub });
138
- expect(existsSync(sub)).toBe(false);
139
- });
140
-
141
- it('screenshot writes a .png and returns its path', async () => {
142
- const debug = createDebug(fakePage(), fakeLogger(), { dir });
143
- const path = await debug.screenshot('shot one');
144
- expect(path).toBe(join(dir, 'shot-one.png'));
145
- expect(existsSync(path)).toBe(true);
146
- });
147
-
148
- it('screenshot returns null (never throws) on failure', async () => {
149
- const debug = createDebug(fakePage({ screenshotThrows: true }), fakeLogger(), { dir });
150
- expect(await debug.screenshot('x')).toBeNull();
151
- });
152
-
153
- it('html writes page content to a .html file', async () => {
154
- const debug = createDebug(fakePage({ content: '<p>real</p>' }), fakeLogger(), { dir });
155
- const path = await debug.html('snap');
156
- expect(readFileSync(path, 'utf8')).toBe('<p>real</p>');
157
- });
158
-
159
- it('dump captures url/title/text and writes a text artifact', async () => {
160
- const page = fakePage({ url: 'https://x/y', title: 'T', evaluateResult: 'BODY TEXT' });
161
- const debug = createDebug(page, fakeLogger(), { dir });
162
- const snap = await debug.dump('d');
163
- expect(snap.url).toBe('https://x/y');
164
- expect(snap.title).toBe('T');
165
- expect(snap.textPreview).toContain('BODY TEXT');
166
- expect(readFileSync(snap.files.text, 'utf8')).toContain('BODY TEXT');
167
- expect(existsSync(snap.files.screenshot)).toBe(true);
168
- });
169
- });
170
-
171
- describe('createDebug() — inspection tools', () => {
172
- let dir;
173
- beforeEach(() => { dir = mkdtempSync(join(tmpdir(), 'debug-spec-')); });
174
- afterEach(() => rmSync(dir, { recursive: true, force: true }));
175
-
176
- it('describe reports count and per-element summaries', async () => {
177
- const els = [{ tag: 'A', role: 'tab', text: 'Checks', visible: true }];
178
- const page = fakePage({
179
- $$eval: (_sel, _fn, arg) => (typeof arg === 'number' ? els : els.length),
180
- });
181
- const debug = createDebug(page, fakeLogger(), { dir });
182
- const out = await debug.describe('a', { limit: 5 });
183
- expect(out.count).toBe(1);
184
- expect(out.elements[0].text).toBe('Checks');
185
- });
186
-
187
- it('count returns the number of matches', async () => {
188
- const debug = createDebug(fakePage({ $$evalResult: 7 }), fakeLogger(), { dir });
189
- expect(await debug.count('div')).toBe(7);
190
- });
191
-
192
- it('clickables returns the evaluated list', async () => {
193
- const list = [{ tag: 'BUTTON', text: 'Go' }];
194
- const debug = createDebug(fakePage({ evaluateResult: list }), fakeLogger(), { dir });
195
- expect(await debug.clickables()).toEqual(list);
196
- });
197
-
198
- it('watch samples the metric N times and spaces them by interval', async () => {
199
- let n = 0;
200
- const page = fakePage({ evaluate: () => ++n });
201
- const debug = createDebug(page, fakeLogger(), { dir });
202
- const series = await debug.watch(() => 0, { samples: 3, intervalMs: 250, label: 'len' });
203
- expect(series.map((s) => s.value)).toEqual([1, 2, 3]);
204
- expect(series.map((s) => s.t)).toEqual([0, 250, 500]);
205
- // waits between samples only (N-1 times)
206
- expect(page.calls.waitForTimeout).toEqual([250, 250]);
207
- });
208
-
209
- it('tools degrade gracefully when the page throws', async () => {
210
- const page = fakePage({ evaluate: () => { throw new Error('nope'); }, $$eval: () => { throw new Error('nope'); } });
211
- const debug = createDebug(page, fakeLogger(), { dir });
212
- expect(await debug.count('x')).toBe(0);
213
- expect(await debug.clickables()).toEqual([]);
214
- expect((await debug.describe('x')).count).toBe(0);
215
- });
216
-
217
- it('findText passes the pattern source/flags through to the page', async () => {
218
- let received;
219
- const page = fakePage({ evaluate: (_fn, arg) => { received = arg; return []; } });
220
- const debug = createDebug(page, fakeLogger(), { dir });
221
- await debug.findText(/SUCCEEDED|FAILED/i, { limit: 3 });
222
- expect(received).toMatchObject({ source: 'SUCCEEDED|FAILED', flags: 'i', limit: 3 });
223
- });
224
-
225
- it('findText returns whatever the page evaluation yields', async () => {
226
- const hits = [{ text: 'Checks', leafTag: 'DIV', clickableAncestor: { tag: 'DIV', role: 'tab' } }];
227
- const debug = createDebug(fakePage({ evaluateResult: hits }), fakeLogger(), { dir });
228
- expect(await debug.findText('Checks')).toEqual(hits);
229
- });
230
- });
231
-
232
- /**
233
- * The own-text matching rule that findText runs in-browser, replicated here as a
234
- * pure check. This is the logic that the leaf-only version got wrong: an element
235
- * may own a matching text node AND have element children (e.g. a tab label beside
236
- * an icon). The in-browser behavior itself is covered by live tests.
237
- */
238
- describe('findText own-text matching rule', () => {
239
- // Mirror of the in-browser predicate: match on an element's OWN text nodes.
240
- const ownText = (node) => (node.childNodes || [])
241
- .filter((n) => n.nodeType === 3)
242
- .map((n) => n.textContent)
243
- .join('');
244
- const matches = (node, re) => re.test(ownText(node));
245
-
246
- it('matches an element whose own text node holds the pattern, even with element children', () => {
247
- // <div role=tab>"Checks"<span(icon)/></div> — a common real-world tab shape.
248
- const tab = {
249
- childNodes: [
250
- { nodeType: 3, textContent: 'Checks' },
251
- { nodeType: 1, textContent: '' }, // icon element child
252
- ],
253
- };
254
- expect(matches(tab, /Checks/i)).toBe(true);
255
- });
256
-
257
- it('does NOT match an ancestor whose text comes only from descendants', () => {
258
- // A wrapper with no own text nodes — only an element child that contains text.
259
- const wrapper = { childNodes: [{ nodeType: 1, textContent: 'Checks' }] };
260
- expect(matches(wrapper, /Checks/i)).toBe(false);
261
- });
262
-
263
- it('matches a plain leaf', () => {
264
- const leaf = { childNodes: [{ nodeType: 3, textContent: 'SUCCEEDED' }] };
265
- expect(matches(leaf, /SUCCEEDED/)).toBe(true);
266
- });
267
- });
@@ -1,56 +0,0 @@
1
- /**
2
- * ESLint flat config for automation scripts.
3
- *
4
- * Scope: mechanical correctness only — unused vars, missing awaits, undeclared
5
- * globals, obvious foot-guns. The *conventions* (decomposition, doc-blocks,
6
- * destructuring, no self-validation, logger usage) are enforced by the skill
7
- * instructions and review, NOT by lint rules; do not try to encode them here.
8
- *
9
- * Usage (from a project that has eslint installed):
10
- * npx eslint --config <this-file> <scriptsDir> <examplesDir>
11
- *
12
- * Requires: eslint >= 9 (flat config). No plugins needed.
13
- */
14
-
15
- export default [
16
- {
17
- files: ['**/*.mjs'],
18
- // Templates are LiquidJS, not valid JS until compiled by sous.
19
- ignores: ['**/*.tpl.mjs'],
20
- languageOptions: {
21
- ecmaVersion: 2023,
22
- sourceType: 'module',
23
- globals: {
24
- // Node globals scripts and the harness rely on.
25
- process: 'readonly',
26
- console: 'readonly',
27
- Buffer: 'readonly',
28
- URL: 'readonly',
29
- setTimeout: 'readonly',
30
- clearTimeout: 'readonly',
31
- __dirname: 'readonly',
32
- },
33
- },
34
- rules: {
35
- // Catch real bugs.
36
- 'no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
37
- 'no-undef': 'error',
38
- 'no-constant-condition': ['error', { checkLoops: false }],
39
- 'no-unreachable': 'error',
40
- 'no-dupe-keys': 'error',
41
- 'no-self-assign': 'error',
42
- 'require-await': 'warn',
43
- 'no-return-await': 'warn',
44
-
45
- // Async foot-guns: a forgotten await on a Playwright call is the most
46
- // common scripting mistake.
47
- 'no-async-promise-executor': 'error',
48
- 'no-await-in-loop': 'off', // sequential page steps legitimately await in loops
49
-
50
- // Style nudges that aid debugging without dictating structure.
51
- 'prefer-const': 'warn',
52
- 'no-var': 'error',
53
- eqeqeq: ['warn', 'smart'],
54
- },
55
- },
56
- ];
@@ -1,169 +0,0 @@
1
- /**
2
- * Execution Harness
3
- *
4
- * Launches headless Playwright with cookies extracted from a Chrome profile.
5
- * Handles auth detection, structured output, and error reporting.
6
- */
7
-
8
- import { chromium } from 'playwright';
9
- import { buildStorageState } from './chrome-state.mjs';
10
- import { createLogger } from './logger.mjs';
11
- import { createUtils } from './utils.mjs';
12
- import { createDebug } from './debug.mjs';
13
- import { resolveParams, ParamError } from './params.mjs';
14
-
15
- export class AuthError extends Error {
16
- constructor(message, { url, indicators } = {}) {
17
- super(message);
18
- this.name = 'AuthError';
19
- this.url = url;
20
- this.indicators = indicators || [];
21
- }
22
- }
23
-
24
- /**
25
- * Run an automation script with Chrome session state injected.
26
- *
27
- * @param {object} script - The script module (must export `execute` and `meta`)
28
- * @param {object} params - Explicit parameters (e.g. from CLI)
29
- * @param {object} options - Harness options
30
- * @param {string} options.profileName - Chrome profile name (default: 'Default')
31
- * @param {string[]} options.domains - Limit cookie extraction to these domain substrings
32
- * @param {number} options.timeout - Overall timeout in ms (default: 60000)
33
- * @param {object} options.settings - Compiled project settings (ctx.settings)
34
- */
35
- export async function runScript(script, params = {}, options = {}) {
36
- const {
37
- profileName = 'Default',
38
- domains = null,
39
- timeout = 60000,
40
- settings = {},
41
- } = options;
42
-
43
- const meta = script.meta || {};
44
- const sessionLog = createLogger('session');
45
-
46
- let browser = null;
47
- let debug = null;
48
-
49
- try {
50
- // Resolve + validate params BEFORE launching anything. A script never
51
- // validates its own input; a ParamError here surfaces before browser work.
52
- const resolvedParams = resolveParams(meta, params, settings);
53
-
54
- sessionLog.info(`Extracting cookies from Chrome profile: "${profileName}"`);
55
- const storageState = await buildStorageState(profileName, domains);
56
- sessionLog.info(`Extracted ${storageState.cookies.length} cookies`);
57
- if (domains) {
58
- sessionLog.info(`Filtered to domains: ${domains.join(', ')}`);
59
- }
60
-
61
- sessionLog.info('Launching headless browser...');
62
- browser = await chromium.launch({
63
- headless: true,
64
- args: [
65
- '--disable-blink-features=AutomationControlled',
66
- '--no-first-run',
67
- '--no-default-browser-check',
68
- ],
69
- });
70
-
71
- const context = await browser.newContext({
72
- storageState,
73
- viewport: { width: 1920, height: 1080 },
74
- });
75
-
76
- const page = await context.newPage();
77
- sessionLog.info('Browser ready\n');
78
-
79
- const logger = createLogger(meta.name || 'script');
80
- debug = createDebug(page, logger, { runId: meta.name || 'script' });
81
-
82
- const ctx = {
83
- page,
84
- context,
85
- browser,
86
- params: resolvedParams,
87
- settings,
88
- timeout,
89
- logger,
90
- utils: createUtils(page, logger),
91
- debug,
92
-
93
- async checkAuth() {
94
- const currentUrl = page.url();
95
- const authIndicators = [
96
- '/login', '/auth/', '/sso', 'signin', 'multipass',
97
- 'accounts.google.com', 'login.microsoftonline.com',
98
- ];
99
-
100
- for (const indicator of authIndicators) {
101
- if (currentUrl.includes(indicator)) {
102
- throw new AuthError(
103
- `Authentication required. The browser was redirected to a login page.\n` +
104
- `Current URL: ${currentUrl}\n\n` +
105
- `Action needed: Log in to the target site in your Chrome browser ` +
106
- `(profile: "${profileName}"), then retry this script.`,
107
- { url: currentUrl, indicators: [indicator] }
108
- );
109
- }
110
- }
111
- },
112
-
113
- async runChild(childScript, childParams = {}) {
114
- const childMeta = childScript.meta || {};
115
- const childResolved = resolveParams(childMeta, childParams, settings);
116
- const childLogger = createLogger(childMeta.name || 'child');
117
- const childCtx = {
118
- ...ctx,
119
- params: childResolved,
120
- logger: childLogger,
121
- utils: createUtils(page, childLogger),
122
- debug: createDebug(page, childLogger, { runId: childMeta.name || 'child' }),
123
- };
124
- return childScript.execute(childCtx);
125
- },
126
- };
127
-
128
- const result = await Promise.race([
129
- script.execute(ctx),
130
- new Promise((_, reject) =>
131
- setTimeout(() => reject(new Error(`Script timed out after ${timeout}ms`)), timeout)
132
- ),
133
- ]);
134
-
135
- return { success: true, data: result };
136
- } catch (error) {
137
- if (error instanceof ParamError) {
138
- return { success: false, error: 'params', message: error.message };
139
- }
140
-
141
- if (error instanceof AuthError) {
142
- return {
143
- success: false,
144
- error: 'auth',
145
- message: error.message,
146
- url: error.url,
147
- indicators: error.indicators,
148
- };
149
- }
150
-
151
- // Auto-capture page state on an unexpected script failure — the single most
152
- // useful debugging artifact. Done before `finally` closes the browser.
153
- let debugDump = null;
154
- if (debug) {
155
- const snapshot = await debug.dump('failure').catch(() => null);
156
- debugDump = snapshot?.dir || null;
157
- }
158
-
159
- return {
160
- success: false,
161
- error: 'script',
162
- message: error.message,
163
- stack: error.stack,
164
- debugDir: debugDump,
165
- };
166
- } finally {
167
- if (browser) await browser.close().catch(() => {});
168
- }
169
- }
@@ -1,59 +0,0 @@
1
- /**
2
- * Read Chrome's encryption password from the GNOME keyring via D-Bus Secret Service API.
3
- * Pure JS — no Python, no native dependencies beyond dbus-next.
4
- */
5
-
6
- import dbus from 'dbus-next';
7
-
8
- const SECRET_SERVICE_IFACE = 'org.freedesktop.Secret.Service';
9
- const SECRET_SERVICE_PATH = '/org/freedesktop/secrets';
10
- const SECRET_SERVICE_BUS = 'org.freedesktop.secrets';
11
-
12
- /**
13
- * Reads the Chrome Safe Storage password from gnome-keyring.
14
- * Returns a Buffer containing the password bytes.
15
- */
16
- export async function getChromeSafeStoragePassword() {
17
- const bus = dbus.sessionBus();
18
-
19
- try {
20
- const serviceProxy = await bus.getProxyObject(SECRET_SERVICE_BUS, SECRET_SERVICE_PATH);
21
- const service = serviceProxy.getInterface(SECRET_SERVICE_IFACE);
22
-
23
- // Open a plain-text session (no encryption over D-Bus — it's local)
24
- const sessionResult = await service.OpenSession('plain', new dbus.Variant('s', ''));
25
- const sessionPath = sessionResult[1];
26
-
27
- // Search for the Chrome Safe Storage item
28
- // SearchItems takes a{ss} — plain string dict, not Variants
29
- const searchResult = await service.SearchItems({
30
- 'application': 'chrome',
31
- 'xdg:schema': 'chrome_libsecret_os_crypt_password_v2',
32
- });
33
-
34
- const unlocked = searchResult[0];
35
- if (!unlocked || unlocked.length === 0) {
36
- throw new Error(
37
- 'Chrome Safe Storage key not found in keyring. ' +
38
- 'Make sure Chrome has been launched at least once and the keyring is unlocked.'
39
- );
40
- }
41
-
42
- const itemPath = unlocked[0];
43
-
44
- // Get the secret
45
- const secretsResult = await service.GetSecrets([itemPath], sessionPath);
46
-
47
- // secretsResult is a dict: {itemPath: (session_path, params_bytes, value_bytes, content_type)}
48
- const secret = secretsResult[itemPath];
49
- if (!secret) {
50
- throw new Error('Failed to retrieve secret from keyring');
51
- }
52
-
53
- // secret is [session_path, params_bytes, value_bytes, content_type_string]
54
- const valueBytes = secret[2];
55
- return Buffer.from(valueBytes);
56
- } finally {
57
- bus.disconnect();
58
- }
59
- }
@@ -1,25 +0,0 @@
1
- /**
2
- * Logger
3
- *
4
- * Plain prefixed-line logger for automation scripts. Output format:
5
- * [script-name] message
6
- * [script-name:section] message (from a child logger)
7
- *
8
- * warn/error are routed to stderr and tagged with their level.
9
- */
10
-
11
- /**
12
- * Create a logger for a script. `child(section)` returns a logger whose
13
- * prefix is extended with `:section`.
14
- *
15
- * @param {string} prefix - Initial prefix (usually the script's meta.name)
16
- * @returns {{info: Function, warn: Function, error: Function, child: Function}}
17
- */
18
- export function createLogger(prefix) {
19
- return {
20
- info: (msg) => console.log(`[${prefix}] ${msg}`),
21
- warn: (msg) => console.warn(`[${prefix}] WARN ${msg}`),
22
- error: (msg) => console.error(`[${prefix}] ERROR ${msg}`),
23
- child: (section) => createLogger(`${prefix}:${section}`),
24
- };
25
- }