canary-lab 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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +282 -0
  3. package/dist/feature-support/load-env.d.ts +1 -0
  4. package/dist/feature-support/load-env.js +5 -0
  5. package/dist/feature-support/log-marker-fixture.d.ts +1 -0
  6. package/dist/feature-support/log-marker-fixture.js +6 -0
  7. package/dist/feature-support/playwright-base.d.ts +1 -0
  8. package/dist/feature-support/playwright-base.js +5 -0
  9. package/dist/feature-support/types.d.ts +1 -0
  10. package/dist/feature-support/types.js +2 -0
  11. package/dist/scripts/cli.d.ts +2 -0
  12. package/dist/scripts/cli.js +47 -0
  13. package/dist/scripts/init-project.d.ts +1 -0
  14. package/dist/scripts/init-project.js +110 -0
  15. package/dist/scripts/new-feature.d.ts +1 -0
  16. package/dist/scripts/new-feature.js +135 -0
  17. package/dist/shared/configs/loadEnv.d.ts +5 -0
  18. package/dist/shared/configs/loadEnv.js +19 -0
  19. package/dist/shared/configs/playwright.base.d.ts +12 -0
  20. package/dist/shared/configs/playwright.base.js +15 -0
  21. package/dist/shared/e2e-runner/log-marker-fixture.d.ts +13 -0
  22. package/dist/shared/e2e-runner/log-marker-fixture.js +45 -0
  23. package/dist/shared/e2e-runner/paths.d.ts +8 -0
  24. package/dist/shared/e2e-runner/paths.js +16 -0
  25. package/dist/shared/e2e-runner/runner.d.ts +1 -0
  26. package/dist/shared/e2e-runner/runner.js +567 -0
  27. package/dist/shared/e2e-runner/summary-reporter.d.ts +7 -0
  28. package/dist/shared/e2e-runner/summary-reporter.js +33 -0
  29. package/dist/shared/env-switcher/root-cli.d.ts +1 -0
  30. package/dist/shared/env-switcher/root-cli.js +84 -0
  31. package/dist/shared/env-switcher/switch.d.ts +1 -0
  32. package/dist/shared/env-switcher/switch.js +249 -0
  33. package/dist/shared/env-switcher/types.d.ts +18 -0
  34. package/dist/shared/env-switcher/types.js +2 -0
  35. package/dist/shared/launcher/iterm.d.ts +2 -0
  36. package/dist/shared/launcher/iterm.js +28 -0
  37. package/dist/shared/launcher/startup.d.ts +9 -0
  38. package/dist/shared/launcher/startup.js +40 -0
  39. package/dist/shared/launcher/terminal.d.ts +2 -0
  40. package/dist/shared/launcher/terminal.js +25 -0
  41. package/dist/shared/launcher/types.d.ts +28 -0
  42. package/dist/shared/launcher/types.js +2 -0
  43. package/dist/shared/runtime/project-root.d.ts +2 -0
  44. package/dist/shared/runtime/project-root.js +32 -0
  45. package/dist/templates/project/.claude/skills/self-fixing-loop.md +53 -0
  46. package/dist/templates/project/.codex/self-fixing-loop.md +49 -0
  47. package/dist/templates/project/AGENTS.md +27 -0
  48. package/dist/templates/project/CLAUDE.md +31 -0
  49. package/dist/templates/project/features/broken_todo_api/.env.example +1 -0
  50. package/dist/templates/project/features/broken_todo_api/e2e/broken-todo-api.spec.js +34 -0
  51. package/dist/templates/project/features/broken_todo_api/e2e/helpers/api.js +48 -0
  52. package/dist/templates/project/features/broken_todo_api/envsets/envsets.config.json +14 -0
  53. package/dist/templates/project/features/broken_todo_api/envsets/local/broken_todo_api.env +1 -0
  54. package/dist/templates/project/features/broken_todo_api/feature.config.cjs +24 -0
  55. package/dist/templates/project/features/broken_todo_api/playwright.config.js +6 -0
  56. package/dist/templates/project/features/broken_todo_api/scripts/server.js +76 -0
  57. package/dist/templates/project/features/broken_todo_api/src/config.js +9 -0
  58. package/dist/templates/project/features/example_todo_api/.env.example +1 -0
  59. package/dist/templates/project/features/example_todo_api/e2e/helpers/api.js +36 -0
  60. package/dist/templates/project/features/example_todo_api/e2e/todo-api.spec.js +25 -0
  61. package/dist/templates/project/features/example_todo_api/envsets/envsets.config.json +14 -0
  62. package/dist/templates/project/features/example_todo_api/envsets/local/example_todo_api.env +1 -0
  63. package/dist/templates/project/features/example_todo_api/feature.config.cjs +24 -0
  64. package/dist/templates/project/features/example_todo_api/playwright.config.js +6 -0
  65. package/dist/templates/project/features/example_todo_api/scripts/server.js +60 -0
  66. package/dist/templates/project/features/example_todo_api/src/config.js +9 -0
  67. package/package.json +71 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Canary Lab Contributors
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,282 @@
1
+ # Canary Lab
2
+
3
+ Canary Lab is an npm package for end-to-end testing with Playwright, local service orchestration, and agent-assisted debugging. It scaffolds a runnable E2E project, starts dependent services, captures service logs, and provides a structured self-fixing workflow for Claude or Codex.
4
+
5
+ If you are looking for a small E2E testing starter for local development, a Playwright-based test harness, or a self-fixing demo for AI coding agents, this package aims to be a useful starting point.
6
+
7
+ ## What It Tries To Help With
8
+
9
+ - Scaffold an E2E test project quickly
10
+ - Run Playwright tests against local services from one command
11
+ - Capture per-service logs and a machine-readable failure summary
12
+ - Give Claude or Codex a documented workflow for diagnosing and fixing failures
13
+ - Keep generated projects visible while shipping package internals as compiled code
14
+
15
+ ## For Users
16
+
17
+ Use Canary Lab when you want to bootstrap a local E2E testing project and experiment with an agent-assisted self-fixing loop.
18
+
19
+ ### Quick Start
20
+
21
+ ```bash
22
+ npx canary-lab init my-lab
23
+ cd my-lab
24
+ npm install
25
+ npm run install:browsers
26
+ npx canary-lab run
27
+ ```
28
+
29
+ ### What Gets Scaffolded
30
+
31
+ The generated project includes:
32
+
33
+ - `features/example_todo_api` as a working Playwright E2E sample
34
+ - `features/broken_todo_api` as an intentionally broken sample for self-fixing
35
+ - `CLAUDE.md` and `.claude/skills/self-fixing-loop.md` for Claude
36
+ - `AGENTS.md` and `.codex/self-fixing-loop.md` for Codex
37
+
38
+ ### Main Commands
39
+
40
+ ```bash
41
+ npx canary-lab init <folder>
42
+ npx canary-lab run
43
+ npx canary-lab env
44
+ npx canary-lab new-feature <name> "Description"
45
+ ```
46
+
47
+ ### Environment Switching
48
+
49
+ `npx canary-lab env` helps manage temporary environment files for a feature.
50
+
51
+ This is useful when you want to:
52
+
53
+ - apply a known env setup before running E2E tests
54
+ - switch between different local test configurations
55
+ - avoid manually editing `.env` files each time
56
+ - restore the previous env files after the run
57
+
58
+ When you run `npx canary-lab env`, Canary Lab:
59
+
60
+ - finds features that define env sets
61
+ - lets you choose whether to apply or revert
62
+ - backs up the current target env files
63
+ - applies the selected env set
64
+ - restores the previous files when you revert
65
+
66
+ An env set is a named group of environment files for a feature, usually stored under `features/<feature>/envsets/`.
67
+
68
+ Typical usage:
69
+
70
+ ```bash
71
+ npx canary-lab env
72
+ ```
73
+
74
+ Then choose the feature and env set you want, such as `local`.
75
+
76
+ ### How `envsets.config.json` Works
77
+
78
+ Each feature can define reusable environment setups under:
79
+
80
+ ```bash
81
+ features/<feature>/envsets/
82
+ ```
83
+
84
+ The main config file is `envsets.config.json`. It tells Canary Lab:
85
+
86
+ - where your local apps live
87
+ - which files it is allowed to swap
88
+ - which of those files belong to the feature
89
+
90
+ Example:
91
+
92
+ ```json
93
+ {
94
+ "appRoots": {
95
+ "CANARY_LAB": "/Users/me/Documents/canary-lab",
96
+ "APP_A": "/Users/me/Documents/app-a",
97
+ "APP_B": "/Users/me/Documents/app-b",
98
+ "APP_C": "/Users/me/Documents/app-c"
99
+ },
100
+ "slots": {
101
+ "feature.env": {
102
+ "description": "Feature .env file",
103
+ "target": "$CANARY_LAB/features/sample_feature/.env"
104
+ },
105
+ "app-a.env.local": {
106
+ "description": "App A local env file",
107
+ "target": "$APP_A/.env.local"
108
+ },
109
+ "app-b.env.local": {
110
+ "description": "App B local env file",
111
+ "target": "$APP_B/.env.local"
112
+ },
113
+ "app-c.config.json": {
114
+ "description": "App C local config file",
115
+ "target": "$APP_C/config/local.json"
116
+ }
117
+ },
118
+ "feature": {
119
+ "slots": [
120
+ "feature.env",
121
+ "app-a.env.local",
122
+ "app-b.env.local",
123
+ "app-c.config.json"
124
+ ],
125
+ "testCommand": "npm run test:e2e",
126
+ "testCwd": "$CANARY_LAB/features/sample_feature"
127
+ }
128
+ }
129
+ ```
130
+
131
+ A simple way to read this:
132
+
133
+ - `appRoots` defines the base folders for your local repos or apps
134
+ - `slots` defines the real files Canary Lab is allowed to replace temporarily
135
+ - `feature.slots` lists which of those files this feature uses
136
+ - folders like `envsets/local/` or `envsets/debug/` contain the actual replacement files
137
+
138
+ When you apply an env set, Canary Lab:
139
+
140
+ 1. finds the target files listed in `feature.slots`
141
+ 2. backs up the current files
142
+ 3. copies the selected env-set files into place
143
+ 4. lets you revert them later
144
+
145
+ This is useful when one test flow depends on env files across multiple local apps and you want to switch them together instead of editing them by hand.
146
+
147
+ ### Self-Fixing Workflow
148
+
149
+ 1. Run `npx canary-lab run`
150
+ 2. Choose the broken sample
151
+ 3. Leave the runner open in watch mode
152
+ 4. Open Claude or Codex in the generated project
153
+ 5. Type:
154
+
155
+ ```text
156
+ self heal
157
+ ```
158
+
159
+ `self heal` is a documented agent phrase, not a CLI command. It tells the agent to follow its canonical self-fixing workflow, inspect `logs/e2e-summary.json` and `logs/svc-*.log`, fix implementation only, and trigger `touch logs/.restart` or `touch logs/.rerun` as needed.
160
+
161
+ ### What Makes It Different
162
+
163
+ Compared with a minimal E2E starter, Canary Lab also includes:
164
+
165
+ - a runner that launches services and waits on health checks
166
+ - structured failure context for agents
167
+ - generated project guidance for both Claude and Codex
168
+ - a built-in broken sample to exercise the workflow end to end
169
+
170
+ ### Observability Matters
171
+
172
+ Canary Lab works best when the application under test emits useful logs.
173
+
174
+ The self-fixing workflow depends on being able to correlate a failing test with the service output that happened during that test window. If a service produces little or no log output, the agent has much less context to work with, and the loop becomes correspondingly less useful.
175
+
176
+ In practice, this means basic observability still matters:
177
+
178
+ - services should write meaningful stdout or stderr logs
179
+ - error paths should emit enough context to explain what failed
180
+ - health checks and startup logs should make service state visible
181
+
182
+ Canary Lab can organize and narrow the logs for a given test case, but it cannot recover context that the application never emitted.
183
+
184
+ ## For Contributors
185
+
186
+ Use this repo when you are improving the package itself: the CLI, scaffold templates, runtime, smoke tests, or publish flow.
187
+
188
+ ### Local Development
189
+
190
+ ```bash
191
+ npm install
192
+ npm run build
193
+ ```
194
+
195
+ ### How It Works
196
+
197
+ ```mermaid
198
+ flowchart TD
199
+ A["Canary Lab spawns app processes in terminal tabs"] --> B["Apps write stdout to logs/svc-*.log"]
200
+ B --> C["Canary Lab waits for health checks to pass"]
201
+ C --> D["Canary Lab runs Playwright tests"]
202
+ D --> E["Before each test, the runner appends &lt;test-tag&gt; to each service log"]
203
+ E --> F["After each test, the runner appends &lt;/test-tag&gt;"]
204
+ F --> G["The run writes logs/e2e-summary.json"]
205
+ G --> H["Agent is told: self heal"]
206
+ H --> I["Agent slices the matching log chunk, reads the failure, and fixes code"]
207
+ I --> J["Agent signals .restart or .rerun"]
208
+ J --> D
209
+ ```
210
+
211
+ The basic flow is:
212
+
213
+ - Canary Lab starts the required apps in terminal tabs
214
+ - those apps write stdout to `logs/svc-*.log`
215
+ - once health checks pass, Canary Lab runs Playwright
216
+ - before a test runs, the runner appends a `<test-tag>` marker to each service log
217
+ - after the test finishes, the runner appends the matching `</test-tag>`
218
+ - the run writes `logs/e2e-summary.json`
219
+
220
+ After that, an agent can be told:
221
+
222
+ ```text
223
+ self heal
224
+ ```
225
+
226
+ At that point the agent can:
227
+
228
+ - `logs/e2e-summary.json`
229
+ - `logs/svc-*.log`
230
+ - the canonical self-fixing workflow doc
231
+
232
+ and use the tags to read the matching chunk of each service log for the failed test, fix the implementation, and signal `touch logs/.restart` or `touch logs/.rerun`.
233
+
234
+ ### Repository Areas
235
+
236
+ - `scripts/` contains the package CLI and scaffold commands
237
+ - `shared/` contains the runtime behind `run` and `env`
238
+ - `templates/project/` contains the generated project files
239
+ - `feature-support/` contains the public imports used by generated projects
240
+
241
+ ### Contributor Workflow
242
+
243
+ Typical loop:
244
+
245
+ ```bash
246
+ npm run build
247
+ npm run smoke:pack
248
+ ```
249
+
250
+ If you change scaffold docs, generated files, or packaging behavior, validate by scaffolding a fresh temp project from the built package.
251
+
252
+ ### Packaging and Release Checks
253
+
254
+ ```bash
255
+ npm run pack:check
256
+ npm run smoke:pack
257
+ npm run publish:package
258
+ ```
259
+
260
+ - `pack:check` inspects the npm tarball contents
261
+ - `smoke:pack` builds, packs, scaffolds a temp project, installs dependencies, and verifies the scaffold flow
262
+ - `publish:package` runs build and tarball checks before `npm publish`
263
+
264
+ ### Local Tarball Test
265
+
266
+ Before publishing, prefer testing the packed tarball instead of `npm link`:
267
+
268
+ ```bash
269
+ npm run build
270
+ npm pack
271
+ mkdir /tmp/canary-lab-smoke
272
+ cd /tmp/canary-lab-smoke
273
+ npm init -y
274
+ npm install /absolute/path/to/canary-lab-0.1.0.tgz
275
+ npx canary-lab init test-folder
276
+ ```
277
+
278
+ This tests the exact tarball npm would publish.
279
+
280
+ ## License
281
+
282
+ [MIT](LICENSE)
@@ -0,0 +1 @@
1
+ export { loadFeatureEnv } from '../shared/configs/loadEnv';
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loadFeatureEnv = void 0;
4
+ var loadEnv_1 = require("../shared/configs/loadEnv");
5
+ Object.defineProperty(exports, "loadFeatureEnv", { enumerable: true, get: function () { return loadEnv_1.loadFeatureEnv; } });
@@ -0,0 +1 @@
1
+ export { test, expect, type APIRequestContext, type Page, } from '../shared/e2e-runner/log-marker-fixture';
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.expect = exports.test = void 0;
4
+ var log_marker_fixture_1 = require("../shared/e2e-runner/log-marker-fixture");
5
+ Object.defineProperty(exports, "test", { enumerable: true, get: function () { return log_marker_fixture_1.test; } });
6
+ Object.defineProperty(exports, "expect", { enumerable: true, get: function () { return log_marker_fixture_1.expect; } });
@@ -0,0 +1 @@
1
+ export { baseConfig } from '../shared/configs/playwright.base';
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.baseConfig = void 0;
4
+ var playwright_base_1 = require("../shared/configs/playwright.base");
5
+ Object.defineProperty(exports, "baseConfig", { enumerable: true, get: function () { return playwright_base_1.baseConfig; } });
@@ -0,0 +1 @@
1
+ export type { FeatureConfig, RepoPrerequisite, StartCommand, HealthCheck, NgrokTunnel, } from '../shared/launcher/types';
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const runner_1 = require("../shared/e2e-runner/runner");
5
+ const root_cli_1 = require("../shared/env-switcher/root-cli");
6
+ const new_feature_1 = require("./new-feature");
7
+ const init_project_1 = require("./init-project");
8
+ function printUsage() {
9
+ console.log(`Canary Lab
10
+
11
+ Usage:
12
+ canary-lab init <folder> [--package-spec <spec>]
13
+ canary-lab run
14
+ canary-lab env
15
+ canary-lab new-feature <name> [description]
16
+ `);
17
+ }
18
+ async function main() {
19
+ const [command, ...args] = process.argv.slice(2);
20
+ switch (command) {
21
+ case 'init':
22
+ await (0, init_project_1.main)(args);
23
+ return;
24
+ case 'run':
25
+ await (0, runner_1.main)();
26
+ return;
27
+ case 'env':
28
+ await (0, root_cli_1.main)(args);
29
+ return;
30
+ case 'new-feature':
31
+ await (0, new_feature_1.main)(args);
32
+ return;
33
+ case '-h':
34
+ case '--help':
35
+ case undefined:
36
+ printUsage();
37
+ return;
38
+ default:
39
+ console.error(`Unknown command: ${command}`);
40
+ printUsage();
41
+ process.exit(1);
42
+ }
43
+ }
44
+ main().catch((err) => {
45
+ console.error(err);
46
+ process.exit(1);
47
+ });
@@ -0,0 +1 @@
1
+ export declare function main(args?: string[]): Promise<void>;
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.main = main;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ function resolveFirstExisting(pathsToTry) {
10
+ const match = pathsToTry.find((candidate) => fs_1.default.existsSync(candidate));
11
+ if (!match) {
12
+ throw new Error(`Could not resolve any expected path: ${pathsToTry.join(', ')}`);
13
+ }
14
+ return match;
15
+ }
16
+ function getPackageJsonPath() {
17
+ return resolveFirstExisting([
18
+ path_1.default.resolve(__dirname, '../package.json'),
19
+ path_1.default.resolve(__dirname, '../../package.json'),
20
+ ]);
21
+ }
22
+ function getTemplateRoot() {
23
+ return resolveFirstExisting([
24
+ path_1.default.resolve(__dirname, '../templates/project'),
25
+ path_1.default.resolve(__dirname, '../../templates/project'),
26
+ ]);
27
+ }
28
+ function copyDir(sourceDir, targetDir) {
29
+ fs_1.default.mkdirSync(targetDir, { recursive: true });
30
+ for (const entry of fs_1.default.readdirSync(sourceDir, { withFileTypes: true })) {
31
+ const sourcePath = path_1.default.join(sourceDir, entry.name);
32
+ const targetPath = path_1.default.join(targetDir, entry.name);
33
+ if (entry.isDirectory()) {
34
+ copyDir(sourcePath, targetPath);
35
+ continue;
36
+ }
37
+ fs_1.default.copyFileSync(sourcePath, targetPath);
38
+ }
39
+ }
40
+ function readPackageVersion() {
41
+ const pkgPath = getPackageJsonPath();
42
+ return JSON.parse(fs_1.default.readFileSync(pkgPath, 'utf-8')).version;
43
+ }
44
+ function parseArgs(args) {
45
+ const folder = args[0];
46
+ if (!folder) {
47
+ console.error('Usage: canary-lab init <folder> [--package-spec <spec>]');
48
+ process.exit(1);
49
+ }
50
+ let packageSpec = `^${readPackageVersion()}`;
51
+ for (let i = 1; i < args.length; i += 1) {
52
+ if (args[i] === '--package-spec') {
53
+ packageSpec = args[i + 1];
54
+ i += 1;
55
+ }
56
+ }
57
+ if (!packageSpec) {
58
+ console.error('Missing value for --package-spec');
59
+ process.exit(1);
60
+ }
61
+ return { folder, packageSpec };
62
+ }
63
+ function buildPackageJson(projectName, packageSpec) {
64
+ return JSON.stringify({
65
+ name: projectName,
66
+ private: true,
67
+ version: '0.1.0',
68
+ description: 'Canary Lab project scaffold',
69
+ scripts: {
70
+ 'canary-lab:run': 'canary-lab run',
71
+ 'canary-lab:env': 'canary-lab env',
72
+ 'canary-lab:new-feature': 'canary-lab new-feature',
73
+ 'install:browsers': 'playwright install chromium',
74
+ },
75
+ devDependencies: {
76
+ '@playwright/test': '^1.54.2',
77
+ 'canary-lab': packageSpec,
78
+ },
79
+ }, null, 2) + '\n';
80
+ }
81
+ async function main(args = process.argv.slice(2)) {
82
+ const { folder, packageSpec } = parseArgs(args);
83
+ const targetDir = path_1.default.resolve(process.cwd(), folder);
84
+ if (fs_1.default.existsSync(targetDir)) {
85
+ const entries = fs_1.default.readdirSync(targetDir);
86
+ if (entries.length > 0) {
87
+ console.error(`Target directory is not empty: ${targetDir}`);
88
+ process.exit(1);
89
+ }
90
+ }
91
+ else {
92
+ fs_1.default.mkdirSync(targetDir, { recursive: true });
93
+ }
94
+ copyDir(getTemplateRoot(), targetDir);
95
+ const projectName = path_1.default.basename(targetDir);
96
+ fs_1.default.writeFileSync(path_1.default.join(targetDir, 'package.json'), buildPackageJson(projectName, packageSpec));
97
+ console.log(`\n Canary Lab project created at ${targetDir}\n`);
98
+ console.log(' Next steps:');
99
+ console.log(` 1. cd ${folder}`);
100
+ console.log(' 2. npm install');
101
+ console.log(' 3. npm run install:browsers');
102
+ console.log(' 4. npx canary-lab run');
103
+ console.log('');
104
+ }
105
+ if (require.main === module) {
106
+ main().catch((err) => {
107
+ console.error(err);
108
+ process.exit(1);
109
+ });
110
+ }
@@ -0,0 +1 @@
1
+ export declare function main(args?: string[]): Promise<void>;
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.main = main;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const project_root_1 = require("../shared/runtime/project-root");
10
+ function buildFeatureConfig(name, description) {
11
+ return `const config = {
12
+ name: '${name}',
13
+ description: '${description}',
14
+ envs: ['local'],
15
+ repos: [
16
+ // {
17
+ // name: 'your-repo',
18
+ // localPath: '/absolute/path/to/your-repo',
19
+ // cloneUrl: 'git@github.com:your-org/your-repo.git',
20
+ // startCommands: [
21
+ // {
22
+ // name: 'your-repo dev server',
23
+ // command: 'npm run dev',
24
+ // healthCheck: {
25
+ // url: 'http://localhost:3000/',
26
+ // timeoutMs: 2000,
27
+ // },
28
+ // },
29
+ // ],
30
+ // },
31
+ ],
32
+ featureDir: __dirname,
33
+ }
34
+
35
+ module.exports = { config }
36
+ `;
37
+ }
38
+ function buildPlaywrightConfig() {
39
+ return `const { defineConfig } = require('@playwright/test')
40
+ const { baseConfig } = require('canary-lab/feature-support/playwright-base')
41
+
42
+ module.exports = defineConfig({
43
+ ...baseConfig,
44
+ })
45
+ `;
46
+ }
47
+ function buildFeatureConfigJs() {
48
+ return `const { loadFeatureEnv } = require('canary-lab/feature-support/load-env')
49
+
50
+ loadFeatureEnv(__dirname + '/..')
51
+
52
+ const GATEWAY_URL = process.env.GATEWAY_URL ?? 'http://localhost:3000'
53
+
54
+ module.exports = {
55
+ GATEWAY_URL,
56
+ }
57
+ `;
58
+ }
59
+ function buildEnvsetsConfig(name) {
60
+ return JSON.stringify({
61
+ appRoots: {},
62
+ slots: {
63
+ [`${name}.env`]: {
64
+ description: `Canary Lab ${name} feature .env`,
65
+ target: `$CANARY_LAB_PROJECT_ROOT/features/${name}/.env`,
66
+ },
67
+ },
68
+ feature: {
69
+ slots: [`${name}.env`],
70
+ testCommand: 'npx playwright test',
71
+ testCwd: `$CANARY_LAB_PROJECT_ROOT/features/${name}`,
72
+ },
73
+ }, null, 2) + '\n';
74
+ }
75
+ function buildSpec(name) {
76
+ return `const { test, expect } = require('canary-lab/feature-support/log-marker-fixture')
77
+
78
+ test.describe('${name}', () => {
79
+ test('example test', async () => {
80
+ expect(true).toBe(true)
81
+ })
82
+ })
83
+ `;
84
+ }
85
+ async function main(args = process.argv.slice(2)) {
86
+ const name = args[0];
87
+ const description = args.slice(1).join(' ') || 'TODO: add description';
88
+ if (!name) {
89
+ console.error('Usage: canary-lab new-feature <name> [description]');
90
+ process.exit(1);
91
+ }
92
+ if (!/^[a-z][a-z0-9_]*$/.test(name)) {
93
+ console.error(`Invalid feature name "${name}". Use snake_case (e.g. cns_webhooks).`);
94
+ process.exit(1);
95
+ }
96
+ const featureDir = path_1.default.join((0, project_root_1.getFeaturesDir)(), name);
97
+ if (fs_1.default.existsSync(featureDir)) {
98
+ console.error(`Feature "${name}" already exists at ${featureDir}`);
99
+ process.exit(1);
100
+ }
101
+ const files = [
102
+ ['feature.config.cjs', buildFeatureConfig(name, description)],
103
+ ['playwright.config.js', buildPlaywrightConfig()],
104
+ ['.env.example', 'GATEWAY_URL=http://localhost:3000\n'],
105
+ ['src/config.js', buildFeatureConfigJs()],
106
+ ['envsets/envsets.config.json', buildEnvsetsConfig(name)],
107
+ [`envsets/local/${name}.env`, 'GATEWAY_URL=http://localhost:3000\n'],
108
+ [`e2e/${name}.spec.js`, buildSpec(name)],
109
+ ];
110
+ for (const [relPath, content] of files) {
111
+ const fullPath = path_1.default.join(featureDir, relPath);
112
+ fs_1.default.mkdirSync(path_1.default.dirname(fullPath), { recursive: true });
113
+ fs_1.default.writeFileSync(fullPath, content);
114
+ }
115
+ fs_1.default.mkdirSync(path_1.default.join(featureDir, 'e2e/helpers'), { recursive: true });
116
+ console.log(`\n Feature "${name}" created at features/${name}/\n`);
117
+ console.log(' Created files:');
118
+ for (const [relPath] of files) {
119
+ console.log(` features/${name}/${relPath}`);
120
+ }
121
+ console.log(` features/${name}/e2e/helpers/`);
122
+ console.log('');
123
+ console.log(' Next steps:');
124
+ console.log(' 1. Edit feature.config.cjs — add your repos, start commands, and health checks');
125
+ console.log(' 2. Edit src/config.js — add feature-specific env var exports');
126
+ console.log(` 3. Write your tests in e2e/${name}.spec.js`);
127
+ console.log(' 4. Run: npx canary-lab run');
128
+ console.log('');
129
+ }
130
+ if (require.main === module) {
131
+ main().catch((err) => {
132
+ console.error(err);
133
+ process.exit(1);
134
+ });
135
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Load .env file from a feature directory. Call this at the top of
3
+ * your feature's src/config.ts before exporting typed constants.
4
+ */
5
+ export declare function loadFeatureEnv(featureDir: string): void;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.loadFeatureEnv = loadFeatureEnv;
7
+ const path_1 = __importDefault(require("path"));
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const dotenv_1 = __importDefault(require("dotenv"));
10
+ /**
11
+ * Load .env file from a feature directory. Call this at the top of
12
+ * your feature's src/config.ts before exporting typed constants.
13
+ */
14
+ function loadFeatureEnv(featureDir) {
15
+ const envFile = path_1.default.join(featureDir, '.env');
16
+ if (fs_1.default.existsSync(envFile)) {
17
+ dotenv_1.default.config({ path: envFile });
18
+ }
19
+ }
@@ -0,0 +1,12 @@
1
+ export declare const baseConfig: {
2
+ testDir: string;
3
+ fullyParallel: boolean;
4
+ workers: number;
5
+ retries: number;
6
+ reporter: any[];
7
+ timeout: number;
8
+ use: {
9
+ trace: "retain-on-failure";
10
+ screenshot: "only-on-failure";
11
+ };
12
+ };