@vrdmr/fnx-test 0.2.0 → 0.3.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/README.md +16 -4
- package/lib/azurite-manager.js +16 -15
- package/lib/cli.js +411 -135
- package/lib/colors.js +34 -0
- package/lib/host-launcher.js +34 -15
- package/lib/host-manager.js +150 -25
- package/lib/live-mcp-server.js +3 -2
- package/lib/pack.js +140 -0
- package/lib/profile-resolver.js +44 -35
- package/lib/warmup.js +12 -12
- package/package.json +1 -1
package/lib/cli.js
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { resolve as resolvePath, dirname, join } from 'node:path';
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { readFile, mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
3
4
|
import { createServer } from 'node:net';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
4
6
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { resolveProfile, listProfiles, setProfilesSource } from './profile-resolver.js';
|
|
6
|
-
import { ensureHost, ensureBundle } from './host-manager.js';
|
|
7
|
+
import { resolveProfile, listProfiles, setProfilesSource, fetchRegistryWithMeta, readProfilesSync } from './profile-resolver.js';
|
|
8
|
+
import { ensureHost, ensureBundle, getCachedHostVersions, getCachedBundleVersions, compareVersions, DEFAULT_KEEP_VERSIONS } from './host-manager.js';
|
|
7
9
|
import { launchHost, createHostState } from './host-launcher.js';
|
|
8
10
|
import { startLiveMcpServer } from './live-mcp-server.js';
|
|
9
11
|
import { detectDotnetModel, printInProcessError } from './dotnet-detector.js';
|
|
12
|
+
import { detectRuntimeFromConfig, packFunctionApp } from './pack.js';
|
|
13
|
+
import { title, info, funcName, url as urlColor, success, error as errorColor, warning, dim, bold, highlightUrls } from './colors.js';
|
|
14
|
+
|
|
15
|
+
const FNX_HOME = join(homedir(), '.fnx');
|
|
16
|
+
const VERSION_CHECK_FILE = join(FNX_HOME, 'version-check.json');
|
|
17
|
+
const VERSION_CHECK_TTL_MS = 24 * 60 * 60 * 1000;
|
|
10
18
|
|
|
11
19
|
function isPortFree(port) {
|
|
12
20
|
return new Promise((resolve) => {
|
|
@@ -24,46 +32,105 @@ async function findOpenPort(start, maxRetries = 10) {
|
|
|
24
32
|
return start; // fall through — let the host report the error
|
|
25
33
|
}
|
|
26
34
|
|
|
35
|
+
function hasHelp(args) {
|
|
36
|
+
return args.includes('-h') || args.includes('--help');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the function app directory.
|
|
41
|
+
* 1. If --app-path is given, use it — but verify host.json exists.
|
|
42
|
+
* 2. Otherwise check cwd for host.json.
|
|
43
|
+
* 3. Fall back to cwd/src if it contains host.json.
|
|
44
|
+
* 4. Error with actionable message if nothing found.
|
|
45
|
+
*/
|
|
46
|
+
function resolveAppPath(args) {
|
|
47
|
+
const explicit = getFlag(args, '--app-path');
|
|
48
|
+
if (explicit) {
|
|
49
|
+
const resolved = resolvePath(explicit);
|
|
50
|
+
if (!existsSync(join(resolved, 'host.json'))) {
|
|
51
|
+
console.error(errorColor(`Error: No host.json found in ${resolved}`));
|
|
52
|
+
console.error(` The --app-path must point to a directory containing host.json.`);
|
|
53
|
+
console.error(dim(` Example: fnx start --app-path ./my-function-app`));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
return resolved;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const cwd = process.cwd();
|
|
60
|
+
if (existsSync(join(cwd, 'host.json'))) {
|
|
61
|
+
return cwd;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const srcDir = join(cwd, 'src');
|
|
65
|
+
if (existsSync(join(srcDir, 'host.json'))) {
|
|
66
|
+
console.log(info(` Using function app at ${dim('./src')} (found host.json there)`));
|
|
67
|
+
return srcDir;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.error(errorColor(`Error: No function app found.`));
|
|
71
|
+
console.error(` Could not find host.json in the current directory or ./src.`);
|
|
72
|
+
console.error(dim(` Use --app-path <dir> to specify the function app location.`));
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
27
76
|
export async function main(args) {
|
|
28
77
|
const cmd = args[0];
|
|
29
78
|
|
|
30
79
|
if (cmd === '-h' || cmd === '--help' || cmd === 'help' || !cmd) {
|
|
31
|
-
|
|
80
|
+
await printHelpWithVersionInfo();
|
|
32
81
|
process.exit(cmd ? 0 : 1);
|
|
33
82
|
}
|
|
34
83
|
|
|
35
84
|
if (cmd === '-v' || cmd === '--version') {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
const { dirname, join } = await import('node:path');
|
|
39
|
-
const dir = dirname(fileURLToPath(import.meta.url));
|
|
40
|
-
const pkg = JSON.parse(readFileSync(join(dir, '..', 'package.json'), 'utf-8'));
|
|
41
|
-
console.log(`fnx v${pkg.version}`);
|
|
85
|
+
const pkg = await getFnxPackage();
|
|
86
|
+
console.log(title(`fnx v${pkg.version}`));
|
|
42
87
|
process.exit(0);
|
|
43
88
|
}
|
|
44
89
|
|
|
45
90
|
if (cmd === 'templates-mcp') {
|
|
91
|
+
if (hasHelp(args.slice(1))) { printTemplatesMcpHelp(); return; }
|
|
46
92
|
await startTemplatesMcp();
|
|
47
93
|
return;
|
|
48
94
|
}
|
|
49
95
|
|
|
50
96
|
if (cmd === 'warmup') {
|
|
97
|
+
if (hasHelp(args.slice(1))) { printWarmupHelp(); return; }
|
|
51
98
|
const { warmup } = await import('./warmup.js');
|
|
52
99
|
await warmup(args.slice(1));
|
|
53
100
|
return;
|
|
54
101
|
}
|
|
55
102
|
|
|
103
|
+
if (cmd === 'sync') {
|
|
104
|
+
if (hasHelp(args.slice(1))) { printSyncHelp(); return; }
|
|
105
|
+
await runSync(args.slice(1));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (cmd === 'pack') {
|
|
110
|
+
if (hasHelp(args.slice(1))) { printPackHelp(); return; }
|
|
111
|
+
const scriptRoot = resolveAppPath(args);
|
|
112
|
+
const runtime = getFlag(args, '--runtime') || await detectRuntimeFromConfig(scriptRoot);
|
|
113
|
+
const outputPath = getFlag(args, '--output');
|
|
114
|
+
const noBuild = args.includes('--no-build');
|
|
115
|
+
await packFunctionApp({ scriptRoot, runtime, outputPath, noBuild });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
56
119
|
if (cmd !== 'start') {
|
|
57
|
-
console.error(`Unknown command: ${cmd}\n`);
|
|
120
|
+
console.error(errorColor(`Unknown command: ${cmd}\n`));
|
|
58
121
|
printHelp();
|
|
59
122
|
process.exit(1);
|
|
60
123
|
}
|
|
61
124
|
|
|
62
|
-
|
|
125
|
+
if (hasHelp(args.slice(1))) { printStartHelp(); return; }
|
|
126
|
+
|
|
127
|
+
await maybeWarnForCliUpgrade();
|
|
128
|
+
|
|
129
|
+
const scriptRoot = resolveAppPath(args);
|
|
63
130
|
const requestedPort = parseInt(getFlag(args, '--port') || '7071');
|
|
64
131
|
const port = await findOpenPort(requestedPort);
|
|
65
132
|
if (port !== requestedPort) {
|
|
66
|
-
console.log(` Port ${requestedPort} in use, using ${port} instead.`);
|
|
133
|
+
console.log(warning(` Port ${requestedPort} in use, using ${port} instead.`));
|
|
67
134
|
}
|
|
68
135
|
const mcpPort = getFlag(args, '--mcp-port') || String(port + 1);
|
|
69
136
|
const verbose = args.includes('--verbose');
|
|
@@ -95,8 +162,8 @@ export async function main(args) {
|
|
|
95
162
|
if (!sku) {
|
|
96
163
|
sku = 'flex';
|
|
97
164
|
skuSource = 'default';
|
|
98
|
-
console.log(`No --sku specified, defaulting to '${sku}'.`);
|
|
99
|
-
console.log(`Tip: Use --sku <name> to target a specific SKU. Run --sku list to see options.\n`);
|
|
165
|
+
console.log(info(`No --sku specified, defaulting to '${sku}'.`));
|
|
166
|
+
console.log(dim(`Tip: Use --sku <name> to target a specific SKU. Run --sku list to see options.\n`));
|
|
100
167
|
}
|
|
101
168
|
|
|
102
169
|
if (sku === 'list') {
|
|
@@ -106,33 +173,29 @@ export async function main(args) {
|
|
|
106
173
|
|
|
107
174
|
// 1. Resolve profile
|
|
108
175
|
if (skuSource !== 'default') {
|
|
109
|
-
console.log(`Resolving SKU profile: ${sku} (from ${skuSource})...`);
|
|
176
|
+
console.log(title(`Resolving SKU profile: ${sku} (from ${skuSource})...`));
|
|
110
177
|
} else {
|
|
111
|
-
console.log(`Resolving SKU profile: ${sku}...`);
|
|
178
|
+
console.log(title(`Resolving SKU profile: ${sku}...`));
|
|
112
179
|
}
|
|
113
|
-
const profile = await resolveProfile(sku);
|
|
114
|
-
console.log(` Target SKU: ${profile.displayName}`);
|
|
115
|
-
console.log(` Host Version: ${profile.hostVersion}`);
|
|
116
|
-
console.log(` Extension Bundle: ${profile.extensionBundleVersion}`);
|
|
117
|
-
if (profile.maxExtensionBundleVersion) {
|
|
118
|
-
console.log(` Max Bundle Cap: ${profile.maxExtensionBundleVersion}`);
|
|
119
|
-
}
|
|
120
|
-
console.log();
|
|
121
180
|
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
181
|
+
const { registry, source } = await fetchRegistryWithMeta();
|
|
182
|
+
const profile = registry.profiles[sku];
|
|
183
|
+
if (!profile) {
|
|
184
|
+
const valid = Object.keys(registry.profiles).join(', ');
|
|
185
|
+
throw new Error(`Unknown SKU '${sku}'. Available: ${valid}`);
|
|
186
|
+
}
|
|
125
187
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (
|
|
131
|
-
console.log(` Bundle
|
|
188
|
+
profile.name = sku;
|
|
189
|
+
console.log(` ${dim('Target SKU:')} ${info(profile.displayName)}`);
|
|
190
|
+
console.log(` ${dim('Host Version:')} ${info(profile.hostVersion)}`);
|
|
191
|
+
console.log(` ${dim('Extension Bundle:')} ${info(profile.extensionBundleVersion)}`);
|
|
192
|
+
if (profile.maxExtensionBundleVersion) {
|
|
193
|
+
console.log(` ${dim('Max Bundle Cap:')} ${info(profile.maxExtensionBundleVersion)}`);
|
|
132
194
|
}
|
|
195
|
+
console.log(` ${dim('Profile Source:')} ${info(source)}`);
|
|
133
196
|
console.log();
|
|
134
197
|
|
|
135
|
-
//
|
|
198
|
+
// Early validation: merge config and check runtime before downloading anything
|
|
136
199
|
const mergedValues = {
|
|
137
200
|
...(appConfig?.Values || {}),
|
|
138
201
|
...(localSettings?.Values || {}),
|
|
@@ -141,7 +204,7 @@ export async function main(args) {
|
|
|
141
204
|
const workerRuntime = mergedValues.FUNCTIONS_WORKER_RUNTIME;
|
|
142
205
|
|
|
143
206
|
if (!workerRuntime) {
|
|
144
|
-
console.error('Error: FUNCTIONS_WORKER_RUNTIME not set in app.config.json or local.settings.json');
|
|
207
|
+
console.error(errorColor('Error: FUNCTIONS_WORKER_RUNTIME not set in app.config.json or local.settings.json'));
|
|
145
208
|
process.exit(1);
|
|
146
209
|
}
|
|
147
210
|
|
|
@@ -155,15 +218,30 @@ export async function main(args) {
|
|
|
155
218
|
}
|
|
156
219
|
}
|
|
157
220
|
|
|
158
|
-
|
|
221
|
+
printHostDriftWarning(profile.hostVersion);
|
|
222
|
+
|
|
223
|
+
// 2. Ensure host is downloaded
|
|
224
|
+
const hostDir = await ensureHost(profile, { keepVersions: DEFAULT_KEEP_VERSIONS });
|
|
225
|
+
console.log(` ${dim('Host path:')} ${info(hostDir)}`);
|
|
226
|
+
|
|
227
|
+
// 3. Pre-download the correct extension bundle for this SKU
|
|
228
|
+
// This resolves the exact version from CDN index, capped by maxExtensionBundleVersion,
|
|
229
|
+
// and downloads it so the host finds it cached and never fetches a wrong version.
|
|
230
|
+
const resolvedBundleVersion = await ensureBundle(profile, { keepVersions: DEFAULT_KEEP_VERSIONS });
|
|
231
|
+
if (resolvedBundleVersion) {
|
|
232
|
+
console.log(` ${dim('Bundle resolved:')} ${info(resolvedBundleVersion)}`);
|
|
233
|
+
}
|
|
234
|
+
console.log();
|
|
235
|
+
|
|
236
|
+
// 4. Create shared host state and start live MCP server
|
|
159
237
|
const hostState = createHostState();
|
|
160
238
|
|
|
161
239
|
if (!noMcp) {
|
|
162
240
|
startLiveMcpServer(hostState, parseInt(mcpPort))
|
|
163
241
|
.then((server) => { hostState._mcpServer = server; })
|
|
164
242
|
.catch((err) => {
|
|
165
|
-
console.error(` ⚠️ MCP server failed to start on port ${mcpPort}: ${err.message}`);
|
|
166
|
-
console.error(` Use --no-mcp to disable, or --mcp-port <port> to change port.`);
|
|
243
|
+
console.error(warning(` ⚠️ MCP server failed to start on port ${mcpPort}: ${err.message}`));
|
|
244
|
+
console.error(dim(` Use --no-mcp to disable, or --mcp-port <port> to change port.`));
|
|
167
245
|
});
|
|
168
246
|
// Don't await — host startup should not depend on MCP server
|
|
169
247
|
}
|
|
@@ -201,6 +279,91 @@ export async function main(args) {
|
|
|
201
279
|
});
|
|
202
280
|
}
|
|
203
281
|
|
|
282
|
+
async function runSync(args) {
|
|
283
|
+
const target = ['host', 'extensions'].includes(args[0]) ? args[0] : 'all';
|
|
284
|
+
const force = args.includes('--force');
|
|
285
|
+
const keep = parseInt(getFlag(args, '--keep') || String(DEFAULT_KEEP_VERSIONS), 10);
|
|
286
|
+
const sku = getFlag(args, '--sku') || 'flex';
|
|
287
|
+
const profilesSource = getFlag(args, '--profiles');
|
|
288
|
+
if (profilesSource) setProfilesSource(profilesSource);
|
|
289
|
+
|
|
290
|
+
if (sku === 'list') {
|
|
291
|
+
await listProfiles();
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const profile = await resolveProfile(sku);
|
|
296
|
+
profile.name = sku;
|
|
297
|
+
|
|
298
|
+
console.log(title(`Syncing SKU '${sku}' (${profile.displayName})...`));
|
|
299
|
+
if (target === 'all' || target === 'host') {
|
|
300
|
+
await ensureHost(profile, { force, keepVersions: keep });
|
|
301
|
+
console.log(success(' ✓ Host synchronized.'));
|
|
302
|
+
}
|
|
303
|
+
if (target === 'all' || target === 'extensions') {
|
|
304
|
+
const bundle = await ensureBundle(profile, { force, keepVersions: keep });
|
|
305
|
+
console.log(success(` ✓ Extensions synchronized (${bundle || 'cached'}).`));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
console.log(dim(` Retention policy: keep latest ${keep} version(s).`));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function printHostDriftWarning(targetHostVersion) {
|
|
312
|
+
const cached = getCachedHostVersions();
|
|
313
|
+
if (cached.length === 0) return;
|
|
314
|
+
|
|
315
|
+
const highest = [...cached].sort(compareVersions).pop();
|
|
316
|
+
if (!highest) return;
|
|
317
|
+
|
|
318
|
+
if (compareVersions(targetHostVersion, highest) > 0) {
|
|
319
|
+
console.log(info(` ℹ️ New host available: ${targetHostVersion} (local latest: ${highest}).`));
|
|
320
|
+
console.log(dim(' Run `fnx sync` or `fnx sync host` to download it.\n'));
|
|
321
|
+
} else if (compareVersions(targetHostVersion, highest) < 0) {
|
|
322
|
+
console.log(warning(` ⚠️ Host rollback detected: local ${highest}, catalog ${targetHostVersion}.`));
|
|
323
|
+
console.log(dim(' Run `fnx sync` or `fnx sync host` to align with the supported version.\n'));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function maybeWarnForCliUpgrade() {
|
|
328
|
+
try {
|
|
329
|
+
const pkg = await getFnxPackage();
|
|
330
|
+
const current = pkg.version;
|
|
331
|
+
const cached = await readJsonFile(VERSION_CHECK_FILE);
|
|
332
|
+
const now = Date.now();
|
|
333
|
+
|
|
334
|
+
if (cached?.checkedAt && now - new Date(cached.checkedAt).getTime() < VERSION_CHECK_TTL_MS) {
|
|
335
|
+
if (cached.latestVersion && compareVersions(cached.latestVersion, current) > 0) {
|
|
336
|
+
printUpgradeTip(cached.latestVersion);
|
|
337
|
+
}
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const registryUrl = `https://registry.npmjs.org/${encodeURIComponent(pkg.name)}/latest`;
|
|
342
|
+
const res = await fetch(registryUrl);
|
|
343
|
+
if (!res.ok) return;
|
|
344
|
+
const latest = await res.json();
|
|
345
|
+
|
|
346
|
+
await mkdir(FNX_HOME, { recursive: true });
|
|
347
|
+
await writeFile(VERSION_CHECK_FILE, JSON.stringify({ checkedAt: new Date().toISOString(), latestVersion: latest.version }, null, 2));
|
|
348
|
+
|
|
349
|
+
if (latest.version && compareVersions(latest.version, current) > 0) {
|
|
350
|
+
printUpgradeTip(latest.version);
|
|
351
|
+
}
|
|
352
|
+
} catch {
|
|
353
|
+
// non-fatal: offline/private registry/etc.
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function printUpgradeTip(latestVersion) {
|
|
358
|
+
console.log(info(` ℹ️ A newer fnx version is available (${latestVersion}).`));
|
|
359
|
+
console.log(dim(' Run `npm i -g @vrdmr/fnx-test@latest` to upgrade.\n'));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function getFnxPackage() {
|
|
363
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
364
|
+
return JSON.parse(await readFile(join(dir, '..', 'package.json'), 'utf-8'));
|
|
365
|
+
}
|
|
366
|
+
|
|
204
367
|
async function startTemplatesMcp() {
|
|
205
368
|
const { runStdioMcpServer } = await import('./mcp-server.js');
|
|
206
369
|
const { getTemplateTools } = await import('./mcp-tools/templates.js');
|
|
@@ -229,102 +392,215 @@ export async function readJsonFile(filePath) {
|
|
|
229
392
|
}
|
|
230
393
|
}
|
|
231
394
|
|
|
395
|
+
async function printHelpWithVersionInfo() {
|
|
396
|
+
const pkg = await getFnxPackage();
|
|
397
|
+
const cachedHosts = getCachedHostVersions().sort(compareVersions);
|
|
398
|
+
const cachedBundles = getCachedBundleVersions().sort(compareVersions);
|
|
399
|
+
|
|
400
|
+
// Build SKU → host version map from cached/bundled profiles (no network)
|
|
401
|
+
const registry = readProfilesSync();
|
|
402
|
+
const hostToSkus = {};
|
|
403
|
+
if (registry?.profiles) {
|
|
404
|
+
for (const [sku, p] of Object.entries(registry.profiles)) {
|
|
405
|
+
const v = p.hostVersion;
|
|
406
|
+
if (!hostToSkus[v]) hostToSkus[v] = [];
|
|
407
|
+
hostToSkus[v].push(sku);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Fire-and-forget: refresh profile cache in background.
|
|
412
|
+
// Won't block -h — process.exit() will terminate regardless.
|
|
413
|
+
fetchRegistryWithMeta().catch(() => {});
|
|
414
|
+
|
|
415
|
+
console.log(`
|
|
416
|
+
${bold(title('Azure Functions Local Emulator (fnx — Phoenix Emulate)'))}
|
|
417
|
+
${dim('fnx Version:')} ${title(pkg.version)}`);
|
|
418
|
+
|
|
419
|
+
if (cachedHosts.length) {
|
|
420
|
+
console.log(`${dim('Cached Hosts:')} ${cachedHosts.map((v) => {
|
|
421
|
+
const skus = hostToSkus[v];
|
|
422
|
+
return skus ? `${info(v)} ${dim('(' + skus.join(', ') + ')')}` : info(v);
|
|
423
|
+
}).join(', ')}`);
|
|
424
|
+
} else {
|
|
425
|
+
console.log(`${dim('Cached Hosts:')} ${dim('(none — run fnx warmup)')}`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (cachedBundles.length) {
|
|
429
|
+
console.log(`${dim('Cached Bundles:')} ${cachedBundles.map((v) => info(v)).join(', ')}`);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
console.log();
|
|
433
|
+
printHelp();
|
|
434
|
+
}
|
|
435
|
+
|
|
232
436
|
function printHelp() {
|
|
437
|
+
console.log(`${title('Usage:')} fnx <command> [options]
|
|
438
|
+
|
|
439
|
+
${title('Commands:')}
|
|
440
|
+
${funcName('start')} Launch the Azure Functions host runtime for a specific SKU.
|
|
441
|
+
${funcName('sync')} Sync cached host/extensions with current catalog profile.
|
|
442
|
+
${funcName('pack')} Package a Functions app into a deployment zip.
|
|
443
|
+
${funcName('warmup')} Pre-download host binaries and extension bundles.
|
|
444
|
+
${funcName('templates-mcp')} Start the Azure Functions templates MCP server (stdio).
|
|
445
|
+
|
|
446
|
+
Run ${dim('fnx <command> -h')} for command-specific options.
|
|
447
|
+
|
|
448
|
+
${title('Common Options:')}
|
|
449
|
+
${success('--sku')} <name> Target SKU to emulate (default: flex).
|
|
450
|
+
Use ${success('--sku list')} to see all available SKUs.
|
|
451
|
+
${success('--verbose')} Show all host output (unfiltered).
|
|
452
|
+
${success('-v')}, ${success('--version')} Display the version of fnx.
|
|
453
|
+
${success('-h')}, ${success('--help')} Display this help information.
|
|
454
|
+
|
|
455
|
+
${title('Start Options:')} ${dim('(fnx start)')}
|
|
456
|
+
${success('--app-path')} <dir> Path to the function app directory (default: cwd).
|
|
457
|
+
${success('--port')} <port> Port for the host HTTP listener (default: 7071).
|
|
458
|
+
${success('--mcp-port')} <port> Port for the live MCP server (default: host port + 1).
|
|
459
|
+
${success('--no-mcp')} Disable the live MCP server.
|
|
460
|
+
${success('--no-azurite')} Skip automatic Azurite start.
|
|
461
|
+
|
|
462
|
+
${title('Sync Options:')} ${dim('(fnx sync [host|extensions])')}
|
|
463
|
+
${success('--keep')} <n> Keep latest N versions in cache (default: 2).
|
|
464
|
+
${success('--force')} Re-download even if already cached.
|
|
465
|
+
|
|
466
|
+
${title('Pack Options:')} ${dim('(fnx pack)')}
|
|
467
|
+
${success('--app-path')} <dir> Path to the function app directory (default: cwd).
|
|
468
|
+
${success('--runtime')} <name> Runtime identifier (default: auto-detected from config).
|
|
469
|
+
${success('--output')} <file> Output zip path (default: <app-name>.zip).
|
|
470
|
+
${success('--no-build')} Skip build steps for java/dotnet-isolated.
|
|
471
|
+
|
|
472
|
+
${title('Available SKUs:')}
|
|
473
|
+
${funcName('flex')} Flex Consumption (latest host, default)
|
|
474
|
+
${funcName('linux-premium')} Linux Premium / Elastic Premium
|
|
475
|
+
${funcName('windows-consumption')} Windows Consumption (classic)
|
|
476
|
+
${funcName('windows-dedicated')} Windows Dedicated (App Service Plan)
|
|
477
|
+
${funcName('linux-consumption')} Linux Consumption (retiring)
|
|
478
|
+
|
|
479
|
+
${title('Examples:')}
|
|
480
|
+
fnx start Start with default SKU (flex)
|
|
481
|
+
fnx start --sku windows-consumption Emulate Windows Consumption
|
|
482
|
+
fnx start --sku flex --port 8080 Custom port
|
|
483
|
+
fnx pack --app-path ./my-app Package function app as zip
|
|
484
|
+
fnx sync host --force Force re-download host binary
|
|
485
|
+
fnx warmup --all Pre-download all SKUs
|
|
486
|
+
fnx templates-mcp Start templates MCP server
|
|
487
|
+
|
|
488
|
+
${dim('Advanced Options:')}
|
|
489
|
+
${success('--profiles')} <src> Override SKU profiles source. Can be a URL, local file path,
|
|
490
|
+
or inline JSON. Default: CDN → cached → bundled.
|
|
491
|
+
Set FUNC_PROFILES_URL env var for persistent override.`.trim());
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function printStartHelp() {
|
|
495
|
+
console.log(`
|
|
496
|
+
${bold(title('fnx start'))} — Launch the Azure Functions host runtime.
|
|
497
|
+
|
|
498
|
+
${title('Usage:')} fnx start [options]
|
|
499
|
+
|
|
500
|
+
${title('Options:')}
|
|
501
|
+
${success('--sku')} <name> Target SKU to emulate (default: flex). Use ${success('--sku list')} to see options.
|
|
502
|
+
${success('--app-path')} <dir> Path to the function app directory (default: cwd).
|
|
503
|
+
${success('--port')} <port> Port for the host HTTP listener (default: 7071).
|
|
504
|
+
${success('--mcp-port')} <port> Port for the live MCP server (default: host port + 1).
|
|
505
|
+
${success('--verbose')} Show all host output (unfiltered).
|
|
506
|
+
${success('--no-mcp')} Disable the live MCP server.
|
|
507
|
+
${success('--no-azurite')} Skip automatic Azurite start.
|
|
508
|
+
${success('-h')}, ${success('--help')} Show this help message.
|
|
509
|
+
|
|
510
|
+
${title('Examples:')}
|
|
511
|
+
fnx start Start with default SKU (flex)
|
|
512
|
+
fnx start --sku flex --port 8080 Custom port
|
|
513
|
+
fnx start --sku windows-consumption Emulate Windows Consumption
|
|
514
|
+
fnx start --verbose Show all host output`.trim());
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function printSyncHelp() {
|
|
518
|
+
console.log(`
|
|
519
|
+
${bold(title('fnx sync'))} — Sync cached host/extensions with current catalog profile.
|
|
520
|
+
|
|
521
|
+
${title('Usage:')} fnx sync [host|extensions] [options]
|
|
522
|
+
|
|
523
|
+
${title('Options:')}
|
|
524
|
+
${success('--sku')} <name> Target SKU to sync (default: flex). Use ${success('--sku list')} to see options.
|
|
525
|
+
${success('--keep')} <n> Keep latest N versions in cache (default: 2).
|
|
526
|
+
${success('--force')} Re-download even if already cached.
|
|
527
|
+
${success('-h')}, ${success('--help')} Show this help message.
|
|
528
|
+
|
|
529
|
+
${title('Examples:')}
|
|
530
|
+
fnx sync Sync host and extensions for default SKU
|
|
531
|
+
fnx sync host Sync host only
|
|
532
|
+
fnx sync extensions Sync extensions only
|
|
533
|
+
fnx sync --force Force re-download`.trim());
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function printPackHelp() {
|
|
537
|
+
console.log(`
|
|
538
|
+
${bold(title('fnx pack'))} — Package a Functions app into a deployment zip.
|
|
539
|
+
|
|
540
|
+
${title('Usage:')} fnx pack [options]
|
|
541
|
+
|
|
542
|
+
${title('Options:')}
|
|
543
|
+
${success('--app-path')} <dir> Path to the function app directory (default: cwd).
|
|
544
|
+
${success('--runtime')} <name> Runtime identifier (default: auto-detected from config).
|
|
545
|
+
${success('--output')} <file> Output zip path (default: <app-name>.zip).
|
|
546
|
+
${success('--no-build')} Skip build steps for java/dotnet-isolated.
|
|
547
|
+
${success('-h')}, ${success('--help')} Show this help message.
|
|
548
|
+
|
|
549
|
+
${title('Supported runtimes:')} node, python, java, powershell, dotnet-isolated
|
|
550
|
+
|
|
551
|
+
${title('Examples:')}
|
|
552
|
+
fnx pack Package current directory
|
|
553
|
+
fnx pack --app-path ./my-app Package a specific app
|
|
554
|
+
fnx pack --runtime python --no-build Skip build step`.trim());
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function printWarmupHelp() {
|
|
233
558
|
console.log(`
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
variables into the host process. local.settings.json values take precedence.
|
|
282
|
-
|
|
283
|
-
Examples:
|
|
284
|
-
fnx start Start with default SKU (flex) in current directory
|
|
285
|
-
fnx start --sku flex Emulate Flex Consumption
|
|
286
|
-
fnx start --sku windows-consumption Emulate Windows Consumption (older host version)
|
|
287
|
-
fnx start --sku list List all available SKU profiles with host versions
|
|
288
|
-
fnx start --sku flex --port 8080 Start on a custom port
|
|
289
|
-
fnx start --scriptroot ./my-app Start from a specific function app directory
|
|
290
|
-
|
|
291
|
-
Side-by-side comparison:
|
|
292
|
-
# Terminal 1: Run as Flex Consumption
|
|
293
|
-
fnx start --sku flex --port 7071
|
|
294
|
-
|
|
295
|
-
# Terminal 2: Run as Windows Consumption (different host version)
|
|
296
|
-
fnx start --sku windows-consumption --port 7072
|
|
297
|
-
|
|
298
|
-
# Compare behavior across SKUs with the same function app!
|
|
299
|
-
|
|
300
|
-
MCP server (for VS Code Copilot / AI assistants):
|
|
301
|
-
fnx templates-mcp Start templates MCP server (stdio)
|
|
302
|
-
fnx start Also starts live MCP server on port+1
|
|
303
|
-
fnx start --mcp-port 9000 Live MCP server on custom port
|
|
304
|
-
fnx start --no-mcp Disable live MCP server
|
|
305
|
-
|
|
306
|
-
# .vscode/mcp.json — templates only (stdio):
|
|
307
|
-
# {
|
|
308
|
-
# "servers": {
|
|
309
|
-
# "azure-functions-templates": {
|
|
310
|
-
# "type": "stdio",
|
|
311
|
-
# "command": "fnx",
|
|
312
|
-
# "args": ["templates-mcp"]
|
|
313
|
-
# }
|
|
314
|
-
# }
|
|
315
|
-
# }
|
|
316
|
-
#
|
|
317
|
-
# .vscode/mcp.json — live host data (when fnx start is running):
|
|
318
|
-
# {
|
|
319
|
-
# "servers": {
|
|
320
|
-
# "fnx-functions-debug": {
|
|
321
|
-
# "type": "http",
|
|
322
|
-
# "url": "http://127.0.0.1:7072/mcp"
|
|
323
|
-
# }
|
|
324
|
-
# }
|
|
325
|
-
# }
|
|
326
|
-
|
|
327
|
-
Supported runtimes: node, python, java, powershell, dotnet-isolated
|
|
328
|
-
(.NET in-process / Microsoft.NET.Sdk.Functions is not supported — isolated worker model only)
|
|
329
|
-
`.trim());
|
|
559
|
+
${bold(title('fnx warmup'))} — Pre-download host binaries and extension bundles for offline use.
|
|
560
|
+
|
|
561
|
+
${title('Usage:')} fnx warmup [options]
|
|
562
|
+
|
|
563
|
+
${title('Options:')}
|
|
564
|
+
${success('--sku')} <name> Target SKU to warm (default: flex). Use ${success('--sku list')} to see options.
|
|
565
|
+
${success('--all')} Warm ALL available SKUs (useful for CI/build agents).
|
|
566
|
+
${success('--dry-run')} Show what would be downloaded without actually downloading.
|
|
567
|
+
${success('--force')} Re-download even if already cached.
|
|
568
|
+
${success('-h')}, ${success('--help')} Show this help message.
|
|
569
|
+
|
|
570
|
+
${title('Environment Variables:')}
|
|
571
|
+
FNX_SKIP_DOWNLOAD=1 Skip warmup entirely (useful for CI/Docker).
|
|
572
|
+
FNX_DEFAULT_SKU=<name> Warm a specific SKU instead of flex.
|
|
573
|
+
|
|
574
|
+
${title('Examples:')}
|
|
575
|
+
fnx warmup Pre-download default SKU (flex)
|
|
576
|
+
fnx warmup --all Warm ALL SKUs
|
|
577
|
+
fnx warmup --dry-run Show what would be downloaded
|
|
578
|
+
fnx warmup --force Re-download even if cached`.trim());
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function printTemplatesMcpHelp() {
|
|
582
|
+
console.log(`
|
|
583
|
+
${bold(title('fnx templates-mcp'))} — Start the Azure Functions templates MCP server.
|
|
584
|
+
|
|
585
|
+
${title('Usage:')} fnx templates-mcp
|
|
586
|
+
|
|
587
|
+
Starts a stdio-based MCP server that provides Azure Functions templates
|
|
588
|
+
and SKU profile information to AI assistants (VS Code Copilot, etc.).
|
|
589
|
+
|
|
590
|
+
${title('Features:')}
|
|
591
|
+
• 68 templates across 4 languages (JavaScript, TypeScript, Python, C#)
|
|
592
|
+
• SKU profile listing and resolution tools
|
|
593
|
+
• Drop-in replacement for manvir-templates-mcp-server
|
|
594
|
+
|
|
595
|
+
${title('VS Code Configuration:')}
|
|
596
|
+
Add to .vscode/mcp.json:
|
|
597
|
+
{
|
|
598
|
+
"servers": {
|
|
599
|
+
"azure-functions-templates": {
|
|
600
|
+
"type": "stdio",
|
|
601
|
+
"command": "fnx",
|
|
602
|
+
"args": ["templates-mcp"]
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}`.trim());
|
|
330
606
|
}
|