@launch77/cli 1.2.0 → 1.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.
Files changed (215) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/cli.js +8 -1
  3. package/dist/cli.js.map +1 -1
  4. package/dist/infrastructure/git.d.ts +37 -0
  5. package/dist/infrastructure/git.d.ts.map +1 -0
  6. package/dist/infrastructure/git.js +82 -0
  7. package/dist/infrastructure/git.js.map +1 -0
  8. package/dist/infrastructure/github.d.ts +43 -0
  9. package/dist/infrastructure/github.d.ts.map +1 -0
  10. package/dist/infrastructure/github.js +89 -0
  11. package/dist/infrastructure/github.js.map +1 -0
  12. package/dist/infrastructure/template-generator.d.ts +1 -1
  13. package/dist/infrastructure/template-generator.d.ts.map +1 -1
  14. package/dist/infrastructure/template.d.ts +5 -0
  15. package/dist/infrastructure/template.d.ts.map +1 -1
  16. package/dist/infrastructure/template.js +11 -0
  17. package/dist/infrastructure/template.js.map +1 -1
  18. package/dist/modules/app/commands/create-app.js +1 -1
  19. package/dist/modules/app/commands/create-app.js.map +1 -1
  20. package/dist/modules/app/commands/delete-app.js +1 -1
  21. package/dist/modules/app/commands/delete-app.js.map +1 -1
  22. package/dist/modules/app/services/app-svc.d.ts +1 -1
  23. package/dist/modules/app/services/app-svc.d.ts.map +1 -1
  24. package/dist/modules/app/services/manifest-svc.d.ts +1 -1
  25. package/dist/modules/app/services/manifest-svc.d.ts.map +1 -1
  26. package/dist/modules/catalog/config/catalog-config.test.js +1 -1
  27. package/dist/modules/catalog/config/catalog-config.test.js.map +1 -1
  28. package/dist/modules/catalog/schemas/catalog-ui-components.schema.json +2 -18
  29. package/dist/modules/git/commands/git-connect.d.ts +3 -0
  30. package/dist/modules/git/commands/git-connect.d.ts.map +1 -0
  31. package/dist/modules/git/commands/git-connect.js +156 -0
  32. package/dist/modules/git/commands/git-connect.js.map +1 -0
  33. package/dist/modules/git/errors/git-errors.d.ts +21 -0
  34. package/dist/modules/git/errors/git-errors.d.ts.map +1 -0
  35. package/dist/modules/git/errors/git-errors.js +41 -0
  36. package/dist/modules/git/errors/git-errors.js.map +1 -0
  37. package/dist/modules/git/index.d.ts +5 -0
  38. package/dist/modules/git/index.d.ts.map +1 -0
  39. package/dist/modules/git/index.js +8 -0
  40. package/dist/modules/git/index.js.map +1 -0
  41. package/dist/modules/git/services/git-service.d.ts +24 -0
  42. package/dist/modules/git/services/git-service.d.ts.map +1 -0
  43. package/dist/modules/git/services/git-service.js +56 -0
  44. package/dist/modules/git/services/git-service.js.map +1 -0
  45. package/dist/modules/git/services/github-service.d.ts +27 -0
  46. package/dist/modules/git/services/github-service.d.ts.map +1 -0
  47. package/dist/modules/git/services/github-service.js +45 -0
  48. package/dist/modules/git/services/github-service.js.map +1 -0
  49. package/dist/modules/plugin/commands/plugin-create.d.ts +3 -0
  50. package/dist/modules/plugin/commands/plugin-create.d.ts.map +1 -0
  51. package/dist/modules/plugin/commands/plugin-create.js +59 -0
  52. package/dist/modules/plugin/commands/plugin-create.js.map +1 -0
  53. package/dist/modules/plugin/commands/plugin-install.d.ts.map +1 -1
  54. package/dist/modules/plugin/commands/plugin-install.js +9 -24
  55. package/dist/modules/plugin/commands/plugin-install.js.map +1 -1
  56. package/dist/modules/plugin/errors/plugin-errors.d.ts +24 -1
  57. package/dist/modules/plugin/errors/plugin-errors.d.ts.map +1 -1
  58. package/dist/modules/plugin/errors/plugin-errors.js +79 -6
  59. package/dist/modules/plugin/errors/plugin-errors.js.map +1 -1
  60. package/dist/modules/plugin/index.d.ts +4 -2
  61. package/dist/modules/plugin/index.d.ts.map +1 -1
  62. package/dist/modules/plugin/index.js +4 -2
  63. package/dist/modules/plugin/index.js.map +1 -1
  64. package/dist/modules/plugin/lib/plugin-registry.d.ts +6 -12
  65. package/dist/modules/plugin/lib/plugin-registry.d.ts.map +1 -1
  66. package/dist/modules/plugin/lib/plugin-registry.js +13 -30
  67. package/dist/modules/plugin/lib/plugin-registry.js.map +1 -1
  68. package/dist/modules/plugin/lib/plugin-resolver.d.ts +76 -0
  69. package/dist/modules/plugin/lib/plugin-resolver.d.ts.map +1 -0
  70. package/dist/modules/plugin/lib/plugin-resolver.js +128 -0
  71. package/dist/modules/plugin/lib/plugin-resolver.js.map +1 -0
  72. package/dist/modules/plugin/lib/plugin-resolver.test.d.ts +2 -0
  73. package/dist/modules/plugin/lib/plugin-resolver.test.d.ts.map +1 -0
  74. package/dist/modules/plugin/lib/plugin-resolver.test.js +175 -0
  75. package/dist/modules/plugin/lib/plugin-resolver.test.js.map +1 -0
  76. package/dist/modules/plugin/services/plugin-create-service.d.ts +16 -0
  77. package/dist/modules/plugin/services/plugin-create-service.d.ts.map +1 -0
  78. package/dist/modules/plugin/services/plugin-create-service.js +47 -0
  79. package/dist/modules/plugin/services/plugin-create-service.js.map +1 -0
  80. package/dist/modules/plugin/services/plugin-svc.d.ts +8 -3
  81. package/dist/modules/plugin/services/plugin-svc.d.ts.map +1 -1
  82. package/dist/modules/plugin/services/plugin-svc.js +96 -15
  83. package/dist/modules/plugin/services/plugin-svc.js.map +1 -1
  84. package/dist/modules/release/commands/release-init.d.ts +3 -0
  85. package/dist/modules/release/commands/release-init.d.ts.map +1 -0
  86. package/dist/modules/release/commands/release-init.js +92 -0
  87. package/dist/modules/release/commands/release-init.js.map +1 -0
  88. package/dist/modules/release/errors/release-errors.d.ts +7 -0
  89. package/dist/modules/release/errors/release-errors.d.ts.map +1 -0
  90. package/dist/modules/release/errors/release-errors.js +13 -0
  91. package/dist/modules/release/errors/release-errors.js.map +1 -0
  92. package/dist/modules/release/index.d.ts +4 -0
  93. package/dist/modules/release/index.d.ts.map +1 -0
  94. package/dist/modules/release/index.js +7 -0
  95. package/dist/modules/release/index.js.map +1 -0
  96. package/dist/modules/release/services/release-service.d.ts +34 -0
  97. package/dist/modules/release/services/release-service.d.ts.map +1 -0
  98. package/dist/modules/release/services/release-service.js +154 -0
  99. package/dist/modules/release/services/release-service.js.map +1 -0
  100. package/dist/modules/workspace/commands/init-workspace.d.ts.map +1 -1
  101. package/dist/modules/workspace/commands/init-workspace.js +4 -5
  102. package/dist/modules/workspace/commands/init-workspace.js.map +1 -1
  103. package/dist/modules/workspace/services/workspace-service.d.ts +2 -1
  104. package/dist/modules/workspace/services/workspace-service.d.ts.map +1 -1
  105. package/dist/modules/workspace/services/workspace-service.js +27 -1
  106. package/dist/modules/workspace/services/workspace-service.js.map +1 -1
  107. package/dist/templates/plugin/README.md.hbs +39 -0
  108. package/dist/{plugins/theme/package.json → templates/plugin/package.json.hbs} +5 -3
  109. package/dist/templates/plugin/plugin.json.hbs +7 -0
  110. package/dist/templates/plugin/src/generator.ts.hbs +64 -0
  111. package/dist/templates/plugin/templates/src/.gitkeep +0 -0
  112. package/dist/templates/plugin/tsconfig.json +10 -0
  113. package/dist/{plugins/theme → templates/plugin}/tsup.config.ts +0 -1
  114. package/dist/templates/workspace/.github/workflows/ci.yml +102 -0
  115. package/dist/templates/workspace/package.json +16 -1
  116. package/dist/templates/workspace/turbo.json +5 -0
  117. package/dist/utils/launch77-context.d.ts +1 -1
  118. package/dist/utils/launch77-context.d.ts.map +1 -1
  119. package/dist/utils/launch77-context.js +25 -2
  120. package/dist/utils/launch77-context.js.map +1 -1
  121. package/dist/utils/launch77-validation.d.ts +1 -1
  122. package/dist/utils/launch77-validation.d.ts.map +1 -1
  123. package/dist/utils/string.d.ts +13 -0
  124. package/dist/utils/string.d.ts.map +1 -0
  125. package/dist/utils/string.js +18 -0
  126. package/dist/utils/string.js.map +1 -0
  127. package/package.json +7 -10
  128. package/src/cli.ts +10 -1
  129. package/src/infrastructure/git.ts +86 -0
  130. package/src/infrastructure/github.ts +111 -0
  131. package/src/infrastructure/template-generator.ts +1 -1
  132. package/src/infrastructure/template.ts +14 -0
  133. package/src/modules/app/commands/create-app.ts +1 -1
  134. package/src/modules/app/commands/delete-app.ts +1 -1
  135. package/src/modules/app/services/app-svc.ts +1 -1
  136. package/src/modules/app/services/manifest-svc.ts +1 -1
  137. package/src/modules/catalog/config/catalog-config.test.ts +1 -1
  138. package/src/modules/git/commands/git-connect.ts +183 -0
  139. package/src/modules/git/errors/git-errors.ts +44 -0
  140. package/src/modules/git/index.ts +9 -0
  141. package/src/modules/git/services/git-service.ts +63 -0
  142. package/src/modules/git/services/github-service.ts +52 -0
  143. package/src/modules/plugin/commands/plugin-create.ts +68 -0
  144. package/src/modules/plugin/commands/plugin-install.ts +9 -26
  145. package/src/modules/plugin/errors/plugin-errors.ts +87 -6
  146. package/src/modules/plugin/index.ts +4 -2
  147. package/src/modules/plugin/lib/plugin-registry.ts +14 -37
  148. package/src/modules/plugin/lib/plugin-resolver.test.ts +215 -0
  149. package/src/modules/plugin/lib/plugin-resolver.ts +160 -0
  150. package/src/modules/plugin/services/plugin-create-service.ts +69 -0
  151. package/src/modules/plugin/services/plugin-svc.ts +108 -15
  152. package/src/modules/release/commands/release-init.ts +102 -0
  153. package/src/modules/release/errors/release-errors.ts +13 -0
  154. package/src/modules/release/index.ts +8 -0
  155. package/src/modules/release/services/release-service.ts +170 -0
  156. package/src/modules/workspace/commands/init-workspace.ts +4 -6
  157. package/src/modules/workspace/services/workspace-service.ts +30 -1
  158. package/src/utils/launch77-context.ts +29 -3
  159. package/src/utils/launch77-validation.ts +1 -1
  160. package/src/utils/string.ts +17 -0
  161. package/templates/plugin/README.md.hbs +39 -0
  162. package/templates/plugin/package.json.hbs +34 -0
  163. package/templates/plugin/plugin.json.hbs +7 -0
  164. package/templates/plugin/src/generator.ts.hbs +64 -0
  165. package/templates/plugin/templates/src/.gitkeep +0 -0
  166. package/templates/plugin/tsconfig.json +10 -0
  167. package/templates/plugin/tsup.config.ts +9 -0
  168. package/templates/workspace/.github/workflows/ci.yml +102 -0
  169. package/templates/workspace/package.json +5 -0
  170. package/templates/workspace/turbo.json +5 -0
  171. package/tests/integration/cli.test.ts +25 -0
  172. package/tests/integration/setup.ts +20 -0
  173. package/vitest.config.ts +9 -0
  174. package/vitest.integration.config.ts +9 -0
  175. package/dist/app-templates/webapp/.env.ci +0 -6
  176. package/dist/app-templates/webapp/.env.example +0 -9
  177. package/dist/app-templates/webapp/.eslintrc.json +0 -6
  178. package/dist/app-templates/webapp/README.md.hbs +0 -80
  179. package/dist/app-templates/webapp/app/about/page.tsx.hbs +0 -41
  180. package/dist/app-templates/webapp/app/dashboard/page.tsx.hbs +0 -51
  181. package/dist/app-templates/webapp/app/globals.css +0 -31
  182. package/dist/app-templates/webapp/app/layout.tsx.hbs +0 -26
  183. package/dist/app-templates/webapp/app/page.tsx.hbs +0 -30
  184. package/dist/app-templates/webapp/next.config.js +0 -99
  185. package/dist/app-templates/webapp/package.json.hbs +0 -30
  186. package/dist/app-templates/webapp/postcss.config.js +0 -6
  187. package/dist/app-templates/webapp/tailwind.config.ts +0 -24
  188. package/dist/app-templates/webapp/tsconfig.json +0 -29
  189. package/dist/app-templates/webapp/vercel.json.hbs +0 -7
  190. package/dist/modules/catalog/schemas/schemas/catalog-ui-components.schema.json +0 -145
  191. package/dist/plugins/theme/plugin.json +0 -9
  192. package/dist/plugins/theme/src/generator.ts +0 -92
  193. package/dist/plugins/theme/src/utils/config-modifier.ts +0 -142
  194. package/dist/plugins/theme/src/utils/css-modifier.ts +0 -89
  195. package/dist/plugins/theme/templates/app/theme-test/page.tsx +0 -156
  196. package/dist/plugins/theme/templates/src/modules/theme/README.md +0 -209
  197. package/dist/plugins/theme/templates/src/modules/theme/config/brand.css +0 -23
  198. package/dist/plugins/theme/tsconfig.json +0 -14
  199. package/dist/templates/templates/startup/apps/.gitkeep +0 -8
  200. package/dist/templates/templates/workspace/.launch77/workspace.json +0 -3
  201. package/dist/templates/templates/workspace/README.md +0 -62
  202. package/dist/templates/templates/workspace/app-templates/.gitkeep +0 -1
  203. package/dist/templates/templates/workspace/apps/.gitkeep +0 -1
  204. package/dist/templates/templates/workspace/libraries/.gitkeep +0 -1
  205. package/dist/templates/templates/workspace/package.json +0 -31
  206. package/dist/templates/templates/workspace/plugins/.gitkeep +0 -1
  207. package/dist/templates/templates/workspace/tsconfig.json +0 -22
  208. package/dist/templates/templates/workspace/turbo.json +0 -25
  209. package/launch77-cli-1.2.0.tgz +0 -0
  210. package/src/modules/plugin/lib/launch77-workspace.code-workspace +0 -14
  211. /package/dist/templates/{templates/workspace → workspace}/.eslintignore +0 -0
  212. /package/dist/templates/{templates/workspace → workspace}/.eslintrc.js +0 -0
  213. /package/dist/templates/{templates/workspace → workspace}/.husky/pre-push +0 -0
  214. /package/dist/templates/{templates/workspace → workspace}/.lintstagedrc.json +0 -0
  215. /package/dist/templates/{templates/workspace → workspace}/.prettierrc +0 -0
