@vrdmr/fnx-test 0.4.3 → 0.5.0

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.
@@ -14,16 +14,65 @@ let azuriteProcess = null;
14
14
  let weStartedAzurite = false;
15
15
 
16
16
  /**
17
- * Determine whether Azurite is needed based on AzureWebJobsStorage value.
18
- * Returns true for "UseDevelopmentStorage=true", empty string, or missing key.
17
+ * Check if a connection string value indicates development/emulator storage.
18
+ * Matches:
19
+ * - "UseDevelopmentStorage=true"
20
+ * - "UseDevelopmentStorage=true;DevelopmentStorageProxyUri=..."
21
+ * - Connection strings pointing to devstoreaccount1 (Azurite default)
22
+ * - Connection strings pointing to 127.0.0.1:10000 (Azurite default ports)
19
23
  */
20
- function needsAzurite(mergedValues) {
21
- const connStr = mergedValues?.AzureWebJobsStorage;
22
- if (!connStr || connStr === '') return true;
23
- if (connStr === 'UseDevelopmentStorage=true') return true;
24
+ function isDevStorageConnectionString(value) {
25
+ if (!value || typeof value !== 'string') return false;
26
+
27
+ const normalized = value.toLowerCase();
28
+
29
+ // Check for UseDevelopmentStorage=true (with or without additional params)
30
+ if (normalized.startsWith('usedevelopmentstorage=true')) {
31
+ return true;
32
+ }
33
+
34
+ // Check for Azurite default account name
35
+ if (normalized.includes('devstoreaccount1')) {
36
+ return true;
37
+ }
38
+
39
+ // Check for localhost Azurite ports (10000, 10001, 10002)
40
+ if (normalized.includes('127.0.0.1:10000') ||
41
+ normalized.includes('127.0.0.1:10001') ||
42
+ normalized.includes('127.0.0.1:10002') ||
43
+ normalized.includes('localhost:10000') ||
44
+ normalized.includes('localhost:10001') ||
45
+ normalized.includes('localhost:10002')) {
46
+ return true;
47
+ }
48
+
24
49
  return false;
25
50
  }
26
51
 
52
+ /**
53
+ * Determine whether Azurite is needed based on any setting using development storage.
54
+ * Returns { needed: boolean, keys: string[] } where keys are the ones using dev storage.
55
+ */
56
+ function needsAzurite(mergedValues) {
57
+ if (!mergedValues) return { needed: false, keys: [] };
58
+
59
+ const devStorageKeys = [];
60
+
61
+ for (const [key, value] of Object.entries(mergedValues)) {
62
+ if (isDevStorageConnectionString(value)) {
63
+ devStorageKeys.push(key);
64
+ }
65
+ }
66
+
67
+ // Also check AzureWebJobsStorage specially - empty/missing means dev storage
68
+ const webJobsStorage = mergedValues.AzureWebJobsStorage;
69
+ if ((!webJobsStorage || webJobsStorage === '') && !devStorageKeys.includes('AzureWebJobsStorage')) {
70
+ devStorageKeys.push('AzureWebJobsStorage');
71
+ }
72
+
73
+ return { needed: devStorageKeys.length > 0, keys: devStorageKeys };
74
+ }
75
+
27
76
  /**
28
77
  * TCP probe — resolves true if a connection can be established on the given port.
29
78
  */
@@ -130,12 +179,21 @@ export async function ensureAzurite(mergedValues, opts = {}) {
130
179
  return null;
131
180
  }
132
181
 
133
- if (!needsAzurite(mergedValues)) {
182
+ const { needed, keys } = needsAzurite(mergedValues);
183
+ if (!needed) {
134
184
  return null;
135
185
  }
136
186
 
137
- const storageVal = mergedValues?.AzureWebJobsStorage || '(empty)';
138
- console.log(info(`[fnx] Detected AzureWebJobsStorage=${storageVal}`));
187
+ // Log which connection strings are using dev storage
188
+ if (keys.length === 1) {
189
+ const val = mergedValues?.[keys[0]] || '(empty)';
190
+ console.log(info(`[fnx] Detected ${keys[0]}=${val}`));
191
+ } else {
192
+ console.log(info(`[fnx] Detected ${keys.length} connection strings requiring Azurite:`));
193
+ for (const key of keys) {
194
+ console.log(info(`[fnx] • ${key}`));
195
+ }
196
+ }
139
197
 
140
198
  // Check if Azurite is already running
141
199
  if (await isAzuriteRunning()) {
@@ -0,0 +1,281 @@
1
+ /**
2
+ * fnx chat — launch a coding agent with Azure Functions context.
3
+ * Detects available agents, generates .fnx/agent.md with project
4
+ * context, and starts the agent with the right flags.
5
+ */
6
+
7
+ import { existsSync } from 'node:fs';
8
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
9
+ import { join, resolve, dirname } from 'node:path';
10
+ import { spawn } from 'node:child_process';
11
+ import { createInterface } from 'node:readline';
12
+ import { detectProject } from '../setup/detect.js';
13
+ import { detectAgents } from '../setup/agent-detect.js';
14
+ import { title, info, funcName, success, error as errorColor, warning, dim, bold } from '../colors.js';
15
+
16
+ /**
17
+ * Agent launcher definitions — how to start each coding agent.
18
+ */
19
+ const LAUNCHERS = {
20
+ 'claude-code': {
21
+ command: 'claude',
22
+ buildArgs: (ctx) => [], // Claude reads CLAUDE.md and .claude/skills/ automatically
23
+ description: 'Claude Code reads .claude/skills/ and CLAUDE.md automatically',
24
+ },
25
+ 'github-copilot': {
26
+ command: 'copilot',
27
+ buildArgs: (ctx) => [], // Copilot reads .github/copilot-instructions.md automatically
28
+ description: 'GitHub Copilot reads .github/copilot-instructions.md automatically',
29
+ },
30
+ 'codex': {
31
+ command: 'codex',
32
+ buildArgs: (ctx) => [], // Codex reads AGENTS.md automatically
33
+ description: 'Codex reads AGENTS.md automatically',
34
+ },
35
+ };
36
+
37
+ /**
38
+ * Run fnx chat.
39
+ * @param {string[]} args - CLI arguments
40
+ */
41
+ export async function runChat(args) {
42
+ const appPath = resolveAppPath(args);
43
+ const agentFlag = getFlag(args, '--agent');
44
+ const promptFlag = getFlag(args, '--prompt');
45
+ const setupOnly = args.includes('--setup-only');
46
+
47
+ console.log();
48
+ console.log(title('fnx chat') + dim(' — AI-assisted Azure Functions development'));
49
+ console.log();
50
+
51
+ // Step 1: Detect project
52
+ console.log(bold('🔍 Loading project context...'));
53
+ const project = await detectProject(appPath);
54
+ if (project) {
55
+ console.log(success(` ✓ ${formatRuntime(project)} (${project.sku})`));
56
+ if (project.functions.length > 0) {
57
+ console.log(dim(` Functions: ${project.functions.map(f => `${f.name} (${f.type})`).join(', ')}`));
58
+ }
59
+ } else {
60
+ console.log(warning(' ⚠ No Azure Functions project detected. The agent can help you create one.'));
61
+ }
62
+
63
+ // Show skill status (informational only — setup runs after agent selection)
64
+ const skillsDir = join(appPath, '.agents', 'skills');
65
+ const needsSetup = !existsSync(skillsDir);
66
+ if (!needsSetup) {
67
+ try {
68
+ const { readdir } = await import('node:fs/promises');
69
+ const skills = (await readdir(skillsDir)).filter(d => !d.startsWith('.'));
70
+ console.log(dim(` Skills: ${skills.length} installed in .agents/skills/`));
71
+ } catch { /* ignore */ }
72
+ }
73
+ console.log();
74
+
75
+ // Step 2: Detect agents and select (only CLI-launchable agents)
76
+ console.log(bold('🤖 Detecting coding agents...'));
77
+ let agents = await detectAgents(appPath);
78
+ const launchableAgents = agents.filter(a => LAUNCHERS[a.id] && a.type === 'cli');
79
+
80
+ let selectedId;
81
+
82
+ if (agentFlag) {
83
+ // Validate explicit agent
84
+ const launcher = LAUNCHERS[agentFlag];
85
+ if (!launcher) {
86
+ console.error(errorColor(` ✗ Unknown agent: ${agentFlag}`));
87
+ console.error(dim(` Available: ${Object.keys(LAUNCHERS).join(', ')}`));
88
+ process.exit(1);
89
+ }
90
+ selectedId = agentFlag;
91
+ } else if (launchableAgents.length === 0) {
92
+ console.log(warning(' ⚠ No supported CLI agents detected.'));
93
+ console.log();
94
+ console.log(' Install one of the following:');
95
+ console.log(dim(' • Claude Code: https://claude.ai/download'));
96
+ console.log(dim(' • GitHub Copilot CLI: https://docs.github.com/en/copilot/using-github-copilot/using-github-copilot-in-the-command-line'));
97
+ console.log(dim(' • Codex CLI: npm install -g @openai/codex'));
98
+ console.log();
99
+ console.log(dim(' Or use --agent to specify: fnx chat --agent claude-code'));
100
+ process.exit(1);
101
+ } else {
102
+ for (const a of launchableAgents) {
103
+ console.log(success(` ✓ ${a.name}`));
104
+ }
105
+ console.log();
106
+
107
+ if (launchableAgents.length === 1) {
108
+ selectedId = launchableAgents[0].id;
109
+ } else {
110
+ selectedId = await promptAgentSelection(launchableAgents);
111
+ }
112
+ }
113
+
114
+ // Step 3: Auto-run setup if needed (after agent is selected)
115
+ if (needsSetup) {
116
+ console.log();
117
+ console.log(warning(' ⚠ No skills installed. Running fnx setup for ' + selectedId + '...'));
118
+ console.log();
119
+ const { runSetup } = await import('../setup/index.js');
120
+ await runSetup(['--all', '--agent', selectedId, '--app-path', appPath]);
121
+ }
122
+
123
+ // Step 4: Generate .fnx/agent.md
124
+ const agentMdPath = join(appPath, '.fnx', 'agent.md');
125
+ await generateAgentMd(appPath, project, agentMdPath);
126
+
127
+ // Step 5: Launch agent (skip if --setup-only)
128
+ if (setupOnly) {
129
+ console.log();
130
+ console.log(success(' ✓ Setup complete. Skipping agent launch (--setup-only).'));
131
+ return;
132
+ }
133
+ const launcher = LAUNCHERS[selectedId];
134
+ await launchAgent(selectedId, launcher, appPath, project, promptFlag);
135
+ }
136
+
137
+ async function launchAgent(agentId, launcher, appPath, project, prompt) {
138
+ const agentName = agentId.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
139
+
140
+ console.log(bold('🚀 Launching ' + agentName + '...'));
141
+ console.log(dim(` ${launcher.description}`));
142
+ console.log();
143
+
144
+ console.log('┌' + '─'.repeat(50) + '┐');
145
+ console.log('│ ' + bold('fnx chat') + ' • ' + agentName.padEnd(38) + '│');
146
+ if (project) {
147
+ console.log('│ ' + dim(`SKU: ${project.sku} | ${project.functions.length} functions`).padEnd(56) + '│');
148
+ }
149
+ console.log('└' + '─'.repeat(50) + '┘');
150
+ console.log();
151
+
152
+ const args = launcher.buildArgs({ appPath, project });
153
+ if (prompt) args.push(prompt);
154
+
155
+ // Launch the agent as an interactive child process
156
+ // Use shell: false to prevent shell injection via user-controlled args (e.g., --prompt)
157
+ const child = spawn(launcher.command, args, {
158
+ cwd: appPath,
159
+ stdio: 'inherit',
160
+ shell: false,
161
+ });
162
+
163
+ child.on('error', (err) => {
164
+ console.error(errorColor(` ✗ Failed to launch ${agentName}: ${err.message}`));
165
+ if (err.code === 'ENOENT') {
166
+ console.error(dim(` Make sure '${launcher.command}' is installed and in your PATH.`));
167
+ }
168
+ process.exit(1);
169
+ });
170
+
171
+ child.on('exit', (code) => {
172
+ if (code !== 0 && code !== null) {
173
+ console.log(warning(`\n ${agentName} exited with code ${code}`));
174
+ }
175
+ });
176
+ }
177
+
178
+ async function generateAgentMd(appPath, project, outputPath) {
179
+ await mkdir(dirname(outputPath), { recursive: true });
180
+
181
+ const lines = [
182
+ '# Azure Functions Development Agent',
183
+ '',
184
+ 'You are assisting a developer building Azure Functions applications with fnx.',
185
+ '',
186
+ ];
187
+
188
+ if (project) {
189
+ const funcList = project.functions.map(f => f.name + ' (' + f.type + ')').join(', ') || 'none detected';
190
+ lines.push(
191
+ '## Project Context',
192
+ '- **Runtime:** ' + formatRuntime(project),
193
+ '- **Programming Model:** ' + (project.programmingModel || 'v4'),
194
+ '- **SKU:** ' + project.sku,
195
+ '- **Functions:** ' + funcList,
196
+ '- **Emulator:** fnx (SKU-aware local emulator)',
197
+ );
198
+ } else {
199
+ lines.push(
200
+ '## No Project Detected',
201
+ 'No Azure Functions project was found in the current directory.',
202
+ );
203
+ }
204
+
205
+ lines.push(
206
+ '',
207
+ '## Available MCP Tools',
208
+ 'If the fnx Templates MCP server is configured, you can use:',
209
+ '- `functions_language_list` — Get supported languages and runtime versions',
210
+ '- `functions_template_get` — Generate function template code',
211
+ '- `functions_project_get` — Scaffold project files',
212
+ '',
213
+ '## Guidelines',
214
+ '- Always use the latest programming model for the detected runtime',
215
+ '- Check SKU compatibility before suggesting triggers/bindings',
216
+ '- Use `fnx start` for local testing (not `func start`)',
217
+ '- Use `app-config.yaml` for non-secret config (committed to git)',
218
+ '- Do NOT put secrets in workspace files',
219
+ '- Refer to installed skills for detailed guidance',
220
+ '',
221
+ );
222
+
223
+ await writeFile(outputPath, lines.join('\n'));
224
+ }
225
+
226
+ async function promptAgentSelection(agents) {
227
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
228
+ console.log('Which agent would you like to use?');
229
+ agents.forEach((a, i) => {
230
+ console.log(` ${i + 1}. ${a.name}${i === 0 ? dim(' (recommended)') : ''}`);
231
+ });
232
+
233
+ return new Promise((resolve) => {
234
+ rl.question('\nSelect [1]: ', (answer) => {
235
+ rl.close();
236
+ const idx = parseInt(answer || '1', 10) - 1;
237
+ resolve(agents[Math.max(0, Math.min(idx, agents.length - 1))].id);
238
+ });
239
+ });
240
+ }
241
+
242
+ function formatRuntime(project) {
243
+ if (!project) return 'unknown';
244
+ const name = project.runtime === 'node' ? 'Node.js' : project.runtime;
245
+ return `${name} (${project.language || project.runtime})`;
246
+ }
247
+
248
+ function resolveAppPath(args) {
249
+ const explicit = getFlag(args, '--app-path');
250
+ return explicit ? resolve(explicit) : process.cwd();
251
+ }
252
+
253
+ function getFlag(args, name) {
254
+ const idx = args.indexOf(name);
255
+ return idx >= 0 && idx + 1 < args.length ? args[idx + 1] : null;
256
+ }
257
+
258
+ export function printChatHelp() {
259
+ console.log(`${title('Usage:')} fnx chat [options]
260
+
261
+ ${title('Description:')}
262
+ Launch a coding agent with Azure Functions context. Detects your project,
263
+ generates context files, and starts your preferred coding agent.
264
+
265
+ ${title('Options:')}
266
+ ${success('--agent')} <name> Use a specific agent: ${funcName('claude-code')}, ${funcName('github-copilot')}, ${funcName('codex')}
267
+ ${success('--app-path')} <dir> Path to function app (default: current directory)
268
+ ${success('--prompt')} <text> Pass prompt text as CLI argument to the agent
269
+ ${success('-h')}, ${success('--help')} Show this help
270
+
271
+ ${title('Examples:')}
272
+ ${dim('# Auto-detect agent and launch')}
273
+ fnx chat
274
+
275
+ ${dim('# Use Claude Code specifically')}
276
+ fnx chat --agent claude-code
277
+
278
+ ${dim('# Non-interactive mode')}
279
+ fnx chat --prompt "Add a timer trigger that runs every 5 minutes"
280
+ `);
281
+ }
package/lib/cli.js CHANGED
@@ -166,6 +166,28 @@ export async function main(args) {
166
166
  return;
167
167
  }
168
168
 
169
+ if (cmd === 'setup') {
170
+ if (hasHelp(args.slice(1))) {
171
+ const { printSetupHelp } = await import('./setup/index.js');
172
+ printSetupHelp();
173
+ return;
174
+ }
175
+ const { runSetup } = await import('./setup/index.js');
176
+ await runSetup(args.slice(1));
177
+ return;
178
+ }
179
+
180
+ if (cmd === 'chat') {
181
+ if (hasHelp(args.slice(1))) {
182
+ const { printChatHelp } = await import('./chat/index.js');
183
+ printChatHelp();
184
+ return;
185
+ }
186
+ const { runChat } = await import('./chat/index.js');
187
+ await runChat(args.slice(1));
188
+ return;
189
+ }
190
+
169
191
  if (cmd !== 'start') {
170
192
  console.error(errorColor(`Unknown command: ${cmd}\n`));
171
193
  printHelp();
@@ -482,6 +504,8 @@ function printHelp() {
482
504
  ${title('Commands:')}
483
505
  ${funcName('init')} Initialize a new Azure Functions project.
484
506
  ${funcName('start')} Launch the Azure Functions host runtime for a specific SKU.
507
+ ${funcName('setup')} Add AI agent skills, MCP config, and instructions.
508
+ ${funcName('chat')} Launch a coding agent with Azure Functions context.
485
509
  ${funcName('doctor')} Validate project setup and diagnose common issues.
486
510
  ${funcName('sync')} Sync cached host/extensions with current catalog profile.
487
511
  ${funcName('pack')} Package a Functions app into a deployment zip.
package/lib/config.js CHANGED
@@ -144,7 +144,7 @@ export async function migrateConfig(appPath) {
144
144
  */
145
145
  export async function createAppConfig(appPath, overrides = {}, options = {}) {
146
146
  const appConfigPath = join(appPath, APP_CONFIG_FILE);
147
-
147
+
148
148
  // Skip if already exists
149
149
  if (await fileExists(appConfigPath)) {
150
150
  return false;
@@ -152,7 +152,7 @@ export async function createAppConfig(appPath, overrides = {}, options = {}) {
152
152
 
153
153
  const localSettingsPath = join(appPath, LOCAL_SETTINGS_FILE);
154
154
  let localSettings = {};
155
-
155
+
156
156
  if (await fileExists(localSettingsPath)) {
157
157
  try {
158
158
  localSettings = await readJsonFile(localSettingsPath);
@@ -164,11 +164,6 @@ export async function createAppConfig(appPath, overrides = {}, options = {}) {
164
164
  // Build config using shared function (overrides take precedence)
165
165
  const config = buildConfigFromLocalSettings(localSettings, overrides);
166
166
 
167
- // Ensure EnableWorkerIndexing is set
168
- config.configurations = config.configurations || {};
169
- config.configurations.AzureWebJobsFeatureFlags =
170
- config.configurations.AzureWebJobsFeatureFlags || 'EnableWorkerIndexing';
171
-
172
167
  // Write app-config.yaml
173
168
  await writeFile(appConfigPath, generateYaml(config), 'utf-8');
174
169
 
@@ -407,7 +402,6 @@ async function interactiveCreate(appPath) {
407
402
  local: { targetSku: 'flex' },
408
403
  runtime: { name: runtime },
409
404
  configurations: {
410
- AzureWebJobsFeatureFlags: 'EnableWorkerIndexing',
411
405
  },
412
406
  };
413
407