Skip to content

Commit 175e85a

Browse files
committed
Add init command
1 parent 16cdf03 commit 175e85a

File tree

13 files changed

+280
-25
lines changed

13 files changed

+280
-25
lines changed

README.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ The era of AI-powered coding is here, but managing multiple AI assistants is a m
3131
npx @donnes/syncode new
3232
```
3333

34+
### Already Have a Repo?
35+
36+
```bash
37+
syncode init
38+
```
39+
3440
### Option 2: Global Install
3541

3642
```bash
@@ -46,7 +52,7 @@ syncode new
4652

4753
## Usage
4854

49-
### Initialize CLI
55+
### Initialize a New Repo
5056

5157
```bash
5258
syncode new
@@ -59,6 +65,18 @@ This will:
5965
- Import your existing configs
6066
- Set up smart sync defaults (symlinks for most, copy for Claude, Gemini, etc.)
6167

68+
### Initialize from an Existing Repo
69+
70+
```bash
71+
syncode init
72+
```
73+
74+
This will:
75+
- Prompt for the repo URL and local storage path
76+
- Clone the repo if needed
77+
- Let you choose which agents to sync
78+
- Save configuration to `~/.syncode/config.json`
79+
6280
### Sync Agents Config
6381

6482
```bash
@@ -212,6 +230,7 @@ syncode push
212230
## Commands
213231

214232
- `syncode new` - Initialize a new agent config repository
233+
- `syncode init` - Initialize from an existing agent config repository
215234
- `syncode sync` - Sync agent configs (import or export)
216235
- `syncode status` - Show status of synced agents
217236
- `syncode push` - Push config changes to git remote
@@ -273,6 +292,8 @@ bun run build
273292
```bash
274293
# Run initialization
275294
syncode new
295+
# Or connect to an existing repo
296+
syncode init
276297
```
277298

278299
### Symlinks not working
@@ -304,4 +325,3 @@ MIT © Donald Silveira
304325
---
305326

306327
**Made with ❤️ for developers**
307-

docs/agents/commands.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@
3333
- Imports existing configurations from system paths to the repository.
3434
- Persists global settings in `~/.syncode/config.json`.
3535

36+
### `syncode init`
37+
- Purpose: Initializes syncode from an existing configuration repository.
38+
- Actions:
39+
- Prompts for the repo URL and local repository path.
40+
- Clones the repo if it does not exist locally.
41+
- Prompts for which agents to sync (preselects agents found in the repo).
42+
- Persists global settings in `~/.syncode/config.json`.
43+
3644
### `syncode sync`
3745
- Purpose: Synchronizes agent configurations between the system and the repository.
3846
- Modes:

docs/agents/project-structure.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
- Entry point: `src/index.ts` (shebang: `#!/usr/bin/env node`)
44
- Build output: `dist/`
5-
- `src/commands/`: User-facing commands (new, sync, status, push).
5+
- `src/commands/`: User-facing commands (new, init, sync, status, push).
66
- `src/adapters/`: Agent adapters implementing `AgentAdapter`.
77
- `src/config/`: Configuration management (manager.ts, types.ts).
88
- `src/utils/`: Shared helpers (fs, git, paths, shell, platform).

