@vrdmr/fnx-test 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,140 @@
1
+ /**
2
+ * MCP template tool definitions for the hand-rolled MCP server.
3
+ * Imports handler functions from templates-mcp/dist (compiled TypeScript).
4
+ *
5
+ * Tools: get_languages_list, get_templates_list, get_template, get_project_template
6
+ */
7
+
8
+ import { dirname, join } from 'node:path';
9
+ import { fileURLToPath, pathToFileURL } from 'node:url';
10
+
11
+ const __dirname = dirname(fileURLToPath(import.meta.url));
12
+ const templatesMcpDist = join(__dirname, '..', '..', 'templates-mcp', 'dist', 'src');
13
+ const templatesRoot = join(__dirname, '..', '..', 'templates-mcp', 'templates');
14
+
15
+ let _handlers = null;
16
+ let _templates = null;
17
+
18
+ // Dynamic import() requires file:// URLs on Windows (backslash paths fail)
19
+ function toImportUrl(filePath) {
20
+ return pathToFileURL(filePath).href;
21
+ }
22
+
23
+ async function loadHandlers() {
24
+ if (!_handlers) {
25
+ _handlers = await import(toImportUrl(join(templatesMcpDist, 'handlers.js')));
26
+ }
27
+ return _handlers;
28
+ }
29
+
30
+ async function loadTemplates() {
31
+ if (!_templates) {
32
+ _templates = await import(toImportUrl(join(templatesMcpDist, 'templates.js')));
33
+ }
34
+ return _templates;
35
+ }
36
+
37
+ export async function getTemplateTools() {
38
+ const tmpl = await loadTemplates();
39
+ const validLanguages = tmpl.VALID_LANGUAGES;
40
+ const validTemplates = tmpl.VALID_TEMPLATES;
41
+ const supportedRuntimes = tmpl.SUPPORTED_RUNTIMES;
42
+
43
+ return [
44
+ {
45
+ name: 'get_languages_list',
46
+ description:
47
+ `Get supported programming languages for Azure Functions code development.\n\n` +
48
+ `Returns runtime versions, prerequisites, quick commands for each language.\n` +
49
+ `Start here when creating a new Azure Functions project.\n\n` +
50
+ `Workflow: get_languages_list → get_project_template → get_templates_list → get_template`,
51
+ inputSchema: { type: 'object', properties: {} },
52
+ async handler() {
53
+ const h = await loadHandlers();
54
+ return h.handleGetLanguagesList();
55
+ },
56
+ },
57
+ {
58
+ name: 'get_project_template',
59
+ description:
60
+ `Get project files for initializing a new Azure Functions app.\n\n` +
61
+ `Returns host.json, local.settings.json, language-specific files, and setup instructions.\n` +
62
+ `Call this BEFORE writing function code manually.\n\n` +
63
+ `Workflow: get_languages_list → get_project_template → get_templates_list → get_template`,
64
+ inputSchema: {
65
+ type: 'object',
66
+ properties: {
67
+ language: {
68
+ type: 'string',
69
+ enum: [...validLanguages],
70
+ description: `Programming language. Valid: ${validLanguages.join(', ')}`,
71
+ },
72
+ runtimeVersion: {
73
+ type: 'string',
74
+ description: 'Optional runtime version (e.g., Java JDK version, Node.js version)',
75
+ },
76
+ },
77
+ required: ['language'],
78
+ },
79
+ async handler(args) {
80
+ const h = await loadHandlers();
81
+ return h.handleGetProjectTemplate(args);
82
+ },
83
+ },
84
+ {
85
+ name: 'get_templates_list',
86
+ description:
87
+ `Browse available Azure Functions templates organized by binding type.\n\n` +
88
+ `Returns triggers, input bindings, and output bindings for a language.\n` +
89
+ `Call this to discover templates before writing function code.\n\n` +
90
+ `Workflow: get_languages_list → get_project_template → get_templates_list → get_template`,
91
+ inputSchema: {
92
+ type: 'object',
93
+ properties: {
94
+ language: {
95
+ type: 'string',
96
+ enum: [...validLanguages],
97
+ description: `Programming language. Valid: ${validLanguages.join(', ')}`,
98
+ },
99
+ },
100
+ required: ['language'],
101
+ },
102
+ async handler(args) {
103
+ const h = await loadHandlers();
104
+ return h.handleGetFunctionTemplatesList(args);
105
+ },
106
+ },
107
+ {
108
+ name: 'get_template',
109
+ description:
110
+ `Get complete, ready-to-use Azure Function code and configuration.\n\n` +
111
+ `ALWAYS call this instead of writing Azure Function code from scratch.\n` +
112
+ `Returns function source code, binding configuration, and integration guidance.\n\n` +
113
+ `Workflow: get_languages_list → get_project_template → get_templates_list → get_template`,
114
+ inputSchema: {
115
+ type: 'object',
116
+ properties: {
117
+ language: {
118
+ type: 'string',
119
+ enum: [...validLanguages],
120
+ description: `Programming language. Valid: ${validLanguages.join(', ')}`,
121
+ },
122
+ template: {
123
+ type: 'string',
124
+ description:
125
+ `Template name from get_templates_list (e.g., HttpTrigger, TimerTrigger, BlobTrigger)`,
126
+ },
127
+ runtimeVersion: {
128
+ type: 'string',
129
+ description: 'Optional runtime version for Java or TypeScript',
130
+ },
131
+ },
132
+ required: ['language', 'template'],
133
+ },
134
+ async handler(args) {
135
+ const h = await loadHandlers();
136
+ return h.handleGetFunctionTemplate(args, templatesRoot);
137
+ },
138
+ },
139
+ ];
140
+ }
@@ -0,0 +1,118 @@
1
+ import { readFile, writeFile, mkdir, stat } from 'node:fs/promises';
2
+ import { join, resolve as resolvePath, isAbsolute } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const CACHE_DIR = join(homedir(), '.fnx', 'profiles');
7
+ const CACHE_FILE = join(CACHE_DIR, 'sku-profiles.json');
8
+ const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
9
+
10
+ const DEFAULT_CDN_URL = 'https://raw.githubusercontent.com/vrdmr/func-emulate/main/fnx/profiles/sku-profiles.json';
11
+
12
+ // Bundled fallback (shipped with the POC)
13
+ const BUNDLED_PROFILES_PATH = fileURLToPath(new URL('../profiles/sku-profiles.json', import.meta.url));
14
+
15
+ let profilesSource = null; // Set via setProfilesSource()
16
+
17
+ export function setProfilesSource(source) {
18
+ profilesSource = source;
19
+ }
20
+
21
+ function isUrl(str) {
22
+ return str.startsWith('http://') || str.startsWith('https://');
23
+ }
24
+
25
+ function isJsonString(str) {
26
+ return str.trimStart().startsWith('{');
27
+ }
28
+
29
+ async function fetchRegistry() {
30
+ // If an explicit source was provided (--profiles flag or inline JSON), use it directly
31
+ if (profilesSource) {
32
+ // Inline JSON string
33
+ if (isJsonString(profilesSource)) {
34
+ return JSON.parse(profilesSource);
35
+ }
36
+
37
+ // URL (http/https)
38
+ if (isUrl(profilesSource)) {
39
+ try {
40
+ const res = await fetch(profilesSource);
41
+ if (res.ok) {
42
+ const json = await res.text();
43
+ await mkdir(CACHE_DIR, { recursive: true });
44
+ await writeFile(CACHE_FILE, json);
45
+ return JSON.parse(json);
46
+ }
47
+ } catch { /* fall through to error */ }
48
+ throw new Error(`Cannot fetch profiles from: ${profilesSource}`);
49
+ }
50
+
51
+ // Local file path
52
+ const filePath = isAbsolute(profilesSource) ? profilesSource : resolvePath(process.cwd(), profilesSource);
53
+ try {
54
+ return JSON.parse(await readFile(filePath, 'utf-8'));
55
+ } catch (err) {
56
+ throw new Error(`Cannot read profiles file: ${filePath} (${err.message})`);
57
+ }
58
+ }
59
+
60
+ // Default resolution chain: env var → cache → CDN → stale cache → bundled
61
+ const cdnUrl = process.env.FUNC_PROFILES_URL || DEFAULT_CDN_URL;
62
+
63
+ // 1. Try cache (if fresh)
64
+ try {
65
+ const cacheStat = await stat(CACHE_FILE);
66
+ if (Date.now() - cacheStat.mtimeMs < CACHE_TTL_MS) {
67
+ return JSON.parse(await readFile(CACHE_FILE, 'utf-8'));
68
+ }
69
+ } catch { /* no cache or stale */ }
70
+
71
+ // 2. Try CDN
72
+ try {
73
+ const res = await fetch(cdnUrl);
74
+ if (res.ok) {
75
+ const json = await res.text();
76
+ await mkdir(CACHE_DIR, { recursive: true });
77
+ await writeFile(CACHE_FILE, json);
78
+ return JSON.parse(json);
79
+ }
80
+ } catch { /* CDN unreachable */ }
81
+
82
+ // 3. Try stale cache
83
+ try {
84
+ return JSON.parse(await readFile(CACHE_FILE, 'utf-8'));
85
+ } catch { /* no cache at all */ }
86
+
87
+ // 4. Fall back to bundled profiles
88
+ try {
89
+ return JSON.parse(await readFile(BUNDLED_PROFILES_PATH, 'utf-8'));
90
+ } catch {
91
+ throw new Error('Cannot load SKU profiles: CDN unreachable, no cache, no bundled profiles.');
92
+ }
93
+ }
94
+
95
+ export async function resolveProfile(skuName) {
96
+ const registry = await fetchRegistry();
97
+ const profile = registry.profiles[skuName];
98
+ if (!profile) {
99
+ const valid = Object.keys(registry.profiles).join(', ');
100
+ throw new Error(`Unknown SKU '${skuName}'. Available: ${valid}`);
101
+ }
102
+ return profile;
103
+ }
104
+
105
+ export async function listProfiles() {
106
+ const registry = await fetchRegistry();
107
+ console.log('Available SKU profiles:\n');
108
+ console.log(' SKU Host Version Bundle Version Max Bundle Status');
109
+ console.log(' ─────────────────────── ──────────────────── ───────────────── ─────────── ──────────');
110
+ for (const [key, p] of Object.entries(registry.profiles)) {
111
+ const sku = key.padEnd(24);
112
+ const host = p.hostVersion.padEnd(21);
113
+ const bundle = p.extensionBundleVersion.padEnd(18);
114
+ const maxBundle = (p.maxExtensionBundleVersion || 'n/a').padEnd(12);
115
+ console.log(` ${sku}${host}${bundle}${maxBundle}${p.status}`);
116
+ }
117
+ console.log(`\n Last updated: ${registry.updatedAt}`);
118
+ }
package/lib/warmup.js ADDED
@@ -0,0 +1,203 @@
1
+ import { existsSync, readdirSync } from 'node:fs';
2
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { homedir } from 'node:os';
5
+ import { resolveProfile, listProfiles } from './profile-resolver.js';
6
+ import { ensureHost, ensureBundle, getHostExeName, getPlatformRid } from './host-manager.js';
7
+
8
+ const FNX_DIR = join(homedir(), '.fnx');
9
+ const META_FILE = join(FNX_DIR, '_meta.json');
10
+ const HOST_CACHE = join(FNX_DIR, 'hosts');
11
+ const BUNDLE_CACHE = join(FNX_DIR, 'bundles');
12
+ const BUNDLE_ID = 'Microsoft.Azure.Functions.ExtensionBundle';
13
+
14
+ function getFlag(args, flag) {
15
+ const idx = args.indexOf(flag);
16
+ return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : null;
17
+ }
18
+
19
+ export async function warmup(args) {
20
+ try {
21
+ await runWarmup(args);
22
+ } catch (err) {
23
+ // Never crash — postinstall must not break npm install
24
+ console.error(`fnx warmup: ${err.message}`);
25
+ }
26
+ }
27
+
28
+ async function runWarmup(args) {
29
+ if (args.includes('-h') || args.includes('--help')) {
30
+ printWarmupHelp();
31
+ return;
32
+ }
33
+
34
+ if (process.env.FNX_SKIP_DOWNLOAD === '1') {
35
+ console.log('fnx warmup: skipped (FNX_SKIP_DOWNLOAD=1)');
36
+ return;
37
+ }
38
+
39
+ const dryRun = args.includes('--dry-run');
40
+ const force = args.includes('--force');
41
+ const all = args.includes('--all');
42
+ let sku = getFlag(args, '--sku');
43
+
44
+ if (sku === 'list') {
45
+ await listProfiles();
46
+ return;
47
+ }
48
+
49
+ let targetSkus;
50
+ if (all) {
51
+ targetSkus = await getAllSkuNames();
52
+ } else {
53
+ sku = sku || process.env.FNX_DEFAULT_SKU || 'flex';
54
+ targetSkus = [sku];
55
+ }
56
+
57
+ const rid = getPlatformRid();
58
+
59
+ console.log();
60
+ console.log(`fnx warmup — pre-downloading assets for ${all ? 'ALL SKUs' : 'offline use'}`);
61
+ console.log();
62
+ console.log(` Platform: ${rid}`);
63
+
64
+ const meta = await loadMeta();
65
+
66
+ for (const skuName of targetSkus) {
67
+ try {
68
+ console.log();
69
+ const profile = await resolveProfile(skuName);
70
+
71
+ console.log(` Target SKU: ${profile.displayName} (${skuName})`);
72
+ console.log(` Host Version: ${profile.hostVersion}`);
73
+ if (profile.maxExtensionBundleVersion) {
74
+ console.log(` Bundle Range: ${profile.extensionBundleVersion} (max: ${profile.maxExtensionBundleVersion})`);
75
+ }
76
+ console.log();
77
+
78
+ if (dryRun) {
79
+ printDryRun(profile, force);
80
+ continue;
81
+ }
82
+
83
+ // Download host + bundle (reuse existing ensureHost/ensureBundle)
84
+ const hostDir = await ensureHost(profile, { force });
85
+ const bundleVersion = await ensureBundle(profile, { force });
86
+
87
+ // Update meta
88
+ if (!meta.warmedSkus.includes(skuName)) {
89
+ meta.warmedSkus.push(skuName);
90
+ }
91
+ meta.hosts[profile.hostVersion] = {
92
+ rid,
93
+ downloadedAt: new Date().toISOString(),
94
+ };
95
+ if (bundleVersion) {
96
+ meta.bundles[bundleVersion] = {
97
+ downloadedAt: new Date().toISOString(),
98
+ };
99
+ }
100
+
101
+ console.log();
102
+ console.log(` ✓ fnx start --sku ${skuName} will work offline.`);
103
+ } catch (err) {
104
+ console.error(` ⚠️ Warmup failed for ${skuName}: ${err.message}`);
105
+ }
106
+ }
107
+
108
+ if (!dryRun) {
109
+ meta.fnxVersion = await getFnxVersion();
110
+ meta.installedAt = new Date().toISOString();
111
+ meta.platform = rid;
112
+ await saveMeta(meta);
113
+ }
114
+
115
+ console.log();
116
+ console.log(' Done.');
117
+ console.log();
118
+ }
119
+
120
+ function printDryRun(profile, force) {
121
+ const hostExePath = join(HOST_CACHE, profile.hostVersion, getHostExeName());
122
+ const hostCached = !force && existsSync(hostExePath);
123
+
124
+ const bundleDir = join(BUNDLE_CACHE, BUNDLE_ID);
125
+ const bundleCached = !force && findAnyCachedBundle(bundleDir);
126
+
127
+ console.log(` [1/3] Profiles ✓ resolved`);
128
+ console.log(` [2/3] Host ${profile.hostVersion.padEnd(14)} ${hostCached ? '✓ cached' : '↓ needs download'}`);
129
+ console.log(` [3/3] Bundle ${bundleCached ? `✓ cached (${bundleCached})` : '↓ needs download'}`);
130
+ }
131
+
132
+ function findAnyCachedBundle(bundleDir) {
133
+ try {
134
+ const dirs = readdirSync(bundleDir);
135
+ for (const d of dirs) {
136
+ if (existsSync(join(bundleDir, d, 'bundle.json'))) {
137
+ return d;
138
+ }
139
+ }
140
+ } catch { /* not cached */ }
141
+ return null;
142
+ }
143
+
144
+ async function getAllSkuNames() {
145
+ try {
146
+ const profilesPath = new URL('../profiles/sku-profiles.json', import.meta.url).pathname;
147
+ const registry = JSON.parse(await readFile(profilesPath, 'utf-8'));
148
+ return Object.keys(registry.profiles);
149
+ } catch {
150
+ return ['flex', 'linux-premium', 'windows-consumption', 'windows-dedicated', 'linux-consumption'];
151
+ }
152
+ }
153
+
154
+ async function loadMeta() {
155
+ try {
156
+ return JSON.parse(await readFile(META_FILE, 'utf-8'));
157
+ } catch {
158
+ return { warmedSkus: [], hosts: {}, bundles: {} };
159
+ }
160
+ }
161
+
162
+ async function saveMeta(meta) {
163
+ await mkdir(FNX_DIR, { recursive: true });
164
+ await writeFile(META_FILE, JSON.stringify(meta, null, 2));
165
+ }
166
+
167
+ async function getFnxVersion() {
168
+ try {
169
+ const pkgPath = new URL('../package.json', import.meta.url).pathname;
170
+ const pkg = JSON.parse(await readFile(pkgPath, 'utf-8'));
171
+ return pkg.version;
172
+ } catch {
173
+ return 'unknown';
174
+ }
175
+ }
176
+
177
+ function printWarmupHelp() {
178
+ console.log(`
179
+ fnx warmup — Pre-download host binaries and extension bundles for offline use.
180
+
181
+ Usage: fnx warmup [options]
182
+
183
+ Options:
184
+ --sku <name> Target SKU to warm (default: flex). Use --sku list to see options.
185
+ --all Warm ALL available SKUs (useful for CI/build agents).
186
+ --dry-run Show what would be downloaded without actually downloading.
187
+ --force Re-download even if already cached.
188
+ -h, --help Show this help message.
189
+
190
+ Environment Variables:
191
+ FNX_SKIP_DOWNLOAD=1 Skip warmup entirely (useful for CI/Docker).
192
+ FNX_DEFAULT_SKU=<name> Warm a specific SKU instead of flex.
193
+
194
+ Examples:
195
+ fnx warmup Pre-download default SKU (flex)
196
+ fnx warmup --sku flex Explicit SKU
197
+ fnx warmup --sku windows-consumption Warm a specific SKU
198
+ fnx warmup --sku list Show available SKUs
199
+ fnx warmup --all Warm ALL SKUs
200
+ fnx warmup --dry-run Show what would be downloaded
201
+ fnx warmup --force Re-download even if cached
202
+ `.trim());
203
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@vrdmr/fnx-test",
3
+ "version": "0.1.1",
4
+ "description": "SKU-aware Azure Functions local emulator",
5
+ "type": "module",
6
+ "bin": {
7
+ "fnx": "./bin/fnx",
8
+ "fnx-template-mcp": "./bin/fnx-template-mcp"
9
+ },
10
+ "files": [
11
+ "bin/",
12
+ "lib/",
13
+ "profiles/",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "postinstall": "node ./bin/fnx warmup || echo 'fnx: warmup skipped (offline or error). Run fnx warmup manually.'"
18
+ },
19
+ "keywords": [
20
+ "azure-functions",
21
+ "serverless",
22
+ "emulator",
23
+ "sku",
24
+ "local-development",
25
+ "mcp"
26
+ ],
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/vrdmr/func-emulate"
30
+ },
31
+ "license": "MIT",
32
+ "engines": {
33
+ "node": ">=18"
34
+ },
35
+ "dependencies": {}
36
+ }
@@ -0,0 +1,87 @@
1
+ {
2
+ "schemaVersion": "1.0",
3
+ "profiles": {
4
+ "flex": {
5
+ "displayName": "Flex Consumption",
6
+ "hostVersion": "4.1047.100",
7
+ "hostGitTag": "v4.1047.100",
8
+ "extensionBundleVersion": "[4.22.*, 5.0.0)",
9
+ "maxExtensionBundleVersion": "4.99.0",
10
+ "hostPackageUrl": {
11
+ "linux-x64": "https://github.com/vrdmr/func-emulate/releases/download/host-v4.1047.100/azure-functions-v4.1047.100-linux-x64.zip",
12
+ "osx-x64": "https://github.com/vrdmr/func-emulate/releases/download/host-v4.1047.100/azure-functions-v4.1047.100-osx-x64.zip",
13
+ "osx-arm64": "https://github.com/vrdmr/func-emulate/releases/download/host-v4.1047.100/azure-functions-v4.1047.100-osx-arm64.zip",
14
+ "win-x64": "https://github.com/vrdmr/func-emulate/releases/download/host-v4.1047.100/azure-functions-v4.1047.100-win-x64.zip"
15
+ },
16
+ "status": "GA",
17
+ "dotnetModel": "isolated",
18
+ "notes": "Newest host — Flex gets bits first (2-week cadence)"
19
+ },
20
+ "linux-premium": {
21
+ "displayName": "Linux Premium (EP)",
22
+ "hostVersion": "4.1046.100",
23
+ "hostGitTag": "v4.1046.100",
24
+ "extensionBundleVersion": "[4.21.*, 5.0.0)",
25
+ "maxExtensionBundleVersion": "4.30.0",
26
+ "hostPackageUrl": {
27
+ "linux-x64": "https://github.com/vrdmr/func-emulate/releases/download/host-v4.1046.100/azure-functions-v4.1046.100-linux-x64.zip",
28
+ "osx-x64": "https://github.com/vrdmr/func-emulate/releases/download/host-v4.1046.100/azure-functions-v4.1046.100-osx-x64.zip",
29
+ "osx-arm64": "https://github.com/vrdmr/func-emulate/releases/download/host-v4.1046.100/azure-functions-v4.1046.100-osx-arm64.zip",
30
+ "win-x64": "https://github.com/vrdmr/func-emulate/releases/download/host-v4.1046.100/azure-functions-v4.1046.100-win-x64.zip"
31
+ },
32
+ "status": "GA",
33
+ "dotnetModel": "isolated",
34
+ "notes": "1 version behind Flex — aligned cadence but slightly delayed"
35
+ },
36
+ "windows-consumption": {
37
+ "displayName": "Windows Consumption",
38
+ "hostVersion": "4.1045.200",
39
+ "hostGitTag": "v4.1045.200",
40
+ "extensionBundleVersion": "[4.19.*, 5.0.0)",
41
+ "maxExtensionBundleVersion": "4.25.0",
42
+ "hostPackageUrl": {
43
+ "linux-x64": "https://github.com/vrdmr/func-emulate/releases/download/host-v4.1045.200/azure-functions-v4.1045.200-linux-x64.zip",
44
+ "osx-x64": "https://github.com/vrdmr/func-emulate/releases/download/host-v4.1045.200/azure-functions-v4.1045.200-osx-x64.zip",
45
+ "osx-arm64": "https://github.com/vrdmr/func-emulate/releases/download/host-v4.1045.200/azure-functions-v4.1045.200-osx-arm64.zip",
46
+ "win-x64": "https://github.com/vrdmr/func-emulate/releases/download/host-v4.1045.200/azure-functions-v4.1045.200-win-x64.zip"
47
+ },
48
+ "status": "GA",
49
+ "dotnetModel": "isolated",
50
+ "notes": "~3 month cadence — 2 versions behind Flex"
51
+ },
52
+ "windows-dedicated": {
53
+ "displayName": "Windows Dedicated (ASP)",
54
+ "hostVersion": "4.1045.100",
55
+ "hostGitTag": "v4.1045.100",
56
+ "extensionBundleVersion": "[4.19.*, 5.0.0)",
57
+ "maxExtensionBundleVersion": "4.25.0",
58
+ "hostPackageUrl": {
59
+ "linux-x64": "https://github.com/vrdmr/func-emulate/releases/download/host-v4.1045.100/azure-functions-v4.1045.100-linux-x64.zip",
60
+ "osx-x64": "https://github.com/vrdmr/func-emulate/releases/download/host-v4.1045.100/azure-functions-v4.1045.100-osx-x64.zip",
61
+ "osx-arm64": "https://github.com/vrdmr/func-emulate/releases/download/host-v4.1045.100/azure-functions-v4.1045.100-osx-arm64.zip",
62
+ "win-x64": "https://github.com/vrdmr/func-emulate/releases/download/host-v4.1045.100/azure-functions-v4.1045.100-win-x64.zip"
63
+ },
64
+ "status": "GA",
65
+ "dotnetModel": "isolated",
66
+ "notes": "Similar cadence to Windows Consumption"
67
+ },
68
+ "linux-consumption": {
69
+ "displayName": "Linux Consumption",
70
+ "hostVersion": "4.1044.400",
71
+ "hostGitTag": "v4.1044.400",
72
+ "extensionBundleVersion": "[4.18.*, 5.0.0)",
73
+ "maxExtensionBundleVersion": "4.22.0",
74
+ "hostPackageUrl": {
75
+ "linux-x64": "https://github.com/vrdmr/func-emulate/releases/download/host-v4.1044.400/azure-functions-v4.1044.400-linux-x64.zip",
76
+ "osx-x64": "https://github.com/vrdmr/func-emulate/releases/download/host-v4.1044.400/azure-functions-v4.1044.400-osx-x64.zip",
77
+ "osx-arm64": "https://github.com/vrdmr/func-emulate/releases/download/host-v4.1044.400/azure-functions-v4.1044.400-osx-arm64.zip",
78
+ "win-x64": "https://github.com/vrdmr/func-emulate/releases/download/host-v4.1044.400/azure-functions-v4.1044.400-win-x64.zip"
79
+ },
80
+ "status": "deprecated",
81
+ "dotnetModel": "isolated",
82
+ "retirementDate": "2028-09-30",
83
+ "notes": "Deprecated — oldest host, ~3x/year releases"
84
+ }
85
+ },
86
+ "updatedAt": "2026-02-15T00:00:00Z"
87
+ }