idea-manager 0.8.4 → 0.9.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/package.json +1 -1
- package/src/cli.ts +34 -0
- package/src/lib/sync/exporter.ts +37 -0
- package/src/lib/sync/git.ts +113 -0
- package/src/lib/sync/importer.ts +69 -0
- package/src/lib/sync/index.ts +211 -0
- package/src/lib/utils/paths.ts +9 -0
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -96,4 +96,38 @@ program
|
|
|
96
96
|
process.on('SIGTERM', () => { child.kill(); process.exit(0); });
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
+
const syncCmd = program
|
|
100
|
+
.command('sync')
|
|
101
|
+
.description('Sync data via GitHub repository')
|
|
102
|
+
.action(async () => {
|
|
103
|
+
const { syncStatus } = await import('./lib/sync');
|
|
104
|
+
await syncStatus();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
syncCmd
|
|
108
|
+
.command('init')
|
|
109
|
+
.description('Initialize sync with a GitHub repository')
|
|
110
|
+
.action(async () => {
|
|
111
|
+
const { syncInit } = await import('./lib/sync');
|
|
112
|
+
await syncInit();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
syncCmd
|
|
116
|
+
.command('push')
|
|
117
|
+
.description('Export data and push to GitHub')
|
|
118
|
+
.option('-m, --message <msg>', 'Custom commit message')
|
|
119
|
+
.action(async (opts) => {
|
|
120
|
+
const { syncPush } = await import('./lib/sync');
|
|
121
|
+
await syncPush(opts.message);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
syncCmd
|
|
125
|
+
.command('pull')
|
|
126
|
+
.description('Pull from GitHub and import data')
|
|
127
|
+
.option('--no-backup', 'Skip database backup before import')
|
|
128
|
+
.action(async (opts) => {
|
|
129
|
+
const { syncPull } = await import('./lib/sync');
|
|
130
|
+
await syncPull({ backup: opts.backup });
|
|
131
|
+
});
|
|
132
|
+
|
|
99
133
|
program.parse();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { getDb } from '../db';
|
|
3
|
+
|
|
4
|
+
// v2 active tables
|
|
5
|
+
const V2_TABLES = ['projects', 'brainstorms', 'sub_projects', 'tasks', 'task_prompts', 'task_conversations'];
|
|
6
|
+
|
|
7
|
+
function getExistingTables(db: ReturnType<typeof getDb>): string[] {
|
|
8
|
+
const rows = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").all() as { name: string }[];
|
|
9
|
+
return rows.map(r => r.name);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function exportToFile(filePath: string): { tables: Record<string, number> } {
|
|
13
|
+
const db = getDb();
|
|
14
|
+
const existingTables = getExistingTables(db);
|
|
15
|
+
|
|
16
|
+
const tables: Record<string, unknown[]> = {};
|
|
17
|
+
const counts: Record<string, number> = {};
|
|
18
|
+
|
|
19
|
+
// Export all known tables using raw SELECT for perfect round-trip
|
|
20
|
+
for (const table of V2_TABLES) {
|
|
21
|
+
if (existingTables.includes(table)) {
|
|
22
|
+
const rows = db.prepare(`SELECT * FROM ${table}`).all();
|
|
23
|
+
tables[table] = rows;
|
|
24
|
+
counts[table] = rows.length;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const data = {
|
|
29
|
+
version: 2,
|
|
30
|
+
exportedAt: new Date().toISOString(),
|
|
31
|
+
tables,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
35
|
+
|
|
36
|
+
return { tables: counts };
|
|
37
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { execFile } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
function quoteArg(arg: string): string {
|
|
6
|
+
// Quote args with spaces for shell mode
|
|
7
|
+
if (arg.includes(' ') || arg.includes('"')) {
|
|
8
|
+
return `"${arg.replace(/"/g, '\\"')}"`;
|
|
9
|
+
}
|
|
10
|
+
return arg;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function exec(cmd: string, args: string[], cwd?: string): Promise<string> {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const quotedArgs = args.map(quoteArg);
|
|
16
|
+
execFile(cmd, quotedArgs, { cwd, shell: true, timeout: 60000 }, (err, stdout, stderr) => {
|
|
17
|
+
if (err) {
|
|
18
|
+
reject(new Error(stderr?.trim() || err.message));
|
|
19
|
+
} else {
|
|
20
|
+
resolve(stdout.trim());
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function isGitInstalled(): Promise<boolean> {
|
|
27
|
+
try { await exec('git', ['--version']); return true; } catch { return false; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function isGhInstalled(): Promise<boolean> {
|
|
31
|
+
try { await exec('gh', ['--version']); return true; } catch { return false; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function isGhAuthenticated(): Promise<boolean> {
|
|
35
|
+
try { await exec('gh', ['auth', 'status']); return true; } catch { return false; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isGitRepo(dir: string): boolean {
|
|
39
|
+
return fs.existsSync(path.join(dir, '.git'));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function ghCreateRepo(name: string): Promise<string> {
|
|
43
|
+
// Create private repo and get URL
|
|
44
|
+
const result = await exec('gh', ['repo', 'create', name, '--private', '--confirm']);
|
|
45
|
+
// Extract repo URL from output
|
|
46
|
+
const urlMatch = result.match(/https:\/\/github\.com\/\S+/);
|
|
47
|
+
if (urlMatch) return urlMatch[0];
|
|
48
|
+
// Fallback: query the repo URL
|
|
49
|
+
const url = await exec('gh', ['repo', 'view', name, '--json', 'url', '-q', '.url']);
|
|
50
|
+
return url;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function gitClone(url: string, targetDir: string): Promise<void> {
|
|
54
|
+
await exec('git', ['clone', url, targetDir]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function gitInit(cwd: string): Promise<void> {
|
|
58
|
+
await exec('git', ['init'], cwd);
|
|
59
|
+
// Set default branch to main
|
|
60
|
+
await exec('git', ['branch', '-M', 'main'], cwd);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function gitAddRemote(cwd: string, url: string): Promise<void> {
|
|
64
|
+
await exec('git', ['remote', 'add', 'origin', url], cwd);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function gitAdd(cwd: string, files: string[]): Promise<void> {
|
|
68
|
+
await exec('git', ['add', ...files], cwd);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function gitCommit(cwd: string, message: string): Promise<void> {
|
|
72
|
+
await exec('git', ['commit', '-m', message], cwd);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function gitPush(cwd: string, setUpstream = false): Promise<void> {
|
|
76
|
+
if (setUpstream) {
|
|
77
|
+
// Ensure branch is named main, then push
|
|
78
|
+
try { await exec('git', ['branch', '-M', 'main'], cwd); } catch { /* already main */ }
|
|
79
|
+
await exec('git', ['push', '-u', 'origin', 'main'], cwd);
|
|
80
|
+
} else {
|
|
81
|
+
await exec('git', ['push'], cwd);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function gitPull(cwd: string): Promise<void> {
|
|
86
|
+
await exec('git', ['pull'], cwd);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function hasRemote(cwd: string): Promise<boolean> {
|
|
90
|
+
try {
|
|
91
|
+
const result = await exec('git', ['remote'], cwd);
|
|
92
|
+
return result.includes('origin');
|
|
93
|
+
} catch { return false; }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function hasCommits(cwd: string): Promise<boolean> {
|
|
97
|
+
try {
|
|
98
|
+
await exec('git', ['rev-parse', 'HEAD'], cwd);
|
|
99
|
+
return true;
|
|
100
|
+
} catch { return false; }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function getRemoteUrl(cwd: string): Promise<string | null> {
|
|
104
|
+
try {
|
|
105
|
+
return await exec('git', ['remote', 'get-url', 'origin'], cwd);
|
|
106
|
+
} catch { return null; }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function getLastCommitInfo(cwd: string): Promise<string | null> {
|
|
110
|
+
try {
|
|
111
|
+
return await exec('git', ['log', '-1', '--format=%ci %s'], cwd);
|
|
112
|
+
} catch { return null; }
|
|
113
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { getDb } from '../db';
|
|
3
|
+
import { getDbPath } from '../utils/paths';
|
|
4
|
+
|
|
5
|
+
// Import order: parents first, then children
|
|
6
|
+
const IMPORT_ORDER = ['projects', 'brainstorms', 'sub_projects', 'tasks', 'task_prompts', 'task_conversations'];
|
|
7
|
+
// Delete order: children first, then parents
|
|
8
|
+
const DELETE_ORDER = [...IMPORT_ORDER].reverse();
|
|
9
|
+
|
|
10
|
+
export function backupDb(): string {
|
|
11
|
+
const dbPath = getDbPath();
|
|
12
|
+
const db = getDb();
|
|
13
|
+
// Flush WAL before backup
|
|
14
|
+
db.pragma('wal_checkpoint(TRUNCATE)');
|
|
15
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
16
|
+
const backupPath = `${dbPath}.backup-${timestamp}`;
|
|
17
|
+
fs.copyFileSync(dbPath, backupPath);
|
|
18
|
+
return backupPath;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function importFromFile(filePath: string): { tables: Record<string, number> } {
|
|
22
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
23
|
+
const data = JSON.parse(raw);
|
|
24
|
+
|
|
25
|
+
if (!data.version || !data.tables) {
|
|
26
|
+
throw new Error('Invalid sync data format');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const db = getDb();
|
|
30
|
+
const counts: Record<string, number> = {};
|
|
31
|
+
|
|
32
|
+
const doImport = db.transaction(() => {
|
|
33
|
+
db.pragma('foreign_keys = OFF');
|
|
34
|
+
|
|
35
|
+
// Delete all data in reverse dependency order
|
|
36
|
+
for (const table of DELETE_ORDER) {
|
|
37
|
+
try {
|
|
38
|
+
db.prepare(`DELETE FROM ${table}`).run();
|
|
39
|
+
} catch {
|
|
40
|
+
// Table might not exist, skip
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Insert data in dependency order
|
|
45
|
+
for (const table of IMPORT_ORDER) {
|
|
46
|
+
const rows = data.tables[table];
|
|
47
|
+
if (!rows || !Array.isArray(rows) || rows.length === 0) {
|
|
48
|
+
counts[table] = 0;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const columns = Object.keys(rows[0]);
|
|
53
|
+
const placeholders = columns.map(() => '?').join(', ');
|
|
54
|
+
const stmt = db.prepare(`INSERT INTO ${table} (${columns.join(', ')}) VALUES (${placeholders})`);
|
|
55
|
+
|
|
56
|
+
for (const row of rows) {
|
|
57
|
+
stmt.run(...columns.map(col => row[col] ?? null));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
counts[table] = rows.length;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
db.pragma('foreign_keys = ON');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
doImport();
|
|
67
|
+
|
|
68
|
+
return { tables: counts };
|
|
69
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import { getSyncDir } from '../utils/paths';
|
|
5
|
+
import * as git from './git';
|
|
6
|
+
import { exportToFile } from './exporter';
|
|
7
|
+
import { importFromFile, backupDb } from './importer';
|
|
8
|
+
|
|
9
|
+
const SYNC_FILE = 'im-data.json';
|
|
10
|
+
|
|
11
|
+
function ask(question: string, defaultValue?: string): Promise<string> {
|
|
12
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
13
|
+
return new Promise(resolve => {
|
|
14
|
+
const prompt = defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `;
|
|
15
|
+
rl.question(prompt, answer => {
|
|
16
|
+
rl.close();
|
|
17
|
+
resolve(answer.trim() || defaultValue || '');
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function printCounts(label: string, counts: Record<string, number>) {
|
|
23
|
+
const parts = Object.entries(counts)
|
|
24
|
+
.filter(([, v]) => v > 0)
|
|
25
|
+
.map(([k, v]) => `${k}: ${v}`);
|
|
26
|
+
console.log(` ${label}: ${parts.join(', ') || 'empty'}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function syncInit() {
|
|
30
|
+
const syncDir = getSyncDir();
|
|
31
|
+
|
|
32
|
+
if (git.isGitRepo(syncDir)) {
|
|
33
|
+
const remoteUrl = await git.getRemoteUrl(syncDir);
|
|
34
|
+
console.log(`\n Sync already initialized.`);
|
|
35
|
+
if (remoteUrl) console.log(` Remote: ${remoteUrl}`);
|
|
36
|
+
console.log(` Use "im sync push" or "im sync pull"\n`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check git
|
|
41
|
+
if (!await git.isGitInstalled()) {
|
|
42
|
+
console.error('\n Error: git is not installed. Please install git first.\n');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let repoUrl = '';
|
|
47
|
+
|
|
48
|
+
// Try gh CLI
|
|
49
|
+
const ghAvailable = await git.isGhInstalled() && await git.isGhAuthenticated();
|
|
50
|
+
|
|
51
|
+
if (ghAvailable) {
|
|
52
|
+
const useGh = await ask('Create a new private GitHub repo? (Y/n)', 'Y');
|
|
53
|
+
|
|
54
|
+
if (useGh.toLowerCase() !== 'n') {
|
|
55
|
+
const repoName = await ask('Repository name', 'idea-manager-sync');
|
|
56
|
+
|
|
57
|
+
console.log(`\n Creating GitHub repo "${repoName}"...`);
|
|
58
|
+
try {
|
|
59
|
+
repoUrl = await git.ghCreateRepo(repoName);
|
|
60
|
+
console.log(` Created: ${repoUrl}`);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.error(` Failed to create repo: ${(err as Error).message}`);
|
|
63
|
+
console.log(' Falling back to manual URL input.\n');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!repoUrl) {
|
|
69
|
+
repoUrl = await ask('Enter git repository URL');
|
|
70
|
+
if (!repoUrl) {
|
|
71
|
+
console.error('\n Error: Repository URL is required.\n');
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Clean sync dir and clone
|
|
77
|
+
console.log(`\n Cloning to ${syncDir}...`);
|
|
78
|
+
try {
|
|
79
|
+
// Remove sync dir contents (keep the dir itself)
|
|
80
|
+
const entries = fs.readdirSync(syncDir);
|
|
81
|
+
for (const e of entries) {
|
|
82
|
+
fs.rmSync(path.join(syncDir, e), { recursive: true, force: true });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await git.gitClone(repoUrl, syncDir);
|
|
86
|
+
} catch {
|
|
87
|
+
// Clone into existing empty dir — init + add remote instead
|
|
88
|
+
try {
|
|
89
|
+
await git.gitInit(syncDir);
|
|
90
|
+
await git.gitAddRemote(syncDir, repoUrl);
|
|
91
|
+
} catch (err2) {
|
|
92
|
+
console.error(` Failed: ${(err2 as Error).message}\n`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Create .gitignore
|
|
98
|
+
fs.writeFileSync(path.join(syncDir, '.gitignore'), '.DS_Store\n', 'utf-8');
|
|
99
|
+
|
|
100
|
+
// Initial export + push if repo is empty
|
|
101
|
+
if (!fs.existsSync(path.join(syncDir, SYNC_FILE))) {
|
|
102
|
+
console.log(' Performing initial export...');
|
|
103
|
+
const { tables } = exportToFile(path.join(syncDir, SYNC_FILE));
|
|
104
|
+
printCounts('Exported', tables);
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
await git.gitAdd(syncDir, ['.gitignore', SYNC_FILE]);
|
|
108
|
+
await git.gitCommit(syncDir, 'sync: initial export');
|
|
109
|
+
await git.gitPush(syncDir, true);
|
|
110
|
+
console.log(' Pushed initial data.\n');
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.log(` Committed locally. Push manually if needed: ${(err as Error).message}\n`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log(` Sync initialized successfully!`);
|
|
117
|
+
console.log(` Remote: ${repoUrl}\n`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function syncPush(message?: string) {
|
|
121
|
+
const syncDir = getSyncDir();
|
|
122
|
+
|
|
123
|
+
if (!git.isGitRepo(syncDir)) {
|
|
124
|
+
console.error('\n Sync not initialized. Run "im sync init" first.\n');
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Export
|
|
129
|
+
const filePath = path.join(syncDir, SYNC_FILE);
|
|
130
|
+
const { tables } = exportToFile(filePath);
|
|
131
|
+
printCounts('Exported', tables);
|
|
132
|
+
|
|
133
|
+
// Commit + push
|
|
134
|
+
try {
|
|
135
|
+
await git.gitAdd(syncDir, [SYNC_FILE]);
|
|
136
|
+
const commitMsg = message || `sync: ${new Date().toISOString().slice(0, 19).replace('T', ' ')}`;
|
|
137
|
+
await git.gitCommit(syncDir, commitMsg);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
const msg = (err as Error).message;
|
|
140
|
+
if (msg.includes('nothing to commit') || msg.includes('no changes')) {
|
|
141
|
+
console.log(' Already up to date.\n');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
throw err;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const firstPush = !await git.hasCommits(syncDir).catch(() => true);
|
|
149
|
+
await git.gitPush(syncDir, firstPush);
|
|
150
|
+
console.log(' Pushed successfully.\n');
|
|
151
|
+
} catch (err) {
|
|
152
|
+
console.error(` Push failed: ${(err as Error).message}`);
|
|
153
|
+
console.log(' Data is committed locally. Try pushing manually.\n');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function syncPull(opts: { backup?: boolean } = {}) {
|
|
158
|
+
const syncDir = getSyncDir();
|
|
159
|
+
|
|
160
|
+
if (!git.isGitRepo(syncDir)) {
|
|
161
|
+
console.error('\n Sync not initialized. Run "im sync init" first.\n');
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Pull
|
|
166
|
+
try {
|
|
167
|
+
await git.gitPull(syncDir);
|
|
168
|
+
console.log(' Pulled latest data.');
|
|
169
|
+
} catch (err) {
|
|
170
|
+
console.error(` Pull failed: ${(err as Error).message}\n`);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const filePath = path.join(syncDir, SYNC_FILE);
|
|
175
|
+
if (!fs.existsSync(filePath)) {
|
|
176
|
+
console.log(' No sync data found. Run "im sync push" on another machine first.\n');
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Backup
|
|
181
|
+
if (opts.backup !== false) {
|
|
182
|
+
const backupPath = backupDb();
|
|
183
|
+
console.log(` Backup: ${backupPath}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Import
|
|
187
|
+
const { tables } = importFromFile(filePath);
|
|
188
|
+
printCounts('Imported', tables);
|
|
189
|
+
console.log(' Done. Refresh the browser to see updated data.\n');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function syncStatus() {
|
|
193
|
+
const syncDir = getSyncDir();
|
|
194
|
+
|
|
195
|
+
if (!git.isGitRepo(syncDir)) {
|
|
196
|
+
console.log('\n Sync not initialized.');
|
|
197
|
+
console.log(' Run "im sync init" to set up.\n');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const remoteUrl = await git.getRemoteUrl(syncDir);
|
|
202
|
+
const lastCommit = await git.getLastCommitInfo(syncDir);
|
|
203
|
+
const filePath = path.join(syncDir, SYNC_FILE);
|
|
204
|
+
const hasData = fs.existsSync(filePath);
|
|
205
|
+
|
|
206
|
+
console.log('\n IM Sync Status');
|
|
207
|
+
console.log(` Remote: ${remoteUrl || 'none'}`);
|
|
208
|
+
console.log(` Last sync: ${lastCommit || 'never'}`);
|
|
209
|
+
console.log(` Data file: ${hasData ? 'exists' : 'not found'}`);
|
|
210
|
+
console.log(`\n Commands: im sync push | im sync pull\n`);
|
|
211
|
+
}
|
package/src/lib/utils/paths.ts
CHANGED
|
@@ -14,3 +14,12 @@ export function getDataDir(): string {
|
|
|
14
14
|
export function getDbPath(): string {
|
|
15
15
|
return path.join(getDataDir(), 'im.db');
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
const SYNC_DIR = path.join(os.homedir(), '.idea-manager', 'sync');
|
|
19
|
+
|
|
20
|
+
export function getSyncDir(): string {
|
|
21
|
+
if (!fs.existsSync(SYNC_DIR)) {
|
|
22
|
+
fs.mkdirSync(SYNC_DIR, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
return SYNC_DIR;
|
|
25
|
+
}
|