octo-dev 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +270 -0
  3. package/package.json +62 -0
  4. package/scripts/install.sh +117 -0
  5. package/src/build/adapters/docker-build-engine.adapter.ts +39 -0
  6. package/src/build/affected-detector.ts +126 -0
  7. package/src/build/build-orchestrator.ts +169 -0
  8. package/src/build/build-scheduler.ts +174 -0
  9. package/src/build/ports/build-engine.port.ts +38 -0
  10. package/src/cli/build.command.ts +101 -0
  11. package/src/cli/bump.command.ts +98 -0
  12. package/src/cli/down.command.ts +36 -0
  13. package/src/cli/graph.command.ts +40 -0
  14. package/src/cli/index.ts +80 -0
  15. package/src/cli/init.command.ts +106 -0
  16. package/src/cli/status.command.ts +46 -0
  17. package/src/cli/up.command.ts +52 -0
  18. package/src/graph/aggregated-graph.ts +77 -0
  19. package/src/graph/build-graph.ts +125 -0
  20. package/src/graph/dependency-graph.ts +82 -0
  21. package/src/graph/index.ts +4 -0
  22. package/src/graph/topological-sort.ts +104 -0
  23. package/src/hooks/hook-runner.ts +57 -0
  24. package/src/infra/compose-aggregator.ts +152 -0
  25. package/src/infra/compose-smart-merger.ts +93 -0
  26. package/src/infra/infra-manager.ts +157 -0
  27. package/src/manifest/manifest-discovery.ts +144 -0
  28. package/src/manifest/manifest-parser.ts +109 -0
  29. package/src/manifest/manifest-printer.ts +75 -0
  30. package/src/manifest/manifest-schema.ts +34 -0
  31. package/src/shared/errors.ts +43 -0
  32. package/src/shared/logger.ts +14 -0
  33. package/src/shared/process-runner.ts +47 -0
  34. package/src/shared/shutdown.ts +36 -0
  35. package/src/version/changelog-generator.ts +112 -0
  36. package/src/version/version-bumper.ts +116 -0
  37. package/src/version/version-propagator.ts +120 -0
