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
2 changes: 1 addition & 1 deletion bun.lock

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

2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@miakapp/cli",
"version": "4.0.0-alpha.0",
"version": "4.0.0-alpha.1",
"description": "Agent-first Miakapp command line: check, build, publish, activate and roll back home components",
"type": "module",
"engines": {
Expand Down
40 changes: 40 additions & 0 deletions packages/cli/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ export interface CommandResult {
readonly summary: string;
readonly fields: readonly Field[];
readonly json: Record<string, unknown>;
/** Complete human-facing document for commands such as `docs start`. */
readonly text?: string;
}

export interface Invocation {
Expand All @@ -99,6 +101,7 @@ Usage
miakapp <command> [options]

Commands
docs start Print the complete agent guide bundled with this CLI
init Write ${PROJECT_FILE} in the current directory
agent-pack Install the guide and the MCP wiring into a repository
discover Inventory an existing Node-RED installation offline
Expand Down Expand Up @@ -158,6 +161,7 @@ const GLOBAL_OPTIONS = ['project'] as const;

/** Exported so the MCP surface can be proved to expose every option, and no other. */
export const COMMAND_OPTIONS: Record<string, readonly string[]> = {
docs: [],
init: ['home', 'control-plane', 'artifact', 'release'],
'agent-pack': ['dir'],
discover: ['flows'],
Expand Down Expand Up @@ -445,6 +449,39 @@ export async function guideAssetPath(): Promise<string> {
return fileURLToPath(new URL('../assets/agent-guide.md', import.meta.url));
}

/**
* Gives an agent its complete starting contract without requiring a repository,
* a network request or an MCP client. The onboarding page can therefore hand a
* person one stable command and the installed CLI remains the source of truth.
*/
async function runDocs(host: CliHost, invocation: Invocation): Promise<CommandResult> {
if (invocation.positional.length !== 1 || invocation.positional[0] !== 'start') {
throw usageError(
'docs requires the topic start',
'Run miakapp docs start to print the complete agent guide.',
);
}

const path = await guideAssetPath();
let guide: string;
try {
const filesystem = await files(host);
guide = new TextDecoder('utf-8', { fatal: true }).decode(await filesystem.read(path));
} catch {
throw projectError(
`The packaged guide is missing or unreadable at ${path}`,
'Reinstall @miakapp/cli: the guide is bundled with the package and opens no network connection.',
);
}

return {
summary: 'Miakapp agent guide',
fields: [],
json: { topic: 'start', guide },
text: guide.endsWith('\n') ? guide : `${guide}\n`,
};
}

/**
* Installs the agent pack. Like `discover`, it loads no project file: the
* repository it prepares is usually one that has no V4 project yet.
Expand Down Expand Up @@ -699,6 +736,8 @@ async function runUpload(host: CliHost, invocation: Invocation): Promise<Command
*/
export async function dispatch(host: CliHost, invocation: Invocation): Promise<CommandResult> {
switch (invocation.command) {
case 'docs':
return await runDocs(host, invocation);
case 'init':
return await runInit(host, invocation);
case 'agent-pack':
Expand All @@ -722,6 +761,7 @@ export async function dispatch(host: CliHost, invocation: Invocation): Promise<C
}

function renderText(result: CommandResult): string {
if (result.text !== undefined) return result.text;
const lines = [result.summary];
for (const [key, value] of result.fields) {
lines.push(` ${key}: ${Array.isArray(value) ? `[${value.join(', ')}]` : String(value)}`);
Expand Down
16 changes: 16 additions & 0 deletions packages/cli/src/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,22 @@ const CONFIRM: ToolArgument = {
* which an MCP client gets from `initialize` and `tools/list` instead.
*/
export const TOOLS: readonly ToolDefinition[] = [
{
name: 'miakapp_docs',
title: 'Read the Miakapp agent guide',
command: 'docs',
description:
'Read the complete agent guide bundled with this exact CLI release. Start here before '
+ 'creating or changing a home: it defines the repository shape, authorization boundary, '
+ 'component contract, verification loop, publication and rollback rules. Offline and read-only.',
args: [],
positional: {
name: 'topic',
description: 'Guide topic. Use "start".',
},
readOnly: true,
guarded: false,
},
{
name: 'miakapp_discover',
title: 'Inventory an existing Node-RED house',
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@
*/
export const PACKAGE_NAME = '@miakapp/cli';

export const CLI_VERSION = '4.0.0-alpha.0';
export const CLI_VERSION = '4.0.0-alpha.1';
23 changes: 21 additions & 2 deletions packages/cli/test/main.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, test } from 'bun:test';
import { EXIT_CODE } from '../src/errors.js';
import { HOME_KEY_VARIABLE, parseArguments, run } from '../src/main.js';
import { HOME_KEY_VARIABLE, guideAssetPath, parseArguments, run } from '../src/main.js';
import { CLI_VERSION } from '../src/version.js';
import { digestOf, fakeControlPlane, homeKey } from './support/control-plane.js';
import {
ARTIFACT_SOURCE,
Expand Down Expand Up @@ -47,7 +48,25 @@ describe('offline commands', () => {
test('version prints the package version', async () => {
const host = testHost();
expect(await run(['version'], host)).toBe(EXIT_CODE.success);
expect(host.stdout().trim()).toBe('4.0.0-alpha.0');
expect(host.stdout().trim()).toBe(CLI_VERSION);
});

test('docs start prints the complete bundled guide without a project or network', async () => {
const files = new MemoryFiles({
[await guideAssetPath()]: '# Miakapp agent guide\n\nStart here.\n',
});
const host = testHost({ files, fetch: async () => { throw new Error('network used'); } });

expect(await run(['docs', 'start'], host)).toBe(EXIT_CODE.success);
expect(host.stdout()).toBe('# Miakapp agent guide\n\nStart here.\n');
expect(host.stderr()).toBe('');
});

test('docs rejects an unknown topic instead of printing the wrong contract', async () => {
const host = testHost();

expect(await run(['docs', 'publish'], host)).toBe(EXIT_CODE.usage);
expect(host.stderr()).toContain('miakapp docs start');
});

test('an unknown command exits with the usage code', async () => {
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/test/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ describe('argument translation', () => {
.toEqual(['release', ARTIFACT_DIGEST]);
});

test('docs start is exposed as the same read-only positional command', () => {
expect(buildArgv(tool('miakapp_docs'), { topic: 'start' }))
.toEqual(['docs', 'start']);
});

test('an invented argument is refused rather than dropped', () => {
expect(() => buildArgv(tool('miakapp_check'), { force: true })).toThrow(/Unknown argument/);
});
Expand Down
Loading