flxvm 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 flvm contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # flvm
2
+
3
+ Flutter 多版本管理器 —— 基于 git worktree + 共享引擎缓存,专为**多分支场景**(OHOS 适配、内部 fork)设计,macOS / Windows / Linux 通用。
4
+
5
+ ## 特性
6
+
7
+ - **秒级切换**:切换 = 改一个文件,全局立即生效
8
+ - **多版本并存**:所有版本同时就绪,不同终端可跑不同版本
9
+ - **省磁盘**:所有 worktree 共享同一份 `.git`;同引擎版本的补丁分支(如 `br_3.27.4-ohos-1.0.4` → `1.0.5`)**共享 `bin/cache`(Dart SDK + 引擎产物)**
10
+ - **分支更新零下载**:`flvm update` 重建 worktree 但保留缓存,升级补丁分支不重下 SDK
11
+ - **自动安装**:`flvm use` 未安装时自动从远程分支创建,支持模糊匹配
12
+
13
+ ## 安装
14
+
15
+ ```bash
16
+ npm install -g flvm
17
+ ```
18
+
19
+ ## 使用
20
+
21
+ ```bash
22
+ flvm init # 初始化,存储在默认目录 ~/.flvm
23
+ flvm init ~/BigDisk/flvm # 指定存储目录(macOS/Linux,支持 ~ 与相对路径)
24
+ flvm init D:\flvm # 指定存储目录(Windows)
25
+ flvm list -r # 查看远程可安装分支
26
+ flvm use 3.27.4 # 安装并切换(模糊匹配 br_3.27.4-ohos-1.0.4)
27
+ flvm use 3.22 # 切到已装版本(唯一匹配即可)
28
+ flvm list # 查看已装版本(* 为当前)
29
+ flvm update # 全部版本更新到各自分支最新(保留缓存)
30
+ flvm remove 3.22.3 # 删除版本(--purge-cache 顺带清理无引用缓存)
31
+ ```
32
+
33
+ 初始化后 `flutter` / `dart` 命令由 `<存储目录>/bin` 代理,始终执行当前选择的版本。
34
+
35
+ 存储目录优先级:`FLVM_HOME` 环境变量 > `flvm init <dir>` 设定的目录(记录在 `~/.flvm-home`)> 默认 `~/.flvm`。要恢复默认目录,运行 `flvm init ~/.flvm`。
36
+
37
+ ## 工作原理
38
+
39
+ ```
40
+ <存储目录>/ # 默认 ~/.flvm,可用 flvm init <dir> 指定
41
+ ├── repo/ # 主仓库,唯一 .git(所有版本共享)
42
+ ├── worktrees/ # 各版本源码(git worktree,~400MB/版本)
43
+ │ └── 3.27.4/ # bin/cache → 软链到 caches/3.27.4
44
+ ├── caches/ # 引擎缓存真身(同引擎版本的补丁分支共享一份)
45
+ ├── bin/ # flutter/dart 代理命令
46
+ └── default # 当前版本名
47
+ ```
48
+
49
+ 切换原理:`flvm use <名>` 只重写 `default` 文件,代理命令按它转发,因此**无需修改 PATH、无需重新下载任何东西**。引擎缓存按版本号(从分支名提取 `x.y.z`)分组共享;若引擎 hash 不匹配,flutter 工具会自动重新下载该组缓存(自愈,不会出错)。
50
+
51
+ ## Windows 注意
52
+
53
+ - `flvm init` 自动设置 `core.longpaths` 与 `core.autocrlf=false`
54
+ - 缓存链接使用 NTFS junction,无需管理员权限
55
+ - 建议为 `~/.flvm` 添加 Windows Defender 排除项以提升速度
56
+
57
+ ## 环境变量
58
+
59
+ - `FLVM_HOME`:临时覆盖存储目录(优先级最高,不持久化)
60
+ - `flvm init <dir>`:持久设定存储目录(推荐做法,记录在 `~/.flvm-home`)
61
+ - `flvm init --no-mirror`:不写入国内镜像(默认写入 `PUB_HOSTED_URL` / `FLUTTER_STORAGE_BASE_URL`)
62
+
63
+ ## 开发
64
+
65
+ ```bash
66
+ npm install
67
+ npm run build # tsc 编译
68
+ npm test # 编译 + 冒烟测试(本地假 git 仓库全流程)
69
+ ```
70
+
71
+ ## License
72
+
73
+ MIT
package/dist/cache.js ADDED
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.cacheGroupFor = cacheGroupFor;
7
+ exports.linkCache = linkCache;
8
+ exports.unlinkCache = unlinkCache;
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const paths_1 = require("./paths");
12
+ const util_1 = require("./util");
13
+ /** 从分支名提取缓存组:br_3.27.4-ohos-1.0.4 → 3.27.4(同引擎版本的补丁分支共享缓存) */
14
+ function cacheGroupFor(ref) {
15
+ const m = ref.match(/(\d+\.\d+\.\d+)/);
16
+ return m ? m[1] : ref.replace(/[/\\]/g, '-');
17
+ }
18
+ /**
19
+ * 删除链接本身(symlink / junction),绝不触碰其指向的共享缓存真身。
20
+ * 注意:不能用 fs.rmSync —— Node 对"指向目录的 symlink"会误判为目录抛 EISDIR。
21
+ */
22
+ function removeLink(link) {
23
+ try {
24
+ node_fs_1.default.unlinkSync(link);
25
+ }
26
+ catch {
27
+ try {
28
+ node_fs_1.default.rmdirSync(link); // Windows junction 兜底
29
+ }
30
+ catch {
31
+ /* 不存在即可 */
32
+ }
33
+ }
34
+ }
35
+ /**
36
+ * 把 <worktree>/bin/cache 链接到共享缓存目录。
37
+ * macOS/Linux 用 symlink,Windows 用 junction(无需管理员权限)。
38
+ */
39
+ function linkCache(worktreeDir, group) {
40
+ const cacheDir = node_path_1.default.join((0, paths_1.cachesDir)(), group);
41
+ const link = node_path_1.default.join(worktreeDir, 'bin', 'cache');
42
+ node_fs_1.default.mkdirSync(cacheDir, { recursive: true });
43
+ if ((0, util_1.exists)(link)) {
44
+ const st = node_fs_1.default.lstatSync(link);
45
+ if (st.isSymbolicLink()) {
46
+ removeLink(link);
47
+ }
48
+ else {
49
+ // 真实目录(个别分支提交过占位文件):删除后替换为链接
50
+ node_fs_1.default.rmSync(link, { recursive: true, force: true });
51
+ }
52
+ }
53
+ node_fs_1.default.symlinkSync(cacheDir, link, process.platform === 'win32' ? 'junction' : 'dir');
54
+ }
55
+ /**
56
+ * 摘除 cache 链接(只删链接本身,绝不触碰共享缓存真身)。
57
+ * 必须在 git worktree remove 之前调用,避免误删共享缓存内容。
58
+ */
59
+ function unlinkCache(worktreeDir) {
60
+ const link = node_path_1.default.join(worktreeDir, 'bin', 'cache');
61
+ if ((0, util_1.exists)(link) && node_fs_1.default.lstatSync(link).isSymbolicLink()) {
62
+ removeLink(link);
63
+ }
64
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ var __importDefault = (this && this.__importDefault) || function (mod) {
37
+ return (mod && mod.__esModule) ? mod : { "default": mod };
38
+ };
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ const commander_1 = require("commander");
41
+ const picocolors_1 = __importDefault(require("picocolors"));
42
+ const init = __importStar(require("./commands/init"));
43
+ const use = __importStar(require("./commands/use"));
44
+ const list = __importStar(require("./commands/list"));
45
+ const remove = __importStar(require("./commands/remove"));
46
+ const update = __importStar(require("./commands/update"));
47
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
48
+ const pkg = require('../package.json');
49
+ const program = new commander_1.Command();
50
+ program
51
+ .name('flvm')
52
+ .version(pkg.version)
53
+ .description('Flutter 多版本管理器(git worktree + 共享引擎缓存)');
54
+ program
55
+ .command('init [dir]')
56
+ .description('初始化:<dir> 自定义存储目录(如 flvm init D:\\flvm,默认 ~/.flvm)')
57
+ .option('--url <git-url>', 'Flutter 仓库地址(支持 OHOS fork)', init.DEFAULT_URL)
58
+ .option('--no-mirror', '不写入国内镜像环境变量')
59
+ .option('--no-shell', '不修改 shell 配置文件(自行配置 PATH)')
60
+ .action(init.run);
61
+ program
62
+ .command('use [query]')
63
+ .description('切换版本(未安装时自动安装,支持模糊匹配;无参数显示当前版本)')
64
+ .action(use.run);
65
+ program
66
+ .command('list')
67
+ .alias('ls')
68
+ .description('列出已安装版本')
69
+ .option('-r, --remote', '显示远程可安装的分支')
70
+ .action(list.run);
71
+ program
72
+ .command('remove <query>')
73
+ .alias('rm')
74
+ .description('删除已安装版本(共享缓存默认保留)')
75
+ .option('-f, --force', '忽略未提交改动')
76
+ .option('--purge-cache', '同时删除不再被引用的共享缓存')
77
+ .action(remove.run);
78
+ program
79
+ .command('update')
80
+ .description('更新所有已安装版本到各自分支最新提交(保留缓存,秒级完成)')
81
+ .option('-f, --force', '忽略未提交改动')
82
+ .action(update.run);
83
+ program.parseAsync(process.argv).catch((err) => {
84
+ console.error(picocolors_1.default.red(`✗ ${err.message}`));
85
+ process.exit(1);
86
+ });
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DEFAULT_URL = void 0;
7
+ exports.run = run;
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const picocolors_1 = __importDefault(require("picocolors"));
11
+ const paths_1 = require("../paths");
12
+ const git_1 = require("../git");
13
+ const shim_1 = require("../shim");
14
+ const util_1 = require("../util");
15
+ exports.DEFAULT_URL = 'https://gitcode.com/openharmony-tpc/flutter_flutter.git';
16
+ async function run(dir, opts) {
17
+ // 0. 指定存储目录:记录到 ~/.flvm-home,之后所有命令与 shim 都使用该目录
18
+ if (dir) {
19
+ const oldHome = (0, paths_1.flvmHome)();
20
+ const abs = (0, paths_1.setFlvmHome)(dir);
21
+ console.log(picocolors_1.default.green(`✓ 存储目录已设定: ${abs}`));
22
+ console.log(picocolors_1.default.dim(' 已记录,后续所有 flvm 命令与 flutter 代理命令都会使用此目录'));
23
+ if (oldHome !== abs && (0, util_1.exists)(node_path_1.default.join(oldHome, 'registry.json'))) {
24
+ console.log(picocolors_1.default.yellow(` 提示: ${oldHome} 下已有安装数据,切换目录后将不再使用它们`));
25
+ }
26
+ }
27
+ const home = (0, paths_1.flvmHome)();
28
+ node_fs_1.default.mkdirSync(home, { recursive: true });
29
+ node_fs_1.default.mkdirSync((0, paths_1.worktreesDir)(), { recursive: true });
30
+ node_fs_1.default.mkdirSync((0, paths_1.cachesDir)(), { recursive: true });
31
+ // 1. 主仓库(唯一 .git,之后所有版本共享)
32
+ if (!(0, util_1.exists)((0, paths_1.repoDir)())) {
33
+ console.log(picocolors_1.default.bold('克隆 Flutter 主仓库(仅此一次,之后所有版本共享 .git)…'));
34
+ console.log(picocolors_1.default.dim(` ${opts.url}`));
35
+ (0, git_1.gitClone)(opts.url, (0, paths_1.repoDir)());
36
+ }
37
+ else {
38
+ console.log(picocolors_1.default.dim('主仓库已存在,跳过克隆'));
39
+ }
40
+ // 2. Windows 兼容配置(longpaths 防深层路径 checkout 失败;autocrlf 防 flutter 自检报错)
41
+ if (process.platform === 'win32') {
42
+ (0, git_1.git)(['config', 'core.longpaths', 'true'], { cwd: (0, paths_1.repoDir)() });
43
+ (0, git_1.git)(['config', 'core.autocrlf', 'false'], { cwd: (0, paths_1.repoDir)() });
44
+ }
45
+ // 3. git 排除规则:bin/cache 链接不计为未跟踪文件
46
+ (0, git_1.ensureGitExclude)();
47
+ // 3. flutter / dart 代理命令
48
+ (0, shim_1.installShims)();
49
+ // 4. shell PATH 集成
50
+ if (opts.shell) {
51
+ const written = (0, shim_1.setupShellIntegration)(opts.mirror);
52
+ for (const f of written) {
53
+ console.log(picocolors_1.default.green(`✓ 已写入 PATH 配置: ${f}`));
54
+ }
55
+ if (written.length === 0) {
56
+ console.log(picocolors_1.default.dim('shell 配置已包含 flvm,跳过'));
57
+ }
58
+ }
59
+ console.log('');
60
+ console.log(picocolors_1.default.green('✓ flvm 初始化完成'));
61
+ console.log('');
62
+ console.log('下一步:');
63
+ console.log(` ${picocolors_1.default.cyan('flvm list -r')} 查看远程可安装的分支`);
64
+ console.log(` ${picocolors_1.default.cyan('flvm use 3.27.4')} 安装并切换(首次会自动创建 worktree)`);
65
+ if (opts.shell) {
66
+ console.log(picocolors_1.default.dim(' 注意: 请打开新终端使 PATH 生效;若 PATH 中已有其他 flutter,请将其移除'));
67
+ }
68
+ else {
69
+ console.log(picocolors_1.default.dim(` 注意: 已跳过 shell 配置,请手动将 ${home}/bin 加入 PATH`));
70
+ }
71
+ }
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.run = run;
7
+ const picocolors_1 = __importDefault(require("picocolors"));
8
+ const paths_1 = require("../paths");
9
+ const git_1 = require("../git");
10
+ const util_1 = require("../util");
11
+ const worktree_1 = require("../worktree");
12
+ async function run(opts) {
13
+ (0, util_1.ensureInit)();
14
+ if (!opts.remote) {
15
+ const reg = (0, paths_1.readRegistry)();
16
+ const names = Object.keys(reg);
17
+ if (names.length === 0) {
18
+ console.log('尚未安装任何版本');
19
+ console.log(` 运行 ${picocolors_1.default.cyan('flvm list -r')} 查看远程分支,${picocolors_1.default.cyan('flvm use <版本>')} 安装`);
20
+ return;
21
+ }
22
+ const cur = (0, paths_1.getDefault)();
23
+ console.log(picocolors_1.default.bold('已安装版本:'));
24
+ for (const name of names) {
25
+ const e = reg[name];
26
+ const mark = name === cur ? picocolors_1.default.green('*') : ' ';
27
+ console.log(` ${mark} ${name.padEnd(12)} ${e.ref.padEnd(28)} ` +
28
+ `${e.commit.slice(0, 8)} ${(0, git_1.commitDate)(e.commit)} cache:${e.cacheGroup}`);
29
+ }
30
+ return;
31
+ }
32
+ // 远程分支视图
33
+ (0, git_1.fetchAll)();
34
+ const branches = (0, git_1.listRemoteBranches)();
35
+ if (branches.length === 0) {
36
+ console.log(picocolors_1.default.yellow('远程没有分支,请检查 flvm init --url'));
37
+ return;
38
+ }
39
+ const installedRefs = new Set(Object.values((0, paths_1.readRegistry)()).map((e) => e.ref));
40
+ console.log(picocolors_1.default.bold(`远程分支(共 ${branches.length} 个):`));
41
+ for (const b of branches.sort()) {
42
+ const tag = installedRefs.has(b) ? picocolors_1.default.green(' [已安装]') : '';
43
+ console.log(` ${(0, worktree_1.nameForRef)(b).padEnd(12)} ${b}${tag}`);
44
+ }
45
+ console.log('');
46
+ console.log(picocolors_1.default.dim(`安装: flvm use <版本号或分支名关键词>`));
47
+ }
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.run = run;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const picocolors_1 = __importDefault(require("picocolors"));
10
+ const paths_1 = require("../paths");
11
+ const git_1 = require("../git");
12
+ const cache_1 = require("../cache");
13
+ const util_1 = require("../util");
14
+ const worktree_1 = require("../worktree");
15
+ async function run(query, opts) {
16
+ (0, util_1.ensureInit)();
17
+ const m = (0, worktree_1.findInstalled)(query);
18
+ if (m.kind === 'none') {
19
+ console.log(picocolors_1.default.red(`✗ 未找到版本 "${query}",运行 flvm list 查看已安装版本`));
20
+ return;
21
+ }
22
+ if (m.kind === 'ambiguous') {
23
+ (0, worktree_1.printCandidates)('匹配到多个已安装版本:', m.candidates);
24
+ return;
25
+ }
26
+ const { name, entry } = m;
27
+ const dir = node_path_1.default.join((0, paths_1.worktreesDir)(), name);
28
+ if (!opts.force && (0, git_1.isDirty)(dir)) {
29
+ console.log(picocolors_1.default.red(`✗ ${name} 有未提交改动,如确认放弃请使用: flvm remove ${name} --force`));
30
+ return;
31
+ }
32
+ // 先摘缓存链接(只删链接本身),再删 worktree —— 保证共享缓存不被误删
33
+ (0, cache_1.unlinkCache)(dir);
34
+ if ((0, util_1.exists)(dir))
35
+ (0, git_1.worktreeRemove)(dir);
36
+ const reg = (0, paths_1.readRegistry)();
37
+ delete reg[name];
38
+ (0, paths_1.writeRegistry)(reg);
39
+ // 处理共享缓存
40
+ if (opts.purgeCache) {
41
+ const stillUsed = Object.values(reg).some((e) => e.cacheGroup === entry.cacheGroup);
42
+ if (stillUsed) {
43
+ console.log(picocolors_1.default.dim(` 缓存组 ${entry.cacheGroup} 仍被其他版本使用,保留`));
44
+ }
45
+ else {
46
+ node_fs_1.default.rmSync(node_path_1.default.join((0, paths_1.cachesDir)(), entry.cacheGroup), { recursive: true, force: true });
47
+ console.log(picocolors_1.default.dim(` 已删除缓存组 ${entry.cacheGroup}`));
48
+ }
49
+ }
50
+ if ((0, paths_1.getDefault)() === name) {
51
+ (0, paths_1.setDefault)(null);
52
+ console.log(picocolors_1.default.yellow(` 当前默认版本已被删除,请重新 flvm use`));
53
+ }
54
+ console.log(picocolors_1.default.green(`✓ 已删除 ${name} (${entry.ref})`));
55
+ }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.run = run;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const picocolors_1 = __importDefault(require("picocolors"));
9
+ const paths_1 = require("../paths");
10
+ const git_1 = require("../git");
11
+ const cache_1 = require("../cache");
12
+ const util_1 = require("../util");
13
+ /** 更新全部已安装版本到各自分支的最新提交(保留共享缓存,不重新下载 SDK) */
14
+ async function run(opts) {
15
+ (0, util_1.ensureInit)();
16
+ const reg = (0, paths_1.readRegistry)();
17
+ const names = Object.keys(reg);
18
+ if (names.length === 0) {
19
+ console.log('没有已安装版本,运行 flvm use <版本> 先安装');
20
+ return;
21
+ }
22
+ console.log(picocolors_1.default.dim('拉取远程更新…'));
23
+ (0, git_1.fetchAll)();
24
+ (0, git_1.worktreePrune)();
25
+ let updated = 0;
26
+ let uptodate = 0;
27
+ let skipped = 0;
28
+ for (const name of names) {
29
+ const e = reg[name];
30
+ const dir = node_path_1.default.join((0, paths_1.worktreesDir)(), name);
31
+ try {
32
+ const { spec, commit } = (0, git_1.resolveRef)(e.ref);
33
+ if ((0, util_1.exists)(dir) && commit === e.commit) {
34
+ console.log(`${picocolors_1.default.dim('=')} ${name} ${picocolors_1.default.dim('已最新')}`);
35
+ uptodate++;
36
+ continue;
37
+ }
38
+ if (!(0, util_1.exists)(dir)) {
39
+ // worktree 目录丢失(曾被手动删除)→ 直接重建
40
+ (0, git_1.worktreeAdd)(dir, spec);
41
+ (0, cache_1.linkCache)(dir, e.cacheGroup);
42
+ e.commit = commit;
43
+ e.updatedAt = new Date().toISOString();
44
+ console.log(`${picocolors_1.default.green('✓')} ${name} ${picocolors_1.default.dim('重建')} ${e.ref} (${commit.slice(0, 8)})`);
45
+ updated++;
46
+ continue;
47
+ }
48
+ if (!opts.force && (0, git_1.isDirty)(dir)) {
49
+ console.log(`${picocolors_1.default.yellow('!')} ${name} ${picocolors_1.default.yellow('有未提交改动,跳过(--force 强制)')}`);
50
+ skipped++;
51
+ continue;
52
+ }
53
+ // 保缓存更新:摘链接 → 删 worktree → 建新 worktree → 挂回链接
54
+ (0, cache_1.unlinkCache)(dir);
55
+ (0, git_1.worktreeRemove)(dir);
56
+ (0, git_1.worktreeAdd)(dir, spec);
57
+ (0, cache_1.linkCache)(dir, e.cacheGroup);
58
+ const old = e.commit.slice(0, 8);
59
+ e.commit = commit;
60
+ e.updatedAt = new Date().toISOString();
61
+ console.log(`${picocolors_1.default.green('✓')} ${name} ${e.ref} ${picocolors_1.default.dim(`${old} → ${commit.slice(0, 8)}`)}`);
62
+ updated++;
63
+ }
64
+ catch (err) {
65
+ console.log(`${picocolors_1.default.red('!')} ${name} 更新失败: ${err.message.split('\n')[0]}`);
66
+ skipped++;
67
+ }
68
+ }
69
+ (0, paths_1.writeRegistry)(reg);
70
+ console.log('');
71
+ console.log(`结果: ${picocolors_1.default.green(`${updated} 更新`)} / ${uptodate} 已最新 / ${skipped} 跳过`);
72
+ }
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.run = run;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const picocolors_1 = __importDefault(require("picocolors"));
9
+ const paths_1 = require("../paths");
10
+ const git_1 = require("../git");
11
+ const util_1 = require("../util");
12
+ const worktree_1 = require("../worktree");
13
+ function printCurrent() {
14
+ const name = (0, paths_1.getDefault)();
15
+ if (!name) {
16
+ console.log(picocolors_1.default.yellow('当前未选择版本,运行: flvm use <版本>'));
17
+ return;
18
+ }
19
+ const e = (0, paths_1.readRegistry)()[name];
20
+ console.log(picocolors_1.default.green(`✓ Flutter → ${name}`));
21
+ if (e)
22
+ console.log(` 分支: ${e.ref} (${e.commit.slice(0, 8)})`);
23
+ console.log(` 路径: ${node_path_1.default.join((0, paths_1.worktreesDir)(), name)}`);
24
+ }
25
+ async function run(query) {
26
+ (0, util_1.ensureInit)();
27
+ if (!query) {
28
+ printCurrent();
29
+ return;
30
+ }
31
+ // 1. 已安装:精确名 / 模糊匹配 → 直接切换
32
+ const m = (0, worktree_1.findInstalled)(query);
33
+ if (m.kind === 'ok') {
34
+ (0, paths_1.setDefault)(m.name);
35
+ printCurrent();
36
+ return;
37
+ }
38
+ if (m.kind === 'ambiguous') {
39
+ (0, worktree_1.printCandidates)('匹配到多个已安装版本:', m.candidates);
40
+ return;
41
+ }
42
+ // 2. 未安装:拉取远程分支并模糊匹配 → 自动安装
43
+ console.log(picocolors_1.default.dim('未匹配到已安装版本,正在获取远程分支…'));
44
+ (0, git_1.fetchAll)();
45
+ const branches = (0, git_1.listRemoteBranches)().filter((b) => b.includes(query));
46
+ if (branches.length === 0) {
47
+ console.log(picocolors_1.default.red(`✗ 没有匹配 "${query}" 的远程分支,运行 flvm list -r 查看全部`));
48
+ return;
49
+ }
50
+ if (branches.length > 1) {
51
+ console.log(`匹配到多个远程分支:`);
52
+ for (const b of branches) {
53
+ console.log(` ${(0, worktree_1.nameForRef)(b).padEnd(12)} ${b}`);
54
+ }
55
+ console.log('请使用更精确的名称重试');
56
+ return;
57
+ }
58
+ const ref = branches[0];
59
+ const name = (0, worktree_1.nameForRef)(ref);
60
+ const reg = (0, paths_1.readRegistry)();
61
+ if (reg[name] && reg[name].ref !== ref) {
62
+ throw new Error(`版本名 "${name}" 已被 ${reg[name].ref} 占用。\n` +
63
+ ` 如需换分支: flvm remove ${name} 后重新 flvm use ${ref}`);
64
+ }
65
+ console.log(`安装 ${picocolors_1.default.cyan(ref)} → worktree "${name}" …`);
66
+ const entry = (0, worktree_1.installWorktree)(name, ref);
67
+ (0, paths_1.setDefault)(name);
68
+ console.log(picocolors_1.default.green(`✓ Flutter → ${name} (${entry.ref})`));
69
+ console.log(` 路径: ${node_path_1.default.join((0, paths_1.worktreesDir)(), name)}`);
70
+ console.log(picocolors_1.default.dim(' 首次运行 flutter 会下载 Dart SDK / 引擎产物(同引擎版本的分支将共享此缓存)'));
71
+ }
package/dist/git.js ADDED
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ensureGitExclude = ensureGitExclude;
7
+ exports.git = git;
8
+ exports.gitClone = gitClone;
9
+ exports.fetchAll = fetchAll;
10
+ exports.listRemoteBranches = listRemoteBranches;
11
+ exports.resolveRef = resolveRef;
12
+ exports.commitDate = commitDate;
13
+ exports.isDirty = isDirty;
14
+ exports.worktreeAdd = worktreeAdd;
15
+ exports.worktreeRemove = worktreeRemove;
16
+ exports.worktreePrune = worktreePrune;
17
+ const node_child_process_1 = require("node:child_process");
18
+ const node_fs_1 = __importDefault(require("node:fs"));
19
+ const node_path_1 = __importDefault(require("node:path"));
20
+ const paths_1 = require("./paths");
21
+ /**
22
+ * 向主仓库 .git/info/exclude 写入 bin/cache 排除规则(对所有 worktree 生效)。
23
+ * 必要性:bin/cache 是 symlink 时,仓库自带 .gitignore 的目录规则(bin/cache/)匹配不到它,
24
+ * git status 会误报未跟踪文件。
25
+ */
26
+ function ensureGitExclude() {
27
+ const exclude = node_path_1.default.join((0, paths_1.repoDir)(), '.git', 'info', 'exclude');
28
+ let content = '';
29
+ try {
30
+ content = node_fs_1.default.readFileSync(exclude, 'utf8');
31
+ }
32
+ catch {
33
+ content = '';
34
+ }
35
+ if (!content.split('\n').some((l) => l.trim() === 'bin/cache')) {
36
+ const prefix = content && !content.endsWith('\n') ? '\n' : '';
37
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(exclude), { recursive: true });
38
+ node_fs_1.default.appendFileSync(exclude, `${prefix}\n# flvm: engine cache link\nbin/cache\n`);
39
+ }
40
+ }
41
+ /** 执行 git 命令,失败抛出含 stderr 的 Error */
42
+ function git(args, opts = {}) {
43
+ const r = (0, node_child_process_1.spawnSync)('git', args, {
44
+ cwd: opts.cwd,
45
+ encoding: 'utf8',
46
+ stdio: opts.inherit ? 'inherit' : ['ignore', 'pipe', 'pipe'],
47
+ maxBuffer: 1024 * 1024 * 64,
48
+ });
49
+ if (r.status !== 0) {
50
+ const err = r.stderr || String(r.error?.message || '');
51
+ throw new Error(`git ${args.join(' ')} 失败:\n${err.trim()}`);
52
+ }
53
+ return opts.inherit ? '' : String(r.stdout).trim();
54
+ }
55
+ /** 克隆主仓库(进度直接输出到终端) */
56
+ function gitClone(url, dest) {
57
+ const r = (0, node_child_process_1.spawnSync)('git', ['clone', url, dest], { stdio: 'inherit' });
58
+ if (r.status !== 0)
59
+ throw new Error(`克隆失败: ${url}`);
60
+ }
61
+ /** 拉取全部远程分支与 tag */
62
+ function fetchAll() {
63
+ git(['fetch', '--all', '--prune', '--tags'], { cwd: (0, paths_1.repoDir)(), inherit: true });
64
+ }
65
+ /** 列出 origin 的全部分支名(去 origin/ 前缀) */
66
+ function listRemoteBranches() {
67
+ const out = git(['for-each-ref', '--format=%(refname:short)', 'refs/remotes/origin'], {
68
+ cwd: (0, paths_1.repoDir)(),
69
+ });
70
+ return out
71
+ .split('\n')
72
+ .map((s) => s.trim())
73
+ .filter((s) => s && !s.startsWith('origin/HEAD'))
74
+ .map((s) => s.replace(/^origin\//, ''));
75
+ }
76
+ /** 解析 ref 为可用 spec + commit。优先 origin/<ref>,其次 ref 本身(tag/commit) */
77
+ function resolveRef(ref) {
78
+ const candidates = [`origin/${ref}`, ref];
79
+ for (const spec of candidates) {
80
+ try {
81
+ const commit = git(['rev-parse', '--verify', `${spec}^{commit}`], { cwd: (0, paths_1.repoDir)() });
82
+ if (commit)
83
+ return { spec, commit };
84
+ }
85
+ catch {
86
+ /* 尝试下一个 */
87
+ }
88
+ }
89
+ throw new Error(`找不到分支或提交: ${ref}(可先运行 flvm update 拉取最新)`);
90
+ }
91
+ /** commit 的提交日期(YYYY-MM-DD) */
92
+ function commitDate(commit) {
93
+ try {
94
+ return git(['show', '-s', '--format=%cs', commit], { cwd: (0, paths_1.repoDir)() });
95
+ }
96
+ catch {
97
+ return '?';
98
+ }
99
+ }
100
+ /** worktree 是否有未提交改动(含未跟踪文件;忽略 bin/cache 链接) */
101
+ function isDirty(dir) {
102
+ try {
103
+ const out = git(['-C', dir, 'status', '--porcelain']);
104
+ return out
105
+ .split('\n')
106
+ .some((l) => l.trim() && !l.trim().endsWith('bin/cache'));
107
+ }
108
+ catch {
109
+ return false;
110
+ }
111
+ }
112
+ function worktreeAdd(dir, spec) {
113
+ git(['worktree', 'add', '--detach', dir, spec], { cwd: (0, paths_1.repoDir)() });
114
+ }
115
+ function worktreeRemove(dir) {
116
+ git(['worktree', 'remove', '--force', dir], { cwd: (0, paths_1.repoDir)() });
117
+ }
118
+ function worktreePrune() {
119
+ try {
120
+ git(['worktree', 'prune'], { cwd: (0, paths_1.repoDir)() });
121
+ }
122
+ catch {
123
+ /* 无害 */
124
+ }
125
+ }
package/dist/paths.js ADDED
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.registryFile = exports.defaultFile = exports.binDir = exports.cachesDir = exports.worktreesDir = exports.repoDir = void 0;
7
+ exports.homeMarkerFile = homeMarkerFile;
8
+ exports.flvmHome = flvmHome;
9
+ exports.setFlvmHome = setFlvmHome;
10
+ exports.readRegistry = readRegistry;
11
+ exports.writeRegistry = writeRegistry;
12
+ exports.getDefault = getDefault;
13
+ exports.setDefault = setDefault;
14
+ const node_fs_1 = __importDefault(require("node:fs"));
15
+ const node_path_1 = __importDefault(require("node:path"));
16
+ const node_os_1 = __importDefault(require("node:os"));
17
+ /** 记录自定义存储目录的标记文件(固定在用户主目录,跨 shell / 跨平台持久) */
18
+ function homeMarkerFile() {
19
+ return node_path_1.default.join(node_os_1.default.homedir(), '.flvm-home');
20
+ }
21
+ /** 展开 ~ 并转为绝对路径 */
22
+ function resolveTilde(p) {
23
+ if (p === '~')
24
+ return node_os_1.default.homedir();
25
+ if (p.startsWith('~/') || p.startsWith('~\\')) {
26
+ return node_path_1.default.join(node_os_1.default.homedir(), p.slice(2));
27
+ }
28
+ return node_path_1.default.resolve(p);
29
+ }
30
+ /**
31
+ * flvm 根目录。优先级:
32
+ * 1. FLVM_HOME 环境变量(临时覆盖,便于测试)
33
+ * 2. flvm init <dir> 记录的目录(~/.flvm-home 标记文件)
34
+ * 3. 默认 ~/.flvm
35
+ */
36
+ function flvmHome() {
37
+ if (process.env.FLVM_HOME)
38
+ return resolveTilde(process.env.FLVM_HOME);
39
+ try {
40
+ const v = node_fs_1.default.readFileSync(homeMarkerFile(), 'utf8').trim();
41
+ if (v)
42
+ return resolveTilde(v);
43
+ }
44
+ catch {
45
+ /* 无标记文件,走默认 */
46
+ }
47
+ return node_path_1.default.join(node_os_1.default.homedir(), '.flvm');
48
+ }
49
+ /** 记录自定义存储目录(flvm init <dir>),返回解析后的绝对路径 */
50
+ function setFlvmHome(dir) {
51
+ const abs = resolveTilde(dir);
52
+ node_fs_1.default.mkdirSync(abs, { recursive: true });
53
+ node_fs_1.default.writeFileSync(homeMarkerFile(), abs + '\n');
54
+ return abs;
55
+ }
56
+ const repoDir = () => node_path_1.default.join(flvmHome(), 'repo');
57
+ exports.repoDir = repoDir;
58
+ const worktreesDir = () => node_path_1.default.join(flvmHome(), 'worktrees');
59
+ exports.worktreesDir = worktreesDir;
60
+ const cachesDir = () => node_path_1.default.join(flvmHome(), 'caches');
61
+ exports.cachesDir = cachesDir;
62
+ const binDir = () => node_path_1.default.join(flvmHome(), 'bin');
63
+ exports.binDir = binDir;
64
+ const defaultFile = () => node_path_1.default.join(flvmHome(), 'default');
65
+ exports.defaultFile = defaultFile;
66
+ const registryFile = () => node_path_1.default.join(flvmHome(), 'registry.json');
67
+ exports.registryFile = registryFile;
68
+ function readRegistry() {
69
+ try {
70
+ return JSON.parse(node_fs_1.default.readFileSync((0, exports.registryFile)(), 'utf8'));
71
+ }
72
+ catch {
73
+ return {};
74
+ }
75
+ }
76
+ function writeRegistry(reg) {
77
+ node_fs_1.default.mkdirSync(flvmHome(), { recursive: true });
78
+ node_fs_1.default.writeFileSync((0, exports.registryFile)(), JSON.stringify(reg, null, 2) + '\n');
79
+ }
80
+ /** 当前默认版本名,未设置返回 null */
81
+ function getDefault() {
82
+ try {
83
+ const v = node_fs_1.default.readFileSync((0, exports.defaultFile)(), 'utf8').trim();
84
+ return v || null;
85
+ }
86
+ catch {
87
+ return null;
88
+ }
89
+ }
90
+ function setDefault(name) {
91
+ if (name === null) {
92
+ try {
93
+ node_fs_1.default.unlinkSync((0, exports.defaultFile)());
94
+ }
95
+ catch {
96
+ /* 不存在即可 */
97
+ }
98
+ }
99
+ else {
100
+ node_fs_1.default.writeFileSync((0, exports.defaultFile)(), name);
101
+ }
102
+ }
package/dist/shim.js ADDED
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.installShims = installShims;
7
+ exports.setupShellIntegration = setupShellIntegration;
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const node_os_1 = __importDefault(require("node:os"));
11
+ const paths_1 = require("./paths");
12
+ const util_1 = require("./util");
13
+ const MARKER = '>>> flvm >>>';
14
+ /** sh 版代理脚本(macOS / Linux) */
15
+ function shShim(tool) {
16
+ const home = (0, paths_1.flvmHome)();
17
+ return `#!/bin/sh
18
+ FLVM_HOME="\${FLVM_HOME:-}"
19
+ if [ -z "$FLVM_HOME" ] && [ -f "$HOME/.flvm-home" ]; then
20
+ FLVM_HOME="$(cat "$HOME/.flvm-home")"
21
+ fi
22
+ if [ -z "$FLVM_HOME" ]; then
23
+ FLVM_HOME="${home}"
24
+ fi
25
+ NAME="$(cat "$FLVM_HOME/default" 2>/dev/null)"
26
+ if [ -z "$NAME" ] || [ ! -d "$FLVM_HOME/worktrees/$NAME" ]; then
27
+ echo "flvm: 尚未选择 Flutter 版本,请运行: flvm use <版本>" >&2
28
+ exit 1
29
+ fi
30
+ exec "$FLVM_HOME/worktrees/$NAME/bin/${tool}" "$@"
31
+ `;
32
+ }
33
+ /** cmd 版代理脚本(Windows,同时供 cmd / PowerShell 使用) */
34
+ function cmdShim(tool) {
35
+ const home = (0, paths_1.flvmHome)();
36
+ return `@echo off
37
+ setlocal
38
+ if "%FLVM_HOME%"=="" (
39
+ if exist "%USERPROFILE%\\.flvm-home" set /p FLVM_HOME=<"%USERPROFILE%\\.flvm-home"
40
+ )
41
+ if "%FLVM_HOME%"=="" set "FLVM_HOME=${home}"
42
+ set "NAME="
43
+ if exist "%FLVM_HOME%\\default" set /p NAME=<"%FLVM_HOME%\\default"
44
+ if "%NAME%"=="" goto :err
45
+ if not exist "%FLVM_HOME%\\worktrees\\%NAME%\\bin\\${tool}.bat" goto :err
46
+ call "%FLVM_HOME%\\worktrees\\%NAME%\\bin\\${tool}.bat" %*
47
+ exit /b %ERRORLEVEL%
48
+ :err
49
+ echo flvm: 尚未选择 Flutter 版本,请运行: flvm use ^<版本^>
50
+ exit /b 1
51
+ `;
52
+ }
53
+ /**
54
+ * 在 ~/.flvm/bin 下安装 flutter / dart 代理命令。
55
+ * 代理读取 ~/.flvm/default 决定转发目标,因此「切换版本 = 改一个文件」,秒级生效。
56
+ */
57
+ function installShims() {
58
+ node_fs_1.default.mkdirSync((0, paths_1.binDir)(), { recursive: true });
59
+ for (const tool of ['flutter', 'dart']) {
60
+ if (process.platform !== 'win32') {
61
+ const p = node_path_1.default.join((0, paths_1.binDir)(), tool);
62
+ node_fs_1.default.writeFileSync(p, shShim(tool), { mode: 0o755 });
63
+ }
64
+ else {
65
+ node_fs_1.default.writeFileSync(node_path_1.default.join((0, paths_1.binDir)(), `${tool}.cmd`), cmdShim(tool));
66
+ }
67
+ }
68
+ }
69
+ function shBlock(mirror) {
70
+ const home = (0, paths_1.flvmHome)();
71
+ const env = mirror
72
+ ? `\nexport PUB_HOSTED_URL="https://pub.flutter-io.cn"\nexport FLUTTER_STORAGE_BASE_URL="https://storage.flutter-io.cn"`
73
+ : '';
74
+ return `\n# ${MARKER}\nexport PATH="${home}/bin:$PATH"${env}\n# <<< flvm <<<\n`;
75
+ }
76
+ function psBlock(mirror) {
77
+ const home = (0, paths_1.flvmHome)();
78
+ const env = mirror
79
+ ? `\n$env:PUB_HOSTED_URL = "https://pub.flutter-io.cn"\n$env:FLUTTER_STORAGE_BASE_URL = "https://storage.flutter-io.cn"`
80
+ : '';
81
+ return `\n# ${MARKER}\n$env:PATH = "${home}\\bin;$env:PATH"${env}\n# <<< flvm <<<\n`;
82
+ }
83
+ function appendBlock(file, block) {
84
+ let content = '';
85
+ try {
86
+ content = node_fs_1.default.readFileSync(file, 'utf8');
87
+ }
88
+ catch {
89
+ content = '';
90
+ }
91
+ if (content.includes(MARKER))
92
+ return false;
93
+ const prefix = content && !content.endsWith('\n') ? '\n' : '';
94
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(file), { recursive: true });
95
+ node_fs_1.default.appendFileSync(file, prefix + block);
96
+ return true;
97
+ }
98
+ /**
99
+ * 把 ~/.flvm/bin 写入 shell 配置(幂等,带标记块)。
100
+ * 返回被修改的配置文件路径列表。
101
+ */
102
+ function setupShellIntegration(mirror) {
103
+ const written = [];
104
+ if (process.platform === 'win32') {
105
+ const docs = process.env.OneDrive
106
+ ? node_path_1.default.join(process.env.OneDrive, 'Documents')
107
+ : node_path_1.default.join(node_os_1.default.homedir(), 'Documents');
108
+ const ps7 = node_path_1.default.join(docs, 'PowerShell', 'Microsoft.PowerShell_profile.ps1');
109
+ const ps5 = node_path_1.default.join(docs, 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1');
110
+ const target = (0, util_1.exists)(ps7) ? ps7 : (0, util_1.exists)(ps5) ? ps5 : ps7;
111
+ if (appendBlock(target, psBlock(mirror)))
112
+ written.push(target);
113
+ }
114
+ else {
115
+ const shell = process.env.SHELL || '';
116
+ const rc = shell.includes('zsh') || process.platform === 'darwin'
117
+ ? node_path_1.default.join(node_os_1.default.homedir(), '.zshrc')
118
+ : node_path_1.default.join(node_os_1.default.homedir(), '.bashrc');
119
+ if (appendBlock(rc, shBlock(mirror)))
120
+ written.push(rc);
121
+ }
122
+ return written;
123
+ }
package/dist/util.js ADDED
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.exists = exists;
7
+ exports.ensureInit = ensureInit;
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const picocolors_1 = __importDefault(require("picocolors"));
10
+ const paths_1 = require("./paths");
11
+ /** lstat 判断存在(对 dangling symlink 也返回 true) */
12
+ function exists(p) {
13
+ try {
14
+ node_fs_1.default.lstatSync(p);
15
+ return true;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ /** 确保已执行 flvm init,否则引导用户 */
22
+ function ensureInit() {
23
+ if (!exists((0, paths_1.repoDir)())) {
24
+ console.error(picocolors_1.default.red('✗ flvm 尚未初始化,请先运行: flvm init'));
25
+ process.exit(1);
26
+ }
27
+ }
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.nameForRef = nameForRef;
7
+ exports.installWorktree = installWorktree;
8
+ exports.findInstalled = findInstalled;
9
+ exports.printCandidates = printCandidates;
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const paths_1 = require("./paths");
12
+ const git_1 = require("./git");
13
+ const cache_1 = require("./cache");
14
+ const util_1 = require("./util");
15
+ /** worktree 目录名:优先取分支名中的 x.y.z 版本号,取不到则清洗分支名 */
16
+ function nameForRef(ref) {
17
+ const m = ref.match(/(\d+\.\d+\.\d+)/);
18
+ return m ? m[1] : ref.replace(/[/\\]/g, '-');
19
+ }
20
+ /**
21
+ * 创建 worktree 并挂共享缓存,登记到注册表。
22
+ * 返回版本名。
23
+ */
24
+ function installWorktree(name, ref) {
25
+ const dir = node_path_1.default.join((0, paths_1.worktreesDir)(), name);
26
+ if ((0, util_1.exists)(dir)) {
27
+ throw new Error(`目录已存在: ${dir},如需重建请先 flvm remove ${name}`);
28
+ }
29
+ (0, git_1.worktreePrune)();
30
+ (0, git_1.ensureGitExclude)();
31
+ const { spec, commit } = (0, git_1.resolveRef)(ref);
32
+ (0, git_1.worktreeAdd)(dir, spec);
33
+ const group = (0, cache_1.cacheGroupFor)(ref);
34
+ (0, cache_1.linkCache)(dir, group);
35
+ const now = new Date().toISOString();
36
+ const entry = { ref, commit, cacheGroup: group, installedAt: now, updatedAt: now };
37
+ const reg = (0, paths_1.readRegistry)();
38
+ reg[name] = entry;
39
+ (0, paths_1.writeRegistry)(reg);
40
+ return entry;
41
+ }
42
+ /** 在已安装版本中查找:精确名 → 名字/分支名子串模糊匹配 */
43
+ function findInstalled(query) {
44
+ const reg = (0, paths_1.readRegistry)();
45
+ if (reg[query])
46
+ return { kind: 'ok', name: query, entry: reg[query] };
47
+ const hits = Object.entries(reg).filter(([n, e]) => n.includes(query) || e.ref.includes(query));
48
+ if (hits.length === 1)
49
+ return { kind: 'ok', name: hits[0][0], entry: hits[0][1] };
50
+ if (hits.length > 1)
51
+ return { kind: 'ambiguous', candidates: hits };
52
+ return { kind: 'none' };
53
+ }
54
+ function printCandidates(title, items) {
55
+ console.log(title);
56
+ for (const [n, e] of items) {
57
+ console.log(` ${n.padEnd(12)} ${e.ref}`);
58
+ }
59
+ console.log('请使用更精确的名称重试');
60
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "flxvm",
3
+ "version": "0.1.0",
4
+ "description": "Flutter multi-version manager based on git worktree with shared engine cache (OHOS friendly)",
5
+ "bin": {
6
+ "flvm": "dist/cli.js"
7
+ },
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "dev": "tsc -w",
11
+ "test": "npm run build && bash test/smoke.sh",
12
+ "prepublishOnly": "npm run build && npm test"
13
+ },
14
+ "keywords": [
15
+ "flutter",
16
+ "version",
17
+ "manager",
18
+ "worktree",
19
+ "ohos",
20
+ "harmonyos",
21
+ "fvm-alternative"
22
+ ],
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "license": "MIT",
27
+ "files": [
28
+ "dist",
29
+ "README.md",
30
+ "LICENSE"
31
+ ],
32
+ "dependencies": {
33
+ "commander": "^12.1.0",
34
+ "picocolors": "^1.0.1"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^20.14.0",
38
+ "typescript": "^5.5.0"
39
+ }
40
+ }