@loopstack/github-integration 0.2.0 → 0.2.1

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/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # @loopstack/github-integration
2
+
3
+ > A module for the [Loopstack AI](https://loopstack.ai) automation framework.
4
+
5
+ End-to-end GitHub integration workflow for Loopstack workspaces. Wires up OAuth, repo creation / linking, remote configuration, and divergence resolution into one reusable workflow.
6
+
7
+ ## Overview
8
+
9
+ Getting a workspace connected to GitHub is a multi-step affair: authenticate with OAuth, either create a new repo or link an existing one, configure the git remote, handle divergence with an existing remote branch, and push. `ConnectGitHubWorkflow` drives this whole flow, asking the user for input at decision points via `@loopstack/hitl`.
10
+
11
+ See [SETUP.md](./SETUP.md) for OAuth application setup (client ID / secret).
12
+
13
+ By using this module you'll get:
14
+
15
+ - **`ConnectGitHubWorkflow`** — a guided workflow that takes a fresh Loopstack workspace from "not connected" to "pushed to GitHub"
16
+
17
+ ## Installation
18
+
19
+ ```sh
20
+ npm install @loopstack/github-integration
21
+ ```
22
+
23
+ Register the module:
24
+
25
+ ```ts
26
+ import { GitHubIntegrationModule } from '@loopstack/github-integration';
27
+
28
+ @Module({
29
+ imports: [GitHubIntegrationModule /* ... */],
30
+ })
31
+ export class AppModule {}
32
+ ```
33
+
34
+ `GitHubIntegrationModule` pulls in its own dependencies (`GitModule`, `GitHubModule`, `HitlModule`, `OAuthModule`, `RemoteClientModule`) — you do not need to import those manually, but they must be installed as dependencies of your app.
35
+
36
+ You'll also need a GitHub OAuth app configured; see `SETUP.md`.
37
+
38
+ ## How It Works
39
+
40
+ `ConnectGitHubWorkflow` is a state machine that composes smaller pieces:
41
+
42
+ 1. **OAuth** — uses `@loopstack/oauth-module` to obtain a GitHub access token for the user.
43
+ 2. **Repo choice** — asks (via `@loopstack/hitl`) whether to create a new repo or link an existing one.
44
+ 3. **Repo operation** — calls `@loopstack/github-module` to create the repo or fetch metadata for the chosen one.
45
+ 4. **Remote configuration** — uses `@loopstack/git-module` tools (`git-remote-configure`, `git-config-user`) to wire the workspace git config.
46
+ 5. **Divergence handling** — if the remote branch exists and has commits, the workflow asks the user how to resolve (rebase / reset / cancel).
47
+ 6. **Push** — `git-push` finalises.
48
+
49
+ Inspect `src/workflows/connect-github/connect-github.workflow.ts` for the full state transitions.
50
+
51
+ ### Running the workflow
52
+
53
+ Trigger it like any other Loopstack workflow (via the Studio, an API call, or programmatically):
54
+
55
+ ```ts
56
+ import { ConnectGitHubWorkflow } from '@loopstack/github-integration';
57
+
58
+ // inject into a service or call via the workflow API; see @loopstack/core docs
59
+ ```
60
+
61
+ ## Public API
62
+
63
+ - **Module:** `GitHubIntegrationModule`
64
+ - **Workflow:** `ConnectGitHubWorkflow`
65
+
66
+ ## Dependencies
67
+
68
+ - `@loopstack/common`, `@loopstack/core` — framework
69
+ - `@loopstack/git-module` — git operations on the workspace
70
+ - `@loopstack/github-module` — GitHub REST API client
71
+ - `@loopstack/hitl` — user prompts for decision points
72
+ - `@loopstack/oauth-module` — GitHub OAuth flow
73
+ - `@loopstack/remote-client` — dispatches git commands to the remote agent
74
+
75
+ ## About
76
+
77
+ Author: [Jakob Klippel](https://www.linkedin.com/in/jakob-klippel/)
78
+
79
+ License: MIT
80
+
81
+ ### Additional Resources
82
+
83
+ - [Loopstack Documentation](https://loopstack.ai/docs)
84
+ - Find more Loopstack modules in the [Loopstack Registry](https://loopstack.ai/registry)
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "integration",
10
10
  "workflow"
11
11
  ],
12
- "version": "0.2.0",
12
+ "version": "0.2.1",
13
13
  "license": "MIT",
14
14
  "author": {
15
15
  "name": "Jakob Klippel",
@@ -18,8 +18,7 @@
18
18
  "main": "dist/index.js",
19
19
  "types": "dist/index.d.ts",
20
20
  "exports": {
21
- ".": "./dist/index.js",
22
- "./src/*": "./src/*"
21
+ ".": "./dist/index.js"
23
22
  },
24
23
  "scripts": {
25
24
  "build": "nest build",
@@ -30,19 +29,18 @@
30
29
  "watch": "nest build --watch"
31
30
  },
32
31
  "dependencies": {
33
- "@loopstack/common": "^0.27.0",
34
- "@loopstack/core": "^0.27.0",
35
- "@loopstack/git-module": "^0.1.1",
36
- "@loopstack/github-module": "^0.2.3",
37
- "@loopstack/hitl": "^0.1.1",
38
- "@loopstack/oauth-module": "^0.2.5",
39
- "@loopstack/remote-client": "^0.23.2",
32
+ "@loopstack/common": "^0.28.0",
33
+ "@loopstack/core": "^0.28.0",
34
+ "@loopstack/git-module": "^0.1.2",
35
+ "@loopstack/github-module": "^0.2.4",
36
+ "@loopstack/hitl": "^0.1.2",
37
+ "@loopstack/oauth-module": "^0.2.6",
38
+ "@loopstack/remote-client": "^0.23.3",
40
39
  "@nestjs/common": "^11.1.19",
41
40
  "zod": "^4.3.6"
42
41
  },
43
42
  "files": [
44
- "dist",
45
- "src"
43
+ "dist"
46
44
  ],
47
45
  "jest": {
48
46
  "moduleFileExtensions": [
@@ -70,7 +68,6 @@
70
68
  }
71
69
  ],
72
70
  "installModes": [
73
- "add",
74
71
  "install"
75
72
  ]
76
73
  }
@@ -1,15 +0,0 @@
1
- import { Module } from '@nestjs/common';
2
- import { LoopCoreModule } from '@loopstack/core';
3
- import { GitModule } from '@loopstack/git-module';
4
- import { GitHubModule } from '@loopstack/github-module';
5
- import { HitlModule } from '@loopstack/hitl';
6
- import { OAuthModule } from '@loopstack/oauth-module';
7
- import { RemoteClientModule } from '@loopstack/remote-client';
8
- import { ConnectGitHubWorkflow } from './workflows/connect-github/connect-github.workflow';
9
-
10
- @Module({
11
- imports: [LoopCoreModule, GitModule, GitHubModule, HitlModule, OAuthModule, RemoteClientModule],
12
- providers: [ConnectGitHubWorkflow],
13
- exports: [ConnectGitHubWorkflow],
14
- })
15
- export class GitHubIntegrationModule {}
package/src/index.ts DELETED
@@ -1,2 +0,0 @@
1
- export * from './workflows';
2
- export * from './github-integration.module';
@@ -1,5 +0,0 @@
1
- title: 'Connect to GitHub'
2
-
3
- description: |
4
- Connects your workspace to a GitHub repository.
5
- Authenticates with GitHub, lets you create or link a repo, and configures git push access.
@@ -1,451 +0,0 @@
1
- import { Inject } from '@nestjs/common';
2
- import { z } from 'zod';
3
- import {
4
- BaseWorkflow,
5
- CallbackSchema,
6
- Final,
7
- Guard,
8
- Initial,
9
- InjectTool,
10
- InjectWorkflow,
11
- LinkDocument,
12
- MarkdownDocument,
13
- ToolResult,
14
- Transition,
15
- Workflow,
16
- } from '@loopstack/common';
17
- import { ClientMessageService } from '@loopstack/core';
18
- import {
19
- GitConfigUserTool,
20
- GitFetchTool,
21
- GitPushTool,
22
- GitRemoteConfigureTool,
23
- GitStatusTool,
24
- } from '@loopstack/git-module';
25
- import { GitHubCreateRepoTool, GitHubGetAuthenticatedUserTool, GitHubListReposTool } from '@loopstack/github-module';
26
- import { AskUserWorkflow } from '@loopstack/hitl';
27
- import { OAuthTokenStore, OAuthWorkflow } from '@loopstack/oauth-module';
28
- import { BashTool } from '@loopstack/remote-client';
29
-
30
- interface GitHubUser {
31
- login: string;
32
- name: string | null;
33
- email: string | null;
34
- }
35
-
36
- interface GitHubRepo {
37
- fullName: string;
38
- name: string;
39
- htmlUrl: string;
40
- private: boolean;
41
- defaultBranch: string;
42
- }
43
-
44
- @Workflow({
45
- uiConfig: __dirname + '/connect-github.ui.yaml',
46
- schema: z.object({}).strict(),
47
- })
48
- export class ConnectGitHubWorkflow extends BaseWorkflow {
49
- @InjectTool() private gitHubGetAuthenticatedUser: GitHubGetAuthenticatedUserTool;
50
- @InjectTool() private gitHubCreateRepo: GitHubCreateRepoTool;
51
- @InjectTool() private gitHubListRepos: GitHubListReposTool;
52
- @InjectTool() private gitRemoteConfigure: GitRemoteConfigureTool;
53
- @InjectTool() private gitConfigUser: GitConfigUserTool;
54
- @InjectTool() private gitStatus: GitStatusTool;
55
- @InjectTool() private gitPush: GitPushTool;
56
- @InjectTool() private gitFetch: GitFetchTool;
57
- @InjectTool() private bash: BashTool;
58
-
59
- @InjectWorkflow() oAuth: OAuthWorkflow;
60
- @InjectWorkflow() askUser: AskUserWorkflow;
61
-
62
- @Inject() private tokenStore: OAuthTokenStore;
63
- @Inject() private clientMessageService: ClientMessageService;
64
-
65
- private async getGitHubToken(): Promise<string | undefined> {
66
- return (await this.tokenStore.getValidAccessToken(this.ctx.context.userId, 'github')) ?? undefined;
67
- }
68
-
69
- requiresAuth?: boolean;
70
- user?: GitHubUser;
71
- repo?: GitHubRepo;
72
- isNewRepo?: boolean;
73
- divergenceState?: 'none' | 'local_ahead' | 'remote_ahead' | 'diverged';
74
-
75
- // ── Step 1: Check if already authenticated ──────────────────────────
76
-
77
- @Initial({ to: 'check_auth' })
78
- async start() {
79
- const result: ToolResult<{ error?: string; user?: GitHubUser }> = await this.gitHubGetAuthenticatedUser.call({});
80
- this.requiresAuth = result.data!.error === 'unauthorized';
81
- this.user = result.data!.user;
82
- }
83
-
84
- // ── Step 2a: OAuth if needed ────────────────────────────────────────
85
-
86
- @Transition({ from: 'check_auth', to: 'awaiting_auth', priority: 10 })
87
- @Guard('needsAuth')
88
- async launchOAuth() {
89
- const result = await this.oAuth.run(
90
- { provider: 'github', scopes: ['repo', 'user'] },
91
- { alias: 'oAuth', callback: { transition: 'authCompleted' } },
92
- );
93
-
94
- await this.repository.save(
95
- LinkDocument,
96
- {
97
- label: 'Sign in with GitHub',
98
- workflowId: result.workflowId,
99
- embed: true,
100
- expanded: true,
101
- },
102
- { id: `link_${result.workflowId}` },
103
- );
104
- }
105
-
106
- private needsAuth(): boolean {
107
- return !!this.requiresAuth;
108
- }
109
-
110
- @Transition({ from: 'awaiting_auth', to: 'check_auth', wait: true, schema: CallbackSchema })
111
- async authCompleted(payload: { workflowId: string }) {
112
- await this.repository.save(
113
- LinkDocument,
114
- {
115
- status: 'success',
116
- label: 'GitHub authentication completed',
117
- workflowId: payload.workflowId,
118
- embed: true,
119
- expanded: false,
120
- },
121
- { id: `link_${payload.workflowId}` },
122
- );
123
-
124
- const result: ToolResult<{ user?: GitHubUser }> = await this.gitHubGetAuthenticatedUser.call({});
125
- this.user = result.data!.user;
126
- this.requiresAuth = false;
127
- }
128
-
129
- // ── Step 2b: Ask create or link ─────────────────────────────────────
130
-
131
- @Transition({ from: 'check_auth', to: 'awaiting_choice' })
132
- async askCreateOrLink() {
133
- const result = await this.askUser.run(
134
- {
135
- question: 'Would you like to create a new GitHub repository or connect an existing one?',
136
- mode: 'options',
137
- options: ['Create new repository', 'Connect existing repository'],
138
- },
139
- { alias: 'chooseAction', callback: { transition: 'choiceReceived' } },
140
- );
141
-
142
- await this.repository.save(
143
- LinkDocument,
144
- { label: 'Create or connect repository', workflowId: result.workflowId, embed: true, expanded: true },
145
- { id: `link_choice` },
146
- );
147
- }
148
-
149
- @Transition({ from: 'awaiting_choice', to: 'route_choice', wait: true, schema: CallbackSchema })
150
- async choiceReceived(payload: { data: { answer: string } }) {
151
- await this.repository.save(
152
- LinkDocument,
153
- { label: 'Create or connect repository', status: 'success', embed: true, expanded: false },
154
- { id: `link_choice` },
155
- );
156
-
157
- if (payload.data.answer === 'Connect existing repository') {
158
- const listResult: ToolResult<{
159
- repos: Array<{ fullName: string; name: string; htmlUrl: string; private: boolean; defaultBranch: string }>;
160
- }> = await this.gitHubListRepos.call({ visibility: 'all', sort: 'updated', perPage: 30 });
161
-
162
- const repos = listResult.data!.repos ?? [];
163
- const repoNames = repos.map((r) => r.fullName);
164
-
165
- const askResult = await this.askUser.run(
166
- { question: 'Select a repository to connect:', mode: 'options', options: repoNames },
167
- { alias: 'chooseRepo', callback: { transition: 'repoSelected' } },
168
- );
169
-
170
- await this.repository.save(
171
- LinkDocument,
172
- { label: 'Select repository', workflowId: askResult.workflowId, embed: true, expanded: true },
173
- { id: `link_repo_select` },
174
- );
175
- } else {
176
- const askResult = await this.askUser.run(
177
- { question: 'Enter a name for your new repository:' },
178
- { alias: 'repoName', callback: { transition: 'createRepo' } },
179
- );
180
-
181
- await this.repository.save(
182
- LinkDocument,
183
- { label: 'Repository name', workflowId: askResult.workflowId, embed: true, expanded: true },
184
- { id: `link_repo_name` },
185
- );
186
- }
187
- }
188
-
189
- // ── Step 3a: Create new repo ────────────────────────────────────────
190
-
191
- @Transition({ from: 'route_choice', to: 'configure_remote', wait: true, schema: CallbackSchema })
192
- async createRepo(payload: { data: { answer: string } }) {
193
- await this.repository.save(
194
- LinkDocument,
195
- { label: 'Repository name', status: 'success', embed: true, expanded: false },
196
- { id: `link_repo_name` },
197
- );
198
-
199
- const repoName = payload.data.answer.trim();
200
- const createResult: ToolResult<{ repo: GitHubRepo }> = await this.gitHubCreateRepo.call({
201
- name: repoName,
202
- private: true,
203
- autoInit: false,
204
- });
205
-
206
- this.repo = createResult.data!.repo;
207
- this.isNewRepo = true;
208
- }
209
-
210
- // ── Step 3b: Link existing repo ─────────────────────────────────────
211
-
212
- @Transition({ from: 'route_choice', to: 'configure_remote', wait: true, schema: CallbackSchema })
213
- async repoSelected(payload: { data: { answer: string } }) {
214
- await this.repository.save(
215
- LinkDocument,
216
- { label: 'Select repository', status: 'success', embed: true, expanded: false },
217
- { id: `link_repo_select` },
218
- );
219
-
220
- const fullName = payload.data.answer;
221
- const [, name] = fullName.split('/');
222
-
223
- this.repo = {
224
- fullName,
225
- name,
226
- htmlUrl: `https://github.com/${fullName}`,
227
- private: false,
228
- defaultBranch: 'main',
229
- };
230
- this.isNewRepo = false;
231
- }
232
-
233
- // ── Step 4a: Check for uncommitted changes ───────────────────────────
234
-
235
- hasUncommittedChanges?: boolean;
236
-
237
- @Transition({ from: 'configure_remote', to: 'check_uncommitted' })
238
- async checkForUncommittedChanges() {
239
- const user = this.user;
240
-
241
- // Configure git user identity early
242
- if (user) {
243
- await this.gitConfigUser.call({
244
- name: user.name || user.login,
245
- email: user.email || `${user.login}@users.noreply.github.com`,
246
- });
247
- }
248
-
249
- const statusResult: ToolResult<{ staged: string[]; modified: string[]; untracked: string[]; deleted: string[] }> =
250
- await this.gitStatus.call();
251
- const status = statusResult.data!;
252
- this.hasUncommittedChanges =
253
- status.staged.length > 0 ||
254
- status.modified.length > 0 ||
255
- status.untracked.length > 0 ||
256
- status.deleted.length > 0;
257
- }
258
-
259
- // Clean workspace — skip straight to remote setup
260
- @Transition({ from: 'check_uncommitted', to: 'setup_remote' })
261
- @Guard('isCleanWorkspace')
262
- async skipCommitCheck() {}
263
-
264
- private isCleanWorkspace(): boolean {
265
- return !this.hasUncommittedChanges;
266
- }
267
-
268
- // Uncommitted changes — ask user
269
- @Transition({ from: 'check_uncommitted', to: 'awaiting_commit_confirm' })
270
- async askCommitChanges() {
271
- const confirmResult = await this.askUser.run(
272
- {
273
- question:
274
- 'There are uncommitted changes in your workspace. They need to be committed before connecting to a remote repository. Would you like to commit them now?',
275
- mode: 'options',
276
- options: ['Commit changes and continue', 'Cancel'],
277
- },
278
- { alias: 'commitChanges', callback: { transition: 'uncommittedChangesHandled' } },
279
- );
280
-
281
- await this.repository.save(
282
- LinkDocument,
283
- { label: 'Uncommitted changes', workflowId: confirmResult.workflowId, embed: true, expanded: true },
284
- { id: `link_uncommitted` },
285
- );
286
- }
287
-
288
- @Transition({ from: 'awaiting_commit_confirm', to: 'setup_remote', wait: true, schema: CallbackSchema })
289
- async uncommittedChangesHandled(payload: { data: { answer: string } }) {
290
- await this.repository.save(
291
- LinkDocument,
292
- { label: 'Uncommitted changes', status: 'success', embed: true, expanded: false },
293
- { id: `link_uncommitted` },
294
- );
295
-
296
- if (payload.data.answer === 'Cancel') {
297
- this.repo = undefined;
298
- return;
299
- }
300
-
301
- await this.bash.call({ command: 'git add -A' });
302
- await this.bash.call({ command: 'git commit -m "Auto-commit before connecting to GitHub"' });
303
- }
304
-
305
- // ── Step 4b: Configure remote and check for divergence ──────────────
306
-
307
- @Transition({ from: 'setup_remote', to: 'check_divergence' })
308
- async setupRemote() {
309
- // If cancelled during commit confirmation, skip to done
310
- if (!this.repo) {
311
- this.divergenceState = 'none';
312
- return;
313
- }
314
-
315
- const repo = this.repo;
316
-
317
- // Configure the remote URL (no credentials stored)
318
- const remoteUrl = `https://github.com/${repo.fullName}.git`;
319
- await this.gitRemoteConfigure.call({ url: remoteUrl });
320
-
321
- // Fetch remote refs using token for auth (token only exists in memory during the command)
322
- const token = await this.getGitHubToken();
323
- await this.gitFetch.call({ remote: 'origin', token });
324
-
325
- // Determine relationship between local and remote branches
326
- if (!this.isNewRepo) {
327
- const checkResult = (await this.bash.call({
328
- command: [
329
- 'REMOTE_EXISTS=$(git rev-parse --verify origin/main 2>/dev/null && echo yes || echo no)',
330
- 'if [ "$REMOTE_EXISTS" = "no" ]; then echo "no_remote"; exit 0; fi',
331
- 'LOCAL=$(git rev-parse main)',
332
- 'REMOTE=$(git rev-parse origin/main)',
333
- 'if [ "$LOCAL" = "$REMOTE" ]; then echo "same"; exit 0; fi',
334
- 'git merge-base --is-ancestor main origin/main && echo "remote_ahead" && exit 0',
335
- 'git merge-base --is-ancestor origin/main main && echo "local_ahead" && exit 0',
336
- 'echo "diverged"',
337
- ].join(' && '),
338
- })) as ToolResult<{ stdout: string }>;
339
- const state = checkResult.data!.stdout.trim().split('\n').pop()!.trim();
340
- if (state === 'same' || state === 'no_remote') {
341
- this.divergenceState = 'none';
342
- } else {
343
- this.divergenceState = state as 'local_ahead' | 'remote_ahead' | 'diverged';
344
- }
345
- } else {
346
- this.divergenceState = 'none';
347
- }
348
- }
349
-
350
- // ── Step 5a: No divergence or local ahead — just push ────────────────
351
-
352
- @Transition({ from: 'check_divergence', to: 'done' })
353
- @Guard('canPushDirectly')
354
- async pushDirectly() {
355
- if (this.divergenceState === 'none') {
356
- // Already in sync — nothing to push
357
- return;
358
- }
359
- // local_ahead — fast-forward push
360
- const statusResult: ToolResult<{ branch: string }> = await this.gitStatus.call();
361
- const branch = statusResult.data!.branch ?? 'main';
362
- const token = await this.getGitHubToken();
363
- await this.gitPush.call({ remote: 'origin', branch, token });
364
- }
365
-
366
- private canPushDirectly(): boolean {
367
- return this.divergenceState === 'none' || this.divergenceState === 'local_ahead';
368
- }
369
-
370
- // ── Step 5b: Divergence detected — ask user how to resolve ──────────
371
-
372
- @Transition({ from: 'check_divergence', to: 'awaiting_sync_choice' })
373
- async askSyncStrategy() {
374
- const isRemoteAhead = this.divergenceState === 'remote_ahead';
375
- const question = isRemoteAhead
376
- ? 'The remote repository has newer commits than your workspace. How would you like to proceed?'
377
- : 'The remote repository has a different commit history than your workspace. How would you like to proceed?';
378
-
379
- const options = isRemoteAhead
380
- ? ['Pull remote changes into workspace', 'Push workspace code (overwrite remote)', 'Cancel (disconnect remote)']
381
- : [
382
- 'Use remote code (replace local files with remote)',
383
- 'Merge remote changes into workspace',
384
- 'Push workspace code (overwrite remote)',
385
- 'Cancel (disconnect remote)',
386
- ];
387
-
388
- const result = await this.askUser.run(
389
- { question, mode: 'options', options },
390
- { alias: 'syncStrategy', callback: { transition: 'syncStrategyChosen' } },
391
- );
392
-
393
- await this.repository.save(
394
- LinkDocument,
395
- { label: 'Resolve differences', workflowId: result.workflowId, embed: true, expanded: true },
396
- { id: `link_sync` },
397
- );
398
- }
399
-
400
- @Transition({ from: 'awaiting_sync_choice', to: 'done', wait: true, schema: CallbackSchema })
401
- async syncStrategyChosen(payload: { data: { answer: string } }) {
402
- await this.repository.save(
403
- LinkDocument,
404
- { label: 'Resolve differences', status: 'success', embed: true, expanded: false },
405
- { id: `link_sync` },
406
- );
407
-
408
- const answer = payload.data.answer;
409
- const statusResult: ToolResult<{ branch: string }> = await this.gitStatus.call();
410
- const branch = statusResult.data!.branch ?? 'main';
411
-
412
- const token = await this.getGitHubToken();
413
-
414
- if (answer.startsWith('Use remote code')) {
415
- await this.bash.call({ command: `git reset --hard origin/${branch}` });
416
- } else if (answer.startsWith('Pull remote changes') || answer.startsWith('Merge remote changes')) {
417
- await this.bash.call({ command: `git merge origin/${branch} --allow-unrelated-histories --no-edit` });
418
- await this.gitPush.call({ remote: 'origin', branch, token });
419
- } else if (answer.startsWith('Push workspace code')) {
420
- await this.gitPush.call({ remote: 'origin', branch, force: true, token });
421
- } else {
422
- await this.bash.call({ command: 'git remote remove origin' });
423
- this.repo = undefined;
424
- }
425
- }
426
-
427
- // ── Final: Show result ──────────────────────────────────────────────
428
-
429
- @Final({ from: 'done' })
430
- async showSuccess() {
431
- if (!this.repo) {
432
- await this.repository.save(MarkdownDocument, {
433
- markdown: '### Cancelled\n\nThe remote connection was removed. No changes were made.',
434
- });
435
- return { cancelled: true };
436
- }
437
-
438
- const repo = this.repo;
439
- await this.repository.save(MarkdownDocument, {
440
- markdown: `### Repository Connected\n\nYour workspace is now connected to [${repo.fullName}](${repo.htmlUrl}).\n\nAll future commits can be pushed to this repository using the git tools in your workflows.`,
441
- });
442
-
443
- this.clientMessageService.dispatchWorkspaceEvent(
444
- 'git.updated',
445
- this.ctx.context.workspaceId,
446
- this.ctx.context.userId,
447
- );
448
-
449
- return { repo: repo.fullName, url: repo.htmlUrl };
450
- }
451
- }
@@ -1 +0,0 @@
1
- export { ConnectGitHubWorkflow } from './connect-github/connect-github.workflow';