@launch77/cli 1.3.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 (154) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/cli.js +6 -3
  3. package/dist/cli.js.map +1 -1
  4. package/dist/infrastructure/template-generator.d.ts +1 -1
  5. package/dist/infrastructure/template-generator.d.ts.map +1 -1
  6. package/dist/infrastructure/template.d.ts +5 -0
  7. package/dist/infrastructure/template.d.ts.map +1 -1
  8. package/dist/infrastructure/template.js +11 -0
  9. package/dist/infrastructure/template.js.map +1 -1
  10. package/dist/modules/app/commands/create-app.js +1 -1
  11. package/dist/modules/app/commands/create-app.js.map +1 -1
  12. package/dist/modules/app/commands/delete-app.js +1 -1
  13. package/dist/modules/app/commands/delete-app.js.map +1 -1
  14. package/dist/modules/app/services/app-svc.d.ts +1 -1
  15. package/dist/modules/app/services/app-svc.d.ts.map +1 -1
  16. package/dist/modules/app/services/manifest-svc.d.ts +1 -1
  17. package/dist/modules/app/services/manifest-svc.d.ts.map +1 -1
  18. package/dist/modules/catalog/config/catalog-config.test.js +1 -1
  19. package/dist/modules/catalog/config/catalog-config.test.js.map +1 -1
  20. package/dist/modules/git/commands/git-connect.js +2 -2
  21. package/dist/modules/git/commands/git-connect.js.map +1 -1
  22. package/dist/modules/git/errors/git-errors.d.ts +3 -0
  23. package/dist/modules/git/errors/git-errors.d.ts.map +1 -1
  24. package/dist/modules/git/errors/git-errors.js +6 -0
  25. package/dist/modules/git/errors/git-errors.js.map +1 -1
  26. package/dist/modules/git/index.d.ts +3 -1
  27. package/dist/modules/git/index.d.ts.map +1 -1
  28. package/dist/modules/git/index.js +6 -1
  29. package/dist/modules/git/index.js.map +1 -1
  30. package/dist/modules/git/services/git-service.d.ts +5 -0
  31. package/dist/modules/git/services/git-service.d.ts.map +1 -1
  32. package/dist/modules/git/services/git-service.js +11 -1
  33. package/dist/modules/git/services/git-service.js.map +1 -1
  34. package/dist/modules/plugin/commands/plugin-create.d.ts +3 -0
  35. package/dist/modules/plugin/commands/plugin-create.d.ts.map +1 -0
  36. package/dist/modules/plugin/commands/plugin-create.js +59 -0
  37. package/dist/modules/plugin/commands/plugin-create.js.map +1 -0
  38. package/dist/modules/plugin/commands/plugin-install.d.ts.map +1 -1
  39. package/dist/modules/plugin/commands/plugin-install.js +9 -24
  40. package/dist/modules/plugin/commands/plugin-install.js.map +1 -1
  41. package/dist/modules/plugin/errors/plugin-errors.d.ts +24 -1
  42. package/dist/modules/plugin/errors/plugin-errors.d.ts.map +1 -1
  43. package/dist/modules/plugin/errors/plugin-errors.js +79 -6
  44. package/dist/modules/plugin/errors/plugin-errors.js.map +1 -1
  45. package/dist/modules/plugin/index.d.ts +4 -2
  46. package/dist/modules/plugin/index.d.ts.map +1 -1
  47. package/dist/modules/plugin/index.js +4 -2
  48. package/dist/modules/plugin/index.js.map +1 -1
  49. package/dist/modules/plugin/lib/plugin-registry.d.ts +6 -12
  50. package/dist/modules/plugin/lib/plugin-registry.d.ts.map +1 -1
  51. package/dist/modules/plugin/lib/plugin-registry.js +13 -30
  52. package/dist/modules/plugin/lib/plugin-registry.js.map +1 -1
  53. package/dist/modules/plugin/lib/plugin-resolver.d.ts +76 -0
  54. package/dist/modules/plugin/lib/plugin-resolver.d.ts.map +1 -0
  55. package/dist/modules/plugin/lib/plugin-resolver.js +128 -0
  56. package/dist/modules/plugin/lib/plugin-resolver.js.map +1 -0
  57. package/dist/modules/plugin/lib/plugin-resolver.test.d.ts +2 -0
  58. package/dist/modules/plugin/lib/plugin-resolver.test.d.ts.map +1 -0
  59. package/dist/modules/plugin/lib/plugin-resolver.test.js +175 -0
  60. package/dist/modules/plugin/lib/plugin-resolver.test.js.map +1 -0
  61. package/dist/modules/plugin/services/plugin-create-service.d.ts +16 -0
  62. package/dist/modules/plugin/services/plugin-create-service.d.ts.map +1 -0
  63. package/dist/modules/plugin/services/plugin-create-service.js +47 -0
  64. package/dist/modules/plugin/services/plugin-create-service.js.map +1 -0
  65. package/dist/modules/plugin/services/plugin-svc.d.ts +8 -3
  66. package/dist/modules/plugin/services/plugin-svc.d.ts.map +1 -1
  67. package/dist/modules/plugin/services/plugin-svc.js +96 -15
  68. package/dist/modules/plugin/services/plugin-svc.js.map +1 -1
  69. package/dist/modules/release/commands/release-init.d.ts +3 -0
  70. package/dist/modules/release/commands/release-init.d.ts.map +1 -0
  71. package/dist/modules/release/commands/release-init.js +92 -0
  72. package/dist/modules/release/commands/release-init.js.map +1 -0
  73. package/dist/modules/release/errors/release-errors.d.ts +7 -0
  74. package/dist/modules/release/errors/release-errors.d.ts.map +1 -0
  75. package/dist/modules/release/errors/release-errors.js +13 -0
  76. package/dist/modules/release/errors/release-errors.js.map +1 -0
  77. package/dist/modules/release/index.d.ts +4 -0
  78. package/dist/modules/release/index.d.ts.map +1 -0
  79. package/dist/modules/release/index.js +7 -0
  80. package/dist/modules/release/index.js.map +1 -0
  81. package/dist/modules/release/services/release-service.d.ts +34 -0
  82. package/dist/modules/release/services/release-service.d.ts.map +1 -0
  83. package/dist/modules/release/services/release-service.js +154 -0
  84. package/dist/modules/release/services/release-service.js.map +1 -0
  85. package/dist/templates/plugin/README.md.hbs +39 -0
  86. package/dist/templates/plugin/package.json.hbs +34 -0
  87. package/dist/templates/plugin/plugin.json.hbs +7 -0
  88. package/dist/templates/plugin/src/generator.ts.hbs +64 -0
  89. package/dist/templates/plugin/templates/src/.gitkeep +0 -0
  90. package/dist/templates/plugin/tsconfig.json +10 -0
  91. package/dist/templates/plugin/tsup.config.ts +9 -0
  92. package/dist/templates/workspace/.github/workflows/ci.yml +8 -5
  93. package/dist/templates/workspace/package.json +1 -0
  94. package/dist/templates/workspace/turbo.json +5 -0
  95. package/dist/utils/launch77-context.d.ts +1 -1
  96. package/dist/utils/launch77-context.d.ts.map +1 -1
  97. package/dist/utils/launch77-context.js +25 -2
  98. package/dist/utils/launch77-context.js.map +1 -1
  99. package/dist/utils/launch77-validation.d.ts +1 -1
  100. package/dist/utils/launch77-validation.d.ts.map +1 -1
  101. package/dist/utils/string.d.ts +13 -0
  102. package/dist/utils/string.d.ts.map +1 -0
  103. package/dist/utils/string.js +18 -0
  104. package/dist/utils/string.js.map +1 -0
  105. package/package.json +6 -9
  106. package/src/cli.ts +7 -3
  107. package/src/infrastructure/template-generator.ts +1 -1
  108. package/src/infrastructure/template.ts +14 -0
  109. package/src/modules/app/commands/create-app.ts +1 -1
  110. package/src/modules/app/commands/delete-app.ts +1 -1
  111. package/src/modules/app/services/app-svc.ts +1 -1
  112. package/src/modules/app/services/manifest-svc.ts +1 -1
  113. package/src/modules/catalog/config/catalog-config.test.ts +1 -1
  114. package/src/modules/git/commands/git-connect.ts +2 -2
  115. package/src/modules/git/errors/git-errors.ts +7 -0
  116. package/src/modules/git/index.ts +8 -1
  117. package/src/modules/git/services/git-service.ts +12 -1
  118. package/src/modules/plugin/commands/plugin-create.ts +68 -0
  119. package/src/modules/plugin/commands/plugin-install.ts +9 -26
  120. package/src/modules/plugin/errors/plugin-errors.ts +87 -6
  121. package/src/modules/plugin/index.ts +4 -2
  122. package/src/modules/plugin/lib/plugin-registry.ts +14 -37
  123. package/src/modules/plugin/lib/plugin-resolver.test.ts +215 -0
  124. package/src/modules/plugin/lib/plugin-resolver.ts +160 -0
  125. package/src/modules/plugin/services/plugin-create-service.ts +69 -0
  126. package/src/modules/plugin/services/plugin-svc.ts +108 -15
  127. package/src/modules/release/commands/release-init.ts +102 -0
  128. package/src/modules/release/errors/release-errors.ts +13 -0
  129. package/src/modules/release/index.ts +8 -0
  130. package/src/modules/release/services/release-service.ts +170 -0
  131. package/src/utils/launch77-context.ts +29 -3
  132. package/src/utils/launch77-validation.ts +1 -1
  133. package/src/utils/string.ts +17 -0
  134. package/templates/plugin/README.md.hbs +39 -0
  135. package/templates/plugin/package.json.hbs +34 -0
  136. package/templates/plugin/plugin.json.hbs +7 -0
  137. package/templates/plugin/src/generator.ts.hbs +64 -0
  138. package/templates/plugin/templates/src/.gitkeep +0 -0
  139. package/templates/plugin/tsconfig.json +10 -0
  140. package/templates/plugin/tsup.config.ts +9 -0
  141. package/templates/workspace/.github/workflows/ci.yml +8 -5
  142. package/templates/workspace/package.json +1 -0
  143. package/templates/workspace/turbo.json +5 -0
  144. package/tests/integration/cli.test.ts +25 -0
  145. package/tests/integration/setup.ts +20 -0
  146. package/vitest.config.ts +9 -0
  147. package/vitest.integration.config.ts +9 -0
  148. package/dist/modules/git/commands/git-setup-releases.d.ts +0 -3
  149. package/dist/modules/git/commands/git-setup-releases.d.ts.map +0 -1
  150. package/dist/modules/git/commands/git-setup-releases.js +0 -128
  151. package/dist/modules/git/commands/git-setup-releases.js.map +0 -1
  152. package/launch77-cli-1.2.0.tgz +0 -0
  153. package/src/modules/git/commands/git-setup-releases.ts +0 -148
  154. package/src/modules/plugin/lib/launch77-workspace.code-workspace +0 -14
