mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 03:35:09 +02:00
chore: remove gstack-publish — no real user need
User feedback: "i don't think i would use gstack-publish, i think we
should remove it." Agreed. The CLI + marketplace wiring was an
ambitious but speculative primitive. Zero users, zero validated demand,
and the existing manual `clawhub publish` workflow already covers the
real case (OpenClaw methodology skill publishing).
Deleted:
- bin/gstack-publish (the CLI)
- skills.json (the marketplace manifest)
- test/publish-dry-run.test.ts (13 tests)
- ship/SKILL.md.tmpl Step 19.5 — the methodology-skill publish-on-ship
check. No target to dispatch to anymore.
- README.md Power tools row for gstack-publish
Updated:
- bin/gstack-model-benchmark doc comment: dropped "matches gstack-publish
--dry-run semantics" reference (self-describing flag now)
- CHANGELOG 1.3.0.0 entry:
* Release summary: "three new binaries" → "two new binaries".
Dropped the /ship publish-check narrative.
* Numbers table: "1 of 3 → 3 of 3 wired" → "1 of 2 → 2 of 2 wired".
Deterministic test count: 45 → 32 (removed publish-dry-run's 13).
* Added section: removed gstack-publish CLI bullet + /ship Step 19.5
bullet.
* "What this means for users" closer: replaced the /ship publish
paragraph with the design-taste-engine learning loop, which IS
real, wired, and something users hit every week via /design-shotgun.
* Contributors section: "Four new test files" → "Three new test files"
Retained:
- openclaw/skills/gstack-openclaw-* skill dirs (pre-existed this PR,
still publishable manually via `clawhub publish`, useful standalone
for ClawHub installs)
- CLAUDE.md publishing-native-skills section (same rationale)
Regenerated SKILL.md across all hosts. Ship golden fixtures refreshed
for claude/codex/factory. 455 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+7
-9
@@ -5,16 +5,16 @@
|
||||
## **Every new CLI wired to a slash command.**
|
||||
## **Zero orphan binaries ship in v1.3.**
|
||||
|
||||
v1.3 ships three new binaries (`gstack-model-benchmark`, `gstack-publish`, `gstack-taste-update`), one new skill (`/benchmark-models`), and a `/ship` Step 19.5 that detects methodology-skill changes and offers to publish them. The delta from v1.2 isn't just "more features." It's that every new primitive is discoverable from a `/command`, not buried in a CHANGELOG bullet nobody reads. Before this cut, `gstack-model-benchmark` and `gstack-publish` existed but no skill called them, so most users would never find them. Now `/benchmark-models` walks you through a cross-model comparison, and `/ship` asks about publishing the moment you touch a methodology skill. First multi-provider benchmark in any agent framework, and it's one slash command away.
|
||||
v1.3 ships two new binaries (`gstack-model-benchmark`, `gstack-taste-update`) and one new skill (`/benchmark-models`). The delta from v1.2 isn't just "more features." It's that every new primitive is discoverable from a `/command`, not buried in a CHANGELOG bullet nobody reads. Before this cut, `gstack-model-benchmark` existed but no skill called it, so most users would never find it. Now `/benchmark-models` walks you through a cross-model comparison, and `gstack-taste-update` is already wired into `/design-shotgun`'s approval flow. First multi-provider benchmark in any agent framework, and it's one slash command away.
|
||||
|
||||
### The numbers that matter
|
||||
|
||||
Headline from this branch's review audit against `origin/main` (32 commits, commit `09466734`). Reproducible: `git log origin/main..HEAD --oneline` for the commit set, `bun test test/taste-engine.test.ts test/publish-dry-run.test.ts test/benchmark-cli.test.ts test/skill-e2e-benchmark-providers.test.ts` for the test count, `grep -rn "gstack-model-benchmark\|gstack-publish\|gstack-taste-update" --include="*.tmpl"` for wiring status.
|
||||
Headline from this branch's review audit against `origin/main` (33 commits). Reproducible: `git log origin/main..HEAD --oneline` for the commit set, `bun test test/taste-engine.test.ts test/benchmark-cli.test.ts test/skill-e2e-benchmark-providers.test.ts` for the test count, `grep -rn "gstack-model-benchmark\|gstack-taste-update" --include="*.tmpl"` for wiring status.
|
||||
|
||||
| Metric | BEFORE (initial v1.2 scope) | AFTER (v1.3) | Δ |
|
||||
|--------------------------------------------------|------------------------------|----------------------|-------------|
|
||||
| **New CLIs wired to a /skill** | 1 of 3 (33%) | **3 of 3 (100%)** | **+2** |
|
||||
| **Deterministic tests for v1.3 CLIs** | 0 | **45** | **+45** |
|
||||
| **New CLIs wired to a /skill** | 1 of 2 (50%) | **2 of 2 (100%)** | **+1** |
|
||||
| **Deterministic tests for v1.3 CLIs** | 0 | **32** | **+32** |
|
||||
| **Live-API adapter E2E (gated on `EVALS=1`)** | 0 | **8** | **+8** |
|
||||
| **Real adapter bugs caught by new tests** | 0 | **1** (codex `--skip-git-repo-check`) | **+1** |
|
||||
| **Preamble composition root** | 740 lines | **~100 lines** | **-86%** |
|
||||
@@ -24,7 +24,7 @@ The single most striking number: the new E2E suite caught a real codex adapter b
|
||||
|
||||
### What this means for gstack users
|
||||
|
||||
If you're a YC founder or solo builder shipping methodology skills from one laptop, `/benchmark-models` answers "is my skill better on Opus, GPT-5.4, or Gemini" with a real benchmark table, not vibes. When you tweak `/office-hours` or `/plan-ceo-review` on a feature branch, `/ship` asks whether to push to ClawHub + SkillsMP + Vercel Skills.sh too, so methodology updates don't die on your main branch. Continuous checkpoint mode (opt-in, local by default) means you can close your laptop mid-refactor and `/context-restore` picks you up from a WIP commit with decisions and remaining work intact, not a stale notes file. Run `/gstack-upgrade` and try `/benchmark-models` on the skill you use most this week.
|
||||
If you're a YC founder or solo builder shipping methodology skills from one laptop, `/benchmark-models` answers "is my skill better on Opus, GPT-5.4, or Gemini" with a real benchmark table, not vibes. The design taste engine remembers which fonts, colors, and aesthetics you approve in `/design-shotgun`, so next round's variants lean toward your actual taste instead of resetting to Inter every time. Continuous checkpoint mode (opt-in, local by default) means you can close your laptop mid-refactor and `/context-restore` picks you up from a WIP commit with decisions and remaining work intact, not a stale notes file. Run `/gstack-upgrade` and try `/benchmark-models` on the skill you use most this week.
|
||||
|
||||
### Itemized changes
|
||||
|
||||
@@ -36,14 +36,12 @@ If you're a YC founder or solo builder shipping methodology skills from one lapt
|
||||
- **Feature discovery prompt after upgrade.** When `JUST_UPGRADED` fires, gstack now offers to enable new features once per user (per-feature marker files at `~/.gstack/.feature-prompted-{name}`). Skipped entirely in spawned sessions (OpenClaw orchestrator). No more silent features that never get discovered.
|
||||
- **Context health soft directive (T2+ skills).** During long-running skills (/qa, /investigate, /cso), gstack now nudges you to write periodic `[PROGRESS]` summaries. Self-monitoring during 50+ tool-call sessions. No fake thresholds — soft directive that the model self-applies. Progress reporting never mutates git state.
|
||||
- **`gstack-model-benchmark` CLI.** Run the same prompt across Claude, GPT, and Gemini, compare latency/tokens/cost/quality. Per-provider auth detection, pricing tables, tool-compatibility map, parallel execution, per-provider error isolation. Quality scoring via Anthropic SDK as the stable judge (`--judge` flag). Output as table, JSON, or markdown. The first multi-provider benchmark in any agent framework — every other tool guesses which model is best, gstack measures it.
|
||||
- **`gstack-publish` CLI for marketplace distribution.** Publishes gstack standalone methodology skills (gstack-office-hours, gstack-ceo-review, gstack-investigate, gstack-retro) to ClawHub, SkillsMP, and Vercel Skills.sh. Supports `--dry-run` (validate manifest + auth without publishing), per-skill error isolation (one failure doesn't abort the batch), idempotent re-runs. New `skills.json` manifest at the repo root declares per-skill marketplace targets and metadata.
|
||||
- **Design taste engine.** Persistent cross-session taste profile at `~/.gstack/projects/$SLUG/taste-profile.json`. Tracks fonts, colors, layouts, and aesthetic directions you approve and reject across sessions. Confidence decays 5% per week. Design-consultation and design-shotgun now factor in your demonstrated preferences. Schema migration handles legacy approved.json. New `gstack-taste-update` CLI updates the profile after design-shotgun decisions.
|
||||
- **Anti-slop design constraints.** Design-consultation now asks "What's the one thing someone will remember?" as a forcing question. Phase 5 self-gate: "Would a human designer be embarrassed by this?" — discards and regenerates if yes. Anti-convergence directive in design-shotgun: each variant must use a different font, palette, and layout, or one of them failed. Space Grotesk added to the overused fonts list (it's the new "safe alternative to Inter" trap). system-ui-as-primary-font added to the AI slop blacklist.
|
||||
- **`gstack-config list` and `gstack-config defaults`** subcommands. `list` shows all config keys with their current value AND source (set/default). `defaults` shows just the defaults table. Fixes the prior gap where `get` returned empty for missing keys instead of falling back to the documented defaults. Telemetry default aligned: header and runtime both say `off` now (previously mismatched).
|
||||
- **`gstack-config checkpoint_mode` and `checkpoint_push` keys.** New config knobs for continuous checkpoint mode. Both default to safe values (`explicit` mode, no auto-push).
|
||||
- **New `/benchmark-models` skill.** Wraps `gstack-model-benchmark` in an interactive flow: pick a prompt (an existing SKILL.md, inline text, or file path), confirm providers (dry-run shows auth status per provider), decide on `--judge` (adds ~$0.05 for quality scoring), run, interpret. Trigger phrases: "compare models", "model shootout", "which model is best". Separate from `/benchmark` (which measures web page performance) — different surface, different domain.
|
||||
- **`/ship` Step 19.5 — methodology skill publish check.** When a PR touches any standalone methodology skill (`openclaw/skills/gstack-*/SKILL.md`) or the `skills.json` manifest, `/ship` runs `gstack-publish --dry-run` after PR creation and asks whether to actually publish. Conditional — silent no-op on PRs that don't touch methodology skills. Eliminates the "shipped to main but never pushed to the marketplace" failure mode.
|
||||
- **`gstack-model-benchmark --dry-run`.** Offline validation mode that matches `gstack-publish --dry-run` semantics. Validates the provider list, resolves per-adapter auth, echoes the resolved flag values, and exits without invoking any provider CLI. Zero-cost pre-flight for CI pipelines and for catching auth drift before starting a paid benchmark run.
|
||||
- **`gstack-model-benchmark --dry-run`.** Offline validation mode. Validates the provider list, resolves per-adapter auth, echoes the resolved flag values, and exits without invoking any provider CLI. Zero-cost pre-flight for CI pipelines and for catching auth drift before starting a paid benchmark run.
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -64,7 +62,7 @@ If you're a YC founder or solo builder shipping methodology skills from one lapt
|
||||
- **`scripts/resolvers/preamble/`** — 18 single-purpose generators, 16-160 lines each. The composition root in `scripts/resolvers/preamble.ts` imports them and wires them into the tier-gated section list.
|
||||
- **Plan and reviews persisted.** Implementation followed `~/.claude/plans/declarative-riding-cook.md` which went through CEO review (SCOPE EXPANSION, 6 expansions accepted), DX review (POLISH, 5 gaps fixed), Eng review (4 architecture issues), and Codex review (11 brutal findings, all integrated and 2 prior decisions reversed).
|
||||
- **Mode-posture energy in Writing Style rules 2-4** (ported from main's v1.1.2.0). Rule 2 and rule 4 now cover three framings — pain reduction, capability unlocked, forcing-question pressure — so expansion, builder, and forcing-question skills keep their edge instead of collapsing into diagnostic-pain framing. Rule 3 adds an explicit exception for stacked forcing questions. Came in via the merge; sits on top of the submodule refactor already shipped in v1.3.
|
||||
- **Lite E2E coverage for v1.3 primitives.** Four new test files fill the real coverage gaps flagged in initial review: `test/taste-engine.test.ts` (24 tests — schema shape, Laplace-smoothed confidence, 5%/week decay clamped at 0, multi-dimension extraction, case-insensitive first-casing-wins policy, session cap via seed-then-one-call, legacy profile migration, taste-drift conflict warning, malformed-JSON recovery), `test/publish-dry-run.test.ts` (13 tests — manifest parsing, missing source file detection, slug filter, per-marketplace auth isolation via fake marketplaces), `test/benchmark-cli.test.ts` (12 tests — CLI flag wiring, provider defaults, unknown-provider WARN path, NOT-READY branch regression catcher that strips auth env vars), `test/skill-e2e-benchmark-providers.test.ts` (8 periodic-tier live-API tests — trivial "echo ok" prompt through claude/codex/gemini adapters, assertions on parsed output + tokens + cost + timeout error codes + Promise.allSettled parallel isolation).
|
||||
- **Lite E2E coverage for v1.3 primitives.** Three new test files fill the real coverage gaps flagged in initial review: `test/taste-engine.test.ts` (24 tests — schema shape, Laplace-smoothed confidence, 5%/week decay clamped at 0, multi-dimension extraction, case-insensitive first-casing-wins policy, session cap via seed-then-one-call, legacy profile migration, taste-drift conflict warning, malformed-JSON recovery), `test/benchmark-cli.test.ts` (12 tests — CLI flag wiring, provider defaults, unknown-provider WARN path, NOT-READY branch regression catcher that strips auth env vars), `test/skill-e2e-benchmark-providers.test.ts` (8 periodic-tier live-API tests — trivial "echo ok" prompt through claude/codex/gemini adapters, assertions on parsed output + tokens + cost + timeout error codes + Promise.allSettled parallel isolation).
|
||||
- **Ship golden fixtures for three hosts.** `test/fixtures/golden/{claude,codex,factory}-ship-SKILL.md` — byte-exact regression pins on the `/ship` generated output. The adversarial subagent pass during /review caught two real bugs before merge: Geist/GEIST casing policy in the taste engine was unpinned, and the live-E2E workdir was created at module load and never cleaned up.
|
||||
|
||||
## [1.1.3.0] - 2026-04-19
|
||||
|
||||
@@ -234,7 +234,6 @@ Beyond the slash-command skills, gstack ships standalone CLIs for workflows that
|
||||
| Command | What it does |
|
||||
|---------|-------------|
|
||||
| `gstack-model-benchmark` | **Cross-model benchmark** — run the same prompt through Claude, GPT (via Codex CLI), and Gemini; compare latency, tokens, cost, and (optionally) LLM-judge quality score. Auth detected per provider, unavailable providers skip cleanly. Output as table, JSON, or markdown. `--dry-run` validates flags + auth without spending API calls. |
|
||||
| `gstack-publish` | **Marketplace distribution** — publishes standalone methodology skills (office-hours, ceo-review, investigate, retro) to ClawHub, SkillsMP, and Vercel Skills.sh. Manifest at `skills.json`. `--dry-run` validates everything without actually publishing. Per-skill, per-marketplace error isolation — one failure never aborts the batch. |
|
||||
| `gstack-taste-update` | **Design taste learning** — writes approvals and rejections from `/design-shotgun` into a persistent per-project taste profile. Decays 5%/week. Feeds back into future variant generation so the system learns what you actually pick. |
|
||||
|
||||
### Continuous checkpoint mode (opt-in, local by default)
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
* --judge Run Anthropic SDK judge on outputs for quality score
|
||||
* (requires ANTHROPIC_API_KEY; adds ~$0.05 per call)
|
||||
* --dry-run Validate flags + resolve auth, don't invoke providers
|
||||
* (matches gstack-publish --dry-run semantics)
|
||||
*
|
||||
* Examples:
|
||||
* gstack-model-benchmark --prompt "Write a haiku about databases" --models claude,gpt
|
||||
|
||||
@@ -1,264 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* gstack-publish — publish gstack standalone methodology skills to
|
||||
* configured marketplaces (ClawHub, SkillsMP, Vercel Skills.sh).
|
||||
*
|
||||
* Usage:
|
||||
* gstack-publish Publish all skills listed in skills.json
|
||||
* gstack-publish <slug> Publish a single skill by slug
|
||||
* gstack-publish --dry-run Validate manifest + resolve auth, don't publish
|
||||
* gstack-publish --list List skills + marketplace targets
|
||||
* gstack-publish --changelog "description"
|
||||
* Pass a changelog message (required by some CLIs)
|
||||
*
|
||||
* Per-skill error isolation: if publishing to one marketplace fails for one skill,
|
||||
* the batch continues. Aggregate failures are reported at the end with exit 1.
|
||||
*
|
||||
* Idempotent: publishing the same version twice is a no-op for marketplaces that
|
||||
* reject duplicate versions; for others, gstack-publish bumps the version check
|
||||
* via their auth_check command and reports if the version already exists.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { execSync, spawnSync } from 'child_process';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
const MANIFEST_PATH = path.join(ROOT, 'skills.json');
|
||||
|
||||
interface SkillEntry {
|
||||
slug: string;
|
||||
source: string;
|
||||
name: string;
|
||||
version: string;
|
||||
category: string;
|
||||
description: string;
|
||||
marketplaces: Record<string, { slug: string; publish: boolean }>;
|
||||
standalone: boolean;
|
||||
compatible_hosts: string[];
|
||||
}
|
||||
|
||||
interface MarketplaceConfig {
|
||||
cli: string;
|
||||
login_cmd: string;
|
||||
publish_cmd_template: string;
|
||||
docs: string;
|
||||
auth_check: string;
|
||||
}
|
||||
|
||||
interface Manifest {
|
||||
version: string;
|
||||
description: string;
|
||||
skills: SkillEntry[];
|
||||
marketplaces: Record<string, MarketplaceConfig>;
|
||||
}
|
||||
|
||||
function arg(name: string, def?: string): string | undefined {
|
||||
const idx = process.argv.findIndex(a => a === name || a.startsWith(name + '='));
|
||||
if (idx < 0) return def;
|
||||
const eqIdx = process.argv[idx].indexOf('=');
|
||||
if (eqIdx >= 0) return process.argv[idx].slice(eqIdx + 1);
|
||||
return process.argv[idx + 1];
|
||||
}
|
||||
|
||||
function flag(name: string): boolean {
|
||||
return process.argv.includes(name);
|
||||
}
|
||||
|
||||
function loadManifest(): Manifest {
|
||||
if (!fs.existsSync(MANIFEST_PATH)) {
|
||||
console.error(`ERROR: ${MANIFEST_PATH} not found.`);
|
||||
process.exit(1);
|
||||
}
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf-8'));
|
||||
} catch (err) {
|
||||
console.error(`ERROR: could not parse ${MANIFEST_PATH}:`, (err as Error).message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function validateSkill(skill: SkillEntry): string[] {
|
||||
const errs: string[] = [];
|
||||
if (!skill.slug) errs.push('missing slug');
|
||||
if (!skill.version) errs.push('missing version');
|
||||
if (!skill.source) errs.push('missing source');
|
||||
if (skill.source) {
|
||||
const fullPath = path.join(ROOT, skill.source);
|
||||
if (!fs.existsSync(fullPath)) errs.push(`source file missing: ${skill.source}`);
|
||||
}
|
||||
if (!Object.keys(skill.marketplaces || {}).length) errs.push('no marketplaces configured');
|
||||
return errs;
|
||||
}
|
||||
|
||||
function checkAuth(marketplace: string, cfg: MarketplaceConfig): { ok: boolean; reason?: string } {
|
||||
// Check binary exists
|
||||
const binCheck = spawnSync('sh', ['-c', `command -v ${cfg.cli}`], { timeout: 2000 });
|
||||
if (binCheck.status !== 0) {
|
||||
return { ok: false, reason: `${cfg.cli} CLI not on PATH. Docs: ${cfg.docs}` };
|
||||
}
|
||||
// Run auth check. Success = ok. Any non-zero = not authenticated.
|
||||
const authCheck = spawnSync('sh', ['-c', cfg.auth_check], { timeout: 5000 });
|
||||
if (authCheck.status !== 0) {
|
||||
return { ok: false, reason: `not authenticated. Run: ${cfg.login_cmd}` };
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
interface PublishResult {
|
||||
skill: string;
|
||||
marketplace: string;
|
||||
ok: boolean;
|
||||
reason?: string;
|
||||
cmd?: string;
|
||||
}
|
||||
|
||||
function buildCommand(template: string, subs: Record<string, string>): string {
|
||||
return template.replace(/\{(\w+)\}/g, (_, key) => subs[key] ?? `{${key}}`);
|
||||
}
|
||||
|
||||
function publishOne(skill: SkillEntry, marketplace: string, cfg: MarketplaceConfig, changelog: string, dryRun: boolean): PublishResult {
|
||||
const sourceDir = path.dirname(path.join(ROOT, skill.source));
|
||||
const target = skill.marketplaces[marketplace];
|
||||
if (!target || !target.publish) {
|
||||
return { skill: skill.slug, marketplace, ok: true, reason: 'not configured for this marketplace' };
|
||||
}
|
||||
const cmd = buildCommand(cfg.publish_cmd_template, {
|
||||
source_dir: sourceDir,
|
||||
slug: target.slug,
|
||||
name: skill.name,
|
||||
version: skill.version,
|
||||
changelog,
|
||||
});
|
||||
if (dryRun) {
|
||||
return { skill: skill.slug, marketplace, ok: true, reason: 'DRY-RUN (not executed)', cmd };
|
||||
}
|
||||
try {
|
||||
const out = execSync(cmd, { stdio: 'pipe', timeout: 120_000, encoding: 'utf-8' });
|
||||
return { skill: skill.slug, marketplace, ok: true, cmd, reason: out.slice(0, 200) };
|
||||
} catch (err: unknown) {
|
||||
const e = err as { stderr?: Buffer; message?: string };
|
||||
const reason = e.stderr?.toString()?.slice(0, 400) ?? e.message ?? 'unknown error';
|
||||
return { skill: skill.slug, marketplace, ok: false, cmd, reason };
|
||||
}
|
||||
}
|
||||
|
||||
function cmdList(m: Manifest): void {
|
||||
console.log(`gstack-publish manifest v${m.version}: ${m.skills.length} skills\n`);
|
||||
for (const s of m.skills) {
|
||||
console.log(` ${s.slug} v${s.version} (${s.category})`);
|
||||
console.log(` source: ${s.source}`);
|
||||
const targets = Object.entries(s.marketplaces).filter(([, v]) => v.publish).map(([k]) => k);
|
||||
console.log(` publish to: ${targets.join(', ') || '(none)'}`);
|
||||
console.log();
|
||||
}
|
||||
console.log('Configured marketplaces:');
|
||||
for (const [name, cfg] of Object.entries(m.marketplaces)) {
|
||||
console.log(` ${name}: ${cfg.cli} (docs: ${cfg.docs})`);
|
||||
}
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const manifest = loadManifest();
|
||||
|
||||
if (flag('--list')) {
|
||||
cmdList(manifest);
|
||||
return;
|
||||
}
|
||||
|
||||
const dryRun = flag('--dry-run');
|
||||
const changelog = arg('--changelog') ?? `gstack-publish ${new Date().toISOString().slice(0, 10)}`;
|
||||
const targetSlug = process.argv.slice(2).find(a => !a.startsWith('--'));
|
||||
|
||||
// 1. Validate manifest entries
|
||||
console.log(`== Validating manifest (${manifest.skills.length} skills) ==`);
|
||||
const validationErrors: Array<{ slug: string; errors: string[] }> = [];
|
||||
for (const s of manifest.skills) {
|
||||
const errs = validateSkill(s);
|
||||
if (errs.length) validationErrors.push({ slug: s.slug, errors: errs });
|
||||
}
|
||||
if (validationErrors.length) {
|
||||
console.error('Manifest validation failed:');
|
||||
for (const { slug, errors } of validationErrors) {
|
||||
console.error(` ${slug}: ${errors.join(', ')}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(' OK\n');
|
||||
|
||||
// 2. Filter skills (single slug or all)
|
||||
const skills = targetSlug
|
||||
? manifest.skills.filter(s => s.slug === targetSlug)
|
||||
: manifest.skills;
|
||||
if (!skills.length) {
|
||||
console.error(`ERROR: skill not found: ${targetSlug}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 3. Check marketplace auth (only for marketplaces we'll actually use)
|
||||
const activeMarketplaces = new Set<string>();
|
||||
for (const s of skills) {
|
||||
for (const [name, cfg] of Object.entries(s.marketplaces)) {
|
||||
if (cfg.publish) activeMarketplaces.add(name);
|
||||
}
|
||||
}
|
||||
console.log(`== Checking auth for ${activeMarketplaces.size} marketplace(s) ==`);
|
||||
const authStatus: Record<string, { ok: boolean; reason?: string }> = {};
|
||||
for (const mp of activeMarketplaces) {
|
||||
const cfg = manifest.marketplaces[mp];
|
||||
if (!cfg) {
|
||||
authStatus[mp] = { ok: false, reason: `no config for marketplace ${mp}` };
|
||||
continue;
|
||||
}
|
||||
const res = checkAuth(mp, cfg);
|
||||
authStatus[mp] = res;
|
||||
console.log(` ${mp}: ${res.ok ? 'OK' : 'NOT READY — ' + res.reason}`);
|
||||
}
|
||||
console.log();
|
||||
|
||||
// 4. Publish
|
||||
const results: PublishResult[] = [];
|
||||
for (const skill of skills) {
|
||||
console.log(`== Publishing ${skill.slug} v${skill.version} ==`);
|
||||
for (const [mp, target] of Object.entries(skill.marketplaces)) {
|
||||
if (!target.publish) continue;
|
||||
if (!authStatus[mp]?.ok && !dryRun) {
|
||||
results.push({ skill: skill.slug, marketplace: mp, ok: false, reason: `auth not ready: ${authStatus[mp]?.reason}` });
|
||||
console.log(` ${mp}: SKIPPED (auth not ready)`);
|
||||
continue;
|
||||
}
|
||||
const cfg = manifest.marketplaces[mp];
|
||||
if (!cfg) {
|
||||
results.push({ skill: skill.slug, marketplace: mp, ok: false, reason: 'no marketplace config' });
|
||||
continue;
|
||||
}
|
||||
const r = publishOne(skill, mp, cfg, changelog, dryRun);
|
||||
results.push(r);
|
||||
const tag = r.ok ? (dryRun ? 'DRY-RUN' : 'OK') : 'FAIL';
|
||||
console.log(` ${mp}: ${tag}${r.reason ? ' — ' + r.reason.slice(0, 80) : ''}`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
// 5. Summary
|
||||
const successes = results.filter(r => r.ok).length;
|
||||
const failures = results.filter(r => !r.ok);
|
||||
console.log(`== Summary ==`);
|
||||
console.log(` Published: ${successes}`);
|
||||
console.log(` Failed: ${failures.length}`);
|
||||
if (failures.length) {
|
||||
console.log();
|
||||
for (const f of failures) {
|
||||
console.log(` ${f.skill} → ${f.marketplace}: ${f.reason}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
if (dryRun) {
|
||||
console.log(`\n(--dry-run — no actual publish happened)`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('FATAL:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
+1
-32
@@ -2930,38 +2930,7 @@ EOF
|
||||
**If neither CLI is available:**
|
||||
Print the branch name, remote URL, and instruct the user to create the PR/MR manually via the web UI. Do not stop — the code is pushed and ready.
|
||||
|
||||
**Output the PR/MR URL** — then proceed to Step 19.5.
|
||||
|
||||
---
|
||||
|
||||
## Step 19.5: Offer methodology skill publishing (conditional)
|
||||
|
||||
If this PR touched any standalone methodology skill (`openclaw/skills/gstack-*/SKILL.md`) or the marketplace manifest (`skills.json`), offer to publish the updated skills to the configured marketplaces after merge.
|
||||
|
||||
```bash
|
||||
git diff origin/<base>...HEAD --name-only | grep -E '^(openclaw/skills/gstack-[^/]+/SKILL\.md|skills\.json)$' || true
|
||||
```
|
||||
|
||||
If the output is empty: skip this step silently. Continue to Step 20.
|
||||
|
||||
If any matches: run a dry-run preview so the user sees exactly what would publish and what auth is missing.
|
||||
|
||||
```bash
|
||||
~/.claude/skills/gstack/bin/gstack-publish --dry-run
|
||||
```
|
||||
|
||||
Then use AskUserQuestion with the structure in the preamble:
|
||||
- **Re-ground:** project, branch, PR just opened.
|
||||
- **Simplify:** "This PR updated methodology skills. gstack-publish can push them to ClawHub, SkillsMP, and Vercel Skills.sh so other developers on other agents can install them. Dry-run above shows what would ship and whether each marketplace is authed."
|
||||
- **RECOMMENDATION:** A because the dry-run just verified the manifest and one-command distribution is the whole point of having a marketplace publisher.
|
||||
- **Options:**
|
||||
- A) Publish now — run `gstack-publish` (human: ~2min / CC: ~30s). Completeness: 9/10.
|
||||
- B) Publish after the PR merges — `/document-release` won't push this automatically; you'll run `gstack-publish` manually once the branch is on main. Completeness: 7/10.
|
||||
- C) Skip — don't publish this release. Completeness: 4/10.
|
||||
|
||||
If A: run `~/.claude/skills/gstack/bin/gstack-publish` (no --dry-run). Report success/failure per marketplace. If any marketplace auth is missing, the dry-run above surfaced it; the real publish will skip that marketplace with an isolated error rather than aborting the batch.
|
||||
|
||||
If B or C: continue to Step 20.
|
||||
**Output the PR/MR URL** — then proceed to Step 20.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+1
-32
@@ -854,38 +854,7 @@ EOF
|
||||
**If neither CLI is available:**
|
||||
Print the branch name, remote URL, and instruct the user to create the PR/MR manually via the web UI. Do not stop — the code is pushed and ready.
|
||||
|
||||
**Output the PR/MR URL** — then proceed to Step 19.5.
|
||||
|
||||
---
|
||||
|
||||
## Step 19.5: Offer methodology skill publishing (conditional)
|
||||
|
||||
If this PR touched any standalone methodology skill (`openclaw/skills/gstack-*/SKILL.md`) or the marketplace manifest (`skills.json`), offer to publish the updated skills to the configured marketplaces after merge.
|
||||
|
||||
```bash
|
||||
git diff origin/<base>...HEAD --name-only | grep -E '^(openclaw/skills/gstack-[^/]+/SKILL\.md|skills\.json)$' || true
|
||||
```
|
||||
|
||||
If the output is empty: skip this step silently. Continue to Step 20.
|
||||
|
||||
If any matches: run a dry-run preview so the user sees exactly what would publish and what auth is missing.
|
||||
|
||||
```bash
|
||||
~/.claude/skills/gstack/bin/gstack-publish --dry-run
|
||||
```
|
||||
|
||||
Then use AskUserQuestion with the structure in the preamble:
|
||||
- **Re-ground:** project, branch, PR just opened.
|
||||
- **Simplify:** "This PR updated methodology skills. gstack-publish can push them to ClawHub, SkillsMP, and Vercel Skills.sh so other developers on other agents can install them. Dry-run above shows what would ship and whether each marketplace is authed."
|
||||
- **RECOMMENDATION:** A because the dry-run just verified the manifest and one-command distribution is the whole point of having a marketplace publisher.
|
||||
- **Options:**
|
||||
- A) Publish now — run `gstack-publish` (human: ~2min / CC: ~30s). Completeness: 9/10.
|
||||
- B) Publish after the PR merges — `/document-release` won't push this automatically; you'll run `gstack-publish` manually once the branch is on main. Completeness: 7/10.
|
||||
- C) Skip — don't publish this release. Completeness: 4/10.
|
||||
|
||||
If A: run `~/.claude/skills/gstack/bin/gstack-publish` (no --dry-run). Report success/failure per marketplace. If any marketplace auth is missing, the dry-run above surfaced it; the real publish will skip that marketplace with an isolated error rather than aborting the batch.
|
||||
|
||||
If B or C: continue to Step 20.
|
||||
**Output the PR/MR URL** — then proceed to Step 20.
|
||||
|
||||
---
|
||||
|
||||
|
||||
-90
@@ -1,90 +0,0 @@
|
||||
{
|
||||
"$schema": "https://gstack.dev/schemas/skills-manifest.v1.json",
|
||||
"version": "1.0.0",
|
||||
"description": "Manifest for publishing gstack standalone methodology skills to marketplaces (ClawHub, Vercel Skills.sh, SkillsMP). These skills have NO gstack runtime dependencies — they work as pure Claude Code / OpenClaw skills.",
|
||||
"skills": [
|
||||
{
|
||||
"slug": "gstack-office-hours",
|
||||
"source": "openclaw/skills/gstack-openclaw-office-hours/SKILL.md",
|
||||
"name": "YC Office Hours",
|
||||
"version": "1.0.0",
|
||||
"category": "strategy",
|
||||
"description": "Product interrogation with six forcing questions. Two modes: startup diagnostic and builder brainstorm. Produces a design doc, not code.",
|
||||
"marketplaces": {
|
||||
"clawhub": { "slug": "gstack-office-hours", "publish": true },
|
||||
"skillsmp": { "slug": "gstack-office-hours", "publish": true },
|
||||
"vercel": { "slug": "gstack-office-hours", "publish": true }
|
||||
},
|
||||
"standalone": true,
|
||||
"compatible_hosts": ["claude-code", "openclaw", "cursor"]
|
||||
},
|
||||
{
|
||||
"slug": "gstack-ceo-review",
|
||||
"source": "openclaw/skills/gstack-openclaw-ceo-review/SKILL.md",
|
||||
"name": "CEO Plan Review",
|
||||
"version": "1.0.0",
|
||||
"category": "strategy",
|
||||
"description": "Founder-mode plan review. Rethink the problem, find the 10x product, challenge premises, expand scope when it creates a better product.",
|
||||
"marketplaces": {
|
||||
"clawhub": { "slug": "gstack-ceo-review", "publish": true },
|
||||
"skillsmp": { "slug": "gstack-ceo-review", "publish": true },
|
||||
"vercel": { "slug": "gstack-ceo-review", "publish": true }
|
||||
},
|
||||
"standalone": true,
|
||||
"compatible_hosts": ["claude-code", "openclaw", "cursor"]
|
||||
},
|
||||
{
|
||||
"slug": "gstack-investigate",
|
||||
"source": "openclaw/skills/gstack-openclaw-investigate/SKILL.md",
|
||||
"name": "Root Cause Investigation",
|
||||
"version": "1.0.0",
|
||||
"category": "debugging",
|
||||
"description": "Systematic debugging with root cause investigation. Four phases: investigate, analyze, hypothesize, implement. Iron Law: no fixes without root cause.",
|
||||
"marketplaces": {
|
||||
"clawhub": { "slug": "gstack-investigate", "publish": true },
|
||||
"skillsmp": { "slug": "gstack-investigate", "publish": true },
|
||||
"vercel": { "slug": "gstack-investigate", "publish": true }
|
||||
},
|
||||
"standalone": true,
|
||||
"compatible_hosts": ["claude-code", "openclaw", "cursor"]
|
||||
},
|
||||
{
|
||||
"slug": "gstack-retro",
|
||||
"source": "openclaw/skills/gstack-openclaw-retro/SKILL.md",
|
||||
"name": "Engineering Retrospective",
|
||||
"version": "1.0.0",
|
||||
"category": "process",
|
||||
"description": "Weekly engineering retrospective. Analyzes commit history and work patterns with per-person breakdown.",
|
||||
"marketplaces": {
|
||||
"clawhub": { "slug": "gstack-retro", "publish": true },
|
||||
"skillsmp": { "slug": "gstack-retro", "publish": true },
|
||||
"vercel": { "slug": "gstack-retro", "publish": true }
|
||||
},
|
||||
"standalone": true,
|
||||
"compatible_hosts": ["claude-code", "openclaw", "cursor"]
|
||||
}
|
||||
],
|
||||
"marketplaces": {
|
||||
"clawhub": {
|
||||
"cli": "clawhub",
|
||||
"login_cmd": "clawhub login",
|
||||
"publish_cmd_template": "clawhub publish {source_dir} --slug {slug} --name \"{name}\" --version {version} --changelog \"{changelog}\"",
|
||||
"docs": "https://clawhub.dev/docs/publishing",
|
||||
"auth_check": "clawhub whoami"
|
||||
},
|
||||
"skillsmp": {
|
||||
"cli": "skillsmp",
|
||||
"login_cmd": "skillsmp login",
|
||||
"publish_cmd_template": "skillsmp publish {source_dir} --name {slug} --version {version}",
|
||||
"docs": "https://skillsmp.com/docs/publish",
|
||||
"auth_check": "skillsmp whoami"
|
||||
},
|
||||
"vercel": {
|
||||
"cli": "skills",
|
||||
"login_cmd": "skills login",
|
||||
"publish_cmd_template": "skills publish {source_dir} --package {slug} --version {version}",
|
||||
"docs": "https://skills.sh/docs",
|
||||
"auth_check": "skills whoami"
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-32
@@ -2930,38 +2930,7 @@ EOF
|
||||
**If neither CLI is available:**
|
||||
Print the branch name, remote URL, and instruct the user to create the PR/MR manually via the web UI. Do not stop — the code is pushed and ready.
|
||||
|
||||
**Output the PR/MR URL** — then proceed to Step 19.5.
|
||||
|
||||
---
|
||||
|
||||
## Step 19.5: Offer methodology skill publishing (conditional)
|
||||
|
||||
If this PR touched any standalone methodology skill (`openclaw/skills/gstack-*/SKILL.md`) or the marketplace manifest (`skills.json`), offer to publish the updated skills to the configured marketplaces after merge.
|
||||
|
||||
```bash
|
||||
git diff origin/<base>...HEAD --name-only | grep -E '^(openclaw/skills/gstack-[^/]+/SKILL\.md|skills\.json)$' || true
|
||||
```
|
||||
|
||||
If the output is empty: skip this step silently. Continue to Step 20.
|
||||
|
||||
If any matches: run a dry-run preview so the user sees exactly what would publish and what auth is missing.
|
||||
|
||||
```bash
|
||||
~/.claude/skills/gstack/bin/gstack-publish --dry-run
|
||||
```
|
||||
|
||||
Then use AskUserQuestion with the structure in the preamble:
|
||||
- **Re-ground:** project, branch, PR just opened.
|
||||
- **Simplify:** "This PR updated methodology skills. gstack-publish can push them to ClawHub, SkillsMP, and Vercel Skills.sh so other developers on other agents can install them. Dry-run above shows what would ship and whether each marketplace is authed."
|
||||
- **RECOMMENDATION:** A because the dry-run just verified the manifest and one-command distribution is the whole point of having a marketplace publisher.
|
||||
- **Options:**
|
||||
- A) Publish now — run `gstack-publish` (human: ~2min / CC: ~30s). Completeness: 9/10.
|
||||
- B) Publish after the PR merges — `/document-release` won't push this automatically; you'll run `gstack-publish` manually once the branch is on main. Completeness: 7/10.
|
||||
- C) Skip — don't publish this release. Completeness: 4/10.
|
||||
|
||||
If A: run `~/.claude/skills/gstack/bin/gstack-publish` (no --dry-run). Report success/failure per marketplace. If any marketplace auth is missing, the dry-run above surfaced it; the real publish will skip that marketplace with an isolated error rather than aborting the batch.
|
||||
|
||||
If B or C: continue to Step 20.
|
||||
**Output the PR/MR URL** — then proceed to Step 20.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+1
-32
@@ -2545,38 +2545,7 @@ EOF
|
||||
**If neither CLI is available:**
|
||||
Print the branch name, remote URL, and instruct the user to create the PR/MR manually via the web UI. Do not stop — the code is pushed and ready.
|
||||
|
||||
**Output the PR/MR URL** — then proceed to Step 19.5.
|
||||
|
||||
---
|
||||
|
||||
## Step 19.5: Offer methodology skill publishing (conditional)
|
||||
|
||||
If this PR touched any standalone methodology skill (`openclaw/skills/gstack-*/SKILL.md`) or the marketplace manifest (`skills.json`), offer to publish the updated skills to the configured marketplaces after merge.
|
||||
|
||||
```bash
|
||||
git diff origin/<base>...HEAD --name-only | grep -E '^(openclaw/skills/gstack-[^/]+/SKILL\.md|skills\.json)$' || true
|
||||
```
|
||||
|
||||
If the output is empty: skip this step silently. Continue to Step 20.
|
||||
|
||||
If any matches: run a dry-run preview so the user sees exactly what would publish and what auth is missing.
|
||||
|
||||
```bash
|
||||
$GSTACK_ROOT/bin/gstack-publish --dry-run
|
||||
```
|
||||
|
||||
Then use AskUserQuestion with the structure in the preamble:
|
||||
- **Re-ground:** project, branch, PR just opened.
|
||||
- **Simplify:** "This PR updated methodology skills. gstack-publish can push them to ClawHub, SkillsMP, and Vercel Skills.sh so other developers on other agents can install them. Dry-run above shows what would ship and whether each marketplace is authed."
|
||||
- **RECOMMENDATION:** A because the dry-run just verified the manifest and one-command distribution is the whole point of having a marketplace publisher.
|
||||
- **Options:**
|
||||
- A) Publish now — run `gstack-publish` (human: ~2min / CC: ~30s). Completeness: 9/10.
|
||||
- B) Publish after the PR merges — `/document-release` won't push this automatically; you'll run `gstack-publish` manually once the branch is on main. Completeness: 7/10.
|
||||
- C) Skip — don't publish this release. Completeness: 4/10.
|
||||
|
||||
If A: run `$GSTACK_ROOT/bin/gstack-publish` (no --dry-run). Report success/failure per marketplace. If any marketplace auth is missing, the dry-run above surfaced it; the real publish will skip that marketplace with an isolated error rather than aborting the batch.
|
||||
|
||||
If B or C: continue to Step 20.
|
||||
**Output the PR/MR URL** — then proceed to Step 20.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+1
-32
@@ -2921,38 +2921,7 @@ EOF
|
||||
**If neither CLI is available:**
|
||||
Print the branch name, remote URL, and instruct the user to create the PR/MR manually via the web UI. Do not stop — the code is pushed and ready.
|
||||
|
||||
**Output the PR/MR URL** — then proceed to Step 19.5.
|
||||
|
||||
---
|
||||
|
||||
## Step 19.5: Offer methodology skill publishing (conditional)
|
||||
|
||||
If this PR touched any standalone methodology skill (`openclaw/skills/gstack-*/SKILL.md`) or the marketplace manifest (`skills.json`), offer to publish the updated skills to the configured marketplaces after merge.
|
||||
|
||||
```bash
|
||||
git diff origin/<base>...HEAD --name-only | grep -E '^(openclaw/skills/gstack-[^/]+/SKILL\.md|skills\.json)$' || true
|
||||
```
|
||||
|
||||
If the output is empty: skip this step silently. Continue to Step 20.
|
||||
|
||||
If any matches: run a dry-run preview so the user sees exactly what would publish and what auth is missing.
|
||||
|
||||
```bash
|
||||
$GSTACK_ROOT/bin/gstack-publish --dry-run
|
||||
```
|
||||
|
||||
Then use AskUserQuestion with the structure in the preamble:
|
||||
- **Re-ground:** project, branch, PR just opened.
|
||||
- **Simplify:** "This PR updated methodology skills. gstack-publish can push them to ClawHub, SkillsMP, and Vercel Skills.sh so other developers on other agents can install them. Dry-run above shows what would ship and whether each marketplace is authed."
|
||||
- **RECOMMENDATION:** A because the dry-run just verified the manifest and one-command distribution is the whole point of having a marketplace publisher.
|
||||
- **Options:**
|
||||
- A) Publish now — run `gstack-publish` (human: ~2min / CC: ~30s). Completeness: 9/10.
|
||||
- B) Publish after the PR merges — `/document-release` won't push this automatically; you'll run `gstack-publish` manually once the branch is on main. Completeness: 7/10.
|
||||
- C) Skip — don't publish this release. Completeness: 4/10.
|
||||
|
||||
If A: run `$GSTACK_ROOT/bin/gstack-publish` (no --dry-run). Report success/failure per marketplace. If any marketplace auth is missing, the dry-run above surfaced it; the real publish will skip that marketplace with an isolated error rather than aborting the batch.
|
||||
|
||||
If B or C: continue to Step 20.
|
||||
**Output the PR/MR URL** — then proceed to Step 20.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,280 +0,0 @@
|
||||
/**
|
||||
* gstack-publish end-to-end tests via --dry-run.
|
||||
*
|
||||
* Verifies manifest parsing, schema validation, marketplace auth checks, per-skill
|
||||
* error isolation, and command building — all without touching real marketplaces.
|
||||
*
|
||||
* --dry-run does NOT run execSync on publish commands. Auth checks still run
|
||||
* against real binaries; we use fake marketplaces whose `auth_check` commands
|
||||
* are always-succeed (`true`) or always-fail (`false`) so the test is hermetic.
|
||||
*/
|
||||
|
||||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import { spawnSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
const BIN = path.join(ROOT, 'bin', 'gstack-publish');
|
||||
|
||||
let sandbox: string;
|
||||
let binCopy: string;
|
||||
|
||||
beforeEach(() => {
|
||||
// gstack-publish reads skills.json relative to the binary's dir (import.meta.dir/..).
|
||||
// To isolate each test's manifest, we create a sandbox repo that mirrors the real
|
||||
// structure: copy the bin into sandbox/bin/, write a controlled skills.json at the root.
|
||||
sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'publish-sandbox-'));
|
||||
fs.mkdirSync(path.join(sandbox, 'bin'));
|
||||
binCopy = path.join(sandbox, 'bin', 'gstack-publish');
|
||||
fs.copyFileSync(BIN, binCopy);
|
||||
fs.chmodSync(binCopy, 0o755);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(sandbox, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function writeManifest(manifest: object): void {
|
||||
fs.writeFileSync(path.join(sandbox, 'skills.json'), JSON.stringify(manifest, null, 2));
|
||||
}
|
||||
|
||||
function writeSkillFile(relPath: string, content = '# Test Skill\n'): void {
|
||||
const full = path.join(sandbox, relPath);
|
||||
fs.mkdirSync(path.dirname(full), { recursive: true });
|
||||
fs.writeFileSync(full, content);
|
||||
}
|
||||
|
||||
function run(args: string[]): { status: number | null; stdout: string; stderr: string } {
|
||||
const result = spawnSync('bun', ['run', binCopy, ...args], {
|
||||
cwd: sandbox,
|
||||
encoding: 'utf-8',
|
||||
timeout: 15000,
|
||||
});
|
||||
return {
|
||||
status: result.status,
|
||||
stdout: result.stdout?.toString() ?? '',
|
||||
stderr: result.stderr?.toString() ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
const VALID_MARKETPLACES = {
|
||||
fakestore_ok: {
|
||||
cli: 'true', // binary that always succeeds
|
||||
login_cmd: 'fakestore_ok login',
|
||||
publish_cmd_template: 'echo publish {slug} {version}',
|
||||
docs: 'https://fakestore.example',
|
||||
auth_check: 'true', // always-authenticated
|
||||
},
|
||||
fakestore_noauth: {
|
||||
cli: 'true',
|
||||
login_cmd: 'fakestore_noauth login',
|
||||
publish_cmd_template: 'echo publish {slug} {version}',
|
||||
docs: 'https://fakestore.example',
|
||||
auth_check: 'false', // always-fails auth
|
||||
},
|
||||
fakestore_missing: {
|
||||
cli: 'nonexistent-binary-xyz',
|
||||
login_cmd: 'fakestore_missing login',
|
||||
publish_cmd_template: 'echo publish {slug} {version}',
|
||||
docs: 'https://fakestore.example',
|
||||
auth_check: 'nonexistent-binary-xyz whoami',
|
||||
},
|
||||
};
|
||||
|
||||
function validSkill(slug: string, sourceRel: string, marketplaces: string[] = ['fakestore_ok']) {
|
||||
const m: Record<string, { slug: string; publish: boolean }> = {};
|
||||
for (const name of marketplaces) m[name] = { slug, publish: true };
|
||||
return {
|
||||
slug,
|
||||
source: sourceRel,
|
||||
name: `Skill ${slug}`,
|
||||
version: '1.0.0',
|
||||
category: 'test',
|
||||
description: 'A test skill',
|
||||
marketplaces: m,
|
||||
standalone: true,
|
||||
compatible_hosts: ['claude-code'],
|
||||
};
|
||||
}
|
||||
|
||||
describe('gstack-publish: manifest loading', () => {
|
||||
test('--list prints every skill and marketplace', () => {
|
||||
writeSkillFile('skills/alpha/SKILL.md');
|
||||
writeSkillFile('skills/beta/SKILL.md');
|
||||
writeManifest({
|
||||
version: '1.0.0',
|
||||
description: 't',
|
||||
skills: [validSkill('alpha', 'skills/alpha/SKILL.md'), validSkill('beta', 'skills/beta/SKILL.md')],
|
||||
marketplaces: VALID_MARKETPLACES,
|
||||
});
|
||||
const r = run(['--list']);
|
||||
expect(r.status).toBe(0);
|
||||
expect(r.stdout).toContain('alpha');
|
||||
expect(r.stdout).toContain('beta');
|
||||
expect(r.stdout).toContain('fakestore_ok');
|
||||
});
|
||||
|
||||
test('missing manifest exits non-zero', () => {
|
||||
// Delete any manifest
|
||||
const r = run(['--dry-run']);
|
||||
expect(r.status).not.toBe(0);
|
||||
expect(r.stderr).toContain('skills.json');
|
||||
});
|
||||
|
||||
test('malformed JSON exits non-zero', () => {
|
||||
fs.writeFileSync(path.join(sandbox, 'skills.json'), '{ not json');
|
||||
const r = run(['--dry-run']);
|
||||
expect(r.status).not.toBe(0);
|
||||
expect(r.stderr).toContain('parse');
|
||||
});
|
||||
});
|
||||
|
||||
describe('gstack-publish: validation', () => {
|
||||
test('missing source file reports validation error and exits 1', () => {
|
||||
writeManifest({
|
||||
version: '1.0.0',
|
||||
description: 't',
|
||||
skills: [validSkill('ghost', 'skills/ghost/DOES_NOT_EXIST.md')],
|
||||
marketplaces: VALID_MARKETPLACES,
|
||||
});
|
||||
const r = run(['--dry-run']);
|
||||
expect(r.status).not.toBe(0);
|
||||
expect(r.stderr).toContain('source file missing');
|
||||
expect(r.stderr).toContain('ghost');
|
||||
});
|
||||
|
||||
test('missing slug reports validation error', () => {
|
||||
writeSkillFile('skills/x/SKILL.md');
|
||||
const s = validSkill('temp', 'skills/x/SKILL.md');
|
||||
delete (s as Partial<typeof s>).slug;
|
||||
writeManifest({
|
||||
version: '1.0.0',
|
||||
description: 't',
|
||||
skills: [s],
|
||||
marketplaces: VALID_MARKETPLACES,
|
||||
});
|
||||
const r = run(['--dry-run']);
|
||||
expect(r.status).not.toBe(0);
|
||||
expect(r.stderr).toContain('missing slug');
|
||||
});
|
||||
|
||||
test('missing version reports validation error', () => {
|
||||
writeSkillFile('skills/x/SKILL.md');
|
||||
const s = validSkill('x', 'skills/x/SKILL.md');
|
||||
delete (s as Partial<typeof s>).version;
|
||||
writeManifest({
|
||||
version: '1.0.0',
|
||||
description: 't',
|
||||
skills: [s],
|
||||
marketplaces: VALID_MARKETPLACES,
|
||||
});
|
||||
const r = run(['--dry-run']);
|
||||
expect(r.status).not.toBe(0);
|
||||
expect(r.stderr).toContain('missing version');
|
||||
});
|
||||
|
||||
test('no marketplaces configured reports validation error', () => {
|
||||
writeSkillFile('skills/x/SKILL.md');
|
||||
const s = { ...validSkill('x', 'skills/x/SKILL.md'), marketplaces: {} };
|
||||
writeManifest({ version: '1.0.0', description: 't', skills: [s], marketplaces: VALID_MARKETPLACES });
|
||||
const r = run(['--dry-run']);
|
||||
expect(r.status).not.toBe(0);
|
||||
expect(r.stderr).toContain('no marketplaces configured');
|
||||
});
|
||||
});
|
||||
|
||||
describe('gstack-publish: dry-run execution', () => {
|
||||
test('happy path reports DRY-RUN tag and templated command', () => {
|
||||
writeSkillFile('skills/alpha/SKILL.md');
|
||||
writeManifest({
|
||||
version: '1.0.0',
|
||||
description: 't',
|
||||
skills: [validSkill('alpha', 'skills/alpha/SKILL.md')],
|
||||
marketplaces: VALID_MARKETPLACES,
|
||||
});
|
||||
const r = run(['--dry-run']);
|
||||
expect(r.status).toBe(0);
|
||||
expect(r.stdout).toContain('DRY-RUN');
|
||||
expect(r.stdout).toContain('alpha');
|
||||
expect(r.stdout).toContain('Published: 1');
|
||||
expect(r.stdout).toContain('Failed: 0');
|
||||
});
|
||||
|
||||
test('per-skill filter publishes only the requested slug', () => {
|
||||
writeSkillFile('skills/alpha/SKILL.md');
|
||||
writeSkillFile('skills/beta/SKILL.md');
|
||||
writeManifest({
|
||||
version: '1.0.0',
|
||||
description: 't',
|
||||
skills: [validSkill('alpha', 'skills/alpha/SKILL.md'), validSkill('beta', 'skills/beta/SKILL.md')],
|
||||
marketplaces: VALID_MARKETPLACES,
|
||||
});
|
||||
const r = run(['alpha', '--dry-run']);
|
||||
expect(r.status).toBe(0);
|
||||
expect(r.stdout).toContain('Publishing alpha');
|
||||
expect(r.stdout).not.toContain('Publishing beta');
|
||||
expect(r.stdout).toContain('Published: 1');
|
||||
});
|
||||
|
||||
test('unknown skill filter exits non-zero', () => {
|
||||
writeSkillFile('skills/alpha/SKILL.md');
|
||||
writeManifest({
|
||||
version: '1.0.0',
|
||||
description: 't',
|
||||
skills: [validSkill('alpha', 'skills/alpha/SKILL.md')],
|
||||
marketplaces: VALID_MARKETPLACES,
|
||||
});
|
||||
const r = run(['nonexistent', '--dry-run']);
|
||||
expect(r.status).not.toBe(0);
|
||||
expect(r.stderr).toContain('skill not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('gstack-publish: auth check isolation', () => {
|
||||
test('failing auth for one marketplace does NOT abort the batch in dry-run', () => {
|
||||
writeSkillFile('skills/alpha/SKILL.md');
|
||||
writeManifest({
|
||||
version: '1.0.0',
|
||||
description: 't',
|
||||
skills: [validSkill('alpha', 'skills/alpha/SKILL.md', ['fakestore_ok', 'fakestore_noauth'])],
|
||||
marketplaces: VALID_MARKETPLACES,
|
||||
});
|
||||
const r = run(['--dry-run']);
|
||||
// In dry-run, auth failures are reported but don't block dispatch
|
||||
expect(r.status).toBe(0);
|
||||
expect(r.stdout).toContain('fakestore_ok: OK');
|
||||
expect(r.stdout).toContain('fakestore_noauth: NOT READY');
|
||||
});
|
||||
|
||||
test('missing binary reported as not-ready with docs link', () => {
|
||||
writeSkillFile('skills/alpha/SKILL.md');
|
||||
writeManifest({
|
||||
version: '1.0.0',
|
||||
description: 't',
|
||||
skills: [validSkill('alpha', 'skills/alpha/SKILL.md', ['fakestore_missing'])],
|
||||
marketplaces: VALID_MARKETPLACES,
|
||||
});
|
||||
const r = run(['--dry-run']);
|
||||
expect(r.stdout).toContain('fakestore_missing: NOT READY');
|
||||
expect(r.stdout).toContain('not on PATH');
|
||||
});
|
||||
});
|
||||
|
||||
describe('gstack-publish: real manifest sanity', () => {
|
||||
test('the real repo skills.json passes --dry-run validation', () => {
|
||||
// This uses the actual bin against the actual manifest (ROOT/skills.json).
|
||||
// If auth to any real marketplace isn't set up it just reports NOT READY;
|
||||
// --dry-run still exits 0 because it doesn't require auth to pass.
|
||||
const real = spawnSync('bun', ['run', path.join(ROOT, 'bin', 'gstack-publish'), '--dry-run'], {
|
||||
cwd: ROOT,
|
||||
encoding: 'utf-8',
|
||||
timeout: 20000,
|
||||
});
|
||||
expect(real.status).toBe(0);
|
||||
expect(real.stdout).toContain('Validating manifest');
|
||||
// Every skill in the real manifest should pass validation
|
||||
expect(real.stderr).not.toContain('Manifest validation failed');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user