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
28 changes: 27 additions & 1 deletion package-lock.json

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

44 changes: 44 additions & 0 deletions packages/cli/src/commands/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
* Extracted from cli.ts to enable modular async dispatch.
*/

import * as fs from 'node:fs';
import * as path from 'node:path';
import { loadConfig, saveConfig, cmdOk, cmdErr } from '../core/index.js';
import { getPositionalArg, type CommandRegistry } from './types.js';

Expand Down Expand Up @@ -61,6 +63,48 @@ export const CONFIG_COMMANDS: CommandRegistry = {
},
},

'config-save-defaults': {
name: 'config-save-defaults',
description: 'Save current config as a defaults snapshot. Usage: config-save-defaults <output-path>',
async handler(args) {
const dest = getPositionalArg(args, 0);
if (!dest) {
return cmdErr('Usage: config-save-defaults <output-path>');
}
const config = loadConfig(process.cwd());
const destPath = path.resolve(dest);
fs.mkdirSync(path.dirname(destPath), { recursive: true });
fs.writeFileSync(destPath, JSON.stringify(config, null, 2), 'utf8');
return cmdOk(`Defaults saved to ${destPath}`);
},
},

'validate-structure': {
name: 'validate-structure',
description: 'Validate the MaxsimCLI directory structure. Usage: validate-structure',
async handler(_args) {
const projectDir = process.cwd();
const checks = [
{ path: '.claude', label: '.claude/ directory' },
{ path: path.join('.claude', 'maxsim'), label: '.claude/maxsim/ directory' },
{ path: path.join('.claude', 'maxsim', 'bin', 'maxsim-tools.cjs'), label: '.claude/maxsim/bin/maxsim-tools.cjs' },
{ path: path.join('.claude', 'maxsim', 'config.json'), label: '.claude/maxsim/config.json' },
];

const results: string[] = [];
let allPassed = true;
for (const check of checks) {
const fullPath = path.join(projectDir, check.path);
const exists = fs.existsSync(fullPath);
results.push(`${exists ? 'PASS' : 'FAIL'}: ${check.label}`);
if (!exists) allPassed = false;
}

const summary = allPassed ? 'All checks passed.' : 'Some checks failed.';
return cmdOk(`${results.join('\n')}\n${summary}`);
},
},

'config-ensure-section': {
name: 'config-ensure-section',
description: 'Create a top-level config section if it does not already exist.',
Expand Down
42 changes: 21 additions & 21 deletions packages/cli/src/commands/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ export const GITHUB_COMMANDS: CommandRegistry = {

'post-comment': {
name: 'post-comment',
description: 'Post a comment on an issue. Usage: post-comment --issue-number 216 --body "text" [--body-file /path] [--type plan]',
description: 'Post a comment on an issue. Usage: post-comment --issue-number 216 --body "text" [--body-file /path] [--type plan] [--plan-number 1]',
async handler(args) {
let issueNumber: number;
try {
Expand All @@ -413,8 +413,13 @@ export const GITHUB_COMMANDS: CommandRegistry = {
}

const commentType = getFlag(args, '--type');
const planNumber = getIntFlag(args, '--plan-number');
if (commentType) {
const header = formatCommentHeader({ type: commentType as Parameters<typeof formatCommentHeader>[0]['type'] });
const meta: Record<string, unknown> = { type: commentType };
if (planNumber !== undefined && !Number.isNaN(planNumber)) {
meta.plan = planNumber;
}
const header = formatCommentHeader(meta as Parameters<typeof formatCommentHeader>[0]);
body = `${header}\n${body}`;
}
Comment on lines 415 to 424
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--plan-number is parsed even when --type is not provided, but it has no effect because the metadata header is only prepended inside if (commentType). Consider either (a) erroring when --plan-number is provided without --type, or (b) treating --plan-number as implying --type plan, to avoid silently ignoring the flag.

Copilot uses AI. Check for mistakes.

Expand Down Expand Up @@ -573,7 +578,7 @@ export const GITHUB_COMMANDS: CommandRegistry = {

'delete-comments': {
name: 'delete-comments',
description: 'Delete comments of a given type from an issue. Usage: delete-comments --issue-number 216 --type plan',
description: 'Delete comments of a given type from an issue. Supports comma-separated types. Usage: delete-comments --issue-number 216 --type plan,context,research',
async handler(args) {
let issueNumber: number;
try {
Expand All @@ -584,26 +589,30 @@ export const GITHUB_COMMANDS: CommandRegistry = {
return cmdErr((e as Error).message);
}

let type: string;
let typeRaw: string;
try {
type = getRequiredFlag(args, '--type');
typeRaw = getRequiredFlag(args, '--type');
} catch (e) {
return cmdErr((e as Error).message);
}

// Support comma-separated types (e.g. "plan,context,research")
const types = new Set(typeRaw.split(',').map((t) => t.trim()).filter(Boolean));
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If --type is provided as only commas/whitespace (e.g. --type ",,"), types becomes empty and the command reports Deleted 0 [] comments... rather than treating it as invalid input. Consider validating types.size > 0 and returning an error when no valid types are parsed.

Suggested change
const types = new Set(typeRaw.split(',').map((t) => t.trim()).filter(Boolean));
const types = new Set(typeRaw.split(',').map((t) => t.trim()).filter(Boolean));
if (types.size === 0) {
return cmdErr('--type must contain at least one non-empty comment type');
}

Copilot uses AI. Check for mistakes.

const commentsResult = await listComments(issueNumber);
if (!commentsResult.ok) return cmdErr(commentsResult.error);

const matching = commentsResult.data.filter(
(c) => parseCommentMeta(c.body)?.type === type,
);
const matching = commentsResult.data.filter((c) => {
const meta = parseCommentMeta(c.body);
return meta ? types.has(meta.type) : false;
});

for (const comment of matching) {
const deleteResult = await deleteComment(comment.id);
if (!deleteResult.ok) return cmdErr(deleteResult.error);
}

return cmdOk(`Deleted ${matching.length} ${type} comments from #${issueNumber}`);
return cmdOk(`Deleted ${matching.length} [${[...types].join(',')}] comments from #${issueNumber}`);
},
},

Expand Down Expand Up @@ -684,15 +693,6 @@ export const GITHUB_COMMANDS: CommandRegistry = {
},
},

'set-status': {
name: 'set-status',
description: 'Set an issue status on the project board (alias for move-issue). Usage: set-status --issue-number 216 --status "Done"',
async handler(args) {
// Delegate to move-issue handler
return GITHUB_COMMANDS['move-issue'].handler(args);
},
},

// ── Task 3.1: GitHub status diagnostic ──────────────────────────────

'status': {
Expand Down Expand Up @@ -1041,13 +1041,13 @@ export const GITHUB_COMMANDS: CommandRegistry = {
name: 'all-progress',
description: 'Show adaptive progress for all phase issues with detail levels and progress bar. Usage: all-progress',
async handler(_args) {
// Fetch all phase issues (label is 'maxsim:phase' per actual GitHub data)
const result = await listIssues({ labels: 'maxsim:phase', state: 'all' });
// Fetch all phase issues by canonical label (type:phase per types.ts)
const result = await listIssues({ labels: 'type:phase', state: 'all' });
if (!result.ok) return cmdErr(result.error);

const phases = result.data;
if (phases.length === 0) {
return cmdOk('No phase issues found (label: maxsim:phase)');
return cmdOk('No phase issues found (label: type:phase)');
}

// Extract phase number from title (e.g., "Phase 2: ..." → 2) for sorting
Expand Down
Loading
Loading