umadev 1.0.6 → 1.0.8
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/bin/cli.js +194 -22
- package/bin/postinstall.js +67 -0
- package/package.json +10 -8
package/bin/cli.js
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
const { spawnSync } = require('node:child_process');
|
|
17
17
|
const fs = require('node:fs');
|
|
18
18
|
const path = require('node:path');
|
|
19
|
+
const https = require('node:https');
|
|
20
|
+
const os = require('node:os');
|
|
19
21
|
|
|
20
22
|
// Node platform/arch → our sub-package name.
|
|
21
23
|
const PLATFORM_PACKAGES = {
|
|
@@ -105,32 +107,202 @@ function findKnowledgeDir() {
|
|
|
105
107
|
return null;
|
|
106
108
|
}
|
|
107
109
|
|
|
108
|
-
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
110
|
+
// ── Local embedding model — ensure it's on disk, else download it (with a
|
|
111
|
+
// progress bar) from THIS version's GitHub Release. Checked on EVERY launch
|
|
112
|
+
// (a cheap stat); the ~224MB fp16 model is too large for npm, so it's a
|
|
113
|
+
// one-time fetch into ~/.umadev/embed-model. Fail-open: any failure launches
|
|
114
|
+
// anyway and the binary degrades to BM25 lexical retrieval, retrying next time.
|
|
115
|
+
function homeDir() {
|
|
116
|
+
return process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
115
117
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (modelDir && !process.env.UMADEV_EMBED_MODEL_DIR) {
|
|
119
|
-
extraEnv.UMADEV_EMBED_MODEL_DIR = modelDir;
|
|
118
|
+
function modelTargetDir() {
|
|
119
|
+
return path.join(homeDir(), '.umadev', 'embed-model');
|
|
120
120
|
}
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
const MODEL_FILES = ['config.json', 'tokenizer.json', 'model.safetensors'];
|
|
122
|
+
function modelPresent(dir) {
|
|
123
|
+
return MODEL_FILES.every((f) => {
|
|
124
|
+
try {
|
|
125
|
+
return fs.statSync(path.join(dir, f)).size > 0;
|
|
126
|
+
} catch (_) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
});
|
|
124
130
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
131
|
+
// Download one URL to `dest`, following redirects (GitHub → CDN), drawing a
|
|
132
|
+
// progress bar when `withBar`. Resolves on success, rejects on any error.
|
|
133
|
+
function downloadTo(url, dest, withBar, label) {
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
const req = https.get(
|
|
136
|
+
url,
|
|
137
|
+
{ headers: { 'User-Agent': 'umadev-cli', Accept: 'application/octet-stream' } },
|
|
138
|
+
(res) => {
|
|
139
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
140
|
+
res.resume();
|
|
141
|
+
downloadTo(res.headers.location, dest, withBar, label).then(resolve, reject);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (res.statusCode !== 200) {
|
|
145
|
+
res.resume();
|
|
146
|
+
reject(new Error('HTTP ' + res.statusCode));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const total = parseInt(res.headers['content-length'] || '0', 10);
|
|
150
|
+
let got = 0;
|
|
151
|
+
let lastPct = -1;
|
|
152
|
+
const tmp = dest + '.part';
|
|
153
|
+
const out = fs.createWriteStream(tmp);
|
|
154
|
+
res.on('data', (chunk) => {
|
|
155
|
+
got += chunk.length;
|
|
156
|
+
if (withBar && total > 0) {
|
|
157
|
+
const pct = Math.floor((got / total) * 100);
|
|
158
|
+
if (pct !== lastPct) {
|
|
159
|
+
lastPct = pct;
|
|
160
|
+
const w = 24;
|
|
161
|
+
const fill = Math.round((pct / 100) * w);
|
|
162
|
+
const bar = '#'.repeat(fill) + '-'.repeat(w - fill);
|
|
163
|
+
const mb = (got / 1048576).toFixed(0);
|
|
164
|
+
const tot = (total / 1048576).toFixed(0);
|
|
165
|
+
process.stderr.write(
|
|
166
|
+
'\r ' + label + ' [' + bar + '] ' + pct + '% (' + mb + '/' + tot + ' MB)',
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
res.pipe(out);
|
|
172
|
+
out.on('finish', () =>
|
|
173
|
+
out.close((e) => {
|
|
174
|
+
if (e) return reject(e);
|
|
175
|
+
try {
|
|
176
|
+
fs.renameSync(tmp, dest);
|
|
177
|
+
} catch (er) {
|
|
178
|
+
return reject(er);
|
|
179
|
+
}
|
|
180
|
+
if (withBar) process.stderr.write('\n');
|
|
181
|
+
resolve();
|
|
182
|
+
}),
|
|
183
|
+
);
|
|
184
|
+
out.on('error', reject);
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
req.on('error', reject);
|
|
188
|
+
req.setTimeout(120000, () => req.destroy(new Error('timeout')));
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
// Ordered list of base URLs to try for the release assets. An explicit override
|
|
192
|
+
// (UMADEV_MODEL_BASE_URL) wins; otherwise zh-CN / China-timezone users get GitHub
|
|
193
|
+
// PROXY MIRRORS first (github.com's release CDN is frequently slow or blocked in
|
|
194
|
+
// mainland China), and everyone else gets github.com first — with the others as
|
|
195
|
+
// fallback either way, so a blocked github.com or a down mirror still recovers.
|
|
196
|
+
function releaseBases(version) {
|
|
197
|
+
if (process.env.UMADEV_MODEL_BASE_URL) {
|
|
198
|
+
return [process.env.UMADEV_MODEL_BASE_URL.replace(/\/+$/, '')];
|
|
199
|
+
}
|
|
200
|
+
const gh = 'https://github.com/umacloud/umadev/releases/download/v' + version;
|
|
201
|
+
const mirrors = [
|
|
202
|
+
'https://ghproxy.net/' + gh,
|
|
203
|
+
'https://ghfast.top/' + gh,
|
|
204
|
+
'https://gh-proxy.com/' + gh,
|
|
205
|
+
];
|
|
206
|
+
let cn = false;
|
|
207
|
+
try {
|
|
208
|
+
const opts = Intl.DateTimeFormat().resolvedOptions();
|
|
209
|
+
const tz = opts.timeZone || '';
|
|
210
|
+
const loc = (process.env.LANG || process.env.LC_ALL || '') + ' ' + (opts.locale || '');
|
|
211
|
+
cn =
|
|
212
|
+
/Shanghai|Chongqing|Urumqi|Harbin|Hong_Kong|Macau/.test(tz) ||
|
|
213
|
+
/zh[_-]?(CN|Hans)/i.test(loc);
|
|
214
|
+
} catch (_) {
|
|
215
|
+
/* default to direct-first */
|
|
216
|
+
}
|
|
217
|
+
return cn ? [...mirrors, gh] : [gh, ...mirrors];
|
|
218
|
+
}
|
|
219
|
+
// Try each base for `name` in order; resolve on first success, throw the last
|
|
220
|
+
// error if all fail. A China mirror can cover a blocked github.com (or vice
|
|
221
|
+
// versa) with zero user configuration.
|
|
222
|
+
async function downloadFile(bases, name, dest, withBar, label) {
|
|
223
|
+
let lastErr;
|
|
224
|
+
for (const base of bases) {
|
|
225
|
+
try {
|
|
226
|
+
await downloadTo(base + '/' + name, dest, withBar, label);
|
|
227
|
+
return;
|
|
228
|
+
} catch (e) {
|
|
229
|
+
lastErr = e;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
throw lastErr || new Error('no source reachable');
|
|
233
|
+
}
|
|
234
|
+
async function ensureModel() {
|
|
235
|
+
const dir = modelTargetDir();
|
|
236
|
+
if (modelPresent(dir)) return dir; // already installed — fast path, no network
|
|
237
|
+
let version = '0.0.0';
|
|
238
|
+
try {
|
|
239
|
+
version = require('../package.json').version;
|
|
240
|
+
} catch (_) {
|
|
241
|
+
/* keep default */
|
|
242
|
+
}
|
|
243
|
+
const bases = releaseBases(version);
|
|
244
|
+
try {
|
|
245
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
246
|
+
process.stderr.write(
|
|
247
|
+
'\n 本地向量检索模型缺失,正在下载 (multilingual-e5-small · fp16 · ~224MB)…\n',
|
|
248
|
+
);
|
|
249
|
+
process.stderr.write(
|
|
250
|
+
' 一次性下载;之后完全本地、运行时无需联网。失败不影响使用(降级为 BM25)。\n',
|
|
251
|
+
);
|
|
252
|
+
await downloadFile(bases, 'config.json', path.join(dir, 'config.json'), false, '');
|
|
253
|
+
await downloadFile(bases, 'tokenizer.json', path.join(dir, 'tokenizer.json'), false, '');
|
|
254
|
+
await downloadFile(
|
|
255
|
+
bases,
|
|
256
|
+
'model.safetensors',
|
|
257
|
+
path.join(dir, 'model.safetensors'),
|
|
258
|
+
true,
|
|
259
|
+
'下载向量模型',
|
|
260
|
+
);
|
|
261
|
+
process.stderr.write(' 本地向量模型就绪 ✓\n\n');
|
|
262
|
+
return dir;
|
|
263
|
+
} catch (e) {
|
|
264
|
+
process.stderr.write(
|
|
265
|
+
'\n [提示] 向量模型下载未完成 (' +
|
|
266
|
+
e.message +
|
|
267
|
+
');本次用 BM25 检索,下次启动重试。\n\n',
|
|
268
|
+
);
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
128
271
|
}
|
|
129
|
-
const result = spawnSync(binary, process.argv.slice(2), spawnOpts);
|
|
130
272
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
273
|
+
async function main() {
|
|
274
|
+
const binary = findBinary();
|
|
275
|
+
// npm artifact round-trips (upload/download-artifact in CI) can strip the
|
|
276
|
+
// executable bit off the prebuilt binary; restore it defensively before exec.
|
|
277
|
+
try {
|
|
278
|
+
fs.chmodSync(binary, 0o755);
|
|
279
|
+
} catch (_) {
|
|
280
|
+
// read-only install dir or already +x — spawnSync below reports real errors
|
|
281
|
+
}
|
|
282
|
+
const extraEnv = {};
|
|
283
|
+
// Prefer a bundled npm model package (dev / sibling layout); otherwise fetch
|
|
284
|
+
// it on demand into ~/.umadev/embed-model (the binary's model_dir() fallback).
|
|
285
|
+
let modelDir = findModelDir();
|
|
286
|
+
if (!modelDir) modelDir = await ensureModel();
|
|
287
|
+
if (modelDir && !process.env.UMADEV_EMBED_MODEL_DIR) {
|
|
288
|
+
extraEnv.UMADEV_EMBED_MODEL_DIR = modelDir;
|
|
289
|
+
}
|
|
290
|
+
const knowledgeDir = findKnowledgeDir();
|
|
291
|
+
if (knowledgeDir && !process.env.UMADEV_KNOWLEDGE_DIR) {
|
|
292
|
+
extraEnv.UMADEV_KNOWLEDGE_DIR = knowledgeDir;
|
|
293
|
+
}
|
|
294
|
+
const spawnOpts = { stdio: 'inherit' };
|
|
295
|
+
if (Object.keys(extraEnv).length > 0) {
|
|
296
|
+
spawnOpts.env = { ...process.env, ...extraEnv };
|
|
297
|
+
}
|
|
298
|
+
const result = spawnSync(binary, process.argv.slice(2), spawnOpts);
|
|
299
|
+
|
|
300
|
+
if (result.error) {
|
|
301
|
+
console.error(`umadev: failed to exec binary: ${result.error.message}`);
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
process.exit(result.status === null ? 1 : result.status);
|
|
134
306
|
}
|
|
135
307
|
|
|
136
|
-
|
|
308
|
+
main();
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
//
|
|
4
|
+
// Post-install PATH check — GLOBAL installs only.
|
|
5
|
+
//
|
|
6
|
+
// Some node setups (notably Homebrew's node) leave npm's global bin dir OFF
|
|
7
|
+
// $PATH. The package installs fine and the `umadev` command is linked, but it
|
|
8
|
+
// lands in a directory the shell doesn't search, so the user later sees a bare
|
|
9
|
+
// "command not found" with no clue why. We cannot change the user's PATH from
|
|
10
|
+
// here, but we can detect the situation and print exactly what happened + how
|
|
11
|
+
// to fix it.
|
|
12
|
+
//
|
|
13
|
+
// FAIL-OPEN BY CONTRACT: every path through this script ends with exit code 0,
|
|
14
|
+
// and the whole body is wrapped in try/catch. A cosmetic PATH check must NEVER
|
|
15
|
+
// break `npm install` — if anything is uncertain, we stay silent.
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
function main() {
|
|
19
|
+
// Only a global install (`npm i -g`) puts a command on PATH. For a local /
|
|
20
|
+
// dependency install the command is not meant to be on PATH — say nothing.
|
|
21
|
+
if (process.env.npm_config_global !== 'true') return;
|
|
22
|
+
|
|
23
|
+
const path = require('node:path');
|
|
24
|
+
|
|
25
|
+
// The prefix npm links global commands under. If npm didn't expose it we
|
|
26
|
+
// can't reason about PATH — stay quiet rather than risk a false warning.
|
|
27
|
+
const prefix = process.env.npm_config_prefix;
|
|
28
|
+
if (!prefix) return;
|
|
29
|
+
|
|
30
|
+
const isWin = process.platform === 'win32';
|
|
31
|
+
// npm links global commands into <prefix>/bin on unix, <prefix> on Windows.
|
|
32
|
+
const binDir = isWin ? prefix : path.join(prefix, 'bin');
|
|
33
|
+
|
|
34
|
+
const sep = isWin ? ';' : ':';
|
|
35
|
+
const strip = (p) => p.replace(/[\\/]+$/, '');
|
|
36
|
+
const fold = (p) => (isWin ? strip(p).toLowerCase() : strip(p));
|
|
37
|
+
const target = fold(binDir);
|
|
38
|
+
const onPath = (process.env.PATH || '')
|
|
39
|
+
.split(sep)
|
|
40
|
+
.filter(Boolean)
|
|
41
|
+
.some((p) => fold(p) === target);
|
|
42
|
+
|
|
43
|
+
// All good — the command will be found. Don't add noise to a clean install.
|
|
44
|
+
if (onPath) return;
|
|
45
|
+
|
|
46
|
+
const w = (s) => process.stderr.write(s + '\n');
|
|
47
|
+
w('');
|
|
48
|
+
w(' [warn] umadev installed OK, but its command directory is NOT on your PATH:');
|
|
49
|
+
w(' ' + binDir);
|
|
50
|
+
w(' so running `umadev` will say "command not found".');
|
|
51
|
+
w(' This is your npm/shell setup (common with Homebrew node), not a umadev bug.');
|
|
52
|
+
w(' umadev 已装好,但命令目录不在 PATH 上,所以敲 umadev 会提示找不到。');
|
|
53
|
+
w('');
|
|
54
|
+
w(' Fix — point npm at a prefix already on your PATH, then reinstall:');
|
|
55
|
+
w(' npm config set prefix ~/.npm-global');
|
|
56
|
+
w(' # ensure ~/.npm-global/bin is on your PATH, then:');
|
|
57
|
+
w(' npm i -g umadev@latest');
|
|
58
|
+
w(' …or just add the directory above to your PATH:');
|
|
59
|
+
w(' export PATH="' + binDir + (isWin ? ';%PATH%"' : ':$PATH"'));
|
|
60
|
+
w('');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
main();
|
|
65
|
+
} catch (_e) {
|
|
66
|
+
// Never fail the install over a cosmetic PATH check.
|
|
67
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "umadev",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "A project-director Agent for AI coding hosts — drives your logged-in Claude Code / Codex through a 9-phase commercial delivery pipeline with governance. No API key needed.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -30,16 +30,18 @@
|
|
|
30
30
|
"bin/",
|
|
31
31
|
"README.md"
|
|
32
32
|
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"postinstall": "node bin/postinstall.js"
|
|
35
|
+
},
|
|
33
36
|
"engines": {
|
|
34
37
|
"node": ">=18"
|
|
35
38
|
},
|
|
36
39
|
"optionalDependencies": {
|
|
37
|
-
"@umacloud/cli-darwin-arm64": "1.0.
|
|
38
|
-
"@umacloud/cli-darwin-x64": "1.0.
|
|
39
|
-
"@umacloud/cli-linux-x64": "1.0.
|
|
40
|
-
"@umacloud/cli-linux-arm64": "1.0.
|
|
41
|
-
"@umacloud/cli-win32-x64": "1.0.
|
|
42
|
-
"@umacloud/
|
|
43
|
-
"@umacloud/knowledge": "1.0.6"
|
|
40
|
+
"@umacloud/cli-darwin-arm64": "1.0.8",
|
|
41
|
+
"@umacloud/cli-darwin-x64": "1.0.8",
|
|
42
|
+
"@umacloud/cli-linux-x64": "1.0.8",
|
|
43
|
+
"@umacloud/cli-linux-arm64": "1.0.8",
|
|
44
|
+
"@umacloud/cli-win32-x64": "1.0.8",
|
|
45
|
+
"@umacloud/knowledge": "1.0.8"
|
|
44
46
|
}
|
|
45
47
|
}
|