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
14 changes: 7 additions & 7 deletions docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -2199,7 +2199,7 @@ Operation: read. Result schema: `treeseed.command.library.show/v1`.
Execution: `local.library.show`.

- `--json`: Emit the stable JSON envelope.
- `--ref <value>`: Exact commit or protected library ref.
- `--ref <value>`: Earlier historical revision; omit for the current library.

### trsd library status <project>

Expand All @@ -2209,7 +2209,7 @@ Operation: read. Result schema: `treeseed.command.library.status/v1`.
Execution: `local.library.status`.

- `--json`: Emit the stable JSON envelope.
- `--ref <value>`: Exact commit or protected library ref.
- `--ref <value>`: Earlier historical revision; omit for the current library.

### trsd library paths <project>

Expand All @@ -2219,7 +2219,7 @@ Operation: read. Result schema: `treeseed.command.library.paths/v1`.
Execution: `local.library.paths`.

- `--json`: Emit the stable JSON envelope.
- `--ref <value>`: Exact commit or protected library ref.
- `--ref <value>`: Earlier historical revision; omit for the current library.
- `--prefix <value>`: Repository-relative path prefix.
- `--limit <value>`: Page size.
- `--cursor <value>`: Opaque page cursor.
Expand All @@ -2232,7 +2232,7 @@ Operation: read. Result schema: `treeseed.command.library.read/v1`.
Execution: `local.library.read`.

- `--json`: Emit the stable JSON envelope.
- `--ref <value>`: Exact commit or protected library ref.
- `--ref <value>`: Earlier historical revision; omit for the current library.

### trsd library search <project> <query>

Expand All @@ -2242,7 +2242,7 @@ Operation: read. Result schema: `treeseed.command.library.search/v1`.
Execution: `local.library.search`.

- `--json`: Emit the stable JSON envelope.
- `--ref <value>`: Exact commit or protected library ref.
- `--ref <value>`: Earlier historical revision; omit for the current library.
- `--path <value>`: Restrict search to a repository-relative path.
- `--limit <value>`: Page size.
- `--cursor <value>`: Opaque page cursor.
Expand All @@ -2255,7 +2255,7 @@ Operation: read. Result schema: `treeseed.command.library.query/v1`.
Execution: `local.library.query`.

- `--json`: Emit the stable JSON envelope.
- `--ref <value>`: Exact commit or protected library ref.
- `--ref <value>`: Earlier historical revision; omit for the current library.
- `--model <value>`: TreeDX content model.
- `--input <value>`: YAML or JSON query body.

Expand All @@ -2267,7 +2267,7 @@ Operation: read. Result schema: `treeseed.command.library.context/v1`.
Execution: `local.library.context`.

- `--json`: Emit the stable JSON envelope.
- `--ref <value>`: Exact commit or protected library ref.
- `--ref <value>`: Earlier historical revision; omit for the current library.
- `--max-items <value>`: Maximum context items.
- `--max-tokens <value>`: Maximum context tokens.

