@wipcomputer/wip-release 1.9.11 → 1.9.13

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 (3) hide show
  1. package/cli.js +8 -1
  2. package/core.mjs +146 -10
  3. package/package.json +1 -1
package/cli.js CHANGED
@@ -125,6 +125,12 @@ Release notes (highest priority wins, files ALWAYS beat --notes flag):
125
125
  4. --notes="text" Fallback only (use for repos without release notes files)
126
126
  Written notes on disk always take priority over a CLI one-liner.
127
127
 
128
+ Skill publish to website:
129
+ Add .publish-skill.json to repo root: { "name": "my-tool" }
130
+ Set WIP_WEBSITE_REPO env var to your website repo path.
131
+ After release, SKILL.md is copied to {website}/wip.computer/install/{name}.txt
132
+ and deploy.sh is run to push to VPS.
133
+
128
134
  Pipeline:
129
135
  1. Bump package.json version
130
136
  2. Sync SKILL.md version (if exists)
@@ -133,7 +139,8 @@ Pipeline:
133
139
  5. Push to remote
134
140
  6. npm publish (via 1Password)
135
141
  7. GitHub Packages publish
136
- 8. GitHub release create`);
142
+ 8. GitHub release create
143
+ 9. Publish SKILL.md to website (if configured)`);
137
144
  process.exit(level ? 0 : 1);
138
145
  }
139
146
 
