@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/profile-resolver.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir
|
|
1
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
2
3
|
import { join, resolve as resolvePath, isAbsolute } from 'node:path';
|
|
3
4
|
import { homedir } from 'node:os';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
5
6
|
|
|
6
7
|
const CACHE_DIR = join(homedir(), '.fnx', 'profiles');
|
|
7
8
|
const CACHE_FILE = join(CACHE_DIR, 'sku-profiles.json');
|
|
8
|
-
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
9
9
|
|
|
10
10
|
const DEFAULT_CDN_URL = 'https://raw.githubusercontent.com/vrdmr/func-emulate/main/fnx/profiles/sku-profiles.json';
|
|
11
11
|
|
|
@@ -26,72 +26,67 @@ function isJsonString(str) {
|
|
|
26
26
|
return str.trimStart().startsWith('{');
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
async function
|
|
30
|
-
|
|
29
|
+
async function persistCache(rawJson) {
|
|
30
|
+
await mkdir(CACHE_DIR, { recursive: true });
|
|
31
|
+
await writeFile(CACHE_FILE, rawJson);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function fetchRegistryWithMeta() {
|
|
35
|
+
// If an explicit source was provided (--profiles flag or inline JSON), use it directly.
|
|
31
36
|
if (profilesSource) {
|
|
32
|
-
// Inline JSON string
|
|
33
37
|
if (isJsonString(profilesSource)) {
|
|
34
|
-
return JSON.parse(profilesSource);
|
|
38
|
+
return { registry: JSON.parse(profilesSource), source: 'inline-json' };
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
// URL (http/https)
|
|
38
41
|
if (isUrl(profilesSource)) {
|
|
39
42
|
try {
|
|
40
43
|
const res = await fetch(profilesSource);
|
|
41
|
-
if (res.ok) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
throw new Error(`Cannot fetch profiles from: ${profilesSource}`);
|
|
44
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
45
|
+
const json = await res.text();
|
|
46
|
+
await persistCache(json);
|
|
47
|
+
return { registry: JSON.parse(json), source: 'remote', url: profilesSource };
|
|
48
|
+
} catch {
|
|
49
|
+
throw new Error(`Cannot fetch profiles from: ${profilesSource}`);
|
|
50
|
+
}
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
// Local file path
|
|
52
53
|
const filePath = isAbsolute(profilesSource) ? profilesSource : resolvePath(process.cwd(), profilesSource);
|
|
53
54
|
try {
|
|
54
|
-
return JSON.parse(await readFile(filePath, 'utf-8'));
|
|
55
|
+
return { registry: JSON.parse(await readFile(filePath, 'utf-8')), source: 'local-file', path: filePath };
|
|
55
56
|
} catch (err) {
|
|
56
57
|
throw new Error(`Cannot read profiles file: ${filePath} (${err.message})`);
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
// Default
|
|
61
|
+
// Default behavior: always attempt CDN first to detect upgrades/rollbacks quickly,
|
|
62
|
+
// then fall back to cache, then bundled profiles.
|
|
61
63
|
const cdnUrl = process.env.FUNC_PROFILES_URL || DEFAULT_CDN_URL;
|
|
62
64
|
|
|
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
65
|
try {
|
|
73
66
|
const res = await fetch(cdnUrl);
|
|
74
67
|
if (res.ok) {
|
|
75
68
|
const json = await res.text();
|
|
76
|
-
await
|
|
77
|
-
|
|
78
|
-
return JSON.parse(json);
|
|
69
|
+
await persistCache(json);
|
|
70
|
+
return { registry: JSON.parse(json), source: 'remote', url: cdnUrl };
|
|
79
71
|
}
|
|
80
72
|
} catch { /* CDN unreachable */ }
|
|
81
73
|
|
|
82
|
-
// 3. Try stale cache
|
|
83
74
|
try {
|
|
84
|
-
return JSON.parse(await readFile(CACHE_FILE, 'utf-8'));
|
|
85
|
-
} catch { /* no cache
|
|
75
|
+
return { registry: JSON.parse(await readFile(CACHE_FILE, 'utf-8')), source: 'cache' };
|
|
76
|
+
} catch { /* no cache */ }
|
|
86
77
|
|
|
87
|
-
// 4. Fall back to bundled profiles
|
|
88
78
|
try {
|
|
89
|
-
return JSON.parse(await readFile(BUNDLED_PROFILES_PATH, 'utf-8'));
|
|
79
|
+
return { registry: JSON.parse(await readFile(BUNDLED_PROFILES_PATH, 'utf-8')), source: 'bundled' };
|
|
90
80
|
} catch {
|
|
91
81
|
throw new Error('Cannot load SKU profiles: CDN unreachable, no cache, no bundled profiles.');
|
|
92
82
|
}
|
|
93
83
|
}
|
|
94
84
|
|
|
85
|
+
async function fetchRegistry() {
|
|
86
|
+
const { registry } = await fetchRegistryWithMeta();
|
|
87
|
+
return registry;
|
|
88
|
+
}
|
|
89
|
+
|
|
95
90
|
export async function resolveProfile(skuName) {
|
|
96
91
|
const registry = await fetchRegistry();
|
|
97
92
|
const profile = registry.profiles[skuName];
|
|
@@ -116,3 +111,17 @@ export async function listProfiles() {
|
|
|
116
111
|
}
|
|
117
112
|
console.log(`\n Last updated: ${registry.updatedAt}`);
|
|
118
113
|
}
|
|
114
|
+
|
|
115
|
+
/** Synchronous read: cached profiles → bundled fallback. No network. */
|
|
116
|
+
export function readProfilesSync() {
|
|
117
|
+
try {
|
|
118
|
+
if (existsSync(CACHE_FILE)) {
|
|
119
|
+
return JSON.parse(readFileSync(CACHE_FILE, 'utf-8'));
|
|
120
|
+
}
|
|
121
|
+
} catch { /* fall through */ }
|
|
122
|
+
try {
|
|
123
|
+
return JSON.parse(readFileSync(BUNDLED_PROFILES_PATH, 'utf-8'));
|
|
124
|
+
} catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
package/lib/warmup.js
CHANGED
|
@@ -2,8 +2,9 @@ import { existsSync, readdirSync } from 'node:fs';
|
|
|
2
2
|
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
|
-
import { resolveProfile, listProfiles } from './profile-resolver.js';
|
|
5
|
+
import { resolveProfile, listProfiles, fetchRegistryWithMeta } from './profile-resolver.js';
|
|
6
6
|
import { ensureHost, ensureBundle, getHostExeName, getPlatformRid } from './host-manager.js';
|
|
7
|
+
import { title, info, success, warning, dim } from './colors.js';
|
|
7
8
|
|
|
8
9
|
const FNX_DIR = join(homedir(), '.fnx');
|
|
9
10
|
const META_FILE = join(FNX_DIR, '_meta.json');
|
|
@@ -57,9 +58,9 @@ async function runWarmup(args) {
|
|
|
57
58
|
const rid = getPlatformRid();
|
|
58
59
|
|
|
59
60
|
console.log();
|
|
60
|
-
console.log(`fnx warmup — pre-downloading assets for ${all ? 'ALL SKUs' : 'offline use'}`);
|
|
61
|
+
console.log(title(`fnx warmup — pre-downloading assets for ${all ? 'ALL SKUs' : 'offline use'}`));
|
|
61
62
|
console.log();
|
|
62
|
-
console.log(` Platform: ${rid}`);
|
|
63
|
+
console.log(` ${dim('Platform:')} ${info(rid)}`);
|
|
63
64
|
|
|
64
65
|
const meta = await loadMeta();
|
|
65
66
|
|
|
@@ -68,10 +69,10 @@ async function runWarmup(args) {
|
|
|
68
69
|
console.log();
|
|
69
70
|
const profile = await resolveProfile(skuName);
|
|
70
71
|
|
|
71
|
-
console.log(` Target SKU: ${profile.displayName} (${skuName})`);
|
|
72
|
-
console.log(` Host Version: ${profile.hostVersion}`);
|
|
72
|
+
console.log(` ${dim('Target SKU:')} ${info(`${profile.displayName} (${skuName})`)}`);
|
|
73
|
+
console.log(` ${dim('Host Version:')} ${info(profile.hostVersion)}`);
|
|
73
74
|
if (profile.maxExtensionBundleVersion) {
|
|
74
|
-
console.log(` Bundle Range: ${profile.extensionBundleVersion} (max: ${profile.maxExtensionBundleVersion})`);
|
|
75
|
+
console.log(` ${dim('Bundle Range:')} ${info(`${profile.extensionBundleVersion} (max: ${profile.maxExtensionBundleVersion})`)}`);
|
|
75
76
|
}
|
|
76
77
|
console.log();
|
|
77
78
|
|
|
@@ -99,9 +100,9 @@ async function runWarmup(args) {
|
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
console.log();
|
|
102
|
-
console.log(` ✓ fnx start --sku ${skuName} will work offline.`);
|
|
103
|
+
console.log(success(` ✓ fnx start --sku ${skuName} will work offline.`));
|
|
103
104
|
} catch (err) {
|
|
104
|
-
console.error(` ⚠️ Warmup failed for ${skuName}: ${err.message}`);
|
|
105
|
+
console.error(warning(` ⚠️ Warmup failed for ${skuName}: ${err.message}`));
|
|
105
106
|
}
|
|
106
107
|
}
|
|
107
108
|
|
|
@@ -113,7 +114,7 @@ async function runWarmup(args) {
|
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
console.log();
|
|
116
|
-
console.log(' Done.');
|
|
117
|
+
console.log(success(' Done.'));
|
|
117
118
|
console.log();
|
|
118
119
|
}
|
|
119
120
|
|
|
@@ -143,9 +144,8 @@ function findAnyCachedBundle(bundleDir) {
|
|
|
143
144
|
|
|
144
145
|
async function getAllSkuNames() {
|
|
145
146
|
try {
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
return Object.keys(registry.profiles);
|
|
147
|
+
const { registry } = await fetchRegistryWithMeta();
|
|
148
|
+
return Object.keys(registry.profiles || {});
|
|
149
149
|
} catch {
|
|
150
150
|
return ['flex', 'linux-premium', 'windows-consumption', 'windows-dedicated', 'linux-consumption'];
|
|
151
151
|
}
|