@grantx/fleet-cli 0.1.4 → 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.
@@ -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
+ }
@@ -28,9 +28,12 @@ export function setupAgentWorkspaces(projectRoot, roster) {
28
28
  // Copy/symlink credentials
29
29
  setupCredentials(agentClaudeDir);
30
30
 
31
- // Write per-agent CLAUDE.md
32
- const claudeMd = generateAgentClaudeMd(agent, projectRoot);
33
- fs.writeFileSync(path.join(agentDir, 'CLAUDE.md'), claudeMd);
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
+ }