@loopstack/github-oauth-example 0.3.1 → 0.4.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.
@@ -1,18 +1,16 @@
1
+ import { Inject } from '@nestjs/common';
1
2
  import { z } from 'zod';
2
3
  import {
3
4
  BaseWorkflow,
4
5
  CallbackSchema,
5
- Final,
6
6
  Guard,
7
- Initial,
8
- InjectTool,
9
- InjectWorkflow,
10
7
  LinkDocument,
11
8
  MarkdownDocument,
12
- ToolResult,
9
+ TEMPLATE_RENDERER,
13
10
  Transition,
14
11
  Workflow,
15
12
  } from '@loopstack/common';
13
+ import type { LoopstackContext, TemplateRenderFn } from '@loopstack/common';
16
14
  import {
17
15
  GitHubGetAuthenticatedUserTool,
18
16
  GitHubGetRepoTool,
@@ -26,17 +24,13 @@ import {
26
24
  } from '@loopstack/github-module';
27
25
  import { OAuthWorkflow } from '@loopstack/oauth-module';
28
26
 
29
- interface GitHubUserResult {
30
- error?: string;
27
+ interface GitHubReposOverviewState {
28
+ owner: string;
29
+ repo: string;
30
+ requiresAuthentication?: boolean;
31
31
  user?: { login: string; name: string | null; htmlUrl: string; publicRepos: number };
32
- }
33
-
34
- interface GitHubOrgsResult {
35
- orgs: Array<{ login: string; description: string | null }>;
36
- }
37
-
38
- interface GitHubRepoResult {
39
- repo: {
32
+ orgs?: Array<{ login: string; description: string | null }>;
33
+ repoDetails?: {
40
34
  fullName: string;
41
35
  description: string | null;
42
36
  language: string | null;
@@ -46,18 +40,9 @@ interface GitHubRepoResult {
46
40
  defaultBranch: string;
47
41
  htmlUrl: string;
48
42
  };
49
- }
50
-
51
- interface GitHubBranchesResult {
52
- branches: Array<{ name: string; protected: boolean }>;
53
- }
54
-
55
- interface GitHubIssuesResult {
56
- issues: Array<{ number: number; title: string; state: string; user: string; htmlUrl: string }>;
57
- }
58
-
59
- interface GitHubPullRequestsResult {
60
- pullRequests: Array<{
43
+ branches?: Array<{ name: string; protected: boolean }>;
44
+ issues?: Array<{ number: number; title: string; state: string; user: string; htmlUrl: string }>;
45
+ pullRequests?: Array<{
61
46
  number: number;
62
47
  title: string;
63
48
  state: string;
@@ -65,30 +50,22 @@ interface GitHubPullRequestsResult {
65
50
  draft: boolean;
66
51
  htmlUrl: string;
67
52
  }>;
68
- }
69
-
70
- interface GitHubDirectoryResult {
71
- entries: Array<{ name: string; type: string; path: string }>;
72
- }
73
-
74
- interface GitHubWorkflowRunsResult {
75
- totalCount: number;
76
- runs: Array<{
53
+ directoryEntries?: Array<{ name: string; type: string; path: string }>;
54
+ workflowRuns?: Array<{
77
55
  id: number;
78
56
  name: string;
79
57
  status: string;
80
58
  conclusion: string | null;
81
59
  htmlUrl: string;
82
60
  }>;
83
- }
84
-
85
- interface GitHubSearchCodeResult {
86
- totalCount: number;
87
- results: Array<{ name: string; path: string; repository: string }>;
61
+ searchResults?: Array<{ name: string; path: string; repository: string }>;
88
62
  }
89
63
 
90
64
  @Workflow({
91
- uiConfig: __dirname + '/github-repos-overview.ui.yaml',
65
+ title: 'GitHub Repository Overview',
66
+ description:
67
+ 'Comprehensive GitHub example that exercises every GitHub tool.\nFetches user info, repository details, issues, pull requests, branches,\ndirectory contents, workflow runs, and search results for a given repository.\nIf not authenticated, launches the OAuth sub-workflow and retries.',
68
+ name: 'github_repos_overview',
92
69
  schema: z
93
70
  .object({
94
71
  owner: z.string().default('octocat'),
@@ -96,75 +73,51 @@ interface GitHubSearchCodeResult {
96
73
  })
97
74
  .strict(),
98
75
  })
99
- export class GitHubReposOverviewWorkflow extends BaseWorkflow<{ owner: string; repo: string }> {
100
- // GitHub tools
101
- @InjectTool() private gitHubGetAuthenticatedUser: GitHubGetAuthenticatedUserTool;
102
- @InjectTool() private gitHubListUserOrgs: GitHubListUserOrgsTool;
103
- @InjectTool() private gitHubGetRepo: GitHubGetRepoTool;
104
- @InjectTool() private gitHubListBranches: GitHubListBranchesTool;
105
- @InjectTool() private gitHubListIssues: GitHubListIssuesTool;
106
- @InjectTool() private gitHubListPullRequests: GitHubListPullRequestsTool;
107
- @InjectTool() private gitHubListDirectory: GitHubListDirectoryTool;
108
- @InjectTool() private gitHubListWorkflowRuns: GitHubListWorkflowRunsTool;
109
- @InjectTool() private gitHubSearchCode: GitHubSearchCodeTool;
110
-
111
- @InjectWorkflow() oAuth: OAuthWorkflow;
76
+ export class GitHubReposOverviewWorkflow extends BaseWorkflow<
77
+ { owner: string; repo: string },
78
+ GitHubReposOverviewState
79
+ > {
80
+ constructor(
81
+ private readonly gitHubGetAuthenticatedUser: GitHubGetAuthenticatedUserTool,
82
+ private readonly gitHubListUserOrgs: GitHubListUserOrgsTool,
83
+ private readonly gitHubGetRepo: GitHubGetRepoTool,
84
+ private readonly gitHubListBranches: GitHubListBranchesTool,
85
+ private readonly gitHubListIssues: GitHubListIssuesTool,
86
+ private readonly gitHubListPullRequests: GitHubListPullRequestsTool,
87
+ private readonly gitHubListDirectory: GitHubListDirectoryTool,
88
+ private readonly gitHubListWorkflowRuns: GitHubListWorkflowRunsTool,
89
+ private readonly gitHubSearchCode: GitHubSearchCodeTool,
90
+ private readonly oAuthWorkflow: OAuthWorkflow,
91
+ @Inject(TEMPLATE_RENDERER) private readonly render: TemplateRenderFn,
92
+ ) {
93
+ super();
94
+ }
112
95
 
113
- owner!: string;
114
- repo!: string;
115
- requiresAuthentication?: boolean;
116
- user?: { login: string; name: string | null; htmlUrl: string; publicRepos: number };
117
- orgs?: Array<{ login: string; description: string | null }>;
118
- repoDetails?: {
119
- fullName: string;
120
- description: string | null;
121
- language: string | null;
122
- stars: number;
123
- forks: number;
124
- openIssues: number;
125
- defaultBranch: string;
126
- htmlUrl: string;
127
- };
128
- branches?: Array<{ name: string; protected: boolean }>;
129
- issues?: Array<{ number: number; title: string; state: string; user: string; htmlUrl: string }>;
130
- pullRequests?: Array<{
131
- number: number;
132
- title: string;
133
- state: string;
134
- user: string;
135
- draft: boolean;
136
- htmlUrl: string;
137
- }>;
138
- directoryEntries?: Array<{ name: string; type: string; path: string }>;
139
- workflowRuns?: Array<{
140
- id: number;
141
- name: string;
142
- status: string;
143
- conclusion: string | null;
144
- htmlUrl: string;
145
- }>;
146
- searchResults?: Array<{ name: string; path: string; repository: string }>;
147
96
  // --- Step 1: Fetch authenticated user ---
148
97
 
149
- @Initial({ to: 'user_fetched' })
150
- async fetchUser(args: { owner: string; repo: string }) {
151
- this.owner = args.owner;
152
- this.repo = args.repo;
153
- const result: ToolResult<GitHubUserResult> = await this.gitHubGetAuthenticatedUser.call();
154
- this.requiresAuthentication = result.data!.error === 'unauthorized';
155
- this.user = result.data!.user;
98
+ @Transition({ to: 'user_fetched' })
99
+ async fetchUser(state: GitHubReposOverviewState, ctx: LoopstackContext): Promise<GitHubReposOverviewState> {
100
+ const args = ctx.args as { owner: string; repo: string };
101
+ const result = await this.gitHubGetAuthenticatedUser.call();
102
+ return {
103
+ ...state,
104
+ owner: args.owner,
105
+ repo: args.repo,
106
+ requiresAuthentication: result.data!.error === 'unauthorized',
107
+ user: result.data!.user,
108
+ };
156
109
  }
157
110
 
158
111
  // If unauthorized -> launch OAuth
159
112
  @Transition({ from: 'user_fetched', to: 'awaiting_auth', priority: 10 })
160
113
  @Guard('needsAuth')
161
- async authRequired() {
162
- const result = await this.oAuth.run(
114
+ async authRequired(state: GitHubReposOverviewState): Promise<GitHubReposOverviewState> {
115
+ const result = await this.oAuthWorkflow.run(
163
116
  { provider: 'github', scopes: ['repo', 'read:org', 'workflow'] },
164
- { alias: 'oAuth', callback: { transition: 'authCompleted' } },
117
+ { callback: { transition: 'authCompleted' } },
165
118
  );
166
119
 
167
- await this.repository.save(
120
+ await this.documentStore.save(
168
121
  LinkDocument,
169
122
  {
170
123
  label: 'GitHub authentication required',
@@ -174,10 +127,11 @@ export class GitHubReposOverviewWorkflow extends BaseWorkflow<{ owner: string; r
174
127
  },
175
128
  { id: `link_${result.workflowId}` },
176
129
  );
130
+ return state;
177
131
  }
178
132
 
179
- needsAuth(): boolean {
180
- return !!this.requiresAuthentication;
133
+ needsAuth(state: GitHubReposOverviewState): boolean {
134
+ return !!state.requiresAuthentication;
181
135
  }
182
136
 
183
137
  // Auth completed -> retry from start
@@ -187,8 +141,11 @@ export class GitHubReposOverviewWorkflow extends BaseWorkflow<{ owner: string; r
187
141
  wait: true,
188
142
  schema: CallbackSchema,
189
143
  })
190
- async authCompleted(payload: { workflowId: string }) {
191
- await this.repository.save(
144
+ async authCompleted(
145
+ state: GitHubReposOverviewState,
146
+ payload: { workflowId: string },
147
+ ): Promise<GitHubReposOverviewState> {
148
+ await this.documentStore.save(
192
149
  LinkDocument,
193
150
  {
194
151
  status: 'success',
@@ -199,99 +156,98 @@ export class GitHubReposOverviewWorkflow extends BaseWorkflow<{ owner: string; r
199
156
  },
200
157
  { id: `link_${payload.workflowId}` },
201
158
  );
159
+ return state;
202
160
  }
203
161
 
204
162
  // --- Step 2: Fetch user orgs ---
205
163
 
206
164
  @Transition({ from: 'user_fetched', to: 'orgs_fetched' })
207
- async fetchOrgs() {
208
- const result: ToolResult<GitHubOrgsResult> = await this.gitHubListUserOrgs.call({ perPage: 10 });
209
- this.orgs = result.data!.orgs;
165
+ async fetchOrgs(state: GitHubReposOverviewState): Promise<GitHubReposOverviewState> {
166
+ const result = await this.gitHubListUserOrgs.call({ perPage: 10 });
167
+ return { ...state, orgs: result.data!.orgs };
210
168
  }
211
169
 
212
170
  // --- Step 3: Fetch repo details and branches ---
213
171
 
214
172
  @Transition({ from: 'orgs_fetched', to: 'repo_fetched' })
215
- async fetchRepoDetails() {
216
- const repoResult: ToolResult<GitHubRepoResult> = await this.gitHubGetRepo.call({
217
- owner: this.owner,
218
- repo: this.repo,
173
+ async fetchRepoDetails(state: GitHubReposOverviewState): Promise<GitHubReposOverviewState> {
174
+ const repoResult = await this.gitHubGetRepo.call({
175
+ owner: state.owner,
176
+ repo: state.repo,
219
177
  });
220
- this.repoDetails = repoResult.data!.repo;
221
178
 
222
- const branchesResult: ToolResult<GitHubBranchesResult> = await this.gitHubListBranches.call({
223
- owner: this.owner,
224
- repo: this.repo,
179
+ const branchesResult = await this.gitHubListBranches.call({
180
+ owner: state.owner,
181
+ repo: state.repo,
225
182
  });
226
- this.branches = branchesResult.data!.branches;
183
+ return { ...state, repoDetails: repoResult.data!.repo, branches: branchesResult.data!.branches };
227
184
  }
228
185
 
229
186
  // --- Step 4: Fetch issues and PRs ---
230
187
 
231
188
  @Transition({ from: 'repo_fetched', to: 'issues_prs_fetched' })
232
- async fetchIssuesPrs() {
233
- const issuesResult: ToolResult<GitHubIssuesResult> = await this.gitHubListIssues.call({
234
- owner: this.owner,
235
- repo: this.repo,
189
+ async fetchIssuesPrs(state: GitHubReposOverviewState): Promise<GitHubReposOverviewState> {
190
+ const issuesResult = await this.gitHubListIssues.call({
191
+ owner: state.owner,
192
+ repo: state.repo,
236
193
  state: 'open',
237
194
  perPage: 10,
238
195
  });
239
- this.issues = issuesResult.data!.issues;
240
196
 
241
- const prsResult: ToolResult<GitHubPullRequestsResult> = await this.gitHubListPullRequests.call({
242
- owner: this.owner,
243
- repo: this.repo,
197
+ const prsResult = await this.gitHubListPullRequests.call({
198
+ owner: state.owner,
199
+ repo: state.repo,
244
200
  state: 'open',
245
201
  perPage: 10,
246
202
  });
247
- this.pullRequests = prsResult.data!.pullRequests;
203
+ return { ...state, issues: issuesResult.data!.issues, pullRequests: prsResult.data!.pullRequests };
248
204
  }
249
205
 
250
206
  // --- Step 5: Fetch directory listing and workflow runs ---
251
207
 
252
208
  @Transition({ from: 'issues_prs_fetched', to: 'content_actions_fetched' })
253
- async fetchContentActions() {
254
- const dirResult: ToolResult<GitHubDirectoryResult> = await this.gitHubListDirectory.call({
255
- owner: this.owner,
256
- repo: this.repo,
209
+ async fetchContentActions(state: GitHubReposOverviewState): Promise<GitHubReposOverviewState> {
210
+ const dirResult = await this.gitHubListDirectory.call({
211
+ owner: state.owner,
212
+ repo: state.repo,
257
213
  });
258
- this.directoryEntries = dirResult.data!.entries;
259
214
 
260
- const runsResult: ToolResult<GitHubWorkflowRunsResult> = await this.gitHubListWorkflowRuns.call({
261
- owner: this.owner,
262
- repo: this.repo,
215
+ const runsResult = await this.gitHubListWorkflowRuns.call({
216
+ owner: state.owner,
217
+ repo: state.repo,
263
218
  perPage: 5,
264
219
  });
265
- this.workflowRuns = runsResult.data!.runs;
220
+ return { ...state, directoryEntries: dirResult.data!.entries, workflowRuns: runsResult.data!.runs };
266
221
  }
267
222
 
268
223
  // --- Step 6: Search code in the repo ---
269
224
 
270
225
  @Transition({ from: 'content_actions_fetched', to: 'search_done' })
271
- async fetchSearch() {
272
- const result: ToolResult<GitHubSearchCodeResult> = await this.gitHubSearchCode.call({
273
- query: `repo:${this.owner}/${this.repo}`,
226
+ async fetchSearch(state: GitHubReposOverviewState): Promise<GitHubReposOverviewState> {
227
+ const result = await this.gitHubSearchCode.call({
228
+ query: `repo:${state.owner}/${state.repo}`,
274
229
  perPage: 5,
275
230
  });
276
- this.searchResults = result.data!.results;
231
+ return { ...state, searchResults: result.data!.results };
277
232
  }
278
233
 
279
234
  // --- Display all results ---
280
235
 
281
- @Final({ from: 'search_done' })
282
- async displayResults() {
283
- await this.repository.save(MarkdownDocument, {
236
+ @Transition({ from: 'search_done', to: 'end' })
237
+ async displayResults(state: GitHubReposOverviewState): Promise<unknown> {
238
+ await this.documentStore.save(MarkdownDocument, {
284
239
  markdown: this.render(__dirname + '/templates/repoOverview.md', {
285
- user: this.user,
286
- orgs: this.orgs,
287
- repo: this.repoDetails,
288
- branches: this.branches,
289
- issues: this.issues,
290
- pullRequests: this.pullRequests,
291
- directoryEntries: this.directoryEntries,
292
- workflowRuns: this.workflowRuns,
293
- searchResults: this.searchResults,
240
+ user: state.user,
241
+ orgs: state.orgs,
242
+ repo: state.repoDetails,
243
+ branches: state.branches,
244
+ issues: state.issues,
245
+ pullRequests: state.pullRequests,
246
+ directoryEntries: state.directoryEntries,
247
+ workflowRuns: state.workflowRuns,
248
+ searchResults: state.searchResults,
294
249
  }),
295
250
  });
251
+ return {};
296
252
  }
297
253
  }
@@ -1,17 +0,0 @@
1
- title: 'GitHub Repository Overview'
2
-
3
- description: |
4
- Comprehensive GitHub example that exercises every GitHub tool.
5
- Fetches user info, repository details, issues, pull requests, branches,
6
- directory contents, workflow runs, and search results for a given repository.
7
- If not authenticated, launches the OAuth sub-workflow and retries.
8
-
9
- ui:
10
- form:
11
- properties:
12
- owner:
13
- title: 'Repository Owner'
14
- placeholder: 'octocat'
15
- repo:
16
- title: 'Repository Name'
17
- placeholder: 'Hello-World'
@@ -1,17 +0,0 @@
1
- title: 'GitHub Repository Overview'
2
-
3
- description: |
4
- Comprehensive GitHub example that exercises every GitHub tool.
5
- Fetches user info, repository details, issues, pull requests, branches,
6
- directory contents, workflow runs, and search results for a given repository.
7
- If not authenticated, launches the OAuth sub-workflow and retries.
8
-
9
- ui:
10
- form:
11
- properties:
12
- owner:
13
- title: 'Repository Owner'
14
- placeholder: 'octocat'
15
- repo:
16
- title: 'Repository Name'
17
- placeholder: 'Hello-World'