@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,56 @@
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
+ ];
@@ -0,0 +1,169 @@
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
+ }
@@ -0,0 +1,59 @@
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
+ }
@@ -0,0 +1,25 @@
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
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Param resolution and validation.
3
+ *
4
+ * Scripts declare their params in `meta.params`. The framework resolves and
5
+ * validates them BEFORE `execute()` runs, so a script never validates its own
6
+ * input.
7
+ *
8
+ * Resolution order (lowest to highest priority):
9
+ * ctx.settings < meta.params[x].default < explicit (CLI) params
10
+ *
11
+ * A param spec supports:
12
+ * required {boolean} - error if no value resolves
13
+ * default {*} - fallback value
14
+ * description {string} - shown in CLI listings and error messages
15
+ * validate {RegExp|Function}
16
+ * RegExp - the resolved value (coerced to string) must match
17
+ * Function - (value, resolvedParams) => true | false | string
18
+ * true = valid
19
+ * string = invalid; the string is used as the error message
20
+ * false = invalid; `invalidMessage` (or a default) is used
21
+ * invalidMessage {string} - error message for a failing RegExp, or for a
22
+ * validate function that returns `false`
23
+ */
24
+
25
+ export class ParamError extends Error {
26
+ constructor(message) {
27
+ super(message);
28
+ this.name = 'ParamError';
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Resolve and validate params for a script.
34
+ *
35
+ * @param {object} meta - The script's `meta` export (may have `.params`).
36
+ * @param {object} explicit - Params explicitly provided (e.g. from CLI).
37
+ * @param {object} settings - Compiled project settings (ctx.settings).
38
+ * @returns {object} The fully-resolved params object.
39
+ * @throws {ParamError} If a required param is missing or a value fails validation.
40
+ */
41
+ export function resolveParams(meta, explicit = {}, settings = {}) {
42
+ const spec = meta?.params || {};
43
+ const scriptName = meta?.name || 'script';
44
+ const resolved = {};
45
+ const missing = [];
46
+
47
+ for (const [key, def] of Object.entries(spec)) {
48
+ let value;
49
+ if (key in explicit) value = explicit[key];
50
+ else if (def.default !== undefined) value = def.default;
51
+ else if (key in settings) value = settings[key];
52
+
53
+ if (value === undefined) {
54
+ if (def.required) missing.push(key);
55
+ continue;
56
+ }
57
+ resolved[key] = value;
58
+ }
59
+
60
+ // Pass through any explicit params not declared in meta (escape hatch).
61
+ for (const [key, value] of Object.entries(explicit)) {
62
+ if (!(key in resolved)) resolved[key] = value;
63
+ }
64
+
65
+ if (missing.length) {
66
+ throw new ParamError(
67
+ `Missing required param(s) for "${scriptName}":\n${missing
68
+ .map((k) => describeParam(k, spec[k]))
69
+ .join('\n')}`
70
+ );
71
+ }
72
+
73
+ const invalid = validateParams(spec, resolved);
74
+ if (invalid.length) {
75
+ throw new ParamError(
76
+ `Invalid param(s) for "${scriptName}":\n${invalid
77
+ .map(({ key, message }) => ` - ${key}: ${message}`)
78
+ .join('\n')}`
79
+ );
80
+ }
81
+
82
+ return resolved;
83
+ }
84
+
85
+ /**
86
+ * Run each declared param's `validate` rule against its resolved value.
87
+ *
88
+ * @param {object} spec - The `meta.params` spec object.
89
+ * @param {object} resolved - The resolved param values.
90
+ * @returns {Array<{key: string, message: string}>} One entry per failure.
91
+ */
92
+ function validateParams(spec, resolved) {
93
+ const failures = [];
94
+
95
+ for (const [key, def] of Object.entries(spec)) {
96
+ if (!(key in resolved) || !def?.validate) continue;
97
+ const message = runValidator(def, resolved[key], resolved);
98
+ if (message) failures.push({ key, message });
99
+ }
100
+
101
+ return failures;
102
+ }
103
+
104
+ /**
105
+ * Apply a single param's validator to a value.
106
+ *
107
+ * @param {object} def - The param spec (has `validate` and optional `invalidMessage`).
108
+ * @param {*} value - The resolved value to check.
109
+ * @param {object} resolved - All resolved params (passed to function validators).
110
+ * @returns {string|null} An error message if invalid, otherwise null.
111
+ */
112
+ function runValidator(def, value, resolved) {
113
+ const { validate, invalidMessage } = def;
114
+
115
+ if (validate instanceof RegExp) {
116
+ return validate.test(String(value))
117
+ ? null
118
+ : invalidMessage || `value "${value}" does not match ${validate}`;
119
+ }
120
+
121
+ if (typeof validate === 'function') {
122
+ const result = validate(value, resolved);
123
+ if (result === true) return null;
124
+ if (typeof result === 'string') return result;
125
+ return invalidMessage || `value "${value}" is invalid`;
126
+ }
127
+
128
+ return null;
129
+ }
130
+
131
+ /**
132
+ * Format a param for inclusion in a "missing required" error message.
133
+ *
134
+ * @param {string} key - The param name.
135
+ * @param {object} [def] - The param spec (may carry a description).
136
+ * @returns {string} A single indented line.
137
+ */
138
+ function describeParam(key, def) {
139
+ return def?.description ? ` - ${key}: ${def.description}` : ` - ${key}`;
140
+ }
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Runner — executes an automation script by absolute path with given params.
5
+ *
6
+ * Usage:
7
+ * node run.mjs <absolute-path-to-script.mjs> [--param=value ...]
8
+ *
9
+ * Harness options are passed the same way and intercepted before params:
10
+ * --profileName=<name> Chrome profile to pull cookies from
11
+ * --timeout=<ms> Overall script timeout
12
+ * --headless=false (debug) run headed — rarely useful for this system
13
+ *
14
+ * Everything else (--foo=bar) becomes a script param. The harness resolves
15
+ * defaults + settings and validates required params before execute() runs.
16
+ *
17
+ * Project settings compiled by sous (settings.mjs, a sibling of this file) are
18
+ * loaded automatically and passed to the harness as ctx.settings.
19
+ */
20
+
21
+ import { resolve, dirname, join } from 'path';
22
+ import { existsSync, writeFileSync } from 'fs';
23
+ import { fileURLToPath, pathToFileURL } from 'url';
24
+
25
+ const HARNESS_OPTIONS = ['headless', 'profileName', 'timeout'];
26
+
27
+ const args = process.argv.slice(2);
28
+ if (args.length === 0 || args[0] === '--help') {
29
+ console.log('Usage: node run.mjs <absolute-path-to-script.mjs> [--param=value ...]');
30
+ process.exit(args.length === 0 ? 1 : 0);
31
+ }
32
+
33
+ const scriptPath = resolve(args[0]);
34
+ if (!existsSync(scriptPath)) {
35
+ console.error(`Script not found: ${scriptPath}`);
36
+ process.exit(1);
37
+ }
38
+
39
+ const { params, options } = parseArgs(args.slice(1));
40
+ const settings = await loadSettings();
41
+ const script = await import(pathToFileURL(scriptPath).href);
42
+ const meta = script.meta || {};
43
+
44
+ printRunHeader(meta, params, options, settings);
45
+
46
+ // Imported lazily so --help and arg validation don't require playwright et al.
47
+ const { runScript } = await import('./harness.mjs');
48
+ const result = await runScript(script, params, { ...options, settings });
49
+
50
+ reportResult(result);
51
+ process.exit(result.success ? 0 : 1);
52
+
53
+ /**
54
+ * Split `--key=value` args into harness options and script params.
55
+ *
56
+ * @param {string[]} rest - CLI args after the script path.
57
+ * @returns {{params: object, options: object}}
58
+ */
59
+ function parseArgs(rest) {
60
+ const params = {};
61
+ const options = {};
62
+ for (const arg of rest) {
63
+ const match = arg.match(/^--([\w-]+)=(.*)$/s);
64
+ if (!match) continue;
65
+ const [, key, value] = match;
66
+ if (!HARNESS_OPTIONS.includes(key)) {
67
+ params[key] = value;
68
+ } else if (key === 'headless') {
69
+ options.headless = value !== 'false';
70
+ } else if (key === 'timeout') {
71
+ options.timeout = parseInt(value, 10);
72
+ } else {
73
+ options[key] = value;
74
+ }
75
+ }
76
+ return { params, options };
77
+ }
78
+
79
+ /**
80
+ * Load compiled project settings (settings.mjs) sitting beside this runner.
81
+ *
82
+ * @returns {Promise<object>} The settings object, or {} if none is present.
83
+ */
84
+ async function loadSettings() {
85
+ const here = dirname(fileURLToPath(import.meta.url));
86
+ const settingsPath = join(here, 'settings.mjs');
87
+ if (!existsSync(settingsPath)) return {};
88
+ const mod = await import(pathToFileURL(settingsPath).href);
89
+ return mod.default || {};
90
+ }
91
+
92
+ /**
93
+ * Print the resolved params (with defaults/settings marked) before running.
94
+ *
95
+ * @param {object} meta - The script's meta export.
96
+ * @param {object} params - Explicit CLI params.
97
+ * @param {object} options - Harness options.
98
+ * @param {object} settings - Compiled project settings.
99
+ * @returns {void}
100
+ */
101
+ function printRunHeader(meta, params, options, settings) {
102
+ console.log(`\n▶ Running: ${meta.name || '(unnamed script)'}`);
103
+ console.log(' Params:');
104
+ const spec = meta.params || {};
105
+ const keys = new Set([...Object.keys(spec), ...Object.keys(params)]);
106
+ for (const key of keys) {
107
+ let value, origin;
108
+ if (key in params) { value = params[key]; origin = ''; }
109
+ else if (spec[key]?.default !== undefined) { value = spec[key].default; origin = ' (default)'; }
110
+ else if (key in settings) { value = settings[key]; origin = ' (settings)'; }
111
+ else continue;
112
+ console.log(` ${key}: ${value}${origin}`);
113
+ }
114
+ if (Object.keys(options).length) console.log(` Options: ${JSON.stringify(options)}`);
115
+ console.log('');
116
+ }
117
+
118
+ /**
119
+ * Print the run outcome and, on success, write any requested output file.
120
+ *
121
+ * @param {object} result - The harness result envelope.
122
+ * @returns {void}
123
+ */
124
+ function reportResult(result) {
125
+ console.log('\n' + '─'.repeat(60));
126
+ if (result.success) {
127
+ console.log('✓ Script completed successfully\n');
128
+ if (result.data?.outputFile && result.data?.content) {
129
+ writeFileSync(result.data.outputFile, result.data.content);
130
+ console.log(`Output written to: ${result.data.outputFile}`);
131
+ } else {
132
+ console.log(JSON.stringify(result.data, null, 2));
133
+ }
134
+ } else {
135
+ console.log(`✗ Script failed (${result.error})\n`);
136
+ console.log(result.message);
137
+ if (result.stack && result.error === 'script') console.log(`\nStack:\n${result.stack}`);
138
+ if (result.debugDir) console.log(`\nDebug snapshot (screenshot, HTML, text): ${result.debugDir}`);
139
+ }
140
+ }