@grantx/fleet-cli 0.1.3 → 0.1.5
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/package.json +2 -2
- package/src/agent-templates.js +354 -0
- package/src/agent-wizard.js +228 -0
- package/src/github-repos.js +159 -0
- package/src/init.js +209 -119
- package/src/prompt-utils.js +127 -0
- package/src/setup-agents.js +6 -3
- package/src/slack-setup.js +153 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// prompt-utils.js — Shared readline prompt helpers for the fleet wizard.
|
|
2
|
+
// All wizard phases import from here for consistent UX.
|
|
3
|
+
|
|
4
|
+
import readline from 'node:readline';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create a readline interface for interactive prompts.
|
|
8
|
+
*/
|
|
9
|
+
export function createReadline() {
|
|
10
|
+
return readline.createInterface({
|
|
11
|
+
input: process.stdin,
|
|
12
|
+
output: process.stdout,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Ask a single-line question with optional default.
|
|
18
|
+
*/
|
|
19
|
+
export function ask(rl, question, defaultValue = '') {
|
|
20
|
+
const prompt = defaultValue
|
|
21
|
+
? ` ${question} [${defaultValue}]: `
|
|
22
|
+
: ` ${question}: `;
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
rl.question(prompt, (answer) => {
|
|
25
|
+
resolve(answer.trim() || defaultValue);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Ask a required question (loops until non-empty).
|
|
32
|
+
*/
|
|
33
|
+
export async function askRequired(rl, question) {
|
|
34
|
+
while (true) {
|
|
35
|
+
const answer = await ask(rl, question);
|
|
36
|
+
if (answer) return answer;
|
|
37
|
+
console.log(' (required)');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Ask a yes/no question.
|
|
43
|
+
*/
|
|
44
|
+
export async function askYesNo(rl, question, defaultYes = true) {
|
|
45
|
+
const hint = defaultYes ? 'Y/n' : 'y/N';
|
|
46
|
+
const answer = await ask(rl, `${question} [${hint}]`);
|
|
47
|
+
if (!answer) return defaultYes;
|
|
48
|
+
return answer.toLowerCase().startsWith('y');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Ask for a numbered list selection.
|
|
53
|
+
* @param {readline.Interface} rl
|
|
54
|
+
* @param {string} question - Header question
|
|
55
|
+
* @param {Array<{label: string, description: string, value: any}>} options
|
|
56
|
+
* @returns {any} The selected option's value
|
|
57
|
+
*/
|
|
58
|
+
export async function askList(rl, question, options) {
|
|
59
|
+
console.log(`\n ${question}`);
|
|
60
|
+
for (let i = 0; i < options.length; i++) {
|
|
61
|
+
const opt = options[i];
|
|
62
|
+
console.log(` ${i + 1}. ${opt.label.padEnd(12)} — ${opt.description}`);
|
|
63
|
+
}
|
|
64
|
+
while (true) {
|
|
65
|
+
const answer = await ask(rl, `Choice [1-${options.length}]`);
|
|
66
|
+
const idx = parseInt(answer, 10) - 1;
|
|
67
|
+
if (idx >= 0 && idx < options.length) {
|
|
68
|
+
return options[idx].value;
|
|
69
|
+
}
|
|
70
|
+
console.log(` (enter 1-${options.length})`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Ask for comma-separated values.
|
|
76
|
+
*/
|
|
77
|
+
export async function askCommaSeparated(rl, question) {
|
|
78
|
+
const answer = await ask(rl, question);
|
|
79
|
+
if (!answer) return [];
|
|
80
|
+
return answer.split(',').map(s => s.trim()).filter(Boolean);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Ask for multi-line input (one per line, blank to finish).
|
|
85
|
+
*/
|
|
86
|
+
export async function askMultiline(rl, question, hint = 'blank line to finish') {
|
|
87
|
+
console.log(` ${question} (${hint}):`);
|
|
88
|
+
const lines = [];
|
|
89
|
+
while (true) {
|
|
90
|
+
const line = await ask(rl, '>');
|
|
91
|
+
if (!line) break;
|
|
92
|
+
lines.push(line);
|
|
93
|
+
}
|
|
94
|
+
return lines;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Ask for a secret (masked display).
|
|
99
|
+
*/
|
|
100
|
+
export async function askSecret(rl, question) {
|
|
101
|
+
const answer = await askRequired(rl, question);
|
|
102
|
+
return answer;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Print a section header.
|
|
107
|
+
*/
|
|
108
|
+
export function printHeader(title) {
|
|
109
|
+
console.log(`\n ${title}`);
|
|
110
|
+
console.log(` ${'─'.repeat(title.length)}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Print a progress step.
|
|
115
|
+
*/
|
|
116
|
+
export function printStep(step, total, label, value) {
|
|
117
|
+
const stepStr = `[${step}/${total}]`;
|
|
118
|
+
const dots = '.'.repeat(Math.max(2, 30 - label.length));
|
|
119
|
+
console.log(` ${stepStr} ${label} ${dots} ${value}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Check if running in interactive mode (TTY available).
|
|
124
|
+
*/
|
|
125
|
+
export function isInteractive() {
|
|
126
|
+
return process.stdin.isTTY === true;
|
|
127
|
+
}
|
package/src/setup-agents.js
CHANGED
|
@@ -28,9 +28,12 @@ export function setupAgentWorkspaces(projectRoot, roster) {
|
|
|
28
28
|
// Copy/symlink credentials
|
|
29
29
|
setupCredentials(agentClaudeDir);
|
|
30
30
|
|
|
31
|
-
// Write
|
|
32
|
-
const
|
|
33
|
-
fs.
|
|
31
|
+
// Write basic CLAUDE.md (caller may overwrite with a richer version)
|
|
32
|
+
const claudeMdPath = path.join(agentDir, 'CLAUDE.md');
|
|
33
|
+
if (!fs.existsSync(claudeMdPath)) {
|
|
34
|
+
const claudeMd = generateAgentClaudeMd(agent, projectRoot);
|
|
35
|
+
fs.writeFileSync(claudeMdPath, claudeMd);
|
|
36
|
+
}
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
// Initialize sessions.json
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// slack-setup.js — Guided Slack bot creation walkthrough for fleet setup.
|
|
2
|
+
// Walks users through creating a Slack app, installing it, and collecting tokens.
|
|
3
|
+
|
|
4
|
+
import { ask, askRequired, askYesNo, printHeader } from './prompt-utils.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Run the Slack bot setup walkthrough.
|
|
8
|
+
* @param {readline.Interface} rl
|
|
9
|
+
* @param {string} teamId - Team ID for app naming
|
|
10
|
+
* @returns {Promise<object|null>} Slack config or null if skipped
|
|
11
|
+
*/
|
|
12
|
+
export async function runSlackSetup(rl, teamId) {
|
|
13
|
+
printHeader('Slack Integration (optional)');
|
|
14
|
+
|
|
15
|
+
const setup = await askYesNo(rl, 'Set up a Slack bot for fleet notifications?', true);
|
|
16
|
+
if (!setup) {
|
|
17
|
+
console.log(' Skipped. You can set this up later in fleet.config.json.\n');
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Step 1: Create the app
|
|
22
|
+
console.log(`
|
|
23
|
+
Step 1: Create a Slack App
|
|
24
|
+
--------------------------
|
|
25
|
+
1. Go to https://api.slack.com/apps
|
|
26
|
+
2. Click "Create New App" → "From a manifest"
|
|
27
|
+
3. Select your workspace
|
|
28
|
+
4. Paste this manifest:
|
|
29
|
+
`);
|
|
30
|
+
|
|
31
|
+
const manifest = generateSlackManifest(teamId);
|
|
32
|
+
console.log(manifest);
|
|
33
|
+
|
|
34
|
+
await ask(rl, 'Press Enter when the app is created...');
|
|
35
|
+
|
|
36
|
+
// Step 2: Install
|
|
37
|
+
console.log(`
|
|
38
|
+
Step 2: Install to Workspace
|
|
39
|
+
----------------------------
|
|
40
|
+
1. In your Slack app settings, click "Install to Workspace"
|
|
41
|
+
2. Authorize the requested permissions
|
|
42
|
+
`);
|
|
43
|
+
|
|
44
|
+
await ask(rl, 'Press Enter when installed...');
|
|
45
|
+
|
|
46
|
+
// Step 3: Tokens
|
|
47
|
+
console.log(`
|
|
48
|
+
Step 3: Collect Tokens
|
|
49
|
+
----------------------
|
|
50
|
+
1. Go to "OAuth & Permissions" → copy "Bot User OAuth Token" (xoxb-...)
|
|
51
|
+
2. Go to "Basic Information" → "App-Level Tokens" → Generate one with
|
|
52
|
+
connections:write scope (this is your xapp- token)
|
|
53
|
+
`);
|
|
54
|
+
|
|
55
|
+
const botToken = await askRequired(rl, 'Bot Token (xoxb-...)');
|
|
56
|
+
if (!botToken.startsWith('xoxb-')) {
|
|
57
|
+
console.log(' ⚠ Token should start with xoxb- — proceeding anyway.');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const appToken = await askRequired(rl, 'App Token (xapp-...)');
|
|
61
|
+
if (!appToken.startsWith('xapp-')) {
|
|
62
|
+
console.log(' ⚠ Token should start with xapp- — proceeding anyway.');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Step 4: Channels
|
|
66
|
+
console.log(`
|
|
67
|
+
Step 4: Create Channels
|
|
68
|
+
-----------------------
|
|
69
|
+
Create two channels in your Slack workspace:
|
|
70
|
+
#fleet-decisions — Agent dispatch decisions and completions
|
|
71
|
+
#fleet-general — General fleet status and check-ins
|
|
72
|
+
|
|
73
|
+
Then get their channel IDs:
|
|
74
|
+
Right-click channel → "View channel details" → scroll to bottom → copy ID
|
|
75
|
+
`);
|
|
76
|
+
|
|
77
|
+
const decisionsChannel = await askRequired(rl, 'Decisions channel ID (starts with C)');
|
|
78
|
+
const generalChannel = await askRequired(rl, 'General channel ID (starts with C)');
|
|
79
|
+
|
|
80
|
+
// Test connection
|
|
81
|
+
console.log('\n Testing connection...');
|
|
82
|
+
const testResult = await testSlackConnection(botToken, generalChannel);
|
|
83
|
+
if (testResult.ok) {
|
|
84
|
+
console.log(' ✓ Posted test message to fleet-general');
|
|
85
|
+
} else {
|
|
86
|
+
console.log(` ⚠ Test failed: ${testResult.error}`);
|
|
87
|
+
console.log(' Check that the bot is invited to the channel and tokens are correct.');
|
|
88
|
+
console.log(' Configuration saved anyway — you can fix tokens in fleet.config.json.');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log('');
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
botToken,
|
|
95
|
+
appToken,
|
|
96
|
+
channels: {
|
|
97
|
+
decisions: decisionsChannel,
|
|
98
|
+
general: generalChannel,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Helpers ───────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Generate a Slack app manifest YAML.
|
|
107
|
+
*/
|
|
108
|
+
function generateSlackManifest(teamId) {
|
|
109
|
+
return ` ---
|
|
110
|
+
display_information:
|
|
111
|
+
name: "Fleet Bot (${teamId})"
|
|
112
|
+
description: "Agent fleet orchestration notifications"
|
|
113
|
+
features:
|
|
114
|
+
bot_user:
|
|
115
|
+
display_name: "Fleet Bot"
|
|
116
|
+
always_online: true
|
|
117
|
+
oauth_config:
|
|
118
|
+
scopes:
|
|
119
|
+
bot:
|
|
120
|
+
- chat:write
|
|
121
|
+
- channels:read
|
|
122
|
+
- channels:history
|
|
123
|
+
- users:read
|
|
124
|
+
settings:
|
|
125
|
+
socket_mode_enabled: true
|
|
126
|
+
org_deploy_enabled: false
|
|
127
|
+
token_rotation_enabled: false
|
|
128
|
+
---`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Test Slack connection by posting a message.
|
|
133
|
+
*/
|
|
134
|
+
async function testSlackConnection(botToken, channelId) {
|
|
135
|
+
try {
|
|
136
|
+
const res = await fetch('https://slack.com/api/chat.postMessage', {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
headers: {
|
|
139
|
+
'Authorization': `Bearer ${botToken}`,
|
|
140
|
+
'Content-Type': 'application/json',
|
|
141
|
+
},
|
|
142
|
+
body: JSON.stringify({
|
|
143
|
+
channel: channelId,
|
|
144
|
+
text: '✓ Fleet bot connected.',
|
|
145
|
+
}),
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const data = await res.json();
|
|
149
|
+
return { ok: data.ok, error: data.error || null };
|
|
150
|
+
} catch (err) {
|
|
151
|
+
return { ok: false, error: err.message };
|
|
152
|
+
}
|
|
153
|
+
}
|