src/commands/init.ts

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import { execSync } from "node:child_process";
2+
import { existsSync, mkdirSync } from "node:fs";
3+
import { dirname, join } from "node:path";
4+
import * as p from "@clack/prompts";
5+
import { adapterRegistry } from "../adapters/registry";
6+
import type { Platform } from "../adapters/types";
7+
import {
8+
detectInstalledAgents,
9+
getAgentMetadata,
10+
getAgentsWithAdapters,
11+
} from "../agents";
12+
import { configExists, initConfig } from "../config/manager";
13+
import { SUPPORTED_AGENTS } from "../config/types";
14+
import { contractHome, expandHome } from "../utils/paths";
15+
16+
export async function initCommand() {
17+
p.intro("Initialize from Existing Repo");
18+
19+
if (configExists()) {
20+
const overwrite = await p.confirm({
21+
message:
22+
"Configuration already exists at ~/.syncode/config.json. Overwrite?",
23+
initialValue: false,
24+
});
25+
26+
if (p.isCancel(overwrite) || !overwrite) {
27+
p.cancel("Initialization cancelled.");
28+
return;
29+
}
30+
}
31+
32+
const repoUrlInput = await p.text({
33+
message: "Repository URL",
34+
placeholder: "https://github.com/<username>/configs.git",
35+
validate: (value) => {
36+
if (!value) return "Repository URL is required";
37+
return undefined;
38+
},
39+
});
40+
41+
if (p.isCancel(repoUrlInput)) {
42+
p.cancel("Initialization cancelled.");
43+
return;
44+
}
45+
46+
const repoPathInput = await p.text({
47+
message: "Where should the agent configs be stored?",
48+
placeholder: "~/.syncode/repo",
49+
initialValue: "~/.syncode/repo",
50+
validate: (value) => {
51+
if (!value) return "Repository path is required";
52+
return undefined;
53+
},
54+
});
55+
56+
if (p.isCancel(repoPathInput)) {
57+
p.cancel("Initialization cancelled.");
58+
return;
59+
}
60+
61+
const repoPath = expandHome(repoPathInput);
62+
63+
if (existsSync(repoPath)) {
64+
const useExisting = await p.confirm({
65+
message: `Directory ${contractHome(repoPath)} already exists. Use it?`,
66+
initialValue: true,
67+
});
68+
69+
if (p.isCancel(useExisting) || !useExisting) {
70+
p.cancel("Initialization cancelled.");
71+
return;
72+
}
73+
74+
const gitDir = join(repoPath, ".git");
75+
if (!existsSync(gitDir)) {
76+
p.cancel("Directory is not a git repository.");
77+
return;
78+
}
79+
} else {
80+
try {
81+
mkdirSync(dirname(repoPath), { recursive: true });
82+
} catch (error) {
83+
p.cancel(`Failed to create directory: ${error}`);
84+
return;
85+
}
86+
87+
try {
88+
execSync(`git clone "${repoUrlInput}" "${repoPath}"`, {
89+
stdio: "pipe",
90+
});
91+
p.log.success("✓ Cloned repository");
92+
} catch (_error) {
93+
p.cancel("Failed to clone repository.");
94+
return;
95+
}
96+
}
97+
98+
const platform: Platform =
99+
process.platform === "darwin"
100+
? "macos"
101+
: process.platform === "win32"
102+
? "windows"
103+
: "linux";
104+
const detectedAgents = detectInstalledAgents(platform);
105+
const agentsWithAdapters = getAgentsWithAdapters();
106+
107+
const repoAgents = SUPPORTED_AGENTS.filter((id) => {
108+
const adapter = adapterRegistry.get(id);
109+
if (!adapter) return false;
110+
return existsSync(adapter.getRepoPath(repoPath));
111+
});
112+
113+
const agentOptions = SUPPORTED_AGENTS.map((id) => {
114+
const detected = detectedAgents.includes(id);
115+
const hasAdapter = agentsWithAdapters.includes(id);
116+
const metadata = getAgentMetadata(id);
117+
const label =
118+
metadata?.displayName || id.charAt(0).toUpperCase() + id.slice(1);
119+
const hint = detected
120+
? hasAdapter
121+
? "Installed • Full sync"
122+
: "Installed • Metadata only"
123+
: hasAdapter
124+
? "Not found • Full sync available"
125+
: "Not found";
126+
127+
return {
128+
value: id,
129+
label: detected ? `${label} (detected ✓)` : label,
130+
hint,
131+
};
132+
});
133+
134+
const agentsInput = await p.multiselect({
135+
message: "Which AI agents do you want to sync?",
136+
options: agentOptions,
137+
initialValues: repoAgents.length > 0 ? repoAgents : detectedAgents,
138+
required: false,
139+
});
140+
141+
if (p.isCancel(agentsInput)) {
142+
p.cancel("Initialization cancelled.");
143+
return;
144+
}
145+
146+
const selectedAgents = agentsInput as string[];
147+
148+
if (selectedAgents.length === 0) {
149+
p.log.warn(
150+
"No agents selected. You can add them later by editing ~/.syncode/config.json",
151+
);
152+
}
153+
154+
if (selectedAgents.length > 0) {
155+
const selectedWithAdapters = selectedAgents.filter((id) =>
156+
agentsWithAdapters.includes(id),
157+
);
158+
const selectedWithoutAdapters = selectedAgents.filter(
159+
(id) => !agentsWithAdapters.includes(id),
160+
);
161+
162+
if (selectedWithAdapters.length > 0) {
163+
p.log.info("Using smart sync defaults:");
164+
p.log.info(
165+
" • Symlinks: Cursor, OpenCode, Windsurf, VSCode (live sync)",
166+
);
167+
p.log.info(" • Copy: Claude Code (preserves cache/history)");
168+
}
169+
170+
if (selectedWithoutAdapters.length > 0) {
171+
const agentNames = selectedWithoutAdapters
172+
.map((id) => getAgentMetadata(id)?.displayName || id)
173+
.join(", ");
174+
p.log.warn(`Note: ${agentNames} - metadata only (no sync adapter yet)`);
175+
}
176+
}
177+
178+
try {
179+
initConfig({
180+
repoPath: repoPathInput,
181+
remote: repoUrlInput,
182+
agents: selectedAgents,
183+
});
184+
185+
const agentList =
186+
selectedAgents.length > 0
187+
? selectedAgents
188+
.map((id) => {
189+
const adapter = adapterRegistry.get(id);
190+
return adapter ? adapter.name : id;
191+
})
192+
.join(", ")
193+
: "none";
194+
195+
p.outro(
196+
`✓ Existing repository connected!
197+
198+
Repository: ${contractHome(repoPath)}
199+
Configuration: ~/.syncode/config.json
200+
Agents: ${agentList}
201+
202+
Next steps:
203+
• Run 'syncode sync' and select "Export"
204+
• Configs in ${contractHome(repoPath)}/configs will apply to this machine`,
205+
);
206+
} catch (error) {
207+
p.cancel(`Failed to create configuration: ${error}`);
208+
}
209+
}