Expand Down
14 changes: 7 additions & 7 deletions schemas/command-tree.json
Original file line number Diff line number Diff line change
Expand Up @@ -5469,7 +5469,7 @@
"options": [
{
"name": "--ref",
"description": "Exact commit or protected library ref.",
"description": "Earlier historical revision; omit for the current library.",
"type": "string"
}
],
Expand All @@ -5494,7 +5494,7 @@
"options": [
{
"name": "--ref",
"description": "Exact commit or protected library ref.",
"description": "Earlier historical revision; omit for the current library.",
"type": "string"
}
],
Expand All @@ -5519,7 +5519,7 @@
"options": [
{
"name": "--ref",
"description": "Exact commit or protected library ref.",
"description": "Earlier historical revision; omit for the current library.",
"type": "string"
},
{
Expand Down Expand Up @@ -5564,7 +5564,7 @@
"options": [
{
"name": "--ref",
"description": "Exact commit or protected library ref.",
"description": "Earlier historical revision; omit for the current library.",
"type": "string"
}
],
Expand Down Expand Up @@ -5594,7 +5594,7 @@
"options": [
{
"name": "--ref",
"description": "Exact commit or protected library ref.",
"description": "Earlier historical revision; omit for the current library.",
"type": "string"
},
{
Expand Down Expand Up @@ -5639,7 +5639,7 @@
"options": [
{
"name": "--ref",
"description": "Exact commit or protected library ref.",
"description": "Earlier historical revision; omit for the current library.",
"type": "string"
},
{
Expand Down Expand Up @@ -5679,7 +5679,7 @@
"options": [
{
"name": "--ref",
"description": "Exact commit or protected library ref.",
"description": "Earlier historical revision; omit for the current library.",
"type": "string"
},
{
Expand Down
20 changes: 15 additions & 5 deletions src/cli/commands/development.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,9 +298,14 @@ async function restartConsumer(input: { state: LocalSessionState; runtime: Devel
if (target.operations.cleanup) runOneShotOperation(state, target.operations.cleanup, worktree, mode, context.env, { TREESEED_DEVELOPMENT_CLEANUP_SCOPE: 'runtime' });
const resolved = await invoke(context, 'local.dev.environment', { sessionId: state.sessionId, projectId: runtime.project.id, targetId: target.id }) as { environment?: NodeJS.ProcessEnv };
if (target.operations.setup) runOneShotOperation(state, target.operations.setup, worktree, mode, context.env, resolved.environment ?? {});
startOperation(state, runtime, target, worktree, mode, context.env, resolved.environment ?? {});
saveState(state, context.env);
await waitForDirectReadiness(target, target.ready.kind === 'process' ? target.ready.graceSeconds : target.ready.timeoutSeconds, state, key);
if (!target.operations.start && target.kind === 'rebuild-restart' && target.operations.build) {
runOneShotOperation(state, target.operations.build, worktree, mode, context.env, resolved.environment ?? {});
await waitForDirectReadiness(target, target.ready.kind === 'process' ? target.ready.graceSeconds : target.ready.timeoutSeconds);
} else {
startOperation(state, runtime, target, worktree, mode, context.env, resolved.environment ?? {});
saveState(state, context.env);
await waitForDirectReadiness(target, target.ready.kind === 'process' ? target.ready.graceSeconds : target.ready.timeoutSeconds, state, key);
}
if (input.recordGeneration !== false) await markRebuilt(context, state.sessionId, runtime.project.id, target.id, mode, target);
}

Expand Down Expand Up @@ -337,8 +342,13 @@ async function rebuild(invocation: ParsedInvocation, context: CommandContext, st
if (target.kind === 'package-watch') await rebuildPackage({ state, runtime, target, worktree: repository.worktree, mode, context });
else if (target.kind === 'rebuild-restart') {
if (!target.operations.build) throw new Error(`${selection.projectId}.${selection.targetId} does not declare a build operation.`);
runOneShotOperation(state, target.operations.build, repository.worktree, mode, context.env);
await restartConsumer({ state, runtime, target, worktree: repository.worktree, mode, context });
const resolved = await invoke(context, 'local.dev.environment', { sessionId, projectId: runtime.project.id, targetId: target.id }) as { environment?: NodeJS.ProcessEnv };
runOneShotOperation(state, target.operations.build, repository.worktree, mode, context.env, resolved.environment ?? {});
if (target.operations.start) await restartConsumer({ state, runtime, target, worktree: repository.worktree, mode, context });
else {
await waitForDirectReadiness(target, target.ready.kind === 'process' ? target.ready.graceSeconds : target.ready.timeoutSeconds);
await markRebuilt(context, sessionId, runtime.project.id, target.id, mode, target);
}
} else await restartConsumer({ state, runtime, target, worktree: repository.worktree, mode, context });
const manual: string[] = [];
for (const dependent of dependentReactions(record.runtimes, selection.projectId, selection.targetId)) {
Expand Down
36 changes: 34 additions & 2 deletions src/cli/commands/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,44 @@ const number = (value: unknown) => value === undefined ? undefined : Number(valu
async function inputFile(invocation: ParsedInvocation, context: CommandContext) {
const file = text(invocation.options.input);
if (!file) return {};
const parsed = parseYaml(await readFile(resolve(context.cwd, file), 'utf8'));
let source = '';
if (file === '-') {
process.stdin.setEncoding('utf8');
for await (const chunk of process.stdin) source += String(chunk);
} else source = await readFile(resolve(context.cwd, file), 'utf8');
const parsed = parseYaml(source);
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw Object.assign(new Error('Library input must be one YAML or JSON object.'), { category: 'invalid_input', code: 'library_input_invalid' });
return parsed as Record<string, unknown>;
}

export async function resolveActiveTeamLibraryProject(invocation: ParsedInvocation, context: CommandContext) {
if (context.operationInvoke) return;
const requestedProject = String(invocation.arguments[0] ?? '');
if (!requestedProject) return;
const realClient = await createControlPlaneClient(invocation, context, true);
const activeTeamId = realClient.session.activeTeam?.id;
let cursor: string | undefined;
const matches: Record<string, any>[] = [];
do {
const response = data(await realClient.client.invoke(controlPlaneOperation('projects.list'), {
path: {}, query: { limit: 200, ...(cursor ? { cursor } : {}) },
}));
for (const project of response.items ?? []) if ([project.id, project.slug].includes(requestedProject)
&& (!activeTeamId || String(project.teamId ?? project.team_id ?? '') === activeTeamId)) matches.push(project);
cursor = text(response.page?.nextCursor ?? response.nextCursor);
} while (cursor && matches.length < 2);
if (matches.length !== 1) {
const code = matches.length ? 'project_ambiguous' : 'project_not_found';
throw Object.assign(new Error(matches.length ? `Project ${requestedProject} is ambiguous.` : `Project ${requestedProject} was not found.`), {
category: matches.length ? 'ambiguous_context' : 'not_found', code,
});
}
invocation.arguments[0] = String(matches[0]!.id);
}

export async function runLibrary(invocation: ParsedInvocation, context: CommandContext) {
const realClient = context.operationInvoke ? null : await createControlPlaneClient(invocation, context, true);
const activeTeamId = realClient?.session?.activeTeam?.id;
const invoke = async (operationId: string, input: Input) => context.operationInvoke
? context.operationInvoke(operationId, input)
: realClient!.client.invoke(controlPlaneOperation(operationId), input);
Expand All @@ -30,7 +61,8 @@ export async function runLibrary(invocation: ParsedInvocation, context: CommandC
const matches: Record<string, any>[] = [];
do {
const response = data(await invoke('projects.list', { path: {}, query: { limit: 200, ...(cursor ? { cursor } : {}) } }));
for (const project of response.items ?? []) if ([project.id, project.slug].includes(requestedProject)) matches.push(project);
for (const project of response.items ?? []) if ([project.id, project.slug].includes(requestedProject)
&& (!activeTeamId || String(project.teamId ?? project.team_id ?? '') === activeTeamId)) matches.push(project);
cursor = text(response.page?.nextCursor ?? response.nextCursor);
} while (cursor && matches.length < 2);
if (matches.length !== 1) {
Expand Down
14 changes: 10 additions & 4 deletions src/cli/commands/operator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,16 @@ function transform(value: unknown, binding: CommandInputBinding) {
return value;
}

async function inputDocument(fileValue: string, context: CommandContext) {
if (fileValue !== '-') return readFile(resolve(context.cwd, fileValue), 'utf8');
let source = '';
process.stdin.setEncoding('utf8');
for await (const chunk of process.stdin) source += String(chunk);
return source;
}

async function portableSeedBundle(fileValue: string, context: CommandContext) {
const file = resolve(context.cwd, fileValue);
const parsed = parseYaml(await readFile(file, 'utf8'));
const parsed = parseYaml(await inputDocument(fileValue, context));
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw Object.assign(new Error('Seed file must contain one portable seed bundle object.'), { category: 'invalid_input', code: 'seed_bundle_file_invalid' });
return parsed as Record<string, unknown>;
}
Expand Down Expand Up @@ -80,8 +87,7 @@ async function operationInput(invocation: ParsedInvocation, context: CommandCont
input.body.bundle = parsed;
}
if (!operation.descriptor.operationId.startsWith('seeds.') && typeof input.body.file === 'string') {
const file = resolve(context.cwd, input.body.file);
const parsed = parseYaml(await readFile(file, 'utf8'));
const parsed = parseYaml(await inputDocument(input.body.file, context));
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw Object.assign(new Error('Input file must contain one YAML or JSON object.'), { category: 'invalid_input', code: 'command_input_file_invalid' });
delete input.body.file;
Object.assign(input.body, parsed);
Expand Down
3 changes: 2 additions & 1 deletion src/cli/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { isCommandBranch, resolveCommand } from './registry.js';
import { runSecrets } from './commands/secrets.js';
import { runHost } from './commands/host.js';
import { runUsers } from './commands/users.js';
import { runLibrary } from './commands/library.js';
import { resolveActiveTeamLibraryProject, runLibrary } from './commands/library.js';
import { runTeams } from './commands/teams.js';
import { runDevelopment } from './commands/development.js';
import { runInbox } from './commands/inbox.js';
Expand Down Expand Up @@ -70,6 +70,7 @@ async function execute(invocation: ParsedInvocation, context: CommandContext) {
if (invocation.command.execution.kind === 'local' && invocation.command.execution.handlerId.startsWith('local.host.')) return runHost(invocation, context);
if (invocation.command.execution.kind === 'local' && invocation.command.path[0] === 'dev') return runDevelopment(invocation, context);
if (invocation.command.execution.kind === 'local' && invocation.command.path[0] === 'library') return runLibrary(invocation, context);
if (invocation.command.path.join(' ') === 'library workspace create') await resolveActiveTeamLibraryProject(invocation, context);
if (invocation.command.execution.kind === 'local' && invocation.command.path[0] === 'inbox') return runInbox(invocation, context);
if (invocation.command.execution.kind === 'local' && invocation.command.path[0] === 'platform') return runPlatform(invocation, context);
return runOperator(invocation, context);
Expand Down
16 changes: 16 additions & 0 deletions src/cli/support/human-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,21 @@ export function renderCommunicationResponses(result: Record<string, unknown>, op
}).join('\n\n\n');
}

function renderLibraryRead(result: Record<string, unknown>, options: RenderOptions = {}) {
const payload = result.result && typeof result.result === 'object' && !Array.isArray(result.result)
? result.result as Record<string, unknown> : result;
const files = Array.isArray(payload.files) ? payload.files : payload.file && typeof payload.file === 'object' ? [payload.file] : [];
const ref = payload.resolvedRef ?? payload.ref;
return files.map((value) => {
const file = value as Record<string, unknown>;
const source = scalar(file.sourcePath ?? file.path);
const logical = file.logicalPath && file.logicalPath !== file.sourcePath ? ` (${scalar(file.logicalPath)})` : '';
const heading = `${ansi(options.color === true, '1;36', source)}${ansi(options.color === true, '2', logical)}`;
const provenance = ref ? ansi(options.color === true, '2', `Ref: ${scalar(ref)}`) : '';
return [heading, provenance, '', markdown(String(file.content ?? file.body ?? ''), options)].filter((line, index) => line || index >= 2).join('\n');
}).join(`\n\n${ansi(options.color === true, '2;36', '═'.repeat(Math.max(48, Math.min(160, Number(options.width) || 100))))}\n\n`);
}

export function renderHumanCommandResult(value: unknown, options: RenderOptions = {}) {
if (!value || typeof value !== 'object') return scalar(value);
const envelope = value as { commandPath?: string[]; ok?: boolean; result?: unknown; warnings?: unknown[]; nextActions?: unknown[] };
Expand Down Expand Up @@ -121,6 +136,7 @@ export function renderHumanCommandResult(value: unknown, options: RenderOptions
}
if (path === 'inbox' && result?.interactiveSession === true) return '';
if (path === 'send' && result) return result.humanStreamed === true || result.interactiveSession === true ? '' : renderCommunicationResponses(result, options);
if (path === 'library read' && result) return renderLibraryRead(result, options) || 'No file content was returned.';
const rendered = lines(envelope.result);
const warnings = Array.isArray(envelope.warnings) && envelope.warnings.length ? [`Warnings: ${envelope.warnings.map(scalar).join('; ')}`] : [];
const next = Array.isArray(envelope.nextActions) && envelope.nextActions.length ? ['Next actions:', ...envelope.nextActions.map((item) => `- ${scalar(item)}`)] : [];
Expand Down
12 changes: 12 additions & 0 deletions tests/unit/command-boundary/human-renderer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ test('generic human output renders labels and values without JSON punctuation',
assert.doesNotMatch(output, /[{}]/u);
});

test('library read displays exact file content and provenance', () => {
const output = renderHumanCommandResult({ commandPath: ['library', 'read'], ok: true, result: { result: {
resolvedRef: 'abc123', files: [{ logicalPath: 'objectives/core', sourcePath: 'objectives/core.md',
content: '---\ntitle: Core objective\n---\n\n# Direction\n\nBuild **carefully**.' }],
} } });
assert.match(output, /objectives\/core\.md \(objectives\/core\)/u);
assert.match(output, /Ref: abc123/u);
assert.match(output, /title: Core objective/u);
assert.match(output, /# Direction/u);
assert.match(output, /Build carefully\./u);
});

test('host security initialization is summarized without dumping its receipt', () => {
const output = renderHumanCommandResult({ commandPath: ['host', 'security', 'initialize'], ok: true, result: {
verified: true, recoveryBundleVerified: true, receipt: { receiptId: 'security-test', state: 'known-good', sandbox: { brokerReady: true, guestImageDigests: ['sha256:a', 'sha256:a'] }, providerVolume: { encrypted: true } },
Expand Down
Loading