@@ -0,0 +1,102 @@
1
+ import chalk from 'chalk'
2
+ import { Command } from 'commander'
3
+ import ora from 'ora'
4
+
5
+ import { detectLaunch77Context } from '@launch77/plugin-runtime'
6
+ import { GitService, GitHubService, GitHubCLINotInstalledError, GitHubNotAuthenticatedError, NotInWorkspaceError, GitHubNotConnectedError } from '../../git/index.js'
7
+ import { ReleaseService } from '../services/release-service.js'
8
+ import { ChangesetNotInitializedError } from '../errors/release-errors.js'
9
+
10
+ export function releaseInitCommand(): Command {
11
+ return new Command('release:init').description('Initialize complete release workflow setup').action(async () => {
12
+ console.log(chalk.blue('\n🚀 Initializing release workflow...\n'))
13
+
14
+ const cwd = process.cwd()
15
+ const gitService = new GitService()
16
+ const githubService = new GitHubService()
17
+ const releaseService = new ReleaseService(githubService)
18
+
19
+ try {
20
+ // 1. Verify we're in a workspace root
21
+ const context = await detectLaunch77Context(cwd)
22
+ if (!context.isValid || context.locationType !== 'workspace-root') {
23
+ throw new NotInWorkspaceError(cwd)
24
+ }
25
+
26
+ // 2. Verify prerequisites (GitHub CLI installed and authenticated)
27
+ const spinner = ora('Checking prerequisites...').start()
28
+ await githubService.verifyPrerequisites()
29
+ spinner.succeed('Prerequisites verified')
30
+
31
+ // 3. Ensure connected to GitHub
32
+ try {
33
+ await gitService.ensureConnectedToGitHub(context.workspaceRoot)
34
+ } catch (error) {
35
+ if (error instanceof GitHubNotConnectedError) {
36
+ console.error(chalk.red(`\n❌ ${error.message}\n`))
37
+ console.log(chalk.gray(` Connect to GitHub first: ${chalk.cyan('launch77 git:connect')}\n`))
38
+ process.exit(1)
39
+ }
40
+ throw error
41
+ }
42
+
43
+ // 4. Get repository information
44
+ const repoSpinner = ora('Detecting repository...').start()
45
+ const { owner, repo } = await githubService.getCurrentRepository(context.workspaceRoot)
46
+ repoSpinner.succeed(`Repository: ${owner}/${repo}`)
47
+
48
+ // 5. Fix changeset config access setting
49
+ console.log(chalk.cyan('\n📝 Configuring changesets...\n'))
50
+ try {
51
+ const changesetSpinner = ora('Checking changeset configuration...').start()
52
+ const wasChanged = await releaseService.fixChangesetAccess(context.workspaceRoot)
53
+
54
+ if (wasChanged) {
55
+ changesetSpinner.succeed('Updated .changeset/config.json access to "public"')
56
+ } else {
57
+ changesetSpinner.succeed('Changeset already configured correctly')
58
+ }
59
+ } catch (error) {
60
+ if (error instanceof ChangesetNotInitializedError) {
61
+ console.error(chalk.red(`\n❌ ${error.message}\n`))
62
+ console.log(chalk.gray(' Changesets should be initialized during workspace creation.'))
63
+ console.log(chalk.gray(` Run manually: ${chalk.cyan('npx changeset init')}\n`))
64
+ process.exit(1)
65
+ }
66
+ throw error
67
+ }
68
+
69
+ // 6. Setup RELEASE_TOKEN
70
+ releaseService.explainReleaseToken()
71
+ const token = await releaseService.promptForReleaseToken()
72
+ await releaseService.setupReleaseToken(owner, repo, token)
73
+
74
+ // 7. Guide npm Trusted Publishing setup
75
+ releaseService.explainNpmTrustedPublishing()
76
+
77
+ // 8. Show success summary
78
+ releaseService.showSuccessSummary()
79
+ } catch (error) {
80
+ if (error instanceof GitHubCLINotInstalledError) {
81
+ console.error(chalk.red(`\n❌ ${error.message}\n`))
82
+ console.log(chalk.gray(` Install with: ${chalk.cyan('brew install gh')}\n`))
83
+ console.log(chalk.gray(` Or visit: ${chalk.cyan('https://cli.github.com/')}\n`))
84
+ process.exit(1)
85
+ }
86
+
87
+ if (error instanceof GitHubNotAuthenticatedError) {
88
+ console.error(chalk.red(`\n❌ ${error.message}\n`))
89
+ console.log(chalk.gray(` Authenticate with: ${chalk.cyan('gh auth login')}\n`))
90
+ process.exit(1)
91
+ }
92
+
93
+ if (error instanceof NotInWorkspaceError) {
94
+ console.error(chalk.red(`\n❌ ${error.message}\n`))
95
+ console.log(chalk.gray(` This command must be run from a Launch77 workspace root directory\n`))
96
+ process.exit(1)
97
+ }
98
+
99
+ throw error
100
+ }
101
+ })
102
+ }
@@ -0,0 +1,13 @@
1
+ export class InvalidReleaseTokenError extends Error {
2
+ constructor(message: string) {
3
+ super(message)
4
+ this.name = 'InvalidReleaseTokenError'
5
+ }
6
+ }
7
+
8
+ export class ChangesetNotInitializedError extends Error {
9
+ constructor() {
10
+ super('Changesets are not initialized in this workspace')
11
+ this.name = 'ChangesetNotInitializedError'
12
+ }
13
+ }
@@ -0,0 +1,8 @@
1
+ // Services
2
+ export { ReleaseService } from './services/release-service.js'
3
+
4
+ // Errors
5
+ export { InvalidReleaseTokenError, ChangesetNotInitializedError } from './errors/release-errors.js'
6
+
7
+ // Commands
8
+ export { releaseInitCommand } from './commands/release-init.js'
@@ -0,0 +1,170 @@
1
+ import chalk from 'chalk'
2
+ import { password, confirm } from '@inquirer/prompts'
3
+ import ora from 'ora'
4
+ import fs from 'fs/promises'
5
+ import path from 'path'
6
+
7
+ import { GitHubService } from '../../git/index.js'
8
+ import { InvalidReleaseTokenError, ChangesetNotInitializedError } from '../errors/release-errors.js'
9
+
10
+ export class ReleaseService {
11
+ constructor(private githubService: GitHubService = new GitHubService()) {}
12
+
13
+ /**
14
+ * Validate GitHub Personal Access Token format
15
+ */
16
+ validateReleaseToken(token: string): void {
17
+ if (!token || token.trim().length === 0) {
18
+ throw new InvalidReleaseTokenError('Token cannot be empty')
19
+ }
20
+
21
+ // GitHub PATs start with specific prefixes
22
+ if (!token.startsWith('ghp_') && !token.startsWith('github_pat_')) {
23
+ throw new InvalidReleaseTokenError('Invalid token format. GitHub PATs should start with "ghp_" or "github_pat_"')
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Explain what RELEASE_TOKEN is and why it's needed
29
+ */
30
+ explainReleaseToken(): void {
31
+ console.log(chalk.cyan('\n📋 About RELEASE_TOKEN:\n'))
32
+ console.log(chalk.white('The RELEASE_TOKEN is a GitHub Personal Access Token (PAT) that allows'))
33
+ console.log(chalk.white('the Changesets action to create Pull Requests for version updates.\n'))
34
+ console.log(chalk.white('Why is this needed?'))
35
+ console.log(chalk.gray(' • The default GITHUB_TOKEN has limited permissions'))
36
+ console.log(chalk.gray(' • Creating PRs that trigger CI requires a PAT'))
37
+ console.log(chalk.gray(' • This enables automated release workflows\n'))
38
+ console.log(chalk.white('Required permissions:'))
39
+ console.log(chalk.gray(' • Contents: Read and write'))
40
+ console.log(chalk.gray(' • Pull requests: Read and write\n'))
41
+
42
+ // Provide link to create PAT
43
+ const tokenUrl = 'https://github.com/settings/personal-access-tokens/new'
44
+ console.log(chalk.cyan('🔗 Create your token:'))
45
+ console.log(chalk.gray(` ${chalk.cyan(tokenUrl)}`))
46
+ console.log(chalk.gray(` Name: Launch77 Release Token`))
47
+ console.log(chalk.gray(` Permissions: Contents (Read and write), Pull requests (Read and write)\n`))
48
+ }
49
+
50
+ /**
51
+ * Prompt user for RELEASE_TOKEN
52
+ */
53
+ async promptForReleaseToken(): Promise<string> {
54
+ return password({
55
+ message: 'Paste your Personal Access Token (PAT):',
56
+ mask: '*',
57
+ validate: (value) => {
58
+ try {
59
+ this.validateReleaseToken(value)
60
+ return true
61
+ } catch (error) {
62
+ if (error instanceof InvalidReleaseTokenError) {
63
+ return error.message
64
+ }
65
+ return 'Invalid token'
66
+ }
67
+ },
68
+ })
69
+ }
70
+
71
+ /**
72
+ * Setup RELEASE_TOKEN secret in GitHub repository
73
+ */
74
+ async setupReleaseToken(owner: string, repo: string, token: string): Promise<void> {
75
+ this.validateReleaseToken(token)
76
+
77
+ // Confirm before setting
78
+ console.log(chalk.yellow('\n⚠ Note: This will overwrite any existing RELEASE_TOKEN secret\n'))
79
+ const shouldContinue = await confirm({
80
+ message: 'Continue and set RELEASE_TOKEN?',
81
+ default: true,
82
+ })
83
+
84
+ if (!shouldContinue) {
85
+ console.log(chalk.green('\n✅ No changes made.\n'))
86
+ process.exit(0)
87
+ }
88
+
89
+ // Set the secret
90
+ const setSpinner = ora('Setting RELEASE_TOKEN secret...').start()
91
+ try {
92
+ await this.githubService.setRepositorySecret(owner, repo, 'RELEASE_TOKEN', token)
93
+ setSpinner.succeed('RELEASE_TOKEN configured successfully!')
94
+ } catch (error) {
95
+ setSpinner.fail('Failed to set RELEASE_TOKEN')
96
+ throw error
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Fix changeset config to set access to "public"
102
+ */
103
+ async fixChangesetAccess(workspaceRoot: string): Promise<boolean> {
104
+ const configPath = path.join(workspaceRoot, '.changeset', 'config.json')
105
+
106
+ try {
107
+ // Check if config exists
108
+ const configContent = await fs.readFile(configPath, 'utf-8')
109
+ const config = JSON.parse(configContent)
110
+
111
+ // Check if already public
112
+ if (config.access === 'public') {
113
+ return false // No change needed
114
+ }
115
+
116
+ // Update to public
117
+ config.access = 'public'
118
+
119
+ // Write back
120
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8')
121
+
122
+ return true // Changed
123
+ } catch (error) {
124
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
125
+ throw new ChangesetNotInitializedError()
126
+ }
127
+ throw error
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Explain npm Trusted Publishing (OIDC)
133
+ */
134
+ explainNpmTrustedPublishing(): void {
135
+ console.log(chalk.cyan('\n📦 Setting up npm publishing with Trusted Publishers (OIDC):\n'))
136
+ console.log(chalk.white('Trusted Publishing uses OpenID Connect (OIDC) for secure, token-free publishing.'))
137
+ console.log(chalk.white('This is the recommended approach (no tokens to manage or expire).\n'))
138
+
139
+ console.log(chalk.white('Steps to configure:'))
140
+ console.log(chalk.gray(' 1. Visit: https://www.npmjs.com/settings/~/publishers'))
141
+ console.log(chalk.gray(' 2. Click "Add a trusted publisher"'))
142
+ console.log(chalk.gray(' 3. Select "GitHub Actions"'))
143
+ console.log(chalk.gray(' 4. Enter your repository information:'))
144
+ console.log(chalk.gray(' - Repository owner (your GitHub username or org)'))
145
+ console.log(chalk.gray(' - Repository name'))
146
+ console.log(chalk.gray(' - Workflow file: .github/workflows/ci.yml'))
147
+ console.log(chalk.gray(' 5. Save the configuration\n'))
148
+
149
+ console.log(chalk.white('Your GitHub workflow already has the required permission:'))
150
+ console.log(chalk.gray(' ✓ id-token: write\n'))
151
+
152
+ console.log(chalk.cyan('📚 Learn more:'))
153
+ console.log(chalk.gray(' https://docs.npmjs.com/trusted-publishers/\n'))
154
+ }
155
+
156
+ /**
157
+ * Show success summary for release setup
158
+ */
159
+ showSuccessSummary(): void {
160
+ console.log(chalk.green('\n✅ Release automation is ready!\n'))
161
+ console.log(chalk.white('What happens now:'))
162
+ console.log(chalk.gray(' • When you push to main, CI runs as usual'))
163
+ console.log(chalk.gray(' • Changesets detects version changes'))
164
+ console.log(chalk.gray(' • A "Version Packages" PR is created automatically'))
165
+ console.log(chalk.gray(' • Merge the PR to publish your packages\n'))
166
+
167
+ console.log(chalk.cyan('📚 Learn more:'))
168
+ console.log(chalk.gray(` ${chalk.cyan('https://github.com/changesets/changesets')}\n`))
169
+ }
170
+ }
@@ -2,7 +2,7 @@ import * as path from 'path'
2
2
 
3
3
  import fs from 'fs-extra'
4
4
 
5
- export type Launch77LocationType = 'workspace-root' | 'workspace-app' | 'unknown'
5
+ export type Launch77LocationType = 'workspace-root' | 'workspace-app' | 'workspace-library' | 'workspace-plugin' | 'workspace-app-template' | 'unknown'
6
6
 
7
7
  export interface Launch77Context {
8
8
  isValid: boolean
@@ -64,6 +64,9 @@ interface ParsedLocation {
64
64
  * Parse the directory structure to determine location context
65
65
  * Based on patterns:
66
66
  * - apps/[name] → workspace-app
67
+ * - libraries/[name] → workspace-library
68
+ * - plugins/[name] → workspace-plugin
69
+ * - app-templates/[name] → workspace-app-template
67
70
  * - (empty or root) → workspace-root
68
71
  */
69
72
  function parseLocationFromPath(cwdPath: string, workspaceRoot: string): ParsedLocation {
@@ -84,8 +87,31 @@ function parseLocationFromPath(cwdPath: string, workspaceRoot: string): ParsedLo
84
87
  }
85
88
  }
86
89
 
87
- // Somewhere else in workspace (libraries, plugins, etc.)
88
- // Still considered workspace-root for now
90
+ // libraries/[lib-name]/...
91
+ if (parts[0] === 'libraries' && parts.length >= 2) {
92
+ return {
93
+ locationType: 'workspace-library',
94
+ appName: parts[1],
95
+ }
96
+ }
97
+
98
+ // plugins/[plugin-name]/...
99
+ if (parts[0] === 'plugins' && parts.length >= 2) {
100
+ return {
101
+ locationType: 'workspace-plugin',
102
+ appName: parts[1],
103
+ }
104
+ }
105
+
106
+ // app-templates/[template-name]/...
107
+ if (parts[0] === 'app-templates' && parts.length >= 2) {
108
+ return {
109
+ locationType: 'workspace-app-template',
110
+ appName: parts[1],
111
+ }
112
+ }
113
+
114
+ // Somewhere else in workspace
89
115
  return { locationType: 'workspace-root' }
90
116
  }
91
117
 
@@ -1,4 +1,4 @@
1
- import type { Launch77Context } from './launch77-context.js'
1
+ import type { Launch77Context } from '@launch77/plugin-runtime'
2
2
 
3
3
  /**
4
4
  * Validation result with helpful error messages
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Convert a kebab-case or snake_case string to PascalCase
3
+ *
4
+ * @param str - The string to convert (e.g., "my-plugin" or "my_plugin")
5
+ * @returns PascalCase string (e.g., "MyPlugin")
6
+ *
7
+ * @example
8
+ * toPascalCase('my-plugin') // 'MyPlugin'
9
+ * toPascalCase('release') // 'Release'
10
+ * toPascalCase('my-awesome-plugin') // 'MyAwesomePlugin'
11
+ */
12
+ export function toPascalCase(str: string): string {
13
+ return str
14
+ .split(/[-_]/)
15
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
16
+ .join('')
17
+ }
@@ -0,0 +1,39 @@
1
+ # {{pluginNamePascal}} Plugin
2
+
3
+ {{description}}
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ launch77 plugin:install {{pluginName}}
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ After installation, the plugin will:
14
+
15
+ - TODO: Describe what the plugin does
16
+ - TODO: List any files created or modified
17
+ - TODO: Explain configuration options
18
+
19
+ ## Development
20
+
21
+ ### Building
22
+
23
+ ```bash
24
+ npm run build
25
+ ```
26
+
27
+ ### Testing
28
+
29
+ ```bash
30
+ npm run typecheck
31
+ ```
32
+
33
+ ## Template Files
34
+
35
+ The `templates/` directory contains files that will be copied to the target application when this plugin is installed. Add any template files your plugin needs here.
36
+
37
+ ## License
38
+
39
+ UNLICENSED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@{{workspaceName}}/plugin-{{pluginName}}",
3
+ "version": "1.0.0",
4
+ "description": "{{description}}",
5
+ "license": "UNLICENSED",
6
+ "private": true,
7
+ "type": "module",
8
+ "main": "dist/generator.js",
9
+ "bin": {
10
+ "generate": "./dist/generator.js"
11
+ },
12
+ "files": [
13
+ "dist/",
14
+ "templates/",
15
+ "plugin.json"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsup",
19
+ "dev": "tsup --watch",
20
+ "typecheck": "tsc --noEmit"
21
+ },
22
+ "dependencies": {
23
+ "@launch77/plugin-runtime": "^0.1.0",
24
+ "chalk": "^5.3.0"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^20.10.0",
28
+ "tsup": "^8.0.0",
29
+ "typescript": "^5.3.0"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ }
34
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "{{pluginName}}",
3
+ "version": "1.0.0",
4
+ "description": "{{description}}",
5
+ "pluginDependencies": {},
6
+ "libraryDependencies": {}
7
+ }
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+
3
+ import chalk from 'chalk'
4
+ import { StandardGenerator } from '@launch77/plugin-runtime'
5
+ import type { GeneratorContext } from '@launch77/plugin-runtime'
6
+
7
+ export class {{pluginNamePascal}}Generator extends StandardGenerator {
8
+ constructor(context: GeneratorContext) {
9
+ super(context)
10
+ }
11
+
12
+ protected async injectCode(): Promise<void> {
13
+ console.log(chalk.cyan('🔧 Setting up {{pluginName}} plugin...\n'))
14
+
15
+ // TODO: Add your plugin's code injection logic here
16
+ // Examples:
17
+ // - Copy template files to the app
18
+ // - Modify package.json
19
+ // - Update configuration files
20
+
21
+ console.log(chalk.green(' ✓ Plugin setup complete\n'))
22
+ }
23
+
24
+ protected showNextSteps(): void {
25
+ console.log(chalk.white('\n' + '─'.repeat(60) + '\n'))
26
+ console.log(chalk.cyan('📋 {{pluginNamePascal}} Plugin Installed!\n'))
27
+ console.log(chalk.white('Next Steps:\n'))
28
+
29
+ console.log(chalk.gray('1. TODO: Add your first step'))
30
+ console.log(chalk.cyan(' npm run <command>\n'))
31
+
32
+ console.log(chalk.gray('2. TODO: Add your second step'))
33
+ console.log(chalk.cyan(' npm run <command>\n'))
34
+
35
+ console.log(chalk.white('Documentation:\n'))
36
+ console.log(chalk.gray('See README.md for detailed instructions.\n'))
37
+ }
38
+ }
39
+
40
+ // CLI entry point
41
+ async function main() {
42
+ const args = process.argv.slice(2)
43
+ const appPath = args.find((arg) => arg.startsWith('--appPath='))?.split('=')[1]
44
+ const appName = args.find((arg) => arg.startsWith('--appName='))?.split('=')[1]
45
+ const workspaceName = args.find((arg) => arg.startsWith('--workspaceName='))?.split('=')[1]
46
+ const pluginPath = args.find((arg) => arg.startsWith('--pluginPath='))?.split('=')[1]
47
+
48
+ if (!appPath || !appName || !workspaceName || !pluginPath) {
49
+ console.error(chalk.red('Error: Missing required arguments'))
50
+ console.error(chalk.gray('Usage: --appPath=<path> --appName=<name> --workspaceName=<name> --pluginPath=<path>'))
51
+ process.exit(1)
52
+ }
53
+
54
+ const generator = new {{pluginNamePascal}}Generator({ appPath, appName, workspaceName, pluginPath })
55
+ await generator.run()
56
+ }
57
+
58
+ if (import.meta.url === `file://${process.argv[1]}`) {
59
+ main().catch((error) => {
60
+ console.error(chalk.red('\n❌ Error during plugin setup:'))
61
+ console.error(error)
62
+ process.exit(1)
63
+ })
64
+ }
File without changes
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "module": "ESNext",
7
+ "target": "ES2022"
8
+ },
9
+ "include": ["src/**/*"]
10
+ }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'tsup'
2
+
3
+ export default defineConfig({
4
+ entry: ['src/generator.ts'],
5
+ format: ['esm'],
6
+ dts: false,
7
+ clean: true,
8
+ shims: true,
9
+ })
@@ -31,20 +31,23 @@ jobs:
31
31
  cache: 'npm'
32
32
 
33
33
  - name: Install dependencies
34
- run: npm ci
34
+ run: npm install
35
35
 
36
36
  - name: Run linting
37
37
  run: npm run lint
38
38
 
39
- - name: Run type checking
40
- run: npm run typecheck
41
-
42
39
  - name: Build packages
43
40
  run: npm run build
44
41
 
42
+ - name: Run type checking
43
+ run: npm run typecheck
44
+
45
45
  - name: Run tests
46
46
  run: npm run test
47
47
 
48
+ - name: Run integration tests
49
+ run: npm run test:integration
50
+
48
51
  quality-checks:
49
52
  runs-on: ubuntu-latest
50
53
 
@@ -58,7 +61,7 @@ jobs:
58
61
  cache: 'npm'
59
62
 
60
63
  - name: Install dependencies
61
- run: npm ci
64
+ run: npm install
62
65
 
63
66
  - name: Check for dependency vulnerabilities
64
67
  run: npm audit --audit-level=high
@@ -13,6 +13,7 @@
13
13
  "dev": "turbo run dev",
14
14
  "build": "turbo run build",
15
15
  "test": "turbo run test",
16
+ "test:integration": "turbo run test:integration",
16
17
  "lint": "turbo run lint",
17
18
  "typecheck": "turbo run typecheck",
18
19
  "changeset": "changeset",
@@ -20,6 +20,11 @@
20
20
  "test": {
21
21
  "outputs": [],
22
22
  "dependsOn": ["build"]
23
+ },
24
+ "test:integration": {
25
+ "outputs": [],
26
+ "dependsOn": ["build"],
27
+ "cache": false
23
28
  }
24
29
  }
25
30
  }
