@ryanreh99/skills-sync 1.0.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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -0
  3. package/dist/assets/contracts/build/bundle.schema.json +76 -0
  4. package/dist/assets/contracts/inputs/config.schema.json +13 -0
  5. package/dist/assets/contracts/inputs/mcp-servers.schema.json +56 -0
  6. package/dist/assets/contracts/inputs/pack-manifest.schema.json +33 -0
  7. package/dist/assets/contracts/inputs/pack-sources.schema.json +47 -0
  8. package/dist/assets/contracts/inputs/profile.schema.json +21 -0
  9. package/dist/assets/contracts/inputs/upstreams.schema.json +45 -0
  10. package/dist/assets/contracts/runtime/targets.schema.json +120 -0
  11. package/dist/assets/contracts/state/upstreams-lock.schema.json +38 -0
  12. package/dist/assets/manifests/targets.linux.json +27 -0
  13. package/dist/assets/manifests/targets.macos.json +27 -0
  14. package/dist/assets/manifests/targets.windows.json +27 -0
  15. package/dist/assets/seed/config.json +3 -0
  16. package/dist/assets/seed/packs/personal/mcp/servers.json +20 -0
  17. package/dist/assets/seed/packs/personal/pack.json +7 -0
  18. package/dist/assets/seed/packs/personal/sources.json +31 -0
  19. package/dist/assets/seed/profiles/personal.json +4 -0
  20. package/dist/assets/seed/upstreams.json +23 -0
  21. package/dist/cli.js +532 -0
  22. package/dist/index.js +27 -0
  23. package/dist/lib/adapters/claude.js +49 -0
  24. package/dist/lib/adapters/codex.js +239 -0
  25. package/dist/lib/adapters/common.js +114 -0
  26. package/dist/lib/adapters/copilot.js +53 -0
  27. package/dist/lib/adapters/cursor.js +53 -0
  28. package/dist/lib/adapters/gemini.js +52 -0
  29. package/dist/lib/agents.js +888 -0
  30. package/dist/lib/bindings.js +510 -0
  31. package/dist/lib/build.js +190 -0
  32. package/dist/lib/bundle.js +165 -0
  33. package/dist/lib/config.js +324 -0
  34. package/dist/lib/core.js +447 -0
  35. package/dist/lib/detect.js +56 -0
  36. package/dist/lib/doctor.js +504 -0
  37. package/dist/lib/init.js +292 -0
  38. package/dist/lib/inventory.js +235 -0
  39. package/dist/lib/manage.js +463 -0
  40. package/dist/lib/mcp-config.js +264 -0
  41. package/dist/lib/profile-transfer.js +221 -0
  42. package/dist/lib/upstreams.js +782 -0
  43. package/docs/agent-storage-map.md +153 -0
  44. package/docs/architecture.md +117 -0
  45. package/docs/changelog.md +12 -0
  46. package/docs/commands.md +94 -0
  47. package/docs/contracts.md +112 -0
  48. package/docs/homebrew.md +46 -0
  49. package/docs/quickstart.md +14 -0
  50. package/docs/roadmap.md +5 -0
  51. package/docs/security.md +32 -0
  52. package/docs/user-guide.md +257 -0
  53. package/package.json +61 -0
