@ytspar/sweetlink 1.10.0 → 1.11.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.
@@ -1,14 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Setup script to symlink shared Claude context files to the consuming project
3
+ * Setup script to symlink shared Claude context and skills to the consuming project.
4
4
  *
5
- * This script runs as postinstall and creates symlinks from the project's
6
- * .claude/context/ directory to the shared context files in this package.
5
+ * Can be run directly (`node scripts/setup-claude-context.mjs`) or via CLI (`pnpm sweetlink setup`).
7
6
  *
8
- * Symlinks are relative paths so they work across different environments.
7
+ * Creates relative symlinks so they work across different environments.
8
+ *
9
+ * What gets linked:
10
+ * - .claude/context/ ← context files from this package's claude-context/
11
+ * - .claude/skills/ ← skill directories from this package's claude-skills/
9
12
  */
10
13
 
11
- import { existsSync, mkdirSync, readlinkSync, symlinkSync, unlinkSync } from 'fs';
14
+ import { existsSync, lstatSync, mkdirSync, readdirSync, readlinkSync, symlinkSync, unlinkSync } from 'fs';
12
15
  import { dirname, join, relative } from 'path';
13
16
  import { fileURLToPath } from 'url';
14
17
 
@@ -20,82 +23,104 @@ const packageRoot = join(__dirname, '..');
20
23
  const nodeModules = join(packageRoot, '..', '..', '..');
21
24
  const projectRoot = join(nodeModules, '..');
22
25
 
