flowspec 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Josh Owens
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,187 @@
1
+ # FlowSpec
2
+
3
+ Immutable user flow specifications for the age of agentic coding.
4
+
5
+ ## The Problem
6
+
7
+ AI coding agents can modify both implementation and tests. When a test fails, the agent might "fix" the test instead of fixing the bug. This breaks the feedback loop that catches regressions.
8
+
9
+ ## The Solution
10
+
11
+ FlowSpec separates **what your app should do** (immutable specs) from **how it does it** (agent-modifiable code).
12
+
13
+ - Write user flows in simple YAML
14
+ - Use human-readable labels (accessibility-first, a la React Testing Library)
15
+ - Protect specs from agent modification via Claude Code hooks
16
+ - Run deterministically in CI or interactively with an agent
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ # Install globally
22
+ npm install -g flowspec
23
+
24
+ # Or with bun
25
+ bun add -g flowspec
26
+ ```
27
+
28
+ Requires [agent-browser](https://github.com/anthropics/agent-browser) for browser automation.
29
+
30
+ ## Usage
31
+
32
+ ### Initialize a New Project
33
+
34
+ ```bash
35
+ # Scaffold a new FlowSpec project
36
+ flowspec init
37
+ ```
38
+
39
+ This creates:
40
+ - `flowspec.config.yaml` - Project configuration
41
+ - `specs/example.flow.yaml` - Sample flow to get started
42
+ - `.claude/settings.local.json` - Hooks to protect specs from AI modification
43
+ - Updates `package.json` with `test:e2e` script
44
+
45
+ ### Run Flows
46
+
47
+ ```bash
48
+ # Run a single flow file
49
+ flowspec run specs/checkout.flow.yaml
50
+
51
+ # Run all flows in a directory
52
+ flowspec run specs/
53
+
54
+ # Specify a custom base URL
55
+ flowspec run specs/ --base-url http://localhost:8080
56
+
57
+ # Set assertion retry timeout (default: 5000ms)
58
+ flowspec run specs/ --timeout 10000
59
+
60
+ # Disable assertion retries (fail immediately)
61
+ flowspec run specs/ --timeout 0
62
+
63
+ # Show help
64
+ flowspec --help
65
+ ```
66
+
67
+ ### Configuration File
68
+
69
+ FlowSpec looks for `flowspec.config.yaml` in the current directory or parent directories:
70
+
71
+ ```yaml
72
+ baseUrl: http://localhost:3000
73
+ timeout: 10000
74
+ specsDir: specs/
75
+ ```
76
+
77
+ CLI options override config file values.
78
+
79
+ ### Exit Codes
80
+
81
+ | Code | Meaning |
82
+ | ---- | ------- |
83
+ | 0 | All flows passed |
84
+ | 1 | One or more flows failed |
85
+ | 2 | Parse error (invalid YAML or schema) |
86
+
87
+ ## Flow File Format
88
+
89
+ Flow files use YAML with a simple structure:
90
+
91
+ ```yaml
92
+ name: user-login
93
+ description: User can log in with valid credentials
94
+ steps:
95
+ - visit: /login
96
+ - fill:
97
+ Email: user@example.com
98
+ Password: secretpassword
99
+ - click: Sign In
100
+ expect:
101
+ - url: /dashboard
102
+ - visible: Welcome back
103
+ ```
104
+
105
+ ### Step Actions
106
+
107
+ | Action | Description | Example |
108
+ | ------ | ----------- | ------- |
109
+ | `visit` | Navigate to a URL (relative or absolute) | `visit: /login` |
110
+ | `click` | Click element by visible text | `click: "Sign In"` |
111
+ | `fill` | Fill form fields by label | `fill: { Email: user@example.com }` |
112
+ | `select` | Select dropdown option by label | `select: { Country: "United States" }` |
113
+ | `wait_for` | Wait for text to appear (with retry) | `wait_for: "Loading complete"` |
114
+
115
+ ### Assertions
116
+
117
+ | Assertion | Description | Example |
118
+ | --------- | ----------- | ------- |
119
+ | `url` | Check current URL contains value | `url: /dashboard` |
120
+ | `visible` | Check text is visible on page | `visible: "Welcome back"` |
121
+ | `matches` | Check page content matches regex | `matches: "Order #\\d+"` |
122
+ | `not_visible` | Check text is NOT on page | `not_visible: "Error"` |
123
+
124
+ ## Quick Example
125
+
126
+ ```yaml
127
+ # specs/checkout.flow.yaml
128
+ name: checkout-flow
129
+ description: |
130
+ User completes a purchase with items in cart.
131
+ Captures shipping/billing info and confirms order.
132
+
133
+ steps:
134
+ - visit: "/cart"
135
+ - click: "Proceed to Checkout"
136
+ - fill:
137
+ "Email": "user@example.com"
138
+ "Shipping Address": "123 Main St"
139
+ - click: "Place Order"
140
+
141
+ expect:
142
+ - url: "/order/confirmation"
143
+ - visible: "Order confirmed"
144
+ - matches: "Order #\\d+"
145
+ ```
146
+
147
+ ## Development
148
+
149
+ ```bash
150
+ bun install # Install dependencies
151
+ bun run build # Build CLI (required for bun link)
152
+ bun link # Link CLI locally as 'flowspec'
153
+ bun test # Run tests
154
+ bun run typecheck # Type check
155
+ bun run lint # Lint with Biome
156
+ bun run lint:fix # Auto-fix lint issues
157
+ bun run format # Format with Biome
158
+ bun run test:coverage # Run with coverage
159
+ ```
160
+
161
+ ### Project Structure
162
+
163
+ ```text
164
+ FlowSpec/
165
+ ├── src/
166
+ │ ├── types.ts # Zod schemas for FlowSpec
167
+ │ ├── parser.ts # YAML parsing with Zod validation
168
+ │ ├── runner.ts # Flow execution via agent-browser
169
+ │ ├── reporter.ts # Result formatting for terminal
170
+ │ └── index.ts # CLI entry point
171
+ ├── test/
172
+ │ ├── fixtures/
173
+ │ │ ├── pages/ # HTML test fixtures
174
+ │ │ └── flows/ # YAML flow fixtures
175
+ │ └── *.test.ts # Test files
176
+ ├── docs/
177
+ │ └── specification.md # Full framework design
178
+ └── specs/ # User flow specs (immutable)
179
+ ```
180
+
181
+ ## Documentation
182
+
183
+ - [Full Specification](docs/specification.md) - Complete framework design and rationale
184
+
185
+ ## License
186
+
187
+ MIT
@@ -0,0 +1,54 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Schema for FlowSpec project configuration
4
+ */
5
+ export declare const FlowSpecConfigSchema: z.ZodObject<{
6
+ baseUrl: z.ZodDefault<z.ZodOptional<z.ZodString>>;
7
+ timeout: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
8
+ specsDir: z.ZodDefault<z.ZodOptional<z.ZodString>>;
9
+ }, "strip", z.ZodTypeAny, {
10
+ baseUrl: string;
11
+ timeout: number;
12
+ specsDir: string;
13
+ }, {
14
+ baseUrl?: string | undefined;
15
+ timeout?: number | undefined;
16
+ specsDir?: string | undefined;
17
+ }>;
18
+ export type FlowSpecConfig = z.infer<typeof FlowSpecConfigSchema>;
19
+ /**
20
+ * Default configuration values
21
+ */
22
+ export declare const DEFAULT_CONFIG: FlowSpecConfig;
23
+ /**
24
+ * The standard configuration file name
25
+ */
26
+ export declare const CONFIG_FILE_NAME = "flowspec.config.yaml";
27
+ /**
28
+ * Find the configuration file by walking up the directory tree
29
+ * @param startDir - Directory to start searching from
30
+ * @returns Path to config file if found, undefined otherwise
31
+ */
32
+ export declare function findConfigFile(startDir?: string): string | undefined;
33
+ /**
34
+ * Load and parse a FlowSpec configuration file
35
+ * @param configPath - Path to the config file
36
+ * @returns Parsed configuration
37
+ * @throws Error if file not found or invalid
38
+ */
39
+ export declare function loadConfigFile(configPath: string): FlowSpecConfig;
40
+ /**
41
+ * Load configuration from the default location or return defaults
42
+ * @param startDir - Directory to start searching from
43
+ * @returns Configuration (from file if found, defaults otherwise)
44
+ */
45
+ export declare function loadConfig(startDir?: string): FlowSpecConfig;
46
+ /**
47
+ * Merge CLI options with configuration file values
48
+ * CLI options take precedence over config file values
49
+ */
50
+ export declare function mergeConfig(config: FlowSpecConfig, cliOptions: {
51
+ baseUrl?: string;
52
+ timeout?: number;
53
+ }): FlowSpecConfig;
54
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;GAEG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;EAI/B,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,cAI5B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,gBAAgB,yBAAyB,CAAC;AAEvD;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,GAAE,MAAsB,GAC/B,MAAM,GAAG,SAAS,CAmBpB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,cAAc,CAkCjE;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,QAAQ,GAAE,MAAsB,GAAG,cAAc,CAQ3E;AAED;;;GAGG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,cAAc,EACtB,UAAU,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GACjD,cAAc,CAMhB"}
package/dist/config.js ADDED
@@ -0,0 +1,102 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import yaml from "js-yaml";
4
+ import { z } from "zod";
5
+ /**
6
+ * Schema for FlowSpec project configuration
7
+ */
8
+ export const FlowSpecConfigSchema = z.object({
9
+ baseUrl: z.string().url().optional().default("http://localhost:3000"),
10
+ timeout: z.number().positive().optional().default(10000),
11
+ specsDir: z.string().optional().default("specs/"),
12
+ });
13
+ /**
14
+ * Default configuration values
15
+ */
16
+ export const DEFAULT_CONFIG = {
17
+ baseUrl: "http://localhost:3000",
18
+ timeout: 10000,
19
+ specsDir: "specs/",
20
+ };
21
+ /**
22
+ * The standard configuration file name
23
+ */
24
+ export const CONFIG_FILE_NAME = "flowspec.config.yaml";
25
+ /**
26
+ * Find the configuration file by walking up the directory tree
27
+ * @param startDir - Directory to start searching from
28
+ * @returns Path to config file if found, undefined otherwise
29
+ */
30
+ export function findConfigFile(startDir = process.cwd()) {
31
+ let currentDir = resolve(startDir);
32
+ const root = resolve("/");
33
+ while (currentDir !== root) {
34
+ const configPath = join(currentDir, CONFIG_FILE_NAME);
35
+ if (existsSync(configPath)) {
36
+ return configPath;
37
+ }
38
+ currentDir = resolve(currentDir, "..");
39
+ }
40
+ // Check root as well
41
+ const rootConfig = join(root, CONFIG_FILE_NAME);
42
+ if (existsSync(rootConfig)) {
43
+ return rootConfig;
44
+ }
45
+ return undefined;
46
+ }
47
+ /**
48
+ * Load and parse a FlowSpec configuration file
49
+ * @param configPath - Path to the config file
50
+ * @returns Parsed configuration
51
+ * @throws Error if file not found or invalid
52
+ */
53
+ export function loadConfigFile(configPath) {
54
+ if (!existsSync(configPath)) {
55
+ throw new Error(`Configuration file not found: ${configPath}`);
56
+ }
57
+ const content = readFileSync(configPath, "utf-8");
58
+ let parsed;
59
+ try {
60
+ parsed = yaml.load(content);
61
+ }
62
+ catch (error) {
63
+ if (error instanceof yaml.YAMLException) {
64
+ throw new Error(`Invalid YAML in config file: ${error.reason} at line ${error.mark?.line ?? "unknown"}`);
65
+ }
66
+ throw error;
67
+ }
68
+ // Handle empty file case
69
+ if (parsed === undefined || parsed === null) {
70
+ return DEFAULT_CONFIG;
71
+ }
72
+ const result = FlowSpecConfigSchema.safeParse(parsed);
73
+ if (!result.success) {
74
+ const issues = result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`);
75
+ throw new Error(`Invalid configuration: ${issues.join("; ")}`);
76
+ }
77
+ return result.data;
78
+ }
79
+ /**
80
+ * Load configuration from the default location or return defaults
81
+ * @param startDir - Directory to start searching from
82
+ * @returns Configuration (from file if found, defaults otherwise)
83
+ */
84
+ export function loadConfig(startDir = process.cwd()) {
85
+ const configPath = findConfigFile(startDir);
86
+ if (configPath) {
87
+ return loadConfigFile(configPath);
88
+ }
89
+ return DEFAULT_CONFIG;
90
+ }
91
+ /**
92
+ * Merge CLI options with configuration file values
93
+ * CLI options take precedence over config file values
94
+ */
95
+ export function mergeConfig(config, cliOptions) {
96
+ return {
97
+ baseUrl: cliOptions.baseUrl ?? config.baseUrl,
98
+ timeout: cliOptions.timeout ?? config.timeout,
99
+ specsDir: config.specsDir,
100
+ };
101
+ }
102
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bun
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env bun
2
+ import { existsSync, readdirSync, statSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { loadConfig, mergeConfig } from "./config";
5
+ import { formatInitResult, initProject } from "./init";
6
+ import { parseFlowFile } from "./parser";
7
+ import { formatResult, formatSummary } from "./reporter";
8
+ import { DEFAULT_TIMEOUT, runFlow } from "./runner";
9
+ function showHelp() {
10
+ console.log(`
11
+ Usage: flowspec <command> [options]
12
+
13
+ Commands:
14
+ init Initialize FlowSpec in the current directory
15
+ run <path> Run FlowSpec flow files
16
+
17
+ Run Command Options:
18
+ --base-url <url> Base URL for relative paths (default from config or http://localhost:3000)
19
+ --timeout <ms> Assertion retry timeout in milliseconds (default: ${DEFAULT_TIMEOUT})
20
+ --help Show help
21
+
22
+ Init Command:
23
+ Creates FlowSpec configuration in the current directory:
24
+ - flowspec.config.yaml (project settings)
25
+ - specs/example.flow.yaml (sample flow)
26
+ - .claude/settings.local.json (protects specs from AI edits)
27
+
28
+ Exit codes:
29
+ 0 All flows passed
30
+ 1 One or more flows failed
31
+ 2 Parse error (invalid YAML/schema)
32
+ `);
33
+ }
34
+ function parseArgs(args) {
35
+ const options = {
36
+ showHelp: false,
37
+ };
38
+ for (let i = 0; i < args.length; i++) {
39
+ const arg = args[i];
40
+ if (arg === "--help" || arg === "-h") {
41
+ options.showHelp = true;
42
+ }
43
+ else if (arg === "--base-url" && i + 1 < args.length) {
44
+ options.baseUrl = args[++i];
45
+ }
46
+ else if (arg === "--timeout" && i + 1 < args.length) {
47
+ const timeoutValue = Number.parseInt(args[++i], 10);
48
+ if (!Number.isNaN(timeoutValue)) {
49
+ options.timeout = timeoutValue;
50
+ }
51
+ }
52
+ else if (!arg.startsWith("-") && !options.path) {
53
+ options.path = arg;
54
+ }
55
+ }
56
+ return options;
57
+ }
58
+ function discoverFlowFiles(path) {
59
+ const absolutePath = resolve(path);
60
+ if (!existsSync(absolutePath)) {
61
+ return [];
62
+ }
63
+ const stats = statSync(absolutePath);
64
+ if (stats.isFile()) {
65
+ return [absolutePath];
66
+ }
67
+ if (stats.isDirectory()) {
68
+ const files = readdirSync(absolutePath);
69
+ return files
70
+ .filter((file) => file.endsWith(".flow.yaml"))
71
+ .map((file) => join(absolutePath, file))
72
+ .sort();
73
+ }
74
+ return [];
75
+ }
76
+ function parseFlowFiles(filePaths) {
77
+ const flows = [];
78
+ const errors = [];
79
+ for (const filePath of filePaths) {
80
+ try {
81
+ const flow = parseFlowFile(filePath);
82
+ flows.push({ filePath, flow });
83
+ }
84
+ catch (error) {
85
+ const message = error instanceof Error ? error.message : String(error);
86
+ errors.push({ filePath, error: message });
87
+ }
88
+ }
89
+ return { flows, errors };
90
+ }
91
+ async function runFlows(parsedFlows, baseUrl, timeout) {
92
+ const results = [];
93
+ for (const { flow } of parsedFlows) {
94
+ const result = await runFlow(flow, { baseUrl, timeout });
95
+ console.log(formatResult(result));
96
+ results.push(result);
97
+ }
98
+ return results;
99
+ }
100
+ function handleInitCommand() {
101
+ const result = initProject(process.cwd());
102
+ console.log(formatInitResult(result));
103
+ process.exit(result.success ? 0 : 1);
104
+ }
105
+ async function handleRunCommand(args) {
106
+ const options = parseArgs(args);
107
+ if (options.showHelp) {
108
+ showHelp();
109
+ process.exit(0);
110
+ }
111
+ if (!options.path) {
112
+ console.error("Error: No path specified");
113
+ showHelp();
114
+ process.exit(1);
115
+ }
116
+ const absolutePath = resolve(options.path);
117
+ if (!existsSync(absolutePath)) {
118
+ console.error(`Error: Path not found: ${options.path}`);
119
+ process.exit(1);
120
+ }
121
+ // Load configuration and merge with CLI options
122
+ const config = loadConfig();
123
+ const mergedConfig = mergeConfig(config, {
124
+ baseUrl: options.baseUrl,
125
+ timeout: options.timeout,
126
+ });
127
+ const flowFiles = discoverFlowFiles(options.path);
128
+ if (flowFiles.length === 0) {
129
+ console.log("No flow files found");
130
+ process.exit(0);
131
+ }
132
+ // Parse all flow files first
133
+ const { flows, errors } = parseFlowFiles(flowFiles);
134
+ // If there are parse errors, report them and exit with code 2
135
+ if (errors.length > 0) {
136
+ for (const { filePath, error } of errors) {
137
+ console.error(`Error parsing ${filePath}:`);
138
+ console.error(` ${error}`);
139
+ }
140
+ process.exit(2);
141
+ }
142
+ // Run all flows
143
+ const results = await runFlows(flows, mergedConfig.baseUrl, mergedConfig.timeout);
144
+ // Print summary
145
+ console.log();
146
+ console.log(formatSummary(results));
147
+ // Exit with appropriate code
148
+ const allPassed = results.every((r) => r.success);
149
+ process.exit(allPassed ? 0 : 1);
150
+ }
151
+ async function main() {
152
+ // Skip first two args: "bun" and script path
153
+ const args = process.argv.slice(2);
154
+ // Handle case when no arguments
155
+ if (args.length === 0) {
156
+ showHelp();
157
+ process.exit(0);
158
+ }
159
+ // Extract the command
160
+ const command = args[0];
161
+ if (command === "init") {
162
+ handleInitCommand();
163
+ }
164
+ else if (command === "run") {
165
+ await handleRunCommand(args.slice(1));
166
+ }
167
+ else if (command === "--help" || command === "-h") {
168
+ showHelp();
169
+ process.exit(0);
170
+ }
171
+ else {
172
+ console.error(`Unknown command: ${command}`);
173
+ showHelp();
174
+ process.exit(1);
175
+ }
176
+ }
177
+ main().catch((error) => {
178
+ console.error("Unexpected error:", error.message);
179
+ process.exit(1);
180
+ });
181
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/init.d.ts ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Result of creating a single file during init
3
+ */
4
+ export interface InitFileResult {
5
+ path: string;
6
+ created: boolean;
7
+ skipped: boolean;
8
+ error?: string;
9
+ }
10
+ /**
11
+ * Result of the init command
12
+ */
13
+ export interface InitResult {
14
+ files: InitFileResult[];
15
+ success: boolean;
16
+ }
17
+ /**
18
+ * Default content for flowspec.config.yaml
19
+ */
20
+ export declare const DEFAULT_CONFIG_CONTENT = "baseUrl: http://localhost:3000\ntimeout: 10000\nspecsDir: specs/\n";
21
+ /**
22
+ * Default content for example flow file
23
+ */
24
+ export declare const DEFAULT_EXAMPLE_FLOW_CONTENT = "name: example-flow\ndescription: Example flow - customize this for your app\nsteps:\n - visit: /\nexpect:\n - visible: Welcome\n";
25
+ /**
26
+ * Default content for Claude settings to protect specs
27
+ */
28
+ export declare const DEFAULT_CLAUDE_SETTINGS_CONTENT = "{\n \"hooks\": {\n \"PreToolUse\": [{\n \"matcher\": { \"tool\": [\"Edit\", \"Write\"], \"path\": \"specs/**/*.flow.yaml\" },\n \"command\": \"echo '\u274C Flow specs are immutable. Fix the implementation, not the spec.' && exit 1\"\n }]\n }\n}\n";
29
+ /**
30
+ * Initialize FlowSpec in a project directory
31
+ * Creates configuration files and example flow
32
+ *
33
+ * @param projectDir - Directory to initialize (defaults to cwd)
34
+ * @returns Result with details about each file created
35
+ */
36
+ export declare function initProject(projectDir?: string): InitResult;
37
+ /**
38
+ * Format the init result for console output
39
+ */
40
+ export declare function formatInitResult(result: InitResult): string;
41
+ //# sourceMappingURL=init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;GAEG;AACH,eAAO,MAAM,sBAAsB,uEAGlC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,4BAA4B,uIAMxC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,+BAA+B,6QAQ3C,CAAC;AAiGF;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,UAAU,GAAE,MAAsB,GAAG,UAAU,CAgC1E;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAuC3D"}