tycono-server 0.1.0-beta.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/bin/cli.js +35 -0
- package/bin/server.ts +160 -0
- package/package.json +50 -0
- package/src/api/package.json +31 -0
- package/src/api/src/create-app.ts +90 -0
- package/src/api/src/create-server.ts +251 -0
- package/src/api/src/engine/agent-loop.ts +738 -0
- package/src/api/src/engine/authority-validator.ts +149 -0
- package/src/api/src/engine/context-assembler.ts +912 -0
- package/src/api/src/engine/index.ts +27 -0
- package/src/api/src/engine/knowledge-gate.ts +365 -0
- package/src/api/src/engine/llm-adapter.ts +304 -0
- package/src/api/src/engine/org-tree.ts +270 -0
- package/src/api/src/engine/role-lifecycle.ts +369 -0
- package/src/api/src/engine/runners/claude-cli.ts +796 -0
- package/src/api/src/engine/runners/direct-api.ts +66 -0
- package/src/api/src/engine/runners/index.ts +30 -0
- package/src/api/src/engine/runners/types.ts +95 -0
- package/src/api/src/engine/skill-template.ts +134 -0
- package/src/api/src/engine/tools/definitions.ts +201 -0
- package/src/api/src/engine/tools/executor.ts +611 -0
- package/src/api/src/routes/active-sessions.ts +134 -0
- package/src/api/src/routes/coins.ts +153 -0
- package/src/api/src/routes/company.ts +57 -0
- package/src/api/src/routes/cost.ts +141 -0
- package/src/api/src/routes/engine.ts +220 -0
- package/src/api/src/routes/execute.ts +1075 -0
- package/src/api/src/routes/git.ts +211 -0
- package/src/api/src/routes/knowledge.ts +378 -0
- package/src/api/src/routes/operations.ts +309 -0
- package/src/api/src/routes/preferences.ts +63 -0
- package/src/api/src/routes/presets.ts +123 -0
- package/src/api/src/routes/projects.ts +82 -0
- package/src/api/src/routes/quests.ts +41 -0
- package/src/api/src/routes/roles.ts +112 -0
- package/src/api/src/routes/save.ts +152 -0
- package/src/api/src/routes/sessions.ts +288 -0
- package/src/api/src/routes/setup.ts +437 -0
- package/src/api/src/routes/skills.ts +357 -0
- package/src/api/src/routes/speech.ts +959 -0
- package/src/api/src/routes/supervision.ts +136 -0
- package/src/api/src/routes/sync.ts +165 -0
- package/src/api/src/server.ts +59 -0
- package/src/api/src/services/activity-stream.ts +184 -0
- package/src/api/src/services/activity-tracker.ts +115 -0
- package/src/api/src/services/claude-md-manager.ts +94 -0
- package/src/api/src/services/company-config.ts +115 -0
- package/src/api/src/services/database.ts +77 -0
- package/src/api/src/services/digest-engine.ts +313 -0
- package/src/api/src/services/execution-manager.ts +1036 -0
- package/src/api/src/services/file-reader.ts +77 -0
- package/src/api/src/services/git-save.ts +614 -0
- package/src/api/src/services/job-manager.ts +16 -0
- package/src/api/src/services/knowledge-importer.ts +466 -0
- package/src/api/src/services/markdown-parser.ts +173 -0
- package/src/api/src/services/port-registry.ts +222 -0
- package/src/api/src/services/preferences.ts +150 -0
- package/src/api/src/services/preset-loader.ts +149 -0
- package/src/api/src/services/pricing.ts +34 -0
- package/src/api/src/services/scaffold.ts +546 -0
- package/src/api/src/services/session-store.ts +340 -0
- package/src/api/src/services/supervisor-heartbeat.ts +897 -0
- package/src/api/src/services/team-recommender.ts +382 -0
- package/src/api/src/services/token-ledger.ts +127 -0
- package/src/api/src/services/wave-messages.ts +194 -0
- package/src/api/src/services/wave-multiplexer.ts +356 -0
- package/src/api/src/services/wave-tracker.ts +359 -0
- package/src/api/src/utils/role-level.ts +31 -0
- package/src/core/scaffolder.ts +620 -0
- package/src/shared/types.ts +224 -0
- package/templates/CLAUDE.md.tmpl +239 -0
- package/templates/company.md.tmpl +17 -0
- package/templates/gitignore.tmpl +28 -0
- package/templates/roles.md.tmpl +8 -0
- package/templates/skills/_manifest.json +23 -0
- package/templates/skills/agent-browser/SKILL.md +159 -0
- package/templates/skills/agent-browser/meta.json +19 -0
- package/templates/skills/akb-linter/SKILL.md +125 -0
- package/templates/skills/akb-linter/meta.json +12 -0
- package/templates/skills/knowledge-gate/SKILL.md +120 -0
- package/templates/skills/knowledge-gate/meta.json +12 -0
- package/templates/teams/agency.json +58 -0
- package/templates/teams/research.json +58 -0
- package/templates/teams/startup.json +58 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import matter from 'gray-matter';
|
|
4
|
+
import { glob } from 'glob';
|
|
5
|
+
|
|
6
|
+
function findCompanyRoot(): string {
|
|
7
|
+
if (process.env.COMPANY_ROOT) return process.env.COMPANY_ROOT;
|
|
8
|
+
// Walk up from cwd to find knowledge/CLAUDE.md (project root marker)
|
|
9
|
+
let dir = process.cwd();
|
|
10
|
+
while (dir !== path.dirname(dir)) {
|
|
11
|
+
if (fs.existsSync(path.join(dir, 'knowledge', 'CLAUDE.md'))) return dir;
|
|
12
|
+
dir = path.dirname(dir);
|
|
13
|
+
}
|
|
14
|
+
return process.cwd();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export let COMPANY_ROOT = findCompanyRoot();
|
|
18
|
+
|
|
19
|
+
/** Update COMPANY_ROOT at runtime (e.g. after scaffold picks a new location) */
|
|
20
|
+
export function setCompanyRoot(root: string): void {
|
|
21
|
+
COMPANY_ROOT = root;
|
|
22
|
+
process.env.COMPANY_ROOT = root;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolve(...segments: string[]): string {
|
|
26
|
+
return path.resolve(COMPANY_ROOT, ...segments);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Markdown 파일을 읽고 frontmatter와 content를 분리하여 반환한다.
|
|
31
|
+
*/
|
|
32
|
+
export function readMarkdown(filePath: string): { frontmatter: Record<string, unknown>; content: string } {
|
|
33
|
+
const absolute = resolve(filePath);
|
|
34
|
+
if (!fs.existsSync(absolute)) {
|
|
35
|
+
throw new FileNotFoundError(filePath);
|
|
36
|
+
}
|
|
37
|
+
const raw = fs.readFileSync(absolute, 'utf-8');
|
|
38
|
+
const { data, content } = matter(raw);
|
|
39
|
+
return { frontmatter: data, content };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 파일의 raw text를 반환한다.
|
|
44
|
+
*/
|
|
45
|
+
export function readFile(filePath: string): string {
|
|
46
|
+
const absolute = resolve(filePath);
|
|
47
|
+
if (!fs.existsSync(absolute)) {
|
|
48
|
+
throw new FileNotFoundError(filePath);
|
|
49
|
+
}
|
|
50
|
+
return fs.readFileSync(absolute, 'utf-8');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 디렉토리 내 파일 목록을 반환한다.
|
|
55
|
+
* pattern 미지정 시 *.md 파일만 반환.
|
|
56
|
+
*/
|
|
57
|
+
export function listFiles(dirPath: string, pattern = '*.md'): string[] {
|
|
58
|
+
const absolute = resolve(dirPath);
|
|
59
|
+
if (!fs.existsSync(absolute)) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
return glob.sync(pattern, { cwd: absolute }).sort();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 파일 존재 여부를 확인한다.
|
|
67
|
+
*/
|
|
68
|
+
export function fileExists(filePath: string): boolean {
|
|
69
|
+
return fs.existsSync(resolve(filePath));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class FileNotFoundError extends Error {
|
|
73
|
+
constructor(filePath: string) {
|
|
74
|
+
super(`File not found: ${filePath}`);
|
|
75
|
+
this.name = 'FileNotFoundError';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* git-save.ts — Git commit + push 기반 세이브 시스템
|
|
3
|
+
*
|
|
4
|
+
* 모든 진행 상황을 GitHub에 영속화한다.
|
|
5
|
+
* 의존성 0: child_process.execSync만 사용.
|
|
6
|
+
*
|
|
7
|
+
* 핵심 원칙 (data-persistence-architecture.md):
|
|
8
|
+
* - SAVE_PATHS의 AKB 파일만 stage (유저 소스코드 미포함)
|
|
9
|
+
* - git 없으면 graceful 비활성화
|
|
10
|
+
* - remote 없으면 commit만 (push skip)
|
|
11
|
+
*
|
|
12
|
+
* Dual-Repo Support (DG-001):
|
|
13
|
+
* - repo='akb': AKB repo (COMPANY_ROOT), SAVE_PATHS 필터 적용
|
|
14
|
+
* - repo='code': Code repo (codeRoot), SAVE_PATHS 필터 비활성화
|
|
15
|
+
*/
|
|
16
|
+
import { execSync } from 'node:child_process';
|
|
17
|
+
import { readFileSync } from 'node:fs';
|
|
18
|
+
import { join, resolve } from 'node:path';
|
|
19
|
+
import { resolveCodeRoot } from './company-config.js';
|
|
20
|
+
|
|
21
|
+
export type RepoType = 'akb' | 'code';
|
|
22
|
+
|
|
23
|
+
export interface GitStatus {
|
|
24
|
+
dirty: boolean;
|
|
25
|
+
modified: string[];
|
|
26
|
+
untracked: string[];
|
|
27
|
+
lastCommit: { sha: string; message: string; date: string } | null;
|
|
28
|
+
branch: string;
|
|
29
|
+
hasRemote: boolean;
|
|
30
|
+
synced: boolean;
|
|
31
|
+
noGit: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SaveResult {
|
|
35
|
+
commitSha: string;
|
|
36
|
+
message: string;
|
|
37
|
+
filesChanged: number;
|
|
38
|
+
pushed: boolean;
|
|
39
|
+
pushError?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface CommitInfo {
|
|
43
|
+
sha: string;
|
|
44
|
+
shortSha: string;
|
|
45
|
+
message: string;
|
|
46
|
+
date: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface RestoreResult {
|
|
50
|
+
commitSha: string;
|
|
51
|
+
restoredFiles: string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface PullResult {
|
|
55
|
+
status: 'ok' | 'dirty' | 'diverged' | 'up-to-date' | 'no-remote' | 'error';
|
|
56
|
+
message: string;
|
|
57
|
+
commits?: number;
|
|
58
|
+
behind?: number;
|
|
59
|
+
ahead?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface SyncStatus {
|
|
63
|
+
ahead: number;
|
|
64
|
+
behind: number;
|
|
65
|
+
branch: string;
|
|
66
|
+
remote: string;
|
|
67
|
+
hasRemote: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Paths to include in save (relative to root).
|
|
72
|
+
* Only AKB files — never user source code.
|
|
73
|
+
* See: knowledge/data-persistence-architecture.md §2
|
|
74
|
+
*/
|
|
75
|
+
const SAVE_PATHS = [
|
|
76
|
+
'knowledge/',
|
|
77
|
+
'.claude/skills/',
|
|
78
|
+
'.tycono/',
|
|
79
|
+
'CLAUDE.md',
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Resolve repository root based on repo type
|
|
84
|
+
* @param akbRoot - AKB repository root (COMPANY_ROOT)
|
|
85
|
+
* @param repo - Repository type ('akb' or 'code')
|
|
86
|
+
* @returns Resolved repository root path
|
|
87
|
+
*/
|
|
88
|
+
function resolveRepoRoot(akbRoot: string, repo: RepoType = 'akb'): string {
|
|
89
|
+
if (repo === 'akb') {
|
|
90
|
+
return akbRoot;
|
|
91
|
+
}
|
|
92
|
+
return resolveCodeRoot(akbRoot);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if SAVE_PATHS filter should be applied
|
|
97
|
+
* @param repo - Repository type
|
|
98
|
+
* @returns true if filter should be applied (AKB only)
|
|
99
|
+
*/
|
|
100
|
+
function shouldFilterPaths(repo: RepoType = 'akb'): boolean {
|
|
101
|
+
return repo === 'akb';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function run(cmd: string, cwd: string): string {
|
|
105
|
+
try {
|
|
106
|
+
return execSync(cmd, { cwd, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
107
|
+
} catch {
|
|
108
|
+
return '';
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function runOrThrow(cmd: string, cwd: string): string {
|
|
113
|
+
return execSync(cmd, { cwd, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Check if git binary is available */
|
|
117
|
+
function isGitAvailable(): boolean {
|
|
118
|
+
try {
|
|
119
|
+
execSync('git --version', { timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
120
|
+
return true;
|
|
121
|
+
} catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Check if directory is (or is inside) a git repository */
|
|
127
|
+
function isGitRepo(root: string): boolean {
|
|
128
|
+
return run('git rev-parse --is-inside-work-tree', root) === 'true';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Check if directory is the root of its own git repository (has .git here) */
|
|
132
|
+
function isGitRoot(root: string): boolean {
|
|
133
|
+
const toplevel = run('git rev-parse --show-toplevel', root);
|
|
134
|
+
if (!toplevel) return false;
|
|
135
|
+
return resolve(toplevel) === resolve(root);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Initialize a new git repository
|
|
140
|
+
* @param root - AKB repository root (COMPANY_ROOT)
|
|
141
|
+
* @param repo - Repository type ('akb' or 'code'), default 'akb'
|
|
142
|
+
*/
|
|
143
|
+
export function gitInit(root: string, repo: RepoType = 'akb'): { ok: boolean; message: string; noGitBinary?: boolean } {
|
|
144
|
+
if (!isGitAvailable()) {
|
|
145
|
+
return { ok: false, message: 'git is not installed', noGitBinary: true };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const repoRoot = resolveRepoRoot(root, repo);
|
|
149
|
+
|
|
150
|
+
if (isGitRoot(repoRoot)) {
|
|
151
|
+
return { ok: true, message: 'Already a git repository' };
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
runOrThrow('git init', repoRoot);
|
|
155
|
+
runOrThrow('git add -A', repoRoot);
|
|
156
|
+
runOrThrow('git commit -m "Initial commit by Tycono"', repoRoot);
|
|
157
|
+
return { ok: true, message: 'Git repository initialized with initial commit' };
|
|
158
|
+
} catch (err) {
|
|
159
|
+
return { ok: false, message: err instanceof Error ? err.message : 'git init failed' };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get current git status. Returns noGit=true if not a git repo.
|
|
165
|
+
* @param root - AKB repository root (COMPANY_ROOT)
|
|
166
|
+
* @param repo - Repository type ('akb' or 'code'), default 'akb'
|
|
167
|
+
*/
|
|
168
|
+
export function getGitStatus(root: string, repo: RepoType = 'akb'): GitStatus {
|
|
169
|
+
const repoRoot = resolveRepoRoot(root, repo);
|
|
170
|
+
|
|
171
|
+
if (!isGitRoot(repoRoot)) {
|
|
172
|
+
return {
|
|
173
|
+
dirty: false,
|
|
174
|
+
modified: [],
|
|
175
|
+
untracked: [],
|
|
176
|
+
lastCommit: null,
|
|
177
|
+
branch: '',
|
|
178
|
+
hasRemote: false,
|
|
179
|
+
synced: false,
|
|
180
|
+
noGit: true,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const porcelain = run('git status --porcelain', repoRoot);
|
|
185
|
+
const lines = porcelain ? porcelain.split('\n').filter(Boolean) : [];
|
|
186
|
+
|
|
187
|
+
const modified: string[] = [];
|
|
188
|
+
const untracked: string[] = [];
|
|
189
|
+
const applyFilter = shouldFilterPaths(repo);
|
|
190
|
+
|
|
191
|
+
for (const line of lines) {
|
|
192
|
+
const status = line.substring(0, 2);
|
|
193
|
+
const file = line.substring(3);
|
|
194
|
+
|
|
195
|
+
// For CODE repo, include all files; for AKB, filter to SAVE_PATHS only
|
|
196
|
+
if (applyFilter && !SAVE_PATHS.some(p => file.startsWith(p))) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (status.includes('?')) {
|
|
201
|
+
untracked.push(file);
|
|
202
|
+
} else {
|
|
203
|
+
modified.push(file);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let lastCommit: GitStatus['lastCommit'] = null;
|
|
208
|
+
const logLine = run('git log -1 --format=%H%n%s%n%aI', repoRoot);
|
|
209
|
+
if (logLine) {
|
|
210
|
+
const [sha, message, date] = logLine.split('\n');
|
|
211
|
+
if (sha) lastCommit = { sha, message: message ?? '', date: date ?? '' };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const branch = run('git rev-parse --abbrev-ref HEAD', repoRoot) || 'unknown';
|
|
215
|
+
const hasRemote = !!run('git remote', repoRoot);
|
|
216
|
+
|
|
217
|
+
let synced = true;
|
|
218
|
+
if (hasRemote) {
|
|
219
|
+
const local = run('git rev-parse HEAD', repoRoot);
|
|
220
|
+
const remote = run(`git rev-parse origin/${branch}`, repoRoot);
|
|
221
|
+
synced = !!local && local === remote;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
dirty: modified.length > 0 || untracked.length > 0,
|
|
226
|
+
modified,
|
|
227
|
+
untracked,
|
|
228
|
+
lastCommit,
|
|
229
|
+
branch,
|
|
230
|
+
hasRemote,
|
|
231
|
+
synced,
|
|
232
|
+
noGit: false,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Commit + push save-tracked files
|
|
238
|
+
* @param root - AKB repository root (COMPANY_ROOT)
|
|
239
|
+
* @param message - Optional commit message
|
|
240
|
+
* @param repo - Repository type ('akb' or 'code'), default 'akb'
|
|
241
|
+
*/
|
|
242
|
+
export function gitSave(root: string, message?: string, repo: RepoType = 'akb'): SaveResult {
|
|
243
|
+
const repoRoot = resolveRepoRoot(root, repo);
|
|
244
|
+
|
|
245
|
+
if (!isGitRoot(repoRoot)) {
|
|
246
|
+
throw new Error('Not a git repository. Run "git init" first.');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const status = getGitStatus(root, repo);
|
|
250
|
+
if (!status.dirty) {
|
|
251
|
+
throw new Error('No changes to save');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const allFiles = [...status.modified, ...status.untracked];
|
|
255
|
+
|
|
256
|
+
// Stage files
|
|
257
|
+
for (const file of allFiles) {
|
|
258
|
+
runOrThrow(`git add "${file}"`, repoRoot);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const prefix = '[tycono] ';
|
|
262
|
+
const commitMsg = message
|
|
263
|
+
? `${prefix}${message}`
|
|
264
|
+
: `${prefix}Save — ${new Date().toISOString().slice(0, 16)} (${allFiles.length} files)`;
|
|
265
|
+
runOrThrow(`git commit -m "${commitMsg.replace(/"/g, '\\"')}"`, repoRoot);
|
|
266
|
+
|
|
267
|
+
const sha = run('git rev-parse HEAD', repoRoot);
|
|
268
|
+
|
|
269
|
+
let pushed = false;
|
|
270
|
+
let pushError: string | undefined;
|
|
271
|
+
if (status.hasRemote) {
|
|
272
|
+
try {
|
|
273
|
+
runOrThrow(`git push origin ${status.branch}`, repoRoot);
|
|
274
|
+
pushed = true;
|
|
275
|
+
} catch (err) {
|
|
276
|
+
pushError = err instanceof Error ? err.message : 'Push failed';
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
commitSha: sha,
|
|
282
|
+
message: commitMsg,
|
|
283
|
+
filesChanged: allFiles.length,
|
|
284
|
+
pushed,
|
|
285
|
+
pushError,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Get commit history
|
|
291
|
+
* @param root - AKB repository root (COMPANY_ROOT)
|
|
292
|
+
* @param limit - Maximum number of commits to retrieve
|
|
293
|
+
* @param repo - Repository type ('akb' or 'code'), default 'akb'
|
|
294
|
+
*/
|
|
295
|
+
export function gitHistory(root: string, limit = 20, repo: RepoType = 'akb'): CommitInfo[] {
|
|
296
|
+
const repoRoot = resolveRepoRoot(root, repo);
|
|
297
|
+
|
|
298
|
+
if (!isGitRoot(repoRoot)) return [];
|
|
299
|
+
|
|
300
|
+
const log = run(`git log --format=%H%n%h%n%s%n%aI -n ${limit}`, repoRoot);
|
|
301
|
+
if (!log) return [];
|
|
302
|
+
|
|
303
|
+
const lines = log.split('\n');
|
|
304
|
+
const commits: CommitInfo[] = [];
|
|
305
|
+
|
|
306
|
+
for (let i = 0; i + 3 < lines.length; i += 4) {
|
|
307
|
+
commits.push({
|
|
308
|
+
sha: lines[i],
|
|
309
|
+
shortSha: lines[i + 1],
|
|
310
|
+
message: lines[i + 2],
|
|
311
|
+
date: lines[i + 3],
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return commits;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Restore files from a previous commit (non-destructive: creates new commit)
|
|
320
|
+
* @param root - AKB repository root (COMPANY_ROOT)
|
|
321
|
+
* @param sha - Commit SHA to restore from
|
|
322
|
+
* @param paths - Optional paths to restore (defaults to SAVE_PATHS for AKB, all for CODE)
|
|
323
|
+
* @param repo - Repository type ('akb' or 'code'), default 'akb'
|
|
324
|
+
*/
|
|
325
|
+
export function gitRestore(root: string, sha: string, paths?: string[], repo: RepoType = 'akb'): RestoreResult {
|
|
326
|
+
const repoRoot = resolveRepoRoot(root, repo);
|
|
327
|
+
|
|
328
|
+
if (!isGitRoot(repoRoot)) {
|
|
329
|
+
throw new Error('Not a git repository');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// For CODE repo without explicit paths, restore everything (use '.')
|
|
333
|
+
// For AKB repo, use SAVE_PATHS
|
|
334
|
+
const targetPaths = paths?.length ? paths : (repo === 'code' ? ['.'] : SAVE_PATHS);
|
|
335
|
+
const restoredFiles: string[] = [];
|
|
336
|
+
|
|
337
|
+
for (const p of targetPaths) {
|
|
338
|
+
try {
|
|
339
|
+
runOrThrow(`git checkout ${sha} -- "${p}"`, repoRoot);
|
|
340
|
+
restoredFiles.push(p);
|
|
341
|
+
} catch {
|
|
342
|
+
// Path may not exist in that commit — skip
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (restoredFiles.length === 0) {
|
|
347
|
+
throw new Error('No files could be restored from that commit');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const msg = `[tycono] Restore from ${sha.slice(0, 7)} (${restoredFiles.length} paths)`;
|
|
351
|
+
runOrThrow('git add -A', repoRoot);
|
|
352
|
+
runOrThrow(`git commit -m "${msg}"`, repoRoot);
|
|
353
|
+
|
|
354
|
+
const newSha = run('git rev-parse HEAD', repoRoot);
|
|
355
|
+
|
|
356
|
+
return { commitSha: newSha, restoredFiles };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Fetch remote and return ahead/behind status
|
|
361
|
+
* @param root - AKB repository root (COMPANY_ROOT)
|
|
362
|
+
* @param repo - Repository type ('akb' or 'code'), default 'akb'
|
|
363
|
+
*/
|
|
364
|
+
export function gitFetchStatus(root: string, repo: RepoType = 'akb'): SyncStatus {
|
|
365
|
+
const repoRoot = resolveRepoRoot(root, repo);
|
|
366
|
+
|
|
367
|
+
if (!isGitRoot(repoRoot)) {
|
|
368
|
+
return { ahead: 0, behind: 0, branch: '', remote: '', hasRemote: false };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const branch = run('git rev-parse --abbrev-ref HEAD', repoRoot) || 'unknown';
|
|
372
|
+
const hasRemote = !!run('git remote', repoRoot);
|
|
373
|
+
|
|
374
|
+
if (!hasRemote) {
|
|
375
|
+
return { ahead: 0, behind: 0, branch, remote: '', hasRemote: false };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Fetch from remote (timeout 15s for network)
|
|
379
|
+
try {
|
|
380
|
+
execSync('git fetch origin', { cwd: repoRoot, encoding: 'utf-8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
381
|
+
} catch {
|
|
382
|
+
// Fetch failed (no network, etc.) — return what we know
|
|
383
|
+
return { ahead: 0, behind: 0, branch, remote: 'origin', hasRemote: true };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Count ahead/behind
|
|
387
|
+
const revList = run(`git rev-list --left-right --count HEAD...origin/${branch}`, repoRoot);
|
|
388
|
+
let ahead = 0;
|
|
389
|
+
let behind = 0;
|
|
390
|
+
if (revList) {
|
|
391
|
+
const parts = revList.split(/\s+/);
|
|
392
|
+
ahead = parseInt(parts[0], 10) || 0;
|
|
393
|
+
behind = parseInt(parts[1], 10) || 0;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return { ahead, behind, branch, remote: 'origin', hasRemote: true };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Safe pull (fast-forward only)
|
|
401
|
+
* @param root - AKB repository root (COMPANY_ROOT)
|
|
402
|
+
* @param repo - Repository type ('akb' or 'code'), default 'akb'
|
|
403
|
+
*/
|
|
404
|
+
export function gitPull(root: string, repo: RepoType = 'akb'): PullResult {
|
|
405
|
+
const repoRoot = resolveRepoRoot(root, repo);
|
|
406
|
+
|
|
407
|
+
if (!isGitRoot(repoRoot)) {
|
|
408
|
+
return { status: 'error', message: 'Not a git repository' };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const branch = run('git rev-parse --abbrev-ref HEAD', repoRoot) || 'unknown';
|
|
412
|
+
const hasRemote = !!run('git remote', repoRoot);
|
|
413
|
+
|
|
414
|
+
if (!hasRemote) {
|
|
415
|
+
return { status: 'no-remote', message: 'No remote configured' };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Fetch first
|
|
419
|
+
try {
|
|
420
|
+
execSync('git fetch origin', { cwd: repoRoot, encoding: 'utf-8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
421
|
+
} catch {
|
|
422
|
+
return { status: 'error', message: 'Failed to fetch from remote' };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Check for uncommitted changes
|
|
426
|
+
const porcelain = run('git status --porcelain', repoRoot);
|
|
427
|
+
if (porcelain) {
|
|
428
|
+
return { status: 'dirty', message: 'Uncommitted changes — save or stash before pulling' };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Check ahead/behind
|
|
432
|
+
const revList = run(`git rev-list --left-right --count HEAD...origin/${branch}`, repoRoot);
|
|
433
|
+
let ahead = 0;
|
|
434
|
+
let behind = 0;
|
|
435
|
+
if (revList) {
|
|
436
|
+
const parts = revList.split(/\s+/);
|
|
437
|
+
ahead = parseInt(parts[0], 10) || 0;
|
|
438
|
+
behind = parseInt(parts[1], 10) || 0;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (behind === 0) {
|
|
442
|
+
return { status: 'up-to-date', message: 'Already up to date', ahead, behind: 0 };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (ahead > 0 && behind > 0) {
|
|
446
|
+
return { status: 'diverged', message: `Branches diverged (${ahead} ahead, ${behind} behind) — manual merge needed`, ahead, behind };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Safe fast-forward pull
|
|
450
|
+
try {
|
|
451
|
+
runOrThrow(`git pull --ff-only origin ${branch}`, repoRoot);
|
|
452
|
+
return { status: 'ok', message: `Pulled ${behind} commit(s)`, commits: behind, ahead: 0, behind: 0 };
|
|
453
|
+
} catch (err) {
|
|
454
|
+
return { status: 'error', message: err instanceof Error ? err.message : 'Pull failed' };
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ─── GitHub Integration ───────────────────────────
|
|
459
|
+
|
|
460
|
+
export interface GitHubStatus {
|
|
461
|
+
ghInstalled: boolean;
|
|
462
|
+
authenticated: boolean;
|
|
463
|
+
username?: string;
|
|
464
|
+
hasRemote: boolean;
|
|
465
|
+
remoteUrl?: string;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export interface GitHubCreateResult {
|
|
469
|
+
ok: boolean;
|
|
470
|
+
message: string;
|
|
471
|
+
repoUrl?: string;
|
|
472
|
+
remoteUrl?: string;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Check GitHub CLI availability and auth status
|
|
477
|
+
*/
|
|
478
|
+
export function githubStatus(root: string, repo: RepoType = 'akb'): GitHubStatus {
|
|
479
|
+
// Check gh CLI
|
|
480
|
+
let ghInstalled = false;
|
|
481
|
+
try {
|
|
482
|
+
execSync('gh --version', { timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
483
|
+
ghInstalled = true;
|
|
484
|
+
} catch {
|
|
485
|
+
// gh not installed
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (!ghInstalled) {
|
|
489
|
+
return { ghInstalled: false, authenticated: false, hasRemote: false };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Check auth
|
|
493
|
+
let authenticated = false;
|
|
494
|
+
let username: string | undefined;
|
|
495
|
+
try {
|
|
496
|
+
const status = execSync('gh auth status', {
|
|
497
|
+
timeout: 5000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
498
|
+
}).trim();
|
|
499
|
+
authenticated = true;
|
|
500
|
+
const match = status.match(/Logged in to github\.com account (\S+)/i)
|
|
501
|
+
?? status.match(/account (\S+)/i);
|
|
502
|
+
if (match) username = match[1];
|
|
503
|
+
} catch (err) {
|
|
504
|
+
// gh auth status exits 1 if not logged in, but still outputs info to stderr
|
|
505
|
+
const output = err instanceof Error ? (err as { stderr?: string }).stderr ?? '' : '';
|
|
506
|
+
if (output.includes('Logged in')) {
|
|
507
|
+
authenticated = true;
|
|
508
|
+
const match = output.match(/account (\S+)/i);
|
|
509
|
+
if (match) username = match[1];
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Check remote
|
|
514
|
+
let hasRemote = false;
|
|
515
|
+
let remoteUrl: string | undefined;
|
|
516
|
+
try {
|
|
517
|
+
const repoRoot = resolveRepoRoot(root, repo);
|
|
518
|
+
if (isGitRoot(repoRoot)) {
|
|
519
|
+
remoteUrl = run('git remote get-url origin', repoRoot) || undefined;
|
|
520
|
+
hasRemote = !!remoteUrl;
|
|
521
|
+
}
|
|
522
|
+
} catch {
|
|
523
|
+
// ignore
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return { ghInstalled, authenticated, username, hasRemote, remoteUrl };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Create a GitHub repo, set remote, and push
|
|
531
|
+
*/
|
|
532
|
+
export function githubCreateRepo(
|
|
533
|
+
root: string,
|
|
534
|
+
repoName: string,
|
|
535
|
+
visibility: 'private' | 'public' = 'private',
|
|
536
|
+
repo: RepoType = 'akb',
|
|
537
|
+
): GitHubCreateResult {
|
|
538
|
+
const repoRoot = resolveRepoRoot(root, repo);
|
|
539
|
+
|
|
540
|
+
// Auto-init git if not a proper git root (e.g. fresh init, or nested inside parent repo)
|
|
541
|
+
if (!isGitRoot(repoRoot)) {
|
|
542
|
+
const initResult = gitInit(root, repo);
|
|
543
|
+
if (!initResult.ok) {
|
|
544
|
+
return { ok: false, message: initResult.message };
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Check gh + auth
|
|
549
|
+
const status = githubStatus(root, repo);
|
|
550
|
+
if (!status.ghInstalled) {
|
|
551
|
+
return { ok: false, message: 'GitHub CLI (gh) is not installed' };
|
|
552
|
+
}
|
|
553
|
+
if (!status.authenticated) {
|
|
554
|
+
return { ok: false, message: 'Not logged in to GitHub — run "gh auth login" first' };
|
|
555
|
+
}
|
|
556
|
+
if (status.hasRemote) {
|
|
557
|
+
return { ok: false, message: `Remote already configured: ${status.remoteUrl}` };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Create repo + set remote + push
|
|
561
|
+
try {
|
|
562
|
+
const flag = visibility === 'public' ? '--public' : '--private';
|
|
563
|
+
const result = execSync(
|
|
564
|
+
`gh repo create "${repoName}" ${flag} --source=. --remote=origin --push`,
|
|
565
|
+
{ cwd: repoRoot, encoding: 'utf-8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'] },
|
|
566
|
+
).trim();
|
|
567
|
+
|
|
568
|
+
// Extract repo URL from output
|
|
569
|
+
const urlMatch = result.match(/(https:\/\/github\.com\/\S+)/);
|
|
570
|
+
const repoUrl = urlMatch ? urlMatch[1] : undefined;
|
|
571
|
+
const remoteUrl = run('git remote get-url origin', repoRoot) || undefined;
|
|
572
|
+
|
|
573
|
+
return { ok: true, message: 'Repository created and pushed', repoUrl, remoteUrl };
|
|
574
|
+
} catch (err) {
|
|
575
|
+
const msg = err instanceof Error ? err.message : 'Failed to create repository';
|
|
576
|
+
// Common errors
|
|
577
|
+
if (msg.includes('already exists')) {
|
|
578
|
+
return { ok: false, message: 'A repository with this name already exists on GitHub' };
|
|
579
|
+
}
|
|
580
|
+
return { ok: false, message: msg };
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Manually add a git remote
|
|
586
|
+
*/
|
|
587
|
+
export function gitAddRemote(root: string, url: string, repo: RepoType = 'akb'): { ok: boolean; message: string } {
|
|
588
|
+
const repoRoot = resolveRepoRoot(root, repo);
|
|
589
|
+
|
|
590
|
+
if (!isGitRoot(repoRoot)) {
|
|
591
|
+
return { ok: false, message: 'Not a git repository' };
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const existing = run('git remote get-url origin', repoRoot);
|
|
595
|
+
if (existing) {
|
|
596
|
+
return { ok: false, message: `Remote already configured: ${existing}` };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
try {
|
|
600
|
+
runOrThrow(`git remote add origin "${url}"`, repoRoot);
|
|
601
|
+
// Try initial push
|
|
602
|
+
const branch = run('git rev-parse --abbrev-ref HEAD', repoRoot) || 'main';
|
|
603
|
+
try {
|
|
604
|
+
execSync(`git push -u origin ${branch}`, {
|
|
605
|
+
cwd: repoRoot, encoding: 'utf-8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
606
|
+
});
|
|
607
|
+
return { ok: true, message: `Remote added and pushed to ${url}` };
|
|
608
|
+
} catch {
|
|
609
|
+
return { ok: true, message: `Remote added: ${url} (push failed — check credentials)` };
|
|
610
|
+
}
|
|
611
|
+
} catch (err) {
|
|
612
|
+
return { ok: false, message: err instanceof Error ? err.message : 'Failed to add remote' };
|
|
613
|
+
}
|
|
614
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backward-compatibility re-export from execution-manager.
|
|
3
|
+
* All consumers should migrate to importing from execution-manager.ts directly.
|
|
4
|
+
*/
|
|
5
|
+
export {
|
|
6
|
+
executionManager as jobManager,
|
|
7
|
+
executionManager,
|
|
8
|
+
type Execution as Job,
|
|
9
|
+
type Execution,
|
|
10
|
+
type StartExecutionParams as StartJobParams,
|
|
11
|
+
type StartExecutionParams,
|
|
12
|
+
type ExecStatus,
|
|
13
|
+
type ExecType,
|
|
14
|
+
canTransition,
|
|
15
|
+
messageStatusToRoleStatus,
|
|
16
|
+
} from './execution-manager.js';
|