tycono 0.1.42 → 0.1.43

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.
@@ -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
- /** Initialize a new git repository */
84
- export function gitInit(root: string): { ok: boolean; message: string } {
85
- if (isGitRepo(root)) {
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', root);
90
- runOrThrow('git add -A', root);
91
- runOrThrow('git commit -m "Initial commit by Tycono"', root);
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
- /** Get current git status. Returns noGit=true if not a git repo. */
99
- export function getGitStatus(root: string): GitStatus {
100
- if (!isGitRepo(root)) {
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', root);
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
- // Filter to save paths only — never include user source code
123
- if (!SAVE_PATHS.some(p => file.startsWith(p))) continue;
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', root);
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', root) || 'unknown';
139
- const hasRemote = !!run('git remote', root);
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', root);
144
- const remote = run(`git rev-parse origin/${branch}`, root);
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
- /** Commit + push save-tracked files */
161
- export function gitSave(root: string, message?: string): SaveResult {
162
- if (!isGitRepo(root)) {
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 only save-tracked files
257
+ // Stage files
174
258
  for (const file of allFiles) {
175
- runOrThrow(`git add "${file}"`, root);
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, '\\"')}"`, root);
266
+ runOrThrow(`git commit -m "${commitMsg.replace(/"/g, '\\"')}"`, repoRoot);
183
267
 
184
- const sha = run('git rev-parse HEAD', root);
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}`, root);
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
- /** Get commit history */
207
- export function gitHistory(root: string, limit = 20): CommitInfo[] {
208
- if (!isGitRepo(root)) return [];
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}`, root);
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
- /** Restore files from a previous commit (non-destructive: creates new commit) */
229
- export function gitRestore(root: string, sha: string, paths?: string[]): RestoreResult {
230
- if (!isGitRepo(root)) {
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
- const targetPaths = paths?.length ? paths : SAVE_PATHS;
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}"`, root);
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', root);
252
- runOrThrow(`git commit -m "${msg}"`, root);
352
+ runOrThrow('git add -A', repoRoot);
353
+ runOrThrow(`git commit -m "${msg}"`, repoRoot);
253
354
 
254
- const newSha = run('git rev-parse HEAD', root);
355
+ const newSha = run('git rev-parse HEAD', repoRoot);
255
356
 
256
357
  return { commitSha: newSha, restoredFiles };
257
358
  }