@vrdmr/fnx-test 0.4.2 → 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
@@ -112,6 +112,17 @@ export async function main(args) {
112
112
  return;
113
113
  }
114
114
 
115
+ if (cmd === 'init') {
116
+ if (hasHelp(args.slice(1))) {
117
+ const { printInitHelp } = await import('./init.js');
118
+ printInitHelp();
119
+ return;
120
+ }
121
+ const { runInit } = await import('./init.js');
122
+ await runInit(args.slice(1));
123
+ return;
124
+ }
125
+
115
126
  if (cmd === 'doctor') {
116
127
  if (hasHelp(args.slice(1))) { printDoctorHelp(); return; }
117
128
  const { runDoctor } = await import('./doctor.js');
@@ -155,6 +166,28 @@ export async function main(args) {
155
166
  return;
156
167
  }
157
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
+
158
191
  if (cmd !== 'start') {
159
192
  console.error(errorColor(`Unknown command: ${cmd}\n`));
160
193
  printHelp();
@@ -469,7 +502,10 @@ function printHelp() {
469
502
  console.log(`${title('Usage:')} fnx <command> [options]
470
503
 
471
504
  ${title('Commands:')}
505
+ ${funcName('init')} Initialize a new Azure Functions project.
472
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.
473
509
  ${funcName('doctor')} Validate project setup and diagnose common issues.
474
510
  ${funcName('sync')} Sync cached host/extensions with current catalog profile.
475
511
  ${funcName('pack')} Package a Functions app into a deployment zip.
package/lib/config.js CHANGED
@@ -3,8 +3,8 @@
3
3
  // Reads app-config.yaml (primary) or auto-creates it from local.settings.json.
4
4
  // Validates against config-schema.js, checks for secrets, manages .gitignore protection.
5
5
 
6
- import { readFile, writeFile, access, readdir } from 'node:fs/promises';
7
- import { join, resolve as resolvePath } from 'node:path';
6
+ import { readFile, writeFile, access } from 'node:fs/promises';
7
+ import { join } from 'node:path';
8
8
  import { createInterface } from 'node:readline';
9
9
  import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
10
10
 
@@ -134,6 +134,46 @@ export async function migrateConfig(appPath) {
134
134
  return autoCreateFromLocalSettings(appPath, localSettingsPath);
135
135
  }
136
136
 
137
+ /**
138
+ * Create app-config.yaml from local.settings.json (if exists) with CLI overrides.
139
+ * Used by fnx init to generate config after template download.
140
+ * @param {string} appPath - Directory containing local.settings.json
141
+ * @param {Object} overrides - { runtime, version, sku } from CLI flags
142
+ * @param {Object} options - { silent: boolean } suppress output
143
+ * @returns {Promise<boolean>} true if file was created, false if already exists
144
+ */
145
+ export async function createAppConfig(appPath, overrides = {}, options = {}) {
146
+ const appConfigPath = join(appPath, APP_CONFIG_FILE);
147
+
148
+ // Skip if already exists
149
+ if (await fileExists(appConfigPath)) {
150
+ return false;
151
+ }
152
+
153
+ const localSettingsPath = join(appPath, LOCAL_SETTINGS_FILE);
154
+ let localSettings = {};
155
+
156
+ if (await fileExists(localSettingsPath)) {
157
+ try {
158
+ localSettings = await readJsonFile(localSettingsPath);
159
+ } catch {
160
+ // Ignore parse errors, proceed with overrides only
161
+ }
162
+ }
163
+
164
+ // Build config using shared function (overrides take precedence)
165
+ const config = buildConfigFromLocalSettings(localSettings, overrides);
166
+
167
+ // Write app-config.yaml
168
+ await writeFile(appConfigPath, generateYaml(config), 'utf-8');
169
+
170
+ if (!options.silent) {
171
+ console.log(successColor(` ✓ Created app-config.yaml`));
172
+ }
173
+
174
+ return true;
175
+ }
176
+
137
177
  /**
138
178
  * Validate app-config.yaml (standalone command).
139
179
  * Returns { valid, errors, warnings, secrets }.
@@ -263,47 +303,62 @@ async function readJsonFile(filePath) {
263
303
  }
264
304
  }
265
305
 
266
- async function autoCreateFromLocalSettings(appPath, localSettingsPath) {
267
- const localSettings = await readJsonFile(localSettingsPath);
268
- if (!localSettings?.Values) {
269
- console.error(errorColor(' local.settings.json has no Values section.'));
270
- process.exit(1);
271
- }
272
-
273
- const values = localSettings.Values;
306
+ /**
307
+ * Build config object from local.settings.json values.
308
+ * Extracts runtime, version, sku, and allowed configurations (excluding secrets).
309
+ * @param {Object} localSettings - Parsed local.settings.json object
310
+ * @param {Object} overrides - Optional overrides { runtime, version, sku }
311
+ * @returns {Object} Config object ready for generateYaml()
312
+ */
313
+ export function buildConfigFromLocalSettings(localSettings, overrides = {}) {
314
+ const values = localSettings?.Values || {};
274
315
  const secrets = detectSecretsInFlatValues(values);
275
- const nonSecretKeys = Object.keys(values).filter(k => !secrets.has(k));
276
316
 
277
317
  // Build structured config
278
318
  const config = {};
279
319
 
280
- // Extract runtime
281
- const runtime = values.FUNCTIONS_WORKER_RUNTIME;
320
+ // Extract runtime (CLI override takes precedence)
321
+ const runtime = overrides.runtime || values.FUNCTIONS_WORKER_RUNTIME;
282
322
  if (runtime) {
283
323
  config.runtime = { name: runtime };
284
- // Check for runtime version in env or values
285
- const version = values.FUNCTIONS_WORKER_RUNTIME_VERSION;
324
+ // Check for runtime version (CLI override takes precedence)
325
+ const version = overrides.version || values.FUNCTIONS_WORKER_RUNTIME_VERSION;
286
326
  if (version) config.runtime.version = version;
287
327
  }
288
328
 
289
- // Extract targetSku from local settings (non-standard but some users set it)
290
- if (localSettings.TargetSku) {
291
- config.local = { targetSku: localSettings.TargetSku };
329
+ // Extract targetSku (CLI override takes precedence)
330
+ const sku = overrides.sku || localSettings.TargetSku;
331
+ if (sku) {
332
+ config.local = { targetSku: sku };
292
333
  }
293
334
 
294
335
  // Remaining non-secret, non-structured values → configurations
295
336
  const structuredEnvVars = new Set(Object.values(STRUCTURED_FIELDS).map(s => s.envVar));
296
337
  const configEntries = {};
297
- for (const key of nonSecretKeys) {
338
+ for (const [key, val] of Object.entries(values)) {
298
339
  if (structuredEnvVars.has(key)) continue; // Already mapped structurally
340
+ if (secrets.has(key)) continue; // Skip secrets
299
341
  if (ALLOWED_CONFIGURATIONS.has(key)) {
300
- configEntries[key] = values[key];
342
+ configEntries[key] = val;
301
343
  }
302
344
  }
303
345
  if (Object.keys(configEntries).length > 0) {
304
346
  config.configurations = configEntries;
305
347
  }
306
348
 
349
+ return config;
350
+ }
351
+
352
+ async function autoCreateFromLocalSettings(appPath, localSettingsPath) {
353
+ const localSettings = await readJsonFile(localSettingsPath);
354
+ if (!localSettings?.Values) {
355
+ console.error(errorColor(' local.settings.json has no Values section.'));
356
+ process.exit(1);
357
+ }
358
+
359
+ const config = buildConfigFromLocalSettings(localSettings);
360
+ const secrets = detectSecretsInFlatValues(localSettings.Values);
361
+
307
362
  // Write app-config.yaml
308
363
  const yaml = generateYaml(config);
309
364
  const appConfigPath = join(appPath, APP_CONFIG_FILE);
@@ -347,7 +402,6 @@ async function interactiveCreate(appPath) {
347
402
  local: { targetSku: 'flex' },
348
403
  runtime: { name: runtime },
349
404
  configurations: {
350
- AzureWebJobsFeatureFlags: 'EnableWorkerIndexing',
351
405
  },
352
406
  };
353
407
 
@@ -372,7 +426,7 @@ async function interactiveCreate(appPath) {
372
426
  return config;
373
427
  }
374
428
 
375
- function generateYaml(config) {
429
+ export function generateYaml(config) {
376
430
  const lines = [
377
431
  '# Azure Functions App Configuration',
378
432
  '# Commit this to source control. Do NOT put secrets here.',
@@ -413,7 +467,7 @@ function generateYaml(config) {
413
467
  * Detect which keys in a flat values map are secrets (for migration).
414
468
  * Returns a Set of secret key names.
415
469
  */
416
- function detectSecretsInFlatValues(values) {
470
+ export function detectSecretsInFlatValues(values) {
417
471
  const secretKeys = new Set();
418
472
  const secretKeyPatterns = [
419
473
  /ConnectionString$/i,