@commandable/integration-data 0.0.6 → 0.0.7
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/dist/credentials-index.d.ts.map +1 -1
- package/dist/credentials-index.js +119 -0
- package/dist/credentials-index.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +33 -0
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +36 -4
- package/dist/loader.js.map +1 -1
- package/integrations/__tests__/liveHarness.ts +16 -3
- package/integrations/airtable/.env.test +9 -0
- package/integrations/airtable/.env.test.example +11 -0
- package/integrations/airtable/README.md +27 -0
- package/integrations/airtable/__tests__/get_handlers.test.ts +43 -5
- package/integrations/confluence/.env.test +25 -0
- package/integrations/confluence/.env.test.example +36 -0
- package/integrations/confluence/README.md +28 -0
- package/integrations/confluence/__tests__/get_handlers.test.ts +121 -0
- package/integrations/confluence/__tests__/usage_parity.test.ts +14 -0
- package/integrations/confluence/__tests__/write_handlers.test.ts +131 -0
- package/integrations/confluence/credentials.json +39 -0
- package/integrations/confluence/credentials_hint.md +4 -0
- package/integrations/confluence/credentials_hint_api_token.md +9 -0
- package/integrations/confluence/credentials_hint_oauth_token.md +8 -0
- package/integrations/confluence/handlers/add_comment.js +19 -0
- package/integrations/confluence/handlers/add_label.js +16 -0
- package/integrations/confluence/handlers/create_page.js +22 -0
- package/integrations/confluence/handlers/delete_page.js +17 -0
- package/integrations/confluence/handlers/get_comments.js +33 -0
- package/integrations/confluence/handlers/get_page_children.js +30 -0
- package/integrations/confluence/handlers/get_space.js +22 -0
- package/integrations/confluence/handlers/list_spaces.js +39 -0
- package/integrations/confluence/handlers/read_page.js +49 -0
- package/integrations/confluence/handlers/search_pages.js +42 -0
- package/integrations/confluence/handlers/update_page.js +42 -0
- package/integrations/confluence/manifest.json +85 -0
- package/integrations/confluence/prompt.md +55 -0
- package/integrations/confluence/schemas/add_comment.json +22 -0
- package/integrations/confluence/schemas/add_label.json +19 -0
- package/integrations/confluence/schemas/create_page.json +33 -0
- package/integrations/confluence/schemas/delete_page.json +23 -0
- package/integrations/confluence/schemas/empty.json +6 -0
- package/integrations/confluence/schemas/get_comments.json +24 -0
- package/integrations/confluence/schemas/get_page_children.json +28 -0
- package/integrations/confluence/schemas/get_space.json +18 -0
- package/integrations/confluence/schemas/list_spaces.json +36 -0
- package/integrations/confluence/schemas/read_page.json +28 -0
- package/integrations/confluence/schemas/search_pages.json +26 -0
- package/integrations/confluence/schemas/update_page.json +31 -0
- package/integrations/github/.env.test +16 -0
- package/integrations/github/.env.test.example +17 -0
- package/integrations/github/README.md +75 -0
- package/integrations/github/__tests__/get_handlers.test.ts +5 -5
- package/integrations/github/__tests__/write_handlers.test.ts +176 -58
- package/integrations/github/handlers/create_file.js +46 -0
- package/integrations/github/handlers/delete_file.js +14 -1
- package/integrations/github/handlers/edit_file.js +52 -0
- package/integrations/github/handlers/edit_files.js +107 -0
- package/integrations/github/manifest.json +74 -47
- package/integrations/github/prompt.md +36 -0
- package/integrations/github/schemas/create_file.json +13 -0
- package/integrations/github/schemas/delete_file.json +2 -2
- package/integrations/github/schemas/edit_file.json +26 -0
- package/integrations/github/schemas/edit_files.json +39 -0
- package/integrations/google-calendar/.env.test.example +11 -0
- package/integrations/google-calendar/README.md +41 -0
- package/integrations/google-calendar/__tests__/write_and_admin_handlers.test.ts +7 -7
- package/integrations/google-calendar/manifest.json +27 -17
- package/integrations/google-docs/README.md +30 -0
- package/integrations/google-drive/README.md +26 -0
- package/integrations/google-gmail/.env.test.example +11 -0
- package/integrations/google-gmail/README.md +49 -0
- package/integrations/google-gmail/handlers/create_draft_email.js +1 -1
- package/integrations/google-gmail/handlers/read_email.js +1 -1
- package/integrations/google-gmail/handlers/send_email.js +1 -1
- package/integrations/google-gmail/manifest.json +36 -25
- package/integrations/google-sheet/README.md +27 -0
- package/integrations/google-slides/README.md +28 -0
- package/integrations/hubspot/.env.test.example +20 -0
- package/integrations/hubspot/README.md +48 -0
- package/integrations/hubspot/__tests__/get_handlers.test.ts +151 -0
- package/integrations/hubspot/__tests__/usage_parity.test.ts +10 -0
- package/integrations/hubspot/__tests__/write_handlers.test.ts +244 -0
- package/integrations/hubspot/credentials.json +48 -0
- package/integrations/hubspot/credentials_hint.md +20 -0
- package/integrations/hubspot/credentials_hint_oauth_token.md +16 -0
- package/integrations/hubspot/handlers/archive_company.js +13 -0
- package/integrations/hubspot/handlers/archive_contact.js +13 -0
- package/integrations/hubspot/handlers/archive_deal.js +13 -0
- package/integrations/hubspot/handlers/archive_ticket.js +13 -0
- package/integrations/hubspot/handlers/create_association.js +18 -0
- package/integrations/hubspot/handlers/create_company.js +13 -0
- package/integrations/hubspot/handlers/create_contact.js +14 -0
- package/integrations/hubspot/handlers/create_deal.js +16 -0
- package/integrations/hubspot/handlers/create_note.js +44 -0
- package/integrations/hubspot/handlers/create_task.js +48 -0
- package/integrations/hubspot/handlers/create_ticket.js +15 -0
- package/integrations/hubspot/handlers/get_associations.js +14 -0
- package/integrations/hubspot/handlers/get_company.js +18 -0
- package/integrations/hubspot/handlers/get_contact.js +18 -0
- package/integrations/hubspot/handlers/get_deal.js +18 -0
- package/integrations/hubspot/handlers/get_ticket.js +20 -0
- package/integrations/hubspot/handlers/list_owners.js +12 -0
- package/integrations/hubspot/handlers/list_pipelines.js +5 -0
- package/integrations/hubspot/handlers/list_properties.js +11 -0
- package/integrations/hubspot/handlers/remove_association.js +22 -0
- package/integrations/hubspot/handlers/search_companies.js +43 -0
- package/integrations/hubspot/handlers/search_contacts.js +43 -0
- package/integrations/hubspot/handlers/search_deals.js +43 -0
- package/integrations/hubspot/handlers/search_notes.js +43 -0
- package/integrations/hubspot/handlers/search_tasks.js +43 -0
- package/integrations/hubspot/handlers/search_tickets.js +43 -0
- package/integrations/hubspot/handlers/update_company.js +13 -0
- package/integrations/hubspot/handlers/update_contact.js +14 -0
- package/integrations/hubspot/handlers/update_deal.js +16 -0
- package/integrations/hubspot/handlers/update_task.js +17 -0
- package/integrations/hubspot/handlers/update_ticket.js +15 -0
- package/integrations/hubspot/manifest.json +230 -0
- package/integrations/hubspot/prompt.md +69 -0
- package/integrations/hubspot/schemas/archive_company.json +13 -0
- package/integrations/hubspot/schemas/archive_contact.json +13 -0
- package/integrations/hubspot/schemas/archive_deal.json +9 -0
- package/integrations/hubspot/schemas/archive_ticket.json +9 -0
- package/integrations/hubspot/schemas/create_association.json +24 -0
- package/integrations/hubspot/schemas/create_company.json +14 -0
- package/integrations/hubspot/schemas/create_contact.json +15 -0
- package/integrations/hubspot/schemas/create_deal.json +20 -0
- package/integrations/hubspot/schemas/create_note.json +37 -0
- package/integrations/hubspot/schemas/create_task.json +51 -0
- package/integrations/hubspot/schemas/create_ticket.json +16 -0
- package/integrations/hubspot/schemas/empty.json +6 -0
- package/integrations/hubspot/schemas/get_associations.json +30 -0
- package/integrations/hubspot/schemas/get_company.json +27 -0
- package/integrations/hubspot/schemas/get_contact.json +27 -0
- package/integrations/hubspot/schemas/get_deal.json +20 -0
- package/integrations/hubspot/schemas/get_ticket.json +20 -0
- package/integrations/hubspot/schemas/list_owners.json +25 -0
- package/integrations/hubspot/schemas/list_pipelines.json +13 -0
- package/integrations/hubspot/schemas/list_properties.json +17 -0
- package/integrations/hubspot/schemas/remove_association.json +24 -0
- package/integrations/hubspot/schemas/search_companies.json +56 -0
- package/integrations/hubspot/schemas/search_contacts.json +56 -0
- package/integrations/hubspot/schemas/search_deals.json +43 -0
- package/integrations/hubspot/schemas/search_notes.json +43 -0
- package/integrations/hubspot/schemas/search_tasks.json +43 -0
- package/integrations/hubspot/schemas/search_tickets.json +43 -0
- package/integrations/hubspot/schemas/update_company.json +20 -0
- package/integrations/hubspot/schemas/update_contact.json +21 -0
- package/integrations/hubspot/schemas/update_deal.json +19 -0
- package/integrations/hubspot/schemas/update_task.json +31 -0
- package/integrations/hubspot/schemas/update_ticket.json +18 -0
- package/integrations/jira/.env.test +46 -0
- package/integrations/jira/.env.test.example +41 -0
- package/integrations/jira/README.md +46 -0
- package/integrations/jira/__tests__/get_handlers.test.ts +193 -0
- package/integrations/jira/__tests__/usage_parity.test.ts +14 -0
- package/integrations/jira/__tests__/write_handlers.test.ts +157 -0
- package/integrations/jira/credentials.json +39 -0
- package/integrations/jira/credentials_hint.md +4 -0
- package/integrations/jira/credentials_hint_api_token.md +6 -0
- package/integrations/jira/credentials_hint_oauth_token.md +6 -0
- package/integrations/jira/handlers/add_comment.js +9 -0
- package/integrations/jira/handlers/assign_issue.js +11 -0
- package/integrations/jira/handlers/create_issue.js +37 -0
- package/integrations/jira/handlers/create_sprint.js +19 -0
- package/integrations/jira/handlers/delete_issue.js +10 -0
- package/integrations/jira/handlers/get_backlog_issues.js +13 -0
- package/integrations/jira/handlers/get_board.js +6 -0
- package/integrations/jira/handlers/get_issue.js +63 -0
- package/integrations/jira/handlers/get_issue_comments.js +31 -0
- package/integrations/jira/handlers/get_myself.js +14 -0
- package/integrations/jira/handlers/get_project.js +28 -0
- package/integrations/jira/handlers/get_sprint.js +5 -0
- package/integrations/jira/handlers/get_sprint_issues.js +13 -0
- package/integrations/jira/handlers/get_transitions.js +23 -0
- package/integrations/jira/handlers/list_boards.js +34 -0
- package/integrations/jira/handlers/list_projects.js +29 -0
- package/integrations/jira/handlers/list_sprints.js +29 -0
- package/integrations/jira/handlers/move_issues_to_sprint.js +11 -0
- package/integrations/jira/handlers/search_issues.js +43 -0
- package/integrations/jira/handlers/search_users.js +21 -0
- package/integrations/jira/handlers/transition_issue.js +44 -0
- package/integrations/jira/handlers/update_issue.js +40 -0
- package/integrations/jira/handlers/update_sprint.js +20 -0
- package/integrations/jira/manifest.json +204 -0
- package/integrations/jira/prompt.md +80 -0
- package/integrations/jira/schemas/add_comment.json +16 -0
- package/integrations/jira/schemas/assign_issue.json +16 -0
- package/integrations/jira/schemas/create_issue.json +49 -0
- package/integrations/jira/schemas/create_sprint.json +29 -0
- package/integrations/jira/schemas/delete_issue.json +12 -0
- package/integrations/jira/schemas/empty.json +6 -0
- package/integrations/jira/schemas/get_backlog_issues.json +33 -0
- package/integrations/jira/schemas/get_board.json +13 -0
- package/integrations/jira/schemas/get_issue.json +23 -0
- package/integrations/jira/schemas/get_issue_comments.json +23 -0
- package/integrations/jira/schemas/get_project.json +17 -0
- package/integrations/jira/schemas/get_sprint.json +13 -0
- package/integrations/jira/schemas/get_sprint_issues.json +33 -0
- package/integrations/jira/schemas/get_transitions.json +12 -0
- package/integrations/jira/schemas/list_boards.json +27 -0
- package/integrations/jira/schemas/list_projects.json +22 -0
- package/integrations/jira/schemas/list_sprints.json +29 -0
- package/integrations/jira/schemas/move_issues_to_sprint.json +19 -0
- package/integrations/jira/schemas/search_issues.json +28 -0
- package/integrations/jira/schemas/search_users.json +18 -0
- package/integrations/jira/schemas/transition_issue.json +38 -0
- package/integrations/jira/schemas/update_issue.json +47 -0
- package/integrations/jira/schemas/update_sprint.json +33 -0
- package/integrations/new_integration_prompt.md +173 -2
- package/integrations/notion/.env.test +10 -0
- package/integrations/notion/.env.test.example +13 -0
- package/integrations/notion/README.md +42 -0
- package/integrations/notion/manifest.json +64 -35
- package/integrations/trello/.env.test +6 -0
- package/integrations/trello/.env.test.example +9 -0
- package/integrations/trello/README.md +50 -0
- package/package.json +7 -3
|
@@ -3,19 +3,19 @@ import { createCredentialStore, createIntegrationNode, createProxy, createToolbo
|
|
|
3
3
|
|
|
4
4
|
// LIVE GitHub read tests -- runs once per available credential variant.
|
|
5
5
|
// Required env vars (at least one):
|
|
6
|
-
// -
|
|
7
|
-
// -
|
|
6
|
+
// - _GITHUB_CLASSIC_PAT
|
|
7
|
+
// - _GITHUB_FINE_GRAINED_PAT
|
|
8
8
|
|
|
9
9
|
const env = process.env as Record<string, string | undefined>
|
|
10
10
|
|
|
11
11
|
interface VariantConfig {
|
|
12
|
-
key: string
|
|
12
|
+
key: string
|
|
13
13
|
token: string
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const variants: VariantConfig[] = [
|
|
17
|
-
{ key: 'classic_pat', token: env.
|
|
18
|
-
{ key: 'fine_grained_pat', token: env.
|
|
17
|
+
{ key: 'classic_pat', token: env._GITHUB_CLASSIC_PAT || '' },
|
|
18
|
+
{ key: 'fine_grained_pat', token: env._GITHUB_FINE_GRAINED_PAT || '' },
|
|
19
19
|
].filter(v => v.token.trim().length > 0)
|
|
20
20
|
|
|
21
21
|
const suiteOrSkip = variants.length > 0 ? describe : describe.skip
|
|
@@ -3,11 +3,11 @@ import { createCredentialStore, createIntegrationNode, createProxy, createToolbo
|
|
|
3
3
|
|
|
4
4
|
// LIVE GitHub write tests -- runs once per available credential variant.
|
|
5
5
|
// Required env vars (at least one):
|
|
6
|
-
// -
|
|
7
|
-
// -
|
|
6
|
+
// - _GITHUB_CLASSIC_PAT (tests all write tools including create_repo/delete_repo)
|
|
7
|
+
// - _GITHUB_FINE_GRAINED_PAT (tests write tools; create_repo/delete_repo are excluded for this variant)
|
|
8
8
|
// Plus:
|
|
9
|
-
// -
|
|
10
|
-
// -
|
|
9
|
+
// - _GITHUB_TEST_OWNER
|
|
10
|
+
// - _GITHUB_TEST_REPO
|
|
11
11
|
|
|
12
12
|
const env = process.env as Record<string, string | undefined>
|
|
13
13
|
|
|
@@ -17,11 +17,11 @@ interface VariantConfig {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
const variants: VariantConfig[] = [
|
|
20
|
-
{ key: 'classic_pat', token: env.
|
|
21
|
-
{ key: 'fine_grained_pat', token: env.
|
|
20
|
+
{ key: 'classic_pat', token: env._GITHUB_CLASSIC_PAT || '' },
|
|
21
|
+
{ key: 'fine_grained_pat', token: env._GITHUB_FINE_GRAINED_PAT || '' },
|
|
22
22
|
].filter(v => v.token.trim().length > 0)
|
|
23
23
|
|
|
24
|
-
const hasWriteEnv = hasEnv('
|
|
24
|
+
const hasWriteEnv = hasEnv('_GITHUB_TEST_OWNER', '_GITHUB_TEST_REPO')
|
|
25
25
|
const suiteOrSkip = (variants.length > 0 && hasWriteEnv) ? describe : describe.skip
|
|
26
26
|
|
|
27
27
|
async function withRetry<T>(
|
|
@@ -49,8 +49,8 @@ suiteOrSkip('github write handlers (live)', () => {
|
|
|
49
49
|
for (const variant of variants) {
|
|
50
50
|
describe(`variant: ${variant.key}`, () => {
|
|
51
51
|
const ctx = {
|
|
52
|
-
owner: env.
|
|
53
|
-
repo: env.
|
|
52
|
+
owner: env._GITHUB_TEST_OWNER,
|
|
53
|
+
repo: env._GITHUB_TEST_REPO,
|
|
54
54
|
}
|
|
55
55
|
let toolbox: ReturnType<typeof createToolbox>
|
|
56
56
|
|
|
@@ -158,97 +158,188 @@ suiteOrSkip('github write handlers (live)', () => {
|
|
|
158
158
|
expect(deleted?.status).toBe(204)
|
|
159
159
|
}, 90000)
|
|
160
160
|
|
|
161
|
-
it('
|
|
161
|
+
it('create_file: create, overwrite, and verify', async () => {
|
|
162
162
|
if (!ctx.owner || !ctx.repo)
|
|
163
163
|
return expect(true).toBe(true)
|
|
164
164
|
|
|
165
165
|
const timestamp = Date.now()
|
|
166
|
-
const branchName = `test-
|
|
166
|
+
const branchName = `test-create-file-${timestamp}`
|
|
167
|
+
const filePath = `test-create-${timestamp}.txt`
|
|
167
168
|
|
|
168
169
|
const create_branch = toolbox.write('create_branch')
|
|
169
170
|
const branch = await create_branch({ owner: ctx.owner, repo: ctx.repo, branch: branchName })
|
|
170
171
|
expect(branch?.ref).toBe(`refs/heads/${branchName}`)
|
|
171
172
|
|
|
172
|
-
const
|
|
173
|
-
const
|
|
173
|
+
const create_file = toolbox.write('create_file')
|
|
174
|
+
const created = await create_file({
|
|
174
175
|
owner: ctx.owner,
|
|
175
176
|
repo: ctx.repo,
|
|
176
|
-
path: `test-single-${timestamp}.txt`,
|
|
177
|
-
message: `Add single test file ${timestamp}`,
|
|
178
|
-
content: `Test content with UTF-8: Hello 世界 🌍\nCreated at ${timestamp}`,
|
|
179
177
|
branch: branchName,
|
|
178
|
+
path: filePath,
|
|
179
|
+
content: `Test content with UTF-8: Hello 世界 🌍\nCreated at ${timestamp}`,
|
|
180
|
+
message: `Add test file ${timestamp}`,
|
|
180
181
|
})
|
|
181
|
-
expect(
|
|
182
|
-
expect(file?.
|
|
182
|
+
expect(created?.commit?.sha).toBeTruthy()
|
|
183
|
+
expect(created?.file?.path).toBe(filePath)
|
|
184
|
+
expect(created?.file?.action).toBe('created')
|
|
185
|
+
|
|
186
|
+
// Overwrite the same file
|
|
187
|
+
const overwritten = await withRetry(
|
|
188
|
+
() => create_file({
|
|
189
|
+
owner: ctx.owner,
|
|
190
|
+
repo: ctx.repo,
|
|
191
|
+
branch: branchName,
|
|
192
|
+
path: filePath,
|
|
193
|
+
content: `Overwritten content at ${timestamp}`,
|
|
194
|
+
message: `Overwrite test file ${timestamp}`,
|
|
195
|
+
}),
|
|
196
|
+
{ maxAttempts: 3, delayMs: 2000 },
|
|
197
|
+
)
|
|
198
|
+
expect(overwritten?.commit?.sha).toBeTruthy()
|
|
199
|
+
expect(overwritten?.file?.action).toBe('overwritten')
|
|
183
200
|
|
|
184
|
-
//
|
|
201
|
+
// Verify content -- retry until the Contents API reflects the overwrite
|
|
185
202
|
const get_file_contents = toolbox.read('get_file_contents')
|
|
186
|
-
const contents = await
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
203
|
+
const contents = await withRetry(
|
|
204
|
+
async () => {
|
|
205
|
+
const c = await get_file_contents({
|
|
206
|
+
owner: ctx.owner,
|
|
207
|
+
repo: ctx.repo,
|
|
208
|
+
path: filePath,
|
|
209
|
+
ref: branchName,
|
|
210
|
+
})
|
|
211
|
+
if (!c?.content?.includes('Overwritten content'))
|
|
212
|
+
throw new Error('stale content: overwrite not yet visible')
|
|
213
|
+
return c
|
|
214
|
+
},
|
|
215
|
+
{ maxAttempts: 5, delayMs: 1500 },
|
|
216
|
+
)
|
|
217
|
+
expect(contents?.content).toContain('Overwritten content')
|
|
195
218
|
|
|
219
|
+
// delete_file without SHA (auto-fetches it)
|
|
196
220
|
const delete_file = toolbox.write('delete_file')
|
|
197
221
|
const deleted = await delete_file({
|
|
198
222
|
owner: ctx.owner,
|
|
199
223
|
repo: ctx.repo,
|
|
200
|
-
path:
|
|
224
|
+
path: filePath,
|
|
201
225
|
message: `Delete test file ${timestamp}`,
|
|
202
|
-
sha: fileSha,
|
|
203
226
|
branch: branchName,
|
|
204
227
|
})
|
|
205
228
|
expect(deleted?.commit?.message).toBe(`Delete test file ${timestamp}`)
|
|
206
229
|
|
|
207
|
-
// delete_branch cleanup
|
|
208
230
|
const delete_branch = toolbox.write('delete_branch')
|
|
209
|
-
|
|
210
|
-
|
|
231
|
+
await delete_branch({ owner: ctx.owner, repo: ctx.repo, branch: branchName })
|
|
232
|
+
}, 120000)
|
|
233
|
+
|
|
234
|
+
it('edit_file: search/replace on a single file', async () => {
|
|
235
|
+
if (!ctx.owner || !ctx.repo)
|
|
236
|
+
return expect(true).toBe(true)
|
|
237
|
+
|
|
238
|
+
const timestamp = Date.now()
|
|
239
|
+
const branchName = `test-edit-file-${timestamp}`
|
|
240
|
+
const filePath = `test-edit-${timestamp}.txt`
|
|
241
|
+
|
|
242
|
+
const create_branch = toolbox.write('create_branch')
|
|
243
|
+
await create_branch({ owner: ctx.owner, repo: ctx.repo, branch: branchName })
|
|
244
|
+
|
|
245
|
+
// Seed a file to edit
|
|
246
|
+
const create_file = toolbox.write('create_file')
|
|
247
|
+
await create_file({
|
|
248
|
+
owner: ctx.owner,
|
|
249
|
+
repo: ctx.repo,
|
|
250
|
+
branch: branchName,
|
|
251
|
+
path: filePath,
|
|
252
|
+
content: 'line 1: hello world\nline 2: foo bar\nline 3: goodbye world\n',
|
|
253
|
+
message: `Seed file for edit test ${timestamp}`,
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
// Apply search/replace edits
|
|
257
|
+
const edit_file = toolbox.write('edit_file')
|
|
258
|
+
const edited = await withRetry(
|
|
259
|
+
() => edit_file({
|
|
260
|
+
owner: ctx.owner,
|
|
261
|
+
repo: ctx.repo,
|
|
262
|
+
branch: branchName,
|
|
263
|
+
path: filePath,
|
|
264
|
+
edits: [
|
|
265
|
+
{ old_text: 'hello world', new_text: 'hello universe' },
|
|
266
|
+
{ old_text: 'foo bar', new_text: 'baz qux' },
|
|
267
|
+
],
|
|
268
|
+
message: `Edit file ${timestamp}`,
|
|
269
|
+
}),
|
|
270
|
+
{ maxAttempts: 3, delayMs: 2000 },
|
|
271
|
+
)
|
|
272
|
+
expect(edited?.commit?.sha).toBeTruthy()
|
|
273
|
+
expect(edited?.file?.path).toBe(filePath)
|
|
274
|
+
|
|
275
|
+
// Verify edits applied -- retry until the Contents API reflects the new commit
|
|
276
|
+
const get_file_contents = toolbox.read('get_file_contents')
|
|
277
|
+
const contents = await withRetry(
|
|
278
|
+
async () => {
|
|
279
|
+
const c = await get_file_contents({
|
|
280
|
+
owner: ctx.owner,
|
|
281
|
+
repo: ctx.repo,
|
|
282
|
+
path: filePath,
|
|
283
|
+
ref: branchName,
|
|
284
|
+
})
|
|
285
|
+
if (!c?.content?.includes('hello universe'))
|
|
286
|
+
throw new Error('stale content: edit not yet visible')
|
|
287
|
+
return c
|
|
288
|
+
},
|
|
289
|
+
{ maxAttempts: 5, delayMs: 1500 },
|
|
290
|
+
)
|
|
291
|
+
expect(contents?.content).toContain('hello universe')
|
|
292
|
+
expect(contents?.content).toContain('baz qux')
|
|
293
|
+
expect(contents?.content).toContain('goodbye world')
|
|
294
|
+
expect(contents?.content).not.toContain('hello world')
|
|
295
|
+
|
|
296
|
+
const delete_branch = toolbox.write('delete_branch')
|
|
297
|
+
await delete_branch({ owner: ctx.owner, repo: ctx.repo, branch: branchName })
|
|
211
298
|
}, 120000)
|
|
212
299
|
|
|
213
|
-
it('
|
|
300
|
+
it('edit_files: mixed create, edit, delete in one atomic commit', async () => {
|
|
214
301
|
if (!ctx.owner || !ctx.repo)
|
|
215
302
|
return expect(true).toBe(true)
|
|
216
303
|
|
|
217
304
|
const timestamp = Date.now()
|
|
218
|
-
const branchName = `test-
|
|
305
|
+
const branchName = `test-edit-files-${timestamp}`
|
|
219
306
|
|
|
220
307
|
const create_branch = toolbox.write('create_branch')
|
|
221
308
|
const branch = await create_branch({ owner: ctx.owner, repo: ctx.repo, branch: branchName })
|
|
222
309
|
expect(branch?.ref).toBe(`refs/heads/${branchName}`)
|
|
223
310
|
|
|
224
|
-
|
|
225
|
-
const
|
|
311
|
+
// Seed initial files via edit_files (create action)
|
|
312
|
+
const edit_files = toolbox.write('edit_files')
|
|
313
|
+
const commit1 = await edit_files({
|
|
226
314
|
owner: ctx.owner,
|
|
227
315
|
repo: ctx.repo,
|
|
228
316
|
branch: branchName,
|
|
229
|
-
message: `Add
|
|
317
|
+
message: `Add initial files ${timestamp}`,
|
|
230
318
|
files: [
|
|
231
|
-
{ path: `multi-test/file1-${timestamp}.txt`, content: 'Content of file 1' },
|
|
232
|
-
{ path: `multi-test/file2-${timestamp}.txt`, content: 'Content of file 2' },
|
|
233
|
-
{ path: `multi-test/file3-${timestamp}.md`, content: '# Test File 3\n\nWith UTF-8: 你好 🚀' },
|
|
319
|
+
{ path: `multi-test/file1-${timestamp}.txt`, action: 'create', content: 'Content of file 1' },
|
|
320
|
+
{ path: `multi-test/file2-${timestamp}.txt`, action: 'create', content: 'Content of file 2' },
|
|
321
|
+
{ path: `multi-test/file3-${timestamp}.md`, action: 'create', content: '# Test File 3\n\nWith UTF-8: 你好 🚀' },
|
|
234
322
|
],
|
|
235
323
|
})
|
|
236
|
-
expect(
|
|
237
|
-
expect(
|
|
238
|
-
expect(
|
|
324
|
+
expect(commit1?.commit?.sha).toBeTruthy()
|
|
325
|
+
expect(commit1?.commit?.message).toBe(`Add initial files ${timestamp}`)
|
|
326
|
+
expect(commit1?.files?.length).toBe(3)
|
|
239
327
|
|
|
240
|
-
//
|
|
241
|
-
// after the first commit before accepting a follow-up on the same branch.
|
|
328
|
+
// Mixed operations: edit file1, delete file2, create file4
|
|
242
329
|
const commit2 = await withRetry(
|
|
243
|
-
() =>
|
|
330
|
+
() => edit_files({
|
|
244
331
|
owner: ctx.owner,
|
|
245
332
|
repo: ctx.repo,
|
|
246
333
|
branch: branchName,
|
|
247
|
-
message: `
|
|
334
|
+
message: `Mixed edit/delete/create ${timestamp}`,
|
|
248
335
|
files: [
|
|
249
|
-
{
|
|
250
|
-
|
|
251
|
-
|
|
336
|
+
{
|
|
337
|
+
path: `multi-test/file1-${timestamp}.txt`,
|
|
338
|
+
action: 'edit',
|
|
339
|
+
edits: [{ old_text: 'Content of file 1', new_text: 'Updated content of file 1' }],
|
|
340
|
+
},
|
|
341
|
+
{ path: `multi-test/file2-${timestamp}.txt`, action: 'delete' },
|
|
342
|
+
{ path: `multi-test/file4-${timestamp}.txt`, action: 'create', content: 'New file 4' },
|
|
252
343
|
],
|
|
253
344
|
}),
|
|
254
345
|
{
|
|
@@ -258,15 +349,42 @@ suiteOrSkip('github write handlers (live)', () => {
|
|
|
258
349
|
},
|
|
259
350
|
)
|
|
260
351
|
expect(commit2?.commit?.sha).toBeTruthy()
|
|
261
|
-
expect(commit2?.
|
|
352
|
+
expect(commit2?.files?.length).toBe(3)
|
|
353
|
+
|
|
354
|
+
// Verify the edit applied -- retry until the Contents API reflects the new commit
|
|
355
|
+
const get_file_contents = toolbox.read('get_file_contents')
|
|
356
|
+
const f1 = await withRetry(
|
|
357
|
+
async () => {
|
|
358
|
+
const c = await get_file_contents({
|
|
359
|
+
owner: ctx.owner,
|
|
360
|
+
repo: ctx.repo,
|
|
361
|
+
path: `multi-test/file1-${timestamp}.txt`,
|
|
362
|
+
ref: branchName,
|
|
363
|
+
})
|
|
364
|
+
if (!c?.content?.includes('Updated content of file 1'))
|
|
365
|
+
throw new Error('stale content: edit not yet visible')
|
|
366
|
+
return c
|
|
367
|
+
},
|
|
368
|
+
{ maxAttempts: 5, delayMs: 1500 },
|
|
369
|
+
)
|
|
370
|
+
expect(f1?.content).toContain('Updated content of file 1')
|
|
371
|
+
|
|
372
|
+
// Verify the new file was created
|
|
373
|
+
const f4 = await get_file_contents({
|
|
374
|
+
owner: ctx.owner,
|
|
375
|
+
repo: ctx.repo,
|
|
376
|
+
path: `multi-test/file4-${timestamp}.txt`,
|
|
377
|
+
ref: branchName,
|
|
378
|
+
})
|
|
379
|
+
expect(f4?.content).toContain('New file 4')
|
|
262
380
|
|
|
263
381
|
// get_commit verifies the commit details
|
|
264
382
|
const get_commit = toolbox.read('get_commit')
|
|
265
383
|
const commitDetails = await get_commit({ owner: ctx.owner, repo: ctx.repo, sha: commit2.commit.sha })
|
|
266
384
|
expect(commitDetails?.sha).toBe(commit2.commit.sha)
|
|
267
|
-
},
|
|
385
|
+
}, 150000)
|
|
268
386
|
|
|
269
|
-
it('full PR workflow: create_branch ->
|
|
387
|
+
it('full PR workflow: create_branch -> edit_files -> create_pull_request -> update_pull_request -> create_pull_request_review -> merge_pull_request -> delete_branch', async () => {
|
|
270
388
|
if (!ctx.owner || !ctx.repo)
|
|
271
389
|
return expect(true).toBe(true)
|
|
272
390
|
|
|
@@ -277,15 +395,15 @@ suiteOrSkip('github write handlers (live)', () => {
|
|
|
277
395
|
const branch = await create_branch({ owner: ctx.owner, repo: ctx.repo, branch: branchName })
|
|
278
396
|
expect(branch?.ref).toBe(`refs/heads/${branchName}`)
|
|
279
397
|
|
|
280
|
-
const
|
|
281
|
-
const commit = await
|
|
398
|
+
const edit_files = toolbox.write('edit_files')
|
|
399
|
+
const commit = await edit_files({
|
|
282
400
|
owner: ctx.owner,
|
|
283
401
|
repo: ctx.repo,
|
|
284
402
|
branch: branchName,
|
|
285
403
|
message: `Add feature files ${timestamp}`,
|
|
286
404
|
files: [
|
|
287
|
-
{ path: `feature-${timestamp}/index.js`, content: 'export default function() { return "Hello"; }' },
|
|
288
|
-
{ path: `feature-${timestamp}/README.md`, content: `# Feature ${timestamp}\n\nThis is a test feature.` },
|
|
405
|
+
{ path: `feature-${timestamp}/index.js`, action: 'create', content: 'export default function() { return "Hello"; }' },
|
|
406
|
+
{ path: `feature-${timestamp}/README.md`, action: 'create', content: `# Feature ${timestamp}\n\nThis is a test feature.` },
|
|
289
407
|
],
|
|
290
408
|
})
|
|
291
409
|
expect(commit?.commit?.sha).toBeTruthy()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
async (input) => {
|
|
2
|
+
const { owner, repo, branch, path, content, message } = input
|
|
3
|
+
|
|
4
|
+
// Check if the file already exists to get its SHA for overwrite
|
|
5
|
+
let existingSha
|
|
6
|
+
try {
|
|
7
|
+
const params = new URLSearchParams()
|
|
8
|
+
params.set('ref', branch)
|
|
9
|
+
const checkRes = await integration.fetch(`/repos/${owner}/${repo}/contents/${path}?${params.toString()}`)
|
|
10
|
+
const checkData = await checkRes.json()
|
|
11
|
+
if (checkData && checkData.sha) {
|
|
12
|
+
existingSha = checkData.sha
|
|
13
|
+
}
|
|
14
|
+
} catch (e) {
|
|
15
|
+
// 404 means file doesn't exist yet -- that's fine
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const contentBase64 = btoa(unescape(encodeURIComponent(content)))
|
|
19
|
+
|
|
20
|
+
const body = {
|
|
21
|
+
message: message,
|
|
22
|
+
content: contentBase64,
|
|
23
|
+
branch: branch,
|
|
24
|
+
}
|
|
25
|
+
if (existingSha) {
|
|
26
|
+
body.sha = existingSha
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const res = await integration.fetch(`/repos/${owner}/${repo}/contents/${path}`, {
|
|
30
|
+
method: 'PUT',
|
|
31
|
+
body: body,
|
|
32
|
+
})
|
|
33
|
+
const result = await res.json()
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
commit: {
|
|
37
|
+
sha: result.commit?.sha,
|
|
38
|
+
message: result.commit?.message,
|
|
39
|
+
url: result.commit?.html_url,
|
|
40
|
+
},
|
|
41
|
+
file: {
|
|
42
|
+
path: path,
|
|
43
|
+
action: existingSha ? 'overwritten' : 'created',
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
async (input) => {
|
|
2
|
-
|
|
2
|
+
let sha = input.sha
|
|
3
|
+
if (!sha) {
|
|
4
|
+
const params = new URLSearchParams()
|
|
5
|
+
if (input.branch) params.set('ref', input.branch)
|
|
6
|
+
const query = params.toString() ? `?${params.toString()}` : ''
|
|
7
|
+
const fileRes = await integration.fetch(`/repos/${input.owner}/${input.repo}/contents/${input.path}${query}`)
|
|
8
|
+
const fileData = await fileRes.json()
|
|
9
|
+
if (!fileData || !fileData.sha) {
|
|
10
|
+
throw new Error(`File not found: ${input.path}. Cannot delete a file that does not exist.`)
|
|
11
|
+
}
|
|
12
|
+
sha = fileData.sha
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const body = { message: input.message, sha: sha }
|
|
3
16
|
if (input.branch) body.branch = input.branch
|
|
4
17
|
const res = await integration.fetch(
|
|
5
18
|
`/repos/${input.owner}/${input.repo}/contents/${input.path}`,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
async (input) => {
|
|
2
|
+
const { owner, repo, branch, path, edits, message } = input
|
|
3
|
+
|
|
4
|
+
const params = new URLSearchParams()
|
|
5
|
+
params.set('ref', branch)
|
|
6
|
+
const fileRes = await integration.fetch(`/repos/${owner}/${repo}/contents/${path}?${params.toString()}`)
|
|
7
|
+
const fileData = await fileRes.json()
|
|
8
|
+
|
|
9
|
+
if (!fileData || !fileData.content || !fileData.sha) {
|
|
10
|
+
throw new Error(`File not found: ${path}. Use get_repo_tree to discover file paths.`)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const b64 = fileData.content.replace(/\n/g, '')
|
|
14
|
+
let content = decodeURIComponent(
|
|
15
|
+
atob(b64).split('').map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join('')
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < edits.length; i++) {
|
|
19
|
+
const { old_text, new_text } = edits[i]
|
|
20
|
+
const idx = content.indexOf(old_text)
|
|
21
|
+
if (idx === -1) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`Edit ${i + 1}/${edits.length} failed: old_text not found in ${path}. `
|
|
24
|
+
+ 'Ensure the search text matches the file exactly, including whitespace and indentation. '
|
|
25
|
+
+ 'Use get_file_contents to verify the current content.'
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
content = content.substring(0, idx) + new_text + content.substring(idx + old_text.length)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const contentBase64 = btoa(unescape(encodeURIComponent(content)))
|
|
32
|
+
|
|
33
|
+
const res = await integration.fetch(`/repos/${owner}/${repo}/contents/${path}`, {
|
|
34
|
+
method: 'PUT',
|
|
35
|
+
body: {
|
|
36
|
+
message: message,
|
|
37
|
+
content: contentBase64,
|
|
38
|
+
sha: fileData.sha,
|
|
39
|
+
branch: branch,
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
const result = await res.json()
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
commit: {
|
|
46
|
+
sha: result.commit?.sha,
|
|
47
|
+
message: result.commit?.message,
|
|
48
|
+
url: result.commit?.html_url,
|
|
49
|
+
},
|
|
50
|
+
file: { path: path },
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
async (input) => {
|
|
2
|
+
const { owner, repo, branch, message, files } = input
|
|
3
|
+
|
|
4
|
+
// Helper: decode base64 GitHub content to UTF-8
|
|
5
|
+
function decodeContent(b64Raw) {
|
|
6
|
+
const b64 = b64Raw.replace(/\n/g, '')
|
|
7
|
+
return decodeURIComponent(
|
|
8
|
+
atob(b64).split('').map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join('')
|
|
9
|
+
)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Helper: apply search/replace edits to text
|
|
13
|
+
function applyEdits(text, edits, filePath) {
|
|
14
|
+
let result = text
|
|
15
|
+
for (let i = 0; i < edits.length; i++) {
|
|
16
|
+
const { old_text, new_text } = edits[i]
|
|
17
|
+
const idx = result.indexOf(old_text)
|
|
18
|
+
if (idx === -1) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
`Edit ${i + 1}/${edits.length} failed on ${filePath}: old_text not found. `
|
|
21
|
+
+ 'Ensure the search text matches exactly, including whitespace and indentation. '
|
|
22
|
+
+ 'Use get_file_contents to verify the current content.'
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
result = result.substring(0, idx) + new_text + result.substring(idx + old_text.length)
|
|
26
|
+
}
|
|
27
|
+
return result
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 1. Get the current commit SHA for the branch
|
|
31
|
+
const refRes = await integration.fetch(`/repos/${owner}/${repo}/git/refs/heads/${branch}`)
|
|
32
|
+
const refData = await refRes.json()
|
|
33
|
+
const currentCommitSha = refData.object.sha
|
|
34
|
+
|
|
35
|
+
// 2. Get the current commit to find its tree
|
|
36
|
+
const commitRes = await integration.fetch(`/repos/${owner}/${repo}/git/commits/${currentCommitSha}`)
|
|
37
|
+
const commitData = await commitRes.json()
|
|
38
|
+
const currentTreeSha = commitData.tree.sha
|
|
39
|
+
|
|
40
|
+
// 3. Resolve final content for each file
|
|
41
|
+
const tree = []
|
|
42
|
+
for (const file of files) {
|
|
43
|
+
if (file.action === 'delete') {
|
|
44
|
+
tree.push({ path: file.path, mode: '100644', type: 'blob', sha: null })
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let finalContent
|
|
49
|
+
if (file.action === 'edit') {
|
|
50
|
+
if (!file.edits || file.edits.length === 0) {
|
|
51
|
+
throw new Error(`File ${file.path} has action 'edit' but no edits provided.`)
|
|
52
|
+
}
|
|
53
|
+
const params = new URLSearchParams()
|
|
54
|
+
params.set('ref', branch)
|
|
55
|
+
const fileRes = await integration.fetch(`/repos/${owner}/${repo}/contents/${file.path}?${params.toString()}`)
|
|
56
|
+
const fileData = await fileRes.json()
|
|
57
|
+
if (!fileData || !fileData.content) {
|
|
58
|
+
throw new Error(`File not found: ${file.path}. Use get_repo_tree to discover file paths.`)
|
|
59
|
+
}
|
|
60
|
+
const currentContent = decodeContent(fileData.content)
|
|
61
|
+
finalContent = applyEdits(currentContent, file.edits, file.path)
|
|
62
|
+
} else {
|
|
63
|
+
// action === 'create'
|
|
64
|
+
if (file.content === undefined || file.content === null) {
|
|
65
|
+
throw new Error(`File ${file.path} has action 'create' but no content provided.`)
|
|
66
|
+
}
|
|
67
|
+
finalContent = file.content
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Create blob
|
|
71
|
+
const blobRes = await integration.fetch(`/repos/${owner}/${repo}/git/blobs`, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
body: { content: finalContent, encoding: 'utf-8' },
|
|
74
|
+
})
|
|
75
|
+
const blobData = await blobRes.json()
|
|
76
|
+
tree.push({ path: file.path, mode: '100644', type: 'blob', sha: blobData.sha })
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 4. Create a new tree
|
|
80
|
+
const treeRes = await integration.fetch(`/repos/${owner}/${repo}/git/trees`, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
body: { base_tree: currentTreeSha, tree: tree },
|
|
83
|
+
})
|
|
84
|
+
const treeData = await treeRes.json()
|
|
85
|
+
|
|
86
|
+
// 5. Create the commit
|
|
87
|
+
const newCommitRes = await integration.fetch(`/repos/${owner}/${repo}/git/commits`, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
body: { message: message, tree: treeData.sha, parents: [currentCommitSha] },
|
|
90
|
+
})
|
|
91
|
+
const newCommitData = await newCommitRes.json()
|
|
92
|
+
|
|
93
|
+
// 6. Update the branch reference
|
|
94
|
+
await integration.fetch(`/repos/${owner}/${repo}/git/refs/heads/${branch}`, {
|
|
95
|
+
method: 'PATCH',
|
|
96
|
+
body: { sha: newCommitData.sha },
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
commit: {
|
|
101
|
+
sha: newCommitData.sha,
|
|
102
|
+
message: newCommitData.message,
|
|
103
|
+
url: newCommitData.html_url,
|
|
104
|
+
},
|
|
105
|
+
files: files.map(f => ({ path: f.path, action: f.action })),
|
|
106
|
+
}
|
|
107
|
+
}
|