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
@@ -0,0 +1,27 @@
1
+ # Canary Lab Agent Guide
2
+
3
+ For the full Codex self-fixing workflow, read:
4
+
5
+ - `.codex/self-fixing-loop.md`
6
+
7
+ ## Quick Start
8
+
9
+ 1. Run `npx canary-lab run`
10
+ 2. Leave the runner open in watch mode
11
+ 3. In Codex, type:
12
+
13
+ ```text
14
+ self heal
15
+ ```
16
+
17
+ ## What `self heal` Means
18
+
19
+ When the user types `self heal`, follow `.codex/self-fixing-loop.md`.
20
+
21
+ That workflow covers:
22
+
23
+ - which logs to inspect
24
+ - how to diagnose the failure
25
+ - the rule to fix implementation only
26
+ - when to use `touch logs/.restart`
27
+ - when to use `touch logs/.rerun`
@@ -0,0 +1,31 @@
1
+ # Canary Lab Project Notes
2
+
3
+ For the full Claude self-fixing workflow, read:
4
+
5
+ - `.claude/skills/self-fixing-loop.md`
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ npm install
11
+ npm run install:browsers
12
+ npx canary-lab run
13
+ ```
14
+
15
+ Leave the runner open in watch mode, then type:
16
+
17
+ ```text
18
+ self heal
19
+ ```
20
+
21
+ ## What `self heal` Means
22
+
23
+ When the user types `self heal`, follow `.claude/skills/self-fixing-loop.md`.
24
+
25
+ That workflow covers:
26
+
27
+ - which logs to inspect
28
+ - how to diagnose the failure
29
+ - the rule to fix implementation only
30
+ - when to use `touch logs/.restart`
31
+ - when to use `touch logs/.rerun`
@@ -0,0 +1 @@
1
+ GATEWAY_URL=http://localhost:4100
@@ -0,0 +1,34 @@
1
+ const { test, expect } = require('canary-lab/feature-support/log-marker-fixture')
2
+ const { TodoApi } = require('./helpers/api')
3
+
4
+ const api = new TodoApi()
5
+
6
+ test.describe('broken_todo_api', () => {
7
+ test('POST /todos creates a todo', async () => {
8
+ const todo = await api.create('Write logs')
9
+ expect(todo.id).toBeTruthy()
10
+ expect(todo.title).toBe('Write logs')
11
+ expect(todo.done).toBe(false)
12
+ })
13
+
14
+ test('GET /todos lists created todos', async () => {
15
+ const todo = await api.create('Visible in list')
16
+ const todos = await api.list()
17
+ expect(todos.find((entry) => entry.id === todo.id)?.title).toBe(
18
+ 'Visible in list',
19
+ )
20
+ })
21
+
22
+ test('PATCH /todos/:id marks a todo as done', async () => {
23
+ const todo = await api.create('Should become done')
24
+ const updated = await api.markDone(todo.id)
25
+ expect(updated.done).toBe(true)
26
+ })
27
+
28
+ test('DELETE /todos/:id removes a todo', async () => {
29
+ const todo = await api.create('This should be removed')
30
+ await api.remove(todo.id)
31
+ const todos = await api.list()
32
+ expect(todos.find((entry) => entry.id === todo.id)).toBeUndefined()
33
+ })
34
+ })
@@ -0,0 +1,48 @@
1
+ const { GATEWAY_URL } = require('../../src/config')
2
+
3
+ class TodoApi {
4
+ constructor() {
5
+ this.baseUrl = GATEWAY_URL
6
+ }
7
+
8
+ async create(title) {
9
+ const res = await fetch(`${this.baseUrl}/todos`, {
10
+ method: 'POST',
11
+ headers: { 'Content-Type': 'application/json' },
12
+ body: JSON.stringify({ title }),
13
+ })
14
+ if (!res.ok) {
15
+ throw new Error(`POST /todos failed: ${res.status}`)
16
+ }
17
+ return res.json()
18
+ }
19
+
20
+ async list() {
21
+ const res = await fetch(`${this.baseUrl}/todos`)
22
+ if (!res.ok) {
23
+ throw new Error(`GET /todos failed: ${res.status}`)
24
+ }
25
+ return res.json()
26
+ }
27
+
28
+ async remove(id) {
29
+ const res = await fetch(`${this.baseUrl}/todos/${id}`, { method: 'DELETE' })
30
+ if (!res.ok && res.status !== 204) {
31
+ throw new Error(`DELETE /todos/${id} failed: ${res.status}`)
32
+ }
33
+ }
34
+
35
+ async markDone(id) {
36
+ const res = await fetch(`${this.baseUrl}/todos/${id}`, {
37
+ method: 'PATCH',
38
+ headers: { 'Content-Type': 'application/json' },
39
+ body: JSON.stringify({ done: true }),
40
+ })
41
+ if (!res.ok) {
42
+ throw new Error(`PATCH /todos/${id} failed: ${res.status}`)
43
+ }
44
+ return res.json()
45
+ }
46
+ }
47
+
48
+ module.exports = { TodoApi }
@@ -0,0 +1,14 @@
1
+ {
2
+ "appRoots": {},
3
+ "slots": {
4
+ "broken_todo_api.env": {
5
+ "description": "Environment file for the intentionally broken TODO API sample",
6
+ "target": "$CANARY_LAB_PROJECT_ROOT/features/broken_todo_api/.env"
7
+ }
8
+ },
9
+ "feature": {
10
+ "slots": ["broken_todo_api.env"],
11
+ "testCommand": "npx playwright test",
12
+ "testCwd": "$CANARY_LAB_PROJECT_ROOT/features/broken_todo_api"
13
+ }
14
+ }
@@ -0,0 +1 @@
1
+ GATEWAY_URL=http://localhost:4100
@@ -0,0 +1,24 @@
1
+ const config = {
2
+ name: 'broken_todo_api',
3
+ description: 'Intentionally broken sample feature with mixed passing and failing tests for self-fixing practice.',
4
+ envs: ['local'],
5
+ repos: [
6
+ {
7
+ name: 'broken_todo_api',
8
+ localPath: __dirname,
9
+ startCommands: [
10
+ {
11
+ name: 'broken-todo-api-server',
12
+ command: 'node scripts/server.js',
13
+ healthCheck: {
14
+ url: 'http://localhost:4100/',
15
+ timeoutMs: 3000,
16
+ },
17
+ },
18
+ ],
19
+ },
20
+ ],
21
+ featureDir: __dirname,
22
+ }
23
+
24
+ module.exports = { config }
@@ -0,0 +1,6 @@
1
+ const { defineConfig } = require('@playwright/test')
2
+ const { baseConfig } = require('canary-lab/feature-support/playwright-base')
3
+
4
+ module.exports = defineConfig({
5
+ ...baseConfig,
6
+ })
@@ -0,0 +1,76 @@
1
+ const http = require('http')
2
+
3
+ const todos = []
4
+ let nextId = 1
5
+
6
+ const server = http.createServer((req, res) => {
7
+ const url = new URL(req.url ?? '/', `http://${req.headers.host}`)
8
+ const method = req.method ?? 'GET'
9
+
10
+ console.log(`[broken_todo_api] ${method} ${url.pathname}`)
11
+ res.setHeader('Content-Type', 'application/json')
12
+
13
+ if (method === 'GET' && url.pathname === '/') {
14
+ res.end(JSON.stringify({ status: 'ok' }))
15
+ return
16
+ }
17
+
18
+ if (method === 'GET' && url.pathname === '/todos') {
19
+ res.end(JSON.stringify(todos))
20
+ return
21
+ }
22
+
23
+ if (method === 'POST' && url.pathname === '/todos') {
24
+ let body = ''
25
+ req.on('data', (chunk) => {
26
+ body += chunk
27
+ })
28
+ req.on('end', () => {
29
+ const parsed = JSON.parse(body || '{}')
30
+ const todo = { id: String(nextId++), title: parsed.title, done: false }
31
+ todos.push(todo)
32
+ res.writeHead(201)
33
+ res.end(JSON.stringify(todo))
34
+ })
35
+ return
36
+ }
37
+
38
+ if (method === 'PATCH' && url.pathname.startsWith('/todos/')) {
39
+ const id = url.pathname.split('/')[2]
40
+ const todo = todos.find((entry) => entry.id === id)
41
+ if (!todo) {
42
+ res.writeHead(404)
43
+ res.end(JSON.stringify({ error: 'not found' }))
44
+ return
45
+ }
46
+
47
+ let body = ''
48
+ req.on('data', (chunk) => {
49
+ body += chunk
50
+ })
51
+ req.on('end', () => {
52
+ const parsed = JSON.parse(body || '{}')
53
+ console.log(
54
+ `[broken_todo_api] simulated bug: ignoring done=${parsed.done} for ${id}`,
55
+ )
56
+ res.end(JSON.stringify(todo))
57
+ })
58
+ return
59
+ }
60
+
61
+ if (method === 'DELETE' && url.pathname.startsWith('/todos/')) {
62
+ const id = url.pathname.split('/')[2]
63
+ console.log(`[broken_todo_api] simulated bug: refusing to delete ${id}`)
64
+ res.writeHead(204)
65
+ res.end()
66
+ return
67
+ }
68
+
69
+ res.writeHead(404)
70
+ res.end(JSON.stringify({ error: 'not found' }))
71
+ })
72
+
73
+ const port = Number.parseInt(process.env.PORT ?? '4100', 10)
74
+ server.listen(port, () => {
75
+ console.log(`Broken TODO API listening on http://localhost:${port}`)
76
+ })
@@ -0,0 +1,9 @@
1
+ const { loadFeatureEnv } = require('canary-lab/feature-support/load-env')
2
+
3
+ loadFeatureEnv(__dirname + '/..')
4
+
5
+ const GATEWAY_URL = process.env.GATEWAY_URL ?? 'http://localhost:4100'
6
+
7
+ module.exports = {
8
+ GATEWAY_URL,
9
+ }
@@ -0,0 +1 @@
1
+ GATEWAY_URL=http://localhost:4000
@@ -0,0 +1,36 @@
1
+ const { GATEWAY_URL } = require('../../src/config')
2
+
3
+ class TodoApi {
4
+ constructor() {
5
+ this.baseUrl = GATEWAY_URL
6
+ }
7
+
8
+ async create(title) {
9
+ const res = await fetch(`${this.baseUrl}/todos`, {
10
+ method: 'POST',
11
+ headers: { 'Content-Type': 'application/json' },
12
+ body: JSON.stringify({ title }),
13
+ })
14
+ if (!res.ok) {
15
+ throw new Error(`POST /todos failed: ${res.status}`)
16
+ }
17
+ return res.json()
18
+ }
19
+
20
+ async list() {
21
+ const res = await fetch(`${this.baseUrl}/todos`)
22
+ if (!res.ok) {
23
+ throw new Error(`GET /todos failed: ${res.status}`)
24
+ }
25
+ return res.json()
26
+ }
27
+
28
+ async remove(id) {
29
+ const res = await fetch(`${this.baseUrl}/todos/${id}`, { method: 'DELETE' })
30
+ if (!res.ok && res.status !== 204) {
31
+ throw new Error(`DELETE /todos/${id} failed: ${res.status}`)
32
+ }
33
+ }
34
+ }
35
+
36
+ module.exports = { TodoApi }
@@ -0,0 +1,25 @@
1
+ const { test, expect } = require('canary-lab/feature-support/log-marker-fixture')
2
+ const { TodoApi } = require('./helpers/api')
3
+
4
+ const api = new TodoApi()
5
+
6
+ test.describe('example_todo_api', () => {
7
+ test('POST /todos creates a todo', async () => {
8
+ const todo = await api.create('Buy milk')
9
+ expect(todo.id).toBeTruthy()
10
+ expect(todo.title).toBe('Buy milk')
11
+ expect(todo.done).toBe(false)
12
+ })
13
+
14
+ test('GET /todos lists todos', async () => {
15
+ const todos = await api.list()
16
+ expect(todos.length).toBeGreaterThan(0)
17
+ })
18
+
19
+ test('DELETE /todos/:id removes a todo', async () => {
20
+ const todo = await api.create('Temporary item')
21
+ await api.remove(todo.id)
22
+ const todos = await api.list()
23
+ expect(todos.find((entry) => entry.id === todo.id)).toBeUndefined()
24
+ })
25
+ })
@@ -0,0 +1,14 @@
1
+ {
2
+ "appRoots": {},
3
+ "slots": {
4
+ "example_todo_api.env": {
5
+ "description": "Environment file for the example TODO API sample",
6
+ "target": "$CANARY_LAB_PROJECT_ROOT/features/example_todo_api/.env"
7
+ }
8
+ },
9
+ "feature": {
10
+ "slots": ["example_todo_api.env"],
11
+ "testCommand": "npx playwright test",
12
+ "testCwd": "$CANARY_LAB_PROJECT_ROOT/features/example_todo_api"
13
+ }
14
+ }
@@ -0,0 +1 @@
1
+ GATEWAY_URL=http://localhost:4000
@@ -0,0 +1,24 @@
1
+ const config = {
2
+ name: 'example_todo_api',
3
+ description: 'Working sample feature for Canary Lab.',
4
+ envs: ['local'],
5
+ repos: [
6
+ {
7
+ name: 'example_todo_api',
8
+ localPath: __dirname,
9
+ startCommands: [
10
+ {
11
+ name: 'example-todo-api-server',
12
+ command: 'node scripts/server.js',
13
+ healthCheck: {
14
+ url: 'http://localhost:4000/',
15
+ timeoutMs: 3000,
16
+ },
17
+ },
18
+ ],
19
+ },
20
+ ],
21
+ featureDir: __dirname,
22
+ }
23
+
24
+ module.exports = { config }
@@ -0,0 +1,6 @@
1
+ const { defineConfig } = require('@playwright/test')
2
+ const { baseConfig } = require('canary-lab/feature-support/playwright-base')
3
+
4
+ module.exports = defineConfig({
5
+ ...baseConfig,
6
+ })
@@ -0,0 +1,60 @@
1
+ const http = require('http')
2
+
3
+ const todos = []
4
+ let nextId = 1
5
+
6
+ const server = http.createServer((req, res) => {
7
+ const url = new URL(req.url ?? '/', `http://${req.headers.host}`)
8
+ const method = req.method ?? 'GET'
9
+
10
+ console.log(`[example_todo_api] ${method} ${url.pathname}`)
11
+ res.setHeader('Content-Type', 'application/json')
12
+
13
+ if (method === 'GET' && url.pathname === '/') {
14
+ res.end(JSON.stringify({ status: 'ok' }))
15
+ return
16
+ }
17
+
18
+ if (method === 'GET' && url.pathname === '/todos') {
19
+ res.end(JSON.stringify(todos))
20
+ return
21
+ }
22
+
23
+ if (method === 'POST' && url.pathname === '/todos') {
24
+ let body = ''
25
+ req.on('data', (chunk) => {
26
+ body += chunk
27
+ })
28
+ req.on('end', () => {
29
+ const parsed = JSON.parse(body || '{}')
30
+ const todo = { id: String(nextId++), title: parsed.title, done: false }
31
+ todos.push(todo)
32
+ res.writeHead(201)
33
+ res.end(JSON.stringify(todo))
34
+ })
35
+ return
36
+ }
37
+
38
+ if (method === 'DELETE' && url.pathname.startsWith('/todos/')) {
39
+ const id = url.pathname.split('/')[2]
40
+ const idx = todos.findIndex((todo) => todo.id === id)
41
+ if (idx === -1) {
42
+ res.writeHead(404)
43
+ res.end(JSON.stringify({ error: 'not found' }))
44
+ return
45
+ }
46
+
47
+ todos.splice(idx, 1)
48
+ res.writeHead(204)
49
+ res.end()
50
+ return
51
+ }
52
+
53
+ res.writeHead(404)
54
+ res.end(JSON.stringify({ error: 'not found' }))
55
+ })
56
+
57
+ const port = Number.parseInt(process.env.PORT ?? '4000', 10)
58
+ server.listen(port, () => {
59
+ console.log(`Example TODO API listening on http://localhost:${port}`)
60
+ })
@@ -0,0 +1,9 @@
1
+ const { loadFeatureEnv } = require('canary-lab/feature-support/load-env')
2
+
3
+ loadFeatureEnv(__dirname + '/..')
4
+
5
+ const GATEWAY_URL = process.env.GATEWAY_URL ?? 'http://localhost:4000'
6
+
7
+ module.exports = {
8
+ GATEWAY_URL,
9
+ }
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "canary-lab",
3
+ "version": "0.1.0",
4
+ "description": "E2E test scaffolder and runner with agent-assisted self-fixing workflows",
5
+ "keywords": [
6
+ "playwright",
7
+ "e2e",
8
+ "testing",
9
+ "test-runner",
10
+ "local-development",
11
+ "observability",
12
+ "developer-tools",
13
+ "ai-agents",
14
+ "claude",
15
+ "codex"
16
+ ],
17
+ "author": {
18
+ "name": "Fer Terahadi",
19
+ "email": "fernanditerahadi@gmail.com",
20
+ "url": "https://github.com/ferterahadi/canary-lab"
21
+ },
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/ferterahadi/canary-lab.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/ferterahadi/canary-lab/issues"
29
+ },
30
+ "homepage": "https://github.com/ferterahadi/canary-lab#readme",
31
+ "engines": {
32
+ "node": ">=20.0.0",
33
+ "npm": ">=9.0.0"
34
+ },
35
+ "bin": {
36
+ "canary-lab": "dist/scripts/cli.js"
37
+ },
38
+ "exports": {
39
+ "./feature-support/playwright-base": "./dist/feature-support/playwright-base.js",
40
+ "./feature-support/load-env": "./dist/feature-support/load-env.js",
41
+ "./feature-support/log-marker-fixture": "./dist/feature-support/log-marker-fixture.js",
42
+ "./feature-support/types": "./dist/feature-support/types.js"
43
+ },
44
+ "files": [
45
+ "dist",
46
+ "README.md",
47
+ "LICENSE"
48
+ ],
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "scripts": {
53
+ "build": "tsc -p tsconfig.build.json && node tools/prepare-assets.mjs",
54
+ "canary-lab:run": "node dist/scripts/cli.js run",
55
+ "canary-lab:env": "node dist/scripts/cli.js env",
56
+ "canary-lab:new-feature": "node dist/scripts/cli.js new-feature",
57
+ "prepack": "npm run build",
58
+ "pack:check": "node tools/pack-check.mjs",
59
+ "smoke:pack": "node tools/smoke-pack.mjs",
60
+ "publish:package": "node tools/publish-package.mjs"
61
+ },
62
+ "devDependencies": {
63
+ "@types/node": "^25.5.2",
64
+ "tsx": "^4.20.3",
65
+ "typescript": "^5.9.2"
66
+ },
67
+ "dependencies": {
68
+ "@playwright/test": "^1.54.2",
69
+ "dotenv": "^16.6.1"
70
+ }
71
+ }