tycono 0.1.42 → 0.1.44
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/package.json +1 -1
- package/src/api/src/engine/agent-loop.ts +170 -65
- package/src/api/src/engine/context-assembler.ts +66 -10
- package/src/api/src/routes/knowledge.ts +3 -3
- package/src/api/src/services/git-save.ts +139 -38
- package/src/web/dist/assets/{index-BSryh72N.js → index-XDE-Ld_X.js} +28 -28
- package/src/web/dist/assets/{preview-app-Bl6BvDdj.js → preview-app-DtCHQ2CL.js} +1 -1
- package/src/web/dist/index.html +1 -1
|
@@ -8,8 +8,16 @@
|
|
|
8
8
|
* - SAVE_PATHS의 AKB 파일만 stage (유저 소스코드 미포함)
|
|
9
9
|
* - git 없으면 graceful 비활성화
|
|
10
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 필터 비활성화
|
|
11
15
|
*/
|
|
12
16
|
import { execSync } from 'node:child_process';
|
|
17
|
+
import { readFileSync } from 'node:fs';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
|
|
20
|
+
export type RepoType = 'akb' | 'code';
|
|
13
21
|
|
|
14
22
|
export interface GitStatus {
|
|
15
23
|
dirty: boolean;
|
|
@@ -63,6 +71,58 @@ const SAVE_PATHS = [
|
|
|
63
71
|
'CLAUDE.md',
|
|
64
72
|
];
|
|
65
73
|
|
|
74
|
+
interface TyconoConfig {
|
|
75
|
+
companyName: string;
|
|
76
|
+
engine: string;
|
|
77
|
+
createdAt: string;
|
|
78
|
+
codeRoot?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Read codeRoot from .tycono/config.json
|
|
83
|
+
* @param akbRoot - AKB repository root (COMPANY_ROOT)
|
|
84
|
+
* @returns codeRoot path if configured, undefined otherwise
|
|
85
|
+
*/
|
|
86
|
+
function getCodeRoot(akbRoot: string): string | undefined {
|
|
87
|
+
try {
|
|
88
|
+
const configPath = join(akbRoot, '.tycono', 'config.json');
|
|
89
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
90
|
+
const config: TyconoConfig = JSON.parse(content);
|
|
91
|
+
return config.codeRoot;
|
|
92
|
+
} catch {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Resolve repository root based on repo type
|
|
99
|
+
* @param akbRoot - AKB repository root (COMPANY_ROOT)
|
|
100
|
+
* @param repo - Repository type ('akb' or 'code')
|
|
101
|
+
* @returns Resolved repository root path
|
|
102
|
+
* @throws Error if repo='code' but codeRoot not configured
|
|
103
|
+
*/
|
|
104
|
+
function resolveRepoRoot(akbRoot: string, repo: RepoType = 'akb'): string {
|
|
105
|
+
if (repo === 'akb') {
|
|
106
|
+
return akbRoot;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const codeRoot = getCodeRoot(akbRoot);
|
|
110
|
+
if (!codeRoot) {
|
|
111
|
+
throw new Error('codeRoot not configured in .tycono/config.json');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return codeRoot;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Check if SAVE_PATHS filter should be applied
|
|
119
|
+
* @param repo - Repository type
|
|
120
|
+
* @returns true if filter should be applied (AKB only)
|
|
121
|
+
*/
|
|
122
|
+
function shouldFilterPaths(repo: RepoType = 'akb'): boolean {
|
|
123
|
+
return repo === 'akb';
|
|
124
|
+
}
|
|
125
|
+
|
|
66
126
|
function run(cmd: string, cwd: string): string {
|
|
67
127
|
try {
|
|
68
128
|
return execSync(cmd, { cwd, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
@@ -80,24 +140,36 @@ function isGitRepo(root: string): boolean {
|
|
|
80
140
|
return run('git rev-parse --is-inside-work-tree', root) === 'true';
|
|
81
141
|
}
|
|
82
142
|
|
|
83
|
-
/**
|
|
84
|
-
|
|
85
|
-
|
|
143
|
+
/**
|
|
144
|
+
* Initialize a new git repository
|
|
145
|
+
* @param root - AKB repository root (COMPANY_ROOT)
|
|
146
|
+
* @param repo - Repository type ('akb' or 'code'), default 'akb'
|
|
147
|
+
*/
|
|
148
|
+
export function gitInit(root: string, repo: RepoType = 'akb'): { ok: boolean; message: string } {
|
|
149
|
+
const repoRoot = resolveRepoRoot(root, repo);
|
|
150
|
+
|
|
151
|
+
if (isGitRepo(repoRoot)) {
|
|
86
152
|
return { ok: true, message: 'Already a git repository' };
|
|
87
153
|
}
|
|
88
154
|
try {
|
|
89
|
-
runOrThrow('git init',
|
|
90
|
-
runOrThrow('git add -A',
|
|
91
|
-
runOrThrow('git commit -m "Initial commit by Tycono"',
|
|
155
|
+
runOrThrow('git init', repoRoot);
|
|
156
|
+
runOrThrow('git add -A', repoRoot);
|
|
157
|
+
runOrThrow('git commit -m "Initial commit by Tycono"', repoRoot);
|
|
92
158
|
return { ok: true, message: 'Git repository initialized with initial commit' };
|
|
93
159
|
} catch (err) {
|
|
94
160
|
return { ok: false, message: err instanceof Error ? err.message : 'git init failed' };
|
|
95
161
|
}
|
|
96
162
|
}
|
|
97
163
|
|
|
98
|
-
/**
|
|
99
|
-
|
|
100
|
-
|
|
164
|
+
/**
|
|
165
|
+
* Get current git status. Returns noGit=true if not a git repo.
|
|
166
|
+
* @param root - AKB repository root (COMPANY_ROOT)
|
|
167
|
+
* @param repo - Repository type ('akb' or 'code'), default 'akb'
|
|
168
|
+
*/
|
|
169
|
+
export function getGitStatus(root: string, repo: RepoType = 'akb'): GitStatus {
|
|
170
|
+
const repoRoot = resolveRepoRoot(root, repo);
|
|
171
|
+
|
|
172
|
+
if (!isGitRepo(repoRoot)) {
|
|
101
173
|
return {
|
|
102
174
|
dirty: false,
|
|
103
175
|
modified: [],
|
|
@@ -110,17 +182,22 @@ export function getGitStatus(root: string): GitStatus {
|
|
|
110
182
|
};
|
|
111
183
|
}
|
|
112
184
|
|
|
113
|
-
const porcelain = run('git status --porcelain',
|
|
185
|
+
const porcelain = run('git status --porcelain', repoRoot);
|
|
114
186
|
const lines = porcelain ? porcelain.split('\n').filter(Boolean) : [];
|
|
115
187
|
|
|
116
188
|
const modified: string[] = [];
|
|
117
189
|
const untracked: string[] = [];
|
|
190
|
+
const applyFilter = shouldFilterPaths(repo);
|
|
118
191
|
|
|
119
192
|
for (const line of lines) {
|
|
120
193
|
const status = line.substring(0, 2);
|
|
121
194
|
const file = line.substring(3);
|
|
122
|
-
|
|
123
|
-
|
|
195
|
+
|
|
196
|
+
// For CODE repo, include all files; for AKB, filter to SAVE_PATHS only
|
|
197
|
+
if (applyFilter && !SAVE_PATHS.some(p => file.startsWith(p))) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
124
201
|
if (status.includes('?')) {
|
|
125
202
|
untracked.push(file);
|
|
126
203
|
} else {
|
|
@@ -129,19 +206,19 @@ export function getGitStatus(root: string): GitStatus {
|
|
|
129
206
|
}
|
|
130
207
|
|
|
131
208
|
let lastCommit: GitStatus['lastCommit'] = null;
|
|
132
|
-
const logLine = run('git log -1 --format=%H%n%s%n%aI',
|
|
209
|
+
const logLine = run('git log -1 --format=%H%n%s%n%aI', repoRoot);
|
|
133
210
|
if (logLine) {
|
|
134
211
|
const [sha, message, date] = logLine.split('\n');
|
|
135
212
|
if (sha) lastCommit = { sha, message: message ?? '', date: date ?? '' };
|
|
136
213
|
}
|
|
137
214
|
|
|
138
|
-
const branch = run('git rev-parse --abbrev-ref HEAD',
|
|
139
|
-
const hasRemote = !!run('git remote',
|
|
215
|
+
const branch = run('git rev-parse --abbrev-ref HEAD', repoRoot) || 'unknown';
|
|
216
|
+
const hasRemote = !!run('git remote', repoRoot);
|
|
140
217
|
|
|
141
218
|
let synced = true;
|
|
142
219
|
if (hasRemote) {
|
|
143
|
-
const local = run('git rev-parse HEAD',
|
|
144
|
-
const remote = run(`git rev-parse origin/${branch}`,
|
|
220
|
+
const local = run('git rev-parse HEAD', repoRoot);
|
|
221
|
+
const remote = run(`git rev-parse origin/${branch}`, repoRoot);
|
|
145
222
|
synced = !!local && local === remote;
|
|
146
223
|
}
|
|
147
224
|
|
|
@@ -157,37 +234,44 @@ export function getGitStatus(root: string): GitStatus {
|
|
|
157
234
|
};
|
|
158
235
|
}
|
|
159
236
|
|
|
160
|
-
/**
|
|
161
|
-
|
|
162
|
-
|
|
237
|
+
/**
|
|
238
|
+
* Commit + push save-tracked files
|
|
239
|
+
* @param root - AKB repository root (COMPANY_ROOT)
|
|
240
|
+
* @param message - Optional commit message
|
|
241
|
+
* @param repo - Repository type ('akb' or 'code'), default 'akb'
|
|
242
|
+
*/
|
|
243
|
+
export function gitSave(root: string, message?: string, repo: RepoType = 'akb'): SaveResult {
|
|
244
|
+
const repoRoot = resolveRepoRoot(root, repo);
|
|
245
|
+
|
|
246
|
+
if (!isGitRepo(repoRoot)) {
|
|
163
247
|
throw new Error('Not a git repository. Run "git init" first.');
|
|
164
248
|
}
|
|
165
249
|
|
|
166
|
-
const status = getGitStatus(root);
|
|
250
|
+
const status = getGitStatus(root, repo);
|
|
167
251
|
if (!status.dirty) {
|
|
168
252
|
throw new Error('No changes to save');
|
|
169
253
|
}
|
|
170
254
|
|
|
171
255
|
const allFiles = [...status.modified, ...status.untracked];
|
|
172
256
|
|
|
173
|
-
// Stage
|
|
257
|
+
// Stage files
|
|
174
258
|
for (const file of allFiles) {
|
|
175
|
-
runOrThrow(`git add "${file}"`,
|
|
259
|
+
runOrThrow(`git add "${file}"`, repoRoot);
|
|
176
260
|
}
|
|
177
261
|
|
|
178
262
|
const prefix = '[tycono] ';
|
|
179
263
|
const commitMsg = message
|
|
180
264
|
? `${prefix}${message}`
|
|
181
265
|
: `${prefix}Save — ${new Date().toISOString().slice(0, 16)} (${allFiles.length} files)`;
|
|
182
|
-
runOrThrow(`git commit -m "${commitMsg.replace(/"/g, '\\"')}"`,
|
|
266
|
+
runOrThrow(`git commit -m "${commitMsg.replace(/"/g, '\\"')}"`, repoRoot);
|
|
183
267
|
|
|
184
|
-
const sha = run('git rev-parse HEAD',
|
|
268
|
+
const sha = run('git rev-parse HEAD', repoRoot);
|
|
185
269
|
|
|
186
270
|
let pushed = false;
|
|
187
271
|
let pushError: string | undefined;
|
|
188
272
|
if (status.hasRemote) {
|
|
189
273
|
try {
|
|
190
|
-
runOrThrow(`git push origin ${status.branch}`,
|
|
274
|
+
runOrThrow(`git push origin ${status.branch}`, repoRoot);
|
|
191
275
|
pushed = true;
|
|
192
276
|
} catch (err) {
|
|
193
277
|
pushError = err instanceof Error ? err.message : 'Push failed';
|
|
@@ -203,11 +287,18 @@ export function gitSave(root: string, message?: string): SaveResult {
|
|
|
203
287
|
};
|
|
204
288
|
}
|
|
205
289
|
|
|
206
|
-
/**
|
|
207
|
-
|
|
208
|
-
|
|
290
|
+
/**
|
|
291
|
+
* Get commit history
|
|
292
|
+
* @param root - AKB repository root (COMPANY_ROOT)
|
|
293
|
+
* @param limit - Maximum number of commits to retrieve
|
|
294
|
+
* @param repo - Repository type ('akb' or 'code'), default 'akb'
|
|
295
|
+
*/
|
|
296
|
+
export function gitHistory(root: string, limit = 20, repo: RepoType = 'akb'): CommitInfo[] {
|
|
297
|
+
const repoRoot = resolveRepoRoot(root, repo);
|
|
298
|
+
|
|
299
|
+
if (!isGitRepo(repoRoot)) return [];
|
|
209
300
|
|
|
210
|
-
const log = run(`git log --format=%H%n%h%n%s%n%aI -n ${limit}`,
|
|
301
|
+
const log = run(`git log --format=%H%n%h%n%s%n%aI -n ${limit}`, repoRoot);
|
|
211
302
|
if (!log) return [];
|
|
212
303
|
|
|
213
304
|
const lines = log.split('\n');
|
|
@@ -225,18 +316,28 @@ export function gitHistory(root: string, limit = 20): CommitInfo[] {
|
|
|
225
316
|
return commits;
|
|
226
317
|
}
|
|
227
318
|
|
|
228
|
-
/**
|
|
229
|
-
|
|
230
|
-
|
|
319
|
+
/**
|
|
320
|
+
* Restore files from a previous commit (non-destructive: creates new commit)
|
|
321
|
+
* @param root - AKB repository root (COMPANY_ROOT)
|
|
322
|
+
* @param sha - Commit SHA to restore from
|
|
323
|
+
* @param paths - Optional paths to restore (defaults to SAVE_PATHS for AKB, all for CODE)
|
|
324
|
+
* @param repo - Repository type ('akb' or 'code'), default 'akb'
|
|
325
|
+
*/
|
|
326
|
+
export function gitRestore(root: string, sha: string, paths?: string[], repo: RepoType = 'akb'): RestoreResult {
|
|
327
|
+
const repoRoot = resolveRepoRoot(root, repo);
|
|
328
|
+
|
|
329
|
+
if (!isGitRepo(repoRoot)) {
|
|
231
330
|
throw new Error('Not a git repository');
|
|
232
331
|
}
|
|
233
332
|
|
|
234
|
-
|
|
333
|
+
// For CODE repo without explicit paths, restore everything (use '.')
|
|
334
|
+
// For AKB repo, use SAVE_PATHS
|
|
335
|
+
const targetPaths = paths?.length ? paths : (repo === 'code' ? ['.'] : SAVE_PATHS);
|
|
235
336
|
const restoredFiles: string[] = [];
|
|
236
337
|
|
|
237
338
|
for (const p of targetPaths) {
|
|
238
339
|
try {
|
|
239
|
-
runOrThrow(`git checkout ${sha} -- "${p}"`,
|
|
340
|
+
runOrThrow(`git checkout ${sha} -- "${p}"`, repoRoot);
|
|
240
341
|
restoredFiles.push(p);
|
|
241
342
|
} catch {
|
|
242
343
|
// Path may not exist in that commit — skip
|
|
@@ -248,10 +349,10 @@ export function gitRestore(root: string, sha: string, paths?: string[]): Restore
|
|
|
248
349
|
}
|
|
249
350
|
|
|
250
351
|
const msg = `[tycono] Restore from ${sha.slice(0, 7)} (${restoredFiles.length} paths)`;
|
|
251
|
-
runOrThrow('git add -A',
|
|
252
|
-
runOrThrow(`git commit -m "${msg}"`,
|
|
352
|
+
runOrThrow('git add -A', repoRoot);
|
|
353
|
+
runOrThrow(`git commit -m "${msg}"`, repoRoot);
|
|
253
354
|
|
|
254
|
-
const newSha = run('git rev-parse HEAD',
|
|
355
|
+
const newSha = run('git rev-parse HEAD', repoRoot);
|
|
255
356
|
|
|
256
357
|
return { commitSha: newSha, restoredFiles };
|
|
257
358
|
}
|