@launch77/cli 1.3.0 ā 1.4.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/CHANGELOG.md +19 -0
- package/dist/cli.js +6 -3
- package/dist/cli.js.map +1 -1
- package/dist/infrastructure/template-generator.d.ts +1 -1
- package/dist/infrastructure/template-generator.d.ts.map +1 -1
- package/dist/infrastructure/template.d.ts +5 -0
- package/dist/infrastructure/template.d.ts.map +1 -1
- package/dist/infrastructure/template.js +11 -0
- package/dist/infrastructure/template.js.map +1 -1
- package/dist/modules/app/commands/create-app.js +1 -1
- package/dist/modules/app/commands/create-app.js.map +1 -1
- package/dist/modules/app/commands/delete-app.js +1 -1
- package/dist/modules/app/commands/delete-app.js.map +1 -1
- package/dist/modules/app/services/app-svc.d.ts +1 -1
- package/dist/modules/app/services/app-svc.d.ts.map +1 -1
- package/dist/modules/app/services/manifest-svc.d.ts +1 -1
- package/dist/modules/app/services/manifest-svc.d.ts.map +1 -1
- package/dist/modules/catalog/config/catalog-config.test.js +1 -1
- package/dist/modules/catalog/config/catalog-config.test.js.map +1 -1
- package/dist/modules/git/commands/git-connect.js +2 -2
- package/dist/modules/git/commands/git-connect.js.map +1 -1
- package/dist/modules/git/errors/git-errors.d.ts +3 -0
- package/dist/modules/git/errors/git-errors.d.ts.map +1 -1
- package/dist/modules/git/errors/git-errors.js +6 -0
- package/dist/modules/git/errors/git-errors.js.map +1 -1
- package/dist/modules/git/index.d.ts +3 -1
- package/dist/modules/git/index.d.ts.map +1 -1
- package/dist/modules/git/index.js +6 -1
- package/dist/modules/git/index.js.map +1 -1
- package/dist/modules/git/services/git-service.d.ts +5 -0
- package/dist/modules/git/services/git-service.d.ts.map +1 -1
- package/dist/modules/git/services/git-service.js +11 -1
- package/dist/modules/git/services/git-service.js.map +1 -1
- package/dist/modules/plugin/commands/plugin-create.d.ts +3 -0
- package/dist/modules/plugin/commands/plugin-create.d.ts.map +1 -0
- package/dist/modules/plugin/commands/plugin-create.js +59 -0
- package/dist/modules/plugin/commands/plugin-create.js.map +1 -0
- package/dist/modules/plugin/commands/plugin-install.d.ts.map +1 -1
- package/dist/modules/plugin/commands/plugin-install.js +9 -24
- package/dist/modules/plugin/commands/plugin-install.js.map +1 -1
- package/dist/modules/plugin/errors/plugin-errors.d.ts +24 -1
- package/dist/modules/plugin/errors/plugin-errors.d.ts.map +1 -1
- package/dist/modules/plugin/errors/plugin-errors.js +79 -6
- package/dist/modules/plugin/errors/plugin-errors.js.map +1 -1
- package/dist/modules/plugin/index.d.ts +4 -2
- package/dist/modules/plugin/index.d.ts.map +1 -1
- package/dist/modules/plugin/index.js +4 -2
- package/dist/modules/plugin/index.js.map +1 -1
- package/dist/modules/plugin/lib/plugin-registry.d.ts +6 -12
- package/dist/modules/plugin/lib/plugin-registry.d.ts.map +1 -1
- package/dist/modules/plugin/lib/plugin-registry.js +13 -30
- package/dist/modules/plugin/lib/plugin-registry.js.map +1 -1
- package/dist/modules/plugin/lib/plugin-resolver.d.ts +76 -0
- package/dist/modules/plugin/lib/plugin-resolver.d.ts.map +1 -0
- package/dist/modules/plugin/lib/plugin-resolver.js +128 -0
- package/dist/modules/plugin/lib/plugin-resolver.js.map +1 -0
- package/dist/modules/plugin/lib/plugin-resolver.test.d.ts +2 -0
- package/dist/modules/plugin/lib/plugin-resolver.test.d.ts.map +1 -0
- package/dist/modules/plugin/lib/plugin-resolver.test.js +175 -0
- package/dist/modules/plugin/lib/plugin-resolver.test.js.map +1 -0
- package/dist/modules/plugin/services/plugin-create-service.d.ts +16 -0
- package/dist/modules/plugin/services/plugin-create-service.d.ts.map +1 -0
- package/dist/modules/plugin/services/plugin-create-service.js +47 -0
- package/dist/modules/plugin/services/plugin-create-service.js.map +1 -0
- package/dist/modules/plugin/services/plugin-svc.d.ts +29 -3
- package/dist/modules/plugin/services/plugin-svc.d.ts.map +1 -1
- package/dist/modules/plugin/services/plugin-svc.js +192 -17
- package/dist/modules/plugin/services/plugin-svc.js.map +1 -1
- package/dist/modules/plugin/services/plugin-svc.test.d.ts +2 -0
- package/dist/modules/plugin/services/plugin-svc.test.d.ts.map +1 -0
- package/dist/modules/plugin/services/plugin-svc.test.js +362 -0
- package/dist/modules/plugin/services/plugin-svc.test.js.map +1 -0
- package/dist/modules/release/commands/release-init.d.ts +3 -0
- package/dist/modules/release/commands/release-init.d.ts.map +1 -0
- package/dist/modules/release/commands/release-init.js +92 -0
- package/dist/modules/release/commands/release-init.js.map +1 -0
- package/dist/modules/release/errors/release-errors.d.ts +7 -0
- package/dist/modules/release/errors/release-errors.d.ts.map +1 -0
- package/dist/modules/release/errors/release-errors.js +13 -0
- package/dist/modules/release/errors/release-errors.js.map +1 -0
- package/dist/modules/release/index.d.ts +4 -0
- package/dist/modules/release/index.d.ts.map +1 -0
- package/dist/modules/release/index.js +7 -0
- package/dist/modules/release/index.js.map +1 -0
- package/dist/modules/release/services/release-service.d.ts +34 -0
- package/dist/modules/release/services/release-service.d.ts.map +1 -0
- package/dist/modules/release/services/release-service.js +154 -0
- package/dist/modules/release/services/release-service.js.map +1 -0
- package/dist/templates/plugin/README.md.hbs +39 -0
- package/dist/templates/plugin/package.json.hbs +34 -0
- package/dist/templates/plugin/plugin.json.hbs +7 -0
- package/dist/templates/plugin/src/generator.ts.hbs +64 -0
- package/dist/templates/plugin/templates/src/.gitkeep +0 -0
- package/dist/templates/plugin/tsconfig.json +10 -0
- package/dist/templates/plugin/tsup.config.ts +9 -0
- package/dist/templates/workspace/.github/workflows/ci.yml +8 -5
- package/dist/templates/workspace/package.json +3 -1
- package/dist/templates/workspace/turbo.json +5 -0
- package/dist/utils/launch77-context.d.ts +1 -1
- package/dist/utils/launch77-context.d.ts.map +1 -1
- package/dist/utils/launch77-context.js +25 -2
- package/dist/utils/launch77-context.js.map +1 -1
- package/dist/utils/launch77-validation.d.ts +1 -1
- package/dist/utils/launch77-validation.d.ts.map +1 -1
- package/dist/utils/string.d.ts +13 -0
- package/dist/utils/string.d.ts.map +1 -0
- package/dist/utils/string.js +18 -0
- package/dist/utils/string.js.map +1 -0
- package/package.json +6 -9
- package/src/cli.ts +7 -3
- package/src/infrastructure/template-generator.ts +1 -1
- package/src/infrastructure/template.ts +14 -0
- package/src/modules/app/commands/create-app.ts +1 -1
- package/src/modules/app/commands/delete-app.ts +1 -1
- package/src/modules/app/services/app-svc.ts +1 -1
- package/src/modules/app/services/manifest-svc.ts +1 -1
- package/src/modules/catalog/config/catalog-config.test.ts +1 -1
- package/src/modules/git/commands/git-connect.ts +2 -2
- package/src/modules/git/errors/git-errors.ts +7 -0
- package/src/modules/git/index.ts +8 -1
- package/src/modules/git/services/git-service.ts +12 -1
- package/src/modules/plugin/commands/plugin-create.ts +68 -0
- package/src/modules/plugin/commands/plugin-install.ts +9 -26
- package/src/modules/plugin/errors/plugin-errors.ts +87 -6
- package/src/modules/plugin/index.ts +4 -2
- package/src/modules/plugin/lib/plugin-registry.ts +14 -37
- package/src/modules/plugin/lib/plugin-resolver.test.ts +215 -0
- package/src/modules/plugin/lib/plugin-resolver.ts +160 -0
- package/src/modules/plugin/services/plugin-create-service.ts +69 -0
- package/src/modules/plugin/services/plugin-svc.test.ts +418 -0
- package/src/modules/plugin/services/plugin-svc.ts +217 -17
- package/src/modules/release/commands/release-init.ts +102 -0
- package/src/modules/release/errors/release-errors.ts +13 -0
- package/src/modules/release/index.ts +8 -0
- package/src/modules/release/services/release-service.ts +170 -0
- package/src/utils/launch77-context.ts +29 -3
- package/src/utils/launch77-validation.ts +1 -1
- package/src/utils/string.ts +17 -0
- package/templates/plugin/README.md.hbs +39 -0
- package/templates/plugin/package.json.hbs +34 -0
- package/templates/plugin/plugin.json.hbs +7 -0
- package/templates/plugin/src/generator.ts.hbs +64 -0
- package/templates/plugin/templates/src/.gitkeep +0 -0
- package/templates/plugin/tsconfig.json +10 -0
- package/templates/plugin/tsup.config.ts +9 -0
- package/templates/workspace/.github/workflows/ci.yml +8 -5
- package/templates/workspace/package.json +3 -1
- package/templates/workspace/turbo.json +5 -0
- package/tests/integration/cli.test.ts +25 -0
- package/tests/integration/setup.ts +20 -0
- package/vitest.config.ts +9 -0
- package/vitest.integration.config.ts +9 -0
- package/dist/modules/git/commands/git-setup-releases.d.ts +0 -3
- package/dist/modules/git/commands/git-setup-releases.d.ts.map +0 -1
- package/dist/modules/git/commands/git-setup-releases.js +0 -128
- package/dist/modules/git/commands/git-setup-releases.js.map +0 -1
- package/launch77-cli-1.2.0.tgz +0 -0
- package/src/modules/git/commands/git-setup-releases.ts +0 -148
- package/src/modules/plugin/lib/launch77-workspace.code-workspace +0 -14
|
@@ -1,34 +1,144 @@
|
|
|
1
1
|
import * as path from 'path'
|
|
2
|
+
import * as fs from 'fs/promises'
|
|
2
3
|
|
|
4
|
+
import chalk from 'chalk'
|
|
3
5
|
import { execa } from 'execa'
|
|
6
|
+
import { readPluginMetadata } from '@launch77/plugin-runtime'
|
|
4
7
|
|
|
5
|
-
import { PluginInstallationError,
|
|
6
|
-
import {
|
|
8
|
+
import { PluginInstallationError, InvalidPluginContextError, createInvalidContextError, MissingPluginTargetsError, createInvalidTargetError, NpmInstallationError, PluginResolutionError } from '../errors/plugin-errors.js'
|
|
9
|
+
import { validatePluginInput, resolvePluginLocation } from '../lib/plugin-resolver.js'
|
|
7
10
|
|
|
8
|
-
import type { Launch77Context } from '
|
|
11
|
+
import type { Launch77Context, Launch77LocationType, Launch77PackageManifest, InstalledPluginMetadata, PluginMetadata } from '@launch77/plugin-runtime'
|
|
9
12
|
import type { InstallPluginRequest, InstallPluginResult } from '../types/plugin-types.js'
|
|
10
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Map location type to target string
|
|
16
|
+
*/
|
|
17
|
+
function locationTypeToTarget(locationType: Launch77LocationType): string | null {
|
|
18
|
+
switch (locationType) {
|
|
19
|
+
case 'workspace-app':
|
|
20
|
+
return 'app'
|
|
21
|
+
case 'workspace-library':
|
|
22
|
+
return 'library'
|
|
23
|
+
case 'workspace-plugin':
|
|
24
|
+
return 'plugin'
|
|
25
|
+
case 'workspace-app-template':
|
|
26
|
+
return 'app-template'
|
|
27
|
+
default:
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
11
32
|
export class PluginService {
|
|
12
33
|
/**
|
|
13
|
-
*
|
|
34
|
+
* Validate that we're in a valid package directory and return the target type
|
|
14
35
|
*/
|
|
15
|
-
|
|
16
|
-
const
|
|
36
|
+
private validateContext(context: Launch77Context): string {
|
|
37
|
+
const currentTarget = locationTypeToTarget(context.locationType)
|
|
38
|
+
if (!currentTarget) throw createInvalidContextError(context.locationType)
|
|
39
|
+
if (!context.appName) throw new InvalidPluginContextError('Could not determine package name. This is a bug. Please report it.')
|
|
40
|
+
return currentTarget
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Validate plugin name, resolve its location, and download if needed
|
|
45
|
+
*/
|
|
46
|
+
private async validateAndResolvePlugin(pluginName: string, workspaceRoot: string, logger: (message: string) => void): Promise<{ pluginPath: string; source: 'local' | 'npm'; npmPackage?: string }> {
|
|
47
|
+
logger(chalk.blue(`\nš Resolving plugin "${pluginName}"...`))
|
|
48
|
+
logger(` āā Validating plugin name...`)
|
|
49
|
+
|
|
50
|
+
const validation = validatePluginInput(pluginName)
|
|
51
|
+
if (!validation.isValid) {
|
|
52
|
+
throw new PluginResolutionError(pluginName, validation.error || 'Invalid plugin name')
|
|
53
|
+
}
|
|
54
|
+
logger(` ā āā ${chalk.green('ā')} Valid plugin name`)
|
|
55
|
+
|
|
56
|
+
logger(` āā Checking local workspace: ${chalk.dim(`plugins/${pluginName}`)}`)
|
|
57
|
+
const resolution = await resolvePluginLocation(pluginName, workspaceRoot)
|
|
58
|
+
|
|
59
|
+
let pluginPath: string
|
|
60
|
+
|
|
61
|
+
if (resolution.source === 'local') {
|
|
62
|
+
logger(` ā āā ${chalk.green('ā')} Found local plugin`)
|
|
63
|
+
pluginPath = resolution.localPath!
|
|
64
|
+
} else {
|
|
65
|
+
logger(` ā āā ${chalk.dim('Not found locally')}`)
|
|
66
|
+
logger(` āā Resolving to npm package: ${chalk.cyan(resolution.npmPackage)}`)
|
|
67
|
+
|
|
68
|
+
pluginPath = await this.downloadNpmPlugin(resolution.npmPackage!, workspaceRoot, logger)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
logger(` āā ${chalk.green('ā')} Plugin resolved\n`)
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
pluginPath,
|
|
75
|
+
source: resolution.source,
|
|
76
|
+
npmPackage: resolution.npmPackage,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Read plugin metadata and validate it supports the current target
|
|
82
|
+
*/
|
|
83
|
+
private async validatePluginTargets(pluginPath: string, pluginName: string, currentTarget: string): Promise<PluginMetadata> {
|
|
84
|
+
const metadata = await readPluginMetadata(pluginPath)
|
|
85
|
+
|
|
86
|
+
if (!metadata.targets || metadata.targets.length === 0) {
|
|
87
|
+
throw new MissingPluginTargetsError(pluginName)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!metadata.targets.includes(currentTarget)) {
|
|
91
|
+
throw createInvalidTargetError(pluginName, currentTarget, metadata.targets)
|
|
92
|
+
}
|
|
17
93
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
94
|
+
return metadata
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check if plugin is already installed and return early-exit result if so
|
|
99
|
+
*/
|
|
100
|
+
private async checkExistingInstallation(pluginName: string, packagePath: string, logger: (message: string) => void): Promise<InstallPluginResult | null> {
|
|
101
|
+
const existingInstallation = await this.isPluginInstalled(pluginName, packagePath)
|
|
102
|
+
if (existingInstallation) {
|
|
103
|
+
logger(chalk.yellow(`\nā¹ļø Plugin '${pluginName}' is already installed in this package.\n`))
|
|
104
|
+
logger(`Package: ${chalk.cyan(existingInstallation.package)} (${existingInstallation.source})`)
|
|
105
|
+
logger(`Version: ${existingInstallation.version}`)
|
|
106
|
+
logger(`Installed: ${existingInstallation.installedAt}\n`)
|
|
107
|
+
logger(chalk.gray('To reinstall: Remove from package.json launch77.installedPlugins'))
|
|
108
|
+
logger(chalk.gray('(plugin:remove command coming soon)\n'))
|
|
109
|
+
return {
|
|
110
|
+
pluginName,
|
|
111
|
+
filesInstalled: false,
|
|
112
|
+
packageJsonUpdated: false,
|
|
113
|
+
dependenciesInstalled: false,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Install a plugin to the current package
|
|
121
|
+
*/
|
|
122
|
+
async installPlugin(request: InstallPluginRequest, context: Launch77Context, logger: (message: string) => void = console.log): Promise<InstallPluginResult> {
|
|
123
|
+
const { pluginName } = request
|
|
21
124
|
|
|
22
|
-
|
|
23
|
-
const
|
|
125
|
+
const currentTarget = this.validateContext(context)
|
|
126
|
+
const { pluginPath, source, npmPackage } = await this.validateAndResolvePlugin(pluginName, context.workspaceRoot, logger)
|
|
127
|
+
const metadata = await this.validatePluginTargets(pluginPath, pluginName, currentTarget)
|
|
24
128
|
|
|
25
|
-
|
|
26
|
-
|
|
129
|
+
const packagePath = this.getPackagePath(context)
|
|
130
|
+
const earlyExit = await this.checkExistingInstallation(pluginName, packagePath, logger)
|
|
131
|
+
if (earlyExit) return earlyExit
|
|
27
132
|
|
|
28
|
-
|
|
133
|
+
await this.runGenerator(pluginPath, packagePath, context)
|
|
29
134
|
|
|
30
|
-
|
|
31
|
-
await this.
|
|
135
|
+
const packageName = source === 'npm' ? npmPackage! : pluginName
|
|
136
|
+
await this.writePluginManifest(packagePath, {
|
|
137
|
+
pluginName,
|
|
138
|
+
packageName,
|
|
139
|
+
version: metadata.version,
|
|
140
|
+
source,
|
|
141
|
+
})
|
|
32
142
|
|
|
33
143
|
return {
|
|
34
144
|
pluginName,
|
|
@@ -38,11 +148,50 @@ export class PluginService {
|
|
|
38
148
|
}
|
|
39
149
|
}
|
|
40
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Download and install an npm plugin package
|
|
153
|
+
*/
|
|
154
|
+
private async downloadNpmPlugin(npmPackage: string, workspaceRoot: string, logger: (message: string) => void): Promise<string> {
|
|
155
|
+
logger(` āā Installing from npm: ${chalk.cyan(npmPackage)}...`)
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
// Install the npm package to the workspace
|
|
159
|
+
await execa('npm', ['install', npmPackage, '--save-dev'], {
|
|
160
|
+
cwd: workspaceRoot,
|
|
161
|
+
stdio: 'pipe', // Capture output for clean logging
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
// Return path to installed plugin in node_modules
|
|
165
|
+
const pluginPath = path.join(workspaceRoot, 'node_modules', npmPackage)
|
|
166
|
+
return pluginPath
|
|
167
|
+
} catch (error) {
|
|
168
|
+
throw new NpmInstallationError(npmPackage, error instanceof Error ? error : undefined)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private getPackagePath(context: Launch77Context): string {
|
|
173
|
+
// Determine the base directory based on location type
|
|
174
|
+
switch (context.locationType) {
|
|
175
|
+
case 'workspace-app':
|
|
176
|
+
return path.join(context.appsDir, context.appName!)
|
|
177
|
+
case 'workspace-library':
|
|
178
|
+
return path.join(context.workspaceRoot, 'libraries', context.appName!)
|
|
179
|
+
case 'workspace-plugin':
|
|
180
|
+
return path.join(context.workspaceRoot, 'plugins', context.appName!)
|
|
181
|
+
case 'workspace-app-template':
|
|
182
|
+
return path.join(context.workspaceRoot, 'app-templates', context.appName!)
|
|
183
|
+
default:
|
|
184
|
+
throw new InvalidPluginContextError(`Cannot install plugin from ${context.locationType}`)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
41
188
|
private async runGenerator(pluginPath: string, appPath: string, context: Launch77Context): Promise<void> {
|
|
42
189
|
try {
|
|
43
190
|
const generatorPath = path.join(pluginPath, 'dist/generator.js')
|
|
44
191
|
|
|
45
|
-
|
|
192
|
+
const args = [generatorPath, `--appPath=${appPath}`, `--appName=${context.appName}`, `--workspaceName=${context.workspaceName}`, `--pluginPath=${pluginPath}`]
|
|
193
|
+
|
|
194
|
+
await execa('node', args, {
|
|
46
195
|
cwd: pluginPath,
|
|
47
196
|
stdio: 'inherit',
|
|
48
197
|
})
|
|
@@ -50,4 +199,55 @@ export class PluginService {
|
|
|
50
199
|
throw new PluginInstallationError(`Generator failed: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined)
|
|
51
200
|
}
|
|
52
201
|
}
|
|
202
|
+
|
|
203
|
+
private async isPluginInstalled(pluginName: string, packagePath: string): Promise<InstalledPluginMetadata | null> {
|
|
204
|
+
try {
|
|
205
|
+
const packageJsonPath = path.join(packagePath, 'package.json')
|
|
206
|
+
const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8')
|
|
207
|
+
const packageJson = JSON.parse(packageJsonContent)
|
|
208
|
+
|
|
209
|
+
const manifest = packageJson.launch77 as Launch77PackageManifest | undefined
|
|
210
|
+
|
|
211
|
+
if (manifest?.installedPlugins?.[pluginName]) {
|
|
212
|
+
return manifest.installedPlugins[pluginName]
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return null
|
|
216
|
+
} catch (error) {
|
|
217
|
+
// If package.json doesn't exist or can't be read, assume not installed
|
|
218
|
+
return null
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Write plugin installation metadata to the target package's package.json
|
|
224
|
+
*/
|
|
225
|
+
private async writePluginManifest(packagePath: string, installationInfo: { pluginName: string; packageName: string; version: string; source: 'local' | 'npm' }): Promise<void> {
|
|
226
|
+
try {
|
|
227
|
+
const packageJsonPath = path.join(packagePath, 'package.json')
|
|
228
|
+
const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8')
|
|
229
|
+
const packageJson = JSON.parse(packageJsonContent)
|
|
230
|
+
|
|
231
|
+
if (!packageJson.launch77) {
|
|
232
|
+
packageJson.launch77 = {}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!packageJson.launch77.installedPlugins) {
|
|
236
|
+
packageJson.launch77.installedPlugins = {}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const manifest = packageJson.launch77 as Launch77PackageManifest
|
|
240
|
+
|
|
241
|
+
manifest.installedPlugins![installationInfo.pluginName] = {
|
|
242
|
+
package: installationInfo.packageName,
|
|
243
|
+
version: installationInfo.version,
|
|
244
|
+
installedAt: new Date().toISOString(),
|
|
245
|
+
source: installationInfo.source,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf-8')
|
|
249
|
+
} catch (error) {
|
|
250
|
+
throw new PluginInstallationError(`Failed to write plugin manifest: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
53
253
|
}
|
|
@@ -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
|
-
//
|
|
88
|
-
|
|
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
|
|
|
@@ -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
|