git-vibe-setup 4.0.0 → 5.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.
package/README.md CHANGED
@@ -20,10 +20,11 @@ npx git-vibe-setup update
20
20
  ```
21
21
 
22
22
  The `update` command fetches `examples/consumer` from the latest stable
23
- `markhuangai/git-vibe` release, rewrites only `.github/workflows/*.yml` GitVibe
24
- wrapper files, and pins them to that release. It does not update
25
- `.github/git-vibe.yml`, `.git-vibe`, secrets, or variables, and it refuses to
26
- overwrite workflow files that do not look like GitVibe wrappers.
23
+ `markhuangai/git-vibe` release, migrates supported `.github/git-vibe.yml`
24
+ settings in place, rewrites `.github/workflows/*.yml` GitVibe wrapper files,
25
+ and pins workflow refs to that release. It does not update `.git-vibe`,
26
+ secrets, or variables, and it refuses to overwrite workflow files that do not
27
+ look like GitVibe wrappers.
27
28
 
28
29
  To test a specific release or prerelease from a consumer repository, pass the
29
30
  release tag explicitly:
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, buildUpdateFiles, 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:
@@ -42,10 +42,11 @@ export async function runUpdate(runtime = {}) {
42
42
  const sourceFiles = await fetchConsumerStarterFiles({ fetchImpl, githubToken, releaseTag });
43
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 config and 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
@@ -16,6 +16,7 @@ export declare function buildUpdateFiles(options: {
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;
package/dist/install.js CHANGED
@@ -1,9 +1,8 @@
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
+ import { isMap, isScalar, parseDocument, YAMLMap } from "yaml";
3
4
  const requiredInstallSourcePaths = [
4
5
  ".github/git-vibe.yml",
5
- ".github/workflows/address-feedback.yml",
6
- ".github/workflows/develop.yml",
7
6
  ".github/workflows/investigate.yml",
8
7
  ".github/workflows/materialize.yml",
9
8
  ".github/workflows/review.yml",
@@ -14,6 +13,7 @@ const requiredInstallSourcePaths = [
14
13
  ];
15
14
  const requiredWorkflowSourcePaths = requiredInstallSourcePaths.filter(isWorkflowSourcePath);
16
15
  const requiredUpdateSourcePaths = [".github/git-vibe.yml", ...requiredWorkflowSourcePaths];
16
+ const managedWorkflowMarker = "# GitVibe managed workflow wrapper";
17
17
  export function buildInstallFiles(options) {
18
18
  requireSourcePaths(options.sourceFiles, requiredInstallSourcePaths, options.releaseTag);
19
19
  return options.sourceFiles
@@ -39,6 +39,12 @@ export function unmanagedWorkflowUpdatePaths(files) {
39
39
  .map((file) => file.targetPath)
40
40
  .sort();
41
41
  }
42
+ export function obsoleteWorkflowCleanupPaths(files) {
43
+ return obsoleteWorkflowPaths(files)
44
+ .filter((targetPath) => !isMarkedManagedWorkflowFile(targetPath, basename(targetPath)))
45
+ .filter((targetPath) => isManagedWorkflowFile(targetPath, basename(targetPath)))
46
+ .sort();
47
+ }
42
48
  export function installFiles(files) {
43
49
  const createdDirectories = [];
44
50
  const createdFiles = [];
@@ -63,6 +69,10 @@ export function updateFiles(files) {
63
69
  snapshots.push(snapshotFile(file.targetPath));
64
70
  writeFileSync(file.targetPath, file.content);
65
71
  }
72
+ for (const targetPath of obsoleteManagedWorkflowPaths(files)) {
73
+ snapshots.push(snapshotFile(targetPath));
74
+ rmSync(targetPath, { force: true });
75
+ }
66
76
  }
67
77
  catch (error) {
68
78
  rollbackUpdate(snapshots, createdDirectories);
@@ -82,12 +92,41 @@ export function pinWorkflowReleaseRefs(content, releaseTag) {
82
92
  }
83
93
  export function migrateGitVibeConfigContent(content) {
84
94
  const migratedComments = migrateLegacyEventDeliveryComments(content);
85
- return ensureTrailingNewline(migrateGithubAuthConfig(migratedComments));
95
+ const migratedAuth = migrateGithubAuthConfig(migratedComments);
96
+ return ensureTrailingNewline(migrateLegacyAiProfiles(migratedAuth));
86
97
  }
87
98
  function isManagedWorkflowTarget(file) {
99
+ return isManagedWorkflowFile(file.targetPath, basename(file.targetPath));
100
+ }
101
+ function isManagedWorkflowFile(targetPath, workflowName) {
102
+ try {
103
+ return managedWorkflowPattern(workflowName).test(readFileSync(targetPath, "utf8"));
104
+ }
105
+ catch {
106
+ return false;
107
+ }
108
+ }
109
+ function obsoleteManagedWorkflowPaths(files) {
110
+ return obsoleteWorkflowPaths(files)
111
+ .filter((targetPath) => isMarkedManagedWorkflowFile(targetPath, basename(targetPath)))
112
+ .sort();
113
+ }
114
+ function obsoleteWorkflowPaths(files) {
115
+ const workflowDirectory = dirname(files.find(isWorkflowInstallFile)?.targetPath || "");
116
+ if (!workflowDirectory || workflowDirectory === ".")
117
+ return [];
118
+ if (!existsSync(workflowDirectory))
119
+ return [];
120
+ const currentWorkflowNames = new Set(files.filter(isWorkflowInstallFile).map((file) => basename(file.targetPath)));
121
+ return readdirSync(workflowDirectory, { withFileTypes: true })
122
+ .filter((entry) => entry.isFile() && !currentWorkflowNames.has(entry.name))
123
+ .map((entry) => join(workflowDirectory, entry.name));
124
+ }
125
+ function isMarkedManagedWorkflowFile(targetPath, workflowName) {
88
126
  try {
89
- const workflowName = basename(file.targetPath);
90
- return managedWorkflowPattern(workflowName).test(readFileSync(file.targetPath, "utf8"));
127
+ const content = readFileSync(targetPath, "utf8");
128
+ return (content.startsWith(`${managedWorkflowMarker}\n`) &&
129
+ managedWorkflowPattern(workflowName).test(content));
91
130
  }
92
131
  catch {
93
132
  return false;
@@ -223,6 +262,144 @@ function migrateGithubAuthConfig(content) {
223
262
  }
224
263
  return `${content.trimEnd()}\n\n${githubAppAuth}`;
225
264
  }
265
+ function migrateLegacyAiProfiles(content) {
266
+ const document = parseDocument(content);
267
+ if (document.errors.length > 0) {
268
+ throw new Error(`git-vibe-setup could not migrate .github/git-vibe.yml because it is not valid YAML: ${document.errors[0]?.message}`);
269
+ }
270
+ const root = document.contents;
271
+ if (!isMap(root))
272
+ return content;
273
+ const ai = root.get("ai", true);
274
+ if (!isMap(ai))
275
+ return content;
276
+ const profiles = ai.get("profiles", true);
277
+ if (!isMap(profiles))
278
+ return content;
279
+ let changed = false;
280
+ for (const pair of profiles.items) {
281
+ if (isMap(pair.value))
282
+ changed = migrateLegacyAiProfile(pair.value) || changed;
283
+ }
284
+ return changed ? document.toString() : content;
285
+ }
286
+ function migrateLegacyAiProfile(profile) {
287
+ const adapter = stringNodeValue(profile.get("adapter"));
288
+ if (adapter === "ai-sdk-agentool") {
289
+ migrateAgentoolProfile(profile);
290
+ return true;
291
+ }
292
+ if (adapter === "cli-claude-code") {
293
+ profile.set("adapter", "claude-code-sdk");
294
+ ensureClaudeModel(profile);
295
+ orderProfileKeys(profile);
296
+ return true;
297
+ }
298
+ if (adapter === "cli-codex") {
299
+ profile.set("adapter", "codex-sdk");
300
+ return true;
301
+ }
302
+ return false;
303
+ }
304
+ function migrateAgentoolProfile(profile) {
305
+ const provider = profile.get("provider", true);
306
+ profile.set("adapter", "claude-code-sdk");
307
+ syncClaudeEnvFromProvider(profile, provider);
308
+ syncReasoningEffort(profile);
309
+ trimProviderOptions(profile);
310
+ ensureClaudeModel(profile);
311
+ profile.delete("context_window_tokens");
312
+ profile.delete("provider");
313
+ orderProfileKeys(profile);
314
+ }
315
+ function syncClaudeEnvFromProvider(profile, provider) {
316
+ const env = ensureMap(profile, "env");
317
+ if (isMap(provider)) {
318
+ copyMapValue(provider, "api_key", env, "ANTHROPIC_API_KEY");
319
+ copyMapValue(provider, "base_url", env, "ANTHROPIC_BASE_URL");
320
+ const providerModel = stringNodeValue(provider.get("model"));
321
+ if (providerModel) {
322
+ setIfMissing(env, "ANTHROPIC_DEFAULT_OPUS_MODEL", providerModel);
323
+ setIfMissing(env, "ANTHROPIC_DEFAULT_SONNET_MODEL", providerModel);
324
+ setIfMissing(env, "ANTHROPIC_DEFAULT_HAIKU_MODEL", providerModel);
325
+ }
326
+ }
327
+ setIfMissing(env, "ANTHROPIC_MODEL", "opus");
328
+ setIfMissing(env, "CLAUDE_CODE_SUBAGENT_MODEL", "opus");
329
+ }
330
+ function syncReasoningEffort(profile) {
331
+ const effort = nodeAt(profile, ["reasoning", "effort"]) ||
332
+ nodeAt(profile, ["provider_options", "anthropic", "effort"]);
333
+ if (!effort)
334
+ return;
335
+ const reasoning = ensureMap(profile, "reasoning");
336
+ if (!reasoning.has("effort"))
337
+ reasoning.set("effort", effort);
338
+ }
339
+ function trimProviderOptions(profile) {
340
+ const providerOptions = profile.get("provider_options", true);
341
+ if (!isMap(providerOptions))
342
+ return;
343
+ providerOptions.delete("openai");
344
+ if (providerOptions.items.length === 0)
345
+ profile.delete("provider_options");
346
+ }
347
+ function ensureClaudeModel(profile) {
348
+ setIfMissing(profile, "model", "opus");
349
+ }
350
+ function ensureMap(parent, key) {
351
+ const value = parent.get(key, true);
352
+ if (isMap(value))
353
+ return value;
354
+ const map = new YAMLMap();
355
+ parent.set(key, map);
356
+ return map;
357
+ }
358
+ function copyMapValue(source, sourceKey, target, targetKey) {
359
+ if (target.has(targetKey))
360
+ return;
361
+ const value = source.get(sourceKey, true);
362
+ if (value !== undefined)
363
+ target.set(targetKey, value);
364
+ }
365
+ function setIfMissing(map, key, value) {
366
+ if (!map.has(key))
367
+ map.set(key, value);
368
+ }
369
+ function nodeAt(map, path) {
370
+ let current = map;
371
+ for (const segment of path) {
372
+ if (!isMap(current))
373
+ return undefined;
374
+ current = current.get(segment, true);
375
+ }
376
+ return current;
377
+ }
378
+ function stringNodeValue(value) {
379
+ if (typeof value === "string")
380
+ return value;
381
+ if (isScalar(value) && typeof value.value === "string")
382
+ return value.value;
383
+ return undefined;
384
+ }
385
+ function orderProfileKeys(profile) {
386
+ const priorities = new Map([
387
+ ["adapter", 0],
388
+ ["env", 1],
389
+ ["model", 2],
390
+ ["reasoning", 3],
391
+ ["provider_options", 4],
392
+ ]);
393
+ profile.items.sort((left, right) => {
394
+ return profileKeyPriority(left, priorities) - profileKeyPriority(right, priorities);
395
+ });
396
+ }
397
+ function profileKeyPriority(pair, priorities) {
398
+ return priorities.get(pairKey(pair)) ?? 100;
399
+ }
400
+ function pairKey(pair) {
401
+ return stringNodeValue(pair.key) || "";
402
+ }
226
403
  function ensureTrailingNewline(content) {
227
404
  return content.endsWith("\n") ? content : `${content}\n`;
228
405
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-vibe-setup",
3
- "version": "4.0.0",
3
+ "version": "5.0.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,6 +16,9 @@
16
16
  "test": "vitest run",
17
17
  "typecheck": "tsc --project tsconfig.json --noEmit"
18
18
  },
19
+ "dependencies": {
20
+ "yaml": "^2.8.4"
21
+ },
19
22
  "devDependencies": {
20
23
  "@types/node": "^25.6.0",
21
24
  "typescript": "^6.0.3",