@@ -0,0 +1,25 @@
1
+ import { describe, it, expect, beforeAll } from 'vitest'
2
+ import { existsSync } from 'fs'
3
+ import { runCli } from './setup.js'
4
+
5
+ describe('CLI Integration Tests', () => {
6
+ beforeAll(() => {
7
+ if (!existsSync('./dist/cli.js')) {
8
+ throw new Error('CLI not built. Run `npm run build` first.')
9
+ }
10
+ })
11
+
12
+ it('should display version', async () => {
13
+ const { stdout, exitCode } = await runCli(['--version'])
14
+
15
+ expect(exitCode).toBe(0)
16
+ expect(stdout).toMatch(/\d+\.\d+\.\d+/)
17
+ })
18
+
19
+ it('should display help', async () => {
20
+ const { stdout, exitCode } = await runCli(['--help'])
21
+
22
+ expect(exitCode).toBe(0)
23
+ expect(stdout).toContain('launch77')
24
+ })
25
+ })
@@ -0,0 +1,20 @@
1
+ import { execa } from 'execa'
2
+ import { fileURLToPath } from 'url'
3
+ import { dirname, join } from 'path'
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url))
6
+ const CLI_PATH = join(__dirname, '../../dist/cli.js')
7
+
8
+ export async function runCli(args: string[], options?: { cwd?: string }) {
9
+ const result = await execa('node', [CLI_PATH, ...args], {
10
+ reject: false,
11
+ cwd: options?.cwd,
12
+ env: { ...process.env, CI: 'true' },
13
+ })
14
+
15
+ return {
16
+ stdout: result.stdout,
17
+ stderr: result.stderr,
18
+ exitCode: result.exitCode ?? 0,
19
+ }
20
+ }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'vitest/config'
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ['src/**/*.test.ts'],
6
+ testTimeout: 10000,
7
+ hookTimeout: 10000,
8
+ },
9
+ })
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'vitest/config'
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ['tests/integration/**/*.test.ts'],
6
+ testTimeout: 30000,
7
+ hookTimeout: 60000,
8
+ },
9
+ })