package/core.mjs CHANGED
@@ -273,19 +273,23 @@ function checkProductDocs(repoPath) {
273
273
  const aiDir = join(repoPath, 'ai');
274
274
  if (!existsSync(aiDir)) return { missing: [], ok: true, skipped: true };
275
275
 
276
- // 1. Dev update: file from today (or last 3 days)
276
+ // 1. Dev update: must have a file modified since last release tag.
277
+ // Old check ("any file from last 3 days") let the same stale file pass
278
+ // across 11 releases in one session. Now uses the same git-based check
279
+ // as roadmap and readme-first: was the file actually changed since the tag?
277
280
  const devUpdatesDir = join(aiDir, 'dev-updates');
278
281
  if (existsSync(devUpdatesDir)) {
279
- const now = new Date();
280
- const recentDates = [];
281
- for (let i = 0; i < 3; i++) {
282
- const d = new Date(now);
283
- d.setDate(d.getDate() - i);
284
- recentDates.push(d.toISOString().split('T')[0]);
285
- }
286
282
  const files = readdirSync(devUpdatesDir).filter(f => f.endsWith('.md'));
287
- const hasRecent = files.some(f => recentDates.some(d => f.startsWith(d)));
288
- if (!hasRecent) missing.push('ai/dev-updates/ (no dev update from last 3 days)');
283
+ if (files.length === 0) {
284
+ missing.push('ai/dev-updates/ (no dev update files)');
285
+ } else {
286
+ const anyModified = files.some(f =>
287
+ fileModifiedSinceLastTag(repoPath, `ai/dev-updates/${f}`)
288
+ );
289
+ if (!anyModified) {
290
+ missing.push('ai/dev-updates/ (no dev update modified since last release)');
291
+ }
292
+ }
289
293
  }
290
294
 
291
295
  // 2. Roadmap: modified since last tag
@@ -458,6 +462,100 @@ export function publishClawHub(repoPath, newVersion, notes) {
458
462
  return true;
459
463
  }
460
464
 
465
+ // ── Skill Publish ────────────────────────────────────────────────────
466
+
467
+ /**
468
+ * Publish SKILL.md to website as plain text.
469
+ *
470
+ * Auto-detects: if SKILL.md exists and WIP_WEBSITE_REPO is set,
471
+ * publishes automatically. No config file needed.
472
+ *
473
+ * Name resolution (first match wins):
474
+ * 1. .publish-skill.json { "name": "memory-crystal" }
475
+ * 2. SKILL.md frontmatter name: field
476
+ * 3. Directory name (basename of repoPath)
477
+ *
478
+ * Copies SKILL.md to {website}/wip.computer/install/{name}.txt
479
+ * Then runs deploy.sh to push to VPS.
480
+ *
481
+ * Non-blocking: returns result, never throws.
482
+ */
483
+ export function publishSkillToWebsite(repoPath) {
484
+ const websiteRepo = process.env.WIP_WEBSITE_REPO;
485
+ if (!websiteRepo) return { skipped: true, reason: 'WIP_WEBSITE_REPO not set' };
486
+
487
+ // Find SKILL.md: check root, then skills/*/SKILL.md
488
+ let skillFile = join(repoPath, 'SKILL.md');
489
+ if (!existsSync(skillFile)) {
490
+ const skillsDir = join(repoPath, 'skills');
491
+ if (existsSync(skillsDir)) {
492
+ for (const sub of readdirSync(skillsDir)) {
493
+ const candidate = join(skillsDir, sub, 'SKILL.md');
494
+ if (existsSync(candidate)) { skillFile = candidate; break; }
495
+ }
496
+ }
497
+ }
498
+ if (!existsSync(skillFile)) return { skipped: true, reason: 'no SKILL.md found' };
499
+
500
+ // Resolve target name: config > package.json > directory name
501
+ // SKILL.md frontmatter name is skipped because it's a short slug
502
+ // (e.g., "memory") not the full install name (e.g., "memory-crystal").
503
+ let targetName;
504
+
505
+ // 1. Explicit config (optional, overrides auto-detect)
506
+ const configPath = join(repoPath, '.publish-skill.json');
507
+ if (existsSync(configPath)) {
508
+ try {
509
+ const config = JSON.parse(readFileSync(configPath, 'utf8'));
510
+ if (config.name) targetName = config.name;
511
+ } catch {}
512
+ }
513
+
514
+ // 2. package.json name (strip @scope/ prefix, most reliable)
515
+ if (!targetName) {
516
+ const pkgPath = join(repoPath, 'package.json');
517
+ if (existsSync(pkgPath)) {
518
+ try {
519
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
520
+ if (pkg.name) targetName = pkg.name.replace(/^@[^/]+\//, '');
521
+ } catch {}
522
+ }
523
+ }
524
+
525
+ // 3. Directory name fallback (strip -private suffix)
526
+ if (!targetName) {
527
+ targetName = basename(repoPath).replace(/-private$/, '').toLowerCase();
528
+ }
529
+
530
+ // Copy to website install dir
531
+ const installDir = join(websiteRepo, 'wip.computer', 'install');
532
+ if (!existsSync(installDir)) {
533
+ try { mkdirSync(installDir, { recursive: true }); } catch {}
534
+ }
535
+
536
+ const targetFile = join(installDir, `${targetName}.txt`);
537
+ try {
538
+ const content = readFileSync(skillFile, 'utf8');
539
+ writeFileSync(targetFile, content);
540
+ } catch (e) {
541
+ return { ok: false, error: `copy failed: ${e.message}` };
542
+ }
543
+
544
+ // Deploy to VPS (non-blocking ... warn on failure)
545
+ const deployScript = join(websiteRepo, 'deploy.sh');
546
+ if (existsSync(deployScript)) {
547
+ try {
548
+ execSync(`bash deploy.sh`, { cwd: websiteRepo, stdio: 'pipe', timeout: 30000 });
549
+ } catch (e) {
550
+ return { ok: true, deployed: false, target: targetName, error: `deploy failed: ${e.message}` };
551
+ }
552
+ } else {
553
+ return { ok: true, deployed: false, target: targetName, error: 'no deploy.sh found' };
554
+ }
555
+
556
+ return { ok: true, deployed: true, target: targetName };
557
+ }
558
+
461
559
  // ── Helpers ──────────────────────────────────────────────────────────
462
560
 
463
561
  function getNpmToken() {
@@ -745,6 +843,28 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
745
843
  console.log(` [dry run] Would publish to GitHub Packages`);
746
844
  console.log(` [dry run] Would create GitHub release v${newVersion}`);
747
845
  if (hasSkill) console.log(` [dry run] Would publish to ClawHub`);
846
+ // Skill-to-website dry run (auto-detects SKILL.md, no config needed)
847
+ if (hasSkill) {
848
+ const envSet = !!process.env.WIP_WEBSITE_REPO;
849
+ if (envSet) {
850
+ // Resolve name same way as publishSkillToWebsite
851
+ let dryName;
852
+ const publishConfig = join(repoPath, '.publish-skill.json');
853
+ if (existsSync(publishConfig)) {
854
+ try { dryName = JSON.parse(readFileSync(publishConfig, 'utf8')).name; } catch {}
855
+ }
856
+ if (!dryName) {
857
+ const pkgPath = join(repoPath, 'package.json');
858
+ if (existsSync(pkgPath)) {
859
+ try { dryName = JSON.parse(readFileSync(pkgPath, 'utf8')).name?.replace(/^@[^/]+\//, ''); } catch {}
860
+ }
861
+ }
862
+ if (!dryName) dryName = basename(repoPath).replace(/-private$/, '').toLowerCase();
863
+ console.log(` [dry run] Would publish SKILL.md to website: install/${dryName}.txt`);
864
+ } else {
865
+ console.log(` [dry run] Would publish SKILL.md to website but WIP_WEBSITE_REPO not set`);
866
+ }
867
+ }
748
868
  }
749
869
  console.log('');
750
870
  console.log(` Dry run complete. No changes made.`);
@@ -878,6 +998,22 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
878
998
  }
879
999
  }
880
1000
  }
1001
+
1002
+ // 9.5. Publish SKILL.md to website as plain text
1003
+ const skillWebResult = publishSkillToWebsite(repoPath);
1004
+ if (skillWebResult.skipped) {
1005
+ // Silent skip ... no config or env var
1006
+ } else if (skillWebResult.ok) {
1007
+ const deployNote = skillWebResult.deployed ? '' : ' (copied, deploy skipped)';
1008
+ distResults.push({ target: 'Website', status: 'ok', detail: `install/${skillWebResult.target}.txt${deployNote}` });
1009
+ console.log(` ✓ Published to website: install/${skillWebResult.target}.txt${deployNote}`);
1010
+ if (!skillWebResult.deployed && skillWebResult.error) {
1011
+ console.log(` ! ${skillWebResult.error}`);
1012
+ }
1013
+ } else {
1014
+ distResults.push({ target: 'Website', status: 'failed', detail: skillWebResult.error });
1015
+ console.log(` ✗ Website publish failed: ${skillWebResult.error}`);
1016
+ }
881
1017
  }
882
1018
 
883
1019
  // Distribution summary (#104)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-release",
3
- "version": "1.9.11",
3
+ "version": "1.9.13",
4
4
  "type": "module",
5
5
  "description": "One-command release pipeline. Bumps version, updates changelog + SKILL.md, publishes to npm + GitHub.",
6
6
  "main": "core.mjs",