File size: 5,104 Bytes
a5d1c1b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61b1da1
a5d1c1b
 
61b1da1
a5d1c1b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61b1da1
a5d1c1b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61b1da1
a5d1c1b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import prompts from 'prompts';
import { projectConfigManager } from '../config/project-config.js';
import { cacheService, computeSha256 } from '../services/cache-service.js';
import { authService } from '../services/auth-service.js';
import { parseRepoUrl } from '../services/sync-service.js';
import { githubWriterService, FileChange } from '../services/github-writer-service.js';
import { logger } from '../utils/logger.js';
import { outputResult, outputError, OutputOptions } from '../utils/output.js';

export interface ProposeOptions extends OutputOptions {
  title?: string;
  message?: string;
  branch?: string;
  token?: string;
  yes?: boolean;
}

export async function handlePropose(options: ProposeOptions = {}): Promise<void> {
  const workspaceRoot = projectConfigManager.findEvbRoot();
  if (!workspaceRoot) {
    return outputError(
      'Not inside an Ever-Brain project. Run `evb init <repo>` first.',
      'NOT_IN_WORKSPACE',
      options
    );
  }

  try {
    const config = projectConfigManager.readConfig(workspaceRoot);
    const { owner, repo } = parseRepoUrl(config.source.repository);
    const baseBranch = options.branch || config.source.branch;

    const dirty = cacheService.computeDirtyState(workspaceRoot);
    if (!dirty.isDirty) {
      outputResult({ status: 'clean', message: 'No local modifications to propose.' }, options, () => {
        logger.info('No local modifications to propose (working tree clean).');
      });
      return;
    }

    // 1. Resolve token
    let token = authService.resolveToken(options.token, workspaceRoot);
    if (!token) {
      if (!options.yes && process.stdout.isTTY) {
        const authRes = await authService.promptForAuth();
        token = authRes.token;
        authService.saveGlobalToken(token);
      } else {
        return outputError(
          'GitHub authentication required to open Pull Requests. Run `evb auth login` or pass --token.',
          'AUTH_REQUIRED',
          options
        );
      }
    }

    const userInfo = await authService.validateToken(token);

    // 2. PR Title & Message
    let title = options.title;
    let message = options.message;

    if (!title && !options.yes && process.stdout.isTTY) {
      const response = await prompts([
        {
          type: 'text',
          name: 'title',
          message: 'Enter Pull Request title:',
          validate: (v) => (v && v.trim().length > 0 ? true : 'PR title is required'),
        },
        {
          type: 'text',
          name: 'message',
          message: 'Enter Pull Request description / summary:',
        },
      ]);
      title = response.title;
      message = response.message || response.title;
    }

    if (!title) {
      title = 'Update Ever-Brain Project Intelligence';
    }
    if (!message) {
      message = title;
    }

    // 3. Build FileChanges payload
    const changes: FileChange[] = [];
    const updatedBaselineHashes: Record<string, string> = {
      ...(projectConfigManager.readState(workspaceRoot)?.files || {}),
    };

    for (const f of dirty.modified) {
      const content = cacheService.readCachedFile(workspaceRoot, f);
      if (content !== null) {
        changes.push({ path: f, content });
        updatedBaselineHashes[f] = computeSha256(content);
      }
    }

    for (const f of dirty.added) {
      const content = cacheService.readCachedFile(workspaceRoot, f);
      if (content !== null) {
        changes.push({ path: f, content });
        updatedBaselineHashes[f] = computeSha256(content);
      }
    }

    for (const f of dirty.deleted) {
      changes.push({ path: f, content: null });
      delete updatedBaselineHashes[f];
    }

    // 4. Create proposal branch & open PR
    logger.info(`Opening Pull Request against ${owner}/${repo} (${baseBranch})...`);
    const result = await githubWriterService.createProposalPullRequest(
      owner,
      repo,
      baseBranch,
      changes,
      title,
      message,
      token,
      userInfo.username
    );

    // 5. Update state.yaml to reflect proposed baseline
    projectConfigManager.writeState(workspaceRoot, {
      last_sync: new Date().toISOString(),
      source_revision: result.commitSha,
      cached_files: Object.keys(updatedBaselineHashes).length,
      files: updatedBaselineHashes,
    });

    outputResult(
      {
        status: 'proposed',
        pullRequestUrl: result.pullRequestUrl,
        pullRequestNumber: result.pullRequestNumber,
        branch: result.branch,
        commitSha: result.commitSha,
        isFork: result.isFork,
        filesChanged: changes.length,
      },
      options,
      () => {
        logger.success(`Opened Pull Request #${result.pullRequestNumber}!`);
        logger.info(`PR URL: ${logger.url(result.pullRequestUrl)}`);
        logger.info(`Branch: ${result.branch}${result.isFork ? ' (forked)' : ''}`);
        logger.success('Proposal submitted for team review.');
      }
    );
  } catch (err: unknown) {
    const message = err instanceof Error ? err.message : String(err);
    outputError(message, 'PROPOSE_ERROR', options);
  }
}