@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.
- package/lib/azurite-manager.js +67 -9
- package/lib/chat/index.js +281 -0
- package/lib/cli.js +36 -0
- package/lib/config.js +77 -23
- package/lib/host-launcher.js +121 -30
- package/lib/init/manifest.js +250 -0
- package/lib/init/prompts.js +673 -0
- package/lib/init/scaffold.js +689 -0
- package/lib/init.js +540 -0
- package/lib/runtimes.js +238 -0
- package/lib/setup/agent-detect.js +92 -0
- package/lib/setup/detect.js +117 -0
- package/lib/setup/index.js +572 -0
- package/lib/utils.js +27 -0
- package/manifests/default.yaml +124 -0
- package/manifests/skills/fnx-best-practices/SKILL.md +64 -0
- package/manifests/skills/fnx-best-practices/references/azure-functions-docs.md +60 -0
- package/manifests/skills/fnx-best-practices/references/fnx-specific.md +97 -0
- package/manifests/skills/fnx-create-function/SKILL.md +133 -0
- package/manifests/skills/fnx-create-function/references/templates.md +105 -0
- package/manifests/skills/fnx-diagnostics/SKILL.md +84 -0
- package/manifests/skills/fnx-diagnostics/references/diagnostic-checklist.md +59 -0
- package/manifests/skills/fnx-diagnostics/references/fnx-error-patterns.md +71 -0
- package/manifests/skills/fnx-feedback/SKILL.md +142 -0
- package/manifests/skills/fnx-intro/SKILL.md +83 -0
- package/manifests/skills/fnx-intro/references/fnx-commands.md +157 -0
- package/manifests/skills/fnx-intro/references/roadmap.md +60 -0
- package/package.json +3 -1
- package/profiles/sku-profiles.json +6 -6
package/lib/azurite-manager.js
CHANGED
|
@@ -14,16 +14,65 @@ let azuriteProcess = null;
|
|
|
14
14
|
let weStartedAzurite = false;
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
182
|
+
const { needed, keys } = needsAzurite(mergedValues);
|
|
183
|
+
if (!needed) {
|
|
134
184
|
return null;
|
|
135
185
|
}
|
|
136
186
|
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
7
|
-
import { join
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
|
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
|
|
290
|
-
|
|
291
|
-
|
|
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
|
|
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] =
|
|
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,
|