Skip to content
Merged
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## [v0.0.2] - 2026-02-11

- Fix PostHog telemetry
- Add Pipelex install skills when installing methods

## [v0.0.1] - 2026-02-11

- Initial commit!
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mthds",
"version": "0.0.1",
"version": "0.0.2",
"description": "CLI for composable methods for AI agents. Turn your knowledge processes into executable methods.",
"license": "MIT",
"type": "module",
Expand Down
41 changes: 36 additions & 5 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { createRequire } from "node:module";
import * as p from "@clack/prompts";
import { showBanner } from "./commands/index.js";
import { printLogo } from "./commands/index.js";
import { installSoftware } from "./commands/setup.js";
import { installRunner } from "./commands/setup.js";
import { installMethod } from "./commands/install.js";
import { configSet, configGet, configList } from "./commands/config.js";

const require = createRequire(import.meta.url);
const pkg = require("../package.json") as { version: string };
Expand All @@ -32,15 +33,45 @@ program
await installMethod(slug);
});

// mthds setup software <name>
// mthds setup runner <name>
const setup = program.command("setup").exitOverride();

setup
.command("software <name>")
.description("Install a software runtime (e.g. pipelex)")
.command("runner <name>")
.description("Install a runner (e.g. pipelex)")
.exitOverride()
.action(async (name: string) => {
await installSoftware(name);
await installRunner(name);
});

// mthds config set|get|list
const config = program.command("config").description("Manage configuration").exitOverride();

config
.command("set")
.argument("<key>", "Config key (runner, api-url, api-key)")
.argument("<value>", "Value to set")
.description("Set a config value")
.exitOverride()
.action(async (key: string, value: string) => {
await configSet(key, value);
});

config
.command("get")
.argument("<key>", "Config key (runner, api-url, api-key)")
.description("Get a config value")
.exitOverride()
.action(async (key: string) => {
await configGet(key);
});

config
.command("list")
.description("List all config values")
.exitOverride()
.action(async () => {
await configList();
});

// Default: show banner
Expand Down
87 changes: 87 additions & 0 deletions src/commands/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import * as p from "@clack/prompts";
import { printLogo } from "./index.js";
import {
VALID_KEYS,
resolveKey,
getConfigValue,
setConfigValue,
listConfig,
} from "../config/config.js";
import type { RunnerType } from "../runners/types.js";

const VALID_RUNNERS: RunnerType[] = ["api", "pipelex"];

function isValidUrl(s: string): boolean {
try {
new URL(s);
return true;
} catch {
return false;
}
}

export async function configSet(cliKey: string, value: string): Promise<void> {
printLogo();
p.intro("mthds config set");

const configKey = resolveKey(cliKey);
if (!configKey) {
p.log.error(`Unknown config key: ${cliKey}`);
p.log.info(`Valid keys: ${VALID_KEYS.join(", ")}`);
p.outro("");
process.exit(1);
}

// Validate value
if (configKey === "runner" && !VALID_RUNNERS.includes(value as RunnerType)) {
p.log.error(`Invalid runner: ${value}`);
p.log.info(`Valid runners: ${VALID_RUNNERS.join(", ")}`);
p.outro("");
process.exit(1);
}

if (configKey === "apiUrl" && !isValidUrl(value)) {
p.log.error(`Invalid URL: ${value}`);
p.outro("");
process.exit(1);
}

setConfigValue(configKey, value);
p.log.success(`${cliKey} = ${value}`);
p.outro("");
}

export async function configGet(cliKey: string): Promise<void> {
printLogo();
p.intro("mthds config get");

const configKey = resolveKey(cliKey);
if (!configKey) {
p.log.error(`Unknown config key: ${cliKey}`);
p.log.info(`Valid keys: ${VALID_KEYS.join(", ")}`);
p.outro("");
process.exit(1);
}

const { value, source } = getConfigValue(configKey);
const sourceLabel = source === "env" ? " (from env)" : source === "default" ? " (default)" : "";
p.log.info(`${cliKey} = ${value}${sourceLabel}`);
p.outro("");
}