@@ -0,0 +1,86 @@
1
+ import { execa } from 'execa'
2
+
3
+ /**
4
+ * Check if git is installed
5
+ */
6
+ export async function isGitInstalled(): Promise<boolean> {
7
+ try {
8
+ await execa('git', ['--version'])
9
+ return true
10
+ } catch {
11
+ return false
12
+ }
13
+ }
14
+
15
+ /**
16
+ * Check if a directory is a git repository
17
+ */
18
+ export async function isGitRepository(cwd: string): Promise<boolean> {
19
+ try {
20
+ await execa('git', ['rev-parse', '--git-dir'], { cwd })
21
+ return true
22
+ } catch {
23
+ return false
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Initialize a git repository
29
+ */
30
+ export async function gitInit(cwd: string): Promise<void> {
31
+ await execa('git', ['init'], { cwd })
32
+ }
33
+
34
+ /**
35
+ * Check if there are any commits in the repository
36
+ */
37
+ export async function hasCommits(cwd: string): Promise<boolean> {
38
+ try {
39
+ await execa('git', ['rev-parse', 'HEAD'], { cwd })
40
+ return true
41
+ } catch {
42
+ return false
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Stage all files
48
+ */
49
+ export async function gitAddAll(cwd: string): Promise<void> {
50
+ await execa('git', ['add', '.'], { cwd })
51
+ }
52
+
53
+ /**
54
+ * Create a commit
55
+ */
56
+ export async function gitCommit(cwd: string, message: string): Promise<void> {
57
+ await execa('git', ['commit', '-m', message], { cwd })
58
+ }
59
+
60
+ /**
61
+ * Get current branch name
62
+ */
63
+ export async function getCurrentBranch(cwd: string): Promise<string> {
64
+ const result = await execa('git', ['branch', '--show-current'], { cwd })
65
+ return result.stdout.trim()
66
+ }
67
+
68
+ /**
69
+ * Check if a remote exists
70
+ */
71
+ export async function hasRemote(cwd: string, remoteName: string): Promise<boolean> {
72
+ try {
73
+ await execa('git', ['remote', 'get-url', remoteName], { cwd })
74
+ return true
75
+ } catch {
76
+ return false
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Get the URL of a remote
82
+ */
83
+ export async function getRemoteUrl(cwd: string, remoteName: string): Promise<string> {
84
+ const result = await execa('git', ['remote', 'get-url', remoteName], { cwd })
85
+ return result.stdout.trim()
86
+ }
@@ -0,0 +1,111 @@
1
+ import { execa } from 'execa'
2
+
3
+ export interface GitHubOwner {
4
+ login: string
5
+ type: 'User' | 'Organization'
6
+ }
7
+
8
+ /**
9
+ * Check if gh CLI is installed
10
+ */
11
+ export async function isGhInstalled(): Promise<boolean> {
12
+ try {
13
+ await execa('gh', ['--version'])
14
+ return true
15
+ } catch {
16
+ return false
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Check if gh is authenticated
22
+ */
23
+ export async function isGhAuthenticated(): Promise<boolean> {
24
+ try {
25
+ await execa('gh', ['auth', 'status'])
26
+ return true
27
+ } catch {
28
+ return false
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Get authenticated user's username
34
+ */
35
+ export async function getUsername(): Promise<string> {
36
+ const result = await execa('gh', ['api', 'user', '--jq', '.login'])
37
+ return result.stdout.trim()
38
+ }
39
+
40
+ /**
41
+ * Get user's organizations
42
+ */
43
+ export async function getOrganizations(): Promise<string[]> {
44
+ const result = await execa('gh', ['api', 'user/orgs', '--jq', '.[].login'])
45
+ const output = result.stdout.trim()
46
+ return output ? output.split('\n') : []
47
+ }
48
+
49
+ /**
50
+ * Get all available owners (user + organizations)
51
+ */
52
+ export async function getAvailableOwners(): Promise<GitHubOwner[]> {
53
+ const [username, orgs] = await Promise.all([getUsername(), getOrganizations()])
54
+
55
+ const owners: GitHubOwner[] = [
56
+ {
57
+ login: username,
58
+ type: 'User',
59
+ },
60
+ ]
61
+
62
+ for (const org of orgs) {
63
+ owners.push({
64
+ login: org,
65
+ type: 'Organization',
66
+ })
67
+ }
68
+
69
+ return owners
70
+ }
71
+
72
+ /**
73
+ * Create a GitHub repository
74
+ */
75
+ export async function createRepository(
76
+ owner: string,
77
+ repo: string,
78
+ options: {
79
+ visibility: 'private' | 'public'
80
+ cwd: string
81
+ }
82
+ ): Promise<string> {
83
+ const repoName = `${owner}/${repo}`
84
+ const visibilityFlag = `--${options.visibility}`
85
+
86
+ await execa('gh', ['repo', 'create', repoName, visibilityFlag, '--source=.', '--remote=origin', '--push'], {
87
+ cwd: options.cwd,
88
+ })
89
+
90
+ // Return the repository URL
91
+ return `https://github.com/${owner}/${repo}`
92
+ }
93
+
94
+ /**
95
+ * Get the current repository information (owner/repo)
96
+ */
97
+ export async function getCurrentRepository(cwd: string): Promise<{ owner: string; repo: string }> {
98
+ const result = await execa('gh', ['repo', 'view', '--json', 'owner,name'], { cwd })
99
+ const data = JSON.parse(result.stdout)
100
+ return {
101
+ owner: data.owner.login,
102
+ repo: data.name,
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Set a repository secret using gh CLI
108
+ */
109
+ export async function setRepositorySecret(owner: string, repo: string, secretName: string, secretValue: string): Promise<void> {
110
+ await execa('gh', ['secret', 'set', secretName, '-R', `${owner}/${repo}`, '--body', secretValue])
111
+ }
@@ -1,6 +1,6 @@
1
1
  import { getTemplatePath, processTemplate, templateExists } from './template.js'
2
2
 
3
- import type { Launch77Context } from '../utils/launch77-context.js'
3
+ import type { Launch77Context } from '@launch77/plugin-runtime'
4
4
 
5
5
  export interface GenerateFromTemplateOptions {
6
6
  port?: string
@@ -57,6 +57,20 @@ export function getTemplatePath(templateName: string): string {
57
57
  return path.join(baseDir, templateName)
58
58
  }
59
59
 
60
+ /**
61
+ * Get the path to a plugin template directory
62
+ * Plugin templates are in dist/templates/ (copied during build)
63
+ */
64
+ export function getPluginTemplatePath(templateName: string): string {
65
+ const __filename = fileURLToPath(import.meta.url)
66
+ const __dirname = path.dirname(__filename)
67
+
68
+ // From dist/infrastructure/ to dist/templates/
69
+ const baseDir = path.join(__dirname, '../templates')
70
+
71
+ return path.join(baseDir, templateName)
72
+ }
73
+
60
74
  /**
61
75
  * Check if a template exists
62
76
  */
@@ -5,7 +5,7 @@ import chalk from 'chalk'
5
5
  import { Command } from 'commander'
6
6
  import ora from 'ora'
7
7
 
8
- import { detectLaunch77Context } from '../../../utils/launch77-context.js'
8
+ import { detectLaunch77Context } from '@launch77/plugin-runtime'
9
9
  import { AppService } from '../services/app-svc.js'
10
10
  import { APP_TYPES, APP_TYPES_LIST } from '../types/app-types.js'
11
11
 
@@ -6,7 +6,7 @@ import { Command } from 'commander'
6
6
  import inquirer from 'inquirer'
7
7
  import ora from 'ora'
8
8
 
9
- import { detectLaunch77Context } from '../../../utils/launch77-context.js'
9
+ import { detectLaunch77Context } from '@launch77/plugin-runtime'
10
10
  import { AppService } from '../services/app-svc.js'
11
11
 
12
12
  export function deleteAppCommand(): Command {
@@ -10,7 +10,7 @@ import { templateExists } from '../../../infrastructure/template.js'
10
10
  import { validateWorkspaceContext } from '../../../utils/launch77-validation.js'
11
11
  import { validateAppName } from '../../../utils/validation.js'
12
12
 
13
- import type { Launch77Context } from '../../../utils/launch77-context.js'
13
+ import type { Launch77Context } from '@launch77/plugin-runtime'
14
14
  import type { CreateAppRequest, CreateAppResult, DeleteAppRequest, DeleteAppResult } from '../types/app-types.js'
15
15
 
16
16
  export class AppService {
@@ -3,7 +3,7 @@ import * as path from 'path'
3
3
 
4
4
  import { parseManifest } from '../lib/manifest-schema.js'
5
5
 
6
- import type { Launch77Context } from '../../../utils/launch77-context.js'
6
+ import type { Launch77Context } from '@launch77/plugin-runtime'
7
7
  import type { AppManifest } from '../lib/manifest-schema.js'
8
8
 
9
9
  /**
@@ -159,7 +159,7 @@ describe('validateConfig', () => {
159
159
  type: 'server-functions',
160
160
  packageName: '@launch77/ui',
161
161
  }
162
- expect(() => validateConfig(config)).toThrow('Type must be "ui-components"')
162
+ expect(() => validateConfig(config)).toThrow(/expected "ui-components"/)
163
163
  })
164
164
 
165
165
  test('type is number', () => {
@@ -0,0 +1,183 @@
1
+ import chalk from 'chalk'
2
+ import { Command } from 'commander'
3
+ import { select, input, confirm } from '@inquirer/prompts'
4
+ import ora from 'ora'
5
+
6
+ import { detectLaunch77Context } from '@launch77/plugin-runtime'
7
+ import { GitHubCLINotInstalledError, GitHubNotAuthenticatedError, NotInWorkspaceError, GitNotInstalledError } from '../errors/git-errors.js'
8
+ import { GitHubService } from '../services/github-service.js'
9
+ import { GitService } from '../services/git-service.js'
10
+
11
+ interface ConnectOptions {
12
+ owner?: string
13
+ repo?: string
14
+ private?: boolean
15
+ public?: boolean
16
+ }
17
+
18
+ export function gitConnectCommand(): Command {
19
+ return new Command('git:connect')
20
+ .description('Connect Launch77 workspace to GitHub repository')
21
+ .option('--owner <name>', 'GitHub owner (username or organization)')
22
+ .option('--repo <name>', 'Repository name (defaults to workspace name)')
23
+ .option('--private', 'Create private repository (default)')
24
+ .option('--public', 'Create public repository')
25
+ .action(async (options: ConnectOptions) => {
26
+ console.log(chalk.blue('\n🔗 Connecting workspace to GitHub...\n'))
27
+
28
+ const cwd = process.cwd()
29
+ const githubService = new GitHubService()
30
+ const gitService = new GitService()
31
+
32
+ try {
33
+ // 1. Verify we're in a workspace root
34
+ const context = await detectLaunch77Context(cwd)
35
+ if (!context.isValid || context.locationType !== 'workspace-root') {
36
+ throw new NotInWorkspaceError(cwd)
37
+ }
38
+
39
+ // 2. Verify prerequisites
40
+ const spinner = ora('Checking prerequisites...').start()
41
+
42
+ await gitService.verifyGitInstalled()
43
+ await githubService.verifyPrerequisites()
44
+
45
+ spinner.succeed('Prerequisites verified')
46
+
47
+ // 3. Check if already connected to a remote
48
+ const hasOrigin = await gitService.hasRemote(context.workspaceRoot)
49
+
50
+ if (hasOrigin) {
51
+ const remoteUrl = await gitService.getRemoteUrl(context.workspaceRoot)
52
+
53
+ console.log(chalk.yellow('\n⚠ This workspace is already connected to a git remote:\n'))
54
+ console.log(chalk.gray(` origin: ${chalk.cyan(remoteUrl)}\n`))
55
+
56
+ const shouldExit = await confirm({
57
+ message: 'Workspace already connected. Exit?',
58
+ default: true,
59
+ })
60
+
61
+ if (shouldExit) {
62
+ console.log(chalk.green('\n✅ No changes made.\n'))
63
+ process.exit(0)
64
+ }
65
+
66
+ console.log(chalk.yellow('\n⚠ Continuing may fail if remote "origin" cannot be overwritten...\n'))
67
+ }
68
+
69
+ // 4. Get available owners
70
+ const availableOwners = await githubService.getAvailableOwners()
71
+
72
+ // 5. Determine owner
73
+ let owner: string
74
+ if (options.owner) {
75
+ owner = options.owner
76
+ } else {
77
+ const ownerChoices = availableOwners.map((o) => ({
78
+ name: o.type === 'User' ? `${o.login} (personal)` : o.login,
79
+ value: o.login,
80
+ }))
81
+
82
+ owner = await select({
83
+ message: 'Select repository owner:',
84
+ choices: ownerChoices,
85
+ })
86
+ }
87
+
88
+ // 6. Determine repository name
89
+ let repoName: string
90
+ if (options.repo) {
91
+ repoName = options.repo
92
+ } else {
93
+ repoName = await input({
94
+ message: 'Repository name:',
95
+ default: context.workspaceName,
96
+ validate: (value) => {
97
+ if (!value || value.trim().length === 0) {
98
+ return 'Repository name cannot be empty'
99
+ }
100
+ // GitHub repo name validation (simplified)
101
+ const validPattern = /^[a-zA-Z0-9._-]+$/
102
+ if (!validPattern.test(value)) {
103
+ return 'Repository name must contain only letters, numbers, dots, hyphens, and underscores'
104
+ }
105
+ return true
106
+ },
107
+ })
108
+ }
109
+
110
+ // 7. Determine visibility
111
+ let visibility: 'private' | 'public'
112
+ if (options.public) {
113
+ visibility = 'public'
114
+ } else if (options.private) {
115
+ visibility = 'private'
116
+ } else {
117
+ const visibilityChoice = await select({
118
+ message: 'Repository visibility:',
119
+ choices: [
120
+ { name: 'Private', value: 'private' as const },
121
+ { name: 'Public', value: 'public' as const },
122
+ ],
123
+ default: 'private' as const,
124
+ })
125
+ visibility = visibilityChoice
126
+ }
127
+
128
+ // 8. Ensure git repository is ready
129
+ await gitService.ensureRepositoryReady(context.workspaceRoot)
130
+
131
+ // 9. Create GitHub repository and push
132
+ const createSpinner = ora('Creating GitHub repository and pushing...').start()
133
+
134
+ try {
135
+ const repoUrl = await githubService.createAndPushRepository(owner, repoName, visibility, context.workspaceRoot)
136
+
137
+ createSpinner.succeed('Repository created and code pushed')
138
+
139
+ // Success message
140
+ console.log(chalk.green(`\n✅ Workspace connected to GitHub!\n`))
141
+ console.log(chalk.white(` Repository: ${chalk.cyan(repoUrl)}\n`))
142
+ console.log(chalk.gray(`Next steps:\n` + ` - Your code has been pushed to GitHub\n` + ` - GitHub Actions will run automatically\n` + ` - View your repository: ${chalk.cyan('gh repo view --web')}\n` + `\n` + ` ${chalk.yellow('📦 Enable automatic releases (recommended):')}\n` + ` - Run: ${chalk.cyan('launch77 release:init')}\n` + ` - This sets up complete release workflow (GitHub tokens, changesets, npm)\n`))
143
+ } catch (error) {
144
+ createSpinner.fail('Failed to create repository')
145
+
146
+ // Check if it's a "repository already exists" error
147
+ if (error instanceof Error && error.message.includes('already exists')) {
148
+ console.error(chalk.red(`\n❌ Repository ${owner}/${repoName} already exists on GitHub\n`))
149
+ console.log(chalk.gray(` View existing repository: ${chalk.cyan(`https://github.com/${owner}/${repoName}`)}\n`))
150
+ } else {
151
+ throw error
152
+ }
153
+ }
154
+ } catch (error) {
155
+ if (error instanceof GitHubCLINotInstalledError) {
156
+ console.error(chalk.red(`\n❌ ${error.message}\n`))
157
+ console.log(chalk.gray(` Install with: ${chalk.cyan('brew install gh')}\n`))
158
+ console.log(chalk.gray(` Or visit: ${chalk.cyan('https://cli.github.com/')}\n`))
159
+ process.exit(1)
160
+ }
161
+
162
+ if (error instanceof GitHubNotAuthenticatedError) {
163
+ console.error(chalk.red(`\n❌ ${error.message}\n`))
164
+ console.log(chalk.gray(` Authenticate with: ${chalk.cyan('gh auth login')}\n`))
165
+ process.exit(1)
166
+ }
167
+
168
+ if (error instanceof NotInWorkspaceError) {
169
+ console.error(chalk.red(`\n❌ ${error.message}\n`))
170
+ console.log(chalk.gray(` This command must be run from a Launch77 workspace root directory\n`))
171
+ process.exit(1)
172
+ }
173
+
174
+ if (error instanceof GitNotInstalledError) {
175
+ console.error(chalk.red(`\n❌ ${error.message}\n`))
176
+ console.log(chalk.gray(` Install git from: ${chalk.cyan('https://git-scm.com/downloads')}\n`))
177
+ process.exit(1)
178
+ }
179
+
180
+ throw error
181
+ }
182
+ })
183
+ }
@@ -0,0 +1,44 @@
1
+ export class GitHubCLINotInstalledError extends Error {
2
+ constructor() {
3
+ super('GitHub CLI (gh) is not installed')
4
+ this.name = 'GitHubCLINotInstalledError'
5
+ }
6
+ }
7
+
8
+ export class GitHubNotAuthenticatedError extends Error {
9
+ constructor() {
10
+ super('GitHub CLI is not authenticated')
11
+ this.name = 'GitHubNotAuthenticatedError'
12
+ }
13
+ }
14
+
15
+ export class NotInWorkspaceError extends Error {
16
+ constructor(cwd: string) {
17
+ super(`Not in a Launch77 workspace root. Current directory: ${cwd}`)
18
+ this.name = 'NotInWorkspaceError'
19
+ }
20
+ }
21
+
22
+ export class GitNotInstalledError extends Error {
23
+ constructor() {
24
+ super('Git is not installed')
25
+ this.name = 'GitNotInstalledError'
26
+ }
27
+ }
28
+
29
+ export class RepositoryAlreadyExistsError extends Error {
30
+ constructor(
31
+ public owner: string,
32
+ public repo: string
33
+ ) {
34
+ super(`Repository ${owner}/${repo} already exists`)
35
+ this.name = 'RepositoryAlreadyExistsError'
36
+ }
37
+ }
38
+
39
+ export class GitHubNotConnectedError extends Error {
40
+ constructor() {
41
+ super('This workspace is not connected to a GitHub repository')
42
+ this.name = 'GitHubNotConnectedError'
43
+ }
44
+ }
@@ -0,0 +1,9 @@
1
+ // Services (for other modules to use)
2
+ export { GitService } from './services/git-service.js'
3
+ export { GitHubService } from './services/github-service.js'
4
+
5
+ // Errors (for proper error handling)
6
+ export { GitHubCLINotInstalledError, GitHubNotAuthenticatedError, NotInWorkspaceError, GitNotInstalledError, RepositoryAlreadyExistsError, GitHubNotConnectedError } from './errors/git-errors.js'
7
+
8
+ // Commands
9
+ export { gitConnectCommand } from './commands/git-connect.js'
@@ -0,0 +1,63 @@
1
+ import * as git from '../../../infrastructure/git.js'
2
+ import { GitNotInstalledError, GitHubNotConnectedError } from '../errors/git-errors.js'
3
+
4
+ export class GitService {
5
+ /**
6
+ * Verify git is installed
7
+ */
8
+ async verifyGitInstalled(): Promise<void> {
9
+ const isInstalled = await git.isGitInstalled()
10
+ if (!isInstalled) {
11
+ throw new GitNotInstalledError()
12
+ }
13
+ }
14
+
15
+ /**
16
+ * Initialize git repository if needed and create initial commit if needed
17
+ */
18
+ async ensureRepositoryReady(cwd: string): Promise<void> {
19
+ // Check if git repository exists
20
+ const isRepo = await git.isGitRepository(cwd)
21
+
22
+ if (!isRepo) {
23
+ console.log('Initializing git repository...')
24
+ await git.gitInit(cwd)
25
+ console.log('✓ Git repository initialized')
26
+ }
27
+
28
+ // Check if there are any commits
29
+ const hasAnyCommits = await git.hasCommits(cwd)
30
+
31
+ if (!hasAnyCommits) {
32
+ console.log('Creating initial commit...')
33
+ await git.gitAddAll(cwd)
34
+ await git.gitCommit(cwd, 'Initial commit')
35
+ console.log('✓ Initial commit created')
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Check if repository already has a remote
41
+ */
42
+ async hasRemote(cwd: string, remoteName: string = 'origin'): Promise<boolean> {
43
+ return git.hasRemote(cwd, remoteName)
44
+ }
45
+
46
+ /**
47
+ * Get the URL of a remote
48
+ */
49
+ async getRemoteUrl(cwd: string, remoteName: string = 'origin'): Promise<string> {
50
+ return git.getRemoteUrl(cwd, remoteName)
51
+ }
52
+
53
+ /**
54
+ * Ensure repository is connected to GitHub (has a remote)
55
+ * Throws GitHubNotConnectedError if not connected
56
+ */
57
+ async ensureConnectedToGitHub(cwd: string, remoteName: string = 'origin'): Promise<void> {
58
+ const hasOrigin = await this.hasRemote(cwd, remoteName)
59
+ if (!hasOrigin) {
60
+ throw new GitHubNotConnectedError()
61
+ }
62
+ }
63
+ }
@@ -0,0 +1,52 @@
1
+ import * as github from '../../../infrastructure/github.js'
2
+ import { GitHubCLINotInstalledError, GitHubNotAuthenticatedError } from '../errors/git-errors.js'
3
+
4
+ import type { GitHubOwner } from '../../../infrastructure/github.js'
5
+
6
+ export class GitHubService {
7
+ /**
8
+ * Verify GitHub CLI prerequisites (installed and authenticated)
9
+ */
10
+ async verifyPrerequisites(): Promise<void> {
11
+ const isInstalled = await github.isGhInstalled()
12
+ if (!isInstalled) {
13
+ throw new GitHubCLINotInstalledError()
14
+ }
15
+
16
+ const isAuthenticated = await github.isGhAuthenticated()
17
+ if (!isAuthenticated) {
18
+ throw new GitHubNotAuthenticatedError()
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Get all available repository owners (personal + organizations)
24
+ */
25
+ async getAvailableOwners(): Promise<GitHubOwner[]> {
26
+ return github.getAvailableOwners()
27
+ }
28
+
29
+ /**
30
+ * Create a GitHub repository and push initial commit
31
+ */
32
+ async createAndPushRepository(owner: string, repo: string, visibility: 'private' | 'public', cwd: string): Promise<string> {
33
+ return github.createRepository(owner, repo, {
34
+ visibility,
35
+ cwd,
36
+ })
37
+ }
38
+
39
+ /**
40
+ * Get the current repository (owner/repo) from the working directory
41
+ */
42
+ async getCurrentRepository(cwd: string): Promise<{ owner: string; repo: string }> {
43
+ return github.getCurrentRepository(cwd)
44
+ }
45
+
46
+ /**
47
+ * Set a repository secret
48
+ */
49
+ async setRepositorySecret(owner: string, repo: string, secretName: string, secretValue: string): Promise<void> {
50
+ return github.setRepositorySecret(owner, repo, secretName, secretValue)
51
+ }
52
+ }