23
- // Source: this package's claude-context/
24
- const sourceDir = join(packageRoot, 'claude-context');
25
-
26
- // Target: project's .claude/context/
27
- const targetDir = join(projectRoot, '.claude', 'context');
28
-
29
- // Files to symlink (add more as needed)
30
- const filesToLink = [
31
- 'ui-verification-mandate.md',
32
- 'debugging-protocol.md',
33
- 'sweetlink-architecture.md',
34
- 'component-development-guide.md',
35
- ];
26
+ /**
27
+ * Create a relative symlink, skipping if already correct.
28
+ * Won't overwrite non-symlink files.
29
+ */
30
+ function linkOne(sourcePath, targetPath, label) {
31
+ const targetDir = dirname(targetPath);
32
+ const relativePath = relative(targetDir, sourcePath);
36
33
 
37
- function setupSymlinks() {
38
- // Skip if running in the tools repo itself (during development)
39
- if (projectRoot.includes('ytspar/devbar')) {
40
- return;
34
+ if (existsSync(targetPath) || lstatSync(targetPath, { throwIfNoEntry: false })) {
35
+ try {
36
+ const currentLink = readlinkSync(targetPath);
37
+ if (currentLink === relativePath) {
38
+ return; // Already correct
39
+ }
40
+ // Remove incorrect symlink
41
+ unlinkSync(targetPath);
42
+ } catch {
43
+ // Not a symlink — don't overwrite user's files
44
+ console.log(` [skip] ${label} — file exists (not a symlink)`);
45
+ return;
46
+ }
41
47
  }
42
48
 
43
- // Check if source directory exists
44
- if (!existsSync(sourceDir)) {
45
- // claude-context not built yet, skip silently
46
- return;
49
+ try {
50
+ symlinkSync(relativePath, targetPath);
51
+ console.log(` [link] ${label}`);
52
+ } catch (err) {
53
+ console.error(` [error] ${label} — ${err.message}`);
47
54
  }
55
+ }
48
56
 
49
- // Check if project has .claude directory (indicates Claude Code project)
50
- const claudeDir = join(projectRoot, '.claude');
51
- if (!existsSync(claudeDir)) {
52
- // Not a Claude Code project, skip silently
53
- return;
54
- }
57
+ function setupContext(claudeDir) {
58
+ const sourceDir = join(packageRoot, 'claude-context');
59
+ if (!existsSync(sourceDir)) return;
55
60
 
56
- // Ensure target directory exists
57
- if (!existsSync(targetDir)) {
58
- mkdirSync(targetDir, { recursive: true });
59
- }
61
+ const targetDir = join(claudeDir, 'context');
62
+ mkdirSync(targetDir, { recursive: true });
63
+
64
+ const files = readdirSync(sourceDir).filter(f => f.endsWith('.md'));
65
+ if (files.length === 0) return;
60
66
 
61
67
  console.log('[@ytspar/sweetlink] Setting up Claude context symlinks...');
68
+ for (const file of files) {
69
+ linkOne(join(sourceDir, file), join(targetDir, file), file);
70
+ }
71
+ }
62
72
 
63
- for (const file of filesToLink) {
64
- const sourcePath = join(sourceDir, file);
65
- const targetPath = join(targetDir, file);
73
+ function setupSkills(claudeDir) {
74
+ const sourceDir = join(packageRoot, 'claude-skills');
75
+ if (!existsSync(sourceDir)) return;
66
76
 
67
- // Skip if source doesn't exist
68
- if (!existsSync(sourcePath)) {
69
- continue;
70
- }
77
+ const targetDir = join(claudeDir, 'skills');
78
+ mkdirSync(targetDir, { recursive: true });
71
79
 
72
- // Calculate relative path for symlink
73
- const relativePath = relative(targetDir, sourcePath);
74
-
75
- // Check if symlink already exists and points to correct location
76
- if (existsSync(targetPath)) {
77
- try {
78
- const currentLink = readlinkSync(targetPath);
79
- if (currentLink === relativePath) {
80
- continue; // Already correct
81
- }
82
- // Remove incorrect symlink
83
- unlinkSync(targetPath);
84
- } catch {
85
- // Not a symlink - don't overwrite user's files
86
- console.log(` [skip] ${file} - file exists (not a symlink)`);
87
- continue;
88
- }
89
- }
80
+ // Each subdirectory in claude-skills/ is a skill
81
+ const skills = readdirSync(sourceDir, { withFileTypes: true })
82
+ .filter(d => d.isDirectory())
83
+ .map(d => d.name);
90
84
 
91
- // Create symlink
92
- try {
93
- symlinkSync(relativePath, targetPath);
94
- console.log(` [link] ${file}`);
95
- } catch (err) {
96
- console.error(` [error] ${file} - ${err.message}`);
85
+ if (skills.length === 0) return;
86
+
87
+ console.log('[@ytspar/sweetlink] Setting up Claude skill symlinks...');
88
+
89
+ // If .claude/skills is itself a symlink (e.g. to a shared tools repo),
90
+ // we can't add entries inside it. Warn and provide instructions.
91
+ try {
92
+ const stat = lstatSync(targetDir);
93
+ if (stat.isSymbolicLink()) {
94
+ const linkTarget = readlinkSync(targetDir);
95
+ console.log(` [info] .claude/skills is a symlink → ${linkTarget}`);
96
+ console.log(` [info] Skills from @ytspar/sweetlink should be symlinked inside that directory.`);
97
+ console.log(` [info] Run: cd ${linkTarget} && ln -sf ${relative(linkTarget, sourceDir)}/<skill> .`);
98
+ return;
97
99
  }
100
+ } catch {
101
+ // Doesn't exist yet, will be created above
102
+ }
103
+
104
+ for (const skill of skills) {
105
+ linkOne(join(sourceDir, skill), join(targetDir, skill), `skills/${skill}`);
106
+ }
107
+ }
108
+
109
+ function setup() {
110
+ // Skip if running inside the devbar repo itself (development)
111
+ if (projectRoot.includes('ytspar/devbar')) {
112
+ console.log('[@ytspar/sweetlink] Skipping setup (running inside devbar repo)');
113
+ return;
114
+ }
115
+
116
+ const claudeDir = join(projectRoot, '.claude');
117
+ if (!existsSync(claudeDir)) {
118
+ // Not a Claude Code project, skip silently
119
+ return;
98
120
  }
121
+
122
+ setupContext(claudeDir);
123
+ setupSkills(claudeDir);
99
124
  }
100
125
 
101
- setupSymlinks();
126
+ setup();