@@ -0,0 +1,292 @@
1
+ import fs from "fs-extra";
2
+ import path from "node:path";
3
+ import {
4
+ ASSETS_ROOT,
5
+ LOCAL_OVERRIDES_ROOT,
6
+ SKILLS_SYNC_HOME,
7
+ logInfo,
8
+ logWarn,
9
+ writeJsonFile,
10
+ writeJsonFileIfMissing
11
+ } from "./core.js";
12
+
13
+ const DEFAULT_PROFILE_NAME = "personal";
14
+
15
+ function resolveProfileName(rawProfile) {
16
+ if (typeof rawProfile !== "string") {
17
+ return DEFAULT_PROFILE_NAME;
18
+ }
19
+ const normalized = rawProfile.trim();
20
+ return normalized.length > 0 ? normalized : DEFAULT_PROFILE_NAME;
21
+ }
22
+
23
+ function getScaffoldFiles(localRoot, profileName) {
24
+ const profilesDir = path.join(localRoot, "profiles");
25
+ const packRoot = path.join(localRoot, "packs", profileName);
26
+ const mcpDir = path.join(packRoot, "mcp");
27
+ const skillsDir = path.join(packRoot, "skills");
28
+ const manifestsDir = path.join(localRoot, "manifests");
29
+ const stateDir = path.join(localRoot, "state");
30
+
31
+ return [
32
+ {
33
+ path: path.join(localRoot, "README.md"),
34
+ value: [
35
+ "# workspace",
36
+ "",
37
+ "Machine-local config for skills-sync.",
38
+ "",
39
+ "Edit these files:",
40
+ "- `packs/<name>/sources.json`",
41
+ "- `packs/<name>/mcp/servers.json`",
42
+ "- optional `profiles/<name>.json`",
43
+ "",
44
+ "Workflow:",
45
+ "1. Run `skills-sync build --profile <name>`",
46
+ "2. Run `skills-sync apply --profile <name>`",
47
+ ""
48
+ ].join("\n"),
49
+ type: "text"
50
+ },
51
+ {
52
+ path: path.join(profilesDir, `${profileName}.json`),
53
+ value: {
54
+ name: profileName,
55
+ packPath: `workspace/packs/${profileName}`
56
+ },
57
+ type: "json"
58
+ },
59
+ {
60
+ path: path.join(packRoot, "pack.json"),
61
+ value: {
62
+ name: profileName,
63
+ version: "0.0.0",
64
+ description: "",
65
+ maintainer: "",
66
+ tags: []
67
+ },
68
+ type: "json"
69
+ },
70
+ {
71
+ path: path.join(mcpDir, "servers.json"),
72
+ value: {
73
+ servers: {}
74
+ },
75
+ type: "json"
76
+ },
77
+ {
78
+ path: path.join(packRoot, "sources.json"),
79
+ value: {
80
+ imports: []
81
+ },
82
+ type: "json"
83
+ },
84
+ {
85
+ path: path.join(skillsDir, ".keep"),
86
+ value: "",
87
+ type: "text"
88
+ },
89
+ {
90
+ path: path.join(manifestsDir, ".keep"),
91
+ value: "",
92
+ type: "text"
93
+ },
94
+ {
95
+ path: path.join(stateDir, ".keep"),
96
+ value: "",
97
+ type: "text"
98
+ }
99
+ ];
100
+ }
101
+
102
+ async function ensureWorkspaceScaffold(localRoot, profileName) {
103
+ const files = getScaffoldFiles(localRoot, profileName);
104
+ let created = 0;
105
+ let skipped = 0;
106
+ for (const file of files) {
107
+ let wrote = false;
108
+ if (file.type === "json") {
109
+ wrote = await writeJsonFileIfMissing(file.path, file.value);
110
+ } else if (!(await fs.pathExists(file.path))) {
111
+ await fs.ensureDir(path.dirname(file.path));
112
+ await fs.writeFile(file.path, file.value, "utf8");
113
+ wrote = true;
114
+ }
115
+ if (wrote) {
116
+ created += 1;
117
+ } else {
118
+ skipped += 1;
119
+ }
120
+ }
121
+ return { created, skipped };
122
+ }
123
+
124
+ async function previewWorkspaceScaffold(localRoot, profileName) {
125
+ const files = getScaffoldFiles(localRoot, profileName);
126
+ let created = 0;
127
+ let skipped = 0;
128
+ for (const file of files) {
129
+ if (await fs.pathExists(file.path)) {
130
+ skipped += 1;
131
+ } else {
132
+ created += 1;
133
+ }
134
+ }
135
+ return { created, skipped };
136
+ }
137
+
138
+ async function inspectDefaultProfileUpdate({ force }) {
139
+ const configPath = path.join(LOCAL_OVERRIDES_ROOT, "config.json");
140
+ let existing = {};
141
+ let currentDefault = null;
142
+ if (await fs.pathExists(configPath)) {
143
+ existing = await fs.readJson(configPath);
144
+ if (typeof existing.defaultProfile === "string" && existing.defaultProfile.trim().length > 0) {
145
+ currentDefault = existing.defaultProfile.trim();
146
+ }
147
+ }
148
+
149
+ if (!force && currentDefault) {
150
+ return { shouldUpdate: false, currentDefault, existing, configPath };
151
+ }
152
+
153
+ return { shouldUpdate: true, currentDefault, existing, configPath };
154
+ }
155
+
156
+ async function ensureDefaultProfile({ profileName, force }) {
157
+ const inspection = await inspectDefaultProfileUpdate({ profileName, force });
158
+ if (!inspection.shouldUpdate) {
159
+ return { updated: false, defaultProfile: inspection.currentDefault };
160
+ }
161
+
162
+ await writeJsonFile(inspection.configPath, { ...inspection.existing, defaultProfile: profileName });
163
+ return { updated: true, defaultProfile: profileName, previousDefault: inspection.currentDefault };
164
+ }
165
+
166
+ async function previewDefaultProfile({ profileName, force }) {
167
+ const inspection = await inspectDefaultProfileUpdate({ profileName, force });
168
+ if (!inspection.shouldUpdate) {
169
+ return { updated: false, defaultProfile: inspection.currentDefault };
170
+ }
171
+ return {
172
+ updated: true,
173
+ defaultProfile: profileName,
174
+ previousDefault: inspection.currentDefault
175
+ };
176
+ }
177
+
178
+ export async function cmdInit({ seed = false, dryRun = false, profile = null } = {}) {
179
+ const selectedProfile = resolveProfileName(profile);
180
+ const profileWasExplicit = typeof profile === "string" && profile.trim().length > 0;
181
+
182
+ if (seed) {
183
+ const seedRoot = path.join(ASSETS_ROOT, "seed");
184
+ const localRoot = LOCAL_OVERRIDES_ROOT;
185
+ const backupPath = path.join(SKILLS_SYNC_HOME, "workspace.backup");
186
+ if (!(await fs.pathExists(seedRoot))) {
187
+ throw new Error("Seed content was not found.");
188
+ }
189
+
190
+ if (dryRun) {
191
+ const hasWorkspace = await fs.pathExists(localRoot);
192
+ const existingEntries = hasWorkspace ? await fs.readdir(localRoot) : [];
193
+ const defaultProfileResult = await previewDefaultProfile({
194
+ profileName: selectedProfile,
195
+ force: profileWasExplicit
196
+ });
197
+
198
+ logInfo(`Dry-run init --seed for profile '${selectedProfile}' complete. No files were modified.`);
199
+ if (existingEntries.length > 0) {
200
+ logInfo("Existing workspace would be backed up.");
201
+ } else {
202
+ logInfo("Seed content would be copied to workspace.");
203
+ }
204
+ if (defaultProfileResult.updated) {
205
+ logInfo(`Would set default profile to '${defaultProfileResult.defaultProfile}'.`);
206
+ } else {
207
+ logInfo(`Default profile would remain '${defaultProfileResult.defaultProfile}'.`);
208
+ }
209
+ return {
210
+ dryRun: true,
211
+ seed: true,
212
+ profile: selectedProfile
213
+ };
214
+ }
215
+
216
+ if (await fs.pathExists(localRoot)) {
217
+ const entries = await fs.readdir(localRoot);
218
+ if (entries.length > 0) {
219
+ await fs.remove(backupPath);
220
+ await fs.move(localRoot, backupPath);
221
+ logWarn("Backed up existing workspace.");
222
+ } else {
223
+ await fs.remove(localRoot);
224
+ }
225
+ }
226
+ await fs.copy(seedRoot, localRoot, { overwrite: true, errorOnExist: false });
227
+ const scaffold = await ensureWorkspaceScaffold(localRoot, selectedProfile);
228
+ const defaultProfileResult = await ensureDefaultProfile({
229
+ profileName: selectedProfile,
230
+ force: profileWasExplicit
231
+ });
232
+ logInfo(`Initialized workspace from seed for profile '${selectedProfile}'.`);
233
+ if (scaffold.created > 0) {
234
+ logInfo(
235
+ `Created ${scaffold.created} additional workspace files for profile '${selectedProfile}', skipped ${scaffold.skipped}.`
236
+ );
237
+ }
238
+ if (defaultProfileResult.updated) {
239
+ logInfo(`Default profile set to '${defaultProfileResult.defaultProfile}'.`);
240
+ }
241
+ process.stdout.write("\n");
242
+ process.stdout.write("Next steps:\n");
243
+ process.stdout.write(` 1) Current profile: '${selectedProfile}'\n`);
244
+ process.stdout.write(" 2) Run: skills-sync build\n");
245
+ process.stdout.write(" 3) Run: skills-sync apply\n");
246
+ process.stdout.write(" 4) Run: skills-sync doctor\n");
247
+ return;
248
+ }
249
+
250
+ const localRoot = LOCAL_OVERRIDES_ROOT;
251
+ if (dryRun) {
252
+ const scaffold = await previewWorkspaceScaffold(localRoot, selectedProfile);
253
+ const defaultProfileResult = await previewDefaultProfile({
254
+ profileName: selectedProfile,
255
+ force: profileWasExplicit
256
+ });
257
+
258
+ logInfo(`Dry-run init for profile '${selectedProfile}' complete. No files were modified.`);
259
+ logInfo(`Would create ${scaffold.created} files, skip ${scaffold.skipped} existing files.`);
260
+ if (defaultProfileResult.updated) {
261
+ logInfo(`Would set default profile to '${defaultProfileResult.defaultProfile}'.`);
262
+ } else {
263
+ logInfo(`Default profile would remain '${defaultProfileResult.defaultProfile}'.`);
264
+ }
265
+ return {
266
+ dryRun: true,
267
+ seed: false,
268
+ profile: selectedProfile,
269
+ created: scaffold.created,
270
+ skipped: scaffold.skipped
271
+ };
272
+ }
273
+
274
+ const { created, skipped } = await ensureWorkspaceScaffold(localRoot, selectedProfile);
275
+ const defaultProfileResult = await ensureDefaultProfile({
276
+ profileName: selectedProfile,
277
+ force: profileWasExplicit
278
+ });
279
+
280
+ logInfo(`Initialized workspace for profile '${selectedProfile}'.`);
281
+ logInfo(`Created ${created} files for profile '${selectedProfile}', skipped ${skipped} existing files.`);
282
+ if (defaultProfileResult.updated) {
283
+ logInfo(`Default profile set to '${defaultProfileResult.defaultProfile}'.`);
284
+ } else {
285
+ logInfo(`Default profile remains '${defaultProfileResult.defaultProfile}'.`);
286
+ }
287
+ process.stdout.write("\n");
288
+ process.stdout.write("Next steps:\n");
289
+ process.stdout.write(` 1) Run: skills-sync build --profile ${selectedProfile}\n`);
290
+ process.stdout.write(` 2) Run: skills-sync apply --profile ${selectedProfile}\n`);
291
+ process.stdout.write(" 3) Run: skills-sync doctor\n");
292
+ }
@@ -0,0 +1,235 @@
1
+ import fs from "fs-extra";
2
+ import path from "node:path";
3
+ import { collectLocalSkillEntries } from "./bundle.js";
4
+ import {
5
+ LOCAL_OVERRIDES_ROOT,
6
+ SCHEMAS,
7
+ assertJsonFileMatchesSchema,
8
+ isInsidePath
9
+ } from "./core.js";
10
+ import {
11
+ listAvailableProfiles,
12
+ loadPackSources,
13
+ readDefaultProfile,
14
+ resolvePack,
15
+ resolveProfile
16
+ } from "./config.js";
17
+ import { collectSourcePlanning, loadUpstreamsConfig } from "./upstreams.js";
18
+
19
+ function normalizeOptionalText(value) {
20
+ if (typeof value !== "string") {
21
+ return null;
22
+ }
23
+ const normalized = value.trim();
24
+ return normalized.length > 0 ? normalized : null;
25
+ }
26
+
27
+ function sortStrings(values) {
28
+ return [...values].sort((left, right) => left.localeCompare(right));
29
+ }
30
+
31
+ function formatEnvAssignment(key, value) {
32
+ const text = String(value ?? "");
33
+ const quoted = text.length === 0 || /[\s,]/.test(text);
34
+ return `${key}=${quoted ? JSON.stringify(text) : text}`;
35
+ }
36
+
37
+ function inferProfileSource(profilePath) {
38
+ return isInsidePath(LOCAL_OVERRIDES_ROOT, profilePath) ? "local" : "seed";
39
+ }
40
+
41
+ function formatSkillsBlock(inventory) {
42
+ const lines = [];
43
+ lines.push(`Skills (${inventory.skills.total} total)`);
44
+ lines.push(` Local (${inventory.skills.local.length})`);
45
+ if (inventory.skills.local.length === 0) {
46
+ lines.push(" (none)");
47
+ } else {
48
+ for (const item of inventory.skills.local) {
49
+ lines.push(` ${item.name}`);
50
+ }
51
+ }
52
+
53
+ lines.push(` Imported (${inventory.skills.imports.length})`);
54
+ if (inventory.skills.imports.length === 0) {
55
+ lines.push(" (none)");
56
+ } else {
57
+ for (const item of inventory.skills.imports) {
58
+ lines.push(
59
+ ` ${item.upstream}@${item.ref} ${item.repoPath} -> ${item.destRelative}`
60
+ );
61
+ }
62
+ }
63
+ return lines;
64
+ }
65
+
66
+ function formatMcpBlock(inventory) {
67
+ const lines = [];
68
+ lines.push(`MCP Servers (${inventory.mcp.total})`);
69
+ if (inventory.mcp.servers.length === 0) {
70
+ lines.push(" (none)");
71
+ return lines;
72
+ }
73
+ for (const server of inventory.mcp.servers) {
74
+ if (typeof server.url === "string" && server.url.length > 0) {
75
+ lines.push(` ${server.name}\t${server.url}`);
76
+ continue;
77
+ }
78
+ const args = server.args.length > 0 ? ` ${server.args.join(" ")}` : "";
79
+ const envEntries = sortStrings(Object.keys(server.env ?? {})).map((key) =>
80
+ formatEnvAssignment(key, server.env[key])
81
+ );
82
+ const env = envEntries.length > 0 ? ` [env:${envEntries.join(", ")}]` : "";
83
+ lines.push(` ${server.name}\t${server.command}${args}${env}`);
84
+ }
85
+ return lines;
86
+ }
87
+
88
+ function profileText(inventory) {
89
+ const lines = [`Profile: ${inventory.profile.name} (${inventory.profile.source})`];
90
+ lines.push(...formatSkillsBlock(inventory));
91
+ lines.push(...formatMcpBlock(inventory));
92
+ return lines.join("\n");
93
+ }
94
+
95
+ export async function buildProfileInventory(profileName) {
96
+ const normalizedProfile = normalizeOptionalText(profileName);
97
+ if (!normalizedProfile) {
98
+ throw new Error("Profile name is required.");
99
+ }
100
+
101
+ const { profilePath, profile } = await resolveProfile(normalizedProfile);
102
+ const packRoot = await resolvePack(profile);
103
+
104
+ const { sources } = await loadPackSources(packRoot);
105
+ const upstreams = await loadUpstreamsConfig();
106
+ const planning = collectSourcePlanning(sources, upstreams.byId);
107
+
108
+ const localSkills = (await collectLocalSkillEntries(packRoot)).map((entry) => ({
109
+ name: entry.destRelative
110
+ }));
111
+ localSkills.sort((left, right) => left.name.localeCompare(right.name));
112
+
113
+ const imports = planning.skillImports
114
+ .map((entry) => ({
115
+ upstream: entry.upstreamId,
116
+ ref: entry.ref,
117
+ repoPath: entry.repoPath,
118
+ destRelative: entry.destRelative
119
+ }))
120
+ .sort((left, right) => {
121
+ const leftKey = `${left.upstream}::${left.ref}::${left.repoPath}::${left.destRelative}`;
122
+ const rightKey = `${right.upstream}::${right.ref}::${right.repoPath}::${right.destRelative}`;
123
+ return leftKey.localeCompare(rightKey);
124
+ });
125
+
126
+ const mcpPath = path.join(packRoot, "mcp", "servers.json");
127
+ const mcpDoc = (await fs.pathExists(mcpPath))
128
+ ? await assertJsonFileMatchesSchema(mcpPath, SCHEMAS.mcpServers)
129
+ : { servers: {} };
130
+ const mcpServers = sortStrings(Object.keys(mcpDoc.servers ?? {})).map((name) => {
131
+ const server = mcpDoc.servers[name] ?? {};
132
+ const url = typeof server.url === "string" && server.url.trim().length > 0 ? server.url.trim() : null;
133
+ const env = {};
134
+ if (server.env && typeof server.env === "object" && !Array.isArray(server.env)) {
135
+ for (const key of sortStrings(Object.keys(server.env))) {
136
+ env[key] = String(server.env[key]);
137
+ }
138
+ }
139
+ if (url) {
140
+ return {
141
+ name,
142
+ url
143
+ };
144
+ }
145
+ return {
146
+ name,
147
+ command: server.command,
148
+ args: Array.isArray(server.args) ? [...server.args] : [],
149
+ env
150
+ };
151
+ });
152
+
153
+ return {
154
+ profile: {
155
+ name: normalizedProfile,
156
+ source: inferProfileSource(profilePath)
157
+ },
158
+ skills: {
159
+ total: localSkills.length + imports.length,
160
+ local: localSkills,
161
+ imports
162
+ },
163
+ mcp: {
164
+ total: mcpServers.length,
165
+ servers: mcpServers
166
+ }
167
+ };
168
+ }
169
+
170
+ export async function cmdShowProfileInventory({ profile, format }) {
171
+ const explicitProfile = normalizeOptionalText(profile);
172
+ const resolvedProfile = explicitProfile ?? await readDefaultProfile();
173
+ if (!resolvedProfile) {
174
+ throw new Error(
175
+ "Profile is required. Provide --profile <name> or set a default with 'skills-sync use <name>'."
176
+ );
177
+ }
178
+
179
+ const inventory = await buildProfileInventory(resolvedProfile);
180
+ if (format === "json") {
181
+ process.stdout.write(`${JSON.stringify(inventory, null, 2)}\n`);
182
+ return;
183
+ }
184
+ process.stdout.write(`${profileText(inventory)}\n`);
185
+ }
186
+
187
+ export async function cmdListEverything({ format }) {
188
+ const profiles = await listAvailableProfiles();
189
+ if (profiles.length === 0) {
190
+ if (format === "json") {
191
+ process.stdout.write(`${JSON.stringify({ profiles: [] }, null, 2)}\n`);
192
+ } else {
193
+ process.stdout.write("No profiles found.\n");
194
+ }
195
+ return;
196
+ }
197
+
198
+ const results = [];
199
+ for (const item of profiles) {
200
+ try {
201
+ const inventory = await buildProfileInventory(item.name);
202
+ results.push({
203
+ profile: item.name,
204
+ source: item.source,
205
+ inventory
206
+ });
207
+ } catch (error) {
208
+ results.push({
209
+ profile: item.name,
210
+ source: item.source,
211
+ error: error.message
212
+ });
213
+ }
214
+ }
215
+
216
+ if (format === "json") {
217
+ process.stdout.write(`${JSON.stringify({ profiles: results }, null, 2)}\n`);
218
+ return;
219
+ }
220
+
221
+ for (let index = 0; index < results.length; index += 1) {
222
+ const item = results[index];
223
+ if (item.error) {
224
+ process.stdout.write(`Profile: ${item.profile} (${item.source})\n`);
225
+ process.stdout.write(`Error: ${item.error}\n`);
226
+ } else {
227
+ process.stdout.write(profileText(item.inventory));
228
+ }
229
+ if (index < results.length - 1) {
230
+ process.stdout.write("\n\n");
231
+ } else {
232
+ process.stdout.write("\n");
233
+ }
234
+ }
235
+ }