@launch77-shared/plugin-release 1.0.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.
@@ -0,0 +1,285 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/commands/connect.ts
4
+ import { execSync } from "child_process";
5
+ import { readFileSync, writeFileSync, existsSync, unlinkSync } from "fs";
6
+ import { join } from "path";
7
+ import readline from "readline";
8
+ import { select } from "@inquirer/prompts";
9
+ import { detectLaunch77Context } from "@launch77/plugin-runtime";
10
+ var colors = {
11
+ reset: "\x1B[0m",
12
+ red: "\x1B[31m",
13
+ green: "\x1B[32m",
14
+ yellow: "\x1B[33m",
15
+ cyan: "\x1B[36m",
16
+ gray: "\x1B[90m"
17
+ };
18
+ function log(message, color = "reset") {
19
+ console.log(`${colors[color]}${message}${colors.reset}`);
20
+ }
21
+ function prompt(question) {
22
+ const rl = readline.createInterface({
23
+ input: process.stdin,
24
+ output: process.stdout
25
+ });
26
+ return new Promise((resolve) => {
27
+ rl.question(`${colors.cyan}${question}${colors.reset} `, (answer) => {
28
+ rl.close();
29
+ resolve(answer.trim().toLowerCase());
30
+ });
31
+ });
32
+ }
33
+ async function main() {
34
+ log("\n\u{1F680} Release Connect - Setup npm Package Publishing\n", "cyan");
35
+ const context = await detectLaunch77Context(process.cwd());
36
+ if (!context.isValid || context.locationType === "workspace-root" || !context.appName) {
37
+ log("\u274C Error: Must be run from within an app directory", "red");
38
+ log(" Run this command from inside your app (e.g., apps/my-app)\n", "gray");
39
+ process.exit(1);
40
+ }
41
+ const dirMap = {
42
+ "workspace-app": "apps",
43
+ "workspace-library": "libraries",
44
+ "workspace-plugin": "plugins",
45
+ "workspace-app-template": "app-templates"
46
+ };
47
+ const dir = dirMap[context.locationType];
48
+ if (!dir) {
49
+ log("\u274C Error: Unsupported location type", "red");
50
+ log(` Location type: ${context.locationType}
51
+ `, "gray");
52
+ process.exit(1);
53
+ }
54
+ const packageRoot = join(context.workspaceRoot, dir, context.appName);
55
+ const packageJsonPath = join(packageRoot, "package.json");
56
+ if (!existsSync(packageJsonPath)) {
57
+ log("\u274C Error: package.json not found", "red");
58
+ log(` Looking in: ${packageRoot}
59
+ `, "gray");
60
+ process.exit(1);
61
+ }
62
+ let packageJson;
63
+ try {
64
+ packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
65
+ } catch (error) {
66
+ log("\u274C Error: Could not parse package.json", "red");
67
+ log(` ${error instanceof Error ? error.message : "Unknown error"}
68
+ `, "gray");
69
+ process.exit(1);
70
+ }
71
+ const packageName = packageJson.name;
72
+ log(`\u{1F4E6} Package: ${packageName}
73
+ `);
74
+ log("Step 1: Validating package.json configuration", "cyan");
75
+ const validations = [];
76
+ if (packageJson.private === true) {
77
+ log(" \u2139 Package is currently private (will be changed during setup)", "gray");
78
+ } else if (packageJson.private === false) {
79
+ log(" \u26A0\uFE0F Package is already marked as non-private", "yellow");
80
+ }
81
+ if (packageJson.license) {
82
+ log(` \u2139 Current license: ${packageJson.license}`, "gray");
83
+ } else {
84
+ log(" \u2139 No license set", "gray");
85
+ }
86
+ if (packageJson.publishConfig?.access === "public") {
87
+ log(" \u2713 publishConfig.access: public", "green");
88
+ } else {
89
+ log(' \u2139 publishConfig.access will be set to "public"', "gray");
90
+ }
91
+ if (validations.length > 0) {
92
+ log("\n\u26A0\uFE0F Configuration issues found:", "yellow");
93
+ validations.forEach((v) => log(` - ${v}`, "gray"));
94
+ log("");
95
+ const answer = await prompt("Continue anyway? (y/N)");
96
+ if (answer !== "y" && answer !== "yes") {
97
+ log("Aborted.\n", "gray");
98
+ process.exit(0);
99
+ }
100
+ }
101
+ log("");
102
+ log("Step 1.5: Select package license", "cyan");
103
+ log(" Learn more about licenses: https://spdx.org/licenses/", "gray");
104
+ log("");
105
+ const selectedLicense = await select({
106
+ message: "Select package license:",
107
+ choices: [
108
+ {
109
+ name: "UNLICENSED (proprietary, no permission to use)",
110
+ value: "UNLICENSED",
111
+ description: "For proprietary/closed-source software. Launch77 default."
112
+ },
113
+ {
114
+ name: "MIT (permissive, most popular open source)",
115
+ value: "MIT",
116
+ description: "Allows commercial use with minimal restrictions."
117
+ },
118
+ {
119
+ name: "ISC (permissive, similar to MIT)",
120
+ value: "ISC",
121
+ description: "Functionally identical to MIT with simpler wording."
122
+ },
123
+ {
124
+ name: "Apache-2.0 (permissive with patent protection)",
125
+ value: "Apache-2.0",
126
+ description: "Like MIT but includes explicit patent rights."
127
+ },
128
+ {
129
+ name: "GPL-3.0 (copyleft, derivatives must be GPL)",
130
+ value: "GPL-3.0",
131
+ description: "Requires derivative works to also be open source."
132
+ },
133
+ {
134
+ name: "BSD-3-Clause (permissive with attribution)",
135
+ value: "BSD-3-Clause",
136
+ description: "Simple permissive license with name protection."
137
+ }
138
+ ],
139
+ default: packageJson.license || "UNLICENSED"
140
+ });
141
+ log("");
142
+ log("\u2139\uFE0F Note: License choice is separate from npm package visibility.", "cyan");
143
+ log(" \u2022 License = what others can legally do with your code", "gray");
144
+ log(" \u2022 Package access = who can download from npm (public vs private)", "gray");
145
+ log(" \u2022 This package will be published as PUBLIC on npm", "gray");
146
+ log("");
147
+ log("Step 2: Building the package", "cyan");
148
+ if (packageJson.scripts?.build) {
149
+ try {
150
+ execSync("npm run build", { cwd: packageRoot, stdio: "inherit" });
151
+ log(" \u2713 Build succeeded\n", "green");
152
+ } catch (error) {
153
+ log(" \u2717 Build failed", "red");
154
+ log(" Fix build errors before publishing\n", "gray");
155
+ process.exit(1);
156
+ }
157
+ } else {
158
+ log(" \u2139 No build script - skipping build step", "yellow");
159
+ log(" This is normal for app-templates and some packages\n", "gray");
160
+ }
161
+ log("Step 3: Creating test tarball", "cyan");
162
+ let tarballPath;
163
+ try {
164
+ const output = execSync("npm pack", { cwd: packageRoot, encoding: "utf-8" });
165
+ tarballPath = join(packageRoot, output.trim());
166
+ log(` \u2713 Created: ${tarballPath}
167
+ `, "green");
168
+ } catch (error) {
169
+ log(" \u2717 Failed to create tarball", "red");
170
+ log(` ${error instanceof Error ? error.message : "Unknown error"}
171
+ `, "gray");
172
+ process.exit(1);
173
+ }
174
+ log("Step 4: Preparing package.json for publishing", "cyan");
175
+ let packageJsonModified = false;
176
+ if (packageJson.license !== selectedLicense) {
177
+ packageJson.license = selectedLicense;
178
+ log(` \u2713 Set license to "${selectedLicense}"`, "green");
179
+ packageJsonModified = true;
180
+ } else {
181
+ log(` \u2713 License already set to "${selectedLicense}"`, "green");
182
+ }
183
+ if (packageJson.private === true) {
184
+ delete packageJson.private;
185
+ log(' \u2713 Removed "private": true', "green");
186
+ packageJsonModified = true;
187
+ }
188
+ if (!packageJson.publishConfig) {
189
+ packageJson.publishConfig = {};
190
+ }
191
+ if (packageJson.publishConfig.access !== "public") {
192
+ packageJson.publishConfig.access = "public";
193
+ log(' \u2713 Set publishConfig.access to "public"', "green");
194
+ packageJsonModified = true;
195
+ }
196
+ if (packageJsonModified) {
197
+ try {
198
+ writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n");
199
+ log(" \u2713 Updated package.json\n", "green");
200
+ } catch (error) {
201
+ log(" \u2717 Failed to update package.json", "red");
202
+ log(` ${error instanceof Error ? error.message : "Unknown error"}
203
+ `, "gray");
204
+ process.exit(1);
205
+ }
206
+ } else {
207
+ log(" \u2713 package.json already configured\n", "green");
208
+ }
209
+ log("Step 5: Checking if package is published to npm", "cyan");
210
+ let isPublished = false;
211
+ try {
212
+ execSync(`npm view ${packageName}`, { stdio: "pipe" });
213
+ isPublished = true;
214
+ log(` \u2713 Package is already published to npm
215
+ `, "green");
216
+ } catch (error) {
217
+ const errorMessage = error instanceof Error ? error.message : String(error);
218
+ const is404 = errorMessage.includes("404") || errorMessage.includes("Not Found") || errorMessage.includes("code E404");
219
+ if (is404) {
220
+ log(` \u2139 Package not yet published to npm
221
+ `, "gray");
222
+ } else {
223
+ log(` \u26A0\uFE0F Error checking npm publication status
224
+ `, "yellow");
225
+ log(` ${errorMessage}
226
+ `, "gray");
227
+ }
228
+ }
229
+ if (!isPublished) {
230
+ log("Step 6: Publishing package to npm", "cyan");
231
+ log(" This is the initial publish required for Trusted Publishers setup.", "gray");
232
+ log("");
233
+ const answer = await prompt("Run npm publish now? (Y/n)");
234
+ if (answer === "" || answer === "y" || answer === "yes") {
235
+ try {
236
+ execSync("npm publish", { cwd: packageRoot, stdio: "inherit" });
237
+ log("\n \u2713 Package published successfully!\n", "green");
238
+ isPublished = true;
239
+ } catch (error) {
240
+ log("\n \u2717 Publish failed", "red");
241
+ log(" You may need to:\n", "gray");
242
+ log(" - Create the npm organization first: https://www.npmjs.com/org/create", "gray");
243
+ log(" - Log in to npm: npm login\n", "gray");
244
+ process.exit(1);
245
+ }
246
+ } else {
247
+ log(" Skipped. You can publish manually later with: npm publish\n", "gray");
248
+ }
249
+ }
250
+ if (isPublished) {
251
+ log("Step 7: Configure npm Trusted Publishing", "cyan");
252
+ log("\n Next, you need to configure Trusted Publishing on npm:\n", "gray");
253
+ log(" 1. Visit your package access page:", "gray");
254
+ log(` https://www.npmjs.com/package/${packageName}/access
255
+ `, "cyan");
256
+ log(' 2. Scroll to "Trusted Publisher" section', "gray");
257
+ log(' 3. Select "Github Actions" as the publisher', "gray");
258
+ log(" 4. Configure Github Actions publisher with:", "gray");
259
+ log(" - Organization or owner: <your-github-org>", "gray");
260
+ log(" - Repository: <your-repo-name>", "gray");
261
+ log(" - Workflow filename: ci.yml", "gray");
262
+ log(" - Environment: (leave empty)\n", "gray");
263
+ log(" Full documentation: https://docs.npmjs.com/trusted-publishers\n", "gray");
264
+ await prompt("Press Enter when you have completed the Trusted Publisher setup...");
265
+ }
266
+ if (tarballPath && existsSync(tarballPath)) {
267
+ try {
268
+ unlinkSync(tarballPath);
269
+ log(`
270
+ \u2713 Cleaned up tarball: ${tarballPath}`, "gray");
271
+ } catch (error) {
272
+ }
273
+ }
274
+ log("\n\u2705 Release setup complete!\n", "green");
275
+ log("Next steps:", "cyan");
276
+ log("1. Verify setup with: npm run release:verify", "gray");
277
+ log("2. Create a changeset: npm run changeset", "gray");
278
+ log("3. Push to GitHub - CI will handle automated publishing\n", "gray");
279
+ }
280
+ main().catch((error) => {
281
+ log(`
282
+ \u274C Unexpected error: ${error.message}
283
+ `, "red");
284
+ process.exit(1);
285
+ });
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/commands/verify.ts
4
+ import { execSync } from "child_process";
5
+ import { readFileSync, existsSync } from "fs";
6
+ import { join } from "path";
7
+ import { detectLaunch77Context } from "@launch77/plugin-runtime";
8
+ var colors = {
9
+ reset: "\x1B[0m",
10
+ red: "\x1B[31m",
11
+ green: "\x1B[32m",
12
+ yellow: "\x1B[33m",
13
+ cyan: "\x1B[36m",
14
+ gray: "\x1B[90m"
15
+ };
16
+ function log(message, color = "reset") {
17
+ console.log(`${colors[color]}${message}${colors.reset}`);
18
+ }
19
+ async function main() {
20
+ log("\n\u{1F50D} Release Verify - Check npm Package Setup\n", "cyan");
21
+ const context = await detectLaunch77Context(process.cwd());
22
+ if (!context.isValid || context.locationType === "workspace-root" || !context.appName) {
23
+ log("\u274C Error: Must be run from within an app directory", "red");
24
+ log(" Run this command from inside your app (e.g., apps/my-app)\n", "gray");
25
+ process.exit(1);
26
+ }
27
+ const dirMap = {
28
+ "workspace-app": "apps",
29
+ "workspace-library": "libraries",
30
+ "workspace-plugin": "plugins",
31
+ "workspace-app-template": "app-templates"
32
+ };
33
+ const dir = dirMap[context.locationType];
34
+ if (!dir) {
35
+ log("\u274C Error: Unsupported location type", "red");
36
+ log(` Location type: ${context.locationType}
37
+ `, "gray");
38
+ process.exit(1);
39
+ }
40
+ const packageRoot = join(context.workspaceRoot, dir, context.appName);
41
+ const packageJsonPath = join(packageRoot, "package.json");
42
+ if (!existsSync(packageJsonPath)) {
43
+ log("\u274C Error: package.json not found", "red");
44
+ log(` Looking in: ${packageRoot}
45
+ `, "gray");
46
+ process.exit(1);
47
+ }
48
+ let packageJson;
49
+ try {
50
+ packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
51
+ } catch (error) {
52
+ log("\u274C Error: Could not parse package.json", "red");
53
+ log(` ${error instanceof Error ? error.message : "Unknown error"}
54
+ `, "gray");
55
+ process.exit(1);
56
+ }
57
+ const packageName = packageJson.name;
58
+ log(`\u{1F4E6} Package: ${packageName}
59
+ `);
60
+ const checks = [];
61
+ let allPassed = true;
62
+ log("Checking package.json configuration...", "cyan");
63
+ if (packageJson.private === true) {
64
+ log(" \u2717 Package is marked as private", "red");
65
+ log(" This prevents publishing to npm", "gray");
66
+ checks.push({ name: "Package not private", passed: false, action: "Run: npm run release:connect" });
67
+ allPassed = false;
68
+ } else {
69
+ log(" \u2713 Package is not private", "green");
70
+ checks.push({ name: "Package not private", passed: true });
71
+ }
72
+ if (packageJson.license === "UNLICENSED") {
73
+ log(" \u2713 License: UNLICENSED", "green");
74
+ checks.push({ name: "License field", passed: true });
75
+ } else {
76
+ log(' \u2717 License should be "UNLICENSED"', "red");
77
+ log(" Current: " + (packageJson.license || "not set"), "gray");
78
+ checks.push({ name: "License field", passed: false, action: 'Set "license": "UNLICENSED"' });
79
+ allPassed = false;
80
+ }
81
+ if (packageJson.publishConfig?.access === "public") {
82
+ log(" \u2713 publishConfig.access: public", "green");
83
+ checks.push({ name: "PublishConfig.access", passed: true });
84
+ } else {
85
+ log(' \u2717 publishConfig.access should be "public"', "red");
86
+ log(" Current: " + (packageJson.publishConfig?.access || "not set"), "gray");
87
+ checks.push({ name: "PublishConfig.access", passed: false, action: 'Add "publishConfig": { "access": "public" }' });
88
+ allPassed = false;
89
+ }
90
+ if (packageJson.main) {
91
+ log(" \u2713 Main entry point defined", "green");
92
+ checks.push({ name: "Main entry point", passed: true });
93
+ } else {
94
+ log(" \u2717 Main entry point not defined", "yellow");
95
+ checks.push({ name: "Main entry point", passed: false, action: 'Add "main" field if needed' });
96
+ }
97
+ log("");
98
+ log("Checking npm publication status...", "cyan");
99
+ let isPublished = false;
100
+ try {
101
+ execSync(`npm view ${packageName}`, { stdio: "pipe" });
102
+ isPublished = true;
103
+ log(" \u2713 Package is published to npm", "green");
104
+ checks.push({ name: "Published to npm", passed: true });
105
+ } catch (error) {
106
+ const errorMessage = error instanceof Error ? error.message : String(error);
107
+ const is404 = errorMessage.includes("404") || errorMessage.includes("Not Found") || errorMessage.includes("code E404");
108
+ if (is404) {
109
+ log(" \u2717 Package not found on npm", "red");
110
+ log(" Note: New packages can take several minutes to propagate through npm's CDN", "gray");
111
+ } else {
112
+ log(" \u2717 Error checking npm publication status", "red");
113
+ log(` ${errorMessage}`, "gray");
114
+ }
115
+ checks.push({ name: "Published to npm", passed: false, action: "Run: npm run release:connect" });
116
+ allPassed = false;
117
+ }
118
+ log("");
119
+ log("Checking build...", "cyan");
120
+ if (packageJson.scripts?.build) {
121
+ try {
122
+ execSync("npm run build", { cwd: packageRoot, stdio: "pipe" });
123
+ log(" \u2713 Build succeeds", "green");
124
+ checks.push({ name: "Build succeeds", passed: true });
125
+ } catch (error) {
126
+ log(" \u2717 Build fails", "red");
127
+ checks.push({ name: "Build succeeds", passed: false, action: "Fix build errors" });
128
+ allPassed = false;
129
+ }
130
+ } else {
131
+ log(" \u26A0 No build script defined", "yellow");
132
+ checks.push({ name: "Build script", passed: true, warning: true });
133
+ }
134
+ log("");
135
+ log("\u2500".repeat(60), "gray");
136
+ log("\nSummary:\n", "cyan");
137
+ const passedChecks = checks.filter((c) => c.passed).length;
138
+ const totalChecks = checks.length;
139
+ if (allPassed) {
140
+ log(`\u2705 All checks passed! (${passedChecks}/${totalChecks})
141
+ `, "green");
142
+ log("Your package is ready for automated publishing via GitHub Actions.\n", "gray");
143
+ log("Next steps:", "cyan");
144
+ log("1. Create a changeset: npm run changeset", "gray");
145
+ log("2. Commit and push to main branch", "gray");
146
+ log('3. CI will automatically create a "Version Packages" PR', "gray");
147
+ log("4. Merge the PR to publish to npm\n", "gray");
148
+ } else {
149
+ log(`\u26A0\uFE0F Some checks failed (${passedChecks}/${totalChecks} passed)
150
+ `, "yellow");
151
+ log("Action items:\n", "cyan");
152
+ checks.filter((c) => !c.passed).forEach((c) => {
153
+ log(`\u2022 ${c.name}:`, "red");
154
+ log(` ${c.action}
155
+ `, "gray");
156
+ });
157
+ if (!isPublished) {
158
+ log("To complete setup, run:", "cyan");
159
+ log(" npm run release:connect\n", "gray");
160
+ }
161
+ }
162
+ }
163
+ main().catch((error) => {
164
+ log(`
165
+ \u274C Unexpected error: ${error.message}
166
+ `, "red");
167
+ process.exit(1);
168
+ });
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/generator.ts
4
+ import * as fs from "fs";
5
+ import * as path from "path";
6
+ import chalk from "chalk";
7
+ import { StandardGenerator } from "@launch77/plugin-runtime";
8
+ var ReleaseGenerator = class extends StandardGenerator {
9
+ constructor(context) {
10
+ super(context);
11
+ }
12
+ async injectCode() {
13
+ console.log(chalk.cyan("\u{1F527} Configuring release automation...\n"));
14
+ await this.addReleaseScripts();
15
+ }
16
+ async addReleaseScripts() {
17
+ const packageJsonPath = path.join(this.context.appPath, "package.json");
18
+ try {
19
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
20
+ const releaseScripts = {
21
+ "release:connect": "launch77-release-connect",
22
+ "release:verify": "launch77-release-verify"
23
+ };
24
+ packageJson.scripts = {
25
+ ...packageJson.scripts,
26
+ ...releaseScripts
27
+ };
28
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n");
29
+ console.log(chalk.green(" \u2713 Added release scripts to package.json"));
30
+ console.log(chalk.gray(" - release:connect (setup npm publishing)"));
31
+ console.log(chalk.gray(" - release:verify (verify setup)\n"));
32
+ } catch (error) {
33
+ console.log(chalk.yellow(` \u26A0\uFE0F Could not update package.json: ${error instanceof Error ? error.message : "Unknown error"}`));
34
+ console.log(chalk.gray(" You may need to add the scripts manually.\n"));
35
+ }
36
+ }
37
+ showNextSteps() {
38
+ console.log(chalk.white("\n" + "\u2500".repeat(60) + "\n"));
39
+ console.log(chalk.cyan("\u{1F4CB} Release Plugin Installed!\n"));
40
+ console.log(chalk.white("Next Steps:\n"));
41
+ console.log(chalk.gray("1. Set up this package for npm publishing:"));
42
+ console.log(chalk.cyan(" npm run release:connect\n"));
43
+ console.log(chalk.gray("2. Verify your setup anytime with:"));
44
+ console.log(chalk.cyan(" npm run release:verify\n"));
45
+ console.log(chalk.gray("3. After setup, create changesets for releases:"));
46
+ console.log(chalk.cyan(" npm run changeset\n"));
47
+ console.log(chalk.white("Documentation:\n"));
48
+ console.log(chalk.gray("See src/modules/release/README.md for detailed instructions.\n"));
49
+ }
50
+ };
51
+ async function main() {
52
+ const args = process.argv.slice(2);
53
+ const appPath = args.find((arg) => arg.startsWith("--appPath="))?.split("=")[1];
54
+ const appName = args.find((arg) => arg.startsWith("--appName="))?.split("=")[1];
55
+ const workspaceName = args.find((arg) => arg.startsWith("--workspaceName="))?.split("=")[1];
56
+ const pluginPath = args.find((arg) => arg.startsWith("--pluginPath="))?.split("=")[1];
57
+ if (!appPath || !appName || !workspaceName || !pluginPath) {
58
+ console.error(chalk.red("Error: Missing required arguments"));
59
+ console.error(chalk.gray("Usage: --appPath=<path> --appName=<name> --workspaceName=<name> --pluginPath=<path>"));
60
+ process.exit(1);
61
+ }
62
+ const generator = new ReleaseGenerator({ appPath, appName, workspaceName, pluginPath });
63
+ await generator.run();
64
+ }
65
+ if (import.meta.url === `file://${process.argv[1]}`) {
66
+ main().catch((error) => {
67
+ console.error(chalk.red("\n\u274C Error during release plugin setup:"));
68
+ console.error(error);
69
+ process.exit(1);
70
+ });
71
+ }
72
+ export {
73
+ ReleaseGenerator
74
+ };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@launch77-shared/plugin-release",
3
+ "version": "1.0.0",
4
+ "description": "Launch77 release plugin - Setup npm packages for automated publishing",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "main": "dist/generator.js",
8
+ "bin": {
9
+ "generate": "./dist/generator.js",
10
+ "launch77-release-connect": "./dist/commands/connect.js",
11
+ "launch77-release-verify": "./dist/commands/verify.js"
12
+ },
13
+ "files": [
14
+ "dist/",
15
+ "templates/",
16
+ "plugin.json"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup",
20
+ "dev": "tsup --watch",
21
+ "typecheck": "tsc --noEmit",
22
+ "lint": "eslint src/**/*.ts",
23
+ "test:integration": "vitest run --config vitest.integration.config.ts",
24
+ "release:connect": "launch77-release-connect",
25
+ "release:verify": "launch77-release-verify"
26
+ },
27
+ "dependencies": {
28
+ "@inquirer/prompts": "^8.0.0",
29
+ "@launch77/plugin-runtime": "^0.3.0",
30
+ "chalk": "^5.3.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^20.10.0",
34
+ "execa": "^9.0.0",
35
+ "fs-extra": "^11.2.0",
36
+ "tsup": "^8.0.0",
37
+ "typescript": "^5.3.0",
38
+ "vitest": "^4.0.16"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "launch77": {
44
+ "installedPlugins": {
45
+ "release": {
46
+ "package": "release",
47
+ "version": "1.0.0",
48
+ "installedAt": "2026-01-23T16:49:37.475Z",
49
+ "source": "local"
50
+ }
51
+ }
52
+ }
53
+ }
package/plugin.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "targets": ["app", "library", "plugin", "app-template"],
3
+ "pluginDependencies": {},
4
+ "libraryDependencies": {}
5
+ }
@@ -0,0 +1,164 @@
1
+ # Release Module
2
+
3
+ This module provides tools to set up and verify npm package publishing with Trusted Publishers.
4
+
5
+ ## Overview
6
+
7
+ The release module automates the setup process for publishing packages to npm using GitHub Actions with Trusted Publishers (OIDC). This eliminates the need for NPM_TOKEN secrets and provides a more secure publishing workflow.
8
+
9
+ **Important:** All packages start as `"private": true` by default. This prevents changesets from attempting to publish unprepared packages. Running `release:connect` removes the private flag and configures the package for publishing.
10
+
11
+ ## Available Scripts
12
+
13
+ ### `npm run release:connect`
14
+
15
+ Interactive script that guides you through the initial npm publishing setup:
16
+
17
+ 1. **Validates package.json** - Checks that license and publishConfig are correct
18
+ 2. **Builds the package** - Runs `npm run build` to ensure it compiles
19
+ 3. **Creates test tarball** - Runs `npm pack` to create a .tgz file
20
+ 4. **Prepares for publishing** - Removes `"private": true` and sets `"publishConfig.access": "public"`
21
+ 5. **Publishes to npm** - Performs the initial `npm publish` (required for Trusted Publishers)
22
+ 6. **Guides Trusted Publisher setup** - Provides step-by-step instructions
23
+
24
+ **When to run:** Once per package, before setting up automated releases.
25
+
26
+ ```bash
27
+ npm run release:connect
28
+ ```
29
+
30
+ ### `npm run release:verify`
31
+
32
+ Verification script that checks if your package is properly configured:
33
+
34
+ - ✓ Checks package is not private
35
+ - ✓ Checks package.json configuration
36
+ - ✓ Verifies package is published to npm
37
+ - ✓ Validates build succeeds
38
+ - ✓ Shows action items if anything is missing
39
+
40
+ **When to run:** Anytime you want to verify your package is ready for publishing.
41
+
42
+ ```bash
43
+ npm run release:verify
44
+ ```
45
+
46
+ ## Setup Process
47
+
48
+ ### Prerequisites
49
+
50
+ Before running `release:connect`, ensure:
51
+
52
+ 1. **npm organization exists** - Create at https://www.npmjs.com/org/create
53
+ 2. **Workspace is connected to GitHub** - Run `launch77 git:connect`
54
+ 3. **Release workflow initialized** - Run `launch77 release:init`
55
+ 4. **You're logged into npm** - Run `npm login`
56
+
57
+ ### Initial Setup
58
+
59
+ 1. Navigate to your package directory:
60
+
61
+ ```bash
62
+ cd apps/my-app
63
+ ```
64
+
65
+ 2. Run the connect script:
66
+
67
+ ```bash
68
+ npm run release:connect
69
+ ```
70
+
71
+ 3. Follow the interactive prompts:
72
+ - Confirm package.json settings
73
+ - Build the package
74
+ - Publish to npm
75
+ - Configure Trusted Publisher on npm website
76
+
77
+ 4. Verify setup:
78
+ ```bash
79
+ npm run release:verify
80
+ ```
81
+
82
+ ## Trusted Publishers Setup
83
+
84
+ After running `release:connect`, you need to configure Trusted Publishers on the npm website:
85
+
86
+ 1. Visit your package access page:
87
+
88
+ ```
89
+ https://www.npmjs.com/package/<your-package-name>/access
90
+ ```
91
+
92
+ 2. Scroll to "Publishing Access" section
93
+
94
+ 3. Click "Configure Trusted Publishers"
95
+
96
+ 4. Add GitHub Actions as a trusted publisher:
97
+ - **Provider:** GitHub Actions
98
+ - **Repository owner:** your-github-org
99
+ - **Repository name:** your-repo-name
100
+ - **Workflow:** .github/workflows/ci.yml
101
+ - **Environment:** (leave empty for any)
102
+
103
+ 5. Save the configuration
104
+
105
+ Full documentation: https://docs.npmjs.com/trusted-publishers
106
+
107
+ ## Publishing Workflow
108
+
109
+ Once setup is complete, use changesets for version management:
110
+
111
+ ```bash
112
+ # Create a changeset (describe what changed)
113
+ npm run changeset
114
+
115
+ # Commit and push to main
116
+ git add .
117
+ git commit -m "Add changeset"
118
+ git push
119
+
120
+ # CI will automatically:
121
+ # 1. Create a "Version Packages" PR
122
+ # 2. When merged, publish to npm via Trusted Publishers
123
+ ```
124
+
125
+ ## Troubleshooting
126
+
127
+ ### Build Fails
128
+
129
+ If `npm run build` fails:
130
+
131
+ - Fix TypeScript/build errors
132
+ - Ensure all dependencies are installed
133
+ - Check tsconfig.json configuration
134
+
135
+ ### Publish Fails (Organization Not Found)
136
+
137
+ If `npm publish` fails with "organization not found":
138
+
139
+ - Create the npm organization first: https://www.npmjs.com/org/create
140
+ - Make sure you're logged in: `npm login`
141
+ - Verify your npm account has access to the organization
142
+
143
+ ### Trusted Publisher Not Working
144
+
145
+ If GitHub Actions can't publish after Trusted Publisher setup:
146
+
147
+ - Verify repository owner/name match exactly
148
+ - Check workflow path is correct (.github/workflows/ci.yml)
149
+ - Ensure workflow has `id-token: write` permission
150
+ - Wait a few minutes for npm to propagate changes
151
+
152
+ ### Package.json Validation Fails
153
+
154
+ Common fixes:
155
+
156
+ - Set `"license": "UNLICENSED"` for proprietary packages
157
+ - Add `"publishConfig": { "access": "public" }` for scoped packages
158
+ - Ensure `"main"` field points to built output (e.g., "dist/index.js")
159
+
160
+ ## Additional Resources
161
+
162
+ - [npm Trusted Publishers Documentation](https://docs.npmjs.com/trusted-publishers)
163
+ - [Changesets Documentation](https://github.com/changesets/changesets)
164
+ - [GitHub Actions OIDC](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)