luckiest-co 1.0.34 → 1.0.36

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.
@@ -9,7 +9,7 @@
9
9
  "name": "luckiest",
10
10
  "source": "./",
11
11
  "description": "Plan, go, finish, plus bundled free Luckiest skills that work in Claude Code, web chat, and Cowork.",
12
- "version": "0.1.33",
12
+ "version": "0.1.35",
13
13
  "author": { "name": "Luckiest" }
14
14
  }
15
15
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "luckiest",
3
3
  "description": "Plan, go, finish. Guided planning, progress dashboards, and your luckiest.co tribe inside Claude Code.",
4
- "version": "0.1.33",
4
+ "version": "0.1.35",
5
5
  "author": { "name": "Luckiest" }
6
6
  }
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Release bump: patch-bumps the npm version and the plugin version, then dates
4
+ // the CHANGELOG (turns "## Unreleased" into the release header, or adds one).
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ const green = '\x1b[32m';
10
+ const reset = '\x1b[0m';
11
+
12
+ const VERSION_RE = /("version":\s*")(\d+)\.(\d+)\.(\d+)(")/;
13
+
14
+ const patch = (v) => v.replace(/\d+$/, (n) => Number(n) + 1);
15
+
16
+ /** Bumps all four release files in dir. Returns { npm, plugin }. Writes nothing if the plugin versions disagree. */
17
+ function bumpVersion({ dir = path.join(__dirname, '..'), date = new Date().toLocaleDateString('en-CA') } = {}) {
18
+ const files = {
19
+ pkg: path.join(dir, 'package.json'),
20
+ plugin: path.join(dir, '.claude-plugin/plugin.json'),
21
+ market: path.join(dir, '.claude-plugin/marketplace.json'),
22
+ changelog: path.join(dir, 'CHANGELOG.md'),
23
+ };
24
+ const text = Object.fromEntries(Object.entries(files).map(([k, f]) => [k, fs.readFileSync(f, 'utf8')]));
25
+ const current = (k) => {
26
+ const m = text[k].match(VERSION_RE);
27
+ if (!m) throw new Error(`No version found in ${files[k]}`);
28
+ return `${m[2]}.${m[3]}.${m[4]}`;
29
+ };
30
+
31
+ const oldPlugin = current('plugin');
32
+ if (current('market') !== oldPlugin) {
33
+ throw new Error(`plugin.json (${oldPlugin}) and marketplace.json (${current('market')}) disagree. Fix them by hand first.`);
34
+ }
35
+ const npm = patch(current('pkg'));
36
+ const plugin = patch(oldPlugin);
37
+
38
+ // ponytail: replaces the first "version" in each file, which is the right one in all three today.
39
+ const write = (k, v) => fs.writeFileSync(files[k], text[k].replace(VERSION_RE, `$1${v}$5`));
40
+ write('pkg', npm);
41
+ write('plugin', plugin);
42
+ write('market', plugin);
43
+ // An "## Unreleased" section becomes the dated header, notes and all. Otherwise a fresh header goes on top.
44
+ const header = `## ${plugin} — ${date} (npm luckiest-co ${npm})`;
45
+ const unreleased = /^## Unreleased$/m;
46
+ fs.writeFileSync(files.changelog, unreleased.test(text.changelog)
47
+ ? text.changelog.replace(unreleased, header)
48
+ : `${header}\n\n${text.changelog}`);
49
+ return { npm, plugin };
50
+ }
51
+
52
+ if (require.main === module) {
53
+ try {
54
+ const { npm, plugin } = bumpVersion();
55
+ console.log(` ${green}Bumped to npm luckiest-co ${npm}, plugin ${plugin}.${reset} Check the notes under the new CHANGELOG.md header.`);
56
+ } catch (err) {
57
+ console.error(` ${err.message}`);
58
+ process.exit(1);
59
+ }
60
+ }
61
+
62
+ module.exports = { bumpVersion };
package/bin/install.js CHANGED
@@ -224,6 +224,58 @@ function placeSkills(extractDir, skillsRoot, fallbackName) {
224
224
  }
225
225
 
