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
|
@@ -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,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 @@
|
|
|
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,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
|
+
})
|
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
|
+
}
|