@@ -0,0 +1,109 @@
1
+ import { parse as parseYaml, YAMLParseError } from 'yaml';
2
+ import { OctoManifestSchema, type OctoManifest } from './manifest-schema.js';
3
+ import { ManifestError } from '../shared/errors.js';
4
+
5
+ export type ParseSuccess = {
6
+ ok: true;
7
+ value: OctoManifest;
8
+ warnings: string[];
9
+ };
10
+
11
+ export type ParseFailure = {
12
+ ok: false;
13
+ error: ManifestError;
14
+ };
15
+
16
+ export type ParseResult = ParseSuccess | ParseFailure;
17
+
18
+ /** Known top-level keys in the manifest schema */
19
+ const KNOWN_TOP_KEYS = new Set(['hooks', 'services', 'packages']);
20
+ const KNOWN_HOOK_KEYS = new Set(['pre-build', 'pre-bump']);
21
+
22
+ /**
23
+ * Detects unknown keys at top level and in hooks, returning warnings.
24
+ */
25
+ function detectUnknownKeys(data: Record<string, unknown>): string[] {
26
+ const warnings: string[] = [];
27
+
28
+ for (const key of Object.keys(data)) {
29
+ if (!KNOWN_TOP_KEYS.has(key)) {
30
+ warnings.push(`Unknown key "${key}" at root level`);
31
+ }
32
+ }
33
+
34
+ if (data.hooks && typeof data.hooks === 'object' && data.hooks !== null) {
35
+ for (const key of Object.keys(data.hooks as Record<string, unknown>)) {
36
+ if (!KNOWN_HOOK_KEYS.has(key)) {
37
+ warnings.push(`Unknown key "${key}" in hooks`);
38
+ }
39
+ }
40
+ }
41
+
42
+ return warnings;
43
+ }
44
+
45
+ /**
46
+ * Parses YAML content and validates against OctoManifestSchema.
47
+ * Returns all validation errors in a single message (does not stop at first error).
48
+ * Emits warnings for unknown keys without blocking the parse.
49
+ */
50
+ export function parseManifest(content: string, filePath?: string): ParseResult {
51
+ // Handle empty/whitespace-only content
52
+ if (!content || content.trim().length === 0) {
53
+ return {
54
+ ok: false,
55
+ error: new ManifestError('Manifest file is empty or contains only whitespace', filePath),
56
+ };
57
+ }
58
+
59
+ // Parse YAML — catch syntax errors with line/column
60
+ let data: unknown;
61
+ try {
62
+ data = parseYaml(content);
63
+ } catch (err) {
64
+ if (err instanceof YAMLParseError) {
65
+ const pos = err.linePos?.[0];
66
+ return {
67
+ ok: false,
68
+ error: new ManifestError(
69
+ `YAML syntax error: ${err.message}`,
70
+ filePath,
71
+ pos?.line,
72
+ pos?.col,
73
+ ),
74
+ };
75
+ }
76
+ return {
77
+ ok: false,
78
+ error: new ManifestError(`Unexpected parse error: ${String(err)}`, filePath),
79
+ };
80
+ }
81
+
82
+ if (data === null || data === undefined || typeof data !== 'object') {
83
+ return {
84
+ ok: false,
85
+ error: new ManifestError('Manifest must be a YAML mapping (object)', filePath),
86
+ };
87
+ }
88
+
89
+ // Detect unknown keys (warnings, non-blocking)
90
+ const warnings = detectUnknownKeys(data as Record<string, unknown>);
91
+
92
+ // Validate with Zod — collect ALL errors
93
+ const result = OctoManifestSchema.safeParse(data);
94
+
95
+ if (!result.success) {
96
+ const issues = result.error.issues.map(
97
+ (issue) => ` - ${issue.path.join('.')}: ${issue.message}`,
98
+ );
99
+ return {
100
+ ok: false,
101
+ error: new ManifestError(
102
+ `Manifest validation failed:\n${issues.join('\n')}`,
103
+ filePath,
104
+ ),
105
+ };
106
+ }
107
+
108
+ return { ok: true, value: result.data, warnings };
109
+ }
@@ -0,0 +1,75 @@
1
+ import { parseDocument, stringify, Document, isSeq, isScalar, isMap } from 'yaml';
2
+ import type { OctoManifest } from './manifest-schema.js';
3
+
4
+ /**
5
+ * Checks if two values are deeply equal (for simple manifest structures).
6
+ */
7
+ function deepEqual(a: unknown, b: unknown): boolean {
8
+ if (a === b) return true;
9
+ if (a == null || b == null) return false;
10
+ if (typeof a !== typeof b) return false;
11
+ if (Array.isArray(a) && Array.isArray(b)) {
12
+ if (a.length !== b.length) return false;
13
+ return a.every((v, i) => deepEqual(v, b[i]));
14
+ }
15
+ if (typeof a === 'object' && typeof b === 'object') {
16
+ const keysA = Object.keys(a as object);
17
+ const keysB = Object.keys(b as object);
18
+ if (keysA.length !== keysB.length) return false;
19
+ return keysA.every(k => deepEqual((a as Record<string, unknown>)[k], (b as Record<string, unknown>)[k]));
20
+ }
21
+ return false;
22
+ }
23
+
24
+ /**
25
+ * Gets the plain JS value from a YAML AST node for comparison.
26
+ */
27
+ function nodeToPlain(node: unknown): unknown {
28
+ if (isScalar(node)) return node.value;
29
+ if (isSeq(node)) return node.items.map(nodeToPlain);
30
+ if (isMap(node)) {
31
+ const obj: Record<string, unknown> = {};
32
+ for (const pair of node.items) {
33
+ const key = isScalar(pair.key) ? String(pair.key.value) : String(pair.key);
34
+ obj[key] = nodeToPlain(pair.value);
35
+ }
36
+ return obj;
37
+ }
38
+ return node;
39
+ }
40
+
41
+ /**
42
+ * Serializes an OctoManifest back to YAML.
43
+ * When originalContent is provided, updates the AST in-place to preserve comments and key order.
44
+ * When no originalContent is provided, stringifies the object directly.
45
+ */
46
+ export function printManifest(manifest: OctoManifest, originalContent?: string): string {
47
+ if (!originalContent) {
48
+ return stringify(manifest, { lineWidth: 0 });
49
+ }
50
+
51
+ // Parse original into a Document (AST) to preserve comments and ordering
52
+ const doc = parseDocument(originalContent);
53
+
54
+ // Update hooks
55
+ if (manifest.hooks !== undefined) {
56
+ const existingHooks = doc.get('hooks', true);
57
+ if (!deepEqual(nodeToPlain(existingHooks), manifest.hooks)) {
58
+ doc.set('hooks', manifest.hooks);
59
+ }
60
+ } else if (doc.has('hooks')) {
61
+ doc.delete('hooks');
62
+ }
63
+
64
+ // Only update services/packages if values actually changed
65
+ for (const key of ['services', 'packages'] as const) {
66
+ const existingNode = doc.get(key, true);
67
+ const existingPlain = nodeToPlain(existingNode);
68
+ if (!deepEqual(existingPlain, manifest[key])) {
69
+ doc.set(key, manifest[key]);
70
+ }
71
+ // If equal, leave the AST node untouched (preserves all comments)
72
+ }
73
+
74
+ return doc.toString({ lineWidth: 0 });
75
+ }
@@ -0,0 +1,34 @@
1
+ import { z } from 'zod';
2
+
3
+ export const HookDefinitionSchema = z.object({
4
+ name: z.string(),
5
+ command: z.string(),
6
+ });
7
+
8
+ export const ServiceEntrySchema = z.union([
9
+ z.string(),
10
+ z.record(z.string(), z.object({
11
+ path: z.string().optional(),
12
+ })),
13
+ ]);
14
+
15
+ export const PackageEntrySchema = z.union([
16
+ z.string(),
17
+ z.record(z.string(), z.object({
18
+ path: z.string().optional(),
19
+ })),
20
+ ]);
21
+
22
+ export const OctoManifestSchema = z.object({
23
+ hooks: z.object({
24
+ 'pre-build': z.array(HookDefinitionSchema).optional(),
25
+ 'pre-bump': z.array(HookDefinitionSchema).optional(),
26
+ }).optional(),
27
+ services: z.array(ServiceEntrySchema),
28
+ packages: z.array(PackageEntrySchema),
29
+ });
30
+
31
+ export type OctoManifest = z.infer<typeof OctoManifestSchema>;
32
+ export type HookDefinition = z.infer<typeof HookDefinitionSchema>;
33
+ export type ServiceEntry = z.infer<typeof ServiceEntrySchema>;
34
+ export type PackageEntry = z.infer<typeof PackageEntrySchema>;
@@ -0,0 +1,43 @@
1
+ /** Base error for all Octo CLI errors */
2
+ export class OctoError extends Error {
3
+ constructor(message: string, public readonly exitCode: number = 1) {
4
+ super(message);
5
+ this.name = 'OctoError';
6
+ }
7
+ }
8
+
9
+ /** Manifest parsing/validation errors */
10
+ export class ManifestError extends OctoError {
11
+ constructor(
12
+ message: string,
13
+ public readonly file?: string,
14
+ public readonly line?: number,
15
+ public readonly column?: number,
16
+ ) {
17
+ super(message);
18
+ this.name = 'ManifestError';
19
+ }
20
+ }
21
+
22
+ /** Build engine/orchestration errors */
23
+ export class BuildError extends OctoError {
24
+ constructor(
25
+ message: string,
26
+ public readonly service?: string,
27
+ public readonly output?: string,
28
+ ) {
29
+ super(message);
30
+ this.name = 'BuildError';
31
+ }
32
+ }
33
+
34
+ /** Dependency cycle detection errors */
35
+ export class CycleError extends OctoError {
36
+ constructor(
37
+ message: string,
38
+ public readonly cycle: string[],
39
+ ) {
40
+ super(message);
41
+ this.name = 'CycleError';
42
+ }
43
+ }
@@ -0,0 +1,14 @@
1
+ /** Formatted output for stdout/stderr with prefixes */
2
+ export const logger = {
3
+ info(message: string): void {
4
+ process.stdout.write(`[INFO] ${message}\n`);
5
+ },
6
+
7
+ error(message: string): void {
8
+ process.stderr.write(`[ERRO] ${message}\n`);
9
+ },
10
+
11
+ warn(message: string): void {
12
+ process.stderr.write(`[AVISO] ${message}\n`);
13
+ },
14
+ };
@@ -0,0 +1,47 @@
1
+ import { spawn } from 'node:child_process';
2
+
3
+ export interface RunOptions {
4
+ cwd?: string;
5
+ timeout?: number;
6
+ env?: Record<string, string>;
7
+ }
8
+
9
+ export interface RunResult {
10
+ stdout: string;
11
+ stderr: string;
12
+ exitCode: number;
13
+ }
14
+
15
+ /** Wrapper for child_process.spawn with Promise, timeout, and stdout/stderr capture */
16
+ export function run(command: string, args: string[] = [], options: RunOptions = {}): Promise<RunResult> {
17
+ const { cwd, timeout = 60_000, env } = options;
18
+
19
+ return new Promise((resolve, reject) => {
20
+ const child = spawn(command, args, {
21
+ cwd,
22
+ env: env ? { ...process.env, ...env } : process.env,
23
+ shell: true,
24
+ });
25
+
26
+ let stdout = '';
27
+ let stderr = '';
28
+
29
+ child.stdout.on('data', (data: Buffer) => { stdout += data.toString(); });
30
+ child.stderr.on('data', (data: Buffer) => { stderr += data.toString(); });
31
+
32
+ const timer = setTimeout(() => {
33
+ child.kill('SIGTERM');
34
+ reject(new Error(`Command timed out after ${timeout}ms: ${command} ${args.join(' ')}`));
35
+ }, timeout);
36
+
37
+ child.on('close', (code) => {
38
+ clearTimeout(timer);
39
+ resolve({ stdout, stderr, exitCode: code ?? 1 });
40
+ });
41
+
42
+ child.on('error', (err) => {
43
+ clearTimeout(timer);
44
+ reject(err);
45
+ });
46
+ });
47
+ }
@@ -0,0 +1,36 @@
1
+ /** Graceful shutdown coordination for Octo CLI */
2
+
3
+ type ShutdownCallback = () => void | Promise<void>;
4
+
5
+ const callbacks: ShutdownCallback[] = [];
6
+ let shuttingDown = false;
7
+
8
+ /** Check if shutdown has been requested */
9
+ export function isShuttingDown(): boolean {
10
+ return shuttingDown;
11
+ }
12
+
13
+ /** Register a cleanup callback to run on shutdown */
14
+ export function onShutdown(callback: ShutdownCallback): void {
15
+ callbacks.push(callback);
16
+ }
17
+
18
+ /** Trigger shutdown — runs all registered callbacks and exits */
19
+ export async function triggerShutdown(signal: string): Promise<void> {
20
+ if (shuttingDown) return; // prevent double-trigger
21
+ shuttingDown = true;
22
+
23
+ const { logger } = await import('./logger.js');
24
+ logger.warn(`Shutdown solicitado (${signal}). Cancelando operações em andamento...`);
25
+
26
+ for (const cb of callbacks) {
27
+ try {
28
+ await cb();
29
+ } catch {
30
+ // best-effort cleanup
31
+ }
32
+ }
33
+
34
+ logger.info('Shutdown completo.');
35
+ process.exit(130);
36
+ }
@@ -0,0 +1,112 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { Ollama } from 'ollama';
4
+ import { run } from '../shared/process-runner.js';
5
+ import { logger } from '../shared/logger.js';
6
+
7
+ const HEADER = '# Changelog\n';
8
+
9
+ export class ChangelogGenerator {
10
+ private ollama = new Ollama();
11
+
12
+ async generate(packageDir: string, newVersion: string): Promise<string> {
13
+ const changelogPath = join(packageDir, 'CHANGELOG.md');
14
+ const commits = await this.getCommitsSinceLastTag(packageDir);
15
+ const entry = await this.buildEntry(newVersion, commits);
16
+
17
+ const existing = await this.readChangelog(changelogPath);
18
+ const updated = this.prependEntry(existing, entry);
19
+ await writeFile(changelogPath, updated, 'utf-8');
20
+
21
+ return entry;
22
+ }
23
+
24
+ private async getCommitsSinceLastTag(cwd: string): Promise<string[]> {
25
+ const tagResult = await run('git', ['describe', '--tags', '--abbrev=0'], { cwd });
26
+ const lastTag = tagResult.exitCode === 0 ? tagResult.stdout.trim() : '';
27
+
28
+ const logArgs = lastTag
29
+ ? ['log', `${lastTag}..HEAD`, '--oneline', '--', '.']
30
+ : ['log', '--oneline', '--', '.'];
31
+
32
+ const logResult = await run('git', logArgs, { cwd });
33
+ if (logResult.exitCode !== 0 || !logResult.stdout.trim()) return [];
34
+
35
+ return logResult.stdout
36
+ .trim()
37
+ .split('\n')
38
+ .map((line) => line.replace(/^[a-f0-9]+\s+/, ''));
39
+ }
40
+
41
+ private async buildEntry(version: string, commits: string[]): Promise<string> {
42
+ const date = new Date().toISOString().slice(0, 10);
43
+
44
+ if (commits.length === 0) {
45
+ return [`## [${version}] - ${date}`, '', '### Changed', '', '- Version bump', ''].join('\n');
46
+ }
47
+
48
+ // Try LLM-generated changelog
49
+ const llmEntry = await this.generateWithLLM(version, date, commits);
50
+ if (llmEntry) return llmEntry;
51
+
52
+ // Fallback: plain commit list
53
+ const lines = [`## [${version}] - ${date}`, '', '### Changed', ''];
54
+ for (const msg of commits) {
55
+ lines.push(`- ${msg}`);
56
+ }
57
+ lines.push('');
58
+ return lines.join('\n');
59
+ }
60
+
61
+ private async generateWithLLM(version: string, date: string, commits: string[]): Promise<string | null> {
62
+ try {
63
+ const models = await this.ollama.list();
64
+ const hasModel = models.models.some((m) => m.name.startsWith('phi4'));
65
+ if (!hasModel) return null;
66
+
67
+ const commitList = commits.map((c) => `- ${c}`).join('\n');
68
+ const prompt = `You are a changelog writer. Given these git commits, generate a concise, well-organized changelog entry in Keep a Changelog format.
69
+
70
+ Group changes into appropriate sections: Added, Changed, Fixed, Removed (only include sections that apply).
71
+ Rewrite commit messages into clear, user-facing descriptions. Merge related commits into single entries when appropriate.
72
+
73
+ Commits:
74
+ ${commitList}
75
+
76
+ Output ONLY the markdown sections (### Added, ### Changed, etc.) with bullet points. No header, no version line.`;
77
+
78
+ const response = await this.ollama.generate({ model: 'phi4', prompt, stream: false });
79
+ const sections = response.response.trim();
80
+
81
+ if (!sections || sections.length < 10) return null;
82
+
83
+ return [`## [${version}] - ${date}`, '', sections, ''].join('\n');
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
88
+
89
+ private async readChangelog(path: string): Promise<string> {
90
+ try {
91
+ return await readFile(path, 'utf-8');
92
+ } catch {
93
+ return '';
94
+ }
95
+ }
96
+
97
+ private prependEntry(existing: string, entry: string): string {
98
+ if (!existing) return HEADER + '\n' + entry;
99
+
100
+ const headerIndex = existing.indexOf('# Changelog');
101
+ if (headerIndex !== -1) {
102
+ const afterHeader = existing.indexOf('\n', headerIndex);
103
+ if (afterHeader !== -1) {
104
+ const before = existing.slice(0, afterHeader + 1);
105
+ const after = existing.slice(afterHeader + 1);
106
+ return before + '\n' + entry + after;
107
+ }
108
+ }
109
+
110
+ return HEADER + '\n' + entry + existing;
111
+ }
112
+ }
@@ -0,0 +1,116 @@
1
+ import { createInterface } from 'node:readline';
2
+ import { readFile, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import semver from 'semver';
5
+ import { run } from '../shared/process-runner.js';
6
+ import { logger } from '../shared/logger.js';
7
+ import { OctoError } from '../shared/errors.js';
8
+
9
+ export type BumpType = 'patch' | 'minor' | 'major';
10
+
11
+ export interface BumpOptions {
12
+ push?: boolean;
13
+ tag?: boolean;
14
+ auto?: boolean;
15
+ }
16
+
17
+ export interface BumpResult {
18
+ package: string;
19
+ previousVersion: string;
20
+ newVersion: string;
21
+ propagated: PropagationEntry[];
22
+ }
23
+
24
+ export interface PropagationEntry {
25
+ project: string;
26
+ previousVersion: string;
27
+ newVersion: string;
28
+ skipped?: boolean;
29
+ reason?: string;
30
+ }
31
+
32
+ function confirm(message: string): Promise<boolean> {
33
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
34
+ return new Promise((resolve) => {
35
+ rl.question(message, (answer) => {
36
+ rl.close();
37
+ resolve(answer.trim().toLowerCase() === 'y');
38
+ });
39
+ });
40
+ }
41
+
42
+ export class VersionBumper {
43
+ async bump(packageDir: string, packageName: string, type: BumpType = 'patch', options: BumpOptions = {}): Promise<BumpResult> {
44
+ const pkgJsonPath = join(packageDir, 'package.json');
45
+ const originalContent = await readFile(pkgJsonPath, 'utf-8');
46
+ const pkg = JSON.parse(originalContent);
47
+ const previousVersion: string = pkg.version;
48
+
49
+ if (!semver.valid(previousVersion)) {
50
+ throw new OctoError(`Invalid version in package.json: ${previousVersion}`);
51
+ }
52
+
53
+ const newVersion = semver.inc(previousVersion, type);
54
+ if (!newVersion) {
55
+ throw new OctoError(`Failed to increment version ${previousVersion} with type ${type}`);
56
+ }
57
+
58
+ // Check uncommitted changes (skip in auto mode)
59
+ if (!options.auto) {
60
+ const status = await run('git', ['status', '--porcelain'], { cwd: packageDir });
61
+ if (status.stdout.trim().length > 0) {
62
+ logger.warn(`Uncommitted changes detected in ${packageName}:`);
63
+ logger.info(status.stdout.trim());
64
+ const accepted = await confirm('Continue with bump? (y/n) ');
65
+ if (!accepted) {
66
+ throw new OctoError('Bump aborted by user');
67
+ }
68
+ }
69
+ }
70
+
71
+ // Write new version
72
+ pkg.version = newVersion;
73
+ await writeFile(pkgJsonPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
74
+ logger.info(`${packageName}: ${previousVersion} → ${newVersion}`);
75
+
76
+ // Run build
77
+ const buildResult = await run('pnpm', ['run', 'build'], { cwd: packageDir });
78
+ if (buildResult.exitCode !== 0) {
79
+ await writeFile(pkgJsonPath, originalContent, 'utf-8');
80
+ logger.error(`Build failed for ${packageName}. Rollback applied.`);
81
+ logger.error(buildResult.stderr || buildResult.stdout);
82
+ throw new OctoError(`Build failed after bump of ${packageName}`);
83
+ }
84
+
85
+ // Commit
86
+ const commitMsg = `chore(${packageName}): bump version to ${newVersion}`;
87
+ await run('git', ['add', pkgJsonPath], { cwd: packageDir });
88
+ await run('git', ['commit', '-m', commitMsg], { cwd: packageDir });
89
+ logger.info(`Commit: ${commitMsg}`);
90
+
91
+ // Tag (if --tag or --auto)
92
+ if (options.tag || options.auto) {
93
+ const tagName = `${packageName}@${newVersion}`;
94
+ await run('git', ['tag', '-a', tagName, '-m', `Release ${tagName}`], { cwd: packageDir });
95
+ logger.info(`Tag created: ${tagName}`);
96
+ }
97
+
98
+ // Push (if --push or --auto)
99
+ if (options.push || options.auto) {
100
+ const pushResult = await run('git', ['push', '--follow-tags'], { cwd: packageDir });
101
+ if (pushResult.exitCode !== 0) {
102
+ logger.error(`Push failed: ${pushResult.stderr}`);
103
+ } else {
104
+ logger.info('Pushed to remote with tags.');
105
+ }
106
+ }
107
+
108
+ return { package: packageName, previousVersion, newVersion, propagated: [] };
109
+ }
110
+
111
+ async rollback(packageDir: string, originalContent: string): Promise<void> {
112
+ const pkgJsonPath = join(packageDir, 'package.json');
113
+ await writeFile(pkgJsonPath, originalContent, 'utf-8');
114
+ logger.info('Rollback of package.json complete.');
115
+ }
116
+ }