226
226
  async function syncSkills() {
227
+ try {
228
+ await syncOwnedSkills();
229
+ } finally {
230
+ // Self-heal the luckiest-x + luckiest:luckiest-x duplicate: the marketplace
231
+ // plugin already registers its skills, so a copy in ~/.claude/skills (from
232
+ // older installers or from this sync) lists every one of them twice.
233
+ const globalDir = expandTilde(explicitConfigDir) || expandTilde(process.env.CLAUDE_CONFIG_DIR) || path.join(os.homedir(), '.claude');
234
+ const removed = dropPluginDuplicates(path.join(globalDir, 'skills'), pluginSkillNames(globalDir));
235
+ if (removed.length) {
236
+ console.log(` ${green}✓${reset} Removed ${removed.length} duplicate skill(s) from skills/ (plugin marketplace already provides them)`);
237
+ }
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Skill folder names shipped by the enabled marketplace plugin, read from its
243
+ * installPath. Empty when the plugin is absent or disabled.
244
+ */
245
+ function pluginSkillNames(globalClaudeDir) {
246
+ const names = new Set();
247
+ if (!hasMarketplacePlugin(globalClaudeDir)) return names;
248
+ try {
249
+ const manifest = JSON.parse(
250
+ fs.readFileSync(path.join(globalClaudeDir, 'plugins', 'installed_plugins.json'), 'utf8')
251
+ );
252
+ for (const [key, value] of Object.entries(manifest.plugins || {})) {
253
+ if (!key.startsWith('luckiest@')) continue;
254
+ for (const item of Array.isArray(value) ? value : [value]) {
255
+ const dir = item && item.installPath && path.join(item.installPath, 'skills');
256
+ if (dir && fs.existsSync(dir)) fs.readdirSync(dir).forEach((n) => names.add(n));
257
+ }
258
+ }
259
+ } catch {
260
+ // Unreadable manifest: remove nothing.
261
+ }
262
+ return names;
263
+ }
264
+
265
+ /** Remove skills/<name> for every name the plugin ships. Returns removed names. */
266
+ function dropPluginDuplicates(skillsRoot, names) {
267
+ const removed = [];
268
+ for (const name of names) {
269
+ const dest = path.join(skillsRoot, name);
270
+ if (fs.existsSync(path.join(dest, 'SKILL.md'))) {
271
+ fs.rmSync(dest, { recursive: true, force: true });
272
+ removed.push(name);
273
+ }
274
+ }
275
+ return removed;
276
+ }
277
+
278
+ async function syncOwnedSkills() {
227
279
  // Self-heal the /luckiest:luckiest:* duplicate: older installers copied
228
280
  // commands into ~/.claude/commands/luckiest even when the marketplace
229
281
  // plugin already registers them. Sync runs every session, so clean it here.
@@ -428,8 +480,12 @@ function install(isGlobal) {
428
480
  }
429
481
 
430
482
  // Copy references/, templates/, .claude-plugin/ into the target root
483
+ // skills/ is skipped when the marketplace plugin ships them, for the same
484
+ // reason as commands/ above (luckiest-x and luckiest:luckiest-x).
485
+ const pluginPresent = hasMarketplacePlugin(defaultGlobalDir);
431
486
  const topLevelDirs = ['references', 'templates', 'skills', '.claude-plugin', 'hooks'];
432
487
  for (const dir of topLevelDirs) {
488
+ if (dir === 'skills' && pluginPresent) continue;
433
489
  const dirSrc = path.join(src, dir);
434
490
  const dirDest = path.join(claudeDir, dir);
435
491
  if (fs.existsSync(dirSrc)) {
@@ -718,4 +774,4 @@ if (require.main === module) {
718
774
  });
719
775
  }
720
776
 
721
- module.exports = { placeSkills };
777
+ module.exports = { placeSkills, dropPluginDuplicates };
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Plugin repo sync: mirrors this package into the public marketplace repo
4
+ // (luckiestco/luckiest-plugin) and opens a PR there. Run after the release is
5
+ // bumped and published. --dry-run stops before anything is pushed.
6
+
7
+ const { execFileSync } = require('child_process');
8
+ const fs = require('fs');
9
+ const os = require('os');
10
+ const path = require('path');
11
+
12
+ const green = '\x1b[32m';
13
+ const yellow = '\x1b[33m';
14
+ const reset = '\x1b[0m';
15
+
16
+ const REPO = process.env.LUCKIEST_PLUGIN_REPO || 'luckiestco/luckiest-plugin';
17
+
18
+ const sh = (cmd, args, cwd) => execFileSync(cmd, args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trimEnd();
19
+ const versionOf = (dir, file) => JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8')).version;
20
+
21
+ /** Returns { status: 'synced' | 'dry-run' | 'pr', dir, changes, url }. */
22
+ function syncPluginRepo({ src = path.join(__dirname, '..'), remote = `https://github.com/${REPO}.git`, dryRun = false } = {}) {
23
+ const plugin = versionOf(src, '.claude-plugin/plugin.json');
24
+ const npm = versionOf(src, 'package.json');
25
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'luckiest-plugin-sync-'));
26
+ sh('git', ['clone', '--depth', '1', remote, dir]);
27
+
28
+ if (versionOf(dir, '.claude-plugin/plugin.json') === plugin) return { status: 'synced', dir, changes: [] };
29
+
30
+ // --checksum: a same-size edit with a matching mtime is otherwise skipped (a version bump is same-size).
31
+ sh('rsync', ['-a', '--checksum', '--delete', '--exclude', '.git', '--exclude', '.DS_Store', '--exclude', '__pycache__', `${src}/`, `${dir}/`]);
32
+ const changes = sh('git', ['status', '--porcelain'], dir).split('\n').filter(Boolean);
33
+ if (dryRun) return { status: 'dry-run', dir, changes };
34
+
35
+ const branch = `sync-${plugin}`;
36
+ const title = `Sync plugin ${plugin} (npm luckiest-co ${npm})`;
37
+ sh('git', ['switch', '-c', branch], dir);
38
+ sh('git', ['add', '-A'], dir);
39
+ sh('git', ['commit', '-m', title], dir);
40
+ sh('git', ['push', '-u', 'origin', branch], dir);
41
+ const url = sh('gh', ['pr', 'create', '--repo', REPO, '--head', branch, '--title', title, '--body', `Mirrors packages/luckiest-plugin at plugin ${plugin}.`], dir);
42
+ return { status: 'pr', dir, changes, url };
43
+ }
44
+
45
+ if (require.main === module) {
46
+ try {
47
+ const { status, dir, changes, url } = syncPluginRepo({ dryRun: process.argv.includes('--dry-run') });
48
+ if (status === 'synced') console.log(` ${green}${REPO} already has this plugin version.${reset}`);
49
+ if (status === 'dry-run') console.log(` ${yellow}Dry run, nothing pushed.${reset} ${changes.length} changed files in ${dir}:\n${changes.map((c) => ` ${c}`).join('\n')}`);
50
+ if (status === 'pr') console.log(` ${green}Opened ${url}${reset}`);
51
+ } catch (err) {
52
+ console.error(` ${(err.stderr || err.message).toString().trim()}`);
53
+ process.exit(1);
54
+ }
55
+ }
56
+
57
+ module.exports = { syncPluginRepo };
package/package.json CHANGED
@@ -1,10 +1,15 @@
1
1
  {
2
2
  "name": "luckiest-co",
3
- "version": "1.0.34",
3
+ "version": "1.0.36",
4
4
  "description": "Luckiest for Claude Code: plan, go, finish. Your luckiest.co skills and tribe, inside Claude.",
5
5
  "bin": {
6
6
  "luckiest-co": "./bin/install.js"
7
7
  },
8
+ "scripts": {
9
+ "prepublishOnly": "node bin/check-version.js",
10
+ "bump": "node bin/bump-version.js",
11
+ "sync-plugin": "node bin/sync-plugin-repo.js"
12
+ },
8
13
  "files": [
9
14
  "bin",
10
15
  "commands",
@@ -15,7 +20,8 @@
15
20
  ".claude-plugin",
16
21
  "!**/__pycache__",
17
22
  "!**/*.pyc",
18
- "!bin/*.test.js"
23
+ "!bin/*.test.js",
24
+ "!bin/check-version.js"
19
25
  ],
20
26
  "license": "SEE LICENSE IN LICENSE",
21
27
  "engines": {
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: luckiest-charms
3
+ user-invocable: false
3
4
  description: "Your Luckiest charms balance and recent related activity. Use when the user says /luckiest charms, luckiest charms, or asks about their charms."
4
5
  ---
5
6
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: luckiest-finish
3
+ user-invocable: false
3
4
  description: "Wrap up a completed Luckiest plan: recap what shipped, close it out, award charms. Use when the user says /luckiest finish, luckiest finish, or all plan tasks are done."
4
5
  ---
5
6
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: luckiest-go
3
+ user-invocable: false
3
4
  description: "Run the active Luckiest plan, one task at a time, checked as it goes. Use when the user says /luckiest go, luckiest go, or wants to continue their staged Luckiest plan."
4
5
  ---
5
6
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: luckiest-helpers
3
+ user-invocable: false
3
4
  description: "See who in your Luckiest tribe needs a hand right now, and claim a request to help. Use when the user says /luckiest helpers, luckiest helpers, or asks who needs help."
4
5
  ---
5
6
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: luckiest-home
3
+ user-invocable: false
3
4
  description: "The Luckiest community dashboard: wishes and charms, plan progress, tribe pulse, and who needs help. Use when the user says /luckiest home, luckiest home, or asks for their Luckiest dashboard."
4
5
  ---
5
6
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: luckiest-leaderboard
3
+ user-invocable: false
3
4
  description: "See how the Luckiest tribe ranks: top scores, one bar chart. Use when the user says /luckiest leaderboard, luckiest leaderboard, or asks for tribe rankings."
4
5
  ---
5
6
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: luckiest-merge
3
+ user-invocable: false
3
4
  description: "Merge two overlapping Luckiest skills into one, after the user reviews the diff. Use when the user says /luckiest merge, luckiest merge, or wants to combine overlapping skills."
4
5
  ---
5
6
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: luckiest-next
3
+ user-invocable: false
3
4
  description: "Do the suggested next step: runs the last Next line from this chat, or picks up the plan where it stands. Use when the user says /next, /luckiest next, luckiest next, or next step."
4
5
  ---
5
6
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: luckiest-overlaps
3
+ user-invocable: false
3
4
  description: "Find installed Luckiest skills that overlap with your own, and resolve them. Use when the user says /luckiest overlaps, luckiest overlaps, or asks about duplicate skills."
4
5
  ---
5
6
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: luckiest-plan
3
+ user-invocable: false
3
4
  description: "Use when the user says /luckiest plan or luckiest plan, or wants to plan a feature, a launch, or their next piece of work before building it, even if they do not say plan (for example \"what should I build next\" or \"break this down into tasks\"). Looks at the project first, drafts 3 to 7 tasks with a suggested skill, model, and runnable check each, saves the context for /luckiest go, and stages the plan after one approval question."
4
5
  ---
5
6
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: luckiest-skills
3
+ user-invocable: false
3
4
  description: "Your Luckiest skills: list what is installed, pull new purchases. Use when the user says /luckiest skills, luckiest skills, or asks what Luckiest skills they have."
4
5
  ---
5
6
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: luckiest-start
3
+ user-invocable: false
3
4
  description: "A short interview about your project that writes your Brand Brief. Use when the user says /luckiest start, luckiest start, or wants to set up Luckiest for a project."
4
5
  ---
5
6
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: luckiest-status
3
+ user-invocable: false
3
4
  description: "Where you are in your Luckiest plan: progress, tasks, and your one next step. Use when the user says /luckiest status, luckiest status, or asks where their plan stands."
4
5
  ---
5
6
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: luckiest-updates
3
+ user-invocable: false
3
4
  description: "Check for updates to your installed Luckiest skills and plugin. Use when the user says /luckiest updates, luckiest updates, or asks if their Luckiest install is current."
4
5
  ---
5
6
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: luckiest-vouch
3
+ user-invocable: false
3
4
  description: "Request or approve a warm intro to someone outside your Luckiest tribe. Use when the user says /luckiest vouch, luckiest vouch, or asks for an intro."
4
5
  ---
5
6
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: luckiest-wishes
3
+ user-invocable: false
3
4
  description: "Your Luckiest wishes balance and recent related activity. Use when the user says /luckiest wishes, luckiest wishes, or asks about their wishes."
4
5
  ---
5
6