atris 2.2.2 → 2.3.1

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.
@@ -13,13 +13,17 @@
13
13
  const { loadCredentials } = require('../utils/auth');
14
14
  const { apiRequestJson } = require('../utils/api');
15
15
 
16
- async function getAuthToken() {
16
+ function getAuth() {
17
17
  const creds = loadCredentials();
18
18
  if (!creds || !creds.token) {
19
19
  console.error('Not logged in. Run: atris login');
20
20
  process.exit(1);
21
21
  }
22
- return creds.token;
22
+ return { token: creds.token, email: creds.email || 'unknown' };
23
+ }
24
+
25
+ async function getAuthToken() {
26
+ return getAuth().token;
23
27
  }
24
28
 
25
29
  // ============================================================================
@@ -27,7 +31,7 @@ async function getAuthToken() {
27
31
  // ============================================================================
28
32
 
29
33
  async function gmailInbox(options = {}) {
30
- const token = await getAuthToken();
34
+ const { token, email } = getAuth();
31
35
  const limit = options.limit || 10;
32
36
 
33
37
  console.log('📬 Fetching inbox...\n');
@@ -38,8 +42,10 @@ async function gmailInbox(options = {}) {
38
42
  });
39
43
 
40
44
  if (!result.ok) {
41
- if (result.status === 401) {
42
- console.error('Gmail not connected. Connect at: https://atris.ai/dashboard/settings');
45
+ if (result.status === 400 || result.status === 401) {
46
+ console.error(`Gmail not connected for ${email}.`);
47
+ console.error('Connect at: https://atris.ai/dashboard/settings');
48
+ console.error(`Make sure you're signed in as ${email} on the web.`);
43
49
  } else {
44
50
  console.error(`Error: ${result.error || 'Failed to fetch inbox'}`);
45
51
  }
@@ -123,7 +129,7 @@ async function gmailCommand(subcommand, ...args) {
123
129
  // ============================================================================
124
130
 
125
131
  async function calendarToday() {
126
- const token = await getAuthToken();
132
+ const { token, email } = getAuth();
127
133
 
128
134
  console.log('📅 Today\'s events:\n');
129
135
 
@@ -133,8 +139,10 @@ async function calendarToday() {
133
139
  });
134
140
 
135
141
  if (!result.ok) {
136
- if (result.status === 401) {
137
- console.error('Calendar not connected. Connect at: https://atris.ai/dashboard/settings');
142
+ if (result.status === 400 || result.status === 401) {
143
+ console.error(`Calendar not connected for ${email}.`);
144
+ console.error('Connect at: https://atris.ai/dashboard/settings');
145
+ console.error(`Make sure you're signed in as ${email} on the web.`);
138
146
  } else {
139
147
  console.error(`Error: ${result.error || 'Failed to fetch events'}`);
140
148
  }
@@ -215,8 +223,11 @@ async function twitterPost(text) {
215
223
  });
216
224
 
217
225
  if (!result.ok) {
218
- if (result.status === 401) {
219
- console.error('Twitter not connected. Connect at: https://atris.ai/dashboard/settings');
226
+ if (result.status === 400 || result.status === 401) {
227
+ const { email } = getAuth();
228
+ console.error(`Twitter not connected for ${email}.`);
229
+ console.error('Connect at: https://atris.ai/dashboard/settings');
230
+ console.error(`Make sure you're signed in as ${email} on the web.`);
220
231
  } else {
221
232
  console.error(`Error: ${result.error || 'Failed to post tweet'}`);
222
233
  }
@@ -256,8 +267,11 @@ async function slackChannels() {
256
267
  });
257
268
 
258
269
  if (!result.ok) {
259
- if (result.status === 401) {
260
- console.error('Slack not connected. Connect at: https://atris.ai/dashboard/settings');
270
+ if (result.status === 400 || result.status === 401) {
271
+ const { email } = getAuth();
272
+ console.error(`Slack not connected for ${email}.`);
273
+ console.error('Connect at: https://atris.ai/dashboard/settings');
274
+ console.error(`Make sure you're signed in as ${email} on the web.`);
261
275
  } else {
262
276
  console.error(`Error: ${result.error || 'Failed to fetch channels'}`);
263
277
  }
@@ -0,0 +1,450 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const { execSync } = require('child_process');
5
+ const { findAllSkills, parseFrontmatter } = require('./skill');
6
+
7
+ // --- Recursive Copy Helper ---
8
+
9
+ function copyRecursive(src, dest) {
10
+ fs.mkdirSync(dest, { recursive: true });
11
+ const entries = fs.readdirSync(src);
12
+ for (const entry of entries) {
13
+ if (entry === '.DS_Store' || entry === '.git') continue;
14
+ const srcPath = path.join(src, entry);
15
+ const destPath = path.join(dest, entry);
16
+ if (fs.statSync(srcPath).isDirectory()) {
17
+ copyRecursive(srcPath, destPath);
18
+ } else {
19
+ fs.copyFileSync(srcPath, destPath);
20
+ }
21
+ }
22
+ }
23
+
24
+ // --- Generate plugin.json manifest ---
25
+
26
+ function generateManifest(skills, projectDir) {
27
+ let pkg = {};
28
+ const pkgPath = path.join(projectDir, 'package.json');
29
+ if (fs.existsSync(pkgPath)) {
30
+ pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
31
+ }
32
+
33
+ return {
34
+ name: 'atris-workspace',
35
+ version: pkg.version || '1.0.0',
36
+ description: `Atris workspace skills for Cowork — ${skills.length} integrations (email, calendar, slack, drive, notion, and more)`,
37
+ author: {
38
+ name: typeof pkg.author === 'string' ? pkg.author : (pkg.author?.name || 'Atris Labs')
39
+ },
40
+ homepage: pkg.homepage || 'https://github.com/atrislabs/atris',
41
+ keywords: ['atris', 'workspace', 'integrations', 'email', 'calendar', 'slack', 'notion', 'drive']
42
+ };
43
+ }
44
+
45
+ // --- Generate /atris-setup command ---
46
+
47
+ function generateSetupCommand() {
48
+ return `---
49
+ description: Set up Atris authentication and connect integrations (Gmail, Calendar, Slack, Notion, Drive)
50
+ allowed-tools: Bash, Read
51
+ ---
52
+
53
+ # Atris Setup
54
+
55
+ Run the following steps to bootstrap Atris for this workspace.
56
+
57
+ ## Step 1: Check if Atris CLI is installed
58
+
59
+ \`\`\`bash
60
+ command -v atris || npm install -g atris
61
+ \`\`\`
62
+
63
+ If the install fails, tell the user to run \`npm install -g atris\` manually.
64
+
65
+ ## Step 2: Authenticate with AtrisOS
66
+
67
+ \`\`\`bash
68
+ atris whoami
69
+ \`\`\`
70
+
71
+ If not logged in, run:
72
+
73
+ \`\`\`bash
74
+ atris login
75
+ \`\`\`
76
+
77
+ This is interactive — the user will need to complete OAuth in their browser and paste the CLI code. Guide them through it.
78
+
79
+ ## Step 3: Check integration status
80
+
81
+ After login, check which integrations are connected:
82
+
83
+ \`\`\`bash
84
+ TOKEN=$(node -e "console.log(require('$HOME/.atris/credentials.json').token)")
85
+
86
+ echo "=== Gmail ==="
87
+ curl -s "https://api.atris.ai/api/integrations/gmail/status" -H "Authorization: Bearer $TOKEN"
88
+
89
+ echo "\\n=== Google Calendar ==="
90
+ curl -s "https://api.atris.ai/api/integrations/google-calendar/status" -H "Authorization: Bearer $TOKEN"
91
+
92
+ echo "\\n=== Slack ==="
93
+ curl -s "https://api.atris.ai/api/integrations/slack/status" -H "Authorization: Bearer $TOKEN"
94
+
95
+ echo "\\n=== Notion ==="
96
+ curl -s "https://api.atris.ai/api/integrations/notion/status" -H "Authorization: Bearer $TOKEN"
97
+
98
+ echo "\\n=== Google Drive ==="
99
+ curl -s "https://api.atris.ai/api/integrations/google-drive/status" -H "Authorization: Bearer $TOKEN"
100
+ \`\`\`
101
+
102
+ ## Step 4: Connect missing integrations
103
+
104
+ For any integration that shows \`"connected": false\`, guide the user through the OAuth flow:
105
+
106
+ 1. Call the integration's \`/start\` endpoint to get an auth URL
107
+ 2. Have the user open the URL in their browser
108
+ 3. After authorizing, confirm the connection by re-checking status
109
+
110
+ Tell the user which integrations are connected and which still need setup. They can connect more later by running \`/atris-setup\` again.
111
+ `;
112
+ }
113
+
114
+ // --- Generate README.md ---
115
+
116
+ function generateREADME(skills) {
117
+ const skillList = skills.map(s => {
118
+ const fm = s.frontmatter || {};
119
+ const name = fm.name || s.folder || 'unknown';
120
+ const desc = (fm.description || '').split('.')[0] || 'No description';
121
+ return `- **${name}**: ${desc}`;
122
+ }).join('\n');
123
+
124
+ return `# Atris Workspace Plugin
125
+
126
+ Brings Atris CLI skills into the Cowork desktop app.
127
+
128
+ ## Setup
129
+
130
+ 1. Install this plugin in Cowork
131
+ 2. Run \`/atris-setup\` to authenticate and connect integrations
132
+ 3. Start using skills — they trigger automatically based on your requests
133
+
134
+ ## Included Skills (${skills.length})
135
+
136
+ ${skillList}
137
+
138
+ ## Requirements
139
+
140
+ - [Atris CLI](https://www.npmjs.com/package/atris) (\`npm install -g atris\`)
141
+ - AtrisOS account (free at https://atris.ai)
142
+
143
+ ## Learn More
144
+
145
+ - Docs: https://github.com/atrislabs/atris
146
+ - CLI help: \`atris help\`
147
+ `;
148
+ }
149
+
150
+ // --- BUILD subcommand ---
151
+
152
+ function buildPlugin(...args) {
153
+ const projectDir = process.cwd();
154
+ const atrisDir = path.join(projectDir, 'atris');
155
+ const skillsDir = path.join(atrisDir, 'skills');
156
+
157
+ // 1. Validate
158
+ if (!fs.existsSync(atrisDir)) {
159
+ console.error('✗ Error: atris/ folder not found. Run "atris init" first.');
160
+ process.exit(1);
161
+ }
162
+ if (!fs.existsSync(skillsDir)) {
163
+ console.error('✗ Error: atris/skills/ folder not found.');
164
+ process.exit(1);
165
+ }
166
+
167
+ // 2. Discover skills
168
+ const allSkills = findAllSkills(skillsDir);
169
+ if (allSkills.length === 0) {
170
+ console.warn('⚠ No skills found in atris/skills/. Plugin will be empty.');
171
+ }
172
+
173
+ console.log(`\n📦 Building Atris plugin (${allSkills.length} skills)...\n`);
174
+
175
+ // 3. Create temp dir
176
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-plugin-'));
177
+
178
+ try {
179
+ // 4. Create .claude-plugin/plugin.json
180
+ const pluginMetaDir = path.join(tempDir, '.claude-plugin');
181
+ fs.mkdirSync(pluginMetaDir, { recursive: true });
182
+ const manifest = generateManifest(allSkills, projectDir);
183
+ fs.writeFileSync(
184
+ path.join(pluginMetaDir, 'plugin.json'),
185
+ JSON.stringify(manifest, null, 2)
186
+ );
187
+ console.log(' ✓ Created plugin.json manifest');
188
+
189
+ // 5. Copy all skills
190
+ const pluginSkillsDir = path.join(tempDir, 'skills');
191
+ fs.mkdirSync(pluginSkillsDir, { recursive: true });
192
+
193
+ for (const skill of allSkills) {
194
+ // skill object from findAllSkills has .dir (full path) and we need folder name
195
+ const skillFolder = skill.folder || path.basename(skill.dir || skill.path || '');
196
+ if (!skillFolder) continue;
197
+
198
+ const srcPath = path.join(skillsDir, skillFolder);
199
+ const destPath = path.join(pluginSkillsDir, skillFolder);
200
+
201
+ if (fs.existsSync(srcPath) && fs.statSync(srcPath).isDirectory()) {
202
+ copyRecursive(srcPath, destPath);
203
+ const fm = skill.frontmatter || {};
204
+ console.log(` ✓ ${fm.name || skillFolder}`);
205
+ }
206
+ }
207
+
208
+ // 6. Create commands/atris-setup.md
209
+ const commandsDir = path.join(tempDir, 'commands');
210
+ fs.mkdirSync(commandsDir, { recursive: true });
211
+ fs.writeFileSync(path.join(commandsDir, 'atris-setup.md'), generateSetupCommand());
212
+ console.log(' ✓ Created /atris-setup command');
213
+
214
+ // 7. Create README.md
215
+ fs.writeFileSync(path.join(tempDir, 'README.md'), generateREADME(allSkills));
216
+ console.log(' ✓ Created README.md');
217
+
218
+ // 8. Parse --output flag
219
+ let outputFile = path.join(projectDir, 'atris-workspace.plugin');
220
+ for (const arg of args) {
221
+ if (typeof arg === 'string' && arg.startsWith('--output=')) {
222
+ outputFile = path.resolve(arg.split('=')[1]);
223
+ }
224
+ }
225
+
226
+ // 9. ZIP it
227
+ try {
228
+ // Create zip in /tmp first (Cowork best practice)
229
+ const tmpZip = path.join(os.tmpdir(), 'atris-workspace.plugin');
230
+ if (fs.existsSync(tmpZip)) fs.unlinkSync(tmpZip);
231
+
232
+ execSync(`cd "${tempDir}" && zip -r "${tmpZip}" . -x "*.DS_Store" "*.git*"`, {
233
+ stdio: 'pipe'
234
+ });
235
+
236
+ // Copy to final destination
237
+ fs.copyFileSync(tmpZip, outputFile);
238
+ fs.unlinkSync(tmpZip);
239
+ } catch (e) {
240
+ console.error('✗ ZIP failed. Ensure "zip" command is available.');
241
+ console.error(` Temp dir preserved at: ${tempDir}`);
242
+ process.exit(1);
243
+ }
244
+
245
+ // 10. Summary
246
+ const stats = fs.statSync(outputFile);
247
+ const sizeKB = (stats.size / 1024).toFixed(1);
248
+ console.log('');
249
+ console.log(`✓ Plugin built: ${path.basename(outputFile)} (${allSkills.length} skills, ${sizeKB}KB)`);
250
+ console.log(` Location: ${outputFile}`);
251
+ console.log('');
252
+ console.log('Install in Cowork:');
253
+ console.log(' Open the .plugin file or drag it into the Cowork app.');
254
+ console.log(' Then run /atris-setup to authenticate.');
255
+ console.log('');
256
+
257
+ } finally {
258
+ // Clean up temp dir (only if ZIP succeeded — we skip cleanup on error above)
259
+ if (fs.existsSync(tempDir)) {
260
+ try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch (e) { /* ignore */ }
261
+ }
262
+ }
263
+ }
264
+
265
+ // --- INFO subcommand ---
266
+
267
+ function showPluginInfo() {
268
+ const skillsDir = path.join(process.cwd(), 'atris', 'skills');
269
+
270
+ if (!fs.existsSync(skillsDir)) {
271
+ console.error('✗ atris/skills/ not found. Run "atris init" first.');
272
+ process.exit(1);
273
+ }
274
+
275
+ const allSkills = findAllSkills(skillsDir);
276
+
277
+ let pkg = {};
278
+ const pkgPath = path.join(process.cwd(), 'package.json');
279
+ if (fs.existsSync(pkgPath)) {
280
+ pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
281
+ }
282
+
283
+ console.log('');
284
+ console.log('Atris Plugin Preview');
285
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
286
+ console.log(` Name: atris-workspace`);
287
+ console.log(` Version: ${pkg.version || '1.0.0'}`);
288
+ console.log(` Skills: ${allSkills.length}`);
289
+ console.log('');
290
+
291
+ if (allSkills.length > 0) {
292
+ console.log('Included skills:');
293
+ for (const skill of allSkills) {
294
+ const fm = skill.frontmatter || {};
295
+ const name = fm.name || skill.folder || 'unknown';
296
+ const desc = (fm.description || 'No description').substring(0, 65);
297
+ console.log(` • ${name.padEnd(16)} ${desc}`);
298
+ }
299
+ console.log('');
300
+ }
301
+
302
+ console.log('Components:');
303
+ console.log(` Skills: ${allSkills.length}`);
304
+ console.log(' Commands: 1 (/atris-setup)');
305
+ console.log('');
306
+ console.log('Run "atris plugin build" to package.');
307
+ console.log('');
308
+ }
309
+
310
+ // --- PUBLISH subcommand ---
311
+
312
+ function publishPlugin(...args) {
313
+ const projectDir = process.cwd();
314
+ const skillsDir = path.join(projectDir, 'atris', 'skills');
315
+
316
+ if (!fs.existsSync(skillsDir)) {
317
+ console.error('✗ atris/skills/ not found. Run "atris init" first.');
318
+ process.exit(1);
319
+ }
320
+
321
+ // Find the marketplace repo — check sibling dir or --repo flag
322
+ let marketplaceDir = null;
323
+ for (const arg of args) {
324
+ if (typeof arg === 'string' && arg.startsWith('--repo=')) {
325
+ marketplaceDir = path.resolve(arg.split('=')[1]);
326
+ }
327
+ }
328
+ if (!marketplaceDir) {
329
+ // Default: look for atris-plugins as a sibling directory
330
+ const siblingDir = path.join(path.dirname(projectDir), 'atris-plugins');
331
+ if (fs.existsSync(siblingDir)) {
332
+ marketplaceDir = siblingDir;
333
+ }
334
+ }
335
+ if (!marketplaceDir || !fs.existsSync(marketplaceDir)) {
336
+ console.error('✗ Marketplace repo not found.');
337
+ console.error(' Expected at: ../atris-plugins/');
338
+ console.error(' Or specify: atris plugin publish --repo=/path/to/atris-plugins');
339
+ process.exit(1);
340
+ }
341
+
342
+ const pluginDir = path.join(marketplaceDir, 'plugins', 'atris-workspace');
343
+ const destSkillsDir = path.join(pluginDir, 'skills');
344
+ const destCommandsDir = path.join(pluginDir, 'commands');
345
+
346
+ console.log(`\n📡 Publishing to ${path.basename(marketplaceDir)}...\n`);
347
+
348
+ // 1. Clear old skills and re-copy
349
+ if (fs.existsSync(destSkillsDir)) {
350
+ fs.rmSync(destSkillsDir, { recursive: true, force: true });
351
+ }
352
+ fs.mkdirSync(destSkillsDir, { recursive: true });
353
+
354
+ const allSkills = findAllSkills(skillsDir);
355
+ for (const skill of allSkills) {
356
+ const skillFolder = skill.folder || path.basename(skill.dir || skill.path || '');
357
+ if (!skillFolder) continue;
358
+ const srcPath = path.join(skillsDir, skillFolder);
359
+ const destPath = path.join(destSkillsDir, skillFolder);
360
+ if (fs.existsSync(srcPath) && fs.statSync(srcPath).isDirectory()) {
361
+ copyRecursive(srcPath, destPath);
362
+ const fm = skill.frontmatter || {};
363
+ console.log(` ✓ ${fm.name || skillFolder}`);
364
+ }
365
+ }
366
+
367
+ // 2. Update commands
368
+ fs.mkdirSync(destCommandsDir, { recursive: true });
369
+ fs.writeFileSync(path.join(destCommandsDir, 'atris-setup.md'), generateSetupCommand());
370
+ console.log(' ✓ /atris-setup command');
371
+
372
+ // 3. Update plugin.json version
373
+ let pkg = {};
374
+ const pkgPath = path.join(projectDir, 'package.json');
375
+ if (fs.existsSync(pkgPath)) {
376
+ pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
377
+ }
378
+ const pluginJsonPath = path.join(pluginDir, '.claude-plugin', 'plugin.json');
379
+ if (fs.existsSync(pluginJsonPath)) {
380
+ const pluginJson = JSON.parse(fs.readFileSync(pluginJsonPath, 'utf8'));
381
+ pluginJson.version = pkg.version || pluginJson.version;
382
+ fs.writeFileSync(pluginJsonPath, JSON.stringify(pluginJson, null, 2) + '\n');
383
+ }
384
+
385
+ // 4. Update marketplace.json version
386
+ const marketplaceJsonPath = path.join(marketplaceDir, '.claude-plugin', 'marketplace.json');
387
+ if (fs.existsSync(marketplaceJsonPath)) {
388
+ const mkt = JSON.parse(fs.readFileSync(marketplaceJsonPath, 'utf8'));
389
+ const entry = (mkt.plugins || []).find(p => p.name === 'atris-workspace');
390
+ if (entry) {
391
+ entry.version = pkg.version || entry.version;
392
+ }
393
+ fs.writeFileSync(marketplaceJsonPath, JSON.stringify(mkt, null, 2) + '\n');
394
+ }
395
+
396
+ // 5. Update README
397
+ fs.writeFileSync(path.join(pluginDir, 'README.md'), generateREADME(allSkills));
398
+ console.log(' ✓ README.md');
399
+
400
+ // 6. Git commit + push
401
+ console.log('');
402
+ const version = pkg.version || 'latest';
403
+ try {
404
+ execSync('git add -A', { cwd: marketplaceDir, stdio: 'pipe' });
405
+ const status = execSync('git status --porcelain', { cwd: marketplaceDir, encoding: 'utf8' }).trim();
406
+ if (!status) {
407
+ console.log('✓ No changes — marketplace already up to date.');
408
+ return;
409
+ }
410
+ execSync(`git commit -m "chore: Update atris-workspace plugin to v${version}"`, {
411
+ cwd: marketplaceDir, stdio: 'pipe'
412
+ });
413
+ execSync('git push', { cwd: marketplaceDir, stdio: 'pipe' });
414
+ console.log(`✓ Published v${version} (${allSkills.length} skills) → pushed to remote`);
415
+ } catch (e) {
416
+ console.error('⚠ Skills synced but git push failed. Commit manually:');
417
+ console.error(` cd ${marketplaceDir} && git add -A && git commit -m "update" && git push`);
418
+ }
419
+ console.log('');
420
+ }
421
+
422
+ // --- Main Dispatcher ---
423
+
424
+ function pluginCommand(subcommand, ...args) {
425
+ switch (subcommand) {
426
+ case 'build':
427
+ buildPlugin(...args);
428
+ break;
429
+ case 'info':
430
+ case 'preview':
431
+ showPluginInfo();
432
+ break;
433
+ case 'publish':
434
+ case 'sync':
435
+ publishPlugin(...args);
436
+ break;
437
+ default:
438
+ console.log('');
439
+ console.log('Usage: atris plugin <subcommand>');
440
+ console.log('');
441
+ console.log('Subcommands:');
442
+ console.log(' build [--output=path] - Package skills into .plugin for Cowork');
443
+ console.log(' publish [--repo=path] - Sync skills to marketplace repo and push');
444
+ console.log(' info - Preview what will be included');
445
+ console.log('');
446
+ break;
447
+ }
448
+ }
449
+
450
+ module.exports = { pluginCommand };
package/commands/sync.js CHANGED
@@ -292,4 +292,90 @@ After displaying the boot output, respond to the user naturally.
292
292
  }
293
293
  }
294
294
 
295
- module.exports = { syncAtris };
295
+ /**
296
+ * Lightweight skill-only sync. Compares package skills against project skills
297
+ * and updates any that differ. Called automatically from `atris activate`.
298
+ * Returns number of files updated (0 = already current).
299
+ */
300
+ function syncSkills({ silent = false } = {}) {
301
+ const targetDir = path.join(process.cwd(), 'atris');
302
+ const packageSkillsDir = path.join(__dirname, '..', 'atris', 'skills');
303
+ const userSkillsDir = path.join(targetDir, 'skills');
304
+ const claudeSkillsBaseDir = path.join(process.cwd(), '.claude', 'skills');
305
+
306
+ if (!fs.existsSync(targetDir) || !fs.existsSync(packageSkillsDir)) {
307
+ return 0;
308
+ }
309
+
310
+ if (!fs.existsSync(userSkillsDir)) {
311
+ fs.mkdirSync(userSkillsDir, { recursive: true });
312
+ }
313
+ if (!fs.existsSync(claudeSkillsBaseDir)) {
314
+ fs.mkdirSync(claudeSkillsBaseDir, { recursive: true });
315
+ }
316
+
317
+ let updated = 0;
318
+
319
+ const skillFolders = fs.readdirSync(packageSkillsDir).filter(f =>
320
+ fs.statSync(path.join(packageSkillsDir, f)).isDirectory()
321
+ );
322
+
323
+ for (const skill of skillFolders) {
324
+ const srcSkillDir = path.join(packageSkillsDir, skill);
325
+ const destSkillDir = path.join(userSkillsDir, skill);
326
+ const symlinkPath = path.join(claudeSkillsBaseDir, skill);
327
+
328
+ const syncRecursive = (src, dest, skillName, basePath = '') => {
329
+ if (!fs.existsSync(dest)) {
330
+ fs.mkdirSync(dest, { recursive: true });
331
+ }
332
+ const entries = fs.readdirSync(src);
333
+ for (const entry of entries) {
334
+ const srcPath = path.join(src, entry);
335
+ const destPath = path.join(dest, entry);
336
+ const relPath = basePath ? `${basePath}/${entry}` : entry;
337
+
338
+ if (fs.statSync(srcPath).isDirectory()) {
339
+ syncRecursive(srcPath, destPath, skillName, relPath);
340
+ } else {
341
+ const srcContent = fs.readFileSync(srcPath, 'utf8');
342
+ const destContent = fs.existsSync(destPath) ? fs.readFileSync(destPath, 'utf8') : '';
343
+ if (srcContent !== destContent) {
344
+ fs.writeFileSync(destPath, srcContent);
345
+ if (entry.endsWith('.sh')) {
346
+ fs.chmodSync(destPath, 0o755);
347
+ }
348
+ if (!silent) {
349
+ console.log(`✓ Updated atris/skills/${skillName}/${relPath}`);
350
+ }
351
+ updated++;
352
+ }
353
+ }
354
+ }
355
+ };
356
+
357
+ syncRecursive(srcSkillDir, destSkillDir, skill);
358
+
359
+ // Create symlink if doesn't exist
360
+ if (!fs.existsSync(symlinkPath)) {
361
+ const relativePath = path.join('..', '..', 'atris', 'skills', skill);
362
+ try {
363
+ fs.symlinkSync(relativePath, symlinkPath);
364
+ if (!silent) {
365
+ console.log(`✓ Linked .claude/skills/${skill}`);
366
+ }
367
+ } catch (e) {
368
+ // Fallback: copy instead of symlink
369
+ fs.mkdirSync(symlinkPath, { recursive: true });
370
+ const skillFile = path.join(destSkillDir, 'SKILL.md');
371
+ if (fs.existsSync(skillFile)) {
372
+ fs.copyFileSync(skillFile, path.join(symlinkPath, 'SKILL.md'));
373
+ }
374
+ }
375
+ }
376
+ }
377
+
378
+ return updated;
379
+ }
380
+
381
+ module.exports = { syncAtris, syncSkills };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "2.2.2",
3
+ "version": "2.3.1",
4
4
  "description": "atrisDev (atris dev) - CLI for AI coding agents. Works with Claude Code, Cursor, Windsurf. Make any codebase AI-navigable.",
5
5
  "main": "bin/atris.js",
6
6
  "bin": {