File size: 5,155 Bytes
a5d1c1b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61b1da1
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
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 PushOptions extends OutputOptions {
  message?: string;
  branch?: string;
  token?: string;
  yes?: boolean;
}

export async function handlePush(options: PushOptions = {}): 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 targetBranch = options.branch || config.source.branch;

    const dirty = cacheService.computeDirtyState(workspaceRoot);
    if (!dirty.isDirty) {
      outputResult({ status: 'clean', message: 'No local modifications to push.' }, options, () => {
        logger.info('No local modifications to push (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. Run `evb auth login` or pass --token / GITHUB_TOKEN.',
          'AUTH_REQUIRED',
          options
        );
      }
    }

    // 2. Validate user identity and repo permissions
    const userInfo = await authService.validateToken(token);
    const permission = await authService.getRepoPermission(owner, repo, userInfo.username, token);

    if (permission !== 'admin' && permission !== 'write') {
      return outputError(
        `User @${userInfo.username} has '${permission}' access on ${owner}/${repo} (write permission required for direct push).\nUse 'evb propose' to open a Pull Request for team review.`,
        'PERMISSION_DENIED',
        options
      );
    }

    // 3. Commit message
    let commitMessage = options.message;
    if (!commitMessage) {
      if (!options.yes && process.stdout.isTTY) {
        const msgRes = await prompts({
          type: 'text',
          name: 'msg',
          message: 'Enter commit message:',
          validate: (v) => (v && v.trim().length > 0 ? true : 'Commit message cannot be empty'),
        });
        commitMessage = msgRes.msg;
      }
      if (!commitMessage) {
        commitMessage = 'Update project intelligence via Ever-Brain';
      }
    }

    // 4. 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];
    }

    // 5. Push direct commit to GitHub
    logger.info(`Pushing ${changes.length} file change(s) directly to ${owner}/${repo} (${targetBranch})...`);
    const result = await githubWriterService.createDirectCommit(
      owner,
      repo,
      targetBranch,
      changes,
      commitMessage,
      token
    );

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

    outputResult(
      {
        status: 'pushed',
        commitSha: result.commitSha,
        branch: result.branch,
        commitUrl: result.commitUrl,
        filesChanged: changes.length,
      },
      options,
      () => {
        logger.success(`Pushed commit ${result.commitSha.slice(0, 7)} to branch '${result.branch}'!`);
        logger.info(`Commit URL: ${logger.url(result.commitUrl)}`);
        logger.success('Local working tree is now clean.');
      }
    );
  } catch (err: unknown) {
    const message = err instanceof Error ? err.message : String(err);
    outputError(message, 'PUSH_ERROR', options);
  }
}