Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 64 additions & 13 deletions cli/selftune/contribute/contribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ import { spawnSync } from "node:child_process";
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { parseArgs } from "node:util";

import { CONTRIBUTIONS_DIR } from "../constants.js";
import { readAlphaIdentity } from "../alpha-identity.js";
import { CONTRIBUTIONS_DIR, SELFTUNE_CONFIG_PATH } from "../constants.js";
import { findCreatorContributionConfig } from "../contribution-config.js";
import { handleCLIError } from "../utils/cli-error.js";
import { getSelftuneVersion } from "../utils/selftune-meta.js";
import { assembleBundle } from "./bundle.js";
import { sanitizeBundle } from "./sanitize.js";

Expand All @@ -29,24 +32,24 @@ export async function cliMain(): Promise<void> {
sanitize: { type: "string", default: "conservative" },
since: { type: "string" },
submit: { type: "boolean", default: false },
endpoint: { type: "string", default: "https://selftune-api.fly.dev" },
endpoint: { type: "string" },
github: { type: "boolean", default: false },
help: { type: "boolean", short: "h", default: false },
},
strict: true,
});

if (values.help) {
console.log(`selftune contribute — Export an anonymized community bundle
console.log(`selftune contribute — Export an anonymized community export bundle

Usage:
selftune contribute --skill <name> [--preview] [--sanitize conservative|aggressive]
selftune contribute --skill <name> [--output <file>] [--submit]

Purpose:
Build a sanitized community contribution bundle from local SQLite data.
Build a sanitized community export bundle from local SQLite data.
This is separate from:
selftune contributions Creator-directed sharing preferences
selftune contributions Sharing preferences (creator-directed opt-in/out)
selftune alpha upload Personal cloud upload cycle

Options:
Expand Down Expand Up @@ -131,7 +134,8 @@ Options:
const ok = submitToGitHub(json, outputPath);
if (!ok) process.exit(1);
} else {
const endpoint = values.endpoint ?? "https://selftune-api.fly.dev";
const auth = getLocalAuthConfig();
const endpoint = values.endpoint ?? auth?.apiUrl ?? "https://api.selftune.dev";
const ok = await submitToService(json, endpoint, skillName);
if (!ok) {
console.log("Falling back to GitHub submission...");
Expand All @@ -143,20 +147,67 @@ Options:
}

// ---------------------------------------------------------------------------
// Service submission
// Auth helpers
// ---------------------------------------------------------------------------

function getLocalAuthConfig(): { apiUrl: string; apiKey: string } | null {
try {
const identity = readAlphaIdentity(SELFTUNE_CONFIG_PATH);
if (!identity?.api_key) return null;
const apiUrl = identity.cloud_api_url || "https://api.selftune.dev";
return { apiUrl, apiKey: identity.api_key };
} catch {
return null;
}
}

function resolveCreatorId(skillName: string): string | null {
const config = findCreatorContributionConfig(skillName);
return config?.creator_id ?? null;
}

// ---------------------------------------------------------------------------
// Service submission (cloud endpoint)
// ---------------------------------------------------------------------------

async function submitToService(
json: string,
endpoint: string,
skillName: string,
): Promise<boolean> {
// Resolve creator_id from the installed selftune.contribute.json
const creatorId = resolveCreatorId(skillName);
if (!creatorId) {
console.error(
`[ERROR] No creator_id found for skill "${skillName}". ` +
`Ensure selftune.contribute.json exists in the skill directory with a valid creator_id.`,
);
return false;
}

// Resolve auth from local config
const auth = getLocalAuthConfig();

try {
const url = `${endpoint}/api/submit`;
const url = `${endpoint}/api/v1/community/bundles`;
// Wrap the already-serialized bundle in the submission envelope
// without an unnecessary parse/stringify cycle
const payload = `{"creator_id":${JSON.stringify(creatorId)},"skill_name":${JSON.stringify(skillName)},"bundle":${json}}`;

const headers: Record<string, string> = {
"Content-Type": "application/json",
"User-Agent": `selftune/${getSelftuneVersion()}`,
};

if (auth?.apiKey) {
headers.Authorization = `Bearer ${auth.apiKey}`;
}

const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: json,
headers,
body: payload,
signal: AbortSignal.timeout(60_000),
});

if (!res.ok) {
Expand All @@ -165,9 +216,9 @@ async function submitToService(
return false;
}

console.log(`\nSubmitted to ${endpoint}`);
console.log(` Badge: ${endpoint}/badge/${encodeURIComponent(skillName)}`);
console.log(` Report: ${endpoint}/report/${encodeURIComponent(skillName)}`);
console.log(`\nSubmitted to ${endpoint}/api/v1/community/bundles`);
console.log(` Skill: ${skillName}`);
console.log(` Creator: ${creatorId}`);
return true;
} catch (err) {
console.error(
Expand Down
22 changes: 22 additions & 0 deletions cli/selftune/contribution-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,22 @@ import {
findRepositorySkillDirs,
} from "./utils/skill-discovery.js";

/**
* The canonical UUID pattern for `creator_id`. This field must always be the
* creator's cloud user UUID (the `cloud_user_id` from alpha enrollment), e.g.
* "550e8400-e29b-41d4-a716-446655440000". Non-UUID values are accepted during
* local development but will be rejected by the relay endpoint.
*/
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

/** Returns `true` when `value` looks like a valid UUID v4 (case-insensitive). */
export function isValidCreatorUUID(value: string): boolean {
return UUID_RE.test(value);
}

export interface CreatorContributionConfig {
version: 1;
/** Must be the creator's cloud user UUID (`cloud_user_id`). */
creator_id: string;
skill_name: string;
config_path: string;
Expand All @@ -22,6 +36,7 @@ export interface CreatorContributionConfig {
}

export interface CreatorContributionConfigInput {
/** Must be the creator's cloud user UUID (`cloud_user_id`). */
creator_id: string;
skill_name: string;
skill_path: string;
Expand Down Expand Up @@ -95,6 +110,13 @@ function normalizeContributionConfig(
.filter(Boolean);
if (signals.length === 0) return null;

if (!isValidCreatorUUID(creatorId)) {
process.stderr.write(
`[selftune] warning: creator_id "${creatorId}" is not a valid UUID. ` +
`Expected a cloud user UUID (e.g. "550e8400-e29b-41d4-a716-446655440000").\n`,
);
}

return {
version: 1,
creator_id: creatorId,
Expand Down
14 changes: 8 additions & 6 deletions cli/selftune/creator-contributions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,9 @@ function enableCreatorContributionConfigs(options: {
const creatorId = inferCreatorId(options.explicitCreatorId);
if (!creatorId) {
throw new CLIError(
"Creator ID is required.",
"Creator ID is required. Must be the creator's cloud user UUID.",
"MISSING_FLAG",
"Pass --creator-id <id> or enroll alpha so cloud_user_id is available.",
"Pass --creator-id <uuid> or enroll alpha so cloud_user_id is available.",
);
}

Expand Down Expand Up @@ -158,7 +158,7 @@ export async function cliMain(): Promise<void> {
const rest = process.argv.slice(3);

if (sub === "--help" || sub === "-h") {
console.log(`selftune creator-contributions — Manage creator-side contribution configs
console.log(`selftune creator-contributions — Manage creator sharing setup configs

Usage:
selftune creator-contributions
Expand All @@ -168,9 +168,11 @@ Usage:
selftune creator-contributions disable --skill <name> [--skill-path <path>]

Purpose:
Manage the local selftune.contribute.json file that a skill creator bundles
with a skill package. This is separate from:
selftune contributions End-user sharing preferences
Manage the local selftune.contribute.json creator sharing setup file that
a skill creator bundles with a skill package. The --creator-id must be the
creator's cloud user UUID (the cloud_user_id from alpha enrollment).
This is separate from:
selftune contributions Sharing preferences (end-user opt-in/out)
selftune contribute Community export bundle`);
return;
}
Expand Down
Loading
Loading