aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
341 lines (306 loc) • 12.3 kB
JavaScript
/**
* Feedback Command Handler
*
* Submit a bug report, feature request, or feedback to the AIWG GitHub repository.
* Prefills system context automatically (version, OS, Node, provider, frameworks).
*
* Usage:
* aiwg feedback # interactive (if TTY)
* aiwg feedback --type bug # skip type selection
* aiwg feedback --type feature # feature request
* aiwg feedback --type doc # documentation gap
* aiwg feedback --title "X" --body "Y" # fully non-interactive
* aiwg feedback --no-context # skip attaching system context
*
* Submission flow:
* 1. If `gh` CLI is available → `gh issue create --repo jmagly/aiwg`
* 2. Else → open browser with pre-filled GitHub issue URL
* 3. If no browser → print the formatted issue body to stdout
*
* @issue #885
*/
import { spawnSync } from 'child_process';
import os from 'os';
import { readAiwgConfig } from '../../config/aiwg-config.js';
import { createPromptInterface, askString, askChoice } from '../prompt-utils.js';
import { debug } from '../log.js';
// ── GitHub repo ───────────────────────────────────────────────────────────────
const GITHUB_REPO = 'jmagly/aiwg';
const GITHUB_ISSUES_URL = `https://github.com/${GITHUB_REPO}/issues/new`;
// ── Arg parsing ───────────────────────────────────────────────────────────────
function parseFeedbackArgs(args) {
let type;
let title;
let body;
let noContext = false;
for (let i = 0; i < args.length; i++) {
const a = args[i];
if (a === '--type' && args[i + 1]) {
const raw = args[++i];
if (['bug', 'feature', 'doc', 'other'].includes(raw)) {
type = raw;
}
}
else if (a === '--title' && args[i + 1]) {
title = args[++i];
}
else if (a === '--body' && args[i + 1]) {
body = args[++i];
}
else if (a === '--no-context') {
noContext = true;
}
}
return { type, title, body, noContext };
}
async function collectSystemContext(cwd) {
// aiwg version from package.json if available
let aiwgVersion = 'unknown';
try {
const r = spawnSync(process.execPath, [process.argv[1], 'version'], {
encoding: 'utf-8',
stdio: 'pipe',
});
const match = (r.stdout ?? '').match(/(\d{4}\.\d+\.\d+[^\s]*)/);
if (match)
aiwgVersion = match[1];
}
catch (err) {
// Feedback command degrades gracefully when version detection fails; log
// under AIWG_DEBUG=cli:feedback:* so the root cause is visible during
// bug reproduction.
debug('cli:feedback:version', 'version detect failed', err);
}
// Provider from project config
let provider = 'claude';
let frameworks = [];
try {
const config = await readAiwgConfig(cwd);
if (config) {
provider = config.providers[0] ?? 'claude';
frameworks = Object.keys(config.installed);
}
}
catch (err) {
debug('cli:feedback:config', 'config read failed', err);
}
return {
aiwgVersion,
nodeVersion: process.version,
platform: `${os.type()} ${os.release()} (${os.platform()})`,
arch: os.arch(),
provider,
frameworks,
shell: process.env.SHELL ?? process.env.COMSPEC ?? 'unknown',
};
}
function formatContext(ctx) {
return [
'**System context** (auto-collected by `aiwg feedback`):',
'',
`| Field | Value |`,
`|-------|-------|`,
`| aiwg version | \`${ctx.aiwgVersion}\` |`,
`| Node.js | \`${ctx.nodeVersion}\` |`,
`| OS | ${ctx.platform} |`,
`| Arch | ${ctx.arch} |`,
`| Provider | ${ctx.provider} |`,
`| Frameworks | ${ctx.frameworks.length > 0 ? ctx.frameworks.join(', ') : '_none_'} |`,
`| Shell | \`${ctx.shell}\` |`,
].join('\n');
}
// ── Issue templates ───────────────────────────────────────────────────────────
function bugTemplate(title, description, context) {
return `## Bug Report
**Title**: ${title}
## What happened
${description}
## Steps to reproduce
1.
2.
3.
## Expected behavior
<!-- What should have happened? -->
## Actual behavior
<!-- What actually happened? Paste error output here. -->
## Doctor output
<!-- Run \`aiwg doctor\` and paste the output here -->
---
${context}
`.trim();
}
function featureTemplate(title, description, context) {
return `## Feature Request
**Title**: ${title}
## Problem / motivation
${description}
## Proposed solution
<!-- How would you like this to work? -->
## Alternatives considered
<!-- Any alternative approaches? -->
---
${context}
`.trim();
}
function docTemplate(title, description, context) {
return `## Documentation Gap
**Title**: ${title}
## What is missing or wrong
${description}
## Where in the docs
<!-- Link to the relevant doc page or section, or paste the path -->
## What the docs should say
<!-- Your proposed correction or addition -->
---
${context}
`.trim();
}
function otherTemplate(title, description, context) {
return `## Feedback
**Title**: ${title}
${description}
---
${context}
`.trim();
}
function buildIssueBody(type, title, description, sysCtx) {
const context = sysCtx ? formatContext(sysCtx) : '_System context omitted_';
switch (type) {
case 'bug': return bugTemplate(title, description, context);
case 'feature': return featureTemplate(title, description, context);
case 'doc': return docTemplate(title, description, context);
default: return otherTemplate(title, description, context);
}
}
// ── Interactive prompts ───────────────────────────────────────────────────────
//
// Both prompts go through the shared prompt-utils wrapper so they inherit the
// AIWG_PROMPT_TIMEOUT_MS hard timeout (default 60s) and .unref()'d timer. A
// detached TTY can no longer hang the CLI on these prompts.
async function prompt(question, signal) {
const rl = createPromptInterface();
try {
return await askString(rl, question, '', signal);
}
finally {
rl.close();
}
}
async function promptSelect(question, options, signal) {
console.log(`\n${question}`);
options.forEach((opt, i) => console.log(` ${i + 1}. ${opt}`));
const rl = createPromptInterface();
try {
return await askChoice(rl, ' Choice: ', options, options[0], signal);
}
finally {
rl.close();
}
}
// ── Submission ────────────────────────────────────────────────────────────────
function hasGhCli() {
try {
const r = spawnSync('gh', ['--version'], { stdio: 'pipe' });
return r.status === 0;
}
catch {
return false;
}
}
function submitViaGh(title, body, type) {
const label = type === 'bug' ? 'bug' : type === 'feature' ? 'enhancement' : type === 'doc' ? 'documentation' : 'feedback';
const result = spawnSync('gh', ['issue', 'create', '--repo', GITHUB_REPO, '--title', title, '--body', body, '--label', label], { stdio: 'inherit' });
return result.status === 0;
}
function submitViaBrowser(title, body) {
const url = new URL(GITHUB_ISSUES_URL);
url.searchParams.set('title', title);
// GitHub truncates long URLs — include as much of the body as fits
const truncatedBody = body.length > 3000 ? body.slice(0, 2900) + '\n\n_[body truncated — paste full content manually]_' : body;
url.searchParams.set('body', truncatedBody);
const urlStr = url.toString();
try {
const openCmd = process.platform === 'darwin'
? 'open'
: process.platform === 'win32'
? 'start'
: 'xdg-open';
spawnSync(openCmd, [urlStr], { stdio: 'ignore' });
console.log(`\n Opened GitHub in your browser to file the issue.`);
console.log(` If the browser did not open, visit:\n ${urlStr}`);
}
catch {
console.log(`\n Could not open browser. Create the issue manually:\n ${urlStr}`);
}
}
function printIssue(title, body) {
console.log('\n── Issue body (copy-paste to GitHub) ──\n');
console.log(`Title: ${title}\n`);
console.log(body);
console.log(`\n── Submit at: ${GITHUB_ISSUES_URL} ──`);
}
// ── Handler ───────────────────────────────────────────────────────────────────
export const feedbackHandler = {
id: 'feedback',
name: 'Feedback',
description: 'Submit a bug report, feature request, or feedback to the AIWG GitHub repository',
category: 'utility',
aliases: ['report'],
async execute(ctx) {
const args = parseFeedbackArgs(ctx.args);
const cwd = ctx.cwd || process.cwd();
const isTTY = Boolean(process.stdin.isTTY);
// Collect system context
const sysCtx = args.noContext ? null : await collectSystemContext(cwd);
// ── Determine type ────────────────────────────────────────────
let type = args.type ?? 'other';
if (!args.type && isTTY) {
const choice = await promptSelect('What kind of feedback?', ['Bug report', 'Feature request', 'Documentation gap', 'Other'], ctx.signal);
if (choice.startsWith('Bug'))
type = 'bug';
else if (choice.startsWith('Feature'))
type = 'feature';
else if (choice.startsWith('Doc'))
type = 'doc';
else
type = 'other';
}
// ── Determine title ───────────────────────────────────────────
let title = args.title ?? '';
if (!title && isTTY) {
title = await prompt('\n Issue title (short phrase): ', ctx.signal);
}
if (!title) {
title = `[${type}] Issue from aiwg feedback`;
}
// ── Determine description ─────────────────────────────────────
let description = args.body ?? '';
if (!description && isTTY) {
description = await prompt('\n Describe the issue (press Enter when done):\n ', ctx.signal);
}
if (!description) {
description = '_No description provided._';
}
// ── Build issue body ──────────────────────────────────────────
const body = buildIssueBody(type, title, description, sysCtx);
// ── Submit ────────────────────────────────────────────────────
console.log(`\n Submitting ${type} report: "${title}"`);
if (hasGhCli()) {
const ok = submitViaGh(title, body, type);
if (ok) {
console.log('\n Issue filed successfully via `gh`.');
return { exitCode: 0 };
}
console.warn(' gh issue create failed — falling back to browser.');
}
// Try browser
if (isTTY) {
submitViaBrowser(title, body);
}
else {
printIssue(title, body);
}
return { exitCode: 0 };
},
};
//# sourceMappingURL=feedback.js.map