git-vibe-setup 3.1.1 → 4.1.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.
package/dist/cli.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { realpathSync } from "node:fs";
3
- import { resolve } from "node:path";
3
+ import { relative, resolve } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { fetchConsumerStarterFiles } from "./consumer-starter.js";
7
7
  import { githubTokenFromEnvironment } from "./github-api.js";
8
- import { blockingInstallPaths, buildInstallFiles, buildWorkflowUpdateFiles, existingFilesError, installFiles, unmanagedWorkflowUpdateError, unmanagedWorkflowUpdatePaths, updateFiles, } from "./install.js";
8
+ import { blockingInstallPaths, buildInstallFiles, buildUpdateFiles, existingFilesError, installFiles, obsoleteWorkflowCleanupPaths, unmanagedWorkflowUpdateError, unmanagedWorkflowUpdatePaths, updateFiles, } from "./install.js";
9
9
  import { renderManualSetupInstructions } from "./instructions.js";
10
10
  import { latestReleaseTag } from "./releases.js";
11
11
  const usage = `Usage:
@@ -40,12 +40,13 @@ export async function runUpdate(runtime = {}) {
40
40
  const githubToken = runtime.githubToken || githubTokenFromEnvironment();
41
41
  const releaseTag = await resolveReleaseTag(runtime, fetchImpl, githubToken);
42
42
  const sourceFiles = await fetchConsumerStarterFiles({ fetchImpl, githubToken, releaseTag });
43
- const files = buildWorkflowUpdateFiles({ cwd, releaseTag, sourceFiles });
43
+ const files = buildUpdateFiles({ cwd, releaseTag, sourceFiles });
44
44
  const unmanagedPaths = unmanagedWorkflowUpdatePaths(files);
45
+ const cleanupPaths = obsoleteWorkflowCleanupPaths(files);
45
46
  if (unmanagedPaths.length > 0)
46
47
  throw unmanagedWorkflowUpdateError(unmanagedPaths, cwd);
47
48
  updateFiles(files);
48
- (runtime.log || console.log)(`GitVibe workflow files updated with reusable workflows pinned to ${releaseTag}.`);
49
+ (runtime.log || console.log)(updateSuccessMessage({ cleanupPaths, cwd, releaseTag }));
49
50
  }
50
51
  export async function setupCli(runtime = {}) {
51
52
  const argv = runtime.argv || process.argv.slice(2);
@@ -149,6 +150,15 @@ function validateReleaseTag(releaseTag) {
149
150
  }
150
151
  throw new Error(`Invalid release tag: ${releaseTag}. Release tags must look like v3.0.4 or v3.0.4-rc.1.`);
151
152
  }
153
+ function updateSuccessMessage(options) {
154
+ const message = `GitVibe config and workflow files updated with reusable workflows pinned to ${options.releaseTag}.`;
155
+ if (options.cleanupPaths.length === 0)
156
+ return message;
157
+ const listed = options.cleanupPaths
158
+ .map((path) => `- ${relative(options.cwd, path) || path}`)
159
+ .join("\n");
160
+ return `${message}\nObsolete workflow files were left in place for manual review:\n${listed}`;
161
+ }
152
162
  /* c8 ignore start */
153
163
  if (isDirectRun(import.meta.url)) {
154
164
  const exitCode = await setupCli();
package/dist/install.d.ts CHANGED
@@ -9,15 +9,17 @@ export declare function buildInstallFiles(options: {
9
9
  releaseTag: string;
10
10
  sourceFiles: ConsumerStarterFile[];
11
11
  }): InstallFile[];
12
- export declare function buildWorkflowUpdateFiles(options: {
12
+ export declare function buildUpdateFiles(options: {
13
13
  cwd: string;
14
14
  releaseTag: string;
15
15
  sourceFiles: ConsumerStarterFile[];
16
16
  }): InstallFile[];
17
17
  export declare function blockingInstallPaths(files: InstallFile[]): string[];
18
18
  export declare function unmanagedWorkflowUpdatePaths(files: InstallFile[]): string[];
19
+ export declare function obsoleteWorkflowCleanupPaths(files: InstallFile[]): string[];
19
20
  export declare function installFiles(files: InstallFile[]): void;
20
21
  export declare function updateFiles(files: InstallFile[]): void;
21
22
  export declare function existingFilesError(paths: string[], cwd: string): Error;
22
23
  export declare function unmanagedWorkflowUpdateError(paths: string[], cwd: string): Error;
23
24
  export declare function pinWorkflowReleaseRefs(content: string, releaseTag: string): string;
25
+ export declare function migrateGitVibeConfigContent(content: string): string;
package/dist/install.js CHANGED
@@ -1,9 +1,7 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { basename, dirname, join, relative } from "node:path";
3
3
  const requiredInstallSourcePaths = [
4
4
  ".github/git-vibe.yml",
5
- ".github/workflows/address-feedback.yml",
6
- ".github/workflows/develop.yml",
7
5
  ".github/workflows/investigate.yml",
8
6
  ".github/workflows/materialize.yml",
9
7
  ".github/workflows/review.yml",
@@ -13,17 +11,19 @@ const requiredInstallSourcePaths = [
13
11
  ".git-vibe/role-group/security.md",
14
12
  ];
15
13
  const requiredWorkflowSourcePaths = requiredInstallSourcePaths.filter(isWorkflowSourcePath);
14
+ const requiredUpdateSourcePaths = [".github/git-vibe.yml", ...requiredWorkflowSourcePaths];
15
+ const managedWorkflowMarker = "# GitVibe managed workflow wrapper";
16
16
  export function buildInstallFiles(options) {
17
17
  requireSourcePaths(options.sourceFiles, requiredInstallSourcePaths, options.releaseTag);
18
18
  return options.sourceFiles
19
19
  .filter((file) => isInstallSourcePath(file.relativePath))
20
20
  .map((file) => buildInstallFile(file, options.cwd, options.releaseTag));
21
21
  }
22
- export function buildWorkflowUpdateFiles(options) {
23
- requireSourcePaths(options.sourceFiles, requiredWorkflowSourcePaths, options.releaseTag);
22
+ export function buildUpdateFiles(options) {
23
+ requireSourcePaths(options.sourceFiles, requiredUpdateSourcePaths, options.releaseTag);
24
24
  return options.sourceFiles
25
- .filter((file) => isWorkflowSourcePath(file.relativePath))
26
- .map((file) => buildInstallFile(file, options.cwd, options.releaseTag));
25
+ .filter((file) => isUpdateSourcePath(file.relativePath))
26
+ .map((file) => buildUpdateFile(file, options.cwd, options.releaseTag));
27
27
  }
28
28
  export function blockingInstallPaths(files) {
29
29
  return files
@@ -33,10 +33,17 @@ export function blockingInstallPaths(files) {
33
33
  }
34
34
  export function unmanagedWorkflowUpdatePaths(files) {
35
35
  return files
36
+ .filter(isWorkflowInstallFile)
36
37
  .filter((file) => existsSync(file.targetPath) && !isManagedWorkflowTarget(file))
37
38
  .map((file) => file.targetPath)
38
39
  .sort();
39
40
  }
41
+ export function obsoleteWorkflowCleanupPaths(files) {
42
+ return obsoleteWorkflowPaths(files)
43
+ .filter((targetPath) => !isMarkedManagedWorkflowFile(targetPath, basename(targetPath)))
44
+ .filter((targetPath) => isManagedWorkflowFile(targetPath, basename(targetPath)))
45
+ .sort();
46
+ }
40
47
  export function installFiles(files) {
41
48
  const createdDirectories = [];
42
49
  const createdFiles = [];
@@ -61,6 +68,10 @@ export function updateFiles(files) {
61
68
  snapshots.push(snapshotFile(file.targetPath));
62
69
  writeFileSync(file.targetPath, file.content);
63
70
  }
71
+ for (const targetPath of obsoleteManagedWorkflowPaths(files)) {
72
+ snapshots.push(snapshotFile(targetPath));
73
+ rmSync(targetPath, { force: true });
74
+ }
64
75
  }
65
76
  catch (error) {
66
77
  rollbackUpdate(snapshots, createdDirectories);
@@ -78,10 +89,42 @@ export function unmanagedWorkflowUpdateError(paths, cwd) {
78
89
  export function pinWorkflowReleaseRefs(content, releaseTag) {
79
90
  return content.replace(/(uses:\s*markhuangai\/git-vibe\/\.github\/workflows\/[^\s@]+)@[^\s]+/g, (_match, workflowReference) => `${workflowReference}@${releaseTag}`);
80
91
  }
92
+ export function migrateGitVibeConfigContent(content) {
93
+ const migratedComments = migrateLegacyEventDeliveryComments(content);
94
+ return ensureTrailingNewline(migrateGithubAuthConfig(migratedComments));
95
+ }
81
96
  function isManagedWorkflowTarget(file) {
97
+ return isManagedWorkflowFile(file.targetPath, basename(file.targetPath));
98
+ }
99
+ function isManagedWorkflowFile(targetPath, workflowName) {
100
+ try {
101
+ return managedWorkflowPattern(workflowName).test(readFileSync(targetPath, "utf8"));
102
+ }
103
+ catch {
104
+ return false;
105
+ }
106
+ }
107
+ function obsoleteManagedWorkflowPaths(files) {
108
+ return obsoleteWorkflowPaths(files)
109
+ .filter((targetPath) => isMarkedManagedWorkflowFile(targetPath, basename(targetPath)))
110
+ .sort();
111
+ }
112
+ function obsoleteWorkflowPaths(files) {
113
+ const workflowDirectory = dirname(files.find(isWorkflowInstallFile)?.targetPath || "");
114
+ if (!workflowDirectory || workflowDirectory === ".")
115
+ return [];
116
+ if (!existsSync(workflowDirectory))
117
+ return [];
118
+ const currentWorkflowNames = new Set(files.filter(isWorkflowInstallFile).map((file) => basename(file.targetPath)));
119
+ return readdirSync(workflowDirectory, { withFileTypes: true })
120
+ .filter((entry) => entry.isFile() && !currentWorkflowNames.has(entry.name))
121
+ .map((entry) => join(workflowDirectory, entry.name));
122
+ }
123
+ function isMarkedManagedWorkflowFile(targetPath, workflowName) {
82
124
  try {
83
- const workflowName = basename(file.targetPath);
84
- return managedWorkflowPattern(workflowName).test(readFileSync(file.targetPath, "utf8"));
125
+ const content = readFileSync(targetPath, "utf8");
126
+ return (content.startsWith(`${managedWorkflowMarker}\n`) &&
127
+ managedWorkflowPattern(workflowName).test(content));
85
128
  }
86
129
  catch {
87
130
  return false;
@@ -150,6 +193,22 @@ function buildInstallFile(sourceFile, cwd, releaseTag) {
150
193
  targetPath: join(cwd, safeRelativePath(sourceFile.relativePath)),
151
194
  };
152
195
  }
196
+ function buildUpdateFile(sourceFile, cwd, releaseTag) {
197
+ const targetPath = join(cwd, safeRelativePath(sourceFile.relativePath));
198
+ const existingContent = fileContentIfExists(targetPath);
199
+ let content = pinWorkflowReleaseRefs(sourceFile.content, releaseTag);
200
+ if (sourceFile.relativePath === ".github/git-vibe.yml" && existingContent !== undefined) {
201
+ content = migrateGitVibeConfigContent(existingContent);
202
+ }
203
+ else if (isWorkflowSourcePath(sourceFile.relativePath) && existingContent !== undefined) {
204
+ content = preserveWorkflowRunner(content, existingContent);
205
+ }
206
+ return {
207
+ content,
208
+ sourcePath: sourceFile.sourcePath,
209
+ targetPath,
210
+ };
211
+ }
153
212
  function requireSourcePaths(sourceFiles, requiredPaths, releaseTag) {
154
213
  const sourcePaths = new Set(sourceFiles.map((file) => file.relativePath));
155
214
  const missingPaths = requiredPaths.filter((path) => !sourcePaths.has(path));
@@ -161,9 +220,15 @@ function requireSourcePaths(sourceFiles, requiredPaths, releaseTag) {
161
220
  function isInstallSourcePath(relativePath) {
162
221
  return relativePath.startsWith(".github/") || relativePath.startsWith(".git-vibe/");
163
222
  }
223
+ function isUpdateSourcePath(relativePath) {
224
+ return relativePath === ".github/git-vibe.yml" || isWorkflowSourcePath(relativePath);
225
+ }
164
226
  function isWorkflowSourcePath(relativePath) {
165
227
  return relativePath.startsWith(".github/workflows/") && relativePath.endsWith(".yml");
166
228
  }
229
+ function isWorkflowInstallFile(file) {
230
+ return file.sourcePath.includes(".github/workflows/") && file.sourcePath.endsWith(".yml");
231
+ }
167
232
  function safeRelativePath(relativePath) {
168
233
  const segments = relativePath.split("/");
169
234
  const isSafe = relativePath.length > 0 &&
@@ -174,3 +239,27 @@ function safeRelativePath(relativePath) {
174
239
  return relativePath;
175
240
  throw new Error(`git-vibe-setup found an unsafe starter file path: ${relativePath}`);
176
241
  }
242
+ function fileContentIfExists(targetPath) {
243
+ if (!existsSync(targetPath))
244
+ return undefined;
245
+ return readFileSync(targetPath, "utf8");
246
+ }
247
+ function preserveWorkflowRunner(generatedContent, existingContent) {
248
+ const runner = /^\s+runner:\s*(.+)$/m.exec(existingContent)?.[1];
249
+ if (!runner)
250
+ return generatedContent;
251
+ return generatedContent.replace(/^(\s+runner:\s*).+$/m, `$1${runner}`);
252
+ }
253
+ function migrateLegacyEventDeliveryComments(content) {
254
+ return content.replace(/^event_delivery:\n # webhook: repository webhook points at the self-hosted GitVibe server\.\n # relay: webhook proxy\/tunnel such as Smee, Hookdeck, Cloudflare Tunnel, or ngrok\.\n # actions: no-server receiver workflows in the consumer repository\.\n # polling: local\/scheduled worker polls GitHub APIs with cursors\/ETags\.\n/m, "event_delivery:\n # Hosted GitHub App installs configure webhooks centrally; repositories do not create hooks.\n");
255
+ }
256
+ function migrateGithubAuthConfig(content) {
257
+ const githubAppAuth = "github_auth:\n mode: github-app\n";
258
+ if (/^github_auth:\n/m.test(content)) {
259
+ return content.replace(/^github_auth:\n(?:[ \t]+.*(?:\n|$))*/m, githubAppAuth);
260
+ }
261
+ return `${content.trimEnd()}\n\n${githubAppAuth}`;
262
+ }
263
+ function ensureTrailingNewline(content) {
264
+ return content.endsWith("\n") ? content : `${content}\n`;
265
+ }
@@ -1,7 +1,5 @@
1
1
  const requiredSecrets = [
2
2
  ["GITVIBE_AI_ENV_JSON", "JSON env bundle for AI provider config and CLI auth."],
3
- ["GITVIBE_GITHUB_TOKEN", "Fine-grained PAT for GitVibe workflow and server writes."],
4
- ["WEBHOOK_SECRET", "Webhook shared secret mapped to GITHUB_WEBHOOK_SECRET in deployment."],
5
3
  ];
6
4
  const usefulVariables = [
7
5
  "GITVIBE_BASE_BRANCH",
@@ -13,8 +11,9 @@ export function renderManualSetupInstructions(releaseTag) {
13
11
  const lines = [
14
12
  `GitVibe starter files installed with reusable workflows pinned to ${releaseTag}.`,
15
13
  "",
16
- "Configure these GitHub secrets manually before running the workflows:",
14
+ "Configure this GitHub secret manually before running the workflows:",
17
15
  ...requiredSecrets.map(([name, description]) => `- ${name}: ${description}`),
16
+ "- GITVIBE_MCP_ENV_JSON: optional JSON env bundle for MCP credentials.",
18
17
  "",
19
18
  "Optional repository variables:",
20
19
  ...usefulVariables.map((name) => `- ${name}`),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-vibe-setup",
3
- "version": "3.1.1",
3
+ "version": "4.1.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {