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.
- package/LICENSE +21 -0
- package/README.md +282 -0
- package/dist/feature-support/load-env.d.ts +1 -0
- package/dist/feature-support/load-env.js +5 -0
- package/dist/feature-support/log-marker-fixture.d.ts +1 -0
- package/dist/feature-support/log-marker-fixture.js +6 -0
- package/dist/feature-support/playwright-base.d.ts +1 -0
- package/dist/feature-support/playwright-base.js +5 -0
- package/dist/feature-support/types.d.ts +1 -0
- package/dist/feature-support/types.js +2 -0
- package/dist/scripts/cli.d.ts +2 -0
- package/dist/scripts/cli.js +47 -0
- package/dist/scripts/init-project.d.ts +1 -0
- package/dist/scripts/init-project.js +110 -0
- package/dist/scripts/new-feature.d.ts +1 -0
- package/dist/scripts/new-feature.js +135 -0
- package/dist/shared/configs/loadEnv.d.ts +5 -0
- package/dist/shared/configs/loadEnv.js +19 -0
- package/dist/shared/configs/playwright.base.d.ts +12 -0
- package/dist/shared/configs/playwright.base.js +15 -0
- package/dist/shared/e2e-runner/log-marker-fixture.d.ts +13 -0
- package/dist/shared/e2e-runner/log-marker-fixture.js +45 -0
- package/dist/shared/e2e-runner/paths.d.ts +8 -0
- package/dist/shared/e2e-runner/paths.js +16 -0
- package/dist/shared/e2e-runner/runner.d.ts +1 -0
- package/dist/shared/e2e-runner/runner.js +567 -0
- package/dist/shared/e2e-runner/summary-reporter.d.ts +7 -0
- package/dist/shared/e2e-runner/summary-reporter.js +33 -0
- package/dist/shared/env-switcher/root-cli.d.ts +1 -0
- package/dist/shared/env-switcher/root-cli.js +84 -0
- package/dist/shared/env-switcher/switch.d.ts +1 -0
- package/dist/shared/env-switcher/switch.js +249 -0
- package/dist/shared/env-switcher/types.d.ts +18 -0
- package/dist/shared/env-switcher/types.js +2 -0
- package/dist/shared/launcher/iterm.d.ts +2 -0
- package/dist/shared/launcher/iterm.js +28 -0
- package/dist/shared/launcher/startup.d.ts +9 -0
- package/dist/shared/launcher/startup.js +40 -0
- package/dist/shared/launcher/terminal.d.ts +2 -0
- package/dist/shared/launcher/terminal.js +25 -0
- package/dist/shared/launcher/types.d.ts +28 -0
- package/dist/shared/launcher/types.js +2 -0
- package/dist/shared/runtime/project-root.d.ts +2 -0
- package/dist/shared/runtime/project-root.js +32 -0
- package/dist/templates/project/.claude/skills/self-fixing-loop.md +53 -0
- package/dist/templates/project/.codex/self-fixing-loop.md +49 -0
- package/dist/templates/project/AGENTS.md +27 -0
- package/dist/templates/project/CLAUDE.md +31 -0
- package/dist/templates/project/features/broken_todo_api/.env.example +1 -0
- package/dist/templates/project/features/broken_todo_api/e2e/broken-todo-api.spec.js +34 -0
- package/dist/templates/project/features/broken_todo_api/e2e/helpers/api.js +48 -0
- package/dist/templates/project/features/broken_todo_api/envsets/envsets.config.json +14 -0
- package/dist/templates/project/features/broken_todo_api/envsets/local/broken_todo_api.env +1 -0
- package/dist/templates/project/features/broken_todo_api/feature.config.cjs +24 -0
- package/dist/templates/project/features/broken_todo_api/playwright.config.js +6 -0
- package/dist/templates/project/features/broken_todo_api/scripts/server.js +76 -0
- package/dist/templates/project/features/broken_todo_api/src/config.js +9 -0
- package/dist/templates/project/features/example_todo_api/.env.example +1 -0
- package/dist/templates/project/features/example_todo_api/e2e/helpers/api.js +36 -0
- package/dist/templates/project/features/example_todo_api/e2e/todo-api.spec.js +25 -0
- package/dist/templates/project/features/example_todo_api/envsets/envsets.config.json +14 -0
- package/dist/templates/project/features/example_todo_api/envsets/local/example_todo_api.env +1 -0
- package/dist/templates/project/features/example_todo_api/feature.config.cjs +24 -0
- package/dist/templates/project/features/example_todo_api/playwright.config.js +6 -0
- package/dist/templates/project/features/example_todo_api/scripts/server.js +60 -0
- package/dist/templates/project/features/example_todo_api/src/config.js +9 -0
- 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 <test-tag> to each service log"]
|
|
203
|
+
E --> F["After each test, the runner appends </test-tag>"]
|
|
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,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,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
|
+
}
|