export async function configList(): Promise<void> {
printLogo();
p.intro("mthds config list");

const entries = listConfig();
for (const entry of entries) {
const sourceLabel =
entry.source === "env"
? " (from env)"
: entry.source === "default"
? " (default)"
: "";
p.log.info(`${entry.cliKey} = ${entry.value}${sourceLabel}`);
}
p.outro("");
}
13 changes: 11 additions & 2 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,16 @@ export function showBanner(): void {
` ${chalk.yellow("install <slug>")} Install a method`
);
console.log(
` ${chalk.yellow("setup software <name>")} Install a software runtime`
` ${chalk.yellow("setup runner <name>")} Install a runner`
);
console.log(
` ${chalk.yellow("config set <key> <val>")} Set a config value`
);
console.log(
` ${chalk.yellow("config get <key>")} Get a config value`
);
console.log(
` ${chalk.yellow("config list")} List all config values`
);
console.log(
` ${chalk.yellow("--help")} Show this help message`
Expand All @@ -52,7 +61,7 @@ export function showBanner(): void {

console.log(chalk.bold(" Examples:"));
console.log(` ${chalk.dim("$")} mthds install my-method-slug`);
console.log(` ${chalk.dim("$")} mthds setup software pipelex\n`);
console.log(` ${chalk.dim("$")} mthds setup runner pipelex\n`);

console.log(
chalk.dim(" Docs: https://pipelex.dev/docs\n")
Expand Down
74 changes: 69 additions & 5 deletions src/commands/install.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { join } from "node:path";
import { homedir } from "node:os";
import { mkdirSync } from "node:fs";
import { exec } from "node:child_process";
import { promisify } from "node:util";

const execAsync = promisify(exec);
import * as p from "@clack/prompts";
import chalk from "chalk";
import { isPipelexInstalled } from "../runtime/check.js";
import { ensureRuntime } from "../runtime/installer.js";
import { trackMethodInstall, shutdown } from "../telemetry/posthog.js";
Expand Down Expand Up @@ -82,18 +87,18 @@ export async function installMethod(slug: string): Promise<void> {
process.exit(0);
}

// Step 3: Optional software install
const wantsSoftware = await p.confirm({
message: "Do you want to install software now? (optional)",
// Step 3: Optional runner install
const wantsRunner = await p.confirm({
message: "Do you want to install the runner now? (optional)",
initialValue: false,
});

if (p.isCancel(wantsSoftware)) {
if (p.isCancel(wantsRunner)) {
p.cancel("Installation cancelled.");
process.exit(0);
}

if (wantsSoftware) {
if (wantsRunner) {
if (!isPipelexInstalled()) {
await ensureRuntime();
p.log.success("pipelex installed.");
Expand All @@ -120,6 +125,65 @@ export async function installMethod(slug: string): Promise<void> {
targetDir,
});

// Step 5: Optional Pipelex skills
const SKILLS_REPO = "https://github.com/pipelex/skills";
const skillChoices = [
{ value: "check", label: "check", hint: "Validate and review Pipelex workflow bundles without making changes" },
{ value: "edit", label: "edit", hint: "Modify existing Pipelex workflow bundles" },
{ value: "build", label: "build", hint: "Create new Pipelex workflow bundles from scratch" },
{ value: "fix", label: "fix", hint: "Automatically fix issues in Pipelex workflow bundles" },
];

let selectedSkills: string[] = [];
let emptyAttempts = 0;

// eslint-disable-next-line no-constant-condition
while (true) {
const hint = emptyAttempts > 0
? chalk.yellow(" press space to select, enter to confirm")
: chalk.dim(" press space to select, enter to confirm");

const result = await p.multiselect({
message: `Which Pipelex skills do you want to install?\n${hint}`,
options: skillChoices,
required: false,
});

if (p.isCancel(result)) {
p.cancel("Installation cancelled.");
process.exit(0);
}

if (result.length === 0) {
emptyAttempts++;
if (emptyAttempts >= 2) {
break;
}
continue;
}

selectedSkills = result;
break;
}

if (selectedSkills.length > 0) {
const globalFlag = selectedLocation === Loc.Global ? " -g" : "";
const locationLabel = selectedLocation === Loc.Global ? "globally" : "locally";
const sk = p.spinner();
for (const skill of selectedSkills) {
sk.start(`Installing skill "${skill}" ${locationLabel}...`);
try {
await execAsync(`npx skills add ${SKILLS_REPO} --skill ${skill} --agent ${selectedAgent}${globalFlag} -y`, {
cwd: process.cwd(),
});
sk.stop(`Skill "${skill}" installed ${locationLabel}.`);
} catch {
sk.stop(`Failed to install skill "${skill}".`);
p.log.warning(`Could not install skill "${skill}". You can retry manually:\n npx skills add ${SKILLS_REPO} --skill ${skill} --agent ${selectedAgent}${globalFlag}`);
}
}
}

p.outro("Done");
await shutdown();
}
6 changes: 3 additions & 3 deletions src/commands/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import { ensureRuntime } from "../runtime/installer.js";
import { shutdown } from "../telemetry/posthog.js";
import { printLogo } from "./index.js";

export async function installSoftware(name: string): Promise<void> {
export async function installRunner(name: string): Promise<void> {
printLogo();
p.intro("mthds setup");

if (name !== "pipelex") {
p.log.error(`Unknown software: ${name}`);
p.log.info("Available software: pipelex");
p.log.error(`Unknown runner: ${name}`);
p.log.info("Available runners: pipelex");
p.outro("Done");
process.exit(1);
}
Expand Down
Loading