ai-control-center 1.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +584 -0
- package/bin/aicc.js +772 -0
- package/lib/actions/approve.js +71 -0
- package/lib/actions/assign-project.js +132 -0
- package/lib/actions/browser-test.js +64 -0
- package/lib/actions/cleanup.js +174 -0
- package/lib/actions/debug.js +298 -0
- package/lib/actions/deploy.js +1229 -0
- package/lib/actions/fix-bug.js +134 -0
- package/lib/actions/new-feature.js +255 -0
- package/lib/actions/reject.js +307 -0
- package/lib/actions/review.js +706 -0
- package/lib/actions/status.js +47 -0
- package/lib/agents/browser-qa-agent.js +611 -0
- package/lib/agents/payment-agent.js +116 -0
- package/lib/agents/suggestion-agent.js +88 -0
- package/lib/cli.js +303 -0
- package/lib/config.js +243 -0
- package/lib/hub/hub-server.js +440 -0
- package/lib/hub/project-poller.js +75 -0
- package/lib/hub/skill-registry.js +89 -0
- package/lib/hub/state-aggregator.js +204 -0
- package/lib/index.js +471 -0
- package/lib/init/doctor.js +523 -0
- package/lib/init/presets.js +222 -0
- package/lib/init/skill-fetcher.js +77 -0
- package/lib/init/wizard.js +973 -0
- package/lib/integrations/codex-runner.js +128 -0
- package/lib/integrations/github-actions.js +248 -0
- package/lib/integrations/github-reporter.js +229 -0
- package/lib/integrations/screenshot-store.js +102 -0
- package/lib/openclaw/bridge.js +650 -0
- package/lib/openclaw/generate-skill.js +235 -0
- package/lib/openclaw/openclaw.json +64 -0
- package/lib/orchestrator/autonomous-loop.js +429 -0
- package/lib/orchestrator/thread-triggers.js +63 -0
- package/lib/roleplay/agent-messenger.js +75 -0
- package/lib/roleplay/discussion-threads.js +303 -0
- package/lib/roleplay/health-monitor.js +121 -0
- package/lib/roleplay/pm-agent.js +513 -0
- package/lib/roleplay/roleplay-config.js +25 -0
- package/lib/roleplay/room.js +164 -0
- package/lib/shared/action-runner.js +2330 -0
- package/lib/shared/event-bus.js +185 -0
- package/lib/slack/bot.js +378 -0
- package/lib/telegram/bot.js +416 -0
- package/lib/telegram/commands.js +1267 -0
- package/lib/telegram/keyboards.js +113 -0
- package/lib/telegram/notifications.js +247 -0
- package/lib/twitch/bot.js +354 -0
- package/lib/twitch/commands.js +302 -0
- package/lib/twitch/notifications.js +63 -0
- package/lib/utils/achievements.js +191 -0
- package/lib/utils/activity-log.js +182 -0
- package/lib/utils/agent-leaderboard.js +119 -0
- package/lib/utils/audit-logger.js +232 -0
- package/lib/utils/codebase-context.js +288 -0
- package/lib/utils/codebase-indexer.js +381 -0
- package/lib/utils/config-schema.js +230 -0
- package/lib/utils/context-compressor.js +172 -0
- package/lib/utils/correlation.js +63 -0
- package/lib/utils/cost-tracker.js +423 -0
- package/lib/utils/cron-scheduler.js +53 -0
- package/lib/utils/db-adapter.js +293 -0
- package/lib/utils/display.js +272 -0
- package/lib/utils/errors.js +116 -0
- package/lib/utils/format.js +134 -0
- package/lib/utils/intent-engine.js +464 -0
- package/lib/utils/mcp-client.js +238 -0
- package/lib/utils/model-ab-test.js +164 -0
- package/lib/utils/notify.js +122 -0
- package/lib/utils/persona-loader.js +80 -0
- package/lib/utils/pipeline-lock.js +73 -0
- package/lib/utils/pipeline.js +214 -0
- package/lib/utils/plugin-runner.js +234 -0
- package/lib/utils/rate-limiter.js +84 -0
- package/lib/utils/rbac.js +74 -0
- package/lib/utils/runner.js +1809 -0
- package/lib/utils/security.js +191 -0
- package/lib/utils/self-healer.js +144 -0
- package/lib/utils/skill-loader.js +255 -0
- package/lib/utils/spinner.js +132 -0
- package/lib/utils/stage-queue.js +50 -0
- package/lib/utils/state-machine.js +89 -0
- package/lib/utils/status-bar.js +327 -0
- package/lib/utils/token-estimator.js +101 -0
- package/lib/utils/ux-analyzer.js +101 -0
- package/lib/utils/webhook-emitter.js +83 -0
- package/lib/web/public/css/styles.css +417 -0
- package/lib/web/public/dark-mode.js +44 -0
- package/lib/web/public/hub/kanban.html +206 -0
- package/lib/web/public/index.html +45 -0
- package/lib/web/public/js/app.js +71 -0
- package/lib/web/public/js/ask.js +110 -0
- package/lib/web/public/js/dashboard.js +165 -0
- package/lib/web/public/js/deploy.js +72 -0
- package/lib/web/public/js/feature.js +79 -0
- package/lib/web/public/js/health.js +65 -0
- package/lib/web/public/js/logs.js +93 -0
- package/lib/web/public/js/review.js +123 -0
- package/lib/web/public/js/ws-client.js +82 -0
- package/lib/web/public/office/css/office.css +678 -0
- package/lib/web/public/office/index.html +148 -0
- package/lib/web/public/office/js/achievements-ui.js +117 -0
- package/lib/web/public/office/js/character.js +1056 -0
- package/lib/web/public/office/js/chat-bubbles.js +177 -0
- package/lib/web/public/office/js/cost-overlay.js +123 -0
- package/lib/web/public/office/js/day-night.js +68 -0
- package/lib/web/public/office/js/effects.js +632 -0
- package/lib/web/public/office/js/engine.js +146 -0
- package/lib/web/public/office/js/feature-ticket.js +216 -0
- package/lib/web/public/office/js/hub-client.js +60 -0
- package/lib/web/public/office/js/main.js +1757 -0
- package/lib/web/public/office/js/office-layout.js +1524 -0
- package/lib/web/public/office/js/pathfinding.js +144 -0
- package/lib/web/public/office/js/pixel-sprites.js +1454 -0
- package/lib/web/public/office/js/progress-bars.js +117 -0
- package/lib/web/public/office/js/replay.js +191 -0
- package/lib/web/public/office/js/sound-effects.js +91 -0
- package/lib/web/public/office/js/sprite-renderer.js +211 -0
- package/lib/web/public/office/js/stamina-system.js +89 -0
- package/lib/web/public/office/js/ui.js +107 -0
- package/lib/web/public/onboarding/index.html +243 -0
- package/lib/web/public/timeline/index.html +195 -0
- package/lib/web/routes/api.js +499 -0
- package/lib/web/routes/logs.js +20 -0
- package/lib/web/routes/metrics.js +99 -0
- package/lib/web/server.js +183 -0
- package/lib/web/ws/handler.js +65 -0
- package/package.json +67 -0
- package/templates/agent-architect.md +69 -0
- package/templates/agent-gemini-pm.md +49 -0
- package/templates/agent-gemini-reviewer.md +52 -0
- package/templates/copilot-instructions.md +36 -0
- package/templates/pipelines/mobile.json +27 -0
- package/templates/pipelines/nodejs-api.json +27 -0
- package/templates/pipelines/python.json +27 -0
- package/templates/pipelines/react.json +27 -0
- package/templates/pipelines/salesforce.json +27 -0
- package/templates/role-gemini.md +97 -0
- package/templates/skill-architect.md +114 -0
- package/templates/skill-browser-qa.md +50 -0
- package/templates/skill-bug-from-qa.md +58 -0
- package/templates/skill-chatbot.md +93 -0
- package/templates/skill-implement.md +78 -0
- package/templates/skill-openclaw.md +174 -0
- package/templates/skill-payment.md +110 -0
- package/templates/skill-pm-spec.md +77 -0
- package/templates/skill-requirement-capture.md +97 -0
- package/templates/skill-review.md +108 -0
- package/templates/skill-reviewer-qa.md +44 -0
- package/templates/skill-suggestion.md +45 -0
- package/templates/skill-template.md +142 -0
|
@@ -0,0 +1,973 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `aicc init` — interactive project setup wizard.
|
|
3
|
+
*
|
|
4
|
+
* Guides the user through:
|
|
5
|
+
* 1. Project configuration (name, deploy command, review settings)
|
|
6
|
+
* 2. Tool verification (git, AI CLIs, deployment tools)
|
|
7
|
+
* 3. AI CLI authentication checks
|
|
8
|
+
* 4. Telegram bot setup (optional)
|
|
9
|
+
* 5. AI mode configuration
|
|
10
|
+
* 6. AI pipeline files (skills, agents, role files)
|
|
11
|
+
*
|
|
12
|
+
* Writes aicc.config.js + creates workflow directories + status.json +
|
|
13
|
+
* skill files + agent definitions + GEMINI.md + copilot-instructions.md.
|
|
14
|
+
*/
|
|
15
|
+
import chalk from 'chalk';
|
|
16
|
+
import { execSync, spawn, spawnSync } from 'child_process';
|
|
17
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
18
|
+
import inquirer from 'inquirer';
|
|
19
|
+
import { dirname, resolve } from 'path';
|
|
20
|
+
import { fileURLToPath } from 'url';
|
|
21
|
+
import { PROJECT_PRESETS, PROJECT_TYPE_CHOICES } from './presets.js';
|
|
22
|
+
import { fetchSkills, printFetchResults } from './skill-fetcher.js';
|
|
23
|
+
|
|
24
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
|
|
26
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
function checkTool(name) {
|
|
29
|
+
try {
|
|
30
|
+
const path = execSync(`which ${name} 2>/dev/null`, { encoding: 'utf8' }).trim();
|
|
31
|
+
return { ok: true, path };
|
|
32
|
+
} catch {
|
|
33
|
+
return { ok: false, path: null };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function checkToolVersion(cmd) {
|
|
38
|
+
try {
|
|
39
|
+
return execSync(cmd, { encoding: 'utf8', timeout: 5000 }).trim().split('\n')[0];
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function icon(ok) {
|
|
46
|
+
return ok ? chalk.green('✓') : chalk.red('✗');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── Phase 1: Project Configuration ─────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
async function phaseProjectConfig(cwd) {
|
|
52
|
+
console.log(chalk.cyan.bold('\n ── Phase 1: Project Configuration ──\n'));
|
|
53
|
+
|
|
54
|
+
// ── Project type selector — drives default values for everything below
|
|
55
|
+
const { projectType } = await inquirer.prompt([{
|
|
56
|
+
type: 'list',
|
|
57
|
+
name: 'projectType',
|
|
58
|
+
message: 'What type of project is this?',
|
|
59
|
+
choices: PROJECT_TYPE_CHOICES,
|
|
60
|
+
pageSize: 16,
|
|
61
|
+
}]);
|
|
62
|
+
|
|
63
|
+
const preset = PROJECT_PRESETS[projectType] || PROJECT_PRESETS.custom;
|
|
64
|
+
const presetExts = preset.extensions.join(', ');
|
|
65
|
+
|
|
66
|
+
const answers = await inquirer.prompt([
|
|
67
|
+
{
|
|
68
|
+
type: 'input',
|
|
69
|
+
name: 'name',
|
|
70
|
+
message: 'Project name:',
|
|
71
|
+
default: cwd.split('/').pop() || 'my-project',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
type: 'input',
|
|
75
|
+
name: 'description',
|
|
76
|
+
message: 'Short description:',
|
|
77
|
+
default: '',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
type: 'input',
|
|
81
|
+
name: 'workflowDir',
|
|
82
|
+
message: 'Workflow directory:',
|
|
83
|
+
default: '.ai-workflow',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
type: 'input',
|
|
87
|
+
name: 'envPrefix',
|
|
88
|
+
message: 'Environment variable prefix:',
|
|
89
|
+
default: (a) => a.name.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase().slice(0, 12),
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
type: 'input',
|
|
93
|
+
name: 'deployCommand',
|
|
94
|
+
message: 'Deploy command (leave empty if none):',
|
|
95
|
+
default: preset.deployCommand || '',
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
type: 'input',
|
|
99
|
+
name: 'extensions',
|
|
100
|
+
message: 'Reviewable file extensions (comma-separated):',
|
|
101
|
+
default: presetExts,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
type: 'input',
|
|
105
|
+
name: 'sourceDir',
|
|
106
|
+
message: 'Source directory for diffs/reviews:',
|
|
107
|
+
default: preset.sourceDir || 'src/',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
type: 'input',
|
|
111
|
+
name: 'skillsDir',
|
|
112
|
+
message: 'AI skills directory:',
|
|
113
|
+
default: '.claude/skills',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
type: 'input',
|
|
117
|
+
name: 'webPort',
|
|
118
|
+
message: 'Web dashboard port:',
|
|
119
|
+
default: '3847',
|
|
120
|
+
},
|
|
121
|
+
]);
|
|
122
|
+
|
|
123
|
+
// Attach preset metadata for use in later phases
|
|
124
|
+
answers.projectType = projectType;
|
|
125
|
+
answers._preset = preset;
|
|
126
|
+
|
|
127
|
+
return answers;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ─── Phase 2: Tool Verification ─────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
async function phaseToolCheck(envPrefix) {
|
|
133
|
+
console.log(chalk.cyan.bold('\n ── Phase 2: Tool Verification ──\n'));
|
|
134
|
+
|
|
135
|
+
const results = {};
|
|
136
|
+
const tools = [
|
|
137
|
+
{ name: 'git', label: 'Git', required: true, versionCmd: 'git --version' },
|
|
138
|
+
{ name: 'gh', label: 'GitHub CLI', required: false, versionCmd: 'gh --version' },
|
|
139
|
+
{ name: 'claude', label: 'Claude Code CLI', required: false, versionCmd: 'claude --version' },
|
|
140
|
+
{ name: 'copilot', label: 'GitHub Copilot CLI', required: false, versionCmd: 'copilot --version' },
|
|
141
|
+
{ name: 'gemini', label: 'Gemini CLI', required: false, versionCmd: 'gemini --version' },
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
for (const tool of tools) {
|
|
145
|
+
const check = checkTool(tool.name);
|
|
146
|
+
const version = check.ok ? checkToolVersion(tool.versionCmd) : null;
|
|
147
|
+
const reqTag = tool.required ? chalk.red(' (required)') : chalk.dim(' (optional)');
|
|
148
|
+
const versionStr = version ? chalk.dim(` — ${version}`) : '';
|
|
149
|
+
|
|
150
|
+
console.log(` ${icon(check.ok)} ${tool.label}${reqTag}${versionStr}`);
|
|
151
|
+
results[tool.name] = check;
|
|
152
|
+
|
|
153
|
+
if (tool.required && !check.ok) {
|
|
154
|
+
console.log(chalk.red(`\n ${tool.label} is required. Please install it and re-run "aicc init".\n`));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Check Ollama (HTTP-based, not a CLI binary)
|
|
159
|
+
let ollamaAvailable = false;
|
|
160
|
+
try {
|
|
161
|
+
execSync('curl -sf http://localhost:11434/api/tags > /dev/null 2>&1', { timeout: 3000 });
|
|
162
|
+
ollamaAvailable = true;
|
|
163
|
+
} catch { /* not running */ }
|
|
164
|
+
console.log(` ${icon(ollamaAvailable)} Ollama (local AI)${chalk.dim(' (optional)')}`);
|
|
165
|
+
results.ollama = { ok: ollamaAvailable };
|
|
166
|
+
|
|
167
|
+
return results;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ─── Phase 3: Authentication & Credentials ──────────────────────────────────
|
|
171
|
+
|
|
172
|
+
async function phaseAuth(toolResults, envPrefix) {
|
|
173
|
+
console.log(chalk.cyan.bold('\n ── Phase 3: Authentication & Credentials ──\n'));
|
|
174
|
+
const setupActions = [];
|
|
175
|
+
|
|
176
|
+
// Git: check if repo initialized + remote configured
|
|
177
|
+
if (toolResults.git?.ok) {
|
|
178
|
+
let gitOk = false;
|
|
179
|
+
try {
|
|
180
|
+
execSync('git rev-parse --is-inside-work-tree 2>/dev/null', { encoding: 'utf8' });
|
|
181
|
+
gitOk = true;
|
|
182
|
+
} catch { /* not a git repo */ }
|
|
183
|
+
|
|
184
|
+
if (gitOk) {
|
|
185
|
+
let hasRemote = false;
|
|
186
|
+
try {
|
|
187
|
+
const remote = execSync('git remote get-url origin 2>/dev/null', { encoding: 'utf8' }).trim();
|
|
188
|
+
hasRemote = !!remote;
|
|
189
|
+
console.log(` ${icon(true)} Git remote: ${chalk.dim(remote)}`);
|
|
190
|
+
} catch { /* no remote */ }
|
|
191
|
+
|
|
192
|
+
if (!hasRemote) {
|
|
193
|
+
console.log(` ${icon(false)} Git remote not configured`);
|
|
194
|
+
setupActions.push(` Run: ${chalk.bold('git remote add origin <your-repo-url>')}`);
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
console.log(` ${icon(false)} Not a git repository`);
|
|
198
|
+
setupActions.push(` Run: ${chalk.bold('git init')} to initialize`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// GitHub CLI: check auth status
|
|
203
|
+
if (toolResults.gh?.ok) {
|
|
204
|
+
try {
|
|
205
|
+
const ghStatus = execSync('gh auth status 2>&1', { encoding: 'utf8', timeout: 5000 });
|
|
206
|
+
const loggedIn = ghStatus.includes('Logged in');
|
|
207
|
+
console.log(` ${icon(loggedIn)} GitHub CLI auth`);
|
|
208
|
+
if (!loggedIn) {
|
|
209
|
+
setupActions.push(` Run: ${chalk.bold('gh auth login')}`);
|
|
210
|
+
}
|
|
211
|
+
} catch {
|
|
212
|
+
console.log(` ${icon(false)} GitHub CLI auth`);
|
|
213
|
+
setupActions.push(` Run: ${chalk.bold('gh auth login')}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Claude: check if authenticated
|
|
218
|
+
if (toolResults.claude?.ok) {
|
|
219
|
+
// Claude Code doesn't have a simple auth-check command
|
|
220
|
+
// We just note that it needs to be run interactively once
|
|
221
|
+
console.log(` ${icon(true)} Claude Code CLI installed`);
|
|
222
|
+
console.log(chalk.dim(` Auth is handled by Claude Code on first run`));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Copilot: check if config exists
|
|
226
|
+
if (toolResults.copilot?.ok) {
|
|
227
|
+
const copilotConfig = resolve(process.env.HOME || '', '.copilot', 'config.json');
|
|
228
|
+
const hasConfig = existsSync(copilotConfig);
|
|
229
|
+
console.log(` ${icon(hasConfig)} Copilot CLI config`);
|
|
230
|
+
if (!hasConfig) {
|
|
231
|
+
setupActions.push(` Run: ${chalk.bold('copilot auth login')}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Gemini: check if credentials cached
|
|
236
|
+
if (toolResults.gemini?.ok) {
|
|
237
|
+
const geminiCache = resolve(process.env.HOME || '', '.gemini');
|
|
238
|
+
const hasCache = existsSync(geminiCache);
|
|
239
|
+
console.log(` ${icon(hasCache)} Gemini CLI credentials`);
|
|
240
|
+
if (!hasCache) {
|
|
241
|
+
setupActions.push(` Run: ${chalk.bold('gemini')} and follow the auth flow`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (setupActions.length) {
|
|
246
|
+
console.log(chalk.yellow('\n Actions needed:'));
|
|
247
|
+
setupActions.forEach(a => console.log(chalk.white(` ${a}`)));
|
|
248
|
+
} else {
|
|
249
|
+
console.log(chalk.green('\n All tools authenticated!'));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return setupActions;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ─── Phase 4: Telegram Setup ────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
async function phaseTelegram(envPrefix, cwd) {
|
|
258
|
+
console.log(chalk.cyan.bold('\n ── Phase 4: Telegram Bot (optional) ──\n'));
|
|
259
|
+
|
|
260
|
+
const { setupTelegram } = await inquirer.prompt([{
|
|
261
|
+
type: 'confirm',
|
|
262
|
+
name: 'setupTelegram',
|
|
263
|
+
message: 'Set up Telegram bot for remote pipeline control?',
|
|
264
|
+
default: false,
|
|
265
|
+
}]);
|
|
266
|
+
|
|
267
|
+
if (!setupTelegram) {
|
|
268
|
+
console.log(chalk.dim(' Skipped. You can set this up later.\n'));
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
console.log('');
|
|
273
|
+
console.log(chalk.dim(' To get a bot token:'));
|
|
274
|
+
console.log(chalk.dim(' 1. Open Telegram and talk to @BotFather'));
|
|
275
|
+
console.log(chalk.dim(' 2. Send /newbot and follow the prompts'));
|
|
276
|
+
console.log(chalk.dim(' 3. Copy the token you receive'));
|
|
277
|
+
console.log('');
|
|
278
|
+
|
|
279
|
+
const telegram = await inquirer.prompt([
|
|
280
|
+
{
|
|
281
|
+
type: 'input',
|
|
282
|
+
name: 'token',
|
|
283
|
+
message: `Telegram bot token (${envPrefix}_TELEGRAM_TOKEN):`,
|
|
284
|
+
default: '',
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
type: 'input',
|
|
288
|
+
name: 'chatId',
|
|
289
|
+
message: `Telegram chat ID for notifications (${envPrefix}_TELEGRAM_CHAT_ID):`,
|
|
290
|
+
default: '',
|
|
291
|
+
when: (a) => a.token,
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
type: 'input',
|
|
295
|
+
name: 'allowedIds',
|
|
296
|
+
message: `Allowed user IDs, comma-separated (${envPrefix}_TELEGRAM_ALLOWED_IDS):`,
|
|
297
|
+
default: '',
|
|
298
|
+
when: (a) => a.token,
|
|
299
|
+
},
|
|
300
|
+
]);
|
|
301
|
+
|
|
302
|
+
if (!telegram.token) {
|
|
303
|
+
console.log(chalk.dim(' No token provided. Skipping.\n'));
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Write to .env file
|
|
308
|
+
const envLines = [`${envPrefix}_TELEGRAM_TOKEN=${telegram.token}`];
|
|
309
|
+
if (telegram.chatId) envLines.push(`${envPrefix}_TELEGRAM_CHAT_ID=${telegram.chatId}`);
|
|
310
|
+
if (telegram.allowedIds) envLines.push(`${envPrefix}_TELEGRAM_ALLOWED_IDS=${telegram.allowedIds}`);
|
|
311
|
+
|
|
312
|
+
const envPath = resolve(cwd, '.env');
|
|
313
|
+
const envContent = '\n# AI Control Center — Telegram Bot\n' + envLines.join('\n') + '\n';
|
|
314
|
+
|
|
315
|
+
if (existsSync(envPath)) {
|
|
316
|
+
const current = readFileSync(envPath, 'utf8');
|
|
317
|
+
if (!current.includes(`${envPrefix}_TELEGRAM_TOKEN`)) {
|
|
318
|
+
appendFileSync(envPath, envContent);
|
|
319
|
+
console.log(chalk.green(` ✓ Appended Telegram config to .env`));
|
|
320
|
+
} else {
|
|
321
|
+
console.log(chalk.yellow(` ~ ${envPrefix}_TELEGRAM_TOKEN already exists in .env`));
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
writeFileSync(envPath, envContent.trimStart());
|
|
325
|
+
console.log(chalk.green(` ✓ Created .env with Telegram config`));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Ensure .env is in .gitignore
|
|
329
|
+
const gitignorePath = resolve(cwd, '.gitignore');
|
|
330
|
+
if (existsSync(gitignorePath)) {
|
|
331
|
+
const gitignore = readFileSync(gitignorePath, 'utf8');
|
|
332
|
+
if (!gitignore.includes('.env')) {
|
|
333
|
+
appendFileSync(gitignorePath, '\n# Environment variables (secrets)\n.env\n');
|
|
334
|
+
console.log(chalk.green(` ✓ Added .env to .gitignore`));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return telegram;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ─── Phase 4b: Twitch Setup ──────────────────────────────────────────────────
|
|
342
|
+
|
|
343
|
+
async function phaseTwitch(envPrefix, cwd) {
|
|
344
|
+
console.log(chalk.cyan.bold('\n ── Phase 4b: Twitch Bot (optional) ──\n'));
|
|
345
|
+
|
|
346
|
+
const { setupTwitch } = await inquirer.prompt([{
|
|
347
|
+
type: 'confirm',
|
|
348
|
+
name: 'setupTwitch',
|
|
349
|
+
message: 'Set up Twitch bot for remote pipeline control?',
|
|
350
|
+
default: false,
|
|
351
|
+
}]);
|
|
352
|
+
|
|
353
|
+
if (!setupTwitch) {
|
|
354
|
+
console.log(chalk.dim(' Skipped. You can set this up later.\n'));
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
console.log('');
|
|
359
|
+
console.log(chalk.dim(' To get credentials:'));
|
|
360
|
+
console.log(chalk.dim(' 1. Create an app at https://dev.twitch.tv/console'));
|
|
361
|
+
console.log(chalk.dim(' 2. Generate an OAuth token with chat:read and chat:edit scopes'));
|
|
362
|
+
console.log(chalk.dim(' 3. Find your Twitch user ID at https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/'));
|
|
363
|
+
console.log('');
|
|
364
|
+
|
|
365
|
+
const twitch = await inquirer.prompt([
|
|
366
|
+
{
|
|
367
|
+
type: 'input',
|
|
368
|
+
name: 'channel',
|
|
369
|
+
message: `Twitch channel to join (${envPrefix}_TWITCH_CHANNEL):`,
|
|
370
|
+
default: '',
|
|
371
|
+
validate: (v) => {
|
|
372
|
+
if (!v || !v.trim()) return true; // optional — skip validation if empty
|
|
373
|
+
if (/\s/.test(v.trim())) return 'Channel name cannot contain spaces';
|
|
374
|
+
return true;
|
|
375
|
+
},
|
|
376
|
+
filter: (v) => v.trim().toLowerCase().replace(/^#/, ''), // normalize: strip # and lowercase
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
type: 'input',
|
|
380
|
+
name: 'accessToken',
|
|
381
|
+
message: `OAuth access token (${envPrefix}_TWITCH_ACCESS_TOKEN):`,
|
|
382
|
+
default: '',
|
|
383
|
+
when: (a) => !!a.channel,
|
|
384
|
+
validate: (v) => {
|
|
385
|
+
if (!v || !v.trim()) return 'Access token is required to run the bot';
|
|
386
|
+
return true;
|
|
387
|
+
},
|
|
388
|
+
filter: (v) => v.trim().replace(/^oauth:/, ''), // strip oauth: prefix if user pastes full token
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
type: 'input',
|
|
392
|
+
name: 'clientId',
|
|
393
|
+
message: `Client ID (${envPrefix}_TWITCH_CLIENT_ID):`,
|
|
394
|
+
default: '',
|
|
395
|
+
when: (a) => !!a.accessToken,
|
|
396
|
+
filter: (v) => v.trim(),
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
type: 'input',
|
|
400
|
+
name: 'clientSecret',
|
|
401
|
+
message: `Client secret (${envPrefix}_TWITCH_CLIENT_SECRET, optional):`,
|
|
402
|
+
default: '',
|
|
403
|
+
when: (a) => !!a.clientId,
|
|
404
|
+
filter: (v) => v.trim(),
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
type: 'input',
|
|
408
|
+
name: 'allowedIds',
|
|
409
|
+
message: `Allowed Twitch user IDs, comma-separated (${envPrefix}_TWITCH_ALLOWED_IDS):`,
|
|
410
|
+
default: '',
|
|
411
|
+
when: (a) => !!a.accessToken,
|
|
412
|
+
validate: (v) => {
|
|
413
|
+
if (!v || !v.trim()) return true; // optional
|
|
414
|
+
const ids = v.split(',').map(s => s.trim()).filter(Boolean);
|
|
415
|
+
if (ids.some(id => !/^\d+$/.test(id))) {
|
|
416
|
+
return 'Twitch user IDs must be numeric (e.g. 12345678). Find yours at id.twitch.tv/oauth2/validate';
|
|
417
|
+
}
|
|
418
|
+
return true;
|
|
419
|
+
},
|
|
420
|
+
filter: (v) => v.split(',').map(s => s.trim()).filter(Boolean).join(','),
|
|
421
|
+
},
|
|
422
|
+
]);
|
|
423
|
+
|
|
424
|
+
if (!twitch.channel || !twitch.accessToken) {
|
|
425
|
+
console.log(chalk.dim(' No channel or token provided. Skipping.\n'));
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Write to .env file
|
|
430
|
+
const envLines = [
|
|
431
|
+
`${envPrefix}_TWITCH_CHANNEL=${twitch.channel}`,
|
|
432
|
+
`${envPrefix}_TWITCH_ACCESS_TOKEN=${twitch.accessToken}`,
|
|
433
|
+
];
|
|
434
|
+
if (twitch.clientId) envLines.push(`${envPrefix}_TWITCH_CLIENT_ID=${twitch.clientId}`);
|
|
435
|
+
if (twitch.clientSecret) envLines.push(`${envPrefix}_TWITCH_CLIENT_SECRET=${twitch.clientSecret}`);
|
|
436
|
+
if (twitch.allowedIds) envLines.push(`${envPrefix}_TWITCH_ALLOWED_IDS=${twitch.allowedIds}`);
|
|
437
|
+
|
|
438
|
+
const envPath = resolve(cwd, '.env');
|
|
439
|
+
const envContent = '\n# AI Control Center — Twitch Bot\n' + envLines.join('\n') + '\n';
|
|
440
|
+
|
|
441
|
+
if (existsSync(envPath)) {
|
|
442
|
+
const current = readFileSync(envPath, 'utf8');
|
|
443
|
+
if (!current.includes(`${envPrefix}_TWITCH_CHANNEL`)) {
|
|
444
|
+
appendFileSync(envPath, envContent);
|
|
445
|
+
console.log(chalk.green(` ✓ Appended Twitch config to .env`));
|
|
446
|
+
} else {
|
|
447
|
+
console.log(chalk.yellow(` ~ ${envPrefix}_TWITCH_CHANNEL already exists in .env`));
|
|
448
|
+
}
|
|
449
|
+
} else {
|
|
450
|
+
writeFileSync(envPath, envContent.trimStart());
|
|
451
|
+
console.log(chalk.green(` ✓ Created .env with Twitch config`));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Ensure .env is in .gitignore
|
|
455
|
+
const gitignorePath = resolve(cwd, '.gitignore');
|
|
456
|
+
if (existsSync(gitignorePath)) {
|
|
457
|
+
const gitignore = readFileSync(gitignorePath, 'utf8');
|
|
458
|
+
if (!gitignore.includes('.env')) {
|
|
459
|
+
appendFileSync(gitignorePath, '\n# Environment variables (secrets)\n.env\n');
|
|
460
|
+
console.log(chalk.green(` ✓ Added .env to .gitignore`));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
console.log(chalk.green(`\n ✓ Twitch bot configured. Run: aicc twitch\n`));
|
|
465
|
+
return twitch;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ─── Phase 5: AI Mode Configuration ────────────────────────────────────────
|
|
469
|
+
|
|
470
|
+
async function phaseAIMode(envPrefix, toolResults, cwd) {
|
|
471
|
+
console.log(chalk.cyan.bold('\n ── Phase 5: AI Mode Configuration ──\n'));
|
|
472
|
+
|
|
473
|
+
const choices = [];
|
|
474
|
+
if (toolResults.ollama?.ok) {
|
|
475
|
+
choices.push({ name: 'Hybrid — Ollama for light tasks, cloud AI for heavy (recommended)', value: 'hybrid' });
|
|
476
|
+
choices.push({ name: 'Local only — Ollama only, no cloud fallback', value: 'local' });
|
|
477
|
+
}
|
|
478
|
+
choices.push({ name: 'Cloud only — Use cloud AI CLIs for everything', value: 'cloud' });
|
|
479
|
+
|
|
480
|
+
const { aiMode } = await inquirer.prompt([{
|
|
481
|
+
type: 'list',
|
|
482
|
+
name: 'aiMode',
|
|
483
|
+
message: 'AI execution mode:',
|
|
484
|
+
choices,
|
|
485
|
+
default: toolResults.ollama?.ok ? 'hybrid' : 'cloud',
|
|
486
|
+
}]);
|
|
487
|
+
|
|
488
|
+
// Write AI_MODE to .env
|
|
489
|
+
const envPath = resolve(cwd, '.env');
|
|
490
|
+
const envLine = `\n# AI execution mode (hybrid | cloud | local)\n${envPrefix}_AI_MODE=${aiMode}\n`;
|
|
491
|
+
|
|
492
|
+
if (existsSync(envPath)) {
|
|
493
|
+
const current = readFileSync(envPath, 'utf8');
|
|
494
|
+
if (!current.includes(`${envPrefix}_AI_MODE`)) {
|
|
495
|
+
appendFileSync(envPath, envLine);
|
|
496
|
+
}
|
|
497
|
+
} else {
|
|
498
|
+
writeFileSync(envPath, envLine.trimStart());
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
console.log(chalk.green(` ✓ AI mode set to: ${aiMode}`));
|
|
502
|
+
return aiMode;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// ─── Phase 6: AI Pipeline Files (skills, agents, roles) ─────────────────────
|
|
506
|
+
|
|
507
|
+
function loadTemplate(name) {
|
|
508
|
+
const templateDir = resolve(__dirname, '../../templates');
|
|
509
|
+
return readFileSync(resolve(templateDir, name), 'utf8');
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function renderTemplate(content, vars) {
|
|
513
|
+
return content
|
|
514
|
+
.replace(/\{\{PROJECT_NAME\}\}/g, vars.name)
|
|
515
|
+
.replace(/\{\{PROJECT_DESCRIPTION\}\}/g, vars.description || `A project managed by AI Control Center.`)
|
|
516
|
+
.replace(/\{\{DESCRIPTION\}\}/g, vars.description || `A project managed by AI Control Center.`)
|
|
517
|
+
.replace(/\{\{PROJECT_ROOT\}\}/g, vars.root || process.cwd())
|
|
518
|
+
.replace(/\{\{WORKFLOW_DIR\}\}/g, vars.workflowDir || '.ai-workflow');
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function writeIfMissing(filePath, content, label) {
|
|
522
|
+
if (existsSync(filePath)) {
|
|
523
|
+
console.log(chalk.dim(` · ${label} (already exists)`));
|
|
524
|
+
return false;
|
|
525
|
+
}
|
|
526
|
+
const dir = resolve(filePath, '..');
|
|
527
|
+
mkdirSync(dir, { recursive: true });
|
|
528
|
+
writeFileSync(filePath, content);
|
|
529
|
+
console.log(chalk.green(` ✓ ${label}`));
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async function phaseAIPipeline(answers, toolResults, cwd) {
|
|
534
|
+
console.log(chalk.cyan.bold('\n ── Phase 6: AI Pipeline Files ──\n'));
|
|
535
|
+
|
|
536
|
+
console.log(chalk.dim(' The pipeline needs skill files, agent definitions, and role files'));
|
|
537
|
+
console.log(chalk.dim(' to instruct each AI tool (Claude, Copilot, Gemini) what to do.\n'));
|
|
538
|
+
|
|
539
|
+
const { setupFiles } = await inquirer.prompt([{
|
|
540
|
+
type: 'confirm',
|
|
541
|
+
name: 'setupFiles',
|
|
542
|
+
message: 'Create AI pipeline files (skills, agents, role files)?',
|
|
543
|
+
default: true,
|
|
544
|
+
}]);
|
|
545
|
+
|
|
546
|
+
if (!setupFiles) {
|
|
547
|
+
console.log(chalk.dim(' Skipped. You can create these manually later.\n'));
|
|
548
|
+
return { skillsCreated: false };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const vars = { name: answers.name, description: answers.description, root: cwd, workflowDir: answers.workflowDir };
|
|
552
|
+
const skillsDir = resolve(cwd, answers.skillsDir);
|
|
553
|
+
const agentsDir = resolve(cwd, '.claude/agents');
|
|
554
|
+
const githubDir = resolve(cwd, '.github');
|
|
555
|
+
let filesCreated = 0;
|
|
556
|
+
|
|
557
|
+
// ── Pipeline skills (auto-created, no user input needed)
|
|
558
|
+
// These are internal prompt templates for each AI stage in the pipeline.
|
|
559
|
+
const skillNames = {
|
|
560
|
+
architect: 'architect',
|
|
561
|
+
implement: 'copilot-implement',
|
|
562
|
+
review: 'code-review',
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
console.log(chalk.white('\n Pipeline skills (prompt templates for each AI stage):'));
|
|
566
|
+
|
|
567
|
+
if (writeIfMissing(
|
|
568
|
+
resolve(skillsDir, `${skillNames.architect}.md`),
|
|
569
|
+
renderTemplate(loadTemplate('skill-architect.md'), vars),
|
|
570
|
+
`${answers.skillsDir}/${skillNames.architect}.md ${chalk.dim('← Claude architect prompt')}`
|
|
571
|
+
)) filesCreated++;
|
|
572
|
+
|
|
573
|
+
if (writeIfMissing(
|
|
574
|
+
resolve(skillsDir, `${skillNames.implement}.md`),
|
|
575
|
+
renderTemplate(loadTemplate('skill-implement.md'), vars),
|
|
576
|
+
`${answers.skillsDir}/${skillNames.implement}.md ${chalk.dim('← Copilot implementation prompt')}`
|
|
577
|
+
)) filesCreated++;
|
|
578
|
+
|
|
579
|
+
if (writeIfMissing(
|
|
580
|
+
resolve(skillsDir, `${skillNames.review}.md`),
|
|
581
|
+
renderTemplate(loadTemplate('skill-review.md'), vars),
|
|
582
|
+
`${answers.skillsDir}/${skillNames.review}.md ${chalk.dim('← Gemini code review prompt')}`
|
|
583
|
+
)) filesCreated++;
|
|
584
|
+
|
|
585
|
+
// ── Chatbot skill (AI's "employee manual" for Telegram/Web natural conversation)
|
|
586
|
+
console.log(chalk.white('\n Chatbot skill (natural conversation + intent detection):'));
|
|
587
|
+
|
|
588
|
+
if (writeIfMissing(
|
|
589
|
+
resolve(skillsDir, 'chatbot.md'),
|
|
590
|
+
renderTemplate(loadTemplate('skill-chatbot.md'), vars),
|
|
591
|
+
`${answers.skillsDir}/chatbot.md ${chalk.dim('← AI chatbot behavior rules')}`
|
|
592
|
+
)) filesCreated++;
|
|
593
|
+
|
|
594
|
+
// ── OpenClaw skill (for openclaw gateway integration)
|
|
595
|
+
console.log(chalk.white('\n OpenClaw skill (gateway AI integration):'));
|
|
596
|
+
|
|
597
|
+
const openclawSkillDir = resolve(cwd, '.openclaw/skills/aicc');
|
|
598
|
+
if (writeIfMissing(
|
|
599
|
+
resolve(openclawSkillDir, 'SKILL.md'),
|
|
600
|
+
renderTemplate(loadTemplate('skill-openclaw.md'), vars),
|
|
601
|
+
`.openclaw/skills/aicc/SKILL.md ${chalk.dim('← OpenClaw pipeline skill')}`
|
|
602
|
+
)) filesCreated++;
|
|
603
|
+
|
|
604
|
+
// ── Agent definitions (for Claude Code agent system)
|
|
605
|
+
console.log(chalk.white('\n Agents (Claude Code agent definitions):'));
|
|
606
|
+
|
|
607
|
+
if (writeIfMissing(
|
|
608
|
+
resolve(agentsDir, 'architect.agent.md'),
|
|
609
|
+
loadTemplate('agent-architect.md'),
|
|
610
|
+
`.claude/agents/architect.agent.md`
|
|
611
|
+
)) filesCreated++;
|
|
612
|
+
|
|
613
|
+
if (toolResults.gemini?.ok) {
|
|
614
|
+
if (writeIfMissing(
|
|
615
|
+
resolve(agentsDir, 'gemini-pm.agent.md'),
|
|
616
|
+
loadTemplate('agent-gemini-pm.md'),
|
|
617
|
+
`.claude/agents/gemini-pm.agent.md`
|
|
618
|
+
)) filesCreated++;
|
|
619
|
+
|
|
620
|
+
if (writeIfMissing(
|
|
621
|
+
resolve(agentsDir, 'gemini-reviewer.agent.md'),
|
|
622
|
+
loadTemplate('agent-gemini-reviewer.md'),
|
|
623
|
+
`.claude/agents/gemini-reviewer.agent.md`
|
|
624
|
+
)) filesCreated++;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// ── Role files (loaded at runtime by pipeline actions)
|
|
628
|
+
console.log(chalk.white('\n Role files (AI instructions):'));
|
|
629
|
+
|
|
630
|
+
if (toolResults.gemini?.ok || toolResults.claude?.ok) {
|
|
631
|
+
if (writeIfMissing(
|
|
632
|
+
resolve(cwd, 'GEMINI.md'),
|
|
633
|
+
renderTemplate(loadTemplate('role-gemini.md'), vars),
|
|
634
|
+
`GEMINI.md (Gemini PM + Reviewer role)`
|
|
635
|
+
)) filesCreated++;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (toolResults.copilot?.ok) {
|
|
639
|
+
if (writeIfMissing(
|
|
640
|
+
resolve(githubDir, 'copilot-instructions.md'),
|
|
641
|
+
renderTemplate(loadTemplate('copilot-instructions.md'), vars),
|
|
642
|
+
`.github/copilot-instructions.md`
|
|
643
|
+
)) filesCreated++;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// ── Fetch technology-specific skills from antigravity-awesome-skills ──
|
|
647
|
+
const preset = answers._preset;
|
|
648
|
+
let fetchedTechSkills = [];
|
|
649
|
+
if (preset && preset.skills && preset.skills.length > 0) {
|
|
650
|
+
console.log(chalk.white('\n Technology skills (from antigravity-awesome-skills):'));
|
|
651
|
+
const results = await fetchSkills(preset.skills, skillsDir);
|
|
652
|
+
printFetchResults(results);
|
|
653
|
+
fetchedTechSkills = results.filter(r => r.status === 'ok' || r.status === 'exists').map(r => r.name);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// ── Additional workflow subdirectories
|
|
657
|
+
const extraDirs = ['inbox', 'implementation-notes', 'deploy-fixes', 'sessions', 'rejected'];
|
|
658
|
+
const workflowDir = resolve(cwd, answers.workflowDir);
|
|
659
|
+
for (const sub of extraDirs) {
|
|
660
|
+
mkdirSync(resolve(workflowDir, sub), { recursive: true });
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
console.log(chalk.green(`\n ✓ ${filesCreated} files created`));
|
|
664
|
+
|
|
665
|
+
return { skillsCreated: true, skillNames, techSkills: fetchedTechSkills };
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// ─── Main: Run All Phases ───────────────────────────────────────────────────
|
|
669
|
+
|
|
670
|
+
export async function runInit() {
|
|
671
|
+
const cwd = process.cwd();
|
|
672
|
+
const configPath = resolve(cwd, 'aicc.config.js');
|
|
673
|
+
|
|
674
|
+
console.log('');
|
|
675
|
+
console.log(chalk.cyan.bold(' ◆ AI Control Center — Project Setup'));
|
|
676
|
+
console.log(chalk.dim(' ' + '─'.repeat(50)));
|
|
677
|
+
console.log('');
|
|
678
|
+
|
|
679
|
+
// Warn if config already exists
|
|
680
|
+
if (existsSync(configPath)) {
|
|
681
|
+
const { overwrite } = await inquirer.prompt([{
|
|
682
|
+
type: 'confirm',
|
|
683
|
+
name: 'overwrite',
|
|
684
|
+
message: 'aicc.config.js already exists. Overwrite?',
|
|
685
|
+
default: false,
|
|
686
|
+
}]);
|
|
687
|
+
if (!overwrite) {
|
|
688
|
+
console.log(chalk.yellow('\n Aborted.\n'));
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// ── Phase 1: Project config
|
|
694
|
+
const answers = await phaseProjectConfig(cwd);
|
|
695
|
+
|
|
696
|
+
// ── Phase 2: Tool verification
|
|
697
|
+
const toolResults = await phaseToolCheck(answers.envPrefix);
|
|
698
|
+
|
|
699
|
+
// ── Phase 3: Auth & credentials
|
|
700
|
+
const authActions = await phaseAuth(toolResults, answers.envPrefix);
|
|
701
|
+
|
|
702
|
+
// ── Phase 4: Telegram (optional)
|
|
703
|
+
const telegram = await phaseTelegram(answers.envPrefix, cwd);
|
|
704
|
+
|
|
705
|
+
// ── Phase 4b: Twitch (optional)
|
|
706
|
+
const twitch = await phaseTwitch(answers.envPrefix, cwd);
|
|
707
|
+
|
|
708
|
+
// ── Phase 5: AI mode
|
|
709
|
+
const aiMode = await phaseAIMode(answers.envPrefix, toolResults, cwd);
|
|
710
|
+
|
|
711
|
+
// ── Phase 6: AI pipeline files
|
|
712
|
+
const pipeline = await phaseAIPipeline(answers, toolResults, cwd);
|
|
713
|
+
|
|
714
|
+
// ── Write aicc.config.js ──────────────────────────────────────────────────
|
|
715
|
+
|
|
716
|
+
console.log(chalk.cyan.bold('\n ── Writing Configuration ──\n'));
|
|
717
|
+
|
|
718
|
+
const extensions = answers.extensions
|
|
719
|
+
.split(',')
|
|
720
|
+
.map(e => e.trim())
|
|
721
|
+
.filter(Boolean)
|
|
722
|
+
.map(e => `'${e}'`)
|
|
723
|
+
.join(', ');
|
|
724
|
+
|
|
725
|
+
const preset = answers._preset || PROJECT_PRESETS.custom;
|
|
726
|
+
const aiSystemStr = preset.aiSystem
|
|
727
|
+
? preset.aiSystem(answers.name).replace(/'/g, "\\'")
|
|
728
|
+
: `You are a concise DevOps assistant for ${answers.name}.`;
|
|
729
|
+
|
|
730
|
+
// Deploy block — include preset options if available
|
|
731
|
+
let deployBlock;
|
|
732
|
+
if (answers.deployCommand) {
|
|
733
|
+
const deployOpts = preset.deployOptions || [];
|
|
734
|
+
if (deployOpts.length > 0) {
|
|
735
|
+
const optsStr = deployOpts.map(o => ` { label: '${o.label}', value: '${o.value}' },`).join('\n');
|
|
736
|
+
deployBlock = ` deploy: {\n command: '${answers.deployCommand}',\n options: [\n${optsStr}\n ],\n },\n`;
|
|
737
|
+
} else {
|
|
738
|
+
deployBlock = ` deploy: {\n command: '${answers.deployCommand}',\n options: [],\n },\n`;
|
|
739
|
+
}
|
|
740
|
+
} else {
|
|
741
|
+
deployBlock = ` deploy: { command: null, options: [] },\n`;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Debug commands from preset
|
|
745
|
+
const debugCmds = preset.debugCommands || [];
|
|
746
|
+
const debugBlock = debugCmds.length > 0
|
|
747
|
+
? ` debug: {\n commands: [\n${debugCmds.map(c => ` { name: '${c.name}', command: '${c.command}' },`).join('\n')}\n ],\n },`
|
|
748
|
+
: ` debug: { commands: [] },`;
|
|
749
|
+
|
|
750
|
+
// Tech skills list
|
|
751
|
+
const techSkillsList = pipeline.techSkills || [];
|
|
752
|
+
const techSkillsStr = techSkillsList.length > 0
|
|
753
|
+
? `\n techSkills: [${techSkillsList.map(s => `'${s}'`).join(', ')}],`
|
|
754
|
+
: '';
|
|
755
|
+
|
|
756
|
+
const configContent = `export default {
|
|
757
|
+
name: '${answers.name}',
|
|
758
|
+
description: '${answers.description.replace(/'/g, "\\'")}',
|
|
759
|
+
projectType: '${answers.projectType}',
|
|
760
|
+
workflowDir: '${answers.workflowDir}',
|
|
761
|
+
skillsDir: '${answers.skillsDir}',
|
|
762
|
+
envPrefix: '${answers.envPrefix}',
|
|
763
|
+
|
|
764
|
+
stages: {
|
|
765
|
+
idle: { label: 'Idle', color: 'gray' },
|
|
766
|
+
inbox: { label: 'Feature Submitted', color: 'yellow' },
|
|
767
|
+
spec_complete: { label: 'PM Spec Ready', color: 'cyan' },
|
|
768
|
+
arch_complete: { label: 'Arch Ready', color: 'blue' },
|
|
769
|
+
implementation_complete: { label: 'Code Done', color: 'magenta' },
|
|
770
|
+
review_complete: { label: 'Review Ready', color: 'green' },
|
|
771
|
+
approved: { label: 'Approved', color: 'green' },
|
|
772
|
+
rejected: { label: 'Rejected', color: 'red' },
|
|
773
|
+
deployed: { label: 'Deployed', color: 'green' },
|
|
774
|
+
},
|
|
775
|
+
|
|
776
|
+
${deployBlock}
|
|
777
|
+
review: {
|
|
778
|
+
extensions: [${extensions}],
|
|
779
|
+
sourceDir: '${answers.sourceDir}',
|
|
780
|
+
},
|
|
781
|
+
|
|
782
|
+
ai: {
|
|
783
|
+
system: '${aiSystemStr}',
|
|
784
|
+
roleFile: ${pipeline.skillsCreated ? "'GEMINI.md'" : 'null'},${pipeline.skillNames ? `
|
|
785
|
+
architectSkill: '${pipeline.skillNames.architect}',
|
|
786
|
+
implementSkill: '${pipeline.skillNames.implement}',
|
|
787
|
+
reviewSkill: '${pipeline.skillNames.review}',` : ''}${techSkillsStr}
|
|
788
|
+
},
|
|
789
|
+
|
|
790
|
+
queries: [],
|
|
791
|
+
${debugBlock}
|
|
792
|
+
web: { port: ${parseInt(answers.webPort, 10) || 3847} },
|
|
793
|
+
|
|
794
|
+
// ─── Pipeline — which AI model runs each stage ─────────────────────────
|
|
795
|
+
// Each line = one stage. Change provider/model to switch instantly.
|
|
796
|
+
// provider: 'openclaw' | 'copilot' | 'claude' | 'gemini' | 'ollama'
|
|
797
|
+
// fallbacks: backup 'provider:model' pairs tried if primary fails
|
|
798
|
+
// Override at runtime: CLAUDE_MODEL=xxx GEMINI_MODEL=xxx aicc ...
|
|
799
|
+
pipeline: [
|
|
800
|
+
{ stage: 'chat', provider: 'openclaw', model: 'auto', fallbacks: ['copilot:claude-haiku-4.5'] },
|
|
801
|
+
{ stage: 'pm', provider: 'copilot', model: 'claude-sonnet-4.6', fallbacks: ['gemini:gemini-2.5-pro', 'ollama'] },
|
|
802
|
+
{ stage: 'architect', provider: 'copilot', model: 'claude-sonnet-4.6', fallbacks: ['gemini:gemini-2.5-pro', 'ollama'] },
|
|
803
|
+
{ stage: 'implement', provider: 'claude', model: 'claude-sonnet-4-6', fallbacks: ['copilot:claude-sonnet-4.6', 'ollama'] },
|
|
804
|
+
{ stage: 'review', provider: 'copilot', model: 'claude-sonnet-4.6', fallbacks: ['gemini:gemini-2.5-pro', 'ollama'] },
|
|
805
|
+
{ stage: 'deploy', provider: 'copilot', model: 'claude-sonnet-4.6', fallbacks: ['gemini:gemini-2.5-pro', 'ollama'] },
|
|
806
|
+
],
|
|
807
|
+
|
|
808
|
+
// Ollama local fallback settings
|
|
809
|
+
ollama: { model: 'llama3.1', baseUrl: 'http://localhost:11434' },
|
|
810
|
+
plugins: [],
|
|
811
|
+
|
|
812
|
+
// ─── Roleplay / Multi-agent chat ─────────────────────────────────────────
|
|
813
|
+
// Enable the PM agent group chat in Telegram, Slack, and the web dashboard.
|
|
814
|
+
// The PM agent posts progress updates, handles errors, and asks for decisions.
|
|
815
|
+
roleplay: {
|
|
816
|
+
enabled: true,
|
|
817
|
+
verbosity: 'normal', // 'minimal' | 'normal' | 'verbose'
|
|
818
|
+
pmPersonality: 'professional', // 'professional' | 'casual' | 'technical'
|
|
819
|
+
agentChat: true, // Show agent-to-agent messages in the room
|
|
820
|
+
progressUpdates: true, // PM posts "still working..." during long stages
|
|
821
|
+
progressInterval: 60000, // How often to post progress updates (ms)
|
|
822
|
+
maxReviewCycles: 3, // After N review cycles, PM auto-approves minor issues
|
|
823
|
+
autoDeployOnApproval: false, // Auto-deploy when review is approved
|
|
824
|
+
},
|
|
825
|
+
};
|
|
826
|
+
`;
|
|
827
|
+
|
|
828
|
+
writeFileSync(configPath, configContent);
|
|
829
|
+
console.log(chalk.green(` ✓ Created aicc.config.js`));
|
|
830
|
+
|
|
831
|
+
// Ensure package.json has "type": "module" (suppresses Node warning for ESM config)
|
|
832
|
+
const pkgJsonPath = resolve(cwd, 'package.json');
|
|
833
|
+
if (existsSync(pkgJsonPath)) {
|
|
834
|
+
try {
|
|
835
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
|
|
836
|
+
if (!pkgJson.type) {
|
|
837
|
+
pkgJson.type = 'module';
|
|
838
|
+
writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + '\n');
|
|
839
|
+
console.log(chalk.green(` ✓ Added "type": "module" to package.json`));
|
|
840
|
+
}
|
|
841
|
+
} catch { /* skip if package.json is not valid JSON */ }
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Create workflow directory + status.json
|
|
845
|
+
const workflowDir = resolve(cwd, answers.workflowDir);
|
|
846
|
+
const subdirs = ['specs', 'architecture', 'tasks', 'reviews', 'approved', 'logs'];
|
|
847
|
+
|
|
848
|
+
mkdirSync(workflowDir, { recursive: true });
|
|
849
|
+
for (const sub of subdirs) {
|
|
850
|
+
mkdirSync(resolve(workflowDir, sub), { recursive: true });
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const statusFile = resolve(workflowDir, 'status.json');
|
|
854
|
+
if (!existsSync(statusFile)) {
|
|
855
|
+
writeFileSync(statusFile, JSON.stringify({
|
|
856
|
+
stage: 'idle',
|
|
857
|
+
current_feature: null,
|
|
858
|
+
pipeline_mode: 'manual',
|
|
859
|
+
}, null, 2));
|
|
860
|
+
}
|
|
861
|
+
console.log(chalk.green(` ✓ Created ${answers.workflowDir}/ with subdirectories`));
|
|
862
|
+
|
|
863
|
+
// Add logs to .gitignore
|
|
864
|
+
const gitignorePath = resolve(cwd, '.gitignore');
|
|
865
|
+
if (existsSync(gitignorePath)) {
|
|
866
|
+
const gitignore = readFileSync(gitignorePath, 'utf8');
|
|
867
|
+
if (!gitignore.includes(`${answers.workflowDir}/logs`)) {
|
|
868
|
+
appendFileSync(gitignorePath, `\n# AI workflow logs\n${answers.workflowDir}/logs/\n`);
|
|
869
|
+
console.log(chalk.green(` ✓ Added ${answers.workflowDir}/logs/ to .gitignore`));
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// ── Summary ─────────────────────────────────────────────────────────────────
|
|
874
|
+
|
|
875
|
+
console.log('');
|
|
876
|
+
console.log(chalk.dim(' ' + '─'.repeat(50)));
|
|
877
|
+
console.log(chalk.cyan.bold('\n ◆ Setup Complete!\n'));
|
|
878
|
+
|
|
879
|
+
const typeLabel = PROJECT_TYPE_CHOICES.find(c => c.value === answers.projectType)?.name || answers.projectType;
|
|
880
|
+
console.log(chalk.white(' Configuration:'));
|
|
881
|
+
console.log(chalk.dim(` Project: ${answers.name}`));
|
|
882
|
+
console.log(chalk.dim(` Type: ${typeLabel}`));
|
|
883
|
+
console.log(chalk.dim(` Prefix: ${answers.envPrefix}`));
|
|
884
|
+
console.log(chalk.dim(` AI Mode: ${aiMode}`));
|
|
885
|
+
console.log(chalk.dim(` Telegram: ${telegram?.token ? 'configured' : 'not configured'}`));
|
|
886
|
+
console.log(chalk.dim(` Web Port: ${answers.webPort}`));
|
|
887
|
+
|
|
888
|
+
if (pipeline.skillsCreated) {
|
|
889
|
+
console.log('');
|
|
890
|
+
console.log(chalk.white(' Generated Files:'));
|
|
891
|
+
console.log(chalk.dim(` Pipeline: ${answers.skillsDir}/architect.md`));
|
|
892
|
+
console.log(chalk.dim(` ${answers.skillsDir}/copilot-implement.md`));
|
|
893
|
+
console.log(chalk.dim(` ${answers.skillsDir}/code-review.md`));
|
|
894
|
+
if (pipeline.techSkills && pipeline.techSkills.length > 0) {
|
|
895
|
+
console.log(chalk.green(` Tech Skills: ${pipeline.techSkills.map(s => s + '.md').join(', ')}`));
|
|
896
|
+
console.log(chalk.dim(` (auto-downloaded from antigravity-awesome-skills)`));
|
|
897
|
+
console.log(chalk.dim(` AIs will follow these during architect/implement/review`));
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
if (authActions.length) {
|
|
902
|
+
console.log(chalk.yellow('\n Remaining setup:'));
|
|
903
|
+
authActions.forEach(a => console.log(chalk.white(` ${a}`)));
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// ── Auto-start services ──────────────────────────────────────────────────────
|
|
907
|
+
|
|
908
|
+
console.log(chalk.cyan('\n Starting services...'));
|
|
909
|
+
|
|
910
|
+
// 1. Generate OpenClaw skill (synchronous, quick)
|
|
911
|
+
try {
|
|
912
|
+
const openclawBin = spawnSync('which', ['openclaw'], { encoding: 'utf8' }).stdout.trim();
|
|
913
|
+
if (openclawBin) {
|
|
914
|
+
// Import and run generateSkill inline since config is not loaded yet via loadConfig()
|
|
915
|
+
// Instead, spawn aicc openclaw as a child so it loads config from the file we just wrote
|
|
916
|
+
const oc = spawnSync(process.execPath, [resolve(__dirname, '../../bin/aicc.js'), 'openclaw'], {
|
|
917
|
+
cwd, encoding: 'utf8', timeout: 15000,
|
|
918
|
+
});
|
|
919
|
+
if (oc.status === 0) {
|
|
920
|
+
console.log(chalk.green(' ✓ OpenClaw skill generated'));
|
|
921
|
+
} else {
|
|
922
|
+
console.log(chalk.yellow(' ~ OpenClaw skill generation skipped (openclaw gateway not running)'));
|
|
923
|
+
}
|
|
924
|
+
} else {
|
|
925
|
+
console.log(chalk.dim(' · OpenClaw CLI not installed — skipping'));
|
|
926
|
+
}
|
|
927
|
+
} catch {
|
|
928
|
+
console.log(chalk.dim(' · OpenClaw — skipped'));
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// 2. Start web dashboard as detached background process
|
|
932
|
+
const webPort = parseInt(answers.webPort, 10) || 3847;
|
|
933
|
+
try {
|
|
934
|
+
const webProc = spawn(process.execPath, [resolve(__dirname, '../../bin/aicc.js'), 'web'], {
|
|
935
|
+
cwd,
|
|
936
|
+
stdio: 'ignore',
|
|
937
|
+
detached: true,
|
|
938
|
+
env: { ...process.env },
|
|
939
|
+
});
|
|
940
|
+
webProc.unref();
|
|
941
|
+
console.log(chalk.green(` ✓ Web dashboard started → `) + chalk.cyan.bold(`http://localhost:${webPort}`));
|
|
942
|
+
} catch {
|
|
943
|
+
console.log(chalk.yellow(` ~ Web dashboard failed to start. Run ${chalk.bold('aicc web')} manually.`));
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// 3. Start Telegram bot as detached background process (if configured)
|
|
947
|
+
if (telegram?.token) {
|
|
948
|
+
try {
|
|
949
|
+
const tgProc = spawn(process.execPath, [resolve(__dirname, '../../bin/aicc.js'), 'telegram'], {
|
|
950
|
+
cwd,
|
|
951
|
+
stdio: 'ignore',
|
|
952
|
+
detached: true,
|
|
953
|
+
env: { ...process.env },
|
|
954
|
+
});
|
|
955
|
+
tgProc.unref();
|
|
956
|
+
console.log(chalk.green(' ✓ Telegram bot started'));
|
|
957
|
+
} catch {
|
|
958
|
+
console.log(chalk.yellow(` ~ Telegram bot failed to start. Run ${chalk.bold('aicc telegram')} manually.`));
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
console.log('');
|
|
963
|
+
console.log(chalk.dim(' ' + '─'.repeat(50)));
|
|
964
|
+
console.log(chalk.cyan('\n Quick reference:'));
|
|
965
|
+
console.log(chalk.white(` ${chalk.bold('aicc')} Launch terminal menu`));
|
|
966
|
+
console.log(chalk.white(` ${chalk.bold('aicc web')} Restart web dashboard`));
|
|
967
|
+
console.log(chalk.white(` ${chalk.bold('aicc status')} Check pipeline status`));
|
|
968
|
+
if (telegram?.token) {
|
|
969
|
+
console.log(chalk.white(` ${chalk.bold('aicc telegram')} Restart Telegram bot`));
|
|
970
|
+
}
|
|
971
|
+
console.log(chalk.white(` ${chalk.bold('aicc openclaw')} Regenerate OpenClaw skill`));
|
|
972
|
+
console.log('');
|
|
973
|
+
}
|