glasspane-install 1.22.0 → 1.23.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/cli.js +51 -17
- package/glasspane.js +9 -3
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -70,7 +70,7 @@ export const INSTALL_SH_URL =
|
|
|
70
70
|
/** 发布线锚点(版本真源 = 根 package.json,由 scripts/set-version.mjs 统一改写,
|
|
71
71
|
* 勿手改):npx 形态下本地没有仓库时,引导 clone 的就是这个 tag,与 install.sh
|
|
72
72
|
* 的 `GLASSPANE_RELEASE` 同值——两条一键入口必须拿到同一份源码。 */
|
|
73
|
-
export const RELEASE_VERSION = '1.
|
|
73
|
+
export const RELEASE_VERSION = '1.23.0'
|
|
74
74
|
/** 发布 ref(tag 名)。GLASSPANE_REF 环境变量可覆盖(追主干用 `main`)。 */
|
|
75
75
|
export const REPO_REF = `v${RELEASE_VERSION}`
|
|
76
76
|
|
|
@@ -2353,7 +2353,15 @@ export function bootstrapPlan({
|
|
|
2353
2353
|
* · 干净但不是那个 tag ⇒ 前进(fetch + detached checkout —— 这是移动**产品自己的 clone**,
|
|
2354
2354
|
* 不是移动任何人的工作树);
|
|
2355
2355
|
* · 脏 ⇒ 拒绝,点名前 10 个挡路的路径,并给一条不销毁任何东西的出路(换一个空目录);
|
|
2356
|
-
* ·
|
|
2356
|
+
* · HEAD 读不出 ⇒ 拒绝。"判不出来"永远不许变成"那就照用/那就切过去"。
|
|
2357
|
+
*
|
|
2358
|
+
* **"该 tag 本地读不出"不是一条拒绝**,2026-10-27 在本机被实测推翻的正是这一格。旧写法把它和
|
|
2359
|
+
* "HEAD 读不出"并成同一句"判不出来就拒",于是 `~/glasspane`(v1.5.0 的 clone,本地 tag 只到
|
|
2360
|
+
* v1.2.0)在 npm 那条入口上**永远**走到 refuse:`bootstrapPlan` 先问判据、判据先拒,而那一步
|
|
2361
|
+
* 之后**会 fetch 这个 tag 的代码(`advanceExistingClone`)根本轮不到执行**。`install.sh` 那一支
|
|
2362
|
+
* 是对的(先 fetch 再判),两条一键入口对同一棵树给出两种答案——finding 10 那族的老形状。
|
|
2363
|
+
* 现在 tag 缺席归到"前进"里:fetch 不成仍由 `advanceExistingClone` 拒,并交出 git 的原话;
|
|
2364
|
+
* HEAD 读不出照旧是拒。前者是能补的事实缺口,后者是判据本身没了。
|
|
2357
2365
|
*/
|
|
2358
2366
|
export function existingCloneDecision({ targetDir, ref, head = null, tagCommit = null, blocking = [] }) {
|
|
2359
2367
|
const dirty = blocking.map((line) => String(line).trim()).filter((line) => line !== '')
|
|
@@ -2372,19 +2380,31 @@ export function existingCloneDecision({ targetDir, ref, head = null, tagCommit =
|
|
|
2372
2380
|
+ '安装器不替你决定这些文件去哪。',
|
|
2373
2381
|
}
|
|
2374
2382
|
}
|
|
2375
|
-
if (!head
|
|
2383
|
+
if (!head) {
|
|
2376
2384
|
return {
|
|
2377
2385
|
action: 'refuse',
|
|
2378
2386
|
blocking: [],
|
|
2379
|
-
text: `${targetDir} 读不出 HEAD
|
|
2380
|
-
+ '
|
|
2381
|
-
+ `\n
|
|
2382
|
-
+ 'GLASSPANE_INSTALL_DIR=<空目录> 重跑一键安装。',
|
|
2387
|
+
text: `${targetDir} 读不出 HEAD(不是 git 仓库、那目录没有一次提交,或 git 本身答不了):`
|
|
2388
|
+
+ '连"这棵树在哪"都判不出来,就不动它,也不装它。'
|
|
2389
|
+
+ `\n下一步:确认 ${targetDir} 是一棵 git 仓库(git -C "${targetDir}" rev-parse HEAD 能不能答话);`
|
|
2390
|
+
+ '要一份全新的 clone 就 GLASSPANE_INSTALL_DIR=<空目录> 重跑一键安装。',
|
|
2383
2391
|
}
|
|
2384
2392
|
}
|
|
2385
|
-
if (head === tagCommit) {
|
|
2393
|
+
if (tagCommit && head === tagCommit) {
|
|
2386
2394
|
return { action: 'use', blocking: [], head, text: `${targetDir} 已经在 ${ref}(${String(head).slice(0, 7)}),直接用它构建。` }
|
|
2387
2395
|
}
|
|
2396
|
+
if (!tagCommit) {
|
|
2397
|
+
return {
|
|
2398
|
+
action: 'advance',
|
|
2399
|
+
blocking: [],
|
|
2400
|
+
head,
|
|
2401
|
+
tagCommit: null,
|
|
2402
|
+
needsFetch: true,
|
|
2403
|
+
text: `${targetDir} 是干净的 clone(HEAD 在 ${String(head).slice(0, 7)}),但本地没有 ${ref} 这个 tag`
|
|
2404
|
+
+ '(旧 clone 的 tag 表停在更早的一版,浅历史也会这样):前进那一步先 fetch 它再切。'
|
|
2405
|
+
+ `fetch 不成就是拒——${targetDir} 原地不动,本轮不构建。`,
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2388
2408
|
return {
|
|
2389
2409
|
action: 'advance',
|
|
2390
2410
|
blocking: [],
|
|
@@ -2396,9 +2416,15 @@ export function existingCloneDecision({ targetDir, ref, head = null, tagCommit =
|
|
|
2396
2416
|
}
|
|
2397
2417
|
|
|
2398
2418
|
/**
|
|
2399
|
-
* 一棵既有 clone
|
|
2400
|
-
* `git status --porcelain`
|
|
2401
|
-
*
|
|
2419
|
+
* 一棵既有 clone 的四份事实(IO 读,不判定):HEAD 指向的 commit、`ref^{commit}`、
|
|
2420
|
+
* `git status --porcelain` 的行,以及这份 clone 是不是浅仓。判据一律在 `existingCloneDecision`,
|
|
2421
|
+
* 两处调用(bootstrap 的纯计划、前进这一步的复判)共用同一份读数,避免出现第二个"什么算脏"的说法。
|
|
2422
|
+
*
|
|
2423
|
+
* `shallow` 是给 fetch 挑形状的:`--depth 1` 会把一棵**完整**的 clone 变成浅仓(`.git/shallow`
|
|
2424
|
+
* 一旦落下,之后的遍历都撞上人为边界,要恢复得 `git fetch --unshallow`)。旧写法对任何树都带
|
|
2425
|
+
* `--depth 1`,而注释写的是"只往 .git 里加对象"——那句话对完整 clone 不成立,它加的是一个边界。
|
|
2426
|
+
* 只有 git 亲口答 `true` 才用 `--depth 1`;答不出来(老 git、不是仓库)按完整仓对待,宁可多取
|
|
2427
|
+
* 一点也不给人家的历史加边界。
|
|
2402
2428
|
*/
|
|
2403
2429
|
export function readCloneFacts({ targetDir, ref, spawn = spawnSync } = {}) {
|
|
2404
2430
|
const read = (args) => spawn('git', ['-C', targetDir, ...args], { encoding: 'utf8' })
|
|
@@ -2407,34 +2433,42 @@ export function readCloneFacts({ targetDir, ref, spawn = spawnSync } = {}) {
|
|
|
2407
2433
|
return res.status === 0 ? String(res.stdout ?? '').trim() : null
|
|
2408
2434
|
}
|
|
2409
2435
|
const status = read(['status', '--porcelain'])
|
|
2436
|
+
const shallow = read(['rev-parse', '--is-shallow-repository'])
|
|
2410
2437
|
return {
|
|
2411
2438
|
head: sha('HEAD'),
|
|
2412
2439
|
tagCommit: sha(`${ref}^{commit}`),
|
|
2413
2440
|
// `git status` 答非零时这里得到空数组,读起来是"干净"——那棵树随后会因为 `head` 读不出而被拒,
|
|
2414
2441
|
// 所以"判不出来"走的是拒绝那一支,不是"那就切过去"。
|
|
2415
2442
|
blocking: status.status === 0 ? String(status.stdout ?? '').split('\n').filter((line) => line !== '') : [],
|
|
2443
|
+
shallow: shallow.status === 0 ? String(shallow.stdout ?? '').trim() === 'true' : null,
|
|
2416
2444
|
}
|
|
2417
2445
|
}
|
|
2418
2446
|
|
|
2419
2447
|
/**
|
|
2420
2448
|
* 把既有 clone 前进到钉住的 ref(IO 步;判据全在 `existingCloneDecision` 里)。
|
|
2421
2449
|
*
|
|
2422
|
-
*
|
|
2423
|
-
*
|
|
2424
|
-
* `
|
|
2450
|
+
* 只有这几种 git 动作:`status --porcelain`、两次 `rev-parse --verify`、`rev-parse
|
|
2451
|
+
* --is-shallow-repository`(全是读),`fetch --tags [origin <ref>]`(只往 .git 里加对象;
|
|
2452
|
+
* 带不带 `--depth 1` 由 git 自己答的那句"是不是浅仓"决定,见 `readCloneFacts`),以及
|
|
2453
|
+
* `checkout --detach <ref>`(只在树干净时执行)。没有 `-f`、没有 `stash`、没有 `clean`、
|
|
2454
|
+
* 没有 `reset --hard`:一个 installer 不该有能把别人的未提交工作抹掉的那一格。
|
|
2425
2455
|
*/
|
|
2426
2456
|
export function advanceExistingClone({ repoDir, ref, spawn = spawnSync } = {}) {
|
|
2427
2457
|
const read = (args) => spawn('git', ['-C', repoDir, ...args], { encoding: 'utf8' })
|
|
2428
2458
|
let facts = readCloneFacts({ targetDir: repoDir, ref, spawn })
|
|
2429
2459
|
if (facts.blocking.length === 0 && facts.head && !facts.tagCommit) {
|
|
2430
|
-
// tag
|
|
2431
|
-
const
|
|
2460
|
+
// tag 不在这份历史里(旧 clone / 浅 clone 都会这样):先把它取回来,再判。fetch 失败不改任何东西。
|
|
2461
|
+
const fetchArgs = facts.shallow === true
|
|
2462
|
+
? ['fetch', '--tags', '--depth', '1', 'origin', ref]
|
|
2463
|
+
: ['fetch', '--tags', 'origin', ref]
|
|
2464
|
+
const fetched = read(fetchArgs)
|
|
2432
2465
|
if (fetched.status !== 0) {
|
|
2433
2466
|
return {
|
|
2434
2467
|
action: 'refuse',
|
|
2435
2468
|
blocking: [],
|
|
2469
|
+
fetchArgs,
|
|
2436
2470
|
text: `${repoDir} 里没有 ${ref} 这个 tag,而 `
|
|
2437
|
-
+ `git -C "${repoDir}"
|
|
2471
|
+
+ `git -C "${repoDir}" ${fetchArgs.join(' ')} 退出 ${String(fetched.status ?? 'null')}:`
|
|
2438
2472
|
+ `${String(fetched.stderr ?? '').trim().split('\n')[0] || '(git 没有留下 stderr)'}。\n`
|
|
2439
2473
|
+ '没有 clone、没有切分支、没有写入任何东西。下一步:确认网络与该 tag 存在('
|
|
2440
2474
|
+ `git ls-remote --tags origin ${ref}),或 GLASSPANE_INSTALL_DIR=<空目录> 换一处全新的 clone。`,
|
package/glasspane.js
CHANGED
|
@@ -32,6 +32,8 @@ import process from 'node:process'
|
|
|
32
32
|
import { spawnSync } from 'node:child_process'
|
|
33
33
|
|
|
34
34
|
export const VERBS = Object.freeze(['update', 'doctor', 'status', 'help'])
|
|
35
|
+
/** 用法里的同义写法:读不出"同一条命令的两种问法"就没有可比性,所以别名是一张明面上的表。 */
|
|
36
|
+
export const VERB_ALIASES = Object.freeze({ '-h': 'help', '--help': 'help' })
|
|
35
37
|
export const DAEMON_JOB_LABEL = 'com.glasspane.daemon'
|
|
36
38
|
export const NPM_PACKAGES = Object.freeze(['glasspane-mcp', 'glasspane-install'])
|
|
37
39
|
/** The three answers a read may give. `unverified` is the one that used to be missing everywhere. */
|
|
@@ -268,7 +270,7 @@ export function usageText() {
|
|
|
268
270
|
' update 问一次有没有新版本,有就装(走这台机器已经装着的那个更新器)',
|
|
269
271
|
' status 把更新器自己的那一行结论原样打出来',
|
|
270
272
|
' doctor 读这台机器的安装状态(安装指针、更新器、daemon 作业、socket、全局 npm 包)——只读,不写',
|
|
271
|
-
' help
|
|
273
|
+
' help 这一页(`-h`、`--help` 同义)',
|
|
272
274
|
'',
|
|
273
275
|
'退出码:0 = 成/都在位;2 = 用法错;3 = 被拒或有一格不在位(doctor 用它表示"有东西要看上面那几行");',
|
|
274
276
|
'4/5 来自 update(换版后回退 / 回退也没成),与更新器本身的口径一致。',
|
|
@@ -279,8 +281,12 @@ export function usageText() {
|
|
|
279
281
|
export function parseVerb(argv) {
|
|
280
282
|
const first = String(argv?.[0] ?? '')
|
|
281
283
|
if (argv.length === 0) return { verb: null, error: '没给命令' }
|
|
282
|
-
|
|
283
|
-
|
|
284
|
+
// `-h` / `--help` 与 `help` 同义:这条命令是本产品给 agent 的入口,而 agent 的第一反应就是
|
|
285
|
+
// `--help`。2026-10-27 用发布物实测:`/usr/local/bin/glasspane --help` 回"不认得命令「--help」"
|
|
286
|
+
// 且退出 2——同产品的 `installer/cli.js` 认 `--help`,两条入口对同一种问法给两种答案。
|
|
287
|
+
const verb = VERB_ALIASES[first] ?? first
|
|
288
|
+
if (!VERBS.includes(verb)) return { verb: null, error: `不认得命令「${first}」` }
|
|
289
|
+
return { verb, error: null, rest: argv.slice(1) }
|
|
284
290
|
}
|
|
285
291
|
|
|
286
292
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "glasspane-install",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.0",
|
|
4
4
|
"description": "GlassPane one-command installer: terminal onboarding → install deps → build TS + Swift engine → start daemon → open settings GUI for permission guide",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|