src/commands/machine/deps.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ export async function machineDepsCommand() {
2222
const config = getConfig();
2323
repoPath = expandHome(config.repoPath);
2424
} catch (_error) {
25-
p.cancel("Configuration not found. Run 'syncode new' first.");
25+
p.cancel(
26+
"Configuration not found. Run 'syncode new' or 'syncode init' first.",
27+
);
2628
return;
2729
}
2830

src/commands/machine/status.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ export async function machineStatusCommand(options?: { skipIntro?: boolean }) {
2323
const config = getConfig();
2424
repoPath = expandHome(config.repoPath);
2525
} catch (_error) {
26-
p.cancel("Configuration not found. Run 'syncode new' first.");
26+
p.cancel(
27+
"Configuration not found. Run 'syncode new' or 'syncode init' first.",
28+
);
2729
return;
2830
}
2931

src/commands/new.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -221,12 +221,8 @@ export async function newCommand() {
221221
if (existsSync(template.target)) {
222222
continue;
223223
}
224-
try {
225-
writeFileSync(template.target, template.content, "utf-8");
226-
s.message(`Added ${template.label}`);
227-
} catch (_error) {
228-
s.message(`Warning: Failed to add ${template.label}`);
229-
}
224+
writeFileSync(template.target, template.content, "utf-8");
225+
s.message(`Added ${template.label}`);
230226
}
231227

232228
for (const agentId of selectedAgents) {
@@ -283,12 +279,13 @@ Managed by [syncode](https://github.com/donnes/syncode)
283279
# Install syncode
284280
npm install -g @donnes/syncode
285281
286-
# Clone this repo
287-
git clone ${remote || "<your-repo-url>"} ~/agent-configs
282+
# Initialize from existing repo
283+
syncode init
284+
# When prompted, enter repo URL: ${remote || "<your-repo-url>"}
288285
289286
# Sync configs (creates symlinks)
290-
cd ~/agent-configs
291287
syncode sync
288+
# Select "Export"
292289
\`\`\`
293290
294291
## Synced Agents

src/commands/push.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ export async function pushCommand() {
2424
try {
2525
config = getConfig();
2626
} catch (_error) {
27-
p.cancel("Configuration not found. Run 'syncode new' first.");
27+
p.cancel(
28+
"Configuration not found. Run 'syncode new' or 'syncode init' first.",
29+
);
2830
return;
2931
}
3032

src/commands/status.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ export async function statusCommand() {
1717
try {
1818
config = getConfig();
1919
} catch (_error) {
20-
p.cancel("Configuration not found. Run 'syncode new' first.");
20+
p.cancel(
21+
"Configuration not found. Run 'syncode new' or 'syncode init' first.",
22+
);
2123
return;
2224
}
2325

src/commands/sync.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,16 @@ export async function syncCommand() {
1616
try {
1717
config = getConfig();
1818
} catch (_error) {
19-
p.cancel("Configuration not found. Run 'syncode new' first.");
19+
p.cancel(
20+
"Configuration not found. Run 'syncode new' or 'syncode init' first.",
21+
);
2022
return;
2123
}
2224

2325
if (config.agents.length === 0) {
24-
p.cancel("No agents configured. Run 'syncode new' to set up agents.");
26+
p.cancel(
27+
"No agents configured. Run 'syncode new' or 'syncode init' to set up agents.",
28+
);
2529
return;
2630
}
2731

0 commit comments

Comments
 (0)